Add ESLint and Prettier, and close the node test gaps
Tooling: - Prettier as the single formatting authority (110 columns, single quotes, measured against the existing style); Markdown excluded because table-cell padding turns the README glossary into 1000-character lines. - Type-aware ESLint ending in eslint-config-prettier, so a rule always means "this is a bug", never "this looks different". - .editorconfig and an `npm run check` that runs typecheck, lint, format and tests in one go. Findings fixed on the first run: three more duplicate imports, four String(unknown) calls (parameter types narrowed instead of casts), two dead initialisers, two redundant casts, a console.log for a real error. Two warnings were false here and are disabled with a reason in place. Tests 132 -> 269: - node-catalog.test.ts covers every node with a run(), with a guard that fails when a new node has no row. - behavior-events.test.ts covers the ten event nodes through a running World, checking timing rather than existence. - Input's state machine (press/release/releaseAll) is reachable without DOM events now; the DOM handlers are thin adapters. Compare fell back to == without a configured operator although the schema default is <; aligned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f140c2fd77
commit
d54a620ebd
113 files changed
+5049
-666
No files matched your search
@@ -6,7 +6,10 @@ export class CommandStack {
|
||||
private readonly undoStack: Command[] = [];
|
||||
private readonly redoStack: Command[] = [];
|
||||
|
||||
constructor(private readonly project: Project, private readonly limit = 200) {}
|
||||
constructor(
|
||||
private readonly project: Project,
|
||||
private readonly limit = 200,
|
||||
) {}
|
||||
|
||||
execute(command: Command): void {
|
||||
command.execute(this.project);
|
||||
|
||||
@@ -7,7 +7,10 @@ import type { Command } from './Command';
|
||||
* so the project is never left half-changed.
|
||||
*/
|
||||
export class CompoundCommand implements Command {
|
||||
constructor(readonly label: string, private readonly commands: readonly Command[]) {}
|
||||
constructor(
|
||||
readonly label: string,
|
||||
private readonly commands: readonly Command[],
|
||||
) {}
|
||||
|
||||
execute(project: Project): void {
|
||||
const done: Command[] = [];
|
||||
|
||||
@@ -15,7 +15,10 @@ export abstract class SetBagValueCommand<T> implements Command {
|
||||
readonly label: string;
|
||||
private previous: PropertyValue | undefined;
|
||||
|
||||
protected constructor(protected readonly key: string, label: string) {
|
||||
protected constructor(
|
||||
protected readonly key: string,
|
||||
label: string,
|
||||
) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,9 +55,18 @@ interface SlotMemo<T> {
|
||||
* with the template along – copies the user changed on their own keep their value. Returns
|
||||
* the memos in application order; `restoreSlots` undoes them.
|
||||
*/
|
||||
function applyToTemplate<T>(project: Project, link: TemplateLink, slots: readonly Slot<T>[], values: readonly T[]): SlotMemo<T>[] {
|
||||
function applyToTemplate<T>(
|
||||
project: Project,
|
||||
link: TemplateLink,
|
||||
slots: readonly Slot<T>[],
|
||||
values: readonly T[],
|
||||
): SlotMemo<T>[] {
|
||||
const before = slots.map((slot) => slot.read(link.template));
|
||||
const memos: SlotMemo<T>[] = slots.map((slot, i) => ({ object: link.template, slot, previous: before[i]! }));
|
||||
const memos: SlotMemo<T>[] = slots.map((slot, i) => ({
|
||||
object: link.template,
|
||||
slot,
|
||||
previous: before[i]!,
|
||||
}));
|
||||
for (const { object } of instancesOf(project, link.template.id)) {
|
||||
slots.forEach((slot, i) => {
|
||||
if (slot.read(object) !== before[i]) return;
|
||||
@@ -84,7 +93,11 @@ export class ApplyOverrideToAssetCommand implements Command {
|
||||
private previousOverride: PropertyValue | undefined;
|
||||
private templateMemos: SlotMemo<PropertyValue | undefined>[] = [];
|
||||
|
||||
constructor(private readonly sceneId: string, private readonly objectId: string, private readonly key: string) {
|
||||
constructor(
|
||||
private readonly sceneId: string,
|
||||
private readonly objectId: string,
|
||||
private readonly key: string,
|
||||
) {
|
||||
this.label = `Apply ${key} to asset`;
|
||||
}
|
||||
|
||||
@@ -116,7 +129,11 @@ export class ApplyOverrideToContainerCommand implements Command {
|
||||
readonly label: string;
|
||||
private memos: SlotMemo<PropertyValue | undefined>[] = [];
|
||||
|
||||
constructor(private readonly sceneId: string, private readonly objectId: string, private readonly key: string) {
|
||||
constructor(
|
||||
private readonly sceneId: string,
|
||||
private readonly objectId: string,
|
||||
private readonly key: string,
|
||||
) {
|
||||
this.label = `Apply ${key} to container`;
|
||||
}
|
||||
|
||||
@@ -136,7 +153,11 @@ export class RevertOverrideToTemplateCommand implements Command {
|
||||
readonly label: string;
|
||||
private previous: PropertyValue | undefined;
|
||||
|
||||
constructor(private readonly sceneId: string, private readonly objectId: string, private readonly key: string) {
|
||||
constructor(
|
||||
private readonly sceneId: string,
|
||||
private readonly objectId: string,
|
||||
private readonly key: string,
|
||||
) {
|
||||
this.label = `Revert ${key}`;
|
||||
}
|
||||
|
||||
@@ -160,7 +181,11 @@ export class ApplyPositionToContainerCommand implements Command {
|
||||
readonly label: string;
|
||||
private memos: SlotMemo<number>[] = [];
|
||||
|
||||
constructor(private readonly sceneId: string, private readonly objectId: string, private readonly axis?: Axis) {
|
||||
constructor(
|
||||
private readonly sceneId: string,
|
||||
private readonly objectId: string,
|
||||
private readonly axis?: Axis,
|
||||
) {
|
||||
this.label = axis ? `Apply ${axis} to container` : 'Apply position to container';
|
||||
}
|
||||
|
||||
@@ -168,7 +193,12 @@ export class ApplyPositionToContainerCommand implements Command {
|
||||
const object = requireObject(project, this.sceneId, this.objectId);
|
||||
const link = requireLink(project, this.sceneId, object);
|
||||
const axes = this.axis ? [this.axis] : ALL_AXES;
|
||||
this.memos = applyToTemplate(project, link, axes.map(positionSlot), axes.map((a) => object.position[a]));
|
||||
this.memos = applyToTemplate(
|
||||
project,
|
||||
link,
|
||||
axes.map(positionSlot),
|
||||
axes.map((a) => object.position[a]),
|
||||
);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
@@ -181,7 +211,11 @@ export class RevertPositionToTemplateCommand implements Command {
|
||||
readonly label: string;
|
||||
private previous: Position = { x: 0, y: 0, z: 0 };
|
||||
|
||||
constructor(private readonly sceneId: string, private readonly objectId: string, private readonly axis?: Axis) {
|
||||
constructor(
|
||||
private readonly sceneId: string,
|
||||
private readonly objectId: string,
|
||||
private readonly axis?: Axis,
|
||||
) {
|
||||
this.label = axis ? `Revert ${axis}` : 'Revert position';
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,10 @@ export class RenameAssetCommand implements Command {
|
||||
readonly label = 'Rename asset';
|
||||
private previousName = '';
|
||||
|
||||
constructor(private readonly assetId: string, private readonly name: string) {}
|
||||
constructor(
|
||||
private readonly assetId: string,
|
||||
private readonly name: string,
|
||||
) {}
|
||||
|
||||
execute(project: Project): void {
|
||||
const asset = requireAsset(project, this.assetId);
|
||||
@@ -119,7 +122,11 @@ 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) {
|
||||
protected constructor(
|
||||
private readonly assetId: string,
|
||||
key: string,
|
||||
label: string,
|
||||
) {
|
||||
super(key, label);
|
||||
}
|
||||
|
||||
@@ -134,7 +141,11 @@ abstract class AssetPropertyCommand extends SetBagValueCommand<Asset> {
|
||||
|
||||
/** Sets an asset property (coerced against its schema entry). */
|
||||
export class SetAssetPropertyCommand extends AssetPropertyCommand {
|
||||
constructor(assetId: string, key: string, private readonly value: PropertyValue) {
|
||||
constructor(
|
||||
assetId: string,
|
||||
key: string,
|
||||
private readonly value: PropertyValue,
|
||||
) {
|
||||
super(assetId, key, `Set ${key}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,10 @@ export class AddColliderCommand implements Command {
|
||||
readonly assetId = newId();
|
||||
readonly objectId = newId();
|
||||
|
||||
constructor(private readonly sceneId: string, private readonly targetId: string) {}
|
||||
constructor(
|
||||
private readonly sceneId: string,
|
||||
private readonly targetId: string,
|
||||
) {}
|
||||
|
||||
execute(project: Project): void {
|
||||
const scene = requireScene(project, this.sceneId);
|
||||
@@ -26,7 +29,11 @@ export class AddColliderCommand implements Command {
|
||||
const targetAsset = requireAsset(project, target.assetId);
|
||||
if (targetAsset.type === 'collider') throw new CommandError('A collider cannot have a collider');
|
||||
|
||||
const asset = buildAsset('collider', nextAssetName(project, `${target.name || targetAsset.name} collider`), this.assetId);
|
||||
const asset = buildAsset(
|
||||
'collider',
|
||||
nextAssetName(project, `${target.name || targetAsset.name} collider`),
|
||||
this.assetId,
|
||||
);
|
||||
if (targetAsset.type === 'sprite') {
|
||||
const props = resolveSceneObjectProps(targetAsset, target);
|
||||
asset.props.width = Number(props.width);
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { newId } from '../ids';
|
||||
import type { ContainerAsset } from '../model/Asset';
|
||||
import { findObject, localPositionFor, subtreeIds, subtreeRoots, treeOrder, worldPosition } from '../model/hierarchy';
|
||||
import {
|
||||
findObject,
|
||||
localPositionFor,
|
||||
subtreeIds,
|
||||
subtreeRoots,
|
||||
treeOrder,
|
||||
worldPosition,
|
||||
} from '../model/hierarchy';
|
||||
import type { Position } from '../model/Properties';
|
||||
import { findScene, type Project } from '../model/Project';
|
||||
import { createSceneObject } from '../model/SceneObject';
|
||||
@@ -25,7 +32,10 @@ export class CreateContainerCommand implements Command {
|
||||
readonly objectId = newId();
|
||||
private previous: { id: string; parentId: string; position: Position; templateId: string }[] = [];
|
||||
|
||||
constructor(private readonly sceneId: string, private readonly objectIds: readonly string[]) {}
|
||||
constructor(
|
||||
private readonly sceneId: string,
|
||||
private readonly objectIds: readonly string[],
|
||||
) {}
|
||||
|
||||
execute(project: Project): void {
|
||||
const scene = requireScene(project, this.sceneId);
|
||||
@@ -41,14 +51,25 @@ export class CreateContainerCommand implements Command {
|
||||
};
|
||||
|
||||
// The subtrees that move into the container, in tree order (parents first).
|
||||
const roots = new Set(subtreeRoots(scene, selected.map((o) => o.id)));
|
||||
const roots = new Set(
|
||||
subtreeRoots(
|
||||
scene,
|
||||
selected.map((o) => o.id),
|
||||
),
|
||||
);
|
||||
const memberIds = subtreeIds(scene, roots);
|
||||
const members = treeOrder(scene).map((e) => e.object).filter((o) => memberIds.has(o.id));
|
||||
const members = treeOrder(scene)
|
||||
.map((e) => e.object)
|
||||
.filter((o) => memberIds.has(o.id));
|
||||
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'), this.assetId) as ContainerAsset;
|
||||
const asset = buildAsset(
|
||||
'container',
|
||||
nextAssetName(project, 'Container'),
|
||||
this.assetId,
|
||||
) as ContainerAsset;
|
||||
const templateIds = new Map(members.map((o) => [o.id, newId()]));
|
||||
for (const object of members) {
|
||||
const isRoot = roots.has(object.id);
|
||||
@@ -57,18 +78,30 @@ export class CreateContainerCommand implements Command {
|
||||
id: templateIds.get(object.id)!,
|
||||
assetId: object.assetId,
|
||||
name: object.name,
|
||||
position: isRoot ? { x: world.x - centre.x, y: world.y - centre.y, z: world.z } : { ...object.position },
|
||||
position: isRoot
|
||||
? { x: world.x - centre.x, y: world.y - centre.y, z: world.z }
|
||||
: { ...object.position },
|
||||
// A view-only offset belongs to the placed object, not to the template (see container.ts).
|
||||
editorOffset: { x: 0, y: 0 },
|
||||
overrides: { ...object.overrides },
|
||||
parentId: isRoot ? '' : templateIds.get(object.parentId) ?? '',
|
||||
parentId: isRoot ? '' : (templateIds.get(object.parentId) ?? ''),
|
||||
templateId: '',
|
||||
});
|
||||
}
|
||||
|
||||
// Scene: the container takes the roots' place, the members become its instance.
|
||||
this.previous = members.map((o) => ({ id: o.id, parentId: o.parentId, position: { ...o.position }, templateId: o.templateId }));
|
||||
const container = createSceneObject(this.objectId, this.assetId, localPositionFor(scene, containerParentId, centre), containerParentId);
|
||||
this.previous = members.map((o) => ({
|
||||
id: o.id,
|
||||
parentId: o.parentId,
|
||||
position: { ...o.position },
|
||||
templateId: o.templateId,
|
||||
}));
|
||||
const container = createSceneObject(
|
||||
this.objectId,
|
||||
this.assetId,
|
||||
localPositionFor(scene, containerParentId, centre),
|
||||
containerParentId,
|
||||
);
|
||||
for (const object of members) {
|
||||
if (roots.has(object.id)) {
|
||||
const world = worldPosition(scene, object);
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { edgesInto, edgesOfNode, findNode, type GraphEdge, type GraphNode, portsCompatible } from '../graph/Graph';
|
||||
import {
|
||||
edgesInto,
|
||||
edgesOfNode,
|
||||
findNode,
|
||||
type GraphEdge,
|
||||
type GraphNode,
|
||||
portsCompatible,
|
||||
} from '../graph/Graph';
|
||||
import { nodeDef } from '../graph/NodeRegistry';
|
||||
import { newId } from '../ids';
|
||||
import type { PropertyBag, PropertyValue } from '../model/Properties';
|
||||
@@ -14,11 +21,23 @@ export class AddNodeCommand implements Command {
|
||||
readonly label: string;
|
||||
readonly node: GraphNode;
|
||||
|
||||
constructor(private readonly assetId: string, type: string, x: number, y: number, params: PropertyBag = {}) {
|
||||
constructor(
|
||||
private readonly assetId: string,
|
||||
type: string,
|
||||
x: number,
|
||||
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.
|
||||
const def = nodeDef(type);
|
||||
this.node = { id: newId(), type, x: Math.round(x), y: Math.round(y), params: { ...defaultsOf(def?.params ?? []), ...params } };
|
||||
this.node = {
|
||||
id: newId(),
|
||||
type,
|
||||
x: Math.round(x),
|
||||
y: Math.round(y),
|
||||
params: { ...defaultsOf(def?.params ?? []), ...params },
|
||||
};
|
||||
this.label = def ? `Add ${def.label}` : `Add ${type}`;
|
||||
}
|
||||
|
||||
@@ -38,7 +57,10 @@ export class RemoveNodeCommand implements Command {
|
||||
readonly label = 'Remove node';
|
||||
private removed?: { node: Removed<GraphNode>; edges: Removed<GraphEdge>[] };
|
||||
|
||||
constructor(private readonly assetId: string, private readonly nodeId: string) {}
|
||||
constructor(
|
||||
private readonly assetId: string,
|
||||
private readonly nodeId: string,
|
||||
) {}
|
||||
|
||||
execute(project: Project): void {
|
||||
const graph = requireBehavior(project, this.assetId).graph;
|
||||
@@ -65,7 +87,12 @@ export class MoveNodeCommand implements Command {
|
||||
readonly label = 'Move node';
|
||||
private previous = { x: 0, y: 0 };
|
||||
|
||||
constructor(private readonly assetId: string, private readonly nodeId: string, private readonly x: number, private readonly y: number) {}
|
||||
constructor(
|
||||
private readonly assetId: string,
|
||||
private readonly nodeId: string,
|
||||
private readonly x: number,
|
||||
private readonly y: number,
|
||||
) {}
|
||||
|
||||
execute(project: Project): void {
|
||||
const node = requireNode(project, this.assetId, this.nodeId);
|
||||
@@ -83,7 +110,12 @@ export class MoveNodeCommand implements Command {
|
||||
|
||||
/** Sets a node param or the inline value of an unconnected data input. */
|
||||
export class SetNodeParamCommand extends SetBagValueCommand<GraphNode> {
|
||||
constructor(private readonly assetId: string, private readonly nodeId: string, key: string, private readonly value: PropertyValue) {
|
||||
constructor(
|
||||
private readonly assetId: string,
|
||||
private readonly nodeId: string,
|
||||
key: string,
|
||||
private readonly value: PropertyValue,
|
||||
) {
|
||||
super(key, `Set ${key}`);
|
||||
}
|
||||
|
||||
@@ -111,7 +143,11 @@ export class ConnectCommand implements Command {
|
||||
readonly edge: GraphEdge;
|
||||
private replaced: Removed<GraphEdge>[] = [];
|
||||
|
||||
constructor(private readonly assetId: string, from: { node: string; port: string }, to: { node: string; port: string }) {
|
||||
constructor(
|
||||
private readonly assetId: string,
|
||||
from: { node: string; port: string },
|
||||
to: { node: string; port: string },
|
||||
) {
|
||||
this.edge = { id: newId(), from: { ...from }, to: { ...to } };
|
||||
}
|
||||
|
||||
@@ -124,7 +160,8 @@ export class ConnectCommand implements Command {
|
||||
const out = requireNodeDef(fromNode.type).outputs.find((p) => p.key === this.edge.from.port);
|
||||
const inp = requireNodeDef(toNode.type).inputs.find((p) => p.key === this.edge.to.port);
|
||||
if (!out || !inp) throw new CommandError('Port not found');
|
||||
if (!portsCompatible(out.type, inp.type)) throw new CommandError(`Cannot connect ${out.type} to ${inp.type}`);
|
||||
if (!portsCompatible(out.type, inp.type))
|
||||
throw new CommandError(`Cannot connect ${out.type} to ${inp.type}`);
|
||||
if (graph.edges.some((e) => sameEnds(e, this.edge))) throw new CommandError('Already connected');
|
||||
const occupied = inp.type === 'exec' ? [] : edgesInto(graph, this.edge.to.node, this.edge.to.port);
|
||||
this.replaced = removeWhere(graph.edges, (e) => occupied.includes(e));
|
||||
@@ -143,7 +180,10 @@ export class DisconnectCommand implements Command {
|
||||
readonly label = 'Disconnect';
|
||||
private removed: Removed<GraphEdge>[] = [];
|
||||
|
||||
constructor(private readonly assetId: string, private readonly edgeId: string) {}
|
||||
constructor(
|
||||
private readonly assetId: string,
|
||||
private readonly edgeId: string,
|
||||
) {}
|
||||
|
||||
execute(project: Project): void {
|
||||
const graph = requireBehavior(project, this.assetId).graph;
|
||||
@@ -157,5 +197,10 @@ export class DisconnectCommand implements Command {
|
||||
}
|
||||
|
||||
function sameEnds(a: GraphEdge, b: GraphEdge): boolean {
|
||||
return a.from.node === b.from.node && a.from.port === b.from.port && a.to.node === b.to.node && a.to.port === b.to.port;
|
||||
return (
|
||||
a.from.node === b.from.node &&
|
||||
a.from.port === b.from.port &&
|
||||
a.to.node === b.to.node &&
|
||||
a.to.port === b.to.port
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,13 @@ import { newId } from '../ids';
|
||||
import { isPlaceableType } from '../model/AssetTypes';
|
||||
import { instantiateContainer } from '../model/container';
|
||||
import { isSelfOrAncestor, localPositionFor, removeSubtrees, worldPosition } from '../model/hierarchy';
|
||||
import { type Position, type PropertyBag, type PropertyValue, toPosition, type Vec2 } from '../model/Properties';
|
||||
import {
|
||||
type Position,
|
||||
type PropertyBag,
|
||||
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';
|
||||
@@ -53,7 +59,10 @@ export class RemoveSceneObjectsCommand implements Command {
|
||||
readonly label: string;
|
||||
private removed: Removed<SceneObject>[] = [];
|
||||
|
||||
constructor(private readonly sceneId: string, private readonly objectIds: readonly string[]) {
|
||||
constructor(
|
||||
private readonly sceneId: string,
|
||||
private readonly objectIds: readonly string[],
|
||||
) {
|
||||
this.label = objectIds.length === 1 ? 'Remove object' : 'Remove objects';
|
||||
}
|
||||
|
||||
@@ -83,7 +92,11 @@ export class MoveSceneObjectCommand implements Command {
|
||||
readonly label = 'Move object';
|
||||
private previous: Position = { x: 0, y: 0, z: 0 };
|
||||
|
||||
constructor(private readonly sceneId: string, private readonly objectId: string, private readonly position: Position) {}
|
||||
constructor(
|
||||
private readonly sceneId: string,
|
||||
private readonly objectId: string,
|
||||
private readonly position: Position,
|
||||
) {}
|
||||
|
||||
execute(project: Project): void {
|
||||
const object = requireObject(project, this.sceneId, this.objectId);
|
||||
@@ -105,7 +118,11 @@ export class SetEditorOffsetCommand implements Command {
|
||||
readonly label = 'Editor offset';
|
||||
private previous: Vec2 = { x: 0, y: 0 };
|
||||
|
||||
constructor(private readonly sceneId: string, private readonly objectId: string, private readonly offset: Vec2) {}
|
||||
constructor(
|
||||
private readonly sceneId: string,
|
||||
private readonly objectId: string,
|
||||
private readonly offset: Vec2,
|
||||
) {}
|
||||
|
||||
execute(project: Project): void {
|
||||
const object = requireObject(project, this.sceneId, this.objectId);
|
||||
@@ -122,7 +139,11 @@ export class RenameSceneObjectCommand implements Command {
|
||||
readonly label = 'Rename object';
|
||||
private previous = '';
|
||||
|
||||
constructor(private readonly sceneId: string, private readonly objectId: string, private readonly name: string) {}
|
||||
constructor(
|
||||
private readonly sceneId: string,
|
||||
private readonly objectId: string,
|
||||
private readonly name: string,
|
||||
) {}
|
||||
|
||||
execute(project: Project): void {
|
||||
const object = requireObject(project, this.sceneId, this.objectId);
|
||||
@@ -137,7 +158,12 @@ 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, private readonly objectId: string, key: string, label: string) {
|
||||
protected constructor(
|
||||
private readonly sceneId: string,
|
||||
private readonly objectId: string,
|
||||
key: string,
|
||||
label: string,
|
||||
) {
|
||||
super(key, label);
|
||||
}
|
||||
|
||||
@@ -152,7 +178,12 @@ abstract class OverrideCommand extends SetBagValueCommand<SceneObject> {
|
||||
|
||||
/** Sets a per-object override of an asset property. */
|
||||
export class SetOverrideCommand extends OverrideCommand {
|
||||
constructor(sceneId: string, objectId: string, key: string, private readonly value: PropertyValue) {
|
||||
constructor(
|
||||
sceneId: string,
|
||||
objectId: string,
|
||||
key: string,
|
||||
private readonly value: PropertyValue,
|
||||
) {
|
||||
super(sceneId, objectId, key, `Override ${key}`);
|
||||
}
|
||||
|
||||
@@ -180,9 +211,16 @@ export class ClearOverrideCommand extends OverrideCommand {
|
||||
*/
|
||||
export class SetParentCommand implements Command {
|
||||
readonly label = 'Set parent';
|
||||
private previous: { parentId: string; position: Position } = { parentId: '', position: { x: 0, y: 0, z: 0 } };
|
||||
private previous: { parentId: string; position: Position } = {
|
||||
parentId: '',
|
||||
position: { x: 0, y: 0, z: 0 },
|
||||
};
|
||||
|
||||
constructor(private readonly sceneId: string, private readonly objectId: string, private readonly parentId: string) {}
|
||||
constructor(
|
||||
private readonly sceneId: string,
|
||||
private readonly objectId: string,
|
||||
private readonly parentId: string,
|
||||
) {}
|
||||
|
||||
execute(project: Project): void {
|
||||
const scene = requireScene(project, this.sceneId);
|
||||
@@ -190,7 +228,8 @@ export class SetParentCommand implements Command {
|
||||
if (this.parentId) {
|
||||
if (this.parentId === this.objectId) throw new CommandError('An object cannot be its own parent');
|
||||
requireObject(project, this.sceneId, this.parentId);
|
||||
if (isSelfOrAncestor(scene, this.objectId, this.parentId)) throw new CommandError('An object cannot be parented to one of its own children');
|
||||
if (isSelfOrAncestor(scene, 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(scene, object);
|
||||
|
||||
@@ -10,7 +10,11 @@ export class AddVariableCommand implements Command {
|
||||
readonly label: string;
|
||||
readonly def: PropertyDef;
|
||||
|
||||
constructor(private readonly assetId: string, key: string, kind: VariableKind) {
|
||||
constructor(
|
||||
private readonly assetId: string,
|
||||
key: string,
|
||||
kind: VariableKind,
|
||||
) {
|
||||
this.def = variableDef(key, kind);
|
||||
this.label = `Add variable ${key}`;
|
||||
}
|
||||
@@ -18,7 +22,8 @@ export class AddVariableCommand implements Command {
|
||||
execute(project: Project): void {
|
||||
const asset = requireBehavior(project, this.assetId);
|
||||
if (!isValidVariableName(this.def.key)) throw new CommandError(INVALID_NAME);
|
||||
if (asset.variables.some((v) => v.key === this.def.key)) throw new CommandError(`Variable "${this.def.key}" already exists`);
|
||||
if (asset.variables.some((v) => v.key === this.def.key))
|
||||
throw new CommandError(`Variable "${this.def.key}" already exists`);
|
||||
asset.variables.push(this.def);
|
||||
asset.props[this.def.key] = this.def.default;
|
||||
}
|
||||
@@ -36,7 +41,10 @@ export class RemoveVariableCommand implements Command {
|
||||
private removed?: { def: Removed<PropertyDef>; value: PropertyValue | undefined };
|
||||
private overrides: { holderId: string; objectId: string; value: PropertyValue }[] = [];
|
||||
|
||||
constructor(private readonly assetId: string, private readonly key: string) {
|
||||
constructor(
|
||||
private readonly assetId: string,
|
||||
private readonly key: string,
|
||||
) {
|
||||
this.label = `Remove variable ${key}`;
|
||||
}
|
||||
|
||||
@@ -75,7 +83,11 @@ export class RemoveVariableCommand implements Command {
|
||||
export class RenameVariableCommand implements Command {
|
||||
readonly label: string;
|
||||
|
||||
constructor(private readonly assetId: string, private readonly from: string, private readonly to: string) {
|
||||
constructor(
|
||||
private readonly assetId: string,
|
||||
private readonly from: string,
|
||||
private readonly to: string,
|
||||
) {
|
||||
this.label = `Rename variable ${from}`;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user