@pooder/kit 3.2.0 → 3.4.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.
package/dist/index.mjs CHANGED
@@ -215,7 +215,7 @@ var ImageTracer = class {
215
215
  * @param options Configuration options.
216
216
  */
217
217
  static async trace(imageUrl, options = {}) {
218
- var _a, _b;
218
+ var _a, _b, _c, _d, _e;
219
219
  const img = await this.loadImage(imageUrl);
220
220
  const width = img.width;
221
221
  const height = img.height;
@@ -226,21 +226,213 @@ var ImageTracer = class {
226
226
  if (!ctx) throw new Error("Could not get 2D context");
227
227
  ctx.drawImage(img, 0, 0);
228
228
  const imageData = ctx.getImageData(0, 0, width, height);
229
- const points = this.marchingSquares(imageData, (_a = options.threshold) != null ? _a : 10);
230
- let finalPoints = points;
231
- if (options.scaleToWidth && options.scaleToHeight && points.length > 0) {
229
+ const threshold = (_a = options.threshold) != null ? _a : 10;
230
+ const adaptiveRadius = Math.max(
231
+ 5,
232
+ Math.floor(Math.max(width, height) * 0.02)
233
+ );
234
+ const radius = (_b = options.morphologyRadius) != null ? _b : adaptiveRadius;
235
+ let mask = this.createMask(imageData, threshold);
236
+ if (radius > 0) {
237
+ mask = this.dilate(mask, width, height, radius);
238
+ mask = this.erode(mask, width, height, radius);
239
+ mask = this.fillHoles(mask, width, height);
240
+ }
241
+ const allContourPoints = this.traceAllContours(mask, width, height);
242
+ if (allContourPoints.length === 0) {
243
+ const w = (_c = options.scaleToWidth) != null ? _c : width;
244
+ const h = (_d = options.scaleToHeight) != null ? _d : height;
245
+ return `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`;
246
+ }
247
+ const primaryContour = allContourPoints.sort(
248
+ (a, b) => b.length - a.length
249
+ )[0];
250
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
251
+ for (const p of primaryContour) {
252
+ if (p.x < minX) minX = p.x;
253
+ if (p.y < minY) minY = p.y;
254
+ if (p.x > maxX) maxX = p.x;
255
+ if (p.y > maxY) maxY = p.y;
256
+ }
257
+ const globalBounds = {
258
+ minX,
259
+ minY,
260
+ width: maxX - minX,
261
+ height: maxY - minY
262
+ };
263
+ let finalPoints = primaryContour;
264
+ if (options.scaleToWidth && options.scaleToHeight) {
232
265
  finalPoints = this.scalePoints(
233
- points,
266
+ primaryContour,
234
267
  options.scaleToWidth,
235
- options.scaleToHeight
268
+ options.scaleToHeight,
269
+ globalBounds
236
270
  );
237
271
  }
238
272
  const simplifiedPoints = this.douglasPeucker(
239
273
  finalPoints,
240
- (_b = options.simplifyTolerance) != null ? _b : 0.5
274
+ (_e = options.simplifyTolerance) != null ? _e : 2
241
275
  );
242
276
  return this.pointsToSVG(simplifiedPoints);
243
277
  }
278
+ static createMask(imageData, threshold) {
279
+ const { width, height, data } = imageData;
280
+ const mask = new Uint8Array(width * height);
281
+ for (let i = 0; i < width * height; i++) {
282
+ const idx = i * 4;
283
+ const r = data[idx];
284
+ const g = data[idx + 1];
285
+ const b = data[idx + 2];
286
+ const a = data[idx + 3];
287
+ if (a > threshold && !(r > 240 && g > 240 && b > 240)) {
288
+ mask[i] = 1;
289
+ } else {
290
+ mask[i] = 0;
291
+ }
292
+ }
293
+ return mask;
294
+ }
295
+ /**
296
+ * Fast 1D-separable Dilation
297
+ */
298
+ static dilate(mask, width, height, radius) {
299
+ const horizontal = new Uint8Array(width * height);
300
+ for (let y = 0; y < height; y++) {
301
+ let count = 0;
302
+ for (let x = -radius; x < width; x++) {
303
+ if (x + radius < width && mask[y * width + x + radius]) count++;
304
+ if (x - radius - 1 >= 0 && mask[y * width + x - radius - 1]) count--;
305
+ if (x >= 0) horizontal[y * width + x] = count > 0 ? 1 : 0;
306
+ }
307
+ }
308
+ const vertical = new Uint8Array(width * height);
309
+ for (let x = 0; x < width; x++) {
310
+ let count = 0;
311
+ for (let y = -radius; y < height; y++) {
312
+ if (y + radius < height && horizontal[(y + radius) * width + x])
313
+ count++;
314
+ if (y - radius - 1 >= 0 && horizontal[(y - radius - 1) * width + x])
315
+ count--;
316
+ if (y >= 0) vertical[y * width + x] = count > 0 ? 1 : 0;
317
+ }
318
+ }
319
+ return vertical;
320
+ }
321
+ /**
322
+ * Fast 1D-separable Erosion
323
+ */
324
+ static erode(mask, width, height, radius) {
325
+ const horizontal = new Uint8Array(width * height);
326
+ for (let y = 0; y < height; y++) {
327
+ let count = 0;
328
+ for (let x = -radius; x < width; x++) {
329
+ if (x + radius < width && mask[y * width + x + radius]) count++;
330
+ if (x - radius - 1 >= 0 && mask[y * width + x - radius - 1]) count--;
331
+ if (x >= 0) {
332
+ const winWidth = Math.min(x + radius, width - 1) - Math.max(x - radius, 0) + 1;
333
+ horizontal[y * width + x] = count === winWidth ? 1 : 0;
334
+ }
335
+ }
336
+ }
337
+ const vertical = new Uint8Array(width * height);
338
+ for (let x = 0; x < width; x++) {
339
+ let count = 0;
340
+ for (let y = -radius; y < height; y++) {
341
+ if (y + radius < height && horizontal[(y + radius) * width + x])
342
+ count++;
343
+ if (y - radius - 1 >= 0 && horizontal[(y - radius - 1) * width + x])
344
+ count--;
345
+ if (y >= 0) {
346
+ const winHeight = Math.min(y + radius, height - 1) - Math.max(y - radius, 0) + 1;
347
+ vertical[y * width + x] = count === winHeight ? 1 : 0;
348
+ }
349
+ }
350
+ }
351
+ return vertical;
352
+ }
353
+ /**
354
+ * Fills internal holes in the binary mask using flood fill from edges.
355
+ */
356
+ static fillHoles(mask, width, height) {
357
+ const background = new Uint8Array(width * height);
358
+ const queue = [];
359
+ for (let x = 0; x < width; x++) {
360
+ if (mask[x] === 0) {
361
+ background[x] = 1;
362
+ queue.push([x, 0]);
363
+ }
364
+ const lastRow = (height - 1) * width + x;
365
+ if (mask[lastRow] === 0) {
366
+ background[lastRow] = 1;
367
+ queue.push([x, height - 1]);
368
+ }
369
+ }
370
+ for (let y = 1; y < height - 1; y++) {
371
+ if (mask[y * width] === 0) {
372
+ background[y * width] = 1;
373
+ queue.push([0, y]);
374
+ }
375
+ if (mask[y * width + width - 1] === 0) {
376
+ background[y * width + width - 1] = 1;
377
+ queue.push([width - 1, y]);
378
+ }
379
+ }
380
+ const dirs = [
381
+ [0, 1],
382
+ [0, -1],
383
+ [1, 0],
384
+ [-1, 0]
385
+ ];
386
+ let head = 0;
387
+ while (head < queue.length) {
388
+ const [cx, cy] = queue[head++];
389
+ for (const [dx, dy] of dirs) {
390
+ const nx = cx + dx;
391
+ const ny = cy + dy;
392
+ if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
393
+ const nidx = ny * width + nx;
394
+ if (mask[nidx] === 0 && background[nidx] === 0) {
395
+ background[nidx] = 1;
396
+ queue.push([nx, ny]);
397
+ }
398
+ }
399
+ }
400
+ }
401
+ const filledMask = new Uint8Array(width * height);
402
+ for (let i = 0; i < width * height; i++) {
403
+ filledMask[i] = background[i] === 0 ? 1 : 0;
404
+ }
405
+ return filledMask;
406
+ }
407
+ /**
408
+ * Traces all contours in the mask with optimized start-point detection
409
+ */
410
+ static traceAllContours(mask, width, height) {
411
+ const visited = new Uint8Array(width * height);
412
+ const allContours = [];
413
+ for (let y = 0; y < height; y++) {
414
+ for (let x = 0; x < width; x++) {
415
+ const idx = y * width + x;
416
+ if (mask[idx] && !visited[idx]) {
417
+ const isLeftEdge = x === 0 || mask[idx - 1] === 0;
418
+ if (isLeftEdge) {
419
+ const contour = this.marchingSquares(
420
+ mask,
421
+ visited,
422
+ x,
423
+ y,
424
+ width,
425
+ height
426
+ );
427
+ if (contour.length > 2) {
428
+ allContours.push(contour);
429
+ }
430
+ }
431
+ }
432
+ }
433
+ }
434
+ return allContours;
435
+ }
244
436
  static loadImage(url) {
245
437
  return new Promise((resolve, reject) => {
246
438
  const img = new Image();
@@ -254,33 +446,11 @@ var ImageTracer = class {
254
446
  * Moore-Neighbor Tracing Algorithm
255
447
  * More robust for irregular shapes than simple Marching Squares walker.
256
448
  */
257
- static marchingSquares(imageData, alphaThreshold) {
258
- const width = imageData.width;
259
- const height = imageData.height;
260
- const data = imageData.data;
449
+ static marchingSquares(mask, visited, startX, startY, width, height) {
261
450
  const isSolid = (x, y) => {
262
451
  if (x < 0 || x >= width || y < 0 || y >= height) return false;
263
- const index = (y * width + x) * 4;
264
- const r = data[index];
265
- const g = data[index + 1];
266
- const b = data[index + 2];
267
- const a = data[index + 3];
268
- if (a <= alphaThreshold) return false;
269
- if (r > 240 && g > 240 && b > 240) return false;
270
- return true;
452
+ return mask[y * width + x] === 1;
271
453
  };
272
- let startX = -1;
273
- let startY = -1;
274
- searchLoop: for (let y = 0; y < height; y++) {
275
- for (let x = 0; x < width; x++) {
276
- if (isSolid(x, y)) {
277
- startX = x;
278
- startY = y;
279
- break searchLoop;
280
- }
281
- }
282
- }
283
- if (startX === -1) return [];
284
454
  const points = [];
285
455
  let cx = startX;
286
456
  let cy = startY;
@@ -299,6 +469,7 @@ var ImageTracer = class {
299
469
  let steps = 0;
300
470
  do {
301
471
  points.push({ x: cx, y: cy });
472
+ visited[cy * width + cx] = 1;
302
473
  let found = false;
303
474
  for (let i = 0; i < 8; i++) {
304
475
  const idx = (backtrack + 1 + i) % 8;
@@ -307,16 +478,12 @@ var ImageTracer = class {
307
478
  if (isSolid(nx, ny)) {
308
479
  cx = nx;
309
480
  cy = ny;
310
- backtrack = (idx + 4) % 8;
311
- backtrack = (idx + 4 + 1) % 8;
312
481
  backtrack = (idx + 4 + 1) % 8;
313
482
  found = true;
314
483
  break;
315
484
  }
316
485
  }
317
- if (!found) {
318
- break;
319
- }
486
+ if (!found) break;
320
487
  steps++;
321
488
  } while ((cx !== startX || cy !== startY) && steps < maxSteps);
322
489
  return points;
@@ -365,23 +532,14 @@ var ImageTracer = class {
365
532
  dy = p.y - y;
366
533
  return dx * dx + dy * dy;
367
534
  }
368
- static scalePoints(points, targetWidth, targetHeight) {
535
+ static scalePoints(points, targetWidth, targetHeight, bounds) {
369
536
  if (points.length === 0) return points;
370
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
371
- for (const p of points) {
372
- if (p.x < minX) minX = p.x;
373
- if (p.y < minY) minY = p.y;
374
- if (p.x > maxX) maxX = p.x;
375
- if (p.y > maxY) maxY = p.y;
376
- }
377
- const srcW = maxX - minX;
378
- const srcH = maxY - minY;
379
- if (srcW === 0 || srcH === 0) return points;
380
- const scaleX = targetWidth / srcW;
381
- const scaleY = targetHeight / srcH;
537
+ if (bounds.width === 0 || bounds.height === 0) return points;
538
+ const scaleX = targetWidth / bounds.width;
539
+ const scaleY = targetHeight / bounds.height;
382
540
  return points.map((p) => ({
383
- x: (p.x - minX) * scaleX,
384
- y: (p.y - minY) * scaleY
541
+ x: (p.x - bounds.minX) * scaleX,
542
+ y: (p.y - bounds.minY) * scaleY
385
543
  }));
386
544
  }
387
545
  static pointsToSVG(points) {
@@ -574,12 +732,25 @@ function getDielineShape(options) {
574
732
  let lugsPath = null;
575
733
  let cutsPath = null;
576
734
  holes.forEach((hole) => {
577
- const lug = new paper.Path.Circle({
578
- center: [hole.x, hole.y],
735
+ const center = new paper.Point(hole.x, hole.y);
736
+ const lug = hole.shape === "square" ? new paper.Path.Rectangle({
737
+ point: [
738
+ center.x - hole.outerRadius,
739
+ center.y - hole.outerRadius
740
+ ],
741
+ size: [hole.outerRadius * 2, hole.outerRadius * 2]
742
+ }) : new paper.Path.Circle({
743
+ center,
579
744
  radius: hole.outerRadius
580
745
  });
581
- const cut = new paper.Path.Circle({
582
- center: [hole.x, hole.y],
746
+ const cut = hole.shape === "square" ? new paper.Path.Rectangle({
747
+ point: [
748
+ center.x - hole.innerRadius,
749
+ center.y - hole.innerRadius
750
+ ],
751
+ size: [hole.innerRadius * 2, hole.innerRadius * 2]
752
+ }) : new paper.Path.Circle({
753
+ center,
583
754
  radius: hole.innerRadius
584
755
  });
585
756
  if (!lugsPath) {
@@ -636,7 +807,9 @@ function getDielineShape(options) {
636
807
  return mainShape;
637
808
  }
638
809
  function generateDielinePath(options) {
639
- ensurePaper(options.width * 2, options.height * 2);
810
+ const paperWidth = options.canvasWidth || options.width * 2 || 2e3;
811
+ const paperHeight = options.canvasHeight || options.height * 2 || 2e3;
812
+ ensurePaper(paperWidth, paperHeight);
640
813
  paper.project.activeLayer.removeChildren();
641
814
  const mainShape = getDielineShape(options);
642
815
  const pathData = mainShape.pathData;
@@ -660,8 +833,9 @@ function generateMaskPath(options) {
660
833
  return pathData;
661
834
  }
662
835
  function generateBleedZonePath(options, offset) {
663
- const maxDim = Math.max(options.width, options.height) + Math.abs(offset) * 4;
664
- ensurePaper(maxDim, maxDim);
836
+ const paperWidth = options.canvasWidth || options.width * 2 || 2e3;
837
+ const paperHeight = options.canvasHeight || options.height * 2 || 2e3;
838
+ ensurePaper(paperWidth, paperHeight);
665
839
  paper.project.activeLayer.removeChildren();
666
840
  const shapeOriginal = getDielineShape(options);
667
841
  let shapeOffset;
@@ -1105,7 +1279,9 @@ var DielineTool = class {
1105
1279
  x: cx,
1106
1280
  y: cy,
1107
1281
  holes: absoluteHoles,
1108
- pathData: this.pathData
1282
+ pathData: this.pathData,
1283
+ canvasWidth: canvasW,
1284
+ canvasHeight: canvasH
1109
1285
  });
1110
1286
  const insideObj = new Path(productPathData, {
1111
1287
  fill: insideColor,
@@ -1128,7 +1304,9 @@ var DielineTool = class {
1128
1304
  x: cx,
1129
1305
  y: cy,
1130
1306
  holes: absoluteHoles,
1131
- pathData: this.pathData
1307
+ pathData: this.pathData,
1308
+ canvasWidth: canvasW,
1309
+ canvasHeight: canvasH
1132
1310
  },
1133
1311
  visualOffset
1134
1312
  );
@@ -1155,7 +1333,9 @@ var DielineTool = class {
1155
1333
  x: cx,
1156
1334
  y: cy,
1157
1335
  holes: absoluteHoles,
1158
- pathData: this.pathData
1336
+ pathData: this.pathData,
1337
+ canvasWidth: canvasW,
1338
+ canvasHeight: canvasH
1159
1339
  });
1160
1340
  const offsetBorderObj = new Path(offsetPathData, {
1161
1341
  fill: null,
@@ -1179,7 +1359,9 @@ var DielineTool = class {
1179
1359
  x: cx,
1180
1360
  y: cy,
1181
1361
  holes: absoluteHoles,
1182
- pathData: this.pathData
1362
+ pathData: this.pathData,
1363
+ canvasWidth: canvasW,
1364
+ canvasHeight: canvasH
1183
1365
  });
1184
1366
  const borderObj = new Path(borderPathData, {
1185
1367
  fill: "transparent",
@@ -1294,7 +1476,9 @@ var DielineTool = class {
1294
1476
  x: cx,
1295
1477
  y: cy,
1296
1478
  holes: absoluteHoles,
1297
- pathData: this.pathData
1479
+ pathData: this.pathData,
1480
+ canvasWidth: canvasW,
1481
+ canvasHeight: canvasH
1298
1482
  });
1299
1483
  const clonedLayer = await userLayer.clone();
1300
1484
  const clipPath = new Path(pathData, {
@@ -1485,7 +1669,7 @@ var FilmTool = class {
1485
1669
  import {
1486
1670
  ContributionPointIds as ContributionPointIds4
1487
1671
  } from "@pooder/core";
1488
- import { Circle, Group, Point } from "fabric";
1672
+ import { Circle, Group, Point, Rect as Rect2 } from "fabric";
1489
1673
  var HoleTool = class {
1490
1674
  constructor(options) {
1491
1675
  this.id = "pooder.kit.hole";
@@ -1591,7 +1775,7 @@ var HoleTool = class {
1591
1775
  command: "addHole",
1592
1776
  title: "Add Hole",
1593
1777
  handler: (x, y) => {
1594
- var _a, _b, _c;
1778
+ var _a, _b, _c, _d;
1595
1779
  if (!this.canvasService) return false;
1596
1780
  let normalizedX = 0.5;
1597
1781
  let normalizedY = 0.5;
@@ -1614,9 +1798,11 @@ var HoleTool = class {
1614
1798
  const lastHole = currentHoles[currentHoles.length - 1];
1615
1799
  const innerRadius = (_b = lastHole == null ? void 0 : lastHole.innerRadius) != null ? _b : 15;
1616
1800
  const outerRadius = (_c = lastHole == null ? void 0 : lastHole.outerRadius) != null ? _c : 25;
1801
+ const shape = (_d = lastHole == null ? void 0 : lastHole.shape) != null ? _d : "circle";
1617
1802
  const newHole = {
1618
1803
  x: normalizedX,
1619
1804
  y: normalizedY,
1805
+ shape,
1620
1806
  innerRadius,
1621
1807
  outerRadius
1622
1808
  };
@@ -1648,6 +1834,7 @@ var HoleTool = class {
1648
1834
  if (!this.handleDielineChange) {
1649
1835
  this.handleDielineChange = (geometry) => {
1650
1836
  this.currentGeometry = geometry;
1837
+ this.redraw();
1651
1838
  const changed = this.enforceConstraints();
1652
1839
  if (changed) {
1653
1840
  this.syncHolesToDieline();
@@ -1761,10 +1948,16 @@ var HoleTool = class {
1761
1948
  }
1762
1949
  syncHolesFromCanvas() {
1763
1950
  if (!this.canvasService) return;
1764
- const objects = this.canvasService.canvas.getObjects().filter((obj) => {
1765
- var _a;
1766
- return ((_a = obj.data) == null ? void 0 : _a.type) === "hole-marker";
1767
- });
1951
+ const objects = this.canvasService.canvas.getObjects().filter(
1952
+ (obj) => {
1953
+ var _a;
1954
+ return ((_a = obj.data) == null ? void 0 : _a.type) === "hole-marker" || obj.name === "hole-marker";
1955
+ }
1956
+ );
1957
+ if (objects.length === 0 && this.holes.length > 0) {
1958
+ console.warn("HoleTool: No markers found on canvas to sync from");
1959
+ return;
1960
+ }
1768
1961
  objects.sort(
1769
1962
  (a, b) => {
1770
1963
  var _a, _b, _c, _d;
@@ -1776,6 +1969,13 @@ var HoleTool = class {
1776
1969
  const original = this.holes[i];
1777
1970
  const newAbsX = obj.left;
1778
1971
  const newAbsY = obj.top;
1972
+ if (isNaN(newAbsX) || isNaN(newAbsY)) {
1973
+ console.error("HoleTool: Invalid marker coordinates", {
1974
+ newAbsX,
1975
+ newAbsY
1976
+ });
1977
+ return original;
1978
+ }
1779
1979
  const scale = ((_a = this.currentGeometry) == null ? void 0 : _a.scale) || 1;
1780
1980
  const unit = ((_b = this.currentGeometry) == null ? void 0 : _b.unit) || "mm";
1781
1981
  const unitScale = Coordinate.convertUnit(1, "mm", unit);
@@ -1832,7 +2032,11 @@ var HoleTool = class {
1832
2032
  offsetY: (newAbsY - by) / scale / unitScale,
1833
2033
  // Clear direct coordinates if we use anchor
1834
2034
  x: void 0,
1835
- y: void 0
2035
+ y: void 0,
2036
+ // Ensure other properties are preserved
2037
+ innerRadius: original.innerRadius,
2038
+ outerRadius: original.outerRadius,
2039
+ shape: original.shape || "circle"
1836
2040
  };
1837
2041
  }
1838
2042
  let normalizedX = 0.5;
@@ -1852,9 +2056,13 @@ var HoleTool = class {
1852
2056
  ...original,
1853
2057
  x: normalizedX,
1854
2058
  y: normalizedY,
1855
- // Ensure radii are preserved
2059
+ // Clear offsets if we are using direct normalized coordinates
2060
+ offsetX: void 0,
2061
+ offsetY: void 0,
2062
+ // Ensure other properties are preserved
1856
2063
  innerRadius: (_c = original == null ? void 0 : original.innerRadius) != null ? _c : 15,
1857
- outerRadius: (_d = original == null ? void 0 : original.outerRadius) != null ? _d : 25
2064
+ outerRadius: (_d = original == null ? void 0 : original.outerRadius) != null ? _d : 25,
2065
+ shape: (original == null ? void 0 : original.shape) || "circle"
1858
2066
  };
1859
2067
  });
1860
2068
  this.holes = newHoles;
@@ -1912,7 +2120,16 @@ var HoleTool = class {
1912
2120
  { width: geometry.width, height: geometry.height }
1913
2121
  // Use geometry dims instead of canvas
1914
2122
  );
1915
- const innerCircle = new Circle({
2123
+ const isSquare = hole.shape === "square";
2124
+ const innerMarker = isSquare ? new Rect2({
2125
+ width: visualInnerRadius * 2,
2126
+ height: visualInnerRadius * 2,
2127
+ fill: "transparent",
2128
+ stroke: "red",
2129
+ strokeWidth: 2,
2130
+ originX: "center",
2131
+ originY: "center"
2132
+ }) : new Circle({
1916
2133
  radius: visualInnerRadius,
1917
2134
  fill: "transparent",
1918
2135
  stroke: "red",
@@ -1920,7 +2137,16 @@ var HoleTool = class {
1920
2137
  originX: "center",
1921
2138
  originY: "center"
1922
2139
  });
1923
- const outerCircle = new Circle({
2140
+ const outerMarker = isSquare ? new Rect2({
2141
+ width: visualOuterRadius * 2,
2142
+ height: visualOuterRadius * 2,
2143
+ fill: "transparent",
2144
+ stroke: "#666",
2145
+ strokeWidth: 1,
2146
+ strokeDashArray: [5, 5],
2147
+ originX: "center",
2148
+ originY: "center"
2149
+ }) : new Circle({
1924
2150
  radius: visualOuterRadius,
1925
2151
  fill: "transparent",
1926
2152
  stroke: "#666",
@@ -1929,7 +2155,7 @@ var HoleTool = class {
1929
2155
  originX: "center",
1930
2156
  originY: "center"
1931
2157
  });
1932
- const holeGroup = new Group([outerCircle, innerCircle], {
2158
+ const holeGroup = new Group([outerMarker, innerMarker], {
1933
2159
  left: pos.x,
1934
2160
  top: pos.y,
1935
2161
  originX: "center",
@@ -2073,6 +2299,7 @@ var ImageTool = class {
2073
2299
  };
2074
2300
  this.items = [];
2075
2301
  this.objectMap = /* @__PURE__ */ new Map();
2302
+ this.loadResolvers = /* @__PURE__ */ new Map();
2076
2303
  this.isUpdatingConfig = false;
2077
2304
  }
2078
2305
  activate(context) {
@@ -2082,19 +2309,15 @@ var ImageTool = class {
2082
2309
  console.warn("CanvasService not found for ImageTool");
2083
2310
  return;
2084
2311
  }
2085
- const configService = context.services.get("ConfigurationService");
2312
+ const configService = context.services.get(
2313
+ "ConfigurationService"
2314
+ );
2086
2315
  if (configService) {
2087
2316
  this.items = configService.get("image.items", []) || [];
2088
2317
  configService.onAnyChange((e) => {
2089
2318
  if (this.isUpdatingConfig) return;
2090
- let shouldUpdate = false;
2091
2319
  if (e.key === "image.items") {
2092
2320
  this.items = e.value || [];
2093
- shouldUpdate = true;
2094
- } else if (e.key.startsWith("dieline.") && e.key !== "dieline.holes") {
2095
- shouldUpdate = true;
2096
- }
2097
- if (shouldUpdate) {
2098
2321
  this.updateImages();
2099
2322
  }
2100
2323
  });
@@ -2130,15 +2353,39 @@ var ImageTool = class {
2130
2353
  {
2131
2354
  command: "addImage",
2132
2355
  title: "Add Image",
2133
- handler: (url, options) => {
2356
+ handler: async (url, options) => {
2357
+ const id = this.generateId();
2134
2358
  const newItem = {
2135
- id: this.generateId(),
2359
+ id,
2136
2360
  url,
2137
2361
  opacity: 1,
2138
2362
  ...options
2139
2363
  };
2364
+ const promise = new Promise((resolve) => {
2365
+ this.loadResolvers.set(id, () => resolve(id));
2366
+ });
2140
2367
  this.updateConfig([...this.items, newItem]);
2141
- return newItem.id;
2368
+ return promise;
2369
+ }
2370
+ },
2371
+ {
2372
+ command: "fitImageToArea",
2373
+ title: "Fit Image to Area",
2374
+ handler: (id, area) => {
2375
+ var _a, _b;
2376
+ const item = this.items.find((i) => i.id === id);
2377
+ const obj = this.objectMap.get(id);
2378
+ if (item && obj && obj.width && obj.height) {
2379
+ const scale = Math.max(
2380
+ area.width / obj.width,
2381
+ area.height / obj.height
2382
+ );
2383
+ this.updateImageInConfig(id, {
2384
+ scale,
2385
+ left: (_a = area.left) != null ? _a : 0.5,
2386
+ top: (_b = area.top) != null ? _b : 0.5
2387
+ });
2388
+ }
2142
2389
  }
2143
2390
  },
2144
2391
  {
@@ -2206,7 +2453,9 @@ var ImageTool = class {
2206
2453
  if (!this.context) return;
2207
2454
  this.isUpdatingConfig = true;
2208
2455
  this.items = newItems;
2209
- const configService = this.context.services.get("ConfigurationService");
2456
+ const configService = this.context.services.get(
2457
+ "ConfigurationService"
2458
+ );
2210
2459
  if (configService) {
2211
2460
  configService.update("image.items", newItems);
2212
2461
  }
@@ -2252,39 +2501,12 @@ var ImageTool = class {
2252
2501
  var _a, _b;
2253
2502
  const canvasW = ((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 800;
2254
2503
  const canvasH = ((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 600;
2255
- let layoutScale = 1;
2256
- let layoutOffsetX = 0;
2257
- let layoutOffsetY = 0;
2258
- let visualWidth = canvasW;
2259
- let visualHeight = canvasH;
2260
- let dielinePhysicalWidth = 500;
2261
- let dielinePhysicalHeight = 500;
2262
- if (this.context) {
2263
- const configService = this.context.services.get("ConfigurationService");
2264
- if (configService) {
2265
- dielinePhysicalWidth = configService.get("dieline.width") || 500;
2266
- dielinePhysicalHeight = configService.get("dieline.height") || 500;
2267
- const padding = configService.get("dieline.padding") || 40;
2268
- const layout = Coordinate.calculateLayout(
2269
- { width: canvasW, height: canvasH },
2270
- { width: dielinePhysicalWidth, height: dielinePhysicalHeight },
2271
- padding
2272
- );
2273
- layoutScale = layout.scale;
2274
- layoutOffsetX = layout.offsetX;
2275
- layoutOffsetY = layout.offsetY;
2276
- visualWidth = layout.width;
2277
- visualHeight = layout.height;
2278
- }
2279
- }
2280
2504
  return {
2281
- layoutScale,
2282
- layoutOffsetX,
2283
- layoutOffsetY,
2284
- visualWidth,
2285
- visualHeight,
2286
- dielinePhysicalWidth,
2287
- dielinePhysicalHeight
2505
+ layoutScale: 1,
2506
+ layoutOffsetX: 0,
2507
+ layoutOffsetY: 0,
2508
+ visualWidth: canvasW,
2509
+ visualHeight: canvasH
2288
2510
  };
2289
2511
  }
2290
2512
  updateImages() {
@@ -2307,8 +2529,8 @@ var ImageTool = class {
2307
2529
  if (!obj) {
2308
2530
  this.loadImage(item, layer, layout);
2309
2531
  } else {
2310
- this.updateObjectProperties(obj, item, layout);
2311
2532
  layer.remove(obj);
2533
+ this.updateObjectProperties(obj, item, layout);
2312
2534
  layer.add(obj);
2313
2535
  }
2314
2536
  });
@@ -2316,10 +2538,17 @@ var ImageTool = class {
2316
2538
  this.canvasService.requestRenderAll();
2317
2539
  }
2318
2540
  updateObjectProperties(obj, item, layout) {
2319
- const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight } = layout;
2541
+ const {
2542
+ layoutScale,
2543
+ layoutOffsetX,
2544
+ layoutOffsetY,
2545
+ visualWidth,
2546
+ visualHeight
2547
+ } = layout;
2320
2548
  const updates = {};
2321
2549
  if (obj.opacity !== item.opacity) updates.opacity = item.opacity;
2322
- if (item.angle !== void 0 && obj.angle !== item.angle) updates.angle = item.angle;
2550
+ if (item.angle !== void 0 && obj.angle !== item.angle)
2551
+ updates.angle = item.angle;
2323
2552
  if (item.left !== void 0) {
2324
2553
  const globalLeft = layoutOffsetX + item.left * visualWidth;
2325
2554
  if (Math.abs(obj.left - globalLeft) > 1) updates.left = globalLeft;
@@ -2328,13 +2557,12 @@ var ImageTool = class {
2328
2557
  const globalTop = layoutOffsetY + item.top * visualHeight;
2329
2558
  if (Math.abs(obj.top - globalTop) > 1) updates.top = globalTop;
2330
2559
  }
2331
- if (item.width !== void 0 && obj.width) {
2332
- const targetScaleX = item.width * layoutScale / obj.width;
2333
- if (Math.abs(obj.scaleX - targetScaleX) > 1e-3) updates.scaleX = targetScaleX;
2334
- }
2335
- if (item.height !== void 0 && obj.height) {
2336
- const targetScaleY = item.height * layoutScale / obj.height;
2337
- if (Math.abs(obj.scaleY - targetScaleY) > 1e-3) updates.scaleY = targetScaleY;
2560
+ if (item.scale !== void 0) {
2561
+ const targetScale = item.scale * layoutScale;
2562
+ if (Math.abs(obj.scaleX - targetScale) > 1e-3) {
2563
+ updates.scaleX = targetScale;
2564
+ updates.scaleY = targetScale;
2565
+ }
2338
2566
  }
2339
2567
  if (obj.originX !== "center") {
2340
2568
  updates.originX = "center";
@@ -2342,6 +2570,7 @@ var ImageTool = class {
2342
2570
  }
2343
2571
  if (Object.keys(updates).length > 0) {
2344
2572
  obj.set(updates);
2573
+ obj.setCoords();
2345
2574
  }
2346
2575
  }
2347
2576
  loadImage(item, layer, layout) {
@@ -2351,24 +2580,20 @@ var ImageTool = class {
2351
2580
  image.set({
2352
2581
  originX: "center",
2353
2582
  originY: "center",
2354
- data: { id: item.id }
2583
+ data: { id: item.id },
2584
+ uniformScaling: true,
2585
+ lockScalingFlip: true
2355
2586
  });
2356
- let { width, height, left, top } = item;
2357
- const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight, dielinePhysicalWidth, dielinePhysicalHeight } = layout;
2358
- if (width === void 0 && height === void 0) {
2359
- const imgAspect = (image.width || 1) / (image.height || 1);
2360
- const dielineAspect = dielinePhysicalWidth / dielinePhysicalHeight;
2361
- if (imgAspect > dielineAspect) {
2362
- const w = dielinePhysicalWidth;
2363
- width = w;
2364
- height = w / imgAspect;
2365
- } else {
2366
- const h = dielinePhysicalHeight;
2367
- height = h;
2368
- width = h * imgAspect;
2369
- }
2370
- item.width = width;
2371
- item.height = height;
2587
+ image.setControlsVisibility({
2588
+ mt: false,
2589
+ mb: false,
2590
+ ml: false,
2591
+ mr: false
2592
+ });
2593
+ let { scale, left, top } = item;
2594
+ if (scale === void 0) {
2595
+ scale = 1;
2596
+ item.scale = scale;
2372
2597
  }
2373
2598
  if (left === void 0 && top === void 0) {
2374
2599
  left = 0.5;
@@ -2379,13 +2604,18 @@ var ImageTool = class {
2379
2604
  this.updateObjectProperties(image, item, layout);
2380
2605
  layer.add(image);
2381
2606
  this.objectMap.set(item.id, image);
2607
+ const resolver = this.loadResolvers.get(item.id);
2608
+ if (resolver) {
2609
+ resolver();
2610
+ this.loadResolvers.delete(item.id);
2611
+ }
2382
2612
  image.on("modified", (e) => {
2383
2613
  this.handleObjectModified(item.id, image);
2384
2614
  });
2385
2615
  layer.dirty = true;
2386
2616
  (_a = this.canvasService) == null ? void 0 : _a.requestRenderAll();
2387
- if (item.width !== width || item.height !== height || item.left !== left || item.top !== top) {
2388
- this.updateImageInConfig(item.id, { width, height, left, top });
2617
+ if (item.scale !== scale || item.left !== left || item.top !== top) {
2618
+ this.updateImageInConfig(item.id, { scale, left, top }, true);
2389
2619
  }
2390
2620
  }).catch((err) => {
2391
2621
  console.error("Failed to load image", item.url, err);
@@ -2393,29 +2623,28 @@ var ImageTool = class {
2393
2623
  }
2394
2624
  handleObjectModified(id, image) {
2395
2625
  const layout = this.getLayoutInfo();
2396
- const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight } = layout;
2626
+ const {
2627
+ layoutScale,
2628
+ layoutOffsetX,
2629
+ layoutOffsetY,
2630
+ visualWidth,
2631
+ visualHeight
2632
+ } = layout;
2397
2633
  const matrix = image.calcTransformMatrix();
2398
2634
  const globalPoint = util.transformPoint(new Point2(0, 0), matrix);
2399
2635
  const updates = {};
2400
2636
  updates.left = (globalPoint.x - layoutOffsetX) / visualWidth;
2401
2637
  updates.top = (globalPoint.y - layoutOffsetY) / visualHeight;
2402
2638
  updates.angle = image.angle;
2403
- if (image.width) {
2404
- const pixelWidth = image.width * image.scaleX;
2405
- updates.width = pixelWidth / layoutScale;
2406
- }
2407
- if (image.height) {
2408
- const pixelHeight = image.height * image.scaleY;
2409
- updates.height = pixelHeight / layoutScale;
2410
- }
2411
- this.updateImageInConfig(id, updates);
2639
+ updates.scale = image.scaleX / layoutScale;
2640
+ this.updateImageInConfig(id, updates, true);
2412
2641
  }
2413
- updateImageInConfig(id, updates) {
2642
+ updateImageInConfig(id, updates, skipCanvasUpdate = false) {
2414
2643
  const index = this.items.findIndex((i) => i.id === id);
2415
2644
  if (index !== -1) {
2416
2645
  const newItems = [...this.items];
2417
2646
  newItems[index] = { ...newItems[index], ...updates };
2418
- this.updateConfig(newItems, true);
2647
+ this.updateConfig(newItems, skipCanvasUpdate);
2419
2648
  }
2420
2649
  }
2421
2650
  };