262 lines
7.8 KiB
TypeScript
262 lines
7.8 KiB
TypeScript
import { SceneGraph } from '#core/scene-graph'
|
|
|
|
import type { ExportTarget } from './types'
|
|
|
|
export interface ExtractedGraph {
|
|
graph: SceneGraph
|
|
pageId: string | null
|
|
nodeIds: string[]
|
|
}
|
|
|
|
function cloneIntoGraph(source: SceneGraph, ids: Set<string>): SceneGraph {
|
|
const graph = new SceneGraph()
|
|
graph.rootId = source.rootId
|
|
graph.nodes = new Map()
|
|
graph.images = new Map()
|
|
graph.variables = new Map()
|
|
graph.variableCollections = new Map()
|
|
graph.activeMode = new Map(source.activeMode)
|
|
graph.figKiwiVersion = source.figKiwiVersion
|
|
graph.documentColorSpace = source.documentColorSpace
|
|
|
|
const sortedIds = [...ids].sort((a, b) => {
|
|
if (a === source.rootId) return -1
|
|
if (b === source.rootId) return 1
|
|
const aNode = source.getNode(a)
|
|
const bNode = source.getNode(b)
|
|
const aDepth = depthOf(source, aNode)
|
|
const bDepth = depthOf(source, bNode)
|
|
return aDepth - bDepth
|
|
})
|
|
|
|
for (const id of sortedIds) {
|
|
const node = source.getNode(id)
|
|
if (!node) continue
|
|
graph.nodes.set(id, structuredClone(node))
|
|
}
|
|
|
|
const rootClone = graph.getNode(source.rootId)
|
|
if (rootClone) {
|
|
rootClone.parentId = null
|
|
rootClone.childIds = rootClone.childIds.filter((id) => ids.has(id))
|
|
}
|
|
|
|
for (const id of sortedIds) {
|
|
if (id === source.rootId) continue
|
|
const node = graph.getNode(id)
|
|
if (!node) continue
|
|
if (!node.parentId || !ids.has(node.parentId)) {
|
|
node.parentId = source.rootId
|
|
}
|
|
node.childIds = node.childIds.filter((childId) => ids.has(childId))
|
|
}
|
|
|
|
const { imageHashes, variableIds } = collectReferencedResources(source, graph)
|
|
|
|
for (const imageHash of imageHashes) {
|
|
const image = source.images.get(imageHash)
|
|
if (image) graph.images.set(imageHash, image)
|
|
}
|
|
|
|
for (const variableId of variableIds) {
|
|
const variable = source.variables.get(variableId)
|
|
if (!variable) continue
|
|
graph.variables.set(variableId, structuredClone(variable))
|
|
const collection = source.variableCollections.get(variable.collectionId)
|
|
if (!collection) continue
|
|
const existing = graph.variableCollections.get(collection.id)
|
|
if (!existing) {
|
|
graph.variableCollections.set(collection.id, {
|
|
...structuredClone(collection),
|
|
variableIds: []
|
|
})
|
|
}
|
|
graph.variableCollections.get(collection.id)?.variableIds.push(variableId)
|
|
}
|
|
|
|
graph.clearAbsPosCache()
|
|
return graph
|
|
}
|
|
|
|
function collectReferencedResources(source: SceneGraph, graph: SceneGraph) {
|
|
const imageHashes = new Set<string>()
|
|
const variableIds = new Set<string>()
|
|
for (const node of graph.nodes.values()) {
|
|
for (const fill of node.fills) {
|
|
if (fill.type === 'IMAGE' && fill.imageHash) imageHashes.add(fill.imageHash)
|
|
}
|
|
for (const variableId of Object.values(node.boundVariables)) {
|
|
collectVariableClosure(source, variableId, variableIds)
|
|
}
|
|
}
|
|
return { imageHashes, variableIds }
|
|
}
|
|
|
|
function collectVariableClosure(source: SceneGraph, variableId: string, out: Set<string>) {
|
|
if (out.has(variableId)) return
|
|
const variable = source.variables.get(variableId)
|
|
if (!variable) return
|
|
out.add(variableId)
|
|
for (const value of Object.values(variable.valuesByMode)) {
|
|
if (typeof value === 'object' && 'aliasId' in value) {
|
|
collectVariableClosure(source, value.aliasId, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
function depthOf(source: SceneGraph, node: ReturnType<SceneGraph['getNode']>): number {
|
|
let depth = 0
|
|
let current = node
|
|
while (current?.parentId) {
|
|
depth += 1
|
|
current = source.getNode(current.parentId)
|
|
}
|
|
return depth
|
|
}
|
|
|
|
function collectDescendants(source: SceneGraph, id: string, out: Set<string>) {
|
|
if (out.has(id)) return
|
|
out.add(id)
|
|
const node = source.getNode(id)
|
|
if (!node) return
|
|
for (const childId of node.childIds) {
|
|
collectDescendants(source, childId, out)
|
|
}
|
|
}
|
|
|
|
function collectAncestors(source: SceneGraph, id: string, out: Set<string>) {
|
|
let current = source.getNode(id)
|
|
while (current?.parentId) {
|
|
out.add(current.parentId)
|
|
current = source.getNode(current.parentId)
|
|
}
|
|
}
|
|
|
|
function resolveInstanceComponentId(source: SceneGraph, componentId: string): string {
|
|
const seen = new Set<string>()
|
|
let currentId = componentId
|
|
while (!seen.has(currentId)) {
|
|
seen.add(currentId)
|
|
const node = source.getNode(currentId)
|
|
if (node?.type !== 'INSTANCE' || !node.componentId) return currentId
|
|
currentId = node.componentId
|
|
}
|
|
return componentId
|
|
}
|
|
|
|
function collectComponentDependencies(source: SceneGraph, ids: Set<string>) {
|
|
let changed = true
|
|
while (changed) {
|
|
changed = false
|
|
for (const id of Array.from(ids)) {
|
|
const node = source.getNode(id)
|
|
if (node?.type !== 'INSTANCE' || !node.componentId) continue
|
|
const componentId = resolveInstanceComponentId(source, node.componentId)
|
|
if (ids.has(componentId)) continue
|
|
const before = ids.size
|
|
collectAncestors(source, componentId, ids)
|
|
collectDescendants(source, componentId, ids)
|
|
changed ||= ids.size !== before
|
|
}
|
|
}
|
|
}
|
|
|
|
export function findPageId(source: SceneGraph, nodeId: string): string | null {
|
|
let current = source.getNode(nodeId)
|
|
while (current?.parentId) {
|
|
const parent = source.getNode(current.parentId)
|
|
if (!parent) return null
|
|
if (parent.type === 'CANVAS') return parent.id
|
|
current = parent
|
|
}
|
|
return current?.type === 'CANVAS' ? current.id : null
|
|
}
|
|
|
|
function ancestorChain(source: SceneGraph, id: string): string[] {
|
|
const chain: string[] = []
|
|
let current = source.getNode(id)
|
|
while (current?.parentId) {
|
|
chain.push(current.parentId)
|
|
current = source.getNode(current.parentId)
|
|
}
|
|
return chain.reverse()
|
|
}
|
|
|
|
function collectSelectionIds(source: SceneGraph, nodeIds: string[]): Set<string> {
|
|
const ids = new Set<string>([source.rootId])
|
|
const pageIds = new Set<string>()
|
|
|
|
for (const nodeId of nodeIds) {
|
|
const node = source.getNode(nodeId)
|
|
if (!node) continue
|
|
for (const ancestorId of ancestorChain(source, nodeId)) {
|
|
ids.add(ancestorId)
|
|
const ancestor = source.getNode(ancestorId)
|
|
if (ancestor?.type === 'CANVAS') pageIds.add(ancestorId)
|
|
}
|
|
collectDescendants(source, nodeId, ids)
|
|
}
|
|
|
|
for (const pageId of pageIds) {
|
|
ids.add(pageId)
|
|
}
|
|
|
|
collectComponentDependencies(source, ids)
|
|
return ids
|
|
}
|
|
|
|
function pageNodeIds(source: SceneGraph, pageId: string): Set<string> {
|
|
const ids = new Set<string>([source.rootId])
|
|
collectDescendants(source, pageId, ids)
|
|
collectComponentDependencies(source, ids)
|
|
return ids
|
|
}
|
|
|
|
function rootNodeIds(source: SceneGraph): Set<string> {
|
|
const ids = new Set<string>()
|
|
for (const node of source.nodes.values()) {
|
|
ids.add(node.id)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
export function extractExportGraph(source: SceneGraph, target: ExportTarget): ExtractedGraph {
|
|
switch (target.scope) {
|
|
case 'document': {
|
|
const graph = cloneIntoGraph(source, rootNodeIds(source))
|
|
return {
|
|
graph,
|
|
pageId: graph.getPages()[0]?.id ?? null,
|
|
nodeIds: graph.getPages()[0]?.childIds ?? []
|
|
}
|
|
}
|
|
case 'page': {
|
|
const graph = cloneIntoGraph(source, pageNodeIds(source, target.pageId))
|
|
const page = graph.getNode(target.pageId)
|
|
return {
|
|
graph,
|
|
pageId: page?.id ?? null,
|
|
nodeIds: page?.childIds ?? []
|
|
}
|
|
}
|
|
case 'selection': {
|
|
const graph = cloneIntoGraph(source, collectSelectionIds(source, target.nodeIds))
|
|
const firstId = target.nodeIds[0]
|
|
const first = firstId ? source.getNode(firstId) : undefined
|
|
const pageId = first
|
|
? (ancestorChain(source, first.id).find((id) => source.getNode(id)?.type === 'CANVAS') ??
|
|
null)
|
|
: null
|
|
return {
|
|
graph,
|
|
pageId,
|
|
nodeIds: target.nodeIds.filter((id) => graph.getNode(id) !== undefined)
|
|
}
|
|
}
|
|
case 'node':
|
|
return extractExportGraph(source, { scope: 'selection', nodeIds: [target.nodeId] })
|
|
default:
|
|
return extractExportGraph(source, { scope: 'document' })
|
|
}
|
|
}
|