Initial work on my library of vehicle and ordnance graphics relied on a mix of SVG and embedded Ruby scripts to dynamically affect the static SVG data. As Ruby supports this type of structure through its erb class, my collection of vehicle graphics consisted of .rsvg files. Over time, I discovered that working with this format was very tedious, especially with regard to vehicle variants, so it was time to expand the capabilities of my toolkits. It was time to move away from data files and rely fully on scripts.
It was the M3/M5 Stuart light tank that showed me that I needed a different way of expressing the drawing operations. I wanted a method that could support all the different hull and turret variants in one script. I also wanted the contents of the scripts to resemble something that wasn’t strictly a Ruby script. At its heart, sure. But I wanted drawing commands to be collected together and not spread around in different Ruby methods.
The result is the MarkingEngine.
The MarkingEngine is a Ruby class that is a macro processor. An associated Ruby module named MEI, for MarkingEngine Instructions, provides a method API that looks like a collection of assembly language instructions for the processor.
The MarkingEngine’s API consists of only three instructions:
- new: create an instance of the MarkingEngine class
- run: execute the macros given during instantiation of the MarkingEngine class
- assemble: a class level method that is used to collect an array of MEI instructions into a single binary stream
Everything the MarkingEngine needs is supplied during instantiation:
- canvas: the surface to draw upon, which is supplied via the Canvas class
- palette: an optional Hash which contains colors, patterns, gradients for fill & stroke operations
- macros: an optional Array of MEI instructions
- traps: an optional Array of Ruby lambdas that can dynamically assemble MEI instructions
- cregs: an optional Array of Boolean values used for configuration / condition settings
- boot: an optional stream of assembled MEI instructions
When the MarkingEngine is run, the MEI contained in the supplied boot code is compiled and run. If no boot code was supplied, then the xm(0) instruction (execute macro at index 0) is compiled and run.
All assembled binary streams are composed of 16-bit words. Compilation consists of, in Ruby parlance, unpacking these binary streams into arrays of 16-bit unsigned integers. The arrays are then processed and interpreted based on whether that particular integer is an instruction or an argument for the instruction. Each MEI method creates a set of packed 16-bit integers large enough to contain all of the information necessary to process that one unit of work. The MarkingEngine’s assemble method simply gathers arrays of these packed units of work and, in Ruby parlance, joins them into one packed binary stream.
The Ruby code to create a binary stream looks like the following:
MarkingEngine.assemble( gspush, gsfp(1), gssp(0), xyrwhz(10,10,50,50), gspop )
That block of code tells the MarkingEngine to push (save) a copy of the current graphics state onto a stack, set the fill property to the value of the contents of the palette array at index 1, set the stroke property to the value of the contents of the palette array at index 0, draw a rectangle at position 10,10 of size 50,50 and close the path and then set the graphics state to the values that were pushed onto the save stack.
Each 16-bit word can be one of the following:
- an MEI instruction
- a signed 16-bit integer value
- an unsigned 16-bit integer value
- half of a 16.16 signed fixed point value where the first half is a signed 16-bit integer and the second half is an unsigned 16-bit integer whose value is a multiple of 1/65536.
- half of an unsigned 16.16 fixed point value
- a 16-bit unsigned angular value in units called sectons, where one secton is 1/180°
There are over 190 methods in the MEI API defined across three categories: marking, graphics state, execution. These methods combine different instruction properties into easy to use mnemonics.
The layout of the 16-bit MEI instruction is divided into two bytes, the properties byte and the extended byte. The properties byte contains the instruction base while the extended byte can either contain an extension of the instruction base to further specify the instruction to be executed or one or more arguments for the instruction.
The properties byte is defined as follows:
- Bits 7,6: Instruction Category Bits:
- 00: Marking
- This value specifies a marking operation
- 01: GSX
- This value specifies either a Graphics State or eXecution instruction
- 10: XY Marking
- This value specifies a marking operation which also moves the marking position to a new absolute X,Y coordinate position
- Two arguments will follow the instruction word and will specify the new coordinate position
- 11: XYR Marking
- This value specifies a marking operation which also moves the marking position to a new relative X,Y coordinate position
- Two arguments will follow the instruction word and will specify the new coordinate position
- 00: Marking
- Bit 5: XZ Bit
- When set and the instruction is a GSX instruction: specifies that the instruction is an eXecution control instruction
- When set and the instruction is a marking instruction: specifies that the current path will be closed and ready for marking
- Bit 4: FP Bit
- When set: specifies that the following argument words, if any, are 16.16 fixed point arguments
- When clear: specifies that the following argument words, if any, are 16-bit arguments
- Bits 3-0: Instruction Specifier Bits
- Marking Instructions:
- NOP
- DX: Draw a line parallel to the X-axis in relative units, one argument will follow
- DY: Draw a line parallel to the Y-axis in relative units, one argument will follow
- DXY: Draw a line to a new X,Y coordinate in relative units, two arguments will follow
- POLY: Draw lines to new X,Y coordinates in relative units, multiple arguments will follow
- DXq: Draw a line parallel to the X-axis in relative units, the argument is embedded in the extended instruction byte
- DYq: Draw a line parallel to the Y-axis in relative units, the argument is embedded in the extended instruction byte
- DXYq: Draw a line to a new X,Y coordinate in relative units, the arguments are embedded in the nibbles of the extended instruction byte
- RWH: Draw a rectangle; two arguments follow for width and height values in relative units, direction bits for up/down, left/right (UDLR) are in the extended byte
- RRWH: Draw a rounded corner rectangle; four arguments follow: two for width/height values, two for a curvature percentage in X,Y directions, UDLR bits in the extended byte
- ELPS: Draw an ellipse; two arguments follow specifying the X and Y radii; UDLR and clockwise (CW) bits in the extended byte
- EARC: Draw an elliptical arc; five arguments follow specifying the target X,Y coordinate, the X,Y radii and the sweep angle in sectons (1/180° units), CW bit in the extended byte
- BEZ: Draw a bezier curve(s); variable arguments follow, bezier property bits in the extended byte:
- four arguments follow if the bezier is a quadratic: X,Y target coordinate position, X,Y control point
- six arguments follow if the bezier is a cubic: X,Y target coordinate position, two X,Y control points
- zero arguments follow if the bezier is a reflection of the previous bezier curve (beziers are placed on a bezier stack which is popped on each reflection)
- bezier property bits include: cubic v/s quadratic, reflection and auto-reflection, the latter allows for a bezier and a reflection bezier to be drawn in one instruction
- Graphic State Instructions:
- GSPUSH: Pushes the current graphics state onto a GS stack
- GSPOP: Sets the graphics state to the last one pushed onto the GS stack, removes the top element from the GS stack
- GSXi: sets the CTM X,Y origin point in integer units; two arguments follow
- GSXf: sets the CTM X,Y origin point in fixed point units; two arguments follow
- GSSi: sets the CTM scaling factor in integer units; two arguments follow
- GSSf: sets the CTM scaling factor in fixed point units; two arguments follow
- GSRd: sets the CTM rotation in clockwise degrees if degrees is < 256°, angle is in the extended byte
- GSRde: sets the CTM rotation in clockwise degrees if degrees is >= 256°, angle is in a following argument
- GSRs: sets the CTM rotation in clockwise sectons, angle is in a following argument
- GSRR: sets the CTM rotation to a random angle
- GSMT: sets the method by which multiple paths are rendered to the canvas: on a graphics state change or on an XY coordinate change
- GSFLUSH: forces the rendering of pending paths
- GSFP: sets the fill property to a palette entry; index is in the extended byte
- GSSP: sets the stroke property to a palette entry; index is in the extended byte
- GSFC: sets the fill property to a solid color: variable arguments follow
- GSSC: sets the stroke property to a solid color: variable arguments follow
- GSFR: sets the fill rule for overlapping path segments
- GSSWq: sets the stroke width; integer width is in the extended byte
- GSSL: sets the stroke location: on, inside or outside of the path segments
- GSLJ: sets the line join style: miter, round, bevel
- GSLC: sets the line cap style: butt, round, square
- GSSWi: sets the stroke width in integer units specified in a following argument
- GSSWf: sets the stroke width in fixed point units specified in a following argument
- eXecution Instructions:
- XM: executes a macro in the macro table, index is in the extended byte
- XMX: executes a macro in the macro table, index is in a following argument
- XMCT: executes a macro in the macro table if a condition register is true; condition register index is in the extended byte, table index is in a following argument
- XMCF: executes a macro in the macro table if a condition register is false; condition register index is in the extended byte, table index is in a following argument
- XT: executes a trap in the trap table, index is in the extended byte
- XTX: executes a trap in the trap table, index is in a following argument
- XTCT: executes a trap in the trap table if a condition register is true; condition register index is in the extended byte, table index is in a following argument
- XTCF: executes a trap in the trap table if a condition register is false; condition register index is in the extended byte, table index is in a following argument
- Marking Instructions:
While expressive, there are many limitations with this scheme. The first is that it is a vector processing engine only. No image or text support is supported directly. It is left for the trap and/or palette mechanism to use Ruby code to load images or text onto the underlying graphic. This limitation may be lifted in future expansions to the MarkingEngine class.