Undo multiple methods in a tool at once?

Hi Sketchup wise folk. I am a new extension creator wannabe (…yep)
I have a working tool that takes input from the user. The tool completes with a process method that skews geometry. I have start and commit statements in the process method which works well both inside the tool and and after the tool has completed. Before the process, as part of the set up, I get the user to create an edge and a plane using separate methods. I would like to undo these methods at the same time if and when the process gets undone. Is the only way to do that to reconfigure the procedure so that everything happens in one method?

You can capture references to the edge and face to erase them later, but I doubt that erasure based on undo of your process can be done. Because of the risk of breaking the internal geometry database and consequent risk of crashes and unfixable damage to the model, the API provides no handles or ids for transactions and no access to the undo stack. You can use an observer to detect when a transaction is undone, but so far as I can tell there is no way to tell what operation it was.

If someone knows a way, I’ll be happy to learn.

Thank you slbaumgartner.
Well I personally am OK with it the way it is… Do you know, however, if it would be acceptable practice for the user to have to undo three actions instead of just one if they don’t like the result?

No. You can create 3 operations and chain the 2nd to the 1st and the 3rd to the 2nd so that they are together one undo.

Make the 1st operation normal. Then use the 4th transparent argument as true for the 2nd and 3rd operation to chain them to the 1st operation.

1 Like

Thankyou DanRathbun. Yes this works much better.
I have noticed some things…

I have 13 methods. The first ones are to select entities and take text parameters. Then I have one to take a start point for an edge and another one to take an end point for an edge. After that I have three to create a plane in similar fashion and the last one processes all that to deform the selected geometry.
I tried including the start operation commands in all the methods ( with the commit only at the end of the last one). I found I could ctrl_z undo back to the beginning at any point while using the tool. However once out of the tool, with the undo label referencing the first start operation command (makes sense), I was unable to undo anything.
So I removed the start commands before the second edge point method where the edge is made. In this scenario, once out of the tool, undo works to reset the geometry and also undoes the edge and plane. Sweet… But… the next undo that comes up is the same one and does nothing it seems. After that any previous actions before using the tool come up as expected. Also, while using the tool, ctrl_z has no effect ( Esc works ), until the method where the first point of the edge is picked.
Anyway. I am happy with it. I hope it passes muster once everything else is perfect. Thanks again for the information. I was not going to dare using that 4th argument.

This coding pattern will not work because each time you call model.start_operation whilst another operation is left open, the SketchUp engine will close the open operation. The docs have a note:

  • Note: Operations in SketchUp are sequential and cannot be nested. If you start a new Ruby operation while another is still open, you will implicitly close the first one.

Do not think of operations as methods. Methods are for separating code into task-based manageable and maintainable chunks. An operation can span more than one method.

In a tool, the ESC key should be used to reset the tool to it’s initial state. Normally tool code keeps track of the state or step number as an internal instance variable. (There is a subgroup of Ruby tool authors who like a double ESC ESC to reset then reactive the Select tool or the previous tool [by popping their tool off the toolstack.] I.e., an ESC whilst at the initial state is taken to be a tool abort.)

You should know that the undo operation only undoes the creation of geometry. I.e., those things that get saved to the undo stack. It does not undo the saving of inputpoints or other data in variables that your tool’s previous tool states have collected. It is up to you as a coder to have a reset() method that reinitializes all the tool’s variables and call it when it is necessary.

The 4th argument is fine. It is the 3rd that is deprecated and strongly discouraged.

2 Likes

Thanks again Dan. I feel like I should be paying for your advice. A real human is still so much better than surfing the net. And may it ever be so… ( you are a real person right? )…
So I put the start operation statement in the activate method and the commit statement at the end of the very last process method after the reset tool is called. Everything works perfectly! I can cleanly undo at any point in the tool and after’.
I guess the chaining of operations using the 4th argument is not applicable to my situation …
A related question… possibly it should be a different topic… I want to add in the exception handling logic which seems to be coupled to the start - commit statements. When I add begin before the start operation it wants to find an end which I have in the last process method that hasn’t been called yet. Is there a way to wrap a whole tool in the start-commit-exception-handling procedure?

No.

At first blush this might seem adequate. But now you see in regard to exception handling, it is not.

Of course not. This is basic Ruby 101. The matching end for a begin must be in the same scope.

Tool coding is challenging because it is event driven and chopped up into multiple callback methods.

