Apply material to all nested or non-nested instances that have the dynamic attribute "x" in the selection

Hello everyone!

I’m new to Ruby and my English is bad.
I have the following code:

model = Sketchup.active_model
definition=model.definitions
selection = model.selection
entities = model.active_entities
mat="Green"
valor="portas"
dcs=[]
    selection.each{|e| e.typename=="ComponentInstance"
			dcs << e if e.get_attribute("dynamic_attributes","materialoptions")== valor
	}
dcs.each{|m|m.material=mat}

It applies a material in the selection that has a dynamic attribute “materialoptions” and “portas”, however I want to apply the material to all nested instances or not that have the attribute and that are within the selection.
How do I get it.
Thanks in advance for your help.

Teste.skp (517.4 KB)

Ruby uses 2 space indents.

Do not use entity.typename == "some string"
It is very slow and deprecated for most use.
Use entities.grep(SomeClass) instead. It is a very fast filter.
Or for a single object, use entity.is_a?(CertainClass)


Component Instances do not “own” entities. Only their definition has an entities collection.
If the instance is not unique, then changing the nested instances will change them in ALL of the instances of that definition.

Try not to create a new string literal object in every loop. Define them before and outside the looping construct.

model = Sketchup.active_model
selection = model.selection

mat   = "Green"
valor = "portas" # "doors"
dict  = "dynamic_attributes"
key   = "materialoptions"

dcs = selection.grep(Sketchup::ComponentInstance).find_all { |dc|
  dc.get_attribute(dict,key) == valor
}

defs = dcs.map { |dc| dc.definition }.uniq!

defs.each { |cdef|
  cdef.entities.grep(Sketchup::ComponentInstance).each { |inst|
    inst.material = mat 
  }
}

Many of the SketchUp API collection classes have the Ruby core Enumerable module mixed into them.

:nerd_face:

2 Likes

Hello!
Sorry for the delay in answering DanRathbun.
I was out and only now can get on my pc.
Thanks for answering and for the tips, they will be very useful for me to learn Ruby.
Your code is very elegant, but it returns an error and I can not understand why.
Follows an image.
Erro Código

The method each is an iterator method for collection type objects (arrays, hashes, sets, … etc., …)

In Ruby when a method is called upon an object that does not have such a method, then a NoMethodError exception is raised. The error message tells you that the each method was called upon the singleton object nil (it’s a singleton because it is the one and only instance of the NilClass.)

Ruby has a tradition of returning nil from many method calls that fail, especially search methods.
So it is normal to check after such method calls that you have a valid object (or array of returned objects.)

There is only two places where each is called. So you can test by outputting the returns to the console …

dcs = selection.grep(Sketchup::ComponentInstance).find_all { |dc|
  dc.get_attribute(dict,key) == valor
}
puts dcs.inspect   # <---<<< corrected by Steve below ;)

… see what you get ?

If all this code was within a method definition, it is normal to add a “bailout” statement …
… and we can also check the DC definition because if the DC instance is using the default valor then the instance’s dictionary will not have the "materialoptions" attribute …

module Tenquin
  extend self

  def change_mat( key, valor, mat )

    model = Sketchup.active_model
    selection = model.selection
    return "Empty Selection!" if selection.empty?

    dict  = "dynamic_attributes"

    dcs = selection.grep(Sketchup::ComponentInstance).find_all { |dc|
      dc.get_attribute(dict,key) == valor ||
      dc.definition.get_attribute(dict,key) == valor
    }
    return "No Dynamic Component Instances found !" if dcs.empty?
    puts " Instances found : #{dcs.count}"

    defs = dcs.map { |dc| dc.definition }.uniq!
    return "Dynamic Component search resulted in nil." if defs.nil?
    return "No Dynamic Component Definitions found !" if defs.empty?
    puts " Definitions found : #{defs.count}"

    defs.each { |cdef|
      change = cdef.entities.grep(Sketchup::ComponentInstance)
      next if change.nil? || change.empty?
      change.each { |inst| inst.material = mat }
    }

  end # change_mat

end # module Tenquin

Note that if a variable has value nil, puts will output a blank line, which can be confusing! To see the nil value you need to do

puts dcs.inspect
1 Like

I need some time to better understand how things work in ruby.
My knowledge boils down to editing a simple code, pasting in console, and seeing what happens to the model.
:joy:

Again I point you to the Learning Resources wiki lists I created …


I made a mistake in the above code sample:

    return "Dynamic Component search resulted in nil." if dcs.nil?
    return "No Dynamic Component Definitions found !" if dcs.empty?

should have used defs in the conditional expression, instead of dcs, and read as …

    return "Dynamic Component search resulted in nil." if defs.nil?
    return "No Dynamic Component Definitions found !" if defs.empty?

I have made the correction in the code above.


Here is a slightly expanded edition … it outputs the number of changed instances and returns an array of those changed instances or nil if none were changed.

