plotboilerplate 1.23.3 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/README.md +19 -1
  2. package/dist/index.esm.js +1684 -394
  3. package/dist/index.esm.js.map +1 -1
  4. package/dist/index.esm.min.js +1 -1
  5. package/dist/index.esm.min.js.map +1 -1
  6. package/dist/plotboilerplate.browser.min.js +1 -1
  7. package/dist/plotboilerplate.browser.min.js.map +1 -1
  8. package/dist/plotboilerplate.js +12450 -11120
  9. package/dist/plotboilerplate.min.js +1 -1
  10. package/package.json +9 -5
  11. package/src/cjs/BezierPath.d.ts +41 -5
  12. package/src/cjs/BezierPath.js +52 -2
  13. package/src/cjs/BezierPath.js.map +1 -1
  14. package/src/cjs/Bounds.d.ts +67 -3
  15. package/src/cjs/Bounds.js +101 -3
  16. package/src/cjs/Bounds.js.map +1 -1
  17. package/src/cjs/Circle.d.ts +46 -4
  18. package/src/cjs/Circle.js +82 -3
  19. package/src/cjs/Circle.js.map +1 -1
  20. package/src/cjs/CircleSector.d.ts +51 -2
  21. package/src/cjs/CircleSector.js +112 -3
  22. package/src/cjs/CircleSector.js.map +1 -1
  23. package/src/cjs/CubicBezierCurve.d.ts +113 -7
  24. package/src/cjs/CubicBezierCurve.js +281 -33
  25. package/src/cjs/CubicBezierCurve.js.map +1 -1
  26. package/src/cjs/Line.d.ts +38 -3
  27. package/src/cjs/Line.js +64 -1
  28. package/src/cjs/Line.js.map +1 -1
  29. package/src/cjs/PlotBoilerplate.d.ts +37 -6
  30. package/src/cjs/PlotBoilerplate.js +224 -125
  31. package/src/cjs/PlotBoilerplate.js.map +1 -1
  32. package/src/cjs/Polygon.d.ts +68 -5
  33. package/src/cjs/Polygon.js +143 -22
  34. package/src/cjs/Polygon.js.map +1 -1
  35. package/src/cjs/Triangle.d.ts +52 -3
  36. package/src/cjs/Triangle.js +80 -1
  37. package/src/cjs/Triangle.js.map +1 -1
  38. package/src/cjs/VEllipse.d.ts +62 -3
  39. package/src/cjs/VEllipse.js +181 -17
  40. package/src/cjs/VEllipse.js.map +1 -1
  41. package/src/cjs/VEllipseSector.d.ts +86 -2
  42. package/src/cjs/VEllipseSector.js +148 -4
  43. package/src/cjs/VEllipseSector.js.map +1 -1
  44. package/src/cjs/Vector.d.ts +12 -2
  45. package/src/cjs/Vector.js +15 -2
  46. package/src/cjs/Vector.js.map +1 -1
  47. package/src/cjs/VertTuple.d.ts +28 -4
  48. package/src/cjs/VertTuple.js +36 -3
  49. package/src/cjs/VertTuple.js.map +1 -1
  50. package/src/cjs/Vertex.d.ts +31 -4
  51. package/src/cjs/Vertex.js +35 -1
  52. package/src/cjs/Vertex.js.map +1 -1
  53. package/src/cjs/interfaces/core.d.ts +67 -0
  54. package/src/cjs/interfaces/core.js +4 -0
  55. package/src/cjs/interfaces/core.js.map +1 -1
  56. package/src/cjs/utils/algorithms/convexPolygonIncircle.d.ts +2 -1
  57. package/src/cjs/utils/algorithms/convexPolygonIncircle.js +14 -6
  58. package/src/cjs/utils/algorithms/convexPolygonIncircle.js.map +1 -1
  59. package/src/cjs/utils/createRandomizedPolygon.d.ts +14 -0
  60. package/src/cjs/utils/createRandomizedPolygon.js +32 -0
  61. package/src/cjs/utils/createRandomizedPolygon.js.map +1 -0
  62. package/src/cjs/utils/datastructures/Color.d.ts +147 -1
  63. package/src/cjs/utils/datastructures/Color.js +385 -41
  64. package/src/cjs/utils/datastructures/Color.js.map +1 -1
  65. package/src/cjs/utils/datastructures/ColorGradient.d.ts +79 -0
  66. package/src/cjs/utils/datastructures/ColorGradient.js +171 -0
  67. package/src/cjs/utils/datastructures/ColorGradient.js.map +1 -0
  68. package/src/cjs/utils/dom/components/ColorGradientPicker.d.ts +245 -0
  69. package/src/cjs/utils/dom/components/ColorGradientPicker.js +667 -0
  70. package/src/cjs/utils/dom/components/ColorGradientPicker.js.map +1 -0
  71. package/src/cjs/utils/dom/components/ColorGradientPickerJSX.d.ts +129 -0
  72. package/src/cjs/utils/dom/components/ColorGradientPickerJSX.js +520 -0
  73. package/src/cjs/utils/dom/components/ColorGradientPickerJSX.js.map +1 -0
  74. package/src/cjs/utils/dom/components/ColorGradientSelector.d.ts +117 -0
  75. package/src/cjs/utils/dom/components/ColorGradientSelector.js +325 -0
  76. package/src/cjs/utils/dom/components/ColorGradientSelector.js.map +1 -0
  77. package/src/cjs/utils/dom/components/Test.d.ts +7 -0
  78. package/src/cjs/utils/dom/components/Test.js +30 -0
  79. package/src/cjs/utils/dom/components/Test.js.map +1 -0
  80. package/src/cjs/utils/dom/guiSizeToggler.js +1 -1
  81. package/src/cjs/utils/dom/guiSizeToggler.js.map +1 -1
  82. package/src/cjs/utils/helpers/BezierPathInteractionHelper.d.ts +16 -8
  83. package/src/cjs/utils/helpers/BezierPathInteractionHelper.js +49 -28
  84. package/src/cjs/utils/helpers/BezierPathInteractionHelper.js.map +1 -1
  85. package/src/cjs/utils/helpers/CircleHelper.d.ts +10 -1
  86. package/src/cjs/utils/helpers/CircleHelper.js +13 -2
  87. package/src/cjs/utils/helpers/CircleHelper.js.map +1 -1
  88. package/src/cjs/utils/helpers/CircleSectorHelper.d.ts +11 -2
  89. package/src/cjs/utils/helpers/CircleSectorHelper.js +20 -20
  90. package/src/cjs/utils/helpers/CircleSectorHelper.js.map +1 -1
  91. package/src/cjs/utils/helpers/TriangleHelper.d.ts +57 -0
  92. package/src/cjs/utils/helpers/TriangleHelper.js +83 -0
  93. package/src/cjs/utils/helpers/TriangleHelper.js.map +1 -0
  94. package/src/cjs/utils/helpers/VEllipseHelper.d.ts +32 -0
  95. package/src/cjs/utils/helpers/VEllipseHelper.js +88 -0
  96. package/src/cjs/utils/helpers/VEllipseHelper.js.map +1 -0
  97. package/src/cjs/utils/helpers/VEllipseSectorHelper.d.ts +44 -4
  98. package/src/cjs/utils/helpers/VEllipseSectorHelper.js +108 -25
  99. package/src/cjs/utils/helpers/VEllipseSectorHelper.js.map +1 -1
  100. package/src/cjs/utils/parsers/LinearColorGradientParser.d.ts +78 -0
  101. package/src/cjs/utils/parsers/LinearColorGradientParser.js +224 -0
  102. package/src/cjs/utils/parsers/LinearColorGradientParser.js.map +1 -0
  103. package/src/createDemoDrawables.js +4 -1
  104. package/src/esm/BezierPath.d.ts +41 -5
  105. package/src/esm/BezierPath.js +50 -2
  106. package/src/esm/BezierPath.js.map +1 -1
  107. package/src/esm/Bounds.d.ts +67 -3
  108. package/src/esm/Bounds.js +101 -3
  109. package/src/esm/Bounds.js.map +1 -1
  110. package/src/esm/Circle.d.ts +46 -4
  111. package/src/esm/Circle.js +79 -3
  112. package/src/esm/Circle.js.map +1 -1
  113. package/src/esm/CircleSector.d.ts +51 -2
  114. package/src/esm/CircleSector.js +108 -3
  115. package/src/esm/CircleSector.js.map +1 -1
  116. package/src/esm/CubicBezierCurve.d.ts +113 -7
  117. package/src/esm/CubicBezierCurve.js +277 -33
  118. package/src/esm/CubicBezierCurve.js.map +1 -1
  119. package/src/esm/Line.d.ts +38 -3
  120. package/src/esm/Line.js +62 -1
  121. package/src/esm/Line.js.map +1 -1
  122. package/src/esm/PlotBoilerplate.d.ts +37 -6
  123. package/src/esm/PlotBoilerplate.js +224 -125
  124. package/src/esm/PlotBoilerplate.js.map +1 -1
  125. package/src/esm/Polygon.d.ts +68 -5
  126. package/src/esm/Polygon.js +138 -22
  127. package/src/esm/Polygon.js.map +1 -1
  128. package/src/esm/Triangle.d.ts +52 -3
  129. package/src/esm/Triangle.js +77 -1
  130. package/src/esm/Triangle.js.map +1 -1
  131. package/src/esm/VEllipse.d.ts +62 -3
  132. package/src/esm/VEllipse.js +178 -17
  133. package/src/esm/VEllipse.js.map +1 -1
  134. package/src/esm/VEllipseSector.d.ts +86 -2
  135. package/src/esm/VEllipseSector.js +146 -7
  136. package/src/esm/VEllipseSector.js.map +1 -1
  137. package/src/esm/Vector.d.ts +12 -2
  138. package/src/esm/Vector.js +15 -2
  139. package/src/esm/Vector.js.map +1 -1
  140. package/src/esm/VertTuple.d.ts +28 -4
  141. package/src/esm/VertTuple.js +36 -3
  142. package/src/esm/VertTuple.js.map +1 -1
  143. package/src/esm/Vertex.d.ts +31 -4
  144. package/src/esm/Vertex.js +35 -1
  145. package/src/esm/Vertex.js.map +1 -1
  146. package/src/esm/interfaces/core.d.ts +67 -0
  147. package/src/esm/interfaces/core.js +4 -0
  148. package/src/esm/interfaces/core.js.map +1 -1
  149. package/src/esm/utils/algorithms/convexPolygonIncircle.d.ts +2 -1
  150. package/src/esm/utils/algorithms/convexPolygonIncircle.js +15 -7
  151. package/src/esm/utils/algorithms/convexPolygonIncircle.js.map +1 -1
  152. package/src/esm/utils/algorithms/createRandomizedPolygon.d.ts +14 -0
  153. package/src/esm/utils/algorithms/createRandomizedPolygon.js +28 -0
  154. package/src/esm/utils/algorithms/createRandomizedPolygon.js.map +1 -0
  155. package/src/esm/utils/createRandomizedPolygon.d.ts +14 -0
  156. package/src/esm/utils/createRandomizedPolygon.js +28 -0
  157. package/src/esm/utils/createRandomizedPolygon.js.map +1 -0
  158. package/src/esm/utils/datastructures/Color.d.ts +147 -1
  159. package/src/esm/utils/datastructures/Color.js +386 -42
  160. package/src/esm/utils/datastructures/Color.js.map +1 -1
  161. package/src/esm/utils/datastructures/ColorGradient.d.ts +79 -0
  162. package/src/esm/utils/datastructures/ColorGradient.js +166 -0
  163. package/src/esm/utils/datastructures/ColorGradient.js.map +1 -0
  164. package/src/esm/utils/dom/components/ColorGradientPicker.d.ts +245 -0
  165. package/src/esm/utils/dom/components/ColorGradientPicker.js +732 -0
  166. package/src/esm/utils/dom/components/ColorGradientPicker.js.map +1 -0
  167. package/src/esm/utils/dom/components/ColorGradientPickerJSX.d.ts +129 -0
  168. package/src/esm/utils/dom/components/ColorGradientPickerJSX.js +582 -0
  169. package/src/esm/utils/dom/components/ColorGradientPickerJSX.js.map +1 -0
  170. package/src/esm/utils/dom/components/ColorGradientSelector.d.ts +117 -0
  171. package/src/esm/utils/dom/components/ColorGradientSelector.js +332 -0
  172. package/src/esm/utils/dom/components/ColorGradientSelector.js.map +1 -0
  173. package/src/esm/utils/dom/components/Test.d.ts +7 -0
  174. package/src/esm/utils/dom/components/Test.js +26 -0
  175. package/src/esm/utils/dom/components/Test.js.map +1 -0
  176. package/src/esm/utils/dom/guiSizeToggler.js +1 -1
  177. package/src/esm/utils/dom/guiSizeToggler.js.map +1 -1
  178. package/src/esm/utils/helpers/BezierPathInteractionHelper.d.ts +16 -8
  179. package/src/esm/utils/helpers/BezierPathInteractionHelper.js +47 -27
  180. package/src/esm/utils/helpers/BezierPathInteractionHelper.js.map +1 -1
  181. package/src/esm/utils/helpers/CircleHelper.d.ts +10 -1
  182. package/src/esm/utils/helpers/CircleHelper.js +13 -2
  183. package/src/esm/utils/helpers/CircleHelper.js.map +1 -1
  184. package/src/esm/utils/helpers/CircleSectorHelper.d.ts +11 -2
  185. package/src/esm/utils/helpers/CircleSectorHelper.js +20 -20
  186. package/src/esm/utils/helpers/CircleSectorHelper.js.map +1 -1
  187. package/src/esm/utils/helpers/TriangleHelper.d.ts +57 -0
  188. package/src/esm/utils/helpers/TriangleHelper.js +78 -0
  189. package/src/esm/utils/helpers/TriangleHelper.js.map +1 -0
  190. package/src/esm/utils/helpers/VEllipseHelper.d.ts +32 -0
  191. package/src/esm/utils/helpers/VEllipseHelper.js +83 -0
  192. package/src/esm/utils/helpers/VEllipseHelper.js.map +1 -0
  193. package/src/esm/utils/helpers/VEllipseSectorHelper.d.ts +44 -4
  194. package/src/esm/utils/helpers/VEllipseSectorHelper.js +107 -24
  195. package/src/esm/utils/helpers/VEllipseSectorHelper.js.map +1 -1
  196. package/src/esm/utils/parsers/LinearColorGradientParser.d.ts +78 -0
  197. package/src/esm/utils/parsers/LinearColorGradientParser.js +217 -0
  198. package/src/esm/utils/parsers/LinearColorGradientParser.js.map +1 -0
  199. package/src/external.d.ts +9 -0
  200. package/src/ts/BezierPath.ts +58 -5
  201. package/src/ts/Bounds.ts +110 -6
  202. package/src/ts/Circle.ts +87 -5
  203. package/src/ts/CircleSector.ts +115 -5
  204. package/src/ts/CubicBezierCurve.ts +332 -42
  205. package/src/ts/Line.ts +66 -3
  206. package/src/ts/PlotBoilerplate.ts +236 -114
  207. package/src/ts/Polygon.ts +164 -29
  208. package/src/ts/Triangle.ts +85 -3
  209. package/src/ts/VEllipse.ts +214 -19
  210. package/src/ts/VEllipseSector.ts +154 -8
  211. package/src/ts/Vector.ts +16 -2
  212. package/src/ts/VertTuple.ts +39 -4
  213. package/src/ts/Vertex.ts +40 -4
  214. package/src/ts/interfaces/core.ts +73 -0
  215. package/src/ts/utils/algorithms/convexPolygonIncircle.ts +82 -78
  216. package/src/ts/utils/createRandomizedPolygon.ts +31 -0
  217. package/src/ts/utils/datastructures/Color.ts +423 -44
  218. package/src/ts/utils/datastructures/ColorGradient.ts +196 -0
  219. package/src/ts/utils/dom/components/ColorGradientPicker.tsx +877 -0
  220. package/src/ts/utils/dom/components/ColorGradientSelector.tsx +445 -0
  221. package/src/ts/utils/dom/components/Test.tsx +44 -0
  222. package/src/ts/utils/dom/components/_ColorGradientPicker_raw.ts_ +392 -0
  223. package/src/ts/utils/dom/guiSizeToggler.ts +1 -1
  224. package/src/ts/utils/helpers/BezierPathInteractionHelper.ts +61 -35
  225. package/src/ts/utils/helpers/CircleHelper.ts +17 -3
  226. package/src/ts/utils/helpers/CircleSectorHelper.ts +118 -126
  227. package/src/ts/utils/helpers/TriangleHelper.ts +102 -0
  228. package/src/ts/utils/helpers/VEllipseHelper.ts +115 -0
  229. package/src/ts/utils/helpers/VEllipseSectorHelper.ts +144 -26
  230. package/src/ts/utils/parsers/LinearColorGradientParser.ts +297 -0
package/dist/index.esm.js CHANGED
@@ -328,168 +328,6 @@ class VertexListeners {
328
328
  }
329
329
  }
330
330
 
