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:
1 parent
1eb28314cf
commit
f140c2fd77
58 files changed
+1969
-1171
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user