packages feed

juicy-gcode-0.3.0.0: src/Approx/BiArc.hs

{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
{-# HLINT ignore "Redundant bracket" #-}
module Approx.BiArc (
    bezier2biarcs
) where

import qualified Graphics.CubicBezier as B
import qualified Graphics.BiArc as BA
import qualified Graphics.CircularArc as CA
import qualified Graphics.Line as L
import Graphics.Curve
import Graphics.Path
import Graphics.Point
import Utils

import Data.Bool (bool)
import Control.Lens
import Linear

eps :: Double
eps = 0.0001
maxiter :: Double
maxiter = 10

-- Approximate a bezier curve with biarcs (Left) and line segments (Right)
-- M.A. Sabin, The use of piecewise forms for the numerical representation of shape (1977)
bezier2biarcs :: B.CubicBezier
              -> Double
              -> [PathCommand]
bezier2biarcs mbezier resolution
    -- Degenerate curve: all points on the same line -> it is a line 
    | L.isOnLine (L.fromPoints (B._p2 mbezier) (B._p1 mbezier)) (B._c1 mbezier) &&
      L.isOnLine (L.fromPoints (B._p2 mbezier) (B._p1 mbezier)) (B._c2 mbezier)
        = [LineTo (toPoint (B._p2 mbezier))]
    -- Degenerate curve: p1 == c1, don't split
    | B._p1 mbezier == B._c1 mbezier
        = approxSpiral mbezier
    -- Degenerate curve: p2 == c2, don't split
    | B._p2 mbezier == B._c2 mbezier
        = approxSpiral mbezier
    -- Split by the inflexion points (if any)
    | otherwise
        = byInflection (B.inflectionPoints mbezier)
    where
        order a b | b < a = (b, a)
                  | otherwise = (a, b)

        byInflection [t] = approxSpiral b1 ++ approxSpiral b2
            where
                (b1, b2) = B.splitAt mbezier t

        byInflection [t1, t2] = approxSpiral b1 ++ approxSpiral b2 ++ approxSpiral b3
            where
                (it1, it2') = order t1 t2

                -- Make the first split and save the first new curve. The second one has to be splitted again
                -- at the recalculated t2 (it is on a new curve)                
                it2 = (1 - it1) * it2'

                (b1, toSplit) = B.splitAt mbezier it1
                (b2, b3) = B.splitAt toSplit it2

        byInflection _ = approxSpiral mbezier

        -- Apprixmate one bezier spiral
        approxSpiral :: B.CubicBezier -> [PathCommand]
        approxSpiral bezier
            -- Approximate bezier length. if max length is smaller than resolution, do not approximate
            | B.maxArcLength bezier < resolution
                = [LineTo (toPoint (B._p2 bezier))]
            -- Edge case: start- and endpoints are the same
            | B._p1 bezier == B._p2 bezier
                = splitAndRecur 0.5
            -- Edge case: control lines are parallel
            | L._m t1 == L._m t2 || isNaN (L._m t1) && isNaN (L._m t2)
                = splitAndRecur 0.5
            -- Biarc triangle has the wrong orientation
            -- Curve looks like this: https://pomax.github.io/bezierinfo/images/chapters/decasteljau/df92f529841f39decf9ad62b0967855a.png
            | B.isClockwise bezier /= isClockwise3 (B._p1 bezier) (B._p2 bezier) v
                = splitAndRecur 0.5
            -- Unstable approximation: split the bezier into half, it will switch to linear approximation if the segments get too small
            | not (isStable biarc)
                = splitAndRecur 0.5
            -- Approximation is not close enough yet, refine
            | maxDistance > resolution
                = splitAndRecur maxDistanceAt
            -- Desired case: approximation is stable and close enough
            | otherwise
                = biarc2path biarc

            where
                -- Edge case: P1==C1 or P2==C2
                -- there is no derivative at P1 or P2, use the other control point
                c1 = bool (B._c1 bezier) (B._c2 bezier) (B._p1 bezier == B._c1 bezier)
                c2 = bool (B._c2 bezier) (B._c1 bezier) (B._p2 bezier == B._c2 bezier)

                -- V: Intersection point of tangent lines
                t1 = L.fromPoints (B._p1 bezier) c1
                t2 = L.fromPoints (B._p2 bezier) c2
                v = L.intersection t1 t2

                -- G: incenter point of the triangle (P1, V, P2)
                dP2V = distance (B._p2 bezier) v
                dP1V = distance (B._p1 bezier) v
                dP1P2 = distance (B._p1 bezier) (B._p2 bezier)
                g = (dP2V *^ B._p1 bezier + dP1V *^ B._p2 bezier + dP1P2 *^ v) ^/ (dP2V + dP1V + dP1P2)

                -- Calculate the BiArc
                biarc = BA.fromPoints (B._p1 bezier) (B._p1 bezier - c1) (B._p2 bezier) (B._p2 bezier - c2) g

                (maxDistanceAt, maxDistance) = calculateMaxDistance bezier biarc

                splitAndRecur t = let (b1, b2) = B.splitAt bezier t
                                   in approxSpiral b1 ++ approxSpiral b2

biarc2path :: BA.BiArc -> [PathCommand]
biarc2path biarc = map
    (\arc -> ArcTo (toPoint (CA._c arc)) (toPoint (CA._p2 arc)) (CA.isClockwise arc))
    [BA._a1 biarc, BA._a2 biarc]

-- Heuristics for unstable biarc: the radius of at least one of the arcs 
-- is too big or too small. Not too scientific...
isStable :: BA.BiArc -> Bool
isStable biarc
    = not (CA._r (BA._a1 biarc) > 99999 || CA._r (BA._a1 biarc) < 0.001 ||
           CA._r (BA._a2 biarc) > 99999 || CA._r (BA._a2 biarc) < 0.001)

-- Calculate the maximum approximation error along the radial direction
-- D.J. Walton, D.S. Meek, Approximation of a planar cubic Bezier spiral by circular arcs (1996)
calculateMaxDistance :: B.CubicBezier -> BA.BiArc -> (Double, Double)
calculateMaxDistance bezier biarc
    -- This should not happenm but if, split the bezier at the middle
    | tj == -1 = (0.5, 0x7FEFFFFFFFFFFFFF)
    | otherwise = bigger (bigger (tj, dj) (t0, d0)) (t1, d1)
    where
        tj = findRadialIntersection bezier biarc (BA.jointAt biarc)
        dj = distance (pointAt bezier tj) (pointAt biarc (BA.jointAt biarc))

        g arc u = dot (pointAt bezier u - CA._c arc) (B.firstDerivativeAt bezier u)
        g' arc u = quadrance (B.firstDerivativeAt bezier u) +
                   dot (pointAt bezier u - CA._c arc) (B.secondDerivativeAt bezier u)

        bigger f@(_, df) s@(ts, ds)
            | ts == -1 = f
            | df > ds = f
            | otherwise = s

        -- Valid in (0,tj]
        t0 = findRoot (g (BA._a1 biarc)) (g' (BA._a1 biarc)) eps tj
        d0 = abs ((distance (pointAt bezier t0) (CA._c (BA._a1 biarc))) - (CA._r (BA._a1 biarc)))
        -- Valid in [tj,1)
        t1 = findRoot (g (BA._a2 biarc)) (g' (BA._a2 biarc)) tj (1 - eps)
        d1 = abs ((distance (pointAt bezier t1) (CA._c (BA._a2 biarc))) - (CA._r (BA._a2 biarc)))

-- Takes a paramater `t` fore the `biarc` and calculates the related parameter fo
-- the `bezier` (which is the intersection point in the radial direction)
findRadialIntersection :: B.CubicBezier -> BA.BiArc -> Double -> Double
findRadialIntersection bezier biarc t
    | t == 0 || t == 1 = t
    | otherwise = findRoot (\u -> dot (pointAt bezier u - p) h) (\u -> dot (B.firstDerivativeAt bezier u) h) 0 1
    where
        p = pointAt biarc t
        c = CA._c $ if' (t <= BA.jointAt biarc) (BA._a1 biarc) (BA._a2 biarc)
        m = p - c
        h = normalize $ V2 (negate (m ^. _y)) (m ^. _x)

-- Tries to find the root of f in interval [lowerBound,upperBound] using a combination of
-- Newton and bisection methods.
-- It is supposed to have at most one solution. If no solution is found, returns -1
findRoot :: (Double -> Double) -> (Double -> Double) -> Double -> Double -> Double
findRoot f df lowerBound upperBound
    | fl * fu > 0 = -1
    | fl == 0 = lowerBound
    | fu == 0 = upperBound
    | otherwise = iter maxiter fl fu lowerBound upperBound ((lowerBound + upperBound) / 2)
    where
        fl = f lowerBound
        fu = f upperBound

        iter i fmin fmax lb ub root
            -- we're good, or if i==0, we may not reached tolarence yet, but hopefully it is close enough
            | abs fx < eps || i <= 0 = root
            -- overshoot or undershoot -> switch to bisection
            | n < lb || n > ub
                = if' (fmin * fx < 0)
                    (iter (i-1) fmin fx lb root ((lb + root) / 2))
                    (iter (i-1) fx fmax root ub ((root + ub) / 2))
            -- Newton step
            | otherwise
                = iter (i-1) fmin fmax lb ub n
            where
                fx = f root
                h = fx / df root
                n = root - h