@pooder/kit 4.3.1 → 5.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.
Files changed (60) hide show
  1. package/.test-dist/src/CanvasService.js +249 -0
  2. package/.test-dist/src/ViewportSystem.js +75 -0
  3. package/.test-dist/src/background.js +203 -0
  4. package/.test-dist/src/bridgeSelection.js +20 -0
  5. package/.test-dist/src/constraints.js +237 -0
  6. package/.test-dist/src/coordinate.js +74 -0
  7. package/.test-dist/src/dieline.js +723 -0
  8. package/.test-dist/src/edgeScale.js +12 -0
  9. package/.test-dist/src/feature.js +752 -0
  10. package/.test-dist/src/featureComplete.js +32 -0
  11. package/.test-dist/src/film.js +167 -0
  12. package/.test-dist/src/geometry.js +506 -0
  13. package/.test-dist/src/image.js +1234 -0
  14. package/.test-dist/src/index.js +35 -0
  15. package/.test-dist/src/maskOps.js +270 -0
  16. package/.test-dist/src/mirror.js +104 -0
  17. package/.test-dist/src/renderSpec.js +2 -0
  18. package/.test-dist/src/ruler.js +343 -0
  19. package/.test-dist/src/sceneLayout.js +99 -0
  20. package/.test-dist/src/sceneLayoutModel.js +196 -0
  21. package/.test-dist/src/sceneView.js +40 -0
  22. package/.test-dist/src/sceneVisibility.js +42 -0
  23. package/.test-dist/src/size.js +332 -0
  24. package/.test-dist/src/tracer.js +544 -0
  25. package/.test-dist/src/units.js +30 -0
  26. package/.test-dist/src/white-ink.js +829 -0
  27. package/.test-dist/src/wrappedOffsets.js +33 -0
  28. package/.test-dist/tests/run.js +94 -0
  29. package/CHANGELOG.md +11 -0
  30. package/dist/index.d.mts +339 -36
  31. package/dist/index.d.ts +339 -36
  32. package/dist/index.js +3572 -850
  33. package/dist/index.mjs +3565 -852
  34. package/package.json +2 -2
  35. package/src/CanvasService.ts +300 -96
  36. package/src/ViewportSystem.ts +92 -92
  37. package/src/background.ts +230 -230
  38. package/src/bridgeSelection.ts +17 -0
  39. package/src/coordinate.ts +106 -106
  40. package/src/dieline.ts +897 -973
  41. package/src/edgeScale.ts +19 -0
  42. package/src/feature.ts +83 -30
  43. package/src/film.ts +194 -194
  44. package/src/geometry.ts +242 -84
  45. package/src/image.ts +1582 -512
  46. package/src/index.ts +14 -10
  47. package/src/maskOps.ts +326 -0
  48. package/src/mirror.ts +128 -128
  49. package/src/renderSpec.ts +18 -0
  50. package/src/ruler.ts +449 -508
  51. package/src/sceneLayout.ts +121 -0
  52. package/src/sceneLayoutModel.ts +335 -0
  53. package/src/sceneVisibility.ts +49 -0
  54. package/src/size.ts +379 -0
  55. package/src/tracer.ts +719 -570
  56. package/src/units.ts +27 -27
  57. package/src/white-ink.ts +1018 -373
  58. package/src/wrappedOffsets.ts +33 -0
  59. package/tests/run.ts +118 -0
  60. package/tsconfig.test.json +15 -15
@@ -0,0 +1,19 @@
1
+ export interface BoundsLike {
2
+ width: number;
3
+ height: number;
4
+ }
5
+
6
+ export function computeDetectEdgeSize(
7
+ currentMax: number,
8
+ baseBounds: BoundsLike,
9
+ expandedBounds: BoundsLike,
10
+ ): { width: number; height: number; scale: number } {
11
+ const baseMax = Math.max(baseBounds.width, baseBounds.height);
12
+ const scale = baseMax > 0 ? currentMax / baseMax : 1;
13
+ return {
14
+ scale,
15
+ width: expandedBounds.width * scale,
16
+ height: expandedBounds.height * scale,
17
+ };
18
+ }
19
+
package/src/feature.ts CHANGED
@@ -4,10 +4,10 @@ import {
4
4
  ContributionPointIds,
5
5
  CommandContribution,
6
6
  ConfigurationService,
7
+ ToolSessionService,
7
8
  } from "@pooder/core";
