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
@@ -1,6 +1,8 @@
|
||||
import { isPlaceableType } from '../model/AssetTypes';
|
||||
import { descendantsOf, isSelfOrAncestor, localPositionFor, worldPosition } from '../model/hierarchy';
|
||||
import type { Position, PropertyValue, Vec2 } from '../model/Properties';
|
||||
import { findScene, findSceneObject, type Project } from '../model/Project';
|
||||
import type { SceneAsset } from '../model/Asset';
|
||||
import { createSceneObject, type SceneObject } from '../model/SceneObject';
|
||||
import { coerceValue, schemaForAsset } from '../properties/PropertySchema';
|
||||
import { newId } from '../ids';
|
||||
@@ -19,18 +21,57 @@ export function requireObject(project: Project, sceneId: string, objectId: strin
|
||||
return object;
|
||||
}
|
||||
|
||||
/** Places an asset into a scene, creating a scene object. Scenes cannot be placed. */
|
||||
/** An object taken out of a scene together with the index it had, so undo can put it back. */
|
||||
export interface RemovedObject {
|
||||
object: SceneObject;
|
||||
index: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given objects and all their descendants from the scene (a subtree goes as a
|
||||
* whole). Returns what was removed in ascending index order.
|
||||
*/
|
||||
export function removeSubtrees(scene: SceneAsset, rootIds: string[]): RemovedObject[] {
|
||||
const ids = new Set<string>();
|
||||
for (const rootId of rootIds) {
|
||||
ids.add(rootId);
|
||||
for (const child of descendantsOf(scene, rootId)) ids.add(child.id);
|
||||
}
|
||||
const removed: RemovedObject[] = [];
|
||||
scene.objects.forEach((object, index) => {
|
||||
if (ids.has(object.id)) removed.push({ object, index });
|
||||
});
|
||||
scene.objects = scene.objects.filter((o) => !ids.has(o.id));
|
||||
return removed;
|
||||
}
|
||||
|
||||
/** Puts removed objects back at their former indices (ascending order restores them exactly). */
|
||||
export function restoreObjects(scene: SceneAsset, removed: RemovedObject[]): void {
|
||||
for (const entry of [...removed].sort((a, b) => a.index - b.index)) scene.objects.splice(entry.index, 0, entry.object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Places an asset into a scene, creating a scene object. Scenes cannot be placed. The
|
||||
* position is a world position; with a parent it is converted to the parent's local space.
|
||||
*/
|
||||
export class PlaceAssetCommand implements Command {
|
||||
readonly label = 'Place asset';
|
||||
readonly objectId = newId();
|
||||
|
||||
constructor(private readonly sceneId: string, private readonly assetId: string, private readonly position: Vec2 | Position) {}
|
||||
constructor(
|
||||
private readonly sceneId: string,
|
||||
private readonly assetId: string,
|
||||
private readonly position: Vec2 | Position,
|
||||
private readonly parentId = '',
|
||||
) {}
|
||||
|
||||
execute(project: Project): void {
|
||||
const scene = requireScene(project, this.sceneId);
|
||||
const asset = requireAsset(project, this.assetId);
|
||||
if (!isPlaceableType(asset.type)) throw new CommandError('A scene cannot be placed into a scene');
|
||||
scene.objects.push(createSceneObject(this.objectId, this.assetId, this.position));
|
||||
if (this.parentId) requireObject(project, this.sceneId, this.parentId);
|
||||
const world = { x: this.position.x, y: this.position.y, z: 'z' in this.position ? this.position.z : 0 };
|
||||
scene.objects.push(createSceneObject(this.objectId, this.assetId, localPositionFor(scene, this.parentId, world), this.parentId));
|
||||
}
|
||||
|
||||
undo(project: Project): void {
|
||||
@@ -39,42 +80,24 @@ export class PlaceAssetCommand implements Command {
|
||||
}
|
||||
}
|
||||
|
||||
/** Removes an object and everything parented to it. */
|
||||
export class RemoveSceneObjectCommand implements Command {
|
||||
readonly label = 'Remove object';
|
||||
private removed?: { object: SceneObject; index: number };
|
||||
private clearedOwners: string[] = [];
|
||||
private removed: RemovedObject[] = [];
|
||||
|
||||
constructor(private readonly sceneId: string, private readonly objectId: string) {}
|
||||
|
||||
execute(project: Project): void {
|
||||
const scene = requireScene(project, this.sceneId);
|
||||
const index = scene.objects.findIndex((o) => o.id === this.objectId);
|
||||
const object = scene.objects[index];
|
||||
if (!object) throw new CommandError(`Scene object ${this.objectId} not found`);
|
||||
this.removed = { object, index };
|
||||
scene.objects.splice(index, 1);
|
||||
// Objects owned by the removed one lose their owner reference.
|
||||
this.clearedOwners = [];
|
||||
for (const other of scene.objects) {
|
||||
if (other.ownerId === this.objectId) {
|
||||
other.ownerId = '';
|
||||
this.clearedOwners.push(other.id);
|
||||
}
|
||||
}
|
||||
requireObject(project, this.sceneId, this.objectId);
|
||||
this.removed = removeSubtrees(requireScene(project, this.sceneId), [this.objectId]);
|
||||
}
|
||||
|
||||
undo(project: Project): void {
|
||||
if (!this.removed) return;
|
||||
const scene = requireScene(project, this.sceneId);
|
||||
scene.objects.splice(this.removed.index, 0, this.removed.object);
|
||||
for (const id of this.clearedOwners) {
|
||||
const other = findSceneObject(scene, id);
|
||||
if (other) other.ownerId = this.objectId;
|
||||
}
|
||||
restoreObjects(requireScene(project, this.sceneId), this.removed);
|
||||
}
|
||||
}
|
||||
|
||||
/** Sets the full position (x, y and draw order z) of a scene object. */
|
||||
/** Sets the full local position (x, y and draw order z) of a scene object. */
|
||||
export class MoveSceneObjectCommand implements Command {
|
||||
readonly label = 'Move object';
|
||||
private previous: Position = { x: 0, y: 0, z: 0 };
|
||||
@@ -167,24 +190,33 @@ export class ClearOverrideCommand implements Command {
|
||||
}
|
||||
}
|
||||
|
||||
/** Points an object (typically a behavior) at another object of the same scene, or at nothing (''). */
|
||||
export class SetOwnerCommand implements Command {
|
||||
readonly label = 'Set owner';
|
||||
private previous = '';
|
||||
/**
|
||||
* Re-parents an object ('' = top level). The object keeps its world position: its local
|
||||
* position is recomputed for the new parent. Cycles (an object below itself) are rejected.
|
||||
*/
|
||||
export class SetParentCommand implements Command {
|
||||
readonly label = 'Set parent';
|
||||
private previous: { parentId: string; position: Position } = { parentId: '', position: { x: 0, y: 0, z: 0 } };
|
||||
|
||||
constructor(private readonly sceneId: string, private readonly objectId: string, private readonly ownerId: string) {}
|
||||
constructor(private readonly sceneId: string, private readonly objectId: string, private readonly parentId: string) {}
|
||||
|
||||
execute(project: Project): void {
|
||||
const scene = requireScene(project, this.sceneId);
|
||||
const object = requireObject(project, this.sceneId, this.objectId);
|
||||
if (this.ownerId) {
|
||||
if (this.ownerId === this.objectId) throw new CommandError('An object cannot own itself');
|
||||
requireObject(project, this.sceneId, this.ownerId);
|
||||
if (this.parentId) {
|
||||
if (this.parentId === this.objectId) throw new CommandError('An object cannot be its own parent');
|
||||
requireObject(project, this.sceneId, this.parentId);
|
||||
if (isSelfOrAncestor(scene, this.objectId, this.parentId)) throw new CommandError('An object cannot be parented to one of its own children');
|
||||
}
|
||||
this.previous = object.ownerId;
|
||||
object.ownerId = this.ownerId;
|
||||
this.previous = { parentId: object.parentId, position: { ...object.position } };
|
||||
const world = worldPosition(scene, object);
|
||||
object.parentId = this.parentId;
|
||||
object.position = localPositionFor(scene, this.parentId, world);
|
||||
}
|
||||
|
||||
undo(project: Project): void {
|
||||
requireObject(project, this.sceneId, this.objectId).ownerId = this.previous;
|
||||
const object = requireObject(project, this.sceneId, this.objectId);
|
||||
object.parentId = this.previous.parentId;
|
||||
object.position = { ...this.previous.position };
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user