# SPDX-License-Identifier: LGPL-2.1-or-later # /**************************************************************************** # * # Copyright (c) 2025 Kindred Systems * # * # 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 * # . * # * # ***************************************************************************/ """ 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)", )