fix(assembly): classify datum plane references in Distance joints
Some checks failed
Build and Test / build (pull_request) Has been cancelled

When a Distance joint references a datum plane (XY_Plane, XZ_Plane,
YZ_Plane), getDistanceType() failed to recognize it because datum
plane sub-names yield an empty element type. This caused the fallback
to DistanceType::Other → BaseJointKind::Planar, which adds spurious
parallel-normal residuals that overconstrain the system.

For example, three vertex-to-datum-plane Distance joints produced
10 residuals (3×Planar) with 6 mutually contradictory orientation
constraints, causing the solver to find garbage least-squares
solutions.

Add early detection of App::Plane datum objects before the main
geometry classification chain. Datum planes are now correctly mapped:
- Vertex + DatumPlane → PointPlane → PointInPlane (1 residual)
- Edge + DatumPlane → LinePlane → LineInPlane
- Face/DatumPlane + DatumPlane → PlanePlane → Planar
This commit is contained in:
forbes
2026-02-21 22:04:18 -06:00
parent cf2fc82eac
commit b4835e1b05
4 changed files with 156 additions and 9 deletions

View File

@@ -191,6 +191,90 @@ class TestAssemblyOriginPlanes(unittest.TestCase):
result = self.assembly.solve()
self.assertEqual(result, 0, "Solve should succeed with origin plane joint")
# ── Distance joint to datum plane tests ────────────────────────
def test_distance_vertex_to_datum_plane_solves(self):
"""Distance(0) joint: vertex → datum plane solves and pins position."""
origin = self._get_origin()
xy = origin.getXY() # Top (Z normal)
xz = origin.getXZ() # Front (Y normal)
yz = origin.getYZ() # Right (X normal)
box = self._make_box(50, 50, 50)
# 3 Distance joints, each vertex→datum, distance=0.
# This should pin the box's Vertex1 (corner at local 0,0,0) to the
# origin, giving 3 PointInPlane constraints (1 residual each = 3 total).
for plane in [xy, xz, yz]:
joint = self._make_joint(
5, # Distance
[box, ["Vertex1", "Vertex1"]],
[origin, [plane.Name + ".", plane.Name + "."]],
)
joint.Distance = 0.0
result = self.assembly.solve()
self.assertEqual(
result, 0, "Solve should succeed for vertex→datum Distance joints"
)
# The box's Vertex1 (at local 0,0,0) should be at the origin.
v = box.Placement.Base
self.assertAlmostEqual(v.x, 0.0, places=2, msg="X should be pinned to 0")
self.assertAlmostEqual(v.y, 0.0, places=2, msg="Y should be pinned to 0")
self.assertAlmostEqual(v.z, 0.0, places=2, msg="Z should be pinned to 0")
def test_distance_vertex_to_datum_plane_preserves_orientation(self):
"""Distance(0) vertex→datum should not constrain orientation."""
origin = self._get_origin()
xy = origin.getXY()
xz = origin.getXZ()
yz = origin.getYZ()
# Start box with a known rotation (45° about Z).
rot = App.Rotation(App.Vector(0, 0, 1), 45)
box = self._make_box(50, 50, 50)
box.Placement = App.Placement(App.Vector(50, 50, 50), rot)
for plane in [xy, xz, yz]:
joint = self._make_joint(
5,
[box, ["Vertex1", "Vertex1"]],
[origin, [plane.Name + ".", plane.Name + "."]],
)
joint.Distance = 0.0
self.assembly.solve()
# 3 PointInPlane constraints pin position (3 DOF) but leave
# orientation free (3 DOF). The solver should keep the original
# orientation since it's the lowest-energy solution from the
# initial placement.
dof = self.assembly.getLastDoF()
self.assertEqual(
dof, 3, "3 PointInPlane constraints should leave 3 DOF (orientation)"
)
def test_distance_face_to_datum_plane_solves(self):
"""Distance(0) joint: face → datum plane solves (PlanePlane/Planar)."""
origin = self._get_origin()
xy = origin.getXY()
box = self._make_box(0, 0, 50)
# Face1 is the -Z face of a Part::Box.
joint = self._make_joint(
5,
[box, ["Face1", "Vertex1"]],
[origin, [xy.Name + ".", xy.Name + "."]],
)
joint.Distance = 0.0
result = self.assembly.solve()
self.assertEqual(
result, 0, "Solve should succeed for face→datum Distance joint"
)
# ── Round-trip test ──────────────────────────────────────────────
def test_save_load_preserves_labels(self):