Making Objects Parametric
We've already looked at how to draw a complex object in SketchUp from user input using the Ruby API, but to take full advantage of this, you want to be able to go back and change those input parameters.
This article covers a more advanced technique and could be useful for quite experienced developers, but hopefully made simple enough that also relative beginners can follow along.

A Dumb Table
To start with, we have the code to draw a table from the previous article, which should look something like this. If you've customized your code to e.g. use tapered legs and additional user inputs, that's okay; the overall steps are the same but you need to make some minor tweaks.
def draw_box(entities, origin, width, depth, height)
face = entities.add_face([
origin,
[origin.x + width, origin.y, origin.z],
[origin.x + width, origin.y + depth, origin.z],
[origin.x, origin.y + depth, origin.z]
])
face.reverse! unless face.normal.samedirection?(Z_AXIS)
face.pushpull(height)
end
def draw_table(entities, width, depth, height, board_thickness, leg_thickness,
leg_inset)
table = entities.add_group
leg_height = height - board_thickness
# Board
board = table.entities.add_group
board.transformation = Geom::Transformation.new([0, 0, leg_height])
draw_box(board.entities, [0, 0, 0], width, depth, board_thickness)
# Legs
first_leg = table.entities.add_group
first_leg.transformation =
Geom::Transformation.new([leg_inset, leg_inset, 0])
draw_box(first_leg.entities, [-leg_thickness / 2, -leg_thickness / 2, 0],
leg_thickness, leg_thickness, leg_height)
table.entities.add_instance(
first_leg.definition,
Geom::Transformation.new([width - leg_inset, leg_inset, 0])
)
table.entities.add_instance(
first_leg.definition,
Geom::Transformation.new([leg_inset, depth - leg_inset, 0])
)
table.entities.add_instance(
first_leg.definition,
Geom::Transformation.new([width - leg_inset, depth - leg_inset, 0])
)
end
def draw_table_ui
width = 1.7.m
depth = 80.cm
height = 72.cm
board_thickness = 20.mm
leg_thickness = 30.mm
leg_inset = 50.mm
inputs = UI.inputbox(
["Width", "Depth", "Height", "Board Thickness", "Leg Thickness", "Leg Inset"],
[width, depth, height, board_thickness, leg_thickness, leg_inset],
"Draw Table"
)
return unless inputs
width, depth, height, board_thickness, leg_thickness, leg_inset = inputs
model = Sketchup.active_model
model.start_operation("Draw Table", true)
draw_table(model.active_entities, width, depth, height, board_thickness,
leg_thickness, leg_inset)
model.commit_operation
end
menu = UI.menu("Extensions")
menu.add_item("Draw Table") { draw_table_ui }
This code allows us to draw a table, but if we later want to change the input parameters, we need to draw a whole new table. The previous input isn't stored anywhere so the user has to manually enter the same values again, and if they get one value wrong, they have to restart and rewrite all inputs, risking getting another one wrong this time. This is not very practical!
A Smart Table
For a better user experience, we can save the input parameters and allow the same table to be redrawn at any time.
Refactor
To begin, we refactor the code to be easier to build upon. It still does the same thing, just structured slightly differently. These changes make it easier to later hold on to the user configurations for each table.
module Shapes
def self.draw_box(entities, origin, width, depth, height)
face = entities.add_face([
origin,
[origin.x + width, origin.y, origin.z],
[origin.x + width, origin.y + depth, origin.z],
[origin.x, origin.y + depth, origin.z]
])
face.reverse! unless face.normal.samedirection?(Z_AXIS)
face.pushpull(height)
end
end
class Table
PARAM_NAMES = ["Width", "Depth", "Height", "Board Thickness", "Leg Thickness",
"Leg Inset"]
DEFAULTS = [1.7.m, 80.cm, 72.cm, 20.mm, 30.mm, 50.mm]
def initialize
@width, @depth, @height, @board_thickness, @leg_thickness, @leg_inset =
DEFAULTS
end
def edit_parameters
inputs = UI.inputbox(
PARAM_NAMES,
[@width, @depth, @height, @board_thickness, @leg_thickness, @leg_inset],
"Draw Table"
)
return unless inputs
@width, @depth, @height, @board_thickness, @leg_thickness, @leg_inset =
inputs
model = Sketchup.active_model
model.start_operation("Draw Table", true)
draw(model.active_entities)
model.commit_operation
end
def draw(entities)
group = entities.add_group
leg_height = @height - @board_thickness
# Board
board = group .entities.add_group
board.transformation = Geom::Transformation.new([0, 0, leg_height])
Shapes.draw_box(board.entities, [0, 0, 0], @width, @depth, @board_thickness)
# Legs
first_leg = group.entities.add_group
first_leg.transformation =
Geom::Transformation.new([@leg_inset, @leg_inset, 0])
Shapes.draw_box(first_leg.entities,
[-@leg_thickness / 2, -@leg_thickness / 2, 0],
@leg_thickness, @leg_thickness, leg_height)
group.entities.add_instance(
first_leg.definition,
Geom::Transformation.new([@width - @leg_inset, @leg_inset, 0])
)
group.entities.add_instance(
first_leg.definition,
Geom::Transformation.new([@leg_inset, @depth - @leg_inset, 0])
)
group.entities.add_instance(
first_leg.definition,
Geom::Transformation.new([@width - @leg_inset, @depth - @leg_inset, 0])
)
end
end
menu = UI.menu("Extensions")
menu.add_item("Draw Table") { Table.new.edit_parameters }
Here all the behaviors and data defining a table is wrapped into a new custom Table class. The generic drawing that isn't necessarily used only by the table can be extracted to a separate Shapes module. This separation allows us to keep each part of the software smaller and to focus on one thing at a time when coding, without being distracted by everything else.
A class is a way to define the common characteristics for a type of things. There can be any number of tables with different measurements, but they all follow the same pattern. Within the class we define the table-specific methods, such as drawing and the UI for editing the parameters.
When we want to create a new table, also known as an instance of the Table class, we call the Table.new method. In Ruby this triggers the initialize constructor method. Here we can set up the table object with the default parameters. These default values are stored as a constant on the top of the Table class. Once a Table object is created, it holds on to its parameters using instance variables, variables prefixed with a @ sign. Instance variables are accessible from the various instance methods of the Table and are kept in memory for as long as the Table is.
Shapes is defined as a module. Modules are similar to classes, but you can't create instances of them. In this example we are not interested in keeping track of the dimensions we give our boxes, we just calculate them on the fly when we need to draw a box.
If a method definition is prefixed with self., it's a class method that runs on the class as a whole. Otherwise it's an instance method that runs an individual object of that class. draw_box is called on the Shapes module as a whole whereas draw or edit_parameters is called on an individual table.
Editable Table
At this point there's no link between the custom Table object and the group representing a table in the SketchUp model. Once we've drawn our table, we can't get back to the parameters to edit and redraw it.
Also our Table object isn't persistent. Unlike a Sketchup::Group or Sketchup::Face object, our custom Table object isn't stored with the SketchUp model. We need a way to serialize (save) the object so we can deserialize it (recreate) next time we want to use it.
To address this, we tweak the Table class to also track the group the table is drawn to. The initialize constructor method is altered to also be able to recreate a table from a group. We add methods to read and write the parameters to the group. Whenever we draw the table, we also save the parameters and whenever we recreate an existing table, we read the parameters back. Lastly, we add a class method to Table to test if an entity in the SketchUp model represents a table.
class Table
PARAM_NAMES = ["Width", "Depth", "Height", "Board Thickness", "Leg Thickness",
"Leg Inset"]
DEFAULTS = [1.7.m, 80.cm, 72.cm, 20.mm, 30.mm, 50.mm]
DICTIONARY = "jane_doe_custom_table"
def self.table?(entity)
entity.is_a?(Sketchup::Group) && entity.attribute_dictionary(DICTIONARY)
end
def initialize(group = nil)
@group = group
if group
read_parameters
else
@width, @depth, @height, @board_thickness, @leg_thickness, @leg_inset =
DEFAULTS
end
end
def read_parameters
@width = @group.get_attribute(DICTIONARY, "width")
@depth = @group.get_attribute(DICTIONARY, "depth")
@height = @group.get_attribute(DICTIONARY, "height")
@board_thickness = @group.get_attribute(DICTIONARY, "board_thickness")
@leg_thickness = @group.get_attribute(DICTIONARY, "leg_thickness")
@leg_inset = @group.get_attribute(DICTIONARY, "leg_inset")
end
def write_parameters
@group.set_attribute(DICTIONARY, "width", @width)
@group.set_attribute(DICTIONARY, "depth", @depth)
@group.set_attribute(DICTIONARY, "height", @height)
@group.set_attribute(DICTIONARY, "board_thickness", @board_thickness)
@group.set_attribute(DICTIONARY, "leg_thickness", @leg_thickness)
@group.set_attribute(DICTIONARY, "leg_inset", @leg_inset)
end
def edit_parameters
inputs = UI.inputbox(
PARAM_NAMES,
[@width, @depth, @height, @board_thickness, @leg_thickness, @leg_inset],
"Draw Table"
)
return unless inputs
@width, @depth, @height, @board_thickness, @leg_thickness, @leg_inset =
inputs
model = Sketchup.active_model
model.start_operation("Draw Table", true)
draw(model.active_entities)
model.commit_operation
end
def draw(parent_entities = nil)
if @group
@group.entities.clear!
else
@group = parent_entities.add_group
end
write_parameters
leg_height = @height - @board_thickness
# Board
board = @group.entities.add_group
board.transformation = Geom::Transformation.new([0, 0, leg_height])
Shapes.draw_box(board.entities, [0, 0, 0], @width, @depth, @board_thickness)
# Legs
first_leg = @group.entities.add_group
first_leg.transformation =
Geom::Transformation.new([@leg_inset, @leg_inset, 0])
Shapes.draw_box(first_leg.entities,
[-@leg_thickness / 2, -@leg_thickness / 2, 0],
@leg_thickness, @leg_thickness, leg_height)
@group.entities.add_instance(
first_leg.definition,
Geom::Transformation.new([@width - @leg_inset, @leg_inset, 0])
)
@group.entities.add_instance(
first_leg.definition,
Geom::Transformation.new([@leg_inset, @depth - @leg_inset, 0])
)
@group.entities.add_instance(
first_leg.definition,
Geom::Transformation.new([@width - @leg_inset, @depth - @leg_inset, 0])
)
end
end
menu = UI.menu("Extensions")
menu.add_item("Draw Table") { Table.new.edit_parameters }
With all of this, we can add a context menu entry for parametrically editing a table.
UI.add_context_menu_handler do |context_menu|
selection = Sketchup.active_model.selection
next unless selection.size == 1
next unless Table.table?(selection[0])
context_menu.add_separator
context_menu.add_item("Edit Table") do
Table.new(selection[0]).edit_parameters
end
end
Now you can finally right click a table to edit it!
The menu entry is only added if exactly one entity is selected in the SketchUpp model, and that entity represents a table. next is a bit similar to return from Writing Your First Code. However, here we want to break out of a block, not the whole method.
When adding entries to the context menu, always add a separator first. Otherwise it looks like your menu entries are grouped together with whatever happens to be the entries above.
Accessible Table
While not necessary for this use case, we could add attribute accessors so we can edit the table parameters from the outside.
class Table
attr_accessor :width, :depth, :height, :board_thickness, :leg_thickness,
:leg_inset
# ...
end
This allows us to play with the table from the Ruby Console.
table = Table.new
table.width = 2.m
table.height = 90.cm
table.draw(Sketchup.active_model.active_entities)
table.width = 1.m
table.draw(Sketchup.active_model.active_entities)
This also allows other parts of your application to create and configure tables. Perhaps there could be a Table tool that lets you pick corner points similar to Rectangle tool? While not very meaningful with tables, this is highly useful for parametric walls, trusses, slabs and other building elements that are tailored to fit into each other.
This outside parameter access would also be useful for creating nested parametric elements. Maybe you have one button to create a complete building from outer overall measurements and roof pitch, but if you later explode it you can interact parametrically with individual trusses and stud walls.
A SketchUppy Table!
At this point we can create and later reconfigure our custom parametric tables, but they don't behave very SketchUppy. Let's fix that!
Double Click Editing
The primary way to edit a complex object in SketchUp is to double click it to access its internal geometry. However, for our custom tables we don't really want the user to go inside and make manual changes. The manual editing using SketchUp tools and the parametric editing are two clashing paradigms, and any manual changes will be lost if the user later changes the parameters and the table redraws.
We could display a warning whenever the user is about to make manual edits to the table, but a better approach is to automatically steer them into the parametric editing mode. The user doesn't double click a component for the purpose of going inside of it, but to edit it. Going into the component is just the means, not the purpose. At a higher level, our edit_parameters method does what the user wants to do when double clicking.
To achieve this, we can intercept the group being opened, immediately close it and bring up our custom UI, using an observer.
class OpenTableObserver
# Called by SketchUp when you enter or exit groups and components
def onActivePathChanged(model)
# Active path is nil when in model root, no empty array as you might
# expect.
return unless model.active_path
# -1 is the last element in the array
inner_child = model.active_path[-1]
return unless Table.table?(inner_child)
# Go back out of group
Sketchup.undo
# Open parametric editing instead
Table.new(inner_child).edit_parameters
end
def attach(observee)
# Remove observer before attaching, to avoid attaching same observer
# multiple times.
observee.remove_observer(self)
observee.add_observer(self)
end
alias onNewModel attach
alias onOpenModel attach
end
observer = OpenTableObserver.new
observer.attach(Sketchup)
observer.attach(Sketchup.active_model)
An Observer is a way to trigger your code from various events in SketchUp, not just from the user calling it directly from a menu or toolbar. They can be used to make your extension feel tighter integrated into SketchUp. Most of this code is just the boilerplate to attach the observer on each new model the user opens or creates. The relevant part is onActivePathChanged.
If the user wants to do manual editing, such as making tweaks not supported by our parametric model, they can explode the group to access its internal parts. This is analogous to exploding an Image entity in SketchUp to access its internal face and edges.
Scale Tool Integration

