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:
1 parent
91ee2feadd
commit
0b78382bcc
26 files changed
+897
-114
No files matched your search
@@ -0,0 +1,103 @@
|
||||
import type { ContainerAsset } from '../model/Asset';
|
||||
import { descendantsOf, localPositionFor, subtreeRoots, treeOrder, worldPosition } from '../model/hierarchy';
|
||||
import type { Position } from '../model/Properties';
|
||||
import { findScene, type Project } from '../model/Project';
|
||||
import { createSceneObject } from '../model/SceneObject';
|
||||
import { newId } from '../ids';
|
||||
import { buildAsset } from './assetCommands';
|
||||
import { type Command, CommandError } from './Command';
|
||||
import { nextAssetName } from './projectCommands';
|
||||
|
||||
/**
|
||||
* Turns the selected objects (with everything parented below them) into a new container
|
||||
* asset and replaces them in the scene by one placed container – exactly as if that asset
|
||||
* had been placed there. The container's origin is the centre of the selection's bounding
|
||||
* box (x/y of the objects' world positions; z = 0), so the template is centred around
|
||||
* (0, 0, 0). The objects keep their ids and world positions: the selected roots are simply
|
||||
* re-parented under the container with recomputed local positions. When all roots share a
|
||||
* parent the container is placed below that parent, otherwise at the top level.
|
||||
*/
|
||||
export class CreateContainerCommand implements Command {
|
||||
readonly label = 'Create container';
|
||||
readonly assetId = newId();
|
||||
readonly objectId = newId();
|
||||
private previous: { id: string; parentId: string; position: Position; templateId: string }[] = [];
|
||||
|
||||
constructor(private readonly sceneId: string, private readonly objectIds: readonly string[]) {}
|
||||
|
||||
execute(project: Project): void {
|
||||
const scene = findScene(project, this.sceneId);
|
||||
if (!scene) throw new CommandError(`Scene ${this.sceneId} not found`);
|
||||
const selected = [...new Set(this.objectIds)].map((id) => {
|
||||
const object = scene.objects.find((o) => o.id === id);
|
||||
if (!object) throw new CommandError(`Scene object ${id} not found`);
|
||||
return object;
|
||||
});
|
||||
if (selected.length === 0) throw new CommandError('Select at least one object to create a container');
|
||||
|
||||
// Centre of the selection in world space – the container's origin.
|
||||
const worlds = selected.map((o) => worldPosition(scene, o));
|
||||
const centre: Position = {
|
||||
x: Math.round((Math.min(...worlds.map((p) => p.x)) + Math.max(...worlds.map((p) => p.x))) / 2),
|
||||
y: Math.round((Math.min(...worlds.map((p) => p.y)) + Math.max(...worlds.map((p) => p.y))) / 2),
|
||||
z: 0,
|
||||
};
|
||||
|
||||
// The subtrees that move into the container, in tree order (parents first).
|
||||
const roots = new Set(subtreeRoots(scene, selected.map((o) => o.id)));
|
||||
const memberIds = new Set<string>();
|
||||
for (const id of roots) {
|
||||
memberIds.add(id);
|
||||
for (const child of descendantsOf(scene, id)) memberIds.add(child.id);
|
||||
}
|
||||
const members = treeOrder(scene).map((e) => e.object).filter((o) => memberIds.has(o.id));
|
||||
const rootParents = new Set([...roots].map((id) => scene.objects.find((o) => o.id === id)!.parentId));
|
||||
const containerParentId = rootParents.size === 1 ? [...rootParents][0]! : '';
|
||||
|
||||
// Template: same structure with private ids, roots relative to the centre.
|
||||
const asset = buildAsset('container', nextAssetName(project, 'Container'), this.assetId) as ContainerAsset;
|
||||
const templateIds = new Map(members.map((o) => [o.id, newId()]));
|
||||
for (const object of members) {
|
||||
const isRoot = roots.has(object.id);
|
||||
const world = worldPosition(scene, object);
|
||||
asset.objects.push({
|
||||
id: templateIds.get(object.id)!,
|
||||
assetId: object.assetId,
|
||||
name: object.name,
|
||||
position: isRoot ? { x: world.x - centre.x, y: world.y - centre.y, z: world.z } : { ...object.position },
|
||||
overrides: { ...object.overrides },
|
||||
parentId: isRoot ? '' : templateIds.get(object.parentId) ?? '',
|
||||
templateId: '',
|
||||
});
|
||||
}
|
||||
|
||||
// Scene: the container takes the roots' place, the members become its instance.
|
||||
this.previous = members.map((o) => ({ id: o.id, parentId: o.parentId, position: { ...o.position }, templateId: o.templateId }));
|
||||
const container = createSceneObject(this.objectId, this.assetId, localPositionFor(scene, containerParentId, centre), containerParentId);
|
||||
for (const object of members) {
|
||||
if (roots.has(object.id)) {
|
||||
const world = worldPosition(scene, object);
|
||||
object.parentId = this.objectId;
|
||||
object.position = { x: world.x - centre.x, y: world.y - centre.y, z: world.z };
|
||||
}
|
||||
object.templateId = templateIds.get(object.id)!;
|
||||
}
|
||||
project.assets.push(asset);
|
||||
scene.objects.push(container);
|
||||
}
|
||||
|
||||
undo(project: Project): void {
|
||||
const scene = findScene(project, this.sceneId);
|
||||
if (scene) {
|
||||
for (const entry of this.previous) {
|
||||
const object = scene.objects.find((o) => o.id === entry.id);
|
||||
if (!object) continue;
|
||||
object.parentId = entry.parentId;
|
||||
object.position = { ...entry.position };
|
||||
object.templateId = entry.templateId;
|
||||
}
|
||||
scene.objects = scene.objects.filter((o) => o.id !== this.objectId);
|
||||
}
|
||||
project.assets = project.assets.filter((a) => a.id !== this.assetId);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user