top of page
TrimbleR-Horiz-RGB-Blue.jpg
SketchUp-Horizontal-RGB-120pxl_0.png

Drawing a Table

In the previous articles we looked at writing our very first code and then setting up a developer environment for SketchUp extensions. In this article we will look into combining the individual API calls into drawing a meaningful object in SketchUp, in this case a table, but the same principle applies if you want to draw a stud wall, a roof truss, a cabinet or any other custom object.

This article is mostly aimed at those new to coding but could also be of use to experienced developers new to SketchUp.

To follow along in this article you could work in a code editor, save it as an .rb file and load it into SketchUp by typing 'load' followed by the file path in quotes in the SketchUp console. To reload the file once you've edited it, you can just press the up arrow key and Enter in the console to reload it. You could also use Eneroth Script Runner to drag and drop the .rb file directly into SketchUp.

Basic Table

To begin with, let's draw the most basic table we can think of: a board with 4 legs. We already have the code for drawing a box (aka cuboid or rectangular prism) from a previous article. Lets wrap it in a nice little method and call it five times with different inputs.

# @param entities [Sketchup::Entities]
# @param origin [Geom::point3d]
# @param width [Length] X extents
# @param depth [Length] Y extents
# @param height [Length] Z extents
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

# @param entities [Sketchup::Entities]
# @param width [Length] X extents
# @param depth [Length] Y extents
# @param height [Length] Z extents
def draw_table(entities, width, depth, height, board_thickness, leg_thickness)
  leg_height = height - board_thickness
  
  # Board
  draw_box(entities, [00, leg_height], width, depth, board_thickness)
  
  # Legs
  draw_box(entities, ORIGIN, leg_thickness, leg_thickness, leg_height)
  draw_box(entities, [width - leg_thickness, 00], leg_thickness,
           leg_thickness, leg_height)
  draw_box(entities, [0, depth - leg_thickness, 0], leg_thickness,
           leg_thickness, leg_height)
  draw_box(entities, [width - leg_thickness, depth - leg_thickness, 0],
           leg_thickness, leg_thickness, leg_height)
end

width = 1.7.m
depth = 80.cm
height = 72.cm
board_thickness = 20.mm
leg_thickness = 30.mm

model = Sketchup.active_model
model.start_operation("Draw Table"true)
draw_table(model.active_entities, width, depth, height, board_thickness, leg_thickness)
model.commit_operation

If you compare draw_box to the corresponding method in Writing Your First Code, this method has gotten shorter and its scope has been refined. Now it only draws a box without handling user inputs or start and commit operation. This is not to say these things are not important, just that they don't belong inside of the box drawing method.

By keeping methods short with as small a scope as possible, you make them more reusable and easier to assemble into a bigger whole. Think of methods as LEGO pieces, modular elements that can be combined in any imaginable way. Whereas a standard 2 x 4 brick can be used for almost anything an Aircraft Fuselage Forward Top Curved 8 x 16 x 5 with Trans-Brown Glass brick can only be used for the front of an airplane (or possibly high speed train). draw_box is our 2 x 4. draw_table is a bit more like the Aircraft Fuselage thingy.

In SketchUp, you can't do start and commit operation within another start and commit operation. Instead of calling these methods within the basic drawing methods, it's recommended to call them closer to the UI level, where the user invokes the code.

An addition from our old draw_box is the origin argument. This lets us place the box anywhere, but also means we have to do a little more math to define the coordinates.

Another addition is the documentation comments above the methods. While they are ignored by the computer and not strictly necessary, they can be helpful to keep track of what arguments a method expects. This is especially true in a loosely typed language like Ruby where we could accidentally pass the wrong parameters and be confused by the results.

For the record, in an actual extension you'd want to wrap the user inputs in a method and invoke from a menu or toolbar, like this. However, when doing quick iterations in development, it can be convenient to make those calls directly when the code loads.

