Two compilers that walk a graph the same way walk it through one emitter (#85)
game-maker / suite (push) Failing after 2m22s
game-maker / gate (push) Successful in 2m53s

Refactoring round two: the particle and shader compilers share emitGraph.ts (cycle detection,
duplicate names, emit order) and keep only their rules. FontLibrary and TextureLibrary sit on
FileLibrary (base URL, loading/failed bookkeeping, listeners); SoundLibrary keeps its own
contract. The three Create*Command share ImportFileCommand, the three renames share
RenameCommand - class names stay. bandBounds() in framing.ts and portNameField() in
portSection.ts replace in-file clones, and three comments that told history now state the rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-16 19:28:28 +02:00
1 parent bf0be66c9d
commit d3563897d8
16 files changed
+376 -379

No files matched your search

+1 -1
View File
@@ -361,7 +361,7 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
canvasRect: () => editor.stateMachineEditor.canvas.canvasRect(),
center: () => centreOf(editor.stateMachineEditor.canvas),
snapping: () => editor.stateMachineEditor.canvas.snapEnabled,
/** Where a state's hidden pin sits. It is no longer grabbable – `portGrabbable` is the check. */
/** Where a state's hidden pin sits; it cannot be grabbed – `portGrabbable` is the check. */
portAt: (nodeId: string, port: string, isOutput: boolean) =>
editor.stateMachineEditor.canvas.portGlobal(nodeId, port, isOutput),
portGrabbable: (nodeId: string, port: string, isOutput: boolean) =>
+11 -8
View File
@@ -68,16 +68,22 @@ export interface Framing {
panY: number;
}
function bandBounds(band: Band): { minX: number; maxX: number; minY: number; maxY: number } {
return {
minX: Math.min(band.x1, band.x2),
maxX: Math.max(band.x1, band.x2),
minY: Math.min(band.y1, band.y2),
maxY: Math.max(band.y1, band.y2),
};
}
/**
* Ids of every box the band overlaps. Nodes are large rectangles, so – unlike the scene view,
* which tests an object's position – touching counts: a band drawn across a node that visibly
* caught it would otherwise leave it out.
*/
export function boxesInBand(boxes: readonly Box[], band: Band): string[] {
const minX = Math.min(band.x1, band.x2);
const maxX = Math.max(band.x1, band.x2);
const minY = Math.min(band.y1, band.y2);
const maxY = Math.max(band.y1, band.y2);
const { minX, maxX, minY, maxY } = bandBounds(band);
return boxes
.filter((b) => b.x <= maxX && b.x + b.w >= minX && b.y <= maxY && b.y + b.h >= minY)
.map((b) => b.id);
@@ -90,10 +96,7 @@ export function boxesInBand(boxes: readonly Box[], band: Band): string[] {
* Enclosing it is a deliberate gesture, and it is the one the eye reads as "this box, too".
*/
export function boxesWithinBand(boxes: readonly Box[], band: Band): string[] {
const minX = Math.min(band.x1, band.x2);
const maxX = Math.max(band.x1, band.x2);
const minY = Math.min(band.y1, band.y2);
const maxY = Math.max(band.y1, band.y2);
const { minX, maxX, minY, maxY } = bandBounds(band);
return boxes
.filter((b) => b.x >= minX && b.x + b.w <= maxX && b.y >= minY && b.y + b.h <= maxY)
.map((b) => b.id);
+24 -25
View File
@@ -1143,6 +1143,21 @@ export class GraphCanvas extends Widget {
}
}
private launchBeads(assetId: string, trace: Trace | null): void {
if (!trace || assetId !== this.beadAsset) {
this.beadTimes.clear();
this.beadFired.clear();
this.beadAsset = assetId;
}
for (const [id, time] of trace?.edges ?? []) {
if (this.beadFired.get(id) === time) continue;
this.beadFired.set(id, time);
const times = this.beadTimes.get(id) ?? [];
this.beadTimes.set(id, times);
launchBead(times, time);
}
}
/** A port's centre in graph coordinates. */
private anchorPoint(side: PortSide | null): { x: number; y: number } | null {
if (!side) return null;
@@ -1170,9 +1185,7 @@ export class GraphCanvas extends Widget {
portGlobal(nodeId: string, port: string, isOutput: boolean): { x: number; y: number } | null {
const view = this.views.get(nodeId);
const p = view?.portPosition(port, isOutput);
if (!view || !p) return null;
const g = view.toGlobal({ x: p.x, y: p.y });
return { x: g.x, y: g.y };
return view && p ? globalPoint(view, p) : null;
}
/** The port a running edge drag would connect to, or null when the drop would do nothing. */
@@ -1222,8 +1235,7 @@ export class GraphCanvas extends Widget {
.drawnMarks()
.find((m) => m.edgeIds.includes(edgeId) && m.isOutput === isOutput);
if (!mark) return null;
const g = this.canvas.toGlobal({ x: mark.x, y: mark.y });
return { x: g.x, y: g.y, links: mark.edgeIds.length, label: mark.text };
return { ...globalPoint(this.canvas, mark), links: mark.edgeIds.length, label: mark.text };
}
nodeRect(nodeId: string): Rect | null {
@@ -1272,21 +1284,6 @@ export class GraphCanvas extends Widget {
return this.views.get(nodeId)?.inputLines ?? null;
}
private launchBeads(assetId: string, trace: Trace | null): void {
if (!trace || assetId !== this.beadAsset) {
this.beadTimes.clear();
this.beadFired.clear();
this.beadAsset = assetId;
}
for (const [id, time] of trace?.edges ?? []) {
if (this.beadFired.get(id) === time) continue;
this.beadFired.set(id, time);
const times = this.beadTimes.get(id) ?? [];
this.beadTimes.set(id, times);
launchBead(times, time);
}
}
beadsDrawn(): readonly { edgeId: string; progress: number; x: number; y: number }[] {
return this.edgeLayer.drawnBeads();
}
@@ -1298,9 +1295,7 @@ export class GraphCanvas extends Widget {
/** Global screen position of a node's title bar centre. */
nodeGlobal(nodeId: string): { x: number; y: number } | null {
const view = this.views.get(nodeId);
if (!view) return null;
const g = view.toGlobal({ x: view.w / 2, y: TITLE_HEIGHT / 2 });
return { x: g.x, y: g.y };
return view ? globalPoint(view, { x: view.w / 2, y: TITLE_HEIGHT / 2 }) : null;
}
/** Every group and comment as it is drawn: the box, the text on it, and both its colours. */
@@ -1334,8 +1329,7 @@ export class GraphCanvas extends Widget {
const local = corner
? { x: view.w - RESIZE_HANDLE / 2, y: view.h - RESIZE_HANDLE / 2 }
: { x: view.w / 2, y: ANNOTATION_TITLE_HEIGHT / 2 };
const g = view.toGlobal(local);
return { x: g.x, y: g.y };
return globalPoint(view, local);
}
/** The rectangle a box is drawn as, on screen – what a fit has to bring into view. */
@@ -1352,6 +1346,11 @@ export class GraphCanvas extends Widget {
type Rect = { x: number; y: number; w: number; h: number };
function globalPoint(target: Container, local: { x: number; y: number }): { x: number; y: number } {
const g = target.toGlobal({ x: local.x, y: local.y });
return { x: g.x, y: g.y };
}
function globalRect(target: Container, w: number, h: number): Rect {
const topLeft = target.toGlobal({ x: 0, y: 0 });
const bottomRight = target.toGlobal({ x: w, y: h });
+9 -13
View File
@@ -175,12 +175,7 @@ function buildPickedPortSection(
for (const port of ports) {
const toggle = writeToggle(host, assetId, node, port);
if (custom) {
const name = new TextField(port.key, {
width: NAME_WIDTH,
onChange: (next) =>
host.state.execute(new RenamePortCommand(assetId, node.id, port.key, next.trim())),
});
portRow(host, assetId, node, port, name, toggle);
portRow(host, assetId, node, port, portNameField(host, assetId, node, port), toggle);
continue;
}
const def = choices.find((d) => d.key === port.key);
@@ -236,13 +231,14 @@ export function buildPortSection(
: 'No outputs yet. Every output added here becomes a result of running this behavior.',
);
for (const port of ports) {
const name = new TextField(port.key, {
width: NAME_WIDTH,
onChange: (next) => host.state.execute(new RenamePortCommand(assetId, node.id, port.key, next.trim())),
});
portRow(host, assetId, node, port, name);
}
for (const port of ports) portRow(host, assetId, node, port, portNameField(host, assetId, node, port));
}
function portNameField(host: InspectorHost, assetId: string, node: GraphNode, port: DeclaredPort): TextField {
return new TextField(port.key, {
width: NAME_WIDTH,
onChange: (next) => host.state.execute(new RenamePortCommand(assetId, node.id, port.key, next.trim())),
});
}
function writeToggle(host: InspectorHost, assetId: string, node: GraphNode, port: DeclaredPort): Widget[] {