- animate-move: factored shared promise/Tween body into tweenPivot;
animateMove builds the pivot from spec, snapAndAnimate accepts a
caller-built pivot. 96 → 80 LOC, no behavior change.
- pointer-gesture: merged abortGesture / finishGesture / dispose busy
release into a single idempotent endGesture; ownedBusy already
encoded the per-gesture semantics. 205 → 190 LOC.
- App: dropped the redundant `solving` state; ControlsPanel derives
it as `busy && !solveActive`. runSolveStep collapsed to a one-line
delegate since the busy gate now lives in CubeView.
- CubeView: extracted cubejs lazy-load + parse into computeSolvePlan
and dropped the dead bracket-undo guard.
41 tests still green; build clean. No deps, features, or behavior
changes. Refs: plans/reports/code-simplifier-260427-1848-rubik-pass.md
Critical:
- Solver lazy-load + table-init window now gates `busy=true` so the
user can't mutate cubies during the 4–5 s compute window and have a
stale algorithm played on a new state.
- Pointer gesture tracks busy ownership (`ownedBusy`) so the
busy-during-PROBING bail path doesn't release a flag set by an
unrelated keyboard-driven animation in flight.
High:
- animate-move onComplete wraps work in try/finally so the awaiter is
never left hanging with `busy=true` if a frame throws.
- New disposeCubieMeshes / clearTweens release per-cubie materials,
the singleton geometry, and any in-flight tweens on CubeView
unmount, fixing a GPU + scene-graph leak across HMR / route changes.
- ControlsPanel move log no longer keys with Math.random() — Svelte
was tearing down + rebuilding every span every render.
- solveStep wraps the cubejs await in try/catch so a thrown solver
error is logged instead of leaking an unhandled rejection.
Medium / minor:
- pointer-gesture filters foreign pointerIds in onPointerMove /
onPointerUp (multitouch hardening).
- ControlsPanel buttons disable while `busy` so users see why their
click does nothing during animations.
- gesture-math.chooseRotationAxis falls back to raw screen-delta when
both in-plane axes project to a near-zero screen vector (camera
looking straight down the face normal).
- Dropped dead `[scramble]` undo guard, dead `onSolved` export, and
unused `isAnimating` state.
- Solver init promise resets on rejection so transient failures can
retry instead of being sticky forever.
- Canvas gets an aria-label describing its drag interaction.
Refs review: plans/reports/code-review-260427-1148-rubik-full-pass.md
The Solve button now walks the user through the solution one move at a
time. First click computes the cubejs solution and animates move 1; each
subsequent click advances one move. Undo rewinds and steps the solve
cursor back, so the same move can be replayed for review.
The control panel surfaces the upcoming algorithm with the next move
highlighted, the completed prefix struck through, and the button label
shows "Next: <move> (k/N)" while a plan is active.
Any state-changing action outside the plan — drag commit, scramble,
reset, manual keyboard move — drops the queued solution so the cursor
never points at an algorithm that no longer matches the cube state.
Pointer drag and keyboard input were independent. Pressing R, Space, Z,
or Esc mid-drag started a second animation that fought with the gesture
for the same meshes — crash or scrambled state.
- Pointer gesture flips the shared `busy` flag through a setBusy callback
on entering DRAGGING and clears it from cleanup() (after the snap
animation resolves), so triggerMove / scramble / undo / solve already
bail via their existing `if (busy) return` checks.
- Reset is gated the same way; it now returns a boolean so App.svelte
knows whether to clear the move log and timer.
- Lock-axis transition re-checks isBusy: if a keyboard animation
started during PROBING, the gesture aborts cleanly instead of racing.
- Solver: cubejs-backed two-phase solver, lazy-loaded chunk so the ~80 KB
table-init cost stays out of the main bundle. New cube-to-facelets
converter (3D model -> 54-char URFDLB string) verified bit-for-bit
against cubejs's own move() output.
- Solve button in ControlsPanel with disabled "Solving..." state, wired
through the CubeView controller; animates each move sequentially.
- Rewrite solved-check to the WCA face-uniformity definition. The old
strict "identity quaternion per cubie" check rejected center spins
(invisible) and whole-cube rotations, both of which are still solved
per WCA / Kociemba.
- Vitest specs under tests/ cover cubie-model, move-definitions,
move-parser, apply-move (4x turns, inverses, sune order=6, R2 == R R),
scrambler, solved-check, algorithm-runner, cube-to-facelets, solver.
39 tests, ~3 s. Adds npm test / npm run test:watch scripts.