def draw_table_ui
  width = 1.7.m
  depth = 80.cm
  height = 72.cm
  board_thickness = 20.mm
  leg_thickness = 30.mm
  
  inputs = UI.inputbox(
    ["Width""Depth""Height""Board Thickness""Leg Thickness"],
    [width, depth, height, board_thickness, leg_thickness],
    "Draw Table"
  )
  return unless inputs
  
  width, depth, height, board_thickness, leg_thickness = inputs
  
  model = Sketchup.active_model
  model.start_operation("Draw Table"true)
  draw_table(model.active_entities, width, depth, height, board_thickness,
             leg_thickness)
  model.commit_operation
end

menu = UI.menu("Extensions")
menu.add_item("Draw Table") { draw_table_ui }

This may look like a decent table but if you try to move a leg or make other adjustments you'll find that all geometry sticks together. SketchUp users will know that each distinct object is supposed to go in its own group or component!

Decent Table

Let's divide our table into proper parts. The simplest thing we can do is to create a group for each part.

# @param entities [Sketchup::Entities]
# @param width [Length] X extents
# @param depth [Length] Y extents
# @param height [Length] Z extents
def draw_table(entities, width, depth, height, board_thickness, leg_thickness)
  table = entities.add_group
  leg_height = height - board_thickness
  
  # Board
  board = table.entities.add_group
  draw_box(board.entities, [00, leg_height], width, depth, board_thickness)
  
  # Legs
  leg = table.entities.add_group
  draw_box(leg.entities, ORIGIN, leg_thickness, leg_thickness, leg_height)
  leg = table.entities.add_group
  draw_box(leg.entities, [width - leg_thickness, 00], leg_thickness,
           leg_thickness, leg_height)
  leg = table.entities.add_group
  draw_box(leg.entities, [0, depth - leg_thickness, 0], leg_thickness,
           leg_thickness, leg_height)
  leg = table.entities.add_group
  draw_box(leg.entities, [width - leg_thickness, depth - leg_thickness, 0],
           leg_thickness, leg_thickness, leg_height)
end

width = 1.7.m
depth = 80.cm
height = 72.cm
board_thickness = 20.mm
leg_thickness = 30.mm

model = Sketchup.active_model
model.start_operation("Draw Table"true)
draw_table(model.active_entities, width, depth, height, board_thickness,
           leg_thickness)
model.commit_operation

Now all parts can be moved freely without sticking together,

However, if you double click your way into these groups you'll see that their axes line up with the parent axes. This is because we didn't specify transformations for the groups and SketchUp defaulted to the identity matrix. This isn't how a typical SketchUp user would draw a table.

Instead we can create our custom transformations for the positioning of the parts. A transformation, or transformation matrix, could be thought of as the placement of the drawing axes or as a coordinate system. It can also be thought of as a movement, but more on that later. There are a number of API methods to create different kinds of transformations, for moving, rotating and sizing. Let's try it out!

# @param entities [Sketchup::Entities]
# @param width [Length] X extents
# @param depth [Length] Y extents
# @param height [Length] Z extents
def draw_table(entities, width, depth, height, board_thickness, leg_thickness)
  table = entities.add_group
  leg_height = height - board_thickness
  
  # Board
  board = table.entities.add_group
  board.transformation = Geom::Transformation.new([00, leg_height])
  draw_box(board.entities, ORIGIN, width, depth, board_thickness)
  
  # Legs
  
  leg = table.entities.add_group
  leg.transformation = Geom::Transformation.new(ORIGIN)
  draw_box(leg.entities, ORIGIN, leg_thickness, leg_thickness, leg_height)
  
  leg = table.entities.add_group
  leg.transformation = Geom::Transformation.new([width - leg_thickness, 00])
  draw_box(leg.entities, ORIGIN, leg_thickness, leg_thickness, leg_height)
  
  leg = table.entities.add_group
  leg.transformation = Geom::Transformation.new([0, depth - leg_thickness, 0])
  draw_box(leg.entities, ORIGIN, leg_thickness, leg_thickness, leg_height)
  
  leg = table.entities.add_group
  leg.transformation =
   Geom::Transformation.new([width - leg_thickness, depth - leg_thickness, 0])
  draw_box(leg.entities, ORIGIN, leg_thickness, leg_thickness, leg_height)
