import type { Position } from './Properties'; import type { SceneObject } from './SceneObject'; /** * Parent/child structure of a scene. `scene.objects` stays a flat list (storage order = * creation order); the tree is implied by `SceneObject.parentId`. Positions are local to * the parent, so the world position is the sum along the chain – there is no rotation or * scale yet, which keeps this trivially invertible. * * The helpers work on anything that holds a flat object list – a scene, or the template * inside a container asset. */ export interface ObjectTree { objects: SceneObject[]; } export function findObject(tree: ObjectTree, id: string): SceneObject | undefined { return tree.objects.find((o) => o.id === id); } export function childrenOf(tree: ObjectTree, parentId: string): SceneObject[] { return tree.objects.filter((o) => o.parentId === parentId); } /** All objects below `id`, depth-first, parents before children. */ export function descendantsOf(tree: ObjectTree, id: string): SceneObject[] { const result: SceneObject[] = []; const visit = (parentId: string) => { for (const child of childrenOf(tree, parentId)) { result.push(child); visit(child.id); } }; visit(id); return result; } /** Whether `ancestorId` is `objectId` itself or one of its ancestors (guards against cycles). */ export function isSelfOrAncestor(tree: ObjectTree, ancestorId: string, objectId: string): boolean { let current: SceneObject | undefined = findObject(tree, objectId); const seen = new Set(); while (current && !seen.has(current.id)) { if (current.id === ancestorId) return true; seen.add(current.id); current = current.parentId ? findObject(tree, current.parentId) : undefined; } return false; } /** * Of the given ids, those that have no ancestor among the ids themselves – the tops of the * selected subtrees. Whole subtrees are what group operations (move, remove, group) act on. */ export function subtreeRoots(tree: ObjectTree, ids: readonly string[]): string[] { const set = new Set(ids); return ids.filter((id) => { let current = findObject(tree, id); const seen = new Set(); while (current?.parentId && !seen.has(current.id)) { seen.add(current.id); if (set.has(current.parentId)) return false; current = findObject(tree, current.parentId); } return true; }); } /** Position of an object in world units (all local positions along the chain summed up). */ export function worldPosition(tree: ObjectTree, object: SceneObject): Position { const result = { ...object.position }; let parent = object.parentId ? findObject(tree, object.parentId) : undefined; const seen = new Set([object.id]); while (parent && !seen.has(parent.id)) { result.x += parent.position.x; result.y += parent.position.y; result.z += parent.position.z; seen.add(parent.id); parent = parent.parentId ? findObject(tree, parent.parentId) : undefined; } return result; } /** The local position that puts an object at `world` under the given parent ('' = top level). */ export function localPositionFor(tree: ObjectTree, parentId: string, world: Position): Position { const parent = parentId ? findObject(tree, parentId) : undefined; if (!parent) return { ...world }; const origin = worldPosition(tree, parent); return { x: world.x - origin.x, y: world.y - origin.y, z: world.z - origin.z }; } export interface TreeEntry { object: SceneObject; depth: number; } /** * Objects in tree order: top-level objects in storage order, each followed by its subtree. * This is the order the hierarchy panel shows and the tie-break order for equal `z`. */ export function treeOrder(tree: ObjectTree): TreeEntry[] { const result: TreeEntry[] = []; const ids = new Set(tree.objects.map((o) => o.id)); const visit = (parentId: string, depth: number) => { for (const object of tree.objects) { if (object.parentId !== parentId) continue; result.push({ object, depth }); visit(object.id, depth + 1); } }; visit('', 0); // Objects whose parent is missing (should not happen after loading) are listed at the top level. for (const object of tree.objects) { if (object.parentId && !ids.has(object.parentId) && !result.some((e) => e.object === object)) result.push({ object, depth: 0 }); } return result; }