refactor(editor): extract canvas geometry to src/geometry.js (R2, step 5) - #149
Conversation
…p 5) The time<->x and string<->y mappings every draw and hit-test path goes through move out of src/main.js (20,799 -> 20,764): timeToX/xToTime, laneToY/yToLane, strToY/yToStr, the lane metrics they read (WAVEFORM_H, LANE_H, BEAT_H, LABEL_W, ANCHOR_LANE_H, HS_LANE_H) and the pure scroll-bound arithmetic. Reads S and the lane model; no DOM. Graph stays acyclic: geometry -> lanes -> state. NO RENAMES, unlike step 4's LC container. The three lane metrics are reassigned on every resize, but their only writer — the lane-sizing arithmetic inside resizeCanvas() — moves with them, as setLaneMetrics(canvasHeightPx). ES import bindings are LIVE and read-only, so main.js's ~100 read sites keep reading the current value verbatim and none of them can write it. LC needed a container only because its writers (draw(), onMouseMove()) had to stay in main.js. main.js's whole diff is the deletions, the import block, and replacing three assignments in resizeCanvas with one setLaneMetrics(h) call. Tests: scroll_bounds — the last @pure-block slicer among the coordinate helpers — imports the real module. New tests/geometry.test.mjs pins the mappings (inversion, out-of-range clamping, gutter offset, lane height dividing by the CURRENT string count) and the live-binding contract: setLaneMetrics updates what importers see, an importer assigning throws TypeError, and laneToY tracks a resize instead of capturing boot-time metrics. Verified: node --test 84/84, pytest 248/248. Served from local uvicorn on core@main (R0): src/geometry.js 200 as text/javascript. Four headless Chromium harnesses green — draw path, state round-trip, hit test, and a NEW resize harness written for this change: shrinking the viewport 663px -> 283px moves the fret-label baseline -276px (so draw() sees the new metrics), a click at the new coordinates still selects the note (so hit-testing agrees with draw), and no phantom hit remains at the stale position. A read site that captured a metric at module-eval time would fail the first of those. Codex preflight: clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughCanvas geometry logic (lane metrics, time/coordinate mapping, scroll-bound calculations) is extracted from src/main.js into a new src/geometry.js ES module with live mutable bindings. src/main.js is updated to import these instead of defining them locally. Tests are added/updated to validate the module, and CHANGELOG.md documents the migration. ChangesCanvas geometry module extraction
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant MainJS as main.js
participant Geometry as geometry.js
participant State as S (state)
MainJS->>Geometry: setLaneMetrics(canvasHeightPx)
Geometry->>State: read lane count
Geometry->>Geometry: recompute WAVEFORM_H, LANE_H, BEAT_H
MainJS->>Geometry: timeToX(t) / laneToY(l)
Geometry-->>MainJS: mapped coordinate
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Comment |
There was a problem hiding this comment.
Pull request overview
This PR continues the editor’s ES-module split (R2, step 5) by extracting the shared canvas geometry layer (time/x mapping, lane/y mapping, lane sizing metrics, and scroll-bound math) into a dedicated src/geometry.js module, keeping the dependency graph clean (geometry → lanes → state) and avoiding DOM coupling.
Changes:
- Added
src/geometry.jsto centralize coordinate mapping helpers, lane metrics, and scroll-bound arithmetic. - Updated
src/main.jsto import geometry helpers/metrics and to delegate resize lane sizing tosetLaneMetrics(h). - Updated and added tests to import the real module (
tests/scroll_bounds.test.mjs) and to pin geometry behavior + the live-binding contract (tests/geometry.test.mjs).
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tests/scroll_bounds.test.mjs | Switches from source-slicing the @pure block to importing scroll-bound helpers from src/geometry.js. |
| tests/geometry.test.mjs | Adds coverage for geometry mappings, clamping behavior, scroll tail export, and live export let metric updates. |
| src/main.js | Removes inline geometry/metric definitions; imports from geometry.js and uses setLaneMetrics() in resizeCanvas(). |
| src/geometry.js | New module containing time/x, string/lane/y mappings, lane metrics + writer (setLaneMetrics), and scroll-bound pure helpers. |
| CHANGELOG.md | Documents the ES-module migration step and the rationale for live exported metric bindings. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Step 5 of the editor's ES-module split (R2). The tier every draw and hit-test path sits on.
What moved
src/geometry.js(82 lines) —main.js20,799 → 20,764.timeToX,xToTimelaneToY,yToLane,strToY,yToStrWAVEFORM_H,LANE_H,BEAT_H,LABEL_W,ANCHOR_LANE_H,HS_LANE_H_editorViewportDurationPure,_editorMaxScrollXPure,_editorClampScrollXPure,EDITOR_SCROLL_TAIL_SECONDSReads
Sand the lane model, no DOM. Graph stays acyclic:geometry → lanes → state.No renames this time — and the reason is worth stating
Step 4 needed the
LCcontainer because the lane cache is reassigned and ES import bindings are read-only. These three lane metrics are reassigned too, on every resize. But their only writer — the lane-sizing arithmetic insideresizeCanvas()— could move with them:ES import bindings are live. So
main.js's ~100 read sites keep reading the current value with no rename at all, and none of them can write it — an assignment from an importer throwsTypeError. That's strictly better than a container: same ergonomics, and the read-only guarantee is enforced by the language instead of by convention.LCneeded a container only because its writers (draw(),onMouseMove()) have to stay inmain.js. The distinction is "can the writer move?", not "is it reassigned?".main.js's entire diff is the deletions, the import block, and three assignments inresizeCanvascollapsing to onesetLaneMetrics(h)call.Tests
scroll_bounds— the last@pure-block slicer among the coordinate helpers — now imports the real module.tests/geometry.test.mjspins the mappings (round-trip inversion while scrolled, out-of-rangeyToStrclamping instead of a phantom string, gutter offset, lane height dividing by the current string count so a 4-string bass gets taller lanes) and the live-binding contract itself:setLaneMetricsupdates what importers see, an importer assigning throwsTypeError, andlaneToYtracks a resize rather than capturing boot-time metrics.Verification
node --test84/84,pytest248/248.Grepped for any surviving assignment to the three metrics anywhere in
main.js— there is none; one would now throw at runtime. (The three assignments ingeometry.jsare all insidesetLaneMetrics.)No unused import, no shadowing. Served from local uvicorn on core@main (R0):
src/geometry.js200 astext/javascript.Four headless Chromium harnesses, all green — draw path, state round-trip, hit test, plus a new resize harness written for this change:
draw()sees the new metricsdrawA read site that captured a metric at module-eval time would leave the baseline delta at 0 and fail the second row. Both draw and hit-test read the same bindings, so the test is that they agree at the new size and that the geometry actually moved.
Codex preflight: NO ISSUES.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests