A large home for a prosperous owner, this four-square sports a ground-floor addition (possibly an original feature?) in the kitchen/dining area topped by a bay window, likely in the master suite.

What really catches the eye, however, are the ionic columns around the front porch. (Actually, Tuscan columns with ionic capitals, as Greek columns would have been fluted.)

In contrast to the columns, the upper panes of the dormer windows are a diamond-shaped Tudor style.

Warwick, NY, c. 1920.

Substantial four-square with odd details.

Edges to Rubies The Complete SketchUp Tutorial


Transformation Matrix WebDialog.

Chapter 15—Animation with the Transformation Matrix

Animation! Here we'll take SketchUp into the fourth dimension: time. We'll put components into motion. You will never again see SketchUp as just a 3D tool.

Uh, oh. The transformation matrix. If you think that sounds complicated, you are right. It is complicated. Fortunately, the SketchUp engineers have programmed all the complicated stuff and given us an API that couldn't be much easier to use. Here I'll give you the basics.

The Transformation Matrix

As you know from our Move/Copy code, a ComponentInstance is a combination of a ComponentDefinition and a Transformation. The Transformation is (in concept, at least) a four-by-four matrix. It stores information that locates the instance in space, that specifies the rotation of the instance around all three axes, as well as the scaling about the three axes.

How does it do all this? See Appendix T. That was originally slated for the beginning of this chapter as the Google docs somewhat unhelpfully state, "Use of the transformation class requires a knowledge of geometrical transformations in 3 dimensions which is covered extensively on the Internet."

Happily, I found that the use of the Transformation class requires no such knowledge. It just requires one little factoid: the r,g,b coordinates of the instance are found at locations 12, 13 and 14 of the matrix. You can go to Appendix T now if you're afraid you might miss something. We'll wait for you.

Circles

martins_sketch_talk.rb has sprouted lots of new functions and classes. I hope you can say the same for your_sketch_talk.rb. Here I'll mention two: the circle function and the Column class. Before we get to them let me mention (but not provide a link!) the download package.

The Tutorial Companion Package includes an updated sketch_talk.rb. The classes are now in sketch_talk_classes.rb. (When your source code file starts to get large, it starts to get inconvenient. Two smaller files can be much handier.) It also includes updated documentation including all the new commands. You can download the new docs without the code. (Extract these files—one HTML and three graphics—to any convenient directory. Open the HTML in any convenient browser.)

I'll provide a link to the full package at the end of this chapter. At this point it would be good to split your own your_sketch_talk.rb into functions and classes. My functions load 'sketch_talk_classes.rb'. In finished code you should require, not load. During development that doesn't work. You improve your classes, and reload your_sketch_talk.rb. It sees require and does not load the improved classes. Make a note in the code to switch later.

The new API you need for a circle is the Entities method add_circle(). Its arguments are:

I've moved the number of sides into first place in my list, as it's first in the manual modeling process. I've made it mandatory in honor of my great uncle, Ebenezar Byte Scrooge.

That normal? It's just like a face's normal. Here's a support function that I've added, which may explain it.

def make_normal( plane_or_normal )

    if plane_or_normal.is_a?( String )
        case plane_or_normal
            when 'rg' then normal = [0,0,1]
            when 'rb' then normal = [0,1,0]
            when 'gb' then normal = [1,0,0]
            else
                raise "Plane must be 'rg', 'rb', 'gb' or a normal vector."
        end
    else
        normal = plane_or_normal
    end
    
    return normal

end # of make_normal()
Got it? Let's skip ahead and use the col() command in SketchUp. It's first four arguments are the circle. The last is the PushPull distance.

Column shaped like Leaning Tower of Pisa.  

SketchTalk command for Leaning Tower of Pisa.

Yes, the tower leans just under four meters. (The Pisans are working on it. It used to lean more than five meters.)

On to more API. The Entities.add_circle() method returns an array of edges: the edges that make up the circle. You need these as adding a circle does not add a face. (Do you think it should? I think it should. Adding coplanar edges that bound a face should add a face, to be consistent.) You have to add the face:

    def draw( ents )
        @group = ents.add_group()
        ents = @group.entities()
        @edges = ents.add_circle( @center, @normal, @radius, @nsides )
        @face = ents.add_face( @edges )
        orient( @face )
        ents.add_face( @edges )
        Sketchup.active_model().selection().add( @group )
        return @group
    end # of draw()

With that, I've already shown you way too much code. Time for you to add a c() function and a Circle class. Good luck! (Hint: my class constructor does nothing except save the inputs into instance variables. The whole class is the constructor and the draw() method.)

