@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.mjs CHANGED
@@ -3,291 +3,94 @@ import {
3
3
  ContributionPointIds
4
4
  } from "@pooder/core";
5
5
  import { FabricImage } from "fabric";
6
- var BACKGROUND_LAYER_ID = "background";
7
- var BACKGROUND_RECT_ID = "background-color-rect";
8
- var BACKGROUND_IMAGE_ID = "background-image";
9
- var DEFAULT_WIDTH = 800;
10
- var DEFAULT_HEIGHT = 600;
11
- var BackgroundTool = class {
12
- constructor(options) {
13
- this.id = "pooder.kit.background";
14
- this.metadata = {
15
- name: "BackgroundTool"
16
- };
17
- this.color = "";
18
- this.url = "";
19
- this.specs = [];
20
- this.renderSeq = 0;
21
- this.renderImageUrl = "";
22
- this.sourceSizeBySrc = /* @__PURE__ */ new Map();
23
- this.pendingSizeBySrc = /* @__PURE__ */ new Map();
24
- this.onCanvasResized = () => {
25
- this.updateBackground();
26
- };
27
- if (options) {
28
- Object.assign(this, options);
6
+
7
+ // src/coordinate.ts
8
+ var Coordinate = class {
9
+ /**
10
+ * Calculate layout to fit content within container while preserving aspect ratio.
11
+ */
12
+ static calculateLayout(container, content, padding = 0) {
13
+ const availableWidth = Math.max(0, container.width - padding * 2);
14
+ const availableHeight = Math.max(0, container.height - padding * 2);
15
+ if (content.width === 0 || content.height === 0) {
16
+ return { scale: 1, offsetX: 0, offsetY: 0, width: 0, height: 0 };
29
17
  }
18
+ const scaleX = availableWidth / content.width;
19
+ const scaleY = availableHeight / content.height;
20
+ const scale = Math.min(scaleX, scaleY);
21
+ const width = content.width * scale;
22
+ const height = content.height * scale;
23
+ const offsetX = (container.width - width) / 2;
24
+ const offsetY = (container.height - height) / 2;
25
+ return { scale, offsetX, offsetY, width, height };
30
26
  }
31
- activate(context) {
32
- var _a;
33
- this.canvasService = context.services.get("CanvasService");
34
- if (!this.canvasService) {
35
- console.warn("CanvasService not found for BackgroundTool");
36
- return;
37
- }
38
- (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
39
- this.renderProducerDisposable = this.canvasService.registerRenderProducer(
40
- this.id,
41
- () => ({
42
- layerSpecs: {
43
- [BACKGROUND_LAYER_ID]: this.specs
44
- }
45
- }),
46
- { priority: 0 }
47
- );
48
- const configService = context.services.get(
49
- "ConfigurationService"
50
- );
51
- if (configService) {
52
- this.color = configService.get("background.color", this.color);
53
- this.url = configService.get("background.url", this.url);
54
- configService.onAnyChange((e) => {
55
- if (e.key.startsWith("background.")) {
56
- const prop = e.key.split(".")[1];
57
- console.log(
58
- `[BackgroundTool] Config change detected: ${e.key} -> ${e.value}, prop: ${prop}`
59
- );
60
- if (prop && prop in this) {
61
- console.log(
62
- `[BackgroundTool] Updating option ${prop} to ${e.value}`
63
- );
64
- this[prop] = e.value;
65
- this.updateBackground();
66
- } else {
67
- console.warn(
68
- `[BackgroundTool] Property ${prop} not found in options`
69
- );
70
- }
71
- }
72
- });
73
- }
74
- context.eventBus.on("canvas:resized", this.onCanvasResized);
75
- this.updateBackground();
27
+ /**
28
+ * Convert an absolute value to a normalized value (0-1).
29
+ * @param value Absolute value (e.g., pixels)
30
+ * @param total Total dimension size (e.g., canvas width)
31
+ */
32
+ static toNormalized(value, total) {
33
+ return total === 0 ? 0 : value / total;
76
34
  }
77
- deactivate(context) {
78
- var _a;
79
- context.eventBus.off("canvas:resized", this.onCanvasResized);
80
- this.renderSeq += 1;
81
- this.specs = [];
82
- this.renderImageUrl = "";
83
- (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
84
- this.renderProducerDisposable = void 0;
85
- if (!this.canvasService) return;
86
- const layer = this.canvasService.getLayer(BACKGROUND_LAYER_ID);
87
- if (layer) {
88
- this.canvasService.canvas.remove(layer);
89
- }
90
- void this.canvasService.flushRenderFromProducers();
91
- this.canvasService.requestRenderAll();
92
- this.canvasService = void 0;
35
+ /**
36
+ * Convert a normalized value (0-1) to an absolute value.
37
+ * @param normalized Normalized value (0-1)
38
+ * @param total Total dimension size (e.g., canvas width)
39
+ */
40
+ static toAbsolute(normalized, total) {
41
+ return normalized * total;
93
42
  }
94
- contribute() {
43
+ /**
44
+ * Normalize a point's coordinates.
45
+ */
46
+ static normalizePoint(point, size) {
95
47
  return {
96
- [ContributionPointIds.CONFIGURATIONS]: [
97
- {
98
- id: "background.color",
99
- type: "color",
100
- label: "Background Color",
101
- default: ""
102
- },
103
- {
104
- id: "background.url",
105
- type: "string",
106
- label: "Image URL",
107
- default: ""
108
- }
109
- ],
110
- [ContributionPointIds.COMMANDS]: [
111
- {
112
- command: "reset",
113
- title: "Reset Background",
114
- handler: () => {
115
- this.updateBackground();
116
- return true;
117
- }
118
- },
119
- {
120
- command: "clear",
121
- title: "Clear Background",
122
- handler: () => {
123
- this.color = "transparent";
124
- this.url = "";
125
- this.updateBackground();
126
- return true;
127
- }
128
- },
129
- {
130
- command: "setBackgroundColor",
131
- title: "Set Background Color",
132
- handler: (color) => {
133
- if (this.color === color) return true;
134
- this.color = color;
135
- this.updateBackground();
136
- return true;
137
- }
138
- },
139
- {
140
- command: "setBackgroundImage",
141
- title: "Set Background Image",
142
- handler: (url) => {
143
- if (this.url === url) return true;
144
- this.url = url;
145
- this.updateBackground();
146
- return true;
147
- }
148
- }
149
- ]
48
+ x: this.toNormalized(point.x, size.width),
49
+ y: this.toNormalized(point.y, size.height)
150
50
  };
151
51
  }
152
- getViewportSize() {
153
- var _a, _b;
154
- const width = Number(((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 0);
155
- const height = Number(((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 0);
52
+ /**
53
+ * Denormalize a point's coordinates to absolute pixels.
54
+ */
55
+ static denormalizePoint(point, size) {
156
56
  return {
157
- width: width > 0 ? width : DEFAULT_WIDTH,
158
- height: height > 0 ? height : DEFAULT_HEIGHT
57
+ x: this.toAbsolute(point.x, size.width),
58
+ y: this.toAbsolute(point.y, size.height)
159
59
  };
160
60
  }
161
- buildBackgroundSpecs(color, imageUrl) {
162
- const { width, height } = this.getViewportSize();
163
- const specs = [
164
- {
165
- id: BACKGROUND_RECT_ID,
166
- type: "rect",
167
- space: "screen",
168
- data: {
169
- id: BACKGROUND_RECT_ID,
170
- layerId: BACKGROUND_LAYER_ID,
171
- type: "background-color"
172
- },
173
- props: {
174
- left: 0,
175
- top: 0,
176
- width,
177
- height,
178
- originX: "left",
179
- originY: "top",
180
- fill: color,
181
- selectable: false,
182
- evented: false,
183
- excludeFromExport: true
184
- }
185
- }
186
- ];
187
- if (!imageUrl) {
188
- return specs;
189
- }
190
- const sourceSize = this.sourceSizeBySrc.get(imageUrl);
191
- const sourceWidth = Math.max(1, Number((sourceSize == null ? void 0 : sourceSize.width) || width));
192
- const sourceHeight = Math.max(1, Number((sourceSize == null ? void 0 : sourceSize.height) || height));
193
- const coverScale = Math.max(width / sourceWidth, height / sourceHeight);
194
- specs.push({
195
- id: BACKGROUND_IMAGE_ID,
196
- type: "image",
197
- src: imageUrl,
198
- space: "screen",
199
- data: {
200
- id: BACKGROUND_IMAGE_ID,
201
- layerId: BACKGROUND_LAYER_ID,
202
- type: "background-image"
203
- },
204
- props: {
205
- left: 0,
206
- top: 0,
207
- originX: "left",
208
- originY: "top",
209
- scaleX: coverScale,
210
- scaleY: coverScale,
211
- selectable: false,
212
- evented: false,
213
- excludeFromExport: true
214
- }
215
- });
216
- return specs;
217
- }
218
- async ensureImageSize(src) {
219
- if (!src) return null;
220
- const cached = this.sourceSizeBySrc.get(src);
221
- if (cached) return cached;
222
- const pending = this.pendingSizeBySrc.get(src);
223
- if (pending) {
224
- return pending;
225
- }
226
- const task = this.loadImageSize(src);
227
- this.pendingSizeBySrc.set(src, task);
228
- try {
229
- return await task;
230
- } finally {
231
- if (this.pendingSizeBySrc.get(src) === task) {
232
- this.pendingSizeBySrc.delete(src);
233
- }
234
- }
235
- }
236
- async loadImageSize(src) {
237
- try {
238
- const image = await FabricImage.fromURL(src, {
239
- crossOrigin: "anonymous"
240
- });
241
- const width = Number((image == null ? void 0 : image.width) || 0);
242
- const height = Number((image == null ? void 0 : image.height) || 0);
243
- if (width > 0 && height > 0) {
244
- const size = { width, height };
245
- this.sourceSizeBySrc.set(src, size);
246
- return size;
247
- }
248
- } catch (error) {
249
- console.error("[BackgroundTool] Failed to load image", src, error);
61
+ static convertUnit(value, from, to) {
62
+ if (from === to) return value;
63
+ const toMM = {
64
+ px: 0.264583,
65
+ // 1px = 0.264583mm (96 DPI)
66
+ mm: 1,
67
+ cm: 10,
68
+ in: 25.4
69
+ };
70
+ const mmValue = value * (from === "px" ? toMM.px : toMM[from] || 1);
71
+ if (to === "px") {
72
+ return mmValue / toMM.px;
250
73
  }
251
- return null;
74
+ return mmValue / (toMM[to] || 1);
252
75
  }
253
- updateBackground() {
254
- void this.updateBackgroundAsync();
76
+ };
77
+
78
+ // src/units.ts
79
+ function parseLengthToMm(input, defaultUnit) {
80
+ var _a, _b;
81
+ if (typeof input === "number") {
82
+ if (!Number.isFinite(input)) return 0;
83
+ return Coordinate.convertUnit(input, defaultUnit, "mm");
255
84
  }
256
- async updateBackgroundAsync() {
257
- if (!this.canvasService) return;
258
- const seq = ++this.renderSeq;
259
- const color = this.color;
260
- const nextUrl = String(this.url || "").trim();
261
- if (!nextUrl) {
262
- this.renderImageUrl = "";
263
- } else if (nextUrl !== this.renderImageUrl) {
264
- const loaded = await this.ensureImageSize(nextUrl);
265
- if (seq !== this.renderSeq) return;
266
- if (loaded) {
267
- this.renderImageUrl = nextUrl;
268
- }
269
- }
270
- this.specs = this.buildBackgroundSpecs(color, this.renderImageUrl);
271
- await this.canvasService.flushRenderFromProducers();
272
- if (seq !== this.renderSeq) return;
273
- const layer = this.canvasService.getLayer(BACKGROUND_LAYER_ID);
274
- if (layer) {
275
- this.canvasService.canvas.sendObjectToBack(layer);
276
- }
277
- this.canvasService.requestRenderAll();
278
- }
279
- };
280
-
281
- // src/extensions/image.ts
282
- import {
283
- ContributionPointIds as ContributionPointIds2
284
- } from "@pooder/core";
285
- import {
286
- Canvas as FabricCanvas,
287
- Image as FabricImage2,
288
- Pattern,
289
- Point
290
- } from "fabric";
85
+ const raw = input.trim();
86
+ if (!raw) return 0;
87
+ const match = raw.match(/^([+-]?\d+(?:\.\d+)?)\s*(px|mm|cm|in)?$/i);
88
+ if (!match) return 0;
89
+ const value = Number(match[1]);
90
+ if (!Number.isFinite(value)) return 0;
91
+ const unit = (_b = (_a = match[2]) == null ? void 0 : _a.toLowerCase()) != null ? _b : defaultUnit;
92
+ return Coordinate.convertUnit(value, unit, "mm");
93
+ }
291
94
 
292
95
  // src/extensions/dielineShape.ts
293
96
  var BUILTIN_DIELINE_SHAPES = [
@@ -304,7 +107,7 @@ var DEFAULT_HEART_SHAPE_PARAMS = {
304
107
  tipSharpness: 0
305
108
  };
306
109
  var DEFAULT_DIELINE_SHAPE_STYLE = {
307
- fitMode: "contain",
110
+ fitMode: "stretch",
308
111
  ...DEFAULT_HEART_SHAPE_PARAMS
309
112
  };
310
113
  function isDielineShape(value) {
@@ -354,962 +157,1393 @@ function getHeartShapeParams(style) {
354
157
  };
355
158
  }
356
159
 
357
- // src/extensions/geometry.ts
358
- import paper from "paper";
359
-
360
- // src/extensions/bridgeSelection.ts
361
- function pickExitIndex(hits) {
362
- for (let i = 0; i < hits.length; i++) {
363
- const h = hits[i];
364
- if (h.insideBelow && !h.insideAbove) return i;
365
- }
366
- return -1;
160
+ // src/extensions/sceneLayoutModel.ts
161
+ var DEFAULT_SIZE_STATE = {
162
+ unit: "mm",
163
+ actualWidthMm: 500,
164
+ actualHeightMm: 500,
165
+ constraintMode: "free",
166
+ aspectRatio: 1,
167
+ cutMode: "trim",
168
+ cutMarginMm: 0,
169
+ viewPadding: 140,
170
+ minMm: 10,
171
+ maxMm: 2e3,
172
+ stepMm: 0.1
173
+ };
174
+ function clamp(value, min, max) {
175
+ return Math.max(min, Math.min(max, value));
367
176
  }
368
- function scoreOutsideAbove(samples) {
369
- let score = 0;
370
- for (const s of samples) {
371
- if (s.outsideAbove) score++;
372
- }
373
- return score;
177
+ function roundToStep(value, step) {
178
+ if (!Number.isFinite(step) || step <= 0) return value;
179
+ return Math.round(value / step) * step;
374
180
  }
375
-
376
- // src/extensions/wrappedOffsets.ts
377
- function wrappedDistance(total, start, end) {
378
- if (!Number.isFinite(total) || total <= 0) return 0;
379
- if (!Number.isFinite(start) || !Number.isFinite(end)) return 0;
380
- const s = (start % total + total) % total;
381
- const e = (end % total + total) % total;
382
- return e >= s ? e - s : total - s + e;
181
+ function sanitizeMmValue(valueMm, limits) {
182
+ if (!Number.isFinite(valueMm)) return limits.minMm;
183
+ const rounded = roundToStep(valueMm, limits.stepMm);
184
+ return clamp(rounded, limits.minMm, limits.maxMm);
383
185
  }
384
- function sampleWrappedOffsets(total, start, end, count) {
385
- if (!Number.isFinite(total) || total <= 0) return [];
386
- if (!Number.isFinite(start) || !Number.isFinite(end)) return [];
387
- const n = Math.max(0, Math.floor(count));
388
- if (n <= 0) return [];
389
- const dist = wrappedDistance(total, start, end);
390
- if (n === 1) return [(start % total + total) % total];
391
- const step = dist / (n - 1);
392
- const offsets = [];
393
- for (let i = 0; i < n; i++) {
394
- const raw = start + step * i;
395
- const wrapped = (raw % total + total) % total;
396
- offsets.push(wrapped);
397
- }
398
- return offsets;
186
+ function normalizeUnit(value) {
187
+ if (value === "cm" || value === "in") return value;
188
+ return "mm";
399
189
  }
400
-
401
- // src/extensions/geometry.ts
402
- function resolveFeaturePosition(feature, geometry) {
403
- const { x, y, width, height } = geometry;
404
- const left = x - width / 2;
405
- const top = y - height / 2;
406
- return {
407
- x: left + feature.x * width,
408
- y: top + feature.y * height
409
- };
190
+ function normalizeConstraintMode(value) {
191
+ if (value === "lockAspect" || value === "equal") return value;
192
+ return "free";
410
193
  }
411
- function ensurePaper(width, height) {
412
- if (!paper.project) {
413
- paper.setup(new paper.Size(width, height));
414
- } else {
415
- paper.view.viewSize = new paper.Size(width, height);
416
- }
194
+ function normalizeCutMode(value) {
195
+ if (value === "outset" || value === "inset") return value;
196
+ return "trim";
417
197
  }
418
- var isBridgeDebugEnabled = () => Boolean(globalThis.__POODER_BRIDGE_DEBUG__);
419
- function normalizePathItem(shape) {
420
- let result = shape;
421
- if (typeof result.resolveCrossings === "function") result = result.resolveCrossings();
422
- if (typeof result.reduce === "function") result = result.reduce({});
423
- if (typeof result.reorient === "function") result = result.reorient(true, true);
424
- if (typeof result.reduce === "function") result = result.reduce({});
425
- return result;
198
+ function toMm(value, fromUnit) {
199
+ return Coordinate.convertUnit(value, fromUnit, "mm");
426
200
  }
427
- function getBridgeDelta(itemBounds, overlap) {
428
- return Math.max(overlap, Math.min(5, Math.max(1, itemBounds.height * 0.02)));
201
+ function fromMm(valueMm, toUnit) {
202
+ return Coordinate.convertUnit(valueMm, "mm", toUnit);
429
203
  }
430
- function getExitHit(args) {
431
- const { mainShape, x, bridgeBottom, toY, eps, delta, overlap, op } = args;
432
- const ray = new paper.Path.Line({
433
- from: [x, bridgeBottom],
434
- to: [x, toY],
435
- insert: false
436
- });
437
- const intersections = mainShape.getIntersections(ray) || [];
438
- ray.remove();
439
- const validHits = intersections.filter((i) => i.point.y < bridgeBottom - eps);
440
- if (validHits.length === 0) return null;
441
- validHits.sort((a, b) => b.point.y - a.point.y);
442
- const flags = validHits.map((h) => {
443
- const above = h.point.add(new paper.Point(0, -delta));
444
- const below = h.point.add(new paper.Point(0, delta));
445
- return {
446
- insideAbove: mainShape.contains(above),
447
- insideBelow: mainShape.contains(below)
448
- };
449
- });
450
- const idx = pickExitIndex(flags);
451
- if (idx < 0) return null;
452
- if (isBridgeDebugEnabled()) {
453
- console.debug("Geometry: Bridge ray", {
454
- x,
455
- validHits: validHits.length,
456
- idx,
457
- delta,
458
- overlap,
459
- op
460
- });
204
+ function resolvePaddingPx(raw, containerWidth, containerHeight) {
205
+ if (typeof raw === "number") return Math.max(0, raw);
206
+ if (typeof raw === "string") {
207
+ if (raw.endsWith("%")) {
208
+ const percent = parseFloat(raw) / 100;
209
+ if (!Number.isFinite(percent)) return 0;
210
+ return Math.max(0, Math.min(containerWidth, containerHeight) * percent);
211
+ }
212
+ const fixed = parseFloat(raw);
213
+ return Number.isFinite(fixed) ? Math.max(0, fixed) : 0;
461
214
  }
462
- const hit = validHits[idx];
463
- return { point: hit.point, location: hit };
215
+ return 0;
464
216
  }
465
- function selectOuterChain(args) {
466
- const { mainShape, pointsA, pointsB, delta, overlap, op } = args;
467
- const scoreA = scoreOutsideAbove(
468
- pointsA.map((p) => ({
469
- outsideAbove: !mainShape.contains(p.add(new paper.Point(0, -delta)))
470
- }))
217
+ function readSizeState(configService) {
218
+ const unit = normalizeUnit(
219
+ configService.get("size.unit", DEFAULT_SIZE_STATE.unit)
471
220
  );
472
- const scoreB = scoreOutsideAbove(
473
- pointsB.map((p) => ({
474
- outsideAbove: !mainShape.contains(p.add(new paper.Point(0, -delta)))
475
- }))
221
+ const minMm = Math.max(
222
+ 0.1,
223
+ Number(configService.get("size.minMm", DEFAULT_SIZE_STATE.minMm))
476
224
  );
477
- const ratioA = scoreA / pointsA.length;
478
- const ratioB = scoreB / pointsB.length;
479
- if (isBridgeDebugEnabled()) {
480
- console.debug("Geometry: Bridge chain", {
481
- scoreA,
482
- scoreB,
483
- lenA: pointsA.length,
484
- lenB: pointsB.length,
485
- ratioA,
486
- ratioB,
487
- delta,
488
- overlap,
489
- op
490
- });
491
- }
492
- const ratioEps = 1e-6;
493
- if (Math.abs(ratioA - ratioB) > ratioEps) {
494
- return ratioA > ratioB ? pointsA : pointsB;
495
- }
496
- if (scoreA !== scoreB) return scoreA > scoreB ? pointsA : pointsB;
497
- return pointsA.length <= pointsB.length ? pointsA : pointsB;
498
- }
499
- function fitPathItemToRect(item, rect, fitMode) {
500
- const { left, top, width, height } = rect;
501
- const bounds = item.bounds;
502
- if (width <= 0 || height <= 0 || !Number.isFinite(bounds.width) || !Number.isFinite(bounds.height) || bounds.width <= 0 || bounds.height <= 0) {
503
- item.position = new paper.Point(left + width / 2, top + height / 2);
504
- return item;
505
- }
506
- item.translate(new paper.Point(-bounds.left, -bounds.top));
507
- if (fitMode === "stretch") {
508
- item.scale(width / bounds.width, height / bounds.height, new paper.Point(0, 0));
509
- item.translate(new paper.Point(left, top));
510
- return item;
511
- }
512
- const uniformScale = Math.min(width / bounds.width, height / bounds.height);
513
- item.scale(uniformScale, uniformScale, new paper.Point(0, 0));
514
- const scaledWidth = bounds.width * uniformScale;
515
- const scaledHeight = bounds.height * uniformScale;
516
- item.translate(
517
- new paper.Point(
518
- left + (width - scaledWidth) / 2,
519
- top + (height - scaledHeight) / 2
520
- )
225
+ const maxMm = Math.max(
226
+ minMm,
227
+ Number(configService.get("size.maxMm", DEFAULT_SIZE_STATE.maxMm))
521
228
  );
522
- return item;
523
- }
524
- function createNormalizedHeartPath(params) {
525
- const { lobeSpread, notchDepth, tipSharpness } = params;
526
- const halfSpread = 0.22 + lobeSpread * 0.18;
527
- const notchY = 0.06 + notchDepth * 0.2;
528
- const shoulderY = 0.24 + notchDepth * 0.2;
529
- const topLift = 0.12 + (1 - notchDepth) * 0.06;
530
- const topY = notchY - topLift;
531
- const sideCtrlY = shoulderY - (0.18 - notchDepth * 0.08);
532
- const lowerCtrlY = 0.58 + (1 - tipSharpness) * 0.16;
533
- const tipCtrlX = 0.34 - tipSharpness * 0.2;
534
- const notchCtrlX = 0.06 + lobeSpread * 0.06;
535
- const lobeCtrlX = 0.1 + lobeSpread * 0.08;
536
- const notchCtrlY = notchY - topLift * 0.45;
537
- const xPeakL = 0.5 - halfSpread;
538
- const xPeakR = 0.5 + halfSpread;
539
- const heartPath = new paper.Path({ insert: false });
540
- heartPath.moveTo(new paper.Point(0.5, notchY));
541
- heartPath.cubicCurveTo(
542
- new paper.Point(0.5 - notchCtrlX, notchCtrlY),
543
- new paper.Point(xPeakL + lobeCtrlX, topY),
544
- new paper.Point(xPeakL, topY)
229
+ const stepMm = Math.max(
230
+ 1e-3,
231
+ Number(configService.get("size.stepMm", DEFAULT_SIZE_STATE.stepMm))
545
232
  );
546
- heartPath.cubicCurveTo(
547
- new paper.Point(xPeakL - lobeCtrlX, topY),
548
- new paper.Point(0, sideCtrlY),
549
- new paper.Point(0, shoulderY)
233
+ const actualWidthMm = sanitizeMmValue(
234
+ parseLengthToMm(
235
+ configService.get("size.actualWidthMm", DEFAULT_SIZE_STATE.actualWidthMm),
236
+ "mm"
237
+ ),
238
+ { minMm, maxMm, stepMm }
550
239
  );
551
- heartPath.cubicCurveTo(
552
- new paper.Point(0, lowerCtrlY),
553
- new paper.Point(tipCtrlX, 1),
554
- new paper.Point(0.5, 1)
240
+ const actualHeightMm = sanitizeMmValue(
241
+ parseLengthToMm(
242
+ configService.get(
243
+ "size.actualHeightMm",
244
+ DEFAULT_SIZE_STATE.actualHeightMm
245
+ ),
246
+ "mm"
247
+ ),
248
+ { minMm, maxMm, stepMm }
555
249
  );
556
- heartPath.cubicCurveTo(
557
- new paper.Point(1 - tipCtrlX, 1),
558
- new paper.Point(1, lowerCtrlY),
559
- new paper.Point(1, shoulderY)
250
+ const aspectRaw = Number(
251
+ configService.get("size.aspectRatio", DEFAULT_SIZE_STATE.aspectRatio)
560
252
  );
561
- heartPath.cubicCurveTo(
562
- new paper.Point(1, sideCtrlY),
563
- new paper.Point(xPeakR + lobeCtrlX, topY),
564
- new paper.Point(xPeakR, topY)
253
+ const aspectRatio = Number.isFinite(aspectRaw) && aspectRaw > 0 ? aspectRaw : actualWidthMm / Math.max(1e-3, actualHeightMm);
254
+ const cutMarginMm = Math.max(
255
+ 0,
256
+ parseLengthToMm(
257
+ configService.get("size.cutMarginMm", DEFAULT_SIZE_STATE.cutMarginMm),
258
+ "mm"
259
+ )
565
260
  );
566
- heartPath.cubicCurveTo(
567
- new paper.Point(xPeakR - lobeCtrlX, topY),
568
- new paper.Point(0.5 + notchCtrlX, notchCtrlY),
569
- new paper.Point(0.5, notchY)
261
+ const viewPadding = configService.get(
262
+ "size.viewPadding",
263
+ DEFAULT_SIZE_STATE.viewPadding
570
264
  );
571
- heartPath.closed = true;
572
- return heartPath;
573
- }
574
- function createHeartBaseShape(options) {
575
- const { x, y, width, height } = options;
576
- const w = Math.max(0, width);
577
- const h = Math.max(0, height);
578
- const left = x - w / 2;
579
- const top = y - h / 2;
580
- const fitMode = getShapeFitMode(options.shapeStyle);
581
- const heartParams = getHeartShapeParams(options.shapeStyle);
582
- const rawHeart = createNormalizedHeartPath(heartParams);
583
- return fitPathItemToRect(rawHeart, { left, top, width: w, height: h }, fitMode);
265
+ return {
266
+ unit,
267
+ actualWidthMm,
268
+ actualHeightMm,
269
+ constraintMode: normalizeConstraintMode(
270
+ configService.get(
271
+ "size.constraintMode",
272
+ DEFAULT_SIZE_STATE.constraintMode
273
+ )
274
+ ),
275
+ aspectRatio,
276
+ cutMode: normalizeCutMode(
277
+ configService.get("size.cutMode", DEFAULT_SIZE_STATE.cutMode)
278
+ ),
279
+ cutMarginMm,
280
+ viewPadding,
281
+ minMm,
282
+ maxMm,
283
+ stepMm
284
+ };
584
285
  }
585
- var BUILTIN_SHAPE_BUILDERS = {
586
- rect: (options) => {
587
- const { x, y, width, height, radius } = options;
588
- return new paper.Path.Rectangle({
589
- point: [x - width / 2, y - height / 2],
590
- size: [Math.max(0, width), Math.max(0, height)],
591
- radius: Math.max(0, radius)
592
- });
593
- },
594
- circle: (options) => {
595
- const { x, y, width, height } = options;
596
- const r = Math.min(width, height) / 2;
597
- return new paper.Path.Circle({
598
- center: new paper.Point(x, y),
599
- radius: Math.max(0, r)
600
- });
601
- },
602
- ellipse: (options) => {
603
- const { x, y, width, height } = options;
604
- return new paper.Path.Ellipse({
605
- center: new paper.Point(x, y),
606
- radius: [Math.max(0, width / 2), Math.max(0, height / 2)]
607
- });
608
- },
609
- heart: createHeartBaseShape
610
- };
611
- function createCustomBaseShape(options) {
612
- var _a;
613
- const {
614
- pathData,
615
- customSourceWidthPx,
616
- customSourceHeightPx,
617
- x,
618
- y,
286
+ function rectByCenter(centerX, centerY, width, height) {
287
+ return {
288
+ left: centerX - width / 2,
289
+ top: centerY - height / 2,
619
290
  width,
620
- height
621
- } = options;
622
- if (typeof pathData !== "string" || pathData.trim().length === 0) {
623
- return null;
624
- }
625
- const center = new paper.Point(x, y);
626
- const hasMultipleSubPaths = ((_a = (pathData.match(/[Mm]/g) || []).length) != null ? _a : 0) > 1;
627
- const path = hasMultipleSubPaths ? new paper.CompoundPath(pathData) : (() => {
628
- const single = new paper.Path();
629
- single.pathData = pathData;
630
- return single;
631
- })();
632
- const sourceWidth = Number(customSourceWidthPx != null ? customSourceWidthPx : 0);
633
- const sourceHeight = Number(customSourceHeightPx != null ? customSourceHeightPx : 0);
634
- if (Number.isFinite(sourceWidth) && Number.isFinite(sourceHeight) && sourceWidth > 0 && sourceHeight > 0 && width > 0 && height > 0) {
635
- const targetLeft = x - width / 2;
636
- const targetTop = y - height / 2;
637
- path.scale(width / sourceWidth, height / sourceHeight, new paper.Point(0, 0));
638
- path.translate(new paper.Point(targetLeft, targetTop));
639
- return path;
640
- }
641
- if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
642
- path.position = center;
643
- path.scale(width / path.bounds.width, height / path.bounds.height);
644
- return path;
645
- }
646
- path.position = center;
647
- return path;
648
- }
649
- function createBaseShape(options) {
650
- const { shape } = options;
651
- if (shape === "custom") {
652
- const customShape = createCustomBaseShape(options);
653
- if (customShape) return customShape;
654
- return BUILTIN_SHAPE_BUILDERS[DEFAULT_DIELINE_SHAPE](options);
655
- }
656
- return BUILTIN_SHAPE_BUILDERS[shape](options);
291
+ height,
292
+ centerX,
293
+ centerY
294
+ };
657
295
  }
658
- function resolveBridgeBasePath(shape, anchor) {
659
- if (shape instanceof paper.Path) {
660
- return shape;
296
+ function getCutSizeMm(size) {
297
+ if (size.cutMode === "trim") {
298
+ return { widthMm: size.actualWidthMm, heightMm: size.actualHeightMm };
661
299
  }
662
- if (shape instanceof paper.CompoundPath) {
663
- const children = (shape.children || []).filter(
664
- (child) => child instanceof paper.Path
665
- );
666
- if (!children.length) return null;
667
- let best = children[0];
668
- let bestDistance = Infinity;
669
- for (const child of children) {
670
- const location = child.getNearestLocation(anchor);
671
- const point = location == null ? void 0 : location.point;
672
- if (!point) continue;
673
- const distance = point.getDistance(anchor);
674
- if (distance < bestDistance) {
675
- bestDistance = distance;
676
- best = child;
677
- }
678
- }
679
- return best;
300
+ const delta = size.cutMarginMm * 2;
301
+ if (size.cutMode === "outset") {
302
+ return {
303
+ widthMm: size.actualWidthMm + delta,
304
+ heightMm: size.actualHeightMm + delta
305
+ };
680
306
  }
681
- return null;
307
+ return {
308
+ widthMm: Math.max(size.minMm, size.actualWidthMm - delta),
309
+ heightMm: Math.max(size.minMm, size.actualHeightMm - delta)
310
+ };
682
311
  }
683
- function createFeatureItem(feature, center) {
684
- let item;
685
- if (feature.shape === "rect") {
686
- const w = feature.width || 10;
687
- const h = feature.height || 10;
688
- const r = feature.radius || 0;
689
- item = new paper.Path.Rectangle({
690
- point: [center.x - w / 2, center.y - h / 2],
691
- size: [w, h],
692
- radius: r
693
- });
694
- } else {
695
- const r = feature.radius || 5;
696
- item = new paper.Path.Circle({
697
- center,
698
- radius: r
699
- });
312
+ function computeSceneLayout(canvasService, size) {
313
+ const canvasWidth = canvasService.canvas.width || 0;
314
+ const canvasHeight = canvasService.canvas.height || 0;
315
+ if (canvasWidth <= 0 || canvasHeight <= 0) return null;
316
+ const { widthMm: cutWidthMm, heightMm: cutHeightMm } = getCutSizeMm(size);
317
+ const viewWidthMm = Math.max(size.actualWidthMm, cutWidthMm);
318
+ const viewHeightMm = Math.max(size.actualHeightMm, cutHeightMm);
319
+ if (!Number.isFinite(viewWidthMm) || !Number.isFinite(viewHeightMm) || viewWidthMm <= 0 || viewHeightMm <= 0) {
320
+ return null;
700
321
  }
701
- if (feature.rotation) {
702
- item.rotate(feature.rotation, center);
322
+ const paddingPx = resolvePaddingPx(
323
+ size.viewPadding,
324
+ canvasWidth,
325
+ canvasHeight
326
+ );
327
+ canvasService.viewport.updateContainer(canvasWidth, canvasHeight);
328
+ canvasService.viewport.setPadding(paddingPx);
329
+ canvasService.viewport.updatePhysical(viewWidthMm, viewHeightMm);
330
+ const layout = canvasService.viewport.layout;
331
+ if (!Number.isFinite(layout.scale) || !Number.isFinite(layout.offsetX) || !Number.isFinite(layout.offsetY) || layout.scale <= 0) {
332
+ return null;
703
333
  }
704
- return item;
705
- }
706
- function getPerimeterShape(options) {
707
- let mainShape = createBaseShape(options);
708
- const { features } = options;
709
- if (features && features.length > 0) {
710
- const edgeFeatures = features.filter(
711
- (f) => !f.renderBehavior || f.renderBehavior === "edge"
712
- );
713
- const adds = [];
714
- const subtracts = [];
715
- edgeFeatures.forEach((f) => {
716
- const pos = resolveFeaturePosition(f, options);
717
- const center = new paper.Point(pos.x, pos.y);
718
- const item = createFeatureItem(f, center);
719
- if (f.bridge && f.bridge.type === "vertical") {
720
- const itemBounds = item.bounds;
721
- const mainBounds = mainShape.bounds;
722
- const bridgeTop = mainBounds.top;
723
- const bridgeBottom = itemBounds.top;
724
- if (bridgeBottom > bridgeTop) {
725
- const overlap = 2;
726
- const rayPadding = 10;
727
- const eps = 0.1;
728
- const delta = getBridgeDelta(itemBounds, overlap);
729
- const toY = bridgeTop - rayPadding;
730
- const inset = Math.min(1, Math.max(0, itemBounds.width * 0.01));
731
- const xLeft = itemBounds.left + inset;
732
- const xRight = itemBounds.right - inset;
733
- const bridgeBasePath = resolveBridgeBasePath(mainShape, center);
734
- const canBridge = !!bridgeBasePath && xRight - xLeft > eps;
735
- if (canBridge && bridgeBasePath) {
736
- const leftHit = getExitHit({
737
- mainShape: bridgeBasePath,
738
- x: xLeft,
739
- bridgeBottom,
740
- toY,
741
- eps,
742
- delta,
743
- overlap,
744
- op: f.operation
745
- });
746
- const rightHit = getExitHit({
747
- mainShape: bridgeBasePath,
748
- x: xRight,
749
- bridgeBottom,
750
- toY,
751
- eps,
752
- delta,
753
- overlap,
754
- op: f.operation
755
- });
756
- if (leftHit && rightHit) {
757
- const pathLength = bridgeBasePath.length;
758
- const leftOffset = leftHit.location.offset;
759
- const rightOffset = rightHit.location.offset;
760
- const distanceA = wrappedDistance(pathLength, leftOffset, rightOffset);
761
- const distanceB = wrappedDistance(pathLength, rightOffset, leftOffset);
762
- const countFor = (d) => Math.max(8, Math.min(80, Math.ceil(d / 6)));
763
- const offsetsA = sampleWrappedOffsets(
764
- pathLength,
765
- leftOffset,
766
- rightOffset,
767
- countFor(distanceA)
334
+ const centerX = layout.offsetX + layout.width / 2;
335
+ const centerY = layout.offsetY + layout.height / 2;
336
+ const trimWidthPx = size.actualWidthMm * layout.scale;
337
+ const trimHeightPx = size.actualHeightMm * layout.scale;
338
+ const cutWidthPx = cutWidthMm * layout.scale;
339
+ const cutHeightPx = cutHeightMm * layout.scale;
340
+ const trimRect = rectByCenter(centerX, centerY, trimWidthPx, trimHeightPx);
341
+ const cutRect = rectByCenter(centerX, centerY, cutWidthPx, cutHeightPx);
342
+ const bleedRect = rectByCenter(
343
+ centerX,
344
+ centerY,
345
+ Math.max(trimWidthPx, cutWidthPx),
346
+ Math.max(trimHeightPx, cutHeightPx)
347
+ );
348
+ return {
349
+ scale: layout.scale,
350
+ canvasWidth,
351
+ canvasHeight,
352
+ trimRect,
353
+ cutRect,
354
+ bleedRect,
355
+ trimWidthMm: size.actualWidthMm,
356
+ trimHeightMm: size.actualHeightMm,
357
+ cutWidthMm,
358
+ cutHeightMm,
359
+ cutMode: size.cutMode,
360
+ cutMarginMm: size.cutMarginMm
361
+ };
362
+ }
363
+ function buildSceneGeometry(configService, layout) {
364
+ const radiusMm = parseLengthToMm(
365
+ configService.get("dieline.radius", 0),
366
+ "mm"
367
+ );
368
+ const offset = (layout.cutRect.width - layout.trimRect.width) / 2;
369
+ const sourceWidth = Number(configService.get("dieline.customSourceWidthPx", 0));
370
+ const sourceHeight = Number(
371
+ configService.get("dieline.customSourceHeightPx", 0)
372
+ );
373
+ const shapeStyle = normalizeShapeStyle(
374
+ configService.get("dieline.shapeStyle", DEFAULT_DIELINE_SHAPE_STYLE)
375
+ );
376
+ return {
377
+ shape: normalizeDielineShape(
378
+ configService.get("dieline.shape", DEFAULT_DIELINE_SHAPE)
379
+ ),
380
+ shapeStyle,
381
+ unit: "px",
382
+ x: layout.trimRect.centerX,
383
+ y: layout.trimRect.centerY,
384
+ width: layout.trimRect.width,
385
+ height: layout.trimRect.height,
386
+ radius: radiusMm * layout.scale,
387
+ offset,
388
+ scale: layout.scale,
389
+ pathData: configService.get("dieline.pathData"),
390
+ customSourceWidthPx: Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : void 0,
391
+ customSourceHeightPx: Number.isFinite(sourceHeight) && sourceHeight > 0 ? sourceHeight : void 0
392
+ };
393
+ }
394
+
395
+ // src/extensions/background.ts
396
+ var BACKGROUND_LAYER_ID = "background";
397
+ var BACKGROUND_CONFIG_KEY = "background.config";
398
+ var DEFAULT_WIDTH = 800;
399
+ var DEFAULT_HEIGHT = 600;
400
+ var DEFAULT_BACKGROUND_CONFIG = {
401
+ version: 1,
402
+ layers: [
403
+ {
404
+ id: "base-color",
405
+ kind: "color",
406
+ anchor: "viewport",
407
+ fit: "cover",
408
+ opacity: 1,
409
+ order: 0,
410
+ enabled: true,
411
+ exportable: false,
412
+ color: "#fff"
413
+ }
414
+ ]
415
+ };
416
+ function clampOpacity(value, fallback) {
417
+ const numeric = Number(value);
418
+ if (!Number.isFinite(numeric)) {
419
+ return Math.max(0, Math.min(1, fallback));
420
+ }
421
+ return Math.max(0, Math.min(1, numeric));
422
+ }
423
+ function normalizeLayerKind(value, fallback) {
424
+ if (value === "color" || value === "image") {
425
+ return value;
426
+ }
427
+ return fallback;
428
+ }
429
+ function normalizeFitMode2(value, fallback) {
430
+ if (value === "contain" || value === "cover" || value === "stretch") {
431
+ return value;
432
+ }
433
+ return fallback;
434
+ }
435
+ function normalizeAnchor(value, fallback) {
436
+ if (typeof value !== "string") return fallback;
437
+ const trimmed = value.trim();
438
+ return trimmed || fallback;
439
+ }
440
+ function normalizeOrder(value, fallback) {
441
+ const numeric = Number(value);
442
+ if (!Number.isFinite(numeric)) return fallback;
443
+ return numeric;
444
+ }
445
+ function normalizeLayer(raw, index, fallback) {
446
+ const fallbackLayer = fallback || {
447
+ id: `layer-${index + 1}`,
448
+ kind: "image",
449
+ anchor: "viewport",
450
+ fit: "contain",
451
+ opacity: 1,
452
+ order: index,
453
+ enabled: true,
454
+ exportable: false,
455
+ src: ""
456
+ };
457
+ if (!raw || typeof raw !== "object") {
458
+ return { ...fallbackLayer };
459
+ }
460
+ const input = raw;
461
+ const kind = normalizeLayerKind(input.kind, fallbackLayer.kind);
462
+ return {
463
+ id: typeof input.id === "string" && input.id.trim().length > 0 ? input.id.trim() : fallbackLayer.id,
464
+ kind,
465
+ anchor: normalizeAnchor(input.anchor, fallbackLayer.anchor),
466
+ fit: normalizeFitMode2(input.fit, fallbackLayer.fit),
467
+ opacity: clampOpacity(input.opacity, fallbackLayer.opacity),
468
+ order: normalizeOrder(input.order, fallbackLayer.order),
469
+ enabled: typeof input.enabled === "boolean" ? input.enabled : fallbackLayer.enabled,
470
+ exportable: typeof input.exportable === "boolean" ? input.exportable : fallbackLayer.exportable,
471
+ color: kind === "color" ? typeof input.color === "string" ? input.color : typeof fallbackLayer.color === "string" ? fallbackLayer.color : "#ffffff" : void 0,
472
+ src: kind === "image" ? typeof input.src === "string" ? input.src.trim() : typeof fallbackLayer.src === "string" ? fallbackLayer.src : "" : void 0
473
+ };
474
+ }
475
+ function normalizeConfig(raw) {
476
+ if (!raw || typeof raw !== "object") {
477
+ return cloneConfig(DEFAULT_BACKGROUND_CONFIG);
478
+ }
479
+ const input = raw;
480
+ const version = Number.isFinite(Number(input.version)) ? Number(input.version) : DEFAULT_BACKGROUND_CONFIG.version;
481
+ const baseLayers = Array.isArray(input.layers) ? input.layers.map((layer, index) => normalizeLayer(layer, index)) : cloneConfig(DEFAULT_BACKGROUND_CONFIG).layers;
482
+ const uniqueLayers = [];
483
+ const seen = /* @__PURE__ */ new Set();
484
+ baseLayers.forEach((layer, index) => {
485
+ let nextId = layer.id || `layer-${index + 1}`;
486
+ let serial = 1;
487
+ while (seen.has(nextId)) {
488
+ serial += 1;
489
+ nextId = `${layer.id || `layer-${index + 1}`}-${serial}`;
490
+ }
491
+ seen.add(nextId);
492
+ uniqueLayers.push({ ...layer, id: nextId });
493
+ });
494
+ return {
495
+ version,
496
+ layers: uniqueLayers
497
+ };
498
+ }
499
+ function cloneConfig(config) {
500
+ return {
501
+ version: config.version,
502
+ layers: (config.layers || []).map((layer) => ({ ...layer }))
503
+ };
504
+ }
505
+ function mergeConfig(base, patch) {
506
+ const merged = {
507
+ version: patch.version === void 0 ? base.version : Number.isFinite(Number(patch.version)) ? Number(patch.version) : base.version,
508
+ layers: Array.isArray(patch.layers) ? patch.layers.map((layer, index) => normalizeLayer(layer, index)) : base.layers.map((layer) => ({ ...layer }))
509
+ };
510
+ return normalizeConfig(merged);
511
+ }
512
+ function configSignature(config) {
513
+ return JSON.stringify(config);
514
+ }
515
+ var BackgroundTool = class {
516
+ constructor(options) {
517
+ this.id = "pooder.kit.background";
518
+ this.metadata = {
519
+ name: "BackgroundTool"
520
+ };
521
+ this.config = cloneConfig(DEFAULT_BACKGROUND_CONFIG);
522
+ this.specs = [];
523
+ this.renderSeq = 0;
524
+ this.latestSceneLayout = null;
525
+ this.sourceSizeBySrc = /* @__PURE__ */ new Map();
526
+ this.pendingSizeBySrc = /* @__PURE__ */ new Map();
527
+ this.onCanvasResized = () => {
528
+ this.latestSceneLayout = null;
529
+ this.updateBackground();
530
+ };
531
+ this.onSceneLayoutChanged = (layout) => {
532
+ this.latestSceneLayout = layout;
533
+ this.updateBackground();
534
+ };
535
+ if (options && typeof options === "object") {
536
+ this.config = mergeConfig(this.config, options);
537
+ }
538
+ }
539
+ activate(context) {
540
+ var _a, _b;
541
+ this.canvasService = context.services.get("CanvasService");
542
+ if (!this.canvasService) {
543
+ console.warn("CanvasService not found for BackgroundTool");
544
+ return;
545
+ }
546
+ this.configService = context.services.get(
547
+ "ConfigurationService"
548
+ );
549
+ if (this.configService) {
550
+ this.config = normalizeConfig(
551
+ this.configService.get(
552
+ BACKGROUND_CONFIG_KEY,
553
+ DEFAULT_BACKGROUND_CONFIG
554
+ )
555
+ );
556
+ (_a = this.configChangeDisposable) == null ? void 0 : _a.dispose();
557
+ this.configChangeDisposable = this.configService.onAnyChange(
558
+ (e) => {
559
+ if (e.key === BACKGROUND_CONFIG_KEY) {
560
+ this.config = normalizeConfig(e.value);
561
+ this.updateBackground();
562
+ return;
563
+ }
564
+ if (e.key.startsWith("size.")) {
565
+ this.latestSceneLayout = null;
566
+ this.updateBackground();
567
+ }
568
+ }
569
+ );
570
+ }
571
+ (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
572
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
573
+ this.id,
574
+ () => ({
575
+ passes: [
576
+ {
577
+ id: BACKGROUND_LAYER_ID,
578
+ stack: 0,
579
+ order: 0,
580
+ objects: this.specs
581
+ }
582
+ ]
583
+ }),
584
+ { priority: 0 }
585
+ );
586
+ context.eventBus.on("canvas:resized", this.onCanvasResized);
587
+ context.eventBus.on("scene:layout:change", this.onSceneLayoutChanged);
588
+ this.updateBackground();
589
+ }
590
+ deactivate(context) {
591
+ var _a, _b;
592
+ context.eventBus.off("canvas:resized", this.onCanvasResized);
593
+ context.eventBus.off("scene:layout:change", this.onSceneLayoutChanged);
594
+ this.renderSeq += 1;
595
+ this.specs = [];
596
+ this.latestSceneLayout = null;
597
+ (_a = this.configChangeDisposable) == null ? void 0 : _a.dispose();
598
+ this.configChangeDisposable = void 0;
599
+ (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
600
+ this.renderProducerDisposable = void 0;
601
+ if (!this.canvasService) return;
602
+ void this.canvasService.flushRenderFromProducers();
603
+ this.canvasService.requestRenderAll();
604
+ this.canvasService = void 0;
605
+ this.configService = void 0;
606
+ }
607
+ contribute() {
608
+ return {
609
+ [ContributionPointIds.CONFIGURATIONS]: [
610
+ {
611
+ id: BACKGROUND_CONFIG_KEY,
612
+ type: "json",
613
+ label: "Background Config",
614
+ default: cloneConfig(DEFAULT_BACKGROUND_CONFIG)
615
+ }
616
+ ],
617
+ [ContributionPointIds.COMMANDS]: [
618
+ {
619
+ command: "background.getConfig",
620
+ title: "Get Background Config",
621
+ handler: () => cloneConfig(this.config)
622
+ },
623
+ {
624
+ command: "background.resetConfig",
625
+ title: "Reset Background Config",
626
+ handler: () => {
627
+ this.commitConfig(cloneConfig(DEFAULT_BACKGROUND_CONFIG));
628
+ return true;
629
+ }
630
+ },
631
+ {
632
+ command: "background.replaceConfig",
633
+ title: "Replace Background Config",
634
+ handler: (config) => {
635
+ this.commitConfig(normalizeConfig(config));
636
+ return true;
637
+ }
638
+ },
639
+ {
640
+ command: "background.patchConfig",
641
+ title: "Patch Background Config",
642
+ handler: (patch) => {
643
+ this.commitConfig(mergeConfig(this.config, patch || {}));
644
+ return true;
645
+ }
646
+ },
647
+ {
648
+ command: "background.upsertLayer",
649
+ title: "Upsert Background Layer",
650
+ handler: (layer) => {
651
+ const normalized = normalizeLayer(layer, 0);
652
+ const existingIndex = this.config.layers.findIndex(
653
+ (item) => item.id === normalized.id
654
+ );
655
+ const nextLayers = [...this.config.layers];
656
+ if (existingIndex >= 0) {
657
+ nextLayers[existingIndex] = normalizeLayer(
658
+ { ...nextLayers[existingIndex], ...layer },
659
+ existingIndex,
660
+ nextLayers[existingIndex]
768
661
  );
769
- const offsetsB = sampleWrappedOffsets(
770
- pathLength,
771
- rightOffset,
772
- leftOffset,
773
- countFor(distanceB)
662
+ } else {
663
+ nextLayers.push(
664
+ normalizeLayer(
665
+ {
666
+ ...normalized,
667
+ order: Number.isFinite(Number(layer.order)) ? Number(layer.order) : nextLayers.length
668
+ },
669
+ nextLayers.length
670
+ )
774
671
  );
775
- const pointsA = offsetsA.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
776
- const pointsB = offsetsB.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
777
- if (pointsA.length >= 2 && pointsB.length >= 2) {
778
- let topBase = selectOuterChain({
779
- mainShape: bridgeBasePath,
780
- pointsA,
781
- pointsB,
782
- delta,
783
- overlap,
784
- op: f.operation
785
- });
786
- const dist2 = (a, b) => {
787
- const dx = a.x - b.x;
788
- const dy = a.y - b.y;
789
- return dx * dx + dy * dy;
790
- };
791
- if (dist2(topBase[0], leftHit.point) > dist2(topBase[0], rightHit.point)) {
792
- topBase = topBase.slice().reverse();
793
- }
794
- topBase = topBase.slice();
795
- topBase[0] = leftHit.point;
796
- topBase[topBase.length - 1] = rightHit.point;
797
- const capShiftY = f.operation === "subtract" ? -Math.max(overlap * 2, delta) : overlap;
798
- const topPoints = topBase.map(
799
- (p) => p.add(new paper.Point(0, capShiftY))
800
- );
801
- const bridgeBottomY = bridgeBottom + overlap * 2;
802
- const bridgePoly = new paper.Path({ insert: false });
803
- for (const p of topPoints) bridgePoly.add(p);
804
- bridgePoly.add(new paper.Point(xRight, bridgeBottomY));
805
- bridgePoly.add(new paper.Point(xLeft, bridgeBottomY));
806
- bridgePoly.closed = true;
807
- const unitedItem = item.unite(bridgePoly);
808
- item.remove();
809
- bridgePoly.remove();
810
- if (f.operation === "add") {
811
- adds.push(unitedItem);
812
- } else {
813
- subtracts.push(unitedItem);
814
- }
815
- return;
816
- }
817
672
  }
673
+ this.commitConfig(
674
+ normalizeConfig({
675
+ ...this.config,
676
+ layers: nextLayers
677
+ })
678
+ );
679
+ return true;
818
680
  }
819
- if (f.operation === "add") {
820
- adds.push(item);
821
- } else {
822
- subtracts.push(item);
823
- }
824
- } else {
825
- if (f.operation === "add") {
826
- adds.push(item);
827
- } else {
828
- subtracts.push(item);
681
+ },
682
+ {
683
+ command: "background.removeLayer",
684
+ title: "Remove Background Layer",
685
+ handler: (id) => {
686
+ const nextLayers = this.config.layers.filter(
687
+ (layer) => layer.id !== id
688
+ );
689
+ this.commitConfig(
690
+ normalizeConfig({
691
+ ...this.config,
692
+ layers: nextLayers
693
+ })
694
+ );
695
+ return true;
829
696
  }
830
697
  }
831
- } else {
832
- if (f.operation === "add") {
833
- adds.push(item);
834
- } else {
835
- subtracts.push(item);
698
+ ]
699
+ };
700
+ }
701
+ commitConfig(next) {
702
+ const normalized = normalizeConfig(next);
703
+ if (configSignature(normalized) === configSignature(this.config)) {
704
+ return;
705
+ }
706
+ if (this.configService) {
707
+ this.configService.update(BACKGROUND_CONFIG_KEY, cloneConfig(normalized));
708
+ return;
709
+ }
710
+ this.config = normalized;
711
+ this.updateBackground();
712
+ }
713
+ getViewportRect() {
714
+ var _a, _b;
715
+ const width = Number(((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 0);
716
+ const height = Number(((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 0);
717
+ return {
718
+ left: 0,
719
+ top: 0,
720
+ width: width > 0 ? width : DEFAULT_WIDTH,
721
+ height: height > 0 ? height : DEFAULT_HEIGHT
722
+ };
723
+ }
724
+ resolveSceneLayout() {
725
+ if (this.latestSceneLayout) return this.latestSceneLayout;
726
+ if (!this.canvasService || !this.configService) return null;
727
+ const layout = computeSceneLayout(
728
+ this.canvasService,
729
+ readSizeState(this.configService)
730
+ );
731
+ this.latestSceneLayout = layout;
732
+ return layout;
733
+ }
734
+ resolveFocusRect() {
735
+ const layout = this.resolveSceneLayout();
736
+ if (!layout) return null;
737
+ return {
738
+ left: layout.trimRect.left,
739
+ top: layout.trimRect.top,
740
+ width: layout.trimRect.width,
741
+ height: layout.trimRect.height
742
+ };
743
+ }
744
+ resolveAnchorRect(anchor) {
745
+ if (anchor === "focus") {
746
+ return this.resolveFocusRect() || this.getViewportRect();
747
+ }
748
+ if (anchor !== "viewport") {
749
+ return this.getViewportRect();
750
+ }
751
+ return this.getViewportRect();
752
+ }
753
+ resolveImagePlacement(target, sourceSize, fit) {
754
+ const targetWidth = Math.max(1, Number(target.width || 0));
755
+ const targetHeight = Math.max(1, Number(target.height || 0));
756
+ const sourceWidth = Math.max(1, Number(sourceSize.width || 0));
757
+ const sourceHeight = Math.max(1, Number(sourceSize.height || 0));
758
+ if (fit === "stretch") {
759
+ return {
760
+ left: target.left,
761
+ top: target.top,
762
+ scaleX: targetWidth / sourceWidth,
763
+ scaleY: targetHeight / sourceHeight
764
+ };
765
+ }
766
+ const scale = fit === "contain" ? Math.min(targetWidth / sourceWidth, targetHeight / sourceHeight) : Math.max(targetWidth / sourceWidth, targetHeight / sourceHeight);
767
+ const renderWidth = sourceWidth * scale;
768
+ const renderHeight = sourceHeight * scale;
769
+ return {
770
+ left: target.left + (targetWidth - renderWidth) / 2,
771
+ top: target.top + (targetHeight - renderHeight) / 2,
772
+ scaleX: scale,
773
+ scaleY: scale
774
+ };
775
+ }
776
+ buildColorLayerSpec(layer) {
777
+ const rect = this.resolveAnchorRect(layer.anchor);
778
+ return {
779
+ id: `background.layer.${layer.id}.color`,
780
+ type: "rect",
781
+ space: "screen",
782
+ data: {
783
+ id: `background.layer.${layer.id}.color`,
784
+ layerId: BACKGROUND_LAYER_ID,
785
+ type: "background-layer",
786
+ layerRef: layer.id,
787
+ layerKind: layer.kind
788
+ },
789
+ props: {
790
+ left: rect.left,
791
+ top: rect.top,
792
+ width: rect.width,
793
+ height: rect.height,
794
+ originX: "left",
795
+ originY: "top",
796
+ fill: layer.color || "transparent",
797
+ opacity: layer.opacity,
798
+ selectable: false,
799
+ evented: false,
800
+ excludeFromExport: !layer.exportable
801
+ }
802
+ };
803
+ }
804
+ buildImageLayerSpec(layer) {
805
+ const src = String(layer.src || "").trim();
806
+ if (!src) return [];
807
+ const sourceSize = this.sourceSizeBySrc.get(src);
808
+ if (!sourceSize) return [];
809
+ const rect = this.resolveAnchorRect(layer.anchor);
810
+ const placement = this.resolveImagePlacement(rect, sourceSize, layer.fit);
811
+ return [
812
+ {
813
+ id: `background.layer.${layer.id}.image`,
814
+ type: "image",
815
+ src,
816
+ space: "screen",
817
+ data: {
818
+ id: `background.layer.${layer.id}.image`,
819
+ layerId: BACKGROUND_LAYER_ID,
820
+ type: "background-layer",
821
+ layerRef: layer.id,
822
+ layerKind: layer.kind
823
+ },
824
+ props: {
825
+ left: placement.left,
826
+ top: placement.top,
827
+ originX: "left",
828
+ originY: "top",
829
+ scaleX: placement.scaleX,
830
+ scaleY: placement.scaleY,
831
+ opacity: layer.opacity,
832
+ selectable: false,
833
+ evented: false,
834
+ excludeFromExport: !layer.exportable
836
835
  }
837
836
  }
837
+ ];
838
+ }
839
+ buildBackgroundSpecs(config) {
840
+ const activeLayers = (config.layers || []).filter((layer) => layer.enabled).map((layer, index) => ({ layer, index })).sort((a, b) => {
841
+ if (a.layer.order !== b.layer.order) {
842
+ return a.layer.order - b.layer.order;
843
+ }
844
+ return a.index - b.index;
838
845
  });
839
- if (adds.length > 0) {
840
- for (const item of adds) {
841
- try {
842
- const temp = mainShape.unite(item);
843
- mainShape.remove();
844
- item.remove();
845
- mainShape = normalizePathItem(temp);
846
- } catch (e) {
847
- console.error("Geometry: Failed to unite feature", e);
848
- item.remove();
849
- }
846
+ const specs = [];
847
+ activeLayers.forEach(({ layer }) => {
848
+ if (layer.kind === "color") {
849
+ specs.push(this.buildColorLayerSpec(layer));
850
+ return;
850
851
  }
852
+ specs.push(...this.buildImageLayerSpec(layer));
853
+ });
854
+ return specs;
855
+ }
856
+ collectActiveImageUrls(config) {
857
+ const urls = /* @__PURE__ */ new Set();
858
+ (config.layers || []).forEach((layer) => {
859
+ if (!layer.enabled || layer.kind !== "image") return;
860
+ const src = String(layer.src || "").trim();
861
+ if (!src) return;
862
+ urls.add(src);
863
+ });
864
+ return Array.from(urls);
865
+ }
866
+ async ensureImageSize(src) {
867
+ if (!src) return null;
868
+ const cached = this.sourceSizeBySrc.get(src);
869
+ if (cached) return cached;
870
+ const pending = this.pendingSizeBySrc.get(src);
871
+ if (pending) {
872
+ return pending;
851
873
  }
852
- if (subtracts.length > 0) {
853
- for (const item of subtracts) {
854
- try {
855
- const temp = mainShape.subtract(item);
856
- mainShape.remove();
857
- item.remove();
858
- mainShape = normalizePathItem(temp);
859
- } catch (e) {
860
- console.error("Geometry: Failed to subtract feature", e);
861
- item.remove();
862
- }
874
+ const task = this.loadImageSize(src);
875
+ this.pendingSizeBySrc.set(src, task);
876
+ try {
877
+ return await task;
878
+ } finally {
879
+ if (this.pendingSizeBySrc.get(src) === task) {
880
+ this.pendingSizeBySrc.delete(src);
863
881
  }
864
882
  }
865
883
  }
866
- return mainShape;
867
- }
868
- function applySurfaceFeatures(shape, features, options) {
869
- const surfaceFeatures = features.filter(
870
- (f) => f.renderBehavior === "surface"
871
- );
872
- if (surfaceFeatures.length === 0) return shape;
873
- let result = shape;
874
- for (const f of surfaceFeatures) {
875
- const pos = resolveFeaturePosition(f, options);
876
- const center = new paper.Point(pos.x, pos.y);
877
- const item = createFeatureItem(f, center);
884
+ async loadImageSize(src) {
878
885
  try {
879
- if (f.operation === "add") {
880
- const temp = result.unite(item);
881
- result.remove();
882
- item.remove();
883
- result = normalizePathItem(temp);
884
- } else {
885
- const temp = result.subtract(item);
886
- result.remove();
887
- item.remove();
888
- result = normalizePathItem(temp);
886
+ const image = await FabricImage.fromURL(src, {
887
+ crossOrigin: "anonymous"
888
+ });
889
+ const width = Number((image == null ? void 0 : image.width) || 0);
890
+ const height = Number((image == null ? void 0 : image.height) || 0);
891
+ if (width > 0 && height > 0) {
892
+ const size = { width, height };
893
+ this.sourceSizeBySrc.set(src, size);
894
+ return size;
889
895
  }
890
- } catch (e) {
891
- console.error("Geometry: Failed to apply surface feature", e);
892
- item.remove();
896
+ } catch (error) {
897
+ console.error("[BackgroundTool] Failed to load image", src, error);
893
898
  }
899
+ return null;
894
900
  }
895
- return result;
901
+ updateBackground() {
902
+ void this.updateBackgroundAsync();
903
+ }
904
+ async updateBackgroundAsync() {
905
+ if (!this.canvasService) return;
906
+ const seq = ++this.renderSeq;
907
+ const currentConfig = cloneConfig(this.config);
908
+ const activeUrls = this.collectActiveImageUrls(currentConfig);
909
+ if (activeUrls.length > 0) {
910
+ await Promise.all(activeUrls.map((url) => this.ensureImageSize(url)));
911
+ if (seq !== this.renderSeq) return;
912
+ }
913
+ this.specs = this.buildBackgroundSpecs(currentConfig);
914
+ await this.canvasService.flushRenderFromProducers();
915
+ if (seq !== this.renderSeq) return;
916
+ this.canvasService.requestRenderAll();
917
+ }
918
+ };
919
+
920
+ // src/extensions/image.ts
921
+ import {
922
+ ContributionPointIds as ContributionPointIds2
923
+ } from "@pooder/core";
924
+ import {
925
+ Canvas as FabricCanvas,
926
+ Image as FabricImage2,
927
+ Pattern,
928
+ Point
929
+ } from "fabric";
930
+
931
+ // src/extensions/geometry.ts
932
+ import paper from "paper";
933
+
934
+ // src/extensions/bridgeSelection.ts
935
+ function pickExitIndex(hits) {
936
+ for (let i = 0; i < hits.length; i++) {
937
+ const h = hits[i];
938
+ if (h.insideBelow && !h.insideAbove) return i;
939
+ }
940
+ return -1;
896
941
  }
897
- function generateDielinePath(options) {
898
- const paperWidth = options.canvasWidth || options.width * 2 || 2e3;
899
- const paperHeight = options.canvasHeight || options.height * 2 || 2e3;
900
- ensurePaper(paperWidth, paperHeight);
901
- paper.project.activeLayer.removeChildren();
902
- const perimeter = getPerimeterShape(options);
903
- const finalShape = applySurfaceFeatures(perimeter, options.features, options);
904
- const pathData = finalShape.pathData;
905
- finalShape.remove();
906
- return pathData;
942
+ function scoreOutsideAbove(samples) {
943
+ let score = 0;
944
+ for (const s of samples) {
945
+ if (s.outsideAbove) score++;
946
+ }
947
+ return score;
907
948
  }
908
- function generateMaskPath(options) {
909
- ensurePaper(options.canvasWidth, options.canvasHeight);
910
- paper.project.activeLayer.removeChildren();
911
- const { canvasWidth, canvasHeight } = options;
912
- const maskRect = new paper.Path.Rectangle({
913
- point: [0, 0],
914
- size: [canvasWidth, canvasHeight]
915
- });
916
- const perimeter = getPerimeterShape(options);
917
- const mainShape = applySurfaceFeatures(perimeter, options.features, options);
918
- const finalMask = maskRect.subtract(mainShape);
919
- maskRect.remove();
920
- mainShape.remove();
921
- const pathData = finalMask.pathData;
922
- finalMask.remove();
923
- return pathData;
949
+
950
+ // src/extensions/wrappedOffsets.ts
951
+ function wrappedDistance(total, start, end) {
952
+ if (!Number.isFinite(total) || total <= 0) return 0;
953
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return 0;
954
+ const s = (start % total + total) % total;
955
+ const e = (end % total + total) % total;
956
+ return e >= s ? e - s : total - s + e;
924
957
  }
925
- function generateBleedZonePath(originalOptions, offsetOptions, offset) {
926
- const paperWidth = originalOptions.canvasWidth || originalOptions.width * 2 || 2e3;
927
- const paperHeight = originalOptions.canvasHeight || originalOptions.height * 2 || 2e3;
928
- ensurePaper(paperWidth, paperHeight);
929
- paper.project.activeLayer.removeChildren();
930
- const pOriginal = getPerimeterShape(originalOptions);
931
- const shapeOriginal = applySurfaceFeatures(
932
- pOriginal,
933
- originalOptions.features,
934
- originalOptions
935
- );
936
- const pOffset = getPerimeterShape(offsetOptions);
937
- const shapeOffset = applySurfaceFeatures(
938
- pOffset,
939
- offsetOptions.features,
940
- offsetOptions
941
- );
942
- let bleedZone;
943
- if (offset > 0) {
944
- bleedZone = shapeOffset.subtract(shapeOriginal);
945
- } else {
946
- bleedZone = shapeOriginal.subtract(shapeOffset);
958
+ function sampleWrappedOffsets(total, start, end, count) {
959
+ if (!Number.isFinite(total) || total <= 0) return [];
960
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return [];
961
+ const n = Math.max(0, Math.floor(count));
962
+ if (n <= 0) return [];
963
+ const dist = wrappedDistance(total, start, end);
964
+ if (n === 1) return [(start % total + total) % total];
965
+ const step = dist / (n - 1);
966
+ const offsets = [];
967
+ for (let i = 0; i < n; i++) {
968
+ const raw = start + step * i;
969
+ const wrapped = (raw % total + total) % total;
970
+ offsets.push(wrapped);
947
971
  }
948
- const pathData = bleedZone.pathData;
949
- shapeOriginal.remove();
950
- shapeOffset.remove();
951
- bleedZone.remove();
952
- return pathData;
972
+ return offsets;
953
973
  }
954
- function getLowestPointOnDieline(options) {
955
- ensurePaper(options.width * 2, options.height * 2);
956
- paper.project.activeLayer.removeChildren();
957
- const shape = createBaseShape(options);
958
- const bounds = shape.bounds;
959
- const result = {
960
- x: bounds.center.x,
961
- y: bounds.bottom
974
+
975
+ // src/extensions/geometry.ts
976
+ function resolveFeaturePosition(feature, geometry) {
977
+ const { x, y, width, height } = geometry;
978
+ const left = x - width / 2;
979
+ const top = y - height / 2;
980
+ return {
981
+ x: left + feature.x * width,
982
+ y: top + feature.y * height
962
983
  };
963
- shape.remove();
964
- return result;
965
984
  }
966
- function getNearestPointOnDieline(point, options) {
967
- ensurePaper(options.width * 2, options.height * 2);
968
- paper.project.activeLayer.removeChildren();
969
- const shape = createBaseShape(options);
970
- const p = new paper.Point(point.x, point.y);
971
- const location = shape.getNearestLocation(p);
972
- const result = {
973
- x: location.point.x,
974
- y: location.point.y,
975
- normal: location.normal ? { x: location.normal.x, y: location.normal.y } : void 0
976
- };
977
- shape.remove();
985
+ function ensurePaper(width, height) {
986
+ if (!paper.project) {
987
+ paper.setup(new paper.Size(width, height));
988
+ } else {
989
+ paper.view.viewSize = new paper.Size(width, height);
990
+ }
991
+ }
992
+ var isBridgeDebugEnabled = () => Boolean(globalThis.__POODER_BRIDGE_DEBUG__);
993
+ function normalizePathItem(shape) {
994
+ let result = shape;
995
+ if (typeof result.resolveCrossings === "function") result = result.resolveCrossings();
996
+ if (typeof result.reduce === "function") result = result.reduce({});
997
+ if (typeof result.reorient === "function") result = result.reorient(true, true);
998
+ if (typeof result.reduce === "function") result = result.reduce({});
978
999
  return result;
979
1000
  }
980
- function getPathBounds(pathData) {
981
- const path = new paper.Path();
982
- path.pathData = pathData;
983
- const bounds = path.bounds;
984
- path.remove();
985
- return {
986
- x: bounds.x,
987
- y: bounds.y,
988
- width: bounds.width,
989
- height: bounds.height
990
- };
1001
+ function getBridgeDelta(itemBounds, overlap) {
1002
+ return Math.max(overlap, Math.min(5, Math.max(1, itemBounds.height * 0.02)));
1003
+ }
1004
+ function getExitHit(args) {
1005
+ const { mainShape, x, bridgeBottom, toY, eps, delta, overlap, op } = args;
1006
+ const ray = new paper.Path.Line({
1007
+ from: [x, bridgeBottom],
1008
+ to: [x, toY],
1009
+ insert: false
1010
+ });
1011
+ const intersections = mainShape.getIntersections(ray) || [];
1012
+ ray.remove();
1013
+ const validHits = intersections.filter((i) => i.point.y < bridgeBottom - eps);
1014
+ if (validHits.length === 0) return null;
1015
+ validHits.sort((a, b) => b.point.y - a.point.y);
1016
+ const flags = validHits.map((h) => {
1017
+ const above = h.point.add(new paper.Point(0, -delta));
1018
+ const below = h.point.add(new paper.Point(0, delta));
1019
+ return {
1020
+ insideAbove: mainShape.contains(above),
1021
+ insideBelow: mainShape.contains(below)
1022
+ };
1023
+ });
1024
+ const idx = pickExitIndex(flags);
1025
+ if (idx < 0) return null;
1026
+ if (isBridgeDebugEnabled()) {
1027
+ console.debug("Geometry: Bridge ray", {
1028
+ x,
1029
+ validHits: validHits.length,
1030
+ idx,
1031
+ delta,
1032
+ overlap,
1033
+ op
1034
+ });
1035
+ }
1036
+ const hit = validHits[idx];
1037
+ return { point: hit.point, location: hit };
991
1038
  }
992
-
993
- // src/coordinate.ts
994
- var Coordinate = class {
995
- /**
996
- * Calculate layout to fit content within container while preserving aspect ratio.
997
- */
998
- static calculateLayout(container, content, padding = 0) {
999
- const availableWidth = Math.max(0, container.width - padding * 2);
1000
- const availableHeight = Math.max(0, container.height - padding * 2);
1001
- if (content.width === 0 || content.height === 0) {
1002
- return { scale: 1, offsetX: 0, offsetY: 0, width: 0, height: 0 };
1003
- }
1004
- const scaleX = availableWidth / content.width;
1005
- const scaleY = availableHeight / content.height;
1006
- const scale = Math.min(scaleX, scaleY);
1007
- const width = content.width * scale;
1008
- const height = content.height * scale;
1009
- const offsetX = (container.width - width) / 2;
1010
- const offsetY = (container.height - height) / 2;
1011
- return { scale, offsetX, offsetY, width, height };
1039
+ function selectOuterChain(args) {
1040
+ const { mainShape, pointsA, pointsB, delta, overlap, op } = args;
1041
+ const scoreA = scoreOutsideAbove(
1042
+ pointsA.map((p) => ({
1043
+ outsideAbove: !mainShape.contains(p.add(new paper.Point(0, -delta)))
1044
+ }))
1045
+ );
1046
+ const scoreB = scoreOutsideAbove(
1047
+ pointsB.map((p) => ({
1048
+ outsideAbove: !mainShape.contains(p.add(new paper.Point(0, -delta)))
1049
+ }))
1050
+ );
1051
+ const ratioA = scoreA / pointsA.length;
1052
+ const ratioB = scoreB / pointsB.length;
1053
+ if (isBridgeDebugEnabled()) {
1054
+ console.debug("Geometry: Bridge chain", {
1055
+ scoreA,
1056
+ scoreB,
1057
+ lenA: pointsA.length,
1058
+ lenB: pointsB.length,
1059
+ ratioA,
1060
+ ratioB,
1061
+ delta,
1062
+ overlap,
1063
+ op
1064
+ });
1012
1065
  }
1013
- /**
1014
- * Convert an absolute value to a normalized value (0-1).
1015
- * @param value Absolute value (e.g., pixels)
1016
- * @param total Total dimension size (e.g., canvas width)
1017
- */
1018
- static toNormalized(value, total) {
1019
- return total === 0 ? 0 : value / total;
1066
+ const ratioEps = 1e-6;
1067
+ if (Math.abs(ratioA - ratioB) > ratioEps) {
1068
+ return ratioA > ratioB ? pointsA : pointsB;
1020
1069
  }
1021
- /**
1022
- * Convert a normalized value (0-1) to an absolute value.
1023
- * @param normalized Normalized value (0-1)
1024
- * @param total Total dimension size (e.g., canvas width)
1025
- */
1026
- static toAbsolute(normalized, total) {
1027
- return normalized * total;
1070
+ if (scoreA !== scoreB) return scoreA > scoreB ? pointsA : pointsB;
1071
+ return pointsA.length <= pointsB.length ? pointsA : pointsB;
1072
+ }
1073
+ function fitPathItemToRect(item, rect, fitMode) {
1074
+ const { left, top, width, height } = rect;
1075
+ const bounds = item.bounds;
1076
+ if (width <= 0 || height <= 0 || !Number.isFinite(bounds.width) || !Number.isFinite(bounds.height) || bounds.width <= 0 || bounds.height <= 0) {
1077
+ item.position = new paper.Point(left + width / 2, top + height / 2);
1078
+ return item;
1028
1079
  }
1029
- /**
1030
- * Normalize a point's coordinates.
1031
- */
1032
- static normalizePoint(point, size) {
1033
- return {
1034
- x: this.toNormalized(point.x, size.width),
1035
- y: this.toNormalized(point.y, size.height)
1036
- };
1080
+ item.translate(new paper.Point(-bounds.left, -bounds.top));
1081
+ if (fitMode === "stretch") {
1082
+ item.scale(width / bounds.width, height / bounds.height, new paper.Point(0, 0));
1083
+ item.translate(new paper.Point(left, top));
1084
+ return item;
1037
1085
  }
1038
- /**
1039
- * Denormalize a point's coordinates to absolute pixels.
1040
- */
1041
- static denormalizePoint(point, size) {
1042
- return {
1043
- x: this.toAbsolute(point.x, size.width),
1044
- y: this.toAbsolute(point.y, size.height)
1045
- };
1086
+ const uniformScale = Math.min(width / bounds.width, height / bounds.height);
1087
+ item.scale(uniformScale, uniformScale, new paper.Point(0, 0));
1088
+ const scaledWidth = bounds.width * uniformScale;
1089
+ const scaledHeight = bounds.height * uniformScale;
1090
+ item.translate(
1091
+ new paper.Point(
1092
+ left + (width - scaledWidth) / 2,
1093
+ top + (height - scaledHeight) / 2
1094
+ )
1095
+ );
1096
+ return item;
1097
+ }
1098
+ function createNormalizedHeartPath(params) {
1099
+ const { lobeSpread, notchDepth, tipSharpness } = params;
1100
+ const halfSpread = 0.22 + lobeSpread * 0.18;
1101
+ const notchY = 0.06 + notchDepth * 0.2;
1102
+ const shoulderY = 0.24 + notchDepth * 0.2;
1103
+ const topLift = 0.12 + (1 - notchDepth) * 0.06;
1104
+ const topY = notchY - topLift;
1105
+ const sideCtrlY = shoulderY - (0.18 - notchDepth * 0.08);
1106
+ const lowerCtrlY = 0.58 + (1 - tipSharpness) * 0.16;
1107
+ const tipCtrlX = 0.34 - tipSharpness * 0.2;
1108
+ const notchCtrlX = 0.06 + lobeSpread * 0.06;
1109
+ const lobeCtrlX = 0.1 + lobeSpread * 0.08;
1110
+ const notchCtrlY = notchY - topLift * 0.45;
1111
+ const xPeakL = 0.5 - halfSpread;
1112
+ const xPeakR = 0.5 + halfSpread;
1113
+ const heartPath = new paper.Path({ insert: false });
1114
+ heartPath.moveTo(new paper.Point(0.5, notchY));
1115
+ heartPath.cubicCurveTo(
1116
+ new paper.Point(0.5 - notchCtrlX, notchCtrlY),
1117
+ new paper.Point(xPeakL + lobeCtrlX, topY),
1118
+ new paper.Point(xPeakL, topY)
1119
+ );
1120
+ heartPath.cubicCurveTo(
1121
+ new paper.Point(xPeakL - lobeCtrlX, topY),
1122
+ new paper.Point(0, sideCtrlY),
1123
+ new paper.Point(0, shoulderY)
1124
+ );
1125
+ heartPath.cubicCurveTo(
1126
+ new paper.Point(0, lowerCtrlY),
1127
+ new paper.Point(tipCtrlX, 1),
1128
+ new paper.Point(0.5, 1)
1129
+ );
1130
+ heartPath.cubicCurveTo(
1131
+ new paper.Point(1 - tipCtrlX, 1),
1132
+ new paper.Point(1, lowerCtrlY),
1133
+ new paper.Point(1, shoulderY)
1134
+ );
1135
+ heartPath.cubicCurveTo(
1136
+ new paper.Point(1, sideCtrlY),
1137
+ new paper.Point(xPeakR + lobeCtrlX, topY),
1138
+ new paper.Point(xPeakR, topY)
1139
+ );
1140
+ heartPath.cubicCurveTo(
1141
+ new paper.Point(xPeakR - lobeCtrlX, topY),
1142
+ new paper.Point(0.5 + notchCtrlX, notchCtrlY),
1143
+ new paper.Point(0.5, notchY)
1144
+ );
1145
+ heartPath.closed = true;
1146
+ return heartPath;
1147
+ }
1148
+ function createHeartBaseShape(options) {
1149
+ const { x, y, width, height } = options;
1150
+ const w = Math.max(0, width);
1151
+ const h = Math.max(0, height);
1152
+ const left = x - w / 2;
1153
+ const top = y - h / 2;
1154
+ const fitMode = getShapeFitMode(options.shapeStyle);
1155
+ const heartParams = getHeartShapeParams(options.shapeStyle);
1156
+ const rawHeart = createNormalizedHeartPath(heartParams);
1157
+ return fitPathItemToRect(rawHeart, { left, top, width: w, height: h }, fitMode);
1158
+ }
1159
+ var BUILTIN_SHAPE_BUILDERS = {
1160
+ rect: (options) => {
1161
+ const { x, y, width, height, radius } = options;
1162
+ return new paper.Path.Rectangle({
1163
+ point: [x - width / 2, y - height / 2],
1164
+ size: [Math.max(0, width), Math.max(0, height)],
1165
+ radius: Math.max(0, radius)
1166
+ });
1167
+ },
1168
+ circle: (options) => {
1169
+ const { x, y, width, height } = options;
1170
+ const r = Math.min(width, height) / 2;
1171
+ return new paper.Path.Circle({
1172
+ center: new paper.Point(x, y),
1173
+ radius: Math.max(0, r)
1174
+ });
1175
+ },
1176
+ ellipse: (options) => {
1177
+ const { x, y, width, height } = options;
1178
+ return new paper.Path.Ellipse({
1179
+ center: new paper.Point(x, y),
1180
+ radius: [Math.max(0, width / 2), Math.max(0, height / 2)]
1181
+ });
1182
+ },
1183
+ heart: createHeartBaseShape
1184
+ };
1185
+ function createCustomBaseShape(options) {
1186
+ var _a;
1187
+ const {
1188
+ pathData,
1189
+ customSourceWidthPx,
1190
+ customSourceHeightPx,
1191
+ x,
1192
+ y,
1193
+ width,
1194
+ height
1195
+ } = options;
1196
+ if (typeof pathData !== "string" || pathData.trim().length === 0) {
1197
+ return null;
1046
1198
  }
1047
- static convertUnit(value, from, to) {
1048
- if (from === to) return value;
1049
- const toMM = {
1050
- px: 0.264583,
1051
- // 1px = 0.264583mm (96 DPI)
1052
- mm: 1,
1053
- cm: 10,
1054
- in: 25.4
1055
- };
1056
- const mmValue = value * (from === "px" ? toMM.px : toMM[from] || 1);
1057
- if (to === "px") {
1058
- return mmValue / toMM.px;
1059
- }
1060
- return mmValue / (toMM[to] || 1);
1199
+ const center = new paper.Point(x, y);
1200
+ const hasMultipleSubPaths = ((_a = (pathData.match(/[Mm]/g) || []).length) != null ? _a : 0) > 1;
1201
+ const path = hasMultipleSubPaths ? new paper.CompoundPath(pathData) : (() => {
1202
+ const single = new paper.Path();
1203
+ single.pathData = pathData;
1204
+ return single;
1205
+ })();
1206
+ const sourceWidth = Number(customSourceWidthPx != null ? customSourceWidthPx : 0);
1207
+ const sourceHeight = Number(customSourceHeightPx != null ? customSourceHeightPx : 0);
1208
+ if (Number.isFinite(sourceWidth) && Number.isFinite(sourceHeight) && sourceWidth > 0 && sourceHeight > 0 && width > 0 && height > 0) {
1209
+ const targetLeft = x - width / 2;
1210
+ const targetTop = y - height / 2;
1211
+ path.scale(width / sourceWidth, height / sourceHeight, new paper.Point(0, 0));
1212
+ path.translate(new paper.Point(targetLeft, targetTop));
1213
+ return path;
1061
1214
  }
1062
- };
1063
-
1064
- // src/units.ts
1065
- function parseLengthToMm(input, defaultUnit) {
1066
- var _a, _b;
1067
- if (typeof input === "number") {
1068
- if (!Number.isFinite(input)) return 0;
1069
- return Coordinate.convertUnit(input, defaultUnit, "mm");
1215
+ if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
1216
+ path.position = center;
1217
+ path.scale(width / path.bounds.width, height / path.bounds.height);
1218
+ return path;
1070
1219
  }
1071
- const raw = input.trim();
1072
- if (!raw) return 0;
1073
- const match = raw.match(/^([+-]?\d+(?:\.\d+)?)\s*(px|mm|cm|in)?$/i);
1074
- if (!match) return 0;
1075
- const value = Number(match[1]);
1076
- if (!Number.isFinite(value)) return 0;
1077
- const unit = (_b = (_a = match[2]) == null ? void 0 : _a.toLowerCase()) != null ? _b : defaultUnit;
1078
- return Coordinate.convertUnit(value, unit, "mm");
1079
- }
1080
-
1081
- // src/extensions/sceneLayoutModel.ts
1082
- var DEFAULT_SIZE_STATE = {
1083
- unit: "mm",
1084
- actualWidthMm: 500,
1085
- actualHeightMm: 500,
1086
- constraintMode: "free",
1087
- aspectRatio: 1,
1088
- cutMode: "trim",
1089
- cutMarginMm: 0,
1090
- viewPadding: 140,
1091
- minMm: 10,
1092
- maxMm: 2e3,
1093
- stepMm: 0.1
1094
- };
1095
- function clamp(value, min, max) {
1096
- return Math.max(min, Math.min(max, value));
1097
- }
1098
- function roundToStep(value, step) {
1099
- if (!Number.isFinite(step) || step <= 0) return value;
1100
- return Math.round(value / step) * step;
1101
- }
1102
- function sanitizeMmValue(valueMm, limits) {
1103
- if (!Number.isFinite(valueMm)) return limits.minMm;
1104
- const rounded = roundToStep(valueMm, limits.stepMm);
1105
- return clamp(rounded, limits.minMm, limits.maxMm);
1106
- }
1107
- function normalizeUnit(value) {
1108
- if (value === "cm" || value === "in") return value;
1109
- return "mm";
1110
- }
1111
- function normalizeConstraintMode(value) {
1112
- if (value === "lockAspect" || value === "equal") return value;
1113
- return "free";
1220
+ path.position = center;
1221
+ return path;
1114
1222
  }
1115
- function normalizeCutMode(value) {
1116
- if (value === "outset" || value === "inset") return value;
1117
- return "trim";
1223
+ function createBaseShape(options) {
1224
+ const { shape } = options;
1225
+ if (shape === "custom") {
1226
+ const customShape = createCustomBaseShape(options);
1227
+ if (customShape) return customShape;
1228
+ return BUILTIN_SHAPE_BUILDERS[DEFAULT_DIELINE_SHAPE](options);
1229
+ }
1230
+ return BUILTIN_SHAPE_BUILDERS[shape](options);
1118
1231
  }
1119
- function toMm(value, fromUnit) {
1120
- return Coordinate.convertUnit(value, fromUnit, "mm");
1232
+ function resolveBridgeBasePath(shape, anchor) {
1233
+ if (shape instanceof paper.Path) {
1234
+ return shape;
1235
+ }
1236
+ if (shape instanceof paper.CompoundPath) {
1237
+ const children = (shape.children || []).filter(
1238
+ (child) => child instanceof paper.Path
1239
+ );
1240
+ if (!children.length) return null;
1241
+ let best = children[0];
1242
+ let bestDistance = Infinity;
1243
+ for (const child of children) {
1244
+ const location = child.getNearestLocation(anchor);
1245
+ const point = location == null ? void 0 : location.point;
1246
+ if (!point) continue;
1247
+ const distance = point.getDistance(anchor);
1248
+ if (distance < bestDistance) {
1249
+ bestDistance = distance;
1250
+ best = child;
1251
+ }
1252
+ }
1253
+ return best;
1254
+ }
1255
+ return null;
1121
1256
  }
1122
- function fromMm(valueMm, toUnit) {
1123
- return Coordinate.convertUnit(valueMm, "mm", toUnit);
1257
+ function createFeatureItem(feature, center) {
1258
+ let item;
1259
+ if (feature.shape === "rect") {
1260
+ const w = feature.width || 10;
1261
+ const h = feature.height || 10;
1262
+ const r = feature.radius || 0;
1263
+ item = new paper.Path.Rectangle({
1264
+ point: [center.x - w / 2, center.y - h / 2],
1265
+ size: [w, h],
1266
+ radius: r
1267
+ });
1268
+ } else {
1269
+ const r = feature.radius || 5;
1270
+ item = new paper.Path.Circle({
1271
+ center,
1272
+ radius: r
1273
+ });
1274
+ }
1275
+ if (feature.rotation) {
1276
+ item.rotate(feature.rotation, center);
1277
+ }
1278
+ return item;
1124
1279
  }
1125
- function resolvePaddingPx(raw, containerWidth, containerHeight) {
1126
- if (typeof raw === "number") return Math.max(0, raw);
1127
- if (typeof raw === "string") {
1128
- if (raw.endsWith("%")) {
1129
- const percent = parseFloat(raw) / 100;
1130
- if (!Number.isFinite(percent)) return 0;
1131
- return Math.max(0, Math.min(containerWidth, containerHeight) * percent);
1280
+ function getPerimeterShape(options) {
1281
+ let mainShape = createBaseShape(options);
1282
+ const { features } = options;
1283
+ if (features && features.length > 0) {
1284
+ const edgeFeatures = features.filter(
1285
+ (f) => !f.renderBehavior || f.renderBehavior === "edge"
1286
+ );
1287
+ const adds = [];
1288
+ const subtracts = [];
1289
+ edgeFeatures.forEach((f) => {
1290
+ const pos = resolveFeaturePosition(f, options);
1291
+ const center = new paper.Point(pos.x, pos.y);
1292
+ const item = createFeatureItem(f, center);
1293
+ if (f.bridge && f.bridge.type === "vertical") {
1294
+ const itemBounds = item.bounds;
1295
+ const mainBounds = mainShape.bounds;
1296
+ const bridgeTop = mainBounds.top;
1297
+ const bridgeBottom = itemBounds.top;
1298
+ if (bridgeBottom > bridgeTop) {
1299
+ const overlap = 2;
1300
+ const rayPadding = 10;
1301
+ const eps = 0.1;
1302
+ const delta = getBridgeDelta(itemBounds, overlap);
1303
+ const toY = bridgeTop - rayPadding;
1304
+ const inset = Math.min(1, Math.max(0, itemBounds.width * 0.01));
1305
+ const xLeft = itemBounds.left + inset;
1306
+ const xRight = itemBounds.right - inset;
1307
+ const bridgeBasePath = resolveBridgeBasePath(mainShape, center);
1308
+ const canBridge = !!bridgeBasePath && xRight - xLeft > eps;
1309
+ if (canBridge && bridgeBasePath) {
1310
+ const leftHit = getExitHit({
1311
+ mainShape: bridgeBasePath,
1312
+ x: xLeft,
1313
+ bridgeBottom,
1314
+ toY,
1315
+ eps,
1316
+ delta,
1317
+ overlap,
1318
+ op: f.operation
1319
+ });
1320
+ const rightHit = getExitHit({
1321
+ mainShape: bridgeBasePath,
1322
+ x: xRight,
1323
+ bridgeBottom,
1324
+ toY,
1325
+ eps,
1326
+ delta,
1327
+ overlap,
1328
+ op: f.operation
1329
+ });
1330
+ if (leftHit && rightHit) {
1331
+ const pathLength = bridgeBasePath.length;
1332
+ const leftOffset = leftHit.location.offset;
1333
+ const rightOffset = rightHit.location.offset;
1334
+ const distanceA = wrappedDistance(pathLength, leftOffset, rightOffset);
1335
+ const distanceB = wrappedDistance(pathLength, rightOffset, leftOffset);
1336
+ const countFor = (d) => Math.max(8, Math.min(80, Math.ceil(d / 6)));
1337
+ const offsetsA = sampleWrappedOffsets(
1338
+ pathLength,
1339
+ leftOffset,
1340
+ rightOffset,
1341
+ countFor(distanceA)
1342
+ );
1343
+ const offsetsB = sampleWrappedOffsets(
1344
+ pathLength,
1345
+ rightOffset,
1346
+ leftOffset,
1347
+ countFor(distanceB)
1348
+ );
1349
+ const pointsA = offsetsA.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
1350
+ const pointsB = offsetsB.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
1351
+ if (pointsA.length >= 2 && pointsB.length >= 2) {
1352
+ let topBase = selectOuterChain({
1353
+ mainShape: bridgeBasePath,
1354
+ pointsA,
1355
+ pointsB,
1356
+ delta,
1357
+ overlap,
1358
+ op: f.operation
1359
+ });
1360
+ const dist2 = (a, b) => {
1361
+ const dx = a.x - b.x;
1362
+ const dy = a.y - b.y;
1363
+ return dx * dx + dy * dy;
1364
+ };
1365
+ if (dist2(topBase[0], leftHit.point) > dist2(topBase[0], rightHit.point)) {
1366
+ topBase = topBase.slice().reverse();
1367
+ }
1368
+ topBase = topBase.slice();
1369
+ topBase[0] = leftHit.point;
1370
+ topBase[topBase.length - 1] = rightHit.point;
1371
+ const capShiftY = f.operation === "subtract" ? -Math.max(overlap * 2, delta) : overlap;
1372
+ const topPoints = topBase.map(
1373
+ (p) => p.add(new paper.Point(0, capShiftY))
1374
+ );
1375
+ const bridgeBottomY = bridgeBottom + overlap * 2;
1376
+ const bridgePoly = new paper.Path({ insert: false });
1377
+ for (const p of topPoints) bridgePoly.add(p);
1378
+ bridgePoly.add(new paper.Point(xRight, bridgeBottomY));
1379
+ bridgePoly.add(new paper.Point(xLeft, bridgeBottomY));
1380
+ bridgePoly.closed = true;
1381
+ const unitedItem = item.unite(bridgePoly);
1382
+ item.remove();
1383
+ bridgePoly.remove();
1384
+ if (f.operation === "add") {
1385
+ adds.push(unitedItem);
1386
+ } else {
1387
+ subtracts.push(unitedItem);
1388
+ }
1389
+ return;
1390
+ }
1391
+ }
1392
+ }
1393
+ if (f.operation === "add") {
1394
+ adds.push(item);
1395
+ } else {
1396
+ subtracts.push(item);
1397
+ }
1398
+ } else {
1399
+ if (f.operation === "add") {
1400
+ adds.push(item);
1401
+ } else {
1402
+ subtracts.push(item);
1403
+ }
1404
+ }
1405
+ } else {
1406
+ if (f.operation === "add") {
1407
+ adds.push(item);
1408
+ } else {
1409
+ subtracts.push(item);
1410
+ }
1411
+ }
1412
+ });
1413
+ if (adds.length > 0) {
1414
+ for (const item of adds) {
1415
+ try {
1416
+ const temp = mainShape.unite(item);
1417
+ mainShape.remove();
1418
+ item.remove();
1419
+ mainShape = normalizePathItem(temp);
1420
+ } catch (e) {
1421
+ console.error("Geometry: Failed to unite feature", e);
1422
+ item.remove();
1423
+ }
1424
+ }
1425
+ }
1426
+ if (subtracts.length > 0) {
1427
+ for (const item of subtracts) {
1428
+ try {
1429
+ const temp = mainShape.subtract(item);
1430
+ mainShape.remove();
1431
+ item.remove();
1432
+ mainShape = normalizePathItem(temp);
1433
+ } catch (e) {
1434
+ console.error("Geometry: Failed to subtract feature", e);
1435
+ item.remove();
1436
+ }
1437
+ }
1132
1438
  }
1133
- const fixed = parseFloat(raw);
1134
- return Number.isFinite(fixed) ? Math.max(0, fixed) : 0;
1135
1439
  }
1136
- return 0;
1440
+ return mainShape;
1137
1441
  }
1138
- function readSizeState(configService) {
1139
- const unit = normalizeUnit(
1140
- configService.get("size.unit", DEFAULT_SIZE_STATE.unit)
1141
- );
1142
- const minMm = Math.max(
1143
- 0.1,
1144
- Number(configService.get("size.minMm", DEFAULT_SIZE_STATE.minMm))
1145
- );
1146
- const maxMm = Math.max(
1147
- minMm,
1148
- Number(configService.get("size.maxMm", DEFAULT_SIZE_STATE.maxMm))
1149
- );
1150
- const stepMm = Math.max(
1151
- 1e-3,
1152
- Number(configService.get("size.stepMm", DEFAULT_SIZE_STATE.stepMm))
1153
- );
1154
- const actualWidthMm = sanitizeMmValue(
1155
- parseLengthToMm(
1156
- configService.get("size.actualWidthMm", DEFAULT_SIZE_STATE.actualWidthMm),
1157
- "mm"
1158
- ),
1159
- { minMm, maxMm, stepMm }
1160
- );
1161
- const actualHeightMm = sanitizeMmValue(
1162
- parseLengthToMm(
1163
- configService.get(
1164
- "size.actualHeightMm",
1165
- DEFAULT_SIZE_STATE.actualHeightMm
1166
- ),
1167
- "mm"
1168
- ),
1169
- { minMm, maxMm, stepMm }
1170
- );
1171
- const aspectRaw = Number(
1172
- configService.get("size.aspectRatio", DEFAULT_SIZE_STATE.aspectRatio)
1173
- );
1174
- const aspectRatio = Number.isFinite(aspectRaw) && aspectRaw > 0 ? aspectRaw : actualWidthMm / Math.max(1e-3, actualHeightMm);
1175
- const cutMarginMm = Math.max(
1176
- 0,
1177
- parseLengthToMm(
1178
- configService.get("size.cutMarginMm", DEFAULT_SIZE_STATE.cutMarginMm),
1179
- "mm"
1180
- )
1181
- );
1182
- const viewPadding = configService.get(
1183
- "size.viewPadding",
1184
- DEFAULT_SIZE_STATE.viewPadding
1442
+ function applySurfaceFeatures(shape, features, options) {
1443
+ const surfaceFeatures = features.filter(
1444
+ (f) => f.renderBehavior === "surface"
1185
1445
  );
1186
- return {
1187
- unit,
1188
- actualWidthMm,
1189
- actualHeightMm,
1190
- constraintMode: normalizeConstraintMode(
1191
- configService.get(
1192
- "size.constraintMode",
1193
- DEFAULT_SIZE_STATE.constraintMode
1194
- )
1195
- ),
1196
- aspectRatio,
1197
- cutMode: normalizeCutMode(
1198
- configService.get("size.cutMode", DEFAULT_SIZE_STATE.cutMode)
1199
- ),
1200
- cutMarginMm,
1201
- viewPadding,
1202
- minMm,
1203
- maxMm,
1204
- stepMm
1205
- };
1446
+ if (surfaceFeatures.length === 0) return shape;
1447
+ let result = shape;
1448
+ for (const f of surfaceFeatures) {
1449
+ const pos = resolveFeaturePosition(f, options);
1450
+ const center = new paper.Point(pos.x, pos.y);
1451
+ const item = createFeatureItem(f, center);
1452
+ try {
1453
+ if (f.operation === "add") {
1454
+ const temp = result.unite(item);
1455
+ result.remove();
1456
+ item.remove();
1457
+ result = normalizePathItem(temp);
1458
+ } else {
1459
+ const temp = result.subtract(item);
1460
+ result.remove();
1461
+ item.remove();
1462
+ result = normalizePathItem(temp);
1463
+ }
1464
+ } catch (e) {
1465
+ console.error("Geometry: Failed to apply surface feature", e);
1466
+ item.remove();
1467
+ }
1468
+ }
1469
+ return result;
1206
1470
  }
1207
- function rectByCenter(centerX, centerY, width, height) {
1208
- return {
1209
- left: centerX - width / 2,
1210
- top: centerY - height / 2,
1211
- width,
1212
- height,
1213
- centerX,
1214
- centerY
1215
- };
1471
+ function generateDielinePath(options) {
1472
+ const paperWidth = options.canvasWidth || options.width * 2 || 2e3;
1473
+ const paperHeight = options.canvasHeight || options.height * 2 || 2e3;
1474
+ ensurePaper(paperWidth, paperHeight);
1475
+ paper.project.activeLayer.removeChildren();
1476
+ const perimeter = getPerimeterShape(options);
1477
+ const finalShape = applySurfaceFeatures(perimeter, options.features, options);
1478
+ const pathData = finalShape.pathData;
1479
+ finalShape.remove();
1480
+ return pathData;
1216
1481
  }
1217
- function getCutSizeMm(size) {
1218
- if (size.cutMode === "trim") {
1219
- return { widthMm: size.actualWidthMm, heightMm: size.actualHeightMm };
1220
- }
1221
- const delta = size.cutMarginMm * 2;
1222
- if (size.cutMode === "outset") {
1223
- return {
1224
- widthMm: size.actualWidthMm + delta,
1225
- heightMm: size.actualHeightMm + delta
1226
- };
1482
+ function generateBleedZonePath(originalOptions, offsetOptions, offset) {
1483
+ const paperWidth = originalOptions.canvasWidth || originalOptions.width * 2 || 2e3;
1484
+ const paperHeight = originalOptions.canvasHeight || originalOptions.height * 2 || 2e3;
1485
+ ensurePaper(paperWidth, paperHeight);
1486
+ paper.project.activeLayer.removeChildren();
1487
+ const pOriginal = getPerimeterShape(originalOptions);
1488
+ const shapeOriginal = applySurfaceFeatures(
1489
+ pOriginal,
1490
+ originalOptions.features,
1491
+ originalOptions
1492
+ );
1493
+ const pOffset = getPerimeterShape(offsetOptions);
1494
+ const shapeOffset = applySurfaceFeatures(
1495
+ pOffset,
1496
+ offsetOptions.features,
1497
+ offsetOptions
1498
+ );
1499
+ let bleedZone;
1500
+ if (offset > 0) {
1501
+ bleedZone = shapeOffset.subtract(shapeOriginal);
1502
+ } else {
1503
+ bleedZone = shapeOriginal.subtract(shapeOffset);
1227
1504
  }
1228
- return {
1229
- widthMm: Math.max(size.minMm, size.actualWidthMm - delta),
1230
- heightMm: Math.max(size.minMm, size.actualHeightMm - delta)
1505
+ const pathData = bleedZone.pathData;
1506
+ shapeOriginal.remove();
1507
+ shapeOffset.remove();
1508
+ bleedZone.remove();
1509
+ return pathData;
1510
+ }
1511
+ function getLowestPointOnDieline(options) {
1512
+ ensurePaper(options.width * 2, options.height * 2);
1513
+ paper.project.activeLayer.removeChildren();
1514
+ const shape = createBaseShape(options);
1515
+ const bounds = shape.bounds;
1516
+ const result = {
1517
+ x: bounds.center.x,
1518
+ y: bounds.bottom
1231
1519
  };
1520
+ shape.remove();
1521
+ return result;
1232
1522
  }
1233
- function computeSceneLayout(canvasService, size) {
1234
- const canvasWidth = canvasService.canvas.width || 0;
1235
- const canvasHeight = canvasService.canvas.height || 0;
1236
- if (canvasWidth <= 0 || canvasHeight <= 0) return null;
1237
- const { widthMm: cutWidthMm, heightMm: cutHeightMm } = getCutSizeMm(size);
1238
- const viewWidthMm = Math.max(size.actualWidthMm, cutWidthMm);
1239
- const viewHeightMm = Math.max(size.actualHeightMm, cutHeightMm);
1240
- if (!Number.isFinite(viewWidthMm) || !Number.isFinite(viewHeightMm) || viewWidthMm <= 0 || viewHeightMm <= 0) {
1241
- return null;
1242
- }
1243
- const paddingPx = resolvePaddingPx(
1244
- size.viewPadding,
1245
- canvasWidth,
1246
- canvasHeight
1247
- );
1248
- canvasService.viewport.updateContainer(canvasWidth, canvasHeight);
1249
- canvasService.viewport.setPadding(paddingPx);
1250
- canvasService.viewport.updatePhysical(viewWidthMm, viewHeightMm);
1251
- const layout = canvasService.viewport.layout;
1252
- if (!Number.isFinite(layout.scale) || !Number.isFinite(layout.offsetX) || !Number.isFinite(layout.offsetY) || layout.scale <= 0) {
1253
- return null;
1254
- }
1255
- const centerX = layout.offsetX + layout.width / 2;
1256
- const centerY = layout.offsetY + layout.height / 2;
1257
- const trimWidthPx = size.actualWidthMm * layout.scale;
1258
- const trimHeightPx = size.actualHeightMm * layout.scale;
1259
- const cutWidthPx = cutWidthMm * layout.scale;
1260
- const cutHeightPx = cutHeightMm * layout.scale;
1261
- const trimRect = rectByCenter(centerX, centerY, trimWidthPx, trimHeightPx);
1262
- const cutRect = rectByCenter(centerX, centerY, cutWidthPx, cutHeightPx);
1263
- const bleedRect = rectByCenter(
1264
- centerX,
1265
- centerY,
1266
- Math.max(trimWidthPx, cutWidthPx),
1267
- Math.max(trimHeightPx, cutHeightPx)
1268
- );
1269
- return {
1270
- scale: layout.scale,
1271
- canvasWidth,
1272
- canvasHeight,
1273
- trimRect,
1274
- cutRect,
1275
- bleedRect,
1276
- trimWidthMm: size.actualWidthMm,
1277
- trimHeightMm: size.actualHeightMm,
1278
- cutWidthMm,
1279
- cutHeightMm,
1280
- cutMode: size.cutMode,
1281
- cutMarginMm: size.cutMarginMm
1523
+ function getNearestPointOnDieline(point, options) {
1524
+ ensurePaper(options.width * 2, options.height * 2);
1525
+ paper.project.activeLayer.removeChildren();
1526
+ const shape = createBaseShape(options);
1527
+ const p = new paper.Point(point.x, point.y);
1528
+ const location = shape.getNearestLocation(p);
1529
+ const result = {
1530
+ x: location.point.x,
1531
+ y: location.point.y,
1532
+ normal: location.normal ? { x: location.normal.x, y: location.normal.y } : void 0
1282
1533
  };
1534
+ shape.remove();
1535
+ return result;
1283
1536
  }
1284
- function buildSceneGeometry(configService, layout) {
1285
- const radiusMm = parseLengthToMm(
1286
- configService.get("dieline.radius", 0),
1287
- "mm"
1288
- );
1289
- const offset = (layout.cutRect.width - layout.trimRect.width) / 2;
1290
- const sourceWidth = Number(configService.get("dieline.customSourceWidthPx", 0));
1291
- const sourceHeight = Number(
1292
- configService.get("dieline.customSourceHeightPx", 0)
1293
- );
1294
- const shapeStyle = normalizeShapeStyle(
1295
- configService.get("dieline.shapeStyle", DEFAULT_DIELINE_SHAPE_STYLE)
1296
- );
1537
+ function getPathBounds(pathData) {
1538
+ const path = new paper.Path();
1539
+ path.pathData = pathData;
1540
+ const bounds = path.bounds;
1541
+ path.remove();
1297
1542
  return {
1298
- shape: normalizeDielineShape(
1299
- configService.get("dieline.shape", DEFAULT_DIELINE_SHAPE)
1300
- ),
1301
- shapeStyle,
1302
- unit: "px",
1303
- x: layout.trimRect.centerX,
1304
- y: layout.trimRect.centerY,
1305
- width: layout.trimRect.width,
1306
- height: layout.trimRect.height,
1307
- radius: radiusMm * layout.scale,
1308
- offset,
1309
- scale: layout.scale,
1310
- pathData: configService.get("dieline.pathData"),
1311
- customSourceWidthPx: Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : void 0,
1312
- customSourceHeightPx: Number.isFinite(sourceHeight) && sourceHeight > 0 ? sourceHeight : void 0
1543
+ x: bounds.x,
1544
+ y: bounds.y,
1545
+ width: bounds.width,
1546
+ height: bounds.height
1313
1547
  };
1314
1548
  }
1315
1549
 
@@ -1332,6 +1566,7 @@ var ImageTool = class {
1332
1566
  this.isImageSelectionActive = false;
1333
1567
  this.focusedImageId = null;
1334
1568
  this.renderSeq = 0;
1569
+ this.imageSpecs = [];
1335
1570
  this.overlaySpecs = [];
1336
1571
  this.onToolActivated = (event) => {
1337
1572
  const before = this.isToolActive;
@@ -1439,9 +1674,34 @@ var ImageTool = class {
1439
1674
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
1440
1675
  this.id,
1441
1676
  () => ({
1442
- rootLayerSpecs: {
1443
- [IMAGE_OVERLAY_LAYER_ID]: this.overlaySpecs
1444
- }
1677
+ passes: [
1678
+ {
1679
+ id: IMAGE_OBJECT_LAYER_ID,
1680
+ stack: 500,
1681
+ order: 0,
1682
+ visibility: {
1683
+ op: "not",
1684
+ expr: {
1685
+ op: "sessionActive",
1686
+ toolId: "pooder.kit.white-ink"
1687
+ }
1688
+ },
1689
+ objects: this.imageSpecs
1690
+ },
1691
+ {
1692
+ id: IMAGE_OVERLAY_LAYER_ID,
1693
+ stack: 800,
1694
+ order: 0,
1695
+ visibility: {
1696
+ op: "not",
1697
+ expr: {
1698
+ op: "sessionActive",
1699
+ toolId: "pooder.kit.white-ink"
1700
+ }
1701
+ },
1702
+ objects: this.overlaySpecs
1703
+ }
1704
+ ]
1445
1705
  }),
1446
1706
  { priority: 300 }
1447
1707
  );
@@ -1498,6 +1758,7 @@ var ImageTool = class {
1498
1758
  this.cropShapeHatchPattern = void 0;
1499
1759
  this.cropShapeHatchPatternColor = void 0;
1500
1760
  this.cropShapeHatchPatternKey = void 0;
1761
+ this.imageSpecs = [];
1501
1762
  this.overlaySpecs = [];
1502
1763
  this.clearRenderedImages();
1503
1764
  (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
@@ -1973,9 +2234,7 @@ var ImageTool = class {
1973
2234
  }
1974
2235
  getOverlayObjects() {
1975
2236
  if (!this.canvasService) return [];
1976
- return this.canvasService.getRootLayerObjects(
1977
- IMAGE_OVERLAY_LAYER_ID
1978
- );
2237
+ return this.canvasService.getPassObjects(IMAGE_OVERLAY_LAYER_ID);
1979
2238
  }
1980
2239
  getImageObject(id) {
1981
2240
  return this.getImageObjects().find((obj) => {
@@ -1985,9 +2244,9 @@ var ImageTool = class {
1985
2244
  }
1986
2245
  clearRenderedImages() {
1987
2246
  if (!this.canvasService) return;
1988
- const canvas = this.canvasService.canvas;
1989
- this.getImageObjects().forEach((obj) => canvas.remove(obj));
1990
- this.canvasService.requestRenderAll();
2247
+ this.imageSpecs = [];
2248
+ this.overlaySpecs = [];
2249
+ this.canvasService.requestRenderFromProducers();
1991
2250
  }
1992
2251
  purgeSourceSizeCacheForItem(item) {
1993
2252
  if (!item) return;
@@ -2015,6 +2274,29 @@ var ImageTool = class {
2015
2274
  }
2016
2275
  return { width: 1, height: 1 };
2017
2276
  }
2277
+ async ensureSourceSize(src) {
2278
+ if (!src) return null;
2279
+ const cached = this.sourceSizeBySrc.get(src);
2280
+ if (cached) return cached;
2281
+ try {
2282
+ const image = await FabricImage2.fromURL(src, {
2283
+ crossOrigin: "anonymous"
2284
+ });
2285
+ const width = Number((image == null ? void 0 : image.width) || 0);
2286
+ const height = Number((image == null ? void 0 : image.height) || 0);
2287
+ if (width > 0 && height > 0) {
2288
+ const size = { width, height };
2289
+ this.sourceSizeBySrc.set(src, size);
2290
+ return size;
2291
+ }
2292
+ } catch (error) {
2293
+ this.debug("image:size:load-failed", {
2294
+ src,
2295
+ error: error instanceof Error ? error.message : String(error)
2296
+ });
2297
+ }
2298
+ return null;
2299
+ }
2018
2300
  getCoverScale(frame, size) {
2019
2301
  const sw = Math.max(1, size.width);
2020
2302
  const sh = Math.max(1, size.height);
@@ -2349,24 +2631,6 @@ var ImageTool = class {
2349
2631
  opacity: render.opacity
2350
2632
  };
2351
2633
  }
2352
- toScreenObjectProps(props) {
2353
- if (!this.canvasService) return props;
2354
- const next = { ...props };
2355
- if (Number.isFinite(next.left) || Number.isFinite(next.top)) {
2356
- const mapped = this.canvasService.toScreenPoint({
2357
- x: Number.isFinite(next.left) ? Number(next.left) : 0,
2358
- y: Number.isFinite(next.top) ? Number(next.top) : 0
2359
- });
2360
- if (Number.isFinite(next.left)) next.left = mapped.x;
2361
- if (Number.isFinite(next.top)) next.top = mapped.y;
2362
- }
2363
- const sceneScale = this.canvasService.getSceneScale();
2364
- const sx = Number.isFinite(next.scaleX) ? Number(next.scaleX) : 1;
2365
- const sy = Number.isFinite(next.scaleY) ? Number(next.scaleY) : 1;
2366
- next.scaleX = sx * sceneScale;
2367
- next.scaleY = sy * sceneScale;
2368
- return next;
2369
- }
2370
2634
  toSceneObjectScale(value) {
2371
2635
  if (!this.canvasService) return value;
2372
2636
  return value / this.canvasService.getSceneScale();
@@ -2377,104 +2641,27 @@ var ImageTool = class {
2377
2641
  if (typeof obj.getSrc === "function") return obj.getSrc();
2378
2642
  return (_a = obj == null ? void 0 : obj._originalElement) == null ? void 0 : _a.src;
2379
2643
  }
2380
- applyImageControlVisibility(obj) {
2381
- if (typeof (obj == null ? void 0 : obj.setControlsVisibility) !== "function") return;
2382
- obj.setControlsVisibility({
2383
- mt: false,
2384
- mb: false,
2385
- ml: false,
2386
- mr: false,
2387
- tl: true,
2388
- tr: true,
2389
- bl: true,
2390
- br: true,
2391
- mtr: true
2392
- });
2393
- }
2394
- async upsertImageObject(item, frame, seq) {
2395
- if (!this.canvasService) return;
2396
- const canvas = this.canvasService.canvas;
2397
- const render = this.resolveRenderImageState(item);
2398
- if (!render.src) return;
2399
- let obj = this.getImageObject(item.id);
2400
- const currentSrc = this.getCurrentSrc(obj);
2401
- if (obj && currentSrc && currentSrc !== render.src) {
2402
- canvas.remove(obj);
2403
- obj = void 0;
2404
- }
2405
- if (!obj) {
2406
- const created = await FabricImage2.fromURL(render.src, {
2407
- crossOrigin: "anonymous"
2408
- });
2409
- if (seq !== this.renderSeq) return;
2410
- created.set({
2644
+ async buildImageSpecs(items, frame) {
2645
+ const specs = [];
2646
+ for (const item of items) {
2647
+ const render = this.resolveRenderImageState(item);
2648
+ if (!render.src) continue;
2649
+ const ensured = await this.ensureSourceSize(render.src);
2650
+ const sourceSize = ensured || this.getSourceSize(render.src);
2651
+ const props = this.computeCanvasProps(render, sourceSize, frame);
2652
+ specs.push({
2653
+ id: item.id,
2654
+ type: "image",
2655
+ src: render.src,
2411
2656
  data: {
2412
2657
  id: item.id,
2413
2658
  layerId: IMAGE_OBJECT_LAYER_ID,
2414
2659
  type: "image-item"
2415
- }
2660
+ },
2661
+ props
2416
2662
  });
2417
- canvas.add(created);
2418
- obj = created;
2419
- }
2420
- this.rememberSourceSize(render.src, obj);
2421
- const sourceSize = this.getSourceSize(render.src, obj);
2422
- const props = this.computeCanvasProps(render, sourceSize, frame);
2423
- const screenProps = this.toScreenObjectProps(props);
2424
- obj.set({
2425
- ...screenProps,
2426
- data: {
2427
- ...obj.data || {},
2428
- id: item.id,
2429
- layerId: IMAGE_OBJECT_LAYER_ID,
2430
- type: "image-item"
2431
- }
2432
- });
2433
- this.applyImageControlVisibility(obj);
2434
- obj.setCoords();
2435
- const resolver = this.loadResolvers.get(item.id);
2436
- if (resolver) {
2437
- resolver();
2438
- this.loadResolvers.delete(item.id);
2439
- }
2440
- }
2441
- syncImageZOrder(items) {
2442
- if (!this.canvasService) return;
2443
- const canvas = this.canvasService.canvas;
2444
- const objects = canvas.getObjects();
2445
- let insertIndex = 0;
2446
- const backgroundLayer = this.canvasService.getLayer("background");
2447
- if (backgroundLayer) {
2448
- const bgIndex = objects.indexOf(backgroundLayer);
2449
- if (bgIndex >= 0) insertIndex = bgIndex + 1;
2450
- }
2451
- items.forEach((item) => {
2452
- const obj = this.getImageObject(item.id);
2453
- if (!obj) return;
2454
- canvas.moveObjectTo(obj, insertIndex);
2455
- insertIndex += 1;
2456
- });
2457
- const overlayObjects = this.getOverlayObjects().sort((a, b) => {
2458
- var _a, _b, _c, _d;
2459
- const az = Number((_b = (_a = a == null ? void 0 : a.data) == null ? void 0 : _a.zIndex) != null ? _b : 0);
2460
- const bz = Number((_d = (_c = b == null ? void 0 : b.data) == null ? void 0 : _c.zIndex) != null ? _d : 0);
2461
- return az - bz;
2462
- });
2463
- overlayObjects.forEach((obj) => {
2464
- canvas.bringObjectToFront(obj);
2465
- });
2466
- if (this.isDebugEnabled()) {
2467
- const stack = canvas.getObjects().map((obj, index) => {
2468
- var _a, _b, _c;
2469
- return {
2470
- index,
2471
- id: (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id,
2472
- layerId: (_b = obj == null ? void 0 : obj.data) == null ? void 0 : _b.layerId,
2473
- zIndex: (_c = obj == null ? void 0 : obj.data) == null ? void 0 : _c.zIndex
2474
- };
2475
- }).filter((item) => item.layerId === IMAGE_OVERLAY_LAYER_ID);
2476
- this.debug("overlay:stack", stack);
2477
2663
  }
2664
+ return specs;
2478
2665
  }
2479
2666
  buildOverlaySpecs(frame, sceneGeometry) {
2480
2667
  const visible = this.isImageEditingVisible();
@@ -2638,7 +2825,7 @@ var ImageTool = class {
2638
2825
  evented: false
2639
2826
  }
2640
2827
  };
2641
- const specs = [...mask, ...shapeOverlay, frameSpec];
2828
+ const specs = shapeOverlay.length > 0 ? [...mask, ...shapeOverlay] : [...mask, ...shapeOverlay, frameSpec];
2642
2829
  this.debug("overlay:built", {
2643
2830
  frame,
2644
2831
  shape: sceneGeometry == null ? void 0 : sceneGeometry.shape,
@@ -2668,30 +2855,32 @@ var ImageTool = class {
2668
2855
  skipRender: true
2669
2856
  });
2670
2857
  }
2671
- this.getImageObjects().forEach((obj) => {
2672
- var _a, _b;
2673
- const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
2674
- if (typeof id === "string" && !desiredIds.has(id)) {
2675
- (_b = this.canvasService) == null ? void 0 : _b.canvas.remove(obj);
2676
- }
2677
- });
2678
- for (const item of renderItems) {
2679
- if (seq !== this.renderSeq) return;
2680
- await this.upsertImageObject(item, frame, seq);
2681
- }
2858
+ const imageSpecs = await this.buildImageSpecs(renderItems, frame);
2682
2859
  if (seq !== this.renderSeq) return;
2683
- this.syncImageZOrder(renderItems);
2684
2860
  const sceneGeometry = await this.resolveSceneGeometryForOverlay();
2685
2861
  if (seq !== this.renderSeq) return;
2686
- const overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
2687
- this.overlaySpecs = overlaySpecs;
2862
+ this.imageSpecs = imageSpecs;
2863
+ this.overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
2688
2864
  await this.canvasService.flushRenderFromProducers();
2689
- this.syncImageZOrder(renderItems);
2865
+ if (seq !== this.renderSeq) return;
2866
+ renderItems.forEach((item) => {
2867
+ if (!this.getImageObject(item.id)) return;
2868
+ const resolver = this.loadResolvers.get(item.id);
2869
+ if (!resolver) return;
2870
+ resolver();
2871
+ this.loadResolvers.delete(item.id);
2872
+ });
2873
+ if (this.focusedImageId && this.isToolActive) {
2874
+ this.setImageFocus(this.focusedImageId, {
2875
+ syncCanvasSelection: true,
2876
+ skipRender: true
2877
+ });
2878
+ }
2690
2879
  const overlayCanvasCount = this.getOverlayObjects().length;
2691
2880
  this.debug("render:done", {
2692
2881
  seq,
2693
2882
  renderCount: renderItems.length,
2694
- overlayCount: overlaySpecs.length,
2883
+ overlayCount: this.overlaySpecs.length,
2695
2884
  overlayCanvasCount,
2696
2885
  isToolActive: this.isToolActive,
2697
2886
  isImageSelectionActive: this.isImageSelectionActive,
@@ -4341,11 +4530,11 @@ var DielineTool = class {
4341
4530
  style: "solid"
4342
4531
  },
4343
4532
  insideColor: "rgba(0,0,0,0)",
4344
- outsideColor: "#ffffff",
4345
4533
  showBleedLines: true,
4346
4534
  features: []
4347
4535
  };
4348
4536
  this.specs = [];
4537
+ this.effects = [];
4349
4538
  this.renderSeq = 0;
4350
4539
  this.onCanvasResized = () => {
4351
4540
  this.updateDieline();
@@ -4382,10 +4571,23 @@ var DielineTool = class {
4382
4571
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
4383
4572
  this.id,
4384
4573
  () => ({
4385
- layerSpecs: {
4386
- [DIELINE_LAYER_ID]: this.specs
4387
- },
4388
- replaceLayerIds: [DIELINE_LAYER_ID]
4574
+ passes: [
4575
+ {
4576
+ id: DIELINE_LAYER_ID,
4577
+ stack: 700,
4578
+ order: 0,
4579
+ replace: true,
4580
+ visibility: {
4581
+ op: "not",
4582
+ expr: {
4583
+ op: "activeToolIn",
4584
+ ids: ["pooder.kit.image", "pooder.kit.white-ink"]
4585
+ }
4586
+ },
4587
+ effects: this.effects,
4588
+ objects: this.specs
4589
+ }
4590
+ ]
4389
4591
  }),
4390
4592
  { priority: 250 }
4391
4593
  );
@@ -4441,10 +4643,6 @@ var DielineTool = class {
4441
4643
  s.offsetLine.style
4442
4644
  );
4443
4645
  s.insideColor = configService.get("dieline.insideColor", s.insideColor);
4444
- s.outsideColor = configService.get(
4445
- "dieline.outsideColor",
4446
- s.outsideColor
4447
- );
4448
4646
  s.showBleedLines = configService.get(
4449
4647
  "dieline.showBleedLines",
4450
4648
  s.showBleedLines
@@ -4507,9 +4705,6 @@ var DielineTool = class {
4507
4705
  case "dieline.insideColor":
4508
4706
  s.insideColor = e.value;
4509
4707
  break;
4510
- case "dieline.outsideColor":
4511
- s.outsideColor = e.value;
4512
- break;
4513
4708
  case "dieline.showBleedLines":
4514
4709
  s.showBleedLines = e.value;
4515
4710
  break;
@@ -4538,6 +4733,7 @@ var DielineTool = class {
4538
4733
  context.eventBus.off("canvas:resized", this.onCanvasResized);
4539
4734
  this.renderSeq += 1;
4540
4735
  this.specs = [];
4736
+ this.effects = [];
4541
4737
  (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
4542
4738
  this.renderProducerDisposable = void 0;
4543
4739
  if (this.canvasService) {
@@ -4654,12 +4850,6 @@ var DielineTool = class {
4654
4850
  label: "Inside Color",
4655
4851
  default: s.insideColor
4656
4852
  },
4657
- {
4658
- id: "dieline.outsideColor",
4659
- type: "color",
4660
- label: "Outside Color",
4661
- default: s.outsideColor
4662
- },
4663
4853
  {
4664
4854
  id: "dieline.features",
4665
4855
  type: "json",
@@ -4783,6 +4973,12 @@ var DielineTool = class {
4783
4973
  "ConfigurationService"
4784
4974
  );
4785
4975
  }
4976
+ hasImageItems() {
4977
+ const configService = this.getConfigService();
4978
+ if (!configService) return false;
4979
+ const items = configService.get("image.items", []);
4980
+ return Array.isArray(items) && items.length > 0;
4981
+ }
4786
4982
  syncSizeState(configService) {
4787
4983
  const sizeState = readSizeState(configService);
4788
4984
  this.state.width = sizeState.actualWidthMm;
@@ -4790,29 +4986,6 @@ var DielineTool = class {
4790
4986
  this.state.padding = sizeState.viewPadding;
4791
4987
  this.state.offset = sizeState.cutMode === "outset" ? sizeState.cutMarginMm : sizeState.cutMode === "inset" ? -sizeState.cutMarginMm : 0;
4792
4988
  }
4793
- bringFeatureMarkersToFront() {
4794
- if (!this.canvasService) return;
4795
- const canvas = this.canvasService.canvas;
4796
- canvas.getObjects().filter((obj) => {
4797
- var _a;
4798
- return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.type) === "feature-marker";
4799
- }).forEach((obj) => canvas.bringObjectToFront(obj));
4800
- }
4801
- ensureLayerStacking() {
4802
- if (!this.canvasService) return;
4803
- const layer = this.canvasService.getLayer(DIELINE_LAYER_ID);
4804
- if (!layer) return;
4805
- const userLayer = this.canvasService.getLayer("user");
4806
- if (userLayer) {
4807
- const layerIndex = this.canvasService.canvas.getObjects().indexOf(layer);
4808
- const userIndex = this.canvasService.canvas.getObjects().indexOf(userLayer);
4809
- if (layerIndex < userIndex) {
4810
- this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
4811
- }
4812
- return;
4813
- }
4814
- this.canvasService.canvas.bringObjectToFront(layer);
4815
- }
4816
4989
  buildDielineSpecs(sceneLayout) {
4817
4990
  var _a, _b;
4818
4991
  const {
@@ -4822,10 +4995,10 @@ var DielineTool = class {
4822
4995
  mainLine,
4823
4996
  offsetLine,
4824
4997
  insideColor,
4825
- outsideColor,
4826
4998
  showBleedLines,
4827
4999
  features
4828
5000
  } = this.state;
5001
+ const hasImages = this.hasImageItems();
4829
5002
  const canvasW = sceneLayout.canvasWidth || ((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 800;
4830
5003
  const canvasH = sceneLayout.canvasHeight || ((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 600;
4831
5004
  const scale = sceneLayout.scale;
@@ -4847,41 +5020,8 @@ var DielineTool = class {
4847
5020
  radius: (f.radius || 0) * scale
4848
5021
  }));
4849
5022
  const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
4850
- const maskPathData = generateMaskPath({
4851
- canvasWidth: canvasW,
4852
- canvasHeight: canvasH,
4853
- shape,
4854
- width: cutW,
4855
- height: cutH,
4856
- radius: cutR,
4857
- x: cx,
4858
- y: cy,
4859
- features: cutFeatures,
4860
- shapeStyle,
4861
- pathData: this.state.pathData,
4862
- customSourceWidthPx: this.state.customSourceWidthPx,
4863
- customSourceHeightPx: this.state.customSourceHeightPx
4864
- });
4865
- const specs = [
4866
- {
4867
- id: "dieline.mask",
4868
- type: "path",
4869
- space: "screen",
4870
- data: { id: "dieline.mask", type: "dieline" },
4871
- props: {
4872
- pathData: maskPathData,
4873
- fill: outsideColor,
4874
- stroke: null,
4875
- selectable: false,
4876
- evented: false,
4877
- originX: "left",
4878
- originY: "top",
4879
- left: 0,
4880
- top: 0
4881
- }
4882
- }
4883
- ];
4884
- if (insideColor && insideColor !== "transparent" && insideColor !== "rgba(0,0,0,0)") {
5023
+ const specs = [];
5024
+ if (insideColor && insideColor !== "transparent" && insideColor !== "rgba(0,0,0,0)" && !hasImages) {
4885
5025
  const productPathData = generateDielinePath({
4886
5026
  shape,
4887
5027
  width: cutW,
@@ -5035,6 +5175,73 @@ var DielineTool = class {
5035
5175
  });
5036
5176
  return specs;
5037
5177
  }
5178
+ buildImageClipEffects(sceneLayout) {
5179
+ var _a, _b;
5180
+ const { shape, shapeStyle, radius, features } = this.state;
5181
+ const canvasW = sceneLayout.canvasWidth || ((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 800;
5182
+ const canvasH = sceneLayout.canvasHeight || ((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 600;
5183
+ const scale = sceneLayout.scale;
5184
+ const cx = sceneLayout.trimRect.centerX;
5185
+ const cy = sceneLayout.trimRect.centerY;
5186
+ const visualWidth = sceneLayout.trimRect.width;
5187
+ const visualRadius = radius * scale;
5188
+ const cutW = sceneLayout.cutRect.width;
5189
+ const cutH = sceneLayout.cutRect.height;
5190
+ const visualOffset = (cutW - visualWidth) / 2;
5191
+ const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
5192
+ const absoluteFeatures = (features || []).map((f) => ({
5193
+ ...f,
5194
+ x: f.x,
5195
+ y: f.y,
5196
+ width: (f.width || 0) * scale,
5197
+ height: (f.height || 0) * scale,
5198
+ radius: (f.radius || 0) * scale
5199
+ }));
5200
+ const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
5201
+ const clipPathData = generateDielinePath({
5202
+ shape,
5203
+ width: cutW,
5204
+ height: cutH,
5205
+ radius: cutR,
5206
+ x: cx,
5207
+ y: cy,
5208
+ features: cutFeatures,
5209
+ shapeStyle,
5210
+ pathData: this.state.pathData,
5211
+ customSourceWidthPx: this.state.customSourceWidthPx,
5212
+ customSourceHeightPx: this.state.customSourceHeightPx,
5213
+ canvasWidth: canvasW,
5214
+ canvasHeight: canvasH
5215
+ });
5216
+ if (!clipPathData) return [];
5217
+ return [
5218
+ {
5219
+ type: "clipPath",
5220
+ id: "dieline.clip.image",
5221
+ targetPassIds: [IMAGE_OBJECT_LAYER_ID2],
5222
+ source: {
5223
+ id: "dieline.effect.clip-path",
5224
+ type: "path",
5225
+ space: "screen",
5226
+ data: {
5227
+ id: "dieline.effect.clip-path",
5228
+ type: "dieline-effect",
5229
+ effect: "clipPath"
5230
+ },
5231
+ props: {
5232
+ pathData: clipPathData,
5233
+ fill: "#000000",
5234
+ stroke: null,
5235
+ originX: "left",
5236
+ originY: "top",
5237
+ selectable: false,
5238
+ evented: false,
5239
+ excludeFromExport: true
5240
+ }
5241
+ }
5242
+ }
5243
+ ];
5244
+ }
5038
5245
  updateDieline(_emitEvent = true) {
5039
5246
  void this.updateDielineAsync();
5040
5247
  }
@@ -5051,17 +5258,17 @@ var DielineTool = class {
5051
5258
  if (!sceneLayout) {
5052
5259
  if (seq !== this.renderSeq) return;
5053
5260
  this.specs = [];
5261
+ this.effects = [];
5054
5262
  await this.canvasService.flushRenderFromProducers();
5055
5263
  return;
5056
5264
  }
5057
5265
  const nextSpecs = this.buildDielineSpecs(sceneLayout);
5266
+ const nextEffects = this.buildImageClipEffects(sceneLayout);
5058
5267
  if (seq !== this.renderSeq) return;
5059
5268
  this.specs = nextSpecs;
5269
+ this.effects = nextEffects;
5060
5270
  await this.canvasService.flushRenderFromProducers();
5061
5271
  if (seq !== this.renderSeq) return;
5062
- this.ensureLayerStacking();
5063
- this.bringFeatureMarkersToFront();
5064
- this.canvasService.bringLayerToFront("ruler-overlay");
5065
5272
  this.canvasService.requestRenderAll();
5066
5273
  }
5067
5274
  getGeometry() {
@@ -5496,9 +5703,14 @@ var FeatureTool = class {
5496
5703
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
5497
5704
  this.id,
5498
5705
  () => ({
5499
- rootLayerSpecs: {
5500
- [FEATURE_OVERLAY_LAYER_ID]: this.specs
5501
- }
5706
+ passes: [
5707
+ {
5708
+ id: FEATURE_OVERLAY_LAYER_ID,
5709
+ stack: 880,
5710
+ order: 0,
5711
+ objects: this.specs
5712
+ }
5713
+ ]
5502
5714
  }),
5503
5715
  { priority: 350 }
5504
5716
  );
@@ -5641,10 +5853,10 @@ var FeatureTool = class {
5641
5853
  await this.refreshGeometry();
5642
5854
  this.setWorkingFeatures(original);
5643
5855
  this.hasWorkingChanges = false;
5856
+ this.clearFeatureSessionState();
5644
5857
  this.redraw();
5645
5858
  this.emitWorkingChange();
5646
5859
  this.updateCommittedFeatures(original);
5647
- this.clearFeatureSessionState();
5648
5860
  return { ok: true };
5649
5861
  }
5650
5862
  },
@@ -5960,6 +6172,7 @@ var FeatureTool = class {
5960
6172
  }
5961
6173
  getDraggableMarkerTarget(target) {
5962
6174
  var _a, _b;
6175
+ if (!this.isFeatureSessionActive || !this.isToolActive) return null;
5963
6176
  if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return null;
5964
6177
  if (((_b = target.data) == null ? void 0 : _b.markerRole) !== "handle") return null;
5965
6178
  return target;
@@ -6082,18 +6295,12 @@ var FeatureTool = class {
6082
6295
  if (seq !== this.renderSeq) return;
6083
6296
  await this.canvasService.flushRenderFromProducers();
6084
6297
  if (seq !== this.renderSeq) return;
6085
- this.syncOverlayOrder();
6086
6298
  if (options.enforceConstraints) {
6087
6299
  this.enforceConstraints();
6088
6300
  }
6089
6301
  }
6090
- syncOverlayOrder() {
6091
- if (!this.canvasService) return;
6092
- this.canvasService.bringLayerToFront(FEATURE_OVERLAY_LAYER_ID);
6093
- this.canvasService.bringLayerToFront("ruler-overlay");
6094
- }
6095
6302
  buildFeatureSpecs() {
6096
- if (!this.currentGeometry || this.workingFeatures.length === 0) {
6303
+ if (!this.isFeatureSessionActive || !this.currentGeometry || this.workingFeatures.length === 0) {
6097
6304
  return [];
6098
6305
  }
6099
6306
  const groups = /* @__PURE__ */ new Map();
@@ -6163,11 +6370,12 @@ var FeatureTool = class {
6163
6370
  const color = feature.color || (feature.operation === "add" ? "#00FF00" : "#FF0000");
6164
6371
  const strokeDash = feature.strokeDash || (feature.operation === "subtract" ? [4, 4] : void 0);
6165
6372
  const interactive = options.markerRole === "handle";
6373
+ const sessionVisible = this.isToolActive && this.isFeatureSessionActive;
6166
6374
  const baseData = this.buildMarkerData(marker, options);
6167
6375
  const commonProps = {
6168
- visible: this.isToolActive,
6169
- selectable: interactive && this.isToolActive,
6170
- evented: interactive && this.isToolActive,
6376
+ visible: sessionVisible,
6377
+ selectable: interactive && sessionVisible,
6378
+ evented: interactive && sessionVisible,
6171
6379
  hasControls: false,
6172
6380
  hasBorders: false,
6173
6381
  hoverCursor: interactive ? "move" : "default",
@@ -6232,7 +6440,7 @@ var FeatureTool = class {
6232
6440
  markerOffsetY: -visualHeight / 2
6233
6441
  },
6234
6442
  props: {
6235
- visible: this.isToolActive,
6443
+ visible: sessionVisible,
6236
6444
  selectable: false,
6237
6445
  evented: false,
6238
6446
  width: visualWidth,
@@ -6402,9 +6610,14 @@ var FilmTool = class {
6402
6610
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
6403
6611
  this.id,
6404
6612
  () => ({
6405
- layerSpecs: {
6406
- [FILM_LAYER_ID]: this.specs
6407
- }
6613
+ passes: [
6614
+ {
6615
+ id: FILM_LAYER_ID,
6616
+ stack: 1e3,
6617
+ order: 0,
6618
+ objects: this.specs
6619
+ }
6620
+ ]
6408
6621
  }),
6409
6622
  { priority: 500 }
6410
6623
  );
@@ -6578,7 +6791,6 @@ var FilmTool = class {
6578
6791
  this.specs = this.buildFilmSpecs(this.renderImageUrl, this.opacity);
6579
6792
  await this.canvasService.flushRenderFromProducers();
6580
6793
  if (seq !== this.renderSeq) return;
6581
- this.canvasService.bringLayerToFront(FILM_LAYER_ID);
6582
6794
  this.canvasService.requestRenderAll();
6583
6795
  }
6584
6796
  };
@@ -6729,10 +6941,22 @@ var RulerTool = class {
6729
6941
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
6730
6942
  this.id,
6731
6943
  () => ({
6732
- rootLayerSpecs: {
6733
- [RULER_LAYER_ID]: this.specs
6734
- },
6735
- replaceRootLayerIds: [RULER_LAYER_ID]
6944
+ passes: [
6945
+ {
6946
+ id: RULER_LAYER_ID,
6947
+ stack: 950,
6948
+ order: 0,
6949
+ replace: true,
6950
+ visibility: {
6951
+ op: "not",
6952
+ expr: {
6953
+ op: "activeToolIn",
6954
+ ids: ["pooder.kit.white-ink"]
6955
+ }
6956
+ },
6957
+ objects: this.specs
6958
+ }
6959
+ ]
6736
6960
  }),
6737
6961
  { priority: 400 }
6738
6962
  );
@@ -7224,7 +7448,6 @@ var RulerTool = class {
7224
7448
  this.specs = specs;
7225
7449
  await this.canvasService.flushRenderFromProducers();
7226
7450
  if (seq !== this.renderSeq) return;
7227
- this.canvasService.bringLayerToFront(RULER_LAYER_ID);
7228
7451
  this.canvasService.requestRenderAll();
7229
7452
  this.log("render:done", { seq });
7230
7453
  }
@@ -7238,7 +7461,6 @@ var WHITE_INK_OBJECT_LAYER_ID = "white-ink.user";
7238
7461
  var WHITE_INK_COVER_LAYER_ID = "white-ink.cover";
7239
7462
  var WHITE_INK_OVERLAY_LAYER_ID = "white-ink.overlay";
7240
7463
  var IMAGE_OBJECT_LAYER_ID3 = "image.user";
7241
- var IMAGE_OVERLAY_LAYER_ID2 = "image-overlay";
7242
7464
  var WHITE_INK_DEBUG_KEY = "whiteInk.debug";
7243
7465
  var WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY = "whiteInk.previewImageVisible";
7244
7466
  var WHITE_INK_DEFAULT_OPACITY = 0.85;
@@ -7316,11 +7538,26 @@ var WhiteInkTool = class {
7316
7538
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
7317
7539
  this.id,
7318
7540
  () => ({
7319
- rootLayerSpecs: {
7320
- [WHITE_INK_OBJECT_LAYER_ID]: this.whiteSpecs,
7321
- [WHITE_INK_COVER_LAYER_ID]: this.coverSpecs,
7322
- [WHITE_INK_OVERLAY_LAYER_ID]: this.overlaySpecs
7323
- }
7541
+ passes: [
7542
+ {
7543
+ id: WHITE_INK_COVER_LAYER_ID,
7544
+ stack: 220,
7545
+ order: 0,
7546
+ objects: this.coverSpecs
7547
+ },
7548
+ {
7549
+ id: WHITE_INK_OBJECT_LAYER_ID,
7550
+ stack: 221,
7551
+ order: 0,
7552
+ objects: this.whiteSpecs
7553
+ },
7554
+ {
7555
+ id: WHITE_INK_OVERLAY_LAYER_ID,
7556
+ stack: 790,
7557
+ order: 0,
7558
+ objects: this.overlaySpecs
7559
+ }
7560
+ ]
7324
7561
  }),
7325
7562
  { priority: 260 }
7326
7563
  );
@@ -7399,7 +7636,6 @@ var WhiteInkTool = class {
7399
7636
  (_a = this.dirtyTrackerDisposable) == null ? void 0 : _a.dispose();
7400
7637
  this.dirtyTrackerDisposable = void 0;
7401
7638
  this.clearRenderedWhiteInks();
7402
- this.applyImageVisibilityForWhiteInk(false);
7403
7639
  (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
7404
7640
  this.renderProducerDisposable = void 0;
7405
7641
  if (this.canvasService) {
@@ -8239,25 +8475,9 @@ var WhiteInkTool = class {
8239
8475
  selectable: false,
8240
8476
  evented: false,
8241
8477
  excludeFromExport: true
8242
- }
8243
- }
8244
- ];
8245
- }
8246
- applyImageVisibilityForWhiteInk(previewActive) {
8247
- if (!this.canvasService) return;
8248
- const visible = !previewActive;
8249
- let changed = false;
8250
- this.canvasService.canvas.getObjects().forEach((obj) => {
8251
- var _a, _b;
8252
- if (((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) !== IMAGE_OBJECT_LAYER_ID3) return;
8253
- if (obj.visible === visible) return;
8254
- obj.set({ visible });
8255
- (_b = obj.setCoords) == null ? void 0 : _b.call(obj);
8256
- changed = true;
8257
- });
8258
- if (changed) {
8259
- this.canvasService.requestRenderAll();
8260
- }
8478
+ }
8479
+ }
8480
+ ];
8261
8481
  }
8262
8482
  resolveRenderItems() {
8263
8483
  if (this.isToolActive) {
@@ -8283,52 +8503,6 @@ var WhiteInkTool = class {
8283
8503
  whiteScaleAdjustY: scaleAdjust.y
8284
8504
  };
8285
8505
  }
8286
- resolveDefaultInsertIndex(objects) {
8287
- if (!this.canvasService) return 0;
8288
- const backgroundLayer = this.canvasService.getLayer("background");
8289
- if (!backgroundLayer) return 0;
8290
- const bgIndex = objects.indexOf(backgroundLayer);
8291
- if (bgIndex < 0) return 0;
8292
- return bgIndex + 1;
8293
- }
8294
- syncZOrder() {
8295
- if (!this.canvasService) return;
8296
- const canvas = this.canvasService.canvas;
8297
- const whiteObjects = this.canvasService.getRootLayerObjects(
8298
- WHITE_INK_OBJECT_LAYER_ID
8299
- );
8300
- const coverObjects = this.canvasService.getRootLayerObjects(
8301
- WHITE_INK_COVER_LAYER_ID
8302
- );
8303
- const frameObjects = this.canvasService.getRootLayerObjects(
8304
- WHITE_INK_OVERLAY_LAYER_ID
8305
- );
8306
- const currentObjects = canvas.getObjects();
8307
- const imageIndexes = currentObjects.map(
8308
- (obj, index) => {
8309
- var _a;
8310
- return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === IMAGE_OBJECT_LAYER_ID3 ? index : -1;
8311
- }
8312
- ).filter((index) => index >= 0);
8313
- let whiteInsertIndex = imageIndexes.length ? Math.min(...imageIndexes) : this.resolveDefaultInsertIndex(currentObjects);
8314
- let coverInsertIndex = whiteInsertIndex;
8315
- coverObjects.forEach((obj) => {
8316
- canvas.moveObjectTo(obj, coverInsertIndex);
8317
- coverInsertIndex += 1;
8318
- });
8319
- whiteInsertIndex = coverInsertIndex;
8320
- whiteObjects.forEach((obj) => {
8321
- canvas.moveObjectTo(obj, whiteInsertIndex);
8322
- whiteInsertIndex += 1;
8323
- });
8324
- frameObjects.forEach((obj) => canvas.bringObjectToFront(obj));
8325
- canvas.getObjects().filter((obj) => {
8326
- var _a;
8327
- return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === IMAGE_OVERLAY_LAYER_ID2;
8328
- }).forEach((obj) => canvas.bringObjectToFront(obj));
8329
- this.canvasService.bringLayerToFront("dieline-overlay");
8330
- this.canvasService.bringLayerToFront("ruler-overlay");
8331
- }
8332
8506
  clearRenderedWhiteInks() {
8333
8507
  if (!this.canvasService) return;
8334
8508
  this.whiteSpecs = [];
@@ -8360,7 +8534,6 @@ var WhiteInkTool = class {
8360
8534
  this.syncToolActiveFromWorkbench();
8361
8535
  const seq = ++this.renderSeq;
8362
8536
  const previewActive = this.isPreviewActive();
8363
- this.applyImageVisibilityForWhiteInk(previewActive);
8364
8537
  const frame = this.getFrameRect();
8365
8538
  const frameSpecs = this.buildFrameSpecs(frame);
8366
8539
  let whiteSpecs = [];
@@ -8408,7 +8581,6 @@ var WhiteInkTool = class {
8408
8581
  this.overlaySpecs = frameSpecs;
8409
8582
  await this.canvasService.flushRenderFromProducers();
8410
8583
  if (seq !== this.renderSeq) return;
8411
- this.syncZOrder();
8412
8584
  this.canvasService.requestRenderAll();
8413
8585
  }
8414
8586
  getMaskCacheKey(sourceUrl, tint) {
@@ -8593,62 +8765,12 @@ var SceneLayoutService = class {
8593
8765
  }
8594
8766
  };
8595
8767
 
8596
- // src/extensions/sceneVisibility.ts
8597
- import { WORKBENCH_SERVICE } from "@pooder/core";
8598
- var CANVAS_SERVICE_ID2 = "CanvasService";
8599
- var HIDDEN_DIELINE_TOOLS = /* @__PURE__ */ new Set(["pooder.kit.image", "pooder.kit.white-ink"]);
8600
- var HIDDEN_RULER_TOOLS = /* @__PURE__ */ new Set(["pooder.kit.white-ink"]);
8601
- var SceneVisibilityService = class {
8602
- constructor() {
8603
- this.activeToolId = null;
8604
- this.onToolActivated = (e) => {
8605
- this.activeToolId = e.id;
8606
- this.apply();
8607
- };
8608
- this.onObjectAdded = () => {
8609
- this.apply();
8610
- };
8611
- }
8612
- init(context) {
8613
- var _a, _b;
8614
- if (this.context) {
8615
- this.dispose(this.context);
8616
- }
8617
- const canvasService = context.get(CANVAS_SERVICE_ID2);
8618
- if (!canvasService) {
8619
- throw new Error("[SceneVisibilityService] CanvasService is required.");
8620
- }
8621
- this.context = context;
8622
- this.canvasService = canvasService;
8623
- this.activeToolId = (_b = (_a = context.get(WORKBENCH_SERVICE)) == null ? void 0 : _a.activeToolId) != null ? _b : null;
8624
- context.eventBus.on("tool:activated", this.onToolActivated);
8625
- context.eventBus.on("object:added", this.onObjectAdded);
8626
- this.apply();
8627
- }
8628
- dispose(context) {
8629
- var _a;
8630
- const activeContext = (_a = this.context) != null ? _a : context;
8631
- activeContext.eventBus.off("tool:activated", this.onToolActivated);
8632
- activeContext.eventBus.off("object:added", this.onObjectAdded);
8633
- this.context = void 0;
8634
- this.activeToolId = null;
8635
- this.canvasService = void 0;
8636
- }
8637
- apply() {
8638
- if (!this.canvasService) return;
8639
- const dielineLayer = this.canvasService.getLayer("dieline-overlay");
8640
- if (dielineLayer) {
8641
- const visible = !HIDDEN_DIELINE_TOOLS.has(this.activeToolId || "");
8642
- this.canvasService.setLayerVisibility("dieline-overlay", visible);
8643
- }
8644
- const rulerVisible = !HIDDEN_RULER_TOOLS.has(this.activeToolId || "");
8645
- this.canvasService.setLayerVisibility("ruler-overlay", rulerVisible);
8646
- this.canvasService.requestRenderAll();
8647
- }
8648
- };
8649
-
8650
8768
  // src/services/CanvasService.ts
8651
- import { Canvas, Group, Rect, Path as Path2, Image as Image2, Text } from "fabric";
8769
+ import { Canvas, Rect, Path as Path2, Image as Image2, Text } from "fabric";
8770
+ import {
8771
+ TOOL_SESSION_SERVICE,
8772
+ WORKBENCH_SERVICE
8773
+ } from "@pooder/core";
8652
8774
 
8653
8775
  // src/services/ViewportSystem.ts
8654
8776
  var ViewportSystem = class {
@@ -8723,6 +8845,53 @@ var ViewportSystem = class {
8723
8845
  }
8724
8846
  };
8725
8847
 
8848
+ // src/services/visibility.ts
8849
+ function compareLayerObjectCount(actual, cmp, expected) {
8850
+ if (cmp === ">") return actual > expected;
8851
+ if (cmp === ">=") return actual >= expected;
8852
+ if (cmp === "<") return actual < expected;
8853
+ if (cmp === "<=") return actual <= expected;
8854
+ return actual === expected;
8855
+ }
8856
+ function layerState(context, layerId) {
8857
+ return context.layers.get(layerId) || { exists: false, objectCount: 0 };
8858
+ }
8859
+ function evaluateVisibilityExpr(expr, context) {
8860
+ var _a;
8861
+ if (!expr) return true;
8862
+ if (expr.op === "const") {
8863
+ return Boolean(expr.value);
8864
+ }
8865
+ if (expr.op === "activeToolIn") {
8866
+ const activeToolId = (_a = context.activeToolId) != null ? _a : null;
8867
+ return !!activeToolId && expr.ids.includes(activeToolId);
8868
+ }
8869
+ if (expr.op === "sessionActive") {
8870
+ const toolId = String(expr.toolId || "").trim();
8871
+ if (!toolId) return false;
8872
+ return context.isSessionActive ? context.isSessionActive(toolId) : false;
8873
+ }
8874
+ if (expr.op === "layerExists") {
8875
+ return layerState(context, expr.layerId).exists === true;
8876
+ }
8877
+ if (expr.op === "layerObjectCount") {
8878
+ const expected = Number(expr.value);
8879
+ if (!Number.isFinite(expected)) return false;
8880
+ const count = layerState(context, expr.layerId).objectCount;
8881
+ return compareLayerObjectCount(count, expr.cmp, expected);
8882
+ }
8883
+ if (expr.op === "not") {
8884
+ return !evaluateVisibilityExpr(expr.expr, context);
8885
+ }
8886
+ if (expr.op === "all") {
8887
+ return expr.exprs.every((item) => evaluateVisibilityExpr(item, context));
8888
+ }
8889
+ if (expr.op === "any") {
8890
+ return expr.exprs.some((item) => evaluateVisibilityExpr(item, context));
8891
+ }
8892
+ return true;
8893
+ }
8894
+
8726
8895
  // src/services/CanvasService.ts
8727
8896
  var CanvasService = class {
8728
8897
  constructor(el, options) {
@@ -8731,8 +8900,45 @@ var CanvasService = class {
8731
8900
  this.producerFlushRequested = false;
8732
8901
  this.producerLoopPending = false;
8733
8902
  this.producerLoopPromise = null;
8734
- this.managedProducerLayerIds = /* @__PURE__ */ new Set();
8735
- this.managedProducerRootLayerIds = /* @__PURE__ */ new Set();
8903
+ this.producerApplyInProgress = false;
8904
+ this.visibilityRefreshScheduled = false;
8905
+ this.managedProducerPassIds = /* @__PURE__ */ new Set();
8906
+ this.managedPassMetas = /* @__PURE__ */ new Map();
8907
+ this.canvasForwardersBound = false;
8908
+ this.forwardSelectionCreated = (e) => {
8909
+ var _a;
8910
+ (_a = this.eventBus) == null ? void 0 : _a.emit("selection:created", e);
8911
+ };
8912
+ this.forwardSelectionUpdated = (e) => {
8913
+ var _a;
8914
+ (_a = this.eventBus) == null ? void 0 : _a.emit("selection:updated", e);
8915
+ };
8916
+ this.forwardSelectionCleared = (e) => {
8917
+ var _a;
8918
+ (_a = this.eventBus) == null ? void 0 : _a.emit("selection:cleared", e);
8919
+ };
8920
+ this.forwardObjectModified = (e) => {
8921
+ var _a;
8922
+ (_a = this.eventBus) == null ? void 0 : _a.emit("object:modified", e);
8923
+ };
8924
+ this.forwardObjectAdded = (e) => {
8925
+ var _a;
8926
+ (_a = this.eventBus) == null ? void 0 : _a.emit("object:added", e);
8927
+ };
8928
+ this.forwardObjectRemoved = (e) => {
8929
+ var _a;
8930
+ (_a = this.eventBus) == null ? void 0 : _a.emit("object:removed", e);
8931
+ };
8932
+ this.onToolActivated = () => {
8933
+ this.applyManagedPassVisibility();
8934
+ };
8935
+ this.onToolSessionChanged = () => {
8936
+ this.applyManagedPassVisibility();
8937
+ };
8938
+ this.onCanvasObjectChanged = () => {
8939
+ if (this.producerApplyInProgress) return;
8940
+ this.scheduleManagedPassVisibilityRefresh();
8941
+ };
8736
8942
  if (el instanceof Canvas) {
8737
8943
  this.canvas = el;
8738
8944
  } else {
@@ -8749,25 +8955,52 @@ var CanvasService = class {
8749
8955
  this.setEventBus(options.eventBus);
8750
8956
  }
8751
8957
  }
8958
+ init(context) {
8959
+ if (this.context) {
8960
+ this.detachContextEvents(this.context.eventBus);
8961
+ }
8962
+ this.context = context;
8963
+ this.workbenchService = context.get(WORKBENCH_SERVICE);
8964
+ this.toolSessionService = context.get(TOOL_SESSION_SERVICE);
8965
+ this.setEventBus(context.eventBus);
8966
+ this.attachContextEvents(context.eventBus);
8967
+ }
8968
+ attachContextEvents(eventBus) {
8969
+ eventBus.on("tool:activated", this.onToolActivated);
8970
+ eventBus.on("tool:session:change", this.onToolSessionChanged);
8971
+ eventBus.on("object:added", this.onCanvasObjectChanged);
8972
+ eventBus.on("object:removed", this.onCanvasObjectChanged);
8973
+ }
8974
+ detachContextEvents(eventBus) {
8975
+ eventBus.off("tool:activated", this.onToolActivated);
8976
+ eventBus.off("tool:session:change", this.onToolSessionChanged);
8977
+ eventBus.off("object:added", this.onCanvasObjectChanged);
8978
+ eventBus.off("object:removed", this.onCanvasObjectChanged);
8979
+ }
8752
8980
  setEventBus(eventBus) {
8753
8981
  this.eventBus = eventBus;
8754
8982
  this.setupEvents();
8755
8983
  }
8756
8984
  setupEvents() {
8757
- if (!this.eventBus) return;
8758
- const bus = this.eventBus;
8759
- const forward = (name) => (e) => bus.emit(name, e);
8760
- this.canvas.on("selection:created", forward("selection:created"));
8761
- this.canvas.on("selection:updated", forward("selection:updated"));
8762
- this.canvas.on("selection:cleared", forward("selection:cleared"));
8763
- this.canvas.on("object:modified", forward("object:modified"));
8764
- this.canvas.on("object:added", forward("object:added"));
8765
- this.canvas.on("object:removed", forward("object:removed"));
8985
+ if (this.canvasForwardersBound) return;
8986
+ this.canvas.on("selection:created", this.forwardSelectionCreated);
8987
+ this.canvas.on("selection:updated", this.forwardSelectionUpdated);
8988
+ this.canvas.on("selection:cleared", this.forwardSelectionCleared);
8989
+ this.canvas.on("object:modified", this.forwardObjectModified);
8990
+ this.canvas.on("object:added", this.forwardObjectAdded);
8991
+ this.canvas.on("object:removed", this.forwardObjectRemoved);
8992
+ this.canvasForwardersBound = true;
8766
8993
  }
8767
8994
  dispose() {
8995
+ if (this.context) {
8996
+ this.detachContextEvents(this.context.eventBus);
8997
+ }
8768
8998
  this.renderProducers.clear();
8769
- this.managedProducerLayerIds.clear();
8770
- this.managedProducerRootLayerIds.clear();
8999
+ this.managedProducerPassIds.clear();
9000
+ this.managedPassMetas.clear();
9001
+ this.context = void 0;
9002
+ this.workbenchService = void 0;
9003
+ this.toolSessionService = void 0;
8771
9004
  this.producerFlushRequested = false;
8772
9005
  this.canvas.dispose();
8773
9006
  }
@@ -8845,118 +9078,274 @@ var CanvasService = class {
8845
9078
  return a.toolId.localeCompare(b.toolId);
8846
9079
  });
8847
9080
  }
8848
- appendLayerSpecMap(map, source) {
8849
- if (!source) return;
8850
- Object.entries(source).forEach(([layerId, specs]) => {
8851
- if (!Array.isArray(specs)) return;
8852
- const list = map.get(layerId) || [];
8853
- list.push(...specs);
8854
- map.set(layerId, list);
9081
+ normalizePassSpecValue(spec) {
9082
+ const id = String(spec.id || "").trim();
9083
+ if (!id) return null;
9084
+ return {
9085
+ id,
9086
+ stack: Number.isFinite(spec.stack) ? Number(spec.stack) : 0,
9087
+ order: Number.isFinite(spec.order) ? Number(spec.order) : 0,
9088
+ replace: spec.replace !== false,
9089
+ visibility: spec.visibility,
9090
+ effects: Array.isArray(spec.effects) ? [...spec.effects] : [],
9091
+ objects: Array.isArray(spec.objects) ? [...spec.objects] : []
9092
+ };
9093
+ }
9094
+ normalizeClipPathEffectSpec(effect, passId, index) {
9095
+ if (!effect || effect.type !== "clipPath") return null;
9096
+ const source = effect.source;
9097
+ if (!source || typeof source !== "object") return null;
9098
+ const sourceId = String(source.id || "").trim();
9099
+ if (!sourceId) return null;
9100
+ const targetPassIds = Array.isArray(effect.targetPassIds) ? effect.targetPassIds.map((item) => String(item || "").trim()).filter((item) => item.length > 0) : [];
9101
+ if (!targetPassIds.length) return null;
9102
+ const customId = String(effect.id || "").trim();
9103
+ const key = customId || `${passId}.effect.clipPath.${index}`;
9104
+ return {
9105
+ type: "clipPath",
9106
+ key,
9107
+ source: {
9108
+ ...source,
9109
+ id: sourceId
9110
+ },
9111
+ targetPassIds
9112
+ };
9113
+ }
9114
+ mergePassSpec(map, rawSpec, producerId) {
9115
+ const normalized = this.normalizePassSpecValue(rawSpec);
9116
+ if (!normalized) return;
9117
+ const existing = map.get(normalized.id);
9118
+ if (!existing) {
9119
+ map.set(normalized.id, normalized);
9120
+ return;
9121
+ }
9122
+ existing.objects.push(...normalized.objects);
9123
+ existing.replace = existing.replace || normalized.replace;
9124
+ existing.stack = normalized.stack;
9125
+ existing.order = normalized.order;
9126
+ if (normalized.visibility !== void 0) {
9127
+ existing.visibility = normalized.visibility;
9128
+ }
9129
+ existing.effects.push(...normalized.effects);
9130
+ if (normalized.objects.length === 0 && normalized.effects.length === 0) {
9131
+ console.debug(
9132
+ `[CanvasService] pass "${normalized.id}" from producer "${producerId}" updated ordering/visibility only.`
9133
+ );
9134
+ }
9135
+ }
9136
+ comparePassMeta(a, b) {
9137
+ if (a.stack !== b.stack) return a.stack - b.stack;
9138
+ if (a.order !== b.order) return a.order - b.order;
9139
+ return a.id.localeCompare(b.id);
9140
+ }
9141
+ getPassObjectOrder(obj) {
9142
+ var _a;
9143
+ const raw = Number((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.passOrder);
9144
+ return Number.isFinite(raw) ? raw : Number.MAX_SAFE_INTEGER;
9145
+ }
9146
+ getPassCanvasObjects(passId) {
9147
+ const all = this.canvas.getObjects();
9148
+ return all.filter((obj) => {
9149
+ var _a;
9150
+ return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.passId) === passId;
9151
+ }).sort((a, b) => {
9152
+ const orderA = this.getPassObjectOrder(a);
9153
+ const orderB = this.getPassObjectOrder(b);
9154
+ if (orderA !== orderB) return orderA - orderB;
9155
+ return all.indexOf(a) - all.indexOf(b);
9156
+ });
9157
+ }
9158
+ getPassObjects(passId) {
9159
+ return this.getPassCanvasObjects(passId);
9160
+ }
9161
+ getRootLayerObjects(layerId) {
9162
+ return this.getPassCanvasObjects(layerId);
9163
+ }
9164
+ isManagedPassObject(obj) {
9165
+ var _a;
9166
+ const passId = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.passId;
9167
+ return typeof passId === "string" && this.managedPassMetas.has(passId);
9168
+ }
9169
+ syncManagedPassStacking(passes) {
9170
+ const orderedPasses = [...passes].sort((a, b) => this.comparePassMeta(a, b));
9171
+ if (!orderedPasses.length) return;
9172
+ const canvasObjects = this.canvas.getObjects();
9173
+ const managedObjects = canvasObjects.filter(
9174
+ (obj) => this.isManagedPassObject(obj)
9175
+ );
9176
+ if (!managedObjects.length) return;
9177
+ const firstManagedIndex = managedObjects.map((obj) => canvasObjects.indexOf(obj)).filter((index) => index >= 0).reduce((min, value) => Math.min(min, value), Number.MAX_SAFE_INTEGER);
9178
+ let targetIndex = Number.isFinite(firstManagedIndex) ? firstManagedIndex : 0;
9179
+ orderedPasses.forEach((meta) => {
9180
+ const objects = this.getPassCanvasObjects(meta.id);
9181
+ objects.forEach((obj) => {
9182
+ this.moveObjectInCanvas(obj, targetIndex);
9183
+ targetIndex += 1;
9184
+ });
9185
+ });
9186
+ }
9187
+ getPassRuntimeState() {
9188
+ const state = /* @__PURE__ */ new Map();
9189
+ const ensure = (passId) => {
9190
+ const id = String(passId || "").trim();
9191
+ if (!id) return { exists: false, objectCount: 0 };
9192
+ let item = state.get(id);
9193
+ if (!item) {
9194
+ item = { exists: false, objectCount: 0 };
9195
+ state.set(id, item);
9196
+ }
9197
+ return item;
9198
+ };
9199
+ this.canvas.getObjects().forEach((obj) => {
9200
+ var _a;
9201
+ const passId = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.passId;
9202
+ if (typeof passId === "string") {
9203
+ const item = ensure(passId);
9204
+ item.exists = true;
9205
+ item.objectCount += 1;
9206
+ }
9207
+ });
9208
+ this.managedPassMetas.forEach((meta) => {
9209
+ const item = ensure(meta.id);
9210
+ item.exists = true;
9211
+ });
9212
+ return state;
9213
+ }
9214
+ applyManagedPassVisibility(options = {}) {
9215
+ var _a, _b;
9216
+ if (!this.managedPassMetas.size) return false;
9217
+ const layers = this.getPassRuntimeState();
9218
+ const activeToolId = (_b = (_a = this.workbenchService) == null ? void 0 : _a.activeToolId) != null ? _b : null;
9219
+ const isSessionActive = (toolId) => {
9220
+ if (!this.toolSessionService) return false;
9221
+ return this.toolSessionService.getState(toolId).status === "active";
9222
+ };
9223
+ let changed = false;
9224
+ this.managedPassMetas.forEach((meta) => {
9225
+ const visible = evaluateVisibilityExpr(meta.visibility, {
9226
+ activeToolId,
9227
+ isSessionActive,
9228
+ layers
9229
+ });
9230
+ changed = this.setPassVisibility(meta.id, visible) || changed;
9231
+ });
9232
+ if (changed && options.render !== false) {
9233
+ this.requestRenderAll();
9234
+ }
9235
+ return changed;
9236
+ }
9237
+ scheduleManagedPassVisibilityRefresh() {
9238
+ if (this.visibilityRefreshScheduled) return;
9239
+ this.visibilityRefreshScheduled = true;
9240
+ void Promise.resolve().then(() => {
9241
+ this.visibilityRefreshScheduled = false;
9242
+ this.applyManagedPassVisibility();
8855
9243
  });
8856
9244
  }
8857
9245
  async collectAndApplyProducerSpecs() {
8858
- const groupLayerSpecs = /* @__PURE__ */ new Map();
8859
- const rootLayerSpecs = /* @__PURE__ */ new Map();
8860
- const replaceLayerIds = /* @__PURE__ */ new Set();
8861
- const replaceRootLayerIds = /* @__PURE__ */ new Set();
9246
+ const passes = /* @__PURE__ */ new Map();
8862
9247
  const entries = this.sortedRenderProducerEntries();
8863
- for (const entry of entries) {
8864
- try {
8865
- const result = await entry.producer();
8866
- if (!result) continue;
8867
- this.appendLayerSpecMap(groupLayerSpecs, result.layerSpecs);
8868
- this.appendLayerSpecMap(rootLayerSpecs, result.rootLayerSpecs);
8869
- if (Array.isArray(result.replaceLayerIds)) {
8870
- result.replaceLayerIds.forEach((layerId) => {
8871
- if (layerId) replaceLayerIds.add(layerId);
8872
- });
8873
- }
8874
- if (Array.isArray(result.replaceRootLayerIds)) {
8875
- result.replaceRootLayerIds.forEach((layerId) => {
8876
- if (layerId) replaceRootLayerIds.add(layerId);
8877
- });
9248
+ this.producerApplyInProgress = true;
9249
+ try {
9250
+ for (const entry of entries) {
9251
+ try {
9252
+ const result = await entry.producer();
9253
+ if (!result) continue;
9254
+ const specs = Array.isArray(result.passes) ? result.passes : [];
9255
+ specs.forEach((spec) => this.mergePassSpec(passes, spec, entry.toolId));
9256
+ } catch (error) {
9257
+ console.error(
9258
+ `[CanvasService] render producer "${entry.toolId}" failed.`,
9259
+ error
9260
+ );
8878
9261
  }
8879
- } catch (error) {
8880
- console.error(
8881
- `[CanvasService] render producer "${entry.toolId}" failed.`,
8882
- error
8883
- );
8884
9262
  }
8885
- }
8886
- const nextLayerIds = new Set(groupLayerSpecs.keys());
8887
- const nextRootLayerIds = new Set(rootLayerSpecs.keys());
8888
- for (const [layerId, specs] of groupLayerSpecs.entries()) {
8889
- if (replaceLayerIds.has(layerId)) {
8890
- const layer = this.getLayer(layerId);
8891
- if (layer) {
8892
- layer.getObjects().forEach((obj) => layer.remove(obj));
8893
- }
9263
+ const nextPassIds = /* @__PURE__ */ new Set();
9264
+ const nextManagedPassMetas = /* @__PURE__ */ new Map();
9265
+ const nextEffects = [];
9266
+ for (const pass of passes.values()) {
9267
+ nextPassIds.add(pass.id);
9268
+ nextManagedPassMetas.set(pass.id, {
9269
+ id: pass.id,
9270
+ stack: pass.stack,
9271
+ order: pass.order,
9272
+ visibility: pass.visibility
9273
+ });
9274
+ await this.applyObjectSpecsToPass(pass.id, pass.objects, {
9275
+ render: false,
9276
+ replace: pass.replace
9277
+ });
9278
+ pass.effects.forEach((effect, index) => {
9279
+ const normalized = this.normalizeClipPathEffectSpec(
9280
+ effect,
9281
+ pass.id,
9282
+ index
9283
+ );
9284
+ if (!normalized) return;
9285
+ nextEffects.push(normalized);
9286
+ });
8894
9287
  }
8895
- await this.applyObjectSpecsToLayer(layerId, specs, { render: false });
8896
- }
8897
- for (const layerId of this.managedProducerLayerIds) {
8898
- if (nextLayerIds.has(layerId)) continue;
8899
- const layer = this.getLayer(layerId);
8900
- if (!layer) continue;
8901
- await this.applyObjectSpecsToContainer(layer, [], { render: false });
8902
- }
8903
- for (const [layerId, specs] of rootLayerSpecs.entries()) {
8904
- if (replaceRootLayerIds.has(layerId)) {
8905
- const existing = this.getRootLayerObjects(layerId);
8906
- existing.forEach((obj) => this.canvas.remove(obj));
9288
+ for (const passId of this.managedProducerPassIds) {
9289
+ if (nextPassIds.has(passId)) continue;
9290
+ await this.applyObjectSpecsToPass(passId, [], {
9291
+ render: false,
9292
+ replace: true
9293
+ });
8907
9294
  }
8908
- await this.applyObjectSpecsToRootLayer(layerId, specs, { render: false });
8909
- }
8910
- for (const layerId of this.managedProducerRootLayerIds) {
8911
- if (nextRootLayerIds.has(layerId)) continue;
8912
- await this.applyObjectSpecsToRootLayer(layerId, [], { render: false });
9295
+ this.managedProducerPassIds = nextPassIds;
9296
+ this.managedPassMetas = nextManagedPassMetas;
9297
+ this.syncManagedPassStacking(Array.from(nextManagedPassMetas.values()));
9298
+ await this.applyManagedPassEffects(nextEffects);
9299
+ this.applyManagedPassVisibility({ render: false });
9300
+ } finally {
9301
+ this.producerApplyInProgress = false;
8913
9302
  }
8914
- this.managedProducerLayerIds = nextLayerIds;
8915
- this.managedProducerRootLayerIds = nextRootLayerIds;
8916
9303
  this.requestRenderAll();
8917
9304
  }
8918
- /**
8919
- * Get a layer (Group) by its ID.
8920
- * We assume layers are Groups directly on the canvas with a data.id property.
8921
- */
8922
- getLayer(id) {
8923
- return this.canvas.getObjects().find((obj) => {
8924
- var _a;
8925
- return ((_a = obj.data) == null ? void 0 : _a.id) === id;
8926
- });
8927
- }
8928
- /**
8929
- * Create a layer (Group) with the given ID if it doesn't exist.
8930
- */
8931
- createLayer(id, options = {}) {
8932
- let layer = this.getLayer(id);
8933
- if (!layer) {
8934
- const defaultOptions = {
8935
- selectable: false,
8936
- evented: false,
8937
- ...options,
8938
- data: { ...options.data, id }
8939
- };
8940
- layer = new Group([], defaultOptions);
8941
- this.canvas.add(layer);
8942
- }
8943
- return layer;
8944
- }
8945
- /**
8946
- * Find an object by ID, optionally within a specific layer.
8947
- */
8948
- getObject(id, layerId) {
8949
- if (layerId) {
8950
- const layer = this.getLayer(layerId);
8951
- if (!layer) return void 0;
8952
- return layer.getObjects().find((obj) => {
8953
- var _a;
8954
- return ((_a = obj.data) == null ? void 0 : _a.id) === id;
9305
+ async applyManagedPassEffects(effects) {
9306
+ const effectTargetMap = /* @__PURE__ */ new Map();
9307
+ for (const effect of effects) {
9308
+ if (effect.type !== "clipPath") continue;
9309
+ effect.targetPassIds.forEach((targetPassId) => {
9310
+ this.getPassCanvasObjects(targetPassId).forEach((obj) => {
9311
+ effectTargetMap.set(obj, effect);
9312
+ });
8955
9313
  });
8956
9314
  }
9315
+ const managedObjects = this.canvas.getObjects().filter(
9316
+ (obj) => this.isManagedPassObject(obj)
9317
+ );
9318
+ const effectTemplateCache = /* @__PURE__ */ new Map();
9319
+ for (const obj of managedObjects) {
9320
+ const targetEffect = effectTargetMap.get(obj);
9321
+ if (!targetEffect) {
9322
+ this.clearClipPathEffectFromObject(obj);
9323
+ continue;
9324
+ }
9325
+ let template = effectTemplateCache.get(targetEffect.key);
9326
+ if (template === void 0) {
9327
+ template = await this.createClipPathTemplate(targetEffect);
9328
+ effectTemplateCache.set(targetEffect.key, template);
9329
+ }
9330
+ if (!template) {
9331
+ this.clearClipPathEffectFromObject(obj);
9332
+ continue;
9333
+ }
9334
+ await this.applyClipPathEffectToObject(
9335
+ obj,
9336
+ template,
9337
+ targetEffect.key
9338
+ );
9339
+ }
9340
+ }
9341
+ getObject(id, passId) {
9342
+ const normalizedId = String(id || "").trim();
9343
+ if (!normalizedId) return void 0;
8957
9344
  return this.canvas.getObjects().find((obj) => {
8958
- var _a;
8959
- return ((_a = obj.data) == null ? void 0 : _a.id) === id;
9345
+ var _a, _b;
9346
+ if (((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id) !== normalizedId) return false;
9347
+ if (!passId) return true;
9348
+ return ((_b = obj == null ? void 0 : obj.data) == null ? void 0 : _b.passId) === passId;
8960
9349
  });
8961
9350
  }
8962
9351
  requestRenderAll() {
@@ -9142,114 +9531,187 @@ var CanvasService = class {
9142
9531
  next.top = objectTop + objectHeight * this.originFactor(originY);
9143
9532
  return next;
9144
9533
  }
9145
- setLayerVisibility(layerId, visible) {
9146
- const layer = this.getLayer(layerId);
9147
- if (layer) {
9148
- layer.set({ visible });
9149
- }
9150
- const objects = this.getRootLayerObjects(layerId);
9534
+ setPassVisibility(passId, visible) {
9535
+ const objects = this.getPassCanvasObjects(passId);
9536
+ let changed = false;
9151
9537
  objects.forEach((obj) => {
9152
9538
  var _a, _b;
9539
+ if (obj.visible === visible) return;
9153
9540
  (_a = obj.set) == null ? void 0 : _a.call(obj, { visible });
9154
9541
  (_b = obj.setCoords) == null ? void 0 : _b.call(obj);
9542
+ changed = true;
9155
9543
  });
9544
+ return changed;
9156
9545
  }
9157
- bringLayerToFront(layerId) {
9158
- const layer = this.getLayer(layerId);
9159
- if (layer) {
9160
- this.canvas.bringObjectToFront(layer);
9161
- }
9162
- const objects = this.getRootLayerObjects(layerId);
9163
- objects.forEach((obj) => this.canvas.bringObjectToFront(obj));
9546
+ setLayerVisibility(layerId, visible) {
9547
+ return this.setPassVisibility(layerId, visible);
9164
9548
  }
9165
- async applyLayerSpec(spec) {
9166
- const layer = this.createLayer(spec.id, spec.props || {});
9167
- await this.applyObjectSpecsToContainer(layer, spec.objects);
9549
+ bringPassToFront(passId) {
9550
+ const objects = this.getPassCanvasObjects(passId);
9551
+ objects.forEach((obj) => this.canvas.bringObjectToFront(obj));
9168
9552
  }
9169
- async applyObjectSpecsToLayer(layerId, objects, options = {}) {
9170
- const layer = this.createLayer(layerId, {});
9171
- await this.applyObjectSpecsToContainer(layer, objects, options);
9553
+ bringLayerToFront(layerId) {
9554
+ this.bringPassToFront(layerId);
9172
9555
  }
9173
- getRootLayerObjects(layerId) {
9174
- return this.canvas.getObjects().filter((obj) => {
9175
- var _a;
9176
- return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === layerId;
9556
+ async applyPassSpec(spec, options = {}) {
9557
+ await this.applyObjectSpecsToPass(spec.id, spec.objects, {
9558
+ render: options.render,
9559
+ replace: spec.replace !== false
9177
9560
  });
9178
9561
  }
9179
- async applyObjectSpecsToRootLayer(layerId, specs, options = {}) {
9180
- const desiredIds = new Set(specs.map((s) => s.id));
9181
- const existing = this.getRootLayerObjects(layerId);
9182
- existing.forEach((obj) => {
9183
- var _a;
9184
- const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
9185
- if (typeof id === "string" && !desiredIds.has(id)) {
9186
- this.canvas.remove(obj);
9187
- }
9562
+ async applyObjectSpecsToRootLayer(passId, specs, options = {}) {
9563
+ await this.applyObjectSpecsToPass(passId, specs, {
9564
+ render: options.render,
9565
+ replace: true
9188
9566
  });
9189
- const byId = /* @__PURE__ */ new Map();
9190
- this.getRootLayerObjects(layerId).forEach((obj) => {
9191
- var _a;
9192
- const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
9193
- if (typeof id === "string") byId.set(id, obj);
9567
+ }
9568
+ normalizeObjectSpecs(specs) {
9569
+ const seen = /* @__PURE__ */ new Set();
9570
+ const normalized = [];
9571
+ (specs || []).forEach((spec) => {
9572
+ const id = String((spec == null ? void 0 : spec.id) || "").trim();
9573
+ if (!id || seen.has(id)) return;
9574
+ seen.add(id);
9575
+ normalized.push({
9576
+ ...spec,
9577
+ id
9578
+ });
9194
9579
  });
9195
- for (let index = 0; index < specs.length; index += 1) {
9196
- const spec = specs[index];
9197
- let current = byId.get(spec.id);
9198
- if (current && spec.type === "image" && spec.src && current.getSrc && current.getSrc() !== spec.src) {
9199
- this.canvas.remove(current);
9200
- byId.delete(spec.id);
9201
- current = void 0;
9202
- }
9203
- if (!current) {
9204
- const created = await this.createFabricObject(spec);
9205
- if (!created) continue;
9206
- this.patchFabricObject(created, spec, { layerId });
9207
- this.canvas.add(created);
9208
- byId.set(spec.id, created);
9209
- continue;
9210
- }
9211
- this.patchFabricObject(current, spec, { layerId });
9580
+ return normalized;
9581
+ }
9582
+ async cloneFabricObject(source) {
9583
+ const clone = source.clone;
9584
+ if (typeof clone !== "function") return void 0;
9585
+ const result = clone.call(source);
9586
+ if (!result || typeof result.then !== "function") {
9587
+ return void 0;
9212
9588
  }
9213
- if (options.render !== false) {
9214
- this.requestRenderAll();
9589
+ try {
9590
+ const copied = await result;
9591
+ return copied;
9592
+ } catch (e) {
9593
+ return void 0;
9215
9594
  }
9216
9595
  }
9217
- async applyObjectSpecsToContainer(container, specs, options = {}) {
9218
- const desiredIds = new Set(specs.map((s) => s.id));
9219
- const existing = container.getObjects();
9220
- existing.forEach((obj) => {
9221
- var _a;
9222
- const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
9223
- if (typeof id === "string" && !desiredIds.has(id)) {
9224
- container.remove(obj);
9596
+ async createClipPathTemplate(effect) {
9597
+ var _a, _b;
9598
+ const source = effect.source;
9599
+ const sourceId = String(source.id || "").trim();
9600
+ if (!sourceId) return null;
9601
+ const template = await this.createFabricObject({
9602
+ ...source,
9603
+ id: sourceId,
9604
+ data: {
9605
+ ...source.data || {},
9606
+ id: sourceId,
9607
+ type: "clip-path-effect-template",
9608
+ effectKey: effect.key
9609
+ },
9610
+ props: {
9611
+ ...source.props || {},
9612
+ selectable: false,
9613
+ evented: false,
9614
+ excludeFromExport: true
9225
9615
  }
9226
9616
  });
9617
+ if (!template) return null;
9618
+ (_a = template.set) == null ? void 0 : _a.call(template, {
9619
+ selectable: false,
9620
+ evented: false,
9621
+ excludeFromExport: true,
9622
+ absolutePositioned: true
9623
+ });
9624
+ (_b = template.setCoords) == null ? void 0 : _b.call(template);
9625
+ return template;
9626
+ }
9627
+ isClipPathEffectManaged(target) {
9628
+ return typeof (target == null ? void 0 : target.__pooderEffectClipKey) === "string";
9629
+ }
9630
+ clearClipPathEffectFromObject(target) {
9631
+ var _a, _b;
9632
+ if (!target) return;
9633
+ if (!this.isClipPathEffectManaged(target)) return;
9634
+ (_a = target.set) == null ? void 0 : _a.call(target, { clipPath: void 0 });
9635
+ (_b = target.setCoords) == null ? void 0 : _b.call(target);
9636
+ delete target.__pooderEffectClipKey;
9637
+ }
9638
+ async applyClipPathEffectToObject(target, clipTemplate, effectKey) {
9639
+ var _a, _b, _c, _d;
9640
+ if (!target) return;
9641
+ const clipPath = await this.cloneFabricObject(clipTemplate);
9642
+ if (!clipPath) {
9643
+ this.clearClipPathEffectFromObject(target);
9644
+ return;
9645
+ }
9646
+ (_a = clipPath.set) == null ? void 0 : _a.call(clipPath, {
9647
+ selectable: false,
9648
+ evented: false,
9649
+ excludeFromExport: true,
9650
+ absolutePositioned: true
9651
+ });
9652
+ (_b = clipPath.setCoords) == null ? void 0 : _b.call(clipPath);
9653
+ (_c = target.set) == null ? void 0 : _c.call(target, { clipPath });
9654
+ (_d = target.setCoords) == null ? void 0 : _d.call(target);
9655
+ target.__pooderEffectClipKey = effectKey;
9656
+ }
9657
+ async applyObjectSpecsToPass(passId, specs, options = {}) {
9658
+ const normalizedPassId = String(passId || "").trim();
9659
+ if (!normalizedPassId) return;
9660
+ const replace = options.replace !== false;
9661
+ const normalizedSpecs = this.normalizeObjectSpecs(specs);
9662
+ const desiredIds = new Set(normalizedSpecs.map((s) => s.id));
9663
+ const existing = this.getPassCanvasObjects(normalizedPassId);
9664
+ if (replace) {
9665
+ existing.forEach((obj) => {
9666
+ var _a;
9667
+ const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
9668
+ if (typeof id === "string" && !desiredIds.has(id)) {
9669
+ this.canvas.remove(obj);
9670
+ }
9671
+ });
9672
+ }
9227
9673
  const byId = /* @__PURE__ */ new Map();
9228
- container.getObjects().forEach((obj) => {
9674
+ this.getPassCanvasObjects(normalizedPassId).forEach((obj) => {
9229
9675
  var _a;
9230
9676
  const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
9231
9677
  if (typeof id === "string") byId.set(id, obj);
9232
9678
  });
9233
- for (let index = 0; index < specs.length; index += 1) {
9234
- const spec = specs[index];
9679
+ for (let index = 0; index < normalizedSpecs.length; index += 1) {
9680
+ const spec = normalizedSpecs[index];
9235
9681
  let current = byId.get(spec.id);
9236
- if (current && spec.type === "image" && spec.src && current.getSrc && current.getSrc() !== spec.src) {
9237
- container.remove(current);
9682
+ if (spec.type === "path") {
9683
+ const nextPathData = this.readPathDataFromSpec(spec);
9684
+ if (!nextPathData || !nextPathData.trim()) {
9685
+ if (current) {
9686
+ this.canvas.remove(current);
9687
+ byId.delete(spec.id);
9688
+ }
9689
+ continue;
9690
+ }
9691
+ }
9692
+ if (current && this.shouldRecreateObject(current, spec)) {
9693
+ this.canvas.remove(current);
9238
9694
  byId.delete(spec.id);
9239
9695
  current = void 0;
9240
9696
  }
9241
9697
  if (!current) {
9242
9698
  const created = await this.createFabricObject(spec);
9243
9699
  if (!created) continue;
9244
- container.add(created);
9245
- current = created;
9246
- byId.set(spec.id, current);
9247
- } else {
9248
- this.patchFabricObject(current, spec);
9700
+ this.patchFabricObject(created, spec, {
9701
+ passId: normalizedPassId,
9702
+ layerId: normalizedPassId,
9703
+ passOrder: index
9704
+ });
9705
+ this.canvas.add(created);
9706
+ byId.set(spec.id, created);
9707
+ continue;
9249
9708
  }
9250
- this.moveObjectInContainer(container, current, index);
9709
+ this.patchFabricObject(current, spec, {
9710
+ passId: normalizedPassId,
9711
+ layerId: normalizedPassId,
9712
+ passOrder: index
9713
+ });
9251
9714
  }
9252
- container.dirty = true;
9253
9715
  if (options.render !== false) {
9254
9716
  this.requestRenderAll();
9255
9717
  }
@@ -9261,10 +9723,56 @@ var CanvasService = class {
9261
9723
  ...extraData || {},
9262
9724
  id: spec.id
9263
9725
  };
9726
+ nextData.__renderSourceKey = this.getSpecRenderSourceKey(spec);
9264
9727
  const props = this.resolveFabricProps(spec, spec.props || {});
9265
9728
  obj.set({ ...props, data: nextData });
9266
9729
  obj.setCoords();
9267
9730
  }
9731
+ readPathDataFromSpec(spec) {
9732
+ var _a, _b;
9733
+ if (spec.type !== "path") return void 0;
9734
+ const raw = ((_a = spec.props) == null ? void 0 : _a.path) || ((_b = spec.props) == null ? void 0 : _b.pathData);
9735
+ if (typeof raw !== "string") return void 0;
9736
+ return raw;
9737
+ }
9738
+ hashText(value) {
9739
+ let hash = 2166136261;
9740
+ for (let i = 0; i < value.length; i += 1) {
9741
+ hash ^= value.charCodeAt(i);
9742
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
9743
+ }
9744
+ return (hash >>> 0).toString(16);
9745
+ }
9746
+ getSpecRenderSourceKey(spec) {
9747
+ var _a, _b;
9748
+ switch (spec.type) {
9749
+ case "path": {
9750
+ const pathData = this.readPathDataFromSpec(spec) || "";
9751
+ return `path:${this.hashText(pathData)}`;
9752
+ }
9753
+ case "image":
9754
+ return `image:${String(spec.src || "")}`;
9755
+ case "text":
9756
+ return `text:${String((_b = (_a = spec.props) == null ? void 0 : _a.text) != null ? _b : "")}`;
9757
+ case "rect":
9758
+ return "rect";
9759
+ default:
9760
+ return String(spec.type || "");
9761
+ }
9762
+ }
9763
+ shouldRecreateObject(current, spec) {
9764
+ var _a;
9765
+ if (!current) return true;
9766
+ const currentType = String((current == null ? void 0 : current.type) || "").toLowerCase();
9767
+ if (currentType !== spec.type) return true;
9768
+ const expectedKey = this.getSpecRenderSourceKey(spec);
9769
+ const currentKey = String(((_a = current == null ? void 0 : current.data) == null ? void 0 : _a.__renderSourceKey) || "");
9770
+ if (currentKey && expectedKey && currentKey !== expectedKey) return true;
9771
+ if (spec.type === "image" && spec.src && current.getSrc) {
9772
+ return current.getSrc() !== spec.src;
9773
+ }
9774
+ return false;
9775
+ }
9268
9776
  resolveFabricProps(spec, props) {
9269
9777
  const space = spec.space || "scene";
9270
9778
  const next = this.resolveLayoutProps(spec, props);
@@ -9288,26 +9796,26 @@ var CanvasService = class {
9288
9796
  next.scaleY = rawScaleY * sceneScale;
9289
9797
  return next;
9290
9798
  }
9291
- moveObjectInContainer(container, obj, index) {
9799
+ moveObjectInCanvas(obj, index) {
9292
9800
  if (!obj) return;
9293
- const moveObjectTo = container.moveObjectTo;
9801
+ const moveObjectTo = this.canvas.moveObjectTo;
9294
9802
  if (typeof moveObjectTo === "function") {
9295
- moveObjectTo.call(container, obj, index);
9803
+ moveObjectTo.call(this.canvas, obj, index);
9296
9804
  return;
9297
9805
  }
9298
- const list = container._objects;
9806
+ const list = this.canvas._objects;
9299
9807
  if (!Array.isArray(list)) return;
9300
9808
  const from = list.indexOf(obj);
9301
9809
  if (from < 0 || from === index) return;
9302
9810
  list.splice(from, 1);
9303
9811
  const target = Math.max(0, Math.min(index, list.length));
9304
9812
  list.splice(target, 0, obj);
9305
- if (typeof container._onStackOrderChanged === "function") {
9306
- container._onStackOrderChanged();
9813
+ if (typeof this.canvas._onStackOrderChanged === "function") {
9814
+ this.canvas._onStackOrderChanged();
9307
9815
  }
9308
9816
  }
9309
9817
  async createFabricObject(spec) {
9310
- var _a, _b, _c, _d;
9818
+ var _a, _b;
9311
9819
  if (spec.type === "rect") {
9312
9820
  const props = this.resolveFabricProps(spec, spec.props || {});
9313
9821
  const rect = new Rect({
@@ -9318,7 +9826,7 @@ var CanvasService = class {
9318
9826
  return rect;
9319
9827
  }
9320
9828
  if (spec.type === "path") {
9321
- const pathData = ((_a = spec.props) == null ? void 0 : _a.path) || ((_b = spec.props) == null ? void 0 : _b.pathData);
9829
+ const pathData = this.readPathDataFromSpec(spec);
9322
9830
  if (!pathData) return void 0;
9323
9831
  const props = this.resolveFabricProps(spec, spec.props || {});
9324
9832
  const path = new Path2(pathData, {
@@ -9340,7 +9848,7 @@ var CanvasService = class {
9340
9848
  return image;
9341
9849
  }
9342
9850
  if (spec.type === "text") {
9343
- const content = String((_d = (_c = spec.props) == null ? void 0 : _c.text) != null ? _d : "");
9851
+ const content = String((_b = (_a = spec.props) == null ? void 0 : _a.text) != null ? _b : "");
9344
9852
  const props = this.resolveFabricProps(spec, spec.props || {});
9345
9853
  const text = new Text(content, {
9346
9854
  ...props,
@@ -9362,8 +9870,8 @@ export {
9362
9870
  MirrorTool,
9363
9871
  RulerTool,
9364
9872
  SceneLayoutService,
9365
- SceneVisibilityService,
9366
9873
  SizeTool,
9367
9874
  ViewportSystem,
9368
- WhiteInkTool
9875
+ WhiteInkTool,
9876
+ evaluateVisibilityExpr
9369
9877
  };