end

width = 1.7.m
depth = 80.cm
height = 72.cm
board_thickness = 20.mm
leg_thickness = 30.mm

model = Sketchup.active_model
model.start_operation("Draw Table"true)
draw_table(model.active_entities, width, depth, height, board_thickness,
           leg_thickness)
model.commit_operation

If you double click into the board and the legs you'll see their drawing axes being in the lower front left corner, as if they were created manually in SketchUp. If you look at the code you'll see it contains the same coordinates, just moved around a bit. Instead of starting drawing each box from a custom point, we draw them from the local origin and instead use our custom point to define the position of the group.

You may also have noticed that the line for drawing the content of a leg is the same for all legs. The legs are now geometrically identical.

draw_box(leg.entitiesORIGINleg_thicknessleg_thicknessleg_height)

As SketchUp users we'd draw the leg once and just copy it. Let's do the same with code!

def draw_table(entities, width, depth, height, board_thickness, leg_thickness)
  table = entities.add_group
  leg_height = height - board_thickness
  
  # Board
  board = table.entities.add_group
  board.transformation = Geom::Transformation.new([00, leg_height])
  draw_box(board.entities, ORIGIN, width, depth, board_thickness)
  
  # Legs
  
  first_leg = table.entities.add_group
  first_leg.transformation = Geom::Transformation.new(ORIGIN)
  draw_box(first_leg.entities, ORIGIN, leg_thickness, leg_thickness, leg_height)
  
  table.entities.add_instance(
    first_leg.definition,
    Geom::Transformation.new([width - leg_thickness, 00])
  )
  table.entities.add_instance(
    first_leg.definition,
    Geom::Transformation.new([0, depth - leg_thickness, 0])
  )
  table.entities.add_instance(
    first_leg.definition,
    Geom::Transformation.new([width - leg_thickness, depth - leg_thickness, 0])
  )
end

width = 1.7.m
depth = 80.cm
height = 72.cm
board_thickness = 20.mm
leg_thickness = 30.mm

model = Sketchup.active_model
model.start_operation("Draw Table"true)
draw_table(model.active_entities, width, depth, height, board_thickness,
           leg_thickness)
model.commit_operation

Instead of creating a brand new group, this code adds additional instances of the first leg group. The difference is subtle. In SketchUp, groups typically represent unique objects and if you enter a group to edit it, it's silently made unique, but if you select a leg and look at Entity Info, it will say there are 4 of them in the model. Reusing group definitions like this makes for a cleaner model and slightly smaller file size.

We can also make the legs components. Components in SketchUp represent classes of identical, interchangeable objects and this is probably how you'd typically draw a table manually. When you edit one component, all instances of the same definition are updated. If you edit one table leg, it's likely because you want all legs to be changed, not just one of them.

def draw_table(entities, width, depth, height, board_thickness, leg_thickness)
  table = entities.add_group
  leg_height = height - board_thickness
  
  # Board
  board = table.entities.add_group
  board.transformation = Geom::Transformation.new([00, leg_height])
  draw_box(board.entities, [000], width, depth, board_thickness)
  
  # Legs
  leg = entities.model.definitions.add("Leg")
  draw_box(leg.entities, ORIGIN, leg_thickness, leg_thickness, leg_height)
  table.entities.add_instance(leg, IDENTITY)
  table.entities.add_instance(
    leg,
    Geom::Transformation.new([width - leg_thickness, 00])
  )
  table.entities.add_instance(
    leg,
    Geom::Transformation.new([0, depth - leg_thickness, 0])
  )
  table.entities.add_instance(
    leg,
    Geom::Transformation.new([width - leg_thickness, depth - leg_thickness, 0])
  )
end

width = 1.7.m
depth = 80.cm
height = 72.cm
board_thickness = 20.mm
leg_thickness = 30.mm

model = Sketchup.active_model
model.start_operation("Draw Table"true)
draw_table(model.active_entities, width, depth, height, board_thickness,
           leg_thickness)
