Trim needless comments and fix the picker width

Remove comments that only restate the code, keeping the section banners
and the notes that carry a reason, a measured fact or an external
contract.

ButtonOptions.width claimed to be a fixed width but only set minWidth,
so the Graph and Texture pickers grew with the name they showed. It is
now what it promises, with a separate minWidth for the buttons that have
two forms. Narrow buttons shrink their padding, otherwise a 24px icon
button had 8px left for its glyph and truncate dropped it entirely.

__gm.labelsDropped() guards that class: the overflow guard only saw text
that was too wide, never a label truncated to nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-08 15:52:05 +02:00
1 parent f16801aef3
commit 4d84231b9f
115 files changed
+707 -1564

No files matched your search

+3 -8
View File
@@ -1,14 +1,9 @@
import type { Project } from '../model/Project';
/**
* An undoable edit of the project. Commands mutate the project in place and remember
* whatever they need to revert. They validate first: `execute` throws a `CommandError` on
* an invalid operation (e.g. placing a scene into a scene) before it changes anything.
*
* The same holds for `undo`: when its target is gone it throws a `CommandError` rather than
* reverting half of the edit. The command then stays on the undo stack (see `CommandStack`)
* and the editor shows the message. Validation belongs in `execute`/`undo`, never in the
* constructor – commands are built outside the editor's error handling.
* An undoable edit of the project, mutating it in place. Both `execute` and `undo` validate before
* changing anything and throw a `CommandError` instead of leaving half an edit behind. Validation
* belongs in them, never in the constructor – commands are built outside the error handling.
*/
export interface Command {
readonly label: string;
+1 -6
View File
@@ -18,11 +18,7 @@ export class CommandStack {
this.redoStack.length = 0;
}
/**
* Reverts the last command; false when there is none. The command moves between the stacks
* only after it succeeded – a throwing `undo` leaves the history exactly as it was instead
* of dropping the command from both stacks.
*/
/** The command moves between the stacks only after it succeeded, so a throwing `undo` loses nothing. */
undo(): boolean {
const command = this.undoStack[this.undoStack.length - 1];
if (!command) return false;
@@ -32,7 +28,6 @@ export class CommandStack {
return true;
}
/** Re-applies the last undone command; false when there is none. Moves it only on success. */
redo(): boolean {
const command = this.redoStack[this.redoStack.length - 1];
if (!command) return false;
+2 -3
View File
@@ -2,9 +2,8 @@ import type { Project } from '../model/Project';
import type { Command } from './Command';
/**
* Several commands as one undo step (moving a multi-selection, for instance). Executes in
* order; if one of them throws, the ones already done are rolled back before re-throwing,
* so the project is never left half-changed.
* Several commands as one undo step. If one throws, the ones already done are rolled back before
* re-throwing, so the project is never left half-changed.
*/
export class CompoundCommand implements Command {
constructor(
+6 -8
View File
@@ -3,13 +3,11 @@ import type { Project } from '../model/Project';
import type { Command } from './Command';
/**
* The skeleton every "write one key into a property bag" edit shares: remember what the bag
* held, write the new value, put the old one back on undo. A key that is not in the bag is
* `undefined` on both ends – as a `computeNext` result it removes the key (this is how
* clearing an override works), and as remembered value it makes undo remove it again.
* A missing key is `undefined` on both ends: as a `computeNext` result it removes the key (this is
* how clearing an override works), and as remembered value it makes undo remove it again.
*
* Subclasses look their target up on every call instead of holding on to it, so an undo whose
* target is gone fails with a `CommandError` instead of writing into an orphaned object.
* Subclasses resolve their target on every call rather than holding it, so an undo whose target is
* gone fails with a `CommandError` instead of writing into an orphaned object.
*/
export abstract class SetBagValueCommand<T> implements Command {
readonly label: string;
@@ -28,8 +26,8 @@ export abstract class SetBagValueCommand<T> implements Command {
protected abstract bagOf(target: T): PropertyBag;
/**
* The value to write, `undefined` to remove the key. Called before anything is mutated, so
* this is where a subclass rejects an invalid edit (an unknown property key, say).
* The value to write, `undefined` to remove the key. Called before anything is mutated, so this
* is where a subclass rejects an invalid edit.
*/
protected abstract computeNext(target: T, project: Project): PropertyValue | undefined;
+9 -23
View File
@@ -1,9 +1,7 @@
/**
* "Apply" is the opposite direction of an override: a value the user changed on a placed
* object is written back to where it came from – the asset, or (for objects that are part
* of a placed container) the container's template. The rule for instances follows the usual
* prefab semantics: copies that were still at the template's value follow the change,
* copies the user modified on their own keep their value.
* "Apply" is the opposite direction of an override: a value changed on a placed object is written
* back to the asset, or to the template of the container it is part of. Prefab semantics for the
* other copies: those still at the template's value follow, those modified on their own do not.
*/
import { instancesOf, type TemplateLink, templateLinkOf } from '../model/container';
import { type Axis, type Position, type PropertyValue, setBagValue } from '../model/Properties';
@@ -22,10 +20,7 @@ function requireLink(project: Project, sceneId: string, object: SceneObject): Te
return link;
}
/**
* One value that is applied on its own: an override key, or a single axis of the position.
* Applying works the same for both – the slot is the only thing that differs.
*/
/** One value that is applied on its own: an override key, or a single axis of the position. */
interface Slot<T> {
read(object: SceneObject): T;
write(object: SceneObject, value: T): void;
@@ -43,7 +38,6 @@ const positionSlot = (axis: Axis): Slot<number> => ({
},
});
/** A slot's value on one object before it was overwritten. */
interface SlotMemo<T> {
object: SceneObject;
slot: Slot<T>;
@@ -51,9 +45,8 @@ interface SlotMemo<T> {
}
/**
* Writes `values` into the template's slots and carries every instance that still agreed
* with the template along – copies the user changed on their own keep their value. Returns
* the memos in application order; `restoreSlots` undoes them.
* Writes `values` into the template's slots and carries along every instance that still agreed with
* it. Returns the memos in application order; `restoreSlots` undoes them.
*/
function applyToTemplate<T>(
project: Project,
@@ -83,9 +76,8 @@ function restoreSlots<T>(memos: readonly SlotMemo<T>[]): void {
}
/**
* Writes the object's value for `key` into its asset and drops the override – the object
* follows the asset again, as does everything else without an override of its own. For a
* container instance the template's override is dropped too (and instances that shared it).
* Writes the object's value for `key` into its asset and drops the override. For a container
* instance the template's override is dropped too, along with the instances that shared it.
*/
export class ApplyOverrideToAssetCommand implements Command {
readonly label: string;
@@ -124,7 +116,6 @@ export class ApplyOverrideToAssetCommand implements Command {
}
}
/** Writes the object's override for `key` into its container template (instances at the old value follow). */
export class ApplyOverrideToContainerCommand implements Command {
readonly label: string;
private memos: SlotMemo<PropertyValue | undefined>[] = [];
@@ -148,7 +139,6 @@ export class ApplyOverrideToContainerCommand implements Command {
}
}
/** Puts the object's override for `key` back to what its container template says. */
export class RevertOverrideToTemplateCommand implements Command {
readonly label: string;
private previous: PropertyValue | undefined;
@@ -173,10 +163,7 @@ export class RevertOverrideToTemplateCommand implements Command {
}
}
/**
* Writes the object's local position (one axis, or all) into its container template.
* Instances whose axis still matched the template follow; the others keep their own value.
*/
/** Writes the object's local position – one axis, or all – into its container template. */
export class ApplyPositionToContainerCommand implements Command {
readonly label: string;
private memos: SlotMemo<number>[] = [];
@@ -206,7 +193,6 @@ export class ApplyPositionToContainerCommand implements Command {
}
}
/** Puts the object's local position (one axis, or all) back to its container template. */
export class RevertPositionToTemplateCommand implements Command {
readonly label: string;
private previous: Position = { x: 0, y: 0, z: 0 };
+6 -22
View File
@@ -13,11 +13,6 @@ 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). A texture starts without a picture; the upload fills it in, which
* is why `CreateTextureCommand` exists rather than a plain create followed by an edit.
*/
export function buildAsset(type: AssetType, name: string, id = newId()): Asset {
const base = { id, name, props: defaultsFor(type) };
switch (type) {
@@ -70,19 +65,14 @@ export interface TextureImage {
height: number;
}
/**
* A texture asset for a stored image, without frames – slicing it is a separate, undoable
* edit. Kept apart from `buildAsset` so callers that need a `TextureAsset` get one, instead
* of the `Asset` union they would have to narrow again.
*/
/** Apart from `buildAsset` so callers get a `TextureAsset` rather than the `Asset` union. */
export function buildTextureAsset(name: string, image: TextureImage, id = newId()): TextureAsset {
return { id, name, props: defaultsFor('texture'), type: 'texture', ...image, frames: [] };
}
/**
* Adds a texture asset for an image that is already stored on the server. Uploading is I/O and
* therefore happens before the command: this one only records the result, so undo and redo of
* an import cost nothing and never touch the network.
* The image is uploaded before the command runs; this only records the result, so undo and redo
* never touch the network.
*/
export class CreateTextureCommand implements Command {
readonly label: string;
@@ -94,8 +84,7 @@ export class CreateTextureCommand implements Command {
}
execute(project: Project): void {
// The name is the hash the server derived from the bytes; anything else never was uploaded
// and would leave a texture that silently draws nothing.
// The name is the hash the server derived from the bytes; anything else was never uploaded.
if (!isTextureFile(this.asset.file))
throw new CommandError(`"${this.asset.file}" is not a stored image file`);
project.assets.push(this.asset);
@@ -107,14 +96,12 @@ export class CreateTextureCommand implements Command {
}
/**
* Deletes an asset and, as a cascade, every scene object referencing it – together with the
* objects parented below those (a subtree is removed as a whole). Container templates
* referencing the asset lose those objects the same way.
* Deletes an asset and, as a cascade, every scene object referencing it, together with the objects
* parented below those – in container templates as well as in scenes.
*/
export class DeleteAssetCommand implements Command {
readonly label = 'Delete asset';
private removed?: Removed<Asset>;
/** Objects removed per scene or container asset (both hold a flat object list). */
private removedObjects: { holderId: string; removed: Removed<SceneObject>[] }[] = [];
private previousActiveScene = '';
@@ -170,7 +157,6 @@ 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,
@@ -189,7 +175,6 @@ abstract class AssetPropertyCommand extends SetBagValueCommand<Asset> {
}
}
/** Sets an asset property (coerced against its schema entry). */
export class SetAssetPropertyCommand extends AssetPropertyCommand {
constructor(
assetId: string,
@@ -204,7 +189,6 @@ export class SetAssetPropertyCommand extends AssetPropertyCommand {
}
}
/** Puts an asset property back to its schema default (the asset-level "clear override"). */
export class ResetAssetPropertyCommand extends AssetPropertyCommand {
constructor(assetId: string, key: string) {
super(assetId, key, `Reset ${key}`);
+2 -3
View File
@@ -9,9 +9,8 @@ import { nextAssetName } from './naming';
import { requireAsset, requireObject, requireScene } from './require';
/**
* The one-click way to give an object a collider: creates a box collider asset sized like
* the object (sprites: width × height; anything else: schema defaults) and places it as a
* child at the object's centre. Undo removes both the placed collider and the asset.
* Creates a collider asset sized like the object (sprites: width × height; anything else: schema
* defaults) and places it as a child at the object's centre.
*/
export class AddColliderCommand implements Command {
readonly label = 'Add collider';
+5 -11
View File
@@ -18,13 +18,10 @@ import { nextAssetName } from './naming';
import { requireObject, requireScene } from './require';
/**
* 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.
* Turns the selected objects, with everything parented below them, into a container asset placed
* in their stead. The origin is the centre of the selection's bounding box (z = 0), so the template
* is centred around (0, 0, 0); the objects keep their ids and world positions. The container goes
* below the roots' shared parent, or at the top level when they do not share one.
*/
export class CreateContainerCommand implements Command {
readonly label = 'Create container';
@@ -42,7 +39,6 @@ export class CreateContainerCommand implements Command {
const selected = [...new Set(this.objectIds)].map((id) => requireObject(project, this.sceneId, id));
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),
@@ -50,7 +46,7 @@ export class CreateContainerCommand implements Command {
z: 0,
};
// The subtrees that move into the container, in tree order (parents first).
// Members are collected in tree order, so a parent is always built before its children.
const roots = new Set(
subtreeRoots(
scene,
@@ -64,7 +60,6 @@ export class CreateContainerCommand implements Command {
const rootParents = new Set([...roots].map((id) => findObject(scene, 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'),
@@ -89,7 +84,6 @@ export class CreateContainerCommand implements Command {
});
}
// Scene: the container takes the roots' place, the members become its instance.
this.previous = members.map((o) => ({
id: o.id,
parentId: o.parentId,
+11 -44
View File
@@ -1,8 +1,6 @@
/**
* Undoable edits of a texture's frames – the named rectangles a sprite sheet is cut into
* (see `model/texture.ts`). Every rectangle that enters the model passes `normalizeFrameRect`
* here rather than at the call site: frames come from dragging in a zoomed view, so callers
* produce fractional and inverted rectangles as a matter of course.
* Every rectangle passes `normalizeFrameRect` here rather than at the call site: frames come from
* dragging in a zoomed view, so callers produce fractional and inverted rectangles routinely.
*/
import { newId } from '../ids';
import type { TextureAsset, TextureFrame } from '../model/Asset';
@@ -21,10 +19,8 @@ import { type Removed, removeById, removeFirst, restoreAt } from '../util/list';
import { type Command, CommandError } from './Command';
import { requireAsset } from './require';
/** The name new frames are derived from when the caller has nothing better to offer. */
const DEFAULT_NAME = 'Frame';
/** The asset as a texture, rejecting both "no such asset" and "that one is not a picture". */
function requireTexture(project: Project, assetId: string): TextureAsset {
const asset = requireAsset(project, assetId);
if (asset.type !== 'texture') throw new CommandError(`${asset.name} is not a texture`);
@@ -37,10 +33,7 @@ function requireFrame(asset: TextureAsset, frameId: string): TextureFrame {
return frame;
}
/**
* Adds one frame to a texture. The id is known before execution, which is what lets the caller
* select the new frame right after executing – the same trick `PlaceAssetCommand` uses.
*/
/** `frameId` is known before execution, so the caller can select the new frame right after executing. */
export class AddFrameCommand implements Command {
readonly label = 'Add frame';
readonly frameId = newId();
@@ -55,8 +48,7 @@ export class AddFrameCommand implements Command {
const asset = requireTexture(project, this.assetId);
asset.frames.push({
id: this.frameId,
// Resolved on every execution rather than in the constructor: a redo has to fit the
// names that exist when it runs, not the ones that existed when the command was built.
// Resolved per execution: a redo has to fit the names that exist when it runs.
name: uniqueFrameName(asset, this.name),
...normalizeFrameRect(this.rect, asset.width, asset.height),
});
@@ -69,13 +61,8 @@ export class AddFrameCommand implements Command {
}
/**
* Moves and resizes a frame – one command per drag, not one per pointer move.
*
* A drag previews itself by writing into the frame directly; on release the caller puts the
* starting rectangle back and executes this once, exactly as `SceneViewport` does for
* `MoveSceneObjectCommand`. The rectangle to revert to is therefore read from the model at
* execution time: a caller that forgets to undo its preview would record the preview as the
* state to return to, and the whole drag would collapse into one undo step of nothing.
* One command per drag, not per pointer move. The rectangle to revert to is read from the model at
* execution time, so a caller that previews a drag in place must restore the start rect before executing.
*/
export class SetFrameRectCommand implements Command {
readonly label = 'Move frame';
@@ -96,13 +83,11 @@ export class SetFrameRectCommand implements Command {
undo(project: Project): void {
const asset = requireTexture(project, this.assetId);
// Restored verbatim instead of normalized again: undo owes the user the state that was
// there, even if it came out of a document written before the image was measured.
// Restored verbatim rather than normalized again: undo owes the state that was there.
setFrameRect(requireFrame(asset, this.frameId), this.previous);
}
}
/** Renames a frame. The name is what the sprite's texture picker shows, so it may not be blank. */
export class RenameFrameCommand implements Command {
readonly label = 'Rename frame';
private previous = '';
@@ -128,12 +113,8 @@ export class RenameFrameCommand implements Command {
}
/**
* Removes a frame, putting it back at its former index on undo – the frame order is the order
* of the sheet, so appending it again would silently reshuffle the picture.
*
* Sprites wearing the frame are left pointing at it. A dangling reference reads as "no
* picture" everywhere (`resolveTexture`) and comes back by itself when this is undone, which
* clearing the references could not offer.
* Undo restores the former index, because frame order is sheet order. Sprites are left pointing at
* the frame: a dangling reference reads as "no picture" and comes back by itself on undo.
*/
export class RemoveFrameCommand implements Command {
readonly label = 'Remove frame';
@@ -157,14 +138,6 @@ export class RemoveFrameCommand implements Command {
}
}
/**
* Cuts a sheet into a grid of frames (see `sliceFrames`), either adding them to the frames
* already there or replacing those.
*
* A grid that yields nothing is rejected rather than applied as an empty change: it means the
* image has not been measured yet or the cells came out smaller than a pixel, and both look to
* the user like the Slice button doing nothing at all.
*/
export class SliceFramesCommand implements Command {
readonly label: string;
private created: TextureFrame[] = [];
@@ -188,8 +161,8 @@ export class SliceFramesCommand implements Command {
this.previous = [...asset.frames];
if (this.replace) asset.frames = [];
// A redo reuses the frames the first run created: while the command sat on the undo stack
// a sprite may have been pointed at one of them, and a fresh id would break that.
// A redo reuses the frames the first run created; a sprite may point at one, and fresh ids
// would break that.
if (this.created.length === 0) this.created = this.buildFrames(asset, rects);
else asset.frames.push(...this.created);
}
@@ -198,17 +171,11 @@ export class SliceFramesCommand implements Command {
requireTexture(project, this.assetId).frames = this.previous;
}
/**
* Frames named after their position in the sheet ("Frame 1" is the first cell in reading
* order). The whole run of names is asked for at once, so that keeping the existing frames
* shifts the numbering past them instead of suffixing a number onto a number.
*/
private buildFrames(asset: TextureAsset, rects: readonly FrameRect[]): TextureFrame[] {
const names = nextFrameNames(asset, rects.length, DEFAULT_NAME);
const created = rects.map((rect, index) => ({
id: newId(),
name: names[index]!,
// A no-op for a whole-pixel grid, but the offsets come from a form and may be anything.
...normalizeFrameRect(rect, asset.width, asset.height),
}));
asset.frames.push(...created);
+5 -10
View File
@@ -16,7 +16,6 @@ import { type Command, CommandError } from './Command';
import { requireBehavior, requireNode, requireNodeDef } from './require';
import { SetBagValueCommand } from './SetBagValueCommand';
/** Adds a node of the given type at graph coordinates. Params start at their defaults. */
export class AddNodeCommand implements Command {
readonly label: string;
readonly node: GraphNode;
@@ -28,8 +27,8 @@ export class AddNodeCommand implements Command {
y: number,
params: PropertyBag = {},
) {
// An unknown type is rejected in `execute`, not here: constructors run outside the
// editor's error handling, so throwing here would crash instead of showing a notice.
// An unknown type is rejected in `execute`: constructors run outside the editor's error
// handling, so throwing here would crash instead of showing a notice.
const def = nodeDef(type);
this.node = {
id: newId(),
@@ -80,8 +79,8 @@ export class RemoveNodeCommand implements Command {
}
/**
* Sets a node's graph position. A caller that previewed the drag must restore the node
* before executing: the previous position is read at execution time.
* The previous position is read at execution time, so a caller that previewed the drag must restore
* the node before executing.
*/
export class MoveNodeCommand implements Command {
readonly label = 'Move node';
@@ -134,10 +133,7 @@ export class SetNodeParamCommand extends SetBagValueCommand<GraphNode> {
}
}
/**
* Connects an output to an input. Validates port existence and type compatibility; a data
* input holds at most one edge, so an existing one is replaced (and restored on undo).
*/
/** A data input holds at most one edge, so an existing one is replaced (and restored on undo). */
export class ConnectCommand implements Command {
readonly label = 'Connect';
readonly edge: GraphEdge;
@@ -175,7 +171,6 @@ export class ConnectCommand implements Command {
}
}
/** Removes one edge. */
export class DisconnectCommand implements Command {
readonly label = 'Disconnect';
private removed: Removed<GraphEdge>[] = [];
-1
View File
@@ -1,7 +1,6 @@
import type { BehaviorAsset } from '../model/Asset';
import type { Project } from '../model/Project';
/** The first of `candidate(from)`, `candidate(from + 1)`, … that is not taken. */
function firstFree(taken: ReadonlySet<string>, candidate: (i: number) => string, from: number): string {
for (let i = from; ; i++) {
const name = candidate(i);
+3 -9
View File
@@ -1,8 +1,4 @@
/**
* Lookups that throw a `CommandError` when the target does not exist. Commands validate
* their inputs with these before mutating anything, so an invalid edit is rejected with a
* message the editor can show.
*/
/** Lookups that throw a `CommandError` when the target does not exist. */
import { findNode, type GraphNode } from '../graph/Graph';
import { type NodeDef, nodeDef } from '../graph/NodeRegistry';
import type { Asset, BehaviorAsset, SceneAsset } from '../model/Asset';
@@ -43,9 +39,8 @@ export function requireNode(project: Project, assetId: string, nodeId: string):
}
/**
* The definition of a node type. `NodeRegistry.requireNodeDef` throws a plain `Error`, which
* the editor does not catch – a command must reject an unknown type as a `CommandError` so it
* becomes a notice instead of a crash.
* `NodeRegistry.requireNodeDef` throws a plain `Error`, which the editor does not catch; a command
* needs a `CommandError` so an unknown type becomes a notice instead of a crash.
*/
export function requireNodeDef(type: string): NodeDef {
const def = nodeDef(type);
@@ -53,7 +48,6 @@ export function requireNodeDef(type: string): NodeDef {
return def;
}
/** The schema entry for `key` on the asset – a property the asset's type (or the behavior) declares. */
export function requirePropDef(asset: Asset, key: string): PropertyDef {
const def = propDef(asset, key);
if (!def) throw new CommandError(`Property "${key}" is not defined for ${asset.type}`);
+9 -20
View File
@@ -18,14 +18,12 @@ import { requireAsset, requireObject, requirePropDef, requireScene } from './req
import { SetBagValueCommand } from './SetBagValueCommand';
/**
* 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.
* 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(
@@ -54,7 +52,7 @@ export class PlaceAssetCommand implements Command {
}
}
/** Removes objects (a multi-selection) together with everything parented below them, as one undo step. */
/** Removes objects together with everything parented below them, as one undo step. */
export class RemoveSceneObjectsCommand implements Command {
readonly label: string;
private removed: Removed<SceneObject>[] = [];
@@ -76,7 +74,6 @@ export class RemoveSceneObjectsCommand implements Command {
}
}
/** Removes one object and everything parented to it. */
export class RemoveSceneObjectCommand extends RemoveSceneObjectsCommand {
constructor(sceneId: string, objectId: string) {
super(sceneId, [objectId]);
@@ -84,9 +81,8 @@ export class RemoveSceneObjectCommand extends RemoveSceneObjectsCommand {
}
/**
* Sets the full local position (x, y and draw order z) of a scene object. A caller that
* previewed the move must restore the model before executing: the previous value is read
* from the object at execution time.
* The previous position is read at execution time, so a caller that previewed the move must
* restore the model before executing.
*/
export class MoveSceneObjectCommand implements Command {
readonly label = 'Move object';
@@ -109,11 +105,7 @@ export class MoveSceneObjectCommand implements Command {
}
}
/**
* Sets the editor-only offset an object is drawn at in the scene view (see
* `SceneObject.editorOffset`). Undoable like any other edit, but it changes nothing the game
* can see – it only pulls overlapping objects apart while working on them.
*/
/** Editor-only: undoable like any other edit, but it changes nothing the game can see. */
export class SetEditorOffsetCommand implements Command {
readonly label = 'Editor offset';
private previous: Vec2 = { x: 0, y: 0 };
@@ -156,7 +148,6 @@ export class RenameSceneObjectCommand implements Command {
}
}
/** The half of `SetBagValueCommand` both override edits share: the bag is the object's overrides. */
abstract class OverrideCommand extends SetBagValueCommand<SceneObject> {
protected constructor(
private readonly sceneId: string,
@@ -176,7 +167,6 @@ abstract class OverrideCommand extends SetBagValueCommand<SceneObject> {
}
}
/** Sets a per-object override of an asset property. */
export class SetOverrideCommand extends OverrideCommand {
constructor(
sceneId: string,
@@ -192,22 +182,21 @@ export class SetOverrideCommand extends OverrideCommand {
}
}
/** Removes an override so the object follows its asset again. */
export class ClearOverrideCommand extends OverrideCommand {
constructor(sceneId: string, objectId: string, key: string) {
super(sceneId, objectId, key, `Reset ${key}`);
}
// Unlike setting one, clearing an override does not check the key against the schema: a
// key the asset no longer declares is exactly what one wants to be able to clear.
// 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 computeNext(): undefined {
return undefined;
}
}
/**
* Re-parents an object ('' = top level). The object keeps its world position: its local
* position is recomputed for the new parent. Cycles (an object below itself) are rejected.
* 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';
+3 -6
View File
@@ -5,7 +5,7 @@ import { type Removed, removeFirst, restoreAt } from '../util/list';
import { type Command, CommandError } from './Command';
import { requireBehavior } from './require';
/** Declares a new variable on a behavior; its default value is written to `props`. */
/** The variable's default value is written to `props` as well. */
export class AddVariableCommand implements Command {
readonly label: string;
readonly def: PropertyDef;
@@ -76,10 +76,7 @@ export class RemoveVariableCommand implements Command {
}
}
/**
* Renames a variable everywhere: definition, default, instance overrides (in scenes and in
* container templates) and graph nodes referring to it.
*/
/** Renames everywhere: definition, default, instance overrides and the graph nodes referring to it. */
export class RenameVariableCommand implements Command {
readonly label: string;
@@ -124,7 +121,7 @@ export class RenameVariableCommand implements Command {
const INVALID_NAME = 'Variable names must start with a letter and contain only letters, digits or _';
/** Identifier-like, and not one of the common property keys (a variable "enabled" would shadow the switch). */
/** Identifier-like, and not a common property key – a variable "enabled" would shadow the switch. */
export function isValidVariableName(name: string): boolean {
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && !isCommonProp(name);
}