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
This commit is contained in:
tiennm99 committed 2026-04-27 18:54:17 +07:00
1 parent 338b65fa54
commit d19a9246d5
6 files changed
+133 -120

No files matched your search

+12 -27
View File
@@ -76,7 +76,7 @@ export function setupPointerGesture({ canvas, camera, controls, parentGroup, mes
if (isBusy && isBusy()) {
// Keyboard animation started during PROBING. We never owned busy,
// so abort this gesture without clearing it.
abortGesture();
endGesture();
return;
}
const hitWorldPos = hitMesh.getWorldPosition(new Vector3());
@@ -124,7 +124,7 @@ export function setupPointerGesture({ canvas, camera, controls, parentGroup, mes
function onPointerUp(e) {
if (e.pointerId !== pointerId) return;
if (state !== STATE_DRAGGING) {
abortGesture();
endGesture();
return;
}
const turns = Math.round(curAngle / HALF_PI);
@@ -136,7 +136,7 @@ export function setupPointerGesture({ canvas, camera, controls, parentGroup, mes
parentGroup, pivot, meshes, cubies,
spec: dummy,
fromAngle: curAngle, toAngle: 0, durationMs: 100
}).then(finishGesture);
}).then(() => endGesture());
return;
}
@@ -148,22 +148,17 @@ export function setupPointerGesture({ canvas, camera, controls, parentGroup, mes
fromAngle: curAngle, toAngle: targetAngle, durationMs: 120
}).then(() => {
onMoveCommitted?.(specToName(spec));
finishGesture();
endGesture();
});
}
// Tear down a gesture that NEVER reached DRAGGING — leaves busy alone.
function abortGesture() {
releasePointer();
controls.enabled = true;
state = STATE_IDLE;
pivot = null;
hitMesh = null;
}
// Tear down a gesture that DID reach DRAGGING — releases busy.
function finishGesture() {
releasePointer();
// Single teardown path. Releases pointer capture + restores OrbitControls,
// and releases the busy gate iff *this* gesture had taken it.
function endGesture() {
if (pointerId !== null) {
try { canvas.releasePointerCapture(pointerId); } catch {}
pointerId = null;
}
controls.enabled = true;
state = STATE_IDLE;
pivot = null;
@@ -174,13 +169,6 @@ export function setupPointerGesture({ canvas, camera, controls, parentGroup, mes
}
}
function releasePointer() {
if (pointerId !== null) {
try { canvas.releasePointerCapture(pointerId); } catch {}
pointerId = null;
}
}
function clamp(v, lo, hi) { return Math.min(hi, Math.max(lo, v)); }
canvas.addEventListener('pointerdown', onPointerDown);
@@ -196,10 +184,7 @@ export function setupPointerGesture({ canvas, camera, controls, parentGroup, mes
canvas.removeEventListener('pointercancel', onPointerUp);
// If unmount happens mid-drag, release busy so the parent isn't
// permanently locked. Safe even if !ownedBusy (no-op).
if (ownedBusy) {
setBusy?.(false);
ownedBusy = false;
}
endGesture();
}
};
}
+34 -50
View File
@@ -1,10 +1,8 @@
// Animates a layer rotation: temporarily reparent moving meshes into a
// THREE.Group, tween group.rotation around the move axis, then bake world
// transforms back into the parent group and update the model.
//
// `applyMoveFn` is called at the end so the cubie model reflects the new state.
import { Group, Quaternion, Vector3 } from 'three';
import { Group } from 'three';
import { Tween, Easing, Group as TweenGroup } from '@tweenjs/tween.js';
import { applyMove, HALF_PI } from '../core/apply-move.js';
import { syncMeshes } from './cubie-meshes.js';
@@ -26,53 +24,12 @@ export function clearTweens() {
TWEENS.removeAll();
}
export function animateMove({ parentGroup, meshes, cubies, spec, durationMs = 200 }) {
return new Promise((resolve) => {
const idx = AXIS_INDEX[spec.axis];
const moving = meshes.filter((m) => {
if (spec.layer === null) return true;
return Math.round(m.userData.cubie.position[idx]) === spec.layer;
});
const pivot = new Group();
parentGroup.add(pivot);
for (const mesh of moving) {
pivot.attach(mesh);
}
const targetAngle = spec.sign * HALF_PI * spec.count;
const state = { a: 0 };
new Tween(state, TWEENS)
.to({ a: targetAngle }, durationMs)
.easing(Easing.Cubic.Out)
.onUpdate(() => {
pivot.rotation[spec.axis] = state.a;
})
.onComplete(() => {
try {
pivot.rotation[spec.axis] = targetAngle;
pivot.updateMatrixWorld(true);
for (const mesh of moving) {
parentGroup.attach(mesh);
}
parentGroup.remove(pivot);
applyMove(cubies, spec);
syncMeshes(meshes);
} finally {
// Resolve even on failure so the awaiter never deadlocks
// with busy=true.
resolve();
}
})
.start();
});
}
export function snapAndAnimate({ parentGroup, pivot, meshes, cubies, spec, fromAngle, toAngle, durationMs = 120 }) {
// Core tween. Caller supplies an already-populated pivot Group plus
// from/to angles; we tween, then bake meshes back to parentGroup and
// optionally apply the move spec to the cubie model.
function tweenPivot({ parentGroup, pivot, meshes, cubies, spec, fromAngle, toAngle, durationMs, applyToModel }) {
return new Promise((resolve) => {
const state = { a: fromAngle };
const wantApply = Math.abs(toAngle) > 1e-3;
new Tween(state, TWEENS)
.to({ a: toAngle }, durationMs)
.easing(Easing.Cubic.Out)
@@ -85,12 +42,39 @@ export function snapAndAnimate({ parentGroup, pivot, meshes, cubies, spec, fromA
parentGroup.attach(mesh);
}
parentGroup.remove(pivot);
if (wantApply) applyMove(cubies, spec);
if (applyToModel) applyMove(cubies, spec);
syncMeshes(meshes);
} finally {
resolve(wantApply);
// Resolve even on failure so the awaiter never deadlocks
// with busy=true.
resolve(applyToModel);
}
})
.start();
});
}
export function animateMove({ parentGroup, meshes, cubies, spec, durationMs = 200 }) {
const idx = AXIS_INDEX[spec.axis];
const pivot = new Group();
parentGroup.add(pivot);
for (const mesh of meshes) {
if (spec.layer === null || Math.round(mesh.userData.cubie.position[idx]) === spec.layer) {
pivot.attach(mesh);
}
}
const targetAngle = spec.sign * HALF_PI * spec.count;
return tweenPivot({
parentGroup, pivot, meshes, cubies, spec,
fromAngle: 0, toAngle: targetAngle,
durationMs, applyToModel: true
});
}
export function snapAndAnimate({ parentGroup, pivot, meshes, cubies, spec, fromAngle, toAngle, durationMs = 120 }) {
return tweenPivot({
parentGroup, pivot, meshes, cubies, spec,
fromAngle, toAngle, durationMs,
applyToModel: Math.abs(toAngle) > 1e-3
});
}