package template import ( "errors" "fmt" "maps" "math" "strconv" "git.wtrh.nl/patterns/gopatterns/pkg/pattern" "git.wtrh.nl/patterns/gopatterns/pkg/pattern/point" "gopkg.in/Knetic/govaluate.v3" ) const maxRecursionDepth = 100 var ( // ErrPointNotFound is returned when a required point is not defined. ErrPointNotFound = errors.New("required point not found") // ErrRelativePointRecursion is returned when a points are relative to itself. ErrRelativePointRecursion = errors.New("point cannot be relative to itself") ErrInvalidArguments = errors.New("invalid arguments to call function") ) // Points contains a map with points. type Points map[point.ID]Point // Point contains the template information for a point. type Point struct { Position Position `yaml:"position"` RelativeTo *point.ID `yaml:"relativeTo,omitempty"` Description string `yaml:"description"` Between *BetweenPoint `yaml:"between"` Extend *ExtendPoint `yaml:"extend"` Hide bool `yaml:"hide"` Polar *PolarPoint `yaml:"polar"` } var ErrInvalidPointID = errors.New("type cannot be converted to a PointID") func (p Points) Functions(pat *pattern.Pattern) map[string]govaluate.ExpressionFunction { functions := p.evaluationFunctions() maps.Copy(functions, map[string]govaluate.ExpressionFunction{ "DistanceBetween": func(args ...interface{}) (interface{}, error) { if len(args) != 2 { return nil, fmt.Errorf("function DistanceBetween() requires 2 arguments: %w", ErrInvalidArguments) } points, err := p.getOrCreateFromArgs(pat, args...) if err != nil { return nil, err } return points[0].Position().Distance(points[1].Position()), nil }, "AngleBetween": func(args ...interface{}) (interface{}, error) { if len(args) != 2 { return nil, fmt.Errorf("function AngleBetween() requires 2 arguments: %w", ErrInvalidArguments) } points, err := p.getOrCreateFromArgs(pat, args...) if err != nil { return nil, err } return points[0].Vector().AngleBetween(points[1].Vector()), nil }, "YDistanceBetween": func(args ...interface{}) (interface{}, error) { if len(args) != 2 { return nil, fmt.Errorf("function DistanceBetween() requires 2 arguments: %w", ErrInvalidArguments) } points, err := p.getOrCreateFromArgs(pat, args...) if err != nil { return nil, err } return points[0].Vector().Y - points[1].Vector().Y, nil }, "XDistanceBetween": func(args ...interface{}) (interface{}, error) { if len(args) != 2 { return nil, fmt.Errorf("function DistanceBetween() requires 2 arguments: %w", ErrInvalidArguments) } points, err := p.getOrCreateFromArgs(pat, args...) if err != nil { return nil, err } return points[0].Vector().X - points[1].Vector().X, nil }, }) return functions } func (p Points) getOrCreateFromArgs(pat *pattern.Pattern, args ...interface{}) ([]point.Point, error) { points := make([]point.Point, 0, len(args)) for i, arg := range args { id, err := toPointID(arg) if err != nil { return nil, fmt.Errorf("parsing args[%d] to pointID: %w", i, err) } newPoint, err := p.getOrCreate(id, pat, 0) if err != nil { return nil, fmt.Errorf("get or create point %q: %w", id, err) } points = append(points, newPoint) } return points, nil } func toPointID(arg interface{}) (point.ID, error) { v1, ok := arg.(string) if !ok { f, ok := arg.(float64) if !ok { return "", fmt.Errorf("parsing %v as PointID: %w", arg, ErrInvalidPointID) } v1 = strconv.FormatFloat(f, 'f', -1, 64) } id1 := point.ID(v1) return id1, nil } // BetweenPoint contains the template information for a point in between two other points. type BetweenPoint struct { From point.ID `yaml:"from"` To point.ID `yaml:"to"` Offset *Value `yaml:"offset"` } func (p Points) evaluationFunctions() map[string]govaluate.ExpressionFunction { return map[string]govaluate.ExpressionFunction{ "acos": func(args ...interface{}) (interface{}, error) { if len(args) != 1 { return nil, fmt.Errorf("function acos() requires 1 argument: %w", ErrInvalidArguments) } x, ok := args[0].(float64) if !ok { return nil, fmt.Errorf("evaluate acos(): parsing %q as float64: %w", args[0], ErrInvalidArguments) } return math.Acos(x), nil }, "asin": func(args ...interface{}) (interface{}, error) { if len(args) != 1 { return nil, fmt.Errorf("function asin() requires 1 argument: %w", ErrInvalidArguments) } x, ok := args[0].(float64) if !ok { return nil, fmt.Errorf("evaluate asin(): parsing %q as float64: %w", args[0], ErrInvalidArguments) } return math.Asin(x), nil }, "atan2": func(args ...interface{}) (interface{}, error) { if len(args) != 2 { return nil, fmt.Errorf("function atan2() requires 2 arguments: %w", ErrInvalidArguments) } x, ok := args[0].(float64) if !ok { return nil, fmt.Errorf("evaluate atan2(): parsing %q as float64: %w", args[0], ErrInvalidArguments) } y, ok := args[1].(float64) if !ok { return nil, fmt.Errorf("evaluate atan2(): parsing %q as float64: %w", args[0], ErrInvalidArguments) } return math.Atan2(x, y), nil }, } } // AddToPattern will add all points to the provided [pattern.Pattern]. func (p Points) AddToPattern(pat *pattern.Pattern) error { for id := range p { if pat.GetPoint(id) == nil { err := p.addSingleToPattern(id, pat, 0) if err != nil { return err } } } return nil } func (p Points) addSingleToPattern(id point.ID, pat *pattern.Pattern, depth int) (err error) { templatePoint, ok := p[id] if !ok { return ErrPointNotFound } var newPoint point.Point switch { case templatePoint.RelativeTo != nil && templatePoint.Polar != nil: newPoint, err = p.createPolar(id, pat, depth) if err != nil { return err } case templatePoint.RelativeTo != nil: newPoint, err = p.createRelative(id, pat, depth) if err != nil { return err } case templatePoint.Between != nil: newPoint, err = p.createBetween(id, pat, depth) if err != nil { return err } case templatePoint.Extend != nil: newPoint, err = p.createExtend(id, pat, depth) if err != nil { return err } default: x, y, r, err := templatePoint.Position.evaluate(pat.Parameters(), p.Functions(pat)) if err != nil { return err } newPoint = point.NewAbsolutePoint(x, y, r, id) } if templatePoint.Hide { newPoint.SetHide() } pat.AddPoint(newPoint) return nil } func (p Points) createRelative( id point.ID, pat *pattern.Pattern, depth int, ) (*point.RelativePoint, error) { templatePoint, ok := p[id] if !ok { return nil, ErrPointNotFound } relativePointID := *templatePoint.RelativeTo if relativePointID == id || depth > maxRecursionDepth { return nil, ErrRelativePointRecursion } relativePoint, err := p.getOrCreate(relativePointID, pat, depth) if err != nil { return nil, err } x, y, r, err := templatePoint.Position.evaluate(pat.Parameters(), p.Functions(pat)) if err != nil { return nil, err } return point.NewRelativePoint(relativePoint). WithXOffset(x).WithYOffset(y).WithRotationOffset(r). MarkWith(id), nil } //nolint:ireturn,dupl func (p Points) createBetween(id point.ID, pat *pattern.Pattern, depth int) (point.Point, error) { newPoint, ok := p[id] if !ok { return nil, ErrPointNotFound } if newPoint.Between.To == id || newPoint.Between.From == id || depth > maxRecursionDepth { return nil, ErrRelativePointRecursion } fromPoint, err := p.getOrCreate(newPoint.Between.From, pat, depth) if err != nil { return nil, err } toPoint, err := p.getOrCreate(newPoint.Between.To, pat, depth) if err != nil { return nil, err } params := pat.Parameters() offset, err := newPoint.Between.Offset.Evaluate(params, p.Functions(pat)) if err != nil { return nil, err } return point.NewBetweenPoint(fromPoint, toPoint, offset, id), nil } //nolint:ireturn func (p Points) getOrCreate(id point.ID, pat *pattern.Pattern, depth int) (point.Point, error) { if pat.GetPoint(id) == nil { err := p.addSingleToPattern(id, pat, depth+1) if err != nil { return nil, err } } createdPoint := pat.GetPoint(id) if createdPoint == nil { panic("getPoint cannot be nil") } return createdPoint, nil } //nolint:ireturn,dupl func (p Points) createExtend(id point.ID, pat *pattern.Pattern, depth int) (point.Point, error) { newPoint, ok := p[id] if !ok { return nil, ErrPointNotFound } if newPoint.Extend.To == id || newPoint.Extend.From == id || depth > maxRecursionDepth { return nil, ErrRelativePointRecursion } fromPoint, err := p.getOrCreate(newPoint.Extend.From, pat, depth) if err != nil { return nil, err } toPoint, err := p.getOrCreate(newPoint.Extend.To, pat, depth) if err != nil { return nil, err } params := pat.Parameters() offset, err := newPoint.Extend.Offset.Evaluate(params, p.Functions(pat)) if err != nil { return nil, err } return point.NewExtendPoint(fromPoint, toPoint, offset, id), nil } //nolint:ireturn func (p Points) createPolar(id point.ID, pat *pattern.Pattern, depth int) (point.Point, error) { templatePoint, ok := p[id] if !ok { return nil, ErrPointNotFound } relativePointID := *templatePoint.RelativeTo if relativePointID == id || depth > maxRecursionDepth { return nil, ErrRelativePointRecursion } relativePoint, err := p.getOrCreate(relativePointID, pat, depth) if err != nil { return nil, err } x, y, err := templatePoint.Polar.evaluate(pat.Parameters(), p.Functions(pat)) if err != nil { return nil, err } return point.NewRelativePoint(relativePoint). WithXOffset(x).WithYOffset(y).MarkWith(id), nil } // ExtendPoint describes how to draw a new point that extends in line with two points. type ExtendPoint struct { From point.ID `yaml:"from"` To point.ID `yaml:"to"` Offset *Value `yaml:"offset"` } // PolarPoint describes how to draw a new point with a direction and a distance from the current // position. type PolarPoint struct { Length *Value `yaml:"length"` Rotation *Value `yaml:"rotation"` } func (p PolarPoint) evaluate( params govaluate.MapParameters, funcs map[string]govaluate.ExpressionFunction, ) (x, y float64, err error) { rotation, err := p.Rotation.Evaluate(params, funcs) if err != nil { return 0, 0, err } length, err := p.Length.Evaluate(params, funcs) if err != nil { return 0, 0, err } return math.Sin(rotation) * -length, math.Cos(rotation) * -length, nil }