// SPDX-License-Identifier: LGPL-2.1-or-later /*************************************************************************** * Copyright (c) 2011 Jürgen Riegel * * Copyright (c) 2011 Werner Mayer * * * * This file is part of the FreeCAD CAx development system. * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this library; see the file COPYING.LIB. If not, * * write to the Free Software Foundation, Inc., 59 Temple Place, * * Suite 330, Boston, MA 02111-1307, USA * * * ***************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include "Expression.h" #include "Application.h" #include "ElementNamingUtils.h" #include "Document.h" #include "DocumentObject.h" #include "DocumentObjectPy.h" #include "DocumentObjectExtension.h" #include "DocumentObjectGroup.h" #include "GeoFeatureGroupExtension.h" #include "Link.h" #include "ObjectIdentifier.h" #include "PropertyExpressionEngine.h" #include "PropertyLinks.h" FC_LOG_LEVEL_INIT("App", true, true) using namespace App; PROPERTY_SOURCE(App::DocumentObject, App::TransactionalObject) DocumentObjectExecReturn* DocumentObject::StdReturn = nullptr; //=========================================================================== // DocumentObject //=========================================================================== DocumentObject::DocumentObject() : ExpressionEngine() { // define Label of type 'Output' to avoid being marked as touched after relabeling ADD_PROPERTY_TYPE(Label, ("Unnamed"), "Base", Prop_Output, "User name of the object (UTF8)"); ADD_PROPERTY_TYPE(Label2, (""), "Base", Prop_Hidden, "User description of the object (UTF8)"); Label2.setStatus(App::Property::Output, true); ADD_PROPERTY_TYPE(ExpressionEngine, (), "Base", Prop_Hidden, "Property expressions"); ADD_PROPERTY(Visibility, (true)); // default set Visibility status to hidden and output (no touch) for // compatibitily reason. We use setStatus instead of PropertyType to // allow user to change its status later Visibility.setStatus(Property::Output, true); Visibility.setStatus(Property::Hidden, true); Visibility.setStatus(Property::NoModify, true); } DocumentObject::~DocumentObject() { if (!PythonObject.is(Py::_None())) { Base::PyGILStateLocker lock; // Remark: The API of Py::Object has been changed to set whether the wrapper owns the passed // Python object or not. In the constructor we forced the wrapper to own the object so we // need not to dec'ref the Python object any more. But we must still invalidate the Python // object because it need not to be destructed right now because the interpreter can own // several references to it. Base::PyObjectBase* obj = static_cast(PythonObject.ptr()); // Call before decrementing the reference counter, otherwise a heap error can occur obj->setInvalid(); } } void DocumentObject::printInvalidLinks() const { try { // Get objects that have invalid link scope, and print their names. // Truncate the invalid object list name strings for readability, if they happen to be very // long. std::vector invalid_linkobjs; std::string objnames, scopenames; GeoFeatureGroupExtension::getInvalidLinkObjects(this, invalid_linkobjs); for (auto& obj : invalid_linkobjs) { objnames += obj->getNameInDocument(); objnames += " "; for (auto& scope : obj->getParents()) { if (scopenames.length() > 80) { scopenames += "... "; break; } scopenames += scope.first->getNameInDocument(); scopenames += " "; } if (objnames.length() > 80) { objnames += "... "; break; } } if (objnames.empty()) { objnames = "N/A"; } else { objnames.pop_back(); } if (scopenames.empty()) { scopenames = "N/A"; } else { scopenames.pop_back(); } Base::Console().warning("%s: Link(s) to object(s) '%s' go out of the allowed scope '%s'. " "Instead, the linked object(s) reside within '%s'.\n", getTypeId().getName(), objnames.c_str(), getNameInDocument(), scopenames.c_str()); } catch (const Base::Exception& e) { e.reportException(); } } App::DocumentObjectExecReturn* DocumentObject::recompute() { // check if the links are valid before making the recompute if (!GeoFeatureGroupExtension::areLinksValid(this)) { printInvalidLinks(); } // set/unset the execution bit Base::ObjectStatusLocker exe(App::Recompute, this); // mark the object to recompute its extensions this->setStatus(App::RecomputeExtension, true); auto ret = this->execute(); if (ret == StdReturn) { // most feature classes don't call the execute() method of its base class // so execute the extensions now if (this->testStatus(App::RecomputeExtension)) { ret = executeExtensions(); } } return ret; } DocumentObjectExecReturn* DocumentObject::execute() { return executeExtensions(); } App::DocumentObjectExecReturn* DocumentObject::executeExtensions() { // execute extensions but stop on error this->setStatus(App::RecomputeExtension, false); // reset the flag auto vector = getExtensionsDerivedFromType(); for (auto ext : vector) { auto ret = ext->extensionExecute(); if (ret != StdReturn) { return ret; } } return StdReturn; } bool DocumentObject::recomputeFeature(bool recursive) { Document* doc = this->getDocument(); if (doc) { return doc->recomputeFeature(this, recursive); } return isValid(); } /** * @brief Set this document object touched. * Touching a document object does not mean to recompute it, it only means that * other document objects that link it (i.e. its InList) will be recomputed. * If it should be forced to recompute a document object then use * \ref enforceRecompute() instead. */ void DocumentObject::touch(bool noRecompute) { if (!noRecompute) { StatusBits.set(ObjectStatus::Enforce); } StatusBits.set(ObjectStatus::Touch); if (_pDoc) { _pDoc->signalTouchedObject(*this); } } /** * @brief Set this document object freezed. * A freezed document object does not recompute ever. */ void DocumentObject::freeze() { StatusBits.set(ObjectStatus::Freeze); // store read-only property names this->readOnlyProperties.clear(); std::vector> list; static_cast(this)->getPropertyNamedList(list); for (auto pair: list){ if (pair.second->isReadOnly()){ this->readOnlyProperties.push_back(pair.first); } else { pair.second->setReadOnly(true); } } // use the signalTouchedObject to refresh the Gui if (_pDoc) { _pDoc->signalTouchedObject(*this); } } /** * @brief Set this document object unfreezed. * A freezed document object does not recompute ever. */ void DocumentObject::unfreeze(bool noRecompute) { StatusBits.reset(ObjectStatus::Freeze); // reset read-only property status std::vector> list; static_cast(this)->getPropertyNamedList(list); for (auto pair: list){ if (! std::count(readOnlyProperties.begin(), readOnlyProperties.end(), pair.first)){ pair.second->setReadOnly(false); } } touch(noRecompute); } /** * @brief Check whether the document object is touched or not. * @return true if document object is touched, false if not. */ bool DocumentObject::isTouched() const { return ExpressionEngine.isTouched() || StatusBits.test(ObjectStatus::Touch); } /** * @brief Enforces this document object to be recomputed. * This can be useful to recompute the feature without * having to change one of its input properties. */ void DocumentObject::enforceRecompute() { touch(false); } /** * @brief Check whether the document object must be recomputed or not. * This means that the 'Enforce' flag is set or that \ref mustExecute() * returns a value > 0. * @return true if document object must be recomputed, false if not. */ bool DocumentObject::mustRecompute() const { if (StatusBits.test(ObjectStatus::Freeze)) { return false; } if (StatusBits.test(ObjectStatus::Enforce)) { return true; } return mustExecute() > 0; } short DocumentObject::mustExecute() const { if (ExpressionEngine.isTouched()) { return 1; } // ask all extensions auto vector = getExtensionsDerivedFromType(); for (auto ext : vector) { if (ext->extensionMustExecute()) { return 1; } } return 0; } const char* DocumentObject::getStatusString() const { if (isError()) { const char* text = getDocument()->getErrorDescription(this); return text ? text : "Error"; } else if (isFreezed()){ return "Freezed"; } else if (isTouched()) { return "Touched"; } else { return "Valid"; } } std::string DocumentObject::getFullName() const { if (!getDocument() || !isAttachedToDocument()) { return "?"; } std::string name(getDocument()->getName()); name += '#'; name += *pcNameInDocument; return name; } std::string DocumentObject::getFullLabel() const { if (!getDocument()) { return "?"; } auto name = getDocument()->Label.getStrValue(); name += "#"; name += Label.getStrValue(); return name; } const char* DocumentObject::getDagKey() const { if (!pcNameInDocument) { return nullptr; } return pcNameInDocument->c_str(); } const char* DocumentObject::getNameInDocument() const { // Note: It can happen that we query the internal name of an object even if it is not // part of a document (anymore). This is the case e.g. if we have a reference in Python // to an object that has been removed from the document. In this case we should rather // return 0. // assert(pcNameInDocument); if (!pcNameInDocument) { return nullptr; } return pcNameInDocument->c_str(); } int DocumentObject::isExporting() const { if (!getDocument() || !isAttachedToDocument()) { return 0; } return getDocument()->isExporting(this); } std::string DocumentObject::getExportName(bool forced) const { if (!isAttachedToDocument()) { return {}; } if (!forced && !isExporting()) { return *pcNameInDocument; } // '@' is an invalid character for an internal name, which ensures the // following returned name will be unique in any document. Saving external // object like that shall only happens in Document::exportObjects(). We // shall strip out this '@' and the following document name during restoring. return *pcNameInDocument + '@' + getDocument()->getName(); } bool DocumentObject::isAttachedToDocument() const { return (pcNameInDocument != nullptr); } const char* DocumentObject::detachFromDocument() { const std::string* name = pcNameInDocument; pcNameInDocument = nullptr; return name ? name->c_str() : nullptr; } const std::vector& DocumentObject::getOutList() const { if (!_outListCached) { _outList.clear(); getOutList(0, _outList); _outListCached = true; } return _outList; } std::vector DocumentObject::getOutList(int options) const { std::vector res; getOutList(options, res); return res; } void DocumentObject::getOutList(int options, std::vector& res) const { if (_outListCached && !options) { res.insert(res.end(), _outList.begin(), _outList.end()); return; } std::vector props; getPropertyList(props); bool noHidden = !!(options & OutListNoHidden); std::size_t size = res.size(); for (auto prop : props) { auto link = freecad_cast(prop); if (link) { link->getLinks(res, noHidden); } } if (!(options & OutListNoExpression)) { ExpressionEngine.getLinks(res); } if (options & OutListNoXLinked) { for (auto it = res.begin() + size; it != res.end();) { auto obj = *it; if (obj && obj->getDocument() != getDocument()) { it = res.erase(it); } else { ++it; } } } } std::vector DocumentObject::getOutListOfProperty(App::Property* prop) const { std::vector ret; if (!prop || prop->getContainer() != this) { return ret; } auto link = freecad_cast(prop); if (link) { link->getLinks(ret); } return ret; } const std::vector& DocumentObject::getInList() const { return _inList; } // The original algorithm is highly inefficient in some special case. // Considering an object is linked by every other objects. After excluding this // object, there is another object linked by every other of the remaining // objects, and so on. The vector 'result' above will be of magnitude n^2. // Even if we replace the vector with a set, we still need to visit that amount // of objects. And this may not be the worst case. getInListEx() has no such // problem. std::vector DocumentObject::getInListRecursive() const { std::set inSet; std::vector res; getInListEx(inSet, true, &res); return res; } // More efficient algorithm to find the recursive inList of an object, // including possible external parents. One shortcoming of this algorithm is // it does not detect cyclic reference, althgouth it won't crash either. void DocumentObject::getInListEx(std::set& inSet, bool recursive, std::vector* inList) const { if (!recursive) { inSet.insert(_inList.begin(), _inList.end()); if (inList) { *inList = _inList; } return; } std::stack pendings; pendings.push(const_cast(this)); while (!pendings.empty()) { auto obj = pendings.top(); pendings.pop(); for (auto o : obj->getInList()) { if (o && o->isAttachedToDocument() && inSet.insert(o).second) { pendings.push(o); if (inList) { inList->push_back(o); } } } } } std::set DocumentObject::getInListEx(bool recursive) const { std::set ret; getInListEx(ret, recursive); return ret; } void _getOutListRecursive(std::set& objSet, const DocumentObject* obj, const DocumentObject* checkObj, int depth) { for (const auto objIt : obj->getOutList()) { // if the check object is in the recursive inList we have a cycle! if (objIt == checkObj || depth <= 0) { throw Base::BadGraphError( "DocumentObject::getOutListRecursive(): cyclic dependency detected!"); } // if the element was already in the set then there is no need to process it again auto pair = objSet.insert(objIt); if (pair.second) { _getOutListRecursive(objSet, objIt, checkObj, depth - 1); } } } std::vector DocumentObject::getOutListRecursive() const { // number of objects in document is a good estimate in result size int maxDepth = GetApplication().checkLinkDepth(0); std::set result; // using a recursive helper to collect all OutLists _getOutListRecursive(result, this, this, maxDepth); std::vector array; array.insert(array.begin(), result.begin(), result.end()); return array; } // helper for isInInListRecursive() bool _isInInListRecursive(const DocumentObject* act, const DocumentObject* checkObj, int depth) { for (auto obj : act->getInList()) { if (obj == checkObj) { return true; } // if we reach the depth limit we have a cycle! if (depth <= 0) { throw Base::BadGraphError( "DocumentObject::isInInListRecursive(): cyclic dependency detected!"); } if (_isInInListRecursive(obj, checkObj, depth - 1)) { return true; } } return false; } bool DocumentObject::isInInListRecursive(DocumentObject* linkTo) const { return this == linkTo || getInListEx(true).contains(linkTo); } bool DocumentObject::isInInList(DocumentObject* linkTo) const { if (std::ranges::find(_inList, linkTo) != _inList.end()) { return true; } else { return false; } } // helper for isInOutListRecursive() bool _isInOutListRecursive(const DocumentObject* act, const DocumentObject* checkObj, int depth) { for (auto obj : act->getOutList()) { if (obj == checkObj) { return true; } // if we reach the depth limit we have a cycle! if (depth <= 0) { throw Base::BadGraphError( "DocumentObject::isInOutListRecursive(): cyclic dependency detected!"); } if (_isInOutListRecursive(obj, checkObj, depth - 1)) { return true; } } return false; } bool DocumentObject::isInOutListRecursive(DocumentObject* linkTo) const { int maxDepth = getDocument()->countObjects() + 2; return _isInOutListRecursive(this, linkTo, maxDepth); } std::vector> DocumentObject::getPathsByOutList(App::DocumentObject* to) const { return _pDoc->getPathsByOutList(this, to); } DocumentObjectGroup* DocumentObject::getGroup() const { return freecad_cast(GroupExtension::getGroupOfObject(this)); } bool DocumentObject::testIfLinkDAGCompatible(DocumentObject* linkTo) const { std::vector linkTo_in_vector; linkTo_in_vector.push_back(linkTo); return this->testIfLinkDAGCompatible(linkTo_in_vector); } bool DocumentObject::testIfLinkDAGCompatible(const std::vector& linksTo) const { auto inLists = getInListEx(true); inLists.emplace(const_cast(this)); for (auto obj : linksTo) { if (inLists.contains(obj)) { return false; } } return true; } bool DocumentObject::testIfLinkDAGCompatible(PropertyLinkSubList& linksTo) const { const std::vector& linksTo_in_vector = linksTo.getValues(); return this->testIfLinkDAGCompatible(linksTo_in_vector); } bool DocumentObject::testIfLinkDAGCompatible(PropertyLinkSub& linkTo) const { std::vector linkTo_in_vector; linkTo_in_vector.reserve(1); linkTo_in_vector.push_back(linkTo.getValue()); return this->testIfLinkDAGCompatible(linkTo_in_vector); } void DocumentObject::onLostLinkToObject(DocumentObject*) {} App::Document* DocumentObject::getDocument() const { return _pDoc; } void DocumentObject::setDocument(App::Document* doc) { _pDoc = doc; onSettingDocument(); } bool DocumentObject::removeDynamicProperty(const char* name) { if (!_pDoc || testStatus(ObjectStatus::Destroy)) { return false; } Property* prop = getDynamicPropertyByName(name); if (!prop || prop->testStatus(App::Property::LockDynamic)) { return false; } if (prop->isDerivedFrom()) { clearOutListCache(); } _pDoc->addOrRemovePropertyOfObject(this, prop, false); auto expressions = ExpressionEngine.getExpressions(); std::vector removeExpr; for (const auto& it : expressions) { if (it.first.getProperty() == prop) { removeExpr.push_back(it.first); } } for (const auto& it : removeExpr) { ExpressionEngine.setValue(it, std::shared_ptr()); } return TransactionalObject::removeDynamicProperty(name); } bool DocumentObject::renameDynamicProperty(Property* prop, const char* name) { std::string oldName = prop->getName(); auto expressions = ExpressionEngine.getExpressions(); std::vector> expressionsToMove; std::vector idsWithExprsToRemove; for (const auto& [id, expr] : expressions) { if (id.getProperty() == prop) { idsWithExprsToRemove.push_back(id); expressionsToMove.emplace_back(expr->copy()); } } for (const auto& it : idsWithExprsToRemove) { ExpressionEngine.setValue(it, std::shared_ptr()); } bool renamed = TransactionalObject::renameDynamicProperty(prop, name); if (renamed && _pDoc) { _pDoc->renamePropertyOfObject(this, prop, oldName.c_str()); } App::ObjectIdentifier idNewProp(prop->getContainer(), std::string(name)); for (auto& exprToMove : expressionsToMove) { ExpressionEngine.setValue(idNewProp, exprToMove); } return renamed; } App::Property* DocumentObject::addDynamicProperty(const char* type, const char* name, const char* group, const char* doc, short attr, bool ro, bool hidden) { auto prop = TransactionalObject::addDynamicProperty(type, name, group, doc, attr, ro, hidden); if (prop && _pDoc) { _pDoc->addOrRemovePropertyOfObject(this, prop, true); } return prop; } void DocumentObject::onBeforeChange(const Property* prop) { if (isFreezed() && prop != &Visibility) { return; } // Store current name in oldLabel, to be able to easily retrieve old name of document object later // when renaming expressions. if (prop == &Label) oldLabel = Label.getStrValue(); if (_pDoc){ onBeforeChangeProperty(_pDoc, prop); } signalBeforeChange(*this, *prop); } std::vector>> DocumentObject::onProposedLabelChange(std::string& newLabel) { // Note that this work can't be done in onBeforeChangeLabel because FeaturePython overrides this // method and does not initially base-call it. // We re only called if the new label differs from the old one, and our code to check duplicates // may not work if this is not the case. std::string oldLabel = Label.getStrValue(); assert(newLabel != oldLabel); if (!isAttachedToDocument()) { return {}; } App::Document* doc = getDocument(); if (doc->isPerformingTransaction() || (doc->testStatus(App::Document::Restoring) && !doc->testStatus(App::Document::Importing))) { return {}; } static ParameterGrp::handle _hPGrp; if (!_hPGrp) { _hPGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document"); } if (doc && !newLabel.empty() && !_hPGrp->GetBool("DuplicateLabels") && !allowDuplicateLabel() && doc->containsLabel(newLabel)) { // The label already exists but settings are such that duplicate labels should not be assigned. std::string objName = getNameInDocument(); if (!doc->containsLabel(objName) && doc->haveSameBaseName(objName, newLabel)) { // The object name is not already a Label and the base name of the proposed label // equals the base name of the object Name, so we use the object Name as the replacement Label. newLabel = objName; } else { // Otherwise we generate a unique Label using newLabel as a prototype name. In doing so, // we must also act as if the current value of the property is not an existing Label // entry. // We deregister the old label so it does not interfere with making the new label, // and re-register it after. This is probably a bit less efficient that having a special // make-unique-label-as-if-this-one-did-not-exist method, but such a method would be a real // ugly wart. doc->unregisterLabel(oldLabel); newLabel = doc->makeUniqueLabel(newLabel); doc->registerLabel(oldLabel); } } // Despite our efforts to make a unique label, onBeforeLabelChange can change it. onBeforeChangeLabel(newLabel); if (oldLabel == newLabel || getDocument()->testStatus(App::Document::Restoring)) { // Don't update label reference if we are restoring or if the label is unchanged. // When importing (which also counts as restoring), it is possible the // new object changes its label. However, we cannot update label // references here, because object being restored is not based on // dependency order. It can only be done in afterRestore(). // // See PropertyLinkBase::restoreLabelReference() for more details. return {}; } return PropertyLinkBase::updateLabelReferences(this, newLabel.c_str()); } void DocumentObject::onEarlyChange(const Property* prop) { if (isFreezed() && prop != &Visibility) { return; } if (GetApplication().isClosingAll()) { return; } if (!GetApplication().isRestoring() && !prop->testStatus(Property::PartialTrigger) && getDocument() && getDocument()->testStatus(Document::PartialDoc)) { static App::Document* warnedDoc; if (warnedDoc != getDocument()) { warnedDoc = getDocument(); FC_WARN("Changes to partial loaded document will not be saved: " << getFullName() << '.' << prop->getName()); } } signalEarlyChanged(*this, *prop); } /// get called by the container when a Property was changed void DocumentObject::onChanged(const Property* prop) { if (prop == &Label && _pDoc && _pDoc->containsObject(this) && oldLabel != Label.getStrValue()) { _pDoc->unregisterLabel(oldLabel); _pDoc->registerLabel(Label.getStrValue()); } if (isFreezed() && prop != &Visibility) { return; } if (GetApplication().isClosingAll()) { return; } if (!GetApplication().isRestoring() && !prop->testStatus(Property::PartialTrigger) && getDocument() && getDocument()->testStatus(Document::PartialDoc)) { static App::Document* warnedDoc; if (warnedDoc != getDocument()) { warnedDoc = getDocument(); FC_WARN("Changes to partial loaded document will not be saved: " << getFullName() << '.' << prop->getName()); } } // Delay signaling view provider until the document object has handled the // change // if (_pDoc) // _pDoc->onChangedProperty(this,prop); if (prop == &Label && _pDoc && oldLabel != Label.getStrValue()) { _pDoc->signalRelabelObject(*this); } // set object touched if it is an input property if (!testStatus(ObjectStatus::NoTouch) && !(prop->getType() & Prop_Output) && !prop->testStatus(Property::Output)) { if (!StatusBits.test(ObjectStatus::Touch)) { FC_TRACE("touch '" << getFullName() << "' on change of '" << prop->getName() << "'"); StatusBits.set(ObjectStatus::Touch); } // must execute on document recompute if (!(prop->getType() & Prop_NoRecompute)) { StatusBits.set(ObjectStatus::Enforce); } } // call the parent for appropriate handling TransactionalObject::onChanged(prop); // Now signal the view provider if (_pDoc) { _pDoc->onChangedProperty(this, prop); } signalChanged(*this, *prop); } void DocumentObject::clearOutListCache() const { _outList.clear(); _outListMap.clear(); _outListCached = false; } PyObject* DocumentObject::getPyObject() { if (PythonObject.is(Py::_None())) { // ref counter is set to 1 PythonObject = Py::Object(new DocumentObjectPy(this), true); } return Py::new_reference_to(PythonObject); } DocumentObject* DocumentObject::getSubObject(const char* subname, PyObject** pyObj, Base::Matrix4D* mat, bool transform, int depth) const { DocumentObject* ret = nullptr; auto exts = getExtensionsDerivedFromType(); for (auto ext : exts) { if (ext->extensionGetSubObject(ret, subname, pyObj, mat, transform, depth)) { return ret; } } std::string name; const char* dot = nullptr; if (!subname || !(dot = strchr(subname, '.'))) { ret = const_cast(this); } else if (subname[0] == '$') { name = std::string(subname + 1, dot); for (auto obj : getOutList()) { if (name == obj->Label.getValue()) { ret = obj; break; } } } else { name = std::string(subname, dot); const auto& outList = getOutList(); if (outList.size() != _outListMap.size()) { _outListMap.clear(); for (auto obj : outList) { _outListMap[obj->getDagKey()] = obj; } } auto it = _outListMap.find(name.c_str()); if (it != _outListMap.end()) { ret = it->second; } } // TODO: By right, normal object's placement does not transform its sub // objects (think of the claimed children of a Fusion). But I do think we // should change that. if (transform && mat) { auto pla = freecad_cast(getPropertyByName("Placement")); if (pla) { *mat *= pla->getValue().toMatrix(); } } if (ret && dot) { return ret->getSubObject(dot + 1, pyObj, mat, true, depth + 1); } return ret; } namespace { std::vector getSubObjectListFlatten(const std::vector& resNotFlatten, std::vector* const subsizes, const App::DocumentObject* sobj, App::DocumentObject** container, bool& lastChild) { auto res {resNotFlatten}; auto linked = sobj->getLinkedObject(); if (*container) { auto grp = App::GeoFeatureGroupExtension::getGroupOfObject(linked); if (grp != *container) { *container = nullptr; } else { if (lastChild && !res.empty()) { res.pop_back(); if (subsizes) { subsizes->pop_back(); } } lastChild = true; } } if (linked->getExtensionByType(true)) { *container = linked; lastChild = false; } else if (linked != sobj || sobj->hasChildElement()) { // Check for Link or LinkGroup *container = nullptr; } else if (auto ext = sobj->getExtensionByType(true)) { // check for Link array if (ext->getElementCountValue() != 0) { *container = nullptr; } } return res; } } // namespace std::vector DocumentObject::getSubObjectList(const char* subname, std::vector* const subsizes, bool flatten) const { std::vector res; res.push_back(const_cast(this)); if (subsizes) { subsizes->push_back(0); } if (!subname || (subname[0] == '\0')) { return res; } auto element = Data::findElementName(subname); std::string sub(subname, element - subname); App::DocumentObject* container = nullptr; bool lastChild = false; if (flatten) { auto linked = getLinkedObject(); if (linked->getExtensionByType(true)) { container = const_cast(this); } else if (auto grp = App::GeoFeatureGroupExtension::getGroupOfObject(linked)) { container = grp; lastChild = true; } } for (auto pos = sub.find('.'); pos != std::string::npos; pos = sub.find('.', pos + 1)) { char subTail = sub[pos + 1]; sub[pos + 1] = '\0'; auto sobj = getSubObject(sub.c_str()); if (!sobj || !sobj->isAttachedToDocument()) { continue; } if (flatten) { res = getSubObjectListFlatten(res, subsizes, sobj, &container, lastChild); } res.push_back(sobj); if (subsizes) { subsizes->push_back((int)pos + 1); } sub[pos + 1] = subTail; } return res; } std::vector DocumentObject::getSubObjects(int reason) const { std::vector ret; auto exts = getExtensionsDerivedFromType(); for (auto ext : exts) { if (ext->extensionGetSubObjects(ret, reason)) { return ret; } } return ret; } std::vector> DocumentObject::getParents(int depth) const { std::vector> ret; if (!isAttachedToDocument() || !GetApplication().checkLinkDepth(depth, MessageOption::Throw)) { return ret; } std::string name(getNameInDocument()); name += "."; for (auto parent : getInList()) { if (!parent || !parent->isAttachedToDocument()) { continue; } if (!parent->hasChildElement() && !parent->hasExtension(GeoFeatureGroupExtension::getExtensionClassTypeId())) { continue; } if (!parent->getSubObject(name.c_str())) { continue; } auto links = GetApplication().getLinksTo(parent, App::GetLinkRecursive); links.insert(parent); for (auto parent : links) { auto parents = parent->getParents(depth + 1); if (parents.empty()) { parents.emplace_back(parent, std::string()); } for (auto& v : parents) { ret.emplace_back(v.first, v.second + name); } } } return ret; } App::DocumentObject* DocumentObject::getFirstParent() const { for (auto obj : getInList()) { if (obj->hasExtension(App::GroupExtension::getExtensionClassTypeId(), true)) { return obj; } } return nullptr; } DocumentObject* DocumentObject::getLinkedObject(bool recursive, Base::Matrix4D* mat, bool transform, int depth) const { DocumentObject* ret = nullptr; auto exts = getExtensionsDerivedFromType(); for (auto ext : exts) { if (ext->extensionGetLinkedObject(ret, recursive, mat, transform, depth)) { return ret; } } if (transform && mat) { auto pla = freecad_cast(getPropertyByName("Placement")); if (pla) { *mat *= pla->getValue().toMatrix(); } } return const_cast(this); } void DocumentObject::Save(Base::Writer& writer) const { if (this->isFreezed()) { throw Base::AbortException("At least one object is frozen, unable to save."); } if (this->isAttachedToDocument()){ writer.ObjectName = this->getNameInDocument(); } App::ExtensionContainer::Save(writer); } /** * @brief Associate the expression \expr with the object identifier \a path in this document object. * @param path Target object identifier for the result of the expression * @param expr Expression tree */ void DocumentObject::setExpression(const ObjectIdentifier& path, std::shared_ptr expr) { ExpressionEngine.setValue(path, std::move(expr)); } /** * @brief Clear the expression of the object identifier \a path in this document object. * @param path Target object identifier */ void DocumentObject::clearExpression(const ObjectIdentifier& path) { setExpression(path, std::shared_ptr()); } /** * @brief Get expression information associated with \a path. * @param path Object identifier * @return Expression info, containing expression and optional comment. */ const PropertyExpressionEngine::ExpressionInfo DocumentObject::getExpression(const ObjectIdentifier& path) const { boost::any value = ExpressionEngine.getPathValue(path); if (value.type() == typeid(PropertyExpressionEngine::ExpressionInfo)) { return boost::any_cast(value); } else { return PropertyExpressionEngine::ExpressionInfo(); } } /** * @brief Invoke ExpressionEngine's renameObjectIdentifier, to possibly rewrite expressions using * the \a paths map with current and new identifiers. * * @param paths */ void DocumentObject::renameObjectIdentifiers( const std::map& paths) { ExpressionEngine.renameObjectIdentifiers(paths); } void DocumentObject::onDocumentRestored() { // call all extensions auto vector = getExtensionsDerivedFromType(); for (auto ext : vector) { ext->onExtendedDocumentRestored(); } if (Visibility.testStatus(Property::Output)) { Visibility.setStatus(Property::NoModify, true); } } void DocumentObject::restoreFinished() { // some link type property cannot restore link information until other // objects has been restored. For example, PropertyExpressionEngine and // PropertySheet with expression containing label reference. // So on document load they are handled in Document::afterRestore, but if the user // use dumpContent and restoreContent then they need to be handled here. std::vector props; getPropertyList(props); for (auto prop : props) { prop->afterRestore(); } } void DocumentObject::onUndoRedoFinished() {} void DocumentObject::onSettingDocument() { // call all extensions auto vector = getExtensionsDerivedFromType(); for (auto ext : vector) { ext->onExtendedSettingDocument(); } } void DocumentObject::setupObject() { // call all extensions auto vector = getExtensionsDerivedFromType(); for (auto ext : vector) { ext->onExtendedSetupObject(); } } void DocumentObject::unsetupObject() { // call all extensions auto vector = getExtensionsDerivedFromType(); for (auto ext : vector) { ext->onExtendedUnsetupObject(); } } void App::DocumentObject::_removeBackLink(DocumentObject* rmvObj) { // do not use erase-remove idom, as this erases ALL entries that match. we only want to remove a // single one. auto it = std::ranges::find(_inList, rmvObj); if (it != _inList.end()) { _inList.erase(it); } } void App::DocumentObject::_addBackLink(DocumentObject* newObj) { // we need to add all links, even if they are available multiple times. The reason for this is // the removal: If a link loses this object it removes the backlink. If we would have added it // only once this removal would clear the object from the inlist, even though there may be other // link properties from this object that link to us. _inList.push_back(newObj); } int DocumentObject::setElementVisible(const char* element, bool visible) { for (auto ext : getExtensionsDerivedFromType()) { int ret = ext->extensionSetElementVisible(element, visible); if (ret >= 0) { return ret; } } return -1; } int DocumentObject::isElementVisible(const char* element) const { for (auto ext : getExtensionsDerivedFromType()) { int ret = ext->extensionIsElementVisible(element); if (ret >= 0) { return ret; } } return -1; } bool DocumentObject::hasChildElement() const { for (auto ext : getExtensionsDerivedFromType()) { if (ext->extensionHasChildElement()) { return true; } } return false; } DocumentObject* DocumentObject::resolve(const char* subname, App::DocumentObject** parent, std::string* childName, const char** subElement, PyObject** pyObj, Base::Matrix4D* pmat, bool transform, int depth) const { auto self = const_cast(this); if (parent) { *parent = nullptr; } if (subElement) { *subElement = nullptr; } auto obj = getSubObject(subname, pyObj, pmat, transform, depth); if (!obj || !subname || *subname == 0) { return self; } if (!parent && !subElement) { return obj; } // NOTE, the convention of '.' separated SubName demands a mandatory ending // '.' for each object name in SubName, even if there is no subelement // following it. So finding the last dot will give us the end of the last // object name. const char* dot = nullptr; if (Data::isMappedElement(subname) || !(dot = strrchr(subname, '.')) || dot == subname) { if (subElement) { *subElement = dot ? dot + 1 : subname; } return obj; // this means no parent object reference in SubName } if (parent) { *parent = self; } bool elementMapChecked = false; const char* lastDot = dot; for (--dot;; --dot) { // check for the second last dot, which is the end of the last parent object if (*dot == '.' || dot == subname) { // We can't get parent object by its name, because the object may be // externally linked (i.e. in a different document). So go through // getSubObject again. if (!elementMapChecked) { elementMapChecked = true; const char* sub = dot == subname ? dot : dot + 1; if (Data::isMappedElement(sub)) { lastDot = dot; if (dot == subname) { break; } else { continue; } } } if (dot == subname) { break; } auto sobj = getSubObject(std::string(subname, dot - subname + 1).c_str()); if (sobj != obj) { if (parent) { // Link/LinkGroup has special visibility handling of plain // group, so keep ascending if (!sobj->hasExtension(GroupExtension::getExtensionClassTypeId(), false)) { *parent = sobj; break; } for (auto ddot = dot - 1; ddot != subname; --ddot) { if (*ddot != '.') { continue; } auto sobj = getSubObject(std::string(subname, ddot - subname + 1).c_str()); if (!sobj->hasExtension(GroupExtension::getExtensionClassTypeId(), false)) { *parent = sobj; break; } } } break; } } } if (childName && lastDot != dot) { if (*dot == '.') { ++dot; } const char* nextDot = strchr(dot, '.'); assert(nextDot); *childName = std::string(dot, nextDot - dot); } if (subElement) { *subElement = *lastDot == '.' ? lastDot + 1 : lastDot; } return obj; } DocumentObject* DocumentObject::resolveRelativeLink(std::string& subname, DocumentObject*& link, std::string& linkSub) const { if (!link || !link->isAttachedToDocument() || !isAttachedToDocument()) { return nullptr; } auto ret = const_cast(this); if (link != ret) { auto sub = subname.c_str(); auto nextsub = sub; for (auto dot = strchr(nextsub, '.'); dot; nextsub = dot + 1, dot = strchr(nextsub, '.')) { std::string subcheck(sub, nextsub - sub); subcheck += link->getNameInDocument(); subcheck += '.'; if (getSubObject(subcheck.c_str()) == link) { ret = getSubObject(std::string(sub, dot + 1 - sub).c_str()); if (!ret) { return nullptr; } subname = std::string(dot + 1); break; } } return ret; } size_t pos = 0, linkPos = 0; std::string linkssub, ssub; do { linkPos = linkSub.find('.', linkPos); if (linkPos == std::string::npos) { link = nullptr; return nullptr; } ++linkPos; pos = subname.find('.', pos); if (pos == std::string::npos) { subname.clear(); ret = nullptr; break; } ++pos; } while (subname.compare(0, pos, linkSub, 0, linkPos) == 0); if (pos != std::string::npos) { ret = getSubObject(subname.substr(0, pos).c_str()); if (!ret) { link = nullptr; return nullptr; } subname = subname.substr(pos); } if (linkPos) { link = link->getSubObject(linkSub.substr(0, linkPos).c_str()); if (!link) { return nullptr; } linkSub = linkSub.substr(linkPos); } return ret; } bool DocumentObject::adjustRelativeLinks(const std::set& inList, std::set* visited) { if (visited) { visited->insert(this); } bool touched = false; std::vector props; getPropertyList(props); for (auto prop : props) { auto linkProp = freecad_cast(prop); if (linkProp && linkProp->adjustLink(inList)) { touched = true; } } if (visited) { for (auto obj : getOutList()) { if (!visited->count(obj)) { if (obj->adjustRelativeLinks(inList, visited)) { touched = true; } } } } return touched; } std::string DocumentObject::getElementMapVersion(const App::Property* _prop, bool restored) const { auto prop = freecad_cast(_prop); if (!prop) { return std::string(); } return prop->getElementMapVersion(restored); } bool DocumentObject::checkElementMapVersion(const App::Property* _prop, const char* ver) const { auto prop = freecad_cast(_prop); if (!prop) { return false; } return prop->checkElementMapVersion(ver); } const std::string& DocumentObject::hiddenMarker() { static std::string marker("!hide"); return marker; } const char* DocumentObject::hasHiddenMarker(const char* subname) { if (!subname) { return nullptr; } const char* marker = strrchr(subname, '.'); if (!marker) { marker = subname; } else { ++marker; } return hiddenMarker() == marker ? marker : nullptr; } bool DocumentObject::redirectSubName(std::ostringstream&, DocumentObject*, DocumentObject*) const { return false; } void DocumentObject::onPropertyStatusChanged(const Property& prop, unsigned long oldStatus) { (void)oldStatus; if (!Document::isAnyRestoring() && isAttachedToDocument() && getDocument()) { 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); } Base::Placement DocumentObject::getPlacement() const { Base::Placement plc; if (auto* prop = getPlacementProperty()) { plc = prop->getValue(); } return plc; } App::PropertyPlacement* DocumentObject::getPlacementProperty() const { if (auto linkExtension = getExtensionByType(true)) { if (auto linkPlacementProp = linkExtension->getLinkPlacementProperty()) { return linkPlacementProp; } return linkExtension->getPlacementProperty(); } return getPropertyByName("Placement"); }