Skip to content

Commit 7d2aed4

Browse files
committed
Implement deCasteljau Bézier curve simplifier
1 parent 84105b6 commit 7d2aed4

File tree

3 files changed

+91
-28
lines changed

3 files changed

+91
-28
lines changed

src/Draw/Plotting.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -821,11 +821,11 @@ instance Plotting Polygon where
821821
lineTo r
822822

823823
-- | FluidNC doesn’t support G05, so we approximate Bezier curves with line pieces.
824-
-- We use the naive Bezier interpolation 'bezierSubdivideT', because it just so
824+
-- We use the naive Bezier interpolation 'bezierSubdivideEquiparametric', because it just so
825825
-- happens to put more points in places with more curvature.
826826
instance Plotting Bezier where
827827
plot bezier = commented "Bezier (cubic)" $ do
828-
let points = bezierSubdivideT 32 bezier
828+
let points = bezierSubdivideEquiparametric 32 bezier
829829
let p:ointsToPlot = points
830830
repositionTo p
831831
traverse_ lineTo ointsToPlot

src/Geometry/Bezier.hs

Lines changed: 88 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ module Geometry.Bezier
99
, bezierS
1010

1111
-- * Subdividing
12-
, bezierSubdivideT
13-
, bezierSubdivideS
12+
, bezierSubdivideEquiparametric
13+
, bezierSubdivideEquidistant
14+
, bezierSubdivideCasteljau
1415

1516
-- * Interpolation
1617
, bezierSmoothen
@@ -41,6 +42,26 @@ import Numerics.LinearEquationSystem
4142

4243

4344

45+
-- $references
46+
--
47+
-- == Arc length parameterization
48+
--
49+
-- * Moving Along a Curve with Specified Speed (2019)
50+
-- by David Eberly
51+
-- https://www.geometrictools.com/Documentation/MovingAlongCurveSpecifiedSpeed.pdf
52+
--
53+
-- == Smoothening
54+
--
55+
-- * Cubic Bézier Splines
56+
-- by Michael Joost
57+
-- https://www.michael-joost.de/bezierfit.pdf
58+
--
59+
-- * Building Smooth Paths Using Bézier Curves
60+
-- by Stuart Kent
61+
-- https://www.stkent.com/2015/07/03/building-smooth-paths-using-bezier-curves.html
62+
63+
64+
4465
-- | Cubic Bezier curve, defined by start, first/second control points, and end.
4566
data Bezier = Bezier !Vec2 !Vec2 !Vec2 !Vec2 deriving (Eq, Ord, Show)
4667

@@ -144,20 +165,20 @@ bezierLength bezier = retryExponentiallyUntilPrecision (integrateSimpson13 f 0 1
144165
-- | Trace a 'Bezier' curve with a number of points, using the polynomial curve
145166
-- parameterization. This is very fast, but leads to unevenly spaced points.
146167
--
147-
-- For subdivision by arc length, use 'bezierSubdivideS'.
148-
bezierSubdivideT
168+
-- For subdivision by arc length, use 'bezierSubdivideEquidistant'.
169+
bezierSubdivideEquiparametric
149170
:: Int
150171
-> Bezier
151172
-> [Vec2]
152-
bezierSubdivideT n bz = map (bezierT bz) points
173+
bezierSubdivideEquiparametric n bz = map (bezierT bz) points
153174
where
154175
points = map (\x -> fromIntegral x / fromIntegral (n-1)) [0..n-1]
155176

156177
-- | Trace a 'Bezier' curve with a number of evenly spaced points by arc length.
157-
-- This is much more expensive than 'bezierSubdivideT', but may be desirable for
178+
-- This is much more expensive than 'bezierSubdivideEquiparametric', but may be desirable for
158179
-- aesthetic purposes.
159180
--
160-
-- Here it is alongside 'bezierSubdivideT':
181+
-- Here it is alongside 'bezierSubdivideEquiparametric':
161182
--
162183
-- <<docs/haddock/Geometry/Bezier/subdivide_s_t_comparison.svg>>
163184
--
@@ -167,8 +188,8 @@ bezierSubdivideT n bz = map (bezierT bz) points
167188
-- let curve = let curveRaw = transform (rotate (deg (-30))) (Bezier (Vec2 0 0) (Vec2 1 5) (Vec2 2.5 (-1)) (Vec2 3 3))
168189
-- fitToBox = transform (transformBoundingBox curveRaw (Vec2 10 10, Vec2 290 90) (TransformBBSettings FitWidthHeight IgnoreAspect FitAlignCenter))
169190
-- in fitToBox curveRaw
170-
-- evenlySpaced = bezierSubdivideS 16 curve
171-
-- unevenlySpaced = bezierSubdivideT 16 curve
191+
-- evenlySpaced = bezierSubdivideEquidistant 16 curve
192+
-- unevenlySpaced = bezierSubdivideEquiparametric 16 curve
172193
-- offsetBelow :: Transform geo => geo -> geo
173194
-- offsetBelow = transform (translate (Vec2 0 50))
174195
-- cairoScope $ do
@@ -189,8 +210,8 @@ bezierSubdivideT n bz = map (bezierT bz) points
189210
-- cairoScope (setColor (black `withOpacity` 0.2) >> connect e u)
190211
-- :}
191212
-- Generated file: size 17KB, crc32: 0x7c147951
192-
bezierSubdivideS :: Int -> Bezier -> [Vec2]
193-
bezierSubdivideS n bz = map bezier distances
213+
bezierSubdivideEquidistant :: Int -> Bezier -> [Vec2]
214+
bezierSubdivideEquidistant n bz = map bezier distances
194215
where
195216

