fix(assembly): classify datum plane references in Distance joints
Some checks failed
Build and Test / build (pull_request) Has been cancelled
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:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user