@pooder/kit 5.4.0 → 6.0.1

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 +935 -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 +1788 -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 +1032 -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 +57 -0
  30. package/.test-dist/src/units.js +30 -0
  31. package/.test-dist/tests/run.js +150 -0
  32. package/CHANGELOG.md +12 -0
  33. package/dist/index.d.mts +164 -62
  34. package/dist/index.d.ts +164 -62
  35. package/dist/index.js +2433 -1719
  36. package/dist/index.mjs +2442 -1723
  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 +1173 -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 +2240 -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 +1317 -832
  62. package/src/services/ViewportSystem.ts +95 -95
  63. package/src/services/index.ts +4 -3
  64. package/src/services/renderSpec.ts +85 -53
  65. package/src/services/visibility.ts +83 -0
  66. package/src/units.ts +27 -27
  67. package/tests/run.ts +258 -118
  68. package/tsconfig.test.json +15 -15
  69. package/src/extensions/sceneVisibility.ts +0 -64
package/dist/index.js CHANGED
@@ -39,294 +39,104 @@ __export(index_exports, {
39
39
  MirrorTool: () => MirrorTool,
40
40
  RulerTool: () => RulerTool,
41
41
  SceneLayoutService: () => SceneLayoutService,
42
- SceneVisibilityService: () => SceneVisibilityService,
43
42
  SizeTool: () => SizeTool,
44
43
  ViewportSystem: () => ViewportSystem,
45
- WhiteInkTool: () => WhiteInkTool
44
+ WhiteInkTool: () => WhiteInkTool,
45
+ evaluateVisibilityExpr: () => evaluateVisibilityExpr
46
46
  });
47
47
  module.exports = __toCommonJS(index_exports);
48
48
 
49
49
  // src/extensions/background.ts
50
50
  var import_core = require("@pooder/core");
51
51
  var import_fabric = require("fabric");
