Problem
AxisLabels.ts exposes two functions — getXLabelX and getYabelY (note the misspelling, propagated to the call site) — that look pure but carry mutable module-global state:
let cameraBaselineX: number | undefined; // AxisLabels.ts:17-18
let cameraBaselineY: number | undefined;
These are lazily captured on the first call (revertCameraBaseline) and never reset. Consequences:
- Output depends on invocation order and prior calls — the least testable code in the 2D layer.
- Split view is broken by design: two canvases on one page (
Canvas2D split mode) share the same module globals, so the first canvas to render sets the baseline for both.
On top of that, the functions reverse-engineer the world→screen transform that CanvasD3 already applied. The base-scale constant 15 (half of GRID_SIZE_2D) is typed literally in four places that must stay in agreement:
ViewBox.ts:40 — zoom (15 / boxWidth)
CanvasD3.svelte:196 — the forward SVG transform (scale(2*width/30, ...))
CanvasD3.svelte:103 — the inverse in transformScene (15 / (width / x))
AxisLabels.ts:65,88,89 — re-derivation of world coordinates from the D3 transform
Any change to the base scale requires edits in all four.
Solution
- Replace the module-global baseline with instance state, so split canvases stop sharing it.
- Have label placement ask the Projection2D module (see related issue) for
toWorld/toScreen instead of re-deriving the transform, so the constant 15 has a single owner.
Benefits
- Locality: the world↔screen transform has one owner; the constant stops being copied four ways.
- Split canvases no longer share baseline state — fixes a live correctness defect.
- Label math becomes order-independent pure functions, testable in isolation.
Affected files
AxisLabels.ts:17-18,42,73, CanvasD3.svelte:103,196,179-182, ViewBox.ts:40.
Related
Depends conceptually on the Projection2D seam (#491). The module-global mutable state is a real bug in split view and is worth fixing regardless of whether Projection2D lands first.
Problem
AxisLabels.tsexposes two functions —getXLabelXandgetYabelY(note the misspelling, propagated to the call site) — that look pure but carry mutable module-global state:These are lazily captured on the first call (
revertCameraBaseline) and never reset. Consequences:Canvas2Dsplit mode) share the same module globals, so the first canvas to render sets the baseline for both.On top of that, the functions reverse-engineer the world→screen transform that
CanvasD3already applied. The base-scale constant15(half ofGRID_SIZE_2D) is typed literally in four places that must stay in agreement:ViewBox.ts:40— zoom (15 / boxWidth)CanvasD3.svelte:196— the forward SVG transform (scale(2*width/30, ...))CanvasD3.svelte:103— the inverse intransformScene(15 / (width / x))AxisLabels.ts:65,88,89— re-derivation of world coordinates from the D3 transformAny change to the base scale requires edits in all four.
Solution
toWorld/toScreeninstead of re-deriving the transform, so the constant15has a single owner.Benefits
Affected files
AxisLabels.ts:17-18,42,73,CanvasD3.svelte:103,196,179-182,ViewBox.ts:40.Related
Depends conceptually on the Projection2D seam (#491). The module-global mutable state is a real bug in split view and is worth fixing regardless of whether Projection2D lands first.