Skip to content

Commit

Permalink
Add support for Path.XMonotone
Browse files Browse the repository at this point in the history
  • Loading branch information
tdewolff committed Apr 19, 2024
1 parent 96dccb6 commit 03a00b7
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 8 deletions.
24 changes: 16 additions & 8 deletions path.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,14 +159,8 @@ func (p *Path) PointClosed() bool {
return 6 < len(p.d) && p.d[len(p.d)-1] == CloseCmd && Equal(p.d[len(p.d)-7], p.d[len(p.d)-3]) && Equal(p.d[len(p.d)-6], p.d[len(p.d)-2])
}

// XMonotone returns true if the path is x-monotone, that is each path segment is either increasing or decreasing with X while moving across the segment. This is always true for line segments, but may not be the case for Beziér or arc segments.
func (p *Path) XMonotone() bool {
// TODO: implement XMonotone
panic("not implemented")
}

// HasSubpaths returns true when path p has subpaths.
// TODO: naming right? A simple path would not self-intersect. Add XMonotone and Flat as well
// TODO: naming right? A simple path would not self-intersect. Add IsXMonotone and IsFlat as well?
func (p *Path) HasSubpaths() bool {
for i := 0; i < len(p.d); {
if p.d[i] == MoveToCmd && i != 0 {
Expand Down Expand Up @@ -1216,11 +1210,25 @@ func (p *Path) Flatten(tolerance float64) *Path {
return p.replace(nil, quad, cube, arc)
}

// ReplaceArcs replaces ArcTo commands by CubeTo commands.
// ReplaceArcs replaces ArcTo commands by CubeTo commands and returns a new path.
func (p *Path) ReplaceArcs() *Path {
return p.replace(nil, nil, nil, arcToCube)
}

// XMonotone replaces all Bézier and arc segments to be x-monotone and returns a new path, that is each path segment is either increasing or decreasing with X while moving across the segment. This is always true for line segments.
func (p *Path) XMonotone() *Path {
quad := func(p0, p1, p2 Point) *Path {
return xmonotoneQuadraticBezier(p0, p1, p2)
}
cube := func(p0, p1, p2, p3 Point) *Path {
return xmonotoneCubicBezier(p0, p1, p2, p3)
}
arc := func(start Point, rx, ry, phi float64, large, sweep bool, end Point) *Path {
return xmonotoneEllipticArc(start, rx, ry, phi, large, sweep, end)
}
return p.replace(nil, quad, cube, arc)
}

// replace replaces path segments by their respective functions, each returning the path that will replace the segment or nil if no replacement is to be performed. The line function will take the start and end points. The bezier function will take the start point, control point 1 and 2, and the end point (i.e. a cubic Bézier, quadratic Béziers will be implicitly converted to cubic ones). The arc function will take a start point, the major and minor radii, the radial rotaton counter clockwise, the large and sweep booleans, and the end point. The replacing path will replace the path segment without any checks, you need to make sure the be moved so that its start point connects with the last end point of the base path before the replacement. If the end point of the replacing path is different that the end point of what is replaced, the path that follows will be displaced.
func (p *Path) replace(
line func(Point, Point) *Path,
Expand Down
42 changes: 42 additions & 0 deletions path_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,20 @@ func addCubicBezierLine(p *Path, p0, p1, p2, p3 Point, t, d float64) {
p.LineTo(pos.X, pos.Y)
}

func xmonotoneQuadraticBezier(p0, p1, p2 Point) *Path {
p := &Path{}
p.MoveTo(p0.X, p0.Y)
if tdenom := (p0.X - 2*p1.X + p2.X); !Equal(tdenom, 0.0) {
if t := (p0.X - p1.X) / tdenom; 0.0 < t && t < 1.0 {
_, q1, q2, _, r1, r2 := quadraticBezierSplit(p0, p1, p2, t)
p.QuadTo(q1.X, q1.Y, q2.X, q2.Y)
p1, p2 = r1, r2
}
}
p.QuadTo(p1.X, p1.Y, p2.X, p2.Y)
return p
}

func flattenQuadraticBezier(p0, p1, p2 Point, tolerance float64) *Path {
// see Flat, precise flattening of cubic Bézier path and offset curves, by T.F. Hain et al., 2005, https://www.sciencedirect.com/science/article/pii/S0097849305001287
t := 0.0
Expand Down Expand Up @@ -711,6 +725,34 @@ func flattenQuadraticBezier(p0, p1, p2 Point, tolerance float64) *Path {
return p
}

func xmonotoneCubicBezier(p0, p1, p2, p3 Point) *Path {
a := -p0.X + 3*p1.X - 3*p2.X + p3.X
b := 2*p0.X - 4*p1.X + 2*p2.X
c := -p0.X + p1.X

p := &Path{}
p.MoveTo(p0.X, p0.Y)

split := false
t1, t2 := solveQuadraticFormula(a, b, c)
if !math.IsNaN(t1) && IntervalExclusive(t1, 0.0, 1.0) {
_, q1, q2, q3, r0, r1, r2, r3 := cubicBezierSplit(p0, p1, p2, p3, t1)
p.CubeTo(q1.X, q1.Y, q2.X, q2.Y, q3.X, q3.Y)
p0, p1, p2, p3 = r0, r1, r2, r3
split = true
}
if !math.IsNaN(t2) && IntervalExclusive(t2, 0.0, 1.0) {
if split {
t2 = (t2 - t1) / (1.0 - t1)
}
_, q1, q2, q3, _, r1, r2, r3 := cubicBezierSplit(p0, p1, p2, p3, t2)
p.CubeTo(q1.X, q1.Y, q2.X, q2.Y, q3.X, q3.Y)
p1, p2, p3 = r1, r2, r3
}
p.CubeTo(p1.X, p1.Y, p2.X, p2.Y, p3.X, p3.Y)
return p
}

func flattenCubicBezier(p0, p1, p2, p3 Point, tolerance float64) *Path {
return strokeCubicBezier(p0, p1, p2, p3, 0.0, tolerance)
}
Expand Down
9 changes: 9 additions & 0 deletions path_util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ func TestQuadraticBezierDistance(t *testing.T) {
}
}

func TestXMonotoneQuadraticBezier(t *testing.T) {
test.T(t, xmonotoneQuadraticBezier(Point{2.0, 0.0}, Point{0.0, 1.0}, Point{2.0, 2.0}), MustParseSVGPath("M2 0Q1 0.5 1 1Q1 1.5 2 2"))
}

func TestQuadraticBezierFlatten(t *testing.T) {
tolerance := 0.1
tests := []struct {
Expand Down Expand Up @@ -414,6 +418,11 @@ func TestCubicBezierStrokeHelpers(t *testing.T) {
test.T(t, p, MustParseSVGPath("L1.5 1"))
}

func TestXMonotoneCubicBezier(t *testing.T) {
test.T(t, xmonotoneCubicBezier(Point{1.0, 0.0}, Point{0.0, 0.0}, Point{0.0, 1.0}, Point{1.0, 1.0}), MustParseSVGPath("M1 0C0.5 0 0.25 0.25 0.25 0.5C0.25 0.75 0.5 1 1 1"))
test.T(t, xmonotoneCubicBezier(Point{0.0, 0.0}, Point{3.0, 0.0}, Point{-2.0, 1.0}, Point{1.0, 1.0}), MustParseSVGPath("M0 0C0.75 0 1 0.0625 1 0.15625C1 0.34375 0.0 0.65625 0.0 0.84375C0.0 0.9375 0.25 1 1 1"))
}

func TestCubicBezierStrokeFlatten(t *testing.T) {
tests := []struct {
path string
Expand Down

0 comments on commit 03a00b7

Please # to comment.