feat(editor): add event bus for editor lifecycle events

Wire nanoevents-based event bus to the editor core:

- EditorEvents type with render, graph, selection, tool, page, viewport events
- All node mutations (create/update/delete/reparent/reorder) forwarded from SceneGraph
- Selection changes routed through setSelectedIds() with diff detection
- Tool changes routed through setActiveTool() with change detection
- Page switches emit page:changed
- Viewport pan/zoom/fit/zoomTo emit viewport:changed
- Graph replacement emits graph:replaced
- Typed onEditorEvent() subscription on the Editor return object
- useEditorEvent() Vue composable with automatic scope cleanup
- Replace all direct state.selectedIds/state.activeTool mutations in core, app, and SDK
This commit is contained in:
Danila Poyarkov 2026-05-06 15:25:27 +03:00
parent dfa9505805
commit 1835903e56
28 changed files with 207 additions and 65 deletions

View file

@ -38,7 +38,7 @@ export function createClipboardActions(ctx: EditorContext) {
}
if (newRootIds.length > 0) {
ctx.state.selectedIds = new Set(newRootIds)
ctx.setSelectedIds(new Set(newRootIds))
ctx.undo.push({
label: 'Duplicate',
forward: () => {
@ -48,11 +48,11 @@ export function createClipboardActions(ctx: EditorContext) {
const parentId = snapshot.parentId ?? ctx.state.currentPageId
restoreSubtree(ctx.graph, snapshot, parentId, allSnapshots)
}
ctx.state.selectedIds = new Set(newRootIds)
ctx.setSelectedIds(new Set(newRootIds))
},
inverse: () => {
for (const id of newRootIds.slice().reverse()) ctx.graph.deleteNode(id)
ctx.state.selectedIds = prevSelection
ctx.setSelectedIds(prevSelection)
}
})
}
@ -82,7 +82,7 @@ export function createClipboardActions(ctx: EditorContext) {
const cy = cursorPos?.y ?? (-ctx.state.panY + viewH / 2) / ctx.state.zoom
placementActions.centerNodesAt(created, cx, cy)
computeAllLayouts(ctx.graph, ctx.state.currentPageId)
ctx.state.selectedIds = new Set(created)
ctx.setSelectedIds(new Set(created))
const allNodes = collectSubtrees(ctx.graph, created)
const pageId = ctx.state.currentPageId
@ -96,12 +96,12 @@ export function createClipboardActions(ctx: EditorContext) {
})
}
computeAllLayouts(ctx.graph, pageId)
ctx.state.selectedIds = new Set(created)
ctx.setSelectedIds(new Set(created))
},
inverse: () => {
for (const id of [...created].reverse()) ctx.graph.deleteNode(id)
computeAllLayouts(ctx.graph, pageId)
ctx.state.selectedIds = prevSelection
ctx.setSelectedIds(prevSelection)
}
})
void fontActions.loadFontsForNodes(created)
@ -137,7 +137,7 @@ export function createClipboardActions(ctx: EditorContext) {
if (cursorPos) placementActions.centerNodesAt(created, cursorPos.x, cursorPos.y)
computeAllLayouts(ctx.graph, ctx.state.currentPageId)
ctx.state.selectedIds = new Set(created)
ctx.setSelectedIds(new Set(created))
const allNodes = collectSubtrees(ctx.graph, created)
const pageId = ctx.state.currentPageId
@ -151,12 +151,12 @@ export function createClipboardActions(ctx: EditorContext) {
})
}
computeAllLayouts(ctx.graph, pageId)
ctx.state.selectedIds = new Set(created)
ctx.setSelectedIds(new Set(created))
},
inverse: () => {
for (const id of [...created].reverse()) ctx.graph.deleteNode(id)
computeAllLayouts(ctx.graph, pageId)
ctx.state.selectedIds = prevSelection
ctx.setSelectedIds(prevSelection)
}
})
}
@ -192,7 +192,7 @@ export function createClipboardActions(ctx: EditorContext) {
label: 'Delete',
forward: () => {
for (const { id } of entries) ctx.graph.deleteNode(id)
ctx.state.selectedIds = new Set()
ctx.setSelectedIds(new Set())
},
inverse: () => {
for (const { id, parentId, index, subtree } of [...entries].reverse()) {
@ -200,10 +200,10 @@ export function createClipboardActions(ctx: EditorContext) {
if (rootSnap) restoreSubtree(ctx.graph, rootSnap, parentId, subtree)
if (index >= 0) ctx.graph.reorderChild(id, parentId, index)
}
ctx.state.selectedIds = prevSelection
ctx.setSelectedIds(prevSelection)
}
})
ctx.state.selectedIds = new Set()
ctx.setSelectedIds(new Set())
}
const copyActions = createClipboardCopyActions(ctx)

