@pooder/kit 3.3.0 → 3.5.0

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