Skip to content

Commit

Permalink
Support MiterClip and ArcsClip stroke joiners
Browse files Browse the repository at this point in the history
  • Loading branch information
tdewolff committed Nov 1, 2023
1 parent cec4139 commit 58c7d8e
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 71 deletions.
2 changes: 1 addition & 1 deletion path.go
Original file line number Diff line number Diff line change
Expand Up @@ -1204,7 +1204,7 @@ func (p *Path) Markers(first, mid, last *Path, align bool) []*Path {
return markers
}

// Split splits the path into its independent subpaths. The path is split before each MoveTo command. None of the subpaths shall be empty.
// Split splits the path into its independent subpaths. The path is split before each MoveTo command.
func (p *Path) Split() []*Path {
var i, j int
ps := []*Path{}
Expand Down
17 changes: 17 additions & 0 deletions path_intersection_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,23 @@ func (zs Intersections) LineEllipse(l0, l1, center, radius Point, phi, theta0, t
// see T.W. Sederberg and T. Nishita, "Curve intersection using Bézier clipping", 1990
// see T.W. Sederberg and S.R. Parry, "Comparison of three curve intersection algorithms", 1986

func intersectionRayLine(a0, a1, b0, b1 Point) (Point, bool) {
da := a1.Sub(a0)
db := b1.Sub(b0)
div := da.PerpDot(db)
if Equal(div, 0.0) {
// parallel
return Point{}, false
}

tb := da.PerpDot(a0.Sub(b0)) / div
if Interval(tb, 0.0, 1.0) {
fmt.Println(tb, b0.Interpolate(b1, tb))
return b0.Interpolate(b1, tb), true
}
return Point{}, false
}

// http://mathworld.wolfram.com/Circle-LineIntersection.html
func intersectionRayCircle(l0, l1, c Point, r float64) (Point, Point, bool) {
d := l1.Sub(l0).Norm(1.0) // along line direction, anchored in l0, its length is 1
Expand Down
197 changes: 145 additions & 52 deletions path_stroke.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ type RoundJoiner struct{}
func (RoundJoiner) Join(rhs, lhs *Path, halfWidth float64, pivot, n0, n1 Point, r0, r1 float64) {
rEnd := pivot.Add(n1)
lEnd := pivot.Sub(n1)
cw := n0.Rot90CW().Dot(n1) >= 0.0
cw := 0.0 <= n0.Rot90CW().Dot(n1)
if cw { // bend to the right, ie. CW (or 180 degree turn)
rhs.LineTo(rEnd.X, rEnd.Y)
lhs.ArcTo(halfWidth, halfWidth, 0.0, false, false, lEnd.X, lEnd.Y)
Expand All @@ -113,13 +113,9 @@ func (RoundJoiner) String() string {
return "Round"
}

// MiterJoin connects two path elements by extending the ends of the paths as lines until they meet. If this point is further than 2 mm * (strokeWidth / 2.0) away, this will result in a bevel join.
var MiterJoin Joiner = MiterJoiner{BevelJoin, 2.0}

// MiterClipJoin returns a MiterJoiner with given limit*strokeWidth/2.0 in mm upon which the gapJoiner function will be used. Limit can be NaN so that the gapJoiner is never used.
func MiterClipJoin(gapJoiner Joiner, limit float64) Joiner {
return MiterJoiner{gapJoiner, limit}
}
// MiterJoin connects two path elements by extending the ends of the paths as lines until they meet. If this point is further than the limit, this will result in a bevel join (MiterJoin) or they will meet at the limit (MiterClipJoin).
var MiterJoin Joiner = MiterJoiner{BevelJoin, 4.0}
var MiterClipJoin Joiner = MiterJoiner{nil, 4.0}

// MiterJoiner is a miter joiner.
type MiterJoiner struct {
Expand All @@ -135,53 +131,80 @@ func (j MiterJoiner) Join(rhs, lhs *Path, halfWidth float64, pivot, n0, n1 Point
}
limit := math.Max(j.Limit, 1.001) // otherwise nearly linear joins will also get clipped

cw := n0.Rot90CW().Dot(n1) >= 0.0
cw := 0.0 <= n0.Rot90CW().Dot(n1)
hw := halfWidth
if cw {
hw = -hw // used to calculate |R|, when running CW then n0 and n1 point the other way, so the sign of r0 and r1 is negated
}

theta := n0.AngleBetween(n1) / 2.0
d := hw / math.Cos(theta)
if !math.IsNaN(limit) && limit*halfWidth < math.Abs(d) {
d := hw / math.Cos(theta) // half the miter length
clip := !math.IsNaN(limit) && limit*halfWidth < math.Abs(d)
if clip && j.GapJoiner != nil {
j.GapJoiner.Join(rhs, lhs, halfWidth, pivot, n0, n1, r0, r1)
return
}
mid := pivot.Add(n0.Add(n1).Norm(d))

rEnd := pivot.Add(n1)
lEnd := pivot.Sub(n1)
if cw { // bend to the right, ie. CW
lhs.LineTo(mid.X, mid.Y)
mid := pivot.Add(n0.Add(n1).Norm(d))
if clip {
// miter-clip
t := math.Abs(limit * halfWidth / d)
if cw { // bend to the right, ie. CW
mid0 := lhs.Pos().Interpolate(mid, t)
mid1 := lEnd.Interpolate(mid, t)
lhs.LineTo(mid0.X, mid0.Y)
lhs.LineTo(mid1.X, mid1.Y)
} else {
mid0 := rhs.Pos().Interpolate(mid, t)
mid1 := rEnd.Interpolate(mid, t)
rhs.LineTo(mid0.X, mid0.Y)
rhs.LineTo(mid1.X, mid1.Y)
}
} else {
rhs.LineTo(mid.X, mid.Y)
if cw { // bend to the right, ie. CW
lhs.LineTo(mid.X, mid.Y)
} else {
rhs.LineTo(mid.X, mid.Y)
}
}
rhs.LineTo(rEnd.X, rEnd.Y)
lhs.LineTo(lEnd.X, lEnd.Y)
}

func (j MiterJoiner) String() string {
if math.IsNaN(j.Limit) {
return "Miter"
if j.GapJoiner == nil {
return "MiterClip"
}
return "MiterClip"
return "Miter"
}

// ArcsJoin connects two path elements by extending the ends of the paths as circle arcs until they meet. If this point is further than 10 mm * (strokeWidth / 2.0) away, this will result in a bevel join.
var ArcsJoin Joiner = ArcsJoiner{BevelJoin, 10.0}

// ArcsClipJoin returns an ArcsJoiner with given limit in mm*strokeWidth/2.0 upon which the gapJoiner function will be used. Limit can be NaN so that the gapJoiner is never used.
func ArcsClipJoin(gapJoiner Joiner, limit float64) Joiner {
return ArcsJoiner{gapJoiner, limit}
}
// ArcsJoin connects two path elements by extending the ends of the paths as circle arcs until they meet. If this point is further than the limit, this will result in a bevel join (ArcsJoin) or they will meet at the limit (ArcsClipJoin).
var ArcsJoin Joiner = ArcsJoiner{BevelJoin, 4.0}
var ArcsClipJoin Joiner = ArcsJoiner{nil, 4.0}

// ArcsJoiner is an arcs joiner.
type ArcsJoiner struct {
GapJoiner Joiner
Limit float64
}

// Join adds a join to a right-hand-side and left-hand-side path, of width 2*halfWidth, around a pivot point with starting and ending normals of n0 and n1, and radius of curvatures of the previous and next segments.
func closestArcIntersection(c Point, cw bool, pivot, i0, i1 Point) Point {
thetaPivot := pivot.Sub(c).Angle()
dtheta0 := i0.Sub(c).Angle() - thetaPivot
dtheta1 := i1.Sub(c).Angle() - thetaPivot
if cw { // arc runs clockwise, so look the other way around
dtheta0 = -dtheta0
dtheta1 = -dtheta1
}
if angleNorm(dtheta1) < angleNorm(dtheta0) {
return i1
}
return i0
}

// Join adds a join to a right-hand-side and left-hand-side path, of width 2*halfWidth, around a pivot point with starting and ending normals of n0 and n1, and radius of curvatures of the previous and next segments, which are positive for CCW arcs.
func (j ArcsJoiner) Join(rhs, lhs *Path, halfWidth float64, pivot, n0, n1 Point, r0, r1 float64) {
if n0.Equals(n1.Neg()) {
BevelJoin.Join(rhs, lhs, halfWidth, pivot, n0, n1, r0, r1)
Expand All @@ -192,7 +215,7 @@ func (j ArcsJoiner) Join(rhs, lhs *Path, halfWidth float64, pivot, n0, n1 Point,
}
limit := math.Max(j.Limit, 1.001) // 1.001 so that nearly linear joins will not get clipped

cw := n0.Rot90CW().Dot(n1) >= 0.0
cw := 0.0 <= n0.Rot90CW().Dot(n1)
hw := halfWidth
if cw {
hw = -hw // used to calculate |R|, when running CW then n0 and n1 point the other way, so the sign of r0 and r1 is negated
Expand Down Expand Up @@ -223,72 +246,142 @@ func (j ArcsJoiner) Join(rhs, lhs *Path, halfWidth float64, pivot, n0, n1 Point,
}
if !ok {
// no intersection
j.GapJoiner.Join(rhs, lhs, halfWidth, pivot, n0, n1, r0, r1)
BevelJoin.Join(rhs, lhs, halfWidth, pivot, n0, n1, r0, r1)
return
}

// find the closest intersection when following the arc (using either arc r0 or r1 with center c0 or c1 respectively)
c, rcw := c0, r0 < 0.0
if math.IsNaN(r0) {
c, rcw = c1, r1 >= 0.0
}
thetaPivot := pivot.Sub(c).Angle()
dtheta0 := i0.Sub(c).Angle() - thetaPivot
dtheta1 := i1.Sub(c).Angle() - thetaPivot
if rcw { // r runs clockwise, so look the other way around
dtheta0 = -dtheta0
dtheta1 = -dtheta1
}
mid := i0
if angleNorm(dtheta1) < angleNorm(dtheta0) {
mid = i1
var mid Point
if !math.IsNaN(r0) {
mid = closestArcIntersection(c0, r0 < 0.0, pivot, i0, i1)
} else {
mid = closestArcIntersection(c1, 0.0 <= r1, pivot, i0, i1)
}

if !math.IsNaN(limit) && limit*halfWidth < mid.Sub(pivot).Length() {
// check arc limit
d := mid.Sub(pivot).Length()
clip := !math.IsNaN(limit) && limit*halfWidth < d
if clip && j.GapJoiner != nil {
j.GapJoiner.Join(rhs, lhs, halfWidth, pivot, n0, n1, r0, r1)
return
}

mid2 := mid
if clip {
// arcs-clip
start, end := pivot.Add(n0), pivot.Add(n1)
if cw {
start, end = pivot.Sub(n0), pivot.Sub(n1)
}

var clipMid, clipNormal Point
if !math.IsNaN(r0) && !math.IsNaN(r1) && (0.0 < r0) == (0.0 < r1) {
// circle have opposite direction/sweep
// NOTE: this may cause the bevel to be imperfectly oriented
clipMid = mid.Sub(pivot).Norm(limit * halfWidth)
clipNormal = clipMid.Rot90CCW()
} else {
// circle in between both stroke edges
rMid := (r0 - r1) / 2.0
if math.IsNaN(r0) {
rMid = -(r1 + hw) * 2.0
} else if math.IsNaN(r1) {
rMid = (r0 + hw) * 2.0
}

sweep := 0.0 < rMid
RMid := math.Abs(rMid)
cx, cy, a0, _ := ellipseToCenter(pivot.X, pivot.Y, RMid, RMid, 0.0, false, sweep, mid.X, mid.Y)
cMid := Point{cx, cy}
dtheta := limit * halfWidth / rMid

clipMid = EllipsePos(RMid, RMid, 0.0, cMid.X, cMid.Y, a0+dtheta)
clipNormal = ellipseNormal(RMid, RMid, 0.0, sweep, a0+dtheta, 1.0)
}

if math.IsNaN(r1) {
i0, ok = intersectionRayLine(clipMid, clipMid.Add(clipNormal), mid, end)
if !ok {
// not sure when this occurs
BevelJoin.Join(rhs, lhs, halfWidth, pivot, n0, n1, r0, r1)
return
}
mid2 = i0
} else {
i0, i1, ok = intersectionRayCircle(clipMid, clipMid.Add(clipNormal), c1, R1)
if !ok {
// not sure when this occurs
BevelJoin.Join(rhs, lhs, halfWidth, pivot, n0, n1, r0, r1)
return
}
mid2 = closestArcIntersection(c1, 0.0 <= r1, pivot, i0, i1)
}

if math.IsNaN(r0) {
i0, ok = intersectionRayLine(clipMid, clipMid.Add(clipNormal), start, mid)
if !ok {
// not sure when this occurs
BevelJoin.Join(rhs, lhs, halfWidth, pivot, n0, n1, r0, r1)
return
}
mid = i0
} else {
i0, i1, ok = intersectionRayCircle(clipMid, clipMid.Add(clipNormal), c0, R0)
if !ok {
// not sure when this occurs
BevelJoin.Join(rhs, lhs, halfWidth, pivot, n0, n1, r0, r1)
return
}
mid = closestArcIntersection(c0, r0 < 0.0, pivot, i0, i1)
}
}

rEnd := pivot.Add(n1)
lEnd := pivot.Sub(n1)
if cw { // bend to the right, ie. CW
rhs.LineTo(rEnd.X, rEnd.Y)
if math.IsNaN(r0) {
lhs.LineTo(mid.X, mid.Y)
} else {
lhs.ArcTo(R0, R0, 0.0, false, r0 > 0.0, mid.X, mid.Y)
lhs.ArcTo(R0, R0, 0.0, false, 0.0 < r0, mid.X, mid.Y)
}
if clip {
lhs.LineTo(mid2.X, mid2.Y)
}
if math.IsNaN(r1) {
lhs.LineTo(lEnd.X, lEnd.Y)
} else {
lhs.ArcTo(R1, R1, 0.0, false, r1 > 0.0, lEnd.X, lEnd.Y)
lhs.ArcTo(R1, R1, 0.0, false, 0.0 < r1, lEnd.X, lEnd.Y)
}
} else { // bend to the left, ie. CCW
if math.IsNaN(r0) {
rhs.LineTo(mid.X, mid.Y)
} else {
rhs.ArcTo(R0, R0, 0.0, false, r0 > 0.0, mid.X, mid.Y)
rhs.ArcTo(R0, R0, 0.0, false, 0.0 < r0, mid.X, mid.Y)
}
if clip {
rhs.LineTo(mid2.X, mid2.Y)
}
if math.IsNaN(r1) {
rhs.LineTo(rEnd.X, rEnd.Y)
} else {
rhs.ArcTo(R1, R1, 0.0, false, r1 > 0.0, rEnd.X, rEnd.Y)
rhs.ArcTo(R1, R1, 0.0, false, 0.0 < r1, rEnd.X, rEnd.Y)
}
lhs.LineTo(lEnd.X, lEnd.Y)
}
}

func (j ArcsJoiner) String() string {
if math.IsNaN(j.Limit) {
return "Arcs"
if j.GapJoiner == nil {
return "ArcsClip"
}
return "ArcsClip"
return "Arcs"
}

type pathStrokeState struct {
cmd float64
p0, p1 Point // position of start and end
n0, n1 Point // normal of start and end
n0, n1 Point // normal of start and end (points right when walking the path)
r0, r1 float64 // radius of start and end

cp1, cp2 Point // Béziers
Expand Down Expand Up @@ -441,7 +534,7 @@ func offsetSegment(p *Path, halfWidth float64, cr Capper, jr Joiner, tolerance f

if !cur.n1.Equals(next.n0.Neg()) {
// all turns except 0 degrees and 180 degrees are added
cw := cur.n1.Rot90CW().Dot(next.n0) >= 0.0
cw := 0.0 <= cur.n1.Rot90CW().Dot(next.n0)
if cw {
rhsInnerBends = append(rhsInnerBends, len(rhs.d)-cmdLen(LineToCmd))
} else {
Expand Down
28 changes: 14 additions & 14 deletions path_stroke_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ func TestPathStroke(t *testing.T) {
{"M0 0L10 0L10 10", 2.0, ButtCap, BevelJoin, "M0 -1L10 -1L11 0L11 10L9 10L9 1L0 1z"},
{"M0 0L10 0L10 -10", 2.0, ButtCap, BevelJoin, "M0 -1L9 -1L9 -10L11 -10L11 0L10 1L0 1z"},

{"M0 0L10 0L20 0", 2.0, ButtCap, MiterClipJoin(BevelJoin, 2.0), "M0 -1L10 -1L20 -1L20 1L10 1L0 1z"},
{"M0 0L10 0L5 0", 2.0, ButtCap, MiterClipJoin(BevelJoin, 2.0), "M0 -1L10 -1L10 1L5 1L5 -1L10 -1L10 1L0 1z"},
{"M0 0L10 0L10 10", 2.0, ButtCap, MiterClipJoin(BevelJoin, 1.0), "M0 -1L10 -1L11 0L11 10L9 10L9 1L0 1z"},
{"M0 0L10 0L10 10", 2.0, ButtCap, MiterClipJoin(BevelJoin, 2.0), "M0 -1L10 -1L11 -1L11 0L11 10L9 10L9 1L0 1z"},
{"M0 0L10 0L10 -10", 2.0, ButtCap, MiterClipJoin(BevelJoin, 2.0), "M0 -1L9 -1L9 -10L11 -10L11 0L11 1L10 1L0 1z"},
{"M0 0L10 0L20 0", 2.0, ButtCap, MiterJoiner{BevelJoin, 2.0}, "M0 -1L10 -1L20 -1L20 1L10 1L0 1z"},
{"M0 0L10 0L5 0", 2.0, ButtCap, MiterJoiner{BevelJoin, 2.0}, "M0 -1L10 -1L10 1L5 1L5 -1L10 -1L10 1L0 1z"},
{"M0 0L10 0L10 10", 2.0, ButtCap, MiterJoiner{BevelJoin, 1.0}, "M0 -1L10 -1L11 0L11 10L9 10L9 1L0 1z"},
{"M0 0L10 0L10 10", 2.0, ButtCap, MiterJoiner{BevelJoin, 2.0}, "M0 -1L10 -1L11 -1L11 0L11 10L9 10L9 1L0 1z"},
{"M0 0L10 0L10 -10", 2.0, ButtCap, MiterJoiner{BevelJoin, 2.0}, "M0 -1L9 -1L9 -10L11 -10L11 0L11 1L10 1L0 1z"},

{"M0 0L10 0L20 0", 2.0, ButtCap, ArcsClipJoin(BevelJoin, 2.0), "M0 -1L10 -1L20 -1L20 1L10 1L0 1z"},
{"M0 0L10 0L5 0", 2.0, ButtCap, ArcsClipJoin(BevelJoin, 2.0), "M0 -1L10 -1L10 1L5 1L5 -1L10 -1L10 1L0 1z"},
{"M0 0L10 0L10 10", 2.0, ButtCap, ArcsClipJoin(BevelJoin, 1.0), "M0 -1L10 -1L11 0L11 10L9 10L9 1L0 1z"},
{"M0 0L10 0L20 0", 2.0, ButtCap, ArcsJoiner{BevelJoin, 2.0}, "M0 -1L10 -1L20 -1L20 1L10 1L0 1z"},
{"M0 0L10 0L5 0", 2.0, ButtCap, ArcsJoiner{BevelJoin, 2.0}, "M0 -1L10 -1L10 1L5 1L5 -1L10 -1L10 1L0 1z"},
{"M0 0L10 0L10 10", 2.0, ButtCap, ArcsJoiner{BevelJoin, 1.0}, "M0 -1L10 -1L11 0L11 10L9 10L9 1L0 1z"},

{"M0 0L10 0L10 10L0 10z", 2.0, ButtCap, BevelJoin, "M0 -1L10 -1L11 0L11 10L10 11L0 11L-1 10L-1 0zM1 1L1 9L9 9L9 1z"},
{"M0 0L0 10L10 10L10 0z", 2.0, ButtCap, BevelJoin, "M-1 0L0 -1L10 -1L11 0L11 10L10 11L0 11L-1 10zM1 1L1 9L9 9L9 1z"},
Expand All @@ -56,16 +56,16 @@ func TestPathStroke(t *testing.T) {
{"M0 0A5 5 0 0 0 10 0A10 10 0 0 1 0 10", 2.0, ButtCap, ArcsJoin, "M1 0A4 4 0 0 0 9 0L11 0A11 11 0 0 1 0 11L0 9A9 9 0 0 0 9 0L11 0A6 6 0 0 1 -1 0z"},

// circle and line intersecting in one point
{"M0 0A2 2 0 0 1 2 2L5 2", 2.0, ButtCap, ArcsClipJoin(BevelJoin, 10.0), "M0 -1A3 3 0 0 1 3 2L2 1L5 1L5 3L2 3L0 3A1 1 0 0 0 1 2A1 1 0 0 0 0 1z"},
{"M0 4A2 2 0 0 0 2 2L5 2", 2.0, ButtCap, ArcsClipJoin(BevelJoin, 10.0), "M0 3A1 1 0 0 0 1 2A1 1 0 0 0 0 1L2 1L5 1L5 3L2 3L3 2A3 3 0 0 1 0 5z"},
{"M5 2L2 2A2 2 0 0 0 0 0", 2.0, ButtCap, ArcsClipJoin(BevelJoin, 10.0), "M5 3L2 3L0 3A1 1 0 0 0 1 2A1 1 0 0 0 0 1L0-1A3 3 0 0 1 3 2L2 1L5 1z"},
{"M5 2L2 2A2 2 0 0 1 0 4", 2.0, ButtCap, ArcsClipJoin(BevelJoin, 10.0), "M5 3L2 3L3 2A3 3 0 0 1 0 5L0 3A1 1 0 0 0 1 2A1 1 0 0 0 0 1L2 1L5 1z"},
{"M0 0A2 2 0 0 1 2 2L5 2", 2.0, ButtCap, ArcsJoiner{BevelJoin, 10.0}, "M0 -1A3 3 0 0 1 3 2L2 1L5 1L5 3L2 3L0 3A1 1 0 0 0 1 2A1 1 0 0 0 0 1z"},
{"M0 4A2 2 0 0 0 2 2L5 2", 2.0, ButtCap, ArcsJoiner{BevelJoin, 10.0}, "M0 3A1 1 0 0 0 1 2A1 1 0 0 0 0 1L2 1L5 1L5 3L2 3L3 2A3 3 0 0 1 0 5z"},
{"M5 2L2 2A2 2 0 0 0 0 0", 2.0, ButtCap, ArcsJoiner{BevelJoin, 10.0}, "M5 3L2 3L0 3A1 1 0 0 0 1 2A1 1 0 0 0 0 1L0-1A3 3 0 0 1 3 2L2 1L5 1z"},
{"M5 2L2 2A2 2 0 0 1 0 4", 2.0, ButtCap, ArcsJoiner{BevelJoin, 10.0}, "M5 3L2 3L3 2A3 3 0 0 1 0 5L0 3A1 1 0 0 0 1 2A1 1 0 0 0 0 1L2 1L5 1z"},

// cut by limit
{"M0 0A2 2 0 0 1 2 2L5 2", 2.0, ButtCap, ArcsClipJoin(BevelJoin, 1.0), "M0 -1A3 3 0 0 1 3 2L2 1L5 1L5 3L2 3L1 2A1 1 0 0 0 0 1z"},
{"M0 0A2 2 0 0 1 2 2L5 2", 2.0, ButtCap, ArcsJoiner{BevelJoin, 1.0}, "M0 -1A3 3 0 0 1 3 2L2 1L5 1L5 3L2 3L1 2A1 1 0 0 0 0 1z"},

// no intersection
{"M0 0A2 2 0 0 1 2 2L5 2", 3.0, ButtCap, ArcsClipJoin(BevelJoin, 10.0), "M0 -1.5A3.5 3.5 0 0 1 3.5 2L2 .5L5 .5L5 3.5L2 3.5L.5 2A.5 .5 0 0 0 0 1.5z"},
{"M0 0A2 2 0 0 1 2 2L5 2", 3.0, ButtCap, ArcsJoiner{BevelJoin, 10.0}, "M0 -1.5A3.5 3.5 0 0 1 3.5 2L2 .5L5 .5L5 3.5L2 3.5L.5 2A.5 .5 0 0 0 0 1.5z"},
}
for _, tt := range tts {
t.Run(tt.orig, func(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions path_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func ellipseDeriv2(rx, ry, phi float64, theta float64) Point {
}

func ellipseCurvatureRadius(rx, ry float64, sweep bool, theta float64) float64 {
// positive for ccw / sweep
// phi has no influence on the curvature
dp := ellipseDeriv(rx, ry, 0.0, sweep, theta)
ddp := ellipseDeriv2(rx, ry, 0.0, theta)
Expand Down
Loading

0 comments on commit 58c7d8e

Please sign in to comment.