@pooder/kit 3.5.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/dieline.ts CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  generateMaskPath,
15
15
  generateBleedZonePath,
16
16
  getPathBounds,
17
- EdgeFeature,
17
+ DielineFeature,
18
18
  } from "./geometry";
19
19
 
20
20
  export interface DielineGeometry {
@@ -52,7 +52,7 @@ export interface DielineState {
52
52
  insideColor: string;
53
53
  outsideColor: string;
54
54
  showBleedLines: boolean;
55
- features: EdgeFeature[];
55
+ features: DielineFeature[];
56
56
  pathData?: string;
57
57
  }
58
58
 
@@ -512,12 +512,8 @@ export class DielineTool implements Extension {
512
512
  };
513
513
  });
514
514
 
515
- const originalFeatures = absoluteFeatures.filter(
516
- (f) => !f.target || f.target === "original" || f.target === "both",
517
- );
518
- const offsetFeatures = absoluteFeatures.filter(
519
- (f) => f.target === "offset" || f.target === "both",
520
- );
515
+ // Split features into Cut (Physical) and Visual (All)
516
+ const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
521
517
 
522
518
  // 1. Draw Mask (Outside)
523
519
  const cutW = Math.max(0, visualWidth + visualOffset * 2);
@@ -525,10 +521,6 @@ export class DielineTool implements Extension {
525
521
  const cutR =
526
522
  visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
527
523
 
528
- // If no bleed offset, mask should match the original dieline (including its features)
529
- // If bleed offset exists (positive or negative), mask matches bleed line (which only includes offset features)
530
- const maskFeatures = visualOffset !== 0 ? offsetFeatures : originalFeatures;
531
-
532
524
  // Use Paper.js to generate the complex mask path
533
525
  const maskPathData = generateMaskPath({
534
526
  canvasWidth: canvasW,
@@ -539,7 +531,7 @@ export class DielineTool implements Extension {
539
531
  radius: cutR,
540
532
  x: cx,
541
533
  y: cy,
542
- features: maskFeatures,
534
+ features: cutFeatures,
543
535
  pathData: this.state.pathData,
544
536
  });
545
537
 
@@ -569,7 +561,7 @@ export class DielineTool implements Extension {
569
561
  radius: cutR,
570
562
  x: cx,
571
563
  y: cy,
572
- features: maskFeatures, // Use same features as mask for consistency
564
+ features: cutFeatures, // Use same features as mask for consistency
573
565
  pathData: this.state.pathData,
574
566
  canvasWidth: canvasW,
575
567
  canvasHeight: canvasH,
@@ -596,7 +588,7 @@ export class DielineTool implements Extension {
596
588
  radius: visualRadius,
597
589
  x: cx,
598
590
  y: cy,
599
- features: originalFeatures,
591
+ features: cutFeatures,
600
592
  pathData: this.state.pathData,
601
593
  canvasWidth: canvasW,
602
594
  canvasHeight: canvasH,
@@ -608,7 +600,7 @@ export class DielineTool implements Extension {
608
600
  radius: cutR,
609
601
  x: cx,
610
602
  y: cy,
611
- features: offsetFeatures,
603
+ features: cutFeatures,
612
604
  pathData: this.state.pathData,
613
605
  canvasWidth: canvasW,
614
606
  canvasHeight: canvasH,
@@ -641,7 +633,7 @@ export class DielineTool implements Extension {
641
633
  radius: cutR,
642
634
  x: cx,
643
635
  y: cy,
644
- features: offsetFeatures,
636
+ features: cutFeatures,
645
637
  pathData: this.state.pathData,
646
638
  canvasWidth: canvasW,
647
639
  canvasHeight: canvasH,
@@ -671,7 +663,7 @@ export class DielineTool implements Extension {
671
663
  radius: visualRadius,
672
664
  x: cx,
673
665
  y: cy,
674
- features: originalFeatures,
666
+ features: absoluteFeatures,
675
667
  pathData: this.state.pathData,
676
668
  canvasWidth: canvasW,
677
669
  canvasHeight: canvasH,
@@ -798,9 +790,7 @@ export class DielineTool implements Extension {
798
790
  };
799
791
  });
800
792
 
801
- const originalFeatures = absoluteFeatures.filter(
802
- (f) => !f.target || f.target === "original" || f.target === "both",
803
- );
793
+ const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
804
794
 
805
795
  const generatedPathData = generateDielinePath({
806
796
  shape,
@@ -809,7 +799,7 @@ export class DielineTool implements Extension {
809
799
  radius: visualRadius,
810
800
  x: cx,
811
801
  y: cy,
812
- features: originalFeatures,
802
+ features: cutFeatures,
813
803
  pathData,
814
804
  canvasWidth: canvasW,
815
805
  canvasHeight: canvasH,
package/src/feature.ts CHANGED
@@ -11,7 +11,7 @@ import CanvasService from "./CanvasService";
11
11
  import { DielineGeometry } from "./dieline";
12
12
  import {
13
13
  getNearestPointOnDieline,
14
- EdgeFeature,
14
+ DielineFeature,
15
15
  resolveFeaturePosition,
16
16
  } from "./geometry";
17
17
  import { Coordinate } from "./coordinate";
@@ -23,10 +23,11 @@ export class FeatureTool implements Extension {
23
23
  name: "FeatureTool",
24
24
  };
25
25
 
26
- private features: EdgeFeature[] = [];
26
+ private features: DielineFeature[] = [];
27
27
  private canvasService?: CanvasService;
28
28
  private context?: ExtensionContext;
29
29
  private isUpdatingConfig = false;
30
+ private isToolActive = false;
30
31
 
31
32
  private handleMoving: ((e: any) => void) | null = null;
32
33
  private handleModified: ((e: any) => void) | null = null;
@@ -37,7 +38,7 @@ export class FeatureTool implements Extension {
37
38
 
38
39
  constructor(
39
40
  options?: Partial<{
40
- features: EdgeFeature[];
41
+ features: DielineFeature[];
41
42
  }>,
42
43
  ) {
43
44
  if (options) {
@@ -70,15 +71,44 @@ export class FeatureTool implements Extension {
70
71
  });
71
72
  }
72
73
 
74
+ // Listen to tool activation
75
+ context.eventBus.on("tool:activated", this.onToolActivated);
76
+
73
77
  this.setup();
74
78
  }
75
79
 
76
80
  deactivate(context: ExtensionContext) {
81
+ context.eventBus.off("tool:activated", this.onToolActivated);
77
82
  this.teardown();
78
83
  this.canvasService = undefined;
79
84
  this.context = undefined;
80
85
  }
81
86
 
87
+ private onToolActivated = (event: { id: string }) => {
88
+ this.isToolActive = event.id === this.id;
89
+ this.updateVisibility();
90
+ };
91
+
92
+ private updateVisibility() {
93
+ if (!this.canvasService) return;
94
+ const canvas = this.canvasService.canvas;
95
+ const markers = canvas
96
+ .getObjects()
97
+ .filter((obj: any) => obj.data?.type === "feature-marker");
98
+
99
+ markers.forEach((marker: any) => {
100
+ // If tool active, allow selection. If not, disable selection.
101
+ // Also might want to hide them entirely or just disable interaction.
102
+ // Assuming we only want to see/edit holes when tool is active.
103
+ marker.set({
104
+ visible: this.isToolActive, // Or just selectable: false if we want them visible but locked
105
+ selectable: this.isToolActive,
106
+ evented: this.isToolActive
107
+ });
108
+ });
109
+ canvas.requestRenderAll();
110
+ }
111
+
82
112
  contribute() {
83
113
  return {
84
114
  [ContributionPointIds.COMMANDS]: [
@@ -131,10 +161,10 @@ export class FeatureTool implements Extension {
131
161
  const defaultSize = Coordinate.convertUnit(10, "mm", unit);
132
162
 
133
163
  // Default to top edge center
134
- const newFeature: EdgeFeature = {
164
+ const newFeature: DielineFeature = {
135
165
  id: Date.now().toString(),
136
166
  operation: type,
137
- target: "original",
167
+ placement: "edge",
138
168
  shape: "rect",
139
169
  x: 0.5,
140
170
  y: 0, // Top edge
@@ -147,7 +177,7 @@ export class FeatureTool implements Extension {
147
177
  const current = configService.get(
148
178
  "dieline.features",
149
179
  [],
150
- ) as EdgeFeature[];
180
+ ) as DielineFeature[];
151
181
  configService.update("dieline.features", [...current, newFeature]);
152
182
  }
153
183
  return true;
@@ -167,11 +197,12 @@ export class FeatureTool implements Extension {
167
197
  const timestamp = Date.now();
168
198
 
169
199
  // 1. Lug (Outer) - Add
170
- const lug: EdgeFeature = {
200
+ const lug: DielineFeature = {
171
201
  id: `${timestamp}-lug`,
172
202
  groupId,
173
203
  operation: "add",
174
204
  shape: "circle",
205
+ placement: "edge",
175
206
  x: 0.5,
176
207
  y: 0,
177
208
  radius: lugRadius, // 20mm
@@ -179,11 +210,12 @@ export class FeatureTool implements Extension {
179
210
  };
180
211
 
181
212
  // 2. Hole (Inner) - Subtract
182
- const hole: EdgeFeature = {
213
+ const hole: DielineFeature = {
183
214
  id: `${timestamp}-hole`,
184
215
  groupId,
185
216
  operation: "subtract",
186
217
  shape: "circle",
218
+ placement: "edge",
187
219
  x: 0.5,
188
220
  y: 0,
189
221
  radius: holeRadius, // 15mm
@@ -194,7 +226,7 @@ export class FeatureTool implements Extension {
194
226
  const current = configService.get(
195
227
  "dieline.features",
196
228
  [],
197
- ) as EdgeFeature[];
229
+ ) as DielineFeature[];
198
230
  configService.update("dieline.features", [...current, lug, hole]);
199
231
  }
200
232
  return true;
@@ -202,19 +234,10 @@ export class FeatureTool implements Extension {
202
234
 
203
235
  private getGeometryForFeature(
204
236
  geometry: DielineGeometry,
205
- feature?: EdgeFeature,
237
+ feature?: DielineFeature,
206
238
  ): DielineGeometry {
207
- if (feature?.target === "offset" && geometry.offset !== 0) {
208
- return {
209
- ...geometry,
210
- width: geometry.width + geometry.offset * 2,
211
- height: geometry.height + geometry.offset * 2,
212
- radius:
213
- geometry.radius === 0
214
- ? 0
215
- : Math.max(0, geometry.radius + geometry.offset),
216
- };
217
- }
239
+ // Legacy support or specialized scaling can go here if needed
240
+ // Currently all features operate on the base geometry (or scaled version of it)
218
241
  return geometry;
219
242
  }
220
243
 
@@ -258,7 +281,7 @@ export class FeatureTool implements Extension {
258
281
  if (!this.currentGeometry) return;
259
282
 
260
283
  // Determine feature to use for snapping context
261
- let feature: EdgeFeature | undefined;
284
+ let feature: DielineFeature | undefined;
262
285
  if (target.data?.isGroup) {
263
286
  const indices = target.data?.indices as number[];
264
287
  if (indices && indices.length > 0) {
@@ -288,7 +311,7 @@ export class FeatureTool implements Extension {
288
311
  const minDim = Math.min(target.getScaledWidth(), target.getScaledHeight());
289
312
  const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
290
313
 
291
- const snapped = this.constrainPosition(p, geometry, limit);
314
+ const snapped = this.constrainPosition(p, geometry, limit, feature);
292
315
 
293
316
  target.set({
294
317
  left: snapped.x,
@@ -409,8 +432,23 @@ export class FeatureTool implements Extension {
409
432
  private constrainPosition(
410
433
  p: Point,
411
434
  geometry: DielineGeometry,
412
- limit: number
435
+ limit: number,
436
+ feature?: DielineFeature
413
437
  ): { x: number; y: number } {
438
+ if (feature && feature.placement === "internal") {
439
+ // Constrain to bounds
440
+ // geometry.x/y is center
441
+ const minX = geometry.x - geometry.width / 2;
442
+ const maxX = geometry.x + geometry.width / 2;
443
+ const minY = geometry.y - geometry.height / 2;
444
+ const maxY = geometry.y + geometry.height / 2;
445
+
446
+ return {
447
+ x: Math.max(minX, Math.min(maxX, p.x)),
448
+ y: Math.max(minY, Math.min(maxY, p.y))
449
+ };
450
+ }
451
+
414
452
  // Use geometry helper to find nearest point on Base Shape
415
453
  // geometry object matches GeometryOptions structure required by getNearestPointOnDieline
416
454
  // except for 'features' which we don't need for base shape snapping
@@ -502,9 +540,9 @@ export class FeatureTool implements Extension {
502
540
  const finalScale = scale;
503
541
 
504
542
  // Group features by groupId
505
- const groups: { [key: string]: { feature: EdgeFeature; index: number }[] } =
543
+ const groups: { [key: string]: { feature: DielineFeature; index: number }[] } =
506
544
  {};
507
- const singles: { feature: EdgeFeature; index: number }[] = [];
545
+ const singles: { feature: DielineFeature; index: number }[] = [];
508
546
 
509
547
  this.features.forEach((f, i) => {
510
548
  if (f.groupId) {
@@ -517,7 +555,7 @@ export class FeatureTool implements Extension {
517
555
 
518
556
  // Helper to create marker shape
519
557
  const createMarkerShape = (
520
- feature: EdgeFeature,
558
+ feature: DielineFeature,
521
559
  pos: { x: number; y: number },
522
560
  ) => {
523
561
  // Features are in the same unit as geometry.unit
@@ -578,7 +616,9 @@ export class FeatureTool implements Extension {
578
616
  const marker = createMarkerShape(feature, pos);
579
617
 
580
618
  marker.set({
581
- selectable: true,
619
+ visible: this.isToolActive,
620
+ selectable: this.isToolActive,
621
+ evented: this.isToolActive,
582
622
  hasControls: false,
583
623
  hasBorders: false,
584
624
  hoverCursor: "move",
@@ -633,7 +673,9 @@ export class FeatureTool implements Extension {
633
673
  });
634
674
 
635
675
  const groupObj = new Group(shapes, {
636
- selectable: true,
676
+ visible: this.isToolActive,
677
+ selectable: this.isToolActive,
678
+ evented: this.isToolActive,
637
679
  hasControls: false,
638
680
  hasBorders: false,
639
681
  hoverCursor: "move",
@@ -689,7 +731,7 @@ export class FeatureTool implements Extension {
689
731
 
690
732
  markers.forEach((marker: any) => {
691
733
  // Find associated feature
692
- let feature: EdgeFeature | undefined;
734
+ let feature: DielineFeature | undefined;
693
735
  if (marker.data?.isGroup) {
694
736
  const indices = marker.data?.indices as number[];
695
737
  if (indices && indices.length > 0) {
@@ -714,7 +756,8 @@ export class FeatureTool implements Extension {
714
756
  const snapped = this.constrainPosition(
715
757
  new Point(marker.left, marker.top),
716
758
  geometry,
717
- limit
759
+ limit,
760
+ feature
718
761
  );
719
762
  marker.set({ left: snapped.x, top: snapped.y });
720
763
  marker.setCoords();
package/src/geometry.ts CHANGED
@@ -3,20 +3,21 @@ import paper from "paper";
3
3
  export type FeatureOperation = "add" | "subtract";
4
4
  export type FeatureShape = "rect" | "circle";
5
5
 
6
- export interface EdgeFeature {
6
+ export interface DielineFeature {
7
7
  id: string;
8
- groupId?: string; // For grouping features together (e.g. double-layer hole)
8
+ groupId?: string;
9
9
  operation: FeatureOperation;
10
10
  shape: FeatureShape;
11
- x: number; // Normalized 0-1 relative to geometry bounds
12
- y: number; // Normalized 0-1 relative to geometry bounds
13
- width?: number; // For rect (Physical units)
14
- height?: number; // For rect (Physical units)
15
- radius?: number; // For circle or rect corners (Physical units)
16
- rotation?: number; // Degrees
17
- target?: "original" | "offset" | "both";
18
- color?: string; // Hex color for the marker
19
- strokeDash?: number[]; // Stroke dash array for the marker
11
+ x: number;
12
+ y: number;
13
+ width?: number;
14
+ height?: number;
15
+ radius?: number;
16
+ rotation?: number;
17
+ placement?: "edge" | "internal";
18
+ color?: string;
19
+ strokeDash?: number[];
20
+ skipCut?: boolean;
20
21
  }
21
22
 
22
23
  export interface GeometryOptions {
@@ -26,7 +27,7 @@ export interface GeometryOptions {
26
27
  radius: number;
27
28
  x: number;
28
29
  y: number;
29
- features: Array<EdgeFeature>;
30
+ features: Array<DielineFeature>;
30
31
  pathData?: string;
31
32
  canvasWidth?: number;
32
33
  canvasHeight?: number;
@@ -41,7 +42,7 @@ export interface MaskGeometryOptions extends GeometryOptions {
41
42
  * Resolves the absolute position of a feature based on normalized coordinates.
42
43
  */
43
44
  export function resolveFeaturePosition(
44
- feature: EdgeFeature,
45
+ feature: DielineFeature,
45
46
  geometry: { x: number; y: number; width: number; height: number },
46
47
  ): { x: number; y: number } {
47
48
  const { x, y, width, height } = geometry;
@@ -116,7 +117,7 @@ function createBaseShape(options: GeometryOptions): paper.PathItem {
116
117
  * Creates a Paper.js Item for a single feature.
117
118
  */
118
119
  function createFeatureItem(
119
- feature: EdgeFeature,
120
+ feature: DielineFeature,
120
121
  center: paper.Point,
121
122
  ): paper.PathItem {
122
123
  let item: paper.PathItem;
@@ -147,20 +148,24 @@ function createFeatureItem(
147
148
  }
148
149
 
149
150
  /**
150
- * Internal helper to generate the Dieline Shape (Paper Item).
151
- * Logic: (Base U Adds) - Subtracts
151
+ * Internal helper to generate the Perimeter Shape (Base + Edge Features).
152
152
  */
153
- function getDielineShape(options: GeometryOptions): paper.PathItem {
153
+ function getPerimeterShape(options: GeometryOptions): paper.PathItem {
154
154
  // 1. Create Base Shape
155
155
  let mainShape = createBaseShape(options);
156
156
 
157
157
  const { features } = options;
158
158
 
159
159
  if (features && features.length > 0) {
160
+ // Filter for Edge Features (Default or explicit 'edge')
161
+ const edgeFeatures = features.filter(
162
+ (f) => !f.placement || f.placement === "edge",
163
+ );
164
+
160
165
  const adds: paper.PathItem[] = [];
161
166
  const subtracts: paper.PathItem[] = [];
162
167
 
163
- features.forEach((f) => {
168
+ edgeFeatures.forEach((f) => {
164
169
  const pos = resolveFeaturePosition(f, options);
165
170
  const center = new paper.Point(pos.x, pos.y);
166
171
  const item = createFeatureItem(f, center);
@@ -174,9 +179,6 @@ function getDielineShape(options: GeometryOptions): paper.PathItem {
174
179
 
175
180
  // 2. Process Additions (Union)
176
181
  if (adds.length > 0) {
177
- // Unite all additions first to avoid artifacts?
178
- // Or unite one by one to mainShape?
179
- // Unite one by one is safer for simple logic.
180
182
  for (const item of adds) {
181
183
  try {
182
184
  const temp = mainShape.unite(item);
@@ -209,6 +211,49 @@ function getDielineShape(options: GeometryOptions): paper.PathItem {
209
211
  return mainShape;
210
212
  }
211
213
 
214
+ /**
215
+ * Applies Internal/Surface features to a shape.
216
+ */
217
+ function applySurfaceFeatures(
218
+ shape: paper.PathItem,
219
+ features: DielineFeature[],
220
+ options: GeometryOptions,
221
+ ): paper.PathItem {
222
+ const internalFeatures = features.filter((f) => f.placement === "internal");
223
+
224
+ if (internalFeatures.length === 0) return shape;
225
+
226
+ let result = shape;
227
+
228
+ // Internal features are usually subtractive (holes)
229
+ // But we support 'add' too (islands? maybe just unite)
230
+
231
+ for (const f of internalFeatures) {
232
+ const pos = resolveFeaturePosition(f, options);
233
+ const center = new paper.Point(pos.x, pos.y);
234
+ const item = createFeatureItem(f, center);
235
+
236
+ try {
237
+ if (f.operation === "add") {
238
+ const temp = result.unite(item);
239
+ result.remove();
240
+ item.remove();
241
+ result = temp;
242
+ } else {
243
+ const temp = result.subtract(item);
244
+ result.remove();
245
+ item.remove();
246
+ result = temp;
247
+ }
248
+ } catch (e) {
249
+ console.error("Geometry: Failed to apply surface feature", e);
250
+ item.remove();
251
+ }
252
+ }
253
+
254
+ return result;
255
+ }
256
+
212
257
  /**
213
258
  * Generates the path data for the Dieline (Product Shape).
214
259
  */
@@ -218,10 +263,11 @@ export function generateDielinePath(options: GeometryOptions): string {
218
263
  ensurePaper(paperWidth, paperHeight);
219
264
  paper.project.activeLayer.removeChildren();
220
265
 
221
- const mainShape = getDielineShape(options);
266
+ const perimeter = getPerimeterShape(options);
267
+ const finalShape = applySurfaceFeatures(perimeter, options.features, options);
222
268
 
223
- const pathData = mainShape.pathData;
224
- mainShape.remove();
269
+ const pathData = finalShape.pathData;
270
+ finalShape.remove();
225
271
 
226
272
  return pathData;
227
273
  }
@@ -241,7 +287,8 @@ export function generateMaskPath(options: MaskGeometryOptions): string {
241
287
  size: [canvasWidth, canvasHeight],
242
288
  });
243
289
 
244
- const mainShape = getDielineShape(options);
290
+ const perimeter = getPerimeterShape(options);
291
+ const mainShape = applySurfaceFeatures(perimeter, options.features, options);
245
292
 
246
293
  const finalMask = maskRect.subtract(mainShape);
247
294
 
@@ -270,10 +317,12 @@ export function generateBleedZonePath(
270
317
  paper.project.activeLayer.removeChildren();
271
318
 
272
319
  // 1. Generate Original Shape
273
- const shapeOriginal = getDielineShape(originalOptions);
320
+ const pOriginal = getPerimeterShape(originalOptions);
321
+ const shapeOriginal = applySurfaceFeatures(pOriginal, originalOptions.features, originalOptions);
274
322
 
275
323
  // 2. Generate Offset Shape
276
- const shapeOffset = getDielineShape(offsetOptions);
324
+ const pOffset = getPerimeterShape(offsetOptions);
325
+ const shapeOffset = applySurfaceFeatures(pOffset, offsetOptions.features, offsetOptions);
277
326
 
278
327
  // 3. Calculate Difference
279
328
  let bleedZone: paper.PathItem;
package/src/image.ts CHANGED
@@ -33,6 +33,7 @@ export class ImageTool implements Extension {
33
33
  private canvasService?: CanvasService;
34
34
  private context?: ExtensionContext;
35
35
  private isUpdatingConfig = false;
36
+ private isToolActive = false;
36
37
 
37
38
  activate(context: ExtensionContext) {
38
39
  this.context = context;
@@ -42,6 +43,9 @@ export class ImageTool implements Extension {
42
43
  return;
43
44
  }
44
45
 
46
+ // Listen to tool activation
47
+ context.eventBus.on("tool:activated", this.onToolActivated);
48
+
45
49
  const configService = context.services.get<ConfigurationService>(
46
50
  "ConfigurationService",
47
51
  );
@@ -65,6 +69,8 @@ export class ImageTool implements Extension {
65
69
  }
66
70
 
67
71
  deactivate(context: ExtensionContext) {
72
+ context.eventBus.off("tool:activated", this.onToolActivated);
73
+
68
74
  if (this.canvasService) {
69
75
  const layer = this.canvasService.getLayer("user");
70
76
  if (layer) {
@@ -79,6 +85,23 @@ export class ImageTool implements Extension {
79
85
  }
80
86
  }
81
87
 
88
+ private onToolActivated = (event: { id: string }) => {
89
+ this.isToolActive = event.id === this.id;
90
+ this.updateInteractivity();
91
+ };
92
+
93
+ private updateInteractivity() {
94
+ this.objectMap.forEach((obj) => {
95
+ obj.set({
96
+ selectable: this.isToolActive,
97
+ evented: this.isToolActive,
98
+ hasControls: this.isToolActive,
99
+ hasBorders: this.isToolActive,
100
+ });
101
+ });
102
+ this.canvasService?.requestRenderAll();
103
+ }
104
+
82
105
  contribute() {
83
106
  return {
84
107
  [ContributionPointIds.CONFIGURATIONS]: [
@@ -291,6 +314,20 @@ export class ImageTool implements Extension {
291
314
  this.items.forEach((item, index) => {
292
315
  let obj = this.objectMap.get(item.id);
293
316
 
317
+ // Check if URL changed, if so remove object to force reload
318
+ // We assume Fabric object has getSrc() or we check data.url if we stored it
319
+ // Since we don't store url on object easily accessible without casting,
320
+ // let's rely on checking if we need to reload.
321
+ // Actually, standard Fabric Image doesn't expose src easily on type without casting to any.
322
+ if (obj && (obj as any).getSrc) {
323
+ const currentSrc = (obj as any).getSrc();
324
+ if (currentSrc !== item.url) {
325
+ layer.remove(obj);
326
+ this.objectMap.delete(item.id);
327
+ obj = undefined;
328
+ }
329
+ }
330
+
294
331
  if (!obj) {
295
332
  // New object, load it
296
333
  this.loadImage(item, layer, layout);
@@ -375,6 +412,10 @@ export class ImageTool implements Extension {
375
412
  data: { id: item.id },
376
413
  uniformScaling: true,
377
414
  lockScalingFlip: true,
415
+ selectable: this.isToolActive,
416
+ evented: this.isToolActive,
417
+ hasControls: this.isToolActive,
418
+ hasBorders: this.isToolActive,
378
419
  });
379
420
 
380
421
  image.setControlsVisibility({