Files
create/src/App/Document.cpp
2025-05-03 22:19:51 +02:00

4382 lines
150 KiB
C++

/***************************************************************************
* Copyright (c) 2002 Jürgen Riegel <juergen.riegel@web.de> *
* *
* 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 "PreCompiled.h"
#ifndef _PreComp_
#include <bitset>
#include <stack>
#include <boost/filesystem.hpp>
#include <deque>
#include <iostream>
#include <utility>
#include <set>
#include <memory>
#include <string>
#include <map>
#include <vector>
#include <list>
#include <algorithm>
#include <filesystem>
#endif
#include <boost/algorithm/string.hpp>
#include <boost/bimap.hpp>
#include <boost/graph/strong_components.hpp>
#include <boost/regex.hpp>
#include <random>
#include <unordered_map>
#include <unordered_set>
#include <QCryptographicHash>
#include <QCoreApplication>
#include <App/DocumentPy.h>
#include <Base/Interpreter.h>
#include <Base/Console.h>
#include <Base/Exception.h>
#include <Base/FileInfo.h>
#include <Base/TimeInfo.h>
#include <Base/Reader.h>
#include <Base/Writer.h>
#include <Base/Profiler.h>
#include <Base/Tools.h>
#include <Base/Uuid.h>
#include <Base/Sequencer.h>
#include <Base/Stream.h>
#include <Base/UnitsApi.h>
#include "Document.h"
#include "private/DocumentP.h"
#include "Application.h"
#include "AutoTransaction.h"
#include "ExpressionParser.h"
#include "GeoFeature.h"
#include "License.h"
#include "Link.h"
#include "MergeDocuments.h"
#include "StringHasher.h"
#include "Transactions.h"
#ifdef _MSC_VER
#include <zipios++/zipios-config.h>
#endif
#include <zipios++/zipfile.h>
#include <zipios++/zipinputstream.h>
#include <zipios++/zipoutputstream.h>
#include <zipios++/meta-iostreams.h>
FC_LOG_LEVEL_INIT("App", true, true, true)
using Base::Console;
using Base::streq;
using Base::Writer;
using namespace App;
using namespace std;
using namespace boost;
using namespace zipios;
#if FC_DEBUG
#define FC_LOGFEATUREUPDATE
#endif
namespace fs = std::filesystem;
namespace App
{
static bool globalIsRestoring;
static bool globalIsRelabeling;
DocumentP::DocumentP()
{
Hasher = new StringHasher;
static std::random_device _RD;
static std::mt19937 _RGEN(_RD());
static std::uniform_int_distribution<> _RDIST(0, 5000);
// Set some random offset to reduce likelihood of ID collision when
// copying shape from other document. It is probably better to randomize
// on each object ID.
lastObjectId = _RDIST(_RGEN);
activeObject = nullptr;
activeUndoTransaction = nullptr;
iTransactionMode = 0;
rollback = false;
undoing = false;
committing = false;
opentransaction = false;
StatusBits.set((size_t)Document::Closable, true);
StatusBits.set((size_t)Document::KeepTrailingDigits, true);
StatusBits.set((size_t)Document::Restoring, false);
iUndoMode = 0;
UndoMemSize = 0;
UndoMaxStackSize = 20;
}
} // namespace App
PROPERTY_SOURCE(App::Document, App::PropertyContainer)
bool Document::testStatus(Status pos) const
{
return d->StatusBits.test((size_t)pos);
}
void Document::setStatus(Status pos, bool on)
{
d->StatusBits.set((size_t)pos, on);
}
// bool _has_cycle_dfs(const DependencyList & g, vertex_t u, default_color_type * color)
//{
// color[u] = gray_color;
// graph_traits < DependencyList >::adjacency_iterator vi, vi_end;
// for (tie(vi, vi_end) = adjacent_vertices(u, g); vi != vi_end; ++vi)
// if (color[*vi] == white_color)
// if (has_cycle_dfs(g, *vi, color))
// return true; // cycle detected, return immediately
// else if (color[*vi] == gray_color) // *vi is an ancestor!
// return true;
// color[u] = black_color;
// return false;
// }
bool Document::checkOnCycle()
{
#if 0
std::vector < default_color_type > color(num_vertices(_DepList), white_color);
graph_traits < DependencyList >::vertex_iterator vi, vi_end;
for (tie(vi, vi_end) = vertices(_DepList); vi != vi_end; ++vi)
if (color[*vi] == white_color)
if (_has_cycle_dfs(_DepList, *vi, &color[0]))
return true;
#endif
return false;
}
bool Document::undo(int id)
{
if (d->iUndoMode) {
if (id) {
auto it = mUndoMap.find(id);
if (it == mUndoMap.end()) {
return false;
}
if (it->second != d->activeUndoTransaction) {
while (!mUndoTransactions.empty() && mUndoTransactions.back() != it->second) {
undo(0);
}
}
}
if (d->activeUndoTransaction) {
_commitTransaction(true);
}
if (mUndoTransactions.empty()) {
return false;
}
// redo
d->activeUndoTransaction = new Transaction(mUndoTransactions.back()->getID());
d->activeUndoTransaction->Name = mUndoTransactions.back()->Name;
{
Base::FlagToggler<bool> flag(d->undoing);
// applying the undo
mUndoTransactions.back()->apply(*this, false);
// save the redo
mRedoMap[d->activeUndoTransaction->getID()] = d->activeUndoTransaction;
mRedoTransactions.push_back(d->activeUndoTransaction);
d->activeUndoTransaction = nullptr;
mUndoMap.erase(mUndoTransactions.back()->getID());
delete mUndoTransactions.back();
mUndoTransactions.pop_back();
}
for (auto& obj : d->objectArray) {
if (obj->testStatus(ObjectStatus::PendingTransactionUpdate)) {
obj->onUndoRedoFinished();
obj->setStatus(ObjectStatus::PendingTransactionUpdate, false);
}
}
signalUndo(*this); // now signal the undo
return true;
}
return false;
}
bool Document::redo(int id)
{
if (d->iUndoMode) {
if (id) {
auto it = mRedoMap.find(id);
if (it == mRedoMap.end()) {
return false;
}
while (!mRedoTransactions.empty() && mRedoTransactions.back() != it->second) {
redo(0);
}
}
if (d->activeUndoTransaction) {
_commitTransaction(true);
}
assert(mRedoTransactions.size() != 0);
// undo
d->activeUndoTransaction = new Transaction(mRedoTransactions.back()->getID());
d->activeUndoTransaction->Name = mRedoTransactions.back()->Name;
// do the redo
{
Base::FlagToggler<bool> flag(d->undoing);
mRedoTransactions.back()->apply(*this, true);
mUndoMap[d->activeUndoTransaction->getID()] = d->activeUndoTransaction;
mUndoTransactions.push_back(d->activeUndoTransaction);
d->activeUndoTransaction = nullptr;
mRedoMap.erase(mRedoTransactions.back()->getID());
delete mRedoTransactions.back();
mRedoTransactions.pop_back();
}
for (auto& obj : d->objectArray) {
if (obj->testStatus(ObjectStatus::PendingTransactionUpdate)) {
obj->onUndoRedoFinished();
obj->setStatus(ObjectStatus::PendingTransactionUpdate, false);
}
}
signalRedo(*this);
return true;
}
return false;
}
void Document::addOrRemovePropertyOfObject(TransactionalObject* obj, Property* prop, bool add)
{
if (!prop || !obj || !obj->isAttachedToDocument()) {
return;
}
if (d->iUndoMode && !isPerformingTransaction() && !d->activeUndoTransaction) {
if (!testStatus(Restoring) || testStatus(Importing)) {
int tid = 0;
const char* name = GetApplication().getActiveTransaction(&tid);
if (name && tid > 0) {
_openTransaction(name, tid);
}
}
}
if (d->activeUndoTransaction && !d->rollback) {
d->activeUndoTransaction->addOrRemoveProperty(obj, prop, add);
}
}
bool Document::isPerformingTransaction() const
{
return d->undoing || d->rollback;
}
std::vector<std::string> Document::getAvailableUndoNames() const
{
std::vector<std::string> vList;
if (d->activeUndoTransaction) {
vList.push_back(d->activeUndoTransaction->Name);
}
for (std::list<Transaction*>::const_reverse_iterator It = mUndoTransactions.rbegin();
It != mUndoTransactions.rend();
++It) {
vList.push_back((**It).Name);
}
return vList;
}
std::vector<std::string> Document::getAvailableRedoNames() const
{
std::vector<std::string> vList;
for (std::list<Transaction*>::const_reverse_iterator It = mRedoTransactions.rbegin();
It != mRedoTransactions.rend();
++It) {
vList.push_back((**It).Name);
}
return vList;
}
void Document::openTransaction(const char* name)
{
if (isPerformingTransaction() || d->committing) {
if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
FC_WARN("Cannot open transaction while transacting");
}
return;
}
GetApplication().setActiveTransaction(name ? name : "<empty>");
}
int Document::_openTransaction(const char* name, int id)
{
if (isPerformingTransaction() || d->committing) {
if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
FC_WARN("Cannot open transaction while transacting");
}
return 0;
}
if (d->iUndoMode) {
// Avoid recursive calls that is possible while
// clearing the redo transactions and will cause
// a double deletion of some transaction and thus
// a segmentation fault
if (d->opentransaction) {
return 0;
}
Base::FlagToggler<> flag(d->opentransaction);
if (id && mUndoMap.find(id) != mUndoMap.end()) {
throw Base::RuntimeError("invalid transaction id");
}
if (d->activeUndoTransaction) {
_commitTransaction(true);
}
_clearRedos();
d->activeUndoTransaction = new Transaction(id);
if (!name) {
name = "<empty>";
}
d->activeUndoTransaction->Name = name;
mUndoMap[d->activeUndoTransaction->getID()] = d->activeUndoTransaction;
id = d->activeUndoTransaction->getID();
signalOpenTransaction(*this, name);
auto& app = GetApplication();
auto activeDoc = app.getActiveDocument();
if (activeDoc && activeDoc != this && !activeDoc->hasPendingTransaction()) {
std::string aname("-> ");
aname += d->activeUndoTransaction->Name;
FC_LOG("auto transaction " << getName() << " -> " << activeDoc->getName());
activeDoc->_openTransaction(aname.c_str(), id);
}
return id;
}
return 0;
}
void Document::renameTransaction(const char* name, int id)
{
if (name && d->activeUndoTransaction && d->activeUndoTransaction->getID() == id) {
if (boost::starts_with(d->activeUndoTransaction->Name, "-> ")) {
d->activeUndoTransaction->Name.resize(3);
}
else {
d->activeUndoTransaction->Name.clear();
}
d->activeUndoTransaction->Name += name;
}
}
void Document::_checkTransaction(DocumentObject* pcDelObj, const Property* What, int line)
{
// if the undo is active but no transaction open, open one!
if (d->iUndoMode && !isPerformingTransaction()) {
if (!d->activeUndoTransaction) {
if (!testStatus(Restoring) || testStatus(Importing)) {
int tid = 0;
const char* name = GetApplication().getActiveTransaction(&tid);
if (name && tid > 0) {
bool ignore = false;
if (What && What->testStatus(Property::NoModify)) {
ignore = true;
}
if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
if (What) {
FC_LOG((ignore ? "ignore" : "auto")
<< " transaction (" << line << ") '" << What->getFullName());
}
else {
FC_LOG((ignore ? "ignore" : "auto") << " transaction (" << line << ") '"
<< name << "' in " << getName());
}
}
if (!ignore) {
_openTransaction(name, tid);
}
return;
}
}
if (!pcDelObj) {
return;
}
// When the object is going to be deleted we have to check if it has already been added
// to the undo transactions
std::list<Transaction*>::iterator it;
for (it = mUndoTransactions.begin(); it != mUndoTransactions.end(); ++it) {
if ((*it)->hasObject(pcDelObj)) {
_openTransaction("Delete");
break;
}
}
}
}
}
void Document::_clearRedos()
{
if (isPerformingTransaction() || d->committing) {
FC_ERR("Cannot clear redo while transacting");
return;
}
mRedoMap.clear();
while (!mRedoTransactions.empty()) {
delete mRedoTransactions.back();
mRedoTransactions.pop_back();
}
}
void Document::commitTransaction()
{
if (isPerformingTransaction() || d->committing) {
if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
FC_WARN("Cannot commit transaction while transacting");
}
return;
}
if (d->activeUndoTransaction) {
GetApplication().closeActiveTransaction(false, d->activeUndoTransaction->getID());
}
}
void Document::_commitTransaction(bool notify)
{
if (isPerformingTransaction()) {
if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
FC_WARN("Cannot commit transaction while transacting");
}
return;
}
else if (d->committing) {
// for a recursive call return without printing a warning
return;
}
if (d->activeUndoTransaction) {
Base::FlagToggler<> flag(d->committing);
Application::TransactionSignaller signaller(false, true);
int id = d->activeUndoTransaction->getID();
mUndoTransactions.push_back(d->activeUndoTransaction);
d->activeUndoTransaction = nullptr;
// check the stack for the limits
if (mUndoTransactions.size() > d->UndoMaxStackSize) {
mUndoMap.erase(mUndoTransactions.front()->getID());
delete mUndoTransactions.front();
mUndoTransactions.pop_front();
}
signalCommitTransaction(*this);
// closeActiveTransaction() may call again _commitTransaction()
if (notify) {
GetApplication().closeActiveTransaction(false, id);
}
}
}
void Document::abortTransaction()
{
if (isPerformingTransaction() || d->committing) {
if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
FC_WARN("Cannot abort transaction while transacting");
}
return;
}
if (d->activeUndoTransaction) {
GetApplication().closeActiveTransaction(true, d->activeUndoTransaction->getID());
}
}
void Document::_abortTransaction()
{
if (isPerformingTransaction() || d->committing) {
if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
FC_WARN("Cannot abort transaction while transacting");
}
}
if (d->activeUndoTransaction) {
Base::FlagToggler<bool> flag(d->rollback);
Application::TransactionSignaller signaller(true, true);
// applying the so far made changes
d->activeUndoTransaction->apply(*this, false);
// destroy the undo
mUndoMap.erase(d->activeUndoTransaction->getID());
delete d->activeUndoTransaction;
d->activeUndoTransaction = nullptr;
signalAbortTransaction(*this);
}
}
bool Document::hasPendingTransaction() const
{
if (d->activeUndoTransaction) {
return true;
}
else {
return false;
}
}
int Document::getTransactionID(bool undo, unsigned pos) const
{
if (undo) {
if (d->activeUndoTransaction) {
if (pos == 0) {
return d->activeUndoTransaction->getID();
}
--pos;
}
if (pos >= mUndoTransactions.size()) {
return 0;
}
auto rit = mUndoTransactions.rbegin();
for (; pos; ++rit, --pos) {
continue;
}
return (*rit)->getID();
}
if (pos >= mRedoTransactions.size()) {
return 0;
}
auto rit = mRedoTransactions.rbegin();
for (; pos; ++rit, --pos) {}
return (*rit)->getID();
}
bool Document::isTransactionEmpty() const
{
if (d->activeUndoTransaction) {
// Transactions are now only created when there are actual changes.
// Empty transaction is now significant for marking external changes. It
// is used to match ID with transactions in external documents and
// trigger undo/redo there.
// return d->activeUndoTransaction->isEmpty();
return false;
}
return true;
}
void Document::clearDocument()
{
d->activeObject = nullptr;
if (!d->objectArray.empty()) {
GetApplication().signalDeleteDocument(*this);
d->clearDocument();
GetApplication().signalNewDocument(*this, false);
}
Base::FlagToggler<> flag(globalIsRestoring, false);
setStatus(Document::PartialDoc, false);
d->clearRecomputeLog();
d->objectLabelManager.clear();
d->objectArray.clear();
d->objectMap.clear();
d->objectNameManager.clear();
d->objectIdMap.clear();
d->lastObjectId = 0;
}
void Document::clearUndos()
{
if (isPerformingTransaction() || d->committing) {
FC_ERR("Cannot clear undos while transacting");
return;
}
if (d->activeUndoTransaction) {
_commitTransaction(true);
}
mUndoMap.clear();
// When cleaning up the undo stack we must delete the transactions from front
// to back because a document object can appear in several transactions but
// once removed from the document the object can never ever appear in any later
// transaction. Since the document object may be also deleted when the transaction
// is deleted we must make sure not access an object once it's destroyed. Thus, we
// go from front to back and not the other way round.
while (!mUndoTransactions.empty()) {
delete mUndoTransactions.front();
mUndoTransactions.pop_front();
}
// while (!mUndoTransactions.empty()) {
// delete mUndoTransactions.back();
// mUndoTransactions.pop_back();
// }
_clearRedos();
}
int Document::getAvailableUndos(int id) const
{
if (id) {
auto it = mUndoMap.find(id);
if (it == mUndoMap.end()) {
return 0;
}
int i = 0;
if (d->activeUndoTransaction) {
++i;
if (d->activeUndoTransaction->getID() == id) {
return i;
}
}
auto rit = mUndoTransactions.rbegin();
for (; rit != mUndoTransactions.rend() && *rit != it->second; ++rit) {
++i;
}
assert(rit != mUndoTransactions.rend());
return i + 1;
}
if (d->activeUndoTransaction) {
return static_cast<int>(mUndoTransactions.size() + 1);
}
else {
return static_cast<int>(mUndoTransactions.size());
}
}
int Document::getAvailableRedos(int id) const
{
if (id) {
auto it = mRedoMap.find(id);
if (it == mRedoMap.end()) {
return 0;
}
int i = 0;
for (auto rit = mRedoTransactions.rbegin(); *rit != it->second; ++rit) {
++i;
}
assert(i < (int)mRedoTransactions.size());
return i + 1;
}
return static_cast<int>(mRedoTransactions.size());
}
void Document::setUndoMode(int iMode)
{
if (d->iUndoMode && !iMode) {
clearUndos();
}
d->iUndoMode = iMode;
}
int Document::getUndoMode() const
{
return d->iUndoMode;
}
unsigned int Document::getUndoMemSize() const
{
return d->UndoMemSize;
}
void Document::setUndoLimit(unsigned int UndoMemSize)
{
d->UndoMemSize = UndoMemSize;
}
void Document::setMaxUndoStackSize(unsigned int UndoMaxStackSize)
{
d->UndoMaxStackSize = UndoMaxStackSize;
}
unsigned int Document::getMaxUndoStackSize() const
{
return d->UndoMaxStackSize;
}
void Document::onBeforeChange(const Property* prop)
{
if (prop == &Label) {
oldLabel = Label.getValue();
}
signalBeforeChange(*this, *prop);
}
void Document::onChanged(const Property* prop)
{
signalChanged(*this, *prop);
// the Name property is a label for display purposes
if (prop == &Label) {
Base::FlagToggler<> flag(globalIsRelabeling);
App::GetApplication().signalRelabelDocument(*this);
}
else if (prop == &ShowHidden) {
App::GetApplication().signalShowHidden(*this);
}
else if (prop == &Uid) {
std::string new_dir =
getTransientDirectoryName(this->Uid.getValueStr(), this->FileName.getStrValue());
std::string old_dir = this->TransientDir.getStrValue();
Base::FileInfo TransDirNew(new_dir);
Base::FileInfo TransDirOld(old_dir);
// this directory should not exist
if (!TransDirNew.exists()) {
if (TransDirOld.exists()) {
if (!TransDirOld.renameFile(new_dir.c_str())) {
Base::Console().Warning("Failed to rename '%s' to '%s'\n",
old_dir.c_str(),
new_dir.c_str());
}
else {
this->TransientDir.setValue(new_dir);
}
}
else {
if (!TransDirNew.createDirectories()) {
Base::Console().Warning("Failed to create '%s'\n", new_dir.c_str());
}
else {
this->TransientDir.setValue(new_dir);
}
}
}
// when reloading an existing document the transient directory doesn't change
// so we must avoid to generate a new uuid
else if (TransDirNew.filePath() != TransDirOld.filePath()) {
// make sure that the uuid is unique
std::string uuid = this->Uid.getValueStr();
Base::Uuid id;
Base::Console().Warning("Document with the UUID '%s' already exists, change to '%s'\n",
uuid.c_str(),
id.getValue().c_str());
// recursive call of onChanged()
this->Uid.setValue(id);
}
}
else if (prop == &UseHasher) {
for (auto obj : d->objectArray) {
auto geofeature = freecad_cast<GeoFeature*>(obj);
if (geofeature && geofeature->getPropertyOfGeometry()) {
geofeature->enforceRecompute();
}
}
}
}
void Document::onBeforeChangeProperty(const TransactionalObject* Who, const Property* What)
{
if (Who->isDerivedFrom<App::DocumentObject>()) {
signalBeforeChangeObject(*static_cast<const App::DocumentObject*>(Who), *What);
}
if (!d->rollback && !globalIsRelabeling) {
_checkTransaction(nullptr, What, __LINE__);
if (d->activeUndoTransaction) {
d->activeUndoTransaction->addObjectChange(Who, What);
}
}
}
void Document::onChangedProperty(const DocumentObject* Who, const Property* What)
{
signalChangedObject(*Who, *What);
}
void Document::setTransactionMode(int iMode)
{
d->iTransactionMode = iMode;
}
//--------------------------------------------------------------------------
// constructor
//--------------------------------------------------------------------------
Document::Document(const char* documentName)
: myName(documentName)
{
// Remark: In a constructor we should never increment a Python object as we cannot be sure
// if the Python interpreter gets a reference of it. E.g. if we increment but Python don't
// get a reference then the object wouldn't get deleted in the destructor.
// So, we must increment only if the interpreter gets a reference.
// Remark: We force the document Python object to own the DocumentPy instance, thus we don't
// have to care about ref counting any more.
setAutoCreated(false);
d = new DocumentP;
Base::PyGILStateLocker lock;
d->DocumentPythonObject = Py::Object(new DocumentPy(this), true);
#ifdef FC_LOGUPDATECHAIN
Console().Log("+App::Document: %p\n", this);
#endif
std::string CreationDateString = Base::Tools::currentDateTimeString();
std::string Author = App::GetApplication()
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
->GetASCII("prefAuthor", "");
std::string AuthorComp =
App::GetApplication()
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
->GetASCII("prefCompany", "");
ADD_PROPERTY_TYPE(Label, ("Unnamed"), 0, Prop_None, "The name of the document");
ADD_PROPERTY_TYPE(FileName,
(""),
0,
PropertyType(Prop_Transient | Prop_ReadOnly),
"The path to the file where the document is saved to");
ADD_PROPERTY_TYPE(CreatedBy, (Author.c_str()), 0, Prop_None, "The creator of the document");
ADD_PROPERTY_TYPE(CreationDate,
(CreationDateString.c_str()),
0,
Prop_ReadOnly,
"Date of creation");
ADD_PROPERTY_TYPE(LastModifiedBy, (""), 0, Prop_None, 0);
ADD_PROPERTY_TYPE(LastModifiedDate, ("Unknown"), 0, Prop_ReadOnly, "Date of last modification");
ADD_PROPERTY_TYPE(Company,
(AuthorComp.c_str()),
0,
Prop_None,
"Additional tag to save the name of the company");
ADD_PROPERTY_TYPE(UnitSystem, (""), 0, Prop_None, "Unit system to use in this project");
// Set up the possible enum values for the unit system
int num = static_cast<int>(Base::UnitSystem::NumUnitSystemTypes);
std::vector<std::string> enumValsAsVector;
for (int i = 0; i < num; i++) {
QString item = Base::UnitsApi::getDescription(static_cast<Base::UnitSystem>(i));
enumValsAsVector.emplace_back(item.toStdString());
}
UnitSystem.setEnums(enumValsAsVector);
// Get the preferences/General unit system as the default for a new document
ParameterGrp::handle hGrpu =
App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Units");
UnitSystem.setValue(hGrpu->GetInt("UserSchema", 0));
ADD_PROPERTY_TYPE(Comment, (""), 0, Prop_None, "Additional tag to save a comment");
ADD_PROPERTY_TYPE(Meta, (), 0, Prop_None, "Map with additional meta information");
ADD_PROPERTY_TYPE(Material, (), 0, Prop_None, "Map with material properties");
// create the uuid for the document
Base::Uuid id;
ADD_PROPERTY_TYPE(Id, (""), 0, Prop_None, "ID of the document");
ADD_PROPERTY_TYPE(Uid, (id), 0, Prop_ReadOnly, "UUID of the document");
// license stuff
auto paramGrp {App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Document")};
auto index = static_cast<int>(paramGrp->GetInt("prefLicenseType", 0));
const char* name = "";
const char* url = "";
std::string licenseUrl = "";
if (index >= 0 && index < App::countOfLicenses) {
name = App::licenseItems.at(index).at(App::posnOfFullName);
url = App::licenseItems.at(index).at(App::posnOfUrl);
licenseUrl = (paramGrp->GetASCII("prefLicenseUrl", url));
}
ADD_PROPERTY_TYPE(License, (name), 0, Prop_None, "License string of the Item");
ADD_PROPERTY_TYPE(LicenseURL,
(licenseUrl.c_str()),
0,
Prop_None,
"URL to the license text/contract");
ADD_PROPERTY_TYPE(ShowHidden,
(false),
0,
PropertyType(Prop_None),
"Whether to show hidden object items in the tree view");
ADD_PROPERTY_TYPE(UseHasher,
(true),
0,
PropertyType(Prop_Hidden),
"Whether to use hasher on topological naming");
// this creates and sets 'TransientDir' in onChanged()
ADD_PROPERTY_TYPE(TransientDir,
(""),
0,
PropertyType(Prop_Transient | Prop_ReadOnly),
"Transient directory, where the files live while the document is open");
ADD_PROPERTY_TYPE(Tip,
(nullptr),
0,
PropertyType(Prop_Transient),
"Link of the tip object of the document");
ADD_PROPERTY_TYPE(TipName,
(""),
0,
PropertyType(Prop_Hidden | Prop_ReadOnly),
"Link of the tip object of the document");
Uid.touch();
}
Document::~Document()
{
#ifdef FC_LOGUPDATECHAIN
Console().Log("-App::Document: %s %p\n", getName(), this);
#endif
try {
clearUndos();
}
catch (const boost::exception&) {
}
#ifdef FC_LOGUPDATECHAIN
Console().Log("-Delete Features of %s \n", getName());
#endif
d->clearDocument();
// 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 doesn't need to be
// destructed right now because the interpreter can own several references to it.
Base::PyGILStateLocker lock;
Base::PyObjectBase* doc = static_cast<Base::PyObjectBase*>(d->DocumentPythonObject.ptr());
// Call before decrementing the reference counter, otherwise a heap error can occur
doc->setInvalid();
// remove Transient directory
try {
Base::FileInfo TransDir(TransientDir.getValue());
TransDir.deleteDirectoryRecursive();
}
catch (const Base::Exception& e) {
std::cerr << "Removing transient directory failed: " << e.what() << std::endl;
}
delete d;
}
std::string Document::getTransientDirectoryName(const std::string& uuid,
const std::string& filename) const
{
// Create a directory name of the form: {ExeName}_Doc_{UUID}_{HASH}_{PID}
std::stringstream out;
QCryptographicHash hash(QCryptographicHash::Sha1);
#if QT_VERSION < QT_VERSION_CHECK(6, 3, 0)
hash.addData(filename.c_str(), filename.size());
#else
hash.addData(QByteArrayView(filename.c_str(), filename.size()));
#endif
out << App::Application::getUserCachePath() << App::Application::getExecutableName() << "_Doc_"
<< uuid << "_" << hash.result().toHex().left(6).constData() << "_"
<< App::Application::applicationPid();
return out.str();
}
//--------------------------------------------------------------------------
// Exported functions
//--------------------------------------------------------------------------
void Document::Save(Base::Writer& writer) const
{
d->hashers.clear();
addStringHasher(d->Hasher);
writer.Stream() << R"(<Document SchemaVersion="4" ProgramVersion=")"
<< App::Application::Config()["BuildVersionMajor"] << "."
<< App::Application::Config()["BuildVersionMinor"] << "R"
<< App::Application::Config()["BuildRevision"] << "\" FileVersion=\""
<< writer.getFileVersion() << "\" StringHasher=\"1\">\n";
writer.incInd();
d->Hasher->setPersistenceFileName("StringHasher.Table");
for (auto o : d->objectArray) {
o->beforeSave();
}
beforeSave();
d->Hasher->Save(writer);
writer.decInd();
PropertyContainer::Save(writer);
// writing the features types
writeObjects(d->objectArray, writer);
writer.Stream() << "</Document>" << endl;
}
void Document::Restore(Base::XMLReader& reader)
{
int i, Cnt;
d->hashers.clear();
d->touchedObjs.clear();
addStringHasher(d->Hasher);
setStatus(Document::PartialDoc, false);
reader.readElement("Document");
long scheme = reader.getAttributeAsInteger("SchemaVersion");
reader.DocumentSchema = scheme;
if (reader.hasAttribute("ProgramVersion")) {
reader.ProgramVersion = reader.getAttribute("ProgramVersion");
}
else {
reader.ProgramVersion = "pre-0.14";
}
if (reader.hasAttribute("FileVersion")) {
reader.FileVersion = reader.getAttributeAsUnsigned("FileVersion");
}
else {
reader.FileVersion = 0;
}
if (reader.hasAttribute("StringHasher")) {
d->Hasher->Restore(reader);
}
else {
d->Hasher->clear();
}
// When this document was created the FileName and Label properties
// were set to the absolute path or file name, respectively. To save
// the document to the file it was loaded from or to show the file name
// in the tree view we must restore them after loading the file because
// they will be overridden.
// Note: This does not affect the internal name of the document in any way
// that is kept in Application.
std::string FilePath = FileName.getValue();
std::string DocLabel = Label.getValue();
// read the Document Properties, when reading in Uid the transient directory gets renamed
// automatically
PropertyContainer::Restore(reader);
// We must restore the correct 'FileName' property again because the stored
// value could be invalid.
FileName.setValue(FilePath.c_str());
Label.setValue(DocLabel.c_str());
// SchemeVersion "2"
if (scheme == 2) {
// read the feature types
reader.readElement("Features");
Cnt = reader.getAttributeAsInteger("Count");
for (i = 0; i < Cnt; i++) {
reader.readElement("Feature");
string type = reader.getAttribute("type");
string name = reader.getAttribute("name");
try {
addObject(type.c_str(), name.c_str(), /*isNew=*/false);
}
catch (Base::Exception&) {
Base::Console().Message("Cannot create object '%s'\n", name.c_str());
}
}
reader.readEndElement("Features");
// read the features itself
reader.readElement("FeatureData");
Cnt = reader.getAttributeAsInteger("Count");
for (i = 0; i < Cnt; i++) {
reader.readElement("Feature");
string name = reader.getAttribute("name");
DocumentObject* pObj = getObject(name.c_str());
if (pObj) { // check if this feature has been registered
pObj->setStatus(ObjectStatus::Restore, true);
pObj->Restore(reader);
pObj->setStatus(ObjectStatus::Restore, false);
}
reader.readEndElement("Feature");
}
reader.readEndElement("FeatureData");
} // SchemeVersion "3" or higher
else if (scheme >= 3) {
// read the feature types
readObjects(reader);
// tip object handling. First the whole document has to be read, then we
// can restore the Tip link out of the TipName Property:
Tip.setValue(getObject(TipName.getValue()));
}
reader.readEndElement("Document");
}
void DocumentP::checkStringHasher(const Base::XMLReader& reader)
{
if (reader.hasReadFailed("StringHasher.Table.txt")) {
Base::Console().Error(QT_TRANSLATE_NOOP(
"Notifications",
"\nIt is recommended that the user right-click the root of "
"the document and select Mark to recompute.\n"
"The user should then click the Refresh button in the main toolbar.\n"));
}
}
std::pair<bool, int> Document::addStringHasher(const StringHasherRef& hasher) const
{
if (!hasher) {
return std::make_pair(false, 0);
}
auto ret =
d->hashers.left.insert(HasherMap::left_map::value_type(hasher, (int)d->hashers.size()));
if (ret.second) {
hasher->clearMarks();
}
return std::make_pair(ret.second, ret.first->second);
}
StringHasherRef Document::getStringHasher(int idx) const
{
StringHasherRef hasher;
if (idx < 0) {
if (UseHasher.getValue()) {
return d->Hasher;
}
return hasher;
}
auto it = d->hashers.right.find(idx);
if (it == d->hashers.right.end()) {
hasher = new StringHasher;
d->hashers.right.insert(HasherMap::right_map::value_type(idx, hasher));
}
else {
hasher = it->second;
}
return hasher;
}
struct DocExportStatus
{
Document::ExportStatus status;
std::set<const App::DocumentObject*> objs;
};
static DocExportStatus _ExportStatus;
// Exception-safe exporting status setter
class DocumentExporting
{
public:
explicit DocumentExporting(const std::vector<App::DocumentObject*>& objs)
{
_ExportStatus.status = Document::Exporting;
_ExportStatus.objs.insert(objs.begin(), objs.end());
}
~DocumentExporting()
{
_ExportStatus.status = Document::NotExporting;
_ExportStatus.objs.clear();
}
};
// The current implementation choose to use a static variable for exporting
// status because we can be exporting multiple objects from multiple documents
// at the same time. I see no benefits in distinguish which documents are
// exporting, so just use a static variable for global status. But the
// implementation can easily be changed here if necessary.
Document::ExportStatus Document::isExporting(const App::DocumentObject* obj) const
{
if (_ExportStatus.status != Document::NotExporting
&& (!obj || _ExportStatus.objs.find(obj) != _ExportStatus.objs.end())) {
return _ExportStatus.status;
}
return Document::NotExporting;
}
void Document::exportObjects(const std::vector<App::DocumentObject*>& obj, std::ostream& out)
{
DocumentExporting exporting(obj);
d->hashers.clear();
if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
for (auto o : obj) {
if (o && o->isAttachedToDocument()) {
FC_LOG("exporting " << o->getFullName());
if (!o->getPropertyByName("_ObjectUUID")) {
auto prop = static_cast<PropertyUUID*>(
o->addDynamicProperty("App::PropertyUUID",
"_ObjectUUID",
nullptr,
nullptr,
Prop_Output | Prop_Hidden));
prop->setValue(Base::Uuid::createUuid());
}
}
}
}
Base::ZipWriter writer(out);
writer.putNextEntry("Document.xml");
writer.Stream() << "<?xml version='1.0' encoding='utf-8'?>" << endl;
writer.Stream() << R"(<Document SchemaVersion="4" ProgramVersion=")"
<< App::Application::Config()["BuildVersionMajor"] << "."
<< App::Application::Config()["BuildVersionMinor"] << "R"
<< App::Application::Config()["BuildRevision"] << R"(" FileVersion="1">)"
<< endl;
// Add this block to have the same layout as for normal documents
writer.Stream() << "<Properties Count=\"0\">" << endl;
writer.Stream() << "</Properties>" << endl;
// writing the object types
writeObjects(obj, writer);
writer.Stream() << "</Document>" << endl;
// Hook for others to add further data.
signalExportObjects(obj, writer);
// write additional files
writer.writeFiles();
d->hashers.clear();
}
#define FC_ATTR_DEPENDENCIES "Dependencies"
#define FC_ELEMENT_OBJECT_DEPS "ObjectDeps"
#define FC_ATTR_DEP_COUNT "Count"
#define FC_ATTR_DEP_OBJ_NAME "Name"
#define FC_ATTR_DEP_ALLOW_PARTIAL "AllowPartial"
#define FC_ELEMENT_OBJECT_DEP "Dep"
void Document::writeObjects(const std::vector<App::DocumentObject*>& obj,
Base::Writer& writer) const
{
// writing the features types
writer.incInd(); // indentation for 'Objects count'
writer.Stream() << writer.ind() << "<Objects Count=\"" << obj.size();
if (!isExporting(nullptr)) {
writer.Stream() << "\" " FC_ATTR_DEPENDENCIES "=\"1";
}
writer.Stream() << "\">" << endl;
writer.incInd(); // indentation for 'Object type'
if (!isExporting(nullptr)) {
for (auto o : obj) {
const auto& outList =
o->getOutList(DocumentObject::OutListNoHidden | DocumentObject::OutListNoXLinked);
writer.Stream() << writer.ind()
<< "<" FC_ELEMENT_OBJECT_DEPS " " FC_ATTR_DEP_OBJ_NAME "=\""
<< o->getNameInDocument() << "\" " FC_ATTR_DEP_COUNT "=\""
<< outList.size();
if (outList.empty()) {
writer.Stream() << "\"/>" << endl;
continue;
}
int partial = o->canLoadPartial();
if (partial > 0) {
writer.Stream() << "\" " FC_ATTR_DEP_ALLOW_PARTIAL << "=\"" << partial;
}
writer.Stream() << "\">" << endl;
writer.incInd();
for (auto dep : outList) {
auto name = dep ? dep->getNameInDocument() : "";
writer.Stream() << writer.ind()
<< "<" FC_ELEMENT_OBJECT_DEP " " FC_ATTR_DEP_OBJ_NAME "=\""
<< (name ? name : "") << "\"/>" << endl;
}
writer.decInd();
writer.Stream() << writer.ind() << "</" FC_ELEMENT_OBJECT_DEPS ">" << endl;
}
}
std::vector<DocumentObject*>::const_iterator it;
for (it = obj.begin(); it != obj.end(); ++it) {
writer.Stream() << writer.ind() << "<Object "
<< "type=\"" << (*it)->getTypeId().getName() << "\" "
<< "name=\"" << (*it)->getExportName() << "\" "
<< "id=\"" << (*it)->getID() << "\" ";
// Only write out custom view provider types
std::string viewType = (*it)->getViewProviderNameStored();
if (viewType != (*it)->getViewProviderName()) {
writer.Stream() << "ViewType=\"" << viewType << "\" ";
}
// See DocumentObjectPy::getState
if ((*it)->testStatus(ObjectStatus::Touch)) {
writer.Stream() << "Touched=\"1\" ";
}
if ((*it)->testStatus(ObjectStatus::Error)) {
writer.Stream() << "Invalid=\"1\" ";
auto desc = getErrorDescription(*it);
if (desc) {
writer.Stream() << "Error=\"" << Property::encodeAttribute(desc) << "\" ";
}
}
writer.Stream() << "/>" << endl;
}
writer.decInd(); // indentation for 'Object type'
writer.Stream() << writer.ind() << "</Objects>" << endl;
// writing the features itself
writer.Stream() << writer.ind() << "<ObjectData Count=\"" << obj.size() << "\">" << endl;
writer.incInd(); // indentation for 'Object name'
for (it = obj.begin(); it != obj.end(); ++it) {
writer.Stream() << writer.ind() << "<Object name=\"" << (*it)->getExportName() << "\"";
if ((*it)->hasExtensions()) {
writer.Stream() << " Extensions=\"True\"";
}
writer.Stream() << ">" << endl;
(*it)->Save(writer);
writer.Stream() << writer.ind() << "</Object>" << endl;
}
writer.decInd(); // indentation for 'Object name'
writer.Stream() << writer.ind() << "</ObjectData>" << endl;
writer.decInd(); // indentation for 'Objects count'
}
struct DepInfo
{
std::unordered_set<std::string> deps;
int canLoadPartial = 0;
};
static void _loadDeps(const std::string& name,
std::unordered_map<std::string, bool>& objs,
const std::unordered_map<std::string, DepInfo>& deps)
{
auto it = deps.find(name);
if (it == deps.end()) {
objs.emplace(name, true);
return;
}
if (it->second.canLoadPartial) {
if (it->second.canLoadPartial == 1) {
// canLoadPartial==1 means all its children will be created but not
// restored, i.e. exists as if newly created object, and therefore no
// need to load dependency of the children
for (auto& dep : it->second.deps) {
objs.emplace(dep, false);
}
objs.emplace(name, true);
}
else {
objs.emplace(name, false);
}
return;
}
objs[name] = true;
// If cannot load partial, then recurse to load all children dependency
for (auto& dep : it->second.deps) {
auto it = objs.find(dep);
if (it != objs.end() && it->second) {
continue;
}
_loadDeps(dep, objs, deps);
}
}
std::vector<App::DocumentObject*> Document::readObjects(Base::XMLReader& reader)
{
d->touchedObjs.clear();
bool keepDigits = testStatus(Document::KeepTrailingDigits);
setStatus(Document::KeepTrailingDigits, !reader.doNameMapping());
std::vector<App::DocumentObject*> objs;
// read the object types
reader.readElement("Objects");
int Cnt = reader.getAttributeAsInteger("Count");
if (!reader.hasAttribute(FC_ATTR_DEPENDENCIES)) {
d->partialLoadObjects.clear();
}
else if (!d->partialLoadObjects.empty()) {
std::unordered_map<std::string, DepInfo> deps;
for (int i = 0; i < Cnt; i++) {
reader.readElement(FC_ELEMENT_OBJECT_DEPS);
int dcount = reader.getAttributeAsInteger(FC_ATTR_DEP_COUNT);
if (!dcount) {
continue;
}
auto& info = deps[reader.getAttribute(FC_ATTR_DEP_OBJ_NAME)];
if (reader.hasAttribute(FC_ATTR_DEP_ALLOW_PARTIAL)) {
info.canLoadPartial = reader.getAttributeAsInteger(FC_ATTR_DEP_ALLOW_PARTIAL);
}
for (int j = 0; j < dcount; ++j) {
reader.readElement(FC_ELEMENT_OBJECT_DEP);
const char* name = reader.getAttribute(FC_ATTR_DEP_OBJ_NAME);
if (!Base::Tools::isNullOrEmpty(name)) {
info.deps.insert(name);
}
}
reader.readEndElement(FC_ELEMENT_OBJECT_DEPS);
}
std::vector<std::string> objs;
objs.reserve(d->partialLoadObjects.size());
for (auto& v : d->partialLoadObjects) {
objs.emplace_back(v.first.c_str());
}
for (auto& name : objs) {
_loadDeps(name, d->partialLoadObjects, deps);
}
if (Cnt > (int)d->partialLoadObjects.size()) {
setStatus(Document::PartialDoc, true);
}
else {
for (auto& v : d->partialLoadObjects) {
if (!v.second) {
setStatus(Document::PartialDoc, true);
break;
}
}
if (!testStatus(Document::PartialDoc)) {
d->partialLoadObjects.clear();
}
}
}
long lastId = 0;
for (int i = 0; i < Cnt; i++) {
reader.readElement("Object");
std::string type = reader.getAttribute("type");
std::string name = reader.getAttribute("name");
std::string viewType =
reader.hasAttribute("ViewType") ? reader.getAttribute("ViewType") : "";
bool partial = false;
if (!d->partialLoadObjects.empty()) {
auto it = d->partialLoadObjects.find(name);
if (it == d->partialLoadObjects.end()) {
continue;
}
partial = !it->second;
}
if (!testStatus(Status::Importing) && reader.hasAttribute("id")) {
// if not importing, then temporary reset lastObjectId and make the
// following addObject() generate the correct id for this object.
d->lastObjectId = reader.getAttributeAsInteger("id") - 1;
}
// To prevent duplicate name when export/import of objects from
// external documents, we append those external object name with
// @<document name>. Before importing (here means we are called by
// importObjects), we shall strip the postfix. What the caller
// (MergeDocument) sees is still the unstripped name mapped to a new
// internal name, and the rest of the link properties will be able to
// correctly unmap the names.
auto pos = name.find('@');
std::string _obj_name;
const char* obj_name;
if (pos != std::string::npos) {
_obj_name = name.substr(0, pos);
obj_name = _obj_name.c_str();
}
else {
obj_name = name.c_str();
}
try {
// Use name from XML as is and do NOT remove trailing digits because
// otherwise we may cause a dependency to itself
// Example: Object 'Cut001' references object 'Cut' and removing the
// digits we make an object 'Cut' referencing itself.
App::DocumentObject* obj =
addObject(type.c_str(), obj_name, /*isNew=*/false, viewType.c_str(), partial);
if (obj) {
if (lastId < obj->_Id) {
lastId = obj->_Id;
}
objs.push_back(obj);
// use this name for the later access because an object with
// the given name may already exist
reader.addName(name.c_str(), obj->getNameInDocument());
// restore touch/error status flags
if (reader.hasAttribute("Touched")) {
if (reader.getAttributeAsInteger("Touched") != 0) {
d->touchedObjs.insert(obj);
}
}
if (reader.hasAttribute("Invalid")) {
obj->setStatus(ObjectStatus::Error,
reader.getAttributeAsInteger("Invalid") != 0);
if (obj->isError() && reader.hasAttribute("Error")) {
d->addRecomputeLog(reader.getAttribute("Error"), obj);
}
}
}
}
catch (const Base::Exception& e) {
Base::Console().Error("Cannot create object '%s': (%s)\n", name.c_str(), e.what());
}
}
if (!testStatus(Status::Importing)) {
d->lastObjectId = lastId;
}
reader.readEndElement("Objects");
setStatus(Document::KeepTrailingDigits, keepDigits);
// read the features itself
reader.clearPartialRestoreDocumentObject();
reader.readElement("ObjectData");
Cnt = reader.getAttributeAsInteger("Count");
for (int i = 0; i < Cnt; i++) {
reader.readElement("Object");
std::string name = reader.getName(reader.getAttribute("name"));
DocumentObject* pObj = getObject(name.c_str());
if (pObj
&& !pObj->testStatus(
App::PartialObject)) { // check if this feature has been registered
pObj->setStatus(ObjectStatus::Restore, true);
try {
FC_TRACE("restoring " << pObj->getFullName());
pObj->Restore(reader);
}
// Try to continue only for certain exception types if not handled
// by the feature type. For all other exception types abort the process.
catch (const Base::UnicodeError& e) {
e.ReportException();
}
catch (const Base::ValueError& e) {
e.ReportException();
}
catch (const Base::IndexError& e) {
e.ReportException();
}
catch (const Base::RuntimeError& e) {
e.ReportException();
}
catch (const Base::XMLAttributeError& e) {
e.ReportException();
}
pObj->setStatus(ObjectStatus::Restore, false);
if (reader.testStatus(Base::XMLReader::ReaderStatus::PartialRestoreInDocumentObject)) {
Base::Console().Error("Object \"%s\" was subject to a partial restore. As a result "
"geometry may have changed or be incomplete.\n",
name.c_str());
reader.clearPartialRestoreDocumentObject();
}
}
reader.readEndElement("Object");
}
reader.readEndElement("ObjectData");
return objs;
}
void Document::addRecomputeObject(DocumentObject* obj)
{
if (testStatus(Status::Restoring) && obj) {
setStatus(Status::RecomputeOnRestore, true);
d->touchedObjs.insert(obj);
obj->touch();
}
}
std::vector<App::DocumentObject*> Document::importObjects(Base::XMLReader& reader)
{
d->hashers.clear();
Base::FlagToggler<> flag(globalIsRestoring, false);
Base::ObjectStatusLocker<Status, Document> restoreBit(Status::Restoring, this);
Base::ObjectStatusLocker<Status, Document> restoreBit2(Status::Importing, this);
ExpressionParser::ExpressionImporter expImporter(reader);
reader.readElement("Document");
long scheme = reader.getAttributeAsInteger("SchemaVersion");
reader.DocumentSchema = scheme;
if (reader.hasAttribute("ProgramVersion")) {
reader.ProgramVersion = reader.getAttribute("ProgramVersion");
}
else {
reader.ProgramVersion = "pre-0.14";
}
if (reader.hasAttribute("FileVersion")) {
reader.FileVersion = reader.getAttributeAsUnsigned("FileVersion");
}
else {
reader.FileVersion = 0;
}
std::vector<App::DocumentObject*> objs = readObjects(reader);
for (auto o : objs) {
if (o && o->isAttachedToDocument()) {
o->setStatus(App::ObjImporting, true);
FC_LOG("importing " << o->getFullName());
if (auto propUUID =
freecad_cast<PropertyUUID*>(o->getPropertyByName("_ObjectUUID"))) {
auto propSource =
freecad_cast<PropertyUUID*>(o->getPropertyByName("_SourceUUID"));
if (!propSource) {
propSource = static_cast<PropertyUUID*>(
o->addDynamicProperty("App::PropertyUUID",
"_SourceUUID",
nullptr,
nullptr,
Prop_Output | Prop_Hidden));
}
if (propSource) {
propSource->setValue(propUUID->getValue());
}
propUUID->setValue(Base::Uuid::createUuid());
}
}
}
reader.readEndElement("Document");
signalImportObjects(objs, reader);
afterRestore(objs, true);
signalFinishImportObjects(objs);
for (auto o : objs) {
if (o && o->isAttachedToDocument()) {
o->setStatus(App::ObjImporting, false);
}
}
d->hashers.clear();
return objs;
}
unsigned int Document::getMemSize() const
{
unsigned int size = 0;
// size of the DocObjects in the document
std::vector<DocumentObject*>::const_iterator it;
for (it = d->objectArray.begin(); it != d->objectArray.end(); ++it) {
size += (*it)->getMemSize();
}
size += d->Hasher->getMemSize();
// size of the document properties...
size += PropertyContainer::getMemSize();
// Undo Redo size
size += getUndoMemSize();
return size;
}
static std::string checkFileName(const char* file)
{
std::string fn(file);
// Append extension if missing. This option is added for security reason, so
// that the user won't accidentally overwrite other file that may be critical.
if (App::GetApplication()
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
->GetBool("CheckExtension", true)) {
const char* ext = strrchr(file, '.');
if (!ext || !boost::iequals(ext + 1, "fcstd")) {
if (ext && ext[1] == 0) {
fn += "FCStd";
}
else {
fn += ".FCStd";
}
}
}
return fn;
}
bool Document::saveAs(const char* _file)
{
std::string file = checkFileName(_file);
Base::FileInfo fi(file.c_str());
if (this->FileName.getStrValue() != file) {
this->FileName.setValue(file);
this->Label.setValue(fi.fileNamePure());
this->Uid.touch(); // this forces a rename of the transient directory
}
return save();
}
bool Document::saveCopy(const char* _file) const
{
std::string file = checkFileName(_file);
if (this->FileName.getStrValue() != file) {
bool result = saveToFile(file.c_str());
return result;
}
return false;
}
// Save the document under the name it has been opened
bool Document::save()
{
if (testStatus(Document::PartialDoc)) {
FC_ERR("Partial loaded document '" << Label.getValue() << "' cannot be saved");
// TODO We don't make this a fatal error and return 'true' to make it possible to
// save other documents that depends on this partial opened document. We need better
// handling to avoid touching partial documents.
return true;
}
if (*(FileName.getValue()) != '\0') {
// Save the name of the tip object in order to handle in Restore()
if (Tip.getValue()) {
TipName.setValue(Tip.getValue()->getNameInDocument());
}
std::string LastModifiedDateString = Base::Tools::currentDateTimeString();
LastModifiedDate.setValue(LastModifiedDateString.c_str());
// set author if needed
bool saveAuthor =
App::GetApplication()
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
->GetBool("prefSetAuthorOnSave", false);
if (saveAuthor) {
std::string Author =
App::GetApplication()
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
->GetASCII("prefAuthor", "");
LastModifiedBy.setValue(Author.c_str());
}
return saveToFile(FileName.getValue());
}
return false;
}
namespace App
{
// Helper class to handle different backup policies
class BackupPolicy
{
public:
enum Policy
{
Standard,
TimeStamp
};
BackupPolicy()
{
policy = Standard;
numberOfFiles = 1;
useFCBakExtension = true;
saveBackupDateFormat = "%Y%m%d-%H%M%S";
}
~BackupPolicy() = default;
void setPolicy(Policy p)
{
policy = p;
}
void setNumberOfFiles(int count)
{
numberOfFiles = count;
}
void useBackupExtension(bool on)
{
useFCBakExtension = on;
}
void setDateFormat(const std::string& fmt)
{
saveBackupDateFormat = fmt;
}
void apply(const std::string& sourcename, const std::string& targetname)
{
switch (policy) {
case Standard:
applyStandard(sourcename, targetname);
break;
case TimeStamp:
applyTimeStamp(sourcename, targetname);
break;
}
}
private:
void applyStandard(const std::string& sourcename, const std::string& targetname)
{
// if saving the project data succeeded rename to the actual file name
Base::FileInfo fi(targetname);
if (fi.exists()) {
if (numberOfFiles > 0) {
int nSuff = 0;
std::string fn = fi.fileName();
Base::FileInfo di(fi.dirPath());
std::vector<Base::FileInfo> backup;
std::vector<Base::FileInfo> files = di.getDirectoryContent();
for (const Base::FileInfo& it : files) {
std::string file = it.fileName();
if (file.substr(0, fn.length()) == fn) {
// starts with the same file name
std::string suf(file.substr(fn.length()));
if (!suf.empty()) {
std::string::size_type nPos = suf.find_first_not_of("0123456789");
if (nPos == std::string::npos) {
// store all backup files
backup.push_back(it);
nSuff = std::max<int>(nSuff, std::atol(suf.c_str()));
}
}
}
}
if (!backup.empty() && (int)backup.size() >= numberOfFiles) {
// delete the oldest backup file we found
Base::FileInfo del = backup.front();
for (const Base::FileInfo& it : backup) {
if (it.lastModified() < del.lastModified()) {
del = it;
}
}
del.deleteFile();
fn = del.filePath();
}
else {
// create a new backup file
std::stringstream str;
str << fi.filePath() << (nSuff + 1);
fn = str.str();
}
if (!fi.renameFile(fn.c_str())) {
Base::Console().Warning("Cannot rename project file to backup file\n");
}
}
else {
fi.deleteFile();
}
}
Base::FileInfo tmp(sourcename);
if (!tmp.renameFile(targetname.c_str())) {
throw Base::FileException("Cannot rename tmp save file to project file",
Base::FileInfo(targetname));
}
}
void applyTimeStamp(const std::string& sourcename, const std::string& targetname)
{
Base::FileInfo fi(targetname);
std::string fn = sourcename;
std::string ext = fi.extension();
std::string bn; // full path with no extension but with "."
std::string pbn; // base name of the project + "."
if (!ext.empty()) {
bn = fi.filePath().substr(0, fi.filePath().length() - ext.length());
pbn = fi.fileName().substr(0, fi.fileName().length() - ext.length());
}
else {
bn = fi.filePath() + ".";
pbn = fi.fileName() + ".";
}
bool backupManagementError = false; // Note error and report at the end
if (fi.exists()) {
if (numberOfFiles > 0) {
// replace . by - in format to avoid . between base name and extension
boost::replace_all(saveBackupDateFormat, ".", "-");
{
// Remove all extra backups
std::string fn = fi.fileName();
Base::FileInfo di(fi.dirPath());
std::vector<Base::FileInfo> backup;
std::vector<Base::FileInfo> files = di.getDirectoryContent();
for (const Base::FileInfo& it : files) {
if (it.isFile()) {
std::string file = it.fileName();
std::string fext = it.extension();
std::string fextUp = fext;
std::transform(fextUp.begin(),
fextUp.end(),
fextUp.begin(),
(int (*)(int))toupper);
// re-enforcing identification of the backup file
// old case : the name starts with the full name of the project and
// follows with numbers
if ((startsWith(file, fn) && (file.length() > fn.length())
&& checkDigits(file.substr(fn.length())))
||
// .FCBak case : The bame starts with the base name of the project +
// "."
// + complement with no "." + ".FCBak"
((fextUp == "FCBAK") && startsWith(file, pbn)
&& (checkValidComplement(file, pbn, fext)))) {
backup.push_back(it);
}
}
}
if (!backup.empty() && (int)backup.size() >= numberOfFiles) {
std::sort(backup.begin(), backup.end(), fileComparisonByDate);
// delete the oldest backup file we found
// Base::FileInfo del = backup.front();
int nb = 0;
for (Base::FileInfo& it : backup) {
nb++;
if (nb >= numberOfFiles) {
try {
if (!it.deleteFile()) {
backupManagementError = true;
Base::Console().Warning("Cannot remove backup file : %s\n",
it.fileName().c_str());
}
}
catch (...) {
backupManagementError = true;
Base::Console().Warning("Cannot remove backup file : %s\n",
it.fileName().c_str());
}
}
}
}
} // end remove backup
// create a new backup file
{
int ext = 1;
if (useFCBakExtension) {
std::stringstream str;
Base::TimeInfo ti = fi.lastModified();
time_t s = ti.getTime_t();
struct tm* timeinfo = localtime(&s);
char buffer[100];
strftime(buffer, sizeof(buffer), saveBackupDateFormat.c_str(), timeinfo);
str << bn << buffer;
fn = str.str();
bool done = false;
if ((fn.empty()) || (fn[fn.length() - 1] == ' ')
|| (fn[fn.length() - 1] == '-')) {
if (fn[fn.length() - 1] == ' ') {
fn = fn.substr(0, fn.length() - 1);
}
}
else {
if (!renameFileNoErase(fi, fn + ".FCBak")) {
fn = fn + "-";
}
else {
done = true;
}
}
if (!done) {
while (ext < numberOfFiles + 10) {
if (renameFileNoErase(fi, fn + std::to_string(ext) + ".FCBak")) {
break;
}
ext++;
}
}
}
else {
// changed but simpler and solves also the delay sometimes introduced by
// google drive
while (ext < numberOfFiles + 10) {
// linux just replace the file if exists, and then the existence is to
// be tested before rename
if (renameFileNoErase(fi, fi.filePath() + std::to_string(ext))) {
break;
}
ext++;
}
}
if (ext >= numberOfFiles + 10) {
Base::Console().Error(
"File not saved: Cannot rename project file to backup file\n");
// throw Base::FileException("File not saved: Cannot rename project file to
// backup file", fi);
}
}
}
else {
try {
fi.deleteFile();
}
catch (...) {
Base::Console().Warning("Cannot remove backup file: %s\n",
fi.fileName().c_str());
backupManagementError = true;
}
}
}
Base::FileInfo tmp(sourcename);
if (!tmp.renameFile(targetname.c_str())) {
throw Base::FileException(
"Save interrupted: Cannot rename temporary file to project file",
tmp);
}
if (backupManagementError) {
throw Base::FileException(
"Warning: Save complete, but error while managing backup history.",
fi);
}
}
static bool fileComparisonByDate(const Base::FileInfo& i, const Base::FileInfo& j)
{
return (i.lastModified() > j.lastModified());
}
bool startsWith(const std::string& st1, const std::string& st2) const
{
return st1.substr(0, st2.length()) == st2;
}
bool checkValidString(const std::string& cmpl, const boost::regex& e) const
{
boost::smatch what;
bool res = boost::regex_search(cmpl, what, e);
return res;
}
bool checkValidComplement(const std::string& file,
const std::string& pbn,
const std::string& ext) const
{
std::string cmpl =
file.substr(pbn.length(), file.length() - pbn.length() - ext.length() - 1);
boost::regex e(R"(^[^.]*$)");
return checkValidString(cmpl, e);
}
bool checkDigits(const std::string& cmpl) const
{
boost::regex e(R"(^[0-9]*$)");
return checkValidString(cmpl, e);
}
bool renameFileNoErase(Base::FileInfo fi, const std::string& newName)
{
// linux just replaces the file if it exists, so the existence is to be tested before rename
Base::FileInfo nf(newName);
if (!nf.exists()) {
return fi.renameFile(newName.c_str());
}
return false;
}
private:
Policy policy;
int numberOfFiles;
bool useFCBakExtension;
std::string saveBackupDateFormat;
};
} // namespace App
bool Document::saveToFile(const char* filename) const
{
signalStartSave(*this, filename);
auto hGrp = App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Document");
int compression = hGrp->GetInt("CompressionLevel", 7);
compression = Base::clamp<int>(compression, Z_NO_COMPRESSION, Z_BEST_COMPRESSION);
bool policy = App::GetApplication()
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
->GetBool("BackupPolicy", true);
auto canonical_path = [](const char* filename) {
try {
#ifdef FC_OS_WIN32
QString utf8Name = QString::fromUtf8(filename);
auto realpath = fs::weakly_canonical(fs::absolute(fs::path(utf8Name.toStdWString())));
std::string nativePath = QString::fromStdWString(realpath.native()).toStdString();
#else
auto realpath = fs::weakly_canonical(fs::absolute(fs::path(filename)));
std::string nativePath = realpath.native();
#endif
// In case some folders in the path do not exist
auto parentPath = realpath.parent_path();
fs::create_directories(parentPath);
return nativePath;
}
catch (const std::exception&) {
#ifdef FC_OS_WIN32
QString utf8Name = QString::fromUtf8(filename);
auto parentPath = fs::absolute(fs::path(utf8Name.toStdWString())).parent_path();
#else
auto parentPath = fs::absolute(fs::path(filename)).parent_path();
#endif
fs::create_directories(parentPath);
return std::string(filename);
}
};
// realpath is canonical filename i.e. without symlink
std::string nativePath = canonical_path(filename);
// make a tmp. file where to save the project data first and then rename to
// the actual file name. This may be useful if overwriting an existing file
// fails so that the data of the work up to now isn't lost.
std::string uuid = Base::Uuid::createUuid();
std::string fn = nativePath;
if (policy) {
fn += ".";
fn += uuid;
}
Base::FileInfo tmp(fn);
// open extra scope to close ZipWriter properly
{
Base::ofstream file(tmp, std::ios::out | std::ios::binary);
Base::ZipWriter writer(file);
if (!file.is_open()) {
throw Base::FileException("Failed to open file", tmp);
}
writer.setComment("FreeCAD Document");
writer.setLevel(compression);
writer.putNextEntry("Document.xml");
if (hGrp->GetBool("SaveBinaryBrep", false)) {
writer.setMode("BinaryBrep");
}
writer.Stream() << "<?xml version='1.0' encoding='utf-8'?>" << endl
<< "<!--" << endl
<< " FreeCAD Document, see https://www.freecad.org for more information..."
<< endl
<< "-->" << endl;
Document::Save(writer);
// Special handling for Gui document.
signalSaveDocument(writer);
// write additional files
writer.writeFiles();
if (writer.hasErrors()) {
// retrieve Writer error strings
std::stringstream message;
message << "Failed to write all data to file ";
message << writer.getErrors().front();
throw Base::FileException(message.str().c_str(), tmp);
}
GetApplication().signalSaveDocument(*this);
}
if (policy) {
// if saving the project data succeeded rename to the actual file name
int count_bak = App::GetApplication()
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
->GetInt("CountBackupFiles", 1);
bool backup = App::GetApplication()
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
->GetBool("CreateBackupFiles", true);
if (!backup) {
count_bak = -1;
}
bool useFCBakExtension =
App::GetApplication()
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
->GetBool("UseFCBakExtension", true);
std::string saveBackupDateFormat =
App::GetApplication()
.GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document")
->GetASCII("SaveBackupDateFormat", "%Y%m%d-%H%M%S");
BackupPolicy policy;
if (useFCBakExtension) {
policy.setPolicy(BackupPolicy::TimeStamp);
policy.useBackupExtension(useFCBakExtension);
policy.setDateFormat(saveBackupDateFormat);
}
else {
policy.setPolicy(BackupPolicy::Standard);
}
policy.setNumberOfFiles(count_bak);
policy.apply(fn, nativePath);
}
signalFinishSave(*this, filename);
return true;
}
void Document::registerLabel(const std::string& newLabel)
{
if (!newLabel.empty()) {
d->objectLabelManager.addExactName(newLabel);
}
}
void Document::unregisterLabel(const std::string& oldLabel)
{
if (!oldLabel.empty()) {
d->objectLabelManager.removeExactName(oldLabel);
}
}
bool Document::containsLabel(const std::string& label)
{
return d->objectLabelManager.containsName(label);
}
std::string Document::makeUniqueLabel(const std::string& modelLabel)
{
if (modelLabel.empty()) {
return {};
}
return d->objectLabelManager.makeUniqueName(modelLabel, 3);
}
bool Document::isAnyRestoring()
{
return globalIsRestoring;
}
// Open the document
void Document::restore(const char* filename,
bool delaySignal,
const std::vector<std::string>& objNames)
{
clearUndos();
d->activeObject = nullptr;
bool signal = false;
Document* activeDoc = GetApplication().getActiveDocument();
if (!d->objectArray.empty()) {
signal = true;
GetApplication().signalDeleteDocument(*this);
d->clearDocument();
}
Base::FlagToggler<> flag(globalIsRestoring, false);
setStatus(Document::PartialDoc, false);
d->clearRecomputeLog();
d->objectLabelManager.clear();
d->objectArray.clear();
d->objectNameManager.clear();
d->objectMap.clear();
d->objectIdMap.clear();
d->lastObjectId = 0;
if (signal) {
GetApplication().signalNewDocument(*this, true);
if (activeDoc == this) {
GetApplication().setActiveDocument(this);
}
}
if (!filename) {
filename = FileName.getValue();
}
Base::FileInfo fi(filename);
Base::ifstream file(fi, std::ios::in | std::ios::binary);
std::streambuf* buf = file.rdbuf();
std::streamoff size = buf->pubseekoff(0, std::ios::end, std::ios::in);
buf->pubseekoff(0, std::ios::beg, std::ios::in);
if (size < 22) { // an empty zip archive has 22 bytes
throw Base::FileException("Invalid project file", filename);
}
zipios::ZipInputStream zipstream(file);
Base::XMLReader reader(filename, zipstream);
if (!reader.isValid()) {
throw Base::FileException("Error reading compression file", filename);
}
GetApplication().signalStartRestoreDocument(*this);
setStatus(Document::Restoring, true);
d->partialLoadObjects.clear();
for (auto& name : objNames) {
d->partialLoadObjects.emplace(name, true);
}
try {
Document::Restore(reader);
}
catch (const Base::Exception& e) {
Base::Console().Error("Invalid Document.xml: %s\n", e.what());
setStatus(Document::RestoreError, true);
}
d->partialLoadObjects.clear();
d->programVersion = reader.ProgramVersion;
// Special handling for Gui document, the view representations must already
// exist, what is done in Restore().
// Note: This file doesn't need to be available if the document has been created
// without GUI. But if available then follow after all data files of the App document.
signalRestoreDocument(reader);
reader.readFiles(zipstream);
DocumentP::checkStringHasher(reader);
if (reader.testStatus(Base::XMLReader::ReaderStatus::PartialRestore)) {
setStatus(Document::PartialRestore, true);
Base::Console().Error("There were errors while loading the file. Some data might have been "
"modified or not recovered at all. Look above for more specific "
"information about the objects involved.\n");
}
if (!delaySignal) {
afterRestore(true);
}
}
bool Document::afterRestore(bool checkPartial)
{
Base::FlagToggler<> flag(globalIsRestoring, false);
if (!afterRestore(d->objectArray, checkPartial)) {
FC_WARN("Reload partial document " << getName());
GetApplication().signalPendingReloadDocument(*this);
return false;
}
GetApplication().signalFinishRestoreDocument(*this);
setStatus(Document::Restoring, false);
return true;
}
bool Document::afterRestore(const std::vector<DocumentObject*>& objArray, bool checkPartial)
{
checkPartial = checkPartial && testStatus(Document::PartialDoc);
if (checkPartial && !d->touchedObjs.empty()) {
return false;
}
// 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 we add the
// Property::afterRestore() interface to let them sort it out. Note, this
// API is not called in object dedpenency order, because the order
// information is not ready yet.
std::map<DocumentObject*, std::vector<App::Property*>> propMap;
for (auto obj : objArray) {
auto& props = propMap[obj];
obj->getPropertyList(props);
for (auto prop : props) {
try {
prop->afterRestore();
}
catch (const Base::Exception& e) {
FC_ERR("Failed to restore " << obj->getFullName() << '.' << prop->getName() << ": "
<< e.what());
}
}
}
if (checkPartial && !d->touchedObjs.empty()) {
// partial document touched, signal full reload
return false;
}
std::set<DocumentObject*> objSet(objArray.begin(), objArray.end());
auto objs = getDependencyList(objArray.empty() ? d->objectArray : objArray, DepSort);
for (auto obj : objs) {
if (objSet.find(obj) == objSet.end()) {
continue;
}
try {
for (auto prop : propMap[obj]) {
prop->onContainerRestored();
}
bool touched = false;
auto returnCode =
obj->ExpressionEngine.execute(PropertyExpressionEngine::ExecuteOnRestore, &touched);
if (returnCode != DocumentObject::StdReturn) {
FC_ERR("Expression engine failed to restore " << obj->getFullName() << ": "
<< returnCode->Why);
d->addRecomputeLog(returnCode);
}
obj->onDocumentRestored();
if (touched) {
d->touchedObjs.insert(obj);
}
}
catch (const Base::Exception& e) {
d->addRecomputeLog(e.what(), obj);
FC_ERR("Failed to restore " << obj->getFullName() << ": " << e.what());
}
catch (std::exception& e) {
d->addRecomputeLog(e.what(), obj);
FC_ERR("Failed to restore " << obj->getFullName() << ": " << e.what());
}
catch (...) {
d->addRecomputeLog("Unknown exception on restore", obj);
FC_ERR("Failed to restore " << obj->getFullName() << ": " << "unknown exception");
}
if (obj->isValid()) {
auto& props = propMap[obj];
props.clear();
// refresh properties in case the object changes its property list
obj->getPropertyList(props);
for (auto prop : props) {
auto link = freecad_cast<PropertyLinkBase*>(prop);
int res;
std::string errMsg;
if (link && (res = link->checkRestore(&errMsg))) {
d->touchedObjs.insert(obj);
if (res == 1 || checkPartial) {
FC_WARN(obj->getFullName() << '.' << prop->getName() << ": " << errMsg);
setStatus(Document::LinkStampChanged, true);
if (checkPartial) {
return false;
}
}
else {
FC_ERR(obj->getFullName() << '.' << prop->getName() << ": " << errMsg);
d->addRecomputeLog(errMsg, obj);
setStatus(Document::PartialRestore, true);
}
}
}
}
if (checkPartial && !d->touchedObjs.empty()) {
// partial document touched, signal full reload
return false;
}
else if (!d->touchedObjs.contains(obj)) {
obj->purgeTouched();
}
signalFinishRestoreObject(*obj);
}
d->touchedObjs.clear();
return true;
}
bool Document::isSaved() const
{
std::string name = FileName.getValue();
return !name.empty();
}
/** Label is the visible name of a document shown e.g. in the windows title
* or in the tree view. The label almost (but not always e.g. if you manually change it)
* matches with the file name where the document is stored to.
* In contrast to Label the method getName() returns the internal name of the document that only
* matches with Label when loading or creating a document because then both are set to the same
* value. Since the internal name cannot be changed during runtime it must differ from the Label
* after saving the document the first time or saving it under a new file name.
* @ note More than one document can have the same label name.
* @ note The internal is always guaranteed to be unique because @ref Application::newDocument()
* checks for a document with the same name and makes it unique if needed. Hence you cannot rely on
* that the internal name matches with the name you passed to Application::newDoument(). You should
* use the method getName() instead.
*/
const char* Document::getName() const
{
// return GetApplication().getDocumentName(this);
return myName.c_str();
}
std::string Document::getFullName() const
{
return myName;
}
void Document::setAutoCreated(bool value) {
autoCreated = value;
}
bool Document::isAutoCreated() const {
return autoCreated;
}
const char* Document::getProgramVersion() const
{
return d->programVersion.c_str();
}
const char* Document::getFileName() const
{
return testStatus(TempDoc) ? TransientDir.getValue() : FileName.getValue();
}
/// Remove all modifications. After this call The document becomes valid again.
void Document::purgeTouched()
{
for (auto It : d->objectArray) {
It->purgeTouched();
}
}
bool Document::isTouched() const
{
for (auto It : d->objectArray) {
if (It->isTouched()) {
return true;
}
}
return false;
}
vector<DocumentObject*> Document::getTouched() const
{
vector<DocumentObject*> result;
for (auto It : d->objectArray) {
if (It->isTouched()) {
result.push_back(It);
}
}
return result;
}
void Document::setClosable(bool c)
{
setStatus(Document::Closable, c);
}
bool Document::isClosable() const
{
return testStatus(Document::Closable);
}
int Document::countObjects() const
{
return static_cast<int>(d->objectArray.size());
}
void Document::getLinksTo(std::set<DocumentObject*>& links,
const DocumentObject* obj,
int options,
int maxCount,
const std::vector<DocumentObject*>& objs) const
{
std::map<const App::DocumentObject*, std::vector<App::DocumentObject*>> linkMap;
for (auto o : !objs.empty() ? objs : d->objectArray) {
if (o == obj) {
continue;
}
auto linked = o;
if (options & GetLinkArrayElement) {
linked = o->getLinkedObject(false);
}
else {
auto ext = o->getExtensionByType<LinkBaseExtension>(true);
if (ext) {
linked = ext->getTrueLinkedObject(false, nullptr, 0, true);
}
else {
linked = o->getLinkedObject(false);
}
}
if (linked && linked != o) {
if (options & GetLinkRecursive) {
linkMap[linked].push_back(o);
}
else if (linked == obj || !obj) {
if ((options & GetLinkExternal) && linked->getDocument() == o->getDocument()) {
continue;
}
else if (options & GetLinkedObject) {
links.insert(linked);
}
else {
links.insert(o);
}
if (maxCount && maxCount <= (int)links.size()) {
return;
}
}
}
}
if (!(options & GetLinkRecursive)) {
return;
}
std::vector<const DocumentObject*> current(1, obj);
for (int depth = 0; !current.empty(); ++depth) {
if (!GetApplication().checkLinkDepth(depth, MessageOption::Error)) {
break;
}
std::vector<const DocumentObject*> next;
for (const App::DocumentObject* o : current) {
auto iter = linkMap.find(o);
if (iter == linkMap.end()) {
continue;
}
for (App::DocumentObject* link : iter->second) {
if (links.insert(link).second) {
if (maxCount && maxCount <= (int)links.size()) {
return;
}
next.push_back(link);
}
}
}
current = std::move(next);
}
return;
}
bool Document::hasLinksTo(const DocumentObject* obj) const
{
std::set<DocumentObject*> links;
getLinksTo(links, obj, 0, 1);
return !links.empty();
}
std::vector<App::DocumentObject*> Document::getInList(const DocumentObject* me) const
{
// result list
std::vector<App::DocumentObject*> result;
// go through all objects
for (const auto& It : d->objectMap) {
// get the outList and search if me is in that list
std::vector<DocumentObject*> OutList = It.second->getOutList();
for (auto obj : OutList) {
if (obj && obj == me) {
// add the parent object
result.push_back(It.second);
}
}
}
return result;
}
// This function unifies the old _rebuildDependencyList() and
// getDependencyList(). The algorithm basically obtains the object dependency
// by recrusivly visiting the OutList of each object in the given object array.
// It makes sure to call getOutList() of each object once and only once, which
// makes it much more efficient than calling getRecursiveOutList() on each
// individual object.
//
// The problem with the original algorithm is that, it assumes the objects
// inside any OutList are all within the given object array, so it does not
// recursively call getOutList() on those dependent objects inside. This
// assumption is broken by the introduction of PropertyXLink which can link to
// external object.
//
static void _buildDependencyList(const std::vector<App::DocumentObject*>& objectArray,
int options,
std::vector<App::DocumentObject*>* depObjs,
DependencyList* depList,
std::map<DocumentObject*, Vertex>* objectMap,
bool* touchCheck = nullptr)
{
std::map<DocumentObject*, std::vector<DocumentObject*>> outLists;
std::deque<DocumentObject*> objs;
if (objectMap) {
objectMap->clear();
}
if (depList) {
depList->clear();
}
int op = (options & Document::DepNoXLinked) ? DocumentObject::OutListNoXLinked : 0;
for (auto obj : objectArray) {
objs.push_back(obj);
while (!objs.empty()) {
auto obj = objs.front();
objs.pop_front();
if (!obj || !obj->isAttachedToDocument()) {
continue;
}
auto it = outLists.find(obj);
if (it != outLists.end()) {
continue;
}
if (touchCheck) {
if (obj->isTouched() || obj->mustExecute()) {
// early termination on touch check
*touchCheck = true;
return;
}
}
if (depObjs) {
depObjs->push_back(obj);
}
if (objectMap && depList) {
(*objectMap)[obj] = add_vertex(*depList);
}
auto& outList = outLists[obj];
outList = obj->getOutList(op);
objs.insert(objs.end(), outList.begin(), outList.end());
}
}
if (objectMap && depList) {
for (const auto& v : outLists) {
for (auto obj : v.second) {
if (obj && obj->isAttachedToDocument()) {
add_edge((*objectMap)[v.first], (*objectMap)[obj], *depList);
}
}
}
}
}
std::vector<App::DocumentObject*>
Document::getDependencyList(const std::vector<App::DocumentObject*>& objectArray, int options)
{
std::vector<App::DocumentObject*> ret;
if (!(options & DepSort)) {
_buildDependencyList(objectArray, options, &ret, nullptr, nullptr);
return ret;
}
DependencyList depList;
std::map<DocumentObject*, Vertex> objectMap;
std::map<Vertex, DocumentObject*> vertexMap;
_buildDependencyList(objectArray, options, nullptr, &depList, &objectMap);
for (auto& v : objectMap) {
vertexMap[v.second] = v.first;
}
std::list<Vertex> make_order;
try {
boost::topological_sort(depList, std::front_inserter(make_order));
}
catch (const std::exception& e) {
if (options & DepNoCycle) {
// Use boost::strong_components to find cycles. It groups strongly
// connected vertices as components, and therefore each component
// forms a cycle.
std::vector<int> c(vertexMap.size());
std::map<int, std::vector<Vertex>> components;
boost::strong_components(
depList,
boost::make_iterator_property_map(c.begin(),
boost::get(boost::vertex_index, depList),
c[0]));
for (size_t i = 0; i < c.size(); ++i) {
components[c[i]].push_back(i);
}
FC_ERR("Dependency cycles: ");
std::ostringstream ss;
ss << std::endl;
for (auto& v : components) {
if (v.second.size() == 1) {
// For components with only one member, we still need to
// check if there it is self looping.
auto it = vertexMap.find(v.second[0]);
if (it == vertexMap.end()) {
continue;
}
// Try search the object in its own out list
for (auto obj : it->second->getOutList()) {
if (obj == it->second) {
ss << std::endl << it->second->getFullName() << std::endl;
break;
}
}
continue;
}
// For components with more than one member, they form a loop together
for (size_t i = 0; i < v.second.size(); ++i) {
auto it = vertexMap.find(v.second[i]);
if (it == vertexMap.end()) {
continue;
}
if (i % 6 == 0) {
ss << std::endl;
}
ss << it->second->getFullName() << ", ";
}
ss << std::endl;
}
FC_ERR(ss.str());
FC_THROWM(Base::RuntimeError, e.what());
}
FC_ERR(e.what());
ret = DocumentP::partialTopologicalSort(objectArray);
std::reverse(ret.begin(), ret.end());
return ret;
}
for (std::list<Vertex>::reverse_iterator i = make_order.rbegin(); i != make_order.rend(); ++i) {
ret.push_back(vertexMap[*i]);
}
return ret;
}
std::vector<App::Document*> Document::getDependentDocuments(bool sort)
{
return getDependentDocuments({this}, sort);
}
std::vector<App::Document*> Document::getDependentDocuments(std::vector<App::Document*> pending,
bool sort)
{
DependencyList depList;
std::map<Document*, Vertex> docMap;
std::map<Vertex, Document*> vertexMap;
std::vector<App::Document*> ret;
if (pending.empty()) {
return ret;
}
auto outLists = PropertyXLink::getDocumentOutList();
std::set<App::Document*> docs;
docs.insert(pending.begin(), pending.end());
if (sort) {
for (auto doc : pending) {
docMap[doc] = add_vertex(depList);
}
}
while (!pending.empty()) {
auto doc = pending.back();
pending.pop_back();
auto it = outLists.find(doc);
if (it == outLists.end()) {
continue;
}
auto& vertex = docMap[doc];
for (auto depDoc : it->second) {
if (docs.insert(depDoc).second) {
pending.push_back(depDoc);
if (sort) {
docMap[depDoc] = add_vertex(depList);
}
}
add_edge(vertex, docMap[depDoc], depList);
}
}
if (!sort) {
ret.insert(ret.end(), docs.begin(), docs.end());
return ret;
}
std::list<Vertex> make_order;
try {
boost::topological_sort(depList, std::front_inserter(make_order));
}
catch (const std::exception& e) {
std::string msg("Document::getDependentDocuments: ");
msg += e.what();
throw Base::RuntimeError(msg);
}
for (auto& v : docMap) {
vertexMap[v.second] = v.first;
}
for (auto rIt = make_order.rbegin(); rIt != make_order.rend(); ++rIt) {
ret.push_back(vertexMap[*rIt]);
}
return ret;
}
void Document::_rebuildDependencyList(const std::vector<App::DocumentObject*>& objs)
{
(void)objs;
}
/**
* @brief Signal that object identifiers, typically a property or document object has been renamed.
*
* This function iterates through all document object in the document, and calls its
* renameObjectIdentifiers functions.
*
* @param paths Map with current and new names
*/
void Document::renameObjectIdentifiers(
const std::map<App::ObjectIdentifier, App::ObjectIdentifier>& paths,
const std::function<bool(const App::DocumentObject*)>& selector)
{
std::map<App::ObjectIdentifier, App::ObjectIdentifier> extendedPaths;
std::map<App::ObjectIdentifier, App::ObjectIdentifier>::const_iterator it = paths.begin();
while (it != paths.end()) {
extendedPaths[it->first.canonicalPath()] = it->second.canonicalPath();
++it;
}
for (auto it : d->objectArray) {
if (selector(it)) {
it->renameObjectIdentifiers(extendedPaths);
}
}
}
int Document::recompute(const std::vector<App::DocumentObject*>& objs,
bool force,
bool* hasError,
int options)
{
ZoneScoped;
if (d->undoing || d->rollback) {
if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
FC_WARN("Ignore document recompute on undo/redo");
}
return 0;
}
int objectCount = 0;
if (testStatus(Document::PartialDoc)) {
if (mustExecute()) {
FC_WARN("Please reload partial document '" << Label.getValue()
<< "' for recomputation.");
}
return 0;
}
if (testStatus(Document::Recomputing)) {
// this is clearly a bug in the calling instance
FC_ERR("Recursive calling of recompute for document " << getName());
return 0;
}
// The 'SkipRecompute' flag can be (tmp.) set to avoid too many
// time expensive recomputes
if (!force && testStatus(Document::SkipRecompute)) {
signalSkipRecompute(*this, objs);
return 0;
}
// delete recompute log
d->clearRecomputeLog();
FC_TIME_INIT(t);
Base::ObjectStatusLocker<Document::Status, Document> exe(Document::Recomputing, this);
signalBeforeRecompute(*this);
#if 0
//////////////////////////////////////////////////////////////////////////
// FIXME Comment by Realthunder:
// the topologicalSrot() below cannot handle partial recompute, haven't got
// time to figure out the code yet, simply use back boost::topological_sort
// for now, that is, rely on getDependencyList() to do the sorting. The
// downside is, it didn't take advantage of the ready built InList, nor will
// it report for cyclic dependency.
//////////////////////////////////////////////////////////////////////////
// get the sorted vector of all dependent objects and go though it from the end
auto depObjs = getDependencyList(objs.empty()?d->objectArray:objs);
vector<DocumentObject*> topoSortedObjects = topologicalSort(depObjs);
if (topoSortedObjects.size() != depObjs.size()){
cerr << "App::Document::recompute(): cyclic dependency detected" << endl;
topoSortedObjects = d->partialTopologicalSort(depObjs);
}
std::reverse(topoSortedObjects.begin(),topoSortedObjects.end());
#else
auto topoSortedObjects =
getDependencyList(objs.empty() ? d->objectArray : objs, DepSort | options);
#endif
for (auto obj : topoSortedObjects) {
obj->setStatus(ObjectStatus::PendingRecompute, true);
}
ParameterGrp::handle hGrp =
GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Document");
bool canAbort = hGrp->GetBool("CanAbortRecompute", true);
std::set<App::DocumentObject*> filter;
size_t idx = 0;
FC_TIME_INIT(t2);
try {
// maximum two passes to allow some form of dependency inversion
for (int passes = 0; passes < 2 && idx < topoSortedObjects.size(); ++passes) {
std::unique_ptr<Base::SequencerLauncher> seq;
if (canAbort) {
seq = std::make_unique<Base::SequencerLauncher>("Recompute...",
topoSortedObjects.size());
}
FC_LOG("Recompute pass " << passes);
for (; idx < topoSortedObjects.size(); ++idx) {
auto obj = topoSortedObjects[idx];
if (!obj->isAttachedToDocument() || filter.find(obj) != filter.end()) {
continue;
}
// ask the object if it should be recomputed
bool doRecompute = false;
if (obj->mustRecompute()) {
doRecompute = true;
++objectCount;
int res = _recomputeFeature(obj);
if (res) {
if (hasError) {
*hasError = true;
}
if (res < 0) {
passes = 2;
break;
}
// if something happened filter all object in its
// inListRecursive from the queue then proceed
obj->getInListEx(filter, true);
filter.insert(obj);
continue;
}
}
if (obj->isTouched() || doRecompute) {
signalRecomputedObject(*obj);
obj->purgeTouched();
// set all dependent object touched to force recompute
for (auto inObjIt : obj->getInList()) {
inObjIt->enforceRecompute();
}
}
if (seq) {
seq->next(true);
}
}
// check if all objects are recomputed but still thouched
for (size_t i = 0; i < topoSortedObjects.size(); ++i) {
auto obj = topoSortedObjects[i];
obj->setStatus(ObjectStatus::Recompute2, false);
if (!filter.contains(obj) && obj->isTouched()) {
if (passes > 0) {
FC_ERR(obj->getFullName() << " still touched after recompute");
}
else {
FC_LOG(obj->getFullName() << " still touched after recompute");
if (idx >= topoSortedObjects.size()) {
// let's start the next pass on the first touched object
idx = i;
}
obj->setStatus(ObjectStatus::Recompute2, true);
}
}
}
}
}
catch (Base::Exception& e) {
e.ReportException();
}
FC_TIME_LOG(t2, "Recompute");
for (auto obj : topoSortedObjects) {
if (!obj->isAttachedToDocument()) {
continue;
}
obj->setStatus(ObjectStatus::PendingRecompute, false);
obj->setStatus(ObjectStatus::Recompute2, false);
}
signalRecomputed(*this, topoSortedObjects);
FC_TIME_LOG(t, "Recompute total");
if (!d->_RecomputeLog.empty()) {
if (!testStatus(Status::IgnoreErrorOnRecompute)) {
for (auto it : topoSortedObjects) {
if (it->isError()) {
const char* text = getErrorDescription(it);
if (text) {
Base::Console().Error("%s: %s\n", it->Label.getValue(), text);
}
}
}
}
}
for (auto doc : GetApplication().getDocuments()) {
decltype(doc->d->pendingRemove) objs;
objs.swap(doc->d->pendingRemove);
for (auto& o : objs) {
try {
if (auto obj = o.getObject()) {
obj->getDocument()->removeObject(obj->getNameInDocument());
}
}
catch (Base::Exception& e) {
e.ReportException();
FC_ERR("error when removing object " << o.getDocumentName() << '#'
<< o.getObjectName());
}
}
}
return objectCount;
}
/*!
Does almost the same as topologicalSort() until no object with an input degree of zero
can be found. It then searches for objects with an output degree of zero until neither
an object with input or output degree can be found. The remaining objects form one or
multiple cycles.
An alternative to this method might be:
https://en.wikipedia.org/wiki/Tarjan%E2%80%99s_strongly_connected_components_algorithm
*/
std::vector<App::DocumentObject*>
DocumentP::partialTopologicalSort(const std::vector<App::DocumentObject*>& objects)
{
vector<App::DocumentObject*> ret;
ret.reserve(objects.size());
// pairs of input and output degree
map<App::DocumentObject*, std::pair<int, int>> countMap;
for (auto objectIt : objects) {
// we need inlist with unique entries
auto in = objectIt->getInList();
std::sort(in.begin(), in.end());
in.erase(std::unique(in.begin(), in.end()), in.end());
// we need outlist with unique entries
auto out = objectIt->getOutList();
std::sort(out.begin(), out.end());
out.erase(std::unique(out.begin(), out.end()), out.end());
countMap[objectIt] = std::make_pair(in.size(), out.size());
}
std::list<App::DocumentObject*> degIn;
std::list<App::DocumentObject*> degOut;
bool removeVertex = true;
while (removeVertex) {
removeVertex = false;
// try input degree
auto degInIt = find_if(countMap.begin(),
countMap.end(),
[](pair<App::DocumentObject*, pair<int, int>> vertex) -> bool {
return vertex.second.first == 0;
});
if (degInIt != countMap.end()) {
removeVertex = true;
degIn.push_back(degInIt->first);
degInIt->second.first = degInIt->second.first - 1;
// we need outlist with unique entries
auto out = degInIt->first->getOutList();
std::sort(out.begin(), out.end());
out.erase(std::unique(out.begin(), out.end()), out.end());
for (auto outListIt : out) {
auto outListMapIt = countMap.find(outListIt);
if (outListMapIt != countMap.end()) {
outListMapIt->second.first = outListMapIt->second.first - 1;
}
}
}
}
// make the output degree negative if input degree is negative
// to mark the vertex as processed
for (auto& countIt : countMap) {
if (countIt.second.first < 0) {
countIt.second.second = -1;
}
}
removeVertex = degIn.size() != objects.size();
while (removeVertex) {
removeVertex = false;
auto degOutIt = find_if(countMap.begin(),
countMap.end(),
[](pair<App::DocumentObject*, pair<int, int>> vertex) -> bool {
return vertex.second.second == 0;
});
if (degOutIt != countMap.end()) {
removeVertex = true;
degOut.push_front(degOutIt->first);
degOutIt->second.second = degOutIt->second.second - 1;
// we need inlist with unique entries
auto in = degOutIt->first->getInList();
std::sort(in.begin(), in.end());
in.erase(std::unique(in.begin(), in.end()), in.end());
for (auto inListIt : in) {
auto inListMapIt = countMap.find(inListIt);
if (inListMapIt != countMap.end()) {
inListMapIt->second.second = inListMapIt->second.second - 1;
}
}
}
}
// at this point we have no root object any more
for (auto countIt : countMap) {
if (countIt.second.first > 0 && countIt.second.second > 0) {
degIn.push_back(countIt.first);
}
}
ret.insert(ret.end(), degIn.begin(), degIn.end());
ret.insert(ret.end(), degOut.begin(), degOut.end());
return ret;
}
std::vector<App::DocumentObject*>
DocumentP::topologicalSort(const std::vector<App::DocumentObject*>& objects) const
{
// topological sort algorithm described here:
// https://de.wikipedia.org/wiki/Topologische_Sortierung#Algorithmus_f.C3.BCr_das_Topologische_Sortieren
vector<App::DocumentObject*> ret;
ret.reserve(objects.size());
map<App::DocumentObject*, int> countMap;
for (auto objectIt : objects) {
// We now support externally linked objects
// if(!obj->isAttachedToDocument() || obj->getDocument()!=this)
if (!objectIt->isAttachedToDocument()) {
continue;
}
// we need inlist with unique entries
auto in = objectIt->getInList();
std::sort(in.begin(), in.end());
in.erase(std::unique(in.begin(), in.end()), in.end());
countMap[objectIt] = in.size();
}
auto rootObjeIt = find_if(countMap.begin(),
countMap.end(),
[](pair<App::DocumentObject*, int> count) -> bool {
return count.second == 0;
});
if (rootObjeIt == countMap.end()) {
cerr << "Document::topologicalSort: cyclic dependency detected (no root object)" << endl;
return ret;
}
while (rootObjeIt != countMap.end()) {
rootObjeIt->second = rootObjeIt->second - 1;
// we need outlist with unique entries
auto out = rootObjeIt->first->getOutList();
std::sort(out.begin(), out.end());
out.erase(std::unique(out.begin(), out.end()), out.end());
for (auto outListIt : out) {
auto outListMapIt = countMap.find(outListIt);
if (outListMapIt != countMap.end()) {
outListMapIt->second = outListMapIt->second - 1;
}
}
ret.push_back(rootObjeIt->first);
rootObjeIt = find_if(countMap.begin(),
countMap.end(),
[](pair<App::DocumentObject*, int> count) -> bool {
return count.second == 0;
});
}
return ret;
}
std::vector<App::DocumentObject*> Document::topologicalSort() const
{
return d->topologicalSort(d->objectArray);
}
const char* Document::getErrorDescription(const App::DocumentObject* Obj) const
{
return d->findRecomputeLog(Obj);
}
// call the recompute of the Feature and handle the exceptions and errors.
int Document::_recomputeFeature(DocumentObject* Feat)
{
FC_LOG("Recomputing " << Feat->getFullName());
DocumentObjectExecReturn* returnCode = nullptr;
try {
returnCode = Feat->ExpressionEngine.execute(PropertyExpressionEngine::ExecuteNonOutput);
if (returnCode == DocumentObject::StdReturn) {
returnCode = Feat->recompute();
if (returnCode == DocumentObject::StdReturn) {
returnCode =
Feat->ExpressionEngine.execute(PropertyExpressionEngine::ExecuteOutput);
}
}
}
catch (Base::AbortException& e) {
e.ReportException();
FC_LOG("Failed to recompute " << Feat->getFullName() << ": " << e.what());
d->addRecomputeLog("User abort", Feat);
return -1;
}
catch (const Base::MemoryException& e) {
FC_ERR("Memory exception in " << Feat->getFullName() << " thrown: " << e.what());
d->addRecomputeLog("Out of memory exception", Feat);
return 1;
}
catch (Base::Exception& e) {
e.ReportException();
FC_LOG("Failed to recompute " << Feat->getFullName() << ": " << e.what());
d->addRecomputeLog(e.what(), Feat);
return 1;
}
catch (std::exception& e) {
FC_ERR("Exception in " << Feat->getFullName() << " thrown: " << e.what());
d->addRecomputeLog(e.what(), Feat);
return 1;
}
#ifndef FC_DEBUG
catch (...) {
FC_ERR("Unknown exception in " << Feat->getFullName() << " thrown");
d->addRecomputeLog("Unknown exception!", Feat);
return 1;
}
#endif
if (returnCode == DocumentObject::StdReturn) {
Feat->resetError();
}
else {
returnCode->Which = Feat;
d->addRecomputeLog(returnCode);
FC_LOG("Failed to recompute " << Feat->getFullName() << ": " << returnCode->Why);
return 1;
}
return 0;
}
bool Document::recomputeFeature(DocumentObject* feature, bool recursive)
{
// delete recompute log
d->clearRecomputeLog(feature);
// verify that the feature is (active) part of the document
if (!feature->isAttachedToDocument()) {
return false;
}
if (recursive) {
bool hasError = false;
recompute({feature}, true, &hasError);
return !hasError;
}
else {
_recomputeFeature(feature);
signalRecomputedObject(*feature);
return feature->isValid();
}
}
DocumentObject* Document::addObject(const char* sType,
const char* pObjectName,
bool isNew,
const char* viewType,
bool isPartial)
{
const Base::Type type =
Base::Type::getTypeIfDerivedFrom(sType, App::DocumentObject::getClassTypeId(), true);
if (type.isBad()) {
std::stringstream str;
str << "'" << sType << "' is not a document object type";
throw Base::TypeError(str.str());
}
void* typeInstance = type.createInstance();
if (!typeInstance) {
return nullptr;
}
auto* pcObject = static_cast<App::DocumentObject*>(typeInstance);
pcObject->setDocument(this);
// do no transactions if we do a rollback!
if (!d->rollback) {
// Undo stuff
_checkTransaction(nullptr, nullptr, __LINE__);
if (d->activeUndoTransaction) {
d->activeUndoTransaction->addObjectDel(pcObject);
}
}
// get Unique name
const bool hasName = !Base::Tools::isNullOrEmpty(pObjectName);
const string ObjectName = getUniqueObjectName(hasName ? pObjectName : type.getName());
d->activeObject = pcObject;
// insert in the name map
d->objectMap[ObjectName] = pcObject;
d->objectNameManager.addExactName(ObjectName);
// generate object id and add to id map;
pcObject->_Id = ++d->lastObjectId;
d->objectIdMap[pcObject->_Id] = pcObject;
// cache the pointer to the name string in the Object (for performance of
// DocumentObject::getNameInDocument())
pcObject->pcNameInDocument = &(d->objectMap.find(ObjectName)->first);
// insert in the vector
d->objectArray.push_back(pcObject);
// Register the current Label even though it is (probably) about to change
registerLabel(pcObject->Label.getStrValue());
// If we are restoring, don't set the Label object now; it will be restored later. This is to
// avoid potential duplicate label conflicts later.
if (!d->StatusBits.test(Restoring)) {
pcObject->Label.setValue(ObjectName);
}
// Call the object-specific initialization
if (!d->undoing && !d->rollback && isNew) {
pcObject->setupObject();
}
// mark the object as new (i.e. set status bit 2) and send the signal
pcObject->setStatus(ObjectStatus::New, true);
pcObject->setStatus(ObjectStatus::PartialObject, isPartial);
if (Base::Tools::isNullOrEmpty(viewType)) {
viewType = pcObject->getViewProviderNameOverride();
}
if (!Base::Tools::isNullOrEmpty(viewType)) {
pcObject->_pcViewProviderName = viewType;
}
signalNewObject(*pcObject);
// do no transactions if we do a rollback!
if (!d->rollback && d->activeUndoTransaction) {
signalTransactionAppend(*pcObject, d->activeUndoTransaction);
}
signalActivatedObject(*pcObject);
// return the Object
return pcObject;
}
std::vector<DocumentObject*>
Document::addObjects(const char* sType, const std::vector<std::string>& objectNames, bool isNew)
{
Base::Type type =
Base::Type::getTypeIfDerivedFrom(sType, App::DocumentObject::getClassTypeId(), true);
if (type.isBad()) {
std::stringstream str;
str << "'" << sType << "' is not a document object type";
throw Base::TypeError(str.str());
}
std::vector<DocumentObject*> objects;
objects.resize(objectNames.size());
std::generate(objects.begin(), objects.end(), [&] {
return static_cast<App::DocumentObject*>(type.createInstance());
});
// the type instance could be a null pointer, it is enough to check the first element
if (!objects.empty() && !objects[0]) {
objects.clear();
return objects;
}
for (auto it = objects.begin(); it != objects.end(); ++it) {
auto index = std::distance(objects.begin(), it);
App::DocumentObject* pcObject = *it;
pcObject->setDocument(this);
// do no transactions if we do a rollback!
if (!d->rollback) {
// Undo stuff
_checkTransaction(nullptr, nullptr, __LINE__);
if (d->activeUndoTransaction) {
d->activeUndoTransaction->addObjectDel(pcObject);
}
}
// get unique name. We don't use getUniqueObjectName because it takes a char* not a std::string
std::string ObjectName = objectNames[index];
if (ObjectName.empty()) {
ObjectName = sType;
}
ObjectName = Base::Tools::getIdentifier(ObjectName);
if (d->objectNameManager.containsName(ObjectName)) {
ObjectName = d->objectNameManager.makeUniqueName(ObjectName, 3);
}
// insert in the name map
d->objectMap[ObjectName] = pcObject;
d->objectNameManager.addExactName(ObjectName);
// generate object id and add to id map;
pcObject->_Id = ++d->lastObjectId;
d->objectIdMap[pcObject->_Id] = pcObject;
// cache the pointer to the name string in the Object (for performance of
// DocumentObject::getNameInDocument())
pcObject->pcNameInDocument = &(d->objectMap.find(ObjectName)->first);
// insert in the vector
d->objectArray.push_back(pcObject);
// Register the current Label even though it is about to change
registerLabel(pcObject->Label.getStrValue());
pcObject->Label.setValue(ObjectName);
// Call the object-specific initialization
if (!d->undoing && !d->rollback && isNew) {
pcObject->setupObject();
}
// mark the object as new (i.e. set status bit 2) and send the signal
pcObject->setStatus(ObjectStatus::New, true);
const char* viewType = pcObject->getViewProviderNameOverride();
pcObject->_pcViewProviderName = viewType ? viewType : "";
signalNewObject(*pcObject);
// do no transactions if we do a rollback!
if (!d->rollback && d->activeUndoTransaction) {
signalTransactionAppend(*pcObject, d->activeUndoTransaction);
}
}
if (!objects.empty()) {
d->activeObject = objects.back();
signalActivatedObject(*objects.back());
}
return objects;
}
void Document::addObject(DocumentObject* pcObject, const char* pObjectName)
{
if (pcObject->getDocument()) {
throw Base::RuntimeError("Document object is already added to a document");
}
pcObject->setDocument(this);
// do no transactions if we do a rollback!
if (!d->rollback) {
// Undo stuff
_checkTransaction(nullptr, nullptr, __LINE__);
if (d->activeUndoTransaction) {
d->activeUndoTransaction->addObjectDel(pcObject);
}
}
// get unique name
string ObjectName;
if (!Base::Tools::isNullOrEmpty(pObjectName)) {
ObjectName = getUniqueObjectName(pObjectName);
}
else {
ObjectName = getUniqueObjectName(pcObject->getTypeId().getName());
}
d->activeObject = pcObject;
// insert in the name map
d->objectMap[ObjectName] = pcObject;
d->objectNameManager.addExactName(ObjectName);
// generate object id and add to id map;
if (!pcObject->_Id) {
pcObject->_Id = ++d->lastObjectId;
}
d->objectIdMap[pcObject->_Id] = pcObject;
// cache the pointer to the name string in the Object (for performance of
// DocumentObject::getNameInDocument())
pcObject->pcNameInDocument = &(d->objectMap.find(ObjectName)->first);
// insert in the vector
d->objectArray.push_back(pcObject);
// Register the current Label even though it is about to change
registerLabel(pcObject->Label.getStrValue());
pcObject->Label.setValue(ObjectName);
// mark the object as new (i.e. set status bit 2) and send the signal
pcObject->setStatus(ObjectStatus::New, true);
const char* viewType = pcObject->getViewProviderNameOverride();
pcObject->_pcViewProviderName = viewType ? viewType : "";
signalNewObject(*pcObject);
// do no transactions if we do a rollback!
if (!d->rollback && d->activeUndoTransaction) {
signalTransactionAppend(*pcObject, d->activeUndoTransaction);
}
signalActivatedObject(*pcObject);
}
void Document::_addObject(DocumentObject* pcObject, const char* pObjectName)
{
std::string ObjectName = getUniqueObjectName(pObjectName);
d->objectMap[ObjectName] = pcObject;
d->objectNameManager.addExactName(ObjectName);
// generate object id and add to id map;
if (!pcObject->_Id) {
pcObject->_Id = ++d->lastObjectId;
}
d->objectIdMap[pcObject->_Id] = pcObject;
d->objectArray.push_back(pcObject);
registerLabel(pcObject->Label.getStrValue());
// cache the pointer to the name string in the Object (for performance of
// DocumentObject::getNameInDocument())
pcObject->pcNameInDocument = &(d->objectMap.find(ObjectName)->first);
// do no transactions if we do a rollback!
if (!d->rollback) {
// Undo stuff
_checkTransaction(nullptr, nullptr, __LINE__);
if (d->activeUndoTransaction) {
d->activeUndoTransaction->addObjectDel(pcObject);
}
}
const char* viewType = pcObject->getViewProviderNameOverride();
pcObject->_pcViewProviderName = viewType ? viewType : "";
// send the signal
signalNewObject(*pcObject);
// do no transactions if we do a rollback!
if (!d->rollback && d->activeUndoTransaction) {
signalTransactionAppend(*pcObject, d->activeUndoTransaction);
}
d->activeObject = pcObject;
signalActivatedObject(*pcObject);
}
bool Document::containsObject(const DocumentObject* pcObject) const
{
// We could look for the object in objectMap (keyed by object name),
// or search in objectArray (a O(n) vector search) but looking by Id
// in objectIdMap would be fastest.
auto found = d->objectIdMap.find(pcObject->getID());
return found != d->objectIdMap.end() && found->second == pcObject;
}
/// Remove an object out of the document
void Document::removeObject(const char* sName)
{
auto pos = d->objectMap.find(sName);
// name not found?
if (pos == d->objectMap.end()) {
return;
}
if (pos->second->testStatus(ObjectStatus::PendingRecompute)) {
// TODO: shall we allow removal if there is active undo transaction?
FC_MSG("pending remove of " << sName << " after recomputing document " << getName());
d->pendingRemove.emplace_back(pos->second);
return;
}
TransactionLocker tlock;
_checkTransaction(pos->second, nullptr, __LINE__);
if (d->activeObject == pos->second) {
d->activeObject = nullptr;
}
// Mark the object as about to be deleted
pos->second->setStatus(ObjectStatus::Remove, true);
if (!d->undoing && !d->rollback) {
pos->second->unsetupObject();
}
signalDeletedObject(*(pos->second));
// do no transactions if we do a rollback!
if (!d->rollback && d->activeUndoTransaction) {
// in this case transaction delete or save the object
signalTransactionRemove(*pos->second, d->activeUndoTransaction);
}
else {
// if not saved in undo -> delete object
signalTransactionRemove(*pos->second, 0);
}
// Before deleting we must nullify all dependent objects
breakDependency(pos->second, true);
// and remove the tip if needed
if (Tip.getValue() && strcmp(Tip.getValue()->getNameInDocument(), sName) == 0) {
Tip.setValue(nullptr);
TipName.setValue("");
}
// remove the ID before possibly deleting the object
d->objectIdMap.erase(pos->second->_Id);
// Unset the bit to be on the safe side
pos->second->setStatus(ObjectStatus::Remove, false);
unregisterLabel(pos->second->Label.getStrValue());
// do no transactions if we do a rollback!
std::unique_ptr<DocumentObject> tobedestroyed;
if (!d->rollback) {
// Undo stuff
if (d->activeUndoTransaction) {
// in this case transaction delete or save the object
d->activeUndoTransaction->addObjectNew(pos->second);
}
else {
// if not saved in undo -> delete object later
std::unique_ptr<DocumentObject> delobj(pos->second);
tobedestroyed.swap(delobj);
tobedestroyed->setStatus(ObjectStatus::Destroy, true);
}
}
for (std::vector<DocumentObject*>::iterator obj = d->objectArray.begin();
obj != d->objectArray.end();
++obj) {
if (*obj == pos->second) {
d->objectArray.erase(obj);
break;
}
}
// In case the object gets deleted the pointer must be nullified
if (tobedestroyed) {
tobedestroyed->pcNameInDocument = nullptr;
}
d->objectNameManager.removeExactName(pos->first);
d->objectMap.erase(pos);
}
/// Remove an object out of the document (internal)
void Document::_removeObject(DocumentObject* pcObject)
{
if (testStatus(Document::Recomputing)) {
FC_ERR("Cannot delete " << pcObject->getFullName() << " while recomputing");
return;
}
TransactionLocker tlock;
// TODO Refactoring: share code with Document::removeObject() (2015-09-01, Fat-Zer)
_checkTransaction(pcObject, nullptr, __LINE__);
auto pos = d->objectMap.find(pcObject->getNameInDocument());
if (pos == d->objectMap.end()) {
FC_ERR("Internal error, could not find " << pcObject->getFullName() << " to remove");
}
if (!d->rollback && d->activeUndoTransaction && pos->second->hasChildElement()) {
// Preserve link group children global visibility. See comments in
// removeObject() for more details.
for (auto& sub : pos->second->getSubObjects()) {
if (sub.empty()) {
continue;
}
if (sub[sub.size() - 1] != '.') {
sub += '.';
}
auto sobj = pos->second->getSubObject(sub.c_str());
if (sobj && sobj->getDocument() == this && !sobj->Visibility.getValue()) {
d->activeUndoTransaction->addObjectChange(sobj, &sobj->Visibility);
}
}
}
if (d->activeObject == pcObject) {
d->activeObject = nullptr;
}
// Mark the object as about to be removed
pcObject->setStatus(ObjectStatus::Remove, true);
if (!d->undoing && !d->rollback) {
pcObject->unsetupObject();
}
signalDeletedObject(*pcObject);
// TODO Check me if it's needed (2015-09-01, Fat-Zer)
// remove the tip if needed
if (Tip.getValue() == pcObject) {
Tip.setValue(nullptr);
TipName.setValue("");
}
// do no transactions if we do a rollback!
if (!d->rollback && d->activeUndoTransaction) {
// Undo stuff
signalTransactionRemove(*pcObject, d->activeUndoTransaction);
breakDependency(pcObject, true);
d->activeUndoTransaction->addObjectNew(pcObject);
}
else {
// for a rollback delete the object
signalTransactionRemove(*pcObject, 0);
breakDependency(pcObject, true);
}
// TODO: Transaction::addObjectName could potentially have freed (deleted) pcObject so some of the following
// code may be dereferencing a pointer to a deleted object which is not legal. if (d->rollback) this does not occur
// and instead pcObject is deleted at the end of this function.
// This either should be fixed, perhaps by moving the following lines up in the code,
// or there should be a comment explaining why the object will never be deleted because of the logic that got us here.
// remove from map
pcObject->setStatus(ObjectStatus::Remove, false); // Unset the bit to be on the safe side
d->objectIdMap.erase(pcObject->_Id);
d->objectNameManager.removeExactName(pos->first);
unregisterLabel(pos->second->Label.getStrValue());
d->objectMap.erase(pos);
for (std::vector<DocumentObject*>::iterator it = d->objectArray.begin();
it != d->objectArray.end();
++it) {
if (*it == pcObject) {
d->objectArray.erase(it);
break;
}
}
// for a rollback delete the object
if (d->rollback) {
pcObject->setStatus(ObjectStatus::Destroy, true);
delete pcObject;
}
}
void Document::breakDependency(DocumentObject* pcObject, bool clear)
{
// Nullify all dependent objects
PropertyLinkBase::breakLinks(pcObject, d->objectArray, clear);
}
std::vector<DocumentObject*>
Document::copyObject(const std::vector<DocumentObject*>& objs, bool recursive, bool returnAll)
{
std::vector<DocumentObject*> deps;
if (!recursive) {
deps = objs;
}
else {
deps = getDependencyList(objs, DepNoXLinked | DepSort);
}
if (!testStatus(TempDoc) && !isSaved() && PropertyXLink::hasXLink(deps)) {
throw Base::RuntimeError(
"Document must be saved at least once before link to external objects");
}
MergeDocuments md(this);
// if not copying recursively then suppress possible warnings
md.setVerbose(recursive);
unsigned int memsize = 1000; // ~ for the meta-information
for (auto it : deps) {
memsize += it->getMemSize();
}
// if less than ~10 MB
bool use_buffer = (memsize < 0xA00000);
QByteArray res;
try {
res.reserve(memsize);
}
catch (const Base::MemoryException&) {
use_buffer = false;
}
std::vector<App::DocumentObject*> imported;
if (use_buffer) {
Base::ByteArrayOStreambuf obuf(res);
std::ostream ostr(&obuf);
exportObjects(deps, ostr);
Base::ByteArrayIStreambuf ibuf(res);
std::istream istr(nullptr);
istr.rdbuf(&ibuf);
imported = md.importObjects(istr);
}
else {
static Base::FileInfo fi(App::Application::getTempFileName());
Base::ofstream ostr(fi, std::ios::out | std::ios::binary);
exportObjects(deps, ostr);
ostr.close();
Base::ifstream istr(fi, std::ios::in | std::ios::binary);
imported = md.importObjects(istr);
}
if (returnAll || imported.size() != deps.size()) {
return imported;
}
std::unordered_map<App::DocumentObject*, size_t> indices;
size_t i = 0;
for (auto o : deps) {
indices[o] = i++;
}
std::vector<App::DocumentObject*> result;
result.reserve(objs.size());
for (auto o : objs) {
result.push_back(imported[indices[o]]);
}
return result;
}
std::vector<App::DocumentObject*>
Document::importLinks(const std::vector<App::DocumentObject*>& objArray)
{
std::set<App::DocumentObject*> links;
getLinksTo(links, nullptr, GetLinkExternal, 0, objArray);
std::vector<App::DocumentObject*> objs;
objs.insert(objs.end(), links.begin(), links.end());
objs = App::Document::getDependencyList(objs);
if (objs.empty()) {
FC_ERR("nothing to import");
return objs;
}
for (auto it = objs.begin(); it != objs.end();) {
auto obj = *it;
if (obj->getDocument() == this) {
it = objs.erase(it);
continue;
}
++it;
if (obj->testStatus(App::PartialObject)) {
throw Base::RuntimeError(
"Cannot import partial loaded object. Please reload the current document");
}
}
Base::FileInfo fi(App::Application::getTempFileName());
{
// save stuff to temp file
Base::ofstream str(fi, std::ios::out | std::ios::binary);
MergeDocuments mimeView(this);
exportObjects(objs, str);
str.close();
}
Base::ifstream str(fi, std::ios::in | std::ios::binary);
MergeDocuments mimeView(this);
objs = mimeView.importObjects(str);
str.close();
fi.deleteFile();
const auto& nameMap = mimeView.getNameMap();
// First, find all link type properties that needs to be changed
std::map<App::Property*, std::unique_ptr<App::Property>> propMap;
std::vector<App::Property*> propList;
for (auto obj : links) {
propList.clear();
obj->getPropertyList(propList);
for (auto prop : propList) {
auto linkProp = freecad_cast<PropertyLinkBase*>(prop);
if (linkProp && !prop->testStatus(Property::Immutable) && !obj->isReadOnly(prop)) {
auto copy = linkProp->CopyOnImportExternal(nameMap);
if (copy) {
propMap[linkProp].reset(copy);
}
}
}
}
// Then change them in one go. Note that we don't make change in previous
// loop, because a changed link property may break other depending link
// properties, e.g. a link sub referring to some sub object of an xlink, If
// that sub object is imported with a different name, and xlink is changed
// before this link sub, it will break.
for (auto& v : propMap) {
v.first->Paste(*v.second);
}
return objs;
}
DocumentObject* Document::moveObject(DocumentObject* obj, bool recursive)
{
if (!obj) {
return nullptr;
}
Document* that = obj->getDocument();
if (that == this) {
return nullptr; // nothing todo
}
// True object move without copy is only safe when undo is off on both
// documents.
if (!recursive && !d->iUndoMode && !that->d->iUndoMode && !that->d->rollback) {
// all object of the other document that refer to this object must be nullified
that->breakDependency(obj, false);
std::string objname = getUniqueObjectName(obj->getNameInDocument());
that->_removeObject(obj);
this->_addObject(obj, objname.c_str());
obj->setDocument(this);
return obj;
}
std::vector<App::DocumentObject*> deps;
if (recursive) {
deps = getDependencyList({obj}, DepNoXLinked | DepSort);
}
else {
deps.push_back(obj);
}
auto objs = copyObject(deps, false);
if (objs.empty()) {
return nullptr;
}
// Some object may delete its children if deleted, so we collect the IDs
// or all depending objects for safety reason.
std::vector<int> ids;
ids.reserve(deps.size());
for (auto o : deps) {
ids.push_back(o->getID());
}
// We only remove object if it is the moving object or it has no
// depending objects, i.e. an empty inList, which is why we need to
// iterate the depending list backwards.
for (auto iter = ids.rbegin(); iter != ids.rend(); ++iter) {
auto o = that->getObjectByID(*iter);
if (!o) {
continue;
}
if (iter == ids.rbegin() || o->getInList().empty()) {
that->removeObject(o->getNameInDocument());
}
}
return objs.back();
}
DocumentObject* Document::getActiveObject() const
{
return d->activeObject;
}
DocumentObject* Document::getObject(const char* Name) const
{
auto pos = d->objectMap.find(Name);
if (pos != d->objectMap.end()) {
return pos->second;
}
else {
return nullptr;
}
}
DocumentObject* Document::getObjectByID(long id) const
{
auto it = d->objectIdMap.find(id);
if (it != d->objectIdMap.end()) {
return it->second;
}
return nullptr;
}
// Note: This method is only used in Tree.cpp slotChangeObject(), see explanation there
bool Document::isIn(const DocumentObject* pFeat) const
{
for (const auto& pos : d->objectMap) {
if (pos.second == pFeat) {
return true;
}
}
return false;
}
const char* Document::getObjectName(DocumentObject* pFeat) const
{
for (const auto& pos : d->objectMap) {
if (pos.second == pFeat) {
return pos.first.c_str();
}
}
return nullptr;
}
std::string Document::getUniqueObjectName(const char* proposedName) const
{
if (!proposedName || *proposedName == '\0') {
return {};
}
std::string cleanName = Base::Tools::getIdentifier(proposedName);
if (!d->objectNameManager.containsName(cleanName)) {
// Not in use yet, name is OK
return cleanName;
}
return d->objectNameManager.makeUniqueName(cleanName, 3);
}
bool
Document::haveSameBaseName(const std::string& name, const std::string& label)
{
// Both Labels and Names use the same decomposition rules for names,
// i.e. the default one supplied by UniqueNameManager, so we can use either
// of the name managers to do this test.
return d->objectNameManager.haveSameBaseName(name, label);
}
std::string Document::getStandardObjectLabel(const char* modelName, int digitCount) const
{
return d->objectLabelManager.makeUniqueName(modelName, digitCount);
}
std::vector<DocumentObject*> Document::getDependingObjects() const
{
return getDependencyList(d->objectArray);
}
const std::vector<DocumentObject*>& Document::getObjects() const
{
return d->objectArray;
}
std::vector<DocumentObject*> Document::getObjectsOfType(const Base::Type& typeId) const
{
std::vector<DocumentObject*> Objects;
for (auto it : d->objectArray) {
if (it->isDerivedFrom(typeId)) {
Objects.push_back(it);
}
}
return Objects;
}
std::vector<DocumentObject*> Document::getObjectsOfType(const std::vector<Base::Type>& types) const
{
std::vector<DocumentObject*> Objects;
for (auto it : d->objectArray) {
for (auto& typeId : types) {
if (it->isDerivedFrom(typeId)) {
Objects.push_back(it);
break; // Prevent adding several times the same object.
}
}
}
return Objects;
}
std::vector<DocumentObject*> Document::getObjectsWithExtension(const Base::Type& typeId,
bool derived) const
{
std::vector<DocumentObject*> Objects;
for (auto it : d->objectArray) {
if (it->hasExtension(typeId, derived)) {
Objects.push_back(it);
}
}
return Objects;
}
std::vector<DocumentObject*>
Document::findObjects(const Base::Type& typeId, const char* objname, const char* label) const
{
boost::cmatch what;
boost::regex rx_name, rx_label;
if (objname) {
rx_name.set_expression(objname);
}
if (label) {
rx_label.set_expression(label);
}
std::vector<DocumentObject*> Objects;
DocumentObject* found = nullptr;
for (auto it : d->objectArray) {
if (it->isDerivedFrom(typeId)) {
found = it;
if (!rx_name.empty() && !boost::regex_search(it->getNameInDocument(), what, rx_name)) {
found = nullptr;
}
if (!rx_label.empty() && !boost::regex_search(it->Label.getValue(), what, rx_label)) {
found = nullptr;
}
if (found) {
Objects.push_back(found);
}
}
}
return Objects;
}
int Document::countObjectsOfType(const Base::Type& typeId) const
{
return std::count_if(d->objectMap.begin(), d->objectMap.end(), [&](const auto& it) {
return it.second->isDerivedFrom(typeId);
});
}
int Document::countObjectsOfType(const char* typeName) const
{
Base::Type type = Base::Type::fromName(typeName);
return type.isBad() ? 0 : countObjectsOfType(type);
}
PyObject* Document::getPyObject()
{
return Py::new_reference_to(d->DocumentPythonObject);
}
std::vector<App::DocumentObject*> Document::getRootObjects() const
{
std::vector<App::DocumentObject*> ret;
for (auto objectIt : d->objectArray) {
if (objectIt->getInList().empty()) {
ret.push_back(objectIt);
}
}
return ret;
}
std::vector<App::DocumentObject*> Document::getRootObjectsIgnoreLinks() const
{
std::vector<App::DocumentObject*> ret;
for (const auto &objectIt : d->objectArray) {
auto list = objectIt->getInList();
bool noParents = list.empty();
if (!noParents) {
// App::Document getRootObjects returns the root objects of the dependency graph.
// So if an object is referenced by an App::Link, it will not be returned by that
// function. So here, as we want the tree-root level objects, we check if all the
// parents are links. In which case it's still a root object.
noParents = std::all_of(list.cbegin(), list.cend(), [](App::DocumentObject* obj) {
return obj->isDerivedFrom<App::Link>();
});
}
if (noParents) {
ret.push_back(objectIt);
}
}
return ret;
}
void DocumentP::findAllPathsAt(const std::vector<Node>& all_nodes,
size_t id,
std::vector<Path>& all_paths,
Path tmp)
{
if (std::ranges::find(tmp, id) != tmp.end()) {
tmp.push_back(id);
all_paths.push_back(std::move(tmp));
return; // a cycle
}
tmp.push_back(id);
if (all_nodes[id].empty()) {
all_paths.push_back(std::move(tmp));
return;
}
for (size_t i = 0; i < all_nodes[id].size(); i++) {
findAllPathsAt(all_nodes, all_nodes[id][i], all_paths, tmp);
}
}
std::vector<std::list<App::DocumentObject*>>
Document::getPathsByOutList(const App::DocumentObject* from, const App::DocumentObject* to) const
{
std::map<const DocumentObject*, size_t> indexMap;
for (size_t i = 0; i < d->objectArray.size(); ++i) {
indexMap[d->objectArray[i]] = i;
}
std::vector<Node> all_nodes(d->objectArray.size());
for (size_t i = 0; i < d->objectArray.size(); ++i) {
DocumentObject* obj = d->objectArray[i];
std::vector<DocumentObject*> outList = obj->getOutList();
for (auto it : outList) {
all_nodes[i].push_back(indexMap[it]);
}
}
std::vector<std::list<App::DocumentObject*>> array;
if (from == to) {
return array;
}
size_t index_from = indexMap[from];
size_t index_to = indexMap[to];
std::vector<Path> all_paths;
DocumentP::findAllPathsAt(all_nodes, index_from, all_paths, Path());
for (const Path& it : all_paths) {
auto jt = std::ranges::find(it, index_to);
if (jt != it.end()) {
array.push_back({});
auto& path = array.back();
for (auto kt = it.begin(); kt != jt; ++kt) {
path.push_back(d->objectArray[*kt]);
}
path.push_back(d->objectArray[*jt]);
}
}
// remove duplicates
std::sort(array.begin(), array.end());
array.erase(std::unique(array.begin(), array.end()), array.end());
return array;
}
bool Document::mustExecute() const
{
if (PropertyXLink::hasXLink(this)) {
bool touched = false;
_buildDependencyList(d->objectArray, false, nullptr, nullptr, nullptr, &touched);
return touched;
}
for (auto It : d->objectArray) {
if (It->isTouched() || It->mustExecute() == 1) {
return true;
}
}
return false;
}