@pooder/kit 3.4.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -33,8 +33,8 @@ __export(index_exports, {
33
33
  BackgroundTool: () => BackgroundTool,
34
34
  CanvasService: () => CanvasService,
35
35
  DielineTool: () => DielineTool,
36
+ FeatureTool: () => FeatureTool,
36
37
  FilmTool: () => FilmTool,
37
- HoleTool: () => HoleTool,
38
38
  ImageTool: () => ImageTool,
39
39
  MirrorTool: () => MirrorTool,
40
40
  RulerTool: () => RulerTool,
@@ -248,6 +248,7 @@ var import_core2 = require("@pooder/core");
248
248
  var import_fabric2 = require("fabric");
249
249
 
250
250
  // src/tracer.ts
251
+ var import_paper = __toESM(require("paper"));
251
252
  var ImageTracer = class {
252
253
  /**
253
254
  * Main entry point: Traces an image URL to an SVG path string.
@@ -255,7 +256,7 @@ var ImageTracer = class {
255
256
  * @param options Configuration options.
256
257
  */
257
258
  static async trace(imageUrl, options = {}) {
258
- var _a, _b, _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,89 +764,76 @@ 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
  }
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
+ }
768
796
  function getDielineShape(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 adds = [];
801
+ const subtracts = [];
802
+ features.forEach((f) => {
803
+ const pos = resolveFeaturePosition(f, options);
804
+ const center = new import_paper2.default.Point(pos.x, pos.y);
805
+ const item = createFeatureItem(f, center);
806
+ if (f.operation === "add") {
807
+ adds.push(item);
798
808
  } else {
809
+ subtracts.push(item);
810
+ }
811
+ });
812
+ if (adds.length > 0) {
813
+ for (const item of adds) {
799
814
  try {
800
- const temp = lugsPath.unite(lug);
801
- lugsPath.remove();
802
- lug.remove();
803
- lugsPath = temp;
815
+ const temp = mainShape.unite(item);
816
+ mainShape.remove();
817
+ item.remove();
818
+ mainShape = temp;
804
819
  } catch (e) {
805
- console.error("Geometry: Failed to unite lug", e);
806
- lug.remove();
820
+ console.error("Geometry: Failed to unite feature", e);
821
+ item.remove();
807
822
  }
808
823
  }
809
- if (!cutsPath) {
810
- cutsPath = cut;
811
- } else {
824
+ }
825
+ if (subtracts.length > 0) {
826
+ for (const item of subtracts) {
812
827
  try {
813
- const temp = cutsPath.unite(cut);
814
- cutsPath.remove();
815
- cut.remove();
816
- cutsPath = temp;
828
+ const temp = mainShape.subtract(item);
829
+ mainShape.remove();
830
+ item.remove();
831
+ mainShape = temp;
817
832
  } catch (e) {
818
- console.error("Geometry: Failed to unite cut", e);
819
- cut.remove();
833
+ console.error("Geometry: Failed to subtract feature", e);
834
+ item.remove();
820
835
  }
821
836
  }
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
- }
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
- );
844
- }
845
837
  }
846
838
  }
847
839
  return mainShape;
@@ -850,7 +842,7 @@ function generateDielinePath(options) {
850
842
  const paperWidth = options.canvasWidth || options.width * 2 || 2e3;
851
843
  const paperHeight = options.canvasHeight || options.height * 2 || 2e3;
852
844
  ensurePaper(paperWidth, paperHeight);
853
- import_paper.default.project.activeLayer.removeChildren();
845
+ import_paper2.default.project.activeLayer.removeChildren();
854
846
  const mainShape = getDielineShape(options);
855
847
  const pathData = mainShape.pathData;
856
848
  mainShape.remove();
@@ -858,9 +850,9 @@ function generateDielinePath(options) {
858
850
  }
859
851
  function generateMaskPath(options) {
860
852
  ensurePaper(options.canvasWidth, options.canvasHeight);
861
- import_paper.default.project.activeLayer.removeChildren();
853
+ import_paper2.default.project.activeLayer.removeChildren();
862
854
  const { canvasWidth, canvasHeight } = options;
863
- const maskRect = new import_paper.default.Path.Rectangle({
855
+ const maskRect = new import_paper2.default.Path.Rectangle({
864
856
  point: [0, 0],
865
857
  size: [canvasWidth, canvasHeight]
866
858
  });
@@ -872,43 +864,13 @@ function generateMaskPath(options) {
872
864
  finalMask.remove();
873
865
  return pathData;
874
866
  }
875
- function generateBleedZonePath(options, offset) {
876
- const paperWidth = options.canvasWidth || options.width * 2 || 2e3;
877
- const paperHeight = options.canvasHeight || options.height * 2 || 2e3;
867
+ function generateBleedZonePath(originalOptions, offsetOptions, offset) {
868
+ const paperWidth = originalOptions.canvasWidth || originalOptions.width * 2 || 2e3;
869
+ const paperHeight = originalOptions.canvasHeight || originalOptions.height * 2 || 2e3;
878
870
  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
- }
871
+ import_paper2.default.project.activeLayer.removeChildren();
872
+ const shapeOriginal = getDielineShape(originalOptions);
873
+ const shapeOffset = getDielineShape(offsetOptions);
912
874
  let bleedZone;
913
875
  if (offset > 0) {
914
876
  bleedZone = shapeOffset.subtract(shapeOriginal);
@@ -923,16 +885,16 @@ function generateBleedZonePath(options, offset) {
923
885
  }
924
886
  function getNearestPointOnDieline(point, options) {
925
887
  ensurePaper(options.width * 2, options.height * 2);
926
- import_paper.default.project.activeLayer.removeChildren();
888
+ import_paper2.default.project.activeLayer.removeChildren();
927
889
  const shape = createBaseShape(options);
928
- const p = new import_paper.default.Point(point.x, point.y);
890
+ const p = new import_paper2.default.Point(point.x, point.y);
929
891
  const nearest = shape.getNearestPoint(p);
930
892
  const result = { x: nearest.x, y: nearest.y };
931
893
  shape.remove();
932
894
  return result;
933
895
  }
934
896
  function getPathBounds(pathData) {
935
- const path = new import_paper.default.Path();
897
+ const path = new import_paper2.default.Path();
936
898
  path.pathData = pathData;
937
899
  const bounds = path.bounds;
938
900
  path.remove();
@@ -951,20 +913,41 @@ var DielineTool = class {
951
913
  this.metadata = {
952
914
  name: "DielineTool"
953
915
  };
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;
916
+ this.state = {
917
+ unit: "mm",
918
+ shape: "rect",
919
+ width: 500,
920
+ height: 500,
921
+ radius: 0,
922
+ offset: 0,
923
+ padding: 140,
924
+ mainLine: {
925
+ width: 2.7,
926
+ color: "#FF0000",
927
+ dashLength: 5,
928
+ style: "solid"
929
+ },
930
+ offsetLine: {
931
+ width: 2.7,
932
+ color: "#FF0000",
933
+ dashLength: 5,
934
+ style: "solid"
935
+ },
936
+ insideColor: "rgba(0,0,0,0)",
937
+ outsideColor: "#ffffff",
938
+ showBleedLines: true,
939
+ features: []
940
+ };
966
941
  if (options) {
967
- Object.assign(this, options);
942
+ if (options.mainLine) {
943
+ Object.assign(this.state.mainLine, options.mainLine);
944
+ delete options.mainLine;
945
+ }
946
+ if (options.offsetLine) {
947
+ Object.assign(this.state.offsetLine, options.offsetLine);
948
+ delete options.offsetLine;
949
+ }
950
+ Object.assign(this.state, options);
968
951
  }
969
952
  }
970
953
  activate(context) {
@@ -976,38 +959,93 @@ var DielineTool = class {
976
959
  }
977
960
  const configService = context.services.get("ConfigurationService");
978
961
  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);
962
+ const s = this.state;
963
+ s.unit = configService.get("dieline.unit", s.unit);
964
+ s.shape = configService.get("dieline.shape", s.shape);
965
+ s.width = configService.get("dieline.width", s.width);
966
+ s.height = configService.get("dieline.height", s.height);
967
+ s.radius = configService.get("dieline.radius", s.radius);
968
+ s.padding = configService.get("dieline.padding", s.padding);
969
+ s.offset = configService.get("dieline.offset", s.offset);
970
+ s.mainLine.width = configService.get("dieline.strokeWidth", s.mainLine.width);
971
+ s.mainLine.color = configService.get("dieline.strokeColor", s.mainLine.color);
972
+ s.mainLine.dashLength = configService.get("dieline.dashLength", s.mainLine.dashLength);
973
+ s.mainLine.style = configService.get("dieline.style", s.mainLine.style);
974
+ s.offsetLine.width = configService.get("dieline.offsetStrokeWidth", s.offsetLine.width);
975
+ s.offsetLine.color = configService.get("dieline.offsetStrokeColor", s.offsetLine.color);
976
+ s.offsetLine.dashLength = configService.get("dieline.offsetDashLength", s.offsetLine.dashLength);
977
+ s.offsetLine.style = configService.get("dieline.offsetStyle", s.offsetLine.style);
978
+ s.insideColor = configService.get("dieline.insideColor", s.insideColor);
979
+ s.outsideColor = configService.get("dieline.outsideColor", s.outsideColor);
980
+ s.showBleedLines = configService.get("dieline.showBleedLines", s.showBleedLines);
981
+ s.features = configService.get("dieline.features", s.features);
982
+ s.pathData = configService.get("dieline.pathData", s.pathData);
1001
983
  configService.onAnyChange((e) => {
1002
984
  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();
985
+ console.log(`[DielineTool] Config change detected: ${e.key} -> ${e.value}`);
986
+ switch (e.key) {
987
+ case "dieline.unit":
988
+ s.unit = e.value;
989
+ break;
990
+ case "dieline.shape":
991
+ s.shape = e.value;
992
+ break;
993
+ case "dieline.width":
994
+ s.width = e.value;
995
+ break;
996
+ case "dieline.height":
997
+ s.height = e.value;
998
+ break;
999
+ case "dieline.radius":
1000
+ s.radius = e.value;
1001
+ break;
1002
+ case "dieline.padding":
1003
+ s.padding = e.value;
1004
+ break;
1005
+ case "dieline.offset":
1006
+ s.offset = e.value;
1007
+ break;
1008
+ case "dieline.strokeWidth":
1009
+ s.mainLine.width = e.value;
1010
+ break;
1011
+ case "dieline.strokeColor":
1012
+ s.mainLine.color = e.value;
1013
+ break;
1014
+ case "dieline.dashLength":
1015
+ s.mainLine.dashLength = e.value;
1016
+ break;
1017
+ case "dieline.style":
1018
+ s.mainLine.style = e.value;
1019
+ break;
1020
+ case "dieline.offsetStrokeWidth":
1021
+ s.offsetLine.width = e.value;
1022
+ break;
1023
+ case "dieline.offsetStrokeColor":
1024
+ s.offsetLine.color = e.value;
1025
+ break;
1026
+ case "dieline.offsetDashLength":
1027
+ s.offsetLine.dashLength = e.value;
1028
+ break;
1029
+ case "dieline.offsetStyle":
1030
+ s.offsetLine.style = e.value;
1031
+ break;
1032
+ case "dieline.insideColor":
1033
+ s.insideColor = e.value;
1034
+ break;
1035
+ case "dieline.outsideColor":
1036
+ s.outsideColor = e.value;
1037
+ break;
1038
+ case "dieline.showBleedLines":
1039
+ s.showBleedLines = e.value;
1040
+ break;
1041
+ case "dieline.features":
1042
+ s.features = e.value;
1043
+ break;
1044
+ case "dieline.pathData":
1045
+ s.pathData = e.value;
1046
+ break;
1010
1047
  }
1048
+ this.updateDieline();
1011
1049
  }
1012
1050
  });
1013
1051
  }
@@ -1020,6 +1058,7 @@ var DielineTool = class {
1020
1058
  this.context = void 0;
1021
1059
  }
1022
1060
  contribute() {
1061
+ const s = this.state;
1023
1062
  return {
1024
1063
  [import_core2.ContributionPointIds.CONFIGURATIONS]: [
1025
1064
  {
@@ -1027,14 +1066,14 @@ var DielineTool = class {
1027
1066
  type: "select",
1028
1067
  label: "Unit",
1029
1068
  options: ["px", "mm", "cm", "in"],
1030
- default: this.unit
1069
+ default: s.unit
1031
1070
  },
1032
1071
  {
1033
1072
  id: "dieline.shape",
1034
1073
  type: "select",
1035
1074
  label: "Shape",
1036
1075
  options: ["rect", "circle", "ellipse", "custom"],
1037
- default: this.shape
1076
+ default: s.shape
1038
1077
  },
1039
1078
  {
1040
1079
  id: "dieline.width",
@@ -1042,7 +1081,7 @@ var DielineTool = class {
1042
1081
  label: "Width",
1043
1082
  min: 10,
1044
1083
  max: 2e3,
1045
- default: this.width
1084
+ default: s.width
1046
1085
  },
1047
1086
  {
1048
1087
  id: "dieline.height",
@@ -1050,7 +1089,7 @@ var DielineTool = class {
1050
1089
  label: "Height",
1051
1090
  min: 10,
1052
1091
  max: 2e3,
1053
- default: this.height
1092
+ default: s.height
1054
1093
  },
1055
1094
  {
1056
1095
  id: "dieline.radius",
@@ -1058,20 +1097,14 @@ var DielineTool = class {
1058
1097
  label: "Corner Radius",
1059
1098
  min: 0,
1060
1099
  max: 500,
1061
- default: this.radius
1062
- },
1063
- {
1064
- id: "dieline.position",
1065
- type: "json",
1066
- label: "Position (Normalized)",
1067
- default: this.radius
1100
+ default: s.radius
1068
1101
  },
1069
1102
  {
1070
1103
  id: "dieline.padding",
1071
1104
  type: "select",
1072
1105
  label: "View Padding",
1073
1106
  options: [0, 10, 20, 40, 60, 100, "2%", "5%", "10%", "15%", "20%"],
1074
- default: this.padding
1107
+ default: s.padding
1075
1108
  },
1076
1109
  {
1077
1110
  id: "dieline.offset",
@@ -1079,38 +1112,91 @@ var DielineTool = class {
1079
1112
  label: "Bleed Offset",
1080
1113
  min: -100,
1081
1114
  max: 100,
1082
- default: this.offset
1115
+ default: s.offset
1083
1116
  },
1084
1117
  {
1085
1118
  id: "dieline.showBleedLines",
1086
1119
  type: "boolean",
1087
1120
  label: "Show Bleed Lines",
1088
- default: this.showBleedLines
1121
+ default: s.showBleedLines
1122
+ },
1123
+ {
1124
+ id: "dieline.strokeWidth",
1125
+ type: "number",
1126
+ label: "Line Width",
1127
+ min: 0.1,
1128
+ max: 10,
1129
+ step: 0.1,
1130
+ default: s.mainLine.width
1131
+ },
1132
+ {
1133
+ id: "dieline.strokeColor",
1134
+ type: "color",
1135
+ label: "Line Color",
1136
+ default: s.mainLine.color
1137
+ },
1138
+ {
1139
+ id: "dieline.dashLength",
1140
+ type: "number",
1141
+ label: "Dash Length",
1142
+ min: 1,
1143
+ max: 50,
1144
+ default: s.mainLine.dashLength
1089
1145
  },
1090
1146
  {
1091
1147
  id: "dieline.style",
1092
1148
  type: "select",
1093
1149
  label: "Line Style",
1094
- options: ["solid", "dashed"],
1095
- default: this.style
1150
+ options: ["solid", "dashed", "hidden"],
1151
+ default: s.mainLine.style
1152
+ },
1153
+ {
1154
+ id: "dieline.offsetStrokeWidth",
1155
+ type: "number",
1156
+ label: "Offset Line Width",
1157
+ min: 0.1,
1158
+ max: 10,
1159
+ step: 0.1,
1160
+ default: s.offsetLine.width
1161
+ },
1162
+ {
1163
+ id: "dieline.offsetStrokeColor",
1164
+ type: "color",
1165
+ label: "Offset Line Color",
1166
+ default: s.offsetLine.color
1167
+ },
1168
+ {
1169
+ id: "dieline.offsetDashLength",
1170
+ type: "number",
1171
+ label: "Offset Dash Length",
1172
+ min: 1,
1173
+ max: 50,
1174
+ default: s.offsetLine.dashLength
1175
+ },
1176
+ {
1177
+ id: "dieline.offsetStyle",
1178
+ type: "select",
1179
+ label: "Offset Line Style",
1180
+ options: ["solid", "dashed", "hidden"],
1181
+ default: s.offsetLine.style
1096
1182
  },
1097
1183
  {
1098
1184
  id: "dieline.insideColor",
1099
1185
  type: "color",
1100
1186
  label: "Inside Color",
1101
- default: this.insideColor
1187
+ default: s.insideColor
1102
1188
  },
1103
1189
  {
1104
1190
  id: "dieline.outsideColor",
1105
1191
  type: "color",
1106
1192
  label: "Outside Color",
1107
- default: this.outsideColor
1193
+ default: s.outsideColor
1108
1194
  },
1109
1195
  {
1110
- id: "dieline.holes",
1196
+ id: "dieline.features",
1111
1197
  type: "json",
1112
- label: "Holes",
1113
- default: this.holes
1198
+ label: "Edge Features",
1199
+ default: s.features
1114
1200
  }
1115
1201
  ],
1116
1202
  [import_core2.ContributionPointIds.COMMANDS]: [
@@ -1132,24 +1218,18 @@ var DielineTool = class {
1132
1218
  command: "detectEdge",
1133
1219
  title: "Detect Edge from Image",
1134
1220
  handler: async (imageUrl, options) => {
1135
- var _a;
1136
1221
  try {
1137
1222
  const pathData = await ImageTracer.trace(imageUrl, options);
1138
1223
  const bounds = getPathBounds(pathData);
1139
- const currentMax = Math.max(this.width, this.height);
1224
+ const currentMax = Math.max(s.width, s.height);
1140
1225
  const scale = currentMax / Math.max(bounds.width, bounds.height);
1141
1226
  const newWidth = bounds.width * scale;
1142
1227
  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;
1228
+ return {
1229
+ pathData,
1230
+ width: newWidth,
1231
+ height: newHeight
1232
+ };
1153
1233
  } catch (e) {
1154
1234
  console.error("Edge detection failed", e);
1155
1235
  throw e;
@@ -1208,15 +1288,15 @@ var DielineTool = class {
1208
1288
  return new import_fabric2.Pattern({ source: canvas, repetition: "repeat" });
1209
1289
  }
1210
1290
  resolvePadding(containerWidth, containerHeight) {
1211
- if (typeof this.padding === "number") {
1212
- return this.padding;
1291
+ if (typeof this.state.padding === "number") {
1292
+ return this.state.padding;
1213
1293
  }
1214
- if (typeof this.padding === "string") {
1215
- if (this.padding.endsWith("%")) {
1216
- const percent = parseFloat(this.padding) / 100;
1294
+ if (typeof this.state.padding === "string") {
1295
+ if (this.state.padding.endsWith("%")) {
1296
+ const percent = parseFloat(this.state.padding) / 100;
1217
1297
  return Math.min(containerWidth, containerHeight) * percent;
1218
1298
  }
1219
- return parseFloat(this.padding) || 0;
1299
+ return parseFloat(this.state.padding) || 0;
1220
1300
  }
1221
1301
  return 0;
1222
1302
  }
@@ -1229,14 +1309,14 @@ var DielineTool = class {
1229
1309
  shape,
1230
1310
  radius,
1231
1311
  offset,
1232
- style,
1312
+ mainLine,
1313
+ offsetLine,
1233
1314
  insideColor,
1234
1315
  outsideColor,
1235
- position,
1236
1316
  showBleedLines,
1237
- holes
1238
- } = this;
1239
- let { width, height } = this;
1317
+ features
1318
+ } = this.state;
1319
+ let { width, height } = this.state;
1240
1320
  const canvasW = this.canvasService.canvas.width || 800;
1241
1321
  const canvasH = this.canvasService.canvas.height || 600;
1242
1322
  const paddingPx = this.resolvePadding(canvasW, canvasH);
@@ -1253,40 +1333,27 @@ var DielineTool = class {
1253
1333
  const visualRadius = radius * scale;
1254
1334
  const visualOffset = offset * scale;
1255
1335
  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
- });
1336
+ const absoluteFeatures = (features || []).map((f) => {
1337
+ const featureScale = scale;
1275
1338
  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
1339
+ ...f,
1340
+ x: f.x,
1341
+ y: f.y,
1342
+ width: (f.width || 0) * featureScale,
1343
+ height: (f.height || 0) * featureScale,
1344
+ radius: (f.radius || 0) * featureScale
1285
1345
  };
1286
1346
  });
1347
+ const originalFeatures = absoluteFeatures.filter(
1348
+ (f) => !f.target || f.target === "original" || f.target === "both"
1349
+ );
1350
+ const offsetFeatures = absoluteFeatures.filter(
1351
+ (f) => f.target === "offset" || f.target === "both"
1352
+ );
1287
1353
  const cutW = Math.max(0, visualWidth + visualOffset * 2);
1288
1354
  const cutH = Math.max(0, visualHeight + visualOffset * 2);
1289
1355
  const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
1356
+ const maskFeatures = visualOffset !== 0 ? offsetFeatures : originalFeatures;
1290
1357
  const maskPathData = generateMaskPath({
1291
1358
  canvasWidth: canvasW,
1292
1359
  canvasHeight: canvasH,
@@ -1296,8 +1363,8 @@ var DielineTool = class {
1296
1363
  radius: cutR,
1297
1364
  x: cx,
1298
1365
  y: cy,
1299
- holes: absoluteHoles,
1300
- pathData: this.pathData
1366
+ features: maskFeatures,
1367
+ pathData: this.state.pathData
1301
1368
  });
1302
1369
  const mask = new import_fabric2.Path(maskPathData, {
1303
1370
  fill: outsideColor,
@@ -1318,8 +1385,9 @@ var DielineTool = class {
1318
1385
  radius: cutR,
1319
1386
  x: cx,
1320
1387
  y: cy,
1321
- holes: absoluteHoles,
1322
- pathData: this.pathData,
1388
+ features: maskFeatures,
1389
+ // Use same features as mask for consistency
1390
+ pathData: this.state.pathData,
1323
1391
  canvasWidth: canvasW,
1324
1392
  canvasHeight: canvasH
1325
1393
  });
@@ -1343,15 +1411,27 @@ var DielineTool = class {
1343
1411
  radius: visualRadius,
1344
1412
  x: cx,
1345
1413
  y: cy,
1346
- holes: absoluteHoles,
1347
- pathData: this.pathData,
1414
+ features: originalFeatures,
1415
+ pathData: this.state.pathData,
1416
+ canvasWidth: canvasW,
1417
+ canvasHeight: canvasH
1418
+ },
1419
+ {
1420
+ shape,
1421
+ width: cutW,
1422
+ height: cutH,
1423
+ radius: cutR,
1424
+ x: cx,
1425
+ y: cy,
1426
+ features: offsetFeatures,
1427
+ pathData: this.state.pathData,
1348
1428
  canvasWidth: canvasW,
1349
1429
  canvasHeight: canvasH
1350
1430
  },
1351
1431
  visualOffset
1352
1432
  );
1353
1433
  if (showBleedLines !== false) {
1354
- const pattern = this.createHatchPattern("red");
1434
+ const pattern = this.createHatchPattern(mainLine.color);
1355
1435
  if (pattern) {
1356
1436
  const bleedObj = new import_fabric2.Path(bleedPathData, {
1357
1437
  fill: pattern,
@@ -1372,18 +1452,16 @@ var DielineTool = class {
1372
1452
  radius: cutR,
1373
1453
  x: cx,
1374
1454
  y: cy,
1375
- holes: absoluteHoles,
1376
- pathData: this.pathData,
1455
+ features: offsetFeatures,
1456
+ pathData: this.state.pathData,
1377
1457
  canvasWidth: canvasW,
1378
1458
  canvasHeight: canvasH
1379
1459
  });
1380
1460
  const offsetBorderObj = new import_fabric2.Path(offsetPathData, {
1381
1461
  fill: null,
1382
- stroke: "#666",
1383
- // Grey
1384
- strokeWidth: 1,
1385
- strokeDashArray: [4, 4],
1386
- // Dashed
1462
+ stroke: offsetLine.style === "hidden" ? null : offsetLine.color,
1463
+ strokeWidth: offsetLine.width,
1464
+ strokeDashArray: offsetLine.style === "dashed" ? [offsetLine.dashLength, offsetLine.dashLength] : void 0,
1387
1465
  selectable: false,
1388
1466
  evented: false,
1389
1467
  originX: "left",
@@ -1398,16 +1476,16 @@ var DielineTool = class {
1398
1476
  radius: visualRadius,
1399
1477
  x: cx,
1400
1478
  y: cy,
1401
- holes: absoluteHoles,
1402
- pathData: this.pathData,
1479
+ features: originalFeatures,
1480
+ pathData: this.state.pathData,
1403
1481
  canvasWidth: canvasW,
1404
1482
  canvasHeight: canvasH
1405
1483
  });
1406
1484
  const borderObj = new import_fabric2.Path(borderPathData, {
1407
1485
  fill: "transparent",
1408
- stroke: "red",
1409
- strokeWidth: 1,
1410
- strokeDashArray: style === "dashed" ? [5, 5] : void 0,
1486
+ stroke: mainLine.style === "hidden" ? null : mainLine.color,
1487
+ strokeWidth: mainLine.width,
1488
+ strokeDashArray: mainLine.style === "dashed" ? [mainLine.dashLength, mainLine.dashLength] : void 0,
1411
1489
  selectable: false,
1412
1490
  evented: false,
1413
1491
  originX: "left",
@@ -1439,7 +1517,7 @@ var DielineTool = class {
1439
1517
  }
1440
1518
  getGeometry() {
1441
1519
  if (!this.canvasService) return null;
1442
- const { unit, shape, width, height, radius, position, offset } = this;
1520
+ const { unit, shape, width, height, radius, offset, mainLine, pathData } = this.state;
1443
1521
  const canvasW = this.canvasService.canvas.width || 800;
1444
1522
  const canvasH = this.canvasService.canvas.height || 600;
1445
1523
  const paddingPx = this.resolvePadding(canvasW, canvasH);
@@ -1462,16 +1540,17 @@ var DielineTool = class {
1462
1540
  height: visualHeight,
1463
1541
  radius: radius * scale,
1464
1542
  offset: offset * scale,
1465
- // Pass scale to help other tools (like HoleTool) convert units
1543
+ // Pass scale to help other tools (like FeatureTool) convert units
1466
1544
  scale,
1467
- pathData: this.pathData
1545
+ strokeWidth: mainLine.width,
1546
+ pathData
1468
1547
  };
1469
1548
  }
1470
1549
  async exportCutImage() {
1471
1550
  if (!this.canvasService) return null;
1472
1551
  const userLayer = this.canvasService.getLayer("user");
1473
1552
  if (!userLayer) return null;
1474
- const { shape, width, height, radius, position, holes } = this;
1553
+ const { shape, width, height, radius, features, unit, pathData } = this.state;
1475
1554
  const canvasW = this.canvasService.canvas.width || 800;
1476
1555
  const canvasH = this.canvasService.canvas.height || 600;
1477
1556
  const paddingPx = this.resolvePadding(canvasW, canvasH);
@@ -1486,55 +1565,45 @@ var DielineTool = class {
1486
1565
  const visualWidth = layout.width;
1487
1566
  const visualHeight = layout.height;
1488
1567
  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
- );
1568
+ const absoluteFeatures = (features || []).map((f) => {
1569
+ const featureScale = scale;
1501
1570
  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
1571
+ ...f,
1572
+ x: f.x,
1573
+ y: f.y,
1574
+ width: (f.width || 0) * featureScale,
1575
+ height: (f.height || 0) * featureScale,
1576
+ radius: (f.radius || 0) * featureScale
1509
1577
  };
1510
1578
  });
1511
- const pathData = generateDielinePath({
1579
+ const originalFeatures = absoluteFeatures.filter(
1580
+ (f) => !f.target || f.target === "original" || f.target === "both"
1581
+ );
1582
+ const generatedPathData = generateDielinePath({
1512
1583
  shape,
1513
1584
  width: visualWidth,
1514
1585
  height: visualHeight,
1515
1586
  radius: visualRadius,
1516
1587
  x: cx,
1517
1588
  y: cy,
1518
- holes: absoluteHoles,
1519
- pathData: this.pathData,
1589
+ features: originalFeatures,
1590
+ pathData,
1520
1591
  canvasWidth: canvasW,
1521
1592
  canvasHeight: canvasH
1522
1593
  });
1523
1594
  const clonedLayer = await userLayer.clone();
1524
- const clipPath = new import_fabric2.Path(pathData, {
1595
+ const clipPath = new import_fabric2.Path(generatedPathData, {
1525
1596
  originX: "left",
1526
1597
  originY: "top",
1527
1598
  left: 0,
1528
1599
  top: 0,
1529
1600
  absolutePositioned: true
1530
- // Important for groups
1531
1601
  });
1532
1602
  clonedLayer.clipPath = clipPath;
1533
1603
  const bounds = clipPath.getBoundingRect();
1534
1604
  const dataUrl = clonedLayer.toDataURL({
1535
1605
  format: "png",
1536
1606
  multiplier: 2,
1537
- // Better quality
1538
1607
  left: bounds.left,
1539
1608
  top: bounds.top,
1540
1609
  width: bounds.width,
@@ -1703,22 +1772,20 @@ var FilmTool = class {
1703
1772
  }
1704
1773
  };
1705
1774
 
1706
- // src/hole.ts
1775
+ // src/feature.ts
1707
1776
  var import_core4 = require("@pooder/core");
1708
1777
  var import_fabric4 = require("fabric");
1709
- var HoleTool = class {
1778
+ var FeatureTool = class {
1710
1779
  constructor(options) {
1711
- this.id = "pooder.kit.hole";
1780
+ this.id = "pooder.kit.feature";
1712
1781
  this.metadata = {
1713
- name: "HoleTool"
1782
+ name: "FeatureTool"
1714
1783
  };
1715
- this.holes = [];
1716
- this.constraintTarget = "bleed";
1784
+ this.features = [];
1717
1785
  this.isUpdatingConfig = false;
1718
1786
  this.handleMoving = null;
1719
1787
  this.handleModified = null;
1720
1788
  this.handleDielineChange = null;
1721
- // Cache geometry to enforce constraints during drag
1722
1789
  this.currentGeometry = null;
1723
1790
  if (options) {
1724
1791
  Object.assign(this, options);
@@ -1728,26 +1795,18 @@ var HoleTool = class {
1728
1795
  this.context = context;
1729
1796
  this.canvasService = context.services.get("CanvasService");
1730
1797
  if (!this.canvasService) {
1731
- console.warn("CanvasService not found for HoleTool");
1798
+ console.warn("CanvasService not found for FeatureTool");
1732
1799
  return;
1733
1800
  }
1734
1801
  const configService = context.services.get(
1735
1802
  "ConfigurationService"
1736
1803
  );
1737
1804
  if (configService) {
1738
- this.constraintTarget = configService.get(
1739
- "hole.constraintTarget",
1740
- this.constraintTarget
1741
- );
1742
- this.holes = configService.get("dieline.holes", []);
1805
+ this.features = configService.get("dieline.features", []);
1743
1806
  configService.onAnyChange((e) => {
1744
1807
  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 || [];
1808
+ if (e.key === "dieline.features") {
1809
+ this.features = e.value || [];
1751
1810
  this.redraw();
1752
1811
  }
1753
1812
  });
@@ -1761,102 +1820,38 @@ var HoleTool = class {
1761
1820
  }
1762
1821
  contribute() {
1763
1822
  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
1823
  [import_core4.ContributionPointIds.COMMANDS]: [
1774
1824
  {
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;
1825
+ command: "addFeature",
1826
+ title: "Add Edge Feature",
1827
+ handler: (type = "subtract") => {
1828
+ return this.addFeature(type);
1808
1829
  }
1809
1830
  },
1810
1831
  {
1811
1832
  command: "addHole",
1812
1833
  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;
1834
+ handler: () => {
1835
+ return this.addFeature("subtract");
1836
+ }
1837
+ },
1838
+ {
1839
+ command: "addDoubleLayerHole",
1840
+ title: "Add Double Layer Hole",
1841
+ handler: () => {
1842
+ return this.addDoubleLayerHole();
1848
1843
  }
1849
1844
  },
1850
1845
  {
1851
- command: "clearHoles",
1852
- title: "Clear Holes",
1846
+ command: "clearFeatures",
1847
+ title: "Clear Features",
1853
1848
  handler: () => {
1854
1849
  var _a;
1855
1850
  const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1856
1851
  "ConfigurationService"
1857
1852
  );
1858
1853
  if (configService) {
1859
- configService.update("dieline.holes", []);
1854
+ configService.update("dieline.features", []);
1860
1855
  }
1861
1856
  return true;
1862
1857
  }
@@ -1864,6 +1859,88 @@ var HoleTool = class {
1864
1859
  ]
1865
1860
  };
1866
1861
  }
1862
+ addFeature(type) {
1863
+ var _a;
1864
+ if (!this.canvasService) return false;
1865
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1866
+ "ConfigurationService"
1867
+ );
1868
+ const unit = (configService == null ? void 0 : configService.get("dieline.unit", "mm")) || "mm";
1869
+ const defaultSize = Coordinate.convertUnit(10, "mm", unit);
1870
+ const newFeature = {
1871
+ id: Date.now().toString(),
1872
+ operation: type,
1873
+ target: "original",
1874
+ shape: "rect",
1875
+ x: 0.5,
1876
+ y: 0,
1877
+ // Top edge
1878
+ width: defaultSize,
1879
+ height: defaultSize,
1880
+ rotation: 0
1881
+ };
1882
+ if (configService) {
1883
+ const current = configService.get(
1884
+ "dieline.features",
1885
+ []
1886
+ );
1887
+ configService.update("dieline.features", [...current, newFeature]);
1888
+ }
1889
+ return true;
1890
+ }
1891
+ addDoubleLayerHole() {
1892
+ var _a;
1893
+ if (!this.canvasService) return false;
1894
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1895
+ "ConfigurationService"
1896
+ );
1897
+ const unit = (configService == null ? void 0 : configService.get("dieline.unit", "mm")) || "mm";
1898
+ const lugRadius = Coordinate.convertUnit(20, "mm", unit);
1899
+ const holeRadius = Coordinate.convertUnit(15, "mm", unit);
1900
+ const groupId = Date.now().toString();
1901
+ const timestamp = Date.now();
1902
+ const lug = {
1903
+ id: `${timestamp}-lug`,
1904
+ groupId,
1905
+ operation: "add",
1906
+ shape: "circle",
1907
+ x: 0.5,
1908
+ y: 0,
1909
+ radius: lugRadius,
1910
+ // 20mm
1911
+ rotation: 0
1912
+ };
1913
+ const hole = {
1914
+ id: `${timestamp}-hole`,
1915
+ groupId,
1916
+ operation: "subtract",
1917
+ shape: "circle",
1918
+ x: 0.5,
1919
+ y: 0,
1920
+ radius: holeRadius,
1921
+ // 15mm
1922
+ rotation: 0
1923
+ };
1924
+ if (configService) {
1925
+ const current = configService.get(
1926
+ "dieline.features",
1927
+ []
1928
+ );
1929
+ configService.update("dieline.features", [...current, lug, hole]);
1930
+ }
1931
+ return true;
1932
+ }
1933
+ getGeometryForFeature(geometry, feature) {
1934
+ if ((feature == null ? void 0 : feature.target) === "offset" && geometry.offset !== 0) {
1935
+ return {
1936
+ ...geometry,
1937
+ width: geometry.width + geometry.offset * 2,
1938
+ height: geometry.height + geometry.offset * 2,
1939
+ radius: geometry.radius === 0 ? 0 : Math.max(0, geometry.radius + geometry.offset)
1940
+ };
1941
+ }
1942
+ return geometry;
1943
+ }
1867
1944
  setup() {
1868
1945
  if (!this.canvasService || !this.context) return;
1869
1946
  const canvas = this.canvasService.canvas;
@@ -1871,10 +1948,7 @@ var HoleTool = class {
1871
1948
  this.handleDielineChange = (geometry) => {
1872
1949
  this.currentGeometry = geometry;
1873
1950
  this.redraw();
1874
- const changed = this.enforceConstraints();
1875
- if (changed) {
1876
- this.syncHolesToDieline();
1877
- }
1951
+ this.enforceConstraints();
1878
1952
  };
1879
1953
  this.context.eventBus.on(
1880
1954
  "dieline:geometry:change",
@@ -1884,69 +1958,101 @@ var HoleTool = class {
1884
1958
  const commandService = this.context.services.get("CommandService");
1885
1959
  if (commandService) {
1886
1960
  try {
1887
- const geometry = commandService.executeCommand("getGeometry");
1888
- if (geometry) {
1889
- Promise.resolve(geometry).then((g) => {
1961
+ Promise.resolve(commandService.executeCommand("getGeometry")).then(
1962
+ (g) => {
1890
1963
  if (g) {
1891
1964
  this.currentGeometry = g;
1892
- this.enforceConstraints();
1893
- this.initializeHoles();
1965
+ this.redraw();
1894
1966
  }
1895
- });
1896
- }
1967
+ }
1968
+ );
1897
1969
  } catch (e) {
1898
1970
  }
1899
1971
  }
1900
1972
  if (!this.handleMoving) {
1901
1973
  this.handleMoving = (e) => {
1902
- var _a, _b, _c, _d, _e;
1974
+ var _a, _b, _c, _d;
1903
1975
  const target = e.target;
1904
- if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "hole-marker") return;
1976
+ if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return;
1905
1977
  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
1978
+ let feature;
1979
+ if ((_b = target.data) == null ? void 0 : _b.isGroup) {
1980
+ const indices = (_c = target.data) == null ? void 0 : _c.indices;
1981
+ if (indices && indices.length > 0) {
1982
+ feature = this.features[indices[0]];
1983
+ }
1984
+ } else {
1985
+ const index = (_d = target.data) == null ? void 0 : _d.index;
1986
+ if (index !== void 0) {
1987
+ feature = this.features[index];
1988
+ }
1989
+ }
1990
+ const geometry = this.getGeometryForFeature(
1991
+ this.currentGeometry,
1992
+ feature
1924
1993
  );
1994
+ const p = new import_fabric4.Point(target.left, target.top);
1995
+ const markerStrokeWidth = (target.strokeWidth || 2) * (target.scaleX || 1);
1996
+ const minDim = Math.min(target.getScaledWidth(), target.getScaledHeight());
1997
+ const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
1998
+ const snapped = this.constrainPosition(p, geometry, limit);
1925
1999
  target.set({
1926
- left: newPos.x,
1927
- top: newPos.y
2000
+ left: snapped.x,
2001
+ top: snapped.y
1928
2002
  });
1929
2003
  };
1930
2004
  canvas.on("object:moving", this.handleMoving);
1931
2005
  }
1932
2006
  if (!this.handleModified) {
1933
2007
  this.handleModified = (e) => {
1934
- var _a;
2008
+ var _a, _b, _c, _d;
1935
2009
  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();
2010
+ if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return;
2011
+ if ((_b = target.data) == null ? void 0 : _b.isGroup) {
2012
+ const groupObj = target;
2013
+ const indices = (_c = groupObj.data) == null ? void 0 : _c.indices;
2014
+ if (!indices) return;
2015
+ const groupCenter = new import_fabric4.Point(groupObj.left, groupObj.top);
2016
+ const newFeatures = [...this.features];
2017
+ const { x, y } = this.currentGeometry;
2018
+ groupObj.getObjects().forEach((child, i) => {
2019
+ const originalIndex = indices[i];
2020
+ const feature = this.features[originalIndex];
2021
+ const geometry = this.getGeometryForFeature(
2022
+ this.currentGeometry,
2023
+ feature
2024
+ );
2025
+ const { width, height } = geometry;
2026
+ const layoutLeft = x - width / 2;
2027
+ const layoutTop = y - height / 2;
2028
+ const absX = groupCenter.x + (child.left || 0);
2029
+ const absY = groupCenter.y + (child.top || 0);
2030
+ const normalizedX = width > 0 ? (absX - layoutLeft) / width : 0.5;
2031
+ const normalizedY = height > 0 ? (absY - layoutTop) / height : 0.5;
2032
+ newFeatures[originalIndex] = {
2033
+ ...newFeatures[originalIndex],
2034
+ x: normalizedX,
2035
+ y: normalizedY
2036
+ };
2037
+ });
2038
+ this.features = newFeatures;
2039
+ const configService = (_d = this.context) == null ? void 0 : _d.services.get(
2040
+ "ConfigurationService"
2041
+ );
2042
+ if (configService) {
2043
+ this.isUpdatingConfig = true;
2044
+ try {
2045
+ configService.update("dieline.features", this.features);
2046
+ } finally {
2047
+ this.isUpdatingConfig = false;
2048
+ }
2049
+ }
2050
+ } else {
2051
+ this.syncFeatureFromCanvas(target);
1940
2052
  }
1941
2053
  };
1942
2054
  canvas.on("object:modified", this.handleModified);
1943
2055
  }
1944
- this.initializeHoles();
1945
- }
1946
- initializeHoles() {
1947
- if (!this.canvasService) return;
1948
- this.redraw();
1949
- this.syncHolesToDieline();
1950
2056
  }
1951
2057
  teardown() {
1952
2058
  if (!this.canvasService) return;
@@ -1968,357 +2074,259 @@ var HoleTool = class {
1968
2074
  }
1969
2075
  const objects = canvas.getObjects().filter((obj) => {
1970
2076
  var _a;
1971
- return ((_a = obj.data) == null ? void 0 : _a.type) === "hole-marker";
2077
+ return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
1972
2078
  });
1973
2079
  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
2080
  this.canvasService.requestRenderAll();
1984
2081
  }
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
- }
2091
- 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"
2102
- };
2082
+ constrainPosition(p, geometry, limit) {
2083
+ const nearest = getNearestPointOnDieline({ x: p.x, y: p.y }, {
2084
+ ...geometry,
2085
+ features: []
2103
2086
  });
2104
- this.holes = newHoles;
2105
- this.syncHolesToDieline();
2087
+ const dx = p.x - nearest.x;
2088
+ const dy = p.y - nearest.y;
2089
+ const dist = Math.sqrt(dx * dx + dy * dy);
2090
+ if (dist <= limit) {
2091
+ return { x: p.x, y: p.y };
2092
+ }
2093
+ const scale = limit / dist;
2094
+ return {
2095
+ x: nearest.x + dx * scale,
2096
+ y: nearest.y + dy * scale
2097
+ };
2106
2098
  }
2107
- syncHolesToDieline() {
2108
- if (!this.context || !this.canvasService) return;
2099
+ syncFeatureFromCanvas(target) {
2100
+ var _a;
2101
+ if (!this.currentGeometry || !this.context) return;
2102
+ const index = (_a = target.data) == null ? void 0 : _a.index;
2103
+ if (index === void 0 || index < 0 || index >= this.features.length)
2104
+ return;
2105
+ const feature = this.features[index];
2106
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
2107
+ const { width, height, x, y } = geometry;
2108
+ const left = x - width / 2;
2109
+ const top = y - height / 2;
2110
+ const normalizedX = width > 0 ? (target.left - left) / width : 0.5;
2111
+ const normalizedY = height > 0 ? (target.top - top) / height : 0.5;
2112
+ const updatedFeature = {
2113
+ ...feature,
2114
+ x: normalizedX,
2115
+ y: normalizedY
2116
+ // Could also update rotation if we allowed rotating markers
2117
+ };
2118
+ const newFeatures = [...this.features];
2119
+ newFeatures[index] = updatedFeature;
2120
+ this.features = newFeatures;
2109
2121
  const configService = this.context.services.get(
2110
2122
  "ConfigurationService"
2111
2123
  );
2112
2124
  if (configService) {
2113
2125
  this.isUpdatingConfig = true;
2114
2126
  try {
2115
- configService.update("dieline.holes", this.holes);
2127
+ configService.update("dieline.features", this.features);
2116
2128
  } finally {
2117
2129
  this.isUpdatingConfig = false;
2118
2130
  }
2119
2131
  }
2120
2132
  }
2121
2133
  redraw() {
2122
- if (!this.canvasService) return;
2134
+ if (!this.canvasService || !this.currentGeometry) return;
2123
2135
  const canvas = this.canvasService.canvas;
2124
- const { width, height } = canvas;
2136
+ const geometry = this.currentGeometry;
2125
2137
  const existing = canvas.getObjects().filter((obj) => {
2126
2138
  var _a;
2127
- return ((_a = obj.data) == null ? void 0 : _a.type) === "hole-marker";
2139
+ return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
2128
2140
  });
2129
2141
  existing.forEach((obj) => canvas.remove(obj));
2130
- const holes = this.holes;
2131
- if (!holes || holes.length === 0) {
2142
+ if (!this.features || this.features.length === 0) {
2132
2143
  this.canvasService.requestRenderAll();
2133
2144
  return;
2134
2145
  }
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
2146
+ const scale = geometry.scale || 1;
2147
+ const finalScale = scale;
2148
+ const groups = {};
2149
+ const singles = [];
2150
+ this.features.forEach((f, i) => {
2151
+ if (f.groupId) {
2152
+ if (!groups[f.groupId]) groups[f.groupId] = [];
2153
+ groups[f.groupId].push({ feature: f, index: i });
2154
+ } else {
2155
+ singles.push({ feature: f, index: i });
2156
+ }
2157
+ });
2158
+ const createMarkerShape = (feature, pos) => {
2159
+ const featureScale = scale;
2160
+ const visualWidth = (feature.width || 10) * featureScale;
2161
+ const visualHeight = (feature.height || 10) * featureScale;
2162
+ const visualRadius = (feature.radius || 0) * featureScale;
2163
+ const color = feature.color || (feature.operation === "add" ? "#00FF00" : "#FF0000");
2164
+ const strokeDash = feature.strokeDash || (feature.operation === "subtract" ? [4, 4] : void 0);
2165
+ let shape;
2166
+ if (feature.shape === "rect") {
2167
+ shape = new import_fabric4.Rect({
2168
+ width: visualWidth,
2169
+ height: visualHeight,
2170
+ rx: visualRadius,
2171
+ ry: visualRadius,
2172
+ fill: "transparent",
2173
+ stroke: color,
2174
+ strokeWidth: 2,
2175
+ strokeDashArray: strokeDash,
2176
+ originX: "center",
2177
+ originY: "center",
2178
+ left: pos.x,
2179
+ top: pos.y
2180
+ });
2181
+ } else {
2182
+ shape = new import_fabric4.Circle({
2183
+ radius: visualRadius || 5 * finalScale,
2184
+ fill: "transparent",
2185
+ stroke: color,
2186
+ strokeWidth: 2,
2187
+ strokeDashArray: strokeDash,
2188
+ originX: "center",
2189
+ originY: "center",
2190
+ left: pos.x,
2191
+ top: pos.y
2192
+ });
2193
+ }
2194
+ if (feature.rotation) {
2195
+ shape.rotate(feature.rotation);
2196
+ }
2197
+ return shape;
2142
2198
  };
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
2199
+ singles.forEach(({ feature, index }) => {
2200
+ const geometry2 = this.getGeometryForFeature(
2201
+ this.currentGeometry,
2202
+ feature
2158
2203
  );
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"
2204
+ const pos = resolveFeaturePosition(feature, geometry2);
2205
+ const marker = createMarkerShape(feature, pos);
2206
+ marker.set({
2207
+ selectable: true,
2208
+ hasControls: false,
2209
+ hasBorders: false,
2210
+ hoverCursor: "move",
2211
+ lockRotation: true,
2212
+ lockScalingX: true,
2213
+ lockScalingY: true,
2214
+ data: { type: "feature-marker", index, isGroup: false }
2175
2215
  });
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"
2216
+ marker.set("opacity", 0);
2217
+ marker.on("mouseover", () => {
2218
+ marker.set("opacity", 1);
2219
+ canvas.requestRenderAll();
2193
2220
  });
2194
- const holeGroup = new import_fabric4.Group([outerMarker, innerMarker], {
2195
- left: pos.x,
2196
- top: pos.y,
2197
- originX: "center",
2198
- originY: "center",
2221
+ marker.on("mouseout", () => {
2222
+ if (canvas.getActiveObject() !== marker) {
2223
+ marker.set("opacity", 0);
2224
+ canvas.requestRenderAll();
2225
+ }
2226
+ });
2227
+ marker.on("selected", () => {
2228
+ marker.set("opacity", 1);
2229
+ canvas.requestRenderAll();
2230
+ });
2231
+ marker.on("deselected", () => {
2232
+ marker.set("opacity", 0);
2233
+ canvas.requestRenderAll();
2234
+ });
2235
+ canvas.add(marker);
2236
+ canvas.bringObjectToFront(marker);
2237
+ });
2238
+ Object.keys(groups).forEach((groupId) => {
2239
+ const members = groups[groupId];
2240
+ if (members.length === 0) return;
2241
+ const shapes = members.map(({ feature }) => {
2242
+ const geometry2 = this.getGeometryForFeature(
2243
+ this.currentGeometry,
2244
+ feature
2245
+ );
2246
+ const pos = resolveFeaturePosition(feature, geometry2);
2247
+ return createMarkerShape(feature, pos);
2248
+ });
2249
+ const groupObj = new import_fabric4.Group(shapes, {
2199
2250
  selectable: true,
2200
2251
  hasControls: false,
2201
- // Don't allow resizing/rotating
2202
2252
  hasBorders: false,
2203
- subTargetCheck: false,
2204
- opacity: 0,
2205
- // Default hidden
2206
2253
  hoverCursor: "move",
2207
- data: { type: "hole-marker", index }
2254
+ lockRotation: true,
2255
+ lockScalingX: true,
2256
+ lockScalingY: true,
2257
+ subTargetCheck: true,
2258
+ // Allow events to pass through if needed, but we treat as one
2259
+ interactive: false,
2260
+ // Children not interactive
2261
+ // @ts-ignore
2262
+ data: {
2263
+ type: "feature-marker",
2264
+ isGroup: true,
2265
+ groupId,
2266
+ indices: members.map((m) => m.index)
2267
+ }
2208
2268
  });
2209
- holeGroup.name = "hole-marker";
2210
- holeGroup.on("mouseover", () => {
2211
- holeGroup.set("opacity", 1);
2269
+ groupObj.set("opacity", 0);
2270
+ groupObj.on("mouseover", () => {
2271
+ groupObj.set("opacity", 1);
2212
2272
  canvas.requestRenderAll();
2213
2273
  });
2214
- holeGroup.on("mouseout", () => {
2215
- if (canvas.getActiveObject() !== holeGroup) {
2216
- holeGroup.set("opacity", 0);
2274
+ groupObj.on("mouseout", () => {
2275
+ if (canvas.getActiveObject() !== groupObj) {
2276
+ groupObj.set("opacity", 0);
2217
2277
  canvas.requestRenderAll();
2218
2278
  }
2219
2279
  });
2220
- holeGroup.on("selected", () => {
2221
- holeGroup.set("opacity", 1);
2280
+ groupObj.on("selected", () => {
2281
+ groupObj.set("opacity", 1);
2222
2282
  canvas.requestRenderAll();
2223
2283
  });
2224
- holeGroup.on("deselected", () => {
2225
- holeGroup.set("opacity", 0);
2284
+ groupObj.on("deselected", () => {
2285
+ groupObj.set("opacity", 0);
2226
2286
  canvas.requestRenderAll();
2227
2287
  });
2228
- canvas.add(holeGroup);
2229
- canvas.bringObjectToFront(holeGroup);
2288
+ canvas.add(groupObj);
2289
+ canvas.bringObjectToFront(groupObj);
2230
2290
  });
2231
- const markers = canvas.getObjects().filter((o) => {
2232
- var _a;
2233
- return ((_a = o.data) == null ? void 0 : _a.type) === "hole-marker";
2234
- });
2235
- markers.forEach((m) => canvas.bringObjectToFront(m));
2236
2291
  this.canvasService.requestRenderAll();
2237
2292
  }
2238
2293
  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) => {
2294
+ if (!this.canvasService || !this.currentGeometry) return;
2295
+ const canvas = this.canvasService.canvas;
2296
+ const markers = canvas.getObjects().filter((obj) => {
2251
2297
  var _a;
2252
- return ((_a = obj.data) == null ? void 0 : _a.type) === "hole-marker";
2298
+ return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
2253
2299
  });
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);
2300
+ markers.forEach((marker) => {
2301
+ var _a, _b, _c;
2302
+ let feature;
2303
+ if ((_a = marker.data) == null ? void 0 : _a.isGroup) {
2304
+ const indices = (_b = marker.data) == null ? void 0 : _b.indices;
2305
+ if (indices && indices.length > 0) {
2306
+ feature = this.features[indices[0]];
2307
+ }
2308
+ } else {
2309
+ const index = (_c = marker.data) == null ? void 0 : _c.index;
2310
+ if (index !== void 0) {
2311
+ feature = this.features[index];
2312
+ }
2259
2313
  }
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
2314
+ const geometry = this.getGeometryForFeature(
2315
+ this.currentGeometry,
2316
+ feature
2276
2317
  );
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
- }
2318
+ const markerStrokeWidth = (marker.strokeWidth || 2) * (marker.scaleX || 1);
2319
+ const minDim = Math.min(marker.getScaledWidth(), marker.getScaledHeight());
2320
+ const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
2321
+ const snapped = this.constrainPosition(
2322
+ new import_fabric4.Point(marker.left, marker.top),
2323
+ geometry,
2324
+ limit
2325
+ );
2326
+ marker.set({ left: snapped.x, top: snapped.y });
2327
+ marker.setCoords();
2285
2328
  });
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);
2329
+ canvas.requestRenderAll();
2322
2330
  }
2323
2331
  };
2324
2332
 
@@ -3524,8 +3532,8 @@ var CanvasService = class {
3524
3532
  BackgroundTool,
3525
3533
  CanvasService,
3526
3534
  DielineTool,
3535
+ FeatureTool,
3527
3536
  FilmTool,
3528
- HoleTool,
3529
3537
  ImageTool,
3530
3538
  MirrorTool,
3531
3539
  RulerTool,