fix(assembly): classify datum planes from all class hierarchies in Distance joints
All checks were successful
Build and Test / build (pull_request) Successful in 30m13s
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.
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
|
||||
#include <BRepAdaptor_Curve.hxx>
|
||||
#include <BRepAdaptor_Surface.hxx>
|
||||
#include <TopExp_Explorer.hxx>
|
||||
#include <TopoDS.hxx>
|
||||
#include <TopoDS_Face.hxx>
|
||||
#include <gp_Circ.hxx>
|
||||
@@ -197,6 +198,120 @@ double getEdgeRadius(const App::DocumentObject* obj, const std::string& elt)
|
||||
return sf.GetType() == GeomAbs_Circle ? sf.Circle().Radius() : 0.0;
|
||||
}
|
||||
|
||||
/// Determine whether \a obj represents a planar datum when referenced with an
|
||||
/// empty element type (bare sub-name ending with ".").
|
||||
///
|
||||
/// Covers three independent class hierarchies:
|
||||
/// 1. App::Plane (origin planes, Part::DatumPlane)
|
||||
/// 2. Part::Datum (PartDesign::Plane — not derived from App::Plane)
|
||||
/// 3. Any Part::Feature whose whole-object shape is a single planar face
|
||||
/// (e.g. Part::Plane primitive referenced without an element)
|
||||
static bool isDatumPlane(const App::DocumentObject* obj)
|
||||
{
|
||||
if (!obj) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Origin planes and Part::DatumPlane (both inherit App::Plane).
|
||||
if (obj->isDerivedFrom<App::Plane>()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// PartDesign datum objects inherit Part::Datum but NOT App::Plane.
|
||||
// Part::Datum is also the base for PartDesign::Line and PartDesign::Point,
|
||||
// so inspect the shape to confirm it is actually planar.
|
||||
if (obj->isDerivedFrom<PartApp::Datum>()) {
|
||||
auto* feat = static_cast<const PartApp::Feature*>(obj);
|
||||
const auto& shape = feat->Shape.getShape().getShape();
|
||||
if (shape.IsNull()) {
|
||||
return false;
|
||||
}
|
||||
TopExp_Explorer ex(shape, TopAbs_FACE);
|
||||
if (ex.More()) {
|
||||
BRepAdaptor_Surface sf(TopoDS::Face(ex.Current()));
|
||||
return sf.GetType() == GeomAbs_Plane;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fallback for any Part::Feature (e.g. Part::Plane primitive) referenced
|
||||
// bare — if its shape is a single planar face, treat it as a datum plane.
|
||||
if (auto* feat = dynamic_cast<const PartApp::Feature*>(obj)) {
|
||||
const auto& shape = feat->Shape.getShape().getShape();
|
||||
if (shape.IsNull()) {
|
||||
return false;
|
||||
}
|
||||
TopExp_Explorer ex(shape, TopAbs_FACE);
|
||||
if (!ex.More()) {
|
||||
return false;
|
||||
}
|
||||
BRepAdaptor_Surface sf(TopoDS::Face(ex.Current()));
|
||||
if (sf.GetType() != GeomAbs_Plane) {
|
||||
return false;
|
||||
}
|
||||
ex.Next();
|
||||
// Only treat as datum if there is exactly one face — a multi-face
|
||||
// solid referenced bare is ambiguous and should not be classified.
|
||||
return !ex.More();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Same idea for datum lines (App::Line, PartDesign::Line, etc.).
|
||||
static bool isDatumLine(const App::DocumentObject* obj)
|
||||
{
|
||||
if (!obj) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (obj->isDerivedFrom<App::Line>()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (obj->isDerivedFrom<PartApp::Datum>()) {
|
||||
auto* feat = static_cast<const PartApp::Feature*>(obj);
|
||||
const auto& shape = feat->Shape.getShape().getShape();
|
||||
if (shape.IsNull()) {
|
||||
return false;
|
||||
}
|
||||
TopExp_Explorer ex(shape, TopAbs_EDGE);
|
||||
if (ex.More()) {
|
||||
BRepAdaptor_Curve cv(TopoDS::Edge(ex.Current()));
|
||||
return cv.GetType() == GeomAbs_Line;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Same idea for datum points (App::Point, PartDesign::Point, etc.).
|
||||
static bool isDatumPoint(const App::DocumentObject* obj)
|
||||
{
|
||||
if (!obj) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (obj->isDerivedFrom<App::Point>()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (obj->isDerivedFrom<PartApp::Datum>()) {
|
||||
auto* feat = static_cast<const PartApp::Feature*>(obj);
|
||||
const auto& shape = feat->Shape.getShape().getShape();
|
||||
if (shape.IsNull()) {
|
||||
return false;
|
||||
}
|
||||
// A datum point has a vertex but no edges or faces.
|
||||
TopExp_Explorer exE(shape, TopAbs_EDGE);
|
||||
TopExp_Explorer exV(shape, TopAbs_VERTEX);
|
||||
return !exE.More() && exV.More();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
DistanceType getDistanceType(App::DocumentObject* joint)
|
||||
{
|
||||
if (!joint) {
|
||||
@@ -210,54 +325,170 @@ DistanceType getDistanceType(App::DocumentObject* joint)
|
||||
auto* obj1 = getLinkedObjFromRef(joint, "Reference1");
|
||||
auto* obj2 = getLinkedObjFromRef(joint, "Reference2");
|
||||
|
||||
// Datum planes (App::Plane) 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.
|
||||
const bool datum1 = type1.empty() && obj1 && obj1->isDerivedFrom<App::Plane>();
|
||||
const bool datum2 = type2.empty() && obj2 && obj2->isDerivedFrom<App::Plane>();
|
||||
// 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.
|
||||
//
|
||||
// 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);
|
||||
const bool datum1 = datumPlane1 || datumLine1 || datumPoint1;
|
||||
const bool datum2 = datumPlane2 || datumLine2 || datumPoint2;
|
||||
|
||||
if (datum1 || datum2) {
|
||||
if (datum1 && datum2) {
|
||||
// Map each datum side to a synthetic element type so the same
|
||||
// classification logic applies regardless of which hierarchy
|
||||
// the object comes from.
|
||||
auto syntheticType = [](bool isPlane, bool isLine, bool isPoint,
|
||||
const std::string& elemType) -> std::string {
|
||||
if (isPlane) return "Face";
|
||||
if (isLine) return "Edge";
|
||||
if (isPoint) return "Vertex";
|
||||
return elemType; // non-datum side keeps its real type
|
||||
};
|
||||
|
||||
const std::string syn1 = syntheticType(datumPlane1, datumLine1, datumPoint1, type1);
|
||||
const std::string syn2 = syntheticType(datumPlane2, datumLine2, datumPoint2, type2);
|
||||
|
||||
// Both sides are datum planes.
|
||||
if (datumPlane1 && datumPlane2) {
|
||||
FC_LOG("Assembly : getDistanceType('" << joint->getFullName()
|
||||
<< "') — datum+datum → PlanePlane");
|
||||
return DistanceType::PlanePlane;
|
||||
}
|
||||
|
||||
// One side is a datum plane, the other has a real element type.
|
||||
// One side is a datum plane, the other has a real element type
|
||||
// (or is another datum kind).
|
||||
// For PointPlane/LinePlane, the solver's PointInPlaneConstraint
|
||||
// reads the plane normal from marker_j (Reference2). Unlike
|
||||
// real Face+Vertex joints (where both Placements carry the
|
||||
// face normal from findPlacement), datum planes only carry
|
||||
// their normal through computeMarkerTransform. So the datum
|
||||
// must end up on Reference2 for the normal to reach marker_j.
|
||||
// plane must end up on Reference2 for the normal to reach marker_j.
|
||||
//
|
||||
// For PlanePlane the convention matches the existing Face+Face
|
||||
// path (plane on Reference1).
|
||||
const auto& otherType = datum1 ? type2 : type1;
|
||||
if (datumPlane1 || datumPlane2) {
|
||||
const auto& otherSyn = datumPlane1 ? syn2 : syn1;
|
||||
|
||||
if (otherType == "Vertex" || otherType == "Edge") {
|
||||
// Datum must be on Reference2 (j side).
|
||||
if (datum1) {
|
||||
swapJCS(joint); // move datum from Ref1 → Ref2
|
||||
if (otherSyn == "Vertex" || otherSyn == "Edge") {
|
||||
// Datum plane must be on Reference2 (j side).
|
||||
if (datumPlane1) {
|
||||
swapJCS(joint); // move datum from Ref1 → Ref2
|
||||
}
|
||||
DistanceType result = (otherSyn == "Vertex")
|
||||
? DistanceType::PointPlane : DistanceType::LinePlane;
|
||||
FC_LOG("Assembly : getDistanceType('" << joint->getFullName()
|
||||
<< "') — datum+" << otherSyn << " → "
|
||||
<< distanceTypeName(result)
|
||||
<< (datumPlane1 ? " (swapped)" : ""));
|
||||
return result;
|
||||
}
|
||||
DistanceType result = (otherType == "Vertex")
|
||||
? DistanceType::PointPlane : DistanceType::LinePlane;
|
||||
|
||||
// Face + datum plane or datum plane + datum plane → PlanePlane.
|
||||
// No swap needed: PlanarConstraint is symmetric (uses both
|
||||
// z_i and z_j), and preserving the original Reference order
|
||||
// keeps the initial Placement values consistent so the solver
|
||||
// stays in the correct orientation branch.
|
||||
FC_LOG("Assembly : getDistanceType('" << joint->getFullName()
|
||||
<< "') — datum+" << otherType << " → "
|
||||
<< distanceTypeName(result)
|
||||
<< (datum1 ? " (swapped)" : ""));
|
||||
return result;
|
||||
<< "') — datum+" << otherSyn << " → PlanePlane");
|
||||
return DistanceType::PlanePlane;
|
||||
}
|
||||
|
||||
// Face + datum or unknown + datum → PlanePlane.
|
||||
// No swap needed: PlanarConstraint is symmetric (uses both
|
||||
// z_i and z_j), and preserving the original Reference order
|
||||
// keeps the initial Placement values consistent so the solver
|
||||
// stays in the correct orientation branch.
|
||||
FC_LOG("Assembly : getDistanceType('" << joint->getFullName()
|
||||
<< "') — datum+" << otherType << " → PlanePlane");
|
||||
return DistanceType::PlanePlane;
|
||||
// Datum line or datum point paired with a real element type.
|
||||
// Map to the appropriate pair using synthetic types and fall
|
||||
// through to the main geometry chain below. The synthetic
|
||||
// types ("Edge", "Vertex") will match the existing if-else
|
||||
// branches — but those branches call isEdgeType/isFaceType on
|
||||
// the object, which requires a real sub-element name. For
|
||||
// datum lines/points the element is empty, so we classify
|
||||
// directly here.
|
||||
if (datumLine1 && datumLine2) {
|
||||
FC_LOG("Assembly : getDistanceType('" << joint->getFullName()
|
||||
<< "') — datumLine+datumLine → LineLine");
|
||||
return DistanceType::LineLine;
|
||||
}
|
||||
if (datumPoint1 && datumPoint2) {
|
||||
FC_LOG("Assembly : getDistanceType('" << joint->getFullName()
|
||||
<< "') — datumPoint+datumPoint → PointPoint");
|
||||
return DistanceType::PointPoint;
|
||||
}
|
||||
if ((datumLine1 && datumPoint2) || (datumPoint1 && datumLine2)) {
|
||||
if (datumPoint1) {
|
||||
swapJCS(joint); // line first
|
||||
}
|
||||
FC_LOG("Assembly : getDistanceType('" << joint->getFullName()
|
||||
<< "') — datumLine+datumPoint → PointLine");
|
||||
return DistanceType::PointLine;
|
||||
}
|
||||
|
||||
// One datum line/point + one real element type.
|
||||
if (datumLine1 || datumLine2) {
|
||||
const auto& otherSyn = datumLine1 ? syn2 : syn1;
|
||||
if (otherSyn == "Face") {
|
||||
// Line + Face — need line on Reference2 (edge side).
|
||||
if (datumLine1) {
|
||||
swapJCS(joint);
|
||||
}
|
||||
// We don't know the face type without inspecting the shape,
|
||||
// but LinePlane is the most common and safest classification.
|
||||
FC_LOG("Assembly : getDistanceType('" << joint->getFullName()
|
||||
<< "') — datumLine+Face → LinePlane");
|
||||
return DistanceType::LinePlane;
|
||||
}
|
||||
if (otherSyn == "Vertex") {
|
||||
if (datumLine2) {
|
||||
swapJCS(joint); // line first
|
||||
}
|
||||
FC_LOG("Assembly : getDistanceType('" << joint->getFullName()
|
||||
<< "') — datumLine+Vertex → PointLine");
|
||||
return DistanceType::PointLine;
|
||||
}
|
||||
if (otherSyn == "Edge") {
|
||||
FC_LOG("Assembly : getDistanceType('" << joint->getFullName()
|
||||
<< "') — datumLine+Edge → LineLine");
|
||||
return DistanceType::LineLine;
|
||||
}
|
||||
}
|
||||
if (datumPoint1 || datumPoint2) {
|
||||
const auto& otherSyn = datumPoint1 ? syn2 : syn1;
|
||||
if (otherSyn == "Face") {
|
||||
// Point + Face — face first, point second.
|
||||
if (!datumPoint2) {
|
||||
swapJCS(joint); // put face on Ref1
|
||||
}
|
||||
FC_LOG("Assembly : getDistanceType('" << joint->getFullName()
|
||||
<< "') — datumPoint+Face → PointPlane");
|
||||
return DistanceType::PointPlane;
|
||||
}
|
||||
if (otherSyn == "Edge") {
|
||||
// Edge first, point second.
|
||||
if (datumPoint1) {
|
||||
swapJCS(joint);
|
||||
}
|
||||
FC_LOG("Assembly : getDistanceType('" << joint->getFullName()
|
||||
<< "') — datumPoint+Edge → PointLine");
|
||||
return DistanceType::PointLine;
|
||||
}
|
||||
if (otherSyn == "Vertex") {
|
||||
FC_LOG("Assembly : getDistanceType('" << joint->getFullName()
|
||||
<< "') — datumPoint+Vertex → PointPoint");
|
||||
return DistanceType::PointPoint;
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, it's an unrecognized datum combination.
|
||||
FC_WARN("Assembly : getDistanceType('" << joint->getFullName()
|
||||
<< "') — unrecognized datum combination (syn1="
|
||||
<< syn1 << ", syn2=" << syn2 << ")");
|
||||
}
|
||||
|
||||
if (type1 == "Vertex" && type2 == "Vertex") {
|
||||
|
||||
Reference in New Issue
Block a user