fix(solver): prevent orientation flips during interactive drag #36

Merged
forbes merged 1 commits from fix/drag-orientation-stability into main 2026-02-25 02:47:27 +00:00
Owner

Problem

During interactive drag, the solver finds mathematically valid solutions where parts drift through planar/distance constraints while honoring revolute joints. The C++ validateNewPlacements() rejects these as 'flipped orientation' (>91° rotation), causing most drag steps to be rejected (e.g. 17/18 rejected).

Root Causes

  1. Missing half-space tracking for compound constraintsPlanarConstraint, RevoluteConstraint, CylindricalConstraint, etc. all contain parallel-axis or point-in-plane sub-constraints with branch ambiguity, but the preference system only tracked standalone DistancePointPoint, Parallel, Angle, and Perpendicular.

  2. Quaternion hemisphere flips — Newton-Raphson can converge to the -q branch of a quaternion (identical rotation, but the C++ angle measurement sees ~340°).

Changes

preference.py — Half-space tracking for all compound constraints

Added half-space handlers for every constraint type with branch ambiguity:

Constraint Ambiguity Tracking
Planar Normal direction + plane side dot(z_i, z_j) or signed distance + reflection correction
Revolute Axis direction dot(z_i, z_j)
Concentric Axis direction dot(z_i, z_j)
Cylindrical Axis direction dot(z_i, z_j)
Slider Axis direction dot(z_i, z_j)
Screw Axis direction dot(z_i, z_j)
Universal Perpendicular quadrant Cross product dominant component
PointInPlane Plane side Signed distance + reflection correction
LineInPlane Plane side Signed distance + reflection correction

solver.py — Quaternion continuity in drag

  • _enforce_quat_continuity(): After each drag solve, checks every non-dragged body's quaternion against its pre-step value. If dot(q_prev, q_solved) < 0, negates the quaternion (standard SLERP short-arc correction). Essentially free — 4 multiplies + 4 negations per body.
  • pre_drag() snapshots initial solved quaternions into _DragCache.pre_step_quats
  • drag_step() applies continuity enforcement after solving, updates stored quaternions for next step

Testing

All 286 existing tests pass. The fix addresses the symptom shown in logs like:

postDrag — 18 steps, 17 rejected

Expected after fix: near-zero rejections during drag.

## Problem During interactive drag, the solver finds mathematically valid solutions where parts drift through planar/distance constraints while honoring revolute joints. The C++ `validateNewPlacements()` rejects these as 'flipped orientation' (>91° rotation), causing most drag steps to be rejected (e.g. 17/18 rejected). ## Root Causes 1. **Missing half-space tracking for compound constraints** — `PlanarConstraint`, `RevoluteConstraint`, `CylindricalConstraint`, etc. all contain parallel-axis or point-in-plane sub-constraints with branch ambiguity, but the preference system only tracked standalone `DistancePointPoint`, `Parallel`, `Angle`, and `Perpendicular`. 2. **Quaternion hemisphere flips** — Newton-Raphson can converge to the -q branch of a quaternion (identical rotation, but the C++ angle measurement sees ~340°). ## Changes ### `preference.py` — Half-space tracking for all compound constraints Added half-space handlers for every constraint type with branch ambiguity: | Constraint | Ambiguity | Tracking | |---|---|---| | Planar | Normal direction + plane side | dot(z_i, z_j) or signed distance + reflection correction | | Revolute | Axis direction | dot(z_i, z_j) | | Concentric | Axis direction | dot(z_i, z_j) | | Cylindrical | Axis direction | dot(z_i, z_j) | | Slider | Axis direction | dot(z_i, z_j) | | Screw | Axis direction | dot(z_i, z_j) | | Universal | Perpendicular quadrant | Cross product dominant component | | PointInPlane | Plane side | Signed distance + reflection correction | | LineInPlane | Plane side | Signed distance + reflection correction | ### `solver.py` — Quaternion continuity in drag - `_enforce_quat_continuity()`: After each drag solve, checks every non-dragged body's quaternion against its pre-step value. If `dot(q_prev, q_solved) < 0`, negates the quaternion (standard SLERP short-arc correction). Essentially free — 4 multiplies + 4 negations per body. - `pre_drag()` snapshots initial solved quaternions into `_DragCache.pre_step_quats` - `drag_step()` applies continuity enforcement after solving, updates stored quaternions for next step ## Testing All 286 existing tests pass. The fix addresses the symptom shown in logs like: ``` postDrag — 18 steps, 17 rejected ``` Expected after fix: near-zero rejections during drag.
forbes added 1 commit 2026-02-25 02:47:10 +00:00
Add half-space tracking for all compound constraints with branch
ambiguity: Planar, Revolute, Concentric, Cylindrical, Slider, Screw,
Universal, PointInPlane, and LineInPlane.  Previously only
DistancePointPoint, Parallel, Angle, and Perpendicular were tracked,
so the Newton-Raphson solver could converge to the wrong branch for
compound constraints — causing parts to drift through plane
constraints while honoring revolute joints.

Add quaternion continuity enforcement in drag_step(): after solving,
each non-dragged body's quaternion is checked against its pre-step
value and negated if in the opposite hemisphere (standard SLERP
short-arc correction).  This prevents the C++ validateNewPlacements()
from rejecting valid solutions as 'flipped orientation' due to the
quaternion double-cover ambiguity (q and -q encode the same rotation
but measure as ~340° apart).
forbes merged commit 9d86bb203e into main 2026-02-25 02:47:27 +00:00
forbes deleted branch fix/drag-orientation-stability 2026-02-25 02:47:27 +00:00
Sign in to join this conversation.