From eb25021f39884ed183f760f82d3689984d7a68d3 Mon Sep 17 00:00:00 2001 From: PaddleStroke Date: Wed, 10 Dec 2025 22:47:20 +0100 Subject: [PATCH] Core: Add getPlacementOf replacing previous getGlobalPlacement logic. (#26059) * Core: Add getPlacementOf replacing previous getGlobalPlacement logic. * Update src/App/DocumentObject.cpp Co-authored-by: Kacper Donat * Update DocumentObject.cpp * Fix error when called from python with targetObj == None --------- Co-authored-by: Kacper Donat --- src/App/DocumentObject.cpp | 29 ++++++++ src/App/DocumentObject.h | 6 ++ src/App/DocumentObject.pyi | 7 ++ src/App/DocumentObjectPyImp.cpp | 30 ++++++++ src/App/FeaturePython.cpp | 98 ++++++++++++++++++++++++ src/App/FeaturePython.h | 45 ++++++++++- src/App/GeoFeature.cpp | 35 +-------- src/App/Link.cpp | 99 +++++++++++++++++++++++++ src/App/Link.h | 4 + src/Mod/Assembly/UtilsAssembly.py | 2 +- src/Mod/Draft/draftobjects/draftlink.py | 48 ++++++++++++ 11 files changed, 368 insertions(+), 35 deletions(-) diff --git a/src/App/DocumentObject.cpp b/src/App/DocumentObject.cpp index 91484d343c..a4f93cf8f9 100644 --- a/src/App/DocumentObject.cpp +++ b/src/App/DocumentObject.cpp @@ -30,6 +30,7 @@ #include #include +#include #include #include @@ -1615,3 +1616,31 @@ void DocumentObject::onPropertyStatusChanged(const Property& prop, unsigned long getDocument()->signalChangePropertyEditor(*getDocument(), prop); } } + +Base::Placement DocumentObject::getPlacementOf(const std::string& sub, DocumentObject* targetObj) +{ + Base::Placement plc; + auto* propPlacement = freecad_cast(getPropertyByName("Placement")); + if (propPlacement) { + // If the object has no placement (like a Group), plc stays identity so we can proceed. + plc = propPlacement->getValue(); + } + + std::vector names = Base::Tools::splitSubName(sub); + + if (names.empty() || this == targetObj) { + return plc; + } + + DocumentObject* subObj = getDocument()->getObject(names.front().c_str()); + + if (!subObj) { + return plc; + } + + std::vector newNames(names.begin() + 1, names.end()); + std::string newSub = Base::Tools::joinList(newNames, "."); + + return plc * subObj->getPlacementOf(newSub, targetObj); +} + diff --git a/src/App/DocumentObject.h b/src/App/DocumentObject.h index e3da15591e..ad3cd2e985 100644 --- a/src/App/DocumentObject.h +++ b/src/App/DocumentObject.h @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -708,6 +709,11 @@ public: /// Check if the subname reference ends with hidden marker. static const char* hasHiddenMarker(const char* subname); + /* Find the placement of a target object as seen from this. + If no targetObj given, the last object found in the subname is used as target. + */ + virtual Base::Placement getPlacementOf(const std::string& sub, DocumentObject* targetObj = nullptr); + protected: /// recompute only this object virtual App::DocumentObjectExecReturn* recompute(); diff --git a/src/App/DocumentObject.pyi b/src/App/DocumentObject.pyi index e2d55d92b3..049c04e811 100644 --- a/src/App/DocumentObject.pyi +++ b/src/App/DocumentObject.pyi @@ -325,3 +325,10 @@ class DocumentObject(ExtensionContainer): Return true if the object is part of a document, false otherwise. """ ... + + def getPlacementOf(self, subname: str, target: DocumentObject = None, /) -> Any: + """ + Return the placement of the sub-object relative to the link object. + getPlacementOf(subname, [targetObj]) -> Base.Placement + """ + ... \ No newline at end of file diff --git a/src/App/DocumentObjectPyImp.cpp b/src/App/DocumentObjectPyImp.cpp index d924b12740..8e35c0dc9e 100644 --- a/src/App/DocumentObjectPyImp.cpp +++ b/src/App/DocumentObjectPyImp.cpp @@ -23,6 +23,7 @@ #include #include +#include #include #include "DocumentObject.h" @@ -1003,3 +1004,32 @@ void DocumentObjectPy::setNoTouch(Py::Boolean value) { getDocumentObjectPtr()->setStatus(ObjectStatus::NoTouch, value.isTrue()); } + +PyObject* DocumentObjectPy::getPlacementOf(PyObject* args) +{ + char* subname; + PyObject* target = Py_None; // Initialize to None + + if (!PyArg_ParseTuple(args, "s|O", &subname, &target)) { + return nullptr; + } + + App::DocumentObject* targetObj = nullptr; + + // Check if a target was provided and is not None + if (target && target != Py_None) { + // Now perform the type check manually + if (!PyObject_TypeCheck(target, &DocumentObjectPy::Type)) { + PyErr_SetString(PyExc_TypeError, "Target argument must be a DocumentObject or None"); + return nullptr; + } + targetObj = static_cast(target)->getDocumentObjectPtr(); + } + + PY_TRY + { + Base::Placement p = getDocumentObjectPtr()->getPlacementOf(subname, targetObj); + return new Base::PlacementPy(new Base::Placement(p)); + } + PY_CATCH +} diff --git a/src/App/FeaturePython.cpp b/src/App/FeaturePython.cpp index 9d501d3397..319863fd6f 100644 --- a/src/App/FeaturePython.cpp +++ b/src/App/FeaturePython.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include "FeaturePython.h" @@ -632,6 +633,103 @@ bool FeaturePythonImp::editProperty(const char* name) return false; } +FeaturePythonImp::ValueT FeaturePythonImp::isLink() const +{ + _FC_PY_CALL_CHECK(isLink, return (NotImplemented)); + Base::PyGILStateLocker lock; + try { + Py::Tuple args(1); + args.setItem(0, Py::Object(object->getPyObject(), true)); + Py::Boolean ok(Base::pyCall(py_isLink.ptr(), args.ptr())); + return ok ? Accepted : Rejected; + } + catch (Py::Exception&) { + if (PyErr_ExceptionMatches(PyExc_NotImplementedError)) { + PyErr_Clear(); + return NotImplemented; + } + + Base::PyException e; // extract the Python error text + e.reportException(); + return Rejected; + } +} + +FeaturePythonImp::ValueT FeaturePythonImp::isLinkGroup() const +{ + _FC_PY_CALL_CHECK(isLinkGroup, return (NotImplemented)); + Base::PyGILStateLocker lock; + try { + Py::Tuple args(1); + args.setItem(0, Py::Object(object->getPyObject(), true)); + Py::Boolean ok(Base::pyCall(py_isLinkGroup.ptr(), args.ptr())); + return ok ? Accepted : Rejected; + } + catch (Py::Exception&) { + if (PyErr_ExceptionMatches(PyExc_NotImplementedError)) { + PyErr_Clear(); + return NotImplemented; + } + + Base::PyException e; // extract the Python error text + e.reportException(); + return Rejected; + } +} + +bool FeaturePythonImp::getPlacementOf( + Base::Placement& ret, + const char* subname, + App::DocumentObject* target +) const +{ + FC_PY_CALL_CHECK(getPlacementOf); // Standard macro check + Base::PyGILStateLocker lock; + try { + Py::Tuple args(3); + + // Arg 0: The object itself + args.setItem(0, Py::Object(object->getPyObject(), true)); + + // Arg 1: The subname string + args.setItem(1, Py::String(subname ? subname : "")); + + // Arg 2: The target object (or None) + if (target) { + args.setItem(2, Py::Object(target->getPyObject(), true)); + } + else { + args.setItem(2, Py::None()); + } + + // Call the Python method + Py::Object res(Base::pyCall(py_getPlacementOf.ptr(), args.ptr())); + + // Check if Python returned None (implies "use base implementation") + if (res.isNone()) { + return false; + } + + // Check return type + if (!PyObject_TypeCheck(res.ptr(), &Base::PlacementPy::Type)) { + throw Py::TypeError("getPlacementOf expects a Base.Placement object return"); + } + + // Convert Python object back to C++ Placement + ret = *static_cast(res.ptr())->getPlacementPtr(); + return true; + } + catch (Py::Exception&) { + if (PyErr_ExceptionMatches(PyExc_NotImplementedError)) { + PyErr_Clear(); + return false; + } + Base::PyException e; + e.reportException(); + return false; + } +} + // --------------------------------------------------------- namespace App diff --git a/src/App/FeaturePython.h b/src/App/FeaturePython.h index 69b71321a5..8068d49857 100644 --- a/src/App/FeaturePython.h +++ b/src/App/FeaturePython.h @@ -91,6 +91,11 @@ public: bool editProperty(const char* propName); + ValueT isLink() const; + ValueT isLinkGroup() const; + + bool getPlacementOf(Base::Placement& ret, const char* subname, App::DocumentObject* target) const; + private: App::DocumentObject* object; bool has__object__ {false}; @@ -114,7 +119,10 @@ private: FC_PY_ELEMENT(hasChildElement) \ FC_PY_ELEMENT(isElementVisible) \ FC_PY_ELEMENT(setElementVisible) \ - FC_PY_ELEMENT(editProperty) + FC_PY_ELEMENT(editProperty) \ + FC_PY_ELEMENT(isLink) \ + FC_PY_ELEMENT(isLinkGroup) \ + FC_PY_ELEMENT(getPlacementOf) #define FC_PY_ELEMENT_DEFINE(_name) Py::Object py_##_name; @@ -349,6 +357,41 @@ public: } } + bool isLink() const override + { + switch (imp->isLink()) { + case FeaturePythonImp::Accepted: + return true; + case FeaturePythonImp::Rejected: + return false; + default: + return FeatureT::isLink(); + } + } + + bool isLinkGroup() const override + { + switch (imp->isLinkGroup()) { + case FeaturePythonImp::Accepted: + return true; + case FeaturePythonImp::Rejected: + return false; + default: + return FeatureT::isLinkGroup(); + } + } + + Base::Placement getPlacementOf(const std::string& sub, DocumentObject* targetObj = nullptr) override + { + Base::Placement ret; + // Try to call the python implementation first + if (imp->getPlacementOf(ret, sub.c_str(), targetObj)) { + return ret; + } + // Fallback to C++ implementation + return FeatureT::getPlacementOf(sub, targetObj); + } + PyObject* getPyObject() override { if (FeatureT::PythonObject.is(Py::_None())) { diff --git a/src/App/GeoFeature.cpp b/src/App/GeoFeature.cpp index 0f529a8570..faa2b9a7ee 100644 --- a/src/App/GeoFeature.cpp +++ b/src/App/GeoFeature.cpp @@ -316,42 +316,11 @@ Base::Placement GeoFeature::getGlobalPlacement(App::DocumentObject* targetObj, App::DocumentObject* rootObj, const std::string& sub) { - if (!targetObj || !rootObj) { + if (!rootObj) { return Base::Placement(); } - std::vector names = Base::Tools::splitSubName(sub); - App::Document* doc = rootObj->getDocument(); - Base::Placement plc = getPlacementFromProp(rootObj, "Placement"); - - if (targetObj == rootObj) { - return plc; - } - - if (rootObj->isLink()) { - // Update doc in case its an external link. - doc = rootObj->getLinkedObject()->getDocument(); - } - - for (auto& name : names) { - App::DocumentObject* obj = doc->getObject(name.c_str()); - if (!obj) { - return Base::Placement(); - } - - plc = plc * getPlacementFromProp(obj, "Placement"); - - if (obj == targetObj) { - return plc; - } - if (obj->isLink()) { - // Update doc in case its an external link. - doc = obj->getLinkedObject()->getDocument(); - } - } - - // If targetObj has not been found there's a problem - return Base::Placement(); + return rootObj->getPlacementOf(sub, targetObj); } Base::Placement GeoFeature::getGlobalPlacement(App::DocumentObject* targetObj, diff --git a/src/App/Link.cpp b/src/App/Link.cpp index 60e168688b..1fbaed54fe 100644 --- a/src/App/Link.cpp +++ b/src/App/Link.cpp @@ -2635,6 +2635,76 @@ bool Link::canLinkProperties() const return true; } +Base::Placement Link::getPlacementOf(const std::string& sub, DocumentObject* targetObj) +{ + if (isLinkGroup() && _getShowElementValue()) { + // In this case we have child document objects, the subname can be used. + // and in this case the Link itself acts as a normal DocumentObject + return DocumentObject::getPlacementOf(sub, targetObj); + } + + Base::Placement plc; + auto* propPlacement = dynamic_cast(getPropertyByName("Placement")); + if (!propPlacement) { + return plc; + } + plc = propPlacement->getValue(); + + std::vector names = Base::Tools::splitSubName(sub); + + if (names.empty() || this == targetObj || !getLinkedObject()) { + return plc; + } + + Document* doc = getLinkedObject()->getDocument(); + + if (isLinkGroup()) { + // !ShowElement, so there are no objects. + // The subname looks like : '1.pad.face3' or '1.face3' + // So names.front() is supposed to be an integer. If not something is wrong. + int i = -1; + try { + i = std::stoi(names.front()); + } + catch (...) { // Conversion failed (not an integer) + return plc; + } + + std::vector plcs = PlacementList.getValues(); + if (plcs.size() <= i) { + return plc; + } + plc = plc * plcs[i]; + + // now that we handled the non-existing LinkElement, we can remove this subname and continue. + if (names.size() < 2) { // Subname was just "1", we are done. + return plc; + } + + DocumentObject* subObj = doc->getObject(names[1].c_str()); + if (!subObj) { + return plc; + } + + std::vector newNames(names.begin() + 2, names.end()); + std::string newSub = Base::Tools::joinList(newNames, "."); + + return plc * subObj->getPlacementOf(newSub, targetObj); + } + + // case of a normal link + DocumentObject* subObj = doc->getObject(names.front().c_str()); + + if (!subObj) { + return plc; + } + + std::vector newNames(names.begin() + 1, names.end()); + std::string newSub = Base::Tools::joinList(newNames, "."); + + return plc * subObj->getPlacementOf(newSub, targetObj); +} + bool Link::isLink() const { return ElementCount.getValue() == 0; @@ -2701,6 +2771,35 @@ App::Link* LinkElement::getLinkGroup() const return nullptr; } +Base::Placement LinkElement::getPlacementOf(const std::string& sub, DocumentObject* targetObj) +{ + Base::Placement plc; + auto* propPlacement = dynamic_cast(getPropertyByName("Placement")); + if (!propPlacement) { + return plc; + } + plc = propPlacement->getValue(); + + std::vector names = Base::Tools::splitSubName(sub); + + if (names.empty() || this == targetObj || !getLinkedObject()) { + return plc; + } + + Document* doc = getLinkedObject()->getDocument(); + + DocumentObject* subObj = doc->getObject(names.front().c_str()); + + if (!subObj) { + return plc; + } + + std::vector newNames(names.begin() + 1, names.end()); + std::string newSub = Base::Tools::joinList(newNames, "."); + + return plc * subObj->getPlacementOf(newSub, targetObj); +} + ////////////////////////////////////////////////////////////////////////////////////////// namespace App diff --git a/src/App/Link.h b/src/App/Link.h index 2fbfeea8a0..ba5d4fa919 100644 --- a/src/App/Link.h +++ b/src/App/Link.h @@ -664,6 +664,8 @@ public: bool canLinkProperties() const override; + Base::Placement getPlacementOf(const std::string& sub, DocumentObject* targetObj = nullptr) override; + bool isLink() const override; bool isLinkGroup() const override; @@ -718,6 +720,8 @@ public: bool isLink() const override; App::Link* getLinkGroup() const; + + Base::Placement getPlacementOf(const std::string& sub, DocumentObject* targetObj = nullptr) override; }; using LinkElementPython = App::FeaturePythonT; diff --git a/src/Mod/Assembly/UtilsAssembly.py b/src/Mod/Assembly/UtilsAssembly.py index 04b4fefb17..3e88d7dd2c 100644 --- a/src/Mod/Assembly/UtilsAssembly.py +++ b/src/Mod/Assembly/UtilsAssembly.py @@ -383,7 +383,7 @@ def getGlobalPlacement(ref, targetObj=None): rootObj = ref[0] subName = ref[1][0] - return App.GeoFeature.getGlobalPlacementOf(targetObj, rootObj, subName) + return rootObj.getPlacementOf(subName, targetObj) def isThereOneRootAssembly(): diff --git a/src/Mod/Draft/draftobjects/draftlink.py b/src/Mod/Draft/draftobjects/draftlink.py index 404792b087..1bea177274 100644 --- a/src/Mod/Draft/draftobjects/draftlink.py +++ b/src/Mod/Draft/draftobjects/draftlink.py @@ -253,6 +253,54 @@ class DraftLink(DraftObject): else: obj.setPropertyStatus("PlacementList", "Immutable") + def getPlacementOf(self, fp, subname, targetObj=None): + """ + Return the placement of the sub-object relative to the link object. + """ + + # _getShowElementValue is mapped to ExpandArray. + if getattr(fp, "ExpandArray", False): + # Return None to fall back to the C++ implementation + return None + + # We start with the object's own placement. + plc = fp.Placement + + if not subname: + return plc + + names = subname.split(".") + + linked_obj = getattr(fp, "Base", None) + if not names or fp == targetObj or not linked_obj: + return plc + + doc = linked_obj.Document + + try: + i = int(names[0]) + except ValueError: + return None + + # Check bounds in PlacementList + plcs = fp.PlacementList + if i < 0 or i >= len(plcs): + return plc + + # Accumulate the element placement + plc = plc * plcs[i] + + if len(names) < 2: + return plc + + subObj = doc.getObject(names[1]) + if not subObj: + return plc + + newSub = ".".join(names[2:]) + + return plc * subObj.getPlacementOf(newSub, targetObj) + # Alias for compatibility with old versions of v0.19 _DraftLink = DraftLink