@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.mjs CHANGED
@@ -208,6 +208,7 @@ import {
208
208
  import { Path, Pattern } from "fabric";
209
209
 
210
210
  // src/tracer.ts
211
+ import paper from "paper";
211
212
  var ImageTracer = class {
212
213
  /**
213
214
  * Main entry point: Traces an image URL to an SVG path string.
@@ -215,7 +216,7 @@ var ImageTracer = class {
215
216
  * @param options Configuration options.
216
217
  */
217
218
  static async trace(imageUrl, options = {}) {
218
- var _a, _b, _c, _d, _e;
219
+ var _a, _b, _c, _d, _e, _f, _g;
219
220
  const img = await this.loadImage(imageUrl);
220
221
  const width = img.width;
221
222
  const height = img.height;
@@ -232,23 +233,37 @@ var ImageTracer = class {
232
233
  Math.floor(Math.max(width, height) * 0.02)
233
234
  );
234
235
  const radius = (_b = options.morphologyRadius) != null ? _b : adaptiveRadius;
235
- let mask = this.createMask(imageData, threshold);
236
+ const expand = (_c = options.expand) != null ? _c : 0;
237
+ const padding = radius + expand + 2;
238
+ const paddedWidth = width + padding * 2;
239
+ const paddedHeight = height + padding * 2;
240
+ let mask = this.createMask(imageData, threshold, padding, paddedWidth, paddedHeight);
236
241
  if (radius > 0) {
237
- mask = this.dilate(mask, width, height, radius);
238
- mask = this.erode(mask, width, height, radius);
239
- mask = this.fillHoles(mask, width, height);
242
+ mask = this.circularMorphology(mask, paddedWidth, paddedHeight, radius, "closing");
243
+ mask = this.fillHoles(mask, paddedWidth, paddedHeight);
244
+ const smoothRadius = Math.max(2, Math.floor(radius * 0.3));
245
+ mask = this.circularMorphology(mask, paddedWidth, paddedHeight, smoothRadius, "closing");
246
+ } else {
247
+ mask = this.fillHoles(mask, paddedWidth, paddedHeight);
248
+ }
249
+ if (expand > 0) {
250
+ mask = this.circularMorphology(mask, paddedWidth, paddedHeight, expand, "dilate");
240
251
  }
241
- const allContourPoints = this.traceAllContours(mask, width, height);
252
+ const allContourPoints = this.traceAllContours(mask, paddedWidth, paddedHeight);
242
253
  if (allContourPoints.length === 0) {
243
- const w = (_c = options.scaleToWidth) != null ? _c : width;
244
- const h = (_d = options.scaleToHeight) != null ? _d : height;
254
+ const w = (_d = options.scaleToWidth) != null ? _d : width;
255
+ const h = (_e = options.scaleToHeight) != null ? _e : height;
245
256
  return `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`;
246
257
  }
247
258
  const primaryContour = allContourPoints.sort(
248
259
  (a, b) => b.length - a.length
249
260
  )[0];
261
+ const unpaddedPoints = primaryContour.map((p) => ({
262
+ x: p.x - padding,
263
+ y: p.y - padding
264
+ }));
250
265
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
251
- for (const p of primaryContour) {
266
+ for (const p of unpaddedPoints) {
252
267
  if (p.x < minX) minX = p.x;
253
268
  if (p.y < minY) minY = p.y;
254
269
  if (p.x > maxX) maxX = p.x;
@@ -260,95 +275,119 @@ var ImageTracer = class {
260
275
  width: maxX - minX,
261
276
  height: maxY - minY
262
277
  };
263
- let finalPoints = primaryContour;
278
+ let finalPoints = unpaddedPoints;
264
279
  if (options.scaleToWidth && options.scaleToHeight) {
265
280
  finalPoints = this.scalePoints(
266
- primaryContour,
281
+ unpaddedPoints,
267
282
  options.scaleToWidth,
268
283
  options.scaleToHeight,
269
284
  globalBounds
270
285
  );
271
286
  }
272
- const simplifiedPoints = this.douglasPeucker(
273
- finalPoints,
274
- (_e = options.simplifyTolerance) != null ? _e : 2
275
- );
276
- return this.pointsToSVG(simplifiedPoints);
287
+ const useSmoothing = options.smoothing !== false;
288
+ if (useSmoothing) {
289
+ return this.pointsToSVGPaper(finalPoints, (_f = options.simplifyTolerance) != null ? _f : 2.5);
290
+ } else {
291
+ const simplifiedPoints = this.douglasPeucker(
292
+ finalPoints,
293
+ (_g = options.simplifyTolerance) != null ? _g : 2
294
+ );
295
+ return this.pointsToSVG(simplifiedPoints);
296
+ }
277
297
  }
278
- static createMask(imageData, threshold) {
298
+ static createMask(imageData, threshold, padding, paddedWidth, paddedHeight) {
279
299
  const { width, height, data } = imageData;
280
- const mask = new Uint8Array(width * height);
281
- for (let i = 0; i < width * height; i++) {
282
- const idx = i * 4;
283
- const r = data[idx];
284
- const g = data[idx + 1];
285
- const b = data[idx + 2];
286
- const a = data[idx + 3];
287
- if (a > threshold && !(r > 240 && g > 240 && b > 240)) {
288
- mask[i] = 1;
289
- } else {
290
- mask[i] = 0;
300
+ const mask = new Uint8Array(paddedWidth * paddedHeight);
301
+ let hasTransparency = false;
302
+ for (let i = 3; i < data.length; i += 4) {
303
+ if (data[i] < 255) {
304
+ hasTransparency = true;
305
+ break;
291
306
  }
292
307
  }
293
- return mask;
294
- }
295
- /**
296
- * Fast 1D-separable Dilation
297
- */
298
- static dilate(mask, width, height, radius) {
299
- const horizontal = new Uint8Array(width * height);
300
308
  for (let y = 0; y < height; y++) {
301
- let count = 0;
302
- for (let x = -radius; x < width; x++) {
303
- if (x + radius < width && mask[y * width + x + radius]) count++;
304
- if (x - radius - 1 >= 0 && mask[y * width + x - radius - 1]) count--;
305
- if (x >= 0) horizontal[y * width + x] = count > 0 ? 1 : 0;
306
- }
307
- }
308
- const vertical = new Uint8Array(width * height);
309
- for (let x = 0; x < width; x++) {
310
- let count = 0;
311
- for (let y = -radius; y < height; y++) {
312
- if (y + radius < height && horizontal[(y + radius) * width + x])
313
- count++;
314
- if (y - radius - 1 >= 0 && horizontal[(y - radius - 1) * width + x])
315
- count--;
316
- if (y >= 0) vertical[y * width + x] = count > 0 ? 1 : 0;
309
+ for (let x = 0; x < width; x++) {
310
+ const srcIdx = (y * width + x) * 4;
311
+ const r = data[srcIdx];
312
+ const g = data[srcIdx + 1];
313
+ const b = data[srcIdx + 2];
314
+ const a = data[srcIdx + 3];
315
+ const destIdx = (y + padding) * paddedWidth + (x + padding);
316
+ if (hasTransparency) {
317
+ if (a > threshold) {
318
+ mask[destIdx] = 1;
319
+ }
320
+ } else {
321
+ if (!(r > 240 && g > 240 && b > 240)) {
322
+ mask[destIdx] = 1;
323
+ }
324
+ }
317
325
  }
318
326
  }
319
- return vertical;
327
+ return mask;
320
328
  }
321
329
  /**
322
- * Fast 1D-separable Erosion
330
+ * Fast circular morphology using a distance-transform inspired separable approach.
331
+ * O(N * R) complexity, where R is the radius.
323
332
  */
324
- static erode(mask, width, height, radius) {
325
- const horizontal = new Uint8Array(width * height);
326
- for (let y = 0; y < height; y++) {
327
- let count = 0;
328
- for (let x = -radius; x < width; x++) {
329
- if (x + radius < width && mask[y * width + x + radius]) count++;
330
- if (x - radius - 1 >= 0 && mask[y * width + x - radius - 1]) count--;
331
- if (x >= 0) {
332
- const winWidth = Math.min(x + radius, width - 1) - Math.max(x - radius, 0) + 1;
333
- horizontal[y * width + x] = count === winWidth ? 1 : 0;
333
+ static circularMorphology(mask, width, height, radius, op) {
334
+ const dilate = (m, r) => {
335
+ const horizontalDist = new Int32Array(width * height);
336
+ for (let y = 0; y < height; y++) {
337
+ let lastSolid = -r * 2;
338
+ for (let x = 0; x < width; x++) {
339
+ if (m[y * width + x]) lastSolid = x;
340
+ horizontalDist[y * width + x] = x - lastSolid;
341
+ }
342
+ lastSolid = width + r * 2;
343
+ for (let x = width - 1; x >= 0; x--) {
344
+ if (m[y * width + x]) lastSolid = x;
345
+ horizontalDist[y * width + x] = Math.min(
346
+ horizontalDist[y * width + x],
347
+ lastSolid - x
348
+ );
334
349
  }
335
350
  }
336
- }
337
- const vertical = new Uint8Array(width * height);
338
- for (let x = 0; x < width; x++) {
339
- let count = 0;
340
- for (let y = -radius; y < height; y++) {
341
- if (y + radius < height && horizontal[(y + radius) * width + x])
342
- count++;
343
- if (y - radius - 1 >= 0 && horizontal[(y - radius - 1) * width + x])
344
- count--;
345
- if (y >= 0) {
346
- const winHeight = Math.min(y + radius, height - 1) - Math.max(y - radius, 0) + 1;
347
- vertical[y * width + x] = count === winHeight ? 1 : 0;
351
+ const result = new Uint8Array(width * height);
352
+ const r2 = r * r;
353
+ for (let x = 0; x < width; x++) {
354
+ for (let y = 0; y < height; y++) {
355
+ let found = false;
356
+ const minY = Math.max(0, y - r);
357
+ const maxY = Math.min(height - 1, y + r);
358
+ for (let dy = minY; dy <= maxY; dy++) {
359
+ const dY = dy - y;
360
+ const hDist = horizontalDist[dy * width + x];
361
+ if (hDist * hDist + dY * dY <= r2) {
362
+ found = true;
363
+ break;
364
+ }
365
+ }
366
+ if (found) result[y * width + x] = 1;
348
367
  }
349
368
  }
369
+ return result;
370
+ };
371
+ const erode = (m, r) => {
372
+ const inverted = new Uint8Array(m.length);
373
+ for (let i = 0; i < m.length; i++) inverted[i] = m[i] ? 0 : 1;
374
+ const dilatedInverted = dilate(inverted, r);
375
+ const result = new Uint8Array(m.length);
376
+ for (let i = 0; i < m.length; i++) result[i] = dilatedInverted[i] ? 0 : 1;
377
+ return result;
378
+ };
379
+ switch (op) {
380
+ case "dilate":
381
+ return dilate(mask, radius);
382
+ case "erode":
383
+ return erode(mask, radius);
384
+ case "closing":
385
+ return erode(dilate(mask, radius), radius);
386
+ case "opening":
387
+ return dilate(erode(mask, radius), radius);
388
+ default:
389
+ return mask;
350
390
  }
351
- return vertical;
352
391
  }
353
392
  /**
354
393
  * Fills internal holes in the binary mask using flood fill from edges.
@@ -548,6 +587,23 @@ var ImageTracer = class {
548
587
  const tail = points.slice(1);
549
588
  return `M ${head.x} ${head.y} ` + tail.map((p) => `L ${p.x} ${p.y}`).join(" ") + " Z";
550
589
  }
590
+ static ensurePaper() {
591
+ if (!paper.project) {
592
+ paper.setup(new paper.Size(100, 100));
593
+ }
594
+ }
595
+ static pointsToSVGPaper(points, tolerance) {
596
+ if (points.length < 3) return this.pointsToSVG(points);
597
+ this.ensurePaper();
598
+ const path = new paper.Path({
599
+ segments: points.map((p) => [p.x, p.y]),
600
+ closed: true
601
+ });
602
+ path.simplify(tolerance);
603
+ const data = path.pathData;
604
+ path.remove();
605
+ return data;
606
+ }
551
607
  };
552
608
 
553
609
  // src/coordinate.ts
@@ -622,96 +678,45 @@ var Coordinate = class {
622
678
  };
623
679
 
624
680
  // src/geometry.ts
625
- import paper from "paper";
626
- function resolveHolePosition(hole, geometry, canvasSize) {
627
- if (hole.anchor) {
628
- const { x, y, width, height } = geometry;
629
- let bx = x;
630
- let by = y;
631
- const left = x - width / 2;
632
- const right = x + width / 2;
633
- const top = y - height / 2;
634
- const bottom = y + height / 2;
635
- switch (hole.anchor) {
636
- case "top-left":
637
- bx = left;
638
- by = top;
639
- break;
640
- case "top-center":
641
- bx = x;
642
- by = top;
643
- break;
644
- case "top-right":
645
- bx = right;
646
- by = top;
647
- break;
648
- case "center-left":
649
- bx = left;
650
- by = y;
651
- break;
652
- case "center":
653
- bx = x;
654
- by = y;
655
- break;
656
- case "center-right":
657
- bx = right;
658
- by = y;
659
- break;
660
- case "bottom-left":
661
- bx = left;
662
- by = bottom;
663
- break;
664
- case "bottom-center":
665
- bx = x;
666
- by = bottom;
667
- break;
668
- case "bottom-right":
669
- bx = right;
670
- by = bottom;
671
- break;
672
- }
673
- return {
674
- x: bx + (hole.offsetX || 0),
675
- y: by + (hole.offsetY || 0)
676
- };
677
- } else if (hole.x !== void 0 && hole.y !== void 0) {
678
- const { x, width, y, height } = geometry;
679
- return {
680
- x: hole.x * width + (x - width / 2) + (hole.offsetX || 0),
681
- y: hole.y * height + (y - height / 2) + (hole.offsetY || 0)
682
- };
683
- }
684
- return { x: 0, y: 0 };
681
+ import paper2 from "paper";
682
+ function resolveFeaturePosition(feature, geometry) {
683
+ const { x, y, width, height } = geometry;
684
+ const left = x - width / 2;
685
+ const top = y - height / 2;
686
+ return {
687
+ x: left + feature.x * width,
688
+ y: top + feature.y * height
689
+ };
685
690
  }
686
691
  function ensurePaper(width, height) {
687
- if (!paper.project) {
688
- paper.setup(new paper.Size(width, height));
692
+ if (!paper2.project) {
693
+ paper2.setup(new paper2.Size(width, height));
689
694
  } else {
690
- paper.view.viewSize = new paper.Size(width, height);
695
+ paper2.view.viewSize = new paper2.Size(width, height);
691
696
  }
692
697
  }
693
698
  function createBaseShape(options) {
694
699
  const { shape, width, height, radius, x, y, pathData } = options;
695
- const center = new paper.Point(x, y);
700
+ const center = new paper2.Point(x, y);
696
701
  if (shape === "rect") {
697
- return new paper.Path.Rectangle({
702
+ return new paper2.Path.Rectangle({
698
703
  point: [x - width / 2, y - height / 2],
699
704
  size: [Math.max(0, width), Math.max(0, height)],
700
705
  radius: Math.max(0, radius)
701
706
  });
702
707
  } else if (shape === "circle") {
703
708
  const r = Math.min(width, height) / 2;
704
- return new paper.Path.Circle({
709
+ return new paper2.Path.Circle({
705
710
  center,
706
711
  radius: Math.max(0, r)
707
712
  });
708
713
  } else if (shape === "ellipse") {
709
- return new paper.Path.Ellipse({
714
+ return new paper2.Path.Ellipse({
710
715
  center,
711
716
  radius: [Math.max(0, width / 2), Math.max(0, height / 2)]
712
717
  });
713
718
  } else if (shape === "custom" && pathData) {
714
- const path = new paper.Path();
719
+ const path = new paper2.Path();
715
720
  path.pathData = pathData;
716
721
  path.position = center;
717
722
  if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
@@ -719,112 +724,131 @@ function createBaseShape(options) {
719
724
  }
720
725
  return path;
721
726
  } else {
722
- return new paper.Path.Rectangle({
727
+ return new paper2.Path.Rectangle({
723
728
  point: [x - width / 2, y - height / 2],
724
729
  size: [Math.max(0, width), Math.max(0, height)]
725
730
  });
726
731
  }
727
732
  }
728
- function getDielineShape(options) {
733
+ function createFeatureItem(feature, center) {
734
+ let item;
735
+ if (feature.shape === "rect") {
736
+ const w = feature.width || 10;
737
+ const h = feature.height || 10;
738
+ const r = feature.radius || 0;
739
+ item = new paper2.Path.Rectangle({
740
+ point: [center.x - w / 2, center.y - h / 2],
741
+ size: [w, h],
742
+ radius: r
743
+ });
744
+ } else {
745
+ const r = feature.radius || 5;
746
+ item = new paper2.Path.Circle({
747
+ center,
748
+ radius: r
749
+ });
750
+ }
751
+ if (feature.rotation) {
752
+ item.rotate(feature.rotation, center);
753
+ }
754
+ return item;
755
+ }
756
+ function getPerimeterShape(options) {
729
757
  let mainShape = createBaseShape(options);
730
- const { holes } = options;
731
- if (holes && holes.length > 0) {
732
- let lugsPath = null;
733
- let cutsPath = null;
734
- holes.forEach((hole) => {
735
- const center = new paper.Point(hole.x, hole.y);
736
- const lug = hole.shape === "square" ? new paper.Path.Rectangle({
737
- point: [
738
- center.x - hole.outerRadius,
739
- center.y - hole.outerRadius
740
- ],
741
- size: [hole.outerRadius * 2, hole.outerRadius * 2]
742
- }) : new paper.Path.Circle({
743
- center,
744
- radius: hole.outerRadius
745
- });
746
- const cut = hole.shape === "square" ? new paper.Path.Rectangle({
747
- point: [
748
- center.x - hole.innerRadius,
749
- center.y - hole.innerRadius
750
- ],
751
- size: [hole.innerRadius * 2, hole.innerRadius * 2]
752
- }) : new paper.Path.Circle({
753
- center,
754
- radius: hole.innerRadius
755
- });
756
- if (!lugsPath) {
757
- lugsPath = lug;
758
+ const { features } = options;
759
+ if (features && features.length > 0) {
760
+ const edgeFeatures = features.filter(
761
+ (f) => !f.placement || f.placement === "edge"
762
+ );
763
+ const adds = [];
764
+ const subtracts = [];
765
+ edgeFeatures.forEach((f) => {
766
+ const pos = resolveFeaturePosition(f, options);
767
+ const center = new paper2.Point(pos.x, pos.y);
768
+ const item = createFeatureItem(f, center);
769
+ if (f.operation === "add") {
770
+ adds.push(item);
758
771
  } else {
772
+ subtracts.push(item);
773
+ }
774
+ });
775
+ if (adds.length > 0) {
776
+ for (const item of adds) {
759
777
  try {
760
- const temp = lugsPath.unite(lug);
761
- lugsPath.remove();
762
- lug.remove();
763
- lugsPath = temp;
778
+ const temp = mainShape.unite(item);
779
+ mainShape.remove();
780
+ item.remove();
781
+ mainShape = temp;
764
782
  } catch (e) {
765
- console.error("Geometry: Failed to unite lug", e);
766
- lug.remove();
783
+ console.error("Geometry: Failed to unite feature", e);
784
+ item.remove();
767
785
  }
768
786
  }
769
- if (!cutsPath) {
770
- cutsPath = cut;
771
- } else {
787
+ }
788
+ if (subtracts.length > 0) {
789
+ for (const item of subtracts) {
772
790
  try {
773
- const temp = cutsPath.unite(cut);
774
- cutsPath.remove();
775
- cut.remove();
776
- cutsPath = temp;
791
+ const temp = mainShape.subtract(item);
792
+ mainShape.remove();
793
+ item.remove();
794
+ mainShape = temp;
777
795
  } catch (e) {
778
- console.error("Geometry: Failed to unite cut", e);
779
- cut.remove();
796
+ console.error("Geometry: Failed to subtract feature", e);
797
+ item.remove();
780
798
  }
781
799
  }
782
- });
783
- if (lugsPath) {
784
- try {
785
- const temp = mainShape.unite(lugsPath);
786
- mainShape.remove();
787
- lugsPath.remove();
788
- mainShape = temp;
789
- } catch (e) {
790
- console.error("Geometry: Failed to unite lugsPath to mainShape", e);
791
- }
792
800
  }
793
- if (cutsPath) {
794
- try {
795
- const temp = mainShape.subtract(cutsPath);
796
- mainShape.remove();
797
- cutsPath.remove();
798
- mainShape = temp;
799
- } catch (e) {
800
- console.error(
801
- "Geometry: Failed to subtract cutsPath from mainShape",
802
- e
803
- );
801
+ }
802
+ return mainShape;
803
+ }
804
+ function applySurfaceFeatures(shape, features, options) {
805
+ const internalFeatures = features.filter((f) => f.placement === "internal");
806
+ if (internalFeatures.length === 0) return shape;
807
+ let result = shape;
808
+ for (const f of internalFeatures) {
809
+ const pos = resolveFeaturePosition(f, options);
810
+ const center = new paper2.Point(pos.x, pos.y);
811
+ const item = createFeatureItem(f, center);
812
+ try {
813
+ if (f.operation === "add") {
814
+ const temp = result.unite(item);
815
+ result.remove();
816
+ item.remove();
817
+ result = temp;
818
+ } else {
819
+ const temp = result.subtract(item);
820
+ result.remove();
821
+ item.remove();
822
+ result = temp;
804
823
  }
824
+ } catch (e) {
825
+ console.error("Geometry: Failed to apply surface feature", e);
826
+ item.remove();
805
827
  }
806
828
  }
807
- return mainShape;
829
+ return result;
808
830
  }
809
831
  function generateDielinePath(options) {
810
832
  const paperWidth = options.canvasWidth || options.width * 2 || 2e3;
811
833
  const paperHeight = options.canvasHeight || options.height * 2 || 2e3;
812
834
  ensurePaper(paperWidth, paperHeight);
813
- paper.project.activeLayer.removeChildren();
814
- const mainShape = getDielineShape(options);
815
- const pathData = mainShape.pathData;
816
- mainShape.remove();
835
+ paper2.project.activeLayer.removeChildren();
836
+ const perimeter = getPerimeterShape(options);
837
+ const finalShape = applySurfaceFeatures(perimeter, options.features, options);
838
+ const pathData = finalShape.pathData;
839
+ finalShape.remove();
817
840
  return pathData;
818
841
  }
819
842
  function generateMaskPath(options) {
820
843
  ensurePaper(options.canvasWidth, options.canvasHeight);
821
- paper.project.activeLayer.removeChildren();
844
+ paper2.project.activeLayer.removeChildren();
822
845
  const { canvasWidth, canvasHeight } = options;
823
- const maskRect = new paper.Path.Rectangle({
846
+ const maskRect = new paper2.Path.Rectangle({
824
847
  point: [0, 0],
825
848
  size: [canvasWidth, canvasHeight]
826
849
  });
827
- const mainShape = getDielineShape(options);
850
+ const perimeter = getPerimeterShape(options);
851
+ const mainShape = applySurfaceFeatures(perimeter, options.features, options);
828
852
  const finalMask = maskRect.subtract(mainShape);
829
853
  maskRect.remove();
830
854
  mainShape.remove();
@@ -832,43 +856,15 @@ function generateMaskPath(options) {
832
856
  finalMask.remove();
833
857
  return pathData;
834
858
  }
835
- function generateBleedZonePath(options, offset) {
836
- const paperWidth = options.canvasWidth || options.width * 2 || 2e3;
837
- const paperHeight = options.canvasHeight || options.height * 2 || 2e3;
859
+ function generateBleedZonePath(originalOptions, offsetOptions, offset) {
860
+ const paperWidth = originalOptions.canvasWidth || originalOptions.width * 2 || 2e3;
861
+ const paperHeight = originalOptions.canvasHeight || originalOptions.height * 2 || 2e3;
838
862
  ensurePaper(paperWidth, paperHeight);
839
- paper.project.activeLayer.removeChildren();
840
- const shapeOriginal = getDielineShape(options);
841
- let shapeOffset;
842
- if (options.shape === "custom") {
843
- const stroker = shapeOriginal.clone();
844
- stroker.strokeColor = new paper.Color("black");
845
- stroker.strokeWidth = Math.abs(offset) * 2;
846
- stroker.strokeJoin = "round";
847
- stroker.strokeCap = "round";
848
- let expanded;
849
- try {
850
- expanded = stroker.expand({ stroke: true, fill: false, insert: false });
851
- } catch (e) {
852
- stroker.remove();
853
- shapeOffset = shapeOriginal.clone();
854
- return shapeOffset.pathData;
855
- }
856
- stroker.remove();
857
- if (offset > 0) {
858
- shapeOffset = shapeOriginal.unite(expanded);
859
- } else {
860
- shapeOffset = shapeOriginal.subtract(expanded);
861
- }
862
- expanded.remove();
863
- } else {
864
- const offsetOptions = {
865
- ...options,
866
- width: Math.max(0, options.width + offset * 2),
867
- height: Math.max(0, options.height + offset * 2),
868
- radius: options.radius === 0 ? 0 : Math.max(0, options.radius + offset)
869
- };
870
- shapeOffset = getDielineShape(offsetOptions);
871
- }
863
+ paper2.project.activeLayer.removeChildren();
864
+ const pOriginal = getPerimeterShape(originalOptions);
865
+ const shapeOriginal = applySurfaceFeatures(pOriginal, originalOptions.features, originalOptions);
866
+ const pOffset = getPerimeterShape(offsetOptions);
867
+ const shapeOffset = applySurfaceFeatures(pOffset, offsetOptions.features, offsetOptions);
872
868
  let bleedZone;
873
869
  if (offset > 0) {
874
870
  bleedZone = shapeOffset.subtract(shapeOriginal);
@@ -883,16 +879,16 @@ function generateBleedZonePath(options, offset) {
883
879
  }
884
880
  function getNearestPointOnDieline(point, options) {
885
881
  ensurePaper(options.width * 2, options.height * 2);
886
- paper.project.activeLayer.removeChildren();
882
+ paper2.project.activeLayer.removeChildren();
887
883
  const shape = createBaseShape(options);
888
- const p = new paper.Point(point.x, point.y);
884
+ const p = new paper2.Point(point.x, point.y);
889
885
  const nearest = shape.getNearestPoint(p);
890
886
  const result = { x: nearest.x, y: nearest.y };
891
887
  shape.remove();
892
888
  return result;
893
889
  }
894
890
  function getPathBounds(pathData) {
895
- const path = new paper.Path();
891
+ const path = new paper2.Path();
896
892
  path.pathData = pathData;
897
893
  const bounds = path.bounds;
898
894
  path.remove();
@@ -911,20 +907,41 @@ var DielineTool = class {
911
907
  this.metadata = {
912
908
  name: "DielineTool"
913
909
  };
914
- this.unit = "mm";
915
- this.shape = "rect";
916
- this.width = 500;
917
- this.height = 500;
918
- this.radius = 0;
919
- this.offset = 0;
920
- this.style = "solid";
921
- this.insideColor = "rgba(0,0,0,0)";
922
- this.outsideColor = "#ffffff";
923
- this.showBleedLines = true;
924
- this.holes = [];
925
- this.padding = 140;
910
+ this.state = {
911
+ unit: "mm",
912
+ shape: "rect",
913
+ width: 500,
914
+ height: 500,
915
+ radius: 0,
916
+ offset: 0,
917
+ padding: 140,
918
+ mainLine: {
919
+ width: 2.7,
920
+ color: "#FF0000",
921
+ dashLength: 5,
922
+ style: "solid"
923
+ },
924
+ offsetLine: {
925
+ width: 2.7,
926
+ color: "#FF0000",
927
+ dashLength: 5,
928
+ style: "solid"
929
+ },
930
+ insideColor: "rgba(0,0,0,0)",
931
+ outsideColor: "#ffffff",
932
+ showBleedLines: true,
933
+ features: []
934
+ };
926
935
  if (options) {
927
- Object.assign(this, options);
936
+ if (options.mainLine) {
937
+ Object.assign(this.state.mainLine, options.mainLine);
938
+ delete options.mainLine;
939
+ }
940
+ if (options.offsetLine) {
941
+ Object.assign(this.state.offsetLine, options.offsetLine);
942
+ delete options.offsetLine;
943
+ }
944
+ Object.assign(this.state, options);
928
945
  }
929
946
  }
930
947
  activate(context) {
@@ -936,38 +953,93 @@ var DielineTool = class {
936
953
  }
937
954
  const configService = context.services.get("ConfigurationService");
938
955
  if (configService) {
939
- this.unit = configService.get("dieline.unit", this.unit);
940
- this.shape = configService.get("dieline.shape", this.shape);
941
- this.width = configService.get("dieline.width", this.width);
942
- this.height = configService.get("dieline.height", this.height);
943
- this.radius = configService.get("dieline.radius", this.radius);
944
- this.padding = configService.get("dieline.padding", this.padding);
945
- this.offset = configService.get("dieline.offset", this.offset);
946
- this.style = configService.get("dieline.style", this.style);
947
- this.insideColor = configService.get(
948
- "dieline.insideColor",
949
- this.insideColor
950
- );
951
- this.outsideColor = configService.get(
952
- "dieline.outsideColor",
953
- this.outsideColor
954
- );
955
- this.showBleedLines = configService.get(
956
- "dieline.showBleedLines",
957
- this.showBleedLines
958
- );
959
- this.holes = configService.get("dieline.holes", this.holes);
960
- this.pathData = configService.get("dieline.pathData", this.pathData);
956
+ const s = this.state;
957
+ s.unit = configService.get("dieline.unit", s.unit);
958
+ s.shape = configService.get("dieline.shape", s.shape);
959
+ s.width = configService.get("dieline.width", s.width);
960
+ s.height = configService.get("dieline.height", s.height);
961
+ s.radius = configService.get("dieline.radius", s.radius);
962
+ s.padding = configService.get("dieline.padding", s.padding);
963
+ s.offset = configService.get("dieline.offset", s.offset);
964
+ s.mainLine.width = configService.get("dieline.strokeWidth", s.mainLine.width);
965
+ s.mainLine.color = configService.get("dieline.strokeColor", s.mainLine.color);
966
+ s.mainLine.dashLength = configService.get("dieline.dashLength", s.mainLine.dashLength);
967
+ s.mainLine.style = configService.get("dieline.style", s.mainLine.style);
968
+ s.offsetLine.width = configService.get("dieline.offsetStrokeWidth", s.offsetLine.width);
969
+ s.offsetLine.color = configService.get("dieline.offsetStrokeColor", s.offsetLine.color);
970
+ s.offsetLine.dashLength = configService.get("dieline.offsetDashLength", s.offsetLine.dashLength);
971
+ s.offsetLine.style = configService.get("dieline.offsetStyle", s.offsetLine.style);
972
+ s.insideColor = configService.get("dieline.insideColor", s.insideColor);
973
+ s.outsideColor = configService.get("dieline.outsideColor", s.outsideColor);
974
+ s.showBleedLines = configService.get("dieline.showBleedLines", s.showBleedLines);
975
+ s.features = configService.get("dieline.features", s.features);
976
+ s.pathData = configService.get("dieline.pathData", s.pathData);
961
977
  configService.onAnyChange((e) => {
962
978
  if (e.key.startsWith("dieline.")) {
963
- const prop = e.key.split(".")[1];
964
- console.log(
965
- `[DielineTool] Config change detected: ${e.key} -> ${e.value}`
966
- );
967
- if (prop && prop in this) {
968
- this[prop] = e.value;
969
- this.updateDieline();
979
+ console.log(`[DielineTool] Config change detected: ${e.key} -> ${e.value}`);
980
+ switch (e.key) {
981
+ case "dieline.unit":
982
+ s.unit = e.value;
983
+ break;
984
+ case "dieline.shape":
985
+ s.shape = e.value;
986
+ break;
987
+ case "dieline.width":
988
+ s.width = e.value;
989
+ break;
990
+ case "dieline.height":
991
+ s.height = e.value;
992
+ break;
993
+ case "dieline.radius":
994
+ s.radius = e.value;
995
+ break;
996
+ case "dieline.padding":
997
+ s.padding = e.value;
998
+ break;
999
+ case "dieline.offset":
1000
+ s.offset = e.value;
1001
+ break;
1002
+ case "dieline.strokeWidth":
1003
+ s.mainLine.width = e.value;
1004
+ break;
1005
+ case "dieline.strokeColor":
1006
+ s.mainLine.color = e.value;
1007
+ break;
1008
+ case "dieline.dashLength":
1009
+ s.mainLine.dashLength = e.value;
1010
+ break;
1011
+ case "dieline.style":
1012
+ s.mainLine.style = e.value;
1013
+ break;
1014
+ case "dieline.offsetStrokeWidth":
1015
+ s.offsetLine.width = e.value;
1016
+ break;
1017
+ case "dieline.offsetStrokeColor":
1018
+ s.offsetLine.color = e.value;
1019
+ break;
1020
+ case "dieline.offsetDashLength":
1021
+ s.offsetLine.dashLength = e.value;
1022
+ break;
1023
+ case "dieline.offsetStyle":
1024
+ s.offsetLine.style = e.value;
1025
+ break;
1026
+ case "dieline.insideColor":
1027
+ s.insideColor = e.value;
1028
+ break;
1029
+ case "dieline.outsideColor":
1030
+ s.outsideColor = e.value;
1031
+ break;
1032
+ case "dieline.showBleedLines":
1033
+ s.showBleedLines = e.value;
1034
+ break;
1035
+ case "dieline.features":
1036
+ s.features = e.value;
1037
+ break;
1038
+ case "dieline.pathData":
1039
+ s.pathData = e.value;
1040
+ break;
970
1041
  }
1042
+ this.updateDieline();
971
1043
  }
972
1044
  });
973
1045
  }
@@ -980,6 +1052,7 @@ var DielineTool = class {
980
1052
  this.context = void 0;
981
1053
  }
982
1054
  contribute() {
1055
+ const s = this.state;
983
1056
  return {
984
1057
  [ContributionPointIds2.CONFIGURATIONS]: [
985
1058
  {
@@ -987,14 +1060,14 @@ var DielineTool = class {
987
1060
  type: "select",
988
1061
  label: "Unit",
989
1062
  options: ["px", "mm", "cm", "in"],
990
- default: this.unit
1063
+ default: s.unit
991
1064
  },
992
1065
  {
993
1066
  id: "dieline.shape",
994
1067
  type: "select",
995
1068
  label: "Shape",
996
1069
  options: ["rect", "circle", "ellipse", "custom"],
997
- default: this.shape
1070
+ default: s.shape
998
1071
  },
999
1072
  {
1000
1073
  id: "dieline.width",
@@ -1002,7 +1075,7 @@ var DielineTool = class {
1002
1075
  label: "Width",
1003
1076
  min: 10,
1004
1077
  max: 2e3,
1005
- default: this.width
1078
+ default: s.width
1006
1079
  },
1007
1080
  {
1008
1081
  id: "dieline.height",
@@ -1010,7 +1083,7 @@ var DielineTool = class {
1010
1083
  label: "Height",
1011
1084
  min: 10,
1012
1085
  max: 2e3,
1013
- default: this.height
1086
+ default: s.height
1014
1087
  },
1015
1088
  {
1016
1089
  id: "dieline.radius",
@@ -1018,20 +1091,14 @@ var DielineTool = class {
1018
1091
  label: "Corner Radius",
1019
1092
  min: 0,
1020
1093
  max: 500,
1021
- default: this.radius
1022
- },
1023
- {
1024
- id: "dieline.position",
1025
- type: "json",
1026
- label: "Position (Normalized)",
1027
- default: this.radius
1094
+ default: s.radius
1028
1095
  },
1029
1096
  {
1030
1097
  id: "dieline.padding",
1031
1098
  type: "select",
1032
1099
  label: "View Padding",
1033
1100
  options: [0, 10, 20, 40, 60, 100, "2%", "5%", "10%", "15%", "20%"],
1034
- default: this.padding
1101
+ default: s.padding
1035
1102
  },
1036
1103
  {
1037
1104
  id: "dieline.offset",
@@ -1039,38 +1106,91 @@ var DielineTool = class {
1039
1106
  label: "Bleed Offset",
1040
1107
  min: -100,
1041
1108
  max: 100,
1042
- default: this.offset
1109
+ default: s.offset
1043
1110
  },
1044
1111
  {
1045
1112
  id: "dieline.showBleedLines",
1046
1113
  type: "boolean",
1047
1114
  label: "Show Bleed Lines",
1048
- default: this.showBleedLines
1115
+ default: s.showBleedLines
1116
+ },
1117
+ {
1118
+ id: "dieline.strokeWidth",
1119
+ type: "number",
1120
+ label: "Line Width",
1121
+ min: 0.1,
1122
+ max: 10,
1123
+ step: 0.1,
1124
+ default: s.mainLine.width
1125
+ },
1126
+ {
1127
+ id: "dieline.strokeColor",
1128
+ type: "color",
1129
+ label: "Line Color",
1130
+ default: s.mainLine.color
1131
+ },
1132
+ {
1133
+ id: "dieline.dashLength",
1134
+ type: "number",
1135
+ label: "Dash Length",
1136
+ min: 1,
1137
+ max: 50,
1138
+ default: s.mainLine.dashLength
1049
1139
  },
1050
1140
  {
1051
1141
  id: "dieline.style",
1052
1142
  type: "select",
1053
1143
  label: "Line Style",
1054
- options: ["solid", "dashed"],
1055
- default: this.style
1144
+ options: ["solid", "dashed", "hidden"],
1145
+ default: s.mainLine.style
1146
+ },
1147
+ {
1148
+ id: "dieline.offsetStrokeWidth",
1149
+ type: "number",
1150
+ label: "Offset Line Width",
1151
+ min: 0.1,
1152
+ max: 10,
1153
+ step: 0.1,
1154
+ default: s.offsetLine.width
1155
+ },
1156
+ {
1157
+ id: "dieline.offsetStrokeColor",
1158
+ type: "color",
1159
+ label: "Offset Line Color",
1160
+ default: s.offsetLine.color
1161
+ },
1162
+ {
1163
+ id: "dieline.offsetDashLength",
1164
+ type: "number",
1165
+ label: "Offset Dash Length",
1166
+ min: 1,
1167
+ max: 50,
1168
+ default: s.offsetLine.dashLength
1169
+ },
1170
+ {
1171
+ id: "dieline.offsetStyle",
1172
+ type: "select",
1173
+ label: "Offset Line Style",
1174
+ options: ["solid", "dashed", "hidden"],
1175
+ default: s.offsetLine.style
1056
1176
  },
1057
1177
  {
1058
1178
  id: "dieline.insideColor",
1059
1179
  type: "color",
1060
1180
  label: "Inside Color",
1061
- default: this.insideColor
1181
+ default: s.insideColor
1062
1182
  },
1063
1183
  {
1064
1184
  id: "dieline.outsideColor",
1065
1185
  type: "color",
1066
1186
  label: "Outside Color",
1067
- default: this.outsideColor
1187
+ default: s.outsideColor
1068
1188
  },
1069
1189
  {
1070
- id: "dieline.holes",
1190
+ id: "dieline.features",
1071
1191
  type: "json",
1072
- label: "Holes",
1073
- default: this.holes
1192
+ label: "Edge Features",
1193
+ default: s.features
1074
1194
  }
1075
1195
  ],
1076
1196
  [ContributionPointIds2.COMMANDS]: [
@@ -1092,24 +1212,18 @@ var DielineTool = class {
1092
1212
  command: "detectEdge",
1093
1213
  title: "Detect Edge from Image",
1094
1214
  handler: async (imageUrl, options) => {
1095
- var _a;
1096
1215
  try {
1097
1216
  const pathData = await ImageTracer.trace(imageUrl, options);
1098
1217
  const bounds = getPathBounds(pathData);
1099
- const currentMax = Math.max(this.width, this.height);
1218
+ const currentMax = Math.max(s.width, s.height);
1100
1219
  const scale = currentMax / Math.max(bounds.width, bounds.height);
1101
1220
  const newWidth = bounds.width * scale;
1102
1221
  const newHeight = bounds.height * scale;
1103
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1104
- "ConfigurationService"
1105
- );
1106
- if (configService) {
1107
- configService.update("dieline.width", newWidth);
1108
- configService.update("dieline.height", newHeight);
1109
- configService.update("dieline.shape", "custom");
1110
- configService.update("dieline.pathData", pathData);
1111
- }
1112
- return pathData;
1222
+ return {
1223
+ pathData,
1224
+ width: newWidth,
1225
+ height: newHeight
1226
+ };
1113
1227
  } catch (e) {
1114
1228
  console.error("Edge detection failed", e);
1115
1229
  throw e;
@@ -1168,15 +1282,15 @@ var DielineTool = class {
1168
1282
  return new Pattern({ source: canvas, repetition: "repeat" });
1169
1283
  }
1170
1284
  resolvePadding(containerWidth, containerHeight) {
1171
- if (typeof this.padding === "number") {
1172
- return this.padding;
1285
+ if (typeof this.state.padding === "number") {
1286
+ return this.state.padding;
1173
1287
  }
1174
- if (typeof this.padding === "string") {
1175
- if (this.padding.endsWith("%")) {
1176
- const percent = parseFloat(this.padding) / 100;
1288
+ if (typeof this.state.padding === "string") {
1289
+ if (this.state.padding.endsWith("%")) {
1290
+ const percent = parseFloat(this.state.padding) / 100;
1177
1291
  return Math.min(containerWidth, containerHeight) * percent;
1178
1292
  }
1179
- return parseFloat(this.padding) || 0;
1293
+ return parseFloat(this.state.padding) || 0;
1180
1294
  }
1181
1295
  return 0;
1182
1296
  }
@@ -1189,14 +1303,14 @@ var DielineTool = class {
1189
1303
  shape,
1190
1304
  radius,
1191
1305
  offset,
1192
- style,
1306
+ mainLine,
1307
+ offsetLine,
1193
1308
  insideColor,
1194
1309
  outsideColor,
1195
- position,
1196
1310
  showBleedLines,
1197
- holes
1198
- } = this;
1199
- let { width, height } = this;
1311
+ features
1312
+ } = this.state;
1313
+ let { width, height } = this.state;
1200
1314
  const canvasW = this.canvasService.canvas.width || 800;
1201
1315
  const canvasH = this.canvasService.canvas.height || 600;
1202
1316
  const paddingPx = this.resolvePadding(canvasW, canvasH);
@@ -1213,37 +1327,18 @@ var DielineTool = class {
1213
1327
  const visualRadius = radius * scale;
1214
1328
  const visualOffset = offset * scale;
1215
1329
  layer.remove(...layer.getObjects());
1216
- const geometryForHoles = {
1217
- x: cx,
1218
- y: cy,
1219
- width: visualWidth,
1220
- height: visualHeight
1221
- // Pass scale/unit context if needed by resolveHolePosition (though currently unused there)
1222
- };
1223
- const absoluteHoles = (holes || []).map((h) => {
1224
- const unitScale = Coordinate.convertUnit(1, "mm", unit);
1225
- const offsetScale = unitScale * scale;
1226
- const hWithPixelOffsets = {
1227
- ...h,
1228
- offsetX: (h.offsetX || 0) * offsetScale,
1229
- offsetY: (h.offsetY || 0) * offsetScale
1230
- };
1231
- const pos = resolveHolePosition(hWithPixelOffsets, geometryForHoles, {
1232
- width: canvasW,
1233
- height: canvasH
1234
- });
1330
+ const absoluteFeatures = (features || []).map((f) => {
1331
+ const featureScale = scale;
1235
1332
  return {
1236
- ...h,
1237
- x: pos.x,
1238
- y: pos.y,
1239
- // Scale hole radii: mm -> current unit -> pixels
1240
- innerRadius: h.innerRadius * offsetScale,
1241
- outerRadius: h.outerRadius * offsetScale,
1242
- // Store scaled offsets in the result for consistency, though pos is already resolved
1243
- offsetX: hWithPixelOffsets.offsetX,
1244
- offsetY: hWithPixelOffsets.offsetY
1333
+ ...f,
1334
+ x: f.x,
1335
+ y: f.y,
1336
+ width: (f.width || 0) * featureScale,
1337
+ height: (f.height || 0) * featureScale,
1338
+ radius: (f.radius || 0) * featureScale
1245
1339
  };
1246
1340
  });
1341
+ const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
1247
1342
  const cutW = Math.max(0, visualWidth + visualOffset * 2);
1248
1343
  const cutH = Math.max(0, visualHeight + visualOffset * 2);
1249
1344
  const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
@@ -1256,8 +1351,8 @@ var DielineTool = class {
1256
1351
  radius: cutR,
1257
1352
  x: cx,
1258
1353
  y: cy,
1259
- holes: absoluteHoles,
1260
- pathData: this.pathData
1354
+ features: cutFeatures,
1355
+ pathData: this.state.pathData
1261
1356
  });
1262
1357
  const mask = new Path(maskPathData, {
1263
1358
  fill: outsideColor,
@@ -1278,8 +1373,9 @@ var DielineTool = class {
1278
1373
  radius: cutR,
1279
1374
  x: cx,
1280
1375
  y: cy,
1281
- holes: absoluteHoles,
1282
- pathData: this.pathData,
1376
+ features: cutFeatures,
1377
+ // Use same features as mask for consistency
1378
+ pathData: this.state.pathData,
1283
1379
  canvasWidth: canvasW,
1284
1380
  canvasHeight: canvasH
1285
1381
  });
@@ -1303,15 +1399,27 @@ var DielineTool = class {
1303
1399
  radius: visualRadius,
1304
1400
  x: cx,
1305
1401
  y: cy,
1306
- holes: absoluteHoles,
1307
- pathData: this.pathData,
1402
+ features: cutFeatures,
1403
+ pathData: this.state.pathData,
1404
+ canvasWidth: canvasW,
1405
+ canvasHeight: canvasH
1406
+ },
1407
+ {
1408
+ shape,
1409
+ width: cutW,
1410
+ height: cutH,
1411
+ radius: cutR,
1412
+ x: cx,
1413
+ y: cy,
1414
+ features: cutFeatures,
1415
+ pathData: this.state.pathData,
1308
1416
  canvasWidth: canvasW,
1309
1417
  canvasHeight: canvasH
1310
1418
  },
1311
1419
  visualOffset
1312
1420
  );
1313
1421
  if (showBleedLines !== false) {
1314
- const pattern = this.createHatchPattern("red");
1422
+ const pattern = this.createHatchPattern(mainLine.color);
1315
1423
  if (pattern) {
1316
1424
  const bleedObj = new Path(bleedPathData, {
1317
1425
  fill: pattern,
@@ -1332,18 +1440,16 @@ var DielineTool = class {
1332
1440
  radius: cutR,
1333
1441
  x: cx,
1334
1442
  y: cy,
1335
- holes: absoluteHoles,
1336
- pathData: this.pathData,
1443
+ features: cutFeatures,
1444
+ pathData: this.state.pathData,
1337
1445
  canvasWidth: canvasW,
1338
1446
  canvasHeight: canvasH
1339
1447
  });
1340
1448
  const offsetBorderObj = new Path(offsetPathData, {
1341
1449
  fill: null,
1342
- stroke: "#666",
1343
- // Grey
1344
- strokeWidth: 1,
1345
- strokeDashArray: [4, 4],
1346
- // Dashed
1450
+ stroke: offsetLine.style === "hidden" ? null : offsetLine.color,
1451
+ strokeWidth: offsetLine.width,
1452
+ strokeDashArray: offsetLine.style === "dashed" ? [offsetLine.dashLength, offsetLine.dashLength] : void 0,
1347
1453
  selectable: false,
1348
1454
  evented: false,
1349
1455
  originX: "left",
@@ -1358,16 +1464,16 @@ var DielineTool = class {
1358
1464
  radius: visualRadius,
1359
1465
  x: cx,
1360
1466
  y: cy,
1361
- holes: absoluteHoles,
1362
- pathData: this.pathData,
1467
+ features: absoluteFeatures,
1468
+ pathData: this.state.pathData,
1363
1469
  canvasWidth: canvasW,
1364
1470
  canvasHeight: canvasH
1365
1471
  });
1366
1472
  const borderObj = new Path(borderPathData, {
1367
1473
  fill: "transparent",
1368
- stroke: "red",
1369
- strokeWidth: 1,
1370
- strokeDashArray: style === "dashed" ? [5, 5] : void 0,
1474
+ stroke: mainLine.style === "hidden" ? null : mainLine.color,
1475
+ strokeWidth: mainLine.width,
1476
+ strokeDashArray: mainLine.style === "dashed" ? [mainLine.dashLength, mainLine.dashLength] : void 0,
1371
1477
  selectable: false,
1372
1478
  evented: false,
1373
1479
  originX: "left",
@@ -1399,7 +1505,7 @@ var DielineTool = class {
1399
1505
  }
1400
1506
  getGeometry() {
1401
1507
  if (!this.canvasService) return null;
1402
- const { unit, shape, width, height, radius, position, offset } = this;
1508
+ const { unit, shape, width, height, radius, offset, mainLine, pathData } = this.state;
1403
1509
  const canvasW = this.canvasService.canvas.width || 800;
1404
1510
  const canvasH = this.canvasService.canvas.height || 600;
1405
1511
  const paddingPx = this.resolvePadding(canvasW, canvasH);
@@ -1422,16 +1528,17 @@ var DielineTool = class {
1422
1528
  height: visualHeight,
1423
1529
  radius: radius * scale,
1424
1530
  offset: offset * scale,
1425
- // Pass scale to help other tools (like HoleTool) convert units
1531
+ // Pass scale to help other tools (like FeatureTool) convert units
1426
1532
  scale,
1427
- pathData: this.pathData
1533
+ strokeWidth: mainLine.width,
1534
+ pathData
1428
1535
  };
1429
1536
  }
1430
1537
  async exportCutImage() {
1431
1538
  if (!this.canvasService) return null;
1432
1539
  const userLayer = this.canvasService.getLayer("user");
1433
1540
  if (!userLayer) return null;
1434
- const { shape, width, height, radius, position, holes } = this;
1541
+ const { shape, width, height, radius, features, unit, pathData } = this.state;
1435
1542
  const canvasW = this.canvasService.canvas.width || 800;
1436
1543
  const canvasH = this.canvasService.canvas.height || 600;
1437
1544
  const paddingPx = this.resolvePadding(canvasW, canvasH);
@@ -1446,55 +1553,43 @@ var DielineTool = class {
1446
1553
  const visualWidth = layout.width;
1447
1554
  const visualHeight = layout.height;
1448
1555
  const visualRadius = radius * scale;
1449
- const absoluteHoles = (holes || []).map((h) => {
1450
- const unit = this.unit || "mm";
1451
- const unitScale = Coordinate.convertUnit(1, "mm", unit);
1452
- const pos = resolveHolePosition(
1453
- {
1454
- ...h,
1455
- offsetX: (h.offsetX || 0) * unitScale * scale,
1456
- offsetY: (h.offsetY || 0) * unitScale * scale
1457
- },
1458
- { x: cx, y: cy, width: visualWidth, height: visualHeight },
1459
- { width: canvasW, height: canvasH }
1460
- );
1556
+ const absoluteFeatures = (features || []).map((f) => {
1557
+ const featureScale = scale;
1461
1558
  return {
1462
- ...h,
1463
- x: pos.x,
1464
- y: pos.y,
1465
- innerRadius: h.innerRadius * unitScale * scale,
1466
- outerRadius: h.outerRadius * unitScale * scale,
1467
- offsetX: (h.offsetX || 0) * unitScale * scale,
1468
- offsetY: (h.offsetY || 0) * unitScale * scale
1559
+ ...f,
1560
+ x: f.x,
1561
+ y: f.y,
1562
+ width: (f.width || 0) * featureScale,
1563
+ height: (f.height || 0) * featureScale,
1564
+ radius: (f.radius || 0) * featureScale
1469
1565
  };
1470
1566
  });
1471
- const pathData = generateDielinePath({
1567
+ const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
1568
+ const generatedPathData = generateDielinePath({
1472
1569
  shape,
1473
1570
  width: visualWidth,
1474
1571
  height: visualHeight,
1475
1572
  radius: visualRadius,
1476
1573
  x: cx,
1477
1574
  y: cy,
1478
- holes: absoluteHoles,
1479
- pathData: this.pathData,
1575
+ features: cutFeatures,
1576
+ pathData,
1480
1577
  canvasWidth: canvasW,
1481
1578
  canvasHeight: canvasH
1482
1579
  });
1483
1580
  const clonedLayer = await userLayer.clone();
1484
- const clipPath = new Path(pathData, {
1581
+ const clipPath = new Path(generatedPathData, {
1485
1582
  originX: "left",
1486
1583
  originY: "top",
1487
1584
  left: 0,
1488
1585
  top: 0,
1489
1586
  absolutePositioned: true
1490
- // Important for groups
1491
1587
  });
1492
1588
  clonedLayer.clipPath = clipPath;
1493
1589
  const bounds = clipPath.getBoundingRect();
1494
1590
  const dataUrl = clonedLayer.toDataURL({
1495
1591
  format: "png",
1496
1592
  multiplier: 2,
1497
- // Better quality
1498
1593
  left: bounds.left,
1499
1594
  top: bounds.top,
1500
1595
  width: bounds.width,
@@ -1665,25 +1760,28 @@ var FilmTool = class {
1665
1760
  }
1666
1761
  };
1667
1762
 
1668
- // src/hole.ts
1763
+ // src/feature.ts
1669
1764
  import {
1670
1765
  ContributionPointIds as ContributionPointIds4
1671
1766
  } from "@pooder/core";
1672
1767
  import { Circle, Group, Point, Rect as Rect2 } from "fabric";
1673
- var HoleTool = class {
1768
+ var FeatureTool = class {
1674
1769
  constructor(options) {
1675
- this.id = "pooder.kit.hole";
1770
+ this.id = "pooder.kit.feature";
1676
1771
  this.metadata = {
1677
- name: "HoleTool"
1772
+ name: "FeatureTool"
1678
1773
  };
1679
- this.holes = [];
1680
- this.constraintTarget = "bleed";
1774
+ this.features = [];
1681
1775
  this.isUpdatingConfig = false;
1776
+ this.isToolActive = false;
1682
1777
  this.handleMoving = null;
1683
1778
  this.handleModified = null;
1684
1779
  this.handleDielineChange = null;
1685
- // Cache geometry to enforce constraints during drag
1686
1780
  this.currentGeometry = null;
1781
+ this.onToolActivated = (event) => {
1782
+ this.isToolActive = event.id === this.id;
1783
+ this.updateVisibility();
1784
+ };
1687
1785
  if (options) {
1688
1786
  Object.assign(this, options);
1689
1787
  }
@@ -1692,135 +1790,82 @@ var HoleTool = class {
1692
1790
  this.context = context;
1693
1791
  this.canvasService = context.services.get("CanvasService");
1694
1792
  if (!this.canvasService) {
1695
- console.warn("CanvasService not found for HoleTool");
1793
+ console.warn("CanvasService not found for FeatureTool");
1696
1794
  return;
1697
1795
  }
1698
1796
  const configService = context.services.get(
1699
1797
  "ConfigurationService"
1700
1798
  );
1701
1799
  if (configService) {
1702
- this.constraintTarget = configService.get(
1703
- "hole.constraintTarget",
1704
- this.constraintTarget
1705
- );
1706
- this.holes = configService.get("dieline.holes", []);
1800
+ this.features = configService.get("dieline.features", []);
1707
1801
  configService.onAnyChange((e) => {
1708
1802
  if (this.isUpdatingConfig) return;
1709
- if (e.key === "hole.constraintTarget") {
1710
- this.constraintTarget = e.value;
1711
- this.enforceConstraints();
1712
- }
1713
- if (e.key === "dieline.holes") {
1714
- this.holes = e.value || [];
1803
+ if (e.key === "dieline.features") {
1804
+ this.features = e.value || [];
1715
1805
  this.redraw();
1716
1806
  }
1717
1807
  });
1718
1808
  }
1809
+ context.eventBus.on("tool:activated", this.onToolActivated);
1719
1810
  this.setup();
1720
1811
  }
1721
1812
  deactivate(context) {
1813
+ context.eventBus.off("tool:activated", this.onToolActivated);
1722
1814
  this.teardown();
1723
1815
  this.canvasService = void 0;
1724
1816
  this.context = void 0;
1725
1817
  }
1818
+ updateVisibility() {
1819
+ if (!this.canvasService) return;
1820
+ const canvas = this.canvasService.canvas;
1821
+ const markers = canvas.getObjects().filter((obj) => {
1822
+ var _a;
1823
+ return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
1824
+ });
1825
+ markers.forEach((marker) => {
1826
+ marker.set({
1827
+ visible: this.isToolActive,
1828
+ // Or just selectable: false if we want them visible but locked
1829
+ selectable: this.isToolActive,
1830
+ evented: this.isToolActive
1831
+ });
1832
+ });
1833
+ canvas.requestRenderAll();
1834
+ }
1726
1835
  contribute() {
1727
1836
  return {
1728
- [ContributionPointIds4.CONFIGURATIONS]: [
1729
- {
1730
- id: "hole.constraintTarget",
1731
- type: "select",
1732
- label: "Constraint Target",
1733
- options: ["original", "bleed"],
1734
- default: "bleed"
1735
- }
1736
- ],
1737
1837
  [ContributionPointIds4.COMMANDS]: [
1738
1838
  {
1739
- command: "resetHoles",
1740
- title: "Reset Holes",
1741
- handler: () => {
1742
- var _a;
1743
- if (!this.canvasService) return false;
1744
- let defaultPos = { x: this.canvasService.canvas.width / 2, y: 50 };
1745
- if (this.currentGeometry) {
1746
- const g = this.currentGeometry;
1747
- const topCenter = { x: g.x, y: g.y - g.height / 2 };
1748
- defaultPos = getNearestPointOnDieline(topCenter, {
1749
- ...g,
1750
- holes: []
1751
- });
1752
- }
1753
- const { width, height } = this.canvasService.canvas;
1754
- const normalizedHole = Coordinate.normalizePoint(defaultPos, {
1755
- width: width || 800,
1756
- height: height || 600
1757
- });
1758
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1759
- "ConfigurationService"
1760
- );
1761
- if (configService) {
1762
- configService.update("dieline.holes", [
1763
- {
1764
- x: normalizedHole.x,
1765
- y: normalizedHole.y,
1766
- innerRadius: 15,
1767
- outerRadius: 25
1768
- }
1769
- ]);
1770
- }
1771
- return true;
1839
+ command: "addFeature",
1840
+ title: "Add Edge Feature",
1841
+ handler: (type = "subtract") => {
1842
+ return this.addFeature(type);
1772
1843
  }
1773
1844
  },
1774
1845
  {
1775
1846
  command: "addHole",
1776
1847
  title: "Add Hole",
1777
- handler: (x, y) => {
1778
- var _a, _b, _c, _d;
1779
- if (!this.canvasService) return false;
1780
- let normalizedX = 0.5;
1781
- let normalizedY = 0.5;
1782
- if (this.currentGeometry) {
1783
- const { x: gx, y: gy, width: gw, height: gh } = this.currentGeometry;
1784
- const left = gx - gw / 2;
1785
- const top = gy - gh / 2;
1786
- normalizedX = gw > 0 ? (x - left) / gw : 0.5;
1787
- normalizedY = gh > 0 ? (y - top) / gh : 0.5;
1788
- } else {
1789
- const { width, height } = this.canvasService.canvas;
1790
- normalizedX = Coordinate.toNormalized(x, width || 800);
1791
- normalizedY = Coordinate.toNormalized(y, height || 600);
1792
- }
1793
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1794
- "ConfigurationService"
1795
- );
1796
- if (configService) {
1797
- const currentHoles = configService.get("dieline.holes", []);
1798
- const lastHole = currentHoles[currentHoles.length - 1];
1799
- const innerRadius = (_b = lastHole == null ? void 0 : lastHole.innerRadius) != null ? _b : 15;
1800
- const outerRadius = (_c = lastHole == null ? void 0 : lastHole.outerRadius) != null ? _c : 25;
1801
- const shape = (_d = lastHole == null ? void 0 : lastHole.shape) != null ? _d : "circle";
1802
- const newHole = {
1803
- x: normalizedX,
1804
- y: normalizedY,
1805
- shape,
1806
- innerRadius,
1807
- outerRadius
1808
- };
1809
- configService.update("dieline.holes", [...currentHoles, newHole]);
1810
- }
1811
- return true;
1848
+ handler: () => {
1849
+ return this.addFeature("subtract");
1850
+ }
1851
+ },
1852
+ {
1853
+ command: "addDoubleLayerHole",
1854
+ title: "Add Double Layer Hole",
1855
+ handler: () => {
1856
+ return this.addDoubleLayerHole();
1812
1857
  }
1813
1858
  },
1814
1859
  {
1815
- command: "clearHoles",
1816
- title: "Clear Holes",
1860
+ command: "clearFeatures",
1861
+ title: "Clear Features",
1817
1862
  handler: () => {
1818
1863
  var _a;
1819
1864
  const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1820
1865
  "ConfigurationService"
1821
1866
  );
1822
1867
  if (configService) {
1823
- configService.update("dieline.holes", []);
1868
+ configService.update("dieline.features", []);
1824
1869
  }
1825
1870
  return true;
1826
1871
  }
@@ -1828,6 +1873,82 @@ var HoleTool = class {
1828
1873
  ]
1829
1874
  };
1830
1875
  }
1876
+ addFeature(type) {
1877
+ var _a;
1878
+ if (!this.canvasService) return false;
1879
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1880
+ "ConfigurationService"
1881
+ );
1882
+ const unit = (configService == null ? void 0 : configService.get("dieline.unit", "mm")) || "mm";
1883
+ const defaultSize = Coordinate.convertUnit(10, "mm", unit);
1884
+ const newFeature = {
1885
+ id: Date.now().toString(),
1886
+ operation: type,
1887
+ placement: "edge",
1888
+ shape: "rect",
1889
+ x: 0.5,
1890
+ y: 0,
1891
+ // Top edge
1892
+ width: defaultSize,
1893
+ height: defaultSize,
1894
+ rotation: 0
1895
+ };
1896
+ if (configService) {
1897
+ const current = configService.get(
1898
+ "dieline.features",
1899
+ []
1900
+ );
1901
+ configService.update("dieline.features", [...current, newFeature]);
1902
+ }
1903
+ return true;
1904
+ }
1905
+ addDoubleLayerHole() {
1906
+ var _a;
1907
+ if (!this.canvasService) return false;
1908
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1909
+ "ConfigurationService"
1910
+ );
1911
+ const unit = (configService == null ? void 0 : configService.get("dieline.unit", "mm")) || "mm";
1912
+ const lugRadius = Coordinate.convertUnit(20, "mm", unit);
1913
+ const holeRadius = Coordinate.convertUnit(15, "mm", unit);
1914
+ const groupId = Date.now().toString();
1915
+ const timestamp = Date.now();
1916
+ const lug = {
1917
+ id: `${timestamp}-lug`,
1918
+ groupId,
1919
+ operation: "add",
1920
+ shape: "circle",
1921
+ placement: "edge",
1922
+ x: 0.5,
1923
+ y: 0,
1924
+ radius: lugRadius,
1925
+ // 20mm
1926
+ rotation: 0
1927
+ };
1928
+ const hole = {
1929
+ id: `${timestamp}-hole`,
1930
+ groupId,
1931
+ operation: "subtract",
1932
+ shape: "circle",
1933
+ placement: "edge",
1934
+ x: 0.5,
1935
+ y: 0,
1936
+ radius: holeRadius,
1937
+ // 15mm
1938
+ rotation: 0
1939
+ };
1940
+ if (configService) {
1941
+ const current = configService.get(
1942
+ "dieline.features",
1943
+ []
1944
+ );
1945
+ configService.update("dieline.features", [...current, lug, hole]);
1946
+ }
1947
+ return true;
1948
+ }
1949
+ getGeometryForFeature(geometry, feature) {
1950
+ return geometry;
1951
+ }
1831
1952
  setup() {
1832
1953
  if (!this.canvasService || !this.context) return;
1833
1954
  const canvas = this.canvasService.canvas;
@@ -1835,10 +1956,7 @@ var HoleTool = class {
1835
1956
  this.handleDielineChange = (geometry) => {
1836
1957
  this.currentGeometry = geometry;
1837
1958
  this.redraw();
1838
- const changed = this.enforceConstraints();
1839
- if (changed) {
1840
- this.syncHolesToDieline();
1841
- }
1959
+ this.enforceConstraints();
1842
1960
  };
1843
1961
  this.context.eventBus.on(
1844
1962
  "dieline:geometry:change",
@@ -1848,69 +1966,101 @@ var HoleTool = class {
1848
1966
  const commandService = this.context.services.get("CommandService");
1849
1967
  if (commandService) {
1850
1968
  try {
1851
- const geometry = commandService.executeCommand("getGeometry");
1852
- if (geometry) {
1853
- Promise.resolve(geometry).then((g) => {
1969
+ Promise.resolve(commandService.executeCommand("getGeometry")).then(
1970
+ (g) => {
1854
1971
  if (g) {
1855
1972
  this.currentGeometry = g;
1856
- this.enforceConstraints();
1857
- this.initializeHoles();
1973
+ this.redraw();
1858
1974
  }
1859
- });
1860
- }
1975
+ }
1976
+ );
1861
1977
  } catch (e) {
1862
1978
  }
1863
1979
  }
1864
1980
  if (!this.handleMoving) {
1865
1981
  this.handleMoving = (e) => {
1866
- var _a, _b, _c, _d, _e;
1982
+ var _a, _b, _c, _d;
1867
1983
  const target = e.target;
1868
- if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "hole-marker") return;
1984
+ if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return;
1869
1985
  if (!this.currentGeometry) return;
1870
- const index = (_c = (_b = target.data) == null ? void 0 : _b.index) != null ? _c : -1;
1871
- const holeData = this.holes[index];
1872
- const effectiveOffset = this.constraintTarget === "original" ? 0 : this.currentGeometry.offset;
1873
- const constraintGeometry = {
1874
- ...this.currentGeometry,
1875
- width: Math.max(0, this.currentGeometry.width + effectiveOffset * 2),
1876
- height: Math.max(
1877
- 0,
1878
- this.currentGeometry.height + effectiveOffset * 2
1879
- ),
1880
- radius: Math.max(0, this.currentGeometry.radius + effectiveOffset)
1881
- };
1882
- const p = new Point(target.left, target.top);
1883
- const newPos = this.calculateConstrainedPosition(
1884
- p,
1885
- constraintGeometry,
1886
- (_d = holeData == null ? void 0 : holeData.innerRadius) != null ? _d : 15,
1887
- (_e = holeData == null ? void 0 : holeData.outerRadius) != null ? _e : 25
1986
+ let feature;
1987
+ if ((_b = target.data) == null ? void 0 : _b.isGroup) {
1988
+ const indices = (_c = target.data) == null ? void 0 : _c.indices;
1989
+ if (indices && indices.length > 0) {
1990
+ feature = this.features[indices[0]];
1991
+ }
1992
+ } else {
1993
+ const index = (_d = target.data) == null ? void 0 : _d.index;
1994
+ if (index !== void 0) {
1995
+ feature = this.features[index];
1996
+ }
1997
+ }
1998
+ const geometry = this.getGeometryForFeature(
1999
+ this.currentGeometry,
2000
+ feature
1888
2001
  );
2002
+ const p = new Point(target.left, target.top);
2003
+ const markerStrokeWidth = (target.strokeWidth || 2) * (target.scaleX || 1);
2004
+ const minDim = Math.min(target.getScaledWidth(), target.getScaledHeight());
2005
+ const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
2006
+ const snapped = this.constrainPosition(p, geometry, limit, feature);
1889
2007
  target.set({
1890
- left: newPos.x,
1891
- top: newPos.y
2008
+ left: snapped.x,
2009
+ top: snapped.y
1892
2010
  });
1893
2011
  };
1894
2012
  canvas.on("object:moving", this.handleMoving);
1895
2013
  }
1896
2014
  if (!this.handleModified) {
1897
2015
  this.handleModified = (e) => {
1898
- var _a;
2016
+ var _a, _b, _c, _d;
1899
2017
  const target = e.target;
1900
- if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "hole-marker") return;
1901
- const changed = this.enforceConstraints();
1902
- if (!changed) {
1903
- this.syncHolesFromCanvas();
2018
+ if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return;
2019
+ if ((_b = target.data) == null ? void 0 : _b.isGroup) {
2020
+ const groupObj = target;
2021
+ const indices = (_c = groupObj.data) == null ? void 0 : _c.indices;
2022
+ if (!indices) return;
2023
+ const groupCenter = new Point(groupObj.left, groupObj.top);
2024
+ const newFeatures = [...this.features];
2025
+ const { x, y } = this.currentGeometry;
2026
+ groupObj.getObjects().forEach((child, i) => {
2027
+ const originalIndex = indices[i];
2028
+ const feature = this.features[originalIndex];
2029
+ const geometry = this.getGeometryForFeature(
2030
+ this.currentGeometry,
2031
+ feature
2032
+ );
2033
+ const { width, height } = geometry;
2034
+ const layoutLeft = x - width / 2;
2035
+ const layoutTop = y - height / 2;
2036
+ const absX = groupCenter.x + (child.left || 0);
2037
+ const absY = groupCenter.y + (child.top || 0);
2038
+ const normalizedX = width > 0 ? (absX - layoutLeft) / width : 0.5;
2039
+ const normalizedY = height > 0 ? (absY - layoutTop) / height : 0.5;
2040
+ newFeatures[originalIndex] = {
2041
+ ...newFeatures[originalIndex],
2042
+ x: normalizedX,
2043
+ y: normalizedY
2044
+ };
2045
+ });
2046
+ this.features = newFeatures;
2047
+ const configService = (_d = this.context) == null ? void 0 : _d.services.get(
2048
+ "ConfigurationService"
2049
+ );
2050
+ if (configService) {
2051
+ this.isUpdatingConfig = true;
2052
+ try {
2053
+ configService.update("dieline.features", this.features);
2054
+ } finally {
2055
+ this.isUpdatingConfig = false;
2056
+ }
2057
+ }
2058
+ } else {
2059
+ this.syncFeatureFromCanvas(target);
1904
2060
  }
1905
2061
  };
1906
2062
  canvas.on("object:modified", this.handleModified);
1907
2063
  }
1908
- this.initializeHoles();
1909
- }
1910
- initializeHoles() {
1911
- if (!this.canvasService) return;
1912
- this.redraw();
1913
- this.syncHolesToDieline();
1914
2064
  }
1915
2065
  teardown() {
1916
2066
  if (!this.canvasService) return;
@@ -1932,357 +2082,274 @@ var HoleTool = class {
1932
2082
  }
1933
2083
  const objects = canvas.getObjects().filter((obj) => {
1934
2084
  var _a;
1935
- return ((_a = obj.data) == null ? void 0 : _a.type) === "hole-marker";
2085
+ return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
1936
2086
  });
1937
2087
  objects.forEach((obj) => canvas.remove(obj));
1938
- if (this.context) {
1939
- const commandService = this.context.services.get("CommandService");
1940
- if (commandService) {
1941
- try {
1942
- commandService.executeCommand("setHoles", []);
1943
- } catch (e) {
1944
- }
1945
- }
1946
- }
1947
2088
  this.canvasService.requestRenderAll();
1948
2089
  }
1949
- syncHolesFromCanvas() {
1950
- if (!this.canvasService) return;
1951
- const objects = this.canvasService.canvas.getObjects().filter(
1952
- (obj) => {
1953
- var _a;
1954
- return ((_a = obj.data) == null ? void 0 : _a.type) === "hole-marker" || obj.name === "hole-marker";
1955
- }
1956
- );
1957
- if (objects.length === 0 && this.holes.length > 0) {
1958
- console.warn("HoleTool: No markers found on canvas to sync from");
1959
- return;
1960
- }
1961
- objects.sort(
1962
- (a, b) => {
1963
- var _a, _b, _c, _d;
1964
- 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);
1965
- }
1966
- );
1967
- const newHoles = objects.map((obj, i) => {
1968
- var _a, _b, _c, _d;
1969
- const original = this.holes[i];
1970
- const newAbsX = obj.left;
1971
- const newAbsY = obj.top;
1972
- if (isNaN(newAbsX) || isNaN(newAbsY)) {
1973
- console.error("HoleTool: Invalid marker coordinates", {
1974
- newAbsX,
1975
- newAbsY
1976
- });
1977
- return original;
1978
- }
1979
- const scale = ((_a = this.currentGeometry) == null ? void 0 : _a.scale) || 1;
1980
- const unit = ((_b = this.currentGeometry) == null ? void 0 : _b.unit) || "mm";
1981
- const unitScale = Coordinate.convertUnit(1, "mm", unit);
1982
- if (original && original.anchor && this.currentGeometry) {
1983
- const { x, y, width, height } = this.currentGeometry;
1984
- let bx = x;
1985
- let by = y;
1986
- const left = x - width / 2;
1987
- const right = x + width / 2;
1988
- const top = y - height / 2;
1989
- const bottom = y + height / 2;
1990
- switch (original.anchor) {
1991
- case "top-left":
1992
- bx = left;
1993
- by = top;
1994
- break;
1995
- case "top-center":
1996
- bx = x;
1997
- by = top;
1998
- break;
1999
- case "top-right":
2000
- bx = right;
2001
- by = top;
2002
- break;
2003
- case "center-left":
2004
- bx = left;
2005
- by = y;
2006
- break;
2007
- case "center":
2008
- bx = x;
2009
- by = y;
2010
- break;
2011
- case "center-right":
2012
- bx = right;
2013
- by = y;
2014
- break;
2015
- case "bottom-left":
2016
- bx = left;
2017
- by = bottom;
2018
- break;
2019
- case "bottom-center":
2020
- bx = x;
2021
- by = bottom;
2022
- break;
2023
- case "bottom-right":
2024
- bx = right;
2025
- by = bottom;
2026
- break;
2027
- }
2028
- return {
2029
- ...original,
2030
- // Denormalize offset back to physical units (mm)
2031
- offsetX: (newAbsX - bx) / scale / unitScale,
2032
- offsetY: (newAbsY - by) / scale / unitScale,
2033
- // Clear direct coordinates if we use anchor
2034
- x: void 0,
2035
- y: void 0,
2036
- // Ensure other properties are preserved
2037
- innerRadius: original.innerRadius,
2038
- outerRadius: original.outerRadius,
2039
- shape: original.shape || "circle"
2040
- };
2041
- }
2042
- let normalizedX = 0.5;
2043
- let normalizedY = 0.5;
2044
- if (this.currentGeometry) {
2045
- const { x, y, width, height } = this.currentGeometry;
2046
- const left = x - width / 2;
2047
- const top = y - height / 2;
2048
- normalizedX = width > 0 ? (newAbsX - left) / width : 0.5;
2049
- normalizedY = height > 0 ? (newAbsY - top) / height : 0.5;
2050
- } else {
2051
- const { width, height } = this.canvasService.canvas;
2052
- normalizedX = Coordinate.toNormalized(newAbsX, width || 800);
2053
- normalizedY = Coordinate.toNormalized(newAbsY, height || 600);
2054
- }
2090
+ constrainPosition(p, geometry, limit, feature) {
2091
+ if (feature && feature.placement === "internal") {
2092
+ const minX = geometry.x - geometry.width / 2;
2093
+ const maxX = geometry.x + geometry.width / 2;
2094
+ const minY = geometry.y - geometry.height / 2;
2095
+ const maxY = geometry.y + geometry.height / 2;
2055
2096
  return {
2056
- ...original,
2057
- x: normalizedX,
2058
- y: normalizedY,
2059
- // Clear offsets if we are using direct normalized coordinates
2060
- offsetX: void 0,
2061
- offsetY: void 0,
2062
- // Ensure other properties are preserved
2063
- innerRadius: (_c = original == null ? void 0 : original.innerRadius) != null ? _c : 15,
2064
- outerRadius: (_d = original == null ? void 0 : original.outerRadius) != null ? _d : 25,
2065
- shape: (original == null ? void 0 : original.shape) || "circle"
2097
+ x: Math.max(minX, Math.min(maxX, p.x)),
2098
+ y: Math.max(minY, Math.min(maxY, p.y))
2066
2099
  };
2100
+ }
2101
+ const nearest = getNearestPointOnDieline({ x: p.x, y: p.y }, {
2102
+ ...geometry,
2103
+ features: []
2067
2104
  });
2068
- this.holes = newHoles;
2069
- this.syncHolesToDieline();
2105
+ const dx = p.x - nearest.x;
2106
+ const dy = p.y - nearest.y;
2107
+ const dist = Math.sqrt(dx * dx + dy * dy);
2108
+ if (dist <= limit) {
2109
+ return { x: p.x, y: p.y };
2110
+ }
2111
+ const scale = limit / dist;
2112
+ return {
2113
+ x: nearest.x + dx * scale,
2114
+ y: nearest.y + dy * scale
2115
+ };
2070
2116
  }
2071
- syncHolesToDieline() {
2072
- if (!this.context || !this.canvasService) return;
2117
+ syncFeatureFromCanvas(target) {
2118
+ var _a;
2119
+ if (!this.currentGeometry || !this.context) return;
2120
+ const index = (_a = target.data) == null ? void 0 : _a.index;
2121
+ if (index === void 0 || index < 0 || index >= this.features.length)
2122
+ return;
2123
+ const feature = this.features[index];
2124
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
2125
+ const { width, height, x, y } = geometry;
2126
+ const left = x - width / 2;
2127
+ const top = y - height / 2;
2128
+ const normalizedX = width > 0 ? (target.left - left) / width : 0.5;
2129
+ const normalizedY = height > 0 ? (target.top - top) / height : 0.5;
2130
+ const updatedFeature = {
2131
+ ...feature,
2132
+ x: normalizedX,
2133
+ y: normalizedY
2134
+ // Could also update rotation if we allowed rotating markers
2135
+ };
2136
+ const newFeatures = [...this.features];
2137
+ newFeatures[index] = updatedFeature;
2138
+ this.features = newFeatures;
2073
2139
  const configService = this.context.services.get(
2074
2140
  "ConfigurationService"
2075
2141
  );
2076
2142
  if (configService) {
2077
2143
  this.isUpdatingConfig = true;
2078
2144
  try {
2079
- configService.update("dieline.holes", this.holes);
2145
+ configService.update("dieline.features", this.features);
2080
2146
  } finally {
2081
2147
  this.isUpdatingConfig = false;
2082
2148
  }
2083
2149
  }
2084
2150
  }
2085
2151
  redraw() {
2086
- if (!this.canvasService) return;
2152
+ if (!this.canvasService || !this.currentGeometry) return;
2087
2153
  const canvas = this.canvasService.canvas;
2088
- const { width, height } = canvas;
2154
+ const geometry = this.currentGeometry;
2089
2155
  const existing = canvas.getObjects().filter((obj) => {
2090
2156
  var _a;
2091
- return ((_a = obj.data) == null ? void 0 : _a.type) === "hole-marker";
2157
+ return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
2092
2158
  });
2093
2159
  existing.forEach((obj) => canvas.remove(obj));
2094
- const holes = this.holes;
2095
- if (!holes || holes.length === 0) {
2160
+ if (!this.features || this.features.length === 0) {
2096
2161
  this.canvasService.requestRenderAll();
2097
2162
  return;
2098
2163
  }
2099
- const geometry = this.currentGeometry || {
2100
- x: (width || 800) / 2,
2101
- y: (height || 600) / 2,
2102
- width: width || 800,
2103
- height: height || 600,
2104
- scale: 1
2105
- // Default scale if no geometry loaded
2164
+ const scale = geometry.scale || 1;
2165
+ const finalScale = scale;
2166
+ const groups = {};
2167
+ const singles = [];
2168
+ this.features.forEach((f, i) => {
2169
+ if (f.groupId) {
2170
+ if (!groups[f.groupId]) groups[f.groupId] = [];
2171
+ groups[f.groupId].push({ feature: f, index: i });
2172
+ } else {
2173
+ singles.push({ feature: f, index: i });
2174
+ }
2175
+ });
2176
+ const createMarkerShape = (feature, pos) => {
2177
+ const featureScale = scale;
2178
+ const visualWidth = (feature.width || 10) * featureScale;
2179
+ const visualHeight = (feature.height || 10) * featureScale;
2180
+ const visualRadius = (feature.radius || 0) * featureScale;
2181
+ const color = feature.color || (feature.operation === "add" ? "#00FF00" : "#FF0000");
2182
+ const strokeDash = feature.strokeDash || (feature.operation === "subtract" ? [4, 4] : void 0);
2183
+ let shape;
2184
+ if (feature.shape === "rect") {
2185
+ shape = new Rect2({
2186
+ width: visualWidth,
2187
+ height: visualHeight,
2188
+ rx: visualRadius,
2189
+ ry: visualRadius,
2190
+ fill: "transparent",
2191
+ stroke: color,
2192
+ strokeWidth: 2,
2193
+ strokeDashArray: strokeDash,
2194
+ originX: "center",
2195
+ originY: "center",
2196
+ left: pos.x,
2197
+ top: pos.y
2198
+ });
2199
+ } else {
2200
+ shape = new Circle({
2201
+ radius: visualRadius || 5 * finalScale,
2202
+ fill: "transparent",
2203
+ stroke: color,
2204
+ strokeWidth: 2,
2205
+ strokeDashArray: strokeDash,
2206
+ originX: "center",
2207
+ originY: "center",
2208
+ left: pos.x,
2209
+ top: pos.y
2210
+ });
2211
+ }
2212
+ if (feature.rotation) {
2213
+ shape.rotate(feature.rotation);
2214
+ }
2215
+ return shape;
2106
2216
  };
2107
- holes.forEach((hole, index) => {
2108
- const scale = geometry.scale || 1;
2109
- const unit = geometry.unit || "mm";
2110
- const unitScale = Coordinate.convertUnit(1, "mm", unit);
2111
- const visualInnerRadius = hole.innerRadius * unitScale * scale;
2112
- const visualOuterRadius = hole.outerRadius * unitScale * scale;
2113
- const pos = resolveHolePosition(
2114
- {
2115
- ...hole,
2116
- offsetX: (hole.offsetX || 0) * unitScale * scale,
2117
- offsetY: (hole.offsetY || 0) * unitScale * scale
2118
- },
2119
- geometry,
2120
- { width: geometry.width, height: geometry.height }
2121
- // Use geometry dims instead of canvas
2217
+ singles.forEach(({ feature, index }) => {
2218
+ const geometry2 = this.getGeometryForFeature(
2219
+ this.currentGeometry,
2220
+ feature
2122
2221
  );
2123
- const isSquare = hole.shape === "square";
2124
- const innerMarker = isSquare ? new Rect2({
2125
- width: visualInnerRadius * 2,
2126
- height: visualInnerRadius * 2,
2127
- fill: "transparent",
2128
- stroke: "red",
2129
- strokeWidth: 2,
2130
- originX: "center",
2131
- originY: "center"
2132
- }) : new Circle({
2133
- radius: visualInnerRadius,
2134
- fill: "transparent",
2135
- stroke: "red",
2136
- strokeWidth: 2,
2137
- originX: "center",
2138
- originY: "center"
2222
+ const pos = resolveFeaturePosition(feature, geometry2);
2223
+ const marker = createMarkerShape(feature, pos);
2224
+ marker.set({
2225
+ visible: this.isToolActive,
2226
+ selectable: this.isToolActive,
2227
+ evented: this.isToolActive,
2228
+ hasControls: false,
2229
+ hasBorders: false,
2230
+ hoverCursor: "move",
2231
+ lockRotation: true,
2232
+ lockScalingX: true,
2233
+ lockScalingY: true,
2234
+ data: { type: "feature-marker", index, isGroup: false }
2139
2235
  });
2140
- const outerMarker = isSquare ? new Rect2({
2141
- width: visualOuterRadius * 2,
2142
- height: visualOuterRadius * 2,
2143
- fill: "transparent",
2144
- stroke: "#666",
2145
- strokeWidth: 1,
2146
- strokeDashArray: [5, 5],
2147
- originX: "center",
2148
- originY: "center"
2149
- }) : new Circle({
2150
- radius: visualOuterRadius,
2151
- fill: "transparent",
2152
- stroke: "#666",
2153
- strokeWidth: 1,
2154
- strokeDashArray: [5, 5],
2155
- originX: "center",
2156
- originY: "center"
2236
+ marker.set("opacity", 0);
2237
+ marker.on("mouseover", () => {
2238
+ marker.set("opacity", 1);
2239
+ canvas.requestRenderAll();
2157
2240
  });
2158
- const holeGroup = new Group([outerMarker, innerMarker], {
2159
- left: pos.x,
2160
- top: pos.y,
2161
- originX: "center",
2162
- originY: "center",
2163
- selectable: true,
2241
+ marker.on("mouseout", () => {
2242
+ if (canvas.getActiveObject() !== marker) {
2243
+ marker.set("opacity", 0);
2244
+ canvas.requestRenderAll();
2245
+ }
2246
+ });
2247
+ marker.on("selected", () => {
2248
+ marker.set("opacity", 1);
2249
+ canvas.requestRenderAll();
2250
+ });
2251
+ marker.on("deselected", () => {
2252
+ marker.set("opacity", 0);
2253
+ canvas.requestRenderAll();
2254
+ });
2255
+ canvas.add(marker);
2256
+ canvas.bringObjectToFront(marker);
2257
+ });
2258
+ Object.keys(groups).forEach((groupId) => {
2259
+ const members = groups[groupId];
2260
+ if (members.length === 0) return;
2261
+ const shapes = members.map(({ feature }) => {
2262
+ const geometry2 = this.getGeometryForFeature(
2263
+ this.currentGeometry,
2264
+ feature
2265
+ );
2266
+ const pos = resolveFeaturePosition(feature, geometry2);
2267
+ return createMarkerShape(feature, pos);
2268
+ });
2269
+ const groupObj = new Group(shapes, {
2270
+ visible: this.isToolActive,
2271
+ selectable: this.isToolActive,
2272
+ evented: this.isToolActive,
2164
2273
  hasControls: false,
2165
- // Don't allow resizing/rotating
2166
2274
  hasBorders: false,
2167
- subTargetCheck: false,
2168
- opacity: 0,
2169
- // Default hidden
2170
2275
  hoverCursor: "move",
2171
- data: { type: "hole-marker", index }
2276
+ lockRotation: true,
2277
+ lockScalingX: true,
2278
+ lockScalingY: true,
2279
+ subTargetCheck: true,
2280
+ // Allow events to pass through if needed, but we treat as one
2281
+ interactive: false,
2282
+ // Children not interactive
2283
+ // @ts-ignore
2284
+ data: {
2285
+ type: "feature-marker",
2286
+ isGroup: true,
2287
+ groupId,
2288
+ indices: members.map((m) => m.index)
2289
+ }
2172
2290
  });
2173
- holeGroup.name = "hole-marker";
2174
- holeGroup.on("mouseover", () => {
2175
- holeGroup.set("opacity", 1);
2291
+ groupObj.set("opacity", 0);
2292
+ groupObj.on("mouseover", () => {
2293
+ groupObj.set("opacity", 1);
2176
2294
  canvas.requestRenderAll();
2177
2295
  });
2178
- holeGroup.on("mouseout", () => {
2179
- if (canvas.getActiveObject() !== holeGroup) {
2180
- holeGroup.set("opacity", 0);
2296
+ groupObj.on("mouseout", () => {
2297
+ if (canvas.getActiveObject() !== groupObj) {
2298
+ groupObj.set("opacity", 0);
2181
2299
  canvas.requestRenderAll();
2182
2300
  }
2183
2301
  });
2184
- holeGroup.on("selected", () => {
2185
- holeGroup.set("opacity", 1);
2302
+ groupObj.on("selected", () => {
2303
+ groupObj.set("opacity", 1);
2186
2304
  canvas.requestRenderAll();
2187
2305
  });
2188
- holeGroup.on("deselected", () => {
2189
- holeGroup.set("opacity", 0);
2306
+ groupObj.on("deselected", () => {
2307
+ groupObj.set("opacity", 0);
2190
2308
  canvas.requestRenderAll();
2191
2309
  });
2192
- canvas.add(holeGroup);
2193
- canvas.bringObjectToFront(holeGroup);
2194
- });
2195
- const markers = canvas.getObjects().filter((o) => {
2196
- var _a;
2197
- return ((_a = o.data) == null ? void 0 : _a.type) === "hole-marker";
2310
+ canvas.add(groupObj);
2311
+ canvas.bringObjectToFront(groupObj);
2198
2312
  });
2199
- markers.forEach((m) => canvas.bringObjectToFront(m));
2200
2313
  this.canvasService.requestRenderAll();
2201
2314
  }
2202
2315
  enforceConstraints() {
2203
- const geometry = this.currentGeometry;
2204
- if (!geometry || !this.canvasService) {
2205
- return false;
2206
- }
2207
- const effectiveOffset = this.constraintTarget === "original" ? 0 : geometry.offset;
2208
- const constraintGeometry = {
2209
- ...geometry,
2210
- width: Math.max(0, geometry.width + effectiveOffset * 2),
2211
- height: Math.max(0, geometry.height + effectiveOffset * 2),
2212
- radius: Math.max(0, geometry.radius + effectiveOffset)
2213
- };
2214
- const objects = this.canvasService.canvas.getObjects().filter((obj) => {
2316
+ if (!this.canvasService || !this.currentGeometry) return;
2317
+ const canvas = this.canvasService.canvas;
2318
+ const markers = canvas.getObjects().filter((obj) => {
2215
2319
  var _a;
2216
- return ((_a = obj.data) == null ? void 0 : _a.type) === "hole-marker";
2320
+ return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
2217
2321
  });
2218
- let changed = false;
2219
- objects.sort(
2220
- (a, b) => {
2221
- var _a, _b, _c, _d;
2222
- 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);
2322
+ markers.forEach((marker) => {
2323
+ var _a, _b, _c;
2324
+ let feature;
2325
+ if ((_a = marker.data) == null ? void 0 : _a.isGroup) {
2326
+ const indices = (_b = marker.data) == null ? void 0 : _b.indices;
2327
+ if (indices && indices.length > 0) {
2328
+ feature = this.features[indices[0]];
2329
+ }
2330
+ } else {
2331
+ const index = (_c = marker.data) == null ? void 0 : _c.index;
2332
+ if (index !== void 0) {
2333
+ feature = this.features[index];
2334
+ }
2223
2335
  }
2224
- );
2225
- const newHoles = [];
2226
- objects.forEach((obj, i) => {
2227
- var _a, _b;
2228
- const currentPos = new Point(obj.left, obj.top);
2229
- const holeData = this.holes[i];
2230
- const scale = geometry.scale || 1;
2231
- const unit = geometry.unit || "mm";
2232
- const unitScale = Coordinate.convertUnit(1, "mm", unit);
2233
- const innerR = ((_a = holeData == null ? void 0 : holeData.innerRadius) != null ? _a : 15) * unitScale * scale;
2234
- const outerR = ((_b = holeData == null ? void 0 : holeData.outerRadius) != null ? _b : 25) * unitScale * scale;
2235
- const newPos = this.calculateConstrainedPosition(
2236
- currentPos,
2237
- constraintGeometry,
2238
- innerR,
2239
- outerR
2336
+ const geometry = this.getGeometryForFeature(
2337
+ this.currentGeometry,
2338
+ feature
2240
2339
  );
2241
- if (currentPos.distanceFrom(newPos) > 0.1) {
2242
- obj.set({
2243
- left: newPos.x,
2244
- top: newPos.y
2245
- });
2246
- obj.setCoords();
2247
- changed = true;
2248
- }
2340
+ const markerStrokeWidth = (marker.strokeWidth || 2) * (marker.scaleX || 1);
2341
+ const minDim = Math.min(marker.getScaledWidth(), marker.getScaledHeight());
2342
+ const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
2343
+ const snapped = this.constrainPosition(
2344
+ new Point(marker.left, marker.top),
2345
+ geometry,
2346
+ limit,
2347
+ feature
2348
+ );
2349
+ marker.set({ left: snapped.x, top: snapped.y });
2350
+ marker.setCoords();
2249
2351
  });
2250
- if (changed) {
2251
- this.syncHolesFromCanvas();
2252
- return true;
2253
- }
2254
- return false;
2255
- }
2256
- calculateConstrainedPosition(p, g, innerRadius, outerRadius) {
2257
- const options = {
2258
- ...g,
2259
- holes: []
2260
- // We don't need holes for boundary calculation
2261
- };
2262
- const nearest = getNearestPointOnDieline(
2263
- { x: p.x, y: p.y },
2264
- options
2265
- );
2266
- const nearestP = new Point(nearest.x, nearest.y);
2267
- const dist = p.distanceFrom(nearestP);
2268
- const v = p.subtract(nearestP);
2269
- const center = new Point(g.x, g.y);
2270
- const distToCenter = p.distanceFrom(center);
2271
- const nearestDistToCenter = nearestP.distanceFrom(center);
2272
- let signedDist = dist;
2273
- if (distToCenter < nearestDistToCenter) {
2274
- signedDist = -dist;
2275
- }
2276
- let clampedDist = signedDist;
2277
- if (signedDist > 0) {
2278
- clampedDist = Math.min(signedDist, innerRadius);
2279
- } else {
2280
- clampedDist = Math.max(signedDist, -outerRadius);
2281
- }
2282
- if (dist < 1e-3) return nearestP;
2283
- const scale = Math.abs(clampedDist) / (dist || 1);
2284
- const offset = v.scalarMultiply(scale);
2285
- return nearestP.add(offset);
2352
+ canvas.requestRenderAll();
2286
2353
  }
2287
2354
  };
2288
2355
 
@@ -2301,6 +2368,11 @@ var ImageTool = class {
2301
2368
  this.objectMap = /* @__PURE__ */ new Map();
2302
2369
  this.loadResolvers = /* @__PURE__ */ new Map();
2303
2370
  this.isUpdatingConfig = false;
2371
+ this.isToolActive = false;
2372
+ this.onToolActivated = (event) => {
2373
+ this.isToolActive = event.id === this.id;
2374
+ this.updateInteractivity();
2375
+ };
2304
2376
  }
2305
2377
  activate(context) {
2306
2378
  this.context = context;
@@ -2309,6 +2381,7 @@ var ImageTool = class {
2309
2381
  console.warn("CanvasService not found for ImageTool");
2310
2382
  return;
2311
2383
  }
2384
+ context.eventBus.on("tool:activated", this.onToolActivated);
2312
2385
  const configService = context.services.get(
2313
2386
  "ConfigurationService"
2314
2387
  );
@@ -2326,6 +2399,7 @@ var ImageTool = class {
2326
2399
  this.updateImages();
2327
2400
  }
2328
2401
  deactivate(context) {
2402
+ context.eventBus.off("tool:activated", this.onToolActivated);
2329
2403
  if (this.canvasService) {
2330
2404
  const layer = this.canvasService.getLayer("user");
2331
2405
  if (layer) {
@@ -2339,6 +2413,18 @@ var ImageTool = class {
2339
2413
  this.context = void 0;
2340
2414
  }
2341
2415
  }
2416
+ updateInteractivity() {
2417
+ var _a;
2418
+ this.objectMap.forEach((obj) => {
2419
+ obj.set({
2420
+ selectable: this.isToolActive,
2421
+ evented: this.isToolActive,
2422
+ hasControls: this.isToolActive,
2423
+ hasBorders: this.isToolActive
2424
+ });
2425
+ });
2426
+ (_a = this.canvasService) == null ? void 0 : _a.requestRenderAll();
2427
+ }
2342
2428
  contribute() {
2343
2429
  return {
2344
2430
  [ContributionPointIds5.CONFIGURATIONS]: [
@@ -2526,6 +2612,14 @@ var ImageTool = class {
2526
2612
  const layout = this.getLayoutInfo();
2527
2613
  this.items.forEach((item, index) => {
2528
2614
  let obj = this.objectMap.get(item.id);
2615
+ if (obj && obj.getSrc) {
2616
+ const currentSrc = obj.getSrc();
2617
+ if (currentSrc !== item.url) {
2618
+ layer.remove(obj);
2619
+ this.objectMap.delete(item.id);
2620
+ obj = void 0;
2621
+ }
2622
+ }
2529
2623
  if (!obj) {
2530
2624
  this.loadImage(item, layer, layout);
2531
2625
  } else {
@@ -2582,7 +2676,11 @@ var ImageTool = class {
2582
2676
  originY: "center",
2583
2677
  data: { id: item.id },
2584
2678
  uniformScaling: true,
2585
- lockScalingFlip: true
2679
+ lockScalingFlip: true,
2680
+ selectable: this.isToolActive,
2681
+ evented: this.isToolActive,
2682
+ hasControls: this.isToolActive,
2683
+ hasBorders: this.isToolActive
2586
2684
  });
2587
2685
  image.setControlsVisibility({
2588
2686
  mt: false,
@@ -3439,6 +3537,24 @@ var CanvasService = class {
3439
3537
  ...options
3440
3538
  });
3441
3539
  }
3540
+ if (options == null ? void 0 : options.eventBus) {
3541
+ this.setEventBus(options.eventBus);
3542
+ }
3543
+ }
3544
+ setEventBus(eventBus) {
3545
+ this.eventBus = eventBus;
3546
+ this.setupEvents();
3547
+ }
3548
+ setupEvents() {
3549
+ if (!this.eventBus) return;
3550
+ const bus = this.eventBus;
3551
+ const forward = (name) => (e) => bus.emit(name, e);
3552
+ this.canvas.on("selection:created", forward("selection:created"));
3553
+ this.canvas.on("selection:updated", forward("selection:updated"));
3554
+ this.canvas.on("selection:cleared", forward("selection:cleared"));
3555
+ this.canvas.on("object:modified", forward("object:modified"));
3556
+ this.canvas.on("object:added", forward("object:added"));
3557
+ this.canvas.on("object:removed", forward("object:removed"));
3442
3558
  }
3443
3559
  dispose() {
3444
3560
  this.canvas.dispose();
@@ -3495,8 +3611,8 @@ export {
3495
3611
  BackgroundTool,
3496
3612
  CanvasService,
3497
3613
  DielineTool,
3614
+ FeatureTool,
3498
3615
  FilmTool,
3499
- HoleTool,
3500
3616
  ImageTool,
3501
3617
  MirrorTool,
3502
3618
  RulerTool,