undo a command (scripting)

Next
 From:  pressure (PEER)
10939.1 
I’ve been having problems with undoing custom commands. Hoping that someone can shed some light on how to change these commands so that undoing them works better.

If I have something selected, run this:

code:
    var factory = moi.command.createFactory( 'line' );
    factory.setInput( 0, moi.vectorMath.createPoint( 0, 0, 0 ) );
    factory.setInput( 1, moi.vectorMath.createPoint( 0, 0, 10 ) );
    var lineInList = factory.calculate();
    
    moi.geometryDatabase.deselectAll();
    
    moi.geometryDatabase.addObjects(lineInList);


and then hit Undo, the new line goes away, but the previously selected object stays unselected.

But, if I swap the order of the last two lines:

code:
    var factory = moi.command.createFactory( 'line' );
    factory.setInput( 0, moi.vectorMath.createPoint( 0, 0, 0 ) );
    factory.setInput( 1, moi.vectorMath.createPoint( 0, 0, 10 ) );
    var lineInList = factory.calculate();
    
    moi.geometryDatabase.addObjects(lineInList);
    
    moi.geometryDatabase.deselectAll();


Then hitting Undo once causes the previously selected object to become selected again, while hitting Undo a second time makes the line finally go away.

How should I get a command to act like a single thing (1 undo unit generated ?) so that hitting Undo once brings back the scene exactly as it was before running the command?

EDITED: 4 Jan 2023 by PEER

  Reply Reply More Options
Post Options
Reply as PM Reply as PM
Print Print
Mark as unread Mark as unread
Relationship Relationship
IP Logged

Previous
Next
 From:  Larry Fahnoe (FAHNOE)
10939.2 In reply to 10939.1 
Hi Peer,

I'm not finding the reference for setCommandSpecificUndo( boolean) right off, but I think it may be part of what you're after. I would add it to the initialization portion of your script.

code:
moi.command.setCommandSpecificUndo( true)

Edited to add:

Hmm, this might be to allow commands to have finer granularity of control over the undo behavior during the script's execution as opposed to an atomic undo that restores the model to the state prior to the script running. I'm sorry, I can't find a better reference.

--Larry

EDITED: 4 Jan 2023 by FAHNOE

  Reply Reply More Options
Post Options
Reply as PM Reply as PM
Print Print
Mark as unread Mark as unread
Relationship Relationship
IP Logged

Previous
Next
 From:  pressure (PEER)
10939.3 In reply to 10939.2 
Thanks for thinking about this Larry. I just tried adding that line to the beginning of the 2 versions above, but don't see any difference in undo behavior. You're probably right about command specific undo being for controlling undo while a command is running rather than after it's finished.

- Peer
  Reply Reply More Options
Post Options
Reply as PM Reply as PM
Print Print
Mark as unread Mark as unread
Relationship Relationship
IP Logged

Previous
Next
 From:  Michael Gibson
10939.4 In reply to 10939.3 
Hi Peer,

re:
> You're probably right about command specific undo being for controlling undo while a
> command is running rather than after it's finished.

Yes that's correct that's for stuff like the polyline command where it handles undo itself within the command.

I'll take a look at your example script in a bit here.

- Michael
  Reply Reply More Options
Post Options
Reply as PM Reply as PM
Print Print
Mark as unread Mark as unread
Relationship Relationship
IP Logged

Previous
Next
 From:  Michael Gibson
10939.5 In reply to 10939.1 
Hi Peer,

re:
> If I have something selected, run this:
> <...>
> and then hit Undo, the new line goes away, but the previously selected object stays unselected.

That's normal behavior, when a geometry undo unit is created which happens after the command exits, the geometry undo unit will have whatever selection is on the object at that time.

Since the previously selected object was not either created nor deleted during the command's execution, it will not get a geometry undo unit generated for it and so it will be unaffected by doing an undo.


re:
> But, if I swap the order of the last two lines:
> <...>
> Then hitting Undo once causes the previously selected object to become selected again, while hitting
> Undo a second time makes the line finally go away.

This happens because moi.geometryDatabase.deselectAll(); generates a "selection undo" unit in addition to the geometry undo unit.

