From 14ee8c673f1f14587e14fb442acb1c44f21435a0 Mon Sep 17 00:00:00 2001 From: forbes Date: Sun, 22 Feb 2026 18:55:39 -0600 Subject: [PATCH] fix(assembly): classify datum planes from all class hierarchies in Distance joints 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() 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. --- src/Mod/Assembly/App/AssemblyUtils.cpp | 27 ++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/Mod/Assembly/App/AssemblyUtils.cpp b/src/Mod/Assembly/App/AssemblyUtils.cpp index f4efe539eb..21e70455f2 100644 --- a/src/Mod/Assembly/App/AssemblyUtils.cpp +++ b/src/Mod/Assembly/App/AssemblyUtils.cpp @@ -325,21 +325,28 @@ DistanceType getDistanceType(App::DocumentObject* joint) auto* obj1 = getLinkedObjFromRef(joint, "Reference1"); auto* obj2 = getLinkedObjFromRef(joint, "Reference2"); - // Datum objects have empty element types because their sub-name ends - // with "." and yields no Face/Edge/Vertex element. Detect them here - // and classify before the main geometry chain, which cannot handle - // the empty element type. + // Datum objects referenced bare have empty element types (sub-name + // ends with "."). PartDesign datums referenced through a body can + // also produce non-standard element types like "Plane" (from a + // sub-name such as "Body.DatumPlane.Plane" — Part::Datum::getSubObject + // ignores the trailing element, but splitSubName still extracts it). + // + // Detect these before the main geometry chain, which only handles + // the standard Face/Edge/Vertex element types. // // isDatumPlane/Line/Point cover all three independent hierarchies: // - App::Plane / App::Line / App::Point (origin datums) // - Part::Datum subclasses (PartDesign datums) // - Part::Feature with single-face shape (Part::Plane primitive, bare ref) - const bool datumPlane1 = type1.empty() && isDatumPlane(obj1); - const bool datumPlane2 = type2.empty() && isDatumPlane(obj2); - const bool datumLine1 = type1.empty() && !datumPlane1 && isDatumLine(obj1); - const bool datumLine2 = type2.empty() && !datumPlane2 && isDatumLine(obj2); - const bool datumPoint1 = type1.empty() && !datumPlane1 && !datumLine1 && isDatumPoint(obj1); - const bool datumPoint2 = type2.empty() && !datumPlane2 && !datumLine2 && isDatumPoint(obj2); + auto isNonGeomElement = [](const std::string& t) { + return t != "Face" && t != "Edge" && t != "Vertex"; + }; + const bool datumPlane1 = isNonGeomElement(type1) && isDatumPlane(obj1); + const bool datumPlane2 = isNonGeomElement(type2) && isDatumPlane(obj2); + const bool datumLine1 = isNonGeomElement(type1) && !datumPlane1 && isDatumLine(obj1); + const bool datumLine2 = isNonGeomElement(type2) && !datumPlane2 && isDatumLine(obj2); + const bool datumPoint1 = isNonGeomElement(type1) && !datumPlane1 && !datumLine1 && isDatumPoint(obj1); + const bool datumPoint2 = isNonGeomElement(type2) && !datumPlane2 && !datumLine2 && isDatumPoint(obj2); const bool datum1 = datumPlane1 || datumLine1 || datumPoint1; const bool datum2 = datumPlane2 || datumLine2 || datumPoint2;