The Smell of Molten Projects in the Morning

Ed Nisley's Blog: Shop notes, electronics, firmware, machinery, 3D printing, laser cuttery, and curiosities. Contents: 100% human thinking, 0% AI slop.

Category: Software

General-purpose computers doing something specific

  • Tree Frog Marquetry: FAIL

    Tree Frog Marquetry: FAIL

    I thought this critter would look great in marquetry:

    Tree frog - on trash can lid
    Tree frog – on trash can lid

    Posterizing the colors to represent a few shades in my Little Box o’ Veneers simplified the problem:

    Tree frog - posterized
    Tree frog – posterized

    Applying LightBurn’s Trace tool to the various shades produced vector outlines, which I then collected together based on the veneer they should come from:

    Tree Frog vector patterns
    Tree Frog vector patterns

    Which seemed similar to my hand-drawn doodles on a larger image:

    Tree frog - sketch vs chipboard
    Tree frog – sketch vs chipboard

    Before committing to actual veneers, though, I cut the shapes from spraypainted chipboard on a small scale, which showed why this wasn’t going to work:

    Tree Frog - auto-trace chipboard
    Tree Frog – auto-trace chipboard

    It’s facing the other way because I cut the chipboard from the back side, so as to keep the colors reasonably clean and bright.

    Contrary to what I initially thought, the automagic tracing routine generates different nodes along a boundary between two colors depending on which side is selected by the color range. Because the nodes (and control points) don’t match exactly, adjacent pieces will have different border shapes and won’t quite match up. The missing pieces at the frog’s rump simply did not fit after the other parts soaked up all the tolerances in between.

    So (I think) a better way to do this requires carefully hand-tracing the borders, then using the same path (all the nodes) for adjoining pieces. This mean duplicating the borders for each of the pieces: tedious bookkeeping and layer manipulation.

    More study is needed …

  • Downgrading Yubikey-Manager

    It seems that Manjaro’s 5.0.0-1 version of the yubikey-manager crashes due to inscrutable errors, with the effect of not letting me use it to sign in at all the sites I’d set up to use TOTP authentication.

    If the previous version (4.0.9-1) were still in the pacman cache, then downgrading would be straightforward:

    sudo pacman -U /var/cache/pacman/pkg/firefox-64.0.2-1-x86_64.pkg.tar.xz

    Regrettably, I had recently cleaned things up and flushed the cache, so I had to fetch the package (and its signature) from the “y” directory of the Arch archive, then install it:

    sudo pacman -U /tmp/yubikey-manager-4.0.9-1-any.pkg.tar.zst
    
    loading packages...
    warning: downgrading package yubikey-manager (5.0.0-1 => 4.0.9-1)
    resolving dependencies...
    looking for conflicting packages...
    
    Packages (1) yubikey-manager-4.0.9-1
    
    Total Installed Size:   1.11 MiB
    Net Upgrade Size:      -0.13 MiB
    
    :: Proceed with installation? [Y/n] Y
    <<< snippage >>>

    Whereupon It Just Worked™ again.

    I expect someone more experienced than I will have long since filed a bug report / sent a pull request / whatever, because I have little idea how to do any of that. The next upgrade should work just fine.

  • LightBurn Grayscale Image Sampling

    LightBurn Grayscale Image Sampling

    Take a sine-wave grayscale pattern with one cycle across 10 pixels at 254 dpi = 10 pixel/mm:

    Sine bars - 10 cycles
    Sine bars – 10 cycles

    Then if you tell LightBurn to engrave the pattern with a line-to-line (vertical) spacing of 127 dpi = 5 pixel/mm, it will sample every other pixel in each row, producing a rather peculiar sine-ish wave:

    Tube Current - analog bandwidth - 10 sine - 25mm-s - beam off - 127dpi
    Tube Current – analog bandwidth – 10 sine – 25mm-s – beam off – 127dpi

    You must engrave at 254 dpi = 10 pixel/mm in order to get all the pixels in the output stream:

    Tube Current - analog bandwidth - 10 sine - 25mm-s - beam off - 254dpi
    Tube Current – analog bandwidth – 10 sine – 25mm-s – beam off – 254dpi

    That still looks gnarly, but it’s more along the lines of what the coarse 10 samples / cycle pattern calls for.

    The risetime for each of those steps is on the order of 2 ms, so the controller’s analog output bandwidth isn’t much better than 150-ish Hz.

    Close examination of the bar pattern shows the end of the first cycle really does hit exactly 0% intensity where the controller raises L-ON (magenta trace) to force the output current to zero. The other minima remain a few percent above zero and cannot be squashed flat.

    Today I Learned: LightBurn enforces square pixels at the line spacing distance for grayscale engraving.

    I think this means you must resize / resample the grayscale image to match the engraving line spacing, because LightBurn could take the nearest adjacent pixel or average two adjacent pixels if its horizontal sampling doesn’t match the image resolution.

  • Lid Box

    Lid Box

    Mary reuses empty sour cream / ricotta cheese / cottage cheese to freeze / store garden produce, which results in a need to store their lids:

    Lid box - filled
    Lid box – filled

    It’s made from 1.5 mm chipboard, which seems both sturdy enough for the purpose and sufficiently stylin’ for life in a middle drawer.

    A bead of Elmer’s yellow wood glue along the tops of meshing fingers (which then hits the bottom of the opposing slots) holds the joints together, with a quartet of steel blocks + magnets ensuring perpendicularity during curing:

    Lid box - gluing
    Lid box – gluing

    The glue cures to a transparent skin, so it doesn’t look nearly as awful as you might think. Besides, being inside with lids all over, nobody will ever see the overage. Right?

    The box pattern comes from the wonderful boxes.py as a magic URL:

    https://festi.info/boxes.py/UnevenHeightBox?FingerJoint_angle=90.0&FingerJoint_style=rectangular&FingerJoint_surroundingspaces=2.0&FingerJoint_edge_width=1.0&FingerJoint_extra_length=0.0&FingerJoint_finger=2.0&FingerJoint_play=0.0&FingerJoint_space=2.0&FingerJoint_width=1.0&Grooved_arc_angle=120&Grooved_gap=0.1&Grooved_interleave=0&Grooved_inverse=0&Grooved_margin=0.3&Grooved_style=arc&Grooved_tri_angle=30&Grooved_width=0.2&bottom_edge=f&x=70&y=80&outside=0&height0=40&height1=40&height2=60&height3=60&lid=0&lid_height=0&edge_types=eeee&thickness=1.5&format=svg&tabs=0.0&debug=0&labels=0&reference=0&inner_corners=corner&burn=0.04&render=0
  • Manjaro Linux: TOTP PSA

    Manjaro Linux: TOTP PSA

    I set up my pobox.com account set up with two-factor authentication through my Yubikey, so logging in requires my user ID, password, and a Time-based One-time Password generated through the Yubikey Authenticator program. A few weeks ago, pobox occasionally rejected the TOTP and it eventually became a hard failure. Oddly, other sites I’ve set up with TOTP 2FA continued to work fine.

    My initial trouble report:

    The last couple of times I’ve tried to sign in, the usual TOTP copy-n-paste from my Yubikey authenticator has failed.

    Up to that point, it worked flawlessly.

    Manually typing the TOTP also fails.

    I have reset my (complex!) password to no avail; I use Firefox’s password manager to fill it in.

    I do have a set of lockout codes, but they’re a solution to a different problem.

    Given the constant updates to Firefox (102.0.3), it’s almost certain the hole is in my end of the boat. I have disabled all the usual ad blocking for pobox.com, although there may be other domains I’ve overlooked.

    Other than that, my email seems to be working just fine …

    Any suggestions on how to proceed? (Obviously, I’m not going to be able to sign on to look at the ticket.)

    Thanks …

    This is the fastest I’ve ever reached Tier 2:

    We’re happy to help you with this. I’ve escalated your ticket to our Tier 2 agents, as they are best suited to assist with this issue.

    There is nothing like a good new problem to take your mind off all your old problems:

    I’ve had a chat with our Tier 2 agents about this and they’ve suggested I escalate it to our developers to have a look at.

    Somewhat later:

    I am afraid to say that our developers were unable to find any clear reason as to why your Yubikey failed.

    Yubikey devices verify by connecting with Yubikey’s server, and it is possible that this connection failed.

    Can you please try using the Yubikey again to see if the issue is still occurring?

    If it’s still failing, can you please try adding a new Yubikey device to see if it works?

    Of course, the problem didn’t magically Go Away, but I did more experimentation and figured out where the hole was in my end of the boat:

    Ah-HA! It’s a PEBKAC error!

    For unknown reasons, this PC was not set for automatic NTP time updates(*). Its time had drifted (presumably since I installed it back in June 2021) and was now 58 seconds behind real time, exceeding pobox’s tolerance.

    Other websites apparently allow a few more seconds of slop before disallowing a TOTP, so I had not yet run afoul of their limit.

    Some lesser-used sites threw me out, however, but I had not looked beyond the most common sites.

    The default TOTP interval is 30 seconds, so perhaps pobox allows only ±1 interval and the other sites allow ±2? Frankly, I think pobox has it right: everybody else prioritizes customer sat over security.

    Got the clock set correctly and, gosh, TOTP works fine.

    Mark it solved, but definitely add “Soooo, is your PC’s clock set for automatic updates?” to the debugging protocol.

    Thanks …

    (*) I’ve installed all of the boxen here and would not ever have picked “Yeah, sure, I want to dink with the clock.”

    The solution looks like this:

    Manjaro Time and Date Settings - Auto Set
    Manjaro Time and Date Settings – Auto Set

    Which was unchecked on this PC.

    Of course, systemd has long since subsumed NTP, making everything I thought I once knew obsolete: now it’s handled by timesyncd.

    How you make sure time synchronization is enabled goes like this:

    $ systemctl status systemd-timesyncd.service
    ● systemd-timesyncd.service - Network Time Synchronization
         Loaded: loaded (/usr/lib/systemd/system/systemd-timesyncd.service; enabled; preset: enabled)
         Active: active (running) since Thu 2022-08-25 06:49:31 EDT; 10h ago
           Docs: man:systemd-timesyncd.service(8)
       Main PID: 355 (systemd-timesyn)
         Status: "Contacted time server 23.157.160.168:123 (2.manjaro.pool.ntp.org)."
          Tasks: 2 (limit: 19063)
         Memory: 2.2M
            CPU: 188ms
         CGroup: /system.slice/systemd-timesyncd.service
                 └─355 /usr/lib/systemd/systemd-timesyncd
    
    Aug 25 06:49:31 shiitake systemd[1]: Starting Network Time Synchronization...
    Aug 25 06:49:31 shiitake systemd[1]: Started Network Time Synchronization.
    Aug 25 06:50:12 shiitake systemd-timesyncd[355]: Timed out waiting for reply from 162.159.200.123:123 (2.manjaro.pool.ntp.org).
    Aug 25 06:50:12 shiitake systemd-timesyncd[355]: Contacted time server 23.157.160.168:123 (2.manjaro.pool.ntp.org).
    Aug 25 06:50:12 shiitake systemd-timesyncd[355]: Initial clock synchronization to Thu 2022-08-25 06:50:12.850444 EDT.
    

    If it’s enabled and running, then it’s all good.

    Whereupon all my TOTP passwords began working again.

    I checked two other Manjaro systems: one had auto updates enabled, one didn’t. I have no explanation.

  • Layered Paper Coaster: GCMC Test

    Layered Paper Coaster: GCMC Test

    A few more attempts at layered paper construction, done with plain white Art Paper of various vintages:

    Layered paper coasters
    Layered paper coasters

    The middle one comes from a version of the original GCMC marquetry shape generator, tweaked to produce just the frame SVG, called by a Bash script to change the sash width, and imported into LightBurn for laser control:

    LightBurn - Marq-6-0.6-0.0mm
    LightBurn – Marq-6-0.6-0.0mm

    I generated the plain disk for the bottom by deleting all the inner shapes.

    The left and right coasters use LightBurn’s Offset tool to reduce the size of the interior holes on successive layers:

    LightBurn - Marq-8-0.40-20.0mm-Layers
    LightBurn – Marq-8-0.40-20.0mm-Layers

    Although the GCMC version turned out OK, you’ll note it lacks the central disk, as I was unwilling to tweak the code enough to make the disk diameter vary with the kerf width.

    Applying the LB Offset tool requires selecting only the inner shapes (it has an option to ignore the inner shapes) and applying the appropriate offset. Because the tool remembers its previous settings, it’s straightforward to step the offset from 1.0 mm to 7.0 mm on successive patterns.

    Applying glue (from a glue stick!) to the bottom of each disk, aligning them atop each other, and pressing them together becomes tedious in short order. If I had to do a lot of these, I’d be tempted to add three wings (not at 120° angles!) around the perimeter with holes for pegs, then stacking the layers in a fixture to ensure good alignment. A polygonal perimeter would simplify trimming the tabs.

    Spray adhesive might be faster, but each layer would have sticky edges and the finished coaster would become a dust collector par excellence.

    I like the overall effect, but …

    The OpenSCAD source code as a GitHub Gist:

    #!/bin/bash
    # Layering paper cutouts
    # Ed Nisley KE4ZNU – 2022-08-21
    Flags='-P 4 –pedantic' # quote to avoid leading hyphen gotcha
    SVGFlags='–svg –svg-no-movelayer –svg-opacity=1.0 –svg-toolwidth=0.2'
    # Set these to match your file layout
    ProjPath='/mnt/bulkdata/Project Files/Laser Cutter/Coasters/Source Code'
    LibPath='/opt/gcmc/library'
    ScriptPath=$ProjPath
    Script='Marquetry Layers.gcmc'
    [ -z "$1" ] && leaves="6" || leaves="$1"
    [ -z "$2" ] && aspect="0.50" || aspect="$2"
    [ -z "$3" ] && center="0.0mm" || center="$3"
    numlayers=8
    sashmin=2.0
    sashstep=2.0
    sashmax=$(echo "$sashmin+$sashstep*($numlayers-1)" | bc)
    echo min: $sashmin step: $sashstep max: $sashmax
    for sash in $(seq $sashmin $sashstep $sashmax) ; do
    fn=Marq-$leaves-$aspect-$center-S$sash.svg
    echo Output: $fn
    gcmc $Flags $SVGFlags –include "$LibPath" \
    -D "NumLeaves=$leaves" -D "LeafAspect=$aspect" -D "CenterDia=$center" \
    -D "Sash=${sash}mm" \
    "$ScriptPath"/"$Script" > "$fn"
    done
    view raw layers.sh hosted with ❤ by GitHub
    // Marquetry Layers
    // Ed Nisley KE4ZNU
    // 2022-08-21 layered paper test piece
    layerstack("Frame","Leaves","Rim","Base","Center","Tool1"); // SVG layers map to LightBurn colors
    //—–
    // Library routines
    include("tracepath.inc.gcmc");
    include("varcs.inc.gcmc");
    FALSE = 0;
    TRUE = !FALSE;
    //—–
    // Command line parameters
    // -D various useful tidbits
    // add unit to speeds and depths: 2000mm / -3.00mm / etc
    if (!isdefined("OuterDia")) {
    OuterDia = 120.0mm;
    }
    if (!isdefined("CenterDia")) {
    CenterDia = 20.0mm;
    }
    if (!isdefined("NumLeaves")) {
    NumLeaves = 8;
    }
    if (!isdefined("Sash")) {
    Sash = 4.0mm;
    }
    if (!isdefined("LeafAspect")) {
    LeafAspect = 0.50;
    }
    // Leaf values
    LeafStemAngle = 360.0deg/NumLeaves; // subtended by inner sides
    LeafStemHA = LeafStemAngle/2;
    LeafOAL = OuterDia/2 – Sash – (Sash/2)/sin(LeafStemHA);
    LeafWidth = LeafAspect*LeafOAL;
    L1 = (LeafWidth/2)/tan(LeafStemHA);
    L2 = LeafOAL – L1;
    // message("Len: ",LeafOAL," L1: ",L1," L2: ",L2);
    LeafTipHA = to_deg(atan(LeafWidth/2,L2)); // subtended by outer sides
    LeafTipAngle = 2*LeafTipHA;
    // message("Width: ",LeafWidth);
    // message("Tip HA: ",LeafTipHA);
    LeafID = CenterDia + 2*Sash;
    LeafOD = LeafID + LeafOAL;
    // message("ID: ",LeafID," OD: ",LeafOD);
    // Find leaf and rim vertices
    P0 = [(Sash/2) / sin(LeafStemHA),0.0mm];
    m = tan(LeafStemHA);
    y0 = -(Sash/2) / cos(LeafStemHA);
    if (CenterDia) { // one sash width around center spot
    a = 1 + pow(m,2);
    b = 2 * m * y0;
    c = pow(y0,2) – pow(LeafID/2,2);
    xp = (-b + sqrt(pow(b,2) – 4*a*c))/(2*a);
    xn = (-b – sqrt(pow(b,2) – 4*a*c))/(2*a);
    y = xp*tan(LeafStemHA) – (Sash/2) / cos(LeafStemHA);
    P1 = [xp,y];
    if (FALSE) {
    message("a: ",a);
    message("b: ",b);
    message("c: ",c);
    message("p: ",xp," n: ",xn," y: ",y);
    }
    }
    else { // force sharp point without center spot
    P1 = P0;
    }
    P2 = P0 + [L1,LeafWidth/2];
    P3 = P0 + [LeafOAL,0mm];
    P4 = P3 + [Sash/sin(LeafTipHA),0.0mm];
    P5r = P4.x * sin(LeafTipHA) / sin(180deg – LeafStemHA – LeafTipHA);
    P5 = rotate_xy([P5r,0.0mm],LeafStemHA);
    P6 = rotate_xy(P4,LeafStemAngle);
    t2 = pow(tan(-LeafTipHA),2);
    a = 1 + t2;
    b = -2 * t2 * P4.x;
    c = t2 * pow(P4.x,2) – pow(P3.x,2);
    xp = (-b + sqrt(pow(b,2) – 4*a*c))/(2*a);
    xn = (-b – sqrt(pow(b,2) – 4*a*c))/(2*a);
    y = (xp – P4.x)*tan(-LeafTipHA);
    // message("p: ",xp," n: ",xn," y: ",y);
    P4a = [xp,y];
    P6a = rotate_xy(P4a,LeafStemAngle – 2*atan(P4a.y,P4a.x));
    if (FALSE) {
    message("P0: ",P0);
    message("P1: ",P1);
    message("P2: ",P2);
    message("P3: ",P3);
    message("P4: ",P4);
    message("P4a: ",P4a);
    message("P5: ",P5);
    message("P6: ",P6);
    message("P6a: ",P6a);
    }
    // Construct paths
    LeafPoints = {P1,P2,P3,[P2.x,-P2.y],[P1.x,-P1.y]};
    if (P0 != P1) {
    StemArc = varc_ccw(P1 – [P1.x,-P1.y],LeafID/2);
    StemArc += [P1.x,-P1.y];
    LeafPoints += StemArc;
    }
    RimChord = length(P4a – P6a);
    RimThick = OuterDia/2 – Sash – length(P5);
    RimPoints = {P4a,P5,P6a};
    RimArc = varc_cw(P4a – P6a,P4a.x);
    RimArc += P6a;
    RimPoints += RimArc;
    //— Lay out the frame
    linecolor(0xff0000);
    layer("Frame");
    if (CenterDia) {
    goto([CenterDia/2,0mm]);
    circle_cw([0mm,0mm]);
    }
    repeat(NumLeaves;i) {
    a = (i-1)*LeafStemAngle;
    tracepath(rotate_xy(LeafPoints,a));
    }
    repeat(NumLeaves;i) {
    a = (i-1)*LeafStemAngle;
    tracepath(rotate_xy(RimPoints,a));
    }
    linecolor(0xff0000);
    goto([OuterDia/2,0]);
    circle_cw([0mm,0mm]);

  • Coaster Generator: Rounded Petals

    Coaster Generator: Rounded Petals

    Making a coaster with petals from the NBC peacock turned out to be trickier than I expected:

    Chipboard coaster - rounded petals
    Chipboard coaster – rounded petals

    Protracted doodling showed that I cannot math hard enough to get a closed-form solution gluing a circular section onto the end of those diverging lines:

    Chipboard coaster - rounded petal geometry doodle
    Chipboard coaster – rounded petal geometry doodle

    However, I can write code to recognize a solution when it comes around on the guitar.

    Point P3 at the center of the end cap circle will be one radius away from both P2 at the sash between the petals and P4 at the sash around the perimeter, because the circle will be tangent at those points. The solution starts by sticking an absurdly small circle around P3 out at P4, then expanding its radius and relocating its center until the circle just kisses the sash, thus revealing the location of P2:

    t1 = tan(PetalHA);
    sc = (Sash/2) / cos(PetalHA);
    
    << snippage >>
    
    P3 = P4;        // initial guess
    r = 1.0mm;      // ditto
    delta = 0.0mm;
    do {
      r += sin(PetalHA) * delta;
      P3.x = P4.x - r;
      dist = abs(P3.x * t1 - sc) / sqrt(pow(t1,2) + 1);
      delta = dist - r;
      message("r: ",r,"  delta: ",delta);
    } while (abs(delta) > 0.001mm);
    
    P2 = [P3.x - r*sin(PetalHA),r*cos(PetalHA)];
    

    The dist variable is the perpendicular distance from the sash line to P3, which will be different than the test radius r between P3 and P4 until it’s equal at the kissing point. The radius update is (pretty close to) the X-axis difference between the two, which is (pretty close to) how wrong the radius is.

    As far as I can tell, this will eventually converge on the right answer:

    r: 1.0000mm  delta: 13.3381mm
    r: 6.1043mm  delta: 6.2805mm
    r: 8.5077mm  delta: 2.9573mm
    r: 9.6394mm  delta: 1.3925mm
    r: 10.1723mm  delta: 0.6557mm
    r: 10.4232mm  delta: 0.3087mm
    r: 10.5414mm  delta: 0.1454mm
    r: 10.5970mm  delta: 0.0685mm
    r: 10.6232mm  delta: 0.0322mm
    r: 10.6355mm  delta: 0.0152mm
    r: 10.6413mm  delta: 0.0071mm
    r: 10.6441mm  delta: 0.0034mm
    r: 10.6454mm  delta: 0.0016mm
    r: 10.6460mm  delta: 0.0007mm

    Obviously, efficiency isn’t a big concern here.

    Having found the center point of the end cap, all the other points fall out easily enough and generating the paths follows the same process as with the simple petals. The program performs no error checking and fails in amusing ways.

    As before, laser cutting the chipboard deposits some soot along both sides of the kerf. It’s noticeable on brown chipboard and painfully obvious on white-surface chipboard, particularly where all those cuts converge toward the middle. I applied low-tack blue masking tape as a (wait for it) mask:

    Chipboard coaster - tape shield
    Chipboard coaster – tape shield

    Whereupon I discovered the white surface has the consistency of tissue paper and removing the tape pretty much peels it right off:

    Chipboard coaster - white surface vs tape
    Chipboard coaster – white surface vs tape

    Putting the chipboard up on spikes and cutting it from the back side, with tabs holding the pieces in place (so they don’t fall out and get torched while cutting the next piece), should solve that problem.

    In the meantime, a black frame conceals many issues:

    Chipboard coaster - rounded petals - front vs back cut
    Chipboard coaster – rounded petals – front vs back cut

    I must up my coloring game; those fat-tip markers just ain’t getting it done.

    The GCMC and Bash source code as a GitHub Gist:

    // Round Petals Test Piece
    // Ed Nisley KE4ZNU
    // 2022-07-17 Coasters with round-end petals
    layerstack("Frame","Petals","Rim","Base","Center","Tool1"); // SVG layers map to LightBurn colors
    //—–
    // Library routines
    include("tracepath.inc.gcmc");
    include("tracepath_comp.inc.gcmc");
    include("varcs.inc.gcmc");
    include("engrave.inc.gcmc");
    FALSE = 0;
    TRUE = !FALSE;
    //—–
    // Command line parameters
    // -D various useful tidbits
    // add unit to speeds and depths: 2000mm / -3.00mm / etc
    if (!isdefined("OuterDia")) {
    OuterDia = 100.0mm;
    }
    if (!isdefined("CenterDia")) {
    CenterDia = 0.0mm;
    }
    if (!isdefined("NumPetals")) {
    NumPetals = 6;
    }
    if (!isdefined("Sash")) {
    Sash = 5.0mm;
    }
    // Petal values
    PetalAngle = 360.0deg/NumPetals; // subtended by inner sides
    PetalHA = PetalAngle/2;
    PetalOD = OuterDia – 2*Sash;
    PetalID = CenterDia + 2*Sash;
    PetalOAL = OuterDia/2 – Sash – (Sash/2)/sin(PetalHA);
    //message("petalOAL: ",PetalOAL);
    P4 = [PetalOD/2,0.0mm];
    // Find petal vertices
    P0 = [(Sash/2) / sin(PetalHA),0.0mm];
    t1 = tan(PetalHA);
    sc = (Sash/2) / cos(PetalHA);
    if (P0.x < PetalID/2) {
    a = 1 + pow(t1,2);
    b = -2 * t1 * sc;
    c = pow(sc,2) – pow(PetalID/2,2);
    xp = (-b + sqrt(pow(b,2) – 4*a*c))/(2*a);
    xn = (-b – sqrt(pow(b,2) – 4*a*c))/(2*a);
    y = xp*t1 – sc;
    if (FALSE) {
    message("a: ",a);
    message("b: ",b);
    message("c: ",c);
    message("p: ",xp," n: ",xn," y: ",y);
    }
    P1 = [xp,y];
    }
    else {
    P1 = P0;
    }
    P3 = P4; // initial guess
    r = 1.0mm; // ditto
    delta = 0.0mm;
    do {
    r += sin(PetalHA) * delta;
    P3.x = P4.x – r;
    dist = abs(P3.x * t1 – sc) / sqrt(pow(t1,2) + 1);
    delta = dist – r;
    message("r: ",r," delta: ",delta);
    } while (abs(delta) > 0.001mm);
    P2 = [P3.x – r*sin(PetalHA),r*cos(PetalHA)];
    PetalWidth = 2*r;
    if (FALSE) {
    message("P0: ",P0);
    message("P1: ",P1);
    message("P2: ",P2);
    message("P3: ",P3);
    message("P4: ",P4);
    }
    // Construct paths
    PetalPoints = {P1,P2};
    OutArc = varc_cw([P2.x,-P2.y] – P2,-r);
    OutArc += P2;
    PetalPoints += OutArc;
    if (P0 != P1) {
    PetalPoints += {[P1.x,-P1.y]};
    InArc = varc_ccw(P1 – [P1.x,-P1.y],PetalID/2);
    InArc += [P1.x,-P1.y];
    PetalPoints += InArc;
    }
    else {
    PetalPoints += {P0};
    }
    //— Lay out the frame
    linecolor(0xff0000);
    layer("Frame");
    if (CenterDia) {
    goto([CenterDia/2,0mm]);
    circle_cw([0mm,0mm]);
    }
    repeat(NumPetals;i) {
    a = (i-1)*PetalAngle;
    tracepath(rotate_xy(PetalPoints,a));
    }
    goto([OuterDia/2,0]);
    circle_cw([0mm,0mm]);
    //— Lay out internal pieces for oriented cutting
    // baseplate
    layer("Base");
    relocate([OuterDia + 2*Sash,0]);
    goto([OuterDia/2,0]);
    circle_cw([0mm,0mm]);
    // central circle
    if (CenterDia) {
    layer("Center");
    relocate([OuterDia/2 + Sash,-(OuterDia – CenterDia)/2]);
    goto([CenterDia/2,0mm]);
    circle_cw([0mm,0mm]);
    }
    // petals
    layer("Petals");
    repeat(NumPetals;i) {
    org = [PetalWidth/2 – OuterDia/2,-(OuterDia + Sash)];
    relocate([(i-1)*(PetalWidth + Sash) + org.x,org.y]);
    tracepath(rotate_xy(PetalPoints,90deg));
    }
    // Debugging by printf()
    if (FALSE) {
    layer("Tool1");
    linecolor(0xff1f00);
    goto([Sash/2,0mm]);
    circle_cw([0mm,0mm]);
    goto(P0);
    circle_cw([0mm,0mm]);
    goto([0,0]);
    move([OuterDia/2,0]);
    goto([0,0]);
    move(OuterDia/2 * [cos(PetalHA),sin(PetalHA)]);
    goto(P2);
    move_r([0,-PetalWidth/2]);
    }
    #!/bin/bash
    # Round petals test piece
    # Ed Nisley KE4ZNU – 2022-07-17
    Flags='-P 4 –pedantic' # quote to avoid leading hyphen gotcha
    SVGFlags='-P 4 –pedantic –svg –svg-no-movelayer –svg-opacity=1.0 –svg-toolwidth=0.2'
    # Set these to match your file layout
    ProjPath='/mnt/bulkdata/Project Files/Laser Cutter/Coasters/Source Code'
    LibPath='/opt/gcmc/library'
    ScriptPath=$ProjPath
    Script='Round Petals.gcmc'
    [ -z "$1" ] && petals="6" || petals="$1"
    fn=RoundPetals-$petals.svg
    echo Output: $fn
    gcmc $SVGFlags \
    -D "NumPetals=$petals" \
    –include "$LibPath" \
    "$ScriptPath"/"$Script" > "$fn"
    view raw roundpetals.sh hosted with ❤ by GitHub