@pooder/kit 5.1.0 → 5.3.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 (86) hide show
  1. package/.test-dist/src/CanvasService.js +249 -249
  2. package/.test-dist/src/ViewportSystem.js +75 -75
  3. package/.test-dist/src/background.js +203 -203
  4. package/.test-dist/src/bridgeSelection.js +20 -20
  5. package/.test-dist/src/constraints.js +237 -237
  6. package/.test-dist/src/dieline.js +818 -818
  7. package/.test-dist/src/edgeScale.js +12 -12
  8. package/.test-dist/src/extensions/background.js +203 -0
  9. package/.test-dist/src/extensions/bridgeSelection.js +20 -0
  10. package/.test-dist/src/extensions/constraints.js +237 -0
  11. package/.test-dist/src/extensions/dieline.js +828 -0
  12. package/.test-dist/src/extensions/edgeScale.js +12 -0
  13. package/.test-dist/src/extensions/feature.js +825 -0
  14. package/.test-dist/src/extensions/featureComplete.js +32 -0
  15. package/.test-dist/src/extensions/film.js +167 -0
  16. package/.test-dist/src/extensions/geometry.js +545 -0
  17. package/.test-dist/src/extensions/image.js +1529 -0
  18. package/.test-dist/src/extensions/index.js +30 -0
  19. package/.test-dist/src/extensions/maskOps.js +279 -0
  20. package/.test-dist/src/extensions/mirror.js +104 -0
  21. package/.test-dist/src/extensions/ruler.js +345 -0
  22. package/.test-dist/src/extensions/sceneLayout.js +96 -0
  23. package/.test-dist/src/extensions/sceneLayoutModel.js +196 -0
  24. package/.test-dist/src/extensions/sceneVisibility.js +62 -0
  25. package/.test-dist/src/extensions/size.js +331 -0
  26. package/.test-dist/src/extensions/tracer.js +538 -0
  27. package/.test-dist/src/extensions/white-ink.js +1190 -0
  28. package/.test-dist/src/extensions/wrappedOffsets.js +33 -0
  29. package/.test-dist/src/feature.js +826 -826
  30. package/.test-dist/src/featureComplete.js +32 -32
  31. package/.test-dist/src/film.js +167 -167
  32. package/.test-dist/src/geometry.js +506 -506
  33. package/.test-dist/src/image.js +1250 -1250
  34. package/.test-dist/src/index.js +2 -19
  35. package/.test-dist/src/maskOps.js +270 -270
  36. package/.test-dist/src/mirror.js +104 -104
  37. package/.test-dist/src/renderSpec.js +2 -2
  38. package/.test-dist/src/ruler.js +343 -343
  39. package/.test-dist/src/sceneLayout.js +99 -99
  40. package/.test-dist/src/sceneLayoutModel.js +196 -196
  41. package/.test-dist/src/sceneView.js +40 -40
  42. package/.test-dist/src/sceneVisibility.js +42 -42
  43. package/.test-dist/src/services/CanvasService.js +249 -0
  44. package/.test-dist/src/services/ViewportSystem.js +76 -0
  45. package/.test-dist/src/services/index.js +24 -0
  46. package/.test-dist/src/services/renderSpec.js +2 -0
  47. package/.test-dist/src/size.js +332 -332
  48. package/.test-dist/src/tracer.js +544 -544
  49. package/.test-dist/src/white-ink.js +829 -829
  50. package/.test-dist/src/wrappedOffsets.js +33 -33
  51. package/CHANGELOG.md +12 -0
  52. package/dist/index.d.mts +14 -0
  53. package/dist/index.d.ts +14 -0
  54. package/dist/index.js +3521 -3220
  55. package/dist/index.mjs +3532 -3226
  56. package/package.json +1 -1
  57. package/src/coordinate.ts +106 -106
  58. package/src/extensions/background.ts +230 -230
  59. package/src/extensions/bridgeSelection.ts +17 -17
  60. package/src/extensions/constraints.ts +322 -322
  61. package/src/extensions/dieline.ts +20 -17
  62. package/src/extensions/edgeScale.ts +19 -19
  63. package/src/extensions/feature.ts +1021 -1021
  64. package/src/extensions/featureComplete.ts +46 -46
  65. package/src/extensions/film.ts +194 -194
  66. package/src/extensions/geometry.ts +719 -719
  67. package/src/extensions/image.ts +1924 -1594
  68. package/src/extensions/index.ts +11 -11
  69. package/src/extensions/maskOps.ts +365 -299
  70. package/src/extensions/mirror.ts +128 -128
  71. package/src/extensions/ruler.ts +451 -451
  72. package/src/extensions/sceneLayout.ts +140 -140
  73. package/src/extensions/sceneLayoutModel.ts +342 -342
  74. package/src/extensions/sceneVisibility.ts +71 -71
  75. package/src/extensions/size.ts +389 -389
  76. package/src/extensions/tracer.ts +302 -370
  77. package/src/extensions/white-ink.ts +1489 -1366
  78. package/src/extensions/wrappedOffsets.ts +33 -33
  79. package/src/index.ts +2 -2
  80. package/src/services/CanvasService.ts +300 -300
  81. package/src/services/ViewportSystem.ts +95 -95
  82. package/src/services/index.ts +3 -3
  83. package/src/services/renderSpec.ts +18 -18
  84. package/src/units.ts +27 -27
  85. package/tests/run.ts +118 -118
  86. package/tsconfig.test.json +15 -15
