path_ops.go
Functions
func Area
func (p *Path) Area() float64 {
var area float64
var current, start Point
p.Iterate(func(verb PathVerb, coords []float64) {
switch verb {
case MoveTo:
start = Pt(coords[0], coords[1])
current = start
case LineTo:
pt := Pt(coords[0], coords[1])
area += lineArea(current, pt)
current = pt
case QuadTo:
ctrl := Pt(coords[0], coords[1])
pt := Pt(coords[2], coords[3])
area += quadArea(current, ctrl, pt)
current = pt
case CubicTo:
ctrl1 := Pt(coords[0], coords[1])
ctrl2 := Pt(coords[2], coords[3])
pt := Pt(coords[4], coords[5])
area += cubicArea(current, ctrl1, ctrl2, pt)
current = pt
case Close:
area += lineArea(current, start)
current = start
}
})
return area
}
func BoundingBox
BoundingBox returns the tight axis-aligned bounding box of the path.
Uses curve extrema for accuracy.
func (p *Path) BoundingBox() Rect {
if len(p.verbs) == 0 {
return Rect{}
}
// Initialize with extreme values
bbox := Rect{
Min: Point{X: math.MaxFloat64, Y: math.MaxFloat64},
Max: Point{X: -math.MaxFloat64, Y: -math.MaxFloat64},
}
var current Point
p.Iterate(func(verb PathVerb, coords []float64) {
switch verb {
case MoveTo:
pt := Pt(coords[0], coords[1])
bbox = expandBBox(bbox, pt)
current = pt
case LineTo:
pt := Pt(coords[0], coords[1])
bbox = expandBBox(bbox, pt)
current = pt
case QuadTo:
ctrl := Pt(coords[0], coords[1])
pt := Pt(coords[2], coords[3])
bbox = bbox.Union(quadBBox(current, ctrl, pt))
current = pt
case CubicTo:
ctrl1 := Pt(coords[0], coords[1])
ctrl2 := Pt(coords[2], coords[3])
pt := Pt(coords[4], coords[5])
bbox = bbox.Union(cubicBBox(current, ctrl1, ctrl2, pt))
current = pt
case Close:
// Close doesn't add new points
}
})
// Handle empty path case
if bbox.Min.X == math.MaxFloat64 {
return Rect{}
}
return bbox
}
func Contains
Contains tests if a point is inside the path using the non-zero fill rule.
func (p *Path) Contains(pt Point) bool {
return p.Winding(pt) != 0
}
func Flatten
Flatten converts all curves to line segments with given tolerance.
tolerance is the maximum distance from the curve.
func (p *Path) Flatten(tolerance float64) []Point {
if len(p.verbs) == 0 {
return nil
}
points := make([]Point, 0, len(p.verbs)*4)
p.FlattenCallback(tolerance, func(pt Point) {
points = append(points, pt)
})
return points
}
func FlattenCallback
FlattenCallback calls fn for each point in the flattened path.
More efficient than Flatten() as it avoids allocation.
func (p *Path) FlattenCallback(tolerance float64, fn func(pt Point)) {
if tolerance <= 0 {
tolerance = 0.1 // Default tolerance
}
var current, start Point
var started bool
p.Iterate(func(verb PathVerb, coords []float64) {
switch verb {
case MoveTo:
if started {
fn(current) // Emit last point of previous subpath
}
pt := Pt(coords[0], coords[1])
fn(pt)
start = pt
current = pt
started = true
case LineTo:
pt := Pt(coords[0], coords[1])
fn(pt)
current = pt
case QuadTo:
ctrl := Pt(coords[0], coords[1])
pt := Pt(coords[2], coords[3])
flattenQuad(current, ctrl, pt, tolerance, fn)
current = pt
case CubicTo:
ctrl1 := Pt(coords[0], coords[1])
ctrl2 := Pt(coords[2], coords[3])
pt := Pt(coords[4], coords[5])
flattenCubic(current, ctrl1, ctrl2, pt, tolerance, fn)
current = pt
case Close:
if current != start {
fn(start)
}
current = start
}
})
}
func Length
Length returns the total arc length of the path.
accuracy controls the precision of the approximation (smaller = more accurate).
func (p *Path) Length(accuracy float64) float64 {
if accuracy <= 0 {
accuracy = 0.001 // Default accuracy
}
var length float64
var current Point
p.Iterate(func(verb PathVerb, coords []float64) {
switch verb {
case MoveTo:
current = Pt(coords[0], coords[1])
case LineTo:
pt := Pt(coords[0], coords[1])
length += current.Distance(pt)
current = pt
case QuadTo:
ctrl := Pt(coords[0], coords[1])
pt := Pt(coords[2], coords[3])
length += quadLength(current, ctrl, pt, accuracy)
current = pt
case CubicTo:
ctrl1 := Pt(coords[0], coords[1])
ctrl2 := Pt(coords[2], coords[3])
pt := Pt(coords[4], coords[5])
length += cubicLength(current, ctrl1, ctrl2, pt, accuracy)
current = pt
case Close:
// Close doesn't add length (already computed if there's a closing line)
}
})
return length
}
func Reversed
Reversed returns a new path with reversed direction.
Each subpath is reversed independently.
func (p *Path) Reversed() *Path {
if len(p.verbs) == 0 {
return NewPath()
}
// Collect subpaths
subpaths := p.collectSubpaths()
// Reverse each subpath and build new path
result := NewPath()
for _, sp := range subpaths {
reverseSubpath(sp, result)
}
return result
}
func Winding
Winding returns the winding number of a point relative to the path.
0 = outside, non-zero = inside (for non-zero fill rule).
Uses ray casting with a horizontal ray to the right.
func (p *Path) Winding(pt Point) int {
var winding int
var current, start Point
p.Iterate(func(verb PathVerb, coords []float64) {
switch verb {
case MoveTo:
start = Pt(coords[0], coords[1])
current = start
case LineTo:
ep := Pt(coords[0], coords[1])
winding += lineWinding(current, ep, pt)
current = ep
case QuadTo:
ctrl := Pt(coords[0], coords[1])
ep := Pt(coords[2], coords[3])
winding += quadWinding(current, ctrl, ep, pt)
current = ep
case CubicTo:
ctrl1 := Pt(coords[0], coords[1])
ctrl2 := Pt(coords[2], coords[3])
ep := Pt(coords[4], coords[5])
winding += cubicWinding(current, ctrl1, ctrl2, ep, pt)
current = ep
case Close:
winding += lineWinding(current, start, pt)
current = start
}
})
return winding
}
Area returns the signed area enclosed by the path.
Positive for clockwise paths, negative for counter-clockwise.
Uses the shoelace formula extended for curves (Green's theorem).
Only closed subpaths contribute to the area.