If you want to change the overall size of an object in SketchUp, Scale tool is the typical way to do it. If you try this on the table now, it just stretches. Legs get thicker in one direction and the leg inset gets inconsistent. Well, that's annoying. Luckily this too can be fixed!
class Table
# ...
def on_resize
x_scale, y_scale, z_scale =
TransformationHelper.extract_scaling(@group.transformation)
# No need to redraw if we didn't resize the table
return if x_scale == 1 && y_scale == 1 && z_scale == 1
# Calling to_l revert back to the Length class after multiplication
# changes values to Float. this affects how values are displayed in
# UI.inputbox.
@width = (@width * x_scale).to_l
@depth = (@depth * y_scale).to_l
@height = (@height * z_scale).to_l
@group.transformation =
TransformationHelper.reset_scaling(@group.transformation)
draw
end
end
module TransformationHelper
# @param [Geom::Transformation]
# @return [Array<Numeric, Numeric, Numeric>]
def self.extract_scaling(transformation)
# Uniform scaling may be expressed by the last element in the transformation
# matrix.
scale = transformation.to_a[15]
# Axial scaling is represented by the length of each coordinate axis,
# defined by the first 3 values of the first 3 rows.
[
Geom::Vector3d.new(transformation.to_a.values_at(0..2)).length / scale,
Geom::Vector3d.new(transformation.to_a.values_at(4..6)).length / scale,
Geom::Vector3d.new(transformation.to_a.values_at(8..10)).length / scale
]
end
# @param [Geom::Transformation]
# @return [Geom::Transformation]
def self.reset_scaling(transformation)
Geom::Transformation.axes(
transformation.origin,
transformation.xaxis.normalize,
transformation.yaxis.normalize,
transformation.zaxis.normalize,
)
end
end
This method on Table is expected to be called just after the table is resized with Scale tool. In SketchUp, Scale tool changes the selected entity, in this case a group, not its inner content. The scaling is represented by the group's transformation matrix, while its content remains the same in the internal coordinate system.
What we want is to read the new size and instead draw the content using it as the new parameters. To do this, we start by extracting the scaling component from the transformation matrix and multiply it into our parameters. Then we remove the scaling from the transformation, so we don't have the scaling applied twice, while retaining any translation or rotation. Lastly we redraw the table to the new configuration.
The lower level code for extracting and removing scaling from a transformation matrix is kept in a separate TransformationHelper module, to keep Table focused on the table-specific behaviors.
This code can be manually tested by first resizing a table with Scale tool and then triggering on_resize from the Ruby console.
Table.new(Sketchup.active_model.selection.first).on_resize
But we want this to happen automatically each time the user scales a table. For this we use another observer.
class ScaleTableObserver
SCALE_TOOL_ID = 21236
# Triggered by SketchUp when a tool changes state
def onToolStateChanged(tools, _tool_name, tool_id, tool_state)
return unless tool_id == SCALE_TOOL_ID
return unless tool_state == 0
# Scale tool has entered its initial state.
# Either it's been activated, reset or finished scaling something.
table_groups = tools.model.selection.select { |e| Table.table?(e) }
return if table_groups.empty?
tools.model.start_operation("_RESIZE_TABLES", true, false, true)
table_groups.each { |g| Table.new(g).on_resize }
tools.model.commit_operation
end
def attach(observee)
# Remove observer before attaching, to avoid attaching same observer
# multiple times.
observee.remove_observer(self)
observee.add_observer(self)
end
def onNewModel(model)
attach(model.tools)
end
alias onOpenModel onNewModel
end
observer = ScaleTableObserver.new
observer.attach(Sketchup)
observer.attach(Sketchup.active_model.tools)
This observer triggers whenever Scale tool goes into its initial state, which happens when the tool is activated, cancelled or finishes scaling something. When this happens we check if the selection contains any tables. If so, we start an operation, call on_resize on each table and commit the operation.
The operation is made transparent to the previous operation using the 4th argument. This means undoing the redraw also undoes the scaling operation that triggered it. To the user there is just one operation, Scaling, and the table magically just resizes correctly as opposed to merely stretching.
When an operation is transparent to the previous operation, its operation name is not visible to the user. We could pass an empty string but a name could also help anyone maintaining the code to understand what it does. Avoiding the usual title case is a way to hint that this string is not user-facing.
Now you have a happy SketchUppy table that is easy to interact with!