Animating SketchUp Models with Ruby
Brad J. Cox, Ph.D.
Binary Consulting; Bethesda MD.
bcox@virtualschool.edu
May 19, 2006
 
 
SketchUp’s embedded Ruby is quite different from using Ruby in the normal fashion.  I'll get several workarounds out of the way before proceeding with the core of this tutorial.
Sketchup-Ruby Peculiarities
SketchUp configures Ruby's load path ($:) to load scripts only from ~/Library/Application Support/SketchUp 5/Plugins. Vim doesn't handle the spaces in that pathname which was a deal-breaker for me. To get around this, I created a symbolic link in the Plugins directory to redirect SketchUp to NagumoLoader.rb in my Eclipse workspace, which is is a Subversion working copy at ~/Projects/workspace/NagumoSim on my system. To set this up:
cd ~/Library/Application Support/SketchUp 5/Plugins
in -s ~/Projects/workspace/NagumoSim/NagumoLoader.rb NagumoLoader.rb
 
SketchUp will now discover NagumoLoader.rb in its plugin directory and load it automatically whenever it starts.
The next obstacle is that SketchUp provides its own version of Ruby which is quite different from the version I use externally. In particular, ruby gems doesn't work so none of your gems are accessible:
> require 'rubygems'
Error: #<RuntimeError: /usr/local/lib/ruby/1.8/powerpc-darwin8.6.0/rbconfig.rb:7: ruby lib version (1.8.4) doesn't match executable version (1.8.0)>
/usr/local/lib/ruby/1.8/powerpc-darwin8.6.0/rbconfig.rb:7
 
Finally, the embedded ruby is configured to load scripts only from the plugins directory. Therefore no classes are available except the Ruby built ins. NagumoLoader.rb works around this by extending the ruby load path. The first group of $: (load path) assignments add the standard Ruby load paths. The second group adds the various source directories that make up the NagumoSim project (src, src/Util, src/Things, src/People, src/Places, etc).
$defout = $stdout = File.new("/NagumoSim.log", "w")
$: << "/usr/local/lib/ruby/site_ruby/1.8"
$: << "/usr/local/lib/ruby/site_ruby/1.8/powerpc-darwin8.6.0"
$: << "/usr/local/lib/ruby/site_ruby"
$: << "/usr/local/lib/ruby/1.8"
$: << "/usr/local/lib/ruby/1.8/powerpc-darwin8.6.0"
$: << "."
dir = "/Volumes/HD2/Users/bcox/Projects/workspace/NagumoSim"
$: << "#{dir}/src";
$: << "#{dir}/src/Things";
$: << "#{dir}/src/People";
$: << "#{dir}/src/Places";
$: << "#{dir}/src/Util";
$, = ", " # define sensible field separator
begin
 require 'Simulation'
rescue Exception
 print $!
 print $!.backtrace.join("\n")
rescue StandardError
 print $!
 print $!.backtrace.join("\n")
rescue RuntimeError
 print $!
 print $!.backtrace.join("\n")
rescue
 print $!
 print $!.backtrace.join("\n")
end
if( not file_loaded?("NagumoLoader.rb") )
 add_separator_to_menu("Camera")
 animation_menu = UI.menu("Camera").add_submenu("NagumoSim")
 animation_menu.add_item("Start") {
    @@simulation = Simulation.new
    SketchUp.active_model.active_view.animation = @@simulation
}
animation_menu.add_item("Stop") {
    @@simulation = nil
    SketchUp.active_model.active_view.animation = nil;
}
animation_menu.add_item("--") { }
animation_menu.add_item("Idle") {
    @@simulation.commander.Idle
}
animation_menu.add_item("Alert") {
    @@simulation.commander.Alert
}
animation_menu.add_item("ArmForLandAttack") {
    @@simulation.commander.ArmForLandAttack
}
animation_menu.add_item("ArmForSeaAttack") {
    @@simulation.commander.ArmForSeaAttack
}
animation_menu.add_item("LaunchAttack") {
    @@simulation.commander.LaunchAttack
}
animation_menu.add_item("RetrievePlanes") {
    @@simulation.commander.RetrievePlanes
}
end
file_loaded("NagumoLoader.rb")
 
