@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.js CHANGED
@@ -49,6 +49,11 @@ module.exports = __toCommonJS(index_exports);
49
49
  // src/extensions/background.ts
50
50
  var import_core = require("@pooder/core");
51
51
  var import_fabric = require("fabric");
52
+ var BACKGROUND_LAYER_ID = "background";
53
+ var BACKGROUND_RECT_ID = "background-color-rect";
54
+ var BACKGROUND_IMAGE_ID = "background-image";
55
+ var DEFAULT_WIDTH = 800;
56
+ var DEFAULT_HEIGHT = 600;
52
57
  var BackgroundTool = class {
53
58
  constructor(options) {
54
59
  this.id = "pooder.kit.background";
@@ -57,17 +62,38 @@ var BackgroundTool = class {
57
62
  };
58
63
  this.color = "";
59
64
  this.url = "";
65
+ this.specs = [];
66
+ this.renderSeq = 0;
67
+ this.renderImageUrl = "";
68
+ this.sourceSizeBySrc = /* @__PURE__ */ new Map();
69
+ this.pendingSizeBySrc = /* @__PURE__ */ new Map();
70
+ this.onCanvasResized = () => {
71
+ this.updateBackground();
72
+ };
60
73
  if (options) {
61
74
  Object.assign(this, options);
62
75
  }
63
76
  }
64
77
  activate(context) {
78
+ var _a;
65
79
  this.canvasService = context.services.get("CanvasService");
66
80
  if (!this.canvasService) {
67
81
  console.warn("CanvasService not found for BackgroundTool");
68
82
  return;
69
83
  }
70
- const configService = context.services.get("ConfigurationService");
84
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
85
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
86
+ this.id,
87
+ () => ({
88
+ layerSpecs: {
89
+ [BACKGROUND_LAYER_ID]: this.specs
90
+ }
91
+ }),
92
+ { priority: 0 }
93
+ );
94
+ const configService = context.services.get(
95
+ "ConfigurationService"
96
+ );
71
97
  if (configService) {
72
98
  this.color = configService.get("background.color", this.color);
73
99
  this.url = configService.get("background.url", this.url);
@@ -91,17 +117,25 @@ var BackgroundTool = class {
91
117
  }
92
118
  });
93
119
  }
94
- this.initLayer();
120
+ context.eventBus.on("canvas:resized", this.onCanvasResized);
95
121
  this.updateBackground();
96
122
  }
