Files
feuerwerk/src/editor/graph/annotationLayer.ts
T
adminandClaude Opus 5.5 b830f9c747 Split wiring, drags and the frame layer out of GraphCanvas (#215)
GraphCanvas had grown to 1406 lines. Wiring, dragging and the frame
views each carry their own state, so they now live in their own
classes behind a narrow host interface. Behaviour is unchanged.

- GraphCanvasHost is what the new classes see of the canvas.
- Wiring owns dragging a pin, the palette on a drop into empty space
  and "Make transition".
- GraphDrags owns moving nodes and frames, including snapping.
- AnnotationLayer holds the frame views. The test hooks stay on the
  canvas and read the views through the layer.
- BeadClock moves into beads.ts and can now be tested without a
  browser.
- The canvas builds host, wiring and drags in its constructor because
  state is a parameter property and not set during field init.
- GraphCanvas is down to 1019 lines.

Tests:
- ./check.sh: 145 files, 2192 tests, CHECK EXIT 0 (+2 for BeadClock)
- e2e 1-28,31,36: ALL PASSED, 932 checks
- New e2e check in 36k: a frame caught by the rubber band is also
  drawn as selected. No check covered that before. Against a bent
  build it fails with selected: false.
- Counter-checks for drop target, snapping and beads fail exactly
  the expected checks.
- Editor check sum is now 1314 (CI measured 1313 at 487b3aa).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 10:20:05 +02:00

55 lines
1.8 KiB
TypeScript

import { Container, type FederatedPointerEvent } from 'pixi.js';
import { ANNOTATION_KINDS, annotationsOf, type Graph } from '../../core/graph';
import { AnnotationView } from './AnnotationView';
/** The groups and comments of a graph, behind its edges: a group is a backdrop, and a comment must never cover a node. */
export class AnnotationLayer extends Container {
private readonly byId = new Map<string, AnnotationView>();
constructor(private readonly onPress: (view: AnnotationView, e: FederatedPointerEvent) => void) {
super();
}
get views(): ReadonlyMap<string, AnnotationView> {
return this.byId;
}
clearViews(): void {
for (const view of this.byId.values()) view.destroy({ children: true });
this.byId.clear();
}
/** Views are reused by id, and a box that is gone takes its view with it. */
sync(graph: Graph): void {
const seen = new Set<string>();
for (const kind of ANNOTATION_KINDS) {
for (const annotation of annotationsOf(graph, kind)) {
seen.add(annotation.id);
let view = this.byId.get(annotation.id);
if (!view || view.kind !== kind) {
view?.destroy({ children: true });
view = new AnnotationView(annotation, kind);
const pressed = view;
view.on('pointerdown', (e: FederatedPointerEvent) => this.onPress(pressed, e));
this.byId.set(annotation.id, view);
this.addChild(view);
}
view.sync(annotation);
}
}
for (const [id, view] of this.byId) {
if (!seen.has(id)) {
view.destroy({ children: true });
this.byId.delete(id);
}
}
}
redrawViews(selected: ReadonlySet<string>): void {
for (const view of this.byId.values()) {
view.selected = selected.has(view.annotation.id);
view.redraw();
}
}
}