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 <[email protected]>

* Update DocumentObject.cpp

* Fix error when called from python with targetObj == None

---------

Co-authored-by: Kacper Donat <[email protected]>
This commit is contained in:
PaddleStroke
2025-12-10 22:47:20 +01:00
committed by GitHub
co-authored by Kacper Donat
parent c12ca7b3ba
commit 7a8135d863
11 changed files with 368 additions and 35 deletions
+29
View File
@@ -30,6 +30,7 @@
#include <Base/Console.h>
#include <Base/Matrix.h>
#include <Base/Placement.h>
#include <Base/Tools.h>
#include <Base/Writer.h>
@@ -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<App::PropertyPlacement*>(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<std::string> 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<std::string> newNames(names.begin() + 1, names.end());
std::string newSub = Base::Tools::joinList(newNames, ".");
return plc * subObj->getPlacementOf(newSub, targetObj);
}
+6
View File
@@ -30,6 +30,7 @@
#include <App/PropertyLinks.h>
#include <App/PropertyStandard.h>
#include <Base/SmartPtrPy.h>
#include <Base/Placement.h>
#include <bitset>
#include <unordered_map>
@@ -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();
+7
View File
@@ -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
"""
...
+30
View File
@@ -23,6 +23,7 @@
#include <Base/GeometryPyCXX.h>
#include <Base/MatrixPy.h>
#include <Base/PlacementPy.h>
#include <Base/PyWrapParseTupleAndKeywords.h>
#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<DocumentObjectPy*>(target)->getDocumentObjectPtr();
}
PY_TRY
{
Base::Placement p = getDocumentObjectPtr()->getPlacementOf(subname, targetObj);
return new Base::PlacementPy(new Base::Placement(p));
}
PY_CATCH
}
+98
View File
@@ -25,6 +25,7 @@
#include <App/DocumentObjectPy.h>
#include <Base/Interpreter.h>
#include <Base/MatrixPy.h>
#include <Base/PlacementPy.h>
#include <Base/Tools.h>
#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<Base::PlacementPy*>(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
+44 -1
View File
@@ -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())) {
+2 -33
View File
@@ -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<std::string> 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,
+99
View File
@@ -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<App::PropertyPlacement*>(getPropertyByName("Placement"));
if (!propPlacement) {
return plc;
}
plc = propPlacement->getValue();
std::vector<std::string> 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<Base::Placement> 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<std::string> 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<std::string> 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<App::PropertyPlacement*>(getPropertyByName("Placement"));
if (!propPlacement) {
return plc;
}
plc = propPlacement->getValue();
std::vector<std::string> 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<std::string> newNames(names.begin() + 1, names.end());
std::string newSub = Base::Tools::joinList(newNames, ".");
return plc * subObj->getPlacementOf(newSub, targetObj);
}
//////////////////////////////////////////////////////////////////////////////////////////
namespace App
+4
View File
@@ -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<LinkElement>;
+1 -1
View File
@@ -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():
+48
View File
@@ -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