-
Notifications
You must be signed in to change notification settings - Fork 537
WIP Named Parameters #1621
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
base: master
Are you sure you want to change the base?
WIP Named Parameters #1621
Conversation
…ame is in the request. Includes text window UI for creating and editing these parameters in a group, including the references group.
… length constraints.
… in distance or length constraints. We will store the expression in comment and the units in valA.
|
I haven't been following the named parameters discussions too closely, so forgive my ignorance, but I'm not sure what the scoping rules are, so a clarification would be very much appreciated. The comments in the code of this PR imply that the scope is based on a group's handle value, but that's not how entity scope is determined today. For regeneration, the scope is based on group order, which is mostly determined by group dependency (through Additionally, the code in this PR doesn't deal with named parameter dependencies, so when a named parameter is deleted, any requests and constraints that depend on it are left dangling. I'm not sure what the best way to deal with it is, but we should at least trigger a regeneration and prune such requests or constraints (honouring the scoping rules), similar to how entity deletion is handled today. A more difficult case is parameter rename, which doesn't seem to check for name conflicts, and can also leave dependants dangling. Lastly, how should we deal with parameters that redefine constants (such as I only skimmed the code in this PR, so I'm sure I'll have more questions later when I go through the code more thoroughly, but these are the ones that came up during my first reading. |
|
@iscgar the current implementation does not follow sketch generation scope rules. The plan is to create a valid set of names for the current group when generating a sketch. That would start with a copy of the parent group and adding any names from the current group. It will also crash if you delete or rename a parameter that is used in a constraint. This will be "solved" when we add the ability to dynamically create a parameter in the parser. To do that it will need the handle for the constraint. Parameters can belong to a request, a group, or a constraint. The text window ones belong to the request. Dynamicly created one will belong to the constraint they are first encountered in. Thats also how it will cope with deletions or renaming - constraints will just create a new parameter with the old name. If someone wants to write a renaming algorithm to update all occurrences when a name is changed thats fine but not strictly necessary. On the fly parameter creation is important for the relation constraints and the old demo. You can also define parameters on the sketch with "width = 12" or similar. That stuff also turns Solvespace into a small symbolic algebra tool. BTW I tagged you specifically because you know the solver and regeneration process. |
Am I correct in my understading that that plan is to effectively follow the sketch regeneration scoping rules, and that the reason the current code doesn't do so is merely because it's a WIP draft for getting the ball rolling?
Since named parameters can also contain expressions, dynamically generated parameters will also need to be generated by requests, not just constraints. One related concern that I forgot to mention: the bit space in constraint handles doesn't allow creating more than a single parameter (see my comment #1574 (comment) and #1574 (comment)), and the space in request handles is also very limited, although it should probably suffice (the approach in this PR, which reuses the 6th bit, allows creating up to 32 parameters). What scope should the dynamically generated parameters belong to? Only the request or constraint that uses them? That seems like a recipe for surprises, since the solver will be free to solve parameters with the same name differently. Scoping them to the group that the request or constraint belong to seems more appropriate to me, although I'm likely missing some other corner cases that would be surprising with that behaviour. This also brings up the issue of defining the same named parameter multiple times in the same group. The current code doesn't seem to handle that case, and from a cursory look at this PR it seems that the last parameter "wins" (because named parameters are iterated in order of request handle values).
Instead of keeping the expression in the request or constraint as a string, we could store the parsed expression, which would refer to the handle values of the parameters (although note that it would require decoupling the expression system from the sketch's temporary heap, as those expressions are long lived, unlike the current use of expressions only during regeneration, which allows them to be discarded as soon as a group is solved). That way, renaming a parameter doesn't require walking the tree, and deleting a paramater can be dealt with by either pruning the request/constraint during regeneration, as we do for dependant requests and constraints today, or we could just resolve them with dynamic parameter generation (although see my comment in the next paragraph about the scoping of dynamically generated parameters).
The old demo is presumably this video by Evil-Spirit? Looking at it seems to imply that dynamically generated parameters don't belong to the constraint that mentions them, since all As for sketch parameters, I'm assuming defining parameters on the sketch is the same as defining them in the references group, since you can't sketch anything in that group anyway, and it's the root of the group tree. |
@iscgar yes. See also the https://github.com/solvespace/solvespace/commits/parameters/ which is the "modern", unfinished implementation. And (in case you are not aware of it) the O.G. issue and discussion #77 And this #1482 where @dgramop has a WIP of using parameters for the number of repeats in "Step trans/rotating" groups. |
Thanks for the links! I did read both of those discussions, but it's always good to referesh my memory. After reading the discussions I'm still not sure how scoping for dynamically generated paremeters should be handled, especially for partial regeneration. This is because we'd need to collect named parameters from parent groups even if they are not being solved, just so we don't end up introducing free parameters where the user expects to reuse the solved ones from the parent groups. If the dynamically generated parameters are scoped to their group rather than to their request/constraint, that would be trvivial to implement. Additionally, what should be the interactions with linked assemblies? Right now their solved form is being used and they never get regenerated during a parent regeneration, so we don't really need to do anything regarding their named parameters. However, if we ever get hierarchical sketches, I suppose we'd like them to be dynamic and be affected by parameters defined in the parent sketch or vice versa? Supposedly child sketches will be handled differently than linked assemblies anyway (i.e. they won't be groups of static entities), so we can just defer the decision to when we get hierarchical sketches. I would love to implmenet the regeneration part, but coming back to my points above, it would be great to properly specify the behaviour of dynamically generated parameters and the scoping rules in general. If we really need constraints to dynamically generate parameters, we might want to hold off the implementation of named parameters until 64-bit handles land, since with the current encoding of contraint handles it's impossible to encode more than a single parameter as I mentioned above. Alternatively, we can let the constraint's group be the owner of these parameters, and in that case there shouldn't be any issue with their encoding. However, we still need to decide what to do in some of the weird cases that I mantioned above, such as multiple parameters with the same name defined ina single group, or worse -- a named parameter getting renamed to the name of an existing named parameter in the same group. If allowed, that last case will be sure to cause confusion since the parameter handles are not exposed in the UI, and it'll not be obvious which parameter shadows which. The behaviour for the rest of the cases I presented (such as shadowing a known constant, which we might want to disallow, and also reserve a bunch of names to give us the option to use them as constants in the future) would also need to be defiend, but I feel that it shouldn't be a blocker for an initial implementation. |
|
@iscgar each group should inherit (copy) the name/handle dictionary of its parent group. Then add any names from requests in the current group, overwriting the handle of any duplicates - most recent wins. Then when generating equations for constraints we would ideally create new parameters for names that don't exist yet. These would also get copied to subsequent child groups. This would allow writing "width=5" on a sketch (relation constraint) and using it throughout, without needing to use a request. Are parameters that belong to constraints recreated on regeneration? Are they saved to files? (I think they probably are?) You noted in the old video a triangle gets a side "sqrt(aa+bb)" and the other sides are "a" and "b". We don't care which constraint those 2 parameters belong to, they could belong to the group. What matters is that all instances of "a" in that group refer to the same parameter. But notice the expression was used instead of "c". If it were called c then we'd have aa + bb = c*c - think pythagorous. After that expression was added it became a right triangle. A and B could be independently changed, but the angle stayed 90 degrees! As for linked sketches. I wrote a plan for those recently ( issue #1436 ). If we have file A and in group 5 we link file B, we want to show parameters defined in B references group in File A group 5. Once we can load and solve linked sketches, those parameters in File A group 5 will be copies of those if File B and we will allow them to be edited. Their values will be copied to that instance of file B prior to solving. In this way you'll be able to create say an extrusion of 8020 with a length defined in references. Then you can use multiple instances of that part in another sketch by linking multiple times, and each one could be a different length. Reusable paramePythagoras. We can do all of this without dynamic creation of new parameters in the parser, using text window definitions. But 1) it can be nice to just write them on a sketch. And 2) we will need to handle renaming and deletion somehow. Those operation will orphan a name in other parts of the sketch and the expressions won't parse. They can't just revert to a previous value either as there is no meaningful number in valA. |
It just occurred to me that if a stored constraint expression becomes invalid because a variable got deleted or renamed, we could simply not generate an equation for that constraint. The sketch would gain a degree of freedom, but nothing should be moved by the solver. We might have a way to inform the user that the constraint is broken and maybe indicate "err" for the group too. Would it be possible to do this with a generic equation like 1=0 ? |
Currently parameters belong to requests, and the only constraint that can generate a (single) parameter is the point on line constraint (see cc07058 for the reason). But yes, parameters get saved into the file, although that's only for the benefit of assemblies. When loading a model into a sketch (as opposed to linking), everything gets regenerated from groups, requests, and constraints.
What I meant was to ask about a case where there aren't any other constraints that use
Why wouldn't the expression parse? We can store named parameter references as new expression nodes (lazily bound parameters, resolvable by name), and fix their references only during regeneration, when they are being solved. This is similar to a resolution step that is already taking place today during solving, where parameter handles in the expression are resolved to direct parameter pointers. That way we can fail at resolution time with a nice message about an unknown parameter, allowing the user to define it and retry, similar to how the solver can fail when a set of constraints is unsolvable, and the user can make changes to let the solver retry. To support falling back on the old value while the user fixes things, we can keep a shadow copy of named parameters when a user renames or deletes a parameter, which will be discarded after the first successful regeneration (as a successful regeneration means the operation didn't orphan any parameters that later expressions depend on). |
Parameters need to be saved in a file so the sketch has reasonable dimensions. When you draw a line the endpoints use 2 parameters each (x, y). The line entity has handles for the 2 point entities and then each point has handles for 2 parameters. All coordinate parameters have to be saved because of that. I was wondering if the constraint-created parameters are also saved, but I suspect the answer is yes for similar reasons. They also should not be discarded and recreated during sketch regeneration for the same reason. I am not very familiar with the whole regenerate process.
But it won't. When you type "a" into a dimension constraint (in the old demo), a parameter gets created and is substituted into an equation defining the distance between 2 points. That parameter value will change as you drag an end of the line and change its length. Currently this will also happen with my named parameters. If you define one in the text window of a group and then use it as a distance for the length of a line in that same group, you can still drag the line length and the parameter value in the text window will change. If the line is in a later group, the length will be fixed because the parameter is treated as constant since it's from a previous group. I don't know if I like this behavior. We may want to "lock" the parameter to the typed value. Relation constraints will also allow writing "x=5" on the sketch, so locking in the text window would prevent that. For that reason I think there needs to be an option to lock them in the text window. The original issue #77 author suggested names could define a variable or a constant. I think one request type is enough, but a checkbox to lock it might be needed. I'm not sure what the internal mechanism would be for that - a request can generate a constraint, so that may work. I thought there might be a flag on parameters that prevent the solver from changing them but I don't know the solver well enough to be sure.
We can do relation constraints without dynamic parameter creation. If we find a way to do it later it should be possible to add without breaking compatibility.
The parser throws an error for parameter/constant/function names it doesn't recognize. We validate an expression in mouse.cpp when it is typed. We store the expression string in a constraint, but it is re-evaluated in constrainteq.cpp during regeneration. If the name is changed and the sketch regenerated there will be an expression that won't parse. This currently causes a crash. As I suggested in a previous comment, we could simply not create an equation for the constraint in that case, or even create an unsolvable one to trigger existing warnings. I'll try that tonight if I have time. |
|
I've read everything but have not thought carefully. That said I want your opinion on what should happen in the following situation:
To clarify "x" is NOT defined in any of the "explicit" ways you are discussing. Bonus questions:
|
…ing or return an invalid constraint equation to let the user know something happened.
|
I prevented the crash when a named parameter is deleted but still used in constraints. The red background will appear and then somehow it will figure out which are the bad constraints and delete them with a message box. |
|
@ruevs as it is right now you can only use parameters that are defined in the text window, and parameters defined in the current group are not fixed value. So you can do the entire demo/video so long as you define all the names (x,y,a,b) in the text window of g002 and draw everything in g002. Dragging a line of length x will change the value of x - you can see that value change in the text window. Adding a second constraint to the length of a line and typing in a number will force the line to that length, and hence x will become that value as well. Drawing a triangle and giving the sides length a, b, sqrt(a * a+b * b) will force legs a and b to be perpendicular but not of fixed length. The only thing missing here is full scope rules. It does obey the rule that params defined in a previous group are held constant for the current group - the solver enforces that because these parameters are defined correctly. All we need now is for the list of parameters to include ONLY those that can be used in the current group (not future groups) and override the handles in the appropriate way if there are duplicate names from different groups. So in expr.cpp and EditControlDone we need to pass in a "current" list of names. |
Right, the constraint created param is also saved. During regeneration all of the parameters are solved again, although the values loaded from the file are used as initial guesses, so in practice the regeneration should converge to almost exactly the same values as in the file.
The solver will generally try to keep the initial values if they still satisfy the constraints. However, when dragging a point of a line segment, it must change either the parameter in the length equation or the parameters of the other point, since the constraint isn't satisfied with the existing value (it has less effect on the dragged point, due to the dragged logic that limits it from making significant changes to dragged parameters, but otherwise it can still affect the dragged point as well), and that's when unexpected things may happen depending on the order of equations provided to the solver. I haven't tested the parameters branch, but you can see that in the old demo Evil-Spirit was forced to put a
Locking parameters should be easy enough to do. All that is needed is for the generation code to mark them as known, which causes the solver to resolve them as constants. So if a "lock" check box is introduced for the named parameter request, it can easily be implemented within the existing regeneration framework.
Sure, but what I'm suggesting is to modify the parser to allow parsing such expressions without crashing. This can be done by introducing a new expression node (which I termed "lazily bound parameters") that can be resolved at evaluation time by passing a list of params to use for the resolution. This is an easy change to do, and is the most logical IMO. That way we also easily deal with renames and deletions, since resolution will be delayed until we actually evaluate the expression. It'll also allow us to bail at solution time and point directly to the broken constraint, without the need for hacks like |
|
@phkahler I took your code and created an initial draft of the scoping rules that you described and my idea of the parser changes in a branch. Note that this branch does not yet persist named parameters to file, so any file that uses named parameters will be broken. The main changes from the code in this PR are:
|
|
@iscgar I'm going to need some time to review what you're doing. At a glance it seems more invase, but a lot of the changes are small. Right now the version in this PR can save and load files - there was no new code needed for that. In mine, I see that diameters are not shown correctly. |
Most of the invasiveness in my branch is due to also decoupling
That's indeed a plus with your implementation, though I don't think it justifies the added complexity with the request handling all over the place. Adding the ability to save and load named parameters shouldn't be too difficult, and I'll try to work on it during the weekend so the two approaches could be compared. |
I'm not sure what you mean by "request handling all over the place". The text window needs something - I do want to define them primarily in the text window, but in relation constraints and on-sketch expressions is a very nice to have. I'm going to move the creation of the "dictionary" into the group generation area next, so that should be cleaner and only in one place. None that means they couldn't belong to the group instead of a request, but groups (so far) have fixed number of parameters depending on the type of group. One thing I hated about the original demo be evil-spirit was that he added names to the parameters themselves and saved those in files. Parameters in Solvespace were never meant to be any kind of object. They are just a double with a handle that can be used by the constraint solver. Objects (groups, requests, constraints) have parameters, not the other way around :-) I hope to have time to finish my approach in the coming week as well. |
What I meant was that with requests there is no easy way to find the group's parameters other than iterating over the entire request list, and that leads to things like the double iteration when showing the group info in the text window, or during regeneration. As you mentioned, though, my approach isn't without downsides (the biggest of which is the need to add new code for saving and loading parameters, whereas your approach Just Works by reusing the request serialisation logic).
In my implementation the named parameters use the upper half of possible group parameter values, in order to allow future groups to use the entire bottom half for fixed parameters (or other uses that may arise) if needed. the total allowed parameter range per group is 2^16, so that should leave plenty of room for future expansion, and the room for named parameters should also be enough, as I don't think users will ever need to define more than 32K named parameters. Regardless of whether named parameters are requests or a group property, another question is whether name resolution should be performed at expression parsing time or deferred to expression evaluation time. Your implementation chooses the first option, whereas I went with the second one. Deferring resolution to evaluation time allows the solver to handle resolution failure as a first class citizen, clearly indicating the issue and pointing to the list of bad constraints, resulting in nicer error handling. I feel that requiring them named parameters to be resolved at parsing time is needlessly limiting, and leads to hacks like the unsolvable equation that you needed to add when resolution fails in order to allow the solver to find the bad constraint (which still isn't a nice solution, because the user has no way to tell why the constraint is bad).
Sounds great. I'm looking forward to seeing the complete implementation. |
|
I cleaned up the code a bit, dropped the decoupling of EDIT: I updated my implementation to include some quality of life improvements, such as showing the unresolved parameter names in the text window on resolution failure, as well as fixes to bugs that I discovered while testing. |
@phkahler @ruevs did you get a chance to test my branch? I would love to hear your thoughts and get your feedback regarding the approach I took. |
|
@iscgar I have not had time yet. How does it handle names that only exist in an expression? Are they added to the group? At what index? @ruevs my version still needs to inherit a list of names from the parent group to get the scope handled correctly, and also needs a toggle to lock a value in the text window (const vs varaible). |
No, you have to create named parameters on the group to use them in an expression. Any unresolved references result in an error in my version, with a clear explanation of the issue in the text window:
Once you get this error, you can either edit the expression if there was a typo, or select the group in the text window and add the missing named parameters in order to fix the issue.
Is this really needed? I was under the impression you preferred to use the references group for "constants". |
Off topic: @iscgar by adding tooltips you've touched the infamous I'll pull and play with your latest changes tomorrow. Not sure if I'll find time to read the code. |
Well, it's different from other GUI libraries I've used in the past. When I first encountered it I wasn't sure what I was looking at (the name While having a proper GUI toolkit abstraction would probably make the code easier to interact with and understand, I'm not sure it justifies the effort that it would require. On the one hand you'd be able to add things like text boxes, and maybe even fancier controls, like combo boxes and proper radio button groups. On the other hand, you'd then need to deal with things like scrollable containers and Z-order, which currently simply aren't a thing. |
|
@iscgar How did you implement the lock/unlock icons? I searched and couldn't find those in the unicode BMP :-) Also, how are you handling units in your parameters branch? Or are you? I like your version of the text window. Also glad you found the value in locking - sometimes you want to have a parameter with a fixed value and just use it in the current group. My intent for the references group would be for "global" or high level parameters to be defined there, and also to reflect those in linking group. |
That's because they aren't standard BMP glyphs :) Lines 675 to 682 in 01f58b7
They are then referenced using raw UTF-8 bytes in the strings handed to I simply added two new PNG files for the lock states, and assigned them to the next empty slots in the PUA.
The same way it's handled in your branch, with the unit multiplier saved in |
|
Any updates on this PR? The discussion here sounds to me like you guys are almost there! |
@TerryGeng We've gone in slightly different directions. With holiday vacations coming up I'm planning to pull this together in the next few weeks. My plan is to use my original concept of having requests for parameters in the text window and internally using a per-group dictionary copied from the parent group and extended by new requests (or values overwritten for duplicate names). This avoids adding any new members to C++ classes or anything new to files, just some new request types. I'll use @iscgar layout of the text window because it's clean and he added nice locks. We'll use 2 request types for locked or unlocked parameters (the UI can just change the type). Actually for future-proof (linked sketches) there will be 2 more reserved parameter request types but that stuff will be transparent to the user. That much I want to start on Dec 20th and get done ASAP. After that is in place and working I'll hope to turn @dgramop loose on bringing relation constraints on top of this (if still interested ;-). We will not be allowing new names to be defined in relation constraints yet - that's going to be a little bit tricky (meaning I'm not sure the best way to create the parameter handles yet, but it will be possible to add). I'd like to get version 3.2 final out this year, and then immediately start putting the named parameters stuff in. If all goes well there could be a 3.3 with that stuff by spring - I'm happy to have a release just for this feature. I'm never as fast as I hope, but we are very close with this like you said. This was first prototyped when? In 2016? so 10 years. Ugh, it's time. |