Selection undo units are ephemeral and only undo the most recent selection or visibility change. You can stop one from being generated here by setting selected=false directly instead of using moi.geometryDatabase.deselectAll(); Like this:

moi.geometryDatabase.getSelectedObjects().setProperty( 'selected', false );


> How should I get a command to act like a single thing (1 undo unit generated ?) so that hitting Undo
> once brings back the scene exactly as it was before running the command?

MoI's undo does not operate like that, it is not guaranteed that selection of objects that were not created or destroyed will be modified when doing an undo.

But if you want consistency between these 2 variations, then use moi.geometryDatabase.getSelectedObjects().setProperty( 'selected', false ); instead of moi.geometryDatabase.deselectAll();

- Michael
  Reply Reply More Options
Post Options
Reply as PM Reply as PM
Print Print
Mark as unread Mark as unread
Relationship Relationship
IP Logged

Previous
Next
 From:  pressure (PEER)
10939.6 In reply to 10939.5 
Hi Michael,

So would it be correct to say:
  1. At most 1 geometry undo unit is made per command.
  2. A geometry undo unit is only made if a command causes one of the following to either be added to or deleted from the database:
    • annotationObject
    • geomObject
    • objectStyle
  3. These methods are the only ones that trigger creation of a selection undo unit:
    • deselectAll
    • invertSelection
    • selectAll
    • selectLastCreated
    • selectNamed
    • selectLoop
    • selectVisible

Maybe this explains some other stuff I've been wondering about. It seems like no undo units are made if either of the following commands is run:

code:
    DoCannotUndo();

    function DoCannotUndo() {
    
        var topObjectsList = moi.geometryDatabase.getObjects();
        var topObj = topObjectsList.item(0);
        var subObjList = topObj.getSubObjects();
    
        for ( var i = 0; i &lt; subObjList.length; ++i ) {
    
            var subObj = subObjList.item(i);
    
            if ( subObj.isSeamEdgeCurve === true ) {
                subObj.hidden = true;
            }
        }
    }


code:
    var selObjList = moi.geometryDatabase.getSelectedObjects();
    var annotationObjectList = selObjList.getAnnotations();

    for (var i = 0; i &lt; annotationObjectList.length; ++i) {
        var annotation = annotationObjectList.item(i);
        annotation.arrowheadType = 'dot';
    }


Is that because they don’t add/delete an annotationObject, geomObject, or objectStyle and also don't contain any of the selection methods above? Is there a way that I can make this sort of command undoable?

Something that confuses me about DoCannotUndo is that if a command that does some other stuff that causes an undo unit to be made also contains DoCannotUndo, then undoing will undo that other stuff, but won't make the seams hidden by DoCannotUndo visible again.

Here's an example: http://moi3d.com/forum/index.php?webtag=MOI&msg=10937.1
  Reply Reply More Options
Post Options
Reply as PM Reply as PM
Print Print
Mark as unread Mark as unread
Relationship Relationship
IP Logged

Previous
Next
 From:  Michael Gibson
10939.7 In reply to 10939.6 
Hi Peer,

re:
> So would it be correct to say:
>
> At most 1 geometry undo unit is made per command.

Yes, it happens automatically as part of the stuff that's done after a command exits.


> A geometry undo unit is only made if a command causes one of the following to either be added to or deleted from the database:
> annotationObject
> geomObject

Yes, it's any object that was added or removed from the geometry database while the command was running. Annotations are a subclass of GeomObject.


> objectStyle

When an object style is added that makes a different kind of undo unit, a StyleList undo unit. These ones are created by script calls to

moi.geometryDatabase.styleEditorOpened();
moi.geometryDatabase.styleEditorClosed();

that bracket the showing of the edit style dialog.


> These methods are the only ones that trigger creation of a selection undo unit:
> deselectAll
> invertSelection
> selectAll
> selectLastCreated
> selectNamed
> selectLoop
> selectVisible

No this isn't a complete list. Doing selection using the scene browser or object properties dialog will also generate them and also things that do hide/show/lock/unlock as well like moi.geometryDatabase.hide(), isolate(), showSubset(), lock(), isolateLock(), unlockSubset().

- Michael
  Reply Reply More Options
Post Options
Reply as PM Reply as PM
Print Print
Mark as unread Mark as unread
Relationship Relationship
IP Logged

