2025-10-02
TL;DR: I finished the FR4 art piece I designed in part one and improved svg-kerf
by
adding cubic Bézier curve support and generally increasing compliance
with the SVG path
specification.
svg-kerf
was still only partly working when I picked
this project back up, so I needed to be a little careful with my input
to make sure it didn’t break. This meant only using absolute coordinates
and line segments, which I created by manually splitting curves in
Inkscape. I managed to get it to hold together for the dozen or so paths
I needed to make the artwork, and then I set the program aside until
after the main part of the project was finished.
With the inflated paths in hand, I split them up into a few files and
tried to pack them relatively efficiently into the size of the FR4
blanks I had on hand (nominally 6 by 8 inches) minus some clearance for
work holding. I probably could have done a better job with this and
saved some material, but not much. I used svg2gcode
to
convert the paths into G-code files with appropriate settings for my
mill1 and sent them with Candle after
electrically probing the work origin to establish a reasonable Z level.
I didn’t use mesh leveling for these cuts because I didn’t need that
level of accuracy in the vertical direction.
My initial intention was to use gctk
to mirror the
copper-down parts so I could always cut with the copper side up (for
some reason?), but it choked on the input. Then I realized that the
whole piece is symmetrical, so I could just cut them all “normally” and
flip them afterward as needed. Soon I had them all cut out and ready for
post-processing.
After cutting, I washed all the parts in warm soapy water and dried them well. I read that a lemon juice and salt paste could brighten the copper, but I didn’t see any change. Maybe the parts were already bright enough that there wasn’t much for the mixture to do. If I understood chemistry better, then maybe I could say something about the expected outcome. Anyway, I continued by sanding the edges lightly to remove some sharp edges leftover from the cuts and roughly arranging the parts in their final positions.
I hadn’t really planned out how I was going to adhere the parts together. Originally I’d thought I’d cut some MDF and mount them on that, but the piece I had was warped and a little too big for the mill, so I decided to try cardstock. This went reasonably well, although I used two layers to improve rigidity slightly. The final product is still a little flexible, but this isn’t a load-bearing art piece, so I think that’s okay. I used double-sided tape to hold the parts onto the cardstock and to hold the layers together.
While the parts do fit together well for the most part, there are
places where they stick out slightly (up to about one millimeter). I
haven’t actually measured the parts to see how close they are to their
nominal dimensions, but it seems likely that if I’d skipped the
svg-kerf
step then there would have been noticeable gaps.
Maybe an improved approach would be to characterize the true kerf of
this setup and use that value instead of using half the nominal tool
width like I did this time.
I’m happy with how this came out, and it was fun to make some art. I think I’d forgotten that this is a vital and fundamental activity for humans.
svg-kerf
As a part of its internal pipeline, svg-kerf
has to
convert lists of SVG path commands into lists of points. This is called
curve flattening, and it’s necessary in this case because
svg-kerf
uses the Clipper2 library for
inflation, and Clipper2 only operates on polygonal paths.2
(That is, paths which are defined by line segments.) The SVG path
commands are well-specified and relatively simple, but there are some
syntax features that elide certain arguments to save space. My first
pass at svg-kerf
was a little hasty with respect to these
details, so I scrapped it and made another attempt with much more
attention to the spec.
The second attempt went well. The authors of the spec seem to have taken care to make the natural language easy to translate into code. Soon I had reimplemented lines and was at parity with my first attempt, except it was now more reliable and compliant. As usual, Rust made this a joyful exercise, and I think after getting it to compile I only had to correct one small logic bug. It’s amazing how making a throwaway attempt at solving a problem in code can sometimes be the best guide for the subsequent “real” solution.
At this point I was able to go further. The SVG path spec defines several curve commands, and among these is the cubic Bézier. Inkscape seems to use these all over, which I suppose may be because all the other curves can be viewed as special cases of the cubic Bézier. I wasn’t really sure how to achieve curve flattening here. A simple approach would be to choose some number of line segments and sample the curve at points with equal spacing in the parameter . This fails to account for local curvature, though, so sharp turns aren’t represented faithfully and straight areas are encoded redundantly.
I sat down with pen and paper and tried to design something that might work. Here’s what I came up with:
This description is sloppy, but I hope it gives the general shape of the algorithm. The output is guaranteed not to deviate from the original curve by more than the threshold, and areas of the curve that are straight get fewer segments. I think of it as sticking a tack in the string that forms a line segment and pulling it onto the curve, which seems like the fastest way to minimize its error. In my implementation, I just sweep over the segment’s start and end points with a small to find something close to the maxmimum deviation, but this is probably slower than it could be. I imagine with some clever math it could be shown that in certain cases binary search is sufficient, or maybe there’s even an analytic solution if the curve is known. One advantage of this algorithm is that it works for any single-parameter function on (I think).
I did some reading after building this solution and found the Ramer-Douglas-Peucker algorithm, which is similar to my idea except it operates explicitly on line segments in the input. I think my solution is equivalent to the RDP algorithm if you consider the input curves as being discrete maps over the floating point values of . I was pretty sure my approach wasn’t novel because it seems like an important operation that would have been solved by now.
My solution could definitely be faster, and it also generates probably too many points, but for now it’s good enough. It was easy enough to build, and it was also pretty easy to integrate it with the cubic Bézier path handler in my existing code. I don’t think it would be too hard to do the same thing for the other path commands, but I didn’t bother because I didn’t need them for my use case. Over the past few projects, I’ve had fun building things with the expectation that they won’t be complete or ideal by some external metric. Paying more attention to the ends than the means helps balance out my neuroticism a little, and it’s soothing.
In case you need or want them, here are the arguments I
passed:
--begin "G21 G90 G17 G0 Z2 F100 M3 S10000 G4 P1.5" --on "G1 Z-2 F60" --feedrate 120 --off "G0 Z2" --end "M5 G4 P1.5 M30"
↩︎
It’s possible to compute offset curves directly from the path commands, but it seemed easier to take the curve flattening approach instead. Maybe this would be an interesting thing to explore in the future.↩︎