Saturday, June 22, 2013

Frankenplotter - Cubic Bezier curves in SVG and Java, still getting drift

I went ahead and coded up the Bezier curve processing code.  I get it now.

Quadratic Bezier curves are like string art.

Cubic Bezier curves are like string art within string art.

The SVG involved had "c" (lowercase "c") commands, which are relative-positioned curves.  The data starts with a "c" and then one or more triplets of x,y coordinate pairs.  So it's like this:1.  You're at current x,y
2.  You see a "c"
3.  You get three x-y pairs of values -- I'll call them dx1,dy1 through dx3,dy3 -- that are all relative to current x,y.  So in the end, you have
(x0,y0) being current x,y
(x1,y1) being current x,y plus dx1, dy1
(x2,y2) being current x,y plus dx2, dy2
(x3,y3) being current x,y plus dx3, dy3
4. The four control points allow for computation of the cubic Bezier.
5. Draw the curve.  Pseudocode:
For time t ranging from 0.0 to 1.0
  At each time t, you get a relative position along the pt0..pt1, pt1..p2, and pt2..pt3.  So, when t is 0.1, you go one tenth of the distance along pt0..pt1, one tenth along pt1..pt2, and one tenth from pt2..pt3.  I named these interim points q0..q2 in the code, probably because that's how they were named on the Wikipedia page.
  Then, do it again.  Find points a "t"-relative distance between q0..q1 and q1..q2.  Those form points r1 and r2.  Ok, I'm not great at numbering my variable names.
  Then, do it one more time.  Find the "t"-relative distance between r1 and r2, and you end up with newx,newy.
  Draw from the current position to newx,newy
Go to the next "t" value, and make sure "t" ranges all the way from 0..1, including the endpoint t=1.

5. When you're done with that triplet,  update the system's "current x,y" value to be the same value as seen in the final control point, so currentx = currentx + dx3, and currenty = currenty + dy3.

If you see multiple sets of triplets of points, it's what's called a polybezier.

To make sure I got it right, I wrote up some quick code in Java (probably should have done this in the first place...) to draw vectors on screen, rather than send them to the Frankenplotter.

I did the coding brute-force.  From a microcode perspective, it's faster to pre-compute the steps along the control points so you don't do multiplying and dividing so much, at least for that portion.  Some slightly higher-level math could be added in to do similar "add instead of muldiv" approaches for q and r.

I also set up the code such that all parameters to the function are absolute values for ease of reading.

The code assumes you've set up a Graphics object on a Panel somewhere.

It also assumes some control booleans have been set up beforehand.
drawBezierControlVectors - draws the control point vectors in red
plotBezierControlVectors - generates absolute-position L commands (line-to xabs,yabs)
plotSendBezierControlVectors - generates some special C,D,E commands for the Frankenplotter so it can do the Bezier drawing itself.  More on that later.
drawBezier - draws the Bezier curve on screen in blue
plotBezier - sends L commands for all the interim points of the Bezier curve.

On other web pages you'll see that one technique you can employ is to choose the step size when marching t from 0.0 to 1.0, typically making it dependent on the hypotenuse sizes of the control vectors.  You can also split the control vectors into sub-Beziers for that purpose.  In doing so, you can avoid large vectors on large legs of a Bezier, and also avoid having too many microsteps on small control vector sizes.  I might do that still, but I took the empirical approach of looking at various step sizes on screen to see how many steps it would take to get a good-looking result.  For my drawing, I got about the same visual rendition at 4 steps and 10 steps.

Here's the code:

    private void bezAbs (Graphics g, double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3) {
        double dx1 = x1-x0;
        double dx2 = x2-x1;
        double dx3 = x3-x2;
        double dy1 = y1-y0;
        double dy2 = y2-y1;
        double dy3 = y3-y2;
        double mycurx = x0;
        double mycury = y0;
       
        if (drawBezierControlVectors) {
            g.setColor(Color.RED);
            drawLine(g,x0,y0,x1,y1);
            drawLine(g,x1,y1,x2,y2);
            drawLine(g,x2,y2,x3,y3);
            g.setColor(Color.BLUE);
        }
        if (plotBezierControlVectors) {
            fout.println(pointStr("L",x1,y1));
            fout.println(pointStr("L",x2,y2));
            fout.println(pointStr("L",x3,y3));
        }
        if (plotSendBezierControlVectors) {
            fout.println(pointStr("C",x1,y1));
            fout.println(pointStr("D",x2,y2));
            fout.println(pointStr("E",x3,y3));
        }
       
        int bezSteps = 4;
        for (int ti=0; (drawBezier || plotBezier) && ti<=bezSteps; ti++) {
            double t = (double)ti / (double)bezSteps;
            double q0x = x0 + t * dx1;
            double q0y = y0 + t * dy1;
            double q1x = x1 + t * dx2;
            double q1y = y1 + t * dy2;
            double q2x = x2 + t * dx3;
            double q2y = y2 + t * dy3;
            double qdx1 = q1x - q0x;
            double qdy1 = q1y - q0y;
            double qdx2 = q2x - q1x;
            double qdy2 = q2y - q1y;
            double r1x = q0x + t * qdx1;
            double r1y = q0y + t * qdy1;
            double r2x = q1x + t * qdx2;
            double r2y = q1y + t * qdy2;
            double newx = r1x + t * (r2x-r1x);
            double newy = r1y + t * (r2y-r1y);

            if (drawBezier) {
                drawLine(g,mycurx,mycury,newx,newy);
            }
            if (plotBezier) {
                fout.println(pointStr("L",newx,newy));
            }

            mycurx = newx;
            mycury = newy;
        }
    }





