@pooder/kit 3.3.0 → 3.5.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.js CHANGED
@@ -33,8 +33,8 @@ __export(index_exports, {
33
33
  BackgroundTool: () => BackgroundTool,
34
34
  CanvasService: () => CanvasService,
35
35
  DielineTool: () => DielineTool,
36
+ FeatureTool: () => FeatureTool,
36
37
  FilmTool: () => FilmTool,
37
- HoleTool: () => HoleTool,
38
38
  ImageTool: () => ImageTool,
39
39
  MirrorTool: () => MirrorTool,
40
40
  RulerTool: () => RulerTool,
@@ -248,6 +248,7 @@ var import_core2 = require("@pooder/core");
248
248
  var import_fabric2 = require("fabric");
249
249
 
250
250
  // src/tracer.ts
251
+ var import_paper = __toESM(require("paper"));
251
252
  var ImageTracer = class {
252
253
  /**
253
254
  * Main entry point: Traces an image URL to an SVG path string.
@@ -255,7 +256,7 @@ var ImageTracer = class {
255
256
  * @param options Configuration options.
256
257
  */
257
258
  static async trace(imageUrl, options = {}) {
258
- var _a, _b;
259
+ var _a, _b, _c, _d, _e, _f, _g;
259
260
  const img = await this.loadImage(imageUrl);
260
261
  const width = img.width;
261
262
  const height = img.height;
@@ -266,20 +267,250 @@ var ImageTracer = class {
266
267
  if (!ctx) throw new Error("Could not get 2D context");
267
268
  ctx.drawImage(img, 0, 0);
268
269
  const imageData = ctx.getImageData(0, 0, width, height);
269
- const points = this.marchingSquares(imageData, (_a = options.threshold) != null ? _a : 10);
270
- let finalPoints = points;
271
- if (options.scaleToWidth && options.scaleToHeight && points.length > 0) {
270
+ const threshold = (_a = options.threshold) != null ? _a : 10;
271
+ const adaptiveRadius = Math.max(
272
+ 5,
273
+ Math.floor(Math.max(width, height) * 0.02)
274
+ );
275
+ const radius = (_b = options.morphologyRadius) != null ? _b : adaptiveRadius;
276
+ const expand = (_c = options.expand) != null ? _c : 0;
277
+ const padding = radius + expand + 2;
278
+ const paddedWidth = width + padding * 2;
279
+ const paddedHeight = height + padding * 2;
280
+ let mask = this.createMask(imageData, threshold, padding, paddedWidth, paddedHeight);
281
+ if (radius > 0) {
282
+ mask = this.circularMorphology(mask, paddedWidth, paddedHeight, radius, "closing");
283
+ mask = this.fillHoles(mask, paddedWidth, paddedHeight);
284
+ const smoothRadius = Math.max(2, Math.floor(radius * 0.3));
285
+ mask = this.circularMorphology(mask, paddedWidth, paddedHeight, smoothRadius, "closing");
286
+ } else {
287
+ mask = this.fillHoles(mask, paddedWidth, paddedHeight);
288
+ }
289
+ if (expand > 0) {
290
+ mask = this.circularMorphology(mask, paddedWidth, paddedHeight, expand, "dilate");
291
+ }
292
+ const allContourPoints = this.traceAllContours(mask, paddedWidth, paddedHeight);
293
+ if (allContourPoints.length === 0) {
294
+ const w = (_d = options.scaleToWidth) != null ? _d : width;
295
+ const h = (_e = options.scaleToHeight) != null ? _e : height;
296
+ return `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`;
297
+ }
298
+ const primaryContour = allContourPoints.sort(
299
+ (a, b) => b.length - a.length
300
+ )[0];
301
+ const unpaddedPoints = primaryContour.map((p) => ({
302
+ x: p.x - padding,
303
+ y: p.y - padding
304
+ }));
305
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
306
+ for (const p of unpaddedPoints) {
307
+ if (p.x < minX) minX = p.x;
308
+ if (p.y < minY) minY = p.y;
309
+ if (p.x > maxX) maxX = p.x;
310
+ if (p.y > maxY) maxY = p.y;
311
+ }
312
+ const globalBounds = {
313
+ minX,
314
+ minY,
315
+ width: maxX - minX,
316
+ height: maxY - minY
317
+ };
318
+ let finalPoints = unpaddedPoints;
319
+ if (options.scaleToWidth && options.scaleToHeight) {
272
320
  finalPoints = this.scalePoints(
273
- points,
321
+ unpaddedPoints,
274
322
  options.scaleToWidth,
275
- options.scaleToHeight
323
+ options.scaleToHeight,
324
+ globalBounds
276
325
  );
277
326
  }
278
- const simplifiedPoints = this.douglasPeucker(
279
- finalPoints,
280
- (_b = options.simplifyTolerance) != null ? _b : 0.5
281
- );
282
- return this.pointsToSVG(simplifiedPoints);
327
+ const useSmoothing = options.smoothing !== false;
328
+ if (useSmoothing) {
329
+ return this.pointsToSVGPaper(finalPoints, (_f = options.simplifyTolerance) != null ? _f : 2.5);
330
+ } else {
331
+ const simplifiedPoints = this.douglasPeucker(
332
+ finalPoints,
333
+ (_g = options.simplifyTolerance) != null ? _g : 2
334
+ );
335
+ return this.pointsToSVG(simplifiedPoints);
336
+ }
337
+ }
338
+ static createMask(imageData, threshold, padding, paddedWidth, paddedHeight) {
339
+ const { width, height, data } = imageData;
340
+ const mask = new Uint8Array(paddedWidth * paddedHeight);
341
+ let hasTransparency = false;
342
+ for (let i = 3; i < data.length; i += 4) {
343
+ if (data[i] < 255) {
344
+ hasTransparency = true;
345
+ break;
346
+ }
347
+ }
348
+ for (let y = 0; y < height; y++) {
349
+ for (let x = 0; x < width; x++) {
350
+ const srcIdx = (y * width + x) * 4;
351
+ const r = data[srcIdx];
352
+ const g = data[srcIdx + 1];
353
+ const b = data[srcIdx + 2];
354
+ const a = data[srcIdx + 3];
355
+ const destIdx = (y + padding) * paddedWidth + (x + padding);
356
+ if (hasTransparency) {
357
+ if (a > threshold) {
358
+ mask[destIdx] = 1;
359
+ }
360
+ } else {
361
+ if (!(r > 240 && g > 240 && b > 240)) {
362
+ mask[destIdx] = 1;
363
+ }
364
+ }
365
+ }
366
+ }
367
+ return mask;
368
+ }
369
+ /**
370
+ * Fast circular morphology using a distance-transform inspired separable approach.
371
+ * O(N * R) complexity, where R is the radius.
372
+ */
373
+ static circularMorphology(mask, width, height, radius, op) {
374
+ const dilate = (m, r) => {
375
+ const horizontalDist = new Int32Array(width * height);
376
+ for (let y = 0; y < height; y++) {
377
+ let lastSolid = -r * 2;
378
+ for (let x = 0; x < width; x++) {
379
+ if (m[y * width + x]) lastSolid = x;
380
+ horizontalDist[y * width + x] = x - lastSolid;
381
+ }
382
+ lastSolid = width + r * 2;
383
+ for (let x = width - 1; x >= 0; x--) {
384
+ if (m[y * width + x]) lastSolid = x;
385
+ horizontalDist[y * width + x] = Math.min(
386
+ horizontalDist[y * width + x],
387
+ lastSolid - x
388
+ );
389
+ }
390
+ }
391
+ const result = new Uint8Array(width * height);
392
+ const r2 = r * r;
393
+ for (let x = 0; x < width; x++) {
394
+ for (let y = 0; y < height; y++) {
395
+ let found = false;
396
+ const minY = Math.max(0, y - r);
397
+ const maxY = Math.min(height - 1, y + r);
398
+ for (let dy = minY; dy <= maxY; dy++) {
399
+ const dY = dy - y;
400
+ const hDist = horizontalDist[dy * width + x];
401
+ if (hDist * hDist + dY * dY <= r2) {
402
+ found = true;
403
+ break;
404
+ }
405
+ }
406
+ if (found) result[y * width + x] = 1;
407
+ }
408
+ }
409
+ return result;
410
+ };
411
+ const erode = (m, r) => {
412
+ const inverted = new Uint8Array(m.length);
413
+ for (let i = 0; i < m.length; i++) inverted[i] = m[i] ? 0 : 1;
414
+ const dilatedInverted = dilate(inverted, r);
415
+ const result = new Uint8Array(m.length);
416
+ for (let i = 0; i < m.length; i++) result[i] = dilatedInverted[i] ? 0 : 1;
417
+ return result;
418
+ };
419
+ switch (op) {
420
+ case "dilate":
421
+ return dilate(mask, radius);
422
+ case "erode":
423
+ return erode(mask, radius);
424
+ case "closing":
425
+ return erode(dilate(mask, radius), radius);
426
+ case "opening":
427
+ return dilate(erode(mask, radius), radius);
428
+ default:
429
+ return mask;
430
+ }
431
+ }
432
+ /**
433
+ * Fills internal holes in the binary mask using flood fill from edges.
434
+ */
435
+ static fillHoles(mask, width, height) {
436
+ const background = new Uint8Array(width * height);
437
+ const queue = [];
438
+ for (let x = 0; x < width; x++) {
439
+ if (mask[x] === 0) {
440
+ background[x] = 1;
441
+ queue.push([x, 0]);
442
+ }
443
+ const lastRow = (height - 1) * width + x;
444
+ if (mask[lastRow] === 0) {
445
+ background[lastRow] = 1;
446
+ queue.push([x, height - 1]);
447
+ }
448
+ }
449
+ for (let y = 1; y < height - 1; y++) {
450
+ if (mask[y * width] === 0) {
451
+ background[y * width] = 1;
452
+ queue.push([0, y]);
453
+ }
454
+ if (mask[y * width + width - 1] === 0) {
455
+ background[y * width + width - 1] = 1;
456
+ queue.push([width - 1, y]);
457
+ }
458
+ }
459
+ const dirs = [
460
+ [0, 1],
461
+ [0, -1],
462
+ [1, 0],
463
+ [-1, 0]
464
+ ];
465
+ let head = 0;
466
+ while (head < queue.length) {
467
+ const [cx, cy] = queue[head++];
468
+ for (const [dx, dy] of dirs) {
469
+ const nx = cx + dx;
470
+ const ny = cy + dy;
471
+ if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
472
+ const nidx = ny * width + nx;
473
+ if (mask[nidx] === 0 && background[nidx] === 0) {
474
+ background[nidx] = 1;
475
+ queue.push([nx, ny]);
476
+ }
477
+ }
478
+ }
479
+ }
480
+ const filledMask = new Uint8Array(width * height);
481
+ for (let i = 0; i < width * height; i++) {
482
+ filledMask[i] = background[i] === 0 ? 1 : 0;
483
+ }
484
+ return filledMask;
485
+ }
486
+ /**
487
+ * Traces all contours in the mask with optimized start-point detection
488
+ */
489
+ static traceAllContours(mask, width, height) {
490
+ const visited = new Uint8Array(width * height);
491
+ const allContours = [];
492
+ for (let y = 0; y < height; y++) {
493
+ for (let x = 0; x < width; x++) {
494
+ const idx = y * width + x;
495
+ if (mask[idx] && !visited[idx]) {
496
+ const isLeftEdge = x === 0 || mask[idx - 1] === 0;
497
+ if (isLeftEdge) {
498
+ const contour = this.marchingSquares(
499
+ mask,
500
+ visited,
501
+ x,
502
+ y,
503
+ width,
504
+ height
505
+ );
506
+ if (contour.length > 2) {
507
+ allContours.push(contour);
508
+ }
509
+ }
510
+ }
511
+ }
512
+ }
513
+ return allContours;
283
514
  }
284
515
  static loadImage(url) {
285
516
  return new Promise((resolve, reject) => {
@@ -294,33 +525,11 @@ var ImageTracer = class {
294
525
  * Moore-Neighbor Tracing Algorithm
295
526
  * More robust for irregular shapes than simple Marching Squares walker.
296
527
  */
297
- static marchingSquares(imageData, alphaThreshold) {
298
- const width = imageData.width;
299
- const height = imageData.height;
300
- const data = imageData.data;
528
+ static marchingSquares(mask, visited, startX, startY, width, height) {
301
529
  const isSolid = (x, y) => {
302
530
  if (x < 0 || x >= width || y < 0 || y >= height) return false;
303
- const index = (y * width + x) * 4;
304
- const r = data[index];
305
- const g = data[index + 1];
306
- const b = data[index + 2];
307
- const a = data[index + 3];
308
- if (a <= alphaThreshold) return false;
309
- if (r > 240 && g > 240 && b > 240) return false;
310
- return true;
531
+ return mask[y * width + x] === 1;
311
532
  };
312
- let startX = -1;
313
- let startY = -1;
314
- searchLoop: for (let y = 0; y < height; y++) {
315
- for (let x = 0; x < width; x++) {
316
- if (isSolid(x, y)) {
317
- startX = x;
318
- startY = y;
319
- break searchLoop;
320
- }
321
- }
322
- }
323
- if (startX === -1) return [];
324
533
  const points = [];
325
534
  let cx = startX;
326
535
  let cy = startY;
@@ -339,6 +548,7 @@ var ImageTracer = class {
339
548
  let steps = 0;
340
549
  do {
341
550
  points.push({ x: cx, y: cy });
551
+ visited[cy * width + cx] = 1;
342
552
  let found = false;
343
553
  for (let i = 0; i < 8; i++) {
344
554
  const idx = (backtrack + 1 + i) % 8;
@@ -347,16 +557,12 @@ var ImageTracer = class {
347
557
  if (isSolid(nx, ny)) {
348
558
  cx = nx;
349
559
  cy = ny;
350
- backtrack = (idx + 4) % 8;
351
- backtrack = (idx + 4 + 1) % 8;
352
560
  backtrack = (idx + 4 + 1) % 8;
353
561
  found = true;
354
562
  break;
355
563
  }
356
564
  }
357
- if (!found) {
358
- break;
359
- }
565
+ if (!found) break;
360
566
  steps++;
361
567
  } while ((cx !== startX || cy !== startY) && steps < maxSteps);
362
568
  return points;
@@ -405,23 +611,14 @@ var ImageTracer = class {
405
611
  dy = p.y - y;
406
612
  return dx * dx + dy * dy;
407
613
  }
408
- static scalePoints(points, targetWidth, targetHeight) {
614
+ static scalePoints(points, targetWidth, targetHeight, bounds) {
409
615
  if (points.length === 0) return points;
410
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
411
- for (const p of points) {
412
- if (p.x < minX) minX = p.x;
413
- if (p.y < minY) minY = p.y;
414
- if (p.x > maxX) maxX = p.x;
415
- if (p.y > maxY) maxY = p.y;
416
- }
417
- const srcW = maxX - minX;
418
- const srcH = maxY - minY;
419
- if (srcW === 0 || srcH === 0) return points;
420
- const scaleX = targetWidth / srcW;
421
- const scaleY = targetHeight / srcH;
616
+ if (bounds.width === 0 || bounds.height === 0) return points;
617
+ const scaleX = targetWidth / bounds.width;
618
+ const scaleY = targetHeight / bounds.height;
422
619
  return points.map((p) => ({
423
- x: (p.x - minX) * scaleX,
424
- y: (p.y - minY) * scaleY
620
+ x: (p.x - bounds.minX) * scaleX,
621
+ y: (p.y - bounds.minY) * scaleY
425
622
  }));
426
623
  }
427
624
  static pointsToSVG(points) {
@@ -430,6 +627,23 @@ var ImageTracer = class {
430
627
  const tail = points.slice(1);
431
628
  return `M ${head.x} ${head.y} ` + tail.map((p) => `L ${p.x} ${p.y}`).join(" ") + " Z";
432
629
  }
630
+ static ensurePaper() {
631
+ if (!import_paper.default.project) {
632
+ import_paper.default.setup(new import_paper.default.Size(100, 100));
633
+ }
634
+ }
635
+ static pointsToSVGPaper(points, tolerance) {
636
+ if (points.length < 3) return this.pointsToSVG(points);
637
+ this.ensurePaper();
638
+ const path = new import_paper.default.Path({
639
+ segments: points.map((p) => [p.x, p.y]),
640
+ closed: true
641
+ });
642
+ path.simplify(tolerance);
643
+ const data = path.pathData;
644
+ path.remove();
645
+ return data;
646
+ }
433
647
  };
434
648
 
435
649
  // src/coordinate.ts
@@ -504,96 +718,45 @@ var Coordinate = class {
504
718
  };
505
719
 
506
720
  // src/geometry.ts
507
- var import_paper = __toESM(require("paper"));
508
- function resolveHolePosition(hole, geometry, canvasSize) {
509
- if (hole.anchor) {
510
- const { x, y, width, height } = geometry;
511
- let bx = x;
512
- let by = y;
513
- const left = x - width / 2;
514
- const right = x + width / 2;
515
- const top = y - height / 2;
516
- const bottom = y + height / 2;
517
- switch (hole.anchor) {
518
- case "top-left":
519
- bx = left;
520
- by = top;
521
- break;
522
- case "top-center":
523
- bx = x;
524
- by = top;
525
- break;
526
- case "top-right":
527
- bx = right;
528
- by = top;
529
- break;
530
- case "center-left":
531
- bx = left;
532
- by = y;
533
- break;
534
- case "center":
535
- bx = x;
536
- by = y;
537
- break;
538
- case "center-right":
539
- bx = right;
540
- by = y;
541
- break;
542
- case "bottom-left":
543
- bx = left;
544
- by = bottom;
545
- break;
546
- case "bottom-center":
547
- bx = x;
548
- by = bottom;
549
- break;
550
- case "bottom-right":
551
- bx = right;
552
- by = bottom;
553
- break;
554
- }
555
- return {
556
- x: bx + (hole.offsetX || 0),
557
- y: by + (hole.offsetY || 0)
558
- };
559
- } else if (hole.x !== void 0 && hole.y !== void 0) {
560
- const { x, width, y, height } = geometry;
561
- return {
562
- x: hole.x * width + (x - width / 2) + (hole.offsetX || 0),
563
- y: hole.y * height + (y - height / 2) + (hole.offsetY || 0)
564
- };
565
- }
566
- return { x: 0, y: 0 };
721
+ var import_paper2 = __toESM(require("paper"));
722
+ function resolveFeaturePosition(feature, geometry) {
723
+ const { x, y, width, height } = geometry;
724
+ const left = x - width / 2;
725
+ const top = y - height / 2;
726
+ return {
727
+ x: left + feature.x * width,
728
+ y: top + feature.y * height
729
+ };
567
730
  }
568
731
  function ensurePaper(width, height) {
569
- if (!import_paper.default.project) {
570
- import_paper.default.setup(new import_paper.default.Size(width, height));
732
+ if (!import_paper2.default.project) {
733
+ import_paper2.default.setup(new import_paper2.default.Size(width, height));
571
734
  } else {
572
- import_paper.default.view.viewSize = new import_paper.default.Size(width, height);
735
+ import_paper2.default.view.viewSize = new import_paper2.default.Size(width, height);
573
736
  }
574
737
  }
575
738
  function createBaseShape(options) {
576
739
  const { shape, width, height, radius, x, y, pathData } = options;
577
- const center = new import_paper.default.Point(x, y);
740
+ const center = new import_paper2.default.Point(x, y);
578
741
  if (shape === "rect") {
579
- return new import_paper.default.Path.Rectangle({
742
+ return new import_paper2.default.Path.Rectangle({
580
743
  point: [x - width / 2, y - height / 2],
581
744
  size: [Math.max(0, width), Math.max(0, height)],
582
745
  radius: Math.max(0, radius)
583
746
  });
584
747
  } else if (shape === "circle") {
585
748
  const r = Math.min(width, height) / 2;
586
- return new import_paper.default.Path.Circle({
749
+ return new import_paper2.default.Path.Circle({
587
750
  center,
588
751
  radius: Math.max(0, r)
589
752
  });
590
753
  } else if (shape === "ellipse") {
591
- return new import_paper.default.Path.Ellipse({
754
+ return new import_paper2.default.Path.Ellipse({
592
755
  center,
593
756
  radius: [Math.max(0, width / 2), Math.max(0, height / 2)]
594
757
  });
595
758
  } else if (shape === "custom" && pathData) {
596
- const path = new import_paper.default.Path();
759
+ const path = new import_paper2.default.Path();
597
760
  path.pathData = pathData;
598
761
  path.position = center;
599
762
  if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
@@ -601,89 +764,76 @@ function createBaseShape(options) {
601
764
  }
602
765
  return path;
603
766
  } else {
604
- return new import_paper.default.Path.Rectangle({
767
+ return new import_paper2.default.Path.Rectangle({
605
768
  point: [x - width / 2, y - height / 2],
606
769
  size: [Math.max(0, width), Math.max(0, height)]
607
770
  });
608
771
  }
609
772
  }
773
+ function createFeatureItem(feature, center) {
774
+ let item;
775
+ if (feature.shape === "rect") {
776
+ const w = feature.width || 10;
777
+ const h = feature.height || 10;
778
+ const r = feature.radius || 0;
779
+ item = new import_paper2.default.Path.Rectangle({
780
+ point: [center.x - w / 2, center.y - h / 2],
781
+ size: [w, h],
782
+ radius: r
783
+ });
784
+ } else {
785
+ const r = feature.radius || 5;
786
+ item = new import_paper2.default.Path.Circle({
787
+ center,
788
+ radius: r
789
+ });
790
+ }
791
+ if (feature.rotation) {
792
+ item.rotate(feature.rotation, center);
793
+ }
794
+ return item;
795
+ }
610
796
  function getDielineShape(options) {
611
797
  let mainShape = createBaseShape(options);
612
- const { holes } = options;
613
- if (holes && holes.length > 0) {
614
- let lugsPath = null;
615
- let cutsPath = null;
616
- holes.forEach((hole) => {
617
- const center = new import_paper.default.Point(hole.x, hole.y);
618
- const lug = hole.shape === "square" ? new import_paper.default.Path.Rectangle({
619
- point: [
620
- center.x - hole.outerRadius,
621
- center.y - hole.outerRadius
622
- ],
623
- size: [hole.outerRadius * 2, hole.outerRadius * 2]
624
- }) : new import_paper.default.Path.Circle({
625
- center,
626
- radius: hole.outerRadius
627
- });
628
- const cut = hole.shape === "square" ? new import_paper.default.Path.Rectangle({
629
- point: [
630
- center.x - hole.innerRadius,
631
- center.y - hole.innerRadius
632
- ],
633
- size: [hole.innerRadius * 2, hole.innerRadius * 2]
634
- }) : new import_paper.default.Path.Circle({
635
- center,
636
- radius: hole.innerRadius
637
- });
638
- if (!lugsPath) {
639
- lugsPath = lug;
798
+ const { features } = options;
799
+ if (features && features.length > 0) {
800
+ const adds = [];
801
+ const subtracts = [];
802
+ features.forEach((f) => {
803
+ const pos = resolveFeaturePosition(f, options);
804
+ const center = new import_paper2.default.Point(pos.x, pos.y);
805
+ const item = createFeatureItem(f, center);
806
+ if (f.operation === "add") {
807
+ adds.push(item);
640
808
  } else {
809
+ subtracts.push(item);
810
+ }
811
+ });
812
+ if (adds.length > 0) {
813
+ for (const item of adds) {
641
814
  try {
642
- const temp = lugsPath.unite(lug);
643
- lugsPath.remove();
644
- lug.remove();
645
- lugsPath = temp;
815
+ const temp = mainShape.unite(item);
816
+ mainShape.remove();
817
+ item.remove();
818
+ mainShape = temp;
646
819
  } catch (e) {
647
- console.error("Geometry: Failed to unite lug", e);
648
- lug.remove();
820
+ console.error("Geometry: Failed to unite feature", e);
821
+ item.remove();
649
822
  }
650
823
  }
651
- if (!cutsPath) {
652
- cutsPath = cut;
653
- } else {
824
+ }
825
+ if (subtracts.length > 0) {
826
+ for (const item of subtracts) {
654
827
  try {
655
- const temp = cutsPath.unite(cut);
656
- cutsPath.remove();
657
- cut.remove();
658
- cutsPath = temp;
828
+ const temp = mainShape.subtract(item);
829
+ mainShape.remove();
830
+ item.remove();
831
+ mainShape = temp;
659
832
  } catch (e) {
660
- console.error("Geometry: Failed to unite cut", e);
661
- cut.remove();
833
+ console.error("Geometry: Failed to subtract feature", e);
834
+ item.remove();
662
835
  }
663
836
  }
664
- });
665
- if (lugsPath) {
666
- try {
667
- const temp = mainShape.unite(lugsPath);
668
- mainShape.remove();
669
- lugsPath.remove();
670
- mainShape = temp;
671
- } catch (e) {
672
- console.error("Geometry: Failed to unite lugsPath to mainShape", e);
673
- }
674
- }
675
- if (cutsPath) {
676
- try {
677
- const temp = mainShape.subtract(cutsPath);
678
- mainShape.remove();
679
- cutsPath.remove();
680
- mainShape = temp;
681
- } catch (e) {
682
- console.error(
683
- "Geometry: Failed to subtract cutsPath from mainShape",
684
- e
685
- );
686
- }
687
837
  }
688
838
  }
689
839
  return mainShape;
@@ -692,7 +842,7 @@ function generateDielinePath(options) {
692
842
  const paperWidth = options.canvasWidth || options.width * 2 || 2e3;
693
843
  const paperHeight = options.canvasHeight || options.height * 2 || 2e3;
694
844
  ensurePaper(paperWidth, paperHeight);
695
- import_paper.default.project.activeLayer.removeChildren();
845
+ import_paper2.default.project.activeLayer.removeChildren();
696
846
  const mainShape = getDielineShape(options);
697
847
  const pathData = mainShape.pathData;
698
848
  mainShape.remove();
@@ -700,9 +850,9 @@ function generateDielinePath(options) {
700
850
  }
701
851
  function generateMaskPath(options) {
702
852
  ensurePaper(options.canvasWidth, options.canvasHeight);
703
- import_paper.default.project.activeLayer.removeChildren();
853
+ import_paper2.default.project.activeLayer.removeChildren();
704
854
  const { canvasWidth, canvasHeight } = options;
705
- const maskRect = new import_paper.default.Path.Rectangle({
855
+ const maskRect = new import_paper2.default.Path.Rectangle({
706
856
  point: [0, 0],
707
857
  size: [canvasWidth, canvasHeight]
708
858
  });
@@ -714,43 +864,13 @@ function generateMaskPath(options) {
714
864
  finalMask.remove();
715
865
  return pathData;
716
866
  }
717
- function generateBleedZonePath(options, offset) {
718
- const paperWidth = options.canvasWidth || options.width * 2 || 2e3;
719
- const paperHeight = options.canvasHeight || options.height * 2 || 2e3;
867
+ function generateBleedZonePath(originalOptions, offsetOptions, offset) {
868
+ const paperWidth = originalOptions.canvasWidth || originalOptions.width * 2 || 2e3;
869
+ const paperHeight = originalOptions.canvasHeight || originalOptions.height * 2 || 2e3;
720
870
  ensurePaper(paperWidth, paperHeight);
721
- import_paper.default.project.activeLayer.removeChildren();
722
- const shapeOriginal = getDielineShape(options);
723
- let shapeOffset;
724
- if (options.shape === "custom") {
725
- const stroker = shapeOriginal.clone();
726
- stroker.strokeColor = new import_paper.default.Color("black");
727
- stroker.strokeWidth = Math.abs(offset) * 2;
728
- stroker.strokeJoin = "round";
729
- stroker.strokeCap = "round";
730
- let expanded;
731
- try {
732
- expanded = stroker.expand({ stroke: true, fill: false, insert: false });
733
- } catch (e) {
734
- stroker.remove();
735
- shapeOffset = shapeOriginal.clone();
736
- return shapeOffset.pathData;
737
- }
738
- stroker.remove();
739
- if (offset > 0) {
740
- shapeOffset = shapeOriginal.unite(expanded);
741
- } else {
742
- shapeOffset = shapeOriginal.subtract(expanded);
743
- }
744
- expanded.remove();
745
- } else {
746
- const offsetOptions = {
747
- ...options,
748
- width: Math.max(0, options.width + offset * 2),
749
- height: Math.max(0, options.height + offset * 2),
750
- radius: options.radius === 0 ? 0 : Math.max(0, options.radius + offset)
751
- };
752
- shapeOffset = getDielineShape(offsetOptions);
753
- }
871
+ import_paper2.default.project.activeLayer.removeChildren();
872
+ const shapeOriginal = getDielineShape(originalOptions);
873
+ const shapeOffset = getDielineShape(offsetOptions);
754
874
  let bleedZone;
755
875
  if (offset > 0) {
756
876
  bleedZone = shapeOffset.subtract(shapeOriginal);
@@ -765,16 +885,16 @@ function generateBleedZonePath(options, offset) {
765
885
  }
766
886
  function getNearestPointOnDieline(point, options) {
767
887
  ensurePaper(options.width * 2, options.height * 2);
768
- import_paper.default.project.activeLayer.removeChildren();
888
+ import_paper2.default.project.activeLayer.removeChildren();
769
889
  const shape = createBaseShape(options);
770
- const p = new import_paper.default.Point(point.x, point.y);
890
+ const p = new import_paper2.default.Point(point.x, point.y);
771
891
  const nearest = shape.getNearestPoint(p);
772
892
  const result = { x: nearest.x, y: nearest.y };
773
893
  shape.remove();
774
894
  return result;
775
895
  }
776
896
  function getPathBounds(pathData) {
777
- const path = new import_paper.default.Path();
897
+ const path = new import_paper2.default.Path();
778
898
  path.pathData = pathData;
779
899
  const bounds = path.bounds;
780
900
  path.remove();
@@ -793,20 +913,41 @@ var DielineTool = class {
793
913
  this.metadata = {
794
914
  name: "DielineTool"
795
915
  };
796
- this.unit = "mm";
797
- this.shape = "rect";
798
- this.width = 500;
799
- this.height = 500;
800
- this.radius = 0;
801
- this.offset = 0;
802
- this.style = "solid";
803
- this.insideColor = "rgba(0,0,0,0)";
804
- this.outsideColor = "#ffffff";
805
- this.showBleedLines = true;
806
- this.holes = [];
807
- this.padding = 140;
916
+ this.state = {
917
+ unit: "mm",
918
+ shape: "rect",
919
+ width: 500,
920
+ height: 500,
921
+ radius: 0,
922
+ offset: 0,
923
+ padding: 140,
924
+ mainLine: {
925
+ width: 2.7,
926
+ color: "#FF0000",
927
+ dashLength: 5,
928
+ style: "solid"
929
+ },
930
+ offsetLine: {
931
+ width: 2.7,
932
+ color: "#FF0000",
933
+ dashLength: 5,
934
+ style: "solid"
935
+ },
936
+ insideColor: "rgba(0,0,0,0)",
937
+ outsideColor: "#ffffff",
938
+ showBleedLines: true,
939
+ features: []
940
+ };
808
941
  if (options) {
809
- Object.assign(this, options);
942
+ if (options.mainLine) {
943
+ Object.assign(this.state.mainLine, options.mainLine);
944
+ delete options.mainLine;
945
+ }
946
+ if (options.offsetLine) {
947
+ Object.assign(this.state.offsetLine, options.offsetLine);
948
+ delete options.offsetLine;
949
+ }
950
+ Object.assign(this.state, options);
810
951
  }
811
952
  }
812
953
  activate(context) {
@@ -818,38 +959,93 @@ var DielineTool = class {
818
959
  }
819
960
  const configService = context.services.get("ConfigurationService");
820
961
  if (configService) {
821
- this.unit = configService.get("dieline.unit", this.unit);
822
- this.shape = configService.get("dieline.shape", this.shape);
823
- this.width = configService.get("dieline.width", this.width);
824
- this.height = configService.get("dieline.height", this.height);
825
- this.radius = configService.get("dieline.radius", this.radius);
826
- this.padding = configService.get("dieline.padding", this.padding);
827
- this.offset = configService.get("dieline.offset", this.offset);
828
- this.style = configService.get("dieline.style", this.style);
829
- this.insideColor = configService.get(
830
- "dieline.insideColor",
831
- this.insideColor
832
- );
833
- this.outsideColor = configService.get(
834
- "dieline.outsideColor",
835
- this.outsideColor
836
- );
837
- this.showBleedLines = configService.get(
838
- "dieline.showBleedLines",
839
- this.showBleedLines
840
- );
841
- this.holes = configService.get("dieline.holes", this.holes);
842
- this.pathData = configService.get("dieline.pathData", this.pathData);
962
+ const s = this.state;
963
+ s.unit = configService.get("dieline.unit", s.unit);
964
+ s.shape = configService.get("dieline.shape", s.shape);
965
+ s.width = configService.get("dieline.width", s.width);
966
+ s.height = configService.get("dieline.height", s.height);
967
+ s.radius = configService.get("dieline.radius", s.radius);
968
+ s.padding = configService.get("dieline.padding", s.padding);
969
+ s.offset = configService.get("dieline.offset", s.offset);
970
+ s.mainLine.width = configService.get("dieline.strokeWidth", s.mainLine.width);
971
+ s.mainLine.color = configService.get("dieline.strokeColor", s.mainLine.color);
972
+ s.mainLine.dashLength = configService.get("dieline.dashLength", s.mainLine.dashLength);
973
+ s.mainLine.style = configService.get("dieline.style", s.mainLine.style);
974
+ s.offsetLine.width = configService.get("dieline.offsetStrokeWidth", s.offsetLine.width);
975
+ s.offsetLine.color = configService.get("dieline.offsetStrokeColor", s.offsetLine.color);
976
+ s.offsetLine.dashLength = configService.get("dieline.offsetDashLength", s.offsetLine.dashLength);
977
+ s.offsetLine.style = configService.get("dieline.offsetStyle", s.offsetLine.style);
978
+ s.insideColor = configService.get("dieline.insideColor", s.insideColor);
979
+ s.outsideColor = configService.get("dieline.outsideColor", s.outsideColor);
980
+ s.showBleedLines = configService.get("dieline.showBleedLines", s.showBleedLines);
981
+ s.features = configService.get("dieline.features", s.features);
982
+ s.pathData = configService.get("dieline.pathData", s.pathData);
843
983
  configService.onAnyChange((e) => {
844
984
  if (e.key.startsWith("dieline.")) {
845
- const prop = e.key.split(".")[1];
846
- console.log(
847
- `[DielineTool] Config change detected: ${e.key} -> ${e.value}`
848
- );
849
- if (prop && prop in this) {
850
- this[prop] = e.value;
851
- this.updateDieline();
985
+ console.log(`[DielineTool] Config change detected: ${e.key} -> ${e.value}`);
986
+ switch (e.key) {
987
+ case "dieline.unit":
988
+ s.unit = e.value;
989
+ break;
990
+ case "dieline.shape":
991
+ s.shape = e.value;
992
+ break;
993
+ case "dieline.width":
994
+ s.width = e.value;
995
+ break;
996
+ case "dieline.height":
997
+ s.height = e.value;
998
+ break;
999
+ case "dieline.radius":
1000
+ s.radius = e.value;
1001
+ break;
1002
+ case "dieline.padding":
1003
+ s.padding = e.value;
1004
+ break;
1005
+ case "dieline.offset":
1006
+ s.offset = e.value;
1007
+ break;
1008
+ case "dieline.strokeWidth":
1009
+ s.mainLine.width = e.value;
1010
+ break;
1011
+ case "dieline.strokeColor":
1012
+ s.mainLine.color = e.value;
1013
+ break;
1014
+ case "dieline.dashLength":
1015
+ s.mainLine.dashLength = e.value;
1016
+ break;
1017
+ case "dieline.style":
1018
+ s.mainLine.style = e.value;
1019
+ break;
1020
+ case "dieline.offsetStrokeWidth":
1021
+ s.offsetLine.width = e.value;
1022
+ break;
1023
+ case "dieline.offsetStrokeColor":
1024
+ s.offsetLine.color = e.value;
1025
+ break;
1026
+ case "dieline.offsetDashLength":
1027
+ s.offsetLine.dashLength = e.value;
1028
+ break;
1029
+ case "dieline.offsetStyle":
1030
+ s.offsetLine.style = e.value;
1031
+ break;
1032
+ case "dieline.insideColor":
1033
+ s.insideColor = e.value;
1034
+ break;
1035
+ case "dieline.outsideColor":
1036
+ s.outsideColor = e.value;
1037
+ break;
1038
+ case "dieline.showBleedLines":
1039
+ s.showBleedLines = e.value;
1040
+ break;
1041
+ case "dieline.features":
1042
+ s.features = e.value;
1043
+ break;
1044
+ case "dieline.pathData":
1045
+ s.pathData = e.value;
1046
+ break;
852
1047
  }
1048
+ this.updateDieline();
853
1049
  }
854
1050
  });
855
1051
  }
@@ -862,6 +1058,7 @@ var DielineTool = class {
862
1058
  this.context = void 0;
863
1059
  }
864
1060
  contribute() {
1061
+ const s = this.state;
865
1062
  return {
866
1063
  [import_core2.ContributionPointIds.CONFIGURATIONS]: [
867
1064
  {
@@ -869,14 +1066,14 @@ var DielineTool = class {
869
1066
  type: "select",
870
1067
  label: "Unit",
871
1068
  options: ["px", "mm", "cm", "in"],
872
- default: this.unit
1069
+ default: s.unit
873
1070
  },
874
1071
  {
875
1072
  id: "dieline.shape",
876
1073
  type: "select",
877
1074
  label: "Shape",
878
1075
  options: ["rect", "circle", "ellipse", "custom"],
879
- default: this.shape
1076
+ default: s.shape
880
1077
  },
881
1078
  {
882
1079
  id: "dieline.width",
@@ -884,7 +1081,7 @@ var DielineTool = class {
884
1081
  label: "Width",
885
1082
  min: 10,
886
1083
  max: 2e3,
887
- default: this.width
1084
+ default: s.width
888
1085
  },
889
1086
  {
890
1087
  id: "dieline.height",
@@ -892,7 +1089,7 @@ var DielineTool = class {
892
1089
  label: "Height",
893
1090
  min: 10,
894
1091
  max: 2e3,
895
- default: this.height
1092
+ default: s.height
896
1093
  },
897
1094
  {
898
1095
  id: "dieline.radius",
@@ -900,20 +1097,14 @@ var DielineTool = class {
900
1097
  label: "Corner Radius",
901
1098
  min: 0,
902
1099
  max: 500,
903
- default: this.radius
904
- },
905
- {
906
- id: "dieline.position",
907
- type: "json",
908
- label: "Position (Normalized)",
909
- default: this.radius
1100
+ default: s.radius
910
1101
  },
911
1102
  {
912
1103
  id: "dieline.padding",
913
1104
  type: "select",
914
1105
  label: "View Padding",
915
1106
  options: [0, 10, 20, 40, 60, 100, "2%", "5%", "10%", "15%", "20%"],
916
- default: this.padding
1107
+ default: s.padding
917
1108
  },
918
1109
  {
919
1110
  id: "dieline.offset",
@@ -921,38 +1112,91 @@ var DielineTool = class {
921
1112
  label: "Bleed Offset",
922
1113
  min: -100,
923
1114
  max: 100,
924
- default: this.offset
1115
+ default: s.offset
925
1116
  },
926
1117
  {
927
1118
  id: "dieline.showBleedLines",
928
1119
  type: "boolean",
929
1120
  label: "Show Bleed Lines",
930
- default: this.showBleedLines
1121
+ default: s.showBleedLines
1122
+ },
1123
+ {
1124
+ id: "dieline.strokeWidth",
1125
+ type: "number",
1126
+ label: "Line Width",
1127
+ min: 0.1,
1128
+ max: 10,
1129
+ step: 0.1,
1130
+ default: s.mainLine.width
1131
+ },
1132
+ {
1133
+ id: "dieline.strokeColor",
1134
+ type: "color",
1135
+ label: "Line Color",
1136
+ default: s.mainLine.color
1137
+ },
1138
+ {
1139
+ id: "dieline.dashLength",
1140
+ type: "number",
1141
+ label: "Dash Length",
1142
+ min: 1,
1143
+ max: 50,
1144
+ default: s.mainLine.dashLength
931
1145
  },
932
1146
  {
933
1147
  id: "dieline.style",
934
1148
  type: "select",
935
1149
  label: "Line Style",
936
- options: ["solid", "dashed"],
937
- default: this.style
1150
+ options: ["solid", "dashed", "hidden"],
1151
+ default: s.mainLine.style
1152
+ },
1153
+ {
1154
+ id: "dieline.offsetStrokeWidth",
1155
+ type: "number",
1156
+ label: "Offset Line Width",
1157
+ min: 0.1,
1158
+ max: 10,
1159
+ step: 0.1,
1160
+ default: s.offsetLine.width
1161
+ },
1162
+ {
1163
+ id: "dieline.offsetStrokeColor",
1164
+ type: "color",
1165
+ label: "Offset Line Color",
1166
+ default: s.offsetLine.color
1167
+ },
1168
+ {
1169
+ id: "dieline.offsetDashLength",
1170
+ type: "number",
1171
+ label: "Offset Dash Length",
1172
+ min: 1,
1173
+ max: 50,
1174
+ default: s.offsetLine.dashLength
1175
+ },
1176
+ {
1177
+ id: "dieline.offsetStyle",
1178
+ type: "select",
1179
+ label: "Offset Line Style",
1180
+ options: ["solid", "dashed", "hidden"],
1181
+ default: s.offsetLine.style
938
1182
  },
939
1183
  {
940
1184
  id: "dieline.insideColor",
941
1185
  type: "color",
942
1186
  label: "Inside Color",
943
- default: this.insideColor
1187
+ default: s.insideColor
944
1188
  },
945
1189
  {
946
1190
  id: "dieline.outsideColor",
947
1191
  type: "color",
948
1192
  label: "Outside Color",
949
- default: this.outsideColor
1193
+ default: s.outsideColor
950
1194
  },
951
1195
  {
952
- id: "dieline.holes",
1196
+ id: "dieline.features",
953
1197
  type: "json",
954
- label: "Holes",
955
- default: this.holes
1198
+ label: "Edge Features",
1199
+ default: s.features
956
1200
  }
957
1201
  ],
958
1202
  [import_core2.ContributionPointIds.COMMANDS]: [
@@ -974,24 +1218,18 @@ var DielineTool = class {
974
1218
  command: "detectEdge",
975
1219
  title: "Detect Edge from Image",
976
1220
  handler: async (imageUrl, options) => {
977
- var _a;
978
1221
  try {
979
1222
  const pathData = await ImageTracer.trace(imageUrl, options);
980
1223
  const bounds = getPathBounds(pathData);
981
- const currentMax = Math.max(this.width, this.height);
1224
+ const currentMax = Math.max(s.width, s.height);
982
1225
  const scale = currentMax / Math.max(bounds.width, bounds.height);
983
1226
  const newWidth = bounds.width * scale;
984
1227
  const newHeight = bounds.height * scale;
985
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
986
- "ConfigurationService"
987
- );
988
- if (configService) {
989
- configService.update("dieline.width", newWidth);
990
- configService.update("dieline.height", newHeight);
991
- configService.update("dieline.shape", "custom");
992
- configService.update("dieline.pathData", pathData);
993
- }
994
- return pathData;
1228
+ return {
1229
+ pathData,
1230
+ width: newWidth,
1231
+ height: newHeight
1232
+ };
995
1233
  } catch (e) {
996
1234
  console.error("Edge detection failed", e);
997
1235
  throw e;
@@ -1050,15 +1288,15 @@ var DielineTool = class {
1050
1288
  return new import_fabric2.Pattern({ source: canvas, repetition: "repeat" });
1051
1289
  }
1052
1290
  resolvePadding(containerWidth, containerHeight) {
1053
- if (typeof this.padding === "number") {
1054
- return this.padding;
1291
+ if (typeof this.state.padding === "number") {
1292
+ return this.state.padding;
1055
1293
  }
1056
- if (typeof this.padding === "string") {
1057
- if (this.padding.endsWith("%")) {
1058
- const percent = parseFloat(this.padding) / 100;
1294
+ if (typeof this.state.padding === "string") {
1295
+ if (this.state.padding.endsWith("%")) {
1296
+ const percent = parseFloat(this.state.padding) / 100;
1059
1297
  return Math.min(containerWidth, containerHeight) * percent;
1060
1298
  }
1061
- return parseFloat(this.padding) || 0;
1299
+ return parseFloat(this.state.padding) || 0;
1062
1300
  }
1063
1301
  return 0;
1064
1302
  }
@@ -1071,14 +1309,14 @@ var DielineTool = class {
1071
1309
  shape,
1072
1310
  radius,
1073
1311
  offset,
1074
- style,
1312
+ mainLine,
1313
+ offsetLine,
1075
1314
  insideColor,
1076
1315
  outsideColor,
1077
- position,
1078
1316
  showBleedLines,
1079
- holes
1080
- } = this;
1081
- let { width, height } = this;
1317
+ features
1318
+ } = this.state;
1319
+ let { width, height } = this.state;
1082
1320
  const canvasW = this.canvasService.canvas.width || 800;
1083
1321
  const canvasH = this.canvasService.canvas.height || 600;
1084
1322
  const paddingPx = this.resolvePadding(canvasW, canvasH);
@@ -1095,40 +1333,27 @@ var DielineTool = class {
1095
1333
  const visualRadius = radius * scale;
1096
1334
  const visualOffset = offset * scale;
1097
1335
  layer.remove(...layer.getObjects());
1098
- const geometryForHoles = {
1099
- x: cx,
1100
- y: cy,
1101
- width: visualWidth,
1102
- height: visualHeight
1103
- // Pass scale/unit context if needed by resolveHolePosition (though currently unused there)
1104
- };
1105
- const absoluteHoles = (holes || []).map((h) => {
1106
- const unitScale = Coordinate.convertUnit(1, "mm", unit);
1107
- const offsetScale = unitScale * scale;
1108
- const hWithPixelOffsets = {
1109
- ...h,
1110
- offsetX: (h.offsetX || 0) * offsetScale,
1111
- offsetY: (h.offsetY || 0) * offsetScale
1112
- };
1113
- const pos = resolveHolePosition(hWithPixelOffsets, geometryForHoles, {
1114
- width: canvasW,
1115
- height: canvasH
1116
- });
1336
+ const absoluteFeatures = (features || []).map((f) => {
1337
+ const featureScale = scale;
1117
1338
  return {
1118
- ...h,
1119
- x: pos.x,
1120
- y: pos.y,
1121
- // Scale hole radii: mm -> current unit -> pixels
1122
- innerRadius: h.innerRadius * offsetScale,
1123
- outerRadius: h.outerRadius * offsetScale,
1124
- // Store scaled offsets in the result for consistency, though pos is already resolved
1125
- offsetX: hWithPixelOffsets.offsetX,
1126
- offsetY: hWithPixelOffsets.offsetY
1339
+ ...f,
1340
+ x: f.x,
1341
+ y: f.y,
1342
+ width: (f.width || 0) * featureScale,
1343
+ height: (f.height || 0) * featureScale,
1344
+ radius: (f.radius || 0) * featureScale
1127
1345
  };
1128
1346
  });
1347
+ const originalFeatures = absoluteFeatures.filter(
1348
+ (f) => !f.target || f.target === "original" || f.target === "both"
1349
+ );
1350
+ const offsetFeatures = absoluteFeatures.filter(
1351
+ (f) => f.target === "offset" || f.target === "both"
1352
+ );
1129
1353
  const cutW = Math.max(0, visualWidth + visualOffset * 2);
1130
1354
  const cutH = Math.max(0, visualHeight + visualOffset * 2);
1131
1355
  const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
1356
+ const maskFeatures = visualOffset !== 0 ? offsetFeatures : originalFeatures;
1132
1357
  const maskPathData = generateMaskPath({
1133
1358
  canvasWidth: canvasW,
1134
1359
  canvasHeight: canvasH,
@@ -1138,8 +1363,8 @@ var DielineTool = class {
1138
1363
  radius: cutR,
1139
1364
  x: cx,
1140
1365
  y: cy,
1141
- holes: absoluteHoles,
1142
- pathData: this.pathData
1366
+ features: maskFeatures,
1367
+ pathData: this.state.pathData
1143
1368
  });
1144
1369
  const mask = new import_fabric2.Path(maskPathData, {
1145
1370
  fill: outsideColor,
@@ -1160,8 +1385,9 @@ var DielineTool = class {
1160
1385
  radius: cutR,
1161
1386
  x: cx,
1162
1387
  y: cy,
1163
- holes: absoluteHoles,
1164
- pathData: this.pathData,
1388
+ features: maskFeatures,
1389
+ // Use same features as mask for consistency
1390
+ pathData: this.state.pathData,
1165
1391
  canvasWidth: canvasW,
1166
1392
  canvasHeight: canvasH
1167
1393
  });
@@ -1185,15 +1411,27 @@ var DielineTool = class {
1185
1411
  radius: visualRadius,
1186
1412
  x: cx,
1187
1413
  y: cy,
1188
- holes: absoluteHoles,
1189
- pathData: this.pathData,
1414
+ features: originalFeatures,
1415
+ pathData: this.state.pathData,
1416
+ canvasWidth: canvasW,
1417
+ canvasHeight: canvasH
1418
+ },
1419
+ {
1420
+ shape,
1421
+ width: cutW,
1422
+ height: cutH,
1423
+ radius: cutR,
1424
+ x: cx,
1425
+ y: cy,
1426
+ features: offsetFeatures,
1427
+ pathData: this.state.pathData,
1190
1428
  canvasWidth: canvasW,
1191
1429
  canvasHeight: canvasH
1192
1430
  },
1193
1431
  visualOffset
1194
1432
  );
1195
1433
  if (showBleedLines !== false) {
1196
- const pattern = this.createHatchPattern("red");
1434
+ const pattern = this.createHatchPattern(mainLine.color);
1197
1435
  if (pattern) {
1198
1436
  const bleedObj = new import_fabric2.Path(bleedPathData, {
1199
1437
  fill: pattern,
@@ -1214,18 +1452,16 @@ var DielineTool = class {
1214
1452
  radius: cutR,
1215
1453
  x: cx,
1216
1454
  y: cy,
1217
- holes: absoluteHoles,
1218
- pathData: this.pathData,
1455
+ features: offsetFeatures,
1456
+ pathData: this.state.pathData,
1219
1457
  canvasWidth: canvasW,
1220
1458
  canvasHeight: canvasH
1221
1459
  });
1222
1460
  const offsetBorderObj = new import_fabric2.Path(offsetPathData, {
1223
1461
  fill: null,
1224
- stroke: "#666",
1225
- // Grey
1226
- strokeWidth: 1,
1227
- strokeDashArray: [4, 4],
1228
- // Dashed
1462
+ stroke: offsetLine.style === "hidden" ? null : offsetLine.color,
1463
+ strokeWidth: offsetLine.width,
1464
+ strokeDashArray: offsetLine.style === "dashed" ? [offsetLine.dashLength, offsetLine.dashLength] : void 0,
1229
1465
  selectable: false,
1230
1466
  evented: false,
1231
1467
  originX: "left",
@@ -1240,16 +1476,16 @@ var DielineTool = class {
1240
1476
  radius: visualRadius,
1241
1477
  x: cx,
1242
1478
  y: cy,
1243
- holes: absoluteHoles,
1244
- pathData: this.pathData,
1479
+ features: originalFeatures,
1480
+ pathData: this.state.pathData,
1245
1481
  canvasWidth: canvasW,
1246
1482
  canvasHeight: canvasH
1247
1483
  });
1248
1484
  const borderObj = new import_fabric2.Path(borderPathData, {
1249
1485
  fill: "transparent",
1250
- stroke: "red",
1251
- strokeWidth: 1,
1252
- strokeDashArray: style === "dashed" ? [5, 5] : void 0,
1486
+ stroke: mainLine.style === "hidden" ? null : mainLine.color,
1487
+ strokeWidth: mainLine.width,
1488
+ strokeDashArray: mainLine.style === "dashed" ? [mainLine.dashLength, mainLine.dashLength] : void 0,
1253
1489
  selectable: false,
1254
1490
  evented: false,
1255
1491
  originX: "left",
@@ -1281,7 +1517,7 @@ var DielineTool = class {
1281
1517
  }
1282
1518
  getGeometry() {
1283
1519
  if (!this.canvasService) return null;
1284
- const { unit, shape, width, height, radius, position, offset } = this;
1520
+ const { unit, shape, width, height, radius, offset, mainLine, pathData } = this.state;
1285
1521
  const canvasW = this.canvasService.canvas.width || 800;
1286
1522
  const canvasH = this.canvasService.canvas.height || 600;
1287
1523
  const paddingPx = this.resolvePadding(canvasW, canvasH);
@@ -1304,16 +1540,17 @@ var DielineTool = class {
1304
1540
  height: visualHeight,
1305
1541
  radius: radius * scale,
1306
1542
  offset: offset * scale,
1307
- // Pass scale to help other tools (like HoleTool) convert units
1543
+ // Pass scale to help other tools (like FeatureTool) convert units
1308
1544
  scale,
1309
- pathData: this.pathData
1545
+ strokeWidth: mainLine.width,
1546
+ pathData
1310
1547
  };
1311
1548
  }
1312
1549
  async exportCutImage() {
1313
1550
  if (!this.canvasService) return null;
1314
1551
  const userLayer = this.canvasService.getLayer("user");
1315
1552
  if (!userLayer) return null;
1316
- const { shape, width, height, radius, position, holes } = this;
1553
+ const { shape, width, height, radius, features, unit, pathData } = this.state;
1317
1554
  const canvasW = this.canvasService.canvas.width || 800;
1318
1555
  const canvasH = this.canvasService.canvas.height || 600;
1319
1556
  const paddingPx = this.resolvePadding(canvasW, canvasH);
@@ -1328,55 +1565,45 @@ var DielineTool = class {
1328
1565
  const visualWidth = layout.width;
1329
1566
  const visualHeight = layout.height;
1330
1567
  const visualRadius = radius * scale;
1331
- const absoluteHoles = (holes || []).map((h) => {
1332
- const unit = this.unit || "mm";
1333
- const unitScale = Coordinate.convertUnit(1, "mm", unit);
1334
- const pos = resolveHolePosition(
1335
- {
1336
- ...h,
1337
- offsetX: (h.offsetX || 0) * unitScale * scale,
1338
- offsetY: (h.offsetY || 0) * unitScale * scale
1339
- },
1340
- { x: cx, y: cy, width: visualWidth, height: visualHeight },
1341
- { width: canvasW, height: canvasH }
1342
- );
1568
+ const absoluteFeatures = (features || []).map((f) => {
1569
+ const featureScale = scale;
1343
1570
  return {
1344
- ...h,
1345
- x: pos.x,
1346
- y: pos.y,
1347
- innerRadius: h.innerRadius * unitScale * scale,
1348
- outerRadius: h.outerRadius * unitScale * scale,
1349
- offsetX: (h.offsetX || 0) * unitScale * scale,
1350
- offsetY: (h.offsetY || 0) * unitScale * scale
1571
+ ...f,
1572
+ x: f.x,
1573
+ y: f.y,
1574
+ width: (f.width || 0) * featureScale,
1575
+ height: (f.height || 0) * featureScale,
1576
+ radius: (f.radius || 0) * featureScale
1351
1577
  };
1352
1578
  });
1353
- const pathData = generateDielinePath({
1579
+ const originalFeatures = absoluteFeatures.filter(
1580
+ (f) => !f.target || f.target === "original" || f.target === "both"
1581
+ );
1582
+ const generatedPathData = generateDielinePath({
1354
1583
  shape,
1355
1584
  width: visualWidth,
1356
1585
  height: visualHeight,
1357
1586
  radius: visualRadius,
1358
1587
  x: cx,
1359
1588
  y: cy,
1360
- holes: absoluteHoles,
1361
- pathData: this.pathData,
1589
+ features: originalFeatures,
1590
+ pathData,
1362
1591
  canvasWidth: canvasW,
1363
1592
  canvasHeight: canvasH
1364
1593
  });
1365
1594
  const clonedLayer = await userLayer.clone();
1366
- const clipPath = new import_fabric2.Path(pathData, {
1595
+ const clipPath = new import_fabric2.Path(generatedPathData, {
1367
1596
  originX: "left",
1368
1597
  originY: "top",
1369
1598
  left: 0,
1370
1599
  top: 0,
1371
1600
  absolutePositioned: true
1372
- // Important for groups
1373
1601
  });
1374
1602
  clonedLayer.clipPath = clipPath;
1375
1603
  const bounds = clipPath.getBoundingRect();
1376
1604
  const dataUrl = clonedLayer.toDataURL({
1377
1605
  format: "png",
1378
1606
  multiplier: 2,
1379
- // Better quality
1380
1607
  left: bounds.left,
1381
1608
  top: bounds.top,
1382
1609
  width: bounds.width,
@@ -1545,22 +1772,20 @@ var FilmTool = class {
1545
1772
  }
1546
1773
  };
1547
1774
 
1548
- // src/hole.ts
1775
+ // src/feature.ts
1549
1776
  var import_core4 = require("@pooder/core");
1550
1777
  var import_fabric4 = require("fabric");
1551
- var HoleTool = class {
1778
+ var FeatureTool = class {
1552
1779
  constructor(options) {
1553
- this.id = "pooder.kit.hole";
1780
+ this.id = "pooder.kit.feature";
1554
1781
  this.metadata = {
1555
- name: "HoleTool"
1782
+ name: "FeatureTool"
1556
1783
  };
1557
- this.holes = [];
1558
- this.constraintTarget = "bleed";
1784
+ this.features = [];
1559
1785
  this.isUpdatingConfig = false;
1560
1786
  this.handleMoving = null;
1561
1787
  this.handleModified = null;
1562
1788
  this.handleDielineChange = null;
1563
- // Cache geometry to enforce constraints during drag
1564
1789
  this.currentGeometry = null;
1565
1790
  if (options) {
1566
1791
  Object.assign(this, options);
@@ -1570,26 +1795,18 @@ var HoleTool = class {
1570
1795
  this.context = context;
1571
1796
  this.canvasService = context.services.get("CanvasService");
1572
1797
  if (!this.canvasService) {
1573
- console.warn("CanvasService not found for HoleTool");
1798
+ console.warn("CanvasService not found for FeatureTool");
1574
1799
  return;
1575
1800
  }
1576
1801
  const configService = context.services.get(
1577
1802
  "ConfigurationService"
1578
1803
  );
1579
1804
  if (configService) {
1580
- this.constraintTarget = configService.get(
1581
- "hole.constraintTarget",
1582
- this.constraintTarget
1583
- );
1584
- this.holes = configService.get("dieline.holes", []);
1805
+ this.features = configService.get("dieline.features", []);
1585
1806
  configService.onAnyChange((e) => {
1586
1807
  if (this.isUpdatingConfig) return;
1587
- if (e.key === "hole.constraintTarget") {
1588
- this.constraintTarget = e.value;
1589
- this.enforceConstraints();
1590
- }
1591
- if (e.key === "dieline.holes") {
1592
- this.holes = e.value || [];
1808
+ if (e.key === "dieline.features") {
1809
+ this.features = e.value || [];
1593
1810
  this.redraw();
1594
1811
  }
1595
1812
  });
@@ -1603,102 +1820,38 @@ var HoleTool = class {
1603
1820
  }
1604
1821
  contribute() {
1605
1822
  return {
1606
- [import_core4.ContributionPointIds.CONFIGURATIONS]: [
1607
- {
1608
- id: "hole.constraintTarget",
1609
- type: "select",
1610
- label: "Constraint Target",
1611
- options: ["original", "bleed"],
1612
- default: "bleed"
1613
- }
1614
- ],
1615
1823
  [import_core4.ContributionPointIds.COMMANDS]: [
1616
1824
  {
1617
- command: "resetHoles",
1618
- title: "Reset Holes",
1619
- handler: () => {
1620
- var _a;
1621
- if (!this.canvasService) return false;
1622
- let defaultPos = { x: this.canvasService.canvas.width / 2, y: 50 };
1623
- if (this.currentGeometry) {
1624
- const g = this.currentGeometry;
1625
- const topCenter = { x: g.x, y: g.y - g.height / 2 };
1626
- defaultPos = getNearestPointOnDieline(topCenter, {
1627
- ...g,
1628
- holes: []
1629
- });
1630
- }
1631
- const { width, height } = this.canvasService.canvas;
1632
- const normalizedHole = Coordinate.normalizePoint(defaultPos, {
1633
- width: width || 800,
1634
- height: height || 600
1635
- });
1636
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1637
- "ConfigurationService"
1638
- );
1639
- if (configService) {
1640
- configService.update("dieline.holes", [
1641
- {
1642
- x: normalizedHole.x,
1643
- y: normalizedHole.y,
1644
- innerRadius: 15,
1645
- outerRadius: 25
1646
- }
1647
- ]);
1648
- }
1649
- return true;
1825
+ command: "addFeature",
1826
+ title: "Add Edge Feature",
1827
+ handler: (type = "subtract") => {
1828
+ return this.addFeature(type);
1650
1829
  }
1651
1830
  },
1652
1831
  {
1653
1832
  command: "addHole",
1654
1833
  title: "Add Hole",
1655
- handler: (x, y) => {
1656
- var _a, _b, _c, _d;
1657
- if (!this.canvasService) return false;
1658
- let normalizedX = 0.5;
1659
- let normalizedY = 0.5;
1660
- if (this.currentGeometry) {
1661
- const { x: gx, y: gy, width: gw, height: gh } = this.currentGeometry;
1662
- const left = gx - gw / 2;
1663
- const top = gy - gh / 2;
1664
- normalizedX = gw > 0 ? (x - left) / gw : 0.5;
1665
- normalizedY = gh > 0 ? (y - top) / gh : 0.5;
1666
- } else {
1667
- const { width, height } = this.canvasService.canvas;
1668
- normalizedX = Coordinate.toNormalized(x, width || 800);
1669
- normalizedY = Coordinate.toNormalized(y, height || 600);
1670
- }
1671
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1672
- "ConfigurationService"
1673
- );
1674
- if (configService) {
1675
- const currentHoles = configService.get("dieline.holes", []);
1676
- const lastHole = currentHoles[currentHoles.length - 1];
1677
- const innerRadius = (_b = lastHole == null ? void 0 : lastHole.innerRadius) != null ? _b : 15;
1678
- const outerRadius = (_c = lastHole == null ? void 0 : lastHole.outerRadius) != null ? _c : 25;
1679
- const shape = (_d = lastHole == null ? void 0 : lastHole.shape) != null ? _d : "circle";
1680
- const newHole = {
1681
- x: normalizedX,
1682
- y: normalizedY,
1683
- shape,
1684
- innerRadius,
1685
- outerRadius
1686
- };
1687
- configService.update("dieline.holes", [...currentHoles, newHole]);
1688
- }
1689
- return true;
1834
+ handler: () => {
1835
+ return this.addFeature("subtract");
1690
1836
  }
1691
1837
  },
1692
1838
  {
1693
- command: "clearHoles",
1694
- title: "Clear Holes",
1839
+ command: "addDoubleLayerHole",
1840
+ title: "Add Double Layer Hole",
1841
+ handler: () => {
1842
+ return this.addDoubleLayerHole();
1843
+ }
1844
+ },
1845
+ {
1846
+ command: "clearFeatures",
1847
+ title: "Clear Features",
1695
1848
  handler: () => {
1696
1849
  var _a;
1697
1850
  const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1698
1851
  "ConfigurationService"
1699
1852
  );
1700
1853
  if (configService) {
1701
- configService.update("dieline.holes", []);
1854
+ configService.update("dieline.features", []);
1702
1855
  }
1703
1856
  return true;
1704
1857
  }
@@ -1706,6 +1859,88 @@ var HoleTool = class {
1706
1859
  ]
1707
1860
  };
1708
1861
  }
1862
+ addFeature(type) {
1863
+ var _a;
1864
+ if (!this.canvasService) return false;
1865
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1866
+ "ConfigurationService"
1867
+ );
1868
+ const unit = (configService == null ? void 0 : configService.get("dieline.unit", "mm")) || "mm";
1869
+ const defaultSize = Coordinate.convertUnit(10, "mm", unit);
1870
+ const newFeature = {
1871
+ id: Date.now().toString(),
1872
+ operation: type,
1873
+ target: "original",
1874
+ shape: "rect",
1875
+ x: 0.5,
1876
+ y: 0,
1877
+ // Top edge
1878
+ width: defaultSize,
1879
+ height: defaultSize,
1880
+ rotation: 0
1881
+ };
1882
+ if (configService) {
1883
+ const current = configService.get(
1884
+ "dieline.features",
1885
+ []
1886
+ );
1887
+ configService.update("dieline.features", [...current, newFeature]);
1888
+ }
1889
+ return true;
1890
+ }
1891
+ addDoubleLayerHole() {
1892
+ var _a;
1893
+ if (!this.canvasService) return false;
1894
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1895
+ "ConfigurationService"
1896
+ );
1897
+ const unit = (configService == null ? void 0 : configService.get("dieline.unit", "mm")) || "mm";
1898
+ const lugRadius = Coordinate.convertUnit(20, "mm", unit);
1899
+ const holeRadius = Coordinate.convertUnit(15, "mm", unit);
1900
+ const groupId = Date.now().toString();
1901
+ const timestamp = Date.now();
1902
+ const lug = {
1903
+ id: `${timestamp}-lug`,
1904
+ groupId,
1905
+ operation: "add",
1906
+ shape: "circle",
1907
+ x: 0.5,
1908
+ y: 0,
1909
+ radius: lugRadius,
1910
+ // 20mm
1911
+ rotation: 0
1912
+ };
1913
+ const hole = {
1914
+ id: `${timestamp}-hole`,
1915
+ groupId,
1916
+ operation: "subtract",
1917
+ shape: "circle",
1918
+ x: 0.5,
1919
+ y: 0,
1920
+ radius: holeRadius,
1921
+ // 15mm
1922
+ rotation: 0
1923
+ };
1924
+ if (configService) {
1925
+ const current = configService.get(
1926
+ "dieline.features",
1927
+ []
1928
+ );
1929
+ configService.update("dieline.features", [...current, lug, hole]);
1930
+ }
1931
+ return true;
1932
+ }
1933
+ getGeometryForFeature(geometry, feature) {
1934
+ if ((feature == null ? void 0 : feature.target) === "offset" && geometry.offset !== 0) {
1935
+ return {
1936
+ ...geometry,
1937
+ width: geometry.width + geometry.offset * 2,
1938
+ height: geometry.height + geometry.offset * 2,
1939
+ radius: geometry.radius === 0 ? 0 : Math.max(0, geometry.radius + geometry.offset)
1940
+ };
1941
+ }
1942
+ return geometry;
1943
+ }
1709
1944
  setup() {
1710
1945
  if (!this.canvasService || !this.context) return;
1711
1946
  const canvas = this.canvasService.canvas;
@@ -1713,10 +1948,7 @@ var HoleTool = class {
1713
1948
  this.handleDielineChange = (geometry) => {
1714
1949
  this.currentGeometry = geometry;
1715
1950
  this.redraw();
1716
- const changed = this.enforceConstraints();
1717
- if (changed) {
1718
- this.syncHolesToDieline();
1719
- }
1951
+ this.enforceConstraints();
1720
1952
  };
1721
1953
  this.context.eventBus.on(
1722
1954
  "dieline:geometry:change",
@@ -1726,69 +1958,101 @@ var HoleTool = class {
1726
1958
  const commandService = this.context.services.get("CommandService");
1727
1959
  if (commandService) {
1728
1960
  try {
1729
- const geometry = commandService.executeCommand("getGeometry");
1730
- if (geometry) {
1731
- Promise.resolve(geometry).then((g) => {
1961
+ Promise.resolve(commandService.executeCommand("getGeometry")).then(
1962
+ (g) => {
1732
1963
  if (g) {
1733
1964
  this.currentGeometry = g;
1734
- this.enforceConstraints();
1735
- this.initializeHoles();
1965
+ this.redraw();
1736
1966
  }
1737
- });
1738
- }
1967
+ }
1968
+ );
1739
1969
  } catch (e) {
1740
1970
  }
1741
1971
  }
1742
1972
  if (!this.handleMoving) {
1743
1973
  this.handleMoving = (e) => {
1744
- var _a, _b, _c, _d, _e;
1974
+ var _a, _b, _c, _d;
1745
1975
  const target = e.target;
1746
- if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "hole-marker") return;
1976
+ if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return;
1747
1977
  if (!this.currentGeometry) return;
1748
- const index = (_c = (_b = target.data) == null ? void 0 : _b.index) != null ? _c : -1;
1749
- const holeData = this.holes[index];
1750
- const effectiveOffset = this.constraintTarget === "original" ? 0 : this.currentGeometry.offset;
1751
- const constraintGeometry = {
1752
- ...this.currentGeometry,
1753
- width: Math.max(0, this.currentGeometry.width + effectiveOffset * 2),
1754
- height: Math.max(
1755
- 0,
1756
- this.currentGeometry.height + effectiveOffset * 2
1757
- ),
1758
- radius: Math.max(0, this.currentGeometry.radius + effectiveOffset)
1759
- };
1760
- const p = new import_fabric4.Point(target.left, target.top);
1761
- const newPos = this.calculateConstrainedPosition(
1762
- p,
1763
- constraintGeometry,
1764
- (_d = holeData == null ? void 0 : holeData.innerRadius) != null ? _d : 15,
1765
- (_e = holeData == null ? void 0 : holeData.outerRadius) != null ? _e : 25
1978
+ let feature;
1979
+ if ((_b = target.data) == null ? void 0 : _b.isGroup) {
1980
+ const indices = (_c = target.data) == null ? void 0 : _c.indices;
1981
+ if (indices && indices.length > 0) {
1982
+ feature = this.features[indices[0]];
1983
+ }
1984
+ } else {
1985
+ const index = (_d = target.data) == null ? void 0 : _d.index;
1986
+ if (index !== void 0) {
1987
+ feature = this.features[index];
1988
+ }
1989
+ }
1990
+ const geometry = this.getGeometryForFeature(
1991
+ this.currentGeometry,
1992
+ feature
1766
1993
  );
1994
+ const p = new import_fabric4.Point(target.left, target.top);
1995
+ const markerStrokeWidth = (target.strokeWidth || 2) * (target.scaleX || 1);
1996
+ const minDim = Math.min(target.getScaledWidth(), target.getScaledHeight());
1997
+ const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
1998
+ const snapped = this.constrainPosition(p, geometry, limit);
1767
1999
  target.set({
1768
- left: newPos.x,
1769
- top: newPos.y
2000
+ left: snapped.x,
2001
+ top: snapped.y
1770
2002
  });
1771
2003
  };
1772
2004
  canvas.on("object:moving", this.handleMoving);
1773
2005
  }
1774
2006
  if (!this.handleModified) {
1775
2007
  this.handleModified = (e) => {
1776
- var _a;
2008
+ var _a, _b, _c, _d;
1777
2009
  const target = e.target;
1778
- if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "hole-marker") return;
1779
- const changed = this.enforceConstraints();
1780
- if (!changed) {
1781
- this.syncHolesFromCanvas();
2010
+ if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return;
2011
+ if ((_b = target.data) == null ? void 0 : _b.isGroup) {
2012
+ const groupObj = target;
2013
+ const indices = (_c = groupObj.data) == null ? void 0 : _c.indices;
2014
+ if (!indices) return;
2015
+ const groupCenter = new import_fabric4.Point(groupObj.left, groupObj.top);
2016
+ const newFeatures = [...this.features];
2017
+ const { x, y } = this.currentGeometry;
2018
+ groupObj.getObjects().forEach((child, i) => {
2019
+ const originalIndex = indices[i];
2020
+ const feature = this.features[originalIndex];
2021
+ const geometry = this.getGeometryForFeature(
2022
+ this.currentGeometry,
2023
+ feature
2024
+ );
2025
+ const { width, height } = geometry;
2026
+ const layoutLeft = x - width / 2;
2027
+ const layoutTop = y - height / 2;
2028
+ const absX = groupCenter.x + (child.left || 0);
2029
+ const absY = groupCenter.y + (child.top || 0);
2030
+ const normalizedX = width > 0 ? (absX - layoutLeft) / width : 0.5;
2031
+ const normalizedY = height > 0 ? (absY - layoutTop) / height : 0.5;
2032
+ newFeatures[originalIndex] = {
2033
+ ...newFeatures[originalIndex],
2034
+ x: normalizedX,
2035
+ y: normalizedY
2036
+ };
2037
+ });
2038
+ this.features = newFeatures;
2039
+ const configService = (_d = this.context) == null ? void 0 : _d.services.get(
2040
+ "ConfigurationService"
2041
+ );
2042
+ if (configService) {
2043
+ this.isUpdatingConfig = true;
2044
+ try {
2045
+ configService.update("dieline.features", this.features);
2046
+ } finally {
2047
+ this.isUpdatingConfig = false;
2048
+ }
2049
+ }
2050
+ } else {
2051
+ this.syncFeatureFromCanvas(target);
1782
2052
  }
1783
2053
  };
1784
2054
  canvas.on("object:modified", this.handleModified);
1785
2055
  }
1786
- this.initializeHoles();
1787
- }
1788
- initializeHoles() {
1789
- if (!this.canvasService) return;
1790
- this.redraw();
1791
- this.syncHolesToDieline();
1792
2056
  }
1793
2057
  teardown() {
1794
2058
  if (!this.canvasService) return;
@@ -1810,357 +2074,259 @@ var HoleTool = class {
1810
2074
  }
1811
2075
  const objects = canvas.getObjects().filter((obj) => {
1812
2076
  var _a;
1813
- return ((_a = obj.data) == null ? void 0 : _a.type) === "hole-marker";
2077
+ return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
1814
2078
  });
1815
2079
  objects.forEach((obj) => canvas.remove(obj));
1816
- if (this.context) {
1817
- const commandService = this.context.services.get("CommandService");
1818
- if (commandService) {
1819
- try {
1820
- commandService.executeCommand("setHoles", []);
1821
- } catch (e) {
1822
- }
1823
- }
1824
- }
1825
2080
  this.canvasService.requestRenderAll();
1826
2081
  }
1827
- syncHolesFromCanvas() {
1828
- if (!this.canvasService) return;
1829
- const objects = this.canvasService.canvas.getObjects().filter(
1830
- (obj) => {
1831
- var _a;
1832
- return ((_a = obj.data) == null ? void 0 : _a.type) === "hole-marker" || obj.name === "hole-marker";
1833
- }
1834
- );
1835
- if (objects.length === 0 && this.holes.length > 0) {
1836
- console.warn("HoleTool: No markers found on canvas to sync from");
1837
- return;
1838
- }
1839
- objects.sort(
1840
- (a, b) => {
1841
- var _a, _b, _c, _d;
1842
- return ((_b = (_a = a.data) == null ? void 0 : _a.index) != null ? _b : 0) - ((_d = (_c = b.data) == null ? void 0 : _c.index) != null ? _d : 0);
1843
- }
1844
- );
1845
- const newHoles = objects.map((obj, i) => {
1846
- var _a, _b, _c, _d;
1847
- const original = this.holes[i];
1848
- const newAbsX = obj.left;
1849
- const newAbsY = obj.top;
1850
- if (isNaN(newAbsX) || isNaN(newAbsY)) {
1851
- console.error("HoleTool: Invalid marker coordinates", {
1852
- newAbsX,
1853
- newAbsY
1854
- });
1855
- return original;
1856
- }
1857
- const scale = ((_a = this.currentGeometry) == null ? void 0 : _a.scale) || 1;
1858
- const unit = ((_b = this.currentGeometry) == null ? void 0 : _b.unit) || "mm";
1859
- const unitScale = Coordinate.convertUnit(1, "mm", unit);
1860
- if (original && original.anchor && this.currentGeometry) {
1861
- const { x, y, width, height } = this.currentGeometry;
1862
- let bx = x;
1863
- let by = y;
1864
- const left = x - width / 2;
1865
- const right = x + width / 2;
1866
- const top = y - height / 2;
1867
- const bottom = y + height / 2;
1868
- switch (original.anchor) {
1869
- case "top-left":
1870
- bx = left;
1871
- by = top;
1872
- break;
1873
- case "top-center":
1874
- bx = x;
1875
- by = top;
1876
- break;
1877
- case "top-right":
1878
- bx = right;
1879
- by = top;
1880
- break;
1881
- case "center-left":
1882
- bx = left;
1883
- by = y;
1884
- break;
1885
- case "center":
1886
- bx = x;
1887
- by = y;
1888
- break;
1889
- case "center-right":
1890
- bx = right;
1891
- by = y;
1892
- break;
1893
- case "bottom-left":
1894
- bx = left;
1895
- by = bottom;
1896
- break;
1897
- case "bottom-center":
1898
- bx = x;
1899
- by = bottom;
1900
- break;
1901
- case "bottom-right":
1902
- bx = right;
1903
- by = bottom;
1904
- break;
1905
- }
1906
- return {
1907
- ...original,
1908
- // Denormalize offset back to physical units (mm)
1909
- offsetX: (newAbsX - bx) / scale / unitScale,
1910
- offsetY: (newAbsY - by) / scale / unitScale,
1911
- // Clear direct coordinates if we use anchor
1912
- x: void 0,
1913
- y: void 0,
1914
- // Ensure other properties are preserved
1915
- innerRadius: original.innerRadius,
1916
- outerRadius: original.outerRadius,
1917
- shape: original.shape || "circle"
1918
- };
1919
- }
1920
- let normalizedX = 0.5;
1921
- let normalizedY = 0.5;
1922
- if (this.currentGeometry) {
1923
- const { x, y, width, height } = this.currentGeometry;
1924
- const left = x - width / 2;
1925
- const top = y - height / 2;
1926
- normalizedX = width > 0 ? (newAbsX - left) / width : 0.5;
1927
- normalizedY = height > 0 ? (newAbsY - top) / height : 0.5;
1928
- } else {
1929
- const { width, height } = this.canvasService.canvas;
1930
- normalizedX = Coordinate.toNormalized(newAbsX, width || 800);
1931
- normalizedY = Coordinate.toNormalized(newAbsY, height || 600);
1932
- }
1933
- return {
1934
- ...original,
1935
- x: normalizedX,
1936
- y: normalizedY,
1937
- // Clear offsets if we are using direct normalized coordinates
1938
- offsetX: void 0,
1939
- offsetY: void 0,
1940
- // Ensure other properties are preserved
1941
- innerRadius: (_c = original == null ? void 0 : original.innerRadius) != null ? _c : 15,
1942
- outerRadius: (_d = original == null ? void 0 : original.outerRadius) != null ? _d : 25,
1943
- shape: (original == null ? void 0 : original.shape) || "circle"
1944
- };
2082
+ constrainPosition(p, geometry, limit) {
2083
+ const nearest = getNearestPointOnDieline({ x: p.x, y: p.y }, {
2084
+ ...geometry,
2085
+ features: []
1945
2086
  });
1946
- this.holes = newHoles;
1947
- this.syncHolesToDieline();
2087
+ const dx = p.x - nearest.x;
2088
+ const dy = p.y - nearest.y;
2089
+ const dist = Math.sqrt(dx * dx + dy * dy);
2090
+ if (dist <= limit) {
2091
+ return { x: p.x, y: p.y };
2092
+ }
2093
+ const scale = limit / dist;
2094
+ return {
2095
+ x: nearest.x + dx * scale,
2096
+ y: nearest.y + dy * scale
2097
+ };
1948
2098
  }
1949
- syncHolesToDieline() {
1950
- if (!this.context || !this.canvasService) return;
2099
+ syncFeatureFromCanvas(target) {
2100
+ var _a;
2101
+ if (!this.currentGeometry || !this.context) return;
2102
+ const index = (_a = target.data) == null ? void 0 : _a.index;
2103
+ if (index === void 0 || index < 0 || index >= this.features.length)
2104
+ return;
2105
+ const feature = this.features[index];
2106
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
2107
+ const { width, height, x, y } = geometry;
2108
+ const left = x - width / 2;
2109
+ const top = y - height / 2;
2110
+ const normalizedX = width > 0 ? (target.left - left) / width : 0.5;
2111
+ const normalizedY = height > 0 ? (target.top - top) / height : 0.5;
2112
+ const updatedFeature = {
2113
+ ...feature,
2114
+ x: normalizedX,
2115
+ y: normalizedY
2116
+ // Could also update rotation if we allowed rotating markers
2117
+ };
2118
+ const newFeatures = [...this.features];
2119
+ newFeatures[index] = updatedFeature;
2120
+ this.features = newFeatures;
1951
2121
  const configService = this.context.services.get(
1952
2122
  "ConfigurationService"
1953
2123
  );
1954
2124
  if (configService) {
1955
2125
  this.isUpdatingConfig = true;
1956
2126
  try {
1957
- configService.update("dieline.holes", this.holes);
2127
+ configService.update("dieline.features", this.features);
1958
2128
  } finally {
1959
2129
  this.isUpdatingConfig = false;
1960
2130
  }
1961
2131
  }
1962
2132
  }
1963
2133
  redraw() {
1964
- if (!this.canvasService) return;
2134
+ if (!this.canvasService || !this.currentGeometry) return;
1965
2135
  const canvas = this.canvasService.canvas;
1966
- const { width, height } = canvas;
2136
+ const geometry = this.currentGeometry;
1967
2137
  const existing = canvas.getObjects().filter((obj) => {
1968
2138
  var _a;
1969
- return ((_a = obj.data) == null ? void 0 : _a.type) === "hole-marker";
2139
+ return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
1970
2140
  });
1971
2141
  existing.forEach((obj) => canvas.remove(obj));
1972
- const holes = this.holes;
1973
- if (!holes || holes.length === 0) {
2142
+ if (!this.features || this.features.length === 0) {
1974
2143
  this.canvasService.requestRenderAll();
1975
2144
  return;
1976
2145
  }
1977
- const geometry = this.currentGeometry || {
1978
- x: (width || 800) / 2,
1979
- y: (height || 600) / 2,
1980
- width: width || 800,
1981
- height: height || 600,
1982
- scale: 1
1983
- // Default scale if no geometry loaded
2146
+ const scale = geometry.scale || 1;
2147
+ const finalScale = scale;
2148
+ const groups = {};
2149
+ const singles = [];
2150
+ this.features.forEach((f, i) => {
2151
+ if (f.groupId) {
2152
+ if (!groups[f.groupId]) groups[f.groupId] = [];
2153
+ groups[f.groupId].push({ feature: f, index: i });
2154
+ } else {
2155
+ singles.push({ feature: f, index: i });
2156
+ }
2157
+ });
2158
+ const createMarkerShape = (feature, pos) => {
2159
+ const featureScale = scale;
2160
+ const visualWidth = (feature.width || 10) * featureScale;
2161
+ const visualHeight = (feature.height || 10) * featureScale;
2162
+ const visualRadius = (feature.radius || 0) * featureScale;
2163
+ const color = feature.color || (feature.operation === "add" ? "#00FF00" : "#FF0000");
2164
+ const strokeDash = feature.strokeDash || (feature.operation === "subtract" ? [4, 4] : void 0);
2165
+ let shape;
2166
+ if (feature.shape === "rect") {
2167
+ shape = new import_fabric4.Rect({
2168
+ width: visualWidth,
2169
+ height: visualHeight,
2170
+ rx: visualRadius,
2171
+ ry: visualRadius,
2172
+ fill: "transparent",
2173
+ stroke: color,
2174
+ strokeWidth: 2,
2175
+ strokeDashArray: strokeDash,
2176
+ originX: "center",
2177
+ originY: "center",
2178
+ left: pos.x,
2179
+ top: pos.y
2180
+ });
2181
+ } else {
2182
+ shape = new import_fabric4.Circle({
2183
+ radius: visualRadius || 5 * finalScale,
2184
+ fill: "transparent",
2185
+ stroke: color,
2186
+ strokeWidth: 2,
2187
+ strokeDashArray: strokeDash,
2188
+ originX: "center",
2189
+ originY: "center",
2190
+ left: pos.x,
2191
+ top: pos.y
2192
+ });
2193
+ }
2194
+ if (feature.rotation) {
2195
+ shape.rotate(feature.rotation);
2196
+ }
2197
+ return shape;
1984
2198
  };
1985
- holes.forEach((hole, index) => {
1986
- const scale = geometry.scale || 1;
1987
- const unit = geometry.unit || "mm";
1988
- const unitScale = Coordinate.convertUnit(1, "mm", unit);
1989
- const visualInnerRadius = hole.innerRadius * unitScale * scale;
1990
- const visualOuterRadius = hole.outerRadius * unitScale * scale;
1991
- const pos = resolveHolePosition(
1992
- {
1993
- ...hole,
1994
- offsetX: (hole.offsetX || 0) * unitScale * scale,
1995
- offsetY: (hole.offsetY || 0) * unitScale * scale
1996
- },
1997
- geometry,
1998
- { width: geometry.width, height: geometry.height }
1999
- // Use geometry dims instead of canvas
2199
+ singles.forEach(({ feature, index }) => {
2200
+ const geometry2 = this.getGeometryForFeature(
2201
+ this.currentGeometry,
2202
+ feature
2000
2203
  );
2001
- const isSquare = hole.shape === "square";
2002
- const innerMarker = isSquare ? new import_fabric4.Rect({
2003
- width: visualInnerRadius * 2,
2004
- height: visualInnerRadius * 2,
2005
- fill: "transparent",
2006
- stroke: "red",
2007
- strokeWidth: 2,
2008
- originX: "center",
2009
- originY: "center"
2010
- }) : new import_fabric4.Circle({
2011
- radius: visualInnerRadius,
2012
- fill: "transparent",
2013
- stroke: "red",
2014
- strokeWidth: 2,
2015
- originX: "center",
2016
- originY: "center"
2204
+ const pos = resolveFeaturePosition(feature, geometry2);
2205
+ const marker = createMarkerShape(feature, pos);
2206
+ marker.set({
2207
+ selectable: true,
2208
+ hasControls: false,
2209
+ hasBorders: false,
2210
+ hoverCursor: "move",
2211
+ lockRotation: true,
2212
+ lockScalingX: true,
2213
+ lockScalingY: true,
2214
+ data: { type: "feature-marker", index, isGroup: false }
2017
2215
  });
2018
- const outerMarker = isSquare ? new import_fabric4.Rect({
2019
- width: visualOuterRadius * 2,
2020
- height: visualOuterRadius * 2,
2021
- fill: "transparent",
2022
- stroke: "#666",
2023
- strokeWidth: 1,
2024
- strokeDashArray: [5, 5],
2025
- originX: "center",
2026
- originY: "center"
2027
- }) : new import_fabric4.Circle({
2028
- radius: visualOuterRadius,
2029
- fill: "transparent",
2030
- stroke: "#666",
2031
- strokeWidth: 1,
2032
- strokeDashArray: [5, 5],
2033
- originX: "center",
2034
- originY: "center"
2216
+ marker.set("opacity", 0);
2217
+ marker.on("mouseover", () => {
2218
+ marker.set("opacity", 1);
2219
+ canvas.requestRenderAll();
2035
2220
  });
2036
- const holeGroup = new import_fabric4.Group([outerMarker, innerMarker], {
2037
- left: pos.x,
2038
- top: pos.y,
2039
- originX: "center",
2040
- originY: "center",
2221
+ marker.on("mouseout", () => {
2222
+ if (canvas.getActiveObject() !== marker) {
2223
+ marker.set("opacity", 0);
2224
+ canvas.requestRenderAll();
2225
+ }
2226
+ });
2227
+ marker.on("selected", () => {
2228
+ marker.set("opacity", 1);
2229
+ canvas.requestRenderAll();
2230
+ });
2231
+ marker.on("deselected", () => {
2232
+ marker.set("opacity", 0);
2233
+ canvas.requestRenderAll();
2234
+ });
2235
+ canvas.add(marker);
2236
+ canvas.bringObjectToFront(marker);
2237
+ });
2238
+ Object.keys(groups).forEach((groupId) => {
2239
+ const members = groups[groupId];
2240
+ if (members.length === 0) return;
2241
+ const shapes = members.map(({ feature }) => {
2242
+ const geometry2 = this.getGeometryForFeature(
2243
+ this.currentGeometry,
2244
+ feature
2245
+ );
2246
+ const pos = resolveFeaturePosition(feature, geometry2);
2247
+ return createMarkerShape(feature, pos);
2248
+ });
2249
+ const groupObj = new import_fabric4.Group(shapes, {
2041
2250
  selectable: true,
2042
2251
  hasControls: false,
2043
- // Don't allow resizing/rotating
2044
2252
  hasBorders: false,
2045
- subTargetCheck: false,
2046
- opacity: 0,
2047
- // Default hidden
2048
2253
  hoverCursor: "move",
2049
- data: { type: "hole-marker", index }
2254
+ lockRotation: true,
2255
+ lockScalingX: true,
2256
+ lockScalingY: true,
2257
+ subTargetCheck: true,
2258
+ // Allow events to pass through if needed, but we treat as one
2259
+ interactive: false,
2260
+ // Children not interactive
2261
+ // @ts-ignore
2262
+ data: {
2263
+ type: "feature-marker",
2264
+ isGroup: true,
2265
+ groupId,
2266
+ indices: members.map((m) => m.index)
2267
+ }
2050
2268
  });
2051
- holeGroup.name = "hole-marker";
2052
- holeGroup.on("mouseover", () => {
2053
- holeGroup.set("opacity", 1);
2269
+ groupObj.set("opacity", 0);
2270
+ groupObj.on("mouseover", () => {
2271
+ groupObj.set("opacity", 1);
2054
2272
  canvas.requestRenderAll();
2055
2273
  });
2056
- holeGroup.on("mouseout", () => {
2057
- if (canvas.getActiveObject() !== holeGroup) {
2058
- holeGroup.set("opacity", 0);
2274
+ groupObj.on("mouseout", () => {
2275
+ if (canvas.getActiveObject() !== groupObj) {
2276
+ groupObj.set("opacity", 0);
2059
2277
  canvas.requestRenderAll();
2060
2278
  }
2061
2279
  });
2062
- holeGroup.on("selected", () => {
2063
- holeGroup.set("opacity", 1);
2280
+ groupObj.on("selected", () => {
2281
+ groupObj.set("opacity", 1);
2064
2282
  canvas.requestRenderAll();
2065
2283
  });
2066
- holeGroup.on("deselected", () => {
2067
- holeGroup.set("opacity", 0);
2284
+ groupObj.on("deselected", () => {
2285
+ groupObj.set("opacity", 0);
2068
2286
  canvas.requestRenderAll();
2069
2287
  });
2070
- canvas.add(holeGroup);
2071
- canvas.bringObjectToFront(holeGroup);
2072
- });
2073
- const markers = canvas.getObjects().filter((o) => {
2074
- var _a;
2075
- return ((_a = o.data) == null ? void 0 : _a.type) === "hole-marker";
2288
+ canvas.add(groupObj);
2289
+ canvas.bringObjectToFront(groupObj);
2076
2290
  });
2077
- markers.forEach((m) => canvas.bringObjectToFront(m));
2078
2291
  this.canvasService.requestRenderAll();
2079
2292
  }
2080
2293
  enforceConstraints() {
2081
- const geometry = this.currentGeometry;
2082
- if (!geometry || !this.canvasService) {
2083
- return false;
2084
- }
2085
- const effectiveOffset = this.constraintTarget === "original" ? 0 : geometry.offset;
2086
- const constraintGeometry = {
2087
- ...geometry,
2088
- width: Math.max(0, geometry.width + effectiveOffset * 2),
2089
- height: Math.max(0, geometry.height + effectiveOffset * 2),
2090
- radius: Math.max(0, geometry.radius + effectiveOffset)
2091
- };
2092
- const objects = this.canvasService.canvas.getObjects().filter((obj) => {
2294
+ if (!this.canvasService || !this.currentGeometry) return;
2295
+ const canvas = this.canvasService.canvas;
2296
+ const markers = canvas.getObjects().filter((obj) => {
2093
2297
  var _a;
2094
- return ((_a = obj.data) == null ? void 0 : _a.type) === "hole-marker";
2298
+ return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
2095
2299
  });
2096
- let changed = false;
2097
- objects.sort(
2098
- (a, b) => {
2099
- var _a, _b, _c, _d;
2100
- return ((_b = (_a = a.data) == null ? void 0 : _a.index) != null ? _b : 0) - ((_d = (_c = b.data) == null ? void 0 : _c.index) != null ? _d : 0);
2300
+ markers.forEach((marker) => {
2301
+ var _a, _b, _c;
2302
+ let feature;
2303
+ if ((_a = marker.data) == null ? void 0 : _a.isGroup) {
2304
+ const indices = (_b = marker.data) == null ? void 0 : _b.indices;
2305
+ if (indices && indices.length > 0) {
2306
+ feature = this.features[indices[0]];
2307
+ }
2308
+ } else {
2309
+ const index = (_c = marker.data) == null ? void 0 : _c.index;
2310
+ if (index !== void 0) {
2311
+ feature = this.features[index];
2312
+ }
2101
2313
  }
2102
- );
2103
- const newHoles = [];
2104
- objects.forEach((obj, i) => {
2105
- var _a, _b;
2106
- const currentPos = new import_fabric4.Point(obj.left, obj.top);
2107
- const holeData = this.holes[i];
2108
- const scale = geometry.scale || 1;
2109
- const unit = geometry.unit || "mm";
2110
- const unitScale = Coordinate.convertUnit(1, "mm", unit);
2111
- const innerR = ((_a = holeData == null ? void 0 : holeData.innerRadius) != null ? _a : 15) * unitScale * scale;
2112
- const outerR = ((_b = holeData == null ? void 0 : holeData.outerRadius) != null ? _b : 25) * unitScale * scale;
2113
- const newPos = this.calculateConstrainedPosition(
2114
- currentPos,
2115
- constraintGeometry,
2116
- innerR,
2117
- outerR
2314
+ const geometry = this.getGeometryForFeature(
2315
+ this.currentGeometry,
2316
+ feature
2118
2317
  );
2119
- if (currentPos.distanceFrom(newPos) > 0.1) {
2120
- obj.set({
2121
- left: newPos.x,
2122
- top: newPos.y
2123
- });
2124
- obj.setCoords();
2125
- changed = true;
2126
- }
2318
+ const markerStrokeWidth = (marker.strokeWidth || 2) * (marker.scaleX || 1);
2319
+ const minDim = Math.min(marker.getScaledWidth(), marker.getScaledHeight());
2320
+ const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
2321
+ const snapped = this.constrainPosition(
2322
+ new import_fabric4.Point(marker.left, marker.top),
2323
+ geometry,
2324
+ limit
2325
+ );
2326
+ marker.set({ left: snapped.x, top: snapped.y });
2327
+ marker.setCoords();
2127
2328
  });
2128
- if (changed) {
2129
- this.syncHolesFromCanvas();
2130
- return true;
2131
- }
2132
- return false;
2133
- }
2134
- calculateConstrainedPosition(p, g, innerRadius, outerRadius) {
2135
- const options = {
2136
- ...g,
2137
- holes: []
2138
- // We don't need holes for boundary calculation
2139
- };
2140
- const nearest = getNearestPointOnDieline(
2141
- { x: p.x, y: p.y },
2142
- options
2143
- );
2144
- const nearestP = new import_fabric4.Point(nearest.x, nearest.y);
2145
- const dist = p.distanceFrom(nearestP);
2146
- const v = p.subtract(nearestP);
2147
- const center = new import_fabric4.Point(g.x, g.y);
2148
- const distToCenter = p.distanceFrom(center);
2149
- const nearestDistToCenter = nearestP.distanceFrom(center);
2150
- let signedDist = dist;
2151
- if (distToCenter < nearestDistToCenter) {
2152
- signedDist = -dist;
2153
- }
2154
- let clampedDist = signedDist;
2155
- if (signedDist > 0) {
2156
- clampedDist = Math.min(signedDist, innerRadius);
2157
- } else {
2158
- clampedDist = Math.max(signedDist, -outerRadius);
2159
- }
2160
- if (dist < 1e-3) return nearestP;
2161
- const scale = Math.abs(clampedDist) / (dist || 1);
2162
- const offset = v.scalarMultiply(scale);
2163
- return nearestP.add(offset);
2329
+ canvas.requestRenderAll();
2164
2330
  }
2165
2331
  };
2166
2332
 
@@ -2175,6 +2341,7 @@ var ImageTool = class {
2175
2341
  };
2176
2342
  this.items = [];
2177
2343
  this.objectMap = /* @__PURE__ */ new Map();
2344
+ this.loadResolvers = /* @__PURE__ */ new Map();
2178
2345
  this.isUpdatingConfig = false;
2179
2346
  }
2180
2347
  activate(context) {
@@ -2184,19 +2351,15 @@ var ImageTool = class {
2184
2351
  console.warn("CanvasService not found for ImageTool");
2185
2352
  return;
2186
2353
  }
2187
- const configService = context.services.get("ConfigurationService");
2354
+ const configService = context.services.get(
2355
+ "ConfigurationService"
2356
+ );
2188
2357
  if (configService) {
2189
2358
  this.items = configService.get("image.items", []) || [];
2190
2359
  configService.onAnyChange((e) => {
2191
2360
  if (this.isUpdatingConfig) return;
2192
- let shouldUpdate = false;
2193
2361
  if (e.key === "image.items") {
2194
2362
  this.items = e.value || [];
2195
- shouldUpdate = true;
2196
- } else if (e.key.startsWith("dieline.") && e.key !== "dieline.holes") {
2197
- shouldUpdate = true;
2198
- }
2199
- if (shouldUpdate) {
2200
2363
  this.updateImages();
2201
2364
  }
2202
2365
  });
@@ -2232,15 +2395,39 @@ var ImageTool = class {
2232
2395
  {
2233
2396
  command: "addImage",
2234
2397
  title: "Add Image",
2235
- handler: (url, options) => {
2398
+ handler: async (url, options) => {
2399
+ const id = this.generateId();
2236
2400
  const newItem = {
2237
- id: this.generateId(),
2401
+ id,
2238
2402
  url,
2239
2403
  opacity: 1,
2240
2404
  ...options
2241
2405
  };
2406
+ const promise = new Promise((resolve) => {
2407
+ this.loadResolvers.set(id, () => resolve(id));
2408
+ });
2242
2409
  this.updateConfig([...this.items, newItem]);
2243
- return newItem.id;
2410
+ return promise;
2411
+ }
2412
+ },
2413
+ {
2414
+ command: "fitImageToArea",
2415
+ title: "Fit Image to Area",
2416
+ handler: (id, area) => {
2417
+ var _a, _b;
2418
+ const item = this.items.find((i) => i.id === id);
2419
+ const obj = this.objectMap.get(id);
2420
+ if (item && obj && obj.width && obj.height) {
2421
+ const scale = Math.max(
2422
+ area.width / obj.width,
2423
+ area.height / obj.height
2424
+ );
2425
+ this.updateImageInConfig(id, {
2426
+ scale,
2427
+ left: (_a = area.left) != null ? _a : 0.5,
2428
+ top: (_b = area.top) != null ? _b : 0.5
2429
+ });
2430
+ }
2244
2431
  }
2245
2432
  },
2246
2433
  {
@@ -2308,7 +2495,9 @@ var ImageTool = class {
2308
2495
  if (!this.context) return;
2309
2496
  this.isUpdatingConfig = true;
2310
2497
  this.items = newItems;
2311
- const configService = this.context.services.get("ConfigurationService");
2498
+ const configService = this.context.services.get(
2499
+ "ConfigurationService"
2500
+ );
2312
2501
  if (configService) {
2313
2502
  configService.update("image.items", newItems);
2314
2503
  }
@@ -2354,53 +2543,12 @@ var ImageTool = class {
2354
2543
  var _a, _b;
2355
2544
  const canvasW = ((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 800;
2356
2545
  const canvasH = ((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 600;
2357
- let layoutScale = 1;
2358
- let layoutOffsetX = 0;
2359
- let layoutOffsetY = 0;
2360
- let visualWidth = canvasW;
2361
- let visualHeight = canvasH;
2362
- let dielinePhysicalWidth = 500;
2363
- let dielinePhysicalHeight = 500;
2364
- let bleedOffset = 0;
2365
- if (this.context) {
2366
- const configService = this.context.services.get("ConfigurationService");
2367
- if (configService) {
2368
- dielinePhysicalWidth = configService.get("dieline.width") || 500;
2369
- dielinePhysicalHeight = configService.get("dieline.height") || 500;
2370
- bleedOffset = configService.get("dieline.offset") || 0;
2371
- const paddingValue = configService.get("dieline.padding") || 40;
2372
- let padding = 0;
2373
- if (typeof paddingValue === "number") {
2374
- padding = paddingValue;
2375
- } else if (typeof paddingValue === "string") {
2376
- if (paddingValue.endsWith("%")) {
2377
- const percent = parseFloat(paddingValue) / 100;
2378
- padding = Math.min(canvasW, canvasH) * percent;
2379
- } else {
2380
- padding = parseFloat(paddingValue) || 0;
2381
- }
2382
- }
2383
- const layout = Coordinate.calculateLayout(
2384
- { width: canvasW, height: canvasH },
2385
- { width: dielinePhysicalWidth, height: dielinePhysicalHeight },
2386
- padding
2387
- );
2388
- layoutScale = layout.scale;
2389
- layoutOffsetX = layout.offsetX;
2390
- layoutOffsetY = layout.offsetY;
2391
- visualWidth = layout.width;
2392
- visualHeight = layout.height;
2393
- }
2394
- }
2395
2546
  return {
2396
- layoutScale,
2397
- layoutOffsetX,
2398
- layoutOffsetY,
2399
- visualWidth,
2400
- visualHeight,
2401
- dielinePhysicalWidth,
2402
- dielinePhysicalHeight,
2403
- bleedOffset
2547
+ layoutScale: 1,
2548
+ layoutOffsetX: 0,
2549
+ layoutOffsetY: 0,
2550
+ visualWidth: canvasW,
2551
+ visualHeight: canvasH
2404
2552
  };
2405
2553
  }
2406
2554
  updateImages() {
@@ -2423,8 +2571,8 @@ var ImageTool = class {
2423
2571
  if (!obj) {
2424
2572
  this.loadImage(item, layer, layout);
2425
2573
  } else {
2426
- this.updateObjectProperties(obj, item, layout);
2427
2574
  layer.remove(obj);
2575
+ this.updateObjectProperties(obj, item, layout);
2428
2576
  layer.add(obj);
2429
2577
  }
2430
2578
  });
@@ -2432,10 +2580,17 @@ var ImageTool = class {
2432
2580
  this.canvasService.requestRenderAll();
2433
2581
  }
2434
2582
  updateObjectProperties(obj, item, layout) {
2435
- const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight } = layout;
2583
+ const {
2584
+ layoutScale,
2585
+ layoutOffsetX,
2586
+ layoutOffsetY,
2587
+ visualWidth,
2588
+ visualHeight
2589
+ } = layout;
2436
2590
  const updates = {};
2437
2591
  if (obj.opacity !== item.opacity) updates.opacity = item.opacity;
2438
- if (item.angle !== void 0 && obj.angle !== item.angle) updates.angle = item.angle;
2592
+ if (item.angle !== void 0 && obj.angle !== item.angle)
2593
+ updates.angle = item.angle;
2439
2594
  if (item.left !== void 0) {
2440
2595
  const globalLeft = layoutOffsetX + item.left * visualWidth;
2441
2596
  if (Math.abs(obj.left - globalLeft) > 1) updates.left = globalLeft;
@@ -2444,13 +2599,12 @@ var ImageTool = class {
2444
2599
  const globalTop = layoutOffsetY + item.top * visualHeight;
2445
2600
  if (Math.abs(obj.top - globalTop) > 1) updates.top = globalTop;
2446
2601
  }
2447
- if (item.width !== void 0 && obj.width) {
2448
- const targetScaleX = item.width * layoutScale / obj.width;
2449
- if (Math.abs(obj.scaleX - targetScaleX) > 1e-3) updates.scaleX = targetScaleX;
2450
- }
2451
- if (item.height !== void 0 && obj.height) {
2452
- const targetScaleY = item.height * layoutScale / obj.height;
2453
- if (Math.abs(obj.scaleY - targetScaleY) > 1e-3) updates.scaleY = targetScaleY;
2602
+ if (item.scale !== void 0) {
2603
+ const targetScale = item.scale * layoutScale;
2604
+ if (Math.abs(obj.scaleX - targetScale) > 1e-3) {
2605
+ updates.scaleX = targetScale;
2606
+ updates.scaleY = targetScale;
2607
+ }
2454
2608
  }
2455
2609
  if (obj.originX !== "center") {
2456
2610
  updates.originX = "center";
@@ -2458,6 +2612,7 @@ var ImageTool = class {
2458
2612
  }
2459
2613
  if (Object.keys(updates).length > 0) {
2460
2614
  obj.set(updates);
2615
+ obj.setCoords();
2461
2616
  }
2462
2617
  }
2463
2618
  loadImage(item, layer, layout) {
@@ -2477,18 +2632,10 @@ var ImageTool = class {
2477
2632
  ml: false,
2478
2633
  mr: false
2479
2634
  });
2480
- let { width, height, left, top } = item;
2481
- const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight, dielinePhysicalWidth, dielinePhysicalHeight, bleedOffset } = layout;
2482
- if (width === void 0 && height === void 0) {
2483
- const targetWidth = dielinePhysicalWidth + 2 * bleedOffset;
2484
- const targetHeight = dielinePhysicalHeight + 2 * bleedOffset;
2485
- const targetMax = Math.max(targetWidth, targetHeight);
2486
- const imageMax = Math.max(image.width || 1, image.height || 1);
2487
- const scale = targetMax / imageMax;
2488
- width = (image.width || 1) * scale;
2489
- height = (image.height || 1) * scale;
2490
- item.width = width;
2491
- item.height = height;
2635
+ let { scale, left, top } = item;
2636
+ if (scale === void 0) {
2637
+ scale = 1;
2638
+ item.scale = scale;
2492
2639
  }
2493
2640
  if (left === void 0 && top === void 0) {
2494
2641
  left = 0.5;
@@ -2499,13 +2646,18 @@ var ImageTool = class {
2499
2646
  this.updateObjectProperties(image, item, layout);
2500
2647
  layer.add(image);
2501
2648
  this.objectMap.set(item.id, image);
2649
+ const resolver = this.loadResolvers.get(item.id);
2650
+ if (resolver) {
2651
+ resolver();
2652
+ this.loadResolvers.delete(item.id);
2653
+ }
2502
2654
  image.on("modified", (e) => {
2503
2655
  this.handleObjectModified(item.id, image);
2504
2656
  });
2505
2657
  layer.dirty = true;
2506
2658
  (_a = this.canvasService) == null ? void 0 : _a.requestRenderAll();
2507
- if (item.width !== width || item.height !== height || item.left !== left || item.top !== top) {
2508
- this.updateImageInConfig(item.id, { width, height, left, top });
2659
+ if (item.scale !== scale || item.left !== left || item.top !== top) {
2660
+ this.updateImageInConfig(item.id, { scale, left, top }, true);
2509
2661
  }
2510
2662
  }).catch((err) => {
2511
2663
  console.error("Failed to load image", item.url, err);
@@ -2513,29 +2665,28 @@ var ImageTool = class {
2513
2665
  }
2514
2666
  handleObjectModified(id, image) {
2515
2667
  const layout = this.getLayoutInfo();
2516
- const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight } = layout;
2668
+ const {
2669
+ layoutScale,
2670
+ layoutOffsetX,
2671
+ layoutOffsetY,
2672
+ visualWidth,
2673
+ visualHeight
2674
+ } = layout;
2517
2675
  const matrix = image.calcTransformMatrix();
2518
2676
  const globalPoint = import_fabric5.util.transformPoint(new import_fabric5.Point(0, 0), matrix);
2519
2677
  const updates = {};
2520
2678
  updates.left = (globalPoint.x - layoutOffsetX) / visualWidth;
2521
2679
  updates.top = (globalPoint.y - layoutOffsetY) / visualHeight;
2522
2680
  updates.angle = image.angle;
2523
- if (image.width) {
2524
- const pixelWidth = image.width * image.scaleX;
2525
- updates.width = pixelWidth / layoutScale;
2526
- }
2527
- if (image.height) {
2528
- const pixelHeight = image.height * image.scaleY;
2529
- updates.height = pixelHeight / layoutScale;
2530
- }
2531
- this.updateImageInConfig(id, updates);
2681
+ updates.scale = image.scaleX / layoutScale;
2682
+ this.updateImageInConfig(id, updates, true);
2532
2683
  }
2533
- updateImageInConfig(id, updates) {
2684
+ updateImageInConfig(id, updates, skipCanvasUpdate = false) {
2534
2685
  const index = this.items.findIndex((i) => i.id === id);
2535
2686
  if (index !== -1) {
2536
2687
  const newItems = [...this.items];
2537
2688
  newItems[index] = { ...newItems[index], ...updates };
2538
- this.updateConfig(newItems, true);
2689
+ this.updateConfig(newItems, skipCanvasUpdate);
2539
2690
  }
2540
2691
  }
2541
2692
  };
@@ -3381,8 +3532,8 @@ var CanvasService = class {
3381
3532
  BackgroundTool,
3382
3533
  CanvasService,
3383
3534
  DielineTool,
3535
+ FeatureTool,
3384
3536
  FilmTool,
3385
- HoleTool,
3386
3537
  ImageTool,
3387
3538
  MirrorTool,
3388
3539
  RulerTool,