The begin...end block is the heart of this simulation. It demonstrates one solution to an uncertainty that has plagued this project from the beginning... exceptions don't work as you'd expect. The symptoms are that exception inheritance can't be relied on to catch Exception subclasses, and when such unhandled exceptions do occur, they crash SketchUp altogether. I think something is broken in how exceptions are passed between ruby and SketchUp's native language (C probably). My workaround is to rescue every exception that might occur explicitly as shown in this example, rather than by counting on exception inheritance to work correctly.
The rest of the file demonstrates SketchUp's technique for extending the SketchUp menu system with new commands. I've added a NagumoSim entry to the Camera menu with start and stop submenus, which call the start and stop methods respectively. The start menu simply creates a simulation instance and assigns it to SketchUp.active_model.active_view.animation This instructs SketchUp to call this instance's nextFrame method about once per second until the assignment is changed back to nil (by the stop menu). I have not found a way to control the frequency of these calls.
SketchUp provides nothing like RDT (ruby debugger toolkit for Eclipse), so you need to adjust to the inability to set breakpoints, examine variables, etc (it may be possible to use the Rails debugger; I've not tried this yet). Worse yet, the traditional debugging technique (scattering puts or print statements inside the source) won't work either because SketchUp redefines these as “private” methods for some reason and assigns $defout to the ruby console. This would be fine were it not for the fact that  unhandled exceptions will crash SketchUp, taking the information you need to debug the problem with it. A work around is to override these methods in the following Object class, which redirects all debug output to a file via $stdout.
class Object
def puts s
    $stdout.write " #{self}: #{s}\n";
end

def print s
    $stdout.write " #{self}: #{s}\n";
    end
end
$stdout and $defout are both redirected to a file by NagumoLoader. This ensures that crucial debug information is not lost if SketchUp crashes.
Simulation Loop
The Simulation class has only two methods, initialize and nextFrame. The initialize method builds the simulation and will be described later. The nextFrame method is automatically called by SketchUp and is shown below
def nextFrame view
begin
 t1 = Time.now
        print "time=#{@time} ================== #{t1-@t0}"
        event = @commands[time]
        if (event != nil)
            print "event=#{event}"
            catch :Blocked do
                case event
                    when :OrderIdle
                        commander.Idle
                    when :OrderAlert
                        commander.Alert
                    when :OrderPrepareLandAttack
                        commander.PrepareLandAttack
                    when :OrderPrepareSeaAttack
                        commander.PrepareSeaAttack
                    when :OrderLaunchAttack
                        commander.LaunchAttack
                    when :OrderRetrievePlanes
                        commander.RetrievePlanes
                    else
                        print "Unrecognized event: #{event}"
                end #case
            end
        end #if
        Person.all.each { |p|
            catch :Blocked do
                p.Update
            end
        }
        @rotationTests.each { |e| e.Update }
        view.show_frame
        @time += 1
        @t0 = t1
        $stdout.flush
    rescue Exception
        $stdout.print $!
        $stdout.print $!.backtrace.join("\n")
    rescue StandardError
        $stdout.print $!
        $stdout.print $!.backtrace.join("\n")
    rescue RuntimeError
        $stdout.print $!
        $stdout.print $!.backtrace.join("\n")
    rescue
        $stdout.print $!
        $stdout.print $!.backtrace.join("\n")
    end
end
    
    def time; @time; end
    def crewQuarters; @crewQuarters; end
    def officerQuarters; @officerQuarters; end
    def deck; @deck; end
    def bombTruck; @bombTruck; end
    def torpedoTruck; @torpedoTruck; end
    def munitionsOfficer; @munitionsOfficer; end
    def flightOfficer; @flightOfficer; end
    def hangar; @hangar; end
    def tower; @tower; end
    def bombs; @bombs; end
    def torpedos; @torpedos; end
    def dollys; @dollys; end
    def planes; @planes; end
    def crew; @crew; end
    def officers; @officers; end
    def dollyPark; @dollyPark; end
    def commander; @commander; end
    def name; "Simulation"; end
    def to_s; name; end
    def tests; @rotationTests; end
The key parts of this code are
  1. event = @commands[time]: Although events can also be generated from the menu system, it is convenient to  drive the simulation automatically instead of via the menus. The Simulation class initializes @commands to contain a map of times and the event names (:OrderIdle, etc) to be issued at the corresponding time. All events are issued to 'commander', the Person instance with overall control of the airfield (i.e. Nagumo). The commander's state machine is programmed such that it will forward each event to each of his staff (FlightOfficer, MunitionsOfficer), and these will forward them to their staff (FlightCrew, MunitionsCrew), and so forth.
  2. Person.all.each...: Currently, all of the dynamic elements in this simulation are instances of Person. Person keeps track of the instances it has created and publishes them via it's Person.all method. The result of this code is that each Person receives an Update message each time SketchUp transfers control to its Ruby subsystem. This Update call is the basis for everything that will follow and thus the core of the event handling system.
  3. catch :Blocked: This intercepts :Blocked signals, which are thrown when agents (People) request resources that are temporarily unavailable. I'll describe this later.
  4. view.show_frame: Any changes made by the Ruby code are buffered in RAM. They are only drawn on the screen when your code calls view.show_frame.
  5. $stdout.flush: Text output is also buffered in ram. So this line ensures that output is flushed to the disk during each cycle.
  6. rescue ...: As explained elsewhere, exception hierarchies don't work, and missing one has severe consequences (SketchUp crashes). This code explicitly catches all exceptions that might conceivably arise.
  7. def time... This long list of accessor methods is a hack to make simulation's internal makeup accessible elsewhere. Its mainly useful in the Ruby console, but its occasionally accessed by other objects (via the simulation constructor argument) that need to access neighboring objects. For example, crew instances do this to locate crewQuarters. It would have been better to pass this via the constuctor. I went for the quick fix.
This is the other Simulation method. It constructs the Ruby objects that make up the simulation.
def initialize()
    @t0 = Time.now
    @time = 0
    @tower = Tower.new(self,"Tower")
    
    @tieDowns = []
    4.times { |i|
        tieDown = TieDown.new(self, "TieDown#{i+1}")
        @tieDowns.push(tieDown);
    }
    @deck = Deck.new(self, "Deck", [], @tieDowns)
    @dollys = []
    4.times { |i|
        dolly = Dolly.new(self,"Dolly#{i+1}")
        @dollys.push dolly
    }
    @dollyPark = DollyPark.new(self, "DollyPark", @dollys)
    @bombs = []
    8.times { |i|
        bomb = Bomb.new(self,"Bomb#{i+1}")
        @bombs.push(bomb)
    }
    @bombTruck = Truck.new(self, "BombTruck", @bombs)
    
    @torpedos = []
    8.times { |i|
        torp = Torpedo.new(self,"Torpedo#{i+1}")
        @torpedos.push(torp)
    }
    @torpedoTruck = Truck.new(self, "TorpedoTruck", @torpedos)
    @planes = []
    4.times { |i|
        plane = Plane.new(self, "Plane#{i+1}", [])
        @planes.push(plane)
    }
    @hangar = Hangar.new(self,"Hangar", @planes)
    
    @munitionsCrew = []
    4.times { |i|
        p = MunitionsCrew.new(self,"MunitionsCrew#{i+1}")
        @munitionsCrew.push(p)
    }
    @munitionsOfficer =         MunitionsOfficer.new(self,"MunitionsOfficer", @munitionsCrew)
    @flightCrew = []
    4.times { |i|
        p = FlightCrew.new(self,"FlightCrew#{i+1}")
        @flightCrew.push(p)
    }
    @flightOfficer =    FlightOfficer.new(self,"FlightOfficer", @flightCrew)
    @crewQuarters = Quonset.new(self,"CrewQuarters", @munitionsCrew+@flightCrew)
    @officers = [@munitionsOfficer, @flightOfficer]
    @officerQuarters = Quonset.new(self,"OfficerQuarters", @officers)
    @commander = Commander.new(self, "Commander", @officers)
    
    # Automated commands, to avoid having to select
    # commands from menus during testing.
    @commands = {}
    @commands[5] = :OrderAlert
    @commands[10] = :OrderPrepareLandAttack
    @commands[20] = :OrderPrepareSeaAttack
    @commands[60] = :OrderLaunchAttack
    @commands[70] = :OrderRetrievePlanes
    @rotationTests = []
    4.times { |i|
        t = RotationTest.new self, "RotationTest#{i+1}"
        @rotationTests.push t
    }
end
There little of interest here; just creating instances and connecting them to each other. The main thing to note here is the String arguments in each constructor ("MunitionsCrew#{i+1}", for example. These provide a way for each Ruby object to associate itself with a Sketchup instance. This is described in more detail in the SimObject Classes section.
Inversion of Control
By far the biggest change is the inversion of control that SketchUp imposes on Ruby. Other than snippets executed within the ruby console, SketchUp calls Ruby, not the other way around. This is why debuggers can't be used. It is also why most code changes require exiting SketchUp, restarting it, and relaunching your application (with rare exceptions due to the fact that you can, if you're careful, reload changed files via the ruby console). But the differences run much deeper than that. Most of us learned to program in the familiar procedural style. This means if you want your animation to do things in a certain order, you'd write each step as a ruby procedure and call them in order. For example, an animation of getting ready for work might consist of a goToWork procedure something like this:
def goToWork()
    wakeUp()
    getUp()
    shower()
    shave()
    ...
end
Then you'd define procedures for each activity (wakeUp, getUp, etc), then procedures for of the microactions within it (shutOffAlarmClock(), throwBackCovers), and so forth to the atomic actions that make each higher-level action up.
SketchUp requires an entirely different approach. You initiate an animation by registering an object with SketchUp.active_model.active_view.animation = anObject. SketchUp responds by sending a stream of nextFrame(view) messages to the object you specified. This instance must remember its state between calls and sequence its behavior accordingly.
The classic solution for problems of this nature is a state machine. There are many ways to build these, manually and otherwise. I started out by using SMC (state machine compiler), hoping that the automation would save time and be easier to extend over time. SMC has code generators for many languages (including ruby), and at first glance seems to be well-documented. However the documentation turned out to thin and superficial in the most critical of areas; how to manage the nesting of states implied by the goToWork example above. So I reverted to a more conventional, manual approach, which is mainly expressed in the Person and State classes.
Person.rb
require "SimObject.rb"
require "Que.rb"
require "State.rb"
class Person < SimObject
    @@all = []
    
    def initialize(simulation, state, name)
        super(simulation, name)
        @selfQ = Que.new("SelfQ", self, [])
        @boss = nil
        @@all.push self
        @currentState = state
        @previousState = nil
        @globalState = nil
        @arguments = []
    end
    def Person.all; @@all; end
    def boss=(boss); @boss = boss; end
    def boss; @boss; end
    def selfQ; @selfQ; end
    def Block; throw :Blocked; end
    def Unblock; end
    def Arguments; @arguments; end
    # Public state management
    def Update; @currentState.Execute self; end
    def CurrentState; @currentState; end
    def PreviousState; @previousState; end
    def GlobalState; @globalState; end
    # Private state management
    def ChangeState newState, *arguments
        @arguments = arguments
        @previousState.Exit(self) if @previousState
        @currentState = newState
        @currentState.Enter self
    end
    def RevertToPreviousState; @currentState = @previousState; end
end
The Update and ChangeState methods are core of the state management machinery. Update, which s sent by the Simulation class during each simulation cycle, implements the Person's behavior while it is in a certain state (@currentState). Thus it call's the current state's Execute method. ChangeState, by contrast, handles transitions between states so it calls the current and next state's Enter and Exit methods respectively. These methods are defined in the State class and overridden in each of its subclasses as appropriate. Notice that ChangeState accepts a variable length argument list, which it makes accessible to subsequent code by saving them in the @arguments instance variable.
State.rb
This is an abstract class that defines three class methods; Enter, Exit and Execute. The Enter and Exit methods are executed once when a state is entered and exited, respectively. The Execute method is called by the agent's Update method; about once per second. The "agent" is whatever instance the state applies to; in this example, the various instances of Commander, FlightOfficer, FlightCrew, MunitionsOfficer or MunitionsCrew.
# Since states are read-only and only have class methods,
# we must provide the agent as an argument to each method. The agent is
# the object whose state is being managed here.
class State
def initialize
end
def State.Enter agent
end
def State.Execute agent
end
def State.Exit agent
end
end
Commander.rb
The Commander class is a simple example of how States are used. The Simulation initialize method constructs a single instance of this class to represent the airfield commander. I'll explain how instance initialization later to focus first on how the state machine works. The six methods after initialize (Idle, Alert, PrepareLandAttack, PrepareSeaAttack, LaunchAttack, and RetrievePlanes) are the interface between commander instances and the Simulation class. They represent the six commands that the Commander recognizes. These six events drive everything else.
require 'Officer'
class Commander < Officer
    def initialize(simulation, name, staff)
        super(simulation, Idle, name, staff)
    end
    def Idle; ChangeState Idle; end
    def Alert; ChangeState Alert; end
    def PrepareLandAttack; ChangeState PrepareLandAttack; end
    def PrepareSeaAttack; ChangeState PrepareSeaAttack; end
    def LaunchAttack; ChangeState LaunchAttack; end
    def RetrievePlanes; ChangeState RetrievePlanes; end
    class Idle < State
        def Idle.Enter agent
            agent.staffQ.resources.each { |e| e.Idle }
        end
    end #Class
    class Alert < State
        def Alert.Enter agent
            agent.staffQ.resources.each { |e| e.Alert }
        end
    end #Class
    class PrepareLandAttack < State
        def PrepareLandAttack.Enter agent
            agent.simulation.flightOfficer.PrepareLandAttack
            agent.simulation.munitionsOfficer.PrepareLandAttack
            #print agent.staffQ.resources
            #agent.staffQ.resources.each { |e|
            #
        print "#{e}.PrepareLandAttack"
            #
        e.PrepareLandAttack}
        end
    end #Class
    class PrepareSeaAttack < State
        def PrepareSeaAttack.Enter agent
            agent.simulation.flightOfficer.PrepareSeaAttack
            agent.simulation.munitionsOfficer.PrepareSeaAttack
            #print agent.staffQ.resources
            #agent.staffQ.resources.each { |e|
            #
        print "#{e}.PrepareSeaAttack"
            #
        e.PrepareSeaAttack
            #}
        end
    end #Class
    class LaunchAttack < State
        def LaunchAttack.Enter agent
            agent.staffQ.resources.each { |e|
                print "#{e}.LaunchAttack"
                e.LaunchAttack}
        end
    end #Class
    class RetrievePlanes < State
        def RetrievePlanes.Enter agent
            agent.staffQ.resources.each { |e|
                print "#{e}.RetrievePlanes"
                e.RetrievePlanes}
        end
    end #Class