model.commit_operation

You can try double clicking into a leg and modify it, e.g. select the bottom face and make it smaller with Scale tool to taper the legs. These changes are seen on all 4 legs.

There is however a case against using components too liberally in extensions. Making the leg a component means it also shows up in the In Model category of the Components panel. If you intend for your tables to be fully parametric and be edited by entering new dimensions, not by the SketchUp drawing tools, you typically don't want their inside parts to be exposed in the Components panel. Not only does it open up for accidental editing, it also floods the Component panel with irrelevant parts. This example just has a table leg, but imagine getting every stud, plate, bracing, nut and bolt in this list when you as a user really want to focus on the bigger picture. When the part is expected to be managed by your extension and not directly by the user, prefer groups.

Nicer Table

This is a very minimalist looking table with the legs perfectly aligned with the corners. Lets make the leg inset user adjustable. While at it, we can also draw the leg centered on their Z axis. It makes sense if we want to have other shaped legs to have the local origin along the symmetry line and not in a corner.

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([00, leg_height])
  draw_box(board.entities, [000], width, depth, board_thickness)
  
  # Legs
  leg = entities.model.definitions.add("Leg")
  draw_box(leg.entities, [-leg_thickness / 2, -leg_thickness / 20],
           leg_thickness, leg_thickness, leg_height)
  table.entities.add_instance(
    leg,
    Geom::Transformation.new([leg_inset, leg_inset, 0])
  )
  table.entities.add_instance(
    leg,
    Geom::Transformation.new([width - leg_inset, leg_inset, 0])
  )
  table.entities.add_instance(
    leg,
    Geom::Transformation.new([leg_inset, depth - leg_inset, 0])
  )
  table.entities.add_instance(
    leg,
    Geom::Transformation.new([width - leg_inset, depth - leg_inset, 0])
  )
end

width = 1.7.m
depth = 80.cm
height = 72.cm
board_thickness = 20.mm
leg_thickness = 30.mm
leg_inset = 50.mm

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

Maybe you've already tinkered with your table and tried nicer legs than these boring blocks. We can do the same using code! Lets taper the legs.

# @note Use an empty entities collection with this method.
#
# @param entities [Sketchup::Entities]
# @param origin [Geom::point3d]
# @param width [Length] X extents
# @param depth [Length] Y extents
# @param height [Length] Z extents
def draw_truncated_pyramid(entities, base, top, height)
  face = entities.add_face([
    [-base / 2, -base / 20],
    [base / 2, -base / 20],
    [base / 2, base / 20],
    [-base / 2, base / 20]
  ])
  face.reverse! unless face.normal.samedirection?(Z_AXIS)
  face.pushpull(height)
  
  # Assume there are no other faces in entities
  top_face =
    entities.grep(Sketchup::Face).find { |f| f.normal.samedirection?(Z_AXIS) }
  transformation = Geom::Transformation.scaling([00, height], top / base)
  entities.transform_entities(transformation, [top_face])
end

draw_truncated_pyramid(Sketchup.active_model.entities5.cm8.cm1.m)

The mathematical name for a tapered box is a truncated pyramid, so that's what we'll call the method. SketchUp has no built-in method for this shape but we can create our own. Just as with draw_box we mimic the steps you'd take using SketchUp tools. After push pull, you'd use Scale tool to change the size of either the top or bottom face. With the Ruby API, we morph the face using a transformation that represents scaling around its center point.

We can also simplify the code quite a bit by introducing some limitations. For draw_box we passed in an origin point representing a corner. Since we know we want these truncated pyramids on the local origin, we can hardcode that. We are hardcoding the tapering to be along the vertical direction anyway. If we ever want a different placement in the future, we can just use a transformation to achieve that, and not have to build it into the basic draw method. For now we can also assume a square cross section.

After push-pulling, we no longer have a reference to the faces to use for the subsequent scaling. Push-pull doesn't return a reference to the newly formed parallel face and the old face can be removed and replaced during the operation. Instead we can iterate the entities and find the face we are looking for based on its normal direction.

