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>
88 lines
3.1 KiB
TypeScript
88 lines
3.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
BEAD_SPACING,
|
|
BEAD_TRAVEL,
|
|
BeadClock,
|
|
beadsInFlight,
|
|
curvePoint,
|
|
launchBead,
|
|
} from '../../src/editor/graph/beads';
|
|
|
|
describe('beads on a firing edge', () => {
|
|
it('a single fire sends one bead from the source to the target and then it is gone', () => {
|
|
const times: number[] = [];
|
|
launchBead(times, 2);
|
|
expect(beadsInFlight(times, 2)).toEqual([0]);
|
|
expect(beadsInFlight(times, 2 + BEAD_TRAVEL / 2)[0]).toBeCloseTo(0.5);
|
|
expect(beadsInFlight(times, 2 + BEAD_TRAVEL)).toHaveLength(1);
|
|
expect(beadsInFlight(times, 2 + BEAD_TRAVEL + 0.01)).toEqual([]);
|
|
});
|
|
|
|
it('a fire closer than the spacing to the last bead sends none', () => {
|
|
const times: number[] = [];
|
|
launchBead(times, 1);
|
|
launchBead(times, 1 + BEAD_SPACING / 2);
|
|
expect(times).toEqual([1]);
|
|
launchBead(times, 1 + BEAD_SPACING);
|
|
expect(times).toEqual([1, 1 + BEAD_SPACING]);
|
|
});
|
|
|
|
it('an edge firing every frame shows an evenly spaced chain that does not pile up', () => {
|
|
const times: number[] = [];
|
|
let now = 0;
|
|
for (let frame = 0; frame < 600; frame++) {
|
|
now = frame / 60;
|
|
launchBead(times, now);
|
|
}
|
|
const flying = beadsInFlight(times, now);
|
|
expect(flying.length).toBeGreaterThanOrEqual(2);
|
|
expect(times.length).toBeLessThanOrEqual(Math.ceil(BEAD_TRAVEL / BEAD_SPACING) + 1);
|
|
for (let i = 1; i < flying.length; i++)
|
|
expect(flying[i - 1]! - flying[i]!).toBeGreaterThanOrEqual(BEAD_SPACING / BEAD_TRAVEL - 1e-9);
|
|
});
|
|
});
|
|
|
|
describe('the beads of one graph', () => {
|
|
it('launches once per fire, even when the same trace is read on every frame', () => {
|
|
const clock = new BeadClock();
|
|
const fired = new Map([['e1', 1]]);
|
|
clock.launch('g', fired);
|
|
clock.launch('g', fired);
|
|
expect(clock.inFlight('e1', 1)).toEqual([0]);
|
|
expect(clock.inFlight('e2', 1)).toEqual([]);
|
|
});
|
|
|
|
it('drops every bead when the trace goes away or another graph is shown', () => {
|
|
const clock = new BeadClock();
|
|
clock.launch('g', new Map([['e1', 1]]));
|
|
clock.launch('g', null);
|
|
expect(clock.inFlight('e1', 1)).toEqual([]);
|
|
clock.launch('g', new Map([['e1', 2]]));
|
|
clock.launch('other', new Map());
|
|
expect(clock.inFlight('e1', 2)).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('the curve a bead follows', () => {
|
|
it('starts in the source port and ends in the target port', () => {
|
|
expect(curvePoint(10, 20, 300, 80, 0)).toEqual({ x: 10, y: 20 });
|
|
expect(curvePoint(10, 20, 300, 80, 1)).toEqual({ x: 300, y: 80 });
|
|
});
|
|
|
|
it('passes the middle of a curve half way, since the curve is symmetric', () => {
|
|
const mid = curvePoint(0, 0, 200, 100, 0.5);
|
|
expect(mid.x).toBeCloseTo(100);
|
|
expect(mid.y).toBeCloseTo(50);
|
|
});
|
|
|
|
it('bows out to the right between two ports stacked above each other, as the drawn curve does', () => {
|
|
expect(curvePoint(0, 0, 0, 100, 0.25).x).toBeCloseTo(11.25);
|
|
});
|
|
|
|
it('leaves its port horizontally, as the drawn curve does', () => {
|
|
const near = curvePoint(0, 0, 200, 100, 0.01);
|
|
expect(near.x).toBeGreaterThan(0);
|
|
expect(Math.abs(near.y / near.x)).toBeLessThan(0.05);
|
|
});
|
|
});
|