@pooder/kit 5.3.1 → 6.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 (65) hide show
  1. package/.test-dist/src/extensions/background.js +475 -131
  2. package/.test-dist/src/extensions/dieline.js +283 -180
  3. package/.test-dist/src/extensions/dielineShape.js +66 -0
  4. package/.test-dist/src/extensions/feature.js +388 -303
  5. package/.test-dist/src/extensions/film.js +133 -74
  6. package/.test-dist/src/extensions/geometry.js +120 -56
  7. package/.test-dist/src/extensions/image.js +296 -212
  8. package/.test-dist/src/extensions/index.js +1 -3
  9. package/.test-dist/src/extensions/maskOps.js +75 -20
  10. package/.test-dist/src/extensions/ruler.js +312 -215
  11. package/.test-dist/src/extensions/sceneLayoutModel.js +9 -3
  12. package/.test-dist/src/extensions/sceneVisibility.js +3 -10
  13. package/.test-dist/src/extensions/tracer.js +229 -58
  14. package/.test-dist/src/extensions/white-ink.js +139 -129
  15. package/.test-dist/src/services/CanvasService.js +888 -126
  16. package/.test-dist/src/services/index.js +1 -0
  17. package/.test-dist/src/services/visibility.js +54 -0
  18. package/.test-dist/tests/run.js +58 -4
  19. package/CHANGELOG.md +12 -0
  20. package/dist/index.d.mts +377 -82
  21. package/dist/index.d.ts +377 -82
  22. package/dist/index.js +3920 -2178
  23. package/dist/index.mjs +3992 -2247
  24. package/package.json +1 -1
  25. package/src/extensions/background.ts +631 -145
  26. package/src/extensions/dieline.ts +280 -187
  27. package/src/extensions/dielineShape.ts +109 -0
  28. package/src/extensions/feature.ts +485 -366
  29. package/src/extensions/film.ts +152 -76
  30. package/src/extensions/geometry.ts +203 -104
  31. package/src/extensions/image.ts +319 -238
  32. package/src/extensions/index.ts +0 -1
  33. package/src/extensions/ruler.ts +481 -268
  34. package/src/extensions/sceneLayoutModel.ts +18 -6
  35. package/src/extensions/white-ink.ts +157 -171
  36. package/src/services/CanvasService.ts +1126 -140
  37. package/src/services/index.ts +1 -0
  38. package/src/services/renderSpec.ts +69 -4
  39. package/src/services/visibility.ts +78 -0
  40. package/tests/run.ts +139 -4
  41. package/.test-dist/src/CanvasService.js +0 -249
  42. package/.test-dist/src/ViewportSystem.js +0 -75
  43. package/.test-dist/src/background.js +0 -203
  44. package/.test-dist/src/bridgeSelection.js +0 -20
  45. package/.test-dist/src/constraints.js +0 -237
  46. package/.test-dist/src/dieline.js +0 -818
  47. package/.test-dist/src/edgeScale.js +0 -12
  48. package/.test-dist/src/feature.js +0 -826
  49. package/.test-dist/src/featureComplete.js +0 -32
  50. package/.test-dist/src/film.js +0 -167
  51. package/.test-dist/src/geometry.js +0 -506
  52. package/.test-dist/src/image.js +0 -1250
  53. package/.test-dist/src/maskOps.js +0 -270
  54. package/.test-dist/src/mirror.js +0 -104
  55. package/.test-dist/src/renderSpec.js +0 -2
  56. package/.test-dist/src/ruler.js +0 -343
  57. package/.test-dist/src/sceneLayout.js +0 -99
  58. package/.test-dist/src/sceneLayoutModel.js +0 -196
  59. package/.test-dist/src/sceneView.js +0 -40
  60. package/.test-dist/src/sceneVisibility.js +0 -42
  61. package/.test-dist/src/size.js +0 -332
  62. package/.test-dist/src/tracer.js +0 -544
  63. package/.test-dist/src/white-ink.js +0 -829
  64. package/.test-dist/src/wrappedOffsets.js +0 -33
  65. package/src/extensions/sceneVisibility.ts +0 -71
@@ -3,16 +3,178 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BackgroundTool = void 0;
4
4
  const core_1 = require("@pooder/core");
5
5
  const fabric_1 = require("fabric");
6
+ const sceneLayoutModel_1 = require("./sceneLayoutModel");
7
+ const BACKGROUND_LAYER_ID = "background";
8
+ const BACKGROUND_CONFIG_KEY = "background.config";
9
+ const DEFAULT_WIDTH = 800;
10
+ const DEFAULT_HEIGHT = 600;
11
+ const DEFAULT_BACKGROUND_CONFIG = {
12
+ version: 1,
13
+ layers: [
14
+ {
15
+ id: "base-color",
16
+ kind: "color",
17
+ anchor: "viewport",
18
+ fit: "cover",
19
+ opacity: 1,
20
+ order: 0,
21
+ enabled: true,
22
+ exportable: false,
23
+ color: "#aaa",
24
+ },
25
+ ],
26
+ };
27
+ function clampOpacity(value, fallback) {
28
+ const numeric = Number(value);
29
+ if (!Number.isFinite(numeric)) {
30
+ return Math.max(0, Math.min(1, fallback));
31
+ }
32
+ return Math.max(0, Math.min(1, numeric));
33
+ }
34
+ function normalizeLayerKind(value, fallback) {
35
+ if (value === "color" || value === "image") {
36
+ return value;
37
+ }
38
+ return fallback;
39
+ }
40
+ function normalizeFitMode(value, fallback) {
41
+ if (value === "contain" || value === "cover" || value === "stretch") {
42
+ return value;
43
+ }
44
+ return fallback;
45
+ }
46
+ function normalizeAnchor(value, fallback) {
47
+ if (typeof value !== "string")
48
+ return fallback;
49
+ const trimmed = value.trim();
50
+ return trimmed || fallback;
51
+ }
52
+ function normalizeOrder(value, fallback) {
53
+ const numeric = Number(value);
54
+ if (!Number.isFinite(numeric))
55
+ return fallback;
56
+ return numeric;
57
+ }
58
+ function normalizeLayer(raw, index, fallback) {
59
+ const fallbackLayer = fallback || {
60
+ id: `layer-${index + 1}`,
61
+ kind: "image",
62
+ anchor: "viewport",
63
+ fit: "contain",
64
+ opacity: 1,
65
+ order: index,
66
+ enabled: true,
67
+ exportable: false,
68
+ src: "",
69
+ };
70
+ if (!raw || typeof raw !== "object") {
71
+ return { ...fallbackLayer };
72
+ }
73
+ const input = raw;
74
+ const kind = normalizeLayerKind(input.kind, fallbackLayer.kind);
75
+ return {
76
+ id: typeof input.id === "string" && input.id.trim().length > 0
77
+ ? input.id.trim()
78
+ : fallbackLayer.id,
79
+ kind,
80
+ anchor: normalizeAnchor(input.anchor, fallbackLayer.anchor),
81
+ fit: normalizeFitMode(input.fit, fallbackLayer.fit),
82
+ opacity: clampOpacity(input.opacity, fallbackLayer.opacity),
83
+ order: normalizeOrder(input.order, fallbackLayer.order),
84
+ enabled: typeof input.enabled === "boolean"
85
+ ? input.enabled
86
+ : fallbackLayer.enabled,
87
+ exportable: typeof input.exportable === "boolean"
88
+ ? input.exportable
89
+ : fallbackLayer.exportable,
90
+ color: kind === "color"
91
+ ? typeof input.color === "string"
92
+ ? input.color
93
+ : typeof fallbackLayer.color === "string"
94
+ ? fallbackLayer.color
95
+ : "#ffffff"
96
+ : undefined,
97
+ src: kind === "image"
98
+ ? typeof input.src === "string"
99
+ ? input.src.trim()
100
+ : typeof fallbackLayer.src === "string"
101
+ ? fallbackLayer.src
102
+ : ""
103
+ : undefined,
104
+ };
105
+ }
106
+ function normalizeConfig(raw) {
107
+ if (!raw || typeof raw !== "object") {
108
+ return cloneConfig(DEFAULT_BACKGROUND_CONFIG);
109
+ }
110
+ const input = raw;
111
+ const version = Number.isFinite(Number(input.version))
112
+ ? Number(input.version)
113
+ : DEFAULT_BACKGROUND_CONFIG.version;
114
+ const baseLayers = Array.isArray(input.layers)
115
+ ? input.layers.map((layer, index) => normalizeLayer(layer, index))
116
+ : cloneConfig(DEFAULT_BACKGROUND_CONFIG).layers;
117
+ const uniqueLayers = [];
118
+ const seen = new Set();
119
+ baseLayers.forEach((layer, index) => {
120
+ let nextId = layer.id || `layer-${index + 1}`;
121
+ let serial = 1;
122
+ while (seen.has(nextId)) {
123
+ serial += 1;
124
+ nextId = `${layer.id || `layer-${index + 1}`}-${serial}`;
125
+ }
126
+ seen.add(nextId);
127
+ uniqueLayers.push({ ...layer, id: nextId });
128
+ });
129
+ return {
130
+ version,
131
+ layers: uniqueLayers,
132
+ };
133
+ }
134
+ function cloneConfig(config) {
135
+ return {
136
+ version: config.version,
137
+ layers: (config.layers || []).map((layer) => ({ ...layer })),
138
+ };
139
+ }
140
+ function mergeConfig(base, patch) {
141
+ const merged = {
142
+ version: patch.version === undefined
143
+ ? base.version
144
+ : Number.isFinite(Number(patch.version))
145
+ ? Number(patch.version)
146
+ : base.version,
147
+ layers: Array.isArray(patch.layers)
148
+ ? patch.layers.map((layer, index) => normalizeLayer(layer, index))
149
+ : base.layers.map((layer) => ({ ...layer })),
150
+ };
151
+ return normalizeConfig(merged);
152
+ }
153
+ function configSignature(config) {
154
+ return JSON.stringify(config);
155
+ }
6
156
  class BackgroundTool {
7
157
  constructor(options) {
8
158
  this.id = "pooder.kit.background";
9
159
  this.metadata = {
10
160
  name: "BackgroundTool",
11
161
  };
12
- this.color = "";
13
- this.url = "";
14
- if (options) {
15
- Object.assign(this, options);
162
+ this.config = cloneConfig(DEFAULT_BACKGROUND_CONFIG);
163
+ this.specs = [];
164
+ this.renderSeq = 0;
165
+ this.latestSceneLayout = null;
166
+ this.sourceSizeBySrc = new Map();
167
+ this.pendingSizeBySrc = new Map();
168
+ this.onCanvasResized = () => {
169
+ this.latestSceneLayout = null;
170
+ this.updateBackground();
171
+ };
172
+ this.onSceneLayoutChanged = (layout) => {
173
+ this.latestSceneLayout = layout;
174
+ this.updateBackground();
175
+ };
176
+ if (options && typeof options === "object") {
177
+ this.config = mergeConfig(this.config, options);
16
178
  }
17
179
  }
18
180
  activate(context) {
@@ -21,182 +183,364 @@ class BackgroundTool {
21
183
  console.warn("CanvasService not found for BackgroundTool");
22
184
  return;
23
185
  }
24
- const configService = context.services.get("ConfigurationService");
25
- if (configService) {
26
- // Load initial config
27
- this.color = configService.get("background.color", this.color);
28
- this.url = configService.get("background.url", this.url);
29
- // Listen for changes
30
- configService.onAnyChange((e) => {
31
- if (e.key.startsWith("background.")) {
32
- const prop = e.key.split(".")[1];
33
- console.log(`[BackgroundTool] Config change detected: ${e.key} -> ${e.value}, prop: ${prop}`);
34
- if (prop && prop in this) {
35
- console.log(`[BackgroundTool] Updating option ${prop} to ${e.value}`);
36
- this[prop] = e.value;
37
- this.updateBackground();
38
- }
39
- else {
40
- console.warn(`[BackgroundTool] Property ${prop} not found in options`);
41
- }
186
+ this.configService = context.services.get("ConfigurationService");
187
+ if (this.configService) {
188
+ this.config = normalizeConfig(this.configService.get(BACKGROUND_CONFIG_KEY, DEFAULT_BACKGROUND_CONFIG));
189
+ this.configChangeDisposable?.dispose();
190
+ this.configChangeDisposable = this.configService.onAnyChange((e) => {
191
+ if (e.key === BACKGROUND_CONFIG_KEY) {
192
+ this.config = normalizeConfig(e.value);
193
+ this.updateBackground();
194
+ return;
195
+ }
196
+ if (e.key.startsWith("size.")) {
197
+ this.latestSceneLayout = null;
198
+ this.updateBackground();
42
199
  }
43
200
  });
44
201
  }
45
- this.initLayer();
202
+ this.renderProducerDisposable?.dispose();
203
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(this.id, () => ({
204
+ passes: [
205
+ {
206
+ id: BACKGROUND_LAYER_ID,
207
+ stack: 0,
208
+ order: 0,
209
+ objects: this.specs,
210
+ },
211
+ ],
212
+ }), { priority: 0 });
213
+ context.eventBus.on("canvas:resized", this.onCanvasResized);
214
+ context.eventBus.on("scene:layout:change", this.onSceneLayoutChanged);
46
215
  this.updateBackground();
47
216
  }
48
217
  deactivate(context) {
49
- if (this.canvasService) {
50
- const layer = this.canvasService.getLayer("background");
51
- if (layer) {
52
- this.canvasService.canvas.remove(layer);
53
- }
54
- this.canvasService = undefined;
55
- }
218
+ context.eventBus.off("canvas:resized", this.onCanvasResized);
219
+ context.eventBus.off("scene:layout:change", this.onSceneLayoutChanged);
220
+ this.renderSeq += 1;
221
+ this.specs = [];
222
+ this.latestSceneLayout = null;
223
+ this.configChangeDisposable?.dispose();
224
+ this.configChangeDisposable = undefined;
225
+ this.renderProducerDisposable?.dispose();
226
+ this.renderProducerDisposable = undefined;
227
+ if (!this.canvasService)
228
+ return;
229
+ void this.canvasService.flushRenderFromProducers();
230
+ this.canvasService.requestRenderAll();
231
+ this.canvasService = undefined;
232
+ this.configService = undefined;
56
233
  }
57
234
  contribute() {
58
235
  return {
59
236
  [core_1.ContributionPointIds.CONFIGURATIONS]: [
60
237
  {
61
- id: "background.color",
62
- type: "color",
63
- label: "Background Color",
64
- default: "",
65
- },
66
- {
67
- id: "background.url",
68
- type: "string",
69
- label: "Image URL",
70
- default: "",
238
+ id: BACKGROUND_CONFIG_KEY,
239
+ type: "json",
240
+ label: "Background Config",
241
+ default: cloneConfig(DEFAULT_BACKGROUND_CONFIG),
71
242
  },
72
243
  ],
73
244
  [core_1.ContributionPointIds.COMMANDS]: [
74
245
  {
75
- command: "reset",
76
- title: "Reset Background",
246
+ command: "background.getConfig",
247
+ title: "Get Background Config",
248
+ handler: () => cloneConfig(this.config),
249
+ },
250
+ {
251
+ command: "background.resetConfig",
252
+ title: "Reset Background Config",
77
253
  handler: () => {
78
- this.updateBackground();
254
+ this.commitConfig(cloneConfig(DEFAULT_BACKGROUND_CONFIG));
79
255
  return true;
80
256
  },
81
257
  },
82
258
  {
83
- command: "clear",
84
- title: "Clear Background",
85
- handler: () => {
86
- this.color = "transparent";
87
- this.url = "";
88
- this.updateBackground();
259
+ command: "background.replaceConfig",
260
+ title: "Replace Background Config",
261
+ handler: (config) => {
262
+ this.commitConfig(normalizeConfig(config));
263
+ return true;
264
+ },
265
+ },
266
+ {
267
+ command: "background.patchConfig",
268
+ title: "Patch Background Config",
269
+ handler: (patch) => {
270
+ this.commitConfig(mergeConfig(this.config, patch || {}));
89
271
  return true;
90
272
  },
91
273
  },
92
274
  {
93
- command: "setBackgroundColor",
94
- title: "Set Background Color",
95
- handler: (color) => {
96
- if (this.color === color)
97
- return true;
98
- this.color = color;
99
- this.updateBackground();
275
+ command: "background.upsertLayer",
276
+ title: "Upsert Background Layer",
277
+ handler: (layer) => {
278
+ const normalized = normalizeLayer(layer, 0);
279
+ const existingIndex = this.config.layers.findIndex((item) => item.id === normalized.id);
280
+ const nextLayers = [...this.config.layers];
281
+ if (existingIndex >= 0) {
282
+ nextLayers[existingIndex] = normalizeLayer({ ...nextLayers[existingIndex], ...layer }, existingIndex, nextLayers[existingIndex]);
283
+ }
284
+ else {
285
+ nextLayers.push(normalizeLayer({
286
+ ...normalized,
287
+ order: Number.isFinite(Number(layer.order))
288
+ ? Number(layer.order)
289
+ : nextLayers.length,
290
+ }, nextLayers.length));
291
+ }
292
+ this.commitConfig(normalizeConfig({
293
+ ...this.config,
294
+ layers: nextLayers,
295
+ }));
100
296
  return true;
101
297
  },
102
298
  },
103
299
  {
104
- command: "setBackgroundImage",
105
- title: "Set Background Image",
106
- handler: (url) => {
107
- if (this.url === url)
108
- return true;
109
- this.url = url;
110
- this.updateBackground();
300
+ command: "background.removeLayer",
301
+ title: "Remove Background Layer",
302
+ handler: (id) => {
303
+ const nextLayers = this.config.layers.filter((layer) => layer.id !== id);
304
+ this.commitConfig(normalizeConfig({
305
+ ...this.config,
306
+ layers: nextLayers,
307
+ }));
111
308
  return true;
112
309
  },
113
310
  },
114
311
  ],
115
312
  };
116
313
  }
117
- initLayer() {
118
- if (!this.canvasService)
314
+ commitConfig(next) {
315
+ const normalized = normalizeConfig(next);
316
+ if (configSignature(normalized) === configSignature(this.config)) {
119
317
  return;
120
- let backgroundLayer = this.canvasService.getLayer("background");
121
- if (!backgroundLayer) {
122
- backgroundLayer = this.canvasService.createLayer("background", {
123
- width: this.canvasService.canvas.width,
124
- height: this.canvasService.canvas.height,
125
- selectable: false,
126
- evented: false,
127
- });
128
- this.canvasService.canvas.sendObjectToBack(backgroundLayer);
129
318
  }
130
- }
131
- async updateBackground() {
132
- if (!this.canvasService)
133
- return;
134
- const layer = this.canvasService.getLayer("background");
135
- if (!layer) {
136
- console.warn("[BackgroundTool] Background layer not found");
319
+ if (this.configService) {
320
+ this.configService.update(BACKGROUND_CONFIG_KEY, cloneConfig(normalized));
137
321
  return;
138
322
  }
139
- const { color, url } = this;
140
- const width = this.canvasService.canvas.width || 800;
141
- const height = this.canvasService.canvas.height || 600;
142
- let rect = this.canvasService.getObject("background-color-rect", "background");
143
- if (rect) {
144
- rect.set({
145
- fill: color,
146
- });
323
+ this.config = normalized;
324
+ this.updateBackground();
325
+ }
326
+ getViewportRect() {
327
+ const width = Number(this.canvasService?.canvas.width || 0);
328
+ const height = Number(this.canvasService?.canvas.height || 0);
329
+ return {
330
+ left: 0,
331
+ top: 0,
332
+ width: width > 0 ? width : DEFAULT_WIDTH,
333
+ height: height > 0 ? height : DEFAULT_HEIGHT,
334
+ };
335
+ }
336
+ resolveSceneLayout() {
337
+ if (this.latestSceneLayout)
338
+ return this.latestSceneLayout;
339
+ if (!this.canvasService || !this.configService)
340
+ return null;
341
+ const layout = (0, sceneLayoutModel_1.computeSceneLayout)(this.canvasService, (0, sceneLayoutModel_1.readSizeState)(this.configService));
342
+ this.latestSceneLayout = layout;
343
+ return layout;
344
+ }
345
+ resolveFocusRect() {
346
+ const layout = this.resolveSceneLayout();
347
+ if (!layout)
348
+ return null;
349
+ return {
350
+ left: layout.trimRect.left,
351
+ top: layout.trimRect.top,
352
+ width: layout.trimRect.width,
353
+ height: layout.trimRect.height,
354
+ };
355
+ }
356
+ resolveAnchorRect(anchor) {
357
+ if (anchor === "focus") {
358
+ return this.resolveFocusRect() || this.getViewportRect();
359
+ }
360
+ if (anchor !== "viewport") {
361
+ return this.getViewportRect();
147
362
  }
148
- else {
149
- rect = new fabric_1.Rect({
150
- width,
151
- height,
152
- fill: color,
363
+ return this.getViewportRect();
364
+ }
365
+ resolveImagePlacement(target, sourceSize, fit) {
366
+ const targetWidth = Math.max(1, Number(target.width || 0));
367
+ const targetHeight = Math.max(1, Number(target.height || 0));
368
+ const sourceWidth = Math.max(1, Number(sourceSize.width || 0));
369
+ const sourceHeight = Math.max(1, Number(sourceSize.height || 0));
370
+ if (fit === "stretch") {
371
+ return {
372
+ left: target.left,
373
+ top: target.top,
374
+ scaleX: targetWidth / sourceWidth,
375
+ scaleY: targetHeight / sourceHeight,
376
+ };
377
+ }
378
+ const scale = fit === "contain"
379
+ ? Math.min(targetWidth / sourceWidth, targetHeight / sourceHeight)
380
+ : Math.max(targetWidth / sourceWidth, targetHeight / sourceHeight);
381
+ const renderWidth = sourceWidth * scale;
382
+ const renderHeight = sourceHeight * scale;
383
+ return {
384
+ left: target.left + (targetWidth - renderWidth) / 2,
385
+ top: target.top + (targetHeight - renderHeight) / 2,
386
+ scaleX: scale,
387
+ scaleY: scale,
388
+ };
389
+ }
390
+ buildColorLayerSpec(layer) {
391
+ const rect = this.resolveAnchorRect(layer.anchor);
392
+ return {
393
+ id: `background.layer.${layer.id}.color`,
394
+ type: "rect",
395
+ space: "screen",
396
+ data: {
397
+ id: `background.layer.${layer.id}.color`,
398
+ layerId: BACKGROUND_LAYER_ID,
399
+ type: "background-layer",
400
+ layerRef: layer.id,
401
+ layerKind: layer.kind,
402
+ },
403
+ props: {
404
+ left: rect.left,
405
+ top: rect.top,
406
+ width: rect.width,
407
+ height: rect.height,
408
+ originX: "left",
409
+ originY: "top",
410
+ fill: layer.color || "transparent",
411
+ opacity: layer.opacity,
153
412
  selectable: false,
154
413
  evented: false,
414
+ excludeFromExport: !layer.exportable,
415
+ },
416
+ };
417
+ }
418
+ buildImageLayerSpec(layer) {
419
+ const src = String(layer.src || "").trim();
420
+ if (!src)
421
+ return [];
422
+ const sourceSize = this.sourceSizeBySrc.get(src);
423
+ if (!sourceSize)
424
+ return [];
425
+ const rect = this.resolveAnchorRect(layer.anchor);
426
+ const placement = this.resolveImagePlacement(rect, sourceSize, layer.fit);
427
+ return [
428
+ {
429
+ id: `background.layer.${layer.id}.image`,
430
+ type: "image",
431
+ src,
432
+ space: "screen",
155
433
  data: {
156
- id: "background-color-rect",
434
+ id: `background.layer.${layer.id}.image`,
435
+ layerId: BACKGROUND_LAYER_ID,
436
+ type: "background-layer",
437
+ layerRef: layer.id,
438
+ layerKind: layer.kind,
157
439
  },
158
- });
159
- layer.add(rect);
160
- layer.sendObjectToBack(rect);
440
+ props: {
441
+ left: placement.left,
442
+ top: placement.top,
443
+ originX: "left",
444
+ originY: "top",
445
+ scaleX: placement.scaleX,
446
+ scaleY: placement.scaleY,
447
+ opacity: layer.opacity,
448
+ selectable: false,
449
+ evented: false,
450
+ excludeFromExport: !layer.exportable,
451
+ },
452
+ },
453
+ ];
454
+ }
455
+ buildBackgroundSpecs(config) {
456
+ const activeLayers = (config.layers || [])
457
+ .filter((layer) => layer.enabled)
458
+ .map((layer, index) => ({ layer, index }))
459
+ .sort((a, b) => {
460
+ if (a.layer.order !== b.layer.order) {
461
+ return a.layer.order - b.layer.order;
462
+ }
463
+ return a.index - b.index;
464
+ });
465
+ const specs = [];
466
+ activeLayers.forEach(({ layer }) => {
467
+ if (layer.kind === "color") {
468
+ specs.push(this.buildColorLayerSpec(layer));
469
+ return;
470
+ }
471
+ specs.push(...this.buildImageLayerSpec(layer));
472
+ });
473
+ return specs;
474
+ }
475
+ collectActiveImageUrls(config) {
476
+ const urls = new Set();
477
+ (config.layers || []).forEach((layer) => {
478
+ if (!layer.enabled || layer.kind !== "image")
479
+ return;
480
+ const src = String(layer.src || "").trim();
481
+ if (!src)
482
+ return;
483
+ urls.add(src);
484
+ });
485
+ return Array.from(urls);
486
+ }
487
+ async ensureImageSize(src) {
488
+ if (!src)
489
+ return null;
490
+ const cached = this.sourceSizeBySrc.get(src);
491
+ if (cached)
492
+ return cached;
493
+ const pending = this.pendingSizeBySrc.get(src);
494
+ if (pending) {
495
+ return pending;
161
496
  }
162
- let img = this.canvasService.getObject("background-image", "background");
497
+ const task = this.loadImageSize(src);
498
+ this.pendingSizeBySrc.set(src, task);
163
499
  try {
164
- if (img) {
165
- if (img.getSrc() !== url) {
166
- if (url) {
167
- await img.setSrc(url);
168
- }
169
- else {
170
- layer.remove(img);
171
- }
172
- }
500
+ return await task;
501
+ }
502
+ finally {
503
+ if (this.pendingSizeBySrc.get(src) === task) {
504
+ this.pendingSizeBySrc.delete(src);
173
505
  }
174
- else {
175
- if (url) {
176
- img = await fabric_1.FabricImage.fromURL(url, { crossOrigin: "anonymous" });
177
- img.set({
178
- originX: "left",
179
- originY: "top",
180
- left: 0,
181
- top: 0,
182
- selectable: false,
183
- evented: false,
184
- data: {
185
- id: "background-image",
186
- },
187
- });
188
- img.scaleToWidth(width);
189
- if (img.getScaledHeight() < height)
190
- img.scaleToHeight(height);
191
- layer.add(img);
192
- }
506
+ }
507
+ }
508
+ async loadImageSize(src) {
509
+ try {
510
+ const image = await fabric_1.FabricImage.fromURL(src, {
511
+ crossOrigin: "anonymous",
512
+ });
513
+ const width = Number(image?.width || 0);
514
+ const height = Number(image?.height || 0);
515
+ if (width > 0 && height > 0) {
516
+ const size = { width, height };
517
+ this.sourceSizeBySrc.set(src, size);
518
+ return size;
193
519
  }
194
- this.canvasService.requestRenderAll();
195
520
  }
196
- catch (e) {
197
- console.error("[BackgroundTool] Failed to load image", e);
521
+ catch (error) {
522
+ console.error("[BackgroundTool] Failed to load image", src, error);
523
+ }
524
+ return null;
525
+ }
526
+ updateBackground() {
527
+ void this.updateBackgroundAsync();
528
+ }
529
+ async updateBackgroundAsync() {
530
+ if (!this.canvasService)
531
+ return;
532
+ const seq = ++this.renderSeq;
533
+ const currentConfig = cloneConfig(this.config);
534
+ const activeUrls = this.collectActiveImageUrls(currentConfig);
535
+ if (activeUrls.length > 0) {
536
+ await Promise.all(activeUrls.map((url) => this.ensureImageSize(url)));
537
+ if (seq !== this.renderSeq)
538
+ return;
198
539
  }
199
- layer.dirty = true;
540
+ this.specs = this.buildBackgroundSpecs(currentConfig);
541
+ await this.canvasService.flushRenderFromProducers();
542
+ if (seq !== this.renderSeq)
543
+ return;
200
544
  this.canvasService.requestRenderAll();
201
545
  }
202
546
  }