The easiest way to prevent a completely different face in the same entities collection from being found instead is to make sure the draw_truncated_pyramid method is only called with an empty entities collection. This should normally be the case anyway as SketchUp objects are supposed to be separated into groups and components, but to prevent future bugs it's best to document this requirement.

Tweaking draw_table to use the new draw_truncated_pyramid is left as an exercise for the reader.

We could also make the legs round.

# @note Use an empty entities collection with this method.
#
# @param entities [Sketchup::Entities]
# @param radius [Length]
# @param height [Length]
def draw_cylinder(entities, radius, height)
  circle = entities.add_circle(ORIGINZ_AXIS, radius)
  circle[0].find_faces
  # Assume there are no other faces in entities
  face = entities.grep(Sketchup::Face)[0]
  face.reverse! unless face.normal.samedirection?(Z_AXIS)
  face.pushpull(height)
end

draw_cylinder(Sketchup.active_model.entities15.mm1.m)

Or combine the two into a nice tapered round leg!

# @note Use an empty entities collection with this method.
#
# @param entities [Sketchup::Entities]
# @param base [Length]
# @param top [Length]
# @param height [Length]
def draw_truncated_cone(entities, base, top, height)
  circle = entities.add_circle(ORIGINZ_AXIS, base)
  circle[0].find_faces
  # Assume there are no other faces in entities
  face = entities.grep(Sketchup::Face)[0]
  face.reverse! unless face.normal.samedirection?(Z_AXIS)
  face.pushpull(height)
  
  # Assume there are no other faces in entities
  top_face =
    entities.grep(Sketchup::Face).find { |f| f.normal.samedirection?(Z_AXIS) }
  transformation = Geom::Transformation.scaling([00, height], top / base)
  entities.transform_entities(transformation, [top_face])
end

draw_truncated_cone(Sketchup.active_model.entities10.mm15.mm1.m)

Here you probably want to divide leg_thickness by 2 as these methods expect a radius, not a diameter.

Fancy Table!

You may want to angle the legs for that gorgeous mid century modern feel. If you enter the leg component and start angling it now by dragging the top face around, you'll notice the legs all tilt in the same direction, not inwards towards the center of the table. To help with this, we can rotate the leg components by introducing a new transformation.

  table.entities.add_instance(
    leg,
    Geom::Transformation.new([leg_insetleg_inset0]) *
      Geom::Transformation.rotation(ORIGINZ_AXIS0)
  )
  table.entities.add_instance(
    leg,
    Geom::Transformation.new([width - leg_insetleg_inset0]) *
      Geom::Transformation.rotation(ORIGINZ_AXIS90.degrees)
  )
  table.entities.add_instance(
    leg,
    Geom::Transformation.new([leg_insetdepth - leg_inset0]) *
      Geom::Transformation.rotation(ORIGINZ_AXIS270.degrees)
  )
  table.entities.add_instance(
    leg,
    Geom::Transformation.new([width - leg_insetdepth - leg_inset0]) *
      Geom::Transformation.rotation(ORIGINZ_AXIS180.degrees)
  )

Note that you need to enter the table component and adjust it for these changes to be visible. Combining coding with some manual drawing can help figure out the orientations of all objects or what you actually want the code to do.

Here the translation transformation (similar to Move tool) is combined with a rotation transformation. Note that the order of the transformations matter. While multiplication of numbers is commutative - 4 5 is the same as 5 4 - matrix multiplication is non-commutative. Moving something 1 m to the model left and then rotating it 90 degrees around the model origin, or first rotating it 90 degrees around the model origin and then moving it 1 m to the model left, will yield different results. To get the order of the transformations right you could try to reason about what order they happen in, or you could just try both ways and see what works.

