All checks were successful
Build and Test / build (pull_request) Successful in 30m13s
The datum plane detection in getDistanceType() only checked for App::Plane (origin planes). This missed two other class hierarchies: - PartDesign::Plane (inherits Part::Datum, NOT App::Plane) - Part::Plane primitive referenced bare (no Face element) Both produce empty element types (sub-name ends with ".") but failed the isDerivedFrom<App::Plane>() check, falling through to DistanceType::Other and the Planar fallback. This caused incorrect constraint geometry, leading to conflicting/unsatisfiable constraints and solver failures. Add shape-based isDatumPlane/Line/Point helpers that cover all three hierarchies by inspecting the actual OCCT geometry rather than relying on class identity alone. Extend getDistanceType() to use these helpers for all datum-vs-datum and datum-vs-element combinations. Adds TestDatumClassification.py with tests for PartDesign::Plane, Part::Plane (bare ref), and cross-hierarchy datum combinations.
267 lines
10 KiB
Python
267 lines
10 KiB
Python
# SPDX-License-Identifier: LGPL-2.1-or-later
|
|
# /****************************************************************************
|
|
# *
|
|
# Copyright (c) 2025 Kindred Systems <development@kindred-systems.com> *
|
|
# *
|
|
# This file is part of FreeCAD. *
|
|
# *
|
|
# FreeCAD is free software: you can redistribute it and/or modify it *
|
|
# under the terms of the GNU Lesser General Public License as *
|
|
# published by the Free Software Foundation, either version 2.1 of the *
|
|
# License, or (at your option) any later version. *
|
|
# *
|
|
# FreeCAD is distributed in the hope that it will be useful, but *
|
|
# WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
|
# Lesser General Public License for more details. *
|
|
# *
|
|
# You should have received a copy of the GNU Lesser General Public *
|
|
# License along with FreeCAD. If not, see *
|
|
# <https://www.gnu.org/licenses/>. *
|
|
# *
|
|
# ***************************************************************************/
|
|
|
|
"""
|
|
Tests for datum plane classification in Distance joints.
|
|
|
|
Verifies that getDistanceType correctly classifies joints involving datum
|
|
planes from all three class hierarchies:
|
|
1. App::Plane — origin planes (XY, XZ, YZ)
|
|
2. PartDesign::Plane — datum planes inside a PartDesign body
|
|
3. Part::Plane — Part workbench plane primitives (bare reference)
|
|
"""
|
|
|
|
import unittest
|
|
|
|
import FreeCAD as App
|
|
import JointObject
|
|
|
|
|
|
class TestDatumClassification(unittest.TestCase):
|
|
"""Tests that Distance joints with datum plane references are
|
|
classified as PlanePlane (not Other) regardless of the datum
|
|
object's class hierarchy."""
|
|
|
|
def setUp(self):
|
|
doc_name = self.__class__.__name__
|
|
if App.ActiveDocument:
|
|
if App.ActiveDocument.Name != doc_name:
|
|
App.newDocument(doc_name)
|
|
else:
|
|
App.newDocument(doc_name)
|
|
App.setActiveDocument(doc_name)
|
|
self.doc = App.ActiveDocument
|
|
|
|
self.assembly = self.doc.addObject("Assembly::AssemblyObject", "Assembly")
|
|
self.jointgroup = self.assembly.newObject("Assembly::JointGroup", "Joints")
|
|
|
|
def tearDown(self):
|
|
App.closeDocument(self.doc.Name)
|
|
|
|
# ── Helpers ─────────────────────────────────────────────────────
|
|
|
|
def _make_box(self, x=0, y=0, z=0, size=10):
|
|
box = self.assembly.newObject("Part::Box", "Box")
|
|
box.Length = size
|
|
box.Width = size
|
|
box.Height = size
|
|
box.Placement = App.Placement(App.Vector(x, y, z), App.Rotation())
|
|
return box
|
|
|
|
def _make_joint(self, joint_type, ref1, ref2):
|
|
joint = self.jointgroup.newObject("App::FeaturePython", "Joint")
|
|
JointObject.Joint(joint, joint_type)
|
|
refs = [
|
|
[ref1[0], ref1[1]],
|
|
[ref2[0], ref2[1]],
|
|
]
|
|
joint.Proxy.setJointConnectors(joint, refs)
|
|
return joint
|
|
|
|
def _make_pd_body_with_datum_plane(self, name="Body"):
|
|
"""Create a PartDesign::Body with a datum plane inside the assembly."""
|
|
body = self.assembly.newObject("PartDesign::Body", name)
|
|
datum = body.newObject("PartDesign::Plane", "DatumPlane")
|
|
self.doc.recompute()
|
|
return body, datum
|
|
|
|
def _make_part_plane(self, name="PartPlane"):
|
|
"""Create a Part::Plane primitive inside the assembly."""
|
|
plane = self.assembly.newObject("Part::Plane", name)
|
|
plane.Length = 10
|
|
plane.Width = 10
|
|
self.doc.recompute()
|
|
return plane
|
|
|
|
# ── Origin plane tests (App::Plane — existing behaviour) ───────
|
|
|
|
def test_origin_plane_face_classified_as_plane_plane(self):
|
|
"""Distance joint: box Face → origin datum plane → PlanePlane."""
|
|
origin = self.assembly.Origin
|
|
xy = origin.getXY()
|
|
box = self._make_box(0, 0, 50)
|
|
|
|
joint = self._make_joint(
|
|
5, # Distance
|
|
[box, ["Face1", "Vertex1"]],
|
|
[origin, [xy.Name + ".", xy.Name + "."]],
|
|
)
|
|
joint.Distance = 0.0
|
|
|
|
result = self.assembly.solve()
|
|
self.assertEqual(
|
|
result,
|
|
0,
|
|
"Distance joint with origin plane should solve (not produce Other)",
|
|
)
|
|
|
|
# ── PartDesign::Plane tests ────────────────────────────────────
|
|
|
|
def test_pd_datum_plane_face_classified_as_plane_plane(self):
|
|
"""Distance joint: box Face → PartDesign::Plane → PlanePlane."""
|
|
body, datum = self._make_pd_body_with_datum_plane()
|
|
box = self._make_box(0, 0, 50)
|
|
|
|
# Ground the body so the solver has a fixed reference.
|
|
gnd = self.jointgroup.newObject("App::FeaturePython", "GroundedJoint")
|
|
JointObject.GroundedJoint(gnd, body)
|
|
|
|
# Reference the datum plane with a bare sub-name (ends with ".").
|
|
joint = self._make_joint(
|
|
5, # Distance
|
|
[box, ["Face1", "Vertex1"]],
|
|
[body, [datum.Name + ".", datum.Name + "."]],
|
|
)
|
|
joint.Distance = 0.0
|
|
|
|
result = self.assembly.solve()
|
|
self.assertNotEqual(
|
|
result,
|
|
-1,
|
|
"Distance joint with PartDesign::Plane should not fail to solve "
|
|
"(DistanceType should be PlanePlane, not Other)",
|
|
)
|
|
|
|
def test_pd_datum_plane_vertex_classified_as_point_plane(self):
|
|
"""Distance joint: box Vertex → PartDesign::Plane → PointPlane."""
|
|
body, datum = self._make_pd_body_with_datum_plane()
|
|
box = self._make_box(0, 0, 50)
|
|
|
|
gnd = self.jointgroup.newObject("App::FeaturePython", "GroundedJoint")
|
|
JointObject.GroundedJoint(gnd, body)
|
|
|
|
joint = self._make_joint(
|
|
5, # Distance
|
|
[box, ["Vertex1", "Vertex1"]],
|
|
[body, [datum.Name + ".", datum.Name + "."]],
|
|
)
|
|
joint.Distance = 0.0
|
|
|
|
result = self.assembly.solve()
|
|
self.assertNotEqual(
|
|
result,
|
|
-1,
|
|
"Distance joint vertex → PartDesign::Plane should not fail "
|
|
"(DistanceType should be PointPlane, not Other)",
|
|
)
|
|
|
|
def test_two_pd_datum_planes_classified_as_plane_plane(self):
|
|
"""Distance joint: PartDesign::Plane → PartDesign::Plane → PlanePlane."""
|
|
body1, datum1 = self._make_pd_body_with_datum_plane("Body1")
|
|
body2, datum2 = self._make_pd_body_with_datum_plane("Body2")
|
|
|
|
gnd = self.jointgroup.newObject("App::FeaturePython", "GroundedJoint")
|
|
JointObject.GroundedJoint(gnd, body1)
|
|
|
|
joint = self._make_joint(
|
|
5, # Distance
|
|
[body1, [datum1.Name + ".", datum1.Name + "."]],
|
|
[body2, [datum2.Name + ".", datum2.Name + "."]],
|
|
)
|
|
joint.Distance = 0.0
|
|
|
|
result = self.assembly.solve()
|
|
self.assertNotEqual(
|
|
result,
|
|
-1,
|
|
"Distance joint PartDesign::Plane → PartDesign::Plane should not fail "
|
|
"(DistanceType should be PlanePlane, not Other)",
|
|
)
|
|
|
|
# ── Part::Plane tests (primitive, bare reference) ──────────────
|
|
|
|
def test_part_plane_bare_ref_face_classified_as_plane_plane(self):
|
|
"""Distance joint: box Face → Part::Plane (bare ref) → PlanePlane."""
|
|
plane = self._make_part_plane()
|
|
box = self._make_box(0, 0, 50)
|
|
|
|
gnd = self.jointgroup.newObject("App::FeaturePython", "GroundedJoint")
|
|
JointObject.GroundedJoint(gnd, plane)
|
|
|
|
# Bare reference to Part::Plane (sub-name ends with ".").
|
|
joint = self._make_joint(
|
|
5, # Distance
|
|
[box, ["Face1", "Vertex1"]],
|
|
[plane, [".", "."]],
|
|
)
|
|
joint.Distance = 0.0
|
|
|
|
result = self.assembly.solve()
|
|
self.assertNotEqual(
|
|
result,
|
|
-1,
|
|
"Distance joint with Part::Plane (bare ref) should not fail "
|
|
"(DistanceType should be PlanePlane, not Other)",
|
|
)
|
|
|
|
def test_part_plane_with_face1_classified_as_plane_plane(self):
|
|
"""Distance joint: box Face → Part::Plane Face1 → PlanePlane.
|
|
|
|
When Part::Plane is referenced with an explicit Face1 element,
|
|
it should enter the normal Face+Face classification path."""
|
|
plane = self._make_part_plane()
|
|
box = self._make_box(0, 0, 50)
|
|
|
|
gnd = self.jointgroup.newObject("App::FeaturePython", "GroundedJoint")
|
|
JointObject.GroundedJoint(gnd, plane)
|
|
|
|
joint = self._make_joint(
|
|
5, # Distance
|
|
[box, ["Face1", "Vertex1"]],
|
|
[plane, ["Face1", "Vertex1"]],
|
|
)
|
|
joint.Distance = 0.0
|
|
|
|
result = self.assembly.solve()
|
|
self.assertNotEqual(
|
|
result,
|
|
-1,
|
|
"Distance joint with Part::Plane Face1 should solve normally",
|
|
)
|
|
|
|
# ── Cross-hierarchy tests ──────────────────────────────────────
|
|
|
|
def test_origin_plane_and_pd_datum_classified_as_plane_plane(self):
|
|
"""Distance joint: origin App::Plane → PartDesign::Plane → PlanePlane."""
|
|
origin = self.assembly.Origin
|
|
xy = origin.getXY()
|
|
body, datum = self._make_pd_body_with_datum_plane()
|
|
|
|
gnd = self.jointgroup.newObject("App::FeaturePython", "GroundedJoint")
|
|
JointObject.GroundedJoint(gnd, body)
|
|
|
|
joint = self._make_joint(
|
|
5, # Distance
|
|
[origin, [xy.Name + ".", xy.Name + "."]],
|
|
[body, [datum.Name + ".", datum.Name + "."]],
|
|
)
|
|
joint.Distance = 0.0
|
|
|
|
result = self.assembly.solve()
|
|
self.assertNotEqual(
|
|
result,
|
|
-1,
|
|
"Distance joint origin plane → PartDesign::Plane should not fail "
|
|
"(DistanceType should be PlanePlane, not Other)",
|
|
)
|