|
- package template
-
- import (
- "errors"
- "fmt"
- "git.wtrh.nl/patterns/gopatterns/pkg/path"
- "git.wtrh.nl/patterns/gopatterns/pkg/util"
- "maps"
- "math"
- "strconv"
-
- "git.wtrh.nl/patterns/gopatterns/pkg/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[util.ID]Point
-
- // Point contains the template information for a point.
- type Point struct {
- Position Position `yaml:"position"`
- RelativeTo *util.ID `yaml:"relativeTo,omitempty"`
- Description string `yaml:"description"`
- Between *BetweenPoint `yaml:"between"`
- Extend *ExtendPoint `yaml:"extend"`
- Hide bool `yaml:"hide"`
- Polar *PolarPoint `yaml:"polar"`
- }
-
- func (t Template) templatePoint(panelName string, id util.ID) (Point, error) {
- if id.Panel() != "" {
- panelName = id.Panel()
- }
-
- panel, err := t.Panel(panelName)
- if !errors.Is(err, ErrPanelNotFound) {
- p, ok := panel.Points[id.Deref()]
- if ok {
- return p, nil
- }
- }
-
- p, ok := t.Points[id.Deref()]
- if !ok {
- return Point{}, ErrPointNotFound
- }
-
- return p, nil
- }
-
- var ErrInvalidPointID = errors.New("type cannot be converted to a PointID")
-
- func (t Template) functions(req request) map[string]govaluate.ExpressionFunction {
- functions := t.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 := t.getOrCreatePointsFromArgs(req, 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 := t.getOrCreatePointsFromArgs(req, 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 := t.getOrCreatePointsFromArgs(req, args...)
- if err != nil {
- return nil, err
- }
-
- return points[1].Vector().Y - points[0].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 := t.getOrCreatePointsFromArgs(req, args...)
- if err != nil {
- return nil, err
- }
-
- return points[1].Vector().X - points[0].Vector().X, nil
- },
- "LineLength": func(args ...interface{}) (interface{}, error) {
- if len(args) != 1 {
- return nil, fmt.Errorf("function LineLength() requires 2 arguments: %w", ErrInvalidArguments)
- }
-
- line, err := t.getOrCreateLinesFromArgs(req, args...)
- if err != nil {
- return nil, err
- }
-
- return line[0].Length()
- },
- })
-
- return functions
- }
-
- func (t Template) getOrCreatePointsFromArgs(req request, args ...interface{}) ([]point.Point, error) {
- points := make([]point.Point, 0, len(args))
-
- for i, arg := range args {
- id, err := toID(arg)
- if err != nil {
- return nil, fmt.Errorf("parsing args[%d] to ID: %w", i, err)
- }
-
- newPoint, err := t.getOrCreatePoint(id, req, 0)
- if err != nil {
- return nil, fmt.Errorf("get or create point %q: %w", id, err)
- }
-
- points = append(points, newPoint)
- }
-
- return points, nil
- }
-
- func (t Template) getOrCreateLinesFromArgs(req request, args ...interface{}) ([]path.Path, error) {
- points := make([]path.Path, 0, len(args))
-
- for i, arg := range args {
- id, err := toID(arg)
- if err != nil {
- return nil, fmt.Errorf("parsing args[%d] to ID: %w", i, err)
- }
-
- newPoint, err := t.getOrCreateLine(id, req, 0)
- if err != nil {
- return nil, fmt.Errorf("get or create line %q: %w", id, err)
- }
-
- points = append(points, newPoint)
- }
-
- return points, nil
- }
-
- func toID(arg interface{}) (util.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)
- }
-
- return util.ID(v1), nil
- }
-
- // BetweenPoint contains the template information for a point in between two other points.
- type BetweenPoint struct {
- From util.ID `yaml:"from"`
- To util.ID `yaml:"to"`
- Offset *Value `yaml:"offset"`
- }
-
- func (t Template) 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
- },
- "abs": func(args ...interface{}) (interface{}, error) {
- if len(args) != 1 {
- return nil, fmt.Errorf("function abs() requires 1 argument: %w", ErrInvalidArguments)
- }
-
- x, ok := args[0].(float64)
- if !ok {
- return nil, fmt.Errorf("evaluate abs(): parsing %q as float64: %w", args[0], ErrInvalidArguments)
- }
-
- return math.Abs(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
- },
- }
- }
-
- func (t Template) createPoint(id util.ID, req request, depth int) (p point.Point, err error) {
- templatePoint, err := t.templatePoint(req.name, id)
- if err != nil {
- return nil, fmt.Errorf("creating point: %w", err)
- }
-
- var newPoint point.Point
-
- switch {
- case templatePoint.RelativeTo != nil && templatePoint.Polar != nil:
- newPoint, err = t.createPolar(id, req, depth)
- if err != nil {
- return nil, err
- }
- case templatePoint.RelativeTo != nil:
- newPoint, err = t.createRelative(id, req, depth)
- if err != nil {
- return nil, err
- }
- case templatePoint.Between != nil:
- newPoint, err = t.createBetween(id, req, depth)
- if err != nil {
- return nil, err
- }
- case templatePoint.Extend != nil:
- newPoint, err = t.createExtend(id, req, depth)
- if err != nil {
- return nil, err
- }
- default:
- x, y, r, err := templatePoint.Position.evaluate(req.dimensions.Parameters(), t.functions(req))
- if err != nil {
- return nil, err
- }
-
- newPoint = point.NewAbsolutePoint(x, y, r, id)
- }
-
- if templatePoint.Hide {
- newPoint.SetHide()
- }
-
- return newPoint, nil
- }
-
- func (t Template) createRelative(id util.ID, req request, depth int) (*point.RelativePoint, error) {
- templatePoint, err := t.templatePoint(req.name, id)
- if err != nil {
- return nil, err
- }
-
- relativePointID := *templatePoint.RelativeTo
- if relativePointID == id || depth > maxRecursionDepth {
- return nil, ErrRelativePointRecursion
- }
-
- relativePoint, err := t.getOrCreatePoint(relativePointID, req, depth)
- if err != nil {
- return nil, err
- }
-
- x, y, r, err := templatePoint.Position.evaluate(req.dimensions.Parameters(), t.functions(req))
- if err != nil {
- return nil, err
- }
-
- return point.NewRelativePoint(relativePoint).
- WithXOffset(x).WithYOffset(y).WithRotationOffset(r).
- MarkWith(id), nil
- }
-
- //nolint:ireturn,dupl
- func (t Template) createBetween(id util.ID, req request, depth int) (point.Point, error) {
- newPoint, err := t.templatePoint(req.name, id)
- if err != nil {
- return nil, err
- }
-
- if newPoint.Between.To == id || newPoint.Between.From == id || depth > maxRecursionDepth {
- return nil, ErrRelativePointRecursion
- }
-
- fromPoint, err := t.getOrCreatePoint(newPoint.Between.From, req, depth)
- if err != nil {
- return nil, err
- }
-
- toPoint, err := t.getOrCreatePoint(newPoint.Between.To, req, depth)
- if err != nil {
- return nil, err
- }
-
- params := req.dimensions.Parameters()
-
- offset, err := newPoint.Between.Offset.Evaluate(params, t.functions(req))
- if err != nil {
- return nil, err
- }
-
- return point.NewBetweenPoint(fromPoint, toPoint, offset, id), nil
- }
-
- func (t Template) getOrCreatePoints(ids []util.ID, req request, depth int) ([]point.Point, error) {
- points := make([]point.Point, 0, len(ids))
-
- for _, id := range ids {
- createPoint, err := t.getOrCreatePoint(id, req, depth)
- if err != nil {
- return nil, err
- }
-
- points = append(points, createPoint)
- }
-
- return points, nil
- }
-
- //nolint:ireturn
- func (t Template) getOrCreatePoint(id util.ID, req request, depth int) (point.Point, error) {
- p, ok := req.points[id]
- if ok {
- return p, nil
- }
-
- newPoint, err := t.createPoint(id, req, depth+1)
- if err != nil {
- return nil, fmt.Errorf("creating point %q: %w", id, err)
- }
-
- req.points[id] = newPoint
-
- return newPoint, nil
- }
-
- //nolint:ireturn,dupl
- func (t Template) createExtend(id util.ID, req request, depth int) (point.Point, error) {
- newPoint, err := t.templatePoint(req.name, id)
- if err != nil {
- return nil, err
- }
-
- if newPoint.Extend.To == id || newPoint.Extend.From == id || depth > maxRecursionDepth {
- return nil, ErrRelativePointRecursion
- }
-
- fromPoint, err := t.getOrCreatePoint(newPoint.Extend.From, req, depth)
- if err != nil {
- return nil, err
- }
-
- toPoint, err := t.getOrCreatePoint(newPoint.Extend.To, req, depth)
- if err != nil {
- return nil, err
- }
-
- offset, err := newPoint.Extend.Offset.Evaluate(req.dimensions.Parameters(), t.functions(req))
- if err != nil {
- return nil, err
- }
-
- return point.NewExtendPoint(fromPoint, toPoint, offset, id), nil
- }
-
- //nolint:ireturn
- func (t Template) createPolar(id util.ID, req request, depth int) (point.Point, error) {
- templatePoint, err := t.templatePoint(req.name, id)
- if err != nil {
- return nil, err
- }
-
- relativePointID := *templatePoint.RelativeTo
- if relativePointID == id || depth > maxRecursionDepth {
- return nil, ErrRelativePointRecursion
- }
-
- relativePoint, err := t.getOrCreatePoint(relativePointID, req, depth)
- if err != nil {
- return nil, err
- }
-
- x, y, r, err := templatePoint.Polar.evaluate(req.dimensions.Parameters(), t.functions(req))
- if err != nil {
- return nil, err
- }
-
- return point.NewRelativePoint(relativePoint).
- WithXOffset(x).WithYOffset(y).MarkWith(id).WithRotationOffset(r), nil
- }
-
- // ExtendPoint describes how to draw a new point that extends in line with two points.
- type ExtendPoint struct {
- From util.ID `yaml:"from"`
- To util.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, r float64, err error) {
- rotation, err := p.Rotation.Evaluate(params, funcs)
- if err != nil {
- return 0, 0, 0, err
- }
-
- length, err := p.Length.Evaluate(params, funcs)
- if err != nil {
- return 0, 0, 0, err
- }
-
- return math.Cos(rotation) * length, math.Sin(rotation) * length, rotation, nil
- }
|