Hey zenwebb, I'll help if I can.
You're right it's somewhat fractured, particularly the server code, and no documentation. You should be working from the most recent codebase though - there's no roving in the new code, and the new controller code is much more clearly separated by function (model / presentation).
Roving was actually the feature that I was trying to use the SD card with - I had it just drawing endlesslessly from one random point to another, great big swirls and wiggles, quite impressive actually, but it was planned that it would be continuously calculating the density under the pen, and dropping or raising it (the pen) accordingly - so that the image emerged from scribbles and gradually became more and more distinct.
I'll briefly describe the elements of the program:
Controller
polargraphcontroller_zoom.pde - This is concerned with drawing the contents of each tab, and all the serial communication between board and computer, as well as the "raw" parts of the keyboard and mouse input.
tabSetup & controlsSetup - Contains all the code to initialise the controls (buttons, numberboxes, toggles etc), setting max, min values, initial values.
controlsActions - These are the methods that get hit when any control is activated, the name of the method is the same as the name of the control. That is, pressing the button with the name "button_mode_renderSquarePixel" will hit the method button_mode_renderSquarePixel() in this.
Rectangle - This is a helper class that describes a rectangular area, along with .getWidth(), getLeft(), surrounds() type accessor methods.
Panel - This is a class that can hold a collection of controls, along with their positions and sizes. At least one of these Panels is created for each tab.
Machine - This is a class that models a polargraph machine, this is the essential part of the application. It has a couple of properties - machine size, page size, image size, position etc, along with a couple of utility methods like asNativeCoords(), asCartesianCoords(), inMM(), inSteps(). It also contains the algorithm that converts from cartesian -> polargraph coords, and the root method for extracting all the pixels.
DisplayMachine - This subclasses Machine and is a kind of machine that knows how to draw itself on screen, translate on-screen positions to on-machine positions.
drawing - This contains most of the code that builds the ASCII messages that get put into the queue, including working out the order that big sets of pixels should get sent in (which corner to start in, what direction to go in etc). Most of the methods are called "send...()" because they send the commands.
I have been fairly sloppy about globals here, because the Processing IDE treats everything as one big class anyway. I have tried to encapsulate where possible, but it's all a bit of a moot point with Processing - it's just not made to do that. You might find it easier to read if you've loaded it into a more featured environment like eclipse that has method trees and will let you click through the method calls.
You've almost got the process right, but the image doesn't come first. The main "thing" that the controller does is hold an instance of Machine - a model of a polargraph machine. It is the logical model. All the rest is presentation really.
The main pixelly method here is Machine.getPixelsPositionsFromArea() that returns a Set<PVector> containing the cartesian positions of all the polargraph pixels in the area specified.
/** This takes in an area defined in cartesian steps,
and returns a set of pixels that are included
in that area. Coordinates are specified
in cartesian steps. The pixels are worked out
based on the gridsize parameter. d*/
Set<PVector> getPixelsPositionsFromArea(PVector p, PVector s,
float gridSize, float sampleSize)
{
// work out the grid
setGridSize(gridSize);
float maxLength = getMaxLength();
float numberOfGridlines = maxLength / gridSize;
float gridIncrement = getMaxLength() / numberOfGridlines;
List<Float> gridLinePositions = getGridLinePositions(gridSize);
Rectangle selectedArea = new Rectangle (p.x,p.y, s.x,s.y);
// now go through all the combinations of the two values.
Set<PVector> nativeCoords = new HashSet<PVector>();
for (Float a : gridLinePositions)
{
for (Float b : gridLinePositions)
{
PVector nativeCoord = new PVector(a, b);
PVector cartesianCoord = asCartesianCoords(nativeCoord);
if (selectedArea.surrounds(cartesianCoord))
{
if (!isChromaKey(cartesianCoord))
{
if (sampleSize >= 1.0)
{
float brightness = getPixelBrightness(cartesianCoord, sampleSize);
nativeCoord.z = brightness;
}
nativeCoords.add(nativeCoord);
}
}
}
}
return nativeCoords;
}
So rather than iterating through pixels of the image, this iterates through a grid of possible polargraph pixel positions, and for each one works out the cartesian position. The grid is based on the gridSize (formerly rowSize). Once the cartesian position has been worked out, then the ones that fall outside the desired area are chucked out. When the pixel falls on top of the image, then the brightness of the underlying image pixel is extracted (or an average of a few pixels). I should really do brightness(pixelColor), but I do red(pixelColor) instead. Then I put that brightness value into a PVector along with the native positions. I use the z value to store the brightness, and the x and y to store the a-string-length and b-string-length (distance from motor A and distance from motor B).
This is basically re-rasterising in the new coordinates system.
DisplayMachine has something very similar but it re-examines the set produced above and converts it all to the cartesian coords and scaled it to be displayed on the screen.
When you hit "render square" or somesuch, it fires a method in controlsAction:
void button_mode_renderSquarePixel()
{
if (isBoxSpecified())
{
// get the pixels
Set<PVector> pixels = getDisplayMachine()
.extractNativePixelsFromArea(getBoxVector1(),
getBoxVectorSize(), getGridSize(), sampleArea);
sendSquarePixels(pixels);
}
}
Which hits up DisplayMachine to get the pixels from the selected area in the native machine coords. It then sends this big collection of pixels (PVectors) to sendSquarePixels() which is in the drawing file.
void sendSquarePixels(Set<PVector> pixels)
{
sendPixels(pixels, CMD_DRAWPIXEL, DRAW_DIR_SE, getGridSize(), false);
}
Which itself hits the generalised pixel sorting/sending method sendPixels(). This sorts the pixels into rows, then sorts each row alternately forwards and backwards, and eventually iterates throught this sorted collection of pixels and for each one, adds a new command to the command queue.
The command queue is just as you've seen - simple. Actions you do in the controller cause commands to be added to the end of it, and the hardware asks for them one by one. It can be paused or cleared or saved or loaded.
What the pixel looks like on the paper is decided entirely by the firmware. The controller says "use a specific algorithm to render a patch with x brightness, make it y steps wide, and place it at z position", but It doesn't know any more than that. The controller can see the big picture, but doesn't know how the details will be rendered.
The hardware can only work on one command at a time, so it can't look ahead or really do any processing. It knows the pen's current location and size, and it knows where it's next task is, and works out how many waves can fit into that area to get the desired brightness and what direction it needs to go to get there.
What I'm working on now is making the hardware modal - so I can switch it into a "buffering" mode and it'll then store the commands locally rather than executing them immediately. Switching to "play" mode will start drawing from it's internal memory. No reason why this is a challenge, but I'm reluctant to build features that don't work on the regular ATMEGA328s.
Let me know if there's anything I can illuminate, cheers!
Sandy Noble
|