Delete re-export shims, import from @open-pencil/core directly

Remove 14 src/engine/ shims and entire src/kiwi/ directory.
Only src/engine/fonts.ts remains (has real Tauri font logic).
Replace isTauri() with IS_TAURI constant.
This commit is contained in:
Danila Poyarkov 2026-03-06 21:42:56 +03:00
parent 6ec8fc518b
commit 9d73297040
38 changed files with 67 additions and 585 deletions

View file

@ -1,8 +1,9 @@
import { valibotSchema } from '@ai-sdk/valibot'
import { ALL_TOOLS, FigmaAPI, toolsToAI } from '@open-pencil/core'
import { tool } from 'ai'
import * as v from 'valibot'
import { ALL_TOOLS, FigmaAPI, toolsToAI } from '@open-pencil/core'
import type { EditorStore } from '@/stores/editor'
import type { ExportFormat } from '@open-pencil/core'

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import ColorPicker from './ColorPicker.vue'
import { colorToHexRaw, parseColor } from '@/engine/color'
import { colorToHexRaw, parseColor } from '@open-pencil/core'
import type { Color } from '@/types'

View file

@ -14,7 +14,7 @@ import {
type Color as RekaColor
} from 'reka-ui'
import { colorToHex } from '@/engine/color'
import { colorToHex } from '@open-pencil/core'
import type { Color } from '@/types'

View file

@ -24,9 +24,9 @@ import IconType from '~icons/lucide/type'
import IconToggleLeft from '~icons/lucide/toggle-left'
import IconX from '~icons/lucide/x'
import ColorInput from './ColorInput.vue'
import { colorToHexRaw, parseColor } from '@/engine/color'
import { colorToHexRaw, parseColor } from '@open-pencil/core'
import { useEditorStore } from '@/stores/editor'
import type { Variable, Color } from '@/engine/scene-graph'
import type { Variable, Color } from '@open-pencil/core'
const open = defineModel<boolean>('open', { default: false })
const store = useEditorStore()

View file

@ -5,7 +5,7 @@ import ScrubInput from '@/components/ScrubInput.vue'
import { useNodeProps } from '@/composables/use-node-props'
import { MIXED, useMultiProps } from '@/composables/use-multi-props'
import type { SceneNode } from '@/engine/scene-graph'
import type { SceneNode } from '@open-pencil/core'
const { store, updateProp, commitProp } = useNodeProps()
const { node, nodes, isMulti, active, merged, updateAllWithUndo } = useMultiProps()

View file

@ -4,7 +4,7 @@ import { ref, computed, watch, onUnmounted } from 'vue'
import AppSelect from '@/components/AppSelect.vue'
import { useEditorStore } from '@/stores/editor'
import type { ExportFormat } from '@/engine/render-image'
import type { ExportFormat } from '@open-pencil/core'
const store = useEditorStore()

View file

@ -5,7 +5,7 @@ import { onClickOutside } from '@vueuse/core'
import ScrubInput from '@/components/ScrubInput.vue'
import { useNodeProps } from '@/composables/use-node-props'
import type { SceneNode, LayoutSizing, LayoutAlign, LayoutCounterAlign } from '@/engine/scene-graph'
import type { SceneNode, LayoutSizing, LayoutAlign, LayoutCounterAlign } from '@open-pencil/core'
const { store, node, updateProp, commitProp } = useNodeProps()

View file

@ -14,8 +14,8 @@ import {
DropdownMenuPortal
} from 'reka-ui'
import type { Color, Stroke } from '@/engine/scene-graph'
import type { SceneNode } from '@/engine/scene-graph'
import type { Color, Stroke } from '@open-pencil/core'
import type { SceneNode } from '@open-pencil/core'
type StrokeSides = 'ALL' | 'TOP' | 'BOTTOM' | 'LEFT' | 'RIGHT' | 'CUSTOM'

View file

