plotboilerplate 1.25.0 → 1.26.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 (105) hide show
  1. package/README.md +1 -0
  2. package/dist/index.esm.js +139 -33
  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 +139 -33
  9. package/dist/plotboilerplate.min.js +1 -1
  10. package/package.json +1 -1
  11. package/src/cjs/Bounds.d.ts +79 -25
  12. package/src/cjs/Bounds.js +102 -29
  13. package/src/cjs/Bounds.js.map +1 -1
  14. package/src/cjs/CircleSector.d.ts +3 -0
  15. package/src/cjs/CircleSector.js +28 -1
  16. package/src/cjs/CircleSector.js.map +1 -1
  17. package/src/cjs/drawutilssvg.d.ts +2 -1
  18. package/src/cjs/drawutilssvg.js +9 -3
  19. package/src/cjs/drawutilssvg.js.map +1 -1
  20. package/src/cjs/utils/LissajousFigure.d.ts +34 -0
  21. package/src/cjs/utils/LissajousFigure.js +100 -0
  22. package/src/cjs/utils/LissajousFigure.js.map +1 -0
  23. package/src/cjs/utils/PerlinNoise.d.ts +19 -0
  24. package/src/cjs/utils/PerlinNoise.js +352 -0
  25. package/src/cjs/utils/PerlinNoise.js.map +1 -0
  26. package/src/cjs/utils/SVGPathUtils.d.ts +22 -0
  27. package/src/cjs/utils/SVGPathUtils.js +45 -0
  28. package/src/cjs/utils/SVGPathUtils.js.map +1 -0
  29. package/src/cjs/utils/algorithms/pixelCornersToRoundPaths.d.ts +23 -0
  30. package/src/cjs/utils/algorithms/pixelCornersToRoundPaths.js +270 -0
  31. package/src/cjs/utils/algorithms/pixelCornersToRoundPaths.js.map +1 -0
  32. package/src/cjs/utils/datastructures/DataGrid2d.d.ts +6 -0
  33. package/src/cjs/utils/datastructures/DataGrid2dArrayMatrix.d.ts +88 -0
  34. package/src/cjs/utils/datastructures/DataGrid2dArrayMatrix.js +155 -0
  35. package/src/cjs/utils/datastructures/DataGrid2dArrayMatrix.js.map +1 -0
  36. package/src/cjs/utils/datastructures/DataGrid2dListAdapter.d.ts +1 -7
  37. package/src/cjs/utils/datastructures/DataGrid2dListAdapter.js +3 -3
  38. package/src/cjs/utils/datastructures/DataGrid2dListAdapter.js.map +1 -1
  39. package/src/cjs/utils/datastructures/GeometryMesh.d.ts +49 -0
  40. package/src/cjs/utils/datastructures/GeometryMesh.js +58 -0
  41. package/src/cjs/utils/datastructures/GeometryMesh.js.map +1 -0
  42. package/src/cjs/utils/detectDarkMode.js +16 -7
  43. package/src/cjs/utils/detectDarkMode.js.map +1 -1
  44. package/src/cjs/utils/dom/components/ColorGradientPicker.d.ts +4 -3
  45. package/src/cjs/utils/dom/components/ColorGradientPicker.js +8 -8
  46. package/src/cjs/utils/dom/components/ColorGradientPicker.js.map +1 -1
  47. package/src/cjs/utils/dom/components/ColorGradientSelector.d.ts +1 -0
  48. package/src/cjs/utils/dom/components/ColorGradientSelector.js +4 -2
  49. package/src/cjs/utils/dom/components/ColorGradientSelector.js.map +1 -1
  50. package/src/cjs/utils/parsers/svg/types.d.ts +8 -8
  51. package/src/esm/Bounds.d.ts +79 -25
  52. package/src/esm/Bounds.js +102 -29
  53. package/src/esm/Bounds.js.map +1 -1
  54. package/src/esm/CircleSector.d.ts +3 -0
  55. package/src/esm/CircleSector.js +28 -1
  56. package/src/esm/CircleSector.js.map +1 -1
  57. package/src/esm/drawutilssvg.d.ts +2 -1
  58. package/src/esm/drawutilssvg.js +9 -3
  59. package/src/esm/drawutilssvg.js.map +1 -1
  60. package/src/esm/utils/LissajousFigure.d.ts +34 -0
  61. package/src/esm/utils/LissajousFigure.js +95 -0
  62. package/src/esm/utils/LissajousFigure.js.map +1 -0
  63. package/src/esm/utils/PerlinNoise.d.ts +19 -0
  64. package/src/esm/utils/PerlinNoise.js +346 -0
  65. package/src/esm/utils/PerlinNoise.js.map +1 -0
  66. package/src/esm/utils/SVGPathUtils.d.ts +22 -0
  67. package/src/esm/utils/SVGPathUtils.js +42 -0
  68. package/src/esm/utils/SVGPathUtils.js.map +1 -0
  69. package/src/esm/utils/algorithms/pixelCornersToRoundPaths.d.ts +23 -0
  70. package/src/esm/utils/algorithms/pixelCornersToRoundPaths.js +266 -0
  71. package/src/esm/utils/algorithms/pixelCornersToRoundPaths.js.map +1 -0
  72. package/src/esm/utils/datastructures/DataGrid2d.d.ts +6 -0
  73. package/src/esm/utils/datastructures/DataGrid2dArrayMatrix.d.ts +88 -0
  74. package/src/esm/utils/datastructures/DataGrid2dArrayMatrix.js +150 -0
  75. package/src/esm/utils/datastructures/DataGrid2dArrayMatrix.js.map +1 -0
  76. package/src/esm/utils/datastructures/DataGrid2dListAdapter.d.ts +1 -7
  77. package/src/esm/utils/datastructures/DataGrid2dListAdapter.js +3 -3
  78. package/src/esm/utils/datastructures/DataGrid2dListAdapter.js.map +1 -1
  79. package/src/esm/utils/datastructures/GeometryMesh.d.ts +49 -0
  80. package/src/esm/utils/datastructures/GeometryMesh.js +53 -0
  81. package/src/esm/utils/datastructures/GeometryMesh.js.map +1 -0
  82. package/src/esm/utils/detectDarkMode.js +16 -7
  83. package/src/esm/utils/detectDarkMode.js.map +1 -1
  84. package/src/esm/utils/dom/components/ColorGradientPicker.d.ts +4 -3
  85. package/src/esm/utils/dom/components/ColorGradientPicker.js +18 -7
  86. package/src/esm/utils/dom/components/ColorGradientPicker.js.map +1 -1
  87. package/src/esm/utils/dom/components/ColorGradientSelector.d.ts +1 -0
  88. package/src/esm/utils/dom/components/ColorGradientSelector.js +9 -2
  89. package/src/esm/utils/dom/components/ColorGradientSelector.js.map +1 -1
  90. package/src/esm/utils/parsers/svg/types.d.ts +8 -8
  91. package/src/ts/Bounds.ts +107 -22
  92. package/src/ts/CircleSector.ts +39 -6
  93. package/src/ts/drawutilssvg.ts +8 -3
  94. package/src/ts/utils/LissajousFigure.ts +110 -0
  95. package/src/ts/utils/PerlinNoise.ts +368 -0
  96. package/src/ts/utils/SVGPathUtils.ts +55 -0
  97. package/src/ts/utils/algorithms/pixelCornersToRoundPaths.ts +379 -0
  98. package/src/ts/utils/datastructures/DataGrid2d.ts +7 -0
  99. package/src/ts/utils/datastructures/DataGrid2dArrayMatrix.ts +182 -0
  100. package/src/ts/utils/datastructures/DataGrid2dListAdapter.ts +4 -11
  101. package/src/ts/utils/datastructures/GeometryMesh.ts +79 -0
  102. package/src/ts/utils/detectDarkMode.ts +17 -8
  103. package/src/ts/utils/dom/components/ColorGradientPicker.tsx +18 -6
  104. package/src/ts/utils/dom/components/ColorGradientSelector.tsx +9 -2
  105. package/src/ts/utils/parsers/svg/types.ts +8 -8
