@pooder/kit 5.3.1 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/index.d.mts +243 -36
  3. package/dist/index.d.ts +243 -36
  4. package/dist/index.js +2278 -1041
  5. package/dist/index.mjs +2278 -1041
  6. package/package.json +1 -1
  7. package/src/coordinate.ts +106 -106
  8. package/src/extensions/background.ts +323 -230
  9. package/src/extensions/bridgeSelection.ts +17 -17
  10. package/src/extensions/constraints.ts +322 -322
  11. package/src/extensions/dieline.ts +1149 -1076
  12. package/src/extensions/dielineShape.ts +109 -0
  13. package/src/extensions/edgeScale.ts +19 -19
  14. package/src/extensions/feature.ts +1137 -1021
  15. package/src/extensions/featureComplete.ts +46 -46
  16. package/src/extensions/film.ts +266 -194
  17. package/src/extensions/geometry.ts +885 -752
  18. package/src/extensions/image.ts +2054 -1926
  19. package/src/extensions/index.ts +11 -11
  20. package/src/extensions/maskOps.ts +283 -283
  21. package/src/extensions/mirror.ts +128 -128
  22. package/src/extensions/ruler.ts +654 -451
  23. package/src/extensions/sceneLayout.ts +140 -140
  24. package/src/extensions/sceneLayoutModel.ts +364 -352
  25. package/src/extensions/sceneVisibility.ts +64 -71
  26. package/src/extensions/size.ts +389 -389
  27. package/src/extensions/tracer.ts +1019 -1019
  28. package/src/extensions/white-ink.ts +1567 -1514
  29. package/src/extensions/wrappedOffsets.ts +33 -33
  30. package/src/index.ts +2 -2
  31. package/src/services/CanvasService.ts +832 -300
  32. package/src/services/ViewportSystem.ts +95 -95
  33. package/src/services/index.ts +3 -3
  34. package/src/services/renderSpec.ts +53 -18
  35. package/src/units.ts +27 -27
  36. package/tests/run.ts +118 -118
  37. package/tsconfig.test.json +15 -15
  38. package/.test-dist/src/CanvasService.js +0 -249
  39. package/.test-dist/src/ViewportSystem.js +0 -75
  40. package/.test-dist/src/background.js +0 -203
  41. package/.test-dist/src/bridgeSelection.js +0 -20
  42. package/.test-dist/src/constraints.js +0 -237
  43. package/.test-dist/src/coordinate.js +0 -74
  44. package/.test-dist/src/dieline.js +0 -818
  45. package/.test-dist/src/edgeScale.js +0 -12
  46. package/.test-dist/src/extensions/background.js +0 -203
  47. package/.test-dist/src/extensions/bridgeSelection.js +0 -20
  48. package/.test-dist/src/extensions/constraints.js +0 -237
  49. package/.test-dist/src/extensions/dieline.js +0 -828
  50. package/.test-dist/src/extensions/edgeScale.js +0 -12
  51. package/.test-dist/src/extensions/feature.js +0 -825
  52. package/.test-dist/src/extensions/featureComplete.js +0 -32
  53. package/.test-dist/src/extensions/film.js +0 -167
  54. package/.test-dist/src/extensions/geometry.js +0 -545
  55. package/.test-dist/src/extensions/image.js +0 -1529
  56. package/.test-dist/src/extensions/index.js +0 -30
  57. package/.test-dist/src/extensions/maskOps.js +0 -279
  58. package/.test-dist/src/extensions/mirror.js +0 -104
  59. package/.test-dist/src/extensions/ruler.js +0 -345
  60. package/.test-dist/src/extensions/sceneLayout.js +0 -96
  61. package/.test-dist/src/extensions/sceneLayoutModel.js +0 -196
  62. package/.test-dist/src/extensions/sceneVisibility.js +0 -62
  63. package/.test-dist/src/extensions/size.js +0 -331
  64. package/.test-dist/src/extensions/tracer.js +0 -538
  65. package/.test-dist/src/extensions/white-ink.js +0 -1190
  66. package/.test-dist/src/extensions/wrappedOffsets.js +0 -33
  67. package/.test-dist/src/feature.js +0 -826
  68. package/.test-dist/src/featureComplete.js +0 -32
  69. package/.test-dist/src/film.js +0 -167
  70. package/.test-dist/src/geometry.js +0 -506
  71. package/.test-dist/src/image.js +0 -1250
  72. package/.test-dist/src/index.js +0 -18
  73. package/.test-dist/src/maskOps.js +0 -270
  74. package/.test-dist/src/mirror.js +0 -104
  75. package/.test-dist/src/renderSpec.js +0 -2
  76. package/.test-dist/src/ruler.js +0 -343
  77. package/.test-dist/src/sceneLayout.js +0 -99
  78. package/.test-dist/src/sceneLayoutModel.js +0 -196
  79. package/.test-dist/src/sceneView.js +0 -40
  80. package/.test-dist/src/sceneVisibility.js +0 -42
  81. package/.test-dist/src/services/CanvasService.js +0 -249
  82. package/.test-dist/src/services/ViewportSystem.js +0 -76
  83. package/.test-dist/src/services/index.js +0 -24
  84. package/.test-dist/src/services/renderSpec.js +0 -2
  85. package/.test-dist/src/size.js +0 -332
  86. package/.test-dist/src/tracer.js +0 -544
  87. package/.test-dist/src/units.js +0 -30
  88. package/.test-dist/src/white-ink.js +0 -829
  89. package/.test-dist/src/wrappedOffsets.js +0 -33
  90. package/.test-dist/tests/run.js +0 -94
package/dist/index.mjs CHANGED
@@ -2,7 +2,12 @@
2
2
  import {
3
3
  ContributionPointIds
4
4
  } from "@pooder/core";
5
- import { Rect, FabricImage as Image2 } from "fabric";
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;
6
11
  var BackgroundTool = class {
7
12
  constructor(options) {
8
13
  this.id = "pooder.kit.background";
@@ -11,17 +16,38 @@ var BackgroundTool = class {
11
16
  };
12
17
  this.color = "";
13
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
+ };
14
27
  if (options) {
15
28
  Object.assign(this, options);
16
29
  }
17
30
  }
18
31
  activate(context) {
32
+ var _a;
19
33
  this.canvasService = context.services.get("CanvasService");
20
34
  if (!this.canvasService) {
21
35
  console.warn("CanvasService not found for BackgroundTool");
22
36
  return;
23
37
  }
24
- const configService = context.services.get("ConfigurationService");
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
+ );
25
51
  if (configService) {
26
52
  this.color = configService.get("background.color", this.color);
27
53
  this.url = configService.get("background.url", this.url);
@@ -45,17 +71,25 @@ var BackgroundTool = class {
45
71
  }
46
72
  });
47
73
  }
48
- this.initLayer();
74
+ context.eventBus.on("canvas:resized", this.onCanvasResized);
49
75
  this.updateBackground();
50
76
  }
51
77
  deactivate(context) {
52
- if (this.canvasService) {
53
- const layer = this.canvasService.getLayer("background");
54
- if (layer) {
55
- this.canvasService.canvas.remove(layer);
56
- }
57
- this.canvasService = void 0;
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);
58
89
  }
90
+ void this.canvasService.flushRenderFromProducers();
91
+ this.canvasService.requestRenderAll();
92
+ this.canvasService = void 0;
59
93
  }
60
94
  contribute() {
61
95
  return {
@@ -115,88 +149,131 @@ var BackgroundTool = class {
115
149
  ]
116
150
  };
117
151
  }
118
- initLayer() {
119
- if (!this.canvasService) return;
120
- let backgroundLayer = this.canvasService.getLayer("background");
121
- if (!backgroundLayer) {
122
- backgroundLayer = this.canvasService.createLayer("background", {
123
- width: this.canvasService.canvas.width,
124
- height: this.canvasService.canvas.height,
125
- selectable: false,
126
- evented: false
127
- });
128
- this.canvasService.canvas.sendObjectToBack(backgroundLayer);
129
- }
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);
156
+ return {
157
+ width: width > 0 ? width : DEFAULT_WIDTH,
158
+ height: height > 0 ? height : DEFAULT_HEIGHT
159
+ };
130
160
  }
131
- async updateBackground() {
132
- if (!this.canvasService) return;
133
- const layer = this.canvasService.getLayer("background");
134
- if (!layer) {
135
- console.warn("[BackgroundTool] Background layer not found");
136
- return;
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;
137
189
  }
138
- const { color, url } = this;
139
- const width = this.canvasService.canvas.width || 800;
140
- const height = this.canvasService.canvas.height || 600;
141
- let rect = this.canvasService.getObject(
142
- "background-color-rect",
143
- "background"
144
- );
145
- if (rect) {
146
- rect.set({
147
- fill: color
148
- });
149
- } else {
150
- rect = new Rect({
151
- width,
152
- height,
153
- fill: color,
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,
154
211
  selectable: false,
155
212
  evented: false,
156
- data: {
157
- id: "background-color-rect"
158
- }
159
- });
160
- layer.add(rect);
161
- layer.sendObjectToBack(rect);
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;
162
225
  }
163
- let img = this.canvasService.getObject(
164
- "background-image",
165
- "background"
166
- );
226
+ const task = this.loadImageSize(src);
227
+ this.pendingSizeBySrc.set(src, task);
167
228
  try {
168
- if (img) {
169
- if (img.getSrc() !== url) {
170
- if (url) {
171
- await img.setSrc(url);
172
- } else {
173
- layer.remove(img);
174
- }
175
- }
176
- } else {
177
- if (url) {
178
- img = await Image2.fromURL(url, { crossOrigin: "anonymous" });
179
- img.set({
180
- originX: "left",
181
- originY: "top",
182
- left: 0,
183
- top: 0,
184
- selectable: false,
185
- evented: false,
186
- data: {
187
- id: "background-image"
188
- }
189
- });
190
- img.scaleToWidth(width);
191
- if (img.getScaledHeight() < height) img.scaleToHeight(height);
192
- layer.add(img);
193
- }
229
+ return await task;
230
+ } finally {
231
+ if (this.pendingSizeBySrc.get(src) === task) {
232
+ this.pendingSizeBySrc.delete(src);
194
233
  }
195
- this.canvasService.requestRenderAll();
196
- } catch (e) {
197
- console.error("[BackgroundTool] Failed to load image", e);
198
234
  }
199
- layer.dirty = true;
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);
250
+ }
251
+ return null;
252
+ }
253
+ updateBackground() {
254
+ void this.updateBackgroundAsync();
255
+ }
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
+ }
200
277
  this.canvasService.requestRenderAll();
201
278
  }
202
279
  };
@@ -207,11 +284,76 @@ import {
207
284
  } from "@pooder/core";
208
285
  import {
209
286
  Canvas as FabricCanvas,
210
- Image as FabricImage,
287
+ Image as FabricImage2,
211
288
  Pattern,
212
289
  Point
213
290
  } from "fabric";
214
291
 
292
+ // src/extensions/dielineShape.ts
293
+ var BUILTIN_DIELINE_SHAPES = [
294
+ "rect",
295
+ "circle",
296
+ "ellipse",
297
+ "heart"
298
+ ];
299
+ var DIELINE_SHAPES = [...BUILTIN_DIELINE_SHAPES, "custom"];
300
+ var DEFAULT_DIELINE_SHAPE = "rect";
301
+ var DEFAULT_HEART_SHAPE_PARAMS = {
302
+ lobeSpread: 0.46,
303
+ notchDepth: 0.24,
304
+ tipSharpness: 0
305
+ };
306
+ var DEFAULT_DIELINE_SHAPE_STYLE = {
307
+ fitMode: "contain",
308
+ ...DEFAULT_HEART_SHAPE_PARAMS
309
+ };
310
+ function isDielineShape(value) {
311
+ return typeof value === "string" && DIELINE_SHAPES.includes(value);
312
+ }
313
+ function normalizeFitMode(value, fallback) {
314
+ if (value === "contain" || value === "stretch") return value;
315
+ return fallback;
316
+ }
317
+ function normalizeUnitInterval(value, fallback) {
318
+ const num = Number(value);
319
+ if (!Number.isFinite(num)) return fallback;
320
+ return Math.max(0, Math.min(1, num));
321
+ }
322
+ function normalizeDielineShape(value, fallback = DEFAULT_DIELINE_SHAPE) {
323
+ return isDielineShape(value) ? value : fallback;
324
+ }
325
+ function normalizeShapeStyle(value, fallback = DEFAULT_DIELINE_SHAPE_STYLE) {
326
+ var _a, _b, _c;
327
+ const raw = value && typeof value === "object" ? value : {};
328
+ return {
329
+ ...fallback,
330
+ fitMode: normalizeFitMode(raw.fitMode, fallback.fitMode),
331
+ lobeSpread: normalizeUnitInterval(
332
+ raw.lobeSpread,
333
+ Number((_a = fallback.lobeSpread) != null ? _a : DEFAULT_HEART_SHAPE_PARAMS.lobeSpread)
334
+ ),
335
+ notchDepth: normalizeUnitInterval(
336
+ raw.notchDepth,
337
+ Number((_b = fallback.notchDepth) != null ? _b : DEFAULT_HEART_SHAPE_PARAMS.notchDepth)
338
+ ),
339
+ tipSharpness: normalizeUnitInterval(
340
+ raw.tipSharpness,
341
+ Number((_c = fallback.tipSharpness) != null ? _c : DEFAULT_HEART_SHAPE_PARAMS.tipSharpness)
342
+ )
343
+ };
344
+ }
345
+ function getShapeFitMode(style) {
346
+ return normalizeShapeStyle(style).fitMode;
347
+ }
348
+ function getHeartShapeParams(style) {
349
+ const normalized = normalizeShapeStyle(style);
350
+ return {
351
+ lobeSpread: Number(normalized.lobeSpread),
352
+ notchDepth: Number(normalized.notchDepth),
353
+ tipSharpness: Number(normalized.tipSharpness)
354
+ };
355
+ }
356
+
215
357
  // src/extensions/geometry.ts
216
358
  import paper from "paper";
217
359
 
@@ -354,66 +496,164 @@ function selectOuterChain(args) {
354
496
  if (scoreA !== scoreB) return scoreA > scoreB ? pointsA : pointsB;
355
497
  return pointsA.length <= pointsB.length ? pointsA : pointsB;
356
498
  }
357
- function createBaseShape(options) {
358
- var _a;
359
- const {
360
- shape,
361
- width,
362
- height,
363
- radius,
364
- x,
365
- y,
366
- pathData,
367
- customSourceWidthPx,
368
- customSourceHeightPx
369
- } = options;
370
- const center = new paper.Point(x, y);
371
- if (shape === "rect") {
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
+ )
521
+ );
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)
545
+ );
546
+ heartPath.cubicCurveTo(
547
+ new paper.Point(xPeakL - lobeCtrlX, topY),
548
+ new paper.Point(0, sideCtrlY),
549
+ new paper.Point(0, shoulderY)
550
+ );
551
+ heartPath.cubicCurveTo(
552
+ new paper.Point(0, lowerCtrlY),
553
+ new paper.Point(tipCtrlX, 1),
554
+ new paper.Point(0.5, 1)
555
+ );
556
+ heartPath.cubicCurveTo(
557
+ new paper.Point(1 - tipCtrlX, 1),
558
+ new paper.Point(1, lowerCtrlY),
559
+ new paper.Point(1, shoulderY)
560
+ );
561
+ heartPath.cubicCurveTo(
562
+ new paper.Point(1, sideCtrlY),
563
+ new paper.Point(xPeakR + lobeCtrlX, topY),
564
+ new paper.Point(xPeakR, topY)
565
+ );
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)
570
+ );
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);
584
+ }
585
+ var BUILTIN_SHAPE_BUILDERS = {
586
+ rect: (options) => {
587
+ const { x, y, width, height, radius } = options;
372
588
  return new paper.Path.Rectangle({
373
589
  point: [x - width / 2, y - height / 2],
374
590
  size: [Math.max(0, width), Math.max(0, height)],
375
591
  radius: Math.max(0, radius)
376
592
  });
377
- } else if (shape === "circle") {
593
+ },
594
+ circle: (options) => {
595
+ const { x, y, width, height } = options;
378
596
  const r = Math.min(width, height) / 2;
379
597
  return new paper.Path.Circle({
380
- center,
598
+ center: new paper.Point(x, y),
381
599
  radius: Math.max(0, r)
382
600
  });
383
- } else if (shape === "ellipse") {
601
+ },
602
+ ellipse: (options) => {
603
+ const { x, y, width, height } = options;
384
604
  return new paper.Path.Ellipse({
385
- center,
605
+ center: new paper.Point(x, y),
386
606
  radius: [Math.max(0, width / 2), Math.max(0, height / 2)]
387
607
  });
388
- } else if (shape === "custom" && pathData) {
389
- const hasMultipleSubPaths = ((_a = (pathData.match(/[Mm]/g) || []).length) != null ? _a : 0) > 1;
390
- const path = hasMultipleSubPaths ? new paper.CompoundPath(pathData) : (() => {
391
- const single = new paper.Path();
392
- single.pathData = pathData;
393
- return single;
394
- })();
395
- const sourceWidth = Number(customSourceWidthPx != null ? customSourceWidthPx : 0);
396
- const sourceHeight = Number(customSourceHeightPx != null ? customSourceHeightPx : 0);
397
- if (Number.isFinite(sourceWidth) && Number.isFinite(sourceHeight) && sourceWidth > 0 && sourceHeight > 0 && width > 0 && height > 0) {
398
- const targetLeft = x - width / 2;
399
- const targetTop = y - height / 2;
400
- path.scale(width / sourceWidth, height / sourceHeight, new paper.Point(0, 0));
401
- path.translate(new paper.Point(targetLeft, targetTop));
402
- return path;
403
- }
404
- if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
405
- path.position = center;
406
- path.scale(width / path.bounds.width, height / path.bounds.height);
407
- return path;
408
- }
608
+ },
609
+ heart: createHeartBaseShape
610
+ };
611
+ function createCustomBaseShape(options) {
612
+ var _a;
613
+ const {
614
+ pathData,
615
+ customSourceWidthPx,
616
+ customSourceHeightPx,
617
+ x,
618
+ y,
619
+ 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) {
409
642
  path.position = center;
643
+ path.scale(width / path.bounds.width, height / path.bounds.height);
410
644
  return path;
411
- } else {
412
- return new paper.Path.Rectangle({
413
- point: [x - width / 2, y - height / 2],
414
- size: [Math.max(0, width), Math.max(0, height)]
415
- });
416
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);
417
657
  }
418
658
  function resolveBridgeBasePath(shape, anchor) {
419
659
  if (shape instanceof paper.Path) {
@@ -737,6 +977,18 @@ function getNearestPointOnDieline(point, options) {
737
977
  shape.remove();
738
978
  return result;
739
979
  }
980
+ function getPathBounds(pathData) {
981
+ const path = new paper.Path();
982
+ path.pathData = pathData;
983
+ const bounds = path.bounds;
984
+ path.remove();
985
+ return {
986
+ x: bounds.x,
987
+ y: bounds.y,
988
+ width: bounds.width,
989
+ height: bounds.height
990
+ };
991
+ }
740
992
 
741
993
  // src/coordinate.ts
742
994
  var Coordinate = class {
@@ -825,12 +1077,6 @@ function parseLengthToMm(input, defaultUnit) {
825
1077
  const unit = (_b = (_a = match[2]) == null ? void 0 : _a.toLowerCase()) != null ? _b : defaultUnit;
826
1078
  return Coordinate.convertUnit(value, unit, "mm");
827
1079
  }
828
- function formatMm(valueMm, displayUnit, fractionDigits = 2) {
829
- if (!Number.isFinite(valueMm)) return "0";
830
- const value = Coordinate.convertUnit(valueMm, "mm", displayUnit);
831
- const rounded = Number(value.toFixed(fractionDigits));
832
- return rounded.toString();
833
- }
834
1080
 
835
1081
  // src/extensions/sceneLayoutModel.ts
836
1082
  var DEFAULT_SIZE_STATE = {
@@ -1045,10 +1291,15 @@ function buildSceneGeometry(configService, layout) {
1045
1291
  const sourceHeight = Number(
1046
1292
  configService.get("dieline.customSourceHeightPx", 0)
1047
1293
  );
1294
+ const shapeStyle = normalizeShapeStyle(
1295
+ configService.get("dieline.shapeStyle", DEFAULT_DIELINE_SHAPE_STYLE)
1296
+ );
1048
1297
  return {
1049
- shape: configService.get("dieline.shape", "rect"),
1050
- unit: "mm",
1051
- displayUnit: normalizeUnit(configService.get("size.unit", "mm")),
1298
+ shape: normalizeDielineShape(
1299
+ configService.get("dieline.shape", DEFAULT_DIELINE_SHAPE)
1300
+ ),
1301
+ shapeStyle,
1302
+ unit: "px",
1052
1303
  x: layout.trimRect.centerX,
1053
1304
  y: layout.trimRect.centerY,
1054
1305
  width: layout.trimRect.width,
@@ -1081,6 +1332,7 @@ var ImageTool = class {
1081
1332
  this.isImageSelectionActive = false;
1082
1333
  this.focusedImageId = null;
1083
1334
  this.renderSeq = 0;
1335
+ this.overlaySpecs = [];
1084
1336
  this.onToolActivated = (event) => {
1085
1337
  const before = this.isToolActive;
1086
1338
  this.syncToolActiveFromWorkbench(event.id);
@@ -1158,28 +1410,41 @@ var ImageTool = class {
1158
1410
  const frame = this.getFrameRect();
1159
1411
  if (!frame.width || !frame.height) return;
1160
1412
  const center = target.getCenterPoint ? target.getCenterPoint() : new Point((_c = target.left) != null ? _c : 0, (_d = target.top) != null ? _d : 0);
1413
+ const centerScene = this.canvasService ? this.canvasService.toScenePoint({ x: center.x, y: center.y }) : { x: center.x, y: center.y };
1161
1414
  const objectScale = Number.isFinite(target == null ? void 0 : target.scaleX) ? target.scaleX : 1;
1415
+ const objectScaleScene = this.toSceneObjectScale(objectScale || 1);
1162
1416
  const workingItem = this.workingItems.find((item) => item.id === id);
1163
1417
  const sourceKey = (workingItem == null ? void 0 : workingItem.sourceUrl) || (workingItem == null ? void 0 : workingItem.url) || "";
1164
1418
  const sourceSize = this.getSourceSize(sourceKey, target);
1165
1419
  const coverScale = this.getCoverScale(frame, sourceSize);
1166
1420
  const updates = {
1167
- left: this.clampNormalized((center.x - frame.left) / frame.width),
1168
- top: this.clampNormalized((center.y - frame.top) / frame.height),
1421
+ left: this.clampNormalized((centerScene.x - frame.left) / frame.width),
1422
+ top: this.clampNormalized((centerScene.y - frame.top) / frame.height),
1169
1423
  angle: Number.isFinite(target.angle) ? target.angle : 0,
1170
- scale: Math.max(0.05, (objectScale || 1) / coverScale)
1424
+ scale: Math.max(0.05, objectScaleScene / coverScale)
1171
1425
  };
1172
1426
  this.focusedImageId = id;
1173
1427
  this.updateImageInWorking(id, updates);
1174
1428
  };
1175
1429
  }
1176
1430
  activate(context) {
1431
+ var _a;
1177
1432
  this.context = context;
1178
1433
  this.canvasService = context.services.get("CanvasService");
1179
1434
  if (!this.canvasService) {
1180
1435
  console.warn("CanvasService not found for ImageTool");
1181
1436
  return;
1182
1437
  }
1438
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
1439
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
1440
+ this.id,
1441
+ () => ({
1442
+ rootLayerSpecs: {
1443
+ [IMAGE_OVERLAY_LAYER_ID]: this.overlaySpecs
1444
+ }
1445
+ }),
1446
+ { priority: 300 }
1447
+ );
1183
1448
  context.eventBus.on("tool:activated", this.onToolActivated);
1184
1449
  context.eventBus.on("object:modified", this.onObjectModified);
1185
1450
  context.eventBus.on("selection:created", this.onSelectionChanged);
@@ -1220,7 +1485,7 @@ var ImageTool = class {
1220
1485
  this.updateImages();
1221
1486
  }
1222
1487
  deactivate(context) {
1223
- var _a;
1488
+ var _a, _b;
1224
1489
  context.eventBus.off("tool:activated", this.onToolActivated);
1225
1490
  context.eventBus.off("object:modified", this.onObjectModified);
1226
1491
  context.eventBus.off("selection:created", this.onSelectionChanged);
@@ -1232,12 +1497,13 @@ var ImageTool = class {
1232
1497
  this.dirtyTrackerDisposable = void 0;
1233
1498
  this.cropShapeHatchPattern = void 0;
1234
1499
  this.cropShapeHatchPatternColor = void 0;
1500
+ this.cropShapeHatchPatternKey = void 0;
1501
+ this.overlaySpecs = [];
1235
1502
  this.clearRenderedImages();
1503
+ (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
1504
+ this.renderProducerDisposable = void 0;
1236
1505
  if (this.canvasService) {
1237
- void this.canvasService.applyObjectSpecsToRootLayer(
1238
- IMAGE_OVERLAY_LAYER_ID,
1239
- []
1240
- );
1506
+ void this.canvasService.flushRenderFromProducers();
1241
1507
  this.canvasService = void 0;
1242
1508
  }
1243
1509
  this.context = void 0;
@@ -1648,38 +1914,38 @@ var ImageTool = class {
1648
1914
  if (!layout) {
1649
1915
  return { left: 0, top: 0, width: 0, height: 0 };
1650
1916
  }
1651
- return {
1917
+ return this.canvasService.toSceneRect({
1652
1918
  left: layout.cutRect.left,
1653
1919
  top: layout.cutRect.top,
1654
1920
  width: layout.cutRect.width,
1655
1921
  height: layout.cutRect.height
1922
+ });
1923
+ }
1924
+ getFrameRectScreen(frame) {
1925
+ if (!this.canvasService) {
1926
+ return { left: 0, top: 0, width: 0, height: 0 };
1927
+ }
1928
+ return this.canvasService.toScreenRect(frame || this.getFrameRect());
1929
+ }
1930
+ toLayoutSceneRect(rect) {
1931
+ return {
1932
+ left: rect.left,
1933
+ top: rect.top,
1934
+ width: rect.width,
1935
+ height: rect.height,
1936
+ space: "scene"
1656
1937
  };
1657
1938
  }
1658
1939
  async resolveDefaultFitArea() {
1659
- if (!this.context || !this.canvasService) return null;
1660
- const commandService = this.context.services.get("CommandService");
1661
- if (!commandService) return null;
1662
- try {
1663
- const layout = await Promise.resolve(
1664
- commandService.executeCommand("getSceneLayout")
1665
- );
1666
- const cutRect = layout == null ? void 0 : layout.cutRect;
1667
- const width = Number(cutRect == null ? void 0 : cutRect.width);
1668
- const height = Number(cutRect == null ? void 0 : cutRect.height);
1669
- const left = Number(cutRect == null ? void 0 : cutRect.left);
1670
- const top = Number(cutRect == null ? void 0 : cutRect.top);
1671
- if (!Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(left) || !Number.isFinite(top)) {
1672
- return null;
1673
- }
1674
- return {
1675
- width: Math.max(1, width),
1676
- height: Math.max(1, height),
1677
- left: left + width / 2,
1678
- top: top + height / 2
1679
- };
1680
- } catch (e) {
1681
- return null;
1682
- }
1940
+ if (!this.canvasService) return null;
1941
+ const frame = this.getFrameRect();
1942
+ if (frame.width <= 0 || frame.height <= 0) return null;
1943
+ return {
1944
+ width: Math.max(1, frame.width),
1945
+ height: Math.max(1, frame.height),
1946
+ left: frame.left + frame.width / 2,
1947
+ top: frame.top + frame.height / 2
1948
+ };
1683
1949
  }
1684
1950
  async fitImageToDefaultArea(id) {
1685
1951
  if (!this.canvasService) return;
@@ -1688,13 +1954,14 @@ var ImageTool = class {
1688
1954
  await this.fitImageToArea(id, area);
1689
1955
  return;
1690
1956
  }
1691
- const canvasW = Math.max(1, this.canvasService.canvas.width || 0);
1692
- const canvasH = Math.max(1, this.canvasService.canvas.height || 0);
1957
+ const viewport = this.canvasService.getSceneViewportRect();
1958
+ const canvasW = Math.max(1, viewport.width || 0);
1959
+ const canvasH = Math.max(1, viewport.height || 0);
1693
1960
  await this.fitImageToArea(id, {
1694
1961
  width: canvasW,
1695
1962
  height: canvasH,
1696
- left: canvasW / 2,
1697
- top: canvasH / 2
1963
+ left: viewport.left + canvasW / 2,
1964
+ top: viewport.top + canvasH / 2
1698
1965
  });
1699
1966
  }
1700
1967
  getImageObjects() {
@@ -1782,13 +2049,17 @@ var ImageTool = class {
1782
2049
  }
1783
2050
  toSceneGeometryLike(raw) {
1784
2051
  const shape = raw == null ? void 0 : raw.shape;
1785
- if (shape !== "rect" && shape !== "circle" && shape !== "ellipse" && shape !== "custom") {
2052
+ if (!isDielineShape(shape)) {
1786
2053
  return null;
1787
2054
  }
1788
- const radius = Number(raw == null ? void 0 : raw.radius);
1789
- const offset = Number(raw == null ? void 0 : raw.offset);
2055
+ const radiusRaw = Number(raw == null ? void 0 : raw.radius);
2056
+ const offsetRaw = Number(raw == null ? void 0 : raw.offset);
2057
+ const unit = typeof (raw == null ? void 0 : raw.unit) === "string" ? raw.unit : "px";
2058
+ const radius = unit === "scene" || !this.canvasService ? radiusRaw : this.canvasService.toSceneLength(radiusRaw);
2059
+ const offset = unit === "scene" || !this.canvasService ? offsetRaw : this.canvasService.toSceneLength(offsetRaw);
1790
2060
  return {
1791
2061
  shape,
2062
+ shapeStyle: normalizeShapeStyle(raw == null ? void 0 : raw.shapeStyle),
1792
2063
  radius: Number.isFinite(radius) ? radius : 0,
1793
2064
  offset: Number.isFinite(offset) ? offset : 0
1794
2065
  };
@@ -1840,8 +2111,11 @@ var ImageTool = class {
1840
2111
  return Math.max(0, Math.min(maxRadius, rawCutRadius));
1841
2112
  }
1842
2113
  getCropShapeHatchPattern(color = "rgba(255, 0, 0, 0.6)") {
2114
+ var _a;
1843
2115
  if (typeof document === "undefined") return void 0;
1844
- if (this.cropShapeHatchPattern && this.cropShapeHatchPatternColor === color) {
2116
+ const sceneScale = ((_a = this.canvasService) == null ? void 0 : _a.getSceneScale()) || 1;
2117
+ const cacheKey = `${color}::${sceneScale.toFixed(6)}`;
2118
+ if (this.cropShapeHatchPattern && this.cropShapeHatchPatternColor === color && this.cropShapeHatchPatternKey === cacheKey) {
1845
2119
  return this.cropShapeHatchPattern;
1846
2120
  }
1847
2121
  const size = 16;
@@ -1870,11 +2144,21 @@ var ImageTool = class {
1870
2144
  // @ts-ignore: Fabric Pattern accepts canvas source here.
1871
2145
  repetition: "repeat"
1872
2146
  });
2147
+ pattern.patternTransform = [
2148
+ 1 / sceneScale,
2149
+ 0,
2150
+ 0,
2151
+ 1 / sceneScale,
2152
+ 0,
2153
+ 0
2154
+ ];
1873
2155
  this.cropShapeHatchPattern = pattern;
1874
2156
  this.cropShapeHatchPatternColor = color;
2157
+ this.cropShapeHatchPatternKey = cacheKey;
1875
2158
  return pattern;
1876
2159
  }
1877
2160
  buildCropShapeOverlaySpecs(frame, sceneGeometry) {
2161
+ var _a, _b;
1878
2162
  if (!sceneGeometry) {
1879
2163
  this.debug("overlay:shape:skip", { reason: "scene-geometry-missing" });
1880
2164
  return [];
@@ -1884,6 +2168,7 @@ var ImageTool = class {
1884
2168
  return [];
1885
2169
  }
1886
2170
  const shape = sceneGeometry.shape;
2171
+ const shapeStyle = sceneGeometry.shapeStyle;
1887
2172
  const inset = 0;
1888
2173
  const shapeWidth = Math.max(1, frame.width);
1889
2174
  const shapeHeight = Math.max(1, frame.height);
@@ -1893,6 +2178,7 @@ var ImageTool = class {
1893
2178
  frameWidth: frame.width,
1894
2179
  frameHeight: frame.height,
1895
2180
  offset: sceneGeometry.offset,
2181
+ shapeStyle,
1896
2182
  inset,
1897
2183
  shapeWidth,
1898
2184
  shapeHeight,
@@ -1914,6 +2200,7 @@ var ImageTool = class {
1914
2200
  x: frame.width / 2,
1915
2201
  y: frame.height / 2,
1916
2202
  features: [],
2203
+ shapeStyle,
1917
2204
  canvasWidth: frame.width,
1918
2205
  canvasHeight: frame.height
1919
2206
  };
@@ -1931,6 +2218,9 @@ var ImageTool = class {
1931
2218
  }
1932
2219
  const patternFill = this.getCropShapeHatchPattern();
1933
2220
  const hatchFill = patternFill || "rgba(255, 0, 0, 0.22)";
2221
+ const shapeBounds = getPathBounds(shapePathData);
2222
+ const hatchBounds = getPathBounds(hatchPathData);
2223
+ const frameRect = this.toLayoutSceneRect(frame);
1934
2224
  const hatchPathLength = hatchPathData.length;
1935
2225
  const shapePathLength = shapePathData.length;
1936
2226
  const specs = [
@@ -1938,10 +2228,16 @@ var ImageTool = class {
1938
2228
  id: "image.cropShapeHatch",
1939
2229
  type: "path",
1940
2230
  data: { id: "image.cropShapeHatch", zIndex: 5 },
2231
+ layout: {
2232
+ reference: "custom",
2233
+ referenceRect: frameRect,
2234
+ alignX: "start",
2235
+ alignY: "start",
2236
+ offsetX: hatchBounds.x,
2237
+ offsetY: hatchBounds.y
2238
+ },
1941
2239
  props: {
1942
2240
  pathData: hatchPathData,
1943
- left: frame.left,
1944
- top: frame.top,
1945
2241
  originX: "left",
1946
2242
  originY: "top",
1947
2243
  fill: hatchFill,
@@ -1958,15 +2254,21 @@ var ImageTool = class {
1958
2254
  id: "image.cropShapePath",
1959
2255
  type: "path",
1960
2256
  data: { id: "image.cropShapePath", zIndex: 6 },
2257
+ layout: {
2258
+ reference: "custom",
2259
+ referenceRect: frameRect,
2260
+ alignX: "start",
2261
+ alignY: "start",
2262
+ offsetX: shapeBounds.x,
2263
+ offsetY: shapeBounds.y
2264
+ },
1961
2265
  props: {
1962
2266
  pathData: shapePathData,
1963
- left: frame.left,
1964
- top: frame.top,
1965
2267
  originX: "left",
1966
2268
  originY: "top",
1967
2269
  fill: "rgba(0,0,0,0)",
1968
2270
  stroke: "rgba(255, 0, 0, 0.9)",
1969
- strokeWidth: 1,
2271
+ strokeWidth: (_b = (_a = this.canvasService) == null ? void 0 : _a.toSceneLength(1)) != null ? _b : 1,
1970
2272
  selectable: false,
1971
2273
  evented: false,
1972
2274
  excludeFromExport: true,
@@ -1983,6 +2285,8 @@ var ImageTool = class {
1983
2285
  fillRule: "evenodd",
1984
2286
  shapePathLength,
1985
2287
  hatchPathLength,
2288
+ shapeBounds,
2289
+ hatchBounds,
1986
2290
  hatchFillType: hatchFill && typeof hatchFill === "object" ? "pattern" : "color",
1987
2291
  ids: specs.map((spec) => spec.id)
1988
2292
  });
@@ -2045,6 +2349,28 @@ var ImageTool = class {
2045
2349
  opacity: render.opacity
2046
2350
  };
2047
2351
  }
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
+ toSceneObjectScale(value) {
2371
+ if (!this.canvasService) return value;
2372
+ return value / this.canvasService.getSceneScale();
2373
+ }
2048
2374
  getCurrentSrc(obj) {
2049
2375
  var _a;
2050
2376
  if (!obj) return void 0;
@@ -2077,7 +2403,7 @@ var ImageTool = class {
2077
2403
  obj = void 0;
2078
2404
  }
2079
2405
  if (!obj) {
2080
- const created = await FabricImage.fromURL(render.src, {
2406
+ const created = await FabricImage2.fromURL(render.src, {
2081
2407
  crossOrigin: "anonymous"
2082
2408
  });
2083
2409
  if (seq !== this.renderSeq) return;
@@ -2094,8 +2420,9 @@ var ImageTool = class {
2094
2420
  this.rememberSourceSize(render.src, obj);
2095
2421
  const sourceSize = this.getSourceSize(render.src, obj);
2096
2422
  const props = this.computeCanvasProps(render, sourceSize, frame);
2423
+ const screenProps = this.toScreenObjectProps(props);
2097
2424
  obj.set({
2098
- ...props,
2425
+ ...screenProps,
2099
2426
  data: {
2100
2427
  ...obj.data || {},
2101
2428
  id: item.id,
@@ -2161,37 +2488,67 @@ var ImageTool = class {
2161
2488
  });
2162
2489
  return [];
2163
2490
  }
2164
- const canvasW = this.canvasService.canvas.width || 0;
2165
- const canvasH = this.canvasService.canvas.height || 0;
2491
+ const viewport = this.canvasService.getSceneViewportRect();
2492
+ const canvasW = viewport.width || 0;
2493
+ const canvasH = viewport.height || 0;
2494
+ const canvasLeft = viewport.left || 0;
2495
+ const canvasTop = viewport.top || 0;
2166
2496
  const visual = this.getFrameVisualConfig();
2167
- const frameLeft = Math.max(0, Math.min(canvasW, frame.left));
2168
- const frameTop = Math.max(0, Math.min(canvasH, frame.top));
2497
+ const strokeWidthScene = this.canvasService.toSceneLength(
2498
+ visual.strokeWidth
2499
+ );
2500
+ const dashLengthScene = this.canvasService.toSceneLength(visual.dashLength);
2501
+ const frameLeft = Math.max(
2502
+ canvasLeft,
2503
+ Math.min(canvasLeft + canvasW, frame.left)
2504
+ );
2505
+ const frameTop = Math.max(
2506
+ canvasTop,
2507
+ Math.min(canvasTop + canvasH, frame.top)
2508
+ );
2169
2509
  const frameRight = Math.max(
2170
2510
  frameLeft,
2171
- Math.min(canvasW, frame.left + frame.width)
2511
+ Math.min(canvasLeft + canvasW, frame.left + frame.width)
2172
2512
  );
2173
2513
  const frameBottom = Math.max(
2174
2514
  frameTop,
2175
- Math.min(canvasH, frame.top + frame.height)
2515
+ Math.min(canvasTop + canvasH, frame.top + frame.height)
2176
2516
  );
2177
2517
  const visibleFrameH = Math.max(0, frameBottom - frameTop);
2178
- const topH = frameTop;
2179
- const bottomH = Math.max(0, canvasH - frameBottom);
2180
- const leftW = frameLeft;
2181
- const rightW = Math.max(0, canvasW - frameRight);
2518
+ const topH = Math.max(0, frameTop - canvasTop);
2519
+ const bottomH = Math.max(0, canvasTop + canvasH - frameBottom);
2520
+ const leftW = Math.max(0, frameLeft - canvasLeft);
2521
+ const rightW = Math.max(0, canvasLeft + canvasW - frameRight);
2522
+ const viewportRect = this.toLayoutSceneRect({
2523
+ left: canvasLeft,
2524
+ top: canvasTop,
2525
+ width: canvasW,
2526
+ height: canvasH
2527
+ });
2528
+ const visibleFrameBandRect = this.toLayoutSceneRect({
2529
+ left: canvasLeft,
2530
+ top: frameTop,
2531
+ width: canvasW,
2532
+ height: visibleFrameH
2533
+ });
2534
+ const frameRect = this.toLayoutSceneRect(frame);
2182
2535
  const shapeOverlay = this.buildCropShapeOverlaySpecs(frame, sceneGeometry);
2183
2536
  const mask = [
2184
2537
  {
2185
2538
  id: "image.cropMask.top",
2186
2539
  type: "rect",
2187
2540
  data: { id: "image.cropMask.top", zIndex: 1 },
2541
+ layout: {
2542
+ reference: "custom",
2543
+ referenceRect: viewportRect,
2544
+ alignX: "start",
2545
+ alignY: "start",
2546
+ width: "100%",
2547
+ height: topH
2548
+ },
2188
2549
  props: {
2189
- left: canvasW / 2,
2190
- top: topH / 2,
2191
- width: canvasW,
2192
- height: topH,
2193
- originX: "center",
2194
- originY: "center",
2550
+ originX: "left",
2551
+ originY: "top",
2195
2552
  fill: visual.outerBackground,
2196
2553
  selectable: false,
2197
2554
  evented: false
@@ -2201,13 +2558,17 @@ var ImageTool = class {
2201
2558
  id: "image.cropMask.bottom",
2202
2559
  type: "rect",
2203
2560
  data: { id: "image.cropMask.bottom", zIndex: 2 },
2561
+ layout: {
2562
+ reference: "custom",
2563
+ referenceRect: viewportRect,
2564
+ alignX: "start",
2565
+ alignY: "end",
2566
+ width: "100%",
2567
+ height: bottomH
2568
+ },
2204
2569
  props: {
2205
- left: canvasW / 2,
2206
- top: frameBottom + bottomH / 2,
2207
- width: canvasW,
2208
- height: bottomH,
2209
- originX: "center",
2210
- originY: "center",
2570
+ originX: "left",
2571
+ originY: "top",
2211
2572
  fill: visual.outerBackground,
2212
2573
  selectable: false,
2213
2574
  evented: false
@@ -2217,13 +2578,17 @@ var ImageTool = class {
2217
2578
  id: "image.cropMask.left",
2218
2579
  type: "rect",
2219
2580
  data: { id: "image.cropMask.left", zIndex: 3 },
2220
- props: {
2221
- left: leftW / 2,
2222
- top: frameTop + visibleFrameH / 2,
2581
+ layout: {
2582
+ reference: "custom",
2583
+ referenceRect: visibleFrameBandRect,
2584
+ alignX: "start",
2585
+ alignY: "start",
2223
2586
  width: leftW,
2224
- height: visibleFrameH,
2225
- originX: "center",
2226
- originY: "center",
2587
+ height: "100%"
2588
+ },
2589
+ props: {
2590
+ originX: "left",
2591
+ originY: "top",
2227
2592
  fill: visual.outerBackground,
2228
2593
  selectable: false,
2229
2594
  evented: false
@@ -2233,13 +2598,17 @@ var ImageTool = class {
2233
2598
  id: "image.cropMask.right",
2234
2599
  type: "rect",
2235
2600
  data: { id: "image.cropMask.right", zIndex: 4 },
2236
- props: {
2237
- left: frameRight + rightW / 2,
2238
- top: frameTop + visibleFrameH / 2,
2601
+ layout: {
2602
+ reference: "custom",
2603
+ referenceRect: visibleFrameBandRect,
2604
+ alignX: "end",
2605
+ alignY: "start",
2239
2606
  width: rightW,
2240
- height: visibleFrameH,
2241
- originX: "center",
2242
- originY: "center",
2607
+ height: "100%"
2608
+ },
2609
+ props: {
2610
+ originX: "left",
2611
+ originY: "top",
2243
2612
  fill: visual.outerBackground,
2244
2613
  selectable: false,
2245
2614
  evented: false
@@ -2250,17 +2619,21 @@ var ImageTool = class {
2250
2619
  id: "image.cropFrame",
2251
2620
  type: "rect",
2252
2621
  data: { id: "image.cropFrame", zIndex: 7 },
2622
+ layout: {
2623
+ reference: "custom",
2624
+ referenceRect: frameRect,
2625
+ alignX: "start",
2626
+ alignY: "start",
2627
+ width: "100%",
2628
+ height: "100%"
2629
+ },
2253
2630
  props: {
2254
- left: frame.left + frame.width / 2,
2255
- top: frame.top + frame.height / 2,
2256
- width: frame.width,
2257
- height: frame.height,
2258
- originX: "center",
2259
- originY: "center",
2631
+ originX: "left",
2632
+ originY: "top",
2260
2633
  fill: visual.innerBackground,
2261
2634
  stroke: visual.strokeStyle === "hidden" ? "rgba(0,0,0,0)" : visual.strokeColor,
2262
- strokeWidth: visual.strokeStyle === "hidden" ? 0 : visual.strokeWidth,
2263
- strokeDashArray: visual.strokeStyle === "dashed" ? [visual.dashLength, visual.dashLength] : void 0,
2635
+ strokeWidth: visual.strokeStyle === "hidden" ? 0 : strokeWidthScene,
2636
+ strokeDashArray: visual.strokeStyle === "dashed" ? [dashLengthScene, dashLengthScene] : void 0,
2264
2637
  selectable: false,
2265
2638
  evented: false
2266
2639
  }
@@ -2311,10 +2684,8 @@ var ImageTool = class {
2311
2684
  const sceneGeometry = await this.resolveSceneGeometryForOverlay();
2312
2685
  if (seq !== this.renderSeq) return;
2313
2686
  const overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
2314
- await this.canvasService.applyObjectSpecsToRootLayer(
2315
- IMAGE_OVERLAY_LAYER_ID,
2316
- overlaySpecs
2317
- );
2687
+ this.overlaySpecs = overlaySpecs;
2688
+ await this.canvasService.flushRenderFromProducers();
2318
2689
  this.syncImageZOrder(renderItems);
2319
2690
  const overlayCanvasCount = this.getOverlayObjects().length;
2320
2691
  this.debug("render:done", {
@@ -2405,7 +2776,7 @@ var ImageTool = class {
2405
2776
  const source = this.getSourceSize(render.src, obj);
2406
2777
  const frame = this.getFrameRect();
2407
2778
  const coverScale = this.getCoverScale(frame, source);
2408
- const currentScale = obj.scaleX || 1;
2779
+ const currentScale = this.toSceneObjectScale(obj.scaleX || 1);
2409
2780
  const zoom = Math.max(0.05, currentScale / coverScale);
2410
2781
  const updated = {
2411
2782
  scale: Number.isFinite(zoom) ? zoom : 1,
@@ -2442,12 +2813,13 @@ var ImageTool = class {
2442
2813
  Math.max(1, area.width) / Math.max(1, source.width),
2443
2814
  Math.max(1, area.height) / Math.max(1, source.height)
2444
2815
  );
2445
- const canvasW = this.canvasService.canvas.width || 1;
2446
- const canvasH = this.canvasService.canvas.height || 1;
2816
+ const viewport = this.canvasService.getSceneViewportRect();
2817
+ const canvasW = viewport.width || 1;
2818
+ const canvasH = viewport.height || 1;
2447
2819
  const areaLeftInput = (_a = area.left) != null ? _a : 0.5;
2448
2820
  const areaTopInput = (_b = area.top) != null ? _b : 0.5;
2449
- const areaLeftPx = areaLeftInput <= 1.5 ? areaLeftInput * canvasW : areaLeftInput;
2450
- const areaTopPx = areaTopInput <= 1.5 ? areaTopInput * canvasH : areaTopInput;
2821
+ const areaLeftPx = areaLeftInput <= 1.5 ? viewport.left + areaLeftInput * canvasW : areaLeftInput;
2822
+ const areaTopPx = areaTopInput <= 1.5 ? viewport.top + areaTopInput * canvasH : areaTopInput;
2451
2823
  const updates = {
2452
2824
  scale: Math.max(0.05, desiredScale / baseCover),
2453
2825
  left: this.clampNormalized(
@@ -2512,7 +2884,8 @@ var ImageTool = class {
2512
2884
  if (!normalizedIds.length) {
2513
2885
  throw new Error("image-ids-required");
2514
2886
  }
2515
- const frame = this.getFrameRect();
2887
+ const frameScene = this.getFrameRect();
2888
+ const frame = this.getFrameRectScreen(frameScene);
2516
2889
  const multiplier = Math.max(1, (_a = options.multiplier) != null ? _a : 2);
2517
2890
  const format = options.format === "jpeg" ? "jpeg" : "png";
2518
2891
  const width = Math.max(1, Math.round(frame.width * multiplier));
@@ -3940,6 +4313,7 @@ var ImageTracer = class {
3940
4313
 
3941
4314
  // src/extensions/dieline.ts
3942
4315
  var IMAGE_OBJECT_LAYER_ID2 = "image.user";
4316
+ var DIELINE_LAYER_ID = "dieline-overlay";
3943
4317
  var DielineTool = class {
3944
4318
  constructor(options) {
3945
4319
  this.id = "pooder.kit.dieline";
@@ -3947,8 +4321,8 @@ var DielineTool = class {
3947
4321
  name: "DielineTool"
3948
4322
  };
3949
4323
  this.state = {
3950
- displayUnit: "mm",
3951
- shape: "rect",
4324
+ shape: DEFAULT_DIELINE_SHAPE,
4325
+ shapeStyle: { ...DEFAULT_DIELINE_SHAPE_STYLE },
3952
4326
  width: 500,
3953
4327
  height: 500,
3954
4328
  radius: 0,
@@ -3971,6 +4345,8 @@ var DielineTool = class {
3971
4345
  showBleedLines: true,
3972
4346
  features: []
3973
4347
  };
4348
+ this.specs = [];
4349
+ this.renderSeq = 0;
3974
4350
  this.onCanvasResized = () => {
3975
4351
  this.updateDieline();
3976
4352
  };
@@ -3983,24 +4359,50 @@ var DielineTool = class {
3983
4359
  Object.assign(this.state.offsetLine, options.offsetLine);
3984
4360
  delete options.offsetLine;
3985
4361
  }
4362
+ if (options.shapeStyle) {
4363
+ this.state.shapeStyle = normalizeShapeStyle(
4364
+ options.shapeStyle,
4365
+ this.state.shapeStyle
4366
+ );
4367
+ delete options.shapeStyle;
4368
+ }
3986
4369
  Object.assign(this.state, options);
4370
+ this.state.shape = normalizeDielineShape(options.shape, this.state.shape);
3987
4371
  }
3988
4372
  }
3989
4373
  activate(context) {
4374
+ var _a;
3990
4375
  this.context = context;
3991
4376
  this.canvasService = context.services.get("CanvasService");
3992
4377
  if (!this.canvasService) {
3993
4378
  console.warn("CanvasService not found for DielineTool");
3994
4379
  return;
3995
4380
  }
4381
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
4382
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
4383
+ this.id,
4384
+ () => ({
4385
+ layerSpecs: {
4386
+ [DIELINE_LAYER_ID]: this.specs
4387
+ },
4388
+ replaceLayerIds: [DIELINE_LAYER_ID]
4389
+ }),
4390
+ { priority: 250 }
4391
+ );
3996
4392
  const configService = context.services.get(
3997
4393
  "ConfigurationService"
3998
4394
  );
3999
4395
  if (configService) {
4000
4396
  const s = this.state;
4001
4397
  const sizeState = readSizeState(configService);
4002
- s.displayUnit = sizeState.unit;
4003
- s.shape = configService.get("dieline.shape", s.shape);
4398
+ s.shape = normalizeDielineShape(
4399
+ configService.get("dieline.shape", s.shape),
4400
+ s.shape
4401
+ );
4402
+ s.shapeStyle = normalizeShapeStyle(
4403
+ configService.get("dieline.shapeStyle", s.shapeStyle),
4404
+ s.shapeStyle
4405
+ );
4004
4406
  s.width = sizeState.actualWidthMm;
4005
4407
  s.height = sizeState.actualHeightMm;
4006
4408
  s.radius = parseLengthToMm(
@@ -4060,7 +4462,6 @@ var DielineTool = class {
4060
4462
  configService.onAnyChange((e) => {
4061
4463
  if (e.key.startsWith("size.")) {
4062
4464
  const nextSize = readSizeState(configService);
4063
- s.displayUnit = nextSize.unit;
4064
4465
  s.width = nextSize.actualWidthMm;
4065
4466
  s.height = nextSize.actualHeightMm;
4066
4467
  s.padding = nextSize.viewPadding;
@@ -4071,7 +4472,10 @@ var DielineTool = class {
4071
4472
  if (e.key.startsWith("dieline.")) {
4072
4473
  switch (e.key) {
4073
4474
  case "dieline.shape":
4074
- s.shape = e.value;
4475
+ s.shape = normalizeDielineShape(e.value, s.shape);
4476
+ break;
4477
+ case "dieline.shapeStyle":
4478
+ s.shapeStyle = normalizeShapeStyle(e.value, s.shapeStyle);
4075
4479
  break;
4076
4480
  case "dieline.radius":
4077
4481
  s.radius = parseLengthToMm(e.value, "mm");
@@ -4127,12 +4531,18 @@ var DielineTool = class {
4127
4531
  });
4128
4532
  }
4129
4533
  context.eventBus.on("canvas:resized", this.onCanvasResized);
4130
- this.createLayer();
4131
4534
  this.updateDieline();
4132
4535
  }
4133
4536
  deactivate(context) {
4537
+ var _a;
4134
4538
  context.eventBus.off("canvas:resized", this.onCanvasResized);
4135
- this.destroyLayer();
4539
+ this.renderSeq += 1;
4540
+ this.specs = [];
4541
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
4542
+ this.renderProducerDisposable = void 0;
4543
+ if (this.canvasService) {
4544
+ void this.canvasService.flushRenderFromProducers();
4545
+ }
4136
4546
  this.canvasService = void 0;
4137
4547
  this.context = void 0;
4138
4548
  }
@@ -4155,7 +4565,7 @@ var DielineTool = class {
4155
4565
  id: "dieline.shape",
4156
4566
  type: "select",
4157
4567
  label: "Shape",
4158
- options: ["rect", "circle", "ellipse", "custom"],
4568
+ options: Array.from(DIELINE_SHAPES),
4159
4569
  default: s.shape
4160
4570
  },
4161
4571
  {
@@ -4166,6 +4576,12 @@ var DielineTool = class {
4166
4576
  max: 500,
4167
4577
  default: s.radius
4168
4578
  },
4579
+ {
4580
+ id: "dieline.shapeStyle",
4581
+ type: "json",
4582
+ label: "Shape Style",
4583
+ default: s.shapeStyle
4584
+ },
4169
4585
  {
4170
4586
  id: "dieline.showBleedLines",
4171
4587
  type: "boolean",
@@ -4341,34 +4757,6 @@ var DielineTool = class {
4341
4757
  ]
4342
4758
  };
4343
4759
  }
4344
- getLayer() {
4345
- var _a;
4346
- return (_a = this.canvasService) == null ? void 0 : _a.getLayer("dieline-overlay");
4347
- }
4348
- createLayer() {
4349
- if (!this.canvasService) return;
4350
- const width = this.canvasService.canvas.width || 800;
4351
- const height = this.canvasService.canvas.height || 600;
4352
- const layer = this.canvasService.createLayer("dieline-overlay", {
4353
- width,
4354
- height,
4355
- selectable: false,
4356
- evented: false
4357
- });
4358
- this.canvasService.canvas.bringObjectToFront(layer);
4359
- const userLayer = this.canvasService.getLayer("user");
4360
- if (userLayer) {
4361
- const userIndex = this.canvasService.canvas.getObjects().indexOf(userLayer);
4362
- this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
4363
- }
4364
- }
4365
- destroyLayer() {
4366
- if (!this.canvasService) return;
4367
- const layer = this.getLayer();
4368
- if (layer) {
4369
- this.canvasService.canvas.remove(layer);
4370
- }
4371
- }
4372
4760
  createHatchPattern(color = "rgba(0, 0, 0, 0.3)") {
4373
4761
  if (typeof document === "undefined") {
4374
4762
  return void 0;
@@ -4397,7 +4785,6 @@ var DielineTool = class {
4397
4785
  }
4398
4786
  syncSizeState(configService) {
4399
4787
  const sizeState = readSizeState(configService);
4400
- this.state.displayUnit = sizeState.unit;
4401
4788
  this.state.width = sizeState.actualWidthMm;
4402
4789
  this.state.height = sizeState.actualHeightMm;
4403
4790
  this.state.padding = sizeState.viewPadding;
@@ -4411,20 +4798,26 @@ var DielineTool = class {
4411
4798
  return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.type) === "feature-marker";
4412
4799
  }).forEach((obj) => canvas.bringObjectToFront(obj));
4413
4800
  }
4414
- updateDieline(_emitEvent = true) {
4801
+ ensureLayerStacking() {
4415
4802
  if (!this.canvasService) return;
4416
- const layer = this.getLayer();
4803
+ const layer = this.canvasService.getLayer(DIELINE_LAYER_ID);
4417
4804
  if (!layer) return;
4418
- const configService = this.getConfigService();
4419
- if (!configService) return;
4420
- this.syncSizeState(configService);
4421
- const sceneLayout = computeSceneLayout(
4422
- this.canvasService,
4423
- readSizeState(configService)
4424
- );
4425
- if (!sceneLayout) 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
+ buildDielineSpecs(sceneLayout) {
4817
+ var _a, _b;
4426
4818
  const {
4427
4819
  shape,
4820
+ shapeStyle,
4428
4821
  radius,
4429
4822
  mainLine,
4430
4823
  offsetLine,
@@ -4433,8 +4826,8 @@ var DielineTool = class {
4433
4826
  showBleedLines,
4434
4827
  features
4435
4828
  } = this.state;
4436
- const canvasW = sceneLayout.canvasWidth || this.canvasService.canvas.width || 800;
4437
- const canvasH = sceneLayout.canvasHeight || this.canvasService.canvas.height || 600;
4829
+ const canvasW = sceneLayout.canvasWidth || ((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 800;
4830
+ const canvasH = sceneLayout.canvasHeight || ((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 600;
4438
4831
  const scale = sceneLayout.scale;
4439
4832
  const cx = sceneLayout.trimRect.centerX;
4440
4833
  const cy = sceneLayout.trimRect.centerY;
@@ -4445,7 +4838,6 @@ var DielineTool = class {
4445
4838
  const cutH = sceneLayout.cutRect.height;
4446
4839
  const visualOffset = (cutW - visualWidth) / 2;
4447
4840
  const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
4448
- layer.remove(...layer.getObjects());
4449
4841
  const absoluteFeatures = (features || []).map((f) => ({
4450
4842
  ...f,
4451
4843
  x: f.x,
@@ -4465,21 +4857,30 @@ var DielineTool = class {
4465
4857
  x: cx,
4466
4858
  y: cy,
4467
4859
  features: cutFeatures,
4860
+ shapeStyle,
4468
4861
  pathData: this.state.pathData,
4469
4862
  customSourceWidthPx: this.state.customSourceWidthPx,
4470
4863
  customSourceHeightPx: this.state.customSourceHeightPx
4471
4864
  });
4472
- const mask = new Path(maskPathData, {
4473
- fill: outsideColor,
4474
- stroke: null,
4475
- selectable: false,
4476
- evented: false,
4477
- originX: "left",
4478
- originY: "top",
4479
- left: 0,
4480
- top: 0
4481
- });
4482
- layer.add(mask);
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
+ ];
4483
4884
  if (insideColor && insideColor !== "transparent" && insideColor !== "rgba(0,0,0,0)") {
4484
4885
  const productPathData = generateDielinePath({
4485
4886
  shape,
@@ -4489,21 +4890,28 @@ var DielineTool = class {
4489
4890
  x: cx,
4490
4891
  y: cy,
4491
4892
  features: cutFeatures,
4893
+ shapeStyle,
4492
4894
  pathData: this.state.pathData,
4493
4895
  customSourceWidthPx: this.state.customSourceWidthPx,
4494
4896
  customSourceHeightPx: this.state.customSourceHeightPx,
4495
4897
  canvasWidth: canvasW,
4496
4898
  canvasHeight: canvasH
4497
4899
  });
4498
- const insideObj = new Path(productPathData, {
4499
- fill: insideColor,
4500
- stroke: null,
4501
- selectable: false,
4502
- evented: false,
4503
- originX: "left",
4504
- originY: "top"
4900
+ specs.push({
4901
+ id: "dieline.inside",
4902
+ type: "path",
4903
+ space: "screen",
4904
+ data: { id: "dieline.inside", type: "dieline" },
4905
+ props: {
4906
+ pathData: productPathData,
4907
+ fill: insideColor,
4908
+ stroke: null,
4909
+ selectable: false,
4910
+ evented: false,
4911
+ originX: "left",
4912
+ originY: "top"
4913
+ }
4505
4914
  });
4506
- layer.add(insideObj);
4507
4915
  }
4508
4916
  if (Math.abs(visualOffset) > 1e-4) {
4509
4917
  const bleedPathData = generateBleedZonePath(
@@ -4515,6 +4923,7 @@ var DielineTool = class {
4515
4923
  x: cx,
4516
4924
  y: cy,
4517
4925
  features: cutFeatures,
4926
+ shapeStyle,
4518
4927
  pathData: this.state.pathData,
4519
4928
  customSourceWidthPx: this.state.customSourceWidthPx,
4520
4929
  customSourceHeightPx: this.state.customSourceHeightPx,
@@ -4529,6 +4938,7 @@ var DielineTool = class {
4529
4938
  x: cx,
4530
4939
  y: cy,
4531
4940
  features: cutFeatures,
4941
+ shapeStyle,
4532
4942
  pathData: this.state.pathData,
4533
4943
  customSourceWidthPx: this.state.customSourceWidthPx,
4534
4944
  customSourceHeightPx: this.state.customSourceHeightPx,
@@ -4540,16 +4950,22 @@ var DielineTool = class {
4540
4950
  if (showBleedLines !== false) {
4541
4951
  const pattern = this.createHatchPattern(mainLine.color);
4542
4952
  if (pattern) {
4543
- const bleedObj = new Path(bleedPathData, {
4544
- fill: pattern,
4545
- stroke: null,
4546
- selectable: false,
4547
- evented: false,
4548
- objectCaching: false,
4549
- originX: "left",
4550
- originY: "top"
4953
+ specs.push({
4954
+ id: "dieline.bleed-zone",
4955
+ type: "path",
4956
+ space: "screen",
4957
+ data: { id: "dieline.bleed-zone", type: "dieline" },
4958
+ props: {
4959
+ pathData: bleedPathData,
4960
+ fill: pattern,
4961
+ stroke: null,
4962
+ selectable: false,
4963
+ evented: false,
4964
+ objectCaching: false,
4965
+ originX: "left",
4966
+ originY: "top"
4967
+ }
4551
4968
  });
4552
- layer.add(bleedObj);
4553
4969
  }
4554
4970
  }
4555
4971
  const offsetPathData = generateDielinePath({
@@ -4560,23 +4976,30 @@ var DielineTool = class {
4560
4976
  x: cx,
4561
4977
  y: cy,
4562
4978
  features: cutFeatures,
4979
+ shapeStyle,
4563
4980
  pathData: this.state.pathData,
4564
4981
  customSourceWidthPx: this.state.customSourceWidthPx,
4565
4982
  customSourceHeightPx: this.state.customSourceHeightPx,
4566
4983
  canvasWidth: canvasW,
4567
4984
  canvasHeight: canvasH
4568
4985
  });
4569
- const offsetBorderObj = new Path(offsetPathData, {
4570
- fill: null,
4571
- stroke: offsetLine.style === "hidden" ? null : offsetLine.color,
4572
- strokeWidth: offsetLine.width,
4573
- strokeDashArray: offsetLine.style === "dashed" ? [offsetLine.dashLength, offsetLine.dashLength] : void 0,
4574
- selectable: false,
4575
- evented: false,
4576
- originX: "left",
4577
- originY: "top"
4986
+ specs.push({
4987
+ id: "dieline.offset-border",
4988
+ type: "path",
4989
+ space: "screen",
4990
+ data: { id: "dieline.offset-border", type: "dieline" },
4991
+ props: {
4992
+ pathData: offsetPathData,
4993
+ fill: null,
4994
+ stroke: offsetLine.style === "hidden" ? null : offsetLine.color,
4995
+ strokeWidth: offsetLine.width,
4996
+ strokeDashArray: offsetLine.style === "dashed" ? [offsetLine.dashLength, offsetLine.dashLength] : void 0,
4997
+ selectable: false,
4998
+ evented: false,
4999
+ originX: "left",
5000
+ originY: "top"
5001
+ }
4578
5002
  });
4579
- layer.add(offsetBorderObj);
4580
5003
  }
4581
5004
  const borderPathData = generateDielinePath({
4582
5005
  shape,
@@ -4586,39 +5009,59 @@ var DielineTool = class {
4586
5009
  x: cx,
4587
5010
  y: cy,
4588
5011
  features: absoluteFeatures,
5012
+ shapeStyle,
4589
5013
  pathData: this.state.pathData,
4590
5014
  customSourceWidthPx: this.state.customSourceWidthPx,
4591
5015
  customSourceHeightPx: this.state.customSourceHeightPx,
4592
5016
  canvasWidth: canvasW,
4593
5017
  canvasHeight: canvasH
4594
5018
  });
4595
- const borderObj = new Path(borderPathData, {
4596
- fill: "transparent",
4597
- stroke: mainLine.style === "hidden" ? null : mainLine.color,
4598
- strokeWidth: mainLine.width,
4599
- strokeDashArray: mainLine.style === "dashed" ? [mainLine.dashLength, mainLine.dashLength] : void 0,
4600
- selectable: false,
4601
- evented: false,
4602
- originX: "left",
4603
- originY: "top"
4604
- });
4605
- layer.add(borderObj);
4606
- const userLayer = this.canvasService.getLayer("user");
4607
- if (layer && userLayer) {
4608
- const layerIndex = this.canvasService.canvas.getObjects().indexOf(layer);
4609
- const userIndex = this.canvasService.canvas.getObjects().indexOf(userLayer);
4610
- if (layerIndex < userIndex) {
4611
- this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
5019
+ specs.push({
5020
+ id: "dieline.border",
5021
+ type: "path",
5022
+ space: "screen",
5023
+ data: { id: "dieline.border", type: "dieline" },
5024
+ props: {
5025
+ pathData: borderPathData,
5026
+ fill: "transparent",
5027
+ stroke: mainLine.style === "hidden" ? null : mainLine.color,
5028
+ strokeWidth: mainLine.width,
5029
+ strokeDashArray: mainLine.style === "dashed" ? [mainLine.dashLength, mainLine.dashLength] : void 0,
5030
+ selectable: false,
5031
+ evented: false,
5032
+ originX: "left",
5033
+ originY: "top"
4612
5034
  }
4613
- } else {
4614
- this.canvasService.canvas.bringObjectToFront(layer);
5035
+ });
5036
+ return specs;
5037
+ }
5038
+ updateDieline(_emitEvent = true) {
5039
+ void this.updateDielineAsync();
5040
+ }
5041
+ async updateDielineAsync() {
5042
+ if (!this.canvasService) return;
5043
+ const configService = this.getConfigService();
5044
+ if (!configService) return;
5045
+ const seq = ++this.renderSeq;
5046
+ this.syncSizeState(configService);
5047
+ const sceneLayout = computeSceneLayout(
5048
+ this.canvasService,
5049
+ readSizeState(configService)
5050
+ );
5051
+ if (!sceneLayout) {
5052
+ if (seq !== this.renderSeq) return;
5053
+ this.specs = [];
5054
+ await this.canvasService.flushRenderFromProducers();
5055
+ return;
4615
5056
  }
5057
+ const nextSpecs = this.buildDielineSpecs(sceneLayout);
5058
+ if (seq !== this.renderSeq) return;
5059
+ this.specs = nextSpecs;
5060
+ await this.canvasService.flushRenderFromProducers();
5061
+ if (seq !== this.renderSeq) return;
5062
+ this.ensureLayerStacking();
4616
5063
  this.bringFeatureMarkersToFront();
4617
- const rulerLayer = this.canvasService.getLayer("ruler-overlay");
4618
- if (rulerLayer) {
4619
- this.canvasService.canvas.bringObjectToFront(rulerLayer);
4620
- }
4621
- layer.dirty = true;
5064
+ this.canvasService.bringLayerToFront("ruler-overlay");
4622
5065
  this.canvasService.requestRenderAll();
4623
5066
  }
4624
5067
  getGeometry() {
@@ -4666,7 +5109,7 @@ var DielineTool = class {
4666
5109
  );
4667
5110
  return null;
4668
5111
  }
4669
- const { shape, radius, features, pathData } = this.state;
5112
+ const { shape, shapeStyle, radius, features, pathData } = this.state;
4670
5113
  const canvasW = sceneLayout.canvasWidth || this.canvasService.canvas.width || 800;
4671
5114
  const canvasH = sceneLayout.canvasHeight || this.canvasService.canvas.height || 600;
4672
5115
  const scale = sceneLayout.scale;
@@ -4694,6 +5137,7 @@ var DielineTool = class {
4694
5137
  x: cx,
4695
5138
  y: cy,
4696
5139
  features: cutFeatures,
5140
+ shapeStyle,
4697
5141
  pathData,
4698
5142
  customSourceWidthPx: this.state.customSourceWidthPx,
4699
5143
  customSourceHeightPx: this.state.customSourceHeightPx,
@@ -4808,7 +5252,6 @@ var DielineTool = class {
4808
5252
  import {
4809
5253
  ContributionPointIds as ContributionPointIds5
4810
5254
  } from "@pooder/core";
4811
- import { Circle, Group, Point as Point2, Rect as Rect2 } from "fabric";
4812
5255
 
4813
5256
  // src/extensions/constraints.ts
4814
5257
  var ConstraintRegistry = class {
@@ -5008,6 +5451,10 @@ function completeFeaturesStrict(features, context, update) {
5008
5451
  }
5009
5452
 
5010
5453
  // src/extensions/feature.ts
5454
+ var FEATURE_OVERLAY_LAYER_ID = "feature-overlay";
5455
+ var FEATURE_STROKE_WIDTH = 2;
5456
+ var DEFAULT_RECT_SIZE = 10;
5457
+ var DEFAULT_CIRCLE_RADIUS = 5;
5011
5458
  var FeatureTool = class {
5012
5459
  constructor(options) {
5013
5460
  this.id = "pooder.kit.feature";
@@ -5020,6 +5467,8 @@ var FeatureTool = class {
5020
5467
  this.isFeatureSessionActive = false;
5021
5468
  this.sessionOriginalFeatures = null;
5022
5469
  this.hasWorkingChanges = false;
5470
+ this.specs = [];
5471
+ this.renderSeq = 0;
5023
5472
  this.handleMoving = null;
5024
5473
  this.handleModified = null;
5025
5474
  this.handleSceneGeometryChange = null;
@@ -5036,12 +5485,23 @@ var FeatureTool = class {
5036
5485
  }
5037
5486
  }
5038
5487
  activate(context) {
5488
+ var _a;
5039
5489
  this.context = context;
5040
5490
  this.canvasService = context.services.get("CanvasService");
5041
5491
  if (!this.canvasService) {
5042
5492
  console.warn("CanvasService not found for FeatureTool");
5043
5493
  return;
5044
5494
  }
5495
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
5496
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
5497
+ this.id,
5498
+ () => ({
5499
+ rootLayerSpecs: {
5500
+ [FEATURE_OVERLAY_LAYER_ID]: this.specs
5501
+ }
5502
+ }),
5503
+ { priority: 350 }
5504
+ );
5045
5505
  const configService = context.services.get(
5046
5506
  "ConfigurationService"
5047
5507
  );
@@ -5080,21 +5540,7 @@ var FeatureTool = class {
5080
5540
  this.context = void 0;
5081
5541
  }
5082
5542
  updateVisibility() {
5083
- if (!this.canvasService) return;
5084
- const canvas = this.canvasService.canvas;
5085
- const markers = canvas.getObjects().filter((obj) => {
5086
- var _a;
5087
- return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
5088
- });
5089
- markers.forEach((marker) => {
5090
- marker.set({
5091
- visible: this.isToolActive,
5092
- // Or just selectable: false if we want them visible but locked
5093
- selectable: this.isToolActive,
5094
- evented: this.isToolActive
5095
- });
5096
- });
5097
- canvas.requestRenderAll();
5543
+ this.redraw();
5098
5544
  }
5099
5545
  contribute() {
5100
5546
  return {
@@ -5326,8 +5772,7 @@ var FeatureTool = class {
5326
5772
  if (!changed) return { ok: true };
5327
5773
  this.setWorkingFeatures(next);
5328
5774
  this.hasWorkingChanges = true;
5329
- this.redraw();
5330
- this.enforceConstraints();
5775
+ this.redraw({ enforceConstraints: true });
5331
5776
  this.emitWorkingChange();
5332
5777
  return { ok: true };
5333
5778
  }
@@ -5375,12 +5820,10 @@ var FeatureTool = class {
5375
5820
  shape: "rect",
5376
5821
  x: 0.5,
5377
5822
  y: 0,
5378
- // Top edge
5379
5823
  width: 10,
5380
5824
  height: 10,
5381
5825
  rotation: 0,
5382
5826
  renderBehavior: "edge",
5383
- // Default constraint: path (snap to edge)
5384
5827
  constraints: [{ type: "path" }]
5385
5828
  };
5386
5829
  this.setWorkingFeatures([...this.workingFeatures || [], newFeature]);
@@ -5423,7 +5866,7 @@ var FeatureTool = class {
5423
5866
  this.emitWorkingChange();
5424
5867
  return true;
5425
5868
  }
5426
- getGeometryForFeature(geometry, feature) {
5869
+ getGeometryForFeature(geometry, _feature) {
5427
5870
  return geometry;
5428
5871
  }
5429
5872
  setup() {
@@ -5432,8 +5875,7 @@ var FeatureTool = class {
5432
5875
  if (!this.handleSceneGeometryChange) {
5433
5876
  this.handleSceneGeometryChange = (geometry) => {
5434
5877
  this.currentGeometry = geometry;
5435
- this.redraw();
5436
- this.enforceConstraints();
5878
+ this.redraw({ enforceConstraints: true });
5437
5879
  };
5438
5880
  this.context.eventBus.on(
5439
5881
  "scene:geometry:change",
@@ -5456,76 +5898,34 @@ var FeatureTool = class {
5456
5898
  }
5457
5899
  if (!this.handleMoving) {
5458
5900
  this.handleMoving = (e) => {
5459
- var _a, _b, _c, _d;
5460
- const target = e.target;
5461
- if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return;
5462
- if (!this.currentGeometry) return;
5463
- let feature;
5464
- if ((_b = target.data) == null ? void 0 : _b.isGroup) {
5465
- const indices = (_c = target.data) == null ? void 0 : _c.indices;
5466
- if (indices && indices.length > 0) {
5467
- feature = this.workingFeatures[indices[0]];
5468
- }
5469
- } else {
5470
- const index = (_d = target.data) == null ? void 0 : _d.index;
5471
- if (index !== void 0) {
5472
- feature = this.workingFeatures[index];
5473
- }
5474
- }
5475
- const geometry = this.getGeometryForFeature(
5476
- this.currentGeometry,
5901
+ const target = this.getDraggableMarkerTarget(e == null ? void 0 : e.target);
5902
+ if (!target || !this.currentGeometry) return;
5903
+ const feature = this.getFeatureForMarker(target);
5904
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
5905
+ const snapped = this.constrainPosition(
5906
+ {
5907
+ x: Number(target.left || 0),
5908
+ y: Number(target.top || 0)
5909
+ },
5910
+ geometry,
5477
5911
  feature
5478
5912
  );
5479
- const p = new Point2(target.left, target.top);
5480
- const markerStrokeWidth = (target.strokeWidth || 2) * (target.scaleX || 1);
5481
- const minDim = Math.min(
5482
- target.getScaledWidth(),
5483
- target.getScaledHeight()
5484
- );
5485
- const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
5486
- const snapped = this.constrainPosition(p, geometry, limit, feature);
5487
5913
  target.set({
5488
5914
  left: snapped.x,
5489
5915
  top: snapped.y
5490
5916
  });
5917
+ target.setCoords();
5918
+ this.syncMarkerVisualsByTarget(target, snapped);
5491
5919
  };
5492
5920
  canvas.on("object:moving", this.handleMoving);
5493
5921
  }
5494
5922
  if (!this.handleModified) {
5495
5923
  this.handleModified = (e) => {
5496
- var _a, _b, _c;
5497
- const target = e.target;
5498
- if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return;
5499
- if ((_b = target.data) == null ? void 0 : _b.isGroup) {
5500
- const groupObj = target;
5501
- const indices = (_c = groupObj.data) == null ? void 0 : _c.indices;
5502
- if (!indices) return;
5503
- const groupCenter = new Point2(groupObj.left, groupObj.top);
5504
- const newFeatures = [...this.workingFeatures];
5505
- const { x, y } = this.currentGeometry;
5506
- groupObj.getObjects().forEach((child, i) => {
5507
- const originalIndex = indices[i];
5508
- const feature = this.workingFeatures[originalIndex];
5509
- const geometry = this.getGeometryForFeature(
5510
- this.currentGeometry,
5511
- feature
5512
- );
5513
- const { width, height } = geometry;
5514
- const layoutLeft = x - width / 2;
5515
- const layoutTop = y - height / 2;
5516
- const absX = groupCenter.x + (child.left || 0);
5517
- const absY = groupCenter.y + (child.top || 0);
5518
- const normalizedX = width > 0 ? (absX - layoutLeft) / width : 0.5;
5519
- const normalizedY = height > 0 ? (absY - layoutTop) / height : 0.5;
5520
- newFeatures[originalIndex] = {
5521
- ...newFeatures[originalIndex],
5522
- x: normalizedX,
5523
- y: normalizedY
5524
- };
5525
- });
5526
- this.setWorkingFeatures(newFeatures);
5527
- this.hasWorkingChanges = true;
5528
- this.emitWorkingChange();
5924
+ var _a;
5925
+ const target = this.getDraggableMarkerTarget(e == null ? void 0 : e.target);
5926
+ if (!target) return;
5927
+ if ((_a = target.data) == null ? void 0 : _a.isGroup) {
5928
+ this.syncGroupFromCanvas(target);
5529
5929
  } else {
5530
5930
  this.syncFeatureFromCanvas(target);
5531
5931
  }
@@ -5534,6 +5934,7 @@ var FeatureTool = class {
5534
5934
  }
5535
5935
  }
5536
5936
  teardown() {
5937
+ var _a;
5537
5938
  if (!this.canvasService) return;
5538
5939
  const canvas = this.canvasService.canvas;
5539
5940
  if (this.handleMoving) {
@@ -5551,14 +5952,25 @@ var FeatureTool = class {
5551
5952
  );
5552
5953
  this.handleSceneGeometryChange = null;
5553
5954
  }
5554
- const objects = canvas.getObjects().filter((obj) => {
5555
- var _a;
5556
- return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
5557
- });
5558
- objects.forEach((obj) => canvas.remove(obj));
5559
- this.canvasService.requestRenderAll();
5955
+ this.renderSeq += 1;
5956
+ this.specs = [];
5957
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
5958
+ this.renderProducerDisposable = void 0;
5959
+ void this.canvasService.flushRenderFromProducers();
5560
5960
  }
5561
- constrainPosition(p, geometry, limit, feature) {
5961
+ getDraggableMarkerTarget(target) {
5962
+ var _a, _b;
5963
+ if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return null;
5964
+ if (((_b = target.data) == null ? void 0 : _b.markerRole) !== "handle") return null;
5965
+ return target;
5966
+ }
5967
+ getFeatureForMarker(target) {
5968
+ const data = (target == null ? void 0 : target.data) || {};
5969
+ const index = data.isGroup ? this.toFeatureIndex(data.anchorIndex) : this.toFeatureIndex(data.index);
5970
+ if (index === null) return void 0;
5971
+ return this.workingFeatures[index];
5972
+ }
5973
+ constrainPosition(p, geometry, feature) {
5562
5974
  var _a;
5563
5975
  if (!feature) {
5564
5976
  return { x: p.x, y: p.y };
@@ -5589,225 +6001,364 @@ var FeatureTool = class {
5589
6001
  y: minY + constrained.y * geometry.height
5590
6002
  };
5591
6003
  }
6004
+ toNormalizedPoint(point, geometry) {
6005
+ const left = geometry.x - geometry.width / 2;
6006
+ const top = geometry.y - geometry.height / 2;
6007
+ return {
6008
+ x: geometry.width > 0 ? (point.x - left) / geometry.width : 0.5,
6009
+ y: geometry.height > 0 ? (point.y - top) / geometry.height : 0.5
6010
+ };
6011
+ }
5592
6012
  syncFeatureFromCanvas(target) {
5593
6013
  var _a;
5594
- if (!this.currentGeometry || !this.context) return;
5595
- const index = (_a = target.data) == null ? void 0 : _a.index;
5596
- if (index === void 0 || index < 0 || index >= this.workingFeatures.length)
5597
- return;
6014
+ if (!this.currentGeometry) return;
6015
+ const index = this.toFeatureIndex((_a = target.data) == null ? void 0 : _a.index);
6016
+ if (index === null || index >= this.workingFeatures.length) return;
5598
6017
  const feature = this.workingFeatures[index];
5599
6018
  const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
5600
- const { width, height, x, y } = geometry;
5601
- const left = x - width / 2;
5602
- const top = y - height / 2;
5603
- const normalizedX = width > 0 ? (target.left - left) / width : 0.5;
5604
- const normalizedY = height > 0 ? (target.top - top) / height : 0.5;
6019
+ const normalized = this.toNormalizedPoint(
6020
+ {
6021
+ x: Number(target.left || 0),
6022
+ y: Number(target.top || 0)
6023
+ },
6024
+ geometry
6025
+ );
5605
6026
  const updatedFeature = {
5606
6027
  ...feature,
5607
- x: normalizedX,
5608
- y: normalizedY
5609
- // Could also update rotation if we allowed rotating markers
6028
+ x: normalized.x,
6029
+ y: normalized.y
5610
6030
  };
5611
- const newFeatures = [...this.workingFeatures];
5612
- newFeatures[index] = updatedFeature;
5613
- this.setWorkingFeatures(newFeatures);
6031
+ const next = [...this.workingFeatures];
6032
+ next[index] = updatedFeature;
6033
+ this.setWorkingFeatures(next);
5614
6034
  this.hasWorkingChanges = true;
5615
6035
  this.emitWorkingChange();
5616
6036
  }
5617
- redraw() {
5618
- if (!this.canvasService || !this.currentGeometry) return;
5619
- const canvas = this.canvasService.canvas;
5620
- const geometry = this.currentGeometry;
5621
- const existing = canvas.getObjects().filter((obj) => {
5622
- var _a;
5623
- return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
6037
+ syncGroupFromCanvas(target) {
6038
+ var _a, _b;
6039
+ if (!this.currentGeometry) return;
6040
+ const indices = this.readGroupIndices((_a = target.data) == null ? void 0 : _a.indices);
6041
+ if (indices.length === 0) return;
6042
+ const offsets = this.readGroupMemberOffsets((_b = target.data) == null ? void 0 : _b.memberOffsets, indices);
6043
+ const anchorCenter = {
6044
+ x: Number(target.left || 0),
6045
+ y: Number(target.top || 0)
6046
+ };
6047
+ const next = [...this.workingFeatures];
6048
+ let changed = false;
6049
+ offsets.forEach((entry) => {
6050
+ const index = entry.index;
6051
+ if (index < 0 || index >= next.length) return;
6052
+ const feature = next[index];
6053
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
6054
+ const normalized = this.toNormalizedPoint(
6055
+ {
6056
+ x: anchorCenter.x + entry.dx,
6057
+ y: anchorCenter.y + entry.dy
6058
+ },
6059
+ geometry
6060
+ );
6061
+ if (feature.x !== normalized.x || feature.y !== normalized.y) {
6062
+ next[index] = {
6063
+ ...feature,
6064
+ x: normalized.x,
6065
+ y: normalized.y
6066
+ };
6067
+ changed = true;
6068
+ }
5624
6069
  });
5625
- existing.forEach((obj) => canvas.remove(obj));
5626
- if (!this.workingFeatures || this.workingFeatures.length === 0) {
5627
- this.canvasService.requestRenderAll();
5628
- return;
6070
+ if (!changed) return;
6071
+ this.setWorkingFeatures(next);
6072
+ this.hasWorkingChanges = true;
6073
+ this.emitWorkingChange();
6074
+ }
6075
+ redraw(options = {}) {
6076
+ void this.redrawAsync(options);
6077
+ }
6078
+ async redrawAsync(options = {}) {
6079
+ if (!this.canvasService) return;
6080
+ const seq = ++this.renderSeq;
6081
+ this.specs = this.buildFeatureSpecs();
6082
+ if (seq !== this.renderSeq) return;
6083
+ await this.canvasService.flushRenderFromProducers();
6084
+ if (seq !== this.renderSeq) return;
6085
+ this.syncOverlayOrder();
6086
+ if (options.enforceConstraints) {
6087
+ this.enforceConstraints();
5629
6088
  }
5630
- const scale = geometry.scale || 1;
5631
- const finalScale = scale;
5632
- const groups = {};
6089
+ }
6090
+ syncOverlayOrder() {
6091
+ if (!this.canvasService) return;
6092
+ this.canvasService.bringLayerToFront(FEATURE_OVERLAY_LAYER_ID);
6093
+ this.canvasService.bringLayerToFront("ruler-overlay");
6094
+ }
6095
+ buildFeatureSpecs() {
6096
+ if (!this.currentGeometry || this.workingFeatures.length === 0) {
6097
+ return [];
6098
+ }
6099
+ const groups = /* @__PURE__ */ new Map();
5633
6100
  const singles = [];
5634
- this.workingFeatures.forEach((f, i) => {
5635
- if (f.groupId) {
5636
- if (!groups[f.groupId]) groups[f.groupId] = [];
5637
- groups[f.groupId].push({ feature: f, index: i });
5638
- } else {
5639
- singles.push({ feature: f, index: i });
6101
+ this.workingFeatures.forEach((feature, index) => {
6102
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
6103
+ const position = resolveFeaturePosition(feature, geometry);
6104
+ const scale = geometry.scale || 1;
6105
+ const marker = {
6106
+ feature,
6107
+ index,
6108
+ position,
6109
+ geometry,
6110
+ scale
6111
+ };
6112
+ if (feature.groupId) {
6113
+ const list = groups.get(feature.groupId) || [];
6114
+ list.push(marker);
6115
+ groups.set(feature.groupId, list);
6116
+ return;
5640
6117
  }
6118
+ singles.push(marker);
6119
+ });
6120
+ const specs = [];
6121
+ singles.forEach((marker) => {
6122
+ this.appendMarkerSpecs(specs, marker, {
6123
+ markerRole: "handle",
6124
+ isGroup: false
6125
+ });
6126
+ });
6127
+ groups.forEach((members, groupId) => {
6128
+ if (!members.length) return;
6129
+ const anchor = members[0];
6130
+ const memberOffsets = members.map((member) => ({
6131
+ index: member.index,
6132
+ dx: member.position.x - anchor.position.x,
6133
+ dy: member.position.y - anchor.position.y
6134
+ }));
6135
+ const indices = members.map((member) => member.index);
6136
+ members.filter((member) => member.index !== anchor.index).forEach((member) => {
6137
+ this.appendMarkerSpecs(specs, member, {
6138
+ markerRole: "member",
6139
+ isGroup: false,
6140
+ groupId
6141
+ });
6142
+ });
6143
+ this.appendMarkerSpecs(specs, anchor, {
6144
+ markerRole: "handle",
6145
+ isGroup: true,
6146
+ groupId,
6147
+ indices,
6148
+ anchorIndex: anchor.index,
6149
+ memberOffsets
6150
+ });
5641
6151
  });
5642
- const createMarkerShape = (feature, pos) => {
5643
- const featureScale = scale;
5644
- const visualWidth = (feature.width || 10) * featureScale;
5645
- const visualHeight = (feature.height || 10) * featureScale;
5646
- const visualRadius = (feature.radius || 0) * featureScale;
5647
- const color = feature.color || (feature.operation === "add" ? "#00FF00" : "#FF0000");
5648
- const strokeDash = feature.strokeDash || (feature.operation === "subtract" ? [4, 4] : void 0);
5649
- let shape;
5650
- if (feature.shape === "rect") {
5651
- shape = new Rect2({
6152
+ return specs;
6153
+ }
6154
+ appendMarkerSpecs(specs, marker, options) {
6155
+ var _a, _b, _c, _d, _e;
6156
+ const { feature, index, position, scale, geometry } = marker;
6157
+ const baseRadius = feature.shape === "circle" ? (_a = feature.radius) != null ? _a : DEFAULT_CIRCLE_RADIUS : (_b = feature.radius) != null ? _b : 0;
6158
+ const baseWidth = feature.shape === "circle" ? baseRadius * 2 : (_c = feature.width) != null ? _c : DEFAULT_RECT_SIZE;
6159
+ const baseHeight = feature.shape === "circle" ? baseRadius * 2 : (_d = feature.height) != null ? _d : DEFAULT_RECT_SIZE;
6160
+ const visualWidth = baseWidth * scale;
6161
+ const visualHeight = baseHeight * scale;
6162
+ const visualRadius = baseRadius * scale;
6163
+ const color = feature.color || (feature.operation === "add" ? "#00FF00" : "#FF0000");
6164
+ const strokeDash = feature.strokeDash || (feature.operation === "subtract" ? [4, 4] : void 0);
6165
+ const interactive = options.markerRole === "handle";
6166
+ const baseData = this.buildMarkerData(marker, options);
6167
+ const commonProps = {
6168
+ visible: this.isToolActive,
6169
+ selectable: interactive && this.isToolActive,
6170
+ evented: interactive && this.isToolActive,
6171
+ hasControls: false,
6172
+ hasBorders: false,
6173
+ hoverCursor: interactive ? "move" : "default",
6174
+ lockRotation: true,
6175
+ lockScalingX: true,
6176
+ lockScalingY: true,
6177
+ fill: "transparent",
6178
+ stroke: color,
6179
+ strokeWidth: FEATURE_STROKE_WIDTH,
6180
+ strokeDashArray: strokeDash,
6181
+ originX: "center",
6182
+ originY: "center",
6183
+ left: position.x,
6184
+ top: position.y,
6185
+ angle: feature.rotation || 0
6186
+ };
6187
+ const markerId = this.markerId(index);
6188
+ if (feature.shape === "rect") {
6189
+ specs.push({
6190
+ id: markerId,
6191
+ type: "rect",
6192
+ space: "screen",
6193
+ data: baseData,
6194
+ props: {
6195
+ ...commonProps,
5652
6196
  width: visualWidth,
5653
6197
  height: visualHeight,
5654
6198
  rx: visualRadius,
5655
- ry: visualRadius,
5656
- fill: "transparent",
5657
- stroke: color,
5658
- strokeWidth: 2,
5659
- strokeDashArray: strokeDash,
5660
- originX: "center",
5661
- originY: "center",
5662
- left: pos.x,
5663
- top: pos.y
5664
- });
5665
- } else {
5666
- shape = new Circle({
5667
- radius: visualRadius || 5 * finalScale,
5668
- fill: "transparent",
5669
- stroke: color,
5670
- strokeWidth: 2,
5671
- strokeDashArray: strokeDash,
5672
- originX: "center",
5673
- originY: "center",
5674
- left: pos.x,
5675
- top: pos.y
5676
- });
5677
- }
5678
- if (feature.rotation) {
5679
- shape.rotate(feature.rotation);
6199
+ ry: visualRadius
6200
+ }
6201
+ });
6202
+ } else {
6203
+ specs.push({
6204
+ id: markerId,
6205
+ type: "rect",
6206
+ space: "screen",
6207
+ data: baseData,
6208
+ props: {
6209
+ ...commonProps,
6210
+ width: visualWidth,
6211
+ height: visualHeight,
6212
+ rx: visualRadius,
6213
+ ry: visualRadius
6214
+ }
6215
+ });
6216
+ }
6217
+ if (((_e = feature.bridge) == null ? void 0 : _e.type) === "vertical") {
6218
+ const featureTopY = position.y - visualHeight / 2;
6219
+ const dielineTopY = geometry.y - geometry.height / 2;
6220
+ const bridgeHeight = Math.max(0, featureTopY - dielineTopY);
6221
+ if (bridgeHeight <= 1e-3) {
6222
+ return;
5680
6223
  }
5681
- if (feature.bridge && feature.bridge.type === "vertical") {
5682
- const bridgeIndicator = new Rect2({
6224
+ specs.push({
6225
+ id: this.bridgeIndicatorId(index),
6226
+ type: "rect",
6227
+ space: "screen",
6228
+ data: {
6229
+ ...baseData,
6230
+ markerRole: "indicator",
6231
+ markerOffsetX: 0,
6232
+ markerOffsetY: -visualHeight / 2
6233
+ },
6234
+ props: {
6235
+ visible: this.isToolActive,
6236
+ selectable: false,
6237
+ evented: false,
5683
6238
  width: visualWidth,
5684
- height: 100 * featureScale,
5685
- // Arbitrary long length to show direction
6239
+ height: bridgeHeight,
5686
6240
  fill: "transparent",
5687
6241
  stroke: "#888",
5688
6242
  strokeWidth: 1,
5689
6243
  strokeDashArray: [2, 2],
5690
- originX: "center",
5691
- originY: "bottom",
5692
- // Anchor at bottom so it extends up
5693
- left: pos.x,
5694
- top: pos.y - visualHeight / 2,
5695
- // Start from top of feature
5696
6244
  opacity: 0.5,
5697
- selectable: false,
5698
- evented: false
5699
- });
5700
- const group = new Group([bridgeIndicator, shape], {
5701
6245
  originX: "center",
5702
- originY: "center",
5703
- left: pos.x,
5704
- top: pos.y
5705
- });
5706
- return group;
5707
- }
5708
- return shape;
5709
- };
5710
- singles.forEach(({ feature, index }) => {
5711
- const geometry2 = this.getGeometryForFeature(
5712
- this.currentGeometry,
5713
- feature
5714
- );
5715
- const pos = resolveFeaturePosition(feature, geometry2);
5716
- const marker = createMarkerShape(feature, pos);
5717
- marker.set({
5718
- visible: this.isToolActive,
5719
- selectable: this.isToolActive,
5720
- evented: this.isToolActive,
5721
- hasControls: false,
5722
- hasBorders: false,
5723
- hoverCursor: "move",
5724
- lockRotation: true,
5725
- lockScalingX: true,
5726
- lockScalingY: true,
5727
- data: { type: "feature-marker", index, isGroup: false }
5728
- });
5729
- canvas.add(marker);
5730
- canvas.bringObjectToFront(marker);
5731
- });
5732
- Object.keys(groups).forEach((groupId) => {
5733
- const members = groups[groupId];
5734
- if (members.length === 0) return;
5735
- const shapes = members.map(({ feature }) => {
5736
- const geometry2 = this.getGeometryForFeature(
5737
- this.currentGeometry,
5738
- feature
5739
- );
5740
- const pos = resolveFeaturePosition(feature, geometry2);
5741
- return createMarkerShape(feature, pos);
6246
+ originY: "bottom",
6247
+ left: position.x,
6248
+ top: position.y - visualHeight / 2
6249
+ }
5742
6250
  });
5743
- const groupObj = new Group(shapes, {
5744
- visible: this.isToolActive,
5745
- selectable: this.isToolActive,
5746
- evented: this.isToolActive,
5747
- hasControls: false,
5748
- hasBorders: false,
5749
- hoverCursor: "move",
5750
- lockRotation: true,
5751
- lockScalingX: true,
5752
- lockScalingY: true,
5753
- subTargetCheck: true,
5754
- // Allow events to pass through if needed, but we treat as one
5755
- interactive: false,
5756
- // Children not interactive
5757
- // @ts-ignore
5758
- data: {
5759
- type: "feature-marker",
5760
- isGroup: true,
5761
- groupId,
5762
- indices: members.map((m) => m.index)
6251
+ }
6252
+ }
6253
+ buildMarkerData(marker, options) {
6254
+ const data = {
6255
+ type: "feature-marker",
6256
+ index: marker.index,
6257
+ featureId: marker.feature.id,
6258
+ markerRole: options.markerRole,
6259
+ markerOffsetX: 0,
6260
+ markerOffsetY: 0,
6261
+ isGroup: options.isGroup
6262
+ };
6263
+ if (options.groupId) data.groupId = options.groupId;
6264
+ if (options.indices) data.indices = options.indices;
6265
+ if (options.anchorIndex !== void 0) data.anchorIndex = options.anchorIndex;
6266
+ if (options.memberOffsets) data.memberOffsets = options.memberOffsets;
6267
+ return data;
6268
+ }
6269
+ markerId(index) {
6270
+ return `feature.marker.${index}`;
6271
+ }
6272
+ bridgeIndicatorId(index) {
6273
+ return `feature.marker.${index}.bridge`;
6274
+ }
6275
+ toFeatureIndex(value) {
6276
+ const numeric = Number(value);
6277
+ if (!Number.isInteger(numeric) || numeric < 0) return null;
6278
+ return numeric;
6279
+ }
6280
+ readGroupIndices(raw) {
6281
+ if (!Array.isArray(raw)) return [];
6282
+ return raw.map((value) => this.toFeatureIndex(value)).filter((value) => value !== null);
6283
+ }
6284
+ readGroupMemberOffsets(raw, fallbackIndices = []) {
6285
+ if (Array.isArray(raw)) {
6286
+ const parsed = raw.map((entry) => {
6287
+ const index = this.toFeatureIndex(entry == null ? void 0 : entry.index);
6288
+ const dx = Number(entry == null ? void 0 : entry.dx);
6289
+ const dy = Number(entry == null ? void 0 : entry.dy);
6290
+ if (index === null || !Number.isFinite(dx) || !Number.isFinite(dy)) {
6291
+ return null;
5763
6292
  }
6293
+ return { index, dx, dy };
6294
+ }).filter((value) => !!value);
6295
+ if (parsed.length > 0) return parsed;
6296
+ }
6297
+ return fallbackIndices.map((index) => ({ index, dx: 0, dy: 0 }));
6298
+ }
6299
+ syncMarkerVisualsByTarget(target, center) {
6300
+ var _a, _b, _c, _d, _e, _f;
6301
+ if ((_a = target.data) == null ? void 0 : _a.isGroup) {
6302
+ const indices = this.readGroupIndices((_b = target.data) == null ? void 0 : _b.indices);
6303
+ const offsets = this.readGroupMemberOffsets((_c = target.data) == null ? void 0 : _c.memberOffsets, indices);
6304
+ offsets.forEach((entry) => {
6305
+ this.syncMarkerVisualObjectsToCenter(entry.index, {
6306
+ x: center.x + entry.dx,
6307
+ y: center.y + entry.dy
6308
+ });
5764
6309
  });
5765
- canvas.add(groupObj);
5766
- canvas.bringObjectToFront(groupObj);
6310
+ (_d = this.canvasService) == null ? void 0 : _d.requestRenderAll();
6311
+ return;
6312
+ }
6313
+ const index = this.toFeatureIndex((_e = target.data) == null ? void 0 : _e.index);
6314
+ if (index === null) return;
6315
+ this.syncMarkerVisualObjectsToCenter(index, center);
6316
+ (_f = this.canvasService) == null ? void 0 : _f.requestRenderAll();
6317
+ }
6318
+ syncMarkerVisualObjectsToCenter(index, center) {
6319
+ if (!this.canvasService) return;
6320
+ const markers = this.canvasService.canvas.getObjects().filter(
6321
+ (obj) => {
6322
+ var _a, _b;
6323
+ return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.type) === "feature-marker" && this.toFeatureIndex((_b = obj == null ? void 0 : obj.data) == null ? void 0 : _b.index) === index;
6324
+ }
6325
+ );
6326
+ markers.forEach((marker) => {
6327
+ var _a, _b;
6328
+ const offsetX = Number(((_a = marker == null ? void 0 : marker.data) == null ? void 0 : _a.markerOffsetX) || 0);
6329
+ const offsetY = Number(((_b = marker == null ? void 0 : marker.data) == null ? void 0 : _b.markerOffsetY) || 0);
6330
+ marker.set({
6331
+ left: center.x + offsetX,
6332
+ top: center.y + offsetY
6333
+ });
6334
+ marker.setCoords();
5767
6335
  });
5768
- this.canvasService.requestRenderAll();
5769
6336
  }
5770
6337
  enforceConstraints() {
5771
6338
  if (!this.canvasService || !this.currentGeometry) return;
5772
- const canvas = this.canvasService.canvas;
5773
- const markers = canvas.getObjects().filter((obj) => {
5774
- var _a;
5775
- return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
5776
- });
5777
- markers.forEach((marker) => {
5778
- var _a, _b, _c;
5779
- let feature;
5780
- if ((_a = marker.data) == null ? void 0 : _a.isGroup) {
5781
- const indices = (_b = marker.data) == null ? void 0 : _b.indices;
5782
- if (indices && indices.length > 0) {
5783
- feature = this.workingFeatures[indices[0]];
5784
- }
5785
- } else {
5786
- const index = (_c = marker.data) == null ? void 0 : _c.index;
5787
- if (index !== void 0) {
5788
- feature = this.workingFeatures[index];
5789
- }
6339
+ const handles = this.canvasService.canvas.getObjects().filter(
6340
+ (obj) => {
6341
+ var _a, _b;
6342
+ return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.type) === "feature-marker" && ((_b = obj == null ? void 0 : obj.data) == null ? void 0 : _b.markerRole) === "handle";
5790
6343
  }
5791
- const geometry = this.getGeometryForFeature(
5792
- this.currentGeometry,
5793
- feature
5794
- );
5795
- const markerStrokeWidth = (marker.strokeWidth || 2) * (marker.scaleX || 1);
5796
- const minDim = Math.min(
5797
- marker.getScaledWidth(),
5798
- marker.getScaledHeight()
5799
- );
5800
- const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
6344
+ );
6345
+ handles.forEach((marker) => {
6346
+ const feature = this.getFeatureForMarker(marker);
6347
+ if (!feature) return;
6348
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
5801
6349
  const snapped = this.constrainPosition(
5802
- new Point2(marker.left, marker.top),
6350
+ {
6351
+ x: Number(marker.left || 0),
6352
+ y: Number(marker.top || 0)
6353
+ },
5803
6354
  geometry,
5804
- limit,
5805
6355
  feature
5806
6356
  );
5807
6357
  marker.set({ left: snapped.x, top: snapped.y });
5808
6358
  marker.setCoords();
6359
+ this.syncMarkerVisualsByTarget(marker, snapped);
5809
6360
  });
5810
- canvas.requestRenderAll();
6361
+ this.canvasService.canvas.requestRenderAll();
5811
6362
  }
5812
6363
  };
5813
6364
 
@@ -5815,7 +6366,11 @@ var FeatureTool = class {
5815
6366
  import {
5816
6367
  ContributionPointIds as ContributionPointIds6
5817
6368
  } from "@pooder/core";
5818
- import { FabricImage as Image3 } from "fabric";
6369
+ import { FabricImage as FabricImage3 } from "fabric";
6370
+ var FILM_LAYER_ID = "overlay";
6371
+ var FILM_IMAGE_ID = "film-image";
6372
+ var DEFAULT_WIDTH2 = 800;
6373
+ var DEFAULT_HEIGHT2 = 600;
5819
6374
  var FilmTool = class {
5820
6375
  constructor(options) {
5821
6376
  this.id = "pooder.kit.film";
@@ -5824,17 +6379,38 @@ var FilmTool = class {
5824
6379
  };
5825
6380
  this.url = "";
5826
6381
  this.opacity = 0.5;
6382
+ this.specs = [];
6383
+ this.renderSeq = 0;
6384
+ this.renderImageUrl = "";
6385
+ this.sourceSizeBySrc = /* @__PURE__ */ new Map();
6386
+ this.pendingSizeBySrc = /* @__PURE__ */ new Map();
6387
+ this.onCanvasResized = () => {
6388
+ this.updateFilm();
6389
+ };
5827
6390
  if (options) {
5828
6391
  Object.assign(this, options);
5829
6392
  }
5830
6393
  }
5831
6394
  activate(context) {
6395
+ var _a;
5832
6396
  this.canvasService = context.services.get("CanvasService");
5833
6397
  if (!this.canvasService) {
5834
6398
  console.warn("CanvasService not found for FilmTool");
5835
6399
  return;
5836
6400
  }
5837
- const configService = context.services.get("ConfigurationService");
6401
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
6402
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
6403
+ this.id,
6404
+ () => ({
6405
+ layerSpecs: {
6406
+ [FILM_LAYER_ID]: this.specs
6407
+ }
6408
+ }),
6409
+ { priority: 500 }
6410
+ );
6411
+ const configService = context.services.get(
6412
+ "ConfigurationService"
6413
+ );
5838
6414
  if (configService) {
5839
6415
  this.url = configService.get("film.url", this.url);
5840
6416
  this.opacity = configService.get("film.opacity", this.opacity);
@@ -5851,21 +6427,21 @@ var FilmTool = class {
5851
6427
  }
5852
6428
  });
5853
6429
  }
5854
- this.initLayer();
6430
+ context.eventBus.on("canvas:resized", this.onCanvasResized);
5855
6431
  this.updateFilm();
5856
6432
  }
5857
6433
  deactivate(context) {
5858
- if (this.canvasService) {
5859
- const layer = this.canvasService.getLayer("overlay");
5860
- if (layer) {
5861
- const img = this.canvasService.getObject("film-image", "overlay");
5862
- if (img) {
5863
- layer.remove(img);
5864
- this.canvasService.requestRenderAll();
5865
- }
5866
- }
5867
- this.canvasService = void 0;
5868
- }
6434
+ var _a;
6435
+ context.eventBus.off("canvas:resized", this.onCanvasResized);
6436
+ this.renderSeq += 1;
6437
+ this.specs = [];
6438
+ this.renderImageUrl = "";
6439
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
6440
+ this.renderProducerDisposable = void 0;
6441
+ if (!this.canvasService) return;
6442
+ void this.canvasService.flushRenderFromProducers();
6443
+ this.canvasService.requestRenderAll();
6444
+ this.canvasService = void 0;
5869
6445
  }
5870
6446
  contribute() {
5871
6447
  return {
@@ -5901,73 +6477,108 @@ var FilmTool = class {
5901
6477
  ]
5902
6478
  };
5903
6479
  }
5904
- initLayer() {
5905
- if (!this.canvasService) return;
5906
- let overlayLayer = this.canvasService.getLayer("overlay");
5907
- if (!overlayLayer) {
5908
- const width = this.canvasService.canvas.width || 800;
5909
- const height = this.canvasService.canvas.height || 600;
5910
- const layer = this.canvasService.createLayer("overlay", {
5911
- width,
5912
- height,
5913
- left: 0,
5914
- top: 0,
5915
- originX: "left",
5916
- originY: "top",
5917
- selectable: false,
5918
- evented: false,
5919
- subTargetCheck: false,
5920
- interactive: false
5921
- });
5922
- this.canvasService.canvas.bringObjectToFront(layer);
5923
- }
6480
+ getViewportSize() {
6481
+ var _a, _b;
6482
+ const width = Number(((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 0);
6483
+ const height = Number(((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 0);
6484
+ return {
6485
+ width: width > 0 ? width : DEFAULT_WIDTH2,
6486
+ height: height > 0 ? height : DEFAULT_HEIGHT2
6487
+ };
5924
6488
  }
5925
- async updateFilm() {
5926
- if (!this.canvasService) return;
5927
- const layer = this.canvasService.getLayer("overlay");
5928
- if (!layer) {
5929
- console.warn("[FilmTool] Overlay layer not found");
5930
- return;
5931
- }
5932
- const { url, opacity } = this;
5933
- if (!url) {
5934
- const img2 = this.canvasService.getObject("film-image", "overlay");
5935
- if (img2) {
5936
- layer.remove(img2);
5937
- this.canvasService.requestRenderAll();
5938
- }
5939
- return;
6489
+ clampOpacity(value) {
6490
+ return Math.max(0, Math.min(1, Number(value)));
6491
+ }
6492
+ buildFilmSpecs(imageUrl, opacity) {
6493
+ if (!imageUrl) {
6494
+ return [];
5940
6495
  }
5941
- const width = this.canvasService.canvas.width || 800;
5942
- const height = this.canvasService.canvas.height || 600;
5943
- let img = this.canvasService.getObject("film-image", "overlay");
5944
- try {
5945
- if (img) {
5946
- if (img.getSrc() !== url) {
5947
- await img.setSrc(url);
5948
- }
5949
- img.set({ opacity });
5950
- } else {
5951
- img = await Image3.fromURL(url, { crossOrigin: "anonymous" });
5952
- img.scaleToWidth(width);
5953
- if (img.getScaledHeight() < height) img.scaleToHeight(height);
5954
- img.set({
5955
- originX: "left",
5956
- originY: "top",
6496
+ const { width, height } = this.getViewportSize();
6497
+ const sourceSize = this.sourceSizeBySrc.get(imageUrl);
6498
+ const sourceWidth = Math.max(1, Number((sourceSize == null ? void 0 : sourceSize.width) || width));
6499
+ const sourceHeight = Math.max(1, Number((sourceSize == null ? void 0 : sourceSize.height) || height));
6500
+ const coverScale = Math.max(width / sourceWidth, height / sourceHeight);
6501
+ return [
6502
+ {
6503
+ id: FILM_IMAGE_ID,
6504
+ type: "image",
6505
+ src: imageUrl,
6506
+ space: "screen",
6507
+ data: {
6508
+ id: FILM_IMAGE_ID,
6509
+ layerId: FILM_LAYER_ID,
6510
+ type: "film-image"
6511
+ },
6512
+ props: {
5957
6513
  left: 0,
5958
6514
  top: 0,
5959
- opacity,
6515
+ originX: "left",
6516
+ originY: "top",
6517
+ opacity: this.clampOpacity(opacity),
6518
+ scaleX: coverScale,
6519
+ scaleY: coverScale,
5960
6520
  selectable: false,
5961
6521
  evented: false,
5962
- data: { id: "film-image" }
5963
- });
5964
- layer.add(img);
6522
+ excludeFromExport: true
6523
+ }
6524
+ }
6525
+ ];
6526
+ }
6527
+ async ensureImageSize(src) {
6528
+ if (!src) return null;
6529
+ const cached = this.sourceSizeBySrc.get(src);
6530
+ if (cached) return cached;
6531
+ const pending = this.pendingSizeBySrc.get(src);
6532
+ if (pending) {
6533
+ return pending;
6534
+ }
6535
+ const task = this.loadImageSize(src);
6536
+ this.pendingSizeBySrc.set(src, task);
6537
+ try {
6538
+ return await task;
6539
+ } finally {
6540
+ if (this.pendingSizeBySrc.get(src) === task) {
6541
+ this.pendingSizeBySrc.delete(src);
6542
+ }
6543
+ }
6544
+ }
6545
+ async loadImageSize(src) {
6546
+ try {
6547
+ const image = await FabricImage3.fromURL(src, {
6548
+ crossOrigin: "anonymous"
6549
+ });
6550
+ const width = Number((image == null ? void 0 : image.width) || 0);
6551
+ const height = Number((image == null ? void 0 : image.height) || 0);
6552
+ if (width > 0 && height > 0) {
6553
+ const size = { width, height };
6554
+ this.sourceSizeBySrc.set(src, size);
6555
+ return size;
5965
6556
  }
5966
- this.canvasService.requestRenderAll();
5967
6557
  } catch (error) {
5968
- console.error("[FilmTool] Failed to load film image", url, error);
6558
+ console.error("[FilmTool] Failed to load film image", src, error);
6559
+ }
6560
+ return null;
6561
+ }
6562
+ updateFilm() {
6563
+ void this.updateFilmAsync();
6564
+ }
6565
+ async updateFilmAsync() {
6566
+ if (!this.canvasService) return;
6567
+ const seq = ++this.renderSeq;
6568
+ const nextUrl = String(this.url || "").trim();
6569
+ if (!nextUrl) {
6570
+ this.renderImageUrl = "";
6571
+ } else if (nextUrl !== this.renderImageUrl) {
6572
+ const loaded = await this.ensureImageSize(nextUrl);
6573
+ if (seq !== this.renderSeq) return;
6574
+ if (loaded) {
6575
+ this.renderImageUrl = nextUrl;
6576
+ }
5969
6577
  }
5970
- layer.dirty = true;
6578
+ this.specs = this.buildFilmSpecs(this.renderImageUrl, this.opacity);
6579
+ await this.canvasService.flushRenderFromProducers();
6580
+ if (seq !== this.renderSeq) return;
6581
+ this.canvasService.bringLayerToFront(FILM_LAYER_ID);
5971
6582
  this.canvasService.requestRenderAll();
5972
6583
  }
5973
6584
  };
@@ -6068,19 +6679,37 @@ var MirrorTool = class {
6068
6679
  import {
6069
6680
  ContributionPointIds as ContributionPointIds8
6070
6681
  } from "@pooder/core";
6071
- import { Line, Text, Group as Group2, Polygon } from "fabric";
6682
+ var RULER_LAYER_ID = "ruler-overlay";
6683
+ var EXTENSION_LINE_LENGTH = 5;
6684
+ var MIN_ARROW_SIZE = 4;
6685
+ var THICKNESS_TO_STROKE_WIDTH_RATIO = 20;
6686
+ var DEFAULT_THICKNESS = 20;
6687
+ var DEFAULT_GAP = 45;
6688
+ var DEFAULT_FONT_SIZE = 10;
6689
+ var DEFAULT_BACKGROUND_COLOR = "#f0f0f0";
6690
+ var DEFAULT_TEXT_COLOR = "#333333";
6691
+ var DEFAULT_LINE_COLOR = "#999999";
6692
+ var RULER_THICKNESS_MIN = 10;
6693
+ var RULER_THICKNESS_MAX = 100;
6694
+ var RULER_GAP_MIN = 0;
6695
+ var RULER_GAP_MAX = 100;
6696
+ var RULER_FONT_SIZE_MIN = 8;
6697
+ var RULER_FONT_SIZE_MAX = 24;
6072
6698
  var RulerTool = class {
6073
6699
  constructor(options) {
6074
6700
  this.id = "pooder.kit.ruler";
6075
6701
  this.metadata = {
6076
6702
  name: "RulerTool"
6077
6703
  };
6078
- this.thickness = 20;
6079
- this.gap = 15;
6080
- this.backgroundColor = "#f0f0f0";
6081
- this.textColor = "#333333";
6082
- this.lineColor = "#999999";
6083
- this.fontSize = 10;
6704
+ this.thickness = DEFAULT_THICKNESS;
6705
+ this.gap = DEFAULT_GAP;
6706
+ this.backgroundColor = DEFAULT_BACKGROUND_COLOR;
6707
+ this.textColor = DEFAULT_TEXT_COLOR;
6708
+ this.lineColor = DEFAULT_LINE_COLOR;
6709
+ this.fontSize = DEFAULT_FONT_SIZE;
6710
+ this.renderSeq = 0;
6711
+ this.numericProps = /* @__PURE__ */ new Set(["thickness", "gap", "fontSize"]);
6712
+ this.specs = [];
6084
6713
  this.onCanvasResized = () => {
6085
6714
  this.updateRuler();
6086
6715
  };
@@ -6089,50 +6718,73 @@ var RulerTool = class {
6089
6718
  }
6090
6719
  }
6091
6720
  activate(context) {
6721
+ var _a;
6092
6722
  this.context = context;
6093
6723
  this.canvasService = context.services.get("CanvasService");
6094
6724
  if (!this.canvasService) {
6095
- console.warn("CanvasService not found for RulerTool");
6725
+ console.warn("[RulerTool] CanvasService not found.");
6096
6726
  return;
6097
6727
  }
6728
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
6729
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
6730
+ this.id,
6731
+ () => ({
6732
+ rootLayerSpecs: {
6733
+ [RULER_LAYER_ID]: this.specs
6734
+ },
6735
+ replaceRootLayerIds: [RULER_LAYER_ID]
6736
+ }),
6737
+ { priority: 400 }
6738
+ );
6098
6739
  const configService = context.services.get(
6099
6740
  "ConfigurationService"
6100
6741
  );
6101
6742
  if (configService) {
6102
- this.thickness = configService.get("ruler.thickness", this.thickness);
6103
- this.gap = configService.get("ruler.gap", this.gap);
6104
- this.backgroundColor = configService.get(
6105
- "ruler.backgroundColor",
6106
- this.backgroundColor
6107
- );
6108
- this.textColor = configService.get("ruler.textColor", this.textColor);
6109
- this.lineColor = configService.get("ruler.lineColor", this.lineColor);
6110
- this.fontSize = configService.get("ruler.fontSize", this.fontSize);
6743
+ this.syncConfig(configService);
6111
6744
  configService.onAnyChange((e) => {
6112
6745
  let shouldUpdate = false;
6113
6746
  if (e.key.startsWith("ruler.")) {
6114
6747
  const prop = e.key.split(".")[1];
6115
6748
  if (prop && prop in this) {
6116
- this[prop] = e.value;
6749
+ if (this.numericProps.has(prop)) {
6750
+ this[prop] = this.toFiniteNumber(
6751
+ e.value,
6752
+ this[prop]
6753
+ );
6754
+ } else {
6755
+ this[prop] = e.value;
6756
+ }
6117
6757
  shouldUpdate = true;
6758
+ this.log("config:update", {
6759
+ key: e.key,
6760
+ raw: e.value,
6761
+ normalized: this[prop]
6762
+ });
6118
6763
  }
6119
6764
  } else if (e.key.startsWith("size.")) {
6120
6765
  shouldUpdate = true;
6766
+ this.log("size:update", { key: e.key, value: e.value });
6121
6767
  }
6122
6768
  if (shouldUpdate) {
6123
6769
  this.updateRuler();
6124
6770
  }
6125
6771
  });
6126
6772
  }
6127
- this.createLayer();
6128
6773
  context.eventBus.on("canvas:resized", this.onCanvasResized);
6129
6774
  this.updateRuler();
6130
6775
  }
6131
6776
  deactivate(context) {
6777
+ var _a;
6132
6778
  context.eventBus.off("canvas:resized", this.onCanvasResized);
6133
- this.destroyLayer();
6779
+ this.specs = [];
6780
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
6781
+ this.renderProducerDisposable = void 0;
6782
+ if (this.canvasService) {
6783
+ void this.canvasService.flushRenderFromProducers();
6784
+ }
6134
6785
  this.canvasService = void 0;
6135
6786
  this.context = void 0;
6787
+ this.renderSeq = 0;
6136
6788
  }
6137
6789
  contribute() {
6138
6790
  return {
@@ -6141,43 +6793,43 @@ var RulerTool = class {
6141
6793
  id: "ruler.thickness",
6142
6794
  type: "number",
6143
6795
  label: "Thickness",
6144
- min: 10,
6145
- max: 100,
6146
- default: 20
6796
+ min: RULER_THICKNESS_MIN,
6797
+ max: RULER_THICKNESS_MAX,
6798
+ default: DEFAULT_THICKNESS
6147
6799
  },
6148
6800
  {
6149
6801
  id: "ruler.gap",
6150
6802
  type: "number",
6151
6803
  label: "Gap",
6152
- min: 0,
6153
- max: 100,
6154
- default: 15
6804
+ min: RULER_GAP_MIN,
6805
+ max: RULER_GAP_MAX,
6806
+ default: DEFAULT_GAP
6155
6807
  },
6156
6808
  {
6157
6809
  id: "ruler.backgroundColor",
6158
6810
  type: "color",
6159
6811
  label: "Background Color",
6160
- default: "#f0f0f0"
6812
+ default: DEFAULT_BACKGROUND_COLOR
6161
6813
  },
6162
6814
  {
6163
6815
  id: "ruler.textColor",
6164
6816
  type: "color",
6165
6817
  label: "Text Color",
6166
- default: "#333333"
6818
+ default: DEFAULT_TEXT_COLOR
6167
6819
  },
6168
6820
  {
6169
6821
  id: "ruler.lineColor",
6170
6822
  type: "color",
6171
6823
  label: "Line Color",
6172
- default: "#999999"
6824
+ default: DEFAULT_LINE_COLOR
6173
6825
  },
6174
6826
  {
6175
6827
  id: "ruler.fontSize",
6176
6828
  type: "number",
6177
6829
  label: "Font Size",
6178
- min: 8,
6179
- max: 24,
6180
- default: 10
6830
+ min: RULER_FONT_SIZE_MIN,
6831
+ max: RULER_FONT_SIZE_MAX,
6832
+ default: DEFAULT_FONT_SIZE
6181
6833
  }
6182
6834
  ],
6183
6835
  [ContributionPointIds8.COMMANDS]: [
@@ -6190,12 +6842,23 @@ var RulerTool = class {
6190
6842
  textColor: this.textColor,
6191
6843
  lineColor: this.lineColor,
6192
6844
  fontSize: this.fontSize,
6193
- thickness: this.thickness
6845
+ thickness: this.thickness,
6846
+ gap: this.gap
6194
6847
  };
6195
6848
  const newState = { ...oldState, ...theme };
6196
- if (JSON.stringify(newState) === JSON.stringify(oldState))
6849
+ if (JSON.stringify(newState) === JSON.stringify(oldState)) {
6197
6850
  return true;
6851
+ }
6198
6852
  Object.assign(this, newState);
6853
+ this.thickness = this.toFiniteNumber(
6854
+ this.thickness,
6855
+ DEFAULT_THICKNESS
6856
+ );
6857
+ this.gap = this.toFiniteNumber(this.gap, DEFAULT_GAP);
6858
+ this.fontSize = this.toFiniteNumber(
6859
+ this.fontSize,
6860
+ DEFAULT_FONT_SIZE
6861
+ );
6199
6862
  this.updateRuler();
6200
6863
  return true;
6201
6864
  }
@@ -6203,225 +6866,367 @@ var RulerTool = class {
6203
6866
  ]
6204
6867
  };
6205
6868
  }
6206
- getLayer() {
6207
- var _a;
6208
- return (_a = this.canvasService) == null ? void 0 : _a.getLayer("ruler-overlay");
6869
+ log(step, payload) {
6870
+ if (payload) {
6871
+ console.debug(`[RulerTool] ${step}`, payload);
6872
+ return;
6873
+ }
6874
+ console.debug(`[RulerTool] ${step}`);
6209
6875
  }
6210
- createLayer() {
6211
- if (!this.canvasService) return;
6212
- const canvas = this.canvasService.canvas;
6213
- const width = canvas.width || 800;
6214
- const height = canvas.height || 600;
6215
- const layer = this.canvasService.createLayer("ruler-overlay", {
6216
- width,
6217
- height,
6218
- selectable: false,
6219
- evented: false,
6220
- left: 0,
6221
- top: 0,
6222
- originX: "left",
6223
- originY: "top"
6876
+ syncConfig(configService) {
6877
+ this.thickness = this.toFiniteNumber(
6878
+ configService.get("ruler.thickness", this.thickness),
6879
+ DEFAULT_THICKNESS
6880
+ );
6881
+ this.gap = Math.max(
6882
+ 0,
6883
+ this.toFiniteNumber(
6884
+ configService.get("ruler.gap", this.gap),
6885
+ DEFAULT_GAP
6886
+ )
6887
+ );
6888
+ this.backgroundColor = configService.get(
6889
+ "ruler.backgroundColor",
6890
+ this.backgroundColor
6891
+ );
6892
+ this.textColor = configService.get("ruler.textColor", this.textColor);
6893
+ this.lineColor = configService.get("ruler.lineColor", this.lineColor);
6894
+ this.fontSize = this.toFiniteNumber(
6895
+ configService.get("ruler.fontSize", this.fontSize),
6896
+ DEFAULT_FONT_SIZE
6897
+ );
6898
+ this.log("config:loaded", {
6899
+ thickness: this.thickness,
6900
+ gap: this.gap,
6901
+ fontSize: this.fontSize,
6902
+ backgroundColor: this.backgroundColor,
6903
+ textColor: this.textColor,
6904
+ lineColor: this.lineColor
6224
6905
  });
6225
- canvas.bringObjectToFront(layer);
6226
6906
  }
6227
- destroyLayer() {
6228
- if (!this.canvasService) return;
6229
- const layer = this.getLayer();
6230
- if (layer) {
6231
- this.canvasService.canvas.remove(layer);
6232
- }
6907
+ toFiniteNumber(value, fallback) {
6908
+ const numeric = Number(value);
6909
+ return Number.isFinite(numeric) ? numeric : fallback;
6233
6910
  }
6234
- createArrowLine(x1, y1, x2, y2, color) {
6235
- const line = new Line([x1, y1, x2, y2], {
6236
- stroke: color,
6237
- strokeWidth: this.thickness / 20,
6238
- // Scale stroke width relative to thickness (default 1)
6239
- selectable: false,
6240
- evented: false
6241
- });
6242
- const arrowSize = Math.max(4, this.thickness * 0.3);
6243
- const angle = Math.atan2(y2 - y1, x2 - x1);
6244
- const endArrow = new Polygon(
6245
- [
6246
- { x: 0, y: 0 },
6247
- { x: -arrowSize, y: -arrowSize / 2 },
6248
- { x: -arrowSize, y: arrowSize / 2 }
6249
- ],
6250
- {
6251
- fill: color,
6252
- left: x2,
6253
- top: y2,
6254
- originX: "right",
6255
- originY: "center",
6256
- angle: angle * 180 / Math.PI,
6911
+ toSceneDisplayLength(value) {
6912
+ if (!this.canvasService) return value;
6913
+ return this.canvasService.toSceneLength(value);
6914
+ }
6915
+ formatLengthMm(valueMm, unit) {
6916
+ const converted = fromMm(valueMm, unit);
6917
+ const fractionDigits = unit === "in" ? 3 : 2;
6918
+ return Number(converted.toFixed(fractionDigits)).toString();
6919
+ }
6920
+ buildLinePath(start, end) {
6921
+ const dx = end.x - start.x;
6922
+ const dy = end.y - start.y;
6923
+ return `M 0 0 L ${dx} ${dy}`;
6924
+ }
6925
+ buildStartArrowPath(size) {
6926
+ return `M 0 0 L ${size} ${-size / 2} L ${size} ${size / 2} Z`;
6927
+ }
6928
+ buildEndArrowPath(size) {
6929
+ return `M 0 0 L ${-size} ${-size / 2} L ${-size} ${size / 2} Z`;
6930
+ }
6931
+ createPathSpec(id, pathData, position, options) {
6932
+ var _a, _b, _c, _d, _e, _f, _g;
6933
+ return {
6934
+ id,
6935
+ type: "path",
6936
+ data: {
6937
+ id,
6938
+ type: "ruler"
6939
+ },
6940
+ props: {
6941
+ pathData,
6942
+ left: position.x,
6943
+ top: position.y,
6944
+ originX: (_a = options.originX) != null ? _a : "left",
6945
+ originY: (_b = options.originY) != null ? _b : "top",
6946
+ angle: (_c = options.angle) != null ? _c : 0,
6947
+ stroke: (_d = options.stroke) != null ? _d : null,
6948
+ fill: (_e = options.fill) != null ? _e : null,
6949
+ strokeWidth: (_f = options.strokeWidth) != null ? _f : 1,
6950
+ strokeLineCap: (_g = options.strokeLineCap) != null ? _g : "butt",
6257
6951
  selectable: false,
6258
- evented: false
6952
+ evented: false,
6953
+ excludeFromExport: true
6259
6954
  }
6260
- );
6261
- const startArrow = new Polygon(
6262
- [
6263
- { x: 0, y: 0 },
6264
- { x: arrowSize, y: -arrowSize / 2 },
6265
- { x: arrowSize, y: arrowSize / 2 }
6266
- ],
6267
- {
6268
- fill: color,
6269
- left: x1,
6270
- top: y1,
6271
- originX: "left",
6955
+ };
6956
+ }
6957
+ createTextSpec(id, text, position, angle = 0) {
6958
+ return {
6959
+ id,
6960
+ type: "text",
6961
+ data: {
6962
+ id,
6963
+ type: "ruler"
6964
+ },
6965
+ props: {
6966
+ text,
6967
+ left: position.x,
6968
+ top: position.y,
6969
+ angle,
6970
+ fontSize: this.toSceneDisplayLength(this.fontSize),
6971
+ fill: this.textColor,
6972
+ fontFamily: "Arial",
6973
+ originX: "center",
6272
6974
  originY: "center",
6273
- angle: angle * 180 / Math.PI,
6975
+ backgroundColor: this.backgroundColor,
6274
6976
  selectable: false,
6275
- evented: false
6977
+ evented: false,
6978
+ excludeFromExport: true
6276
6979
  }
6277
- );
6278
- return new Group2([line, startArrow, endArrow], {
6279
- selectable: false,
6280
- evented: false
6281
- });
6980
+ };
6282
6981
  }
6283
- updateRuler() {
6284
- var _a;
6285
- if (!this.canvasService) return;
6286
- const layer = this.getLayer();
6287
- if (!layer) return;
6288
- layer.remove(...layer.getObjects());
6289
- const { backgroundColor, lineColor, textColor, fontSize } = this;
6290
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
6291
- "ConfigurationService"
6982
+ buildRulerSpecs(input) {
6983
+ const { left, top, right, bottom, widthLabel, heightLabel } = input;
6984
+ const gap = Math.max(
6985
+ 0,
6986
+ this.toSceneDisplayLength(this.toFiniteNumber(this.gap, DEFAULT_GAP))
6292
6987
  );
6293
- if (!configService) return;
6294
- const sizeState = readSizeState(configService);
6295
- const layout = computeSceneLayout(this.canvasService, sizeState);
6296
- if (!layout) return;
6297
- const trimRect = layout.trimRect;
6298
- const cutRect = layout.cutRect;
6299
- const useCutAsRuler = layout.cutMode === "outset";
6300
- const rulerRect = useCutAsRuler ? cutRect : trimRect;
6301
- const gap = this.gap || 15;
6302
- const rulerLeft = rulerRect.left;
6303
- const rulerTop = rulerRect.top;
6304
- const rulerRight = rulerRect.left + rulerRect.width;
6305
- const rulerBottom = rulerRect.top + rulerRect.height;
6306
- const displayWidthMm = useCutAsRuler ? layout.cutWidthMm : layout.trimWidthMm;
6307
- const displayHeightMm = useCutAsRuler ? layout.cutHeightMm : layout.trimHeightMm;
6308
- const displayUnit = sizeState.unit;
6309
- const topRulerY = rulerTop - gap;
6310
- const topRulerXStart = rulerLeft;
6311
- const topRulerXEnd = rulerRight;
6312
- const leftRulerX = rulerLeft - gap;
6313
- const leftRulerYStart = rulerTop;
6314
- const leftRulerYEnd = rulerBottom;
6315
- const topDimLine = this.createArrowLine(
6316
- topRulerXStart,
6317
- topRulerY,
6318
- topRulerXEnd,
6319
- topRulerY,
6320
- lineColor
6988
+ const topY = top - gap;
6989
+ const leftX = left - gap;
6990
+ const arrowSize = Math.max(
6991
+ this.toSceneDisplayLength(MIN_ARROW_SIZE),
6992
+ this.toSceneDisplayLength(this.thickness * 0.3)
6321
6993
  );
6322
- layer.add(topDimLine);
6323
- const extLen = 5;
6324
- layer.add(
6325
- new Line(
6326
- [
6327
- topRulerXStart,
6328
- topRulerY - extLen,
6329
- topRulerXStart,
6330
- topRulerY + extLen
6331
- ],
6332
- {
6333
- stroke: lineColor,
6334
- strokeWidth: 1,
6335
- selectable: false,
6336
- evented: false
6337
- }
6994
+ const strokeWidth = Math.max(
6995
+ this.toSceneDisplayLength(1),
6996
+ this.toSceneDisplayLength(
6997
+ this.thickness / THICKNESS_TO_STROKE_WIDTH_RATIO
6338
6998
  )
6339
6999
  );
6340
- layer.add(
6341
- new Line(
6342
- [topRulerXEnd, topRulerY - extLen, topRulerXEnd, topRulerY + extLen],
7000
+ const extensionLength = this.toSceneDisplayLength(EXTENSION_LINE_LENGTH);
7001
+ const topLineAngleDeg = 0;
7002
+ const leftLineAngleDeg = 90;
7003
+ const topMidX = left + (right - left) / 2;
7004
+ const leftMidY = top + (bottom - top) / 2;
7005
+ const topLineStartX = Math.min(left + arrowSize, topMidX);
7006
+ const topLineEndX = Math.max(right - arrowSize, topMidX);
7007
+ const leftLineStartY = Math.min(top + arrowSize, leftMidY);
7008
+ const leftLineEndY = Math.max(bottom - arrowSize, leftMidY);
7009
+ const specs = [];
7010
+ specs.push(
7011
+ this.createPathSpec(
7012
+ "ruler.top.line",
7013
+ this.buildLinePath(
7014
+ { x: topLineStartX, y: topY },
7015
+ { x: topLineEndX, y: topY }
7016
+ ),
7017
+ { x: topLineStartX, y: topY },
6343
7018
  {
6344
- stroke: lineColor,
6345
- strokeWidth: 1,
6346
- selectable: false,
6347
- evented: false
7019
+ stroke: this.lineColor,
7020
+ strokeWidth,
7021
+ strokeLineCap: "butt"
6348
7022
  }
6349
- )
6350
- );
6351
- const widthStr = formatMm(displayWidthMm, displayUnit);
6352
- const topTextContent = `${widthStr} ${displayUnit}`;
6353
- const topText = new Text(topTextContent, {
6354
- left: topRulerXStart + (rulerRight - rulerLeft) / 2,
6355
- top: topRulerY,
6356
- fontSize,
6357
- fill: textColor,
6358
- fontFamily: "Arial",
6359
- originX: "center",
6360
- originY: "center",
6361
- backgroundColor,
6362
- // Background mask for readability
6363
- selectable: false,
6364
- evented: false
6365
- });
6366
- layer.add(topText);
6367
- const leftDimLine = this.createArrowLine(
6368
- leftRulerX,
6369
- leftRulerYStart,
6370
- leftRulerX,
6371
- leftRulerYEnd,
6372
- lineColor
6373
- );
6374
- layer.add(leftDimLine);
6375
- layer.add(
6376
- new Line(
6377
- [
6378
- leftRulerX - extLen,
6379
- leftRulerYStart,
6380
- leftRulerX + extLen,
6381
- leftRulerYStart
6382
- ],
6383
- {
6384
- stroke: lineColor,
6385
- strokeWidth: 1,
6386
- selectable: false,
6387
- evented: false
7023
+ ),
7024
+ this.createPathSpec(
7025
+ "ruler.top.arrow.start",
7026
+ this.buildStartArrowPath(arrowSize),
7027
+ { x: left, y: topY },
7028
+ {
7029
+ fill: this.lineColor,
7030
+ stroke: this.lineColor,
7031
+ strokeWidth: this.toSceneDisplayLength(1),
7032
+ originX: "left",
7033
+ originY: "center",
7034
+ angle: topLineAngleDeg
6388
7035
  }
6389
- )
7036
+ ),
7037
+ this.createPathSpec(
7038
+ "ruler.top.arrow.end",
7039
+ this.buildEndArrowPath(arrowSize),
7040
+ { x: right, y: topY },
7041
+ {
7042
+ fill: this.lineColor,
7043
+ stroke: this.lineColor,
7044
+ strokeWidth: this.toSceneDisplayLength(1),
7045
+ originX: "right",
7046
+ originY: "center",
7047
+ angle: topLineAngleDeg
7048
+ }
7049
+ ),
7050
+ this.createPathSpec(
7051
+ "ruler.top.ext.start",
7052
+ this.buildLinePath(
7053
+ { x: left, y: topY - extensionLength },
7054
+ { x: left, y: topY + extensionLength }
7055
+ ),
7056
+ { x: left, y: topY - extensionLength },
7057
+ {
7058
+ stroke: this.lineColor,
7059
+ strokeWidth: this.toSceneDisplayLength(1)
7060
+ }
7061
+ ),
7062
+ this.createPathSpec(
7063
+ "ruler.top.ext.end",
7064
+ this.buildLinePath(
7065
+ { x: right, y: topY - extensionLength },
7066
+ { x: right, y: topY + extensionLength }
7067
+ ),
7068
+ { x: right, y: topY - extensionLength },
7069
+ {
7070
+ stroke: this.lineColor,
7071
+ strokeWidth: this.toSceneDisplayLength(1)
7072
+ }
7073
+ ),
7074
+ this.createTextSpec("ruler.top.label", widthLabel, {
7075
+ x: left + (right - left) / 2,
7076
+ y: topY
7077
+ })
6390
7078
  );
6391
- layer.add(
6392
- new Line(
6393
- [
6394
- leftRulerX - extLen,
6395
- leftRulerYEnd,
6396
- leftRulerX + extLen,
6397
- leftRulerYEnd
6398
- ],
6399
- {
6400
- stroke: lineColor,
6401
- strokeWidth: 1,
6402
- selectable: false,
6403
- evented: false
7079
+ specs.push(
7080
+ this.createPathSpec(
7081
+ "ruler.left.line",
7082
+ this.buildLinePath(
7083
+ { x: leftX, y: leftLineStartY },
7084
+ { x: leftX, y: leftLineEndY }
7085
+ ),
7086
+ { x: leftX, y: leftLineStartY },
7087
+ {
7088
+ stroke: this.lineColor,
7089
+ strokeWidth,
7090
+ strokeLineCap: "butt"
6404
7091
  }
7092
+ ),
7093
+ this.createPathSpec(
7094
+ "ruler.left.arrow.start",
7095
+ this.buildStartArrowPath(arrowSize),
7096
+ { x: leftX, y: top },
7097
+ {
7098
+ fill: this.lineColor,
7099
+ stroke: this.lineColor,
7100
+ strokeWidth: this.toSceneDisplayLength(1),
7101
+ originX: "left",
7102
+ originY: "center",
7103
+ angle: leftLineAngleDeg
7104
+ }
7105
+ ),
7106
+ this.createPathSpec(
7107
+ "ruler.left.arrow.end",
7108
+ this.buildEndArrowPath(arrowSize),
7109
+ { x: leftX, y: bottom },
7110
+ {
7111
+ fill: this.lineColor,
7112
+ stroke: this.lineColor,
7113
+ strokeWidth: this.toSceneDisplayLength(1),
7114
+ originX: "right",
7115
+ originY: "center",
7116
+ angle: leftLineAngleDeg
7117
+ }
7118
+ ),
7119
+ this.createPathSpec(
7120
+ "ruler.left.ext.start",
7121
+ this.buildLinePath(
7122
+ { x: leftX - extensionLength, y: top },
7123
+ { x: leftX + extensionLength, y: top }
7124
+ ),
7125
+ { x: leftX - extensionLength, y: top },
7126
+ {
7127
+ stroke: this.lineColor,
7128
+ strokeWidth: this.toSceneDisplayLength(1)
7129
+ }
7130
+ ),
7131
+ this.createPathSpec(
7132
+ "ruler.left.ext.end",
7133
+ this.buildLinePath(
7134
+ { x: leftX - extensionLength, y: bottom },
7135
+ { x: leftX + extensionLength, y: bottom }
7136
+ ),
7137
+ { x: leftX - extensionLength, y: bottom },
7138
+ {
7139
+ stroke: this.lineColor,
7140
+ strokeWidth: this.toSceneDisplayLength(1)
7141
+ }
7142
+ ),
7143
+ this.createTextSpec(
7144
+ "ruler.left.label",
7145
+ heightLabel,
7146
+ {
7147
+ x: leftX,
7148
+ y: top + (bottom - top) / 2
7149
+ },
7150
+ -90
6405
7151
  )
6406
7152
  );
6407
- const heightStr = formatMm(displayHeightMm, displayUnit);
6408
- const leftTextContent = `${heightStr} ${displayUnit}`;
6409
- const leftText = new Text(leftTextContent, {
6410
- left: leftRulerX,
6411
- top: leftRulerYStart + (rulerBottom - rulerTop) / 2,
6412
- angle: -90,
6413
- fontSize,
6414
- fill: textColor,
6415
- fontFamily: "Arial",
6416
- originX: "center",
6417
- originY: "center",
6418
- backgroundColor,
6419
- selectable: false,
6420
- evented: false
7153
+ return specs;
7154
+ }
7155
+ updateRuler() {
7156
+ void this.updateRulerAsync();
7157
+ }
7158
+ async updateRulerAsync() {
7159
+ var _a, _b;
7160
+ if (!this.canvasService) return;
7161
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get(
7162
+ "ConfigurationService"
7163
+ );
7164
+ if (!configService) return;
7165
+ const seq = ++this.renderSeq;
7166
+ const sizeState = readSizeState(configService);
7167
+ const layout = computeSceneLayout(this.canvasService, sizeState);
7168
+ this.log("render:start", {
7169
+ seq,
7170
+ unit: sizeState.unit,
7171
+ gap: this.gap,
7172
+ thickness: this.thickness,
7173
+ fontSize: this.fontSize,
7174
+ hasLayout: !!layout,
7175
+ scale: (_b = layout == null ? void 0 : layout.scale) != null ? _b : null
6421
7176
  });
6422
- layer.add(leftText);
6423
- this.canvasService.canvas.bringObjectToFront(layer);
6424
- this.canvasService.canvas.requestRenderAll();
7177
+ if (!layout || layout.scale <= 0) {
7178
+ if (seq !== this.renderSeq) return;
7179
+ this.log("render:skip", { seq, reason: "invalid-layout" });
7180
+ this.specs = [];
7181
+ await this.canvasService.flushRenderFromProducers();
7182
+ return;
7183
+ }
7184
+ const geometry = buildSceneGeometry(configService, layout);
7185
+ if (geometry.unit !== "px") {
7186
+ console.warn("[RulerTool] Unexpected geometry unit.", geometry.unit);
7187
+ }
7188
+ const centerScene = this.canvasService.toScenePoint({
7189
+ x: geometry.x,
7190
+ y: geometry.y
7191
+ });
7192
+ const widthScene = this.canvasService.toSceneLength(geometry.width);
7193
+ const heightScene = this.canvasService.toSceneLength(geometry.height);
7194
+ const rulerLeft = centerScene.x - widthScene / 2;
7195
+ const rulerTop = centerScene.y - heightScene / 2;
7196
+ const rulerRight = rulerLeft + widthScene;
7197
+ const rulerBottom = rulerTop + heightScene;
7198
+ const widthMm = widthScene;
7199
+ const heightMm = heightScene;
7200
+ const unit = sizeState.unit;
7201
+ const widthLabel = `${this.formatLengthMm(widthMm, unit)} ${unit}`;
7202
+ const heightLabel = `${this.formatLengthMm(heightMm, unit)} ${unit}`;
7203
+ const specs = this.buildRulerSpecs({
7204
+ left: rulerLeft,
7205
+ top: rulerTop,
7206
+ right: rulerRight,
7207
+ bottom: rulerBottom,
7208
+ widthLabel,
7209
+ heightLabel
7210
+ });
7211
+ this.log("render:geometry", {
7212
+ seq,
7213
+ left: rulerLeft,
7214
+ top: rulerTop,
7215
+ right: rulerRight,
7216
+ bottom: rulerBottom,
7217
+ widthScene,
7218
+ heightScene,
7219
+ widthMm,
7220
+ heightMm,
7221
+ specCount: specs.length
7222
+ });
7223
+ if (seq !== this.renderSeq) return;
7224
+ this.specs = specs;
7225
+ await this.canvasService.flushRenderFromProducers();
7226
+ if (seq !== this.renderSeq) return;
7227
+ this.canvasService.bringLayerToFront(RULER_LAYER_ID);
7228
+ this.canvasService.requestRenderAll();
7229
+ this.log("render:done", { seq });
6425
7230
  }
6426
7231
  };
6427
7232
 
@@ -6460,6 +7265,9 @@ var WhiteInkTool = class {
6460
7265
  this.printWithWhiteInk = true;
6461
7266
  this.previewImageVisible = true;
6462
7267
  this.renderSeq = 0;
7268
+ this.whiteSpecs = [];
7269
+ this.coverSpecs = [];
7270
+ this.overlaySpecs = [];
6463
7271
  this.onToolActivated = (event) => {
6464
7272
  const before = this.isToolActive;
6465
7273
  this.syncToolActiveFromWorkbench(event.id);
@@ -6497,12 +7305,25 @@ var WhiteInkTool = class {
6497
7305
  };
6498
7306
  }
6499
7307
  activate(context) {
7308
+ var _a;
6500
7309
  this.context = context;
6501
7310
  this.canvasService = context.services.get("CanvasService");
6502
7311
  if (!this.canvasService) {
6503
7312
  console.warn("CanvasService not found for WhiteInkTool");
6504
7313
  return;
6505
7314
  }
7315
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
7316
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
7317
+ this.id,
7318
+ () => ({
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
+ }
7324
+ }),
7325
+ { priority: 260 }
7326
+ );
6506
7327
  context.eventBus.on("tool:activated", this.onToolActivated);
6507
7328
  context.eventBus.on("scene:layout:change", this.onSceneLayoutChanged);
6508
7329
  context.eventBus.on("object:added", this.onObjectAdded);
@@ -6568,7 +7389,7 @@ var WhiteInkTool = class {
6568
7389
  this.updateWhiteInks();
6569
7390
  }
6570
7391
  deactivate(context) {
6571
- var _a;
7392
+ var _a, _b;
6572
7393
  context.eventBus.off("tool:activated", this.onToolActivated);
6573
7394
  context.eventBus.off("scene:layout:change", this.onSceneLayoutChanged);
6574
7395
  context.eventBus.off("object:added", this.onObjectAdded);
@@ -6579,6 +7400,11 @@ var WhiteInkTool = class {
6579
7400
  this.dirtyTrackerDisposable = void 0;
6580
7401
  this.clearRenderedWhiteInks();
6581
7402
  this.applyImageVisibilityForWhiteInk(false);
7403
+ (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
7404
+ this.renderProducerDisposable = void 0;
7405
+ if (this.canvasService) {
7406
+ void this.canvasService.flushRenderFromProducers();
7407
+ }
6582
7408
  this.canvasService = void 0;
6583
7409
  this.context = void 0;
6584
7410
  }
@@ -7001,11 +7827,20 @@ var WhiteInkTool = class {
7001
7827
  if (!layout) {
7002
7828
  return { left: 0, top: 0, width: 0, height: 0 };
7003
7829
  }
7004
- return {
7830
+ return this.canvasService.toSceneRect({
7005
7831
  left: layout.cutRect.left,
7006
7832
  top: layout.cutRect.top,
7007
7833
  width: layout.cutRect.width,
7008
7834
  height: layout.cutRect.height
7835
+ });
7836
+ }
7837
+ toLayoutSceneRect(rect) {
7838
+ return {
7839
+ left: rect.left,
7840
+ top: rect.top,
7841
+ width: rect.width,
7842
+ height: rect.height,
7843
+ space: "scene"
7009
7844
  };
7010
7845
  }
7011
7846
  getImageObjects() {
@@ -7028,7 +7863,7 @@ var WhiteInkTool = class {
7028
7863
  return (_a = obj == null ? void 0 : obj._originalElement) == null ? void 0 : _a.src;
7029
7864
  }
7030
7865
  getImageSnapshot(obj) {
7031
- var _a;
7866
+ var _a, _b;
7032
7867
  if (!obj) return null;
7033
7868
  const src = this.getCurrentSrc(obj);
7034
7869
  if (!src) return null;
@@ -7036,14 +7871,18 @@ var WhiteInkTool = class {
7036
7871
  const width = Number((obj == null ? void 0 : obj.width) || 0);
7037
7872
  const height = Number((obj == null ? void 0 : obj.height) || 0);
7038
7873
  this.rememberSourceSize(src, { width, height });
7874
+ const sceneScale = ((_a = this.canvasService) == null ? void 0 : _a.getSceneScale()) || 1;
7875
+ const leftScreen = Number.isFinite(obj == null ? void 0 : obj.left) ? Number(obj.left) : 0;
7876
+ const topScreen = Number.isFinite(obj == null ? void 0 : obj.top) ? Number(obj.top) : 0;
7877
+ const scenePoint = this.canvasService ? this.canvasService.toScenePoint({ x: leftScreen, y: topScreen }) : { x: leftScreen, y: topScreen };
7039
7878
  return {
7040
- id: String(((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id) || "image"),
7879
+ id: String(((_b = obj == null ? void 0 : obj.data) == null ? void 0 : _b.id) || "image"),
7041
7880
  src,
7042
7881
  element,
7043
- left: Number.isFinite(obj == null ? void 0 : obj.left) ? Number(obj.left) : 0,
7044
- top: Number.isFinite(obj == null ? void 0 : obj.top) ? Number(obj.top) : 0,
7045
- scaleX: Number.isFinite(obj == null ? void 0 : obj.scaleX) ? Number(obj.scaleX) : 1,
7046
- scaleY: Number.isFinite(obj == null ? void 0 : obj.scaleY) ? Number(obj.scaleY) : 1,
7882
+ left: scenePoint.x,
7883
+ top: scenePoint.y,
7884
+ scaleX: (Number.isFinite(obj == null ? void 0 : obj.scaleX) ? Number(obj.scaleX) : 1) / sceneScale,
7885
+ scaleY: (Number.isFinite(obj == null ? void 0 : obj.scaleY) ? Number(obj.scaleY) : 1) / sceneScale,
7047
7886
  angle: Number.isFinite(obj == null ? void 0 : obj.angle) ? Number(obj.angle) : 0,
7048
7887
  originX: typeof (obj == null ? void 0 : obj.originX) === "string" ? obj.originX : "center",
7049
7888
  originY: typeof (obj == null ? void 0 : obj.originY) === "string" ? obj.originY : "center",
@@ -7218,8 +8057,11 @@ var WhiteInkTool = class {
7218
8057
  var _a, _b;
7219
8058
  if (!this.isToolActive || !this.canvasService) return [];
7220
8059
  if (frame.width <= 0 || frame.height <= 0) return [];
7221
- const canvasW = this.canvasService.canvas.width || 0;
7222
- const canvasH = this.canvasService.canvas.height || 0;
8060
+ const viewport = this.canvasService.getSceneViewportRect();
8061
+ const canvasW = viewport.width || 0;
8062
+ const canvasH = viewport.height || 0;
8063
+ const canvasLeft = viewport.left || 0;
8064
+ const canvasTop = viewport.top || 0;
7223
8065
  const strokeColor = this.getConfig("image.frame.strokeColor", "#808080") || "#808080";
7224
8066
  const strokeWidthRaw = Number(
7225
8067
  (_a = this.getConfig("image.frame.strokeWidth", 2)) != null ? _a : 2
@@ -7231,21 +8073,42 @@ var WhiteInkTool = class {
7231
8073
  const innerBackground = this.getConfig("image.frame.innerBackground", "rgba(0,0,0,0)") || "rgba(0,0,0,0)";
7232
8074
  const strokeWidth = Number.isFinite(strokeWidthRaw) ? Math.max(0, strokeWidthRaw) : 2;
7233
8075
  const dashLength = Number.isFinite(dashLengthRaw) ? Math.max(1, dashLengthRaw) : 8;
7234
- const frameLeft = Math.max(0, Math.min(canvasW, frame.left));
7235
- const frameTop = Math.max(0, Math.min(canvasH, frame.top));
8076
+ const strokeWidthScene = this.canvasService.toSceneLength(strokeWidth);
8077
+ const dashLengthScene = this.canvasService.toSceneLength(dashLength);
8078
+ const frameLeft = Math.max(
8079
+ canvasLeft,
8080
+ Math.min(canvasLeft + canvasW, frame.left)
8081
+ );
8082
+ const frameTop = Math.max(
8083
+ canvasTop,
8084
+ Math.min(canvasTop + canvasH, frame.top)
8085
+ );
7236
8086
  const frameRight = Math.max(
7237
8087
  frameLeft,
7238
- Math.min(canvasW, frame.left + frame.width)
8088
+ Math.min(canvasLeft + canvasW, frame.left + frame.width)
7239
8089
  );
7240
8090
  const frameBottom = Math.max(
7241
8091
  frameTop,
7242
- Math.min(canvasH, frame.top + frame.height)
8092
+ Math.min(canvasTop + canvasH, frame.top + frame.height)
7243
8093
  );
7244
8094
  const visibleFrameH = Math.max(0, frameBottom - frameTop);
7245
- const topH = frameTop;
7246
- const bottomH = Math.max(0, canvasH - frameBottom);
7247
- const leftW = frameLeft;
7248
- const rightW = Math.max(0, canvasW - frameRight);
8095
+ const topH = Math.max(0, frameTop - canvasTop);
8096
+ const bottomH = Math.max(0, canvasTop + canvasH - frameBottom);
8097
+ const leftW = Math.max(0, frameLeft - canvasLeft);
8098
+ const rightW = Math.max(0, canvasLeft + canvasW - frameRight);
8099
+ const viewportRect = this.toLayoutSceneRect({
8100
+ left: canvasLeft,
8101
+ top: canvasTop,
8102
+ width: canvasW,
8103
+ height: canvasH
8104
+ });
8105
+ const visibleFrameBandRect = this.toLayoutSceneRect({
8106
+ left: canvasLeft,
8107
+ top: frameTop,
8108
+ width: canvasW,
8109
+ height: visibleFrameH
8110
+ });
8111
+ const frameRect = this.toLayoutSceneRect(frame);
7249
8112
  const maskSpecs = [
7250
8113
  {
7251
8114
  id: "white-ink.cropMask.top",
@@ -7255,13 +8118,17 @@ var WhiteInkTool = class {
7255
8118
  layerId: WHITE_INK_OVERLAY_LAYER_ID,
7256
8119
  type: "white-ink-mask"
7257
8120
  },
8121
+ layout: {
8122
+ reference: "custom",
8123
+ referenceRect: viewportRect,
8124
+ alignX: "start",
8125
+ alignY: "start",
8126
+ width: "100%",
8127
+ height: topH
8128
+ },
7258
8129
  props: {
7259
- left: canvasW / 2,
7260
- top: topH / 2,
7261
- width: canvasW,
7262
- height: topH,
7263
- originX: "center",
7264
- originY: "center",
8130
+ originX: "left",
8131
+ originY: "top",
7265
8132
  fill: outerBackground,
7266
8133
  selectable: false,
7267
8134
  evented: false,
@@ -7276,13 +8143,17 @@ var WhiteInkTool = class {
7276
8143
  layerId: WHITE_INK_OVERLAY_LAYER_ID,
7277
8144
  type: "white-ink-mask"
7278
8145
  },
8146
+ layout: {
8147
+ reference: "custom",
8148
+ referenceRect: viewportRect,
8149
+ alignX: "start",
8150
+ alignY: "end",
8151
+ width: "100%",
8152
+ height: bottomH
8153
+ },
7279
8154
  props: {
7280
- left: canvasW / 2,
7281
- top: frameBottom + bottomH / 2,
7282
- width: canvasW,
7283
- height: bottomH,
7284
- originX: "center",
7285
- originY: "center",
8155
+ originX: "left",
8156
+ originY: "top",
7286
8157
  fill: outerBackground,
7287
8158
  selectable: false,
7288
8159
  evented: false,
@@ -7297,13 +8168,17 @@ var WhiteInkTool = class {
7297
8168
  layerId: WHITE_INK_OVERLAY_LAYER_ID,
7298
8169
  type: "white-ink-mask"
7299
8170
  },
7300
- props: {
7301
- left: leftW / 2,
7302
- top: frameTop + visibleFrameH / 2,
8171
+ layout: {
8172
+ reference: "custom",
8173
+ referenceRect: visibleFrameBandRect,
8174
+ alignX: "start",
8175
+ alignY: "start",
7303
8176
  width: leftW,
7304
- height: visibleFrameH,
7305
- originX: "center",
7306
- originY: "center",
8177
+ height: "100%"
8178
+ },
8179
+ props: {
8180
+ originX: "left",
8181
+ originY: "top",
7307
8182
  fill: outerBackground,
7308
8183
  selectable: false,
7309
8184
  evented: false,
@@ -7318,13 +8193,17 @@ var WhiteInkTool = class {
7318
8193
  layerId: WHITE_INK_OVERLAY_LAYER_ID,
7319
8194
  type: "white-ink-mask"
7320
8195
  },
7321
- props: {
7322
- left: frameRight + rightW / 2,
7323
- top: frameTop + visibleFrameH / 2,
8196
+ layout: {
8197
+ reference: "custom",
8198
+ referenceRect: visibleFrameBandRect,
8199
+ alignX: "end",
8200
+ alignY: "start",
7324
8201
  width: rightW,
7325
- height: visibleFrameH,
7326
- originX: "center",
7327
- originY: "center",
8202
+ height: "100%"
8203
+ },
8204
+ props: {
8205
+ originX: "left",
8206
+ originY: "top",
7328
8207
  fill: outerBackground,
7329
8208
  selectable: false,
7330
8209
  evented: false,
@@ -7342,17 +8221,21 @@ var WhiteInkTool = class {
7342
8221
  layerId: WHITE_INK_OVERLAY_LAYER_ID,
7343
8222
  type: "white-ink-frame"
7344
8223
  },
8224
+ layout: {
8225
+ reference: "custom",
8226
+ referenceRect: frameRect,
8227
+ alignX: "start",
8228
+ alignY: "start",
8229
+ width: "100%",
8230
+ height: "100%"
8231
+ },
7345
8232
  props: {
7346
- left: frame.left + frame.width / 2,
7347
- top: frame.top + frame.height / 2,
7348
- width: frame.width,
7349
- height: frame.height,
7350
- originX: "center",
7351
- originY: "center",
8233
+ originX: "left",
8234
+ originY: "top",
7352
8235
  fill: innerBackground,
7353
8236
  stroke: strokeColor,
7354
- strokeWidth,
7355
- strokeDashArray: [dashLength, dashLength],
8237
+ strokeWidth: strokeWidthScene,
8238
+ strokeDashArray: [dashLengthScene, dashLengthScene],
7356
8239
  selectable: false,
7357
8240
  evented: false,
7358
8241
  excludeFromExport: true
@@ -7428,50 +8311,30 @@ var WhiteInkTool = class {
7428
8311
  }
7429
8312
  ).filter((index) => index >= 0);
7430
8313
  let whiteInsertIndex = imageIndexes.length ? Math.min(...imageIndexes) : this.resolveDefaultInsertIndex(currentObjects);
7431
- whiteObjects.forEach((obj) => {
7432
- canvas.moveObjectTo(obj, whiteInsertIndex);
7433
- whiteInsertIndex += 1;
7434
- });
7435
- const afterWhiteObjects = canvas.getObjects();
7436
- const afterImageIndexes = afterWhiteObjects.map(
7437
- (obj, index) => {
7438
- var _a;
7439
- return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === IMAGE_OBJECT_LAYER_ID3 ? index : -1;
7440
- }
7441
- ).filter((index) => index >= 0);
7442
- let coverInsertIndex = afterImageIndexes.length ? Math.max(...afterImageIndexes) + 1 : whiteInsertIndex;
8314
+ let coverInsertIndex = whiteInsertIndex;
7443
8315
  coverObjects.forEach((obj) => {
7444
8316
  canvas.moveObjectTo(obj, coverInsertIndex);
7445
8317
  coverInsertIndex += 1;
7446
8318
  });
8319
+ whiteInsertIndex = coverInsertIndex;
8320
+ whiteObjects.forEach((obj) => {
8321
+ canvas.moveObjectTo(obj, whiteInsertIndex);
8322
+ whiteInsertIndex += 1;
8323
+ });
7447
8324
  frameObjects.forEach((obj) => canvas.bringObjectToFront(obj));
7448
8325
  canvas.getObjects().filter((obj) => {
7449
8326
  var _a;
7450
8327
  return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === IMAGE_OVERLAY_LAYER_ID2;
7451
8328
  }).forEach((obj) => canvas.bringObjectToFront(obj));
7452
- const dielineOverlay = this.canvasService.getLayer("dieline-overlay");
7453
- if (dielineOverlay) {
7454
- canvas.bringObjectToFront(dielineOverlay);
7455
- }
7456
- const rulerOverlay = this.canvasService.getLayer("ruler-overlay");
7457
- if (rulerOverlay) {
7458
- canvas.bringObjectToFront(rulerOverlay);
7459
- }
8329
+ this.canvasService.bringLayerToFront("dieline-overlay");
8330
+ this.canvasService.bringLayerToFront("ruler-overlay");
7460
8331
  }
7461
8332
  clearRenderedWhiteInks() {
7462
8333
  if (!this.canvasService) return;
7463
- void this.canvasService.applyObjectSpecsToRootLayer(
7464
- WHITE_INK_OBJECT_LAYER_ID,
7465
- []
7466
- );
7467
- void this.canvasService.applyObjectSpecsToRootLayer(
7468
- WHITE_INK_COVER_LAYER_ID,
7469
- []
7470
- );
7471
- void this.canvasService.applyObjectSpecsToRootLayer(
7472
- WHITE_INK_OVERLAY_LAYER_ID,
7473
- []
7474
- );
8334
+ this.whiteSpecs = [];
8335
+ this.coverSpecs = [];
8336
+ this.overlaySpecs = [];
8337
+ this.canvasService.requestRenderFromProducers();
7475
8338
  }
7476
8339
  purgeSourceCaches(item) {
7477
8340
  const sourceUrl = this.resolveSourceUrl(item);
@@ -7538,20 +8401,12 @@ var WhiteInkTool = class {
7538
8401
  }
7539
8402
  }
7540
8403
  }
7541
- await this.canvasService.applyObjectSpecsToRootLayer(
7542
- WHITE_INK_OBJECT_LAYER_ID,
7543
- whiteSpecs
7544
- );
8404
+ this.whiteSpecs = whiteSpecs;
7545
8405
  if (seq !== this.renderSeq) return;
7546
- await this.canvasService.applyObjectSpecsToRootLayer(
7547
- WHITE_INK_COVER_LAYER_ID,
7548
- coverSpecs
7549
- );
8406
+ this.coverSpecs = coverSpecs;
7550
8407
  if (seq !== this.renderSeq) return;
7551
- await this.canvasService.applyObjectSpecsToRootLayer(
7552
- WHITE_INK_OVERLAY_LAYER_ID,
7553
- frameSpecs
7554
- );
8408
+ this.overlaySpecs = frameSpecs;
8409
+ await this.canvasService.flushRenderFromProducers();
7555
8410
  if (seq !== this.renderSeq) return;
7556
8411
  this.syncZOrder();
7557
8412
  this.canvasService.requestRenderAll();
@@ -7784,23 +8639,16 @@ var SceneVisibilityService = class {
7784
8639
  const dielineLayer = this.canvasService.getLayer("dieline-overlay");
7785
8640
  if (dielineLayer) {
7786
8641
  const visible = !HIDDEN_DIELINE_TOOLS.has(this.activeToolId || "");
7787
- if (dielineLayer.visible !== visible) {
7788
- dielineLayer.set({ visible });
7789
- }
7790
- }
7791
- const rulerLayer = this.canvasService.getLayer("ruler-overlay");
7792
- if (rulerLayer) {
7793
- const visible = !HIDDEN_RULER_TOOLS.has(this.activeToolId || "");
7794
- if (rulerLayer.visible !== visible) {
7795
- rulerLayer.set({ visible });
7796
- }
8642
+ this.canvasService.setLayerVisibility("dieline-overlay", visible);
7797
8643
  }
8644
+ const rulerVisible = !HIDDEN_RULER_TOOLS.has(this.activeToolId || "");
8645
+ this.canvasService.setLayerVisibility("ruler-overlay", rulerVisible);
7798
8646
  this.canvasService.requestRenderAll();
7799
8647
  }
7800
8648
  };
7801
8649
 
7802
8650
  // src/services/CanvasService.ts
7803
- import { Canvas, Group as Group3, Rect as Rect4, Path as Path2, Image as Image4 } from "fabric";
8651
+ import { Canvas, Group, Rect, Path as Path2, Image as Image2, Text } from "fabric";
7804
8652
 
7805
8653
  // src/services/ViewportSystem.ts
7806
8654
  var ViewportSystem = class {
@@ -7878,6 +8726,13 @@ var ViewportSystem = class {
7878
8726
  // src/services/CanvasService.ts
7879
8727
  var CanvasService = class {
7880
8728
  constructor(el, options) {
8729
+ this.renderProducers = /* @__PURE__ */ new Map();
8730
+ this.producerOrder = 0;
8731
+ this.producerFlushRequested = false;
8732
+ this.producerLoopPending = false;
8733
+ this.producerLoopPromise = null;
8734
+ this.managedProducerLayerIds = /* @__PURE__ */ new Set();
8735
+ this.managedProducerRootLayerIds = /* @__PURE__ */ new Set();
7881
8736
  if (el instanceof Canvas) {
7882
8737
  this.canvas = el;
7883
8738
  } else {
@@ -7910,8 +8765,156 @@ var CanvasService = class {
7910
8765
  this.canvas.on("object:removed", forward("object:removed"));
7911
8766
  }
7912
8767
  dispose() {
8768
+ this.renderProducers.clear();
8769
+ this.managedProducerLayerIds.clear();
8770
+ this.managedProducerRootLayerIds.clear();
8771
+ this.producerFlushRequested = false;
7913
8772
  this.canvas.dispose();
7914
8773
  }
8774
+ registerRenderProducer(toolId, producer, options = {}) {
8775
+ const normalizedToolId = String(toolId || "").trim();
8776
+ if (!normalizedToolId) {
8777
+ throw new Error(
8778
+ "[CanvasService] registerRenderProducer requires a toolId."
8779
+ );
8780
+ }
8781
+ if (typeof producer !== "function") {
8782
+ throw new Error(
8783
+ `[CanvasService] registerRenderProducer("${normalizedToolId}") requires a producer function.`
8784
+ );
8785
+ }
8786
+ const entry = {
8787
+ toolId: normalizedToolId,
8788
+ producer,
8789
+ priority: Number.isFinite(options.priority) ? Number(options.priority) : 0,
8790
+ order: this.producerOrder++
8791
+ };
8792
+ this.renderProducers.set(normalizedToolId, entry);
8793
+ this.requestRenderFromProducers();
8794
+ return {
8795
+ dispose: () => {
8796
+ this.unregisterRenderProducer(normalizedToolId);
8797
+ }
8798
+ };
8799
+ }
8800
+ unregisterRenderProducer(toolId) {
8801
+ const normalizedToolId = String(toolId || "").trim();
8802
+ if (!normalizedToolId) return false;
8803
+ const removed = this.renderProducers.delete(normalizedToolId);
8804
+ if (removed) {
8805
+ this.requestRenderFromProducers();
8806
+ }
8807
+ return removed;
8808
+ }
8809
+ requestRenderFromProducers() {
8810
+ this.producerFlushRequested = true;
8811
+ this.scheduleProducerLoop();
8812
+ }
8813
+ async flushRenderFromProducers() {
8814
+ this.requestRenderFromProducers();
8815
+ if (this.producerLoopPromise) {
8816
+ await this.producerLoopPromise;
8817
+ }
8818
+ }
8819
+ scheduleProducerLoop() {
8820
+ if (this.producerLoopPending) return;
8821
+ this.producerLoopPending = true;
8822
+ this.producerLoopPromise = Promise.resolve().then(() => this.runProducerLoop()).catch((error) => {
8823
+ console.error("[CanvasService] render producer loop failed.", error);
8824
+ }).finally(() => {
8825
+ this.producerLoopPending = false;
8826
+ if (this.producerFlushRequested) {
8827
+ this.scheduleProducerLoop();
8828
+ }
8829
+ });
8830
+ }
8831
+ async runProducerLoop() {
8832
+ while (this.producerFlushRequested) {
8833
+ this.producerFlushRequested = false;
8834
+ await this.collectAndApplyProducerSpecs();
8835
+ }
8836
+ }
8837
+ sortedRenderProducerEntries() {
8838
+ return Array.from(this.renderProducers.values()).sort((a, b) => {
8839
+ if (a.priority !== b.priority) {
8840
+ return a.priority - b.priority;
8841
+ }
8842
+ if (a.order !== b.order) {
8843
+ return a.order - b.order;
8844
+ }
8845
+ return a.toolId.localeCompare(b.toolId);
8846
+ });
8847
+ }
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);
8855
+ });
8856
+ }
8857
+ 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();
8862
+ 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
+ });
8878
+ }
8879
+ } catch (error) {
8880
+ console.error(
8881
+ `[CanvasService] render producer "${entry.toolId}" failed.`,
8882
+ error
8883
+ );
8884
+ }
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
+ }
8894
+ }
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));
8907
+ }
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 });
8913
+ }
8914
+ this.managedProducerLayerIds = nextLayerIds;
8915
+ this.managedProducerRootLayerIds = nextRootLayerIds;
8916
+ this.requestRenderAll();
8917
+ }
7915
8918
  /**
7916
8919
  * Get a layer (Group) by its ID.
7917
8920
  * We assume layers are Groups directly on the canvas with a data.id property.
@@ -7934,7 +8937,7 @@ var CanvasService = class {
7934
8937
  ...options,
7935
8938
  data: { ...options.data, id }
7936
8939
  };
7937
- layer = new Group3([], defaultOptions);
8940
+ layer = new Group([], defaultOptions);
7938
8941
  this.canvas.add(layer);
7939
8942
  }
7940
8943
  return layer;
@@ -7966,13 +8969,206 @@ var CanvasService = class {
7966
8969
  (_a = this.eventBus) == null ? void 0 : _a.emit("canvas:resized", { width, height });
7967
8970
  this.requestRenderAll();
7968
8971
  }
8972
+ getSceneScale() {
8973
+ const scale = Number(this.viewport.scale);
8974
+ return Number.isFinite(scale) && scale > 0 ? scale : 1;
8975
+ }
8976
+ getSceneOffset() {
8977
+ const offset = this.viewport.offset;
8978
+ const x = Number(offset.x);
8979
+ const y = Number(offset.y);
8980
+ return {
8981
+ x: Number.isFinite(x) ? x : 0,
8982
+ y: Number.isFinite(y) ? y : 0
8983
+ };
8984
+ }
8985
+ toScreenPoint(point) {
8986
+ const scale = this.getSceneScale();
8987
+ const offset = this.getSceneOffset();
8988
+ return {
8989
+ x: point.x * scale + offset.x,
8990
+ y: point.y * scale + offset.y
8991
+ };
8992
+ }
8993
+ toScenePoint(point) {
8994
+ const scale = this.getSceneScale();
8995
+ const offset = this.getSceneOffset();
8996
+ return {
8997
+ x: (point.x - offset.x) / scale,
8998
+ y: (point.y - offset.y) / scale
8999
+ };
9000
+ }
9001
+ toScreenLength(value) {
9002
+ return value * this.getSceneScale();
9003
+ }
9004
+ toSceneLength(value) {
9005
+ return value / this.getSceneScale();
9006
+ }
9007
+ toScreenRect(rect) {
9008
+ const start = this.toScreenPoint({ x: rect.left, y: rect.top });
9009
+ return {
9010
+ left: start.x,
9011
+ top: start.y,
9012
+ width: this.toScreenLength(rect.width),
9013
+ height: this.toScreenLength(rect.height)
9014
+ };
9015
+ }
9016
+ toSceneRect(rect) {
9017
+ const start = this.toScenePoint({ x: rect.left, y: rect.top });
9018
+ return {
9019
+ left: start.x,
9020
+ top: start.y,
9021
+ width: this.toSceneLength(rect.width),
9022
+ height: this.toSceneLength(rect.height)
9023
+ };
9024
+ }
9025
+ getSceneViewportRect() {
9026
+ const width = Number(this.canvas.width || 0);
9027
+ const height = Number(this.canvas.height || 0);
9028
+ return this.toSceneRect({ left: 0, top: 0, width, height });
9029
+ }
9030
+ getScreenViewportRect() {
9031
+ return {
9032
+ left: 0,
9033
+ top: 0,
9034
+ width: Number(this.canvas.width || 0),
9035
+ height: Number(this.canvas.height || 0)
9036
+ };
9037
+ }
9038
+ toSpaceRect(rect, from, to) {
9039
+ if (from === to) return { ...rect };
9040
+ if (from === "scene") {
9041
+ return this.toScreenRect(rect);
9042
+ }
9043
+ return this.toSceneRect(rect);
9044
+ }
9045
+ resolveLayoutLength(value, base) {
9046
+ if (typeof value === "number") {
9047
+ return Number.isFinite(value) ? value : void 0;
9048
+ }
9049
+ if (typeof value !== "string") {
9050
+ return void 0;
9051
+ }
9052
+ const raw = value.trim();
9053
+ if (!raw) return void 0;
9054
+ if (raw.endsWith("%")) {
9055
+ const percent = parseFloat(raw.slice(0, -1));
9056
+ if (!Number.isFinite(percent)) return void 0;
9057
+ return base * percent / 100;
9058
+ }
9059
+ const parsed = parseFloat(raw);
9060
+ return Number.isFinite(parsed) ? parsed : void 0;
9061
+ }
9062
+ resolveLayoutInsets(inset, reference) {
9063
+ var _a, _b, _c, _d, _e;
9064
+ if (typeof inset === "number" || typeof inset === "string") {
9065
+ const all = (_a = this.resolveLayoutLength(
9066
+ inset,
9067
+ Math.min(reference.width, reference.height)
9068
+ )) != null ? _a : 0;
9069
+ return { top: all, right: all, bottom: all, left: all };
9070
+ }
9071
+ const source = inset || {};
9072
+ const top = (_b = this.resolveLayoutLength(source.top, reference.height)) != null ? _b : 0;
9073
+ const right = (_c = this.resolveLayoutLength(source.right, reference.width)) != null ? _c : 0;
9074
+ const bottom = (_d = this.resolveLayoutLength(source.bottom, reference.height)) != null ? _d : 0;
9075
+ const left = (_e = this.resolveLayoutLength(source.left, reference.width)) != null ? _e : 0;
9076
+ return { top, right, bottom, left };
9077
+ }
9078
+ resolveLayoutReferenceRect(layout, space) {
9079
+ if (layout.referenceRect) {
9080
+ const sourceSpace = layout.referenceRect.space || space;
9081
+ return this.toSpaceRect(layout.referenceRect, sourceSpace, space);
9082
+ }
9083
+ const reference = layout.reference || "sceneViewport";
9084
+ if (reference === "screenViewport") {
9085
+ const screenRect = this.getScreenViewportRect();
9086
+ return space === "screen" ? screenRect : this.toSceneRect(screenRect);
9087
+ }
9088
+ const sceneRect = this.getSceneViewportRect();
9089
+ return space === "scene" ? sceneRect : this.toScreenRect(sceneRect);
9090
+ }
9091
+ alignFactor(value) {
9092
+ if (value === "end") return 1;
9093
+ if (value === "center") return 0.5;
9094
+ return 0;
9095
+ }
9096
+ normalizeOriginX(value) {
9097
+ if (value === "center") return "center";
9098
+ if (value === "right") return "right";
9099
+ return "left";
9100
+ }
9101
+ normalizeOriginY(value) {
9102
+ if (value === "center") return "center";
9103
+ if (value === "bottom") return "bottom";
9104
+ return "top";
9105
+ }
9106
+ originFactor(value) {
9107
+ if (value === "center") return 0.5;
9108
+ if (value === "right" || value === "bottom") return 1;
9109
+ return 0;
9110
+ }
9111
+ resolveLayoutProps(spec, props) {
9112
+ var _a, _b, _c, _d;
9113
+ const layout = spec.layout;
9114
+ if (!layout) {
9115
+ return { ...props };
9116
+ }
9117
+ const space = spec.space || "scene";
9118
+ const reference = this.resolveLayoutReferenceRect(layout, space);
9119
+ const inset = this.resolveLayoutInsets(layout.inset, reference);
9120
+ const area = {
9121
+ left: reference.left + inset.left,
9122
+ top: reference.top + inset.top,
9123
+ width: Math.max(0, reference.width - inset.left - inset.right),
9124
+ height: Math.max(0, reference.height - inset.top - inset.bottom)
9125
+ };
9126
+ const next = { ...props };
9127
+ const width = (_a = this.resolveLayoutLength(layout.width, area.width)) != null ? _a : Number.isFinite(next.width) ? Number(next.width) : void 0;
9128
+ const height = (_b = this.resolveLayoutLength(layout.height, area.height)) != null ? _b : Number.isFinite(next.height) ? Number(next.height) : void 0;
9129
+ if (width !== void 0) next.width = width;
9130
+ if (height !== void 0) next.height = height;
9131
+ const alignX = this.alignFactor(layout.alignX);
9132
+ const alignY = this.alignFactor(layout.alignY);
9133
+ const offsetX = (_c = this.resolveLayoutLength(layout.offsetX, area.width)) != null ? _c : 0;
9134
+ const offsetY = (_d = this.resolveLayoutLength(layout.offsetY, area.height)) != null ? _d : 0;
9135
+ const objectWidth = Number.isFinite(next.width) ? Number(next.width) : 0;
9136
+ const objectHeight = Number.isFinite(next.height) ? Number(next.height) : 0;
9137
+ const objectLeft = area.left + (area.width - objectWidth) * alignX + offsetX;
9138
+ const objectTop = area.top + (area.height - objectHeight) * alignY + offsetY;
9139
+ const originX = this.normalizeOriginX(next.originX);
9140
+ const originY = this.normalizeOriginY(next.originY);
9141
+ next.left = objectLeft + objectWidth * this.originFactor(originX);
9142
+ next.top = objectTop + objectHeight * this.originFactor(originY);
9143
+ return next;
9144
+ }
9145
+ setLayerVisibility(layerId, visible) {
9146
+ const layer = this.getLayer(layerId);
9147
+ if (layer) {
9148
+ layer.set({ visible });
9149
+ }
9150
+ const objects = this.getRootLayerObjects(layerId);
9151
+ objects.forEach((obj) => {
9152
+ var _a, _b;
9153
+ (_a = obj.set) == null ? void 0 : _a.call(obj, { visible });
9154
+ (_b = obj.setCoords) == null ? void 0 : _b.call(obj);
9155
+ });
9156
+ }
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));
9164
+ }
7969
9165
  async applyLayerSpec(spec) {
7970
9166
  const layer = this.createLayer(spec.id, spec.props || {});
7971
9167
  await this.applyObjectSpecsToContainer(layer, spec.objects);
7972
9168
  }
7973
- async applyObjectSpecsToLayer(layerId, objects) {
9169
+ async applyObjectSpecsToLayer(layerId, objects, options = {}) {
7974
9170
  const layer = this.createLayer(layerId, {});
7975
- await this.applyObjectSpecsToContainer(layer, objects);
9171
+ await this.applyObjectSpecsToContainer(layer, objects, options);
7976
9172
  }
7977
9173
  getRootLayerObjects(layerId) {
7978
9174
  return this.canvas.getObjects().filter((obj) => {
@@ -7980,7 +9176,7 @@ var CanvasService = class {
7980
9176
  return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === layerId;
7981
9177
  });
7982
9178
  }
7983
- async applyObjectSpecsToRootLayer(layerId, specs) {
9179
+ async applyObjectSpecsToRootLayer(layerId, specs, options = {}) {
7984
9180
  const desiredIds = new Set(specs.map((s) => s.id));
7985
9181
  const existing = this.getRootLayerObjects(layerId);
7986
9182
  existing.forEach((obj) => {
@@ -8014,9 +9210,11 @@ var CanvasService = class {
8014
9210
  }
8015
9211
  this.patchFabricObject(current, spec, { layerId });
8016
9212
  }
8017
- this.requestRenderAll();
9213
+ if (options.render !== false) {
9214
+ this.requestRenderAll();
9215
+ }
8018
9216
  }
8019
- async applyObjectSpecsToContainer(container, specs) {
9217
+ async applyObjectSpecsToContainer(container, specs, options = {}) {
8020
9218
  const desiredIds = new Set(specs.map((s) => s.id));
8021
9219
  const existing = container.getObjects();
8022
9220
  existing.forEach((obj) => {
@@ -8052,7 +9250,9 @@ var CanvasService = class {
8052
9250
  this.moveObjectInContainer(container, current, index);
8053
9251
  }
8054
9252
  container.dirty = true;
8055
- this.requestRenderAll();
9253
+ if (options.render !== false) {
9254
+ this.requestRenderAll();
9255
+ }
8056
9256
  }
8057
9257
  patchFabricObject(obj, spec, extraData) {
8058
9258
  const nextData = {
@@ -8061,9 +9261,33 @@ var CanvasService = class {
8061
9261
  ...extraData || {},
8062
9262
  id: spec.id
8063
9263
  };
8064
- obj.set({ ...spec.props || {}, data: nextData });
9264
+ const props = this.resolveFabricProps(spec, spec.props || {});
9265
+ obj.set({ ...props, data: nextData });
8065
9266
  obj.setCoords();
8066
9267
  }
9268
+ resolveFabricProps(spec, props) {
9269
+ const space = spec.space || "scene";
9270
+ const next = this.resolveLayoutProps(spec, props);
9271
+ if (space === "screen") {
9272
+ return next;
9273
+ }
9274
+ const hasLeft = Number.isFinite(next.left);
9275
+ const hasTop = Number.isFinite(next.top);
9276
+ if (hasLeft || hasTop) {
9277
+ const mapped = this.toScreenPoint({
9278
+ x: hasLeft ? Number(next.left) : 0,
9279
+ y: hasTop ? Number(next.top) : 0
9280
+ });
9281
+ if (hasLeft) next.left = mapped.x;
9282
+ if (hasTop) next.top = mapped.y;
9283
+ }
9284
+ const rawScaleX = Number.isFinite(next.scaleX) ? Number(next.scaleX) : 1;
9285
+ const rawScaleY = Number.isFinite(next.scaleY) ? Number(next.scaleY) : 1;
9286
+ const sceneScale = this.getSceneScale();
9287
+ next.scaleX = rawScaleX * sceneScale;
9288
+ next.scaleY = rawScaleY * sceneScale;
9289
+ return next;
9290
+ }
8067
9291
  moveObjectInContainer(container, obj, index) {
8068
9292
  if (!obj) return;
8069
9293
  const moveObjectTo = container.moveObjectTo;
@@ -8083,10 +9307,11 @@ var CanvasService = class {
8083
9307
  }
8084
9308
  }
8085
9309
  async createFabricObject(spec) {
8086
- var _a, _b;
9310
+ var _a, _b, _c, _d;
8087
9311
  if (spec.type === "rect") {
8088
- const rect = new Rect4({
8089
- ...spec.props || {},
9312
+ const props = this.resolveFabricProps(spec, spec.props || {});
9313
+ const rect = new Rect({
9314
+ ...props,
8090
9315
  data: { ...spec.data || {}, id: spec.id }
8091
9316
  });
8092
9317
  rect.setCoords();
@@ -8095,8 +9320,9 @@ var CanvasService = class {
8095
9320
  if (spec.type === "path") {
8096
9321
  const pathData = ((_a = spec.props) == null ? void 0 : _a.path) || ((_b = spec.props) == null ? void 0 : _b.pathData);
8097
9322
  if (!pathData) return void 0;
9323
+ const props = this.resolveFabricProps(spec, spec.props || {});
8098
9324
  const path = new Path2(pathData, {
8099
- ...spec.props || {},
9325
+ ...props,
8100
9326
  data: { ...spec.data || {}, id: spec.id }
8101
9327
  });
8102
9328
  path.setCoords();
@@ -8104,14 +9330,25 @@ var CanvasService = class {
8104
9330
  }
8105
9331
  if (spec.type === "image") {
8106
9332
  if (!spec.src) return void 0;
8107
- const image = await Image4.fromURL(spec.src, { crossOrigin: "anonymous" });
9333
+ const image = await Image2.fromURL(spec.src, { crossOrigin: "anonymous" });
9334
+ const props = this.resolveFabricProps(spec, spec.props || {});
8108
9335
  image.set({
8109
- ...spec.props || {},
9336
+ ...props,
8110
9337
  data: { ...spec.data || {}, id: spec.id }
8111
9338
  });
8112
9339
  image.setCoords();
8113
9340
  return image;
8114
9341
  }
9342
+ if (spec.type === "text") {
9343
+ const content = String((_d = (_c = spec.props) == null ? void 0 : _c.text) != null ? _d : "");
9344
+ const props = this.resolveFabricProps(spec, spec.props || {});
9345
+ const text = new Text(content, {
9346
+ ...props,
9347
+ data: { ...spec.data || {}, id: spec.id }
9348
+ });
9349
+ text.setCoords();
9350
+ return text;
9351
+ }
8115
9352
  return void 0;
8116
9353
  }
8117
9354
  };