path_ops.go

Functions

Functions

func Area

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.

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
}