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