@@ -0,0 +1,379 @@
1
+ /**
2
+ * A script to convert any boolean matrix to rounded paths.
3
+ *
4
+ * @author Ikaros Kappler
5
+ * @date 2025-10-17 (ported to Typescript from a demo-script)
6
+ * @version 1.0.0
7
+ */
8
+
9
+ import { Bounds } from "../../Bounds";
10
+ import { SVGPathParams, XYCoords } from "../../interfaces";
11
+ import { RasterPosition } from "../datastructures/DataGrid2d";
12
+ import { DataGrid2dArrayMatrix } from "../datastructures/DataGrid2dArrayMatrix";
13
+
14
+ const LEFT_BORDER: number = 0;
15
+ const TOP_BORDER: number = 1;
16
+ const RIGHT_BORDER: number = 2;
17
+ const BOTTOM_BORDER: number = 3;
18
+
19
+ export type PixelCornerPathOptions = {
20
+ origin?: XYCoords;
21
+ squareSize?: number;
22
+ gapSize?: number;
23
+ curveFactor?: number;
24
+ };
25
+
26
+ /**
27
+ * Computes an array of paths from the given boolean matrix.
28
+ *
29
+ * @param {DataGrid2dArrayMatrix<boolean>} matrix
30
+ * @param {PixelCornerPathOptions} options
31
+ * @returns
32
+ */
33
+ export const pixelCornersToRoundPaths = (
34
+ matrix: DataGrid2dArrayMatrix<boolean>,
35
+ options: PixelCornerPathOptions
36
+ ): Array<SVGPathParams> => {
37
+ const paths: Array<SVGPathParams> = [];
38
+ const visitedLeftBorderMatrix = new DataGrid2dArrayMatrix(matrix.xSegmentCount, matrix.ySegmentCount, false);
39
+
40
+ const origin: XYCoords = options && options.origin ? options.origin : { x: 0, y: 0 };
41
+ const squareSize: number = options && typeof options.squareSize === "number" ? options.squareSize : 10;
42
+ const gapSize: number = options && typeof options.gapSize === "number" ? options.gapSize : 2;
43
+ const curveFactor: number = options && typeof options.curveFactor === "number" ? options.curveFactor : 0.666;
44
+
45
+ // Find an unvisited pixel with a free left neighbour
46
+ const condition = (value: boolean, x: number, y: number, _matrix: DataGrid2dArrayMatrix<boolean>) => {
47
+ return (
48
+ value === true &&
49
+ visitedLeftBorderMatrix.get(x, y) === false &&
50
+ // is left neighbour free?
51
+ (x - 1 < 0 || _matrix.get(x - 1, y) === false)
52
+ );
53
+ };
54
+
55
+ // Just to be sure the while loop terminates after a maximum number of iterations.
56
+ const safetyLimit: number = matrix.xSegmentCount * matrix.ySegmentCount + 1;
57
+
58
+ // The next unvisited pixel position.
59
+ var nonVisitedPosition: RasterPosition;
60
+
61
+ var i: number = 0;
62
+ while ((nonVisitedPosition = matrix.find(condition)) && i++ < safetyLimit) {
63
+ // console.log("nonVisitedPosition", nonVisitedPosition);
64
+ // Condition: if a point was found then its left neighbour is empty.
65
+ // So by this the current pixel's left bound is definitely a path start.
66
+ const squareBox: Bounds = getSquareBox(origin, squareSize, gapSize, nonVisitedPosition.xIndex, nonVisitedPosition.yIndex);
67
+ const west: XYCoords = squareBox.getWestPoint();
68
+ // A starting point was found: Initialize the next path.
69
+ var pathData: SVGPathParams = [
70
+ // Start point
71
+ "M",
72
+ west.x,
73
+ west.y
74
+ ];
75
+ // Now from this point starting (left pixel corner) construct the next sequence of path commands.
76
+ __constructPath(
77
+ pathData,
78
+ matrix,
79
+ visitedLeftBorderMatrix,
80
+ nonVisitedPosition,
81
+ LEFT_BORDER,
82
+ origin,
83
+ squareSize,
84
+ gapSize,
85
+ curveFactor
86
+ );
87
+ paths.push(pathData);
88
+ // console.log("pathData", pathData);
89
+ }
90
+
91
+ return paths;
92
+ };
93
+
94
+ /**
95
+ * Construct a bounding box representing the visual square bounds for the given pixel.
96
+ *
97
+ * @param {XYCoords} origin - The origin to build the paths from (upper left corner of the graphics).
98
+ * @param {number} squareSize - The number of pixels to use to represent a pixel square.
99
+ * @param {number} gapSize - The number of pixels to use for gaps between squares.
100
+ * @param {number} x - The x position inside the data grid (matrix offset).
101
+ * @param {number} y - They position inside the data grid (matrix offset).
102
+ * @returns
103
+ */
104
+ const getSquareBox = (origin: XYCoords, squareSize: number, gapSize: number, x: number, y: number): Bounds => {
105
+ return new Bounds(
106
+ {
107
+ x: origin.x + x * (squareSize + gapSize),
108
+ y: origin.y + y * (squareSize + gapSize)
109
+ },
110
+ {
111
+ x: origin.x + x * (squareSize + gapSize) + squareSize,
112
+ y: origin.y + y * (squareSize + gapSize) + squareSize
113
+ }
114
+ );
115
+ };
116
+
117
+ /**
118
+ * Lerp between these two numbers using the passed lerp amount (usually a value in 0.0-1.0).
119
+ *
120
+ * @param {number} a
121
+ * @param {number} b
122
+ * @param {number} lerpFactor
123
+ * @returns {number}
124
+ */
125
+ const lerp = (a: number, b: number, lerpFactor): number => {
126
+ return a + (b - a) * lerpFactor;
127
+ };
128
+
129
+ /**
130
+ * Construct a path command for a cubic Bézier curve.
131
+ *
132
+ * @param {SVGPathParams} pathData
133
+ * @param {XYCoords} start
134
+ * @param {XYCoords} controlA
135
+ * @param {XYCoords} controlB
136
+ * @param {XYCoords} end
137
+ * @param {number} curveFactor
138
+ */
139
+ const addRoundCorner = (
140
+ pathData: SVGPathParams,
141
+ start: XYCoords,
142
+ controlA: XYCoords,
143
+ controlB: XYCoords,
144
+ end: XYCoords,
145
+ curveFactor: number
146
+ ) => {
147
+ pathData.push(
148
+ "C",
149
+ lerp(start.x, controlA.x, curveFactor),
150
+ lerp(start.y, controlA.y, curveFactor),
151
+ lerp(end.x, controlB.x, curveFactor),
152
+ lerp(end.y, controlB.y, curveFactor),
153
+ end.x,
154
+ end.y
155
+ );
156
+ };
157
+
158
+ /**
159
+ * Construct a new path command for a straight line.
160
+ *
161
+ * @param {SVGPathParams} pathData
162
+ * @param {XYCoords} target
163
+ */
164
+ const addStraightLine = (pathData: SVGPathParams, target: XYCoords) => {
165
+ pathData.push("L", target.x, target.y);
166
+ };
167
+
168
+ /**
169
+ * A helper function for constructing the next path, beginning at the passed matrix position.
170
+ *
171
+ * Note the path will be created at the left border of the given 'pixel', constructe
172
+ * in clockwise order.
173
+ *
174
+ * @param {SVGPathParams} pathData
175
+ * @param {DataGrid2dArrayMatrix<boolean>} matrix - The data matrix to use.
176
+ * @param {DataGrid2dArrayMatrix<boolean>} visitedLeftBordersMatrix - A helper matrix to keep track of visited pixels.
177
+ * @param {RasterPosition} startingPosition
178
+ * @param {number} startingBorderDirection
179
+ * @param {XYCoords} origin
180
+ * @param {number} squareSize
181
+ * @param {number} gapSize
182
+ * @param {number} curveFactor
183
+ */
184
+ const __constructPath = (
185
+ pathData: SVGPathParams,
186
+ matrix: DataGrid2dArrayMatrix<boolean>,
187
+ visitedLeftBordersMatrix: DataGrid2dArrayMatrix<boolean>,
188
+ startingPosition: RasterPosition,
189
+ startingBorderDirection: number,
190
+ origin: XYCoords,
191
+ squareSize: number,
192
+ gapSize: number,
193
+ curveFactor: number
194
+ ) => {
195
+ // Pre: position's neighbour pixel at ehe given border direction is unset.
196
+ var isPathComplete: boolean = false;
197
+ const maxInterations = matrix.xSegmentCount * matrix.ySegmentCount * 4 + 1;
198
+ var i = 0;
199
+ var position = startingPosition;
200
+ var borderDirection = startingBorderDirection;
201
+
202
+ while (!isPathComplete && i++ < maxInterations) {
203
+ var x = position.xIndex;
204
+ var y = position.yIndex;
205
+ // console.log("x", x, "y", y, "borderDirection", borderDirection);
206
+ // Get the pixel's box bounds.
207
+ const squareBox: Bounds = getSquareBox(origin, squareSize, gapSize, x, y);
208
+ // Find next step when walking 'clockwise'
209
+ if (borderDirection === LEFT_BORDER) {
210
+ // Only track visits on left borders. By this we can detect non-visited holes :)
211
+ visitedLeftBordersMatrix.set(x, y, true);
212
+ // We want to expand the left border
213
+ const west: XYCoords = squareBox.getWestPoint();
214
+ // Pixel in the north?
215
+ if (y - 1 < 0 || matrix.get(x, y - 1) === false) {
216
+ // Northwise pixel is empty
217
+ // -> construct round corner to right
218
+ addRoundCorner(
219
+ pathData,
220
+ west,
221
+ squareBox.getNorthWestPoint(),
222
+ squareBox.getNorthWestPoint(),
223
+ squareBox.getNorthPoint(),
224
+ curveFactor
225
+ );
226
+ borderDirection = TOP_BORDER;
227
+ // Keep pixel position unchanged
228
+ } else if (x - 1 >= 0 && matrix.get(x - 1, y - 1) === true) {
229
+ // Top neighbour pixel is set AND top-left pixel is set too.
230
+ // -> construct rounded edge to top-left.
231
+ const squareBoxNW: Bounds = getSquareBox(origin, squareSize, gapSize, x - 1, y - 1);
232
+ addRoundCorner(
233
+ pathData,
234
+ west,
235
+ squareBox.getNorthWestPoint(),
236
+ squareBoxNW.getSouthEastPoint(),
237
+ squareBoxNW.getSouthPoint(),
238
+ curveFactor
239
+ );
240
+ borderDirection = BOTTOM_BORDER;
241
+ position = { xIndex: x - 1, yIndex: y - 1 };
242
+ } else {
243
+ // Straight to top
244
+ const squareBoxN: Bounds = getSquareBox(origin, squareSize, gapSize, x, y - 1);
245
+ addStraightLine(pathData, squareBoxN.getWestPoint());
246
+ position = { xIndex: x, yIndex: y - 1 };
247
+ // Keep borderDirection: LEFT_BORDER
248
+ // isPathComplete = true; // TODO
249
+ }
250
+ } else if (borderDirection === TOP_BORDER) {
251
+ // North pixel is definitely empty.
252
+ const north: XYCoords = squareBox.getNorthPoint();
253
+ if (x + 1 >= matrix.xSegmentCount || matrix.get(x + 1, y) === false) {
254
+ // Top border and right pixel is empty
255
+ // --> create rounded corner to right south
256
+ addRoundCorner(
257
+ pathData,
258
+ north,
259
+ squareBox.getNorthEastPoint(),
260
+ squareBox.getNorthEastPoint(),
261
+ squareBox.getEastPoint(),
262
+ curveFactor
263
+ );
264
+ borderDirection = RIGHT_BORDER;
265
+ // Keep pixel position unchanged
266
+ } else if (y - 1 < 0 || matrix.get(x + 1, y - 1) === false) {
267
+ // Top border and right pixel is set, and north-east pixel is clear.
268
+ // -> linear path to right
269
+ const squareBoxE: Bounds = getSquareBox(origin, squareSize, gapSize, x + 1, y);
270
+ addStraightLine(pathData, squareBoxE.getNorthPoint());
271
+ position = { xIndex: x + 1, yIndex: y };
272
+ // Keep borderDirection TOP
273
+ } else {
274
+ // Last case: right neighbour is set and north-east pixel is set
275
+ //-> construct a rounded corner to east-north.
276
+ const squareBoxNE: Bounds = getSquareBox(origin, squareSize, gapSize, x + 1, y - 1);
277
+ addRoundCorner(
278
+ pathData,
279
+ north,
280
+ squareBox.getNorthEastPoint(),
281
+ squareBoxNE.getSouthWestPoint(),
282
+ squareBoxNE.getWestPoint(),
283
+ curveFactor
284
+ );
285
+ borderDirection = LEFT_BORDER;
286
+ position = { xIndex: x + 1, yIndex: y - 1 };
287
+ // isPathComplete = true; // TODO!!!
288
+ }
289
+ } else if (borderDirection === RIGHT_BORDER) {
290
+ // East pixel is definitely empty.
291
+ var east = squareBox.getEastPoint();
292
+ if (y + 1 >= matrix.ySegmentCount || matrix.get(x, y + 1) === false) {
293
+ // South pixel is empty.
294
+ // -> construct rounded corner to south-east
295
+ addRoundCorner(
296
+ pathData,
297
+ east,
298
+ squareBox.getSouthEastPoint(),
299
+ squareBox.getSouthEastPoint(),
300
+ squareBox.getSouthPoint(),
301
+ curveFactor
302
+ );
303
+ borderDirection = BOTTOM_BORDER;
304
+ // Keep pixel position unchanged
305
+ } else if (x + 1 < matrix.xSegmentCount && y + 1 < matrix.ySegmentCount && matrix.get(x + 1, y + 1) === true) {
306
+ // South pixel is set AND right lower pixel is set, too.
307
+ // -> construct round corner to south-east.
308
+ const squareBoxSE: Bounds = getSquareBox(origin, squareSize, gapSize, x + 1, y + 1);
309
+ addRoundCorner(
310
+ pathData,
311
+ east,
312
+ squareBox.getSouthEastPoint(),
313
+ squareBoxSE.getNorthWestPoint(),
314
+ squareBoxSE.getNorthPoint(),
315
+ curveFactor
316
+ );
317
+ borderDirection = TOP_BORDER;
318
+ position = { xIndex: x + 1, yIndex: y + 1 };
319
+ } else {
320
+ // South pixel is set and south-east pixel is clear.
321
+ // -> construct linear connection to south
322
+ const squareBoxS: Bounds = getSquareBox(origin, squareSize, gapSize, x, y + 1);
323
+ addStraightLine(pathData, squareBoxS.getEastPoint());
324
+ position = { xIndex: x, yIndex: y + 1 };
325
+ // Keep borderDirection RIGHT
326
+ }
327
+ } else if (borderDirection === BOTTOM_BORDER) {
328
+ // Pre: south pixel is definitely empty
329
+ const south: XYCoords = squareBox.getSouthPoint();
330
+ if (x - 1 < 0 || matrix.get(x - 1, y) === false) {
331
+ // Left pixel is empty
332
+ // -> construct round corner to west-north
333
+ addRoundCorner(
334
+ pathData,
335
+ south,
336
+ squareBox.getSouthWestPoint(),
337
+ squareBox.getSouthWestPoint(),
338
+ squareBox.getWestPoint(),
339
+ curveFactor
340
+ );
341
+ borderDirection = LEFT_BORDER;
342
+ } else if (x - 1 >= 0 && y + 1 < matrix.ySegmentCount && matrix.get(x - 1, y + 1) === true) {
343
+ // Left pixel AND left lower pixel are set
344
+ // -> construct rounded edge to south-west
345
+ const squareBoxSW: Bounds = getSquareBox(origin, squareSize, gapSize, x - 1, y + 1);
346
+ addRoundCorner(
347
+ pathData,
348
+ south,
349
+ squareBox.getSouthWestPoint(),
350
+ squareBoxSW.getNorthEastPoint(),
351
+ squareBoxSW.getEastPoint(),
352
+ curveFactor
353
+ );
354
+ borderDirection = RIGHT_BORDER;
355
+ position = { xIndex: x - 1, yIndex: y + 1 };
356
+ } else {
357
+ // Left pixel is set AND south pixel is empty
358
+ // -> construct linear edge to left neighbour.
359
+ const squareBoxW: Bounds = getSquareBox(origin, squareSize, gapSize, x - 1, y);
360
+ addStraightLine(pathData, squareBoxW.getSouthPoint());
361
+ position = { xIndex: x - 1, yIndex: y };
362
+ // Keep borderDirection BOTTOM
363
+ }
364
+ } else {
365
+ console.warn(`[Error] Cannot construct full rounded path; 'borderDirection' has wrong value (${borderDirection}).`);
366
+ isPathComplete = true;
367
+ }
368
+
369
+ // End condition reached?
370
+ if (
371
+ startingPosition.xIndex === position.xIndex &&
372
+ startingPosition.yIndex === position.yIndex &&
373
+ startingBorderDirection === borderDirection
374
+ ) {
375
+ pathData.push("Z");
376
+ isPathComplete = true;
377
+ }
378
+ }
379
+ };
@@ -18,6 +18,13 @@
18
18
 