And here are screenshots

Bezier control vectors in red, blue are the few LineTo straight line commands.  The drawing is primarily comprised of cubic Bezier curves.

Bezier controls overlaid with blue generated Bezier curve vectors

Bezier curves without control vectors showing
Note on the lower, left side of the picture, and in the center area, the result is a much smoother curve. 

Generating this on screen gave me a proof point that the Bezier curve code was working.  So as an initial test, I added code to write output to another vector file, and passed through appropriate values for L and M (Line-to and Move-to, absolute) commands.  Then, I added the code for the plotBezierControlVectors, making it generate more L commands for the Bezier sub-vectors.

I did notice that in the original SVG, there are "z" commands, which I took to mean "close curve to original x,y location you started at the beginning of the polybezier.  But I added that into the code and it just made things offset all over the place.  So at this point, I treat the "z" command as an "I'm done sending you Bezier point triplets", which for my purposes means I can just ignore them.

I ran the generated file of L and M commands (prefixed with an S4.5 scale and suffixed with a Z to finish the drawing, and disable motors and solenoid), and it looked promising, but took a long time because of the RXTX send-acknowledge sequence with every little vector.

So, I changed the code to send the Bezier commands to the plotter, and shifted the Bezier drawing computation to the Arduino sketch.  To make things consistent with other commands, I chose one character each for the absolute-position Bezier control points 1,2,3 and called those commands C, D, and E.  All three cause storage of the control points in persistent variables, and upon receiving the E command, the sketch puts the pen down, and draws the Bezier curve.  That was much faster.  The resulting drawing looks nicer, as you'd expect from seeing the on-screen renditions.

Still, I am getting drift problems.  I determined that some of it was mathematical, but some of it is mechanical.  The mathematical part was diagnosed by sending circle=720 commands to the plotter at 400 units from center, thus generating a ton of very small vectors.  I was disappointed to find that I was getting consistent X-axis offsets when doing that.  I restructured a lot of the sketch code to have a clean separation between a "virtual" coordinate world, and a "physical" coordinate world.  The physical world is all integer-based as it represents the number of steps moves by the plotter, whereas the virtual world is in double-precision floating point.  Once I did that, the X-axis drift went away.  I can only assume there was some similar Y-axis drift occurring.

But, the Y-axis still showed drift. 
Y-axis drift, drawing stopped.

I tightened the belt on the Frankenprinter carriage and clamped it down better, and found that a good portion of the offset problem went away, which strongly suggests that what I'm seeing is mechanical, not a problem in the code.
Same code, but with Frankenprinter carriage belt and idle pulley tightened.

One thing I am considering doing is to heat-sink the motor, but I think there's more going on than that, and I can't really rely on the motor to get me what I want.  So I think my next step is to go to the Teco motor, anti-backlash coupler, and a 5/8"-18 threaded rod as a lead screw, and somehow have it connect to the printer carriage so that it moves on an extended worm gear system.  That'll give me 1.8 deg turns (200 steps per rev), 18 full steps per inch, therefore 3600 full steps per inch for resolution, or 2.77e-4 inches per step, or 0.00705555 mm/step at full step.  At half-step, I'd get half that, or 0.00352777 mm/step.  More importantly, I should end up with a build that I can trust for moving the carriage as far as it's supposed to move, though in using steppers without feedback, I don't really have anything to check to make sure it got there.  (I want to mess around with the cool rotor mechanism I found in the H-P 720 printer to see how it'd work.)

I've already disassembled the Frankenprinter so it's just a base, rod, and carriage now -- no pen assembly, motor, gears, or belt -- and am measuring it at points to see how it might accommodate a lead-screw-type mechanism.

No comments:

Post a Comment