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:
rootandClaude Fable 5.1 committed 2026-09-07 15:04:56 +02:00
1 parent 551a50ffc9
commit 26d5edc496
22 files changed
+729 -166

No files matched your search

+17 -1
View File
@@ -12,10 +12,26 @@ export type Value = PropertyValue | EntityHandle | null;
export interface EntityHandle {
id: string;
name: string;
/** Position local to `parent` (world position for top-level entities). */
x: number;
y: number;
z: number;
props: PropertyBag;
/** The entity this one is parented to; children move with their parent. */
parent: EntityHandle | null;
}
/** World position of a handle: its local position plus those of all ancestors. */
export function worldPositionOf(handle: EntityHandle): { x: number; y: number; z: number } {
const result = { x: handle.x, y: handle.y, z: handle.z };
const seen = new Set<EntityHandle>([handle]);
for (let p = handle.parent; p && !seen.has(p); p = p.parent) {
result.x += p.x;
result.y += p.y;
result.z += p.z;
seen.add(p);
}
return result;
}
/**
@@ -26,7 +42,7 @@ export interface EntityHandle {
export interface NodeContext {
/** The behavior entity itself. */
self: EntityHandle;
/** The scene object the behavior was attached to, if any. */
/** The behavior's parent object (the one the graph acts on), if any. */
owner: EntityHandle | null;
/** Mutable variable state of this behavior instance. */
variables: PropertyBag;
+16 -5
View File
@@ -1,5 +1,5 @@
import type { PropertyDef } from '../properties/PropertySchema';
import { asBoolean, asEntity, asNumber, asString, type PortDef, registerNode } from './NodeRegistry';
import { asBoolean, asEntity, asNumber, asString, type PortDef, registerNode, worldPositionOf } from './NodeRegistry';
/**
* The built-in node set. Each entry is data + a `run` function; everything else (palette,
@@ -59,7 +59,17 @@ registerNode({
// --- data: references --------------------------------------------------------------------
registerNode({ type: 'Self', label: 'Self', category: 'data', description: 'This behavior object.', inputs: [], outputs: [obj('object')], params: [], run: (ctx) => ({ outputs: { object: ctx.self } }) });
registerNode({ type: 'Owner', label: 'Owner', category: 'data', description: 'The object this behavior is attached to.', inputs: [], outputs: [obj('object')], params: [], run: (ctx) => ({ outputs: { object: ctx.owner } }) });
registerNode({ type: 'Owner', label: 'Owner', category: 'data', description: 'The object this behavior is parented to.', inputs: [], outputs: [obj('object')], params: [], run: (ctx) => ({ outputs: { object: ctx.owner } }) });
registerNode({
type: 'Parent',
label: 'Parent',
category: 'data',
description: 'The parent of an object (none for top-level objects).',
inputs: [obj('object')],
outputs: [obj('parent')],
params: [],
run: (_ctx, inputs) => ({ outputs: { parent: asEntity(inputs.object)?.parent ?? null } }),
});
registerNode({
type: 'ObjectByName',
label: 'Object by name',
@@ -141,13 +151,14 @@ registerNode({
type: 'GetPosition',
label: 'Get position',
category: 'data',
description: 'The x/y position of an object.',
description: 'The position of an object: local to its parent (x/y) and in world units (world x/y).',
inputs: [obj('object')],
outputs: [num('x'), num('y')],
outputs: [num('x'), num('y'), num('worldX', 'world x'), num('worldY', 'world y')],
params: [],
run: (_ctx, inputs) => {
const target = asEntity(inputs.object);
return { outputs: { x: target?.x ?? 0, y: target?.y ?? 0 } };
const world = target ? worldPositionOf(target) : { x: 0, y: 0 };
return { outputs: { x: target?.x ?? 0, y: target?.y ?? 0, worldX: world.x, worldY: world.y } };
},
});