So, you will likely find the answer is to again rely upon a state variable. In the tool’s onLButtonUp callback, when the user first clicks a point the code compares @state == 0 and if true it starts the operation.
In a similar fashion when your tool completes the geometry (the @state has probably already been checked) the operation is committed.

With regard to exception handling, you have 2 choices. Handle them individually at the bottom of each method where they are likely to occur, or collect them at the bottom of the methods and pass the exception object to a common exception handler method within your tool class. Ex:

    def error_handler(error, methname, view)
      line = error.backtrace_locations.first.lineno
      puts "Nifty Tool Error: #{error.class} in #{methname}, at #{line}"
      puts error.inspect
      view.model.abort_operation
      # ... perhaps display a friendly message to the user here?
      reset()
    end

    def onButtonUp(flags, x, y, view)
      # tool code ...
    rescue => error
      error_handler(error, __method__, view)
    end

The difficult thing is what happens during an view manipulation interception tool. Ie, suspend() and resume(). The Orbit and Pan tools could themselves have their own operations. (I don’t see Undo Orbit or Undo Pan appearing for interruptions of say the Select tool.)

2 Likes

I don’t think the view manipulators (orbit, zoom, pan) are considered operations. They certainly don’t add or remove Entities from the model. So far as I can tell they don’t generate entries in the undo stack and can’t be undone.

1 Like

Thanks. I reworked it and hopefully it’s getting shipshape by now… I used your nifty error_handler but put the display message in my reset tool, conditional on @error_handler = true, so that it wouldn’t get overridden by the reset tool.
I now have the geometry creating methods and the process method wrapped in begin-start-commit-rescue-end code.
I also wrapped onLButtonDown, onUserText and onReturn, but with out using start and commit. This is because I didn’t want to add too many undo operations.

Such as:

def onUserText(text, view)
  begin
    puts "regex_is_number?(onUserText) " + regex_is_number?(text).to_s
    @resume = false
    if @oktext == true
      if regex_is_number?(text)
        task_selector(text.to_f, view)    
        view.invalidate   
      else 
        if @tasks == 3
          Sketchup.status_text ='Incorrect value entered - Enter skew curvature in range (1 - 5), 1 = no curvature.'  
        elsif @tasks == 4 
          Sketchup.status_text ='Incorrect value entered - Enter division_factor for selection - max 100'  
        elsif 
          @tasks == 5 
          Sketchup.status_text ='Incorrect value entered - Enter recurve value: 0 = no recurve, 1 = opposite to skew direction, 2 = skew direction.'  
        end
      end     
    end
  rescue => error
    error_handler(error, __method__, view)
  end 
end

Pursuant to… My tool in it’s present condition will require the user to make 4 undo’s in order to return to the start condition.
In Extension Requirements I read:

When your extension makes several low level draw calls, join them together as one entry to the undo stack using the start_operation and commit_operation methods. If the user activates it as a single high level action, let them also undo it in a single step.

… so is that a no no?

To my thinking what matters is whether the action is perceived by the user as a single thing, or several steps of which they might want to undo part to fix an issue and then redo the rest. But bloating the undo stack with more steps than really needed is not a good thing.

A method does not need a begin. The def statement is the opening of the method block.

And then (of course) you do not need a double end.

Ex:

def some_method( *args )
  puts "Within method: #{__method__}"
  puts args.inspect
rescue => error
  puts error.inspect
else
  puts "All is well."
ensure
  puts "Leaving method: #{__method__}"
end

It is undesirable and confusing for users.

I think you need to go back to the start of this topic and reread the responses. I gave the answer(s) above.

Thanks some more. You could be saving me days of head scratching.
Here is a condensed version of what I have now:

def edge(value, view)
Sketchup.active_model.start_operation(‘Skew’, true)
create_edge(points) # rescue is in create edge method
Sketchup.active_model.commit_operation
end

def plane(value, view)
Sketchup.active_model.start_operation(‘Skew’, true, false, true)
draw_plane(some_points) # rescue is in draw plane method
Sketchup.active_model.commit_operation
end

def do_process(value, view)
Sketchup.active_model.start_operation(‘Skew’, true, false, true)
process(some_arguements)
erase edge
erase plane
reset_tool(view)
Sketchup.active_model.commit_operation
rescue => error
error_handler(error, method, view)
end

It works

That is to say, everything undoes with one undo command.

Please read …