The classic Craftsman bungalow has a gable-ended dormer facing the street. This one has a shed dormer. The Craftsman ideal was for designer, builder and owner to be the same person. Bungalows were often designed by amateurs, not architects. They show lots of variety.

The tapered wood columns (resting on masonry) are a very popular Craftsman detail.

Florida, NY, c. 1910.

Classic Craftsman Bungalow

Edges to Rubies The Complete SketchUp Tutorial


Defining synonym classes for easy typing.

Chapter 14—SketchTalk Objects

From zero to object-oriented programming in just four chapters? Yes, that's exactly what we're doing. But before we get on to OOP, let's take a quick look at structured programming.

Coders: If you understand structured programming and class-based OOP (C++, Java, Python, Visual Basic) follow this link to inheritance where we'll create a pair of convenience classes.

Structured Programming

In the bad old days (the 1960s) one used the "goto" statement to tell the computer where to resume executing statements.

Structured Unstructured
if nice_weather()
    go_cycling()
else
    write_rubies()
end
		
10 if nice_weather() goto 40
20 write_rubies()
30 goto 50
40 go_cycling()
50 ...
		

There are, fortunately, almost no unstructured programming languages. Basic and Fortran became structured in the last century. In some languages you will meet a vestigial "goto" capability. If you meet a statement that directs processing to a specific program location, immediately forget it. That is never necessary; that is always a source for mystery bugs.

Introduction to Object-Oriented Programming

There are various object-oriented paradigms. Here we'll be talking about the class-based objects found in C++, Java, Python and Ruby. (JavaScript, for example, has very different objects.) You've already been using objects as the Ruby API treats the whole SketchUp model as a set of objects.

What is an object? It's a little data structure.

How do I make an object? Commonly, you call the class's constructor function. It returns a reference to the little data structure it creates.

Is a reference an address? Possibly. There are many ways to implement a reference. For example, a reference could be an index into a table of addresses. You don't know and don't need to know.

Who defines the data structure? The class. When you program the class you tell it what data items you need and what names you will use to refer to them.

What is a method? A function. The methods are part of the class.

How do I call these functions? my_object.method()

How do I define these functions? You'll see. It's pretty much like defining other functions, except that they're defined inside the class.

What ... ? Hold it! No more questions. Let's do it.

Inheritance (and Typing)

Inheritance is a way of making a simple concept sound complicated. You say that one class "inherits" from another. I will not say that again. You have classes that extend and classes that are extended. The extending class starts with the extended class and adds new data points and/or new methods. (Or maybe just helps with typing in the Ruby Console.) The Ruby syntax is:

class classname [< class that is being extended]
    # new methods and data go here
end

Add these two useful classes to your SketchTalk:

# Classes -------------------------------------------------------

class GT < Geom::Transformation
end

class P3d < Geom::Point3d
end
Note the new additions: there are none. A GT can do whatever a Geom::Transformation can do, and nothing else. A P3d has whatever data the Geom::Point3d has, and nothing else.

"Hey, Mr. Tutor. You said 'do this'. I did it but nothing happens. What's wrong?" The solution: lst. That's "t", as in Load SketchTalk. Every time you add even a line to your SketchTalk, you have to "lst" to get the new version in your Ruby Console.

What's the point? In the Ruby Console you can now type GT.new anyplace where you typed, in the last chapter, Geom::Transformation.new. Life is good when you know a little OOP!

Note that using a P3d is slightly less efficient than using a Geom::Point3d. When a method is called, the extending class is searched first for that method. If not found, then the extended class is searched for the method. (This could go on in a chain if the extended class itself extended another class.) Although I doubt it takes very long to search an empty class, you should use these in the Ruby console, not in your code.

Two small points before we start writing the SketchTalk classes: class names begin with a capital letter, and every class you write extends Object (or it extends a class that extends Object, or it extends a class that extends a class that extends Object, and so on—sooner or later you get back to Object). If your class extends nothing, class Foo, the implication is class Foo < Object. Before we get started, let me show you a little Ruby card trick:

Ruby Console output.  

An Object instance calls Object.inspect() to report on itself in the console. (Or it should. This might also be Object.to_s(). Both are the same.) An object has an id, reported by Object.id(). This is an integer. If you double the id and convert it to a hexadecimal string (base 16), you get the hex number used by the object to report on itself. Remember this and you may win a Trivial Pursuit prize at a geek convention.