This is my concept for named parameters. I think it's good so far but needs a couple tweaks.
You can create named parameters in the text window of any group (including references g001). You can edit their values in the text window but they are not fixed as constants.
Parameters from previous groups will be held constant while solving the current group, so anything you define in references will not change in later sketch groups.
Units are handled. Any dimension takes on the Units currently selected at the time you type the expression. Internally the units conversion is held in valA of the constraint and the expression in the comment field.
TODO:
There is code in GraphicsWindow::EditControlDone() and ConstraintBase::GenerateEquations() that creates a dictionary of parameters from the entire sketch. This should be in a separate function. That will also be where scope rules are enforced by making the list valid for the current group. I propose adding such a dictionary (std::map) to each group (not to be saved in files). Then we can copy the parent group and add any new names ONLY from the current group and overwrite the handle for duplicate names. This should give us the appropriate scope rules.
Once scope is handled properly I think we could enable on-the-fly creation of new variables by passing down the constraint handle to the parser. New names would be added to the current group dictionary. It won't matter which constraint uses it first as these will be temporary and recreated with sketch regeneration. I think they'll also be usable in subsequent groups.
Once step 2 is done we can reintegrate the relation constraints from @dgramop very easily.
Testing and comments welcome.
Also wondering if @iscgar might want to comment on or implement part 1 above.