Need guidance on surface area script

Hello all,
Year 0 Newbie here. I’m trying to write a simple script that would output the surface area of each types of materials in each component in the model.
I’m not totally new to coding but I come from an excel-vba kind of background and am pretty lost in how Ruby API works. I’ve done about 3 day’s worth of research and while I can do simple things like create faces and copy things, I don’t know how to extract information from an existing model.
Not exactly asking for a full handout code here, but if I can get some sample code or a few relevant lines thrown my way that’s related to what I’m trying to do I’d be really grateful.

A general idea of my code is:
Define the all the entities in my model with

model = Sketchup.active_model
entities = model.entities

And then I’d need to loop through each entity (I’m assuming an entity is a unique component), assigning it to a key in a dictionary.

While I’m referring to each entity, I will start another for each loop through all the faces to create a nested dictionary of existing faces in each component. Then I want to sum up the total surface area for each material within the component. And write that as the value for each material.

Like a

for i in total_faces
material_dictionary = [:colour01] = sum_colour01 + facei_colour01
export = {:component_name => material_dictionary{}}

Then finally export this all to a csv.
In the end this should give me the material surface area breakdown for every component in the model. Fredo6’s extension have the two halves that I want (material break down, and outlines component paths) but I can’t exactly open it up to see the code.

So to break it down:
how does one loop through each individual component (preferrably through individual instances as well)?
how does one get a list of faces, and loop through them?
how does one export a dictionary to csv?
Any help is welcome, thank you!

Okay, the zeroth thing to learn is how to post code blocks in the forum using markdown:

Second I compiled a set Ruby Learning Resource list here in a pinned topic thread …

Just a few points, … this references the model’s entities collection. (Things that are created and managed in the core C++ like the model and it’s collections, do not have constructor methods exposed as we would never need to “create” them.)

And (2nd) this means only at the top level.

Some how the API doc overview for this class got changed as I think I remember it being more descriptive and informative (ie, that component definition’s for instances and groups also have an entities collection.)
Within the past week I logged a documentation issue about this in the official tracker.

So, if you only iterate this collection your snippet references, then you’ll only get materials that a user painted at the top level, and then only the “instance / group” wrapper material assignments.

Often users need to open groups and paint the faces directly because of UV mapping features. (Especially for wood grain textures. etc.)

So you may find yourself needing to check each instance of used definitions, and check which faces within it’s entities are directly painted, and which ones still have nil assigned (which mean to inherit the instance wrapper’s material assignment.)

Don’t assume. Most SketchUp collections are exposed to Ruby with Ruby’s core Enumerable library module mixed in. From this library comes the #grep method …

model = Sketchup.active_model
comps = model.entities.grep(Sketchup::ComponentInstance)
groups = model.entities.grep(Sketchup::Group)

This method is very fast because it does class identity comparison.

The attribute dictionaries are attached to Sketchup::Entity API objects in the model database.

This would modify the model’s objects as code is iterating it’s collections. This is not necessary, and the amounts can quickly become incorrect (ie, out of sync with the model when edited.)

If you are looking for temporary memory storage of a hierarchical nature then Ruby’s core Hash class is very similar to the SketchUp API’s dictionaries (which come from the C++ class of the same name.)
Hash class objects are more powerful than the SketchUp API dictionaries. For example the #to_json method added by the JSON library can create a data string in JSON format easily output to a text file object.

The Ruby Standard Library has the CSV library. You can use.

But if your text lines are going to be rather simple, there are other simple core class methods of Array and String classes that can easily stitch text data together.

Ie, … say you have an array line1 whose members are strings that you wish to join together separated by commas.

csv1 = line1.join(',')

Say you have a nested array lines of line arrays (that you wish to join with commas) and join together with newlines, into one CSV string (so you can write it to a file object.)

csv_text = lines.map {|line| line.join(',') }.join("\n")

This looks weird but it is shorthand for …

# Create a new array whose items are csv text from the nested data arrays:
joined_csv_lines = lines.map {|line| line.join(',') }
# Now join the outer array into one big string (newline at end of each line):
csv_text = joined_csv_lines.join("\n")
1 Like

Just an FYI, you might be pleased to know that on Windows, there is a Ruby standard library object named WIN32OLE that can be used to open Excel (or Word) files and take Ruby data and insert it directly into worksheet cells.

So first look at the examples on this page and see if you really want to mess around with CSV files.
(Regardless learning to create a CSV file is a nice exercise for coding and you should learn how to create simple plain text data files of various formats.)

https://ruby-doc.org/stdlib-2.5.5/libdoc/win32ole/rdoc/WIN32OLE.html

You cannot loop through instances per se because (group and component) instances have no entities. It is their definition’s that own the entities collection.

What this means is that as you iterate the model’s entities collection, and come upon a group or component, you’ll need to then iterate it’s definition's entities (first having stored the scaling factor of each level as you drill down.)
If an instance has been scaled you’ll need to apply a transformation when getting the face areas.
As you drill down you’ll need to multiply the transformations together. (Most of the time it’ll be identity * identity which is a scale of 1.0.)

For top level “loose” faces (outside group or component instances) …