module Tenquin
  extend self

  def change_mat( key, valor, mat )

    model = Sketchup.active_model
    selection = model.selection
    return "Empty Selection!" if selection.empty?

    dict  = "dynamic_attributes"

    dcs = selection.grep(Sketchup::ComponentInstance).find_all { |dc|
      dc.get_attribute(dict,key) == valor ||
      dc.definition.get_attribute(dict,key) == valor
    }
    return "No Dynamic Component Instances found !" if dcs.empty?
    puts "Instances found : #{dcs.count}"

    defs = dcs.map { |dc| dc.definition }.uniq!
    return "DC Definitions search resulted in nil." if defs.nil?
    return "No Dynamic Component Definitions found !" if defs.empty?
    puts "Definitions found : #{defs.count}"

    changed = []
    num = 0

    defs.each { |cdef|
      change = cdef.entities.grep(Sketchup::ComponentInstance)
      next if change.nil? || change.empty?
      change.each { |inst| inst.material = mat }
      num += change.size
      changed.push(*change)
    }
    
    puts "Total DC Instances material changed : #{num}"
    if num > 0
      puts "Returning an array of references to each changed instance."
      return changed
    else
      return nil
    end

  end # change_mat

end # module Tenquin

I checked your code and saw that it returns the number of instances if the component has not nested … If it is nested it returns as below.

‘’'ruby

mat   = "Green"
valor = "portas" # "doors"
dict  = "dynamic_attributes"
key   = "materialoptions"
Tenquin.change_mat( key, valor, mat )
No Dynamic Component Instances found !

‘’

I can not get where I want to: Apply material in the instances it contains…
‘’'ruby

value = "portas"
dict = "dynamic_attributes"
key = "materialoptions"

‘’

… at any nesting level …

I did a re-reading of the first code I posted, but only applies material to instances that are at a nesting level, I’d like to apply at any level and without nesting.
Maybe you’re leading me the right way, but I still can not adapt your code to do what I want.
NOTE: I do not know if I’m putting the codes in the correct way, but I followed what you said …

‘’'ruby

## 1 nível de aninhamento
model = Sketchup.active_model
definition=model.definitions
selection = model.selection
entities = model.active_entities
mat="Green"
valor="portas"
dict  = "dynamic_attributes"
key   = "materialoptions"
dcs = []
selection.grep(Sketchup::ComponentInstance).each{|s|
s.definition.entities.grep(Sketchup::ComponentInstance).each{|e|
dcs << e if e.definition.get_attribute(dict,key)== valor
}}
dcs.each{|m|m.material=mat}

That needs to be three backticks before ruby, not single quotes. Backtick is at the upper left of the keyboard (at least on a US keyboard) along with tilde ‘~’.

It was really an example of how to write a module, and how to inspect collections at certain times in the code.

Build on it and learn.

Well this is complicated. You are dealing with Dynamic Components which are complex. If they have their own material attributes they may be overriding what you are trying to do.

Perhaps explain in more basic terms what and why you want to do this ?

    ## 1 nível de aninhamento
    model = Sketchup.active_model
    definition=model.definitions
    selection = model.selection
    entities = model.active_entities
    mat="Green"
    valor="portas"

I think I’m learning!
It is?

I work with woodworking and would like to develop a tool that applies material to all instances of nested components or not, which contains the attributes and keys previously mentioned.
It would be a kind of filter, in all selected cabinets that have pieces with dynamic attributes “materialoptions” and keys “doors” apply the green material, for example.
At first I’m trying to make the code, but then I want to put a collection of materials in a webdialog that when triggered executes the code applying the material as described.

Hello Tenquin,

To apply a material to all existing instances in a selection, you can follow this example:

def add_mat(ents)
  ents.grep(Sketchup::ComponentInstance).each do |e|
    e.material = "Green" 
    add_mat(e.definition.entities)
  end
end  
      
mod = Sketchup.active_model
sel = mod.selection
add_mat(sel)

To add a dynamic material attribute to all nested components, you can follow this example:

def add_attribut_mat(ents)
  ents.grep(Sketchup::ComponentInstance).each do |e|
    e.set_attribute 'dynamic_attributes','material','0'
    e.set_attribute 'dynamic_attributes','_material_formula','"Green"'
    $dc_observers.get_latest_class.redraw_with_undo(e)  
    add_attribut_mat(e.definition.entities)
  end
end  
      
mod = Sketchup.active_model
sel = mod.selection
add_attribut_mat(sel)

Edit

Corrected example!

To test the codes, you must select your components and then copy and paste into the Ruby console.

cordially

David

This is silly code. You have already filtered just component instances from the ents twice !
You need not do it three times. Once is enough.

And you did the same thing in the example add_attribut_mat().


You should not create literal string objects in each loop of an iterative block.
Define them first outside the block and then each loop will use the same object …

def add_attribut_mat(ents)
  dict = 'dynamic_attributes'
  attb = 'material'
  form = '_material_formula'
  matl = '"Green"'
  indx = '0'
  ents.grep(Sketchup::ComponentInstance).each do |e|
    e.set_attribute( dict, attb, indx )
    e.set_attribute( dict, form, matl )
    $dc_observers.get_latest_class.redraw_with_undo(e) 
    add_attribut_mat(e.definition.entities)
  end
