Hide selected layers only hides first layer then stops


#1

Hi, I have just started creating a plugin to hide the selected layers or hide the non-selected layers from the context menu. My workflow is to draw everything in Layer 0 and put components onto the various layers to control what parts of the model are visible etc.
If I just iterate through a collection of Entities, the selected items are hidden, but the layers are not so other items on that layer are still visible… If I iterate through the layers only the first layer is hidden, similarly if I hide the other layers, every layer is hidden
Any thoughts on this, it is my first attempt at Ruby

mod = Sketchup.active_model # Open model
ents = mod.entities # All entities in model
sel = mod.selection # Current selection

def get_layers(sel_objects)
sel_layers = Array.new
sel_objects.each { |entity| 
  puts "Adding layer " + entity.layer.name + " to array"
  sel_layers << entity.layer
  }
  return sel_layers
end

def hide_layers(lays)
lays.each { |layer| 
  puts "Hiding layer " + layer.name.to_s
  layer.visible = false
  }
end

def show_layers(lays)
  lays.each { |layer| 
    puts "Showing layer " + layer.name.to_s
    layer.visible = true
  }
end

def hide_other_layers(lays)
  all_ents = Sketchup.active_model.entities
  hide_layers(all_ents)
  show_layers(lays)
end

all_lays = get_layers(ents)
sel_lays = get_layers(sel)

UI.add_context_menu_handler do |menu|
   # Add an item to the context menu
   menu.add_separator
  item1 = menu.add_item("Hide layers") { hide_layers(sel_lays) }
  item2 = menu.add_item("Hide other layers") { hide_other_layers(sel_lays) }
  item3 = menu.add_item("Show all layers") { show_layers(all_lays) }
end

Thanks Malcolm


#2

Do you see errors on the Ruby Console? If not, you should definitely want to see errors in case when errors happen.

All procs attached to the UI don’t automatically bubble up errors (because their root is not script loading or manually execution through console, but events in SketchUp’s UI). That means code in UI::Command.new, add_item(){}, add_action_callback is better wrapped in

begin
  to_fail_or_not_to_fail
rescue Exception => error
  # Or use your own logging mechanism here. 
  # Actually this should go to $stderr, but SketchUp doesn't implement $stderr.puts
  puts error
  puts error.backtrace
end

When your script is loaded, do the entities and layers all exist? You store them in a local variable at top level (!). The purpose of local variables is to be used only locally, temporarily, and grabage collected (forgotten) as soon as not needed anymore. When you click the menu item, your local variable may not exist anymore, or it holds the array of layers at the point of time when the script was loaded, not the current state of the model.

Also you don’t need to literally instantiate Array, you can just use []. Alternative for that method:

def get_layers(selected_entities)
  return selected_entities.map{ |entity| entity.layer }.uniq
end

Edit: added uniq following John’s tip.


#3

Thanks Aerilius for your reply,
I have been using Ruby for about 2 days now, I am used to Java and knew the fields should be not local variables. I get no error in console. I will look more into Ruby and those methods you mention. The local variable holding the layers is probably GCed the first time layer.hide is called. A debugger would be handy to see what is going on, at the moment I am using Ruby Code Editor and the console, just hacking away trying to get the hang of it.
Thanks again
Malcolm


#4

A few more words to expand on what @Aerilius wrote: The basic flaw with your code is that all of the key variables are set when the code is loaded by Ruby. mod, ents, sel, all_lays, and sel_lays all have the values that were in place during the load. But the context menu callbacks occur later, when these values may no longer be valid. These variables have been defined at global scope, so they should persist, but even if they have not themselves been GC’d, they may point to entities that have been GC’d or that have been deleted or marked invalid by SketchUp. The selection, in particular, is not likely to be what you want - odds are it was empty when your code was loaded!

So, to fix it you need to read current values at the time the callbacks occur. This will require separate methods for handling the layers for all entities and for the current selection. For example,

def get_ents_layers()
  ents = SketchUp.active_model.entities
  get_layers(ents)
end

then

item 3=menu.add_item("Show all layers") {show_layers(get_ents_layers())}

For future reference, you should always wrap your own code in a module so that its namespace can’t collide with anything else in the global Ruby namespace. In your example, there is a danger that someone else may have defined things named mod, ents, sel, etc. and you will collide!


[Template] Ruby Plugin main file
#5

I would also look at returning each layer only once, i.e

return sel_layers.unique!

or not push a copy…

sel_layers << entity.layer unless sel_layers.include? entity.layer

my two pence…
john


#6

As a footnote, I encourage you to learn plugin programming by doing this exercise, but you should be aware that there are multiple existing plugins that already do what you are setting out to do!


#7

Seconded! And also since it’s likely you will write more than one plugin, each plugin should be sub-wrapped in it’s own sub module of your toplevel module.

