Fix findUsages/undo/node-lookup bugs, split Viewport and Editor, share command and inspector helpers

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-08 09:22:49 +02:00
1 parent 1eb28314cf
commit f140c2fd77
58 files changed
+1969 -1171

No files matched your search

+38 -47
View File
@@ -1,15 +1,16 @@
import { createEmptyGraph } from '../graph/Graph';
import { newId } from '../ids';
import type { Asset, ContainerAsset, SceneAsset } from '../model/Asset';
import type { Asset } from '../model/Asset';
import type { AssetType } from '../model/AssetTypes';
import { removeSubtrees } from '../model/hierarchy';
import { type PropertyValue, setBagValue } from '../model/Properties';
import type { Project } from '../model/Project';
import type { PropertyBag, PropertyValue } from '../model/Properties';
import { objectHolders, type Project } from '../model/Project';
import type { SceneObject } from '../model/SceneObject';
import { coerceValue, defaultsFor } from '../properties/PropertySchema';
import { type Removed, restoreAt } from '../util/list';
import { type Removed, removeById, removeFirst, restoreAt } from '../util/list';
import { type Command, CommandError } from './Command';
import { requireAsset, requirePropDef } from './require';
import { SetBagValueCommand } from './SetBagValueCommand';
/** A new asset of the given type with default properties (and empty content for scenes, behaviors, containers). */
export function buildAsset(type: AssetType, name: string, id = newId()): Asset {
@@ -50,7 +51,7 @@ export class CreateAssetCommand implements Command {
}
undo(project: Project): void {
project.assets = project.assets.filter((a) => a.id !== this.asset.id);
project.assets = removeById(project.assets, this.asset.id);
project.activeSceneId = this.previousActiveScene;
}
}
@@ -70,17 +71,16 @@ export class DeleteAssetCommand implements Command {
constructor(private readonly assetId: string) {}
execute(project: Project): void {
const index = project.assets.findIndex((a) => a.id === this.assetId);
const asset = project.assets[index];
if (!asset) throw new CommandError(`Asset ${this.assetId} not found`);
requireAsset(project, this.assetId);
this.removedObjects = [];
// The cascade runs while the asset is still in the list: a container asset is an object
// holder itself, so removing it first would change what `objectHolders` reports.
for (const holder of objectHolders(project)) {
const roots = holder.objects.filter((o) => o.assetId === this.assetId).map((o) => o.id);
if (roots.length === 0) continue;
this.removedObjects.push({ holderId: holder.id, removed: removeSubtrees(holder, roots) });
}
this.removed = { item: asset, index };
project.assets.splice(index, 1);
this.removed = removeFirst(project.assets, (a) => a.id === this.assetId);
this.previousActiveScene = project.activeSceneId;
if (project.activeSceneId === this.assetId) {
const next = project.assets.find((a) => a.type === 'scene');
@@ -90,20 +90,16 @@ export class DeleteAssetCommand implements Command {
undo(project: Project): void {
if (!this.removed) return;
project.assets.splice(this.removed.index, 0, this.removed.item);
restoreAt(project.assets, [this.removed]);
const holders = new Map(objectHolders(project).map((h) => [h.id, h]));
for (const entry of this.removedObjects) {
const holder = objectHolders(project).find((h) => h.id === entry.holderId);
const holder = holders.get(entry.holderId);
if (holder) restoreAt(holder.objects, entry.removed);
}
project.activeSceneId = this.previousActiveScene;
}
}
/** Scenes and container assets: everything that holds placed objects. */
function objectHolders(project: Project): (SceneAsset | ContainerAsset)[] {
return project.assets.filter((a): a is SceneAsset | ContainerAsset => a.type === 'scene' || a.type === 'container');
}
export class RenameAssetCommand implements Command {
readonly label = 'Rename asset';
private previousName = '';
@@ -121,45 +117,40 @@ export class RenameAssetCommand implements Command {
}
}
/** The half of `SetBagValueCommand` both asset property edits share: the bag is the asset's own. */
abstract class AssetPropertyCommand extends SetBagValueCommand<Asset> {
protected constructor(private readonly assetId: string, key: string, label: string) {
super(key, label);
}
protected override resolveTarget(project: Project): Asset {
return requireAsset(project, this.assetId);
}
protected override bagOf(asset: Asset): PropertyBag {
return asset.props;
}
}
/** Sets an asset property (coerced against its schema entry). */
export class SetAssetPropertyCommand implements Command {
readonly label: string;
private previous: PropertyValue | undefined;
constructor(private readonly assetId: string, private readonly key: string, private readonly value: PropertyValue) {
this.label = `Set ${key}`;
export class SetAssetPropertyCommand extends AssetPropertyCommand {
constructor(assetId: string, key: string, private readonly value: PropertyValue) {
super(assetId, key, `Set ${key}`);
}
execute(project: Project): void {
const asset = requireAsset(project, this.assetId);
const def = requirePropDef(asset, this.key);
this.previous = asset.props[this.key];
asset.props[this.key] = coerceValue(def, this.value);
}
undo(project: Project): void {
setBagValue(requireAsset(project, this.assetId).props, this.key, this.previous);
protected override computeNext(asset: Asset): PropertyValue {
return coerceValue(requirePropDef(asset, this.key), this.value);
}
}
/** Puts an asset property back to its schema default (the asset-level "clear override"). */
export class ResetAssetPropertyCommand implements Command {
readonly label: string;
private previous: PropertyValue | undefined;
constructor(private readonly assetId: string, private readonly key: string) {
this.label = `Reset ${key}`;
export class ResetAssetPropertyCommand extends AssetPropertyCommand {
constructor(assetId: string, key: string) {
super(assetId, key, `Reset ${key}`);
}
execute(project: Project): void {
const asset = requireAsset(project, this.assetId);
const def = requirePropDef(asset, this.key);
this.previous = asset.props[this.key];
asset.props[this.key] = def.default;
}
undo(project: Project): void {
setBagValue(requireAsset(project, this.assetId).props, this.key, this.previous);
protected override computeNext(asset: Asset): PropertyValue {
return requirePropDef(asset, this.key).default;
}
}