52
- var BACKGROUND_LAYER_ID = "background";
53
- var BACKGROUND_RECT_ID = "background-color-rect";
54
- var BACKGROUND_IMAGE_ID = "background-image";
55
- var DEFAULT_WIDTH = 800;
56
- var DEFAULT_HEIGHT = 600;
57
- var BackgroundTool = class {
58
- constructor(options) {
59
- this.id = "pooder.kit.background";
60
- this.metadata = {
61
- name: "BackgroundTool"
62
- };
63
- this.color = "";
64
- this.url = "";
65
- this.specs = [];
66
- this.renderSeq = 0;
67
- this.renderImageUrl = "";
68
- this.sourceSizeBySrc = /* @__PURE__ */ new Map();
69
- this.pendingSizeBySrc = /* @__PURE__ */ new Map();
70
- this.onCanvasResized = () => {
71
- this.updateBackground();
72
- };
73
- if (options) {
74
- Object.assign(this, options);
52
+
53
+ // src/coordinate.ts
54
+ var Coordinate = class {
55
+ /**
56
+ * Calculate layout to fit content within container while preserving aspect ratio.
57
+ */
58
+ static calculateLayout(container, content, padding = 0) {
59
+ const availableWidth = Math.max(0, container.width - padding * 2);
60
+ const availableHeight = Math.max(0, container.height - padding * 2);
61
+ if (content.width === 0 || content.height === 0) {
62
+ return { scale: 1, offsetX: 0, offsetY: 0, width: 0, height: 0 };
75
63
  }
64
+ const scaleX = availableWidth / content.width;
65
+ const scaleY = availableHeight / content.height;
66
+ const scale = Math.min(scaleX, scaleY);
67
+ const width = content.width * scale;
68
+ const height = content.height * scale;
69
+ const offsetX = (container.width - width) / 2;
70
+ const offsetY = (container.height - height) / 2;
71
+ return { scale, offsetX, offsetY, width, height };
76
72
  }
77
- activate(context) {
78
- var _a;
79
- this.canvasService = context.services.get("CanvasService");
80
- if (!this.canvasService) {
81
- console.warn("CanvasService not found for BackgroundTool");
82
- return;
83
- }
84
- (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
85
- this.renderProducerDisposable = this.canvasService.registerRenderProducer(
86
- this.id,
87
- () => ({
88
- layerSpecs: {
89
- [BACKGROUND_LAYER_ID]: this.specs
90
- }
91
- }),
92
- { priority: 0 }
93
- );
94
- const configService = context.services.get(
95
- "ConfigurationService"
96
- );
97
- if (configService) {
98
- this.color = configService.get("background.color", this.color);
99
- this.url = configService.get("background.url", this.url);
100
- configService.onAnyChange((e) => {
101
- if (e.key.startsWith("background.")) {
102
- const prop = e.key.split(".")[1];
103
- console.log(
104
- `[BackgroundTool] Config change detected: ${e.key} -> ${e.value}, prop: ${prop}`
105
- );
106
- if (prop && prop in this) {
107
- console.log(
108
- `[BackgroundTool] Updating option ${prop} to ${e.value}`
109
- );
110
- this[prop] = e.value;
111
- this.updateBackground();
112
- } else {
113
- console.warn(
114
- `[BackgroundTool] Property ${prop} not found in options`
115
- );
116
- }
117
- }
118
- });
119
- }
120
- context.eventBus.on("canvas:resized", this.onCanvasResized);
121
- this.updateBackground();
73
+ /**
74
+ * Convert an absolute value to a normalized value (0-1).
75
+ * @param value Absolute value (e.g., pixels)
76
+ * @param total Total dimension size (e.g., canvas width)
77
+ */
78
+ static toNormalized(value, total) {
79
+ return total === 0 ? 0 : value / total;
122
80
  }
123
- deactivate(context) {
124
- var _a;
125
- context.eventBus.off("canvas:resized", this.onCanvasResized);
126
- this.renderSeq += 1;
127
- this.specs = [];
128
- this.renderImageUrl = "";
129
- (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
130
- this.renderProducerDisposable = void 0;
131
- if (!this.canvasService) return;
132
- const layer = this.canvasService.getLayer(BACKGROUND_LAYER_ID);
133
- if (layer) {
134
- this.canvasService.canvas.remove(layer);
135
- }
136
- void this.canvasService.flushRenderFromProducers();
137
- this.canvasService.requestRenderAll();
138
- this.canvasService = void 0;
81
+ /**
82
+ * Convert a normalized value (0-1) to an absolute value.
83
+ * @param normalized Normalized value (0-1)
84
+ * @param total Total dimension size (e.g., canvas width)
85
+ */
86
+ static toAbsolute(normalized, total) {
87
+ return normalized * total;
139
88
  }
140
- contribute() {
89
+ /**
90
+ * Normalize a point's coordinates.
91
+ */
92
+ static normalizePoint(point, size) {
141
93
  return {
142
- [import_core.ContributionPointIds.CONFIGURATIONS]: [
143
- {
144
- id: "background.color",
145
- type: "color",
146
- label: "Background Color",
147
- default: ""
148
- },
149
- {
150
- id: "background.url",
151
- type: "string",
152
- label: "Image URL",
153
- default: ""
154
- }
155
- ],
156
- [import_core.ContributionPointIds.COMMANDS]: [
157
- {
158
- command: "reset",
159
- title: "Reset Background",
160
- handler: () => {
161
- this.updateBackground();
162
- return true;
163
- }
164
- },
165
- {
166
- command: "clear",
167
- title: "Clear Background",
168
- handler: () => {
169
- this.color = "transparent";
170
- this.url = "";
171
- this.updateBackground();
172
- return true;
173
- }
174
- },
175
- {
176
- command: "setBackgroundColor",
177
- title: "Set Background Color",
178
- handler: (color) => {
179
- if (this.color === color) return true;
180
- this.color = color;
181
- this.updateBackground();
182
- return true;
183
- }
184
- },
185
- {
186
- command: "setBackgroundImage",
187
- title: "Set Background Image",
188
- handler: (url) => {
189
- if (this.url === url) return true;
190
- this.url = url;
191
- this.updateBackground();
192
- return true;
193
- }
194
- }
195
- ]
94
+ x: this.toNormalized(point.x, size.width),
95
+ y: this.toNormalized(point.y, size.height)
196
96
  };
197
97
  }
198
- getViewportSize() {
199
- var _a, _b;
200
- const width = Number(((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 0);
201
- const height = Number(((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 0);
98
+ /**
99
+ * Denormalize a point's coordinates to absolute pixels.
100
+ */
101
+ static denormalizePoint(point, size) {
202
102
  return {
203
- width: width > 0 ? width : DEFAULT_WIDTH,
204
- height: height > 0 ? height : DEFAULT_HEIGHT
103
+ x: this.toAbsolute(point.x, size.width),
104
+ y: this.toAbsolute(point.y, size.height)
205
105
  };
206
106
  }
207
- buildBackgroundSpecs(color, imageUrl) {
208
- const { width, height } = this.getViewportSize();
209
- const specs = [
210
- {
211
- id: BACKGROUND_RECT_ID,
212
- type: "rect",
213
- space: "screen",
214
- data: {
215
- id: BACKGROUND_RECT_ID,
216
- layerId: BACKGROUND_LAYER_ID,
217
- type: "background-color"
218
- },
219
- props: {
220
- left: 0,
221
- top: 0,
222
- width,
223
- height,
224
- originX: "left",
225
- originY: "top",
226
- fill: color,
227
- selectable: false,
228
- evented: false,
229
- excludeFromExport: true
230
- }
231
- }
232
- ];
233
- if (!imageUrl) {
234
- return specs;
235
- }
236
- const sourceSize = this.sourceSizeBySrc.get(imageUrl);
237
- const sourceWidth = Math.max(1, Number((sourceSize == null ? void 0 : sourceSize.width) || width));
238
- const sourceHeight = Math.max(1, Number((sourceSize == null ? void 0 : sourceSize.height) || height));
239
- const coverScale = Math.max(width / sourceWidth, height / sourceHeight);
240
- specs.push({
241
- id: BACKGROUND_IMAGE_ID,
242
- type: "image",
243
- src: imageUrl,
244
- space: "screen",
245
- data: {
246
- id: BACKGROUND_IMAGE_ID,
247
- layerId: BACKGROUND_LAYER_ID,
248
- type: "background-image"
249
- },
250
- props: {
251
- left: 0,
252
- top: 0,
253
- originX: "left",
254
- originY: "top",
255
- scaleX: coverScale,
256
- scaleY: coverScale,
257
- selectable: false,
258
- evented: false,
259
- excludeFromExport: true
260
- }
261
- });
262
- return specs;
263
- }
264
- async ensureImageSize(src) {
265
- if (!src) return null;
266
- const cached = this.sourceSizeBySrc.get(src);
267
- if (cached) return cached;
268
- const pending = this.pendingSizeBySrc.get(src);
269
- if (pending) {
270
- return pending;
271
- }
272
- const task = this.loadImageSize(src);
273
- this.pendingSizeBySrc.set(src, task);
274
- try {
275
- return await task;
276
- } finally {
277
- if (this.pendingSizeBySrc.get(src) === task) {
278
- this.pendingSizeBySrc.delete(src);
279
- }
280
- }
281
- }
282
- async loadImageSize(src) {
283
- try {
284
- const image = await import_fabric.FabricImage.fromURL(src, {
285
- crossOrigin: "anonymous"
286
- });
287
- const width = Number((image == null ? void 0 : image.width) || 0);
288
- const height = Number((image == null ? void 0 : image.height) || 0);
289
- if (width > 0 && height > 0) {
290
- const size = { width, height };
291
- this.sourceSizeBySrc.set(src, size);
292
- return size;
293
- }
294
- } catch (error) {
295
- console.error("[BackgroundTool] Failed to load image", src, error);
107
+ static convertUnit(value, from, to) {
108
+ if (from === to) return value;
109
+ const toMM = {
110
+ px: 0.264583,
111
+ // 1px = 0.264583mm (96 DPI)
112
+ mm: 1,
113
+ cm: 10,
114
+ in: 25.4
115
+ };
116
+ const mmValue = value * (from === "px" ? toMM.px : toMM[from] || 1);
117
+ if (to === "px") {
118
+ return mmValue / toMM.px;
296
119
  }
297
- return null;
120
+ return mmValue / (toMM[to] || 1);
298
121
  }
299
- updateBackground() {
300
- void this.updateBackgroundAsync();
122
+ };
123
+
124
+ // src/units.ts
125
+ function parseLengthToMm(input, defaultUnit) {
126
+ var _a, _b;
127
+ if (typeof input === "number") {
128
+ if (!Number.isFinite(input)) return 0;
129
+ return Coordinate.convertUnit(input, defaultUnit, "mm");
301
130
  }
302
- async updateBackgroundAsync() {
303
- if (!this.canvasService) return;
304
- const seq = ++this.renderSeq;
305
- const color = this.color;
306
- const nextUrl = String(this.url || "").trim();
307
- if (!nextUrl) {
308
- this.renderImageUrl = "";
309
- } else if (nextUrl !== this.renderImageUrl) {
310
- const loaded = await this.ensureImageSize(nextUrl);
311
- if (seq !== this.renderSeq) return;
312
- if (loaded) {
313
- this.renderImageUrl = nextUrl;
314
- }
315
- }
316
- this.specs = this.buildBackgroundSpecs(color, this.renderImageUrl);
317
- await this.canvasService.flushRenderFromProducers();
318
- if (seq !== this.renderSeq) return;
319
- const layer = this.canvasService.getLayer(BACKGROUND_LAYER_ID);
320
- if (layer) {
321
- this.canvasService.canvas.sendObjectToBack(layer);
322
- }
323
- this.canvasService.requestRenderAll();
324
- }
325
- };
326
-
327
- // src/extensions/image.ts
328
- var import_core2 = require("@pooder/core");
329
- var import_fabric2 = require("fabric");
131
+ const raw = input.trim();
132
+ if (!raw) return 0;
133
+ const match = raw.match(/^([+-]?\d+(?:\.\d+)?)\s*(px|mm|cm|in)?$/i);
134
+ if (!match) return 0;
135
+ const value = Number(match[1]);
136
+ if (!Number.isFinite(value)) return 0;
137
+ const unit = (_b = (_a = match[2]) == null ? void 0 : _a.toLowerCase()) != null ? _b : defaultUnit;
138
+ return Coordinate.convertUnit(value, unit, "mm");
139
+ }
330
140
 
331
141
  // src/extensions/dielineShape.ts
332
142
  var BUILTIN_DIELINE_SHAPES = [
@@ -343,7 +153,7 @@ var DEFAULT_HEART_SHAPE_PARAMS = {
343
153
  tipSharpness: 0
344
154
  };
345
155
  var DEFAULT_DIELINE_SHAPE_STYLE = {
346
- fitMode: "contain",
156
+ fitMode: "stretch",
347
157
  ...DEFAULT_HEART_SHAPE_PARAMS
348
158
  };
349
159
  function isDielineShape(value) {
@@ -393,968 +203,1420 @@ function getHeartShapeParams(style) {
393
203
  };
394
204
  }
395
205
 
396
- // src/extensions/geometry.ts
397
- var import_paper = __toESM(require("paper"));
398
-
399
- // src/extensions/bridgeSelection.ts
400
- function pickExitIndex(hits) {
401
- for (let i = 0; i < hits.length; i++) {
402
- const h = hits[i];
403
- if (h.insideBelow && !h.insideAbove) return i;
404
- }
405
- return -1;
206
+ // src/extensions/sceneLayoutModel.ts
207
+ var DEFAULT_SIZE_STATE = {
208
+ unit: "mm",
209
+ actualWidthMm: 500,
210
+ actualHeightMm: 500,
211
+ constraintMode: "free",
212
+ aspectRatio: 1,
213
+ cutMode: "trim",
214
+ cutMarginMm: 0,
215
+ viewPadding: 140,
216
+ minMm: 10,
217
+ maxMm: 2e3,
218
+ stepMm: 0.1
219
+ };
220
+ function clamp(value, min, max) {
221
+ return Math.max(min, Math.min(max, value));
406
222
  }
407
- function scoreOutsideAbove(samples) {
408
- let score = 0;
409
- for (const s of samples) {
410
- if (s.outsideAbove) score++;
411
- }
412
- return score;
223
+ function roundToStep(value, step) {
224
+ if (!Number.isFinite(step) || step <= 0) return value;
225
+ return Math.round(value / step) * step;
413
226
  }
414
-
415
- // src/extensions/wrappedOffsets.ts
416
- function wrappedDistance(total, start, end) {
417
- if (!Number.isFinite(total) || total <= 0) return 0;
418
- if (!Number.isFinite(start) || !Number.isFinite(end)) return 0;
419
- const s = (start % total + total) % total;
420
- const e = (end % total + total) % total;
421
- return e >= s ? e - s : total - s + e;
227
+ function sanitizeMmValue(valueMm, limits) {
228
+ if (!Number.isFinite(valueMm)) return limits.minMm;
229
+ const rounded = roundToStep(valueMm, limits.stepMm);
230
+ return clamp(rounded, limits.minMm, limits.maxMm);
422
231
  }
423
- function sampleWrappedOffsets(total, start, end, count) {
424
- if (!Number.isFinite(total) || total <= 0) return [];
425
- if (!Number.isFinite(start) || !Number.isFinite(end)) return [];
426
- const n = Math.max(0, Math.floor(count));
427
- if (n <= 0) return [];
428
- const dist = wrappedDistance(total, start, end);
429
- if (n === 1) return [(start % total + total) % total];
430
- const step = dist / (n - 1);
431
- const offsets = [];
432
- for (let i = 0; i < n; i++) {
433
- const raw = start + step * i;
434
- const wrapped = (raw % total + total) % total;
435
- offsets.push(wrapped);
436
- }
437
- return offsets;
232
+ function normalizeUnit(value) {
233
+ if (value === "cm" || value === "in") return value;
234
+ return "mm";
438
235
  }
439
-
440
- // src/extensions/geometry.ts
441
- function resolveFeaturePosition(feature, geometry) {
442
- const { x, y, width, height } = geometry;
443
- const left = x - width / 2;
444
- const top = y - height / 2;
445
- return {
446
- x: left + feature.x * width,
447
- y: top + feature.y * height
448
- };
236
+ function normalizeConstraintMode(value) {
237
+ if (value === "lockAspect" || value === "equal") return value;
238
+ return "free";
449
239
  }
450
- function ensurePaper(width, height) {
451
- if (!import_paper.default.project) {
452
- import_paper.default.setup(new import_paper.default.Size(width, height));
453
- } else {
454
- import_paper.default.view.viewSize = new import_paper.default.Size(width, height);
455
- }
240
+ function normalizeCutMode(value) {
241
+ if (value === "outset" || value === "inset") return value;
242
+ return "trim";
456
243
  }
457
- var isBridgeDebugEnabled = () => Boolean(globalThis.__POODER_BRIDGE_DEBUG__);
458
- function normalizePathItem(shape) {
459
- let result = shape;
460
- if (typeof result.resolveCrossings === "function") result = result.resolveCrossings();
461
- if (typeof result.reduce === "function") result = result.reduce({});
462
- if (typeof result.reorient === "function") result = result.reorient(true, true);
463
- if (typeof result.reduce === "function") result = result.reduce({});
464
- return result;
244
+ function toMm(value, fromUnit) {
245
+ return Coordinate.convertUnit(value, fromUnit, "mm");
465
246
  }
466
- function getBridgeDelta(itemBounds, overlap) {
467
- return Math.max(overlap, Math.min(5, Math.max(1, itemBounds.height * 0.02)));
247
+ function fromMm(valueMm, toUnit) {
248
+ return Coordinate.convertUnit(valueMm, "mm", toUnit);
468
249
  }
469
- function getExitHit(args) {
470
- const { mainShape, x, bridgeBottom, toY, eps, delta, overlap, op } = args;
471
- const ray = new import_paper.default.Path.Line({
472
- from: [x, bridgeBottom],
473
- to: [x, toY],
474
- insert: false
475
- });
476
- const intersections = mainShape.getIntersections(ray) || [];
477
- ray.remove();
478
- const validHits = intersections.filter((i) => i.point.y < bridgeBottom - eps);
479
- if (validHits.length === 0) return null;
480
- validHits.sort((a, b) => b.point.y - a.point.y);
481
- const flags = validHits.map((h) => {
482
- const above = h.point.add(new import_paper.default.Point(0, -delta));
483
- const below = h.point.add(new import_paper.default.Point(0, delta));
484
- return {
485
- insideAbove: mainShape.contains(above),
486
- insideBelow: mainShape.contains(below)
487
- };
488
- });
489
- const idx = pickExitIndex(flags);
490
- if (idx < 0) return null;
491
- if (isBridgeDebugEnabled()) {
492
- console.debug("Geometry: Bridge ray", {
493
- x,
494
- validHits: validHits.length,
495
- idx,
496
- delta,
497
- overlap,
498
- op
499
- });
250
+ function resolvePaddingPx(raw, containerWidth, containerHeight) {
251
+ if (typeof raw === "number") return Math.max(0, raw);
252
+ if (typeof raw === "string") {
253
+ if (raw.endsWith("%")) {
254
+ const percent = parseFloat(raw) / 100;
255
+ if (!Number.isFinite(percent)) return 0;
256
+ return Math.max(0, Math.min(containerWidth, containerHeight) * percent);
257
+ }
258
+ const fixed = parseFloat(raw);
259
+ return Number.isFinite(fixed) ? Math.max(0, fixed) : 0;
500
260
  }
501
- const hit = validHits[idx];
502
- return { point: hit.point, location: hit };
261
+ return 0;
503
262
  }
504
- function selectOuterChain(args) {
505
- const { mainShape, pointsA, pointsB, delta, overlap, op } = args;
506
- const scoreA = scoreOutsideAbove(
507
- pointsA.map((p) => ({
508
- outsideAbove: !mainShape.contains(p.add(new import_paper.default.Point(0, -delta)))
509
- }))
263
+ function readSizeState(configService) {
264
+ const unit = normalizeUnit(
265
+ configService.get("size.unit", DEFAULT_SIZE_STATE.unit)
510
266
  );
511
- const scoreB = scoreOutsideAbove(
512
- pointsB.map((p) => ({
513
- outsideAbove: !mainShape.contains(p.add(new import_paper.default.Point(0, -delta)))
514
- }))
267
+ const minMm = Math.max(
268
+ 0.1,
269
+ Number(configService.get("size.minMm", DEFAULT_SIZE_STATE.minMm))
515
270
  );
516
- const ratioA = scoreA / pointsA.length;
517
- const ratioB = scoreB / pointsB.length;
518
- if (isBridgeDebugEnabled()) {
519
- console.debug("Geometry: Bridge chain", {
520
- scoreA,
521
- scoreB,
522
- lenA: pointsA.length,
523
- lenB: pointsB.length,
524
- ratioA,
525
- ratioB,
526
- delta,
527
- overlap,
528
- op
529
- });
530
- }
531
- const ratioEps = 1e-6;
532
- if (Math.abs(ratioA - ratioB) > ratioEps) {
533
- return ratioA > ratioB ? pointsA : pointsB;
534
- }
535
- if (scoreA !== scoreB) return scoreA > scoreB ? pointsA : pointsB;
536
- return pointsA.length <= pointsB.length ? pointsA : pointsB;
537
- }
538
- function fitPathItemToRect(item, rect, fitMode) {
539
- const { left, top, width, height } = rect;
540
- const bounds = item.bounds;
541
- if (width <= 0 || height <= 0 || !Number.isFinite(bounds.width) || !Number.isFinite(bounds.height) || bounds.width <= 0 || bounds.height <= 0) {
542
- item.position = new import_paper.default.Point(left + width / 2, top + height / 2);
543
- return item;
544
- }
545
- item.translate(new import_paper.default.Point(-bounds.left, -bounds.top));
546
- if (fitMode === "stretch") {
547
- item.scale(width / bounds.width, height / bounds.height, new import_paper.default.Point(0, 0));
548
- item.translate(new import_paper.default.Point(left, top));
549
- return item;
550
- }
551
- const uniformScale = Math.min(width / bounds.width, height / bounds.height);
552
- item.scale(uniformScale, uniformScale, new import_paper.default.Point(0, 0));
553
- const scaledWidth = bounds.width * uniformScale;
554
- const scaledHeight = bounds.height * uniformScale;
555
- item.translate(
556
- new import_paper.default.Point(
557
- left + (width - scaledWidth) / 2,
558
- top + (height - scaledHeight) / 2
559
- )
271
+ const maxMm = Math.max(
272
+ minMm,
273
+ Number(configService.get("size.maxMm", DEFAULT_SIZE_STATE.maxMm))
560
274
  );
561
- return item;
562
- }
563
- function createNormalizedHeartPath(params) {
564
- const { lobeSpread, notchDepth, tipSharpness } = params;
565
- const halfSpread = 0.22 + lobeSpread * 0.18;
566
- const notchY = 0.06 + notchDepth * 0.2;
567
- const shoulderY = 0.24 + notchDepth * 0.2;
568
- const topLift = 0.12 + (1 - notchDepth) * 0.06;
569
- const topY = notchY - topLift;
570
- const sideCtrlY = shoulderY - (0.18 - notchDepth * 0.08);
571
- const lowerCtrlY = 0.58 + (1 - tipSharpness) * 0.16;
572
- const tipCtrlX = 0.34 - tipSharpness * 0.2;
573
- const notchCtrlX = 0.06 + lobeSpread * 0.06;
574
- const lobeCtrlX = 0.1 + lobeSpread * 0.08;
575
- const notchCtrlY = notchY - topLift * 0.45;
576
- const xPeakL = 0.5 - halfSpread;
577
- const xPeakR = 0.5 + halfSpread;
578
- const heartPath = new import_paper.default.Path({ insert: false });
579
- heartPath.moveTo(new import_paper.default.Point(0.5, notchY));
580
- heartPath.cubicCurveTo(
581
- new import_paper.default.Point(0.5 - notchCtrlX, notchCtrlY),
582
- new import_paper.default.Point(xPeakL + lobeCtrlX, topY),
583
- new import_paper.default.Point(xPeakL, topY)
275
+ const stepMm = Math.max(
276
+ 1e-3,
277
+ Number(configService.get("size.stepMm", DEFAULT_SIZE_STATE.stepMm))
584
278
  );
585
- heartPath.cubicCurveTo(
586
- new import_paper.default.Point(xPeakL - lobeCtrlX, topY),
587
- new import_paper.default.Point(0, sideCtrlY),
588
- new import_paper.default.Point(0, shoulderY)
279
+ const actualWidthMm = sanitizeMmValue(
280
+ parseLengthToMm(
281
+ configService.get("size.actualWidthMm", DEFAULT_SIZE_STATE.actualWidthMm),
282
+ "mm"
283
+ ),
284
+ { minMm, maxMm, stepMm }
589
285
  );
590
- heartPath.cubicCurveTo(
591
- new import_paper.default.Point(0, lowerCtrlY),
592
- new import_paper.default.Point(tipCtrlX, 1),
593
- new import_paper.default.Point(0.5, 1)
286
+ const actualHeightMm = sanitizeMmValue(
287
+ parseLengthToMm(
288
+ configService.get(
289
+ "size.actualHeightMm",
290
+ DEFAULT_SIZE_STATE.actualHeightMm
291
+ ),
292
+ "mm"
293
+ ),
294
+ { minMm, maxMm, stepMm }
594
295
  );
595
- heartPath.cubicCurveTo(
596
- new import_paper.default.Point(1 - tipCtrlX, 1),
597
- new import_paper.default.Point(1, lowerCtrlY),
598
- new import_paper.default.Point(1, shoulderY)
296
+ const aspectRaw = Number(
297
+ configService.get("size.aspectRatio", DEFAULT_SIZE_STATE.aspectRatio)
599
298
  );
600
- heartPath.cubicCurveTo(
601
- new import_paper.default.Point(1, sideCtrlY),
602
- new import_paper.default.Point(xPeakR + lobeCtrlX, topY),
603
- new import_paper.default.Point(xPeakR, topY)
299
+ const aspectRatio = Number.isFinite(aspectRaw) && aspectRaw > 0 ? aspectRaw : actualWidthMm / Math.max(1e-3, actualHeightMm);
300
+ const cutMarginMm = Math.max(
301
+ 0,
302
+ parseLengthToMm(
303
+ configService.get("size.cutMarginMm", DEFAULT_SIZE_STATE.cutMarginMm),
304
+ "mm"
305
+ )
604
306
  );
605
- heartPath.cubicCurveTo(
606
- new import_paper.default.Point(xPeakR - lobeCtrlX, topY),
607
- new import_paper.default.Point(0.5 + notchCtrlX, notchCtrlY),
608
- new import_paper.default.Point(0.5, notchY)
307
+ const viewPadding = configService.get(
308
+ "size.viewPadding",
309
+ DEFAULT_SIZE_STATE.viewPadding
609
310
  );
610
- heartPath.closed = true;
611
- return heartPath;
612
- }
613
- function createHeartBaseShape(options) {
614
- const { x, y, width, height } = options;
615
- const w = Math.max(0, width);
616
- const h = Math.max(0, height);
617
- const left = x - w / 2;
618
- const top = y - h / 2;
619
- const fitMode = getShapeFitMode(options.shapeStyle);
620
- const heartParams = getHeartShapeParams(options.shapeStyle);
621
- const rawHeart = createNormalizedHeartPath(heartParams);
622
- return fitPathItemToRect(rawHeart, { left, top, width: w, height: h }, fitMode);
311
+ return {
312
+ unit,
313
+ actualWidthMm,
314
+ actualHeightMm,
315
+ constraintMode: normalizeConstraintMode(
316
+ configService.get(
317
+ "size.constraintMode",
318
+ DEFAULT_SIZE_STATE.constraintMode
319
+ )
320
+ ),
321
+ aspectRatio,
322
+ cutMode: normalizeCutMode(
323
+ configService.get("size.cutMode", DEFAULT_SIZE_STATE.cutMode)
324
+ ),
325
+ cutMarginMm,
326
+ viewPadding,
327
+ minMm,
328
+ maxMm,
329
+ stepMm
330
+ };
623
331
  }
624
- var BUILTIN_SHAPE_BUILDERS = {
625
- rect: (options) => {
626
- const { x, y, width, height, radius } = options;
627
- return new import_paper.default.Path.Rectangle({
628
- point: [x - width / 2, y - height / 2],
629
- size: [Math.max(0, width), Math.max(0, height)],
630
- radius: Math.max(0, radius)
631
- });
632
- },
633
- circle: (options) => {
634
- const { x, y, width, height } = options;
635
- const r = Math.min(width, height) / 2;
636
- return new import_paper.default.Path.Circle({
637
- center: new import_paper.default.Point(x, y),
638
- radius: Math.max(0, r)
639
- });
640
- },
641
- ellipse: (options) => {
642
- const { x, y, width, height } = options;
643
- return new import_paper.default.Path.Ellipse({
644
- center: new import_paper.default.Point(x, y),
645
- radius: [Math.max(0, width / 2), Math.max(0, height / 2)]
646
- });
647
- },
648
- heart: createHeartBaseShape
649
- };
650
- function createCustomBaseShape(options) {
651
- var _a;
652
- const {
653
- pathData,
654
- customSourceWidthPx,
655
- customSourceHeightPx,
656
- x,
657
- y,
332
+ function rectByCenter(centerX, centerY, width, height) {
333
+ return {
334
+ left: centerX - width / 2,
335
+ top: centerY - height / 2,
658
336
  width,
659
- height
660
- } = options;
661
- if (typeof pathData !== "string" || pathData.trim().length === 0) {
662
- return null;
663
- }
664
- const center = new import_paper.default.Point(x, y);
665
- const hasMultipleSubPaths = ((_a = (pathData.match(/[Mm]/g) || []).length) != null ? _a : 0) > 1;
666
- const path = hasMultipleSubPaths ? new import_paper.default.CompoundPath(pathData) : (() => {
667
- const single = new import_paper.default.Path();
668
- single.pathData = pathData;
669
- return single;
670
- })();
671
- const sourceWidth = Number(customSourceWidthPx != null ? customSourceWidthPx : 0);
672
- const sourceHeight = Number(customSourceHeightPx != null ? customSourceHeightPx : 0);
673
- if (Number.isFinite(sourceWidth) && Number.isFinite(sourceHeight) && sourceWidth > 0 && sourceHeight > 0 && width > 0 && height > 0) {
674
- const targetLeft = x - width / 2;
675
- const targetTop = y - height / 2;
676
- path.scale(width / sourceWidth, height / sourceHeight, new import_paper.default.Point(0, 0));
677
- path.translate(new import_paper.default.Point(targetLeft, targetTop));
678
- return path;
679
- }
680
- if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
681
- path.position = center;
682
- path.scale(width / path.bounds.width, height / path.bounds.height);
683
- return path;
684
- }
685
- path.position = center;
686
- return path;
687
- }
688
- function createBaseShape(options) {
689
- const { shape } = options;
690
- if (shape === "custom") {
691
- const customShape = createCustomBaseShape(options);
692
- if (customShape) return customShape;
693
- return BUILTIN_SHAPE_BUILDERS[DEFAULT_DIELINE_SHAPE](options);
694
- }
695
- return BUILTIN_SHAPE_BUILDERS[shape](options);
337
+ height,
338
+ centerX,
339
+ centerY
340
+ };
696
341
  }
697
- function resolveBridgeBasePath(shape, anchor) {
698
- if (shape instanceof import_paper.default.Path) {
699
- return shape;
342
+ function getCutSizeMm(size) {
343
+ if (size.cutMode === "trim") {
344
+ return { widthMm: size.actualWidthMm, heightMm: size.actualHeightMm };
700
345
  }
701
- if (shape instanceof import_paper.default.CompoundPath) {
702
- const children = (shape.children || []).filter(
703
- (child) => child instanceof import_paper.default.Path
704
- );
705
- if (!children.length) return null;
706
- let best = children[0];
707
- let bestDistance = Infinity;
708
- for (const child of children) {
709
- const location = child.getNearestLocation(anchor);
710
- const point = location == null ? void 0 : location.point;
711
- if (!point) continue;
712
- const distance = point.getDistance(anchor);
713
- if (distance < bestDistance) {
714
- bestDistance = distance;
715
- best = child;
716
- }
717
- }
718
- return best;
346
+ const delta = size.cutMarginMm * 2;
347
+ if (size.cutMode === "outset") {
348
+ return {
349
+ widthMm: size.actualWidthMm + delta,
350
+ heightMm: size.actualHeightMm + delta
351
+ };
719
352
  }
720
- return null;
353
+ return {
354
+ widthMm: Math.max(size.minMm, size.actualWidthMm - delta),
355
+ heightMm: Math.max(size.minMm, size.actualHeightMm - delta)
356
+ };
721
357
  }
722
- function createFeatureItem(feature, center) {
723
- let item;
724
- if (feature.shape === "rect") {
725
- const w = feature.width || 10;
726
- const h = feature.height || 10;
727
- const r = feature.radius || 0;
728
- item = new import_paper.default.Path.Rectangle({
729
- point: [center.x - w / 2, center.y - h / 2],
730
- size: [w, h],
731
- radius: r
732
- });
733
- } else {
734
- const r = feature.radius || 5;
735
- item = new import_paper.default.Path.Circle({
736
- center,
737
- radius: r
738
- });
358
+ function computeSceneLayout(canvasService, size) {
359
+ const canvasWidth = canvasService.canvas.width || 0;
360
+ const canvasHeight = canvasService.canvas.height || 0;
361
+ if (canvasWidth <= 0 || canvasHeight <= 0) return null;
362
+ const { widthMm: cutWidthMm, heightMm: cutHeightMm } = getCutSizeMm(size);
363
+ const viewWidthMm = Math.max(size.actualWidthMm, cutWidthMm);
364
+ const viewHeightMm = Math.max(size.actualHeightMm, cutHeightMm);
365
+ if (!Number.isFinite(viewWidthMm) || !Number.isFinite(viewHeightMm) || viewWidthMm <= 0 || viewHeightMm <= 0) {
366
+ return null;
739
367
  }
740
- if (feature.rotation) {
741
- item.rotate(feature.rotation, center);
368
+ const paddingPx = resolvePaddingPx(
369
+ size.viewPadding,
370
+ canvasWidth,
371
+ canvasHeight
372
+ );
373
+ canvasService.viewport.updateContainer(canvasWidth, canvasHeight);
374
+ canvasService.viewport.setPadding(paddingPx);
375
+ canvasService.viewport.updatePhysical(viewWidthMm, viewHeightMm);
376
+ const layout = canvasService.viewport.layout;
377
+ if (!Number.isFinite(layout.scale) || !Number.isFinite(layout.offsetX) || !Number.isFinite(layout.offsetY) || layout.scale <= 0) {
378
+ return null;
742
379
  }
743
- return item;
744
- }
745
- function getPerimeterShape(options) {
746
- let mainShape = createBaseShape(options);
747
- const { features } = options;
748
- if (features && features.length > 0) {
749
- const edgeFeatures = features.filter(
750
- (f) => !f.renderBehavior || f.renderBehavior === "edge"
751
- );
752
- const adds = [];
753
- const subtracts = [];
754
- edgeFeatures.forEach((f) => {
755
- const pos = resolveFeaturePosition(f, options);
756
- const center = new import_paper.default.Point(pos.x, pos.y);
757
- const item = createFeatureItem(f, center);
758
- if (f.bridge && f.bridge.type === "vertical") {
759
- const itemBounds = item.bounds;
760
- const mainBounds = mainShape.bounds;
761
- const bridgeTop = mainBounds.top;
762
- const bridgeBottom = itemBounds.top;
763
- if (bridgeBottom > bridgeTop) {
764
- const overlap = 2;
765
- const rayPadding = 10;
766
- const eps = 0.1;
767
- const delta = getBridgeDelta(itemBounds, overlap);
768
- const toY = bridgeTop - rayPadding;
769
- const inset = Math.min(1, Math.max(0, itemBounds.width * 0.01));
770
- const xLeft = itemBounds.left + inset;
771
- const xRight = itemBounds.right - inset;
772
- const bridgeBasePath = resolveBridgeBasePath(mainShape, center);
773
- const canBridge = !!bridgeBasePath && xRight - xLeft > eps;
774
- if (canBridge && bridgeBasePath) {
775
- const leftHit = getExitHit({
776
- mainShape: bridgeBasePath,
777
- x: xLeft,
778
- bridgeBottom,
779
- toY,
780
- eps,
781
- delta,
782
- overlap,
783
- op: f.operation
784
- });
785
- const rightHit = getExitHit({
786
- mainShape: bridgeBasePath,
787
- x: xRight,
788
- bridgeBottom,
789
- toY,
790
- eps,
791
- delta,
792
- overlap,
793
- op: f.operation
794
- });
795
- if (leftHit && rightHit) {
796
- const pathLength = bridgeBasePath.length;
797
- const leftOffset = leftHit.location.offset;
798
- const rightOffset = rightHit.location.offset;
799
- const distanceA = wrappedDistance(pathLength, leftOffset, rightOffset);
800
- const distanceB = wrappedDistance(pathLength, rightOffset, leftOffset);
801
- const countFor = (d) => Math.max(8, Math.min(80, Math.ceil(d / 6)));
802
- const offsetsA = sampleWrappedOffsets(
803
- pathLength,
804
- leftOffset,
805
- rightOffset,
806
- countFor(distanceA)
807
- );
808
- const offsetsB = sampleWrappedOffsets(
809
- pathLength,
810
- rightOffset,
811
- leftOffset,
812
- countFor(distanceB)
813
- );
814
- const pointsA = offsetsA.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
815
- const pointsB = offsetsB.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
816
- if (pointsA.length >= 2 && pointsB.length >= 2) {
817
- let topBase = selectOuterChain({
818
- mainShape: bridgeBasePath,
819
- pointsA,
820
- pointsB,
821
- delta,
822
- overlap,
823
- op: f.operation
824
- });
825
- const dist2 = (a, b) => {
826
- const dx = a.x - b.x;
827
- const dy = a.y - b.y;
828
- return dx * dx + dy * dy;
829
- };
830
- if (dist2(topBase[0], leftHit.point) > dist2(topBase[0], rightHit.point)) {
831
- topBase = topBase.slice().reverse();
832
- }
833
- topBase = topBase.slice();
834
- topBase[0] = leftHit.point;
835
- topBase[topBase.length - 1] = rightHit.point;
836
- const capShiftY = f.operation === "subtract" ? -Math.max(overlap * 2, delta) : overlap;
837
- const topPoints = topBase.map(
838
- (p) => p.add(new import_paper.default.Point(0, capShiftY))
839
- );
840
- const bridgeBottomY = bridgeBottom + overlap * 2;
841
- const bridgePoly = new import_paper.default.Path({ insert: false });
842
- for (const p of topPoints) bridgePoly.add(p);
843
- bridgePoly.add(new import_paper.default.Point(xRight, bridgeBottomY));
844
- bridgePoly.add(new import_paper.default.Point(xLeft, bridgeBottomY));
845
- bridgePoly.closed = true;
846
- const unitedItem = item.unite(bridgePoly);
847
- item.remove();
848
- bridgePoly.remove();
849
- if (f.operation === "add") {
850
- adds.push(unitedItem);
851
- } else {
852
- subtracts.push(unitedItem);
853
- }
854
- return;
855
- }
856
- }
857
- }
858
- if (f.operation === "add") {
859
- adds.push(item);
860
- } else {
861
- subtracts.push(item);
862
- }
863
- } else {
864
- if (f.operation === "add") {
865
- adds.push(item);
866
- } else {
867
- subtracts.push(item);
868
- }
869
- }
870
- } else {
871
- if (f.operation === "add") {
872
- adds.push(item);
873
- } else {
874
- subtracts.push(item);
875
- }
876
- }
877
- });
878
- if (adds.length > 0) {
879
- for (const item of adds) {
880
- try {
881
- const temp = mainShape.unite(item);
882
- mainShape.remove();
883
- item.remove();
884
- mainShape = normalizePathItem(temp);
885
- } catch (e) {
886
- console.error("Geometry: Failed to unite feature", e);
887
- item.remove();
888
- }
889
- }
890
- }
891
- if (subtracts.length > 0) {
892
- for (const item of subtracts) {
893
- try {
894
- const temp = mainShape.subtract(item);
895
- mainShape.remove();
896
- item.remove();
897
- mainShape = normalizePathItem(temp);
898
- } catch (e) {
899
- console.error("Geometry: Failed to subtract feature", e);
900
- item.remove();
901
- }
902
- }
903
- }
904
- }
905
- return mainShape;
380
+ const centerX = layout.offsetX + layout.width / 2;
381
+ const centerY = layout.offsetY + layout.height / 2;
382
+ const trimWidthPx = size.actualWidthMm * layout.scale;
383
+ const trimHeightPx = size.actualHeightMm * layout.scale;
384
+ const cutWidthPx = cutWidthMm * layout.scale;
385
+ const cutHeightPx = cutHeightMm * layout.scale;
386
+ const trimRect = rectByCenter(centerX, centerY, trimWidthPx, trimHeightPx);
387
+ const cutRect = rectByCenter(centerX, centerY, cutWidthPx, cutHeightPx);
388
+ const bleedRect = rectByCenter(
389
+ centerX,
390
+ centerY,
391
+ Math.max(trimWidthPx, cutWidthPx),
392
+ Math.max(trimHeightPx, cutHeightPx)
393
+ );
394
+ return {
395
+ scale: layout.scale,
396
+ canvasWidth,
397
+ canvasHeight,
398
+ trimRect,
399
+ cutRect,
400
+ bleedRect,
401
+ trimWidthMm: size.actualWidthMm,
402
+ trimHeightMm: size.actualHeightMm,
403
+ cutWidthMm,
404
+ cutHeightMm,
405
+ cutMode: size.cutMode,
406
+ cutMarginMm: size.cutMarginMm
407
+ };
906
408
  }
907
- function applySurfaceFeatures(shape, features, options) {
908
- const surfaceFeatures = features.filter(
909
- (f) => f.renderBehavior === "surface"
409
+ function buildSceneGeometry(configService, layout) {
410
+ const radiusMm = parseLengthToMm(
411
+ configService.get("dieline.radius", 0),
412
+ "mm"
910
413
  );
911
- if (surfaceFeatures.length === 0) return shape;
912
- let result = shape;
913
- for (const f of surfaceFeatures) {
914
- const pos = resolveFeaturePosition(f, options);
915
- const center = new import_paper.default.Point(pos.x, pos.y);
916
- const item = createFeatureItem(f, center);
917
- try {
918
- if (f.operation === "add") {
919
- const temp = result.unite(item);
920
- result.remove();
921
- item.remove();
922
- result = normalizePathItem(temp);
923
- } else {
924
- const temp = result.subtract(item);
925
- result.remove();
926
- item.remove();
927
- result = normalizePathItem(temp);
928
- }
929
- } catch (e) {
930
- console.error("Geometry: Failed to apply surface feature", e);
931
- item.remove();
414
+ const offset = (layout.cutRect.width - layout.trimRect.width) / 2;
415
+ const sourceWidth = Number(configService.get("dieline.customSourceWidthPx", 0));
416
+ const sourceHeight = Number(
417
+ configService.get("dieline.customSourceHeightPx", 0)
418
+ );
419
+ const shapeStyle = normalizeShapeStyle(
420
+ configService.get("dieline.shapeStyle", DEFAULT_DIELINE_SHAPE_STYLE)
421
+ );
422
+ return {
423
+ shape: normalizeDielineShape(
424
+ configService.get("dieline.shape", DEFAULT_DIELINE_SHAPE)
425
+ ),
426
+ shapeStyle,
427
+ unit: "px",
428
+ x: layout.trimRect.centerX,
429
+ y: layout.trimRect.centerY,
430
+ width: layout.trimRect.width,
431
+ height: layout.trimRect.height,
432
+ radius: radiusMm * layout.scale,
433
+ offset,
434
+ scale: layout.scale,
435
+ pathData: configService.get("dieline.pathData"),
436
+ customSourceWidthPx: Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : void 0,
437
+ customSourceHeightPx: Number.isFinite(sourceHeight) && sourceHeight > 0 ? sourceHeight : void 0
438
+ };
439
+ }
440
+
441
+ // src/extensions/background.ts
442
+ var BACKGROUND_LAYER_ID = "background";
443
+ var BACKGROUND_CONFIG_KEY = "background.config";
444
+ var DEFAULT_WIDTH = 800;
445
+ var DEFAULT_HEIGHT = 600;
446
+ var DEFAULT_BACKGROUND_CONFIG = {
447
+ version: 1,
448
+ layers: [
449
+ {
450
+ id: "base-color",
451
+ kind: "color",
452
+ anchor: "viewport",
453
+ fit: "cover",
454
+ opacity: 1,
455
+ order: 0,
456
+ enabled: true,
457
+ exportable: false,
458
+ color: "#fff"
932
459
  }
460
+ ]
461
+ };
462
+ function clampOpacity(value, fallback) {
463
+ const numeric = Number(value);
464
+ if (!Number.isFinite(numeric)) {
465
+ return Math.max(0, Math.min(1, fallback));
933
466
  }
934
- return result;
467
+ return Math.max(0, Math.min(1, numeric));
935
468
  }
936
- function generateDielinePath(options) {
937
- const paperWidth = options.canvasWidth || options.width * 2 || 2e3;
938
- const paperHeight = options.canvasHeight || options.height * 2 || 2e3;
939
- ensurePaper(paperWidth, paperHeight);
940
- import_paper.default.project.activeLayer.removeChildren();
941
- const perimeter = getPerimeterShape(options);
942
- const finalShape = applySurfaceFeatures(perimeter, options.features, options);
943
- const pathData = finalShape.pathData;
944
- finalShape.remove();
945
- return pathData;
469
+ function normalizeLayerKind(value, fallback) {
470
+ if (value === "color" || value === "image") {
471
+ return value;
472
+ }
473
+ return fallback;
946
474
  }
947
- function generateMaskPath(options) {
948
- ensurePaper(options.canvasWidth, options.canvasHeight);
949
- import_paper.default.project.activeLayer.removeChildren();
950
- const { canvasWidth, canvasHeight } = options;
951
- const maskRect = new import_paper.default.Path.Rectangle({
952
- point: [0, 0],
953
- size: [canvasWidth, canvasHeight]
954
- });
955
- const perimeter = getPerimeterShape(options);
956
- const mainShape = applySurfaceFeatures(perimeter, options.features, options);
957
- const finalMask = maskRect.subtract(mainShape);
958
- maskRect.remove();
959
- mainShape.remove();
960
- const pathData = finalMask.pathData;
961
- finalMask.remove();
962
- return pathData;
475
+ function normalizeFitMode2(value, fallback) {
476
+ if (value === "contain" || value === "cover" || value === "stretch") {
477
+ return value;
478
+ }
479
+ return fallback;
963
480
  }
964
- function generateBleedZonePath(originalOptions, offsetOptions, offset) {
965
- const paperWidth = originalOptions.canvasWidth || originalOptions.width * 2 || 2e3;
966
- const paperHeight = originalOptions.canvasHeight || originalOptions.height * 2 || 2e3;
967
- ensurePaper(paperWidth, paperHeight);
968
- import_paper.default.project.activeLayer.removeChildren();
969
- const pOriginal = getPerimeterShape(originalOptions);
970
- const shapeOriginal = applySurfaceFeatures(
971
- pOriginal,
972
- originalOptions.features,
973
- originalOptions
974
- );
975
- const pOffset = getPerimeterShape(offsetOptions);
976
- const shapeOffset = applySurfaceFeatures(
977
- pOffset,
978
- offsetOptions.features,
979
- offsetOptions
980
- );
981
- let bleedZone;
982
- if (offset > 0) {
983
- bleedZone = shapeOffset.subtract(shapeOriginal);
984
- } else {
985
- bleedZone = shapeOriginal.subtract(shapeOffset);
986
- }
987
- const pathData = bleedZone.pathData;
988
- shapeOriginal.remove();
989
- shapeOffset.remove();
990
- bleedZone.remove();
991
- return pathData;
481
+ function normalizeAnchor(value, fallback) {
482
+ if (typeof value !== "string") return fallback;
483
+ const trimmed = value.trim();
484
+ return trimmed || fallback;
992
485
  }
993
- function getLowestPointOnDieline(options) {
994
- ensurePaper(options.width * 2, options.height * 2);
995
- import_paper.default.project.activeLayer.removeChildren();
996
- const shape = createBaseShape(options);
997
- const bounds = shape.bounds;
998
- const result = {
999
- x: bounds.center.x,
1000
- y: bounds.bottom
486
+ function normalizeOrder(value, fallback) {
487
+ const numeric = Number(value);
488
+ if (!Number.isFinite(numeric)) return fallback;
489
+ return numeric;
490
+ }
491
+ function normalizeLayer(raw, index, fallback) {
492
+ const fallbackLayer = fallback || {
493
+ id: `layer-${index + 1}`,
494
+ kind: "image",
495
+ anchor: "viewport",
496
+ fit: "contain",
497
+ opacity: 1,
498
+ order: index,
499
+ enabled: true,
500
+ exportable: false,
501
+ src: ""
502
+ };
503
+ if (!raw || typeof raw !== "object") {
504
+ return { ...fallbackLayer };
505
+ }
506
+ const input = raw;
507
+ const kind = normalizeLayerKind(input.kind, fallbackLayer.kind);
508
+ return {
509
+ id: typeof input.id === "string" && input.id.trim().length > 0 ? input.id.trim() : fallbackLayer.id,
510
+ kind,
511
+ anchor: normalizeAnchor(input.anchor, fallbackLayer.anchor),
512
+ fit: normalizeFitMode2(input.fit, fallbackLayer.fit),
513
+ opacity: clampOpacity(input.opacity, fallbackLayer.opacity),
514
+ order: normalizeOrder(input.order, fallbackLayer.order),
515
+ enabled: typeof input.enabled === "boolean" ? input.enabled : fallbackLayer.enabled,
516
+ exportable: typeof input.exportable === "boolean" ? input.exportable : fallbackLayer.exportable,
517
+ color: kind === "color" ? typeof input.color === "string" ? input.color : typeof fallbackLayer.color === "string" ? fallbackLayer.color : "#ffffff" : void 0,
518
+ src: kind === "image" ? typeof input.src === "string" ? input.src.trim() : typeof fallbackLayer.src === "string" ? fallbackLayer.src : "" : void 0
1001
519
  };
1002
- shape.remove();
1003
- return result;
1004
520
  }
1005
- function getNearestPointOnDieline(point, options) {
1006
- ensurePaper(options.width * 2, options.height * 2);
1007
- import_paper.default.project.activeLayer.removeChildren();
1008
- const shape = createBaseShape(options);
1009
- const p = new import_paper.default.Point(point.x, point.y);
1010
- const location = shape.getNearestLocation(p);
1011
- const result = {
1012
- x: location.point.x,
1013
- y: location.point.y,
1014
- normal: location.normal ? { x: location.normal.x, y: location.normal.y } : void 0
521
+ function normalizeConfig(raw) {
522
+ if (!raw || typeof raw !== "object") {
523
+ return cloneConfig(DEFAULT_BACKGROUND_CONFIG);
524
+ }
525
+ const input = raw;
526
+ const version = Number.isFinite(Number(input.version)) ? Number(input.version) : DEFAULT_BACKGROUND_CONFIG.version;
527
+ const baseLayers = Array.isArray(input.layers) ? input.layers.map((layer, index) => normalizeLayer(layer, index)) : cloneConfig(DEFAULT_BACKGROUND_CONFIG).layers;
528
+ const uniqueLayers = [];
529
+ const seen = /* @__PURE__ */ new Set();
530
+ baseLayers.forEach((layer, index) => {
531
+ let nextId = layer.id || `layer-${index + 1}`;
532
+ let serial = 1;
533
+ while (seen.has(nextId)) {
534
+ serial += 1;
535
+ nextId = `${layer.id || `layer-${index + 1}`}-${serial}`;
536
+ }
537
+ seen.add(nextId);
538
+ uniqueLayers.push({ ...layer, id: nextId });
539
+ });
540
+ return {
541
+ version,
542
+ layers: uniqueLayers
1015
543
  };
1016
- shape.remove();
1017
- return result;
1018
544
  }
1019
- function getPathBounds(pathData) {
1020
- const path = new import_paper.default.Path();
1021
- path.pathData = pathData;
1022
- const bounds = path.bounds;
1023
- path.remove();
545
+ function cloneConfig(config) {
1024
546
  return {
1025
- x: bounds.x,
1026
- y: bounds.y,
1027
- width: bounds.width,
1028
- height: bounds.height
547
+ version: config.version,
548
+ layers: (config.layers || []).map((layer) => ({ ...layer }))
1029
549
  };
1030
550
  }
1031
-
1032
- // src/coordinate.ts
1033
- var Coordinate = class {
1034
- /**
1035
- * Calculate layout to fit content within container while preserving aspect ratio.
1036
- */
1037
- static calculateLayout(container, content, padding = 0) {
1038
- const availableWidth = Math.max(0, container.width - padding * 2);
1039
- const availableHeight = Math.max(0, container.height - padding * 2);
1040
- if (content.width === 0 || content.height === 0) {
1041
- return { scale: 1, offsetX: 0, offsetY: 0, width: 0, height: 0 };
551
+ function mergeConfig(base, patch) {
552
+ const merged = {
553
+ version: patch.version === void 0 ? base.version : Number.isFinite(Number(patch.version)) ? Number(patch.version) : base.version,
554
+ layers: Array.isArray(patch.layers) ? patch.layers.map((layer, index) => normalizeLayer(layer, index)) : base.layers.map((layer) => ({ ...layer }))
555
+ };
556
+ return normalizeConfig(merged);
557
+ }
558
+ function configSignature(config) {
559
+ return JSON.stringify(config);
560
+ }
561
+ var BackgroundTool = class {
562
+ constructor(options) {
563
+ this.id = "pooder.kit.background";
564
+ this.metadata = {
565
+ name: "BackgroundTool"
566
+ };
567
+ this.config = cloneConfig(DEFAULT_BACKGROUND_CONFIG);
568
+ this.specs = [];
569
+ this.renderSeq = 0;
570
+ this.latestSceneLayout = null;
571
+ this.sourceSizeBySrc = /* @__PURE__ */ new Map();
572
+ this.pendingSizeBySrc = /* @__PURE__ */ new Map();
573
+ this.onCanvasResized = () => {
574
+ this.latestSceneLayout = null;
575
+ this.updateBackground();
576
+ };
577
+ this.onSceneLayoutChanged = (layout) => {
578
+ this.latestSceneLayout = layout;
579
+ this.updateBackground();
580
+ };
581
+ if (options && typeof options === "object") {
582
+ this.config = mergeConfig(this.config, options);
1042
583
  }
1043
- const scaleX = availableWidth / content.width;
1044
- const scaleY = availableHeight / content.height;
1045
- const scale = Math.min(scaleX, scaleY);
1046
- const width = content.width * scale;
1047
- const height = content.height * scale;
1048
- const offsetX = (container.width - width) / 2;
1049
- const offsetY = (container.height - height) / 2;
1050
- return { scale, offsetX, offsetY, width, height };
1051
584
  }
1052
- /**
1053
- * Convert an absolute value to a normalized value (0-1).
1054
- * @param value Absolute value (e.g., pixels)
1055
- * @param total Total dimension size (e.g., canvas width)
1056
- */
1057
- static toNormalized(value, total) {
1058
- return total === 0 ? 0 : value / total;
585
+ activate(context) {
586
+ var _a, _b;
587
+ this.canvasService = context.services.get("CanvasService");
588
+ if (!this.canvasService) {
589
+ console.warn("CanvasService not found for BackgroundTool");
590
+ return;
591
+ }
592
+ this.configService = context.services.get(
593
+ "ConfigurationService"
594
+ );
595
+ if (this.configService) {
596
+ this.config = normalizeConfig(
597
+ this.configService.get(
598
+ BACKGROUND_CONFIG_KEY,
599
+ DEFAULT_BACKGROUND_CONFIG
600
+ )
601
+ );
602
+ (_a = this.configChangeDisposable) == null ? void 0 : _a.dispose();
603
+ this.configChangeDisposable = this.configService.onAnyChange(
604
+ (e) => {
605
+ if (e.key === BACKGROUND_CONFIG_KEY) {
606
+ this.config = normalizeConfig(e.value);
607
+ this.updateBackground();
608
+ return;
609
+ }
610
+ if (e.key.startsWith("size.")) {
611
+ this.latestSceneLayout = null;
612
+ this.updateBackground();
613
+ }
614
+ }
615
+ );
616
+ }
617
+ (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
618
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
619
+ this.id,
620
+ () => ({
621
+ passes: [
622
+ {
623
+ id: BACKGROUND_LAYER_ID,
624
+ stack: 0,
625
+ order: 0,
626
+ objects: this.specs
627
+ }
628
+ ]
629
+ }),
630
+ { priority: 0 }
631
+ );
632
+ context.eventBus.on("canvas:resized", this.onCanvasResized);
633
+ context.eventBus.on("scene:layout:change", this.onSceneLayoutChanged);
634
+ this.updateBackground();
1059
635
  }
1060
- /**
1061
- * Convert a normalized value (0-1) to an absolute value.
1062
- * @param normalized Normalized value (0-1)
1063
- * @param total Total dimension size (e.g., canvas width)
1064
- */
1065
- static toAbsolute(normalized, total) {
1066
- return normalized * total;
636
+ deactivate(context) {
637
+ var _a, _b;
638
+ context.eventBus.off("canvas:resized", this.onCanvasResized);
639
+ context.eventBus.off("scene:layout:change", this.onSceneLayoutChanged);
640
+ this.renderSeq += 1;
641
+ this.specs = [];
642
+ this.latestSceneLayout = null;
643
+ (_a = this.configChangeDisposable) == null ? void 0 : _a.dispose();
644
+ this.configChangeDisposable = void 0;
645
+ (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
646
+ this.renderProducerDisposable = void 0;
647
+ if (!this.canvasService) return;
648
+ void this.canvasService.flushRenderFromProducers();
649
+ this.canvasService.requestRenderAll();
650
+ this.canvasService = void 0;
651
+ this.configService = void 0;
1067
652
  }
1068
- /**
1069
- * Normalize a point's coordinates.
1070
- */
1071
- static normalizePoint(point, size) {
653
+ contribute() {
1072
654
  return {
1073
- x: this.toNormalized(point.x, size.width),
1074
- y: this.toNormalized(point.y, size.height)
655
+ [import_core.ContributionPointIds.CONFIGURATIONS]: [
656
+ {
657
+ id: BACKGROUND_CONFIG_KEY,
658
+ type: "json",
659
+ label: "Background Config",
660
+ default: cloneConfig(DEFAULT_BACKGROUND_CONFIG)
661
+ }
662
+ ],
663
+ [import_core.ContributionPointIds.COMMANDS]: [
664
+ {
665
+ command: "background.getConfig",
666
+ title: "Get Background Config",
667
+ handler: () => cloneConfig(this.config)
668
+ },
669
+ {
670
+ command: "background.resetConfig",
671
+ title: "Reset Background Config",
672
+ handler: () => {
673
+ this.commitConfig(cloneConfig(DEFAULT_BACKGROUND_CONFIG));
674
+ return true;
675
+ }
676
+ },
677
+ {
678
+ command: "background.replaceConfig",
679
+ title: "Replace Background Config",
680
+ handler: (config) => {
681
+ this.commitConfig(normalizeConfig(config));
682
+ return true;
683
+ }
684
+ },
685
+ {
686
+ command: "background.patchConfig",
687
+ title: "Patch Background Config",
688
+ handler: (patch) => {
689
+ this.commitConfig(mergeConfig(this.config, patch || {}));
690
+ return true;
691
+ }
692
+ },
693
+ {
694
+ command: "background.upsertLayer",
695
+ title: "Upsert Background Layer",
696
+ handler: (layer) => {
697
+ const normalized = normalizeLayer(layer, 0);
698
+ const existingIndex = this.config.layers.findIndex(
699
+ (item) => item.id === normalized.id
700
+ );
701
+ const nextLayers = [...this.config.layers];
702
+ if (existingIndex >= 0) {
703
+ nextLayers[existingIndex] = normalizeLayer(
704
+ { ...nextLayers[existingIndex], ...layer },
705
+ existingIndex,
706
+ nextLayers[existingIndex]
707
+ );
708
+ } else {
709
+ nextLayers.push(
710
+ normalizeLayer(
711
+ {
712
+ ...normalized,
713
+ order: Number.isFinite(Number(layer.order)) ? Number(layer.order) : nextLayers.length
714
+ },
715
+ nextLayers.length
716
+ )
717
+ );
718
+ }
719
+ this.commitConfig(
720
+ normalizeConfig({
721
+ ...this.config,
722
+ layers: nextLayers
723
+ })
724
+ );
725
+ return true;
726
+ }
727
+ },
728
+ {
729
+ command: "background.removeLayer",
730
+ title: "Remove Background Layer",
731
+ handler: (id) => {
732
+ const nextLayers = this.config.layers.filter(
733
+ (layer) => layer.id !== id
734
+ );
735
+ this.commitConfig(
736
+ normalizeConfig({
737
+ ...this.config,
738
+ layers: nextLayers
739
+ })
740
+ );
741
+ return true;
742
+ }
743
+ }
744
+ ]
1075
745
  };
1076
746
  }
1077
- /**
1078
- * Denormalize a point's coordinates to absolute pixels.
1079
- */
1080
- static denormalizePoint(point, size) {
747
+ commitConfig(next) {
748
+ const normalized = normalizeConfig(next);
749
+ if (configSignature(normalized) === configSignature(this.config)) {
750
+ return;
751
+ }
752
+ if (this.configService) {
753
+ this.configService.update(BACKGROUND_CONFIG_KEY, cloneConfig(normalized));
754
+ return;
755
+ }
756
+ this.config = normalized;
757
+ this.updateBackground();
758
+ }
759
+ getViewportRect() {
760
+ var _a, _b;
761
+ const width = Number(((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 0);
762
+ const height = Number(((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 0);
1081
763
  return {
1082
- x: this.toAbsolute(point.x, size.width),
1083
- y: this.toAbsolute(point.y, size.height)
764
+ left: 0,
765
+ top: 0,
766
+ width: width > 0 ? width : DEFAULT_WIDTH,
767
+ height: height > 0 ? height : DEFAULT_HEIGHT
1084
768
  };
1085
769
  }
1086
- static convertUnit(value, from, to) {
1087
- if (from === to) return value;
1088
- const toMM = {
1089
- px: 0.264583,
1090
- // 1px = 0.264583mm (96 DPI)
1091
- mm: 1,
1092
- cm: 10,
1093
- in: 25.4
770
+ resolveSceneLayout() {
771
+ if (this.latestSceneLayout) return this.latestSceneLayout;
772
+ if (!this.canvasService || !this.configService) return null;
773
+ const layout = computeSceneLayout(
774
+ this.canvasService,
775
+ readSizeState(this.configService)
776
+ );
777
+ this.latestSceneLayout = layout;
778
+ return layout;
779
+ }
780
+ resolveFocusRect() {
781
+ const layout = this.resolveSceneLayout();
782
+ if (!layout) return null;
783
+ return {
784
+ left: layout.trimRect.left,
785
+ top: layout.trimRect.top,
786
+ width: layout.trimRect.width,
787
+ height: layout.trimRect.height
1094
788
  };
1095
- const mmValue = value * (from === "px" ? toMM.px : toMM[from] || 1);
1096
- if (to === "px") {
1097
- return mmValue / toMM.px;
1098
- }
1099
- return mmValue / (toMM[to] || 1);
1100
789
  }
1101
- };
1102
-
1103
- // src/units.ts
1104
- function parseLengthToMm(input, defaultUnit) {
1105
- var _a, _b;
1106
- if (typeof input === "number") {
1107
- if (!Number.isFinite(input)) return 0;
1108
- return Coordinate.convertUnit(input, defaultUnit, "mm");
790
+ resolveAnchorRect(anchor) {
791
+ if (anchor === "focus") {
792
+ return this.resolveFocusRect() || this.getViewportRect();
793
+ }
794
+ if (anchor !== "viewport") {
795
+ return this.getViewportRect();
796
+ }
797
+ return this.getViewportRect();
798
+ }
799
+ resolveImagePlacement(target, sourceSize, fit) {
800
+ const targetWidth = Math.max(1, Number(target.width || 0));
801
+ const targetHeight = Math.max(1, Number(target.height || 0));
802
+ const sourceWidth = Math.max(1, Number(sourceSize.width || 0));
803
+ const sourceHeight = Math.max(1, Number(sourceSize.height || 0));
804
+ if (fit === "stretch") {
805
+ return {
806
+ left: target.left,
807
+ top: target.top,
808
+ scaleX: targetWidth / sourceWidth,
809
+ scaleY: targetHeight / sourceHeight
810
+ };
811
+ }
812
+ const scale = fit === "contain" ? Math.min(targetWidth / sourceWidth, targetHeight / sourceHeight) : Math.max(targetWidth / sourceWidth, targetHeight / sourceHeight);
813
+ const renderWidth = sourceWidth * scale;
814
+ const renderHeight = sourceHeight * scale;
815
+ return {
816
+ left: target.left + (targetWidth - renderWidth) / 2,
817
+ top: target.top + (targetHeight - renderHeight) / 2,
818
+ scaleX: scale,
819
+ scaleY: scale
820
+ };
821
+ }
822
+ buildColorLayerSpec(layer) {
823
+ const rect = this.resolveAnchorRect(layer.anchor);
824
+ return {
825
+ id: `background.layer.${layer.id}.color`,
826
+ type: "rect",
827
+ space: "screen",
828
+ data: {
829
+ id: `background.layer.${layer.id}.color`,
830
+ layerId: BACKGROUND_LAYER_ID,
831
+ type: "background-layer",
832
+ layerRef: layer.id,
833
+ layerKind: layer.kind
834
+ },
835
+ props: {
836
+ left: rect.left,
837
+ top: rect.top,
838
+ width: rect.width,
839
+ height: rect.height,
840
+ originX: "left",
841
+ originY: "top",
842
+ fill: layer.color || "transparent",
843
+ opacity: layer.opacity,
844
+ selectable: false,
845
+ evented: false,
846
+ excludeFromExport: !layer.exportable
847
+ }
848
+ };
849
+ }
850
+ buildImageLayerSpec(layer) {
851
+ const src = String(layer.src || "").trim();
852
+ if (!src) return [];
853
+ const sourceSize = this.sourceSizeBySrc.get(src);
854
+ if (!sourceSize) return [];
855
+ const rect = this.resolveAnchorRect(layer.anchor);
856
+ const placement = this.resolveImagePlacement(rect, sourceSize, layer.fit);
857
+ return [
858
+ {
859
+ id: `background.layer.${layer.id}.image`,
860
+ type: "image",
861
+ src,
862
+ space: "screen",
863
+ data: {
864
+ id: `background.layer.${layer.id}.image`,
865
+ layerId: BACKGROUND_LAYER_ID,
866
+ type: "background-layer",
867
+ layerRef: layer.id,
868
+ layerKind: layer.kind
869
+ },
870
+ props: {
871
+ left: placement.left,
872
+ top: placement.top,
873
+ originX: "left",
874
+ originY: "top",
875
+ scaleX: placement.scaleX,
876
+ scaleY: placement.scaleY,
877
+ opacity: layer.opacity,
878
+ selectable: false,
879
+ evented: false,
880
+ excludeFromExport: !layer.exportable
881
+ }
882
+ }
883
+ ];
884
+ }
885
+ buildBackgroundSpecs(config) {
886
+ const activeLayers = (config.layers || []).filter((layer) => layer.enabled).map((layer, index) => ({ layer, index })).sort((a, b) => {
887
+ if (a.layer.order !== b.layer.order) {
888
+ return a.layer.order - b.layer.order;
889
+ }
890
+ return a.index - b.index;
891
+ });
892
+ const specs = [];
893
+ activeLayers.forEach(({ layer }) => {
894
+ if (layer.kind === "color") {
895
+ specs.push(this.buildColorLayerSpec(layer));
896
+ return;
897
+ }
898
+ specs.push(...this.buildImageLayerSpec(layer));
899
+ });
900
+ return specs;
901
+ }
902
+ collectActiveImageUrls(config) {
903
+ const urls = /* @__PURE__ */ new Set();
904
+ (config.layers || []).forEach((layer) => {
905
+ if (!layer.enabled || layer.kind !== "image") return;
906
+ const src = String(layer.src || "").trim();
907
+ if (!src) return;
908
+ urls.add(src);
909
+ });
910
+ return Array.from(urls);
911
+ }
912
+ async ensureImageSize(src) {
913
+ if (!src) return null;
914
+ const cached = this.sourceSizeBySrc.get(src);
915
+ if (cached) return cached;
916
+ const pending = this.pendingSizeBySrc.get(src);
917
+ if (pending) {
918
+ return pending;
919
+ }
920
+ const task = this.loadImageSize(src);
921
+ this.pendingSizeBySrc.set(src, task);
922
+ try {
923
+ return await task;
924
+ } finally {
925
+ if (this.pendingSizeBySrc.get(src) === task) {
926
+ this.pendingSizeBySrc.delete(src);
927
+ }
928
+ }
929
+ }
930
+ async loadImageSize(src) {
931
+ try {
932
+ const image = await import_fabric.FabricImage.fromURL(src, {
933
+ crossOrigin: "anonymous"
934
+ });
935
+ const width = Number((image == null ? void 0 : image.width) || 0);
936
+ const height = Number((image == null ? void 0 : image.height) || 0);
937
+ if (width > 0 && height > 0) {
938
+ const size = { width, height };
939
+ this.sourceSizeBySrc.set(src, size);
940
+ return size;
941
+ }
942
+ } catch (error) {
943
+ console.error("[BackgroundTool] Failed to load image", src, error);
944
+ }
945
+ return null;
946
+ }
947
+ updateBackground() {
948
+ void this.updateBackgroundAsync();
949
+ }
950
+ async updateBackgroundAsync() {
951
+ if (!this.canvasService) return;
952
+ const seq = ++this.renderSeq;
953
+ const currentConfig = cloneConfig(this.config);
954
+ const activeUrls = this.collectActiveImageUrls(currentConfig);
955
+ if (activeUrls.length > 0) {
956
+ await Promise.all(activeUrls.map((url) => this.ensureImageSize(url)));
957
+ if (seq !== this.renderSeq) return;
958
+ }
959
+ this.specs = this.buildBackgroundSpecs(currentConfig);
960
+ await this.canvasService.flushRenderFromProducers();
961
+ if (seq !== this.renderSeq) return;
962
+ this.canvasService.requestRenderAll();
1109
963
  }
1110
- const raw = input.trim();
1111
- if (!raw) return 0;
1112
- const match = raw.match(/^([+-]?\d+(?:\.\d+)?)\s*(px|mm|cm|in)?$/i);
1113
- if (!match) return 0;
1114
- const value = Number(match[1]);
1115
- if (!Number.isFinite(value)) return 0;
1116
- const unit = (_b = (_a = match[2]) == null ? void 0 : _a.toLowerCase()) != null ? _b : defaultUnit;
1117
- return Coordinate.convertUnit(value, unit, "mm");
1118
- }
1119
-
1120
- // src/extensions/sceneLayoutModel.ts
1121
- var DEFAULT_SIZE_STATE = {
1122
- unit: "mm",
1123
- actualWidthMm: 500,
1124
- actualHeightMm: 500,
1125
- constraintMode: "free",
1126
- aspectRatio: 1,
1127
- cutMode: "trim",
1128
- cutMarginMm: 0,
1129
- viewPadding: 140,
1130
- minMm: 10,
1131
- maxMm: 2e3,
1132
- stepMm: 0.1
1133
964
  };
1134
- function clamp(value, min, max) {
1135
- return Math.max(min, Math.min(max, value));
965
+
966
+ // src/extensions/image.ts
967
+ var import_core2 = require("@pooder/core");
968
+ var import_fabric2 = require("fabric");
969
+
970
+ // src/extensions/geometry.ts
971
+ var import_paper = __toESM(require("paper"));
972
+
973
+ // src/extensions/bridgeSelection.ts
974
+ function pickExitIndex(hits) {
975
+ for (let i = 0; i < hits.length; i++) {
976
+ const h = hits[i];
977
+ if (h.insideBelow && !h.insideAbove) return i;
978
+ }
979
+ return -1;
1136
980
  }
1137
- function roundToStep(value, step) {
1138
- if (!Number.isFinite(step) || step <= 0) return value;
1139
- return Math.round(value / step) * step;
981
+ function scoreOutsideAbove(samples) {
982
+ let score = 0;
983
+ for (const s of samples) {
984
+ if (s.outsideAbove) score++;
985
+ }
986
+ return score;
1140
987
  }
1141
- function sanitizeMmValue(valueMm, limits) {
1142
- if (!Number.isFinite(valueMm)) return limits.minMm;
1143
- const rounded = roundToStep(valueMm, limits.stepMm);
1144
- return clamp(rounded, limits.minMm, limits.maxMm);
988
+
989
+ // src/extensions/wrappedOffsets.ts
990
+ function wrappedDistance(total, start, end) {
991
+ if (!Number.isFinite(total) || total <= 0) return 0;
992
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return 0;
993
+ const s = (start % total + total) % total;
994
+ const e = (end % total + total) % total;
995
+ return e >= s ? e - s : total - s + e;
1145
996
  }
1146
- function normalizeUnit(value) {
1147
- if (value === "cm" || value === "in") return value;
1148
- return "mm";
997
+ function sampleWrappedOffsets(total, start, end, count) {
998
+ if (!Number.isFinite(total) || total <= 0) return [];
999
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return [];
1000
+ const n = Math.max(0, Math.floor(count));
1001
+ if (n <= 0) return [];
1002
+ const dist = wrappedDistance(total, start, end);
1003
+ if (n === 1) return [(start % total + total) % total];
1004
+ const step = dist / (n - 1);
1005
+ const offsets = [];
1006
+ for (let i = 0; i < n; i++) {
1007
+ const raw = start + step * i;
1008
+ const wrapped = (raw % total + total) % total;
1009
+ offsets.push(wrapped);
1010
+ }
1011
+ return offsets;
1149
1012
  }
1150
- function normalizeConstraintMode(value) {
1151
- if (value === "lockAspect" || value === "equal") return value;
1152
- return "free";
1013
+
1014
+ // src/extensions/geometry.ts
1015
+ function resolveFeaturePosition(feature, geometry) {
1016
+ const { x, y, width, height } = geometry;
1017
+ const left = x - width / 2;
1018
+ const top = y - height / 2;
1019
+ return {
1020
+ x: left + feature.x * width,
1021
+ y: top + feature.y * height
1022
+ };
1153
1023
  }
1154
- function normalizeCutMode(value) {
1155
- if (value === "outset" || value === "inset") return value;
1156
- return "trim";
1024
+ function ensurePaper(width, height) {
1025
+ if (!import_paper.default.project) {
1026
+ import_paper.default.setup(new import_paper.default.Size(width, height));
1027
+ } else {
1028
+ import_paper.default.view.viewSize = new import_paper.default.Size(width, height);
1029
+ }
1157
1030
  }
1158
- function toMm(value, fromUnit) {
1159
- return Coordinate.convertUnit(value, fromUnit, "mm");
1031
+ var isBridgeDebugEnabled = () => Boolean(globalThis.__POODER_BRIDGE_DEBUG__);
1032
+ function normalizePathItem(shape) {
1033
+ let result = shape;
1034
+ if (typeof result.resolveCrossings === "function") result = result.resolveCrossings();
1035
+ if (typeof result.reduce === "function") result = result.reduce({});
1036
+ if (typeof result.reorient === "function") result = result.reorient(true, true);
1037
+ if (typeof result.reduce === "function") result = result.reduce({});
1038
+ return result;
1160
1039
  }
1161
- function fromMm(valueMm, toUnit) {
1162
- return Coordinate.convertUnit(valueMm, "mm", toUnit);
1040
+ function getBridgeDelta(itemBounds, overlap) {
1041
+ return Math.max(overlap, Math.min(5, Math.max(1, itemBounds.height * 0.02)));
1163
1042
  }
1164
- function resolvePaddingPx(raw, containerWidth, containerHeight) {
1165
- if (typeof raw === "number") return Math.max(0, raw);
1166
- if (typeof raw === "string") {
1167
- if (raw.endsWith("%")) {
1168
- const percent = parseFloat(raw) / 100;
1169
- if (!Number.isFinite(percent)) return 0;
1170
- return Math.max(0, Math.min(containerWidth, containerHeight) * percent);
1171
- }
1172
- const fixed = parseFloat(raw);
1173
- return Number.isFinite(fixed) ? Math.max(0, fixed) : 0;
1043
+ function getExitHit(args) {
1044
+ const { mainShape, x, bridgeBottom, toY, eps, delta, overlap, op } = args;
1045
+ const ray = new import_paper.default.Path.Line({
1046
+ from: [x, bridgeBottom],
1047
+ to: [x, toY],
1048
+ insert: false
1049
+ });
1050
+ const intersections = mainShape.getIntersections(ray) || [];
1051
+ ray.remove();
1052
+ const validHits = intersections.filter((i) => i.point.y < bridgeBottom - eps);
1053
+ if (validHits.length === 0) return null;
1054
+ validHits.sort((a, b) => b.point.y - a.point.y);
1055
+ const flags = validHits.map((h) => {
1056
+ const above = h.point.add(new import_paper.default.Point(0, -delta));
1057
+ const below = h.point.add(new import_paper.default.Point(0, delta));
1058
+ return {
1059
+ insideAbove: mainShape.contains(above),
1060
+ insideBelow: mainShape.contains(below)
1061
+ };
1062
+ });
1063
+ const idx = pickExitIndex(flags);
1064
+ if (idx < 0) return null;
1065
+ if (isBridgeDebugEnabled()) {
1066
+ console.debug("Geometry: Bridge ray", {
1067
+ x,
1068
+ validHits: validHits.length,
1069
+ idx,
1070
+ delta,
1071
+ overlap,
1072
+ op
1073
+ });
1174
1074
  }
1175
- return 0;
1075
+ const hit = validHits[idx];
1076
+ return { point: hit.point, location: hit };
1176
1077
  }
1177
- function readSizeState(configService) {
1178
- const unit = normalizeUnit(
1179
- configService.get("size.unit", DEFAULT_SIZE_STATE.unit)
1180
- );
1181
- const minMm = Math.max(
1182
- 0.1,
1183
- Number(configService.get("size.minMm", DEFAULT_SIZE_STATE.minMm))
1078
+ function selectOuterChain(args) {
1079
+ const { mainShape, pointsA, pointsB, delta, overlap, op } = args;
1080
+ const scoreA = scoreOutsideAbove(
1081
+ pointsA.map((p) => ({
1082
+ outsideAbove: !mainShape.contains(p.add(new import_paper.default.Point(0, -delta)))
1083
+ }))
1184
1084
  );
1185
- const maxMm = Math.max(
1186
- minMm,
1187
- Number(configService.get("size.maxMm", DEFAULT_SIZE_STATE.maxMm))
1085
+ const scoreB = scoreOutsideAbove(
1086
+ pointsB.map((p) => ({
1087
+ outsideAbove: !mainShape.contains(p.add(new import_paper.default.Point(0, -delta)))
1088
+ }))
1188
1089
  );
1189
- const stepMm = Math.max(
1190
- 1e-3,
1191
- Number(configService.get("size.stepMm", DEFAULT_SIZE_STATE.stepMm))
1090
+ const ratioA = scoreA / pointsA.length;
1091
+ const ratioB = scoreB / pointsB.length;
1092
+ if (isBridgeDebugEnabled()) {
1093
+ console.debug("Geometry: Bridge chain", {
1094
+ scoreA,
1095
+ scoreB,
1096
+ lenA: pointsA.length,
1097
+ lenB: pointsB.length,
1098
+ ratioA,
1099
+ ratioB,
1100
+ delta,
1101
+ overlap,
1102
+ op
1103
+ });
1104
+ }
1105
+ const ratioEps = 1e-6;
1106
+ if (Math.abs(ratioA - ratioB) > ratioEps) {
1107
+ return ratioA > ratioB ? pointsA : pointsB;
1108
+ }
1109
+ if (scoreA !== scoreB) return scoreA > scoreB ? pointsA : pointsB;
1110
+ return pointsA.length <= pointsB.length ? pointsA : pointsB;
1111
+ }
1112
+ function fitPathItemToRect(item, rect, fitMode) {
1113
+ const { left, top, width, height } = rect;
1114
+ const bounds = item.bounds;
1115
+ if (width <= 0 || height <= 0 || !Number.isFinite(bounds.width) || !Number.isFinite(bounds.height) || bounds.width <= 0 || bounds.height <= 0) {
1116
+ item.position = new import_paper.default.Point(left + width / 2, top + height / 2);
1117
+ return item;
1118
+ }
1119
+ item.translate(new import_paper.default.Point(-bounds.left, -bounds.top));
1120
+ if (fitMode === "stretch") {
1121
+ item.scale(width / bounds.width, height / bounds.height, new import_paper.default.Point(0, 0));
1122
+ item.translate(new import_paper.default.Point(left, top));
1123
+ return item;
1124
+ }
1125
+ const uniformScale = Math.min(width / bounds.width, height / bounds.height);
1126
+ item.scale(uniformScale, uniformScale, new import_paper.default.Point(0, 0));
1127
+ const scaledWidth = bounds.width * uniformScale;
1128
+ const scaledHeight = bounds.height * uniformScale;
1129
+ item.translate(
1130
+ new import_paper.default.Point(
1131
+ left + (width - scaledWidth) / 2,
1132
+ top + (height - scaledHeight) / 2
1133
+ )
1192
1134
  );
1193
- const actualWidthMm = sanitizeMmValue(
1194
- parseLengthToMm(
1195
- configService.get("size.actualWidthMm", DEFAULT_SIZE_STATE.actualWidthMm),
1196
- "mm"
1197
- ),
1198
- { minMm, maxMm, stepMm }
1135
+ return item;
1136
+ }
1137
+ function createNormalizedHeartPath(params) {
1138
+ const { lobeSpread, notchDepth, tipSharpness } = params;
1139
+ const halfSpread = 0.22 + lobeSpread * 0.18;
1140
+ const notchY = 0.06 + notchDepth * 0.2;
1141
+ const shoulderY = 0.24 + notchDepth * 0.2;
1142
+ const topLift = 0.12 + (1 - notchDepth) * 0.06;
1143
+ const topY = notchY - topLift;
1144
+ const sideCtrlY = shoulderY - (0.18 - notchDepth * 0.08);
1145
+ const lowerCtrlY = 0.58 + (1 - tipSharpness) * 0.16;
1146
+ const tipCtrlX = 0.34 - tipSharpness * 0.2;
1147
+ const notchCtrlX = 0.06 + lobeSpread * 0.06;
1148
+ const lobeCtrlX = 0.1 + lobeSpread * 0.08;
1149
+ const notchCtrlY = notchY - topLift * 0.45;
1150
+ const xPeakL = 0.5 - halfSpread;
1151
+ const xPeakR = 0.5 + halfSpread;
1152
+ const heartPath = new import_paper.default.Path({ insert: false });
1153
+ heartPath.moveTo(new import_paper.default.Point(0.5, notchY));
1154
+ heartPath.cubicCurveTo(
1155
+ new import_paper.default.Point(0.5 - notchCtrlX, notchCtrlY),
1156
+ new import_paper.default.Point(xPeakL + lobeCtrlX, topY),
1157
+ new import_paper.default.Point(xPeakL, topY)
1199
1158
  );
1200
- const actualHeightMm = sanitizeMmValue(
1201
- parseLengthToMm(
1202
- configService.get(
1203
- "size.actualHeightMm",
1204
- DEFAULT_SIZE_STATE.actualHeightMm
1205
- ),
1206
- "mm"
1207
- ),
1208
- { minMm, maxMm, stepMm }
1159
+ heartPath.cubicCurveTo(
1160
+ new import_paper.default.Point(xPeakL - lobeCtrlX, topY),
1161
+ new import_paper.default.Point(0, sideCtrlY),
1162
+ new import_paper.default.Point(0, shoulderY)
1209
1163
  );
1210
- const aspectRaw = Number(
1211
- configService.get("size.aspectRatio", DEFAULT_SIZE_STATE.aspectRatio)
1164
+ heartPath.cubicCurveTo(
1165
+ new import_paper.default.Point(0, lowerCtrlY),
1166
+ new import_paper.default.Point(tipCtrlX, 1),
1167
+ new import_paper.default.Point(0.5, 1)
1212
1168
  );
1213
- const aspectRatio = Number.isFinite(aspectRaw) && aspectRaw > 0 ? aspectRaw : actualWidthMm / Math.max(1e-3, actualHeightMm);
1214
- const cutMarginMm = Math.max(
1215
- 0,
1216
- parseLengthToMm(
1217
- configService.get("size.cutMarginMm", DEFAULT_SIZE_STATE.cutMarginMm),
1218
- "mm"
1219
- )
1169
+ heartPath.cubicCurveTo(
1170
+ new import_paper.default.Point(1 - tipCtrlX, 1),
1171
+ new import_paper.default.Point(1, lowerCtrlY),
1172
+ new import_paper.default.Point(1, shoulderY)
1220
1173
  );
1221
- const viewPadding = configService.get(
1222
- "size.viewPadding",
1223
- DEFAULT_SIZE_STATE.viewPadding
1174
+ heartPath.cubicCurveTo(
1175
+ new import_paper.default.Point(1, sideCtrlY),
1176
+ new import_paper.default.Point(xPeakR + lobeCtrlX, topY),
1177
+ new import_paper.default.Point(xPeakR, topY)
1224
1178
  );
1225
- return {
1226
- unit,
1227
- actualWidthMm,
1228
- actualHeightMm,
1229
- constraintMode: normalizeConstraintMode(
1230
- configService.get(
1231
- "size.constraintMode",
1232
- DEFAULT_SIZE_STATE.constraintMode
1233
- )
1234
- ),
1235
- aspectRatio,
1236
- cutMode: normalizeCutMode(
1237
- configService.get("size.cutMode", DEFAULT_SIZE_STATE.cutMode)
1238
- ),
1239
- cutMarginMm,
1240
- viewPadding,
1241
- minMm,
1242
- maxMm,
1243
- stepMm
1244
- };
1179
+ heartPath.cubicCurveTo(
1180
+ new import_paper.default.Point(xPeakR - lobeCtrlX, topY),
1181
+ new import_paper.default.Point(0.5 + notchCtrlX, notchCtrlY),
1182
+ new import_paper.default.Point(0.5, notchY)
1183
+ );
1184
+ heartPath.closed = true;
1185
+ return heartPath;
1245
1186
  }
1246
- function rectByCenter(centerX, centerY, width, height) {
1247
- return {
1248
- left: centerX - width / 2,
1249
- top: centerY - height / 2,
1187
+ function createHeartBaseShape(options) {
1188
+ const { x, y, width, height } = options;
1189
+ const w = Math.max(0, width);
1190
+ const h = Math.max(0, height);
1191
+ const left = x - w / 2;
1192
+ const top = y - h / 2;
1193
+ const fitMode = getShapeFitMode(options.shapeStyle);
1194
+ const heartParams = getHeartShapeParams(options.shapeStyle);
1195
+ const rawHeart = createNormalizedHeartPath(heartParams);
1196
+ return fitPathItemToRect(rawHeart, { left, top, width: w, height: h }, fitMode);
1197
+ }
1198
+ var BUILTIN_SHAPE_BUILDERS = {
1199
+ rect: (options) => {
1200
+ const { x, y, width, height, radius } = options;
1201
+ return new import_paper.default.Path.Rectangle({
1202
+ point: [x - width / 2, y - height / 2],
1203
+ size: [Math.max(0, width), Math.max(0, height)],
1204
+ radius: Math.max(0, radius)
1205
+ });
1206
+ },
1207
+ circle: (options) => {
1208
+ const { x, y, width, height } = options;
1209
+ const r = Math.min(width, height) / 2;
1210
+ return new import_paper.default.Path.Circle({
1211
+ center: new import_paper.default.Point(x, y),
1212
+ radius: Math.max(0, r)
1213
+ });
1214
+ },
1215
+ ellipse: (options) => {
1216
+ const { x, y, width, height } = options;
1217
+ return new import_paper.default.Path.Ellipse({
1218
+ center: new import_paper.default.Point(x, y),
1219
+ radius: [Math.max(0, width / 2), Math.max(0, height / 2)]
1220
+ });
1221
+ },
1222
+ heart: createHeartBaseShape
1223
+ };
1224
+ function createCustomBaseShape(options) {
1225
+ var _a;
1226
+ const {
1227
+ pathData,
1228
+ customSourceWidthPx,
1229
+ customSourceHeightPx,
1230
+ x,
1231
+ y,
1250
1232
  width,
1251
- height,
1252
- centerX,
1253
- centerY
1254
- };
1233
+ height
1234
+ } = options;
1235
+ if (typeof pathData !== "string" || pathData.trim().length === 0) {
1236
+ return null;
1237
+ }
1238
+ const center = new import_paper.default.Point(x, y);
1239
+ const hasMultipleSubPaths = ((_a = (pathData.match(/[Mm]/g) || []).length) != null ? _a : 0) > 1;
1240
+ const path = hasMultipleSubPaths ? new import_paper.default.CompoundPath(pathData) : (() => {
1241
+ const single = new import_paper.default.Path();
1242
+ single.pathData = pathData;
1243
+ return single;
1244
+ })();
1245
+ const sourceWidth = Number(customSourceWidthPx != null ? customSourceWidthPx : 0);
1246
+ const sourceHeight = Number(customSourceHeightPx != null ? customSourceHeightPx : 0);
1247
+ if (Number.isFinite(sourceWidth) && Number.isFinite(sourceHeight) && sourceWidth > 0 && sourceHeight > 0 && width > 0 && height > 0) {
1248
+ const targetLeft = x - width / 2;
1249
+ const targetTop = y - height / 2;
1250
+ path.scale(width / sourceWidth, height / sourceHeight, new import_paper.default.Point(0, 0));
1251
+ path.translate(new import_paper.default.Point(targetLeft, targetTop));
1252
+ return path;
1253
+ }
1254
+ if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
1255
+ path.position = center;
1256
+ path.scale(width / path.bounds.width, height / path.bounds.height);
1257
+ return path;
1258
+ }
1259
+ path.position = center;
1260
+ return path;
1255
1261
  }
1256
- function getCutSizeMm(size) {
1257
- if (size.cutMode === "trim") {
1258
- return { widthMm: size.actualWidthMm, heightMm: size.actualHeightMm };
1262
+ function createBaseShape(options) {
1263
+ const { shape } = options;
1264
+ if (shape === "custom") {
1265
+ const customShape = createCustomBaseShape(options);
1266
+ if (customShape) return customShape;
1267
+ return BUILTIN_SHAPE_BUILDERS[DEFAULT_DIELINE_SHAPE](options);
1259
1268
  }
1260
- const delta = size.cutMarginMm * 2;
1261
- if (size.cutMode === "outset") {
1262
- return {
1263
- widthMm: size.actualWidthMm + delta,
1264
- heightMm: size.actualHeightMm + delta
1265
- };
1269
+ return BUILTIN_SHAPE_BUILDERS[shape](options);
1270
+ }
1271
+ function resolveBridgeBasePath(shape, anchor) {
1272
+ if (shape instanceof import_paper.default.Path) {
1273
+ return shape;
1274
+ }
1275
+ if (shape instanceof import_paper.default.CompoundPath) {
1276
+ const children = (shape.children || []).filter(
1277
+ (child) => child instanceof import_paper.default.Path
1278
+ );
1279
+ if (!children.length) return null;
1280
+ let best = children[0];
1281
+ let bestDistance = Infinity;
1282
+ for (const child of children) {
1283
+ const location = child.getNearestLocation(anchor);
1284
+ const point = location == null ? void 0 : location.point;
1285
+ if (!point) continue;
1286
+ const distance = point.getDistance(anchor);
1287
+ if (distance < bestDistance) {
1288
+ bestDistance = distance;
1289
+ best = child;
1290
+ }
1291
+ }
1292
+ return best;
1293
+ }
1294
+ return null;
1295
+ }
1296
+ function createFeatureItem(feature, center) {
1297
+ let item;
1298
+ if (feature.shape === "rect") {
1299
+ const w = feature.width || 10;
1300
+ const h = feature.height || 10;
1301
+ const r = feature.radius || 0;
1302
+ item = new import_paper.default.Path.Rectangle({
1303
+ point: [center.x - w / 2, center.y - h / 2],
1304
+ size: [w, h],
1305
+ radius: r
1306
+ });
1307
+ } else {
1308
+ const r = feature.radius || 5;
1309
+ item = new import_paper.default.Path.Circle({
1310
+ center,
1311
+ radius: r
1312
+ });
1313
+ }
1314
+ if (feature.rotation) {
1315
+ item.rotate(feature.rotation, center);
1316
+ }
1317
+ return item;
1318
+ }
1319
+ function getPerimeterShape(options) {
1320
+ let mainShape = createBaseShape(options);
1321
+ const { features } = options;
1322
+ if (features && features.length > 0) {
1323
+ const edgeFeatures = features.filter(
1324
+ (f) => !f.renderBehavior || f.renderBehavior === "edge"
1325
+ );
1326
+ const adds = [];
1327
+ const subtracts = [];
1328
+ edgeFeatures.forEach((f) => {
1329
+ const pos = resolveFeaturePosition(f, options);
1330
+ const center = new import_paper.default.Point(pos.x, pos.y);
1331
+ const item = createFeatureItem(f, center);
1332
+ if (f.bridge && f.bridge.type === "vertical") {
1333
+ const itemBounds = item.bounds;
1334
+ const mainBounds = mainShape.bounds;
1335
+ const bridgeTop = mainBounds.top;
1336
+ const bridgeBottom = itemBounds.top;
1337
+ if (bridgeBottom > bridgeTop) {
1338
+ const overlap = 2;
1339
+ const rayPadding = 10;
1340
+ const eps = 0.1;
1341
+ const delta = getBridgeDelta(itemBounds, overlap);
1342
+ const toY = bridgeTop - rayPadding;
1343
+ const inset = Math.min(1, Math.max(0, itemBounds.width * 0.01));
1344
+ const xLeft = itemBounds.left + inset;
1345
+ const xRight = itemBounds.right - inset;
1346
+ const bridgeBasePath = resolveBridgeBasePath(mainShape, center);
1347
+ const canBridge = !!bridgeBasePath && xRight - xLeft > eps;
1348
+ if (canBridge && bridgeBasePath) {
1349
+ const leftHit = getExitHit({
1350
+ mainShape: bridgeBasePath,
1351
+ x: xLeft,
1352
+ bridgeBottom,
1353
+ toY,
1354
+ eps,
1355
+ delta,
1356
+ overlap,
1357
+ op: f.operation
1358
+ });
1359
+ const rightHit = getExitHit({
1360
+ mainShape: bridgeBasePath,
1361
+ x: xRight,
1362
+ bridgeBottom,
1363
+ toY,
1364
+ eps,
1365
+ delta,
1366
+ overlap,
1367
+ op: f.operation
1368
+ });
1369
+ if (leftHit && rightHit) {
1370
+ const pathLength = bridgeBasePath.length;
1371
+ const leftOffset = leftHit.location.offset;
1372
+ const rightOffset = rightHit.location.offset;
1373
+ const distanceA = wrappedDistance(pathLength, leftOffset, rightOffset);
1374
+ const distanceB = wrappedDistance(pathLength, rightOffset, leftOffset);
1375
+ const countFor = (d) => Math.max(8, Math.min(80, Math.ceil(d / 6)));
1376
+ const offsetsA = sampleWrappedOffsets(
1377
+ pathLength,
1378
+ leftOffset,
1379
+ rightOffset,
1380
+ countFor(distanceA)
1381
+ );
1382
+ const offsetsB = sampleWrappedOffsets(
1383
+ pathLength,
1384
+ rightOffset,
1385
+ leftOffset,
1386
+ countFor(distanceB)
1387
+ );
1388
+ const pointsA = offsetsA.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
1389
+ const pointsB = offsetsB.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
1390
+ if (pointsA.length >= 2 && pointsB.length >= 2) {
1391
+ let topBase = selectOuterChain({
1392
+ mainShape: bridgeBasePath,
1393
+ pointsA,
1394
+ pointsB,
1395
+ delta,
1396
+ overlap,
1397
+ op: f.operation
1398
+ });
1399
+ const dist2 = (a, b) => {
1400
+ const dx = a.x - b.x;
1401
+ const dy = a.y - b.y;
1402
+ return dx * dx + dy * dy;
1403
+ };
1404
+ if (dist2(topBase[0], leftHit.point) > dist2(topBase[0], rightHit.point)) {
1405
+ topBase = topBase.slice().reverse();
1406
+ }
1407
+ topBase = topBase.slice();
1408
+ topBase[0] = leftHit.point;
1409
+ topBase[topBase.length - 1] = rightHit.point;
1410
+ const capShiftY = f.operation === "subtract" ? -Math.max(overlap * 2, delta) : overlap;
1411
+ const topPoints = topBase.map(
1412
+ (p) => p.add(new import_paper.default.Point(0, capShiftY))
1413
+ );
1414
+ const bridgeBottomY = bridgeBottom + overlap * 2;
1415
+ const bridgePoly = new import_paper.default.Path({ insert: false });
1416
+ for (const p of topPoints) bridgePoly.add(p);
1417
+ bridgePoly.add(new import_paper.default.Point(xRight, bridgeBottomY));
1418
+ bridgePoly.add(new import_paper.default.Point(xLeft, bridgeBottomY));
1419
+ bridgePoly.closed = true;
1420
+ const unitedItem = item.unite(bridgePoly);
1421
+ item.remove();
1422
+ bridgePoly.remove();
1423
+ if (f.operation === "add") {
1424
+ adds.push(unitedItem);
1425
+ } else {
1426
+ subtracts.push(unitedItem);
1427
+ }
1428
+ return;
1429
+ }
1430
+ }
1431
+ }
1432
+ if (f.operation === "add") {
1433
+ adds.push(item);
1434
+ } else {
1435
+ subtracts.push(item);
1436
+ }
1437
+ } else {
1438
+ if (f.operation === "add") {
1439
+ adds.push(item);
1440
+ } else {
1441
+ subtracts.push(item);
1442
+ }
1443
+ }
1444
+ } else {
1445
+ if (f.operation === "add") {
1446
+ adds.push(item);
1447
+ } else {
1448
+ subtracts.push(item);
1449
+ }
1450
+ }
1451
+ });
1452
+ if (adds.length > 0) {
1453
+ for (const item of adds) {
1454
+ try {
1455
+ const temp = mainShape.unite(item);
1456
+ mainShape.remove();
1457
+ item.remove();
1458
+ mainShape = normalizePathItem(temp);
1459
+ } catch (e) {
1460
+ console.error("Geometry: Failed to unite feature", e);
1461
+ item.remove();
1462
+ }
1463
+ }
1464
+ }
1465
+ if (subtracts.length > 0) {
1466
+ for (const item of subtracts) {
1467
+ try {
1468
+ const temp = mainShape.subtract(item);
1469
+ mainShape.remove();
1470
+ item.remove();
1471
+ mainShape = normalizePathItem(temp);
1472
+ } catch (e) {
1473
+ console.error("Geometry: Failed to subtract feature", e);
1474
+ item.remove();
1475
+ }
1476
+ }
1477
+ }
1266
1478
  }
1267
- return {
1268
- widthMm: Math.max(size.minMm, size.actualWidthMm - delta),
1269
- heightMm: Math.max(size.minMm, size.actualHeightMm - delta)
1270
- };
1479
+ return mainShape;
1271
1480
  }
1272
- function computeSceneLayout(canvasService, size) {
1273
- const canvasWidth = canvasService.canvas.width || 0;
1274
- const canvasHeight = canvasService.canvas.height || 0;
1275
- if (canvasWidth <= 0 || canvasHeight <= 0) return null;
1276
- const { widthMm: cutWidthMm, heightMm: cutHeightMm } = getCutSizeMm(size);
1277
- const viewWidthMm = Math.max(size.actualWidthMm, cutWidthMm);
1278
- const viewHeightMm = Math.max(size.actualHeightMm, cutHeightMm);
1279
- if (!Number.isFinite(viewWidthMm) || !Number.isFinite(viewHeightMm) || viewWidthMm <= 0 || viewHeightMm <= 0) {
1280
- return null;
1281
- }
1282
- const paddingPx = resolvePaddingPx(
1283
- size.viewPadding,
1284
- canvasWidth,
1285
- canvasHeight
1481
+ function applySurfaceFeatures(shape, features, options) {
1482
+ const surfaceFeatures = features.filter(
1483
+ (f) => f.renderBehavior === "surface"
1286
1484
  );
1287
- canvasService.viewport.updateContainer(canvasWidth, canvasHeight);
1288
- canvasService.viewport.setPadding(paddingPx);
1289
- canvasService.viewport.updatePhysical(viewWidthMm, viewHeightMm);
1290
- const layout = canvasService.viewport.layout;
1291
- if (!Number.isFinite(layout.scale) || !Number.isFinite(layout.offsetX) || !Number.isFinite(layout.offsetY) || layout.scale <= 0) {
1292
- return null;
1485
+ if (surfaceFeatures.length === 0) return shape;
1486
+ let result = shape;
1487
+ for (const f of surfaceFeatures) {
1488
+ const pos = resolveFeaturePosition(f, options);
1489
+ const center = new import_paper.default.Point(pos.x, pos.y);
1490
+ const item = createFeatureItem(f, center);
1491
+ try {
1492
+ if (f.operation === "add") {
1493
+ const temp = result.unite(item);
1494
+ result.remove();
1495
+ item.remove();
1496
+ result = normalizePathItem(temp);
1497
+ } else {
1498
+ const temp = result.subtract(item);
1499
+ result.remove();
1500
+ item.remove();
1501
+ result = normalizePathItem(temp);
1502
+ }
1503
+ } catch (e) {
1504
+ console.error("Geometry: Failed to apply surface feature", e);
1505
+ item.remove();
1506
+ }
1293
1507
  }
1294
- const centerX = layout.offsetX + layout.width / 2;
1295
- const centerY = layout.offsetY + layout.height / 2;
1296
- const trimWidthPx = size.actualWidthMm * layout.scale;
1297
- const trimHeightPx = size.actualHeightMm * layout.scale;
1298
- const cutWidthPx = cutWidthMm * layout.scale;
1299
- const cutHeightPx = cutHeightMm * layout.scale;
1300
- const trimRect = rectByCenter(centerX, centerY, trimWidthPx, trimHeightPx);
1301
- const cutRect = rectByCenter(centerX, centerY, cutWidthPx, cutHeightPx);
1302
- const bleedRect = rectByCenter(
1303
- centerX,
1304
- centerY,
1305
- Math.max(trimWidthPx, cutWidthPx),
1306
- Math.max(trimHeightPx, cutHeightPx)
1307
- );
1308
- return {
1309
- scale: layout.scale,
1310
- canvasWidth,
1311
- canvasHeight,
1312
- trimRect,
1313
- cutRect,
1314
- bleedRect,
1315
- trimWidthMm: size.actualWidthMm,
1316
- trimHeightMm: size.actualHeightMm,
1317
- cutWidthMm,
1318
- cutHeightMm,
1319
- cutMode: size.cutMode,
1320
- cutMarginMm: size.cutMarginMm
1321
- };
1508
+ return result;
1322
1509
  }
1323
- function buildSceneGeometry(configService, layout) {
1324
- const radiusMm = parseLengthToMm(
1325
- configService.get("dieline.radius", 0),
1326
- "mm"
1327
- );
1328
- const offset = (layout.cutRect.width - layout.trimRect.width) / 2;
1329
- const sourceWidth = Number(configService.get("dieline.customSourceWidthPx", 0));
1330
- const sourceHeight = Number(
1331
- configService.get("dieline.customSourceHeightPx", 0)
1510
+ function generateDielinePath(options) {
1511
+ const paperWidth = options.canvasWidth || options.width * 2 || 2e3;
1512
+ const paperHeight = options.canvasHeight || options.height * 2 || 2e3;
1513
+ ensurePaper(paperWidth, paperHeight);
1514
+ import_paper.default.project.activeLayer.removeChildren();
1515
+ const perimeter = getPerimeterShape(options);
1516
+ const finalShape = applySurfaceFeatures(perimeter, options.features, options);
1517
+ const pathData = finalShape.pathData;
1518
+ finalShape.remove();
1519
+ return pathData;
1520
+ }
1521
+ function generateBleedZonePath(originalOptions, offsetOptions, offset) {
1522
+ const paperWidth = originalOptions.canvasWidth || originalOptions.width * 2 || 2e3;
1523
+ const paperHeight = originalOptions.canvasHeight || originalOptions.height * 2 || 2e3;
1524
+ ensurePaper(paperWidth, paperHeight);
1525
+ import_paper.default.project.activeLayer.removeChildren();
1526
+ const pOriginal = getPerimeterShape(originalOptions);
1527
+ const shapeOriginal = applySurfaceFeatures(
1528
+ pOriginal,
1529
+ originalOptions.features,
1530
+ originalOptions
1332
1531
  );
1333
- const shapeStyle = normalizeShapeStyle(
1334
- configService.get("dieline.shapeStyle", DEFAULT_DIELINE_SHAPE_STYLE)
1532
+ const pOffset = getPerimeterShape(offsetOptions);
1533
+ const shapeOffset = applySurfaceFeatures(
1534
+ pOffset,
1535
+ offsetOptions.features,
1536
+ offsetOptions
1335
1537
  );
1538
+ let bleedZone;
1539
+ if (offset > 0) {
1540
+ bleedZone = shapeOffset.subtract(shapeOriginal);
1541
+ } else {
1542
+ bleedZone = shapeOriginal.subtract(shapeOffset);
1543
+ }
1544
+ const pathData = bleedZone.pathData;
1545
+ shapeOriginal.remove();
1546
+ shapeOffset.remove();
1547
+ bleedZone.remove();
1548
+ return pathData;
1549
+ }
1550
+ function getLowestPointOnDieline(options) {
1551
+ ensurePaper(options.width * 2, options.height * 2);
1552
+ import_paper.default.project.activeLayer.removeChildren();
1553
+ const shape = createBaseShape(options);
1554
+ const bounds = shape.bounds;
1555
+ const result = {
1556
+ x: bounds.center.x,
1557
+ y: bounds.bottom
1558
+ };
1559
+ shape.remove();
1560
+ return result;
1561
+ }
1562
+ function getNearestPointOnDieline(point, options) {
1563
+ ensurePaper(options.width * 2, options.height * 2);
1564
+ import_paper.default.project.activeLayer.removeChildren();
1565
+ const shape = createBaseShape(options);
1566
+ const p = new import_paper.default.Point(point.x, point.y);
1567
+ const location = shape.getNearestLocation(p);
1568
+ const result = {
1569
+ x: location.point.x,
1570
+ y: location.point.y,
1571
+ normal: location.normal ? { x: location.normal.x, y: location.normal.y } : void 0
1572
+ };
1573
+ shape.remove();
1574
+ return result;
1575
+ }
1576
+ function getPathBounds(pathData) {
1577
+ const path = new import_paper.default.Path();
1578
+ path.pathData = pathData;
1579
+ const bounds = path.bounds;
1580
+ path.remove();
1336
1581
  return {
1337
- shape: normalizeDielineShape(
1338
- configService.get("dieline.shape", DEFAULT_DIELINE_SHAPE)
1339
- ),
1340
- shapeStyle,
1341
- unit: "px",
1342
- x: layout.trimRect.centerX,
1343
- y: layout.trimRect.centerY,
1344
- width: layout.trimRect.width,
1345
- height: layout.trimRect.height,
1346
- radius: radiusMm * layout.scale,
1347
- offset,
1348
- scale: layout.scale,
1349
- pathData: configService.get("dieline.pathData"),
1350
- customSourceWidthPx: Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : void 0,
1351
- customSourceHeightPx: Number.isFinite(sourceHeight) && sourceHeight > 0 ? sourceHeight : void 0
1582
+ x: bounds.x,
1583
+ y: bounds.y,
1584
+ width: bounds.width,
1585
+ height: bounds.height
1352
1586
  };
1353
1587
  }
1354
1588
 
1355
1589
  // src/extensions/image.ts
1356
1590
  var IMAGE_OBJECT_LAYER_ID = "image.user";
1357
1591
  var IMAGE_OVERLAY_LAYER_ID = "image-overlay";
1592
+ var IMAGE_DEFAULT_CONTROL_CAPABILITIES = [
1593
+ "rotate",
1594
+ "scale"
1595
+ ];
1596
+ var IMAGE_CONTROL_DESCRIPTORS = [
1597
+ {
1598
+ key: "tl",
1599
+ capability: "rotate",
1600
+ create: () => new import_fabric2.Control({
1601
+ x: -0.5,
1602
+ y: -0.5,
1603
+ actionName: "rotate",
1604
+ actionHandler: import_fabric2.controlsUtils.rotationWithSnapping,
1605
+ cursorStyleHandler: import_fabric2.controlsUtils.rotationStyleHandler
1606
+ })
1607
+ },
1608
+ {
1609
+ key: "br",
1610
+ capability: "scale",
1611
+ create: () => new import_fabric2.Control({
1612
+ x: 0.5,
1613
+ y: 0.5,
1614
+ actionName: "scale",
1615
+ actionHandler: import_fabric2.controlsUtils.scalingEqually,
1616
+ cursorStyleHandler: import_fabric2.controlsUtils.scaleCursorStyleHandler
1617
+ })
1618
+ }
1619
+ ];
1358
1620
  var ImageTool = class {
1359
1621
  constructor() {
1360
1622
  this.id = "pooder.kit.image";
@@ -1371,7 +1633,9 @@ var ImageTool = class {
1371
1633
  this.isImageSelectionActive = false;
1372
1634
  this.focusedImageId = null;
1373
1635
  this.renderSeq = 0;
1636
+ this.imageSpecs = [];
1374
1637
  this.overlaySpecs = [];
1638
+ this.imageControlsByCapabilityKey = /* @__PURE__ */ new Map();
1375
1639
  this.onToolActivated = (event) => {
1376
1640
  const before = this.isToolActive;
1377
1641
  this.syncToolActiveFromWorkbench(event.id);
@@ -1478,9 +1742,34 @@ var ImageTool = class {
1478
1742
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
1479
1743
  this.id,
1480
1744
  () => ({
1481
- rootLayerSpecs: {
1482
- [IMAGE_OVERLAY_LAYER_ID]: this.overlaySpecs
1483
- }
1745
+ passes: [
1746
+ {
1747
+ id: IMAGE_OBJECT_LAYER_ID,
1748
+ stack: 500,
1749
+ order: 0,
1750
+ visibility: {
1751
+ op: "not",
1752
+ expr: {
1753
+ op: "sessionActive",
1754
+ toolId: "pooder.kit.white-ink"
1755
+ }
1756
+ },
1757
+ objects: this.imageSpecs
1758
+ },
1759
+ {
1760
+ id: IMAGE_OVERLAY_LAYER_ID,
1761
+ stack: 800,
1762
+ order: 0,
1763
+ visibility: {
1764
+ op: "not",
1765
+ expr: {
1766
+ op: "sessionActive",
1767
+ toolId: "pooder.kit.white-ink"
1768
+ }
1769
+ },
1770
+ objects: this.overlaySpecs
1771
+ }
1772
+ ]
1484
1773
  }),
1485
1774
  { priority: 300 }
1486
1775
  );
@@ -1511,7 +1800,10 @@ var ImageTool = class {
1511
1800
  this.updateImages();
1512
1801
  return;
1513
1802
  }
1514
- if (e.key.startsWith("size.") || e.key.startsWith("image.frame.")) {
1803
+ if (e.key.startsWith("size.") || e.key.startsWith("image.frame.") || e.key.startsWith("image.control.")) {
1804
+ if (e.key.startsWith("image.control.")) {
1805
+ this.imageControlsByCapabilityKey.clear();
1806
+ }
1515
1807
  this.updateImages();
1516
1808
  }
1517
1809
  });
@@ -1537,7 +1829,9 @@ var ImageTool = class {
1537
1829
  this.cropShapeHatchPattern = void 0;
1538
1830
  this.cropShapeHatchPatternColor = void 0;
1539
1831
  this.cropShapeHatchPatternKey = void 0;
1832
+ this.imageSpecs = [];
1540
1833
  this.overlaySpecs = [];
1834
+ this.imageControlsByCapabilityKey.clear();
1541
1835
  this.clearRenderedImages();
1542
1836
  (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
1543
1837
  this.renderProducerDisposable = void 0;
@@ -1560,6 +1854,90 @@ var ImageTool = class {
1560
1854
  isImageEditingVisible() {
1561
1855
  return this.isToolActive || this.isImageSelectionActive || !!this.focusedImageId;
1562
1856
  }
1857
+ getEnabledImageControlCapabilities() {
1858
+ return IMAGE_DEFAULT_CONTROL_CAPABILITIES;
1859
+ }
1860
+ getImageControls(capabilities) {
1861
+ const normalized = [...new Set(capabilities)].sort();
1862
+ const cacheKey = normalized.join("|");
1863
+ const cached = this.imageControlsByCapabilityKey.get(cacheKey);
1864
+ if (cached) {
1865
+ return cached;
1866
+ }
1867
+ const enabled = new Set(normalized);
1868
+ const controls = {};
1869
+ IMAGE_CONTROL_DESCRIPTORS.forEach((descriptor) => {
1870
+ if (!enabled.has(descriptor.capability)) return;
1871
+ controls[descriptor.key] = descriptor.create();
1872
+ });
1873
+ this.imageControlsByCapabilityKey.set(cacheKey, controls);
1874
+ return controls;
1875
+ }
1876
+ getImageControlVisualConfig() {
1877
+ var _a, _b, _c, _d;
1878
+ const cornerSizeRaw = Number(
1879
+ (_a = this.getConfig("image.control.cornerSize", 14)) != null ? _a : 14
1880
+ );
1881
+ const touchCornerSizeRaw = Number(
1882
+ (_b = this.getConfig("image.control.touchCornerSize", 24)) != null ? _b : 24
1883
+ );
1884
+ const borderScaleFactorRaw = Number(
1885
+ (_c = this.getConfig("image.control.borderScaleFactor", 1.5)) != null ? _c : 1.5
1886
+ );
1887
+ const paddingRaw = Number(
1888
+ (_d = this.getConfig("image.control.padding", 0)) != null ? _d : 0
1889
+ );
1890
+ const cornerStyleRaw = this.getConfig(
1891
+ "image.control.cornerStyle",
1892
+ "circle"
1893
+ ) || "circle";
1894
+ const cornerStyle = cornerStyleRaw === "rect" ? "rect" : "circle";
1895
+ return {
1896
+ cornerSize: Number.isFinite(cornerSizeRaw) ? Math.max(4, Math.min(64, cornerSizeRaw)) : 14,
1897
+ touchCornerSize: Number.isFinite(touchCornerSizeRaw) ? Math.max(8, Math.min(96, touchCornerSizeRaw)) : 24,
1898
+ cornerStyle,
1899
+ cornerColor: this.getConfig("image.control.cornerColor", "#ffffff") || "#ffffff",
1900
+ cornerStrokeColor: this.getConfig("image.control.cornerStrokeColor", "#1677ff") || "#1677ff",
1901
+ transparentCorners: !!this.getConfig(
1902
+ "image.control.transparentCorners",
1903
+ false
1904
+ ),
1905
+ borderColor: this.getConfig("image.control.borderColor", "#1677ff") || "#1677ff",
1906
+ borderScaleFactor: Number.isFinite(borderScaleFactorRaw) ? Math.max(0.5, Math.min(8, borderScaleFactorRaw)) : 1.5,
1907
+ padding: Number.isFinite(paddingRaw) ? Math.max(0, Math.min(64, paddingRaw)) : 0
1908
+ };
1909
+ }
1910
+ applyImageObjectInteractionState(obj) {
1911
+ var _a;
1912
+ if (!obj) return;
1913
+ const visible = this.isImageEditingVisible();
1914
+ const visual = this.getImageControlVisualConfig();
1915
+ obj.set({
1916
+ selectable: visible,
1917
+ evented: visible,
1918
+ hasControls: visible,
1919
+ hasBorders: visible,
1920
+ lockScalingFlip: true,
1921
+ cornerSize: visual.cornerSize,
1922
+ touchCornerSize: visual.touchCornerSize,
1923
+ cornerStyle: visual.cornerStyle,
1924
+ cornerColor: visual.cornerColor,
1925
+ cornerStrokeColor: visual.cornerStrokeColor,
1926
+ transparentCorners: visual.transparentCorners,
1927
+ borderColor: visual.borderColor,
1928
+ borderScaleFactor: visual.borderScaleFactor,
1929
+ padding: visual.padding
1930
+ });
1931
+ obj.controls = this.getImageControls(
1932
+ this.getEnabledImageControlCapabilities()
1933
+ );
1934
+ (_a = obj.setCoords) == null ? void 0 : _a.call(obj);
1935
+ }
1936
+ refreshImageObjectInteractionState() {
1937
+ this.getImageObjects().forEach(
1938
+ (obj) => this.applyImageObjectInteractionState(obj)
1939
+ );
1940
+ }
1563
1941
  isDebugEnabled() {
1564
1942
  return !!this.getConfig("image.debug", false);
1565
1943
  }
@@ -1591,17 +1969,84 @@ var ImageTool = class {
1591
1969
  ],
1592
1970
  [import_core2.ContributionPointIds.CONFIGURATIONS]: [
1593
1971
  {
1594
- id: "image.items",
1595
- type: "array",
1596
- label: "Images",
1597
- default: []
1972
+ id: "image.items",
1973
+ type: "array",
1974
+ label: "Images",
1975
+ default: []
1976
+ },
1977
+ {
1978
+ id: "image.debug",
1979
+ type: "boolean",
1980
+ label: "Image Debug Log",
1981
+ default: false
1982
+ },
1983
+ {
1984
+ id: "image.control.cornerSize",
1985
+ type: "number",
1986
+ label: "Image Control Corner Size",
1987
+ min: 4,
1988
+ max: 64,
1989
+ step: 1,
1990
+ default: 14
1991
+ },
1992
+ {
1993
+ id: "image.control.touchCornerSize",
1994
+ type: "number",
1995
+ label: "Image Control Touch Corner Size",
1996
+ min: 8,
1997
+ max: 96,
1998
+ step: 1,
1999
+ default: 24
2000
+ },
2001
+ {
2002
+ id: "image.control.cornerStyle",
2003
+ type: "select",
2004
+ label: "Image Control Corner Style",
2005
+ options: ["circle", "rect"],
2006
+ default: "circle"
1598
2007
  },
1599
2008
  {
1600
- id: "image.debug",
2009
+ id: "image.control.cornerColor",
2010
+ type: "color",
2011
+ label: "Image Control Corner Color",
2012
+ default: "#ffffff"
2013
+ },
2014
+ {
2015
+ id: "image.control.cornerStrokeColor",
2016
+ type: "color",
2017
+ label: "Image Control Corner Stroke Color",
2018
+ default: "#1677ff"
2019
+ },
2020
+ {
2021
+ id: "image.control.transparentCorners",
1601
2022
  type: "boolean",
1602
- label: "Image Debug Log",
2023
+ label: "Image Control Transparent Corners",
1603
2024
  default: false
1604
2025
  },
2026
+ {
2027
+ id: "image.control.borderColor",
2028
+ type: "color",
2029
+ label: "Image Control Border Color",
2030
+ default: "#1677ff"
2031
+ },
2032
+ {
2033
+ id: "image.control.borderScaleFactor",
2034
+ type: "number",
2035
+ label: "Image Control Border Width",
2036
+ min: 0.5,
2037
+ max: 8,
2038
+ step: 0.1,
2039
+ default: 1.5
2040
+ },
2041
+ {
2042
+ id: "image.control.padding",
2043
+ type: "number",
2044
+ label: "Image Control Padding",
2045
+ min: 0,
2046
+ max: 64,
2047
+ step: 1,
2048
+ default: 0
2049
+ },
1605
2050
  {
1606
2051
  id: "image.frame.strokeColor",
1607
2052
  type: "color",
@@ -1839,12 +2284,7 @@ var ImageTool = class {
1839
2284
  } else {
1840
2285
  const obj = this.getImageObject(id);
1841
2286
  if (obj) {
1842
- obj.set({
1843
- selectable: true,
1844
- evented: true,
1845
- hasControls: true,
1846
- hasBorders: true
1847
- });
2287
+ this.applyImageObjectInteractionState(obj);
1848
2288
  canvas.setActiveObject(obj);
1849
2289
  }
1850
2290
  }
@@ -2012,9 +2452,7 @@ var ImageTool = class {
2012
2452
  }
2013
2453
  getOverlayObjects() {
2014
2454
  if (!this.canvasService) return [];
2015
- return this.canvasService.getRootLayerObjects(
2016
- IMAGE_OVERLAY_LAYER_ID
2017
- );
2455
+ return this.canvasService.getPassObjects(IMAGE_OVERLAY_LAYER_ID);
2018
2456
  }
2019
2457
  getImageObject(id) {
2020
2458
  return this.getImageObjects().find((obj) => {
@@ -2024,9 +2462,9 @@ var ImageTool = class {
2024
2462
  }
2025
2463
  clearRenderedImages() {
2026
2464
  if (!this.canvasService) return;
2027
- const canvas = this.canvasService.canvas;
2028
- this.getImageObjects().forEach((obj) => canvas.remove(obj));
2029
- this.canvasService.requestRenderAll();
2465
+ this.imageSpecs = [];
2466
+ this.overlaySpecs = [];
2467
+ this.canvasService.requestRenderFromProducers();
2030
2468
  }
2031
2469
  purgeSourceSizeCacheForItem(item) {
2032
2470
  if (!item) return;
@@ -2054,6 +2492,29 @@ var ImageTool = class {
2054
2492
  }
2055
2493
  return { width: 1, height: 1 };
2056
2494
  }
2495
+ async ensureSourceSize(src) {
2496
+ if (!src) return null;
2497
+ const cached = this.sourceSizeBySrc.get(src);
2498
+ if (cached) return cached;
2499
+ try {
2500
+ const image = await import_fabric2.Image.fromURL(src, {
2501
+ crossOrigin: "anonymous"
2502
+ });
2503
+ const width = Number((image == null ? void 0 : image.width) || 0);
2504
+ const height = Number((image == null ? void 0 : image.height) || 0);
2505
+ if (width > 0 && height > 0) {
2506
+ const size = { width, height };
2507
+ this.sourceSizeBySrc.set(src, size);
2508
+ return size;
2509
+ }
2510
+ } catch (error) {
2511
+ this.debug("image:size:load-failed", {
2512
+ src,
2513
+ error: error instanceof Error ? error.message : String(error)
2514
+ });
2515
+ }
2516
+ return null;
2517
+ }
2057
2518
  getCoverScale(frame, size) {
2058
2519
  const sw = Math.max(1, size.width);
2059
2520
  const sh = Math.max(1, size.height);
@@ -2388,24 +2849,6 @@ var ImageTool = class {
2388
2849
  opacity: render.opacity
2389
2850
  };
2390
2851
  }
2391
- toScreenObjectProps(props) {
2392
- if (!this.canvasService) return props;
2393
- const next = { ...props };
2394
- if (Number.isFinite(next.left) || Number.isFinite(next.top)) {
2395
- const mapped = this.canvasService.toScreenPoint({
2396
- x: Number.isFinite(next.left) ? Number(next.left) : 0,
2397
- y: Number.isFinite(next.top) ? Number(next.top) : 0
2398
- });
2399
- if (Number.isFinite(next.left)) next.left = mapped.x;
2400
- if (Number.isFinite(next.top)) next.top = mapped.y;
2401
- }
2402
- const sceneScale = this.canvasService.getSceneScale();
2403
- const sx = Number.isFinite(next.scaleX) ? Number(next.scaleX) : 1;
2404
- const sy = Number.isFinite(next.scaleY) ? Number(next.scaleY) : 1;
2405
- next.scaleX = sx * sceneScale;
2406
- next.scaleY = sy * sceneScale;
2407
- return next;
2408
- }
2409
2852
  toSceneObjectScale(value) {
2410
2853
  if (!this.canvasService) return value;
2411
2854
  return value / this.canvasService.getSceneScale();
@@ -2416,104 +2859,27 @@ var ImageTool = class {
2416
2859
  if (typeof obj.getSrc === "function") return obj.getSrc();
2417
2860
  return (_a = obj == null ? void 0 : obj._originalElement) == null ? void 0 : _a.src;
2418
2861
  }
2419
- applyImageControlVisibility(obj) {
2420
- if (typeof (obj == null ? void 0 : obj.setControlsVisibility) !== "function") return;
2421
- obj.setControlsVisibility({
2422
- mt: false,
2423
- mb: false,
2424
- ml: false,
2425
- mr: false,
2426
- tl: true,
2427
- tr: true,
2428
- bl: true,
2429
- br: true,
2430
- mtr: true
2431
- });
2432
- }
2433
- async upsertImageObject(item, frame, seq) {
2434
- if (!this.canvasService) return;
2435
- const canvas = this.canvasService.canvas;
2436
- const render = this.resolveRenderImageState(item);
2437
- if (!render.src) return;
2438
- let obj = this.getImageObject(item.id);
2439
- const currentSrc = this.getCurrentSrc(obj);
2440
- if (obj && currentSrc && currentSrc !== render.src) {
2441
- canvas.remove(obj);
2442
- obj = void 0;
2443
- }
2444
- if (!obj) {
2445
- const created = await import_fabric2.Image.fromURL(render.src, {
2446
- crossOrigin: "anonymous"
2447
- });
2448
- if (seq !== this.renderSeq) return;
2449
- created.set({
2862
+ async buildImageSpecs(items, frame) {
2863
+ const specs = [];
2864
+ for (const item of items) {
2865
+ const render = this.resolveRenderImageState(item);
2866
+ if (!render.src) continue;
2867
+ const ensured = await this.ensureSourceSize(render.src);
2868
+ const sourceSize = ensured || this.getSourceSize(render.src);
2869
+ const props = this.computeCanvasProps(render, sourceSize, frame);
2870
+ specs.push({
2871
+ id: item.id,
2872
+ type: "image",
2873
+ src: render.src,
2450
2874
  data: {
2451
2875
  id: item.id,
2452
2876
  layerId: IMAGE_OBJECT_LAYER_ID,
2453
2877
  type: "image-item"
2454
- }
2878
+ },
2879
+ props
2455
2880
  });
2456
- canvas.add(created);
2457
- obj = created;
2458
- }
2459
- this.rememberSourceSize(render.src, obj);
2460
- const sourceSize = this.getSourceSize(render.src, obj);
2461
- const props = this.computeCanvasProps(render, sourceSize, frame);
2462
- const screenProps = this.toScreenObjectProps(props);
2463
- obj.set({
2464
- ...screenProps,
2465
- data: {
2466
- ...obj.data || {},
2467
- id: item.id,
2468
- layerId: IMAGE_OBJECT_LAYER_ID,
2469
- type: "image-item"
2470
- }
2471
- });
2472
- this.applyImageControlVisibility(obj);
2473
- obj.setCoords();
2474
- const resolver = this.loadResolvers.get(item.id);
2475
- if (resolver) {
2476
- resolver();
2477
- this.loadResolvers.delete(item.id);
2478
- }
2479
- }
2480
- syncImageZOrder(items) {
2481
- if (!this.canvasService) return;
2482
- const canvas = this.canvasService.canvas;
2483
- const objects = canvas.getObjects();
2484
- let insertIndex = 0;
2485
- const backgroundLayer = this.canvasService.getLayer("background");
2486
- if (backgroundLayer) {
2487
- const bgIndex = objects.indexOf(backgroundLayer);
2488
- if (bgIndex >= 0) insertIndex = bgIndex + 1;
2489
- }
2490
- items.forEach((item) => {
2491
- const obj = this.getImageObject(item.id);
2492
- if (!obj) return;
2493
- canvas.moveObjectTo(obj, insertIndex);
2494
- insertIndex += 1;
2495
- });
2496
- const overlayObjects = this.getOverlayObjects().sort((a, b) => {
2497
- var _a, _b, _c, _d;
2498
- const az = Number((_b = (_a = a == null ? void 0 : a.data) == null ? void 0 : _a.zIndex) != null ? _b : 0);
2499
- const bz = Number((_d = (_c = b == null ? void 0 : b.data) == null ? void 0 : _c.zIndex) != null ? _d : 0);
2500
- return az - bz;
2501
- });
2502
- overlayObjects.forEach((obj) => {
2503
- canvas.bringObjectToFront(obj);
2504
- });
2505
- if (this.isDebugEnabled()) {
2506
- const stack = canvas.getObjects().map((obj, index) => {
2507
- var _a, _b, _c;
2508
- return {
2509
- index,
2510
- id: (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id,
2511
- layerId: (_b = obj == null ? void 0 : obj.data) == null ? void 0 : _b.layerId,
2512
- zIndex: (_c = obj == null ? void 0 : obj.data) == null ? void 0 : _c.zIndex
2513
- };
2514
- }).filter((item) => item.layerId === IMAGE_OVERLAY_LAYER_ID);
2515
- this.debug("overlay:stack", stack);
2516
2881
  }
2882
+ return specs;
2517
2883
  }
2518
2884
  buildOverlaySpecs(frame, sceneGeometry) {
2519
2885
  const visible = this.isImageEditingVisible();
@@ -2677,7 +3043,7 @@ var ImageTool = class {
2677
3043
  evented: false
2678
3044
  }
2679
3045
  };
2680
- const specs = [...mask, ...shapeOverlay, frameSpec];
3046
+ const specs = shapeOverlay.length > 0 ? [...mask, ...shapeOverlay] : [...mask, ...shapeOverlay, frameSpec];
2681
3047
  this.debug("overlay:built", {
2682
3048
  frame,
2683
3049
  shape: sceneGeometry == null ? void 0 : sceneGeometry.shape,
@@ -2707,30 +3073,33 @@ var ImageTool = class {
2707
3073
  skipRender: true
2708
3074
  });
2709
3075
  }
2710
- this.getImageObjects().forEach((obj) => {
2711
- var _a, _b;
2712
- const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
2713
- if (typeof id === "string" && !desiredIds.has(id)) {
2714
- (_b = this.canvasService) == null ? void 0 : _b.canvas.remove(obj);
2715
- }
2716
- });
2717
- for (const item of renderItems) {
2718
- if (seq !== this.renderSeq) return;
2719
- await this.upsertImageObject(item, frame, seq);
2720
- }
3076
+ const imageSpecs = await this.buildImageSpecs(renderItems, frame);
2721
3077
  if (seq !== this.renderSeq) return;
2722
- this.syncImageZOrder(renderItems);
2723
3078
  const sceneGeometry = await this.resolveSceneGeometryForOverlay();
2724
3079
  if (seq !== this.renderSeq) return;
2725
- const overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
2726
- this.overlaySpecs = overlaySpecs;
3080
+ this.imageSpecs = imageSpecs;
3081
+ this.overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
2727
3082
  await this.canvasService.flushRenderFromProducers();
2728
- this.syncImageZOrder(renderItems);
3083
+ if (seq !== this.renderSeq) return;
3084
+ this.refreshImageObjectInteractionState();
3085
+ renderItems.forEach((item) => {
3086
+ if (!this.getImageObject(item.id)) return;
3087
+ const resolver = this.loadResolvers.get(item.id);
3088
+ if (!resolver) return;
3089
+ resolver();
3090
+ this.loadResolvers.delete(item.id);
3091
+ });
3092
+ if (this.focusedImageId && this.isToolActive) {
3093
+ this.setImageFocus(this.focusedImageId, {
3094
+ syncCanvasSelection: true,
3095
+ skipRender: true
3096
+ });
3097
+ }
2729
3098
  const overlayCanvasCount = this.getOverlayObjects().length;
2730
3099
  this.debug("render:done", {
2731
3100
  seq,
2732
3101
  renderCount: renderItems.length,
2733
- overlayCount: overlaySpecs.length,
3102
+ overlayCount: this.overlaySpecs.length,
2734
3103
  overlayCanvasCount,
2735
3104
  isToolActive: this.isToolActive,
2736
3105
  isImageSelectionActive: this.isImageSelectionActive,
@@ -4376,11 +4745,11 @@ var DielineTool = class {
4376
4745
  style: "solid"
4377
4746
  },
4378
4747
  insideColor: "rgba(0,0,0,0)",
4379
- outsideColor: "#ffffff",
4380
4748
  showBleedLines: true,
4381
4749
  features: []
4382
4750
  };
4383
4751
  this.specs = [];
4752
+ this.effects = [];
4384
4753
  this.renderSeq = 0;
4385
4754
  this.onCanvasResized = () => {
4386
4755
  this.updateDieline();
@@ -4417,10 +4786,23 @@ var DielineTool = class {
4417
4786
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
4418
4787
  this.id,
4419
4788
  () => ({
4420
- layerSpecs: {
4421
- [DIELINE_LAYER_ID]: this.specs
4422
- },
4423
- replaceLayerIds: [DIELINE_LAYER_ID]
4789
+ passes: [
4790
+ {
4791
+ id: DIELINE_LAYER_ID,
4792
+ stack: 700,
4793
+ order: 0,
4794
+ replace: true,
4795
+ visibility: {
4796
+ op: "not",
4797
+ expr: {
4798
+ op: "activeToolIn",
4799
+ ids: ["pooder.kit.image", "pooder.kit.white-ink"]
4800
+ }
4801
+ },
4802
+ effects: this.effects,
4803
+ objects: this.specs
4804
+ }
4805
+ ]
4424
4806
  }),
4425
4807
  { priority: 250 }
4426
4808
  );
@@ -4476,10 +4858,6 @@ var DielineTool = class {
4476
4858
  s.offsetLine.style
4477
4859
  );
4478
4860
  s.insideColor = configService.get("dieline.insideColor", s.insideColor);
4479
- s.outsideColor = configService.get(
4480
- "dieline.outsideColor",
4481
- s.outsideColor
4482
- );
4483
4861
  s.showBleedLines = configService.get(
4484
4862
  "dieline.showBleedLines",
4485
4863
  s.showBleedLines
@@ -4542,9 +4920,6 @@ var DielineTool = class {
4542
4920
  case "dieline.insideColor":
4543
4921
  s.insideColor = e.value;
4544
4922
  break;
4545
- case "dieline.outsideColor":
4546
- s.outsideColor = e.value;
4547
- break;
4548
4923
  case "dieline.showBleedLines":
4549
4924
  s.showBleedLines = e.value;
4550
4925
  break;
@@ -4573,6 +4948,7 @@ var DielineTool = class {
4573
4948
  context.eventBus.off("canvas:resized", this.onCanvasResized);
4574
4949
  this.renderSeq += 1;
4575
4950
  this.specs = [];
4951
+ this.effects = [];
4576
4952
  (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
4577
4953
  this.renderProducerDisposable = void 0;
4578
4954
  if (this.canvasService) {
@@ -4689,12 +5065,6 @@ var DielineTool = class {
4689
5065
  label: "Inside Color",
4690
5066
  default: s.insideColor
4691
5067
  },
4692
- {
4693
- id: "dieline.outsideColor",
4694
- type: "color",
4695
- label: "Outside Color",
4696
- default: s.outsideColor
4697
- },
4698
5068
  {
4699
5069
  id: "dieline.features",
4700
5070
  type: "json",
@@ -4818,6 +5188,12 @@ var DielineTool = class {
4818
5188
  "ConfigurationService"
4819
5189
  );
4820
5190
  }
5191
+ hasImageItems() {
5192
+ const configService = this.getConfigService();
5193
+ if (!configService) return false;
5194
+ const items = configService.get("image.items", []);
5195
+ return Array.isArray(items) && items.length > 0;
5196
+ }
4821
5197
  syncSizeState(configService) {
4822
5198
  const sizeState = readSizeState(configService);
4823
5199
  this.state.width = sizeState.actualWidthMm;
@@ -4825,29 +5201,6 @@ var DielineTool = class {
4825
5201
  this.state.padding = sizeState.viewPadding;
4826
5202
  this.state.offset = sizeState.cutMode === "outset" ? sizeState.cutMarginMm : sizeState.cutMode === "inset" ? -sizeState.cutMarginMm : 0;
4827
5203
  }
4828
- bringFeatureMarkersToFront() {
4829
- if (!this.canvasService) return;
4830
- const canvas = this.canvasService.canvas;
4831
- canvas.getObjects().filter((obj) => {
4832
- var _a;
4833
- return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.type) === "feature-marker";
4834
- }).forEach((obj) => canvas.bringObjectToFront(obj));
4835
- }
4836
- ensureLayerStacking() {
4837
- if (!this.canvasService) return;
4838
- const layer = this.canvasService.getLayer(DIELINE_LAYER_ID);
4839
- if (!layer) return;
4840
- const userLayer = this.canvasService.getLayer("user");
4841
- if (userLayer) {
4842
- const layerIndex = this.canvasService.canvas.getObjects().indexOf(layer);
4843
- const userIndex = this.canvasService.canvas.getObjects().indexOf(userLayer);
4844
- if (layerIndex < userIndex) {
4845
- this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
4846
- }
4847
- return;
4848
- }
4849
- this.canvasService.canvas.bringObjectToFront(layer);
4850
- }
4851
5204
  buildDielineSpecs(sceneLayout) {
4852
5205
  var _a, _b;
4853
5206
  const {
@@ -4857,10 +5210,10 @@ var DielineTool = class {
4857
5210
  mainLine,
4858
5211
  offsetLine,
4859
5212
  insideColor,
4860
- outsideColor,
4861
5213
  showBleedLines,
4862
5214
  features
4863
5215
  } = this.state;
5216
+ const hasImages = this.hasImageItems();
4864
5217
  const canvasW = sceneLayout.canvasWidth || ((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 800;
4865
5218
  const canvasH = sceneLayout.canvasHeight || ((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 600;
4866
5219
  const scale = sceneLayout.scale;
@@ -4882,41 +5235,8 @@ var DielineTool = class {
4882
5235
  radius: (f.radius || 0) * scale
4883
5236
  }));
4884
5237
  const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
4885
- const maskPathData = generateMaskPath({
4886
- canvasWidth: canvasW,
4887
- canvasHeight: canvasH,
4888
- shape,
4889
- width: cutW,
4890
- height: cutH,
4891
- radius: cutR,
4892
- x: cx,
4893
- y: cy,
4894
- features: cutFeatures,
4895
- shapeStyle,
4896
- pathData: this.state.pathData,
4897
- customSourceWidthPx: this.state.customSourceWidthPx,
4898
- customSourceHeightPx: this.state.customSourceHeightPx
4899
- });
4900
- const specs = [
4901
- {
4902
- id: "dieline.mask",
4903
- type: "path",
4904
- space: "screen",
4905
- data: { id: "dieline.mask", type: "dieline" },
4906
- props: {
4907
- pathData: maskPathData,
4908
- fill: outsideColor,
4909
- stroke: null,
4910
- selectable: false,
4911
- evented: false,
4912
- originX: "left",
4913
- originY: "top",
4914
- left: 0,
4915
- top: 0
4916
- }
4917
- }
4918
- ];
4919
- if (insideColor && insideColor !== "transparent" && insideColor !== "rgba(0,0,0,0)") {
5238
+ const specs = [];
5239
+ if (insideColor && insideColor !== "transparent" && insideColor !== "rgba(0,0,0,0)" && !hasImages) {
4920
5240
  const productPathData = generateDielinePath({
4921
5241
  shape,
4922
5242
  width: cutW,
@@ -5070,6 +5390,77 @@ var DielineTool = class {
5070
5390
  });
5071
5391
  return specs;
5072
5392
  }
5393
+ buildImageClipEffects(sceneLayout) {
5394
+ var _a, _b;
5395
+ const { shape, shapeStyle, radius, features } = this.state;
5396
+ const canvasW = sceneLayout.canvasWidth || ((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 800;
5397
+ const canvasH = sceneLayout.canvasHeight || ((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 600;
5398
+ const scale = sceneLayout.scale;
5399
+ const cx = sceneLayout.trimRect.centerX;
5400
+ const cy = sceneLayout.trimRect.centerY;
5401
+ const visualWidth = sceneLayout.trimRect.width;
5402
+ const visualRadius = radius * scale;
5403
+ const cutW = sceneLayout.cutRect.width;
5404
+ const cutH = sceneLayout.cutRect.height;
5405
+ const visualOffset = (cutW - visualWidth) / 2;
5406
+ const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
5407
+ const absoluteFeatures = (features || []).map((f) => ({
5408
+ ...f,
5409
+ x: f.x,
5410
+ y: f.y,
5411
+ width: (f.width || 0) * scale,
5412
+ height: (f.height || 0) * scale,
5413
+ radius: (f.radius || 0) * scale
5414
+ }));
5415
+ const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
5416
+ const clipPathData = generateDielinePath({
5417
+ shape,
5418
+ width: cutW,
5419
+ height: cutH,
5420
+ radius: cutR,
5421
+ x: cx,
5422
+ y: cy,
5423
+ features: cutFeatures,
5424
+ shapeStyle,
5425
+ pathData: this.state.pathData,
5426
+ customSourceWidthPx: this.state.customSourceWidthPx,
5427
+ customSourceHeightPx: this.state.customSourceHeightPx,
5428
+ canvasWidth: canvasW,
5429
+ canvasHeight: canvasH
5430
+ });
5431
+ if (!clipPathData) return [];
5432
+ return [
5433
+ {
5434
+ type: "clipPath",
5435
+ id: "dieline.clip.image",
5436
+ visibility: {
5437
+ op: "not",
5438
+ expr: { op: "anySessionActive" }
5439
+ },
5440
+ targetPassIds: [IMAGE_OBJECT_LAYER_ID2],
5441
+ source: {
5442
+ id: "dieline.effect.clip-path",
5443
+ type: "path",
5444
+ space: "screen",
5445
+ data: {
5446
+ id: "dieline.effect.clip-path",
5447
+ type: "dieline-effect",
5448
+ effect: "clipPath"
5449
+ },
5450
+ props: {
5451
+ pathData: clipPathData,
5452
+ fill: "#000000",
5453
+ stroke: null,
5454
+ originX: "left",
5455
+ originY: "top",
5456
+ selectable: false,
5457
+ evented: false,
5458
+ excludeFromExport: true
5459
+ }
5460
+ }
5461
+ }
5462
+ ];
5463
+ }
5073
5464
  updateDieline(_emitEvent = true) {
5074
5465
  void this.updateDielineAsync();
5075
5466
  }
@@ -5086,17 +5477,17 @@ var DielineTool = class {
5086
5477
  if (!sceneLayout) {
5087
5478
  if (seq !== this.renderSeq) return;
5088
5479
  this.specs = [];
5480
+ this.effects = [];
5089
5481
  await this.canvasService.flushRenderFromProducers();
5090
5482
  return;
5091
5483
  }
5092
5484
  const nextSpecs = this.buildDielineSpecs(sceneLayout);
5485
+ const nextEffects = this.buildImageClipEffects(sceneLayout);
5093
5486
  if (seq !== this.renderSeq) return;
5094
5487
  this.specs = nextSpecs;
5488
+ this.effects = nextEffects;
5095
5489
  await this.canvasService.flushRenderFromProducers();
5096
5490
  if (seq !== this.renderSeq) return;
5097
- this.ensureLayerStacking();
5098
- this.bringFeatureMarkersToFront();
5099
- this.canvasService.bringLayerToFront("ruler-overlay");
5100
5491
  this.canvasService.requestRenderAll();
5101
5492
  }
5102
5493
  getGeometry() {
@@ -5529,9 +5920,14 @@ var FeatureTool = class {
5529
5920
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
5530
5921
  this.id,
5531
5922
  () => ({
5532
- rootLayerSpecs: {
5533
- [FEATURE_OVERLAY_LAYER_ID]: this.specs
5534
- }
5923
+ passes: [
5924
+ {
5925
+ id: FEATURE_OVERLAY_LAYER_ID,
5926
+ stack: 880,
5927
+ order: 0,
5928
+ objects: this.specs
5929
+ }
5930
+ ]
5535
5931
  }),
5536
5932
  { priority: 350 }
5537
5933
  );
@@ -5674,10 +6070,10 @@ var FeatureTool = class {
5674
6070
  await this.refreshGeometry();
5675
6071
  this.setWorkingFeatures(original);
5676
6072
  this.hasWorkingChanges = false;
6073
+ this.clearFeatureSessionState();
5677
6074
  this.redraw();
5678
6075
  this.emitWorkingChange();
5679
6076
  this.updateCommittedFeatures(original);
5680
- this.clearFeatureSessionState();
5681
6077
  return { ok: true };
5682
6078
  }
5683
6079
  },
@@ -5993,6 +6389,7 @@ var FeatureTool = class {
5993
6389
  }
5994
6390
  getDraggableMarkerTarget(target) {
5995
6391
  var _a, _b;
6392
+ if (!this.isFeatureSessionActive || !this.isToolActive) return null;
5996
6393
  if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return null;
5997
6394
  if (((_b = target.data) == null ? void 0 : _b.markerRole) !== "handle") return null;
5998
6395
  return target;
@@ -6115,18 +6512,12 @@ var FeatureTool = class {
6115
6512
  if (seq !== this.renderSeq) return;
6116
6513
  await this.canvasService.flushRenderFromProducers();
6117
6514
  if (seq !== this.renderSeq) return;
6118
- this.syncOverlayOrder();
6119
6515
  if (options.enforceConstraints) {
6120
6516
  this.enforceConstraints();
6121
6517
  }
6122
6518
  }
6123
- syncOverlayOrder() {
6124
- if (!this.canvasService) return;
6125
- this.canvasService.bringLayerToFront(FEATURE_OVERLAY_LAYER_ID);
6126
- this.canvasService.bringLayerToFront("ruler-overlay");
6127
- }
6128
6519
  buildFeatureSpecs() {
6129
- if (!this.currentGeometry || this.workingFeatures.length === 0) {
6520
+ if (!this.isFeatureSessionActive || !this.currentGeometry || this.workingFeatures.length === 0) {
6130
6521
  return [];
6131
6522
  }
6132
6523
  const groups = /* @__PURE__ */ new Map();
@@ -6196,11 +6587,12 @@ var FeatureTool = class {
6196
6587
  const color = feature.color || (feature.operation === "add" ? "#00FF00" : "#FF0000");
6197
6588
  const strokeDash = feature.strokeDash || (feature.operation === "subtract" ? [4, 4] : void 0);
6198
6589
  const interactive = options.markerRole === "handle";
6590
+ const sessionVisible = this.isToolActive && this.isFeatureSessionActive;
6199
6591
  const baseData = this.buildMarkerData(marker, options);
6200
6592
  const commonProps = {
6201
- visible: this.isToolActive,
6202
- selectable: interactive && this.isToolActive,
6203
- evented: interactive && this.isToolActive,
6593
+ visible: sessionVisible,
6594
+ selectable: interactive && sessionVisible,
6595
+ evented: interactive && sessionVisible,
6204
6596
  hasControls: false,
6205
6597
  hasBorders: false,
6206
6598
  hoverCursor: interactive ? "move" : "default",
@@ -6265,7 +6657,7 @@ var FeatureTool = class {
6265
6657
  markerOffsetY: -visualHeight / 2
6266
6658
  },
6267
6659
  props: {
6268
- visible: this.isToolActive,
6660
+ visible: sessionVisible,
6269
6661
  selectable: false,
6270
6662
  evented: false,
6271
6663
  width: visualWidth,
@@ -6433,9 +6825,14 @@ var FilmTool = class {
6433
6825
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
6434
6826
  this.id,
6435
6827
  () => ({
6436
- layerSpecs: {
6437
- [FILM_LAYER_ID]: this.specs
6438
- }
6828
+ passes: [
6829
+ {
6830
+ id: FILM_LAYER_ID,
6831
+ stack: 1e3,
6832
+ order: 0,
6833
+ objects: this.specs
6834
+ }
6835
+ ]
6439
6836
  }),
6440
6837
  { priority: 500 }
6441
6838
  );
@@ -6609,7 +7006,6 @@ var FilmTool = class {
6609
7006
  this.specs = this.buildFilmSpecs(this.renderImageUrl, this.opacity);
6610
7007
  await this.canvasService.flushRenderFromProducers();
6611
7008
  if (seq !== this.renderSeq) return;
6612
- this.canvasService.bringLayerToFront(FILM_LAYER_ID);
6613
7009
  this.canvasService.requestRenderAll();
6614
7010
  }
6615
7011
  };
@@ -6756,10 +7152,22 @@ var RulerTool = class {
6756
7152
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
6757
7153
  this.id,
6758
7154
  () => ({
6759
- rootLayerSpecs: {
6760
- [RULER_LAYER_ID]: this.specs
6761
- },
6762
- replaceRootLayerIds: [RULER_LAYER_ID]
7155
+ passes: [
7156
+ {
7157
+ id: RULER_LAYER_ID,
7158
+ stack: 950,
7159
+ order: 0,
7160
+ replace: true,
7161
+ visibility: {
7162
+ op: "not",
7163
+ expr: {
7164
+ op: "activeToolIn",
7165
+ ids: ["pooder.kit.white-ink"]
7166
+ }
7167
+ },
7168
+ objects: this.specs
7169
+ }
7170
+ ]
6763
7171
  }),
6764
7172
  { priority: 400 }
6765
7173
  );
@@ -7251,7 +7659,6 @@ var RulerTool = class {
7251
7659
  this.specs = specs;
7252
7660
  await this.canvasService.flushRenderFromProducers();
7253
7661
  if (seq !== this.renderSeq) return;
7254
- this.canvasService.bringLayerToFront(RULER_LAYER_ID);
7255
7662
  this.canvasService.requestRenderAll();
7256
7663
  this.log("render:done", { seq });
7257
7664
  }
@@ -7263,7 +7670,6 @@ var WHITE_INK_OBJECT_LAYER_ID = "white-ink.user";
7263
7670
  var WHITE_INK_COVER_LAYER_ID = "white-ink.cover";
7264
7671
  var WHITE_INK_OVERLAY_LAYER_ID = "white-ink.overlay";
7265
7672
  var IMAGE_OBJECT_LAYER_ID3 = "image.user";
7266
- var IMAGE_OVERLAY_LAYER_ID2 = "image-overlay";
7267
7673
  var WHITE_INK_DEBUG_KEY = "whiteInk.debug";
7268
7674
  var WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY = "whiteInk.previewImageVisible";
7269
7675
  var WHITE_INK_DEFAULT_OPACITY = 0.85;
@@ -7341,11 +7747,26 @@ var WhiteInkTool = class {
7341
7747
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
7342
7748
  this.id,
7343
7749
  () => ({
7344
- rootLayerSpecs: {
7345
- [WHITE_INK_OBJECT_LAYER_ID]: this.whiteSpecs,
7346
- [WHITE_INK_COVER_LAYER_ID]: this.coverSpecs,
7347
- [WHITE_INK_OVERLAY_LAYER_ID]: this.overlaySpecs
7348
- }
7750
+ passes: [
7751
+ {
7752
+ id: WHITE_INK_COVER_LAYER_ID,
7753
+ stack: 220,
7754
+ order: 0,
7755
+ objects: this.coverSpecs
7756
+ },
7757
+ {
7758
+ id: WHITE_INK_OBJECT_LAYER_ID,
7759
+ stack: 221,
7760
+ order: 0,
7761
+ objects: this.whiteSpecs
7762
+ },
7763
+ {
7764
+ id: WHITE_INK_OVERLAY_LAYER_ID,
7765
+ stack: 790,
7766
+ order: 0,
7767
+ objects: this.overlaySpecs
7768
+ }
7769
+ ]
7349
7770
  }),
7350
7771
  { priority: 260 }
7351
7772
  );
@@ -7424,7 +7845,6 @@ var WhiteInkTool = class {
7424
7845
  (_a = this.dirtyTrackerDisposable) == null ? void 0 : _a.dispose();
7425
7846
  this.dirtyTrackerDisposable = void 0;
7426
7847
  this.clearRenderedWhiteInks();
7427
- this.applyImageVisibilityForWhiteInk(false);
7428
7848
  (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
7429
7849
  this.renderProducerDisposable = void 0;
7430
7850
  if (this.canvasService) {
@@ -8268,22 +8688,6 @@ var WhiteInkTool = class {
8268
8688
  }
8269
8689
  ];
8270
8690
  }
8271
- applyImageVisibilityForWhiteInk(previewActive) {
8272
- if (!this.canvasService) return;
8273
- const visible = !previewActive;
8274
- let changed = false;
8275
- this.canvasService.canvas.getObjects().forEach((obj) => {
8276
- var _a, _b;
8277
- if (((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) !== IMAGE_OBJECT_LAYER_ID3) return;
8278
- if (obj.visible === visible) return;
8279
- obj.set({ visible });
8280
- (_b = obj.setCoords) == null ? void 0 : _b.call(obj);
8281
- changed = true;
8282
- });
8283
- if (changed) {
8284
- this.canvasService.requestRenderAll();
8285
- }
8286
- }
8287
8691
  resolveRenderItems() {
8288
8692
  if (this.isToolActive) {
8289
8693
  return this.cloneItems(this.workingItems);
@@ -8302,57 +8706,11 @@ var WhiteInkTool = class {
8302
8706
  ]);
8303
8707
  const scaleAdjust = this.computeWhiteScaleAdjust(imageSource, whiteSource);
8304
8708
  return {
8305
- whiteSrc: whiteMaskSrc || "",
8306
- coverSrc: coverMaskSrc || "",
8307
- whiteScaleAdjustX: scaleAdjust.x,
8308
- whiteScaleAdjustY: scaleAdjust.y
8309
- };
8310
- }
8311
- resolveDefaultInsertIndex(objects) {
8312
- if (!this.canvasService) return 0;
8313
- const backgroundLayer = this.canvasService.getLayer("background");
8314
- if (!backgroundLayer) return 0;
8315
- const bgIndex = objects.indexOf(backgroundLayer);
8316
- if (bgIndex < 0) return 0;
8317
- return bgIndex + 1;
8318
- }
8319
- syncZOrder() {
8320
- if (!this.canvasService) return;
8321
- const canvas = this.canvasService.canvas;
8322
- const whiteObjects = this.canvasService.getRootLayerObjects(
8323
- WHITE_INK_OBJECT_LAYER_ID
8324
- );
8325
- const coverObjects = this.canvasService.getRootLayerObjects(
8326
- WHITE_INK_COVER_LAYER_ID
8327
- );
8328
- const frameObjects = this.canvasService.getRootLayerObjects(
8329
- WHITE_INK_OVERLAY_LAYER_ID
8330
- );
8331
- const currentObjects = canvas.getObjects();
8332
- const imageIndexes = currentObjects.map(
8333
- (obj, index) => {
8334
- var _a;
8335
- return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === IMAGE_OBJECT_LAYER_ID3 ? index : -1;
8336
- }
8337
- ).filter((index) => index >= 0);
8338
- let whiteInsertIndex = imageIndexes.length ? Math.min(...imageIndexes) : this.resolveDefaultInsertIndex(currentObjects);
8339
- let coverInsertIndex = whiteInsertIndex;
8340
- coverObjects.forEach((obj) => {
8341
- canvas.moveObjectTo(obj, coverInsertIndex);
8342
- coverInsertIndex += 1;
8343
- });
8344
- whiteInsertIndex = coverInsertIndex;
8345
- whiteObjects.forEach((obj) => {
8346
- canvas.moveObjectTo(obj, whiteInsertIndex);
8347
- whiteInsertIndex += 1;
8348
- });
8349
- frameObjects.forEach((obj) => canvas.bringObjectToFront(obj));
8350
- canvas.getObjects().filter((obj) => {
8351
- var _a;
8352
- return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === IMAGE_OVERLAY_LAYER_ID2;
8353
- }).forEach((obj) => canvas.bringObjectToFront(obj));
8354
- this.canvasService.bringLayerToFront("dieline-overlay");
8355
- this.canvasService.bringLayerToFront("ruler-overlay");
8709
+ whiteSrc: whiteMaskSrc || "",
8710
+ coverSrc: coverMaskSrc || "",
8711
+ whiteScaleAdjustX: scaleAdjust.x,
8712
+ whiteScaleAdjustY: scaleAdjust.y
8713
+ };
8356
8714
  }
8357
8715
  clearRenderedWhiteInks() {
8358
8716
  if (!this.canvasService) return;
@@ -8385,7 +8743,6 @@ var WhiteInkTool = class {
8385
8743
  this.syncToolActiveFromWorkbench();
8386
8744
  const seq = ++this.renderSeq;
8387
8745
  const previewActive = this.isPreviewActive();
8388
- this.applyImageVisibilityForWhiteInk(previewActive);
8389
8746
  const frame = this.getFrameRect();
8390
8747
  const frameSpecs = this.buildFrameSpecs(frame);
8391
8748
  let whiteSpecs = [];
@@ -8433,7 +8790,6 @@ var WhiteInkTool = class {
8433
8790
  this.overlaySpecs = frameSpecs;
8434
8791
  await this.canvasService.flushRenderFromProducers();
8435
8792
  if (seq !== this.renderSeq) return;
8436
- this.syncZOrder();
8437
8793
  this.canvasService.requestRenderAll();
8438
8794
  }
8439
8795
  getMaskCacheKey(sourceUrl, tint) {
@@ -8615,62 +8971,9 @@ var SceneLayoutService = class {
8615
8971
  }
8616
8972
  };
8617
8973
 
8618
- // src/extensions/sceneVisibility.ts
8619
- var import_core11 = require("@pooder/core");
8620
- var CANVAS_SERVICE_ID2 = "CanvasService";
8621
- var HIDDEN_DIELINE_TOOLS = /* @__PURE__ */ new Set(["pooder.kit.image", "pooder.kit.white-ink"]);
8622
- var HIDDEN_RULER_TOOLS = /* @__PURE__ */ new Set(["pooder.kit.white-ink"]);
8623
- var SceneVisibilityService = class {
8624
- constructor() {
8625
- this.activeToolId = null;
8626
- this.onToolActivated = (e) => {
8627
- this.activeToolId = e.id;
8628
- this.apply();
8629
- };
8630
- this.onObjectAdded = () => {
8631
- this.apply();
8632
- };
8633
- }
8634
- init(context) {
8635
- var _a, _b;
8636
- if (this.context) {
8637
- this.dispose(this.context);
8638
- }
8639
- const canvasService = context.get(CANVAS_SERVICE_ID2);
8640
- if (!canvasService) {
8641
- throw new Error("[SceneVisibilityService] CanvasService is required.");
8642
- }
8643
- this.context = context;
8644
- this.canvasService = canvasService;
8645
- this.activeToolId = (_b = (_a = context.get(import_core11.WORKBENCH_SERVICE)) == null ? void 0 : _a.activeToolId) != null ? _b : null;
8646
- context.eventBus.on("tool:activated", this.onToolActivated);
8647
- context.eventBus.on("object:added", this.onObjectAdded);
8648
- this.apply();
8649
- }
8650
- dispose(context) {
8651
- var _a;
8652
- const activeContext = (_a = this.context) != null ? _a : context;
8653
- activeContext.eventBus.off("tool:activated", this.onToolActivated);
8654
- activeContext.eventBus.off("object:added", this.onObjectAdded);
8655
- this.context = void 0;
8656
- this.activeToolId = null;
8657
- this.canvasService = void 0;
8658
- }
8659
- apply() {
8660
- if (!this.canvasService) return;
8661
- const dielineLayer = this.canvasService.getLayer("dieline-overlay");
8662
- if (dielineLayer) {
8663
- const visible = !HIDDEN_DIELINE_TOOLS.has(this.activeToolId || "");
8664
- this.canvasService.setLayerVisibility("dieline-overlay", visible);
8665
- }
8666
- const rulerVisible = !HIDDEN_RULER_TOOLS.has(this.activeToolId || "");
8667
- this.canvasService.setLayerVisibility("ruler-overlay", rulerVisible);
8668
- this.canvasService.requestRenderAll();
8669
- }
8670
- };
8671
-
8672
8974
  // src/services/CanvasService.ts
8673
8975
  var import_fabric5 = require("fabric");
8976
+ var import_core11 = require("@pooder/core");
8674
8977
 
8675
8978
  // src/services/ViewportSystem.ts
8676
8979
  var ViewportSystem = class {
@@ -8745,6 +9048,56 @@ var ViewportSystem = class {
8745
9048
  }
8746
9049
  };
8747
9050
 
9051
+ // src/services/visibility.ts
9052
+ function compareLayerObjectCount(actual, cmp, expected) {
9053
+ if (cmp === ">") return actual > expected;
9054
+ if (cmp === ">=") return actual >= expected;
9055
+ if (cmp === "<") return actual < expected;
9056
+ if (cmp === "<=") return actual <= expected;
9057
+ return actual === expected;
9058
+ }
9059
+ function layerState(context, layerId) {
9060
+ return context.layers.get(layerId) || { exists: false, objectCount: 0 };
9061
+ }
9062
+ function evaluateVisibilityExpr(expr, context) {
9063
+ var _a;
9064
+ if (!expr) return true;
9065
+ if (expr.op === "const") {
9066
+ return Boolean(expr.value);
9067
+ }
9068
+ if (expr.op === "activeToolIn") {
9069
+ const activeToolId = (_a = context.activeToolId) != null ? _a : null;
9070
+ return !!activeToolId && expr.ids.includes(activeToolId);
9071
+ }
9072
+ if (expr.op === "sessionActive") {
9073
+ const toolId = String(expr.toolId || "").trim();
9074
+ if (!toolId) return false;
9075
+ return context.isSessionActive ? context.isSessionActive(toolId) : false;
9076
+ }
9077
+ if (expr.op === "anySessionActive") {
9078
+ return context.hasAnyActiveSession ? context.hasAnyActiveSession() : false;
9079
+ }
9080
+ if (expr.op === "layerExists") {
9081
+ return layerState(context, expr.layerId).exists === true;
9082
+ }
9083
+ if (expr.op === "layerObjectCount") {
9084
+ const expected = Number(expr.value);
9085
+ if (!Number.isFinite(expected)) return false;
9086
+ const count = layerState(context, expr.layerId).objectCount;
9087
+ return compareLayerObjectCount(count, expr.cmp, expected);
9088
+ }
9089
+ if (expr.op === "not") {
9090
+ return !evaluateVisibilityExpr(expr.expr, context);
9091
+ }
9092
+ if (expr.op === "all") {
9093
+ return expr.exprs.every((item) => evaluateVisibilityExpr(item, context));
9094
+ }
9095
+ if (expr.op === "any") {
9096
+ return expr.exprs.some((item) => evaluateVisibilityExpr(item, context));
9097
+ }
9098
+ return true;
9099
+ }
9100
+
8748
9101
  // src/services/CanvasService.ts
8749
9102
  var CanvasService = class {
8750
9103
  constructor(el, options) {
@@ -8753,8 +9106,48 @@ var CanvasService = class {
8753
9106
  this.producerFlushRequested = false;
8754
9107
  this.producerLoopPending = false;
8755
9108
  this.producerLoopPromise = null;
8756
- this.managedProducerLayerIds = /* @__PURE__ */ new Set();
8757
- this.managedProducerRootLayerIds = /* @__PURE__ */ new Set();
9109
+ this.producerApplyInProgress = false;
9110
+ this.visibilityRefreshScheduled = false;
9111
+ this.managedProducerPassIds = /* @__PURE__ */ new Set();
9112
+ this.managedPassMetas = /* @__PURE__ */ new Map();
9113
+ this.managedPassEffects = [];
9114
+ this.canvasForwardersBound = false;
9115
+ this.forwardSelectionCreated = (e) => {
9116
+ var _a;
9117
+ (_a = this.eventBus) == null ? void 0 : _a.emit("selection:created", e);
9118
+ };
9119
+ this.forwardSelectionUpdated = (e) => {
9120
+ var _a;
9121
+ (_a = this.eventBus) == null ? void 0 : _a.emit("selection:updated", e);
9122
+ };
9123
+ this.forwardSelectionCleared = (e) => {
9124
+ var _a;
9125
+ (_a = this.eventBus) == null ? void 0 : _a.emit("selection:cleared", e);
9126
+ };
9127
+ this.forwardObjectModified = (e) => {
9128
+ var _a;
9129
+ (_a = this.eventBus) == null ? void 0 : _a.emit("object:modified", e);
9130
+ };
9131
+ this.forwardObjectAdded = (e) => {
9132
+ var _a;
9133
+ (_a = this.eventBus) == null ? void 0 : _a.emit("object:added", e);
9134
+ };
9135
+ this.forwardObjectRemoved = (e) => {
9136
+ var _a;
9137
+ (_a = this.eventBus) == null ? void 0 : _a.emit("object:removed", e);
9138
+ };
9139
+ this.onToolActivated = () => {
9140
+ this.applyManagedPassVisibility();
9141
+ void this.applyManagedPassEffects(void 0, { render: true });
9142
+ };
9143
+ this.onToolSessionChanged = () => {
9144
+ this.applyManagedPassVisibility();
9145
+ void this.applyManagedPassEffects(void 0, { render: true });
9146
+ };
9147
+ this.onCanvasObjectChanged = () => {
9148
+ if (this.producerApplyInProgress) return;
9149
+ this.scheduleManagedPassVisibilityRefresh();
9150
+ };
8758
9151
  if (el instanceof import_fabric5.Canvas) {
8759
9152
  this.canvas = el;
8760
9153
  } else {
@@ -8771,25 +9164,53 @@ var CanvasService = class {
8771
9164
  this.setEventBus(options.eventBus);
8772
9165
  }
8773
9166
  }
9167
+ init(context) {
9168
+ if (this.context) {
9169
+ this.detachContextEvents(this.context.eventBus);
9170
+ }
9171
+ this.context = context;
9172
+ this.workbenchService = context.get(import_core11.WORKBENCH_SERVICE);
9173
+ this.toolSessionService = context.get(import_core11.TOOL_SESSION_SERVICE);
9174
+ this.setEventBus(context.eventBus);
9175
+ this.attachContextEvents(context.eventBus);
9176
+ }
9177
+ attachContextEvents(eventBus) {
9178
+ eventBus.on("tool:activated", this.onToolActivated);
9179
+ eventBus.on("tool:session:change", this.onToolSessionChanged);
9180
+ eventBus.on("object:added", this.onCanvasObjectChanged);
9181
+ eventBus.on("object:removed", this.onCanvasObjectChanged);
9182
+ }
9183
+ detachContextEvents(eventBus) {
9184
+ eventBus.off("tool:activated", this.onToolActivated);
9185
+ eventBus.off("tool:session:change", this.onToolSessionChanged);
9186
+ eventBus.off("object:added", this.onCanvasObjectChanged);
9187
+ eventBus.off("object:removed", this.onCanvasObjectChanged);
9188
+ }
8774
9189
  setEventBus(eventBus) {
8775
9190
  this.eventBus = eventBus;
8776
9191
  this.setupEvents();
8777
9192
  }
8778
9193
  setupEvents() {
8779
- if (!this.eventBus) return;
8780
- const bus = this.eventBus;
8781
- const forward = (name) => (e) => bus.emit(name, e);
8782
- this.canvas.on("selection:created", forward("selection:created"));
8783
- this.canvas.on("selection:updated", forward("selection:updated"));
8784
- this.canvas.on("selection:cleared", forward("selection:cleared"));
8785
- this.canvas.on("object:modified", forward("object:modified"));
8786
- this.canvas.on("object:added", forward("object:added"));
8787
- this.canvas.on("object:removed", forward("object:removed"));
9194
+ if (this.canvasForwardersBound) return;
9195
+ this.canvas.on("selection:created", this.forwardSelectionCreated);
9196
+ this.canvas.on("selection:updated", this.forwardSelectionUpdated);
9197
+ this.canvas.on("selection:cleared", this.forwardSelectionCleared);
9198
+ this.canvas.on("object:modified", this.forwardObjectModified);
9199
+ this.canvas.on("object:added", this.forwardObjectAdded);
9200
+ this.canvas.on("object:removed", this.forwardObjectRemoved);
9201
+ this.canvasForwardersBound = true;
8788
9202
  }
8789
9203
  dispose() {
9204
+ if (this.context) {
9205
+ this.detachContextEvents(this.context.eventBus);
9206
+ }
8790
9207
  this.renderProducers.clear();
8791
- this.managedProducerLayerIds.clear();
8792
- this.managedProducerRootLayerIds.clear();
9208
+ this.managedProducerPassIds.clear();
9209
+ this.managedPassMetas.clear();
9210
+ this.managedPassEffects = [];
9211
+ this.context = void 0;
9212
+ this.workbenchService = void 0;
9213
+ this.toolSessionService = void 0;
8793
9214
  this.producerFlushRequested = false;
8794
9215
  this.canvas.dispose();
8795
9216
  }
@@ -8867,118 +9288,292 @@ var CanvasService = class {
8867
9288
  return a.toolId.localeCompare(b.toolId);
8868
9289
  });
8869
9290
  }
8870
- appendLayerSpecMap(map, source) {
8871
- if (!source) return;
8872
- Object.entries(source).forEach(([layerId, specs]) => {
8873
- if (!Array.isArray(specs)) return;
8874
- const list = map.get(layerId) || [];
8875
- list.push(...specs);
8876
- map.set(layerId, list);
9291
+ normalizePassSpecValue(spec) {
9292
+ const id = String(spec.id || "").trim();
9293
+ if (!id) return null;
9294
+ return {
9295
+ id,
9296
+ stack: Number.isFinite(spec.stack) ? Number(spec.stack) : 0,
9297
+ order: Number.isFinite(spec.order) ? Number(spec.order) : 0,
9298
+ replace: spec.replace !== false,
9299
+ visibility: spec.visibility,
9300
+ effects: Array.isArray(spec.effects) ? [...spec.effects] : [],
9301
+ objects: Array.isArray(spec.objects) ? [...spec.objects] : []
9302
+ };
9303
+ }
9304
+ normalizeClipPathEffectSpec(effect, passId, index) {
9305
+ if (!effect || effect.type !== "clipPath") return null;
9306
+ const source = effect.source;
9307
+ if (!source || typeof source !== "object") return null;
9308
+ const sourceId = String(source.id || "").trim();
9309
+ if (!sourceId) return null;
9310
+ const targetPassIds = Array.isArray(effect.targetPassIds) ? effect.targetPassIds.map((item) => String(item || "").trim()).filter((item) => item.length > 0) : [];
9311
+ if (!targetPassIds.length) return null;
9312
+ const customId = String(effect.id || "").trim();
9313
+ const key = customId || `${passId}.effect.clipPath.${index}`;
9314
+ return {
9315
+ type: "clipPath",
9316
+ key,
9317
+ visibility: effect.visibility,
9318
+ source: {
9319
+ ...source,
9320
+ id: sourceId
9321
+ },
9322
+ targetPassIds
9323
+ };
9324
+ }
9325
+ mergePassSpec(map, rawSpec, producerId) {
9326
+ const normalized = this.normalizePassSpecValue(rawSpec);
9327
+ if (!normalized) return;
9328
+ const existing = map.get(normalized.id);
9329
+ if (!existing) {
9330
+ map.set(normalized.id, normalized);
9331
+ return;
9332
+ }
9333
+ existing.objects.push(...normalized.objects);
9334
+ existing.replace = existing.replace || normalized.replace;
9335
+ existing.stack = normalized.stack;
9336
+ existing.order = normalized.order;
9337
+ if (normalized.visibility !== void 0) {
9338
+ existing.visibility = normalized.visibility;
9339
+ }
9340
+ existing.effects.push(...normalized.effects);
9341
+ if (normalized.objects.length === 0 && normalized.effects.length === 0) {
9342
+ console.debug(
9343
+ `[CanvasService] pass "${normalized.id}" from producer "${producerId}" updated ordering/visibility only.`
9344
+ );
9345
+ }
9346
+ }
9347
+ comparePassMeta(a, b) {
9348
+ if (a.stack !== b.stack) return a.stack - b.stack;
9349
+ if (a.order !== b.order) return a.order - b.order;
9350
+ return a.id.localeCompare(b.id);
9351
+ }
9352
+ getPassObjectOrder(obj) {
9353
+ var _a;
9354
+ const raw = Number((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.passOrder);
9355
+ return Number.isFinite(raw) ? raw : Number.MAX_SAFE_INTEGER;
9356
+ }
9357
+ getPassCanvasObjects(passId) {
9358
+ const all = this.canvas.getObjects();
9359
+ return all.filter((obj) => {
9360
+ var _a;
9361
+ return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.passId) === passId;
9362
+ }).sort((a, b) => {
9363
+ const orderA = this.getPassObjectOrder(a);
9364
+ const orderB = this.getPassObjectOrder(b);
9365
+ if (orderA !== orderB) return orderA - orderB;
9366
+ return all.indexOf(a) - all.indexOf(b);
9367
+ });
9368
+ }
9369
+ getPassObjects(passId) {
9370
+ return this.getPassCanvasObjects(passId);
9371
+ }
9372
+ getRootLayerObjects(layerId) {
9373
+ return this.getPassCanvasObjects(layerId);
9374
+ }
9375
+ isManagedPassObject(obj) {
9376
+ var _a;
9377
+ const passId = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.passId;
9378
+ return typeof passId === "string" && this.managedPassMetas.has(passId);
9379
+ }
9380
+ syncManagedPassStacking(passes) {
9381
+ const orderedPasses = [...passes].sort((a, b) => this.comparePassMeta(a, b));
9382
+ if (!orderedPasses.length) return;
9383
+ const canvasObjects = this.canvas.getObjects();
9384
+ const managedObjects = canvasObjects.filter(
9385
+ (obj) => this.isManagedPassObject(obj)
9386
+ );
9387
+ if (!managedObjects.length) return;
9388
+ const firstManagedIndex = managedObjects.map((obj) => canvasObjects.indexOf(obj)).filter((index) => index >= 0).reduce((min, value) => Math.min(min, value), Number.MAX_SAFE_INTEGER);
9389
+ let targetIndex = Number.isFinite(firstManagedIndex) ? firstManagedIndex : 0;
9390
+ orderedPasses.forEach((meta) => {
9391
+ const objects = this.getPassCanvasObjects(meta.id);
9392
+ objects.forEach((obj) => {
9393
+ this.moveObjectInCanvas(obj, targetIndex);
9394
+ targetIndex += 1;
9395
+ });
9396
+ });
9397
+ }
9398
+ getPassRuntimeState() {
9399
+ const state = /* @__PURE__ */ new Map();
9400
+ const ensure = (passId) => {
9401
+ const id = String(passId || "").trim();
9402
+ if (!id) return { exists: false, objectCount: 0 };
9403
+ let item = state.get(id);
9404
+ if (!item) {
9405
+ item = { exists: false, objectCount: 0 };
9406
+ state.set(id, item);
9407
+ }
9408
+ return item;
9409
+ };
9410
+ this.canvas.getObjects().forEach((obj) => {
9411
+ var _a;
9412
+ const passId = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.passId;
9413
+ if (typeof passId === "string") {
9414
+ const item = ensure(passId);
9415
+ item.exists = true;
9416
+ item.objectCount += 1;
9417
+ }
9418
+ });
9419
+ this.managedPassMetas.forEach((meta) => {
9420
+ const item = ensure(meta.id);
9421
+ item.exists = true;
9422
+ });
9423
+ return state;
9424
+ }
9425
+ isSessionActive(toolId) {
9426
+ if (!this.toolSessionService) return false;
9427
+ return this.toolSessionService.getState(toolId).status === "active";
9428
+ }
9429
+ hasAnyActiveSession() {
9430
+ var _a, _b;
9431
+ return (_b = (_a = this.toolSessionService) == null ? void 0 : _a.hasAnyActiveSession()) != null ? _b : false;
9432
+ }
9433
+ buildVisibilityEvalContext(layers) {
9434
+ var _a, _b;
9435
+ return {
9436
+ activeToolId: (_b = (_a = this.workbenchService) == null ? void 0 : _a.activeToolId) != null ? _b : null,
9437
+ isSessionActive: (toolId) => this.isSessionActive(toolId),
9438
+ hasAnyActiveSession: () => this.hasAnyActiveSession(),
9439
+ layers
9440
+ };
9441
+ }
9442
+ applyManagedPassVisibility(options = {}) {
9443
+ if (!this.managedPassMetas.size) return false;
9444
+ const layers = this.getPassRuntimeState();
9445
+ const context = this.buildVisibilityEvalContext(layers);
9446
+ let changed = false;
9447
+ this.managedPassMetas.forEach((meta) => {
9448
+ const visible = evaluateVisibilityExpr(meta.visibility, context);
9449
+ changed = this.setPassVisibility(meta.id, visible) || changed;
9450
+ });
9451
+ if (changed && options.render !== false) {
9452
+ this.requestRenderAll();
9453
+ }
9454
+ return changed;
9455
+ }
9456
+ scheduleManagedPassVisibilityRefresh() {
9457
+ if (this.visibilityRefreshScheduled) return;
9458
+ this.visibilityRefreshScheduled = true;
9459
+ void Promise.resolve().then(() => {
9460
+ this.visibilityRefreshScheduled = false;
9461
+ this.applyManagedPassVisibility();
8877
9462
  });
8878
9463
  }
8879
9464
  async collectAndApplyProducerSpecs() {
8880
- const groupLayerSpecs = /* @__PURE__ */ new Map();
8881
- const rootLayerSpecs = /* @__PURE__ */ new Map();
8882
- const replaceLayerIds = /* @__PURE__ */ new Set();
8883
- const replaceRootLayerIds = /* @__PURE__ */ new Set();
9465
+ const passes = /* @__PURE__ */ new Map();
8884
9466
  const entries = this.sortedRenderProducerEntries();
8885
- for (const entry of entries) {
8886
- try {
8887
- const result = await entry.producer();
8888
- if (!result) continue;
8889
- this.appendLayerSpecMap(groupLayerSpecs, result.layerSpecs);
8890
- this.appendLayerSpecMap(rootLayerSpecs, result.rootLayerSpecs);
8891
- if (Array.isArray(result.replaceLayerIds)) {
8892
- result.replaceLayerIds.forEach((layerId) => {
8893
- if (layerId) replaceLayerIds.add(layerId);
8894
- });
8895
- }
8896
- if (Array.isArray(result.replaceRootLayerIds)) {
8897
- result.replaceRootLayerIds.forEach((layerId) => {
8898
- if (layerId) replaceRootLayerIds.add(layerId);
8899
- });
9467
+ this.producerApplyInProgress = true;
9468
+ try {
9469
+ for (const entry of entries) {
9470
+ try {
9471
+ const result = await entry.producer();
9472
+ if (!result) continue;
9473
+ const specs = Array.isArray(result.passes) ? result.passes : [];
9474
+ specs.forEach((spec) => this.mergePassSpec(passes, spec, entry.toolId));
9475
+ } catch (error) {
9476
+ console.error(
9477
+ `[CanvasService] render producer "${entry.toolId}" failed.`,
9478
+ error
9479
+ );
8900
9480
  }
8901
- } catch (error) {
8902
- console.error(
8903
- `[CanvasService] render producer "${entry.toolId}" failed.`,
8904
- error
8905
- );
8906
9481
  }
8907
- }
8908
- const nextLayerIds = new Set(groupLayerSpecs.keys());
8909
- const nextRootLayerIds = new Set(rootLayerSpecs.keys());
8910
- for (const [layerId, specs] of groupLayerSpecs.entries()) {
8911
- if (replaceLayerIds.has(layerId)) {
8912
- const layer = this.getLayer(layerId);
8913
- if (layer) {
8914
- layer.getObjects().forEach((obj) => layer.remove(obj));
8915
- }
9482
+ const nextPassIds = /* @__PURE__ */ new Set();
9483
+ const nextManagedPassMetas = /* @__PURE__ */ new Map();
9484
+ const nextEffects = [];
9485
+ for (const pass of passes.values()) {
9486
+ nextPassIds.add(pass.id);
9487
+ nextManagedPassMetas.set(pass.id, {
9488
+ id: pass.id,
9489
+ stack: pass.stack,
9490
+ order: pass.order,
9491
+ visibility: pass.visibility
9492
+ });
9493
+ await this.applyObjectSpecsToPass(pass.id, pass.objects, {
9494
+ render: false,
9495
+ replace: pass.replace
9496
+ });
9497
+ pass.effects.forEach((effect, index) => {
9498
+ const normalized = this.normalizeClipPathEffectSpec(
9499
+ effect,
9500
+ pass.id,
9501
+ index
9502
+ );
9503
+ if (!normalized) return;
9504
+ nextEffects.push(normalized);
9505
+ });
8916
9506
  }
8917
- await this.applyObjectSpecsToLayer(layerId, specs, { render: false });
8918
- }
8919
- for (const layerId of this.managedProducerLayerIds) {
8920
- if (nextLayerIds.has(layerId)) continue;
8921
- const layer = this.getLayer(layerId);
8922
- if (!layer) continue;
8923
- await this.applyObjectSpecsToContainer(layer, [], { render: false });
8924
- }
8925
- for (const [layerId, specs] of rootLayerSpecs.entries()) {
8926
- if (replaceRootLayerIds.has(layerId)) {
8927
- const existing = this.getRootLayerObjects(layerId);
8928
- existing.forEach((obj) => this.canvas.remove(obj));
9507
+ for (const passId of this.managedProducerPassIds) {
9508
+ if (nextPassIds.has(passId)) continue;
9509
+ await this.applyObjectSpecsToPass(passId, [], {
9510
+ render: false,
9511
+ replace: true
9512
+ });
8929
9513
  }
8930
- await this.applyObjectSpecsToRootLayer(layerId, specs, { render: false });
8931
- }
8932
- for (const layerId of this.managedProducerRootLayerIds) {
8933
- if (nextRootLayerIds.has(layerId)) continue;
8934
- await this.applyObjectSpecsToRootLayer(layerId, [], { render: false });
9514
+ this.managedProducerPassIds = nextPassIds;
9515
+ this.managedPassMetas = nextManagedPassMetas;
9516
+ this.managedPassEffects = nextEffects;
9517
+ this.syncManagedPassStacking(Array.from(nextManagedPassMetas.values()));
9518
+ await this.applyManagedPassEffects(nextEffects, { render: false });
9519
+ this.applyManagedPassVisibility({ render: false });
9520
+ } finally {
9521
+ this.producerApplyInProgress = false;
8935
9522
  }
8936
- this.managedProducerLayerIds = nextLayerIds;
8937
- this.managedProducerRootLayerIds = nextRootLayerIds;
8938
9523
  this.requestRenderAll();
8939
9524
  }
8940
- /**
8941
- * Get a layer (Group) by its ID.
8942
- * We assume layers are Groups directly on the canvas with a data.id property.
8943
- */
8944
- getLayer(id) {
8945
- return this.canvas.getObjects().find((obj) => {
8946
- var _a;
8947
- return ((_a = obj.data) == null ? void 0 : _a.id) === id;
8948
- });
8949
- }
8950
- /**
8951
- * Create a layer (Group) with the given ID if it doesn't exist.
8952
- */
8953
- createLayer(id, options = {}) {
8954
- let layer = this.getLayer(id);
8955
- if (!layer) {
8956
- const defaultOptions = {
8957
- selectable: false,
8958
- evented: false,
8959
- ...options,
8960
- data: { ...options.data, id }
8961
- };
8962
- layer = new import_fabric5.Group([], defaultOptions);
8963
- this.canvas.add(layer);
8964
- }
8965
- return layer;
8966
- }
8967
- /**
8968
- * Find an object by ID, optionally within a specific layer.
8969
- */
8970
- getObject(id, layerId) {
8971
- if (layerId) {
8972
- const layer = this.getLayer(layerId);
8973
- if (!layer) return void 0;
8974
- return layer.getObjects().find((obj) => {
8975
- var _a;
8976
- return ((_a = obj.data) == null ? void 0 : _a.id) === id;
9525
+ async applyManagedPassEffects(effects = this.managedPassEffects, options = {}) {
9526
+ const effectTargetMap = /* @__PURE__ */ new Map();
9527
+ const layers = this.getPassRuntimeState();
9528
+ const visibilityContext = this.buildVisibilityEvalContext(layers);
9529
+ for (const effect of effects) {
9530
+ if (effect.type !== "clipPath") continue;
9531
+ if (!evaluateVisibilityExpr(effect.visibility, visibilityContext)) {
9532
+ continue;
9533
+ }
9534
+ effect.targetPassIds.forEach((targetPassId) => {
9535
+ this.getPassCanvasObjects(targetPassId).forEach((obj) => {
9536
+ effectTargetMap.set(obj, effect);
9537
+ });
8977
9538
  });
8978
9539
  }
9540
+ const managedObjects = this.canvas.getObjects().filter(
9541
+ (obj) => this.isManagedPassObject(obj)
9542
+ );
9543
+ const effectTemplateCache = /* @__PURE__ */ new Map();
9544
+ for (const obj of managedObjects) {
9545
+ const targetEffect = effectTargetMap.get(obj);
9546
+ if (!targetEffect) {
9547
+ this.clearClipPathEffectFromObject(obj);
9548
+ continue;
9549
+ }
9550
+ let template = effectTemplateCache.get(targetEffect.key);
9551
+ if (template === void 0) {
9552
+ template = await this.createClipPathTemplate(targetEffect);
9553
+ effectTemplateCache.set(targetEffect.key, template);
9554
+ }
9555
+ if (!template) {
9556
+ this.clearClipPathEffectFromObject(obj);
9557
+ continue;
9558
+ }
9559
+ await this.applyClipPathEffectToObject(
9560
+ obj,
9561
+ template,
9562
+ targetEffect.key
9563
+ );
9564
+ }
9565
+ if (options.render !== false) {
9566
+ this.requestRenderAll();
9567
+ }
9568
+ }
9569
+ getObject(id, passId) {
9570
+ const normalizedId = String(id || "").trim();
9571
+ if (!normalizedId) return void 0;
8979
9572
  return this.canvas.getObjects().find((obj) => {
8980
- var _a;
8981
- return ((_a = obj.data) == null ? void 0 : _a.id) === id;
9573
+ var _a, _b;
9574
+ if (((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id) !== normalizedId) return false;
9575
+ if (!passId) return true;
9576
+ return ((_b = obj == null ? void 0 : obj.data) == null ? void 0 : _b.passId) === passId;
8982
9577
  });
8983
9578
  }
8984
9579
  requestRenderAll() {
@@ -9164,114 +9759,187 @@ var CanvasService = class {
9164
9759
  next.top = objectTop + objectHeight * this.originFactor(originY);
9165
9760
  return next;
9166
9761
  }
9167
- setLayerVisibility(layerId, visible) {
9168
- const layer = this.getLayer(layerId);
9169
- if (layer) {
9170
- layer.set({ visible });
9171
- }
9172
- const objects = this.getRootLayerObjects(layerId);
9762
+ setPassVisibility(passId, visible) {
9763
+ const objects = this.getPassCanvasObjects(passId);
9764
+ let changed = false;
9173
9765
  objects.forEach((obj) => {
9174
9766
  var _a, _b;
9767
+ if (obj.visible === visible) return;
9175
9768
  (_a = obj.set) == null ? void 0 : _a.call(obj, { visible });
9176
9769
  (_b = obj.setCoords) == null ? void 0 : _b.call(obj);
9770
+ changed = true;
9177
9771
  });
9772
+ return changed;
9178
9773
  }
9179
- bringLayerToFront(layerId) {
9180
- const layer = this.getLayer(layerId);
9181
- if (layer) {
9182
- this.canvas.bringObjectToFront(layer);
9183
- }
9184
- const objects = this.getRootLayerObjects(layerId);
9185
- objects.forEach((obj) => this.canvas.bringObjectToFront(obj));
9774
+ setLayerVisibility(layerId, visible) {
9775
+ return this.setPassVisibility(layerId, visible);
9186
9776
  }
9187
- async applyLayerSpec(spec) {
9188
- const layer = this.createLayer(spec.id, spec.props || {});
9189
- await this.applyObjectSpecsToContainer(layer, spec.objects);
9777
+ bringPassToFront(passId) {
9778
+ const objects = this.getPassCanvasObjects(passId);
9779
+ objects.forEach((obj) => this.canvas.bringObjectToFront(obj));
9190
9780
  }
9191
- async applyObjectSpecsToLayer(layerId, objects, options = {}) {
9192
- const layer = this.createLayer(layerId, {});
9193
- await this.applyObjectSpecsToContainer(layer, objects, options);
9781
+ bringLayerToFront(layerId) {
9782
+ this.bringPassToFront(layerId);
9194
9783
  }
9195
- getRootLayerObjects(layerId) {
9196
- return this.canvas.getObjects().filter((obj) => {
9197
- var _a;
9198
- return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === layerId;
9784
+ async applyPassSpec(spec, options = {}) {
9785
+ await this.applyObjectSpecsToPass(spec.id, spec.objects, {
9786
+ render: options.render,
9787
+ replace: spec.replace !== false
9199
9788
  });
9200
9789
  }
9201
- async applyObjectSpecsToRootLayer(layerId, specs, options = {}) {
9202
- const desiredIds = new Set(specs.map((s) => s.id));
9203
- const existing = this.getRootLayerObjects(layerId);
9204
- existing.forEach((obj) => {
9205
- var _a;
9206
- const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
9207
- if (typeof id === "string" && !desiredIds.has(id)) {
9208
- this.canvas.remove(obj);
9209
- }
9790
+ async applyObjectSpecsToRootLayer(passId, specs, options = {}) {
9791
+ await this.applyObjectSpecsToPass(passId, specs, {
9792
+ render: options.render,
9793
+ replace: true
9210
9794
  });
9211
- const byId = /* @__PURE__ */ new Map();
9212
- this.getRootLayerObjects(layerId).forEach((obj) => {
9213
- var _a;
9214
- const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
9215
- if (typeof id === "string") byId.set(id, obj);
9795
+ }
9796
+ normalizeObjectSpecs(specs) {
9797
+ const seen = /* @__PURE__ */ new Set();
9798
+ const normalized = [];
9799
+ (specs || []).forEach((spec) => {
9800
+ const id = String((spec == null ? void 0 : spec.id) || "").trim();
9801
+ if (!id || seen.has(id)) return;
9802
+ seen.add(id);
9803
+ normalized.push({
9804
+ ...spec,
9805
+ id
9806
+ });
9216
9807
  });
9217
- for (let index = 0; index < specs.length; index += 1) {
9218
- const spec = specs[index];
9219
- let current = byId.get(spec.id);
9220
- if (current && spec.type === "image" && spec.src && current.getSrc && current.getSrc() !== spec.src) {
9221
- this.canvas.remove(current);
9222
- byId.delete(spec.id);
9223
- current = void 0;
9224
- }
9225
- if (!current) {
9226
- const created = await this.createFabricObject(spec);
9227
- if (!created) continue;
9228
- this.patchFabricObject(created, spec, { layerId });
9229
- this.canvas.add(created);
9230
- byId.set(spec.id, created);
9231
- continue;
9232
- }
9233
- this.patchFabricObject(current, spec, { layerId });
9808
+ return normalized;
9809
+ }
9810
+ async cloneFabricObject(source) {
9811
+ const clone = source.clone;
9812
+ if (typeof clone !== "function") return void 0;
9813
+ const result = clone.call(source);
9814
+ if (!result || typeof result.then !== "function") {
9815
+ return void 0;
9234
9816
  }
9235
- if (options.render !== false) {
9236
- this.requestRenderAll();
9817
+ try {
9818
+ const copied = await result;
9819
+ return copied;
9820
+ } catch (e) {
9821
+ return void 0;
9237
9822
  }
9238
9823
  }
9239
- async applyObjectSpecsToContainer(container, specs, options = {}) {
9240
- const desiredIds = new Set(specs.map((s) => s.id));
9241
- const existing = container.getObjects();
9242
- existing.forEach((obj) => {
9243
- var _a;
9244
- const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
9245
- if (typeof id === "string" && !desiredIds.has(id)) {
9246
- container.remove(obj);
9824
+ async createClipPathTemplate(effect) {
9825
+ var _a, _b;
9826
+ const source = effect.source;
9827
+ const sourceId = String(source.id || "").trim();
9828
+ if (!sourceId) return null;
9829
+ const template = await this.createFabricObject({
9830
+ ...source,
9831
+ id: sourceId,
9832
+ data: {
9833
+ ...source.data || {},
9834
+ id: sourceId,
9835
+ type: "clip-path-effect-template",
9836
+ effectKey: effect.key
9837
+ },
9838
+ props: {
9839
+ ...source.props || {},
9840
+ selectable: false,
9841
+ evented: false,
9842
+ excludeFromExport: true
9247
9843
  }
9248
9844
  });
9845
+ if (!template) return null;
9846
+ (_a = template.set) == null ? void 0 : _a.call(template, {
9847
+ selectable: false,
9848
+ evented: false,
9849
+ excludeFromExport: true,
9850
+ absolutePositioned: true
9851
+ });
9852
+ (_b = template.setCoords) == null ? void 0 : _b.call(template);
9853
+ return template;
9854
+ }
9855
+ isClipPathEffectManaged(target) {
9856
+ return typeof (target == null ? void 0 : target.__pooderEffectClipKey) === "string";
9857
+ }
9858
+ clearClipPathEffectFromObject(target) {
9859
+ var _a, _b;
9860
+ if (!target) return;
9861
+ if (!this.isClipPathEffectManaged(target)) return;
9862
+ (_a = target.set) == null ? void 0 : _a.call(target, { clipPath: void 0 });
9863
+ (_b = target.setCoords) == null ? void 0 : _b.call(target);
9864
+ delete target.__pooderEffectClipKey;
9865
+ }
9866
+ async applyClipPathEffectToObject(target, clipTemplate, effectKey) {
9867
+ var _a, _b, _c, _d;
9868
+ if (!target) return;
9869
+ const clipPath = await this.cloneFabricObject(clipTemplate);
9870
+ if (!clipPath) {
9871
+ this.clearClipPathEffectFromObject(target);
9872
+ return;
9873
+ }
9874
+ (_a = clipPath.set) == null ? void 0 : _a.call(clipPath, {
9875
+ selectable: false,
9876
+ evented: false,
9877
+ excludeFromExport: true,
9878
+ absolutePositioned: true
9879
+ });
9880
+ (_b = clipPath.setCoords) == null ? void 0 : _b.call(clipPath);
9881
+ (_c = target.set) == null ? void 0 : _c.call(target, { clipPath });
9882
+ (_d = target.setCoords) == null ? void 0 : _d.call(target);
9883
+ target.__pooderEffectClipKey = effectKey;
9884
+ }
9885
+ async applyObjectSpecsToPass(passId, specs, options = {}) {
9886
+ const normalizedPassId = String(passId || "").trim();
9887
+ if (!normalizedPassId) return;
9888
+ const replace = options.replace !== false;
9889
+ const normalizedSpecs = this.normalizeObjectSpecs(specs);
9890
+ const desiredIds = new Set(normalizedSpecs.map((s) => s.id));
9891
+ const existing = this.getPassCanvasObjects(normalizedPassId);
9892
+ if (replace) {
9893
+ existing.forEach((obj) => {
9894
+ var _a;
9895
+ const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
9896
+ if (typeof id === "string" && !desiredIds.has(id)) {
9897
+ this.canvas.remove(obj);
9898
+ }
9899
+ });
9900
+ }
9249
9901
  const byId = /* @__PURE__ */ new Map();
9250
- container.getObjects().forEach((obj) => {
9902
+ this.getPassCanvasObjects(normalizedPassId).forEach((obj) => {
9251
9903
  var _a;
9252
9904
  const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
9253
9905
  if (typeof id === "string") byId.set(id, obj);
9254
9906
  });
9255
- for (let index = 0; index < specs.length; index += 1) {
9256
- const spec = specs[index];
9907
+ for (let index = 0; index < normalizedSpecs.length; index += 1) {
9908
+ const spec = normalizedSpecs[index];
9257
9909
  let current = byId.get(spec.id);
9258
- if (current && spec.type === "image" && spec.src && current.getSrc && current.getSrc() !== spec.src) {
9259
- container.remove(current);
9910
+ if (spec.type === "path") {
9911
+ const nextPathData = this.readPathDataFromSpec(spec);
9912
+ if (!nextPathData || !nextPathData.trim()) {
9913
+ if (current) {
9914
+ this.canvas.remove(current);
9915
+ byId.delete(spec.id);
9916
+ }
9917
+ continue;
9918
+ }
9919
+ }
9920
+ if (current && this.shouldRecreateObject(current, spec)) {
9921
+ this.canvas.remove(current);
9260
9922
  byId.delete(spec.id);
9261
9923
  current = void 0;
9262
9924
  }
9263
9925
  if (!current) {
9264
9926
  const created = await this.createFabricObject(spec);
9265
9927
  if (!created) continue;
9266
- container.add(created);
9267
- current = created;
9268
- byId.set(spec.id, current);
9269
- } else {
9270
- this.patchFabricObject(current, spec);
9928
+ this.patchFabricObject(created, spec, {
9929
+ passId: normalizedPassId,
9930
+ layerId: normalizedPassId,
9931
+ passOrder: index
9932
+ });
9933
+ this.canvas.add(created);
9934
+ byId.set(spec.id, created);
9935
+ continue;
9271
9936
  }
9272
- this.moveObjectInContainer(container, current, index);
9937
+ this.patchFabricObject(current, spec, {
9938
+ passId: normalizedPassId,
9939
+ layerId: normalizedPassId,
9940
+ passOrder: index
9941
+ });
9273
9942
  }
9274
- container.dirty = true;
9275
9943
  if (options.render !== false) {
9276
9944
  this.requestRenderAll();
9277
9945
  }
@@ -9283,10 +9951,56 @@ var CanvasService = class {
9283
9951
  ...extraData || {},
9284
9952
  id: spec.id
9285
9953
  };
9954
+ nextData.__renderSourceKey = this.getSpecRenderSourceKey(spec);
9286
9955
  const props = this.resolveFabricProps(spec, spec.props || {});
9287
9956
  obj.set({ ...props, data: nextData });
9288
9957
  obj.setCoords();
9289
9958
  }
9959
+ readPathDataFromSpec(spec) {
9960
+ var _a, _b;
9961
+ if (spec.type !== "path") return void 0;
9962
+ const raw = ((_a = spec.props) == null ? void 0 : _a.path) || ((_b = spec.props) == null ? void 0 : _b.pathData);
9963
+ if (typeof raw !== "string") return void 0;
9964
+ return raw;
9965
+ }
9966
+ hashText(value) {
9967
+ let hash = 2166136261;
9968
+ for (let i = 0; i < value.length; i += 1) {
9969
+ hash ^= value.charCodeAt(i);
9970
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
9971
+ }
9972
+ return (hash >>> 0).toString(16);
9973
+ }
9974
+ getSpecRenderSourceKey(spec) {
9975
+ var _a, _b;
9976
+ switch (spec.type) {
9977
+ case "path": {
9978
+ const pathData = this.readPathDataFromSpec(spec) || "";
9979
+ return `path:${this.hashText(pathData)}`;
9980
+ }
9981
+ case "image":
9982
+ return `image:${String(spec.src || "")}`;
9983
+ case "text":
9984
+ return `text:${String((_b = (_a = spec.props) == null ? void 0 : _a.text) != null ? _b : "")}`;
9985
+ case "rect":
9986
+ return "rect";
9987
+ default:
9988
+ return String(spec.type || "");
9989
+ }
9990
+ }
9991
+ shouldRecreateObject(current, spec) {
9992
+ var _a;
9993
+ if (!current) return true;
9994
+ const currentType = String((current == null ? void 0 : current.type) || "").toLowerCase();
9995
+ if (currentType !== spec.type) return true;
9996
+ const expectedKey = this.getSpecRenderSourceKey(spec);
9997
+ const currentKey = String(((_a = current == null ? void 0 : current.data) == null ? void 0 : _a.__renderSourceKey) || "");
9998
+ if (currentKey && expectedKey && currentKey !== expectedKey) return true;
9999
+ if (spec.type === "image" && spec.src && current.getSrc) {
10000
+ return current.getSrc() !== spec.src;
10001
+ }
10002
+ return false;
10003
+ }
9290
10004
  resolveFabricProps(spec, props) {
9291
10005
  const space = spec.space || "scene";
9292
10006
  const next = this.resolveLayoutProps(spec, props);
@@ -9310,26 +10024,26 @@ var CanvasService = class {
9310
10024
  next.scaleY = rawScaleY * sceneScale;
9311
10025
  return next;
9312
10026
  }
9313
- moveObjectInContainer(container, obj, index) {
10027
+ moveObjectInCanvas(obj, index) {
9314
10028
  if (!obj) return;
9315
- const moveObjectTo = container.moveObjectTo;
10029
+ const moveObjectTo = this.canvas.moveObjectTo;
9316
10030
  if (typeof moveObjectTo === "function") {
9317
- moveObjectTo.call(container, obj, index);
10031
+ moveObjectTo.call(this.canvas, obj, index);
9318
10032
  return;
9319
10033
  }
9320
- const list = container._objects;
10034
+ const list = this.canvas._objects;
9321
10035
  if (!Array.isArray(list)) return;
9322
10036
  const from = list.indexOf(obj);
9323
10037
  if (from < 0 || from === index) return;
9324
10038
  list.splice(from, 1);
9325
10039
  const target = Math.max(0, Math.min(index, list.length));
9326
10040
  list.splice(target, 0, obj);
9327
- if (typeof container._onStackOrderChanged === "function") {
9328
- container._onStackOrderChanged();
10041
+ if (typeof this.canvas._onStackOrderChanged === "function") {
10042
+ this.canvas._onStackOrderChanged();
9329
10043
  }
9330
10044
  }
9331
10045
  async createFabricObject(spec) {
9332
- var _a, _b, _c, _d;
10046
+ var _a, _b;
9333
10047
  if (spec.type === "rect") {
9334
10048
  const props = this.resolveFabricProps(spec, spec.props || {});
9335
10049
  const rect = new import_fabric5.Rect({
@@ -9340,7 +10054,7 @@ var CanvasService = class {
9340
10054
  return rect;
9341
10055
  }
9342
10056
  if (spec.type === "path") {
9343
- const pathData = ((_a = spec.props) == null ? void 0 : _a.path) || ((_b = spec.props) == null ? void 0 : _b.pathData);
10057
+ const pathData = this.readPathDataFromSpec(spec);
9344
10058
  if (!pathData) return void 0;
9345
10059
  const props = this.resolveFabricProps(spec, spec.props || {});
9346
10060
  const path = new import_fabric5.Path(pathData, {
@@ -9362,7 +10076,7 @@ var CanvasService = class {
9362
10076
  return image;
9363
10077
  }
9364
10078
  if (spec.type === "text") {
9365
- const content = String((_d = (_c = spec.props) == null ? void 0 : _c.text) != null ? _d : "");
10079
+ const content = String((_b = (_a = spec.props) == null ? void 0 : _a.text) != null ? _b : "");
9366
10080
  const props = this.resolveFabricProps(spec, spec.props || {});
9367
10081
  const text = new import_fabric5.Text(content, {
9368
10082
  ...props,
@@ -9385,8 +10099,8 @@ var CanvasService = class {
9385
10099
  MirrorTool,
9386
10100
  RulerTool,
9387
10101
  SceneLayoutService,
9388
- SceneVisibilityService,
9389
10102
  SizeTool,
9390
10103
  ViewportSystem,
9391
- WhiteInkTool
10104
+ WhiteInkTool,
10105
+ evaluateVisibilityExpr
9392
10106
  });