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); }); });