Files
feuerwerk/src/core/commands/sceneObjectCommands.ts
T
rootandClaude Opus 5 495756b500 Remove exports and methods nothing uses any more
A reference search over every export and method in src/ found code that
only tests still called, or that was never called at all:

- RemoveSceneObjectCommand: the product only removes through the
  multi-object command; the tests now use that one too.
- NodeRegistry.requireNodeDef: a second function of the same name in
  commands/require.ts raises a CommandError instead of throwing, and is
  the one the product uses. The twin invited the wrong import.
- isTextureAsset, listContainers, AssetTabEditor.show,
  FocusManager.destroy, KeyboardBridge.selectAll and the getters
  ColorField.value, ListView.selected, NumberField.number.

Also: Theme.controlActive now matches the resting control colour, and a
new note asks what editing the graph during play would take.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 20:15:49 +02:00

296 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { newId } from '../ids';
import { isSelfOrAncestor, localPositionFor, removeSubtrees, worldPosition } from '../model/hierarchy';
import {
captureObject,
isLiveInstance,
type ObjectMemo,
type ObjectRef,
restoreObject,
} from '../model/instance';
import { placementRejection } from '../model/placement';
import { type Position, type PropertyValue, toPosition, type Vec2 } from '../model/Properties';
import type { Project } from '../model/Project';
import { createSceneObject, type SceneObject } from '../model/SceneObject';
import { coerceValue } from '../properties/PropertySchema';
import { type Removed, restoreAt } from '../util/list';
import { type Command, CommandError } from './Command';
import {
requireAsset,
requireObject,
requireObjectRef,
requirePropDef,
requireHolder,
requireStructural,
} from './require';
/**
* An edit to one placed object. The base captures the object before the edit and puts it back on
* undo, so a subclass only says what changes. `applyCommands` deliberately does not use this: its
* commands carry extra undo state whose restore order matters.
*/
abstract class ObjectEditCommand implements Command {
abstract readonly label: string;
private memo: ObjectMemo | undefined;
protected constructor(
private readonly holderId: string,
private readonly objectId: string,
) {}
protected abstract apply(ref: ObjectRef, project: Project): void;
execute(project: Project): void {
const ref = requireObjectRef(project, this.holderId, this.objectId);
this.memo = captureObject(ref);
this.apply(ref, project);
}
undo(): void {
if (this.memo) restoreObject(this.memo);
}
}
/**
* The position is a world position; with a parent it is converted to the parent's local space.
* Placing a container adds the container object alone – its content is resolved from the template
* (see `model/instance.ts`), so it is never copied into the scene.
*/
export class PlaceAssetCommand implements Command {
readonly label = 'Place asset';
readonly objectId = newId();
constructor(
private readonly holderId: string,
private readonly assetId: string,
private readonly position: Vec2 | Position,
private readonly parentId = '',
) {}
execute(project: Project): void {
const holder = requireHolder(project, this.holderId);
const asset = requireAsset(project, this.assetId);
const rejection = placementRejection(project, holder, asset);
if (rejection) throw new CommandError(rejection);
if (this.parentId) {
requireObject(project, this.holderId, this.parentId);
requireStructural(project, holder, this.parentId);
}
const local = localPositionFor(holder, this.parentId, toPosition(this.position));
holder.objects.push(createSceneObject(this.objectId, this.assetId, local, this.parentId));
}
undo(project: Project): void {
const holder = requireHolder(project, this.holderId);
holder.objects = holder.objects.filter((o) => o.id !== this.objectId);
}
}
/** Removes objects together with everything parented below them, as one undo step. */
export class RemoveSceneObjectsCommand implements Command {
readonly label: string;
private removed: Removed<SceneObject>[] = [];
constructor(
private readonly holderId: string,
private readonly objectIds: readonly string[],
) {
this.label = objectIds.length === 1 ? 'Remove object' : 'Remove objects';
}
execute(project: Project): void {
const holder = requireHolder(project, this.holderId);
for (const id of this.objectIds) {
requireStructural(project, holder, id);
requireObject(project, this.holderId, id);
}
this.removed = removeSubtrees(holder, this.objectIds);
}
undo(project: Project): void {
restoreAt(requireHolder(project, this.holderId).objects, this.removed);
}
}
/**
* The previous position is read at execution time, so a caller that previewed the move must
* restore the model before executing.
*/
export class MoveSceneObjectCommand extends ObjectEditCommand {
readonly label = 'Move object';
constructor(
holderId: string,
objectId: string,
private readonly position: Position,
) {
super(holderId, objectId);
}
protected override apply(ref: ObjectRef): void {
ref.setPosition(this.position);
}
}
/** Editor-only: undoable like any other edit, but it changes nothing the game can see. */
export class SetEditorOffsetCommand extends ObjectEditCommand {
readonly label = 'Editor offset';
constructor(
holderId: string,
objectId: string,
private readonly offset: Vec2,
) {
super(holderId, objectId);
}
protected override apply(ref: ObjectRef): void {
ref.setEditorOffset(this.offset);
}
}
export class RenameSceneObjectCommand extends ObjectEditCommand {
readonly label = 'Rename object';
constructor(
holderId: string,
objectId: string,
private readonly name: string,
) {
super(holderId, objectId);
}
protected override apply(ref: ObjectRef): void {
ref.setName(this.name);
}
}
abstract class OverrideCommand extends ObjectEditCommand {
protected constructor(
holderId: string,
objectId: string,
protected readonly key: string,
readonly label: string,
) {
super(holderId, objectId);
}
protected abstract nextValue(object: SceneObject, project: Project): PropertyValue | undefined;
protected override apply(ref: ObjectRef, project: Project): void {
ref.setOverride(this.key, this.nextValue(ref.object, project));
}
}
export class SetOverrideCommand extends OverrideCommand {
constructor(
holderId: string,
objectId: string,
key: string,
private readonly value: PropertyValue,
) {
super(holderId, objectId, key, `Override ${key}`);
}
protected override nextValue(object: SceneObject, project: Project): PropertyValue {
return coerceValue(requirePropDef(requireAsset(project, object.assetId), this.key), this.value);
}
}
export class ClearOverrideCommand extends OverrideCommand {
constructor(holderId: string, objectId: string, key: string) {
super(holderId, objectId, key, `Reset ${key}`);
}
// No schema check, unlike setting one: a key the asset no longer declares is exactly what one
// wants to be able to clear.
protected override nextValue(): undefined {
return undefined;
}
}
/**
* Moves an object among its siblings. Sibling order is the order the stored list happens to have
* (`treeOrder`), so the move is a splice – and because objects sharing a `z` fall back to that
* order, it is also how two overlapping objects are put in front of each other.
*/
export class ReorderObjectCommand implements Command {
readonly label = 'Reorder object';
private previousIndex = -1;
/** `beforeId` empty means "behind its last sibling". */
constructor(
private readonly holderId: string,
private readonly objectId: string,
private readonly beforeId: string,
) {}
execute(project: Project): void {
const holder = requireHolder(project, this.holderId);
requireStructural(project, holder, this.objectId);
const object = requireObject(project, this.holderId, this.objectId);
if (this.beforeId === this.objectId)
throw new CommandError('An object cannot be moved in front of itself');
let at = holder.objects.length;
if (this.beforeId) {
requireStructural(project, holder, this.beforeId);
const before = requireObject(project, this.holderId, this.beforeId);
if (before.parentId !== object.parentId)
throw new CommandError('An object can only be reordered among its own siblings');
at = holder.objects.indexOf(before);
}
this.previousIndex = holder.objects.indexOf(object);
holder.objects.splice(this.previousIndex, 1);
// The index was read before the removal, so anything behind it has moved up by one.
holder.objects.splice(at > this.previousIndex ? at - 1 : at, 0, object);
}
undo(project: Project): void {
const holder = requireHolder(project, this.holderId);
const object = requireObject(project, this.holderId, this.objectId);
holder.objects.splice(holder.objects.indexOf(object), 1);
holder.objects.splice(this.previousIndex, 0, object);
}
}
/**
* Re-parents an object ('' = top level). It keeps its world position: the local one is recomputed
* for the new parent.
*/
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 holderId: string,
private readonly objectId: string,
private readonly parentId: string,
) {}
execute(project: Project): void {
const holder = requireHolder(project, this.holderId);
requireStructural(project, holder, this.objectId);
const object = requireObject(project, this.holderId, this.objectId);
if (this.parentId) {
if (this.parentId === this.objectId) throw new CommandError('An object cannot be its own parent');
requireStructural(project, holder, this.parentId);
if (isLiveInstance(project, holder, requireObject(project, this.holderId, this.parentId)))
throw new CommandError('A placed container shows its template; unpack it to put objects inside');
if (isSelfOrAncestor(holder, this.objectId, this.parentId))
throw new CommandError('An object cannot be parented to one of its own children');
}
this.previous = { parentId: object.parentId, position: { ...object.position } };
const world = worldPosition(holder, object);
object.parentId = this.parentId;
object.position = localPositionFor(holder, this.parentId, world);
}
undo(project: Project): void {
const object = requireObject(project, this.holderId, this.objectId);
object.parentId = this.previous.parentId;
object.position = { ...this.previous.position };
}
}