Tip for exploring definitions

Good morning,

Here is a simple method to find out if the definition with the name “Heather” is present in the selection:

    mod = Sketchup.active_model
    sel = mod.selection  
    sel.grep(Sketchup::ComponentInstance).map(&:definition).map(&:name).include?("Heather")

Instead of that :

	presence = 0
    mod = Sketchup.active_model
    sel = mod.selection  
    sel.grep(Sketchup::ComponentInstance) do |i| 
	  next unless i.definition.name.include?("Heather")
	  presence += 1
	end
	presence == 0 ? false : true

I would like to do the same thing with the value of a dynamic attribute:

    mod = Sketchup.active_model
    sel = mod.selection  
    sel.grep(Sketchup::ComponentInstance).map(&:definition).map(&:attribute_dictionaries["dynamic_attributes"])

I don’t know how to create an array of all values because (&:values) didn’t seem to work.

Do you have an idea how to do it?

Any tricks to avoid digging through an array with the “do” method are welcome.

THANKS

You must not use the #attribute_dictionaries method this way because it returns nil if an entity’s dictionaries collection is empty. When this is so (an empty dictionaries collection,) and nil is returned, an attempt to call #[] on nil will raise a NoMethodError exception for NilClass as this class has no #[] method.

This means that extension code must always test for a “truthy” result when calling #attribute_dictionaries method.

If any instance (or it’s definition) does not have any dictionaries, the result is nil and calling #[] on nil raises a NoMethodError for NilClass.

So, you must use #select instead of #map to ensure only definitions with dictionaries are selected.

    mod = Sketchup.active_model
    sel = mod.selection  
    definitions = sel.grep(Sketchup::ComponentInstance).map(&:definition)
    those_with_dicts = definitions.select(&:attribute_dictionaries)
    dynamic_definitions = those_with_dicts.select(&:attribute_dictionaries['dynamic_attributes')

Now find a dynamic definition with attribute named “Heather”:

    has_heather_key = dynamic_definitions.find { |dyna|
      dyna.attribute_dictionaries['dynamic_attributes'].keys.include?('Heather')
    }
    # has_heather_key is nil if not found

Now find a dynamic definition with any attribute value of “Heather”:

    has_heather_value = dynamic_definitions.find { |dyna|
      dyna.attribute_dictionaries['dynamic_attributes'].values.include?('Heather')
    }
    # has_heather_value is nil if not found
2 Likes

Thank you Dan I expected nothing less from you.

I don’t know if it’s a good thing but I want to reduce the number of intermediate variables as much as possible with the number of lines :

	def include_value_in_dynamic_attribut?( ents, value )
	  ents.grep(Sketchup::ComponentInstance).map(&:definition).select(&:attribute_dictionaries).select(&:attribute_dictionaries["dynamic_attributes"]).find{ |dyna| 
	  @val = dyna.attribute_dictionaries["dynamic_attributes"].values.include?("#{value}") }	
	  @val
	end
	include_value_in_dynamic_attribut?( Sketchup.active_model.selection, "Heather" )

Even though the lines are much longer it makes it easier for me to read.

Ps: How would you do now to check if a specific attribute key has a value sought?

You don’t need to have the extra @val line as the result of the previous line would also return the result.
And, what is returned is not the value but a Boolean true or false whether it exists.

Using a literal string argument with the insertion of an interpolated reference is frivolous.
Ie, the reference value should be a String object. If not the interpolation will call #to_s upon it.
So it would be simpler to just do:

collection.include?(value.to_s)

Well, you should try for readability and maintainability’s sake, to keep your code lines to 80 characters max.
In order for us to read what you posted, we must scroll horizontally, and that makes your code harder to understand.

Yes, reference assignment takes time, but if the method is not going to be within a loop running many times, then a few intermediate references won’t hurt much. We are talking milliseconds to assign a reference.

1 Like

I don’t think this works. Ie, &:attribute_dictionaries["dynamic_attributes"] is two methods.
Only the first is getting called.

1 Like

I probably have said this many times now, in this and the DC category.

For dynamic attributes, you always check the instance first (for components and nested dynamic groups) and if not a group, then you check the definition second.

DC_DICT ||= 'dynamic_attributes'

def has_dc_attribute?(inst, key, value)
  # Check the instance
  check = inst.get_attribute(DC_DICT, key)
  if !check.nil?
    # inst had attribute key
    check == value
  else
    # inst did not have attribute key
    definition = inst.definition
    # Dynamic groups always have dynamic attributes in instance dictionary:
    return false if definition.group?
    # Check the component's definition:
    check = definition.get_attribute(DC_DICT, key)
    !check.nil? && check == value
  end
end
1 Like

Inside the block we have access to the boolean but not outside!

As dynamic attributes added manually on components are automatically attached to instances and definitions, we can do this:

DC ||= "dynamic_attributes"

def include_value_in_dynamic_attribut?( i, value )
  i.select(&:attribute_dictionaries).find{ |d| 
    @boolean = d.attribute_dictionaries[DC].values.include?(value.to_s)
  }
end
@boolean = false
mod = Sketchup.active_model
sel = mod.selection
i = sel.grep(Sketchup::ComponentInstance) + sel.grep(Sketchup::Group)
include_value_in_dynamic_attribut?( i, "Heather" )
@boolean

Excellent I take note!

I understand but what matters most to me is to publish the code as it is written in my rb file.

Especially since adapting it to the constraints of the forum will change my logic which is to reduce useless variables as much as possible.

Precisely if, the code will be called in loops or it will be executed several times.
As I said, storing information in intermediate variables increases the number of lines unnecessarily and slows down the code.

I want to optimize my way of coding as much as possible.

Reminder of my goal:

Classify in a array all the values of the “heather” attribute using “(&:keys)” then check once if the value sought is found.

This will allow in a single search to obtain a boolean even if hundreds of groups or components are in the selection.

Note on your example:

If components or groups are in the selection, we get an array of Boolean.
If components and groups are in the selection, we get an array of boolean, instances and nil.

For this it is essential to give complete examples without omitting the beginning to check the operation of your code.

No! I’m not asking you to do my job but to publish executable examples with a simple “copy/paste” to help beginners who will go through this. :wink:

It is better to write “if check” instead of “if !check.nil?” which requires two conversions makes the code difficult to read.

Note on adding attributes:

When an attribute is added manually the component instances and definitions are attached by the dictionary, which makes the definition.get_attribute test unnecessary.

However if the attributes are added with a Ruby method, your reasoning is correct!

Conclusion :

This research is not really useful to me, I just want to see how an experienced coder can optimize their code.

Here is the solution to my problem:

  def include_key_value_in_dc?( array_inst, key, value )
    array_inst.select(&:attribute_dictionaries).find{ |d| 
      d.attribute_dictionaries["dynamic_attributes"].each_pair{ |k, v| 
	    @boolean = k == key.to_s.downcase && v == value.to_s
	  }
    }
  end
  @boolean = false
  mod = Sketchup.active_model
  sel = mod.selection
  array_inst = sel.grep(Sketchup::ComponentInstance) + sel.grep(Sketchup::Group)
  include_key_value_in_dc?( array_inst, "Reference", "2023" )
  @boolean

This method is only valid for dynamic attributes added manually.

It avoids going through all the instances unnecessarily, which optimizes the execution time as well as the writing of the method itself. :wink:

  • it does not using “(&:keys)
  • it does not avoiding “do |e| … end” which is the same as “{|e| …}”
  • will raise error if there is no attribute dictionaries or the AttributeDictionary string name not “dynamic_attributes”
  • contains an unnecessary String to String conversions

This will return myinstance, a first group or component instance which fulfil the condition , or nil if not found.

def d_instances(sel = Sketchup.active_model.selection)
  i = sel.grep(Sketchup::ComponentInstance).concat(sel.grep(Sketchup::Group))
  i.select(&:attribute_dictionaries)
end

def include_key_value_in_dc?(key, value)
  d_instances.find{|i|
    next unless i.attribute_dictionaries["dynamic_attributes"]
    i.attribute_dictionaries["dynamic_attributes"][key.downcase] == value
  }
end

myinstance = include_key_value_in_dc?( "Reference", "2023" )

you can convert it to Boolean e.g. like:
my_boolean = !myinstance.nil?

or you just use like “if myinstance ” as condition…

however, you also need to understand what is the difference between:
if check ” instead of “if !check.nil?