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