@pooder/kit 6.1.2 → 6.2.1

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.
Files changed (30) hide show
  1. package/.test-dist/src/extensions/background/BackgroundTool.js +177 -5
  2. package/.test-dist/src/extensions/constraintUtils.js +44 -0
  3. package/.test-dist/src/extensions/dieline/DielineTool.js +52 -409
  4. package/.test-dist/src/extensions/dieline/featureResolution.js +29 -0
  5. package/.test-dist/src/extensions/dieline/model.js +83 -0
  6. package/.test-dist/src/extensions/dieline/renderBuilder.js +227 -0
  7. package/.test-dist/src/extensions/feature/FeatureTool.js +156 -45
  8. package/.test-dist/src/extensions/featureCoordinates.js +21 -0
  9. package/.test-dist/src/extensions/featurePlacement.js +46 -0
  10. package/.test-dist/src/extensions/image/ImageTool.js +281 -25
  11. package/.test-dist/src/extensions/ruler/RulerTool.js +24 -1
  12. package/.test-dist/src/shared/constants/layers.js +3 -1
  13. package/.test-dist/tests/run.js +25 -0
  14. package/CHANGELOG.md +12 -0
  15. package/dist/index.d.mts +47 -13
  16. package/dist/index.d.ts +47 -13
  17. package/dist/index.js +1325 -977
  18. package/dist/index.mjs +1311 -966
  19. package/package.json +1 -1
  20. package/src/extensions/background/BackgroundTool.ts +264 -4
  21. package/src/extensions/dieline/DielineTool.ts +67 -548
  22. package/src/extensions/dieline/model.ts +165 -1
  23. package/src/extensions/dieline/renderBuilder.ts +301 -0
  24. package/src/extensions/feature/FeatureTool.ts +190 -47
  25. package/src/extensions/featureCoordinates.ts +35 -0
  26. package/src/extensions/featurePlacement.ts +118 -0
  27. package/src/extensions/image/ImageTool.ts +139 -157
  28. package/src/extensions/ruler/RulerTool.ts +24 -2
  29. package/src/shared/constants/layers.ts +2 -0
  30. package/tests/run.ts +37 -0
@@ -10,7 +10,6 @@ import {
10
10
  Canvas as FabricCanvas,
11
11
  Control,
12
12
  Image as FabricImage,
13
- Path as FabricPath,
14
13
  Pattern,
15
14
  Point,
16
15
  controlsUtils,
@@ -177,9 +176,6 @@ const IMAGE_DEFAULT_CONTROL_CAPABILITIES: ImageControlCapability[] = [
177
176
  ];
178
177
 
179
178
  const IMAGE_MOVE_SNAP_THRESHOLD_PX = 6;
180
- const IMAGE_MOVE_SNAP_RELEASE_THRESHOLD_PX = 10;
181
- const IMAGE_SNAP_GUIDE_LAYER_ID = "image.snapGuide";
182
-
183
179
  const IMAGE_CONTROL_DESCRIPTORS: ImageControlDescriptor[] = [
184
180
  {
185
181
  key: "tl",
@@ -236,9 +232,12 @@ export class ImageTool implements Extension {
236
232
  private overlaySpecs: RenderObjectSpec[] = [];
237
233
  private activeSnapX: SnapMatch | null = null;
238
234
  private activeSnapY: SnapMatch | null = null;
239
- private snapGuideXObject?: FabricPath;
240
- private snapGuideYObject?: FabricPath;
235
+ private movingImageId: string | null = null;
236
+ private hasRenderedSnapGuides = false;
241
237
  private canvasObjectMovingHandler?: (e: any) => void;
238
+ private canvasMouseUpHandler?: (e: any) => void;
239
+ private canvasBeforeRenderHandler?: () => void;
240
+ private canvasAfterRenderHandler?: () => void;
242
241
  private renderProducerDisposable?: { dispose: () => void };
243
242
  private readonly subscriptions = new SubscriptionBag();
244
243
  private imageControlsByCapabilityKey: Map<string, Record<string, Control>> =
@@ -377,7 +376,7 @@ export class ImageTool implements Extension {
377
376
  this.imageSpecs = [];
378
377
  this.overlaySpecs = [];
379
378
  this.imageControlsByCapabilityKey.clear();
380
- this.clearSnapGuides();
379
+ this.endMoveSnapInteraction();
381
380
  this.unbindCanvasInteractionHandlers();
382
381
 
383
382
  this.clearRenderedImages();
@@ -398,7 +397,7 @@ export class ImageTool implements Extension {
398
397
  const before = this.isToolActive;
399
398
  this.syncToolActiveFromWorkbench(event.id);
400
399
  if (!this.isToolActive) {
401
- this.clearSnapGuides();
400
+ this.endMoveSnapInteraction();
402
401
  this.setImageFocus(null, {
403
402
  syncCanvasSelection: true,
404
403
  skipRender: true,
@@ -448,7 +447,7 @@ export class ImageTool implements Extension {
448
447
  };
449
448
 
450
449
  private onSelectionCleared = () => {
451
- this.clearSnapGuides();
450
+ this.endMoveSnapInteraction();
452
451
  this.setImageFocus(null, {
453
452
  syncCanvasSelection: false,
454
453
  skipRender: true,
@@ -458,7 +457,7 @@ export class ImageTool implements Extension {
458
457
  };
459
458
 
460
459
  private onSceneLayoutChanged = () => {
461
- this.updateSnapGuideVisuals();
460
+ this.canvasService?.requestRenderAll();
462
461
  this.updateImages();
463
462
  };
464
463
 
@@ -468,22 +467,65 @@ export class ImageTool implements Extension {
468
467
 
469
468
  private bindCanvasInteractionHandlers() {
470
469
  if (!this.canvasService || this.canvasObjectMovingHandler) return;
470
+ this.canvasMouseUpHandler = (e: any) => {
471
+ const target = this.getActiveImageTarget(e?.target);
472
+ if (
473
+ target &&
474
+ typeof target?.data?.id === "string" &&
475
+ target.data.id === this.movingImageId
476
+ ) {
477
+ this.applyMoveSnapToTarget(target);
478
+ }
479
+ this.endMoveSnapInteraction();
480
+ };
471
481
  this.canvasObjectMovingHandler = (e: any) => {
472
482
  this.handleCanvasObjectMoving(e);
473
483
  };
484
+ this.canvasBeforeRenderHandler = () => {
485
+ this.handleCanvasBeforeRender();
486
+ };
487
+ this.canvasAfterRenderHandler = () => {
488
+ this.handleCanvasAfterRender();
489
+ };
490
+ this.canvasService.canvas.on("mouse:up", this.canvasMouseUpHandler);
474
491
  this.canvasService.canvas.on(
475
492
  "object:moving",
476
493
  this.canvasObjectMovingHandler,
477
494
  );
495
+ this.canvasService.canvas.on(
496
+ "before:render",
497
+ this.canvasBeforeRenderHandler,
498
+ );
499
+ this.canvasService.canvas.on("after:render", this.canvasAfterRenderHandler);
478
500
  }
479
501
 
480
502
  private unbindCanvasInteractionHandlers() {
481
- if (!this.canvasService || !this.canvasObjectMovingHandler) return;
482
- this.canvasService.canvas.off(
483
- "object:moving",
484
- this.canvasObjectMovingHandler,
485
- );
503
+ if (!this.canvasService) return;
504
+ if (this.canvasMouseUpHandler) {
505
+ this.canvasService.canvas.off("mouse:up", this.canvasMouseUpHandler);
506
+ }
507
+ if (this.canvasObjectMovingHandler) {
508
+ this.canvasService.canvas.off(
509
+ "object:moving",
510
+ this.canvasObjectMovingHandler,
511
+ );
512
+ }
513
+ if (this.canvasBeforeRenderHandler) {
514
+ this.canvasService.canvas.off(
515
+ "before:render",
516
+ this.canvasBeforeRenderHandler,
517
+ );
518
+ }
519
+ if (this.canvasAfterRenderHandler) {
520
+ this.canvasService.canvas.off(
521
+ "after:render",
522
+ this.canvasAfterRenderHandler,
523
+ );
524
+ }
525
+ this.canvasMouseUpHandler = undefined;
486
526
  this.canvasObjectMovingHandler = undefined;
527
+ this.canvasBeforeRenderHandler = undefined;
528
+ this.canvasAfterRenderHandler = undefined;
487
529
  }
488
530
 
489
531
  private getActiveImageTarget(target: any): any | null {
@@ -518,28 +560,12 @@ export class ImageTool implements Extension {
518
560
  return this.canvasService.toSceneLength(px);
519
561
  }
520
562
 
521
- private pickSnapMatch(
522
- candidates: SnapCandidate[],
523
- previous: SnapMatch | null,
524
- ): SnapMatch | null {
563
+ private pickSnapMatch(candidates: SnapCandidate[]): SnapMatch | null {
525
564
  if (!candidates.length) return null;
526
565
 
527
566
  const snapThreshold = this.getSnapThresholdScene(
528
567
  IMAGE_MOVE_SNAP_THRESHOLD_PX,
529
568
  );
530
- const releaseThreshold = this.getSnapThresholdScene(
531
- IMAGE_MOVE_SNAP_RELEASE_THRESHOLD_PX,
532
- );
533
-
534
- if (previous) {
535
- const sticky = candidates.find((candidate) => {
536
- return (
537
- candidate.lineId === previous.lineId &&
538
- Math.abs(candidate.deltaScene) <= releaseThreshold
539
- );
540
- });
541
- if (sticky) return sticky;
542
- }
543
569
 
544
570
  let best: SnapCandidate | null = null;
545
571
  candidates.forEach((candidate) => {
@@ -552,10 +578,9 @@ export class ImageTool implements Extension {
552
578
  }
553
579
 
554
580
  private computeMoveSnapMatches(
555
- target: any,
581
+ bounds: FrameRect | null,
556
582
  frame: FrameRect,
557
583
  ): { x: SnapMatch | null; y: SnapMatch | null } {
558
- const bounds = this.getTargetBoundsScene(target);
559
584
  if (!bounds || frame.width <= 0 || frame.height <= 0) {
560
585
  return { x: null, y: null };
561
586
  }
@@ -610,8 +635,8 @@ export class ImageTool implements Extension {
610
635
  ];
611
636
 
612
637
  return {
613
- x: this.pickSnapMatch(xCandidates, this.activeSnapX),
614
- y: this.pickSnapMatch(yCandidates, this.activeSnapY),
638
+ x: this.pickSnapMatch(xCandidates),
639
+ y: this.pickSnapMatch(yCandidates),
615
640
  };
616
641
  }
617
642
 
@@ -634,93 +659,104 @@ export class ImageTool implements Extension {
634
659
  this.activeSnapX = nextX;
635
660
  this.activeSnapY = nextY;
636
661
  if (changed) {
637
- this.updateSnapGuideVisuals();
662
+ this.canvasService?.requestRenderAll();
638
663
  }
639
664
  }
640
665
 
641
- private clearSnapGuides() {
666
+ private clearSnapPreview() {
642
667
  this.activeSnapX = null;
643
668
  this.activeSnapY = null;
644
- this.removeSnapGuideObject("x");
645
- this.removeSnapGuideObject("y");
669
+ this.hasRenderedSnapGuides = false;
646
670
  this.canvasService?.requestRenderAll();
647
671
  }
648
672
 
649
- private removeSnapGuideObject(axis: SnapAxis) {
673
+ private endMoveSnapInteraction() {
674
+ this.movingImageId = null;
675
+ this.clearSnapPreview();
676
+ }
677
+
678
+ private applyMoveSnapToTarget(target: any): {
679
+ x: SnapMatch | null;
680
+ y: SnapMatch | null;
681
+ } {
682
+ if (!this.canvasService) {
683
+ return { x: null, y: null };
684
+ }
685
+ const frame = this.getFrameRect();
686
+ if (frame.width <= 0 || frame.height <= 0) {
687
+ return { x: null, y: null };
688
+ }
689
+ const bounds = this.getTargetBoundsScene(target);
690
+ const matches = this.computeMoveSnapMatches(bounds, frame);
691
+ const deltaScreenX = this.canvasService.toScreenLength(
692
+ matches.x?.deltaScene ?? 0,
693
+ );
694
+ const deltaScreenY = this.canvasService.toScreenLength(
695
+ matches.y?.deltaScene ?? 0,
696
+ );
697
+ if (deltaScreenX || deltaScreenY) {
698
+ target.set({
699
+ left: Number(target.left || 0) + deltaScreenX,
700
+ top: Number(target.top || 0) + deltaScreenY,
701
+ });
702
+ target.setCoords();
703
+ }
704
+ return matches;
705
+ }
706
+
707
+ private handleCanvasBeforeRender() {
650
708
  if (!this.canvasService) return;
651
- const canvas = this.canvasService.canvas;
652
- const current =
653
- axis === "x" ? this.snapGuideXObject : this.snapGuideYObject;
654
- if (!current) return;
655
- canvas.remove(current);
656
- if (axis === "x") {
657
- this.snapGuideXObject = undefined;
709
+ if (!this.hasRenderedSnapGuides && !this.activeSnapX && !this.activeSnapY) {
658
710
  return;
659
711
  }
660
- this.snapGuideYObject = undefined;
712
+ this.canvasService.canvas.clearContext(
713
+ this.canvasService.canvas.contextTop,
714
+ );
715
+ this.hasRenderedSnapGuides = false;
661
716
  }
662
717
 
663
- private createOrUpdateSnapGuideObject(axis: SnapAxis, pathData: string) {
718
+ private drawSnapGuideLine(
719
+ from: { x: number; y: number },
720
+ to: { x: number; y: number },
721
+ ) {
664
722
  if (!this.canvasService) return;
665
- const canvas = this.canvasService.canvas;
723
+ const ctx = this.canvasService.canvas.contextTop;
724
+ if (!ctx) return;
666
725
  const color =
667
726
  this.getConfig<string>("image.control.borderColor", "#1677ff") ||
668
727
  "#1677ff";
669
- const strokeWidth = 1;
670
- this.removeSnapGuideObject(axis);
671
-
672
- const created = new FabricPath(pathData, {
673
- originX: "left",
674
- originY: "top",
675
- fill: "rgba(0,0,0,0)",
676
- stroke: color,
677
- strokeWidth,
678
- selectable: false,
679
- evented: false,
680
- excludeFromExport: true,
681
- objectCaching: false,
682
- data: {
683
- id: `${IMAGE_SNAP_GUIDE_LAYER_ID}.${axis}`,
684
- layerId: IMAGE_SNAP_GUIDE_LAYER_ID,
685
- type: "image-snap-guide",
686
- },
687
- } as any);
688
- created.setCoords();
689
- canvas.add(created);
690
- canvas.bringObjectToFront(created);
691
- if (axis === "x") {
692
- this.snapGuideXObject = created;
693
- return;
694
- }
695
- this.snapGuideYObject = created;
728
+ ctx.save();
729
+ ctx.strokeStyle = color;
730
+ ctx.lineWidth = 1;
731
+ ctx.beginPath();
732
+ ctx.moveTo(from.x, from.y);
733
+ ctx.lineTo(to.x, to.y);
734
+ ctx.stroke();
735
+ ctx.restore();
696
736
  }
697
737
 
698
- private updateSnapGuideVisuals() {
738
+ private handleCanvasAfterRender() {
699
739
  if (!this.canvasService || !this.isImageEditingVisible()) {
700
- this.removeSnapGuideObject("x");
701
- this.removeSnapGuideObject("y");
702
740
  return;
703
741
  }
704
742
 
705
743
  const frame = this.getFrameRect();
706
744
  if (frame.width <= 0 || frame.height <= 0) {
707
- this.removeSnapGuideObject("x");
708
- this.removeSnapGuideObject("y");
709
745
  return;
710
746
  }
711
747
  const frameScreen = this.getFrameRectScreen(frame);
748
+ let drew = false;
712
749
 
713
750
  if (this.activeSnapX) {
714
751
  const x = this.canvasService.toScreenPoint({
715
752
  x: this.activeSnapX.lineScene,
716
753
  y: frame.top,
717
754
  }).x;
718
- this.createOrUpdateSnapGuideObject(
719
- "x",
720
- `M ${x} ${frameScreen.top} L ${x} ${frameScreen.top + frameScreen.height}`,
755
+ this.drawSnapGuideLine(
756
+ { x, y: frameScreen.top },
757
+ { x, y: frameScreen.top + frameScreen.height },
721
758
  );
722
- } else {
723
- this.removeSnapGuideObject("x");
759
+ drew = true;
724
760
  }
725
761
 
726
762
  if (this.activeSnapY) {
@@ -728,60 +764,31 @@ export class ImageTool implements Extension {
728
764
  x: frame.left,
729
765
  y: this.activeSnapY.lineScene,
730
766
  }).y;
731
- this.createOrUpdateSnapGuideObject(
732
- "y",
733
- `M ${frameScreen.left} ${y} L ${frameScreen.left + frameScreen.width} ${y}`,
767
+ this.drawSnapGuideLine(
768
+ { x: frameScreen.left, y },
769
+ { x: frameScreen.left + frameScreen.width, y },
734
770
  );
735
- } else {
736
- this.removeSnapGuideObject("y");
771
+ drew = true;
737
772
  }
738
-
739
- this.canvasService.requestRenderAll();
773
+ this.hasRenderedSnapGuides = drew;
740
774
  }
741
775
 
742
776
  private handleCanvasObjectMoving(e: any) {
743
777
  const target = this.getActiveImageTarget(e?.target);
744
778
  if (!target || !this.canvasService) return;
779
+ this.movingImageId =
780
+ typeof target?.data?.id === "string" ? target.data.id : null;
745
781
 
746
782
  const frame = this.getFrameRect();
747
783
  if (frame.width <= 0 || frame.height <= 0) {
748
- this.clearSnapGuides();
784
+ this.endMoveSnapInteraction();
749
785
  return;
750
786
  }
751
-
752
- const matches = this.computeMoveSnapMatches(target, frame);
753
- const deltaX = matches.x?.deltaScene ?? 0;
754
- const deltaY = matches.y?.deltaScene ?? 0;
755
-
756
- if (deltaX || deltaY) {
757
- target.set({
758
- left:
759
- Number(target.left || 0) + this.canvasService.toScreenLength(deltaX),
760
- top:
761
- Number(target.top || 0) + this.canvasService.toScreenLength(deltaY),
762
- });
763
- target.setCoords();
764
- }
765
-
787
+ const rawBounds = this.getTargetBoundsScene(target);
788
+ const matches = this.computeMoveSnapMatches(rawBounds, frame);
766
789
  this.updateSnapMatchState(matches.x, matches.y);
767
790
  }
768
791
 
769
- private applySnapMatchesToTarget(
770
- target: any,
771
- matches: { x: SnapMatch | null; y: SnapMatch | null },
772
- ) {
773
- if (!this.canvasService || !target) return;
774
- const deltaX = matches.x?.deltaScene ?? 0;
775
- const deltaY = matches.y?.deltaScene ?? 0;
776
- if (!deltaX && !deltaY) return;
777
-
778
- target.set({
779
- left: Number(target.left || 0) + this.canvasService.toScreenLength(deltaX),
780
- top: Number(target.top || 0) + this.canvasService.toScreenLength(deltaY),
781
- });
782
- target.setCoords();
783
- }
784
-
785
792
  private syncToolActiveFromWorkbench(fallbackId?: string | null) {
786
793
  const wb = this.context?.services.get<WorkbenchService>("WorkbenchService");
787
794
  const activeId = wb?.activeToolId;
@@ -1546,33 +1553,9 @@ export class ImageTool implements Extension {
1546
1553
  originY: "top",
1547
1554
  fill: hatchFill,
1548
1555
  opacity: patternFill ? 1 : 0.8,
1549
- stroke: null,
1550
- fillRule: "evenodd",
1551
- selectable: false,
1552
- evented: false,
1553
- excludeFromExport: true,
1554
- objectCaching: false,
1555
- },
1556
- },
1557
- {
1558
- id: "image.cropShapePath",
1559
- type: "path",
1560
- data: { id: "image.cropShapePath", zIndex: 6 },
1561
- layout: {
1562
- reference: "custom",
1563
- referenceRect: frameRect,
1564
- alignX: "start",
1565
- alignY: "start",
1566
- offsetX: shapeBounds.x,
1567
- offsetY: shapeBounds.y,
1568
- },
1569
- props: {
1570
- pathData: shapePathData,
1571
- originX: "left",
1572
- originY: "top",
1573
- fill: "rgba(0,0,0,0)",
1574
1556
  stroke: "rgba(255, 0, 0, 0.9)",
1575
1557
  strokeWidth: this.canvasService?.toSceneLength(1) ?? 1,
1558
+ fillRule: "evenodd",
1576
1559
  selectable: false,
1577
1560
  evented: false,
1578
1561
  excludeFromExport: true,
@@ -1959,7 +1942,6 @@ export class ImageTool implements Extension {
1959
1942
  isImageSelectionActive: this.isImageSelectionActive,
1960
1943
  focusedImageId: this.focusedImageId,
1961
1944
  });
1962
- this.updateSnapGuideVisuals();
1963
1945
  this.canvasService.requestRenderAll();
1964
1946
  }
1965
1947
 
@@ -1973,12 +1955,12 @@ export class ImageTool implements Extension {
1973
1955
  const id = target?.data?.id;
1974
1956
  const layerId = target?.data?.layerId;
1975
1957
  if (typeof id !== "string" || layerId !== IMAGE_OBJECT_LAYER_ID) return;
1976
-
1958
+ if (this.movingImageId === id) {
1959
+ this.applyMoveSnapToTarget(target);
1960
+ }
1977
1961
  const frame = this.getFrameRect();
1962
+ this.endMoveSnapInteraction();
1978
1963
  if (!frame.width || !frame.height) return;
1979
- const matches = this.computeMoveSnapMatches(target, frame);
1980
- this.applySnapMatchesToTarget(target, matches);
1981
- this.clearSnapGuides();
1982
1964
 
1983
1965
  const center = target.getCenterPoint
1984
1966
  ? target.getCenterPoint()
@@ -20,11 +20,12 @@ const MIN_ARROW_SIZE = 4;
20
20
  const THICKNESS_TO_STROKE_WIDTH_RATIO = 20;
21
21
 
22
22
  const DEFAULT_THICKNESS = 20;
23
- const DEFAULT_GAP = 45;
23
+ const DEFAULT_GAP = 65;
24
24
  const DEFAULT_FONT_SIZE = 10;
25
25
  const DEFAULT_BACKGROUND_COLOR = "#f0f0f0";
26
26
  const DEFAULT_TEXT_COLOR = "#333333";
27
27
  const DEFAULT_LINE_COLOR = "#999999";
28
+ const RULER_DEBUG_KEY = "ruler.debug";
28
29
 
29
30
  const RULER_THICKNESS_MIN = 10;
30
31
  const RULER_THICKNESS_MAX = 100;
@@ -48,6 +49,7 @@ export class RulerTool implements Extension {
48
49
  private textColor = DEFAULT_TEXT_COLOR;
49
50
  private lineColor = DEFAULT_LINE_COLOR;
50
51
  private fontSize = DEFAULT_FONT_SIZE;
52
+ private debugEnabled = false;
51
53
  private renderSeq = 0;
52
54
  private readonly numericProps = new Set(["thickness", "gap", "fontSize"]);
53
55
  private specs: RenderObjectSpec[] = [];
@@ -112,7 +114,14 @@ export class RulerTool implements Extension {
112
114
  this.syncConfig(configService);
113
115
  configService.onAnyChange((e: { key: string; value: any }) => {
114
116
  let shouldUpdate = false;
115
- if (e.key.startsWith("ruler.")) {
117
+ if (e.key === RULER_DEBUG_KEY) {
118
+ this.debugEnabled = e.value === true;
119
+ this.log("config:update", {
120
+ key: e.key,
121
+ raw: e.value,
122
+ normalized: this.debugEnabled,
123
+ });
124
+ } else if (e.key.startsWith("ruler.")) {
116
125
  const prop = e.key.split(".")[1];
117
126
  if (prop && prop in this) {
118
127
  if (this.numericProps.has(prop)) {
@@ -203,6 +212,12 @@ export class RulerTool implements Extension {
203
212
  max: RULER_FONT_SIZE_MAX,
204
213
  default: DEFAULT_FONT_SIZE,
205
214
  },
215
+ {
216
+ id: RULER_DEBUG_KEY,
217
+ type: "boolean",
218
+ label: "Ruler Debug Log",
219
+ default: false,
220
+ },
206
221
  ] as ConfigurationContribution[],
207
222
  [ContributionPointIds.COMMANDS]: [
208
223
  {
@@ -249,7 +264,12 @@ export class RulerTool implements Extension {
249
264
  };
250
265
  }
251
266
 
267
+ private isDebugEnabled(): boolean {
268
+ return this.debugEnabled;
269
+ }
270
+
252
271
  private log(step: string, payload?: Record<string, unknown>) {
272
+ if (!this.isDebugEnabled()) return;
253
273
  if (payload) {
254
274
  console.debug(`[RulerTool] ${step}`, payload);
255
275
  return;
@@ -279,6 +299,8 @@ export class RulerTool implements Extension {
279
299
  configService.get("ruler.fontSize", this.fontSize),
280
300
  DEFAULT_FONT_SIZE,
281
301
  );
302
+ this.debugEnabled =
303
+ configService.get(RULER_DEBUG_KEY, this.debugEnabled) === true;
282
304
 
283
305
  this.log("config:loaded", {
284
306
  thickness: this.thickness,
@@ -5,6 +5,7 @@ export const WHITE_INK_OBJECT_LAYER_ID = "white-ink.user";
5
5
  export const WHITE_INK_COVER_LAYER_ID = "white-ink.cover";
6
6
  export const WHITE_INK_OVERLAY_LAYER_ID = "white-ink.overlay";
7
7
  export const DIELINE_LAYER_ID = "dieline-overlay";
8
+ export const FEATURE_DIELINE_LAYER_ID = "feature-dieline-overlay";
8
9
  export const FEATURE_OVERLAY_LAYER_ID = "feature-overlay";
9
10
  export const RULER_LAYER_ID = "ruler-overlay";
10
11
  export const FILM_LAYER_ID = "overlay";
@@ -17,6 +18,7 @@ export const LAYER_IDS = {
17
18
  whiteInkCover: WHITE_INK_COVER_LAYER_ID,
18
19
  whiteInkOverlay: WHITE_INK_OVERLAY_LAYER_ID,
19
20
  dieline: DIELINE_LAYER_ID,
21
+ featureDieline: FEATURE_DIELINE_LAYER_ID,
20
22
  featureOverlay: FEATURE_OVERLAY_LAYER_ID,
21
23
  rulerOverlay: RULER_LAYER_ID,
22
24
  filmOverlay: FILM_LAYER_ID,
package/tests/run.ts CHANGED
@@ -21,6 +21,10 @@ import { createWhiteInkCommands } from "../src/extensions/white-ink/commands";
21
21
  import { createWhiteInkConfigurations } from "../src/extensions/white-ink/config";
22
22
  import { createDielineCommands } from "../src/extensions/dieline/commands";
23
23
  import { createDielineConfigurations } from "../src/extensions/dieline/config";
24
+ import {
25
+ normalizePointInGeometry,
26
+ resolveFeaturePosition,
27
+ } from "../src/extensions/featureCoordinates";
24
28
 
25
29
  function assert(condition: unknown, message: string) {
26
30
  if (!condition) throw new Error(message);
@@ -120,6 +124,38 @@ function testEdgeScale() {
120
124
  assert(height === 80, `expected height 80, got ${height}`);
121
125
  }
122
126
 
127
+ function testFeaturePlacementProjection() {
128
+ const trimGeometry = {
129
+ x: 100,
130
+ y: 120,
131
+ width: 120,
132
+ height: 180,
133
+ };
134
+ const cutGeometry = {
135
+ x: 100,
136
+ y: 120,
137
+ width: 150,
138
+ height: 210,
139
+ };
140
+ const trimFeature = {
141
+ x: 0.82,
142
+ y: 0.68,
143
+ };
144
+
145
+ const trimCenter = resolveFeaturePosition(trimFeature, trimGeometry);
146
+ const cutFeature = normalizePointInGeometry(trimCenter, cutGeometry);
147
+ const cutCenter = resolveFeaturePosition(cutFeature, cutGeometry);
148
+
149
+ assert(
150
+ Math.abs(trimCenter.x - cutCenter.x) < 1e-6,
151
+ `expected projected feature x to stay fixed, got ${trimCenter.x} vs ${cutCenter.x}`,
152
+ );
153
+ assert(
154
+ Math.abs(trimCenter.y - cutCenter.y) < 1e-6,
155
+ `expected projected feature y to stay fixed, got ${trimCenter.y} vs ${cutCenter.y}`,
156
+ );
157
+ }
158
+
123
159
  function testVisibilityDsl() {
124
160
  const layers = new Map([
125
161
  ["ruler-overlay", { exists: true, objectCount: 2 }],
@@ -396,6 +432,7 @@ function main() {
396
432
  testBridgeSelection();
397
433
  testMaskOps();
398
434
  testEdgeScale();
435
+ testFeaturePlacementProjection();
399
436
  testVisibilityDsl();
400
437
  testContributionCompatibility();
401
438
  console.log("ok");