Previous
Next
 From:  pressure (PEER)
10939.8 In reply to 10939.7 
Hi Michael,

Thanks for giving a peek into the workings of undo.

- Peer
  Reply Reply More Options
Post Options
Reply as PM Reply as PM
Print Print
Mark as unread Mark as unread
Relationship Relationship
IP Logged

Previous
Next
 From:  pressure (PEER)
10939.9 In reply to 10939.7 
Hi Michael,

I've been digesting your last couple of messages while trying to figure out how to force a command that doesn't create/destroy geometry to get an undo unit.

I figured out that using moi.ui.propertiesPanel.setAnnotationProp() is a way to do this for my annotation example.

But, I'm still not sure how to get an undo unit when changing object visibility, lock state, or selection state. One hack for getting an undo unit associated with setting hidden=true is:
code:
    moi.geometryDatabase.deselectAll();
    seamEdgesList.setProperty('selected', true);
    moi.geometryDatabase.hide();

But, that doesn't seem quite right. I'm wondering about what gets run when I click on a lock or visibility icon or in the selection dot column in the browser. What happens when I click on the Visible.png icon in the browser?

What gets called when I do selection using the scene browser or object properties dialog?

- Peer
  Reply Reply More Options
Post Options
Reply as PM Reply as PM
Print Print
Mark as unread Mark as unread
Relationship Relationship
IP Logged

Previous
Next
 From:  Michael Gibson
10939.10 In reply to 10939.9 
Hi Peer,

re:
> But, I'm still not sure how to get an undo unit when changing object visibility, lock state, or selection state.

There isn't currently any facility exposed for a script command to generate this kind of undo.

Normally in MoI this type of object property manipulation is not done in a dedicated command because it can also be useful to be able to perform them while still running inside of a command.


> What happens when I click on the Visible.png icon in the browser?

The function SceneBrowserItem::OnStatusClick() is called, which among other things calls the internal C++ functions:

SaveSelectionUndo();

AlterStatus( ... );

SetSelectionUndoRevision();


> What gets called when I do selection using the scene browser or object properties dialog?

When you do selection using the scene browser, SceneBrowserItem::OnSelectionClick() is called which among other things calls:

SaveSelectionUndo();

Selects stuff...

SetSelectionUndoRevision();


When you do it selection by clicking on a label in the object properties dialog, detailedFilterClicked() is called which among other things calls:

SaveSelectionUndo();

... Manipulates selection ....

SetSelectionUndoRevision();


So what you need to do for having a selection undo is to call SaveSelectionUndo() first, then after all changes to hidden/locked/selected have been done SetSelectionUndoRevision() needs to be called to establish which geometry revision number the selection undo is tired to.

Selection undo doesn't actually generate a normal undo unit that goes in the undo stack, it's a special case in undo where before processing anything from the regular undo stack it checks if there is a stored selection undo available that matches the current database revision number and if there is restores that instead of performing the regular undo.

I'll set up script access to SaveSelectionUndo()/SetSelectionUndoRevision() off of geometryDatabase so it will be possible for you to use it too.

- Michael
  Reply Reply More Options
Post Options
Reply as PM Reply as PM
Print Print
Mark as unread Mark as unread
Relationship Relationship
IP Logged

Previous
Next
 From:  pressure (PEER)
10939.11 In reply to 10939.10 
Hi Michael,

Thanks for explaining the inner workings of selection undo and for setting up access. It's going to be nice to be able make some script commands undoable.

- Peer
  Reply Reply More Options
Post Options
Reply as PM Reply as PM
Print Print
Mark as unread Mark as unread
Relationship Relationship
IP Logged

Previous
Next
 From:  pressure (PEER)
10939.12 In reply to 10939.10 
Hi Michael,

I've had some trouble with undoing scripts that include an object picker. In one case the object picker seemed to interfere with creation of a selection undo unit. Now I've got a situation where I'm using an object picker to get an edit point so that I can move that edit point.

Here's a self-contained snippet. If you run it on a new blank file you'll see:



And then if you hit Undo you'll see an intermediate stage of what the script did:



No matter how many times you hit Undo you'll never get back to an empty environment.

Is there a way for me to clean this up so that it's neatly undoable?