19
19
  export type DataGridFace4<T> = [[T | null, T | null], [T | null, T | null]];
20
20
 
21
+ export type RasterPosition = {
22
+ xIndex: number;
23
+ yIndex: number;
24
+ xRel?: number;
25
+ yRel?: number;
26
+ };
27
+
21
28
  export interface IDataGrid2d<T> {
22
29
  readonly xSegmentCount: number;
23
30
 
@@ -0,0 +1,182 @@
1
+ /**
2
+ * A DataGrid2d implementation on 2 dimensional arrays.
3
+ *
4
+ * @author Ikaros Kappler
5
+ * @date 2025-10-16
6
+ * @modified 2025-10-28 Added `DataGrid2dArrayMatrix.setAll(function)`.
7
+ * @modified 2025-10-28 Fixed a bug in the `DataGrid2dArrayMatrix.toString()` method.
8
+ * @version 1.0.0
9
+ **/
10
+
11
+ import { arrayFill } from "../algorithms/arrayFill";
12
+ import { DataGridFace4, IDataGrid2d, RasterPosition } from "./DataGrid2d";
13
+
14
+ export class DataGrid2dArrayMatrix<T> implements IDataGrid2d<T> {
15
+ /**
16
+ * @override
17
+ */
18
+ readonly xSegmentCount: number;
19
+
20
+ /**
21
+ * @override
22
+ */
23
+ readonly ySegmentCount: number;
24
+
25
+ // This is not really useful here ...
26
+ minDataValue: T;
27
+ maxDataValue: T;
28
+
29
+ private _matrix: Array<Array<T>>;
30
+
31
+ /**
32
+ * Create a new DataGrid2d from the given
33
+ * @throws Error if xSegmentCount*ySegmentCount is larger than the capacity of the array/list.
34
+ * @param list
35
+ * @param xSegmentCount
36
+ * @param ySegmentCount
37
+ */
38
+ constructor(xSegmentCount: number, ySegmentCount: number, initialValue: T) {
39
+ if (xSegmentCount <= 0) {
40
+ throw `Cannot create DataGrid2dArray, xSegmentCount (${xSegmentCount}) must be > 0.`;
41
+ }
42
+ if (ySegmentCount <= 0) {
43
+ throw `Cannot create DataGrid2dArray, ySegmentCount (${ySegmentCount}) must be > 0.`;
44
+ }
45
+
46
+ // this.canvas = document.getElementById(canvasId);
47
+ this.xSegmentCount = xSegmentCount;
48
+ this.ySegmentCount = ySegmentCount;
49
+
50
+ this._matrix = [];
51
+ for (var i = 0; i < ySegmentCount; i++) {
52
+ this._matrix.push(arrayFill(xSegmentCount, initialValue));
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Set the matrix value at the given (x,y) position to a new value.
58
+ *
59
+ * @param {number} xIndex - The horizontal matrix position to set.
60
+ * @param {number} yIndex - The vertical matrix position to set.
61
+ * @param {T} value - The new value.
62
+ */
63
+ public set(xIndex: number, yIndex: number, value: T) {
64
+ this._matrix[yIndex][xIndex] = value;
65
+ }
66
+
67
+ /**
68
+ * Getthe matrix value at the given (x,y) position.
69
+ *
70
+ * @param {number} xIndex - The horizontal matrix position to set.
71
+ * @param {number} yIndex - The vertical matrix position to set.
72
+ * @return {T} value - The new value.
73
+ */
74
+ public get(xIndex: number, yIndex: number): T {
75
+ return this.getDataValueAt(xIndex, yIndex);
76
+ }
77
+
78
+ /**
79
+ * Set all matrix values. The passed factory function must accept respective (x,y) positions.
80
+ *
81
+ * @param {Function} factory - The factory function to determine the new values to set.
82
+ * @return {DataGrid2dArrayMatrix<T>} this - for chaning.
83
+ */
84
+ public setAll(factory: (xIndex: number, yIndex: number) => T): DataGrid2dArrayMatrix<T> {
85
+ for (var x = 0; x < this.xSegmentCount; x++) {
86
+ for (var y = 0; y < this.ySegmentCount; y++) {
87
+ this.set(x, y, factory(x, y));
88
+ }
89
+ }
90
+ return this;
91
+ }
92
+
93
+ public find(
94
+ condition: (value: T, xIndex: number, yIndex: number, sourceMatrix: DataGrid2dArrayMatrix<T>) => boolean
95
+ ): RasterPosition {
96
+ for (var x = 0; x < this.xSegmentCount; x++) {
97
+ for (var y = 0; y < this.ySegmentCount; y++) {
98
+ if (condition(this.get(x, y), x, y, this)) {
99
+ return {
100
+ xIndex: x,
101
+ yIndex: y,
102
+ xRel: x / this.xSegmentCount,
103
+ yRel: y / this.ySegmentCount
104
+ };
105
+ }
106
+ }
107
+ }
108
+ return null;
109
+ }
110
+
111
+ /**
112
+ * @override
113
+ */
114
+ getMinDataValue(): T {
115
+ return this.minDataValue;
116
+ }
117
+
118
+ /**
119
+ * @override
120
+ */
121
+ getMaxDataValue(): T {
122
+ return this.maxDataValue;
123
+ }
124
+
125
+ /**
126
+ * @override
127
+ */
128
+ getDataValueAt(xIndex: number, yIndex: number, isDebug?: boolean): T | null {
129
+ return this._matrix[yIndex][xIndex];
130
+ }
131
+
132
+ /**
133
+ * @override
134
+ */
135
+ getDataFace4At(xIndex: number, yIndex: number, buffer: DataGridFace4<T>) {
136
+ // buffer: array[2][2]
137
+ buffer[0][0] = this.getDataValueAt(xIndex, yIndex);
138
+ buffer[1][0] = this.getDataValueAt(xIndex + 1, yIndex);
139
+ buffer[1][1] = this.getDataValueAt(xIndex + 1, yIndex + 1);
140
+ buffer[0][1] = this.getDataValueAt(xIndex, yIndex + 1);
141
+ }
142
+
143
+ /**
144
+ * A helper method to convert a boolean matrix to a parsable string.
145
+ *
146
+ * @param {DataGrid2dArrayMatrix<boolean>} matrix
147
+ * @returns {string}
148
+ */
149
+ static toString(matrix: DataGrid2dArrayMatrix<boolean>): string {
150
+ const buffer: Array<string> = [];
151
+ for (var y = 0; y < matrix.ySegmentCount; y++) {
152
+ for (var x = 0; x < matrix.xSegmentCount; x++) {
153
+ buffer.push(matrix.get(x, y) ? "X" : ".");
154
+ }
155
+ buffer.push("\n");
156
+ }
157
+ return buffer.join("");
158
+ }
159
+
160
+ /**
161
+ * Simple way to parse a data matrix from a string. `false` should be coded as '.' character,
162
+ * `true` must be coded as 'X' character.
163
+ *
164
+ * @param {string} str - The input string.
165
+ * @param {number} width - The desired width of the matrix (xSegmentCount).
166
+ * @param {number} height - The desired height of the matrix (ySegmentCount).
167
+ * @returns {DataGrid2dArrayMatrix<boolean>}
168
+ */
169
+ static parseBooleanMatrix(str: string, width: number, height: number): DataGrid2dArrayMatrix<boolean> {
170
+ const matrix = new DataGrid2dArrayMatrix<boolean>(width, height, false);
171
+ const lines: String[] = str.trim().split("\n");
172
+ for (var y = 0; y < lines.length && y < height; y++) {
173
+ lines[y] = lines[y].trim();
174
+ for (var x = 0; x < lines[y].length && x < width; x++) {
175
+ if (y < height && x < width && lines[y].charAt(x) === "X") {
176
+ matrix.set(x, y, true);
177
+ }
178
+ }
179
+ }
180
+ return matrix;
181
+ }
182
+ }
@@ -7,7 +7,7 @@
7
7
  * @version 1.0.0
8
8
  **/
9
9
 
10
- import { DataGridFace4, IDataGrid2d } from "./DataGrid2d";
10
+ import { DataGridFace4, IDataGrid2d, RasterPosition } from "./DataGrid2d";
11
11
 
12
12
  type Triplet<T> = {
13
13
  x: T;
@@ -15,13 +15,6 @@ type Triplet<T> = {
15
15
  z: T;
16
16
  };
17
17
 
18
- type RasterPosition = {
19
- xIndex: number;
20
- yIndex: number;
21
- xRel?: number;
22
- yRel?: number;
23
- };
24
-
25
18
  export class DataGrid2dListAdapter<T> implements IDataGrid2d<T> {
26
19
  /**
27
20
  * @override
@@ -96,9 +89,9 @@ export class DataGrid2dListAdapter<T> implements IDataGrid2d<T> {
96
89
  getDataValueAt(xIndex: number, yIndex: number, isDebug?: boolean): T | null {
97
90
  // var bufferIndex = yIndex * this.xSegmentCount + xIndex;
98
91
  var bufferIndex = this.coordinateIndicesToBufferIndex(xIndex, yIndex);
99
- if (isDebug) {
100
- console.log("xIndex", xIndex, "yIndex", yIndex, "bufferIndex", bufferIndex);
101
- }
92
+ // if (isDebug) {
93
+ // console.log("xIndex", xIndex, "yIndex", yIndex, "bufferIndex", bufferIndex);
94
+ // }
102
95
  if (bufferIndex > this._dataList.length) {
103
96
  console.error("ERR buffer index is out of bounds! xIndex", xIndex, "yIndex", yIndex, "bufferIndex", bufferIndex);
104
97
  }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Adapted from the 3D geometry datastuctured from demo-33 (depth mesh).
3
+ *
4
+ * Refactored the strucure into a class.
5
+ *
6
+ * @author Ikaros Kappler
7
+ * @date 2025-11-14
8
+ * @modified 2025-11-17 Moved `getBounds` and `normalizeGeometry` from helper functions to this class.
9
+ * @version 1.0.0
10
+ */
11
+
12
+ export type Vertex2 = { x: number; y: number };
13
+ export type Vertex3 = { x: number; y: number; z: number };
14
+
15
+ export class GeometryMesh<T extends Vertex2 | Vertex3> {
16
+ /**
17
+ * A set (array) of vertices (points) used in this mesh. Can be 2d or 3d.
18
+ *
19
+ * @member {Array<T>}
20
+ * @memberof GeometryMesh
21
+ * @type {Array<T>}
22
+ * @instance
23
+ */
24
+ vertices: Array<T>;
25
+
26
+ /**
27
+ * A set (array) of edges used in this mesh.
28
+ * Each edge consist of two indices indicating the vertex position.
29
+ *
30
+ * @member {Array<T>}
31
+ * @memberof GeometryMesh
32
+ * @type {Array<T>}
33
+ * @instance
34
+ */
35
+ edges: Array<[number, number]>;
36
+
37
+ constructor(vertices?: Array<T>, edges?: Array<[number, number]>) {
38
+ this.vertices = vertices || [];
39
+ this.edges = edges || [];
40
+ }
41
+
42
+ normalize() {
43
+ // Desired bounds is a max bounding box. The object will be scaled keeping aspect ratio
44
+ // in that way that the largest axis size touches the desired bounds.
45
+ var desiredBounds = { min: { x: -1, y: -1, z: -1 }, max: { x: 1, y: 1, z: 1 } };
46
+ var bounds = this.getGeometryBounds();
47
+ var sizeX = bounds.max.x - bounds.min.x;
48
+ var sizeY = bounds.max.y - bounds.min.y;
49
+ var sizeZ = bounds.max.z - bounds.min.z;
50
+ // Scale uniform so the dimension with the max expansion fits into [-1,1]
51
+ var maxDimension = Math.max(sizeX, sizeY, sizeZ);
52
+ var desiredSizeX = (desiredBounds.max.x - desiredBounds.min.x) / maxDimension;
53
+ var desiredSizeY = (desiredBounds.max.y - desiredBounds.min.y) / maxDimension;
54
+ var desiredSizeZ = (desiredBounds.max.z - desiredBounds.min.z) / maxDimension;
55
+ for (var i in this.vertices) {
56
+ var vert = this.vertices[i] as Vertex3; // TODO: Check if this is safe
57
+ vert.x = desiredBounds.min.x + (bounds.max.x - vert.x) * desiredSizeX;
58
+ vert.y = desiredBounds.min.y + (bounds.max.y - vert.y) * desiredSizeY;
59
+ vert.z = desiredBounds.min.z + (bounds.max.z - vert.z) * desiredSizeZ;
60
+ }
61
+ }
62
+
63
+ getGeometryBounds(): { min: Vertex3; max: Vertex3; width: number; height: number; depth: number } {
64
+ // var min = new Vert3(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
65
+ // var max = new Vert3(Number.MIN_VALUE, Number.MIN_VALUE, Number.MIN_VALUE);
66
+ var min = { x: Number.MAX_VALUE, y: Number.MAX_VALUE, z: Number.MAX_VALUE };
67
+ var max = { x: Number.MIN_VALUE, y: Number.MIN_VALUE, z: Number.MIN_VALUE };
68
+ for (var i in this.vertices) {
69
+ var vert = this.vertices[i] as Vertex3;
70
+ min.x = Math.min(vert.x, min.x);
71
+ min.y = Math.min(vert.y, min.y);
72
+ min.z = Math.min(vert.z, min.z);
73
+ max.x = Math.max(vert.x, max.x);
74
+ max.y = Math.max(vert.y, max.y);
75
+ max.z = Math.max(vert.z, max.z);
76
+ }
77
+ return { min: min, max: max, width: max.x - min.x, height: max.y - min.y, depth: max.z - min.z };
78
+ }
79
+ }