@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.mjs CHANGED
@@ -3,291 +3,94 @@ import {
3
3
  ContributionPointIds
4
4
  } from "@pooder/core";
5
5
  import { FabricImage } from "fabric";
6
- var BACKGROUND_LAYER_ID = "background";
7
- var BACKGROUND_RECT_ID = "background-color-rect";
8
- var BACKGROUND_IMAGE_ID = "background-image";
9
- var DEFAULT_WIDTH = 800;
10
- var DEFAULT_HEIGHT = 600;
11
- var BackgroundTool = class {
12
- constructor(options) {
13
- this.id = "pooder.kit.background";
14
- this.metadata = {
15
- name: "BackgroundTool"
16
- };
17
- this.color = "";
18
- this.url = "";
19
- this.specs = [];
20
- this.renderSeq = 0;
21
- this.renderImageUrl = "";
22
- this.sourceSizeBySrc = /* @__PURE__ */ new Map();
23
- this.pendingSizeBySrc = /* @__PURE__ */ new Map();
24
- this.onCanvasResized = () => {
25
- this.updateBackground();
26
- };
27
- if (options) {
28
- Object.assign(this, options);
6
+
7
+ // src/coordinate.ts
8
+ var Coordinate = class {
9
+ /**
10
+ * Calculate layout to fit content within container while preserving aspect ratio.
11
+ */
12
+ static calculateLayout(container, content, padding = 0) {
13
+ const availableWidth = Math.max(0, container.width - padding * 2);
14
+ const availableHeight = Math.max(0, container.height - padding * 2);
15
+ if (content.width === 0 || content.height === 0) {
16
+ return { scale: 1, offsetX: 0, offsetY: 0, width: 0, height: 0 };
29
17
  }
18
+ const scaleX = availableWidth / content.width;
19
+ const scaleY = availableHeight / content.height;
20
+ const scale = Math.min(scaleX, scaleY);
21
+ const width = content.width * scale;
22
+ const height = content.height * scale;
23
+ const offsetX = (container.width - width) / 2;
24
+ const offsetY = (container.height - height) / 2;
25
+ return { scale, offsetX, offsetY, width, height };
30
26
  }
31
- activate(context) {
32
- var _a;
33
- this.canvasService = context.services.get("CanvasService");
34
- if (!this.canvasService) {
35
- console.warn("CanvasService not found for BackgroundTool");
36
- return;
37
- }
38
- (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
39
- this.renderProducerDisposable = this.canvasService.registerRenderProducer(
40
- this.id,
41
- () => ({
42
- layerSpecs: {
43
- [BACKGROUND_LAYER_ID]: this.specs
44
- }
45
- }),
46
- { priority: 0 }
47
- );
48
- const configService = context.services.get(
49
- "ConfigurationService"
50
- );
51
- if (configService) {
52
- this.color = configService.get("background.color", this.color);
53
- this.url = configService.get("background.url", this.url);
54
- configService.onAnyChange((e) => {
55
- if (e.key.startsWith("background.")) {
56
- const prop = e.key.split(".")[1];
57
- console.log(
58
- `[BackgroundTool] Config change detected: ${e.key} -> ${e.value}, prop: ${prop}`
59
- );
60
- if (prop && prop in this) {
61
- console.log(
62
- `[BackgroundTool] Updating option ${prop} to ${e.value}`
63
- );
64
- this[prop] = e.value;
65
- this.updateBackground();
66
- } else {
67
- console.warn(
68
- `[BackgroundTool] Property ${prop} not found in options`
69
- );
70
- }
71
- }
72
- });
73
- }
74
- context.eventBus.on("canvas:resized", this.onCanvasResized);
75
- this.updateBackground();
27
+ /**
28
+ * Convert an absolute value to a normalized value (0-1).
29
+ * @param value Absolute value (e.g., pixels)
30
+ * @param total Total dimension size (e.g., canvas width)
31
+ */
32
+ static toNormalized(value, total) {
33
+ return total === 0 ? 0 : value / total;
76
34
  }
77
- deactivate(context) {
78
- var _a;
79
- context.eventBus.off("canvas:resized", this.onCanvasResized);
80
- this.renderSeq += 1;
81
- this.specs = [];
82
- this.renderImageUrl = "";
83
- (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
84
- this.renderProducerDisposable = void 0;
85
- if (!this.canvasService) return;
86
- const layer = this.canvasService.getLayer(BACKGROUND_LAYER_ID);
87
- if (layer) {
88
- this.canvasService.canvas.remove(layer);
89
- }
90
- void this.canvasService.flushRenderFromProducers();
91
- this.canvasService.requestRenderAll();
92
- this.canvasService = void 0;
35
+ /**
36
+ * Convert a normalized value (0-1) to an absolute value.
37
+ * @param normalized Normalized value (0-1)
38
+ * @param total Total dimension size (e.g., canvas width)
39
+ */
40
+ static toAbsolute(normalized, total) {
41
+ return normalized * total;
93
42
  }
94
- contribute() {
43
+ /**
44
+ * Normalize a point's coordinates.
45
+ */
46
+ static normalizePoint(point, size) {
95
47
  return {
96
- [ContributionPointIds.CONFIGURATIONS]: [
97
- {
98
- id: "background.color",
99
- type: "color",
100
- label: "Background Color",
101
- default: ""
102
- },
103
- {
104
- id: "background.url",
105
- type: "string",
106
- label: "Image URL",
107
- default: ""
108
- }
109
- ],
110
- [ContributionPointIds.COMMANDS]: [
111
- {
112
- command: "reset",
113
- title: "Reset Background",
114
- handler: () => {
115
- this.updateBackground();
116
- return true;
117
- }
118
- },
119
- {
120
- command: "clear",
121
- title: "Clear Background",
122
- handler: () => {
123
- this.color = "transparent";
124
- this.url = "";
125
- this.updateBackground();
126
- return true;
127
- }
128
- },
129
- {
130
- command: "setBackgroundColor",
131
- title: "Set Background Color",
132
- handler: (color) => {
133
- if (this.color === color) return true;
134
- this.color = color;
135
- this.updateBackground();
136
- return true;
137
- }
138
- },
139
- {
140
- command: "setBackgroundImage",
141
- title: "Set Background Image",
142
- handler: (url) => {
143
- if (this.url === url) return true;
144
- this.url = url;
145
- this.updateBackground();
146
- return true;
147
- }
148
- }
149
- ]
48
+ x: this.toNormalized(point.x, size.width),
49
+ y: this.toNormalized(point.y, size.height)
150
50
  };
151
51
  }
152
- getViewportSize() {
153
- var _a, _b;
154
- const width = Number(((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 0);
155
- const height = Number(((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 0);
52
+ /**
53
+ * Denormalize a point's coordinates to absolute pixels.
54
+ */
55
+ static denormalizePoint(point, size) {
156
56
  return {
157
- width: width > 0 ? width : DEFAULT_WIDTH,
158
- height: height > 0 ? height : DEFAULT_HEIGHT
57
+ x: this.toAbsolute(point.x, size.width),
58
+ y: this.toAbsolute(point.y, size.height)
159
59
  };
160
60
  }
161
- buildBackgroundSpecs(color, imageUrl) {
162
- const { width, height } = this.getViewportSize();
163
- const specs = [
164
- {
165
- id: BACKGROUND_RECT_ID,
166
- type: "rect",
167
- space: "screen",
168
- data: {
169
- id: BACKGROUND_RECT_ID,
170
- layerId: BACKGROUND_LAYER_ID,
171
- type: "background-color"
172
- },
173
- props: {
174
- left: 0,
175
- top: 0,
176
- width,
177
- height,
178
- originX: "left",
179
- originY: "top",
180
- fill: color,
181
- selectable: false,
182
- evented: false,
183
- excludeFromExport: true
184
- }
185
- }
186
- ];
187
- if (!imageUrl) {
188
- return specs;
189
- }
190
- const sourceSize = this.sourceSizeBySrc.get(imageUrl);
191
- const sourceWidth = Math.max(1, Number((sourceSize == null ? void 0 : sourceSize.width) || width));
192
- const sourceHeight = Math.max(1, Number((sourceSize == null ? void 0 : sourceSize.height) || height));
193
- const coverScale = Math.max(width / sourceWidth, height / sourceHeight);
194
- specs.push({
195
- id: BACKGROUND_IMAGE_ID,
196
- type: "image",
197
- src: imageUrl,
198
- space: "screen",
199
- data: {
200
- id: BACKGROUND_IMAGE_ID,
201
- layerId: BACKGROUND_LAYER_ID,
202
- type: "background-image"
203
- },
204
- props: {
205
- left: 0,
206
- top: 0,
207
- originX: "left",
208
- originY: "top",
209
- scaleX: coverScale,
210
- scaleY: coverScale,
211
- selectable: false,
212
- evented: false,
213
- excludeFromExport: true
214
- }
215
- });
216
- return specs;
217
- }
218
- async ensureImageSize(src) {
219
- if (!src) return null;
220
- const cached = this.sourceSizeBySrc.get(src);
221
- if (cached) return cached;
222
- const pending = this.pendingSizeBySrc.get(src);
223
- if (pending) {
224
- return pending;
225
- }
226
- const task = this.loadImageSize(src);
227
- this.pendingSizeBySrc.set(src, task);
228
- try {
229
- return await task;
230
- } finally {
231
- if (this.pendingSizeBySrc.get(src) === task) {
232
- this.pendingSizeBySrc.delete(src);
233
- }
234
- }
235
- }
236
- async loadImageSize(src) {
237
- try {
238
- const image = await FabricImage.fromURL(src, {
239
- crossOrigin: "anonymous"
240
- });
241
- const width = Number((image == null ? void 0 : image.width) || 0);
242
- const height = Number((image == null ? void 0 : image.height) || 0);
243
- if (width > 0 && height > 0) {
244
- const size = { width, height };
245
- this.sourceSizeBySrc.set(src, size);
246
- return size;
247
- }
248
- } catch (error) {
249
- console.error("[BackgroundTool] Failed to load image", src, error);
61
+ static convertUnit(value, from, to) {
62
+ if (from === to) return value;
63
+ const toMM = {
64
+ px: 0.264583,
65
+ // 1px = 0.264583mm (96 DPI)
66
+ mm: 1,
67
+ cm: 10,
68
+ in: 25.4
69
+ };
70
+ const mmValue = value * (from === "px" ? toMM.px : toMM[from] || 1);
71
+ if (to === "px") {
72
+ return mmValue / toMM.px;
250
73
  }
251
- return null;
74
+ return mmValue / (toMM[to] || 1);
252
75
  }
253
- updateBackground() {
254
- void this.updateBackgroundAsync();
76
+ };
77
+
78
+ // src/units.ts
79
+ function parseLengthToMm(input, defaultUnit) {
80
+ var _a, _b;
81
+ if (typeof input === "number") {
82
+ if (!Number.isFinite(input)) return 0;
83
+ return Coordinate.convertUnit(input, defaultUnit, "mm");
255
84
  }
256
- async updateBackgroundAsync() {
257
- if (!this.canvasService) return;
258
- const seq = ++this.renderSeq;
259
- const color = this.color;
260
- const nextUrl = String(this.url || "").trim();
261
- if (!nextUrl) {
262
- this.renderImageUrl = "";
263
- } else if (nextUrl !== this.renderImageUrl) {
264
- const loaded = await this.ensureImageSize(nextUrl);
265
- if (seq !== this.renderSeq) return;
266
- if (loaded) {
267
- this.renderImageUrl = nextUrl;
268
- }
269
- }
270
- this.specs = this.buildBackgroundSpecs(color, this.renderImageUrl);
271
- await this.canvasService.flushRenderFromProducers();
272
- if (seq !== this.renderSeq) return;
273
- const layer = this.canvasService.getLayer(BACKGROUND_LAYER_ID);
274
- if (layer) {
275
- this.canvasService.canvas.sendObjectToBack(layer);
276
- }
277
- this.canvasService.requestRenderAll();
278
- }
279
- };
280
-
281
- // src/extensions/image.ts
282
- import {
283
- ContributionPointIds as ContributionPointIds2
284
- } from "@pooder/core";
285
- import {
286
- Canvas as FabricCanvas,
287
- Image as FabricImage2,
288
- Pattern,
289
- Point
290
- } from "fabric";
85
+ const raw = input.trim();
86
+ if (!raw) return 0;
87
+ const match = raw.match(/^([+-]?\d+(?:\.\d+)?)\s*(px|mm|cm|in)?$/i);
88
+ if (!match) return 0;
89
+ const value = Number(match[1]);
90
+ if (!Number.isFinite(value)) return 0;
91
+ const unit = (_b = (_a = match[2]) == null ? void 0 : _a.toLowerCase()) != null ? _b : defaultUnit;
92
+ return Coordinate.convertUnit(value, unit, "mm");
93
+ }
291
94
 
292
95
  // src/extensions/dielineShape.ts
293
96
  var BUILTIN_DIELINE_SHAPES = [
@@ -304,7 +107,7 @@ var DEFAULT_HEART_SHAPE_PARAMS = {
304
107
  tipSharpness: 0
305
108
  };
306
109
  var DEFAULT_DIELINE_SHAPE_STYLE = {
307
- fitMode: "contain",
110
+ fitMode: "stretch",
308
111
  ...DEFAULT_HEART_SHAPE_PARAMS
309
112
  };
310
113
  function isDielineShape(value) {
@@ -354,968 +157,1429 @@ function getHeartShapeParams(style) {
354
157
  };
355
158
  }
356
159
 
357
- // src/extensions/geometry.ts
358
- import paper from "paper";
359
-
360
- // src/extensions/bridgeSelection.ts
361
- function pickExitIndex(hits) {
362
- for (let i = 0; i < hits.length; i++) {
363
- const h = hits[i];
364
- if (h.insideBelow && !h.insideAbove) return i;
365
- }
366
- return -1;
160
+ // src/extensions/sceneLayoutModel.ts
161
+ var DEFAULT_SIZE_STATE = {
162
+ unit: "mm",
163
+ actualWidthMm: 500,
164
+ actualHeightMm: 500,
165
+ constraintMode: "free",
166
+ aspectRatio: 1,
167
+ cutMode: "trim",
168
+ cutMarginMm: 0,
169
+ viewPadding: 140,
170
+ minMm: 10,
171
+ maxMm: 2e3,
172
+ stepMm: 0.1
173
+ };
174
+ function clamp(value, min, max) {
175
+ return Math.max(min, Math.min(max, value));
367
176
  }
368
- function scoreOutsideAbove(samples) {
369
- let score = 0;
370
- for (const s of samples) {
371
- if (s.outsideAbove) score++;
372
- }
373
- return score;
177
+ function roundToStep(value, step) {
178
+ if (!Number.isFinite(step) || step <= 0) return value;
179
+ return Math.round(value / step) * step;
374
180
  }
375
-
376
- // src/extensions/wrappedOffsets.ts
377
- function wrappedDistance(total, start, end) {
378
- if (!Number.isFinite(total) || total <= 0) return 0;
379
- if (!Number.isFinite(start) || !Number.isFinite(end)) return 0;
380
- const s = (start % total + total) % total;
381
- const e = (end % total + total) % total;
382
- return e >= s ? e - s : total - s + e;
181
+ function sanitizeMmValue(valueMm, limits) {
182
+ if (!Number.isFinite(valueMm)) return limits.minMm;
183
+ const rounded = roundToStep(valueMm, limits.stepMm);
184
+ return clamp(rounded, limits.minMm, limits.maxMm);
383
185
  }
384
- function sampleWrappedOffsets(total, start, end, count) {
385
- if (!Number.isFinite(total) || total <= 0) return [];
386
- if (!Number.isFinite(start) || !Number.isFinite(end)) return [];
387
- const n = Math.max(0, Math.floor(count));
388
- if (n <= 0) return [];
389
- const dist = wrappedDistance(total, start, end);
390
- if (n === 1) return [(start % total + total) % total];
391
- const step = dist / (n - 1);
392
- const offsets = [];
393
- for (let i = 0; i < n; i++) {
394
- const raw = start + step * i;
395
- const wrapped = (raw % total + total) % total;
396
- offsets.push(wrapped);
397
- }
398
- return offsets;
186
+ function normalizeUnit(value) {
187
+ if (value === "cm" || value === "in") return value;
188
+ return "mm";
399
189
  }
400
-
401
- // src/extensions/geometry.ts
402
- function resolveFeaturePosition(feature, geometry) {
403
- const { x, y, width, height } = geometry;
404
- const left = x - width / 2;
405
- const top = y - height / 2;
406
- return {
407
- x: left + feature.x * width,
408
- y: top + feature.y * height
409
- };
190
+ function normalizeConstraintMode(value) {
191
+ if (value === "lockAspect" || value === "equal") return value;
192
+ return "free";
410
193
  }
411
- function ensurePaper(width, height) {
412
- if (!paper.project) {
413
- paper.setup(new paper.Size(width, height));
414
- } else {
415
- paper.view.viewSize = new paper.Size(width, height);
416
- }
194
+ function normalizeCutMode(value) {
195
+ if (value === "outset" || value === "inset") return value;
196
+ return "trim";
417
197
  }
418
- var isBridgeDebugEnabled = () => Boolean(globalThis.__POODER_BRIDGE_DEBUG__);
419
- function normalizePathItem(shape) {
420
- let result = shape;
421
- if (typeof result.resolveCrossings === "function") result = result.resolveCrossings();
422
- if (typeof result.reduce === "function") result = result.reduce({});
423
- if (typeof result.reorient === "function") result = result.reorient(true, true);
424
- if (typeof result.reduce === "function") result = result.reduce({});
425
- return result;
198
+ function toMm(value, fromUnit) {
199
+ return Coordinate.convertUnit(value, fromUnit, "mm");
426
200
  }
427
- function getBridgeDelta(itemBounds, overlap) {
428
- return Math.max(overlap, Math.min(5, Math.max(1, itemBounds.height * 0.02)));
201
+ function fromMm(valueMm, toUnit) {
202
+ return Coordinate.convertUnit(valueMm, "mm", toUnit);
429
203
  }
430
- function getExitHit(args) {
431
- const { mainShape, x, bridgeBottom, toY, eps, delta, overlap, op } = args;
432
- const ray = new paper.Path.Line({
433
- from: [x, bridgeBottom],
434
- to: [x, toY],
435
- insert: false
436
- });
437
- const intersections = mainShape.getIntersections(ray) || [];
438
- ray.remove();
439
- const validHits = intersections.filter((i) => i.point.y < bridgeBottom - eps);
440
- if (validHits.length === 0) return null;
441
- validHits.sort((a, b) => b.point.y - a.point.y);
442
- const flags = validHits.map((h) => {
443
- const above = h.point.add(new paper.Point(0, -delta));
444
- const below = h.point.add(new paper.Point(0, delta));
445
- return {
446
- insideAbove: mainShape.contains(above),
447
- insideBelow: mainShape.contains(below)
448
- };
449
- });
450
- const idx = pickExitIndex(flags);
451
- if (idx < 0) return null;
452
- if (isBridgeDebugEnabled()) {
453
- console.debug("Geometry: Bridge ray", {
454
- x,
455
- validHits: validHits.length,
456
- idx,
457
- delta,
458
- overlap,
459
- op
460
- });
204
+ function resolvePaddingPx(raw, containerWidth, containerHeight) {
205
+ if (typeof raw === "number") return Math.max(0, raw);
206
+ if (typeof raw === "string") {
207
+ if (raw.endsWith("%")) {
208
+ const percent = parseFloat(raw) / 100;
209
+ if (!Number.isFinite(percent)) return 0;
210
+ return Math.max(0, Math.min(containerWidth, containerHeight) * percent);
211
+ }
212
+ const fixed = parseFloat(raw);
213
+ return Number.isFinite(fixed) ? Math.max(0, fixed) : 0;
461
214
  }
462
- const hit = validHits[idx];
463
- return { point: hit.point, location: hit };
215
+ return 0;
464
216
  }
465
- function selectOuterChain(args) {
466
- const { mainShape, pointsA, pointsB, delta, overlap, op } = args;
467
- const scoreA = scoreOutsideAbove(
468
- pointsA.map((p) => ({
469
- outsideAbove: !mainShape.contains(p.add(new paper.Point(0, -delta)))
470
- }))
217
+ function readSizeState(configService) {
218
+ const unit = normalizeUnit(
219
+ configService.get("size.unit", DEFAULT_SIZE_STATE.unit)
471
220
  );
472
- const scoreB = scoreOutsideAbove(
473
- pointsB.map((p) => ({
474
- outsideAbove: !mainShape.contains(p.add(new paper.Point(0, -delta)))
475
- }))
221
+ const minMm = Math.max(
222
+ 0.1,
223
+ Number(configService.get("size.minMm", DEFAULT_SIZE_STATE.minMm))
476
224
  );
477
- const ratioA = scoreA / pointsA.length;
478
- const ratioB = scoreB / pointsB.length;
479
- if (isBridgeDebugEnabled()) {
480
- console.debug("Geometry: Bridge chain", {
481
- scoreA,
482
- scoreB,
483
- lenA: pointsA.length,
484
- lenB: pointsB.length,
485
- ratioA,
486
- ratioB,
487
- delta,
488
- overlap,
489
- op
490
- });
491
- }
492
- const ratioEps = 1e-6;
493
- if (Math.abs(ratioA - ratioB) > ratioEps) {
494
- return ratioA > ratioB ? pointsA : pointsB;
495
- }
496
- if (scoreA !== scoreB) return scoreA > scoreB ? pointsA : pointsB;
497
- return pointsA.length <= pointsB.length ? pointsA : pointsB;
498
- }
499
- function fitPathItemToRect(item, rect, fitMode) {
500
- const { left, top, width, height } = rect;
501
- const bounds = item.bounds;
502
- if (width <= 0 || height <= 0 || !Number.isFinite(bounds.width) || !Number.isFinite(bounds.height) || bounds.width <= 0 || bounds.height <= 0) {
503
- item.position = new paper.Point(left + width / 2, top + height / 2);
504
- return item;
505
- }
506
- item.translate(new paper.Point(-bounds.left, -bounds.top));
507
- if (fitMode === "stretch") {
508
- item.scale(width / bounds.width, height / bounds.height, new paper.Point(0, 0));
509
- item.translate(new paper.Point(left, top));
510
- return item;
511
- }
512
- const uniformScale = Math.min(width / bounds.width, height / bounds.height);
513
- item.scale(uniformScale, uniformScale, new paper.Point(0, 0));
514
- const scaledWidth = bounds.width * uniformScale;
515
- const scaledHeight = bounds.height * uniformScale;
516
- item.translate(
517
- new paper.Point(
518
- left + (width - scaledWidth) / 2,
519
- top + (height - scaledHeight) / 2
520
- )
225
+ const maxMm = Math.max(
226
+ minMm,
227
+ Number(configService.get("size.maxMm", DEFAULT_SIZE_STATE.maxMm))
521
228
  );
522
- return item;
523
- }
524
- function createNormalizedHeartPath(params) {
525
- const { lobeSpread, notchDepth, tipSharpness } = params;
526
- const halfSpread = 0.22 + lobeSpread * 0.18;
527
- const notchY = 0.06 + notchDepth * 0.2;
528
- const shoulderY = 0.24 + notchDepth * 0.2;
529
- const topLift = 0.12 + (1 - notchDepth) * 0.06;
530
- const topY = notchY - topLift;
531
- const sideCtrlY = shoulderY - (0.18 - notchDepth * 0.08);
532
- const lowerCtrlY = 0.58 + (1 - tipSharpness) * 0.16;
533
- const tipCtrlX = 0.34 - tipSharpness * 0.2;
534
- const notchCtrlX = 0.06 + lobeSpread * 0.06;
535
- const lobeCtrlX = 0.1 + lobeSpread * 0.08;
536
- const notchCtrlY = notchY - topLift * 0.45;
537
- const xPeakL = 0.5 - halfSpread;
538
- const xPeakR = 0.5 + halfSpread;
539
- const heartPath = new paper.Path({ insert: false });
540
- heartPath.moveTo(new paper.Point(0.5, notchY));
541
- heartPath.cubicCurveTo(
542
- new paper.Point(0.5 - notchCtrlX, notchCtrlY),
543
- new paper.Point(xPeakL + lobeCtrlX, topY),
544
- new paper.Point(xPeakL, topY)
229
+ const stepMm = Math.max(
230
+ 1e-3,
231
+ Number(configService.get("size.stepMm", DEFAULT_SIZE_STATE.stepMm))
545
232
  );
546
- heartPath.cubicCurveTo(
547
- new paper.Point(xPeakL - lobeCtrlX, topY),
548
- new paper.Point(0, sideCtrlY),
549
- new paper.Point(0, shoulderY)
233
+ const actualWidthMm = sanitizeMmValue(
234
+ parseLengthToMm(
235
+ configService.get("size.actualWidthMm", DEFAULT_SIZE_STATE.actualWidthMm),
236
+ "mm"
237
+ ),
238
+ { minMm, maxMm, stepMm }
550
239
  );
551
- heartPath.cubicCurveTo(
552
- new paper.Point(0, lowerCtrlY),
553
- new paper.Point(tipCtrlX, 1),
554
- new paper.Point(0.5, 1)
240
+ const actualHeightMm = sanitizeMmValue(
241
+ parseLengthToMm(
242
+ configService.get(
243
+ "size.actualHeightMm",
244
+ DEFAULT_SIZE_STATE.actualHeightMm
245
+ ),
246
+ "mm"
247
+ ),
248
+ { minMm, maxMm, stepMm }
555
249
  );
556
- heartPath.cubicCurveTo(
557
- new paper.Point(1 - tipCtrlX, 1),
558
- new paper.Point(1, lowerCtrlY),
559
- new paper.Point(1, shoulderY)
250
+ const aspectRaw = Number(
251
+ configService.get("size.aspectRatio", DEFAULT_SIZE_STATE.aspectRatio)
560
252
  );
561
- heartPath.cubicCurveTo(
562
- new paper.Point(1, sideCtrlY),
563
- new paper.Point(xPeakR + lobeCtrlX, topY),
564
- new paper.Point(xPeakR, topY)
253
+ const aspectRatio = Number.isFinite(aspectRaw) && aspectRaw > 0 ? aspectRaw : actualWidthMm / Math.max(1e-3, actualHeightMm);
254
+ const cutMarginMm = Math.max(
255
+ 0,
256
+ parseLengthToMm(
257
+ configService.get("size.cutMarginMm", DEFAULT_SIZE_STATE.cutMarginMm),
258
+ "mm"
259
+ )
565
260
  );
566
- heartPath.cubicCurveTo(
567
- new paper.Point(xPeakR - lobeCtrlX, topY),
568
- new paper.Point(0.5 + notchCtrlX, notchCtrlY),
569
- new paper.Point(0.5, notchY)
261
+ const viewPadding = configService.get(
262
+ "size.viewPadding",
263
+ DEFAULT_SIZE_STATE.viewPadding
570
264
  );
571
- heartPath.closed = true;
572
- return heartPath;
573
- }
574
- function createHeartBaseShape(options) {
575
- const { x, y, width, height } = options;
576
- const w = Math.max(0, width);
577
- const h = Math.max(0, height);
578
- const left = x - w / 2;
579
- const top = y - h / 2;
580
- const fitMode = getShapeFitMode(options.shapeStyle);
581
- const heartParams = getHeartShapeParams(options.shapeStyle);
582
- const rawHeart = createNormalizedHeartPath(heartParams);
583
- return fitPathItemToRect(rawHeart, { left, top, width: w, height: h }, fitMode);
265
+ return {
266
+ unit,
267
+ actualWidthMm,
268
+ actualHeightMm,
269
+ constraintMode: normalizeConstraintMode(
270
+ configService.get(
271
+ "size.constraintMode",
272
+ DEFAULT_SIZE_STATE.constraintMode
273
+ )
274
+ ),
275
+ aspectRatio,
276
+ cutMode: normalizeCutMode(
277
+ configService.get("size.cutMode", DEFAULT_SIZE_STATE.cutMode)
278
+ ),
279
+ cutMarginMm,
280
+ viewPadding,
281
+ minMm,
282
+ maxMm,
283
+ stepMm
284
+ };
584
285
  }
585
- var BUILTIN_SHAPE_BUILDERS = {
586
- rect: (options) => {
587
- const { x, y, width, height, radius } = options;
588
- return new paper.Path.Rectangle({
589
- point: [x - width / 2, y - height / 2],
590
- size: [Math.max(0, width), Math.max(0, height)],
591
- radius: Math.max(0, radius)
592
- });
593
- },
594
- circle: (options) => {
595
- const { x, y, width, height } = options;
596
- const r = Math.min(width, height) / 2;
597
- return new paper.Path.Circle({
598
- center: new paper.Point(x, y),
599
- radius: Math.max(0, r)
600
- });
601
- },
602
- ellipse: (options) => {
603
- const { x, y, width, height } = options;
604
- return new paper.Path.Ellipse({
605
- center: new paper.Point(x, y),
606
- radius: [Math.max(0, width / 2), Math.max(0, height / 2)]
607
- });
608
- },
609
- heart: createHeartBaseShape
610
- };
611
- function createCustomBaseShape(options) {
612
- var _a;
613
- const {
614
- pathData,
615
- customSourceWidthPx,
616
- customSourceHeightPx,
617
- x,
618
- y,
286
+ function rectByCenter(centerX, centerY, width, height) {
287
+ return {
288
+ left: centerX - width / 2,
289
+ top: centerY - height / 2,
619
290
  width,
620
- height
621
- } = options;
622
- if (typeof pathData !== "string" || pathData.trim().length === 0) {
623
- return null;
624
- }
625
- const center = new paper.Point(x, y);
626
- const hasMultipleSubPaths = ((_a = (pathData.match(/[Mm]/g) || []).length) != null ? _a : 0) > 1;
627
- const path = hasMultipleSubPaths ? new paper.CompoundPath(pathData) : (() => {
628
- const single = new paper.Path();
629
- single.pathData = pathData;
630
- return single;
631
- })();
632
- const sourceWidth = Number(customSourceWidthPx != null ? customSourceWidthPx : 0);
633
- const sourceHeight = Number(customSourceHeightPx != null ? customSourceHeightPx : 0);
634
- if (Number.isFinite(sourceWidth) && Number.isFinite(sourceHeight) && sourceWidth > 0 && sourceHeight > 0 && width > 0 && height > 0) {
635
- const targetLeft = x - width / 2;
636
- const targetTop = y - height / 2;
637
- path.scale(width / sourceWidth, height / sourceHeight, new paper.Point(0, 0));
638
- path.translate(new paper.Point(targetLeft, targetTop));
639
- return path;
640
- }
641
- if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
642
- path.position = center;
643
- path.scale(width / path.bounds.width, height / path.bounds.height);
644
- return path;
645
- }
646
- path.position = center;
647
- return path;
648
- }
649
- function createBaseShape(options) {
650
- const { shape } = options;
651
- if (shape === "custom") {
652
- const customShape = createCustomBaseShape(options);
653
- if (customShape) return customShape;
654
- return BUILTIN_SHAPE_BUILDERS[DEFAULT_DIELINE_SHAPE](options);
655
- }
656
- return BUILTIN_SHAPE_BUILDERS[shape](options);
291
+ height,
292
+ centerX,
293
+ centerY
294
+ };
657
295
  }
658
- function resolveBridgeBasePath(shape, anchor) {
659
- if (shape instanceof paper.Path) {
660
- return shape;
296
+ function getCutSizeMm(size) {
297
+ if (size.cutMode === "trim") {
298
+ return { widthMm: size.actualWidthMm, heightMm: size.actualHeightMm };
661
299
  }
662
- if (shape instanceof paper.CompoundPath) {
663
- const children = (shape.children || []).filter(
664
- (child) => child instanceof paper.Path
665
- );
666
- if (!children.length) return null;
667
- let best = children[0];
668
- let bestDistance = Infinity;
669
- for (const child of children) {
670
- const location = child.getNearestLocation(anchor);
671
- const point = location == null ? void 0 : location.point;
672
- if (!point) continue;
673
- const distance = point.getDistance(anchor);
674
- if (distance < bestDistance) {
675
- bestDistance = distance;
676
- best = child;
677
- }
678
- }
679
- return best;
300
+ const delta = size.cutMarginMm * 2;
301
+ if (size.cutMode === "outset") {
302
+ return {
303
+ widthMm: size.actualWidthMm + delta,
304
+ heightMm: size.actualHeightMm + delta
305
+ };
680
306
  }
681
- return null;
307
+ return {
308
+ widthMm: Math.max(size.minMm, size.actualWidthMm - delta),
309
+ heightMm: Math.max(size.minMm, size.actualHeightMm - delta)
310
+ };
682
311
  }
683
- function createFeatureItem(feature, center) {
684
- let item;
685
- if (feature.shape === "rect") {
686
- const w = feature.width || 10;
687
- const h = feature.height || 10;
688
- const r = feature.radius || 0;
689
- item = new paper.Path.Rectangle({
690
- point: [center.x - w / 2, center.y - h / 2],
691
- size: [w, h],
692
- radius: r
693
- });
694
- } else {
695
- const r = feature.radius || 5;
696
- item = new paper.Path.Circle({
697
- center,
698
- radius: r
699
- });
312
+ function computeSceneLayout(canvasService, size) {
313
+ const canvasWidth = canvasService.canvas.width || 0;
314
+ const canvasHeight = canvasService.canvas.height || 0;
315
+ if (canvasWidth <= 0 || canvasHeight <= 0) return null;
316
+ const { widthMm: cutWidthMm, heightMm: cutHeightMm } = getCutSizeMm(size);
317
+ const viewWidthMm = Math.max(size.actualWidthMm, cutWidthMm);
318
+ const viewHeightMm = Math.max(size.actualHeightMm, cutHeightMm);
319
+ if (!Number.isFinite(viewWidthMm) || !Number.isFinite(viewHeightMm) || viewWidthMm <= 0 || viewHeightMm <= 0) {
320
+ return null;
700
321
  }
701
- if (feature.rotation) {
702
- item.rotate(feature.rotation, center);
322
+ const paddingPx = resolvePaddingPx(
323
+ size.viewPadding,
324
+ canvasWidth,
325
+ canvasHeight
326
+ );
327
+ canvasService.viewport.updateContainer(canvasWidth, canvasHeight);
328
+ canvasService.viewport.setPadding(paddingPx);
329
+ canvasService.viewport.updatePhysical(viewWidthMm, viewHeightMm);
330
+ const layout = canvasService.viewport.layout;
331
+ if (!Number.isFinite(layout.scale) || !Number.isFinite(layout.offsetX) || !Number.isFinite(layout.offsetY) || layout.scale <= 0) {
332
+ return null;
703
333
  }
704
- return item;
705
- }
706
- function getPerimeterShape(options) {
707
- let mainShape = createBaseShape(options);
708
- const { features } = options;
709
- if (features && features.length > 0) {
710
- const edgeFeatures = features.filter(
711
- (f) => !f.renderBehavior || f.renderBehavior === "edge"
712
- );
713
- const adds = [];
714
- const subtracts = [];
715
- edgeFeatures.forEach((f) => {
716
- const pos = resolveFeaturePosition(f, options);
717
- const center = new paper.Point(pos.x, pos.y);
718
- const item = createFeatureItem(f, center);
719
- if (f.bridge && f.bridge.type === "vertical") {
720
- const itemBounds = item.bounds;
721
- const mainBounds = mainShape.bounds;
722
- const bridgeTop = mainBounds.top;
723
- const bridgeBottom = itemBounds.top;
724
- if (bridgeBottom > bridgeTop) {
725
- const overlap = 2;
726
- const rayPadding = 10;
727
- const eps = 0.1;
728
- const delta = getBridgeDelta(itemBounds, overlap);
729
- const toY = bridgeTop - rayPadding;
730
- const inset = Math.min(1, Math.max(0, itemBounds.width * 0.01));
731
- const xLeft = itemBounds.left + inset;
732
- const xRight = itemBounds.right - inset;
733
- const bridgeBasePath = resolveBridgeBasePath(mainShape, center);
734
- const canBridge = !!bridgeBasePath && xRight - xLeft > eps;
735
- if (canBridge && bridgeBasePath) {
736
- const leftHit = getExitHit({
737
- mainShape: bridgeBasePath,
738
- x: xLeft,
739
- bridgeBottom,
740
- toY,
741
- eps,
742
- delta,
743
- overlap,
744
- op: f.operation
745
- });
746
- const rightHit = getExitHit({
747
- mainShape: bridgeBasePath,
748
- x: xRight,
749
- bridgeBottom,
750
- toY,
751
- eps,
752
- delta,
753
- overlap,
754
- op: f.operation
755
- });
756
- if (leftHit && rightHit) {
757
- const pathLength = bridgeBasePath.length;
758
- const leftOffset = leftHit.location.offset;
759
- const rightOffset = rightHit.location.offset;
760
- const distanceA = wrappedDistance(pathLength, leftOffset, rightOffset);
761
- const distanceB = wrappedDistance(pathLength, rightOffset, leftOffset);
762
- const countFor = (d) => Math.max(8, Math.min(80, Math.ceil(d / 6)));
763
- const offsetsA = sampleWrappedOffsets(
764
- pathLength,
765
- leftOffset,
766
- rightOffset,
767
- countFor(distanceA)
768
- );
769
- const offsetsB = sampleWrappedOffsets(
770
- pathLength,
771
- rightOffset,
772
- leftOffset,
773
- countFor(distanceB)
774
- );
775
- const pointsA = offsetsA.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
776
- const pointsB = offsetsB.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
777
- if (pointsA.length >= 2 && pointsB.length >= 2) {
778
- let topBase = selectOuterChain({
779
- mainShape: bridgeBasePath,
780
- pointsA,
781
- pointsB,
782
- delta,
783
- overlap,
784
- op: f.operation
785
- });
786
- const dist2 = (a, b) => {
787
- const dx = a.x - b.x;
788
- const dy = a.y - b.y;
789
- return dx * dx + dy * dy;
790
- };
791
- if (dist2(topBase[0], leftHit.point) > dist2(topBase[0], rightHit.point)) {
792
- topBase = topBase.slice().reverse();
793
- }
794
- topBase = topBase.slice();
795
- topBase[0] = leftHit.point;
796
- topBase[topBase.length - 1] = rightHit.point;
797
- const capShiftY = f.operation === "subtract" ? -Math.max(overlap * 2, delta) : overlap;
798
- const topPoints = topBase.map(
799
- (p) => p.add(new paper.Point(0, capShiftY))
800
- );
801
- const bridgeBottomY = bridgeBottom + overlap * 2;
802
- const bridgePoly = new paper.Path({ insert: false });
803
- for (const p of topPoints) bridgePoly.add(p);
804
- bridgePoly.add(new paper.Point(xRight, bridgeBottomY));
805
- bridgePoly.add(new paper.Point(xLeft, bridgeBottomY));
806
- bridgePoly.closed = true;
807
- const unitedItem = item.unite(bridgePoly);
808
- item.remove();
809
- bridgePoly.remove();
810
- if (f.operation === "add") {
811
- adds.push(unitedItem);
812
- } else {
813
- subtracts.push(unitedItem);
814
- }
815
- return;
816
- }
817
- }
818
- }
819
- if (f.operation === "add") {
820
- adds.push(item);
821
- } else {
822
- subtracts.push(item);
823
- }
824
- } else {
825
- if (f.operation === "add") {
826
- adds.push(item);
827
- } else {
828
- subtracts.push(item);
829
- }
830
- }
831
- } else {
832
- if (f.operation === "add") {
833
- adds.push(item);
834
- } else {
835
- subtracts.push(item);
836
- }
837
- }
838
- });
839
- if (adds.length > 0) {
840
- for (const item of adds) {
841
- try {
842
- const temp = mainShape.unite(item);
843
- mainShape.remove();
844
- item.remove();
845
- mainShape = normalizePathItem(temp);
846
- } catch (e) {
847
- console.error("Geometry: Failed to unite feature", e);
848
- item.remove();
849
- }
850
- }
851
- }
852
- if (subtracts.length > 0) {
853
- for (const item of subtracts) {
854
- try {
855
- const temp = mainShape.subtract(item);
856
- mainShape.remove();
857
- item.remove();
858
- mainShape = normalizePathItem(temp);
859
- } catch (e) {
860
- console.error("Geometry: Failed to subtract feature", e);
861
- item.remove();
862
- }
863
- }
864
- }
865
- }
866
- return mainShape;
334
+ const centerX = layout.offsetX + layout.width / 2;
335
+ const centerY = layout.offsetY + layout.height / 2;
336
+ const trimWidthPx = size.actualWidthMm * layout.scale;
337
+ const trimHeightPx = size.actualHeightMm * layout.scale;
338
+ const cutWidthPx = cutWidthMm * layout.scale;
339
+ const cutHeightPx = cutHeightMm * layout.scale;
340
+ const trimRect = rectByCenter(centerX, centerY, trimWidthPx, trimHeightPx);
341
+ const cutRect = rectByCenter(centerX, centerY, cutWidthPx, cutHeightPx);
342
+ const bleedRect = rectByCenter(
343
+ centerX,
344
+ centerY,
345
+ Math.max(trimWidthPx, cutWidthPx),
346
+ Math.max(trimHeightPx, cutHeightPx)
347
+ );
348
+ return {
349
+ scale: layout.scale,
350
+ canvasWidth,
351
+ canvasHeight,
352
+ trimRect,
353
+ cutRect,
354
+ bleedRect,
355
+ trimWidthMm: size.actualWidthMm,
356
+ trimHeightMm: size.actualHeightMm,
357
+ cutWidthMm,
358
+ cutHeightMm,
359
+ cutMode: size.cutMode,
360
+ cutMarginMm: size.cutMarginMm
361
+ };
867
362
  }
868
- function applySurfaceFeatures(shape, features, options) {
869
- const surfaceFeatures = features.filter(
870
- (f) => f.renderBehavior === "surface"
363
+ function buildSceneGeometry(configService, layout) {
364
+ const radiusMm = parseLengthToMm(
365
+ configService.get("dieline.radius", 0),
366
+ "mm"
871
367
  );
872
- if (surfaceFeatures.length === 0) return shape;
873
- let result = shape;
874
- for (const f of surfaceFeatures) {
875
- const pos = resolveFeaturePosition(f, options);
876
- const center = new paper.Point(pos.x, pos.y);
877
- const item = createFeatureItem(f, center);
878
- try {
879
- if (f.operation === "add") {
880
- const temp = result.unite(item);
881
- result.remove();
882
- item.remove();
883
- result = normalizePathItem(temp);
884
- } else {
885
- const temp = result.subtract(item);
886
- result.remove();
887
- item.remove();
888
- result = normalizePathItem(temp);
889
- }
890
- } catch (e) {
891
- console.error("Geometry: Failed to apply surface feature", e);
892
- item.remove();
368
+ const offset = (layout.cutRect.width - layout.trimRect.width) / 2;
369
+ const sourceWidth = Number(configService.get("dieline.customSourceWidthPx", 0));
370
+ const sourceHeight = Number(
371
+ configService.get("dieline.customSourceHeightPx", 0)
372
+ );
373
+ const shapeStyle = normalizeShapeStyle(
374
+ configService.get("dieline.shapeStyle", DEFAULT_DIELINE_SHAPE_STYLE)
375
+ );
376
+ return {
377
+ shape: normalizeDielineShape(
378
+ configService.get("dieline.shape", DEFAULT_DIELINE_SHAPE)
379
+ ),
380
+ shapeStyle,
381
+ unit: "px",
382
+ x: layout.trimRect.centerX,
383
+ y: layout.trimRect.centerY,
384
+ width: layout.trimRect.width,
385
+ height: layout.trimRect.height,
386
+ radius: radiusMm * layout.scale,
387
+ offset,
388
+ scale: layout.scale,
389
+ pathData: configService.get("dieline.pathData"),
390
+ customSourceWidthPx: Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : void 0,
391
+ customSourceHeightPx: Number.isFinite(sourceHeight) && sourceHeight > 0 ? sourceHeight : void 0
392
+ };
393
+ }
394
+
395
+ // src/extensions/background.ts
396
+ var BACKGROUND_LAYER_ID = "background";
397
+ var BACKGROUND_CONFIG_KEY = "background.config";
398
+ var DEFAULT_WIDTH = 800;
399
+ var DEFAULT_HEIGHT = 600;
400
+ var DEFAULT_BACKGROUND_CONFIG = {
401
+ version: 1,
402
+ layers: [
403
+ {
404
+ id: "base-color",
405
+ kind: "color",
406
+ anchor: "viewport",
407
+ fit: "cover",
408
+ opacity: 1,
409
+ order: 0,
410
+ enabled: true,
411
+ exportable: false,
412
+ color: "#fff"
893
413
  }
414
+ ]
415
+ };
416
+ function clampOpacity(value, fallback) {
417
+ const numeric = Number(value);
418
+ if (!Number.isFinite(numeric)) {
419
+ return Math.max(0, Math.min(1, fallback));
894
420
  }
895
- return result;
421
+ return Math.max(0, Math.min(1, numeric));
896
422
  }
897
- function generateDielinePath(options) {
898
- const paperWidth = options.canvasWidth || options.width * 2 || 2e3;
899
- const paperHeight = options.canvasHeight || options.height * 2 || 2e3;
900
- ensurePaper(paperWidth, paperHeight);
901
- paper.project.activeLayer.removeChildren();
902
- const perimeter = getPerimeterShape(options);
903
- const finalShape = applySurfaceFeatures(perimeter, options.features, options);
904
- const pathData = finalShape.pathData;
905
- finalShape.remove();
906
- return pathData;
423
+ function normalizeLayerKind(value, fallback) {
424
+ if (value === "color" || value === "image") {
425
+ return value;
426
+ }
427
+ return fallback;
907
428
  }
908
- function generateMaskPath(options) {
909
- ensurePaper(options.canvasWidth, options.canvasHeight);
910
- paper.project.activeLayer.removeChildren();
911
- const { canvasWidth, canvasHeight } = options;
912
- const maskRect = new paper.Path.Rectangle({
913
- point: [0, 0],
914
- size: [canvasWidth, canvasHeight]
915
- });
916
- const perimeter = getPerimeterShape(options);
917
- const mainShape = applySurfaceFeatures(perimeter, options.features, options);
918
- const finalMask = maskRect.subtract(mainShape);
919
- maskRect.remove();
920
- mainShape.remove();
921
- const pathData = finalMask.pathData;
922
- finalMask.remove();
923
- return pathData;
924
- }
925
- function generateBleedZonePath(originalOptions, offsetOptions, offset) {
926
- const paperWidth = originalOptions.canvasWidth || originalOptions.width * 2 || 2e3;
927
- const paperHeight = originalOptions.canvasHeight || originalOptions.height * 2 || 2e3;
928
- ensurePaper(paperWidth, paperHeight);
929
- paper.project.activeLayer.removeChildren();
930
- const pOriginal = getPerimeterShape(originalOptions);
931
- const shapeOriginal = applySurfaceFeatures(
932
- pOriginal,
933
- originalOptions.features,
934
- originalOptions
935
- );
936
- const pOffset = getPerimeterShape(offsetOptions);
937
- const shapeOffset = applySurfaceFeatures(
938
- pOffset,
939
- offsetOptions.features,
940
- offsetOptions
941
- );
942
- let bleedZone;
943
- if (offset > 0) {
944
- bleedZone = shapeOffset.subtract(shapeOriginal);
945
- } else {
946
- bleedZone = shapeOriginal.subtract(shapeOffset);
429
+ function normalizeFitMode2(value, fallback) {
430
+ if (value === "contain" || value === "cover" || value === "stretch") {
431
+ return value;
947
432
  }
948
- const pathData = bleedZone.pathData;
949
- shapeOriginal.remove();
950
- shapeOffset.remove();
951
- bleedZone.remove();
952
- return pathData;
433
+ return fallback;
953
434
  }
954
- function getLowestPointOnDieline(options) {
955
- ensurePaper(options.width * 2, options.height * 2);
956
- paper.project.activeLayer.removeChildren();
957
- const shape = createBaseShape(options);
958
- const bounds = shape.bounds;
959
- const result = {
960
- x: bounds.center.x,
961
- y: bounds.bottom
435
+ function normalizeAnchor(value, fallback) {
436
+ if (typeof value !== "string") return fallback;
437
+ const trimmed = value.trim();
438
+ return trimmed || fallback;
439
+ }
440
+ function normalizeOrder(value, fallback) {
441
+ const numeric = Number(value);
442
+ if (!Number.isFinite(numeric)) return fallback;
443
+ return numeric;
444
+ }
445
+ function normalizeLayer(raw, index, fallback) {
446
+ const fallbackLayer = fallback || {
447
+ id: `layer-${index + 1}`,
448
+ kind: "image",
449
+ anchor: "viewport",
450
+ fit: "contain",
451
+ opacity: 1,
452
+ order: index,
453
+ enabled: true,
454
+ exportable: false,
455
+ src: ""
456
+ };
457
+ if (!raw || typeof raw !== "object") {
458
+ return { ...fallbackLayer };
459
+ }
460
+ const input = raw;
461
+ const kind = normalizeLayerKind(input.kind, fallbackLayer.kind);
462
+ return {
463
+ id: typeof input.id === "string" && input.id.trim().length > 0 ? input.id.trim() : fallbackLayer.id,
464
+ kind,
465
+ anchor: normalizeAnchor(input.anchor, fallbackLayer.anchor),
466
+ fit: normalizeFitMode2(input.fit, fallbackLayer.fit),
467
+ opacity: clampOpacity(input.opacity, fallbackLayer.opacity),
468
+ order: normalizeOrder(input.order, fallbackLayer.order),
469
+ enabled: typeof input.enabled === "boolean" ? input.enabled : fallbackLayer.enabled,
470
+ exportable: typeof input.exportable === "boolean" ? input.exportable : fallbackLayer.exportable,
471
+ color: kind === "color" ? typeof input.color === "string" ? input.color : typeof fallbackLayer.color === "string" ? fallbackLayer.color : "#ffffff" : void 0,
472
+ src: kind === "image" ? typeof input.src === "string" ? input.src.trim() : typeof fallbackLayer.src === "string" ? fallbackLayer.src : "" : void 0
962
473
  };
963
- shape.remove();
964
- return result;
965
474
  }
966
- function getNearestPointOnDieline(point, options) {
967
- ensurePaper(options.width * 2, options.height * 2);
968
- paper.project.activeLayer.removeChildren();
969
- const shape = createBaseShape(options);
970
- const p = new paper.Point(point.x, point.y);
971
- const location = shape.getNearestLocation(p);
972
- const result = {
973
- x: location.point.x,
974
- y: location.point.y,
975
- normal: location.normal ? { x: location.normal.x, y: location.normal.y } : void 0
475
+ function normalizeConfig(raw) {
476
+ if (!raw || typeof raw !== "object") {
477
+ return cloneConfig(DEFAULT_BACKGROUND_CONFIG);
478
+ }
479
+ const input = raw;
480
+ const version = Number.isFinite(Number(input.version)) ? Number(input.version) : DEFAULT_BACKGROUND_CONFIG.version;
481
+ const baseLayers = Array.isArray(input.layers) ? input.layers.map((layer, index) => normalizeLayer(layer, index)) : cloneConfig(DEFAULT_BACKGROUND_CONFIG).layers;
482
+ const uniqueLayers = [];
483
+ const seen = /* @__PURE__ */ new Set();
484
+ baseLayers.forEach((layer, index) => {
485
+ let nextId = layer.id || `layer-${index + 1}`;
486
+ let serial = 1;
487
+ while (seen.has(nextId)) {
488
+ serial += 1;
489
+ nextId = `${layer.id || `layer-${index + 1}`}-${serial}`;
490
+ }
491
+ seen.add(nextId);
492
+ uniqueLayers.push({ ...layer, id: nextId });
493
+ });
494
+ return {
495
+ version,
496
+ layers: uniqueLayers
976
497
  };
977
- shape.remove();
978
- return result;
979
498
  }
980
- function getPathBounds(pathData) {
981
- const path = new paper.Path();
982
- path.pathData = pathData;
983
- const bounds = path.bounds;
984
- path.remove();
499
+ function cloneConfig(config) {
985
500
  return {
986
- x: bounds.x,
987
- y: bounds.y,
988
- width: bounds.width,
989
- height: bounds.height
501
+ version: config.version,
502
+ layers: (config.layers || []).map((layer) => ({ ...layer }))
990
503
  };
991
504
  }
992
-
993
- // src/coordinate.ts
994
- var Coordinate = class {
995
- /**
996
- * Calculate layout to fit content within container while preserving aspect ratio.
997
- */
998
- static calculateLayout(container, content, padding = 0) {
999
- const availableWidth = Math.max(0, container.width - padding * 2);
1000
- const availableHeight = Math.max(0, container.height - padding * 2);
1001
- if (content.width === 0 || content.height === 0) {
1002
- return { scale: 1, offsetX: 0, offsetY: 0, width: 0, height: 0 };
505
+ function mergeConfig(base, patch) {
506
+ const merged = {
507
+ version: patch.version === void 0 ? base.version : Number.isFinite(Number(patch.version)) ? Number(patch.version) : base.version,
508
+ layers: Array.isArray(patch.layers) ? patch.layers.map((layer, index) => normalizeLayer(layer, index)) : base.layers.map((layer) => ({ ...layer }))
509
+ };
510
+ return normalizeConfig(merged);
511
+ }
512
+ function configSignature(config) {
513
+ return JSON.stringify(config);
514
+ }
515
+ var BackgroundTool = class {
516
+ constructor(options) {
517
+ this.id = "pooder.kit.background";
518
+ this.metadata = {
519
+ name: "BackgroundTool"
520
+ };
521
+ this.config = cloneConfig(DEFAULT_BACKGROUND_CONFIG);
522
+ this.specs = [];
523
+ this.renderSeq = 0;
524
+ this.latestSceneLayout = null;
525
+ this.sourceSizeBySrc = /* @__PURE__ */ new Map();
526
+ this.pendingSizeBySrc = /* @__PURE__ */ new Map();
527
+ this.onCanvasResized = () => {
528
+ this.latestSceneLayout = null;
529
+ this.updateBackground();
530
+ };
531
+ this.onSceneLayoutChanged = (layout) => {
532
+ this.latestSceneLayout = layout;
533
+ this.updateBackground();
534
+ };
535
+ if (options && typeof options === "object") {
536
+ this.config = mergeConfig(this.config, options);
1003
537
  }
1004
- const scaleX = availableWidth / content.width;
1005
- const scaleY = availableHeight / content.height;
1006
- const scale = Math.min(scaleX, scaleY);
1007
- const width = content.width * scale;
1008
- const height = content.height * scale;
1009
- const offsetX = (container.width - width) / 2;
1010
- const offsetY = (container.height - height) / 2;
1011
- return { scale, offsetX, offsetY, width, height };
1012
538
  }
1013
- /**
1014
- * Convert an absolute value to a normalized value (0-1).
1015
- * @param value Absolute value (e.g., pixels)
1016
- * @param total Total dimension size (e.g., canvas width)
1017
- */
1018
- static toNormalized(value, total) {
1019
- return total === 0 ? 0 : value / total;
539
+ activate(context) {
540
+ var _a, _b;
541
+ this.canvasService = context.services.get("CanvasService");
542
+ if (!this.canvasService) {
543
+ console.warn("CanvasService not found for BackgroundTool");
544
+ return;
545
+ }
546
+ this.configService = context.services.get(
547
+ "ConfigurationService"
548
+ );
549
+ if (this.configService) {
550
+ this.config = normalizeConfig(
551
+ this.configService.get(
552
+ BACKGROUND_CONFIG_KEY,
553
+ DEFAULT_BACKGROUND_CONFIG
554
+ )
555
+ );
556
+ (_a = this.configChangeDisposable) == null ? void 0 : _a.dispose();
557
+ this.configChangeDisposable = this.configService.onAnyChange(
558
+ (e) => {
559
+ if (e.key === BACKGROUND_CONFIG_KEY) {
560
+ this.config = normalizeConfig(e.value);
561
+ this.updateBackground();
562
+ return;
563
+ }
564
+ if (e.key.startsWith("size.")) {
565
+ this.latestSceneLayout = null;
566
+ this.updateBackground();
567
+ }
568
+ }
569
+ );
570
+ }
571
+ (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
572
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
573
+ this.id,
574
+ () => ({
575
+ passes: [
576
+ {
577
+ id: BACKGROUND_LAYER_ID,
578
+ stack: 0,
579
+ order: 0,
580
+ objects: this.specs
581
+ }
582
+ ]
583
+ }),
584
+ { priority: 0 }
585
+ );
586
+ context.eventBus.on("canvas:resized", this.onCanvasResized);
587
+ context.eventBus.on("scene:layout:change", this.onSceneLayoutChanged);
588
+ this.updateBackground();
1020
589
  }
1021
- /**
1022
- * Convert a normalized value (0-1) to an absolute value.
1023
- * @param normalized Normalized value (0-1)
1024
- * @param total Total dimension size (e.g., canvas width)
1025
- */
1026
- static toAbsolute(normalized, total) {
1027
- return normalized * total;
590
+ deactivate(context) {
591
+ var _a, _b;
592
+ context.eventBus.off("canvas:resized", this.onCanvasResized);
593
+ context.eventBus.off("scene:layout:change", this.onSceneLayoutChanged);
594
+ this.renderSeq += 1;
595
+ this.specs = [];
596
+ this.latestSceneLayout = null;
597
+ (_a = this.configChangeDisposable) == null ? void 0 : _a.dispose();
598
+ this.configChangeDisposable = void 0;
599
+ (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
600
+ this.renderProducerDisposable = void 0;
601
+ if (!this.canvasService) return;
602
+ void this.canvasService.flushRenderFromProducers();
603
+ this.canvasService.requestRenderAll();
604
+ this.canvasService = void 0;
605
+ this.configService = void 0;
1028
606
  }
1029
- /**
1030
- * Normalize a point's coordinates.
1031
- */
1032
- static normalizePoint(point, size) {
607
+ contribute() {
1033
608
  return {
1034
- x: this.toNormalized(point.x, size.width),
1035
- y: this.toNormalized(point.y, size.height)
609
+ [ContributionPointIds.CONFIGURATIONS]: [
610
+ {
611
+ id: BACKGROUND_CONFIG_KEY,
612
+ type: "json",
613
+ label: "Background Config",
614
+ default: cloneConfig(DEFAULT_BACKGROUND_CONFIG)
615
+ }
616
+ ],
617
+ [ContributionPointIds.COMMANDS]: [
618
+ {
619
+ command: "background.getConfig",
620
+ title: "Get Background Config",
621
+ handler: () => cloneConfig(this.config)
622
+ },
623
+ {
624
+ command: "background.resetConfig",
625
+ title: "Reset Background Config",
626
+ handler: () => {
627
+ this.commitConfig(cloneConfig(DEFAULT_BACKGROUND_CONFIG));
628
+ return true;
629
+ }
630
+ },
631
+ {
632
+ command: "background.replaceConfig",
633
+ title: "Replace Background Config",
634
+ handler: (config) => {
635
+ this.commitConfig(normalizeConfig(config));
636
+ return true;
637
+ }
638
+ },
639
+ {
640
+ command: "background.patchConfig",
641
+ title: "Patch Background Config",
642
+ handler: (patch) => {
643
+ this.commitConfig(mergeConfig(this.config, patch || {}));
644
+ return true;
645
+ }
646
+ },
647
+ {
648
+ command: "background.upsertLayer",
649
+ title: "Upsert Background Layer",
650
+ handler: (layer) => {
651
+ const normalized = normalizeLayer(layer, 0);
652
+ const existingIndex = this.config.layers.findIndex(
653
+ (item) => item.id === normalized.id
654
+ );
655
+ const nextLayers = [...this.config.layers];
656
+ if (existingIndex >= 0) {
657
+ nextLayers[existingIndex] = normalizeLayer(
658
+ { ...nextLayers[existingIndex], ...layer },
659
+ existingIndex,
660
+ nextLayers[existingIndex]
661
+ );
662
+ } else {
663
+ nextLayers.push(
664
+ normalizeLayer(
665
+ {
666
+ ...normalized,
667
+ order: Number.isFinite(Number(layer.order)) ? Number(layer.order) : nextLayers.length
668
+ },
669
+ nextLayers.length
670
+ )
671
+ );
672
+ }
673
+ this.commitConfig(
674
+ normalizeConfig({
675
+ ...this.config,
676
+ layers: nextLayers
677
+ })
678
+ );
679
+ return true;
680
+ }
681
+ },
682
+ {
683
+ command: "background.removeLayer",
684
+ title: "Remove Background Layer",
685
+ handler: (id) => {
686
+ const nextLayers = this.config.layers.filter(
687
+ (layer) => layer.id !== id
688
+ );
689
+ this.commitConfig(
690
+ normalizeConfig({
691
+ ...this.config,
692
+ layers: nextLayers
693
+ })
694
+ );
695
+ return true;
696
+ }
697
+ }
698
+ ]
1036
699
  };
1037
700
  }
1038
- /**
1039
- * Denormalize a point's coordinates to absolute pixels.
1040
- */
1041
- static denormalizePoint(point, size) {
701
+ commitConfig(next) {
702
+ const normalized = normalizeConfig(next);
703
+ if (configSignature(normalized) === configSignature(this.config)) {
704
+ return;
705
+ }
706
+ if (this.configService) {
707
+ this.configService.update(BACKGROUND_CONFIG_KEY, cloneConfig(normalized));
708
+ return;
709
+ }
710
+ this.config = normalized;
711
+ this.updateBackground();
712
+ }
713
+ getViewportRect() {
714
+ var _a, _b;
715
+ const width = Number(((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 0);
716
+ const height = Number(((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 0);
1042
717
  return {
1043
- x: this.toAbsolute(point.x, size.width),
1044
- y: this.toAbsolute(point.y, size.height)
718
+ left: 0,
719
+ top: 0,
720
+ width: width > 0 ? width : DEFAULT_WIDTH,
721
+ height: height > 0 ? height : DEFAULT_HEIGHT
1045
722
  };
1046
723
  }
1047
- static convertUnit(value, from, to) {
1048
- if (from === to) return value;
1049
- const toMM = {
1050
- px: 0.264583,
1051
- // 1px = 0.264583mm (96 DPI)
1052
- mm: 1,
1053
- cm: 10,
1054
- in: 25.4
724
+ resolveSceneLayout() {
725
+ if (this.latestSceneLayout) return this.latestSceneLayout;
726
+ if (!this.canvasService || !this.configService) return null;
727
+ const layout = computeSceneLayout(
728
+ this.canvasService,
729
+ readSizeState(this.configService)
730
+ );
731
+ this.latestSceneLayout = layout;
732
+ return layout;
733
+ }
734
+ resolveFocusRect() {
735
+ const layout = this.resolveSceneLayout();
736
+ if (!layout) return null;
737
+ return {
738
+ left: layout.trimRect.left,
739
+ top: layout.trimRect.top,
740
+ width: layout.trimRect.width,
741
+ height: layout.trimRect.height
1055
742
  };
1056
- const mmValue = value * (from === "px" ? toMM.px : toMM[from] || 1);
1057
- if (to === "px") {
1058
- return mmValue / toMM.px;
743
+ }
744
+ resolveAnchorRect(anchor) {
745
+ if (anchor === "focus") {
746
+ return this.resolveFocusRect() || this.getViewportRect();
1059
747
  }
1060
- return mmValue / (toMM[to] || 1);
748
+ if (anchor !== "viewport") {
749
+ return this.getViewportRect();
750
+ }
751
+ return this.getViewportRect();
1061
752
  }
1062
- };
1063
-
1064
- // src/units.ts
1065
- function parseLengthToMm(input, defaultUnit) {
1066
- var _a, _b;
1067
- if (typeof input === "number") {
1068
- if (!Number.isFinite(input)) return 0;
1069
- return Coordinate.convertUnit(input, defaultUnit, "mm");
753
+ resolveImagePlacement(target, sourceSize, fit) {
754
+ const targetWidth = Math.max(1, Number(target.width || 0));
755
+ const targetHeight = Math.max(1, Number(target.height || 0));
756
+ const sourceWidth = Math.max(1, Number(sourceSize.width || 0));
757
+ const sourceHeight = Math.max(1, Number(sourceSize.height || 0));
758
+ if (fit === "stretch") {
759
+ return {
760
+ left: target.left,
761
+ top: target.top,
762
+ scaleX: targetWidth / sourceWidth,
763
+ scaleY: targetHeight / sourceHeight
764
+ };
765
+ }
766
+ const scale = fit === "contain" ? Math.min(targetWidth / sourceWidth, targetHeight / sourceHeight) : Math.max(targetWidth / sourceWidth, targetHeight / sourceHeight);
767
+ const renderWidth = sourceWidth * scale;
768
+ const renderHeight = sourceHeight * scale;
769
+ return {
770
+ left: target.left + (targetWidth - renderWidth) / 2,
771
+ top: target.top + (targetHeight - renderHeight) / 2,
772
+ scaleX: scale,
773
+ scaleY: scale
774
+ };
775
+ }
776
+ buildColorLayerSpec(layer) {
777
+ const rect = this.resolveAnchorRect(layer.anchor);
778
+ return {
779
+ id: `background.layer.${layer.id}.color`,
780
+ type: "rect",
781
+ space: "screen",
782
+ data: {
783
+ id: `background.layer.${layer.id}.color`,
784
+ layerId: BACKGROUND_LAYER_ID,
785
+ type: "background-layer",
786
+ layerRef: layer.id,
787
+ layerKind: layer.kind
788
+ },
789
+ props: {
790
+ left: rect.left,
791
+ top: rect.top,
792
+ width: rect.width,
793
+ height: rect.height,
794
+ originX: "left",
795
+ originY: "top",
796
+ fill: layer.color || "transparent",
797
+ opacity: layer.opacity,
798
+ selectable: false,
799
+ evented: false,
800
+ excludeFromExport: !layer.exportable
801
+ }
802
+ };
803
+ }
804
+ buildImageLayerSpec(layer) {
805
+ const src = String(layer.src || "").trim();
806
+ if (!src) return [];
807
+ const sourceSize = this.sourceSizeBySrc.get(src);
808
+ if (!sourceSize) return [];
809
+ const rect = this.resolveAnchorRect(layer.anchor);
810
+ const placement = this.resolveImagePlacement(rect, sourceSize, layer.fit);
811
+ return [
812
+ {
813
+ id: `background.layer.${layer.id}.image`,
814
+ type: "image",
815
+ src,
816
+ space: "screen",
817
+ data: {
818
+ id: `background.layer.${layer.id}.image`,
819
+ layerId: BACKGROUND_LAYER_ID,
820
+ type: "background-layer",
821
+ layerRef: layer.id,
822
+ layerKind: layer.kind
823
+ },
824
+ props: {
825
+ left: placement.left,
826
+ top: placement.top,
827
+ originX: "left",
828
+ originY: "top",
829
+ scaleX: placement.scaleX,
830
+ scaleY: placement.scaleY,
831
+ opacity: layer.opacity,
832
+ selectable: false,
833
+ evented: false,
834
+ excludeFromExport: !layer.exportable
835
+ }
836
+ }
837
+ ];
838
+ }
839
+ buildBackgroundSpecs(config) {
840
+ const activeLayers = (config.layers || []).filter((layer) => layer.enabled).map((layer, index) => ({ layer, index })).sort((a, b) => {
841
+ if (a.layer.order !== b.layer.order) {
842
+ return a.layer.order - b.layer.order;
843
+ }
844
+ return a.index - b.index;
845
+ });
846
+ const specs = [];
847
+ activeLayers.forEach(({ layer }) => {
848
+ if (layer.kind === "color") {
849
+ specs.push(this.buildColorLayerSpec(layer));
850
+ return;
851
+ }
852
+ specs.push(...this.buildImageLayerSpec(layer));
853
+ });
854
+ return specs;
855
+ }
856
+ collectActiveImageUrls(config) {
857
+ const urls = /* @__PURE__ */ new Set();
858
+ (config.layers || []).forEach((layer) => {
859
+ if (!layer.enabled || layer.kind !== "image") return;
860
+ const src = String(layer.src || "").trim();
861
+ if (!src) return;
862
+ urls.add(src);
863
+ });
864
+ return Array.from(urls);
865
+ }
866
+ async ensureImageSize(src) {
867
+ if (!src) return null;
868
+ const cached = this.sourceSizeBySrc.get(src);
869
+ if (cached) return cached;
870
+ const pending = this.pendingSizeBySrc.get(src);
871
+ if (pending) {
872
+ return pending;
873
+ }
874
+ const task = this.loadImageSize(src);
875
+ this.pendingSizeBySrc.set(src, task);
876
+ try {
877
+ return await task;
878
+ } finally {
879
+ if (this.pendingSizeBySrc.get(src) === task) {
880
+ this.pendingSizeBySrc.delete(src);
881
+ }
882
+ }
883
+ }
884
+ async loadImageSize(src) {
885
+ try {
886
+ const image = await FabricImage.fromURL(src, {
887
+ crossOrigin: "anonymous"
888
+ });
889
+ const width = Number((image == null ? void 0 : image.width) || 0);
890
+ const height = Number((image == null ? void 0 : image.height) || 0);
891
+ if (width > 0 && height > 0) {
892
+ const size = { width, height };
893
+ this.sourceSizeBySrc.set(src, size);
894
+ return size;
895
+ }
896
+ } catch (error) {
897
+ console.error("[BackgroundTool] Failed to load image", src, error);
898
+ }
899
+ return null;
900
+ }
901
+ updateBackground() {
902
+ void this.updateBackgroundAsync();
903
+ }
904
+ async updateBackgroundAsync() {
905
+ if (!this.canvasService) return;
906
+ const seq = ++this.renderSeq;
907
+ const currentConfig = cloneConfig(this.config);
908
+ const activeUrls = this.collectActiveImageUrls(currentConfig);
909
+ if (activeUrls.length > 0) {
910
+ await Promise.all(activeUrls.map((url) => this.ensureImageSize(url)));
911
+ if (seq !== this.renderSeq) return;
912
+ }
913
+ this.specs = this.buildBackgroundSpecs(currentConfig);
914
+ await this.canvasService.flushRenderFromProducers();
915
+ if (seq !== this.renderSeq) return;
916
+ this.canvasService.requestRenderAll();
1070
917
  }
1071
- const raw = input.trim();
1072
- if (!raw) return 0;
1073
- const match = raw.match(/^([+-]?\d+(?:\.\d+)?)\s*(px|mm|cm|in)?$/i);
1074
- if (!match) return 0;
1075
- const value = Number(match[1]);
1076
- if (!Number.isFinite(value)) return 0;
1077
- const unit = (_b = (_a = match[2]) == null ? void 0 : _a.toLowerCase()) != null ? _b : defaultUnit;
1078
- return Coordinate.convertUnit(value, unit, "mm");
1079
- }
1080
-
1081
- // src/extensions/sceneLayoutModel.ts
1082
- var DEFAULT_SIZE_STATE = {
1083
- unit: "mm",
1084
- actualWidthMm: 500,
1085
- actualHeightMm: 500,
1086
- constraintMode: "free",
1087
- aspectRatio: 1,
1088
- cutMode: "trim",
1089
- cutMarginMm: 0,
1090
- viewPadding: 140,
1091
- minMm: 10,
1092
- maxMm: 2e3,
1093
- stepMm: 0.1
1094
918
  };
1095
- function clamp(value, min, max) {
1096
- return Math.max(min, Math.min(max, value));
919
+
920
+ // src/extensions/image.ts
921
+ import {
922
+ ContributionPointIds as ContributionPointIds2
923
+ } from "@pooder/core";
924
+ import {
925
+ Canvas as FabricCanvas,
926
+ Control,
927
+ Image as FabricImage2,
928
+ Pattern,
929
+ Point,
930
+ controlsUtils
931
+ } from "fabric";
932
+
933
+ // src/extensions/geometry.ts
934
+ import paper from "paper";
935
+
936
+ // src/extensions/bridgeSelection.ts
937
+ function pickExitIndex(hits) {
938
+ for (let i = 0; i < hits.length; i++) {
939
+ const h = hits[i];
940
+ if (h.insideBelow && !h.insideAbove) return i;
941
+ }
942
+ return -1;
1097
943
  }
1098
- function roundToStep(value, step) {
1099
- if (!Number.isFinite(step) || step <= 0) return value;
1100
- return Math.round(value / step) * step;
944
+ function scoreOutsideAbove(samples) {
945
+ let score = 0;
946
+ for (const s of samples) {
947
+ if (s.outsideAbove) score++;
948
+ }
949
+ return score;
1101
950
  }
1102
- function sanitizeMmValue(valueMm, limits) {
1103
- if (!Number.isFinite(valueMm)) return limits.minMm;
1104
- const rounded = roundToStep(valueMm, limits.stepMm);
1105
- return clamp(rounded, limits.minMm, limits.maxMm);
951
+
952
+ // src/extensions/wrappedOffsets.ts
953
+ function wrappedDistance(total, start, end) {
954
+ if (!Number.isFinite(total) || total <= 0) return 0;
955
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return 0;
956
+ const s = (start % total + total) % total;
957
+ const e = (end % total + total) % total;
958
+ return e >= s ? e - s : total - s + e;
1106
959
  }
1107
- function normalizeUnit(value) {
1108
- if (value === "cm" || value === "in") return value;
1109
- return "mm";
960
+ function sampleWrappedOffsets(total, start, end, count) {
961
+ if (!Number.isFinite(total) || total <= 0) return [];
962
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return [];
963
+ const n = Math.max(0, Math.floor(count));
964
+ if (n <= 0) return [];
965
+ const dist = wrappedDistance(total, start, end);
966
+ if (n === 1) return [(start % total + total) % total];
967
+ const step = dist / (n - 1);
968
+ const offsets = [];
969
+ for (let i = 0; i < n; i++) {
970
+ const raw = start + step * i;
971
+ const wrapped = (raw % total + total) % total;
972
+ offsets.push(wrapped);
973
+ }
974
+ return offsets;
1110
975
  }
1111
- function normalizeConstraintMode(value) {
1112
- if (value === "lockAspect" || value === "equal") return value;
1113
- return "free";
976
+
977
+ // src/extensions/geometry.ts
978
+ function resolveFeaturePosition(feature, geometry) {
979
+ const { x, y, width, height } = geometry;
980
+ const left = x - width / 2;
981
+ const top = y - height / 2;
982
+ return {
983
+ x: left + feature.x * width,
984
+ y: top + feature.y * height
985
+ };
1114
986
  }
1115
- function normalizeCutMode(value) {
1116
- if (value === "outset" || value === "inset") return value;
1117
- return "trim";
987
+ function ensurePaper(width, height) {
988
+ if (!paper.project) {
989
+ paper.setup(new paper.Size(width, height));
990
+ } else {
991
+ paper.view.viewSize = new paper.Size(width, height);
992
+ }
1118
993
  }
1119
- function toMm(value, fromUnit) {
1120
- return Coordinate.convertUnit(value, fromUnit, "mm");
994
+ var isBridgeDebugEnabled = () => Boolean(globalThis.__POODER_BRIDGE_DEBUG__);
995
+ function normalizePathItem(shape) {
996
+ let result = shape;
997
+ if (typeof result.resolveCrossings === "function") result = result.resolveCrossings();
998
+ if (typeof result.reduce === "function") result = result.reduce({});
999
+ if (typeof result.reorient === "function") result = result.reorient(true, true);
1000
+ if (typeof result.reduce === "function") result = result.reduce({});
1001
+ return result;
1121
1002
  }
1122
- function fromMm(valueMm, toUnit) {
1123
- return Coordinate.convertUnit(valueMm, "mm", toUnit);
1003
+ function getBridgeDelta(itemBounds, overlap) {
1004
+ return Math.max(overlap, Math.min(5, Math.max(1, itemBounds.height * 0.02)));
1124
1005
  }
1125
- function resolvePaddingPx(raw, containerWidth, containerHeight) {
1126
- if (typeof raw === "number") return Math.max(0, raw);
1127
- if (typeof raw === "string") {
1128
- if (raw.endsWith("%")) {
1129
- const percent = parseFloat(raw) / 100;
1130
- if (!Number.isFinite(percent)) return 0;
1131
- return Math.max(0, Math.min(containerWidth, containerHeight) * percent);
1132
- }
1133
- const fixed = parseFloat(raw);
1134
- return Number.isFinite(fixed) ? Math.max(0, fixed) : 0;
1006
+ function getExitHit(args) {
1007
+ const { mainShape, x, bridgeBottom, toY, eps, delta, overlap, op } = args;
1008
+ const ray = new paper.Path.Line({
1009
+ from: [x, bridgeBottom],
1010
+ to: [x, toY],
1011
+ insert: false
1012
+ });
1013
+ const intersections = mainShape.getIntersections(ray) || [];
1014
+ ray.remove();
1015
+ const validHits = intersections.filter((i) => i.point.y < bridgeBottom - eps);
1016
+ if (validHits.length === 0) return null;
1017
+ validHits.sort((a, b) => b.point.y - a.point.y);
1018
+ const flags = validHits.map((h) => {
1019
+ const above = h.point.add(new paper.Point(0, -delta));
1020
+ const below = h.point.add(new paper.Point(0, delta));
1021
+ return {
1022
+ insideAbove: mainShape.contains(above),
1023
+ insideBelow: mainShape.contains(below)
1024
+ };
1025
+ });
1026
+ const idx = pickExitIndex(flags);
1027
+ if (idx < 0) return null;
1028
+ if (isBridgeDebugEnabled()) {
1029
+ console.debug("Geometry: Bridge ray", {
1030
+ x,
1031
+ validHits: validHits.length,
1032
+ idx,
1033
+ delta,
1034
+ overlap,
1035
+ op
1036
+ });
1135
1037
  }
1136
- return 0;
1038
+ const hit = validHits[idx];
1039
+ return { point: hit.point, location: hit };
1137
1040
  }
1138
- function readSizeState(configService) {
1139
- const unit = normalizeUnit(
1140
- configService.get("size.unit", DEFAULT_SIZE_STATE.unit)
1041
+ function selectOuterChain(args) {
1042
+ const { mainShape, pointsA, pointsB, delta, overlap, op } = args;
1043
+ const scoreA = scoreOutsideAbove(
1044
+ pointsA.map((p) => ({
1045
+ outsideAbove: !mainShape.contains(p.add(new paper.Point(0, -delta)))
1046
+ }))
1141
1047
  );
1142
- const minMm = Math.max(
1143
- 0.1,
1144
- Number(configService.get("size.minMm", DEFAULT_SIZE_STATE.minMm))
1048
+ const scoreB = scoreOutsideAbove(
1049
+ pointsB.map((p) => ({
1050
+ outsideAbove: !mainShape.contains(p.add(new paper.Point(0, -delta)))
1051
+ }))
1145
1052
  );
1146
- const maxMm = Math.max(
1147
- minMm,
1148
- Number(configService.get("size.maxMm", DEFAULT_SIZE_STATE.maxMm))
1053
+ const ratioA = scoreA / pointsA.length;
1054
+ const ratioB = scoreB / pointsB.length;
1055
+ if (isBridgeDebugEnabled()) {
1056
+ console.debug("Geometry: Bridge chain", {
1057
+ scoreA,
1058
+ scoreB,
1059
+ lenA: pointsA.length,
1060
+ lenB: pointsB.length,
1061
+ ratioA,
1062
+ ratioB,
1063
+ delta,
1064
+ overlap,
1065
+ op
1066
+ });
1067
+ }
1068
+ const ratioEps = 1e-6;
1069
+ if (Math.abs(ratioA - ratioB) > ratioEps) {
1070
+ return ratioA > ratioB ? pointsA : pointsB;
1071
+ }
1072
+ if (scoreA !== scoreB) return scoreA > scoreB ? pointsA : pointsB;
1073
+ return pointsA.length <= pointsB.length ? pointsA : pointsB;
1074
+ }
1075
+ function fitPathItemToRect(item, rect, fitMode) {
1076
+ const { left, top, width, height } = rect;
1077
+ const bounds = item.bounds;
1078
+ if (width <= 0 || height <= 0 || !Number.isFinite(bounds.width) || !Number.isFinite(bounds.height) || bounds.width <= 0 || bounds.height <= 0) {
1079
+ item.position = new paper.Point(left + width / 2, top + height / 2);
1080
+ return item;
1081
+ }
1082
+ item.translate(new paper.Point(-bounds.left, -bounds.top));
1083
+ if (fitMode === "stretch") {
1084
+ item.scale(width / bounds.width, height / bounds.height, new paper.Point(0, 0));
1085
+ item.translate(new paper.Point(left, top));
1086
+ return item;
1087
+ }
1088
+ const uniformScale = Math.min(width / bounds.width, height / bounds.height);
1089
+ item.scale(uniformScale, uniformScale, new paper.Point(0, 0));
1090
+ const scaledWidth = bounds.width * uniformScale;
1091
+ const scaledHeight = bounds.height * uniformScale;
1092
+ item.translate(
1093
+ new paper.Point(
1094
+ left + (width - scaledWidth) / 2,
1095
+ top + (height - scaledHeight) / 2
1096
+ )
1149
1097
  );
1150
- const stepMm = Math.max(
1151
- 1e-3,
1152
- Number(configService.get("size.stepMm", DEFAULT_SIZE_STATE.stepMm))
1098
+ return item;
1099
+ }
1100
+ function createNormalizedHeartPath(params) {
1101
+ const { lobeSpread, notchDepth, tipSharpness } = params;
1102
+ const halfSpread = 0.22 + lobeSpread * 0.18;
1103
+ const notchY = 0.06 + notchDepth * 0.2;
1104
+ const shoulderY = 0.24 + notchDepth * 0.2;
1105
+ const topLift = 0.12 + (1 - notchDepth) * 0.06;
1106
+ const topY = notchY - topLift;
1107
+ const sideCtrlY = shoulderY - (0.18 - notchDepth * 0.08);
1108
+ const lowerCtrlY = 0.58 + (1 - tipSharpness) * 0.16;
1109
+ const tipCtrlX = 0.34 - tipSharpness * 0.2;
1110
+ const notchCtrlX = 0.06 + lobeSpread * 0.06;
1111
+ const lobeCtrlX = 0.1 + lobeSpread * 0.08;
1112
+ const notchCtrlY = notchY - topLift * 0.45;
1113
+ const xPeakL = 0.5 - halfSpread;
1114
+ const xPeakR = 0.5 + halfSpread;
1115
+ const heartPath = new paper.Path({ insert: false });
1116
+ heartPath.moveTo(new paper.Point(0.5, notchY));
1117
+ heartPath.cubicCurveTo(
1118
+ new paper.Point(0.5 - notchCtrlX, notchCtrlY),
1119
+ new paper.Point(xPeakL + lobeCtrlX, topY),
1120
+ new paper.Point(xPeakL, topY)
1153
1121
  );
1154
- const actualWidthMm = sanitizeMmValue(
1155
- parseLengthToMm(
1156
- configService.get("size.actualWidthMm", DEFAULT_SIZE_STATE.actualWidthMm),
1157
- "mm"
1158
- ),
1159
- { minMm, maxMm, stepMm }
1122
+ heartPath.cubicCurveTo(
1123
+ new paper.Point(xPeakL - lobeCtrlX, topY),
1124
+ new paper.Point(0, sideCtrlY),
1125
+ new paper.Point(0, shoulderY)
1160
1126
  );
1161
- const actualHeightMm = sanitizeMmValue(
1162
- parseLengthToMm(
1163
- configService.get(
1164
- "size.actualHeightMm",
1165
- DEFAULT_SIZE_STATE.actualHeightMm
1166
- ),
1167
- "mm"
1168
- ),
1169
- { minMm, maxMm, stepMm }
1127
+ heartPath.cubicCurveTo(
1128
+ new paper.Point(0, lowerCtrlY),
1129
+ new paper.Point(tipCtrlX, 1),
1130
+ new paper.Point(0.5, 1)
1170
1131
  );
1171
- const aspectRaw = Number(
1172
- configService.get("size.aspectRatio", DEFAULT_SIZE_STATE.aspectRatio)
1132
+ heartPath.cubicCurveTo(
1133
+ new paper.Point(1 - tipCtrlX, 1),
1134
+ new paper.Point(1, lowerCtrlY),
1135
+ new paper.Point(1, shoulderY)
1173
1136
  );
1174
- const aspectRatio = Number.isFinite(aspectRaw) && aspectRaw > 0 ? aspectRaw : actualWidthMm / Math.max(1e-3, actualHeightMm);
1175
- const cutMarginMm = Math.max(
1176
- 0,
1177
- parseLengthToMm(
1178
- configService.get("size.cutMarginMm", DEFAULT_SIZE_STATE.cutMarginMm),
1179
- "mm"
1180
- )
1137
+ heartPath.cubicCurveTo(
1138
+ new paper.Point(1, sideCtrlY),
1139
+ new paper.Point(xPeakR + lobeCtrlX, topY),
1140
+ new paper.Point(xPeakR, topY)
1181
1141
  );
1182
- const viewPadding = configService.get(
1183
- "size.viewPadding",
1184
- DEFAULT_SIZE_STATE.viewPadding
1142
+ heartPath.cubicCurveTo(
1143
+ new paper.Point(xPeakR - lobeCtrlX, topY),
1144
+ new paper.Point(0.5 + notchCtrlX, notchCtrlY),
1145
+ new paper.Point(0.5, notchY)
1185
1146
  );
1186
- return {
1187
- unit,
1188
- actualWidthMm,
1189
- actualHeightMm,
1190
- constraintMode: normalizeConstraintMode(
1191
- configService.get(
1192
- "size.constraintMode",
1193
- DEFAULT_SIZE_STATE.constraintMode
1194
- )
1195
- ),
1196
- aspectRatio,
1197
- cutMode: normalizeCutMode(
1198
- configService.get("size.cutMode", DEFAULT_SIZE_STATE.cutMode)
1199
- ),
1200
- cutMarginMm,
1201
- viewPadding,
1202
- minMm,
1203
- maxMm,
1204
- stepMm
1205
- };
1147
+ heartPath.closed = true;
1148
+ return heartPath;
1206
1149
  }
1207
- function rectByCenter(centerX, centerY, width, height) {
1208
- return {
1209
- left: centerX - width / 2,
1210
- top: centerY - height / 2,
1150
+ function createHeartBaseShape(options) {
1151
+ const { x, y, width, height } = options;
1152
+ const w = Math.max(0, width);
1153
+ const h = Math.max(0, height);
1154
+ const left = x - w / 2;
1155
+ const top = y - h / 2;
1156
+ const fitMode = getShapeFitMode(options.shapeStyle);
1157
+ const heartParams = getHeartShapeParams(options.shapeStyle);
1158
+ const rawHeart = createNormalizedHeartPath(heartParams);
1159
+ return fitPathItemToRect(rawHeart, { left, top, width: w, height: h }, fitMode);
1160
+ }
1161
+ var BUILTIN_SHAPE_BUILDERS = {
1162
+ rect: (options) => {
1163
+ const { x, y, width, height, radius } = options;
1164
+ return new paper.Path.Rectangle({
1165
+ point: [x - width / 2, y - height / 2],
1166
+ size: [Math.max(0, width), Math.max(0, height)],
1167
+ radius: Math.max(0, radius)
1168
+ });
1169
+ },
1170
+ circle: (options) => {
1171
+ const { x, y, width, height } = options;
1172
+ const r = Math.min(width, height) / 2;
1173
+ return new paper.Path.Circle({
1174
+ center: new paper.Point(x, y),
1175
+ radius: Math.max(0, r)
1176
+ });
1177
+ },
1178
+ ellipse: (options) => {
1179
+ const { x, y, width, height } = options;
1180
+ return new paper.Path.Ellipse({
1181
+ center: new paper.Point(x, y),
1182
+ radius: [Math.max(0, width / 2), Math.max(0, height / 2)]
1183
+ });
1184
+ },
1185
+ heart: createHeartBaseShape
1186
+ };
1187
+ function createCustomBaseShape(options) {
1188
+ var _a;
1189
+ const {
1190
+ pathData,
1191
+ customSourceWidthPx,
1192
+ customSourceHeightPx,
1193
+ x,
1194
+ y,
1211
1195
  width,
1212
- height,
1213
- centerX,
1214
- centerY
1215
- };
1196
+ height
1197
+ } = options;
1198
+ if (typeof pathData !== "string" || pathData.trim().length === 0) {
1199
+ return null;
1200
+ }
1201
+ const center = new paper.Point(x, y);
1202
+ const hasMultipleSubPaths = ((_a = (pathData.match(/[Mm]/g) || []).length) != null ? _a : 0) > 1;
1203
+ const path = hasMultipleSubPaths ? new paper.CompoundPath(pathData) : (() => {
1204
+ const single = new paper.Path();
1205
+ single.pathData = pathData;
1206
+ return single;
1207
+ })();
1208
+ const sourceWidth = Number(customSourceWidthPx != null ? customSourceWidthPx : 0);
1209
+ const sourceHeight = Number(customSourceHeightPx != null ? customSourceHeightPx : 0);
1210
+ if (Number.isFinite(sourceWidth) && Number.isFinite(sourceHeight) && sourceWidth > 0 && sourceHeight > 0 && width > 0 && height > 0) {
1211
+ const targetLeft = x - width / 2;
1212
+ const targetTop = y - height / 2;
1213
+ path.scale(width / sourceWidth, height / sourceHeight, new paper.Point(0, 0));
1214
+ path.translate(new paper.Point(targetLeft, targetTop));
1215
+ return path;
1216
+ }
1217
+ if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
1218
+ path.position = center;
1219
+ path.scale(width / path.bounds.width, height / path.bounds.height);
1220
+ return path;
1221
+ }
1222
+ path.position = center;
1223
+ return path;
1216
1224
  }
1217
- function getCutSizeMm(size) {
1218
- if (size.cutMode === "trim") {
1219
- return { widthMm: size.actualWidthMm, heightMm: size.actualHeightMm };
1225
+ function createBaseShape(options) {
1226
+ const { shape } = options;
1227
+ if (shape === "custom") {
1228
+ const customShape = createCustomBaseShape(options);
1229
+ if (customShape) return customShape;
1230
+ return BUILTIN_SHAPE_BUILDERS[DEFAULT_DIELINE_SHAPE](options);
1220
1231
  }
1221
- const delta = size.cutMarginMm * 2;
1222
- if (size.cutMode === "outset") {
1223
- return {
1224
- widthMm: size.actualWidthMm + delta,
1225
- heightMm: size.actualHeightMm + delta
1226
- };
1232
+ return BUILTIN_SHAPE_BUILDERS[shape](options);
1233
+ }
1234
+ function resolveBridgeBasePath(shape, anchor) {
1235
+ if (shape instanceof paper.Path) {
1236
+ return shape;
1237
+ }
1238
+ if (shape instanceof paper.CompoundPath) {
1239
+ const children = (shape.children || []).filter(
1240
+ (child) => child instanceof paper.Path
1241
+ );
1242
+ if (!children.length) return null;
1243
+ let best = children[0];
1244
+ let bestDistance = Infinity;
1245
+ for (const child of children) {
1246
+ const location = child.getNearestLocation(anchor);
1247
+ const point = location == null ? void 0 : location.point;
1248
+ if (!point) continue;
1249
+ const distance = point.getDistance(anchor);
1250
+ if (distance < bestDistance) {
1251
+ bestDistance = distance;
1252
+ best = child;
1253
+ }
1254
+ }
1255
+ return best;
1256
+ }
1257
+ return null;
1258
+ }
1259
+ function createFeatureItem(feature, center) {
1260
+ let item;
1261
+ if (feature.shape === "rect") {
1262
+ const w = feature.width || 10;
1263
+ const h = feature.height || 10;
1264
+ const r = feature.radius || 0;
1265
+ item = new paper.Path.Rectangle({
1266
+ point: [center.x - w / 2, center.y - h / 2],
1267
+ size: [w, h],
1268
+ radius: r
1269
+ });
1270
+ } else {
1271
+ const r = feature.radius || 5;
1272
+ item = new paper.Path.Circle({
1273
+ center,
1274
+ radius: r
1275
+ });
1276
+ }
1277
+ if (feature.rotation) {
1278
+ item.rotate(feature.rotation, center);
1279
+ }
1280
+ return item;
1281
+ }
1282
+ function getPerimeterShape(options) {
1283
+ let mainShape = createBaseShape(options);
1284
+ const { features } = options;
1285
+ if (features && features.length > 0) {
1286
+ const edgeFeatures = features.filter(
1287
+ (f) => !f.renderBehavior || f.renderBehavior === "edge"
1288
+ );
1289
+ const adds = [];
1290
+ const subtracts = [];
1291
+ edgeFeatures.forEach((f) => {
1292
+ const pos = resolveFeaturePosition(f, options);
1293
+ const center = new paper.Point(pos.x, pos.y);
1294
+ const item = createFeatureItem(f, center);
1295
+ if (f.bridge && f.bridge.type === "vertical") {
1296
+ const itemBounds = item.bounds;
1297
+ const mainBounds = mainShape.bounds;
1298
+ const bridgeTop = mainBounds.top;
1299
+ const bridgeBottom = itemBounds.top;
1300
+ if (bridgeBottom > bridgeTop) {
1301
+ const overlap = 2;
1302
+ const rayPadding = 10;
1303
+ const eps = 0.1;
1304
+ const delta = getBridgeDelta(itemBounds, overlap);
1305
+ const toY = bridgeTop - rayPadding;
1306
+ const inset = Math.min(1, Math.max(0, itemBounds.width * 0.01));
1307
+ const xLeft = itemBounds.left + inset;
1308
+ const xRight = itemBounds.right - inset;
1309
+ const bridgeBasePath = resolveBridgeBasePath(mainShape, center);
1310
+ const canBridge = !!bridgeBasePath && xRight - xLeft > eps;
1311
+ if (canBridge && bridgeBasePath) {
1312
+ const leftHit = getExitHit({
1313
+ mainShape: bridgeBasePath,
1314
+ x: xLeft,
1315
+ bridgeBottom,
1316
+ toY,
1317
+ eps,
1318
+ delta,
1319
+ overlap,
1320
+ op: f.operation
1321
+ });
1322
+ const rightHit = getExitHit({
1323
+ mainShape: bridgeBasePath,
1324
+ x: xRight,
1325
+ bridgeBottom,
1326
+ toY,
1327
+ eps,
1328
+ delta,
1329
+ overlap,
1330
+ op: f.operation
1331
+ });
1332
+ if (leftHit && rightHit) {
1333
+ const pathLength = bridgeBasePath.length;
1334
+ const leftOffset = leftHit.location.offset;
1335
+ const rightOffset = rightHit.location.offset;
1336
+ const distanceA = wrappedDistance(pathLength, leftOffset, rightOffset);
1337
+ const distanceB = wrappedDistance(pathLength, rightOffset, leftOffset);
1338
+ const countFor = (d) => Math.max(8, Math.min(80, Math.ceil(d / 6)));
1339
+ const offsetsA = sampleWrappedOffsets(
1340
+ pathLength,
1341
+ leftOffset,
1342
+ rightOffset,
1343
+ countFor(distanceA)
1344
+ );
1345
+ const offsetsB = sampleWrappedOffsets(
1346
+ pathLength,
1347
+ rightOffset,
1348
+ leftOffset,
1349
+ countFor(distanceB)
1350
+ );
1351
+ const pointsA = offsetsA.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
1352
+ const pointsB = offsetsB.map((o) => bridgeBasePath.getPointAt(o)).filter((p) => Boolean(p));
1353
+ if (pointsA.length >= 2 && pointsB.length >= 2) {
1354
+ let topBase = selectOuterChain({
1355
+ mainShape: bridgeBasePath,
1356
+ pointsA,
1357
+ pointsB,
1358
+ delta,
1359
+ overlap,
1360
+ op: f.operation
1361
+ });
1362
+ const dist2 = (a, b) => {
1363
+ const dx = a.x - b.x;
1364
+ const dy = a.y - b.y;
1365
+ return dx * dx + dy * dy;
1366
+ };
1367
+ if (dist2(topBase[0], leftHit.point) > dist2(topBase[0], rightHit.point)) {
1368
+ topBase = topBase.slice().reverse();
1369
+ }
1370
+ topBase = topBase.slice();
1371
+ topBase[0] = leftHit.point;
1372
+ topBase[topBase.length - 1] = rightHit.point;
1373
+ const capShiftY = f.operation === "subtract" ? -Math.max(overlap * 2, delta) : overlap;
1374
+ const topPoints = topBase.map(
1375
+ (p) => p.add(new paper.Point(0, capShiftY))
1376
+ );
1377
+ const bridgeBottomY = bridgeBottom + overlap * 2;
1378
+ const bridgePoly = new paper.Path({ insert: false });
1379
+ for (const p of topPoints) bridgePoly.add(p);
1380
+ bridgePoly.add(new paper.Point(xRight, bridgeBottomY));
1381
+ bridgePoly.add(new paper.Point(xLeft, bridgeBottomY));
1382
+ bridgePoly.closed = true;
1383
+ const unitedItem = item.unite(bridgePoly);
1384
+ item.remove();
1385
+ bridgePoly.remove();
1386
+ if (f.operation === "add") {
1387
+ adds.push(unitedItem);
1388
+ } else {
1389
+ subtracts.push(unitedItem);
1390
+ }
1391
+ return;
1392
+ }
1393
+ }
1394
+ }
1395
+ if (f.operation === "add") {
1396
+ adds.push(item);
1397
+ } else {
1398
+ subtracts.push(item);
1399
+ }
1400
+ } else {
1401
+ if (f.operation === "add") {
1402
+ adds.push(item);
1403
+ } else {
1404
+ subtracts.push(item);
1405
+ }
1406
+ }
1407
+ } else {
1408
+ if (f.operation === "add") {
1409
+ adds.push(item);
1410
+ } else {
1411
+ subtracts.push(item);
1412
+ }
1413
+ }
1414
+ });
1415
+ if (adds.length > 0) {
1416
+ for (const item of adds) {
1417
+ try {
1418
+ const temp = mainShape.unite(item);
1419
+ mainShape.remove();
1420
+ item.remove();
1421
+ mainShape = normalizePathItem(temp);
1422
+ } catch (e) {
1423
+ console.error("Geometry: Failed to unite feature", e);
1424
+ item.remove();
1425
+ }
1426
+ }
1427
+ }
1428
+ if (subtracts.length > 0) {
1429
+ for (const item of subtracts) {
1430
+ try {
1431
+ const temp = mainShape.subtract(item);
1432
+ mainShape.remove();
1433
+ item.remove();
1434
+ mainShape = normalizePathItem(temp);
1435
+ } catch (e) {
1436
+ console.error("Geometry: Failed to subtract feature", e);
1437
+ item.remove();
1438
+ }
1439
+ }
1440
+ }
1227
1441
  }
1228
- return {
1229
- widthMm: Math.max(size.minMm, size.actualWidthMm - delta),
1230
- heightMm: Math.max(size.minMm, size.actualHeightMm - delta)
1231
- };
1442
+ return mainShape;
1232
1443
  }
1233
- function computeSceneLayout(canvasService, size) {
1234
- const canvasWidth = canvasService.canvas.width || 0;
1235
- const canvasHeight = canvasService.canvas.height || 0;
1236
- if (canvasWidth <= 0 || canvasHeight <= 0) return null;
1237
- const { widthMm: cutWidthMm, heightMm: cutHeightMm } = getCutSizeMm(size);
1238
- const viewWidthMm = Math.max(size.actualWidthMm, cutWidthMm);
1239
- const viewHeightMm = Math.max(size.actualHeightMm, cutHeightMm);
1240
- if (!Number.isFinite(viewWidthMm) || !Number.isFinite(viewHeightMm) || viewWidthMm <= 0 || viewHeightMm <= 0) {
1241
- return null;
1242
- }
1243
- const paddingPx = resolvePaddingPx(
1244
- size.viewPadding,
1245
- canvasWidth,
1246
- canvasHeight
1444
+ function applySurfaceFeatures(shape, features, options) {
1445
+ const surfaceFeatures = features.filter(
1446
+ (f) => f.renderBehavior === "surface"
1247
1447
  );
1248
- canvasService.viewport.updateContainer(canvasWidth, canvasHeight);
1249
- canvasService.viewport.setPadding(paddingPx);
1250
- canvasService.viewport.updatePhysical(viewWidthMm, viewHeightMm);
1251
- const layout = canvasService.viewport.layout;
1252
- if (!Number.isFinite(layout.scale) || !Number.isFinite(layout.offsetX) || !Number.isFinite(layout.offsetY) || layout.scale <= 0) {
1253
- return null;
1448
+ if (surfaceFeatures.length === 0) return shape;
1449
+ let result = shape;
1450
+ for (const f of surfaceFeatures) {
1451
+ const pos = resolveFeaturePosition(f, options);
1452
+ const center = new paper.Point(pos.x, pos.y);
1453
+ const item = createFeatureItem(f, center);
1454
+ try {
1455
+ if (f.operation === "add") {
1456
+ const temp = result.unite(item);
1457
+ result.remove();
1458
+ item.remove();
1459
+ result = normalizePathItem(temp);
1460
+ } else {
1461
+ const temp = result.subtract(item);
1462
+ result.remove();
1463
+ item.remove();
1464
+ result = normalizePathItem(temp);
1465
+ }
1466
+ } catch (e) {
1467
+ console.error("Geometry: Failed to apply surface feature", e);
1468
+ item.remove();
1469
+ }
1254
1470
  }
1255
- const centerX = layout.offsetX + layout.width / 2;
1256
- const centerY = layout.offsetY + layout.height / 2;
1257
- const trimWidthPx = size.actualWidthMm * layout.scale;
1258
- const trimHeightPx = size.actualHeightMm * layout.scale;
1259
- const cutWidthPx = cutWidthMm * layout.scale;
1260
- const cutHeightPx = cutHeightMm * layout.scale;
1261
- const trimRect = rectByCenter(centerX, centerY, trimWidthPx, trimHeightPx);
1262
- const cutRect = rectByCenter(centerX, centerY, cutWidthPx, cutHeightPx);
1263
- const bleedRect = rectByCenter(
1264
- centerX,
1265
- centerY,
1266
- Math.max(trimWidthPx, cutWidthPx),
1267
- Math.max(trimHeightPx, cutHeightPx)
1268
- );
1269
- return {
1270
- scale: layout.scale,
1271
- canvasWidth,
1272
- canvasHeight,
1273
- trimRect,
1274
- cutRect,
1275
- bleedRect,
1276
- trimWidthMm: size.actualWidthMm,
1277
- trimHeightMm: size.actualHeightMm,
1278
- cutWidthMm,
1279
- cutHeightMm,
1280
- cutMode: size.cutMode,
1281
- cutMarginMm: size.cutMarginMm
1282
- };
1471
+ return result;
1283
1472
  }
1284
- function buildSceneGeometry(configService, layout) {
1285
- const radiusMm = parseLengthToMm(
1286
- configService.get("dieline.radius", 0),
1287
- "mm"
1288
- );
1289
- const offset = (layout.cutRect.width - layout.trimRect.width) / 2;
1290
- const sourceWidth = Number(configService.get("dieline.customSourceWidthPx", 0));
1291
- const sourceHeight = Number(
1292
- configService.get("dieline.customSourceHeightPx", 0)
1473
+ function generateDielinePath(options) {
1474
+ const paperWidth = options.canvasWidth || options.width * 2 || 2e3;
1475
+ const paperHeight = options.canvasHeight || options.height * 2 || 2e3;
1476
+ ensurePaper(paperWidth, paperHeight);
1477
+ paper.project.activeLayer.removeChildren();
1478
+ const perimeter = getPerimeterShape(options);
1479
+ const finalShape = applySurfaceFeatures(perimeter, options.features, options);
1480
+ const pathData = finalShape.pathData;
1481
+ finalShape.remove();
1482
+ return pathData;
1483
+ }
1484
+ function generateBleedZonePath(originalOptions, offsetOptions, offset) {
1485
+ const paperWidth = originalOptions.canvasWidth || originalOptions.width * 2 || 2e3;
1486
+ const paperHeight = originalOptions.canvasHeight || originalOptions.height * 2 || 2e3;
1487
+ ensurePaper(paperWidth, paperHeight);
1488
+ paper.project.activeLayer.removeChildren();
1489
+ const pOriginal = getPerimeterShape(originalOptions);
1490
+ const shapeOriginal = applySurfaceFeatures(
1491
+ pOriginal,
1492
+ originalOptions.features,
1493
+ originalOptions
1293
1494
  );
1294
- const shapeStyle = normalizeShapeStyle(
1295
- configService.get("dieline.shapeStyle", DEFAULT_DIELINE_SHAPE_STYLE)
1495
+ const pOffset = getPerimeterShape(offsetOptions);
1496
+ const shapeOffset = applySurfaceFeatures(
1497
+ pOffset,
1498
+ offsetOptions.features,
1499
+ offsetOptions
1296
1500
  );
1501
+ let bleedZone;
1502
+ if (offset > 0) {
1503
+ bleedZone = shapeOffset.subtract(shapeOriginal);
1504
+ } else {
1505
+ bleedZone = shapeOriginal.subtract(shapeOffset);
1506
+ }
1507
+ const pathData = bleedZone.pathData;
1508
+ shapeOriginal.remove();
1509
+ shapeOffset.remove();
1510
+ bleedZone.remove();
1511
+ return pathData;
1512
+ }
1513
+ function getLowestPointOnDieline(options) {
1514
+ ensurePaper(options.width * 2, options.height * 2);
1515
+ paper.project.activeLayer.removeChildren();
1516
+ const shape = createBaseShape(options);
1517
+ const bounds = shape.bounds;
1518
+ const result = {
1519
+ x: bounds.center.x,
1520
+ y: bounds.bottom
1521
+ };
1522
+ shape.remove();
1523
+ return result;
1524
+ }
1525
+ function getNearestPointOnDieline(point, options) {
1526
+ ensurePaper(options.width * 2, options.height * 2);
1527
+ paper.project.activeLayer.removeChildren();
1528
+ const shape = createBaseShape(options);
1529
+ const p = new paper.Point(point.x, point.y);
1530
+ const location = shape.getNearestLocation(p);
1531
+ const result = {
1532
+ x: location.point.x,
1533
+ y: location.point.y,
1534
+ normal: location.normal ? { x: location.normal.x, y: location.normal.y } : void 0
1535
+ };
1536
+ shape.remove();
1537
+ return result;
1538
+ }
1539
+ function getPathBounds(pathData) {
1540
+ const path = new paper.Path();
1541
+ path.pathData = pathData;
1542
+ const bounds = path.bounds;
1543
+ path.remove();
1297
1544
  return {
1298
- shape: normalizeDielineShape(
1299
- configService.get("dieline.shape", DEFAULT_DIELINE_SHAPE)
1300
- ),
1301
- shapeStyle,
1302
- unit: "px",
1303
- x: layout.trimRect.centerX,
1304
- y: layout.trimRect.centerY,
1305
- width: layout.trimRect.width,
1306
- height: layout.trimRect.height,
1307
- radius: radiusMm * layout.scale,
1308
- offset,
1309
- scale: layout.scale,
1310
- pathData: configService.get("dieline.pathData"),
1311
- customSourceWidthPx: Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : void 0,
1312
- customSourceHeightPx: Number.isFinite(sourceHeight) && sourceHeight > 0 ? sourceHeight : void 0
1545
+ x: bounds.x,
1546
+ y: bounds.y,
1547
+ width: bounds.width,
1548
+ height: bounds.height
1313
1549
  };
1314
1550
  }
1315
1551
 
1316
1552
  // src/extensions/image.ts
1317
1553
  var IMAGE_OBJECT_LAYER_ID = "image.user";
1318
1554
  var IMAGE_OVERLAY_LAYER_ID = "image-overlay";
1555
+ var IMAGE_DEFAULT_CONTROL_CAPABILITIES = [
1556
+ "rotate",
1557
+ "scale"
1558
+ ];
1559
+ var IMAGE_CONTROL_DESCRIPTORS = [
1560
+ {
1561
+ key: "tl",
1562
+ capability: "rotate",
1563
+ create: () => new Control({
1564
+ x: -0.5,
1565
+ y: -0.5,
1566
+ actionName: "rotate",
1567
+ actionHandler: controlsUtils.rotationWithSnapping,
1568
+ cursorStyleHandler: controlsUtils.rotationStyleHandler
1569
+ })
1570
+ },
1571
+ {
1572
+ key: "br",
1573
+ capability: "scale",
1574
+ create: () => new Control({
1575
+ x: 0.5,
1576
+ y: 0.5,
1577
+ actionName: "scale",
1578
+ actionHandler: controlsUtils.scalingEqually,
1579
+ cursorStyleHandler: controlsUtils.scaleCursorStyleHandler
1580
+ })
1581
+ }
1582
+ ];
1319
1583
  var ImageTool = class {
1320
1584
  constructor() {
1321
1585
  this.id = "pooder.kit.image";
@@ -1332,7 +1596,9 @@ var ImageTool = class {
1332
1596
  this.isImageSelectionActive = false;
1333
1597
  this.focusedImageId = null;
1334
1598
  this.renderSeq = 0;
1599
+ this.imageSpecs = [];
1335
1600
  this.overlaySpecs = [];
1601
+ this.imageControlsByCapabilityKey = /* @__PURE__ */ new Map();
1336
1602
  this.onToolActivated = (event) => {
1337
1603
  const before = this.isToolActive;
1338
1604
  this.syncToolActiveFromWorkbench(event.id);
@@ -1439,9 +1705,34 @@ var ImageTool = class {
1439
1705
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
1440
1706
  this.id,
1441
1707
  () => ({
1442
- rootLayerSpecs: {
1443
- [IMAGE_OVERLAY_LAYER_ID]: this.overlaySpecs
1444
- }
1708
+ passes: [
1709
+ {
1710
+ id: IMAGE_OBJECT_LAYER_ID,
1711
+ stack: 500,
1712
+ order: 0,
1713
+ visibility: {
1714
+ op: "not",
1715
+ expr: {
1716
+ op: "sessionActive",
1717
+ toolId: "pooder.kit.white-ink"
1718
+ }
1719
+ },
1720
+ objects: this.imageSpecs
1721
+ },
1722
+ {
1723
+ id: IMAGE_OVERLAY_LAYER_ID,
1724
+ stack: 800,
1725
+ order: 0,
1726
+ visibility: {
1727
+ op: "not",
1728
+ expr: {
1729
+ op: "sessionActive",
1730
+ toolId: "pooder.kit.white-ink"
1731
+ }
1732
+ },
1733
+ objects: this.overlaySpecs
1734
+ }
1735
+ ]
1445
1736
  }),
1446
1737
  { priority: 300 }
1447
1738
  );
@@ -1472,7 +1763,10 @@ var ImageTool = class {
1472
1763
  this.updateImages();
1473
1764
  return;
1474
1765
  }
1475
- if (e.key.startsWith("size.") || e.key.startsWith("image.frame.")) {
1766
+ if (e.key.startsWith("size.") || e.key.startsWith("image.frame.") || e.key.startsWith("image.control.")) {
1767
+ if (e.key.startsWith("image.control.")) {
1768
+ this.imageControlsByCapabilityKey.clear();
1769
+ }
1476
1770
  this.updateImages();
1477
1771
  }
1478
1772
  });
@@ -1498,7 +1792,9 @@ var ImageTool = class {
1498
1792
  this.cropShapeHatchPattern = void 0;
1499
1793
  this.cropShapeHatchPatternColor = void 0;
1500
1794
  this.cropShapeHatchPatternKey = void 0;
1795
+ this.imageSpecs = [];
1501
1796
  this.overlaySpecs = [];
1797
+ this.imageControlsByCapabilityKey.clear();
1502
1798
  this.clearRenderedImages();
1503
1799
  (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
1504
1800
  this.renderProducerDisposable = void 0;
@@ -1521,6 +1817,90 @@ var ImageTool = class {
1521
1817
  isImageEditingVisible() {
1522
1818
  return this.isToolActive || this.isImageSelectionActive || !!this.focusedImageId;
1523
1819
  }
1820
+ getEnabledImageControlCapabilities() {
1821
+ return IMAGE_DEFAULT_CONTROL_CAPABILITIES;
1822
+ }
1823
+ getImageControls(capabilities) {
1824
+ const normalized = [...new Set(capabilities)].sort();
1825
+ const cacheKey = normalized.join("|");
1826
+ const cached = this.imageControlsByCapabilityKey.get(cacheKey);
1827
+ if (cached) {
1828
+ return cached;
1829
+ }
1830
+ const enabled = new Set(normalized);
1831
+ const controls = {};
1832
+ IMAGE_CONTROL_DESCRIPTORS.forEach((descriptor) => {
1833
+ if (!enabled.has(descriptor.capability)) return;
1834
+ controls[descriptor.key] = descriptor.create();
1835
+ });
1836
+ this.imageControlsByCapabilityKey.set(cacheKey, controls);
1837
+ return controls;
1838
+ }
1839
+ getImageControlVisualConfig() {
1840
+ var _a, _b, _c, _d;
1841
+ const cornerSizeRaw = Number(
1842
+ (_a = this.getConfig("image.control.cornerSize", 14)) != null ? _a : 14
1843
+ );
1844
+ const touchCornerSizeRaw = Number(
1845
+ (_b = this.getConfig("image.control.touchCornerSize", 24)) != null ? _b : 24
1846
+ );
1847
+ const borderScaleFactorRaw = Number(
1848
+ (_c = this.getConfig("image.control.borderScaleFactor", 1.5)) != null ? _c : 1.5
1849
+ );
1850
+ const paddingRaw = Number(
1851
+ (_d = this.getConfig("image.control.padding", 0)) != null ? _d : 0
1852
+ );
1853
+ const cornerStyleRaw = this.getConfig(
1854
+ "image.control.cornerStyle",
1855
+ "circle"
1856
+ ) || "circle";
1857
+ const cornerStyle = cornerStyleRaw === "rect" ? "rect" : "circle";
1858
+ return {
1859
+ cornerSize: Number.isFinite(cornerSizeRaw) ? Math.max(4, Math.min(64, cornerSizeRaw)) : 14,
1860
+ touchCornerSize: Number.isFinite(touchCornerSizeRaw) ? Math.max(8, Math.min(96, touchCornerSizeRaw)) : 24,
1861
+ cornerStyle,
1862
+ cornerColor: this.getConfig("image.control.cornerColor", "#ffffff") || "#ffffff",
1863
+ cornerStrokeColor: this.getConfig("image.control.cornerStrokeColor", "#1677ff") || "#1677ff",
1864
+ transparentCorners: !!this.getConfig(
1865
+ "image.control.transparentCorners",
1866
+ false
1867
+ ),
1868
+ borderColor: this.getConfig("image.control.borderColor", "#1677ff") || "#1677ff",
1869
+ borderScaleFactor: Number.isFinite(borderScaleFactorRaw) ? Math.max(0.5, Math.min(8, borderScaleFactorRaw)) : 1.5,
1870
+ padding: Number.isFinite(paddingRaw) ? Math.max(0, Math.min(64, paddingRaw)) : 0
1871
+ };
1872
+ }
1873
+ applyImageObjectInteractionState(obj) {
1874
+ var _a;
1875
+ if (!obj) return;
1876
+ const visible = this.isImageEditingVisible();
1877
+ const visual = this.getImageControlVisualConfig();
1878
+ obj.set({
1879
+ selectable: visible,
1880
+ evented: visible,
1881
+ hasControls: visible,
1882
+ hasBorders: visible,
1883
+ lockScalingFlip: true,
1884
+ cornerSize: visual.cornerSize,
1885
+ touchCornerSize: visual.touchCornerSize,
1886
+ cornerStyle: visual.cornerStyle,
1887
+ cornerColor: visual.cornerColor,
1888
+ cornerStrokeColor: visual.cornerStrokeColor,
1889
+ transparentCorners: visual.transparentCorners,
1890
+ borderColor: visual.borderColor,
1891
+ borderScaleFactor: visual.borderScaleFactor,
1892
+ padding: visual.padding
1893
+ });
1894
+ obj.controls = this.getImageControls(
1895
+ this.getEnabledImageControlCapabilities()
1896
+ );
1897
+ (_a = obj.setCoords) == null ? void 0 : _a.call(obj);
1898
+ }
1899
+ refreshImageObjectInteractionState() {
1900
+ this.getImageObjects().forEach(
1901
+ (obj) => this.applyImageObjectInteractionState(obj)
1902
+ );
1903
+ }
1524
1904
  isDebugEnabled() {
1525
1905
  return !!this.getConfig("image.debug", false);
1526
1906
  }
@@ -1552,17 +1932,84 @@ var ImageTool = class {
1552
1932
  ],
1553
1933
  [ContributionPointIds2.CONFIGURATIONS]: [
1554
1934
  {
1555
- id: "image.items",
1556
- type: "array",
1557
- label: "Images",
1558
- default: []
1935
+ id: "image.items",
1936
+ type: "array",
1937
+ label: "Images",
1938
+ default: []
1939
+ },
1940
+ {
1941
+ id: "image.debug",
1942
+ type: "boolean",
1943
+ label: "Image Debug Log",
1944
+ default: false
1945
+ },
1946
+ {
1947
+ id: "image.control.cornerSize",
1948
+ type: "number",
1949
+ label: "Image Control Corner Size",
1950
+ min: 4,
1951
+ max: 64,
1952
+ step: 1,
1953
+ default: 14
1954
+ },
1955
+ {
1956
+ id: "image.control.touchCornerSize",
1957
+ type: "number",
1958
+ label: "Image Control Touch Corner Size",
1959
+ min: 8,
1960
+ max: 96,
1961
+ step: 1,
1962
+ default: 24
1963
+ },
1964
+ {
1965
+ id: "image.control.cornerStyle",
1966
+ type: "select",
1967
+ label: "Image Control Corner Style",
1968
+ options: ["circle", "rect"],
1969
+ default: "circle"
1559
1970
  },
1560
1971
  {
1561
- id: "image.debug",
1972
+ id: "image.control.cornerColor",
1973
+ type: "color",
1974
+ label: "Image Control Corner Color",
1975
+ default: "#ffffff"
1976
+ },
1977
+ {
1978
+ id: "image.control.cornerStrokeColor",
1979
+ type: "color",
1980
+ label: "Image Control Corner Stroke Color",
1981
+ default: "#1677ff"
1982
+ },
1983
+ {
1984
+ id: "image.control.transparentCorners",
1562
1985
  type: "boolean",
1563
- label: "Image Debug Log",
1986
+ label: "Image Control Transparent Corners",
1564
1987
  default: false
1565
1988
  },
1989
+ {
1990
+ id: "image.control.borderColor",
1991
+ type: "color",
1992
+ label: "Image Control Border Color",
1993
+ default: "#1677ff"
1994
+ },
1995
+ {
1996
+ id: "image.control.borderScaleFactor",
1997
+ type: "number",
1998
+ label: "Image Control Border Width",
1999
+ min: 0.5,
2000
+ max: 8,
2001
+ step: 0.1,
2002
+ default: 1.5
2003
+ },
2004
+ {
2005
+ id: "image.control.padding",
2006
+ type: "number",
2007
+ label: "Image Control Padding",
2008
+ min: 0,
2009
+ max: 64,
2010
+ step: 1,
2011
+ default: 0
2012
+ },
1566
2013
  {
1567
2014
  id: "image.frame.strokeColor",
1568
2015
  type: "color",
@@ -1800,12 +2247,7 @@ var ImageTool = class {
1800
2247
  } else {
1801
2248
  const obj = this.getImageObject(id);
1802
2249
  if (obj) {
1803
- obj.set({
1804
- selectable: true,
1805
- evented: true,
1806
- hasControls: true,
1807
- hasBorders: true
1808
- });
2250
+ this.applyImageObjectInteractionState(obj);
1809
2251
  canvas.setActiveObject(obj);
1810
2252
  }
1811
2253
  }
@@ -1973,9 +2415,7 @@ var ImageTool = class {
1973
2415
  }
1974
2416
  getOverlayObjects() {
1975
2417
  if (!this.canvasService) return [];
1976
- return this.canvasService.getRootLayerObjects(
1977
- IMAGE_OVERLAY_LAYER_ID
1978
- );
2418
+ return this.canvasService.getPassObjects(IMAGE_OVERLAY_LAYER_ID);
1979
2419
  }
1980
2420
  getImageObject(id) {
1981
2421
  return this.getImageObjects().find((obj) => {
@@ -1985,9 +2425,9 @@ var ImageTool = class {
1985
2425
  }
1986
2426
  clearRenderedImages() {
1987
2427
  if (!this.canvasService) return;
1988
- const canvas = this.canvasService.canvas;
1989
- this.getImageObjects().forEach((obj) => canvas.remove(obj));
1990
- this.canvasService.requestRenderAll();
2428
+ this.imageSpecs = [];
2429
+ this.overlaySpecs = [];
2430
+ this.canvasService.requestRenderFromProducers();
1991
2431
  }
1992
2432
  purgeSourceSizeCacheForItem(item) {
1993
2433
  if (!item) return;
@@ -2015,6 +2455,29 @@ var ImageTool = class {
2015
2455
  }
2016
2456
  return { width: 1, height: 1 };
2017
2457
  }
2458
+ async ensureSourceSize(src) {
2459
+ if (!src) return null;
2460
+ const cached = this.sourceSizeBySrc.get(src);
2461
+ if (cached) return cached;
2462
+ try {
2463
+ const image = await FabricImage2.fromURL(src, {
2464
+ crossOrigin: "anonymous"
2465
+ });
2466
+ const width = Number((image == null ? void 0 : image.width) || 0);
2467
+ const height = Number((image == null ? void 0 : image.height) || 0);
2468
+ if (width > 0 && height > 0) {
2469
+ const size = { width, height };
2470
+ this.sourceSizeBySrc.set(src, size);
2471
+ return size;
2472
+ }
2473
+ } catch (error) {
2474
+ this.debug("image:size:load-failed", {
2475
+ src,
2476
+ error: error instanceof Error ? error.message : String(error)
2477
+ });
2478
+ }
2479
+ return null;
2480
+ }
2018
2481
  getCoverScale(frame, size) {
2019
2482
  const sw = Math.max(1, size.width);
2020
2483
  const sh = Math.max(1, size.height);
@@ -2349,24 +2812,6 @@ var ImageTool = class {
2349
2812
  opacity: render.opacity
2350
2813
  };
2351
2814
  }
2352
- toScreenObjectProps(props) {
2353
- if (!this.canvasService) return props;
2354
- const next = { ...props };
2355
- if (Number.isFinite(next.left) || Number.isFinite(next.top)) {
2356
- const mapped = this.canvasService.toScreenPoint({
2357
- x: Number.isFinite(next.left) ? Number(next.left) : 0,
2358
- y: Number.isFinite(next.top) ? Number(next.top) : 0
2359
- });
2360
- if (Number.isFinite(next.left)) next.left = mapped.x;
2361
- if (Number.isFinite(next.top)) next.top = mapped.y;
2362
- }
2363
- const sceneScale = this.canvasService.getSceneScale();
2364
- const sx = Number.isFinite(next.scaleX) ? Number(next.scaleX) : 1;
2365
- const sy = Number.isFinite(next.scaleY) ? Number(next.scaleY) : 1;
2366
- next.scaleX = sx * sceneScale;
2367
- next.scaleY = sy * sceneScale;
2368
- return next;
2369
- }
2370
2815
  toSceneObjectScale(value) {
2371
2816
  if (!this.canvasService) return value;
2372
2817
  return value / this.canvasService.getSceneScale();
@@ -2377,104 +2822,27 @@ var ImageTool = class {
2377
2822
  if (typeof obj.getSrc === "function") return obj.getSrc();
2378
2823
  return (_a = obj == null ? void 0 : obj._originalElement) == null ? void 0 : _a.src;
2379
2824
  }
2380
- applyImageControlVisibility(obj) {
2381
- if (typeof (obj == null ? void 0 : obj.setControlsVisibility) !== "function") return;
2382
- obj.setControlsVisibility({
2383
- mt: false,
2384
- mb: false,
2385
- ml: false,
2386
- mr: false,
2387
- tl: true,
2388
- tr: true,
2389
- bl: true,
2390
- br: true,
2391
- mtr: true
2392
- });
2393
- }
2394
- async upsertImageObject(item, frame, seq) {
2395
- if (!this.canvasService) return;
2396
- const canvas = this.canvasService.canvas;
2397
- const render = this.resolveRenderImageState(item);
2398
- if (!render.src) return;
2399
- let obj = this.getImageObject(item.id);
2400
- const currentSrc = this.getCurrentSrc(obj);
2401
- if (obj && currentSrc && currentSrc !== render.src) {
2402
- canvas.remove(obj);
2403
- obj = void 0;
2404
- }
2405
- if (!obj) {
2406
- const created = await FabricImage2.fromURL(render.src, {
2407
- crossOrigin: "anonymous"
2408
- });
2409
- if (seq !== this.renderSeq) return;
2410
- created.set({
2825
+ async buildImageSpecs(items, frame) {
2826
+ const specs = [];
2827
+ for (const item of items) {
2828
+ const render = this.resolveRenderImageState(item);
2829
+ if (!render.src) continue;
2830
+ const ensured = await this.ensureSourceSize(render.src);
2831
+ const sourceSize = ensured || this.getSourceSize(render.src);
2832
+ const props = this.computeCanvasProps(render, sourceSize, frame);
2833
+ specs.push({
2834
+ id: item.id,
2835
+ type: "image",
2836
+ src: render.src,
2411
2837
  data: {
2412
2838
  id: item.id,
2413
2839
  layerId: IMAGE_OBJECT_LAYER_ID,
2414
2840
  type: "image-item"
2415
- }
2841
+ },
2842
+ props
2416
2843
  });
2417
- canvas.add(created);
2418
- obj = created;
2419
- }
2420
- this.rememberSourceSize(render.src, obj);
2421
- const sourceSize = this.getSourceSize(render.src, obj);
2422
- const props = this.computeCanvasProps(render, sourceSize, frame);
2423
- const screenProps = this.toScreenObjectProps(props);
2424
- obj.set({
2425
- ...screenProps,
2426
- data: {
2427
- ...obj.data || {},
2428
- id: item.id,
2429
- layerId: IMAGE_OBJECT_LAYER_ID,
2430
- type: "image-item"
2431
- }
2432
- });
2433
- this.applyImageControlVisibility(obj);
2434
- obj.setCoords();
2435
- const resolver = this.loadResolvers.get(item.id);
2436
- if (resolver) {
2437
- resolver();
2438
- this.loadResolvers.delete(item.id);
2439
- }
2440
- }
2441
- syncImageZOrder(items) {
2442
- if (!this.canvasService) return;
2443
- const canvas = this.canvasService.canvas;
2444
- const objects = canvas.getObjects();
2445
- let insertIndex = 0;
2446
- const backgroundLayer = this.canvasService.getLayer("background");
2447
- if (backgroundLayer) {
2448
- const bgIndex = objects.indexOf(backgroundLayer);
2449
- if (bgIndex >= 0) insertIndex = bgIndex + 1;
2450
- }
2451
- items.forEach((item) => {
2452
- const obj = this.getImageObject(item.id);
2453
- if (!obj) return;
2454
- canvas.moveObjectTo(obj, insertIndex);
2455
- insertIndex += 1;
2456
- });
2457
- const overlayObjects = this.getOverlayObjects().sort((a, b) => {
2458
- var _a, _b, _c, _d;
2459
- const az = Number((_b = (_a = a == null ? void 0 : a.data) == null ? void 0 : _a.zIndex) != null ? _b : 0);
2460
- const bz = Number((_d = (_c = b == null ? void 0 : b.data) == null ? void 0 : _c.zIndex) != null ? _d : 0);
2461
- return az - bz;
2462
- });
2463
- overlayObjects.forEach((obj) => {
2464
- canvas.bringObjectToFront(obj);
2465
- });
2466
- if (this.isDebugEnabled()) {
2467
- const stack = canvas.getObjects().map((obj, index) => {
2468
- var _a, _b, _c;
2469
- return {
2470
- index,
2471
- id: (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id,
2472
- layerId: (_b = obj == null ? void 0 : obj.data) == null ? void 0 : _b.layerId,
2473
- zIndex: (_c = obj == null ? void 0 : obj.data) == null ? void 0 : _c.zIndex
2474
- };
2475
- }).filter((item) => item.layerId === IMAGE_OVERLAY_LAYER_ID);
2476
- this.debug("overlay:stack", stack);
2477
2844
  }
2845
+ return specs;
2478
2846
  }
2479
2847
  buildOverlaySpecs(frame, sceneGeometry) {
2480
2848
  const visible = this.isImageEditingVisible();
@@ -2638,7 +3006,7 @@ var ImageTool = class {
2638
3006
  evented: false
2639
3007
  }
2640
3008
  };
2641
- const specs = [...mask, ...shapeOverlay, frameSpec];
3009
+ const specs = shapeOverlay.length > 0 ? [...mask, ...shapeOverlay] : [...mask, ...shapeOverlay, frameSpec];
2642
3010
  this.debug("overlay:built", {
2643
3011
  frame,
2644
3012
  shape: sceneGeometry == null ? void 0 : sceneGeometry.shape,
@@ -2668,30 +3036,33 @@ var ImageTool = class {
2668
3036
  skipRender: true
2669
3037
  });
2670
3038
  }
2671
- this.getImageObjects().forEach((obj) => {
2672
- var _a, _b;
2673
- const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
2674
- if (typeof id === "string" && !desiredIds.has(id)) {
2675
- (_b = this.canvasService) == null ? void 0 : _b.canvas.remove(obj);
2676
- }
2677
- });
2678
- for (const item of renderItems) {
2679
- if (seq !== this.renderSeq) return;
2680
- await this.upsertImageObject(item, frame, seq);
2681
- }
3039
+ const imageSpecs = await this.buildImageSpecs(renderItems, frame);
2682
3040
  if (seq !== this.renderSeq) return;
2683
- this.syncImageZOrder(renderItems);
2684
3041
  const sceneGeometry = await this.resolveSceneGeometryForOverlay();
2685
3042
  if (seq !== this.renderSeq) return;
2686
- const overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
2687
- this.overlaySpecs = overlaySpecs;
3043
+ this.imageSpecs = imageSpecs;
3044
+ this.overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
2688
3045
  await this.canvasService.flushRenderFromProducers();
2689
- this.syncImageZOrder(renderItems);
3046
+ if (seq !== this.renderSeq) return;
3047
+ this.refreshImageObjectInteractionState();
3048
+ renderItems.forEach((item) => {
3049
+ if (!this.getImageObject(item.id)) return;
3050
+ const resolver = this.loadResolvers.get(item.id);
3051
+ if (!resolver) return;
3052
+ resolver();
3053
+ this.loadResolvers.delete(item.id);
3054
+ });
3055
+ if (this.focusedImageId && this.isToolActive) {
3056
+ this.setImageFocus(this.focusedImageId, {
3057
+ syncCanvasSelection: true,
3058
+ skipRender: true
3059
+ });
3060
+ }
2690
3061
  const overlayCanvasCount = this.getOverlayObjects().length;
2691
3062
  this.debug("render:done", {
2692
3063
  seq,
2693
3064
  renderCount: renderItems.length,
2694
- overlayCount: overlaySpecs.length,
3065
+ overlayCount: this.overlaySpecs.length,
2695
3066
  overlayCanvasCount,
2696
3067
  isToolActive: this.isToolActive,
2697
3068
  isImageSelectionActive: this.isImageSelectionActive,
@@ -4341,11 +4712,11 @@ var DielineTool = class {
4341
4712
  style: "solid"
4342
4713
  },
4343
4714
  insideColor: "rgba(0,0,0,0)",
4344
- outsideColor: "#ffffff",
4345
4715
  showBleedLines: true,
4346
4716
  features: []
4347
4717
  };
4348
4718
  this.specs = [];
4719
+ this.effects = [];
4349
4720
  this.renderSeq = 0;
4350
4721
  this.onCanvasResized = () => {
4351
4722
  this.updateDieline();
@@ -4382,10 +4753,23 @@ var DielineTool = class {
4382
4753
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
4383
4754
  this.id,
4384
4755
  () => ({
4385
- layerSpecs: {
4386
- [DIELINE_LAYER_ID]: this.specs
4387
- },
4388
- replaceLayerIds: [DIELINE_LAYER_ID]
4756
+ passes: [
4757
+ {
4758
+ id: DIELINE_LAYER_ID,
4759
+ stack: 700,
4760
+ order: 0,
4761
+ replace: true,
4762
+ visibility: {
4763
+ op: "not",
4764
+ expr: {
4765
+ op: "activeToolIn",
4766
+ ids: ["pooder.kit.image", "pooder.kit.white-ink"]
4767
+ }
4768
+ },
4769
+ effects: this.effects,
4770
+ objects: this.specs
4771
+ }
4772
+ ]
4389
4773
  }),
4390
4774
  { priority: 250 }
4391
4775
  );
@@ -4441,10 +4825,6 @@ var DielineTool = class {
4441
4825
  s.offsetLine.style
4442
4826
  );
4443
4827
  s.insideColor = configService.get("dieline.insideColor", s.insideColor);
4444
- s.outsideColor = configService.get(
4445
- "dieline.outsideColor",
4446
- s.outsideColor
4447
- );
4448
4828
  s.showBleedLines = configService.get(
4449
4829
  "dieline.showBleedLines",
4450
4830
  s.showBleedLines
@@ -4507,9 +4887,6 @@ var DielineTool = class {
4507
4887
  case "dieline.insideColor":
4508
4888
  s.insideColor = e.value;
4509
4889
  break;
4510
- case "dieline.outsideColor":
4511
- s.outsideColor = e.value;
4512
- break;
4513
4890
  case "dieline.showBleedLines":
4514
4891
  s.showBleedLines = e.value;
4515
4892
  break;
@@ -4538,6 +4915,7 @@ var DielineTool = class {
4538
4915
  context.eventBus.off("canvas:resized", this.onCanvasResized);
4539
4916
  this.renderSeq += 1;
4540
4917
  this.specs = [];
4918
+ this.effects = [];
4541
4919
  (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
4542
4920
  this.renderProducerDisposable = void 0;
4543
4921
  if (this.canvasService) {
@@ -4654,12 +5032,6 @@ var DielineTool = class {
4654
5032
  label: "Inside Color",
4655
5033
  default: s.insideColor
4656
5034
  },
4657
- {
4658
- id: "dieline.outsideColor",
4659
- type: "color",
4660
- label: "Outside Color",
4661
- default: s.outsideColor
4662
- },
4663
5035
  {
4664
5036
  id: "dieline.features",
4665
5037
  type: "json",
@@ -4783,6 +5155,12 @@ var DielineTool = class {
4783
5155
  "ConfigurationService"
4784
5156
  );
4785
5157
  }
5158
+ hasImageItems() {
5159
+ const configService = this.getConfigService();
5160
+ if (!configService) return false;
5161
+ const items = configService.get("image.items", []);
5162
+ return Array.isArray(items) && items.length > 0;
5163
+ }
4786
5164
  syncSizeState(configService) {
4787
5165
  const sizeState = readSizeState(configService);
4788
5166
  this.state.width = sizeState.actualWidthMm;
@@ -4790,29 +5168,6 @@ var DielineTool = class {
4790
5168
  this.state.padding = sizeState.viewPadding;
4791
5169
  this.state.offset = sizeState.cutMode === "outset" ? sizeState.cutMarginMm : sizeState.cutMode === "inset" ? -sizeState.cutMarginMm : 0;
4792
5170
  }
4793
- bringFeatureMarkersToFront() {
4794
- if (!this.canvasService) return;
4795
- const canvas = this.canvasService.canvas;
4796
- canvas.getObjects().filter((obj) => {
4797
- var _a;
4798
- return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.type) === "feature-marker";
4799
- }).forEach((obj) => canvas.bringObjectToFront(obj));
4800
- }
4801
- ensureLayerStacking() {
4802
- if (!this.canvasService) return;
4803
- const layer = this.canvasService.getLayer(DIELINE_LAYER_ID);
4804
- if (!layer) return;
4805
- const userLayer = this.canvasService.getLayer("user");
4806
- if (userLayer) {
4807
- const layerIndex = this.canvasService.canvas.getObjects().indexOf(layer);
4808
- const userIndex = this.canvasService.canvas.getObjects().indexOf(userLayer);
4809
- if (layerIndex < userIndex) {
4810
- this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
4811
- }
4812
- return;
4813
- }
4814
- this.canvasService.canvas.bringObjectToFront(layer);
4815
- }
4816
5171
  buildDielineSpecs(sceneLayout) {
4817
5172
  var _a, _b;
4818
5173
  const {
@@ -4822,10 +5177,10 @@ var DielineTool = class {
4822
5177
  mainLine,
4823
5178
  offsetLine,
4824
5179
  insideColor,
4825
- outsideColor,
4826
5180
  showBleedLines,
4827
5181
  features
4828
5182
  } = this.state;
5183
+ const hasImages = this.hasImageItems();
4829
5184
  const canvasW = sceneLayout.canvasWidth || ((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 800;
4830
5185
  const canvasH = sceneLayout.canvasHeight || ((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 600;
4831
5186
  const scale = sceneLayout.scale;
@@ -4847,41 +5202,8 @@ var DielineTool = class {
4847
5202
  radius: (f.radius || 0) * scale
4848
5203
  }));
4849
5204
  const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
4850
- const maskPathData = generateMaskPath({
4851
- canvasWidth: canvasW,
4852
- canvasHeight: canvasH,
4853
- shape,
4854
- width: cutW,
4855
- height: cutH,
4856
- radius: cutR,
4857
- x: cx,
4858
- y: cy,
4859
- features: cutFeatures,
4860
- shapeStyle,
4861
- pathData: this.state.pathData,
4862
- customSourceWidthPx: this.state.customSourceWidthPx,
4863
- customSourceHeightPx: this.state.customSourceHeightPx
4864
- });
4865
- const specs = [
4866
- {
4867
- id: "dieline.mask",
4868
- type: "path",
4869
- space: "screen",
4870
- data: { id: "dieline.mask", type: "dieline" },
4871
- props: {
4872
- pathData: maskPathData,
4873
- fill: outsideColor,
4874
- stroke: null,
4875
- selectable: false,
4876
- evented: false,
4877
- originX: "left",
4878
- originY: "top",
4879
- left: 0,
4880
- top: 0
4881
- }
4882
- }
4883
- ];
4884
- if (insideColor && insideColor !== "transparent" && insideColor !== "rgba(0,0,0,0)") {
5205
+ const specs = [];
5206
+ if (insideColor && insideColor !== "transparent" && insideColor !== "rgba(0,0,0,0)" && !hasImages) {
4885
5207
  const productPathData = generateDielinePath({
4886
5208
  shape,
4887
5209
  width: cutW,
@@ -5035,6 +5357,77 @@ var DielineTool = class {
5035
5357
  });
5036
5358
  return specs;
5037
5359
  }
5360
+ buildImageClipEffects(sceneLayout) {
5361
+ var _a, _b;
5362
+ const { shape, shapeStyle, radius, features } = this.state;
5363
+ const canvasW = sceneLayout.canvasWidth || ((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 800;
5364
+ const canvasH = sceneLayout.canvasHeight || ((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 600;
5365
+ const scale = sceneLayout.scale;
5366
+ const cx = sceneLayout.trimRect.centerX;
5367
+ const cy = sceneLayout.trimRect.centerY;
5368
+ const visualWidth = sceneLayout.trimRect.width;
5369
+ const visualRadius = radius * scale;
5370
+ const cutW = sceneLayout.cutRect.width;
5371
+ const cutH = sceneLayout.cutRect.height;
5372
+ const visualOffset = (cutW - visualWidth) / 2;
5373
+ const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
5374
+ const absoluteFeatures = (features || []).map((f) => ({
5375
+ ...f,
5376
+ x: f.x,
5377
+ y: f.y,
5378
+ width: (f.width || 0) * scale,
5379
+ height: (f.height || 0) * scale,
5380
+ radius: (f.radius || 0) * scale
5381
+ }));
5382
+ const cutFeatures = absoluteFeatures.filter((f) => !f.skipCut);
5383
+ const clipPathData = generateDielinePath({
5384
+ shape,
5385
+ width: cutW,
5386
+ height: cutH,
5387
+ radius: cutR,
5388
+ x: cx,
5389
+ y: cy,
5390
+ features: cutFeatures,
5391
+ shapeStyle,
5392
+ pathData: this.state.pathData,
5393
+ customSourceWidthPx: this.state.customSourceWidthPx,
5394
+ customSourceHeightPx: this.state.customSourceHeightPx,
5395
+ canvasWidth: canvasW,
5396
+ canvasHeight: canvasH
5397
+ });
5398
+ if (!clipPathData) return [];
5399
+ return [
5400
+ {
5401
+ type: "clipPath",
5402
+ id: "dieline.clip.image",
5403
+ visibility: {
5404
+ op: "not",
5405
+ expr: { op: "anySessionActive" }
5406
+ },
5407
+ targetPassIds: [IMAGE_OBJECT_LAYER_ID2],
5408
+ source: {
5409
+ id: "dieline.effect.clip-path",
5410
+ type: "path",
5411
+ space: "screen",
5412
+ data: {
5413
+ id: "dieline.effect.clip-path",
5414
+ type: "dieline-effect",
5415
+ effect: "clipPath"
5416
+ },
5417
+ props: {
5418
+ pathData: clipPathData,
5419
+ fill: "#000000",
5420
+ stroke: null,
5421
+ originX: "left",
5422
+ originY: "top",
5423
+ selectable: false,
5424
+ evented: false,
5425
+ excludeFromExport: true
5426
+ }
5427
+ }
5428
+ }
5429
+ ];
5430
+ }
5038
5431
  updateDieline(_emitEvent = true) {
5039
5432
  void this.updateDielineAsync();
5040
5433
  }
@@ -5051,17 +5444,17 @@ var DielineTool = class {
5051
5444
  if (!sceneLayout) {
5052
5445
  if (seq !== this.renderSeq) return;
5053
5446
  this.specs = [];
5447
+ this.effects = [];
5054
5448
  await this.canvasService.flushRenderFromProducers();
5055
5449
  return;
5056
5450
  }
5057
5451
  const nextSpecs = this.buildDielineSpecs(sceneLayout);
5452
+ const nextEffects = this.buildImageClipEffects(sceneLayout);
5058
5453
  if (seq !== this.renderSeq) return;
5059
5454
  this.specs = nextSpecs;
5455
+ this.effects = nextEffects;
5060
5456
  await this.canvasService.flushRenderFromProducers();
5061
5457
  if (seq !== this.renderSeq) return;
5062
- this.ensureLayerStacking();
5063
- this.bringFeatureMarkersToFront();
5064
- this.canvasService.bringLayerToFront("ruler-overlay");
5065
5458
  this.canvasService.requestRenderAll();
5066
5459
  }
5067
5460
  getGeometry() {
@@ -5496,9 +5889,14 @@ var FeatureTool = class {
5496
5889
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
5497
5890
  this.id,
5498
5891
  () => ({
5499
- rootLayerSpecs: {
5500
- [FEATURE_OVERLAY_LAYER_ID]: this.specs
5501
- }
5892
+ passes: [
5893
+ {
5894
+ id: FEATURE_OVERLAY_LAYER_ID,
5895
+ stack: 880,
5896
+ order: 0,
5897
+ objects: this.specs
5898
+ }
5899
+ ]
5502
5900
  }),
5503
5901
  { priority: 350 }
5504
5902
  );
@@ -5641,10 +6039,10 @@ var FeatureTool = class {
5641
6039
  await this.refreshGeometry();
5642
6040
  this.setWorkingFeatures(original);
5643
6041
  this.hasWorkingChanges = false;
6042
+ this.clearFeatureSessionState();
5644
6043
  this.redraw();
5645
6044
  this.emitWorkingChange();
5646
6045
  this.updateCommittedFeatures(original);
5647
- this.clearFeatureSessionState();
5648
6046
  return { ok: true };
5649
6047
  }
5650
6048
  },
@@ -5960,6 +6358,7 @@ var FeatureTool = class {
5960
6358
  }
5961
6359
  getDraggableMarkerTarget(target) {
5962
6360
  var _a, _b;
6361
+ if (!this.isFeatureSessionActive || !this.isToolActive) return null;
5963
6362
  if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return null;
5964
6363
  if (((_b = target.data) == null ? void 0 : _b.markerRole) !== "handle") return null;
5965
6364
  return target;
@@ -6082,18 +6481,12 @@ var FeatureTool = class {
6082
6481
  if (seq !== this.renderSeq) return;
6083
6482
  await this.canvasService.flushRenderFromProducers();
6084
6483
  if (seq !== this.renderSeq) return;
6085
- this.syncOverlayOrder();
6086
6484
  if (options.enforceConstraints) {
6087
6485
  this.enforceConstraints();
6088
6486
  }
6089
6487
  }
6090
- syncOverlayOrder() {
6091
- if (!this.canvasService) return;
6092
- this.canvasService.bringLayerToFront(FEATURE_OVERLAY_LAYER_ID);
6093
- this.canvasService.bringLayerToFront("ruler-overlay");
6094
- }
6095
6488
  buildFeatureSpecs() {
6096
- if (!this.currentGeometry || this.workingFeatures.length === 0) {
6489
+ if (!this.isFeatureSessionActive || !this.currentGeometry || this.workingFeatures.length === 0) {
6097
6490
  return [];
6098
6491
  }
6099
6492
  const groups = /* @__PURE__ */ new Map();
@@ -6163,11 +6556,12 @@ var FeatureTool = class {
6163
6556
  const color = feature.color || (feature.operation === "add" ? "#00FF00" : "#FF0000");
6164
6557
  const strokeDash = feature.strokeDash || (feature.operation === "subtract" ? [4, 4] : void 0);
6165
6558
  const interactive = options.markerRole === "handle";
6559
+ const sessionVisible = this.isToolActive && this.isFeatureSessionActive;
6166
6560
  const baseData = this.buildMarkerData(marker, options);
6167
6561
  const commonProps = {
6168
- visible: this.isToolActive,
6169
- selectable: interactive && this.isToolActive,
6170
- evented: interactive && this.isToolActive,
6562
+ visible: sessionVisible,
6563
+ selectable: interactive && sessionVisible,
6564
+ evented: interactive && sessionVisible,
6171
6565
  hasControls: false,
6172
6566
  hasBorders: false,
6173
6567
  hoverCursor: interactive ? "move" : "default",
@@ -6232,7 +6626,7 @@ var FeatureTool = class {
6232
6626
  markerOffsetY: -visualHeight / 2
6233
6627
  },
6234
6628
  props: {
6235
- visible: this.isToolActive,
6629
+ visible: sessionVisible,
6236
6630
  selectable: false,
6237
6631
  evented: false,
6238
6632
  width: visualWidth,
@@ -6402,9 +6796,14 @@ var FilmTool = class {
6402
6796
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
6403
6797
  this.id,
6404
6798
  () => ({
6405
- layerSpecs: {
6406
- [FILM_LAYER_ID]: this.specs
6407
- }
6799
+ passes: [
6800
+ {
6801
+ id: FILM_LAYER_ID,
6802
+ stack: 1e3,
6803
+ order: 0,
6804
+ objects: this.specs
6805
+ }
6806
+ ]
6408
6807
  }),
6409
6808
  { priority: 500 }
6410
6809
  );
@@ -6578,7 +6977,6 @@ var FilmTool = class {
6578
6977
  this.specs = this.buildFilmSpecs(this.renderImageUrl, this.opacity);
6579
6978
  await this.canvasService.flushRenderFromProducers();
6580
6979
  if (seq !== this.renderSeq) return;
6581
- this.canvasService.bringLayerToFront(FILM_LAYER_ID);
6582
6980
  this.canvasService.requestRenderAll();
6583
6981
  }
6584
6982
  };
@@ -6729,10 +7127,22 @@ var RulerTool = class {
6729
7127
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
6730
7128
  this.id,
6731
7129
  () => ({
6732
- rootLayerSpecs: {
6733
- [RULER_LAYER_ID]: this.specs
6734
- },
6735
- replaceRootLayerIds: [RULER_LAYER_ID]
7130
+ passes: [
7131
+ {
7132
+ id: RULER_LAYER_ID,
7133
+ stack: 950,
7134
+ order: 0,
7135
+ replace: true,
7136
+ visibility: {
7137
+ op: "not",
7138
+ expr: {
7139
+ op: "activeToolIn",
7140
+ ids: ["pooder.kit.white-ink"]
7141
+ }
7142
+ },
7143
+ objects: this.specs
7144
+ }
7145
+ ]
6736
7146
  }),
6737
7147
  { priority: 400 }
6738
7148
  );
@@ -7224,7 +7634,6 @@ var RulerTool = class {
7224
7634
  this.specs = specs;
7225
7635
  await this.canvasService.flushRenderFromProducers();
7226
7636
  if (seq !== this.renderSeq) return;
7227
- this.canvasService.bringLayerToFront(RULER_LAYER_ID);
7228
7637
  this.canvasService.requestRenderAll();
7229
7638
  this.log("render:done", { seq });
7230
7639
  }
@@ -7238,7 +7647,6 @@ var WHITE_INK_OBJECT_LAYER_ID = "white-ink.user";
7238
7647
  var WHITE_INK_COVER_LAYER_ID = "white-ink.cover";
7239
7648
  var WHITE_INK_OVERLAY_LAYER_ID = "white-ink.overlay";
7240
7649
  var IMAGE_OBJECT_LAYER_ID3 = "image.user";
7241
- var IMAGE_OVERLAY_LAYER_ID2 = "image-overlay";
7242
7650
  var WHITE_INK_DEBUG_KEY = "whiteInk.debug";
7243
7651
  var WHITE_INK_PREVIEW_IMAGE_VISIBLE_KEY = "whiteInk.previewImageVisible";
7244
7652
  var WHITE_INK_DEFAULT_OPACITY = 0.85;
@@ -7316,11 +7724,26 @@ var WhiteInkTool = class {
7316
7724
  this.renderProducerDisposable = this.canvasService.registerRenderProducer(
7317
7725
  this.id,
7318
7726
  () => ({
7319
- rootLayerSpecs: {
7320
- [WHITE_INK_OBJECT_LAYER_ID]: this.whiteSpecs,
7321
- [WHITE_INK_COVER_LAYER_ID]: this.coverSpecs,
7322
- [WHITE_INK_OVERLAY_LAYER_ID]: this.overlaySpecs
7323
- }
7727
+ passes: [
7728
+ {
7729
+ id: WHITE_INK_COVER_LAYER_ID,
7730
+ stack: 220,
7731
+ order: 0,
7732
+ objects: this.coverSpecs
7733
+ },
7734
+ {
7735
+ id: WHITE_INK_OBJECT_LAYER_ID,
7736
+ stack: 221,
7737
+ order: 0,
7738
+ objects: this.whiteSpecs
7739
+ },
7740
+ {
7741
+ id: WHITE_INK_OVERLAY_LAYER_ID,
7742
+ stack: 790,
7743
+ order: 0,
7744
+ objects: this.overlaySpecs
7745
+ }
7746
+ ]
7324
7747
  }),
7325
7748
  { priority: 260 }
7326
7749
  );
@@ -7399,7 +7822,6 @@ var WhiteInkTool = class {
7399
7822
  (_a = this.dirtyTrackerDisposable) == null ? void 0 : _a.dispose();
7400
7823
  this.dirtyTrackerDisposable = void 0;
7401
7824
  this.clearRenderedWhiteInks();
7402
- this.applyImageVisibilityForWhiteInk(false);
7403
7825
  (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
7404
7826
  this.renderProducerDisposable = void 0;
7405
7827
  if (this.canvasService) {
@@ -8243,22 +8665,6 @@ var WhiteInkTool = class {
8243
8665
  }
8244
8666
  ];
8245
8667
  }
8246
- applyImageVisibilityForWhiteInk(previewActive) {
8247
- if (!this.canvasService) return;
8248
- const visible = !previewActive;
8249
- let changed = false;
8250
- this.canvasService.canvas.getObjects().forEach((obj) => {
8251
- var _a, _b;
8252
- if (((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) !== IMAGE_OBJECT_LAYER_ID3) return;
8253
- if (obj.visible === visible) return;
8254
- obj.set({ visible });
8255
- (_b = obj.setCoords) == null ? void 0 : _b.call(obj);
8256
- changed = true;
8257
- });
8258
- if (changed) {
8259
- this.canvasService.requestRenderAll();
8260
- }
8261
- }
8262
8668
  resolveRenderItems() {
8263
8669
  if (this.isToolActive) {
8264
8670
  return this.cloneItems(this.workingItems);
@@ -8277,57 +8683,11 @@ var WhiteInkTool = class {
8277
8683
  ]);
8278
8684
  const scaleAdjust = this.computeWhiteScaleAdjust(imageSource, whiteSource);
8279
8685
  return {
8280
- whiteSrc: whiteMaskSrc || "",
8281
- coverSrc: coverMaskSrc || "",
8282
- whiteScaleAdjustX: scaleAdjust.x,
8283
- whiteScaleAdjustY: scaleAdjust.y
8284
- };
8285
- }
8286
- resolveDefaultInsertIndex(objects) {
8287
- if (!this.canvasService) return 0;
8288
- const backgroundLayer = this.canvasService.getLayer("background");
8289
- if (!backgroundLayer) return 0;
8290
- const bgIndex = objects.indexOf(backgroundLayer);
8291
- if (bgIndex < 0) return 0;
8292
- return bgIndex + 1;
8293
- }
8294
- syncZOrder() {
8295
- if (!this.canvasService) return;
8296
- const canvas = this.canvasService.canvas;
8297
- const whiteObjects = this.canvasService.getRootLayerObjects(
8298
- WHITE_INK_OBJECT_LAYER_ID
8299
- );
8300
- const coverObjects = this.canvasService.getRootLayerObjects(
8301
- WHITE_INK_COVER_LAYER_ID
8302
- );
8303
- const frameObjects = this.canvasService.getRootLayerObjects(
8304
- WHITE_INK_OVERLAY_LAYER_ID
8305
- );
8306
- const currentObjects = canvas.getObjects();
8307
- const imageIndexes = currentObjects.map(
8308
- (obj, index) => {
8309
- var _a;
8310
- return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === IMAGE_OBJECT_LAYER_ID3 ? index : -1;
8311
- }
8312
- ).filter((index) => index >= 0);
8313
- let whiteInsertIndex = imageIndexes.length ? Math.min(...imageIndexes) : this.resolveDefaultInsertIndex(currentObjects);
8314
- let coverInsertIndex = whiteInsertIndex;
8315
- coverObjects.forEach((obj) => {
8316
- canvas.moveObjectTo(obj, coverInsertIndex);
8317
- coverInsertIndex += 1;
8318
- });
8319
- whiteInsertIndex = coverInsertIndex;
8320
- whiteObjects.forEach((obj) => {
8321
- canvas.moveObjectTo(obj, whiteInsertIndex);
8322
- whiteInsertIndex += 1;
8323
- });
8324
- frameObjects.forEach((obj) => canvas.bringObjectToFront(obj));
8325
- canvas.getObjects().filter((obj) => {
8326
- var _a;
8327
- return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === IMAGE_OVERLAY_LAYER_ID2;
8328
- }).forEach((obj) => canvas.bringObjectToFront(obj));
8329
- this.canvasService.bringLayerToFront("dieline-overlay");
8330
- this.canvasService.bringLayerToFront("ruler-overlay");
8686
+ whiteSrc: whiteMaskSrc || "",
8687
+ coverSrc: coverMaskSrc || "",
8688
+ whiteScaleAdjustX: scaleAdjust.x,
8689
+ whiteScaleAdjustY: scaleAdjust.y
8690
+ };
8331
8691
  }
8332
8692
  clearRenderedWhiteInks() {
8333
8693
  if (!this.canvasService) return;
@@ -8360,7 +8720,6 @@ var WhiteInkTool = class {
8360
8720
  this.syncToolActiveFromWorkbench();
8361
8721
  const seq = ++this.renderSeq;
8362
8722
  const previewActive = this.isPreviewActive();
8363
- this.applyImageVisibilityForWhiteInk(previewActive);
8364
8723
  const frame = this.getFrameRect();
8365
8724
  const frameSpecs = this.buildFrameSpecs(frame);
8366
8725
  let whiteSpecs = [];
@@ -8408,7 +8767,6 @@ var WhiteInkTool = class {
8408
8767
  this.overlaySpecs = frameSpecs;
8409
8768
  await this.canvasService.flushRenderFromProducers();
8410
8769
  if (seq !== this.renderSeq) return;
8411
- this.syncZOrder();
8412
8770
  this.canvasService.requestRenderAll();
8413
8771
  }
8414
8772
  getMaskCacheKey(sourceUrl, tint) {
@@ -8593,62 +8951,12 @@ var SceneLayoutService = class {
8593
8951
  }
8594
8952
  };
8595
8953
 
8596
- // src/extensions/sceneVisibility.ts
8597
- import { WORKBENCH_SERVICE } from "@pooder/core";
8598
- var CANVAS_SERVICE_ID2 = "CanvasService";
8599
- var HIDDEN_DIELINE_TOOLS = /* @__PURE__ */ new Set(["pooder.kit.image", "pooder.kit.white-ink"]);
8600
- var HIDDEN_RULER_TOOLS = /* @__PURE__ */ new Set(["pooder.kit.white-ink"]);
8601
- var SceneVisibilityService = class {
8602
- constructor() {
8603
- this.activeToolId = null;
8604
- this.onToolActivated = (e) => {
8605
- this.activeToolId = e.id;
8606
- this.apply();
8607
- };
8608
- this.onObjectAdded = () => {
8609
- this.apply();
8610
- };
8611
- }
8612
- init(context) {
8613
- var _a, _b;
8614
- if (this.context) {
8615
- this.dispose(this.context);
8616
- }
8617
- const canvasService = context.get(CANVAS_SERVICE_ID2);
8618
- if (!canvasService) {
8619
- throw new Error("[SceneVisibilityService] CanvasService is required.");
8620
- }
8621
- this.context = context;
8622
- this.canvasService = canvasService;
8623
- this.activeToolId = (_b = (_a = context.get(WORKBENCH_SERVICE)) == null ? void 0 : _a.activeToolId) != null ? _b : null;
8624
- context.eventBus.on("tool:activated", this.onToolActivated);
8625
- context.eventBus.on("object:added", this.onObjectAdded);
8626
- this.apply();
8627
- }
8628
- dispose(context) {
8629
- var _a;
8630
- const activeContext = (_a = this.context) != null ? _a : context;
8631
- activeContext.eventBus.off("tool:activated", this.onToolActivated);
8632
- activeContext.eventBus.off("object:added", this.onObjectAdded);
8633
- this.context = void 0;
8634
- this.activeToolId = null;
8635
- this.canvasService = void 0;
8636
- }
8637
- apply() {
8638
- if (!this.canvasService) return;
8639
- const dielineLayer = this.canvasService.getLayer("dieline-overlay");
8640
- if (dielineLayer) {
8641
- const visible = !HIDDEN_DIELINE_TOOLS.has(this.activeToolId || "");
8642
- this.canvasService.setLayerVisibility("dieline-overlay", visible);
8643
- }
8644
- const rulerVisible = !HIDDEN_RULER_TOOLS.has(this.activeToolId || "");
8645
- this.canvasService.setLayerVisibility("ruler-overlay", rulerVisible);
8646
- this.canvasService.requestRenderAll();
8647
- }
8648
- };
8649
-
8650
8954
  // src/services/CanvasService.ts
8651
- import { Canvas, Group, Rect, Path as Path2, Image as Image2, Text } from "fabric";
8955
+ import { Canvas, Rect, Path as Path2, Image as Image2, Text } from "fabric";
8956
+ import {
8957
+ TOOL_SESSION_SERVICE,
8958
+ WORKBENCH_SERVICE
8959
+ } from "@pooder/core";
8652
8960
 
8653
8961
  // src/services/ViewportSystem.ts
8654
8962
  var ViewportSystem = class {
@@ -8723,6 +9031,56 @@ var ViewportSystem = class {
8723
9031
  }
8724
9032
  };
8725
9033
 
9034
+ // src/services/visibility.ts
9035
+ function compareLayerObjectCount(actual, cmp, expected) {
9036
+ if (cmp === ">") return actual > expected;
9037
+ if (cmp === ">=") return actual >= expected;
9038
+ if (cmp === "<") return actual < expected;
9039
+ if (cmp === "<=") return actual <= expected;
9040
+ return actual === expected;
9041
+ }
9042
+ function layerState(context, layerId) {
9043
+ return context.layers.get(layerId) || { exists: false, objectCount: 0 };
9044
+ }
9045
+ function evaluateVisibilityExpr(expr, context) {
9046
+ var _a;
9047
+ if (!expr) return true;
9048
+ if (expr.op === "const") {
9049
+ return Boolean(expr.value);
9050
+ }
9051
+ if (expr.op === "activeToolIn") {
9052
+ const activeToolId = (_a = context.activeToolId) != null ? _a : null;
9053
+ return !!activeToolId && expr.ids.includes(activeToolId);
9054
+ }
9055
+ if (expr.op === "sessionActive") {
9056
+ const toolId = String(expr.toolId || "").trim();
9057
+ if (!toolId) return false;
9058
+ return context.isSessionActive ? context.isSessionActive(toolId) : false;
9059
+ }
9060
+ if (expr.op === "anySessionActive") {
9061
+ return context.hasAnyActiveSession ? context.hasAnyActiveSession() : false;
9062
+ }
9063
+ if (expr.op === "layerExists") {
9064
+ return layerState(context, expr.layerId).exists === true;
9065
+ }
9066
+ if (expr.op === "layerObjectCount") {
9067
+ const expected = Number(expr.value);
9068
+ if (!Number.isFinite(expected)) return false;
9069
+ const count = layerState(context, expr.layerId).objectCount;
9070
+ return compareLayerObjectCount(count, expr.cmp, expected);
9071
+ }
9072
+ if (expr.op === "not") {
9073
+ return !evaluateVisibilityExpr(expr.expr, context);
9074
+ }
9075
+ if (expr.op === "all") {
9076
+ return expr.exprs.every((item) => evaluateVisibilityExpr(item, context));
9077
+ }
9078
+ if (expr.op === "any") {
9079
+ return expr.exprs.some((item) => evaluateVisibilityExpr(item, context));
9080
+ }
9081
+ return true;
9082
+ }
9083
+
8726
9084
  // src/services/CanvasService.ts
8727
9085
  var CanvasService = class {
8728
9086
  constructor(el, options) {
@@ -8731,8 +9089,48 @@ var CanvasService = class {
8731
9089
  this.producerFlushRequested = false;
8732
9090
  this.producerLoopPending = false;
8733
9091
  this.producerLoopPromise = null;
8734
- this.managedProducerLayerIds = /* @__PURE__ */ new Set();
8735
- this.managedProducerRootLayerIds = /* @__PURE__ */ new Set();
9092
+ this.producerApplyInProgress = false;
9093
+ this.visibilityRefreshScheduled = false;
9094
+ this.managedProducerPassIds = /* @__PURE__ */ new Set();
9095
+ this.managedPassMetas = /* @__PURE__ */ new Map();
9096
+ this.managedPassEffects = [];
9097
+ this.canvasForwardersBound = false;
9098
+ this.forwardSelectionCreated = (e) => {
9099
+ var _a;
9100
+ (_a = this.eventBus) == null ? void 0 : _a.emit("selection:created", e);
9101
+ };
9102
+ this.forwardSelectionUpdated = (e) => {
9103
+ var _a;
9104
+ (_a = this.eventBus) == null ? void 0 : _a.emit("selection:updated", e);
9105
+ };
9106
+ this.forwardSelectionCleared = (e) => {
9107
+ var _a;
9108
+ (_a = this.eventBus) == null ? void 0 : _a.emit("selection:cleared", e);
9109
+ };
9110
+ this.forwardObjectModified = (e) => {
9111
+ var _a;
9112
+ (_a = this.eventBus) == null ? void 0 : _a.emit("object:modified", e);
9113
+ };
9114
+ this.forwardObjectAdded = (e) => {
9115
+ var _a;
9116
+ (_a = this.eventBus) == null ? void 0 : _a.emit("object:added", e);
9117
+ };
9118
+ this.forwardObjectRemoved = (e) => {
9119
+ var _a;
9120
+ (_a = this.eventBus) == null ? void 0 : _a.emit("object:removed", e);
9121
+ };
9122
+ this.onToolActivated = () => {
9123
+ this.applyManagedPassVisibility();
9124
+ void this.applyManagedPassEffects(void 0, { render: true });
9125
+ };
9126
+ this.onToolSessionChanged = () => {
9127
+ this.applyManagedPassVisibility();
9128
+ void this.applyManagedPassEffects(void 0, { render: true });
9129
+ };
9130
+ this.onCanvasObjectChanged = () => {
9131
+ if (this.producerApplyInProgress) return;
9132
+ this.scheduleManagedPassVisibilityRefresh();
9133
+ };
8736
9134
  if (el instanceof Canvas) {
8737
9135
  this.canvas = el;
8738
9136
  } else {
@@ -8749,25 +9147,53 @@ var CanvasService = class {
8749
9147
  this.setEventBus(options.eventBus);
8750
9148
  }
8751
9149
  }
9150
+ init(context) {
9151
+ if (this.context) {
9152
+ this.detachContextEvents(this.context.eventBus);
9153
+ }
9154
+ this.context = context;
9155
+ this.workbenchService = context.get(WORKBENCH_SERVICE);
9156
+ this.toolSessionService = context.get(TOOL_SESSION_SERVICE);
9157
+ this.setEventBus(context.eventBus);
9158
+ this.attachContextEvents(context.eventBus);
9159
+ }
9160
+ attachContextEvents(eventBus) {
9161
+ eventBus.on("tool:activated", this.onToolActivated);
9162
+ eventBus.on("tool:session:change", this.onToolSessionChanged);
9163
+ eventBus.on("object:added", this.onCanvasObjectChanged);
9164
+ eventBus.on("object:removed", this.onCanvasObjectChanged);
9165
+ }
9166
+ detachContextEvents(eventBus) {
9167
+ eventBus.off("tool:activated", this.onToolActivated);
9168
+ eventBus.off("tool:session:change", this.onToolSessionChanged);
9169
+ eventBus.off("object:added", this.onCanvasObjectChanged);
9170
+ eventBus.off("object:removed", this.onCanvasObjectChanged);
9171
+ }
8752
9172
  setEventBus(eventBus) {
8753
9173
  this.eventBus = eventBus;
8754
9174
  this.setupEvents();
8755
9175
  }
8756
9176
  setupEvents() {
8757
- if (!this.eventBus) return;
8758
- const bus = this.eventBus;
8759
- const forward = (name) => (e) => bus.emit(name, e);
8760
- this.canvas.on("selection:created", forward("selection:created"));
8761
- this.canvas.on("selection:updated", forward("selection:updated"));
8762
- this.canvas.on("selection:cleared", forward("selection:cleared"));
8763
- this.canvas.on("object:modified", forward("object:modified"));
8764
- this.canvas.on("object:added", forward("object:added"));
8765
- this.canvas.on("object:removed", forward("object:removed"));
9177
+ if (this.canvasForwardersBound) return;
9178
+ this.canvas.on("selection:created", this.forwardSelectionCreated);
9179
+ this.canvas.on("selection:updated", this.forwardSelectionUpdated);
9180
+ this.canvas.on("selection:cleared", this.forwardSelectionCleared);
9181
+ this.canvas.on("object:modified", this.forwardObjectModified);
9182
+ this.canvas.on("object:added", this.forwardObjectAdded);
9183
+ this.canvas.on("object:removed", this.forwardObjectRemoved);
9184
+ this.canvasForwardersBound = true;
8766
9185
  }
8767
9186
  dispose() {
9187
+ if (this.context) {
9188
+ this.detachContextEvents(this.context.eventBus);
9189
+ }
8768
9190
  this.renderProducers.clear();
8769
- this.managedProducerLayerIds.clear();
8770
- this.managedProducerRootLayerIds.clear();
9191
+ this.managedProducerPassIds.clear();
9192
+ this.managedPassMetas.clear();
9193
+ this.managedPassEffects = [];
9194
+ this.context = void 0;
9195
+ this.workbenchService = void 0;
9196
+ this.toolSessionService = void 0;
8771
9197
  this.producerFlushRequested = false;
8772
9198
  this.canvas.dispose();
8773
9199
  }
@@ -8845,118 +9271,292 @@ var CanvasService = class {
8845
9271
  return a.toolId.localeCompare(b.toolId);
8846
9272
  });
8847
9273
  }
8848
- appendLayerSpecMap(map, source) {
8849
- if (!source) return;
8850
- Object.entries(source).forEach(([layerId, specs]) => {
8851
- if (!Array.isArray(specs)) return;
8852
- const list = map.get(layerId) || [];
8853
- list.push(...specs);
8854
- map.set(layerId, list);
9274
+ normalizePassSpecValue(spec) {
9275
+ const id = String(spec.id || "").trim();
9276
+ if (!id) return null;
9277
+ return {
9278
+ id,
9279
+ stack: Number.isFinite(spec.stack) ? Number(spec.stack) : 0,
9280
+ order: Number.isFinite(spec.order) ? Number(spec.order) : 0,
9281
+ replace: spec.replace !== false,
9282
+ visibility: spec.visibility,
9283
+ effects: Array.isArray(spec.effects) ? [...spec.effects] : [],
9284
+ objects: Array.isArray(spec.objects) ? [...spec.objects] : []
9285
+ };
9286
+ }
9287
+ normalizeClipPathEffectSpec(effect, passId, index) {
9288
+ if (!effect || effect.type !== "clipPath") return null;
9289
+ const source = effect.source;
9290
+ if (!source || typeof source !== "object") return null;
9291
+ const sourceId = String(source.id || "").trim();
9292
+ if (!sourceId) return null;
9293
+ const targetPassIds = Array.isArray(effect.targetPassIds) ? effect.targetPassIds.map((item) => String(item || "").trim()).filter((item) => item.length > 0) : [];
9294
+ if (!targetPassIds.length) return null;
9295
+ const customId = String(effect.id || "").trim();
9296
+ const key = customId || `${passId}.effect.clipPath.${index}`;
9297
+ return {
9298
+ type: "clipPath",
9299
+ key,
9300
+ visibility: effect.visibility,
9301
+ source: {
9302
+ ...source,
9303
+ id: sourceId
9304
+ },
9305
+ targetPassIds
9306
+ };
9307
+ }
9308
+ mergePassSpec(map, rawSpec, producerId) {
9309
+ const normalized = this.normalizePassSpecValue(rawSpec);
9310
+ if (!normalized) return;
9311
+ const existing = map.get(normalized.id);
9312
+ if (!existing) {
9313
+ map.set(normalized.id, normalized);
9314
+ return;
9315
+ }
9316
+ existing.objects.push(...normalized.objects);
9317
+ existing.replace = existing.replace || normalized.replace;
9318
+ existing.stack = normalized.stack;
9319
+ existing.order = normalized.order;
9320
+ if (normalized.visibility !== void 0) {
9321
+ existing.visibility = normalized.visibility;
9322
+ }
9323
+ existing.effects.push(...normalized.effects);
9324
+ if (normalized.objects.length === 0 && normalized.effects.length === 0) {
9325
+ console.debug(
9326
+ `[CanvasService] pass "${normalized.id}" from producer "${producerId}" updated ordering/visibility only.`
9327
+ );
9328
+ }
9329
+ }
9330
+ comparePassMeta(a, b) {
9331
+ if (a.stack !== b.stack) return a.stack - b.stack;
9332
+ if (a.order !== b.order) return a.order - b.order;
9333
+ return a.id.localeCompare(b.id);
9334
+ }
9335
+ getPassObjectOrder(obj) {
9336
+ var _a;
9337
+ const raw = Number((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.passOrder);
9338
+ return Number.isFinite(raw) ? raw : Number.MAX_SAFE_INTEGER;
9339
+ }
9340
+ getPassCanvasObjects(passId) {
9341
+ const all = this.canvas.getObjects();
9342
+ return all.filter((obj) => {
9343
+ var _a;
9344
+ return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.passId) === passId;
9345
+ }).sort((a, b) => {
9346
+ const orderA = this.getPassObjectOrder(a);
9347
+ const orderB = this.getPassObjectOrder(b);
9348
+ if (orderA !== orderB) return orderA - orderB;
9349
+ return all.indexOf(a) - all.indexOf(b);
9350
+ });
9351
+ }
9352
+ getPassObjects(passId) {
9353
+ return this.getPassCanvasObjects(passId);
9354
+ }
9355
+ getRootLayerObjects(layerId) {
9356
+ return this.getPassCanvasObjects(layerId);
9357
+ }
9358
+ isManagedPassObject(obj) {
9359
+ var _a;
9360
+ const passId = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.passId;
9361
+ return typeof passId === "string" && this.managedPassMetas.has(passId);
9362
+ }
9363
+ syncManagedPassStacking(passes) {
9364
+ const orderedPasses = [...passes].sort((a, b) => this.comparePassMeta(a, b));
9365
+ if (!orderedPasses.length) return;
9366
+ const canvasObjects = this.canvas.getObjects();
9367
+ const managedObjects = canvasObjects.filter(
9368
+ (obj) => this.isManagedPassObject(obj)
9369
+ );
9370
+ if (!managedObjects.length) return;
9371
+ const firstManagedIndex = managedObjects.map((obj) => canvasObjects.indexOf(obj)).filter((index) => index >= 0).reduce((min, value) => Math.min(min, value), Number.MAX_SAFE_INTEGER);
9372
+ let targetIndex = Number.isFinite(firstManagedIndex) ? firstManagedIndex : 0;
9373
+ orderedPasses.forEach((meta) => {
9374
+ const objects = this.getPassCanvasObjects(meta.id);
9375
+ objects.forEach((obj) => {
9376
+ this.moveObjectInCanvas(obj, targetIndex);
9377
+ targetIndex += 1;
9378
+ });
9379
+ });
9380
+ }
9381
+ getPassRuntimeState() {
9382
+ const state = /* @__PURE__ */ new Map();
9383
+ const ensure = (passId) => {
9384
+ const id = String(passId || "").trim();
9385
+ if (!id) return { exists: false, objectCount: 0 };
9386
+ let item = state.get(id);
9387
+ if (!item) {
9388
+ item = { exists: false, objectCount: 0 };
9389
+ state.set(id, item);
9390
+ }
9391
+ return item;
9392
+ };
9393
+ this.canvas.getObjects().forEach((obj) => {
9394
+ var _a;
9395
+ const passId = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.passId;
9396
+ if (typeof passId === "string") {
9397
+ const item = ensure(passId);
9398
+ item.exists = true;
9399
+ item.objectCount += 1;
9400
+ }
9401
+ });
9402
+ this.managedPassMetas.forEach((meta) => {
9403
+ const item = ensure(meta.id);
9404
+ item.exists = true;
9405
+ });
9406
+ return state;
9407
+ }
9408
+ isSessionActive(toolId) {
9409
+ if (!this.toolSessionService) return false;
9410
+ return this.toolSessionService.getState(toolId).status === "active";
9411
+ }
9412
+ hasAnyActiveSession() {
9413
+ var _a, _b;
9414
+ return (_b = (_a = this.toolSessionService) == null ? void 0 : _a.hasAnyActiveSession()) != null ? _b : false;
9415
+ }
9416
+ buildVisibilityEvalContext(layers) {
9417
+ var _a, _b;
9418
+ return {
9419
+ activeToolId: (_b = (_a = this.workbenchService) == null ? void 0 : _a.activeToolId) != null ? _b : null,
9420
+ isSessionActive: (toolId) => this.isSessionActive(toolId),
9421
+ hasAnyActiveSession: () => this.hasAnyActiveSession(),
9422
+ layers
9423
+ };
9424
+ }
9425
+ applyManagedPassVisibility(options = {}) {
9426
+ if (!this.managedPassMetas.size) return false;
9427
+ const layers = this.getPassRuntimeState();
9428
+ const context = this.buildVisibilityEvalContext(layers);
9429
+ let changed = false;
9430
+ this.managedPassMetas.forEach((meta) => {
9431
+ const visible = evaluateVisibilityExpr(meta.visibility, context);
9432
+ changed = this.setPassVisibility(meta.id, visible) || changed;
9433
+ });
9434
+ if (changed && options.render !== false) {
9435
+ this.requestRenderAll();
9436
+ }
9437
+ return changed;
9438
+ }
9439
+ scheduleManagedPassVisibilityRefresh() {
9440
+ if (this.visibilityRefreshScheduled) return;
9441
+ this.visibilityRefreshScheduled = true;
9442
+ void Promise.resolve().then(() => {
9443
+ this.visibilityRefreshScheduled = false;
9444
+ this.applyManagedPassVisibility();
8855
9445
  });
8856
9446
  }
8857
9447
  async collectAndApplyProducerSpecs() {
8858
- const groupLayerSpecs = /* @__PURE__ */ new Map();
8859
- const rootLayerSpecs = /* @__PURE__ */ new Map();
8860
- const replaceLayerIds = /* @__PURE__ */ new Set();
8861
- const replaceRootLayerIds = /* @__PURE__ */ new Set();
9448
+ const passes = /* @__PURE__ */ new Map();
8862
9449
  const entries = this.sortedRenderProducerEntries();
8863
- for (const entry of entries) {
8864
- try {
8865
- const result = await entry.producer();
8866
- if (!result) continue;
8867
- this.appendLayerSpecMap(groupLayerSpecs, result.layerSpecs);
8868
- this.appendLayerSpecMap(rootLayerSpecs, result.rootLayerSpecs);
8869
- if (Array.isArray(result.replaceLayerIds)) {
8870
- result.replaceLayerIds.forEach((layerId) => {
8871
- if (layerId) replaceLayerIds.add(layerId);
8872
- });
8873
- }
8874
- if (Array.isArray(result.replaceRootLayerIds)) {
8875
- result.replaceRootLayerIds.forEach((layerId) => {
8876
- if (layerId) replaceRootLayerIds.add(layerId);
8877
- });
9450
+ this.producerApplyInProgress = true;
9451
+ try {
9452
+ for (const entry of entries) {
9453
+ try {
9454
+ const result = await entry.producer();
9455
+ if (!result) continue;
9456
+ const specs = Array.isArray(result.passes) ? result.passes : [];
9457
+ specs.forEach((spec) => this.mergePassSpec(passes, spec, entry.toolId));
9458
+ } catch (error) {
9459
+ console.error(
9460
+ `[CanvasService] render producer "${entry.toolId}" failed.`,
9461
+ error
9462
+ );
8878
9463
  }
8879
- } catch (error) {
8880
- console.error(
8881
- `[CanvasService] render producer "${entry.toolId}" failed.`,
8882
- error
8883
- );
8884
9464
  }
8885
- }
8886
- const nextLayerIds = new Set(groupLayerSpecs.keys());
8887
- const nextRootLayerIds = new Set(rootLayerSpecs.keys());
8888
- for (const [layerId, specs] of groupLayerSpecs.entries()) {
8889
- if (replaceLayerIds.has(layerId)) {
8890
- const layer = this.getLayer(layerId);
8891
- if (layer) {
8892
- layer.getObjects().forEach((obj) => layer.remove(obj));
8893
- }
9465
+ const nextPassIds = /* @__PURE__ */ new Set();
9466
+ const nextManagedPassMetas = /* @__PURE__ */ new Map();
9467
+ const nextEffects = [];
9468
+ for (const pass of passes.values()) {
9469
+ nextPassIds.add(pass.id);
9470
+ nextManagedPassMetas.set(pass.id, {
9471
+ id: pass.id,
9472
+ stack: pass.stack,
9473
+ order: pass.order,
9474
+ visibility: pass.visibility
9475
+ });
9476
+ await this.applyObjectSpecsToPass(pass.id, pass.objects, {
9477
+ render: false,
9478
+ replace: pass.replace
9479
+ });
9480
+ pass.effects.forEach((effect, index) => {
9481
+ const normalized = this.normalizeClipPathEffectSpec(
9482
+ effect,
9483
+ pass.id,
9484
+ index
9485
+ );
9486
+ if (!normalized) return;
9487
+ nextEffects.push(normalized);
9488
+ });
8894
9489
  }
8895
- await this.applyObjectSpecsToLayer(layerId, specs, { render: false });
8896
- }
8897
- for (const layerId of this.managedProducerLayerIds) {
8898
- if (nextLayerIds.has(layerId)) continue;
8899
- const layer = this.getLayer(layerId);
8900
- if (!layer) continue;
8901
- await this.applyObjectSpecsToContainer(layer, [], { render: false });
8902
- }
8903
- for (const [layerId, specs] of rootLayerSpecs.entries()) {
8904
- if (replaceRootLayerIds.has(layerId)) {
8905
- const existing = this.getRootLayerObjects(layerId);
8906
- existing.forEach((obj) => this.canvas.remove(obj));
9490
+ for (const passId of this.managedProducerPassIds) {
9491
+ if (nextPassIds.has(passId)) continue;
9492
+ await this.applyObjectSpecsToPass(passId, [], {
9493
+ render: false,
9494
+ replace: true
9495
+ });
8907
9496
  }
8908
- await this.applyObjectSpecsToRootLayer(layerId, specs, { render: false });
8909
- }
8910
- for (const layerId of this.managedProducerRootLayerIds) {
8911
- if (nextRootLayerIds.has(layerId)) continue;
8912
- await this.applyObjectSpecsToRootLayer(layerId, [], { render: false });
9497
+ this.managedProducerPassIds = nextPassIds;
9498
+ this.managedPassMetas = nextManagedPassMetas;
9499
+ this.managedPassEffects = nextEffects;
9500
+ this.syncManagedPassStacking(Array.from(nextManagedPassMetas.values()));
9501
+ await this.applyManagedPassEffects(nextEffects, { render: false });
9502
+ this.applyManagedPassVisibility({ render: false });
9503
+ } finally {
9504
+ this.producerApplyInProgress = false;
8913
9505
  }
8914
- this.managedProducerLayerIds = nextLayerIds;
8915
- this.managedProducerRootLayerIds = nextRootLayerIds;
8916
9506
  this.requestRenderAll();
8917
9507
  }
8918
- /**
8919
- * Get a layer (Group) by its ID.
8920
- * We assume layers are Groups directly on the canvas with a data.id property.
8921
- */
8922
- getLayer(id) {
8923
- return this.canvas.getObjects().find((obj) => {
8924
- var _a;
8925
- return ((_a = obj.data) == null ? void 0 : _a.id) === id;
8926
- });
8927
- }
8928
- /**
8929
- * Create a layer (Group) with the given ID if it doesn't exist.
8930
- */
8931
- createLayer(id, options = {}) {
8932
- let layer = this.getLayer(id);
8933
- if (!layer) {
8934
- const defaultOptions = {
8935
- selectable: false,
8936
- evented: false,
8937
- ...options,
8938
- data: { ...options.data, id }
8939
- };
8940
- layer = new Group([], defaultOptions);
8941
- this.canvas.add(layer);
8942
- }
8943
- return layer;
8944
- }
8945
- /**
8946
- * Find an object by ID, optionally within a specific layer.
8947
- */
8948
- getObject(id, layerId) {
8949
- if (layerId) {
8950
- const layer = this.getLayer(layerId);
8951
- if (!layer) return void 0;
8952
- return layer.getObjects().find((obj) => {
8953
- var _a;
8954
- return ((_a = obj.data) == null ? void 0 : _a.id) === id;
9508
+ async applyManagedPassEffects(effects = this.managedPassEffects, options = {}) {
9509
+ const effectTargetMap = /* @__PURE__ */ new Map();
9510
+ const layers = this.getPassRuntimeState();
9511
+ const visibilityContext = this.buildVisibilityEvalContext(layers);
9512
+ for (const effect of effects) {
9513
+ if (effect.type !== "clipPath") continue;
9514
+ if (!evaluateVisibilityExpr(effect.visibility, visibilityContext)) {
9515
+ continue;
9516
+ }
9517
+ effect.targetPassIds.forEach((targetPassId) => {
9518
+ this.getPassCanvasObjects(targetPassId).forEach((obj) => {
9519
+ effectTargetMap.set(obj, effect);
9520
+ });
8955
9521
  });
8956
9522
  }
9523
+ const managedObjects = this.canvas.getObjects().filter(
9524
+ (obj) => this.isManagedPassObject(obj)
9525
+ );
9526
+ const effectTemplateCache = /* @__PURE__ */ new Map();
9527
+ for (const obj of managedObjects) {
9528
+ const targetEffect = effectTargetMap.get(obj);
9529
+ if (!targetEffect) {
9530
+ this.clearClipPathEffectFromObject(obj);
9531
+ continue;
9532
+ }
9533
+ let template = effectTemplateCache.get(targetEffect.key);
9534
+ if (template === void 0) {
9535
+ template = await this.createClipPathTemplate(targetEffect);
9536
+ effectTemplateCache.set(targetEffect.key, template);
9537
+ }
9538
+ if (!template) {
9539
+ this.clearClipPathEffectFromObject(obj);
9540
+ continue;
9541
+ }
9542
+ await this.applyClipPathEffectToObject(
9543
+ obj,
9544
+ template,
9545
+ targetEffect.key
9546
+ );
9547
+ }
9548
+ if (options.render !== false) {
9549
+ this.requestRenderAll();
9550
+ }
9551
+ }
9552
+ getObject(id, passId) {
9553
+ const normalizedId = String(id || "").trim();
9554
+ if (!normalizedId) return void 0;
8957
9555
  return this.canvas.getObjects().find((obj) => {
8958
- var _a;
8959
- return ((_a = obj.data) == null ? void 0 : _a.id) === id;
9556
+ var _a, _b;
9557
+ if (((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id) !== normalizedId) return false;
9558
+ if (!passId) return true;
9559
+ return ((_b = obj == null ? void 0 : obj.data) == null ? void 0 : _b.passId) === passId;
8960
9560
  });
8961
9561
  }
8962
9562
  requestRenderAll() {
@@ -9142,114 +9742,187 @@ var CanvasService = class {
9142
9742
  next.top = objectTop + objectHeight * this.originFactor(originY);
9143
9743
  return next;
9144
9744
  }
9145
- setLayerVisibility(layerId, visible) {
9146
- const layer = this.getLayer(layerId);
9147
- if (layer) {
9148
- layer.set({ visible });
9149
- }
9150
- const objects = this.getRootLayerObjects(layerId);
9745
+ setPassVisibility(passId, visible) {
9746
+ const objects = this.getPassCanvasObjects(passId);
9747
+ let changed = false;
9151
9748
  objects.forEach((obj) => {
9152
9749
  var _a, _b;
9750
+ if (obj.visible === visible) return;
9153
9751
  (_a = obj.set) == null ? void 0 : _a.call(obj, { visible });
9154
9752
  (_b = obj.setCoords) == null ? void 0 : _b.call(obj);
9753
+ changed = true;
9155
9754
  });
9755
+ return changed;
9156
9756
  }
9157
- bringLayerToFront(layerId) {
9158
- const layer = this.getLayer(layerId);
9159
- if (layer) {
9160
- this.canvas.bringObjectToFront(layer);
9161
- }
9162
- const objects = this.getRootLayerObjects(layerId);
9163
- objects.forEach((obj) => this.canvas.bringObjectToFront(obj));
9757
+ setLayerVisibility(layerId, visible) {
9758
+ return this.setPassVisibility(layerId, visible);
9164
9759
  }
9165
- async applyLayerSpec(spec) {
9166
- const layer = this.createLayer(spec.id, spec.props || {});
9167
- await this.applyObjectSpecsToContainer(layer, spec.objects);
9760
+ bringPassToFront(passId) {
9761
+ const objects = this.getPassCanvasObjects(passId);
9762
+ objects.forEach((obj) => this.canvas.bringObjectToFront(obj));
9168
9763
  }
9169
- async applyObjectSpecsToLayer(layerId, objects, options = {}) {
9170
- const layer = this.createLayer(layerId, {});
9171
- await this.applyObjectSpecsToContainer(layer, objects, options);
9764
+ bringLayerToFront(layerId) {
9765
+ this.bringPassToFront(layerId);
9172
9766
  }
9173
- getRootLayerObjects(layerId) {
9174
- return this.canvas.getObjects().filter((obj) => {
9175
- var _a;
9176
- return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === layerId;
9767
+ async applyPassSpec(spec, options = {}) {
9768
+ await this.applyObjectSpecsToPass(spec.id, spec.objects, {
9769
+ render: options.render,
9770
+ replace: spec.replace !== false
9177
9771
  });
9178
9772
  }
9179
- async applyObjectSpecsToRootLayer(layerId, specs, options = {}) {
9180
- const desiredIds = new Set(specs.map((s) => s.id));
9181
- const existing = this.getRootLayerObjects(layerId);
9182
- existing.forEach((obj) => {
9183
- var _a;
9184
- const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
9185
- if (typeof id === "string" && !desiredIds.has(id)) {
9186
- this.canvas.remove(obj);
9187
- }
9773
+ async applyObjectSpecsToRootLayer(passId, specs, options = {}) {
9774
+ await this.applyObjectSpecsToPass(passId, specs, {
9775
+ render: options.render,
9776
+ replace: true
9188
9777
  });
9189
- const byId = /* @__PURE__ */ new Map();
9190
- this.getRootLayerObjects(layerId).forEach((obj) => {
9191
- var _a;
9192
- const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
9193
- if (typeof id === "string") byId.set(id, obj);
9778
+ }
9779
+ normalizeObjectSpecs(specs) {
9780
+ const seen = /* @__PURE__ */ new Set();
9781
+ const normalized = [];
9782
+ (specs || []).forEach((spec) => {
9783
+ const id = String((spec == null ? void 0 : spec.id) || "").trim();
9784
+ if (!id || seen.has(id)) return;
9785
+ seen.add(id);
9786
+ normalized.push({
9787
+ ...spec,
9788
+ id
9789
+ });
9194
9790
  });
9195
- for (let index = 0; index < specs.length; index += 1) {
9196
- const spec = specs[index];
9197
- let current = byId.get(spec.id);
9198
- if (current && spec.type === "image" && spec.src && current.getSrc && current.getSrc() !== spec.src) {
9199
- this.canvas.remove(current);
9200
- byId.delete(spec.id);
9201
- current = void 0;
9202
- }
9203
- if (!current) {
9204
- const created = await this.createFabricObject(spec);
9205
- if (!created) continue;
9206
- this.patchFabricObject(created, spec, { layerId });
9207
- this.canvas.add(created);
9208
- byId.set(spec.id, created);
9209
- continue;
9210
- }
9211
- this.patchFabricObject(current, spec, { layerId });
9791
+ return normalized;
9792
+ }
9793
+ async cloneFabricObject(source) {
9794
+ const clone = source.clone;
9795
+ if (typeof clone !== "function") return void 0;
9796
+ const result = clone.call(source);
9797
+ if (!result || typeof result.then !== "function") {
9798
+ return void 0;
9212
9799
  }
9213
- if (options.render !== false) {
9214
- this.requestRenderAll();
9800
+ try {
9801
+ const copied = await result;
9802
+ return copied;
9803
+ } catch (e) {
9804
+ return void 0;
9215
9805
  }
9216
9806
  }
9217
- async applyObjectSpecsToContainer(container, specs, options = {}) {
9218
- const desiredIds = new Set(specs.map((s) => s.id));
9219
- const existing = container.getObjects();
9220
- existing.forEach((obj) => {
9221
- var _a;
9222
- const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
9223
- if (typeof id === "string" && !desiredIds.has(id)) {
9224
- container.remove(obj);
9807
+ async createClipPathTemplate(effect) {
9808
+ var _a, _b;
9809
+ const source = effect.source;
9810
+ const sourceId = String(source.id || "").trim();
9811
+ if (!sourceId) return null;
9812
+ const template = await this.createFabricObject({
9813
+ ...source,
9814
+ id: sourceId,
9815
+ data: {
9816
+ ...source.data || {},
9817
+ id: sourceId,
9818
+ type: "clip-path-effect-template",
9819
+ effectKey: effect.key
9820
+ },
9821
+ props: {
9822
+ ...source.props || {},
9823
+ selectable: false,
9824
+ evented: false,
9825
+ excludeFromExport: true
9225
9826
  }
9226
9827
  });
9828
+ if (!template) return null;
9829
+ (_a = template.set) == null ? void 0 : _a.call(template, {
9830
+ selectable: false,
9831
+ evented: false,
9832
+ excludeFromExport: true,
9833
+ absolutePositioned: true
9834
+ });
9835
+ (_b = template.setCoords) == null ? void 0 : _b.call(template);
9836
+ return template;
9837
+ }
9838
+ isClipPathEffectManaged(target) {
9839
+ return typeof (target == null ? void 0 : target.__pooderEffectClipKey) === "string";
9840
+ }
9841
+ clearClipPathEffectFromObject(target) {
9842
+ var _a, _b;
9843
+ if (!target) return;
9844
+ if (!this.isClipPathEffectManaged(target)) return;
9845
+ (_a = target.set) == null ? void 0 : _a.call(target, { clipPath: void 0 });
9846
+ (_b = target.setCoords) == null ? void 0 : _b.call(target);
9847
+ delete target.__pooderEffectClipKey;
9848
+ }
9849
+ async applyClipPathEffectToObject(target, clipTemplate, effectKey) {
9850
+ var _a, _b, _c, _d;
9851
+ if (!target) return;
9852
+ const clipPath = await this.cloneFabricObject(clipTemplate);
9853
+ if (!clipPath) {
9854
+ this.clearClipPathEffectFromObject(target);
9855
+ return;
9856
+ }
9857
+ (_a = clipPath.set) == null ? void 0 : _a.call(clipPath, {
9858
+ selectable: false,
9859
+ evented: false,
9860
+ excludeFromExport: true,
9861
+ absolutePositioned: true
9862
+ });
9863
+ (_b = clipPath.setCoords) == null ? void 0 : _b.call(clipPath);
9864
+ (_c = target.set) == null ? void 0 : _c.call(target, { clipPath });
9865
+ (_d = target.setCoords) == null ? void 0 : _d.call(target);
9866
+ target.__pooderEffectClipKey = effectKey;
9867
+ }
9868
+ async applyObjectSpecsToPass(passId, specs, options = {}) {
9869
+ const normalizedPassId = String(passId || "").trim();
9870
+ if (!normalizedPassId) return;
9871
+ const replace = options.replace !== false;
9872
+ const normalizedSpecs = this.normalizeObjectSpecs(specs);
9873
+ const desiredIds = new Set(normalizedSpecs.map((s) => s.id));
9874
+ const existing = this.getPassCanvasObjects(normalizedPassId);
9875
+ if (replace) {
9876
+ existing.forEach((obj) => {
9877
+ var _a;
9878
+ const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
9879
+ if (typeof id === "string" && !desiredIds.has(id)) {
9880
+ this.canvas.remove(obj);
9881
+ }
9882
+ });
9883
+ }
9227
9884
  const byId = /* @__PURE__ */ new Map();
9228
- container.getObjects().forEach((obj) => {
9885
+ this.getPassCanvasObjects(normalizedPassId).forEach((obj) => {
9229
9886
  var _a;
9230
9887
  const id = (_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id;
9231
9888
  if (typeof id === "string") byId.set(id, obj);
9232
9889
  });
9233
- for (let index = 0; index < specs.length; index += 1) {
9234
- const spec = specs[index];
9890
+ for (let index = 0; index < normalizedSpecs.length; index += 1) {
9891
+ const spec = normalizedSpecs[index];
9235
9892
  let current = byId.get(spec.id);
9236
- if (current && spec.type === "image" && spec.src && current.getSrc && current.getSrc() !== spec.src) {
9237
- container.remove(current);
9893
+ if (spec.type === "path") {
9894
+ const nextPathData = this.readPathDataFromSpec(spec);
9895
+ if (!nextPathData || !nextPathData.trim()) {
9896
+ if (current) {
9897
+ this.canvas.remove(current);
9898
+ byId.delete(spec.id);
9899
+ }
9900
+ continue;
9901
+ }
9902
+ }
9903
+ if (current && this.shouldRecreateObject(current, spec)) {
9904
+ this.canvas.remove(current);
9238
9905
  byId.delete(spec.id);
9239
9906
  current = void 0;
9240
9907
  }
9241
9908
  if (!current) {
9242
9909
  const created = await this.createFabricObject(spec);
9243
9910
  if (!created) continue;
9244
- container.add(created);
9245
- current = created;
9246
- byId.set(spec.id, current);
9247
- } else {
9248
- this.patchFabricObject(current, spec);
9911
+ this.patchFabricObject(created, spec, {
9912
+ passId: normalizedPassId,
9913
+ layerId: normalizedPassId,
9914
+ passOrder: index
9915
+ });
9916
+ this.canvas.add(created);
9917
+ byId.set(spec.id, created);
9918
+ continue;
9249
9919
  }
9250
- this.moveObjectInContainer(container, current, index);
9920
+ this.patchFabricObject(current, spec, {
9921
+ passId: normalizedPassId,
9922
+ layerId: normalizedPassId,
9923
+ passOrder: index
9924
+ });
9251
9925
  }
9252
- container.dirty = true;
9253
9926
  if (options.render !== false) {
9254
9927
  this.requestRenderAll();
9255
9928
  }
@@ -9261,10 +9934,56 @@ var CanvasService = class {
9261
9934
  ...extraData || {},
9262
9935
  id: spec.id
9263
9936
  };
9937
+ nextData.__renderSourceKey = this.getSpecRenderSourceKey(spec);
9264
9938
  const props = this.resolveFabricProps(spec, spec.props || {});
9265
9939
  obj.set({ ...props, data: nextData });
9266
9940
  obj.setCoords();
9267
9941
  }
9942
+ readPathDataFromSpec(spec) {
9943
+ var _a, _b;
9944
+ if (spec.type !== "path") return void 0;
9945
+ const raw = ((_a = spec.props) == null ? void 0 : _a.path) || ((_b = spec.props) == null ? void 0 : _b.pathData);
9946
+ if (typeof raw !== "string") return void 0;
9947
+ return raw;
9948
+ }
9949
+ hashText(value) {
9950
+ let hash = 2166136261;
9951
+ for (let i = 0; i < value.length; i += 1) {
9952
+ hash ^= value.charCodeAt(i);
9953
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
9954
+ }
9955
+ return (hash >>> 0).toString(16);
9956
+ }
9957
+ getSpecRenderSourceKey(spec) {
9958
+ var _a, _b;
9959
+ switch (spec.type) {
9960
+ case "path": {
9961
+ const pathData = this.readPathDataFromSpec(spec) || "";
9962
+ return `path:${this.hashText(pathData)}`;
9963
+ }
9964
+ case "image":
9965
+ return `image:${String(spec.src || "")}`;
9966
+ case "text":
9967
+ return `text:${String((_b = (_a = spec.props) == null ? void 0 : _a.text) != null ? _b : "")}`;
9968
+ case "rect":
9969
+ return "rect";
9970
+ default:
9971
+ return String(spec.type || "");
9972
+ }
9973
+ }
9974
+ shouldRecreateObject(current, spec) {
9975
+ var _a;
9976
+ if (!current) return true;
9977
+ const currentType = String((current == null ? void 0 : current.type) || "").toLowerCase();
9978
+ if (currentType !== spec.type) return true;
9979
+ const expectedKey = this.getSpecRenderSourceKey(spec);
9980
+ const currentKey = String(((_a = current == null ? void 0 : current.data) == null ? void 0 : _a.__renderSourceKey) || "");
9981
+ if (currentKey && expectedKey && currentKey !== expectedKey) return true;
9982
+ if (spec.type === "image" && spec.src && current.getSrc) {
9983
+ return current.getSrc() !== spec.src;
9984
+ }
9985
+ return false;
9986
+ }
9268
9987
  resolveFabricProps(spec, props) {
9269
9988
  const space = spec.space || "scene";
9270
9989
  const next = this.resolveLayoutProps(spec, props);
@@ -9288,26 +10007,26 @@ var CanvasService = class {
9288
10007
  next.scaleY = rawScaleY * sceneScale;
9289
10008
  return next;
9290
10009
  }
9291
- moveObjectInContainer(container, obj, index) {
10010
+ moveObjectInCanvas(obj, index) {
9292
10011
  if (!obj) return;
9293
- const moveObjectTo = container.moveObjectTo;
10012
+ const moveObjectTo = this.canvas.moveObjectTo;
9294
10013
  if (typeof moveObjectTo === "function") {
9295
- moveObjectTo.call(container, obj, index);
10014
+ moveObjectTo.call(this.canvas, obj, index);
9296
10015
  return;
9297
10016
  }
9298
- const list = container._objects;
10017
+ const list = this.canvas._objects;
9299
10018
  if (!Array.isArray(list)) return;
9300
10019
  const from = list.indexOf(obj);
9301
10020
  if (from < 0 || from === index) return;
9302
10021
  list.splice(from, 1);
9303
10022
  const target = Math.max(0, Math.min(index, list.length));
9304
10023
  list.splice(target, 0, obj);
9305
- if (typeof container._onStackOrderChanged === "function") {
9306
- container._onStackOrderChanged();
10024
+ if (typeof this.canvas._onStackOrderChanged === "function") {
10025
+ this.canvas._onStackOrderChanged();
9307
10026
  }
9308
10027
  }
9309
10028
  async createFabricObject(spec) {
9310
- var _a, _b, _c, _d;
10029
+ var _a, _b;
9311
10030
  if (spec.type === "rect") {
9312
10031
  const props = this.resolveFabricProps(spec, spec.props || {});
9313
10032
  const rect = new Rect({
@@ -9318,7 +10037,7 @@ var CanvasService = class {
9318
10037
  return rect;
9319
10038
  }
9320
10039
  if (spec.type === "path") {
9321
- const pathData = ((_a = spec.props) == null ? void 0 : _a.path) || ((_b = spec.props) == null ? void 0 : _b.pathData);
10040
+ const pathData = this.readPathDataFromSpec(spec);
9322
10041
  if (!pathData) return void 0;
9323
10042
  const props = this.resolveFabricProps(spec, spec.props || {});
9324
10043
  const path = new Path2(pathData, {
@@ -9340,7 +10059,7 @@ var CanvasService = class {
9340
10059
  return image;
9341
10060
  }
9342
10061
  if (spec.type === "text") {
9343
- const content = String((_d = (_c = spec.props) == null ? void 0 : _c.text) != null ? _d : "");
10062
+ const content = String((_b = (_a = spec.props) == null ? void 0 : _a.text) != null ? _b : "");
9344
10063
  const props = this.resolveFabricProps(spec, spec.props || {});
9345
10064
  const text = new Text(content, {
9346
10065
  ...props,
@@ -9362,8 +10081,8 @@ export {
9362
10081
  MirrorTool,
9363
10082
  RulerTool,
9364
10083
  SceneLayoutService,
9365
- SceneVisibilityService,
9366
10084
  SizeTool,
9367
10085
  ViewportSystem,
9368
- WhiteInkTool
10086
+ WhiteInkTool,
10087
+ evaluateVisibilityExpr
9369
10088
  };