plotboilerplate 1.23.1 → 1.23.3

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 (136) hide show
  1. package/README.md +1 -1
  2. package/dist/index.esm.js +7055 -6751
  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 +342 -34
  9. package/dist/plotboilerplate.min.js +1 -1
  10. package/package.json +4 -2
  11. package/src/cjs/PlotBoilerplate.d.ts +5 -2
  12. package/src/cjs/PlotBoilerplate.js +10 -4
  13. package/src/cjs/PlotBoilerplate.js.map +1 -1
  14. package/src/cjs/Polygon.d.ts +107 -4
  15. package/src/cjs/Polygon.js +215 -9
  16. package/src/cjs/Polygon.js.map +1 -1
  17. package/src/cjs/Triangle.d.ts +23 -3
  18. package/src/cjs/Triangle.js +15 -2
  19. package/src/cjs/Triangle.js.map +1 -1
  20. package/src/cjs/VertTuple.d.ts +12 -7
  21. package/src/cjs/VertTuple.js +14 -9
  22. package/src/cjs/VertTuple.js.map +1 -1
  23. package/src/cjs/Vertex.d.ts +2 -1
  24. package/src/cjs/Vertex.js +5 -2
  25. package/src/cjs/Vertex.js.map +1 -1
  26. package/src/cjs/draw.d.ts +1 -0
  27. package/src/cjs/draw.js +2 -1
  28. package/src/cjs/draw.js.map +1 -1
  29. package/src/cjs/drawutilssvg.d.ts +6 -1
  30. package/src/cjs/drawutilssvg.js +34 -6
  31. package/src/cjs/drawutilssvg.js.map +1 -1
  32. package/src/cjs/geomutils.d.ts +32 -1
  33. package/src/cjs/geomutils.js +47 -1
  34. package/src/cjs/geomutils.js.map +1 -1
  35. package/src/cjs/interfaces/core.d.ts +4 -2
  36. package/src/cjs/interfaces/core.js +1 -0
  37. package/src/cjs/interfaces/core.js.map +1 -1
  38. package/src/cjs/interfaces/externals.d.ts +30 -0
  39. package/src/cjs/utils/InitializationObserver.js +7 -0
  40. package/src/cjs/utils/InitializationObserver.js.map +1 -1
  41. package/src/cjs/utils/Params.d.ts +5 -3
  42. package/src/cjs/utils/Params.js +7 -3
  43. package/src/cjs/utils/Params.js.map +1 -1
  44. package/src/cjs/utils/algorithms/PolygonInset.d.ts +184 -0
  45. package/src/cjs/utils/algorithms/PolygonInset.js +431 -0
  46. package/src/cjs/utils/algorithms/PolygonInset.js.map +1 -0
  47. package/src/cjs/utils/algorithms/clearDuplicateVertices.d.ts +5 -4
  48. package/src/cjs/utils/algorithms/clearDuplicateVertices.js +6 -3
  49. package/src/cjs/utils/algorithms/clearDuplicateVertices.js.map +1 -1
  50. package/src/cjs/utils/algorithms/findPolygonSelfIntersections.d.ts +2 -1
  51. package/src/cjs/utils/algorithms/findPolygonSelfIntersections.js +3 -3
  52. package/src/cjs/utils/algorithms/findPolygonSelfIntersections.js.map +1 -1
  53. package/src/cjs/utils/algorithms/splitPolygonToNonIntersecting.d.ts +3 -2
  54. package/src/cjs/utils/algorithms/splitPolygonToNonIntersecting.js +11 -9
  55. package/src/cjs/utils/algorithms/splitPolygonToNonIntersecting.js.map +1 -1
  56. package/src/cjs/utils/creategui.js +69 -1
  57. package/src/cjs/utils/detectDarkMode.js.map +1 -1
  58. package/src/cjs/utils/dom/guiSizeToggler.d.ts +2 -1
  59. package/src/cjs/utils/dom/guiSizeToggler.js +5 -3
  60. package/src/cjs/utils/dom/guiSizeToggler.js.map +1 -1
  61. package/src/cjs/utils/gup.d.ts +1 -0
  62. package/src/cjs/utils/gup.js +3 -1
  63. package/src/cjs/utils/gup.js.map +1 -1
  64. package/src/esm/PlotBoilerplate.d.ts +5 -2
  65. package/src/esm/PlotBoilerplate.js +10 -4
  66. package/src/esm/PlotBoilerplate.js.map +1 -1
  67. package/src/esm/Polygon.d.ts +107 -4
  68. package/src/esm/Polygon.js +214 -9
  69. package/src/esm/Polygon.js.map +1 -1
  70. package/src/esm/Triangle.d.ts +23 -3
  71. package/src/esm/Triangle.js +15 -2
  72. package/src/esm/Triangle.js.map +1 -1
  73. package/src/esm/VertTuple.d.ts +12 -7
  74. package/src/esm/VertTuple.js +14 -9
  75. package/src/esm/VertTuple.js.map +1 -1
  76. package/src/esm/Vertex.d.ts +2 -1
  77. package/src/esm/Vertex.js +5 -2
  78. package/src/esm/Vertex.js.map +1 -1
  79. package/src/esm/draw.d.ts +1 -0
  80. package/src/esm/draw.js +2 -1
  81. package/src/esm/draw.js.map +1 -1
  82. package/src/esm/drawutilssvg.d.ts +6 -1
  83. package/src/esm/drawutilssvg.js +34 -6
  84. package/src/esm/drawutilssvg.js.map +1 -1
  85. package/src/esm/geomutils.d.ts +32 -1
  86. package/src/esm/geomutils.js +47 -1
  87. package/src/esm/geomutils.js.map +1 -1
  88. package/src/esm/interfaces/core.d.ts +4 -2
  89. package/src/esm/interfaces/core.js +1 -0
  90. package/src/esm/interfaces/core.js.map +1 -1
  91. package/src/esm/interfaces/externals.d.ts +30 -0
  92. package/src/esm/utils/InitializationObserver.js +7 -0
  93. package/src/esm/utils/InitializationObserver.js.map +1 -1
  94. package/src/esm/utils/Params.d.ts +5 -3
  95. package/src/esm/utils/Params.js +7 -3
  96. package/src/esm/utils/Params.js.map +1 -1
  97. package/src/esm/utils/algorithms/PolygonInset.d.ts +184 -0
  98. package/src/esm/utils/algorithms/PolygonInset.js +424 -0
  99. package/src/esm/utils/algorithms/PolygonInset.js.map +1 -0
  100. package/src/esm/utils/algorithms/clearDuplicateVertices.d.ts +5 -4
  101. package/src/esm/utils/algorithms/clearDuplicateVertices.js +6 -3
  102. package/src/esm/utils/algorithms/clearDuplicateVertices.js.map +1 -1
  103. package/src/esm/utils/algorithms/findPolygonSelfIntersections.d.ts +2 -1
  104. package/src/esm/utils/algorithms/findPolygonSelfIntersections.js +3 -3
  105. package/src/esm/utils/algorithms/findPolygonSelfIntersections.js.map +1 -1
  106. package/src/esm/utils/algorithms/splitPolygonToNonIntersecting.d.ts +3 -2
  107. package/src/esm/utils/algorithms/splitPolygonToNonIntersecting.js +11 -9
  108. package/src/esm/utils/algorithms/splitPolygonToNonIntersecting.js.map +1 -1
  109. package/src/esm/utils/detectDarkMode.js.map +1 -1
  110. package/src/esm/utils/dom/guiSizeToggler.d.ts +2 -1
  111. package/src/esm/utils/dom/guiSizeToggler.js +4 -2
  112. package/src/esm/utils/dom/guiSizeToggler.js.map +1 -1
  113. package/src/esm/utils/gup.d.ts +1 -0
  114. package/src/esm/utils/gup.js +3 -1
  115. package/src/esm/utils/gup.js.map +1 -1
  116. package/src/index-gl.js +3 -1
  117. package/src/index.js +2 -1
  118. package/src/ts/PlotBoilerplate.ts +11 -4
  119. package/src/ts/Polygon.ts +230 -9
  120. package/src/ts/Triangle.ts +18 -4
  121. package/src/ts/VertTuple.ts +15 -10
  122. package/src/ts/Vertex.ts +5 -2
  123. package/src/ts/draw.ts +2 -1
  124. package/src/ts/drawutilssvg.ts +46 -6
  125. package/src/ts/geomutils.ts +49 -1
  126. package/src/ts/interfaces/core.ts +5 -2
  127. package/src/ts/interfaces/externals.ts +34 -0
  128. package/src/ts/utils/InitializationObserver.ts +7 -0
  129. package/src/ts/utils/Params.ts +8 -3
  130. package/src/ts/utils/algorithms/PolygonInset.ts +529 -0
  131. package/src/ts/utils/algorithms/clearDuplicateVertices.ts +11 -8
  132. package/src/ts/utils/algorithms/findPolygonSelfIntersections.ts +25 -26
  133. package/src/ts/utils/algorithms/splitPolygonToNonIntersecting.ts +92 -99
  134. package/src/ts/utils/detectDarkMode.ts +1 -3
  135. package/src/ts/utils/dom/guiSizeToggler.ts +4 -2
  136. package/src/ts/utils/gup.ts +3 -1
