* perf(fig): scope lazy page population work - Limit override propagation and clone indexing to active page nodes - Reuse clone membership sets instead of rebuilding them per subtree - Coalesce Layers tree structural rebuilds during population - Add a regression test that rejects document-wide scans * fix(fig): preserve global override propagation - Keep fill and child-placement propagation across clone lineages - Retain scoped work only for passes proven independent of inactive pages - Cover the two required global propagation scans * perf(fig): finish scoped population review - Stream active override candidates without temporary arrays - Scope fallback clone maps when no lineage index is supplied - Skip stale queued Layers tree rebuilds after synchronous refreshes
158 lines
5.1 KiB
TypeScript
158 lines
5.1 KiB
TypeScript
import type { SceneGraph, SceneNode } from '@open-pencil/scene-graph'
|
|
|
|
import { overrideCandidates } from '../utils'
|
|
|
|
interface ChildSourceSnapshot {
|
|
id: string
|
|
path: number[]
|
|
type: SceneNode['type']
|
|
}
|
|
|
|
const refreshedCloneSourceMaps = new WeakSet<Map<string, string[]>>()
|
|
const cloneSourceIdCaches = new WeakMap<Map<string, string[]>, Map<string, Set<string>>>()
|
|
|
|
function cloneSourceIds(cloneSources: Map<string, string[]>): Map<string, Set<string>> {
|
|
let cached = cloneSourceIdCaches.get(cloneSources)
|
|
if (!cached) {
|
|
cached = new Map([...cloneSources].map(([sourceId, cloneIds]) => [sourceId, new Set(cloneIds)]))
|
|
cloneSourceIdCaches.set(cloneSources, cached)
|
|
}
|
|
return cached
|
|
}
|
|
|
|
// SceneGraph.instanceIndex contains INSTANCE nodes only, while generated text,
|
|
// frame, and vector descendants also use componentId as clone provenance.
|
|
function refreshCloneSources(
|
|
graph: SceneGraph,
|
|
cloneSources: Map<string, string[]>,
|
|
activeNodeIds?: Set<string>
|
|
): void {
|
|
if (refreshedCloneSourceMaps.has(cloneSources)) return
|
|
const knownIds = cloneSourceIds(cloneSources)
|
|
for (const node of overrideCandidates(graph, activeNodeIds)) {
|
|
if (!node.componentId) continue
|
|
let known = knownIds.get(node.componentId)
|
|
if (!known) {
|
|
known = new Set()
|
|
knownIds.set(node.componentId, known)
|
|
cloneSources.set(node.componentId, [])
|
|
}
|
|
if (known.has(node.id)) continue
|
|
known.add(node.id)
|
|
cloneSources.get(node.componentId)?.push(node.id)
|
|
}
|
|
refreshedCloneSourceMaps.add(cloneSources)
|
|
}
|
|
|
|
export function indexCloneNodes(
|
|
graph: SceneGraph,
|
|
nodeIds: Iterable<string>,
|
|
cloneSources: Map<string, string[]>
|
|
): void {
|
|
const knownIds = cloneSourceIds(cloneSources)
|
|
for (const nodeId of nodeIds) {
|
|
const node = graph.getNode(nodeId)
|
|
if (!node?.componentId) continue
|
|
let known = knownIds.get(node.componentId)
|
|
if (!known) {
|
|
known = new Set()
|
|
knownIds.set(node.componentId, known)
|
|
}
|
|
if (known.has(node.id)) continue
|
|
known.add(node.id)
|
|
const clones = cloneSources.get(node.componentId)
|
|
if (clones) clones.push(node.id)
|
|
else cloneSources.set(node.componentId, [node.id])
|
|
}
|
|
}
|
|
|
|
export function indexCloneSubtree(
|
|
graph: SceneGraph,
|
|
rootId: string,
|
|
cloneSources: Map<string, string[]>
|
|
): void {
|
|
const nodeIds: string[] = []
|
|
const queue = [rootId]
|
|
let index = 0
|
|
while (index < queue.length) {
|
|
const node = graph.getNode(queue[index])
|
|
index++
|
|
if (!node) continue
|
|
nodeIds.push(node.id)
|
|
queue.push(...node.childIds)
|
|
}
|
|
indexCloneNodes(graph, nodeIds, cloneSources)
|
|
}
|
|
|
|
/** Capture clone-source identities by path before a populated branch is replaced. */
|
|
export function snapshotChildSources(graph: SceneGraph, parentId: string): ChildSourceSnapshot[] {
|
|
const result: ChildSourceSnapshot[] = []
|
|
const parent = graph.getNode(parentId)
|
|
if (!parent) return result
|
|
|
|
const visit = (nodeId: string, path: number[]) => {
|
|
const node = graph.getNode(nodeId)
|
|
if (!node) return
|
|
result.push({ id: node.id, path, type: node.type })
|
|
node.childIds.forEach((childId, index) => visit(childId, [...path, index]))
|
|
}
|
|
parent.childIds.forEach((childId, index) => visit(childId, [index]))
|
|
return result
|
|
}
|
|
|
|
function resolveChildPath(graph: SceneGraph, parentId: string, path: number[]): SceneNode | null {
|
|
let node = graph.getNode(parentId)
|
|
if (!node) return null
|
|
for (const index of path) {
|
|
const childId = node.childIds[index]
|
|
if (!childId) return null
|
|
node = graph.getNode(childId)
|
|
if (!node) return null
|
|
}
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* Redirect descendants that cloned the removed branch to its structural
|
|
* replacements. Without this, deep instances keep componentId references to
|
|
* deleted nodes and miss later text, visibility, and geometry synchronization.
|
|
*/
|
|
export function remapRepopulatedChildSources(
|
|
graph: SceneGraph,
|
|
parentId: string,
|
|
previousSources: ChildSourceSnapshot[],
|
|
cloneSources?: Map<string, string[]>,
|
|
activeNodeIds?: Set<string>
|
|
): void {
|
|
if (cloneSources) refreshCloneSources(graph, cloneSources, activeNodeIds)
|
|
for (const previous of previousSources) {
|
|
const replacement = resolveChildPath(graph, parentId, previous.path)
|
|
if (!replacement || replacement.type !== previous.type) continue
|
|
const cloneIds = new Set([
|
|
...(cloneSources?.get(previous.id) ?? []),
|
|
...(graph.instanceIndex.get(previous.id) ?? [])
|
|
])
|
|
for (const cloneId of cloneIds) {
|
|
const clone = graph.getNode(cloneId)
|
|
if (clone?.componentId !== previous.id) continue
|
|
graph.updateNode(cloneId, { componentId: replacement.id })
|
|
if (cloneSources) {
|
|
let replacements = cloneSources.get(replacement.id)
|
|
if (!replacements) {
|
|
replacements = []
|
|
cloneSources.set(replacement.id, replacements)
|
|
}
|
|
if (!replacements.includes(cloneId)) {
|
|
replacements.push(cloneId)
|
|
let known = cloneSourceIds(cloneSources).get(replacement.id)
|
|
if (!known) {
|
|
known = new Set()
|
|
cloneSourceIds(cloneSources).set(replacement.id, known)
|
|
}
|
|
known.add(cloneId)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|