97
123
  deactivate(context) {
98
- if (this.canvasService) {
99
- const layer = this.canvasService.getLayer("background");
100
- if (layer) {
101
- this.canvasService.canvas.remove(layer);
102
- }
103
- this.canvasService = void 0;
124
+ var _a;
125
+ context.eventBus.off("canvas:resized", this.onCanvasResized);
126
+ this.renderSeq += 1;
127
+ this.specs = [];
128
+ this.renderImageUrl = "";
129
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
130
+ this.renderProducerDisposable = void 0;
131
+ if (!this.canvasService) return;
132
+ const layer = this.canvasService.getLayer(BACKGROUND_LAYER_ID);
133
+ if (layer) {
134
+ this.canvasService.canvas.remove(layer);
104
135
  }
136
+ void this.canvasService.flushRenderFromProducers();
137
+ this.canvasService.requestRenderAll();
138
+ this.canvasService = void 0;
105
139
  }
106
140
  contribute() {
107
141
  return {
@@ -161,88 +195,131 @@ var BackgroundTool = class {
161
195
  ]
162
196
  };
163
197
  }
164
- initLayer() {
165
- if (!this.canvasService) return;
166
- let backgroundLayer = this.canvasService.getLayer("background");
167
- if (!backgroundLayer) {
168
- backgroundLayer = this.canvasService.createLayer("background", {
169
- width: this.canvasService.canvas.width,
170
- height: this.canvasService.canvas.height,
171
- selectable: false,
172
- evented: false
173
- });
174
- this.canvasService.canvas.sendObjectToBack(backgroundLayer);
175
- }
198
+ getViewportSize() {
199
+ var _a, _b;
200
+ const width = Number(((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 0);
201
+ const height = Number(((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 0);
202
+ return {
203
+ width: width > 0 ? width : DEFAULT_WIDTH,
204
+ height: height > 0 ? height : DEFAULT_HEIGHT
205
+ };
176
206
  }
177
- async updateBackground() {
178
- if (!this.canvasService) return;
179
- const layer = this.canvasService.getLayer("background");
180
- if (!layer) {
181
- console.warn("[BackgroundTool] Background layer not found");
182
- return;
207
+ buildBackgroundSpecs(color, imageUrl) {
208
+ const { width, height } = this.getViewportSize();
209
+ const specs = [
210
+ {
211
+ id: BACKGROUND_RECT_ID,
212
+ type: "rect",
213
+ space: "screen",
214
+ data: {
215
+ id: BACKGROUND_RECT_ID,
216
+ layerId: BACKGROUND_LAYER_ID,
217
+ type: "background-color"
218
+ },
219
+ props: {
220
+ left: 0,
221
+ top: 0,
222
+ width,
223
+ height,
224
+ originX: "left",
225
+ originY: "top",
226
+ fill: color,
227
+ selectable: false,
228
+ evented: false,
229
+ excludeFromExport: true
230
+ }
231
+ }
232
+ ];
233
+ if (!imageUrl) {
234
+ return specs;
183
235
  }
184
- const { color, url } = this;
185
- const width = this.canvasService.canvas.width || 800;
186
- const height = this.canvasService.canvas.height || 600;
187
- let rect = this.canvasService.getObject(
188
- "background-color-rect",
189
- "background"
190
- );
191
- if (rect) {
192
- rect.set({
193
- fill: color
194
- });
195
- } else {
196
- rect = new import_fabric.Rect({
197
- width,
198
- height,
199
- fill: color,
236
+ const sourceSize = this.sourceSizeBySrc.get(imageUrl);
237
+ const sourceWidth = Math.max(1, Number((sourceSize == null ? void 0 : sourceSize.width) || width));
238
+ const sourceHeight = Math.max(1, Number((sourceSize == null ? void 0 : sourceSize.height) || height));
239
+ const coverScale = Math.max(width / sourceWidth, height / sourceHeight);
240
+ specs.push({
241
+ id: BACKGROUND_IMAGE_ID,
242
+ type: "image",
243
+ src: imageUrl,
244
+ space: "screen",
245
+ data: {
246
+ id: BACKGROUND_IMAGE_ID,
247
+ layerId: BACKGROUND_LAYER_ID,
248
+ type: "background-image"
249
+ },
250
+ props: {
251
+ left: 0,
252
+ top: 0,
253
+ originX: "left",
254
+ originY: "top",
255
+ scaleX: coverScale,
256
+ scaleY: coverScale,
200
257
  selectable: false,
201
258
  evented: false,
202
- data: {
203
- id: "background-color-rect"
204
- }
205
- });
206
- layer.add(rect);
207
- layer.sendObjectToBack(rect);
259
+ excludeFromExport: true
260
+ }
261
+ });
262
+ return specs;
263
+ }
264
+ async ensureImageSize(src) {
265
+ if (!src) return null;
266
+ const cached = this.sourceSizeBySrc.get(src);
267
+ if (cached) return cached;
268
+ const pending = this.pendingSizeBySrc.get(src);
269
+ if (pending) {
270
+ return pending;
208
271
  }
209
- let img = this.canvasService.getObject(
210
- "background-image",
211
- "background"
212
- );
272
+ const task = this.loadImageSize(src);
273
+ this.pendingSizeBySrc.set(src, task);
213
274
  try {
214
- if (img) {
215
- if (img.getSrc() !== url) {
216
- if (url) {
217
- await img.setSrc(url);
218
- } else {
219
- layer.remove(img);
220
- }
221
- }
222
- } else {
223
- if (url) {
224
- img = await import_fabric.FabricImage.fromURL(url, { crossOrigin: "anonymous" });
225
- img.set({
226
- originX: "left",
227
- originY: "top",
228
- left: 0,
229
- top: 0,
230
- selectable: false,
231
- evented: false,
232
- data: {
233
- id: "background-image"
234
- }
235
- });
236
- img.scaleToWidth(width);
237
- if (img.getScaledHeight() < height) img.scaleToHeight(height);
238
- layer.add(img);
239
- }
275
+ return await task;
276
+ } finally {
277
+ if (this.pendingSizeBySrc.get(src) === task) {
278
+ this.pendingSizeBySrc.delete(src);
240
279
  }
241
- this.canvasService.requestRenderAll();
242
- } catch (e) {
243
- console.error("[BackgroundTool] Failed to load image", e);
244
280
  }
245
- layer.dirty = true;
281
+ }
282
+ async loadImageSize(src) {
283
+ try {
284
+ const image = await import_fabric.FabricImage.fromURL(src, {
285
+ crossOrigin: "anonymous"
286
+ });
287
+ const width = Number((image == null ? void 0 : image.width) || 0);
288
+ const height = Number((image == null ? void 0 : image.height) || 0);
289
+ if (width > 0 && height > 0) {
290
+ const size = { width, height };
291
+ this.sourceSizeBySrc.set(src, size);
292
+ return size;
293
+ }
294
+ } catch (error) {
295
+ console.error("[BackgroundTool] Failed to load image", src, error);
296
+ }
297
+ return null;
298
+ }
299
+ updateBackground() {
300
+ void this.updateBackgroundAsync();
301
+ }
302
+ async updateBackgroundAsync() {
303
+ if (!this.canvasService) return;
304
+ const seq = ++this.renderSeq;
305
+ const color = this.color;
306
+ const nextUrl = String(this.url || "").trim();
307
+ if (!nextUrl) {
308
+ this.renderImageUrl = "";
309
+ } else if (nextUrl !== this.renderImageUrl) {
310
+ const loaded = await this.ensureImageSize(nextUrl);
311
+ if (seq !== this.renderSeq) return;
312
+ if (loaded) {
313
+ this.renderImageUrl = nextUrl;
314
+ }
315
+ }
316
+ this.specs = this.buildBackgroundSpecs(color, this.renderImageUrl);
317
+ await this.canvasService.flushRenderFromProducers();
318
+ if (seq !== this.renderSeq) return;
319
+ const layer = this.canvasService.getLayer(BACKGROUND_LAYER_ID);
320
+ if (layer) {
321
+ this.canvasService.canvas.sendObjectToBack(layer);
322
+ }
246
323
  this.canvasService.requestRenderAll();
247
324
  }
248
325
  };
@@ -251,6 +328,71 @@ var BackgroundTool = class {
251
328
  var import_core2 = require("@pooder/core");
252
329
  var import_fabric2 = require("fabric");
253
330
 
331
+ // src/extensions/dielineShape.ts
332
+ var BUILTIN_DIELINE_SHAPES = [
333
+ "rect",
334
+ "circle",
335
+ "ellipse",
336
+ "heart"
337
+ ];
338
+ var DIELINE_SHAPES = [...BUILTIN_DIELINE_SHAPES, "custom"];
339
+ var DEFAULT_DIELINE_SHAPE = "rect";
340
+ var DEFAULT_HEART_SHAPE_PARAMS = {
341
+ lobeSpread: 0.46,
342
+ notchDepth: 0.24,
343
+ tipSharpness: 0
344
+ };
345
+ var DEFAULT_DIELINE_SHAPE_STYLE = {
346
+ fitMode: "contain",
347
+ ...DEFAULT_HEART_SHAPE_PARAMS
348
+ };
349
+ function isDielineShape(value) {
350
+ return typeof value === "string" && DIELINE_SHAPES.includes(value);
351
+ }
352
+ function normalizeFitMode(value, fallback) {
353
+ if (value === "contain" || value === "stretch") return value;
354
+ return fallback;
355
+ }
356
+ function normalizeUnitInterval(value, fallback) {
357
+ const num = Number(value);
358
+ if (!Number.isFinite(num)) return fallback;
359
+ return Math.max(0, Math.min(1, num));
360
+ }
361
+ function normalizeDielineShape(value, fallback = DEFAULT_DIELINE_SHAPE) {
362
+ return isDielineShape(value) ? value : fallback;
363
+ }
364
+ function normalizeShapeStyle(value, fallback = DEFAULT_DIELINE_SHAPE_STYLE) {
365
+ var _a, _b, _c;
366
+ const raw = value && typeof value === "object" ? value : {};
367
+ return {
368
+ ...fallback,
369
+ fitMode: normalizeFitMode(raw.fitMode, fallback.fitMode),
370
+ lobeSpread: normalizeUnitInterval(
371
+ raw.lobeSpread,
372
+ Number((_a = fallback.lobeSpread) != null ? _a : DEFAULT_HEART_SHAPE_PARAMS.lobeSpread)
373
+ ),
374
+ notchDepth: normalizeUnitInterval(
375
+ raw.notchDepth,
376
+ Number((_b = fallback.notchDepth) != null ? _b : DEFAULT_HEART_SHAPE_PARAMS.notchDepth)
377
+ ),
378
+ tipSharpness: normalizeUnitInterval(
379
+ raw.tipSharpness,
380
+ Number((_c = fallback.tipSharpness) != null ? _c : DEFAULT_HEART_SHAPE_PARAMS.tipSharpness)
381
+ )
382
+ };
383
+ }
384
+ function getShapeFitMode(style) {
385
+ return normalizeShapeStyle(style).fitMode;
386
+ }
387
+ function getHeartShapeParams(style) {
388
+ const normalized = normalizeShapeStyle(style);
389
+ return {
390
+ lobeSpread: Number(normalized.lobeSpread),
391
+ notchDepth: Number(normalized.notchDepth),
392
+ tipSharpness: Number(normalized.tipSharpness)
393
+ };
394
+ }
395
+
254
396
  // src/extensions/geometry.ts
255
397
  var import_paper = __toESM(require("paper"));
256
398
 
@@ -393,66 +535,164 @@ function selectOuterChain(args) {
393
535
  if (scoreA !== scoreB) return scoreA > scoreB ? pointsA : pointsB;
394
536
  return pointsA.length <= pointsB.length ? pointsA : pointsB;
395
537
  }
396
- function createBaseShape(options) {
397
- var _a;
398
- const {
399
- shape,
400
- width,
401
- height,
402
- radius,
403
- x,
404
- y,
405
- pathData,
406
- customSourceWidthPx,
407
- customSourceHeightPx
408
- } = options;
409
- const center = new import_paper.default.Point(x, y);
410
- if (shape === "rect") {
538
+ function fitPathItemToRect(item, rect, fitMode) {
539
+ const { left, top, width, height } = rect;
540
+ const bounds = item.bounds;
541
+ if (width <= 0 || height <= 0 || !Number.isFinite(bounds.width) || !Number.isFinite(bounds.height) || bounds.width <= 0 || bounds.height <= 0) {
542
+ item.position = new import_paper.default.Point(left + width / 2, top + height / 2);
543
+ return item;
544
+ }
545
+ item.translate(new import_paper.default.Point(-bounds.left, -bounds.top));
546
+ if (fitMode === "stretch") {
547
+ item.scale(width / bounds.width, height / bounds.height, new import_paper.default.Point(0, 0));
548
+ item.translate(new import_paper.default.Point(left, top));
549
+ return item;
550
+ }
551
+ const uniformScale = Math.min(width / bounds.width, height / bounds.height);
552
+ item.scale(uniformScale, uniformScale, new import_paper.default.Point(0, 0));
553
+ const scaledWidth = bounds.width * uniformScale;
554
+ const scaledHeight = bounds.height * uniformScale;
555
+ item.translate(
556
+ new import_paper.default.Point(
557
+ left + (width - scaledWidth) / 2,
558
+ top + (height - scaledHeight) / 2
559
+ )
560
+ );
561
+ return item;
562
+ }
563
+ function createNormalizedHeartPath(params) {
564
+ const { lobeSpread, notchDepth, tipSharpness } = params;
565
+ const halfSpread = 0.22 + lobeSpread * 0.18;
566
+ const notchY = 0.06 + notchDepth * 0.2;
567
+ const shoulderY = 0.24 + notchDepth * 0.2;
568
+ const topLift = 0.12 + (1 - notchDepth) * 0.06;
569
+ const topY = notchY - topLift;
570
+ const sideCtrlY = shoulderY - (0.18 - notchDepth * 0.08);
571
+ const lowerCtrlY = 0.58 + (1 - tipSharpness) * 0.16;
572
+ const tipCtrlX = 0.34 - tipSharpness * 0.2;
573
+ const notchCtrlX = 0.06 + lobeSpread * 0.06;
574
+ const lobeCtrlX = 0.1 + lobeSpread * 0.08;
575
+ const notchCtrlY = notchY - topLift * 0.45;
576
+ const xPeakL = 0.5 - halfSpread;
577
+ const xPeakR = 0.5 + halfSpread;
578
+ const heartPath = new import_paper.default.Path({ insert: false });
579
+ heartPath.moveTo(new import_paper.default.Point(0.5, notchY));
580
+ heartPath.cubicCurveTo(
581
+ new import_paper.default.Point(0.5 - notchCtrlX, notchCtrlY),
582
+ new import_paper.default.Point(xPeakL + lobeCtrlX, topY),
583
+ new import_paper.default.Point(xPeakL, topY)
584
+ );
585
+ heartPath.cubicCurveTo(
586
+ new import_paper.default.Point(xPeakL - lobeCtrlX, topY),
587
+ new import_paper.default.Point(0, sideCtrlY),
588
+ new import_paper.default.Point(0, shoulderY)
589
+ );
590
+ heartPath.cubicCurveTo(
591
+ new import_paper.default.Point(0, lowerCtrlY),
592
+ new import_paper.default.Point(tipCtrlX, 1),
593
+ new import_paper.default.Point(0.5, 1)
594
+ );
595
+ heartPath.cubicCurveTo(
596
+ new import_paper.default.Point(1 - tipCtrlX, 1),
597
+ new import_paper.default.Point(1, lowerCtrlY),
598
+ new import_paper.default.Point(1, shoulderY)
599
+ );
600
+ heartPath.cubicCurveTo(
601
+ new import_paper.default.Point(1, sideCtrlY),
602
+ new import_paper.default.Point(xPeakR + lobeCtrlX, topY),
603
+ new import_paper.default.Point(xPeakR, topY)
604
+ );
605
+ heartPath.cubicCurveTo(
606
+ new import_paper.default.Point(xPeakR - lobeCtrlX, topY),
607
+ new import_paper.default.Point(0.5 + notchCtrlX, notchCtrlY),
608
+ new import_paper.default.Point(0.5, notchY)
609
+ );
610
+ heartPath.closed = true;
611
+ return heartPath;
612
+ }
613
+ function createHeartBaseShape(options) {
614
+ const { x, y, width, height } = options;
615
+ const w = Math.max(0, width);
616
+ const h = Math.max(0, height);
617
+ const left = x - w / 2;
618
+ const top = y - h / 2;
619
+ const fitMode = getShapeFitMode(options.shapeStyle);
620
+ const heartParams = getHeartShapeParams(options.shapeStyle);
621
+ const rawHeart = createNormalizedHeartPath(heartParams);
622
+ return fitPathItemToRect(rawHeart, { left, top, width: w, height: h }, fitMode);
623
+ }
624
+ var BUILTIN_SHAPE_BUILDERS = {
625
+ rect: (options) => {
626
+ const { x, y, width, height, radius } = options;
411
627
  return new import_paper.default.Path.Rectangle({
412
628
  point: [x - width / 2, y - height / 2],
413
629
  size: [Math.max(0, width), Math.max(0, height)],
414
630
  radius: Math.max(0, radius)
415
631
  });
416
- } else if (shape === "circle") {
632
+ },
633
+ circle: (options) => {
634
+ const { x, y, width, height } = options;
417
635
  const r = Math.min(width, height) / 2;
418
636
  return new import_paper.default.Path.Circle({
419
- center,
637
+ center: new import_paper.default.Point(x, y),
420
638
  radius: Math.max(0, r)
421
639
  });
422
- } else if (shape === "ellipse") {
640
+ },
641
+ ellipse: (options) => {
642
+ const { x, y, width, height } = options;
423
643
  return new import_paper.default.Path.Ellipse({
424
- center,
644
+ center: new import_paper.default.Point(x, y),
425
645
  radius: [Math.max(0, width / 2), Math.max(0, height / 2)]
426
646
  });
427
- } else if (shape === "custom" && pathData) {
428
- const hasMultipleSubPaths = ((_a = (pathData.match(/[Mm]/g) || []).length) != null ? _a : 0) > 1;
429
- const path = hasMultipleSubPaths ? new import_paper.default.CompoundPath(pathData) : (() => {
430
- const single = new import_paper.default.Path();
431
- single.pathData = pathData;
432
- return single;
433
- })();
434
- const sourceWidth = Number(customSourceWidthPx != null ? customSourceWidthPx : 0);
435
- const sourceHeight = Number(customSourceHeightPx != null ? customSourceHeightPx : 0);
436
- if (Number.isFinite(sourceWidth) && Number.isFinite(sourceHeight) && sourceWidth > 0 && sourceHeight > 0 && width > 0 && height > 0) {
437
- const targetLeft = x - width / 2;
438
- const targetTop = y - height / 2;
439
- path.scale(width / sourceWidth, height / sourceHeight, new import_paper.default.Point(0, 0));
440
- path.translate(new import_paper.default.Point(targetLeft, targetTop));
441
- return path;
442
- }
443
- if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
444
- path.position = center;
445
- path.scale(width / path.bounds.width, height / path.bounds.height);
446
- return path;
447
- }
647
+ },
648
+ heart: createHeartBaseShape
649
+ };
650
+ function createCustomBaseShape(options) {
651
+ var _a;
652
+ const {
653
+ pathData,
654
+ customSourceWidthPx,
655
+ customSourceHeightPx,
656
+ x,
657
+ y,
658
+ width,
659
+ height
660
+ } = options;
661
+ if (typeof pathData !== "string" || pathData.trim().length === 0) {
662
+ return null;
663
+ }
664
+ const center = new import_paper.default.Point(x, y);
665
+ const hasMultipleSubPaths = ((_a = (pathData.match(/[Mm]/g) || []).length) != null ? _a : 0) > 1;
666
+ const path = hasMultipleSubPaths ? new import_paper.default.CompoundPath(pathData) : (() => {
667
+ const single = new import_paper.default.Path();
668
+ single.pathData = pathData;
669
+ return single;
670
+ })();
671
+ const sourceWidth = Number(customSourceWidthPx != null ? customSourceWidthPx : 0);
672
+ const sourceHeight = Number(customSourceHeightPx != null ? customSourceHeightPx : 0);
673
+ if (Number.isFinite(sourceWidth) && Number.isFinite(sourceHeight) && sourceWidth > 0 && sourceHeight > 0 && width > 0 && height > 0) {
674
+ const targetLeft = x - width / 2;
675
+ const targetTop = y - height / 2;
676
+ path.scale(width / sourceWidth, height / sourceHeight, new import_paper.default.Point(0, 0));
677
+ path.translate(new import_paper.default.Point(targetLeft, targetTop));
678
+ return path;
679
+ }
680
+ if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
448
681
  path.position = center;
682
+ path.scale(width / path.bounds.width, height / path.bounds.height);
449
683
  return path;
450
- } else {
451
- return new import_paper.default.Path.Rectangle({
452
- point: [x - width / 2, y - height / 2],
453
- size: [Math.max(0, width), Math.max(0, height)]
454
- });
455
684
  }
685
+ path.position = center;
686
+ return path;
687
+ }
688
+ function createBaseShape(options) {
689
+ const { shape } = options;
690
+ if (shape === "custom") {
691
+ const customShape = createCustomBaseShape(options);
692
+ if (customShape) return customShape;
693
+ return BUILTIN_SHAPE_BUILDERS[DEFAULT_DIELINE_SHAPE](options);
694
+ }
695
+ return BUILTIN_SHAPE_BUILDERS[shape](options);
456
696
  }
457
697
  function resolveBridgeBasePath(shape, anchor) {
458
698
  if (shape instanceof import_paper.default.Path) {
@@ -776,6 +1016,18 @@ function getNearestPointOnDieline(point, options) {
776
1016
  shape.remove();
777
1017
  return result;
778
1018
  }
1019
+ function getPathBounds(pathData) {
1020
+ const path = new import_paper.default.Path();
1021
+ path.pathData = pathData;
1022
+ const bounds = path.bounds;
1023
+ path.remove();
1024
+ return {
1025
+ x: bounds.x,
1026
+ y: bounds.y,
1027
+ width: bounds.width,
1028
+ height: bounds.height
1029
+ };
1030
+ }
779
1031
 
780
1032
  // src/coordinate.ts
781
1033
  var Coordinate = class {
@@ -864,12 +1116,6 @@ function parseLengthToMm(input, defaultUnit) {
864
1116
  const unit = (_b = (_a = match[2]) == null ? void 0 : _a.toLowerCase()) != null ? _b : defaultUnit;
865
1117
  return Coordinate.convertUnit(value, unit, "mm");
866
1118
  }
867
- function formatMm(valueMm, displayUnit, fractionDigits = 2) {
868
- if (!Number.isFinite(valueMm)) return "0";
869
- const value = Coordinate.convertUnit(valueMm, "mm", displayUnit);
870
- const rounded = Number(value.toFixed(fractionDigits));
871
- return rounded.toString();
872
- }
873
1119
 
874
1120
  // src/extensions/sceneLayoutModel.ts
875
1121
  var DEFAULT_SIZE_STATE = {
@@ -1084,10 +1330,15 @@ function buildSceneGeometry(configService, layout) {
1084
1330
  const sourceHeight = Number(
1085
1331
  configService.get("dieline.customSourceHeightPx", 0)
1086
1332
  );
1333
+ const shapeStyle = normalizeShapeStyle(
1334
+ configService.get("dieline.shapeStyle", DEFAULT_DIELINE_SHAPE_STYLE)
1335
+ );
1087
1336
  return {
1088
- shape: configService.get("dieline.shape", "rect"),
1089
- unit: "mm",
1090
- displayUnit: normalizeUnit(configService.get("size.unit", "mm")),
1337
+ shape: normalizeDielineShape(
1338
+ configService.get("dieline.shape", DEFAULT_DIELINE_SHAPE)
1339
+ ),
1340
+ shapeStyle,
1341
+ unit: "px",
1091
1342
  x: layout.trimRect.centerX,
1092
1343
  y: layout.trimRect.centerY,
1093
1344
  width: layout.trimRect.width,
@@ -1120,6 +1371,7 @@ var ImageTool = class {
1120
1371
  this.isImageSelectionActive = false;
1121
1372
  this.focusedImageId = null;
1122
1373
  this.renderSeq = 0;
1374
+ this.overlaySpecs = [];
1123
1375
  this.onToolActivated = (event) => {
1124
1376
  const before = this.isToolActive;
1125
1377
  this.syncToolActiveFromWorkbench(event.id);
@@ -1197,28 +1449,41 @@ var ImageTool = class {
1197
1449
  const frame = this.getFrameRect();
1198
1450
  if (!frame.width || !frame.height) return;
1199
1451
  const center = target.getCenterPoint ? target.getCenterPoint() : new import_fabric2.Point((_c = target.left) != null ? _c : 0, (_d = target.top) != null ? _d : 0);
1452
+ const centerScene = this.canvasService ? this.canvasService.toScenePoint({ x: center.x, y: center.y }) : { x: center.x, y: center.y };
1200
1453
  const objectScale = Number.isFinite(target == null ? void 0 : target.scaleX) ? target.scaleX : 1;
1454
+ const objectScaleScene = this.toSceneObjectScale(objectScale || 1);
1201
1455
  const workingItem = this.workingItems.find((item) => item.id === id);
1202
1456
  const sourceKey = (workingItem == null ? void 0 : workingItem.sourceUrl) || (workingItem == null ? void 0 : workingItem.url) || "";
1203
1457
  const sourceSize = this.getSourceSize(sourceKey, target);
1204
1458
  const coverScale = this.getCoverScale(frame, sourceSize);
1205
1459
  const updates = {
1206
- left: this.clampNormalized((center.x - frame.left) / frame.width),
1207
- top: this.clampNormalized((center.y - frame.top) / frame.height),
1460
+ left: this.clampNormalized((centerScene.x - frame.left) / frame.width),
1461
+ top: this.clampNormalized((centerScene.y - frame.top) / frame.height),
1208
1462
  angle: Number.isFinite(target.angle) ? target.angle : 0,
1209
- scale: Math.max(0.05, (objectScale || 1) / coverScale)
1463
+ scale: Math.max(0.05, objectScaleScene / coverScale)
1210
1464
  };
1211
1465
  this.focusedImageId = id;
1212
1466
  this.updateImageInWorking(id, updates);
1213
1467
  };
1214
1468
  }
1215
1469
  activate(context) {
1470
+ var _a;
1216
1471
  this.context = context;
1217
1472
  this.canvasService = context.services.get("CanvasService");
1218
1473
  if (!this.canvasService) {
1219
1474
  console.warn("CanvasService not found for ImageTool");
1220
1475
  return;
1221
1476
  }
1477
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
1478
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
1479
+ this.id,
1480
+ () => ({
1481
+ rootLayerSpecs: {
1482
+ [IMAGE_OVERLAY_LAYER_ID]: this.overlaySpecs
1483
+ }
1484
+ }),
1485
+ { priority: 300 }
1486
+ );
1222
1487
  context.eventBus.on("tool:activated", this.onToolActivated);
1223
1488
  context.eventBus.on("object:modified", this.onObjectModified);
1224
1489
  context.eventBus.on("selection:created", this.onSelectionChanged);
@@ -1259,7 +1524,7 @@ var ImageTool = class {
1259
1524
  this.updateImages();
1260
1525
  }
1261
1526
  deactivate(context) {
1262
- var _a;
1527
+ var _a, _b;
1263
1528
  context.eventBus.off("tool:activated", this.onToolActivated);
1264
1529
  context.eventBus.off("object:modified", this.onObjectModified);
1265
1530
  context.eventBus.off("selection:created", this.onSelectionChanged);
@@ -1271,12 +1536,13 @@ var ImageTool = class {
1271
1536
  this.dirtyTrackerDisposable = void 0;
1272
1537
  this.cropShapeHatchPattern = void 0;
1273
1538
  this.cropShapeHatchPatternColor = void 0;
1539
+ this.cropShapeHatchPatternKey = void 0;
1540
+ this.overlaySpecs = [];
1274
1541
  this.clearRenderedImages();
1542
+ (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
1543
+ this.renderProducerDisposable = void 0;
1275
1544
  if (this.canvasService) {
1276
- void this.canvasService.applyObjectSpecsToRootLayer(
1277
- IMAGE_OVERLAY_LAYER_ID,
1278
- []
1279
- );
1545
+ void this.canvasService.flushRenderFromProducers();
1280
1546
  this.canvasService = void 0;
1281
1547
  }
1282
1548
  this.context = void 0;
@@ -1687,38 +1953,38 @@ var ImageTool = class {
1687
1953
  if (!layout) {
1688
1954
  return { left: 0, top: 0, width: 0, height: 0 };
1689
1955
  }
1690
- return {
1956
+ return this.canvasService.toSceneRect({
1691
1957
  left: layout.cutRect.left,
1692
1958
  top: layout.cutRect.top,
1693
1959
  width: layout.cutRect.width,
1694
1960
  height: layout.cutRect.height
1961
+ });
1962
+ }
1963
+ getFrameRectScreen(frame) {
1964
+ if (!this.canvasService) {
1965
+ return { left: 0, top: 0, width: 0, height: 0 };
1966
+ }
1967
+ return this.canvasService.toScreenRect(frame || this.getFrameRect());
1968
+ }
1969
+ toLayoutSceneRect(rect) {
1970
+ return {
1971
+ left: rect.left,
1972
+ top: rect.top,
1973
+ width: rect.width,
1974
+ height: rect.height,
1975
+ space: "scene"
1695
1976
  };
1696
1977
  }
1697
1978
  async resolveDefaultFitArea() {
1698
- if (!this.context || !this.canvasService) return null;
1699
- const commandService = this.context.services.get("CommandService");
1700
- if (!commandService) return null;
1701
- try {
1702
- const layout = await Promise.resolve(
1703
- commandService.executeCommand("getSceneLayout")
1704
- );
1705
- const cutRect = layout == null ? void 0 : layout.cutRect;
1706
- const width = Number(cutRect == null ? void 0 : cutRect.width);
1707
- const height = Number(cutRect == null ? void 0 : cutRect.height);
1708
- const left = Number(cutRect == null ? void 0 : cutRect.left);
1709
- const top = Number(cutRect == null ? void 0 : cutRect.top);
1710
- if (!Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(left) || !Number.isFinite(top)) {
1711
- return null;
1712
- }
1713
- return {
1714
- width: Math.max(1, width),
1715
- height: Math.max(1, height),
1716
- left: left + width / 2,
1717
- top: top + height / 2
1718
- };
1719
- } catch (e) {
1720
- return null;
1721
- }
1979
+ if (!this.canvasService) return null;
1980
+ const frame = this.getFrameRect();
1981
+ if (frame.width <= 0 || frame.height <= 0) return null;
1982
+ return {
1983
+ width: Math.max(1, frame.width),
1984
+ height: Math.max(1, frame.height),
1985
+ left: frame.left + frame.width / 2,
1986
+ top: frame.top + frame.height / 2
1987
+ };
1722
1988
  }
1723
1989
  async fitImageToDefaultArea(id) {
1724
1990
  if (!this.canvasService) return;
@@ -1727,13 +1993,14 @@ var ImageTool = class {
1727
1993
  await this.fitImageToArea(id, area);
1728
1994
  return;
1729
1995
  }
1730
- const canvasW = Math.max(1, this.canvasService.canvas.width || 0);
1731
- const canvasH = Math.max(1, this.canvasService.canvas.height || 0);
1996
+ const viewport = this.canvasService.getSceneViewportRect();
1997
+ const canvasW = Math.max(1, viewport.width || 0);
1998
+ const canvasH = Math.max(1, viewport.height || 0);
1732
1999
  await this.fitImageToArea(id, {
1733
2000
  width: canvasW,
1734
2001
  height: canvasH,
1735
- left: canvasW / 2,
1736
- top: canvasH / 2
2002
+ left: viewport.left + canvasW / 2,
2003
+ top: viewport.top + canvasH / 2
1737
2004
  });
1738
2005
  }
1739
2006
  getImageObjects() {
@@ -1821,13 +2088,17 @@ var ImageTool = class {
1821
2088
  }
1822
2089
  toSceneGeometryLike(raw) {
1823
2090
  const shape = raw == null ? void 0 : raw.shape;
1824
- if (shape !== "rect" && shape !== "circle" && shape !== "ellipse" && shape !== "custom") {
2091
+ if (!isDielineShape(shape)) {
1825
2092
  return null;
1826
2093
  }
1827
- const radius = Number(raw == null ? void 0 : raw.radius);
1828
- const offset = Number(raw == null ? void 0 : raw.offset);
2094
+ const radiusRaw = Number(raw == null ? void 0 : raw.radius);
2095
+ const offsetRaw = Number(raw == null ? void 0 : raw.offset);
2096
+ const unit = typeof (raw == null ? void 0 : raw.unit) === "string" ? raw.unit : "px";
2097
+ const radius = unit === "scene" || !this.canvasService ? radiusRaw : this.canvasService.toSceneLength(radiusRaw);
2098
+ const offset = unit === "scene" || !this.canvasService ? offsetRaw : this.canvasService.toSceneLength(offsetRaw);
1829
2099
  return {
1830
2100
  shape,
2101
+ shapeStyle: normalizeShapeStyle(raw == null ? void 0 : raw.shapeStyle),
1831
2102
  radius: Number.isFinite(radius) ? radius : 0,
1832
2103
  offset: Number.isFinite(offset) ? offset : 0
1833
2104
  };
@@ -1879,8 +2150,11 @@ var ImageTool = class {
1879
2150
  return Math.max(0, Math.min(maxRadius, rawCutRadius));
1880
2151
  }
1881
2152
  getCropShapeHatchPattern(color = "rgba(255, 0, 0, 0.6)") {
2153
+ var _a;
1882
2154
  if (typeof document === "undefined") return void 0;
1883
- if (this.cropShapeHatchPattern && this.cropShapeHatchPatternColor === color) {
2155
+ const sceneScale = ((_a = this.canvasService) == null ? void 0 : _a.getSceneScale()) || 1;
2156
+ const cacheKey = `${color}::${sceneScale.toFixed(6)}`;
2157
+ if (this.cropShapeHatchPattern && this.cropShapeHatchPatternColor === color && this.cropShapeHatchPatternKey === cacheKey) {
1884
2158
  return this.cropShapeHatchPattern;
1885
2159
  }
1886
2160
  const size = 16;
@@ -1909,11 +2183,21 @@ var ImageTool = class {
1909
2183
  // @ts-ignore: Fabric Pattern accepts canvas source here.
1910
2184
  repetition: "repeat"
1911
2185
  });
2186
+ pattern.patternTransform = [
2187
+ 1 / sceneScale,
2188
+ 0,
2189
+ 0,
2190
+ 1 / sceneScale,
2191
+ 0,
2192
+ 0
2193
+ ];
1912
2194
  this.cropShapeHatchPattern = pattern;
1913
2195
  this.cropShapeHatchPatternColor = color;
2196
+ this.cropShapeHatchPatternKey = cacheKey;
1914
2197
  return pattern;
1915
2198
  }
1916
2199
  buildCropShapeOverlaySpecs(frame, sceneGeometry) {
2200
+ var _a, _b;
1917
2201
  if (!sceneGeometry) {
1918
2202
  this.debug("overlay:shape:skip", { reason: "scene-geometry-missing" });
1919
2203
  return [];
@@ -1923,6 +2207,7 @@ var ImageTool = class {
1923
2207
  return [];
1924
2208
  }
1925
2209
  const shape = sceneGeometry.shape;
2210
+ const shapeStyle = sceneGeometry.shapeStyle;
1926
2211
  const inset = 0;
1927
2212
  const shapeWidth = Math.max(1, frame.width);
1928
2213
  const shapeHeight = Math.max(1, frame.height);
@@ -1932,6 +2217,7 @@ var ImageTool = class {
1932
2217
  frameWidth: frame.width,
1933
2218
  frameHeight: frame.height,
1934
2219
  offset: sceneGeometry.offset,
2220
+ shapeStyle,
1935
2221
  inset,
1936
2222
  shapeWidth,
1937
2223
  shapeHeight,
@@ -1953,6 +2239,7 @@ var ImageTool = class {
1953
2239
  x: frame.width / 2,
1954
2240
  y: frame.height / 2,
1955
2241
  features: [],
2242
+ shapeStyle,
1956
2243
  canvasWidth: frame.width,
1957
2244
  canvasHeight: frame.height
1958
2245
  };
@@ -1970,6 +2257,9 @@ var ImageTool = class {
1970
2257
  }
1971
2258
  const patternFill = this.getCropShapeHatchPattern();
1972
2259
  const hatchFill = patternFill || "rgba(255, 0, 0, 0.22)";
2260
+ const shapeBounds = getPathBounds(shapePathData);
2261
+ const hatchBounds = getPathBounds(hatchPathData);
2262
+ const frameRect = this.toLayoutSceneRect(frame);
1973
2263
  const hatchPathLength = hatchPathData.length;
1974
2264
  const shapePathLength = shapePathData.length;
1975
2265
  const specs = [
@@ -1977,10 +2267,16 @@ var ImageTool = class {
1977
2267
  id: "image.cropShapeHatch",
1978
2268
  type: "path",
1979
2269
  data: { id: "image.cropShapeHatch", zIndex: 5 },
2270
+ layout: {
2271
+ reference: "custom",
2272
+ referenceRect: frameRect,
2273
+ alignX: "start",
2274
+ alignY: "start",
2275
+ offsetX: hatchBounds.x,
2276
+ offsetY: hatchBounds.y
2277
+ },
1980
2278
  props: {
1981
2279
  pathData: hatchPathData,
1982
- left: frame.left,
1983
- top: frame.top,
1984
2280
  originX: "left",
1985
2281
  originY: "top",
1986
2282
  fill: hatchFill,
@@ -1997,15 +2293,21 @@ var ImageTool = class {
1997
2293
  id: "image.cropShapePath",
1998
2294
  type: "path",
1999
2295
  data: { id: "image.cropShapePath", zIndex: 6 },
2296
+ layout: {
2297
+ reference: "custom",
2298
+ referenceRect: frameRect,
2299
+ alignX: "start",
2300
+ alignY: "start",
2301
+ offsetX: shapeBounds.x,
2302
+ offsetY: shapeBounds.y
2303
+ },
2000
2304
  props: {
2001
2305
  pathData: shapePathData,
2002
- left: frame.left,
2003
- top: frame.top,
2004
2306
  originX: "left",
2005
2307
  originY: "top",
2006
2308
  fill: "rgba(0,0,0,0)",
2007
2309
  stroke: "rgba(255, 0, 0, 0.9)",
2008
- strokeWidth: 1,
2310
+ strokeWidth: (_b = (_a = this.canvasService) == null ? void 0 : _a.toSceneLength(1)) != null ? _b : 1,
2009
2311
  selectable: false,
2010
2312
  evented: false,
2011
2313
  excludeFromExport: true,
@@ -2022,6 +2324,8 @@ var ImageTool = class {
2022
2324
  fillRule: "evenodd",
2023
2325
  shapePathLength,
2024
2326
  hatchPathLength,
2327
+ shapeBounds,
2328
+ hatchBounds,
2025
2329
  hatchFillType: hatchFill && typeof hatchFill === "object" ? "pattern" : "color",
2026
2330
  ids: specs.map((spec) => spec.id)
2027
2331
  });
@@ -2084,6 +2388,28 @@ var ImageTool = class {
2084
2388
  opacity: render.opacity
2085
2389
  };
2086
2390
  }
2391
+ toScreenObjectProps(props) {
2392
+ if (!this.canvasService) return props;
2393
+ const next = { ...props };
2394
+ if (Number.isFinite(next.left) || Number.isFinite(next.top)) {
2395
+ const mapped = this.canvasService.toScreenPoint({
2396
+ x: Number.isFinite(next.left) ? Number(next.left) : 0,
2397
+ y: Number.isFinite(next.top) ? Number(next.top) : 0
2398
+ });
2399
+ if (Number.isFinite(next.left)) next.left = mapped.x;
2400
+ if (Number.isFinite(next.top)) next.top = mapped.y;
2401
+ }
2402
+ const sceneScale = this.canvasService.getSceneScale();
2403
+ const sx = Number.isFinite(next.scaleX) ? Number(next.scaleX) : 1;
2404
+ const sy = Number.isFinite(next.scaleY) ? Number(next.scaleY) : 1;
2405
+ next.scaleX = sx * sceneScale;
2406
+ next.scaleY = sy * sceneScale;
2407
+ return next;
2408
+ }
2409
+ toSceneObjectScale(value) {
2410
+ if (!this.canvasService) return value;
2411
+ return value / this.canvasService.getSceneScale();
2412
+ }
2087
2413
  getCurrentSrc(obj) {
2088
2414
  var _a;
2089
2415
  if (!obj) return void 0;
@@ -2133,8 +2459,9 @@ var ImageTool = class {
2133
2459
  this.rememberSourceSize(render.src, obj);
2134
2460
  const sourceSize = this.getSourceSize(render.src, obj);
2135
2461
  const props = this.computeCanvasProps(render, sourceSize, frame);
2462
+ const screenProps = this.toScreenObjectProps(props);
2136
2463
  obj.set({
2137
- ...props,
2464
+ ...screenProps,
2138
2465
  data: {
2139
2466
  ...obj.data || {},
2140
2467
  id: item.id,
@@ -2200,37 +2527,67 @@ var ImageTool = class {
2200
2527
  });
2201
2528
  return [];
2202
2529
  }
2203
- const canvasW = this.canvasService.canvas.width || 0;
2204
- const canvasH = this.canvasService.canvas.height || 0;
2530
+ const viewport = this.canvasService.getSceneViewportRect();
2531
+ const canvasW = viewport.width || 0;
2532
+ const canvasH = viewport.height || 0;
2533
+ const canvasLeft = viewport.left || 0;
2534
+ const canvasTop = viewport.top || 0;
2205
2535
  const visual = this.getFrameVisualConfig();
2206
- const frameLeft = Math.max(0, Math.min(canvasW, frame.left));
2207
- const frameTop = Math.max(0, Math.min(canvasH, frame.top));
2536
+ const strokeWidthScene = this.canvasService.toSceneLength(
2537
+ visual.strokeWidth
2538
+ );
2539
+ const dashLengthScene = this.canvasService.toSceneLength(visual.dashLength);
2540
+ const frameLeft = Math.max(
2541
+ canvasLeft,
2542
+ Math.min(canvasLeft + canvasW, frame.left)
2543
+ );
2544
+ const frameTop = Math.max(
2545
+ canvasTop,
2546
+ Math.min(canvasTop + canvasH, frame.top)
2547
+ );
2208
2548
  const frameRight = Math.max(
2209
2549
  frameLeft,
2210
- Math.min(canvasW, frame.left + frame.width)
2550
+ Math.min(canvasLeft + canvasW, frame.left + frame.width)
2211
2551
  );
2212
2552
  const frameBottom = Math.max(
2213
2553
  frameTop,
2214
- Math.min(canvasH, frame.top + frame.height)
2554
+ Math.min(canvasTop + canvasH, frame.top + frame.height)
2215
2555
  );
2216
2556
  const visibleFrameH = Math.max(0, frameBottom - frameTop);
2217
- const topH = frameTop;
2218
- const bottomH = Math.max(0, canvasH - frameBottom);
2219
- const leftW = frameLeft;
2220
- const rightW = Math.max(0, canvasW - frameRight);
2557
+ const topH = Math.max(0, frameTop - canvasTop);
2558
+ const bottomH = Math.max(0, canvasTop + canvasH - frameBottom);
2559
+ const leftW = Math.max(0, frameLeft - canvasLeft);
2560
+ const rightW = Math.max(0, canvasLeft + canvasW - frameRight);
2561
+ const viewportRect = this.toLayoutSceneRect({
2562
+ left: canvasLeft,
2563
+ top: canvasTop,
2564
+ width: canvasW,
2565
+ height: canvasH
2566
+ });
2567
+ const visibleFrameBandRect = this.toLayoutSceneRect({
2568
+ left: canvasLeft,
2569
+ top: frameTop,
2570
+ width: canvasW,
2571
+ height: visibleFrameH
2572
+ });
2573
+ const frameRect = this.toLayoutSceneRect(frame);
2221
2574
  const shapeOverlay = this.buildCropShapeOverlaySpecs(frame, sceneGeometry);
2222
2575
  const mask = [
2223
2576
  {
2224
2577
  id: "image.cropMask.top",
2225
2578
  type: "rect",
2226
2579
  data: { id: "image.cropMask.top", zIndex: 1 },
2580
+ layout: {
2581
+ reference: "custom",
2582
+ referenceRect: viewportRect,
2583
+ alignX: "start",
2584
+ alignY: "start",
2585
+ width: "100%",
2586
+ height: topH
2587
+ },
2227
2588
  props: {
2228
- left: canvasW / 2,
2229
- top: topH / 2,
2230
- width: canvasW,
2231
- height: topH,
2232
- originX: "center",
2233
- originY: "center",
2589
+ originX: "left",
2590
+ originY: "top",
2234
2591
  fill: visual.outerBackground,
2235
2592
  selectable: false,
2236
2593
  evented: false
@@ -2240,13 +2597,17 @@ var ImageTool = class {
2240
2597
  id: "image.cropMask.bottom",
2241
2598
  type: "rect",
2242
2599
  data: { id: "image.cropMask.bottom", zIndex: 2 },
2600
+ layout: {
2601
+ reference: "custom",
2602
+ referenceRect: viewportRect,
2603
+ alignX: "start",
2604
+ alignY: "end",
2605
+ width: "100%",
2606
+ height: bottomH
2607
+ },
2243
2608
  props: {
2244
- left: canvasW / 2,
2245
- top: frameBottom + bottomH / 2,
2246
- width: canvasW,
2247
- height: bottomH,
2248
- originX: "center",
2249
- originY: "center",
2609
+ originX: "left",
2610
+ originY: "top",
2250
2611
  fill: visual.outerBackground,
2251
2612
  selectable: false,
2252
2613
  evented: false
@@ -2256,13 +2617,17 @@ var ImageTool = class {
2256
2617
  id: "image.cropMask.left",
2257
2618
  type: "rect",
2258
2619
  data: { id: "image.cropMask.left", zIndex: 3 },
2259
- props: {
2260
- left: leftW / 2,
2261
- top: frameTop + visibleFrameH / 2,
2620
+ layout: {
2621
+ reference: "custom",
2622
+ referenceRect: visibleFrameBandRect,
2623
+ alignX: "start",
2624
+ alignY: "start",
2262
2625
  width: leftW,
2263
- height: visibleFrameH,
2264
- originX: "center",
2265
- originY: "center",
2626
+ height: "100%"
2627
+ },
2628
+ props: {
2629
+ originX: "left",
2630
+ originY: "top",
2266
2631
  fill: visual.outerBackground,
2267
2632
  selectable: false,
2268
2633
  evented: false
@@ -2272,13 +2637,17 @@ var ImageTool = class {
2272
2637
  id: "image.cropMask.right",
2273
2638
  type: "rect",
2274
2639
  data: { id: "image.cropMask.right", zIndex: 4 },
2275
- props: {
2276
- left: frameRight + rightW / 2,
2277
- top: frameTop + visibleFrameH / 2,
2640
+ layout: {
2641
+ reference: "custom",
2642
+ referenceRect: visibleFrameBandRect,
2643
+ alignX: "end",
2644
+ alignY: "start",
2278
2645
  width: rightW,
2279
- height: visibleFrameH,
2280
- originX: "center",
2281
- originY: "center",
2646
+ height: "100%"
2647
+ },
2648
+ props: {
2649
+ originX: "left",
2650
+ originY: "top",
2282
2651
  fill: visual.outerBackground,
2283
2652
  selectable: false,
2284
2653
  evented: false
@@ -2289,17 +2658,21 @@ var ImageTool = class {
2289
2658
  id: "image.cropFrame",
2290
2659
  type: "rect",
2291
2660
  data: { id: "image.cropFrame", zIndex: 7 },
2661
+ layout: {
2662
+ reference: "custom",
2663
+ referenceRect: frameRect,
2664
+ alignX: "start",
2665
+ alignY: "start",
2666
+ width: "100%",
2667
+ height: "100%"
2668
+ },
2292
2669
  props: {
2293
- left: frame.left + frame.width / 2,
2294
- top: frame.top + frame.height / 2,
2295
- width: frame.width,
2296
- height: frame.height,
2297
- originX: "center",
2298
- originY: "center",
2670
+ originX: "left",
2671
+ originY: "top",
2299
2672
  fill: visual.innerBackground,
2300
2673
  stroke: visual.strokeStyle === "hidden" ? "rgba(0,0,0,0)" : visual.strokeColor,
2301
- strokeWidth: visual.strokeStyle === "hidden" ? 0 : visual.strokeWidth,
2302
- strokeDashArray: visual.strokeStyle === "dashed" ? [visual.dashLength, visual.dashLength] : void 0,
2674
+ strokeWidth: visual.strokeStyle === "hidden" ? 0 : strokeWidthScene,
2675
+ strokeDashArray: visual.strokeStyle === "dashed" ? [dashLengthScene, dashLengthScene] : void 0,
2303
2676
  selectable: false,
2304
2677
  evented: false
2305
2678
  }
@@ -2350,10 +2723,8 @@ var ImageTool = class {
2350
2723
  const sceneGeometry = await this.resolveSceneGeometryForOverlay();
2351
2724
  if (seq !== this.renderSeq) return;
2352
2725
  const overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
2353
- await this.canvasService.applyObjectSpecsToRootLayer(
2354
- IMAGE_OVERLAY_LAYER_ID,
2355
- overlaySpecs
2356
- );
2726
+ this.overlaySpecs = overlaySpecs;
2727
+ await this.canvasService.flushRenderFromProducers();
2357
2728
  this.syncImageZOrder(renderItems);
2358
2729
  const overlayCanvasCount = this.getOverlayObjects().length;
2359
2730
  this.debug("render:done", {
@@ -2444,7 +2815,7 @@ var ImageTool = class {
2444
2815
  const source = this.getSourceSize(render.src, obj);
2445
2816
  const frame = this.getFrameRect();
2446
2817
  const coverScale = this.getCoverScale(frame, source);
2447
- const currentScale = obj.scaleX || 1;
2818
+ const currentScale = this.toSceneObjectScale(obj.scaleX || 1);
2448
2819
  const zoom = Math.max(0.05, currentScale / coverScale);
2449
2820
  const updated = {
2450
2821
  scale: Number.isFinite(zoom) ? zoom : 1,
@@ -2481,12 +2852,13 @@ var ImageTool = class {
2481
2852
  Math.max(1, area.width) / Math.max(1, source.width),
2482
2853
  Math.max(1, area.height) / Math.max(1, source.height)
2483
2854
  );
2484
- const canvasW = this.canvasService.canvas.width || 1;
2485
- const canvasH = this.canvasService.canvas.height || 1;
2855
+ const viewport = this.canvasService.getSceneViewportRect();
2856
+ const canvasW = viewport.width || 1;
2857
+ const canvasH = viewport.height || 1;
2486
2858
  const areaLeftInput = (_a = area.left) != null ? _a : 0.5;
2487
2859
  const areaTopInput = (_b = area.top) != null ? _b : 0.5;
2488
- const areaLeftPx = areaLeftInput <= 1.5 ? areaLeftInput * canvasW : areaLeftInput;
2489
- const areaTopPx = areaTopInput <= 1.5 ? areaTopInput * canvasH : areaTopInput;
2860
+ const areaLeftPx = areaLeftInput <= 1.5 ? viewport.left + areaLeftInput * canvasW : areaLeftInput;
2861
+ const areaTopPx = areaTopInput <= 1.5 ? viewport.top + areaTopInput * canvasH : areaTopInput;
2490
2862
  const updates = {
2491
2863
  scale: Math.max(0.05, desiredScale / baseCover),
2492
2864
  left: this.clampNormalized(
@@ -2551,7 +2923,8 @@ var ImageTool = class {
2551
2923
  if (!normalizedIds.length) {
2552
2924
  throw new Error("image-ids-required");
2553
2925
  }
2554
- const frame = this.getFrameRect();
2926
+ const frameScene = this.getFrameRect();
2927
+ const frame = this.getFrameRectScreen(frameScene);
2555
2928
  const multiplier = Math.max(1, (_a = options.multiplier) != null ? _a : 2);
2556
2929
  const format = options.format === "jpeg" ? "jpeg" : "png";
2557
2930
  const width = Math.max(1, Math.round(frame.width * multiplier));
@@ -3975,6 +4348,7 @@ var ImageTracer = class {
3975
4348
 
3976
4349
  // src/extensions/dieline.ts
3977
4350
  var IMAGE_OBJECT_LAYER_ID2 = "image.user";
4351
+ var DIELINE_LAYER_ID = "dieline-overlay";
3978
4352
  var DielineTool = class {
3979
4353
  constructor(options) {
3980
4354
  this.id = "pooder.kit.dieline";
@@ -3982,8 +4356,8 @@ var DielineTool = class {
3982
4356
  name: "DielineTool"
3983
4357
  };
3984
4358
  this.state = {
3985
- displayUnit: "mm",
3986
- shape: "rect",
4359
+ shape: DEFAULT_DIELINE_SHAPE,
4360
+ shapeStyle: { ...DEFAULT_DIELINE_SHAPE_STYLE },
3987
4361
  width: 500,
3988
4362
  height: 500,
3989
4363
  radius: 0,
@@ -4006,6 +4380,8 @@ var DielineTool = class {
4006
4380
  showBleedLines: true,
4007
4381
  features: []
4008
4382
  };
4383
+ this.specs = [];
4384
+ this.renderSeq = 0;
4009
4385
  this.onCanvasResized = () => {
4010
4386
  this.updateDieline();
4011
4387
  };
@@ -4018,24 +4394,50 @@ var DielineTool = class {
4018
4394
  Object.assign(this.state.offsetLine, options.offsetLine);
4019
4395
  delete options.offsetLine;
4020
4396
  }
4397
+ if (options.shapeStyle) {
4398
+ this.state.shapeStyle = normalizeShapeStyle(
4399
+ options.shapeStyle,
4400
+ this.state.shapeStyle
4401
+ );
4402
+ delete options.shapeStyle;
4403
+ }
4021
4404
  Object.assign(this.state, options);
4405
+ this.state.shape = normalizeDielineShape(options.shape, this.state.shape);
4022
4406
  }
4023
4407
  }
4024
4408
  activate(context) {
4409
+ var _a;
4025
4410
  this.context = context;
4026
4411
  this.canvasService = context.services.get("CanvasService");
4027
4412
  if (!this.canvasService) {
4028
4413
  console.warn("CanvasService not found for DielineTool");
4029
4414
  return;
4030
4415
  }
4416
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
4417
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
4418
+ this.id,
4419
+ () => ({
4420
+ layerSpecs: {
4421
+ [DIELINE_LAYER_ID]: this.specs
4422
+ },
4423
+ replaceLayerIds: [DIELINE_LAYER_ID]
4424
+ }),
4425
+ { priority: 250 }
4426
+ );
4031
4427
  const configService = context.services.get(
4032
4428
  "ConfigurationService"
4033
4429
  );
4034
4430
  if (configService) {
4035
4431
  const s = this.state;
4036
4432
  const sizeState = readSizeState(configService);
4037
- s.displayUnit = sizeState.unit;
4038
- s.shape = configService.get("dieline.shape", s.shape);
4433
+ s.shape = normalizeDielineShape(
4434
+ configService.get("dieline.shape", s.shape),
4435
+ s.shape
4436
+ );
4437
+ s.shapeStyle = normalizeShapeStyle(
4438
+ configService.get("dieline.shapeStyle", s.shapeStyle),
4439
+ s.shapeStyle
4440
+ );
4039
4441
  s.width = sizeState.actualWidthMm;
4040
4442
  s.height = sizeState.actualHeightMm;
4041
4443
  s.radius = parseLengthToMm(
@@ -4095,7 +4497,6 @@ var DielineTool = class {
4095
4497
  configService.onAnyChange((e) => {
4096
4498
  if (e.key.startsWith("size.")) {
4097
4499
  const nextSize = readSizeState(configService);
4098
- s.displayUnit = nextSize.unit;
4099
4500
  s.width = nextSize.actualWidthMm;
4100
4501
  s.height = nextSize.actualHeightMm;
4101
4502
  s.padding = nextSize.viewPadding;
@@ -4106,7 +4507,10 @@ var DielineTool = class {
4106
4507
  if (e.key.startsWith("dieline.")) {
4107
4508
  switch (e.key) {
4108
4509
  case "dieline.shape":
4109
- s.shape = e.value;
4510
+ s.shape = normalizeDielineShape(e.value, s.shape);
4511
+ break;
4512
+ case "dieline.shapeStyle":
4513
+ s.shapeStyle = normalizeShapeStyle(e.value, s.shapeStyle);
4110
4514
  break;
4111
4515
  case "dieline.radius":
4112
4516
  s.radius = parseLengthToMm(e.value, "mm");
@@ -4162,12 +4566,18 @@ var DielineTool = class {
4162
4566
  });
4163
4567
  }
4164
4568
  context.eventBus.on("canvas:resized", this.onCanvasResized);
4165
- this.createLayer();
4166
4569
  this.updateDieline();
4167
4570
  }
4168
4571
  deactivate(context) {
4572
+ var _a;
4169
4573
  context.eventBus.off("canvas:resized", this.onCanvasResized);
4170
- this.destroyLayer();
4574
+ this.renderSeq += 1;
4575
+ this.specs = [];
4576
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
4577
+ this.renderProducerDisposable = void 0;
4578
+ if (this.canvasService) {
4579
+ void this.canvasService.flushRenderFromProducers();
4580
+ }
4171
4581
  this.canvasService = void 0;
4172
4582
  this.context = void 0;
4173
4583
  }
@@ -4190,7 +4600,7 @@ var DielineTool = class {
4190
4600
  id: "dieline.shape",
4191
4601
  type: "select",
4192
4602
  label: "Shape",
4193
- options: ["rect", "circle", "ellipse", "custom"],
4603
+ options: Array.from(DIELINE_SHAPES),
4194
4604
  default: s.shape
4195
4605
  },
4196
4606
  {
@@ -4201,6 +4611,12 @@ var DielineTool = class {
4201
4611
  max: 500,
4202
4612
  default: s.radius
4203
4613
  },
4614
+ {
4615
+ id: "dieline.shapeStyle",
4616
+ type: "json",
4617
+ label: "Shape Style",
4618
+ default: s.shapeStyle
4619
+ },
4204
4620
  {
4205
4621
  id: "dieline.showBleedLines",
4206
4622
  type: "boolean",
@@ -4376,34 +4792,6 @@ var DielineTool = class {
4376
4792
  ]
4377
4793
  };
4378
4794
  }
4379
- getLayer() {
4380
- var _a;
4381
- return (_a = this.canvasService) == null ? void 0 : _a.getLayer("dieline-overlay");
4382
- }
4383
- createLayer() {
4384
- if (!this.canvasService) return;
4385
- const width = this.canvasService.canvas.width || 800;
4386
- const height = this.canvasService.canvas.height || 600;
4387
- const layer = this.canvasService.createLayer("dieline-overlay", {
4388
- width,
4389
- height,
4390
- selectable: false,
4391
- evented: false
4392
- });
4393
- this.canvasService.canvas.bringObjectToFront(layer);
4394
- const userLayer = this.canvasService.getLayer("user");
4395
- if (userLayer) {
4396
- const userIndex = this.canvasService.canvas.getObjects().indexOf(userLayer);
4397
- this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
4398
- }
4399
- }
4400
- destroyLayer() {
4401
- if (!this.canvasService) return;
4402
- const layer = this.getLayer();
4403
- if (layer) {
4404
- this.canvasService.canvas.remove(layer);
4405
- }
4406
- }
4407
4795
  createHatchPattern(color = "rgba(0, 0, 0, 0.3)") {
4408
4796
  if (typeof document === "undefined") {
4409
4797
  return void 0;
@@ -4432,7 +4820,6 @@ var DielineTool = class {
4432
4820
  }
4433
4821
  syncSizeState(configService) {
4434
4822
  const sizeState = readSizeState(configService);
4435
- this.state.displayUnit = sizeState.unit;
4436
4823
  this.state.width = sizeState.actualWidthMm;
4437
4824
  this.state.height = sizeState.actualHeightMm;
4438
4825
  this.state.padding = sizeState.viewPadding;
@@ -4446,20 +4833,26 @@ var DielineTool = class {
4446
4833
  return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.type) === "feature-marker";
4447
4834
  }).forEach((obj) => canvas.bringObjectToFront(obj));
4448
4835
  }
4449
- updateDieline(_emitEvent = true) {
4836
+ ensureLayerStacking() {
4450
4837
  if (!this.canvasService) return;
4451
- const layer = this.getLayer();
4838
+ const layer = this.canvasService.getLayer(DIELINE_LAYER_ID);
4452
4839
  if (!layer) return;
4453
- const configService = this.getConfigService();
4454
- if (!configService) return;
4455
- this.syncSizeState(configService);
4456
- const sceneLayout = computeSceneLayout(
4457
- this.canvasService,
4458
- readSizeState(configService)
4459
- );
4460
- if (!sceneLayout) return;
4840
+ const userLayer = this.canvasService.getLayer("user");
4841
+ if (userLayer) {
4842
+ const layerIndex = this.canvasService.canvas.getObjects().indexOf(layer);
4843
+ const userIndex = this.canvasService.canvas.getObjects().indexOf(userLayer);
4844
+ if (layerIndex < userIndex) {
4845
+ this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
4846
+ }
4847
+ return;
4848
+ }
4849
+ this.canvasService.canvas.bringObjectToFront(layer);
4850
+ }
4851
+ buildDielineSpecs(sceneLayout) {
4852
+ var _a, _b;
4461
4853
  const {
4462
4854
  shape,
4855
+ shapeStyle,
4463
4856
  radius,
4464
4857
  mainLine,
4465
4858
  offsetLine,
@@ -4468,8 +4861,8 @@ var DielineTool = class {
4468
4861
  showBleedLines,
4469
4862
  features
4470
4863
  } = this.state;
4471
- const canvasW = sceneLayout.canvasWidth || this.canvasService.canvas.width || 800;
4472
- const canvasH = sceneLayout.canvasHeight || this.canvasService.canvas.height || 600;
4864
+ const canvasW = sceneLayout.canvasWidth || ((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 800;
4865
+ const canvasH = sceneLayout.canvasHeight || ((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 600;
4473
4866
  const scale = sceneLayout.scale;
4474
4867
  const cx = sceneLayout.trimRect.centerX;
4475
4868
  const cy = sceneLayout.trimRect.centerY;
@@ -4480,7 +4873,6 @@ var DielineTool = class {
4480
4873
  const cutH = sceneLayout.cutRect.height;
4481
4874
  const visualOffset = (cutW - visualWidth) / 2;
4482
4875
  const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
4483
- layer.remove(...layer.getObjects());
4484
4876
  const absoluteFeatures = (features || []).map((f) => ({
4485
4877
  ...f,
4486
4878
  x: f.x,
@@ -4500,21 +4892,30 @@ var DielineTool = class {
4500
4892
  x: cx,
4501
4893
  y: cy,
4502
4894
  features: cutFeatures,
4895
+ shapeStyle,
4503
4896
  pathData: this.state.pathData,
4504
4897
  customSourceWidthPx: this.state.customSourceWidthPx,
4505
4898
  customSourceHeightPx: this.state.customSourceHeightPx
4506
4899
  });
4507
- const mask = new import_fabric3.Path(maskPathData, {
4508
- fill: outsideColor,
4509
- stroke: null,
4510
- selectable: false,
4511
- evented: false,
4512
- originX: "left",
4513
- originY: "top",
4514
- left: 0,
4515
- top: 0
4516
- });
4517
- layer.add(mask);
4900
+ const specs = [
4901
+ {
4902
+ id: "dieline.mask",
4903
+ type: "path",
4904
+ space: "screen",
4905
+ data: { id: "dieline.mask", type: "dieline" },
4906
+ props: {
4907
+ pathData: maskPathData,
4908
+ fill: outsideColor,
4909
+ stroke: null,
4910
+ selectable: false,
4911
+ evented: false,
4912
+ originX: "left",
4913
+ originY: "top",
4914
+ left: 0,
4915
+ top: 0
4916
+ }
4917
+ }
4918
+ ];
4518
4919
  if (insideColor && insideColor !== "transparent" && insideColor !== "rgba(0,0,0,0)") {
4519
4920
  const productPathData = generateDielinePath({
4520
4921
  shape,
@@ -4524,21 +4925,28 @@ var DielineTool = class {
4524
4925
  x: cx,
4525
4926
  y: cy,
4526
4927
  features: cutFeatures,
4928
+ shapeStyle,
4527
4929
  pathData: this.state.pathData,
4528
4930
  customSourceWidthPx: this.state.customSourceWidthPx,
4529
4931
  customSourceHeightPx: this.state.customSourceHeightPx,
4530
4932
  canvasWidth: canvasW,
4531
4933
  canvasHeight: canvasH
4532
4934
  });
4533
- const insideObj = new import_fabric3.Path(productPathData, {
4534
- fill: insideColor,
4535
- stroke: null,
4536
- selectable: false,
4537
- evented: false,
4538
- originX: "left",
4539
- originY: "top"
4935
+ specs.push({
4936
+ id: "dieline.inside",
4937
+ type: "path",
4938
+ space: "screen",
4939
+ data: { id: "dieline.inside", type: "dieline" },
4940
+ props: {
4941
+ pathData: productPathData,
4942
+ fill: insideColor,
4943
+ stroke: null,
4944
+ selectable: false,
4945
+ evented: false,
4946
+ originX: "left",
4947
+ originY: "top"
4948
+ }
4540
4949
  });
4541
- layer.add(insideObj);
4542
4950
  }
4543
4951
  if (Math.abs(visualOffset) > 1e-4) {
4544
4952
  const bleedPathData = generateBleedZonePath(
@@ -4550,6 +4958,7 @@ var DielineTool = class {
4550
4958
  x: cx,
4551
4959
  y: cy,
4552
4960
  features: cutFeatures,
4961
+ shapeStyle,
4553
4962
  pathData: this.state.pathData,
4554
4963
  customSourceWidthPx: this.state.customSourceWidthPx,
4555
4964
  customSourceHeightPx: this.state.customSourceHeightPx,
@@ -4564,6 +4973,7 @@ var DielineTool = class {
4564
4973
  x: cx,
4565
4974
  y: cy,
4566
4975
  features: cutFeatures,
4976
+ shapeStyle,
4567
4977
  pathData: this.state.pathData,
4568
4978
  customSourceWidthPx: this.state.customSourceWidthPx,
4569
4979
  customSourceHeightPx: this.state.customSourceHeightPx,
@@ -4575,16 +4985,22 @@ var DielineTool = class {
4575
4985
  if (showBleedLines !== false) {
4576
4986
  const pattern = this.createHatchPattern(mainLine.color);
4577
4987
  if (pattern) {
4578
- const bleedObj = new import_fabric3.Path(bleedPathData, {
4579
- fill: pattern,
4580
- stroke: null,
4581
- selectable: false,
4582
- evented: false,
4583
- objectCaching: false,
4584
- originX: "left",
4585
- originY: "top"
4988
+ specs.push({
4989
+ id: "dieline.bleed-zone",
4990
+ type: "path",
4991
+ space: "screen",
4992
+ data: { id: "dieline.bleed-zone", type: "dieline" },
4993
+ props: {
4994
+ pathData: bleedPathData,
4995
+ fill: pattern,
4996
+ stroke: null,
4997
+ selectable: false,
4998
+ evented: false,
4999
+ objectCaching: false,
5000
+ originX: "left",
5001
+ originY: "top"
5002
+ }
4586
5003
  });
4587
- layer.add(bleedObj);
4588
5004
  }
4589
5005
  }
4590
5006
  const offsetPathData = generateDielinePath({
@@ -4595,23 +5011,30 @@ var DielineTool = class {
4595
5011
  x: cx,
4596
5012
  y: cy,
4597
5013
  features: cutFeatures,
5014
+ shapeStyle,
4598
5015
  pathData: this.state.pathData,
4599
5016
  customSourceWidthPx: this.state.customSourceWidthPx,
4600
5017
  customSourceHeightPx: this.state.customSourceHeightPx,
4601
5018
  canvasWidth: canvasW,
4602
5019
  canvasHeight: canvasH
4603
5020
  });
4604
- const offsetBorderObj = new import_fabric3.Path(offsetPathData, {
4605
- fill: null,
4606
- stroke: offsetLine.style === "hidden" ? null : offsetLine.color,
4607
- strokeWidth: offsetLine.width,
4608
- strokeDashArray: offsetLine.style === "dashed" ? [offsetLine.dashLength, offsetLine.dashLength] : void 0,
4609
- selectable: false,
4610
- evented: false,
4611
- originX: "left",
4612
- originY: "top"
5021
+ specs.push({
5022
+ id: "dieline.offset-border",
5023
+ type: "path",
5024
+ space: "screen",
5025
+ data: { id: "dieline.offset-border", type: "dieline" },
5026
+ props: {
5027
+ pathData: offsetPathData,
5028
+ fill: null,
5029
+ stroke: offsetLine.style === "hidden" ? null : offsetLine.color,
5030
+ strokeWidth: offsetLine.width,
5031
+ strokeDashArray: offsetLine.style === "dashed" ? [offsetLine.dashLength, offsetLine.dashLength] : void 0,
5032
+ selectable: false,
5033
+ evented: false,
5034
+ originX: "left",
5035
+ originY: "top"
5036
+ }
4613
5037
  });
4614
- layer.add(offsetBorderObj);
4615
5038
  }
4616
5039
  const borderPathData = generateDielinePath({
4617
5040
  shape,
@@ -4621,39 +5044,59 @@ var DielineTool = class {
4621
5044
  x: cx,
4622
5045
  y: cy,
4623
5046
  features: absoluteFeatures,
5047
+ shapeStyle,
4624
5048
  pathData: this.state.pathData,
4625
5049
  customSourceWidthPx: this.state.customSourceWidthPx,
4626
5050
  customSourceHeightPx: this.state.customSourceHeightPx,
4627
5051
  canvasWidth: canvasW,
4628
5052
  canvasHeight: canvasH
4629
5053
  });
4630
- const borderObj = new import_fabric3.Path(borderPathData, {
4631
- fill: "transparent",
4632
- stroke: mainLine.style === "hidden" ? null : mainLine.color,
4633
- strokeWidth: mainLine.width,
4634
- strokeDashArray: mainLine.style === "dashed" ? [mainLine.dashLength, mainLine.dashLength] : void 0,
4635
- selectable: false,
4636
- evented: false,
4637
- originX: "left",
4638
- originY: "top"
4639
- });
4640
- layer.add(borderObj);
4641
- const userLayer = this.canvasService.getLayer("user");
4642
- if (layer && userLayer) {
4643
- const layerIndex = this.canvasService.canvas.getObjects().indexOf(layer);
4644
- const userIndex = this.canvasService.canvas.getObjects().indexOf(userLayer);
4645
- if (layerIndex < userIndex) {
4646
- this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
5054
+ specs.push({
5055
+ id: "dieline.border",
5056
+ type: "path",
5057
+ space: "screen",
5058
+ data: { id: "dieline.border", type: "dieline" },
5059
+ props: {
5060
+ pathData: borderPathData,
5061
+ fill: "transparent",
5062
+ stroke: mainLine.style === "hidden" ? null : mainLine.color,
5063
+ strokeWidth: mainLine.width,
5064
+ strokeDashArray: mainLine.style === "dashed" ? [mainLine.dashLength, mainLine.dashLength] : void 0,
5065
+ selectable: false,
5066
+ evented: false,
5067
+ originX: "left",
5068
+ originY: "top"
4647
5069
  }
4648
- } else {
4649
- this.canvasService.canvas.bringObjectToFront(layer);
5070
+ });
5071
+ return specs;
5072
+ }
5073
+ updateDieline(_emitEvent = true) {
5074
+ void this.updateDielineAsync();
5075
+ }
5076
+ async updateDielineAsync() {
5077
+ if (!this.canvasService) return;
5078
+ const configService = this.getConfigService();
5079
+ if (!configService) return;
5080
+ const seq = ++this.renderSeq;
5081
+ this.syncSizeState(configService);
5082
+ const sceneLayout = computeSceneLayout(
5083
+ this.canvasService,
5084
+ readSizeState(configService)
5085
+ );
5086
+ if (!sceneLayout) {
5087
+ if (seq !== this.renderSeq) return;
5088
+ this.specs = [];
5089
+ await this.canvasService.flushRenderFromProducers();
5090
+ return;
4650
5091
  }
5092
+ const nextSpecs = this.buildDielineSpecs(sceneLayout);
5093
+ if (seq !== this.renderSeq) return;
5094
+ this.specs = nextSpecs;
5095
+ await this.canvasService.flushRenderFromProducers();
5096
+ if (seq !== this.renderSeq) return;
5097
+ this.ensureLayerStacking();
4651
5098
  this.bringFeatureMarkersToFront();
4652
- const rulerLayer = this.canvasService.getLayer("ruler-overlay");
4653
- if (rulerLayer) {
4654
- this.canvasService.canvas.bringObjectToFront(rulerLayer);
4655
- }
4656
- layer.dirty = true;
5099
+ this.canvasService.bringLayerToFront("ruler-overlay");
4657
5100
  this.canvasService.requestRenderAll();
4658
5101
  }
4659
5102
  getGeometry() {
@@ -4701,7 +5144,7 @@ var DielineTool = class {
4701
5144
  );
4702
5145
  return null;
4703
5146
  }
4704
- const { shape, radius, features, pathData } = this.state;
5147
+ const { shape, shapeStyle, radius, features, pathData } = this.state;
4705
5148
  const canvasW = sceneLayout.canvasWidth || this.canvasService.canvas.width || 800;
4706
5149
  const canvasH = sceneLayout.canvasHeight || this.canvasService.canvas.height || 600;
4707
5150
  const scale = sceneLayout.scale;
@@ -4729,6 +5172,7 @@ var DielineTool = class {
4729
5172
  x: cx,
4730
5173
  y: cy,
4731
5174
  features: cutFeatures,
5175
+ shapeStyle,
4732
5176
  pathData,
4733
5177
  customSourceWidthPx: this.state.customSourceWidthPx,
4734
5178
  customSourceHeightPx: this.state.customSourceHeightPx,
@@ -4841,7 +5285,6 @@ var DielineTool = class {
4841
5285
 
4842
5286
  // src/extensions/feature.ts
4843
5287
  var import_core5 = require("@pooder/core");
4844
- var import_fabric4 = require("fabric");
4845
5288
 
4846
5289
  // src/extensions/constraints.ts
4847
5290
  var ConstraintRegistry = class {
@@ -5041,6 +5484,10 @@ function completeFeaturesStrict(features, context, update) {
5041
5484
  }
5042
5485
 
5043
5486
  // src/extensions/feature.ts
5487
+ var FEATURE_OVERLAY_LAYER_ID = "feature-overlay";
5488
+ var FEATURE_STROKE_WIDTH = 2;
5489
+ var DEFAULT_RECT_SIZE = 10;
5490
+ var DEFAULT_CIRCLE_RADIUS = 5;
5044
5491
  var FeatureTool = class {
5045
5492
  constructor(options) {
5046
5493
  this.id = "pooder.kit.feature";
@@ -5053,6 +5500,8 @@ var FeatureTool = class {
5053
5500
  this.isFeatureSessionActive = false;
5054
5501
  this.sessionOriginalFeatures = null;
5055
5502
  this.hasWorkingChanges = false;
5503
+ this.specs = [];
5504
+ this.renderSeq = 0;
5056
5505
  this.handleMoving = null;
5057
5506
  this.handleModified = null;
5058
5507
  this.handleSceneGeometryChange = null;
@@ -5069,12 +5518,23 @@ var FeatureTool = class {
5069
5518
  }
5070
5519
  }
5071
5520
  activate(context) {
5521
+ var _a;
5072
5522
  this.context = context;
5073
5523
  this.canvasService = context.services.get("CanvasService");
5074
5524
  if (!this.canvasService) {
5075
5525
  console.warn("CanvasService not found for FeatureTool");
5076
5526
  return;
5077
5527
  }
5528
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
5529
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
5530
+ this.id,
5531
+ () => ({
5532
+ rootLayerSpecs: {
5533
+ [FEATURE_OVERLAY_LAYER_ID]: this.specs
5534
+ }
5535
+ }),
5536
+ { priority: 350 }
5537
+ );
5078
5538
  const configService = context.services.get(
5079
5539
  "ConfigurationService"
5080
5540
  );
@@ -5113,21 +5573,7 @@ var FeatureTool = class {
5113
5573
  this.context = void 0;
5114
5574
  }
5115
5575
  updateVisibility() {
5116
- if (!this.canvasService) return;
5117
- const canvas = this.canvasService.canvas;
5118
- const markers = canvas.getObjects().filter((obj) => {
5119
- var _a;
5120
- return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
5121
- });
5122
- markers.forEach((marker) => {
5123
- marker.set({
5124
- visible: this.isToolActive,
5125
- // Or just selectable: false if we want them visible but locked
5126
- selectable: this.isToolActive,
5127
- evented: this.isToolActive
5128
- });
5129
- });
5130
- canvas.requestRenderAll();
5576
+ this.redraw();
5131
5577
  }
5132
5578
  contribute() {
5133
5579
  return {
@@ -5359,8 +5805,7 @@ var FeatureTool = class {
5359
5805
  if (!changed) return { ok: true };
5360
5806
  this.setWorkingFeatures(next);
5361
5807
  this.hasWorkingChanges = true;
5362
- this.redraw();
5363
- this.enforceConstraints();
5808
+ this.redraw({ enforceConstraints: true });
5364
5809
  this.emitWorkingChange();
5365
5810
  return { ok: true };
5366
5811
  }
@@ -5408,12 +5853,10 @@ var FeatureTool = class {
5408
5853
  shape: "rect",
5409
5854
  x: 0.5,
5410
5855
  y: 0,
5411
- // Top edge
5412
5856
  width: 10,
5413
5857
  height: 10,
5414
5858
  rotation: 0,
5415
5859
  renderBehavior: "edge",
5416
- // Default constraint: path (snap to edge)
5417
5860
  constraints: [{ type: "path" }]
5418
5861
  };
5419
5862
  this.setWorkingFeatures([...this.workingFeatures || [], newFeature]);
@@ -5456,7 +5899,7 @@ var FeatureTool = class {
5456
5899
  this.emitWorkingChange();
5457
5900
  return true;
5458
5901
  }
5459
- getGeometryForFeature(geometry, feature) {
5902
+ getGeometryForFeature(geometry, _feature) {
5460
5903
  return geometry;
5461
5904
  }
5462
5905
  setup() {
@@ -5465,8 +5908,7 @@ var FeatureTool = class {
5465
5908
  if (!this.handleSceneGeometryChange) {
5466
5909
  this.handleSceneGeometryChange = (geometry) => {
5467
5910
  this.currentGeometry = geometry;
5468
- this.redraw();
5469
- this.enforceConstraints();
5911
+ this.redraw({ enforceConstraints: true });
5470
5912
  };
5471
5913
  this.context.eventBus.on(
5472
5914
  "scene:geometry:change",
@@ -5489,76 +5931,34 @@ var FeatureTool = class {
5489
5931
  }
5490
5932
  if (!this.handleMoving) {
5491
5933
  this.handleMoving = (e) => {
5492
- var _a, _b, _c, _d;
5493
- const target = e.target;
5494
- if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return;
5495
- if (!this.currentGeometry) return;
5496
- let feature;
5497
- if ((_b = target.data) == null ? void 0 : _b.isGroup) {
5498
- const indices = (_c = target.data) == null ? void 0 : _c.indices;
5499
- if (indices && indices.length > 0) {
5500
- feature = this.workingFeatures[indices[0]];
5501
- }
5502
- } else {
5503
- const index = (_d = target.data) == null ? void 0 : _d.index;
5504
- if (index !== void 0) {
5505
- feature = this.workingFeatures[index];
5506
- }
5507
- }
5508
- const geometry = this.getGeometryForFeature(
5509
- this.currentGeometry,
5934
+ const target = this.getDraggableMarkerTarget(e == null ? void 0 : e.target);
5935
+ if (!target || !this.currentGeometry) return;
5936
+ const feature = this.getFeatureForMarker(target);
5937
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
5938
+ const snapped = this.constrainPosition(
5939
+ {
5940
+ x: Number(target.left || 0),
5941
+ y: Number(target.top || 0)
5942
+ },
5943
+ geometry,
5510
5944
  feature
5511
5945
  );
5512
- const p = new import_fabric4.Point(target.left, target.top);
5513
- const markerStrokeWidth = (target.strokeWidth || 2) * (target.scaleX || 1);
5514
- const minDim = Math.min(
5515
- target.getScaledWidth(),
5516
- target.getScaledHeight()
5517
- );
5518
- const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
5519
- const snapped = this.constrainPosition(p, geometry, limit, feature);
5520
5946
  target.set({
5521
5947
  left: snapped.x,
5522
5948
  top: snapped.y
5523
5949
  });
5950
+ target.setCoords();
5951
+ this.syncMarkerVisualsByTarget(target, snapped);
5524
5952
  };
5525
5953
  canvas.on("object:moving", this.handleMoving);
5526
5954
  }
5527
5955
  if (!this.handleModified) {
5528
5956
  this.handleModified = (e) => {
5529
- var _a, _b, _c;
5530
- const target = e.target;
5531
- if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return;
5532
- if ((_b = target.data) == null ? void 0 : _b.isGroup) {
5533
- const groupObj = target;
5534
- const indices = (_c = groupObj.data) == null ? void 0 : _c.indices;
5535
- if (!indices) return;
5536
- const groupCenter = new import_fabric4.Point(groupObj.left, groupObj.top);
5537
- const newFeatures = [...this.workingFeatures];
5538
- const { x, y } = this.currentGeometry;
5539
- groupObj.getObjects().forEach((child, i) => {
5540
- const originalIndex = indices[i];
5541
- const feature = this.workingFeatures[originalIndex];
5542
- const geometry = this.getGeometryForFeature(
5543
- this.currentGeometry,
5544
- feature
5545
- );
5546
- const { width, height } = geometry;
5547
- const layoutLeft = x - width / 2;
5548
- const layoutTop = y - height / 2;
5549
- const absX = groupCenter.x + (child.left || 0);
5550
- const absY = groupCenter.y + (child.top || 0);
5551
- const normalizedX = width > 0 ? (absX - layoutLeft) / width : 0.5;
5552
- const normalizedY = height > 0 ? (absY - layoutTop) / height : 0.5;
5553
- newFeatures[originalIndex] = {
5554
- ...newFeatures[originalIndex],
5555
- x: normalizedX,
5556
- y: normalizedY
5557
- };
5558
- });
5559
- this.setWorkingFeatures(newFeatures);
5560
- this.hasWorkingChanges = true;
5561
- this.emitWorkingChange();
5957
+ var _a;
5958
+ const target = this.getDraggableMarkerTarget(e == null ? void 0 : e.target);
5959
+ if (!target) return;
5960
+ if ((_a = target.data) == null ? void 0 : _a.isGroup) {
5961
+ this.syncGroupFromCanvas(target);
5562
5962
  } else {
5563
5963
  this.syncFeatureFromCanvas(target);
5564
5964
  }
@@ -5567,6 +5967,7 @@ var FeatureTool = class {
5567
5967
  }
5568
5968
  }
5569
5969
  teardown() {
5970
+ var _a;
5570
5971
  if (!this.canvasService) return;
5571
5972
  const canvas = this.canvasService.canvas;
5572
5973
  if (this.handleMoving) {
@@ -5584,14 +5985,25 @@ var FeatureTool = class {
5584
5985
  );
5585
5986
  this.handleSceneGeometryChange = null;
5586
5987
  }
5587
- const objects = canvas.getObjects().filter((obj) => {
5588
- var _a;
5589
- return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
5590
- });
5591
- objects.forEach((obj) => canvas.remove(obj));
5592
- this.canvasService.requestRenderAll();
5988
+ this.renderSeq += 1;
5989
+ this.specs = [];
5990
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
5991
+ this.renderProducerDisposable = void 0;
5992
+ void this.canvasService.flushRenderFromProducers();
5993
+ }
5994
+ getDraggableMarkerTarget(target) {
5995
+ var _a, _b;
5996
+ if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return null;
5997
+ if (((_b = target.data) == null ? void 0 : _b.markerRole) !== "handle") return null;
5998
+ return target;
5999
+ }
6000
+ getFeatureForMarker(target) {
6001
+ const data = (target == null ? void 0 : target.data) || {};
6002
+ const index = data.isGroup ? this.toFeatureIndex(data.anchorIndex) : this.toFeatureIndex(data.index);
6003
+ if (index === null) return void 0;
6004
+ return this.workingFeatures[index];
5593
6005
  }
5594
- constrainPosition(p, geometry, limit, feature) {
6006
+ constrainPosition(p, geometry, feature) {
5595
6007
  var _a;
5596
6008
  if (!feature) {
5597
6009
  return { x: p.x, y: p.y };
@@ -5622,231 +6034,374 @@ var FeatureTool = class {
5622
6034
  y: minY + constrained.y * geometry.height
5623
6035
  };
5624
6036
  }
6037
+ toNormalizedPoint(point, geometry) {
6038
+ const left = geometry.x - geometry.width / 2;
6039
+ const top = geometry.y - geometry.height / 2;
6040
+ return {
6041
+ x: geometry.width > 0 ? (point.x - left) / geometry.width : 0.5,
6042
+ y: geometry.height > 0 ? (point.y - top) / geometry.height : 0.5
6043
+ };
6044
+ }
5625
6045
  syncFeatureFromCanvas(target) {
5626
6046
  var _a;
5627
- if (!this.currentGeometry || !this.context) return;
5628
- const index = (_a = target.data) == null ? void 0 : _a.index;
5629
- if (index === void 0 || index < 0 || index >= this.workingFeatures.length)
5630
- return;
6047
+ if (!this.currentGeometry) return;
6048
+ const index = this.toFeatureIndex((_a = target.data) == null ? void 0 : _a.index);
6049
+ if (index === null || index >= this.workingFeatures.length) return;
5631
6050
  const feature = this.workingFeatures[index];
5632
6051
  const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
5633
- const { width, height, x, y } = geometry;
5634
- const left = x - width / 2;
5635
- const top = y - height / 2;
5636
- const normalizedX = width > 0 ? (target.left - left) / width : 0.5;
5637
- const normalizedY = height > 0 ? (target.top - top) / height : 0.5;
6052
+ const normalized = this.toNormalizedPoint(
6053
+ {
6054
+ x: Number(target.left || 0),
6055
+ y: Number(target.top || 0)
6056
+ },
6057
+ geometry
6058
+ );
5638
6059
  const updatedFeature = {
5639
6060
  ...feature,
5640
- x: normalizedX,
5641
- y: normalizedY
5642
- // Could also update rotation if we allowed rotating markers
6061
+ x: normalized.x,
6062
+ y: normalized.y
5643
6063
  };
5644
- const newFeatures = [...this.workingFeatures];
5645
- newFeatures[index] = updatedFeature;
5646
- this.setWorkingFeatures(newFeatures);
6064
+ const next = [...this.workingFeatures];
6065
+ next[index] = updatedFeature;
6066
+ this.setWorkingFeatures(next);
5647
6067
  this.hasWorkingChanges = true;
5648
6068
  this.emitWorkingChange();
5649
6069
  }
5650
- redraw() {
5651
- if (!this.canvasService || !this.currentGeometry) return;
5652
- const canvas = this.canvasService.canvas;
5653
- const geometry = this.currentGeometry;
5654
- const existing = canvas.getObjects().filter((obj) => {
5655
- var _a;
5656
- return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
6070
+ syncGroupFromCanvas(target) {
6071
+ var _a, _b;
6072
+ if (!this.currentGeometry) return;
6073
+ const indices = this.readGroupIndices((_a = target.data) == null ? void 0 : _a.indices);
6074
+ if (indices.length === 0) return;
6075
+ const offsets = this.readGroupMemberOffsets((_b = target.data) == null ? void 0 : _b.memberOffsets, indices);
6076
+ const anchorCenter = {
6077
+ x: Number(target.left || 0),
6078
+ y: Number(target.top || 0)
6079
+ };
6080
+ const next = [...this.workingFeatures];
6081
+ let changed = false;
6082
+ offsets.forEach((entry) => {
6083
+ const index = entry.index;
6084
+ if (index < 0 || index >= next.length) return;
6085
+ const feature = next[index];
6086
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
6087
+ const normalized = this.toNormalizedPoint(
6088
+ {
6089
+ x: anchorCenter.x + entry.dx,
6090
+ y: anchorCenter.y + entry.dy
6091
+ },
6092
+ geometry
6093
+ );
6094
+ if (feature.x !== normalized.x || feature.y !== normalized.y) {
6095
+ next[index] = {
6096
+ ...feature,
6097
+ x: normalized.x,
6098
+ y: normalized.y
6099
+ };
6100
+ changed = true;
6101
+ }
5657
6102
  });
5658
- existing.forEach((obj) => canvas.remove(obj));
5659
- if (!this.workingFeatures || this.workingFeatures.length === 0) {
5660
- this.canvasService.requestRenderAll();
5661
- return;
6103
+ if (!changed) return;
6104
+ this.setWorkingFeatures(next);
6105
+ this.hasWorkingChanges = true;
6106
+ this.emitWorkingChange();
6107
+ }
6108
+ redraw(options = {}) {
6109
+ void this.redrawAsync(options);
6110
+ }
6111
+ async redrawAsync(options = {}) {
6112
+ if (!this.canvasService) return;
6113
+ const seq = ++this.renderSeq;
6114
+ this.specs = this.buildFeatureSpecs();
6115
+ if (seq !== this.renderSeq) return;
6116
+ await this.canvasService.flushRenderFromProducers();
6117
+ if (seq !== this.renderSeq) return;
6118
+ this.syncOverlayOrder();
6119
+ if (options.enforceConstraints) {
6120
+ this.enforceConstraints();
5662
6121
  }
5663
- const scale = geometry.scale || 1;
5664
- const finalScale = scale;
5665
- const groups = {};
6122
+ }
6123
+ syncOverlayOrder() {
6124
+ if (!this.canvasService) return;
6125
+ this.canvasService.bringLayerToFront(FEATURE_OVERLAY_LAYER_ID);
6126
+ this.canvasService.bringLayerToFront("ruler-overlay");
6127
+ }
6128
+ buildFeatureSpecs() {
6129
+ if (!this.currentGeometry || this.workingFeatures.length === 0) {
6130
+ return [];
6131
+ }
6132
+ const groups = /* @__PURE__ */ new Map();
5666
6133
  const singles = [];
5667
- this.workingFeatures.forEach((f, i) => {
5668
- if (f.groupId) {
5669
- if (!groups[f.groupId]) groups[f.groupId] = [];
5670
- groups[f.groupId].push({ feature: f, index: i });
5671
- } else {
5672
- singles.push({ feature: f, index: i });
6134
+ this.workingFeatures.forEach((feature, index) => {
6135
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
6136
+ const position = resolveFeaturePosition(feature, geometry);
6137
+ const scale = geometry.scale || 1;
6138
+ const marker = {
6139
+ feature,
6140
+ index,
6141
+ position,
6142
+ geometry,
6143
+ scale
6144
+ };
6145
+ if (feature.groupId) {
6146
+ const list = groups.get(feature.groupId) || [];
6147
+ list.push(marker);
6148
+ groups.set(feature.groupId, list);
6149
+ return;
5673
6150
  }
6151
+ singles.push(marker);
6152
+ });
6153
+ const specs = [];
6154
+ singles.forEach((marker) => {
6155
+ this.appendMarkerSpecs(specs, marker, {
6156
+ markerRole: "handle",
6157
+ isGroup: false
6158
+ });
6159
+ });
6160
+ groups.forEach((members, groupId) => {
6161
+ if (!members.length) return;
6162
+ const anchor = members[0];
6163
+ const memberOffsets = members.map((member) => ({
6164
+ index: member.index,
6165
+ dx: member.position.x - anchor.position.x,
6166
+ dy: member.position.y - anchor.position.y
6167
+ }));
6168
+ const indices = members.map((member) => member.index);
6169
+ members.filter((member) => member.index !== anchor.index).forEach((member) => {
6170
+ this.appendMarkerSpecs(specs, member, {
6171
+ markerRole: "member",
6172
+ isGroup: false,
6173
+ groupId
6174
+ });
6175
+ });
6176
+ this.appendMarkerSpecs(specs, anchor, {
6177
+ markerRole: "handle",
6178
+ isGroup: true,
6179
+ groupId,
6180
+ indices,
6181
+ anchorIndex: anchor.index,
6182
+ memberOffsets
6183
+ });
5674
6184
  });
5675
- const createMarkerShape = (feature, pos) => {
5676
- const featureScale = scale;
5677
- const visualWidth = (feature.width || 10) * featureScale;
5678
- const visualHeight = (feature.height || 10) * featureScale;
5679
- const visualRadius = (feature.radius || 0) * featureScale;
5680
- const color = feature.color || (feature.operation === "add" ? "#00FF00" : "#FF0000");
5681
- const strokeDash = feature.strokeDash || (feature.operation === "subtract" ? [4, 4] : void 0);
5682
- let shape;
5683
- if (feature.shape === "rect") {
5684
- shape = new import_fabric4.Rect({
6185
+ return specs;
6186
+ }
6187
+ appendMarkerSpecs(specs, marker, options) {
6188
+ var _a, _b, _c, _d, _e;
6189
+ const { feature, index, position, scale, geometry } = marker;
6190
+ const baseRadius = feature.shape === "circle" ? (_a = feature.radius) != null ? _a : DEFAULT_CIRCLE_RADIUS : (_b = feature.radius) != null ? _b : 0;
6191
+ const baseWidth = feature.shape === "circle" ? baseRadius * 2 : (_c = feature.width) != null ? _c : DEFAULT_RECT_SIZE;
6192
+ const baseHeight = feature.shape === "circle" ? baseRadius * 2 : (_d = feature.height) != null ? _d : DEFAULT_RECT_SIZE;
6193
+ const visualWidth = baseWidth * scale;
6194
+ const visualHeight = baseHeight * scale;
6195
+ const visualRadius = baseRadius * scale;
6196
+ const color = feature.color || (feature.operation === "add" ? "#00FF00" : "#FF0000");
6197
+ const strokeDash = feature.strokeDash || (feature.operation === "subtract" ? [4, 4] : void 0);
6198
+ const interactive = options.markerRole === "handle";
6199
+ const baseData = this.buildMarkerData(marker, options);
6200
+ const commonProps = {
6201
+ visible: this.isToolActive,
6202
+ selectable: interactive && this.isToolActive,
6203
+ evented: interactive && this.isToolActive,
6204
+ hasControls: false,
6205
+ hasBorders: false,
6206
+ hoverCursor: interactive ? "move" : "default",
6207
+ lockRotation: true,
6208
+ lockScalingX: true,
6209
+ lockScalingY: true,
6210
+ fill: "transparent",
6211
+ stroke: color,
6212
+ strokeWidth: FEATURE_STROKE_WIDTH,
6213
+ strokeDashArray: strokeDash,
6214
+ originX: "center",
6215
+ originY: "center",
6216
+ left: position.x,
6217
+ top: position.y,
6218
+ angle: feature.rotation || 0
6219
+ };
6220
+ const markerId = this.markerId(index);
6221
+ if (feature.shape === "rect") {
6222
+ specs.push({
6223
+ id: markerId,
6224
+ type: "rect",
6225
+ space: "screen",
6226
+ data: baseData,
6227
+ props: {
6228
+ ...commonProps,
5685
6229
  width: visualWidth,
5686
6230
  height: visualHeight,
5687
6231
  rx: visualRadius,
5688
- ry: visualRadius,
5689
- fill: "transparent",
5690
- stroke: color,
5691
- strokeWidth: 2,
5692
- strokeDashArray: strokeDash,
5693
- originX: "center",
5694
- originY: "center",
5695
- left: pos.x,
5696
- top: pos.y
5697
- });
5698
- } else {
5699
- shape = new import_fabric4.Circle({
5700
- radius: visualRadius || 5 * finalScale,
5701
- fill: "transparent",
5702
- stroke: color,
5703
- strokeWidth: 2,
5704
- strokeDashArray: strokeDash,
5705
- originX: "center",
5706
- originY: "center",
5707
- left: pos.x,
5708
- top: pos.y
5709
- });
5710
- }
5711
- if (feature.rotation) {
5712
- shape.rotate(feature.rotation);
6232
+ ry: visualRadius
6233
+ }
6234
+ });
6235
+ } else {
6236
+ specs.push({
6237
+ id: markerId,
6238
+ type: "rect",
6239
+ space: "screen",
6240
+ data: baseData,
6241
+ props: {
6242
+ ...commonProps,
6243
+ width: visualWidth,
6244
+ height: visualHeight,
6245
+ rx: visualRadius,
6246
+ ry: visualRadius
6247
+ }
6248
+ });
6249
+ }
6250
+ if (((_e = feature.bridge) == null ? void 0 : _e.type) === "vertical") {
6251
+ const featureTopY = position.y - visualHeight / 2;
6252
+ const dielineTopY = geometry.y - geometry.height / 2;
6253
+ const bridgeHeight = Math.max(0, featureTopY - dielineTopY);
6254
+ if (bridgeHeight <= 1e-3) {
6255
+ return;
5713
6256
  }
5714
- if (feature.bridge && feature.bridge.type === "vertical") {
5715
- const bridgeIndicator = new import_fabric4.Rect({
6257
+ specs.push({
6258
+ id: this.bridgeIndicatorId(index),
6259
+ type: "rect",
6260
+ space: "screen",
6261
+ data: {
6262
+ ...baseData,
6263
+ markerRole: "indicator",
6264
+ markerOffsetX: 0,
6265
+ markerOffsetY: -visualHeight / 2
6266
+ },
6267
+ props: {
6268
+ visible: this.isToolActive,
6269
+ selectable: false,
6270
+ evented: false,
5716
6271
  width: visualWidth,
5717
- height: 100 * featureScale,
5718
- // Arbitrary long length to show direction
6272
+ height: bridgeHeight,
5719
6273
  fill: "transparent",
5720
6274
  stroke: "#888",
5721
6275
  strokeWidth: 1,
5722
6276
  strokeDashArray: [2, 2],
5723
- originX: "center",
5724
- originY: "bottom",
5725
- // Anchor at bottom so it extends up
5726
- left: pos.x,
5727
- top: pos.y - visualHeight / 2,
5728
- // Start from top of feature
5729
6277
  opacity: 0.5,
5730
- selectable: false,
5731
- evented: false
5732
- });
5733
- const group = new import_fabric4.Group([bridgeIndicator, shape], {
5734
6278
  originX: "center",
5735
- originY: "center",
5736
- left: pos.x,
5737
- top: pos.y
5738
- });
5739
- return group;
5740
- }
5741
- return shape;
5742
- };
5743
- singles.forEach(({ feature, index }) => {
5744
- const geometry2 = this.getGeometryForFeature(
5745
- this.currentGeometry,
5746
- feature
5747
- );
5748
- const pos = resolveFeaturePosition(feature, geometry2);
5749
- const marker = createMarkerShape(feature, pos);
5750
- marker.set({
5751
- visible: this.isToolActive,
5752
- selectable: this.isToolActive,
5753
- evented: this.isToolActive,
5754
- hasControls: false,
5755
- hasBorders: false,
5756
- hoverCursor: "move",
5757
- lockRotation: true,
5758
- lockScalingX: true,
5759
- lockScalingY: true,
5760
- data: { type: "feature-marker", index, isGroup: false }
5761
- });
5762
- canvas.add(marker);
5763
- canvas.bringObjectToFront(marker);
5764
- });
5765
- Object.keys(groups).forEach((groupId) => {
5766
- const members = groups[groupId];
5767
- if (members.length === 0) return;
5768
- const shapes = members.map(({ feature }) => {
5769
- const geometry2 = this.getGeometryForFeature(
5770
- this.currentGeometry,
5771
- feature
5772
- );
5773
- const pos = resolveFeaturePosition(feature, geometry2);
5774
- return createMarkerShape(feature, pos);
6279
+ originY: "bottom",
6280
+ left: position.x,
6281
+ top: position.y - visualHeight / 2
6282
+ }
5775
6283
  });
5776
- const groupObj = new import_fabric4.Group(shapes, {
5777
- visible: this.isToolActive,
5778
- selectable: this.isToolActive,
5779
- evented: this.isToolActive,
5780
- hasControls: false,
5781
- hasBorders: false,
5782
- hoverCursor: "move",
5783
- lockRotation: true,
5784
- lockScalingX: true,
5785
- lockScalingY: true,
5786
- subTargetCheck: true,
5787
- // Allow events to pass through if needed, but we treat as one
5788
- interactive: false,
5789
- // Children not interactive
5790
- // @ts-ignore
5791
- data: {
5792
- type: "feature-marker",
5793
- isGroup: true,
5794
- groupId,
5795
- indices: members.map((m) => m.index)
6284
+ }
6285
+ }
6286
+ buildMarkerData(marker, options) {
6287
+ const data = {
6288
+ type: "feature-marker",
6289
+ index: marker.index,
6290
+ featureId: marker.feature.id,
6291
+ markerRole: options.markerRole,
6292
+ markerOffsetX: 0,
6293
+ markerOffsetY: 0,
6294
+ isGroup: options.isGroup
6295
+ };
6296
+ if (options.groupId) data.groupId = options.groupId;
6297
+ if (options.indices) data.indices = options.indices;
6298
+ if (options.anchorIndex !== void 0) data.anchorIndex = options.anchorIndex;
6299
+ if (options.memberOffsets) data.memberOffsets = options.memberOffsets;
6300
+ return data;
6301
+ }
6302
+ markerId(index) {
6303
+ return `feature.marker.${index}`;
6304
+ }
6305
+ bridgeIndicatorId(index) {
6306
+ return `feature.marker.${index}.bridge`;
6307
+ }
6308
+ toFeatureIndex(value) {
6309
+ const numeric = Number(value);
6310
+ if (!Number.isInteger(numeric) || numeric < 0) return null;
6311
+ return numeric;
6312
+ }
6313
+ readGroupIndices(raw) {
6314
+ if (!Array.isArray(raw)) return [];
6315
+ return raw.map((value) => this.toFeatureIndex(value)).filter((value) => value !== null);
6316
+ }
6317
+ readGroupMemberOffsets(raw, fallbackIndices = []) {
6318
+ if (Array.isArray(raw)) {
6319
+ const parsed = raw.map((entry) => {
6320
+ const index = this.toFeatureIndex(entry == null ? void 0 : entry.index);
6321
+ const dx = Number(entry == null ? void 0 : entry.dx);
6322
+ const dy = Number(entry == null ? void 0 : entry.dy);
6323
+ if (index === null || !Number.isFinite(dx) || !Number.isFinite(dy)) {
6324
+ return null;
5796
6325
  }
6326
+ return { index, dx, dy };
6327
+ }).filter((value) => !!value);
6328
+ if (parsed.length > 0) return parsed;
6329
+ }
6330
+ return fallbackIndices.map((index) => ({ index, dx: 0, dy: 0 }));
6331
+ }
6332
+ syncMarkerVisualsByTarget(target, center) {
6333
+ var _a, _b, _c, _d, _e, _f;
6334
+ if ((_a = target.data) == null ? void 0 : _a.isGroup) {
6335
+ const indices = this.readGroupIndices((_b = target.data) == null ? void 0 : _b.indices);
6336
+ const offsets = this.readGroupMemberOffsets((_c = target.data) == null ? void 0 : _c.memberOffsets, indices);
6337
+ offsets.forEach((entry) => {
6338
+ this.syncMarkerVisualObjectsToCenter(entry.index, {
6339
+ x: center.x + entry.dx,
6340
+ y: center.y + entry.dy
6341
+ });
5797
6342
  });
5798
- canvas.add(groupObj);
5799
- canvas.bringObjectToFront(groupObj);
6343
+ (_d = this.canvasService) == null ? void 0 : _d.requestRenderAll();
6344
+ return;
6345
+ }
6346
+ const index = this.toFeatureIndex((_e = target.data) == null ? void 0 : _e.index);
6347
+ if (index === null) return;
6348
+ this.syncMarkerVisualObjectsToCenter(index, center);
6349
+ (_f = this.canvasService) == null ? void 0 : _f.requestRenderAll();
6350
+ }
6351
+ syncMarkerVisualObjectsToCenter(index, center) {
6352
+ if (!this.canvasService) return;
6353
+ const markers = this.canvasService.canvas.getObjects().filter(
6354
+ (obj) => {
6355
+ var _a, _b;
6356
+ 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;
6357
+ }
6358
+ );
6359
+ markers.forEach((marker) => {
6360
+ var _a, _b;
6361
+ const offsetX = Number(((_a = marker == null ? void 0 : marker.data) == null ? void 0 : _a.markerOffsetX) || 0);
6362
+ const offsetY = Number(((_b = marker == null ? void 0 : marker.data) == null ? void 0 : _b.markerOffsetY) || 0);
6363
+ marker.set({
6364
+ left: center.x + offsetX,
6365
+ top: center.y + offsetY
6366
+ });
6367
+ marker.setCoords();
5800
6368
  });
5801
- this.canvasService.requestRenderAll();
5802
6369
  }
5803
6370
  enforceConstraints() {
5804
6371
  if (!this.canvasService || !this.currentGeometry) return;
5805
- const canvas = this.canvasService.canvas;
5806
- const markers = canvas.getObjects().filter((obj) => {
5807
- var _a;
5808
- return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
5809
- });
5810
- markers.forEach((marker) => {
5811
- var _a, _b, _c;
5812
- let feature;
5813
- if ((_a = marker.data) == null ? void 0 : _a.isGroup) {
5814
- const indices = (_b = marker.data) == null ? void 0 : _b.indices;
5815
- if (indices && indices.length > 0) {
5816
- feature = this.workingFeatures[indices[0]];
5817
- }
5818
- } else {
5819
- const index = (_c = marker.data) == null ? void 0 : _c.index;
5820
- if (index !== void 0) {
5821
- feature = this.workingFeatures[index];
5822
- }
6372
+ const handles = this.canvasService.canvas.getObjects().filter(
6373
+ (obj) => {
6374
+ var _a, _b;
6375
+ 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";
5823
6376
  }
5824
- const geometry = this.getGeometryForFeature(
5825
- this.currentGeometry,
5826
- feature
5827
- );
5828
- const markerStrokeWidth = (marker.strokeWidth || 2) * (marker.scaleX || 1);
5829
- const minDim = Math.min(
5830
- marker.getScaledWidth(),
5831
- marker.getScaledHeight()
5832
- );
5833
- const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
6377
+ );
6378
+ handles.forEach((marker) => {
6379
+ const feature = this.getFeatureForMarker(marker);
6380
+ if (!feature) return;
6381
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
5834
6382
  const snapped = this.constrainPosition(
5835
- new import_fabric4.Point(marker.left, marker.top),
6383
+ {
6384
+ x: Number(marker.left || 0),
6385
+ y: Number(marker.top || 0)
6386
+ },
5836
6387
  geometry,
5837
- limit,
5838
6388
  feature
5839
6389
  );
5840
6390
  marker.set({ left: snapped.x, top: snapped.y });
5841
6391
  marker.setCoords();
6392
+ this.syncMarkerVisualsByTarget(marker, snapped);
5842
6393
  });
5843
- canvas.requestRenderAll();
6394
+ this.canvasService.canvas.requestRenderAll();
5844
6395
  }
5845
6396
  };
5846
6397
 
5847
6398
  // src/extensions/film.ts
5848
6399
  var import_core6 = require("@pooder/core");
5849
- var import_fabric5 = require("fabric");
6400
+ var import_fabric4 = require("fabric");
6401
+ var FILM_LAYER_ID = "overlay";
6402
+ var FILM_IMAGE_ID = "film-image";
6403
+ var DEFAULT_WIDTH2 = 800;
6404
+ var DEFAULT_HEIGHT2 = 600;
5850
6405
  var FilmTool = class {
5851
6406
  constructor(options) {
5852
6407
  this.id = "pooder.kit.film";
@@ -5855,17 +6410,38 @@ var FilmTool = class {
5855
6410
  };
5856
6411
  this.url = "";
5857
6412
  this.opacity = 0.5;
6413
+ this.specs = [];
6414
+ this.renderSeq = 0;
6415
+ this.renderImageUrl = "";
6416
+ this.sourceSizeBySrc = /* @__PURE__ */ new Map();
6417
+ this.pendingSizeBySrc = /* @__PURE__ */ new Map();
6418
+ this.onCanvasResized = () => {
6419
+ this.updateFilm();
6420
+ };
5858
6421
  if (options) {
5859
6422
  Object.assign(this, options);
5860
6423
  }
5861
6424
  }
5862
6425
  activate(context) {
6426
+ var _a;
5863
6427
  this.canvasService = context.services.get("CanvasService");
5864
6428
  if (!this.canvasService) {
5865
6429
  console.warn("CanvasService not found for FilmTool");
5866
6430
  return;
5867
6431
  }
5868
- const configService = context.services.get("ConfigurationService");
6432
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
6433
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
6434
+ this.id,
6435
+ () => ({
6436
+ layerSpecs: {
6437
+ [FILM_LAYER_ID]: this.specs
6438
+ }
6439
+ }),
6440
+ { priority: 500 }
6441
+ );
6442
+ const configService = context.services.get(
6443
+ "ConfigurationService"
6444
+ );
5869
6445
  if (configService) {
5870
6446
  this.url = configService.get("film.url", this.url);
5871
6447
  this.opacity = configService.get("film.opacity", this.opacity);
@@ -5882,21 +6458,21 @@ var FilmTool = class {
5882
6458
  }
5883
6459
  });
5884
6460
  }
5885
- this.initLayer();
6461
+ context.eventBus.on("canvas:resized", this.onCanvasResized);
5886
6462
  this.updateFilm();
5887
6463
  }
5888
6464
  deactivate(context) {
5889
- if (this.canvasService) {
5890
- const layer = this.canvasService.getLayer("overlay");
5891
- if (layer) {
5892
- const img = this.canvasService.getObject("film-image", "overlay");
5893
- if (img) {
5894
- layer.remove(img);
5895
- this.canvasService.requestRenderAll();
5896
- }
5897
- }
5898
- this.canvasService = void 0;
5899
- }
6465
+ var _a;
6466
+ context.eventBus.off("canvas:resized", this.onCanvasResized);
6467
+ this.renderSeq += 1;
6468
+ this.specs = [];
6469
+ this.renderImageUrl = "";
6470
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
6471
+ this.renderProducerDisposable = void 0;
6472
+ if (!this.canvasService) return;
6473
+ void this.canvasService.flushRenderFromProducers();
6474
+ this.canvasService.requestRenderAll();
6475
+ this.canvasService = void 0;
5900
6476
  }
5901
6477
  contribute() {
5902
6478
  return {
@@ -5932,73 +6508,108 @@ var FilmTool = class {
5932
6508
  ]
5933
6509
  };
5934
6510
  }
5935
- initLayer() {
5936
- if (!this.canvasService) return;
5937
- let overlayLayer = this.canvasService.getLayer("overlay");
5938
- if (!overlayLayer) {
5939
- const width = this.canvasService.canvas.width || 800;
5940
- const height = this.canvasService.canvas.height || 600;
5941
- const layer = this.canvasService.createLayer("overlay", {
5942
- width,
5943
- height,
5944
- left: 0,
5945
- top: 0,
5946
- originX: "left",
5947
- originY: "top",
5948
- selectable: false,
5949
- evented: false,
5950
- subTargetCheck: false,
5951
- interactive: false
5952
- });
5953
- this.canvasService.canvas.bringObjectToFront(layer);
5954
- }
6511
+ getViewportSize() {
6512
+ var _a, _b;
6513
+ const width = Number(((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 0);
6514
+ const height = Number(((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 0);
6515
+ return {
6516
+ width: width > 0 ? width : DEFAULT_WIDTH2,
6517
+ height: height > 0 ? height : DEFAULT_HEIGHT2
6518
+ };
5955
6519
  }
5956
- async updateFilm() {
5957
- if (!this.canvasService) return;
5958
- const layer = this.canvasService.getLayer("overlay");
5959
- if (!layer) {
5960
- console.warn("[FilmTool] Overlay layer not found");
5961
- return;
5962
- }
5963
- const { url, opacity } = this;
5964
- if (!url) {
5965
- const img2 = this.canvasService.getObject("film-image", "overlay");
5966
- if (img2) {
5967
- layer.remove(img2);
5968
- this.canvasService.requestRenderAll();
5969
- }
5970
- return;
6520
+ clampOpacity(value) {
6521
+ return Math.max(0, Math.min(1, Number(value)));
6522
+ }
6523
+ buildFilmSpecs(imageUrl, opacity) {
6524
+ if (!imageUrl) {
6525
+ return [];
5971
6526
  }
5972
- const width = this.canvasService.canvas.width || 800;
5973
- const height = this.canvasService.canvas.height || 600;
5974
- let img = this.canvasService.getObject("film-image", "overlay");
5975
- try {
5976
- if (img) {
5977
- if (img.getSrc() !== url) {
5978
- await img.setSrc(url);
5979
- }
5980
- img.set({ opacity });
5981
- } else {
5982
- img = await import_fabric5.FabricImage.fromURL(url, { crossOrigin: "anonymous" });
5983
- img.scaleToWidth(width);
5984
- if (img.getScaledHeight() < height) img.scaleToHeight(height);
5985
- img.set({
5986
- originX: "left",
5987
- originY: "top",
6527
+ const { width, height } = this.getViewportSize();
6528
+ const sourceSize = this.sourceSizeBySrc.get(imageUrl);
6529
+ const sourceWidth = Math.max(1, Number((sourceSize == null ? void 0 : sourceSize.width) || width));
6530
+ const sourceHeight = Math.max(1, Number((sourceSize == null ? void 0 : sourceSize.height) || height));
6531
+ const coverScale = Math.max(width / sourceWidth, height / sourceHeight);
6532
+ return [
6533
+ {
6534
+ id: FILM_IMAGE_ID,
6535
+ type: "image",
6536
+ src: imageUrl,
6537
+ space: "screen",
6538
+ data: {
6539
+ id: FILM_IMAGE_ID,
6540
+ layerId: FILM_LAYER_ID,
6541
+ type: "film-image"
6542
+ },
6543
+ props: {
5988
6544
  left: 0,
5989
6545
  top: 0,
5990
- opacity,
6546
+ originX: "left",
6547
+ originY: "top",
6548
+ opacity: this.clampOpacity(opacity),
6549
+ scaleX: coverScale,
6550
+ scaleY: coverScale,
5991
6551
  selectable: false,
5992
6552
  evented: false,
5993
- data: { id: "film-image" }
5994
- });
5995
- layer.add(img);
6553
+ excludeFromExport: true
6554
+ }
6555
+ }
6556
+ ];
6557
+ }
6558
+ async ensureImageSize(src) {
6559
+ if (!src) return null;
6560
+ const cached = this.sourceSizeBySrc.get(src);
6561
+ if (cached) return cached;
6562
+ const pending = this.pendingSizeBySrc.get(src);
6563
+ if (pending) {
6564
+ return pending;
6565
+ }
6566
+ const task = this.loadImageSize(src);
6567
+ this.pendingSizeBySrc.set(src, task);
6568
+ try {
6569
+ return await task;
6570
+ } finally {
6571
+ if (this.pendingSizeBySrc.get(src) === task) {
6572
+ this.pendingSizeBySrc.delete(src);
6573
+ }
6574
+ }
6575
+ }
6576
+ async loadImageSize(src) {
6577
+ try {
6578
+ const image = await import_fabric4.FabricImage.fromURL(src, {
6579
+ crossOrigin: "anonymous"
6580
+ });
6581
+ const width = Number((image == null ? void 0 : image.width) || 0);
6582
+ const height = Number((image == null ? void 0 : image.height) || 0);
6583
+ if (width > 0 && height > 0) {
6584
+ const size = { width, height };
6585
+ this.sourceSizeBySrc.set(src, size);
6586
+ return size;
5996
6587
  }
5997
- this.canvasService.requestRenderAll();
5998
6588
  } catch (error) {
5999
- console.error("[FilmTool] Failed to load film image", url, error);
6589
+ console.error("[FilmTool] Failed to load film image", src, error);
6590
+ }
6591
+ return null;
6592
+ }
6593
+ updateFilm() {
6594
+ void this.updateFilmAsync();
6595
+ }
6596
+ async updateFilmAsync() {
6597
+ if (!this.canvasService) return;
6598
+ const seq = ++this.renderSeq;
6599
+ const nextUrl = String(this.url || "").trim();
6600
+ if (!nextUrl) {
6601
+ this.renderImageUrl = "";
6602
+ } else if (nextUrl !== this.renderImageUrl) {
6603
+ const loaded = await this.ensureImageSize(nextUrl);
6604
+ if (seq !== this.renderSeq) return;
6605
+ if (loaded) {
6606
+ this.renderImageUrl = nextUrl;
6607
+ }
6000
6608
  }
6001
- layer.dirty = true;
6609
+ this.specs = this.buildFilmSpecs(this.renderImageUrl, this.opacity);
6610
+ await this.canvasService.flushRenderFromProducers();
6611
+ if (seq !== this.renderSeq) return;
6612
+ this.canvasService.bringLayerToFront(FILM_LAYER_ID);
6002
6613
  this.canvasService.requestRenderAll();
6003
6614
  }
6004
6615
  };
@@ -6095,19 +6706,37 @@ var MirrorTool = class {
6095
6706
 
6096
6707
  // src/extensions/ruler.ts
6097
6708
  var import_core8 = require("@pooder/core");
6098
- var import_fabric6 = require("fabric");
6709
+ var RULER_LAYER_ID = "ruler-overlay";
6710
+ var EXTENSION_LINE_LENGTH = 5;
6711
+ var MIN_ARROW_SIZE = 4;
6712
+ var THICKNESS_TO_STROKE_WIDTH_RATIO = 20;
6713
+ var DEFAULT_THICKNESS = 20;
6714
+ var DEFAULT_GAP = 45;
6715
+ var DEFAULT_FONT_SIZE = 10;
6716
+ var DEFAULT_BACKGROUND_COLOR = "#f0f0f0";
6717
+ var DEFAULT_TEXT_COLOR = "#333333";
6718
+ var DEFAULT_LINE_COLOR = "#999999";
6719
+ var RULER_THICKNESS_MIN = 10;
6720
+ var RULER_THICKNESS_MAX = 100;
6721
+ var RULER_GAP_MIN = 0;
6722
+ var RULER_GAP_MAX = 100;
6723
+ var RULER_FONT_SIZE_MIN = 8;
6724
+ var RULER_FONT_SIZE_MAX = 24;
6099
6725
  var RulerTool = class {
6100
6726
  constructor(options) {
6101
6727
  this.id = "pooder.kit.ruler";
6102
6728
  this.metadata = {
6103
6729
  name: "RulerTool"
6104
6730
  };
6105
- this.thickness = 20;
6106
- this.gap = 15;
6107
- this.backgroundColor = "#f0f0f0";
6108
- this.textColor = "#333333";
6109
- this.lineColor = "#999999";
6110
- this.fontSize = 10;
6731
+ this.thickness = DEFAULT_THICKNESS;
6732
+ this.gap = DEFAULT_GAP;
6733
+ this.backgroundColor = DEFAULT_BACKGROUND_COLOR;
6734
+ this.textColor = DEFAULT_TEXT_COLOR;
6735
+ this.lineColor = DEFAULT_LINE_COLOR;
6736
+ this.fontSize = DEFAULT_FONT_SIZE;
6737
+ this.renderSeq = 0;
6738
+ this.numericProps = /* @__PURE__ */ new Set(["thickness", "gap", "fontSize"]);
6739
+ this.specs = [];
6111
6740
  this.onCanvasResized = () => {
6112
6741
  this.updateRuler();
6113
6742
  };
@@ -6116,50 +6745,73 @@ var RulerTool = class {
6116
6745
  }
6117
6746
  }
6118
6747
  activate(context) {
6748
+ var _a;
6119
6749
  this.context = context;
6120
6750
  this.canvasService = context.services.get("CanvasService");
6121
6751
  if (!this.canvasService) {
6122
- console.warn("CanvasService not found for RulerTool");
6752
+ console.warn("[RulerTool] CanvasService not found.");
6123
6753
  return;
6124
6754
  }
6755
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
6756
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
6757
+ this.id,
6758
+ () => ({
6759
+ rootLayerSpecs: {
6760
+ [RULER_LAYER_ID]: this.specs
6761
+ },
6762
+ replaceRootLayerIds: [RULER_LAYER_ID]
6763
+ }),
6764
+ { priority: 400 }
6765
+ );
6125
6766
  const configService = context.services.get(
6126
6767
  "ConfigurationService"
6127
6768
  );
6128
6769
  if (configService) {
6129
- this.thickness = configService.get("ruler.thickness", this.thickness);
6130
- this.gap = configService.get("ruler.gap", this.gap);
6131
- this.backgroundColor = configService.get(
6132
- "ruler.backgroundColor",
6133
- this.backgroundColor
6134
- );
6135
- this.textColor = configService.get("ruler.textColor", this.textColor);
6136
- this.lineColor = configService.get("ruler.lineColor", this.lineColor);
6137
- this.fontSize = configService.get("ruler.fontSize", this.fontSize);
6770
+ this.syncConfig(configService);
6138
6771
  configService.onAnyChange((e) => {
6139
6772
  let shouldUpdate = false;
6140
6773
  if (e.key.startsWith("ruler.")) {
6141
6774
  const prop = e.key.split(".")[1];
6142
6775
  if (prop && prop in this) {
6143
- this[prop] = e.value;
6776
+ if (this.numericProps.has(prop)) {
6777
+ this[prop] = this.toFiniteNumber(
6778
+ e.value,
6779
+ this[prop]
6780
+ );
6781
+ } else {
6782
+ this[prop] = e.value;
6783
+ }
6144
6784
  shouldUpdate = true;
6785
+ this.log("config:update", {
6786
+ key: e.key,
6787
+ raw: e.value,
6788
+ normalized: this[prop]
6789
+ });
6145
6790
  }
6146
6791
  } else if (e.key.startsWith("size.")) {
6147
6792
  shouldUpdate = true;
6793
+ this.log("size:update", { key: e.key, value: e.value });
6148
6794
  }
6149
6795
  if (shouldUpdate) {
6150
6796
  this.updateRuler();
6151
6797
  }
6152
6798
  });
6153
6799
  }
6154
- this.createLayer();
6155
6800
  context.eventBus.on("canvas:resized", this.onCanvasResized);
6156
6801
  this.updateRuler();
6157
6802
  }
6158
6803
  deactivate(context) {
6804
+ var _a;
6159
6805
  context.eventBus.off("canvas:resized", this.onCanvasResized);
6160
- this.destroyLayer();
6806
+ this.specs = [];
6807
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
6808
+ this.renderProducerDisposable = void 0;
6809
+ if (this.canvasService) {
6810
+ void this.canvasService.flushRenderFromProducers();
6811
+ }
6161
6812
  this.canvasService = void 0;
6162
6813
  this.context = void 0;
6814
+ this.renderSeq = 0;
6163
6815
  }
6164
6816
  contribute() {
6165
6817
  return {
@@ -6168,43 +6820,43 @@ var RulerTool = class {
6168
6820
  id: "ruler.thickness",
6169
6821
  type: "number",
6170
6822
  label: "Thickness",
6171
- min: 10,
6172
- max: 100,
6173
- default: 20
6823
+ min: RULER_THICKNESS_MIN,
6824
+ max: RULER_THICKNESS_MAX,
6825
+ default: DEFAULT_THICKNESS
6174
6826
  },
6175
6827
  {
6176
6828
  id: "ruler.gap",
6177
6829
  type: "number",
6178
6830
  label: "Gap",
6179
- min: 0,
6180
- max: 100,
6181
- default: 15
6831
+ min: RULER_GAP_MIN,
6832
+ max: RULER_GAP_MAX,
6833
+ default: DEFAULT_GAP
6182
6834
  },
6183
6835
  {
6184
6836
  id: "ruler.backgroundColor",
6185
6837
  type: "color",
6186
6838
  label: "Background Color",
6187
- default: "#f0f0f0"
6839
+ default: DEFAULT_BACKGROUND_COLOR
6188
6840
  },
6189
6841
  {
6190
6842
  id: "ruler.textColor",
6191
6843
  type: "color",
6192
6844
  label: "Text Color",
6193
- default: "#333333"
6845
+ default: DEFAULT_TEXT_COLOR
6194
6846
  },
6195
6847
  {
6196
6848
  id: "ruler.lineColor",
6197
6849
  type: "color",
6198
6850
  label: "Line Color",
6199
- default: "#999999"
6851
+ default: DEFAULT_LINE_COLOR
6200
6852
  },
6201
6853
  {
6202
6854
  id: "ruler.fontSize",
6203
6855
  type: "number",
6204
6856
  label: "Font Size",
6205
- min: 8,
6206
- max: 24,
6207
- default: 10
6857
+ min: RULER_FONT_SIZE_MIN,
6858
+ max: RULER_FONT_SIZE_MAX,
6859
+ default: DEFAULT_FONT_SIZE
6208
6860
  }
6209
6861
  ],
6210
6862
  [import_core8.ContributionPointIds.COMMANDS]: [
@@ -6217,12 +6869,23 @@ var RulerTool = class {
6217
6869
  textColor: this.textColor,
6218
6870
  lineColor: this.lineColor,
6219
6871
  fontSize: this.fontSize,
6220
- thickness: this.thickness
6872
+ thickness: this.thickness,
6873
+ gap: this.gap
6221
6874
  };
6222
6875
  const newState = { ...oldState, ...theme };
6223
- if (JSON.stringify(newState) === JSON.stringify(oldState))
6876
+ if (JSON.stringify(newState) === JSON.stringify(oldState)) {
6224
6877
  return true;
6878
+ }
6225
6879
  Object.assign(this, newState);
6880
+ this.thickness = this.toFiniteNumber(
6881
+ this.thickness,
6882
+ DEFAULT_THICKNESS
6883
+ );
6884
+ this.gap = this.toFiniteNumber(this.gap, DEFAULT_GAP);
6885
+ this.fontSize = this.toFiniteNumber(
6886
+ this.fontSize,
6887
+ DEFAULT_FONT_SIZE
6888
+ );
6226
6889
  this.updateRuler();
6227
6890
  return true;
6228
6891
  }
@@ -6230,225 +6893,367 @@ var RulerTool = class {
6230
6893
  ]
6231
6894
  };
6232
6895
  }
6233
- getLayer() {
6234
- var _a;
6235
- return (_a = this.canvasService) == null ? void 0 : _a.getLayer("ruler-overlay");
6896
+ log(step, payload) {
6897
+ if (payload) {
6898
+ console.debug(`[RulerTool] ${step}`, payload);
6899
+ return;
6900
+ }
6901
+ console.debug(`[RulerTool] ${step}`);
6236
6902
  }
6237
- createLayer() {
6238
- if (!this.canvasService) return;
6239
- const canvas = this.canvasService.canvas;
6240
- const width = canvas.width || 800;
6241
- const height = canvas.height || 600;
6242
- const layer = this.canvasService.createLayer("ruler-overlay", {
6243
- width,
6244
- height,
6245
- selectable: false,
6246
- evented: false,
6247
- left: 0,
6248
- top: 0,
6249
- originX: "left",
6250
- originY: "top"
6903
+ syncConfig(configService) {
6904
+ this.thickness = this.toFiniteNumber(
6905
+ configService.get("ruler.thickness", this.thickness),
6906
+ DEFAULT_THICKNESS
6907
+ );
6908
+ this.gap = Math.max(
6909
+ 0,
6910
+ this.toFiniteNumber(
6911
+ configService.get("ruler.gap", this.gap),
6912
+ DEFAULT_GAP
6913
+ )
6914
+ );
6915
+ this.backgroundColor = configService.get(
6916
+ "ruler.backgroundColor",
6917
+ this.backgroundColor
6918
+ );
6919
+ this.textColor = configService.get("ruler.textColor", this.textColor);
6920
+ this.lineColor = configService.get("ruler.lineColor", this.lineColor);
6921
+ this.fontSize = this.toFiniteNumber(
6922
+ configService.get("ruler.fontSize", this.fontSize),
6923
+ DEFAULT_FONT_SIZE
6924
+ );
6925
+ this.log("config:loaded", {
6926
+ thickness: this.thickness,
6927
+ gap: this.gap,
6928
+ fontSize: this.fontSize,
6929
+ backgroundColor: this.backgroundColor,
6930
+ textColor: this.textColor,
6931
+ lineColor: this.lineColor
6251
6932
  });
6252
- canvas.bringObjectToFront(layer);
6253
6933
  }
6254
- destroyLayer() {
6255
- if (!this.canvasService) return;
6256
- const layer = this.getLayer();
6257
- if (layer) {
6258
- this.canvasService.canvas.remove(layer);
6259
- }
6934
+ toFiniteNumber(value, fallback) {
6935
+ const numeric = Number(value);
6936
+ return Number.isFinite(numeric) ? numeric : fallback;
6260
6937
  }
6261
- createArrowLine(x1, y1, x2, y2, color) {
6262
- const line = new import_fabric6.Line([x1, y1, x2, y2], {
6263
- stroke: color,
6264
- strokeWidth: this.thickness / 20,
6265
- // Scale stroke width relative to thickness (default 1)
6266
- selectable: false,
6267
- evented: false
6268
- });
6269
- const arrowSize = Math.max(4, this.thickness * 0.3);
6270
- const angle = Math.atan2(y2 - y1, x2 - x1);
6271
- const endArrow = new import_fabric6.Polygon(
6272
- [
6273
- { x: 0, y: 0 },
6274
- { x: -arrowSize, y: -arrowSize / 2 },
6275
- { x: -arrowSize, y: arrowSize / 2 }
6276
- ],
6277
- {
6278
- fill: color,
6279
- left: x2,
6280
- top: y2,
6281
- originX: "right",
6282
- originY: "center",
6283
- angle: angle * 180 / Math.PI,
6938
+ toSceneDisplayLength(value) {
6939
+ if (!this.canvasService) return value;
6940
+ return this.canvasService.toSceneLength(value);
6941
+ }
6942
+ formatLengthMm(valueMm, unit) {
6943
+ const converted = fromMm(valueMm, unit);
6944
+ const fractionDigits = unit === "in" ? 3 : 2;
6945
+ return Number(converted.toFixed(fractionDigits)).toString();
6946
+ }
6947
+ buildLinePath(start, end) {
6948
+ const dx = end.x - start.x;
6949
+ const dy = end.y - start.y;
6950
+ return `M 0 0 L ${dx} ${dy}`;
6951
+ }
6952
+ buildStartArrowPath(size) {
6953
+ return `M 0 0 L ${size} ${-size / 2} L ${size} ${size / 2} Z`;
6954
+ }
6955
+ buildEndArrowPath(size) {
6956
+ return `M 0 0 L ${-size} ${-size / 2} L ${-size} ${size / 2} Z`;
6957
+ }
6958
+ createPathSpec(id, pathData, position, options) {
6959
+ var _a, _b, _c, _d, _e, _f, _g;
6960
+ return {
6961
+ id,
6962
+ type: "path",
6963
+ data: {
6964
+ id,
6965
+ type: "ruler"
6966
+ },
6967
+ props: {
6968
+ pathData,
6969
+ left: position.x,
6970
+ top: position.y,
6971
+ originX: (_a = options.originX) != null ? _a : "left",
6972
+ originY: (_b = options.originY) != null ? _b : "top",
6973
+ angle: (_c = options.angle) != null ? _c : 0,
6974
+ stroke: (_d = options.stroke) != null ? _d : null,
6975
+ fill: (_e = options.fill) != null ? _e : null,
6976
+ strokeWidth: (_f = options.strokeWidth) != null ? _f : 1,
6977
+ strokeLineCap: (_g = options.strokeLineCap) != null ? _g : "butt",
6284
6978
  selectable: false,
6285
- evented: false
6979
+ evented: false,
6980
+ excludeFromExport: true
6286
6981
  }
6287
- );
6288
- const startArrow = new import_fabric6.Polygon(
6289
- [
6290
- { x: 0, y: 0 },
6291
- { x: arrowSize, y: -arrowSize / 2 },
6292
- { x: arrowSize, y: arrowSize / 2 }
6293
- ],
6294
- {
6295
- fill: color,
6296
- left: x1,
6297
- top: y1,
6298
- originX: "left",
6982
+ };
6983
+ }
6984
+ createTextSpec(id, text, position, angle = 0) {
6985
+ return {
6986
+ id,
6987
+ type: "text",
6988
+ data: {
6989
+ id,
6990
+ type: "ruler"
6991
+ },
6992
+ props: {
6993
+ text,
6994
+ left: position.x,
6995
+ top: position.y,
6996
+ angle,
6997
+ fontSize: this.toSceneDisplayLength(this.fontSize),
6998
+ fill: this.textColor,
6999
+ fontFamily: "Arial",
7000
+ originX: "center",
6299
7001
  originY: "center",
6300
- angle: angle * 180 / Math.PI,
7002
+ backgroundColor: this.backgroundColor,
6301
7003
  selectable: false,
6302
- evented: false
7004
+ evented: false,
7005
+ excludeFromExport: true
6303
7006
  }
6304
- );
6305
- return new import_fabric6.Group([line, startArrow, endArrow], {
6306
- selectable: false,
6307
- evented: false
6308
- });
7007
+ };
6309
7008
  }
6310
- updateRuler() {
6311
- var _a;
6312
- if (!this.canvasService) return;
6313
- const layer = this.getLayer();
6314
- if (!layer) return;
6315
- layer.remove(...layer.getObjects());
6316
- const { backgroundColor, lineColor, textColor, fontSize } = this;
6317
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
6318
- "ConfigurationService"
7009
+ buildRulerSpecs(input) {
7010
+ const { left, top, right, bottom, widthLabel, heightLabel } = input;
7011
+ const gap = Math.max(
7012
+ 0,
7013
+ this.toSceneDisplayLength(this.toFiniteNumber(this.gap, DEFAULT_GAP))
6319
7014
  );
6320
- if (!configService) return;
6321
- const sizeState = readSizeState(configService);
6322
- const layout = computeSceneLayout(this.canvasService, sizeState);
6323
- if (!layout) return;
6324
- const trimRect = layout.trimRect;
6325
- const cutRect = layout.cutRect;
6326
- const useCutAsRuler = layout.cutMode === "outset";
6327
- const rulerRect = useCutAsRuler ? cutRect : trimRect;
6328
- const gap = this.gap || 15;
6329
- const rulerLeft = rulerRect.left;
6330
- const rulerTop = rulerRect.top;
6331
- const rulerRight = rulerRect.left + rulerRect.width;
6332
- const rulerBottom = rulerRect.top + rulerRect.height;
6333
- const displayWidthMm = useCutAsRuler ? layout.cutWidthMm : layout.trimWidthMm;
6334
- const displayHeightMm = useCutAsRuler ? layout.cutHeightMm : layout.trimHeightMm;
6335
- const displayUnit = sizeState.unit;
6336
- const topRulerY = rulerTop - gap;
6337
- const topRulerXStart = rulerLeft;
6338
- const topRulerXEnd = rulerRight;
6339
- const leftRulerX = rulerLeft - gap;
6340
- const leftRulerYStart = rulerTop;
6341
- const leftRulerYEnd = rulerBottom;
6342
- const topDimLine = this.createArrowLine(
6343
- topRulerXStart,
6344
- topRulerY,
6345
- topRulerXEnd,
6346
- topRulerY,
6347
- lineColor
7015
+ const topY = top - gap;
7016
+ const leftX = left - gap;
7017
+ const arrowSize = Math.max(
7018
+ this.toSceneDisplayLength(MIN_ARROW_SIZE),
7019
+ this.toSceneDisplayLength(this.thickness * 0.3)
6348
7020
  );
6349
- layer.add(topDimLine);
6350
- const extLen = 5;
6351
- layer.add(
6352
- new import_fabric6.Line(
6353
- [
6354
- topRulerXStart,
6355
- topRulerY - extLen,
6356
- topRulerXStart,
6357
- topRulerY + extLen
6358
- ],
6359
- {
6360
- stroke: lineColor,
6361
- strokeWidth: 1,
6362
- selectable: false,
6363
- evented: false
6364
- }
7021
+ const strokeWidth = Math.max(
7022
+ this.toSceneDisplayLength(1),
7023
+ this.toSceneDisplayLength(
7024
+ this.thickness / THICKNESS_TO_STROKE_WIDTH_RATIO
6365
7025
  )
6366
7026
  );
6367
- layer.add(
6368
- new import_fabric6.Line(
6369
- [topRulerXEnd, topRulerY - extLen, topRulerXEnd, topRulerY + extLen],
7027
+ const extensionLength = this.toSceneDisplayLength(EXTENSION_LINE_LENGTH);
7028
+ const topLineAngleDeg = 0;
7029
+ const leftLineAngleDeg = 90;
7030
+ const topMidX = left + (right - left) / 2;
7031
+ const leftMidY = top + (bottom - top) / 2;
7032
+ const topLineStartX = Math.min(left + arrowSize, topMidX);
7033
+ const topLineEndX = Math.max(right - arrowSize, topMidX);
7034
+ const leftLineStartY = Math.min(top + arrowSize, leftMidY);
7035
+ const leftLineEndY = Math.max(bottom - arrowSize, leftMidY);
7036
+ const specs = [];
7037
+ specs.push(
7038
+ this.createPathSpec(
7039
+ "ruler.top.line",
7040
+ this.buildLinePath(
7041
+ { x: topLineStartX, y: topY },
7042
+ { x: topLineEndX, y: topY }
7043
+ ),
7044
+ { x: topLineStartX, y: topY },
6370
7045
  {
6371
- stroke: lineColor,
6372
- strokeWidth: 1,
6373
- selectable: false,
6374
- evented: false
7046
+ stroke: this.lineColor,
7047
+ strokeWidth,
7048
+ strokeLineCap: "butt"
6375
7049
  }
6376
- )
6377
- );
6378
- const widthStr = formatMm(displayWidthMm, displayUnit);
6379
- const topTextContent = `${widthStr} ${displayUnit}`;
6380
- const topText = new import_fabric6.Text(topTextContent, {
6381
- left: topRulerXStart + (rulerRight - rulerLeft) / 2,
6382
- top: topRulerY,
6383
- fontSize,
6384
- fill: textColor,
6385
- fontFamily: "Arial",
6386
- originX: "center",
6387
- originY: "center",
6388
- backgroundColor,
6389
- // Background mask for readability
6390
- selectable: false,
6391
- evented: false
6392
- });
6393
- layer.add(topText);
6394
- const leftDimLine = this.createArrowLine(
6395
- leftRulerX,
6396
- leftRulerYStart,
6397
- leftRulerX,
6398
- leftRulerYEnd,
6399
- lineColor
6400
- );
6401
- layer.add(leftDimLine);
6402
- layer.add(
6403
- new import_fabric6.Line(
6404
- [
6405
- leftRulerX - extLen,
6406
- leftRulerYStart,
6407
- leftRulerX + extLen,
6408
- leftRulerYStart
6409
- ],
6410
- {
6411
- stroke: lineColor,
6412
- strokeWidth: 1,
6413
- selectable: false,
6414
- evented: false
7050
+ ),
7051
+ this.createPathSpec(
7052
+ "ruler.top.arrow.start",
7053
+ this.buildStartArrowPath(arrowSize),
7054
+ { x: left, y: topY },
7055
+ {
7056
+ fill: this.lineColor,
7057
+ stroke: this.lineColor,
7058
+ strokeWidth: this.toSceneDisplayLength(1),
7059
+ originX: "left",
7060
+ originY: "center",
7061
+ angle: topLineAngleDeg
6415
7062
  }
6416
- )
7063
+ ),
7064
+ this.createPathSpec(
7065
+ "ruler.top.arrow.end",
7066
+ this.buildEndArrowPath(arrowSize),
7067
+ { x: right, y: topY },
7068
+ {
7069
+ fill: this.lineColor,
7070
+ stroke: this.lineColor,
7071
+ strokeWidth: this.toSceneDisplayLength(1),
7072
+ originX: "right",
7073
+ originY: "center",
7074
+ angle: topLineAngleDeg
7075
+ }
7076
+ ),
7077
+ this.createPathSpec(
7078
+ "ruler.top.ext.start",
7079
+ this.buildLinePath(
7080
+ { x: left, y: topY - extensionLength },
7081
+ { x: left, y: topY + extensionLength }
7082
+ ),
7083
+ { x: left, y: topY - extensionLength },
7084
+ {
7085
+ stroke: this.lineColor,
7086
+ strokeWidth: this.toSceneDisplayLength(1)
7087
+ }
7088
+ ),
7089
+ this.createPathSpec(
7090
+ "ruler.top.ext.end",
7091
+ this.buildLinePath(
7092
+ { x: right, y: topY - extensionLength },
7093
+ { x: right, y: topY + extensionLength }
7094
+ ),
7095
+ { x: right, y: topY - extensionLength },
7096
+ {
7097
+ stroke: this.lineColor,
7098
+ strokeWidth: this.toSceneDisplayLength(1)
7099
+ }
7100
+ ),
7101
+ this.createTextSpec("ruler.top.label", widthLabel, {
7102
+ x: left + (right - left) / 2,
7103
+ y: topY
7104
+ })
6417
7105
  );
6418
- layer.add(
6419
- new import_fabric6.Line(
6420
- [
6421
- leftRulerX - extLen,
6422
- leftRulerYEnd,
6423
- leftRulerX + extLen,
6424
- leftRulerYEnd
6425
- ],
6426
- {
6427
- stroke: lineColor,
6428
- strokeWidth: 1,
6429
- selectable: false,
6430
- evented: false
7106
+ specs.push(
7107
+ this.createPathSpec(
7108
+ "ruler.left.line",
7109
+ this.buildLinePath(
7110
+ { x: leftX, y: leftLineStartY },
7111
+ { x: leftX, y: leftLineEndY }
7112
+ ),
7113
+ { x: leftX, y: leftLineStartY },
7114
+ {
7115
+ stroke: this.lineColor,
7116
+ strokeWidth,
7117
+ strokeLineCap: "butt"
6431
7118
  }
7119
+ ),
7120
+ this.createPathSpec(
7121
+ "ruler.left.arrow.start",
7122
+ this.buildStartArrowPath(arrowSize),
7123
+ { x: leftX, y: top },
7124
+ {
7125
+ fill: this.lineColor,
7126
+ stroke: this.lineColor,
7127
+ strokeWidth: this.toSceneDisplayLength(1),
7128
+ originX: "left",
7129
+ originY: "center",
7130
+ angle: leftLineAngleDeg
7131
+ }
7132
+ ),
7133
+ this.createPathSpec(
7134
+ "ruler.left.arrow.end",
7135
+ this.buildEndArrowPath(arrowSize),
7136
+ { x: leftX, y: bottom },
7137
+ {
7138
+ fill: this.lineColor,
7139
+ stroke: this.lineColor,
7140
+ strokeWidth: this.toSceneDisplayLength(1),
7141
+ originX: "right",
7142
+ originY: "center",
7143
+ angle: leftLineAngleDeg
7144
+ }
7145
+ ),
7146
+ this.createPathSpec(
7147
+ "ruler.left.ext.start",
7148
+ this.buildLinePath(
7149
+ { x: leftX - extensionLength, y: top },
7150
+ { x: leftX + extensionLength, y: top }
7151
+ ),
7152
+ { x: leftX - extensionLength, y: top },
7153
+ {
7154
+ stroke: this.lineColor,
7155
+ strokeWidth: this.toSceneDisplayLength(1)
7156
+ }
7157
+ ),
7158
+ this.createPathSpec(
7159
+ "ruler.left.ext.end",
7160
+ this.buildLinePath(
7161
+ { x: leftX - extensionLength, y: bottom },
7162
+ { x: leftX + extensionLength, y: bottom }
7163
+ ),
7164
+ { x: leftX - extensionLength, y: bottom },
7165
+ {
7166
+ stroke: this.lineColor,
7167
+ strokeWidth: this.toSceneDisplayLength(1)
7168
+ }
7169
+ ),
7170
+ this.createTextSpec(
7171
+ "ruler.left.label",
7172
+ heightLabel,
7173
+ {
7174
+ x: leftX,
7175
+ y: top + (bottom - top) / 2
7176
+ },
7177
+ -90
6432
7178
  )
6433
7179
  );
6434
- const heightStr = formatMm(displayHeightMm, displayUnit);
6435
- const leftTextContent = `${heightStr} ${displayUnit}`;
6436
- const leftText = new import_fabric6.Text(leftTextContent, {
6437
- left: leftRulerX,
6438
- top: leftRulerYStart + (rulerBottom - rulerTop) / 2,
6439
- angle: -90,
6440
- fontSize,
6441
- fill: textColor,
6442
- fontFamily: "Arial",
6443
- originX: "center",
6444
- originY: "center",
6445
- backgroundColor,
6446
- selectable: false,
6447
- evented: false
7180
+ return specs;
7181
+ }
7182
+ updateRuler() {
7183
+ void this.updateRulerAsync();
7184
+ }
7185
+ async updateRulerAsync() {
7186
+ var _a, _b;
7187
+ if (!this.canvasService) return;
7188
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get(
7189
+ "ConfigurationService"
7190
+ );
7191
+ if (!configService) return;
7192
+ const seq = ++this.renderSeq;
7193
+ const sizeState = readSizeState(configService);
7194
+ const layout = computeSceneLayout(this.canvasService, sizeState);
7195
+ this.log("render:start", {
7196
+ seq,
7197
+ unit: sizeState.unit,
7198
+ gap: this.gap,
7199
+ thickness: this.thickness,
7200
+ fontSize: this.fontSize,
7201
+ hasLayout: !!layout,
7202
+ scale: (_b = layout == null ? void 0 : layout.scale) != null ? _b : null
6448
7203
  });
6449
- layer.add(leftText);
6450
- this.canvasService.canvas.bringObjectToFront(layer);
6451
- this.canvasService.canvas.requestRenderAll();
7204
+ if (!layout || layout.scale <= 0) {
7205
+ if (seq !== this.renderSeq) return;
7206
+ this.log("render:skip", { seq, reason: "invalid-layout" });
7207
+ this.specs = [];
7208
+ await this.canvasService.flushRenderFromProducers();
7209
+ return;
7210
+ }
7211
+ const geometry = buildSceneGeometry(configService, layout);
7212
+ if (geometry.unit !== "px") {
7213
+ console.warn("[RulerTool] Unexpected geometry unit.", geometry.unit);
7214
+ }
7215
+ const centerScene = this.canvasService.toScenePoint({
7216
+ x: geometry.x,
7217
+ y: geometry.y
7218
+ });
7219
+ const widthScene = this.canvasService.toSceneLength(geometry.width);
7220
+ const heightScene = this.canvasService.toSceneLength(geometry.height);
7221
+ const rulerLeft = centerScene.x - widthScene / 2;
7222
+ const rulerTop = centerScene.y - heightScene / 2;
7223
+ const rulerRight = rulerLeft + widthScene;
7224
+ const rulerBottom = rulerTop + heightScene;
7225
+ const widthMm = widthScene;
7226
+ const heightMm = heightScene;
7227
+ const unit = sizeState.unit;
7228
+ const widthLabel = `${this.formatLengthMm(widthMm, unit)} ${unit}`;
7229
+ const heightLabel = `${this.formatLengthMm(heightMm, unit)} ${unit}`;
7230
+ const specs = this.buildRulerSpecs({
7231
+ left: rulerLeft,
7232
+ top: rulerTop,
7233
+ right: rulerRight,
7234
+ bottom: rulerBottom,
7235
+ widthLabel,
7236
+ heightLabel
7237
+ });
7238
+ this.log("render:geometry", {
7239
+ seq,
7240
+ left: rulerLeft,
7241
+ top: rulerTop,
7242
+ right: rulerRight,
7243
+ bottom: rulerBottom,
7244
+ widthScene,
7245
+ heightScene,
7246
+ widthMm,
7247
+ heightMm,
7248
+ specCount: specs.length
7249
+ });
7250
+ if (seq !== this.renderSeq) return;
7251
+ this.specs = specs;
7252
+ await this.canvasService.flushRenderFromProducers();
7253
+ if (seq !== this.renderSeq) return;
7254
+ this.canvasService.bringLayerToFront(RULER_LAYER_ID);
7255
+ this.canvasService.requestRenderAll();
7256
+ this.log("render:done", { seq });
6452
7257
  }
6453
7258
  };
6454
7259
 
@@ -6485,6 +7290,9 @@ var WhiteInkTool = class {
6485
7290
  this.printWithWhiteInk = true;
6486
7291
  this.previewImageVisible = true;
6487
7292
  this.renderSeq = 0;
7293
+ this.whiteSpecs = [];
7294
+ this.coverSpecs = [];
7295
+ this.overlaySpecs = [];
6488
7296
  this.onToolActivated = (event) => {
6489
7297
  const before = this.isToolActive;
6490
7298
  this.syncToolActiveFromWorkbench(event.id);
@@ -6522,12 +7330,25 @@ var WhiteInkTool = class {
6522
7330
  };
6523
7331
  }
6524
7332
  activate(context) {
7333
+ var _a;
6525
7334
  this.context = context;
6526
7335
  this.canvasService = context.services.get("CanvasService");
6527
7336
  if (!this.canvasService) {
6528
7337
  console.warn("CanvasService not found for WhiteInkTool");
6529
7338
  return;
6530
7339
  }
7340
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
7341
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
7342
+ this.id,
7343
+ () => ({
7344
+ rootLayerSpecs: {
7345
+ [WHITE_INK_OBJECT_LAYER_ID]: this.whiteSpecs,
7346
+ [WHITE_INK_COVER_LAYER_ID]: this.coverSpecs,
7347
+ [WHITE_INK_OVERLAY_LAYER_ID]: this.overlaySpecs
7348
+ }
7349
+ }),
7350
+ { priority: 260 }
7351
+ );
6531
7352
  context.eventBus.on("tool:activated", this.onToolActivated);
6532
7353
  context.eventBus.on("scene:layout:change", this.onSceneLayoutChanged);
6533
7354
  context.eventBus.on("object:added", this.onObjectAdded);
@@ -6593,7 +7414,7 @@ var WhiteInkTool = class {
6593
7414
  this.updateWhiteInks();
6594
7415
  }
6595
7416
  deactivate(context) {
6596
- var _a;
7417
+ var _a, _b;
6597
7418
  context.eventBus.off("tool:activated", this.onToolActivated);
6598
7419
  context.eventBus.off("scene:layout:change", this.onSceneLayoutChanged);
6599
7420
  context.eventBus.off("object:added", this.onObjectAdded);
@@ -6604,6 +7425,11 @@ var WhiteInkTool = class {
6604
7425
  this.dirtyTrackerDisposable = void 0;
6605
7426
  this.clearRenderedWhiteInks();
6606
7427
  this.applyImageVisibilityForWhiteInk(false);
7428
+ (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
7429
+ this.renderProducerDisposable = void 0;
7430
+ if (this.canvasService) {
7431
+ void this.canvasService.flushRenderFromProducers();
7432
+ }
6607
7433
  this.canvasService = void 0;
6608
7434
  this.context = void 0;
6609
7435
  }
@@ -7026,11 +7852,20 @@ var WhiteInkTool = class {
7026
7852
  if (!layout) {
7027
7853
  return { left: 0, top: 0, width: 0, height: 0 };
7028
7854
  }
7029
- return {
7855
+ return this.canvasService.toSceneRect({
7030
7856
  left: layout.cutRect.left,
7031
7857
  top: layout.cutRect.top,
7032
7858
  width: layout.cutRect.width,
7033
7859
  height: layout.cutRect.height
7860
+ });
7861
+ }
7862
+ toLayoutSceneRect(rect) {
7863
+ return {
7864
+ left: rect.left,
7865
+ top: rect.top,
7866
+ width: rect.width,
7867
+ height: rect.height,
7868
+ space: "scene"
7034
7869
  };
7035
7870
  }
7036
7871
  getImageObjects() {
@@ -7053,7 +7888,7 @@ var WhiteInkTool = class {
7053
7888
  return (_a = obj == null ? void 0 : obj._originalElement) == null ? void 0 : _a.src;
7054
7889
  }
7055
7890
  getImageSnapshot(obj) {
7056
- var _a;
7891
+ var _a, _b;
7057
7892
  if (!obj) return null;
7058
7893
  const src = this.getCurrentSrc(obj);
7059
7894
  if (!src) return null;
@@ -7061,14 +7896,18 @@ var WhiteInkTool = class {
7061
7896
  const width = Number((obj == null ? void 0 : obj.width) || 0);
7062
7897
  const height = Number((obj == null ? void 0 : obj.height) || 0);
7063
7898
  this.rememberSourceSize(src, { width, height });
7899
+ const sceneScale = ((_a = this.canvasService) == null ? void 0 : _a.getSceneScale()) || 1;
7900
+ const leftScreen = Number.isFinite(obj == null ? void 0 : obj.left) ? Number(obj.left) : 0;
7901
+ const topScreen = Number.isFinite(obj == null ? void 0 : obj.top) ? Number(obj.top) : 0;
7902
+ const scenePoint = this.canvasService ? this.canvasService.toScenePoint({ x: leftScreen, y: topScreen }) : { x: leftScreen, y: topScreen };
7064
7903
  return {
7065
- id: String(((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id) || "image"),
7904
+ id: String(((_b = obj == null ? void 0 : obj.data) == null ? void 0 : _b.id) || "image"),
7066
7905
  src,
7067
7906
  element,
7068
- left: Number.isFinite(obj == null ? void 0 : obj.left) ? Number(obj.left) : 0,
7069
- top: Number.isFinite(obj == null ? void 0 : obj.top) ? Number(obj.top) : 0,
7070
- scaleX: Number.isFinite(obj == null ? void 0 : obj.scaleX) ? Number(obj.scaleX) : 1,
7071
- scaleY: Number.isFinite(obj == null ? void 0 : obj.scaleY) ? Number(obj.scaleY) : 1,
7907
+ left: scenePoint.x,
7908
+ top: scenePoint.y,
7909
+ scaleX: (Number.isFinite(obj == null ? void 0 : obj.scaleX) ? Number(obj.scaleX) : 1) / sceneScale,
7910
+ scaleY: (Number.isFinite(obj == null ? void 0 : obj.scaleY) ? Number(obj.scaleY) : 1) / sceneScale,
7072
7911
  angle: Number.isFinite(obj == null ? void 0 : obj.angle) ? Number(obj.angle) : 0,
7073
7912
  originX: typeof (obj == null ? void 0 : obj.originX) === "string" ? obj.originX : "center",
7074
7913
  originY: typeof (obj == null ? void 0 : obj.originY) === "string" ? obj.originY : "center",
@@ -7243,8 +8082,11 @@ var WhiteInkTool = class {
7243
8082
  var _a, _b;
7244
8083
  if (!this.isToolActive || !this.canvasService) return [];
7245
8084
  if (frame.width <= 0 || frame.height <= 0) return [];
7246
- const canvasW = this.canvasService.canvas.width || 0;
7247
- const canvasH = this.canvasService.canvas.height || 0;
8085
+ const viewport = this.canvasService.getSceneViewportRect();
8086
+ const canvasW = viewport.width || 0;
8087
+ const canvasH = viewport.height || 0;
8088
+ const canvasLeft = viewport.left || 0;
8089
+ const canvasTop = viewport.top || 0;
7248
8090
  const strokeColor = this.getConfig("image.frame.strokeColor", "#808080") || "#808080";
7249
8091
  const strokeWidthRaw = Number(
7250
8092
  (_a = this.getConfig("image.frame.strokeWidth", 2)) != null ? _a : 2
@@ -7256,21 +8098,42 @@ var WhiteInkTool = class {
7256
8098
  const innerBackground = this.getConfig("image.frame.innerBackground", "rgba(0,0,0,0)") || "rgba(0,0,0,0)";
7257
8099
  const strokeWidth = Number.isFinite(strokeWidthRaw) ? Math.max(0, strokeWidthRaw) : 2;
7258
8100
  const dashLength = Number.isFinite(dashLengthRaw) ? Math.max(1, dashLengthRaw) : 8;
7259
- const frameLeft = Math.max(0, Math.min(canvasW, frame.left));
7260
- const frameTop = Math.max(0, Math.min(canvasH, frame.top));
8101
+ const strokeWidthScene = this.canvasService.toSceneLength(strokeWidth);
8102
+ const dashLengthScene = this.canvasService.toSceneLength(dashLength);
8103
+ const frameLeft = Math.max(
8104
+ canvasLeft,
8105
+ Math.min(canvasLeft + canvasW, frame.left)
8106
+ );
8107
+ const frameTop = Math.max(
8108
+ canvasTop,
8109
+ Math.min(canvasTop + canvasH, frame.top)
8110
+ );
7261
8111
  const frameRight = Math.max(
7262
8112
  frameLeft,
7263
- Math.min(canvasW, frame.left + frame.width)
8113
+ Math.min(canvasLeft + canvasW, frame.left + frame.width)
7264
8114
  );
7265
8115
  const frameBottom = Math.max(
7266
8116
  frameTop,
7267
- Math.min(canvasH, frame.top + frame.height)
8117
+ Math.min(canvasTop + canvasH, frame.top + frame.height)
7268
8118
  );
7269
8119
  const visibleFrameH = Math.max(0, frameBottom - frameTop);
7270
- const topH = frameTop;
7271
- const bottomH = Math.max(0, canvasH - frameBottom);
7272
- const leftW = frameLeft;
7273
- const rightW = Math.max(0, canvasW - frameRight);
8120
+ const topH = Math.max(0, frameTop - canvasTop);
8121
+ const bottomH = Math.max(0, canvasTop + canvasH - frameBottom);
8122
+ const leftW = Math.max(0, frameLeft - canvasLeft);
8123
+ const rightW = Math.max(0, canvasLeft + canvasW - frameRight);
8124
+ const viewportRect = this.toLayoutSceneRect({
8125
+ left: canvasLeft,
8126
+ top: canvasTop,
8127
+ width: canvasW,
8128
+ height: canvasH
8129
+ });
8130
+ const visibleFrameBandRect = this.toLayoutSceneRect({
8131
+ left: canvasLeft,
8132
+ top: frameTop,
8133
+ width: canvasW,
8134
+ height: visibleFrameH
8135
+ });
8136
+ const frameRect = this.toLayoutSceneRect(frame);
7274
8137
  const maskSpecs = [
7275
8138
  {
7276
8139
  id: "white-ink.cropMask.top",
@@ -7280,13 +8143,17 @@ var WhiteInkTool = class {
7280
8143
  layerId: WHITE_INK_OVERLAY_LAYER_ID,
7281
8144
  type: "white-ink-mask"
7282
8145
  },
8146
+ layout: {
8147
+ reference: "custom",
8148
+ referenceRect: viewportRect,
8149
+ alignX: "start",
8150
+ alignY: "start",
8151
+ width: "100%",
8152
+ height: topH
8153
+ },
7283
8154
  props: {
7284
- left: canvasW / 2,
7285
- top: topH / 2,
7286
- width: canvasW,
7287
- height: topH,
7288
- originX: "center",
7289
- originY: "center",
8155
+ originX: "left",
8156
+ originY: "top",
7290
8157
  fill: outerBackground,
7291
8158
  selectable: false,
7292
8159
  evented: false,
@@ -7301,13 +8168,17 @@ var WhiteInkTool = class {
7301
8168
  layerId: WHITE_INK_OVERLAY_LAYER_ID,
7302
8169
  type: "white-ink-mask"
7303
8170
  },
8171
+ layout: {
8172
+ reference: "custom",
8173
+ referenceRect: viewportRect,
8174
+ alignX: "start",
8175
+ alignY: "end",
8176
+ width: "100%",
8177
+ height: bottomH
8178
+ },
7304
8179
  props: {
7305
- left: canvasW / 2,
7306
- top: frameBottom + bottomH / 2,
7307
- width: canvasW,
7308
- height: bottomH,
7309
- originX: "center",
7310
- originY: "center",
8180
+ originX: "left",
8181
+ originY: "top",
7311
8182
  fill: outerBackground,
7312
8183
  selectable: false,
7313
8184
  evented: false,
@@ -7322,13 +8193,17 @@ var WhiteInkTool = class {
7322
8193
  layerId: WHITE_INK_OVERLAY_LAYER_ID,
7323
8194
  type: "white-ink-mask"
7324
8195
  },
7325
- props: {
7326
- left: leftW / 2,
7327
- top: frameTop + visibleFrameH / 2,
8196
+ layout: {
8197
+ reference: "custom",
8198
+ referenceRect: visibleFrameBandRect,
8199
+ alignX: "start",
8200
+ alignY: "start",
7328
8201
  width: leftW,
7329
- height: visibleFrameH,
7330
- originX: "center",
7331
- originY: "center",
8202
+ height: "100%"
8203
+ },
8204
+ props: {
8205
+ originX: "left",
8206
+ originY: "top",
7332
8207
  fill: outerBackground,
7333
8208
  selectable: false,
7334
8209
  evented: false,
@@ -7343,13 +8218,17 @@ var WhiteInkTool = class {
7343
8218
  layerId: WHITE_INK_OVERLAY_LAYER_ID,
7344
8219
  type: "white-ink-mask"
7345
8220
  },
7346
- props: {
7347
- left: frameRight + rightW / 2,
7348
- top: frameTop + visibleFrameH / 2,
8221
+ layout: {
8222
+ reference: "custom",
8223
+ referenceRect: visibleFrameBandRect,
8224
+ alignX: "end",
8225
+ alignY: "start",
7349
8226
  width: rightW,
7350
- height: visibleFrameH,
7351
- originX: "center",
7352
- originY: "center",
8227
+ height: "100%"
8228
+ },
8229
+ props: {
8230
+ originX: "left",
8231
+ originY: "top",
7353
8232
  fill: outerBackground,
7354
8233
  selectable: false,
7355
8234
  evented: false,
@@ -7367,17 +8246,21 @@ var WhiteInkTool = class {
7367
8246
  layerId: WHITE_INK_OVERLAY_LAYER_ID,
7368
8247
  type: "white-ink-frame"
7369
8248
  },
8249
+ layout: {
8250
+ reference: "custom",
8251
+ referenceRect: frameRect,
8252
+ alignX: "start",
8253
+ alignY: "start",
8254
+ width: "100%",
8255
+ height: "100%"
8256
+ },
7370
8257
  props: {
7371
- left: frame.left + frame.width / 2,
7372
- top: frame.top + frame.height / 2,
7373
- width: frame.width,
7374
- height: frame.height,
7375
- originX: "center",
7376
- originY: "center",
8258
+ originX: "left",
8259
+ originY: "top",
7377
8260
  fill: innerBackground,
7378
8261
  stroke: strokeColor,
7379
- strokeWidth,
7380
- strokeDashArray: [dashLength, dashLength],
8262
+ strokeWidth: strokeWidthScene,
8263
+ strokeDashArray: [dashLengthScene, dashLengthScene],
7381
8264
  selectable: false,
7382
8265
  evented: false,
7383
8266
  excludeFromExport: true
@@ -7453,50 +8336,30 @@ var WhiteInkTool = class {
7453
8336
  }
7454
8337
  ).filter((index) => index >= 0);
7455
8338
  let whiteInsertIndex = imageIndexes.length ? Math.min(...imageIndexes) : this.resolveDefaultInsertIndex(currentObjects);
7456
- whiteObjects.forEach((obj) => {
7457
- canvas.moveObjectTo(obj, whiteInsertIndex);
7458
- whiteInsertIndex += 1;
7459
- });
7460
- const afterWhiteObjects = canvas.getObjects();
7461
- const afterImageIndexes = afterWhiteObjects.map(
7462
- (obj, index) => {
7463
- var _a;
7464
- return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === IMAGE_OBJECT_LAYER_ID3 ? index : -1;
7465
- }
7466
- ).filter((index) => index >= 0);
7467
- let coverInsertIndex = afterImageIndexes.length ? Math.max(...afterImageIndexes) + 1 : whiteInsertIndex;
8339
+ let coverInsertIndex = whiteInsertIndex;
7468
8340
  coverObjects.forEach((obj) => {
7469
8341
  canvas.moveObjectTo(obj, coverInsertIndex);
7470
8342
  coverInsertIndex += 1;
7471
8343
  });
8344
+ whiteInsertIndex = coverInsertIndex;
8345
+ whiteObjects.forEach((obj) => {
8346
+ canvas.moveObjectTo(obj, whiteInsertIndex);
8347
+ whiteInsertIndex += 1;
8348
+ });
7472
8349
  frameObjects.forEach((obj) => canvas.bringObjectToFront(obj));
7473
8350
  canvas.getObjects().filter((obj) => {
7474
8351
  var _a;
7475
8352
  return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === IMAGE_OVERLAY_LAYER_ID2;
7476
8353
  }).forEach((obj) => canvas.bringObjectToFront(obj));
7477
- const dielineOverlay = this.canvasService.getLayer("dieline-overlay");
7478
- if (dielineOverlay) {
7479
- canvas.bringObjectToFront(dielineOverlay);
7480
- }
7481
- const rulerOverlay = this.canvasService.getLayer("ruler-overlay");
7482
- if (rulerOverlay) {
7483
- canvas.bringObjectToFront(rulerOverlay);
7484
- }
8354
+ this.canvasService.bringLayerToFront("dieline-overlay");
8355
+ this.canvasService.bringLayerToFront("ruler-overlay");
7485
8356
  }
7486
8357
  clearRenderedWhiteInks() {
7487
8358
  if (!this.canvasService) return;
7488
- void this.canvasService.applyObjectSpecsToRootLayer(
7489
- WHITE_INK_OBJECT_LAYER_ID,
7490
- []
7491
- );
7492
- void this.canvasService.applyObjectSpecsToRootLayer(
7493
- WHITE_INK_COVER_LAYER_ID,
7494
- []
7495
- );
7496
- void this.canvasService.applyObjectSpecsToRootLayer(
7497
- WHITE_INK_OVERLAY_LAYER_ID,
7498
- []
7499
- );
8359
+ this.whiteSpecs = [];
8360
+ this.coverSpecs = [];
8361
+ this.overlaySpecs = [];
8362
+ this.canvasService.requestRenderFromProducers();
7500
8363
  }
7501
8364
  purgeSourceCaches(item) {
7502
8365
  const sourceUrl = this.resolveSourceUrl(item);
@@ -7563,20 +8426,12 @@ var WhiteInkTool = class {
7563
8426
  }
7564
8427
  }
7565
8428
  }
7566
- await this.canvasService.applyObjectSpecsToRootLayer(
7567
- WHITE_INK_OBJECT_LAYER_ID,
7568
- whiteSpecs
7569
- );
8429
+ this.whiteSpecs = whiteSpecs;
7570
8430
  if (seq !== this.renderSeq) return;
7571
- await this.canvasService.applyObjectSpecsToRootLayer(
7572
- WHITE_INK_COVER_LAYER_ID,
7573
- coverSpecs
7574
- );
8431
+ this.coverSpecs = coverSpecs;
7575
8432
  if (seq !== this.renderSeq) return;
7576
- await this.canvasService.applyObjectSpecsToRootLayer(
7577
- WHITE_INK_OVERLAY_LAYER_ID,
7578
- frameSpecs
7579
- );
8433
+ this.overlaySpecs = frameSpecs;
8434
+ await this.canvasService.flushRenderFromProducers();
7580
8435
  if (seq !== this.renderSeq) return;
7581
8436
  this.syncZOrder();
7582
8437
  this.canvasService.requestRenderAll();
@@ -7806,23 +8661,16 @@ var SceneVisibilityService = class {
7806
8661
  const dielineLayer = this.canvasService.getLayer("dieline-overlay");
7807
8662
  if (dielineLayer) {
7808
8663
  const visible = !HIDDEN_DIELINE_TOOLS.has(this.activeToolId || "");
7809
- if (dielineLayer.visible !== visible) {
7810
- dielineLayer.set({ visible });
7811
- }
7812
- }
7813
- const rulerLayer = this.canvasService.getLayer("ruler-overlay");
7814
- if (rulerLayer) {
7815
- const visible = !HIDDEN_RULER_TOOLS.has(this.activeToolId || "");
7816
- if (rulerLayer.visible !== visible) {
7817
- rulerLayer.set({ visible });
7818
- }
8664
+ this.canvasService.setLayerVisibility("dieline-overlay", visible);
7819
8665
  }
8666
+ const rulerVisible = !HIDDEN_RULER_TOOLS.has(this.activeToolId || "");
8667
+ this.canvasService.setLayerVisibility("ruler-overlay", rulerVisible);
7820
8668
  this.canvasService.requestRenderAll();
7821
8669
  }
7822
8670
  };
7823
8671
 
7824
8672
  // src/services/CanvasService.ts
7825
- var import_fabric7 = require("fabric");
8673
+ var import_fabric5 = require("fabric");
7826
8674
 
7827
8675
  // src/services/ViewportSystem.ts
7828
8676
  var ViewportSystem = class {
@@ -7900,10 +8748,17 @@ var ViewportSystem = class {
7900
8748
  // src/services/CanvasService.ts
7901
8749
  var CanvasService = class {
7902
8750
  constructor(el, options) {
7903
- if (el instanceof import_fabric7.Canvas) {
8751
+ this.renderProducers = /* @__PURE__ */ new Map();
8752
+ this.producerOrder = 0;
8753
+ this.producerFlushRequested = false;
8754
+ this.producerLoopPending = false;
8755
+ this.producerLoopPromise = null;
8756
+ this.managedProducerLayerIds = /* @__PURE__ */ new Set();
8757
+ this.managedProducerRootLayerIds = /* @__PURE__ */ new Set();
8758
+ if (el instanceof import_fabric5.Canvas) {
7904
8759
  this.canvas = el;
7905
8760
  } else {
7906
- this.canvas = new import_fabric7.Canvas(el, {
8761
+ this.canvas = new import_fabric5.Canvas(el, {
7907
8762
  preserveObjectStacking: true,
7908
8763
  ...options
7909
8764
  });
@@ -7932,8 +8787,156 @@ var CanvasService = class {
7932
8787
  this.canvas.on("object:removed", forward("object:removed"));
7933
8788
  }
7934
8789
  dispose() {
8790
+ this.renderProducers.clear();
8791
+ this.managedProducerLayerIds.clear();
8792
+ this.managedProducerRootLayerIds.clear();
8793
+ this.producerFlushRequested = false;
7935
8794
  this.canvas.dispose();
7936
8795
  }
8796
+ registerRenderProducer(toolId, producer, options = {}) {
8797
+ const normalizedToolId = String(toolId || "").trim();
8798
+ if (!normalizedToolId) {
8799
+ throw new Error(
8800
+ "[CanvasService] registerRenderProducer requires a toolId."
8801
+ );
8802
+ }
8803
+ if (typeof producer !== "function") {
8804
+ throw new Error(
8805
+ `[CanvasService] registerRenderProducer("${normalizedToolId}") requires a producer function.`
8806
+ );
8807
+ }
8808
+ const entry = {
8809
+ toolId: normalizedToolId,
8810
+ producer,
8811
+ priority: Number.isFinite(options.priority) ? Number(options.priority) : 0,
8812
+ order: this.producerOrder++
8813
+ };
8814
+ this.renderProducers.set(normalizedToolId, entry);
8815
+ this.requestRenderFromProducers();
8816
+ return {
8817
+ dispose: () => {
8818
+ this.unregisterRenderProducer(normalizedToolId);
8819
+ }
8820
+ };
8821
+ }
8822
+ unregisterRenderProducer(toolId) {
8823
+ const normalizedToolId = String(toolId || "").trim();
8824
+ if (!normalizedToolId) return false;
8825
+ const removed = this.renderProducers.delete(normalizedToolId);
8826
+ if (removed) {
8827
+ this.requestRenderFromProducers();
8828
+ }
8829
+ return removed;
8830
+ }
8831
+ requestRenderFromProducers() {
8832
+ this.producerFlushRequested = true;
8833
+ this.scheduleProducerLoop();
8834
+ }
8835
+ async flushRenderFromProducers() {
8836
+ this.requestRenderFromProducers();
8837
+ if (this.producerLoopPromise) {
8838
+ await this.producerLoopPromise;
8839
+ }
8840
+ }
8841
+ scheduleProducerLoop() {
8842
+ if (this.producerLoopPending) return;
8843
+ this.producerLoopPending = true;
8844
+ this.producerLoopPromise = Promise.resolve().then(() => this.runProducerLoop()).catch((error) => {
8845
+ console.error("[CanvasService] render producer loop failed.", error);
8846
+ }).finally(() => {
8847
+ this.producerLoopPending = false;
8848
+ if (this.producerFlushRequested) {
8849
+ this.scheduleProducerLoop();
8850
+ }
8851
+ });
8852
+ }
8853
+ async runProducerLoop() {
8854
+ while (this.producerFlushRequested) {
8855
+ this.producerFlushRequested = false;
8856
+ await this.collectAndApplyProducerSpecs();
8857
+ }
8858
+ }
8859
+ sortedRenderProducerEntries() {
8860
+ return Array.from(this.renderProducers.values()).sort((a, b) => {
8861
+ if (a.priority !== b.priority) {
8862
+ return a.priority - b.priority;
8863
+ }
8864
+ if (a.order !== b.order) {
8865
+ return a.order - b.order;
8866
+ }
8867
+ return a.toolId.localeCompare(b.toolId);
8868
+ });
8869
+ }
8870
+ appendLayerSpecMap(map, source) {
8871
+ if (!source) return;
8872
+ Object.entries(source).forEach(([layerId, specs]) => {
8873
+ if (!Array.isArray(specs)) return;
8874
+ const list = map.get(layerId) || [];
8875
+ list.push(...specs);
8876
+ map.set(layerId, list);
8877
+ });
8878
+ }
8879
+ async collectAndApplyProducerSpecs() {
8880
+ const groupLayerSpecs = /* @__PURE__ */ new Map();
8881
+ const rootLayerSpecs = /* @__PURE__ */ new Map();
8882
+ const replaceLayerIds = /* @__PURE__ */ new Set();
8883
+ const replaceRootLayerIds = /* @__PURE__ */ new Set();
8884
+ const entries = this.sortedRenderProducerEntries();
8885
+ for (const entry of entries) {
8886
+ try {
8887
+ const result = await entry.producer();
8888
+ if (!result) continue;
8889
+ this.appendLayerSpecMap(groupLayerSpecs, result.layerSpecs);
8890
+ this.appendLayerSpecMap(rootLayerSpecs, result.rootLayerSpecs);
8891
+ if (Array.isArray(result.replaceLayerIds)) {
8892
+ result.replaceLayerIds.forEach((layerId) => {
8893
+ if (layerId) replaceLayerIds.add(layerId);
8894
+ });
8895
+ }
8896
+ if (Array.isArray(result.replaceRootLayerIds)) {
8897
+ result.replaceRootLayerIds.forEach((layerId) => {
8898
+ if (layerId) replaceRootLayerIds.add(layerId);
8899
+ });
8900
+ }
8901
+ } catch (error) {
8902
+ console.error(
8903
+ `[CanvasService] render producer "${entry.toolId}" failed.`,
8904
+ error
8905
+ );
8906
+ }
8907
+ }
8908
+ const nextLayerIds = new Set(groupLayerSpecs.keys());
8909
+ const nextRootLayerIds = new Set(rootLayerSpecs.keys());
8910
+ for (const [layerId, specs] of groupLayerSpecs.entries()) {
8911
+ if (replaceLayerIds.has(layerId)) {
8912
+ const layer = this.getLayer(layerId);
8913
+ if (layer) {
8914
+ layer.getObjects().forEach((obj) => layer.remove(obj));
8915
+ }
8916
+ }
8917
+ await this.applyObjectSpecsToLayer(layerId, specs, { render: false });
8918
+ }
8919
+ for (const layerId of this.managedProducerLayerIds) {
8920
+ if (nextLayerIds.has(layerId)) continue;
8921
+ const layer = this.getLayer(layerId);
8922
+ if (!layer) continue;
8923
+ await this.applyObjectSpecsToContainer(layer, [], { render: false });
8924
+ }
8925
+ for (const [layerId, specs] of rootLayerSpecs.entries()) {
8926
+ if (replaceRootLayerIds.has(layerId)) {
8927
+ const existing = this.getRootLayerObjects(layerId);
8928
+ existing.forEach((obj) => this.canvas.remove(obj));
8929
+ }
8930
+ await this.applyObjectSpecsToRootLayer(layerId, specs, { render: false });
8931
+ }
8932
+ for (const layerId of this.managedProducerRootLayerIds) {
8933
+ if (nextRootLayerIds.has(layerId)) continue;
8934
+ await this.applyObjectSpecsToRootLayer(layerId, [], { render: false });
8935
+ }
8936
+ this.managedProducerLayerIds = nextLayerIds;
8937
+ this.managedProducerRootLayerIds = nextRootLayerIds;
8938
+ this.requestRenderAll();
8939
+ }
7937
8940
  /**
7938
8941
  * Get a layer (Group) by its ID.
7939
8942
  * We assume layers are Groups directly on the canvas with a data.id property.
@@ -7956,7 +8959,7 @@ var CanvasService = class {
7956
8959
  ...options,
7957
8960
  data: { ...options.data, id }
7958
8961
  };
7959
- layer = new import_fabric7.Group([], defaultOptions);
8962
+ layer = new import_fabric5.Group([], defaultOptions);
7960
8963
  this.canvas.add(layer);
7961
8964
  }
7962
8965
  return layer;
@@ -7988,13 +8991,206 @@ var CanvasService = class {
7988
8991
  (_a = this.eventBus) == null ? void 0 : _a.emit("canvas:resized", { width, height });
7989
8992
  this.requestRenderAll();
7990
8993
  }
8994
+ getSceneScale() {
8995
+ const scale = Number(this.viewport.scale);
8996
+ return Number.isFinite(scale) && scale > 0 ? scale : 1;
8997
+ }
8998
+ getSceneOffset() {
8999
+ const offset = this.viewport.offset;
9000
+ const x = Number(offset.x);
9001
+ const y = Number(offset.y);
9002
+ return {
9003
+ x: Number.isFinite(x) ? x : 0,
9004
+ y: Number.isFinite(y) ? y : 0
9005
+ };
9006
+ }
9007
+ toScreenPoint(point) {
9008
+ const scale = this.getSceneScale();
9009
+ const offset = this.getSceneOffset();
9010
+ return {
9011
+ x: point.x * scale + offset.x,
9012
+ y: point.y * scale + offset.y
9013
+ };
9014
+ }
9015
+ toScenePoint(point) {
9016
+ const scale = this.getSceneScale();
9017
+ const offset = this.getSceneOffset();
9018
+ return {
9019
+ x: (point.x - offset.x) / scale,
9020
+ y: (point.y - offset.y) / scale
9021
+ };
9022
+ }
9023
+ toScreenLength(value) {
9024
+ return value * this.getSceneScale();
9025
+ }
9026
+ toSceneLength(value) {
9027
+ return value / this.getSceneScale();
9028
+ }
9029
+ toScreenRect(rect) {
9030
+ const start = this.toScreenPoint({ x: rect.left, y: rect.top });
9031
+ return {
9032
+ left: start.x,
9033
+ top: start.y,
9034
+ width: this.toScreenLength(rect.width),
9035
+ height: this.toScreenLength(rect.height)
9036
+ };
9037
+ }
9038
+ toSceneRect(rect) {
9039
+ const start = this.toScenePoint({ x: rect.left, y: rect.top });
9040
+ return {
9041
+ left: start.x,
9042
+ top: start.y,
9043
+ width: this.toSceneLength(rect.width),
9044
+ height: this.toSceneLength(rect.height)
9045
+ };
9046
+ }
9047
+ getSceneViewportRect() {
9048
+ const width = Number(this.canvas.width || 0);
9049
+ const height = Number(this.canvas.height || 0);
9050
+ return this.toSceneRect({ left: 0, top: 0, width, height });
9051
+ }
9052
+ getScreenViewportRect() {
9053
+ return {
9054
+ left: 0,
9055
+ top: 0,
9056
+ width: Number(this.canvas.width || 0),
9057
+ height: Number(this.canvas.height || 0)
9058
+ };
9059
+ }
9060
+ toSpaceRect(rect, from, to) {
9061
+ if (from === to) return { ...rect };
9062
+ if (from === "scene") {
9063
+ return this.toScreenRect(rect);
9064
+ }
9065
+ return this.toSceneRect(rect);
9066
+ }
9067
+ resolveLayoutLength(value, base) {
9068
+ if (typeof value === "number") {
9069
+ return Number.isFinite(value) ? value : void 0;
9070
+ }
9071
+ if (typeof value !== "string") {
9072
+ return void 0;
9073
+ }
9074
+ const raw = value.trim();
9075
+ if (!raw) return void 0;
9076
+ if (raw.endsWith("%")) {
9077
+ const percent = parseFloat(raw.slice(0, -1));
9078
+ if (!Number.isFinite(percent)) return void 0;
9079
+ return base * percent / 100;
9080
+ }
9081
+ const parsed = parseFloat(raw);
9082
+ return Number.isFinite(parsed) ? parsed : void 0;
9083
+ }
9084
+ resolveLayoutInsets(inset, reference) {
9085
+ var _a, _b, _c, _d, _e;
9086
+ if (typeof inset === "number" || typeof inset === "string") {
9087
+ const all = (_a = this.resolveLayoutLength(
9088
+ inset,
9089
+ Math.min(reference.width, reference.height)
9090
+ )) != null ? _a : 0;
9091
+ return { top: all, right: all, bottom: all, left: all };
9092
+ }
9093
+ const source = inset || {};
9094
+ const top = (_b = this.resolveLayoutLength(source.top, reference.height)) != null ? _b : 0;
9095
+ const right = (_c = this.resolveLayoutLength(source.right, reference.width)) != null ? _c : 0;
9096
+ const bottom = (_d = this.resolveLayoutLength(source.bottom, reference.height)) != null ? _d : 0;
9097
+ const left = (_e = this.resolveLayoutLength(source.left, reference.width)) != null ? _e : 0;
9098
+ return { top, right, bottom, left };
9099
+ }
9100
+ resolveLayoutReferenceRect(layout, space) {
9101
+ if (layout.referenceRect) {
9102
+ const sourceSpace = layout.referenceRect.space || space;
9103
+ return this.toSpaceRect(layout.referenceRect, sourceSpace, space);
9104
+ }
9105
+ const reference = layout.reference || "sceneViewport";
9106
+ if (reference === "screenViewport") {
9107
+ const screenRect = this.getScreenViewportRect();
9108
+ return space === "screen" ? screenRect : this.toSceneRect(screenRect);
9109
+ }
9110
+ const sceneRect = this.getSceneViewportRect();
9111
+ return space === "scene" ? sceneRect : this.toScreenRect(sceneRect);
9112
+ }
9113
+ alignFactor(value) {
9114
+ if (value === "end") return 1;
9115
+ if (value === "center") return 0.5;
9116
+ return 0;
9117
+ }
9118
+ normalizeOriginX(value) {
9119
+ if (value === "center") return "center";
9120
+ if (value === "right") return "right";
9121
+ return "left";
9122
+ }
9123
+ normalizeOriginY(value) {
9124
+ if (value === "center") return "center";
9125
+ if (value === "bottom") return "bottom";
9126
+ return "top";
9127
+ }
9128
+ originFactor(value) {
9129
+ if (value === "center") return 0.5;
9130
+ if (value === "right" || value === "bottom") return 1;
9131
+ return 0;
9132
+ }
9133
+ resolveLayoutProps(spec, props) {
9134
+ var _a, _b, _c, _d;
9135
+ const layout = spec.layout;
9136
+ if (!layout) {
9137
+ return { ...props };
9138
+ }
9139
+ const space = spec.space || "scene";
9140
+ const reference = this.resolveLayoutReferenceRect(layout, space);
9141
+ const inset = this.resolveLayoutInsets(layout.inset, reference);
9142
+ const area = {
9143
+ left: reference.left + inset.left,
9144
+ top: reference.top + inset.top,
9145
+ width: Math.max(0, reference.width - inset.left - inset.right),
9146
+ height: Math.max(0, reference.height - inset.top - inset.bottom)
9147
+ };
9148
+ const next = { ...props };
9149
+ const width = (_a = this.resolveLayoutLength(layout.width, area.width)) != null ? _a : Number.isFinite(next.width) ? Number(next.width) : void 0;
9150
+ const height = (_b = this.resolveLayoutLength(layout.height, area.height)) != null ? _b : Number.isFinite(next.height) ? Number(next.height) : void 0;
9151
+ if (width !== void 0) next.width = width;
9152
+ if (height !== void 0) next.height = height;
9153
+ const alignX = this.alignFactor(layout.alignX);
9154
+ const alignY = this.alignFactor(layout.alignY);
9155
+ const offsetX = (_c = this.resolveLayoutLength(layout.offsetX, area.width)) != null ? _c : 0;
9156
+ const offsetY = (_d = this.resolveLayoutLength(layout.offsetY, area.height)) != null ? _d : 0;
9157
+ const objectWidth = Number.isFinite(next.width) ? Number(next.width) : 0;
9158
+ const objectHeight = Number.isFinite(next.height) ? Number(next.height) : 0;
9159
+ const objectLeft = area.left + (area.width - objectWidth) * alignX + offsetX;
9160
+ const objectTop = area.top + (area.height - objectHeight) * alignY + offsetY;
9161
+ const originX = this.normalizeOriginX(next.originX);
9162
+ const originY = this.normalizeOriginY(next.originY);
9163
+ next.left = objectLeft + objectWidth * this.originFactor(originX);
9164
+ next.top = objectTop + objectHeight * this.originFactor(originY);
9165
+ return next;
9166
+ }
9167
+ setLayerVisibility(layerId, visible) {
9168
+ const layer = this.getLayer(layerId);
9169
+ if (layer) {
9170
+ layer.set({ visible });
9171
+ }
9172
+ const objects = this.getRootLayerObjects(layerId);
9173
+ objects.forEach((obj) => {
9174
+ var _a, _b;
9175
+ (_a = obj.set) == null ? void 0 : _a.call(obj, { visible });
9176
+ (_b = obj.setCoords) == null ? void 0 : _b.call(obj);
9177
+ });
9178
+ }
9179
+ bringLayerToFront(layerId) {
9180
+ const layer = this.getLayer(layerId);
9181
+ if (layer) {
9182
+ this.canvas.bringObjectToFront(layer);
9183
+ }
9184
+ const objects = this.getRootLayerObjects(layerId);
9185
+ objects.forEach((obj) => this.canvas.bringObjectToFront(obj));
9186
+ }
7991
9187
  async applyLayerSpec(spec) {
7992
9188
  const layer = this.createLayer(spec.id, spec.props || {});
7993
9189
  await this.applyObjectSpecsToContainer(layer, spec.objects);
7994
9190
  }
7995
- async applyObjectSpecsToLayer(layerId, objects) {
9191
+ async applyObjectSpecsToLayer(layerId, objects, options = {}) {
7996
9192
  const layer = this.createLayer(layerId, {});
7997
- await this.applyObjectSpecsToContainer(layer, objects);
9193
+ await this.applyObjectSpecsToContainer(layer, objects, options);
7998
9194
  }
7999
9195
  getRootLayerObjects(layerId) {
8000
9196
  return this.canvas.getObjects().filter((obj) => {
@@ -8002,7 +9198,7 @@ var CanvasService = class {
8002
9198
  return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === layerId;
8003
9199
  });
8004
9200
  }
8005
- async applyObjectSpecsToRootLayer(layerId, specs) {
9201
+ async applyObjectSpecsToRootLayer(layerId, specs, options = {}) {
8006
9202
  const desiredIds = new Set(specs.map((s) => s.id));
8007
9203
  const existing = this.getRootLayerObjects(layerId);
8008
9204
  existing.forEach((obj) => {
@@ -8036,9 +9232,11 @@ var CanvasService = class {
8036
9232
  }
8037
9233
  this.patchFabricObject(current, spec, { layerId });
8038
9234
  }
8039
- this.requestRenderAll();
9235
+ if (options.render !== false) {
9236
+ this.requestRenderAll();
9237
+ }
8040
9238
  }
8041
- async applyObjectSpecsToContainer(container, specs) {
9239
+ async applyObjectSpecsToContainer(container, specs, options = {}) {
8042
9240
  const desiredIds = new Set(specs.map((s) => s.id));
8043
9241
  const existing = container.getObjects();
8044
9242
  existing.forEach((obj) => {
@@ -8074,7 +9272,9 @@ var CanvasService = class {
8074
9272
  this.moveObjectInContainer(container, current, index);
8075
9273
  }
8076
9274
  container.dirty = true;
8077
- this.requestRenderAll();
9275
+ if (options.render !== false) {
9276
+ this.requestRenderAll();
9277
+ }
8078
9278
  }
8079
9279
  patchFabricObject(obj, spec, extraData) {
8080
9280
  const nextData = {
@@ -8083,9 +9283,33 @@ var CanvasService = class {
8083
9283
  ...extraData || {},
8084
9284
  id: spec.id
8085
9285
  };
8086
- obj.set({ ...spec.props || {}, data: nextData });
9286
+ const props = this.resolveFabricProps(spec, spec.props || {});
9287
+ obj.set({ ...props, data: nextData });
8087
9288
  obj.setCoords();
8088
9289
  }
9290
+ resolveFabricProps(spec, props) {
9291
+ const space = spec.space || "scene";
9292
+ const next = this.resolveLayoutProps(spec, props);
9293
+ if (space === "screen") {
9294
+ return next;
9295
+ }
9296
+ const hasLeft = Number.isFinite(next.left);
9297
+ const hasTop = Number.isFinite(next.top);
9298
+ if (hasLeft || hasTop) {
9299
+ const mapped = this.toScreenPoint({
9300
+ x: hasLeft ? Number(next.left) : 0,
9301
+ y: hasTop ? Number(next.top) : 0
9302
+ });
9303
+ if (hasLeft) next.left = mapped.x;
9304
+ if (hasTop) next.top = mapped.y;
9305
+ }
9306
+ const rawScaleX = Number.isFinite(next.scaleX) ? Number(next.scaleX) : 1;
9307
+ const rawScaleY = Number.isFinite(next.scaleY) ? Number(next.scaleY) : 1;
9308
+ const sceneScale = this.getSceneScale();
9309
+ next.scaleX = rawScaleX * sceneScale;
9310
+ next.scaleY = rawScaleY * sceneScale;
9311
+ return next;
9312
+ }
8089
9313
  moveObjectInContainer(container, obj, index) {
8090
9314
  if (!obj) return;
8091
9315
  const moveObjectTo = container.moveObjectTo;
@@ -8105,10 +9329,11 @@ var CanvasService = class {
8105
9329
  }
8106
9330
  }
8107
9331
  async createFabricObject(spec) {
8108
- var _a, _b;
9332
+ var _a, _b, _c, _d;
8109
9333
  if (spec.type === "rect") {
8110
- const rect = new import_fabric7.Rect({
8111
- ...spec.props || {},
9334
+ const props = this.resolveFabricProps(spec, spec.props || {});
9335
+ const rect = new import_fabric5.Rect({
9336
+ ...props,
8112
9337
  data: { ...spec.data || {}, id: spec.id }
8113
9338
  });
8114
9339
  rect.setCoords();
@@ -8117,8 +9342,9 @@ var CanvasService = class {
8117
9342
  if (spec.type === "path") {
8118
9343
  const pathData = ((_a = spec.props) == null ? void 0 : _a.path) || ((_b = spec.props) == null ? void 0 : _b.pathData);
8119
9344
  if (!pathData) return void 0;
8120
- const path = new import_fabric7.Path(pathData, {
8121
- ...spec.props || {},
9345
+ const props = this.resolveFabricProps(spec, spec.props || {});
9346
+ const path = new import_fabric5.Path(pathData, {
9347
+ ...props,
8122
9348
  data: { ...spec.data || {}, id: spec.id }
8123
9349
  });
8124
9350
  path.setCoords();
@@ -8126,14 +9352,25 @@ var CanvasService = class {
8126
9352
  }
8127
9353
  if (spec.type === "image") {
8128
9354
  if (!spec.src) return void 0;
8129
- const image = await import_fabric7.Image.fromURL(spec.src, { crossOrigin: "anonymous" });
9355
+ const image = await import_fabric5.Image.fromURL(spec.src, { crossOrigin: "anonymous" });
9356
+ const props = this.resolveFabricProps(spec, spec.props || {});
8130
9357
  image.set({
8131
- ...spec.props || {},
9358
+ ...props,
8132
9359
  data: { ...spec.data || {}, id: spec.id }
8133
9360
  });
8134
9361
  image.setCoords();
8135
9362
  return image;
8136
9363
  }
9364
+ if (spec.type === "text") {
9365
+ const content = String((_d = (_c = spec.props) == null ? void 0 : _c.text) != null ? _d : "");
9366
+ const props = this.resolveFabricProps(spec, spec.props || {});
9367
+ const text = new import_fabric5.Text(content, {
9368
+ ...props,
9369
+ data: { ...spec.data || {}, id: spec.id }
9370
+ });
9371
+ text.setCoords();
9372
+ return text;
9373
+ }
8137
9374
  return void 0;
8138
9375
  }
8139
9376
  };