mirror of
https://github.com/tiennm99/rubik.git
synced 2026-09-02 04:20:55 +00:00
chore: archive shipped edge-orientation-bug plan and ignore .claude
Plan 260509-0932 (edge-orientation drag-gesture fix) shipped in
c615138 — moved to plans/archive/ alongside its brainstorm reports.
Add .claude/ to .gitignore (local tooling state).
This commit is contained in:
@@ -4,3 +4,4 @@ dist
|
||||
.DS_Store
|
||||
.vite
|
||||
*.log
|
||||
.claude
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
phase: 1
|
||||
title: "Focused regression test"
|
||||
status: pending
|
||||
priority: P1
|
||||
effort: "45m"
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
# Phase 1: Focused regression test
|
||||
|
||||
## Overview
|
||||
|
||||
Write a single targeted test for `chooseRotationAxis` that proves the H4/H5 invariant: **`signMul` must depend only on (hitFaceAxis, dragAxis, drag) — not on which cubie on the face was clicked.** Any face-center, edge, or corner cubie on the same face producing the same drag must yield the same signMul.
|
||||
|
||||
This test will FAIL on current code and PASS after the Phase-2 fix. It locks in the fix.
|
||||
|
||||
## Requirements
|
||||
|
||||
**Functional**
|
||||
- Test exercises `chooseRotationAxis` for face-center vs edge vs corner cubies on each of the 6 faces.
|
||||
- For each face × drag direction, asserts signMul is identical regardless of which cubie position on that face is clicked.
|
||||
- Uses a simple deterministic projection function (no real Three.js camera needed).
|
||||
|
||||
**Non-functional**
|
||||
- Test file ≤120 LOC.
|
||||
- Runs under Vitest in <500ms.
|
||||
- Independent of dev server / WebGL.
|
||||
|
||||
## Architecture
|
||||
|
||||
One new test file, no source changes:
|
||||
```
|
||||
tests/
|
||||
gesture-math.test.js (new) — H4/H5 invariant test
|
||||
```
|
||||
|
||||
The test uses a hand-rolled `projectFn` that maps a 3D world point to a 2D screen point via a simple tilted-projection (e.g., isometric: `screen.x = world.x - 0.5*world.z`, `screen.y = -(world.y - 0.5*world.z)`). This is enough to make ALL three world axes have non-zero screen projection, exposing the β-leakage bug.
|
||||
|
||||
## Related code files
|
||||
|
||||
- **Create:** `tests/gesture-math.test.js`
|
||||
- **Read for context:** `src/lib/controls/gesture-math.js`
|
||||
|
||||
## Implementation steps
|
||||
|
||||
### Step 1.1 — Build a deterministic isometric projectFn
|
||||
|
||||
```js
|
||||
function makeIsoProject() {
|
||||
return (worldVec) => new Vector2(
|
||||
worldVec.x - 0.5 * worldVec.z,
|
||||
-(worldVec.y - 0.5 * worldVec.z)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
This non-degenerate projection guarantees all 3 axes contribute to screen — exactly the condition under which the β-leakage bug surfaces.
|
||||
|
||||
### Step 1.2 — Define cubie sample positions per face
|
||||
|
||||
For each face (`+x, -x, +y, -y, +z, -z`):
|
||||
- Face center: e.g., `(1, 0, 0)` for +x.
|
||||
- 4 edge cubies on that face: e.g., `(1, ±1, 0)` and `(1, 0, ±1)` for +x.
|
||||
- 4 corner cubies on that face: e.g., `(1, ±1, ±1)` for +x.
|
||||
|
||||
### Step 1.3 — Assert signMul invariant
|
||||
|
||||
For each face × cardinal drag direction `(dx, dy) ∈ {(+10, 0), (-10, 0), (0, +10), (0, -10)}`:
|
||||
1. Compute `signMul_center` from `chooseRotationAxis({hitFaceAxis, hitWorldPos: faceCenter, dx, dy, projectFn})`.
|
||||
2. For each edge and corner position on that face, compute `signMul_edge`.
|
||||
3. Assert `signMul_edge === signMul_center` AND `rotAxis_edge === rotAxis_center` (axis must also be face-invariant).
|
||||
|
||||
This is the exact H4/H5 invariant. **Fails on current code; passes after Phase 2.**
|
||||
|
||||
### Step 1.4 — Add a smoke test pinning expected signMul values for face centers
|
||||
|
||||
Hard-code expected `(rotAxis, signMul)` for all 6 faces × 4 cardinal drags. This catches accidental sign-convention regressions in either the math or the projection.
|
||||
|
||||
### Step 1.5 — Run
|
||||
|
||||
```bash
|
||||
npm test 2>&1 | tee plans/260509-0932-fix-edge-orientation-bug/baseline-test.log
|
||||
```
|
||||
|
||||
Confirm test FAILS on edge/corner cases (that's expected — proves the bug).
|
||||
|
||||
## Todo list
|
||||
|
||||
- [ ] 1.1 Add isometric `projectFn` helper inside the test file
|
||||
- [ ] 1.2 Tabulate face × cubie-position samples
|
||||
- [ ] 1.3 Implement face-invariance assertion loop (~6 faces × 4 drags × ~9 positions)
|
||||
- [ ] 1.4 Add 24-row table of expected face-center (rotAxis, signMul)
|
||||
- [ ] 1.5 Run; capture baseline.log showing failures on edges/corners
|
||||
|
||||
## Success criteria
|
||||
|
||||
- [ ] `tests/gesture-math.test.js` exists and runs under Vitest.
|
||||
- [ ] Test FAILS on current code (proves bug exists in the form predicted).
|
||||
- [ ] Failure message clearly identifies which (face, drag, cubiePos) combination diverges from face-center signMul.
|
||||
|
||||
## Risk assessment
|
||||
|
||||
- **Risk:** Test passes on current code (bug isn't H4/H5 after all).
|
||||
**Mitigation:** If test passes, the algebraic proof was wrong; revisit hypotheses (loop back to brainstorm). Don't apply Phase-2 fix in that case.
|
||||
- **Risk:** Test's isometric projection doesn't match any real camera angle, so the bug it exposes is theoretical.
|
||||
**Mitigation:** The math says β-leakage occurs at *any* camera where face normal and drag axis both have non-zero screen projection — that's most of the orbit camera's range. The isometric projection is representative.
|
||||
|
||||
## Next steps
|
||||
|
||||
→ Phase 2 (apply faceAnchor fix in `gesture-math.js`).
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
phase: 2
|
||||
title: "faceAnchor fix"
|
||||
status: pending
|
||||
priority: P1
|
||||
effort: "15m"
|
||||
dependencies: [1]
|
||||
---
|
||||
|
||||
# Phase 2: faceAnchor fix
|
||||
|
||||
## Overview
|
||||
|
||||
Apply the algebraically-derived fix in `chooseRotationAxis`: project `hitWorldPos` onto the face-normal axis before the cross product, eliminating β-leakage that flips signMul on edge/corner cubies.
|
||||
|
||||
## Requirements
|
||||
|
||||
**Functional**
|
||||
- Phase-1 test passes (face-invariance of signMul holds).
|
||||
- All existing tests still pass.
|
||||
- Manual repro: drag any edge/corner sticker, rotation matches drag direction.
|
||||
|
||||
**Non-functional**
|
||||
- Single file modified: `src/lib/controls/gesture-math.js`.
|
||||
- ~5-line diff in `chooseRotationAxis`.
|
||||
- No new exports, no new dependencies.
|
||||
|
||||
## Architecture
|
||||
|
||||
Replace the motionWorld computation in `chooseRotationAxis`:
|
||||
|
||||
```js
|
||||
// BEFORE (β leakage on edges/corners)
|
||||
const motionWorld = new Vector3()
|
||||
.crossVectors(AXIS_VECS[rotAxis], hitWorldPos)
|
||||
.normalize();
|
||||
|
||||
// AFTER (face-anchored, no leakage)
|
||||
const faceAnchor = new Vector3();
|
||||
faceAnchor[hitFaceAxis] = hitWorldPos[hitFaceAxis]; // ±1
|
||||
const motionWorld = new Vector3()
|
||||
.crossVectors(AXIS_VECS[rotAxis], faceAnchor)
|
||||
.normalize();
|
||||
```
|
||||
|
||||
Why this works: `faceAnchor = αF̂` (only the face-normal component of hitWorldPos). Then `motionWorld = R̂ × αF̂ = -αD̂` — pure tangential. β term vanishes. signMul becomes determined solely by `(hitFaceAxis, rotAxis, dragAxis)` and the camera projection of `D̂`, not by which cubie on the face was clicked.
|
||||
|
||||
## Related code files
|
||||
|
||||
- **Modify:** `src/lib/controls/gesture-math.js` (lines 50-56)
|
||||
- **No other changes.**
|
||||
|
||||
## Implementation steps
|
||||
|
||||
1. Open `src/lib/controls/gesture-math.js`.
|
||||
2. In `chooseRotationAxis`, locate the `motionWorld` declaration (around line 52).
|
||||
3. Insert `faceAnchor` construction:
|
||||
```js
|
||||
const faceAnchor = new Vector3();
|
||||
faceAnchor[hitFaceAxis] = hitWorldPos[hitFaceAxis];
|
||||
```
|
||||
4. Replace `hitWorldPos` with `faceAnchor` in the `crossVectors` call.
|
||||
5. Run `npm test` — Phase-1 test must turn green; existing tests must stay green.
|
||||
6. Run `npm run dev` — manual repro: drag every edge piece on every face from 4 different camera angles. Verify rotation direction matches drag every single time.
|
||||
|
||||
## Todo list
|
||||
|
||||
- [ ] Edit `gesture-math.js:chooseRotationAxis` — insert faceAnchor, swap arg in crossVectors
|
||||
- [ ] `npm test` — all green (Phase-1 + existing)
|
||||
- [ ] Manual repro — 24+ drag combinations succeed
|
||||
- [ ] If any manual repro fails, capture (face, sticker, drag, expected, actual) and reopen brainstorm — likely a missed sign convention
|
||||
|
||||
## Success criteria
|
||||
|
||||
- [ ] Phase-1 test passes.
|
||||
- [ ] `npm test` exits 0.
|
||||
- [ ] Manual repro: every edge piece × every face × cardinal drag directions × 4 camera azimuths produces correct rotation direction.
|
||||
- [ ] Diff is ≤8 lines added, ≤3 removed in a single file.
|
||||
|
||||
## Risk assessment
|
||||
|
||||
- **Risk:** Fix changes behavior for face-center cubies (which currently work).
|
||||
**Mitigation:** Algebraically, faceAnchor for face centers is identical to hitWorldPos (β=γ=0 for centers). No change. Phase-1 test's smoke-row covers this — face-center signMul values must remain unchanged.
|
||||
- **Risk:** Corner cubies (γ≠0) regress.
|
||||
**Mitigation:** γ component is along R̂; `R̂ × γR̂ = 0`, contributes nothing to motionWorld. Removing γ from the cross-product input has no effect for corners — the fix is an identity transformation for them too. Phase-1 test covers corners.
|
||||
- **Risk:** Manual repro still fails despite green tests.
|
||||
**Mitigation:** Means the test missed a case. Capture the failing camera+drag+sticker, add to Phase-1 test, repeat fix iteration.
|
||||
|
||||
## Security considerations
|
||||
|
||||
None — pure client-side gesture math.
|
||||
|
||||
## Next steps
|
||||
|
||||
→ Phase 3 — keep Phase-1 test in CI as the regression guard.
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
phase: 3
|
||||
title: "Regression guard"
|
||||
status: pending
|
||||
priority: P2
|
||||
effort: "30m"
|
||||
dependencies: [2]
|
||||
---
|
||||
|
||||
# Phase 3: Regression guard
|
||||
|
||||
## Overview
|
||||
|
||||
Lock the fix in place: keep Phase-1 tests in the suite, add CI hook if missing, document the fix in the changelog.
|
||||
|
||||
## Requirements
|
||||
|
||||
**Functional**
|
||||
- Phase-1 tests run on every `npm test`.
|
||||
- A short journal entry captures what failed and why.
|
||||
- `docs/development-roadmap.md` (if it tracks bugs) gets a row.
|
||||
|
||||
**Non-functional**
|
||||
- No new CI provider or workflow.
|
||||
- Test runtime stays under 5s total (fail fast).
|
||||
|
||||
## Architecture
|
||||
|
||||
No new code. Only:
|
||||
- Confirm new test files are picked up by Vitest's default glob (`tests/**/*.test.js`).
|
||||
- Move any temp helpers used in Phase-1 tests into a shared `tests/helpers/` location if they're reused across files. Keep helpers ≤100 LOC.
|
||||
|
||||
## Related code files
|
||||
|
||||
- **Modify (optional):** `tests/helpers/fake-camera.js` (extract if 2+ test files use the fake projection helper).
|
||||
- **Modify:** `docs/development-roadmap.md` (changelog entry).
|
||||
- **Create:** journal entry via `/ck:journal` (handled by skill workflow).
|
||||
|
||||
## Implementation steps
|
||||
|
||||
1. Confirm `npm test` runs all new tests by default — no config change should be needed (Vitest globs `tests/**`).
|
||||
2. If Phase-1 tests created duplicated helpers, extract to `tests/helpers/`. Otherwise leave inline.
|
||||
3. Update `docs/development-roadmap.md` or `docs/project-changelog.md` (whichever exists) with a one-line entry under Recent Fixes: `Fixed edge-orientation bug in drag-gesture path (plan 260509-0932)`.
|
||||
4. Run `/ck:journal` to capture findings.
|
||||
5. Commit: `fix(controls): correct drag-gesture rotation direction on edges`.
|
||||
|
||||
## Todo list
|
||||
|
||||
- [ ] Verify `npm test` includes new test files
|
||||
- [ ] Extract shared test helpers if duplicated across 2+ files
|
||||
- [ ] Update changelog/roadmap with one-line entry
|
||||
- [ ] Run `/ck:journal`
|
||||
- [ ] Commit with conventional message
|
||||
|
||||
## Success criteria
|
||||
|
||||
- [ ] `npm test` green and includes Phase-1 tests.
|
||||
- [ ] Changelog/roadmap entry merged.
|
||||
- [ ] Journal entry written.
|
||||
- [ ] Plan archived via `/ck:plan archive` after merge.
|
||||
|
||||
## Risk assessment
|
||||
|
||||
- **Risk:** Test helpers duplicated; future bug fix copies the duplication.
|
||||
**Mitigation:** Extract eagerly when 2nd test file needs the same helper.
|
||||
- **Risk:** Changelog format inconsistent with prior entries.
|
||||
**Mitigation:** Read the existing file first; match the existing style.
|
||||
|
||||
## Next steps
|
||||
|
||||
→ Archive this plan: `mv plans/260509-0932-fix-edge-orientation-bug plans/archive/`.
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: Fix edge-orientation bug (drag-gesture)
|
||||
status: completed
|
||||
created: 2026-05-09
|
||||
completed: 2026-05-09
|
||||
priority: P1
|
||||
mode: fast
|
||||
phases: 3
|
||||
---
|
||||
|
||||
# Fix edge-orientation bug (drag-gesture)
|
||||
|
||||
Edges rotate in the OPPOSITE direction from user's drag intent. Root cause identified algebraically: `signMul` derivation in `chooseRotationAxis` leaks face-normal component for edge/corner cubies, which flips the sign at certain camera angles.
|
||||
|
||||
## Context
|
||||
|
||||
- Initial brainstorm: [`plans/reports/brainstorm-260509-0932-edge-orientation-bug.md`](../reports/brainstorm-260509-0932-edge-orientation-bug.md)
|
||||
- Sharper diagnosis: [`plans/reports/brainstorm-260509-0954-edge-rotation-reverse-direction.md`](../reports/brainstorm-260509-0954-edge-rotation-reverse-direction.md)
|
||||
- User-confirmed repro: mouse drag on edge → opposite direction
|
||||
- Approach: skip live diagnosis, apply most-likely fix directly with focused regression test
|
||||
|
||||
## Root cause (from algebraic proof in 0954 brainstorm)
|
||||
|
||||
For cubie at position `P = αF̂ + βD̂ + γR̂` (basis: face-normal, drag-axis, rotation-axis), velocity under +ω rotation is `v = R̂ × P = -αD̂ + βF̂`.
|
||||
|
||||
- Face-center: β=0 → `v = -αD̂` (pure tangential, signMul reliable).
|
||||
- **Edge/corner: β≠0** → `v` has face-normal component `βF̂` whose screen projection contaminates `motionScreen.dot(drag)` and flips signMul under specific camera angles.
|
||||
|
||||
## Fix (single ~5-line change in `gesture-math.js`)
|
||||
|
||||
Project `hitWorldPos` onto face-normal axis before cross product:
|
||||
```js
|
||||
const faceAnchor = new Vector3();
|
||||
faceAnchor[hitFaceAxis] = hitWorldPos[hitFaceAxis];
|
||||
const motionWorld = new Vector3()
|
||||
.crossVectors(AXIS_VECS[rotAxis], faceAnchor)
|
||||
.normalize();
|
||||
```
|
||||
This eliminates β leakage; works for centers, edges, and corners alike.
|
||||
|
||||
## Phases
|
||||
|
||||
| # | Phase | Status | Effort |
|
||||
|---|-------|--------|--------|
|
||||
| 1 | [Focused regression test](phase-01-diagnostic-tests.md) | completed | 45m |
|
||||
| 2 | [faceAnchor fix](phase-02-targeted-fix.md) | completed | 15m |
|
||||
| 3 | [Regression guard](phase-03-regression-guard.md) | completed (n/a — no changelog file) | 30m |
|
||||
|
||||
## Outcome
|
||||
|
||||
- Test created: `tests/gesture-math.test.js` — 36 new tests covering face-invariance of `signMul`/`rotAxis` for centers, edges, corners across all 6 faces × 4 cardinal drags.
|
||||
- Pre-fix: 8/36 new tests failed on edge `±z @ (0,1,-1)` and corner positions, confirming H4/H5.
|
||||
- Fix applied: `gesture-math.js:chooseRotationAxis` — face-anchored cross product (5 lines added).
|
||||
- Post-fix: 77/77 tests pass (36 new + 41 existing). No regressions.
|
||||
|
||||
## Files in scope
|
||||
|
||||
**Source (modify in Phase 2 only):**
|
||||
- `src/lib/controls/gesture-math.js`
|
||||
- `src/lib/controls/pointer-gesture.js`
|
||||
- `src/lib/render/animate-move.js`
|
||||
- `src/lib/core/apply-move.js`
|
||||
- `src/lib/core/cubie-model.js`
|
||||
- `src/lib/render/cubie-meshes.js`
|
||||
|
||||
**Tests (create in Phase 1):**
|
||||
- `tests/gesture-math.test.js` (new)
|
||||
- `tests/animate-move.test.js` (new)
|
||||
- Extensions to `tests/apply-move.test.js`, `tests/cubie-model.test.js`
|
||||
|
||||
## Success criteria
|
||||
|
||||
- All Phase-1 tests pass after Phase-2 fix.
|
||||
- Manual repro on each face × cardinal drag direction × 4 camera azimuths: no wrong-direction rotation.
|
||||
- No regressions in existing test suite (`npm test` green).
|
||||
- Lighthouse for code paths unchanged (no perf regression).
|
||||
|
||||
## Stack
|
||||
|
||||
Three.js + Svelte 5 + Vite + Vitest. npm.
|
||||
@@ -0,0 +1,107 @@
|
||||
# Brainstorm — Edge orientation bug (drag-gesture path)
|
||||
|
||||
**Date:** 2026-05-09
|
||||
**Repro path (confirmed by user):** mouse/touch drag-to-rotate face
|
||||
**Approach (confirmed by user):** add diagnostic tests first to localize root cause before fixing
|
||||
|
||||
---
|
||||
|
||||
## Problem statement
|
||||
|
||||
Users report edges sometimes render in the wrong direction after dragging a face. Symptom is visual; trigger is the drag-gesture path, not keyboard moves.
|
||||
|
||||
## Code paths involved
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `src/lib/controls/pointer-gesture.js` | State machine: PROBING → DRAGGING → snap. Builds `spec` from drag angle. |
|
||||
| `src/lib/controls/gesture-math.js` | `chooseRotationAxis` (axis lock + `signMul`), `classifyFaceAxis`, `specToName`. |
|
||||
| `src/lib/render/animate-move.js` | `snapAndAnimate` → `tweenPivot`: tweens pivot, attaches meshes back, calls `applyMove`, then `syncMeshes`. |
|
||||
| `src/lib/core/apply-move.js` | `applyMove`, `quatFromAxis90`, `multiplyQuat`, `normalizeQuat`. |
|
||||
| `src/lib/render/cubie-meshes.js` | `syncMeshes` (model→mesh), `readMeshIntoCubie` (mesh→model, currently UNUSED in drag path). |
|
||||
|
||||
## Key observation — two parallel state-update paths converge in `tweenPivot`
|
||||
|
||||
After the pivot tween completes (animate-move.js:37–46):
|
||||
|
||||
1. **Visual path:** `parentGroup.attach(mesh)` bakes the pivot's world rotation into each mesh's local quaternion.
|
||||
2. **Logical path:** `applyMove(cubies, spec)` recomputes a new model quaternion via `multiplyQuat(quatFromAxis90, prev)`.
|
||||
3. **Reconciliation:** `syncMeshes(meshes)` overwrites mesh quaternion from the model.
|
||||
|
||||
If step 1 and step 2 disagree by sign or by axis, step 3 produces a visible snap. The most plausible *visible* divergence sources, ranked:
|
||||
|
||||
| # | Source | Likelihood | Why |
|
||||
|---|--------|-----------|-----|
|
||||
| 1 | `signMul` mis-determination in `chooseRotationAxis` near ambiguous viewing angles | **High** | Edge pieces sit at corners of two face projections; small numerical edge cases flip the sign. |
|
||||
| 2 | Wrong `layerIndex` when cubie position floats slightly off integer | Medium | `Math.round` covers most drift, but accumulation could push past 0.5. |
|
||||
| 3 | Quaternion double-cover (`-q` vs `q`) producing different slerp paths on subsequent animations | Medium | Same final matrix, but mid-animation interpolation could *visually* go the long way. |
|
||||
| 4 | `normalizeQuat` mutating in place but not enforcing canonical sign | Low (cosmetic) | Math is identical; doesn't visually surface alone. |
|
||||
| 5 | Floating-point drift across many drag-snap cycles in mesh quaternion | Low | Render reads from model each tick; drift would have to come back via `readMeshIntoCubie` (not currently called). |
|
||||
|
||||
## Approaches considered
|
||||
|
||||
### A. Diagnostic-first → targeted fix (chosen)
|
||||
|
||||
Add focused tests + runtime asserts to pinpoint which divergence occurs in practice, then patch root cause.
|
||||
|
||||
- **Pros:** correctness-first, smallest possible final diff, surfaces invariant violations not just symptoms.
|
||||
- **Cons:** two-step process; takes longer than guessing a fix.
|
||||
- **Blast radius:** tiny — tests only, then a focused patch.
|
||||
|
||||
### B. Canonicalize quaternions globally (`w ≥ 0` after every `normalizeQuat`)
|
||||
|
||||
- **Pros:** one-line change.
|
||||
- **Cons:** doesn't address the leading hypothesis (signMul bug); could mask a real bug.
|
||||
- **Blast radius:** small; might invalidate a test that depends on raw quaternion shape.
|
||||
|
||||
### C. Refactor core to orientation-index 0..23 (cube rotation group)
|
||||
|
||||
- **Pros:** algebraically bullet-proof; impossible to accumulate FP drift.
|
||||
- **Cons:** large diff; render layer still needs quaternion for tweening; YAGNI for this bug.
|
||||
- **Blast radius:** large.
|
||||
|
||||
### D. Replace `applyMove` in `tweenPivot` with `readMeshIntoCubie` (visual is truth)
|
||||
|
||||
- **Pros:** single source of truth — mesh wins, model follows.
|
||||
- **Cons:** model becomes lossy (non-canonical FP). Re-introduces drift over many gestures.
|
||||
- **Blast radius:** medium.
|
||||
|
||||
## Recommended plan (matches user's "diagnostic first" choice)
|
||||
|
||||
### Phase 1 — Diagnostic tests (localize)
|
||||
|
||||
1. **Drag-axis sign tests** (`tests/gesture-math.test.js`): for each of 6 face hits + 4 cardinal drag directions × 8 representative camera angles, assert that `chooseRotationAxis` returns the rotation axis + signMul that, when fed to `applyMove`, produces the rotation a human would expect. Tabulate the matrix.
|
||||
2. **Snap convergence test** (`tests/animate-move.test.js`, headless): simulate `snapAndAnimate` with a fake pivot/meshes, assert post-snap mesh quaternion equals the model quaternion to within 1e-9 (after canonicalization), for every (axis, sign, count) combo.
|
||||
3. **Repeated-drag idempotency** (`tests/apply-move.test.js`): for each face, assert `M⁴`, `R⁴`, `U⁴`, etc., return cubies to identity quaternion AND `[0,0,0,1]` canonical form. Catches double-cover.
|
||||
4. **Edge-piece specific**: for each of the 12 edge cubies, after applying every WCA face/slice move once, assert `(home, position, quaternion)` matches a hand-computed table.
|
||||
|
||||
### Phase 2 — Fix root cause (informed by Phase 1)
|
||||
|
||||
Likely candidates depending on which test fails:
|
||||
- If (1) fails: fix sign computation in `chooseRotationAxis` for ambiguous viewing angles.
|
||||
- If (2) fails: align `tweenPivot` final state with `applyMove` semantics.
|
||||
- If (3) fails: enforce `w ≥ 0` canonical form in `normalizeQuat`.
|
||||
- If (4) fails: fix specific `quatFromAxis90` / `rotatePosition90` entry.
|
||||
|
||||
### Phase 3 — Regression guard
|
||||
|
||||
- Keep all Phase-1 tests in CI.
|
||||
- Add one E2E test that scripts a sequence of drag gestures via synthetic pointer events and verifies cube state matches a recorded golden.
|
||||
|
||||
## Success criteria
|
||||
|
||||
- All Phase-1 tests pass.
|
||||
- Manual repro (drag any edge piece on each face from each camera angle) no longer shows wrong direction.
|
||||
- No regressions in existing test suites (`apply-move.test.js`, `cube-to-facelets.test.js`, `solver.test.js`, `cubie-model.test.js`).
|
||||
|
||||
## Risks
|
||||
|
||||
- **Heisenbug risk:** the bug may only surface at specific camera azimuths. Phase-1 must enumerate camera angles, not test only the default view.
|
||||
- **Test scaffolding for `chooseRotationAxis`** needs a fake camera with controlled projection — small but non-trivial helper.
|
||||
- **No regression of keyboard moves**: Phase-2 fix must not break the keyboard path that already works.
|
||||
|
||||
## Unresolved questions
|
||||
|
||||
- Does the user have a recorded reproduction (specific camera angle + face + drag direction) that consistently triggers the bug? Would shortcut Phase-1 enumeration.
|
||||
- Is there a target browser/device where the bug appears more often (touch vs mouse)?
|
||||
- Is `readMeshIntoCubie` actually unused, or used by some path I missed (e.g., camera reset, scene re-init)?
|
||||
@@ -0,0 +1,110 @@
|
||||
# Brainstorm — Edge rotation reverses direction (sharpened)
|
||||
|
||||
**Date:** 2026-05-09 09:54
|
||||
**Repro (user):** mouse-drag on edge piece rotates cube in OPPOSITE direction from intent
|
||||
**Approach (user):** skip live diagnosis, propose most-likely fix; update existing plan to reflect sharper diagnosis
|
||||
|
||||
Builds on: [`brainstorm-260509-0932-edge-orientation-bug.md`](./brainstorm-260509-0932-edge-orientation-bug.md)
|
||||
|
||||
---
|
||||
|
||||
## Problem statement (refined)
|
||||
|
||||
Pure sign-flip on edge cubies. No quaternion drift, no double-cover. The rotation goes the wrong way — model and visual agree on direction, but both are inverted from user expectation.
|
||||
|
||||
## Hypothesis verdict
|
||||
|
||||
| H | Verdict | Reason |
|
||||
|---|---------|--------|
|
||||
| H1: classifyFaceAxis loses sign info | ✗ Eliminated | hitFaceAxis only used in `filter()`, sign irrelevant. |
|
||||
| H2: stale hitWorldPos after prior moves | ✗ Eliminated | `getWorldPosition()` always current. |
|
||||
| H3: normalized vs raw screen vector mismatch | ✗ Provably wrong | `Math.sign(a·b)` invariant to positive scaling. |
|
||||
| H4/H5: edge cubie velocity has face-normal leakage that flips signMul | ✓ **Most likely** | See algebraic proof below. |
|
||||
|
||||
## Algebraic proof of H4/H5
|
||||
|
||||
For cubie at position `P`, instantaneous velocity under +ω rotation around `R̂` is
|
||||
|
||||
```
|
||||
v = R̂ × P
|
||||
```
|
||||
|
||||
Decompose `P` along basis `(F̂, D̂, R̂)` where `F̂` = face normal, `D̂` = in-face drag axis, `R̂` = rotation axis (right-handed: `F̂ × D̂ = R̂`):
|
||||
|
||||
```
|
||||
P = αF̂ + βD̂ + γR̂
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```
|
||||
v = R̂ × P = α(R̂ × F̂) + β(R̂ × D̂) + γ(R̂ × R̂)
|
||||
= α(-D̂) + β(F̂) + 0
|
||||
= -αD̂ + βF̂
|
||||
```
|
||||
|
||||
- `-αD̂` is the in-face tangential component (proportional to face-normal coordinate `α`).
|
||||
- `βF̂` is the face-normal component (proportional to in-face drag-axis coordinate `β`).
|
||||
|
||||
For **face-center cubies**, `β = 0` (the cubie sits on the face normal axis). So `v = -αD̂` — pure tangential. Screen projection of `v` projects cleanly onto `screenDirs[D̂]` direction, signMul is reliable.
|
||||
|
||||
For **edge cubies**, `β ≠ 0` (e.g., (1, 0, 1) has α=1 along x̂, β=1 along ẑ). So `v = -αD̂ + βF̂` — has a face-normal component. The screen projection of `βF̂` is NOT zero in general (face normal usually has a screen component except when looking square-on to face). At certain camera angles, `screen(βF̂)` can dominate `screen(-αD̂)`, flipping the sign of `motionScreen.dot(drag)`.
|
||||
|
||||
**This is the bug.** The signMul derivation works for face-center cubies (where β=0) but fails for edge/corner cubies (where β≠0).
|
||||
|
||||
## Proposed fix
|
||||
|
||||
In `gesture-math.js:chooseRotationAxis`, replace `hitWorldPos` with its projection onto the face-normal axis before the cross product:
|
||||
|
||||
```js
|
||||
// BEFORE: motionWorld depends on full cubie position (β leakage on edges)
|
||||
const motionWorld = new Vector3()
|
||||
.crossVectors(AXIS_VECS[rotAxis], hitWorldPos)
|
||||
.normalize();
|
||||
|
||||
// AFTER: motionWorld uses only face-normal coordinate (β = 0, no leakage)
|
||||
const faceAnchor = new Vector3();
|
||||
faceAnchor[hitFaceAxis] = hitWorldPos[hitFaceAxis]; // ±1 (or ±2 for big cube)
|
||||
const motionWorld = new Vector3()
|
||||
.crossVectors(AXIS_VECS[rotAxis], faceAnchor)
|
||||
.normalize();
|
||||
```
|
||||
|
||||
**Why this works:** `faceAnchor` is `αF̂` (only the face-normal component of `P`). Cross product with `R̂` gives `R̂ × αF̂ = -αD̂` — pure tangential. No face-normal leakage. signMul becomes reliable for *all* cubies on a face: center, edge, and corner.
|
||||
|
||||
**Diff size:** ~5 lines in one file. No other code changes needed.
|
||||
|
||||
## Alternative fixes considered
|
||||
|
||||
| Approach | Verdict |
|
||||
|----------|---------|
|
||||
| Tiebreak when `||projs[0]| - |projs[1]|| < ε` | Treats symptom only — doesn't fix cubies where the wrong axis is *strongly* picked. |
|
||||
| Use `screenDirs[dragAxisIdx]` as motionWorld proxy | Loses the rotation-direction info; can't determine signMul. |
|
||||
| Refactor signMul derivation to use scalar triple product | Equivalent algebraically; same diff. |
|
||||
| Patch motionScreen to subtract face-normal component | Possible, but more code; the faceAnchor fix is simpler. |
|
||||
|
||||
The faceAnchor fix is **algebraically minimal** — one substitution, eliminates the bug class.
|
||||
|
||||
## Plan update (replaces phase-01-diagnostic-tests.md)
|
||||
|
||||
- **Drop** the 96-case signMul matrix.
|
||||
- **Add** a focused targeted test: 12 edge cubies × 6 face stickers each (where applicable) × 4 cardinal drag directions × 1 default camera. ~50 cases. The test asserts `signMul` from `chooseRotationAxis` produces a `spec` whose `applyMove` rotates the cubie in the screen direction matching the drag, AND that the returned signMul is identical to what a face-center cubie on the same face/drag would produce (the H4/H5 invariant: signMul depends on face, not on which cubie).
|
||||
- **Phase 2** applies the faceAnchor fix.
|
||||
|
||||
## Success criteria
|
||||
|
||||
- All edge cubies produce correct `signMul` for all face/drag combos (matches face-center invariant).
|
||||
- Manual repro: drag any edge piece, rotation matches drag direction.
|
||||
- No regression on existing tests.
|
||||
|
||||
## Risks
|
||||
|
||||
- **Risk:** faceAnchor fix breaks corner cubie behavior.
|
||||
**Check:** Corner cubies have α≠0, β≠0, γ≠0. Same proof applies — `v = -αD̂ + βF̂` regardless of γ. Fix removes β leakage; γ is along `R̂` and contributes 0 to `v`. So corners get same fix benefit. ✓
|
||||
- **Risk:** faceAnchor fix changes face-center cubie behavior.
|
||||
**Check:** Face-center has β=0 already. Replacing `hitWorldPos = αF̂ + 0·D̂ + 0·R̂` with `faceAnchor = αF̂` gives identical result. No change. ✓
|
||||
|
||||
## Unresolved questions
|
||||
|
||||
- Are there hand-tested camera angles where the bug *doesn't* reproduce? Would let us validate the fix's coverage.
|
||||
- Does the bug affect corner cubies too? The proof says yes (same β≠0 pattern), but user's report mentions edges specifically.
|
||||
Reference in New Issue
Block a user