331
- /**
332
- * @author Ikaros Kappler
333
- * @date 2019-01-30
334
- * @modified 2019-02-23 Added the toSVGString function, overriding Line.toSVGString.
335
- * @modified 2019-03-20 Added JSDoc tags.
336
- * @modified 2019-04-19 Added the clone function (overriding Line.clone()).
337
- * @modified 2019-09-02 Added the Vector.perp() function.
338
- * @modified 2019-09-02 Added the Vector.inverse() function.
339
- * @modified 2019-12-04 Added the Vector.inv() function.
340
- * @modified 2020-03-23 Ported to Typescript from JS.
341
- * @modified 2021-01-20 Added UID.
342
- * @modified 2022-02-02 Added the `destroy` method.
343
- * @modified 2022-02-02 Cleared the `Vector.toSVGString` function (deprecated). Use `drawutilssvg` instead.
344
- * @modified 2022-10-25 Added the `getOrthogonal` method.
345
- * @version 1.5.0
346
- *
347
- * @file Vector
348
- * @public
349
- **/
350
- /**
351
- * @classdesc A vector (Vertex,Vertex) is a line with a visible direction.<br>
352
- * <br>
353
- * Vectors are drawn with an arrow at their end point.<br>
354
- * <b>The Vector class extends the Line class.</b>
355
- *
356
- * @requires VertTuple
357
- * @requires Vertex
358
- **/
359
- class Vector extends VertTuple {
360
- /**
361
- * The constructor.
362
- *
363
- * @constructor
364
- * @name Vector
365
- * @extends Line
366
- * @param {Vertex} vertA - The start vertex of the vector.
367
- * @param {Vertex} vertB - The end vertex of the vector.
368
- **/
369
- constructor(vertA, vertB) {
370
- super(vertA, vertB, (a, b) => new Vector(a, b));
371
- /**
372
- * Required to generate proper CSS classes and other class related IDs.
373
- **/
374
- this.className = "Vector";
375
- }
376
- /**
377
- * Get the perpendicular of this vector which is located at a.
378
- *
379
- * @param {Number} t The position on the vector.
380
- * @return {Vector} A new vector being the perpendicular of this vector sitting on a.
381
- **/
382
- perp() {
383
- var v = this.clone();
384
- v.sub(this.a);
385
- v = new Vector(new Vertex(), new Vertex(-v.b.y, v.b.x));
386
- v.a.add(this.a);
387
- v.b.add(this.a);
388
- return v;
389
- }
390
- /**
391
- * The inverse of a vector is a vector with the same magnitude but oppose direction.
392
- *
393
- * Please not that the origin of this vector changes here: a->b becomes b->a.
394
- *
395
- * @return {Vector}
396
- **/
397
- inverse() {
398
- var tmp = this.a;
399
- this.a = this.b;
400
- this.b = tmp;
401
- return this;
402
- }
403
- /**
404
- * This function computes the inverse of the vector, which means 'a' stays untouched.
405
- *
406
- * @return {Vector} this for chaining.
407
- **/
408
- inv() {
409
- this.b.x = this.a.x - (this.b.x - this.a.x);
410
- this.b.y = this.a.y - (this.b.y - this.a.y);
411
- return this;
412
- }
413
- /**
414
- * Get the intersection if this vector and the specified vector.
415
- *
416
- * @method intersection
417
- * @param {Vector} line The second vector.
418
- * @return {Vertex} The intersection (may lie outside the end-points).
419
- * @instance
420
- * @memberof Line
421
- **/
422
- intersection(line) {
423
- var denominator = this.denominator(line);
424
- if (denominator == 0)
425
- return null;
426
- var a = this.a.y - line.a.y;
427
- var b = this.a.x - line.a.x;
428
- var numerator1 = (line.b.x - line.a.x) * a - (line.b.y - line.a.y) * b;
429
- var numerator2 = (this.b.x - this.a.x) * a - (this.b.y - this.a.y) * b;
430
- a = numerator1 / denominator; // NaN if parallel lines
431
- b = numerator2 / denominator;
432
- // TODO:
433
- // FOR A VECTOR THE LINE-INTERSECTION MUST BE ON BOTH VECTORS
434
- // if we cast these lines infinitely in both directions, they intersect here:
435
- return new Vertex(this.a.x + a * (this.b.x - this.a.x), this.a.y + a * (this.b.y - this.a.y));
436
- }
437
- /**
438
- * Get the orthogonal "vector" of this vector (rotated by 90° clockwise).
439
- *
440
- * @name getOrthogonal
441
- * @method getOrthogonal
442
- * @return {Vector} A new vector with the same length that stands on this vector's point a.
443
- * @instance
444
- * @memberof Vector
445
- **/
446
- getOrthogonal() {
447
- // Orthogonal of vector (0,0)->(x,y) is (0,0)->(-y,x)
448
- const linePoint = this.a.clone();
449
- const startPoint = this.b.clone().sub(this.a);
450
- const tmp = startPoint.x;
451
- startPoint.x = -startPoint.y;
452
- startPoint.y = tmp;
453
- return new Vector(linePoint, startPoint.add(this.a));
454
- }
455
- }
456
- Vector.utils = {
457
- /**
458
- * Generate a four-point arrow head, starting at the vector end minus the
459
- * arrow head length.
460
- *
461
- * The first vertex in the returned array is guaranteed to be the located
462
- * at the vector line end minus the arrow head length.
463
- *
464
- *
465
- * Due to performance all params are required.
466
- *
467
- * The params scaleX and scaleY are required for the case that the scaling is not uniform (x and y
468
- * scaling different). Arrow heads should not look distored on non-uniform scaling.
469
- *
470
- * If unsure use 1.0 for scaleX and scaleY (=no distortion).
471
- * For headlen use 8, it's a good arrow head size.
472
- *
473
- * Example:
474
- * buildArrowHead( new Vertex(0,0), new Vertex(50,100), 8, 1.0, 1.0 )
475
- *
476
- * @param {XYCoords} zA - The start vertex of the vector to calculate the arrow head for.
477
- * @param {XYCoords} zB - The end vertex of the vector.
478
- * @param {number} headlen - The length of the arrow head (along the vector direction. A good value is 12).
479
- * @param {number} scaleX - The horizontal scaling during draw.
480
- * @param {number} scaleY - the vertical scaling during draw.
481
- **/
482
- buildArrowHead: (zA, zB, headlen, scaleX, scaleY) => {
483
- const angle = Math.atan2((zB.y - zA.y) * scaleY, (zB.x - zA.x) * scaleX);
484
- const vertices = [];
485
- vertices.push(new Vertex(zB.x * scaleX - headlen * Math.cos(angle), zB.y * scaleY - headlen * Math.sin(angle)));
486
- vertices.push(new Vertex(zB.x * scaleX - headlen * 1.35 * Math.cos(angle - Math.PI / 8), zB.y * scaleY - headlen * 1.35 * Math.sin(angle - Math.PI / 8)));
487
- vertices.push(new Vertex(zB.x * scaleX, zB.y * scaleY));
488
- vertices.push(new Vertex(zB.x * scaleX - headlen * 1.35 * Math.cos(angle + Math.PI / 8), zB.y * scaleY - headlen * 1.35 * Math.sin(angle + Math.PI / 8)));
489
- return vertices;
490
- }
491
- };
492
-
493
331
  /**
494
332
  * @author Ikaros Kappler
495
333
  * @date 2020-05-04
@@ -504,7 +342,9 @@ Vector.utils = {
504
342
  * @modified 2022-08-15 Added the `containsPoint` function.
505
343
  * @modified 2022-08-23 Added the `lineIntersection` function.
506
344
  * @modified 2022-08-23 Added the `closestPoint` function.
507
- * @version 1.4.0
345
+ * @modified 2025-04-09 Added the `Circle.move(amount: XYCoords)` method.
346
+ * @modified 2025-04-16 Class `Circle` now implements interface `Intersectable`.
347
+ * @version 1.5.0
508
348
  **/
509
349
  /**
510
350
  * @classdesc A simple circle: center point and radius.
@@ -535,6 +375,19 @@ class Circle {
535
375
  this.center = center;
536
376
  this.radius = radius;
537
377
  }
378
+ /**
379
+ * Move the circle by the given amount.
380
+ *
381
+ * @method move
382
+ * @param {XYCoords} amount - The amount to move.
383
+ * @instance
384
+ * @memberof Circle
385
+ * @return {Circle} this for chaining
386
+ **/
387
+ move(amount) {
388
+ this.center.add(amount);
389
+ return this;
390
+ }
538
391
  /**
539
392
  * Check if the given circle is fully contained inside this circle.
540
393
  *
@@ -605,7 +458,21 @@ class Circle {
605
458
  const pointA = Circle.circleUtils.vertAt(angle, this.radius);
606
459
  // Construct the perpendicular of the line in point a. Then move relative to center.
607
460
  return new Vector(pointA, new Vertex(0, 0)).add(this.center).perp();
461
+ // return (new Vector(this.center.clone(), pointA).add(pointA) as Vector).perp() as Vector;
608
462
  }
463
+ //--- BEGIN --- Implement interface `Intersectable`
464
+ /**
465
+ * Get the bounding box (bounds) of this Circle.
466
+ *
467
+ * @method getBounds
468
+ * @instance
469
+ * @memberof Circle
470
+ * @return {Bounds} The rectangular bounds of this Circle.
471
+ **/
472
+ getBounds() {
473
+ return new Bounds(this.center.clone().subXY(Math.abs(this.radius), Math.abs(this.radius)), this.center.clone().addXY(Math.abs(this.radius), Math.abs(this.radius)));
474
+ }
475
+ //--- END --- Implement interface `Intersectable`
609
476
  /**
610
477
  * Calculate the intersection points (if exists) with the given circle.
611
478
  *
@@ -665,7 +532,7 @@ class Circle {
665
532
  * @instance
666
533
  * @memberof Circle
667
534
  * @param {Vertex} a- The first of the two points defining the line.
668
- * @param {Vertex} b - The second of the two points defining the line.
535
+ * @param {XYCoords} b - The second of the two points defining the line.
669
536
  * @return {Line|null} The intersection points (as a line) or null if this circle does not intersect the line given.
670
537
  **/
