Refactoring: one file per node, consistent comments, shared helpers, split large modules

- Graph: nodes.ts split into nodes/<category>/<Type>.ts (62 files) with
  shared.ts factories and index.ts registry; NodeContext/values/NodeRegistry
  separated, writeTyped() replaces duplicated typed writes
- Comments: one style (JSDoc on non-obvious exports, // for decisions and
  traps), wrong/stale comments fixed or removed, ids.ts now real 10 base-36 chars
- Core: chainOf, shared worldPosition/editorPosition core, commands/require.ts,
  commands/naming.ts, util/list.ts, setBagValue/toPosition/propDef; dead code
  removed (isBehaviorAsset, SCENE_OBJECT_SCHEMA, CommandStack.subscribe,
  Input.axis, setMode, titleText, World.worldPosition, unused ctor params)
- Editor: Inspector split into inspector/ sections, App.ts into Editor.ts +
  debugHook.ts + shortcuts.ts, Viewport into items.ts + symbols.ts, shared
  "Add collider" action, unified type labels/swatches
- UI toolkit: Widget.trackHover/onPointerRelease, drawControlBox, math helpers,
  Popup.openBelow, Menu/Dialog/ColorPicker/ListRow in own files, named Theme sizes
- Tools: seed-demo/smoke-demo and demo-check project removed, tools/lib
  (harness, projects, builders), e2e.mjs as runner over tools/e2e/*.mjs sections
- Tests: tests/helpers.ts replaces copied fixtures and NodeContext doubles

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-08 08:37:22 +02:00
1 parent 3234eb480f
commit 1eb28314cf
189 files changed
+6231 -5543

No files matched your search

+40 -52
View File
@@ -1,35 +1,22 @@
import { describe, expect, it } from 'vitest';
import { CreateAssetCommand, DeleteAssetCommand } from '../../src/core/commands/assetCommands';
import { CommandStack } from '../../src/core/commands/CommandStack';
import { DeleteAssetCommand } from '../../src/core/commands/assetCommands';
import { CompoundCommand } from '../../src/core/commands/CompoundCommand';
import { CreateContainerCommand } from '../../src/core/commands/containerCommands';
import { MoveSceneObjectCommand, PlaceAssetCommand, RemoveSceneObjectsCommand, SetParentCommand } from '../../src/core/commands/sceneObjectCommands';
import { MoveSceneObjectCommand, RemoveSceneObjectsCommand, SetParentCommand } from '../../src/core/commands/sceneObjectCommands';
import { worldPositionOf } from '../../src/core/graph';
import type { ContainerAsset } from '../../src/core/model/Asset';
import { subtreeRoots, treeOrder, worldPosition } from '../../src/core/model/hierarchy';
import { createEmptyProject, findScene } from '../../src/core/model/Project';
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
import { World } from '../../src/runtime/World';
import { subtreeRoots, treeOrder } from '../../src/core/model/hierarchy';
import { findAsset, findScene } from '../../src/core/model/Project';
import { makeSpriteScene, place, roundTrip, startWorld } from '../helpers';
/**
* Scene with a sprite asset and three boxes: a at (100,100,z1), b at (10,0) and c at (0,5)
* parented under b (local (-10,5)).
*/
/** Three boxes: a at (100,100,z1), b at (10,0) and c at (0,5) parented under b (local (-10,5)). */
function setup() {
const project = createEmptyProject('p', 'Containers');
const stack = new CommandStack(project);
const scene = new CreateAssetCommand('scene', 'Level');
const sprite = new CreateAssetCommand('sprite', 'Box');
stack.execute(scene);
stack.execute(sprite);
const a = new PlaceAssetCommand(scene.asset.id, sprite.asset.id, { x: 100, y: 100, z: 1 });
const b = new PlaceAssetCommand(scene.asset.id, sprite.asset.id, { x: 10, y: 0 });
const c = new PlaceAssetCommand(scene.asset.id, sprite.asset.id, { x: 0, y: 5 });
for (const cmd of [a, b, c]) stack.execute(cmd);
stack.execute(new SetParentCommand(scene.asset.id, c.objectId, b.objectId));
const sceneOf = () => findScene(project, scene.asset.id)!;
const object = (id: string) => sceneOf().objects.find((o) => o.id === id)!;
const world = (id: string) => worldPosition(sceneOf(), object(id));
return { project, stack, sceneId: scene.asset.id, spriteId: sprite.asset.id, a: a.objectId, b: b.objectId, c: c.objectId, sceneOf, object, world };
const f = makeSpriteScene();
const a = place(f, f.spriteId, { x: 100, y: 100, z: 1 });
const b = place(f, f.spriteId, { x: 10, y: 0 });
const c = place(f, f.spriteId, { x: 0, y: 5 });
f.stack.execute(new SetParentCommand(f.sceneId, c, b));
return { ...f, a, b, c };
}
describe('subtree roots', () => {
@@ -60,7 +47,7 @@ describe('CreateContainerCommand', () => {
expect({ a: world(a), b: world(b), c: world(c) }).toEqual(before);
// The asset holds the same structure with private ids; the members point at it.
const asset = project.assets.find((x) => x.id === command.assetId) as ContainerAsset;
const asset = findAsset(project, command.assetId) as ContainerAsset;
expect(asset.type).toBe('container');
expect(asset.objects).toHaveLength(3);
const template = (id: string) => asset.objects.find((t) => t.id === object(id).templateId)!;
@@ -83,17 +70,17 @@ describe('CreateContainerCommand', () => {
});
it('places the container below the parent all roots share', () => {
const { stack, sceneId, spriteId, b, c, object, world } = setup();
const d = new PlaceAssetCommand(sceneId, spriteId, { x: 30, y: 25 }, b);
stack.execute(d);
const command = new CreateContainerCommand(sceneId, [c, d.objectId]);
const f = setup();
const { stack, sceneId, spriteId, b, c, object, world } = f;
const d = place(f, spriteId, { x: 30, y: 25 }, b);
const command = new CreateContainerCommand(sceneId, [c, d]);
stack.execute(command);
const container = object(command.objectId);
expect(container.parentId).toBe(b);
// World centre of c (0,5) and d (30,25) is (15,15); b is at (10,0) → local (5,15).
expect(container.position).toEqual({ x: 5, y: 15, z: 0 });
expect(world(c)).toEqual({ x: 0, y: 5, z: 0 });
expect(world(d.objectId)).toEqual({ x: 30, y: 25, z: 0 });
expect(world(d)).toEqual({ x: 30, y: 25, z: 0 });
});
it('rejects an empty or unknown selection', () => {
@@ -105,19 +92,19 @@ describe('CreateContainerCommand', () => {
describe('placing a container', () => {
it('instantiates the template with fresh ids below the placed object', () => {
const { project, stack, sceneId, a, b, c, sceneOf, object, world } = setup();
const f = setup();
const { project, stack, sceneId, a, b, c, sceneOf, object, world } = f;
const create = new CreateContainerCommand(sceneId, [a, b]);
stack.execute(create);
const place = new PlaceAssetCommand(sceneId, create.assetId, { x: 1000, y: 0 });
stack.execute(place);
const placed = place(f, create.assetId, { x: 1000, y: 0 });
const copies = sceneOf().objects.filter((o) => ![a, b, c, create.objectId].includes(o.id));
expect(copies).toHaveLength(4); // container + 3 template objects
const root = object(place.objectId);
const root = object(placed);
expect(root.assetId).toBe(create.assetId);
const asset = project.assets.find((x) => x.id === create.assetId) as ContainerAsset;
const asset = findAsset(project, create.assetId) as ContainerAsset;
const copyOf = (id: string) => copies.find((o) => o.templateId === object(id).templateId)!;
expect(copyOf(a).parentId).toBe(place.objectId);
expect(copyOf(a).parentId).toBe(placed);
expect(world(copyOf(a).id)).toEqual({ x: 1045, y: 50, z: 1 });
expect(copyOf(c).parentId).toBe(copyOf(b).id);
expect(world(copyOf(c).id)).toEqual({ x: 945, y: -45, z: 0 });
@@ -126,15 +113,15 @@ describe('placing a container', () => {
stack.undo();
expect(sceneOf().objects).toHaveLength(4);
expect(sceneOf().objects.some((o) => o.id === place.objectId)).toBe(false);
expect(sceneOf().objects.some((o) => o.id === placed)).toBe(false);
});
it('survives a save/load round trip', () => {
const { project, stack, sceneId, a, b } = setup();
const create = new CreateContainerCommand(sceneId, [a, b]);
stack.execute(create);
const loaded = deserializeProject(serializeProject(project));
const asset = loaded.assets.find((x) => x.id === create.assetId) as ContainerAsset;
const loaded = roundTrip(project);
const asset = findAsset(loaded, create.assetId) as ContainerAsset;
expect(asset.type).toBe('container');
expect(asset.objects).toHaveLength(3);
const scene = findScene(loaded, sceneId)!;
@@ -143,43 +130,44 @@ describe('placing a container', () => {
});
it('deleting an asset removes its template objects from containers too', () => {
const { project, stack, sceneId, spriteId, a, b } = setup();
const { project, stack, sceneId, spriteId, a, b, sceneOf } = setup();
const create = new CreateContainerCommand(sceneId, [a, b]);
stack.execute(create);
const asset = () => project.assets.find((x) => x.id === create.assetId) as ContainerAsset;
const asset = () => findAsset(project, create.assetId) as ContainerAsset;
stack.execute(new DeleteAssetCommand(spriteId));
expect(asset().objects).toHaveLength(0);
expect(findScene(project, sceneId)!.objects.map((o) => o.id)).toEqual([create.objectId]);
expect(sceneOf().objects.map((o) => o.id)).toEqual([create.objectId]);
stack.undo();
expect(asset().objects).toHaveLength(3);
expect(findScene(project, sceneId)!.objects).toHaveLength(4);
expect(sceneOf().objects).toHaveLength(4);
});
it('spawns a container with its content at runtime', () => {
const { project, stack, sceneId, a, b } = setup();
const f = setup();
const { stack, sceneId, a, b, sceneOf } = f;
const create = new CreateContainerCommand(sceneId, [a, b]);
stack.execute(create);
const world = World.instantiate(project, sceneId);
const { world } = startWorld(f);
const spawned = world.spawn(create.assetId, { x: 500, y: 500, z: 0 }, null)!;
expect(spawned.type).toBe('container');
const children = world.entities.filter((e) => e.parent === spawned);
expect(children).toHaveLength(2);
const grandchild = world.entities.find((e) => e.parent && children.includes(e.parent))!;
expect(world.worldPosition(children.find((e) => e.z === 1)!)).toEqual({ x: 545, y: 550, z: 1 });
expect(world.worldPosition(grandchild)).toEqual({ x: 445, y: 455, z: 0 });
expect(worldPositionOf(children.find((e) => e.z === 1)!)).toEqual({ x: 545, y: 550, z: 1 });
expect(worldPositionOf(grandchild)).toEqual({ x: 445, y: 455, z: 0 });
expect(world.syncToModel()).toBe(true);
expect(findScene(project, sceneId)!.objects).toHaveLength(8);
expect(sceneOf().objects).toHaveLength(8);
});
});
describe('multi-selection commands', () => {
it('removes several subtrees as one step, covering nested ids', () => {
const { stack, sceneId, a, b, c, sceneOf } = setup();
const { stack, sceneId, a, b, c, sceneOf, object } = setup();
stack.execute(new RemoveSceneObjectsCommand(sceneId, [b, c, a]));
expect(sceneOf().objects).toHaveLength(0);
stack.undo();
expect(sceneOf().objects.map((o) => o.id)).toEqual([a, b, c]);
expect(sceneOf().objects.find((o) => o.id === c)?.parentId).toBe(b);
expect(object(c).parentId).toBe(b);
});
it('a compound move rolls back entirely when one part fails', () => {