@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
package/dist/index.js CHANGED
@@ -39,294 +39,104 @@ __export(index_exports, {
39
39
  MirrorTool: () => MirrorTool,
40
40
  RulerTool: () => RulerTool,
41
41
  SceneLayoutService: () => SceneLayoutService,
42
- SceneVisibilityService: () => SceneVisibilityService,
43
42
  SizeTool: () => SizeTool,
44
43
  ViewportSystem: () => ViewportSystem,
45
- WhiteInkTool: () => WhiteInkTool
44
+ WhiteInkTool: () => WhiteInkTool,
45
+ evaluateVisibilityExpr: () => evaluateVisibilityExpr
46
46
  });
47
47
  module.exports = __toCommonJS(index_exports);
48
48
 
49
49
  // src/extensions/background.ts
50
50
  var import_core = require("@pooder/core");
51
51
  var import_fabric = require("fabric");
52
- var BACKGROUND_LAYER_ID = "background";
53
- var BACKGROUND_RECT_ID = "background-color-rect";
54
- var BACKGROUND_IMAGE_ID = "background-image";
55
- var DEFAULT_WIDTH = 800;
56
- var DEFAULT_HEIGHT = 600;
57
- var BackgroundTool = class {
58
- constructor(options) {
59
- this.id = "pooder.kit.background";
60
- this.metadata = {
61
- name: "BackgroundTool"
62
- };
63
- this.color = "";
64
- this.url = "";
65
- this.specs = [];
66
- this.renderSeq = 0;
67
- this.renderImageUrl = "";
68
- this.sourceSizeBySrc = /* @__PURE__ */ new Map();
69
- this.pendingSizeBySrc = /* @__PURE__ */ new Map();
70
- this.onCanvasResized = () => {
71
- this.updateBackground();
72
- };
73
- if (options) {
74
- Object.assign(this, options);
52
+
53
+ // src/coordinate.ts
54
+ var Coordinate = class {
55
+ /**
56
+ * Calculate layout to fit content within container while preserving aspect ratio.
57
+ */
58
+ static calculateLayout(container, content, padding = 0) {
59
+ const availableWidth = Math.max(0, container.width - padding * 2);
60
+ const availableHeight = Math.max(0, container.height - padding * 2);
61
+ if (content.width === 0 || content.height === 0) {
62
+ return { scale: 1, offsetX: 0, offsetY: 0, width: 0, height: 0 };
75
63
  }
64
+ const scaleX = availableWidth / content.width;
65
+ const scaleY = availableHeight / content.height;
66
+ const scale = Math.min(scaleX, scaleY);
67
+ const width = content.width * scale;
68
+ const height = content.height * scale;
69
+ const offsetX = (container.width - width) / 2;
70
+ const offsetY = (container.height - height) / 2;
71
+ return { scale, offsetX, offsetY, width, height };
76
72
  }
77
- activate(context) {
78
- var _a;
79
- this.canvasService = context.services.get("CanvasService");
80
- if (!this.canvasService) {
81
- console.warn("CanvasService not found for BackgroundTool");
82
- return;
83
- }
84
- (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
85
- this.renderProducerDisposable = this.canvasService.registerRenderProducer(
86
- this.id,
87
- () => ({
88
- layerSpecs: {
89
- [BACKGROUND_LAYER_ID]: this.specs
90
- }
91
- }),
92
- { priority: 0 }
93
- );
94
- const configService = context.services.get(
95
- "ConfigurationService"
96
- );
97
- if (configService) {
98
- this.color = configService.get("background.color", this.color);
99
- this.url = configService.get("background.url", this.url);
100
- configService.onAnyChange((e) => {
101
- if (e.key.startsWith("background.")) {
102
- const prop = e.key.split(".")[1];
103
- console.log(
104
- `[BackgroundTool] Config change detected: ${e.key} -> ${e.value}, prop: ${prop}`
105
- );
106
- if (prop && prop in this) {
107
- console.log(
108
- `[BackgroundTool] Updating option ${prop} to ${e.value}`
109
- );
110
- this[prop] = e.value;
111
- this.updateBackground();
112
- } else {
113
- console.warn(
114
- `[BackgroundTool] Property ${prop} not found in options`
115
- );
116
- }
117
- }
118
- });
119
- }
120
- context.eventBus.on("canvas:resized", this.onCanvasResized);
121
- this.updateBackground();
73
+ /**
74
+ * Convert an absolute value to a normalized value (0-1).
75
+ * @param value Absolute value (e.g., pixels)
76
+ * @param total Total dimension size (e.g., canvas width)
77
+ */
78
+ static toNormalized(value, total) {
79
+ return total === 0 ? 0 : value / total;
122
80
  }
123
- deactivate(context) {
124
- var _a;
125
- context.eventBus.off("canvas:resized", this.onCanvasResized);
126
- this.renderSeq += 1;
127
- this.specs = [];
128
- this.renderImageUrl = "";
129
- (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
130
- this.renderProducerDisposable = void 0;
131
- if (!this.canvasService) return;
132
- const layer = this.canvasService.getLayer(BACKGROUND_LAYER_ID);
133
- if (layer) {
134
- this.canvasService.canvas.remove(layer);
135
- }
136
- void this.canvasService.flushRenderFromProducers();
137
- this.canvasService.requestRenderAll();
138
- this.canvasService = void 0;
81
+ /**
82
+ * Convert a normalized value (0-1) to an absolute value.
83
+ * @param normalized Normalized value (0-1)
84
+ * @param total Total dimension size (e.g., canvas width)
85
+ */
86
+ static toAbsolute(normalized, total) {
87
+ return normalized * total;
139
88
  }
140
- contribute() {
89
+ /**
90
+ * Normalize a point's coordinates.
91
+ */
92
+ static normalizePoint(point, size) {
141
93
  return {
142
- [import_core.ContributionPointIds.CONFIGURATIONS]: [
143
- {
144
- id: "background.color",
145
- type: "color",
146
- label: "Background Color",
147
- default: ""
148
- },
149
- {
150
- id: "background.url",
151
- type: "string",
152
- label: "Image URL",
153
- default: ""
154
- }
155
- ],
156
- [import_core.ContributionPointIds.COMMANDS]: [
157
- {
158
- command: "reset",
159
- title: "Reset Background",
160
- handler: () => {
161
- this.updateBackground();
162
- return true;
163
- }
164
- },
165
- {
166
- command: "clear",
167
- title: "Clear Background",
168
- handler: () => {
169
- this.color = "transparent";
170
- this.url = "";
171
- this.updateBackground();
172
- return true;
173
- }
174
- },
175
- {
176
- command: "setBackgroundColor",
177
- title: "Set Background Color",
178
- handler: (color) => {
179
- if (this.color === color) return true;
180
- this.color = color;
181
- this.updateBackground();
182
- return true;
183
- }
184
- },
185
- {
186
- command: "setBackgroundImage",
187
- title: "Set Background Image",
188
- handler: (url) => {
189
- if (this.url === url) return true;
190
- this.url = url;
191
- this.updateBackground();
192
- return true;
193
- }
194
- }
195
- ]
94
+ x: this.toNormalized(point.x, size.width),
95
+ y: this.toNormalized(point.y, size.height)
196
96
  };
197
97
  }
198
- getViewportSize() {
199
- var _a, _b;
200
- const width = Number(((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 0);
201
- const height = Number(((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 0);
98
+ /**
99
+ * Denormalize a point's coordinates to absolute pixels.
100
+ */
101
+ static denormalizePoint(point, size) {
202
102
  return {
203
- width: width > 0 ? width : DEFAULT_WIDTH,
204
- height: height > 0 ? height : DEFAULT_HEIGHT
103
+ x: this.toAbsolute(point.x, size.width),
104
+ y: this.toAbsolute(point.y, size.height)
205
105
  };
206
106
  }
207
- buildBackgroundSpecs(color, imageUrl) {
208
- const { width, height } = this.getViewportSize();
209
- const specs = [
210
- {
211
- id: BACKGROUND_RECT_ID,
212
- type: "rect",
213
- space: "screen",
214
- data: {
215
- id: BACKGROUND_RECT_ID,
216
- layerId: BACKGROUND_LAYER_ID,
217
- type: "background-color"
218
- },
219
- props: {
220
- left: 0,
221
- top: 0,
222
- width,
223
- height,
224
- originX: "left",
225
- originY: "top",
226
- fill: color,
227
- selectable: false,
228
- evented: false,
229
- excludeFromExport: true
230
- }
231
- }
232
- ];
233
- if (!imageUrl) {
234
- return specs;
235
- }
236
- const sourceSize = this.sourceSizeBySrc.get(imageUrl);
237
- const sourceWidth = Math.max(1, Number((sourceSize == null ? void 0 : sourceSize.width) || width));
238
- const sourceHeight = Math.max(1, Number((sourceSize == null ? void 0 : sourceSize.height) || height));
239
- const coverScale = Math.max(width / sourceWidth, height / sourceHeight);
240
- specs.push({
241
- id: BACKGROUND_IMAGE_ID,
242
- type: "image",
243
- src: imageUrl,
244
- space: "screen",
245
- data: {
246
- id: BACKGROUND_IMAGE_ID,
247
- layerId: BACKGROUND_LAYER_ID,
248
- type: "background-image"
249
- },
250
- props: {
251
- left: 0,
252
- top: 0,
253
- originX: "left",
254
- originY: "top",
255
- scaleX: coverScale,
256
- scaleY: coverScale,
257
- selectable: false,
258
- evented: false,
259
- excludeFromExport: true
260
- }
261
- });
262
- return specs;
263
- }
264
- async ensureImageSize(src) {
265
- if (!src) return null;
266
- const cached = this.sourceSizeBySrc.get(src);
267
- if (cached) return cached;
268
- const pending = this.pendingSizeBySrc.get(src);
269
- if (pending) {
270
- return pending;
271
- }
272
- const task = this.loadImageSize(src);
273
- this.pendingSizeBySrc.set(src, task);
274
- try {
275
- return await task;
276
- } finally {
277
- if (this.pendingSizeBySrc.get(src) === task) {
278
- this.pendingSizeBySrc.delete(src);
279
- }
280
- }
281
- }
282
- async loadImageSize(src) {
283
- try {
284
- const image = await import_fabric.FabricImage.fromURL(src, {
285
- crossOrigin: "anonymous"
286
- });
287
- const width = Number((image == null ? void 0 : image.width) || 0);
288
- const height = Number((image == null ? void 0 : image.height) || 0);
289
- if (width > 0 && height > 0) {
290
- const size = { width, height };
291
- this.sourceSizeBySrc.set(src, size);
292
- return size;
293
- }
294
- } catch (error) {
295
- console.error("[BackgroundTool] Failed to load image", src, error);
107
+ static convertUnit(value, from, to) {
108
+ if (from === to) return value;
109
+ const toMM = {
110
+ px: 0.264583,
111
+ // 1px = 0.264583mm (96 DPI)
112
+ mm: 1,
113
+ cm: 10,
114
+ in: 25.4
115
+ };
116
+ const mmValue = value * (from === "px" ? toMM.px : toMM[from] || 1);
117
+ if (to === "px") {
118
+ return mmValue / toMM.px;
296
119
  }
297
- return null;
120
+ return mmValue / (toMM[to] || 1);
298
121
  }
299
- updateBackground() {
300
- void this.updateBackgroundAsync();
122
+ };
123
+
124
+ // src/units.ts
125
+ function parseLengthToMm(input, defaultUnit) {
126
+ var _a, _b;
127
+ if (typeof input === "number") {
128
+ if (!Number.isFinite(input)) return 0;
129
+ return Coordinate.convertUnit(input, defaultUnit, "mm");
301
130
  }
302
- async updateBackgroundAsync() {
303
- if (!this.canvasService) return;
304
- const seq = ++this.renderSeq;
305
- const color = this.color;
306
- const nextUrl = String(this.url || "").trim();
307
- if (!nextUrl) {
308
- this.renderImageUrl = "";
309
- } else if (nextUrl !== this.renderImageUrl) {
310
- const loaded = await this.ensureImageSize(nextUrl);
311
- if (seq !== this.renderSeq) return;
312
- if (loaded) {
313
- this.renderImageUrl = nextUrl;
314
- }
315
- }
316
- this.specs = this.buildBackgroundSpecs(color, this.renderImageUrl);
317
- await this.canvasService.flushRenderFromProducers();
318
- if (seq !== this.renderSeq) return;
319
- const layer = this.canvasService.getLayer(BACKGROUND_LAYER_ID);
320
- if (layer) {
321
- this.canvasService.canvas.sendObjectToBack(layer);
322
- }
323
- this.canvasService.requestRenderAll();
324
- }
325
- };
326
-
327
- // src/extensions/image.ts
328
- var import_core2 = require("@pooder/core");
329
- var import_fabric2 = require("fabric");
131
+ const raw = input.trim();
132
+ if (!raw) return 0;
133
+ const match = raw.match(/^([+-]?\d+(?:\.\d+)?)\s*(px|mm|cm|in)?$/i);
134
+ if (!match) return 0;
135
+ const value = Number(match[1]);
136
+ if (!Number.isFinite(value)) return 0;
137
+ const unit = (_b = (_a = match[2]) == null ? void 0 : _a.toLowerCase()) != null ? _b : defaultUnit;
138
+ return Coordinate.convertUnit(value, unit, "mm");
139
+ }
330
140
 
331
141
  // src/extensions/dielineShape.ts
332
142
  var BUILTIN_DIELINE_SHAPES = [
@@ -343,7 +153,7 @@ var DEFAULT_HEART_SHAPE_PARAMS = {
343
153
  tipSharpness: 0
344
154
  };
345
155
  var DEFAULT_DIELINE_SHAPE_STYLE = {
346
- fitMode: "contain",
156
+ fitMode: "stretch",
347
157
  ...DEFAULT_HEART_SHAPE_PARAMS
348
158
  };
349
159
  function isDielineShape(value) {
@@ -393,962 +203,1386 @@ function getHeartShapeParams(style) {
393
203
  };
394
204
  }
395
205
 
396
- // src/extensions/geometry.ts
397
- var import_paper = __toESM(require("paper"));
398
-
399
- // src/extensions/bridgeSelection.ts
400
- function pickExitIndex(hits) {
401
- for (let i = 0; i < hits.length; i++) {
402
- const h = hits[i];
403
- if (h.insideBelow && !h.insideAbove) return i;
404
- }
405
- return -1;
206
+ // src/extensions/sceneLayoutModel.ts
207
+ var DEFAULT_SIZE_STATE = {
208
+ unit: "mm",
209
+ actualWidthMm: 500,
210
+ actualHeightMm: 500,
211
+ constraintMode: "free",
212
+ aspectRatio: 1,
213
+ cutMode: "trim",
214
+ cutMarginMm: 0,
215
+ viewPadding: 140,
216
+ minMm: 10,
217
+ maxMm: 2e3,
218
+ stepMm: 0.1
219
+ };
220
+ function clamp(value, min, max) {
221
+ return Math.max(min, Math.min(max, value));
406
222
  }
407
- function scoreOutsideAbove(samples) {
408
- let score = 0;
409
- for (const s of samples) {
410
- if (s.outsideAbove) score++;
411
- }
412
- return score;
223
+ function roundToStep(value, step) {
224
+ if (!Number.isFinite(step) || step <= 0) return value;
225
+ return Math.round(value / step) * step;
413
226
  }
414
-
415
- // src/extensions/wrappedOffsets.ts
416
- function wrappedDistance(total, start, end) {
417
- if (!Number.isFinite(total) || total <= 0) return 0;
418
- if (!Number.isFinite(start) || !Number.isFinite(end)) return 0;
419
- const s = (start % total + total) % total;
420
- const e = (end % total + total) % total;
421
- return e >= s ? e - s : total - s + e;
227
+ function sanitizeMmValue(valueMm, limits) {
228
+ if (!Number.isFinite(valueMm)) return limits.minMm;
229
+ const rounded = roundToStep(valueMm, limits.stepMm);
230
+ return clamp(rounded, limits.minMm, limits.maxMm);
422
231
  }
423
- function sampleWrappedOffsets(total, start, end, count) {
424
- if (!Number.isFinite(total) || total <= 0) return [];
425
- if (!Number.isFinite(start) || !Number.isFinite(end)) return [];
426
- const n = Math.max(0, Math.floor(count));
427
- if (n <= 0) return [];
428
- const dist = wrappedDistance(total, start, end);
429
- if (n === 1) return [(start % total + total) % total];
430
- const step = dist / (n - 1);
431
- const offsets = [];
432
- for (let i = 0; i < n; i++) {
433
- const raw = start + step * i;
434
- const wrapped = (raw % total + total) % total;
435
- offsets.push(wrapped);
436
- }
437
- return offsets;
232
+ function normalizeUnit(value) {
233
+ if (value === "cm" || value === "in") return value;
234
+ return "mm";
438
235
  }
439
-
440
- // src/extensions/geometry.ts
441
- function resolveFeaturePosition(feature, geometry) {
442
- const { x, y, width, height } = geometry;
443
- const left = x - width / 2;
444
- const top = y - height / 2;
445
- return {
446
- x: left + feature.x * width,
447
- y: top + feature.y * height
448
- };
236
+ function normalizeConstraintMode(value) {
237
+ if (value === "lockAspect" || value === "equal") return value;
238
+ return "free";
449
239
  }
450
- function ensurePaper(width, height) {
451
- if (!import_paper.default.project) {
452
- import_paper.default.setup(new import_paper.default.Size(width, height));
453
- } else {
454
- import_paper.default.view.viewSize = new import_paper.default.Size(width, height);
455
- }
240
+ function normalizeCutMode(value) {
241
+ if (value === "outset" || value === "inset") return value;
242
+ return "trim";
456
243
  }
457
- var isBridgeDebugEnabled = () => Boolean(globalThis.__POODER_BRIDGE_DEBUG__);
458
- function normalizePathItem(shape) {
459
- let result = shape;
460
- if (typeof result.resolveCrossings === "function") result = result.resolveCrossings();
461
- if (typeof result.reduce === "function") result = result.reduce({});
462
- if (typeof result.reorient === "function") result = result.reorient(true, true);
463
- if (typeof result.reduce === "function") result = result.reduce({});
464
- return result;
244
+ function toMm(value, fromUnit) {
245
+ return Coordinate.convertUnit(value, fromUnit, "mm");
465
246
  }
466
- function getBridgeDelta(itemBounds, overlap) {
467
- return Math.max(overlap, Math.min(5, Math.max(1, itemBounds.height * 0.02)));
247
+ function fromMm(valueMm, toUnit) {
248
+ return Coordinate.convertUnit(valueMm, "mm", toUnit);
468
249
  }
469
- function getExitHit(args) {
470
- const { mainShape, x, bridgeBottom, toY, eps, delta, overlap, op } = args;
471
- const ray = new import_paper.default.Path.Line({
472
- from: [x, bridgeBottom],
473
- to: [x, toY],
474
- insert: false
475
- });
476
- const intersections = mainShape.getIntersections(ray) || [];
477
- ray.remove();
478
- const validHits = intersections.filter((i) => i.point.y < bridgeBottom - eps);
479
- if (validHits.length === 0) return null;
480
- validHits.sort((a, b) => b.point.y - a.point.y);
481
- const flags = validHits.map((h) => {
482
- const above = h.point.add(new import_paper.default.Point(0, -delta));
483
- const below = h.point.add(new import_paper.default.Point(0, delta));
484
- return {
485
- insideAbove: mainShape.contains(above),
486
- insideBelow: mainShape.contains(below)
487
- };
488
- });
489
- const idx = pickExitIndex(flags);
490
- if (idx < 0) return null;
491
- if (isBridgeDebugEnabled()) {
492
- console.debug("Geometry: Bridge ray", {
493
- x,
494
- validHits: validHits.length,
495
- idx,
496
- delta,
497
- overlap,
498
- op
499
- });
250
+ function resolvePaddingPx(raw, containerWidth, containerHeight) {
251
+ if (typeof raw === "number") return Math.max(0, raw);
252
+ if (typeof raw === "string") {
253
+ if (raw.endsWith("%")) {
254
+ const percent = parseFloat(raw) / 100;
255
+ if (!Number.isFinite(percent)) return 0;
256
+ return Math.max(0, Math.min(containerWidth, containerHeight) * percent);
257
+ }
258
+ const fixed = parseFloat(raw);
259
+ return Number.isFinite(fixed) ? Math.max(0, fixed) : 0;
500
260
  }
501
- const hit = validHits[idx];
502
- return { point: hit.point, location: hit };
261
+ return 0;
503
262
  }
504
- function selectOuterChain(args) {
505
- const { mainShape, pointsA, pointsB, delta, overlap, op } = args;
506
- const scoreA = scoreOutsideAbove(
507
- pointsA.map((p) => ({
508
- outsideAbove: !mainShape.contains(p.add(new import_paper.default.Point(0, -delta)))
509
- }))
263
+ function readSizeState(configService) {
264
+ const unit = normalizeUnit(
265
+ configService.get("size.unit", DEFAULT_SIZE_STATE.unit)
510
266
  );
511
- const scoreB = scoreOutsideAbove(
512
- pointsB.map((p) => ({
513
- outsideAbove: !mainShape.contains(p.add(new import_paper.default.Point(0, -delta)))
514
- }))
267
+ const minMm = Math.max(
268
+ 0.1,
269
+ Number(configService.get("size.minMm", DEFAULT_SIZE_STATE.minMm))
515
270
  );
516
- const ratioA = scoreA / pointsA.length;
517
- const ratioB = scoreB / pointsB.length;
518
- if (isBridgeDebugEnabled()) {
519
- console.debug("Geometry: Bridge chain", {
520
- scoreA,
521
- scoreB,
522
- lenA: pointsA.length,
523
- lenB: pointsB.length,
524
- ratioA,
525
- ratioB,
526
- delta,
527
- overlap,
528
- op
529
- });
530
- }
531
- const ratioEps = 1e-6;
532
- if (Math.abs(ratioA - ratioB) > ratioEps) {
533
- return ratioA > ratioB ? pointsA : pointsB;
534
- }
535
- if (scoreA !== scoreB) return scoreA > scoreB ? pointsA : pointsB;
536
- return pointsA.length <= pointsB.length ? pointsA : pointsB;
537
- }
538
- function fitPathItemToRect(item, rect, fitMode) {
539
- const { left, top, width, height } = rect;
540
- const bounds = item.bounds;
541
- if (width <= 0 || height <= 0 || !Number.isFinite(bounds.width) || !Number.isFinite(bounds.height) || bounds.width <= 0 || bounds.height <= 0) {
542
- item.position = new import_paper.default.Point(left + width / 2, top + height / 2);
543
- return item;
544
- }
545
- item.translate(new import_paper.default.Point(-bounds.left, -bounds.top));
546
- if (fitMode === "stretch") {
547
- item.scale(width / bounds.width, height / bounds.height, new import_paper.default.Point(0, 0));
548
- item.translate(new import_paper.default.Point(left, top));
549
- return item;
550
- }
551
- const uniformScale = Math.min(width / bounds.width, height / bounds.height);
552
- item.scale(uniformScale, uniformScale, new import_paper.default.Point(0, 0));
553
- const scaledWidth = bounds.width * uniformScale;
554
- const scaledHeight = bounds.height * uniformScale;
555
- item.translate(
556
- new import_paper.default.Point(
557
- left + (width - scaledWidth) / 2,
558
- top + (height - scaledHeight) / 2
559
- )
271
+ const maxMm = Math.max(
272
+ minMm,
273
+ Number(configService.get("size.maxMm", DEFAULT_SIZE_STATE.maxMm))
560
274
  );
561
- return item;
562
- }
563
- function createNormalizedHeartPath(params) {
564
- const { lobeSpread, notchDepth, tipSharpness } = params;
565
- const halfSpread = 0.22 + lobeSpread * 0.18;
566
- const notchY = 0.06 + notchDepth * 0.2;
567
- const shoulderY = 0.24 + notchDepth * 0.2;
568
- const topLift = 0.12 + (1 - notchDepth) * 0.06;
569
- const topY = notchY - topLift;
570
- const sideCtrlY = shoulderY - (0.18 - notchDepth * 0.08);
571
- const lowerCtrlY = 0.58 + (1 - tipSharpness) * 0.16;
572
- const tipCtrlX = 0.34 - tipSharpness * 0.2;
573
- const notchCtrlX = 0.06 + lobeSpread * 0.06;
574
- const lobeCtrlX = 0.1 + lobeSpread * 0.08;
575
- const notchCtrlY = notchY - topLift * 0.45;
576
- const xPeakL = 0.5 - halfSpread;
577
- const xPeakR = 0.5 + halfSpread;
578
- const heartPath = new import_paper.default.Path({ insert: false });
579
- heartPath.moveTo(new import_paper.default.Point(0.5, notchY));
580
- heartPath.cubicCurveTo(
581
- new import_paper.default.Point(0.5 - notchCtrlX, notchCtrlY),
582
- new import_paper.default.Point(xPeakL + lobeCtrlX, topY),
583
- new import_paper.default.Point(xPeakL, topY)
275
+ const stepMm = Math.max(
276
+ 1e-3,
277
+ Number(configService.get("size.stepMm", DEFAULT_SIZE_STATE.stepMm))
584
278
  );
585
- heartPath.cubicCurveTo(
586
- new import_paper.default.Point(xPeakL - lobeCtrlX, topY),
587
- new import_paper.default.Point(0, sideCtrlY),
588
- new import_paper.default.Point(0, shoulderY)
279
+ const actualWidthMm = sanitizeMmValue(
280
+ parseLengthToMm(
281
+ configService.get("size.actualWidthMm", DEFAULT_SIZE_STATE.actualWidthMm),
282
+ "mm"
283
+ ),
284
+ { minMm, maxMm, stepMm }
589
285
  );
590
- heartPath.cubicCurveTo(
591
- new import_paper.default.Point(0, lowerCtrlY),
592
- new import_paper.default.Point(tipCtrlX, 1),
593
- new import_paper.default.Point(0.5, 1)
286
+ const actualHeightMm = sanitizeMmValue(
287
+ parseLengthToMm(
288
+ configService.get(
289
+ "size.actualHeightMm",
290
+ DEFAULT_SIZE_STATE.actualHeightMm
291
+ ),
292
+ "mm"
293
+ ),
294
+ { minMm, maxMm, stepMm }
594
295
  );
595
- heartPath.cubicCurveTo(
596
- new import_paper.default.Point(1 - tipCtrlX, 1),
597
- new import_paper.default.Point(1, lowerCtrlY),
598
- new import_paper.default.Point(1, shoulderY)
296
+ const aspectRaw = Number(
297
+ configService.get("size.aspectRatio", DEFAULT_SIZE_STATE.aspectRatio)
599
298
  );
600
- heartPath.cubicCurveTo(
601
- new import_paper.default.Point(1, sideCtrlY),
602
- new import_paper.default.Point(xPeakR + lobeCtrlX, topY),
603
- new import_paper.default.Point(xPeakR, topY)
299
+ const aspectRatio = Number.isFinite(aspectRaw) && aspectRaw > 0 ? aspectRaw : actualWidthMm / Math.max(1e-3, actualHeightMm);
300
+ const cutMarginMm = Math.max(
301
+ 0,
302
+ parseLengthToMm(
303
+ configService.get("size.cutMarginMm", DEFAULT_SIZE_STATE.cutMarginMm),
304
+ "mm"
305
+ )
604
306
  );
605
- heartPath.cubicCurveTo(
606
- new import_paper.default.Point(xPeakR - lobeCtrlX, topY),
607
- new import_paper.default.Point(0.5 + notchCtrlX, notchCtrlY),
608
- new import_paper.default.Point(0.5, notchY)
307
+ const viewPadding = configService.get(
308
+ "size.viewPadding",
309
+ DEFAULT_SIZE_STATE.viewPadding
609
310
  );
610
- heartPath.closed = true;
611
- return heartPath;
612
- }
613
- function createHeartBaseShape(options) {
614
- const { x, y, width, height } = options;
615
- const w = Math.max(0, width);
616
- const h = Math.max(0, height);
617
- const left = x - w / 2;
618
- const top = y - h / 2;
619
- const fitMode = getShapeFitMode(options.shapeStyle);
620
- const heartParams = getHeartShapeParams(options.shapeStyle);
621
- const rawHeart = createNormalizedHeartPath(heartParams);
622
- return fitPathItemToRect(rawHeart, { left, top, width: w, height: h }, fitMode);
311
+ return {
312
+ unit,
313
+ actualWidthMm,
314
+ actualHeightMm,
315
+ constraintMode: normalizeConstraintMode(
316
+ configService.get(
317
+ "size.constraintMode",
318
+ DEFAULT_SIZE_STATE.constraintMode
319
+ )
320
+ ),
321
+ aspectRatio,
322
+ cutMode: normalizeCutMode(
323
+ configService.get("size.cutMode", DEFAULT_SIZE_STATE.cutMode)
324
+ ),
325
+ cutMarginMm,
326
+ viewPadding,
327
+ minMm,
328
+ maxMm,
329
+ stepMm
330
+ };
623
331
  }
624
- var BUILTIN_SHAPE_BUILDERS = {
625
- rect: (options) => {
626
- const { x, y, width, height, radius } = options;
627
- return new import_paper.default.Path.Rectangle({
628
- point: [x - width / 2, y - height / 2],
629
- size: [Math.max(0, width), Math.max(0, height)],
630
- radius: Math.max(0, radius)
631
- });
632
- },
633
- circle: (options) => {
634
- const { x, y, width, height } = options;
635
- const r = Math.min(width, height) / 2;
636
- return new import_paper.default.Path.Circle({
637
- center: new import_paper.default.Point(x, y),
638
- radius: Math.max(0, r)
639
- });
640
- },
641
- ellipse: (options) => {
642
- const { x, y, width, height } = options;
643
- return new import_paper.default.Path.Ellipse({
644
- center: new import_paper.default.Point(x, y),
645
- radius: [Math.max(0, width / 2), Math.max(0, height / 2)]
646
- });
647
- },
648
- heart: createHeartBaseShape
649
- };
650
- function createCustomBaseShape(options) {
651
- var _a;
652
- const {
653
- pathData,
654
- customSourceWidthPx,
655
- customSourceHeightPx,
656
- x,
657
- y,
332
+ function rectByCenter(centerX, centerY, width, height) {
333
+ return {
334
+ left: centerX - width / 2,
335
+ top: centerY - height / 2,
658
336
  width,
659
- height
660
- } = options;
661
- if (typeof pathData !== "string" || pathData.trim().length === 0) {
662
- return null;
663
- }
664
- const center = new import_paper.default.Point(x, y);
665
- const hasMultipleSubPaths = ((_a = (pathData.match(/[Mm]/g) || []).length) != null ? _a : 0) > 1;
666
- const path = hasMultipleSubPaths ? new import_paper.default.CompoundPath(pathData) : (() => {
667
- const single = new import_paper.default.Path();
668
- single.pathData = pathData;
669
- return single;
670
- })();
671
- const sourceWidth = Number(customSourceWidthPx != null ? customSourceWidthPx : 0);
672
- const sourceHeight = Number(customSourceHeightPx != null ? customSourceHeightPx : 0);
673
- if (Number.isFinite(sourceWidth) && Number.isFinite(sourceHeight) && sourceWidth > 0 && sourceHeight > 0 && width > 0 && height > 0) {
674
- const targetLeft = x - width / 2;
675
- const targetTop = y - height / 2;
676
- path.scale(width / sourceWidth, height / sourceHeight, new import_paper.default.Point(0, 0));
677
- path.translate(new import_paper.default.Point(targetLeft, targetTop));
678
- return path;
679
- }
680
- if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
681
- path.position = center;
682
- path.scale(width / path.bounds.width, height / path.bounds.height);
683
- return path;
684
- }
685
- path.position = center;
686
- return path;
687
- }
688
- function createBaseShape(options) {
689
- const { shape } = options;
690
- if (shape === "custom") {
691
- const customShape = createCustomBaseShape(options);
692
- if (customShape) return customShape;
693
- return BUILTIN_SHAPE_BUILDERS[DEFAULT_DIELINE_SHAPE](options);
694
- }
695
- return BUILTIN_SHAPE_BUILDERS[shape](options);
337
+ height,
338
+ centerX,
339
+ centerY
340
+ };
696
341
  }
697
- function resolveBridgeBasePath(shape, anchor) {
698
- if (shape instanceof import_paper.default.Path) {
699
- return shape;
342
+ function getCutSizeMm(size) {
343
+ if (size.cutMode === "trim") {
344
+ return { widthMm: size.actualWidthMm, heightMm: size.actualHeightMm };
700
345
  }
701
- if (shape instanceof import_paper.default.CompoundPath) {
702
- const children = (shape.children || []).filter(
703
- (child) => child instanceof import_paper.default.Path
704
- );
705
- if (!children.length) return null;
706
- let best = children[0];
707
- let bestDistance = Infinity;
708
- for (const child of children) {
709
- const location = child.getNearestLocation(anchor);
710
- const point = location == null ? void 0 : location.point;
711
- if (!point) continue;
712
- const distance = point.getDistance(anchor);
713
- if (distance < bestDistance) {
714
- bestDistance = distance;
715
- best = child;
716
- }
717
- }
718
- return best;
346
+ const delta = size.cutMarginMm * 2;
347
+ if (size.cutMode === "outset") {
348
+ return {
349
+ widthMm: size.actualWidthMm + delta,
350
+ heightMm: size.actualHeightMm + delta
351
+ };
719
352
  }
720
- return null;
353
+ return {
354
+ widthMm: Math.max(size.minMm, size.actualWidthMm - delta),
355
+ heightMm: Math.max(size.minMm, size.actualHeightMm - delta)
356
+ };
721
357
  }
722
- function createFeatureItem(feature, center) {
723
- let item;
724
- if (feature.shape === "rect") {
725
- const w = feature.width || 10;
726
- const h = feature.height || 10;
727
- const r = feature.radius || 0;
728
- item = new import_paper.default.Path.Rectangle({
729
- point: [center.x - w / 2, center.y - h / 2],
730
- size: [w, h],
731
- radius: r
732
- });
733
- } else {
734
- const r = feature.radius || 5;
735
- item = new import_paper.default.Path.Circle({
736
- center,
737
- radius: r
738
- });
358
+ function computeSceneLayout(canvasService, size) {
359
+ const canvasWidth = canvasService.canvas.width || 0;
360
+ const canvasHeight = canvasService.canvas.height || 0;
361
+ if (canvasWidth <= 0 || canvasHeight <= 0) return null;
362
+ const { widthMm: cutWidthMm, heightMm: cutHeightMm } = getCutSizeMm(size);
363
+ const viewWidthMm = Math.max(size.actualWidthMm, cutWidthMm);
364
+ const viewHeightMm = Math.max(size.actualHeightMm, cutHeightMm);
365
+ if (!Number.isFinite(viewWidthMm) || !Number.isFinite(viewHeightMm) || viewWidthMm <= 0 || viewHeightMm <= 0) {
366
+ return null;
739
367
  }
740
- if (feature.rotation) {
741
- item.rotate(feature.rotation, center);
368
+ const paddingPx = resolvePaddingPx(
369
+ size.viewPadding,
370
+ canvasWidth,
371
+ canvasHeight
372
+ );
373
+ canvasService.viewport.updateContainer(canvasWidth, canvasHeight);
374
+ canvasService.viewport.setPadding(paddingPx);
375
+ canvasService.viewport.updatePhysical(viewWidthMm, viewHeightMm);
376
+ const layout = canvasService.viewport.layout;
377
+ if (!Number.isFinite(layout.scale) || !Number.isFinite(layout.offsetX) || !Number.isFinite(layout.offsetY) || layout.scale <= 0) {
378
+ return null;
742
379
  }
743
- return item;
744
- }
745
- function getPerimeterShape(options) {
746
- let mainShape = createBaseShape(options);
747
- const { features } = options;
748
- if (features && features.length > 0) {
749
- const edgeFeatures = features.filter(
750
- (f) => !f.renderBehavior || f.renderBehavior === "edge"
751
- );
752
- const adds = [];
753
- const subtracts = [];
754
- edgeFeatures.forEach((f) => {
755
- const pos = resolveFeaturePosition(f, options);
756
- const center = new import_paper.default.Point(pos.x, pos.y);
757
- const item = createFeatureItem(f, center);
758
- if (f.bridge && f.bridge.type === "vertical") {
759
- const itemBounds = item.bounds;
760
- const mainBounds = mainShape.bounds;
761
- const bridgeTop = mainBounds.top;
762
- const bridgeBottom = itemBounds.top;
763
- if (bridgeBottom > bridgeTop) {
764
- const overlap = 2;
765
- const rayPadding = 10;
766
- const eps = 0.1;
767
- const delta = getBridgeDelta(itemBounds, overlap);
768
- const toY = bridgeTop - rayPadding;
769
- const inset = Math.min(1, Math.max(0, itemBounds.width * 0.01));
770
- const xLeft = itemBounds.left + inset;
771
- const xRight = itemBounds.right - inset;
772
- const bridgeBasePath = resolveBridgeBasePath(mainShape, center);
773
- const canBridge = !!bridgeBasePath && xRight - xLeft > eps;
774
- if (canBridge && bridgeBasePath) {
775
- const leftHit = getExitHit({
776
- mainShape: bridgeBasePath,
777
- x: xLeft,
778
- bridgeBottom,
779
- toY,
780
- eps,
781
- delta,
782
- overlap,
783
- op: f.operation
784
- });
785
- const rightHit = getExitHit({
786
- mainShape: bridgeBasePath,
787
- x: xRight,
788
- bridgeBottom,
789
- toY,
790
- eps,
791
- delta,
792
- overlap,
793
- op: f.operation
794
- });
795
- if (leftHit && rightHit) {
796
- const pathLength = bridgeBasePath.length;
797
- const leftOffset = leftHit.location.offset;
798
- const rightOffset = rightHit.location.offset;
799
- const distanceA = wrappedDistance(pathLength, leftOffset, rightOffset);
800
- const distanceB = wrappedDistance(pathLength, rightOffset, leftOffset);
801
- const countFor = (d) => Math.max(8, Math.min(80, Math.ceil(d / 6)));
802
- const offsetsA = sampleWrappedOffsets(
803
- pathLength,
804
- leftOffset,
805
- rightOffset,
806
- countFor(distanceA)
807
- );
808
- const offsetsB = sampleWrappedOffsets(
809
- pathLength,
810
- rightOffset,
811
- leftOffset,
812
- countFor(distanceB)
813
- );
814
- const pointsA = offsetsA.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
815
- const pointsB = offsetsB.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
816
- if (pointsA.length >= 2 && pointsB.length >= 2) {
817
- let topBase = selectOuterChain({
818
- mainShape: bridgeBasePath,
819
- pointsA,
820
- pointsB,
821
- delta,
822
- overlap,
823
- op: f.operation
824
- });
825
- const dist2 = (a, b) => {
826
- const dx = a.x - b.x;
827
- const dy = a.y - b.y;
828
- return dx * dx + dy * dy;
829
- };
830
- if (dist2(topBase[0], leftHit.point) > dist2(topBase[0], rightHit.point)) {
831
- topBase = topBase.slice().reverse();
832
- }
833
- topBase = topBase.slice();
834
- topBase[0] = leftHit.point;
835
- topBase[topBase.length - 1] = rightHit.point;
836
- const capShiftY = f.operation === "subtract" ? -Math.max(overlap * 2, delta) : overlap;
837
- const topPoints = topBase.map(
838
- (p) => p.add(new import_paper.default.Point(0, capShiftY))
839
- );
840
- const bridgeBottomY = bridgeBottom + overlap * 2;
841
- const bridgePoly = new import_paper.default.Path({ insert: false });
842
- for (const p of topPoints) bridgePoly.add(p);
843
- bridgePoly.add(new import_paper.default.Point(xRight, bridgeBottomY));
844
- bridgePoly.add(new import_paper.default.Point(xLeft, bridgeBottomY));
845
- bridgePoly.closed = true;
846
- const unitedItem = item.unite(bridgePoly);
847
- item.remove();
848
- bridgePoly.remove();
849
- if (f.operation === "add") {
850
- adds.push(unitedItem);
851
- } else {
852
- subtracts.push(unitedItem);
853
- }
854
- return;
855
- }
856
- }
857
- }
858
- if (f.operation === "add") {
859
- adds.push(item);
860
- } else {
861
- subtracts.push(item);
862
- }
863
- } else {
864
- if (f.operation === "add") {
865
- adds.push(item);
866
- } else {
867
- subtracts.push(item);
868
- }
869
- }
870
- } else {
871
- if (f.operation === "add") {
872
- adds.push(item);
873
- } else {
874
- subtracts.push(item);
875
- }
876
- }
877
- });
878
- if (adds.length > 0) {
879
- for (const item of adds) {
880
- try {
881
- const temp = mainShape.unite(item);
882
- mainShape.remove();
883
- item.remove();
884
- mainShape = normalizePathItem(temp);
885
- } catch (e) {
886
- console.error("Geometry: Failed to unite feature", e);
887
- item.remove();
888
- }
889
- }
890
- }
891
- if (subtracts.length > 0) {
892
- for (const item of subtracts) {
893
- try {
894
- const temp = mainShape.subtract(item);
895
- mainShape.remove();
896
- item.remove();
897
- mainShape = normalizePathItem(temp);
898
- } catch (e) {
899
- console.error("Geometry: Failed to subtract feature", e);
900
- item.remove();
901
- }
902
- }
903
- }
904
- }
905
- return mainShape;
380
+ const centerX = layout.offsetX + layout.width / 2;
381
+ const centerY = layout.offsetY + layout.height / 2;
382
+ const trimWidthPx = size.actualWidthMm * layout.scale;
383
+ const trimHeightPx = size.actualHeightMm * layout.scale;
384
+ const cutWidthPx = cutWidthMm * layout.scale;
385
+ const cutHeightPx = cutHeightMm * layout.scale;
386
+ const trimRect = rectByCenter(centerX, centerY, trimWidthPx, trimHeightPx);
387
+ const cutRect = rectByCenter(centerX, centerY, cutWidthPx, cutHeightPx);
388
+ const bleedRect = rectByCenter(
389
+ centerX,
390
+ centerY,
391
+ Math.max(trimWidthPx, cutWidthPx),
392
+ Math.max(trimHeightPx, cutHeightPx)
393
+ );
394
+ return {
395
+ scale: layout.scale,
396
+ canvasWidth,
397
+ canvasHeight,
398
+ trimRect,
399
+ cutRect,
400
+ bleedRect,
401
+ trimWidthMm: size.actualWidthMm,
402
+ trimHeightMm: size.actualHeightMm,
403
+ cutWidthMm,
404
+ cutHeightMm,
405
+ cutMode: size.cutMode,
406
+ cutMarginMm: size.cutMarginMm
407
+ };
906
408
  }
907
- function applySurfaceFeatures(shape, features, options) {
908
- const surfaceFeatures = features.filter(
909
- (f) => f.renderBehavior === "surface"
409
+ function buildSceneGeometry(configService, layout) {
410
+ const radiusMm = parseLengthToMm(
411
+ configService.get("dieline.radius", 0),
412
+ "mm"
910
413
  );
911
- if (surfaceFeatures.length === 0) return shape;
912
- let result = shape;
913
- for (const f of surfaceFeatures) {
914
- const pos = resolveFeaturePosition(f, options);
915
- const center = new import_paper.default.Point(pos.x, pos.y);
916
- const item = createFeatureItem(f, center);
917
- try {
918
- if (f.operation === "add") {
919
- const temp = result.unite(item);
920
- result.remove();
921
- item.remove();
922
- result = normalizePathItem(temp);
923
- } else {
924
- const temp = result.subtract(item);
925
- result.remove();
926
- item.remove();
927
- result = normalizePathItem(temp);
928
- }
929
- } catch (e) {
930
- console.error("Geometry: Failed to apply surface feature", e);
931
- item.remove();
414
+ const offset = (layout.cutRect.width - layout.trimRect.width) / 2;
415
+ const sourceWidth = Number(configService.get("dieline.customSourceWidthPx", 0));
416
+ const sourceHeight = Number(
417
+ configService.get("dieline.customSourceHeightPx", 0)
418
+ );
419
+ const shapeStyle = normalizeShapeStyle(
420
+ configService.get("dieline.shapeStyle", DEFAULT_DIELINE_SHAPE_STYLE)
421
+ );
422
+ return {
423
+ shape: normalizeDielineShape(
424
+ configService.get("dieline.shape", DEFAULT_DIELINE_SHAPE)
425
+ ),
426
+ shapeStyle,
427
+ unit: "px",
428
+ x: layout.trimRect.centerX,
429
+ y: layout.trimRect.centerY,
430
+ width: layout.trimRect.width,
431
+ height: layout.trimRect.height,
432
+ radius: radiusMm * layout.scale,
433
+ offset,
434
+ scale: layout.scale,
435
+ pathData: configService.get("dieline.pathData"),
436
+ customSourceWidthPx: Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : void 0,
437
+ customSourceHeightPx: Number.isFinite(sourceHeight) && sourceHeight > 0 ? sourceHeight : void 0
438
+ };
439
+ }
440
+
441
+ // src/extensions/background.ts
442
+ var BACKGROUND_LAYER_ID = "background";
443
+ var BACKGROUND_CONFIG_KEY = "background.config";
444
+ var DEFAULT_WIDTH = 800;
445
+ var DEFAULT_HEIGHT = 600;
446
+ var DEFAULT_BACKGROUND_CONFIG = {
447
+ version: 1,
448
+ layers: [
449
+ {
450
+ id: "base-color",
451
+ kind: "color",
452
+ anchor: "viewport",
453
+ fit: "cover",
454
+ opacity: 1,
455
+ order: 0,
456
+ enabled: true,
457
+ exportable: false,
458
+ color: "#fff"
932
459
  }
460
+ ]
461
+ };
462
+ function clampOpacity(value, fallback) {
463
+ const numeric = Number(value);
464
+ if (!Number.isFinite(numeric)) {
465
+ return Math.max(0, Math.min(1, fallback));
933
466
  }
934
- return result;
467
+ return Math.max(0, Math.min(1, numeric));
935
468
  }
936
- function generateDielinePath(options) {
937
- const paperWidth = options.canvasWidth || options.width * 2 || 2e3;
938
- const paperHeight = options.canvasHeight || options.height * 2 || 2e3;
939
- ensurePaper(paperWidth, paperHeight);
940
- import_paper.default.project.activeLayer.removeChildren();
941
- const perimeter = getPerimeterShape(options);
942
- const finalShape = applySurfaceFeatures(perimeter, options.features, options);
943
- const pathData = finalShape.pathData;
944
- finalShape.remove();
945
- return pathData;
469
+ function normalizeLayerKind(value, fallback) {
470
+ if (value === "color" || value === "image") {
471
+ return value;
472
+ }
473
+ return fallback;
946
474
  }
947
- function generateMaskPath(options) {
948
- ensurePaper(options.canvasWidth, options.canvasHeight);
949
- import_paper.default.project.activeLayer.removeChildren();
950
- const { canvasWidth, canvasHeight } = options;
951
- const maskRect = new import_paper.default.Path.Rectangle({
952
- point: [0, 0],
953
- size: [canvasWidth, canvasHeight]
954
- });
955
- const perimeter = getPerimeterShape(options);
956
- const mainShape = applySurfaceFeatures(perimeter, options.features, options);
957
- const finalMask = maskRect.subtract(mainShape);
958
- maskRect.remove();
959
- mainShape.remove();
960
- const pathData = finalMask.pathData;
961
- finalMask.remove();
962
- return pathData;
475
+ function normalizeFitMode2(value, fallback) {
476
+ if (value === "contain" || value === "cover" || value === "stretch") {
477
+ return value;
478
+ }
479
+ return fallback;
963
480
  }
964
- function generateBleedZonePath(originalOptions, offsetOptions, offset) {
965
- const paperWidth = originalOptions.canvasWidth || originalOptions.width * 2 || 2e3;
966
- const paperHeight = originalOptions.canvasHeight || originalOptions.height * 2 || 2e3;
967
- ensurePaper(paperWidth, paperHeight);
968
- import_paper.default.project.activeLayer.removeChildren();
969
- const pOriginal = getPerimeterShape(originalOptions);
970
- const shapeOriginal = applySurfaceFeatures(
971
- pOriginal,
972
- originalOptions.features,
973
- originalOptions
974
- );
975
- const pOffset = getPerimeterShape(offsetOptions);
976
- const shapeOffset = applySurfaceFeatures(
977
- pOffset,
978
- offsetOptions.features,
979
- offsetOptions
980
- );
981
- let bleedZone;
982
- if (offset > 0) {
983
- bleedZone = shapeOffset.subtract(shapeOriginal);
984
- } else {
985
- bleedZone = shapeOriginal.subtract(shapeOffset);
986
- }
987
- const pathData = bleedZone.pathData;
988
- shapeOriginal.remove();
989
- shapeOffset.remove();
990
- bleedZone.remove();
991
- return pathData;
481
+ function normalizeAnchor(value, fallback) {
482
+ if (typeof value !== "string") return fallback;
483
+ const trimmed = value.trim();
484
+ return trimmed || fallback;
992
485
  }
993
- function getLowestPointOnDieline(options) {
994
- ensurePaper(options.width * 2, options.height * 2);
995
- import_paper.default.project.activeLayer.removeChildren();
996
- const shape = createBaseShape(options);
997
- const bounds = shape.bounds;
998
- const result = {
999
- x: bounds.center.x,
1000
- y: bounds.bottom
486
+ function normalizeOrder(value, fallback) {
487
+ const numeric = Number(value);
488
+ if (!Number.isFinite(numeric)) return fallback;
489
+ return numeric;
490
+ }
491
+ function normalizeLayer(raw, index, fallback) {
492
+ const fallbackLayer = fallback || {
493
+ id: `layer-${index + 1}`,
494
+ kind: "image",
495
+ anchor: "viewport",
496
+ fit: "contain",
497
+ opacity: 1,
498
+ order: index,
499
+ enabled: true,
500
+ exportable: false,
501
+ src: ""
502
+ };
503
+ if (!raw || typeof raw !== "object") {
504
+ return { ...fallbackLayer };
505
+ }
506
+ const input = raw;
507
+ const kind = normalizeLayerKind(input.kind, fallbackLayer.kind);
508
+ return {
509
+ id: typeof input.id === "string" && input.id.trim().length > 0 ? input.id.trim() : fallbackLayer.id,
510
+ kind,
511
+ anchor: normalizeAnchor(input.anchor, fallbackLayer.anchor),
512
+ fit: normalizeFitMode2(input.fit, fallbackLayer.fit),
513
+ opacity: clampOpacity(input.opacity, fallbackLayer.opacity),
514
+ order: normalizeOrder(input.order, fallbackLayer.order),
515
+ enabled: typeof input.enabled === "boolean" ? input.enabled : fallbackLayer.enabled,
516
+ exportable: typeof input.exportable === "boolean" ? input.exportable : fallbackLayer.exportable,
517
+ color: kind === "color" ? typeof input.color === "string" ? input.color : typeof fallbackLayer.color === "string" ? fallbackLayer.color : "#ffffff" : void 0,
518
+ src: kind === "image" ? typeof input.src === "string" ? input.src.trim() : typeof fallbackLayer.src === "string" ? fallbackLayer.src : "" : void 0
1001
519
  };
1002
- shape.remove();
1003
- return result;
1004
520
  }
1005
- function getNearestPointOnDieline(point, options) {
1006
- ensurePaper(options.width * 2, options.height * 2);
1007
- import_paper.default.project.activeLayer.removeChildren();
1008
- const shape = createBaseShape(options);
1009
- const p = new import_paper.default.Point(point.x, point.y);
1010
- const location = shape.getNearestLocation(p);
1011
- const result = {
1012
- x: location.point.x,
1013
- y: location.point.y,
1014
- normal: location.normal ? { x: location.normal.x, y: location.normal.y } : void 0
521
+ function normalizeConfig(raw) {
522
+ if (!raw || typeof raw !== "object") {
523
+ return cloneConfig(DEFAULT_BACKGROUND_CONFIG);
524
+ }
525
+ const input = raw;
526
+ const version = Number.isFinite(Number(input.version)) ? Number(input.version) : DEFAULT_BACKGROUND_CONFIG.version;
527
+ const baseLayers = Array.isArray(input.layers) ? input.layers.map((layer, index) => normalizeLayer(layer, index)) : cloneConfig(DEFAULT_BACKGROUND_CONFIG).layers;
528
+ const uniqueLayers = [];
529
+ const seen = /* @__PURE__ */ new Set();
530
+ baseLayers.forEach((layer, index) => {
531
+ let nextId = layer.id || `layer-${index + 1}`;
532
+ let serial = 1;
533
+ while (seen.has(nextId)) {
534
+ serial += 1;
535
+ nextId = `${layer.id || `layer-${index + 1}`}-${serial}`;
536
+ }
537
+ seen.add(nextId);
538
+ uniqueLayers.push({ ...layer, id: nextId });
539
+ });
540
+ return {
541
+ version,
542
+ layers: uniqueLayers
1015
543
  };
1016
- shape.remove();
1017
- return result;
1018
544
  }
1019
- function getPathBounds(pathData) {
1020
- const path = new import_paper.default.Path();
1021
- path.pathData = pathData;
1022
- const bounds = path.bounds;
1023
- path.remove();
545
+ function cloneConfig(config) {
1024
546
  return {
1025
- x: bounds.x,
1026
- y: bounds.y,
1027
- width: bounds.width,
1028
- height: bounds.height
547
+ version: config.version,
548
+ layers: (config.layers || []).map((layer) => ({ ...layer }))
1029
549
  };
1030
550
  }
1031
-
1032
- // src/coordinate.ts
1033
- var Coordinate = class {
1034
- /**
1035
- * Calculate layout to fit content within container while preserving aspect ratio.
1036
- */
1037
- static calculateLayout(container, content, padding = 0) {
1038
- const availableWidth = Math.max(0, container.width - padding * 2);
1039
- const availableHeight = Math.max(0, container.height - padding * 2);
1040
- if (content.width === 0 || content.height === 0) {
1041
- return { scale: 1, offsetX: 0, offsetY: 0, width: 0, height: 0 };
551
+ function mergeConfig(base, patch) {
552
+ const merged = {
553
+ version: patch.version === void 0 ? base.version : Number.isFinite(Number(patch.version)) ? Number(patch.version) : base.version,
554
+ layers: Array.isArray(patch.layers) ? patch.layers.map((layer, index) => normalizeLayer(layer, index)) : base.layers.map((layer) => ({ ...layer }))
555
+ };
556
+ return normalizeConfig(merged);
557
+ }
558
+ function configSignature(config) {
559
+ return JSON.stringify(config);
560
+ }
561
+ var BackgroundTool = class {
562
+ constructor(options) {
563
+ this.id = "pooder.kit.background";
564
+ this.metadata = {
565
+ name: "BackgroundTool"
566
+ };
567
+ this.config = cloneConfig(DEFAULT_BACKGROUND_CONFIG);
568
+ this.specs = [];
569
+ this.renderSeq = 0;
570
+ this.latestSceneLayout = null;
571
+ this.sourceSizeBySrc = /* @__PURE__ */ new Map();
572
+ this.pendingSizeBySrc = /* @__PURE__ */ new Map();
573
+ this.onCanvasResized = () => {
574
+ this.latestSceneLayout = null;
575
+ this.updateBackground();
576
+ };
577
+ this.onSceneLayoutChanged = (layout) => {
578
+ this.latestSceneLayout = layout;
579
+ this.updateBackground();
580
+ };
581
+ if (options && typeof options === "object") {
582
+ this.config = mergeConfig(this.config, options);
1042
583
  }
1043
- const scaleX = availableWidth / content.width;
1044
- const scaleY = availableHeight / content.height;
1045
- const scale = Math.min(scaleX, scaleY);
1046
- const width = content.width * scale;
1047
- const height = content.height * scale;
1048
- const offsetX = (container.width - width) / 2;
1049
- const offsetY = (container.height - height) / 2;
1050
- return { scale, offsetX, offsetY, width, height };
1051
584
  }
1052
- /**
1053
- * Convert an absolute value to a normalized value (0-1).
1054
- * @param value Absolute value (e.g., pixels)
1055
- * @param total Total dimension size (e.g., canvas width)
1056
- */
1057
- static toNormalized(value, total) {
1058
- return total === 0 ? 0 : value / total;
585
+ activate(context) {
586
+ var _a, _b;
587
+ this.canvasService = context.services.get("CanvasService");
588
+ if (!this.canvasService) {
589
+ console.warn("CanvasService not found for BackgroundTool");
590
+ return;
591
+ }
592
+ this.configService = context.services.get(
593
+ "ConfigurationService"
594
+ );
595
+ if (this.configService) {
596
+ this.config = normalizeConfig(
597
+ this.configService.get(
598
+ BACKGROUND_CONFIG_KEY,
599
+ DEFAULT_BACKGROUND_CONFIG
600
+ )
601
+ );
602
+ (_a = this.configChangeDisposable) == null ? void 0 : _a.dispose();
603
+ this.configChangeDisposable = this.configService.onAnyChange(
604
+ (e) => {
605
+ if (e.key === BACKGROUND_CONFIG_KEY) {
606
+ this.config = normalizeConfig(e.value);
607
+ this.updateBackground();
608
+ return;
609
+ }
610
+ if (e.key.startsWith("size.")) {
611
+ this.latestSceneLayout = null;
612
+ this.updateBackground();
613
+ }
614
+ }
615
+ );
616
+ }
617
+ (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
618
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
619
+ this.id,
620
+ () => ({
621
+ passes: [
622
+ {
623
+ id: BACKGROUND_LAYER_ID,
624
+ stack: 0,
625
+ order: 0,
626
+ objects: this.specs
627
+ }
628
+ ]
629
+ }),
630
+ { priority: 0 }
631
+ );
632
+ context.eventBus.on("canvas:resized", this.onCanvasResized);
633
+ context.eventBus.on("scene:layout:change", this.onSceneLayoutChanged);
634
+ this.updateBackground();
1059
635
  }
1060
- /**
1061
- * Convert a normalized value (0-1) to an absolute value.
1062
- * @param normalized Normalized value (0-1)
1063
- * @param total Total dimension size (e.g., canvas width)
1064
- */
1065
- static toAbsolute(normalized, total) {
1066
- return normalized * total;
636
+ deactivate(context) {
637
+ var _a, _b;
638
+ context.eventBus.off("canvas:resized", this.onCanvasResized);
639
+ context.eventBus.off("scene:layout:change", this.onSceneLayoutChanged);
640
+ this.renderSeq += 1;
641
+ this.specs = [];
642
+ this.latestSceneLayout = null;
643
+ (_a = this.configChangeDisposable) == null ? void 0 : _a.dispose();
644
+ this.configChangeDisposable = void 0;
645
+ (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
646
+ this.renderProducerDisposable = void 0;
647
+ if (!this.canvasService) return;
648
+ void this.canvasService.flushRenderFromProducers();
649
+ this.canvasService.requestRenderAll();
650
+ this.canvasService = void 0;
651
+ this.configService = void 0;
1067
652
  }
1068
- /**
1069
- * Normalize a point's coordinates.
1070
- */
1071
- static normalizePoint(point, size) {
653
+ contribute() {
1072
654
  return {
1073
- x: this.toNormalized(point.x, size.width),
1074
- y: this.toNormalized(point.y, size.height)
655
+ [import_core.ContributionPointIds.CONFIGURATIONS]: [
656
+ {
657
+ id: BACKGROUND_CONFIG_KEY,
658
+ type: "json",
659
+ label: "Background Config",
660
+ default: cloneConfig(DEFAULT_BACKGROUND_CONFIG)
661
+ }
662
+ ],
663
+ [import_core.ContributionPointIds.COMMANDS]: [
664
+ {
665
+ command: "background.getConfig",
666
+ title: "Get Background Config",
667
+ handler: () => cloneConfig(this.config)
668
+ },
669
+ {
670
+ command: "background.resetConfig",
671
+ title: "Reset Background Config",
672
+ handler: () => {
673
+ this.commitConfig(cloneConfig(DEFAULT_BACKGROUND_CONFIG));
674
+ return true;
675
+ }
676
+ },
677
+ {
678
+ command: "background.replaceConfig",
679
+ title: "Replace Background Config",
680
+ handler: (config) => {
681
+ this.commitConfig(normalizeConfig(config));
682
+ return true;
683
+ }
684
+ },
685
+ {
686
+ command: "background.patchConfig",
687
+ title: "Patch Background Config",
688
+ handler: (patch) => {
689
+ this.commitConfig(mergeConfig(this.config, patch || {}));
690
+ return true;
691
+ }
692
+ },
693
+ {
694
+ command: "background.upsertLayer",
695
+ title: "Upsert Background Layer",
696
+ handler: (layer) => {
697
+ const normalized = normalizeLayer(layer, 0);
698
+ const existingIndex = this.config.layers.findIndex(
699
+ (item) => item.id === normalized.id
700
+ );
701
+ const nextLayers = [...this.config.layers];
702
+ if (existingIndex >= 0) {
703
+ nextLayers[existingIndex] = normalizeLayer(
704
+ { ...nextLayers[existingIndex], ...layer },
705
+ existingIndex,
706
+ nextLayers[existingIndex]
707
+ );
708
+ } else {
709
+ nextLayers.push(
710
+ normalizeLayer(
711
+ {
712
+ ...normalized,
713
+ order: Number.isFinite(Number(layer.order)) ? Number(layer.order) : nextLayers.length
714
+ },
715
+ nextLayers.length
716
+ )
717
+ );
718
+ }
719
+ this.commitConfig(
720
+ normalizeConfig({
721
+ ...this.config,
722
+ layers: nextLayers
723
+ })
724
+ );
725
+ return true;
726
+ }
727
+ },
728
+ {
729
+ command: "background.removeLayer",
730
+ title: "Remove Background Layer",
731
+ handler: (id) => {
732
+ const nextLayers = this.config.layers.filter(
733
+ (layer) => layer.id !== id
734
+ );
735
+ this.commitConfig(
736
+ normalizeConfig({
737
+ ...this.config,
738
+ layers: nextLayers
739
+ })
740
+ );
741
+ return true;
742
+ }
743
+ }
744
+ ]
1075
745
  };
1076
746
  }
1077
- /**
1078
- * Denormalize a point's coordinates to absolute pixels.
1079
- */
1080
- static denormalizePoint(point, size) {
747
+ commitConfig(next) {
748
+ const normalized = normalizeConfig(next);
749
+ if (configSignature(normalized) === configSignature(this.config)) {
750
+ return;
751
+ }
752
+ if (this.configService) {
753
+ this.configService.update(BACKGROUND_CONFIG_KEY, cloneConfig(normalized));
754
+ return;
755
+ }
756
+ this.config = normalized;
757
+ this.updateBackground();
758
+ }
759
+ getViewportRect() {
760
+ var _a, _b;
761
+ const width = Number(((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 0);
762
+ const height = Number(((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 0);
1081
763
  return {
1082
- x: this.toAbsolute(point.x, size.width),
1083
- y: this.toAbsolute(point.y, size.height)
764
+ left: 0,
765
+ top: 0,
766
+ width: width > 0 ? width : DEFAULT_WIDTH,
767
+ height: height > 0 ? height : DEFAULT_HEIGHT
1084
768
  };
1085
769
  }
1086
- static convertUnit(value, from, to) {
1087
- if (from === to) return value;
1088
- const toMM = {
1089
- px: 0.264583,
1090
- // 1px = 0.264583mm (96 DPI)
1091
- mm: 1,
1092
- cm: 10,
1093
- in: 25.4
770
+ resolveSceneLayout() {
771
+ if (this.latestSceneLayout) return this.latestSceneLayout;
772
+ if (!this.canvasService || !this.configService) return null;
773
+ const layout = computeSceneLayout(
774
+ this.canvasService,
775
+ readSizeState(this.configService)
776
+ );
777
+ this.latestSceneLayout = layout;
778
+ return layout;
779
+ }
780
+ resolveFocusRect() {
781
+ const layout = this.resolveSceneLayout();
782
+ if (!layout) return null;
783
+ return {
784
+ left: layout.trimRect.left,
785
+ top: layout.trimRect.top,
786
+ width: layout.trimRect.width,
787
+ height: layout.trimRect.height
1094
788
  };
1095
- const mmValue = value * (from === "px" ? toMM.px : toMM[from] || 1);
1096
- if (to === "px") {
1097
- return mmValue / toMM.px;
1098
- }
1099
- return mmValue / (toMM[to] || 1);
1100
789
  }
1101
- };
1102
-
1103
- // src/units.ts
1104
- function parseLengthToMm(input, defaultUnit) {
1105
- var _a, _b;
1106
- if (typeof input === "number") {
1107
- if (!Number.isFinite(input)) return 0;
1108
- return Coordinate.convertUnit(input, defaultUnit, "mm");
790
+ resolveAnchorRect(anchor) {
791
+ if (anchor === "focus") {
792
+ return this.resolveFocusRect() || this.getViewportRect();
793
+ }
794
+ if (anchor !== "viewport") {
795
+ return this.getViewportRect();
796
+ }
797
+ return this.getViewportRect();
798
+ }
799
+ resolveImagePlacement(target, sourceSize, fit) {
800
+ const targetWidth = Math.max(1, Number(target.width || 0));
801
+ const targetHeight = Math.max(1, Number(target.height || 0));
802
+ const sourceWidth = Math.max(1, Number(sourceSize.width || 0));
803
+ const sourceHeight = Math.max(1, Number(sourceSize.height || 0));
804
+ if (fit === "stretch") {
805
+ return {
806
+ left: target.left,
807
+ top: target.top,
808
+ scaleX: targetWidth / sourceWidth,
809
+ scaleY: targetHeight / sourceHeight
810
+ };
811
+ }
812
+ const scale = fit === "contain" ? Math.min(targetWidth / sourceWidth, targetHeight / sourceHeight) : Math.max(targetWidth / sourceWidth, targetHeight / sourceHeight);
813
+ const renderWidth = sourceWidth * scale;
814
+ const renderHeight = sourceHeight * scale;
815
+ return {
816
+ left: target.left + (targetWidth - renderWidth) / 2,
817
+ top: target.top + (targetHeight - renderHeight) / 2,
818
+ scaleX: scale,
819
+ scaleY: scale
820
+ };
821
+ }
822
+ buildColorLayerSpec(layer) {
823
+ const rect = this.resolveAnchorRect(layer.anchor);
824
+ return {
825
+ id: `background.layer.${layer.id}.color`,
826
+ type: "rect",
827
+ space: "screen",
828
+ data: {
829
+ id: `background.layer.${layer.id}.color`,
830
+ layerId: BACKGROUND_LAYER_ID,
831
+ type: "background-layer",
832
+ layerRef: layer.id,
833
+ layerKind: layer.kind
834
+ },
835
+ props: {
836
+ left: rect.left,
837
+ top: rect.top,
838
+ width: rect.width,
839
+ height: rect.height,
840
+ originX: "left",
841
+ originY: "top",
842
+ fill: layer.color || "transparent",
843
+ opacity: layer.opacity,
844
+ selectable: false,
845
+ evented: false,
846
+ excludeFromExport: !layer.exportable
847
+ }
848
+ };
849
+ }
850
+ buildImageLayerSpec(layer) {
851
+ const src = String(layer.src || "").trim();
852
+ if (!src) return [];
853
+ const sourceSize = this.sourceSizeBySrc.get(src);
854
+ if (!sourceSize) return [];
855
+ const rect = this.resolveAnchorRect(layer.anchor);
856
+ const placement = this.resolveImagePlacement(rect, sourceSize, layer.fit);
857
+ return [
858
+ {
859
+ id: `background.layer.${layer.id}.image`,
860
+ type: "image",
861
+ src,
862
+ space: "screen",
863
+ data: {
864
+ id: `background.layer.${layer.id}.image`,
865
+ layerId: BACKGROUND_LAYER_ID,
866
+ type: "background-layer",
867
+ layerRef: layer.id,
868
+ layerKind: layer.kind
869
+ },
870
+ props: {
871
+ left: placement.left,
872
+ top: placement.top,
873
+ originX: "left",
874
+ originY: "top",
875
+ scaleX: placement.scaleX,
876
+ scaleY: placement.scaleY,
877
+ opacity: layer.opacity,
878
+ selectable: false,
879
+ evented: false,
880
+ excludeFromExport: !layer.exportable
881
+ }
882
+ }
883
+ ];
884
+ }
885
+ buildBackgroundSpecs(config) {
886
+ const activeLayers = (config.layers || []).filter((layer) => layer.enabled).map((layer, index) => ({ layer, index })).sort((a, b) => {
887
+ if (a.layer.order !== b.layer.order) {
888
+ return a.layer.order - b.layer.order;
889
+ }
890
+ return a.index - b.index;
891
+ });
892
+ const specs = [];
893
+ activeLayers.forEach(({ layer }) => {
894
+ if (layer.kind === "color") {
895
+ specs.push(this.buildColorLayerSpec(layer));
896
+ return;
897
+ }
898
+ specs.push(...this.buildImageLayerSpec(layer));
899
+ });
900
+ return specs;
901
+ }
902
+ collectActiveImageUrls(config) {
903
+ const urls = /* @__PURE__ */ new Set();
904
+ (config.layers || []).forEach((layer) => {
905
+ if (!layer.enabled || layer.kind !== "image") return;
906
+ const src = String(layer.src || "").trim();
907
+ if (!src) return;
908
+ urls.add(src);
909
+ });
910
+ return Array.from(urls);
911
+ }
912
+ async ensureImageSize(src) {
913
+ if (!src) return null;
914
+ const cached = this.sourceSizeBySrc.get(src);
915
+ if (cached) return cached;
916
+ const pending = this.pendingSizeBySrc.get(src);
917
+ if (pending) {
918
+ return pending;
919
+ }
920
+ const task = this.loadImageSize(src);
921
+ this.pendingSizeBySrc.set(src, task);
922
+ try {
923
+ return await task;
924
+ } finally {
925
+ if (this.pendingSizeBySrc.get(src) === task) {
926
+ this.pendingSizeBySrc.delete(src);
927
+ }
928
+ }
929
+ }
930
+ async loadImageSize(src) {
931
+ try {
932
+ const image = await import_fabric.FabricImage.fromURL(src, {
933
+ crossOrigin: "anonymous"
934
+ });
935
+ const width = Number((image == null ? void 0 : image.width) || 0);
936
+ const height = Number((image == null ? void 0 : image.height) || 0);
937
+ if (width > 0 && height > 0) {
938
+ const size = { width, height };
939
+ this.sourceSizeBySrc.set(src, size);
940
+ return size;
941
+ }
942
+ } catch (error) {
943
+ console.error("[BackgroundTool] Failed to load image", src, error);
944
+ }
945
+ return null;
946
+ }
947
+ updateBackground() {
948
+ void this.updateBackgroundAsync();
949
+ }
950
+ async updateBackgroundAsync() {
951
+ if (!this.canvasService) return;
952
+ const seq = ++this.renderSeq;
953
+ const currentConfig = cloneConfig(this.config);
954
+ const activeUrls = this.collectActiveImageUrls(currentConfig);
955
+ if (activeUrls.length > 0) {
956
+ await Promise.all(activeUrls.map((url) => this.ensureImageSize(url)));
957
+ if (seq !== this.renderSeq) return;
958
+ }
959
+ this.specs = this.buildBackgroundSpecs(currentConfig);
960
+ await this.canvasService.flushRenderFromProducers();
961
+ if (seq !== this.renderSeq) return;
962
+ this.canvasService.requestRenderAll();
1109
963
  }
1110
- const raw = input.trim();
1111
- if (!raw) return 0;
1112
- const match = raw.match(/^([+-]?\d+(?:\.\d+)?)\s*(px|mm|cm|in)?$/i);
1113
- if (!match) return 0;
1114
- const value = Number(match[1]);
1115
- if (!Number.isFinite(value)) return 0;
1116
- const unit = (_b = (_a = match[2]) == null ? void 0 : _a.toLowerCase()) != null ? _b : defaultUnit;
1117
- return Coordinate.convertUnit(value, unit, "mm");
1118
- }
1119
-
1120
- // src/extensions/sceneLayoutModel.ts
1121
- var DEFAULT_SIZE_STATE = {
1122
- unit: "mm",
1123
- actualWidthMm: 500,
1124
- actualHeightMm: 500,
1125
- constraintMode: "free",
1126
- aspectRatio: 1,
1127
- cutMode: "trim",
1128
- cutMarginMm: 0,
1129
- viewPadding: 140,
1130
- minMm: 10,
1131
- maxMm: 2e3,
1132
- stepMm: 0.1
1133
964
  };
1134
- function clamp(value, min, max) {
1135
- return Math.max(min, Math.min(max, value));
965
+
966
+ // src/extensions/image.ts
967
+ var import_core2 = require("@pooder/core");
968
+ var import_fabric2 = require("fabric");
969
+
970
+ // src/extensions/geometry.ts
971
+ var import_paper = __toESM(require("paper"));
972
+
973
+ // src/extensions/bridgeSelection.ts
974
+ function pickExitIndex(hits) {
975
+ for (let i = 0; i < hits.length; i++) {
976
+ const h = hits[i];
977
+ if (h.insideBelow && !h.insideAbove) return i;
978
+ }
979
+ return -1;
1136
980
  }
1137
- function roundToStep(value, step) {
1138
- if (!Number.isFinite(step) || step <= 0) return value;
1139
- return Math.round(value / step) * step;
981
+ function scoreOutsideAbove(samples) {
982
+ let score = 0;
983
+ for (const s of samples) {
984
+ if (s.outsideAbove) score++;
985
+ }
986
+ return score;
1140
987
  }
1141
- function sanitizeMmValue(valueMm, limits) {
1142
- if (!Number.isFinite(valueMm)) return limits.minMm;
1143
- const rounded = roundToStep(valueMm, limits.stepMm);
1144
- return clamp(rounded, limits.minMm, limits.maxMm);
988
+
989
+ // src/extensions/wrappedOffsets.ts
990
+ function wrappedDistance(total, start, end) {
991
+ if (!Number.isFinite(total) || total <= 0) return 0;
992
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return 0;
993
+ const s = (start % total + total) % total;
994
+ const e = (end % total + total) % total;
995
+ return e >= s ? e - s : total - s + e;
1145
996
  }
1146
- function normalizeUnit(value) {
1147
- if (value === "cm" || value === "in") return value;
1148
- return "mm";
997
+ function sampleWrappedOffsets(total, start, end, count) {
998
+ if (!Number.isFinite(total) || total <= 0) return [];
999
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return [];
1000
+ const n = Math.max(0, Math.floor(count));
1001
+ if (n <= 0) return [];
1002
+ const dist = wrappedDistance(total, start, end);
1003
+ if (n === 1) return [(start % total + total) % total];
1004
+ const step = dist / (n - 1);
1005
+ const offsets = [];
1006
+ for (let i = 0; i < n; i++) {
1007
+ const raw = start + step * i;
1008
+ const wrapped = (raw % total + total) % total;
1009
+ offsets.push(wrapped);
1010
+ }
1011
+ return offsets;
1149
1012
  }
1150
- function normalizeConstraintMode(value) {
1151
- if (value === "lockAspect" || value === "equal") return value;
1152
- return "free";
1013
+
1014
+ // src/extensions/geometry.ts
1015
+ function resolveFeaturePosition(feature, geometry) {
1016
+ const { x, y, width, height } = geometry;
1017
+ const left = x - width / 2;
1018
+ const top = y - height / 2;
1019
+ return {
1020
+ x: left + feature.x * width,
1021
+ y: top + feature.y * height
1022
+ };
1153
1023
  }
1154
- function normalizeCutMode(value) {
1155
- if (value === "outset" || value === "inset") return value;
1156
- return "trim";
1024
+ function ensurePaper(width, height) {
1025
+ if (!import_paper.default.project) {
1026
+ import_paper.default.setup(new import_paper.default.Size(width, height));
1027
+ } else {
1028
+ import_paper.default.view.viewSize = new import_paper.default.Size(width, height);
1029
+ }
1157
1030
  }
1158
- function toMm(value, fromUnit) {
1159
- return Coordinate.convertUnit(value, fromUnit, "mm");
1031
+ var isBridgeDebugEnabled = () => Boolean(globalThis.__POODER_BRIDGE_DEBUG__);
1032
+ function normalizePathItem(shape) {
1033
+ let result = shape;
1034
+ if (typeof result.resolveCrossings === "function") result = result.resolveCrossings();
1035
+ if (typeof result.reduce === "function") result = result.reduce({});
1036
+ if (typeof result.reorient === "function") result = result.reorient(true, true);
1037
+ if (typeof result.reduce === "function") result = result.reduce({});
1038
+ return result;
1160
1039
  }
1161
- function fromMm(valueMm, toUnit) {
1162
- return Coordinate.convertUnit(valueMm, "mm", toUnit);
1040
+ function getBridgeDelta(itemBounds, overlap) {
1041
+ return Math.max(overlap, Math.min(5, Math.max(1, itemBounds.height * 0.02)));
1163
1042
  }
1164
- function resolvePaddingPx(raw, containerWidth, containerHeight) {
1165
- if (typeof raw === "number") return Math.max(0, raw);
1166
- if (typeof raw === "string") {
1167
- if (raw.endsWith("%")) {
1168
- const percent = parseFloat(raw) / 100;
1169
- if (!Number.isFinite(percent)) return 0;
1170
- return Math.max(0, Math.min(containerWidth, containerHeight) * percent);
1043
+ function getExitHit(args) {
1044
+ const { mainShape, x, bridgeBottom, toY, eps, delta, overlap, op } = args;
1045
+ const ray = new import_paper.default.Path.Line({
1046
+ from: [x, bridgeBottom],
1047
+ to: [x, toY],
1048
+ insert: false
1049
+ });
1050
+ const intersections = mainShape.getIntersections(ray) || [];
1051
+ ray.remove();
1052
+ const validHits = intersections.filter((i) => i.point.y < bridgeBottom - eps);
1053
+ if (validHits.length === 0) return null;
1054
+ validHits.sort((a, b) => b.point.y - a.point.y);
1055
+ const flags = validHits.map((h) => {
1056
+ const above = h.point.add(new import_paper.default.Point(0, -delta));
1057
+ const below = h.point.add(new import_paper.default.Point(0, delta));
1058
+ return {
1059
+ insideAbove: mainShape.contains(above),
1060
+ insideBelow: mainShape.contains(below)
1061
+ };
1062
+ });
1063
+ const idx = pickExitIndex(flags);
1064
+ if (idx < 0) return null;
1065
+ if (isBridgeDebugEnabled()) {
1066
+ console.debug("Geometry: Bridge ray", {
1067
+ x,
1068
+ validHits: validHits.length,
1069
+ idx,
1070
+ delta,
1071
+ overlap,
1072
+ op
1073
+ });
1074
+ }
1075
+ const hit = validHits[idx];
1076
+ return { point: hit.point, location: hit };
1077
+ }
1078
+ function selectOuterChain(args) {
1079
+ const { mainShape, pointsA, pointsB, delta, overlap, op } = args;
1080
+ const scoreA = scoreOutsideAbove(
1081
+ pointsA.map((p) => ({
1082
+ outsideAbove: !mainShape.contains(p.add(new import_paper.default.Point(0, -delta)))
1083
+ }))
1084
+ );
1085
+ const scoreB = scoreOutsideAbove(
1086
+ pointsB.map((p) => ({
1087
+ outsideAbove: !mainShape.contains(p.add(new import_paper.default.Point(0, -delta)))
1088
+ }))
1089
+ );
1090
+ const ratioA = scoreA / pointsA.length;
1091
+ const ratioB = scoreB / pointsB.length;
1092
+ if (isBridgeDebugEnabled()) {
1093
+ console.debug("Geometry: Bridge chain", {
1094
+ scoreA,
1095
+ scoreB,
1096
+ lenA: pointsA.length,
1097
+ lenB: pointsB.length,
1098
+ ratioA,
1099
+ ratioB,
1100
+ delta,
1101
+ overlap,
1102
+ op
1103
+ });
1104
+ }
1105
+ const ratioEps = 1e-6;
1106
+ if (Math.abs(ratioA - ratioB) > ratioEps) {
1107
+ return ratioA > ratioB ? pointsA : pointsB;
1108
+ }
1109
+ if (scoreA !== scoreB) return scoreA > scoreB ? pointsA : pointsB;
1110
+ return pointsA.length <= pointsB.length ? pointsA : pointsB;
1111
+ }
1112
+ function fitPathItemToRect(item, rect, fitMode) {
1113
+ const { left, top, width, height } = rect;
1114
+ const bounds = item.bounds;
1115
+ if (width <= 0 || height <= 0 || !Number.isFinite(bounds.width) || !Number.isFinite(bounds.height) || bounds.width <= 0 || bounds.height <= 0) {
1116
+ item.position = new import_paper.default.Point(left + width / 2, top + height / 2);
1117
+ return item;
1118
+ }
1119
+ item.translate(new import_paper.default.Point(-bounds.left, -bounds.top));
1120
+ if (fitMode === "stretch") {
1121
+ item.scale(width / bounds.width, height / bounds.height, new import_paper.default.Point(0, 0));
1122
+ item.translate(new import_paper.default.Point(left, top));
1123
+ return item;
1124
+ }
1125
+ const uniformScale = Math.min(width / bounds.width, height / bounds.height);
1126
+ item.scale(uniformScale, uniformScale, new import_paper.default.Point(0, 0));
1127
+ const scaledWidth = bounds.width * uniformScale;
1128
+ const scaledHeight = bounds.height * uniformScale;
1129
+ item.translate(
1130
+ new import_paper.default.Point(
1131
+ left + (width - scaledWidth) / 2,
1132
+ top + (height - scaledHeight) / 2
1133
+ )
1134
+ );
1135
+ return item;
1136
+ }
1137
+ function createNormalizedHeartPath(params) {
1138
+ const { lobeSpread, notchDepth, tipSharpness } = params;
1139
+ const halfSpread = 0.22 + lobeSpread * 0.18;
1140
+ const notchY = 0.06 + notchDepth * 0.2;
1141
+ const shoulderY = 0.24 + notchDepth * 0.2;
1142
+ const topLift = 0.12 + (1 - notchDepth) * 0.06;
1143
+ const topY = notchY - topLift;
1144
+ const sideCtrlY = shoulderY - (0.18 - notchDepth * 0.08);
1145
+ const lowerCtrlY = 0.58 + (1 - tipSharpness) * 0.16;
1146
+ const tipCtrlX = 0.34 - tipSharpness * 0.2;
1147
+ const notchCtrlX = 0.06 + lobeSpread * 0.06;
1148
+ const lobeCtrlX = 0.1 + lobeSpread * 0.08;
1149
+ const notchCtrlY = notchY - topLift * 0.45;
1150
+ const xPeakL = 0.5 - halfSpread;
1151
+ const xPeakR = 0.5 + halfSpread;
1152
+ const heartPath = new import_paper.default.Path({ insert: false });
1153
+ heartPath.moveTo(new import_paper.default.Point(0.5, notchY));
1154
+ heartPath.cubicCurveTo(
1155
+ new import_paper.default.Point(0.5 - notchCtrlX, notchCtrlY),
1156
+ new import_paper.default.Point(xPeakL + lobeCtrlX, topY),
1157
+ new import_paper.default.Point(xPeakL, topY)
1158
+ );
1159
+ heartPath.cubicCurveTo(
1160
+ new import_paper.default.Point(xPeakL - lobeCtrlX, topY),
1161
+ new import_paper.default.Point(0, sideCtrlY),
1162
+ new import_paper.default.Point(0, shoulderY)
1163
+ );
1164
+ heartPath.cubicCurveTo(
1165
+ new import_paper.default.Point(0, lowerCtrlY),
1166
+ new import_paper.default.Point(tipCtrlX, 1),
1167
+ new import_paper.default.Point(0.5, 1)
1168
+ );
1169
+ heartPath.cubicCurveTo(
1170
+ new import_paper.default.Point(1 - tipCtrlX, 1),
1171
+ new import_paper.default.Point(1, lowerCtrlY),
1172
+ new import_paper.default.Point(1, shoulderY)
1173
+ );
1174
+ heartPath.cubicCurveTo(
1175
+ new import_paper.default.Point(1, sideCtrlY),
1176
+ new import_paper.default.Point(xPeakR + lobeCtrlX, topY),
1177
+ new import_paper.default.Point(xPeakR, topY)
1178
+ );
1179
+ heartPath.cubicCurveTo(
1180
+ new import_paper.default.Point(xPeakR - lobeCtrlX, topY),
1181
+ new import_paper.default.Point(0.5 + notchCtrlX, notchCtrlY),
1182
+ new import_paper.default.Point(0.5, notchY)
1183
+ );
1184
+ heartPath.closed = true;
1185
+ return heartPath;
1186
+ }
1187
+ function createHeartBaseShape(options) {
1188
+ const { x, y, width, height } = options;
1189
+ const w = Math.max(0, width);
1190
+ const h = Math.max(0, height);
1191
+ const left = x - w / 2;
1192
+ const top = y - h / 2;
1193
+ const fitMode = getShapeFitMode(options.shapeStyle);
1194
+ const heartParams = getHeartShapeParams(options.shapeStyle);
1195
+ const rawHeart = createNormalizedHeartPath(heartParams);
1196
+ return fitPathItemToRect(rawHeart, { left, top, width: w, height: h }, fitMode);
1197
+ }
1198
+ var BUILTIN_SHAPE_BUILDERS = {
1199
+ rect: (options) => {
1200
+ const { x, y, width, height, radius } = options;
1201
+ return new import_paper.default.Path.Rectangle({
1202
+ point: [x - width / 2, y - height / 2],
1203
+ size: [Math.max(0, width), Math.max(0, height)],
1204
+ radius: Math.max(0, radius)
1205
+ });
1206
+ },
1207
+ circle: (options) => {
1208
+ const { x, y, width, height } = options;
1209
+ const r = Math.min(width, height) / 2;
1210
+ return new import_paper.default.Path.Circle({
1211
+ center: new import_paper.default.Point(x, y),
1212
+ radius: Math.max(0, r)
1213
+ });
1214
+ },
1215
+ ellipse: (options) => {
1216
+ const { x, y, width, height } = options;
1217
+ return new import_paper.default.Path.Ellipse({
1218
+ center: new import_paper.default.Point(x, y),
1219
+ radius: [Math.max(0, width / 2), Math.max(0, height / 2)]
1220
+ });
1221
+ },
1222
+ heart: createHeartBaseShape
1223
+ };
1224
+ function createCustomBaseShape(options) {
1225
+ var _a;
1226
+ const {
1227
+ pathData,
1228
+ customSourceWidthPx,
1229
+ customSourceHeightPx,
1230
+ x,
1231
+ y,
1232
+ width,
1233
+ height
1234
+ } = options;
1235
+ if (typeof pathData !== "string" || pathData.trim().length === 0) {
1236
+ return null;
1237
+ }
1238
+ const center = new import_paper.default.Point(x, y);
1239
+ const hasMultipleSubPaths = ((_a = (pathData.match(/[Mm]/g) || []).length) != null ? _a : 0) > 1;
1240
+ const path = hasMultipleSubPaths ? new import_paper.default.CompoundPath(pathData) : (() => {
1241
+ const single = new import_paper.default.Path();
1242
+ single.pathData = pathData;
1243
+ return single;
1244
+ })();
1245
+ const sourceWidth = Number(customSourceWidthPx != null ? customSourceWidthPx : 0);
1246
+ const sourceHeight = Number(customSourceHeightPx != null ? customSourceHeightPx : 0);
1247
+ if (Number.isFinite(sourceWidth) && Number.isFinite(sourceHeight) && sourceWidth > 0 && sourceHeight > 0 && width > 0 && height > 0) {
1248
+ const targetLeft = x - width / 2;
1249
+ const targetTop = y - height / 2;
1250
+ path.scale(width / sourceWidth, height / sourceHeight, new import_paper.default.Point(0, 0));
1251
+ path.translate(new import_paper.default.Point(targetLeft, targetTop));
1252
+ return path;
1253
+ }
1254
+ if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
1255
+ path.position = center;
1256
+ path.scale(width / path.bounds.width, height / path.bounds.height);
1257
+ return path;
1258
+ }
1259
+ path.position = center;
1260
+ return path;
1261
+ }
1262
+ function createBaseShape(options) {
1263
+ const { shape } = options;
1264
+ if (shape === "custom") {
1265
+ const customShape = createCustomBaseShape(options);
1266
+ if (customShape) return customShape;
1267
+ return BUILTIN_SHAPE_BUILDERS[DEFAULT_DIELINE_SHAPE](options);
1268
+ }
1269
+ return BUILTIN_SHAPE_BUILDERS[shape](options);
1270
+ }
1271
+ function resolveBridgeBasePath(shape, anchor) {
1272
+ if (shape instanceof import_paper.default.Path) {
1273
+ return shape;
1274
+ }
1275
+ if (shape instanceof import_paper.default.CompoundPath) {
1276
+ const children = (shape.children || []).filter(
1277
+ (child) => child instanceof import_paper.default.Path
1278
+ );
1279
+ if (!children.length) return null;
1280
+ let best = children[0];
1281
+ let bestDistance = Infinity;
1282
+ for (const child of children) {
1283
+ const location = child.getNearestLocation(anchor);
1284
+ const point = location == null ? void 0 : location.point;
1285
+ if (!point) continue;
1286
+ const distance = point.getDistance(anchor);
1287
+ if (distance < bestDistance) {
1288
+ bestDistance = distance;
1289
+ best = child;
1290
+ }
1291
+ }
1292
+ return best;
1293
+ }
1294
+ return null;
1295
+ }
1296
+ function createFeatureItem(feature, center) {
1297
+ let item;
1298
+ if (feature.shape === "rect") {
1299
+ const w = feature.width || 10;
1300
+ const h = feature.height || 10;
1301
+ const r = feature.radius || 0;
1302
+ item = new import_paper.default.Path.Rectangle({
1303
+ point: [center.x - w / 2, center.y - h / 2],
1304
+ size: [w, h],
1305
+ radius: r
1306
+ });
1307
+ } else {
1308
+ const r = feature.radius || 5;
1309
+ item = new import_paper.default.Path.Circle({
1310
+ center,
1311
+ radius: r
1312
+ });
1313
+ }
1314
+ if (feature.rotation) {
1315
+ item.rotate(feature.rotation, center);
1316
+ }
1317
+ return item;
1318
+ }
1319
+ function getPerimeterShape(options) {
1320
+ let mainShape = createBaseShape(options);
1321
+ const { features } = options;
1322
+ if (features && features.length > 0) {
1323
+ const edgeFeatures = features.filter(
1324
+ (f) => !f.renderBehavior || f.renderBehavior === "edge"
1325
+ );
1326
+ const adds = [];
1327
+ const subtracts = [];
1328
+ edgeFeatures.forEach((f) => {
1329
+ const pos = resolveFeaturePosition(f, options);
1330
+ const center = new import_paper.default.Point(pos.x, pos.y);
1331
+ const item = createFeatureItem(f, center);
1332
+ if (f.bridge && f.bridge.type === "vertical") {
1333
+ const itemBounds = item.bounds;
1334
+ const mainBounds = mainShape.bounds;
1335
+ const bridgeTop = mainBounds.top;
1336
+ const bridgeBottom = itemBounds.top;
1337
+ if (bridgeBottom > bridgeTop) {
1338
+ const overlap = 2;
1339
+ const rayPadding = 10;
1340
+ const eps = 0.1;
1341
+ const delta = getBridgeDelta(itemBounds, overlap);
1342
+ const toY = bridgeTop - rayPadding;
1343
+ const inset = Math.min(1, Math.max(0, itemBounds.width * 0.01));
1344
+ const xLeft = itemBounds.left + inset;
1345
+ const xRight = itemBounds.right - inset;
1346
+ const bridgeBasePath = resolveBridgeBasePath(mainShape, center);
1347
+ const canBridge = !!bridgeBasePath && xRight - xLeft > eps;
1348
+ if (canBridge && bridgeBasePath) {
1349
+ const leftHit = getExitHit({
1350
+ mainShape: bridgeBasePath,
1351
+ x: xLeft,
1352
+ bridgeBottom,
1353
+ toY,
1354
+ eps,
1355
+ delta,
1356
+ overlap,
1357
+ op: f.operation
1358
+ });
1359
+ const rightHit = getExitHit({
1360
+ mainShape: bridgeBasePath,
1361
+ x: xRight,
1362
+ bridgeBottom,
1363
+ toY,
1364
+ eps,
1365
+ delta,
1366
+ overlap,
1367
+ op: f.operation
1368
+ });
1369
+ if (leftHit && rightHit) {
1370
+ const pathLength = bridgeBasePath.length;
1371
+ const leftOffset = leftHit.location.offset;
1372
+ const rightOffset = rightHit.location.offset;
1373
+ const distanceA = wrappedDistance(pathLength, leftOffset, rightOffset);
1374
+ const distanceB = wrappedDistance(pathLength, rightOffset, leftOffset);
1375
+ const countFor = (d) => Math.max(8, Math.min(80, Math.ceil(d / 6)));
1376
+ const offsetsA = sampleWrappedOffsets(
1377
+ pathLength,
1378
+ leftOffset,
1379
+ rightOffset,
1380
+ countFor(distanceA)
1381
+ );
1382
+ const offsetsB = sampleWrappedOffsets(
1383
+ pathLength,
1384
+ rightOffset,
1385
+ leftOffset,
1386
+ countFor(distanceB)
1387
+ );
1388
+ const pointsA = offsetsA.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
1389
+ const pointsB = offsetsB.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
1390
+ if (pointsA.length >= 2 && pointsB.length >= 2) {
1391
+ let topBase = selectOuterChain({
1392
+ mainShape: bridgeBasePath,
1393
+ pointsA,
1394
+ pointsB,
1395
+ delta,
1396
+ overlap,
1397
+ op: f.operation
1398
+ });
1399
+ const dist2 = (a, b) => {
1400
+ const dx = a.x - b.x;
1401
+ const dy = a.y - b.y;
1402
+ return dx * dx + dy * dy;
1403
+ };
1404
+ if (dist2(topBase[0], leftHit.point) > dist2(topBase[0], rightHit.point)) {
1405
+ topBase = topBase.slice().reverse();
1406
+ }
1407
+ topBase = topBase.slice();
1408
+ topBase[0] = leftHit.point;
1409
+ topBase[topBase.length - 1] = rightHit.point;
1410
+ const capShiftY = f.operation === "subtract" ? -Math.max(overlap * 2, delta) : overlap;
1411
+ const topPoints = topBase.map(
1412
+ (p) => p.add(new import_paper.default.Point(0, capShiftY))
1413
+ );
1414
+ const bridgeBottomY = bridgeBottom + overlap * 2;
1415
+ const bridgePoly = new import_paper.default.Path({ insert: false });
1416
+ for (const p of topPoints) bridgePoly.add(p);
1417
+ bridgePoly.add(new import_paper.default.Point(xRight, bridgeBottomY));
1418
+ bridgePoly.add(new import_paper.default.Point(xLeft, bridgeBottomY));
1419
+ bridgePoly.closed = true;
1420
+ const unitedItem = item.unite(bridgePoly);
1421
+ item.remove();
1422
+ bridgePoly.remove();
1423
+ if (f.operation === "add") {
1424
+ adds.push(unitedItem);
1425
+ } else {
1426
+ subtracts.push(unitedItem);
1427
+ }
1428
+ return;
1429
+ }
1430
+ }
1431
+ }
1432
+ if (f.operation === "add") {
1433
+ adds.push(item);
1434
+ } else {
1435
+ subtracts.push(item);
1436
+ }
1437
+ } else {
1438
+ if (f.operation === "add") {
1439
+ adds.push(item);
1440
+ } else {
1441
+ subtracts.push(item);
1442
+ }
1443
+ }
1444
+ } else {
1445
+ if (f.operation === "add") {
1446
+ adds.push(item);
1447
+ } else {
1448
+ subtracts.push(item);
1449
+ }
1450
+ }
1451
+ });
1452
+ if (adds.length > 0) {
1453
+ for (const item of adds) {
1454
+ try {
1455
+ const temp = mainShape.unite(item);
1456
+ mainShape.remove();
1457
+ item.remove();
1458
+ mainShape = normalizePathItem(temp);
1459
+ } catch (e) {
1460
+ console.error("Geometry: Failed to unite feature", e);
1461
+ item.remove();
1462
+ }
1463
+ }
1464
+ }
1465
+ if (subtracts.length > 0) {
1466
+ for (const item of subtracts) {
1467
+ try {
1468
+ const temp = mainShape.subtract(item);
1469
+ mainShape.remove();
1470
+ item.remove();
1471
+ mainShape = normalizePathItem(temp);
1472
+ } catch (e) {
1473
+ console.error("Geometry: Failed to subtract feature", e);
1474
+ item.remove();
1475
+ }
1476
+ }
1171
1477
  }
1172
- const fixed = parseFloat(raw);
1173
- return Number.isFinite(fixed) ? Math.max(0, fixed) : 0;
1174
1478
  }
1175
- return 0;
1479
+ return mainShape;
1176
1480
  }
1177
- function readSizeState(configService) {
1178
- const unit = normalizeUnit(
1179
- configService.get("size.unit", DEFAULT_SIZE_STATE.unit)
1180
- );
1181
- const minMm = Math.max(
1182
- 0.1,
1183
- Number(configService.get("size.minMm", DEFAULT_SIZE_STATE.minMm))
1184
- );
1185
- const maxMm = Math.max(
1186
- minMm,
1187
- Number(configService.get("size.maxMm", DEFAULT_SIZE_STATE.maxMm))
1188
- );
1189
- const stepMm = Math.max(
1190
- 1e-3,
1191
- Number(configService.get("size.stepMm", DEFAULT_SIZE_STATE.stepMm))
1192
- );
1193
- const actualWidthMm = sanitizeMmValue(
1194
- parseLengthToMm(
1195
- configService.get("size.actualWidthMm", DEFAULT_SIZE_STATE.actualWidthMm),
1196
- "mm"
1197
- ),
1198
- { minMm, maxMm, stepMm }
1199
- );
1200
- const actualHeightMm = sanitizeMmValue(
1201
- parseLengthToMm(
1202
- configService.get(
1203
- "size.actualHeightMm",
1204
- DEFAULT_SIZE_STATE.actualHeightMm
1205
- ),
1206
- "mm"
1207
- ),
1208
- { minMm, maxMm, stepMm }
1209
- );
1210
- const aspectRaw = Number(
1211
- configService.get("size.aspectRatio", DEFAULT_SIZE_STATE.aspectRatio)
1212
- );
1213
- const aspectRatio = Number.isFinite(aspectRaw) && aspectRaw > 0 ? aspectRaw : actualWidthMm / Math.max(1e-3, actualHeightMm);
1214
- const cutMarginMm = Math.max(
1215
- 0,
1216
- parseLengthToMm(
1217
- configService.get("size.cutMarginMm", DEFAULT_SIZE_STATE.cutMarginMm),
1218
- "mm"
1219
- )
1220
- );
1221
- const viewPadding = configService.get(
1222
- "size.viewPadding",
1223
- DEFAULT_SIZE_STATE.viewPadding
1481
+ function applySurfaceFeatures(shape, features, options) {
1482
+ const surfaceFeatures = features.filter(
1483
+ (f) => f.renderBehavior === "surface"
1224
1484
  );
1225
- return {
1226
- unit,
1227
- actualWidthMm,
1228
- actualHeightMm,
1229
- constraintMode: normalizeConstraintMode(
1230
- configService.get(
1231
- "size.constraintMode",
1232
- DEFAULT_SIZE_STATE.constraintMode
1233
- )
1234
- ),
1235
- aspectRatio,
1236
- cutMode: normalizeCutMode(
1237
- configService.get("size.cutMode", DEFAULT_SIZE_STATE.cutMode)
1238
- ),
1239
- cutMarginMm,
1240
- viewPadding,
1241
- minMm,
1242
- maxMm,
1243
- stepMm
1244
- };
1485
+ if (surfaceFeatures.length === 0) return shape;
1486
+ let result = shape;
1487
+ for (const f of surfaceFeatures) {
1488
+ const pos = resolveFeaturePosition(f, options);
1489
+ const center = new import_paper.default.Point(pos.x, pos.y);
1490
+ const item = createFeatureItem(f, center);
1491
+ try {
1492
+ if (f.operation === "add") {
1493
+ const temp = result.unite(item);
1494
+ result.remove();
1495
+ item.remove();
1496
+ result = normalizePathItem(temp);
1497
+ } else {
1498
+ const temp = result.subtract(item);
1499
+ result.remove();
1500
+ item.remove();
1501
+ result = normalizePathItem(temp);
1502
+ }
1503
+ } catch (e) {
1504
+ console.error("Geometry: Failed to apply surface feature", e);
1505
+ item.remove();
1506
+ }
1507
+ }
1508
+ return result;
1245
1509
  }
1246
- function rectByCenter(centerX, centerY, width, height) {
1247
- return {
1248
- left: centerX - width / 2,
1249
- top: centerY - height / 2,
1250
- width,
1251
- height,
1252
- centerX,
1253
- centerY
1254
- };
1510
+ function generateDielinePath(options) {
1511
+ const paperWidth = options.canvasWidth || options.width * 2 || 2e3;
1512
+ const paperHeight = options.canvasHeight || options.height * 2 || 2e3;
1513
+ ensurePaper(paperWidth, paperHeight);
1514
+ import_paper.default.project.activeLayer.removeChildren();
1515
+ const perimeter = getPerimeterShape(options);
1516
+ const finalShape = applySurfaceFeatures(perimeter, options.features, options);
1517
+ const pathData = finalShape.pathData;
1518
+ finalShape.remove();
1519
+ return pathData;
1255
1520
  }
1256
- function getCutSizeMm(size) {
1257
- if (size.cutMode === "trim") {
1258
- return { widthMm: size.actualWidthMm, heightMm: size.actualHeightMm };
1259
- }
1260
- const delta = size.cutMarginMm * 2;
1261
- if (size.cutMode === "outset") {
1262
- return {
1263
- widthMm: size.actualWidthMm + delta,
1264
- heightMm: size.actualHeightMm + delta
1265
- };
1521
+ function generateBleedZonePath(originalOptions, offsetOptions, offset) {
1522
+ const paperWidth = originalOptions.canvasWidth || originalOptions.width * 2 || 2e3;
1523
+ const paperHeight = originalOptions.canvasHeight || originalOptions.height * 2 || 2e3;
1524
+ ensurePaper(paperWidth, paperHeight);
1525
+ import_paper.default.project.activeLayer.removeChildren();
1526
+ const pOriginal = getPerimeterShape(originalOptions);
1527
+ const shapeOriginal = applySurfaceFeatures(
1528
+ pOriginal,
1529
+ originalOptions.features,
1530
+ originalOptions
1531
+ );
1532
+ const pOffset = getPerimeterShape(offsetOptions);
1533
+ const shapeOffset = applySurfaceFeatures(
1534
+ pOffset,
1535
+ offsetOptions.features,
1536
+ offsetOptions
1537
+ );
1538
+ let bleedZone;
1539
+ if (offset > 0) {
1540
+ bleedZone = shapeOffset.subtract(shapeOriginal);
1541
+ } else {
1542
+ bleedZone = shapeOriginal.subtract(shapeOffset);
1266
1543
  }
1267
- return {
1268
- widthMm: Math.max(size.minMm, size.actualWidthMm - delta),
1269
- heightMm: Math.max(size.minMm, size.actualHeightMm - delta)
1544
+ const pathData = bleedZone.pathData;
1545
+ shapeOriginal.remove();
1546
+ shapeOffset.remove();
1547
+ bleedZone.remove();
1548
+ return pathData;
1549
+ }
1550
+ function getLowestPointOnDieline(options) {
1551
+ ensurePaper(options.width * 2, options.height * 2);
1552
+ import_paper.default.project.activeLayer.removeChildren();
1553
+ const shape = createBaseShape(options);
1554
+ const bounds = shape.bounds;
1555
+ const result = {
1556
+ x: bounds.center.x,
1557
+ y: bounds.bottom
1270
1558
  };
1559
+ shape.remove();
1560
+ return result;
1271
1561
  }
1272
- function computeSceneLayout(canvasService, size) {
1273
- const canvasWidth = canvasService.canvas.width || 0;
1274
- const canvasHeight = canvasService.canvas.height || 0;
1275
- if (canvasWidth <= 0 || canvasHeight <= 0) return null;
1276
- const { widthMm: cutWidthMm, heightMm: cutHeightMm } = getCutSizeMm(size);
1277
- const viewWidthMm = Math.max(size.actualWidthMm, cutWidthMm);
1278
- const viewHeightMm = Math.max(size.actualHeightMm, cutHeightMm);
1279
- if (!Number.isFinite(viewWidthMm) || !Number.isFinite(viewHeightMm) || viewWidthMm <= 0 || viewHeightMm <= 0) {
1280
- return null;
1281
- }
1282
- const paddingPx = resolvePaddingPx(
1283
- size.viewPadding,
1284
- canvasWidth,
1285
- canvasHeight
1286
- );
1287
- canvasService.viewport.updateContainer(canvasWidth, canvasHeight);
1288
- canvasService.viewport.setPadding(paddingPx);
1289
- canvasService.viewport.updatePhysical(viewWidthMm, viewHeightMm);
1290
- const layout = canvasService.viewport.layout;
1291
- if (!Number.isFinite(layout.scale) || !Number.isFinite(layout.offsetX) || !Number.isFinite(layout.offsetY) || layout.scale <= 0) {
1292
- return null;
1293
- }
1294
- const centerX = layout.offsetX + layout.width / 2;
1295
- const centerY = layout.offsetY + layout.height / 2;
1296
- const trimWidthPx = size.actualWidthMm * layout.scale;
1297
- const trimHeightPx = size.actualHeightMm * layout.scale;
1298
- const cutWidthPx = cutWidthMm * layout.scale;
1299
- const cutHeightPx = cutHeightMm * layout.scale;
1300
- const trimRect = rectByCenter(centerX, centerY, trimWidthPx, trimHeightPx);
1301
- const cutRect = rectByCenter(centerX, centerY, cutWidthPx, cutHeightPx);
1302
- const bleedRect = rectByCenter(
1303
- centerX,
1304
- centerY,
1305
- Math.max(trimWidthPx, cutWidthPx),
1306
- Math.max(trimHeightPx, cutHeightPx)
1307
- );
1308
- return {
1309
- scale: layout.scale,
1310
- canvasWidth,
1311
- canvasHeight,
1312
- trimRect,
1313
- cutRect,
1314
- bleedRect,
1315
- trimWidthMm: size.actualWidthMm,
1316
- trimHeightMm: size.actualHeightMm,
1317
- cutWidthMm,
1318
- cutHeightMm,
1319
- cutMode: size.cutMode,
1320
- cutMarginMm: size.cutMarginMm
1562
+ function getNearestPointOnDieline(point, options) {
1563
+ ensurePaper(options.width * 2, options.height * 2);
1564
+ import_paper.default.project.activeLayer.removeChildren();
1565
+ const shape = createBaseShape(options);
1566
+ const p = new import_paper.default.Point(point.x, point.y);
1567
+ const location = shape.getNearestLocation(p);
1568
+ const result = {
1569
+ x: location.point.x,
1570
+ y: location.point.y,
1571
+ normal: location.normal ? { x: location.normal.x, y: location.normal.y } : void 0
1321
1572
  };
1573
+ shape.remove();
1574
+ return result;
1322
1575
  }
1323
- function buildSceneGeometry(configService, layout) {
1324
- const radiusMm = parseLengthToMm(
1325
- configService.get("dieline.radius", 0),
1326
- "mm"
1327
- );
1328
- const offset = (layout.cutRect.width - layout.trimRect.width) / 2;
1329
- const sourceWidth = Number(configService.get("dieline.customSourceWidthPx", 0));
1330
- const sourceHeight = Number(
1331
- configService.get("dieline.customSourceHeightPx", 0)
1332
- );
1333
- const shapeStyle = normalizeShapeStyle(
1334
- configService.get("dieline.shapeStyle", DEFAULT_DIELINE_SHAPE_STYLE)
1335
- );
1576
+ function getPathBounds(pathData) {
1577
+ const path = new import_paper.default.Path();
1578
+ path.pathData = pathData;
1579
+ const bounds = path.bounds;
1580
+ path.remove();
1336
1581
  return {
1337
- shape: normalizeDielineShape(
1338
- configService.get("dieline.shape", DEFAULT_DIELINE_SHAPE)
1339
- ),
1340
- shapeStyle,
1341
- unit: "px",
1342
- x: layout.trimRect.centerX,
1343
- y: layout.trimRect.centerY,
1344
- width: layout.trimRect.width,
1345
- height: layout.trimRect.height,
1346
- radius: radiusMm * layout.scale,
1347
- offset,
1348
- scale: layout.scale,
1349
- pathData: configService.get("dieline.pathData"),
1350
- customSourceWidthPx: Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : void 0,
1351
- customSourceHeightPx: Number.isFinite(sourceHeight) && sourceHeight > 0 ? sourceHeight : void 0
1582
+ x: bounds.x,
1583
+ y: bounds.y,
1584
+ width: bounds.width,
1585
+ height: bounds.height
1352
1586
  };
1353
1587
  }
1354
1588
 
@@ -1371,6 +1605,7 @@ var ImageTool = class {
1371
1605
  this.isImageSelectionActive = false;
1372
1606
  this.focusedImageId = null;
1373
1607
  this.renderSeq = 0;
1608
+ this.imageSpecs = [];
1374
1609
  this.overlaySpecs = [];
1375
1610
  this.onToolActivated = (event) => {
1376
1611
  const before = this.isToolActive;
@@ -1478,9 +1713,34 @@ var ImageTool = class {
1478
1713
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
1479
1714
  this.id,
1480
1715
  () => ({
1481
- rootLayerSpecs: {
1482
- [IMAGE_OVERLAY_LAYER_ID]: this.overlaySpecs
1483
- }
1716
+ passes: [
1717
+ {
1718
+ id: IMAGE_OBJECT_LAYER_ID,
1719
+ stack: 500,
1720
+ order: 0,
1721
+ visibility: {
1722
+ op: "not",
1723
+ expr: {
1724
+ op: "sessionActive",
1725
+ toolId: "pooder.kit.white-ink"
1726
+ }
1727
+ },
1728
+ objects: this.imageSpecs
1729
+ },
1730
+ {
1731
+ id: IMAGE_OVERLAY_LAYER_ID,
1732
+ stack: 800,
1733
+ order: 0,
1734
+ visibility: {
1735
+ op: "not",
1736
+ expr: {
1737
+ op: "sessionActive",
1738
+ toolId: "pooder.kit.white-ink"
1739
+ }
1740
+ },
1741
+ objects: this.overlaySpecs
1742
+ }
1743
+ ]
1484
1744
  }),
1485
1745
  { priority: 300 }
1486
1746
  );
@@ -1537,6 +1797,7 @@ var ImageTool = class {
1537
1797
  this.cropShapeHatchPattern = void 0;
1538
1798
  this.cropShapeHatchPatternColor = void 0;
1539
1799
  this.cropShapeHatchPatternKey = void 0;
1800
+ this.imageSpecs = [];
1540
1801
  this.overlaySpecs = [];
1541
1802
  this.clearRenderedImages();
1542
1803
  (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
@@ -2012,9 +2273,7 @@ var ImageTool = class {
2012
2273
  }
2013
2274
  getOverlayObjects() {
2014
2275
  if (!this.canvasService) return [];
2015
- return this.canvasService.getRootLayerObjects(
2016
- IMAGE_OVERLAY_LAYER_ID
2017
- );
2276
+ return this.canvasService.getPassObjects(IMAGE_OVERLAY_LAYER_ID);
2018
2277
  }
2019
2278
  getImageObject(id) {
2020
2279
  return this.getImageObjects().find((obj) => {
@@ -2024,9 +2283,9 @@ var ImageTool = class {
2024
2283
  }
2025
2284
  clearRenderedImages() {
2026
2285
  if (!this.canvasService) return;
2027
- const canvas = this.canvasService.canvas;
2028
- this.getImageObjects().forEach((obj) => canvas.remove(obj));
2029
- this.canvasService.requestRenderAll();
2286
+ this.imageSpecs = [];
2287
+ this.overlaySpecs = [];
2288
+ this.canvasService.requestRenderFromProducers();
2030
2289
  }
2031
2290
  purgeSourceSizeCacheForItem(item) {
2032
2291
  if (!item) return;
@@ -2054,6 +2313,29 @@ var ImageTool = class {
2054
2313
  }
2055
2314
  return { width: 1, height: 1 };
2056
2315
  }
2316
+ async ensureSourceSize(src) {
2317
+ if (!src) return null;
2318
+ const cached = this.sourceSizeBySrc.get(src);
2319
+ if (cached) return cached;
2320
+ try {
2321
+ const image = await import_fabric2.Image.fromURL(src, {
2322
+ crossOrigin: "anonymous"
2323
+ });
2324
+ const width = Number((image == null ? void 0 : image.width) || 0);
2325
+ const height = Number((image == null ? void 0 : image.height) || 0);
2326
+ if (width > 0 && height > 0) {
2327
+ const size = { width, height };
2328
+ this.sourceSizeBySrc.set(src, size);
2329
+ return size;
2330
+ }
2331
+ } catch (error) {
2332
+ this.debug("image:size:load-failed", {
2333
+ src,
2334
+ error: error instanceof Error ? error.message : String(error)
2335
+ });
2336
+ }
2337
+ return null;
2338
+ }
2057
2339
  getCoverScale(frame, size) {
2058
2340
  const sw = Math.max(1, size.width);
2059
2341
  const sh = Math.max(1, size.height);
@@ -2388,24 +2670,6 @@ var ImageTool = class {
2388
2670
  opacity: render.opacity
2389
2671
  };
2390
2672
  }
2391
- toScreenObjectProps(props) {
2392
- if (!this.canvasService) return props;
2393
- const next = { ...props };
2394
- if (Number.isFinite(next.left) || Number.isFinite(next.top)) {
2395
- const mapped = this.canvasService.toScreenPoint({
2396
- x: Number.isFinite(next.left) ? Number(next.left) : 0,
2397
- y: Number.isFinite(next.top) ? Number(next.top) : 0
2398
- });
2399
- if (Number.isFinite(next.left)) next.left = mapped.x;
2400
- if (Number.isFinite(next.top)) next.top = mapped.y;
2401
- }
2402
- const sceneScale = this.canvasService.getSceneScale();
2403
- const sx = Number.isFinite(next.scaleX) ? Number(next.scaleX) : 1;
2404
- const sy = Number.isFinite(next.scaleY) ? Number(next.scaleY) : 1;
2405
- next.scaleX = sx * sceneScale;
2406
- next.scaleY = sy * sceneScale;
2407
- return next;
2408
- }
2409
2673
  toSceneObjectScale(value) {
2410
2674
  if (!this.canvasService) return value;
2411
2675
  return value / this.canvasService.getSceneScale();
@@ -2416,104 +2680,27 @@ var ImageTool = class {
2416
2680
  if (typeof obj.getSrc === "function") return obj.getSrc();
2417
2681
  return (_a = obj == null ? void 0 : obj._originalElement) == null ? void 0 : _a.src;
2418
2682
  }
2419
- applyImageControlVisibility(obj) {
2420
- if (typeof (obj == null ? void 0 : obj.setControlsVisibility) !== "function") return;
2421
- obj.setControlsVisibility({
2422
- mt: false,
2423
- mb: false,
2424
- ml: false,
2425
- mr: false,
2426
- tl: true,
2427
- tr: true,
2428
- bl: true,
2429
- br: true,
2430
- mtr: true
2431
- });
2432
- }
2433
- async upsertImageObject(item, frame, seq) {
2434
- if (!this.canvasService) return;
2435
- const canvas = this.canvasService.canvas;
2436
- const render = this.resolveRenderImageState(item);
2437
- if (!render.src) return;
2438
- let obj = this.getImageObject(item.id);
2439
- const currentSrc = this.getCurrentSrc(obj);
2440
- if (obj && currentSrc && currentSrc !== render.src) {
2441
- canvas.remove(obj);
2442
- obj = void 0;
2443
- }
2444
- if (!obj) {
2445
- const created = await import_fabric2.Image.fromURL(render.src, {
2446
- crossOrigin: "anonymous"
2447
- });
2448
- if (seq !== this.renderSeq) return;
2449
- created.set({
2683
+ async buildImageSpecs(items, frame) {
2684
+ const specs = [];
2685
+ for (const item of items) {
2686
+ const render = this.resolveRenderImageState(item);
2687
+ if (!render.src) continue;
2688
+ const ensured = await this.ensureSourceSize(render.src);
2689
+ const sourceSize = ensured || this.getSourceSize(render.src);
2690
+ const props = this.computeCanvasProps(render, sourceSize, frame);
2691
+ specs.push({
2692
+ id: item.id,
2693
+ type: "image",
2694
+ src: render.src,
2450
2695
  data: {
2451
2696
  id: item.id,
2452
2697
  layerId: IMAGE_OBJECT_LAYER_ID,
2453
2698
  type: "image-item"
2454
- }
2699
+ },
2700
+ props
2455
2701
  });
2456
- canvas.add(created);
2457
- obj = created;
2458
- }
2459
- this.rememberSourceSize(render.src, obj);
2460
- const sourceSize = this.getSourceSize(render.src, obj);
2461
- const props = this.computeCanvasProps(render, sourceSize, frame);
2462
- const screenProps = this.toScreenObjectProps(props);
2463
- obj.set({
2464
- ...screenProps,
2465
- data: {
2466
- ...obj.data || {},
2467
- id: item.id,
2468
- layerId: IMAGE_OBJECT_LAYER_ID,
2469
- type: "image-item"
2470
- }
2471
- });
2472
- this.applyImageControlVisibility(obj);
2473
- obj.setCoords();
2474
- const resolver = this.loadResolvers.get(item.id);
2475
- if (resolver) {
2476
- resolver();
2477
- this.loadResolvers.delete(item.id);
2478
- }
2479
- }
2480
- syncImageZOrder(items) {
2481
- if (!this.canvasService) return;
2482
- const canvas = this.canvasService.canvas;
2483
- const objects = canvas.getObjects();
2484
- let insertIndex = 0;
2485
- const backgroundLayer = this.canvasService.getLayer("background");
2486
- if (backgroundLayer) {
2487
- const bgIndex = objects.indexOf(backgroundLayer);
2488
- if (bgIndex >= 0) insertIndex = bgIndex + 1;
2489
- }
2490
- items.forEach((item) => {
2491
- const obj = this.getImageObject(item.id);
2492
- if (!obj) return;
2493
- canvas.moveObjectTo(obj, insertIndex);
2494
- insertIndex += 1;
2495
- });
2496
- const overlayObjects = this.getOverlayObjects().sort((a, b) => {
2497
- var _a, _b, _c, _d;
2498
- const az = Number((_b = (_a = a == null ? void 0 : a.data) == null ? void 0 : _a.zIndex) != null ? _b : 0);
2499
- const bz = Number((_d = (_c = b == null ? void 0 : b.data) == null ? void 0 : _c.zIndex) != null ? _d : 0);
2500
- return az - bz;
2501
- });
2502
- overlayObjects.forEach((obj) => {
2503
- canvas.bringObjectToFront(obj);
2504
- });
2505
- if (this.isDebugEnabled()) {
2506
- const stack = canvas.getObjects().map((obj, index) => {
2507
- var _a, _b, _c;
2508
- return {
2509
- index,
2510
- id: (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id,
2511
- layerId: (_b = obj == null ? void 0 : obj.data) == null ? void 0 : _b.layerId,
2512
- zIndex: (_c = obj == null ? void 0 : obj.data) == null ? void 0 : _c.zIndex
2513
- };
2514
- }).filter((item) => item.layerId === IMAGE_OVERLAY_LAYER_ID);
2515
- this.debug("overlay:stack", stack);
2516
2702
  }
2703
+ return specs;
2517
2704
  }
2518
2705
  buildOverlaySpecs(frame, sceneGeometry) {
2519
2706
  const visible = this.isImageEditingVisible();
@@ -2677,7 +2864,7 @@ var ImageTool = class {
2677
2864
  evented: false
2678
2865
  }
2679
2866
  };
2680
- const specs = [...mask, ...shapeOverlay, frameSpec];
2867
+ const specs = shapeOverlay.length > 0 ? [...mask, ...shapeOverlay] : [...mask, ...shapeOverlay, frameSpec];
2681
2868
  this.debug("overlay:built", {
2682
2869
  frame,
2683
2870
  shape: sceneGeometry == null ? void 0 : sceneGeometry.shape,
@@ -2707,30 +2894,32 @@ var ImageTool = class {
2707
2894
  skipRender: true
2708
2895
  });
2709
2896
  }
2710
- this.getImageObjects().forEach((obj) => {
2711
- var _a, _b;
2712
- const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
2713
- if (typeof id === "string" && !desiredIds.has(id)) {
2714
- (_b = this.canvasService) == null ? void 0 : _b.canvas.remove(obj);
2715
- }
2716
- });
2717
- for (const item of renderItems) {
2718
- if (seq !== this.renderSeq) return;
2719
- await this.upsertImageObject(item, frame, seq);
2720
- }
2897
+ const imageSpecs = await this.buildImageSpecs(renderItems, frame);
2721
2898
  if (seq !== this.renderSeq) return;
2722
- this.syncImageZOrder(renderItems);
2723
2899
  const sceneGeometry = await this.resolveSceneGeometryForOverlay();
2724
2900
  if (seq !== this.renderSeq) return;
2725
- const overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
2726
- this.overlaySpecs = overlaySpecs;
2901
+ this.imageSpecs = imageSpecs;
2902
+ this.overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
2727
2903
  await this.canvasService.flushRenderFromProducers();
2728
- this.syncImageZOrder(renderItems);
2904
+ if (seq !== this.renderSeq) return;
2905
+ renderItems.forEach((item) => {
2906
+ if (!this.getImageObject(item.id)) return;
2907
+ const resolver = this.loadResolvers.get(item.id);
2908
+ if (!resolver) return;
2909
+ resolver();
2910
+ this.loadResolvers.delete(item.id);
2911
+ });
2912
+ if (this.focusedImageId && this.isToolActive) {
2913
+ this.setImageFocus(this.focusedImageId, {
2914
+ syncCanvasSelection: true,
2915
+ skipRender: true
2916
+ });
2917
+ }
2729
2918
  const overlayCanvasCount = this.getOverlayObjects().length;
2730
2919
  this.debug("render:done", {
2731
2920
  seq,
2732
2921
  renderCount: renderItems.length,
2733
- overlayCount: overlaySpecs.length,
2922
+ overlayCount: this.overlaySpecs.length,
2734
2923
  overlayCanvasCount,
2735
2924
  isToolActive: this.isToolActive,
2736
2925
  isImageSelectionActive: this.isImageSelectionActive,
@@ -4376,11 +4565,11 @@ var DielineTool = class {
4376
4565
  style: "solid"
4377
4566
  },
4378
4567
  insideColor: "rgba(0,0,0,0)",
4379
- outsideColor: "#ffffff",
4380
4568
  showBleedLines: true,
4381
4569
  features: []
4382
4570
  };
4383
4571
  this.specs = [];
4572
+ this.effects = [];
4384
4573
  this.renderSeq = 0;
4385
4574
  this.onCanvasResized = () => {
4386
4575
  this.updateDieline();
@@ -4417,10 +4606,23 @@ var DielineTool = class {
4417
4606
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
4418
4607
  this.id,
4419
4608
  () => ({
4420
- layerSpecs: {
4421
- [DIELINE_LAYER_ID]: this.specs
4422
- },
4423
- replaceLayerIds: [DIELINE_LAYER_ID]
4609
+ passes: [
4610
+ {
4611
+ id: DIELINE_LAYER_ID,
4612
+ stack: 700,
4613
+ order: 0,
4614
+ replace: true,
4615
+ visibility: {
4616
+ op: "not",
4617
+ expr: {
4618
+ op: "activeToolIn",
4619
+ ids: ["pooder.kit.image", "pooder.kit.white-ink"]
4620
+ }
4621
+ },
4622
+ effects: this.effects,
4623
+ objects: this.specs
4624
+ }
4625
+ ]
4424
4626
  }),
4425
4627
  { priority: 250 }
4426
4628
  );
@@ -4476,10 +4678,6 @@ var DielineTool = class {
4476
4678
  s.offsetLine.style
4477
4679
  );
4478
4680
  s.insideColor = configService.get("dieline.insideColor", s.insideColor);
4479
- s.outsideColor = configService.get(
4480
- "dieline.outsideColor",
4481
- s.outsideColor
4482
- );
4483
4681
  s.showBleedLines = configService.get(
4484
4682
  "dieline.showBleedLines",
4485
4683
  s.showBleedLines
@@ -4542,9 +4740,6 @@ var DielineTool = class {
4542
4740
  case "dieline.insideColor":
4543
4741
  s.insideColor = e.value;
4544
4742
  break;
4545
- case "dieline.outsideColor":
4546
- s.outsideColor = e.value;
4547
- break;
4548
4743
  case "dieline.showBleedLines":
4549
4744
  s.showBleedLines = e.value;
4550
4745
  break;
@@ -4573,6 +4768,7 @@ var DielineTool = class {
4573
4768
  context.eventBus.off("canvas:resized", this.onCanvasResized);
4574
4769
  this.renderSeq += 1;
4575
4770
  this.specs = [];
4771
+ this.effects = [];
4576
4772
  (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
4577
4773
  this.renderProducerDisposable = void 0;
4578
4774
  if (this.canvasService) {
@@ -4689,12 +4885,6 @@ var DielineTool = class {
4689
4885
  label: "Inside Color",
4690
4886
  default: s.insideColor
4691
4887
  },
4692
- {
4693
- id: "dieline.outsideColor",
4694
- type: "color",
4695
- label: "Outside Color",
4696
- default: s.outsideColor
4697
- },
4698
4888
  {
4699
4889
  id: "dieline.features",
4700
4890
  type: "json",
@@ -4818,6 +5008,12 @@ var DielineTool = class {
4818
5008
  "ConfigurationService"
4819
5009
  );
4820
5010
  }
5011
+ hasImageItems() {
5012
+ const configService = this.getConfigService();
5013
+ if (!configService) return false;
5014
+ const items = configService.get("image.items", []);
5015
+ return Array.isArray(items) && items.length > 0;
5016
+ }
4821
5017
  syncSizeState(configService) {
4822
5018
  const sizeState = readSizeState(configService);
4823
5019
  this.state.width = sizeState.actualWidthMm;
@@ -4825,29 +5021,6 @@ var DielineTool = class {
4825
5021
  this.state.padding = sizeState.viewPadding;
4826
5022
  this.state.offset = sizeState.cutMode === "outset" ? sizeState.cutMarginMm : sizeState.cutMode === "inset" ? -sizeState.cutMarginMm : 0;
4827
5023
  }
4828
- bringFeatureMarkersToFront() {
4829
- if (!this.canvasService) return;
4830
- const canvas = this.canvasService.canvas;
4831
- canvas.getObjects().filter((obj) => {
4832
- var _a;
4833
- return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.type) === "feature-marker";
4834
- }).forEach((obj) => canvas.bringObjectToFront(obj));
4835
- }
4836
- ensureLayerStacking() {
4837
- if (!this.canvasService) return;
4838
- const layer = this.canvasService.getLayer(DIELINE_LAYER_ID);
4839
- if (!layer) return;
4840
- const userLayer = this.canvasService.getLayer("user");
4841
- if (userLayer) {
4842
- const layerIndex = this.canvasService.canvas.getObjects().indexOf(layer);
4843
- const userIndex = this.canvasService.canvas.getObjects().indexOf(userLayer);
4844
- if (layerIndex < userIndex) {
4845
- this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
4846
- }
4847
- return;
4848
- }
4849
- this.canvasService.canvas.bringObjectToFront(layer);
4850
- }
4851
5024
  buildDielineSpecs(sceneLayout) {
4852
5025
  var _a, _b;
4853
5026
  const {
@@ -4857,10 +5030,10 @@ var DielineTool = class {
4857
5030
  mainLine,
4858
5031
  offsetLine,
4859
5032
  insideColor,
4860
- outsideColor,
4861
5033
  showBleedLines,
4862
5034
  features
4863
5035
  } = this.state;
5036
+ const hasImages = this.hasImageItems();
4864
5037
  const canvasW = sceneLayout.canvasWidth || ((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 800;
4865
5038
  const canvasH = sceneLayout.canvasHeight || ((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 600;
4866
5039
  const scale = sceneLayout.scale;
@@ -4882,41 +5055,8 @@ var DielineTool = class {
4882
5055
  radius: (f.radius || 0) * scale
4883
5056
  }));
4884
5057
  const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
4885
- const maskPathData = generateMaskPath({
4886
- canvasWidth: canvasW,
4887
- canvasHeight: canvasH,
4888
- shape,
4889
- width: cutW,
4890
- height: cutH,
4891
- radius: cutR,
4892
- x: cx,
4893
- y: cy,
4894
- features: cutFeatures,
4895
- shapeStyle,
4896
- pathData: this.state.pathData,
4897
- customSourceWidthPx: this.state.customSourceWidthPx,
4898
- customSourceHeightPx: this.state.customSourceHeightPx
4899
- });
4900
- const specs = [
4901
- {
4902
- id: "dieline.mask",
4903
- type: "path",
4904
- space: "screen",
4905
- data: { id: "dieline.mask", type: "dieline" },
4906
- props: {
4907
- pathData: maskPathData,
4908
- fill: outsideColor,
4909
- stroke: null,
4910
- selectable: false,
4911
- evented: false,
4912
- originX: "left",
4913
- originY: "top",
4914
- left: 0,
4915
- top: 0
4916
- }
4917
- }
4918
- ];
4919
- if (insideColor && insideColor !== "transparent" && insideColor !== "rgba(0,0,0,0)") {
5058
+ const specs = [];
5059
+ if (insideColor && insideColor !== "transparent" && insideColor !== "rgba(0,0,0,0)" && !hasImages) {
4920
5060
  const productPathData = generateDielinePath({
4921
5061
  shape,
4922
5062
  width: cutW,
@@ -5070,6 +5210,73 @@ var DielineTool = class {
5070
5210
  });
5071
5211
  return specs;
5072
5212
  }
5213
+ buildImageClipEffects(sceneLayout) {
5214
+ var _a, _b;
5215
+ const { shape, shapeStyle, radius, features } = this.state;
5216
+ const canvasW = sceneLayout.canvasWidth || ((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 800;
5217
+ const canvasH = sceneLayout.canvasHeight || ((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 600;
5218
+ const scale = sceneLayout.scale;
5219
+ const cx = sceneLayout.trimRect.centerX;
5220
+ const cy = sceneLayout.trimRect.centerY;
5221
+ const visualWidth = sceneLayout.trimRect.width;
5222
+ const visualRadius = radius * scale;
5223
+ const cutW = sceneLayout.cutRect.width;
5224
+ const cutH = sceneLayout.cutRect.height;
5225
+ const visualOffset = (cutW - visualWidth) / 2;
5226
+ const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
5227
+ const absoluteFeatures = (features || []).map((f) => ({
5228
+ ...f,
5229
+ x: f.x,
5230
+ y: f.y,
5231
+ width: (f.width || 0) * scale,
5232
+ height: (f.height || 0) * scale,
5233
+ radius: (f.radius || 0) * scale
5234
+ }));
5235
+ const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
5236
+ const clipPathData = generateDielinePath({
5237
+ shape,
5238
+ width: cutW,
5239
+ height: cutH,
5240
+ radius: cutR,
5241
+ x: cx,
5242
+ y: cy,
5243
+ features: cutFeatures,
5244
+ shapeStyle,
5245
+ pathData: this.state.pathData,
5246
+ customSourceWidthPx: this.state.customSourceWidthPx,
5247
+ customSourceHeightPx: this.state.customSourceHeightPx,
5248
+ canvasWidth: canvasW,
5249
+ canvasHeight: canvasH
5250
+ });
5251
+ if (!clipPathData) return [];
5252
+ return [
5253
+ {
5254
+ type: "clipPath",
5255
+ id: "dieline.clip.image",
5256
+ targetPassIds: [IMAGE_OBJECT_LAYER_ID2],
5257
+ source: {
5258
+ id: "dieline.effect.clip-path",
5259
+ type: "path",
5260
+ space: "screen",
5261
+ data: {
5262
+ id: "dieline.effect.clip-path",
5263
+ type: "dieline-effect",
5264
+ effect: "clipPath"
5265
+ },
5266
+ props: {
5267
+ pathData: clipPathData,
5268
+ fill: "#000000",
5269
+ stroke: null,
5270
+ originX: "left",
5271
+ originY: "top",
5272
+ selectable: false,
5273
+ evented: false,
5274
+ excludeFromExport: true
5275
+ }
5276
+ }
5277
+ }
5278
+ ];
5279
+ }
5073
5280
  updateDieline(_emitEvent = true) {
5074
5281
  void this.updateDielineAsync();
5075
5282
  }
@@ -5086,17 +5293,17 @@ var DielineTool = class {
5086
5293
  if (!sceneLayout) {
5087
5294
  if (seq !== this.renderSeq) return;
5088
5295
  this.specs = [];
5296
+ this.effects = [];
5089
5297
  await this.canvasService.flushRenderFromProducers();
5090
5298
  return;
5091
5299
  }
5092
5300
  const nextSpecs = this.buildDielineSpecs(sceneLayout);
5301
+ const nextEffects = this.buildImageClipEffects(sceneLayout);
5093
5302
  if (seq !== this.renderSeq) return;
5094
5303
  this.specs = nextSpecs;
5304
+ this.effects = nextEffects;
5095
5305
  await this.canvasService.flushRenderFromProducers();
5096
5306
  if (seq !== this.renderSeq) return;
5097
- this.ensureLayerStacking();
5098
- this.bringFeatureMarkersToFront();
5099
- this.canvasService.bringLayerToFront("ruler-overlay");
5100
5307
  this.canvasService.requestRenderAll();
5101
5308
  }
5102
5309
  getGeometry() {
@@ -5529,9 +5736,14 @@ var FeatureTool = class {
5529
5736
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
5530
5737
  this.id,
5531
5738
  () => ({
5532
- rootLayerSpecs: {
5533
- [FEATURE_OVERLAY_LAYER_ID]: this.specs
5534
- }
5739
+ passes: [
5740
+ {
5741
+ id: FEATURE_OVERLAY_LAYER_ID,
5742
+ stack: 880,
5743
+ order: 0,
5744
+ objects: this.specs
5745
+ }
5746
+ ]
5535
5747
  }),
5536
5748
  { priority: 350 }
5537
5749
  );
@@ -5674,10 +5886,10 @@ var FeatureTool = class {
5674
5886
  await this.refreshGeometry();
5675
5887
  this.setWorkingFeatures(original);
5676
5888
  this.hasWorkingChanges = false;
5889
+ this.clearFeatureSessionState();
5677
5890
  this.redraw();
5678
5891
  this.emitWorkingChange();
5679
5892
  this.updateCommittedFeatures(original);
5680
- this.clearFeatureSessionState();
5681
5893
  return { ok: true };
5682
5894
  }
5683
5895
  },
@@ -5993,6 +6205,7 @@ var FeatureTool = class {
5993
6205
  }
5994
6206
  getDraggableMarkerTarget(target) {
5995
6207
  var _a, _b;
6208
+ if (!this.isFeatureSessionActive || !this.isToolActive) return null;
5996
6209
  if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return null;
5997
6210
  if (((_b = target.data) == null ? void 0 : _b.markerRole) !== "handle") return null;
5998
6211
  return target;
@@ -6115,18 +6328,12 @@ var FeatureTool = class {
6115
6328
  if (seq !== this.renderSeq) return;
6116
6329
  await this.canvasService.flushRenderFromProducers();
6117
6330
  if (seq !== this.renderSeq) return;
6118
- this.syncOverlayOrder();
6119
6331
  if (options.enforceConstraints) {
6120
6332
  this.enforceConstraints();
6121
6333
  }
6122
6334
  }
6123
- syncOverlayOrder() {
6124
- if (!this.canvasService) return;
6125
- this.canvasService.bringLayerToFront(FEATURE_OVERLAY_LAYER_ID);
6126
- this.canvasService.bringLayerToFront("ruler-overlay");
6127
- }
6128
6335
  buildFeatureSpecs() {
6129
- if (!this.currentGeometry || this.workingFeatures.length === 0) {
6336
+ if (!this.isFeatureSessionActive || !this.currentGeometry || this.workingFeatures.length === 0) {
6130
6337
  return [];
6131
6338
  }
6132
6339
  const groups = /* @__PURE__ */ new Map();
@@ -6196,11 +6403,12 @@ var FeatureTool = class {
6196
6403
  const color = feature.color || (feature.operation === "add" ? "#00FF00" : "#FF0000");
6197
6404
  const strokeDash = feature.strokeDash || (feature.operation === "subtract" ? [4, 4] : void 0);
6198
6405
  const interactive = options.markerRole === "handle";
6406
+ const sessionVisible = this.isToolActive && this.isFeatureSessionActive;
6199
6407
  const baseData = this.buildMarkerData(marker, options);
6200
6408
  const commonProps = {
6201
- visible: this.isToolActive,
6202
- selectable: interactive && this.isToolActive,
6203
- evented: interactive && this.isToolActive,
6409
+ visible: sessionVisible,
6410
+ selectable: interactive && sessionVisible,
6411
+ evented: interactive && sessionVisible,
6204
6412
  hasControls: false,
6205
6413
  hasBorders: false,
6206
6414
  hoverCursor: interactive ? "move" : "default",
@@ -6265,7 +6473,7 @@ var FeatureTool = class {
6265
6473
  markerOffsetY: -visualHeight / 2
6266
6474
  },
6267
6475
  props: {
6268
- visible: this.isToolActive,
6476
+ visible: sessionVisible,
6269
6477
  selectable: false,
6270
6478
  evented: false,
6271
6479
  width: visualWidth,
@@ -6433,9 +6641,14 @@ var FilmTool = class {
6433
6641
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
6434
6642
  this.id,
6435
6643
  () => ({
6436
- layerSpecs: {
6437
- [FILM_LAYER_ID]: this.specs
6438
- }
6644
+ passes: [
6645
+ {
6646
+ id: FILM_LAYER_ID,
6647
+ stack: 1e3,
6648
+ order: 0,
6649
+ objects: this.specs
6650
+ }
6651
+ ]
6439
6652
  }),
6440
6653
  { priority: 500 }
6441
6654
  );
@@ -6609,7 +6822,6 @@ var FilmTool = class {
6609
6822
  this.specs = this.buildFilmSpecs(this.renderImageUrl, this.opacity);
6610
6823
  await this.canvasService.flushRenderFromProducers();
6611
6824
  if (seq !== this.renderSeq) return;
6612
- this.canvasService.bringLayerToFront(FILM_LAYER_ID);
6613
6825
  this.canvasService.requestRenderAll();
6614
6826
  }
6615
6827
  };
@@ -6756,10 +6968,22 @@ var RulerTool = class {
6756
6968
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
6757
6969
  this.id,
6758
6970
  () => ({
6759
- rootLayerSpecs: {
6760
- [RULER_LAYER_ID]: this.specs
6761
- },
6762
- replaceRootLayerIds: [RULER_LAYER_ID]
6971
+ passes: [
6972
+ {
6973
+ id: RULER_LAYER_ID,
6974
+ stack: 950,
6975
+ order: 0,
6976
+ replace: true,
6977
+ visibility: {
6978
+ op: "not",
6979
+ expr: {
6980
+ op: "activeToolIn",
6981
+ ids: ["pooder.kit.white-ink"]
6982
+ }
6983
+ },
6984
+ objects: this.specs
6985
+ }
6986
+ ]
6763
6987
  }),
6764
6988
  { priority: 400 }
6765
6989
  );
@@ -7251,7 +7475,6 @@ var RulerTool = class {
7251
7475
  this.specs = specs;
7252
7476
  await this.canvasService.flushRenderFromProducers();
7253
7477
  if (seq !== this.renderSeq) return;
7254
- this.canvasService.bringLayerToFront(RULER_LAYER_ID);
7255
7478
  this.canvasService.requestRenderAll();
7256
7479
  this.log("render:done", { seq });
7257
7480
  }
@@ -7263,7 +7486,6 @@ var WHITE_INK_OBJECT_LAYER_ID = "white-ink.user";
7263
7486
  var WHITE_INK_COVER_LAYER_ID = "white-ink.cover";
7264
7487
  var WHITE_INK_OVERLAY_LAYER_ID = "white-ink.overlay";
7265
7488
  var IMAGE_OBJECT_LAYER_ID3 = "image.user";
7266
- var IMAGE_OVERLAY_LAYER_ID2 = "image-overlay";
7267
7489
  var WHITE_INK_DEBUG_KEY = "whiteInk.debug";
7268
7490
  var WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY = "whiteInk.previewImageVisible";
7269
7491
  var WHITE_INK_DEFAULT_OPACITY = 0.85;
@@ -7341,11 +7563,26 @@ var WhiteInkTool = class {
7341
7563
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
7342
7564
  this.id,
7343
7565
  () => ({
7344
- rootLayerSpecs: {
7345
- [WHITE_INK_OBJECT_LAYER_ID]: this.whiteSpecs,
7346
- [WHITE_INK_COVER_LAYER_ID]: this.coverSpecs,
7347
- [WHITE_INK_OVERLAY_LAYER_ID]: this.overlaySpecs
7348
- }
7566
+ passes: [
7567
+ {
7568
+ id: WHITE_INK_COVER_LAYER_ID,
7569
+ stack: 220,
7570
+ order: 0,
7571
+ objects: this.coverSpecs
7572
+ },
7573
+ {
7574
+ id: WHITE_INK_OBJECT_LAYER_ID,
7575
+ stack: 221,
7576
+ order: 0,
7577
+ objects: this.whiteSpecs
7578
+ },
7579
+ {
7580
+ id: WHITE_INK_OVERLAY_LAYER_ID,
7581
+ stack: 790,
7582
+ order: 0,
7583
+ objects: this.overlaySpecs
7584
+ }
7585
+ ]
7349
7586
  }),
7350
7587
  { priority: 260 }
7351
7588
  );
@@ -7424,7 +7661,6 @@ var WhiteInkTool = class {
7424
7661
  (_a = this.dirtyTrackerDisposable) == null ? void 0 : _a.dispose();
7425
7662
  this.dirtyTrackerDisposable = void 0;
7426
7663
  this.clearRenderedWhiteInks();
7427
- this.applyImageVisibilityForWhiteInk(false);
7428
7664
  (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
7429
7665
  this.renderProducerDisposable = void 0;
7430
7666
  if (this.canvasService) {
@@ -8264,25 +8500,9 @@ var WhiteInkTool = class {
8264
8500
  selectable: false,
8265
8501
  evented: false,
8266
8502
  excludeFromExport: true
8267
- }
8268
- }
8269
- ];
8270
- }
8271
- applyImageVisibilityForWhiteInk(previewActive) {
8272
- if (!this.canvasService) return;
8273
- const visible = !previewActive;
8274
- let changed = false;
8275
- this.canvasService.canvas.getObjects().forEach((obj) => {
8276
- var _a, _b;
8277
- if (((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) !== IMAGE_OBJECT_LAYER_ID3) return;
8278
- if (obj.visible === visible) return;
8279
- obj.set({ visible });
8280
- (_b = obj.setCoords) == null ? void 0 : _b.call(obj);
8281
- changed = true;
8282
- });
8283
- if (changed) {
8284
- this.canvasService.requestRenderAll();
8285
- }
8503
+ }
8504
+ }
8505
+ ];
8286
8506
  }
8287
8507
  resolveRenderItems() {
8288
8508
  if (this.isToolActive) {
@@ -8308,52 +8528,6 @@ var WhiteInkTool = class {
8308
8528
  whiteScaleAdjustY: scaleAdjust.y
8309
8529
  };
8310
8530
  }
8311
- resolveDefaultInsertIndex(objects) {
8312
- if (!this.canvasService) return 0;
8313
- const backgroundLayer = this.canvasService.getLayer("background");
8314
- if (!backgroundLayer) return 0;
8315
- const bgIndex = objects.indexOf(backgroundLayer);
8316
- if (bgIndex < 0) return 0;
8317
- return bgIndex + 1;
8318
- }
8319
- syncZOrder() {
8320
- if (!this.canvasService) return;
8321
- const canvas = this.canvasService.canvas;
8322
- const whiteObjects = this.canvasService.getRootLayerObjects(
8323
- WHITE_INK_OBJECT_LAYER_ID
8324
- );
8325
- const coverObjects = this.canvasService.getRootLayerObjects(
8326
- WHITE_INK_COVER_LAYER_ID
8327
- );
8328
- const frameObjects = this.canvasService.getRootLayerObjects(
8329
- WHITE_INK_OVERLAY_LAYER_ID
8330
- );
8331
- const currentObjects = canvas.getObjects();
8332
- const imageIndexes = currentObjects.map(
8333
- (obj, index) => {
8334
- var _a;
8335
- return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === IMAGE_OBJECT_LAYER_ID3 ? index : -1;
8336
- }
8337
- ).filter((index) => index >= 0);
8338
- let whiteInsertIndex = imageIndexes.length ? Math.min(...imageIndexes) : this.resolveDefaultInsertIndex(currentObjects);
8339
- let coverInsertIndex = whiteInsertIndex;
8340
- coverObjects.forEach((obj) => {
8341
- canvas.moveObjectTo(obj, coverInsertIndex);
8342
- coverInsertIndex += 1;
8343
- });
8344
- whiteInsertIndex = coverInsertIndex;
8345
- whiteObjects.forEach((obj) => {
8346
- canvas.moveObjectTo(obj, whiteInsertIndex);
8347
- whiteInsertIndex += 1;
8348
- });
8349
- frameObjects.forEach((obj) => canvas.bringObjectToFront(obj));
8350
- canvas.getObjects().filter((obj) => {
8351
- var _a;
8352
- return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === IMAGE_OVERLAY_LAYER_ID2;
8353
- }).forEach((obj) => canvas.bringObjectToFront(obj));
8354
- this.canvasService.bringLayerToFront("dieline-overlay");
8355
- this.canvasService.bringLayerToFront("ruler-overlay");
8356
- }
8357
8531
  clearRenderedWhiteInks() {
8358
8532
  if (!this.canvasService) return;
8359
8533
  this.whiteSpecs = [];
@@ -8385,7 +8559,6 @@ var WhiteInkTool = class {
8385
8559
  this.syncToolActiveFromWorkbench();
8386
8560
  const seq = ++this.renderSeq;
8387
8561
  const previewActive = this.isPreviewActive();
8388
- this.applyImageVisibilityForWhiteInk(previewActive);
8389
8562
  const frame = this.getFrameRect();
8390
8563
  const frameSpecs = this.buildFrameSpecs(frame);
8391
8564
  let whiteSpecs = [];
@@ -8433,7 +8606,6 @@ var WhiteInkTool = class {
8433
8606
  this.overlaySpecs = frameSpecs;
8434
8607
  await this.canvasService.flushRenderFromProducers();
8435
8608
  if (seq !== this.renderSeq) return;
8436
- this.syncZOrder();
8437
8609
  this.canvasService.requestRenderAll();
8438
8610
  }
8439
8611
  getMaskCacheKey(sourceUrl, tint) {
@@ -8615,62 +8787,9 @@ var SceneLayoutService = class {
8615
8787
  }
8616
8788
  };
8617
8789
 
8618
- // src/extensions/sceneVisibility.ts
8619
- var import_core11 = require("@pooder/core");
8620
- var CANVAS_SERVICE_ID2 = "CanvasService";
8621
- var HIDDEN_DIELINE_TOOLS = /* @__PURE__ */ new Set(["pooder.kit.image", "pooder.kit.white-ink"]);
8622
- var HIDDEN_RULER_TOOLS = /* @__PURE__ */ new Set(["pooder.kit.white-ink"]);
8623
- var SceneVisibilityService = class {
8624
- constructor() {
8625
- this.activeToolId = null;
8626
- this.onToolActivated = (e) => {
8627
- this.activeToolId = e.id;
8628
- this.apply();
8629
- };
8630
- this.onObjectAdded = () => {
8631
- this.apply();
8632
- };
8633
- }
8634
- init(context) {
8635
- var _a, _b;
8636
- if (this.context) {
8637
- this.dispose(this.context);
8638
- }
8639
- const canvasService = context.get(CANVAS_SERVICE_ID2);
8640
- if (!canvasService) {
8641
- throw new Error("[SceneVisibilityService] CanvasService is required.");
8642
- }
8643
- this.context = context;
8644
- this.canvasService = canvasService;
8645
- this.activeToolId = (_b = (_a = context.get(import_core11.WORKBENCH_SERVICE)) == null ? void 0 : _a.activeToolId) != null ? _b : null;
8646
- context.eventBus.on("tool:activated", this.onToolActivated);
8647
- context.eventBus.on("object:added", this.onObjectAdded);
8648
- this.apply();
8649
- }
8650
- dispose(context) {
8651
- var _a;
8652
- const activeContext = (_a = this.context) != null ? _a : context;
8653
- activeContext.eventBus.off("tool:activated", this.onToolActivated);
8654
- activeContext.eventBus.off("object:added", this.onObjectAdded);
8655
- this.context = void 0;
8656
- this.activeToolId = null;
8657
- this.canvasService = void 0;
8658
- }
8659
- apply() {
8660
- if (!this.canvasService) return;
8661
- const dielineLayer = this.canvasService.getLayer("dieline-overlay");
8662
- if (dielineLayer) {
8663
- const visible = !HIDDEN_DIELINE_TOOLS.has(this.activeToolId || "");
8664
- this.canvasService.setLayerVisibility("dieline-overlay", visible);
8665
- }
8666
- const rulerVisible = !HIDDEN_RULER_TOOLS.has(this.activeToolId || "");
8667
- this.canvasService.setLayerVisibility("ruler-overlay", rulerVisible);
8668
- this.canvasService.requestRenderAll();
8669
- }
8670
- };
8671
-
8672
8790
  // src/services/CanvasService.ts
8673
8791
  var import_fabric5 = require("fabric");
8792
+ var import_core11 = require("@pooder/core");
8674
8793
 
8675
8794
  // src/services/ViewportSystem.ts
8676
8795
  var ViewportSystem = class {
@@ -8745,6 +8864,53 @@ var ViewportSystem = class {
8745
8864
  }
8746
8865
  };
8747
8866
 
8867
+ // src/services/visibility.ts
8868
+ function compareLayerObjectCount(actual, cmp, expected) {
8869
+ if (cmp === ">") return actual > expected;
8870
+ if (cmp === ">=") return actual >= expected;
8871
+ if (cmp === "<") return actual < expected;
8872
+ if (cmp === "<=") return actual <= expected;
8873
+ return actual === expected;
8874
+ }
8875
+ function layerState(context, layerId) {
8876
+ return context.layers.get(layerId) || { exists: false, objectCount: 0 };
8877
+ }
8878
+ function evaluateVisibilityExpr(expr, context) {
8879
+ var _a;
8880
+ if (!expr) return true;
8881
+ if (expr.op === "const") {
8882
+ return Boolean(expr.value);
8883
+ }
8884
+ if (expr.op === "activeToolIn") {
8885
+ const activeToolId = (_a = context.activeToolId) != null ? _a : null;
8886
+ return !!activeToolId && expr.ids.includes(activeToolId);
8887
+ }
8888
+ if (expr.op === "sessionActive") {
8889
+ const toolId = String(expr.toolId || "").trim();
8890
+ if (!toolId) return false;
8891
+ return context.isSessionActive ? context.isSessionActive(toolId) : false;
8892
+ }
8893
+ if (expr.op === "layerExists") {
8894
+ return layerState(context, expr.layerId).exists === true;
8895
+ }
8896
+ if (expr.op === "layerObjectCount") {
8897
+ const expected = Number(expr.value);
8898
+ if (!Number.isFinite(expected)) return false;
8899
+ const count = layerState(context, expr.layerId).objectCount;
8900
+ return compareLayerObjectCount(count, expr.cmp, expected);
8901
+ }
8902
+ if (expr.op === "not") {
8903
+ return !evaluateVisibilityExpr(expr.expr, context);
8904
+ }
8905
+ if (expr.op === "all") {
8906
+ return expr.exprs.every((item) => evaluateVisibilityExpr(item, context));
8907
+ }
8908
+ if (expr.op === "any") {
8909
+ return expr.exprs.some((item) => evaluateVisibilityExpr(item, context));
8910
+ }
8911
+ return true;
8912
+ }
8913
+
8748
8914
  // src/services/CanvasService.ts
8749
8915
  var CanvasService = class {
8750
8916
  constructor(el, options) {
@@ -8753,8 +8919,45 @@ var CanvasService = class {
8753
8919
  this.producerFlushRequested = false;
8754
8920
  this.producerLoopPending = false;
8755
8921
  this.producerLoopPromise = null;
8756
- this.managedProducerLayerIds = /* @__PURE__ */ new Set();
8757
- this.managedProducerRootLayerIds = /* @__PURE__ */ new Set();
8922
+ this.producerApplyInProgress = false;
8923
+ this.visibilityRefreshScheduled = false;
8924
+ this.managedProducerPassIds = /* @__PURE__ */ new Set();
8925
+ this.managedPassMetas = /* @__PURE__ */ new Map();
8926
+ this.canvasForwardersBound = false;
8927
+ this.forwardSelectionCreated = (e) => {
8928
+ var _a;
8929
+ (_a = this.eventBus) == null ? void 0 : _a.emit("selection:created", e);
8930
+ };
8931
+ this.forwardSelectionUpdated = (e) => {
8932
+ var _a;
8933
+ (_a = this.eventBus) == null ? void 0 : _a.emit("selection:updated", e);
8934
+ };
8935
+ this.forwardSelectionCleared = (e) => {
8936
+ var _a;
8937
+ (_a = this.eventBus) == null ? void 0 : _a.emit("selection:cleared", e);
8938
+ };
8939
+ this.forwardObjectModified = (e) => {
8940
+ var _a;
8941
+ (_a = this.eventBus) == null ? void 0 : _a.emit("object:modified", e);
8942
+ };
8943
+ this.forwardObjectAdded = (e) => {
8944
+ var _a;
8945
+ (_a = this.eventBus) == null ? void 0 : _a.emit("object:added", e);
8946
+ };
8947
+ this.forwardObjectRemoved = (e) => {
8948
+ var _a;
8949
+ (_a = this.eventBus) == null ? void 0 : _a.emit("object:removed", e);
8950
+ };
8951
+ this.onToolActivated = () => {
8952
+ this.applyManagedPassVisibility();
8953
+ };
8954
+ this.onToolSessionChanged = () => {
8955
+ this.applyManagedPassVisibility();
8956
+ };
8957
+ this.onCanvasObjectChanged = () => {
8958
+ if (this.producerApplyInProgress) return;
8959
+ this.scheduleManagedPassVisibilityRefresh();
8960
+ };
8758
8961
  if (el instanceof import_fabric5.Canvas) {
8759
8962
  this.canvas = el;
8760
8963
  } else {
@@ -8771,25 +8974,52 @@ var CanvasService = class {
8771
8974
  this.setEventBus(options.eventBus);
8772
8975
  }
8773
8976
  }
8977
+ init(context) {
8978
+ if (this.context) {
8979
+ this.detachContextEvents(this.context.eventBus);
8980
+ }
8981
+ this.context = context;
8982
+ this.workbenchService = context.get(import_core11.WORKBENCH_SERVICE);
8983
+ this.toolSessionService = context.get(import_core11.TOOL_SESSION_SERVICE);
8984
+ this.setEventBus(context.eventBus);
8985
+ this.attachContextEvents(context.eventBus);
8986
+ }
8987
+ attachContextEvents(eventBus) {
8988
+ eventBus.on("tool:activated", this.onToolActivated);
8989
+ eventBus.on("tool:session:change", this.onToolSessionChanged);
8990
+ eventBus.on("object:added", this.onCanvasObjectChanged);
8991
+ eventBus.on("object:removed", this.onCanvasObjectChanged);
8992
+ }
8993
+ detachContextEvents(eventBus) {
8994
+ eventBus.off("tool:activated", this.onToolActivated);
8995
+ eventBus.off("tool:session:change", this.onToolSessionChanged);
8996
+ eventBus.off("object:added", this.onCanvasObjectChanged);
8997
+ eventBus.off("object:removed", this.onCanvasObjectChanged);
8998
+ }
8774
8999
  setEventBus(eventBus) {
8775
9000
  this.eventBus = eventBus;
8776
9001
  this.setupEvents();
8777
9002
  }
8778
9003
  setupEvents() {
8779
- if (!this.eventBus) return;
8780
- const bus = this.eventBus;
8781
- const forward = (name) => (e) => bus.emit(name, e);
8782
- this.canvas.on("selection:created", forward("selection:created"));
8783
- this.canvas.on("selection:updated", forward("selection:updated"));
8784
- this.canvas.on("selection:cleared", forward("selection:cleared"));
8785
- this.canvas.on("object:modified", forward("object:modified"));
8786
- this.canvas.on("object:added", forward("object:added"));
8787
- this.canvas.on("object:removed", forward("object:removed"));
9004
+ if (this.canvasForwardersBound) return;
9005
+ this.canvas.on("selection:created", this.forwardSelectionCreated);
9006
+ this.canvas.on("selection:updated", this.forwardSelectionUpdated);
9007
+ this.canvas.on("selection:cleared", this.forwardSelectionCleared);
9008
+ this.canvas.on("object:modified", this.forwardObjectModified);
9009
+ this.canvas.on("object:added", this.forwardObjectAdded);
9010
+ this.canvas.on("object:removed", this.forwardObjectRemoved);
9011
+ this.canvasForwardersBound = true;
8788
9012
  }
8789
9013
  dispose() {
9014
+ if (this.context) {
9015
+ this.detachContextEvents(this.context.eventBus);
9016
+ }
8790
9017
  this.renderProducers.clear();
8791
- this.managedProducerLayerIds.clear();
8792
- this.managedProducerRootLayerIds.clear();
9018
+ this.managedProducerPassIds.clear();
9019
+ this.managedPassMetas.clear();
9020
+ this.context = void 0;
9021
+ this.workbenchService = void 0;
9022
+ this.toolSessionService = void 0;
8793
9023
  this.producerFlushRequested = false;
8794
9024
  this.canvas.dispose();
8795
9025
  }
@@ -8867,118 +9097,274 @@ var CanvasService = class {
8867
9097
  return a.toolId.localeCompare(b.toolId);
8868
9098
  });
8869
9099
  }
8870
- appendLayerSpecMap(map, source) {
8871
- if (!source) return;
8872
- Object.entries(source).forEach(([layerId, specs]) => {
8873
- if (!Array.isArray(specs)) return;
8874
- const list = map.get(layerId) || [];
8875
- list.push(...specs);
8876
- map.set(layerId, list);
9100
+ normalizePassSpecValue(spec) {
9101
+ const id = String(spec.id || "").trim();
9102
+ if (!id) return null;
9103
+ return {
9104
+ id,
9105
+ stack: Number.isFinite(spec.stack) ? Number(spec.stack) : 0,
9106
+ order: Number.isFinite(spec.order) ? Number(spec.order) : 0,
9107
+ replace: spec.replace !== false,
9108
+ visibility: spec.visibility,
9109
+ effects: Array.isArray(spec.effects) ? [...spec.effects] : [],
9110
+ objects: Array.isArray(spec.objects) ? [...spec.objects] : []
9111
+ };
9112
+ }
9113
+ normalizeClipPathEffectSpec(effect, passId, index) {
9114
+ if (!effect || effect.type !== "clipPath") return null;
9115
+ const source = effect.source;
9116
+ if (!source || typeof source !== "object") return null;
9117
+ const sourceId = String(source.id || "").trim();
9118
+ if (!sourceId) return null;
9119
+ const targetPassIds = Array.isArray(effect.targetPassIds) ? effect.targetPassIds.map((item) => String(item || "").trim()).filter((item) => item.length > 0) : [];
9120
+ if (!targetPassIds.length) return null;
9121
+ const customId = String(effect.id || "").trim();
9122
+ const key = customId || `${passId}.effect.clipPath.${index}`;
9123
+ return {
9124
+ type: "clipPath",
9125
+ key,
9126
+ source: {
9127
+ ...source,
9128
+ id: sourceId
9129
+ },
9130
+ targetPassIds
9131
+ };
9132
+ }
9133
+ mergePassSpec(map, rawSpec, producerId) {
9134
+ const normalized = this.normalizePassSpecValue(rawSpec);
9135
+ if (!normalized) return;
9136
+ const existing = map.get(normalized.id);
9137
+ if (!existing) {
9138
+ map.set(normalized.id, normalized);
9139
+ return;
9140
+ }
9141
+ existing.objects.push(...normalized.objects);
9142
+ existing.replace = existing.replace || normalized.replace;
9143
+ existing.stack = normalized.stack;
9144
+ existing.order = normalized.order;
9145
+ if (normalized.visibility !== void 0) {
9146
+ existing.visibility = normalized.visibility;
9147
+ }
9148
+ existing.effects.push(...normalized.effects);
9149
+ if (normalized.objects.length === 0 && normalized.effects.length === 0) {
9150
+ console.debug(
9151
+ `[CanvasService] pass "${normalized.id}" from producer "${producerId}" updated ordering/visibility only.`
9152
+ );
9153
+ }
9154
+ }
9155
+ comparePassMeta(a, b) {
9156
+ if (a.stack !== b.stack) return a.stack - b.stack;
9157
+ if (a.order !== b.order) return a.order - b.order;
9158
+ return a.id.localeCompare(b.id);
9159
+ }
9160
+ getPassObjectOrder(obj) {
9161
+ var _a;
9162
+ const raw = Number((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.passOrder);
9163
+ return Number.isFinite(raw) ? raw : Number.MAX_SAFE_INTEGER;
9164
+ }
9165
+ getPassCanvasObjects(passId) {
9166
+ const all = this.canvas.getObjects();
9167
+ return all.filter((obj) => {
9168
+ var _a;
9169
+ return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.passId) === passId;
9170
+ }).sort((a, b) => {
9171
+ const orderA = this.getPassObjectOrder(a);
9172
+ const orderB = this.getPassObjectOrder(b);
9173
+ if (orderA !== orderB) return orderA - orderB;
9174
+ return all.indexOf(a) - all.indexOf(b);
9175
+ });
9176
+ }
9177
+ getPassObjects(passId) {
9178
+ return this.getPassCanvasObjects(passId);
9179
+ }
9180
+ getRootLayerObjects(layerId) {
9181
+ return this.getPassCanvasObjects(layerId);
9182
+ }
9183
+ isManagedPassObject(obj) {
9184
+ var _a;
9185
+ const passId = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.passId;
9186
+ return typeof passId === "string" && this.managedPassMetas.has(passId);
9187
+ }
9188
+ syncManagedPassStacking(passes) {
9189
+ const orderedPasses = [...passes].sort((a, b) => this.comparePassMeta(a, b));
9190
+ if (!orderedPasses.length) return;
9191
+ const canvasObjects = this.canvas.getObjects();
9192
+ const managedObjects = canvasObjects.filter(
9193
+ (obj) => this.isManagedPassObject(obj)
9194
+ );
9195
+ if (!managedObjects.length) return;
9196
+ const firstManagedIndex = managedObjects.map((obj) => canvasObjects.indexOf(obj)).filter((index) => index >= 0).reduce((min, value) => Math.min(min, value), Number.MAX_SAFE_INTEGER);
9197
+ let targetIndex = Number.isFinite(firstManagedIndex) ? firstManagedIndex : 0;
9198
+ orderedPasses.forEach((meta) => {
9199
+ const objects = this.getPassCanvasObjects(meta.id);
9200
+ objects.forEach((obj) => {
9201
+ this.moveObjectInCanvas(obj, targetIndex);
9202
+ targetIndex += 1;
9203
+ });
9204
+ });
9205
+ }
9206
+ getPassRuntimeState() {
9207
+ const state = /* @__PURE__ */ new Map();
9208
+ const ensure = (passId) => {
9209
+ const id = String(passId || "").trim();
9210
+ if (!id) return { exists: false, objectCount: 0 };
9211
+ let item = state.get(id);
9212
+ if (!item) {
9213
+ item = { exists: false, objectCount: 0 };
9214
+ state.set(id, item);
9215
+ }
9216
+ return item;
9217
+ };
9218
+ this.canvas.getObjects().forEach((obj) => {
9219
+ var _a;
9220
+ const passId = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.passId;
9221
+ if (typeof passId === "string") {
9222
+ const item = ensure(passId);
9223
+ item.exists = true;
9224
+ item.objectCount += 1;
9225
+ }
9226
+ });
9227
+ this.managedPassMetas.forEach((meta) => {
9228
+ const item = ensure(meta.id);
9229
+ item.exists = true;
9230
+ });
9231
+ return state;
9232
+ }
9233
+ applyManagedPassVisibility(options = {}) {
9234
+ var _a, _b;
9235
+ if (!this.managedPassMetas.size) return false;
9236
+ const layers = this.getPassRuntimeState();
9237
+ const activeToolId = (_b = (_a = this.workbenchService) == null ? void 0 : _a.activeToolId) != null ? _b : null;
9238
+ const isSessionActive = (toolId) => {
9239
+ if (!this.toolSessionService) return false;
9240
+ return this.toolSessionService.getState(toolId).status === "active";
9241
+ };
9242
+ let changed = false;
9243
+ this.managedPassMetas.forEach((meta) => {
9244
+ const visible = evaluateVisibilityExpr(meta.visibility, {
9245
+ activeToolId,
9246
+ isSessionActive,
9247
+ layers
9248
+ });
9249
+ changed = this.setPassVisibility(meta.id, visible) || changed;
9250
+ });
9251
+ if (changed && options.render !== false) {
9252
+ this.requestRenderAll();
9253
+ }
9254
+ return changed;
9255
+ }
9256
+ scheduleManagedPassVisibilityRefresh() {
9257
+ if (this.visibilityRefreshScheduled) return;
9258
+ this.visibilityRefreshScheduled = true;
9259
+ void Promise.resolve().then(() => {
9260
+ this.visibilityRefreshScheduled = false;
9261
+ this.applyManagedPassVisibility();
8877
9262
  });
8878
9263
  }
8879
9264
  async collectAndApplyProducerSpecs() {
8880
- const groupLayerSpecs = /* @__PURE__ */ new Map();
8881
- const rootLayerSpecs = /* @__PURE__ */ new Map();
8882
- const replaceLayerIds = /* @__PURE__ */ new Set();
8883
- const replaceRootLayerIds = /* @__PURE__ */ new Set();
9265
+ const passes = /* @__PURE__ */ new Map();
8884
9266
  const entries = this.sortedRenderProducerEntries();
8885
- for (const entry of entries) {
8886
- try {
8887
- const result = await entry.producer();
8888
- if (!result) continue;
8889
- this.appendLayerSpecMap(groupLayerSpecs, result.layerSpecs);
8890
- this.appendLayerSpecMap(rootLayerSpecs, result.rootLayerSpecs);
8891
- if (Array.isArray(result.replaceLayerIds)) {
8892
- result.replaceLayerIds.forEach((layerId) => {
8893
- if (layerId) replaceLayerIds.add(layerId);
8894
- });
8895
- }
8896
- if (Array.isArray(result.replaceRootLayerIds)) {
8897
- result.replaceRootLayerIds.forEach((layerId) => {
8898
- if (layerId) replaceRootLayerIds.add(layerId);
8899
- });
9267
+ this.producerApplyInProgress = true;
9268
+ try {
9269
+ for (const entry of entries) {
9270
+ try {
9271
+ const result = await entry.producer();
9272
+ if (!result) continue;
9273
+ const specs = Array.isArray(result.passes) ? result.passes : [];
9274
+ specs.forEach((spec) => this.mergePassSpec(passes, spec, entry.toolId));
9275
+ } catch (error) {
9276
+ console.error(
9277
+ `[CanvasService] render producer "${entry.toolId}" failed.`,
9278
+ error
9279
+ );
8900
9280
  }
8901
- } catch (error) {
8902
- console.error(
8903
- `[CanvasService] render producer "${entry.toolId}" failed.`,
8904
- error
8905
- );
8906
9281
  }
8907
- }
8908
- const nextLayerIds = new Set(groupLayerSpecs.keys());
8909
- const nextRootLayerIds = new Set(rootLayerSpecs.keys());
8910
- for (const [layerId, specs] of groupLayerSpecs.entries()) {
8911
- if (replaceLayerIds.has(layerId)) {
8912
- const layer = this.getLayer(layerId);
8913
- if (layer) {
8914
- layer.getObjects().forEach((obj) => layer.remove(obj));
8915
- }
9282
+ const nextPassIds = /* @__PURE__ */ new Set();
9283
+ const nextManagedPassMetas = /* @__PURE__ */ new Map();
9284
+ const nextEffects = [];
9285
+ for (const pass of passes.values()) {
9286
+ nextPassIds.add(pass.id);
9287
+ nextManagedPassMetas.set(pass.id, {
9288
+ id: pass.id,
9289
+ stack: pass.stack,
9290
+ order: pass.order,
9291
+ visibility: pass.visibility
9292
+ });
9293
+ await this.applyObjectSpecsToPass(pass.id, pass.objects, {
9294
+ render: false,
9295
+ replace: pass.replace
9296
+ });
9297
+ pass.effects.forEach((effect, index) => {
9298
+ const normalized = this.normalizeClipPathEffectSpec(
9299
+ effect,
9300
+ pass.id,
9301
+ index
9302
+ );
9303
+ if (!normalized) return;
9304
+ nextEffects.push(normalized);
9305
+ });
8916
9306
  }
8917
- await this.applyObjectSpecsToLayer(layerId, specs, { render: false });
8918
- }
8919
- for (const layerId of this.managedProducerLayerIds) {
8920
- if (nextLayerIds.has(layerId)) continue;
8921
- const layer = this.getLayer(layerId);
8922
- if (!layer) continue;
8923
- await this.applyObjectSpecsToContainer(layer, [], { render: false });
8924
- }
8925
- for (const [layerId, specs] of rootLayerSpecs.entries()) {
8926
- if (replaceRootLayerIds.has(layerId)) {
8927
- const existing = this.getRootLayerObjects(layerId);
8928
- existing.forEach((obj) => this.canvas.remove(obj));
9307
+ for (const passId of this.managedProducerPassIds) {
9308
+ if (nextPassIds.has(passId)) continue;
9309
+ await this.applyObjectSpecsToPass(passId, [], {
9310
+ render: false,
9311
+ replace: true
9312
+ });
8929
9313
  }
8930
- await this.applyObjectSpecsToRootLayer(layerId, specs, { render: false });
8931
- }
8932
- for (const layerId of this.managedProducerRootLayerIds) {
8933
- if (nextRootLayerIds.has(layerId)) continue;
8934
- await this.applyObjectSpecsToRootLayer(layerId, [], { render: false });
9314
+ this.managedProducerPassIds = nextPassIds;
9315
+ this.managedPassMetas = nextManagedPassMetas;
9316
+ this.syncManagedPassStacking(Array.from(nextManagedPassMetas.values()));
9317
+ await this.applyManagedPassEffects(nextEffects);
9318
+ this.applyManagedPassVisibility({ render: false });
9319
+ } finally {
9320
+ this.producerApplyInProgress = false;
8935
9321
  }
8936
- this.managedProducerLayerIds = nextLayerIds;
8937
- this.managedProducerRootLayerIds = nextRootLayerIds;
8938
9322
  this.requestRenderAll();
8939
9323
  }
8940
- /**
8941
- * Get a layer (Group) by its ID.
8942
- * We assume layers are Groups directly on the canvas with a data.id property.
8943
- */
8944
- getLayer(id) {
8945
- return this.canvas.getObjects().find((obj) => {
8946
- var _a;
8947
- return ((_a = obj.data) == null ? void 0 : _a.id) === id;
8948
- });
8949
- }
8950
- /**
8951
- * Create a layer (Group) with the given ID if it doesn't exist.
8952
- */
8953
- createLayer(id, options = {}) {
8954
- let layer = this.getLayer(id);
8955
- if (!layer) {
8956
- const defaultOptions = {
8957
- selectable: false,
8958
- evented: false,
8959
- ...options,
8960
- data: { ...options.data, id }
8961
- };
8962
- layer = new import_fabric5.Group([], defaultOptions);
8963
- this.canvas.add(layer);
8964
- }
8965
- return layer;
8966
- }
8967
- /**
8968
- * Find an object by ID, optionally within a specific layer.
8969
- */
8970
- getObject(id, layerId) {
8971
- if (layerId) {
8972
- const layer = this.getLayer(layerId);
8973
- if (!layer) return void 0;
8974
- return layer.getObjects().find((obj) => {
8975
- var _a;
8976
- return ((_a = obj.data) == null ? void 0 : _a.id) === id;
9324
+ async applyManagedPassEffects(effects) {
9325
+ const effectTargetMap = /* @__PURE__ */ new Map();
9326
+ for (const effect of effects) {
9327
+ if (effect.type !== "clipPath") continue;
9328
+ effect.targetPassIds.forEach((targetPassId) => {
9329
+ this.getPassCanvasObjects(targetPassId).forEach((obj) => {
9330
+ effectTargetMap.set(obj, effect);
9331
+ });
8977
9332
  });
8978
9333
  }
9334
+ const managedObjects = this.canvas.getObjects().filter(
9335
+ (obj) => this.isManagedPassObject(obj)
9336
+ );
9337
+ const effectTemplateCache = /* @__PURE__ */ new Map();
9338
+ for (const obj of managedObjects) {
9339
+ const targetEffect = effectTargetMap.get(obj);
9340
+ if (!targetEffect) {
9341
+ this.clearClipPathEffectFromObject(obj);
9342
+ continue;
9343
+ }
9344
+ let template = effectTemplateCache.get(targetEffect.key);
9345
+ if (template === void 0) {
9346
+ template = await this.createClipPathTemplate(targetEffect);
9347
+ effectTemplateCache.set(targetEffect.key, template);
9348
+ }
9349
+ if (!template) {
9350
+ this.clearClipPathEffectFromObject(obj);
9351
+ continue;
9352
+ }
9353
+ await this.applyClipPathEffectToObject(
9354
+ obj,
9355
+ template,
9356
+ targetEffect.key
9357
+ );
9358
+ }
9359
+ }
9360
+ getObject(id, passId) {
9361
+ const normalizedId = String(id || "").trim();
9362
+ if (!normalizedId) return void 0;
8979
9363
  return this.canvas.getObjects().find((obj) => {
8980
- var _a;
8981
- return ((_a = obj.data) == null ? void 0 : _a.id) === id;
9364
+ var _a, _b;
9365
+ if (((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id) !== normalizedId) return false;
9366
+ if (!passId) return true;
9367
+ return ((_b = obj == null ? void 0 : obj.data) == null ? void 0 : _b.passId) === passId;
8982
9368
  });
8983
9369
  }
8984
9370
  requestRenderAll() {
@@ -9164,114 +9550,187 @@ var CanvasService = class {
9164
9550
  next.top = objectTop + objectHeight * this.originFactor(originY);
9165
9551
  return next;
9166
9552
  }
9167
- setLayerVisibility(layerId, visible) {
9168
- const layer = this.getLayer(layerId);
9169
- if (layer) {
9170
- layer.set({ visible });
9171
- }
9172
- const objects = this.getRootLayerObjects(layerId);
9553
+ setPassVisibility(passId, visible) {
9554
+ const objects = this.getPassCanvasObjects(passId);
9555
+ let changed = false;
9173
9556
  objects.forEach((obj) => {
9174
9557
  var _a, _b;
9558
+ if (obj.visible === visible) return;
9175
9559
  (_a = obj.set) == null ? void 0 : _a.call(obj, { visible });
9176
9560
  (_b = obj.setCoords) == null ? void 0 : _b.call(obj);
9561
+ changed = true;
9177
9562
  });
9563
+ return changed;
9178
9564
  }
9179
- bringLayerToFront(layerId) {
9180
- const layer = this.getLayer(layerId);
9181
- if (layer) {
9182
- this.canvas.bringObjectToFront(layer);
9183
- }
9184
- const objects = this.getRootLayerObjects(layerId);
9185
- objects.forEach((obj) => this.canvas.bringObjectToFront(obj));
9565
+ setLayerVisibility(layerId, visible) {
9566
+ return this.setPassVisibility(layerId, visible);
9186
9567
  }
9187
- async applyLayerSpec(spec) {
9188
- const layer = this.createLayer(spec.id, spec.props || {});
9189
- await this.applyObjectSpecsToContainer(layer, spec.objects);
9568
+ bringPassToFront(passId) {
9569
+ const objects = this.getPassCanvasObjects(passId);
9570
+ objects.forEach((obj) => this.canvas.bringObjectToFront(obj));
9190
9571
  }
9191
- async applyObjectSpecsToLayer(layerId, objects, options = {}) {
9192
- const layer = this.createLayer(layerId, {});
9193
- await this.applyObjectSpecsToContainer(layer, objects, options);
9572
+ bringLayerToFront(layerId) {
9573
+ this.bringPassToFront(layerId);
9194
9574
  }
9195
- getRootLayerObjects(layerId) {
9196
- return this.canvas.getObjects().filter((obj) => {
9197
- var _a;
9198
- return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === layerId;
9575
+ async applyPassSpec(spec, options = {}) {
9576
+ await this.applyObjectSpecsToPass(spec.id, spec.objects, {
9577
+ render: options.render,
9578
+ replace: spec.replace !== false
9199
9579
  });
9200
9580
  }
9201
- async applyObjectSpecsToRootLayer(layerId, specs, options = {}) {
9202
- const desiredIds = new Set(specs.map((s) => s.id));
9203
- const existing = this.getRootLayerObjects(layerId);
9204
- existing.forEach((obj) => {
9205
- var _a;
9206
- const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
9207
- if (typeof id === "string" && !desiredIds.has(id)) {
9208
- this.canvas.remove(obj);
9209
- }
9581
+ async applyObjectSpecsToRootLayer(passId, specs, options = {}) {
9582
+ await this.applyObjectSpecsToPass(passId, specs, {
9583
+ render: options.render,
9584
+ replace: true
9210
9585
  });
9211
- const byId = /* @__PURE__ */ new Map();
9212
- this.getRootLayerObjects(layerId).forEach((obj) => {
9213
- var _a;
9214
- const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
9215
- if (typeof id === "string") byId.set(id, obj);
9586
+ }
9587
+ normalizeObjectSpecs(specs) {
9588
+ const seen = /* @__PURE__ */ new Set();
9589
+ const normalized = [];
9590
+ (specs || []).forEach((spec) => {
9591
+ const id = String((spec == null ? void 0 : spec.id) || "").trim();
9592
+ if (!id || seen.has(id)) return;
9593
+ seen.add(id);
9594
+ normalized.push({
9595
+ ...spec,
9596
+ id
9597
+ });
9216
9598
  });
9217
- for (let index = 0; index < specs.length; index += 1) {
9218
- const spec = specs[index];
9219
- let current = byId.get(spec.id);
9220
- if (current && spec.type === "image" && spec.src && current.getSrc && current.getSrc() !== spec.src) {
9221
- this.canvas.remove(current);
9222
- byId.delete(spec.id);
9223
- current = void 0;
9224
- }
9225
- if (!current) {
9226
- const created = await this.createFabricObject(spec);
9227
- if (!created) continue;
9228
- this.patchFabricObject(created, spec, { layerId });
9229
- this.canvas.add(created);
9230
- byId.set(spec.id, created);
9231
- continue;
9232
- }
9233
- this.patchFabricObject(current, spec, { layerId });
9599
+ return normalized;
9600
+ }
9601
+ async cloneFabricObject(source) {
9602
+ const clone = source.clone;
9603
+ if (typeof clone !== "function") return void 0;
9604
+ const result = clone.call(source);
9605
+ if (!result || typeof result.then !== "function") {
9606
+ return void 0;
9234
9607
  }
9235
- if (options.render !== false) {
9236
- this.requestRenderAll();
9608
+ try {
9609
+ const copied = await result;
9610
+ return copied;
9611
+ } catch (e) {
9612
+ return void 0;
9237
9613
  }
9238
9614
  }
9239
- async applyObjectSpecsToContainer(container, specs, options = {}) {
9240
- const desiredIds = new Set(specs.map((s) => s.id));
9241
- const existing = container.getObjects();
9242
- existing.forEach((obj) => {
9243
- var _a;
9244
- const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
9245
- if (typeof id === "string" && !desiredIds.has(id)) {
9246
- container.remove(obj);
9615
+ async createClipPathTemplate(effect) {
9616
+ var _a, _b;
9617
+ const source = effect.source;
9618
+ const sourceId = String(source.id || "").trim();
9619
+ if (!sourceId) return null;
9620
+ const template = await this.createFabricObject({
9621
+ ...source,
9622
+ id: sourceId,
9623
+ data: {
9624
+ ...source.data || {},
9625
+ id: sourceId,
9626
+ type: "clip-path-effect-template",
9627
+ effectKey: effect.key
9628
+ },
9629
+ props: {
9630
+ ...source.props || {},
9631
+ selectable: false,
9632
+ evented: false,
9633
+ excludeFromExport: true
9247
9634
  }
9248
9635
  });
9636
+ if (!template) return null;
9637
+ (_a = template.set) == null ? void 0 : _a.call(template, {
9638
+ selectable: false,
9639
+ evented: false,
9640
+ excludeFromExport: true,
9641
+ absolutePositioned: true
9642
+ });
9643
+ (_b = template.setCoords) == null ? void 0 : _b.call(template);
9644
+ return template;
9645
+ }
9646
+ isClipPathEffectManaged(target) {
9647
+ return typeof (target == null ? void 0 : target.__pooderEffectClipKey) === "string";
9648
+ }
9649
+ clearClipPathEffectFromObject(target) {
9650
+ var _a, _b;
9651
+ if (!target) return;
9652
+ if (!this.isClipPathEffectManaged(target)) return;
9653
+ (_a = target.set) == null ? void 0 : _a.call(target, { clipPath: void 0 });
9654
+ (_b = target.setCoords) == null ? void 0 : _b.call(target);
9655
+ delete target.__pooderEffectClipKey;
9656
+ }
9657
+ async applyClipPathEffectToObject(target, clipTemplate, effectKey) {
9658
+ var _a, _b, _c, _d;
9659
+ if (!target) return;
9660
+ const clipPath = await this.cloneFabricObject(clipTemplate);
9661
+ if (!clipPath) {
9662
+ this.clearClipPathEffectFromObject(target);
9663
+ return;
9664
+ }
9665
+ (_a = clipPath.set) == null ? void 0 : _a.call(clipPath, {
9666
+ selectable: false,
9667
+ evented: false,
9668
+ excludeFromExport: true,
9669
+ absolutePositioned: true
9670
+ });
9671
+ (_b = clipPath.setCoords) == null ? void 0 : _b.call(clipPath);
9672
+ (_c = target.set) == null ? void 0 : _c.call(target, { clipPath });
9673
+ (_d = target.setCoords) == null ? void 0 : _d.call(target);
9674
+ target.__pooderEffectClipKey = effectKey;
9675
+ }
9676
+ async applyObjectSpecsToPass(passId, specs, options = {}) {
9677
+ const normalizedPassId = String(passId || "").trim();
9678
+ if (!normalizedPassId) return;
9679
+ const replace = options.replace !== false;
9680
+ const normalizedSpecs = this.normalizeObjectSpecs(specs);
9681
+ const desiredIds = new Set(normalizedSpecs.map((s) => s.id));
9682
+ const existing = this.getPassCanvasObjects(normalizedPassId);
9683
+ if (replace) {
9684
+ existing.forEach((obj) => {
9685
+ var _a;
9686
+ const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
9687
+ if (typeof id === "string" && !desiredIds.has(id)) {
9688
+ this.canvas.remove(obj);
9689
+ }
9690
+ });
9691
+ }
9249
9692
  const byId = /* @__PURE__ */ new Map();
9250
- container.getObjects().forEach((obj) => {
9693
+ this.getPassCanvasObjects(normalizedPassId).forEach((obj) => {
9251
9694
  var _a;
9252
9695
  const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
9253
9696
  if (typeof id === "string") byId.set(id, obj);
9254
9697
  });
9255
- for (let index = 0; index < specs.length; index += 1) {
9256
- const spec = specs[index];
9698
+ for (let index = 0; index < normalizedSpecs.length; index += 1) {
9699
+ const spec = normalizedSpecs[index];
9257
9700
  let current = byId.get(spec.id);
9258
- if (current && spec.type === "image" && spec.src && current.getSrc && current.getSrc() !== spec.src) {
9259
- container.remove(current);
9701
+ if (spec.type === "path") {
9702
+ const nextPathData = this.readPathDataFromSpec(spec);
9703
+ if (!nextPathData || !nextPathData.trim()) {
9704
+ if (current) {
9705
+ this.canvas.remove(current);
9706
+ byId.delete(spec.id);
9707
+ }
9708
+ continue;
9709
+ }
9710
+ }
9711
+ if (current && this.shouldRecreateObject(current, spec)) {
9712
+ this.canvas.remove(current);
9260
9713
  byId.delete(spec.id);
9261
9714
  current = void 0;
9262
9715
  }
9263
9716
  if (!current) {
9264
9717
  const created = await this.createFabricObject(spec);
9265
9718
  if (!created) continue;
9266
- container.add(created);
9267
- current = created;
9268
- byId.set(spec.id, current);
9269
- } else {
9270
- this.patchFabricObject(current, spec);
9719
+ this.patchFabricObject(created, spec, {
9720
+ passId: normalizedPassId,
9721
+ layerId: normalizedPassId,
9722
+ passOrder: index
9723
+ });
9724
+ this.canvas.add(created);
9725
+ byId.set(spec.id, created);
9726
+ continue;
9271
9727
  }
9272
- this.moveObjectInContainer(container, current, index);
9728
+ this.patchFabricObject(current, spec, {
9729
+ passId: normalizedPassId,
9730
+ layerId: normalizedPassId,
9731
+ passOrder: index
9732
+ });
9273
9733
  }
9274
- container.dirty = true;
9275
9734
  if (options.render !== false) {
9276
9735
  this.requestRenderAll();
9277
9736
  }
@@ -9283,10 +9742,56 @@ var CanvasService = class {
9283
9742
  ...extraData || {},
9284
9743
  id: spec.id
9285
9744
  };
9745
+ nextData.__renderSourceKey = this.getSpecRenderSourceKey(spec);
9286
9746
  const props = this.resolveFabricProps(spec, spec.props || {});
9287
9747
  obj.set({ ...props, data: nextData });
9288
9748
  obj.setCoords();
9289
9749
  }
9750
+ readPathDataFromSpec(spec) {
9751
+ var _a, _b;
9752
+ if (spec.type !== "path") return void 0;
9753
+ const raw = ((_a = spec.props) == null ? void 0 : _a.path) || ((_b = spec.props) == null ? void 0 : _b.pathData);
9754
+ if (typeof raw !== "string") return void 0;
9755
+ return raw;
9756
+ }
9757
+ hashText(value) {
9758
+ let hash = 2166136261;
9759
+ for (let i = 0; i < value.length; i += 1) {
9760
+ hash ^= value.charCodeAt(i);
9761
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
9762
+ }
9763
+ return (hash >>> 0).toString(16);
9764
+ }
9765
+ getSpecRenderSourceKey(spec) {
9766
+ var _a, _b;
9767
+ switch (spec.type) {
9768
+ case "path": {
9769
+ const pathData = this.readPathDataFromSpec(spec) || "";
9770
+ return `path:${this.hashText(pathData)}`;
9771
+ }
9772
+ case "image":
9773
+ return `image:${String(spec.src || "")}`;
9774
+ case "text":
9775
+ return `text:${String((_b = (_a = spec.props) == null ? void 0 : _a.text) != null ? _b : "")}`;
9776
+ case "rect":
9777
+ return "rect";
9778
+ default:
9779
+ return String(spec.type || "");
9780
+ }
9781
+ }
9782
+ shouldRecreateObject(current, spec) {
9783
+ var _a;
9784
+ if (!current) return true;
9785
+ const currentType = String((current == null ? void 0 : current.type) || "").toLowerCase();
9786
+ if (currentType !== spec.type) return true;
9787
+ const expectedKey = this.getSpecRenderSourceKey(spec);
9788
+ const currentKey = String(((_a = current == null ? void 0 : current.data) == null ? void 0 : _a.__renderSourceKey) || "");
9789
+ if (currentKey && expectedKey && currentKey !== expectedKey) return true;
9790
+ if (spec.type === "image" && spec.src && current.getSrc) {
9791
+ return current.getSrc() !== spec.src;
9792
+ }
9793
+ return false;
9794
+ }
9290
9795
  resolveFabricProps(spec, props) {
9291
9796
  const space = spec.space || "scene";
9292
9797
  const next = this.resolveLayoutProps(spec, props);
@@ -9310,26 +9815,26 @@ var CanvasService = class {
9310
9815
  next.scaleY = rawScaleY * sceneScale;
9311
9816
  return next;
9312
9817
  }
9313
- moveObjectInContainer(container, obj, index) {
9818
+ moveObjectInCanvas(obj, index) {
9314
9819
  if (!obj) return;
9315
- const moveObjectTo = container.moveObjectTo;
9820
+ const moveObjectTo = this.canvas.moveObjectTo;
9316
9821
  if (typeof moveObjectTo === "function") {
9317
- moveObjectTo.call(container, obj, index);
9822
+ moveObjectTo.call(this.canvas, obj, index);
9318
9823
  return;
9319
9824
  }
9320
- const list = container._objects;
9825
+ const list = this.canvas._objects;
9321
9826
  if (!Array.isArray(list)) return;
9322
9827
  const from = list.indexOf(obj);
9323
9828
  if (from < 0 || from === index) return;
9324
9829
  list.splice(from, 1);
9325
9830
  const target = Math.max(0, Math.min(index, list.length));
9326
9831
  list.splice(target, 0, obj);
9327
- if (typeof container._onStackOrderChanged === "function") {
9328
- container._onStackOrderChanged();
9832
+ if (typeof this.canvas._onStackOrderChanged === "function") {
9833
+ this.canvas._onStackOrderChanged();
9329
9834
  }
9330
9835
  }
9331
9836
  async createFabricObject(spec) {
9332
- var _a, _b, _c, _d;
9837
+ var _a, _b;
9333
9838
  if (spec.type === "rect") {
9334
9839
  const props = this.resolveFabricProps(spec, spec.props || {});
9335
9840
  const rect = new import_fabric5.Rect({
@@ -9340,7 +9845,7 @@ var CanvasService = class {
9340
9845
  return rect;
9341
9846
  }
9342
9847
  if (spec.type === "path") {
9343
- const pathData = ((_a = spec.props) == null ? void 0 : _a.path) || ((_b = spec.props) == null ? void 0 : _b.pathData);
9848
+ const pathData = this.readPathDataFromSpec(spec);
9344
9849
  if (!pathData) return void 0;
9345
9850
  const props = this.resolveFabricProps(spec, spec.props || {});
9346
9851
  const path = new import_fabric5.Path(pathData, {
@@ -9362,7 +9867,7 @@ var CanvasService = class {
9362
9867
  return image;
9363
9868
  }
9364
9869
  if (spec.type === "text") {
9365
- const content = String((_d = (_c = spec.props) == null ? void 0 : _c.text) != null ? _d : "");
9870
+ const content = String((_b = (_a = spec.props) == null ? void 0 : _a.text) != null ? _b : "");
9366
9871
  const props = this.resolveFabricProps(spec, spec.props || {});
9367
9872
  const text = new import_fabric5.Text(content, {
9368
9873
  ...props,
@@ -9385,8 +9890,8 @@ var CanvasService = class {
9385
9890
  MirrorTool,
9386
9891
  RulerTool,
9387
9892
  SceneLayoutService,
9388
- SceneVisibilityService,
9389
9893
  SizeTool,
9390
9894
  ViewportSystem,
9391
- WhiteInkTool
9895
+ WhiteInkTool,
9896
+ evaluateVisibilityExpr
9392
9897
  });