end #Class
The rest of the Commander class consists internal classes that define the various States that the commander will be in as the simulation proceeds. Each is a subclass of State, and each defines one or more of the three State class methods methods. Each takes a single argument, agent, which is a reference to the commander instance. (This argument is the main cost of having made the State class read-only.) The method bodies use this to reference the commander instance which holds whatever instance information they need.
MunitionsOfficer.rb
The munitions officer is a member of commander's staff  who manages several instances of MunitionsCrew as his own staff. It publicizes methods (Idle, Alert, PrepareLandAttack, PrepareSeaAttack, etc) as methods. Each of these use ChangeState to advance its own state machine, which is implemented as internal subclasses of State.
require "Officer"
require "State"
class MunitionsOfficer < Officer
    def initialize(simulation, name, staff)
        super(simulation, Idle, name, staff)
    end
    def Idle; ChangeState Idle; end
    def Alert; ChangeState Alert; end
    def PrepareLandAttack; ChangeState PrepareLandAttack; end
    def PrepareSeaAttack; ChangeState PrepareSeaAttack; end
    def LaunchAttack; ChangeState LaunchAttack; end
    def RetrievePlanes; ChangeState RetrievePlanes; end
    class Idle < State
        def Idle.Enter agent
            agent.staffQ.resources.each { |e| e.Idle }
        end
    end #Idle
    class Alert < State
        def Alert.Enter agent
            agent.staffQ.resources.each { |e| e.Alert }
        end
    end #Alert
    class PrepareLandAttack < State
        def PrepareLandAttack.Enter agent
            agent.simulation.hangar.planeQ.resources.each { |plane|
                crew = agent.staffQ.get(self)
                crew.PrepareLandAttack plane
            }
        end
    end #PrepareLandAttack
    class PrepareSeaAttack < State
        def PrepareSeaAttack.Enter agent
            agent.simulation.hangar.planeQ.resources.each   { |plane|
                crew = agent.staffQ.get(self)
                crew.PrepareSeaAttack plane
            }
        end
    end #PrepareSeaAttack
    class LaunchAttack < State
        def LaunchAttack.Enter agent
            agent.simulation.hangar.planeQ.resources.each   { |plane|
                if (plane.hasBombs? || plane.hasTorpedos?)
                    crew = agent.staffQ.get(self)
                    crew.LaunchAttack plane
                end
            }
        end
    end #LaunchAttack
    class RetrievePlanes < State
        def RetrievePlanes.Enter agent
            agent.simulation.hangar.planeQ.resources.each   { |plane|
                crew = agent.staffQ.get(self)
                crew.RetrievePlane plane
            }
        end
    end #RetrievePlanes
