117 lines
4.3 KiB
TypeScript
117 lines
4.3 KiB
TypeScript
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<string>();
|
||
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<string>();
|
||
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<string>([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;
|
||
}
|