Another way to achieve the same result is to use Transformation.axes to define the placement using an origin point and axes vectors. This approach is especially handy when angling objects in increments of 90 degrees.

  table.entities.add_instance(
    leg,
    Geom::Transformation.axes([leg_insetleg_inset0], X_AXISY_AXISZ_AXIS)
  )
  table.entities.add_instance(
    leg,
    Geom::Transformation.axes([width - leg_insetleg_inset0], Y_AXIS,
                              X_AXIS.reverseZ_AXIS)
  )
  table.entities.add_instance(
    leg,
    Geom::Transformation.axes([leg_insetdepth - leg_inset0], X_AXIS,
                              Y_AXIS.reverseZ_AXIS)
  )
  table.entities.add_instance(
    leg,
    Geom::Transformation.axes([width - leg_insetdepth - leg_inset0],
                              X_AXIS.reverseY_AXIS.reverseZ_AXIS)
  )

In both these cases however the table looks odd if we move the top face a different distance in the local X and Y axis. The legs are just rotated by increments of 90 degrees. We can improve this by flipping two opposite legs.

  table.entities.add_instance(
    leg,
    Geom::Transformation.axes([leg_insetleg_inset0], X_AXISY_AXISZ_AXIS)
  )
  table.entities.add_instance(
    leg,
    Geom::Transformation.axes([width - leg_insetleg_inset0], X_AXIS.reverse,
                              Y_AXISZ_AXIS)
  )
  table.entities.add_instance(
    leg,
    Geom::Transformation.axes([leg_insetdepth - leg_inset0], X_AXIS,
                              Y_AXIS.reverseZ_AXIS)
  )
  table.entities.add_instance(
    leg,
    Geom::Transformation.axes([width - leg_insetdepth - leg_inset0],
                              X_AXIS.reverseY_AXIS.reverseZ_AXIS)
  )

Now you can have asymmetrically tilting legs! Maybe just tilt slightly along the width and more along the length of the table?

Another way is to not just rotate the legs by increments of 90 degrees, but add in 45 degrees. With this code, you only need to drag the top of the leg along one axis to tilt it. This makes for a cleaner SketchUp model where the local axes are better aligned with your geometry.

  table.entities.add_instance(
    leg,
    Geom::Transformation.new([leg_insetleg_inset0]) *
      Geom::Transformation.rotation(ORIGINZ_AXIS45.degrees)
  )
  table.entities.add_instance(
    leg,
    Geom::Transformation.new([width - leg_insetleg_inset0]) *
      Geom::Transformation.rotation(ORIGINZ_AXIS135.degrees)
  )
  table.entities.add_instance(
    leg,
    Geom::Transformation.new([leg_insetdepth - leg_inset0]) *
      Geom::Transformation.rotation(ORIGINZ_AXIS315.degrees)
  )
  table.entities.add_instance(
    leg,
    Geom::Transformation.new([width - leg_insetdepth - leg_inset0]) *
      Geom::Transformation.rotation(ORIGINZ_AXIS225.degrees)
  )

Of course we don't need to hardcode this angle to 45 degrees, but can change it so the legs are a little more aligned with the length of the table than the width.

  z_angle = 30.degrees
  table.entities.add_instance(
    leg,
    Geom::Transformation.new([leg_insetleg_inset0]) *
      Geom::Transformation.rotation(ORIGINZ_AXIS, z_angle)
  )
  table.entities.add_instance(
    leg,
    Geom::Transformation.new([width - leg_insetleg_inset0]) *
      Geom::Transformation.rotation(ORIGINZ_AXIS180.degrees - z_angle)
  )
  table.entities.add_instance(
    leg,
    Geom::Transformation.new([leg_insetdepth - leg_inset0]) *
      Geom::Transformation.rotation(ORIGINZ_AXIS, -z_angle)
  )
  table.entities.add_instance(
    leg,
    Geom::Transformation.new([width - leg_insetdepth - leg_inset0]) *
      Geom::Transformation.rotation(ORIGINZ_AXIS180.degrees + z_angle)
  )

You could even expose this angle as a user option.

You could also calculate the angle so the legs are angled towards the center of the table. Some of these decisions are up to the user to make, some are made by you when writing the code.

z_angle = Math.atan((depth / 2 - leg_inset) / (width / 2 - leg_inset))