end #MunitionsOfficer
MunitionsCrew.rb
MunitionsOfficer was relatively straightforward because its internal states were a direct reflection of its external command structure. MunitionsCrew is more complicated because most of its commands (PrepareLandAttack, for example) trigger multiple internal states (AcquireDolly, XferBombsStoreToDolly, XferBombsDollyToPlane, etc).
Some commands (PrepareLandAttack, PrepareSeaAttack) accept arguments (plane). FlightCommander provides these to tell the crew which plane to prepare Notice how some states (PrepareLandAttack, for example) are decomposed into finer-granularity states by assigning lists of sub-states to the @agenda variable in the macro-state's Entry method.Finally, this is the first class that shows how visual animations are handled.
For example, the Idle state is the military's "at ease' state. This state's Execute method is called once per clock tick (about 1/second). The at? method determines whether this crew member, identified by the agent argument, is within a stride length (@@strideLength class variable) of the simulation's crew quarters (agent.simulation.crewQuarters.origin). If so, the crew 'piddles', which is implemented (Person class) as a random walk at that location. Otherwise the stride method is called, which implements a stride (by @@strideLength) of the agent's visual representation towards that location.
require "Person.rb"
class MunitionsCrew < Person
    def initialize(simulation, name)
        super(simulation, Idle, name)
        @selfQ = Que.new("SelfQ", self, [])
        @agenda = []
        @plane = nil
    end
    
    def commander; boss.commander; end
    def selfQ; @selfQ; end
    def plane; @plane; end
    def SetAgenda agenda; @agenda = agenda; end
    def NextAgenda; ChangeState @agenda.shift; end
    def Idle; ChangeState Idle; end
    def Alert; ChangeState Alert; end
    def PrepareLandAttack plane; @plane = plane; ChangeState PrepareLandAttack, plane; end
    def PrepareSeaAttack plane; @plane = plane; ChangeState PrepareSeaAttack, plane; end
    def LaunchAttack plane; @plane = plane; ChangeState LaunchAttack, plane; end
    def RetrievePlane plane; @plane = plane; ChangeState RetrievePlane, plane; end
        
    class Idle < State
        def Idle.Execute agent
            if (agent.at?(agent.simulation.crewQuarters.origin))
                agent.piddle
            else
                agent.stride(agent.simulation.crewQuarters.origin, agent.selfQ.resources)
            end
        end
    end #Idle
    class Alert < State
        def Alert.Execute agent
            if (agent.at?(agent.simulation.hangar.origin))
                agent.piddle
            else
                agent.stride(agent.simulation.hangar.origin, agent.selfQ.resources)
            end
        end
    end #Alert
    class PrepareLandAttack < State
        def PrepareLandAttack.Enter agent
            agent.SetAgenda [
                AcquireDolly,
                XferBombsStoreToDolly,
                DumpTorpedos,
                XferBombsDollyToPlane,
                ReleaseDolly
            ]
        end
        def PrepareLandAttack.Execute agent
            if (agent.at?(agent.simulation.hangar.origin))
                agent.piddle
            else
                agent.stride(agent.simulation.hangar.origin, agent.selfQ.resources)
                agent.NextAgenda
            end
        end
    end # PrepareLandAttack
    class PrepareSeaAttack < State
        def PrepareSeaAttack.Enter agent
            agent.SetAgenda [
                AcquireDolly,
                XferTorpedosStoreToDolly,
                DumpBombs,
                XferTorpedosDollyToPlane,
                ReleaseDolly
            ]
        end
        def PrepareSeaAttack.Execute agent
            if (agent.at?(agent.simulation.hangar.origin))
                agent.NextAgenda
            else
                agent.stride(agent.simulation.hangar.origin, agent.selfQ.resources)
            end
        end
    end #PrepareSeaAttack
    class DumpBombs < State
        def DumpBombs.Execute agent
            plane = agent.plane
            deck = agent.simulation.deck
            if (agent.at?(plane.origin))
                if (plane.hasBombs?)
                    munitions = plane.muntionsQ.get(self)
                    deck.deckQ.put(self, munitions)
                end
                agent.piddle
            else
                agent.stride(plane.origin, agent.selfQ.resources)
            end
        end
    end # DumpBombs
    class DumpTorpedos < State
        def DumpTorpedos.Execute agent
            plane = agent.plane
            deck = agent.simulation.deck
            if (agent.at?(plane.origin))
                if (plane.hasTorpedos?)
                    munitions = plane.muntionsQ.get(self)
                    deck.deckQ.put(self, munitions)
                end
                agent.piddle
            else
                agent.stride(plane.origin, agent.selfQ.resources)
            end
        end
    end #DumpTorpedos
    class LaunchAttack < State
    end #LaunchAttack
    class RetrievePlane < State
    end #RetrievePlane
    # micro states
    class AcquireDolly < State
        def AcquireDolly.Execute agent
            if (agent.at?(agent.simulation.dollyPark.origin))
                dolly = agent.simulation.dollyPark.dollyQ.get(self)
                agent.selfQ.put(agent, dolly)
                agent.NextAgenda
            else
                agent.stride(agent.simulation.dollyPark.origin, agent.selfQ.resources)
            end
        end
    end #AcquireDolly
    class ReleaseDolly < State
        def ReleaseDolly.Execute agent
            if (agent.at?(agent.simulation.dollyPark.origin))
                dolly = agent.selfQ.get(self)
                agent.simulation.dollyPark.put(self, dolly)
                agent.NextAgenda
            else
                agent.stride(agent.simulation.dollyPark.origin, agent.selfQ.resources)
            end
        end
    end #ReleaseDolly
    class XferBombsStoreToDolly < State
        def XferBombsStoreToDolly.Execute agent
            if (agent.at?(agent.simulation.bombTruck.origin))
                bomb = agent.simulation.bombTruck.truckQ.get(self)
                munitionsQ = agent.selfQ.resources[0].munitionsQ
                munitionsQ.put(self, bomb)
                agent.NextAgenda
            else
                agent.stride(agent.simulation.bombTruck.origin, agent.selfQ.resources)
            end
        end
    end #XferBombsStoreToDolly
    class XferBombsDollyToStore < State
        def XferBombsDollyToStore.Execute agent
            if (agent.at?(agent.simulation.bombTruck.origin))
                munitionsQ = agent.selfQ.resources[0].munitionsQ
                munitionsQ.get(self)
                agent.simulation.bombTruck.truckQ.put(self, bomb)
                agent.NextAgenda
            else
                agent.stride(agent.simulation.bombTruck.origin, agent.selfQ.resources)
            end
        end
    end #XferBombsDollyToStore
    class XferBombsDollyToPlane < State
    end #XferBombsDollyToPlane
    class XferBombsPlaneToDolly < State
        def XferBombsPlaneToDolly.Execute agent
            if (agent.at?(agent.plane.origin))
                torpedo = agent.plane.muntionsQ.get(self)
                munitionsQ = agent.selfQ.resources[0].munitionsQ
                munitionsQ.put(self, torpedo)
                agent.NextAgenda
            else
                agent.stride(agent.plane.origin, agent.selfQ.resources)
            end
        end
    end #XferBombsPlaneToDolly
    class XferTorpedosPlaneToStore < State
    end #XferTorpedosPlaneToStore
    class XferTorpedosStoreToDolly < State
        def XferTorpedosStoreToDolly.Execute agent
            if (agent.at?(agent.simulation.dollyPark.origin))
                torp = agent.simulation.dollyPark.dollyQ.get(self)
                munitionsQ = agent.selfQ.resources[0].munitionsQ
                munitionsQ.put(self, torp)
                agent.NextAgenda
            else
                agent.stride(agent.simulation.dollyPark.origin, agent.selfQ.resources)
            end
        end
    end #XferTorpedosStoreToDolly
    class XferTorpedosDollyToStore < State
        def XferTorpedosDollyToStore.Execute agent
            if (agent.at?(agent.plane.origin))
                torp = agent.plane.munitionsQ.get(self)
                munitionsQ = agent.selfQ.resources[0].munitionsQ
                munitionsQ.put(self, torp)
                agent.NextAgenda
            else
                agent.stride(agent.plane.origin, agent.selfQ.resources)
            end
        end
    end #XferTorpedosDollyToStore
    class XferTorpedosDollyToPlane < State
        def XferTorpedosDollyToPlane.Execute agent
            if (agent.at?(agent.plane.origin))
                torpedo = agent.plane.muntionsQ.get(self)
                munitionsQ = agent.selfQ.resources[0].munitionsQ
                munitionsQ.put(self, torpedo)
                agent.NextAgenda
            else
                agent.stride(agent.plane.origin, agent.selfQ.resources)
            end
        end
    end #XferTorpedosDollyToPlane
