Skip to content

Commit 5cffe09

Browse files
authored
Merge pull request #67 from quchen/various
Various
2 parents 7b3bc85 + 7d2aed4 commit 5cffe09

File tree

4 files changed

+150
-59
lines changed

4 files changed

+150
-59
lines changed

src/Draw/Plotting.hs

+2-2
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

+88-25
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

src/Geometry/Core.hs

+59-31
Original file line numberDiff line numberDiff line change
@@ -1204,10 +1204,11 @@ resizeLineSymmetric f line@(Line start end) = (centerLine . resizeLine f . trans
12041204
--
12051205
-- Useful for painting lines going through a point symmetrically.
12061206
centerLine :: Line -> Line
1207-
centerLine line@(Line start end) = transform (translate delta) line
1208-
where
1209-
middle = 0.5 *. (start +. end)
1210-
delta = start -. middle
1207+
centerLine (Line start end) =
1208+
let middle = 0.5 *. (start +. end)
1209+
end' = middle
1210+
start' = start -. end +. middle
1211+
in Line start' end'
12111212

12121213
-- | Move the end point of the line so that it has length 1.
12131214
normalizeLine :: Line -> Line
@@ -1291,6 +1292,8 @@ intersectionPoint _ = Nothing
12911292
--
12921293
-- Returns the point of the intersection, and whether it is inside both, one, or
12931294
-- none of the provided finite line segments.
1295+
--
1296+
-- See 'intersectInfiniteLines' for a more performant, but less nuanced result.
12941297
intersectionLL :: Line -> Line -> LLIntersection
12951298
intersectionLL lineL lineR
12961299
= intersectionType
@@ -2004,51 +2007,76 @@ isConvex (Polygon ps) = allSameSign angleDotProducts
20042007
--
20052008
-- === __(image code)__
20062009
-- >>> :{
2007-
-- haddockRender "Geometry/Core/perpendicular_bisector.svg" 150 150 $ do
2008-
-- let line = Line (Vec2 10 60) (Vec2 140 100)
2010+
-- haddockRender "Geometry/Core/perpendicular_bisector.svg" 200 160 $ do
2011+
-- let line = Line (Vec2 20 20) (Vec2 190 90)
20092012
-- bisector = perpendicularBisector line
20102013
-- C.setLineWidth 2
20112014
-- sketch line >> C.stroke
20122015
-- sketch bisector >> setColor (mathematica97 1) >> C.stroke
20132016
-- :}
2014-
-- Generated file: size 2KB, crc32: 0x1f7d2821
2017+
-- Generated file: size 2KB, crc32: 0x9940c32d
20152018
perpendicularBisector :: Line -> Line
2016-
perpendicularBisector line@(Line start end) = perpendicularLineThrough middle line
2019+
perpendicularBisector (Line start end) =
2020+
let middle = 0.5 *. (end +. start)
2021+
in rotateLine90 (Line middle end)
2022+
2023+
rotateLine90 :: Line -> Line
2024+
rotateLine90 (Line start end) = Line start end'
20172025
where
2018-
middle = 0.5 *. (start +. end)
2026+
end' = rotate90 (end -. start) +. start
2027+
2028+
rotate90 :: Vec2 -> Vec2
2029+
rotate90 (Vec2 x y) = Vec2 (-y) x
20192030

2020-
-- | Line perpendicular to a given line through a point.
2031+
-- | Line perpendicular to a given line through a point, starting at the
2032+
-- intersection point.
2033+
--
2034+
-- If the point is on the line directly, fall back to a perpendicular line through
2035+
-- the point of half the length of the input.
20212036
--
2022-
-- The result has the same length as the input, point in its center, and points
2023-
-- to the left (90° turned CCW) relative to the input.
2037+
-- This is also known as the vector projection. For vectors \(\mathbf a\) (pointing
2038+
-- from the start of the line to \(\mathbf p\)) and \(\mathbf b\) (pointing from
2039+
-- the start of the line to its end),
2040+
--
2041+
-- \[
2042+
-- \mathrm{proj}_{\mathbf b}(\mathbf a)
2043+
-- = \frac{\mathbf a\cdot\mathbf b}{\mathbf b\cdot\mathbf b}\mathbf b
2044+
-- \]
20242045
--
20252046
-- <<docs/haddock/Geometry/Core/perpendicular_line_through.svg>>
20262047
--
20272048
-- === __(image code)__
20282049
-- >>> :{
2029-
-- haddockRender "Geometry/Core/perpendicular_line_through.svg" 150 140 $ do
2030-
-- let line = Line (Vec2 20 20) (Vec2 140 70)
2031-
-- point = Vec2 70 70
2032-
-- perpendicular = perpendicularLineThrough point line
2050+
-- haddockRender "Geometry/Core/perpendicular_line_through.svg" 170 170 $ do
2051+
-- let line = transform (translate (Vec2 20 20))
2052+
-- (Line zero (Vec2 (3*40) (4*20)))
2053+
-- points =
2054+
-- [ Vec2 20 110 -- above
2055+
-- , Vec2 70 90 -- above
2056+
-- , Vec2 130 20 -- below
2057+
-- , Vec2 110 80 -- directly on
2058+
-- , Vec2 130 150 -- beyond
2059+
-- ]
20332060
-- C.setLineWidth 2
20342061
-- sketch line >> C.stroke
2035-
-- setColor (mathematica97 1)
2036-
-- sketch (Circle point 5) >> C.fill
2037-
-- sketch perpendicular >> C.stroke
2062+
-- for_ (zip [1..] points) $ \(i, p) -> do
2063+
-- setColor (mathematica97 i)
2064+
-- cairoScope $ sketch (perpendicularLineThrough p line) >> C.setDash [3,5] 0 >> C.stroke
2065+
-- cairoScope $ sketch (Circle p 5) >> C.fill
20382066
-- :}
2039-
-- Generated file: size 2KB, crc32: 0xb8e5d1d9
2067+
-- Generated file: size 4KB, crc32: 0xac73c163
20402068
perpendicularLineThrough :: Vec2 -> Line -> Line
2041-
perpendicularLineThrough p line@(Line start _) = centerLine line'
2042-
where
2043-
-- Move line so it starts at the origin
2044-
Line start0 end0 = transform (translate (negateV start)) line
2045-
-- Rotate end point 90° CCW
2046-
end0' = let Vec2 x y = end0
2047-
in Vec2 (-y) x
2048-
-- Construct rotated line
2049-
lineAt0' = Line start0 end0'
2050-
-- Move line back so it goes through the point
2051-
line' = transform (translate p) lineAt0'
2069+
perpendicularLineThrough p (Line start end) =
2070+
let a = p -. start
2071+
b = end -. start
2072+
proj = (dotProduct a b/dotProduct b b) *. b +. start
2073+
in if normSquare (p -. proj) <= 0.01^2
2074+
then -- p is too close to proj, so a 'Line' does not make
2075+
-- any sense. Fall back to a line of half the input
2076+
-- length perpendicular to the original one.
2077+
let p' = proj +. rotate90 ((end -. start) /. 2)
2078+
in Line proj p'
2079+
else Line proj p
20522080

20532081
-- | Optical reflection of a ray on a mirror. Note that the outgoing line has
20542082
-- reversed direction like light rays would. The second result element is the

test/testsuite/Test/Uncategorized/Bezier.hs

+1-1
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)