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