mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-26 14:52:13 +00:00
* Initial commit — clean relaunch * Initial commit — clean relaunch * Remove external game/format terminology from docs and code Reword references to the external game and its proprietary file formats in comments, docstrings, UI text and identifiers; no behaviour change. * Repoint dead slopsmith URLs -> got-feedback Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(midi): consume core midi-input domain + session robustness fixes (#2) * feat(midi): consume the core midi-input domain instead of private requestMIDIAccess (#881) Route Web-MIDI device access through window.slopsmith.midiInput (discover/list/select/open/close) rather than a private navigator.requestMIDIAccess(). One shared device-access boundary with piano/drums/onboarding; retires this plugin's private Web-MIDI. The note-detection 'midi' exact-verdict provider role and the audio-input source export are preserved (now backed by midi-input source data). Saved pick ({id,name}) stays compatible — domain sourceId == the old MIDIInput.id. Live listener is the domain handle's addListener/removeListener; the async init/resume/pause gate and device-vanish handling are kept. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): stop exporting MIDI into the audio-input domain Keys MIDI now lives in the dedicated midi-input domain (#881). Continuing to register pseudonymized 'midi-input-N' sources into audio-input polluted audio device pickers — notably the onboarding guitar audio-input dropdown showed MIDI devices under cryptic names. Unregister any leftovers and register none. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): guard async connect races, denied-discovery latch, key consistency Codex preflight findings on the midi-input consumption: - Don't latch `_midiReady` when discover() resolves denied/unavailable — only on a handled outcome — so reopening retries the permission prompt. - Add a generation guard to async `_midiConnect`: a slower open() from an earlier selection can resolve after a device/None switch and install a stale handle/listener. Discard (and close) superseded opens. - Carry the domain `logicalSourceKey` on the source/selection descriptor and use it for select/open/close instead of synthesizing `web-midi::<id>`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): invalidate pending opens on detach; clear phantom selection; manifest Codex re-review (round 2): - A detach driven by device removal (sources-changed) didn't advance _midiConnectSeq, so a pending _midiConnect open could resume and install a handle/listener for a now-gone source. Bump the generation inside _midiDetach and capture myGen after it, so any later detach supersedes an in-flight open. - If mi.open() yields no handle, _midiInput stayed set — a phantom connected device the render loop's miss-sweeping would penalize. Clear it on the no-handle and catch paths. - plugin.json: replace the stale audio-input provider role (the removed MIDI-into-audio-input export bridge) with the midi-input requester it now uses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): make _midiResume idempotent under multiple live instances Codex re-review (round 3): with the domain addListener API, a second live renderer instance (splitscreen/overlapping lifetimes) calling _midiResume() while already active could register the same listener again and double-deliver a MIDI note to the focused instance (hit + duplicate misses). Return early when already active rather than relying on the provider's Set-backed de-dup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): release domain session on teardown; counter-free legacy source cleanup Codex re-review (round 4): - [P2] Last-instance teardown only detached the listener and never released the midi-input domain session, leaking the requester ref. Add _midiReleaseSession() (delegates to _midiDetach: close + null + generation bump) at the destroy site; re-mount's _midiInit auto-connects from the saved pick. - [P3] The legacy audio-input MIDI-source cleanup looped over the module-local _aiRegisteredCount, which resets to 0 on an in-page upgrade so the prior build's 'keys-midi:input-N' entries were never unregistered. Iterate a fixed bound over the known sourceId pattern instead (unregister of an absent source is a no-op). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): gate miss-sweep on live handle; don't open without a live renderer Codex re-review (round 5): - Miss-sweeping was gated on _midiInput, set as soon as a device is picked, but the async mi.open() may still be pending (slow / permission prompt) — notes passing during that window banked false misses. Gate on _midiHandle, truthy only after a handle is opened and wired. - A settings-only ensure-init (or a discover resolving after the last instance was torn down) could open a midi-input session with no renderer to release it. Gate mi.open() on _instances.size > 0; the pick is saved and a later mount re-runs auto-connect. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): guard open-failure clobber + don't clobber saved device on unplug Bringing keys-highway-3d in line with the piano/drums hardening: - The _midiConnect catch cleared _midiInput unconditionally; a stale older open's rejection could wipe a newer connect's _midiInput/_midiHandle (and leak the handle). Only clear when myGen === _midiConnectSeq. - The sources-changed recovery called _midiAutoConnect(), whose fallback persists a substitute device — overwriting the user's saved pick on a transient multi-device unplug. Parameterize _midiAutoConnect(allowFallback); recovery passes false (reconnect the saved device only, never a persisted fallback). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): keep first-hotplug working; don't tear down a live session on re-init Codex round-11: - The unplug-recovery _midiAutoConnect(false) skipped ALL fallbacks, which broke first-hotplug (open visualizer with no keyboard, plug one in → never picked → no connect). Skip the substitute only when a saved pick exists but is absent (preserve it); allow the fallback when nothing was ever picked. - _midiInit re-ran _midiAutoConnect on every ready re-init (settings/splitscreen), tearing down the live handle + releasing held keys. Only re-connect when there's no live handle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(midi): consume core midi-input domain + session robustness fixes (#2) * feat(midi): consume the core midi-input domain instead of private requestMIDIAccess (#881) Route Web-MIDI device access through window.slopsmith.midiInput (discover/list/select/open/close) rather than a private navigator.requestMIDIAccess(). One shared device-access boundary with piano/drums/keys/onboarding. Saved pick ({id,name}, with legacy id-only fallback) stays compatible — domain sourceId == the old MIDIInput.id. The async in-flight init guard, _midiActive teardown gate, device-vanish handling, and settings device APIs are preserved; the live listener is the domain handle's addListener/removeListener. Degrades to no-MIDI when the domain is absent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): guard async connect races, denied-discovery latch, key consistency Codex preflight findings on the midi-input consumption: - Don't latch `_midiReady` when discover() resolves denied/unavailable — only on a handled outcome — so reopening retries the permission prompt. - Add a generation guard to async `_midiConnect`: a slower open() from an earlier selection can resolve after a device/None switch and install a stale handle/listener. Discard (and close) superseded opens. - Carry the domain `logicalSourceKey` on the source/selection descriptor and use it for select/open/close instead of synthesizing `web-midi::<id>`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): invalidate pending opens on detach; clear phantom selection; manifest Codex re-review (round 2): - A detach driven by device removal (sources-changed) didn't advance _midiConnectSeq, so a pending _midiConnect open could resume and install a handle/listener for a now-gone source. Bump the generation inside _midiDetach and capture myGen after it, so any later detach supersedes an in-flight open. - If mi.open() yields no handle, _midiInput stayed set — a phantom connected device the render loop's miss-sweeping would penalize. Clear it on the no-handle and catch paths. - Declare the midi-input requester capability (degrade-noop) in plugin.json. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): make _midiResume idempotent under multiple live instances Proactively mirror the keys-highway-3d round-3 fix (same multi-instance lifecycle and identical code): a second live renderer instance calling _midiResume() while already active could double-register the listener and double-deliver a MIDI hit. Return early when already active. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): release the domain session on final teardown Mirror the keys-highway-3d round-4 fix: last-instance teardown only detached the listener and never released the midi-input domain session, leaking the requester ref. Add _midiReleaseSession() (delegates to _midiDetach: close + null + generation bump) at the destroy site; re-mount's _midiInit auto-connects. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): gate miss-sweep on live handle; don't open without a live renderer Codex re-review (round 5): - [P2] drumH3dEnsureMidiInit (settings-only, no renderer) auto-connected and opened a midi-input session that destroy()/_midiReleaseSession would never run to release — held until reload. Gate mi.open() on _instances.size > 0; the pick is saved and a later renderer mount re-runs auto-connect. - Mirror the keys-highway-3d miss-sweep fix: gate accumulation on _midiHandle (live wired session), not _midiInput (set before the async open resolves), so notes during a pending open don't bank false misses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): guard open-failure clobber + reconnect saved device on replug Bringing drum-highway-3d in line with the piano/drums hardening: - The _midiConnect catch cleared _midiInput unconditionally; a stale older open's rejection could wipe a newer connect's _midiInput/_midiHandle. Only clear when myGen === _midiConnectSeq. - The sources-changed handler only refreshed the list (never reconnected), so a replug of the saved device didn't reattach. Reconnect on sources-changed via _midiAutoConnect(false) — saved device only, no fallback, so a transient unplug can't switch to / persist another input. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): keep first-hotplug working; don't tear down a live session on re-init Same two round-11 fixes as keys-highway-3d (shared structure): the recovery _midiAutoConnect(false) now allows a fallback when nothing was ever picked (first-hotplug) while still preserving a saved-but-absent pick; and _midiInit only re-connects when there's no live handle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): select by logicalSourceKey, not provider-local sourceId (#4) Codex review of the midi-input migration: resolving a selection with find(s => s.id === id) treats the provider-local sourceId as globally unique, so when the core midi-input domain aggregates multiple providers two devices sharing a sourceId can't be distinguished — the wrong device opens and unplug detection can miss that the selected key vanished. Thread the globally-unique logicalSourceKey through save/read, _midiConnect, auto-connect reconnect, the public keysH3dSetMidiInput entry, and the capability _aiOpen path, preferring the key and keeping the bare sourceId / name only as a legacy fallback. Mirrors the already-merged drums/piano migration (_midiResolveSaved). Self-reviewed against that reference. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(midi): select by logicalSourceKey, not provider-local sourceId (#3) Codex review of the midi-input migration (same issue as keys-highway-3d): resolving a selection with find(s => s.id === id) treats the provider-local sourceId as globally unique, so when the core midi-input domain aggregates multiple providers two devices sharing a sourceId can't be distinguished — the wrong device opens and unplug detection can miss the vanished key. Thread the globally-unique logicalSourceKey through save/read, _midiConnect, the auto-connect reconnect, and the public drumH3dSetMidiInput entry, preferring the key with the bare sourceId / name only as a legacy fallback. Mirrors the already-merged drums/piano migration. Self-reviewed. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: add plugin metadata (category, description, thumbnail) (#5) * chore: add plugin metadata (category/description/thumbnail) Refs got-feedback/feedBack#571 * chore: add placeholder thumbnail Refs got-feedback/feedBack#571 * chore: add plugin metadata (category, description, thumbnail) (#4) * chore: add plugin metadata (category/description/thumbnail) Refs got-feedback/feedBack#571 * chore: add placeholder thumbnail Refs got-feedback/feedBack#571 * fix(viz): register feedBackViz_<id> so the player viz picker lists this plugin (#5) After the slopsmith->feedBack rename the factory was still registered as window.slopsmithViz_<id>; the host viz picker only looks up window.feedBackViz_<id> (no slopsmithViz alias exists) and silently skipped this plugin. Add the feedBackViz_ global (aliased to the existing slopsmithViz_ one for back-compat). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(viz): register feedBackViz_<id> so the player viz picker lists this plugin (#6) After the slopsmith->feedBack rename the factory was still registered as window.slopsmithViz_<id>; the host viz picker only looks up window.feedBackViz_<id> (no slopsmithViz alias exists) and silently skipped this plugin. Add the feedBackViz_ global (aliased to the existing slopsmithViz_ one for back-compat). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(plugins): bundle drum_highway_3d + keys_highway_3d as in-tree core plugins Import both 3D highway plugins from their standalone repos via git subtree (history preserved), ahead of the visual-parity epic that ports the guitar highway's polish to them. - .gitignore: !plugins/drum_highway_3d/ + !plugins/keys_highway_3d/ exceptions - keys plugin.json: "bundled": true (drum already had it) - CI: JS test step gains 'plugins/*/tests/*.test.js' (+20 keys tests) - static/tailwind.min.css regenerated (core build scans plugins/**) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(drum_highway_3d): narrow Auto-mode predicate so bundling can't steal guitar arrangements has_drum_tab is pack-level; first-match-wins Auto order sorts this plugin before highway_3d. Claim only drum arrangements, or packs nothing more specific can render (Codex preflight P2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: CHANGELOG — note the deliberate Auto-predicate narrowing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(drum_highway_3d): MIDI lifecycle — no false misses on mid-song connect, promote survivor on destroy - _missSweepFloor exempts notes that passed before the device wired up from the miss sweep (lowered on seek-back; cleared with scoring resets) - destroy() promotes a surviving instance to _activeInstance so splitscreen panel teardown doesn't drop all MIDI routing (Codex preflight round 2, both pre-existing in the imported code) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Sin <deathlysin@outlook.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
191 lines
7.5 KiB
JavaScript
191 lines
7.5 KiB
JavaScript
// Pure data-layer tests: load screen.js in a bare vm window and exercise the
|
|
// __test exports (no DOM, no WebGL, no network).
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const vm = require('node:vm');
|
|
|
|
function load() {
|
|
const window = {
|
|
console,
|
|
location: { protocol: 'http:', host: 'localhost' },
|
|
slopsmith: {},
|
|
};
|
|
window.window = window;
|
|
window.globalThis = window;
|
|
const context = vm.createContext(window);
|
|
const src = fs.readFileSync(path.join(__dirname, '..', 'screen.js'), 'utf8');
|
|
vm.runInContext(src, context, { filename: 'screen.js' });
|
|
return window.slopsmithViz_keys_highway_3d.__test;
|
|
}
|
|
|
|
test('beatDurSec: base, dotted, double-dotted, tuplet', () => {
|
|
const { beatDurSec } = load();
|
|
// 120 BPM → quarter = 0.5s
|
|
assert.equal(beatDurSec({ dur: 4 }, 120), 0.5);
|
|
assert.equal(beatDurSec({ dur: 2 }, 120), 1.0);
|
|
assert.equal(beatDurSec({ dur: 8 }, 120), 0.25);
|
|
// dotted quarter = 0.75s; double-dotted = 0.875s
|
|
assert.equal(beatDurSec({ dur: 4, dot: 1 }, 120), 0.75);
|
|
assert.equal(beatDurSec({ dur: 4, dot: 2 }, 120), 0.875);
|
|
// triplet eighth: 0.25 * 2/3
|
|
assert.ok(Math.abs(beatDurSec({ dur: 8, tu: [3, 2] }, 120) - 0.25 * 2 / 3) < 1e-9);
|
|
// invalid → null
|
|
assert.equal(beatDurSec({ dur: 0 }, 120), null);
|
|
assert.equal(beatDurSec({ dur: 4 }, null), null);
|
|
});
|
|
|
|
function measure(idx, t, opts = {}) {
|
|
return { idx, t, ...opts };
|
|
}
|
|
|
|
test('flattenNotation: basic two-hand flatten, sorted, durSec from tempo', () => {
|
|
const { flattenNotation } = load();
|
|
const notes = flattenNotation([
|
|
{
|
|
idx: 1, t: 0, tempo: 120,
|
|
staves: {
|
|
rh: { voices: [{ v: 1, beats: [
|
|
{ t: 0.5, dur: 4, notes: [{ midi: 64 }] },
|
|
{ t: 1.0, dur: 8, notes: [{ midi: 67 }] },
|
|
] }] },
|
|
lh: { voices: [{ v: 1, beats: [
|
|
{ t: 0.0, dur: 2, notes: [{ midi: 48 }] },
|
|
] }] },
|
|
},
|
|
},
|
|
]);
|
|
assert.equal(notes.length, 3);
|
|
assert.deepEqual(JSON.parse(JSON.stringify(notes.map(n => n.midi))), [48, 64, 67]); // time-sorted
|
|
assert.equal(notes[0].hand, 'lh');
|
|
assert.equal(notes[0].durSec, 1.0); // half at 120
|
|
assert.equal(notes[1].durSec, 0.5); // quarter
|
|
assert.equal(notes[2].durSec, 0.25); // eighth
|
|
assert.equal(notes[1].measureIdx, 1);
|
|
});
|
|
|
|
test('flattenNotation: tempo state carries across measures and changes apply', () => {
|
|
const { flattenNotation } = load();
|
|
const notes = flattenNotation([
|
|
measure(1, 0, { tempo: 120, staves: { rh: { voices: [{ v: 1, beats: [{ t: 0, dur: 4, notes: [{ midi: 60 }] }] }] } } }),
|
|
measure(2, 2, { staves: { rh: { voices: [{ v: 1, beats: [{ t: 2, dur: 4, notes: [{ midi: 62 }] }] }] } } }),
|
|
measure(3, 4, { tempo: 60, staves: { rh: { voices: [{ v: 1, beats: [{ t: 4, dur: 4, notes: [{ midi: 64 }] }] }] } } }),
|
|
]);
|
|
assert.equal(notes[0].durSec, 0.5); // 120 BPM
|
|
assert.equal(notes[1].durSec, 0.5); // tempo carried
|
|
assert.equal(notes[2].durSec, 1.0); // 60 BPM
|
|
});
|
|
|
|
test('flattenNotation: tied notes extend instead of emitting a new block', () => {
|
|
const { flattenNotation } = load();
|
|
const notes = flattenNotation([
|
|
{
|
|
idx: 1, t: 0, tempo: 120,
|
|
staves: { rh: { voices: [{ v: 1, beats: [
|
|
{ t: 0.0, dur: 2, notes: [{ midi: 60 }] },
|
|
{ t: 1.0, dur: 2, notes: [{ midi: 60, tied: true }] },
|
|
] }] } },
|
|
},
|
|
]);
|
|
assert.equal(notes.length, 1);
|
|
assert.equal(notes[0].durSec, 2.0); // half + tied half
|
|
});
|
|
|
|
test('flattenNotation: no tempo anywhere falls back to next-onset gap', () => {
|
|
const { flattenNotation } = load();
|
|
const notes = flattenNotation([
|
|
{
|
|
idx: 1, t: 0,
|
|
staves: { rh: { voices: [{ v: 1, beats: [
|
|
{ t: 0.0, dur: 4, notes: [{ midi: 60 }] },
|
|
{ t: 0.8, dur: 4, notes: [{ midi: 62 }] },
|
|
] }] } },
|
|
},
|
|
]);
|
|
assert.ok(Math.abs(notes[0].durSec - 0.8) < 1e-9);
|
|
assert.equal(notes[1].durSec, 2.0); // final-beat fallback
|
|
});
|
|
|
|
test('flattenNotation: overlap clamp against next same-hand same-midi onset', () => {
|
|
const { flattenNotation } = load();
|
|
const notes = flattenNotation([
|
|
{
|
|
idx: 1, t: 0, tempo: 30, // whole note = 8s — way past the next onset
|
|
staves: { rh: { voices: [{ v: 1, beats: [
|
|
{ t: 0.0, dur: 1, notes: [{ midi: 60 }] },
|
|
{ t: 1.0, dur: 1, notes: [{ midi: 60 }] },
|
|
] }] } },
|
|
},
|
|
]);
|
|
assert.equal(notes[0].durSec, 1.0); // clamped to next onset
|
|
});
|
|
|
|
test('flattenNotation: rests, malformed beats, and out-of-range midi are skipped', () => {
|
|
const { flattenNotation } = load();
|
|
const notes = flattenNotation([
|
|
{
|
|
idx: 1, t: 0, tempo: 120,
|
|
staves: { rh: { voices: [{ v: 1, beats: [
|
|
{ t: 0.0, dur: 4, rest: true },
|
|
{ t: 0.5, dur: 4, notes: [{ midi: 200 }] },
|
|
null,
|
|
{ t: 1.0, dur: 4, notes: [{ midi: 64 }] },
|
|
] }] } },
|
|
},
|
|
]);
|
|
assert.equal(notes.length, 1);
|
|
assert.equal(notes[0].midi, 64);
|
|
});
|
|
|
|
test('keyRange pads and clamps to the 88-key piano, keeps active span explicit', () => {
|
|
const { keyRange } = load();
|
|
assert.deepEqual(
|
|
JSON.parse(JSON.stringify(keyRange([{ midi: 60 }, { midi: 72 }]))),
|
|
{ low: 58, high: 74, activeLow: 60, activeHigh: 72 },
|
|
);
|
|
// At the clamp edges the active span still reflects the chart extremes
|
|
// (not low+pad — that would mark A0/C8 inactive when actually played).
|
|
assert.deepEqual(
|
|
JSON.parse(JSON.stringify(keyRange([{ midi: 21 }, { midi: 108 }]))),
|
|
{ low: 21, high: 108, activeLow: 21, activeHigh: 108 },
|
|
);
|
|
const empty = keyRange([]);
|
|
assert.ok(empty.low < 60 && empty.high > 60);
|
|
assert.ok(empty.activeLow > empty.activeHigh, 'empty chart has an empty active span');
|
|
});
|
|
|
|
test('noteLetter maps midi to pitch-class letters', () => {
|
|
const { noteLetter } = load();
|
|
assert.equal(noteLetter(60), 'C');
|
|
assert.equal(noteLetter(61), 'C#');
|
|
assert.equal(noteLetter(69), 'A');
|
|
assert.equal(noteLetter(71), 'B');
|
|
assert.equal(noteLetter(72), 'C'); // octave wraps
|
|
assert.equal(noteLetter(21), 'A'); // A0
|
|
});
|
|
|
|
test('scrollZ: events sit at hitZ exactly at their time and approach from -Z', () => {
|
|
const { scrollZ } = load();
|
|
const hitZ = -0.5, speed = 2.0;
|
|
// At now === eventT the event is exactly on the hit-line.
|
|
assert.equal(scrollZ(10, 10, hitZ, speed), hitZ);
|
|
// 1s before its time it is `speed` units further away (towards -Z).
|
|
assert.equal(scrollZ(10, 9, hitZ, speed), hitZ - speed);
|
|
// After its time it has moved past the hit-line (towards +Z).
|
|
assert.equal(scrollZ(10, 11, hitZ, speed), hitZ + speed);
|
|
// Marker and note-front-edge maths agree by construction: a note of
|
|
// length L positioned at scrollZ(t) - L/2 has its front edge at
|
|
// scrollZ(t).
|
|
const len = 0.8;
|
|
assert.equal(scrollZ(10, 10, hitZ, speed) - len / 2 + len / 2, hitZ);
|
|
});
|
|
|
|
test('measureMarkers extracts idx/t pairs', () => {
|
|
const { measureMarkers } = load();
|
|
assert.deepEqual(
|
|
JSON.parse(JSON.stringify(measureMarkers([{ idx: 1, t: 0 }, { idx: 2, t: 2.5 }, { bogus: true }]))),
|
|
[{ idx: 1, t: 0 }, { idx: 2, t: 2.5 }],
|
|
);
|
|
});
|