6 Commits
Author SHA1 Message Date
tiennm99 d19a9246d5 refactor: simplifier pass — collapse duplicate code paths
- 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
2026-04-27 18:54:17 +07:00
tiennm99 338b65fa54 fix: address code-review findings (busy-gate races, dispose leaks, a11y)
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
2026-04-27 14:16:07 +07:00
tiennm99 be5253caa0 feat(solve): step-by-step solver with visible plan
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.
2026-04-27 10:53:19 +07:00
tiennm99 0c9a160608 fix(input): gate keyboard moves behind drag state to avoid races
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.
2026-04-27 10:37:06 +07:00
tiennm99 eb592e5c98 feat: add kociemba solver and vitest unit tests
- 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.
2026-04-27 10:25:28 +07:00
tiennm99 996a07a7bf feat: initial 3x3 rubik cube simulator (svelte + three.js) 2026-04-27 09:13:43 +07:00