model = Sketchup.active_model
entities = model.entities
faces = entities.grep(Sketchup::Face)
model_face_area = faces.sum(&:area)

I need to explain the last line. It uses a relatively new shorthand iterator syntax.
It is synonymous with …

model_face_area = faces.sum { |face| face.area }

It works for when the block will call a simple method upon each member passed in. But the method call cannot have arguments if using the shorhand syntax.

So, the #area method can also take a transformation argument. If the instance you are summing has itself been scaled (or is within another parent that has been scaled,) you’ll need to use the block form of Array#sum and pass in the transformation reference.

For group or component instances …

model = Sketchup.active_model
target_material = model.materials[3]
entities = model.entities
# Assume no "loose" faces at top level.
instset = entities.grep(Sketchup::ComponentInstance)
tm = Geom::Transformation.new
sum = 0
instset.each do |inst|
  t = tm * inst.transformation
  faces = inst.definition.entities.grep(Sketchup::Face)
  painted = faces.select {|face| face.material == target_material }
  sum += painted.sum {|face| face.area(t) }
end

This only show 1 level and treating components only. You more than likely do both groups and components …

tm = Geom::Transformation.new
sum = 0
entities.each do |ent|
  next unless ent.is_a?(Sketchup::Group) ||
  ent.is_a?(Sketchup::ComponentInstance)
  t = tm * ent.transformation
  ent.definition.entities.each do |e|
    next unless e.is_a?(Sketchup::Group) || e.is_a?(Sketchup::ComponentInstances)
    faces = e.definition.entities.grep(Sketchup::Face)
    painted = faces.select {|face| face.material == target_material }
    sum += painted.sum {|face| face.area(t) }
  end
end

Again only 1 level down.

I think you can see the need to define methods because this is going to be an exercise in recursive coding.


ADD: Also … SketchUp has a convention that when a nested face has it’s material set to nil it will display with the material (if set) to it’s parent wrapper (group or component instance.)

So when iterating, you also need to take this into account. Some components / groups are drawn with some of the faces directly painted with a material that is not meant to change often, (think black tires and chrome trimmings on a car,) and other faces set to nil so as to display with the material a user paints the whole instance with. (There is an example dynamic car component that does this in the samples that come with SketchUp.)

1 Like

This has numerous errors … the main one being it never uses the i iteration variable.

But Ruby has defined many nifty iteration methods that help to eliminate “fence post errors” that often happen when a coder has to keep track of the number of iterations for a collection in other languages such as C or JavaScript.

In Ruby you just do …

collection.each do |item|
  puts item.inspect
end

… or …

collection.each { |item| puts item.inspect }

Both are block forms.


If you absolutely need an index variable then Ruby also has …

collection.each_with_index do |item,i|
  puts "No. #{i} : #{item.name}" # <-- String interpolation
end

Many, many more nifty filtering, searching and mapping iterators in Ruby core classes and any class that mixes in the Enumerable library module.


:bulb:

1 Like

Thanks Dan.
Looks like it won’t be a simple solution. Hope you will be patient with me as I work through your comments to try and advance my understanding.
So for every instance created of a component, if I had scaled it for whatever reason, I can’t just drill into the component and get the area, I have to mathematically scale it first before I get the correct area?
What kind of commands are available if I want to figure out:

  1. how many levels deep is my current component
  2. how many levels deep is there in the model
  3. How to reference a component x layers deep?

Not exactly. You need to get a reference to the instance’s transformation (which has it’s scaling).

As you drill down each level you, multiple the current level’s transform by the cumulative transform.

Then you access the instance’s definition’s entities collection and get the faces that you want to add together (by some filtering, ie … perhaps by material applied, etc.,) and sum their areas applying the cumulative transform, as shown above.

For example, the model itself is always scaling == 1, ie the IDENTITY transform.
Inside that, let us say that there is a component instance that was uniformly scaled by 5x.
Inside this, let us say that there is a group instance that was uniformly scaled by 2x.
The resultant scale of the group instance, and therefore it’s definition’s entities, is 10x.

So as you drill into the component, you multiply the model’s IDENTITY transform (btw, this is a global constant defined by the API,) against the component instance’s transformation, and you use this as the transform argument to the Face#area() method as shown above, as you compile the sum of the component instance’s definition’s face areas.

Then as you iterate the component instance’s definition’s entities, you come upon a group instance. Now you get it’s transformation and multiply that by the cumulative transform you are using at the previous (parent) level resulting in a new transform that you’ll use when compiling the sum of the group’s definition’s face areas.

And so on …

You’d have to walk it’s definition’s entities collection to find out.
At the minimum 1 level at least for it’s primitives (edges, faces, etc.)

You could write a Ruby method just to return the maximum number of nesting levels.
That would be a good exercise in programming. (Walking a tree data structure.)
Later you can use it and it’s methods as a boilerplate to expand into an area compiler.

There as many as there are. There is no max. But there is always 1 level minimum which the model’s entities collection.

Be aware that a .skp file is both a model and a component file. When you import a .skp file, it is added as a component. So there could any number of models nesting within each other.

You’d need to know it’s instance path …

… which can be saved from session to session by a path of persistent IDs …

:bulb: