@pooder/kit 5.4.0 → 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 (69) hide show
  1. package/.test-dist/src/coordinate.js +74 -0
  2. package/.test-dist/src/extensions/background.js +547 -0
  3. package/.test-dist/src/extensions/bridgeSelection.js +20 -0
  4. package/.test-dist/src/extensions/constraints.js +237 -0
  5. package/.test-dist/src/extensions/dieline.js +931 -0
  6. package/.test-dist/src/extensions/dielineShape.js +66 -0
  7. package/.test-dist/src/extensions/edgeScale.js +12 -0
  8. package/.test-dist/src/extensions/feature.js +910 -0
  9. package/.test-dist/src/extensions/featureComplete.js +32 -0
  10. package/.test-dist/src/extensions/film.js +226 -0
  11. package/.test-dist/src/extensions/geometry.js +609 -0
  12. package/.test-dist/src/extensions/image.js +1613 -0
  13. package/.test-dist/src/extensions/index.js +28 -0
  14. package/.test-dist/src/extensions/maskOps.js +334 -0
  15. package/.test-dist/src/extensions/mirror.js +104 -0
  16. package/.test-dist/src/extensions/ruler.js +442 -0
  17. package/.test-dist/src/extensions/sceneLayout.js +96 -0
  18. package/.test-dist/src/extensions/sceneLayoutModel.js +202 -0
  19. package/.test-dist/src/extensions/sceneVisibility.js +55 -0
  20. package/.test-dist/src/extensions/size.js +331 -0
  21. package/.test-dist/src/extensions/tracer.js +709 -0
  22. package/.test-dist/src/extensions/white-ink.js +1200 -0
  23. package/.test-dist/src/extensions/wrappedOffsets.js +33 -0
  24. package/.test-dist/src/index.js +18 -0
  25. package/.test-dist/src/services/CanvasService.js +1011 -0
  26. package/.test-dist/src/services/ViewportSystem.js +76 -0
  27. package/.test-dist/src/services/index.js +25 -0
  28. package/.test-dist/src/services/renderSpec.js +2 -0
  29. package/.test-dist/src/services/visibility.js +54 -0
  30. package/.test-dist/src/units.js +30 -0
  31. package/.test-dist/tests/run.js +148 -0
  32. package/CHANGELOG.md +6 -0
  33. package/dist/index.d.mts +150 -62
  34. package/dist/index.d.ts +150 -62
  35. package/dist/index.js +2219 -1714
  36. package/dist/index.mjs +2226 -1718
  37. package/package.json +1 -1
  38. package/src/coordinate.ts +106 -106
  39. package/src/extensions/background.ts +716 -323
  40. package/src/extensions/bridgeSelection.ts +17 -17
  41. package/src/extensions/constraints.ts +322 -322
  42. package/src/extensions/dieline.ts +1169 -1149
  43. package/src/extensions/dielineShape.ts +109 -109
  44. package/src/extensions/edgeScale.ts +19 -19
  45. package/src/extensions/feature.ts +1140 -1137
  46. package/src/extensions/featureComplete.ts +46 -46
  47. package/src/extensions/film.ts +270 -266
  48. package/src/extensions/geometry.ts +851 -885
  49. package/src/extensions/image.ts +2007 -2054
  50. package/src/extensions/index.ts +10 -11
  51. package/src/extensions/maskOps.ts +283 -283
  52. package/src/extensions/mirror.ts +128 -128
  53. package/src/extensions/ruler.ts +664 -654
  54. package/src/extensions/sceneLayout.ts +140 -140
  55. package/src/extensions/sceneLayoutModel.ts +364 -364
  56. package/src/extensions/size.ts +389 -389
  57. package/src/extensions/tracer.ts +1019 -1019
  58. package/src/extensions/white-ink.ts +1508 -1575
  59. package/src/extensions/wrappedOffsets.ts +33 -33
  60. package/src/index.ts +2 -2
  61. package/src/services/CanvasService.ts +1286 -832
  62. package/src/services/ViewportSystem.ts +95 -95
  63. package/src/services/index.ts +4 -3
  64. package/src/services/renderSpec.ts +83 -53
  65. package/src/services/visibility.ts +78 -0
  66. package/src/units.ts +27 -27
  67. package/tests/run.ts +253 -118
  68. package/tsconfig.test.json +15 -15
  69. package/src/extensions/sceneVisibility.ts +0 -64
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Coordinate = void 0;
4
+ class Coordinate {
5
+ /**
6
+ * Calculate layout to fit content within container while preserving aspect ratio.
7
+ */
8
+ static calculateLayout(container, content, padding = 0) {
9
+ const availableWidth = Math.max(0, container.width - padding * 2);
10
+ const availableHeight = Math.max(0, container.height - padding * 2);
11
+ if (content.width === 0 || content.height === 0) {
12
+ return { scale: 1, offsetX: 0, offsetY: 0, width: 0, height: 0 };
13
+ }
14
+ const scaleX = availableWidth / content.width;
15
+ const scaleY = availableHeight / content.height;
16
+ const scale = Math.min(scaleX, scaleY);
17
+ const width = content.width * scale;
18
+ const height = content.height * scale;
19
+ const offsetX = (container.width - width) / 2;
20
+ const offsetY = (container.height - height) / 2;
21
+ return { scale, offsetX, offsetY, width, height };
22
+ }
23
+ /**
24
+ * Convert an absolute value to a normalized value (0-1).
25
+ * @param value Absolute value (e.g., pixels)
26
+ * @param total Total dimension size (e.g., canvas width)
27
+ */
28
+ static toNormalized(value, total) {
29
+ return total === 0 ? 0 : value / total;
30
+ }
31
+ /**
32
+ * Convert a normalized value (0-1) to an absolute value.
33
+ * @param normalized Normalized value (0-1)
34
+ * @param total Total dimension size (e.g., canvas width)
35
+ */
36
+ static toAbsolute(normalized, total) {
37
+ return normalized * total;
38
+ }
39
+ /**
40
+ * Normalize a point's coordinates.
41
+ */
42
+ static normalizePoint(point, size) {
43
+ return {
44
+ x: this.toNormalized(point.x, size.width),
45
+ y: this.toNormalized(point.y, size.height),
46
+ };
47
+ }
48
+ /**
49
+ * Denormalize a point's coordinates to absolute pixels.
50
+ */
51
+ static denormalizePoint(point, size) {
52
+ return {
53
+ x: this.toAbsolute(point.x, size.width),
54
+ y: this.toAbsolute(point.y, size.height),
55
+ };
56
+ }
57
+ static convertUnit(value, from, to) {
58
+ if (from === to)
59
+ return value;
60
+ // Base unit: mm
61
+ const toMM = {
62
+ px: 0.264583, // 1px = 0.264583mm (96 DPI)
63
+ mm: 1,
64
+ cm: 10,
65
+ in: 25.4
66
+ };
67
+ const mmValue = value * (from === 'px' ? toMM.px : toMM[from] || 1);
68
+ if (to === 'px') {
69
+ return mmValue / toMM.px;
70
+ }
71
+ return mmValue / (toMM[to] || 1);
72
+ }
73
+ }
74
+ exports.Coordinate = Coordinate;
@@ -0,0 +1,547 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BackgroundTool = void 0;
4
+ const core_1 = require("@pooder/core");
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
+ }
156
+ class BackgroundTool {
157
+ constructor(options) {
158
+ this.id = "pooder.kit.background";
159
+ this.metadata = {
160
+ name: "BackgroundTool",
161
+ };
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);
178
+ }
179
+ }
180
+ activate(context) {
181
+ this.canvasService = context.services.get("CanvasService");
182
+ if (!this.canvasService) {
183
+ console.warn("CanvasService not found for BackgroundTool");
184
+ return;
185
+ }
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();
199
+ }
200
+ });
201
+ }
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);
215
+ this.updateBackground();
216
+ }
217
+ deactivate(context) {
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;
233
+ }
234
+ contribute() {
235
+ return {
236
+ [core_1.ContributionPointIds.CONFIGURATIONS]: [
237
+ {
238
+ id: BACKGROUND_CONFIG_KEY,
239
+ type: "json",
240
+ label: "Background Config",
241
+ default: cloneConfig(DEFAULT_BACKGROUND_CONFIG),
242
+ },
243
+ ],
244
+ [core_1.ContributionPointIds.COMMANDS]: [
245
+ {
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",
253
+ handler: () => {
254
+ this.commitConfig(cloneConfig(DEFAULT_BACKGROUND_CONFIG));
255
+ return true;
256
+ },
257
+ },
258
+ {
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 || {}));
271
+ return true;
272
+ },
273
+ },
274
+ {
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
+ }));
296
+ return true;
297
+ },
298
+ },
299
+ {
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
+ }));
308
+ return true;
309
+ },
310
+ },
311
+ ],
312
+ };
313
+ }
314
+ commitConfig(next) {
315
+ const normalized = normalizeConfig(next);
316
+ if (configSignature(normalized) === configSignature(this.config)) {
317
+ return;
318
+ }
319
+ if (this.configService) {
320
+ this.configService.update(BACKGROUND_CONFIG_KEY, cloneConfig(normalized));
321
+ return;
322
+ }
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();
362
+ }
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,
412
+ selectable: false,
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",
433
+ data: {
434
+ id: `background.layer.${layer.id}.image`,
435
+ layerId: BACKGROUND_LAYER_ID,
436
+ type: "background-layer",
437
+ layerRef: layer.id,
438
+ layerKind: layer.kind,
439
+ },
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;
496
+ }
497
+ const task = this.loadImageSize(src);
498
+ this.pendingSizeBySrc.set(src, task);
499
+ try {
500
+ return await task;
501
+ }
502
+ finally {
503
+ if (this.pendingSizeBySrc.get(src) === task) {
504
+ this.pendingSizeBySrc.delete(src);
505
+ }
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;
519
+ }
520
+ }
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;
539
+ }
540
+ this.specs = this.buildBackgroundSpecs(currentConfig);
541
+ await this.canvasService.flushRenderFromProducers();
542
+ if (seq !== this.renderSeq)
543
+ return;
544
+ this.canvasService.requestRenderAll();
545
+ }
546
+ }
547
+ exports.BackgroundTool = BackgroundTool;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pickExitIndex = pickExitIndex;
4
+ exports.scoreOutsideAbove = scoreOutsideAbove;
5
+ function pickExitIndex(hits) {
6
+ for (let i = 0; i < hits.length; i++) {
7
+ const h = hits[i];
8
+ if (h.insideBelow && !h.insideAbove)
9
+ return i;
10
+ }
11
+ return -1;
12
+ }
13
+ function scoreOutsideAbove(samples) {
14
+ let score = 0;
15
+ for (const s of samples) {
16
+ if (s.outsideAbove)
17
+ score++;
18
+ }
19
+ return score;
20
+ }