196217
-- The step width should correlate with the length of the curve to get a decent
@@ -246,23 +267,65 @@ s_to_t_lut_ode bz ds = LookupTable1 (sol_to_vec sol)
246267
t0 = 0
247268
s0 = 0
248269

249-
-- $references
270+
-- | Approximage a Bezier curve with line segments up to a certain precision, using
271+
-- relatively few points.
250272
--
251-
-- == Arc length parameterization
252-
--
253-
-- * Moving Along a Curve with Specified Speed (2019)
254-
-- by David Eberly
255-
-- https://www.geometrictools.com/Documentation/MovingAlongCurveSpecifiedSpeed.pdf
273+
-- The idea behind Casteljau subdivision is that each Bézier curve can be exactly
274+
-- subdivided into two Bézier curves (of same degree). This is done recursively, in
275+
-- this implementation (and commonly) in the middle of the curve. Once a curve
276+
-- segment is flat enough (given by the tolerance parameter), it is simply rendered
277+
-- as a line.
256278
--
257-
-- == Smoothening
279+
-- <<docs/haddock/Geometry/Bezier/bezierSubdivideCasteljau.svg>>
258280
--
259-
-- * Cubic Bézier Splines
260-
-- by Michael Joost
261-
-- https://www.michael-joost.de/bezierfit.pdf
262-
--
263-
-- * Building Smooth Paths Using Bézier Curves
264-
-- by Stuart Kent
265-
-- https://www.stkent.com/2015/07/03/building-smooth-paths-using-bezier-curves.html
281+
-- === __(image code)__
282+
-- >>> :{
283+
-- haddockRender "Geometry/Bezier/bezierSubdivideCasteljau.svg" 500 330 $ do
284+
-- let curve = let curveRaw = transform (rotate (deg (-30))) (Bezier (Vec2 0 0) (Vec2 1 5) (Vec2 2.5 (-1)) (Vec2 3 3))
285+
-- fitToBox = transform (transformBoundingBox curveRaw (shrinkBoundingBox 10 [zero, Vec2 500 200]) (TransformBBSettings FitWidthHeight IgnoreAspect FitAlignCenter))
286+
-- in fitToBox curveRaw
287+
-- paintOffset = Vec2 0 30
288+
-- for_ (zip [0..] [50,25,10,2]) $ \(i, tolerance) -> cairoScope $ do
289+
-- let points = bezierSubdivideCasteljau tolerance (transform (translate (fromIntegral i *. paintOffset)) curve)
290+
-- setColor (mathematica97 i)
291+
-- C.setLineWidth 2
292+
-- sketch (Polyline points) >> C.stroke
293+
-- for_ points $ \p -> sketch (Circle p 3) >> C.fill
294+
-- cairoScope $ do
295+
-- C.setLineWidth 3
296+
-- setColor black
297+
-- sketch (transform (translate (4*.paintOffset)) curve)
298+
-- C.stroke
299+
-- :}
300+
-- Generated file: size 20KB, crc32: 0x679b311c
301+
bezierSubdivideCasteljau :: Double -> Bezier -> [Vec2]
302+
bezierSubdivideCasteljau tolerance curve@(Bezier pFirst _ _ _) = pFirst : go curve
303+
where
304+
go (Bezier p1 p2@(Vec2 x2 y2) p3@(Vec2 x3 y3) p4@(Vec2 x4 y4)) =
305+
let
306+
p12 = (p1 +. p2 ) /. 2
307+
p23 = (p2 +. p3 ) /. 2
308+
p34 = (p3 +. p4 ) /. 2
309+
p123 = (p12 +. p23 ) /. 2
310+
p234 = (p23 +. p34 ) /. 2
311+
p1234 = (p123 +. p234) /. 2
312+
313+
dp@(Vec2 dx dy) = p4 -. p1
314+
315+
-- d2, d3 are the distance from p2, p3 from the line
316+
-- connecting p1 and p4. A curve is flat when those
317+
-- two are short together.
318+
d2 = abs ((x2-x4)*dy - (y2-y4)*dx)
319+
d3 = abs ((x3-x4)*dy - (y3-y4)*dx)
320+
curveIsFlat = (d2 + d3)*(d2 + d3) < tolerance^2 * normSquare dp
321+
in if curveIsFlat
322+
then -- We return only the last point so we don’t get duplicate
323+
-- points for each start+end of adjacent curves.
324+
-- The very first point is forgotten by the
325+
[p4]
326+
else go (Bezier p1 p12 p123 p1234)
327+
++
328+
go (Bezier p1234 p234 p34 p4)
266329

267330
-- | Smoothen a number of points by putting a Bezier curve between each pair.
268331
-- Useful to e.g. make a sketch nicer, or interpolate between points of a crude

test/testsuite/Test/Uncategorized/Bezier.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ subdivideBezierCurveTest = testVisual "Subdivide" 300 300 "docs/interpolation/4_
167167
moveTo 200 70
168168
showText (show (length beziers) ++ " curves")
169169

170-
let subpoints = beziers >>= (V.fromList . bezierSubdivideT 10)
170+
let subpoints = beziers >>= (V.fromList . bezierSubdivideEquiparametric 10)
171171
let simplified = simplifyTrajectoryRdp 0.05 subpoints
172172
cairoScope $ do
173173
let fit :: Transform geo => geo -> geo

0 commit comments

Comments
 (0)