Multiselection and container assets: create container from selection, template instances

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-07 17:40:51 +02:00
1 parent 91ee2feadd
commit 0b78382bcc
26 files changed
+897 -114

No files matched your search

+37 -12
View File
@@ -1,13 +1,13 @@
import { isPlaceableType } from '../model/AssetTypes';
import { descendantsOf, isSelfOrAncestor, localPositionFor, worldPosition } from '../model/hierarchy';
import { descendantsOf, isSelfOrAncestor, localPositionFor, type ObjectTree, 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';
import { type Command, CommandError } from './Command';
import { requireAsset } from './assetCommands';
import { instantiateContainer } from '../model/container';
function requireScene(project: Project, sceneId: string) {
const scene = findScene(project, sceneId);
@@ -28,35 +28,39 @@ export interface RemovedObject {
}
/**
* 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.
* Removes the given objects and all their descendants from the object list (a subtree goes
* as a whole; ids that are descendants of other ids are simply covered). Returns what was
* removed in ascending index order.
*/
export function removeSubtrees(scene: SceneAsset, rootIds: string[]): RemovedObject[] {
export function removeSubtrees(tree: ObjectTree, rootIds: readonly 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);
for (const child of descendantsOf(tree, rootId)) ids.add(child.id);
}
const removed: RemovedObject[] = [];
scene.objects.forEach((object, index) => {
tree.objects.forEach((object, index) => {
if (ids.has(object.id)) removed.push({ object, index });
});
scene.objects = scene.objects.filter((o) => !ids.has(o.id));
tree.objects = tree.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);
export function restoreObjects(tree: ObjectTree, removed: RemovedObject[]): void {
for (const entry of [...removed].sort((a, b) => a.index - b.index)) tree.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.
* Placing a container also creates its content (see `instantiateContainer`).
*/
export class PlaceAssetCommand implements Command {
readonly label = 'Place asset';
readonly objectId = newId();
/** Every object this command added: the placed one plus a container's content. */
private createdIds: string[] = [];
constructor(
private readonly sceneId: string,
@@ -71,12 +75,16 @@ export class PlaceAssetCommand implements Command {
if (!isPlaceableType(asset.type)) throw new CommandError('A scene cannot be placed into a scene');
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));
const created = [createSceneObject(this.objectId, this.assetId, localPositionFor(scene, this.parentId, world), this.parentId)];
if (asset.type === 'container') created.push(...instantiateContainer(project, asset, this.objectId));
this.createdIds = created.map((o) => o.id);
scene.objects.push(...created);
}
undo(project: Project): void {
const scene = requireScene(project, this.sceneId);
scene.objects = scene.objects.filter((o) => o.id !== this.objectId);
const ids = new Set(this.createdIds);
scene.objects = scene.objects.filter((o) => !ids.has(o.id));
}
}
@@ -97,6 +105,23 @@ export class RemoveSceneObjectCommand implements Command {
}
}
/** Removes several objects (a multi-selection) with their subtrees as one undo step. */
export class RemoveSceneObjectsCommand implements Command {
readonly label = 'Remove objects';
private removed: RemovedObject[] = [];
constructor(private readonly sceneId: string, private readonly objectIds: readonly string[]) {}
execute(project: Project): void {
for (const id of this.objectIds) requireObject(project, this.sceneId, id);
this.removed = removeSubtrees(requireScene(project, this.sceneId), this.objectIds);
}
undo(project: Project): void {
restoreObjects(requireScene(project, this.sceneId), this.removed);
}
}
/** Sets the full local position (x, y and draw order z) of a scene object. */
export class MoveSceneObjectCommand implements Command {
readonly label = 'Move object';