code:
var index = 0;
var destinationPoint = moi.vectorMath.createPoint(1,1,1);

var rect_factory = moi.command.createFactory( 'rectangle' );
rect_factory.setInput( 0, moi.vectorMath.createFrame() );
rect_factory.setInput( 1, null );
rect_factory.setInput( 2, 5 );
rect_factory.setInput( 3, 5 );
rect_factory.setInput( 4, false );
var objList = rect_factory.calculate();
moi.geometryDatabase.addObjects(objList);

var obj = objList.item(0);

obj.showPoints = true; // must show points to make them editable
obj.setEditPointSelected(index, true);

/*
non-interactive object picker with allowEditPoints()
simply to get edit point into an objectList so that it can be manipulated
*/
var objectpicker = moi.ui.createObjectPicker();
objectpicker.allowEditPoints();
objectpicker.done();
var editPointInList = objectpicker.objects;

var basePt = obj.getEditPoint(index);

var factory = moi.command.createFactory('move');
factory.setInput(0, editPointInList);
factory.setInput(1, basePt)
factory.setInput(2, destinationPoint);

// remove the original object, gather up the newly moved object, and add to database
moi.geometryDatabase.removeObject(obj);
var movedObjInList = factory.calculate();
moi.geometryDatabase.addObjects(movedObjInList);

// re-hide the edit points
movedObjInList.item(0).showPoints = false;


- Peer

  Reply Reply More Options
Post Options
Reply as PM Reply as PM
Print Print
Mark as unread Mark as unread
Relationship Relationship
IP Logged

Previous
Next
 From:  Michael Gibson
10939.13 In reply to 10939.12 
Hi Peer, try this:

code:
var index = 0;
var destinationPoint = moi.vectorMath.createPoint(1,1,1);

var rect_factory = moi.command.createFactory( 'rectangle' );
rect_factory.setInput( 0, moi.vectorMath.createFrame() );
rect_factory.setInput( 1, null );
rect_factory.setInput( 2, 5 );
rect_factory.setInput( 3, 5 );
rect_factory.setInput( 4, false );
var objList = rect_factory.calculate();
//moi.geometryDatabase.addObjects(objList);

var obj = objList.item(0);

obj.showPoints = true; // must show points to make them editable
obj.setEditPointSelected(index, true);

/*
non-interactive object picker with allowEditPoints()
simply to get edit point into an objectList so that it can be manipulated
*/
/*
var objectpicker = moi.ui.createObjectPicker();
objectpicker.allowEditPoints();
objectpicker.done();
var editPointInList = objectpicker.objects;
*/

var basePt = obj.getEditPoint(index);

var factory = moi.command.createFactory('move');
factory.setInput(0, objList);
factory.setInput(1, basePt)
factory.setInput(2, destinationPoint);

// remove the original object, gather up the newly moved object, and add to database
moi.geometryDatabase.removeObject(obj);
var movedObjInList = factory.calculate();
moi.geometryDatabase.addObjects(movedObjInList);

// re-hide the edit points
movedObjInList.item(0).showPoints = false;


That removes moi.geometryDatabase.addObjects(objList); , and removes the objectpicker piece
  Reply Reply More Options
Post Options
Reply as PM Reply as PM
Print Print
Mark as unread Mark as unread
Relationship Relationship
IP Logged

Previous
Next
 From:  pressure (PEER)
10939.14 In reply to 10939.13 
Thanks Michael for the edited code!

What you posted works for me by itself, but when I try incorporating it into the larger thing I'm working on I still have the same kind of problem with undo.

I removed the other instances of moi.geometryDatabase.addObjects() and don't have any object pickers, but the problem persists.

Is there a general principal I can use to troubleshoot this?

- Peer
  Reply Reply More Options
Post Options
Reply as PM Reply as PM
Print Print
Mark as unread Mark as unread
Relationship Relationship
IP Logged

Previous
 From:  Michael Gibson
10939.15 In reply to 10939.12 
Hi Peer, it looks like there's a bug where undo can get confused in certain cases if you both add and remove the same object within the same command.

- Michael
  Reply Reply More Options
Post Options
Reply as PM Reply as PM
Print Print
Mark as unread Mark as unread
Relationship Relationship
IP Logged
 

Reply to All Reply to All