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

+35 -31
View File
@@ -1,5 +1,5 @@
import { isHandleEnabled } from '../../core/graph';
import { type Axis, axisCorrection, colliderShape, colliderSize, isTrigger, shapesOverlap } from '../../core/properties/collider';
import { isHandleEnabled, worldPositionOf } from '../../core/graph';
import { axisCorrection, colliderShape, colliderSize, isTrigger, type PlaneAxis, shapesOverlap } from '../../core/properties/collider';
import type { Entity } from '../Entity';
import type { World } from '../World';
import type { System } from './System';
@@ -36,7 +36,7 @@ export class CollisionSystem implements System {
const a = colliders[i]!;
const b = colliders[j]!;
if (bodyOf(a) === bodyOf(b)) continue;
if (this.overlapping(world, a, b)) current.set(`${a.id}|${b.id}`, { a, b });
if (this.overlapping(a, b)) current.set(`${a.id}|${b.id}`, { a, b });
}
}
this.enters = [];
@@ -55,15 +55,15 @@ export class CollisionSystem implements System {
}
/** Live test of two collider entities (world positions applied). */
overlapping(world: World, a: Entity, b: Entity): boolean {
return shapesOverlap(colliderShape(a.props, world.worldPosition(a)), colliderShape(b.props, world.worldPosition(b)));
overlapping(a: Entity, b: Entity): boolean {
return shapesOverlap(shapeOf(a), shapeOf(b));
}
/** Whether any collider of `a` overlaps any collider of `b` (see `collidersOf`). */
touching(world: World, a: Entity, b: Entity): boolean {
for (const ca of collidersOf(world, a)) {
for (const cb of collidersOf(world, b)) {
if (ca !== cb && this.overlapping(world, ca, cb)) return true;
if (ca !== cb && this.overlapping(ca, cb)) return true;
}
}
return false;
@@ -80,20 +80,20 @@ export class CollisionSystem implements System {
*/
moveAndSlide(world: World, entity: Entity, dx: number, dy: number): { blockedX: boolean; blockedY: boolean } {
const own = collidersOf(world, entity).filter((c) => !isTrigger(c.props) && isHandleEnabled(c));
const obstacles = own.length === 0 ? [] : world.entities.filter((c) => c.type === 'collider' && !isTrigger(c.props) && isHandleEnabled(c) && !this.travelsWith(c, entity));
return { blockedX: this.slideAxis(world, entity, own, obstacles, 'x', dx), blockedY: this.slideAxis(world, entity, own, obstacles, 'y', dy) };
const obstacles = own.length === 0 ? [] : world.entities.filter((c) => c.type === 'collider' && !isTrigger(c.props) && isHandleEnabled(c) && !travelsWith(c, entity));
return { blockedX: this.slideAxis(entity, own, obstacles, 'x', dx), blockedY: this.slideAxis(entity, own, obstacles, 'y', dy) };
}
private slideAxis(world: World, entity: Entity, own: Entity[], obstacles: Entity[], axis: Axis, delta: number): boolean {
private slideAxis(entity: Entity, own: Entity[], obstacles: Entity[], axis: PlaneAxis, delta: number): boolean {
if (delta === 0 || own.length === 0) return false;
// Long steps are split so a fast mover cannot jump across a thin obstacle in one frame.
const steps = Math.min(MAX_STEPS, Math.max(1, Math.ceil(Math.abs(delta) / stepLimit(own, obstacles, axis))));
for (let i = 0; i < steps; i++) if (this.stepAxis(world, entity, own, obstacles, axis, delta / steps)) return true;
for (let i = 0; i < steps; i++) if (this.stepAxis(entity, own, obstacles, axis, delta / steps)) return true;
return false;
}
/** One indivisible move along `axis`; returns whether an obstacle stopped it. */
private stepAxis(world: World, entity: Entity, own: Entity[], obstacles: Entity[], axis: Axis, delta: number): boolean {
private stepAxis(entity: Entity, own: Entity[], obstacles: Entity[], axis: PlaneAxis, delta: number): boolean {
const dir: 1 | -1 = delta > 0 ? 1 : -1;
// How deep each pair already sits in the other before the step. A step may never deepen
// that, but resolving it is not our job (no jolt, and leaving a wall is always possible).
@@ -102,49 +102,53 @@ export class CollisionSystem implements System {
const before: [Entity, Entity, number][] = [];
for (const a of own) {
for (const b of obstacles) {
const p = this.penetration(world, a, b, axis, dir);
const p = penetration(a, b, axis, dir);
before.push([a, b, p > SKIN ? p : 0]);
}
}
entity[axis] += delta;
let correction = 0;
for (const [a, b, was] of before) {
const t = this.penetration(world, a, b, axis, dir) - was;
const t = penetration(a, b, axis, dir) - was;
if (t > correction) correction = t;
}
if (correction <= 0) return false;
entity[axis] -= dir * Math.min(correction, Math.abs(delta));
return true;
}
}
/**
* How far `a` would have to be pushed back against `dir` to just clear `b` (0 when they are
* apart). Once a's centre has passed b's centre along `dir` the shorter way out is forwards,
* so nothing blocks – that keeps an object that ended up inside something able to leave.
*/
private penetration(world: World, a: Entity, b: Entity, axis: Axis, dir: 1 | -1): number {
const mover = colliderShape(a.props, world.worldPosition(a));
const obstacle = colliderShape(b.props, world.worldPosition(b));
if (dir * (mover[axis] - obstacle[axis]) >= 0) return 0;
return axisCorrection(mover, obstacle, axis, dir);
}
function shapeOf(collider: Entity) {
return colliderShape(collider.props, worldPositionOf(collider));
}
/** Whether `collider` belongs to `entity` or to something parented (at any depth) under it. */
private travelsWith(collider: Entity, entity: Entity): boolean {
for (let e: Entity | null = collider; e; e = e.parent) if (e === entity) return true;
return false;
}
/**
* How far `a` would have to be pushed back against `dir` to just clear `b` (0 when they are
* apart). Once a's centre has passed b's centre along `dir` the shorter way out is forwards,
* so nothing blocks – that keeps an object that ended up inside something able to leave.
*/
function penetration(a: Entity, b: Entity, axis: PlaneAxis, dir: 1 | -1): number {
const mover = shapeOf(a);
const obstacle = shapeOf(b);
if (dir * (mover[axis] - obstacle[axis]) >= 0) return 0;
return axisCorrection(mover, obstacle, axis, dir);
}
/** Whether `collider` belongs to `entity` or to something parented (at any depth) under it. */
function travelsWith(collider: Entity, entity: Entity): boolean {
for (let e: Entity | null = collider; e; e = e.parent) if (e === entity) return true;
return false;
}
/** Longest safe step along `axis`: half of the thinnest collider involved (never below 1). */
function stepLimit(own: Entity[], obstacles: Entity[], axis: Axis): number {
function stepLimit(own: Entity[], obstacles: Entity[], axis: PlaneAxis): number {
let min = Infinity;
for (const e of own) min = Math.min(min, extentOf(e, axis));
for (const e of obstacles) min = Math.min(min, extentOf(e, axis));
return Math.max(1, min / 2);
}
function extentOf(collider: Entity, axis: Axis): number {
function extentOf(collider: Entity, axis: PlaneAxis): number {
const size = colliderSize(collider.props);
return axis === 'x' ? size.w : size.h;
}