Traversing the SketchUp Model

This article explains how to iterate over all the geometric content of a SketchUp model, for instance for constructing an exporter.

 

This article focuses on the pure geometry content and skips other entities such as Scenes (saved views) or nuances like how the same component definition can be reused across the model hierarchy.

 

tl;dr Scroll to the last code examples if you just want to copy and paste something.

Flat Model

 

If a SketchUp has a flat hierarchy, i.e. contains only basic geometry, it is trivial to iterate over its content.

 

```

Sketchup.active_model.entities.each do |entity|

  # Do something with the entity…

  p entity

end

```

 

However, by opening the Outliner (View > Default Tray > Outliner), we'll see most models aren't flat.

Nested Objects

 

A SketchUp model can in addition to basic geometry contain Components and Groups.

 

A Component is like a nested model within the model. It has its own internal coordinate system and a transformation matrix specifying its location in the model.

For the purpose of this article a Group behaves similar to a component.

 

To iterate over this nested content, we need to create a method that recursively calls itself.

 

```

def traverse_entities(entities)

  entities.each do |entity|

    # Both Sketchup::Group and Sketchup::ComponentInstance reference a

    # Sketchup::ComponentDefinition specifying their insides.

    if entity.respond_to?(:definition)

      traverse_entities(entity.definition.entities)

    end

    

    # Do something with the entity…

    p entity

  end

end

 

traverse_entities(Sketchup.active_model.entities)

```

Transformation

 

The above example doesn't take the transformation matrices of the groups and components into account. It would work in cases where these objects are placed at the origin and lined up with the model axes, but few SketchUp models look like that.

 

For this we need to pass the transformation to the recursive method, and multiply the transformation matrices as we go deeper.

 

```

def traverse_entities(entities, transformation = IDENTITY)

  entities.each do |entity|

    # Both Sketchup::Group and Sketchup::ComponentInstance reference a

    # Sketchup::ComponentDefinition specifying their insides.

    if entity.respond_to?(:definition)

      traverse_entities(

        entity.definition.entities,

        # The order matters as matrix multiplication is non-commutative.

        transformation * entity.transformation

      )

    end

    

    # Do something with the entity…

    p entity

  end

end

 

traverse_entities(Sketchup.active_model.entities)

```

 

This code could already be used for a very simple exporter.

 

Hidden Elements

SketchUp models can contain a lot of hidden objects, e.g. half-finished sketches or previous iterations of the design. This hidden content typically isn't expected in a file export.

 

```

def traverse_entities(entities, transformation = IDENTITY)

  entities.each do |entity|

    # Entity itself can be hidden.

    next unless entity.visible?

    # The entity can have a Tag that is hidden (formerly called Layer).

    next unless entity.layer.visible?

    # And the tag can be in a hidden Tag Folder as of SketchUp 2021

    if entity.layer.respond_to?(:folder)

      next unless visible_tag_folder?(entity.layer.folder)

    end

 

    if entity.respond_to?(:definition)

      traverse_entities(

        entity.definition.entities,

        transformation * entity.transformation

      )

    end

    

    # Do something with the entity...

    p entity

  end

end

 

# Helper method to resolve Tag Folder visibility

def visible_tag_folder?(folder)

  loop do

    return true unless folder

    return false unless folder.visible?

    folder = folder.folder

  end

end

 

traverse_entities(Sketchup.active_model.entities)

```

 

Material Inheritance

For an exporter or a renderer it's typically useful to know what material an object is painted with.

 

SketchUp has a quite special material inheritance model where you can either paint individual faces, or paint a group or component "from the outside'' to have the material apply to it as a whole. Materials coming from the parent have effect only when there is no material painted directly to the child entity.

 

```

def traverse_entities(entities, transformation = IDENTITY, material = nil)

  entities.each do |entity|

    next unless entity.visible?

    next unless entity.layer.visible?

    if entity.layer.respond_to?(:folder)

      next unless visible_tag_folder?(entity.layer.folder)

    end

    

    if entity.respond_to?(:definition)

      traverse_entities(

        entity.definition.entities,

        transformation * entity.transformation,

        entity.material || material

      )

    end

    

    # Do something with the entity...

    p entity

    p entity.material || material

  end

end

 

def visible_tag_folder?(folder)

  loop do

    return true unless folder

    return false unless folder.visible?

    folder = folder.folder

  end

end

 

traverse_entities(Sketchup.active_model.entities)

```

Final Touches

To make your code more readable, you can separate the boilerplate model traversal code from the stuff you want to get done on each entity.

 

```

def traverse_entities(entities, transformation = IDENTITY, material = nil, &block)

  entities.each do |entity|

    next unless entity.visible?

    next unless entity.layer.visible?

    if entity.layer.respond_to?(:folder)

      next unless visible_tag_folder?(entity.layer.folder)

    end

 

    

    if entity.respond_to?(:definition)

      traverse_entities(

        entity.definition.entities,

        transformation * entity.transformation,

        entity.material || material,

        &block

      )

    end

 

    yield entity, transformation, entity.material || material

  end

end

 

def visible_tag_folder?(folder)

  loop do

    return true unless folder

    return false unless folder.visible?

    folder = folder.folder

  end

end

 

```

 

And then we can use this code for something useful, like summing up the area for the materials in the model.

 

```

material_areas = {}

traverse_entities(Sketchup.active_model.entities) do |entity, transformation, material|

  next unless material

  next unless entity.is_a?(Sketchup::Face)

  

  material_areas[material] ||= 0

  material_areas[material] += entity.area(transformation)

end

material_areas.each do |material, area|

  puts "#{material.name}: #{Sketchup.format_area(area)}"

end

```

 

…or a super simple SVG exporter.

 

 

```

# Rudimentary SVG exporter

 

model = Sketchup.active_model

default_color = model.rendering_options["FaceFrontColor"]

 

svg = "<svg xmlns=\"http://www.w3.org/2000/svg\">"

traverse_entities(model.entities) do |entity, transformation, material|

  next unless entity.is_a?(Sketchup::Face)

  

  points = entity.vertices.map(&:position)

  # Transform points into model coordinates.

  points.each { |pt| pt.transform!(transformation) }

  # Dropping Z coordinate. Exporting the top view of first quadrant only.

  # Not compensating for different Y directions in SketchUp and SVG.

  # Not caring for units. Let Inch translate to pixel.

  points = points.map { |pt| pt.to_a[0..1].map(&:to_s).join(" ") }

  

  d = "M" # SVG Move to command

  d += points.join("L") # SVG line to command

  d += "Z" # SVG close loop command

  

  color = material ? material.color : default_color

  color_string = "rgba(#{color.to_a.map(&:to_s).join(',')})"

  

  svg += "<path d='#{d}' style='fill: #{color_string}' />"

end

svg += "</svg>"

 

path = UI.savepanel

File.write(path, svg)

 

```

And Beyond

There are several paths this code could evolve depending on what you want to use it for.

 

If you create an exporter for a file format that reuses geometry, similar to SketchUp components, you may want to export the `DefinitionsList` and then the model root entities rather than traversing the model recursively.

 

For a format that has a concept of hidden elements, the exporter maybe shouldn't skip what's hidden, but rather export a visibility state.

 

If you want to traverse the geometry currently shown in the SketchUp UI, rather than just skipping hidden entities, you could check the `RenderingOptions` to see if Hidden Objects and Hidden Geometry are displayed.

Further Reading

SketchUp Entity Overview

Tags

Traverse, Walk, Iterate, Model, Scenegraph, Export, Convert