Ruby script to write component size to attributes only partially working

I’m standing at the bottom of the Ruby learning curve, it looks like a cliff face from here. I need some guidance to develop my script a bit further.

The script should work through all the components in a model. If a component definition begins “Plate” or “plate” then the script is to get the actual X, Y, Z lengths of the component and paste them into the SU_DefinitionSet" - Size field.

These components are timber framing timbers, so always long and thin. The component axes are aligned so that the “length” is blue axis, “thickness” red, “height” green.

My script generally works but there’s a problem. Sometimes a component might have dimensions applied within it which affects the component bounds but not the actual geometry; screen grab shows a component with large bounds due to dimension inside it.

Is there a way to ignore the dimensions inside a component?

require 'sketchup.rb'

def write_size_attribute(component)
  x, y, z = component.bounds.width, component.bounds.height, component.bounds.depth
  size = "#{x}x#{y}x#{z}"
  component.set_attribute("SU_DefinitionSet", "Size", size)
end

model = Sketchup.active_model
entities = model.entities

entities.each do |entity|
  if entity.typename == "ComponentInstance"
    component = entity.definition
    if component.name =~ /^plate/i
      write_size_attribute(component)
    end
  end
end

Is not as simple as you wrote. Components may have transformations and you have to apply those transformations to each dimension.
You can download an extension called Add Ifc Quantities to Selection which adds all dimnensions if you add to your component IFC clasification.

I hope this function helps you

#retrieves boundingbox dimensions
    def boundingBoxDimensions(instance)
      bbb = instance.definition.bounds
      tttt = instance.transformation
      x = ((((bbb.max.x-bbb.min.x) * tttt.xscale)/39.37).round(2)).to_f
      y = ((((bbb.max.y-bbb.min.y) * tttt.yscale)/39.37).round(2)).to_f
      z = ((((bbb.max.z-bbb.min.z) * tttt.zscale)/39.37).round(2)).to_f
      v = (x*y*z).round(2)
      return [x,y,z,v]
    end

you can test if an entity is a component with

if entity.is_a? Sketchup::ComponentInstance
3 Likes

Tell the person who drew the dimension inside the components not to do it. :wink:

You can extend a script to check the entity.definition entities, if there a dimension object, then you can check the edge lengths… but you need to deal with lots of other conditions too.

@rtches Thanks!

I rather feared this would be the case. Right now, the script works correctly, even if components have been rotated away from the model axes. However it makes sense to make it robust and incorporate your code snippet… but as I am very new to Ruby (and coding) I don’t see how to do it.

Well, that’s me! I have had a chat with myself :smile:

Following up on what @rtches said above,

The #typename returns String and comparison of strings in Ruby is relatively slow, and is seen especially in iterations.

It is much faster to #grep like kinds of entities and use it’s block form …

entities.grep(Sketchup::ComponentInstance) do |entity|
  component = entity.definition
  if component.name =~ /^plate/i
    write_size_attribute(component)
  end
end

But it is inefficient to iterate the model’s entities as you will likely be setting the size attribute of the same definition multiple times.

It’s probably better to iterate the model’s DefintionList collection …

model.definitions.each do |cdef|
  next unless cdef.name =~ /^plate/i
  write_size_attribute(cdef)
end

You can do the inverse of collecting like types, …
ie, getting all entities except dimensions using #grep_v

 geometry = entities.grep_v(Sketchup::Dimension)

But as @dezmo noted reusable component definitions should not really have dimensions or text callouts within their entities collection.

3 Likes

Thanks @DanRathbun. Very informative, I’ve learned much in just a few comments.

I’ll implement this.

Good to know, makes sense. I’ll improve my workflow.

Edit:

Implementing this is doubly advantageous for my use case because it catches all Plate components in all assemblies and sub assemblies, whereas my version did not. Thanks Dan!

2 Likes

Your welcome!

Another thing to consider is that the bounding box dimensional methods return Length values in inches.

If you’d rather have the attribute size text in model units, you can use Sketchup::format_length() to return text in model units.

def write_size_attribute(component)
  bounds = component.bounds
  x, y, z = bounds.width, bounds.height, bounds.depth
  w = Sketchup.format_length(x)
  h = Sketchup.format_length(y)
  d = Sketchup.format_length(z)
  size = "#{w} x #{h} x #{d}"
  component.set_attribute("SU_DefinitionSet", "Size", size)
end

There is also another argument for precision, but have not shown using it.

Interesting. So I achieved the same result by doing this:

def write_size_attribute(component)
  x = component.bounds.width.to_mm.round(0)
  y = component.bounds.height.to_mm.round(0)
  z = component.bounds.depth.to_mm.round(0)
  size = "#{x}x#{y}x#{z}"
  component.set_attribute("SU_DefinitionSet", "Size", size)
end

Is there a benefit (efficiency?) for doing it one way or the other?

It’s not obvious which way is more efficient without knowing the C code behind the Ruby. I think you’d need to profile the two to find out, though the first involves fewer method calls.

The first way will format the values the same way as set in the model units, whereas the second will always get whole mm regardless of how the units are set. Which is better depends on your goal. For example, it could be annoying to get cm to 5 places if you want whole mm, and vice versa.

2 Likes

Agreed.

If @lanerow will be the only consumer of the components or all consumers will be using metric units, and it is standard that such products are described in mm dimensions, then the @lanerow way is likely to serve better.

I could suggest that the units used be incorporated into the method name (ie, write_size_attribute_mm() or similar.

The only critique I have is it’s not desirable to repeatedly call the same methods (component.bounds) as each time will cause a new Geom::BoundingBox object to be instantiated.

The pattern I showed is more agile, but also might give unexpected results if used (for example) whilst the model units had been temporarily changed for some reason. Meaning, that (now that I think on it,) my example would link two separate features together which can violate the “keep it simple principle”.

I guess then that I would lean toward the @lanerow code pattern.

  • It could even be enhanced using an inputbox to allow entry of the name filter and a dropdown to set product size units.
1 Like