end #  
FlightOfficer.rb
While MunitionsOfficer and MunitionsCrew are concerned with moving bombs or torpedoes to/from the Bomb and Torpedo trucks and planes, FlightOfficer and FlightCrew are concerned with moving planes from the hangar to the flight deck.
require "Officer"
require "State"
class FlightOfficer < Officer
    def initialize(simulation, name, staff)
        super(simulation, Idle, name, staff)
    end
    def Idle; ChangeState Idle; end
    def Alert; ChangeState Alert; end
    def PrepareLandAttack; ChangeState PrepareAttack; end
    def PrepareSeaAttack; ChangeState PrepareAttack; end
    def LaunchAttack; ChangeState LaunchAttack; end
    def RetrievePlanes; ChangeState RetrievePlanes; end
    class Idle < State
        def Idle.Enter agent
            agent.staffQ.resources.each { |e| e.Idle }
        end
    end #Idle
    class Alert < State
        def Alert.Enter agent
            agent.staffQ.resources.each { |e| e.Alert }
        end
    end #Alert
    class PrepareAttack < State
        def PrepareAttack.Enter agent
            agent.simulation.hangar.planeQ.resources.each { |plane|
                crew = agent.staffQ.get(self)
                crew.PrepareAttack plane
            }
        end
    end #PrepareAttack
    class LaunchAttack < State
        def LaunchAttack.Enter agent
            agent.simulation.hangar.planeQ.resources.each   { |plane|
                if (plane.hasBombs? || plane.hasTorpedos?)
                    crew = agent.staffQ.get(self)
                    crew.LaunchAttack plane
                end
            }
        end
    end #LaunchAttack
    class RetrievePlanes < State
        def RetrievePlanes.Enter agent
            agent.simulation.hangar.planeQ.resources.each   { |plane|
                crew = agent.staffQ.get(self)
                crew.RetrievePlane plane
            }
        end
    end #RetrievePlanes