The Rectangle Class

Now on to the business at hand. If we had a set of coplanar (all in one plane) points, we could easily create a face:

face = entities.add_face( pt1, pt2, ... )

The points could be Geom::Point3d objects, or they could be 3-valued arrays. Use your Ruby Console to create a face:

Adding a face with the Ruby Console.

Hey, we've already got what we need. No more code required, right? Actually, not quite. I want a function r( near, far ) that creates a rectangle with the SketchTalk equivalent of just clicking two points.

My face above was an easy one. The blue axis values are all zero. If all the points on one axis are the same, you are guaranteed to have coplanar points. Now challenge yourself to add a coplanar triangle where none of the points are the same.

Get it? Actually, that was a trick. Any three points define a plane, so they are automatically coplanar. Try again, no trick, with a non-orthogonal rectangle. (Remember that banister, way back in Chapter 2?)

It turns out that the Rectangle class code is not as simple as you might think. Let's get started. Begin by learning some more Ruby. In the console, try [1,2,3].to_s() and then [1,2,3].inspect(). Not the same! Now a little more Ruby. The name of the constructor method is initialize. (Why doesn't Foo.new() call a method named new()? If you find out, please let me know.) Here's a good way to begin your class:

# SketchTalk Classes --------------------------------------------

class Rectangle

    def initialize( near, far )
        UI.messagebox( near.inspect() + ', ' + far.inspect() )
    end

end

Calling the constructor.  

In the console, try Rectangle.new( [0,0,0], [20,20,0] ). You should get the cheery report at the left.

Topic: Instance Variables

My first rectangle attempt used functions. It failed in practice. As I created rectangles while building things, I regularly wanted to know the near and far points that I'd used to create the rectangles. These cried out to become instance variables.

An instance variable is a data value stored with each "instance" of the class. When you say rec = r( [near], [far] ) you want to record, those values in the instance. Later you can refer to rec.near and rec.far. Instance variables record the "properties" of the object instance.

