Three stores, three importers and three pickers that differ only in their type are one of each (#85)
game-maker / gate (push) Successful in 1m33s
game-maker / suite (push) Successful in 5m46s

Refactoring round one: HttpFontStore/HttpSoundStore/HttpTextureStore collapse into a generic
HttpFileStore over a FileStore contract; fontImport/soundImport/textureImport share assetImport
with an ImportKind per sort; ImagePicker/FontPicker/SoundPicker sit on FilePicker and describe
only their row. The group/comment drag logic leaves GraphCanvas for annotationDrag.ts (with its
own unit test), declaredRows.ts replaces the duplicated row build in particles.ts and shaders.ts,
and dead code in font/sound/compile/emission/transform/actions is gone. README: asset list,
check count 958, the two-job CI, the Scenes wiki page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Fable 5.1 committed 2026-09-16 17:38:54 +02:00
1 parent 15bdfdf07a
commit cc6db5f350
42 files changed
+1375 -1650

No files matched your search

+161
View File
@@ -0,0 +1,161 @@
/**
* What a drag in the graph carries and how it is written back. A group takes the nodes whose centre
* lies in it and the boxes inside it, a band may have caught nodes and boxes together, and either
* way a thing must move exactly once – a node inside a selected group is claimed twice and offset
* once. Pure over the graph and the drawn node boxes, so the rule is testable without a canvas.
*/
import { type AnnotationBox, SetAnnotationBoxCommand } from '../../core/commands/annotationCommands';
import type { Command } from '../../core/commands/Command';
import { MoveNodeCommand } from '../../core/commands/graphCommands';
import {
type AnnotationKind,
ANNOTATION_KINDS,
annotationsOf,
findAnnotation,
type Graph,
type GraphAnnotation,
} from '../../core/graph';
import { type Box, boxesCentredIn, type Point } from '../framing';
/** A box as the drag found it. */
export interface BoxSeed {
id: string;
kind: AnnotationKind;
start: AnnotationBox;
}
/** A node as the drag found it. */
export interface NodeSeed {
nodeId: string;
start: Point;
}
export interface Travellers {
boxes: BoxSeed[];
nodes: NodeSeed[];
}
export function boxOf(annotation: GraphAnnotation): AnnotationBox {
return { x: annotation.x, y: annotation.y, w: annotation.w, h: annotation.h };
}
/** Every group and comment as the rectangle it is drawn as – a box states its own size. */
export function annotationBoxes(graph: Graph): Box[] {
return ANNOTATION_KINDS.flatMap((kind) =>
annotationsOf(graph, kind).map((a) => ({ id: a.id, ...boxOf(a) })),
);
}
/** The boxes with these ids as drag seeds, `exceptId` left out (it is the grabbed one). */
export function boxSeeds(graph: Graph, ids: readonly string[], exceptId?: string): BoxSeed[] {
const seeds: BoxSeed[] = [];
for (const id of ids) {
if (id === exceptId) continue;
const found = findAnnotation(graph, id);
if (found) seeds.push({ id, kind: found.kind, start: boxOf(found.annotation) });
}
return seeds;
}
/**
* Everything one move gesture carries: the boxes it starts from, the boxes those hold, and every
* node beneath any of them – each node exactly once, however many groups claim it. `nodeBoxes`
* are the nodes as drawn, since a node's height is the view's to know.
*/
export function travellers(
graph: Graph,
nodeBoxes: readonly Box[],
seedBoxes: readonly BoxSeed[],
seedNodeIds: readonly string[],
): Travellers {
const boxes = new Map(seedBoxes.map((b) => [b.id, b]));
for (const seed of seedBoxes) {
if (seed.kind !== 'group') continue;
for (const nested of nestedMembers(graph, seed.id, seed.start))
if (!boxes.has(nested.id)) boxes.set(nested.id, nested);
}
const nodes = new Map<string, NodeSeed>();
for (const id of seedNodeIds) {
const node = graph.nodes.find((n) => n.id === id);
if (node) nodes.set(id, { nodeId: id, start: { x: node.x, y: node.y } });
}
for (const box of boxes.values()) {
if (box.kind !== 'group') continue;
for (const member of nodeMembers(graph, nodeBoxes, box.start))
if (!nodes.has(member.nodeId)) nodes.set(member.nodeId, member);
}
return { boxes: [...boxes.values()], nodes: [...nodes.values()] };
}
/** The nodes a group at this box carries – the centre rule, against what is drawn right now. */
function nodeMembers(graph: Graph, nodeBoxes: readonly Box[], box: AnnotationBox): NodeSeed[] {
const ids = new Set(boxesCentredIn(nodeBoxes, box));
return graph.nodes.filter((n) => ids.has(n.id)).map((n) => ({ nodeId: n.id, start: { x: n.x, y: n.y } }));
}
/** Groups and comments inside a group: the outer one takes them, and with them their own nodes. */
function nestedMembers(graph: Graph, selfId: string, box: AnnotationBox): BoxSeed[] {
const inside: BoxSeed[] = [];
for (const kind of ANNOTATION_KINDS) {
const list = annotationsOf(graph, kind).filter((a) => a.id !== selfId);
const ids = new Set(
boxesCentredIn(
list.map((a) => ({ id: a.id, ...boxOf(a) })),
box,
),
);
for (const a of list) if (ids.has(a.id)) inside.push({ id: a.id, kind, start: boxOf(a) });
}
return inside;
}
/**
* Puts every traveller where the gesture has it now, for the preview. `offsetFor` answers per start
* position, because with snapping on each thing lands on the grid for itself, as a node does.
*/
export function placeTravellers(graph: Graph, along: Travellers, offsetFor: (start: Point) => Point): void {
for (const member of along.nodes) {
const node = graph.nodes.find((n) => n.id === member.nodeId);
if (!node) continue;
const offset = offsetFor(member.start);
node.x = member.start.x + offset.x;
node.y = member.start.y + offset.y;
}
for (const member of along.boxes) {
const box = findAnnotation(graph, member.id)?.annotation;
if (!box) continue;
const offset = offsetFor(member.start);
box.x = member.start.x + offset.x;
box.y = member.start.y + offset.y;
}
}
/**
* The commands that take everything from where the drag found it to where the preview left it.
* The preview has already moved the model; each thing is put back first so the command records the
* start, and the caller runs them as one step. `label` names a box command that is not a plain move.
*/
export function recordTravellers(
graph: Graph,
assetId: string,
along: Travellers,
label?: string,
): Command[] {
const commands: Command[] = [];
for (const member of along.boxes) {
const box = findAnnotation(graph, member.id)?.annotation;
if (!box) continue;
const next = boxOf(box);
Object.assign(box, member.start);
commands.push(new SetAnnotationBoxCommand(assetId, member.kind, member.id, next, label));
}
for (const member of along.nodes) {
const node = graph.nodes.find((n) => n.id === member.nodeId);
if (!node) continue;
const next = { x: node.x, y: node.y };
node.x = member.start.x;
node.y = member.start.y;
commands.push(new MoveNodeCommand(assetId, member.nodeId, next.x, next.y));
}
return commands;
}