package template import ( "errors" "fmt" "gopkg.in/Knetic/govaluate.v3" "math" "strconv" "git.wtrh.nl/wouter/gopatterns/pkg/pattern" "git.wtrh.nl/wouter/gopatterns/pkg/pattern/point" ) 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") ) // 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 { return map[string]govaluate.ExpressionFunction{ "DistanceBetween": func(args ...interface{}) (interface{}, error) { id0, err := toPointID(args[0]) if err != nil { return nil, fmt.Errorf("parsing args[0] to pointID: %w", err) } id1, err := toPointID(args[1]) if err != nil { return nil, fmt.Errorf("parsing args[0] to pointID: %w", err) } p0, err := p.getOrCreate(id0, pat, 0) if err != nil { return nil, fmt.Errorf("get or create point %q: %w", id0, err) } p1, err := p.getOrCreate(id1, pat, 0) if err != nil { return nil, fmt.Errorf("get or create point %q: %w", id1, err) } return p0.Position().Distance(p1.Position()), nil }, "AngleBetween": func(args ...interface{}) (interface{}, error) { id0, err := toPointID(args[0]) if err != nil { return nil, fmt.Errorf("parsing args[0] to pointID: %w", err) } id1, err := toPointID(args[1]) if err != nil { return nil, fmt.Errorf("parsing args[0] to pointID: %w", err) } p0, err := p.getOrCreate(id0, pat, 0) if err != nil { return nil, fmt.Errorf("get or create point %q: %w", id0, err) } p1, err := p.getOrCreate(id1, pat, 0) if err != nil { return nil, fmt.Errorf("get or create point %q: %w", id1, err) } return p0.Vector().AngleBetween(p1.Vector()), 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) { return math.Acos(args[0].(float64)), nil }, "atan2": func(args ...interface{}) (interface{}, error) { return math.Atan2(args[0].(float64), args[1].(float64)), 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 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 } 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 } 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 } type ExtendPoint struct { From point.ID `yaml:"from"` To point.ID `yaml:"to"` Offset *Value `yaml:"offset"` } 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 }