-
-
Notifications
You must be signed in to change notification settings - Fork 3.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Core] Enable exposing VarSets in App::Part #12532
base: main
Are you sure you want to change the base?
Conversation
Dear all, In issue #12120 I've added the video Custom Data Elements, Variants, Modularity - Part 1 to discuss the problem with modularity. Part 2 of the video is here: This video was created with the code that is currently in this PR. |
9d42214
to
9c9e028
Compare
The initial implementation automatically redirected VarSet references. To make the implementation less intrusive and similar to other approaches such as Spreadsheet configuration tables, we switched to an implementation that makes use of hiddenrefs.
Before this commit, users had to make references to the parent part hidden manually. This commit extends expression visitors and expression modifiers with an expression rewriter. An expression visitor visits the expression tree to gather information. An expression modifier can modify individual nodes within the tree. An expression rewriter can rewrite the tree itself, that is, add to and remove nodes from the tree.
9c9e028
to
ec9ac44
Compare
The problem sketched in #12531 can be solved in several ways. The cleanest solution would be that a part can remain absolutely unmodified as soon as the VarSet is exposed. The first commit attempts that by redirecting references to VarSets automatically. However, in that case it is challenging to maintain dependencies between VarSets that are replacing the geometry. To make that happen, we would need to maintain a mapping between replacing VarSets and the Parts they are used in to create the variant geometry. This is especially challenging with subshapebinders that create copies of the object in a temporary document. Another solution is to change the references inside a Part that refer to the exposed VarSet to point to the propery within the parent Part that references the replacing VarSet. However, this creates cyclic dependencies and this makes it challenging to maintain proper dependencies as well. In the end, I chose for a solution that makes as much as possible use of existing logic. This has several benefits:
|
My opinion here is more or less the same as last time, that there is a lot of core change, with new functionality added to the expression engine, all this for a very specific use case (basically the "exposed / not exposed" system). It seems to me the general tendency should be to avoid that, and, if a specific use case needs to be implemented, to confine as much as possible and do with what FreeCAD has instead of trying to adapt large parts of FreeCAD, specially core stuff, to such case. I feel this adds a lot of complexity for little use outside that specific use case. I'd prefer to have that use case solved without so much core change. IMHO all that happens here could and should somehow be contained inside the VarSet object itself But I might be wrong on this, I would like others to have a look (@chennes or @wwmayer maybe?) |
I just tried your PR and created a file as shown in your "Part 2" video. Maybe I missed something, but when I change the VarSet outside of Part, no automatic recompute happens. The outside VarSet is set in the Part property created from the exposed VarSet inside of Part. This is obvious because the expression now contains a I'm not familiar with the depency code in FreeCAD, but my understanding is, that the core problem is, that the dependency resolution (for a recompute?) has a granularity of a DocumentObject.
Would it be feasable to change the dependency resolution to a granularity of a property, such that the logic becomes:
As long as Part.X is not dependent on Part.Shape there is no cycle. This fits with the separation of computing the shape and the execute() function as mentioned in #12868. (Just a thought I had, even with a cycle, some kind of fixed-point iteration could also be useful in some constelations) This is obviously a bigger change to the core code, but one that would allow a more precise dependency tracking across all workbenches. Implementing variant links might then become much more straight forward. |
I agree you would want to avoid many core changes and that you want to confine the functionality as much as possible. Indeed, it is a specific use-case, but a very powerful one and not one that I believe other CAD programs have. In that light, I believe the changes are warranted. I think it may be useful to make clear what exactly is added to the core. We can make a distinction between things that are confined and things that are not confined. Confined changes to the core:
Not confined changes to the core:
So, from my point of view the following has happened here:
So, I can see that the proposed changes feel large, but in fact, I think it is as small as can be to acquire the functionality. For convenience, I will comment on each diff to which category it belongs. |
@@ -282,6 +282,13 @@ void ExpressionVisitor::offsetCells(Expression &e, int rowOffset, int colOffset) | |||
e._offsetCells(rowOffset,colOffset,*this); | |||
} | |||
|
|||
Expression* ExpressionVisitor::rewriteVarSetExpression(Expression &e, const DocumentObject* parent, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
VarSet rewriter
@@ -822,6 +829,12 @@ void Expression::Component::visit(ExpressionVisitor &v) { | |||
if(e3) e3->visit(v); | |||
} | |||
|
|||
void Expression::Component::rewrite(ExpressionVisitor &v) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generic rewriter
@@ -1106,6 +1119,19 @@ ExpressionPtr Expression::replaceObject(const DocumentObject *parent, | |||
return ExpressionPtr(expr); | |||
} | |||
|
|||
VarSet* Expression::findReferencedVarSet() const { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
find referenced VarSet
@@ -1137,6 +1163,25 @@ void Expression::visit(ExpressionVisitor &v) { | |||
v.visit(*this); | |||
} | |||
|
|||
Expression* Expression::rewrite(ExpressionVisitor &v) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generic rewriter
@@ -1184,6 +1229,13 @@ Expression* Expression::copy() const { | |||
return expr; | |||
} | |||
|
|||
void Expression::replaceExpression(Expression** expression, Expression* newExpression) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generic rewriter
@@ -1663,6 +1715,17 @@ void OperatorExpression::_visit(ExpressionVisitor &v) | |||
right->visit(v); | |||
} | |||
|
|||
void OperatorExpression::_rewrite(ExpressionVisitor &v) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generic rewriter
@@ -2867,6 +2930,29 @@ void FunctionExpression::_visit(ExpressionVisitor &v) | |||
} | |||
} | |||
|
|||
void FunctionExpression::_rewrite(ExpressionVisitor &v) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generic rewriter
} | ||
} | ||
|
||
Expression* FunctionExpression::_rewriteVarSetExpression(const DocumentObject* parent, const char* nameProperty, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
VarSet rewriter
@@ -3062,6 +3148,64 @@ bool VariableExpression::_renameObjectIdentifier( | |||
return false; | |||
} | |||
|
|||
Expression* VariableExpression::_rewriteVarSetExpression(const DocumentObject* parent, const char* nameProperty, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
VarSet rewriter
@@ -3301,6 +3445,13 @@ void ConditionalExpression::_visit(ExpressionVisitor &v) | |||
falseExpr->visit(v); | |||
} | |||
|
|||
void ConditionalExpression::_rewrite(ExpressionVisitor &v) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generic rewriter
@@ -57,6 +57,22 @@ class AppExport ExpressionVisitor { | |||
public: | |||
virtual ~ExpressionVisitor() = default; | |||
virtual void visit(Expression &e) = 0; | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generic rewriter
@@ -76,6 +92,27 @@ class AppExport ExpressionVisitor { | |||
const std::string &ref, const char *newLabel); | |||
void moveCells(Expression &e, const CellAddress &address, int rowCount, int colCount); | |||
void offsetCells(Expression &e, int rowOffset, int colOffset); | |||
/** @brief Rewrite an expression that references a variable set. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
VarSet rewriter
@@ -161,6 +198,8 @@ class AppExport Expression : public Base::BaseClass { | |||
|
|||
void visit(ExpressionVisitor & v); | |||
|
|||
Expression* rewrite(ExpressionVisitor &v); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generic rewriter
@@ -858,6 +882,17 @@ void PropertyExpressionEngine::renameObjectIdentifiers(const std::map<ObjectIden | |||
} | |||
} | |||
|
|||
void PropertyExpressionEngine::rewriteVarSetExpressions(const DocumentObject* parent, const char* nameProperty, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
VarSet rewriter
@@ -1079,3 +1114,8 @@ void PropertyExpressionEngine::onRelabeledDocument(const App::Document &doc) | |||
e.second.expression->visit(v); | |||
} | |||
} | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
VarSet rewriter
@@ -150,6 +150,9 @@ class AppExport PropertyExpressionEngine : public App::PropertyExpressionContain | |||
|
|||
void renameObjectIdentifiers(const std::map<App::ObjectIdentifier, App::ObjectIdentifier> & paths); | |||
|
|||
void rewriteVarSetExpressions(const DocumentObject * parent, const char * nameProperty, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
VarSet rewriter
@@ -189,6 +192,7 @@ class AppExport PropertyExpressionEngine : public App::PropertyExpressionContain | |||
DiGraph &g, ExecuteOption option=ExecuteAll) const; | |||
|
|||
void slotChangedObject(const App::DocumentObject &obj, const App::Property &prop); | |||
void varSetChanged(const App::DocumentObject &obj, const App::Property &prop); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dependencies hiddenrefs VarSets
Hm, that is not good and not as intended. Thanks for reporting, I will investigate.
I don't think this is the issue here. I've added dependency tracking to the dependency tracking for hiddenrefs, so I would expect the above to work. I believe I have also a test for exactly this. The property-based dependency tracking you sketch above is something I have in mind as well and I could envision it to be a conclusion of #12868 that we need that kind of functionality. But indeed, it would be a big change from what we have now. |
Linter report:
|
I've just redid the video with the new implementation and for me it works as intended. That is, if I change the global properties of length height and thickness of the wood, the assembly changes accordingly. The only thing I can think of is that somehow property Part.Params is pointing to a different VarSet than you expect? |
Now it somehow works. Not sure what went wrong. I did a clean build of main with your PR merged. When are the expressions rewritten? After toggling the Exposed property the expression was rewritten, but I had to relink my custom VarSet. |
I can imagine that you can end up in a situation where it doesn't work properly. The dependencies are quite complicated because temporary hidden files are in the loop as well. But I think this is true for all subshapebinders that do bind copy-on-change.
Yes, good point. Indeed, you figured out yourself that this is the moment that the expressions are rewritten. Adding a property to the original VarSet breaks the model because the VarSet as argument is not equivalent with the original anymore (in the same way where a computer program is broken if you change the argument list of a function). You solved it by adding the property to the argument VarSet as well. I'm thinking on how to best approach this problem. It will probably be common that you want to add a variable to an orginal VarSet. One solution would be to only allow adding properties if the VarSet is not exposed. However, although it doesn't necessarily break existing binders immediately, it is true that you have to relink all the argument VarSets. A different approach is to allow adding properties to the original VarSets, and add validation to the variants at strategic places that tell you that a PropertyVarSet is invalid and allow you to make the current VarSet equivalent to the original one. It can then redo the expression rewrite. @NomAnor, do you have any preference or perhaps other suggestions? |
The dependency management could get hairy, I don't know enough about it to give a good suggestion. If I put myself in the shoes of a user, I would want the following features:
A someone using a parametric Part, it would be nice if the interface would not be strict. That is, if the VarSet I use does not contain all properties of the original VarSet, the values from the original VarSet are used. This way I can only overwrite the values I want to change and I'm not that suceptible to changes in the original VarSet. |
Thank you for pointing that out. I was relying on the pre-commit hooks but it turns out |
For an App::Part that has exposed variable sets, check if the expressions are rewritten.
Good point. Setting expose to false and then to true again is a solution that doesn't scale, because all the variants loose the VarSets they're parameterized with, so the user has to reset everything. I've added a check in Part that rewrites the expressions in the geometry of the part that is parameterized with new variables in an exposed VarSet. On recompute of a variant, the variant is broken because the updated geometry will reference properties that are not in the VarSets that replace the exposed one. I think this is the right behavior because the error message is quite clear and prompts the user to add the missing property in the replacing VarSet to make it equivalent again.
Yes, that is what is happening with the latest commit. The user can simply reference the exposed VarSet and then the expression is rewritten immediately.
That is a good point. That would indeed be nice. This would be very hard to do with the current implementation, but it may be possible with what I propose in #12868. |
@pieterhijma and me scheduled a video meeting tomorrow (thursday 18 april) at 15h00 CEST (13h00 UTC) to go through the code and explain in detail. Anyone interested is welcome to join/listen. We'll post a jitsi link here at that time tomorrow |
As Yorik mentioned in the comment above, we had a meeting about this PR. In this comment I will provide a summary of the meeting. Yorik and Shai were present. Thank you both for your interest! The meeting had two purposes. Firstly, I wanted to explain why this PR touches so many files and why the functionality is necessary. Secondly, Yorik and Shai could give feedback on the FPA grant proposal that was declined. In this comment, I will provide a summary on the former part. In the grant proposal issue, I will summarize the feedback on the grant proposal. I showed Yorik and Shai the purpose of this PR, enabling exposing variable sets making use as much as possible of the logic already available in FreeCAD, notably subshapebinders and the copy-on-change logic. The current implementation is not fully satisfying because it runs into some limitations of the copy-on-change logic and that's why I submitted the grant proposal. While I was explaining the code, the question came up why exactly I needed the functionality to rewrite expressions. To show this, I started FreeCAD in this branch and explained that to expose variables (i.e. making sure properties of a parent Part can be used within the child document objects), I need to get around the dependency cycle problem which I can achieve by making use of hidden references. To make it as transparent as possible for the user, I automatically rewrite expressions to use these hidden references. This also gave the opportunity to explain that although FreeCAD has logic available for variant parts, the functionality is very difficult to use for regular users and is brittle. We discussed several existing ways to achieve variant parts:
Since hidden references are not incorporated from the dependency checks (that is what the hidden means), there is separate logic available to manage dependencies. So, in a sense, FreeCAD has two dependency mechanisms. Since this PR makes use of the logic above that has drawbacks, this implementation suffers from the same drawbacks. Hence I created an FPA grant-proposal to address these issues. Although the idea behind the PR is now more clear, Yorik has reservations for adding more complicated code to the core code base. The core should be understandable by all developers and large changes should receive a high amount of scrutiny. The same reservations are also valid for the grant proposal, so I will continue to elaborate on this in the grant proposal issue. I will continue the summary of this meeting regarding the FPA grant-proposal there. @yorikvanhavre and @shaise, please comment if I misrepresent anything or if I miss something. |
I think @yorikvanhavre also mentioned that it will be better if this proposal can be divided to several steps with smaller PRs. This way it will be easier to follow the changes. |
Yes, thank you. In my opinion that was more about the proposal, so I incorporated that in the summary of the proposal (that wasn't available when you replied to this, sorry). |
I would indeed prefer this to be separated into clearer distinct topics. Basically I would make the expression rewrite system a separate PR, then one that shows more clearly what the VarSet is doing. |
@pieterhijma also here a heads up for the upcoming feature freeze on Monday, 2024-06-03. Are you planning to split this up as mentioned? |
Closes #12531
Closes #12799