Scene hierarchy: parentId with local positions, tree panel, reparenting and Parent node
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
551a50ffc9
commit
26d5edc496
22 files changed
+729
-166
No files matched your search
@@ -10,24 +10,28 @@ export interface SceneObject {
|
||||
assetId: string;
|
||||
/** Optional display name; falls back to the asset name when empty. */
|
||||
name: string;
|
||||
/** World position plus draw order (`z`). */
|
||||
/**
|
||||
* Position **relative to the parent** (world position when there is no parent) plus the
|
||||
* draw order `z`, which is relative as well. See `hierarchy.ts` for world coordinates.
|
||||
*/
|
||||
position: Position;
|
||||
overrides: PropertyBag;
|
||||
/**
|
||||
* Another object of the same scene this one acts on (used by behaviors: "the owner").
|
||||
* Empty string = none. Any object may carry one, only behaviors interpret it.
|
||||
* Parent object of the same scene, empty string = top level. Children move with their
|
||||
* parent and are removed with it. Behaviors treat their parent as the "owner" the graph
|
||||
* acts on.
|
||||
*/
|
||||
ownerId: string;
|
||||
parentId: string;
|
||||
}
|
||||
|
||||
/** Creates a scene object; `z` defaults to 0 when the caller only has a 2D point. */
|
||||
export function createSceneObject(id: string, assetId: string, position: Vec2 | Position): SceneObject {
|
||||
export function createSceneObject(id: string, assetId: string, position: Vec2 | Position, parentId = ''): SceneObject {
|
||||
return {
|
||||
id,
|
||||
assetId,
|
||||
name: '',
|
||||
position: { x: position.x, y: position.y, z: 'z' in position ? position.z : 0 },
|
||||
overrides: {},
|
||||
ownerId: '',
|
||||
parentId,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import type { SceneAsset } from './Asset';
|
||||
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.
|
||||
*/
|
||||
|
||||
export function findObject(scene: SceneAsset, id: string): SceneObject | undefined {
|
||||
return scene.objects.find((o) => o.id === id);
|
||||
}
|
||||
|
||||
export function childrenOf(scene: SceneAsset, parentId: string): SceneObject[] {
|
||||
return scene.objects.filter((o) => o.parentId === parentId);
|
||||
}
|
||||
|
||||
/** All objects below `id`, depth-first, parents before children. */
|
||||
export function descendantsOf(scene: SceneAsset, id: string): SceneObject[] {
|
||||
const result: SceneObject[] = [];
|
||||
const visit = (parentId: string) => {
|
||||
for (const child of childrenOf(scene, 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(scene: SceneAsset, ancestorId: string, objectId: string): boolean {
|
||||
let current: SceneObject | undefined = findObject(scene, 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(scene, current.parentId) : undefined;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Position of an object in world units (all local positions along the chain summed up). */
|
||||
export function worldPosition(scene: SceneAsset, object: SceneObject): Position {
|
||||
const result = { ...object.position };
|
||||
let parent = object.parentId ? findObject(scene, 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(scene, parent.parentId) : undefined;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The local position that puts an object at `world` under the given parent ('' = top level). */
|
||||
export function localPositionFor(scene: SceneAsset, parentId: string, world: Position): Position {
|
||||
const parent = parentId ? findObject(scene, parentId) : undefined;
|
||||
if (!parent) return { ...world };
|
||||
const origin = worldPosition(scene, 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(scene: SceneAsset): TreeEntry[] {
|
||||
const result: TreeEntry[] = [];
|
||||
const ids = new Set(scene.objects.map((o) => o.id));
|
||||
const visit = (parentId: string, depth: number) => {
|
||||
for (const object of scene.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 scene.objects) {
|
||||
if (object.parentId && !ids.has(object.parentId) && !result.some((e) => e.object === object)) result.push({ object, depth: 0 });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Reference in new issue
Block a user