require 'sketchup.rb'
module Obeirn
  module ThisPlugin

    # CONSTANTS defined here

    # Module @@variables defined here

    # Classes unique to ThisPlugin defined here

    class << self

      # define instance @variables here

      # def plugin methods here

    end # proxy class

    # Run Once at startup block here:
    this_file = Module::nesting[0].name <<':'<< File.basename(__FILE__)
    unless file_loaded?( this_file )

      # define commands, menus and menu items here

      file_loaded( this_file )
    end

  end # plugin module
end # module Obeirn

#8

These are only the toplevel model entities. (Each group and component definition has an entities collection also.)

Beware you cannot hide the current layer, so this may be causing your loop to exit prematurely. (In the UI, if the user attempts to hide the active layer, a popup warning message box appears. But this does not happen when Ruby code tries this.)

layer.visible = false unless layer == layer.model.active_layer


#9

Good catch, Dan! There was no filtering to eliminate layer0, so the code is certain to hit this snag even if layer0 is left active as usually recommended!


#10

I actually meant to reply to the original poster here (above):
http://forums.sketchup.com/t/hide-selected-layers-only-hides-first-layer-then-stops/10771/7


#11

Thanks John, I was thinking since along those line, many elements are on the same layer and that may be causing problems when iterating. @DanRathbun The code was just hacking so far and not for releasing in the wild.Do you just declare the module or is it a sub-folder of plugins? The template you show above is very handy Thanks
Malcolm


#12

I normally work in Layer 0 all the time an just put things into layers to organise them, but it is a good point. If I publish the plugin, somebody else may not work that way


#13

You should look at the SketchupExtension class for an example of packaging an extension into a loader and it’s folder of code and support files.

But generally in Ruby, the keywords class and module mean “open this object for editing, creating a new one if needed.

You can open a class or module instance, and change it any number of times, both during SketchUp startup, and any time during runtime. (This is why Ruby is in the dynamic family of languages.) You can add new methods, remove old methods, change variables, etc.etc.

Therefore, a class or module definition, or later re-definition(s), can span as many files as you wish.

ADD: You do not need to “close” a class or module (with end,) before loading another file that also helps define the current class or module. (This is because load and require evaluate all files at the toplevel binding, which is Object. This is also why it is a no-no to define methods and variables at the toplevel. Every class in Ruby is descended from Object, and so will inherit whatever is defined at the toplevel.)

It allows for nice organization of large projects.

Advice… get in the habit of putting your methods in alphabetical order in their files and classes/modules. Makes it much easier to find them later.

P.S.: The plugin module name ThisPlugin, in the example is meant to be changed to your actual plugin sub-module identifier, and same for your toplevel namespace module. Use what you wish.


#14

I have done a bit more work on it and the hide layers command works now. Would appreciate any comments, style tips etc

require "sketchup.rb"
#require "langhandler.rb"
module Obeirn
  module LayerTools

  #Check if file already loaded
  this_file = "menu_plugin.rb"
    unless file_loaded?( this_file )

    	hide_lay_cmd = UI::Command.new("Hide layers") {
    	#Get all layers of selected items
    	lays =[]
    	sel = Sketchup.active_model.selection
    	sel.each { |elem| 
    		elem_lay = elem.layer
    		lays << elem_lay
    		}
        #Hide each layer
    	lays.each { |lay|
    		lay.visible= false}

    }
    #Access Sketchup's Context menu
      UI.add_context_menu_handler do |menu|
      	menu.add_separator
      	item = menu.add_item hide_lay_cmd
      	
      end
    #Mark plugin as loaded
    file_loaded this_file
    end
  end
end

Thanks
Malcolm


#15

Two minor nits:
As @DanRathbun illustrated, you should use __FILE__ instead of hard-wiring the name of the file. That way you will be protected if anyone ever renames the file. He also showed some more subtle code to track the name with nesting in modules.

As @john_drivenupthewall noted, you might invoke lays.unique! to eliminate duplicates before the iteration to set visibility false. Whether this actually saves any runtime will depend on how many entities there are in your model, but it is good practice.


#16

Thanks @slbaumgartner,
so _FILE refers to the current file?

I had pasted in his code to track the name but it didn’t work, I’ll look at it in more detail.
I am slowly getting there…
Regards

Malcolm


#17

Another optimization you might consider is to use a hash instead of an array to gather the list of layers in the selection. Use the layer as the key and true as the value. Then iterate the keys of the hash when doing the visibility assignment. That way the final size of the layers hash is exactly the number of layers found, whereas in your example the size of the lays array is equal to the number of entities in the selection. And like uniq! (which isn’t needed in this case), this will not make a detectable difference in performance unless the selection contains a large number of entities compared to the number of layers they use, it’s just a hedge against the future.


#18

Yes, __FILE__ (“double-underscore+FILE+double-underscore”) is automatically set by Ruby to the name of the file currently being loaded.


#19

That makes sense, in Java I would never use arrays for this sort of thing, too slow.