@@ -0,0 +1,538 @@
1
+ "use strict";
2
+ /**
3
+ * Image Tracer Utility
4
+ * Converts raster images (URL/Base64) to SVG Path Data using Marching Squares algorithm.
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.ImageTracer = void 0;
11
+ const paper_1 = __importDefault(require("paper"));
12
+ const maskOps_1 = require("./maskOps");
13
+ class ImageTracer {
14
+ /**
15
+ * Main entry point: Traces an image URL to an SVG path string.
16
+ * @param imageUrl The URL or Base64 string of the image.
17
+ * @param options Configuration options.
18
+ */
19
+ static async trace(imageUrl, options = {}) {
20
+ const { pathData } = await this.traceWithBounds(imageUrl, options);
21
+ return pathData;
22
+ }
23
+ static async traceWithBounds(imageUrl, options = {}) {
24
+ const img = await this.loadImage(imageUrl);
25
+ const width = img.width;
26
+ const height = img.height;
27
+ const debug = options.debug === true;
28
+ const debugLog = (message, payload) => {
29
+ if (!debug)
30
+ return;
31
+ if (payload) {
32
+ console.info(`[ImageTracer] ${message}`, payload);
33
+ return;
34
+ }
35
+ console.info(`[ImageTracer] ${message}`);
36
+ };
37
+ // 1. Draw to canvas and get pixel data
38
+ const canvas = document.createElement("canvas");
39
+ canvas.width = width;
40
+ canvas.height = height;
41
+ const ctx = canvas.getContext("2d");
42
+ if (!ctx)
43
+ throw new Error("Could not get 2D context");
44
+ ctx.drawImage(img, 0, 0);
45
+ const imageData = ctx.getImageData(0, 0, width, height);
46
+ // 2. Morphology processing
47
+ const threshold = options.threshold ?? 10;
48
+ const componentMode = options.componentMode ?? "largest";
49
+ const minComponentArea = Math.max(0, options.minComponentArea ?? 0);
50
+ // Adaptive radius: 3% of the image's largest dimension, at least 6px
51
+ const adaptiveRadius = Math.max(6, Math.floor(Math.max(width, height) * 0.03));
52
+ const radius = options.morphologyRadius ?? adaptiveRadius;
53
+ const expand = options.expand ?? 0;
54
+ const noChannels = options.noChannels !== false;
55
+ const alphaOpaqueCutoff = options.alphaOpaqueCutoff ?? 250;
56
+ const resolvedMaskMode = (options.maskMode ?? "auto") === "auto"
57
+ ? (0, maskOps_1.inferMaskMode)(imageData, alphaOpaqueCutoff)
58
+ : options.maskMode;
59
+ const alphaAnalysis = (0, maskOps_1.analyzeAlpha)(imageData, alphaOpaqueCutoff);
60
+ debugLog("traceWithBounds:start", {
61
+ width,
62
+ height,
63
+ threshold,
64
+ radius,
65
+ expand,
66
+ noChannels,
67
+ maskMode: options.maskMode ?? "auto",
68
+ resolvedMaskMode,
69
+ alphaOpaqueCutoff,
70
+ alpha: {
71
+ minAlpha: alphaAnalysis.minAlpha,
72
+ belowOpaqueRatio: Number(alphaAnalysis.belowOpaqueRatio.toFixed(4)),
73
+ veryTransparentRatio: Number(alphaAnalysis.veryTransparentRatio.toFixed(4)),
74
+ },
75
+ componentMode,
76
+ minComponentArea,
77
+ forceConnected: options.forceConnected === true,
78
+ simplifyTolerance: options.simplifyTolerance ?? 2.5,
79
+ smoothing: options.smoothing !== false,
80
+ });
81
+ // Add padding to the processing canvas to avoid edge clipping during dilation
82
+ // Padding should be at least the radius + expansion size
83
+ const padding = radius + expand + 2;
84
+ const paddedWidth = width + padding * 2;
85
+ const paddedHeight = height + padding * 2;
86
+ let mask = (0, maskOps_1.createMask)(imageData, {
87
+ threshold,
88
+ padding,
89
+ paddedWidth,
90
+ paddedHeight,
91
+ maskMode: options.maskMode,
92
+ whiteThreshold: options.whiteThreshold,
93
+ alphaOpaqueCutoff,
94
+ });
95
+ if (radius > 0) {
96
+ mask = (0, maskOps_1.circularMorphology)(mask, paddedWidth, paddedHeight, radius, "closing");
97
+ }
98
+ if (noChannels) {
99
+ mask = (0, maskOps_1.fillHoles)(mask, paddedWidth, paddedHeight);
100
+ }
101
+ if (radius > 0) {
102
+ const smoothRadius = Math.max(1, Math.floor(radius * 0.2));
103
+ mask = (0, maskOps_1.circularMorphology)(mask, paddedWidth, paddedHeight, smoothRadius, "closing");
104
+ }
105
+ const baseMask = mask;
106
+ const baseContoursRaw = this.traceAllContours(baseMask, paddedWidth, paddedHeight);
107
+ const baseContours = this.selectContours(baseContoursRaw, componentMode, minComponentArea);
108
+ if (!baseContours.length) {
109
+ // Fallback: Return a rectangular outline matching dimensions
110
+ const w = options.scaleToWidth ?? width;
111
+ const h = options.scaleToHeight ?? height;
112
+ debugLog("fallback:no-base-contour", { width: w, height: h });
113
+ return {
114
+ pathData: `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`,
115
+ baseBounds: { x: 0, y: 0, width: w, height: h },
116
+ bounds: { x: 0, y: 0, width: w, height: h },
117
+ };
118
+ }
119
+ const baseUnpaddedContours = baseContours
120
+ .map((contour) => this.clampPointsToImageBounds(contour.map((p) => ({
121
+ x: p.x - padding,
122
+ y: p.y - padding,
123
+ })), width, height))
124
+ .filter((contour) => contour.length > 2);
125
+ if (!baseUnpaddedContours.length) {
126
+ const w = options.scaleToWidth ?? width;
127
+ const h = options.scaleToHeight ?? height;
128
+ debugLog("fallback:empty-base-contours", { width: w, height: h });
129
+ return {
130
+ pathData: `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`,
131
+ baseBounds: { x: 0, y: 0, width: w, height: h },
132
+ bounds: { x: 0, y: 0, width: w, height: h },
133
+ };
134
+ }
135
+ let baseBounds = this.boundsFromPoints(this.flattenContours(baseUnpaddedContours));
136
+ let maskExpanded = baseMask;
137
+ if (expand > 0) {
138
+ maskExpanded = (0, maskOps_1.circularMorphology)(baseMask, paddedWidth, paddedHeight, expand, "dilate");
139
+ }
140
+ const expandedContoursRaw = this.traceAllContours(maskExpanded, paddedWidth, paddedHeight);
141
+ const expandedContours = this.selectContours(expandedContoursRaw, componentMode, minComponentArea);
142
+ if (!expandedContours.length) {
143
+ debugLog("fallback:no-expanded-contour", {
144
+ baseBounds,
145
+ width,
146
+ height,
147
+ expand,
148
+ });
149
+ return {
150
+ pathData: `M 0 0 L ${width} 0 L ${width} ${height} L 0 ${height} Z`,
151
+ baseBounds,
152
+ bounds: baseBounds,
153
+ };
154
+ }
155
+ // Keep expanded coordinates in the unpadded space without clamping to
156
+ // original image bounds. If the shape touches an edge, clamping would
157
+ // drop one-sided expand distance (e.g. bottom/right expansion).
158
+ const expandedUnpaddedContours = expandedContours
159
+ .map((contour) => contour.map((p) => ({
160
+ x: p.x - padding,
161
+ y: p.y - padding,
162
+ })))
163
+ .filter((contour) => contour.length > 2);
164
+ if (!expandedUnpaddedContours.length) {
165
+ debugLog("fallback:empty-expanded-contours", {
166
+ baseBounds,
167
+ width,
168
+ height,
169
+ expand,
170
+ });
171
+ return {
172
+ pathData: `M 0 0 L ${width} 0 L ${width} ${height} L 0 ${height} Z`,
173
+ baseBounds,
174
+ bounds: baseBounds,
175
+ };
176
+ }
177
+ let globalBounds = this.boundsFromPoints(this.flattenContours(expandedUnpaddedContours));
178
+ // 9. Post-processing (Scale)
179
+ let finalContours = expandedUnpaddedContours;
180
+ if (options.scaleToWidth && options.scaleToHeight) {
181
+ finalContours = this.scaleContours(expandedUnpaddedContours, options.scaleToWidth, options.scaleToHeight, globalBounds);
182
+ globalBounds = this.boundsFromPoints(this.flattenContours(finalContours));
183
+ const baseScaledContours = this.scaleContours(baseUnpaddedContours, options.scaleToWidth, options.scaleToHeight, baseBounds);
184
+ baseBounds = this.boundsFromPoints(this.flattenContours(baseScaledContours));
185
+ }
186
+ // 10. Simplify and Generate SVG
187
+ const useSmoothing = options.smoothing !== false; // Default true
188
+ debugLog("traceWithBounds:contours", {
189
+ baseContourCount: baseContoursRaw.length,
190
+ baseSelectedCount: baseContours.length,
191
+ expandedContourCount: expandedContoursRaw.length,
192
+ expandedSelectedCount: expandedContours.length,
193
+ baseBounds,
194
+ expandedBounds: globalBounds,
195
+ expandedDeltaX: globalBounds.width - baseBounds.width,
196
+ expandedDeltaY: globalBounds.height - baseBounds.height,
197
+ expandedMayOverflowImageBounds: expand > 0,
198
+ useSmoothing,
199
+ componentMode,
200
+ });
201
+ if (useSmoothing) {
202
+ return {
203
+ pathData: this.contoursToSVGPaper(finalContours, options.simplifyTolerance ?? 2.5),
204
+ baseBounds,
205
+ bounds: globalBounds,
206
+ };
207
+ }
208
+ else {
209
+ const simplifiedContours = finalContours
210
+ .map((points) => this.douglasPeucker(points, options.simplifyTolerance ?? 2.0))
211
+ .filter((points) => points.length > 2);
212
+ const pathData = this.contoursToSVG(simplifiedContours) || this.contoursToSVG(finalContours);
213
+ return {
214
+ pathData,
215
+ baseBounds,
216
+ bounds: globalBounds,
217
+ };
218
+ }
219
+ }
220
+ static pickPrimaryContour(contours) {
221
+ if (contours.length === 0)
222
+ return null;
223
+ return contours.reduce((best, cur) => {
224
+ if (!best)
225
+ return cur;
226
+ const bestArea = Math.abs((0, maskOps_1.polygonSignedArea)(best));
227
+ const curArea = Math.abs((0, maskOps_1.polygonSignedArea)(cur));
228
+ if (curArea !== bestArea)
229
+ return curArea > bestArea ? cur : best;
230
+ return cur.length > best.length ? cur : best;
231
+ }, contours[0]);
232
+ }
233
+ static flattenContours(contours) {
234
+ return contours.flatMap((contour) => contour);
235
+ }
236
+ static contourCentroid(points) {
237
+ if (!points.length)
238
+ return { x: 0, y: 0 };
239
+ const sum = points.reduce((acc, p) => ({ x: acc.x + p.x, y: acc.y + p.y }), { x: 0, y: 0 });
240
+ return {
241
+ x: sum.x / points.length,
242
+ y: sum.y / points.length,
243
+ };
244
+ }
245
+ static pointInPolygon(point, polygon) {
246
+ let inside = false;
247
+ const { x, y } = point;
248
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
249
+ const xi = polygon[i].x;
250
+ const yi = polygon[i].y;
251
+ const xj = polygon[j].x;
252
+ const yj = polygon[j].y;
253
+ const intersects = yi > y !== yj > y &&
254
+ x < ((xj - xi) * (y - yi)) / ((yj - yi) || Number.EPSILON) + xi;
255
+ if (intersects)
256
+ inside = !inside;
257
+ }
258
+ return inside;
259
+ }
260
+ static keepOutermostContours(contours) {
261
+ if (contours.length <= 1)
262
+ return contours;
263
+ const sorted = [...contours].sort((a, b) => Math.abs((0, maskOps_1.polygonSignedArea)(b)) - Math.abs((0, maskOps_1.polygonSignedArea)(a)));
264
+ const selected = [];
265
+ for (const contour of sorted) {
266
+ const centroid = this.contourCentroid(contour);
267
+ const isNested = selected.some((outer) => this.pointInPolygon(centroid, outer));
268
+ if (!isNested) {
269
+ selected.push(contour);
270
+ }
271
+ }
272
+ return selected;
273
+ }
274
+ static selectContours(contours, mode, minComponentArea) {
275
+ if (!contours.length)
276
+ return [];
277
+ if (mode === "largest") {
278
+ const primary = this.pickPrimaryContour(contours);
279
+ return primary ? [primary] : [];
280
+ }
281
+ const threshold = Math.max(0, minComponentArea);
282
+ if (threshold <= 0) {
283
+ return this.keepOutermostContours(contours);
284
+ }
285
+ const filtered = contours.filter((contour) => Math.abs((0, maskOps_1.polygonSignedArea)(contour)) >= threshold);
286
+ if (filtered.length > 0) {
287
+ return this.keepOutermostContours(filtered);
288
+ }
289
+ const primary = this.pickPrimaryContour(contours);
290
+ return primary ? [primary] : [];
291
+ }
292
+ static boundsFromPoints(points) {
293
+ let minX = Infinity;
294
+ let minY = Infinity;
295
+ let maxX = -Infinity;
296
+ let maxY = -Infinity;
297
+ for (const p of points) {
298
+ if (p.x < minX)
299
+ minX = p.x;
300
+ if (p.y < minY)
301
+ minY = p.y;
302
+ if (p.x > maxX)
303
+ maxX = p.x;
304
+ if (p.y > maxY)
305
+ maxY = p.y;
306
+ }
307
+ if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
308
+ return { x: 0, y: 0, width: 0, height: 0 };
309
+ }
310
+ return {
311
+ x: minX,
312
+ y: minY,
313
+ width: maxX - minX,
314
+ height: maxY - minY,
315
+ };
316
+ }
317
+ /**
318
+ * Traces all contours in the mask with optimized start-point detection
319
+ */
320
+ static traceAllContours(mask, width, height) {
321
+ const visited = new Uint8Array(width * height);
322
+ const allContours = [];
323
+ for (let y = 0; y < height; y++) {
324
+ for (let x = 0; x < width; x++) {
325
+ const idx = y * width + x;
326
+ if (mask[idx] && !visited[idx]) {
327
+ // Only start a new trace if it's a potential outer boundary (left edge)
328
+ const isLeftEdge = x === 0 || mask[idx - 1] === 0;
329
+ if (isLeftEdge) {
330
+ const contour = this.marchingSquares(mask, visited, x, y, width, height);
331
+ if (contour.length > 2) {
332
+ allContours.push(contour);
333
+ }
334
+ }
335
+ }
336
+ }
337
+ }
338
+ return allContours;
339
+ }
340
+ static loadImage(url) {
341
+ return new Promise((resolve, reject) => {
342
+ const img = new Image();
343
+ img.crossOrigin = "Anonymous";
344
+ img.onload = () => resolve(img);
345
+ img.onerror = (e) => reject(e);
346
+ img.src = url;
347
+ });
348
+ }
349
+ /**
350
+ * Moore-Neighbor Tracing Algorithm
351
+ * More robust for irregular shapes than simple Marching Squares walker.
352
+ */
353
+ static marchingSquares(mask, visited, startX, startY, width, height) {
354
+ const isSolid = (x, y) => {
355
+ if (x < 0 || x >= width || y < 0 || y >= height)
356
+ return false;
357
+ return mask[y * width + x] === 1;
358
+ };
359
+ const points = [];
360
+ // Moore-Neighbor Tracing
361
+ // We enter from the Left (since we scan Left->Right), so "backtrack" is Left.
362
+ // B = (startX - 1, startY)
363
+ // P = (startX, startY)
364
+ let cx = startX;
365
+ let cy = startY;
366
+ // Start backtrack direction: Left (since we found it scanning from left)
367
+ // Directions: 0=Up, 1=UpRight, 2=Right, 3=DownRight, 4=Down, 5=DownLeft, 6=Left, 7=UpLeft
368
+ // Offsets for 8 neighbors starting from Up (0,-1) clockwise
369
+ const neighbors = [
370
+ { x: 0, y: -1 },
371
+ { x: 1, y: -1 },
372
+ { x: 1, y: 0 },
373
+ { x: 1, y: 1 },
374
+ { x: 0, y: 1 },
375
+ { x: -1, y: 1 },
376
+ { x: -1, y: 0 },
377
+ { x: -1, y: -1 },
378
+ ];
379
+ // Backtrack is Left -> Index 6.
380
+ let backtrack = 6;
381
+ const maxSteps = width * height * 3;
382
+ let steps = 0;
383
+ do {
384
+ points.push({ x: cx, y: cy });
385
+ visited[cy * width + cx] = 1; // Mark as visited to avoid re-starting here
386
+ // Search for next solid neighbor in clockwise order, starting from backtrack
387
+ let found = false;
388
+ for (let i = 0; i < 8; i++) {
389
+ const idx = (backtrack + 1 + i) % 8;
390
+ const nx = cx + neighbors[idx].x;
391
+ const ny = cy + neighbors[idx].y;
392
+ if (isSolid(nx, ny)) {
393
+ cx = nx;
394
+ cy = ny;
395
+ backtrack = (idx + 4 + 1) % 8;
396
+ found = true;
397
+ break;
398
+ }
399
+ }
400
+ if (!found)
401
+ break;
402
+ steps++;
403
+ } while ((cx !== startX || cy !== startY) && steps < maxSteps);
404
+ return points;
405
+ }
406
+ /**
407
+ * Douglas-Peucker Line Simplification
408
+ */
409
+ static douglasPeucker(points, tolerance) {
410
+ if (points.length <= 2)
411
+ return points;
412
+ const sqTolerance = tolerance * tolerance;
413
+ let maxSqDist = 0;
414
+ let index = 0;
415
+ const first = points[0];
416
+ const last = points[points.length - 1];
417
+ for (let i = 1; i < points.length - 1; i++) {
418
+ const sqDist = this.getSqSegDist(points[i], first, last);
419
+ if (sqDist > maxSqDist) {
420
+ index = i;
421
+ maxSqDist = sqDist;
422
+ }
423
+ }
424
+ if (maxSqDist > sqTolerance) {
425
+ // Check if closed loop?
426
+ // If closed loop, we shouldn't simplify start/end connection too much?
427
+ // Douglas-Peucker works on segments.
428
+ const left = this.douglasPeucker(points.slice(0, index + 1), tolerance);
429
+ const right = this.douglasPeucker(points.slice(index), tolerance);
430
+ return left.slice(0, left.length - 1).concat(right);
431
+ }
432
+ else {
433
+ return [first, last];
434
+ }
435
+ }
436
+ static getSqSegDist(p, p1, p2) {
437
+ let x = p1.x;
438
+ let y = p1.y;
439
+ let dx = p2.x - x;
440
+ let dy = p2.y - y;
441
+ if (dx !== 0 || dy !== 0) {
442
+ const t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy);
443
+ if (t > 1) {
444
+ x = p2.x;
445
+ y = p2.y;
446
+ }
447
+ else if (t > 0) {
448
+ x += dx * t;
449
+ y += dy * t;
450
+ }
451
+ }
452
+ dx = p.x - x;
453
+ dy = p.y - y;
454
+ return dx * dx + dy * dy;
455
+ }
456
+ static scalePoints(points, targetWidth, targetHeight, bounds) {
457
+ if (points.length === 0)
458
+ return points;
459
+ if (bounds.width === 0 || bounds.height === 0)
460
+ return points;
461
+ const scaleX = targetWidth / bounds.width;
462
+ const scaleY = targetHeight / bounds.height;
463
+ return points.map((p) => ({
464
+ x: (p.x - bounds.x) * scaleX,
465
+ y: (p.y - bounds.y) * scaleY,
466
+ }));
467
+ }
468
+ static scaleContours(contours, targetWidth, targetHeight, bounds) {
469
+ return contours.map((points) => this.scalePoints(points, targetWidth, targetHeight, bounds));
470
+ }
471
+ static clampPointsToImageBounds(points, width, height) {
472
+ const maxX = Math.max(0, width);
473
+ const maxY = Math.max(0, height);
474
+ return points.map((p) => ({
475
+ x: Math.max(0, Math.min(maxX, p.x)),
476
+ y: Math.max(0, Math.min(maxY, p.y)),
477
+ }));
478
+ }
479
+ static pointsToSVG(points) {
480
+ if (points.length === 0)
481
+ return "";
482
+ const head = points[0];
483
+ const tail = points.slice(1);
484
+ return (`M ${head.x} ${head.y} ` +
485
+ tail.map((p) => `L ${p.x} ${p.y}`).join(" ") +
486
+ " Z");
487
+ }
488
+ static contoursToSVG(contours) {
489
+ return contours
490
+ .filter((points) => points.length > 2)
491
+ .map((points) => this.pointsToSVG(points))
492
+ .join(" ")
493
+ .trim();
494
+ }
495
+ static ensurePaper() {
496
+ if (!paper_1.default.project) {
497
+ paper_1.default.setup(new paper_1.default.Size(100, 100));
498
+ }
499
+ }
500
+ static pointsToSVGPaper(points, tolerance) {
501
+ if (points.length < 3)
502
+ return this.pointsToSVG(points);
503
+ this.ensurePaper();
504
+ // Create Path
505
+ const path = new paper_1.default.Path({
506
+ segments: points.map(p => [p.x, p.y]),
507
+ closed: true
508
+ });
509
+ // Simplify
510
+ path.simplify(tolerance);
511
+ const data = path.pathData;
512
+ path.remove();
513
+ return data;
514
+ }
515
+ static contoursToSVGPaper(contours, tolerance) {
516
+ const normalizedContours = contours.filter((points) => points.length > 2);
517
+ if (!normalizedContours.length)
518
+ return "";
519
+ if (normalizedContours.length === 1) {
520
+ return this.pointsToSVGPaper(normalizedContours[0], tolerance);
521
+ }
522
+ this.ensurePaper();
523
+ const compound = new paper_1.default.CompoundPath({ insert: false });
524
+ for (const points of normalizedContours) {
525
+ const child = new paper_1.default.Path({
526
+ segments: points.map((p) => [p.x, p.y]),
527
+ closed: true,
528
+ insert: false,
529
+ });
530
+ child.simplify(tolerance);
531
+ compound.addChild(child);
532
+ }
533
+ const data = compound.pathData || this.contoursToSVG(normalizedContours);
534
+ compound.remove();
535
+ return data;
536
+ }
537
+ }
538
+ exports.ImageTracer = ImageTracer;