Finally, instead of manually dragging a part of the leg around to tilt it, we could do this by code.

  leg_offset = 150.mm
  top_face = leg.entities.grep(Sketchup::Face)
                .find { |f| f.normal.samedirection?(Z_AXIS) }
  transformation = Geom::Transformation.new([leg_offset, 00])
  leg.entities.transform_entities(transformation, [top_face])

At this point, why not find a nice teak texture in 3D Warehouse and apply to your table for that sleek mid century modern look!

Further Exercises

For a stronger (or more realistic) table, you could try drawing aprons. You can base it on the same draw_box method and calculate the coordinates based on an apron_height and apron_inset input. This is easiest to combine with straight rectangular legs.

You could also look into having the tapering at just the lower part of the leg and not all the way up to the board, to have straight sides to attach the apron to. For this you could rework draw_truncated_pyramid into a draw_partially_tapered_box method that also takes a taper_height parameter. You can push-pull to that height, scale the top face and then push-pull the remaining distance.

If you like the angled legs, they too can be improved upon. By just shifting the top face inwards as we did above, the width changes and circular cross section become elliptic. A better approach is to draw the leg a little too long, tilt it by rotating along its local Y axis and then trim away the top and bottom where it meets the board and floor. For this you can draw temporary boxes and use the trim or union methods.

You could also try making a triangular table or a hexagonal table. For this I'd draw the table around its local origin and use rotation transformations for the legs. For a triangular table, use multiples of 120 degrees and for hexagonal multiples of 60. For the board, use add_ngon.

You could also try a "user-choice-agonal" table by exposing the number of sides in the UI. For this, you need your code to iterate a varying number of times and calculate a different angle for each iteration.

sides = 5
sides.times do |i|
  angle = 360.degrees / sides * i
  # Add leg...
end

Interestingly, even if you want your table hardcoded to 3 or 5 sides, using a loop like this probably allows you to do that with less code.

Lastly, you could look into exposing these different ways to draw the table as user options, using branching code. There's a balance you'll learn over time how much branching and special cases you want to handle in the same method and when it's better to copy the method and make a slightly different one. For the tables, maybe one draw_rectangular_table method taking width and depth arguments and another draw_ngon_table with a radius argument, both using the same draw_leg method, is the most appropriate.

Reflection

SketchUp is designed with depth, not width. There is a small number of drawing tools that can be combined to do a lot of things. The API is similar. There are no methods for drawing boxes or truncated cones, but we can create them ourselves. We create our own building blocks and stack them into something beautiful.

Some of my favorite starter methods in the SketchUp Ruby API are:

Geom::Transformation.new

Geom::Transformation.axes

Geom::Transformation.scaling

Geom::Transformation.rotation


Sketchup::Entities.add_group

Sketchup::Entities.add_instance

Sketchup::Entities.add_face

Sketchup::Entities.add_line

Sketchup::Entities.add_circle

Sketchup::Entities.find (inherited from Enumerable)


Sketchup::Face.pushpull

But you can also just take a stroll through the API docs and see what other methods you may like. There are thousands of them!

These can be combined to create higher level methods like:

draw_box
draw_cylinder
draw_truncated_box
draw_truncated_cone

You can build your own little library of methods and expand it over time. As an exercise you could also write draw_torus (mmm, donut!).

Transformations are very useful to learn. They can be used similar to Move, Rotate and Scale tool, both for morphing primitive geometry or position group and components.

Lastly, I'd recommend a workflow of quick iterations. Make a change to the code, reload it and try it in SketchUp. I've made so many small errors when writing these examples: mirrored along the wrong axis, getting angles of by 180 degrees, added instead of subtracted and so on. With SketchUp and Ruby, the cost of running the code and seeing the result is very low, often lower than trying to get it right on the first try. Don't be afraid to just try!

Next Steps

Once you know how to draw a custom object in SketchUp, you may want to wrap your code into an extension to share it with the world. You could also learn how to make your object parametric so it can be easily edited by the user at a later point.

bottom of page