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>
55 lines
1.8 KiB
TypeScript
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();
|
|
}
|
|
}
|
|
}
|