Fix review round 2 remaining: duplicate children, JSON.stringify perf

- duplicateSelected: recursive subtree cloning via graph.cloneTree()
  Filters to top-level selection to avoid double-cloning. Children
  get new IDs wired to new parent. Single undo entry.
- PropertyListRoot isMixed: compare array lengths first (fast path),
  only JSON.stringify when lengths match (rare).
- glContext in use-canvas.ts: ownership verified correct, no change.
This commit is contained in:
Danila Poyarkov 2026-03-17 16:58:25 +03:00
parent 4d5b3482d4
commit c01e5ec141
2 changed files with 24 additions and 15 deletions

View file

@ -51,34 +51,38 @@ export function createClipboardActions(ctx: EditorContext) {
function duplicateSelected(selectedNodes: SceneNode[]) {
const prevSelection = new Set(ctx.state.selectedIds)
const newIds: string[] = []
const snapshots: Array<{ id: string; parentId: string; snapshot: SceneNode }> = []
const selectedSet = new Set(selectedNodes.map((n) => n.id))
const topLevel = selectedNodes.filter((n) => !n.parentId || !selectedSet.has(n.parentId))
for (const node of selectedNodes) {
const newRootIds: string[] = []
for (const node of topLevel) {
const parentId = node.parentId ?? ctx.state.currentPageId
const { id: _srcId, parentId: _srcParent, childIds: _srcChildren, ...srcRest } = node
const created = ctx.graph.createNode(node.type, parentId, {
...srcRest,
const clone = ctx.graph.cloneTree(node.id, parentId, {
name: node.name + ' copy',
x: node.x + 20,
y: node.y + 20
})
newIds.push(created.id)
snapshots.push({ id: created.id, parentId, snapshot: { ...created } })
if (clone) newRootIds.push(clone.id)
}
if (newIds.length > 0) {
ctx.state.selectedIds = new Set(newIds)
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, parentId } of snapshots) {
ctx.graph.createNode(snapshot.type, parentId, snapshot)
for (const snapshot of allCloned) {
ctx.graph.createNode(snapshot.type, snapshot.parentId ?? pageId, {
...snapshot,
childIds: []
})
}
ctx.state.selectedIds = new Set(newIds)
ctx.state.selectedIds = new Set(newRootIds)
},
inverse: () => {
for (const { id } of snapshots) ctx.graph.deleteNode(id)
for (const id of newRootIds) ctx.graph.deleteNode(id)
ctx.state.selectedIds = prevSelection
}
})

View file

@ -24,7 +24,12 @@ const active = computed(() => selectedNodes.value.length > 0)
const isMixed = computed(() => {
const all = selectedNodes.value
if (all.length <= 1) return false
const first = JSON.stringify(all[0][propKey])
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)
})