Columns

The col() command adds one parameter: the distance to pushpull() after you've drawn your circle. Add your own function and class. (Hint: if your Column's draw() method goes more than three lines, you are working too hard.)

Looking at the Transformation Matrix

The transformation matrix (hereinafter: xform matrix) contains these parts:

RS RS RS U
RS RS RS U
RS RS RS U
Xt Yt Zt Wt

RS Rotation and Scale Matrix
U Unused or application-specific use
(may always be zero)
Xt ... Translation Vector
(Wt may always be one)

Here's a ComponentInstance and its xform matrix in the Ruby Console:

Translated, rotated box.  

Ruby Console view of xform matrix.

The first thing I learned when I began looking at the xform matrix was that the Ruby Console didn't give you a good picture. I wanted a nice picture, so I frogged up a little WebDialog. That is the topic of Chapter 19, so I just give you the code here and invite you to copy it, not type it, into your_sketch_talk.rb. First, the function xf() goes into your SketchTalk command functions.

def xf( *args ) # display selected object's transformation matrix
=begin
No args? Launch dialog showing selected object's transformation matrix

One arg? Arg is an xform matrix. 
Apply it to selection, redraw and display it in dialog.
=end
    if args.length == 0 
        xform( "
        <html>
        <body>
        <table align=left bgcolor=#f0f0ff border=1 cellpadding=3>
            <tr>
                <td id=c0 align=right> </td>
                <td id=c1 align=right> </td>
                <td id=c2 align=right> </td>
                <td id=c3 align=right> </td>
            </tr>
            <tr>
                <td id=c4 align=right> </td>
                <td id=c5 align=right> </td>
                <td id=c6 align=right> </td>
                <td id=c7 align=right> </td>
            </tr>
            <tr>
                <td id=c8 align=right> </td>
                <td id=c9 align=right> </td>
                <td id=c10 align=right> </td>
                <td id=c11 align=right> </td>
            </tr>
            <tr>
                <td id=c12 align=right> </td>
                <td id=c13 align=right> </td>
                <td id=c14 align=right> </td>
                <td id=c15 align=right> </td>
            </tr>

        <script type='text/javascript'>
            function set_vals( vals ) {
            for ( var i = 0; i < 16; i++ ) {

                td = document.getElementById( 'c' + i );
                td.innerHTML = vals[i];
            }
        }
        vals =
        " )
    else # end of display new dialog showing xform matrix
        sel.move!( args[0] )
        draw()
        xf()
    end

end # of xf()
Did you know Ruby supported multi-line strings? Yes, that HTML will be loaded into the WebDialog that pops up.
Topic: sprintf() Going right back to C, reappearing here in Ruby, the printf() function outputs to the Ruby Console and the sprintf() function outputs a string. Both can take arguments that direct the formatting of numbers. If we leave the formatting to the Ruby default, we'll get fractions with 15 decimal places. This is out of hand.

Here we'll use this format specifier: '%6.3f' to specify floating point, 6 places total, 3 places after the decimal point. (Ask Google for additional sprintf() documentation.)

You also need the xform() function, in your support functions section:

def xform( html )

    model = Sketchup.active_model()
    sel = model.selection()
    
    if sel.length() == 0
        puts 'Nothing selected.'
        return
    end

    thing = sel[0]

    if thing.is_a?( Sketchup::ComponentInstance ) ||
            thing.is_a?( Sketchup::Group )
        trans = thing.transformation()
    else
        puts 'model.selection()[0] is not a Group or ComponentInstance.'
        return
    end
    
    # we don't really want stuff like 0.707106781186548
    nums = trans.to_a()
    snums = []
    for i in 0..2 do snums.push( sprintf('%6.3f', nums[i]) ) end
    snums.push( nums[3].to_s() )
    for i in 4..6 do snums.push( sprintf('%6.3f', nums[i]) ) end
    snums.push( nums[7].to_s() )
    for i in 8..10 do snums.push( sprintf('%6.3f', nums[i]) ) end
    snums.push( nums[11].to_s() )
    for i in 12..15 do snums.push( nums[i].to_s() ) end
    
    html += snums.inspect()
    html += "
        set_vals( vals );
    


"
    wd = UI::WebDialog.new( "Transformation Matrix", true, 
			'xform', 350, 200, 0, 0, true )

    wd.set_html( html )

    wd.show()

end # of xform()

With that in place, giving an "xf" command in the Ruby Console gets you this:

Webdialog view of the xform matrix.  

That's a lot easier to read! Well-worth the trouble, I thought.

Note the r,g,b coordinates in the bottom row. Those are the ones we'll need to change to move our ComponentInstance in the model.

Moving!

Here we're going to move, rotate and scale our geometry. And while we do this, we'll show an animated move and rotate, though we'll cheat a bit.

How do you move a ComponentInstance? It's simple. Remember that one little factoid: the r, g, b location is stored in the matrix [12], [13] and [14].

To move r units in the red axis direction, just xform_matrix[12] += r. Before I give you the translate() function, let me mention the use of multiple statements in Ruby (and in many other languages).

You write multiple statements on a single line, separating them with semicolons. This is almost always bad form. I make an exception for multiple statements so closely related that they form almost a single operation. You'll see that here.

def translate( *args ) # add a translation vector to a transformation
=begin
May be called with a transformation and a vector, 
or with a transformation and r, g, b values.
=end
    trans = args[0]
    if args.length == 2
        vec = args[1]
        r = vec[0]; g = vec[1]; b = vec[2] 
    else
        r = args[1]; g = args[2]; b = args[3] 
    end
    arr = trans.to_a()
    arr[12] += r; arr[13] += g; arr[14] += b 
    return Geom::Transformation.new( arr )
    
end # of translate()
Some API notes are in order. There are two methods that give a ComponentInstance a new xform matrix, transform!( xform_matrix ) and move!( xform_matrix ). (Do not mistake move!() with the Move Tool's moves. move!() replaces the transformation, which can move, scale and rotate or do any combination of these changes.) These methods differ in that transform!() puts the changes on the undo stack, while move!() does not. Additionally, it also does not force a redraw, which transform!() does. This is perfect for animation, letting you rotate, scale and translate before you redraw. (Redraw can burn lots of time if the model is large). The move!() method is provided specifically for animation operations. I love move!()!

Actually, I'd love move!() more if it worked. Unfortunately, move!() needs a lot of help before you can really use it for animation. We'll get there in Chapter 16. We're faking it here for translation.

There are numerous ways to create a new xform matrix. The one in the translate() function above takes a 16-element array, letting you fiddle with the xform matrix any way you like. We're using it here to change the location by a vector.

The documentation states that passing a Geom::Vector3d to the Geom::Transformation.new() will translate by the vector. It does if you transform!() but it does not if you move!(). A move!() moves to the point origin+vector (exactly what happens if you call Geom::Transformation.new() with a Geom::Point3d). There is also a Geom::Transformation.translation() method that is the same as creating a transformation matrix with a point argument if you move(). If you want to translate relative to the existing position and you don't want your animation overflowing redo buffers, you have to do translate yourself.

Ready to move a component? Draw something. Make it a component. Select it. Now copy this method (you won't learn much typing it) into your SketchTalk commands:

def m( *args )
    unless ( args.length > 0 ) && ( args[0].is_a?(Sketchup::ComponentInstance) )
        UI.messagebox( "First argument of \"m()\" must be a Sketchup::ComponentInstance.\n" +
            'Use "xm()" to translate by r, g, b values.') 
        return
    end
    
    case args.length
        when 2 then move( args ) # static move
        when 3 then movie( args ) # animated move
        when 1 then xmove( args[0] )
        else UI.messagebox( 'm args must be: ' + 
            '( transformation ) or ( comp, vec ) or ( comp, vec, ntimes ).' )
    end
end # of m()

Yes, the optional third argument is a repeat count. Moving one unit a hundred times gets you to the same place as moving a hundred units, one time. But it's a lot more fun!

Before we go there, add this to your SketchTalk commands:

def draw()
    Sketchup.active_model().active_view().invalidate()
end

Say what? Your model has an active_view() method that returns a reference to the current View. (Much more on View objects in Chapter 16.) If you invalidate() a View, you tell SketchUp that it needs to be redrawn. (Very frustrating: you fiddle with the xform matrix in the Ruby Console. Nothing happens. You try again. Nothing happens. You need to type "draw". Guess how I learned this? Yes, it was the hard way, before there was a "draw" command.)

You now have enough information to write the move() support function. Get back into the typing business with this one:

def move( args ) # static move

    inst = args[0]
    trans = translate( inst.transformation, args[1] )
    inst.move!( trans )
    draw()

end # of move()

Your instance (args[0]) has its xform matrix translated by a move vector (args[1]). The translated xform matrix is reinstalled, without adding an undo stack entry. Last, we tell SketchUp that we'd like to see the result. This is dead simple, once you know how.

Install that much and convince yourself that you can move a component instance via a SketchTalk command.

Now, on to the movie() support function. First, some new material.

Topic: Global Variables

Global variables are generally a bad thing. They can be changed from anywhere in the code. If you have your own code this is not a big issue. If you are working on a large project with a programming team, this is a big issue.

Use gobal variables sparingly. It's usually OK to make a single assignment to a global variable where you create it. This is what we'll be doing. It's usually a design error if you'll be updating your global from different modules. Find a way to avoid this.

In Ruby, global variables are created with the "$" prefix: $global_variable. This was chosen for its ugliness, in hopes that global variables would be avoided.
Add this at the top of your file:

$your_sketch_talk_fps = 24

(Use your actual name, not "your".) If you have global variables, always use your plugin name as a prefix. Otherwise name conflicts are likely. "fps" is a common abbreviation of "frames per second." $fps would be a horrible choice.

$your_sketch_talk_fps is your "frames per second" specification. When you start making movies, you'll want to fiddle with this. 24 is the rate at which movies are filmed—the rate at which they don't look choppy.

To do this animation we'll use UI.start_timer() and UI.stop_timer(). The timer delays a specified amount before executing an attached block of code. A boolean, if true, tells the timer to repeat until the stop_timer() method is called.

This is the movie() function.

def movie( args ) # animated move

    inst = args[0]
    vec = args[1]
    ntimes = args[2]
    
    id = UI.start_timer( 1.0/$sketch_talk_fps, true ) {
        move( [inst, vec] )
        ntimes -= 1
        UI.stop_timer( id ) if ntimes == 0
    }

end # of movie()

Topic: Threads

Your computer, assuming it's a single-processor model, appears to do multiple things at once courtesy of threads of execution. A single processor runs some code from one thread, then runs some code from another thread, then another, and so on. (At 2GHz, 2 million clock cycles tick by every millisecond. You can get a lot of work done in 2 million clock cycles.) Some operating sytems use the word "process" to define a big-deal operation (your browser, for example) and use "thread" to define smaller portions of a process, such as downloading a web page.

SketchUp runs on multiple threads. One thread is responsible for looking at the view and redrawing it, when necessary. Other threads handle your inputs and run your Ruby code.

A dual-core computer can, in fact, do two things at once. SketchUp cannot, as yet, take advantage of this power as it runs in just one core.

I should mention that the timer is vital to this animation but it doesn't actually work. If you replaced it with a loop, it's likely that your loop would run to completion before the SketchUp thread that checks for redraws got its turn to execute. The timer is smart enough to wait on the redraw thread. Otherwise, it rounds the frame rate down to the nearest integer—zero in our movie() code.

We'll replace the timer in Chapter 16. For now, it's enough to let you see things in motion. Add the movie() support function and try it out. Have some fun with it.

Here's some code that looks like it gets a biplane, taxis down the runway and takes off into the wild blue yonder. It doesn't work.
# flying_failure.rb
bp = i '/models/biplane'
for i in (1..10) do m bp, [i,0,0], 100 # taxi for forty seconds
for i in (11..100) do m bp, [10, 0, 2], 100 # take off and fly!

This was one of the first things I tried after moving a box around the screen got old. Total failure.

Why? The thread that's running your Ruby has its millisecond. It runs both those move loops to completion. You've stacked up a hundred timers. They all run at the same time, not in sequence. Your poor biplane jumps like a cat on a hot tin roof.

Rotating!

Ready for the Qrotate Tool in SketchTalk? Here's what you need to know about the API. There is a rotation() method of the Geom::Transformation class. It creates an xform matrix that, when handed to a ComponentInstance.move!() (or transform!()) method will rotate the ComponentInstance around any point and axis you specify. It happily applies additional rotations to objects that are already rotated.

It's arguments are:

I want SketchTalk's q() command to take these arguments: On to the code. The q() command documents the arguments but leaves all the work to the qrotate() support function:

def q( *args )
=begin
Qrotate component around axis set by pt plus plane_or_normal, angle degrees.

Arguments:
"comp" is any ComponentInstance. 
"pt" is an [r,g,b] array. 
"plane_or_normal" is "rg", "gb", "rb" or another [r,g,b] array. 
"ntimes" is an optional repeat specification
=end

qrotate( *args )

end # of q()
Add another global variable, again one that is defined once and never changed: $radians_per_degree = Math::PI / 180.0. Note that here I don't use a plugin-name suffix, believing that anyone else who created a global named $radians_per_degree would have exactly what I have.

This is the code that does the actual work:

def qrotate( *args ) # ( comp, pt, plane_or_normal, angle[, ntimes] )
=begin
Qrotate component around axis set by pt plus plane_or_normal, by angle degrees.

"comp" is any ComponentInstance. 
"pt" is an [r,g,b] array. 
"plane_or_normal" is "rg", "gb", "rb" or another [r,g,b] array. 
=end

    if args.length == 5
        ntimes = args.pop()
        id = UI.start_timer( 1.0/$sketch_talk_fps, true ) {
            qrotate( args[0], args[1], args[2], args[3] )
            ntimes -= 1
            UI.stop_timer( id ) if ntimes == 0
        }
        return
    end
    
    comp = args[0]

    unless comp.is_a?( Sketchup::ComponentInstance )
        UI.messagebox( 
            'Qrotate: first argument must be a Sketchup::ComponentInstance' )
        return
    end

    normal = make_normal( args[2] )
    angle = args[3] * $radians_per_degree
    
    trans = Geom::Transformation.rotation( args[1], normal, angle )
    comp.transform!( trans )
    draw()

end # of qrotate()

This is actually two functions in one. The top part, five-argument version, starts a timer that calls the bottom, four-argument function. This is a bad practice, but I don't care as this is a throwaway that gets replaced in the next chapter.

Enter this support function and you can start an animated rotation. A clock would be a good project at this point. (But due to the timer's limitations, don't expect to tell time with it.)

Due to the same reason that you can't run consecutive moves, you can move and rotate together. Try a program like this:

# /r2/t.rb

return unless Sketchup.active_model.selection[0].is_a?( Sketchup::ComponentInstance )

m sel, [0,1,0], 180
q sel, o, 'rb', 1, 180
Have some fun with this!

Scaling!

No, I'm not going to put some throwaway animation code into scaling, although you certainly can if you want. And I should tell you that the SketchTalk scaling command has lots more power than the Scale Tool. So much more, in fact, that you're going to need to practice with it to see how much can be done.

Scaling is very much like rotating. You use Geom::Transformation.scaling() to get a new xform matrix that applies scaling. You use your instance's move!() or transform!() to apply this xform matrix's magic to existing rotations and scalings. The only difference is that this time we have four distinct scaling choices.

Compare this to the Scale Tool. Push or pull a handle and you scale about the opposite handle. Add the Ctrl key (Option on Mac) and you scale about the center. That's a choice of two points for each handle. The Ruby API and SketchTalk give you a choice of infinite points. You'll want to experiment and keep asking for xf to view your xform matrix.

This all works as a SketchTalk command, with no support function:

def s( *args )
    case args.length
        when 1 then
            t = Geom::Transformation.scaling( args[0] )
        when 2 then
            t = Geom::Transformation.scaling( args[0], args[1] )
        when 3 then
            t = Geom::Transformation.scaling( args[0], args[1], args[2] )
        when 4 then
            t = Geom::Transformation.scaling( args[0], args[1], args[2], args[3] )
        else
            UI.messagebox( "Scale arg(s) must be one of:\n",
            "\tglobal_scale (about origin)\n",
            "\tpoint, glogal_scale\n",
            "\trscale, gscale, bscale (about origin)\n",
            "\tpoint, rscale, gscale, bscale." )
            return
    end # of cases

    sel().transform!( t )
    draw()
end

(Note that this code depends on, and doesn't bother checking, your having selected a ComponentInstance.) Enter this command and start experimenting. Move your component away from the origin. Begin with the single-argument case. If your component was at [20,20,0] an s 2 will move your component to [40,40,0], in addition to doubling its size. s [20,20,0], 2 would give you a doubling of size without moving the component.

Use the xf command a lot. Do you see the last matrix element go to 0.5 when you s 2? Try it! Maybe you want to read Appendix T after all. That's the effect of a "homogeneous vector" (which is easier to understand than to pronounce).

Poking around in SketchTalk, it is OK to use the global scale factor (one and two argument forms). In your code this is not OK. When the global scale factor is other than one, as I write this (3/1/10, version 7.1) there are bugs in SketchUp that surface when the global scale factor is other than 1. Don't scale by s 2; scale by s 2,2,2. The latter looks less efficient, but it avoids the bugs.

Congratulations. You've got SketchTalk moving, rotating and scaling geometry and you've done your first animations. In the next chapter we'll get serious about our frame rate.

If you feel you need a look at my code, download the Tutorial Companion Package for this chapter. Extract to any convenient folder. To load my latest, load '/your/folder/martins_sketch_talk2.rb'. For documentation, open /your/folder/sketch_talk.html in your favorite browser.


Defining synonum classes for easy typing. View of apartment contents. Biplane, star of Airshow!.