feat: Introduce an action system and command language #138
No reviewers
Labels
No labels
bug
design
duplicate
engine
enhancement
maintenance
prospective
question
regression
stub
todo
ui
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
StudioInfinity/dyna3!138
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "Vectornaut/dyna3:actions"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The branch to be merged introduces actions: trait objects representing things a user can do. It also introduces a command language for describing actions.
One purpose of the action system is to ensure consistency between different user affordances for doing the same thing. On the branch to be merged, as a proof of concept, the element and regulator creation buttons work by creating and executing actions, rather than by acting directly on the application state. A newly introduced keyboard shortcut system provides a different way to trigger each of these actions (addressing issue #132). You can also carry out actions by describing them in the command language and entering those descriptions into the newly introduced command prompt component (addressing issue #133).
subject_array_from_vecless generic a485dd0ee0@glen I've moved the
Actortrait into its own module, as we discussed during the meeting, so the pull request is ready for review! I think this move is an all-around improvement: it reduces clutter in theactionmodule, and it improves the organization of action and actor imports.actormodulefeat: Introduce an action system and command languageto WIP: feat: Introduce an action system and command languageI just noticed that regulators created by descendant actions aren't actually being resolved correctly. The command
doesn't set the regulator of the newly created sphere, because we end up looking for the half-curvature regulator in a clone of the sphere creation action rather than in the original sphere creation action. The cloned sphere creation action creates a different sphere, with a different default half-curvature regulator.
I didn't notice this before because the sphere created by the cloned action has the same curvature has the sphere created by the original action. The command
HalfCurvature Spheretherefore prints the expected measurement, even though it's not reading from the expected regulator.I've put the pull request in draft mode while I work on a fix.
Selftype moreI've drafted a fix for the bug discussed in this comment. The draft includes a strengthened
descendant_regulator_resolution_testthat's able to catch the bug, using the technique described in the comment.The fix works by introducing an environment that keeps track of all the elements and regulators that can be referenced within a command. We carry the environment through the whole interpretation process, updating it whenever the thing we're interpreting creates a regulator.
The current implementation of the environment is a little rough around the edges. For example:
elements_by_idindex from the state into the environment for convenience, even though we could've saved a clone by passing the application state and using theelements_by_idindex from there.The implementation is decent enough, though, that I'd feel okay including it in this first pass. Shall I go ahead and push it to the branch to be merged, or would you like to discuss it at a check-in meeting first?
@Vectornaut wrote in #138 (comment):
Yes, I am keen to move this along and in to the main branch. Thanks!
WIP: feat: Introduce an action system and command languageto feat: Introduce an action system and command languageOkay, I've pushed the fix and reactivated the pull request! Commit
1405329does a reorganization that I'm finding helpful for implementing the "details" or "attributes" syntax we'll use to avoid repetition in the engine block and elsewhere in our save files.In one of the comments you say
I just want to say that aspect of UI design is not at all clear. There are absolutely cases where one wants to run multiple actions on the same selection, and there can be cases where it's a bit of a process to get just the selection one wants -- say all of the elements of some conceptually coherent "subassembly," especially if we don't have a formalized concept of that within dyna3 -- and it could be frustrating for that selection to disappear and have to be selected again after, say, a color change but before wanting to do a move or a copy or something. So my intuition is that actually the default for actions is that they leave the selection alone. Actions that delete some or all of the selection elements should likely reset the selection. Possibly actions that create new element(s) should switch the selection to those new element(s). Not at all sure on some of these points, we should just implement the path of least resistance (which I gather is to leave the selection be) and then later see if we find ourselves wishing something different had happened.
You don't need to change the comment in this PR, to avoid rebasing later work, but as convenient please remove the comment and add something in the issues/wiki about designing selection behavior resulting from actions. Thanks!
@ -141,2 +158,4 @@}impl Display for dyn Element {fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {Where is this used? Isn't it more important to show the id, which uniquely designates the element, than the label? I'm not necessarily saying don't show the label, but the id seems a higher priority piece of information, if anything.
When you enter an element descriptor at the command prompt, this
Displayimplementation is used to print the element on the browser console. In general, theDisplaytrait is used for "pretty printing" structures. Note thatdyn Elementalso implements theDebugtrait, which I think is more often used to format information about a structure's internals; theDebugformatting just gives the ID.In the next pull request, we'll start showing each element's ID after its label in the outline view. For consistency, in that pull request, maybe we should also start showing an element's ID in parentheses after its label in the
Displayformatting.Sounds fine to leave this until the next PR, but please make the ID the primary identifier and if you want to show the label, make that appear "subordinate" in some way, like being in quotes after the ID for example. Resolving for now.
@ -208,3 +228,2 @@Self::new(id,format!("Sphere {id_num}"),"sphere".to_string(),Is it a worry that multiple calls to the default() method for Sphere will produce entities with the same id()? Ultimately we never want there to be such an occurrence. Is there currently no provision to prevent such id collisions?
The default ID provided by
Element::defaultis used as a base for the ID assigned byAssembly::insert_element_sequential, which appends a sequence number to prevent collisions.In the current design of the command system, it's hard to check for ID collisions at the time when elements are created. This is because when we interpret a command, we create all the new elements it refers to before any of them are inserted into the assembly.
The next pull request will introduce an interpretation environment structure (
Environment), which might make it easier to check for collisions at the time when elements are created. I'm not sure we'll want to do that, though. The current design, withinsert_element_sequential, feels like a hack to me, but I also feel like it's too early to put a lot of effort into trying to come up with something better.@ -309,3 +331,2 @@Self::new(id,format!("Point {id_num}"),"point".to_string(),Same question as for Spheres -- issue with always multiple ids "point"?
Same answer as for
Sphere.@ -458,3 +489,3 @@let [row, col] = self.subjects.each_ref().map(|subj| subj.column_index().expect(indexing_error("Subject", subj.id(),indexing_error("Subject", &subj.id().get_clone(),I probably skipped over this same point a couple places above, but does get_clone produce a new signal? If so, it seems odd to me that an error message should need a signal rather than just the current value of the signal... Or am I misunderstanding?
When a signal's value type implements
Clone, you can useget_cloneorget_clone_untrackedto get a clone of its current value, with or without reactive tracking. When a signal's value type also implementsCopy, you can usegetorget_untrackedto get a copy of its current value. When a signal's value type doesn't even implementClone, you can work with its current value usingwithandwith_untracked.Following up on our discussion during yesterday's check-in: I've confirmed that this code should call
get_clone_untrackedrather thanget_cloneto avoid unwanted reactivity. It's corrected in the next pull request, where it would otherwise have caused a noticeable bug for the first time.OK, I will merge despite this bug since you have a correction on its heels; let's get the next PR filed as soon as feasible.
@ -571,3 +618,2 @@let col = self.subject.column_index().expect(indexing_error("Subject", &self.subject.id,"point-coordinate regulator").as_str());indexing_error(Another comment about all of these error messages: it's a bit of a code smell when there are so many copies of the same code phrase. Is there any way to reduce the boilerplate here? Move this identical shared logic "up the tree", so to speak?
Nice catch! I've filed issue #144 to propose a refactoring.
@ -717,3 +813,3 @@id_num += 1;id = format!("{default_id}{id_num}");id = format!("{id_base}{id_num}");}Just for the record, I'd suggest a sort of loop that sets the id and tries it inside the loop, so that the acceptable id is properly set once the loop is exited, so that the logic of converting the id_num to an id does not have to be repeated (creating the possibility that the two expressions would get out of sync by accident). No change necessary given the stack of changes hereafter, but please do watch out for this sort of non-DRY code pattern.
I'm reading this as a suggestion to make one or both of the following changes (in future code). Could you clarify whether you meant one, the other, or both?
Move the initialization
inside the trial loop, so that "the logic of converting the id_num to an id does not have to be repeated."
Move the assignment
inside the trial loop, and do it on every loop step, so we can't accidentally remove it later. Note that this assignment does not repeat the logic of converting the
id_numto an ID. The ID we assign is exactly the one for whichelts_by_id.contains_key(&id)returnsfalse, breaking us out of the loop.I meant only the first of these. I think you'll find it best in the long run not to have two lines of code that need to match for the code to really make sense, even when the lines are close to each other.
I don't see any reason to reassign the id of the element multiple times as you try to find a fresh id; setting it once after the loop indeed seems better. Resolving.
@ -8,4 +14,4 @@};#[component]pub fn AddRemove() -> View {I realize that this is not a change in this PR, but I am confused about the name of the AddRemove component -- there is not actually any provision for removing anything from an assembly at the moment? If not, then the next touch on this source file should probably either add a removal ability or rename it to just be about adding. Again, no change needed now, and I understand that maybe the plan is to put removal here, but plans have a way of changing and it seems better to have things consistent in the meantime. Or maybe I am mistaken and there is some facility for removal?
Yes, I named the component
AddRemovebecause I intend for it to eventually include a removal button. You're correct that we don't yet have any way to remove elements or regulators.Fine with me. Keep in mind, though, that if we rename
AddRemove, we'll also have to editmain.rs. We'll have to editmain.csstoo if we want the corresponding DOM element ID to be consistently named.One heads-up: since the command language doesn't have any "verbs" yet, adding a removal command would involve adding new syntax to the command language. Of course, we can always add removal actions without corresponding commands, use those actions in removal buttons and keyboard shortcuts, and file an issue to make those actions accessible through the command prompt later.
All fine.
Please let's not -- I very much want to maintain one-one parity between commands and actions, so that everything that occurs can show up in the command listener. So every action should be the possible result of a command, and of course vice-versa. (Not every action neccessarily has to be available via some shortcut or via some menu item, though.)
I would mildly disagree that the command language has no verbs;
regulator @ 0.5is implicitly a verb -- it could have been writtenset regulator to 0.5.I doubt that a syntax like this one for
@is appropriate for most verbs, though. So I agree, the most straghtforward sort of a command for deleting an entity would be just an imperativedelete regulator. And yes, the language will have to be able to parse such things and we will have to worry about whether there could be an entity with iddeleteand so on -- but that's life.Closing this conversation for the sake of this PR, as it was just prospective.
P.S. if there was a strong reason for preferring a syntax like
regulator deletethat had the operand(s) first followed by the verb, that could be OK, but absent any such reason, the pseudo-English imperative word orderdelete regulatoris better@ -66,1 +33,3 @@}ParametricActorButton(parameter = state.selection_receivers.map(/* TO DO */On our call today, let's quickly go over the selection_receivers mechanism -- I just don't have enough of a grasp of Rust yet to decipher the fairly intricate typing/closures/whatever is going on here... Thanks!
OK, I have finished going through the code. I don't think any of my comments require a code change at this point, unless you are motivated by any of them to do so. I think some of them may involve scheduling future code tweaks, but I have tried to be cognizant of the fact that you have code stacked on top of this. So I think you can generally just respond to/acknowledge the comments and then we can merge. (Well, I will also give it a spin and run the tests, but I presume as you have been using this for some time that all that will go fine.) Thanks!
OK, it seems to run fine. Once this PR is merged, please file issue(s) for the following lack-of-feedback situations:
The second item raises the question on whether there should be any feedback on unbound keys. I think as long as bound keys reliably produce feedback that something occurred, there need not be any feedback for unbound keys, so no action item on that unless you have a different view on this point.
And then there is the question of what sort of keys we should use for shortcuts. I believe that generally speaking the most commonly used default shortcuts should not need any modifiers. So if "create inversive distance regulator" is going to be a basic standard shortcut and it is going to be on the letter r, I would recommend that the shortcut be just plain (i.e. lowercase) "r" rather than "R" requiring Shift-R to invoke. Of course, that's really a future issue (I sort of doubt this will end up on the letter "r" in the end anyway), so please just create a wiki item or issue on shortcut design. Thanks!
OK, the examples and tests compile and run without error, so once we've resolved all the above conversation points this will be ready to merge. As I said, I suspect that can happen without any code changes except any you might be motivated to do by the comments. Thanks so much!
Both of these situations would be resolved if we addressed the interface enhancement issue #139. Should I give them their own issues anyway, since we could also resolve them without addressing #139?
@Vectornaut wrote in #138 (comment):
Excellent question. I think it's best for them to have their own issue, as it isn't necessarily clear if updating the command listener entirely suffices for feedback: we definitely want in the future the ability to hide/show/possibly rearrange the various views (e.g. have two distinct graphical views open, maybe not have the outline view, etc.). Such configurability might encompass not having the command listener visible. So if that's the only source of feedback, just updating the command history might not be enough feedback.
I've added a section on keyboard controls to the "Interface" wiki page! (I also tidied the page up a bit.)
@Vectornaut wrote in #138 (comment):
Looks great.
Done, through issues #142 and #143! I've written that #142 could be resolved by addressing #139, because I think the command listener will necessarily be visible and salient when someone is entering a command. In contrast, I've emphasized that efforts to address #143 shouldn't depend on a particular view being visible, as you've pointed out here.
I think I've now addressed all the review questions and filed all of the issues on our to-do list. If you don't see anything else to do, the branch should be ready to merge.
OK, merging in the interest of moving on to the next, more polished PR.