feat: sorting algorithms lab — 7 algorithms × 4 datasets racing on one clock - #18
Merged
Merged
Conversation
Adds Engine::rule_action so the upcoming sorting lab can mutate live rule state (toggle a sub-simulation) without hard-resetting playback, which update_rule_config does. Default apply_action is Ok(false); a malformed action surfaces as ErasedError::Action / a JS error string. No sorting code yet — this is the engine-generic hook only.
Batched WebGL2 renderer for filled rects (sorting-lab bars, up to ~8,400 per frame in one instanced draw call). Modeled on InstancedRings: static unit quad at attrib 0, per-instance min/max/color at attribs 1-3 with divisor 1, TRIANGLE_STRIP draw_arrays_instanced, Drop deletes VAO + both buffers. Packing factored into a pure `pack` fn with native unit tests.
…pi rule-action hooks Adds `playback`/`zoom` boolean props (both default true, so the two existing labs are unaffected) and an `overlay` Snippet<[LabApi]> rendered over the canvas below the info toggle/backdrop — the seams the upcoming sorting lab needs to swap in its own toolbar and a 7x4 grid of DOM cells. LabApi gains patchVizConfig/ruleAction/readSummary wrapping the engine's update_viz_config/rule_action/rule_summary. fakeViz's FakeEngine gains matching spies (ruleActionSpy, updateVizConfigSpy) and a sortingSummaryFixture (28 idle lanes, row-major) returned by rule_summary() for lab === 'sorting'.
One lane per (algorithm, dataset) cell, row-major. init records each lane's op trace once; advance_to applies one op per clock tick to every running lane, so all 28 cells race on the engine's single clock. Lanes are started, paused, restarted and reset through Rule::apply_action (toggle / set_running / reset_all), which never touches playback. Each column shares one input array so every algorithm in it sorts the identical data; the column index is mixed into the seed so two columns of the same dataset still differ.
`#/sorting` parses to the new lab id (with its query intact, e.g. `?n=80`); the nav entry and the lab page follow.
SortingLab.svelte overlays a CSS grid of row/column headers and 28 cell
buttons on the canvas, pushing device-pixel cell rects to the viz via
patchVizConfig and driving per-lane/row/column/global rule actions
(toggle, set_running, reset_all). Speed and array size are controlled
from a bespoke footer toolbar; size is shareable via ?n=.
Pure lane/grouping arithmetic, cell-rect geometry, and rule-summary
parsing live in src/lib/sorting/{lanes,layout,summary}.ts so the
component itself stays under the ~350-line guideline.
Wires the route into App.svelte (nav link + branch), already backed by
the 'sorting' LabId committed in cc68f5e.
…" lab One InstancedQuads draw per frame fills the cell rectangles the lab measures in the DOM and pushes through `update_viz_config` as `cells: [[x, y, w, h], …]` in device pixels, so Rust does no layout. - Pixel-space y-down projection, matching the DOM's coordinate frame. - Bar heights scale to each lane's own max; compare/write highlights come from `Lane::last_op()`, and a finished lane turns uniformly done-colored. - Registry: `LAB_IDS` gains "sorting", pairing SortingRace with SortingViz. - Browser tests for construction, per-lane `rule_action`, its error cases, the Ok(false) default on other labs, and a 28-rect cells config.
…rd a lab frame() previously fed an unbounded now_ms - last_frame_ms gap straight into dt; a backgrounded/throttled tab could resume with a multi-minute gap and fast-forward any rule by that much in one tick. Clamp to [0, 0.25]s for the whole engine.
…alting - SortingRace::init now clamps cfg.size to [10, 300] defensively (the schema already enforces this, but a hand-crafted config could bypass it) instead of trusting the raw value. - Dataset generators mixed a small loop index with a fixed salt via plain XOR, which lets a low-bit index and a low-bit salt cancel out. Multiply the index by splitmix64's own golden-ratio constant first so consecutive indices land on well-spread inputs. - Pin known-answer vectors for splitmix64(0) and splitmix64(1) so a change to the mixing constants is caught directly, not just via downstream behavior. - few_unique_has_at_most_five_distinct_values now also asserts the dataset is genuinely repeated and non-trivial (2 <= distinct < n) once n >= 10.
fourier_epicycles's apply_path test used assert_eq!(cfg.path[2].pen, false) (clippy's bool_assert_comparison territory) — assert!(!...) instead. Rule::summary's doc claimed it's "never" read per frame, which the sorting lab's live lane grid already contradicts; reword to allow it while still asking implementers to keep it cheap.
…l guard - Grid: switch the inline grid-template to minmax(0, 1fr) tracks and a --rowhdr custom property so the 4th dataset column no longer clips at 375px; give .hdr min-width: 0 so its ellipsis can actually kick in, and shrink the row header column + row-header font under the 768px query. - onSize: re-write the clamped value back into the input and skip the rule-config push (and the playback reset it causes) when a repeated out-of-range entry clamps to the value already in effect. - paramsOf now returns the raw, unclamped ?n= so onReady can tell an out-of-range link apart from an already-canonical one and rewrite the URL to the value that actually loaded. - runAll always sends running: true — it no longer toggles the whole race to paused just because every lane happened to already be running. - pushCells: skip a frame where the bound cell-button array is short or has a hole (a resize callback firing mid-render), instead of measuring bogus rects. - fakeViz: drop the duplicated SortingLane/SortingSummary interfaces in favor of importing LaneSummary/SortingSummary from lib/sorting/summary, so the test fixture can't drift from the real shape. - summary.ts: note algorithms.rs::Algorithm::label / datasets.rs::Dataset::label as the source of truth this list mirrors.
Rust native (177), WASM browser (24), and vitest (105 across 13 files) counts, plus a fresh coverage run, were all stale from before the sorting lab landed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A third lab,
#/sorting: a 7×4 matrix of bar-chart panels (Bubble, Insertion, Selection, Shell, Merge, Quick, Heap × Random, Nearly sorted, Reversed, Few unique). Every panel is an independent lane on one engine clock — each tick, every running lane applies one recorded op (compare / swap / write) — so O(n²) vs O(n log n) is visible as a race. Click a cell to run / pause / restart, ▶ on row and column headers, toolbar with Run all / Pause all / Reset / New data / Speed / Size, share links via?n=<size>.How it's built
Rule::apply_action+Engine::rule_action(generic hook to mutate live rule state without resetting playback);rules/sorting/{algorithms,datasets}record op traces by running the real algorithms through aRecorder(tests prove replaying the trace reaches the sorted array for every algorithm × dataset × size);SortingRacerule (lanes, per-lane cursor/running, actions, per-frame summary);InstancedQuadsrenderer;SortingVizdraws bars into DOM-supplied cell rects (device px) — Rust does no layout.dtis now clamped to 0.25 s so a hidden tab can't fast-forward a lab by minutes.LabShellgainsplayback/zoom/overlayprops;LabApi.patchVizConfig/ruleAction/readSummary;SortingLab.svelteoverlays a CSS grid on the canvas, measures cells with aResizeObserver, and reads the rule summary every frame for the badges.Tests
Rust native 116 → 177, browser (wasm) 18 → 24, vitest 55 → 105. Verified live on the dev server (desktop + 375 px mobile), including lab switching with no console errors.