To maintain consistency, the instance variables may be accessed through functions commonly known as "getters" and "setters" (more pretentious: "accessors"). Our Rectangle constructor records the near and far points and then looks for the orthogonal plane, which is one of "rg", "rb", "gb" or "no" (Not Orthogonal). Once that is determined, you don't want the corners changed. We'll provide getters for the corners, but not setters. (You could, if you really wanted to, have corner setters that reran the "what plane is this?" logic. I can't figure out why that would be useful, though.)

Coders: Ruby's getters and setters are unique to Ruby. Read on.

In Ruby, instance variables are prefixed with an "@" sign. For our Rectangle class, we'll store @near and @far. These variables are private. They are only accessible within the class. If you want access to @near and @far you have to provide getters:

    def near()
        return @near
    end
    def far()
        return @far
    end

If you wanted setters (we don't for the corners but we will for other things) you would write these:

    def near=(val)
        @near = val
    end
    def far=(val)
        @far = val
    end

Writing getters and setters is tedious, brainless work. Ruby provides a shortcut. You can start a class with:

class Rectangle
    attr_reader :near, :far
That automatically generates the getter methods for @near and @far. If you want setters you use attr_writer. If you want both you use attr_accessor. (The ":" prefix to a name creates a "symbol" in Ruby. Everything in Ruby is an object, including names. :near is a "name 'near'" object.)

If you want data that is stored in the class, not in instances, prefix it with two "@" signs: @@class_variable. (We'll put class variables to use in Chapter 15.)

Let's put all this new knowledge to use in a real Rectangle constructor:

class Rectangle

    attr_reader :near, :far, :plane, :face, :group
    
    def initialize( near, far )
        
        r = 0; g = 1; b = 2;
        @near = near; @far = far
        
        if near[r] == far[r]
            @plane = 'gb'
        elsif near[g] == far[g]
            @plane = 'rb'
        elsif near[b] == far[b]
            @plane = 'rg'
        else
            @plane = 'no'
        end
        UI.messagebox( 'The plane is ' + @plane ) 
        
    end # of initialize()
end

Test all four possibilities. When they all work, delete the messagebox line.

I'm cheating here. I gave you the full list of getters, which jumps ahead a bit. You've seen @plane in the code. We'll have a draw() method that actually does the modeling. That will create the model's face. We'll store a reference to it in @face. And it will create it in a new group, reference to which we store in @group.

I also cheated by leaving out a comment. To write good code, fold your arms. Think. Write down what you are doing. Here are my thoughts. (Copy them. You won't learn anything retyping block comments.)

class Rectangle
=begin
A Rectangle is created from a near corner and a far corner. 
The corners are [r,g,b] arrays. 

An orthogonal rectangle is created, if possible. 
The rectangle's @plane is one of 'rg', 'gb', 'br' or 'no' (Not Orthogonal). 

An orthogonal Rectangle's normal points in the positive direction of the
perpendicular axis. 

A non-orthogonal Rectangle's outside face will face the positive end 
of the blue axis.
=end
    attr_reader :near, :far, :plane, :face, :group

Before we can actually create the rectangle, we need to convert the near and far corners into an array of four corners. In this array, the first corner is the near corner and the third corner is the far corner. It's the second and fourth that take some thought.

Here's some code to add to your class:

    end # of initialize()

    def get_corners()

        r = 0; g = 1; b = 2;
        ret = []
        ret[0] = @near
        ret[2] = @far
        if ( @plane == 'rg' ) || ( @plane == 'no' )
            ret[1] = [ @ar[r], @ar[g], @ar[b] ]
            ret[3] = 
        elsif @plane == 'gb'
            ret[1] = 
            ret[3] = 
        else # @plane == 'rb'
            ret[1] = 
            ret[3] = 
        end

        return ret

    end # get_corners()

end
That first assignment to ret[1] should be copied into the other five corner assignments. Then it's up to you to convert each @ar[x] by adding "ne" or "f".

Cube drawn to trace around corners.  

It will help if you turn to SketchUp, draw a rectangle in the rg plane and PushPull it up the blue axis. Work counterclockwise around one face at a time.


Ruby Console tests and results  

Here are test rectangles and the results of their get_corners() methods. (Aside: it's fine to type Foo.new().do_something() into the Ruby console, but don't do this in a program. It will work, but two lines are much more readable: f = Foo.new(), then f.do_something().)

Once you get all four cases right (three orthogonal plus 'no'), you are ready to draw. We will draw into model.entities by default. We may want to draw into an existing group. (The donut will draw a rectangle, and then, in the rectangle's group, draw a smaller rectangle, the hole, and delete the donut hole's face.)

Some bits of API:

In Model materials.  

model.materials is a reference to the materials in the model (the ones you see in the "In Model" group in the Materials window). Materials is one of the Collection classes. model.materials.current is the selected material or, if none is selected, nil.

model.selection is a reference to the currently selected entities. Selection is also one of the collection classes.

The face.reverse!() method does just what it promises: swap inside and outside faces. Why you need to reverse unless ( @plane == 'rb' ) is a mystery to me, but it works. The non-orthogonal part of that unless ensures that the outside faces the positive end of the blue axis. This is the code for draw(). It completes the Rectangle class.

    end # of initialize()

    def draw( *args ) # draw( [group] )

        model=Sketchup.active_model()
    
        if args.length == 0
            ents = model.entities()
            @group = ents.add_group()
        else
            @group = args[0]
        end 
        
        corners = get_corners()
        @face = @group.entities().add_face( 
            corners[0], corners[1], corners[2], corners[3] )
        @face.reverse!() unless ( @plane == 'rb' ) || 
            ( (@plane == 'no') && (@face.normal.z > 0) )
        m = model.materials.current
        if m
            @face.material=m
            @face.back_material=m
        end
        
        model.selection.add( @group )
        
    end # of draw()
    
    def get_corners()

Ready to test this out? Add these two methods to your SketchTalk if you didn't add them at the end of Chapter 13.


# this function goes in the SketchTalk Functions section

def r( near, far )
    return rectangle( near, far )
end # of r()

# this function goes in the Support Functions section

def rectangle( near, far )
=begin
Draw a rectangular face from the "near" corner to the "far" corner.

Note: An infinite number of rectangles are possible between any two points.
This draws an orthogonal rectangle, if possible. 
See #rg plane code for non-orthogonal rectangles.
=end

	r = Rectangle.new( near, far )
	r.draw()
	return r

end # rectangle()

Back to your Ruby Console. Try r( [0,0,0], [20,20,0] ). (Try just r. If it's not assigned to anything, you can skip the parentheses.) You should get your rectangle in the model, enclosed neatly in its own group. Try r( [0,0,0],[0,20,20] ). You can't do that with the mouse! Try r( [0,0,0],[20,0,20] ).

How many times have you Rectangled, PushPulled, grouped, just to get a drawing surface? History! SketchTalk makes this dead simple. The Rectangle class also makes Boxes dead simple.

The Box Class

Before I wrote the code I thought that Rectangles would be quite simple, Boxes more complex. Talk about wrong! Here's the Box class in its entirety:

# SketchTalk Classes --------------------------------------------

class Box
=begin
A box is a rectangular parallelepiped: six faces, all rectangular, all
meeting at right angles. A box is defined by near and far corners of a
rectangle plus a distance to PushPull the rectangle, creating the box 
shape. If the rectangle is orthogonal, a positive PushPull is done 
toward the positive end of the perpendicular axis. All six faces of the 
box will face the outside of the box.
=end
    attr_reader :rectangle, :pushpull_distance

    def initialize( near, far, pushpull_distance )     
        @rectangle = Rectangle.new( near, far )
        @pushpull_distance = pushpull_distance
    end
    
    def draw
        @rectangle.draw()
        @rectangle.face().pushpull( pushpull_distance )
    end 
    
end # of class Box

class Rectangle
I suggest you type in the code, copy in the block comment. Add a box() function (call the constructor, call the draw() method, return a reference) to the SketchTalk Functions and then have some fun modeling boxes. Enjoy the fact that you know absolutely which way the box will be pushpulled.

The Donut Class

I discovered Krispy Kremes in Las Vegas. Yummy! I discovered that they were extremely messy eating out of a bag while driving. What a mess. "Ambivalent? Well, yes and no." (Jimmy Buffett). Our donuts will not be yummy, but they will be very neat. Let's begin with the constructor and a stub draw() method.

class Doughnut
=begin
By hand: drag a Rectangle from near to far. oFFset inside the rectangle. 
Delete the face in the center. PushPull the remaining face.
=end
    attr_reader :rectangle, :pushpull_distance, :thickness

    def initialize( near, far, pushpull_distance, thickness )
=begin
Parameters near and far are [r,g,b] arrays. 
The thickness is the distance from the outer rectangle to the inner 
(hole) one.
=end    
        
        @rectangle = Rectangle.new( near, far )
        @pushpull_distance = pushpull_distance
        @thickness = thickness
        
    end # of initialize()

end # of class Doughnut

Drop a draw function into your SketchUp Functions area and you are ready to test.

def donut( near, far, pushpull, thickness )
    d = Doughnut.new( near, far, pushpull, thickness )
    d.draw()
    return d
end # of donut()

Creating our first donut.  

In your Ruby Console, create your first, do-nothing donut.

The default inspect() (found in the Object class) lists all instance variables and their values. If an instance variable is itself an Object (such as our @rectangle) that will be inspect()ed, too. This is often good enough. If it's not good enough, write an inspect() method. Now let's get to work on that draw() method.

If you were modeling, the first thing you would do would be to draw your outer rectangle. We'll start that way here, too.

    def draw()
        @rectangle.draw()
    end # of draw()

That was simple enough. Try it out.

Now the bad news. Drawing the inner rectangle is not so simple. To begin, we've called our end points "near" and "far." This is fine if you need two sensible names, but is not fine otherwise. Is "near" closer to the origin than "far"? Maybe. Consider a rectangle between [-40,-40,0], [40,40,0]. Neither is nearer. Worse, consider [40,-40,0], [-40,40,0]. "Near" and "far" are nice names, not mathematical truths.

I begin with a simple method that takes two values, adds thickness to the smaller and subtracts from the larger. We'll call this successively with two red values, greeen values and then blue values. It's simple.

    def ins( v1, v2 )
        if v1 < v2
            return [ v1+@thickness, v2-@thickness ] 
        else
            return [ v2+@thickness, v1-@thickness ]
        end 
    end # ins()

end # of class Doughnut

Ruby Console tests of the ins() method.  

Test this in the Ruby Console.

Topic: Private Methods

Some methods are written for the benefit of the class's consumers. The constructor is one. You want outsiders to be able to Rectangle.new(), for example. Most methods in most classes are written so that the objects can go about their own business in their own way. Object-Oriented enthusiasts call this "encapsulation."

Ideally, you can entirely rewrite a class and the programs that use that class will never know, provided your public methods function the same way. Right now, you are your classes' only customer, so this doesn't really matter. A key OOP goal, however, is code reuse—the more people that use your code, the more value you've created.

Coder: Ruby has an elegantly simple way of separating public and private methods. Read on.

You just drop the word "private" between public and private methods.

class Doughnut
    def initialize()
        ...
    end
    def draw()
        ...
    end
    
    private
    
    def ins()
        ...
    end
end # of class Doughnut

After many years of C++ and Java I thought I'd miss my "protected" methods. I don't.

You can also declare a method private, inelegantly, by writing after the method appears private :ins. Do you think that a big hug from Monty could squeeze some of this Tim Toady out of Ruby?

On to the inset() method. It uses our first Ruby switch. New to programming? This does what it looks like it does. Experienced coder? This does what it looks like it does:

            case @rectangle.plane()
                when 'rg': i = 2
                when 'rb': i = 1
                when 'gb': i = 0
            end

Why are we doing this? Our points are three-item arrays. We want to adjust blue if we're an orthogonal rectangle in the rg plane, and so on. near[i] will be near[2] if our rectangle's @plane (accessed from outside the class via the plane() getter method) is 'rg'.

With all that by way of explanation, here is the complete inset() method.

    end # of draw()

    def inset( near, far, thickness )
=begin
        Computes new near/far pair, oFFset inside by thickness.
        This is messy as there is no guarantee that the coordinates
        of "near" are smaller than the coordinates of "far".
=end
        near2 = []
        far2 = []
        
        # First, assign smaller+thickness to near2, larger-thickness 
        # to far2.
        for i in 0..2
            arr = ins( near[i], far[i] )
            near2[i] = arr[0]
            far2[i] = arr[1]
        end

        # The coordinates of the perpendicular value V are now V+thickness 
        # and V-thickness. ([0,0,0],[100,100,0] is now [8,8,8], [92,92,-8]).
        # Adjust them back to V and V.
        unless @plane == 'no'
            case @rectangle.plane()
                when 'rg': i = 2
                when 'rb': i = 1
                when 'gb': i = 0
            end
            
            near2[i] = far2[i] = near[i] 
        end # adjust orthogonal planes

        rec = Rectangle.new( near2, far2 )
        return rec
        
    end # of inset()
    
    def ins( v1, v2 )

Testing inset() in the Ruby Console.  

You can test inset in the Ruby Console. Here it insets [0,0,0],[20,20,0] to [8,8,0],[12,12,0] (thickness is 8). Ready to rock and roll?

All that's left now is to complete the draw() method. The inset() method did all the hard work. We're up to the fun part. The only new thing is that we're going to draw the inset rectangle in the outer rectangle's group. If you grumbled to yourself about the added complication when you did the Rectangle class, you see now that it was worth it.

    def draw()
        @rectangle.draw()
        inner_rec = inset( @rectangle.near, @rectangle.far, @thickness )
        inner_rec.draw( @rectangle.group )
        inner_rec.face().erase!()
        @rectangle.face().pushpull( @pushpull_distance )
    end # of draw()
I didn't explain erase!(). Anyone smart enough to be here, in the middle of a SketchUp Ruby tutorial, is smart enough to figure it out. One little mention is in order. All the entities that get drawn (such as faces, edges, construction lines) extend the Drawingelement class, which defines the erase!() method. Drawingelement is one of the Entity classes.

Three donuts.  

# /r/krispy_kreme.rb

require '/r/sketch_talk'
n
donut [0,0,0], [30,30,0], -10, 8
donut [0,0,0], [30,0,30], -10, 8
donut [0,0,0], [0,30,30], -10, 8
Donuts are fun!

Basement walls and footings are two donuts.  

I want basement walls for a 20' by 30' house. Make them 8' deep plus 3" extra so we can pour a concrete floor. 8" is a good thickness. The walls should stand on footings as deep as the walls are thick and twice as wide.

donut [-4,-4,-(8*12)-3], [20*12+4,30*12+4,-(8*12)-3],-8,16 donut [0,0,-(8*12)-3], [20*12, 30*12,-(8*12)-3], 8*12+3,8

Donuts are serious.

Go reload your SketchTalk and give a donut command. Have a little fun.

A SketchTalk entertainment cabinet.  

Then write a program that builds an entertainment cabinet for your livingroom. One donut will create top, base and end walls. A box will create the back. A none (see below) will clear the selections. A box will create a shelf. A g and mc will give you as many shelves as you want. Another none will clear the selections. A donut will create a door frame. An r will put glass in the frame. A g will create a component from frame and glass. Another mc will create as many doors as you need. Dimensions are up to you.

How many tries do you need to get the whole cabinet program right? If you get it in less than five tries, you're better than me.

I added a SketchTalk Function none (the opposite of all) to do this one. SketchTalk has everything you need when you know how to drop in whatever you need whenever you need it!

def none()
    Sketchup.active_model().selection().clear()
end # of none()

Bug Splat City

For my first attempt, I selected "Wood_Cherry_Original" planning to manually Bucket the glass in the door component. The cabinet looked great, but it was seriously sick. Bucket the glass and most of the components turn to glass. Bucket again and Bug Splat! I was up to my sixth Bug Splat! when I gave up and used SketchUp default material. As everything is a group or component, it's easy to Bucket wood and glass after it's built. I'm thinking about taking that "use the current material" logic out of the Rectangle class. One more topic: File I/O.

Reading and Writing Files in Ruby

Actually, we've been reading and writing models which, if you choose, can be sent to disk with a File/Save and retrieved with a File/Open. This short section is about files you create the old-fashioned way.

I had hoped to add a logging feature to SketchTalk. Have it write the commands we type into a disk file. Many times I've found my Sketch Talking, with a wee bit if editing, would make a useful little program. Hey! Why type it into a disk file after you've typed it into the Ruby Console?

Actually, the answer to that rhetorical question is that you have to. Consider mc step, [0,9,7], 15. What is step? It's a reference to a component instance, right now it's #<Sketchup::ComponentInstance:0x551f3e8>, not a string. There is no way to retrieve the text from the Ruby Console. Darn. Here's the file I/O, regardless.

File I/O in Ruby couldn't be much simpler. You open or create a file this way:

someFile = File.new( "pathname", "mode" )
    # ... code here processes the file
someFile.close()

The mode is one of these six:

Mode Pointer Read? Write? Empties? Creates?
r 0 Yes No No No
r+ 0 Yes Yes No No
w 0 No Yes Yes Yes
w+ 0 Yes Yes Yes Yes
a EOF No Yes No Yes
a+ EOF Yes Yes No Yes

The "r" and "r+" modes don't create a file if it doesn't exist. (Quite sensibly. How can you open a file for reading if it doesn't exist?) The "w" and "w+" modes erase any existing content. The "a" and "a+" modes open your file with the pointer (the pointer points to the spot where you will next read or write) at the end of the file, so a write appends new lines.

For a logging application, you need only "a" to write new log entries. Your text editor will be the tool that reads and writes the file. If you have need to read text files, the easy way is with the File.readlines() method:

textFile = File.new( "pathname", "r" )
    arr = textFile.readlines()
textFile.close()

Assuming your file is a text file, arr[0] will hold the first line; arr[1] will hold the second line, etc.

Writing a log file is best done one line at a time. (Note: your OS will buffer these writes. For applications more critical than logging, closing the file after each write will probably—not guaranteed, but probably—force the file writes to disk.)

$unique_name_logFile = File.new( "pathname", "a" )
   $unique_name_logFile.puts( log_line )
   #...
   $unique_name_logFile.puts( log_line ) # repeat as needed
   #...
$unique_name_logFile.close()

Helpful File methods that can be used in a console include File.exist?( pathname ) and File.file?( pathname ). The former returns true if the given pathname exists. The latter returns true if the pathname exists and is not a directory.

Ruby has a rich, Tim-Toady-pleasing, collection of ways to manipulate text files. It has almost no options for binary files, and no binary capabilities outside of Windows. (Monty, my pet Python, is reading this over my shoulder. At the moment, he's looking quite smug.)

EACCES: Permission denied

You are just trying to write a file. You are on Windows, which barely has permissions. What's happening?

Drop the first "E" and see if the error name makes sense. "ACCES" with another "S" would spell "access". I'll bet you are trying to write to a directory that doesn't exist. I was.

That logging feature would be useful. Hmmm. I wonder what's involved in writing a Sketch Talk console? Hmmm.

I want to congratulate you for getting here. You've got genuine Ruby classes working together. You've come a long way from just adding typing-saving synonyms. OOP is easier to do than to explain.

Except for the Move/Copy code, we've created static geometry this far. In Chapter 15 we'll make our geometry sing and dance!


Defining synonym classes for easy typing. View of apartment contents. The Transformation Matrix.