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:
1 parent
f16801aef3
commit
4d84231b9f
115 files changed
+707
-1564
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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,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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user