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.
Chapter 14—SketchTalk Objects
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.
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?
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.
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
GTcan do whatever a
Geom::Transformationcan do, and nothing else. A
P3dhas whatever data the
Geom::Point3dhas, 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:
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.
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:
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
In the console, try
Rectangle.new( [0,0,0], [20,20,0] ). You should get the cheery report at the left.
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.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.)
In Ruby, instance variables are prefixed with an "@" sign. For our Rectangle class, we'll store
@far. These variables are private. They are only accessible within the class. If you want access to
@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
@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.
:nearis 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
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
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 = @near ret = @far if ( @plane == 'rg' ) || ( @plane == 'no' ) ret = [ @ar[r], @ar[g], @ar[b] ] ret = elsif @plane == 'gb' ret = ret = else # @plane == 'rb' ret = ret = end return ret end # get_corners() end
retshould be copied into the other five corner assignments. Then it's up to you to convert each
@ar[x]by adding "ne" or "f".
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.
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
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:
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,
model.selection is a reference to the currently selected entities. Selection is also one of the collection classes.
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 end corners = get_corners() @face = @group.entities().add_face( corners, corners, corners, corners ) @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.
# 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
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.
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()
In your Ruby Console, create your first, do-nothing donut.
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
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
Test this in the Ruby Console.
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.
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 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
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 far2[i] = arr 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 )
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()
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
Drawingelementclass, which defines the
Drawingelementis one of the Entity classes.
# /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
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.
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
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()
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.
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
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:
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
textFile = File.new( "pathname", "r" ) arr = textFile.readlines() textFile.close()
Assuming your file is a text file,
arr will hold the first line;
arr 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()
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.)
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!