671
538
  lineIntersection(a, b) {
@@ -698,8 +565,54 @@ class Circle {
698
565
  interA.y = (-det * diff.x + Math.abs(diff.y) * sqrt) / distSquared;
699
566
  interB.y = (-det * diff.x - Math.abs(diff.y) * sqrt) / distSquared;
700
567
  return new Line(interA.add(this.center), interB.add(this.center));
701
- // return new Line(interA, interB);
702
568
  }
569
+ //--- BEGIN --- Implement interface `Intersectable`
570
+ /**
571
+ * Get all line intersections with this circle.
572
+ *
573
+ * This method returns all intersections (as vertices) with this shape. The returned array of vertices is in no specific order.
574
+ *
575
+ * @param {VertTuple} line - The line to find intersections with.
576
+ * @param {boolean} inVectorBoundsOnly - If set to true only intersecion points on the passed vector are returned (located strictly between start and end vertex).
577
+ * @returns {Array<Vertex>} - An array of all intersections with the circle outline.
578
+ */
579
+ lineIntersections(line, inVectorBoundsOnly = false) {
580
+ // Find the intersections of all lines inside the edge bounds
581
+ const intersectioLine = this.lineIntersection(line.a, line.b);
582
+ if (!intersectioLine) {
583
+ return [];
584
+ }
585
+ if (inVectorBoundsOnly) {
586
+ // const maxDist = line.length();
587
+ return [intersectioLine.a, intersectioLine.b].filter((vert) => line.hasPoint(vert, true));
588
+ }
589
+ else {
590
+ return [intersectioLine.a, intersectioLine.b];
591
+ }
592
+ }
593
+ /**
594
+ * Get all line intersections of this polygon and their tangents along the shape.
595
+ *
596
+ * This method returns all intersection tangents (as vectors) with this shape. The returned array of vectors is in no specific order.
597
+ *
598
+ * @param line
599
+ * @param lineIntersectionTangents
600
+ * @returns
601
+ */
602
+ lineIntersectionTangents(line, inVectorBoundsOnly = false) {
603
+ // Find the intersections of all lines plus their tangents inside the circle bounds
604
+ const interSectionPoints = this.lineIntersections(line, inVectorBoundsOnly);
605
+ return interSectionPoints.map((vert) => {
606
+ // Calculate angle
607
+ const lineFromCenter = new Line(this.center, vert);
608
+ const angle = lineFromCenter.angle();
609
+ // console.log("angle", (angle / Math.PI) * 180.0);
610
+ // const angle = Math.random() * Math.PI * 2; // TODO
611
+ // Calculate tangent at angle
612
+ return this.tangentAt(angle);
613
+ });
614
+ }
615
+ //--- END --- Implement interface `Intersectable`
703
616
  /**
704
617
  * Calculate the closest point on the outline of this circle to the given point.
705
618
  *
@@ -766,7 +679,11 @@ Circle.circleUtils = {
766
679
  * @modified 2022-02-02 Cleared the `Triangle.toSVGString` function (deprecated). Use `drawutilssvg` instead.
767
680
  * @modified 2024-11-22 Added static utility function Triangle.utils.determinant; adapted method `determinant`.
768
681
  * @modified 2024-11-22 Changing visibility of `Triangle.utils` from `private` to `public`.
769
- * @version 2.8.0
682
+ * @modified 2025-14-16 Class `Triangle` now implements interface `Intersectable`.
683
+ * @modified 2025-14-16 Class `Triangle` now implements interface `IBounded`.
684
+ * @modified 2025-14-16 Class `Triangle` now implements interface `Intersectable`.
685
+ * @modified 2025-14-16 Added method `Triangle.move`.
686
+ * @version 2.10.0
770
687
  *
771
688
  * @file Triangle
772
689
  * @fileoverview A simple triangle class: three vertices.
@@ -871,6 +788,35 @@ class Triangle {
871
788
  this.c.scale(factor, centroid);
872
789
  return this;
873
790
  }
791
+ //--- BEGIN --- Implement interface `IBounded`
792
+ /**
793
+ * Get the bounding box (bounds) of this Triangle.
794
+ *
795
+ * @method getBounds
796
+ * @instance
797
+ * @memberof Triangle
798
+ * @return {Bounds} The rectangular bounds of this Triangle.
799
+ **/
800
+ getBounds() {
801
+ // return Bounds.computeFromVertices([this.a, this.b, this.c]);
802
+ return this.bounds();
803
+ }
804
+ //--- END --- Implement interface `IBounded`
805
+ /**
806
+ * Move the Triangle's vertices by the given amount.
807
+ *
808
+ * @method move
809
+ * @param {XYCoords} amount - The amount to move.
810
+ * @instance
811
+ * @memberof Triangle
812
+ * @return {Triangle} this for chaining
813
+ **/
814
+ move(amount) {
815
+ this.a.add(amount);
816
+ this.b.add(amount);
817
+ this.c.add(amount);
818
+ return this;
819
+ }
874
820
  /**
875
821
  * Get the circumcircle of this triangle.
876
822
  *
@@ -996,6 +942,48 @@ class Triangle {
996
942
  bounds() {
997
943
  return new Bounds(new Vertex(Triangle.utils.min3(this.a.x, this.b.x, this.c.x), Triangle.utils.min3(this.a.y, this.b.y, this.c.y)), new Vertex(Triangle.utils.max3(this.a.x, this.b.x, this.c.x), Triangle.utils.max3(this.a.y, this.b.y, this.c.y)));
998
944
  }
945
+ //--- BEGIN --- Implement interface `Intersectable`
946
+ /**
947
+ * Get all line intersections with this polygon.
948
+ *
949
+ * This method returns all intersections (as vertices) with this shape. The returned array of vertices is in no specific order.
950
+ *
951
+ * See demo `47-closest-vector-projection-on-polygon` for how it works.
952
+ *
953
+ * @param {VertTuple} line - The line to find intersections with.
954
+ * @param {boolean} inVectorBoundsOnly - If set to true only intersecion points on the passed vector are returned (located strictly between start and end vertex).
955
+ * @returns {Array<Vertex>} - An array of all intersections within the polygon bounds.
956
+ */
957
+ lineIntersections(line, inVectorBoundsOnly = false) {
958
+ // Find the intersections of all lines inside the edge bounds
959
+ return Polygon.utils
960
+ .locateLineIntersecion(line, [this.a, this.b, this.c], false, inVectorBoundsOnly)
961
+ .map(intersectionTuple => intersectionTuple.intersection);
962
+ }
963
+ /**
964
+ * Get all line intersections of this polygon and their tangents along the shape.
965
+ *
966
+ * This method returns all intersection tangents (as vectors) with this shape. The returned array of vectors is in no specific order.
967
+ *
968
+ * @param line
969
+ * @param inVectorBoundsOnly
970
+ * @returns
971
+ */
972
+ lineIntersectionTangents(line, inVectorBoundsOnly = false) {
973
+ // Find the intersection tangents of all lines inside the edge bounds
974
+ return Polygon.utils
975
+ .locateLineIntersecion(line, [this.a, this.b, this.c], false, inVectorBoundsOnly)
976
+ .map(intersectionTuple => {
977
+ // const polyLine = this.getEdgeAt(intersectionTuple.edgeIndex);
978
+ const polyLine = this.getEdgeAt(intersectionTuple.edgeIndex);
979
+ return new Vector(polyLine.a.clone(), polyLine.b.clone()).moveTo(intersectionTuple.intersection);
980
+ });
981
+ }
982
+ //--- END --- Implement interface `Intersectable`
983
+ getEdgeAt(edgeIndex) {
984
+ var modIndex = edgeIndex % 3;
985
+ return modIndex === 0 ? new Line(this.a, this.b) : modIndex === 1 ? new Line(this.b, this.c) : new Line(this.c, this.a);
986
+ }
999
987
  /**
1000
988
  * Convert this triangle to a polygon instance.
1001
989
  *
@@ -1303,7 +1291,10 @@ const geomutils = {
1303
1291
  * @modified 2023-09-29 Added the `Vertex.abs()` method as it seems useful.
1304
1292
  * @modified 2024-03-08 Added the optional `precision` param to the `toString` method.
1305
1293
  * @modified 2024-12-17 Outsourced the euclidean distance calculation of `Vertex.distance` to `geomutils.dist4`.
1306
- * @version 2.9.1
1294
+ * @modified 2025-03-24 Making the second parameter `center` of the `Vertex.rotate` method optional.
1295
+ * @modified 2025-04-13 Adding the `Vertex.move(amount: XYCoords)` method (does the same as `add`, added by naming convention).
1296
+ * @modified 2025-05-07 Class `Vertex` is now implementing interface `IBounded` (to meet convention).
1297
+ * @version 2.11.0
1307
1298
  *
1308
1299
  * @file Vertex
1309
1300
  * @public
@@ -1494,6 +1485,21 @@ class Vertex {
1494
1485
  }
1495
1486
  return this;
1496
1487
  }
1488
+ /**
1489
+ * Move this point by the given amount.
1490
+ *
1491
+ * This method just calls `add(amount).
1492
+ *
1493
+ *
1494
+ * @method move
1495
+ * @param {Vertex} amount - The amount to move this vertex.
1496
+ * @return {Vertex} this - For chaining.
1497
+ * @instance
1498
+ * @memberof Vertex
1499
+ */
1500
+ move(amount) {
1501
+ return this.add(amount);
1502
+ }
1497
1503
  /**
1498
1504
  * Add the passed amounts to the x- and y- components of this vertex.
1499
1505
  *
@@ -1833,6 +1839,21 @@ class Vertex {
1833
1839
  this.y = Math.abs(this.y);
1834
1840
  return this;
1835
1841
  }
1842
+ //--- BEGIN --- Implement interface `IBounded`
1843
+ /**
1844
+ * Get the bounding box (bounds) of this Vertex.
1845
+ * This is just by convention – any vertex is just a position without any useful with or height (0).
1846
+ *
1847
+ * @method getBounds
1848
+ * @instance
1849
+ * @memberof Vertex
1850
+ * @return {Bounds} The rectangular bounds of this Vertex (width and height are zero).
1851
+ **/
1852
+ getBounds() {
1853
+ // return Bounds.computeFromVertices([this.a, this.b, this.c]);
1854
+ return Bounds.computeFromVertices([this]);
1855
+ }
1856
+ //--- END --- Implement interface `IBounded`
1836
1857
  /**
1837
1858
  * Get a string representation of this vertex.
1838
1859
  *
@@ -1943,7 +1964,10 @@ Vertex.utils = {
1943
1964
  * @modified 2024-09-10 Chaging the first param of `pointDistance` from `Vertex` to less strict type `XYCoords`. This should not break anything.
1944
1965
  * @modified 2024-09-10 Adding the optional `epsilon` param to the `hasPoint` method.
1945
1966
  * @modified 2024-12-02 Added the `epsilon` param to the `colinear` method. Default is 1.0e-6.
1946
- * @version 1.3.0
1967
+ * @modified 2025-03-31 Added the `VertTuple.revert` method.
1968
+ * @modified 2025-04-15 Changed param of `VertTuple.moveTo` method from `Vertex` to `XYCoords`.
1969
+ * @modified 2025-04-15 Added method `VertTuple.move` method.
1970
+ * @version 1.4.0
1947
1971
  */
1948
1972
  /**
1949
1973
  * @classdesc An abstract base classes for vertex tuple constructs, like Lines or Vectors.
@@ -2008,15 +2032,31 @@ class VertTuple {
2008
2032
  *
2009
2033
  * @method add
2010
2034
  * @param {XYCoords} amount The amount (x,y) to add.
2011
- * @return {Line} this
2012
2035
  * @instance
2013
2036
  * @memberof VertTuple
2037
+ * @return {VertTuple<T>} this
2014
2038
  **/
2015
2039
  add(amount) {
2016
2040
  this.a.add(amount);
2017
2041
  this.b.add(amount);
2018
2042
  return this;
2019
2043
  }
2044
+ /**
2045
+ * Reverse this vertex tuple: a becomes b, and b becomes a.
2046
+ * This operation is in-place.
2047
+ *
2048
+ * @method add
2049
+ * @param {XYCoords} amount The amount (x,y) to add.
2050
+ * @instance
2051
+ * @memberof VertTuple
2052
+ * @return {VertTuple<T>} this
2053
+ */
2054
+ revert() {
2055
+ const tmp = this.a;
2056
+ this.a = this.b;
2057
+ this.b = tmp;
2058
+ return this;
2059
+ }
2020
2060
  /**
2021
2061
  * Normalize this line (set to length 1).
2022
2062
  *
@@ -2046,7 +2086,7 @@ class VertTuple {
2046
2086
  * Move this line to a new location.
2047
2087
  *
2048
2088
  * @method moveTo
2049
- * @param {Vertex} newA - The new desired location of 'a'. Vertex 'b' will be moved, too.
2089
+ * @param {XYCoords} newA - The new desired location of 'a'. Vertex 'b' will be moved, too.
2050
2090
  * @return {VertTuple} this
2051
2091
  * @instance
2052
2092
  * @memberof VertTuple
@@ -2057,6 +2097,20 @@ class VertTuple {
2057
2097
  this.b.add(diff);
2058
2098
  return this;
2059
2099
  }
2100
+ /**
2101
+ * Move this line by the given amount
2102
+ *
2103
+ * @method move
2104
+ * @param {XYCoords} amount - The amount to move both point of this tuple.
2105
+ * @return {VertTuple} this
2106
+ * @instance
2107
+ * @memberof VertTuple
2108
+ **/
2109
+ move(amount) {
2110
+ this.a.add(amount);
2111
+ this.b.add(amount);
2112
+ return this;
2113
+ }
2060
2114
  /**
2061
2115
  * Get the angle between this and the passed line (in radians).
2062
2116
  *
@@ -2221,18 +2275,193 @@ class VertTuple {
2221
2275
  * After calling this function the object might not hold valid data any more and
2222
2276
  * should not be used.
2223
2277
  */
2224
- destroy() {
2225
- this.a.destroy();
2226
- this.b.destroy();
2227
- this.isDestroyed = true;
2278
+ destroy() {
2279
+ this.a.destroy();
2280
+ this.b.destroy();
2281
+ this.isDestroyed = true;
2282
+ }
2283
+ }
2284
+ /**
2285
+ * @private
2286
+ **/
2287
+ VertTuple.vtutils = {
2288
+ dist2: (v, w) => {
2289
+ return (v.x - w.x) * (v.x - w.x) + (v.y - w.y) * (v.y - w.y);
2290
+ }
2291
+ };
2292
+
2293
+ /**
2294
+ * @author Ikaros Kappler
2295
+ * @date 2019-01-30
2296
+ * @modified 2019-02-23 Added the toSVGString function, overriding Line.toSVGString.
2297
+ * @modified 2019-03-20 Added JSDoc tags.
2298
+ * @modified 2019-04-19 Added the clone function (overriding Line.clone()).
2299
+ * @modified 2019-09-02 Added the Vector.perp() function.
2300
+ * @modified 2019-09-02 Added the Vector.inverse() function.
2301
+ * @modified 2019-12-04 Added the Vector.inv() function.
2302
+ * @modified 2020-03-23 Ported to Typescript from JS.
2303
+ * @modified 2021-01-20 Added UID.
2304
+ * @modified 2022-02-02 Added the `destroy` method.
2305
+ * @modified 2022-02-02 Cleared the `Vector.toSVGString` function (deprecated). Use `drawutilssvg` instead.
2306
+ * @modified 2022-10-25 Added the `getOrthogonal` method.
2307
+ * @modified 2025-04-14 Added the `Vector.rotate(number)` method.
2308
+ * @version 1.6.0
2309
+ *
2310
+ * @file Vector
2311
+ * @public
2312
+ **/
2313
+ /**
2314
+ * @classdesc A vector (Vertex,Vertex) is a line with a visible direction.<br>
2315
+ * <br>
2316
+ * Vectors are drawn with an arrow at their end point.<br>
2317
+ * <b>The Vector class extends the Line class.</b>
2318
+ *
2319
+ * @requires VertTuple
2320
+ * @requires Vertex
2321
+ **/
2322
+ class Vector extends VertTuple {
2323
+ /**
2324
+ * The constructor.
2325
+ *
2326
+ * @constructor
2327
+ * @name Vector
2328
+ * @extends Line
2329
+ * @param {Vertex} vertA - The start vertex of the vector.
2330
+ * @param {Vertex} vertB - The end vertex of the vector.
2331
+ **/
2332
+ constructor(vertA, vertB) {
2333
+ super(vertA, vertB, (a, b) => new Vector(a, b));
2334
+ /**
2335
+ * Required to generate proper CSS classes and other class related IDs.
2336
+ **/
2337
+ this.className = "Vector";
2338
+ }
2339
+ /**
2340
+ * Get the perpendicular of this vector which is located at a.
2341
+ *
2342
+ * @return {Vector} A new vector being the perpendicular of this vector sitting on a.
2343
+ **/
2344
+ perp() {
2345
+ var v = this.clone();
2346
+ v.sub(this.a);
2347
+ v = new Vector(new Vertex(), new Vertex(-v.b.y, v.b.x));
2348
+ v.a.add(this.a);
2349
+ v.b.add(this.a);
2350
+ return v;
2351
+ }
2352
+ /**
2353
+ * The inverse of a vector is a vector with the same magnitude but oppose direction.
2354
+ *
2355
+ * Please not that the origin of this vector changes here: a->b becomes b->a.
2356
+ *
2357
+ * @return {Vector}
2358
+ **/
2359
+ inverse() {
2360
+ var tmp = this.a;
2361
+ this.a = this.b;
2362
+ this.b = tmp;
2363
+ return this;
2364
+ }
2365
+ /**
2366
+ * This function computes the inverse of the vector, which means 'a' stays untouched.
2367
+ *
2368
+ * @return {Vector} this for chaining.
2369
+ **/
2370
+ inv() {
2371
+ this.b.x = this.a.x - (this.b.x - this.a.x);
2372
+ this.b.y = this.a.y - (this.b.y - this.a.y);
2373
+ return this;
2374
+ }
2375
+ /**
2376
+ * Get the intersection if this vector and the specified vector.
2377
+ *
2378
+ * @method intersection
2379
+ * @param {Vector} line The second vector.
2380
+ * @return {Vertex} The intersection (may lie outside the end-points).
2381
+ * @instance
2382
+ * @memberof Line
2383
+ **/
2384
+ intersection(line) {
2385
+ var denominator = this.denominator(line);
2386
+ if (denominator == 0)
2387
+ return null;
2388
+ var a = this.a.y - line.a.y;
2389
+ var b = this.a.x - line.a.x;
2390
+ var numerator1 = (line.b.x - line.a.x) * a - (line.b.y - line.a.y) * b;
2391
+ var numerator2 = (this.b.x - this.a.x) * a - (this.b.y - this.a.y) * b;
2392
+ a = numerator1 / denominator; // NaN if parallel lines
2393
+ b = numerator2 / denominator;
2394
+ // TODO:
2395
+ // FOR A VECTOR THE LINE-INTERSECTION MUST BE ON BOTH VECTORS
2396
+ // if we cast these lines infinitely in both directions, they intersect here:
2397
+ return new Vertex(this.a.x + a * (this.b.x - this.a.x), this.a.y + a * (this.b.y - this.a.y));
2398
+ }
2399
+ /**
2400
+ * Get the orthogonal "vector" of this vector (rotated by 90° clockwise).
2401
+ *
2402
+ * @name getOrthogonal
2403
+ * @method getOrthogonal
2404
+ * @return {Vector} A new vector with the same length that stands on this vector's point a.
2405
+ * @instance
2406
+ * @memberof Vector
2407
+ **/
2408
+ getOrthogonal() {
2409
+ // Orthogonal of vector (0,0)->(x,y) is (0,0)->(-y,x)
2410
+ const linePoint = this.a.clone();
2411
+ const startPoint = this.b.clone().sub(this.a);
2412
+ const tmp = startPoint.x;
2413
+ startPoint.x = -startPoint.y;
2414
+ startPoint.y = tmp;
2415
+ return new Vector(linePoint, startPoint.add(this.a));
2416
+ }
2417
+ /**
2418
+ * Rotate this vector by the given angle around the first point `a`.
2419
+ *
2420
+ * @name rotate
2421
+ * @method rotate
2422
+ * @return {Vector} this - for chaining.
2423
+ * @instance
2424
+ * @memberof Vector
2425
+ */
2426
+ rotate(angle) {
2427
+ this.b.rotate(angle, this.a);
2428
+ return this;
2228
2429
  }
2229
2430
  }
2230
- /**
2231
- * @private
2232
- **/
2233
- VertTuple.vtutils = {
2234
- dist2: (v, w) => {
2235
- return (v.x - w.x) * (v.x - w.x) + (v.y - w.y) * (v.y - w.y);
2431
+ Vector.utils = {
2432
+ /**
2433
+ * Generate a four-point arrow head, starting at the vector end minus the
2434
+ * arrow head length.
2435
+ *
2436
+ * The first vertex in the returned array is guaranteed to be the located
2437
+ * at the vector line end minus the arrow head length.
2438
+ *
2439
+ *
2440
+ * Due to performance all params are required.
2441
+ *
2442
+ * The params scaleX and scaleY are required for the case that the scaling is not uniform (x and y
2443
+ * scaling different). Arrow heads should not look distored on non-uniform scaling.
2444
+ *
2445
+ * If unsure use 1.0 for scaleX and scaleY (=no distortion).
2446
+ * For headlen use 8, it's a good arrow head size.
2447
+ *
2448
+ * Example:
2449
+ * buildArrowHead( new Vertex(0,0), new Vertex(50,100), 8, 1.0, 1.0 )
2450
+ *
2451
+ * @param {XYCoords} zA - The start vertex of the vector to calculate the arrow head for.
2452
+ * @param {XYCoords} zB - The end vertex of the vector.
2453
+ * @param {number} headlen - The length of the arrow head (along the vector direction. A good value is 12).
2454
+ * @param {number} scaleX - The horizontal scaling during draw.
2455
+ * @param {number} scaleY - the vertical scaling during draw.
2456
+ **/
2457
+ buildArrowHead: (zA, zB, headlen, scaleX, scaleY) => {
2458
+ const angle = Math.atan2((zB.y - zA.y) * scaleY, (zB.x - zA.x) * scaleX);
2459
+ const vertices = [];
2460
+ vertices.push(new Vertex(zB.x * scaleX - headlen * Math.cos(angle), zB.y * scaleY - headlen * Math.sin(angle)));
2461
+ vertices.push(new Vertex(zB.x * scaleX - headlen * 1.35 * Math.cos(angle - Math.PI / 8), zB.y * scaleY - headlen * 1.35 * Math.sin(angle - Math.PI / 8)));
2462
+ vertices.push(new Vertex(zB.x * scaleX, zB.y * scaleY));
2463
+ vertices.push(new Vertex(zB.x * scaleX - headlen * 1.35 * Math.cos(angle + Math.PI / 8), zB.y * scaleY - headlen * 1.35 * Math.sin(angle + Math.PI / 8)));
2464
+ return vertices;
2236
2465
  }
2237
2466
  };
2238
2467
 
@@ -2254,7 +2483,9 @@ VertTuple.vtutils = {
2254
2483
  * @modified 2022-10-09 Changed the actual return value of the `intersection` function to null (was undefined before).
2255
2484
  * @modified 2022-10-17 Adding these methods from the `PathSegment` interface: getStartPoint, getEndPoint, revert.
2256
2485
  * @modified 2023-09-25 Changed param type of `intersection()` from Line to VertTuple.
2257
- * @version 2.3.0
2486
+ * @modified 2025-04-15 Class `Line` now implements interface `Intersectable`.
2487
+ * @modified 2025-04-16 Class `Line` now implements interface `IBounded`.
2488
+ * @version 2.4.0
2258
2489
  *
2259
2490
  * @file Line
2260
2491
  * @public
@@ -2313,6 +2544,19 @@ class Line extends VertTuple {
2313
2544
  // if we cast these lines infinitely in both directions, they intersect here:
2314
2545
  return new Vertex(x, y);
2315
2546
  }
2547
+ //--- BEGIN --- Implement interface `IBounded`
2548
+ /**
2549
+ * Get the bounding box (bounds) of this Line.
2550
+ *
2551
+ * @method getBounds
2552
+ * @instance
2553
+ * @memberof Line
2554
+ * @return {Bounds} The rectangular bounds of this Line.
2555
+ **/
2556
+ getBounds() {
2557
+ return Bounds.computeFromVertices([this.a, this.b]);
2558
+ }
2559
+ //--- END --- Implement interface `IBounded`
2316
2560
  //--- Implement PathSegment ---
2317
2561
  /**
2318
2562
  * Get the start point of this path segment.
@@ -2367,6 +2611,50 @@ class Line extends VertTuple {
2367
2611
  this.b = tmp;
2368
2612
  return this;
2369
2613
  }
2614
+ //--- END Implement PathSegment ---
2615
+ //--- BEGIN --- Implement interface `Intersectable`
2616
+ /**
2617
+ * Get all line intersections with this polygon.
2618
+ *
2619
+ * This method returns all intersections (as vertices) with this shape. The returned array of vertices is in no specific order.
2620
+ *
2621
+ * See demo `47-closest-vector-projection-on-polygon` for how it works.
2622
+ *
2623
+ * @param {VertTuple} line - The line to find intersections with.
2624
+ * @param {boolean} inVectorBoundsOnly - If set to true only intersecion points on the passed vector are returned (located strictly between start and end vertex).
2625
+ * @returns {Array<Vertex>} - An array of all intersections within the polygon bounds.
2626
+ */
2627
+ lineIntersections(line, inVectorBoundsOnly = false) {
2628
+ // Find the intersections of all lines inside the edge bounds
2629
+ const intersection = this.intersection(line);
2630
+ if (!intersection) {
2631
+ return []; // Both lines parallel
2632
+ }
2633
+ if (this.hasPoint(intersection, true) && (!inVectorBoundsOnly || line.hasPoint(intersection, inVectorBoundsOnly))) {
2634
+ return [intersection];
2635
+ }
2636
+ else {
2637
+ return [];
2638
+ }
2639
+ }
2640
+ /**
2641
+ * Get all line intersections of this polygon and their tangents along the shape.
2642
+ *
2643
+ * This method returns all intersection tangents (as vectors) with this shape. The returned array of vectors is in no specific order.
2644
+ *
2645
+ * @param line
2646
+ * @param inVectorBoundsOnly
2647
+ * @returns
2648
+ */
2649
+ lineIntersectionTangents(line, inVectorBoundsOnly = false) {
2650
+ // Find the intersection tangents of all lines inside the edge bounds
2651
+ const intersections = this.lineIntersections(line, inVectorBoundsOnly);
2652
+ if (intersections.length === 0) {
2653
+ return [];
2654
+ }
2655
+ const intrsctn = intersections[0];
2656
+ return [new Vector(this.a.clone(), this.b.clone()).moveTo(intrsctn)];
2657
+ }
2370
2658
  }
2371
2659
 
2372
2660
  /**
@@ -2403,7 +2691,13 @@ class Line extends VertTuple {
2403
2691
  * @modified 2024-10-30 Added the `getEdges` method.
2404
2692
  * @modified 2024-12-02 Added the `elimitateColinearEdges` method.
2405
2693
  * @modified 2025-02-12 Added the `containsVerts` method to test multiple vertices for containment.
2406
- * @version 1.14.0
2694
+ * @modified 2025-03-28 Added the `Polygon.utils.locateLineIntersecion` static helper method.
2695
+ * @modified 2025-03-28 Added the `Polygon.lineIntersectionTangents` method.
2696
+ * @modified 2025-04-09 Added the `Polygon.getCentroid` method.
2697
+ * @modified 2025-05-16 Class `Polygon` now implements `IBounded`.
2698
+ * @modified 2025-05-20 Tweaking `Polygon.getInnerAngleAt` and `Polygo.isAngleAcute` to handle indices out of array bounds as well.
2699
+ * @modified 2025-06-07 Adding `Polygon.closestLineIntersectionIndex` to determine line intersections plus detected edge index.
2700
+ * @version 1.16.0
2407
2701
  *
2408
2702
  * @file Polygon
2409
2703
  * @public
@@ -2510,7 +2804,7 @@ class Polygon {
2510
2804
  * @returns {boolean} `true` is angle is acute, `false` is obtuse.
2511
2805
  */
2512
2806
  getInnerAngleAt(vertIndex) {
2513
- const p2 = this.vertices[vertIndex];
2807
+ const p2 = this.vertices[vertIndex % this.vertices.length];
2514
2808
  const p1 = this.vertices[(vertIndex + this.vertices.length - 1) % this.vertices.length].clone();
2515
2809
  const p3 = this.vertices[(vertIndex + 1) % this.vertices.length].clone();
2516
2810
  // See
@@ -2546,7 +2840,7 @@ class Polygon {
2546
2840
  */
2547
2841
  isAngleAcute(vertIndex) {
2548
2842
  const A = this.vertices[(vertIndex + this.vertices.length - 1) % this.vertices.length].clone();
2549
- const B = this.vertices[vertIndex];
2843
+ const B = this.vertices[vertIndex % this.vertices.length];
2550
2844
  const C = this.vertices[(vertIndex + 1) % this.vertices.length].clone();
2551
2845
  // Find local winding number for triangle A B C
2552
2846
  const windingNumber = Triangle.utils.determinant(A, B, C);
@@ -2612,8 +2906,9 @@ class Polygon {
2612
2906
  let xi = this.vertices[i].x, yi = this.vertices[i].y;
2613
2907
  let xj = this.vertices[j].x, yj = this.vertices[j].y;
2614
2908
  var intersect = yi > vert.y != yj > vert.y && vert.x < ((xj - xi) * (vert.y - yi)) / (yj - yi) + xi;
2615
- if (intersect)
2909
+ if (intersect) {
2616
2910
  inside = !inside;
2911
+ }
2617
2912
  }
2618
2913
  return inside;
2619
2914
  }
@@ -2775,9 +3070,44 @@ class Polygon {
2775
3070
  center.y /= this.vertices.length;
2776
3071
  return center;
2777
3072
  }
3073
+ /**
3074
+ * Get centroid.
3075
+ * Centroids define the barycenter of any non self-intersecting convex polygon.
3076
+ *
3077
+ * If the polygon is self intersecting or non konvex then the barycenter is not well defined.
3078
+ *
3079
+ * https://mathworld.wolfram.com/PolygonCentroid.html
3080
+ *
3081
+ * @method getCentroid
3082
+ * @instance
3083
+ * @memberof Polygon
3084
+ * @returns {Vertex|null}
3085
+ */
3086
+ getCentroid() {
3087
+ if (this.vertices.length === 0) {
3088
+ return null;
3089
+ }
3090
+ const center = new Vertex(0.0, 0.0);
3091
+ const n = this.vertices.length;
3092
+ for (var i = 0; i < n; i++) {
3093
+ // center.add(this.vertices[i]);
3094
+ const cur = this.vertices[i];
3095
+ const next = this.vertices[(i + 1) % n];
3096
+ var factor = cur.x * next.y - next.x * cur.y;
3097
+ center.x += (cur.x + next.x) * factor;
3098
+ center.y += (cur.y + next.y) * factor;
3099
+ }
3100
+ const area = this.area();
3101
+ center.x *= 1 / (6 * area);
3102
+ center.y *= 1 / (6 * area);
3103
+ return center;
3104
+ }
3105
+ //--- BEGIN --- Implement interface `Intersectable`
2778
3106
  /**
2779
3107
  * Get all line intersections with this polygon.
2780
3108
  *
3109
+ * This method returns all intersections (as vertices) with this shape. The returned array of vertices is in no specific order.
3110
+ *
2781
3111
  * See demo `47-closest-vector-projection-on-polygon` for how it works.
2782
3112
  *
2783
3113
  * @param {VertTuple} line - The line to find intersections with.
@@ -2786,20 +3116,45 @@ class Polygon {
2786
3116
  */
2787
3117
  lineIntersections(line, inVectorBoundsOnly = false) {
2788
3118
  // Find the intersections of all lines inside the edge bounds
2789
- const intersectionPoints = [];
2790
- for (var i = 0; i < this.vertices.length; i++) {
2791
- const polyLine = new Line(this.vertices[i], this.vertices[(i + 1) % this.vertices.length]);
2792
- const intersection = polyLine.intersection(line);
2793
- // true => only inside bounds
2794
- // ignore last edge if open
2795
- if ((!this.isOpen || i + 1 !== this.vertices.length) &&
2796
- intersection !== null &&
2797
- polyLine.hasPoint(intersection, true) &&
2798
- (!inVectorBoundsOnly || line.hasPoint(intersection, inVectorBoundsOnly))) {
2799
- intersectionPoints.push(intersection);
2800
- }
2801
- }
2802
- return intersectionPoints;
3119
+ return Polygon.utils
3120
+ .locateLineIntersecion(line, this.vertices, this.isOpen, inVectorBoundsOnly)
3121
+ .map(intersectionTuple => intersectionTuple.intersection);
3122
+ }
3123
+ /**
3124
+ * Get all line intersections of this polygon and their tangents along the shape.
3125
+ *
3126
+ * This method returns all intersection tangents (as vectors) with this shape. The returned array of vectors is in no specific order.
3127
+ *
3128
+ * @param line
3129
+ * @param inVectorBoundsOnly
3130
+ * @returns
3131
+ */
3132
+ lineIntersectionTangents(line, inVectorBoundsOnly = false) {
3133
+ // Find the intersection tangents of all lines inside the edge bounds
3134
+ return Polygon.utils.locateLineIntersecion(line, this.vertices, this.isOpen, inVectorBoundsOnly).map(intersectionTuple => {
3135
+ const polyLine = this.getEdgeAt(intersectionTuple.edgeIndex);
3136
+ return new Vector(polyLine.a.clone(), polyLine.b.clone()).moveTo(intersectionTuple.intersection);
3137
+ });
3138
+ }
3139
+ //--- END --- Implement interface `Intersectable`
3140
+ /**
3141
+ * Get all line intersections of this polygon and their tangents along the shape.
3142
+ *
3143
+ * This method returns all intersection tangents (as vectors) with this shape. The returned array of vectors is in no specific order.
3144
+ *
3145
+ * @param line
3146
+ * @param inVectorBoundsOnly
3147
+ * @returns
3148
+ */
3149
+ lineIntersectionTangentsIndices(line, inVectorBoundsOnly = false) {
3150
+ // Find the intersection tangents of all lines inside the edge bounds
3151
+ return Polygon.utils.locateLineIntersecion(line, this.vertices, this.isOpen, inVectorBoundsOnly).map(intersectionTuple => {
3152
+ const polyLine = this.getEdgeAt(intersectionTuple.edgeIndex);
3153
+ return {
3154
+ intersection: new Vector(polyLine.a.clone(), polyLine.b.clone()).moveTo(intersectionTuple.intersection),
3155
+ edgeIndex: intersectionTuple.edgeIndex
3156
+ };
3157
+ });
2803
3158
  }
2804
3159
  /**
2805
3160
  * Get the closest line-polygon-intersection point (closest the line point A).
@@ -2808,9 +3163,27 @@ class Polygon {
2808
3163
  *
2809
3164
  * @param {VertTuple} line - The line to find intersections with.
2810
3165
  * @param {boolean} inVectorBoundsOnly - If set to true only intersecion points on the passed vector are considered (located strictly between start and end vertex).
2811
- * @returns {Array<Vertex>} - An array of all intersections within the polygon bounds.
3166
+ * @returns {Vertex | null} - The intersection point within the polygon bounds.
2812
3167
  */
2813
3168
  closestLineIntersection(line, inVectorBoundsOnly = false) {
3169
+ var closestInterSectionIndex = this.closestLineIntersectionIndex(line, inVectorBoundsOnly);
3170
+ if (closestInterSectionIndex) {
3171
+ return closestInterSectionIndex.intersection;
3172
+ }
3173
+ else {
3174
+ return null;
3175
+ }
3176
+ }
3177
+ /**
3178
+ * Get the closest line-polygon-intersection point (closest the line point A) plus the edge index..
3179
+ *
3180
+ * See demo `63-measure-angles-on-polygon` for how it works.
3181
+ *
3182
+ * @param {VertTuple} line - The line to find intersections with.
3183
+ * @param {boolean} inVectorBoundsOnly - If set to true only intersecion points on the passed vector are considered (located strictly between start and end vertex).
3184
+ * @returns {PolygonIntersectionTuple| null} - A pair containing the intersection point and the affected polygon edge index.
3185
+ */
3186
+ closestLineIntersectionIndex(line, inVectorBoundsOnly = false) {
2814
3187
  const allIntersections = this.lineIntersections(line, inVectorBoundsOnly);
2815
3188
  if (allIntersections.length <= 0) {
2816
3189
  // Empty polygon -> no intersections
@@ -2818,17 +3191,20 @@ class Polygon {
2818
3191
  }
2819
3192
  // Find the closest intersection
2820
3193
  let closestIntersection = new Vertex(Number.MAX_VALUE, Number.MAX_VALUE);
3194
+ let closestInterSectionIndex = -1;
2821
3195
  let curDist = Number.MAX_VALUE;
2822
- for (var i in allIntersections) {
3196
+ for (var i = 0; i < allIntersections.length; i++) {
2823
3197
  const curVert = allIntersections[i];
2824
3198
  const dist = curVert.distance(line.a);
2825
3199
  if (dist < curDist) {
2826
3200
  // && line.hasPoint(curVert)) {
2827
3201
  curDist = dist;
2828
3202
  closestIntersection = curVert;
3203
+ closestInterSectionIndex = i;
2829
3204
  }
2830
3205
  }
2831
- return closestIntersection;
3206
+ // return [closestIntersection, closestInterSectionIndex];
3207
+ return { edgeIndex: closestInterSectionIndex, intersection: closestIntersection };
2832
3208
  }
2833
3209
  /**
2834
3210
  * Construct a new polygon from this polygon with more vertices on each edge. The
@@ -2901,6 +3277,7 @@ class Polygon {
2901
3277
  }
2902
3278
  return result;
2903
3279
  }
3280
+ //--- BEGIN --- Implement interface `IBounded`
2904
3281
  /**
2905
3282
  * Get the bounding box (bounds) of this polygon.
2906
3283
  *
@@ -2912,6 +3289,7 @@ class Polygon {
2912
3289
  getBounds() {
2913
3290
  return Bounds.computeFromVertices(this.vertices);
2914
3291
  }
3292
+ //--- END --- Implement interface `IBounded`
2915
3293
  /**
2916
3294
  * Create a deep copy of this polygon.
2917
3295
  *
@@ -2966,7 +3344,6 @@ class Polygon {
2966
3344
  // -> delete all vertices in between
2967
3345
  if (j - i > 2) {
2968
3346
  // Means: there have been 'colinear vertices' in between
2969
- // console.log("Splice", "i", i, "j", j, i + 1, j - i - 1);
2970
3347
  verts.splice(i + 1, j - i - 2);
2971
3348
  }
2972
3349
  i++;
@@ -3163,6 +3540,32 @@ Polygon.utils = {
3163
3540
  sum += (vertices[j].x - vertices[i].x) * (vertices[i].y + vertices[j].y);
3164
3541
  }
3165
3542
  return sum;
3543
+ },
3544
+ /**
3545
+ * Find intersections of a line with a polygon (vertices).
3546
+ *
3547
+ * @param {VertTuple<any>} line - The line to find intersections with.
3548
+ * @param {Array<Vertex>} vertices - The polygon's vertices.
3549
+ * @param {boolean} isOpen - True if the polygon is open, false otherwise.
3550
+ * @param {boolean} inVectorBoundsOnly - If only intersections in strict vector bounds should be returned.
3551
+ * @returns
3552
+ */
3553
+ locateLineIntersecion(line, vertices, isOpen, inVectorBoundsOnly) {
3554
+ // Find the intersections of all lines inside the edge bounds
3555
+ const intersectionPoints = [];
3556
+ var n = isOpen ? vertices.length - 1 : vertices.length;
3557
+ for (var i = 0; i < n; i++) {
3558
+ const polyLine = new Line(vertices[i % n], vertices[(i + 1) % n]);
3559
+ const intersection = polyLine.intersection(line);
3560
+ // true => only inside bounds
3561
+ // ignore last edge if open
3562
+ if (intersection !== null &&
3563
+ polyLine.hasPoint(intersection, true) &&
3564
+ (!inVectorBoundsOnly || line.hasPoint(intersection, inVectorBoundsOnly))) {
3565
+ intersectionPoints.push({ edgeIndex: i, intersection: intersection });
3566
+ }
3567
+ }
3568
+ return intersectionPoints;
3166
3569
  }
3167
3570
  };
3168
3571
 
@@ -3177,7 +3580,11 @@ Polygon.utils = {
3177
3580
  * @modified 2022-10-09 Added the `fromDimension` function.
3178
3581
  * @modified 2022-11-28 Added the `clone` method.
3179
3582
  * @modified 2023-09-29 Added the `randomPoint` method.
3180
- * @version 1.7.0
3583
+ * @modified 2025-03-23 Added the `getMinDimension` and `getMaxDimension` methods.
3584
+ * @modified 2025-04-18 Change parameter type in `Bounds.computeFromVertices` from `Vertex` to more general `XYCoords`.
3585
+ * @modified 2025-04-19 Added methods to `Bounds` class: `getNorthPoint`, `getSouthPoint`, `getEastPoint` and `getWestPoint`.
3586
+ * @modified 2025-04-26 Added static method `Bounds.computeFromBoundsSet` to calculate containing bounds for a set of bounding boxes.
3587
+ * @version 1.8.0
3181
3588
  **/
3182
3589
  /**
3183
3590
  * @classdesc A bounds class with min and max values. Implementing IBounds.
@@ -3201,6 +3608,54 @@ class Bounds {
3201
3608
  this.width = max.x - min.x;
3202
3609
  this.height = max.y - min.y;
3203
3610
  }
3611
+ /**
3612
+ * Get the center point of the north bound.
3613
+ *
3614
+ * @method getNorthPoint
3615
+ * @instance
3616
+ * @memberof Bounds
3617
+ * @return {Vertex} The "northmost" centered point of this bounding box.
3618
+ */
3619
+ getNorthPoint() {
3620
+ return new Vertex(this.min.x + this.width / 2.0, this.min.y);
3621
+ }
3622
+ ;
3623
+ /**
3624
+ * Get the center point of the south bound.
3625
+ *
3626
+ * @method getNorthPoint
3627
+ * @instance
3628
+ * @memberof Bounds
3629
+ * @return {Vertex} The "southhmost" centered point of this bounding box.
3630
+ */
3631
+ getSouthPoint() {
3632
+ return new Vertex(this.min.x + this.width / 2.0, this.max.y);
3633
+ }
3634
+ ;
3635
+ /**
3636
+ * Get the center point of the west bound.
3637
+ *
3638
+ * @method getWestPoint
3639
+ * @instance
3640
+ * @memberof Bounds
3641
+ * @return {Vertex} The "westhmost" centered point of this bounding box.
3642
+ */
3643
+ getWestPoint() {
3644
+ return new Vertex(this.min.x, this.min.y + this.height / 2.0);
3645
+ }
3646
+ ;
3647
+ /**
3648
+ * Get the center point of the east bound.
3649
+ *
3650
+ * @method getEastPoint
3651
+ * @instance
3652
+ * @memberof Bounds
3653
+ * @return {Vertex} The "easthmost" centered point of this bounding box.
3654
+ */
3655
+ getEastPoint() {
3656
+ return new Vertex(this.max.x, this.min.y + this.height / 2.0);
3657
+ }
3658
+ ;
3204
3659
  /**
3205
3660
  * Convert this rectangular bounding box to a polygon with four vertices.
3206
3661
  *
@@ -3223,6 +3678,22 @@ class Bounds {
3223
3678
  getCenter() {
3224
3679
  return new Vertex(this.min.x + (this.max.x - this.min.x) / 2.0, this.min.y + (this.max.y - this.min.y) / 2);
3225
3680
  }
3681
+ /**
3682
+ * Get the minimum of `width` and `height`.
3683
+ *
3684
+ * @returns {number} The value of Math.min( this.width, this.height )
3685
+ */
3686
+ getMinDimension() {
3687
+ return Math.min(this.width, this.height);
3688
+ }
3689
+ /**
3690
+ * Get the minimum of `width` and `height`.
3691
+ *
3692
+ * @returns {number} The value of Math.min( this.width, this.height )
3693
+ */
3694
+ getMaxDimension() {
3695
+ return Math.max(this.width, this.height);
3696
+ }
3226
3697
  /**
3227
3698
  * Generate a random point inside this bounds object. Safe areas at the border to avoid
3228
3699
  * included.
@@ -3273,12 +3744,13 @@ class Bounds {
3273
3744
  * @static
3274
3745
  * @method computeFromVertices
3275
3746
  * @memberof Bounds
3276
- * @param {Array<Vertex>} vertices - The set of vertices you want to get the bounding box for.
3747
+ * @param {Array<XYCoords>} vertices - The set of vertices you want to get the bounding box for.
3277
3748
  * @return The minimal Bounds for the given vertices.
3278
3749
  **/
3279
3750
  static computeFromVertices(vertices) {
3280
- if (vertices.length == 0)
3751
+ if (vertices.length == 0) {
3281
3752
  return new Bounds(new Vertex(0, 0), new Vertex(0, 0));
3753
+ }
3282
3754
  let xMin = vertices[0].x;
3283
3755
  let xMax = vertices[0].x;
3284
3756
  let yMin = vertices[0].y;
@@ -3293,6 +3765,35 @@ class Bounds {
3293
3765
  }
3294
3766
  return new Bounds(new Vertex(xMin, yMin), new Vertex(xMax, yMax));
3295
3767
  }
3768
+ /**
3769
+ * Compute the minimal bounding box for a given set of existing bounding boxes.
3770
+ *
3771
+ * An empty vertex array will return an empty bounding box located at (0,0).
3772
+ *
3773
+ * @static
3774
+ * @method computeFromBoundsSet
3775
+ * @memberof Bounds
3776
+ * @param {Array<IBounds>} boundingBoxes - The set of existing bounding boxes to get the containing bounding box for.
3777
+ * @return The minimal Bounds for the given bounds instances.
3778
+ **/
3779
+ static computeFromBoundsSet(boundingBoxes) {
3780
+ if (boundingBoxes.length == 0) {
3781
+ return new Bounds(new Vertex(0, 0), new Vertex(0, 0));
3782
+ }
3783
+ let xMin = boundingBoxes[0].min.x;
3784
+ let xMax = boundingBoxes[0].max.x;
3785
+ let yMin = boundingBoxes[0].min.y;
3786
+ let yMax = boundingBoxes[0].min.y;
3787
+ let bounds;
3788
+ for (var i in boundingBoxes) {
3789
+ bounds = boundingBoxes[i];
3790
+ xMin = Math.min(xMin, bounds.min.x);
3791
+ xMax = Math.max(xMax, bounds.max.x);
3792
+ yMin = Math.min(yMin, bounds.min.y);
3793
+ yMax = Math.max(yMax, bounds.min.y);
3794
+ }
3795
+ return new Bounds(new Vertex(xMin, yMin), new Vertex(xMax, yMax));
3796
+ }
3296
3797
  /**
3297
3798
  * Create a new `Bounds` instance just from `width` and `height`, located at (0,0) or the optionally given origin.
3298
3799
  *
@@ -3331,7 +3832,17 @@ class Bounds {
3331
3832
  * @modified 2022-10-17 The `CubicBezierCurve` class now implements the new `PathSegment` interface.
3332
3833
  * @modified 2023-09-30 Added the function `CubicbezierCurve.getSubCurve(number,number)` – similar to `getSubCurveAt(...)` but with absolute position parameters.
3333
3834
  * @modified 2023-10-07 Added the `trimEnd`, `trimEndAt`, `trimStart`, `trimStartAt` methods.
3334
- * @version 2.8.0
3835
+ * @modified 2025-04-09 Added the `CubicBezierCurve.move` method to match the convention – which just calls `translate`.
3836
+ * @modified 2025-04-09 Modified the `CubicBezierCurve.translate` method: chaning parameter `Vertex` to more generalized `XYCoords`.
3837
+ * @modified 2025-04-13 Changed visibility of `CubicBezierCurve.utils` from 'private' to 'public'.
3838
+ * @modified 2025-04-13 Added helper function `CubicBezierCurve.utils.bezierCoeffs`.
3839
+ * @modified 2025-04-13 Added helper functopn `CubicBezierCurve.utils.sgn(number)` for division safe sign calculation.
3840
+ * @modified 2025-03-13 Class `CubicBezierCurve` is now implementing interface `Intersectable`.
3841
+ * @modified 2025-04-18 Added evaluation method for cubic Bézier curves `CubicBezierCurve.utils.evaluateT`.
3842
+ * @modified 2025-04-18 Refactored method `CubicBezierCurve.getPointAt` to use `evaluateT`.
3843
+ * @modified 2025-04-18 Fixed the `CubicBezierCurve.getBounds` method: now returning the real bounding box. Before it was an approximated one.
3844
+ * @modified 2025-ß4-18 Added helper methods for bounding box calculation `CubucBezierCurve.util.cubicPolyMinMax` and `cubicPoly`.
3845
+ * @version 2.9.0
3335
3846
  *
3336
3847
  * @file CubicBezierCurve
3337
3848
  * @public
@@ -3420,7 +3931,7 @@ class CubicBezierCurve {
3420
3931
  * Translate the whole curve by the given {x,y} amount: moves all four points.
3421
3932
  *
3422
3933
  * @method translate
3423
- * @param {Vertex} amount - The amount to translate this curve by.
3934
+ * @param {XYCoords} amount - The amount to translate this curve by.
3424
3935
  * @instance
3425
3936
  * @memberof CubicBezierCurve
3426
3937
  * @return {CubicBezierCurve} this (for chaining).
@@ -3432,6 +3943,18 @@ class CubicBezierCurve {
3432
3943
  this.endPoint.add(amount);
3433
3944
  return this;
3434
3945
  }
3946
+ /**
3947
+ * Translate the whole curve by the given {x,y} amount: moves all four points.
3948
+ *
3949
+ * @method translate
3950
+ * @param {XYCoords} amount - The amount to translate this curve by.
3951
+ * @instance
3952
+ * @memberof CubicBezierCurve
3953
+ * @return {CubicBezierCurve} this (for chaining).
3954
+ **/
3955
+ move(amount) {
3956
+ return this.translate(amount);
3957
+ }
3435
3958
  /**
3436
3959
  * Reverse this curve, means swapping start- and end-point and swapping
3437
3960
  * start-control- and end-control-point.
@@ -3512,6 +4035,9 @@ class CubicBezierCurve {
3512
4035
  *
3513
4036
  * This function uses a recursive approach by cutting the curve into several linear segments.
3514
4037
  *
4038
+ * @method getClosestT
4039
+ * @instance
4040
+ * @memberof CubicBezierCurve
3515
4041
  * @param {Vertex} p - The point to find the closest position ('t' on the curve).
3516
4042
  * @return {number}
3517
4043
  **/
@@ -3561,27 +4087,35 @@ class CubicBezierCurve {
3561
4087
  tNext: tStart + tDiff * (Math.min(stepCount, minIndex + 1) / stepCount)
3562
4088
  };
3563
4089
  }
4090
+ //--- BEGIN --- Implement interface `IBounded`
3564
4091
  /**
3565
4092
  * Get the bounds of this bezier curve.
3566
4093
  *
3567
4094
  * The bounds are approximated by the underlying segment buffer; the more segment there are,
3568
4095
  * the more accurate will be the returned bounds.
3569
4096
  *
3570
- * @return {Bounds} The bounds of this curve.
4097
+ * @method getClosestT
4098
+ * @instance
4099
+ * @memberof CubicBezierCurve
4100
+ * @return {Bounds} The bounds of this ellipse.
3571
4101
  **/
3572
4102
  getBounds() {
3573
- var min = new Vertex(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY);
3574
- var max = new Vertex(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY);
3575
- let v;
3576
- for (var i = 0; i < this.segmentCache.length; i++) {
3577
- v = this.segmentCache[i];
3578
- min.x = Math.min(min.x, v.x);
3579
- min.y = Math.min(min.y, v.y);
3580
- max.x = Math.max(max.x, v.x);
3581
- max.y = Math.max(max.y, v.y);
3582
- }
3583
- return new Bounds(min, max);
3584
- }
4103
+ // Thanks to Richard "RM" for the Bézier bounds calculatin
4104
+ // https://jsfiddle.net/SalixAlba/QQnvm/4/
4105
+ const xMinMax = CubicBezierCurve.utils.cubicPolyMinMax(this.startPoint.x, this.startControlPoint.x, this.endControlPoint.x, this.endPoint.x);
4106
+ const xl = xMinMax.min;
4107
+ const xh = xMinMax.max;
4108
+ const yMinMax = CubicBezierCurve.utils.cubicPolyMinMax(this.startPoint.y, this.startControlPoint.y, this.endControlPoint.y, this.endPoint.y);
4109
+ const yl = yMinMax.min;
4110
+ const yh = yMinMax.max;
4111
+ return Bounds.computeFromVertices([
4112
+ { x: xl, y: yl },
4113
+ { x: xl, y: yh },
4114
+ { x: xh, y: yh },
4115
+ { x: xh, y: yl }
4116
+ ]);
4117
+ }
4118
+ //--- END --- Implement interface `IBounded`
3585
4119
  /**
3586
4120
  * Get the start point of the curve.<br>
3587
4121
  * <br>
@@ -3668,14 +4202,19 @@ class CubicBezierCurve {
3668
4202
  **/
3669
4203
  getPointAt(t) {
3670
4204
  // Perform some powerful math magic
3671
- const x = this.startPoint.x * Math.pow(1.0 - t, 3) +
3672
- this.startControlPoint.x * 3 * t * Math.pow(1.0 - t, 2) +
3673
- this.endControlPoint.x * 3 * Math.pow(t, 2) * (1.0 - t) +
3674
- this.endPoint.x * Math.pow(t, 3);
3675
- const y = this.startPoint.y * Math.pow(1.0 - t, 3) +
3676
- this.startControlPoint.y * 3 * t * Math.pow(1.0 - t, 2) +
3677
- this.endControlPoint.y * 3 * Math.pow(t, 2) * (1.0 - t) +
3678
- this.endPoint.y * Math.pow(t, 3);
4205
+ // TODO: cleanup
4206
+ // const x: number =
4207
+ // this.startPoint.x * Math.pow(1.0 - t, 3) +
4208
+ // this.startControlPoint.x * 3 * t * Math.pow(1.0 - t, 2) +
4209
+ // this.endControlPoint.x * 3 * Math.pow(t, 2) * (1.0 - t) +
4210
+ // this.endPoint.x * Math.pow(t, 3);
4211
+ // const y: number =
4212
+ // this.startPoint.y * Math.pow(1.0 - t, 3) +
4213
+ // this.startControlPoint.y * 3 * t * Math.pow(1.0 - t, 2) +
4214
+ // this.endControlPoint.y * 3 * Math.pow(t, 2) * (1.0 - t) +
4215
+ // this.endPoint.y * Math.pow(t, 3);
4216
+ const x = CubicBezierCurve.utils.evaluateT(this.startPoint.x, this.startControlPoint.x, this.endControlPoint.x, this.endPoint.x, t);
4217
+ const y = CubicBezierCurve.utils.evaluateT(this.startPoint.y, this.startControlPoint.y, this.endControlPoint.y, this.endPoint.y, t);
3679
4218
  return new Vertex(x, y);
3680
4219
  }
3681
4220
  /**
@@ -3816,17 +4355,7 @@ class CubicBezierCurve {
3816
4355
  * @return {CubicBezierCurve} The sub curve as a new curve.
3817
4356
  **/
3818
4357
  getSubCurveAt(tStart, tEnd) {
3819
- // const startVec: Vector = new Vector(this.getPointAt(tStart), this.getTangentAt(tStart));
3820
- // const endVec: Vector = new Vector(this.getPointAt(tEnd), this.getTangentAt(tEnd).inv());
3821
- // // Tangents are relative. Make absolute.
3822
- // startVec.b.add(startVec.a);
3823
- // endVec.b.add(endVec.a);
3824
- // // This 'splits' the curve at the given point at t.
3825
- // startVec.scale(0.33333333 * (tEnd - tStart));
3826
- // endVec.scale(0.33333333 * (tEnd - tStart));
3827
- // // Draw the bezier curve
3828
- // // pb.draw.cubicBezier( startVec.a, endVec.a, startVec.b, endVec.b, '#8800ff', 2 );
3829
- // return new CubicBezierCurve(startVec.a, endVec.a, startVec.b, endVec.b);
4358
+ // This 'splits' the curve at the given point at t.
3830
4359
  const subCurbePoints = CubicBezierCurve.utils.getSubCurvePointsAt(this, tStart, tEnd);
3831
4360
  return new CubicBezierCurve(subCurbePoints[0], subCurbePoints[1], subCurbePoints[2], subCurbePoints[3]);
3832
4361
  }
@@ -3918,6 +4447,69 @@ class CubicBezierCurve {
3918
4447
  return this.endControlPoint;
3919
4448
  }
3920
4449
  //---END PathSegment-------------------------
4450
+ //--- BEGIN --- Implement interface `Intersectable`
4451
+ /**
4452
+ * Get all line intersections with this shape.
4453
+ *
4454
+ * This method returns all intersections (as vertices) with this shape. The returned array of vertices is in no specific order.
4455
+ *
4456
+ * @param {VertTuple} line - The line to find intersections with.
4457
+ * @param {boolean} inVectorBoundsOnly - If set to true only intersecion points on the passed vector are returned (located strictly between start and end vertex).
4458
+ * @returns {Array<Vertex>} - An array of all intersections with the shape outline.
4459
+ */
4460
+ lineIntersections(line, inVectorBoundsOnly = false) {
4461
+ const intersectionTs = this.lineIntersectionTs(line);
4462
+ const intersectionPoints = intersectionTs.map((t) => {
4463
+ return this.getPointAt(t);
4464
+ });
4465
+ if (inVectorBoundsOnly) {
4466
+ // const maxDist = line.length();
4467
+ return intersectionPoints.filter((vert) => line.hasPoint(vert, true));
4468
+ }
4469
+ else {
4470
+ return intersectionPoints;
4471
+ }
4472
+ }
4473
+ /**
4474
+ * Get all line intersections of this polygon and their tangents along the shape.
4475
+ *
4476
+ * This method returns all intersection tangents (as vectors) with this shape. The returned array of vectors is in no specific order.
4477
+ *
4478
+ * @param line
4479
+ * @param lineIntersectionTangents
4480
+ * @returns
4481
+ */
4482
+ lineIntersectionTangents(line, inVectorBoundsOnly = false) {
4483
+ const intersectionTs = this.lineIntersectionTs(line);
4484
+ const intersectionTangents = intersectionTs.map((t) => {
4485
+ const startPoint = this.getPointAt(t);
4486
+ const endPoint = this.getTangentAt(t);
4487
+ return new Vector(startPoint, endPoint.add(startPoint));
4488
+ });
4489
+ if (inVectorBoundsOnly) {
4490
+ return intersectionTangents.filter((vec) => line.hasPoint(vec.a, true));
4491
+ }
4492
+ else {
4493
+ return intersectionTangents;
4494
+ }
4495
+ }
4496
+ //--- END --- Implement interface `Intersectable`
4497
+ lineIntersectionTs(line) {
4498
+ var A = line.b.y - line.a.y; // A=y2-y1
4499
+ var B = line.a.x - line.b.x; // B=x1-x2
4500
+ var C = line.a.x * (line.a.y - line.b.y) + line.a.y * (line.b.x - line.a.x); //C=x1*(y1-y2)+y1*(x2-x1)
4501
+ // var bx = bezierCoeffs(px[0], px[1], px[2], px[3]);
4502
+ // var by = bezierCoeffs(py[0], py[1], py[2], py[3]);
4503
+ var bx = CubicBezierCurve.utils.bezierCoeffs(this.startPoint.x, this.startControlPoint.x, this.endControlPoint.x, this.endPoint.x);
4504
+ var by = CubicBezierCurve.utils.bezierCoeffs(this.startPoint.y, this.startControlPoint.y, this.endControlPoint.y, this.endPoint.y);
4505
+ const poly = Array(4);
4506
+ poly[0] = A * bx[0] + B * by[0]; /*t^3*/
4507
+ poly[1] = A * bx[1] + B * by[1]; /*t^2*/
4508
+ poly[2] = A * bx[2] + B * by[2]; /*t*/
4509
+ poly[3] = A * bx[3] + B * by[3] + C; /*1*/
4510
+ var roots = CubicBezierCurve.utils.cubicRoots(poly);
4511
+ return roots.filter((root) => root != -1);
4512
+ }
3921
4513
  /**
3922
4514
  * Check if this and the specified curve are equal.<br>
3923
4515
  * <br>
@@ -4085,6 +4677,71 @@ CubicBezierCurve.END_POINT = 3;
4085
4677
  * Helper utils.
4086
4678
  */
4087
4679
  CubicBezierCurve.utils = {
4680
+ evaluateT: (p0, p1, p2, p3, t) => {
4681
+ return p0 * Math.pow(1.0 - t, 3) +
4682
+ p1 * 3 * t * Math.pow(1.0 - t, 2) +
4683
+ p2 * 3 * Math.pow(t, 2) * (1.0 - t) +
4684
+ p3 * Math.pow(t, 3);
4685
+ },
4686
+ cubicPolyMinMax: (p0, p1, p2, p3) => {
4687
+ // var polyX = CubicBezierCurve.utils.cubicPoly2(
4688
+ // p0, // P[0].X, // bezierCurve.startPoint.x,
4689
+ // p1, // P[1].X, // bezierCurve.startControlPoint.x,
4690
+ // p2, // P[2].X, // bezierCurve.endControlPoint.x,
4691
+ // p3 // P[3].X // bezierCurve.endPoint.x
4692
+ // );
4693
+ // var a = polyX.a;
4694
+ // var b = polyX.b;
4695
+ // var c = polyX.c;
4696
+ // var disc = polyX.b * polyX.b - 4 * polyX.a * polyX.c;
4697
+ var polyX = CubicBezierCurve.utils.cubicPoly(p0, // P[0].X, // bezierCurve.startPoint.x,
4698
+ p1, // P[1].X, // bezierCurve.startControlPoint.x,
4699
+ p2, // P[2].X, // bezierCurve.endControlPoint.x,
4700
+ p3 // P[3].X // bezierCurve.endPoint.x
4701
+ );
4702
+ var a = polyX[0]; // .a;
4703
+ var b = polyX[1]; // .b;
4704
+ polyX[2]; // .c;
4705
+ //alert("a "+a+" "+b+" "+c);
4706
+ // var disc = b * b - 4 * a * c;
4707
+ var disc = polyX[1] * polyX[1] - 4 * polyX[0] * polyX[2];
4708
+ // var polyX = CubicBezierCurve.utils.bezierCoeffs(p3,p2,p1,p0);
4709
+ // var a = polyX[0]; //polyX.a;
4710
+ // var b = polyX[1]; // .b;
4711
+ // var c = polyX[2]; //.c;
4712
+ // var disc = polyX[1] * polyX[1] - 4 * polyX[0] * polyX[2];
4713
+ // var xl = Math.min(bCurve.endPoint.x, bCurve.startPoint.x); // P[0].X;
4714
+ // var xh = Math.max(bCurve.endPoint.x, bCurve.startPoint.x); // P[0].X;
4715
+ var xl = Math.min(p3, p0); // P[0].X;
4716
+ var xh = Math.max(p3, p0); // P[0].X;
4717
+ // if (P[3].X < xl) xl = P[3].X;
4718
+ // if (P[3].X > xh) xh = P[3].X;
4719
+ if (disc >= 0) {
4720
+ var t1 = (-b + Math.sqrt(disc)) / (2 * a);
4721
+ // alert("t1 " + t1);
4722
+ if (t1 > 0 && t1 < 1) {
4723
+ // var x1 = evalBez(PX, t1);
4724
+ // var x1 = bCurve.getPointAt(t1).x;
4725
+ var x1 = CubicBezierCurve.utils.evaluateT(p0, p1, p2, p3, t1); // bCurve.getPointAt(t1).x;
4726
+ if (x1 < xl)
4727
+ xl = x1;
4728
+ if (x1 > xh)
4729
+ xh = x1;
4730
+ }
4731
+ var t2 = (-b - Math.sqrt(disc)) / (2 * a);
4732
+ // alert("t2 " + t2);
4733
+ if (t2 > 0 && t2 < 1) {
4734
+ // var x2 = evalBez(PX, t2);
4735
+ // var x2 = bCurve.getPointAt(t2).x;
4736
+ var x2 = CubicBezierCurve.utils.evaluateT(p0, p1, p2, p3, t2); //
4737
+ if (x2 < xl)
4738
+ xl = x2;
4739
+ if (x2 > xh)
4740
+ xh = x2;
4741
+ }
4742
+ }
4743
+ return { min: xl, max: xh };
4744
+ },
4088
4745
  /**
4089
4746
  * Get the points of a sub curve at the given start end end offsets (values between 0.0 and 1.0).
4090
4747
  *
@@ -4108,6 +4765,94 @@ CubicBezierCurve.utils = {
4108
4765
  startVec.scale(0.33333333 * (tEnd - tStart));
4109
4766
  endVec.scale(0.33333333 * (tEnd - tStart));
4110
4767
  return [startVec.a, endVec.a, startVec.b, endVec.b];
4768
+ },
4769
+ /**
4770
+ * Compute the cubic roots for the given cubic polynomial coefficients.
4771
+ *
4772
+ * Based on
4773
+ * http://mysite.verizon.net/res148h4j/javascript/script_exact_cubic.html#the%20source%20code
4774
+ * Inspired by
4775
+ * https://www.particleincell.com/2013/cubic-line-intersection/
4776
+ * Thanks to Stephan Schmitt and Particle-In-Cell!
4777
+ *
4778
+ * @param poly
4779
+ * @returns
4780
+ */
4781
+ cubicRoots: (poly) => {
4782
+ const a = poly[0];
4783
+ const b = poly[1];
4784
+ const c = poly[2];
4785
+ const d = poly[3];
4786
+ const A = b / a;
4787
+ const B = c / a;
4788
+ const C = d / a;
4789
+ var S, T, Im;
4790
+ const Q = (3 * B - Math.pow(A, 2)) / 9;
4791
+ const R = (9 * A * B - 27 * C - 2 * Math.pow(A, 3)) / 54;
4792
+ const D = Math.pow(Q, 3) + Math.pow(R, 2); // polynomial discriminant
4793
+ const ts = []; // Array();
4794
+ if (D >= 0) {
4795
+ // complex or duplicate roots
4796
+ S = CubicBezierCurve.utils.sgn(R + Math.sqrt(D)) * Math.pow(Math.abs(R + Math.sqrt(D)), 1 / 3);
4797
+ T = CubicBezierCurve.utils.sgn(R - Math.sqrt(D)) * Math.pow(Math.abs(R - Math.sqrt(D)), 1 / 3);
4798
+ ts[0] = -A / 3 + (S + T); // real root
4799
+ ts[1] = -A / 3 - (S + T) / 2; // real part of complex root
4800
+ ts[2] = -A / 3 - (S + T) / 2; // real part of complex root
4801
+ Im = Math.abs((Math.sqrt(3) * (S - T)) / 2); // complex part of root pair
4802
+ // Mark complex roots to be discarded
4803
+ if (Im != 0) {
4804
+ ts[1] = -1;
4805
+ ts[2] = -1;
4806
+ }
4807
+ } // distinct real roots
4808
+ else {
4809
+ const th = Math.acos(R / Math.sqrt(-Math.pow(Q, 3)));
4810
+ ts[0] = 2 * Math.sqrt(-Q) * Math.cos(th / 3) - A / 3;
4811
+ ts[1] = 2 * Math.sqrt(-Q) * Math.cos((th + 2 * Math.PI) / 3) - A / 3;
4812
+ ts[2] = 2 * Math.sqrt(-Q) * Math.cos((th + 4 * Math.PI) / 3) - A / 3;
4813
+ Im = 0.0;
4814
+ }
4815
+ // Discard all t's out of spec and sort the rest
4816
+ return ts.filter((t) => t >= 0 && t <= 1.0).sort();
4817
+ },
4818
+ /**
4819
+ * Compute the Bézier coefficients from the given Bézier point coordinates.
4820
+ *
4821
+ * @param {number} p0 - The start point coordinate.
4822
+ * @param {number} p1 - The start control point coordinate.
4823
+ * @param {number} p2 - The end control point coordinate.
4824
+ * @param {number} p3 - The end point coordinate.
4825
+ * @returns {[number,number,number,number]}
4826
+ */
4827
+ bezierCoeffs: (p0, p1, p2, p3) => {
4828
+ const coeffs = [NaN, NaN, NaN, NaN]; //Array(4);
4829
+ coeffs[0] = -p0 + 3 * p1 + -3 * p2 + p3;
4830
+ coeffs[1] = 3 * p0 - 6 * p1 + 3 * p2;
4831
+ coeffs[2] = -3 * p0 + 3 * p1;
4832
+ coeffs[3] = p0;
4833
+ return coeffs;
4834
+ },
4835
+ /**
4836
+ * Calculate the cubic polynomial coefficients used to find the bounding box.
4837
+ *
4838
+ * @param {number} p0 - The start point coordinate.
4839
+ * @param {number} p1 - The start control point coordinate.
4840
+ * @param {number} p2 - The end control point coordinate.
4841
+ * @param {number} p3 - The end point coordinate.
4842
+ * @returns {[number,number,number]}
4843
+ */
4844
+ cubicPoly: (p0, p1, p2, p3) => {
4845
+ return [
4846
+ 3 * p3 - 9 * p2 + 9 * p1 - 3 * p0,
4847
+ 6 * p0 - 12 * p1 + 6 * p2,
4848
+ 3 * p1 - 3 * p0
4849
+ ];
4850
+ },
4851
+ /**
4852
+ * sign of number, but is division safe: no zero returned :)
4853
+ */
4854
+ sgn(x) {
4855
+ return x < 0.0 ? -1 : 1;
4111
4856
  }
4112
4857
  };
4113
4858
 
@@ -4138,7 +4883,10 @@ CubicBezierCurve.utils = {
4138
4883
  * @modified 2022-02-02 Cleared the `toSVGString` function (deprecated). Use `drawutilssvg` instead.
4139
4884
  * @modified 2023-10-06 Adding the `BezierPath.toPathPoints()` method.
4140
4885
  * @modified 2023-10-07 Adding the `BezierPath.fromCurve(CubicBezierCurve)` static function.
4141
- * @version 2.6.0
4886
+ * @modified 2025-04-09 Added the `BezierPath.move` method to match the convention – which just calls `translate`.
4887
+ * @modified 2025-04-09 Modified the `BezierPath.translate` method: chaning parameter `Vertex` to more generalized `XYCoords`.
4888
+ * @modified 2025-04-14 Class `BezierPath` is now implementing interface `Intersectable`.
4889
+ * @version 2.7.0
4142
4890
  *
4143
4891
  * @file BezierPath
4144
4892
  * @public
@@ -4343,7 +5091,7 @@ class BezierPath {
4343
5091
  * Move the whole bezier path by the given (x,y)-amount.
4344
5092
  *
4345
5093
  * @method translate
4346
- * @param {Vertex} amount - The amount to be added (amount.x and amount.y)
5094
+ * @param {XYCoords} amount - The amount to be added (amount.x and amount.y)
4347
5095
  * to each vertex of the curve.
4348
5096
  * @instance
4349
5097
  * @memberof BezierPath
@@ -4362,6 +5110,19 @@ class BezierPath {
4362
5110
  this.updateArcLengths();
4363
5111
  return this;
4364
5112
  }
5113
+ /**
5114
+ * Move the whole bezier path by the given (x,y)-amount.
5115
+ *
5116
+ * @method move
5117
+ * @param {XYCoords} amount - The amount to be added (amount.x and amount.y)
5118
+ * to each vertex of the curve.
5119
+ * @instance
5120
+ * @memberof BezierPath
5121
+ * @return {BezierPath} this for chaining
5122
+ **/
5123
+ move(amount) {
5124
+ return this.translate(amount);
5125
+ }
4365
5126
  /**
4366
5127
  * Scale the whole bezier path by the given uniform factor.
4367
5128
  *
@@ -4572,6 +5333,36 @@ class BezierPath {
4572
5333
  var relativeU = u - uResult.uPart;
4573
5334
  return bCurve.getPerpendicular(relativeU);
4574
5335
  }
5336
+ //--- BEGIN --- Implement interface `Intersectable`
5337
+ /**
5338
+ * Get all line intersections with this shape.
5339
+ *
5340
+ * This method returns all intersections (as vertices) with this shape. The returned array of vertices is in no specific order.
5341
+ *
5342
+ * @param {VertTuple} line - The line to find intersections with.
5343
+ * @param {boolean} inVectorBoundsOnly - If set to true only intersecion points on the passed vector are returned (located strictly between start and end vertex).
5344
+ * @returns {Array<Vertex>} - An array of all intersections with the shape's outline.
5345
+ */
5346
+ lineIntersections(line, inVectorBoundsOnly = false) {
5347
+ return this.bezierCurves.reduce((accu, curCurve) => {
5348
+ return accu.concat(curCurve.lineIntersections(line, inVectorBoundsOnly));
5349
+ }, []);
5350
+ }
5351
+ /**
5352
+ * Get all line intersections of this polygon and their tangents along the shape.
5353
+ *
5354
+ * This method returns all intersection tangents (as vectors) with this shape. The returned array of vectors is in no specific order.
5355
+ *
5356
+ * @param line
5357
+ * @param lineIntersectionTangents
5358
+ * @returns
5359
+ */
5360
+ lineIntersectionTangents(line, inVectorBoundsOnly = false) {
5361
+ return this.bezierCurves.reduce((accu, curCurve) => {
5362
+ return accu.concat(curCurve.lineIntersectionTangents(line, inVectorBoundsOnly));
5363
+ }, []);
5364
+ }
5365
+ //--- END --- Implement interface `Intersectable`
4575
5366
  /**
4576
5367
  * This is a helper function to locate the curve index for a given
4577
5368
  * absolute path position u.
@@ -4785,6 +5576,7 @@ class BezierPath {
4785
5576
  }
4786
5577
  neighbourCurve.updateArcLengths();
4787
5578
  }
5579
+ //--- BEGIN --- Implement interface `IBounded`
4788
5580
  /**
4789
5581
  * Get the bounds of this Bézier path.
4790
5582
  *
@@ -4806,6 +5598,7 @@ class BezierPath {
4806
5598
  }
4807
5599
  return new Bounds(min, max);
4808
5600
  }
5601
+ //--- END --- Implement interface `IBounded`
4809
5602
  /**
4810
5603
  * Get n 'equally' distributed vertices along this Bézier path.
4811
5604
  *
@@ -5189,6 +5982,10 @@ BezierPath.END_POINT = 3;
5189
5982
  * @modified 2024-03-08 Added the `containsAngle` method.
5190
5983
  * @modified 2024-03-09 Added the `circleSectorIntersection` method to find coherent sector intersections..
5191
5984
  * @modified 2024-03-09 Added the `angleAt` method to determine any angle at some ratio.
5985
+ * @modified 2025-04-02 Adding the `CircleSector.lineIntersections` and `CircleSector.lineIntersectionTangents` and implementing `Intersectable`.
5986
+ * @modified 2025-04-09 Adding the `CircleSector.move()` method.
5987
+ * @modified 2025-04-19 Tweaking the `CircleSector.containsAngle` method: all values (input angle, start- and end- angle) are wrapped into [0,2*PI) now.
5988
+ * @modified 2025-04-19 Class `CircleSector` implements interface `Bounded` now (method `getBounds` added).
5192
5989
  * @version 1.2.0
5193
5990
  **/
5194
5991
  /**
@@ -5220,6 +6017,49 @@ class CircleSector {
5220
6017
  this.startAngle = startAngle;
5221
6018
  this.endAngle = endAngle;
5222
6019
  }
6020
+ //--- BEGIN --- Implement interface `IBounded`
6021
+ /**
6022
+ * Get the bounds of this ellipse.
6023
+ *
6024
+ * The bounds are approximated by the underlying segment buffer; the more segment there are,
6025
+ * the more accurate will be the returned bounds.
6026
+ *
6027
+ * @method getBounds
6028
+ * @instance
6029
+ * @memberof VEllipse
6030
+ * @return {Bounds} The bounds of this curve.
6031
+ **/
6032
+ getBounds() {
6033
+ const _self = this;
6034
+ const circleBounds = this.circle.getBounds();
6035
+ // Calculage angles from east, west, north and south box points and check if they are inside
6036
+ const candidates = [
6037
+ circleBounds.getNorthPoint(),
6038
+ circleBounds.getSouthPoint(),
6039
+ circleBounds.getWestPoint(),
6040
+ circleBounds.getEastPoint()
6041
+ ].filter((point) => {
6042
+ // Check for each candidate points if they are contained in this sector. Drop if not.
6043
+ const angle = new Line(_self.circle.center, point).angle();
6044
+ return _self.containsAngle(angle);
6045
+ });
6046
+ // Compute bounds and inlcude start end end point (they are definitely part of the bounds)
6047
+ return Bounds.computeFromVertices(candidates.concat([this.getStartPoint(), this.getEndPoint()]));
6048
+ }
6049
+ //--- BEGIN --- Implement interface `IBounded`
6050
+ /**
6051
+ * Move the circle sector by the given amount.
6052
+ *
6053
+ * @method move
6054
+ * @param {XYCoords} amount - The amount to move.
6055
+ * @instance
6056
+ * @memberof CircleSector
6057
+ * @return {CircleSector} this for chaining
6058
+ **/
6059
+ move(amount) {
6060
+ this.circle.move(amount);
6061
+ return this;
6062
+ }
5223
6063
  /**
5224
6064
  * Checks wether the given angle (must be inside 0 and PI*2) is contained inside this sector.
5225
6065
  *
@@ -5230,12 +6070,22 @@ class CircleSector {
5230
6070
  * @return {boolean} True if (and only if) this sector contains the given angle.
5231
6071
  */
5232
6072
  containsAngle(angle) {
5233
- if (this.startAngle <= this.endAngle) {
5234
- return angle >= this.startAngle && angle < this.endAngle;
6073
+ var wrappedAngle = geomutils.mapAngleTo2PI(angle);
6074
+ var wrappedStart = geomutils.mapAngleTo2PI(this.startAngle);
6075
+ var wrappedEnd = geomutils.mapAngleTo2PI(this.endAngle);
6076
+ // TODO: cleanup
6077
+ // if (this.startAngle <= this.endAngle) {
6078
+ // return angle >= this.startAngle && angle < this.endAngle;
6079
+ // } else {
6080
+ // // startAngle > endAngle
6081
+ // return angle >= this.startAngle || angle < this.endAngle;
6082
+ // }
6083
+ if (wrappedStart <= wrappedEnd) {
6084
+ return wrappedAngle >= wrappedStart && wrappedAngle < wrappedEnd;
5235
6085
  }
5236
6086
  else {
5237
6087
  // startAngle > endAngle
5238
- return angle >= this.startAngle || angle < this.endAngle;
6088
+ return wrappedAngle >= wrappedStart || wrappedAngle < wrappedEnd;
5239
6089
  }
5240
6090
  }
5241
6091
  /**
@@ -5329,6 +6179,50 @@ class CircleSector {
5329
6179
  }
5330
6180
  return resultSector;
5331
6181
  }
6182
+ //--- BEGIN --- Implement interface `Intersectable`
6183
+ /**
6184
+ * Get the line intersections as vectors with this ellipse.
6185
+ *
6186
+ * @method lineIntersections
6187
+ * @instance
6188
+ * @param {VertTuple<Vector> ray - The line/ray to intersect this ellipse with.
6189
+ * @param {boolean} inVectorBoundsOnly - (default=false) Set to true if only intersections within the vector bounds are of interest.
6190
+ * @returns
6191
+ */
6192
+ lineIntersections(ray, inVectorBoundsOnly = false) {
6193
+ // First get all line intersections from underlying ellipse.
6194
+ const ellipseIntersections = this.circle.lineIntersections(ray, inVectorBoundsOnly);
6195
+ // Drop all intersection points that are not contained in the circle sectors bounds.
6196
+ const tmpLine = new Line(this.circle.center, new Vertex());
6197
+ return ellipseIntersections.filter((intersectionPoint) => {
6198
+ tmpLine.b.set(intersectionPoint);
6199
+ const lineAngle = tmpLine.angle();
6200
+ return this.containsAngle(geomutils.wrapMinMax(lineAngle, 0, Math.PI * 2));
6201
+ });
6202
+ }
6203
+ /**
6204
+ * Get all line intersections of this polygon and their tangents along the shape.
6205
+ *
6206
+ * This method returns all intersection tangents (as vectors) with this shape. The returned array of vectors is in no specific order.
6207
+ *
6208
+ * @param line
6209
+ * @param lineIntersectionTangents
6210
+ * @returns
6211
+ */
6212
+ lineIntersectionTangents(line, inVectorBoundsOnly = false) {
6213
+ // Find the intersections of all lines plus their tangents inside the circle bounds
6214
+ const interSectionPoints = this.lineIntersections(line, inVectorBoundsOnly);
6215
+ return interSectionPoints.map((vert) => {
6216
+ // Calculate angle
6217
+ const lineFromCenter = new Line(this.circle.center, vert);
6218
+ const angle = lineFromCenter.angle();
6219
+ // console.log("angle", (angle / Math.PI) * 180.0);
6220
+ // const angle = Math.random() * Math.PI * 2; // TODO
6221
+ // Calculate tangent at angle
6222
+ return this.circle.tangentAt(angle);
6223
+ });
6224
+ }
6225
+ //--- END --- Implement interface `Intersectable`
5332
6226
  /**
5333
6227
  * This function should invalidate any installed listeners and invalidate this object.
5334
6228
  * After calling this function the object might not hold valid data any more and
@@ -10445,7 +11339,11 @@ class AlloyFinger {
10445
11339
  * @modified 2021-03-19 Added the `VEllipse.rotate` function.
10446
11340
  * @modified 2022-02-02 Added the `destroy` method.
10447
11341
  * @modified 2022-02-02 Cleared the `VEllipse.toSVGString` function (deprecated). Use `drawutilssvg` instead.
10448
- * @version 1.3.0
11342
+ * @modified 2025-03-31 ATTENTION: modified the winding direction of the `tangentAt` method to match with the Circle method. This is a breaking change!
11343
+ * @modified 2025-03-31 Adding the `VEllipse.move(amount: XYCoords)` method.
11344
+ * @modified 2025-04-19 Adding the `VEllipse.getBounds()` method.
11345
+ * @modified 2025-04-24 Adding the `VEllipse.getExtremePoints()` method for calculating minima and maxima.
11346
+ * @version 1.4.0
10449
11347
  *
10450
11348
  * @file VEllipse
10451
11349
  * @fileoverview Ellipses with a center and an x- and a y-axis (stored as a vertex).
@@ -10531,11 +11429,96 @@ class VEllipse {
10531
11429
  * @return {number} The signed vertical radius of this ellipse.
10532
11430
  */
10533
11431
  signedRadiusV() {
10534
- // return Math.abs(this.axis.y - this.center.y);
10535
11432
  // Rotate axis back to origin before calculating radius
10536
- // return Math.abs(new Vertex(this.axis).rotate(-this.rotation,this.center).y - this.center.y);
10537
11433
  return new Vertex(this.axis).rotate(-this.rotation, this.center).y - this.center.y;
10538
11434
  }
11435
+ /**
11436
+ * Get the the minima and maxima (points) of this (rotated) ellipse.
11437
+ *
11438
+ * @method getExtremePoints
11439
+ * @instance
11440
+ * @memberof VEllipse
11441
+ * @return {[Vertex, Vertex, Vertex, Vertex]} Get the the minima and maxima (points) of this (rotated) ellipse.
11442
+ */
11443
+ getExtremePoints() {
11444
+ const a = this.radiusH();
11445
+ const b = this.radiusV();
11446
+ // Calculate t_x values
11447
+ const t_x1 = Math.atan2(-b * Math.sin(this.rotation), a * Math.cos(this.rotation));
11448
+ const t_x2 = t_x1 + Math.PI;
11449
+ // Calculate x values at t_x
11450
+ const x_x1 = this.center.x + a * Math.cos(t_x1) * Math.cos(this.rotation) - b * Math.sin(t_x1) * Math.sin(this.rotation);
11451
+ const y_x1 = this.center.y + a * Math.cos(t_x1) * Math.sin(this.rotation) + b * Math.sin(t_x1) * Math.cos(this.rotation);
11452
+ const x_x2 = this.center.x + a * Math.cos(t_x2) * Math.cos(this.rotation) - b * Math.sin(t_x2) * Math.sin(this.rotation);
11453
+ const y_x2 = this.center.y + a * Math.cos(t_x2) * Math.sin(this.rotation) + b * Math.sin(t_x2) * Math.cos(this.rotation);
11454
+ let x_max, x_min;
11455
+ if (x_x1 > x_x2) {
11456
+ x_max = new Vertex(x_x1, y_x1);
11457
+ x_min = new Vertex(x_x2, y_x2);
11458
+ }
11459
+ else {
11460
+ x_max = new Vertex(x_x2, y_x2);
11461
+ x_min = new Vertex(x_x1, y_x1);
11462
+ }
11463
+ // Calculate t_y values
11464
+ const t_y1 = Math.atan2(b * Math.cos(this.rotation), a * Math.sin(this.rotation));
11465
+ const t_y2 = t_y1 + Math.PI;
11466
+ // Calculate y values at t_y
11467
+ const x_y1 = this.center.x + a * Math.cos(t_y1) * Math.cos(this.rotation) - b * Math.sin(t_y1) * Math.sin(this.rotation);
11468
+ const y_y1 = this.center.y + a * Math.cos(t_y1) * Math.sin(this.rotation) + b * Math.sin(t_y1) * Math.cos(this.rotation);
11469
+ const x_y2 = this.center.x + a * Math.cos(t_y2) * Math.cos(this.rotation) - b * Math.sin(t_y2) * Math.sin(this.rotation);
11470
+ const y_y2 = this.center.y + a * Math.cos(t_y2) * Math.sin(this.rotation) + b * Math.sin(t_y2) * Math.cos(this.rotation);
11471
+ let y_max, y_min;
11472
+ if (y_y1 > y_y2) {
11473
+ y_max = new Vertex(x_y1, y_y1);
11474
+ y_min = new Vertex(x_y2, y_y2);
11475
+ }
11476
+ else {
11477
+ y_max = new Vertex(x_y2, y_y2);
11478
+ y_min = new Vertex(x_y1, y_y1);
11479
+ }
11480
+ return [x_max, x_min, y_max, y_min];
11481
+ }
11482
+ //--- BEGIN --- Implement interface `IBounded`
11483
+ /**
11484
+ * Get the bounds of this ellipse.
11485
+ *
11486
+ * The bounds are approximated by the underlying segment buffer; the more segment there are,
11487
+ * the more accurate will be the returned bounds.
11488
+ *
11489
+ * @method getBounds
11490
+ * @instance
11491
+ * @memberof VEllipse
11492
+ * @return {Bounds} The bounds of this ellipse.
11493
+ **/
11494
+ getBounds() {
11495
+ // Thanks to Cuixiping
11496
+ // https://stackoverflow.com/questions/87734/how-do-you-calculate-the-axis-aligned-bounding-box-of-an-ellipse
11497
+ const r1 = this.radiusH();
11498
+ const r2 = this.radiusV();
11499
+ const ux = r1 * Math.cos(this.rotation);
11500
+ const uy = r1 * Math.sin(this.rotation);
11501
+ const vx = r2 * Math.cos(this.rotation + Math.PI / 2);
11502
+ const vy = r2 * Math.sin(this.rotation + Math.PI / 2);
11503
+ const bbox_halfwidth = Math.sqrt(ux * ux + vx * vx);
11504
+ const bbox_halfheight = Math.sqrt(uy * uy + vy * vy);
11505
+ return new Bounds({ x: this.center.x - bbox_halfwidth, y: this.center.y - bbox_halfheight }, { x: this.center.x + bbox_halfwidth, y: this.center.y + bbox_halfheight });
11506
+ }
11507
+ //--- BEGIN --- Implement interface `IBounded`
11508
+ /**
11509
+ * Move the ellipse by the given amount. This is equivalent by moving the `center` and `axis` points.
11510
+ *
11511
+ * @method move
11512
+ * @param {XYCoords} amount - The amount to move.
11513
+ * @instance
11514
+ * @memberof VEllipse
11515
+ * @return {VEllipse} this for chaining
11516
+ **/
11517
+ move(amount) {
11518
+ this.center.add(amount);
11519
+ this.axis.add(amount);
11520
+ return this;
11521
+ }
10539
11522
  /**
10540
11523
  * Scale this ellipse by the given factor from the center point. The factor will be applied to both radii.
10541
11524
  *
@@ -10593,7 +11576,7 @@ class VEllipse {
10593
11576
  * @param {number=1.0} length - [optional, default=1] The length of the returned vector.
10594
11577
  */
10595
11578
  normalAt(angle, length) {
10596
- const point = this.vertAt(angle);
11579
+ const point = this.vertAt(angle - this.rotation); // HERE IS THE CORRECT BEHAVIOR!
10597
11580
  const foci = this.getFoci();
10598
11581
  // Calculate the angle between [point,focusA] and [point,focusB]
10599
11582
  const angleA = new Line(point, foci[0]).angle();
@@ -10605,12 +11588,13 @@ class VEllipse {
10605
11588
  .addX(50)
10606
11589
  .clone()
10607
11590
  .rotate(Math.PI + centerAngle, point);
10608
- if (this.center.distance(endPointA) < this.center.distance(endPointB)) {
10609
- return new Vector(point, endPointB);
10610
- }
10611
- else {
10612
- return new Vector(point, endPointA);
11591
+ const resultVector = this.center.distance(endPointA) < this.center.distance(endPointB)
11592
+ ? new Vector(point, endPointB)
11593
+ : new Vector(point, endPointA);
11594
+ if (typeof length === "number") {
11595
+ resultVector.setLength(length);
10613
11596
  }
11597
+ return resultVector;
10614
11598
  }
10615
11599
  /**
10616
11600
  * Get the tangent vector at the given angle.
@@ -10629,9 +11613,7 @@ class VEllipse {
10629
11613
  */
10630
11614
  tangentAt(angle, length) {
10631
11615
  const normal = this.normalAt(angle, length);
10632
- // Rotate the normal by 90 degrees, then it is the tangent.
10633
- normal.b.rotate(Math.PI / 2, normal.a);
10634
- return normal;
11616
+ return normal.inv().perp();
10635
11617
  }
10636
11618
  /**
10637
11619
  * Get the perimeter of this ellipse.
@@ -10682,6 +11664,8 @@ class VEllipse {
10682
11664
  /**
10683
11665
  * Get equally distributed points on the outline of this ellipse.
10684
11666
  *
11667
+ * @method getEquidistantVertices
11668
+ * @instance
10685
11669
  * @param {number} pointCount - The number of points.
10686
11670
  * @returns {Array<Vertex>}
10687
11671
  */
@@ -10693,6 +11677,75 @@ class VEllipse {
10693
11677
  }
10694
11678
  return result;
10695
11679
  }
11680
+ //--- BEGIN --- Implement interface `Intersectable`
11681
+ /**
11682
+ * Get the line intersections as vectors with this ellipse.
11683
+ *
11684
+ * @method lineIntersections
11685
+ * @instance
11686
+ * @param {VertTuple<Vector> ray - The line/ray to intersect this ellipse with.
11687
+ * @param {boolean} inVectorBoundsOnly - (default=false) Set to true if only intersections within the vector bounds are of interest.
11688
+ * @returns
11689
+ */
11690
+ lineIntersections(ray, inVectorBoundsOnly = false) {
11691
+ // Question: what happens to extreme versions when ellipse is a line (width or height is zero)?
11692
+ // This would result in a Division_by_Zero exception!
11693
+ // Step A: create clones for operations (keep originals unchanged)
11694
+ const ellipseCopy = this.clone(); // VEllipse
11695
+ const rayCopy = ray.clone(); // Vector
11696
+ // Step B: move both so ellipse's center is located at (0,0)
11697
+ const moveAmount = ellipseCopy.center.clone().inv();
11698
+ ellipseCopy.move(moveAmount);
11699
+ rayCopy.add(moveAmount);
11700
+ // Step C: rotate eclipse backwards it's rotation, so that rotation is zero (0.0).
11701
+ // Rotate together with ray!
11702
+ const rotationAmount = -ellipseCopy.rotation;
11703
+ ellipseCopy.rotate(rotationAmount); // Rotation around (0,0) = center of translated ellipse
11704
+ rayCopy.a.rotate(rotationAmount, ellipseCopy.center);
11705
+ rayCopy.b.rotate(rotationAmount, ellipseCopy.center);
11706
+ // Step D: find x/y factors to use for scaling to transform the ellipse to a circle.
11707
+ // Scale together with vector ray.
11708
+ const radiusH = ellipseCopy.radiusH();
11709
+ const radiusV = ellipseCopy.radiusV();
11710
+ const scalingFactors = radiusH > radiusV ? { x: radiusV / radiusH, y: 1.0 } : { x: 1.0, y: radiusH / radiusV };
11711
+ // Step E: scale ellipse AND ray by calculated factors.
11712
+ ellipseCopy.axis.scaleXY(scalingFactors);
11713
+ rayCopy.a.scaleXY(scalingFactors);
11714
+ rayCopy.b.scaleXY(scalingFactors);
11715
+ // Intermediate result: now the ellipse is transformed to a circle and we can calculate intersections :)
11716
+ // Step F: calculate circle+line intersecions
11717
+ const tmpCircle = new Circle(new Vertex(), ellipseCopy.radiusH()); // radiusH() === radiusV()
11718
+ const intersections = tmpCircle.lineIntersections(rayCopy, inVectorBoundsOnly);
11719
+ // Step G: transform intersecions back to original configuration
11720
+ intersections.forEach(function (intersectionPoint) {
11721
+ // Reverse transformation from above.
11722
+ intersectionPoint.scaleXY({ x: 1 / scalingFactors.x, y: 1 / scalingFactors.y }, ellipseCopy.center);
11723
+ intersectionPoint.rotate(-rotationAmount, ellipseCopy.center);
11724
+ intersectionPoint.sub(moveAmount);
11725
+ });
11726
+ return intersections;
11727
+ }
11728
+ /**
11729
+ * Get all line intersections of this polygon and their tangents along the shape.
11730
+ *
11731
+ * This method returns all intersection tangents (as vectors) with this shape. The returned array of vectors is in no specific order.
11732
+ *
11733
+ * @param line
11734
+ * @param lineIntersectionTangents
11735
+ * @returns
11736
+ */
11737
+ lineIntersectionTangents(line, inVectorBoundsOnly = false) {
11738
+ // Find the intersections of all lines plus their tangents inside the circle bounds
11739
+ const interSectionPoints = this.lineIntersections(line, inVectorBoundsOnly);
11740
+ return interSectionPoints.map((vert) => {
11741
+ // Calculate angle
11742
+ const lineFromCenter = new Line(this.center, vert);
11743
+ const angle = lineFromCenter.angle();
11744
+ // Calculate tangent at angle
11745
+ return this.tangentAt(angle);
11746
+ });
11747
+ }
11748
+ //--- END --- Implement interface `Intersectable`
10696
11749
  /**
10697
11750
  * Convert this ellipse into cubic Bézier curves.
10698
11751
  *
@@ -10705,7 +11758,7 @@ class VEllipse {
10705
11758
  // Math by Luc Maisonobe
10706
11759
  // http://www.spaceroots.org/documents/ellipse/node22.html
10707
11760
  // Note that ellipses with radiusH=0 or radiusV=0 cannot be represented as Bézier curves.
10708
- // Return a single line here (as a Bézier curve)
11761
+ // Return a single line here (as a Bézier curve)?
10709
11762
  // if (Math.abs(this.radiusV()) < 0.00001) {
10710
11763
  // const radiusH = this.radiusH();
10711
11764
  // return [
@@ -10735,10 +11788,10 @@ class VEllipse {
10735
11788
  const radiusV = this.radiusV();
10736
11789
  const curves = [];
10737
11790
  const angles = VEllipse.utils.equidistantVertAngles(radiusH, radiusV, segmentCount);
10738
- let curAngle = angles[0];
11791
+ let curAngle = angles[0] + this.rotation;
10739
11792
  let startPoint = this.vertAt(curAngle);
10740
11793
  for (var i = 0; i < angles.length; i++) {
10741
- let nextAngle = angles[(i + 1) % angles.length];
11794
+ let nextAngle = angles[(i + 1) % angles.length] + this.rotation;
10742
11795
  let endPoint = this.vertAt(nextAngle);
10743
11796
  if (Math.abs(radiusV) < 0.0001 || Math.abs(radiusH) < 0.0001) {
10744
11797
  // Distorted ellipses can only be approximated by linear Bézier segments
@@ -10747,8 +11800,8 @@ class VEllipse {
10747
11800
  curves.push(curve);
10748
11801
  }
10749
11802
  else {
10750
- let startTangent = this.tangentAt(curAngle);
10751
- let endTangent = this.tangentAt(nextAngle);
11803
+ let startTangent = this.tangentAt(curAngle + this.rotation);
11804
+ let endTangent = this.tangentAt(nextAngle + this.rotation);
10752
11805
  // Find intersection (ignore that the result might be null in some extreme cases)
10753
11806
  let intersection = startTangent.intersection(endTangent);
10754
11807
  // What if intersection is undefined?
@@ -10840,7 +11893,14 @@ VEllipse.utils = {
10840
11893
  * @date 2021-02-26
10841
11894
  * @modified 2022-02-02 Added the `destroy` method.
10842
11895
  * @modified 2022-11-01 Tweaked the `endpointToCenterParameters` function to handle negative values, too, without errors.
10843
- * @version 1.1.1
11896
+ * @modified 2025-04-01 Adapting a the `toCubicBezier` calculation to match an underlying change in the vertAt and tangentAt calculation of ellipses (was required to hamonize both methods with circles).
11897
+ * @modified 2025-04-02 Adding `VEllipseSector.containsAngle` method.
11898
+ * @modified 2025-04-02 Adding `VEllipseSector.lineIntersections` and `VEllipseSector.lineIntersectionTangents` and implementing `Intersectable`.
11899
+ * @modified 2025-04-07 Adding value wrapping (0 to TWO_PI) to the `VEllipseSector.containsAngle` method.
11900
+ * @modified 2025-04-09 Adding the `VEllipseSector.move` method.
11901
+ * @modified 2025-04-19 Added the `VEllipseSector.getStartPoint` and `getEndPoint` methods.
11902
+ * @modified 2025-04-23 Added the `VEllipseSector.getBounds` method.
11903
+ * @version 1.2.0
10844
11904
  */
10845
11905
  /**
10846
11906
  * @classdesc A class for elliptic sectors.
@@ -10876,6 +11936,137 @@ class VEllipseSector {
10876
11936
  this.startAngle = geomutils.wrapMinMax(startAngle, 0, Math.PI * 2);
10877
11937
  this.endAngle = geomutils.wrapMinMax(endAngle, 0, Math.PI * 2);
10878
11938
  }
11939
+ /**
11940
+ * Move the ellipse sector by the given amount.
11941
+ *
11942
+ * @method move
11943
+ * @param {XYCoords} amount - The amount to move.
11944
+ * @instance
11945
+ * @memberof VEllipseSector
11946
+ * @return {VEllipseSector} this for chaining
11947
+ **/
11948
+ move(amount) {
11949
+ this.ellipse.move(amount);
11950
+ return this;
11951
+ }
11952
+ /**
11953
+ * Checks wether the given angle (must be inside 0 and PI*2) is contained inside this sector.
11954
+ *
11955
+ * @param {number} angle - The numeric angle to check.
11956
+ * @method containsAngle
11957
+ * @instance
11958
+ * @memberof VEllipseSectpr
11959
+ * @return {boolean} True if (and only if) this sector contains the given angle.
11960
+ */
11961
+ containsAngle(angle) {
11962
+ angle = geomutils.mapAngleTo2PI(angle); // wrapMinMax(angle, 0, Math.PI * 2);
11963
+ const sAngle = geomutils.mapAngleTo2PI(this.startAngle);
11964
+ const eAngle = geomutils.mapAngleTo2PI(this.endAngle);
11965
+ // TODO: cleanup
11966
+ // if (this.startAngle <= this.endAngle) {
11967
+ // return angle >= this.startAngle && angle < this.endAngle;
11968
+ // } else {
11969
+ // // startAngle > endAngle
11970
+ // return angle >= this.startAngle || angle < this.endAngle;
11971
+ // }
11972
+ if (sAngle <= eAngle) {
11973
+ return angle >= sAngle && angle < eAngle;
11974
+ }
11975
+ else {
11976
+ // startAngle > endAngle
11977
+ return angle >= sAngle || angle < eAngle;
11978
+ }
11979
+ }
11980
+ /**
11981
+ * Get the sectors starting point (on the underlying ellipse, located at the start angle).
11982
+ *
11983
+ * @method getStartPoint
11984
+ * @instance
11985
+ * @memberof VEllipseSector
11986
+ * @return {Vertex} The sector's stating point.
11987
+ */
11988
+ getStartPoint() {
11989
+ return this.ellipse.vertAt(this.startAngle);
11990
+ }
11991
+ /**
11992
+ * Get the sectors ending point (on the underlying ellipse, located at the end angle).
11993
+ *
11994
+ * @method getEndPoint
11995
+ * @instance
11996
+ * @memberof VEllipseSector
11997
+ * @return {Vertex} The sector's ending point.
11998
+ */
11999
+ getEndPoint() {
12000
+ return this.ellipse.vertAt(this.endAngle);
12001
+ }
12002
+ //--- BEGIN --- Implement interface `IBounded`
12003
+ /**
12004
+ * Get the bounds of this elliptic sector.
12005
+ *
12006
+ * The bounds are approximated by the underlying segment buffer; the more segment there are,
12007
+ * the more accurate will be the returned bounds.
12008
+ *
12009
+ * @method getBounds
12010
+ * @instance
12011
+ * @memberof VEllipse
12012
+ * @return {Bounds} The bounds of this elliptic sector.
12013
+ **/
12014
+ getBounds() {
12015
+ // Calculage angles from east, west, north and south box points and check if they are inside
12016
+ const extremes = this.ellipse.getExtremePoints();
12017
+ const candidates = extremes.filter(point => {
12018
+ const angle = new Line(this.ellipse.center, point).angle() - this.ellipse.rotation;
12019
+ return this.containsAngle(angle);
12020
+ });
12021
+ return Bounds.computeFromVertices([this.getStartPoint(), this.getEndPoint()].concat(candidates));
12022
+ }
12023
+ //--- BEGIN --- Implement interface `Intersectable`
12024
+ /**
12025
+ * Get the line intersections as vectors with this ellipse.
12026
+ *
12027
+ * @method lineIntersections
12028
+ * @instance
12029
+ * @memberof VEllipseSectpr
12030
+ * @param {VertTuple<Vector>} ray - The line/ray to intersect this ellipse with.
12031
+ * @param {boolean} inVectorBoundsOnly - (default=false) Set to true if only intersections within the vector bounds are of interest.
12032
+ * @returns
12033
+ */
12034
+ lineIntersections(ray, inVectorBoundsOnly = false) {
12035
+ // First get all line intersections from underlying ellipse.
12036
+ const ellipseIntersections = this.ellipse.lineIntersections(ray, inVectorBoundsOnly);
12037
+ // Drop all intersection points that are not contained in the circle sectors bounds.
12038
+ const tmpLine = new Line(this.ellipse.center, new Vertex());
12039
+ return ellipseIntersections.filter((intersectionPoint) => {
12040
+ tmpLine.b.set(intersectionPoint);
12041
+ const lineAngle = tmpLine.angle();
12042
+ return this.containsAngle(lineAngle - this.ellipse.rotation);
12043
+ });
12044
+ }
12045
+ /**
12046
+ * Get all line intersections of this polygon and their tangents along the shape.
12047
+ *
12048
+ * This method returns all intersection tangents (as vectors) with this shape. The returned array of vectors is in no specific order.
12049
+ *
12050
+ * @method lineIntersections
12051
+ * @memberof VEllipseSectpr
12052
+ * @param line
12053
+ * @param lineIntersectionTangents
12054
+ * @returns
12055
+ */
12056
+ lineIntersectionTangents(line, inVectorBoundsOnly = false) {
12057
+ // Find the intersections of all lines plus their tangents inside the circle bounds
12058
+ const interSectionPoints = this.lineIntersections(line, inVectorBoundsOnly);
12059
+ return interSectionPoints.map((vert) => {
12060
+ // Calculate angle
12061
+ const lineFromCenter = new Line(this.ellipse.center, vert);
12062
+ const angle = lineFromCenter.angle();
12063
+ // console.log("angle", (angle / Math.PI) * 180.0);
12064
+ // const angle = Math.random() * Math.PI * 2; // TODO
12065
+ // Calculate tangent at angle
12066
+ return this.ellipse.tangentAt(angle);
12067
+ });
12068
+ }
12069
+ //--- END --- Implement interface `Intersectable`
10879
12070
  /**
10880
12071
  * Convert this elliptic sector into cubic Bézier curves.
10881
12072
  *
@@ -10903,8 +12094,8 @@ class VEllipseSector {
10903
12094
  for (var i = 0; i + 1 < angles.length; i++) {
10904
12095
  let nextAngle = angles[(i + 1) % angles.length];
10905
12096
  let endPoint = this.ellipse.vertAt(nextAngle);
10906
- let startTangent = this.ellipse.tangentAt(curAngle);
10907
- let endTangent = this.ellipse.tangentAt(nextAngle);
12097
+ let startTangent = this.ellipse.tangentAt(curAngle + this.ellipse.rotation);
12098
+ let endTangent = this.ellipse.tangentAt(nextAngle + this.ellipse.rotation);
10908
12099
  // Distorted ellipses can only be approximated by linear Bézier segments
10909
12100
  if (Math.abs(radiusV) < 0.0001 || Math.abs(radiusH) < 0.0001) {
10910
12101
  let diff = startPoint.difference(endPoint);
@@ -11008,20 +12199,20 @@ VEllipseSector.ellipseSectorUtils = {
11008
12199
  equidistantVertAngles: (radiusH, radiusV, startAngle, endAngle, fullEllipsePointCount) => {
11009
12200
  var ellipseAngles = VEllipse.utils.equidistantVertAngles(radiusH, radiusV, fullEllipsePointCount);
11010
12201
  ellipseAngles = ellipseAngles.map((angle) => VEllipseSector.ellipseSectorUtils.normalizeAngle(angle));
11011
- var angleIsInRange = (angle) => {
12202
+ const angleIsInRange = (angle) => {
11012
12203
  if (startAngle < endAngle)
11013
12204
  return angle >= startAngle && angle <= endAngle;
11014
12205
  else
11015
12206
  return angle >= startAngle || (angle <= endAngle && angle >= 0);
11016
12207
  };
11017
12208
  // Drop all angles outside the sector
11018
- var ellipseAngles = ellipseAngles.filter(angleIsInRange);
12209
+ ellipseAngles = ellipseAngles.filter(angleIsInRange);
11019
12210
  // Now we need to sort the angles to the first one in the array is the closest to startAngle.
11020
12211
  // --> find the angle that is closest to the start angle
11021
- var startIndex = VEllipseSector.ellipseSectorUtils.findClosestToStartAngle(startAngle, endAngle, ellipseAngles);
12212
+ const startIndex = VEllipseSector.ellipseSectorUtils.findClosestToStartAngle(startAngle, endAngle, ellipseAngles);
11022
12213
  // Bring all angles into the correct order
11023
12214
  // Idea: use splice or slice here?
11024
- var angles = [];
12215
+ const angles = [];
11025
12216
  for (var i = 0; i < ellipseAngles.length; i++) {
11026
12217
  angles.push(ellipseAngles[(startIndex + i) % ellipseAngles.length]);
11027
12218
  }
@@ -11193,8 +12384,13 @@ VEllipseSector.ellipseSectorUtils = {
11193
12384
  * @modified 2024-07-08 Adding `PlotBoilerplate.getGUI()` to retrieve the GUI instance.
11194
12385
  * @modified 2024-08-25 Extending main class `PlotBoilerplate` optional param `isBackdropFiltersEnabled`.
11195
12386
  * @modified 2024-12-02 Adding the `triggerRedraw` to the `removeAll` method.
12387
+ * @modified 2025-05-07 Changing the return type of `removeVertex` from `void` to `boolean`.
12388
+ * @modified 2025-05-07 Handling content changes now with `contentChangeListeners`.
12389
+ * @modified 2025-05-07 Added `PlogBoilerplate.addContentChangeListener` and `.removeContentChangeListener`.
12390
+ * @modified 2025-05-07 Moving full vectors now by default when vector point a is moved.
12391
+ * @modified 2025-05-20 Applying `lineWith` parameter in the draw routine for vectors (had been missing).
11196
12392
  *
11197
- * @version 1.20.0
12393
+ * @version 1.21.1
11198
12394
  *
11199
12395
  * @file PlotBoilerplate
11200
12396
  * @fileoverview The main class.
@@ -11297,6 +12493,10 @@ class PlotBoilerplate {
11297
12493
  */
11298
12494
  constructor(config, drawConfig) {
11299
12495
  var _b, _c;
12496
+ /**
12497
+ * A list of content change listeners.
12498
+ */
12499
+ this.contentChangeListeners = [];
11300
12500
  /**
11301
12501
  * A discrete timestamp to identify single render cycles.
11302
12502
  * Note that using system time milliseconds is not a safe way to identify render frames, as on modern powerful machines
@@ -11684,6 +12884,52 @@ class PlotBoilerplate {
11684
12884
  PlotBoilerplate.utils.setCSSscale(this.canvas, (_d = this.config.cssScaleX) !== null && _d !== void 0 ? _d : 1.0, (_e = this.config.cssScaleY) !== null && _e !== void 0 ? _e : 1.0);
11685
12885
  }
11686
12886
  }
12887
+ /**
12888
+ * Adds a new content change listener to this instance. Adding duplicates is not possible.
12889
+ *
12890
+ * @param {PBContentChangeListener} listener - The listenre to add.
12891
+ * @method addContentChangeListener
12892
+ * @instance
12893
+ * @memberof PlotBoilerplate
12894
+ * @returns {void}
12895
+ */
12896
+ addContentChangeListener(listener) {
12897
+ for (var i in this.contentChangeListeners) {
12898
+ if (this.contentChangeListeners[i] === listener) {
12899
+ return;
12900
+ }
12901
+ }
12902
+ this.contentChangeListeners.push(listener);
12903
+ }
12904
+ /**
12905
+ * Removes an existing content change listener from this instance.
12906
+ *
12907
+ * @param {PBContentChangeListener} listener - The listenre to add.
12908
+ * @method removeContentChangeListener
12909
+ * @instance
12910
+ * @memberof PlotBoilerplate
12911
+ * @returns {void}
12912
+ */
12913
+ removeContentChangeListener(listener) {
12914
+ for (var i = 0; i < this.contentChangeListeners.length; i++) {
12915
+ if (this.contentChangeListeners[i] === listener) {
12916
+ this.contentChangeListeners.splice(i, 1);
12917
+ return;
12918
+ }
12919
+ }
12920
+ }
12921
+ _fireContentChanged(addedDrawables, removedDrawables) {
12922
+ for (var i in this.contentChangeListeners) {
12923
+ const listener = this.contentChangeListeners[i];
12924
+ if (listener && typeof listener === "function") {
12925
+ listener({
12926
+ type: addedDrawables.length > 0 ? "DRAWABLES_ADDED" : "DRAWABLES_REMOVED",
12927
+ addedDrawables: addedDrawables,
12928
+ removedDrawables: removedDrawables
12929
+ });
12930
+ }
12931
+ }
12932
+ }
11687
12933
  /**
11688
12934
  * Add a drawable object.<br>
11689
12935
  * <br>
@@ -11708,100 +12954,121 @@ class PlotBoilerplate {
11708
12954
  * @memberof PlotBoilerplate
11709
12955
  * @return {void}
11710
12956
  **/
11711
- add(drawable, redraw) {
12957
+ add(drawable, redraw, doNotFireEvent) {
11712
12958
  if (Array.isArray(drawable)) {
11713
12959
  const arr = drawable;
11714
12960
  for (var i = 0; i < arr.length; i++) {
11715
- this.add(arr[i], false);
12961
+ this.add(arr[i], false, doNotFireEvent);
11716
12962
  }
12963
+ // !doNotFireEvent && this._fireContentChanged(arr, []);
11717
12964
  }
11718
- else if (drawable instanceof Vertex) {
11719
- this.drawables.push(drawable);
11720
- this.vertices.push(drawable);
11721
- }
11722
- else if (drawable instanceof Line) {
11723
- // Add some lines
11724
- this.drawables.push(drawable);
11725
- this.vertices.push(drawable.a);
11726
- this.vertices.push(drawable.b);
11727
- }
11728
- else if (drawable instanceof Vector) {
11729
- this.drawables.push(drawable);
11730
- this.vertices.push(drawable.a);
11731
- this.vertices.push(drawable.b);
11732
- }
11733
- else if (drawable instanceof VEllipse) {
11734
- this.vertices.push(drawable.center);
11735
- this.vertices.push(drawable.axis);
11736
- this.drawables.push(drawable);
11737
- drawable.center.listeners.addDragListener((event) => {
11738
- drawable.axis.add(event.params.dragAmount);
11739
- });
11740
- }
11741
- else if (drawable instanceof VEllipseSector) {
11742
- this.vertices.push(drawable.ellipse.center);
11743
- this.vertices.push(drawable.ellipse.axis);
11744
- this.drawables.push(drawable);
11745
- drawable.ellipse.center.listeners.addDragListener((event) => {
11746
- drawable.ellipse.axis.add(event.params.dragAmount);
11747
- });
11748
- }
11749
- else if (drawable instanceof Circle) {
11750
- this.vertices.push(drawable.center);
11751
- this.drawables.push(drawable);
11752
- }
11753
- else if (drawable instanceof CircleSector) {
11754
- this.vertices.push(drawable.circle.center);
11755
- this.drawables.push(drawable);
11756
- }
11757
- else if (drawable instanceof Polygon) {
11758
- this.drawables.push(drawable);
11759
- for (var i = 0; i < drawable.vertices.length; i++) {
11760
- this.vertices.push(drawable.vertices[i]);
12965
+ else {
12966
+ const addedDrawables = [drawable];
12967
+ if (drawable instanceof Vertex) {
12968
+ this.drawables.push(drawable);
12969
+ this.vertices.push(drawable);
11761
12970
  }
11762
- }
11763
- else if (drawable instanceof Triangle) {
11764
- this.drawables.push(drawable);
11765
- this.vertices.push(drawable.a);
11766
- this.vertices.push(drawable.b);
11767
- this.vertices.push(drawable.c);
11768
- }
11769
- else if (drawable instanceof BezierPath) {
11770
- this.drawables.push(drawable);
11771
- const bezierPath = drawable;
11772
- for (var i = 0; i < bezierPath.bezierCurves.length; i++) {
11773
- if (!drawable.adjustCircular && i == 0) {
11774
- this.vertices.push(bezierPath.bezierCurves[i].startPoint);
12971
+ else if (drawable instanceof Line) {
12972
+ // Add some lines
12973
+ this.drawables.push(drawable);
12974
+ this.vertices.push(drawable.a);
12975
+ this.vertices.push(drawable.b);
12976
+ addedDrawables.push(drawable.a, drawable.b);
12977
+ }
12978
+ else if (drawable instanceof Vector) {
12979
+ this.drawables.push(drawable);
12980
+ this.vertices.push(drawable.a);
12981
+ this.vertices.push(drawable.b);
12982
+ addedDrawables.push(drawable.a, drawable.b);
12983
+ drawable.a.listeners.addDragListener((event) => {
12984
+ drawable.b.add(event.params.dragAmount);
12985
+ });
12986
+ }
12987
+ else if (drawable instanceof VEllipse) {
12988
+ this.vertices.push(drawable.center);
12989
+ this.vertices.push(drawable.axis);
12990
+ addedDrawables.push(drawable.center, drawable.axis);
12991
+ this.drawables.push(drawable);
12992
+ drawable.center.listeners.addDragListener((event) => {
12993
+ drawable.axis.add(event.params.dragAmount);
12994
+ });
12995
+ }
12996
+ else if (drawable instanceof VEllipseSector) {
12997
+ this.vertices.push(drawable.ellipse.center);
12998
+ this.vertices.push(drawable.ellipse.axis);
12999
+ addedDrawables.push(drawable.ellipse.center, drawable.ellipse.axis);
13000
+ this.drawables.push(drawable);
13001
+ drawable.ellipse.center.listeners.addDragListener((event) => {
13002
+ drawable.ellipse.axis.add(event.params.dragAmount);
13003
+ });
13004
+ }
13005
+ else if (drawable instanceof Circle) {
13006
+ this.vertices.push(drawable.center);
13007
+ addedDrawables.push(drawable.center);
13008
+ this.drawables.push(drawable);
13009
+ }
13010
+ else if (drawable instanceof CircleSector) {
13011
+ this.vertices.push(drawable.circle.center);
13012
+ addedDrawables.push(drawable.circle.center);
13013
+ this.drawables.push(drawable);
13014
+ }
13015
+ else if (drawable instanceof Polygon) {
13016
+ this.drawables.push(drawable);
13017
+ for (var i = 0; i < drawable.vertices.length; i++) {
13018
+ this.vertices.push(drawable.vertices[i]);
13019
+ addedDrawables.push(drawable.vertices[i]);
11775
13020
  }
11776
- this.vertices.push(bezierPath.bezierCurves[i].endPoint);
11777
- this.vertices.push(bezierPath.bezierCurves[i].startControlPoint);
11778
- this.vertices.push(bezierPath.bezierCurves[i].endControlPoint);
11779
- bezierPath.bezierCurves[i].startControlPoint.attr.selectable = false;
11780
- bezierPath.bezierCurves[i].endControlPoint.attr.selectable = false;
11781
13021
  }
11782
- PlotBoilerplate.utils.enableBezierPathAutoAdjust(drawable);
11783
- }
11784
- else if (drawable instanceof PBImage) {
11785
- this.vertices.push(drawable.upperLeft);
11786
- this.vertices.push(drawable.lowerRight);
11787
- this.drawables.push(drawable);
11788
- // Todo: think about a IDragEvent interface
11789
- drawable.upperLeft.listeners.addDragListener((e) => {
11790
- drawable.lowerRight.add(e.params.dragAmount);
11791
- });
11792
- drawable.lowerRight.attr.selectable = false;
11793
- }
11794
- else if (drawable instanceof PBText) {
11795
- this.vertices.push(drawable.anchor);
11796
- this.drawables.push(drawable);
11797
- drawable.anchor.attr.selectable = false;
11798
- }
11799
- else {
11800
- throw "Cannot add drawable of unrecognized type: " + typeof drawable + ".";
13022
+ else if (drawable instanceof Triangle) {
13023
+ this.drawables.push(drawable);
13024
+ this.vertices.push(drawable.a);
13025
+ this.vertices.push(drawable.b);
13026
+ this.vertices.push(drawable.c);
13027
+ addedDrawables.push(drawable.a, drawable.b, drawable.c);
13028
+ }
13029
+ else if (drawable instanceof BezierPath) {
13030
+ this.drawables.push(drawable);
13031
+ const bezierPath = drawable;
13032
+ for (var i = 0; i < bezierPath.bezierCurves.length; i++) {
13033
+ if (!drawable.adjustCircular && i == 0) {
13034
+ this.vertices.push(bezierPath.bezierCurves[i].startPoint);
13035
+ addedDrawables.push(bezierPath.bezierCurves[i].startPoint);
13036
+ }
13037
+ this.vertices.push(bezierPath.bezierCurves[i].endPoint);
13038
+ this.vertices.push(bezierPath.bezierCurves[i].startControlPoint);
13039
+ this.vertices.push(bezierPath.bezierCurves[i].endControlPoint);
13040
+ addedDrawables.push(bezierPath.bezierCurves[i].endPoint, bezierPath.bezierCurves[i].startControlPoint, bezierPath.bezierCurves[i].endControlPoint);
13041
+ bezierPath.bezierCurves[i].startControlPoint.attr.selectable = false;
13042
+ bezierPath.bezierCurves[i].endControlPoint.attr.selectable = false;
13043
+ }
13044
+ PlotBoilerplate.utils.enableBezierPathAutoAdjust(drawable);
13045
+ }
13046
+ else if (drawable instanceof PBImage) {
13047
+ this.vertices.push(drawable.upperLeft);
13048
+ this.vertices.push(drawable.lowerRight);
13049
+ addedDrawables.push(drawable.upperLeft, drawable.lowerRight);
13050
+ this.drawables.push(drawable);
13051
+ // Todo: think about a IDragEvent interface
13052
+ drawable.upperLeft.listeners.addDragListener((e) => {
13053
+ drawable.lowerRight.add(e.params.dragAmount);
13054
+ });
13055
+ drawable.lowerRight.attr.selectable = false;
13056
+ }
13057
+ else if (drawable instanceof PBText) {
13058
+ this.vertices.push(drawable.anchor);
13059
+ addedDrawables.push(drawable.anchor);
13060
+ this.drawables.push(drawable);
13061
+ drawable.anchor.attr.selectable = false;
13062
+ }
13063
+ else {
13064
+ throw "Cannot add drawable of unrecognized type: " + typeof drawable + ".";
13065
+ }
13066
+ !doNotFireEvent && this._fireContentChanged(addedDrawables, []);
11801
13067
  }
11802
13068
  // This is a workaround for backwards compatibility when the 'redraw' param was not yet present.
11803
- if (redraw || typeof redraw == "undefined")
13069
+ if (redraw || typeof redraw == "undefined") {
11804
13070
  this.redraw();
13071
+ }
11805
13072
  }
11806
13073
  /**
11807
13074
  * Remove a drawable object.<br>
@@ -11821,88 +13088,113 @@ class PlotBoilerplate {
11821
13088
  *
11822
13089
  * @param {Drawable|Array<Drawable>} drawable - The drawable (of one of the allowed class instance) to remove.
11823
13090
  * @param {boolean} [redraw=false]
13091
+ * @param {removeWidth}
11824
13092
  * @method remove
11825
13093
  * @instance
11826
13094
  * @memberof PlotBoilerplate
11827
13095
  * @return {void}
11828
13096
  **/
11829
- remove(drawable, redraw, removeWithVertices) {
13097
+ remove(drawable, redraw, removeWithVertices, doNotFireEvent) {
11830
13098
  if (Array.isArray(drawable)) {
13099
+ const removedDrawables = [];
11831
13100
  for (var i = 0; i < drawable.length; i++) {
11832
- this.remove(drawable[i], false, removeWithVertices);
13101
+ if (this.remove(drawable[i], false, removeWithVertices, true)) {
13102
+ removedDrawables.push(drawable[i]);
13103
+ }
11833
13104
  }
11834
13105
  if (redraw) {
11835
13106
  this.redraw();
11836
13107
  }
11837
- return;
13108
+ !doNotFireEvent && this._fireContentChanged([], removedDrawables);
13109
+ return removedDrawables.length > 0;
11838
13110
  }
11839
13111
  if (drawable instanceof Vertex) {
11840
- this.removeVertex(drawable, false);
13112
+ const wasRemoved = this.removeVertex(drawable, false, false);
11841
13113
  if (redraw) {
11842
13114
  this.redraw();
11843
13115
  }
13116
+ !doNotFireEvent && this._fireContentChanged([], [drawable]);
13117
+ return wasRemoved;
11844
13118
  }
13119
+ let wasRemoved = false;
13120
+ const removedDrawables = [];
11845
13121
  for (var i = 0; i < this.drawables.length; i++) {
11846
13122
  if (this.drawables[i] === drawable || this.drawables[i].uid === drawable.uid) {
11847
13123
  this.drawables.splice(i, 1);
13124
+ removedDrawables.push(drawable);
11848
13125
  if (removeWithVertices) {
11849
13126
  // Check if some listeners need to be removed
11850
13127
  if (drawable instanceof Line) {
11851
13128
  // Add some lines
11852
- this.removeVertex(drawable.a, false);
11853
- this.removeVertex(drawable.b, false);
13129
+ this.removeVertex(drawable.a, false, true);
13130
+ this.removeVertex(drawable.b, false, true);
13131
+ removedDrawables.push(drawable.a, drawable.b);
11854
13132
  }
11855
13133
  else if (drawable instanceof Vector) {
11856
- this.removeVertex(drawable.a, false);
11857
- this.removeVertex(drawable.b, false);
13134
+ this.removeVertex(drawable.a, false, true);
13135
+ this.removeVertex(drawable.b, false, true);
13136
+ removedDrawables.push(drawable.a, drawable.b);
11858
13137
  }
11859
13138
  else if (drawable instanceof VEllipse) {
11860
- this.removeVertex(drawable.center, false);
11861
- this.removeVertex(drawable.axis, false);
13139
+ this.removeVertex(drawable.center, false, true);
13140
+ this.removeVertex(drawable.axis, false, true);
13141
+ removedDrawables.push(drawable.center, drawable.axis);
11862
13142
  }
11863
13143
  else if (drawable instanceof VEllipseSector) {
11864
- this.removeVertex(drawable.ellipse.center);
11865
- this.removeVertex(drawable.ellipse.axis);
13144
+ this.removeVertex(drawable.ellipse.center, false, true);
13145
+ this.removeVertex(drawable.ellipse.axis, false, true);
13146
+ removedDrawables.push(drawable.ellipse.center, drawable.ellipse.axis);
11866
13147
  }
11867
13148
  else if (drawable instanceof Circle) {
11868
- this.removeVertex(drawable.center, false);
13149
+ this.removeVertex(drawable.center, false, true);
11869
13150
  }
11870
13151
  else if (drawable instanceof CircleSector) {
11871
- this.removeVertex(drawable.circle.center, false);
13152
+ this.removeVertex(drawable.circle.center, false, true);
13153
+ removedDrawables.push(drawable.circle.center);
11872
13154
  }
11873
13155
  else if (drawable instanceof Polygon) {
11874
13156
  // for( var i in drawable.vertices )
11875
- for (var i = 0; i < drawable.vertices.length; i++)
11876
- this.removeVertex(drawable.vertices[i], false);
13157
+ for (var i = 0; i < drawable.vertices.length; i++) {
13158
+ this.removeVertex(drawable.vertices[i], false, true);
13159
+ removedDrawables.push(drawable.vertices[i]);
13160
+ }
11877
13161
  }
11878
13162
  else if (drawable instanceof Triangle) {
11879
- this.removeVertex(drawable.a, false);
11880
- this.removeVertex(drawable.b, false);
11881
- this.removeVertex(drawable.c, false);
13163
+ this.removeVertex(drawable.a, false, true);
13164
+ this.removeVertex(drawable.b, false, true);
13165
+ this.removeVertex(drawable.c, false, true);
13166
+ removedDrawables.push(drawable.a, drawable.b, drawable.c);
11882
13167
  }
11883
13168
  else if (drawable instanceof BezierPath) {
11884
13169
  for (var i = 0; i < drawable.bezierCurves.length; i++) {
11885
- this.removeVertex(drawable.bezierCurves[i].startPoint, false);
11886
- this.removeVertex(drawable.bezierCurves[i].startControlPoint, false);
11887
- this.removeVertex(drawable.bezierCurves[i].endControlPoint, false);
13170
+ this.removeVertex(drawable.bezierCurves[i].startPoint, false, true);
13171
+ this.removeVertex(drawable.bezierCurves[i].startControlPoint, false, true);
13172
+ this.removeVertex(drawable.bezierCurves[i].endControlPoint, false, true);
13173
+ removedDrawables.push(drawable.bezierCurves[i].startPoint, drawable.bezierCurves[i].startControlPoint, drawable.bezierCurves[i].endControlPoint);
11888
13174
  if (i + 1 == drawable.bezierCurves.length) {
11889
- this.removeVertex(drawable.bezierCurves[i].endPoint, false);
13175
+ this.removeVertex(drawable.bezierCurves[i].endPoint, false, true);
13176
+ removedDrawables.push(drawable.bezierCurves[i].endPoint);
11890
13177
  }
11891
13178
  }
11892
13179
  }
11893
13180
  else if (drawable instanceof PBImage) {
11894
- this.removeVertex(drawable.upperLeft, false);
11895
- this.removeVertex(drawable.lowerRight, false);
13181
+ this.removeVertex(drawable.upperLeft, false, true);
13182
+ this.removeVertex(drawable.lowerRight, false, true);
13183
+ removedDrawables.push(drawable.upperLeft, drawable.lowerRight);
11896
13184
  }
11897
13185
  else if (drawable instanceof PBText) {
11898
- this.removeVertex(drawable.anchor, false);
13186
+ this.removeVertex(drawable.anchor, false, true);
13187
+ removedDrawables.push(drawable.anchor);
11899
13188
  }
11900
13189
  } // END removeWithVertices
11901
13190
  if (redraw) {
11902
13191
  this.redraw();
11903
13192
  }
11904
- }
11905
- }
13193
+ !doNotFireEvent && this._fireContentChanged([], removedDrawables);
13194
+ wasRemoved = true;
13195
+ } // END if
13196
+ } // END for
13197
+ return wasRemoved;
11906
13198
  }
11907
13199
  /**
11908
13200
  * Remove a vertex from the vertex list.<br>
@@ -11912,18 +13204,20 @@ class PlotBoilerplate {
11912
13204
  * @method removeVertex
11913
13205
  * @instance
11914
13206
  * @memberof PlotBoilerplate
11915
- * @return {void}
13207
+ * @return {boolean}
11916
13208
  **/
11917
- removeVertex(vert, redraw) {
13209
+ removeVertex(vert, redraw, doNotFireEvent) {
11918
13210
  for (var i = 0; i < this.vertices.length; i++) {
11919
13211
  if (this.vertices[i] === vert) {
11920
13212
  this.vertices.splice(i, 1);
11921
13213
  if (redraw) {
11922
13214
  this.redraw();
11923
13215
  }
11924
- return;
13216
+ !doNotFireEvent && this._fireContentChanged([], [vert]);
13217
+ return true;
11925
13218
  }
11926
13219
  }
13220
+ return false;
11927
13221
  }
11928
13222
  /**
11929
13223
  * Remove all elements.
@@ -11938,13 +13232,16 @@ class PlotBoilerplate {
11938
13232
  * @return {void}
11939
13233
  */
11940
13234
  removeAll(keepVertices, triggerRedraw) {
13235
+ let removedDrawables = this.drawables;
11941
13236
  this.drawables = [];
11942
13237
  if (!Boolean(keepVertices)) {
13238
+ removedDrawables = removedDrawables.concat(this.vertices);
11943
13239
  this.vertices = [];
11944
13240
  }
11945
13241
  if (triggerRedraw || typeof triggerRedraw === "undefined") {
11946
13242
  this.redraw();
11947
13243
  }
13244
+ removedDrawables.length > 0 && this._fireContentChanged([], removedDrawables);
11948
13245
  }
11949
13246
  /**
11950
13247
  * Find the vertex near the given position.
@@ -12149,18 +13446,17 @@ class PlotBoilerplate {
12149
13446
  }
12150
13447
  else if (d instanceof Triangle) {
12151
13448
  draw.polyline([d.a, d.b, d.c], false, this.drawConfig.triangle.color, this.drawConfig.triangle.lineWidth);
12152
- if (!this.drawConfig.drawHandlePoints)
13449
+ if (!this.drawConfig.drawHandlePoints) {
12153
13450
  d.a.attr.renderTime = d.b.attr.renderTime = d.c.attr.renderTime = renderTime;
13451
+ }
12154
13452
  }
12155
13453
  else if (d instanceof VEllipse) {
12156
13454
  if (this.drawConfig.drawHandleLines) {
12157
13455
  draw.setCurrentId(`${d.uid}_e0`);
12158
13456
  draw.setCurrentClassName(`${d.className}-v-line`);
12159
- // draw.line( d.center.clone().add(0,d.axis.y-d.center.y), d.axis, '#c8c8c8' );
12160
13457
  draw.handleLine(d.center.clone().add(0, d.signedRadiusV()).rotate(d.rotation, d.center), d.axis); // , "#c8c8c8");
12161
13458
  draw.setCurrentId(`${d.uid}_e1`);
12162
13459
  draw.setCurrentClassName(`${d.className}-h-line`);
12163
- // draw.line( d.center.clone().add(d.axis.x-d.center.x,0), d.axis, '#c8c8c8' );
12164
13460
  draw.handleLine(d.center.clone().add(d.signedRadiusH(), 0).rotate(d.rotation, d.center), d.axis); // , "#c8c8c8");
12165
13461
  }
12166
13462
  draw.setCurrentId(d.uid);
@@ -12176,12 +13472,6 @@ class PlotBoilerplate {
12176
13472
  else if (d instanceof VEllipseSector) {
12177
13473
  draw.setCurrentId(d.uid);
12178
13474
  draw.setCurrentClassName(`${d.className}`);
12179
- /* draw.ellipse( d.center,
12180
- // Math.abs(d.axis.x-d.center.x), Math.abs(d.axis.y-d.center.y),
12181
- d.radiusH(), d.radiusV(),
12182
- this.drawConfig.ellipse.color,
12183
- this.drawConfig.ellipse.lineWidth,
12184
- d.rotation ); */
12185
13475
  const data = VEllipseSector.ellipseSectorUtils.describeSVGArc(d.ellipse.center.x, d.ellipse.center.y, d.ellipse.radiusH(), d.ellipse.radiusV(), d.startAngle, d.endAngle, d.ellipse.rotation, { moveToStart: true });
12186
13476
  draw.path(data, this.drawConfig.ellipseSector.color, this.drawConfig.ellipseSector.lineWidth);
12187
13477
  }
@@ -12206,7 +13496,7 @@ class PlotBoilerplate {
12206
13496
  d.b.attr.renderTime = renderTime;
12207
13497
  }
12208
13498
  else if (d instanceof Vector) {
12209
- draw.arrow(d.a, d.b, this.drawConfig.vector.color);
13499
+ draw.arrow(d.a, d.b, this.drawConfig.vector.color, this.drawConfig.vector.lineWidth);
12210
13500
  if (this.drawConfig.drawHandlePoints && d.b.attr.selectable && d.b.attr.visible) {
12211
13501
  draw.setCurrentId(`${d.uid}_h0`);
12212
13502
  draw.setCurrentClassName(`${d.className}-handle`);