Three stores, three importers and three pickers that differ only in their type are one of each (#85)
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:
1 parent
15bdfdf07a
commit
cc6db5f350
42 files changed
+1375
-1650
No files matched your search
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user