1188 lines
38 KiB
C++
1188 lines
38 KiB
C++
// SPDX-License-Identifier: LGPL-2.1-or-later
|
|
|
|
/***************************************************************************
|
|
* Copyright (c) 2015 Eivind Kvedalen <eivind@kvedalen.name> *
|
|
* *
|
|
* 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 <App/Application.h>
|
|
#include <App/Document.h>
|
|
#include <App/DocumentObject.h>
|
|
#include <App/DocumentObserver.h>
|
|
#include <Base/Reader.h>
|
|
#include <Base/Tools.h>
|
|
#include <Base/Writer.h>
|
|
#include <CXX/Objects.hxx>
|
|
|
|
#include "PropertyExpressionEngine.h"
|
|
#include "ExpressionVisitors.h"
|
|
|
|
|
|
FC_LOG_LEVEL_INIT("App", true);
|
|
|
|
using namespace App;
|
|
using namespace Base;
|
|
using namespace boost;
|
|
namespace sp = std::placeholders;
|
|
|
|
TYPESYSTEM_SOURCE_ABSTRACT(App::PropertyExpressionContainer, App::PropertyXLinkContainer)
|
|
|
|
static std::set<PropertyExpressionContainer*> _ExprContainers;
|
|
|
|
PropertyExpressionContainer::PropertyExpressionContainer()
|
|
{
|
|
static bool inited;
|
|
if (!inited) {
|
|
inited = true;
|
|
GetApplication().signalRelabelDocument.connect(
|
|
PropertyExpressionContainer::slotRelabelDocument);
|
|
GetApplication().signalRenameDynamicProperty.connect(
|
|
PropertyExpressionContainer::slotRenameDynamicProperty);
|
|
}
|
|
_ExprContainers.insert(this);
|
|
}
|
|
|
|
PropertyExpressionContainer::~PropertyExpressionContainer()
|
|
{
|
|
_ExprContainers.erase(this);
|
|
}
|
|
|
|
void PropertyExpressionContainer::slotRelabelDocument(const App::Document& doc)
|
|
{
|
|
// For use a private _ExprContainers to track all living
|
|
// PropertyExpressionContainer including those inside undo/redo stack,
|
|
// because document relabel is not undoable/redoable.
|
|
|
|
if (doc.getOldLabel() != doc.Label.getValue()) {
|
|
for (auto prop : _ExprContainers) {
|
|
prop->onRelabeledDocument(doc);
|
|
}
|
|
}
|
|
}
|
|
|
|
void PropertyExpressionContainer::slotRenameDynamicProperty(const App::Property& prop, const char* oldName)
|
|
{
|
|
for (auto container : _ExprContainers) {
|
|
container->onRenameDynamicProperty(prop, oldName);
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
struct PropertyExpressionEngine::Private
|
|
{
|
|
// For some reason, MSVC has trouble with vector of scoped_connection if
|
|
// defined in header, hence the private structure here.
|
|
std::vector<fastsignals::scoped_connection> conns;
|
|
std::unordered_map<std::string, std::vector<ObjectIdentifier>> propMap;
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
TYPESYSTEM_SOURCE(App::PropertyExpressionEngine, App::PropertyExpressionContainer)
|
|
|
|
PropertyExpressionEngine::PropertyExpressionEngine()
|
|
: validator(0)
|
|
{}
|
|
|
|
PropertyExpressionEngine::~PropertyExpressionEngine() = default;
|
|
|
|
// fixme Should probably return something else than 0.
|
|
unsigned int PropertyExpressionEngine::getMemSize() const
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
Property* PropertyExpressionEngine::Copy() const
|
|
{
|
|
PropertyExpressionEngine* engine = new PropertyExpressionEngine();
|
|
|
|
for (const auto& it : expressions) {
|
|
ExpressionInfo info;
|
|
if (it.second.expression) {
|
|
info.expression = std::shared_ptr<Expression>(it.second.expression->copy());
|
|
}
|
|
engine->expressions[it.first] = info;
|
|
}
|
|
|
|
engine->validator = validator;
|
|
|
|
return engine;
|
|
}
|
|
|
|
void PropertyExpressionEngine::hasSetValue()
|
|
{
|
|
App::DocumentObject* owner = dynamic_cast<App::DocumentObject*>(getContainer());
|
|
if (!owner || !owner->isAttachedToDocument() || owner->isRestoring()
|
|
|| testFlag(LinkDetached)) {
|
|
PropertyExpressionContainer::hasSetValue();
|
|
return;
|
|
}
|
|
|
|
std::map<App::DocumentObject*, bool> deps;
|
|
std::vector<std::string> labels;
|
|
unregisterElementReference();
|
|
UpdateElementReferenceExpressionVisitor<PropertyExpressionEngine> v(*this);
|
|
for (auto& e : expressions) {
|
|
auto expr = e.second.expression;
|
|
if (expr) {
|
|
expr->getDepObjects(deps, &labels);
|
|
if (!restoring) {
|
|
expr->visit(v);
|
|
}
|
|
}
|
|
}
|
|
registerLabelReferences(std::move(labels));
|
|
|
|
updateDeps(std::move(deps));
|
|
|
|
if (pimpl) {
|
|
pimpl->conns.clear();
|
|
pimpl->propMap.clear();
|
|
}
|
|
// check if there is any hidden references
|
|
bool hasHidden = false;
|
|
for (auto& v : _Deps) {
|
|
if (v.second) {
|
|
hasHidden = true;
|
|
break;
|
|
}
|
|
}
|
|
if (hasHidden) {
|
|
if (!pimpl) {
|
|
pimpl = std::make_unique<Private>();
|
|
}
|
|
for (auto& e : expressions) {
|
|
auto expr = e.second.expression;
|
|
if (!expr) {
|
|
continue;
|
|
}
|
|
for (auto& dep : expr->getIdentifiers()) {
|
|
if (!dep.second) {
|
|
continue;
|
|
}
|
|
const ObjectIdentifier& var = dep.first;
|
|
for (auto& vdep : var.getDep(true)) {
|
|
auto obj = vdep.first;
|
|
auto objName = obj->getFullName() + ".";
|
|
for (auto& propName : vdep.second) {
|
|
std::string key = objName + propName;
|
|
auto& propDeps = pimpl->propMap[key];
|
|
if (propDeps.empty()) {
|
|
// NOLINTBEGIN
|
|
if (!propName.empty()) {
|
|
pimpl->conns.emplace_back(obj->signalChanged.connect(
|
|
std::bind(&PropertyExpressionEngine::slotChangedProperty,
|
|
this,
|
|
sp::_1,
|
|
sp::_2)));
|
|
}
|
|
else {
|
|
pimpl->conns.emplace_back(obj->signalChanged.connect(
|
|
std::bind(&PropertyExpressionEngine::slotChangedObject,
|
|
this,
|
|
sp::_1,
|
|
sp::_2)));
|
|
}
|
|
// NOLINTEND
|
|
}
|
|
propDeps.push_back(e.first);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
PropertyExpressionContainer::hasSetValue();
|
|
}
|
|
|
|
void PropertyExpressionEngine::updateHiddenReference(const std::string& key)
|
|
{
|
|
if (!pimpl) {
|
|
return;
|
|
}
|
|
auto it = pimpl->propMap.find(key);
|
|
if (it == pimpl->propMap.end()) {
|
|
return;
|
|
}
|
|
for (auto& var : it->second) {
|
|
auto it = expressions.find(var);
|
|
if (it == expressions.end() || it->second.busy) {
|
|
continue;
|
|
}
|
|
Property* myProp = var.getProperty();
|
|
if (!myProp) {
|
|
continue;
|
|
}
|
|
Base::StateLocker guard(it->second.busy);
|
|
App::any value;
|
|
try {
|
|
value = it->second.expression->getValueAsAny();
|
|
if (!isAnyEqual(value, myProp->getPathValue(var))) {
|
|
myProp->setPathValue(var, value);
|
|
}
|
|
}
|
|
catch (Base::Exception& e) {
|
|
e.reportException();
|
|
FC_ERR("Failed to evaluate property binding " << myProp->getFullName()
|
|
<< " on change of " << key);
|
|
}
|
|
catch (std::bad_cast&) {
|
|
FC_ERR("Invalid type '" << value.type().name() << "' in property binding "
|
|
<< myProp->getFullName() << " on change of " << key);
|
|
}
|
|
catch (std::exception& e) {
|
|
FC_ERR(e.what());
|
|
FC_ERR("Failed to evaluate property binding " << myProp->getFullName()
|
|
<< " on change of " << key);
|
|
}
|
|
}
|
|
}
|
|
|
|
void PropertyExpressionEngine::slotChangedObject(const App::DocumentObject& obj,
|
|
const App::Property&)
|
|
{
|
|
updateHiddenReference(obj.getFullName());
|
|
}
|
|
|
|
void PropertyExpressionEngine::slotChangedProperty(const App::DocumentObject&,
|
|
const App::Property& prop)
|
|
{
|
|
updateHiddenReference(prop.getFullName());
|
|
}
|
|
|
|
void PropertyExpressionEngine::Paste(const Property& from)
|
|
{
|
|
const PropertyExpressionEngine& fromee = dynamic_cast<const PropertyExpressionEngine&>(from);
|
|
|
|
AtomicPropertyChange signaller(*this);
|
|
|
|
expressions.clear();
|
|
for (auto& e : fromee.expressions) {
|
|
ExpressionInfo info;
|
|
if (e.second.expression) {
|
|
info.expression = std::shared_ptr<Expression>(e.second.expression->copy());
|
|
}
|
|
expressions[e.first] = info;
|
|
expressionChanged(e.first);
|
|
}
|
|
validator = fromee.validator;
|
|
signaller.tryInvoke();
|
|
}
|
|
|
|
void PropertyExpressionEngine::Save(Base::Writer& writer) const
|
|
{
|
|
writer.Stream() << writer.ind() << "<ExpressionEngine count=\"" << expressions.size();
|
|
if (PropertyExpressionContainer::_XLinks.empty()) {
|
|
writer.Stream() << "\">" << std::endl;
|
|
writer.incInd();
|
|
}
|
|
else {
|
|
writer.Stream() << R"(" xlink="1">)" << std::endl;
|
|
writer.incInd();
|
|
PropertyExpressionContainer::Save(writer);
|
|
}
|
|
for (const auto& it : expressions) {
|
|
std::string expression, comment;
|
|
if (it.second.expression) {
|
|
expression = it.second.expression->toString(true);
|
|
comment = it.second.expression->comment;
|
|
}
|
|
|
|
writer.Stream() << writer.ind() << "<Expression path=\""
|
|
<< Property::encodeAttribute(it.first.toString()) << "\" expression=\""
|
|
<< Property::encodeAttribute(expression) << "\"";
|
|
if (!comment.empty()) {
|
|
writer.Stream() << " comment=\"" << Property::encodeAttribute(comment) << "\"";
|
|
}
|
|
writer.Stream() << "/>" << std::endl;
|
|
}
|
|
writer.decInd();
|
|
writer.Stream() << writer.ind() << "</ExpressionEngine>" << std::endl;
|
|
}
|
|
|
|
void PropertyExpressionEngine::Restore(Base::XMLReader& reader)
|
|
{
|
|
reader.readElement("ExpressionEngine");
|
|
int count = reader.getAttribute<double>("count");
|
|
|
|
if (reader.hasAttribute("xlink") && reader.getAttribute<bool>("xlink")) {
|
|
PropertyExpressionContainer::Restore(reader);
|
|
}
|
|
|
|
restoredExpressions = std::make_unique<std::vector<RestoredExpression>>();
|
|
restoredExpressions->reserve(count);
|
|
for (int i = 0; i < count; ++i) {
|
|
|
|
reader.readElement("Expression");
|
|
restoredExpressions->emplace_back();
|
|
auto& info = restoredExpressions->back();
|
|
info.path = reader.getAttribute<const char*>("path");
|
|
info.expr = reader.getAttribute<const char*>("expression");
|
|
if (reader.hasAttribute("comment")) {
|
|
info.comment = reader.getAttribute<const char*>("comment");
|
|
}
|
|
}
|
|
|
|
reader.readEndElement("ExpressionEngine");
|
|
}
|
|
|
|
void PropertyExpressionEngine::buildGraphStructures(
|
|
const ObjectIdentifier& path,
|
|
const std::shared_ptr<Expression> expression,
|
|
boost::unordered_map<ObjectIdentifier, int>& nodes,
|
|
boost::unordered_map<int, ObjectIdentifier>& revNodes,
|
|
std::vector<Edge>& edges) const
|
|
{
|
|
/* Insert target property into nodes structure */
|
|
if (nodes.find(path) == nodes.end()) {
|
|
int s = nodes.size();
|
|
|
|
revNodes[s] = path;
|
|
nodes[path] = s;
|
|
}
|
|
else {
|
|
revNodes[nodes[path]] = path;
|
|
}
|
|
|
|
/* Insert dependencies into nodes structure */
|
|
ExpressionDeps deps;
|
|
if (expression) {
|
|
deps = expression->getDeps();
|
|
}
|
|
|
|
for (auto& dep : deps) {
|
|
for (auto& info : dep.second) {
|
|
if (info.first.empty()) {
|
|
continue;
|
|
}
|
|
for (auto& oid : info.second) {
|
|
ObjectIdentifier cPath(oid.canonicalPath());
|
|
if (nodes.find(cPath) == nodes.end()) {
|
|
int s = nodes.size();
|
|
nodes[cPath] = s;
|
|
}
|
|
edges.emplace_back(nodes[path], nodes[cPath]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ObjectIdentifier PropertyExpressionEngine::canonicalPath(const ObjectIdentifier& oid) const
|
|
{
|
|
DocumentObject* docObj = freecad_cast<DocumentObject*>(getContainer());
|
|
|
|
// Am I owned by a DocumentObject?
|
|
if (!docObj) {
|
|
throw Base::RuntimeError("PropertyExpressionEngine must be owned by a DocumentObject.");
|
|
}
|
|
|
|
int ptype;
|
|
Property* prop = oid.getProperty(&ptype);
|
|
|
|
// oid pointing to a property...?
|
|
if (!prop) {
|
|
throw Base::RuntimeError(oid.resolveErrorString().c_str());
|
|
}
|
|
|
|
if (ptype || prop->getContainer() != getContainer()) {
|
|
return oid;
|
|
}
|
|
|
|
// In case someone calls this with p pointing to a PropertyExpressionEngine for some reason
|
|
if (prop->isDerivedFrom(PropertyExpressionEngine::classTypeId)) {
|
|
return oid;
|
|
}
|
|
|
|
// Dispatch call to actual canonicalPath implementation
|
|
return oid.canonicalPath();
|
|
}
|
|
|
|
size_t PropertyExpressionEngine::numExpressions() const
|
|
{
|
|
return expressions.size();
|
|
}
|
|
|
|
void PropertyExpressionEngine::afterRestore()
|
|
{
|
|
DocumentObject* docObj = freecad_cast<DocumentObject*>(getContainer());
|
|
if (restoredExpressions && docObj) {
|
|
Base::FlagToggler<bool> flag(restoring);
|
|
AtomicPropertyChange signaller(*this);
|
|
|
|
PropertyExpressionContainer::afterRestore();
|
|
ObjectIdentifier::DocumentMapper mapper(this->_DocMap);
|
|
|
|
for (auto& info : *restoredExpressions) {
|
|
tryRestoreExpression(docObj, info);
|
|
}
|
|
signaller.tryInvoke();
|
|
}
|
|
restoredExpressions.reset();
|
|
}
|
|
|
|
void PropertyExpressionEngine::tryRestoreExpression(DocumentObject* docObj,
|
|
const RestoredExpression& info)
|
|
{
|
|
try {
|
|
ObjectIdentifier path = ObjectIdentifier::parse(docObj, info.path);
|
|
if (!info.expr.empty()) {
|
|
std::shared_ptr<Expression> expression(
|
|
Expression::parse(docObj, info.expr));
|
|
if (expression) {
|
|
expression->comment = info.comment;
|
|
}
|
|
setValue(path, expression);
|
|
}
|
|
}
|
|
catch (const Base::Exception& e) {
|
|
FC_ERR("Failed to restore " << docObj->getFullName()
|
|
<< '.'
|
|
<< getName()
|
|
<< ": "
|
|
<< e.what());
|
|
}
|
|
}
|
|
|
|
void PropertyExpressionEngine::onContainerRestored()
|
|
{
|
|
Base::FlagToggler<bool> flag(restoring);
|
|
unregisterElementReference();
|
|
UpdateElementReferenceExpressionVisitor<PropertyExpressionEngine> v(*this);
|
|
for (auto& e : expressions) {
|
|
auto expr = e.second.expression;
|
|
if (expr) {
|
|
expr->visit(v);
|
|
}
|
|
}
|
|
}
|
|
|
|
const boost::any PropertyExpressionEngine::getPathValue(const App::ObjectIdentifier& path) const
|
|
{
|
|
// Get a canonical path
|
|
ObjectIdentifier usePath(canonicalPath(path));
|
|
|
|
auto i = expressions.find(usePath);
|
|
if (i != expressions.end()) {
|
|
return i->second;
|
|
}
|
|
|
|
return boost::any();
|
|
}
|
|
|
|
void PropertyExpressionEngine::setValue(const ObjectIdentifier& path,
|
|
std::shared_ptr<Expression> expr)
|
|
{
|
|
ObjectIdentifier usePath(canonicalPath(path));
|
|
const Property* prop = usePath.getProperty();
|
|
|
|
// Try to access value; it should trigger an exception if it is not supported, or if the path is
|
|
// invalid
|
|
prop->getPathValue(usePath);
|
|
|
|
// Check if the current expression equals the new one and do nothing if so to reduce unneeded
|
|
// computations
|
|
auto it = expressions.find(usePath);
|
|
if (it != expressions.end()
|
|
&& (expr == it->second.expression
|
|
|| (expr && it->second.expression && expr->isSame(*it->second.expression)))) {
|
|
return;
|
|
}
|
|
|
|
if (expr) {
|
|
std::string error = validateExpression(usePath, expr);
|
|
if (!error.empty()) {
|
|
throw Base::RuntimeError(error.c_str());
|
|
}
|
|
AtomicPropertyChange signaller(*this);
|
|
expressions[usePath] = ExpressionInfo(expr);
|
|
expressionChanged(usePath);
|
|
signaller.tryInvoke();
|
|
}
|
|
else if (it != expressions.end()) {
|
|
AtomicPropertyChange signaller(*this);
|
|
expressions.erase(it);
|
|
expressionChanged(usePath);
|
|
signaller.tryInvoke();
|
|
}
|
|
}
|
|
|
|
|
|
/* The cycle_detector struct is used by the boost graph routines to detect
|
|
* cycles in the graph. */
|
|
struct cycle_detector: public boost::dfs_visitor<>
|
|
{
|
|
cycle_detector(bool& has_cycle, int& src)
|
|
: _has_cycle(has_cycle)
|
|
, _src(src)
|
|
{}
|
|
|
|
template<class Edge, class Graph>
|
|
void back_edge(Edge e, Graph& g)
|
|
{
|
|
_has_cycle = true;
|
|
_src = source(e, g);
|
|
}
|
|
|
|
protected:
|
|
bool& _has_cycle;
|
|
int& _src;
|
|
};
|
|
|
|
void PropertyExpressionEngine::buildGraph(const ExpressionMap& exprs,
|
|
boost::unordered_map<int, ObjectIdentifier>& revNodes,
|
|
DiGraph& g,
|
|
ExecuteOption option) const
|
|
{
|
|
boost::unordered_map<ObjectIdentifier, int> nodes;
|
|
std::vector<Edge> edges;
|
|
|
|
// Build data structure for graph
|
|
for (const auto& expr : exprs) {
|
|
if (option != ExecuteAll) {
|
|
auto prop = expr.first.getProperty();
|
|
if (!prop) {
|
|
throw Base::RuntimeError("Path does not resolve to a property.");
|
|
}
|
|
bool is_output =
|
|
prop->testStatus(App::Property::Output) || (prop->getType() & App::Prop_Output);
|
|
if ((is_output && option == ExecuteNonOutput)
|
|
|| (!is_output && option == ExecuteOutput)) {
|
|
continue;
|
|
}
|
|
if (option == ExecuteOnRestore && !prop->testStatus(Property::Transient)
|
|
&& !(prop->getType() & Prop_Transient)
|
|
&& !prop->testStatus(Property::EvalOnRestore)) {
|
|
continue;
|
|
}
|
|
}
|
|
buildGraphStructures(expr.first, expr.second.expression, nodes, revNodes, edges);
|
|
}
|
|
|
|
// Create graph
|
|
g = DiGraph(nodes.size());
|
|
|
|
// Add edges to graph
|
|
for (const auto& edge : edges) {
|
|
add_edge(edge.first, edge.second, g);
|
|
}
|
|
|
|
// Check for cycles
|
|
bool has_cycle = false;
|
|
int src = -1;
|
|
cycle_detector vis(has_cycle, src);
|
|
depth_first_search(g, visitor(vis));
|
|
|
|
if (has_cycle) {
|
|
std::string s = revNodes[src].toString() + " reference creates a cyclic dependency.";
|
|
|
|
throw Base::RuntimeError(s.c_str());
|
|
}
|
|
}
|
|
|
|
std::vector<App::ObjectIdentifier>
|
|
PropertyExpressionEngine::computeEvaluationOrder(ExecuteOption option)
|
|
{
|
|
std::vector<App::ObjectIdentifier> evaluationOrder;
|
|
boost::unordered_map<int, ObjectIdentifier> revNodes;
|
|
DiGraph g;
|
|
|
|
buildGraph(expressions, revNodes, g, option);
|
|
|
|
/* Compute evaluation order for expressions */
|
|
std::vector<int> c;
|
|
topological_sort(g, std::back_inserter(c));
|
|
|
|
for (int i : c) {
|
|
// we return the evaluation order for our properties, not the dependencies
|
|
// the topo sort will contain node ids for both our props and their deps
|
|
if (revNodes.find(i) != revNodes.end()) {
|
|
evaluationOrder.push_back(revNodes[i]);
|
|
}
|
|
}
|
|
|
|
return evaluationOrder;
|
|
}
|
|
|
|
DocumentObjectExecReturn* App::PropertyExpressionEngine::execute(ExecuteOption option,
|
|
bool* touched)
|
|
{
|
|
DocumentObject* docObj = freecad_cast<DocumentObject*>(getContainer());
|
|
|
|
if (!docObj) {
|
|
throw Base::RuntimeError("PropertyExpressionEngine must be owned by a DocumentObject.");
|
|
}
|
|
|
|
if (running) {
|
|
return DocumentObject::StdReturn;
|
|
}
|
|
|
|
if (option == ExecuteOnRestore) {
|
|
bool found = false;
|
|
for (auto& e : expressions) {
|
|
auto prop = e.first.getProperty();
|
|
if (!prop) {
|
|
continue;
|
|
}
|
|
if (prop->testStatus(App::Property::Transient)
|
|
|| (prop->getType() & App::Prop_Transient)
|
|
|| prop->testStatus(App::Property::EvalOnRestore)) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
return DocumentObject::StdReturn;
|
|
}
|
|
}
|
|
|
|
/* Resetter class, to ensure that the "running" variable gets set to false, even if
|
|
* an exception is thrown.
|
|
*/
|
|
|
|
class resetter
|
|
{
|
|
public:
|
|
explicit resetter(bool& b)
|
|
: _b(b)
|
|
{
|
|
_b = true;
|
|
}
|
|
~resetter()
|
|
{
|
|
_b = false;
|
|
}
|
|
|
|
private:
|
|
bool& _b;
|
|
};
|
|
|
|
resetter r(running);
|
|
|
|
// Compute evaluation order
|
|
std::vector<App::ObjectIdentifier> evaluationOrder = computeEvaluationOrder(option);
|
|
std::vector<ObjectIdentifier>::const_iterator it = evaluationOrder.begin();
|
|
|
|
#ifdef FC_PROPERTYEXPRESSIONENGINE_LOG
|
|
std::clog << "Computing expressions for " << getName() << std::endl;
|
|
#endif
|
|
|
|
/* Evaluate the expressions, and update properties */
|
|
for (; it != evaluationOrder.end(); ++it) {
|
|
|
|
// Get property to update
|
|
Property* prop = it->getProperty();
|
|
|
|
if (!prop) {
|
|
throw Base::RuntimeError("Path does not resolve to a property.");
|
|
}
|
|
|
|
DocumentObject* parent = freecad_cast<DocumentObject*>(prop->getContainer());
|
|
|
|
/* Make sure property belongs to the same container as this PropertyExpressionEngine */
|
|
if (parent != docObj) {
|
|
throw Base::RuntimeError("Invalid property owner.");
|
|
}
|
|
|
|
/* Set value of property */
|
|
App::any value;
|
|
try {
|
|
// Evaluate expression
|
|
std::shared_ptr<App::Expression> expression = expressions[*it].expression;
|
|
if (expression) {
|
|
value = expression->getValueAsAny();
|
|
|
|
// Enable value comparison for all expression bindings to reduce
|
|
// unnecessary touch and recompute.
|
|
//
|
|
// This optimization is necessary for some hidden reference to
|
|
// work because it introduce dependency loop. The repeativtive
|
|
// recompute can be stopped if the expression evaluates the same
|
|
// value.
|
|
//
|
|
// In the future, we can generalize the optimization to all
|
|
// property modification, i.e. do not touch unless value change
|
|
//
|
|
// if (option == ExecuteOnRestore && prop->testStatus(Property::EvalOnRestore))
|
|
{
|
|
if (isAnyEqual(value, prop->getPathValue(*it))) {
|
|
continue;
|
|
}
|
|
if (touched) {
|
|
*touched = true;
|
|
}
|
|
}
|
|
prop->setPathValue(*it, value);
|
|
}
|
|
}
|
|
catch (Base::Exception& e) {
|
|
std::ostringstream ss;
|
|
ss << e.what() << std::endl << "in property binding '" << prop->getFullName() << "'";
|
|
e.setMessage(ss.str());
|
|
throw;
|
|
}
|
|
catch (std::bad_cast&) {
|
|
std::ostringstream ss;
|
|
ss << "Invalid type '" << value.type().name() << "'";
|
|
ss << "\nin property binding '" << prop->getFullName() << "'";
|
|
throw Base::TypeError(ss.str().c_str());
|
|
}
|
|
catch (std::exception& e) {
|
|
std::ostringstream ss;
|
|
ss << e.what() << "\nin property binding '" << prop->getFullName() << "'";
|
|
throw Base::RuntimeError(ss.str().c_str());
|
|
}
|
|
}
|
|
return DocumentObject::StdReturn;
|
|
}
|
|
|
|
void PropertyExpressionEngine::getPathsToDocumentObject(
|
|
DocumentObject* obj,
|
|
std::vector<App::ObjectIdentifier>& paths) const
|
|
{
|
|
DocumentObject* owner = freecad_cast<DocumentObject*>(getContainer());
|
|
|
|
if (!owner || owner == obj) {
|
|
return;
|
|
}
|
|
|
|
for (auto& v : expressions) {
|
|
if (!v.second.expression) {
|
|
continue;
|
|
}
|
|
const auto& deps = v.second.expression->getDeps();
|
|
auto it = deps.find(obj);
|
|
if (it == deps.end()) {
|
|
continue;
|
|
}
|
|
for (auto& dep : it->second) {
|
|
paths.insert(paths.end(), dep.second.begin(), dep.second.end());
|
|
}
|
|
}
|
|
}
|
|
|
|
bool PropertyExpressionEngine::depsAreTouched() const
|
|
{
|
|
for (auto& v : _Deps) {
|
|
// v.second indicates if it is a hidden reference
|
|
if (!v.second && v.first->isTouched()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::string
|
|
PropertyExpressionEngine::validateExpression(const ObjectIdentifier& path,
|
|
std::shared_ptr<const Expression> expr) const
|
|
{
|
|
std::string error;
|
|
ObjectIdentifier usePath(canonicalPath(path));
|
|
|
|
if (validator) {
|
|
error = validator(usePath, expr);
|
|
if (!error.empty()) {
|
|
return error;
|
|
}
|
|
}
|
|
|
|
// Get document object
|
|
DocumentObject* pathDocObj = usePath.getDocumentObject();
|
|
assert(pathDocObj);
|
|
|
|
auto inList = pathDocObj->getInListEx(true);
|
|
for (auto& v : expr->getDepObjects()) {
|
|
auto docObj = v.first;
|
|
if (!v.second && inList.contains(docObj)) {
|
|
std::stringstream ss;
|
|
ss << "cyclic reference to " << docObj->getFullName();
|
|
return ss.str();
|
|
}
|
|
}
|
|
|
|
// Check for internal document object dependencies
|
|
|
|
// Copy current expressions
|
|
ExpressionMap newExpressions = expressions;
|
|
|
|
// Add expression in question
|
|
std::shared_ptr<Expression> exprClone(expr->copy());
|
|
newExpressions[usePath].expression = exprClone;
|
|
|
|
// Build graph; an exception will be thrown if it is not a DAG
|
|
try {
|
|
boost::unordered_map<int, ObjectIdentifier> revNodes;
|
|
DiGraph g;
|
|
|
|
buildGraph(newExpressions, revNodes, g);
|
|
}
|
|
catch (const Base::Exception& e) {
|
|
return e.what();
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
void PropertyExpressionEngine::renameExpressions(
|
|
const std::map<ObjectIdentifier, ObjectIdentifier>& paths)
|
|
{
|
|
ExpressionMap newExpressions;
|
|
std::map<ObjectIdentifier, ObjectIdentifier> canonicalPaths;
|
|
|
|
/* ensure input map uses canonical paths */
|
|
for (const auto& path : paths) {
|
|
canonicalPaths[canonicalPath(path.first)] = path.second;
|
|
}
|
|
|
|
for (auto i = expressions.begin(); i != expressions.end(); ++i) {
|
|
auto j = canonicalPaths.find(i->first);
|
|
|
|
// Renamed now?
|
|
if (j != canonicalPaths.end()) {
|
|
newExpressions[j->second] = i->second;
|
|
}
|
|
else {
|
|
newExpressions[i->first] = i->second;
|
|
}
|
|
}
|
|
|
|
aboutToSetValue();
|
|
expressions = newExpressions;
|
|
for (auto i = expressions.begin(); i != expressions.end(); ++i) {
|
|
expressionChanged(i->first);
|
|
}
|
|
|
|
hasSetValue();
|
|
}
|
|
|
|
void PropertyExpressionEngine::renameObjectIdentifiers(
|
|
const std::map<ObjectIdentifier, ObjectIdentifier>& paths)
|
|
{
|
|
for (const auto& it : expressions) {
|
|
RenameObjectIdentifierExpressionVisitor<PropertyExpressionEngine> v(*this, paths, it.first);
|
|
it.second.expression->visit(v);
|
|
}
|
|
}
|
|
|
|
PyObject* PropertyExpressionEngine::getPyObject()
|
|
{
|
|
Py::List list;
|
|
for (const auto& it : expressions) {
|
|
Py::Tuple tuple(2);
|
|
tuple.setItem(0, Py::String(it.first.toString()));
|
|
auto expr = it.second.expression;
|
|
tuple.setItem(1, expr ? Py::String(expr->toString()) : Py::None());
|
|
list.append(tuple);
|
|
}
|
|
return Py::new_reference_to(list);
|
|
}
|
|
|
|
void PropertyExpressionEngine::setPyObject(PyObject*)
|
|
{
|
|
throw Base::RuntimeError("Property is read-only");
|
|
}
|
|
|
|
/* The policy implemented in the following function is to auto erase binding in
|
|
* case linked object is gone. I think it is better to cause error and get
|
|
* user's attention
|
|
*
|
|
void PropertyExpressionEngine::breakLink(App::DocumentObject *obj, bool clear) {
|
|
auto owner = dynamic_cast<App::DocumentObject*>(getContainer());
|
|
if(!owner)
|
|
return;
|
|
if(_Deps.count(obj)==0 && (!clear || obj!=owner || _Deps.empty()))
|
|
return;
|
|
AtomicPropertyChange signaler(*this);
|
|
for(auto it=expressions.begin(),itNext=it;it!=expressions.end();it=itNext) {
|
|
++itNext;
|
|
const auto &deps = it->second.expression->getDepObjects();
|
|
if(clear) {
|
|
// here means we are breaking all expression, except those that has
|
|
// no depdenecy or self dependency
|
|
if(deps.empty() || (deps.size()==1 && *deps.begin()==owner))
|
|
continue;
|
|
}else if(!deps.count(obj))
|
|
continue;
|
|
auto path = it->first;
|
|
expressions.erase(it);
|
|
expressionChanged(path);
|
|
}
|
|
}
|
|
*/
|
|
|
|
bool PropertyExpressionEngine::adjustLink(const std::set<DocumentObject*>& inList)
|
|
{
|
|
auto owner = dynamic_cast<App::DocumentObject*>(getContainer());
|
|
if (!owner) {
|
|
return false;
|
|
}
|
|
bool found = false;
|
|
for (auto& v : _Deps) {
|
|
if (inList.contains(v.first)) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
return false;
|
|
}
|
|
|
|
AtomicPropertyChange signaler(*this);
|
|
for (auto& v : expressions) {
|
|
try {
|
|
if (v.second.expression && v.second.expression->adjustLinks(inList)) {
|
|
expressionChanged(v.first);
|
|
}
|
|
}
|
|
catch (Base::Exception& e) {
|
|
std::ostringstream ss;
|
|
ss << "Failed to adjust link for " << owner->getFullName() << " in expression "
|
|
<< v.second.expression->toString() << ": " << e.what();
|
|
throw Base::RuntimeError(ss.str());
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void PropertyExpressionEngine::updateElementReference(DocumentObject* feature,
|
|
bool reverse,
|
|
bool notify)
|
|
{
|
|
(void)notify;
|
|
if (!feature) {
|
|
unregisterElementReference();
|
|
}
|
|
UpdateElementReferenceExpressionVisitor<PropertyExpressionEngine> v(*this, feature, reverse);
|
|
for (auto& e : expressions) {
|
|
if (e.second.expression) {
|
|
e.second.expression->visit(v);
|
|
if (v.changed()) {
|
|
expressionChanged(e.first);
|
|
v.reset();
|
|
}
|
|
}
|
|
}
|
|
if (feature && v.changed()) {
|
|
auto owner = dynamic_cast<App::DocumentObject*>(getContainer());
|
|
if (owner) {
|
|
owner->onUpdateElementReference(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool PropertyExpressionEngine::referenceChanged() const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Property* PropertyExpressionEngine::CopyOnImportExternal(
|
|
const std::map<std::string, std::string>& nameMap) const
|
|
{
|
|
std::unique_ptr<PropertyExpressionEngine> engine;
|
|
for (auto it = expressions.begin(); it != expressions.end(); ++it) {
|
|
#ifdef BOOST_NO_CXX11_SMART_PTR
|
|
std::shared_ptr<Expression> expr(it->second.expression->importSubNames(nameMap).release());
|
|
#else
|
|
std::shared_ptr<Expression> expr(it->second.expression->importSubNames(nameMap));
|
|
#endif
|
|
if (!expr && !engine) {
|
|
continue;
|
|
}
|
|
if (!engine) {
|
|
engine = std::make_unique<PropertyExpressionEngine>();
|
|
for (auto it2 = expressions.begin(); it2 != it; ++it2) {
|
|
engine->expressions[it2->first] =
|
|
ExpressionInfo(std::shared_ptr<Expression>(it2->second.expression->copy()));
|
|
}
|
|
}
|
|
else if (!expr) {
|
|
expr = it->second.expression;
|
|
}
|
|
engine->expressions[it->first] = ExpressionInfo(expr);
|
|
}
|
|
if (!engine) {
|
|
return nullptr;
|
|
}
|
|
engine->validator = validator;
|
|
return engine.release();
|
|
}
|
|
|
|
Property* PropertyExpressionEngine::CopyOnLabelChange(App::DocumentObject* obj,
|
|
const std::string& ref,
|
|
const char* newLabel) const
|
|
{
|
|
std::unique_ptr<PropertyExpressionEngine> engine;
|
|
for (auto it = expressions.begin(); it != expressions.end(); ++it) {
|
|
#ifdef BOOST_NO_CXX11_SMART_PTR
|
|
std::shared_ptr<Expression> expr(
|
|
it->second.expression->updateLabelReference(obj, ref, newLabel).release());
|
|
#else
|
|
std::shared_ptr<Expression> expr(
|
|
it->second.expression->updateLabelReference(obj, ref, newLabel));
|
|
#endif
|
|
if (!expr && !engine) {
|
|
continue;
|
|
}
|
|
if (!engine) {
|
|
engine = std::make_unique<PropertyExpressionEngine>();
|
|
for (auto it2 = expressions.begin(); it2 != it; ++it2) {
|
|
ExpressionInfo info;
|
|
if (it2->second.expression) {
|
|
info.expression = std::shared_ptr<Expression>(it2->second.expression->copy());
|
|
}
|
|
engine->expressions[it2->first] = info;
|
|
}
|
|
}
|
|
else if (!expr) {
|
|
expr = it->second.expression;
|
|
}
|
|
engine->expressions[it->first] = ExpressionInfo(expr);
|
|
}
|
|
if (!engine) {
|
|
return nullptr;
|
|
}
|
|
engine->validator = validator;
|
|
return engine.release();
|
|
}
|
|
|
|
Property* PropertyExpressionEngine::CopyOnLinkReplace(const App::DocumentObject* parent,
|
|
App::DocumentObject* oldObj,
|
|
App::DocumentObject* newObj) const
|
|
{
|
|
std::unique_ptr<PropertyExpressionEngine> engine;
|
|
for (auto it = expressions.begin(); it != expressions.end(); ++it) {
|
|
#ifdef BOOST_NO_CXX11_SMART_PTR
|
|
std::shared_ptr<Expression> expr(
|
|
it->second.expression->replaceObject(parent, oldObj, newObj).release());
|
|
#else
|
|
std::shared_ptr<Expression> expr(
|
|
it->second.expression->replaceObject(parent, oldObj, newObj));
|
|
#endif
|
|
if (!expr && !engine) {
|
|
continue;
|
|
}
|
|
if (!engine) {
|
|
engine = std::make_unique<PropertyExpressionEngine>();
|
|
for (auto it2 = expressions.begin(); it2 != it; ++it2) {
|
|
ExpressionInfo info;
|
|
if (it2->second.expression) {
|
|
info.expression = std::shared_ptr<Expression>(it2->second.expression->copy());
|
|
}
|
|
engine->expressions[it2->first] = info;
|
|
}
|
|
}
|
|
else if (!expr) {
|
|
expr = it->second.expression;
|
|
}
|
|
engine->expressions[it->first] = ExpressionInfo(expr);
|
|
}
|
|
if (!engine) {
|
|
return nullptr;
|
|
}
|
|
engine->validator = validator;
|
|
return engine.release();
|
|
}
|
|
|
|
std::map<App::ObjectIdentifier, const App::Expression*>
|
|
PropertyExpressionEngine::getExpressions() const
|
|
{
|
|
std::map<App::ObjectIdentifier, const Expression*> res;
|
|
for (auto& v : expressions) {
|
|
res[v.first] = v.second.expression.get();
|
|
}
|
|
return res;
|
|
}
|
|
|
|
void PropertyExpressionEngine::setExpressions(
|
|
std::map<App::ObjectIdentifier, App::ExpressionPtr>&& exprs)
|
|
{
|
|
AtomicPropertyChange signaller(*this);
|
|
#ifdef BOOST_NO_CXX11_SMART_PTR
|
|
for (auto& v : exprs) {
|
|
setValue(v.first, std::shared_ptr<Expression>(v.second.release()));
|
|
}
|
|
#else
|
|
for (auto& v : exprs) {
|
|
setValue(v.first, std::move(v.second));
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void PropertyExpressionEngine::onRelabeledDocument(const App::Document& doc)
|
|
{
|
|
RelabelDocumentExpressionVisitor v(doc);
|
|
for (auto& e : expressions) {
|
|
if (e.second.expression) {
|
|
e.second.expression->visit(v);
|
|
}
|
|
}
|
|
}
|
|
|
|
void PropertyExpressionEngine::onRenameDynamicProperty(const App::Property& prop, const char* oldName)
|
|
{
|
|
ObjectIdentifier oldNameId = ObjectIdentifier(prop.getContainer(), std::string(oldName));
|
|
ObjectIdentifier newNameId = ObjectIdentifier(prop);
|
|
const std::map<ObjectIdentifier, ObjectIdentifier> paths = {
|
|
{oldNameId, newNameId},
|
|
};
|
|
|
|
renameObjectIdentifiers(paths);
|
|
}
|
|
|
|
void PropertyExpressionEngine::getLinksTo(std::vector<App::ObjectIdentifier>& identifiers,
|
|
App::DocumentObject* obj,
|
|
const char* subname,
|
|
bool all) const
|
|
{
|
|
Expression::DepOption option =
|
|
all ? Expression::DepOption::DepAll : Expression::DepOption::DepNormal;
|
|
|
|
App::SubObjectT objT(obj, subname);
|
|
auto sobj = objT.getSubObject();
|
|
auto subElement = objT.getOldElementName();
|
|
|
|
for (auto& [expressionId, expressionInfo] : expressions) {
|
|
const auto& deps = expressionInfo.expression->getDeps(option);
|
|
auto it = deps.find(obj);
|
|
if (it == deps.end()) {
|
|
continue;
|
|
}
|
|
auto [docObj, map] = *it;
|
|
for (auto& [key, paths] : map) {
|
|
if (!subname) {
|
|
identifiers.push_back(expressionId);
|
|
break;
|
|
}
|
|
if (std::ranges::any_of(paths, [subname, obj, sobj, &subElement](const auto& path) {
|
|
if (path.getSubObjectName() == subname) {
|
|
return true;
|
|
}
|
|
|
|
App::SubObjectT sobjT(obj, path.getSubObjectName().c_str());
|
|
return (sobjT.getSubObject() == sobj
|
|
&& sobjT.getOldElementName() == subElement);
|
|
})) {
|
|
identifiers.push_back(expressionId);
|
|
}
|
|
}
|
|
}
|
|
}
|