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:
parent
dfa9505805
commit
1835903e56
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
12
packages/vue/src/editor/events/use.ts
Normal file
12
packages/vue/src/editor/events/use.ts
Normal 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
|
||||
}
|
||||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export function createVectorEditLifecycle(
|
|||
hoveredHandleInfo: null
|
||||
}
|
||||
|
||||
state.selectedIds = new Set([nodeId])
|
||||
editor.select([nodeId])
|
||||
editor.requestRender()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue