There are multiple ways of creating geometry in SketchUp using the Ruby API. Which approach to choose depends on what the extension is doing.
Sketchup::Entities#add_face et. al.
Sketchup::Entities lets you add faces and edges via Sketchup::Entities#add_face, Sketchup::Entities#add_line and Sketchup::Entities#add_edges.
These methods will merge and split entities similar to how SketchUp's own Rectangle and Line tool. They are best suited for scenarios when you only add a few entities at a time - like a drawing tool.
module Example
# @example
# Example.create_faces_splitting_shared_edge
#
def self.create_faces_splitting_shared_edge
model = Sketchup.active_model
entities = model.active_entities
model.start_operation('Faces', true)
face1 = entities.add_face([0, 0, 0], [6, 0, 0], [6, 6, 0], [0, 6, 0])
face2 = entities.add_face([6, 3, 0], [9, 3, 0], [9, 6, 0], [6, 6, 0])
model.commit_operation
end
end
module Example
# @example
# Example.create_cube
#
def self.create_cube
model = Sketchup.active_model
model.start_operation('Create Cube', true)
group = model.active_entities.add_group
entities = group.entities
points = [
Geom::Point3d.new(0, 0, 0),
Geom::Point3d.new(1.m, 0, 0),
Geom::Point3d.new(1.m, 1.m, 0),
Geom::Point3d.new(0, 1.m, 0)
]
face = entities.add_face(points)
face.pushpull(-1.m)
model.commit_operation
end
end
Geom::PolygonMesh and Sketchup::Entities#fill_from_mesh
Geom::PolygonMesh serves two purposes. One is to return the triangulation of a Sketchup::Face, via Sketchup::Face#mesh, useful for exporters.
The other purpose is to generate geometry when used along with Sketchup::Entities#fill_from_mesh and Sketchup::Entities#add_faces_from_mesh. Sketchup::Entities#fill_from_mesh is significantly faster than Sketchup::Entities#add_faces_from_mesh.
Note that as of SketchUp 2022 Sketchup::EntitiesBuilder is the recommended method for created large amount of geometry.
The combination of Geom::PolygonMesh and Sketchup::Entities#fill_from_mesh is fast because it doesn't merge and split geometry like Sketchup::Entities#add_face does. Instead it only ensures that vertices and edges are de-duplicated and leaves it to the API user to create well formed geometry.
Beware that before SketchUp 2021.1 Geom::PolygonMesh was very inefficient when de-duplicating points. For that reason it was best to use Geom::PolygonMesh#add_point first and then use the returned indices with Geom::PolygonMesh#add_polygon.
module Example
# @param [Integer] rows
# @param [Integer] columns
# @return [Geom::PolygonMesh]
def self.generate_grid_mesh(rows, columns)
# Compute the number of points and polygons we'll create. This is important
# for max performance so PolygonMesh can allocate enough memory up front
# and choose appropriate internal algorithm for looking up points.
num_polygons = rows * columns
row_points = rows + 1
col_points = columns + 1
num_points = row_points * col_points
mesh = Geom::PolygonMesh.new(num_points, num_polygons)
# To minimize the number of times points are looked up they are added
# explicitly before adding any polygons.
# As of SketchUp 2021.1 this step is less important, one can pass the points
# to mesh.add_polygon instead of the indicies. There is however always a
# performance benefit of building the polygons with indicies.
indicies = []
row_points.times { |x|
col_points.times { |y|
point = Geom::Point3d.new(x * 10, y * 10, 0)
indicies << mesh.add_point(point)
}
}
(0...rows).each { |x|
(0...columns).each { |y|
i1 = (col_points * y) + x
i2 = i1 + 1
i3 = i2 + col_points
i4 = i3 - 1
polygon = [i1, i2, i3, i4].map { |i| indicies[i] }
mesh.add_polygon(polygon)
}
}
mesh
end
# @example
# Example.create_grid(6, 8)
#
# @param [Integer] rows
# @param [Integer] columns
def self.create_grid(rows = 5, columns = 5)
mesh = self.generate_grid_mesh(rows, columns)
model = Sketchup.active_model
model.start_operation('Grid', true)
group = model.active_entities.add_group
group.entities.fill_from_mesh(mesh, true, Geom::PolygonMesh::NO_SMOOTH_OR_HIDE)
model.commit_operation
end
end
The downside of this approach is that there is less control per-entity generated. Materials are added by Sketchup::Entities#fill_from_mesh to the whole mesh. Only limited control over per-edge properties such as Sketchup::Edge#soft?, Sketchup::Edge#smooth? and Sketchup::Drawingelement#hidden? is offered. This comes a problem for importers of formats that support per-face materials.
Sketchup::EntitiesBuilder
As of SketchUp 2022.0 the best alterative to generating bulk geometry is the Sketchup::EntitiesBuilder interface. Similar to Geom::PolygonMesh it only de-duplicates vertices and edges. It does however have an interface very similar to Sketchup::Entities which allows full control over per-entity properties.
Maximizing performance
In scenarios with very high amount of geometry it might be a slight gain by creating all the unique 3D points in a pool up front and reusing them when creating the faces. Benchmark and profile before you go to this extent.
module Example
# @example
# Example.create_grid(6, 8)
#
# @param [Integer] rows
# @param [Integer] columns
def self.create_grid(rows = 5, columns = 5)
model = Sketchup.active_model
model.start_operation('Grid', true)
# In scenarios with very high amount of geometry it might be a slight gain
# by creating all the unique 3D points in a pool up front and reusing them
# when creating the faces. Benchmark and profile before you go to this
# extent.
row_points = rows + 1
col_points = columns + 1
points = []
row_points.times { |x|
col_points.times { |y|
points << Geom::Point3d.new(x * 10, y * 10, 0)
}
}
model.active_entities.build { |builder|
(0...rows).each { |x|
(0...columns).each { |y|
i1 = (col_points * y) + x
i2 = i1 + 1
i3 = i2 + col_points
i4 = i3 - 1
polygon = [i1, i2, i3, i4].map { |i| points[i] }
builder.add_face(polygon)
}
}
}
model.commit_operation
end
end
Supporting older SketchUp versions
In some scenarios it is possible to leverage duck-typing to allow an extension to generate geometry with either Sketchup::Entities or Sketchup::EntitiesBuilder.
For this to work the code must not rely on Sketchup::Entities#add_face's ability to split and intersect geometry as Sketchup::EntitiesBuilder#add_face will not behave similarly. The code must also not modify the position of the vertices for the duration of the builder's block.
module Example
# @example
# Example.create_cube
def self.create_cube
model = Sketchup.active_model
model.start_operation('Cube', true)
group = model.active_entities.add_group
entities = group.entities
if entities.respond_to?(:build)
entities.build do |builder|
self.add_cube_faces(builder)
end
else
self.add_cube_faces(entities)
end
model.commit_operation
end
# This works even though {Sketchup::Entities} and {Sketchup::EntitiesBuilder}
# are different interfaces because the method only uses methods that have
# method signatures that are similar between them.
#
# @param [Sketchup::Entities, Sketchup::EntitiesBuilder] entities
def self.add_cube_faces(entities)
# ...
entities.add_face(points1)
entities.add_face(points2)
entities.add_face(points3)
entities.add_face(points4)
entities.add_face(points5)
entities.add_face(points6)
end
end
Ruby C Extension considerations
Calling Sketchup::Entities#build
// Good source on how to use Ruby's C API:
// https://silverhammermba.github.io/emberb/c/
VALUE handler(RB_BLOCK_CALL_FUNC_ARGLIST(builder, callback_arg))
{
VALUE rb_ary_new_capa(4);
// Add points ...
VALUE face = rb_funcall(builder, rb_intern("add_face"), 1, points);
return Qnil;
}
void generate(VALUE entities)
{
VALUE result =
rb_block_call(entities, rb_intern("build"), 0, NULL, handler, Qnil);
}
Optimizing and keeping Ruby objects alive
If you are generating geometry from an internal geometry representation you might have code that looks something like this:
#include <algorithm>
#include <vector>
void generate(std::vector<Face*> faces)
{
for (const auto& face : faces) {
VALUE points = rb_ary_new_capa(static_cast<long>(vertices->size()));
for (const auto& vertex : face->vertices()) {
// This ends up creating a new Ruby point every time.
VALUE point = GetRubyVALUE(vertex->position());
rb_ary_push(points, point);
}
// builder.add_face(point) ...
}
}
Similar to the optimization maximization described for Sketchup::EntitiesBuilder you might find that avoiding unnecessarily creating new Ruby objects can yield some performance benefits. Below is an example of one way of doing that:
#include <algorithm>
#include <unordered_map>
#include <vector>
void generate(std::vector<Face*> faces)
{
// In some cases it might make sense to create all the unique the Ruby objects
// up front to reduce the overhead.
VALUE keep_alive = rb_ary_new_capa(static_cast<long>(vertices->size()));
std::unordered_map<Vertex*, VALUE> ruby_points;
for (const auto& vertex : vertices) {
VALUE point = GetRubyVALUE(vertex->position());
ruby_points[vertex] = point;
// Ruby will protect VALUE object in the stack from the garbage collection.
// VALUE objects in an unordered_map is on the stack so it's not protected.
// Because of this we need to protect them by using a temporary array.
rb_ary_push(keep_alive, point);
}
for (const auto& face : faces) {
VALUE points = rb_ary_new_capa(static_cast<long>(vertices->size()));
for (const auto& vertex : face->vertices()) {
VALUE point = ruby_points[vertex];
rb_ary_push(points, point);
}
// builder.add_face(point) ...
}
}