View file

@ -68,7 +68,7 @@ export function createClipboardImageActions(ctx: EditorContext) {
ctx.graph.images.delete(hash)
const next = new Set(ctx.state.selectedIds)
next.delete(id)
ctx.state.selectedIds = next
ctx.setSelectedIds(next)
}
})
return id
@ -97,7 +97,7 @@ export function createClipboardImageActions(ctx: EditorContext) {
curX += p.w + IMAGE_GAP
}
if (ids.length) {
ctx.state.selectedIds = new Set(ids)
ctx.setSelectedIds(new Set(ids))
ctx.requestRender()
}
}

View file

@ -25,16 +25,16 @@ export function createComponentActions(ctx: EditorContext) {
if (node.type === 'FRAME' || node.type === 'GROUP') {
ctx.graph.updateNode(node.id, { type: 'COMPONENT' })
ctx.state.selectedIds = new Set([node.id])
ctx.setSelectedIds(new Set([node.id]))
ctx.undo.push({
label: 'Create component',
forward: () => {
ctx.graph.updateNode(node.id, { type: 'COMPONENT' })
ctx.state.selectedIds = new Set([node.id])
ctx.setSelectedIds(new Set([node.id]))
},
inverse: () => {
ctx.graph.updateNode(node.id, { type: prevType })
ctx.state.selectedIds = prevSelection
ctx.setSelectedIds(prevSelection)
}
})
return

View file

@ -18,7 +18,7 @@ export function createComponentFocusActions(ctx: EditorContext) {
void switchPage(current.id)
}
ctx.state.selectedIds = new Set([main.id])
ctx.setSelectedIds(new Set([main.id]))
const abs = ctx.graph.getAbsolutePosition(main.id)
const { width: viewW, height: viewH } = ctx.getViewportSize()

View file

@ -14,17 +14,17 @@ export function createComponentInstanceActions(ctx: EditorContext) {
if (!instance) return null
const instanceId = instance.id
ctx.state.selectedIds = new Set([instanceId])
ctx.setSelectedIds(new Set([instanceId]))
ctx.undo.push({
label: 'Create instance',
forward: () => {
ctx.graph.createInstance(componentId, parentId, { ...instance })
ctx.state.selectedIds = new Set([instanceId])
ctx.setSelectedIds(new Set([instanceId]))
},
inverse: () => {
ctx.graph.deleteNode(instanceId)
ctx.state.selectedIds = new Set([componentId])
ctx.setSelectedIds(new Set([componentId]))
}
})
return instanceId
@ -36,7 +36,7 @@ export function createComponentInstanceActions(ctx: EditorContext) {
const prevComponentId = selectedNode.componentId
ctx.graph.detachInstance(selectedNode.id)
ctx.state.selectedIds = new Set([selectedNode.id])
ctx.setSelectedIds(new Set([selectedNode.id]))
ctx.undo.push({
label: 'Detach instance',

View file

@ -1,4 +1,6 @@
import type { CanvasKit } from 'canvaskit-wasm'
import { createNanoEvents } from 'nanoevents'
import type { Emitter } from 'nanoevents'
import type { SkiaRenderer } from '#core/canvas/renderer'
import { prefetchFigmaSchema } from '#core/clipboard'
@ -28,7 +30,13 @@ import { createShapeActions } from './shapes'
import { createDefaultEditorState } from './state'
import { createStructureActions } from './structure'
import { createTextActions } from './text'
import type { EditorContext, EditorOptions, EditorState } from './types'
import type {
EditorContext,
EditorEventName,
EditorEvents,
EditorOptions,
EditorState
} from './types'
import { createUndoActions } from './undo'
import { createVariableActions } from './variables'
import { createViewportActions } from './viewport'
@ -50,18 +58,56 @@ export function createEditor(options?: EditorOptions) {
let _renderer: SkiaRenderer | null = null
const _renderers = new Set<SkiaRenderer>()
let _textEditor: TextEditor | null = null
const events: Emitter<EditorEvents> = createNanoEvents()
void prefetchFigmaSchema()
const state: EditorState = options?.state ?? createDefaultEditorState(_graph.getPages()[0].id)
function emitEditorEvent<K extends EditorEventName>(
event: K,
...args: Parameters<EditorEvents[K]>
) {
events.emit(event, ...args)
}
function onEditorEvent<K extends EditorEventName>(event: K, handler: EditorEvents[K]) {
return events.on(event, handler)
}
function requestRender() {
state.renderVersion++
state.sceneVersion++
emitEditorEvent('render:requested', {
renderVersion: state.renderVersion,
sceneVersion: state.sceneVersion
})
}
function requestRepaint() {
state.renderVersion++
emitEditorEvent('repaint:requested', {
renderVersion: state.renderVersion,
sceneVersion: state.sceneVersion
})
}
function setSelectedIds(ids: Set<string>) {
const previous = [...state.selectedIds]
state.selectedIds = ids
const selected = [...ids]
if (
previous.length !== selected.length ||
previous.some((id, index) => id !== selected[index])
) {
emitEditorEvent('selection:changed', selected, previous)
}
}
function setActiveTool(tool: EditorState['activeTool']) {
const previous = state.activeTool
state.activeTool = tool
if (previous !== tool) emitEditorEvent('tool:changed', tool, previous)
}
const graphReads = createGraphReadActions(() => _graph)
@ -72,7 +118,8 @@ export function createEditor(options?: EditorOptions) {
getGraph: () => _graph,
getRenderers: () => _renderers,
scheduleComponentSync,
requestRender
requestRender,
emitEditorEvent
})
if (!skipInitialGraphSetup) {
@ -96,6 +143,9 @@ export function createEditor(options?: EditorOptions) {
getTextEditor: () => _textEditor,
requestRender,
requestRepaint,
emitEditorEvent,
setSelectedIds,
setActiveTool,
runLayoutForNode,
subscribeToGraph
}
@ -137,10 +187,15 @@ export function createEditor(options?: EditorOptions) {
function replaceGraph(newGraph: SceneGraph) {
_graph = newGraph
subscribeToGraph()
const previousPageId = state.currentPageId
state.currentPageId = _graph.getPages()[0]?.id ?? _graph.rootId
state.selectedIds = new Set()
setSelectedIds(new Set())
state.hoveredNodeId = null
pages.clearPageViewports()
emitEditorEvent('graph:replaced', _graph)
if (previousPageId !== state.currentPageId) {
emitEditorEvent('page:changed', state.currentPageId, previousPageId)
}
requestRender()
}
@ -163,6 +218,7 @@ export function createEditor(options?: EditorOptions) {
// Lifecycle
requestRender,
requestRepaint,
onEditorEvent,
setCanvasKit,
removeCanvasRenderer,
replaceGraph,

View file

@ -1,11 +1,15 @@
import type { SkiaRenderer } from '#core/canvas/renderer'
import type { SceneGraph, SceneNode } from '#core/scene-graph'
import type { SceneGraph, SceneGraphEvents, SceneNode } from '#core/scene-graph'
type GraphEventOptions = {
getGraph: () => SceneGraph
getRenderers: () => Iterable<SkiaRenderer>
scheduleComponentSync: (nodeId: string) => void
requestRender: () => void
emitEditorEvent: <K extends keyof SceneGraphEvents>(
event: K,
...args: Parameters<SceneGraphEvents[K]>
) => void
}
export function createGraphEventSubscription(options: GraphEventOptions) {
@ -16,6 +20,7 @@ export function createGraphEventSubscription(options: GraphEventOptions) {
if ('vectorNetwork' in changes) renderer.invalidateVectorPath(id)
renderer.invalidateNodePicture(id)
}
options.emitEditorEvent('node:updated', id, changes)
options.scheduleComponentSync(id)
options.requestRender()
}
@ -29,10 +34,22 @@ export function createGraphEventSubscription(options: GraphEventOptions) {
unbindGraphEvents?.()
unbindGraphEvents = options.getGraph().onNodeEvents({
updated: onNodeUpdated,
created: (node) => onNodeStructureChanged(node.id),
deleted: onNodeStructureChanged,
reparented: onNodeStructureChanged,
reordered: onNodeStructureChanged
created: (node) => {
options.emitEditorEvent('node:created', node)
onNodeStructureChanged(node.id)
},
deleted: (id) => {
options.emitEditorEvent('node:deleted', id)
onNodeStructureChanged(id)
},
reparented: (nodeId, oldParentId, newParentId) => {
options.emitEditorEvent('node:reparented', nodeId, oldParentId, newParentId)
onNodeStructureChanged(nodeId)
},
reordered: (nodeId, parentId, index) => {
options.emitEditorEvent('node:reordered', nodeId, parentId, index)
onNodeStructureChanged(nodeId)
}
})
}

View file

@ -27,7 +27,7 @@ export function restorePageFromSnapshot(ctx: EditorContext, snapshot: PageSnapsh
ctx.graph.clearAbsPosCache()
computeAllLayouts(ctx.graph, pageId)
ctx.state.selectedIds = new Set()
ctx.setSelectedIds(new Set())
ctx.state.hoveredNodeId = null
ctx.requestRender()
}

View file

@ -3,4 +3,11 @@ export type { Editor } from './create'
export { createTextActions } from './text'
export { EDITOR_TOOLS, TOOL_SHORTCUTS } from './tool-registry'
export type { EditorToolDef } from './tool-registry'
export type { EditorContext, EditorOptions, EditorState, Tool } from './types'
export type {
EditorContext,
EditorEventName,
EditorEvents,
EditorOptions,
EditorState,
Tool
} from './types'

View file

@ -14,9 +14,11 @@ export function createPageActions(ctx: EditorContext) {
pageViewportStore.saveCurrentPageViewport()
const previousPageId = ctx.state.currentPageId
ctx.state.currentPageId = pageId
ctx.state.enteredContainerId = null
ctx.state.selectedIds = new Set()
ctx.setSelectedIds(new Set())
if (previousPageId !== pageId) ctx.emitEditorEvent('page:changed', pageId, previousPageId)
pageViewportStore.restorePageViewport(pageId)

View file

@ -12,19 +12,19 @@ export function createSelectionActions(ctx: EditorContext) {
if (next.has(id)) next.delete(id)
else next.add(id)
}
ctx.state.selectedIds = next
ctx.setSelectedIds(next)
} else {
ctx.state.selectedIds = new Set(ids)
ctx.setSelectedIds(new Set(ids))
}
}
function clearSelection() {
ctx.state.selectedIds = new Set()
ctx.setSelectedIds(new Set())
}
function selectAll() {
const children = ctx.graph.getChildren(ctx.state.currentPageId)
ctx.state.selectedIds = new Set(children.map((n) => n.id))
ctx.setSelectedIds(new Set(children.map((n) => n.id)))
}
const containerActions = createSelectionContainerActions(ctx)

View file

@ -21,7 +21,7 @@ export function createSelectionContainerActions(ctx: EditorContext) {
} else {
ctx.state.enteredContainerId = null
}
ctx.state.selectedIds = new Set(entered ? [entered] : [])
ctx.setSelectedIds(new Set(entered ? [entered] : []))
}
return { validateEnteredContainer, enterContainer, exitContainer }

View file

@ -70,7 +70,7 @@ export function createShapeActions(ctx: EditorContext) {
ctx.graph.deleteNode(id)
const next = new Set(ctx.state.selectedIds)
next.delete(id)
ctx.state.selectedIds = next
ctx.setSelectedIds(next)
}
})
return id
@ -79,7 +79,7 @@ export function createShapeActions(ctx: EditorContext) {
const penActions = createPenActions(ctx, createShape)
function setTool(tool: typeof ctx.state.activeTool) {
ctx.state.activeTool = tool
ctx.setActiveTool(tool)
}
return {

View file

@ -205,12 +205,12 @@ export function createPenActions(ctx: EditorContext, createShape: CreateShape) {
fills,
strokes
})
ctx.state.selectedIds = new Set([nodeId])
ctx.setSelectedIds(new Set([nodeId]))
ctx.state.penState = null
ctx.state.penCursorX = null
ctx.state.penCursorY = null
ctx.state.activeTool = 'SELECT'
ctx.setActiveTool('SELECT')
ctx.requestRender()
}
@ -218,7 +218,7 @@ export function createPenActions(ctx: EditorContext, createShape: CreateShape) {
ctx.state.penState = null
ctx.state.penCursorX = null
ctx.state.penCursorY = null
ctx.state.activeTool = 'SELECT'
ctx.setActiveTool('SELECT')
ctx.requestRender()
}

View file

@ -60,7 +60,7 @@ export function createStructureActions(ctx: EditorContext) {
for (const id of ids) {
ctx.graph.reparentNode(id, pageId)
}
ctx.state.selectedIds = new Set()
ctx.setSelectedIds(new Set())
}
function renameNode(id: string, name: string) {

View file

@ -52,7 +52,7 @@ export function wrapInAutoLayout(
computeLayout(ctx.graph, frameId)
ctx.runLayoutForNode(frameId)
ctx.state.selectedIds = new Set([frameId])
ctx.setSelectedIds(new Set([frameId]))
ctx.undo.push({
label: 'Wrap in auto layout',
@ -61,7 +61,7 @@ export function wrapInAutoLayout(
for (const n of origPositions) ctx.graph.reparentNode(n.id, f.id)
computeLayout(ctx.graph, f.id)
ctx.runLayoutForNode(f.id)
ctx.state.selectedIds = new Set([f.id])
ctx.setSelectedIds(new Set([f.id]))
},
inverse: () => {
for (const orig of origPositions) {
@ -69,7 +69,7 @@ export function wrapInAutoLayout(
ctx.graph.updateNode(orig.id, { x: orig.x, y: orig.y })
}
ctx.graph.deleteNode(frameId)
ctx.state.selectedIds = prevSelection
ctx.setSelectedIds(prevSelection)
}
})
}

View file

@ -70,7 +70,7 @@ export function wrapSelectionInContainer(
ctx.graph.reparentNode(n.id, containerId)
}
ctx.state.selectedIds = new Set([containerId])
ctx.setSelectedIds(new Set([containerId]))
ctx.undo.push({
label: `Create ${containerType.toLowerCase().replace('_', ' ')}`,
@ -82,7 +82,7 @@ export function wrapSelectionInContainer(
})
ctx.graph.insertChildAt(c.id, parentId, firstIndex)
for (const n of origPositions) ctx.graph.reparentNode(n.id, c.id)
ctx.state.selectedIds = new Set([c.id])
ctx.setSelectedIds(new Set([c.id]))
},
inverse: () => {
for (const orig of origPositions) {
@ -90,7 +90,7 @@ export function wrapSelectionInContainer(
ctx.graph.updateNode(orig.id, { x: orig.x, y: orig.y })
}
ctx.graph.deleteNode(containerId)
ctx.state.selectedIds = prevSelection
ctx.setSelectedIds(prevSelection)
}
})

View file

@ -26,7 +26,7 @@ export function ungroupSelected(ctx: EditorContext, selectedNode: SceneNode | un
}
ctx.graph.deleteNode(node.id)
ctx.state.selectedIds = new Set(childIds)
ctx.setSelectedIds(new Set(childIds))
ctx.undo.push({
label: 'Ungroup',
@ -36,7 +36,7 @@ export function ungroupSelected(ctx: EditorContext, selectedNode: SceneNode | un
ctx.graph.insertChildAt(childIds[i], parentId, groupIndex + i)
}
ctx.graph.deleteNode(groupId)
ctx.state.selectedIds = new Set(childIds)
ctx.setSelectedIds(new Set(childIds))
},
inverse: () => {
const g = ctx.graph.createNode('GROUP', parentId, {
@ -49,7 +49,7 @@ export function ungroupSelected(ctx: EditorContext, selectedNode: SceneNode | un
ctx.graph.reparentNode(orig.id, g.id)
ctx.graph.updateNode(orig.id, { x: orig.x, y: orig.y })
}
ctx.state.selectedIds = prevSelection
ctx.setSelectedIds(prevSelection)
}
})
}

View file

@ -1,7 +1,7 @@
import type { CanvasKit } from 'canvaskit-wasm'
import type { RulerTheme, SkiaRenderer } from '#core/canvas/renderer'
import type { SceneGraph, VectorSegment, VectorVertex } from '#core/scene-graph'
import type { SceneGraph, SceneNode, VectorSegment, VectorVertex } from '#core/scene-graph'
import type { SnapGuide } from '#core/scene-graph/snap'
import type { UndoManager } from '#core/scene-graph/undo'
import type { TextEditor } from '#core/text/editor'
@ -67,6 +67,26 @@ export interface EditorState {
enteredContainerId: string | null
}
export interface EditorEvents {
'render:requested': (versions: { renderVersion: number; sceneVersion: number }) => void
'repaint:requested': (versions: { renderVersion: number; sceneVersion: number }) => void
'graph:replaced': (graph: SceneGraph) => void
'node:created': (node: SceneNode) => void
'node:updated': (id: string, changes: Partial<SceneNode>) => void
'node:deleted': (id: string) => void
'node:reparented': (nodeId: string, oldParentId: string | null, newParentId: string) => void
'node:reordered': (nodeId: string, parentId: string, index: number) => void
'selection:changed': (selectedIds: string[], previousIds: string[]) => void
'tool:changed': (tool: Tool, previousTool: Tool) => void
'page:changed': (pageId: string, previousPageId: string) => void
'viewport:changed': (
viewport: { panX: number; panY: number; zoom: number },
previous: { panX: number; panY: number; zoom: number }
) => void
}
export type EditorEventName = keyof EditorEvents
export interface EditorOptions {
graph?: SceneGraph
state?: EditorState
@ -87,6 +107,12 @@ export interface EditorContext {
getTextEditor: () => TextEditor | null
requestRender: () => void
requestRepaint: () => void
emitEditorEvent: <K extends EditorEventName>(
event: K,
...args: Parameters<EditorEvents[K]>
) => void
setSelectedIds: (ids: Set<string>) => void
setActiveTool: (tool: Tool) => void
runLayoutForNode: (id: string) => void
subscribeToGraph: () => void
}

View file

@ -66,11 +66,11 @@ export function createUndoActions(ctx: EditorContext) {
)
ctx.runLayoutForNode(id)
}
ctx.state.selectedIds = new Set(nextSelection)
ctx.setSelectedIds(new Set(nextSelection))
},
inverse: () => {
for (const id of rootIds.toReversed()) ctx.graph.deleteNode(id)
ctx.state.selectedIds = new Set(previousSelection)
ctx.setSelectedIds(new Set(previousSelection))
}
})
}

View file

@ -4,6 +4,17 @@ import { computeBounds, computeAbsoluteBounds } from '#core/geometry'
import type { EditorContext } from './types'
export function createViewportActions(ctx: EditorContext) {
function currentViewport() {
return { panX: ctx.state.panX, panY: ctx.state.panY, zoom: ctx.state.zoom }
}
function emitViewportChanged(previous: ReturnType<typeof currentViewport>) {
const next = currentViewport()
if (next.panX !== previous.panX || next.panY !== previous.panY || next.zoom !== previous.zoom) {
ctx.emitEditorEvent('viewport:changed', next, previous)
}
}
function screenToCanvas(sx: number, sy: number) {
return {
x: (sx - ctx.state.panX) / ctx.state.zoom,
@ -12,11 +23,13 @@ export function createViewportActions(ctx: EditorContext) {
}
function setZoomAroundPoint(level: number, centerX: number, centerY: number) {
const previous = currentViewport()
const newZoom = Math.max(0.02, Math.min(256, level))
ctx.state.panX = centerX - (centerX - ctx.state.panX) * (newZoom / ctx.state.zoom)
ctx.state.panY = centerY - (centerY - ctx.state.panY) * (newZoom / ctx.state.zoom)
ctx.state.zoom = newZoom
ctx.requestRepaint()
emitViewportChanged(previous)
}
function applyZoom(delta: number, centerX: number, centerY: number) {
@ -28,12 +41,15 @@ export function createViewportActions(ctx: EditorContext) {
}
function pan(dx: number, dy: number) {
const previous = currentViewport()
ctx.state.panX += dx
ctx.state.panY += dy
ctx.requestRepaint()
emitViewportChanged(previous)
}
function zoomToBounds(minX: number, minY: number, maxX: number, maxY: number) {
const previous = currentViewport()
const padding = 80
const w = maxX - minX + padding * 2
const h = maxY - minY + padding * 2
@ -45,6 +61,7 @@ export function createViewportActions(ctx: EditorContext) {
ctx.state.panX = (viewW - w * zoom) / 2 - minX * zoom + padding * zoom
ctx.state.panY = (viewH - h * zoom) / 2 - minY * zoom + padding * zoom
ctx.requestRepaint()
emitViewportChanged(previous)
}
function zoomToFit() {
@ -60,10 +77,12 @@ export function createViewportActions(ctx: EditorContext) {
const centerX = (-ctx.state.panX + viewW / 2) / ctx.state.zoom
const centerY = (-ctx.state.panY + viewH / 2) / ctx.state.zoom
const previous = currentViewport()
ctx.state.zoom = Math.max(0.02, Math.min(256, level))
ctx.state.panX = viewW / 2 - centerX
ctx.state.panY = viewH / 2 - centerY
ctx.requestRepaint()
emitViewportChanged(previous)
}
function zoomTo100() {

View file

@ -0,0 +1,12 @@
import { onScopeDispose } from 'vue'
import type { EditorEventName, EditorEvents } from '@open-pencil/core/editor'
import { useEditor } from '#vue/editor/context'
export function useEditorEvent<K extends EditorEventName>(event: K, handler: EditorEvents[K]) {
const editor = useEditor()
const stop = editor.onEditorEvent(event, handler)
onScopeDispose(stop)
return stop
}

View file

@ -2,6 +2,8 @@ export type {
Editor,
EditorState,
EditorOptions,
EditorEvents,
EditorEventName,
Tool,
EditorToolDef
} from '@open-pencil/core/editor'
@ -27,6 +29,7 @@ export { useNodeProps, MIXED } from '#vue/controls/node-props/use'
export type { MixedValue } from '#vue/controls/node-props/use'
export { useSceneComputed } from '#vue/internal/scene-computed/use'
export { useSelectionState } from '#vue/editor/selection-state/use'
export { useEditorEvent } from '#vue/editor/events/use'
export { useSelectionCapabilities } from '#vue/editor/selection-capabilities/use'
/** Command and menu composition helpers. */

View file

@ -4,7 +4,7 @@ import type { SceneGraph, SceneNode } from '@open-pencil/core/scene-graph'
export async function applyImportedDocument(editor: Editor, imported: SceneGraph) {
editor.replaceGraph(imported)
editor.undo.clear()
editor.state.selectedIds = new Set()
editor.clearSelection()
const firstPage = editor.graph.getPages()[0] as SceneNode | undefined
const pageId = firstPage?.id ?? editor.graph.rootId
await editor.switchPage(pageId)

View file

@ -23,7 +23,7 @@ export function restoreReloadState(
state: EditorState,
snapshot: ReloadStateSnapshot
) {
state.selectedIds = new Set()
editor.clearSelection()
if (editor.graph.getNode(snapshot.pageId)) {
state.currentPageId = snapshot.pageId
} else {

View file

@ -15,7 +15,7 @@ export function createPenActions(editor: Editor, graph: SceneGraph, state: PenSt
if (state.penState && tool !== 'PEN' && tool !== 'HAND') {
editor.penCommit(false)
}
state.activeTool = tool
editor.setTool(tool)
}
function penResumeOnPath(nodeId: string) {
@ -29,8 +29,8 @@ export function createPenActions(editor: Editor, graph: SceneGraph, state: PenSt
)
graph.deleteNode(nodeId)
state.selectedIds = new Set()
state.activeTool = 'PEN'
editor.clearSelection()
editor.setTool('PEN')
editor.requestRender()
}
@ -49,8 +49,8 @@ export function createPenActions(editor: Editor, graph: SceneGraph, state: PenSt
state.penState = createResumedPenState(node, orderedVertices, orderedSegments)
graph.deleteNode(nodeId)
state.selectedIds = new Set()
state.activeTool = 'PEN'
editor.clearSelection()
editor.setTool('PEN')
editor.requestRender()
}

View file

@ -71,7 +71,7 @@ export function createVectorEditLifecycle(
hoveredHandleInfo: null
}
state.selectedIds = new Set([nodeId])
editor.select([nodeId])
editor.requestRender()
}

View file

@ -116,7 +116,7 @@ export async function openFileInNewTab(
store.replaceGraph(imported)
store.undo.clear()
store.setDocumentSource(file.name, sourceFormat, handle, path)
store.state.selectedIds = new Set()
store.clearSelection()
const pageId = store.graph.getPages()[0]?.id ?? store.graph.rootId
await store.switchPage(pageId)
await store.fitCurrentPageToViewport()