fix(solver): enforce quaternion continuity on dragged parts during drag (#338) #40

Merged
forbes merged 2 commits from fix/drag-quat-continuity into main 2026-02-27 15:39:18 +00:00
Owner

Problem

During interactive drag with Cylindrical + Planar constraints, the solver converges to valid but distinct quaternion branches on certain drag angles. The C++ validateNewPlacements() then sees a >91° rotation from the baseline and rejects the step, freezing the drag.

The root cause: _enforce_quat_continuity skipped dragged parts (if body.part_id in dragged_ids: continue). In a 2-body assembly (grounded + dragged bar), this meant the function did nothing. Newton re-solves the dragged part's free parameters to satisfy constraints and can land on a different constraint branch.

Fix

Two-level correction in _enforce_quat_continuity, applied to all non-grounded bodies including dragged parts:

  1. Hemisphere check (existing): if dot(q_prev, q_solved) < 0, negate q_solved. Catches simple q vs -q sign flips.

  2. Rotation angle check (new): compute the relative quaternion angle using the same formula as the C++ validator (2*acos(w)). If it exceeds 91°, reset to the previous step's quaternion. This catches deeper branch jumps where the solver finds a geometrically different but constraint-satisfying orientation (e.g. Cylindrical + Planar with 180° ambiguity).

Verification

  • All 291 solver tests pass
  • Confirmed via math that the Level 2 check catches the exact failure from the console test: step 16 (240° drag) produces a quaternion 104.6° from baseline — dot product is 0.61 (positive, same hemisphere), so Level 1 misses it, but Level 2 catches it and resets

Test

See solver#39 for the console test that reproduces this failure.

Refs: kindred/create#338

## Problem During interactive drag with Cylindrical + Planar constraints, the solver converges to valid but distinct quaternion branches on certain drag angles. The C++ `validateNewPlacements()` then sees a >91° rotation from the baseline and rejects the step, freezing the drag. The root cause: `_enforce_quat_continuity` **skipped dragged parts** (`if body.part_id in dragged_ids: continue`). In a 2-body assembly (grounded + dragged bar), this meant the function did nothing. Newton re-solves the dragged part's free parameters to satisfy constraints and can land on a different constraint branch. ## Fix Two-level correction in `_enforce_quat_continuity`, applied to **all non-grounded bodies** including dragged parts: 1. **Hemisphere check** (existing): if `dot(q_prev, q_solved) < 0`, negate `q_solved`. Catches simple `q` vs `-q` sign flips. 2. **Rotation angle check** (new): compute the relative quaternion angle using the same formula as the C++ validator (`2*acos(w)`). If it exceeds 91°, reset to the previous step's quaternion. This catches deeper branch jumps where the solver finds a geometrically different but constraint-satisfying orientation (e.g. Cylindrical + Planar with 180° ambiguity). ## Verification - All 291 solver tests pass - Confirmed via math that the Level 2 check catches the exact failure from the console test: step 16 (240° drag) produces a quaternion 104.6° from baseline — dot product is 0.61 (positive, same hemisphere), so Level 1 misses it, but Level 2 catches it and resets ## Test See solver#39 for the console test that reproduces this failure. Refs: kindred/create#338
forbes added 1 commit 2026-02-27 15:37:32 +00:00
The _enforce_quat_continuity function previously skipped dragged parts,
assuming the GUI directly controls their placement.  However, Newton
re-solves all free params (including the dragged part's) to satisfy
constraints, and can converge to an equivalent but distinct quaternion
branch.  The C++ validateNewPlacements() then sees a >91 degree rotation
and rejects the step.

Two-level fix:

1. Remove the dragged_ids skip — apply continuity to ALL non-grounded
   bodies, including the dragged part.

2. Add rotation angle check beyond simple hemisphere negation: compute
   the relative quaternion angle using the same formula as the C++
   validator (2*acos(w)).  If it exceeds 91 degrees, reset to the
   previous step's quaternion.  This catches branch jumps where the
   solver finds a geometrically different but constraint-satisfying
   orientation (e.g. Cylindrical + Planar with 180-degree ambiguity).

Verified: all 291 solver tests pass.
forbes added 1 commit 2026-02-27 15:39:05 +00:00
forbes merged commit cd7f66f20a into main 2026-02-27 15:39:18 +00:00
forbes deleted branch fix/drag-quat-continuity 2026-02-27 15:39:18 +00:00
Sign in to join this conversation.