package path import ( "fmt" "git.wtrh.nl/patterns/gopatterns/pkg/util" "log/slog" "git.wtrh.nl/patterns/gopatterns/pkg/point" "github.com/tdewolff/canvas" splines "gitlab.com/Achilleshiel/gosplines" ) const resolution = 40 // Spline defines a smooth curved path through points. type Spline struct { *Polygon start, end point.Point } type SplineOpts struct { Start, End point.Point Points []point.Point Style Style ID util.ID } // NewSpline returns a new spline through points. Start and end points can be provided as // the start and stop direction of the spline. When start or end point arguments are left nil there // are no constraints on the direction. func NewSpline(opts SplineOpts) Spline { s := Spline{ Polygon: NewPolygon(opts.Points, opts.Style, opts.ID), start: opts.Start, end: opts.End, } if opts.Start == nil && len(opts.Points) > 1 { s.start = opts.Points[0] } if opts.End == nil && len(opts.Points) > 1 { s.end = opts.Points[len(opts.Points)-1] } return s } // Draw the spline to the provided [canvas.Canvas]. func (s Spline) Draw(c *canvas.Canvas) error { points, err := s.build() if err != nil { return fmt.Errorf("generating spline: %w", err) } path := NewPolygon(points, s.style, s.id) if err = path.Draw(c); err != nil { return fmt.Errorf("draw spline points to canvas: %w", err) } length, _ := s.Length() slog.Debug("Draw spline", "length", length, "from", points[0].Name(), "to", points[len(points)-1].Name()) return nil } func (s Spline) Length() (float64, error) { points, err := s.build() if err != nil { return 0.0, fmt.Errorf("generating spline: %w", err) } length := 0.0 for i := range points[1:] { length += points[i].Position().Distance(points[i+1].Position()) } return length, nil } func (s Spline) build() (points []point.Point, err error) { if len(s.points) < 2 { return s.points, nil } x := make([]float64, len(s.points)) y := make([]float64, len(s.points)) for i, p := range s.points { x[i], y[i] = p.Vector().Values() } diffStart := s.start.Vector().Subtract(s.points[0].Vector()) diffEnd := s.points[len(s.points)-1].Vector().Subtract(s.end.Vector()) xCoefficient, err := splines.SolveSplineWithConstraint(x, diffStart.X, diffEnd.X) if err != nil { return nil, fmt.Errorf("unable to calculate coefficients for x: %w", err) } yCoefficient, err := splines.SolveSplineWithConstraint(y, diffStart.Y, diffEnd.Y) if err != nil { return nil, fmt.Errorf("unable to calculate coefficients for y: %w", err) } points = make([]point.Point, 0, len(x)*resolution) stepSize := 1.0 / float64(resolution-1) for i := range len(s.points) - 1 { points = append(points, s.points[i]) for t := stepSize; t < 1.0; t += stepSize { xCalculated := xCoefficient[i].Calculate(t) yCalculated := yCoefficient[i].Calculate(t) points = append(points, point.NewAbsolutePoint(xCalculated, yCalculated, 0, "0")) } } points = append(points, s.points[len(s.points)-1]) return points, nil }