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

I liked the context menu.
I’ll take some time to study your code.
On the other hand it returns an error when the selection is not nested.
And when it is nested it does not return nor execute anything.

I also have a few questions:

1 - “rescue” followed by “false” what does it mean? I read a bit about exceptions, but followed by false how does it work?
2 - dict can I replace by DCDICT? In the code

3 -

The “dc_def_matlopts?” checks if the definition contains key and value, if yes push for “ents”, if not go ahead??

Thank you :smile:

(Okay, remeber you did ask.)

This is called using the keyword rescue “in modifier position”.

When the preceding statement causes an exception the “rescue modifier” traps it and returns the expression following the rescue keyword.

In this case we are catering to the Enumerable#find_all() method, which cares only that the result of evaluating the block is true or false. If true then the iterator object referenced by ci will be included in the “found” collection.

But the SketchUp API’s Entity#attribute_dictionaries method will return nil when an entity has no attribute dictionary objects, instead of returning an empty collection. (It is a bit weird.)

SO, … just in case an entity has an empty Sketchup::AttributeDictionaries collection, and calling the Entity#attribute_dictionaries method returns nil, and then the statement (in the get_dc_instances method) calls the [] method upon nil, which is the one and only singleton instance of the NilClass class, which does not have such a method,… and then a NoMethodError exception would be raised with a "undefined method '[]' for nil:NilClass" error message, …

… so having the rescue false modifier will in the nil case, trap this NoMethodError and return a false result for that block iteration, which lets the Enumerable#find_all() method know NOT to include that “loop” item in the found results.

:nerd_face:

Yes, I forgot to replace those 2 occurrences of dict. (I’ve edited the above code.)

I simply took your boolean test expression from 3 places …

if ref.definition.get_attribute(dict,key) == valor

… and wrapped it up inside 1 boolean method …

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

“Boolean” means logical true or false.
Ruby boolean methods return true or false and traditionally have their names end with a “?” character.

Oh I did not see the test model in the first post. So I had not tested it on your model.

downloading … firing up a new SketchUp instance … loading model … testing …

Ah … I see we have errors …

Error: #<NoMethodError: undefined method `grep' for #<Sketchup::ComponentInstance:0x0000000bfc5868>>
<main>:11:in `get_dc_instances'
<main>:38:in `block in add_attribut_mat'
<main>:35:in `each'
<main>:35:in `add_attribut_mat'
<main>:67:in `block (2 levels) in <module:Tequin>'
SketchUp:1:in `call'

Okay, fixed that error. It was my booboo. In the second level (and third) it should call …

get_dc_instances(primario.definition.entities).each

… instead of …

get_dc_instances(primario).each

… _etc., …

Also I forgot to pass the first member of the array returned by UI.inputbox(), rather than the whole array.
Fixed that and now color choosing works.


Then I got lockup for too long and this error …

Error: #<SystemStackError: stack level too deep>
.../sketchup/plugins/su_dynamiccomponents/ruby/dcclass_v1.rbe:3322

… which was an unneeded recursive call in the add_attribut_mat() method in the last loop after the dc redraws. I commented out the recursive method call, so you could take note of it.


I also wrapped everything up in one named undo operation.
So afterward the user sees on the “Edit” menu a "Pinte todo o #{color name}" as an undo.


latest version:

# 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.definition.entities).each {|secundario|
          if dc_def_matlopts?(secundario,key,valor)
            ents << secundario
            get_dc_instances(secundario.definition.entities).each {|terciario|
              if dc_def_matlopts?(terciario,key,valor)
                ents << terciario
              end # terciario
            }
          end # secundario
        }
      end # primario
    }

    ents.uniq!

    model.start_operation("Pinte todo o "<<matl,true,true)
      #
      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
      #
    model.commit_operation

  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 ..."
          )
          if matl
            add_attribut_mat(sel,'portas',matl[0])
          end
        }
      end

    }
  
    @@loaded = true
  end

end
1 Like

You can also try this:

def add_attribut_mat(ents)
  ents.grep(Sketchup::ComponentInstance).each do |e|
    attribut = e.get_attribute 'dynamic_attributes','materialoptions'
      unless attribut.nil?
        if attribut.include?("portas")
          e.set_attribute 'dynamic_attributes','_material_formula','"Green"'
          $dc_observers.get_latest_class.redraw_with_undo(e)
        end
      end
	add_attribut_mat(e.definition.entities)
  end
end  
     	
mod = Sketchup.active_model
sel = mod.selection
add_attribut_mat(sel)
1 Like

You are certainly right but each custom attribute will ask for one more line with your method!

If you have hundreds of attributes it will be very confusing!

As I do not see any slowdown in my codes, I continue with my method. :wink:

That’s exactly what I was looking for!
It seems that ruby has several ways of doing the same thing and I confess that this leaves me a bit confused, because I do not know what concept to follow.
I have a lot to study!
Thank you
:smile:

I did not know do…

…You did it the way I wanted do.
But I see that in order to reach “tertiary” it is necessary that “secondary” and “primary” have the dynamic atruibutos and not necessarily they would have to have.
I want to apply the material in the instance that has the attributes “materialoptions” and “portas” if the parents have attributes apply to them, if not, just apply to whoever has and skip who does not.
the code of @dynamiqueagencement does exactly what I want.
But yours has more content to study
thank you
:slight_smile:

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.