8
9
  import { Circle, Group, Point, Rect } from "fabric";
9
10
  import CanvasService from "./CanvasService";
10
- import { DielineGeometry } from "./dieline";
11
11
  import {
12
12
  getNearestPointOnDieline,
13
13
  DielineFeature,
@@ -17,7 +17,10 @@ import { ConstraintRegistry, ConstraintFeature } from "./constraints";
17
17
  import {
18
18
  completeFeaturesStrict,
19
19
  } from "./featureComplete";
20
- import { parseLengthToMm } from "./units";
20
+ import {
21
+ readSizeState,
22
+ type SceneGeometrySnapshot as DielineGeometry,
23
+ } from "./sceneLayoutModel";
21
24
 
22
25
  export class FeatureTool implements Extension {
23
26
  id = "pooder.kit.feature";
@@ -31,10 +34,12 @@ export class FeatureTool implements Extension {
31
34
  private context?: ExtensionContext;
32
35
  private isUpdatingConfig = false;
33
36
  private isToolActive = false;
37
+ private hasWorkingChanges = false;
38
+ private dirtyTrackerDisposable?: { dispose(): void };
34
39
 
35
40
  private handleMoving: ((e: any) => void) | null = null;
36
41
  private handleModified: ((e: any) => void) | null = null;
37
- private handleDielineChange: ((geometry: DielineGeometry) => void) | null =
42
+ private handleSceneGeometryChange: ((geometry: DielineGeometry) => void) | null =
38
43
  null;
39
44
 
40
45
  private currentGeometry: DielineGeometry | null = null;
@@ -65,6 +70,7 @@ export class FeatureTool implements Extension {
65
70
  const features = (configService.get("dieline.features", []) ||
66
71
  []) as ConstraintFeature[];
67
72
  this.workingFeatures = this.cloneFeatures(features);
73
+ this.hasWorkingChanges = false;
68
74
 
69
75
  configService.onAnyChange((e: { key: string; value: any }) => {
70
76
  if (this.isUpdatingConfig) return;
@@ -72,12 +78,20 @@ export class FeatureTool implements Extension {
72
78
  if (e.key === "dieline.features") {
73
79
  const next = (e.value || []) as ConstraintFeature[];
74
80
  this.workingFeatures = this.cloneFeatures(next);
81
+ this.hasWorkingChanges = false;
75
82
  this.redraw();
76
83
  this.emitWorkingChange();
77
84
  }
78
85
  });
79
86
  }
80
87
 
88
+ const toolSessionService =
89
+ context.services.get<ToolSessionService>("ToolSessionService");
90
+ this.dirtyTrackerDisposable = toolSessionService?.registerDirtyTracker(
91
+ this.id,
92
+ () => this.hasWorkingChanges,
93
+ );
94
+
81
95
  // Listen to tool activation
82
96
  context.eventBus.on("tool:activated", this.onToolActivated);
83
97
 
@@ -86,6 +100,8 @@ export class FeatureTool implements Extension {
86
100
 
87
101
  deactivate(context: ExtensionContext) {
88
102
  context.eventBus.off("tool:activated", this.onToolActivated);
103
+ this.dirtyTrackerDisposable?.dispose();
104
+ this.dirtyTrackerDisposable = undefined;
89
105
  this.teardown();
90
106
  this.canvasService = undefined;
91
107
  this.context = undefined;
@@ -118,6 +134,22 @@ export class FeatureTool implements Extension {
118
134
 
119
135
  contribute() {
120
136
  return {
137
+ [ContributionPointIds.TOOLS]: [
138
+ {
139
+ id: this.id,
140
+ name: "Feature",
141
+ interaction: "session",
142
+ commands: {
143
+ begin: "resetWorkingFeatures",
144
+ commit: "completeFeatures",
145
+ rollback: "resetWorkingFeatures",
146
+ },
147
+ session: {
148
+ autoBegin: false,
149
+ leavePolicy: "block",
150
+ },
151
+ },
152
+ ],
121
153
  [ContributionPointIds.COMMANDS]: [
122
154
  {
123
155
  command: "addFeature",
@@ -145,6 +177,7 @@ export class FeatureTool implements Extension {
145
177
  title: "Clear Features",
146
178
  handler: () => {
147
179
  this.setWorkingFeatures([]);
180
+ this.hasWorkingChanges = true;
148
181
  this.redraw();
149
182
  this.emitWorkingChange();
150
183
  return true;
@@ -163,6 +196,26 @@ export class FeatureTool implements Extension {
163
196
  handler: async (features: ConstraintFeature[]) => {
164
197
  await this.refreshGeometry();
165
198
  this.setWorkingFeatures(this.cloneFeatures(features || []));
199
+ this.hasWorkingChanges = true;
200
+ this.redraw();
201
+ this.emitWorkingChange();
202
+ return { ok: true };
203
+ },
204
+ },
205
+ {
206
+ command: "resetWorkingFeatures",
207
+ title: "Reset Working Features",
208
+ handler: async () => {
209
+ const configService =
210
+ this.context?.services.get<ConfigurationService>(
211
+ "ConfigurationService",
212
+ );
213
+ const next = (configService?.get("dieline.features", []) ||
214
+ []) as ConstraintFeature[];
215
+
216
+ await this.refreshGeometry();
217
+ this.setWorkingFeatures(this.cloneFeatures(next));
218
+ this.hasWorkingChanges = false;
166
219
  this.redraw();
167
220
  this.emitWorkingChange();
168
221
  return { ok: true };
@@ -201,7 +254,9 @@ export class FeatureTool implements Extension {
201
254
  const commandService = this.context.services.get<any>("CommandService");
202
255
  if (!commandService) return;
203
256
  try {
204
- const g = await Promise.resolve(commandService.executeCommand("getGeometry"));
257
+ const g = await Promise.resolve(
258
+ commandService.executeCommand("getSceneGeometry"),
259
+ );
205
260
  if (g) this.currentGeometry = g as DielineGeometry;
206
261
  } catch (e) {}
207
262
  }
@@ -217,14 +272,9 @@ export class FeatureTool implements Extension {
217
272
  this.context?.services.get<ConfigurationService>("ConfigurationService");
218
273
  if (!configService) return { ok: false };
219
274
 
220
- const dielineWidth = parseLengthToMm(
221
- configService.get("dieline.width") ?? 500,
222
- "mm",
223
- );
224
- const dielineHeight = parseLengthToMm(
225
- configService.get("dieline.height") ?? 500,
226
- "mm",
227
- );
275
+ const sizeState = readSizeState(configService);
276
+ const dielineWidth = sizeState.actualWidthMm;
277
+ const dielineHeight = sizeState.actualHeightMm;
228
278
 
229
279
  let changed = false;
230
280
  const next = this.workingFeatures.map((f) => {
@@ -250,6 +300,7 @@ export class FeatureTool implements Extension {
250
300
  if (!changed) return { ok: true };
251
301
 
252
302
  this.setWorkingFeatures(next);
303
+ this.hasWorkingChanges = true;
253
304
  this.redraw();
254
305
  this.enforceConstraints();
255
306
  this.emitWorkingChange();
@@ -276,14 +327,9 @@ export class FeatureTool implements Extension {
276
327
  };
277
328
  }
278
329
 
279
- const dielineWidth = parseLengthToMm(
280
- configService.get("dieline.width") ?? 500,
281
- "mm",
282
- );
283
- const dielineHeight = parseLengthToMm(
284
- configService.get("dieline.height") ?? 500,
285
- "mm",
286
- );
330
+ const sizeState = readSizeState(configService);
331
+ const dielineWidth = sizeState.actualWidthMm;
332
+ const dielineHeight = sizeState.actualHeightMm;
287
333
 
288
334
  const result = completeFeaturesStrict(
289
335
  this.workingFeatures,
@@ -308,6 +354,9 @@ export class FeatureTool implements Extension {
308
354
  };
309
355
  }
310
356
 
357
+ this.hasWorkingChanges = false;
358
+ // Keep feature markers above dieline overlay after config-driven redraw.
359
+ this.redraw();
311
360
  return { ok: true };
312
361
  }
313
362
 
@@ -330,6 +379,7 @@ export class FeatureTool implements Extension {
330
379
  };
331
380
 
332
381
  this.setWorkingFeatures([...(this.workingFeatures || []), newFeature]);
382
+ this.hasWorkingChanges = true;
333
383
  this.redraw();
334
384
  this.emitWorkingChange();
335
385
  return true;
@@ -370,6 +420,7 @@ export class FeatureTool implements Extension {
370
420
  };
371
421
 
372
422
  this.setWorkingFeatures([...(this.workingFeatures || []), lug, hole]);
423
+ this.hasWorkingChanges = true;
373
424
  this.redraw();
374
425
  this.emitWorkingChange();
375
426
  return true;
@@ -388,16 +439,16 @@ export class FeatureTool implements Extension {
388
439
  if (!this.canvasService || !this.context) return;
389
440
  const canvas = this.canvasService.canvas;
390
441
 
391
- // 1. Listen for Dieline Geometry Changes
392
- if (!this.handleDielineChange) {
393
- this.handleDielineChange = (geometry: DielineGeometry) => {
442
+ // 1. Listen for Scene Geometry Changes
443
+ if (!this.handleSceneGeometryChange) {
444
+ this.handleSceneGeometryChange = (geometry: DielineGeometry) => {
394
445
  this.currentGeometry = geometry;
395
446
  this.redraw();
396
447
  this.enforceConstraints();
397
448
  };
398
449
  this.context.eventBus.on(
399
- "dieline:geometry:change",
400
- this.handleDielineChange,
450
+ "scene:geometry:change",
451
+ this.handleSceneGeometryChange,
401
452
  );
402
453
  }
403
454
 
@@ -405,7 +456,7 @@ export class FeatureTool implements Extension {
405
456
  const commandService = this.context.services.get<any>("CommandService");
406
457
  if (commandService) {
407
458
  try {
408
- Promise.resolve(commandService.executeCommand("getGeometry")).then(
459
+ Promise.resolve(commandService.executeCommand("getSceneGeometry")).then(
409
460
  (g) => {
410
461
  if (g) {
411
462
  this.currentGeometry = g as DielineGeometry;
@@ -521,6 +572,7 @@ export class FeatureTool implements Extension {
521
572
  });
522
573
 
523
574
  this.setWorkingFeatures(newFeatures);
575
+ this.hasWorkingChanges = true;
524
576
  this.emitWorkingChange();
525
577
  } else {
526
578
  // Single object
@@ -543,12 +595,12 @@ export class FeatureTool implements Extension {
543
595
  canvas.off("object:modified", this.handleModified);
544
596
  this.handleModified = null;
545
597
  }
546
- if (this.handleDielineChange && this.context) {
598
+ if (this.handleSceneGeometryChange && this.context) {
547
599
  this.context.eventBus.off(
548
- "dieline:geometry:change",
549
- this.handleDielineChange,
600
+ "scene:geometry:change",
601
+ this.handleSceneGeometryChange,
550
602
  );
551
- this.handleDielineChange = null;
603
+ this.handleSceneGeometryChange = null;
552
604
  }
553
605
 
554
606
  const objects = canvas
@@ -636,6 +688,7 @@ export class FeatureTool implements Extension {
636
688
  const newFeatures = [...this.workingFeatures];
637
689
  newFeatures[index] = updatedFeature;
638
690
  this.setWorkingFeatures(newFeatures);
691
+ this.hasWorkingChanges = true;
639
692
  this.emitWorkingChange();
640
693
  }
641
694
 
package/src/film.ts CHANGED
@@ -1,194 +1,194 @@
1
- import {
2
- Extension,
3
- ExtensionContext,
4
- ContributionPointIds,
5
- CommandContribution,
6
- ConfigurationContribution,
7
- } from "@pooder/core";
8
- import { FabricImage as Image } from "fabric";
9
- import CanvasService from "./CanvasService";
10
-
11
- export class FilmTool implements Extension {
12
- id = "pooder.kit.film";
13
-
14
- public metadata = {
15
- name: "FilmTool",
16
- };
17
-
18
- private url: string = "";
19
- private opacity: number = 0.5;
20
-
21
- private canvasService?: CanvasService;
22
-
23
- constructor(
24
- options?: Partial<{
25
- url: string;
26
- opacity: number;
27
- }>,
28
- ) {
29
- if (options) {
30
- Object.assign(this, options);
31
- }
32
- }
33
-
34
- activate(context: ExtensionContext) {
35
- this.canvasService = context.services.get<CanvasService>("CanvasService");
36
- if (!this.canvasService) {
37
- console.warn("CanvasService not found for FilmTool");
38
- return;
39
- }
40
-
41
- const configService = context.services.get<any>("ConfigurationService");
42
- if (configService) {
43
- // Load initial config
44
- this.url = configService.get("film.url", this.url);
45
- this.opacity = configService.get("film.opacity", this.opacity);
46
-
47
- // Listen for changes
48
- configService.onAnyChange((e: { key: string; value: any }) => {
49
- if (e.key.startsWith("film.")) {
50
- const prop = e.key.split(".")[1];
51
- console.log(
52
- `[FilmTool] Config change detected: ${e.key} -> ${e.value}`,
53
- );
54
- if (prop && prop in this) {
55
- (this as any)[prop] = e.value;
56
- this.updateFilm();
57
- }
58
- }
59
- });
60
- }
61
-
62
- this.initLayer();
63
- this.updateFilm();
64
- }
65
-
66
- deactivate(context: ExtensionContext) {
67
- if (this.canvasService) {
68
- const layer = this.canvasService.getLayer("overlay");
69
- if (layer) {
70
- const img = this.canvasService.getObject("film-image", "overlay");
71
- if (img) {
72
- layer.remove(img);
73
- this.canvasService.requestRenderAll();
74
- }
75
- }
76
- this.canvasService = undefined;
77
- }
78
- }
79
-
80
- contribute() {
81
- return {
82
- [ContributionPointIds.CONFIGURATIONS]: [
83
- {
84
- id: "film.url",
85
- type: "string",
86
- label: "Film Image URL",
87
- default: "",
88
- },
89
- {
90
- id: "film.opacity",
91
- type: "number",
92
- label: "Opacity",
93
- min: 0,
94
- max: 1,
95
- step: 0.1,
96
- default: 0.5,
97
- },
98
- ] as ConfigurationContribution[],
99
- [ContributionPointIds.COMMANDS]: [
100
- {
101
- command: "setFilmImage",
102
- title: "Set Film Image",
103
- handler: (url: string, opacity: number) => {
104
- if (this.url === url && this.opacity === opacity) return true;
105
-
106
- this.url = url;
107
- this.opacity = opacity;
108
-
109
- this.updateFilm();
110
-
111
- return true;
112
- },
113
- },
114
- ] as CommandContribution[],
115
- };
116
- }
117
-
118
- private initLayer() {
119
- if (!this.canvasService) return;
120
- let overlayLayer = this.canvasService.getLayer("overlay");
121
- if (!overlayLayer) {
122
- const width = this.canvasService.canvas.width || 800;
123
- const height = this.canvasService.canvas.height || 600;
124
-
125
- const layer = this.canvasService.createLayer("overlay", {
126
- width,
127
- height,
128
- left: 0,
129
- top: 0,
130
- originX: "left",
131
- originY: "top",
132
- selectable: false,
133
- evented: false,
134
- subTargetCheck: false,
135
- interactive: false,
136
- });
137
-
138
- this.canvasService.canvas.bringObjectToFront(layer);
139
- }
140
- }
141
-
142
- private async updateFilm() {
143
- if (!this.canvasService) return;
144
- const layer = this.canvasService.getLayer("overlay");
145
- if (!layer) {
146
- console.warn("[FilmTool] Overlay layer not found");
147
- return;
148
- }
149
-
150
- const { url, opacity } = this;
151
-
152
- if (!url) {
153
- const img = this.canvasService.getObject("film-image", "overlay");
154
- if (img) {
155
- layer.remove(img);
156
- this.canvasService.requestRenderAll();
157
- }
158
- return;
159
- }
160
-
161
- const width = this.canvasService.canvas.width || 800;
162
- const height = this.canvasService.canvas.height || 600;
163
-
164
- let img = this.canvasService.getObject("film-image", "overlay") as Image;
165
- try {
166
- if (img) {
167
- if (img.getSrc() !== url) {
168
- await img.setSrc(url);
169
- }
170
- img.set({ opacity });
171
- } else {
172
- img = await Image.fromURL(url, { crossOrigin: "anonymous" });
173
- img.scaleToWidth(width);
174
- if (img.getScaledHeight() < height) img.scaleToHeight(height);
175
- img.set({
176
- originX: "left",
177
- originY: "top",
178
- left: 0,
179
- top: 0,
180
- opacity,
181
- selectable: false,
182
- evented: false,
183
- data: { id: "film-image" },
184
- });
185
- layer.add(img);
186
- }
187
- this.canvasService.requestRenderAll();
188
- } catch (error) {
189
- console.error("[FilmTool] Failed to load film image", url, error);
190
- }
191
- layer.dirty = true;
192
- this.canvasService.requestRenderAll();
193
- }
194
- }
1
+ import {
2
+ Extension,
3
+ ExtensionContext,
4
+ ContributionPointIds,
5
+ CommandContribution,
6
+ ConfigurationContribution,
7
+ } from "@pooder/core";
8
+ import { FabricImage as Image } from "fabric";
9
+ import CanvasService from "./CanvasService";
10
+
11
+ export class FilmTool implements Extension {
12
+ id = "pooder.kit.film";
13
+
14
+ public metadata = {
15
+ name: "FilmTool",
16
+ };
17
+
18
+ private url: string = "";
19
+ private opacity: number = 0.5;
20
+
21
+ private canvasService?: CanvasService;
22
+
23
+ constructor(
24
+ options?: Partial<{
25
+ url: string;
26
+ opacity: number;
27
+ }>,
28
+ ) {
29
+ if (options) {
30
+ Object.assign(this, options);
31
+ }
32
+ }
33
+
34
+ activate(context: ExtensionContext) {
35
+ this.canvasService = context.services.get<CanvasService>("CanvasService");
36
+ if (!this.canvasService) {
37
+ console.warn("CanvasService not found for FilmTool");
38
+ return;
39
+ }
40
+
41
+ const configService = context.services.get<any>("ConfigurationService");
42
+ if (configService) {
43
+ // Load initial config
44
+ this.url = configService.get("film.url", this.url);
45
+ this.opacity = configService.get("film.opacity", this.opacity);
46
+
47
+ // Listen for changes
48
+ configService.onAnyChange((e: { key: string; value: any }) => {
49
+ if (e.key.startsWith("film.")) {
50
+ const prop = e.key.split(".")[1];
51
+ console.log(
52
+ `[FilmTool] Config change detected: ${e.key} -> ${e.value}`,
53
+ );
54
+ if (prop && prop in this) {
55
+ (this as any)[prop] = e.value;
56
+ this.updateFilm();
57
+ }
58
+ }
59
+ });
60
+ }
61
+
62
+ this.initLayer();
63
+ this.updateFilm();
64
+ }
65
+
66
+ deactivate(context: ExtensionContext) {
67
+ if (this.canvasService) {
68
+ const layer = this.canvasService.getLayer("overlay");
69
+ if (layer) {
70
+ const img = this.canvasService.getObject("film-image", "overlay");
71
+ if (img) {
72
+ layer.remove(img);
73
+ this.canvasService.requestRenderAll();
74
+ }
75
+ }
76
+ this.canvasService = undefined;
77
+ }
78
+ }
79
+
80
+ contribute() {
81
+ return {
82
+ [ContributionPointIds.CONFIGURATIONS]: [
83
+ {
84
+ id: "film.url",
85
+ type: "string",
86
+ label: "Film Image URL",
87
+ default: "",
88
+ },
89
+ {
90
+ id: "film.opacity",
91
+ type: "number",
92
+ label: "Opacity",
93
+ min: 0,
94
+ max: 1,
95
+ step: 0.1,
96
+ default: 0.5,
97
+ },
98
+ ] as ConfigurationContribution[],
99
+ [ContributionPointIds.COMMANDS]: [
100
+ {
101
+ command: "setFilmImage",
102
+ title: "Set Film Image",
103
+ handler: (url: string, opacity: number) => {
104
+ if (this.url === url && this.opacity === opacity) return true;
105
+
106
+ this.url = url;
107
+ this.opacity = opacity;
108
+
109
+ this.updateFilm();
110
+
111
+ return true;
112
+ },
113
+ },
114
+ ] as CommandContribution[],
115
+ };
116
+ }
117
+
118
+ private initLayer() {
119
+ if (!this.canvasService) return;
120
+ let overlayLayer = this.canvasService.getLayer("overlay");
121
+ if (!overlayLayer) {
122
+ const width = this.canvasService.canvas.width || 800;
123
+ const height = this.canvasService.canvas.height || 600;
124
+
125
+ const layer = this.canvasService.createLayer("overlay", {
126
+ width,
127
+ height,
128
+ left: 0,
129
+ top: 0,
130
+ originX: "left",
131
+ originY: "top",
132
+ selectable: false,
133
+ evented: false,
134
+ subTargetCheck: false,
135
+ interactive: false,
136
+ });
137
+
138
+ this.canvasService.canvas.bringObjectToFront(layer);
139
+ }
140
+ }
141
+
142
+ private async updateFilm() {
143
+ if (!this.canvasService) return;
144
+ const layer = this.canvasService.getLayer("overlay");
145
+ if (!layer) {
146
+ console.warn("[FilmTool] Overlay layer not found");
147
+ return;
148
+ }
149
+
150
+ const { url, opacity } = this;
151
+
152
+ if (!url) {
153
+ const img = this.canvasService.getObject("film-image", "overlay");
154
+ if (img) {
155
+ layer.remove(img);
156
+ this.canvasService.requestRenderAll();
157
+ }
158
+ return;
159
+ }
160
+
161
+ const width = this.canvasService.canvas.width || 800;
162
+ const height = this.canvasService.canvas.height || 600;
163
+
164
+ let img = this.canvasService.getObject("film-image", "overlay") as Image;
165
+ try {
166
+ if (img) {
167
+ if (img.getSrc() !== url) {
168
+ await img.setSrc(url);
169
+ }
170
+ img.set({ opacity });
171
+ } else {
172
+ img = await Image.fromURL(url, { crossOrigin: "anonymous" });
173
+ img.scaleToWidth(width);
174
+ if (img.getScaledHeight() < height) img.scaleToHeight(height);
175
+ img.set({
176
+ originX: "left",
177
+ originY: "top",
178
+ left: 0,
179
+ top: 0,
180
+ opacity,
181
+ selectable: false,
182
+ evented: false,
183
+ data: { id: "film-image" },
184
+ });
185
+ layer.add(img);
186
+ }
187
+ this.canvasService.requestRenderAll();
188
+ } catch (error) {
189
+ console.error("[FilmTool] Failed to load film image", url, error);
190
+ }
191
+ layer.dirty = true;
192
+ this.canvasService.requestRenderAll();
193
+ }
194
+ }