Fix review issues in undo and Vue SDK
This commit is contained in:
parent
bff8895775
commit
80a2855bb3
|
|
@ -51,6 +51,7 @@ export function createClipboardActions(ctx: EditorContext) {
|
|||
const topLevel = selectedNodes.filter((n) => !n.parentId || !selectedSet.has(n.parentId))
|
||||
|
||||
const newRootIds: string[] = []
|
||||
const rootSnapshots: SceneNode[] = []
|
||||
|
||||
for (const node of topLevel) {
|
||||
const parentId = node.parentId ?? ctx.state.currentPageId
|
||||
|
|
@ -59,26 +60,56 @@ export function createClipboardActions(ctx: EditorContext) {
|
|||
x: node.x + 20,
|
||||
y: node.y + 20
|
||||
})
|
||||
if (clone) newRootIds.push(clone.id)
|
||||
if (!clone) continue
|
||||
newRootIds.push(clone.id)
|
||||
const snapshot = ctx.graph.getNode(clone.id)
|
||||
if (snapshot) rootSnapshots.push(structuredClone(snapshot))
|
||||
}
|
||||
|
||||
const cloneSnapshotTree = (snapshot: SceneNode): SceneNode => {
|
||||
const cloned = structuredClone(snapshot)
|
||||
cloned.childIds = snapshot.childIds
|
||||
.map((childId) => ctx.graph.getNode(childId))
|
||||
.filter((child): child is SceneNode => child != null)
|
||||
.map((child) => cloneSnapshotTree(child))
|
||||
.map((child) => child.id)
|
||||
return cloned
|
||||
}
|
||||
|
||||
for (let i = 0; i < rootSnapshots.length; i++) {
|
||||
rootSnapshots[i] = cloneSnapshotTree(rootSnapshots[i])
|
||||
}
|
||||
|
||||
const snapshotIndex = new Map<string, SceneNode>()
|
||||
for (const snapshot of rootSnapshots) {
|
||||
snapshotIndex.set(snapshot.id, snapshot)
|
||||
}
|
||||
|
||||
const restoreTree = (snapshot: SceneNode, parentId: string) => {
|
||||
const { id: _snapshotId, parentId: _snapshotParentId, childIds, ...rest } = snapshot
|
||||
const created = ctx.graph.createNode(snapshot.type, parentId, rest)
|
||||
for (const childId of childIds) {
|
||||
const child = snapshotIndex.get(childId)
|
||||
if (!child) continue
|
||||
restoreTree(child, created.id)
|
||||
}
|
||||
return created.id
|
||||
}
|
||||
|
||||
if (newRootIds.length > 0) {
|
||||
const allCloned = collectSubtrees(ctx.graph, newRootIds)
|
||||
const pageId = ctx.state.currentPageId
|
||||
ctx.state.selectedIds = new Set(newRootIds)
|
||||
ctx.undo.push({
|
||||
label: 'Duplicate',
|
||||
forward: () => {
|
||||
for (const snapshot of allCloned) {
|
||||
ctx.graph.createNode(snapshot.type, snapshot.parentId ?? pageId, {
|
||||
...snapshot,
|
||||
childIds: []
|
||||
})
|
||||
const restoredRootIds: string[] = []
|
||||
for (const snapshot of rootSnapshots) {
|
||||
const parentId = snapshot.parentId ?? ctx.state.currentPageId
|
||||
restoredRootIds.push(restoreTree(snapshot, parentId))
|
||||
}
|
||||
ctx.state.selectedIds = new Set(newRootIds)
|
||||
ctx.state.selectedIds = new Set(restoredRootIds)
|
||||
},
|
||||
inverse: () => {
|
||||
for (const id of newRootIds) ctx.graph.deleteNode(id)
|
||||
for (const id of [...ctx.state.selectedIds].reverse()) ctx.graph.deleteNode(id)
|
||||
ctx.state.selectedIds = prevSelection
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -131,26 +131,32 @@ export function createUndoActions(ctx: EditorContext) {
|
|||
}
|
||||
|
||||
function restorePageFromSnapshot(snapshot: Map<string, SceneNode>) {
|
||||
const page = ctx.graph.getNode(ctx.state.currentPageId)
|
||||
if (!page) return
|
||||
const pageId = ctx.state.currentPageId
|
||||
const page = ctx.graph.getNode(pageId)
|
||||
const pageSnap = snapshot.get(pageId)
|
||||
if (!page || !pageSnap) return
|
||||
|
||||
for (const childId of page.childIds.slice()) {
|
||||
ctx.graph.deleteNode(childId)
|
||||
}
|
||||
|
||||
const pageSnap = snapshot.get(ctx.state.currentPageId)
|
||||
if (pageSnap) page.childIds = [...pageSnap.childIds]
|
||||
|
||||
for (const [id, snap] of snapshot) {
|
||||
if (id === ctx.state.currentPageId) continue
|
||||
ctx.graph.nodes.set(id, structuredClone(snap))
|
||||
const restoreChildren = (parentId: string, childIds: string[]) => {
|
||||
for (const childId of childIds) {
|
||||
const snap = snapshot.get(childId)
|
||||
if (!snap) continue
|
||||
const { id: _snapId, parentId: _snapParentId, childIds: snapChildIds, ...rest } = snap
|
||||
const restored = ctx.graph.createNode(snap.type, parentId, rest)
|
||||
ctx.graph.reorderChild(restored.id, parentId, childIds.indexOf(childId))
|
||||
restoreChildren(restored.id, snapChildIds)
|
||||
}
|
||||
}
|
||||
|
||||
restoreChildren(pageId, pageSnap.childIds)
|
||||
|
||||
ctx.graph.clearAbsPosCache()
|
||||
computeAllLayouts(ctx.graph, ctx.state.currentPageId)
|
||||
computeAllLayouts(ctx.graph, pageId)
|
||||
ctx.state.selectedIds = new Set()
|
||||
ctx.state.hoveredNodeId = null
|
||||
ctx.graph.emitter.emit('node:reordered', ctx.state.currentPageId, ctx.graph.rootId, 0)
|
||||
ctx.requestRender()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { computed } from 'vue'
|
||||
|
||||
import { useEditor } from '@open-pencil/vue/context/editorContext'
|
||||
import { useNodeProps } from '@open-pencil/vue/controls/useNodeProps'
|
||||
import { providePropertyList } from './context'
|
||||
|
||||
import type { Fill, Stroke, Effect, SceneNode } from '@open-pencil/core'
|
||||
|
|
@ -23,6 +24,7 @@ const emit = defineEmits<{
|
|||
}>()
|
||||
|
||||
const editor = useEditor()
|
||||
const { isArrayMixed } = useNodeProps()
|
||||
|
||||
const selectedNodes = computed(() => editor.getSelectedNodes())
|
||||
const activeNode = computed<SceneNode | null>(
|
||||
|
|
@ -31,17 +33,7 @@ const activeNode = computed<SceneNode | null>(
|
|||
const isMulti = computed(() => selectedNodes.value.length > 1)
|
||||
const active = computed(() => selectedNodes.value.length > 0)
|
||||
|
||||
const isMixed = computed(() => {
|
||||
const all = selectedNodes.value
|
||||
if (all.length <= 1) return false
|
||||
const firstArr = all[0][propKey] as unknown[]
|
||||
for (let i = 1; i < all.length; i++) {
|
||||
const arr = all[i][propKey] as unknown[]
|
||||
if (arr.length !== firstArr.length) return true
|
||||
}
|
||||
const first = JSON.stringify(firstArr)
|
||||
return all.some((n) => JSON.stringify(n[propKey]) !== first)
|
||||
})
|
||||
const isMixed = computed(() => isArrayMixed(propKey))
|
||||
|
||||
const items = computed(() => {
|
||||
if (isMixed.value) return []
|
||||
|
|
|
|||
|
|
@ -169,7 +169,10 @@ export function useEditorCommands() {
|
|||
id: 'selection.moveToPage',
|
||||
label: 'Move to page',
|
||||
enabled: capabilities.canMoveToPage,
|
||||
run: () => {}
|
||||
run: () => {
|
||||
const targetPage = otherPages.value[0]
|
||||
if (targetPage) moveSelectionToPage(targetPage.id)
|
||||
}
|
||||
},
|
||||
'view.zoom100': {
|
||||
id: 'view.zoom100',
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
import { computed } from 'vue'
|
||||
|
||||
import { useEditor } from '@open-pencil/vue/context/editorContext'
|
||||
import { MIXED } from '@open-pencil/vue/controls/useNodeProps'
|
||||
import { usePropScrub } from '@open-pencil/vue/controls/usePropScrub'
|
||||
import { useSceneComputed } from '@open-pencil/vue/internal/useSceneComputed'
|
||||
import { MIXED, useNodeProps } from '@open-pencil/vue/controls/useNodeProps'
|
||||
|
||||
import type { SceneNode } from '@open-pencil/core'
|
||||
import type { MixedValue } from '@open-pencil/vue/controls/useNodeProps'
|
||||
|
||||
const CORNER_RADIUS_TYPES = new Set([
|
||||
'RECTANGLE',
|
||||
|
|
@ -18,21 +15,7 @@ const CORNER_RADIUS_TYPES = new Set([
|
|||
|
||||
export function useAppearance() {
|
||||
const editor = useEditor()
|
||||
|
||||
const nodes = useSceneComputed(() => editor.getSelectedNodes())
|
||||
const node = useSceneComputed<SceneNode | null>(() => editor.getSelectedNode() ?? null)
|
||||
const active = computed(() => nodes.value.length > 0)
|
||||
const isMulti = computed(() => nodes.value.length > 1)
|
||||
|
||||
function merged<K extends keyof SceneNode>(key: K): MixedValue<SceneNode[K]> {
|
||||
const all = nodes.value
|
||||
if (all.length === 0) return MIXED
|
||||
const first = all[0][key]
|
||||
for (let i = 1; i < all.length; i++) {
|
||||
if (all[i][key] !== first) return MIXED
|
||||
}
|
||||
return first
|
||||
}
|
||||
const { nodes, node, active, isMulti, merged, updateProp, commitProp } = useNodeProps()
|
||||
|
||||
const hasCornerRadius = computed(() => {
|
||||
if (isMulti.value) return nodes.value.every((n) => CORNER_RADIUS_TYPES.has(n.type))
|
||||
|
|
@ -60,16 +43,6 @@ export function useAppearance() {
|
|||
return v ? 'visible' : 'hidden'
|
||||
})
|
||||
|
||||
const { updateProp: _updateProp, commitProp: _commitProp } = usePropScrub(editor)
|
||||
|
||||
function updateProp(key: string, value: number | string) {
|
||||
_updateProp(nodes.value, key, value)
|
||||
}
|
||||
|
||||
function commitProp(key: string, value: number | string, previous: number | string) {
|
||||
_commitProp(nodes.value, key, value, previous)
|
||||
}
|
||||
|
||||
function toggleVisibility() {
|
||||
if (isMulti.value) {
|
||||
const allVisible = nodes.value.every((n) => n.visible)
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ import { computed } from 'vue'
|
|||
import { useEditor } from '@open-pencil/vue/context/editorContext'
|
||||
import { useSceneComputed } from '@open-pencil/vue/internal/useSceneComputed'
|
||||
|
||||
import type { Fill, SceneNode, Stroke } from '@open-pencil/core'
|
||||
import type { Effect, Fill, SceneNode, Stroke } from '@open-pencil/core'
|
||||
|
||||
export const MIXED = Symbol('mixed')
|
||||
export type MixedValue<T> = T | typeof MIXED
|
||||
|
||||
type ArrayItem = Fill | Stroke | Effect | Record<string, unknown>
|
||||
|
||||
export function useNodeProps() {
|
||||
const store = useEditor()
|
||||
const node = useSceneComputed(() => store.getSelectedNode() ?? null)
|
||||
|
|
@ -26,6 +28,46 @@ export function useNodeProps() {
|
|||
return first
|
||||
}
|
||||
|
||||
function areArrayItemsEqual(a: ArrayItem, b: ArrayItem): boolean {
|
||||
if (a === b) return true
|
||||
const aKeys = Object.keys(a)
|
||||
const bKeys = Object.keys(b)
|
||||
if (aKeys.length !== bKeys.length) return false
|
||||
for (const key of aKeys) {
|
||||
const aValue = a[key as keyof typeof a]
|
||||
const bValue = b[key as keyof typeof b]
|
||||
if (Array.isArray(aValue) && Array.isArray(bValue)) {
|
||||
if (aValue.length !== bValue.length) return false
|
||||
for (let i = 0; i < aValue.length; i++) {
|
||||
const left = aValue[i]
|
||||
const right = bValue[i]
|
||||
if (
|
||||
typeof left === 'object' &&
|
||||
left != null &&
|
||||
typeof right === 'object' &&
|
||||
right != null
|
||||
) {
|
||||
if (!areArrayItemsEqual(left as ArrayItem, right as ArrayItem)) return false
|
||||
} else if (left !== right) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (
|
||||
typeof aValue === 'object' &&
|
||||
aValue != null &&
|
||||
typeof bValue === 'object' &&
|
||||
bValue != null
|
||||
) {
|
||||
if (!areArrayItemsEqual(aValue as ArrayItem, bValue as ArrayItem)) return false
|
||||
continue
|
||||
}
|
||||
if (aValue !== bValue) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function prop<K extends keyof SceneNode>(key: K) {
|
||||
return computed(() => merged(key))
|
||||
}
|
||||
|
|
@ -39,8 +81,27 @@ export function useNodeProps() {
|
|||
function isArrayMixed(key: keyof SceneNode): boolean {
|
||||
const all = nodes.value
|
||||
if (all.length <= 1) return false
|
||||
const first = JSON.stringify(all[0][key])
|
||||
return all.some((n) => JSON.stringify(n[key]) !== first)
|
||||
const first = all[0][key]
|
||||
if (!Array.isArray(first)) return all.some((n) => n[key] !== first)
|
||||
for (let i = 1; i < all.length; i++) {
|
||||
const current = all[i][key]
|
||||
if (!Array.isArray(current) || current.length !== first.length) return true
|
||||
for (let j = 0; j < first.length; j++) {
|
||||
const left = first[j]
|
||||
const right = current[j]
|
||||
if (
|
||||
typeof left === 'object' &&
|
||||
left != null &&
|
||||
typeof right === 'object' &&
|
||||
right != null
|
||||
) {
|
||||
if (!areArrayItemsEqual(left as ArrayItem, right as ArrayItem)) return true
|
||||
} else if (left !== right) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ArrayPropKey = 'fills' | 'strokes' | 'effects'
|
||||
|
|
|
|||
|
|
@ -1,18 +1,5 @@
|
|||
import { computed, type ComputedRef } from 'vue'
|
||||
|
||||
/**
|
||||
* Creates a computed ref that re-evaluates when the scene graph changes.
|
||||
*
|
||||
* Tracks `state.sceneVersion` — a counter on a shallowReactive object
|
||||
* that the core editor increments synchronously on every mutation via
|
||||
* requestRender(). Vue tracks the read automatically, so the computed
|
||||
* re-evaluates in the same tick as the change. Zero latency.
|
||||
*/
|
||||
export function useSceneComputed<T>(fn: () => T, sceneVersion?: () => number): ComputedRef<T> {
|
||||
return computed(() => {
|
||||
if (sceneVersion) {
|
||||
void sceneVersion()
|
||||
}
|
||||
return fn()
|
||||
})
|
||||
export function useSceneComputed<T>(fn: () => T): ComputedRef<T> {
|
||||
return computed(fn)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { computed } from 'vue'
|
||||
|
||||
import { useSceneComputed } from '@open-pencil/vue/internal/useSceneComputed'
|
||||
import { useSelectionState } from '@open-pencil/vue/selection/useSelectionState'
|
||||
|
||||
export function useSelectionCapabilities() {
|
||||
|
|
@ -30,9 +31,11 @@ export function useSelectionCapabilities() {
|
|||
const canToggleLock = computed(() => hasSelection.value)
|
||||
const canGoToMainComponent = computed(() => isInstance.value)
|
||||
const canCreateInstance = computed(() => selectedNode.value?.type === 'COMPONENT')
|
||||
const canMoveToPage = computed(() => hasSelection.value && editor.graph.getPages().length > 1)
|
||||
const canMoveToPage = useSceneComputed(
|
||||
() => hasSelection.value && editor.graph.getPages().length > 1
|
||||
)
|
||||
const canPaste = computed(() => true)
|
||||
const canSelectAll = computed(
|
||||
const canSelectAll = useSceneComputed(
|
||||
() => editor.graph.getChildren(editor.state.currentPageId).length > 0
|
||||
)
|
||||
const canUndo = computed(() => editor.undo.canUndo)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export function useSelectionState() {
|
|||
const isComponent = computed(() => selectedNodeType.value === 'COMPONENT')
|
||||
const isGroup = computed(() => selectedNodeType.value === 'GROUP')
|
||||
|
||||
const canCreateComponentSet = computed(() => {
|
||||
const canCreateComponentSet = useSceneComputed(() => {
|
||||
if (selectedIds.value.size < 2) return false
|
||||
for (const id of selectedIds.value) {
|
||||
if (editor.graph.getNode(id)?.type !== 'COMPONENT') return false
|
||||
|
|
|
|||
|
|
@ -201,6 +201,7 @@ export function useCanvas(
|
|||
|
||||
const surface = makeGLSurface(canvas)
|
||||
if (!surface) {
|
||||
console.warn('Falling back to full surface recreation after resize')
|
||||
createSurface(canvas)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,14 +20,12 @@ function toggleFormat() {
|
|||
jsxFormat.value = jsxFormat.value === 'openpencil' ? 'tailwind' : 'openpencil'
|
||||
}
|
||||
|
||||
const jsxCode = useSceneComputed(
|
||||
() => {
|
||||
const ids = [...store.state.selectedIds]
|
||||
if (ids.length === 0) return ''
|
||||
return selectionToJSX(ids, store.graph, jsxFormat.value)
|
||||
},
|
||||
() => store.state.sceneVersion
|
||||
)
|
||||
const jsxCode = useSceneComputed(() => {
|
||||
void store.state.sceneVersion
|
||||
const ids = [...store.state.selectedIds]
|
||||
if (ids.length === 0) return ''
|
||||
return selectionToJSX(ids, store.graph, jsxFormat.value)
|
||||
})
|
||||
|
||||
const highlightedLines = computed(() => {
|
||||
if (!jsxCode.value) return []
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@ import { useEditorStore } from '@/stores/editor'
|
|||
const emit = defineEmits<{ openDialog: [] }>()
|
||||
|
||||
const editor = useEditorStore()
|
||||
const collectionCount = useSceneComputed(
|
||||
() => editor.getCollectionCount(),
|
||||
() => editor.state.sceneVersion
|
||||
)
|
||||
const variableCount = useSceneComputed(
|
||||
() => editor.getVariableCount(),
|
||||
() => editor.state.sceneVersion
|
||||
)
|
||||
const collectionCount = useSceneComputed(() => {
|
||||
void editor.state.sceneVersion
|
||||
return editor.getCollectionCount()
|
||||
})
|
||||
const variableCount = useSceneComputed(() => {
|
||||
void editor.state.sceneVersion
|
||||
return editor.getVariableCount()
|
||||
})
|
||||
const hasVariables = computed(() => variableCount.value > 0)
|
||||
</script>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue