@pooder/kit 3.4.0 → 4.0.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, _c, _d, _e;
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;
@@ -272,23 +273,37 @@ var ImageTracer = class {
272
273
  Math.floor(Math.max(width, height) * 0.02)
273
274
  );
274
275
  const radius = (_b = options.morphologyRadius) != null ? _b : adaptiveRadius;
275
- let mask = this.createMask(imageData, threshold);
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);
276
281
  if (radius > 0) {
277
- mask = this.dilate(mask, width, height, radius);
278
- mask = this.erode(mask, width, height, radius);
279
- mask = this.fillHoles(mask, width, height);
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");
280
291
  }
281
- const allContourPoints = this.traceAllContours(mask, width, height);
292
+ const allContourPoints = this.traceAllContours(mask, paddedWidth, paddedHeight);
282
293
  if (allContourPoints.length === 0) {
283
- const w = (_c = options.scaleToWidth) != null ? _c : width;
284
- const h = (_d = options.scaleToHeight) != null ? _d : height;
294
+ const w = (_d = options.scaleToWidth) != null ? _d : width;
295
+ const h = (_e = options.scaleToHeight) != null ? _e : height;
285
296
  return `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`;
286
297
  }
287
298
  const primaryContour = allContourPoints.sort(
288
299
  (a, b) => b.length - a.length
289
300
  )[0];
301
+ const unpaddedPoints = primaryContour.map((p) => ({
302
+ x: p.x - padding,
303
+ y: p.y - padding
304
+ }));
290
305
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
291
- for (const p of primaryContour) {
306
+ for (const p of unpaddedPoints) {
292
307
  if (p.x < minX) minX = p.x;
293
308
  if (p.y < minY) minY = p.y;
294
309
  if (p.x > maxX) maxX = p.x;
@@ -300,95 +315,119 @@ var ImageTracer = class {
300
315
  width: maxX - minX,
301
316
  height: maxY - minY
302
317
  };
303
- let finalPoints = primaryContour;
318
+ let finalPoints = unpaddedPoints;
304
319
  if (options.scaleToWidth && options.scaleToHeight) {
305
320
  finalPoints = this.scalePoints(
306
- primaryContour,
321
+ unpaddedPoints,
307
322
  options.scaleToWidth,
308
323
  options.scaleToHeight,
309
324
  globalBounds
310
325
  );
311
326
  }
312
- const simplifiedPoints = this.douglasPeucker(
313
- finalPoints,
314
- (_e = options.simplifyTolerance) != null ? _e : 2
315
- );
316
- 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
+ }
317
337
  }
318
- static createMask(imageData, threshold) {
338
+ static createMask(imageData, threshold, padding, paddedWidth, paddedHeight) {
319
339
  const { width, height, data } = imageData;
320
- const mask = new Uint8Array(width * height);
321
- for (let i = 0; i < width * height; i++) {
322
- const idx = i * 4;
323
- const r = data[idx];
324
- const g = data[idx + 1];
325
- const b = data[idx + 2];
326
- const a = data[idx + 3];
327
- if (a > threshold && !(r > 240 && g > 240 && b > 240)) {
328
- mask[i] = 1;
329
- } else {
330
- mask[i] = 0;
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;
331
346
  }
332
347
  }
333
- return mask;
334
- }
335
- /**
336
- * Fast 1D-separable Dilation
337
- */
338
- static dilate(mask, width, height, radius) {
339
- const horizontal = new Uint8Array(width * height);
340
348
  for (let y = 0; y < height; y++) {
341
- let count = 0;
342
- for (let x = -radius; x < width; x++) {
343
- if (x + radius < width && mask[y * width + x + radius]) count++;
344
- if (x - radius - 1 >= 0 && mask[y * width + x - radius - 1]) count--;
345
- if (x >= 0) horizontal[y * width + x] = count > 0 ? 1 : 0;
346
- }
347
- }
348
- const vertical = new Uint8Array(width * height);
349
- for (let x = 0; x < width; x++) {
350
- let count = 0;
351
- for (let y = -radius; y < height; y++) {
352
- if (y + radius < height && horizontal[(y + radius) * width + x])
353
- count++;
354
- if (y - radius - 1 >= 0 && horizontal[(y - radius - 1) * width + x])
355
- count--;
356
- if (y >= 0) vertical[y * width + x] = count > 0 ? 1 : 0;
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
+ }
357
365
  }
358
366
  }
359
- return vertical;
367
+ return mask;
360
368
  }
361
369
  /**
362
- * Fast 1D-separable Erosion
370
+ * Fast circular morphology using a distance-transform inspired separable approach.
371
+ * O(N * R) complexity, where R is the radius.
363
372
  */
364
- static erode(mask, width, height, radius) {
365
- const horizontal = new Uint8Array(width * height);
366
- for (let y = 0; y < height; y++) {
367
- let count = 0;
368
- for (let x = -radius; x < width; x++) {
369
- if (x + radius < width && mask[y * width + x + radius]) count++;
370
- if (x - radius - 1 >= 0 && mask[y * width + x - radius - 1]) count--;
371
- if (x >= 0) {
372
- const winWidth = Math.min(x + radius, width - 1) - Math.max(x - radius, 0) + 1;
373
- horizontal[y * width + x] = count === winWidth ? 1 : 0;
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
+ );
374
389
  }
375
390
  }
376
- }
377
- const vertical = new Uint8Array(width * height);
378
- for (let x = 0; x < width; x++) {
379
- let count = 0;
380
- for (let y = -radius; y < height; y++) {
381
- if (y + radius < height && horizontal[(y + radius) * width + x])
382
- count++;
383
- if (y - radius - 1 >= 0 && horizontal[(y - radius - 1) * width + x])
384
- count--;
385
- if (y >= 0) {
386
- const winHeight = Math.min(y + radius, height - 1) - Math.max(y - radius, 0) + 1;
387
- vertical[y * width + x] = count === winHeight ? 1 : 0;
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;
388
407
  }
389
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;
390
430
  }
391
- return vertical;
392
431
  }
393
432
  /**
394
433
  * Fills internal holes in the binary mask using flood fill from edges.
@@ -588,6 +627,23 @@ var ImageTracer = class {
588
627
  const tail = points.slice(1);
589
628
  return `M ${head.x} ${head.y} ` + tail.map((p) => `L ${p.x} ${p.y}`).join(" ") + " Z";
590
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
+ }
591
647
  };
592
648
 
593
649
  // src/coordinate.ts
@@ -662,96 +718,45 @@ var Coordinate = class {
662
718
  };
663
719
 
664
720
  // src/geometry.ts
665
- var import_paper = __toESM(require("paper"));
666
- function resolveHolePosition(hole, geometry, canvasSize) {
667
- if (hole.anchor) {
668
- const { x, y, width, height } = geometry;
669
- let bx = x;
670
- let by = y;
671
- const left = x - width / 2;
672
- const right = x + width / 2;
673
- const top = y - height / 2;
674
- const bottom = y + height / 2;
675
- switch (hole.anchor) {
676
- case "top-left":
677
- bx = left;
678
- by = top;
679
- break;
680
- case "top-center":
681
- bx = x;
682
- by = top;
683
- break;
684
- case "top-right":
685
- bx = right;
686
- by = top;
687
- break;
688
- case "center-left":
689
- bx = left;
690
- by = y;
691
- break;
692
- case "center":
693
- bx = x;
694
- by = y;
695
- break;
696
- case "center-right":
697
- bx = right;
698
- by = y;
699
- break;
700
- case "bottom-left":
701
- bx = left;
702
- by = bottom;
703
- break;
704
- case "bottom-center":
705
- bx = x;
706
- by = bottom;
707
- break;
708
- case "bottom-right":
709
- bx = right;
710
- by = bottom;
711
- break;
712
- }
713
- return {
714
- x: bx + (hole.offsetX || 0),
715
- y: by + (hole.offsetY || 0)
716
- };
717
- } else if (hole.x !== void 0 && hole.y !== void 0) {
718
- const { x, width, y, height } = geometry;
719
- return {
720
- x: hole.x * width + (x - width / 2) + (hole.offsetX || 0),
721
- y: hole.y * height + (y - height / 2) + (hole.offsetY || 0)
722
- };
723
- }
724
- 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
+ };
725
730
  }
726
731
  function ensurePaper(width, height) {
727
- if (!import_paper.default.project) {
728
- 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));
729
734
  } else {
730
- 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);
731
736
  }
732
737
  }
733
738
  function createBaseShape(options) {
734
739
  const { shape, width, height, radius, x, y, pathData } = options;
735
- const center = new import_paper.default.Point(x, y);
740
+ const center = new import_paper2.default.Point(x, y);
736
741
  if (shape === "rect") {
737
- return new import_paper.default.Path.Rectangle({
742
+ return new import_paper2.default.Path.Rectangle({
738
743
  point: [x - width / 2, y - height / 2],
739
744
  size: [Math.max(0, width), Math.max(0, height)],
740
745
  radius: Math.max(0, radius)
741
746
  });
742
747
  } else if (shape === "circle") {
743
748
  const r = Math.min(width, height) / 2;
744
- return new import_paper.default.Path.Circle({
749
+ return new import_paper2.default.Path.Circle({
745
750
  center,
746
751
  radius: Math.max(0, r)
747
752
  });
748
753
  } else if (shape === "ellipse") {
749
- return new import_paper.default.Path.Ellipse({
754
+ return new import_paper2.default.Path.Ellipse({
750
755
  center,
751
756
  radius: [Math.max(0, width / 2), Math.max(0, height / 2)]
752
757
  });
753
758
  } else if (shape === "custom" && pathData) {
754
- const path = new import_paper.default.Path();
759
+ const path = new import_paper2.default.Path();
755
760
  path.pathData = pathData;
756
761
  path.position = center;
757
762
  if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
@@ -759,112 +764,131 @@ function createBaseShape(options) {
759
764
  }
760
765
  return path;
761
766
  } else {
762
- return new import_paper.default.Path.Rectangle({
767
+ return new import_paper2.default.Path.Rectangle({
763
768
  point: [x - width / 2, y - height / 2],
764
769
  size: [Math.max(0, width), Math.max(0, height)]
765
770
  });
766
771
  }
767
772
  }
768
- function getDielineShape(options) {
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
+ }
796
+ function getPerimeterShape(options) {
769
797
  let mainShape = createBaseShape(options);
770
- const { holes } = options;
771
- if (holes && holes.length > 0) {
772
- let lugsPath = null;
773
- let cutsPath = null;
774
- holes.forEach((hole) => {
775
- const center = new import_paper.default.Point(hole.x, hole.y);
776
- const lug = hole.shape === "square" ? new import_paper.default.Path.Rectangle({
777
- point: [
778
- center.x - hole.outerRadius,
779
- center.y - hole.outerRadius
780
- ],
781
- size: [hole.outerRadius * 2, hole.outerRadius * 2]
782
- }) : new import_paper.default.Path.Circle({
783
- center,
784
- radius: hole.outerRadius
785
- });
786
- const cut = hole.shape === "square" ? new import_paper.default.Path.Rectangle({
787
- point: [
788
- center.x - hole.innerRadius,
789
- center.y - hole.innerRadius
790
- ],
791
- size: [hole.innerRadius * 2, hole.innerRadius * 2]
792
- }) : new import_paper.default.Path.Circle({
793
- center,
794
- radius: hole.innerRadius
795
- });
796
- if (!lugsPath) {
797
- lugsPath = lug;
798
+ const { features } = options;
799
+ if (features && features.length > 0) {
800
+ const edgeFeatures = features.filter(
801
+ (f) => !f.placement || f.placement === "edge"
802
+ );
803
+ const adds = [];
804
+ const subtracts = [];
805
+ edgeFeatures.forEach((f) => {
806
+ const pos = resolveFeaturePosition(f, options);
807
+ const center = new import_paper2.default.Point(pos.x, pos.y);
808
+ const item = createFeatureItem(f, center);
809
+ if (f.operation === "add") {
810
+ adds.push(item);
798
811
  } else {
812
+ subtracts.push(item);
813
+ }
814
+ });
815
+ if (adds.length > 0) {
816
+ for (const item of adds) {
799
817
  try {
800
- const temp = lugsPath.unite(lug);
801
- lugsPath.remove();
802
- lug.remove();
803
- lugsPath = temp;
818
+ const temp = mainShape.unite(item);
819
+ mainShape.remove();
820
+ item.remove();
821
+ mainShape = temp;
804
822
  } catch (e) {
805
- console.error("Geometry: Failed to unite lug", e);
806
- lug.remove();
823
+ console.error("Geometry: Failed to unite feature", e);
824
+ item.remove();
807
825
  }
808
826
  }
809
- if (!cutsPath) {
810
- cutsPath = cut;
811
- } else {
827
+ }
828
+ if (subtracts.length > 0) {
829
+ for (const item of subtracts) {
812
830
  try {
813
- const temp = cutsPath.unite(cut);
814
- cutsPath.remove();
815
- cut.remove();
816
- cutsPath = temp;
831
+ const temp = mainShape.subtract(item);
832
+ mainShape.remove();
833
+ item.remove();
834
+ mainShape = temp;
817
835
  } catch (e) {
818
- console.error("Geometry: Failed to unite cut", e);
819
- cut.remove();
836
+ console.error("Geometry: Failed to subtract feature", e);
837
+ item.remove();
820
838
  }
821
839
  }
822
- });
823
- if (lugsPath) {
824
- try {
825
- const temp = mainShape.unite(lugsPath);
826
- mainShape.remove();
827
- lugsPath.remove();
828
- mainShape = temp;
829
- } catch (e) {
830
- console.error("Geometry: Failed to unite lugsPath to mainShape", e);
831
- }
832
840
  }
833
- if (cutsPath) {
834
- try {
835
- const temp = mainShape.subtract(cutsPath);
836
- mainShape.remove();
837
- cutsPath.remove();
838
- mainShape = temp;
839
- } catch (e) {
840
- console.error(
841
- "Geometry: Failed to subtract cutsPath from mainShape",
842
- e
843
- );
841
+ }
842
+ return mainShape;
843
+ }
844
+ function applySurfaceFeatures(shape, features, options) {
845
+ const internalFeatures = features.filter((f) => f.placement === "internal");
846
+ if (internalFeatures.length === 0) return shape;
847
+ let result = shape;
848
+ for (const f of internalFeatures) {
849
+ const pos = resolveFeaturePosition(f, options);
850
+ const center = new import_paper2.default.Point(pos.x, pos.y);
851
+ const item = createFeatureItem(f, center);
852
+ try {
853
+ if (f.operation === "add") {
854
+ const temp = result.unite(item);
855
+ result.remove();
856
+ item.remove();
857
+ result = temp;
858
+ } else {
859
+ const temp = result.subtract(item);
860
+ result.remove();
861
+ item.remove();
862
+ result = temp;
844
863
  }
864
+ } catch (e) {
865
+ console.error("Geometry: Failed to apply surface feature", e);
866
+ item.remove();
845
867
  }
846
868
  }
847
- return mainShape;
869
+ return result;
848
870
  }
849
871
  function generateDielinePath(options) {
850
872
  const paperWidth = options.canvasWidth || options.width * 2 || 2e3;
851
873
  const paperHeight = options.canvasHeight || options.height * 2 || 2e3;
852
874
  ensurePaper(paperWidth, paperHeight);
853
- import_paper.default.project.activeLayer.removeChildren();
854
- const mainShape = getDielineShape(options);
855
- const pathData = mainShape.pathData;
856
- mainShape.remove();
875
+ import_paper2.default.project.activeLayer.removeChildren();
876
+ const perimeter = getPerimeterShape(options);
877
+ const finalShape = applySurfaceFeatures(perimeter, options.features, options);
878
+ const pathData = finalShape.pathData;
879
+ finalShape.remove();
857
880
  return pathData;
858
881
  }
859
882
  function generateMaskPath(options) {
860
883
  ensurePaper(options.canvasWidth, options.canvasHeight);
861
- import_paper.default.project.activeLayer.removeChildren();
884
+ import_paper2.default.project.activeLayer.removeChildren();
862
885
  const { canvasWidth, canvasHeight } = options;
863
- const maskRect = new import_paper.default.Path.Rectangle({
886
+ const maskRect = new import_paper2.default.Path.Rectangle({
864
887
  point: [0, 0],
865
888
  size: [canvasWidth, canvasHeight]
866
889
  });
867
- const mainShape = getDielineShape(options);
890
+ const perimeter = getPerimeterShape(options);
891
+ const mainShape = applySurfaceFeatures(perimeter, options.features, options);
868
892
  const finalMask = maskRect.subtract(mainShape);
869
893
  maskRect.remove();
870
894
  mainShape.remove();
@@ -872,43 +896,15 @@ function generateMaskPath(options) {
872
896
  finalMask.remove();
873
897
  return pathData;
874
898
  }
875
- function generateBleedZonePath(options, offset) {
876
- const paperWidth = options.canvasWidth || options.width * 2 || 2e3;
877
- const paperHeight = options.canvasHeight || options.height * 2 || 2e3;
899
+ function generateBleedZonePath(originalOptions, offsetOptions, offset) {
900
+ const paperWidth = originalOptions.canvasWidth || originalOptions.width * 2 || 2e3;
901
+ const paperHeight = originalOptions.canvasHeight || originalOptions.height * 2 || 2e3;
878
902
  ensurePaper(paperWidth, paperHeight);
879
- import_paper.default.project.activeLayer.removeChildren();
880
- const shapeOriginal = getDielineShape(options);
881
- let shapeOffset;
882
- if (options.shape === "custom") {
883
- const stroker = shapeOriginal.clone();
884
- stroker.strokeColor = new import_paper.default.Color("black");
885
- stroker.strokeWidth = Math.abs(offset) * 2;
886
- stroker.strokeJoin = "round";
887
- stroker.strokeCap = "round";
888
- let expanded;
889
- try {
890
- expanded = stroker.expand({ stroke: true, fill: false, insert: false });
891
- } catch (e) {
892
- stroker.remove();
893
- shapeOffset = shapeOriginal.clone();
894
- return shapeOffset.pathData;
895
- }
896
- stroker.remove();
897
- if (offset > 0) {
898
- shapeOffset = shapeOriginal.unite(expanded);
899
- } else {
900
- shapeOffset = shapeOriginal.subtract(expanded);
901
- }
902
- expanded.remove();
903
- } else {
904
- const offsetOptions = {
905
- ...options,
906
- width: Math.max(0, options.width + offset * 2),
907
- height: Math.max(0, options.height + offset * 2),
908
- radius: options.radius === 0 ? 0 : Math.max(0, options.radius + offset)
909
- };
910
- shapeOffset = getDielineShape(offsetOptions);
911
- }
903
+ import_paper2.default.project.activeLayer.removeChildren();
904
+ const pOriginal = getPerimeterShape(originalOptions);
905
+ const shapeOriginal = applySurfaceFeatures(pOriginal, originalOptions.features, originalOptions);
906
+ const pOffset = getPerimeterShape(offsetOptions);
907
+ const shapeOffset = applySurfaceFeatures(pOffset, offsetOptions.features, offsetOptions);
912
908
  let bleedZone;
913
909
  if (offset > 0) {
914
910
  bleedZone = shapeOffset.subtract(shapeOriginal);
@@ -923,16 +919,16 @@ function generateBleedZonePath(options, offset) {
923
919
  }
924
920
  function getNearestPointOnDieline(point, options) {
925
921
  ensurePaper(options.width * 2, options.height * 2);
926
- import_paper.default.project.activeLayer.removeChildren();
922
+ import_paper2.default.project.activeLayer.removeChildren();
927
923
  const shape = createBaseShape(options);
928
- const p = new import_paper.default.Point(point.x, point.y);
924
+ const p = new import_paper2.default.Point(point.x, point.y);
929
925
  const nearest = shape.getNearestPoint(p);
930
926
  const result = { x: nearest.x, y: nearest.y };
931
927
  shape.remove();
932
928
  return result;
933
929
  }
934
930
  function getPathBounds(pathData) {
935
- const path = new import_paper.default.Path();
931
+ const path = new import_paper2.default.Path();
936
932
  path.pathData = pathData;
937
933
  const bounds = path.bounds;
938
934
  path.remove();
@@ -951,20 +947,41 @@ var DielineTool = class {
951
947
  this.metadata = {
952
948
  name: "DielineTool"
953
949
  };
954
- this.unit = "mm";
955
- this.shape = "rect";
956
- this.width = 500;
957
- this.height = 500;
958
- this.radius = 0;
959
- this.offset = 0;
960
- this.style = "solid";
961
- this.insideColor = "rgba(0,0,0,0)";
962
- this.outsideColor = "#ffffff";
963
- this.showBleedLines = true;
964
- this.holes = [];
965
- this.padding = 140;
950
+ this.state = {
951
+ unit: "mm",
952
+ shape: "rect",
953
+ width: 500,
954
+ height: 500,
955
+ radius: 0,
956
+ offset: 0,
957
+ padding: 140,
958
+ mainLine: {
959
+ width: 2.7,
960
+ color: "#FF0000",
961
+ dashLength: 5,
962
+ style: "solid"
963
+ },
964
+ offsetLine: {
965
+ width: 2.7,
966
+ color: "#FF0000",
967
+ dashLength: 5,
968
+ style: "solid"
969
+ },
970
+ insideColor: "rgba(0,0,0,0)",
971
+ outsideColor: "#ffffff",
972
+ showBleedLines: true,
973
+ features: []
974
+ };
966
975
  if (options) {
967
- Object.assign(this, options);
976
+ if (options.mainLine) {
977
+ Object.assign(this.state.mainLine, options.mainLine);
978
+ delete options.mainLine;
979
+ }
980
+ if (options.offsetLine) {
981
+ Object.assign(this.state.offsetLine, options.offsetLine);
982
+ delete options.offsetLine;
983
+ }
984
+ Object.assign(this.state, options);
968
985
  }
969
986
  }
970
987
  activate(context) {
@@ -976,38 +993,93 @@ var DielineTool = class {
976
993
  }
977
994
  const configService = context.services.get("ConfigurationService");
978
995
  if (configService) {
979
- this.unit = configService.get("dieline.unit", this.unit);
980
- this.shape = configService.get("dieline.shape", this.shape);
981
- this.width = configService.get("dieline.width", this.width);
982
- this.height = configService.get("dieline.height", this.height);
983
- this.radius = configService.get("dieline.radius", this.radius);
984
- this.padding = configService.get("dieline.padding", this.padding);
985
- this.offset = configService.get("dieline.offset", this.offset);
986
- this.style = configService.get("dieline.style", this.style);
987
- this.insideColor = configService.get(
988
- "dieline.insideColor",
989
- this.insideColor
990
- );
991
- this.outsideColor = configService.get(
992
- "dieline.outsideColor",
993
- this.outsideColor
994
- );
995
- this.showBleedLines = configService.get(
996
- "dieline.showBleedLines",
997
- this.showBleedLines
998
- );
999
- this.holes = configService.get("dieline.holes", this.holes);
1000
- this.pathData = configService.get("dieline.pathData", this.pathData);
996
+ const s = this.state;
997
+ s.unit = configService.get("dieline.unit", s.unit);
998
+ s.shape = configService.get("dieline.shape", s.shape);
999
+ s.width = configService.get("dieline.width", s.width);
1000
+ s.height = configService.get("dieline.height", s.height);
1001
+ s.radius = configService.get("dieline.radius", s.radius);
1002
+ s.padding = configService.get("dieline.padding", s.padding);
1003
+ s.offset = configService.get("dieline.offset", s.offset);
1004
+ s.mainLine.width = configService.get("dieline.strokeWidth", s.mainLine.width);
1005
+ s.mainLine.color = configService.get("dieline.strokeColor", s.mainLine.color);
1006
+ s.mainLine.dashLength = configService.get("dieline.dashLength", s.mainLine.dashLength);
1007
+ s.mainLine.style = configService.get("dieline.style", s.mainLine.style);
1008
+ s.offsetLine.width = configService.get("dieline.offsetStrokeWidth", s.offsetLine.width);
1009
+ s.offsetLine.color = configService.get("dieline.offsetStrokeColor", s.offsetLine.color);
1010
+ s.offsetLine.dashLength = configService.get("dieline.offsetDashLength", s.offsetLine.dashLength);
1011
+ s.offsetLine.style = configService.get("dieline.offsetStyle", s.offsetLine.style);
1012
+ s.insideColor = configService.get("dieline.insideColor", s.insideColor);
1013
+ s.outsideColor = configService.get("dieline.outsideColor", s.outsideColor);
1014
+ s.showBleedLines = configService.get("dieline.showBleedLines", s.showBleedLines);
1015
+ s.features = configService.get("dieline.features", s.features);
1016
+ s.pathData = configService.get("dieline.pathData", s.pathData);
1001
1017
  configService.onAnyChange((e) => {
1002
1018
  if (e.key.startsWith("dieline.")) {
1003
- const prop = e.key.split(".")[1];
1004
- console.log(
1005
- `[DielineTool] Config change detected: ${e.key} -> ${e.value}`
1006
- );
1007
- if (prop && prop in this) {
1008
- this[prop] = e.value;
1009
- this.updateDieline();
1019
+ console.log(`[DielineTool] Config change detected: ${e.key} -> ${e.value}`);
1020
+ switch (e.key) {
1021
+ case "dieline.unit":
1022
+ s.unit = e.value;
1023
+ break;
1024
+ case "dieline.shape":
1025
+ s.shape = e.value;
1026
+ break;
1027
+ case "dieline.width":
1028
+ s.width = e.value;
1029
+ break;
1030
+ case "dieline.height":
1031
+ s.height = e.value;
1032
+ break;
1033
+ case "dieline.radius":
1034
+ s.radius = e.value;
1035
+ break;
1036
+ case "dieline.padding":
1037
+ s.padding = e.value;
1038
+ break;
1039
+ case "dieline.offset":
1040
+ s.offset = e.value;
1041
+ break;
1042
+ case "dieline.strokeWidth":
1043
+ s.mainLine.width = e.value;
1044
+ break;
1045
+ case "dieline.strokeColor":
1046
+ s.mainLine.color = e.value;
1047
+ break;
1048
+ case "dieline.dashLength":
1049
+ s.mainLine.dashLength = e.value;
1050
+ break;
1051
+ case "dieline.style":
1052
+ s.mainLine.style = e.value;
1053
+ break;
1054
+ case "dieline.offsetStrokeWidth":
1055
+ s.offsetLine.width = e.value;
1056
+ break;
1057
+ case "dieline.offsetStrokeColor":
1058
+ s.offsetLine.color = e.value;
1059
+ break;
1060
+ case "dieline.offsetDashLength":
1061
+ s.offsetLine.dashLength = e.value;
1062
+ break;
1063
+ case "dieline.offsetStyle":
1064
+ s.offsetLine.style = e.value;
1065
+ break;
1066
+ case "dieline.insideColor":
1067
+ s.insideColor = e.value;
1068
+ break;
1069
+ case "dieline.outsideColor":
1070
+ s.outsideColor = e.value;
1071
+ break;
1072
+ case "dieline.showBleedLines":
1073
+ s.showBleedLines = e.value;
1074
+ break;
1075
+ case "dieline.features":
1076
+ s.features = e.value;
1077
+ break;
1078
+ case "dieline.pathData":
1079
+ s.pathData = e.value;
1080
+ break;
1010
1081
  }
1082
+ this.updateDieline();
1011
1083
  }
1012
1084
  });
1013
1085
  }
@@ -1020,6 +1092,7 @@ var DielineTool = class {
1020
1092
  this.context = void 0;
1021
1093
  }
1022
1094
  contribute() {
1095
+ const s = this.state;
1023
1096
  return {
1024
1097
  [import_core2.ContributionPointIds.CONFIGURATIONS]: [
1025
1098
  {
@@ -1027,14 +1100,14 @@ var DielineTool = class {
1027
1100
  type: "select",
1028
1101
  label: "Unit",
1029
1102
  options: ["px", "mm", "cm", "in"],
1030
- default: this.unit
1103
+ default: s.unit
1031
1104
  },
1032
1105
  {
1033
1106
  id: "dieline.shape",
1034
1107
  type: "select",
1035
1108
  label: "Shape",
1036
1109
  options: ["rect", "circle", "ellipse", "custom"],
1037
- default: this.shape
1110
+ default: s.shape
1038
1111
  },
1039
1112
  {
1040
1113
  id: "dieline.width",
@@ -1042,7 +1115,7 @@ var DielineTool = class {
1042
1115
  label: "Width",
1043
1116
  min: 10,
1044
1117
  max: 2e3,
1045
- default: this.width
1118
+ default: s.width
1046
1119
  },
1047
1120
  {
1048
1121
  id: "dieline.height",
@@ -1050,7 +1123,7 @@ var DielineTool = class {
1050
1123
  label: "Height",
1051
1124
  min: 10,
1052
1125
  max: 2e3,
1053
- default: this.height
1126
+ default: s.height
1054
1127
  },
1055
1128
  {
1056
1129
  id: "dieline.radius",
@@ -1058,20 +1131,14 @@ var DielineTool = class {
1058
1131
  label: "Corner Radius",
1059
1132
  min: 0,
1060
1133
  max: 500,
1061
- default: this.radius
1062
- },
1063
- {
1064
- id: "dieline.position",
1065
- type: "json",
1066
- label: "Position (Normalized)",
1067
- default: this.radius
1134
+ default: s.radius
1068
1135
  },
1069
1136
  {
1070
1137
  id: "dieline.padding",
1071
1138
  type: "select",
1072
1139
  label: "View Padding",
1073
1140
  options: [0, 10, 20, 40, 60, 100, "2%", "5%", "10%", "15%", "20%"],
1074
- default: this.padding
1141
+ default: s.padding
1075
1142
  },
1076
1143
  {
1077
1144
  id: "dieline.offset",
@@ -1079,38 +1146,91 @@ var DielineTool = class {
1079
1146
  label: "Bleed Offset",
1080
1147
  min: -100,
1081
1148
  max: 100,
1082
- default: this.offset
1149
+ default: s.offset
1083
1150
  },
1084
1151
  {
1085
1152
  id: "dieline.showBleedLines",
1086
1153
  type: "boolean",
1087
1154
  label: "Show Bleed Lines",
1088
- default: this.showBleedLines
1155
+ default: s.showBleedLines
1156
+ },
1157
+ {
1158
+ id: "dieline.strokeWidth",
1159
+ type: "number",
1160
+ label: "Line Width",
1161
+ min: 0.1,
1162
+ max: 10,
1163
+ step: 0.1,
1164
+ default: s.mainLine.width
1165
+ },
1166
+ {
1167
+ id: "dieline.strokeColor",
1168
+ type: "color",
1169
+ label: "Line Color",
1170
+ default: s.mainLine.color
1171
+ },
1172
+ {
1173
+ id: "dieline.dashLength",
1174
+ type: "number",
1175
+ label: "Dash Length",
1176
+ min: 1,
1177
+ max: 50,
1178
+ default: s.mainLine.dashLength
1089
1179
  },
1090
1180
  {
1091
1181
  id: "dieline.style",
1092
1182
  type: "select",
1093
1183
  label: "Line Style",
1094
- options: ["solid", "dashed"],
1095
- default: this.style
1184
+ options: ["solid", "dashed", "hidden"],
1185
+ default: s.mainLine.style
1186
+ },
1187
+ {
1188
+ id: "dieline.offsetStrokeWidth",
1189
+ type: "number",
1190
+ label: "Offset Line Width",
1191
+ min: 0.1,
1192
+ max: 10,
1193
+ step: 0.1,
1194
+ default: s.offsetLine.width
1195
+ },
1196
+ {
1197
+ id: "dieline.offsetStrokeColor",
1198
+ type: "color",
1199
+ label: "Offset Line Color",
1200
+ default: s.offsetLine.color
1201
+ },
1202
+ {
1203
+ id: "dieline.offsetDashLength",
1204
+ type: "number",
1205
+ label: "Offset Dash Length",
1206
+ min: 1,
1207
+ max: 50,
1208
+ default: s.offsetLine.dashLength
1209
+ },
1210
+ {
1211
+ id: "dieline.offsetStyle",
1212
+ type: "select",
1213
+ label: "Offset Line Style",
1214
+ options: ["solid", "dashed", "hidden"],
1215
+ default: s.offsetLine.style
1096
1216
  },
1097
1217
  {
1098
1218
  id: "dieline.insideColor",
1099
1219
  type: "color",
1100
1220
  label: "Inside Color",
1101
- default: this.insideColor
1221
+ default: s.insideColor
1102
1222
  },
1103
1223
  {
1104
1224
  id: "dieline.outsideColor",
1105
1225
  type: "color",
1106
1226
  label: "Outside Color",
1107
- default: this.outsideColor
1227
+ default: s.outsideColor
1108
1228
  },
1109
1229
  {
1110
- id: "dieline.holes",
1230
+ id: "dieline.features",
1111
1231
  type: "json",
1112
- label: "Holes",
1113
- default: this.holes
1232
+ label: "Edge Features",
1233
+ default: s.features
1114
1234
  }
1115
1235
  ],
1116
1236
  [import_core2.ContributionPointIds.COMMANDS]: [
@@ -1132,24 +1252,18 @@ var DielineTool = class {
1132
1252
  command: "detectEdge",
1133
1253
  title: "Detect Edge from Image",
1134
1254
  handler: async (imageUrl, options) => {
1135
- var _a;
1136
1255
  try {
1137
1256
  const pathData = await ImageTracer.trace(imageUrl, options);
1138
1257
  const bounds = getPathBounds(pathData);
1139
- const currentMax = Math.max(this.width, this.height);
1258
+ const currentMax = Math.max(s.width, s.height);
1140
1259
  const scale = currentMax / Math.max(bounds.width, bounds.height);
1141
1260
  const newWidth = bounds.width * scale;
1142
1261
  const newHeight = bounds.height * scale;
1143
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1144
- "ConfigurationService"
1145
- );
1146
- if (configService) {
1147
- configService.update("dieline.width", newWidth);
1148
- configService.update("dieline.height", newHeight);
1149
- configService.update("dieline.shape", "custom");
1150
- configService.update("dieline.pathData", pathData);
1151
- }
1152
- return pathData;
1262
+ return {
1263
+ pathData,
1264
+ width: newWidth,
1265
+ height: newHeight
1266
+ };
1153
1267
  } catch (e) {
1154
1268
  console.error("Edge detection failed", e);
1155
1269
  throw e;
@@ -1208,15 +1322,15 @@ var DielineTool = class {
1208
1322
  return new import_fabric2.Pattern({ source: canvas, repetition: "repeat" });
1209
1323
  }
1210
1324
  resolvePadding(containerWidth, containerHeight) {
1211
- if (typeof this.padding === "number") {
1212
- return this.padding;
1325
+ if (typeof this.state.padding === "number") {
1326
+ return this.state.padding;
1213
1327
  }
1214
- if (typeof this.padding === "string") {
1215
- if (this.padding.endsWith("%")) {
1216
- const percent = parseFloat(this.padding) / 100;
1328
+ if (typeof this.state.padding === "string") {
1329
+ if (this.state.padding.endsWith("%")) {
1330
+ const percent = parseFloat(this.state.padding) / 100;
1217
1331
  return Math.min(containerWidth, containerHeight) * percent;
1218
1332
  }
1219
- return parseFloat(this.padding) || 0;
1333
+ return parseFloat(this.state.padding) || 0;
1220
1334
  }
1221
1335
  return 0;
1222
1336
  }
@@ -1229,14 +1343,14 @@ var DielineTool = class {
1229
1343
  shape,
1230
1344
  radius,
1231
1345
  offset,
1232
- style,
1346
+ mainLine,
1347
+ offsetLine,
1233
1348
  insideColor,
1234
1349
  outsideColor,
1235
- position,
1236
1350
  showBleedLines,
1237
- holes
1238
- } = this;
1239
- let { width, height } = this;
1351
+ features
1352
+ } = this.state;
1353
+ let { width, height } = this.state;
1240
1354
  const canvasW = this.canvasService.canvas.width || 800;
1241
1355
  const canvasH = this.canvasService.canvas.height || 600;
1242
1356
  const paddingPx = this.resolvePadding(canvasW, canvasH);
@@ -1253,37 +1367,18 @@ var DielineTool = class {
1253
1367
  const visualRadius = radius * scale;
1254
1368
  const visualOffset = offset * scale;
1255
1369
  layer.remove(...layer.getObjects());
1256
- const geometryForHoles = {
1257
- x: cx,
1258
- y: cy,
1259
- width: visualWidth,
1260
- height: visualHeight
1261
- // Pass scale/unit context if needed by resolveHolePosition (though currently unused there)
1262
- };
1263
- const absoluteHoles = (holes || []).map((h) => {
1264
- const unitScale = Coordinate.convertUnit(1, "mm", unit);
1265
- const offsetScale = unitScale * scale;
1266
- const hWithPixelOffsets = {
1267
- ...h,
1268
- offsetX: (h.offsetX || 0) * offsetScale,
1269
- offsetY: (h.offsetY || 0) * offsetScale
1270
- };
1271
- const pos = resolveHolePosition(hWithPixelOffsets, geometryForHoles, {
1272
- width: canvasW,
1273
- height: canvasH
1274
- });
1370
+ const absoluteFeatures = (features || []).map((f) => {
1371
+ const featureScale = scale;
1275
1372
  return {
1276
- ...h,
1277
- x: pos.x,
1278
- y: pos.y,
1279
- // Scale hole radii: mm -> current unit -> pixels
1280
- innerRadius: h.innerRadius * offsetScale,
1281
- outerRadius: h.outerRadius * offsetScale,
1282
- // Store scaled offsets in the result for consistency, though pos is already resolved
1283
- offsetX: hWithPixelOffsets.offsetX,
1284
- offsetY: hWithPixelOffsets.offsetY
1373
+ ...f,
1374
+ x: f.x,
1375
+ y: f.y,
1376
+ width: (f.width || 0) * featureScale,
1377
+ height: (f.height || 0) * featureScale,
1378
+ radius: (f.radius || 0) * featureScale
1285
1379
  };
1286
1380
  });
1381
+ const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
1287
1382
  const cutW = Math.max(0, visualWidth + visualOffset * 2);
1288
1383
  const cutH = Math.max(0, visualHeight + visualOffset * 2);
1289
1384
  const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
@@ -1296,8 +1391,8 @@ var DielineTool = class {
1296
1391
  radius: cutR,
1297
1392
  x: cx,
1298
1393
  y: cy,
1299
- holes: absoluteHoles,
1300
- pathData: this.pathData
1394
+ features: cutFeatures,
1395
+ pathData: this.state.pathData
1301
1396
  });
1302
1397
  const mask = new import_fabric2.Path(maskPathData, {
1303
1398
  fill: outsideColor,
@@ -1318,8 +1413,9 @@ var DielineTool = class {
1318
1413
  radius: cutR,
1319
1414
  x: cx,
1320
1415
  y: cy,
1321
- holes: absoluteHoles,
1322
- pathData: this.pathData,
1416
+ features: cutFeatures,
1417
+ // Use same features as mask for consistency
1418
+ pathData: this.state.pathData,
1323
1419
  canvasWidth: canvasW,
1324
1420
  canvasHeight: canvasH
1325
1421
  });
@@ -1343,15 +1439,27 @@ var DielineTool = class {
1343
1439
  radius: visualRadius,
1344
1440
  x: cx,
1345
1441
  y: cy,
1346
- holes: absoluteHoles,
1347
- pathData: this.pathData,
1442
+ features: cutFeatures,
1443
+ pathData: this.state.pathData,
1444
+ canvasWidth: canvasW,
1445
+ canvasHeight: canvasH
1446
+ },
1447
+ {
1448
+ shape,
1449
+ width: cutW,
1450
+ height: cutH,
1451
+ radius: cutR,
1452
+ x: cx,
1453
+ y: cy,
1454
+ features: cutFeatures,
1455
+ pathData: this.state.pathData,
1348
1456
  canvasWidth: canvasW,
1349
1457
  canvasHeight: canvasH
1350
1458
  },
1351
1459
  visualOffset
1352
1460
  );
1353
1461
  if (showBleedLines !== false) {
1354
- const pattern = this.createHatchPattern("red");
1462
+ const pattern = this.createHatchPattern(mainLine.color);
1355
1463
  if (pattern) {
1356
1464
  const bleedObj = new import_fabric2.Path(bleedPathData, {
1357
1465
  fill: pattern,
@@ -1372,18 +1480,16 @@ var DielineTool = class {
1372
1480
  radius: cutR,
1373
1481
  x: cx,
1374
1482
  y: cy,
1375
- holes: absoluteHoles,
1376
- pathData: this.pathData,
1483
+ features: cutFeatures,
1484
+ pathData: this.state.pathData,
1377
1485
  canvasWidth: canvasW,
1378
1486
  canvasHeight: canvasH
1379
1487
  });
1380
1488
  const offsetBorderObj = new import_fabric2.Path(offsetPathData, {
1381
1489
  fill: null,
1382
- stroke: "#666",
1383
- // Grey
1384
- strokeWidth: 1,
1385
- strokeDashArray: [4, 4],
1386
- // Dashed
1490
+ stroke: offsetLine.style === "hidden" ? null : offsetLine.color,
1491
+ strokeWidth: offsetLine.width,
1492
+ strokeDashArray: offsetLine.style === "dashed" ? [offsetLine.dashLength, offsetLine.dashLength] : void 0,
1387
1493
  selectable: false,
1388
1494
  evented: false,
1389
1495
  originX: "left",
@@ -1398,16 +1504,16 @@ var DielineTool = class {
1398
1504
  radius: visualRadius,
1399
1505
  x: cx,
1400
1506
  y: cy,
1401
- holes: absoluteHoles,
1402
- pathData: this.pathData,
1507
+ features: absoluteFeatures,
1508
+ pathData: this.state.pathData,
1403
1509
  canvasWidth: canvasW,
1404
1510
  canvasHeight: canvasH
1405
1511
  });
1406
1512
  const borderObj = new import_fabric2.Path(borderPathData, {
1407
1513
  fill: "transparent",
1408
- stroke: "red",
1409
- strokeWidth: 1,
1410
- strokeDashArray: style === "dashed" ? [5, 5] : void 0,
1514
+ stroke: mainLine.style === "hidden" ? null : mainLine.color,
1515
+ strokeWidth: mainLine.width,
1516
+ strokeDashArray: mainLine.style === "dashed" ? [mainLine.dashLength, mainLine.dashLength] : void 0,
1411
1517
  selectable: false,
1412
1518
  evented: false,
1413
1519
  originX: "left",
@@ -1439,7 +1545,7 @@ var DielineTool = class {
1439
1545
  }
1440
1546
  getGeometry() {
1441
1547
  if (!this.canvasService) return null;
1442
- const { unit, shape, width, height, radius, position, offset } = this;
1548
+ const { unit, shape, width, height, radius, offset, mainLine, pathData } = this.state;
1443
1549
  const canvasW = this.canvasService.canvas.width || 800;
1444
1550
  const canvasH = this.canvasService.canvas.height || 600;
1445
1551
  const paddingPx = this.resolvePadding(canvasW, canvasH);
@@ -1462,16 +1568,17 @@ var DielineTool = class {
1462
1568
  height: visualHeight,
1463
1569
  radius: radius * scale,
1464
1570
  offset: offset * scale,
1465
- // Pass scale to help other tools (like HoleTool) convert units
1571
+ // Pass scale to help other tools (like FeatureTool) convert units
1466
1572
  scale,
1467
- pathData: this.pathData
1573
+ strokeWidth: mainLine.width,
1574
+ pathData
1468
1575
  };
1469
1576
  }
1470
1577
  async exportCutImage() {
1471
1578
  if (!this.canvasService) return null;
1472
1579
  const userLayer = this.canvasService.getLayer("user");
1473
1580
  if (!userLayer) return null;
1474
- const { shape, width, height, radius, position, holes } = this;
1581
+ const { shape, width, height, radius, features, unit, pathData } = this.state;
1475
1582
  const canvasW = this.canvasService.canvas.width || 800;
1476
1583
  const canvasH = this.canvasService.canvas.height || 600;
1477
1584
  const paddingPx = this.resolvePadding(canvasW, canvasH);
@@ -1486,55 +1593,43 @@ var DielineTool = class {
1486
1593
  const visualWidth = layout.width;
1487
1594
  const visualHeight = layout.height;
1488
1595
  const visualRadius = radius * scale;
1489
- const absoluteHoles = (holes || []).map((h) => {
1490
- const unit = this.unit || "mm";
1491
- const unitScale = Coordinate.convertUnit(1, "mm", unit);
1492
- const pos = resolveHolePosition(
1493
- {
1494
- ...h,
1495
- offsetX: (h.offsetX || 0) * unitScale * scale,
1496
- offsetY: (h.offsetY || 0) * unitScale * scale
1497
- },
1498
- { x: cx, y: cy, width: visualWidth, height: visualHeight },
1499
- { width: canvasW, height: canvasH }
1500
- );
1596
+ const absoluteFeatures = (features || []).map((f) => {
1597
+ const featureScale = scale;
1501
1598
  return {
1502
- ...h,
1503
- x: pos.x,
1504
- y: pos.y,
1505
- innerRadius: h.innerRadius * unitScale * scale,
1506
- outerRadius: h.outerRadius * unitScale * scale,
1507
- offsetX: (h.offsetX || 0) * unitScale * scale,
1508
- offsetY: (h.offsetY || 0) * unitScale * scale
1599
+ ...f,
1600
+ x: f.x,
1601
+ y: f.y,
1602
+ width: (f.width || 0) * featureScale,
1603
+ height: (f.height || 0) * featureScale,
1604
+ radius: (f.radius || 0) * featureScale
1509
1605
  };
1510
1606
  });
1511
- const pathData = generateDielinePath({
1607
+ const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
1608
+ const generatedPathData = generateDielinePath({
1512
1609
  shape,
1513
1610
  width: visualWidth,
1514
1611
  height: visualHeight,
1515
1612
  radius: visualRadius,
1516
1613
  x: cx,
1517
1614
  y: cy,
1518
- holes: absoluteHoles,
1519
- pathData: this.pathData,
1615
+ features: cutFeatures,
1616
+ pathData,
1520
1617
  canvasWidth: canvasW,
1521
1618
  canvasHeight: canvasH
1522
1619
  });
1523
1620
  const clonedLayer = await userLayer.clone();
1524
- const clipPath = new import_fabric2.Path(pathData, {
1621
+ const clipPath = new import_fabric2.Path(generatedPathData, {
1525
1622
  originX: "left",
1526
1623
  originY: "top",
1527
1624
  left: 0,
1528
1625
  top: 0,
1529
1626
  absolutePositioned: true
1530
- // Important for groups
1531
1627
  });
1532
1628
  clonedLayer.clipPath = clipPath;
1533
1629
  const bounds = clipPath.getBoundingRect();
1534
1630
  const dataUrl = clonedLayer.toDataURL({
1535
1631
  format: "png",
1536
1632
  multiplier: 2,
1537
- // Better quality
1538
1633
  left: bounds.left,
1539
1634
  top: bounds.top,
1540
1635
  width: bounds.width,
@@ -1703,23 +1798,26 @@ var FilmTool = class {
1703
1798
  }
1704
1799
  };
1705
1800
 
1706
- // src/hole.ts
1801
+ // src/feature.ts
1707
1802
  var import_core4 = require("@pooder/core");
1708
1803
  var import_fabric4 = require("fabric");
1709
- var HoleTool = class {
1804
+ var FeatureTool = class {
1710
1805
  constructor(options) {
1711
- this.id = "pooder.kit.hole";
1806
+ this.id = "pooder.kit.feature";
1712
1807
  this.metadata = {
1713
- name: "HoleTool"
1808
+ name: "FeatureTool"
1714
1809
  };
1715
- this.holes = [];
1716
- this.constraintTarget = "bleed";
1810
+ this.features = [];
1717
1811
  this.isUpdatingConfig = false;
1812
+ this.isToolActive = false;
1718
1813
  this.handleMoving = null;
1719
1814
  this.handleModified = null;
1720
1815
  this.handleDielineChange = null;
1721
- // Cache geometry to enforce constraints during drag
1722
1816
  this.currentGeometry = null;
1817
+ this.onToolActivated = (event) => {
1818
+ this.isToolActive = event.id === this.id;
1819
+ this.updateVisibility();
1820
+ };
1723
1821
  if (options) {
1724
1822
  Object.assign(this, options);
1725
1823
  }
@@ -1728,135 +1826,82 @@ var HoleTool = class {
1728
1826
  this.context = context;
1729
1827
  this.canvasService = context.services.get("CanvasService");
1730
1828
  if (!this.canvasService) {
1731
- console.warn("CanvasService not found for HoleTool");
1829
+ console.warn("CanvasService not found for FeatureTool");
1732
1830
  return;
1733
1831
  }
1734
1832
  const configService = context.services.get(
1735
1833
  "ConfigurationService"
1736
1834
  );
1737
1835
  if (configService) {
1738
- this.constraintTarget = configService.get(
1739
- "hole.constraintTarget",
1740
- this.constraintTarget
1741
- );
1742
- this.holes = configService.get("dieline.holes", []);
1836
+ this.features = configService.get("dieline.features", []);
1743
1837
  configService.onAnyChange((e) => {
1744
1838
  if (this.isUpdatingConfig) return;
1745
- if (e.key === "hole.constraintTarget") {
1746
- this.constraintTarget = e.value;
1747
- this.enforceConstraints();
1748
- }
1749
- if (e.key === "dieline.holes") {
1750
- this.holes = e.value || [];
1839
+ if (e.key === "dieline.features") {
1840
+ this.features = e.value || [];
1751
1841
  this.redraw();
1752
1842
  }
1753
1843
  });
1754
1844
  }
1845
+ context.eventBus.on("tool:activated", this.onToolActivated);
1755
1846
  this.setup();
1756
1847
  }
1757
1848
  deactivate(context) {
1849
+ context.eventBus.off("tool:activated", this.onToolActivated);
1758
1850
  this.teardown();
1759
1851
  this.canvasService = void 0;
1760
1852
  this.context = void 0;
1761
1853
  }
1854
+ updateVisibility() {
1855
+ if (!this.canvasService) return;
1856
+ const canvas = this.canvasService.canvas;
1857
+ const markers = canvas.getObjects().filter((obj) => {
1858
+ var _a;
1859
+ return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
1860
+ });
1861
+ markers.forEach((marker) => {
1862
+ marker.set({
1863
+ visible: this.isToolActive,
1864
+ // Or just selectable: false if we want them visible but locked
1865
+ selectable: this.isToolActive,
1866
+ evented: this.isToolActive
1867
+ });
1868
+ });
1869
+ canvas.requestRenderAll();
1870
+ }
1762
1871
  contribute() {
1763
1872
  return {
1764
- [import_core4.ContributionPointIds.CONFIGURATIONS]: [
1765
- {
1766
- id: "hole.constraintTarget",
1767
- type: "select",
1768
- label: "Constraint Target",
1769
- options: ["original", "bleed"],
1770
- default: "bleed"
1771
- }
1772
- ],
1773
1873
  [import_core4.ContributionPointIds.COMMANDS]: [
1774
1874
  {
1775
- command: "resetHoles",
1776
- title: "Reset Holes",
1777
- handler: () => {
1778
- var _a;
1779
- if (!this.canvasService) return false;
1780
- let defaultPos = { x: this.canvasService.canvas.width / 2, y: 50 };
1781
- if (this.currentGeometry) {
1782
- const g = this.currentGeometry;
1783
- const topCenter = { x: g.x, y: g.y - g.height / 2 };
1784
- defaultPos = getNearestPointOnDieline(topCenter, {
1785
- ...g,
1786
- holes: []
1787
- });
1788
- }
1789
- const { width, height } = this.canvasService.canvas;
1790
- const normalizedHole = Coordinate.normalizePoint(defaultPos, {
1791
- width: width || 800,
1792
- height: height || 600
1793
- });
1794
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1795
- "ConfigurationService"
1796
- );
1797
- if (configService) {
1798
- configService.update("dieline.holes", [
1799
- {
1800
- x: normalizedHole.x,
1801
- y: normalizedHole.y,
1802
- innerRadius: 15,
1803
- outerRadius: 25
1804
- }
1805
- ]);
1806
- }
1807
- return true;
1875
+ command: "addFeature",
1876
+ title: "Add Edge Feature",
1877
+ handler: (type = "subtract") => {
1878
+ return this.addFeature(type);
1808
1879
  }
1809
1880
  },
1810
1881
  {
1811
1882
  command: "addHole",
1812
1883
  title: "Add Hole",
1813
- handler: (x, y) => {
1814
- var _a, _b, _c, _d;
1815
- if (!this.canvasService) return false;
1816
- let normalizedX = 0.5;
1817
- let normalizedY = 0.5;
1818
- if (this.currentGeometry) {
1819
- const { x: gx, y: gy, width: gw, height: gh } = this.currentGeometry;
1820
- const left = gx - gw / 2;
1821
- const top = gy - gh / 2;
1822
- normalizedX = gw > 0 ? (x - left) / gw : 0.5;
1823
- normalizedY = gh > 0 ? (y - top) / gh : 0.5;
1824
- } else {
1825
- const { width, height } = this.canvasService.canvas;
1826
- normalizedX = Coordinate.toNormalized(x, width || 800);
1827
- normalizedY = Coordinate.toNormalized(y, height || 600);
1828
- }
1829
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1830
- "ConfigurationService"
1831
- );
1832
- if (configService) {
1833
- const currentHoles = configService.get("dieline.holes", []);
1834
- const lastHole = currentHoles[currentHoles.length - 1];
1835
- const innerRadius = (_b = lastHole == null ? void 0 : lastHole.innerRadius) != null ? _b : 15;
1836
- const outerRadius = (_c = lastHole == null ? void 0 : lastHole.outerRadius) != null ? _c : 25;
1837
- const shape = (_d = lastHole == null ? void 0 : lastHole.shape) != null ? _d : "circle";
1838
- const newHole = {
1839
- x: normalizedX,
1840
- y: normalizedY,
1841
- shape,
1842
- innerRadius,
1843
- outerRadius
1844
- };
1845
- configService.update("dieline.holes", [...currentHoles, newHole]);
1846
- }
1847
- return true;
1884
+ handler: () => {
1885
+ return this.addFeature("subtract");
1886
+ }
1887
+ },
1888
+ {
1889
+ command: "addDoubleLayerHole",
1890
+ title: "Add Double Layer Hole",
1891
+ handler: () => {
1892
+ return this.addDoubleLayerHole();
1848
1893
  }
1849
1894
  },
1850
1895
  {
1851
- command: "clearHoles",
1852
- title: "Clear Holes",
1896
+ command: "clearFeatures",
1897
+ title: "Clear Features",
1853
1898
  handler: () => {
1854
1899
  var _a;
1855
1900
  const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1856
1901
  "ConfigurationService"
1857
1902
  );
1858
1903
  if (configService) {
1859
- configService.update("dieline.holes", []);
1904
+ configService.update("dieline.features", []);
1860
1905
  }
1861
1906
  return true;
1862
1907
  }
@@ -1864,6 +1909,82 @@ var HoleTool = class {
1864
1909
  ]
1865
1910
  };
1866
1911
  }
1912
+ addFeature(type) {
1913
+ var _a;
1914
+ if (!this.canvasService) return false;
1915
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1916
+ "ConfigurationService"
1917
+ );
1918
+ const unit = (configService == null ? void 0 : configService.get("dieline.unit", "mm")) || "mm";
1919
+ const defaultSize = Coordinate.convertUnit(10, "mm", unit);
1920
+ const newFeature = {
1921
+ id: Date.now().toString(),
1922
+ operation: type,
1923
+ placement: "edge",
1924
+ shape: "rect",
1925
+ x: 0.5,
1926
+ y: 0,
1927
+ // Top edge
1928
+ width: defaultSize,
1929
+ height: defaultSize,
1930
+ rotation: 0
1931
+ };
1932
+ if (configService) {
1933
+ const current = configService.get(
1934
+ "dieline.features",
1935
+ []
1936
+ );
1937
+ configService.update("dieline.features", [...current, newFeature]);
1938
+ }
1939
+ return true;
1940
+ }
1941
+ addDoubleLayerHole() {
1942
+ var _a;
1943
+ if (!this.canvasService) return false;
1944
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1945
+ "ConfigurationService"
1946
+ );
1947
+ const unit = (configService == null ? void 0 : configService.get("dieline.unit", "mm")) || "mm";
1948
+ const lugRadius = Coordinate.convertUnit(20, "mm", unit);
1949
+ const holeRadius = Coordinate.convertUnit(15, "mm", unit);
1950
+ const groupId = Date.now().toString();
1951
+ const timestamp = Date.now();
1952
+ const lug = {
1953
+ id: `${timestamp}-lug`,
1954
+ groupId,
1955
+ operation: "add",
1956
+ shape: "circle",
1957
+ placement: "edge",
1958
+ x: 0.5,
1959
+ y: 0,
1960
+ radius: lugRadius,
1961
+ // 20mm
1962
+ rotation: 0
1963
+ };
1964
+ const hole = {
1965
+ id: `${timestamp}-hole`,
1966
+ groupId,
1967
+ operation: "subtract",
1968
+ shape: "circle",
1969
+ placement: "edge",
1970
+ x: 0.5,
1971
+ y: 0,
1972
+ radius: holeRadius,
1973
+ // 15mm
1974
+ rotation: 0
1975
+ };
1976
+ if (configService) {
1977
+ const current = configService.get(
1978
+ "dieline.features",
1979
+ []
1980
+ );
1981
+ configService.update("dieline.features", [...current, lug, hole]);
1982
+ }
1983
+ return true;
1984
+ }
1985
+ getGeometryForFeature(geometry, feature) {
1986
+ return geometry;
1987
+ }
1867
1988
  setup() {
1868
1989
  if (!this.canvasService || !this.context) return;
1869
1990
  const canvas = this.canvasService.canvas;
@@ -1871,10 +1992,7 @@ var HoleTool = class {
1871
1992
  this.handleDielineChange = (geometry) => {
1872
1993
  this.currentGeometry = geometry;
1873
1994
  this.redraw();
1874
- const changed = this.enforceConstraints();
1875
- if (changed) {
1876
- this.syncHolesToDieline();
1877
- }
1995
+ this.enforceConstraints();
1878
1996
  };
1879
1997
  this.context.eventBus.on(
1880
1998
  "dieline:geometry:change",
@@ -1884,69 +2002,101 @@ var HoleTool = class {
1884
2002
  const commandService = this.context.services.get("CommandService");
1885
2003
  if (commandService) {
1886
2004
  try {
1887
- const geometry = commandService.executeCommand("getGeometry");
1888
- if (geometry) {
1889
- Promise.resolve(geometry).then((g) => {
2005
+ Promise.resolve(commandService.executeCommand("getGeometry")).then(
2006
+ (g) => {
1890
2007
  if (g) {
1891
2008
  this.currentGeometry = g;
1892
- this.enforceConstraints();
1893
- this.initializeHoles();
2009
+ this.redraw();
1894
2010
  }
1895
- });
1896
- }
2011
+ }
2012
+ );
1897
2013
  } catch (e) {
1898
2014
  }
1899
2015
  }
1900
2016
  if (!this.handleMoving) {
1901
2017
  this.handleMoving = (e) => {
1902
- var _a, _b, _c, _d, _e;
2018
+ var _a, _b, _c, _d;
1903
2019
  const target = e.target;
1904
- if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "hole-marker") return;
2020
+ if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return;
1905
2021
  if (!this.currentGeometry) return;
1906
- const index = (_c = (_b = target.data) == null ? void 0 : _b.index) != null ? _c : -1;
1907
- const holeData = this.holes[index];
1908
- const effectiveOffset = this.constraintTarget === "original" ? 0 : this.currentGeometry.offset;
1909
- const constraintGeometry = {
1910
- ...this.currentGeometry,
1911
- width: Math.max(0, this.currentGeometry.width + effectiveOffset * 2),
1912
- height: Math.max(
1913
- 0,
1914
- this.currentGeometry.height + effectiveOffset * 2
1915
- ),
1916
- radius: Math.max(0, this.currentGeometry.radius + effectiveOffset)
1917
- };
1918
- const p = new import_fabric4.Point(target.left, target.top);
1919
- const newPos = this.calculateConstrainedPosition(
1920
- p,
1921
- constraintGeometry,
1922
- (_d = holeData == null ? void 0 : holeData.innerRadius) != null ? _d : 15,
1923
- (_e = holeData == null ? void 0 : holeData.outerRadius) != null ? _e : 25
2022
+ let feature;
2023
+ if ((_b = target.data) == null ? void 0 : _b.isGroup) {
2024
+ const indices = (_c = target.data) == null ? void 0 : _c.indices;
2025
+ if (indices && indices.length > 0) {
2026
+ feature = this.features[indices[0]];
2027
+ }
2028
+ } else {
2029
+ const index = (_d = target.data) == null ? void 0 : _d.index;
2030
+ if (index !== void 0) {
2031
+ feature = this.features[index];
2032
+ }
2033
+ }
2034
+ const geometry = this.getGeometryForFeature(
2035
+ this.currentGeometry,
2036
+ feature
1924
2037
  );
2038
+ const p = new import_fabric4.Point(target.left, target.top);
2039
+ const markerStrokeWidth = (target.strokeWidth || 2) * (target.scaleX || 1);
2040
+ const minDim = Math.min(target.getScaledWidth(), target.getScaledHeight());
2041
+ const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
2042
+ const snapped = this.constrainPosition(p, geometry, limit, feature);
1925
2043
  target.set({
1926
- left: newPos.x,
1927
- top: newPos.y
2044
+ left: snapped.x,
2045
+ top: snapped.y
1928
2046
  });
1929
2047
  };
1930
2048
  canvas.on("object:moving", this.handleMoving);
1931
2049
  }
1932
2050
  if (!this.handleModified) {
1933
2051
  this.handleModified = (e) => {
1934
- var _a;
2052
+ var _a, _b, _c, _d;
1935
2053
  const target = e.target;
1936
- if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "hole-marker") return;
1937
- const changed = this.enforceConstraints();
1938
- if (!changed) {
1939
- this.syncHolesFromCanvas();
2054
+ if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return;
2055
+ if ((_b = target.data) == null ? void 0 : _b.isGroup) {
2056
+ const groupObj = target;
2057
+ const indices = (_c = groupObj.data) == null ? void 0 : _c.indices;
2058
+ if (!indices) return;
2059
+ const groupCenter = new import_fabric4.Point(groupObj.left, groupObj.top);
2060
+ const newFeatures = [...this.features];
2061
+ const { x, y } = this.currentGeometry;
2062
+ groupObj.getObjects().forEach((child, i) => {
2063
+ const originalIndex = indices[i];
2064
+ const feature = this.features[originalIndex];
2065
+ const geometry = this.getGeometryForFeature(
2066
+ this.currentGeometry,
2067
+ feature
2068
+ );
2069
+ const { width, height } = geometry;
2070
+ const layoutLeft = x - width / 2;
2071
+ const layoutTop = y - height / 2;
2072
+ const absX = groupCenter.x + (child.left || 0);
2073
+ const absY = groupCenter.y + (child.top || 0);
2074
+ const normalizedX = width > 0 ? (absX - layoutLeft) / width : 0.5;
2075
+ const normalizedY = height > 0 ? (absY - layoutTop) / height : 0.5;
2076
+ newFeatures[originalIndex] = {
2077
+ ...newFeatures[originalIndex],
2078
+ x: normalizedX,
2079
+ y: normalizedY
2080
+ };
2081
+ });
2082
+ this.features = newFeatures;
2083
+ const configService = (_d = this.context) == null ? void 0 : _d.services.get(
2084
+ "ConfigurationService"
2085
+ );
2086
+ if (configService) {
2087
+ this.isUpdatingConfig = true;
2088
+ try {
2089
+ configService.update("dieline.features", this.features);
2090
+ } finally {
2091
+ this.isUpdatingConfig = false;
2092
+ }
2093
+ }
2094
+ } else {
2095
+ this.syncFeatureFromCanvas(target);
1940
2096
  }
1941
2097
  };
1942
2098
  canvas.on("object:modified", this.handleModified);
1943
2099
  }
1944
- this.initializeHoles();
1945
- }
1946
- initializeHoles() {
1947
- if (!this.canvasService) return;
1948
- this.redraw();
1949
- this.syncHolesToDieline();
1950
2100
  }
1951
2101
  teardown() {
1952
2102
  if (!this.canvasService) return;
@@ -1968,357 +2118,274 @@ var HoleTool = class {
1968
2118
  }
1969
2119
  const objects = canvas.getObjects().filter((obj) => {
1970
2120
  var _a;
1971
- return ((_a = obj.data) == null ? void 0 : _a.type) === "hole-marker";
2121
+ return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
1972
2122
  });
1973
2123
  objects.forEach((obj) => canvas.remove(obj));
1974
- if (this.context) {
1975
- const commandService = this.context.services.get("CommandService");
1976
- if (commandService) {
1977
- try {
1978
- commandService.executeCommand("setHoles", []);
1979
- } catch (e) {
1980
- }
1981
- }
1982
- }
1983
2124
  this.canvasService.requestRenderAll();
1984
2125
  }
1985
- syncHolesFromCanvas() {
1986
- if (!this.canvasService) return;
1987
- const objects = this.canvasService.canvas.getObjects().filter(
1988
- (obj) => {
1989
- var _a;
1990
- return ((_a = obj.data) == null ? void 0 : _a.type) === "hole-marker" || obj.name === "hole-marker";
1991
- }
1992
- );
1993
- if (objects.length === 0 && this.holes.length > 0) {
1994
- console.warn("HoleTool: No markers found on canvas to sync from");
1995
- return;
1996
- }
1997
- objects.sort(
1998
- (a, b) => {
1999
- var _a, _b, _c, _d;
2000
- 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);
2001
- }
2002
- );
2003
- const newHoles = objects.map((obj, i) => {
2004
- var _a, _b, _c, _d;
2005
- const original = this.holes[i];
2006
- const newAbsX = obj.left;
2007
- const newAbsY = obj.top;
2008
- if (isNaN(newAbsX) || isNaN(newAbsY)) {
2009
- console.error("HoleTool: Invalid marker coordinates", {
2010
- newAbsX,
2011
- newAbsY
2012
- });
2013
- return original;
2014
- }
2015
- const scale = ((_a = this.currentGeometry) == null ? void 0 : _a.scale) || 1;
2016
- const unit = ((_b = this.currentGeometry) == null ? void 0 : _b.unit) || "mm";
2017
- const unitScale = Coordinate.convertUnit(1, "mm", unit);
2018
- if (original && original.anchor && this.currentGeometry) {
2019
- const { x, y, width, height } = this.currentGeometry;
2020
- let bx = x;
2021
- let by = y;
2022
- const left = x - width / 2;
2023
- const right = x + width / 2;
2024
- const top = y - height / 2;
2025
- const bottom = y + height / 2;
2026
- switch (original.anchor) {
2027
- case "top-left":
2028
- bx = left;
2029
- by = top;
2030
- break;
2031
- case "top-center":
2032
- bx = x;
2033
- by = top;
2034
- break;
2035
- case "top-right":
2036
- bx = right;
2037
- by = top;
2038
- break;
2039
- case "center-left":
2040
- bx = left;
2041
- by = y;
2042
- break;
2043
- case "center":
2044
- bx = x;
2045
- by = y;
2046
- break;
2047
- case "center-right":
2048
- bx = right;
2049
- by = y;
2050
- break;
2051
- case "bottom-left":
2052
- bx = left;
2053
- by = bottom;
2054
- break;
2055
- case "bottom-center":
2056
- bx = x;
2057
- by = bottom;
2058
- break;
2059
- case "bottom-right":
2060
- bx = right;
2061
- by = bottom;
2062
- break;
2063
- }
2064
- return {
2065
- ...original,
2066
- // Denormalize offset back to physical units (mm)
2067
- offsetX: (newAbsX - bx) / scale / unitScale,
2068
- offsetY: (newAbsY - by) / scale / unitScale,
2069
- // Clear direct coordinates if we use anchor
2070
- x: void 0,
2071
- y: void 0,
2072
- // Ensure other properties are preserved
2073
- innerRadius: original.innerRadius,
2074
- outerRadius: original.outerRadius,
2075
- shape: original.shape || "circle"
2076
- };
2077
- }
2078
- let normalizedX = 0.5;
2079
- let normalizedY = 0.5;
2080
- if (this.currentGeometry) {
2081
- const { x, y, width, height } = this.currentGeometry;
2082
- const left = x - width / 2;
2083
- const top = y - height / 2;
2084
- normalizedX = width > 0 ? (newAbsX - left) / width : 0.5;
2085
- normalizedY = height > 0 ? (newAbsY - top) / height : 0.5;
2086
- } else {
2087
- const { width, height } = this.canvasService.canvas;
2088
- normalizedX = Coordinate.toNormalized(newAbsX, width || 800);
2089
- normalizedY = Coordinate.toNormalized(newAbsY, height || 600);
2090
- }
2126
+ constrainPosition(p, geometry, limit, feature) {
2127
+ if (feature && feature.placement === "internal") {
2128
+ const minX = geometry.x - geometry.width / 2;
2129
+ const maxX = geometry.x + geometry.width / 2;
2130
+ const minY = geometry.y - geometry.height / 2;
2131
+ const maxY = geometry.y + geometry.height / 2;
2091
2132
  return {
2092
- ...original,
2093
- x: normalizedX,
2094
- y: normalizedY,
2095
- // Clear offsets if we are using direct normalized coordinates
2096
- offsetX: void 0,
2097
- offsetY: void 0,
2098
- // Ensure other properties are preserved
2099
- innerRadius: (_c = original == null ? void 0 : original.innerRadius) != null ? _c : 15,
2100
- outerRadius: (_d = original == null ? void 0 : original.outerRadius) != null ? _d : 25,
2101
- shape: (original == null ? void 0 : original.shape) || "circle"
2133
+ x: Math.max(minX, Math.min(maxX, p.x)),
2134
+ y: Math.max(minY, Math.min(maxY, p.y))
2102
2135
  };
2136
+ }
2137
+ const nearest = getNearestPointOnDieline({ x: p.x, y: p.y }, {
2138
+ ...geometry,
2139
+ features: []
2103
2140
  });
2104
- this.holes = newHoles;
2105
- this.syncHolesToDieline();
2141
+ const dx = p.x - nearest.x;
2142
+ const dy = p.y - nearest.y;
2143
+ const dist = Math.sqrt(dx * dx + dy * dy);
2144
+ if (dist <= limit) {
2145
+ return { x: p.x, y: p.y };
2146
+ }
2147
+ const scale = limit / dist;
2148
+ return {
2149
+ x: nearest.x + dx * scale,
2150
+ y: nearest.y + dy * scale
2151
+ };
2106
2152
  }
2107
- syncHolesToDieline() {
2108
- if (!this.context || !this.canvasService) return;
2153
+ syncFeatureFromCanvas(target) {
2154
+ var _a;
2155
+ if (!this.currentGeometry || !this.context) return;
2156
+ const index = (_a = target.data) == null ? void 0 : _a.index;
2157
+ if (index === void 0 || index < 0 || index >= this.features.length)
2158
+ return;
2159
+ const feature = this.features[index];
2160
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
2161
+ const { width, height, x, y } = geometry;
2162
+ const left = x - width / 2;
2163
+ const top = y - height / 2;
2164
+ const normalizedX = width > 0 ? (target.left - left) / width : 0.5;
2165
+ const normalizedY = height > 0 ? (target.top - top) / height : 0.5;
2166
+ const updatedFeature = {
2167
+ ...feature,
2168
+ x: normalizedX,
2169
+ y: normalizedY
2170
+ // Could also update rotation if we allowed rotating markers
2171
+ };
2172
+ const newFeatures = [...this.features];
2173
+ newFeatures[index] = updatedFeature;
2174
+ this.features = newFeatures;
2109
2175
  const configService = this.context.services.get(
2110
2176
  "ConfigurationService"
2111
2177
  );
2112
2178
  if (configService) {
2113
2179
  this.isUpdatingConfig = true;
2114
2180
  try {
2115
- configService.update("dieline.holes", this.holes);
2181
+ configService.update("dieline.features", this.features);
2116
2182
  } finally {
2117
2183
  this.isUpdatingConfig = false;
2118
2184
  }
2119
2185
  }
2120
2186
  }
2121
2187
  redraw() {
2122
- if (!this.canvasService) return;
2188
+ if (!this.canvasService || !this.currentGeometry) return;
2123
2189
  const canvas = this.canvasService.canvas;
2124
- const { width, height } = canvas;
2190
+ const geometry = this.currentGeometry;
2125
2191
  const existing = canvas.getObjects().filter((obj) => {
2126
2192
  var _a;
2127
- return ((_a = obj.data) == null ? void 0 : _a.type) === "hole-marker";
2193
+ return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
2128
2194
  });
2129
2195
  existing.forEach((obj) => canvas.remove(obj));
2130
- const holes = this.holes;
2131
- if (!holes || holes.length === 0) {
2196
+ if (!this.features || this.features.length === 0) {
2132
2197
  this.canvasService.requestRenderAll();
2133
2198
  return;
2134
2199
  }
2135
- const geometry = this.currentGeometry || {
2136
- x: (width || 800) / 2,
2137
- y: (height || 600) / 2,
2138
- width: width || 800,
2139
- height: height || 600,
2140
- scale: 1
2141
- // Default scale if no geometry loaded
2200
+ const scale = geometry.scale || 1;
2201
+ const finalScale = scale;
2202
+ const groups = {};
2203
+ const singles = [];
2204
+ this.features.forEach((f, i) => {
2205
+ if (f.groupId) {
2206
+ if (!groups[f.groupId]) groups[f.groupId] = [];
2207
+ groups[f.groupId].push({ feature: f, index: i });
2208
+ } else {
2209
+ singles.push({ feature: f, index: i });
2210
+ }
2211
+ });
2212
+ const createMarkerShape = (feature, pos) => {
2213
+ const featureScale = scale;
2214
+ const visualWidth = (feature.width || 10) * featureScale;
2215
+ const visualHeight = (feature.height || 10) * featureScale;
2216
+ const visualRadius = (feature.radius || 0) * featureScale;
2217
+ const color = feature.color || (feature.operation === "add" ? "#00FF00" : "#FF0000");
2218
+ const strokeDash = feature.strokeDash || (feature.operation === "subtract" ? [4, 4] : void 0);
2219
+ let shape;
2220
+ if (feature.shape === "rect") {
2221
+ shape = new import_fabric4.Rect({
2222
+ width: visualWidth,
2223
+ height: visualHeight,
2224
+ rx: visualRadius,
2225
+ ry: visualRadius,
2226
+ fill: "transparent",
2227
+ stroke: color,
2228
+ strokeWidth: 2,
2229
+ strokeDashArray: strokeDash,
2230
+ originX: "center",
2231
+ originY: "center",
2232
+ left: pos.x,
2233
+ top: pos.y
2234
+ });
2235
+ } else {
2236
+ shape = new import_fabric4.Circle({
2237
+ radius: visualRadius || 5 * finalScale,
2238
+ fill: "transparent",
2239
+ stroke: color,
2240
+ strokeWidth: 2,
2241
+ strokeDashArray: strokeDash,
2242
+ originX: "center",
2243
+ originY: "center",
2244
+ left: pos.x,
2245
+ top: pos.y
2246
+ });
2247
+ }
2248
+ if (feature.rotation) {
2249
+ shape.rotate(feature.rotation);
2250
+ }
2251
+ return shape;
2142
2252
  };
2143
- holes.forEach((hole, index) => {
2144
- const scale = geometry.scale || 1;
2145
- const unit = geometry.unit || "mm";
2146
- const unitScale = Coordinate.convertUnit(1, "mm", unit);
2147
- const visualInnerRadius = hole.innerRadius * unitScale * scale;
2148
- const visualOuterRadius = hole.outerRadius * unitScale * scale;
2149
- const pos = resolveHolePosition(
2150
- {
2151
- ...hole,
2152
- offsetX: (hole.offsetX || 0) * unitScale * scale,
2153
- offsetY: (hole.offsetY || 0) * unitScale * scale
2154
- },
2155
- geometry,
2156
- { width: geometry.width, height: geometry.height }
2157
- // Use geometry dims instead of canvas
2253
+ singles.forEach(({ feature, index }) => {
2254
+ const geometry2 = this.getGeometryForFeature(
2255
+ this.currentGeometry,
2256
+ feature
2158
2257
  );
2159
- const isSquare = hole.shape === "square";
2160
- const innerMarker = isSquare ? new import_fabric4.Rect({
2161
- width: visualInnerRadius * 2,
2162
- height: visualInnerRadius * 2,
2163
- fill: "transparent",
2164
- stroke: "red",
2165
- strokeWidth: 2,
2166
- originX: "center",
2167
- originY: "center"
2168
- }) : new import_fabric4.Circle({
2169
- radius: visualInnerRadius,
2170
- fill: "transparent",
2171
- stroke: "red",
2172
- strokeWidth: 2,
2173
- originX: "center",
2174
- originY: "center"
2258
+ const pos = resolveFeaturePosition(feature, geometry2);
2259
+ const marker = createMarkerShape(feature, pos);
2260
+ marker.set({
2261
+ visible: this.isToolActive,
2262
+ selectable: this.isToolActive,
2263
+ evented: this.isToolActive,
2264
+ hasControls: false,
2265
+ hasBorders: false,
2266
+ hoverCursor: "move",
2267
+ lockRotation: true,
2268
+ lockScalingX: true,
2269
+ lockScalingY: true,
2270
+ data: { type: "feature-marker", index, isGroup: false }
2175
2271
  });
2176
- const outerMarker = isSquare ? new import_fabric4.Rect({
2177
- width: visualOuterRadius * 2,
2178
- height: visualOuterRadius * 2,
2179
- fill: "transparent",
2180
- stroke: "#666",
2181
- strokeWidth: 1,
2182
- strokeDashArray: [5, 5],
2183
- originX: "center",
2184
- originY: "center"
2185
- }) : new import_fabric4.Circle({
2186
- radius: visualOuterRadius,
2187
- fill: "transparent",
2188
- stroke: "#666",
2189
- strokeWidth: 1,
2190
- strokeDashArray: [5, 5],
2191
- originX: "center",
2192
- originY: "center"
2272
+ marker.set("opacity", 0);
2273
+ marker.on("mouseover", () => {
2274
+ marker.set("opacity", 1);
2275
+ canvas.requestRenderAll();
2193
2276
  });
2194
- const holeGroup = new import_fabric4.Group([outerMarker, innerMarker], {
2195
- left: pos.x,
2196
- top: pos.y,
2197
- originX: "center",
2198
- originY: "center",
2199
- selectable: true,
2277
+ marker.on("mouseout", () => {
2278
+ if (canvas.getActiveObject() !== marker) {
2279
+ marker.set("opacity", 0);
2280
+ canvas.requestRenderAll();
2281
+ }
2282
+ });
2283
+ marker.on("selected", () => {
2284
+ marker.set("opacity", 1);
2285
+ canvas.requestRenderAll();
2286
+ });
2287
+ marker.on("deselected", () => {
2288
+ marker.set("opacity", 0);
2289
+ canvas.requestRenderAll();
2290
+ });
2291
+ canvas.add(marker);
2292
+ canvas.bringObjectToFront(marker);
2293
+ });
2294
+ Object.keys(groups).forEach((groupId) => {
2295
+ const members = groups[groupId];
2296
+ if (members.length === 0) return;
2297
+ const shapes = members.map(({ feature }) => {
2298
+ const geometry2 = this.getGeometryForFeature(
2299
+ this.currentGeometry,
2300
+ feature
2301
+ );
2302
+ const pos = resolveFeaturePosition(feature, geometry2);
2303
+ return createMarkerShape(feature, pos);
2304
+ });
2305
+ const groupObj = new import_fabric4.Group(shapes, {
2306
+ visible: this.isToolActive,
2307
+ selectable: this.isToolActive,
2308
+ evented: this.isToolActive,
2200
2309
  hasControls: false,
2201
- // Don't allow resizing/rotating
2202
2310
  hasBorders: false,
2203
- subTargetCheck: false,
2204
- opacity: 0,
2205
- // Default hidden
2206
2311
  hoverCursor: "move",
2207
- data: { type: "hole-marker", index }
2312
+ lockRotation: true,
2313
+ lockScalingX: true,
2314
+ lockScalingY: true,
2315
+ subTargetCheck: true,
2316
+ // Allow events to pass through if needed, but we treat as one
2317
+ interactive: false,
2318
+ // Children not interactive
2319
+ // @ts-ignore
2320
+ data: {
2321
+ type: "feature-marker",
2322
+ isGroup: true,
2323
+ groupId,
2324
+ indices: members.map((m) => m.index)
2325
+ }
2208
2326
  });
2209
- holeGroup.name = "hole-marker";
2210
- holeGroup.on("mouseover", () => {
2211
- holeGroup.set("opacity", 1);
2327
+ groupObj.set("opacity", 0);
2328
+ groupObj.on("mouseover", () => {
2329
+ groupObj.set("opacity", 1);
2212
2330
  canvas.requestRenderAll();
2213
2331
  });
2214
- holeGroup.on("mouseout", () => {
2215
- if (canvas.getActiveObject() !== holeGroup) {
2216
- holeGroup.set("opacity", 0);
2332
+ groupObj.on("mouseout", () => {
2333
+ if (canvas.getActiveObject() !== groupObj) {
2334
+ groupObj.set("opacity", 0);
2217
2335
  canvas.requestRenderAll();
2218
2336
  }
2219
2337
  });
2220
- holeGroup.on("selected", () => {
2221
- holeGroup.set("opacity", 1);
2338
+ groupObj.on("selected", () => {
2339
+ groupObj.set("opacity", 1);
2222
2340
  canvas.requestRenderAll();
2223
2341
  });
2224
- holeGroup.on("deselected", () => {
2225
- holeGroup.set("opacity", 0);
2342
+ groupObj.on("deselected", () => {
2343
+ groupObj.set("opacity", 0);
2226
2344
  canvas.requestRenderAll();
2227
2345
  });
2228
- canvas.add(holeGroup);
2229
- canvas.bringObjectToFront(holeGroup);
2230
- });
2231
- const markers = canvas.getObjects().filter((o) => {
2232
- var _a;
2233
- return ((_a = o.data) == null ? void 0 : _a.type) === "hole-marker";
2346
+ canvas.add(groupObj);
2347
+ canvas.bringObjectToFront(groupObj);
2234
2348
  });
2235
- markers.forEach((m) => canvas.bringObjectToFront(m));
2236
2349
  this.canvasService.requestRenderAll();
2237
2350
  }
2238
2351
  enforceConstraints() {
2239
- const geometry = this.currentGeometry;
2240
- if (!geometry || !this.canvasService) {
2241
- return false;
2242
- }
2243
- const effectiveOffset = this.constraintTarget === "original" ? 0 : geometry.offset;
2244
- const constraintGeometry = {
2245
- ...geometry,
2246
- width: Math.max(0, geometry.width + effectiveOffset * 2),
2247
- height: Math.max(0, geometry.height + effectiveOffset * 2),
2248
- radius: Math.max(0, geometry.radius + effectiveOffset)
2249
- };
2250
- const objects = this.canvasService.canvas.getObjects().filter((obj) => {
2352
+ if (!this.canvasService || !this.currentGeometry) return;
2353
+ const canvas = this.canvasService.canvas;
2354
+ const markers = canvas.getObjects().filter((obj) => {
2251
2355
  var _a;
2252
- return ((_a = obj.data) == null ? void 0 : _a.type) === "hole-marker";
2356
+ return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
2253
2357
  });
2254
- let changed = false;
2255
- objects.sort(
2256
- (a, b) => {
2257
- var _a, _b, _c, _d;
2258
- 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);
2358
+ markers.forEach((marker) => {
2359
+ var _a, _b, _c;
2360
+ let feature;
2361
+ if ((_a = marker.data) == null ? void 0 : _a.isGroup) {
2362
+ const indices = (_b = marker.data) == null ? void 0 : _b.indices;
2363
+ if (indices && indices.length > 0) {
2364
+ feature = this.features[indices[0]];
2365
+ }
2366
+ } else {
2367
+ const index = (_c = marker.data) == null ? void 0 : _c.index;
2368
+ if (index !== void 0) {
2369
+ feature = this.features[index];
2370
+ }
2259
2371
  }
2260
- );
2261
- const newHoles = [];
2262
- objects.forEach((obj, i) => {
2263
- var _a, _b;
2264
- const currentPos = new import_fabric4.Point(obj.left, obj.top);
2265
- const holeData = this.holes[i];
2266
- const scale = geometry.scale || 1;
2267
- const unit = geometry.unit || "mm";
2268
- const unitScale = Coordinate.convertUnit(1, "mm", unit);
2269
- const innerR = ((_a = holeData == null ? void 0 : holeData.innerRadius) != null ? _a : 15) * unitScale * scale;
2270
- const outerR = ((_b = holeData == null ? void 0 : holeData.outerRadius) != null ? _b : 25) * unitScale * scale;
2271
- const newPos = this.calculateConstrainedPosition(
2272
- currentPos,
2273
- constraintGeometry,
2274
- innerR,
2275
- outerR
2372
+ const geometry = this.getGeometryForFeature(
2373
+ this.currentGeometry,
2374
+ feature
2276
2375
  );
2277
- if (currentPos.distanceFrom(newPos) > 0.1) {
2278
- obj.set({
2279
- left: newPos.x,
2280
- top: newPos.y
2281
- });
2282
- obj.setCoords();
2283
- changed = true;
2284
- }
2376
+ const markerStrokeWidth = (marker.strokeWidth || 2) * (marker.scaleX || 1);
2377
+ const minDim = Math.min(marker.getScaledWidth(), marker.getScaledHeight());
2378
+ const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
2379
+ const snapped = this.constrainPosition(
2380
+ new import_fabric4.Point(marker.left, marker.top),
2381
+ geometry,
2382
+ limit,
2383
+ feature
2384
+ );
2385
+ marker.set({ left: snapped.x, top: snapped.y });
2386
+ marker.setCoords();
2285
2387
  });
2286
- if (changed) {
2287
- this.syncHolesFromCanvas();
2288
- return true;
2289
- }
2290
- return false;
2291
- }
2292
- calculateConstrainedPosition(p, g, innerRadius, outerRadius) {
2293
- const options = {
2294
- ...g,
2295
- holes: []
2296
- // We don't need holes for boundary calculation
2297
- };
2298
- const nearest = getNearestPointOnDieline(
2299
- { x: p.x, y: p.y },
2300
- options
2301
- );
2302
- const nearestP = new import_fabric4.Point(nearest.x, nearest.y);
2303
- const dist = p.distanceFrom(nearestP);
2304
- const v = p.subtract(nearestP);
2305
- const center = new import_fabric4.Point(g.x, g.y);
2306
- const distToCenter = p.distanceFrom(center);
2307
- const nearestDistToCenter = nearestP.distanceFrom(center);
2308
- let signedDist = dist;
2309
- if (distToCenter < nearestDistToCenter) {
2310
- signedDist = -dist;
2311
- }
2312
- let clampedDist = signedDist;
2313
- if (signedDist > 0) {
2314
- clampedDist = Math.min(signedDist, innerRadius);
2315
- } else {
2316
- clampedDist = Math.max(signedDist, -outerRadius);
2317
- }
2318
- if (dist < 1e-3) return nearestP;
2319
- const scale = Math.abs(clampedDist) / (dist || 1);
2320
- const offset = v.scalarMultiply(scale);
2321
- return nearestP.add(offset);
2388
+ canvas.requestRenderAll();
2322
2389
  }
2323
2390
  };
2324
2391
 
@@ -2335,6 +2402,11 @@ var ImageTool = class {
2335
2402
  this.objectMap = /* @__PURE__ */ new Map();
2336
2403
  this.loadResolvers = /* @__PURE__ */ new Map();
2337
2404
  this.isUpdatingConfig = false;
2405
+ this.isToolActive = false;
2406
+ this.onToolActivated = (event) => {
2407
+ this.isToolActive = event.id === this.id;
2408
+ this.updateInteractivity();
2409
+ };
2338
2410
  }
2339
2411
  activate(context) {
2340
2412
  this.context = context;
@@ -2343,6 +2415,7 @@ var ImageTool = class {
2343
2415
  console.warn("CanvasService not found for ImageTool");
2344
2416
  return;
2345
2417
  }
2418
+ context.eventBus.on("tool:activated", this.onToolActivated);
2346
2419
  const configService = context.services.get(
2347
2420
  "ConfigurationService"
2348
2421
  );
@@ -2360,6 +2433,7 @@ var ImageTool = class {
2360
2433
  this.updateImages();
2361
2434
  }
2362
2435
  deactivate(context) {
2436
+ context.eventBus.off("tool:activated", this.onToolActivated);
2363
2437
  if (this.canvasService) {
2364
2438
  const layer = this.canvasService.getLayer("user");
2365
2439
  if (layer) {
@@ -2373,6 +2447,18 @@ var ImageTool = class {
2373
2447
  this.context = void 0;
2374
2448
  }
2375
2449
  }
2450
+ updateInteractivity() {
2451
+ var _a;
2452
+ this.objectMap.forEach((obj) => {
2453
+ obj.set({
2454
+ selectable: this.isToolActive,
2455
+ evented: this.isToolActive,
2456
+ hasControls: this.isToolActive,
2457
+ hasBorders: this.isToolActive
2458
+ });
2459
+ });
2460
+ (_a = this.canvasService) == null ? void 0 : _a.requestRenderAll();
2461
+ }
2376
2462
  contribute() {
2377
2463
  return {
2378
2464
  [import_core5.ContributionPointIds.CONFIGURATIONS]: [
@@ -2560,6 +2646,14 @@ var ImageTool = class {
2560
2646
  const layout = this.getLayoutInfo();
2561
2647
  this.items.forEach((item, index) => {
2562
2648
  let obj = this.objectMap.get(item.id);
2649
+ if (obj && obj.getSrc) {
2650
+ const currentSrc = obj.getSrc();
2651
+ if (currentSrc !== item.url) {
2652
+ layer.remove(obj);
2653
+ this.objectMap.delete(item.id);
2654
+ obj = void 0;
2655
+ }
2656
+ }
2563
2657
  if (!obj) {
2564
2658
  this.loadImage(item, layer, layout);
2565
2659
  } else {
@@ -2616,7 +2710,11 @@ var ImageTool = class {
2616
2710
  originY: "center",
2617
2711
  data: { id: item.id },
2618
2712
  uniformScaling: true,
2619
- lockScalingFlip: true
2713
+ lockScalingFlip: true,
2714
+ selectable: this.isToolActive,
2715
+ evented: this.isToolActive,
2716
+ hasControls: this.isToolActive,
2717
+ hasBorders: this.isToolActive
2620
2718
  });
2621
2719
  image.setControlsVisibility({
2622
2720
  mt: false,
@@ -3467,6 +3565,24 @@ var CanvasService = class {
3467
3565
  ...options
3468
3566
  });
3469
3567
  }
3568
+ if (options == null ? void 0 : options.eventBus) {
3569
+ this.setEventBus(options.eventBus);
3570
+ }
3571
+ }
3572
+ setEventBus(eventBus) {
3573
+ this.eventBus = eventBus;
3574
+ this.setupEvents();
3575
+ }
3576
+ setupEvents() {
3577
+ if (!this.eventBus) return;
3578
+ const bus = this.eventBus;
3579
+ const forward = (name) => (e) => bus.emit(name, e);
3580
+ this.canvas.on("selection:created", forward("selection:created"));
3581
+ this.canvas.on("selection:updated", forward("selection:updated"));
3582
+ this.canvas.on("selection:cleared", forward("selection:cleared"));
3583
+ this.canvas.on("object:modified", forward("object:modified"));
3584
+ this.canvas.on("object:added", forward("object:added"));
3585
+ this.canvas.on("object:removed", forward("object:removed"));
3470
3586
  }
3471
3587
  dispose() {
3472
3588
  this.canvas.dispose();
@@ -3524,8 +3640,8 @@ var CanvasService = class {
3524
3640
  BackgroundTool,
3525
3641
  CanvasService,
3526
3642
  DielineTool,
3643
+ FeatureTool,
3527
3644
  FilmTool,
3528
- HoleTool,
3529
3645
  ImageTool,
3530
3646
  MirrorTool,
3531
3647
  RulerTool,