end #FlightOfficer
FlightCrew.rb
In this scenario, flight crew are responsible for moving planes between the hangar and tie down points on the flight deck.
require "Person.rb"
class FlightCrew < Person
    def initialize(simulation, name)
        super(simulation, Idle, name)
        @selfQ = Que.new("SelfQ", self, [])
        @agenda = []
        @plane = nil
        @tieDown = nil
    end
    
    def commander; boss.commander; end
    def selfQ; @selfQ; end
    def plane; @plane; end
    def agenda; @agenda; end
    def tieDown; @tieDown; end
    def tieDown=(tieDown); @tieDown = tieDown; end
    def SetAgenda agenda; @agenda = agenda; end
    def NextAgenda; ChangeState @agenda.shift; end
    # Externally-driven Transitions (Events)
    def Idle; ChangeState Idle; end
    def Alert; ChangeState Alert; end
    def PrepareAttack plane; @plane = plane; ChangeState PrepareAttack, plane; end
    def LaunchAttack plane; @plane = plane; ChangeState LaunchAttack, plane; end
    def RetrievePlane plane; @plane = plane; ChangeState RetrievePlane, plane; end
        
    class Idle < State
        def Idle.Execute agent
            if (agent.at?(agent.simulation.crewQuarters.origin))
                agent.piddle
            else
                agent.stride(agent.simulation.crewQuarters.origin, agent.selfQ.resources)
            end
        end
    end #Idle
    class Alert < State
        def Alert.Execute agent
            if (agent.at?(agent.simulation.hangar.origin))
                agent.piddle
            else
                agent.stride(agent.simulation.hangar.origin, agent.selfQ.resources)
            end
        end
    end #Alert
    class PrepareAttack < State
        def PrepareAttack.Enter agent
            agent.SetAgenda [
                GetPlaneFromHangar,
                PutPlaneOnDeck
            ]
            agent.NextAgenda
        end
    end
    class LaunchAttack < State
    end #LaunchAttack
    class RetrievePlane < State
    end #RetrievePlane
    # micro states
    class GetPlaneFromHangar < State
        def GetPlaneFromHangar.Execute agent
            if (agent.at?(agent.plane.origin))
                agent.selfQ.put(agent, agent.plane)
                agent.NextAgenda
            else
                agent.stride(agent.plane.origin, agent.selfQ.resources)
            end
        end
    end #GetPlaneFromHangar
    class PutPlaneOnDeck < State
        def PutPlaneOnDeck.Enter agent
                agent.tieDown=agent.simulation.deck.tieDownQ.get(self)
        end
        def PutPlaneOnDeck.Execute agent
            if (agent.at?(agent.tieDown.origin))
                plane = agent.simulation.hangar.planeQ.get(self)
                agent.selfQ.put(agent, agent.plane)
                agent.NextAgenda
            else
                agent.stride(agent.tieDown.origin, agent.selfQ.resources)
            end
        end
        def PutPlaneOnDeck.Exit agent
            agent.tieDown = nil
        end
    end #PutPlaneOnDeck
end #
SimObject.rb
The state machine logic emphasized so far is stable and unlikely to be changed further. Most of the still-unresolved issues are encapsulated in the SimObject and Que classes, which (along with several trivial intermediary classes, like Officer) defines key animation methods like piddle and stride. The class is in transition but important nonetheless:  provides the animation methods via which ruby simulations interact with their SketchUp environment. The animation methods are undergoing active development. Many of them are experiments in progress and might well don't work.
require 'sketchup.rb'
require "Object.rb"
class SimObject < Object
    @@visualDefinitionMap = {}
    @@worldOriginPoint = Geom::Point3d.new(0,0,0)
    @@worldOriginTranslation = Geom::Transformation.translation(@@worldOriginPoint)
    @@zaxis = Geom::Vector3d.new(0, 0, 1)
    def initialize(simulation, entityName)
        @name = entityName
        @simulation = simulation
        Sketchup.active_model.active_entities.each { |e|
            if(e.instance_of?(Sketchup::ComponentInstance) && e.name == entityName)
                @visualAspect = e
                return self
            end
        }
        raise Exception, "Sketchup ComponentInstance #{entityName} not found"
        end
    def at?(destination, strideLength=100)
        return (origin.distance(destination) < strideLength)
    end
    #angle = vector.angle_between visualAspect.transformation.xaxis
    #rotation = Geom::Transformation.rotation origin, @zaxis, angle
    #self.visualAspect.move!(rotation)              
    def stride destination, attachments=[], strideLength=30
        vector = origin.vector_to destination
        step = origin.offset vector, strideLength
        # entities = Sketchup.active_model.active_entities
        # entities.add_line(origin, step)
        translation = Geom::Transformation.translation step
        @visualAspect.move! translation
        attachments.each { |a| a.visualAspect.move! translation }
    end
    def jumpTo origin
        point = Geom::Point3d.new origin
        translation = Geom::Transformation.translation(point)
        @visualAspect.move!(translation)
    end
    def jumpTo(x, y, z)
        point = Geom::Point3d.new(x, y, z);
        translation = Geom::Transformation.translation(point)
        @visualAspect.move!(translation)
    end
    
    def turnBy(radians)
        origin = self.origin
        rotation = Geom::Transformation.rotation origin, @@zaxis, radians
        @visualAspect.move! rotation
    end
    
    def rotateBy(radians)
        centerPt = @visualAspect.transformation.origin
        rotation = Geom::Transformation.rotation(centerPt, @@zaxis, radians)
        translation = Geom::Transformation.translation centerPt
        @visualAspect.move!(rotation * translation)
    end
    def piddle(strideLength=100)
            origin = self.origin
            jumpTo(origin.x+rand(strideLength), origin.y+rand(strideLength), 0)
    end
    
    def origin; @visualAspect.transformation.origin; end
    def model; @visualAspect.model; end
    def bounds; model.bounds; end
    def width; bounds.width; end
    def depth; bounds.depth; end
    def height; bounds.height; end
    def center; bounds.center; end
    def simulation; @simulation; end
    def visualAspect; @visualAspect; end
    def name; @name; end
    def to_s; @name; end
end
First, notice that the initialize method takes two arguments. The simulation argument publicizes the simulation environment to all instances as a convenience feature useful for rapidly changing problems such as this. (This argument has since been replaced with a global parameter; $simulation, set by NagumoLoader.rb. The old way was making console printouts impossibly long since everything referenced everything else). The entityName argument is the name of the visual entity that represents this object in the SketchUp window. In other words, adding a new object involves three steps:
  1. Defining a Ruby class for that object (a subclass of SimObject) as described earlier.
  2. Using SketchUp's 3D drawing capabilities to create a visual representation for that object within the NagumoSim.skp file. Since these will often be replicated several times, it is usually wise to define each one as SketchUp component.
  3. Creating a uniquely named SketchUp instance for each object. I generally do this by creating as many visual instances as I'll need with the move/replicate tool. Then I use SketchUp's Outliner tool (Window/Outliner menu) in conjunction with the Entity Info (Window/Entity Info menu) to give each instance a unique name as shown below:
The need to preallocate visual instances like this at first feels awkward. But it soon feels liberating because it corresponds to how objects work in the real world. Thing's don't magically spring into existence during an initialize method, but are present (visually, but not otherwise) when SketchUp is first launched. Contrast this with the older approach (still visible in the SimObject.visualDefinition method in the above source). In the older approach, the Simulation class used this method to instantiate as many SketchUp component definition instances as it required, positioning each one via origin= calls. It proved far easier to use SketchUp's move tool to move preallocated instances than writing positioning commands as code.
Que.rb (resource conflicts)
The real world is automatically governed by the physical conservation laws that underlie resource conflicts and we 'handle' them naturally. But computers know nothing of physical conservation laws, so they must be explicitly dealt with in some manner. Obviously an airfield has only so many airplanes, bombs and torpedoes, and agents who request more than that must somehow be blocked until more are available. Less obviously, but conversely, the same truism is faced by those who deliver airplanes, bombs and torpedoes, in that the airfield has a finite amount of space to receive more. The same truism applies universally, to plane tie down spots on the tarmac, and even to individual crewmen, who must be explicitly provided the restriction that their hand's can only contain a single dolly, or that dolly can only hold so many bombs or torpedoes, and so forth.
Resource constraints are currently managed by the Que class, to be described below. Que is a corruption of Queue, chosen to avoid possible conflicts with other Queue classes I've experimented with. I'll rename it once I think of a better one.Almost every SimObject use Que's to hold (contain) resources. For example, Officers allocate Que's to "contain" the staff under their control, hangar's use Que's to "contain" planes, the FlightDeck uses Que's to "contain" tie down points, tie down points use Que's to "contain" whatever plane is tied down there. Even crew members use Que's to contain the tools in their hands.
For example, the first step in the MunitionsQueue agendas is AcquireDolly. This involves two separate Que operations, a "get" to check out a dolly from the DollyPark, and a "put" to add it to the crew member's internal Que. Both of these operations might in principle block; the get if no more dolly's are available in the park, and the put if the crew member's Que already contains a dolly. Here's how this is handled currently:
require 'sketchup';
# NagumoQueue: like a bread queue: some (fixed) number of loaves, some (predetermined, fixed)
# capacity for storing them, and two queues waiting to take loaves or leave them. A simple
# abstraction, but general enoough to support most of the resource limit problems in NagumoSim.
class Que
    
    def initialize(name, attachedTo, initialResources=[], maxCapacity=1)
        @name = name
        @attachedTo = attachedTo
        @maxCapacity = maxCapacity
        @resources = initialResources
        @waitingToPut = []
        @waitingToGet = []
    end
    
    def name; @name; end
    def attachedTo; @attachedTo; end
    def origin; @attachedTo.origin; end
    def isEmpty?(); @resources.size() == 0;; end
    def isFull?(); @resources.size() >= @maxCapacity-1;; end
    def maxCapacity; @maxCapacity; end
    def resources; @resources; end
    def waitingToPut; @waitingToPut; end
    def waitingToGet; @waitingToGet; end
    def resourceCount(); @resources.size;; end
    def emptyCount(); @maxCapacity-@resources.size;; end
    
    # Get single resource
    def get(getter)
        # print "#{getter}.get(#{self}) [#{resources}]";
        n = 1;
        if (@resources.size < n) # Insufficient resources
            # print("
        #{getter}.get blocked at #{self} [#{resources}]")
            @waitingToGet.push(getter)
            # getter.Block() # getter blocks
            # raise BlockException, "#{getter} blocked"
            throw :Blocked
    else
        return @resources.pop();
    end
        putter=@waitingToPut.pop();
        putter.Unblock() if (putter != nil)
    end
    
    def put(putter, resource)
        # print "#{putter}.put(#{resource}) [#{resources}]";
        n = 1;
        if (@resources.size+n > maxCapacity)
            # print("#{putter}.put(#{resource}) blocked at #{self} [#{resources}]");
            @waitingToPut.push(putter)
            # putter.Block() # putter blocks
            # raise BlockException, "#{putter} blocked"
            throw :Blocked
        else
            @resources.push(resource);
        end
        if nextGetter = @waitingToGet.pop
            nextGetter.Unblock()
        end
    end
    def getArray(sender, n=1)
        # print "getArray(#{sender}, #{n}))";
        if (@resources.size < n)
            # print("#{sender} blocking getting #{n} from #{self} (resources=#{resources.size()}")
            @waitingToGet.push(sender)
            sender.Block() # sender blocks
            # raise BlockException, "#{putter} blocked"
            throw :Blocked
        else
            resources = @resources[0..n-1]
            resources.each { |e| @resources.delete(e) }
            return resources
    end
    putter = @waitingToPut.pop();
    putter.Unblock() if (putter != nil)
    end
    
    def putArray(sender, resourceArray, n=1)
        # print "putArray(#{sender}, #{resourceArray}, #{n})";
        if (@resources.size+n >= maxCapacity)
            # print("#{sender} blocks putting #{n} to #{self} (resources=#{resources.size()}");
            @waitingToPut.push(sender);
            sender.Block();
        else
            resourceArray.each { |r| @resources.push(r); }
        end
        nextGetter = @waitingToGet.pop()
        nextGetter.Unblock() if (nextGetter != nil)
    end
    
