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:
parent
6ec8fc518b
commit
9d73297040
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
export {
|
||||
getCanvasKit,
|
||||
getGpuBackend,
|
||||
type CanvasKitOptions,
|
||||
type GpuBackend
|
||||
} from '@open-pencil/core'
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
export {
|
||||
parseFigmaClipboard,
|
||||
importClipboardNodes,
|
||||
figmaNodesBounds,
|
||||
parseOpenPencilClipboard,
|
||||
buildFigmaClipboardHTML,
|
||||
buildOpenPencilClipboardHTML,
|
||||
prefetchFigmaSchema
|
||||
} from '@open-pencil/core'
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { parseColor, colorToHex, colorToHexRaw, colorToRgba255 } from '@open-pencil/core'
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { exportFigFile } from '@open-pencil/core'
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { computeLayout, computeAllLayouts, setTextMeasurer } from '@open-pencil/core'
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { renderNodesToImage, renderThumbnail, type ExportFormat } from '@open-pencil/core'
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { SkiaRenderer, type RenderOverlays } from '@open-pencil/core'
|
||||
|
|
@ -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'
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { computeSelectionBounds, computeSnap, type SnapGuide } from '@open-pencil/core'
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
export {
|
||||
toggleBoldInRange,
|
||||
toggleItalicInRange,
|
||||
toggleDecorationInRange,
|
||||
adjustRunsForInsert,
|
||||
adjustRunsForDelete
|
||||
} from '@open-pencil/core'
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { renderNodesToSVG, type SVGExportOptions } from '@open-pencil/core'
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { TextEditor, type TextCaret, type TextEditorState } from '@open-pencil/core'
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { UndoManager } from '@open-pencil/core'
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export {
|
||||
vectorNetworkToPath,
|
||||
decodeVectorNetworkBlob,
|
||||
encodeVectorNetworkBlob,
|
||||
computeVectorBounds
|
||||
} from '@open-pencil/core'
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { readFigFile } from '@open-pencil/core'
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { importNodeChanges } from '@open-pencil/core'
|
||||
|
|
@ -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'
|
||||
|
|
@ -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'
|
||||
|
|
@ -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: () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue