@pooder/kit 6.1.1 → 6.2.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.
@@ -14,7 +14,11 @@ import {
14
14
  Point,
15
15
  controlsUtils,
16
16
  } from "fabric";
17
- import { CanvasService, RenderLayoutRect, RenderObjectSpec } from "../../services";
17
+ import {
18
+ CanvasService,
19
+ RenderLayoutRect,
20
+ RenderObjectSpec,
21
+ } from "../../services";
18
22
  import { isDielineShape, normalizeShapeStyle } from "../dielineShape";
19
23
  import type { DielineShape, DielineShapeStyle } from "../dielineShape";
20
24
  import { generateDielinePath, getPathBounds } from "../geometry";
@@ -140,11 +144,38 @@ interface ImageControlDescriptor {
140
144
  create: () => Control;
141
145
  }
142
146
 
147
+ type SnapAxis = "x" | "y";
148
+ type SnapLineKind = "edge" | "center";
149
+ type SnapLineId =
150
+ | "frame-left"
151
+ | "frame-center-x"
152
+ | "frame-right"
153
+ | "frame-top"
154
+ | "frame-center-y"
155
+ | "frame-bottom";
156
+
157
+ interface SnapMatch {
158
+ axis: SnapAxis;
159
+ lineId: SnapLineId;
160
+ kind: SnapLineKind;
161
+ lineScene: number;
162
+ deltaScene: number;
163
+ }
164
+
165
+ interface SnapCandidate {
166
+ axis: SnapAxis;
167
+ lineId: SnapLineId;
168
+ kind: SnapLineKind;
169
+ lineScene: number;
170
+ deltaScene: number;
171
+ }
172
+
143
173
  const IMAGE_DEFAULT_CONTROL_CAPABILITIES: ImageControlCapability[] = [
144
174
  "rotate",
145
175
  "scale",
146
176
  ];
147
177
 
178
+ const IMAGE_MOVE_SNAP_THRESHOLD_PX = 6;
148
179
  const IMAGE_CONTROL_DESCRIPTORS: ImageControlDescriptor[] = [
149
180
  {
150
181
  key: "tl",
@@ -199,6 +230,14 @@ export class ImageTool implements Extension {
199
230
  private cropShapeHatchPatternKey?: string;
200
231
  private imageSpecs: RenderObjectSpec[] = [];
201
232
  private overlaySpecs: RenderObjectSpec[] = [];
233
+ private activeSnapX: SnapMatch | null = null;
234
+ private activeSnapY: SnapMatch | null = null;
235
+ private movingImageId: string | null = null;
236
+ private hasRenderedSnapGuides = false;
237
+ private canvasObjectMovingHandler?: (e: any) => void;
238
+ private canvasMouseUpHandler?: (e: any) => void;
239
+ private canvasBeforeRenderHandler?: () => void;
240
+ private canvasAfterRenderHandler?: () => void;
202
241
  private renderProducerDisposable?: { dispose: () => void };
203
242
  private readonly subscriptions = new SubscriptionBag();
204
243
  private imageControlsByCapabilityKey: Map<string, Record<string, Control>> =
@@ -247,9 +286,18 @@ export class ImageTool implements Extension {
247
286
  }),
248
287
  { priority: 300 },
249
288
  );
289
+ this.bindCanvasInteractionHandlers();
250
290
 
251
- this.subscriptions.on(context.eventBus, "tool:activated", this.onToolActivated);
252
- this.subscriptions.on(context.eventBus, "object:modified", this.onObjectModified);
291
+ this.subscriptions.on(
292
+ context.eventBus,
293
+ "tool:activated",
294
+ this.onToolActivated,
295
+ );
296
+ this.subscriptions.on(
297
+ context.eventBus,
298
+ "object:modified",
299
+ this.onObjectModified,
300
+ );
253
301
  this.subscriptions.on(
254
302
  context.eventBus,
255
303
  "selection:created",
@@ -328,6 +376,8 @@ export class ImageTool implements Extension {
328
376
  this.imageSpecs = [];
329
377
  this.overlaySpecs = [];
330
378
  this.imageControlsByCapabilityKey.clear();
379
+ this.endMoveSnapInteraction();
380
+ this.unbindCanvasInteractionHandlers();
331
381
 
332
382
  this.clearRenderedImages();
333
383
  this.renderProducerDisposable?.dispose();
@@ -347,6 +397,7 @@ export class ImageTool implements Extension {
347
397
  const before = this.isToolActive;
348
398
  this.syncToolActiveFromWorkbench(event.id);
349
399
  if (!this.isToolActive) {
400
+ this.endMoveSnapInteraction();
350
401
  this.setImageFocus(null, {
351
402
  syncCanvasSelection: true,
352
403
  skipRender: true,
@@ -396,6 +447,7 @@ export class ImageTool implements Extension {
396
447
  };
397
448
 
398
449
  private onSelectionCleared = () => {
450
+ this.endMoveSnapInteraction();
399
451
  this.setImageFocus(null, {
400
452
  syncCanvasSelection: false,
401
453
  skipRender: true,
@@ -405,6 +457,7 @@ export class ImageTool implements Extension {
405
457
  };
406
458
 
407
459
  private onSceneLayoutChanged = () => {
460
+ this.canvasService?.requestRenderAll();
408
461
  this.updateImages();
409
462
  };
410
463
 
@@ -412,6 +465,330 @@ export class ImageTool implements Extension {
412
465
  this.updateImages();
413
466
  };
414
467
 
468
+ private bindCanvasInteractionHandlers() {
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
+ };
481
+ this.canvasObjectMovingHandler = (e: any) => {
482
+ this.handleCanvasObjectMoving(e);
483
+ };
484
+ this.canvasBeforeRenderHandler = () => {
485
+ this.handleCanvasBeforeRender();
486
+ };
487
+ this.canvasAfterRenderHandler = () => {
488
+ this.handleCanvasAfterRender();
489
+ };
490
+ this.canvasService.canvas.on("mouse:up", this.canvasMouseUpHandler);
491
+ this.canvasService.canvas.on(
492
+ "object:moving",
493
+ this.canvasObjectMovingHandler,
494
+ );
495
+ this.canvasService.canvas.on(
496
+ "before:render",
497
+ this.canvasBeforeRenderHandler,
498
+ );
499
+ this.canvasService.canvas.on("after:render", this.canvasAfterRenderHandler);
500
+ }
501
+
502
+ private unbindCanvasInteractionHandlers() {
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;
526
+ this.canvasObjectMovingHandler = undefined;
527
+ this.canvasBeforeRenderHandler = undefined;
528
+ this.canvasAfterRenderHandler = undefined;
529
+ }
530
+
531
+ private getActiveImageTarget(target: any): any | null {
532
+ if (!this.isToolActive) return null;
533
+ if (!target) return null;
534
+ if (target?.data?.layerId !== IMAGE_OBJECT_LAYER_ID) return null;
535
+ if (typeof target?.data?.id !== "string") return null;
536
+ return target;
537
+ }
538
+
539
+ private getTargetBoundsScene(target: any): FrameRect | null {
540
+ if (!this.canvasService || !target) return null;
541
+ const rawBounds =
542
+ typeof target.getBoundingRect === "function"
543
+ ? target.getBoundingRect()
544
+ : {
545
+ left: Number(target.left || 0),
546
+ top: Number(target.top || 0),
547
+ width: Number(target.width || 0),
548
+ height: Number(target.height || 0),
549
+ };
550
+ return this.canvasService.toSceneRect({
551
+ left: Number(rawBounds.left || 0),
552
+ top: Number(rawBounds.top || 0),
553
+ width: Number(rawBounds.width || 0),
554
+ height: Number(rawBounds.height || 0),
555
+ });
556
+ }
557
+
558
+ private getSnapThresholdScene(px: number): number {
559
+ if (!this.canvasService) return px;
560
+ return this.canvasService.toSceneLength(px);
561
+ }
562
+
563
+ private pickSnapMatch(candidates: SnapCandidate[]): SnapMatch | null {
564
+ if (!candidates.length) return null;
565
+
566
+ const snapThreshold = this.getSnapThresholdScene(
567
+ IMAGE_MOVE_SNAP_THRESHOLD_PX,
568
+ );
569
+
570
+ let best: SnapCandidate | null = null;
571
+ candidates.forEach((candidate) => {
572
+ if (Math.abs(candidate.deltaScene) > snapThreshold) return;
573
+ if (!best || Math.abs(candidate.deltaScene) < Math.abs(best.deltaScene)) {
574
+ best = candidate;
575
+ }
576
+ });
577
+ return best;
578
+ }
579
+
580
+ private computeMoveSnapMatches(
581
+ bounds: FrameRect | null,
582
+ frame: FrameRect,
583
+ ): { x: SnapMatch | null; y: SnapMatch | null } {
584
+ if (!bounds || frame.width <= 0 || frame.height <= 0) {
585
+ return { x: null, y: null };
586
+ }
587
+
588
+ const xCandidates: SnapCandidate[] = [
589
+ {
590
+ axis: "x",
591
+ lineId: "frame-left",
592
+ kind: "edge",
593
+ lineScene: frame.left,
594
+ deltaScene: frame.left - bounds.left,
595
+ },
596
+ {
597
+ axis: "x",
598
+ lineId: "frame-center-x",
599
+ kind: "center",
600
+ lineScene: frame.left + frame.width / 2,
601
+ deltaScene:
602
+ frame.left + frame.width / 2 - (bounds.left + bounds.width / 2),
603
+ },
604
+ {
605
+ axis: "x",
606
+ lineId: "frame-right",
607
+ kind: "edge",
608
+ lineScene: frame.left + frame.width,
609
+ deltaScene: frame.left + frame.width - (bounds.left + bounds.width),
610
+ },
611
+ ];
612
+ const yCandidates: SnapCandidate[] = [
613
+ {
614
+ axis: "y",
615
+ lineId: "frame-top",
616
+ kind: "edge",
617
+ lineScene: frame.top,
618
+ deltaScene: frame.top - bounds.top,
619
+ },
620
+ {
621
+ axis: "y",
622
+ lineId: "frame-center-y",
623
+ kind: "center",
624
+ lineScene: frame.top + frame.height / 2,
625
+ deltaScene:
626
+ frame.top + frame.height / 2 - (bounds.top + bounds.height / 2),
627
+ },
628
+ {
629
+ axis: "y",
630
+ lineId: "frame-bottom",
631
+ kind: "edge",
632
+ lineScene: frame.top + frame.height,
633
+ deltaScene: frame.top + frame.height - (bounds.top + bounds.height),
634
+ },
635
+ ];
636
+
637
+ return {
638
+ x: this.pickSnapMatch(xCandidates),
639
+ y: this.pickSnapMatch(yCandidates),
640
+ };
641
+ }
642
+
643
+ private areSnapMatchesEqual(
644
+ a: SnapMatch | null,
645
+ b: SnapMatch | null,
646
+ ): boolean {
647
+ if (!a && !b) return true;
648
+ if (!a || !b) return false;
649
+ return a.lineId === b.lineId && a.axis === b.axis && a.kind === b.kind;
650
+ }
651
+
652
+ private updateSnapMatchState(
653
+ nextX: SnapMatch | null,
654
+ nextY: SnapMatch | null,
655
+ ) {
656
+ const changed =
657
+ !this.areSnapMatchesEqual(this.activeSnapX, nextX) ||
658
+ !this.areSnapMatchesEqual(this.activeSnapY, nextY);
659
+ this.activeSnapX = nextX;
660
+ this.activeSnapY = nextY;
661
+ if (changed) {
662
+ this.canvasService?.requestRenderAll();
663
+ }
664
+ }
665
+
666
+ private clearSnapPreview() {
667
+ this.activeSnapX = null;
668
+ this.activeSnapY = null;
669
+ this.hasRenderedSnapGuides = false;
670
+ this.canvasService?.requestRenderAll();
671
+ }
672
+
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() {
708
+ if (!this.canvasService) return;
709
+ if (!this.hasRenderedSnapGuides && !this.activeSnapX && !this.activeSnapY) {
710
+ return;
711
+ }
712
+ this.canvasService.canvas.clearContext(
713
+ this.canvasService.canvas.contextTop,
714
+ );
715
+ this.hasRenderedSnapGuides = false;
716
+ }
717
+
718
+ private drawSnapGuideLine(
719
+ from: { x: number; y: number },
720
+ to: { x: number; y: number },
721
+ ) {
722
+ if (!this.canvasService) return;
723
+ const ctx = this.canvasService.canvas.contextTop;
724
+ if (!ctx) return;
725
+ const color =
726
+ this.getConfig<string>("image.control.borderColor", "#1677ff") ||
727
+ "#1677ff";
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();
736
+ }
737
+
738
+ private handleCanvasAfterRender() {
739
+ if (!this.canvasService || !this.isImageEditingVisible()) {
740
+ return;
741
+ }
742
+
743
+ const frame = this.getFrameRect();
744
+ if (frame.width <= 0 || frame.height <= 0) {
745
+ return;
746
+ }
747
+ const frameScreen = this.getFrameRectScreen(frame);
748
+ let drew = false;
749
+
750
+ if (this.activeSnapX) {
751
+ const x = this.canvasService.toScreenPoint({
752
+ x: this.activeSnapX.lineScene,
753
+ y: frame.top,
754
+ }).x;
755
+ this.drawSnapGuideLine(
756
+ { x, y: frameScreen.top },
757
+ { x, y: frameScreen.top + frameScreen.height },
758
+ );
759
+ drew = true;
760
+ }
761
+
762
+ if (this.activeSnapY) {
763
+ const y = this.canvasService.toScreenPoint({
764
+ x: frame.left,
765
+ y: this.activeSnapY.lineScene,
766
+ }).y;
767
+ this.drawSnapGuideLine(
768
+ { x: frameScreen.left, y },
769
+ { x: frameScreen.left + frameScreen.width, y },
770
+ );
771
+ drew = true;
772
+ }
773
+ this.hasRenderedSnapGuides = drew;
774
+ }
775
+
776
+ private handleCanvasObjectMoving(e: any) {
777
+ const target = this.getActiveImageTarget(e?.target);
778
+ if (!target || !this.canvasService) return;
779
+ this.movingImageId =
780
+ typeof target?.data?.id === "string" ? target.data.id : null;
781
+
782
+ const frame = this.getFrameRect();
783
+ if (frame.width <= 0 || frame.height <= 0) {
784
+ this.endMoveSnapInteraction();
785
+ return;
786
+ }
787
+ const rawBounds = this.getTargetBoundsScene(target);
788
+ const matches = this.computeMoveSnapMatches(rawBounds, frame);
789
+ this.updateSnapMatchState(matches.x, matches.y);
790
+ }
791
+
415
792
  private syncToolActiveFromWorkbench(fallbackId?: string | null) {
416
793
  const wb = this.context?.services.get<WorkbenchService>("WorkbenchService");
417
794
  const activeId = wb?.activeToolId;
@@ -1176,33 +1553,9 @@ export class ImageTool implements Extension {
1176
1553
  originY: "top",
1177
1554
  fill: hatchFill,
1178
1555
  opacity: patternFill ? 1 : 0.8,
1179
- stroke: null,
1180
- fillRule: "evenodd",
1181
- selectable: false,
1182
- evented: false,
1183
- excludeFromExport: true,
1184
- objectCaching: false,
1185
- },
1186
- },
1187
- {
1188
- id: "image.cropShapePath",
1189
- type: "path",
1190
- data: { id: "image.cropShapePath", zIndex: 6 },
1191
- layout: {
1192
- reference: "custom",
1193
- referenceRect: frameRect,
1194
- alignX: "start",
1195
- alignY: "start",
1196
- offsetX: shapeBounds.x,
1197
- offsetY: shapeBounds.y,
1198
- },
1199
- props: {
1200
- pathData: shapePathData,
1201
- originX: "left",
1202
- originY: "top",
1203
- fill: "rgba(0,0,0,0)",
1204
1556
  stroke: "rgba(255, 0, 0, 0.9)",
1205
1557
  strokeWidth: this.canvasService?.toSceneLength(1) ?? 1,
1558
+ fillRule: "evenodd",
1206
1559
  selectable: false,
1207
1560
  evented: false,
1208
1561
  excludeFromExport: true,
@@ -1602,8 +1955,11 @@ export class ImageTool implements Extension {
1602
1955
  const id = target?.data?.id;
1603
1956
  const layerId = target?.data?.layerId;
1604
1957
  if (typeof id !== "string" || layerId !== IMAGE_OBJECT_LAYER_ID) return;
1605
-
1958
+ if (this.movingImageId === id) {
1959
+ this.applyMoveSnapToTarget(target);
1960
+ }
1606
1961
  const frame = this.getFrameRect();
1962
+ this.endMoveSnapInteraction();
1607
1963
  if (!frame.width || !frame.height) return;
1608
1964
 
1609
1965
  const center = target.getCenterPoint
@@ -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,