You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

492 lines
13KB

  1. package template
  2. import (
  3. "errors"
  4. "fmt"
  5. "maps"
  6. "math"
  7. "strconv"
  8. "git.wtrh.nl/patterns/gopatterns/pkg/path"
  9. "git.wtrh.nl/patterns/gopatterns/pkg/point"
  10. "git.wtrh.nl/patterns/gopatterns/pkg/util"
  11. "gopkg.in/Knetic/govaluate.v3"
  12. )
  13. const maxRecursionDepth = 100
  14. var (
  15. // ErrPointNotFound is returned when a required point is not defined.
  16. ErrPointNotFound = errors.New("required point not found")
  17. // ErrRelativePointRecursion is returned when a points are relative to itself.
  18. ErrRelativePointRecursion = errors.New("point cannot be relative to itself")
  19. ErrInvalidArguments = errors.New("invalid arguments to call function")
  20. )
  21. // Points contains a map with points.
  22. type Points map[util.ID]Point
  23. // Point contains the template information for a point.
  24. type Point struct {
  25. Position Position `yaml:"position"`
  26. RelativeTo *util.ID `yaml:"relativeTo,omitempty"`
  27. Description string `yaml:"description"`
  28. Between *BetweenPoint `yaml:"between"`
  29. Extend *ExtendPoint `yaml:"extend"`
  30. Hide bool `yaml:"hide"`
  31. Polar *PolarPoint `yaml:"polar"`
  32. }
  33. func (t Template) templatePoint(panelName string, id util.ID) (Point, error) {
  34. if id.Panel() != "" {
  35. panelName = id.Panel()
  36. }
  37. panel, err := t.Panel(panelName)
  38. if !errors.Is(err, ErrPanelNotFound) {
  39. p, ok := panel.Points[id.Deref()]
  40. if ok {
  41. return p, nil
  42. }
  43. }
  44. p, ok := t.Points[id.Deref()]
  45. if !ok {
  46. return Point{}, ErrPointNotFound
  47. }
  48. return p, nil
  49. }
  50. var ErrInvalidPointID = errors.New("type cannot be converted to a PointID")
  51. func (t Template) functions(req request) map[string]govaluate.ExpressionFunction {
  52. functions := t.evaluationFunctions()
  53. maps.Copy(functions, map[string]govaluate.ExpressionFunction{
  54. "DistanceBetween": func(args ...interface{}) (interface{}, error) {
  55. if len(args) != 2 {
  56. return nil, fmt.Errorf("function DistanceBetween() requires 2 arguments: %w",
  57. ErrInvalidArguments)
  58. }
  59. points, err := t.getOrCreatePointsFromArgs(req, args...)
  60. if err != nil {
  61. return nil, err
  62. }
  63. return points[0].Position().Distance(points[1].Position()), nil
  64. },
  65. "AngleBetween": func(args ...interface{}) (interface{}, error) {
  66. if len(args) != 2 {
  67. return nil, fmt.Errorf("function AngleBetween() requires 2 arguments: %w",
  68. ErrInvalidArguments)
  69. }
  70. points, err := t.getOrCreatePointsFromArgs(req, args...)
  71. if err != nil {
  72. return nil, err
  73. }
  74. return points[0].Vector().AngleBetween(points[1].Vector()), nil
  75. },
  76. "YDistanceBetween": func(args ...interface{}) (interface{}, error) {
  77. if len(args) != 2 {
  78. return nil, fmt.Errorf("function DistanceBetween() requires 2 arguments: %w",
  79. ErrInvalidArguments)
  80. }
  81. points, err := t.getOrCreatePointsFromArgs(req, args...)
  82. if err != nil {
  83. return nil, err
  84. }
  85. return points[1].Vector().Y - points[0].Vector().Y, nil
  86. },
  87. "XDistanceBetween": func(args ...interface{}) (interface{}, error) {
  88. if len(args) != 2 {
  89. return nil, fmt.Errorf("function DistanceBetween() requires 2 arguments: %w",
  90. ErrInvalidArguments)
  91. }
  92. points, err := t.getOrCreatePointsFromArgs(req, args...)
  93. if err != nil {
  94. return nil, err
  95. }
  96. return points[1].Vector().X - points[0].Vector().X, nil
  97. },
  98. "LineLength": func(args ...interface{}) (interface{}, error) {
  99. if len(args) != 1 {
  100. return nil, fmt.Errorf("function LineLength() requires 2 arguments: %w", ErrInvalidArguments)
  101. }
  102. line, err := t.getOrCreateLinesFromArgs(req, args...)
  103. if err != nil {
  104. return nil, err
  105. }
  106. return line[0].Length()
  107. },
  108. "DiagonalTo": func(args ...interface{}) (interface{}, error) {
  109. if len(args) != 3 {
  110. return nil, fmt.Errorf("function DiagonalTo() requires 3 arguments: %w", ErrInvalidArguments)
  111. }
  112. points, err := t.getOrCreatePointsFromArgs(req, args[0:2]...)
  113. if err != nil {
  114. return nil, err
  115. }
  116. f, ok := args[2].(float64)
  117. if !ok {
  118. return nil, fmt.Errorf("function DiagonalTo() requires the third argument to be a float: %w",
  119. ErrInvalidArguments)
  120. }
  121. return math.Sqrt(math.Pow(f, 2) - math.Pow(points[0].Position().Distance(points[1].Position()), 2)), nil
  122. },
  123. })
  124. return functions
  125. }
  126. func (t Template) getOrCreatePointsFromArgs(req request, args ...interface{}) ([]point.Point, error) {
  127. points := make([]point.Point, 0, len(args))
  128. for i, arg := range args {
  129. id, err := toID(arg)
  130. if err != nil {
  131. return nil, fmt.Errorf("parsing args[%d] to ID: %w", i, err)
  132. }
  133. newPoint, err := t.getOrCreatePoint(id, req, 0)
  134. if err != nil {
  135. return nil, fmt.Errorf("get or create point %q: %w", id, err)
  136. }
  137. points = append(points, newPoint)
  138. }
  139. return points, nil
  140. }
  141. func (t Template) getOrCreateLinesFromArgs(req request, args ...interface{}) ([]path.Path, error) {
  142. points := make([]path.Path, 0, len(args))
  143. for i, arg := range args {
  144. id, err := toID(arg)
  145. if err != nil {
  146. return nil, fmt.Errorf("parsing args[%d] to ID: %w", i, err)
  147. }
  148. newPoint, err := t.getOrCreateLine(id, req, 0)
  149. if err != nil {
  150. return nil, fmt.Errorf("get or create line %q: %w", id, err)
  151. }
  152. points = append(points, newPoint)
  153. }
  154. return points, nil
  155. }
  156. func toID(arg interface{}) (util.ID, error) {
  157. v1, ok := arg.(string)
  158. if !ok {
  159. f, ok := arg.(float64)
  160. if !ok {
  161. return "", fmt.Errorf("parsing %v as PointID: %w", arg, ErrInvalidPointID)
  162. }
  163. v1 = strconv.FormatFloat(f, 'f', -1, 64)
  164. }
  165. return util.ID(v1), nil
  166. }
  167. // BetweenPoint contains the template information for a point in between two other points.
  168. type BetweenPoint struct {
  169. From util.ID `yaml:"from"`
  170. To util.ID `yaml:"to"`
  171. Offset *Value `yaml:"offset"`
  172. }
  173. func (t Template) evaluationFunctions() map[string]govaluate.ExpressionFunction {
  174. return map[string]govaluate.ExpressionFunction{
  175. "acos": func(args ...interface{}) (interface{}, error) {
  176. if len(args) != 1 {
  177. return nil, fmt.Errorf("function acos() requires 1 argument: %w", ErrInvalidArguments)
  178. }
  179. x, ok := args[0].(float64)
  180. if !ok {
  181. return nil, fmt.Errorf("evaluate acos(): parsing %q as float64: %w", args[0], ErrInvalidArguments)
  182. }
  183. return math.Acos(x), nil
  184. },
  185. "asin": func(args ...interface{}) (interface{}, error) {
  186. if len(args) != 1 {
  187. return nil, fmt.Errorf("function asin() requires 1 argument: %w", ErrInvalidArguments)
  188. }
  189. x, ok := args[0].(float64)
  190. if !ok {
  191. return nil, fmt.Errorf("evaluate asin(): parsing %q as float64: %w", args[0], ErrInvalidArguments)
  192. }
  193. return math.Asin(x), nil
  194. },
  195. "abs": func(args ...interface{}) (interface{}, error) {
  196. if len(args) != 1 {
  197. return nil, fmt.Errorf("function abs() requires 1 argument: %w", ErrInvalidArguments)
  198. }
  199. x, ok := args[0].(float64)
  200. if !ok {
  201. return nil, fmt.Errorf("evaluate abs(): parsing %q as float64: %w", args[0], ErrInvalidArguments)
  202. }
  203. return math.Abs(x), nil
  204. },
  205. "atan2": func(args ...interface{}) (interface{}, error) {
  206. if len(args) != 2 {
  207. return nil, fmt.Errorf("function atan2() requires 2 arguments: %w", ErrInvalidArguments)
  208. }
  209. x, ok := args[0].(float64)
  210. if !ok {
  211. return nil, fmt.Errorf("evaluate atan2(): parsing %q as float64: %w", args[0], ErrInvalidArguments)
  212. }
  213. y, ok := args[1].(float64)
  214. if !ok {
  215. return nil, fmt.Errorf("evaluate atan2(): parsing %q as float64: %w", args[0], ErrInvalidArguments)
  216. }
  217. return math.Atan2(x, y), nil
  218. },
  219. }
  220. }
  221. func (t Template) createPoint(id util.ID, req request, depth int) (p point.Point, err error) {
  222. templatePoint, err := t.templatePoint(req.panel, id)
  223. if err != nil {
  224. return nil, fmt.Errorf("creating point: %w", err)
  225. }
  226. var newPoint point.Point
  227. switch {
  228. case templatePoint.RelativeTo != nil && templatePoint.Polar != nil:
  229. newPoint, err = t.createPolar(id, req, depth)
  230. if err != nil {
  231. return nil, err
  232. }
  233. case templatePoint.RelativeTo != nil:
  234. newPoint, err = t.createRelative(id, req, depth)
  235. if err != nil {
  236. return nil, err
  237. }
  238. case templatePoint.Between != nil:
  239. newPoint, err = t.createBetween(id, req, depth)
  240. if err != nil {
  241. return nil, err
  242. }
  243. case templatePoint.Extend != nil:
  244. newPoint, err = t.createExtend(id, req, depth)
  245. if err != nil {
  246. return nil, err
  247. }
  248. default:
  249. x, y, r, err := templatePoint.Position.evaluate(req.dimensions.Parameters(), t.functions(req))
  250. if err != nil {
  251. return nil, err
  252. }
  253. newPoint = point.NewAbsolutePoint(x, y, r, id)
  254. }
  255. if templatePoint.Hide {
  256. newPoint.SetHide()
  257. }
  258. return newPoint, nil
  259. }
  260. func (t Template) createRelative(id util.ID, req request, depth int) (*point.RelativePoint, error) {
  261. templatePoint, err := t.templatePoint(req.panel, id)
  262. if err != nil {
  263. return nil, err
  264. }
  265. relativePointID := *templatePoint.RelativeTo
  266. if relativePointID == id || depth > maxRecursionDepth {
  267. return nil, ErrRelativePointRecursion
  268. }
  269. relativePoint, err := t.getOrCreatePoint(relativePointID, req, depth)
  270. if err != nil {
  271. return nil, err
  272. }
  273. x, y, r, err := templatePoint.Position.evaluate(req.dimensions.Parameters(), t.functions(req))
  274. if err != nil {
  275. return nil, err
  276. }
  277. return point.NewRelativePoint(relativePoint).
  278. WithXOffset(x).WithYOffset(y).WithRotationOffset(r).
  279. MarkWith(id), nil
  280. }
  281. //nolint:ireturn,dupl
  282. func (t Template) createBetween(id util.ID, req request, depth int) (point.Point, error) {
  283. newPoint, err := t.templatePoint(req.panel, id)
  284. if err != nil {
  285. return nil, err
  286. }
  287. if newPoint.Between.To == id || newPoint.Between.From == id || depth > maxRecursionDepth {
  288. return nil, ErrRelativePointRecursion
  289. }
  290. fromPoint, err := t.getOrCreatePoint(newPoint.Between.From, req, depth)
  291. if err != nil {
  292. return nil, err
  293. }
  294. toPoint, err := t.getOrCreatePoint(newPoint.Between.To, req, depth)
  295. if err != nil {
  296. return nil, err
  297. }
  298. params := req.dimensions.Parameters()
  299. offset, err := newPoint.Between.Offset.Evaluate(params, t.functions(req))
  300. if err != nil {
  301. return nil, err
  302. }
  303. return point.NewBetweenPoint(fromPoint, toPoint, offset, id), nil
  304. }
  305. func (t Template) getOrCreatePoints(ids []util.ID, req request, depth int) ([]point.Point, error) {
  306. points := make([]point.Point, 0, len(ids))
  307. for _, id := range ids {
  308. createPoint, err := t.getOrCreatePoint(id, req, depth)
  309. if err != nil {
  310. return nil, err
  311. }
  312. points = append(points, createPoint)
  313. }
  314. return points, nil
  315. }
  316. //nolint:ireturn
  317. func (t Template) getOrCreatePoint(id util.ID, req request, depth int) (point.Point, error) {
  318. p, ok := req.points[id]
  319. if ok {
  320. return p, nil
  321. }
  322. newPoint, err := t.createPoint(id, req, depth+1)
  323. if err != nil {
  324. return nil, fmt.Errorf("creating point %q: %w", id, err)
  325. }
  326. req.points[id] = newPoint
  327. return newPoint, nil
  328. }
  329. //nolint:ireturn,dupl
  330. func (t Template) createExtend(id util.ID, req request, depth int) (point.Point, error) {
  331. newPoint, err := t.templatePoint(req.panel, id)
  332. if err != nil {
  333. return nil, err
  334. }
  335. if newPoint.Extend.To == id || newPoint.Extend.From == id || depth > maxRecursionDepth {
  336. return nil, ErrRelativePointRecursion
  337. }
  338. fromPoint, err := t.getOrCreatePoint(newPoint.Extend.From, req, depth)
  339. if err != nil {
  340. return nil, err
  341. }
  342. toPoint, err := t.getOrCreatePoint(newPoint.Extend.To, req, depth)
  343. if err != nil {
  344. return nil, err
  345. }
  346. offset, err := newPoint.Extend.Offset.Evaluate(req.dimensions.Parameters(), t.functions(req))
  347. if err != nil {
  348. return nil, err
  349. }
  350. return point.NewExtendPoint(fromPoint, toPoint, offset, id), nil
  351. }
  352. //nolint:ireturn
  353. func (t Template) createPolar(id util.ID, req request, depth int) (point.Point, error) {
  354. templatePoint, err := t.templatePoint(req.panel, id)
  355. if err != nil {
  356. return nil, err
  357. }
  358. relativePointID := *templatePoint.RelativeTo
  359. if relativePointID == id || depth > maxRecursionDepth {
  360. return nil, ErrRelativePointRecursion
  361. }
  362. relativePoint, err := t.getOrCreatePoint(relativePointID, req, depth)
  363. if err != nil {
  364. return nil, err
  365. }
  366. x, y, r, err := templatePoint.Polar.evaluate(req.dimensions.Parameters(), t.functions(req))
  367. if err != nil {
  368. return nil, err
  369. }
  370. return point.NewRelativePoint(relativePoint).
  371. WithXOffset(x).WithYOffset(y).MarkWith(id).WithRotationOffset(r), nil
  372. }
  373. // ExtendPoint describes how to draw a new point that extends in line with two points.
  374. type ExtendPoint struct {
  375. From util.ID `yaml:"from"`
  376. To util.ID `yaml:"to"`
  377. Offset *Value `yaml:"offset"`
  378. }
  379. // PolarPoint describes how to draw a new point with a direction and a distance from the current
  380. // position.
  381. type PolarPoint struct {
  382. Length *Value `yaml:"length"`
  383. Rotation *Value `yaml:"rotation"`
  384. }
  385. func (p PolarPoint) evaluate(
  386. params govaluate.MapParameters,
  387. funcs map[string]govaluate.ExpressionFunction,
  388. ) (x, y, r float64, err error) {
  389. rotation, err := p.Rotation.Evaluate(params, funcs)
  390. if err != nil {
  391. return 0, 0, 0, err
  392. }
  393. length, err := p.Length.Evaluate(params, funcs)
  394. if err != nil {
  395. return 0, 0, 0, err
  396. }
  397. return math.Cos(rotation) * length, math.Sin(rotation) * length, rotation, nil
  398. }