fix(solver): world-anchored reference normal prevents planar axial drift #336

Merged
forbes merged 3 commits from fix/planar-halfspace-drag-flip into main 2026-02-26 17:10:41 +00:00
Owner

Problem

When a PlanarConstraint is combined with a CylindricalConstraint on the same body pair, the body drifts along the cylinder axis during drag. The planar distance=0 constraint fails to hold.

Root Cause

PlanarConstraint.residuals() computes its point-in-plane distance using z_j = marker_z_axis(body_j, marker_j_quat) — a body-attached normal that rotates with body_j. The CylindricalConstraint allows rotation about the shared axis, so as the body rotates during Newton iteration, the plane normal tilts. The distance residual (p_i - p_j) · z_j then constrains a rotating plane instead of the original one, allowing the body to drift axially.

Fix

Snapshot the world-frame normal at system-build time (_build_system()) and store it on the PlanarConstraint as reference_normal. The distance residual then uses Const nodes: (p_i - p_j) · z_ref - offset = 0, constraining displacement along the original plane normal regardless of body rotation.

The cross-product (parallelism) residuals still use body-attached normals — correctly enforcing orientation alignment. Only the distance residual needs the fixed reference direction. Works for any offset value.

Changes

  • constraints.py: Add reference_normal field to PlanarConstraint; use Const nodes in distance residual when set
  • solver.py: Evaluate and set reference_normal in _build_system() for all PlanarConstraint objects
  • test_drag.py: Add TestPlanarCylindricalAxialDrift — 3 regression tests (zero offset, non-zero offset, drag perturbation)

All 294 solver tests pass.

## Problem When a PlanarConstraint is combined with a CylindricalConstraint on the same body pair, the body drifts along the cylinder axis during drag. The planar distance=0 constraint fails to hold. ## Root Cause PlanarConstraint.residuals() computes its point-in-plane distance using z_j = marker_z_axis(body_j, marker_j_quat) — a body-attached normal that rotates with body_j. The CylindricalConstraint allows rotation about the shared axis, so as the body rotates during Newton iteration, the plane normal tilts. The distance residual (p_i - p_j) · z_j then constrains a rotating plane instead of the original one, allowing the body to drift axially. ## Fix Snapshot the world-frame normal at system-build time (_build_system()) and store it on the PlanarConstraint as reference_normal. The distance residual then uses Const nodes: (p_i - p_j) · z_ref - offset = 0, constraining displacement along the original plane normal regardless of body rotation. The cross-product (parallelism) residuals still use body-attached normals — correctly enforcing orientation alignment. Only the distance residual needs the fixed reference direction. Works for any offset value. ### Changes - constraints.py: Add reference_normal field to PlanarConstraint; use Const nodes in distance residual when set - solver.py: Evaluate and set reference_normal in _build_system() for all PlanarConstraint objects - test_drag.py: Add TestPlanarCylindricalAxialDrift — 3 regression tests (zero offset, non-zero offset, drag perturbation) All 294 solver tests pass.
forbes added 2 commits 2026-02-26 17:07:48 +00:00
fix(assembly): update solver submodule — fix planar half-space drag flip
Some checks failed
Build and Test / build (pull_request) Has been cancelled
559a240799
Updates mods/solver to include fix for the planar half-space correction
that caused 'flipped orientation' rejections when dragging a body
connected by a Cylindrical joint + distance=0 Planar constraint.

The solver's PlanarConstraint half-space tracker was reflecting the body
through the plane when the face normal dot product crossed zero during
legitimate rotation about the cylindrical axis. Now returns a tracking-
only HalfSpace (no correction) for on-plane constraints, matching the
pattern used by Cylindrical/Revolute/Concentric trackers.

See: kindred/solver#38
fix(solver): update solver submodule — world-anchored planar reference normal
All checks were successful
Build and Test / build (pull_request) Successful in 29m11s
9aaf244179
Updates solver to include the fix for PlanarConstraint axial drift
when combined with CylindricalConstraint. The distance residual now
uses a world-frame reference normal (Const nodes) instead of the
body-attached normal that rotates with the body.
forbes added 1 commit 2026-02-26 17:10:32 +00:00
Merge branch 'main' into fix/planar-halfspace-drag-flip
All checks were successful
Build and Test / build (pull_request) Successful in 28m48s
4c9ff957e3
forbes merged commit 418e947cbd into main 2026-02-26 17:10:41 +00:00
forbes deleted branch fix/planar-halfspace-drag-flip 2026-02-26 17:10:42 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: kindred/create#336