@@ -19,6 +19,7 @@
19
19
  * @modified 2022-11-23 Added `drawRaster` to the `Config` interface.
20
20
  * @modified 2023-02-10 All non-function attributes of the `Config` interface are now mandatory.
21
21
  * @modified 2023-09-29 Added the `randomPoint(...)` function declaration to the IBounds interface.
22
+ * @modified 2024-08-25 Added the `CSSBackdropFilterParams` params to the global params (all optional).
22
23
  **/
23
24
 
24
25
  import { Vertex } from "../Vertex";
@@ -35,6 +36,7 @@ import { Line } from "../Line";
35
36
  import { PlotBoilerplate } from "../PlotBoilerplate";
36
37
  import { DrawLib } from "./DrawLib";
37
38
  import { PBText } from "../PBText";
39
+ import { CSSBackdropFilterParams } from "./externals";
38
40
 
39
41
  /**
40
42
  * @classdesc Coordinates (x,y) on the plane.
@@ -113,7 +115,8 @@ export interface CanvasWrapper {
113
115
  * The config that's used by PB.
114
116
  */
115
117
  export interface Config
116
- extends Record<string, boolean | number | string | Function | HTMLCanvasElement | SVGElement | undefined> {
118
+ extends Record<string, boolean | number | string | Function | HTMLCanvasElement | SVGElement | undefined>,
119
+ Pick<CSSBackdropFilterParams, "isBackdropFiltersEnabled"> {
117
120
  canvas: HTMLCanvasElement | SVGElement | string; // Your canvas element in the DOM (required).
118
121
  fullSize: boolean; // If set to true the canvas will gain full window size.
119
122
  fitToParent: boolean; // If set to true the canvas will gain the size of its parent container (overrides fullSize).
@@ -174,7 +177,7 @@ export interface Config
174
177
  /**
175
178
  * For initialization the constructor needs a mix of config and draw-settings.
176
179
  */
177
- export interface PBParams extends Config {
180
+ export interface PBParams extends Config, CSSBackdropFilterParams {
178
181
  title?: string; // Specify any hover tile here. It will be attached as a `title` attribute to the most elevated element.
179
182
  // No more additional attributes, just merge two interfaces.
180
183
  }
@@ -30,3 +30,37 @@ export interface LilGuiProps {
30
30
  closed?: boolean;
31
31
  closeOnTop?: boolean;
32
32
  }
33
+
34
+ /**
35
+ * This is just for the backdrop-filter vaules (CSS after effects filters). They are not directly
36
+ * used by PB but can be passed through by the config/params.
37
+ */
38
+ export interface CSSBackdropFilterParams {
39
+ // A global switch to enable/disable all filters
40
+ isBackdropFiltersEnabled?: boolean;
41
+ // This is just for the global effect color
42
+ effectFilterColor?: string; // "#204a87",
43
+ isEffectsColorEnabled?: boolean;
44
+ // These are real filter values
45
+ opacity?: number; // 0.5,
46
+ isOpacityEnabled?: boolean; // false,
47
+ invert?: number; // 0.8,
48
+ isInvertEnabled?: boolean; // false,
49
+ sepia?: number; // 0.9,
50
+ isSepiaEnabled?: boolean; // false,
51
+ blur?: number; // 2, // px
52
+ isBlurEnabled?: boolean; // false,
53
+ brightness?: number; // 0.6,
54
+ isBrightnessEnabled?: boolean; // false,
55
+ contrast?: number; // 0.9,
56
+ isContrastEnabled?: boolean; // false,
57
+ dropShadow?: number; // 4, // px
58
+ dropShadowColor?: string; // "#00ffff",
59
+ isDropShadowEnabled?: boolean; // false,
60
+ grayscale?: number; // 0.3,
61
+ isGrayscaleEnabled?: boolean; // false,
62
+ hueRotate?: number; // 120, // deg
63
+ isHueRotateEnabled?: boolean; // false,
64
+ saturate?: number; // 2.0,
65
+ isSaturateEnabled?: boolean; // false
66
+ }
@@ -22,6 +22,10 @@ export class InitializationObserver<T, E> {
22
22
  this._timeoutMessage = timeoutMessage ?? null;
23
23
 
24
24
  globalThis.setTimeout(() => {
25
+ if (this._isInitialized) {
26
+ return; // NOOP
27
+ }
28
+ // else
25
29
  if (!this._timeoutMessage) {
26
30
  console.warn("Failed to initialize object. Reason unknown.");
27
31
  }
@@ -30,6 +34,9 @@ export class InitializationObserver<T, E> {
30
34
  }
31
35
 
32
36
  public accept(value: T): void {
37
+ if (this._isInitialized) {
38
+ return;
39
+ }
33
40
  this._isInitialized = true;
34
41
  this._initializedValue = value;
35
42
  this._fireAccept(value);
@@ -2,9 +2,10 @@
2
2
  * Wraps a Record<string,string> and adds type conversion methods (developed this in
3
3
  * some other project) and added this here (2023-10-28).
4
4
  *
5
- * @author Ikars Kappler
6
- * @version 1.0.0
7
- * @date 2023-03-13
5
+ * @author Ikars Kappler
6
+ * @version 1.0.0
7
+ * @date 2023-03-13
8
+ * @modified 2024-08-26 Added the `hasParam` method.
8
9
  */
9
10
 
10
11
  export class Params {
@@ -14,6 +15,10 @@ export class Params {
14
15
  this.baseParams = baseParams;
15
16
  }
16
17
 
18
+ hasParam(name: string): boolean {
19
+ return this.baseParams.hasOwnProperty(name);
20
+ }
21
+
17
22
  getString(name: string, fallback: string): string {
18
23
  let value = this.baseParams[name];
19
24
  if (typeof value === "undefined" || !value || (value = value.trim()).length === 0) {
@@ -0,0 +1,529 @@
1
+ /**
2
+ * Calculate the inset of any non-self-overlapping polygon.
3
+ *
4
+ * The polygon may be
5
+ * - convex or non-convex
6
+ * - self-intersecting
7
+ *
8
+ * The algorithms will
9
+ * - first construct a general offset polygon which may be corrupt due to ouf-of-bounds or in illegal-area problems.
10
+ * - then dissect the possibly self-intersecting inset polygon into separate simple polygons.
11
+ * - then detect for each if it's out of bounds or in some illegal area.
12
+ * - keeping only the valid ones.
13
+ *
14
+ * @required sutherlandHodgman
15
+ * @required splitPolygonToNonIntersecting
16
+ *
17
+ * @author Ikaros Kappler
18
+ * @date 2024-11-04 Ported the script to a class.
19
+ * @modified 2024-12-02 Ported to Typescript.
20
+ */
21
+
22
+ import { Line } from "../../Line";
23
+ import { Polygon } from "../../Polygon";
24
+ import { Vector } from "../../Vector";
25
+ import { Vertex } from "../../Vertex";
26
+ import { XYCoords } from "../../interfaces";
27
+ import { splitPolygonToNonIntersecting } from "./splitPolygonToNonIntersecting";
28
+ import { sutherlandHodgman } from "./sutherlandHodgman";
29
+
30
+ // import { GreinerHorman } from "greiner-hormann-typescript";
31
+ // import DEBUG from "debug";
32
+
33
+ export interface IPolygonInsetOptions {
34
+ innerPolygonOffset: number;
35
+ maxPolygonSplitDepth?: number;
36
+ intersectionEpsilon?: number;
37
+ removeEars?: boolean;
38
+ }
39
+
40
+ export class PolygonInset {
41
+ /**
42
+ * The polygon to work with.
43
+ */
44
+ readonly polygon: Polygon;
45
+
46
+ /**
47
+ * The input polygon, but without ear edges.
48
+ */
49
+ optimizedPolygon: Polygon;
50
+
51
+ /**
52
+ * The original polygon as a sequence of line objects. This is easier to use than a list of points.
53
+ */
54
+ originalPolygonLines: Array<Line> = [];
55
+
56
+ /**
57
+ * The simple inset lines, directly generated by offsetting the original polygon lines by the given amount.
58
+ */
59
+ insetLines: Array<Line> = [];
60
+
61
+ /**
62
+ * The cropped inset lines that makes up the first direct iteration of the inset polygon.
63
+ * This one will probably be self-intersecting.
64
+ */
65
+ insetPolygonLines: Array<Line> = [];
66
+
67
+ /**
68
+ * The inset polygon lines resembling an actual polygon instance (not just a sequence of lines).
69
+ */
70
+ insetPolygon: Polygon;
71
+
72
+ /**
73
+ * Each polygon line and it's offsetted inset-line resemble a rectangular polygon.
74
+ * Array<Polygon> with 4 vertices each
75
+ **/
76
+ insetRectanglePolygons: Array<Polygon>;
77
+
78
+ /**
79
+ * The inset polygon split up into simple non-self-intersecting polygons.
80
+ * Represented as a list of vertex-lists. Each sub-list represents a single polygon.
81
+ */
82
+ splitPolygons: Array<Array<Vertex>> = [];
83
+
84
+ /**
85
+ * The final result: all valid split-polygons. These are making up the actual polygon inset.
86
+ * Note: list may be empty. Depending on the offset amount there is not a guaranteed offset polygon existing.
87
+ */
88
+ filteredSplitPolygons: Array<Array<Vertex>>;
89
+
90
+ /**
91
+ * Constructs a new PolygonInset instance with the underlying given polygon to work with.
92
+ *
93
+ * Please note that the algorithm will reverse the vertex order if the given polygon
94
+ * is not clockwise.
95
+ *
96
+ * @param {Polygon} polygon - The polygon to calculate the offset for.
97
+ */
98
+ constructor(polygon: Polygon) {
99
+ this.polygon = polygon;
100
+ }
101
+
102
+ /**
103
+ * This is the main method.
104
+ *
105
+ * Call this with the required option params to compute your desired offset polygon(s).
106
+ *
107
+ * @param {number} options.innerPolygonOffset - The offset to use. Should be a positive number. Correct result for negative numbers are not guaranteed.
108
+ * @param {number?} options.maxPolygonSplitDepth - (default is the number of polygon.vertices) The maximum amount of recursive stepts to simplify the complex first iteration of the offset polygon.
109
+ * @param {number?} options.intersectionEpsilon - (default is 1.0) The epsilon to use for detecting overlapping polygons. This indicates a tolerance value, because directly adjecent polygons may have a little arithmetic intersection area.
110
+ * @returns {Array<Array<Vertex>>} A list of vertex-lists, each one representing a simple polygon from the offset-polygons.
111
+ */
112
+ public computeOutputPolygons(options: IPolygonInsetOptions): Array<Array<Vertex>> {
113
+ // No offset implies: no change.
114
+ if (options.innerPolygonOffset === 0) {
115
+ return [this.polygon.vertices]; // No change
116
+ }
117
+
118
+ // This algorithm only works with polygon in clockwise vertex order.
119
+ // If the polygon is not clockwise: revert.
120
+ PolygonInset._assertPolygonIsClockwise(this.polygon.vertices);
121
+
122
+ // The potential result polygons might overlap with other parts. These are not part of the desired
123
+ // result set and need to be filtered out.
124
+ // Define an intersection-epsilon: minimal critical overlaps might result of rounding errors and
125
+ // don't represent real overlaps.
126
+ const intersectionEpsilon: number | undefined = options.intersectionEpsilon;
127
+
128
+ // This will create a deep clone
129
+ this.optimizedPolygon = this.polygon.clone();
130
+
131
+ // In case the potential result polygons are self intersecting we need to split them apart later.
132
+ // Using the initial vertex number as the upper split limit should be safe here.
133
+ const maxPolygonSplitDepth = options?.maxPolygonSplitDepth ?? this.optimizedPolygon.vertices.length;
134
+
135
+ // For calculations of initial inset rectagles we need the original polygon's lines.
136
+ this.originalPolygonLines = this.optimizedPolygon.getEdges();
137
+ this._collectInsetLines(this.originalPolygonLines, options.innerPolygonOffset);
138
+ this._collectInsetPolygonLines(this.insetLines);
139
+ this.insetRectanglePolygons = this._collectRectangularPolygonInsets(this.originalPolygonLines, this.insetLines);
140
+
141
+ // Optimize inset polygon AND the insetRectanglePolygons?
142
+ if (options.removeEars) {
143
+ // console.log("Remove ears");
144
+ // This will modify both params!
145
+ PolygonInset._optimizeInsetPolygon(this.optimizedPolygon, this.insetRectanglePolygons);
146
+ }
147
+
148
+ // Create a naive first inset polygon for later optimization.
149
+ this.insetPolygon = PolygonInset.convertToBasicInsetPolygon(this.insetPolygonLines);
150
+
151
+ // // Optimize inset polygon AND the insetRectanglePolygons!
152
+ // if (options.removeEars) {
153
+ // // This will modify both params!
154
+ // PolygonInset._optimizeInsetPolygon(this.insetPolygon, this.insetRectanglePolygons);
155
+ // }
156
+
157
+ // The resulting polygon will likely intersect itself multiple times.
158
+ // For further calculations split apart into non-self-intersecting.
159
+ this.splitPolygons = splitPolygonToNonIntersecting(this.insetPolygon.vertices, maxPolygonSplitDepth, true); // insideBoundsOnly
160
+
161
+ // This method was initially meant to calculate inset-polygons only.
162
+ // But with a simple filter we COULD also create outer offset-polygons.
163
+ // Maybe this is a task for the future
164
+ // console.log("DEBUG", DEBUG);
165
+ // DEBUG("TEST");
166
+ if (options.innerPolygonOffset < 0) {
167
+ // return [this.polygon.vertices]; // No change
168
+ }
169
+
170
+ // console.log("splitPolygons.length", this.splitPolygons.length);
171
+ // Assert all polygons are clockwise!
172
+ PolygonInset._assertAllPolygonsAreClockwise(this.splitPolygons);
173
+
174
+ this.filteredSplitPolygons = PolygonInset._filterInnerSplitPolygonsByCoverage(
175
+ this.splitPolygons,
176
+ this.insetRectanglePolygons,
177
+ intersectionEpsilon
178
+ );
179
+ // console.log("[0] filteredSplitPolygons.length", this.filteredSplitPolygons.length);
180
+ this.filteredSplitPolygons = PolygonInset._filterInnerSplitPolygonsByOriginalBounds(this.filteredSplitPolygons, this.polygon);
181
+ // console.log("[1] filteredSplitPolygons.length", this.filteredSplitPolygons.length);
182
+ return this.filteredSplitPolygons;
183
+ }
184
+
185
+ /**
186
+ * This method transforms each polygon line into a new line
187
+ * by moving it to the inside direction of the polygon (by the given `insetAmount`).
188
+ *
189
+ * @param {Array<Line>} polygonLines
190
+ * @param {number} insetAmount
191
+ * @return {Array<Line>} The transformed lines. The result array has the same length and order as the input array.
192
+ */
193
+ private _collectInsetLines(polygonLines: Array<Line>, insetAmount: number): Array<Line> {
194
+ const insetLines: Array<Line> = []; // Array<Line>
195
+ for (var i = 0; i < polygonLines.length; i++) {
196
+ const line = polygonLines[i];
197
+ const perp = new Vector(line.a, line.b).perp();
198
+ const t = insetAmount / perp.length();
199
+ const offsetOnPerp = perp.vertAt(t);
200
+ const diff = line.a.difference(offsetOnPerp);
201
+ // Polygon is is clockwise order.
202
+ // Move line inside polygon
203
+ const movedLine = line.clone();
204
+ movedLine.a.add(diff);
205
+ movedLine.b.add(diff);
206
+ insetLines.push(movedLine);
207
+ }
208
+ this.insetLines = insetLines;
209
+ return insetLines;
210
+ }
211
+
212
+ /**
213
+ * For a sequence of inset polygon lines get the inset polygon by detecting
214
+ * useful intersections (by cropping or extending them).
215
+ *
216
+ * The returned lines resemble a new polygon.
217
+ *
218
+ * Please note that the returned polygon can be self-intersecting!
219
+ *
220
+ * @param {Array<Line>} insetLines
221
+ * @returns {Array<Line>} The cropped or exented inset polygon lines.
222
+ */
223
+ private _collectInsetPolygonLines(insetLines: Array<Line>): Array<Line> {
224
+ if (insetLines.length <= 1) {
225
+ return [];
226
+ }
227
+ const insetPolygonLines: Array<Line> = [];
228
+ // Collect first intersection at beginning :)
229
+ const lastInsetLine: Line = insetLines[insetLines.length - 1];
230
+ const firstInsetLine: Line = insetLines[0];
231
+ let lastIntersectionPoint = lastInsetLine.intersection(firstInsetLine); // Must not be null
232
+ for (var i = 0; i < insetLines.length; i++) {
233
+ const insetLine: Line = insetLines[i];
234
+
235
+ const nextInsetLine: Line = insetLines[(i + 1) % insetLines.length];
236
+ // Find desired intersection
237
+ const intersection: Vertex | null = insetLine.intersection(nextInsetLine);
238
+ if (intersection == null) {
239
+ console.warn("[collectInsetPolygon] WARN intersection line must not be null", i, nextInsetLine);
240
+ } else {
241
+ // By construction they MUST have any non-null intersection!
242
+ if (lastIntersectionPoint != null) {
243
+ const resultLine: Line = new Line(lastIntersectionPoint, intersection);
244
+ insetPolygonLines.push(resultLine);
245
+ }
246
+ }
247
+ lastIntersectionPoint = intersection;
248
+ }
249
+ // Store intermediate result for later retrieval.
250
+ this.insetPolygonLines = insetPolygonLines;
251
+ return insetPolygonLines;
252
+ }
253
+
254
+ /**
255
+ * Converts two lists (same length) of original polygon lines and inset lines (interpreted as
256
+ * pairs) to a list of rectangular polyons.
257
+ *
258
+ * @static
259
+ * @param {Array<Line>} originalPolygonLines
260
+ * @param {Array<Line>} insetLines
261
+ * @returns {Array<Polygon>} A list of rectangular polygons; each returned polyon has exactly four vertices.
262
+ */
263
+ private _collectRectangularPolygonInsets(originalPolygonLines: Array<Line>, insetLines: Array<Line>): Array<Polygon> {
264
+ // Convert to rectangle polygon
265
+ const insetRectanglePolygons: Array<Polygon> = originalPolygonLines.map((polygonLine, index) => {
266
+ const rectPolygon: Polygon = new Polygon([], false);
267
+ // Add in original order
268
+ rectPolygon.vertices.push(polygonLine.a.clone());
269
+ rectPolygon.vertices.push(polygonLine.b.clone());
270
+ // Add in reverse order
271
+ const insetLine: Line = insetLines[index];
272
+ rectPolygon.vertices.push(insetLine.b.clone());
273
+ rectPolygon.vertices.push(insetLine.a.clone());
274
+ return rectPolygon;
275
+ });
276
+ return insetRectanglePolygons;
277
+ }
278
+
279
+ /**
280
+ * Converts a sequence of (hopefully adjacent) lines to a polygon by using all second line vertices `b`.
281
+ *
282
+ * @static
283
+ * @param insetPolygonLines
284
+ * @returns
285
+ */
286
+ private static convertToBasicInsetPolygon(insetPolygonLines: Array<Line>): Polygon {
287
+ const insetPolygon = new Polygon([], false);
288
+ insetPolygonLines.forEach((insetLine: Line): void => {
289
+ insetPolygon.vertices.push(insetLine.a);
290
+ });
291
+ return insetPolygon;
292
+ }
293
+
294
+ /**
295
+ * Optimize the inset polygon by removing critical `ear` edges.
296
+ * Such an is identified by: the edge itself is completely located inside its neighbours inset rectangle.
297
+ *
298
+ * In this process multiple edges from the ear might be dropped and two adjacent edges get a new
299
+ * common intersection point.
300
+ *
301
+ * Note: this method is not working as expected in all cases and quite experimental.
302
+ *
303
+ * @param insetPolygon
304
+ * @param insetRectangles
305
+ */
306
+ private static _optimizeInsetPolygon = (insetPolygon: Polygon, insetRectangles: Array<Polygon>): void => {
307
+ // Locate edge that can be removed:
308
+ var earEdgeIndex = -1;
309
+ var maxLimit = insetPolygon.vertices.length;
310
+ var i = 0;
311
+ while (
312
+ i++ < maxLimit &&
313
+ insetPolygon.vertices.length > 3 &&
314
+ (earEdgeIndex = PolygonInset._locateExcessiveEarEdge(insetPolygon, insetRectangles)) != -1
315
+ ) {
316
+ // console.log("REMOVE EAR EDGE", earEdgeIndex);
317
+ // As this is an ear edge: don't just remove the vertex.
318
+ // i) Remove the vertex and calculate the new intersection point of neighbour edges.
319
+ // ii) Update neightbour rectangles
320
+ var leftEdge = insetPolygon.getEdgeAt(earEdgeIndex - 2);
321
+ var rightEdge = insetPolygon.getEdgeAt(earEdgeIndex + 1);
322
+ var newIntersection = leftEdge.intersection(rightEdge);
323
+ newIntersection && rightEdge.a.set(newIntersection);
324
+
325
+ // Update rectangles
326
+ // ...
327
+
328
+ insetPolygon.vertices.splice(earEdgeIndex, 1);
329
+ insetRectangles.splice(earEdgeIndex, 1);
330
+ }
331
+ // TODO remove if not -1
332
+ };
333
+
334
+ private static _locateExcessiveEarEdge = (insetPolygon: Polygon, insetRectangles: Array<Polygon>): number => {
335
+ for (var i = 0; i < insetPolygon.vertices.length; i++) {
336
+ const thisEdge = insetPolygon.getEdgeAt(i);
337
+ const leftIndex = i == 0 ? insetPolygon.vertices.length - 1 : i - 1;
338
+ // const rightIndex = i + 1 >= insetPolygon.vertices.length ? 0 : i + 1;
339
+
340
+ const leftRect = insetRectangles[leftIndex];
341
+ // const rightRect = insetRectangles[rightIndex];
342
+
343
+ const leftRectContainsThisEdge = PolygonInset._rectangleFullyContainsLine(leftRect, thisEdge);
344
+
345
+ if (leftRectContainsThisEdge) {
346
+ return i;
347
+ }
348
+
349
+ const leftEdge = insetPolygon.getEdgeAt(leftIndex);
350
+ const thisRect = insetRectangles[i];
351
+
352
+ const thisRectContainsLeftEdge = PolygonInset._rectangleFullyContainsLine(thisRect, leftEdge);
353
+ if (thisRectContainsLeftEdge) {
354
+ return i; // leftIndex;
355
+ }
356
+ }
357
+ return -1;
358
+ };
359
+
360
+ // Pre: rectangle.vertices.length === 4
361
+ private static _rectangleFullyContainsLine = (rectangle: Polygon, edge: Line): boolean => {
362
+ var rectWidth: number = rectangle.getEdgeAt(0).length();
363
+ var rectHeight: number = rectangle.getEdgeAt(1).length();
364
+
365
+ // Both line corners must be at least 1% within the rectangle limits.
366
+ const eps = Math.min(rectWidth, rectHeight) / 100;
367
+ const edgeLen = edge.length();
368
+
369
+ var epsPointA = edge.vertAt(0.01); // edgeLen * eps);
370
+ var epsPointB = edge.vertAt(0.99); // 1.0 - edgeLen * eps);
371
+ var centerPoint = edge.vertAt(0.5);
372
+ return rectangle.containsVerts([epsPointA, epsPointB, centerPoint]);
373
+ };
374
+
375
+ /**
376
+ * Filter split polygons: only keep those whose vertices are all contained inside the original polygon.
377
+ * Reason: scaling too much will result in excessive translation beyond the opposite bounds of the polygon (like more than 200% of possible insetting).
378
+ *
379
+ * @param {Array<Array<Vertex>>} splitPolygonsVertices
380
+ * @param {Polygon} originalPolygon
381
+ * @return {Array<Array<Vertex>>} The filtered polygon list.
382
+ */
383
+ private static _filterInnerSplitPolygonsByOriginalBounds(
384
+ splitPolygonsVertices: Array<Array<Vertex>>,
385
+ originalPolygon: Polygon
386
+ ) {
387
+ return splitPolygonsVertices.filter((splitPolyVerts: Array<Vertex>, _splitPolyIndex: number): boolean => {
388
+ return splitPolyVerts.every((splitPVert: Vertex): boolean => {
389
+ return originalPolygon.containsVert(splitPVert);
390
+ });
391
+ });
392
+ }
393
+
394
+ /**
395
+ * Filter split polygons: only keep those that do not (signifiantly) interset with any rectangles.
396
+ *
397
+ * @static
398
+ * @param {Array<Array<Vertex>>} splitPolygonsVertices
399
+ * @param {Array<Polygon>} insetRectanglePolygons
400
+ * @param {number?=1.0} intersectionEpsilon - (optional, default is 1.0) A epsislon to define a tolerance for checking if two polygons intersect.
401
+ */
402
+ private static _filterInnerSplitPolygonsByCoverage(
403
+ splitPolygonsVertices: Array<Array<Vertex>>,
404
+ insetRectanglePolygons: Array<Polygon>,
405
+ intersectionEpsilon?: number
406
+ ) {
407
+ // TEST: Add some jitter
408
+ // splitPolygonsVertices.forEach(split => {
409
+ // split.forEach(vert => {
410
+ // vert.x += (0.5 - Math.random()) * 0.01;
411
+ // vert.y += (0.5 - Math.random()) * 0.01;
412
+ // });
413
+ // });
414
+
415
+ splitPolygonsVertices.forEach((split, index) => {
416
+ const isCW: boolean = Polygon.utils.isClockwise(split);
417
+ if (!isCW) {
418
+ console.warn("------ split is not isClockwise!", index, isCW);
419
+ }
420
+ });
421
+ insetRectanglePolygons.forEach((rect, index) => {
422
+ const isCW: boolean = rect.isClockwise();
423
+ if (!isCW) {
424
+ console.warn("------ rect is not isClockwise!", index, isCW);
425
+ }
426
+ });
427
+
428
+ const eps: number =
429
+ intersectionEpsilon === undefined || typeof intersectionEpsilon === "undefined" ? 1.0 : intersectionEpsilon;
430
+ return splitPolygonsVertices.filter((splitPolyVerts: Vertex[], _splitPolyIndex: number) => {
431
+ const intersectionTestCallback = PolygonInset._hasIntersectionCallback(splitPolyVerts, eps, _splitPolyIndex);
432
+ const intersectsWithAnyRect = insetRectanglePolygons.some(intersectionTestCallback);
433
+ return !intersectsWithAnyRect;
434
+ });
435
+ }
436
+
437
+ /**
438
+ * This private method will reverse each polygon's vertex order that's not clockwise.
439
+ *
440
+ * @param {Array<Vertex[]>} polygons
441
+ */
442
+ private static _assertAllPolygonsAreClockwise(polygons: Array<Vertex[]>): void {
443
+ polygons.forEach((polygonVerts: Vertex[], _polyIndex: number) => {
444
+ // if (!Polygon.utils.isClockwise(polygonVerts)) {
445
+ // polygonVerts.reverse(); // Attention: this happens in-place (Array.reverse is destructive!)
446
+ // }
447
+ PolygonInset._assertPolygonIsClockwise(polygonVerts);
448
+ });
449
+ }
450
+
451
+ /**
452
+ * This private method will revert the vertex order if the polygon is not clockwise.
453
+ *
454
+ * @param {Vertex[]} polygonVerts
455
+ */
456
+ private static _assertPolygonIsClockwise(polygonVerts: Vertex[]): void {
457
+ if (!Polygon.utils.isClockwise(polygonVerts)) {
458
+ polygonVerts.reverse(); // Attention: this happens in-place (Array.reverse is destructive!)
459
+ }
460
+ }
461
+
462
+ /**
463
+ * For simplification I'm using a callback generator function here.
464
+ *
465
+ * @param splitPolyVerts
466
+ * @param eps
467
+ * @param _splitPolyIndex
468
+ * @returns {Function} The callback for an `Array.some` parameter.
469
+ */
470
+ private static _hasIntersectionCallback =
471
+ (splitPolyVerts: Vertex[], eps: number, _splitPolyIndex: number) =>
472
+ (rectanglePoly: Polygon, _rectanglePolyIndex: number): boolean => {
473
+ // const intersectionVerts: XYCoords[] = sutherlandHodgman(splitPolyVerts, rectanglePoly.vertices);
474
+ const intersectionVerts: XYCoords[] = sutherlandHodgman(rectanglePoly.vertices, splitPolyVerts);
475
+
476
+ // var intersection = GreinerHorman.intersection(sourcePolygon.vertices, clipPolygon.vertices);
477
+ // const uniqueIntersectionVerts = clearDuplicateVertices(intersectionVerts);
478
+ const intersectionAreaSize: number = Polygon.utils.area(intersectionVerts);
479
+ if (intersectionAreaSize < 0) {
480
+ console.warn(
481
+ "%cFound a polygon split with negative area. Counterclockwise against all odds?",
482
+ "color: red; background: black;", // font-size: 30px",
483
+ "intersectionAreaSize",
484
+ intersectionAreaSize,
485
+ "_splitPolyIndex",
486
+ _splitPolyIndex,
487
+ "_rectanglePolyIndex",
488
+ _rectanglePolyIndex,
489
+ "intersectionVerts",
490
+ intersectionVerts
491
+ // "uniqueIntersectionVerts",
492
+ // uniqueIntersectionVerts
493
+ );
494
+ }
495
+ // if (intersectionAreaSize >= eps) {
496
+ // console.log(
497
+ // "%cFound a polygon split for removal",
498
+ // "color: orange; background: grey;", // font-size: 30px",
499
+ // "intersectionAreaSize",
500
+ // intersectionAreaSize,
501
+ // "_splitPolyIndex",
502
+ // _splitPolyIndex,
503
+ // "_rectanglePolyIndex",
504
+ // _rectanglePolyIndex,
505
+ // "intersectionVerts",
506
+ // intersectionVerts
507
+ // // "uniqueIntersectionVerts",
508
+ // // uniqueIntersectionVerts
509
+ // );
510
+ // }
511
+ // if (intersectionAreaSize < eps) {
512
+ // // console.log("%cHello", "color: green; background: yellow; font-size: 30px");
513
+ // console.log(
514
+ // "Polygon split can be kept. Area is position but below epsilon.",
515
+ // "intersectionAreaSize",
516
+ // intersectionAreaSize,
517
+ // "_splitPolyIndex",
518
+ // _splitPolyIndex,
519
+ // "_rectanglePolyIndex",
520
+ // _rectanglePolyIndex,
521
+ // "intersectionVerts",
522
+ // intersectionVerts
523
+ // // "uniqueIntersectionVerts",
524
+ // // uniqueIntersectionVerts
525
+ // );
526
+ // }
527
+ return intersectionAreaSize >= eps;
528
+ };
529
+ }