end
Blocking is handled by raising a :Blocked signal to prevent further progress of the Update invocation for the currently executing object. This code may be OK, but the Unblocking operation is almost certainly wrong.
Note added June 7 2006: The solution for blocking/unblocking turned out to be so simple I'd overlooked it until I began coverting the simulation from Ruby to Java (as described in the last section). As I've not updated the Ruby code, the following Java example will have to do. The new Queue class is far simpler, little more than an ArrayList of resources and an maximum number of resources to store there. The modified version simply throws a ResourceConstraintException if either resource limit (too few or too many) is exceeded. The exception is caught by the State subclass and handled as shown here:
private class XferBombsFromStoreToDolly extends State
{
    protected void enter()
    {
    
    turnToFace(World.instance.bombTruck);
    }
    protected void execute()
    {
        if (intersects(World.instance.bombTruck))
        {
    try
    {
        Dolly agentDolly = (Dolly) loadQ.resources.get(0);
        xferResource(Bomb.class, World.instance.bombTruck.loadQ, agentDolly.loadQ);
        advanceAgenda();
    }
    catch (ResourceConstraintFault e)
    {
        log.info(e+"");
    }
        }
        else
    moveTowards(World.instance.bombTruck);
    }
    protected void exit() {}
}
Paraphrasing, in the state of getting bombs from the bomb store, if you're not at the bombTruck (the intersects test), continue moving towards the truck (the moveTowards call). Once you are there, attempt to transfer a bomb from the truck to the dolly, counting on the truck's queue to throw a ResourceConstraintFault if there are no more bombs on the truck or if the dolly won't accept any more. If the transfer succeeds, advance to the next state on the agenda. If the transfer fails for either reason, remain in the current state until the resource constraint is relieved by the activities of other agents. The pattern shown here is repeated almost verbatim in all States that access constrained resources.
Translation/Rotation Transformations
The second obvious shortcoming is that the animation methods (of the SimObject class) handle translations but not rotations. This makes figures move like cardboard cutouts, without ever turning to face their destination. This is  because SketchUp's documentation never explain how to combine translation and rotation transformations. I'm familiar with how to do this in OpenGL, but SketchUp's way escapes me. I'll document it here once I figure it out myself.These are just notes on what I've tried, mainly to keep track this for my own purposes.
Consistent observation is that combinations of translations with rotations don't work. Rotations work. So do translations. But doing one then the other erases the former. Visual effect is that rotating an object positioned over yonder makes it jump to the origin and rotate (and/or translate) from there.
A promising idea which failed to... manually build a transformation that combines the two operations. But print statements show that the translation info (in the 4th column) is simply erased during Geom::Transformation.new.
        # Does not work. The Geom::Transformation.new tv returns a transformation
    # whose origin is 0,0,0. A complete mystery.
    # transformation = Geom::Transformation.translation offset
    def stride1 destination, attachments=[], strideLength=100
        vector = origin.vector_to destination
        offset = origin.offset vector, strideLength
        radians = vector.angle_between visualAspect.transformation.xaxis
        print "origin=#{origin}\ndestination=#{destination}\nvector=#{vector}\noffset=#{offset}\nradians=#{radians}"
        cos = Math.cos radians
        sin = Math.sin radians
        print "dx=#{origin.x+offset.x} dy=#{origin.y+offset.y} z=#{origin.z+offset.z}"
        tv = [
             cos, sin,         0,         origin.x+offset.x,
            -sin, cos,         0,         origin.y+offset.y,
        
         0,      0,        1,         origin.z+offset.z,
        
         0,      0,        0,         1,
        ]
        print "tv=#{tv}"
        transformation = Geom::Transformation.new tv
        print "transformation=#{transformation.to_a}"
        @visualAspect.move! transformation
        print "va.transformation=#{@visualAspect.transformation.to_a}"
        attachments.each { |a| a.visualAspect.move! translation }
    end
The usual solution is to combine transformations by multiplying them like this, where T is the translation matrix and R is the rotational one: T-1 * M * T. Sketchup is doing the math OK insofar as I could check via Excel. But the net effect is that everything translates either to the origin to way off in left field. Here's the latest shot at it:
# The traditional solution: inverse * rotate * translate
    def stride destination, attachments=[], strideLength=100
        vector = origin.vector_to destination
        offset = origin.offset vector, strideLength
        print "origin=\n#{origin} dest=\n#{destination} vector=\n#{vector} offset=\n#{offset}"
        translation = Geom::Transformation.translation offset
        printMatrix "translation", translation
        inverse = translation.inverse
        printMatrix "translation", translation
        radians = vector.angle_between visualAspect.transformation.xaxis
        axis = @@zaxis
        point = offset
        rotation = Geom::Transformation.rotation point, axis, radians
        transformation = inverse * rotation * translation
        printMatrix "transformation", transformation
        status = @visualAspect.move! transformation
        printMatrix "result", @visualAspect.transformation
        attachments.each { |a| a.visualAspect.move! transformation }
    end
Finally, a real clue!! The hand-built transformation idea was right. The only thing wrong is that Sketchup's storing the transform of the usual transformation matrix; i.e. in column-major format. This version translates+rotates almost correctly, but with a 15 degree oscillation in the rotation each step that still needs to be debugged. Once that's sorted, I need to split out the rotation stuff to a separate rotate method. Only need to rotate during Entry/Exit methods, not every Execute as now. Still its about as fast as before even now.
def stride destination, attachments=[], strideLength=100
        vector = origin.vector_to destination
        offset = origin.offset vector, strideLength
        radians = vector.angle_between visualAspect.transformation.xaxis
        print "origin=#{origin}\ndestination=#{destination}\nvector=#{vector}\noffset=#{offset}\nradians=#{radians}"
        cos = Math.cos radians
        sin = Math.sin radians
        print "dx=#{origin.x+offset.x} dy=#{origin.y+offset.y} z=#{origin.z+offset.z}"
        tv = [
             cos, -sin,        0,         0,
             sin,   cos,       0,         0,
        
         0,        0,         1,         0,
        
         offset.x, offset.y, offset.z, 1
        ]
        printMatrix "tv", tv
        transformation = Geom::Transformation.new tv
        printMatrix "transformation", transformation
        @visualAspect.move! transformation
        printMatrix "result", @visualAspect.transformation
        attachments.each { |a| a.visualAspect.move! transformation }
    end
Back to Java
The uncertainties in the last section plus the slow frame rates I'd been experiencing made me reconsider the Schetchup and Ruby approach for this application. I've returned, at least experimentally, to the Java implementation I'd started with. I'll do a similar tutorial for this environment once it settles down a bit.
The new strategy is to leverage only Sketchup strengths as a 3D drawing environment, exporting models as .obj and .mtl files to a real game engine. Before I began exploring the Ruby approach, I'd tried JME (Java Monkey Engine), which uses LWJGL (Lightweight Java GL) to connect to the GL hardware. Although Sketchup is better documented than JME,  the far side of the documented API is invisible, undocumented, and proprietary. For example, the reasons that some exceptions crash SketchUp, how translations/rotations are handled, and why performance seems limited to about 1 frame/second is still a mystery after weeks of determined digging for such information.
So with about a day's work, I converted all the Ruby code to Java, making major cleanups along the way. Although JME's documentation still sux, full source is available, there's an active and helpful user community, and the Eclipse debugger means everything is open to inspection. Plus I seem to getting a 200 fold speed improvement (200 fps) over SketchUp at the moment.
The main problem is that models imported from Sketchup as .obj and .mtl files are rendered as ugly black and white shapes. After considerable help from JME's key developers, they've been unable to explain it either. But I've since confirmed that SketchUp's materials (.mtl files) are imported successfully by Blender, which is proving very useful in understanding still-murky matters like material, textures, coordinates, and coordinate transforms.