@ -11,11 +11,11 @@ import {
DEFAULT_TEXT_WIDTH,
DEFAULT_TEXT_HEIGHT
} from '@/constants'
import { computeSelectionBounds, computeSnap } from '@/engine/snap'
import { computeSelectionBounds, computeSnap } from '@open-pencil/core'
import type { NodeType, SceneNode } from '@/engine/scene-graph'
import type { EditorStore, Tool } from '@/stores/editor'
import type { Rect } from '@/types'
import type { NodeType, SceneNode } from '@open-pencil/core'
type HandlePosition = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w'
@ -151,7 +151,7 @@ function getHandlePositions(
s: { x: mx, y: y2 },
sw: { x: x1, y: y2 },
w: { x: x1, y: my }
} as Record<HandlePosition, { x: number; y: number }>
} satisfies Record<HandlePosition, { x: number; y: number }>
}
function unrotate(
@ -224,11 +224,8 @@ function hitTestRotationHandle(
export function useCanvasInput(
canvasRef: Ref<HTMLCanvasElement | null>,
store: EditorStore,
hitTestSectionTitle: (cx: number, cy: number) => import('@/engine/scene-graph').SceneNode | null,
hitTestComponentLabel: (
cx: number,
cy: number
) => import('@/engine/scene-graph').SceneNode | null,
hitTestSectionTitle: (cx: number, cy: number) => import('@open-pencil/core').SceneNode | null,
hitTestComponentLabel: (cx: number, cy: number) => import('@open-pencil/core').SceneNode | null,
onCursorMove?: (cx: number, cy: number) => void
) {
const drag = ref<DragState | null>(null)

View file

@ -1,8 +1,8 @@
import { useRafFn, useResizeObserver } from '@vueuse/core'
import { onMounted, onUnmounted, type Ref } from 'vue'
import { getCanvasKit, getGpuBackend } from '@/engine/canvaskit'
import { SkiaRenderer } from '@/engine/renderer'
import { getCanvasKit, getGpuBackend } from '@open-pencil/core'
import { SkiaRenderer } from '@open-pencil/core'
import type { EditorStore } from '@/stores/editor'
import type { CanvasKit } from 'canvaskit-wasm'

View file

@ -1,5 +1,4 @@
import { Chat } from '@ai-sdk/vue'
import { DEFAULT_AI_MODEL } from '@open-pencil/core'
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
import { DirectChatTransport, ToolLoopAgent } from 'ai'
import dedent from 'dedent'
@ -7,6 +6,7 @@ import { computed, ref, watch } from 'vue'
import { createAITools } from '@/ai/tools'
import { useEditorStore } from '@/stores/editor'
import { DEFAULT_AI_MODEL } from '@open-pencil/core'
import type { UIMessage } from 'ai'

View file

@ -12,9 +12,9 @@ import {
YJS_JSON_FIELDS
} from '@/constants'
import type { SceneNode } from '@/engine/scene-graph'
import type { EditorStore } from '@/stores/editor'
import type { Color } from '@/types'
import type { SceneNode } from '@open-pencil/core'
import type { Room } from 'trystero'
export interface RemotePeer {
@ -179,7 +179,9 @@ export function useCollab(store: EditorStore) {
() => store.state.zoom,
(zoom) => {
if (!awareness) return
const prev = awareness.getLocalState()?.cursor as Record<string, unknown> | undefined
const prev = awareness.getLocalState()?.cursor as
| { x: number; y: number; pageId: string; zoom: number }
| undefined
if (prev) {
awareness.setLocalStateField('cursor', { ...prev, zoom })
}

View file

@ -1,6 +1,7 @@
import { isFontLoaded, DEFAULT_FONT_FAMILY } from '@open-pencil/core'
import { computed } from 'vue'
import { isFontLoaded, DEFAULT_FONT_FAMILY } from '@open-pencil/core'
import type { SceneNode } from '@open-pencil/core'
export function useNodeFontStatus(node: () => SceneNode) {

View file

@ -2,7 +2,7 @@ import { computed } from 'vue'
import { useEditorStore } from '@/stores/editor'
import type { SceneNode } from '@/engine/scene-graph'
import type { SceneNode } from '@open-pencil/core'
export const MIXED = Symbol('mixed')
export type MixedValue<T> = T | typeof MIXED

View file

@ -2,7 +2,7 @@ import { computed } from 'vue'
import { useEditorStore } from '@/stores/editor'
import type { SceneNode } from '@/engine/scene-graph'
import type { SceneNode } from '@open-pencil/core'
export function useNodeProps() {
const store = useEditorStore()

View file

@ -7,10 +7,10 @@ import {
toggleBoldInRange,
toggleDecorationInRange,
toggleItalicInRange
} from '@/engine/style-runs'
} from '@open-pencil/core'
import type { SceneNode } from '@/engine/scene-graph'
import type { EditorStore } from '@/stores/editor'
import type { SceneNode } from '@open-pencil/core'
const CARET_BLINK_MS = 530

View file

@ -1,7 +1,7 @@
import { computeAllLayouts } from './engine/layout'
import { computeAllLayouts } from '@open-pencil/core'
import type { Color, Effect, Fill, GradientStop } from './engine/scene-graph'
import type { EditorStore } from './stores/editor'
import type { Color, Effect, Fill, GradientStop } from '@open-pencil/core'
const WHITE: Color = { r: 1, g: 1, b: 1, a: 1 }
const BLACK: Color = { r: 0, g: 0, b: 0, a: 1 }

View file

@ -1,6 +0,0 @@
export {
getCanvasKit,
getGpuBackend,
type CanvasKitOptions,
type GpuBackend
} from '@open-pencil/core'

View file

@ -1,9 +0,0 @@
export {
parseFigmaClipboard,
importClipboardNodes,
figmaNodesBounds,
parseOpenPencilClipboard,
buildFigmaClipboardHTML,
buildOpenPencilClipboardHTML,
prefetchFigmaSchema
} from '@open-pencil/core'

View file

@ -1 +0,0 @@
export { parseColor, colorToHex, colorToHexRaw, colorToRgba255 } from '@open-pencil/core'

View file

@ -1 +0,0 @@
export { exportFigFile } from '@open-pencil/core'

View file

@ -1 +0,0 @@
export { computeLayout, computeAllLayouts, setTextMeasurer } from '@open-pencil/core'

View file

@ -1 +0,0 @@
export { renderNodesToImage, renderThumbnail, type ExportFormat } from '@open-pencil/core'

View file

@ -1 +0,0 @@
export { SkiaRenderer, type RenderOverlays } from '@open-pencil/core'

View file

@ -1,39 +0,0 @@
export { SceneGraph } from '@open-pencil/core'
export type {
GUID,
Color,
SceneNode,
NodeType,
Fill,
FillType,
Stroke,
StrokeCap,
StrokeJoin,
Effect,
BlendMode,
ImageScaleMode,
GradientStop,
GradientTransform,
LayoutMode,
LayoutSizing,
LayoutAlign,
LayoutCounterAlign,
LayoutWrap,
ConstraintType,
TextAutoResize,
TextAlignVertical,
TextCase,
TextDecoration,
ArcData,
VectorNetwork,
VectorVertex,
VectorSegment,
VectorRegion,
HandleMirroring,
WindingRule,
VariableType,
VariableValue,
Variable,
VariableCollection,
VariableCollectionMode
} from '@open-pencil/core'

View file

@ -1 +0,0 @@
export { computeSelectionBounds, computeSnap, type SnapGuide } from '@open-pencil/core'

View file

@ -1,7 +0,0 @@
export {
toggleBoldInRange,
toggleItalicInRange,
toggleDecorationInRange,
adjustRunsForInsert,
adjustRunsForDelete
} from '@open-pencil/core'

View file

@ -1 +0,0 @@
export { renderNodesToSVG, type SVGExportOptions } from '@open-pencil/core'

View file

@ -1 +0,0 @@
export { TextEditor, type TextCaret, type TextEditorState } from '@open-pencil/core'

View file

@ -1 +0,0 @@
export { UndoManager } from '@open-pencil/core'

View file

@ -1,6 +0,0 @@
export {
vectorNetworkToPath,
decodeVectorNetworkBlob,
encodeVectorNetworkBlob,
computeVectorBounds
} from '@open-pencil/core'

View file

@ -1,350 +0,0 @@
/**
* Figma Multiplayer WebSocket Client
*
* High-level interface for connecting to Figma's multiplayer server
* and creating/modifying nodes directly via WebSocket.
*
* Performance: ~1000 nodes in 15-20ms (vs 50-100s via plugin API)
*/
import {
initCodec,
encodeMessage,
decodeMessage,
decompress,
createNodeChangesMessage,
type NodeChange,
type FigmaMessage
} from './codec.ts'
import {
buildMultiplayerUrl,
MESSAGE_TYPES,
isZstdCompressed,
hasFigWireHeader,
skipFigWireHeader,
isKiwiMessage,
getKiwiMessageType
} from './protocol.ts'
import type { ChromeDevToolsTarget } from '@/types.ts'
export interface SessionInfo {
sessionID: number
reconnectSequenceNumber: number
}
export interface ConnectionOptions {
connectionTimeout?: number
onMessage?: (message: FigmaMessage) => void
onError?: (error: Error) => void
}
type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'ready'
export class FigmaMultiplayerClient {
private fileKey: string
private ws: WebSocket | null = null
private state: ConnectionState = 'disconnected'
private sessionInfo: SessionInfo | null = null
private localIDCounter: number
private options: ConnectionOptions
constructor(fileKey: string, options: ConnectionOptions = {}) {
this.fileKey = fileKey
this.localIDCounter = Date.now() % 10000000
this.options = {
connectionTimeout: 30000,
...options
}
}
/**
* Connect to Figma multiplayer server
*/
async connect(cookies: string): Promise<SessionInfo> {
if (this.state !== 'disconnected') {
throw new Error(`Cannot connect: state is ${this.state}`)
}
await initCodec()
this.state = 'connecting'
return new Promise((resolve, reject) => {
const url = buildMultiplayerUrl(this.fileKey)
this.ws = new WebSocket(url, {
headers: {
Cookie: cookies,
Origin: 'https://www.figma.com'
}
} as WebSocketInit)
this.ws.binaryType = 'arraybuffer'
const timeout = setTimeout(() => {
this.close()
reject(new Error('Connection timeout'))
}, this.options.connectionTimeout)
let sessionID = 0
let reconnectSequenceNumber = 0
let joinEndReceived = false
this.ws.onmessage = (event) => {
if (!(event.data instanceof ArrayBuffer)) return
let data: Uint8Array = new Uint8Array(event.data as ArrayBuffer)
// Skip fig-wire header if present
if (hasFigWireHeader(data)) {
data = new Uint8Array(skipFigWireHeader(data))
}
if (!isZstdCompressed(data)) return
try {
const decompressed = decompress(data)
// Skip non-Kiwi message data (e.g., schema definitions)
if (!isKiwiMessage(decompressed)) return
const msgType = getKiwiMessageType(decompressed)
if (!msgType) return
// JOIN_END: sync complete
if (msgType === MESSAGE_TYPES.JOIN_END) {
joinEndReceived = true
}
// SIGNAL: extract reconnect-sequence-number
if (msgType === MESSAGE_TYPES.SIGNAL) {
const str = new TextDecoder().decode(decompressed)
const match = str.match(/reconnect-sequence-number[^\d]*(\d+)/)
if (match?.[1]) {
reconnectSequenceNumber = parseInt(match[1])
}
}
// Forward messages when ready
if (this.options.onMessage && this.state === 'ready') {
try {
const message = decodeMessage(data)
this.options.onMessage(message)
} catch {
// Ignore decode errors
}
}
// Check if handshake complete
// Note: sessionID may be 0 if Figma changed protocol; caller gets it from plugin API
if (joinEndReceived && (this.state === 'connecting' || this.state === 'connected')) {
clearTimeout(timeout)
this.state = 'ready'
this.sessionInfo = { sessionID: sessionID || 0, reconnectSequenceNumber }
resolve(this.sessionInfo)
}
} catch {
// Ignore parse errors
}
}
this.ws.onopen = () => {
this.state = 'connected'
}
this.ws.onerror = () => {
clearTimeout(timeout)
const error = new Error('WebSocket connection failed')
this.options.onError?.(error)
if (this.state === 'connecting') {
reject(error)
}
}
this.ws.onclose = () => {
clearTimeout(timeout)
const wasConnecting = this.state === 'connecting'
this.state = 'disconnected'
this.ws = null
if (wasConnecting) {
reject(new Error('Connection closed during handshake'))
}
}
})
}
/**
* Send node changes to Figma (fire and forget)
*/
async sendNodeChanges(nodeChanges: NodeChange[]): Promise<void> {
if (this.state !== 'ready' || !this.ws || !this.sessionInfo) {
throw new Error('Not connected')
}
const message = createNodeChangesMessage(
this.sessionInfo.sessionID,
this.sessionInfo.reconnectSequenceNumber,
nodeChanges
)
const encoded = encodeMessage(message)
this.ws.send(encoded)
}
/**
* Send node changes and wait for server ACK (NODE_CHANGES echo)
* This guarantees nodes are synced before returning
*/
async sendNodeChangesSync(nodeChanges: NodeChange[], timeout = 5000): Promise<void> {
if (this.state !== 'ready' || !this.ws || !this.sessionInfo) {
throw new Error('Not connected')
}
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this.ws?.removeEventListener('message', handler)
reject(new Error('ACK timeout'))
}, timeout)
const handler = (event: MessageEvent) => {
if (!(event.data instanceof ArrayBuffer)) return
let data: Uint8Array = new Uint8Array(event.data as ArrayBuffer)
if (hasFigWireHeader(data)) data = new Uint8Array(skipFigWireHeader(data))
if (!isZstdCompressed(data)) return
try {
const dec = decompress(data)
if (isKiwiMessage(dec) && getKiwiMessageType(dec) === MESSAGE_TYPES.NODE_CHANGES) {
clearTimeout(timer)
this.ws?.removeEventListener('message', handler)
resolve()
}
} catch {}
}
this.ws?.addEventListener('message', handler)
this.sendNodeChanges(nodeChanges)
})
}
/**
* Generate unique local ID for new nodes
*/
nextLocalID(): number {
return this.localIDCounter++
}
/**
* Get current session info
*/
getSession(): SessionInfo | null {
return this.sessionInfo
}
/**
* Check if connected and ready
*/
isReady(): boolean {
return this.state === 'ready'
}
/**
* Check if connection is still alive
*/
isConnected(): boolean {
return this.state === 'connected' && this.ws !== null && this.ws.readyState === WebSocket.OPEN
}
/**
* Close the connection
*/
close(): void {
if (this.ws) {
this.ws.close()
this.ws = null
}
this.state = 'disconnected'
this.sessionInfo = null
}
}
// WebSocket with headers support (Bun/Node specific)
interface WebSocketInit {
headers?: Record<string, string>
}
/**
* Get Figma cookies from Chrome DevTools Protocol
*
* Requires Chrome/Figma to be running with:
* --remote-debugging-port=9222
*/
export async function getCookiesFromDevTools(pageId?: string): Promise<string> {
// Get list of pages if no ID provided
if (!pageId) {
const listResponse = await fetch('http://localhost:9222/json')
const targets = (await listResponse.json()) as ChromeDevToolsTarget[]
const figmaTarget = targets.find((t) => t.url.includes('figma.com'))
if (!figmaTarget) {
throw new Error('No Figma tab found. Open Figma in Chrome with --remote-debugging-port=9222')
}
pageId = figmaTarget.id
}
const wsUrl = `ws://localhost:9222/devtools/page/${pageId}`
return new Promise((resolve, reject) => {
const ws = new WebSocket(wsUrl)
const timeout = setTimeout(() => {
ws.close()
reject(new Error('DevTools connection timeout'))
}, 5000)
ws.onopen = () => {
ws.send(
JSON.stringify({
id: 1,
method: 'Network.getCookies',
params: { urls: ['https://www.figma.com'] }
})
)
}
ws.onmessage = (e) => {
clearTimeout(timeout)
const data = JSON.parse(e.data as string)
if (data.result?.cookies) {
ws.close()
const cookieString = data.result.cookies
.map((c: { name: string; value: string }) => `${c.name}=${c.value}`)
.join('; ')
resolve(cookieString)
}
}
ws.onerror = () => {
clearTimeout(timeout)
reject(
new Error(
'Cannot connect to Chrome DevTools. Is Chrome running with --remote-debugging-port=9222?'
)
)
}
})
}
/**
* Parse Figma file key from URL
*/
export function parseFileKey(urlOrKey: string): string {
if (!urlOrKey.includes('/')) {
return urlOrKey
}
const match = urlOrKey.match(/figma\.com\/(?:file|design)\/([a-zA-Z0-9]+)/)
if (!match?.[1]) {
throw new Error('Invalid Figma URL')
}
return match[1]
}

View file

@ -1,21 +0,0 @@
export {
initCodec,
encodeMessage,
decodeMessage,
compress,
decompress,
getCompiledSchema,
getSchemaBytes,
createNodeChangesMessage,
createNodeChange,
parseVariableId,
encodePaintWithVariableBinding,
encodeNodeChangeWithVariables,
type NodeChange,
type KiwiGUID as GUID,
type KiwiColor as Color,
type KiwiPaint as Paint,
type VariableBinding,
type ParentIndex,
type FigmaMessage
} from '@open-pencil/core'

View file

@ -1 +0,0 @@
export { readFigFile } from '@open-pencil/core'

View file

@ -1 +0,0 @@
export { importNodeChanges } from '@open-pencil/core'

View file

@ -1,50 +0,0 @@
/**
* Figma Multiplayer Module
*
* Direct WebSocket access to Figma's multiplayer protocol.
* Enables creating nodes 1000-5000x faster than the plugin API.
*/
export {
FigmaMultiplayerClient,
getCookiesFromDevTools,
parseFileKey,
type SessionInfo,
type ConnectionOptions
} from './client.ts'
export {
initCodec,
encodeMessage,
decodeMessage,
createNodeChangesMessage,
createNodeChange,
parseVariableId,
encodePaintWithVariableBinding,
encodeNodeChangeWithVariables,
type GUID,
type Color,
type Paint,
type VariableBinding,
type ParentIndex,
type FigmaMessage
} from './codec.ts'
export {
MESSAGE_TYPES,
NODE_TYPES,
NODE_PHASES,
BLEND_MODES,
PAINT_TYPES,
PROTOCOL_VERSION,
KIWI,
SESSION_ID,
ZSTD_MAGIC,
buildMultiplayerUrl,
isZstdCompressed,
hasFigWireHeader,
skipFigWireHeader,
isKiwiMessage,
getKiwiMessageType,
parseVarint
} from './protocol.ts'

View file

@ -1,19 +0,0 @@
export {
MESSAGE_TYPES,
NODE_TYPES,
NODE_PHASES,
BLEND_MODES,
PAINT_TYPES,
PROTOCOL_VERSION,
KIWI,
SESSION_ID,
ZSTD_MAGIC,
FIG_WIRE_MAGIC,
buildMultiplayerUrl,
isZstdCompressed,
hasFigWireHeader,
skipFigWireHeader,
isKiwiMessage,
getKiwiMessageType,
parseVarint
} from '@open-pencil/core'

View file

@ -1,4 +1,3 @@
import { collectFontKeys } from '@open-pencil/core'
import { shallowReactive, shallowRef, computed, watch } from 'vue'
import {
@ -12,39 +11,42 @@ import {
ZOOM_SCALE_MIN,
ZOOM_SCALE_MAX
} from '@/constants'
import { loadFont } from '@/engine/fonts'
import {
parseFigmaClipboard,
collectFontKeys,
computeLayout,
computeAllLayouts,
computeVectorBounds,
exportFigFile,
importClipboardNodes,
figmaNodesBounds,
parseFigmaClipboard,
parseOpenPencilClipboard,
buildFigmaClipboardHTML,
buildOpenPencilClipboardHTML,
prefetchFigmaSchema
} from '@/engine/clipboard'
import { exportFigFile } from '@/engine/fig-export'
import { loadFont } from '@/engine/fonts'
import { computeLayout, computeAllLayouts, setTextMeasurer } from '@/engine/layout'
import { renderNodesToImage } from '@/engine/render-image'
import { SceneGraph } from '@/engine/scene-graph'
import { renderNodesToSVG } from '@/engine/svg-export'
import { TextEditor } from '@/engine/text-editor'
import { UndoManager } from '@/engine/undo'
import { computeVectorBounds } from '@/engine/vector'
import { readFigFile } from '@/kiwi/fig-file'
prefetchFigmaSchema,
renderNodesToImage,
renderNodesToSVG,
SceneGraph,
setTextMeasurer,
TextEditor,
UndoManager
} from '@open-pencil/core'
import { readFigFile } from '@open-pencil/core'
import type { ExportFormat } from '@/engine/render-image'
import type { Color, Rect } from '@/types'
import type {
SceneNode,
NodeType,
ExportFormat,
Fill,
LayoutMode,
VectorVertex,
VectorSegment,
NodeType,
SceneNode,
SnapGuide,
VectorNetwork,
VectorRegion,
VectorNetwork
} from '@/engine/scene-graph'
import type { SnapGuide } from '@/engine/snap'
import type { Color, Rect } from '@/types'
VectorSegment,
VectorVertex
} from '@open-pencil/core'
export type Tool =
| 'SELECT'
@ -129,7 +131,7 @@ export function createEditorStore() {
let lastWriteTime = 0
let unwatchFile: (() => void) | null = null
let _ck: import('canvaskit-wasm').CanvasKit | null = null
let _renderer: import('@/engine/renderer').SkiaRenderer | null = null
let _renderer: import('@open-pencil/core').SkiaRenderer | null = null
let _textEditor: TextEditor | null = null
prefetchFigmaSchema()
@ -624,7 +626,7 @@ export function createEditorStore() {
function setCanvasKit(
ck: import('canvaskit-wasm').CanvasKit,
renderer: import('@/engine/renderer').SkiaRenderer
renderer: import('@open-pencil/core').SkiaRenderer
) {
_ck = ck
_renderer = renderer
@ -945,10 +947,9 @@ export function createEditorStore() {
function updateNodeWithUndo(id: string, changes: Partial<SceneNode>, label = 'Update') {
const node = graph.getNode(id)
if (!node) return
const previous: Partial<SceneNode> = {}
for (const key of Object.keys(changes) as (keyof SceneNode)[]) {
;(previous as Record<string, unknown>)[key] = node[key]
}
const previous = Object.fromEntries(
(Object.keys(changes) as (keyof SceneNode)[]).map((key) => [key, node[key]])
) as Partial<SceneNode>
graph.updateNode(id, changes)
runLayoutForNode(id)
syncIfInsideComponent(id)
@ -1006,12 +1007,11 @@ export function createEditorStore() {
if (mode !== 'NONE') computeLayout(graph, id)
runLayoutForNode(id)
const finalState: Partial<SceneNode> = {}
const updated = graph.getNode(id)
if (!updated) return
for (const key of Object.keys(previous) as (keyof SceneNode)[]) {
;(finalState as Record<string, unknown>)[key] = updated[key]
}
const finalState = Object.fromEntries(
(Object.keys(previous) as (keyof SceneNode)[]).map((key) => [key, updated[key]])
) as Partial<SceneNode>
undo.push({
label: mode === 'NONE' ? 'Remove auto layout' : 'Add auto layout',
@ -1989,10 +1989,9 @@ export function createEditorStore() {
function commitNodeUpdate(nodeId: string, previous: Partial<SceneNode>, label = 'Update') {
const node = graph.getNode(nodeId)
if (!node) return
const current: Partial<SceneNode> = {}
for (const key of Object.keys(previous) as (keyof SceneNode)[]) {
;(current as Record<string, unknown>)[key] = node[key]
}
const current = Object.fromEntries(
(Object.keys(previous) as (keyof SceneNode)[]).map((key) => [key, node[key]])
) as Partial<SceneNode>
undo.push({
label,
forward: () => {