end

Better 3 times than none. :grinning:
I corrected so that “tenquin” can begin to advance in this research.

Do you have a specific reason for this?
Storing everything in variables seems more complicated!

Hi David!
Thank you for taking the time to answer my question.

Thank you DanRathbun.
I’m going to study David’s codes and try to tailor them by following his concept
:sorrir:

I told you …

When you create a literal String, the Ruby interpreter calls String::new("Your literal string") each pass through the loop.

Ruby does not actually have real variables like Pascal or BASIC.
Ruby is a 100% object oriented programming language.
Ruby has 2 things,… objects and references that point at objects.

So we do not “store” strings “in” variables.
In Ruby the interpretive = is the assignment operator.
It assigns a reference name to point at an object.

So, running the code …

attb = 'material'

… creates a new String class instance object "material"
… and then, assigns the reference name attb to point to it.

Anyway, … creating the same objects over and over again,
in each pass through your loops will slow down your code.


Oh, and …

dict = 'dynamic_attributes'

… should be a constant at the top of your module as you’ll likely be using it many places …

DCDICT ||= 'dynamic_attributes'

It’s not a pretty code, but it’s close to what I want.
Apply the material at up to three nesting levels, but one at a time.
I’m in the right way? How would you do it?

#Meu código
def add_attribut_mat(ents)
  dict = "dynamic_attributes"
  key = 'materialoptions'
  valor = 'portas'
  attb = 'material'
  form = '_material_formula'
  matl = '"Green"'
  indx = '0'
  model = Sketchup.active_model
  selection = model.selection
  return "Empty Selection!" if selection.empty?
  
  ents = []  
  selection.grep(Sketchup::ComponentInstance).each{|a|
     ents << a if a.definition.get_attribute(dict,key)== valor
    }
   
   selection.grep(Sketchup::ComponentInstance).each{|b|
     b.definition.entities.grep(Sketchup::ComponentInstance).each{|c|
     ents << c if c.definition.get_attribute(dict,key)== valor
    }}
	
	selection.grep(Sketchup::ComponentInstance).each{|d|
     d.definition.entities.grep(Sketchup::ComponentInstance).each{|e|
     e.definition.entities.grep(Sketchup::ComponentInstance).each{|f|
     ents << f if f.definition.get_attribute(dict,key)== valor
    }}}
	
  ents.each do |e|
    e.set_attribute( dict, attb, indx )
    e.set_attribute( dict, form, matl )
    $dc_observers.get_latest_class.redraw_with_undo(e)
    add_attribut_mat(e.definition.entities)
  end
end

mod = Sketchup.active_model
sel = mod.selection
add_attribut_mat(sel)
# Código melhor

module Tequin

  extend self

  DCDICT ||= 'dynamic_attributes'
  
  @@loaded ||= false

  def get_dc_instances(ents)
    ents.grep(Sketchup::ComponentInstance).find_all {|ci|
      ci.definition.attribute_dictionaries[DCDICT] rescue false
    }
  end

  def dc_def_matlopts?(inst,key,valor)
    inst.definition.get_attribute(DCDICT,key) == valor
  end

  def add_attribut_mat(
    ents,
    valor = 'portas',
    matl  = 'Green'
  )
    key   = 'materialoptions'
    attb  = 'material'
    form  = '_material_formula'
    indx  = '0'
    model = Sketchup.active_model
    selection = model.selection
    return "Empty Selection!" if selection.empty?

    ents = []

    get_dc_instances(selection).each {|primario|
      if dc_def_matlopts?(primario,key,valor)
        ents << primario
        get_dc_instances(primario).each {|secundario|
          if dc_def_matlopts?(secundario,key,valor)
            ents << secundario
            get_dc_instances(secundario).each {|terciario|
              if dc_def_matlopts?(terciario,key,valor)
                ents << terciario
              end # terciario
            }
          end # secundario
        }
      end # primario
    }

    ents.each do |e|
      e.set_attribute( DCDICT, attb, indx )
      e.set_attribute( DCDICT, form, matl.inspect )
      $dc_observers.get_latest_class.redraw_with_undo(e)
      add_attribut_mat(e.definition.entities)
    end
  end

  if !@@loaded # Execute uma vez na inicialização
  
    UI.add_context_menu_handler {|popup|

      mod = Sketchup.active_model
      sel = mod.selection
      if !sel.empty?
        popup.add_item("Pinte todo o Aqua") {
          add_attribut_mat(sel,'portas','Aqua')
        }
        popup.add_item("Escolha a cor ...") {
          matl = UI.inputbox(
            ["Cor"],
            ["Aqua"],
            [Sketchup::Color::names[0..24].join('|')],
            "Escolha a cor ..."
          )
          add_attribut_mat(sel,'portas',matl) if matl
        }
      end

    }
  
    @@loaded = true
  end

end
1 Like