@pooder/kit 5.3.0 → 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 (73) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/index.d.mts +249 -36
  3. package/dist/index.d.ts +249 -36
  4. package/dist/index.js +2374 -1049
  5. package/dist/index.mjs +2375 -1050
  6. package/package.json +1 -1
  7. package/src/extensions/background.ts +178 -85
  8. package/src/extensions/dieline.ts +1149 -1030
  9. package/src/extensions/dielineShape.ts +109 -0
  10. package/src/extensions/feature.ts +482 -366
  11. package/src/extensions/film.ts +148 -76
  12. package/src/extensions/geometry.ts +210 -44
  13. package/src/extensions/image.ts +244 -114
  14. package/src/extensions/ruler.ts +471 -268
  15. package/src/extensions/sceneLayoutModel.ts +28 -6
  16. package/src/extensions/sceneVisibility.ts +3 -10
  17. package/src/extensions/tracer.ts +1019 -980
  18. package/src/extensions/white-ink.ts +284 -231
  19. package/src/services/CanvasService.ts +543 -11
  20. package/src/services/renderSpec.ts +37 -2
  21. package/.test-dist/src/CanvasService.js +0 -249
  22. package/.test-dist/src/ViewportSystem.js +0 -75
  23. package/.test-dist/src/background.js +0 -203
  24. package/.test-dist/src/bridgeSelection.js +0 -20
  25. package/.test-dist/src/constraints.js +0 -237
  26. package/.test-dist/src/coordinate.js +0 -74
  27. package/.test-dist/src/dieline.js +0 -818
  28. package/.test-dist/src/edgeScale.js +0 -12
  29. package/.test-dist/src/extensions/background.js +0 -203
  30. package/.test-dist/src/extensions/bridgeSelection.js +0 -20
  31. package/.test-dist/src/extensions/constraints.js +0 -237
  32. package/.test-dist/src/extensions/dieline.js +0 -828
  33. package/.test-dist/src/extensions/edgeScale.js +0 -12
  34. package/.test-dist/src/extensions/feature.js +0 -825
  35. package/.test-dist/src/extensions/featureComplete.js +0 -32
  36. package/.test-dist/src/extensions/film.js +0 -167
  37. package/.test-dist/src/extensions/geometry.js +0 -545
  38. package/.test-dist/src/extensions/image.js +0 -1529
  39. package/.test-dist/src/extensions/index.js +0 -30
  40. package/.test-dist/src/extensions/maskOps.js +0 -279
  41. package/.test-dist/src/extensions/mirror.js +0 -104
  42. package/.test-dist/src/extensions/ruler.js +0 -345
  43. package/.test-dist/src/extensions/sceneLayout.js +0 -96
  44. package/.test-dist/src/extensions/sceneLayoutModel.js +0 -196
  45. package/.test-dist/src/extensions/sceneVisibility.js +0 -62
  46. package/.test-dist/src/extensions/size.js +0 -331
  47. package/.test-dist/src/extensions/tracer.js +0 -538
  48. package/.test-dist/src/extensions/white-ink.js +0 -1190
  49. package/.test-dist/src/extensions/wrappedOffsets.js +0 -33
  50. package/.test-dist/src/feature.js +0 -826
  51. package/.test-dist/src/featureComplete.js +0 -32
  52. package/.test-dist/src/film.js +0 -167
  53. package/.test-dist/src/geometry.js +0 -506
  54. package/.test-dist/src/image.js +0 -1250
  55. package/.test-dist/src/index.js +0 -18
  56. package/.test-dist/src/maskOps.js +0 -270
  57. package/.test-dist/src/mirror.js +0 -104
  58. package/.test-dist/src/renderSpec.js +0 -2
  59. package/.test-dist/src/ruler.js +0 -343
  60. package/.test-dist/src/sceneLayout.js +0 -99
  61. package/.test-dist/src/sceneLayoutModel.js +0 -196
  62. package/.test-dist/src/sceneView.js +0 -40
  63. package/.test-dist/src/sceneVisibility.js +0 -42
  64. package/.test-dist/src/services/CanvasService.js +0 -249
  65. package/.test-dist/src/services/ViewportSystem.js +0 -76
  66. package/.test-dist/src/services/index.js +0 -24
  67. package/.test-dist/src/services/renderSpec.js +0 -2
  68. package/.test-dist/src/size.js +0 -332
  69. package/.test-dist/src/tracer.js +0 -544
  70. package/.test-dist/src/units.js +0 -30
  71. package/.test-dist/src/white-ink.js +0 -829
  72. package/.test-dist/src/wrappedOffsets.js +0 -33
  73. 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,45 +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 { shape, width, height, radius, x, y, pathData } = options;
399
- const center = new import_paper.default.Point(x, y);
400
- 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;
401
627
  return new import_paper.default.Path.Rectangle({
402
628
  point: [x - width / 2, y - height / 2],
403
629
  size: [Math.max(0, width), Math.max(0, height)],
404
630
  radius: Math.max(0, radius)
405
631
  });
406
- } else if (shape === "circle") {
632
+ },
633
+ circle: (options) => {
634
+ const { x, y, width, height } = options;
407
635
  const r = Math.min(width, height) / 2;
408
636
  return new import_paper.default.Path.Circle({
409
- center,
637
+ center: new import_paper.default.Point(x, y),
410
638
  radius: Math.max(0, r)
411
639
  });
412
- } else if (shape === "ellipse") {
640
+ },
641
+ ellipse: (options) => {
642
+ const { x, y, width, height } = options;
413
643
  return new import_paper.default.Path.Ellipse({
414
- center,
644
+ center: new import_paper.default.Point(x, y),
415
645
  radius: [Math.max(0, width / 2), Math.max(0, height / 2)]
416
646
  });
417
- } else if (shape === "custom" && pathData) {
418
- const hasMultipleSubPaths = ((_a = (pathData.match(/[Mm]/g) || []).length) != null ? _a : 0) > 1;
419
- const path = hasMultipleSubPaths ? new import_paper.default.CompoundPath(pathData) : (() => {
420
- const single = new import_paper.default.Path();
421
- single.pathData = pathData;
422
- return single;
423
- })();
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) {
424
681
  path.position = center;
425
- if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
426
- path.scale(width / path.bounds.width, height / path.bounds.height);
427
- }
682
+ path.scale(width / path.bounds.width, height / path.bounds.height);
428
683
  return path;
429
- } else {
430
- return new import_paper.default.Path.Rectangle({
431
- point: [x - width / 2, y - height / 2],
432
- size: [Math.max(0, width), Math.max(0, height)]
433
- });
434
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);
435
696
  }
436
697
  function resolveBridgeBasePath(shape, anchor) {
437
698
  if (shape instanceof import_paper.default.Path) {
@@ -755,6 +1016,18 @@ function getNearestPointOnDieline(point, options) {
755
1016
  shape.remove();
756
1017
  return result;
757
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
+ }
758
1031
 
759
1032
  // src/coordinate.ts
760
1033
  var Coordinate = class {
@@ -843,12 +1116,6 @@ function parseLengthToMm(input, defaultUnit) {
843
1116
  const unit = (_b = (_a = match[2]) == null ? void 0 : _a.toLowerCase()) != null ? _b : defaultUnit;
844
1117
  return Coordinate.convertUnit(value, unit, "mm");
845
1118
  }
846
- function formatMm(valueMm, displayUnit, fractionDigits = 2) {
847
- if (!Number.isFinite(valueMm)) return "0";
848
- const value = Coordinate.convertUnit(valueMm, "mm", displayUnit);
849
- const rounded = Number(value.toFixed(fractionDigits));
850
- return rounded.toString();
851
- }
852
1119
 
853
1120
  // src/extensions/sceneLayoutModel.ts
854
1121
  var DEFAULT_SIZE_STATE = {
@@ -1059,10 +1326,19 @@ function buildSceneGeometry(configService, layout) {
1059
1326
  "mm"
1060
1327
  );
1061
1328
  const offset = (layout.cutRect.width - layout.trimRect.width) / 2;
1329
+ const sourceWidth = Number(configService.get("dieline.customSourceWidthPx", 0));
1330
+ const sourceHeight = Number(
1331
+ configService.get("dieline.customSourceHeightPx", 0)
1332
+ );
1333
+ const shapeStyle = normalizeShapeStyle(
1334
+ configService.get("dieline.shapeStyle", DEFAULT_DIELINE_SHAPE_STYLE)
1335
+ );
1062
1336
  return {
1063
- shape: configService.get("dieline.shape", "rect"),
1064
- unit: "mm",
1065
- displayUnit: normalizeUnit(configService.get("size.unit", "mm")),
1337
+ shape: normalizeDielineShape(
1338
+ configService.get("dieline.shape", DEFAULT_DIELINE_SHAPE)
1339
+ ),
1340
+ shapeStyle,
1341
+ unit: "px",
1066
1342
  x: layout.trimRect.centerX,
1067
1343
  y: layout.trimRect.centerY,
1068
1344
  width: layout.trimRect.width,
@@ -1070,7 +1346,9 @@ function buildSceneGeometry(configService, layout) {
1070
1346
  radius: radiusMm * layout.scale,
1071
1347
  offset,
1072
1348
  scale: layout.scale,
1073
- pathData: configService.get("dieline.pathData")
1349
+ pathData: configService.get("dieline.pathData"),
1350
+ customSourceWidthPx: Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : void 0,
1351
+ customSourceHeightPx: Number.isFinite(sourceHeight) && sourceHeight > 0 ? sourceHeight : void 0
1074
1352
  };
1075
1353
  }
1076
1354
 
@@ -1093,6 +1371,7 @@ var ImageTool = class {
1093
1371
  this.isImageSelectionActive = false;
1094
1372
  this.focusedImageId = null;
1095
1373
  this.renderSeq = 0;
1374
+ this.overlaySpecs = [];
1096
1375
  this.onToolActivated = (event) => {
1097
1376
  const before = this.isToolActive;
1098
1377
  this.syncToolActiveFromWorkbench(event.id);
@@ -1170,28 +1449,41 @@ var ImageTool = class {
1170
1449
  const frame = this.getFrameRect();
1171
1450
  if (!frame.width || !frame.height) return;
1172
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 };
1173
1453
  const objectScale = Number.isFinite(target == null ? void 0 : target.scaleX) ? target.scaleX : 1;
1454
+ const objectScaleScene = this.toSceneObjectScale(objectScale || 1);
1174
1455
  const workingItem = this.workingItems.find((item) => item.id === id);
1175
1456
  const sourceKey = (workingItem == null ? void 0 : workingItem.sourceUrl) || (workingItem == null ? void 0 : workingItem.url) || "";
1176
1457
  const sourceSize = this.getSourceSize(sourceKey, target);
1177
1458
  const coverScale = this.getCoverScale(frame, sourceSize);
1178
1459
  const updates = {
1179
- left: this.clampNormalized((center.x - frame.left) / frame.width),
1180
- 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),
1181
1462
  angle: Number.isFinite(target.angle) ? target.angle : 0,
1182
- scale: Math.max(0.05, (objectScale || 1) / coverScale)
1463
+ scale: Math.max(0.05, objectScaleScene / coverScale)
1183
1464
  };
1184
1465
  this.focusedImageId = id;
1185
1466
  this.updateImageInWorking(id, updates);
1186
1467
  };
1187
1468
  }
1188
1469
  activate(context) {
1470
+ var _a;
1189
1471
  this.context = context;
1190
1472
  this.canvasService = context.services.get("CanvasService");
1191
1473
  if (!this.canvasService) {
1192
1474
  console.warn("CanvasService not found for ImageTool");
1193
1475
  return;
1194
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
+ );
1195
1487
  context.eventBus.on("tool:activated", this.onToolActivated);
1196
1488
  context.eventBus.on("object:modified", this.onObjectModified);
1197
1489
  context.eventBus.on("selection:created", this.onSelectionChanged);
@@ -1232,7 +1524,7 @@ var ImageTool = class {
1232
1524
  this.updateImages();
1233
1525
  }
1234
1526
  deactivate(context) {
1235
- var _a;
1527
+ var _a, _b;
1236
1528
  context.eventBus.off("tool:activated", this.onToolActivated);
1237
1529
  context.eventBus.off("object:modified", this.onObjectModified);
1238
1530
  context.eventBus.off("selection:created", this.onSelectionChanged);
@@ -1244,12 +1536,13 @@ var ImageTool = class {
1244
1536
  this.dirtyTrackerDisposable = void 0;
1245
1537
  this.cropShapeHatchPattern = void 0;
1246
1538
  this.cropShapeHatchPatternColor = void 0;
1539
+ this.cropShapeHatchPatternKey = void 0;
1540
+ this.overlaySpecs = [];
1247
1541
  this.clearRenderedImages();
1542
+ (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
1543
+ this.renderProducerDisposable = void 0;
1248
1544
  if (this.canvasService) {
1249
- void this.canvasService.applyObjectSpecsToRootLayer(
1250
- IMAGE_OVERLAY_LAYER_ID,
1251
- []
1252
- );
1545
+ void this.canvasService.flushRenderFromProducers();
1253
1546
  this.canvasService = void 0;
1254
1547
  }
1255
1548
  this.context = void 0;
@@ -1660,38 +1953,38 @@ var ImageTool = class {
1660
1953
  if (!layout) {
1661
1954
  return { left: 0, top: 0, width: 0, height: 0 };
1662
1955
  }
1663
- return {
1956
+ return this.canvasService.toSceneRect({
1664
1957
  left: layout.cutRect.left,
1665
1958
  top: layout.cutRect.top,
1666
1959
  width: layout.cutRect.width,
1667
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"
1668
1976
  };
1669
1977
  }
1670
1978
  async resolveDefaultFitArea() {
1671
- if (!this.context || !this.canvasService) return null;
1672
- const commandService = this.context.services.get("CommandService");
1673
- if (!commandService) return null;
1674
- try {
1675
- const layout = await Promise.resolve(
1676
- commandService.executeCommand("getSceneLayout")
1677
- );
1678
- const cutRect = layout == null ? void 0 : layout.cutRect;
1679
- const width = Number(cutRect == null ? void 0 : cutRect.width);
1680
- const height = Number(cutRect == null ? void 0 : cutRect.height);
1681
- const left = Number(cutRect == null ? void 0 : cutRect.left);
1682
- const top = Number(cutRect == null ? void 0 : cutRect.top);
1683
- if (!Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(left) || !Number.isFinite(top)) {
1684
- return null;
1685
- }
1686
- return {
1687
- width: Math.max(1, width),
1688
- height: Math.max(1, height),
1689
- left: left + width / 2,
1690
- top: top + height / 2
1691
- };
1692
- } catch (e) {
1693
- return null;
1694
- }
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
+ };
1695
1988
  }
1696
1989
  async fitImageToDefaultArea(id) {
1697
1990
  if (!this.canvasService) return;
@@ -1700,13 +1993,14 @@ var ImageTool = class {
1700
1993
  await this.fitImageToArea(id, area);
1701
1994
  return;
1702
1995
  }
1703
- const canvasW = Math.max(1, this.canvasService.canvas.width || 0);
1704
- 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);
1705
1999
  await this.fitImageToArea(id, {
1706
2000
  width: canvasW,
1707
2001
  height: canvasH,
1708
- left: canvasW / 2,
1709
- top: canvasH / 2
2002
+ left: viewport.left + canvasW / 2,
2003
+ top: viewport.top + canvasH / 2
1710
2004
  });
1711
2005
  }
1712
2006
  getImageObjects() {
@@ -1794,13 +2088,17 @@ var ImageTool = class {
1794
2088
  }
1795
2089
  toSceneGeometryLike(raw) {
1796
2090
  const shape = raw == null ? void 0 : raw.shape;
1797
- if (shape !== "rect" && shape !== "circle" && shape !== "ellipse" && shape !== "custom") {
2091
+ if (!isDielineShape(shape)) {
1798
2092
  return null;
1799
2093
  }
1800
- const radius = Number(raw == null ? void 0 : raw.radius);
1801
- 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);
1802
2099
  return {
1803
2100
  shape,
2101
+ shapeStyle: normalizeShapeStyle(raw == null ? void 0 : raw.shapeStyle),
1804
2102
  radius: Number.isFinite(radius) ? radius : 0,
1805
2103
  offset: Number.isFinite(offset) ? offset : 0
1806
2104
  };
@@ -1852,8 +2150,11 @@ var ImageTool = class {
1852
2150
  return Math.max(0, Math.min(maxRadius, rawCutRadius));
1853
2151
  }
1854
2152
  getCropShapeHatchPattern(color = "rgba(255, 0, 0, 0.6)") {
2153
+ var _a;
1855
2154
  if (typeof document === "undefined") return void 0;
1856
- 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) {
1857
2158
  return this.cropShapeHatchPattern;
1858
2159
  }
1859
2160
  const size = 16;
@@ -1882,11 +2183,21 @@ var ImageTool = class {
1882
2183
  // @ts-ignore: Fabric Pattern accepts canvas source here.
1883
2184
  repetition: "repeat"
1884
2185
  });
2186
+ pattern.patternTransform = [
2187
+ 1 / sceneScale,
2188
+ 0,
2189
+ 0,
2190
+ 1 / sceneScale,
2191
+ 0,
2192
+ 0
2193
+ ];
1885
2194
  this.cropShapeHatchPattern = pattern;
1886
2195
  this.cropShapeHatchPatternColor = color;
2196
+ this.cropShapeHatchPatternKey = cacheKey;
1887
2197
  return pattern;
1888
2198
  }
1889
2199
  buildCropShapeOverlaySpecs(frame, sceneGeometry) {
2200
+ var _a, _b;
1890
2201
  if (!sceneGeometry) {
1891
2202
  this.debug("overlay:shape:skip", { reason: "scene-geometry-missing" });
1892
2203
  return [];
@@ -1896,6 +2207,7 @@ var ImageTool = class {
1896
2207
  return [];
1897
2208
  }
1898
2209
  const shape = sceneGeometry.shape;
2210
+ const shapeStyle = sceneGeometry.shapeStyle;
1899
2211
  const inset = 0;
1900
2212
  const shapeWidth = Math.max(1, frame.width);
1901
2213
  const shapeHeight = Math.max(1, frame.height);
@@ -1905,6 +2217,7 @@ var ImageTool = class {
1905
2217
  frameWidth: frame.width,
1906
2218
  frameHeight: frame.height,
1907
2219
  offset: sceneGeometry.offset,
2220
+ shapeStyle,
1908
2221
  inset,
1909
2222
  shapeWidth,
1910
2223
  shapeHeight,
@@ -1926,6 +2239,7 @@ var ImageTool = class {
1926
2239
  x: frame.width / 2,
1927
2240
  y: frame.height / 2,
1928
2241
  features: [],
2242
+ shapeStyle,
1929
2243
  canvasWidth: frame.width,
1930
2244
  canvasHeight: frame.height
1931
2245
  };
@@ -1943,6 +2257,9 @@ var ImageTool = class {
1943
2257
  }
1944
2258
  const patternFill = this.getCropShapeHatchPattern();
1945
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);
1946
2263
  const hatchPathLength = hatchPathData.length;
1947
2264
  const shapePathLength = shapePathData.length;
1948
2265
  const specs = [
@@ -1950,10 +2267,16 @@ var ImageTool = class {
1950
2267
  id: "image.cropShapeHatch",
1951
2268
  type: "path",
1952
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
+ },
1953
2278
  props: {
1954
2279
  pathData: hatchPathData,
1955
- left: frame.left,
1956
- top: frame.top,
1957
2280
  originX: "left",
1958
2281
  originY: "top",
1959
2282
  fill: hatchFill,
@@ -1970,15 +2293,21 @@ var ImageTool = class {
1970
2293
  id: "image.cropShapePath",
1971
2294
  type: "path",
1972
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
+ },
1973
2304
  props: {
1974
2305
  pathData: shapePathData,
1975
- left: frame.left,
1976
- top: frame.top,
1977
2306
  originX: "left",
1978
2307
  originY: "top",
1979
2308
  fill: "rgba(0,0,0,0)",
1980
2309
  stroke: "rgba(255, 0, 0, 0.9)",
1981
- strokeWidth: 1,
2310
+ strokeWidth: (_b = (_a = this.canvasService) == null ? void 0 : _a.toSceneLength(1)) != null ? _b : 1,
1982
2311
  selectable: false,
1983
2312
  evented: false,
1984
2313
  excludeFromExport: true,
@@ -1995,6 +2324,8 @@ var ImageTool = class {
1995
2324
  fillRule: "evenodd",
1996
2325
  shapePathLength,
1997
2326
  hatchPathLength,
2327
+ shapeBounds,
2328
+ hatchBounds,
1998
2329
  hatchFillType: hatchFill && typeof hatchFill === "object" ? "pattern" : "color",
1999
2330
  ids: specs.map((spec) => spec.id)
2000
2331
  });
@@ -2057,6 +2388,28 @@ var ImageTool = class {
2057
2388
  opacity: render.opacity
2058
2389
  };
2059
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
+ }
2060
2413
  getCurrentSrc(obj) {
2061
2414
  var _a;
2062
2415
  if (!obj) return void 0;
@@ -2106,8 +2459,9 @@ var ImageTool = class {
2106
2459
  this.rememberSourceSize(render.src, obj);
2107
2460
  const sourceSize = this.getSourceSize(render.src, obj);
2108
2461
  const props = this.computeCanvasProps(render, sourceSize, frame);
2462
+ const screenProps = this.toScreenObjectProps(props);
2109
2463
  obj.set({
2110
- ...props,
2464
+ ...screenProps,
2111
2465
  data: {
2112
2466
  ...obj.data || {},
2113
2467
  id: item.id,
@@ -2173,37 +2527,67 @@ var ImageTool = class {
2173
2527
  });
2174
2528
  return [];
2175
2529
  }
2176
- const canvasW = this.canvasService.canvas.width || 0;
2177
- 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;
2178
2535
  const visual = this.getFrameVisualConfig();
2179
- const frameLeft = Math.max(0, Math.min(canvasW, frame.left));
2180
- 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
+ );
2181
2548
  const frameRight = Math.max(
2182
2549
  frameLeft,
2183
- Math.min(canvasW, frame.left + frame.width)
2550
+ Math.min(canvasLeft + canvasW, frame.left + frame.width)
2184
2551
  );
2185
2552
  const frameBottom = Math.max(
2186
2553
  frameTop,
2187
- Math.min(canvasH, frame.top + frame.height)
2554
+ Math.min(canvasTop + canvasH, frame.top + frame.height)
2188
2555
  );
2189
2556
  const visibleFrameH = Math.max(0, frameBottom - frameTop);
2190
- const topH = frameTop;
2191
- const bottomH = Math.max(0, canvasH - frameBottom);
2192
- const leftW = frameLeft;
2193
- 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);
2194
2574
  const shapeOverlay = this.buildCropShapeOverlaySpecs(frame, sceneGeometry);
2195
2575
  const mask = [
2196
2576
  {
2197
2577
  id: "image.cropMask.top",
2198
2578
  type: "rect",
2199
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
+ },
2200
2588
  props: {
2201
- left: canvasW / 2,
2202
- top: topH / 2,
2203
- width: canvasW,
2204
- height: topH,
2205
- originX: "center",
2206
- originY: "center",
2589
+ originX: "left",
2590
+ originY: "top",
2207
2591
  fill: visual.outerBackground,
2208
2592
  selectable: false,
2209
2593
  evented: false
@@ -2213,13 +2597,17 @@ var ImageTool = class {
2213
2597
  id: "image.cropMask.bottom",
2214
2598
  type: "rect",
2215
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
+ },
2216
2608
  props: {
2217
- left: canvasW / 2,
2218
- top: frameBottom + bottomH / 2,
2219
- width: canvasW,
2220
- height: bottomH,
2221
- originX: "center",
2222
- originY: "center",
2609
+ originX: "left",
2610
+ originY: "top",
2223
2611
  fill: visual.outerBackground,
2224
2612
  selectable: false,
2225
2613
  evented: false
@@ -2229,13 +2617,17 @@ var ImageTool = class {
2229
2617
  id: "image.cropMask.left",
2230
2618
  type: "rect",
2231
2619
  data: { id: "image.cropMask.left", zIndex: 3 },
2232
- props: {
2233
- left: leftW / 2,
2234
- top: frameTop + visibleFrameH / 2,
2620
+ layout: {
2621
+ reference: "custom",
2622
+ referenceRect: visibleFrameBandRect,
2623
+ alignX: "start",
2624
+ alignY: "start",
2235
2625
  width: leftW,
2236
- height: visibleFrameH,
2237
- originX: "center",
2238
- originY: "center",
2626
+ height: "100%"
2627
+ },
2628
+ props: {
2629
+ originX: "left",
2630
+ originY: "top",
2239
2631
  fill: visual.outerBackground,
2240
2632
  selectable: false,
2241
2633
  evented: false
@@ -2245,13 +2637,17 @@ var ImageTool = class {
2245
2637
  id: "image.cropMask.right",
2246
2638
  type: "rect",
2247
2639
  data: { id: "image.cropMask.right", zIndex: 4 },
2248
- props: {
2249
- left: frameRight + rightW / 2,
2250
- top: frameTop + visibleFrameH / 2,
2640
+ layout: {
2641
+ reference: "custom",
2642
+ referenceRect: visibleFrameBandRect,
2643
+ alignX: "end",
2644
+ alignY: "start",
2251
2645
  width: rightW,
2252
- height: visibleFrameH,
2253
- originX: "center",
2254
- originY: "center",
2646
+ height: "100%"
2647
+ },
2648
+ props: {
2649
+ originX: "left",
2650
+ originY: "top",
2255
2651
  fill: visual.outerBackground,
2256
2652
  selectable: false,
2257
2653
  evented: false
@@ -2262,17 +2658,21 @@ var ImageTool = class {
2262
2658
  id: "image.cropFrame",
2263
2659
  type: "rect",
2264
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
+ },
2265
2669
  props: {
2266
- left: frame.left + frame.width / 2,
2267
- top: frame.top + frame.height / 2,
2268
- width: frame.width,
2269
- height: frame.height,
2270
- originX: "center",
2271
- originY: "center",
2670
+ originX: "left",
2671
+ originY: "top",
2272
2672
  fill: visual.innerBackground,
2273
2673
  stroke: visual.strokeStyle === "hidden" ? "rgba(0,0,0,0)" : visual.strokeColor,
2274
- strokeWidth: visual.strokeStyle === "hidden" ? 0 : visual.strokeWidth,
2275
- 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,
2276
2676
  selectable: false,
2277
2677
  evented: false
2278
2678
  }
@@ -2323,10 +2723,8 @@ var ImageTool = class {
2323
2723
  const sceneGeometry = await this.resolveSceneGeometryForOverlay();
2324
2724
  if (seq !== this.renderSeq) return;
2325
2725
  const overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
2326
- await this.canvasService.applyObjectSpecsToRootLayer(
2327
- IMAGE_OVERLAY_LAYER_ID,
2328
- overlaySpecs
2329
- );
2726
+ this.overlaySpecs = overlaySpecs;
2727
+ await this.canvasService.flushRenderFromProducers();
2330
2728
  this.syncImageZOrder(renderItems);
2331
2729
  const overlayCanvasCount = this.getOverlayObjects().length;
2332
2730
  this.debug("render:done", {
@@ -2417,7 +2815,7 @@ var ImageTool = class {
2417
2815
  const source = this.getSourceSize(render.src, obj);
2418
2816
  const frame = this.getFrameRect();
2419
2817
  const coverScale = this.getCoverScale(frame, source);
2420
- const currentScale = obj.scaleX || 1;
2818
+ const currentScale = this.toSceneObjectScale(obj.scaleX || 1);
2421
2819
  const zoom = Math.max(0.05, currentScale / coverScale);
2422
2820
  const updated = {
2423
2821
  scale: Number.isFinite(zoom) ? zoom : 1,
@@ -2454,12 +2852,13 @@ var ImageTool = class {
2454
2852
  Math.max(1, area.width) / Math.max(1, source.width),
2455
2853
  Math.max(1, area.height) / Math.max(1, source.height)
2456
2854
  );
2457
- const canvasW = this.canvasService.canvas.width || 1;
2458
- 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;
2459
2858
  const areaLeftInput = (_a = area.left) != null ? _a : 0.5;
2460
2859
  const areaTopInput = (_b = area.top) != null ? _b : 0.5;
2461
- const areaLeftPx = areaLeftInput <= 1.5 ? areaLeftInput * canvasW : areaLeftInput;
2462
- 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;
2463
2862
  const updates = {
2464
2863
  scale: Math.max(0.05, desiredScale / baseCover),
2465
2864
  left: this.clampNormalized(
@@ -2497,6 +2896,8 @@ var ImageTool = class {
2497
2896
  this.normalizeItem({
2498
2897
  ...item,
2499
2898
  url,
2899
+ // Keep original source for next image-tool session editing,
2900
+ // and use committedUrl as non-image-tools render source.
2500
2901
  sourceUrl,
2501
2902
  committedUrl: url
2502
2903
  })
@@ -2522,7 +2923,8 @@ var ImageTool = class {
2522
2923
  if (!normalizedIds.length) {
2523
2924
  throw new Error("image-ids-required");
2524
2925
  }
2525
- const frame = this.getFrameRect();
2926
+ const frameScene = this.getFrameRect();
2927
+ const frame = this.getFrameRectScreen(frameScene);
2526
2928
  const multiplier = Math.max(1, (_a = options.multiplier) != null ? _a : 2);
2527
2929
  const format = options.format === "jpeg" ? "jpeg" : "png";
2528
2930
  const width = Math.max(1, Math.round(frame.width * multiplier));
@@ -3400,14 +3802,10 @@ var ImageTracer = class {
3400
3802
  };
3401
3803
  }
3402
3804
  const baseUnpaddedContours = baseContours.map(
3403
- (contour) => this.clampPointsToImageBounds(
3404
- contour.map((p) => ({
3405
- x: p.x - padding,
3406
- y: p.y - padding
3407
- })),
3408
- width,
3409
- height
3410
- )
3805
+ (contour) => contour.map((p) => ({
3806
+ x: p.x - padding,
3807
+ y: p.y - padding
3808
+ }))
3411
3809
  ).filter((contour) => contour.length > 2);
3412
3810
  if (!baseUnpaddedContours.length) {
3413
3811
  const w = (_h = options.scaleToWidth) != null ? _h : width;
@@ -3496,13 +3894,46 @@ var ImageTracer = class {
3496
3894
  this.flattenContours(baseScaledContours)
3497
3895
  );
3498
3896
  }
3499
- debugLog("traceWithBounds:contours", {
3500
- baseContourCount: baseContoursRaw.length,
3501
- baseSelectedCount: baseContours.length,
3502
- expandedContourCount: expandedContoursRaw.length,
3503
- expandedSelectedCount: expandedContours.length,
3504
- baseBounds,
3505
- expandedBounds: globalBounds,
3897
+ if (expand > 0) {
3898
+ const expectedExpandedBounds = {
3899
+ x: baseBounds.x - expand,
3900
+ y: baseBounds.y - expand,
3901
+ width: baseBounds.width + expand * 2,
3902
+ height: baseBounds.height + expand * 2
3903
+ };
3904
+ if (expectedExpandedBounds.width > 0 && expectedExpandedBounds.height > 0 && globalBounds.width > 0 && globalBounds.height > 0) {
3905
+ const shouldNormalizeExpandBounds = Math.abs(globalBounds.x - expectedExpandedBounds.x) > 1 || Math.abs(globalBounds.y - expectedExpandedBounds.y) > 1 || Math.abs(globalBounds.width - expectedExpandedBounds.width) > 1 || Math.abs(globalBounds.height - expectedExpandedBounds.height) > 1;
3906
+ if (shouldNormalizeExpandBounds) {
3907
+ const beforeNormalize = globalBounds;
3908
+ finalContours = this.translateContours(
3909
+ this.scaleContours(
3910
+ finalContours,
3911
+ expectedExpandedBounds.width,
3912
+ expectedExpandedBounds.height,
3913
+ globalBounds
3914
+ ),
3915
+ expectedExpandedBounds.x,
3916
+ expectedExpandedBounds.y
3917
+ );
3918
+ globalBounds = this.boundsFromPoints(
3919
+ this.flattenContours(finalContours)
3920
+ );
3921
+ debugLog("traceWithBounds:expand-normalized", {
3922
+ expand,
3923
+ expectedExpandedBounds,
3924
+ beforeNormalize,
3925
+ afterNormalize: globalBounds
3926
+ });
3927
+ }
3928
+ }
3929
+ }
3930
+ debugLog("traceWithBounds:contours", {
3931
+ baseContourCount: baseContoursRaw.length,
3932
+ baseSelectedCount: baseContours.length,
3933
+ expandedContourCount: expandedContoursRaw.length,
3934
+ expandedSelectedCount: expandedContours.length,
3935
+ baseBounds,
3936
+ expandedBounds: globalBounds,
3506
3937
  expandedDeltaX: globalBounds.width - baseBounds.width,
3507
3938
  expandedDeltaY: globalBounds.height - baseBounds.height,
3508
3939
  expandedMayOverflowImageBounds: expand > 0,
@@ -3858,13 +4289,13 @@ var ImageTracer = class {
3858
4289
  (points) => this.scalePoints(points, targetWidth, targetHeight, bounds)
3859
4290
  );
3860
4291
  }
3861
- static clampPointsToImageBounds(points, width, height) {
3862
- const maxX = Math.max(0, width);
3863
- const maxY = Math.max(0, height);
3864
- return points.map((p) => ({
3865
- x: Math.max(0, Math.min(maxX, p.x)),
3866
- y: Math.max(0, Math.min(maxY, p.y))
3867
- }));
4292
+ static translateContours(contours, offsetX, offsetY) {
4293
+ return contours.map(
4294
+ (points) => points.map((p) => ({
4295
+ x: p.x + offsetX,
4296
+ y: p.y + offsetY
4297
+ }))
4298
+ );
3868
4299
  }
3869
4300
  static pointsToSVG(points) {
3870
4301
  if (points.length === 0) return "";
@@ -3917,6 +4348,7 @@ var ImageTracer = class {
3917
4348
 
3918
4349
  // src/extensions/dieline.ts
3919
4350
  var IMAGE_OBJECT_LAYER_ID2 = "image.user";
4351
+ var DIELINE_LAYER_ID = "dieline-overlay";
3920
4352
  var DielineTool = class {
3921
4353
  constructor(options) {
3922
4354
  this.id = "pooder.kit.dieline";
@@ -3924,8 +4356,8 @@ var DielineTool = class {
3924
4356
  name: "DielineTool"
3925
4357
  };
3926
4358
  this.state = {
3927
- displayUnit: "mm",
3928
- shape: "rect",
4359
+ shape: DEFAULT_DIELINE_SHAPE,
4360
+ shapeStyle: { ...DEFAULT_DIELINE_SHAPE_STYLE },
3929
4361
  width: 500,
3930
4362
  height: 500,
3931
4363
  radius: 0,
@@ -3948,6 +4380,8 @@ var DielineTool = class {
3948
4380
  showBleedLines: true,
3949
4381
  features: []
3950
4382
  };
4383
+ this.specs = [];
4384
+ this.renderSeq = 0;
3951
4385
  this.onCanvasResized = () => {
3952
4386
  this.updateDieline();
3953
4387
  };
@@ -3960,24 +4394,50 @@ var DielineTool = class {
3960
4394
  Object.assign(this.state.offsetLine, options.offsetLine);
3961
4395
  delete options.offsetLine;
3962
4396
  }
4397
+ if (options.shapeStyle) {
4398
+ this.state.shapeStyle = normalizeShapeStyle(
4399
+ options.shapeStyle,
4400
+ this.state.shapeStyle
4401
+ );
4402
+ delete options.shapeStyle;
4403
+ }
3963
4404
  Object.assign(this.state, options);
4405
+ this.state.shape = normalizeDielineShape(options.shape, this.state.shape);
3964
4406
  }
3965
4407
  }
3966
4408
  activate(context) {
4409
+ var _a;
3967
4410
  this.context = context;
3968
4411
  this.canvasService = context.services.get("CanvasService");
3969
4412
  if (!this.canvasService) {
3970
4413
  console.warn("CanvasService not found for DielineTool");
3971
4414
  return;
3972
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
+ );
3973
4427
  const configService = context.services.get(
3974
4428
  "ConfigurationService"
3975
4429
  );
3976
4430
  if (configService) {
3977
4431
  const s = this.state;
3978
4432
  const sizeState = readSizeState(configService);
3979
- s.displayUnit = sizeState.unit;
3980
- 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
+ );
3981
4441
  s.width = sizeState.actualWidthMm;
3982
4442
  s.height = sizeState.actualHeightMm;
3983
4443
  s.radius = parseLengthToMm(
@@ -4026,10 +4486,17 @@ var DielineTool = class {
4026
4486
  );
4027
4487
  s.features = configService.get("dieline.features", s.features);
4028
4488
  s.pathData = configService.get("dieline.pathData", s.pathData);
4489
+ const sourceWidth = Number(
4490
+ configService.get("dieline.customSourceWidthPx", 0)
4491
+ );
4492
+ const sourceHeight = Number(
4493
+ configService.get("dieline.customSourceHeightPx", 0)
4494
+ );
4495
+ s.customSourceWidthPx = Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : void 0;
4496
+ s.customSourceHeightPx = Number.isFinite(sourceHeight) && sourceHeight > 0 ? sourceHeight : void 0;
4029
4497
  configService.onAnyChange((e) => {
4030
4498
  if (e.key.startsWith("size.")) {
4031
4499
  const nextSize = readSizeState(configService);
4032
- s.displayUnit = nextSize.unit;
4033
4500
  s.width = nextSize.actualWidthMm;
4034
4501
  s.height = nextSize.actualHeightMm;
4035
4502
  s.padding = nextSize.viewPadding;
@@ -4040,7 +4507,10 @@ var DielineTool = class {
4040
4507
  if (e.key.startsWith("dieline.")) {
4041
4508
  switch (e.key) {
4042
4509
  case "dieline.shape":
4043
- 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);
4044
4514
  break;
4045
4515
  case "dieline.radius":
4046
4516
  s.radius = parseLengthToMm(e.value, "mm");
@@ -4084,18 +4554,30 @@ var DielineTool = class {
4084
4554
  case "dieline.pathData":
4085
4555
  s.pathData = e.value;
4086
4556
  break;
4557
+ case "dieline.customSourceWidthPx":
4558
+ s.customSourceWidthPx = Number.isFinite(Number(e.value)) && Number(e.value) > 0 ? Number(e.value) : void 0;
4559
+ break;
4560
+ case "dieline.customSourceHeightPx":
4561
+ s.customSourceHeightPx = Number.isFinite(Number(e.value)) && Number(e.value) > 0 ? Number(e.value) : void 0;
4562
+ break;
4087
4563
  }
4088
4564
  this.updateDieline();
4089
4565
  }
4090
4566
  });
4091
4567
  }
4092
4568
  context.eventBus.on("canvas:resized", this.onCanvasResized);
4093
- this.createLayer();
4094
4569
  this.updateDieline();
4095
4570
  }
4096
4571
  deactivate(context) {
4572
+ var _a;
4097
4573
  context.eventBus.off("canvas:resized", this.onCanvasResized);
4098
- 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
+ }
4099
4581
  this.canvasService = void 0;
4100
4582
  this.context = void 0;
4101
4583
  }
@@ -4118,7 +4600,7 @@ var DielineTool = class {
4118
4600
  id: "dieline.shape",
4119
4601
  type: "select",
4120
4602
  label: "Shape",
4121
- options: ["rect", "circle", "ellipse", "custom"],
4603
+ options: Array.from(DIELINE_SHAPES),
4122
4604
  default: s.shape
4123
4605
  },
4124
4606
  {
@@ -4129,6 +4611,12 @@ var DielineTool = class {
4129
4611
  max: 500,
4130
4612
  default: s.radius
4131
4613
  },
4614
+ {
4615
+ id: "dieline.shapeStyle",
4616
+ type: "json",
4617
+ label: "Shape Style",
4618
+ default: s.shapeStyle
4619
+ },
4132
4620
  {
4133
4621
  id: "dieline.showBleedLines",
4134
4622
  type: "boolean",
@@ -4304,34 +4792,6 @@ var DielineTool = class {
4304
4792
  ]
4305
4793
  };
4306
4794
  }
4307
- getLayer() {
4308
- var _a;
4309
- return (_a = this.canvasService) == null ? void 0 : _a.getLayer("dieline-overlay");
4310
- }
4311
- createLayer() {
4312
- if (!this.canvasService) return;
4313
- const width = this.canvasService.canvas.width || 800;
4314
- const height = this.canvasService.canvas.height || 600;
4315
- const layer = this.canvasService.createLayer("dieline-overlay", {
4316
- width,
4317
- height,
4318
- selectable: false,
4319
- evented: false
4320
- });
4321
- this.canvasService.canvas.bringObjectToFront(layer);
4322
- const userLayer = this.canvasService.getLayer("user");
4323
- if (userLayer) {
4324
- const userIndex = this.canvasService.canvas.getObjects().indexOf(userLayer);
4325
- this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
4326
- }
4327
- }
4328
- destroyLayer() {
4329
- if (!this.canvasService) return;
4330
- const layer = this.getLayer();
4331
- if (layer) {
4332
- this.canvasService.canvas.remove(layer);
4333
- }
4334
- }
4335
4795
  createHatchPattern(color = "rgba(0, 0, 0, 0.3)") {
4336
4796
  if (typeof document === "undefined") {
4337
4797
  return void 0;
@@ -4360,7 +4820,6 @@ var DielineTool = class {
4360
4820
  }
4361
4821
  syncSizeState(configService) {
4362
4822
  const sizeState = readSizeState(configService);
4363
- this.state.displayUnit = sizeState.unit;
4364
4823
  this.state.width = sizeState.actualWidthMm;
4365
4824
  this.state.height = sizeState.actualHeightMm;
4366
4825
  this.state.padding = sizeState.viewPadding;
@@ -4374,20 +4833,26 @@ var DielineTool = class {
4374
4833
  return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.type) === "feature-marker";
4375
4834
  }).forEach((obj) => canvas.bringObjectToFront(obj));
4376
4835
  }
4377
- updateDieline(_emitEvent = true) {
4836
+ ensureLayerStacking() {
4378
4837
  if (!this.canvasService) return;
4379
- const layer = this.getLayer();
4838
+ const layer = this.canvasService.getLayer(DIELINE_LAYER_ID);
4380
4839
  if (!layer) return;
4381
- const configService = this.getConfigService();
4382
- if (!configService) return;
4383
- this.syncSizeState(configService);
4384
- const sceneLayout = computeSceneLayout(
4385
- this.canvasService,
4386
- readSizeState(configService)
4387
- );
4388
- 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;
4389
4853
  const {
4390
4854
  shape,
4855
+ shapeStyle,
4391
4856
  radius,
4392
4857
  mainLine,
4393
4858
  offsetLine,
@@ -4396,8 +4861,8 @@ var DielineTool = class {
4396
4861
  showBleedLines,
4397
4862
  features
4398
4863
  } = this.state;
4399
- const canvasW = sceneLayout.canvasWidth || this.canvasService.canvas.width || 800;
4400
- 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;
4401
4866
  const scale = sceneLayout.scale;
4402
4867
  const cx = sceneLayout.trimRect.centerX;
4403
4868
  const cy = sceneLayout.trimRect.centerY;
@@ -4408,7 +4873,6 @@ var DielineTool = class {
4408
4873
  const cutH = sceneLayout.cutRect.height;
4409
4874
  const visualOffset = (cutW - visualWidth) / 2;
4410
4875
  const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
4411
- layer.remove(...layer.getObjects());
4412
4876
  const absoluteFeatures = (features || []).map((f) => ({
4413
4877
  ...f,
4414
4878
  x: f.x,
@@ -4428,19 +4892,30 @@ var DielineTool = class {
4428
4892
  x: cx,
4429
4893
  y: cy,
4430
4894
  features: cutFeatures,
4431
- pathData: this.state.pathData
4432
- });
4433
- const mask = new import_fabric3.Path(maskPathData, {
4434
- fill: outsideColor,
4435
- stroke: null,
4436
- selectable: false,
4437
- evented: false,
4438
- originX: "left",
4439
- originY: "top",
4440
- left: 0,
4441
- top: 0
4895
+ shapeStyle,
4896
+ pathData: this.state.pathData,
4897
+ customSourceWidthPx: this.state.customSourceWidthPx,
4898
+ customSourceHeightPx: this.state.customSourceHeightPx
4442
4899
  });
4443
- 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
+ ];
4444
4919
  if (insideColor && insideColor !== "transparent" && insideColor !== "rgba(0,0,0,0)") {
4445
4920
  const productPathData = generateDielinePath({
4446
4921
  shape,
@@ -4450,19 +4925,28 @@ var DielineTool = class {
4450
4925
  x: cx,
4451
4926
  y: cy,
4452
4927
  features: cutFeatures,
4928
+ shapeStyle,
4453
4929
  pathData: this.state.pathData,
4930
+ customSourceWidthPx: this.state.customSourceWidthPx,
4931
+ customSourceHeightPx: this.state.customSourceHeightPx,
4454
4932
  canvasWidth: canvasW,
4455
4933
  canvasHeight: canvasH
4456
4934
  });
4457
- const insideObj = new import_fabric3.Path(productPathData, {
4458
- fill: insideColor,
4459
- stroke: null,
4460
- selectable: false,
4461
- evented: false,
4462
- originX: "left",
4463
- 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
+ }
4464
4949
  });
4465
- layer.add(insideObj);
4466
4950
  }
4467
4951
  if (Math.abs(visualOffset) > 1e-4) {
4468
4952
  const bleedPathData = generateBleedZonePath(
@@ -4474,7 +4958,10 @@ var DielineTool = class {
4474
4958
  x: cx,
4475
4959
  y: cy,
4476
4960
  features: cutFeatures,
4961
+ shapeStyle,
4477
4962
  pathData: this.state.pathData,
4963
+ customSourceWidthPx: this.state.customSourceWidthPx,
4964
+ customSourceHeightPx: this.state.customSourceHeightPx,
4478
4965
  canvasWidth: canvasW,
4479
4966
  canvasHeight: canvasH
4480
4967
  },
@@ -4486,7 +4973,10 @@ var DielineTool = class {
4486
4973
  x: cx,
4487
4974
  y: cy,
4488
4975
  features: cutFeatures,
4976
+ shapeStyle,
4489
4977
  pathData: this.state.pathData,
4978
+ customSourceWidthPx: this.state.customSourceWidthPx,
4979
+ customSourceHeightPx: this.state.customSourceHeightPx,
4490
4980
  canvasWidth: canvasW,
4491
4981
  canvasHeight: canvasH
4492
4982
  },
@@ -4495,16 +4985,22 @@ var DielineTool = class {
4495
4985
  if (showBleedLines !== false) {
4496
4986
  const pattern = this.createHatchPattern(mainLine.color);
4497
4987
  if (pattern) {
4498
- const bleedObj = new import_fabric3.Path(bleedPathData, {
4499
- fill: pattern,
4500
- stroke: null,
4501
- selectable: false,
4502
- evented: false,
4503
- objectCaching: false,
4504
- originX: "left",
4505
- 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
+ }
4506
5003
  });
4507
- layer.add(bleedObj);
4508
5004
  }
4509
5005
  }
4510
5006
  const offsetPathData = generateDielinePath({
@@ -4515,21 +5011,30 @@ var DielineTool = class {
4515
5011
  x: cx,
4516
5012
  y: cy,
4517
5013
  features: cutFeatures,
5014
+ shapeStyle,
4518
5015
  pathData: this.state.pathData,
5016
+ customSourceWidthPx: this.state.customSourceWidthPx,
5017
+ customSourceHeightPx: this.state.customSourceHeightPx,
4519
5018
  canvasWidth: canvasW,
4520
5019
  canvasHeight: canvasH
4521
5020
  });
4522
- const offsetBorderObj = new import_fabric3.Path(offsetPathData, {
4523
- fill: null,
4524
- stroke: offsetLine.style === "hidden" ? null : offsetLine.color,
4525
- strokeWidth: offsetLine.width,
4526
- strokeDashArray: offsetLine.style === "dashed" ? [offsetLine.dashLength, offsetLine.dashLength] : void 0,
4527
- selectable: false,
4528
- evented: false,
4529
- originX: "left",
4530
- 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
+ }
4531
5037
  });
4532
- layer.add(offsetBorderObj);
4533
5038
  }
4534
5039
  const borderPathData = generateDielinePath({
4535
5040
  shape,
@@ -4539,37 +5044,59 @@ var DielineTool = class {
4539
5044
  x: cx,
4540
5045
  y: cy,
4541
5046
  features: absoluteFeatures,
5047
+ shapeStyle,
4542
5048
  pathData: this.state.pathData,
5049
+ customSourceWidthPx: this.state.customSourceWidthPx,
5050
+ customSourceHeightPx: this.state.customSourceHeightPx,
4543
5051
  canvasWidth: canvasW,
4544
5052
  canvasHeight: canvasH
4545
5053
  });
4546
- const borderObj = new import_fabric3.Path(borderPathData, {
4547
- fill: "transparent",
4548
- stroke: mainLine.style === "hidden" ? null : mainLine.color,
4549
- strokeWidth: mainLine.width,
4550
- strokeDashArray: mainLine.style === "dashed" ? [mainLine.dashLength, mainLine.dashLength] : void 0,
4551
- selectable: false,
4552
- evented: false,
4553
- originX: "left",
4554
- originY: "top"
4555
- });
4556
- layer.add(borderObj);
4557
- const userLayer = this.canvasService.getLayer("user");
4558
- if (layer && userLayer) {
4559
- const layerIndex = this.canvasService.canvas.getObjects().indexOf(layer);
4560
- const userIndex = this.canvasService.canvas.getObjects().indexOf(userLayer);
4561
- if (layerIndex < userIndex) {
4562
- 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"
4563
5069
  }
4564
- } else {
4565
- 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;
4566
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();
4567
5098
  this.bringFeatureMarkersToFront();
4568
- const rulerLayer = this.canvasService.getLayer("ruler-overlay");
4569
- if (rulerLayer) {
4570
- this.canvasService.canvas.bringObjectToFront(rulerLayer);
4571
- }
4572
- layer.dirty = true;
5099
+ this.canvasService.bringLayerToFront("ruler-overlay");
4573
5100
  this.canvasService.requestRenderAll();
4574
5101
  }
4575
5102
  getGeometry() {
@@ -4585,7 +5112,9 @@ var DielineTool = class {
4585
5112
  return {
4586
5113
  ...sceneGeometry,
4587
5114
  strokeWidth: this.state.mainLine.width,
4588
- pathData: this.state.pathData
5115
+ pathData: this.state.pathData,
5116
+ customSourceWidthPx: this.state.customSourceWidthPx,
5117
+ customSourceHeightPx: this.state.customSourceHeightPx
4589
5118
  };
4590
5119
  }
4591
5120
  async exportCutImage(options) {
@@ -4615,7 +5144,7 @@ var DielineTool = class {
4615
5144
  );
4616
5145
  return null;
4617
5146
  }
4618
- const { shape, radius, features, pathData } = this.state;
5147
+ const { shape, shapeStyle, radius, features, pathData } = this.state;
4619
5148
  const canvasW = sceneLayout.canvasWidth || this.canvasService.canvas.width || 800;
4620
5149
  const canvasH = sceneLayout.canvasHeight || this.canvasService.canvas.height || 600;
4621
5150
  const scale = sceneLayout.scale;
@@ -4643,7 +5172,10 @@ var DielineTool = class {
4643
5172
  x: cx,
4644
5173
  y: cy,
4645
5174
  features: cutFeatures,
5175
+ shapeStyle,
4646
5176
  pathData,
5177
+ customSourceWidthPx: this.state.customSourceWidthPx,
5178
+ customSourceHeightPx: this.state.customSourceHeightPx,
4647
5179
  canvasWidth: canvasW,
4648
5180
  canvasHeight: canvasH
4649
5181
  });
@@ -4753,7 +5285,6 @@ var DielineTool = class {
4753
5285
 
4754
5286
  // src/extensions/feature.ts
4755
5287
  var import_core5 = require("@pooder/core");
4756
- var import_fabric4 = require("fabric");
4757
5288
 
4758
5289
  // src/extensions/constraints.ts
4759
5290
  var ConstraintRegistry = class {
@@ -4953,6 +5484,10 @@ function completeFeaturesStrict(features, context, update) {
4953
5484
  }
4954
5485
 
4955
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;
4956
5491
  var FeatureTool = class {
4957
5492
  constructor(options) {
4958
5493
  this.id = "pooder.kit.feature";
@@ -4965,6 +5500,8 @@ var FeatureTool = class {
4965
5500
  this.isFeatureSessionActive = false;
4966
5501
  this.sessionOriginalFeatures = null;
4967
5502
  this.hasWorkingChanges = false;
5503
+ this.specs = [];
5504
+ this.renderSeq = 0;
4968
5505
  this.handleMoving = null;
4969
5506
  this.handleModified = null;
4970
5507
  this.handleSceneGeometryChange = null;
@@ -4981,12 +5518,23 @@ var FeatureTool = class {
4981
5518
  }
4982
5519
  }
4983
5520
  activate(context) {
5521
+ var _a;
4984
5522
  this.context = context;
4985
5523
  this.canvasService = context.services.get("CanvasService");
4986
5524
  if (!this.canvasService) {
4987
5525
  console.warn("CanvasService not found for FeatureTool");
4988
5526
  return;
4989
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
+ );
4990
5538
  const configService = context.services.get(
4991
5539
  "ConfigurationService"
4992
5540
  );
@@ -5025,21 +5573,7 @@ var FeatureTool = class {
5025
5573
  this.context = void 0;
5026
5574
  }
5027
5575
  updateVisibility() {
5028
- if (!this.canvasService) return;
5029
- const canvas = this.canvasService.canvas;
5030
- const markers = canvas.getObjects().filter((obj) => {
5031
- var _a;
5032
- return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
5033
- });
5034
- markers.forEach((marker) => {
5035
- marker.set({
5036
- visible: this.isToolActive,
5037
- // Or just selectable: false if we want them visible but locked
5038
- selectable: this.isToolActive,
5039
- evented: this.isToolActive
5040
- });
5041
- });
5042
- canvas.requestRenderAll();
5576
+ this.redraw();
5043
5577
  }
5044
5578
  contribute() {
5045
5579
  return {
@@ -5271,8 +5805,7 @@ var FeatureTool = class {
5271
5805
  if (!changed) return { ok: true };
5272
5806
  this.setWorkingFeatures(next);
5273
5807
  this.hasWorkingChanges = true;
5274
- this.redraw();
5275
- this.enforceConstraints();
5808
+ this.redraw({ enforceConstraints: true });
5276
5809
  this.emitWorkingChange();
5277
5810
  return { ok: true };
5278
5811
  }
@@ -5320,12 +5853,10 @@ var FeatureTool = class {
5320
5853
  shape: "rect",
5321
5854
  x: 0.5,
5322
5855
  y: 0,
5323
- // Top edge
5324
5856
  width: 10,
5325
5857
  height: 10,
5326
5858
  rotation: 0,
5327
5859
  renderBehavior: "edge",
5328
- // Default constraint: path (snap to edge)
5329
5860
  constraints: [{ type: "path" }]
5330
5861
  };
5331
5862
  this.setWorkingFeatures([...this.workingFeatures || [], newFeature]);
@@ -5368,7 +5899,7 @@ var FeatureTool = class {
5368
5899
  this.emitWorkingChange();
5369
5900
  return true;
5370
5901
  }
5371
- getGeometryForFeature(geometry, feature) {
5902
+ getGeometryForFeature(geometry, _feature) {
5372
5903
  return geometry;
5373
5904
  }
5374
5905
  setup() {
@@ -5377,8 +5908,7 @@ var FeatureTool = class {
5377
5908
  if (!this.handleSceneGeometryChange) {
5378
5909
  this.handleSceneGeometryChange = (geometry) => {
5379
5910
  this.currentGeometry = geometry;
5380
- this.redraw();
5381
- this.enforceConstraints();
5911
+ this.redraw({ enforceConstraints: true });
5382
5912
  };
5383
5913
  this.context.eventBus.on(
5384
5914
  "scene:geometry:change",
@@ -5401,76 +5931,34 @@ var FeatureTool = class {
5401
5931
  }
5402
5932
  if (!this.handleMoving) {
5403
5933
  this.handleMoving = (e) => {
5404
- var _a, _b, _c, _d;
5405
- const target = e.target;
5406
- if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return;
5407
- if (!this.currentGeometry) return;
5408
- let feature;
5409
- if ((_b = target.data) == null ? void 0 : _b.isGroup) {
5410
- const indices = (_c = target.data) == null ? void 0 : _c.indices;
5411
- if (indices && indices.length > 0) {
5412
- feature = this.workingFeatures[indices[0]];
5413
- }
5414
- } else {
5415
- const index = (_d = target.data) == null ? void 0 : _d.index;
5416
- if (index !== void 0) {
5417
- feature = this.workingFeatures[index];
5418
- }
5419
- }
5420
- const geometry = this.getGeometryForFeature(
5421
- 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,
5422
5944
  feature
5423
5945
  );
5424
- const p = new import_fabric4.Point(target.left, target.top);
5425
- const markerStrokeWidth = (target.strokeWidth || 2) * (target.scaleX || 1);
5426
- const minDim = Math.min(
5427
- target.getScaledWidth(),
5428
- target.getScaledHeight()
5429
- );
5430
- const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
5431
- const snapped = this.constrainPosition(p, geometry, limit, feature);
5432
5946
  target.set({
5433
5947
  left: snapped.x,
5434
5948
  top: snapped.y
5435
5949
  });
5950
+ target.setCoords();
5951
+ this.syncMarkerVisualsByTarget(target, snapped);
5436
5952
  };
5437
5953
  canvas.on("object:moving", this.handleMoving);
5438
5954
  }
5439
5955
  if (!this.handleModified) {
5440
5956
  this.handleModified = (e) => {
5441
- var _a, _b, _c;
5442
- const target = e.target;
5443
- if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return;
5444
- if ((_b = target.data) == null ? void 0 : _b.isGroup) {
5445
- const groupObj = target;
5446
- const indices = (_c = groupObj.data) == null ? void 0 : _c.indices;
5447
- if (!indices) return;
5448
- const groupCenter = new import_fabric4.Point(groupObj.left, groupObj.top);
5449
- const newFeatures = [...this.workingFeatures];
5450
- const { x, y } = this.currentGeometry;
5451
- groupObj.getObjects().forEach((child, i) => {
5452
- const originalIndex = indices[i];
5453
- const feature = this.workingFeatures[originalIndex];
5454
- const geometry = this.getGeometryForFeature(
5455
- this.currentGeometry,
5456
- feature
5457
- );
5458
- const { width, height } = geometry;
5459
- const layoutLeft = x - width / 2;
5460
- const layoutTop = y - height / 2;
5461
- const absX = groupCenter.x + (child.left || 0);
5462
- const absY = groupCenter.y + (child.top || 0);
5463
- const normalizedX = width > 0 ? (absX - layoutLeft) / width : 0.5;
5464
- const normalizedY = height > 0 ? (absY - layoutTop) / height : 0.5;
5465
- newFeatures[originalIndex] = {
5466
- ...newFeatures[originalIndex],
5467
- x: normalizedX,
5468
- y: normalizedY
5469
- };
5470
- });
5471
- this.setWorkingFeatures(newFeatures);
5472
- this.hasWorkingChanges = true;
5473
- 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);
5474
5962
  } else {
5475
5963
  this.syncFeatureFromCanvas(target);
5476
5964
  }
@@ -5479,6 +5967,7 @@ var FeatureTool = class {
5479
5967
  }
5480
5968
  }
5481
5969
  teardown() {
5970
+ var _a;
5482
5971
  if (!this.canvasService) return;
5483
5972
  const canvas = this.canvasService.canvas;
5484
5973
  if (this.handleMoving) {
@@ -5496,14 +5985,25 @@ var FeatureTool = class {
5496
5985
  );
5497
5986
  this.handleSceneGeometryChange = null;
5498
5987
  }
5499
- const objects = canvas.getObjects().filter((obj) => {
5500
- var _a;
5501
- return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
5502
- });
5503
- objects.forEach((obj) => canvas.remove(obj));
5504
- 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;
5505
5999
  }
5506
- constrainPosition(p, geometry, limit, feature) {
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];
6005
+ }
6006
+ constrainPosition(p, geometry, feature) {
5507
6007
  var _a;
5508
6008
  if (!feature) {
5509
6009
  return { x: p.x, y: p.y };
@@ -5534,231 +6034,374 @@ var FeatureTool = class {
5534
6034
  y: minY + constrained.y * geometry.height
5535
6035
  };
5536
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
+ }
5537
6045
  syncFeatureFromCanvas(target) {
5538
6046
  var _a;
5539
- if (!this.currentGeometry || !this.context) return;
5540
- const index = (_a = target.data) == null ? void 0 : _a.index;
5541
- if (index === void 0 || index < 0 || index >= this.workingFeatures.length)
5542
- 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;
5543
6050
  const feature = this.workingFeatures[index];
5544
6051
  const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
5545
- const { width, height, x, y } = geometry;
5546
- const left = x - width / 2;
5547
- const top = y - height / 2;
5548
- const normalizedX = width > 0 ? (target.left - left) / width : 0.5;
5549
- 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
+ );
5550
6059
  const updatedFeature = {
5551
6060
  ...feature,
5552
- x: normalizedX,
5553
- y: normalizedY
5554
- // Could also update rotation if we allowed rotating markers
6061
+ x: normalized.x,
6062
+ y: normalized.y
5555
6063
  };
5556
- const newFeatures = [...this.workingFeatures];
5557
- newFeatures[index] = updatedFeature;
5558
- this.setWorkingFeatures(newFeatures);
6064
+ const next = [...this.workingFeatures];
6065
+ next[index] = updatedFeature;
6066
+ this.setWorkingFeatures(next);
5559
6067
  this.hasWorkingChanges = true;
5560
6068
  this.emitWorkingChange();
5561
6069
  }
5562
- redraw() {
5563
- if (!this.canvasService || !this.currentGeometry) return;
5564
- const canvas = this.canvasService.canvas;
5565
- const geometry = this.currentGeometry;
5566
- const existing = canvas.getObjects().filter((obj) => {
5567
- var _a;
5568
- 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
+ }
5569
6102
  });
5570
- existing.forEach((obj) => canvas.remove(obj));
5571
- if (!this.workingFeatures || this.workingFeatures.length === 0) {
5572
- this.canvasService.requestRenderAll();
5573
- 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();
5574
6121
  }
5575
- const scale = geometry.scale || 1;
5576
- const finalScale = scale;
5577
- 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();
5578
6133
  const singles = [];
5579
- this.workingFeatures.forEach((f, i) => {
5580
- if (f.groupId) {
5581
- if (!groups[f.groupId]) groups[f.groupId] = [];
5582
- groups[f.groupId].push({ feature: f, index: i });
5583
- } else {
5584
- 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;
5585
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
+ });
5586
6184
  });
5587
- const createMarkerShape = (feature, pos) => {
5588
- const featureScale = scale;
5589
- const visualWidth = (feature.width || 10) * featureScale;
5590
- const visualHeight = (feature.height || 10) * featureScale;
5591
- const visualRadius = (feature.radius || 0) * featureScale;
5592
- const color = feature.color || (feature.operation === "add" ? "#00FF00" : "#FF0000");
5593
- const strokeDash = feature.strokeDash || (feature.operation === "subtract" ? [4, 4] : void 0);
5594
- let shape;
5595
- if (feature.shape === "rect") {
5596
- 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,
5597
6229
  width: visualWidth,
5598
6230
  height: visualHeight,
5599
6231
  rx: visualRadius,
5600
- ry: visualRadius,
5601
- fill: "transparent",
5602
- stroke: color,
5603
- strokeWidth: 2,
5604
- strokeDashArray: strokeDash,
5605
- originX: "center",
5606
- originY: "center",
5607
- left: pos.x,
5608
- top: pos.y
5609
- });
5610
- } else {
5611
- shape = new import_fabric4.Circle({
5612
- radius: visualRadius || 5 * finalScale,
5613
- fill: "transparent",
5614
- stroke: color,
5615
- strokeWidth: 2,
5616
- strokeDashArray: strokeDash,
5617
- originX: "center",
5618
- originY: "center",
5619
- left: pos.x,
5620
- top: pos.y
5621
- });
5622
- }
5623
- if (feature.rotation) {
5624
- shape.rotate(feature.rotation);
5625
- }
5626
- if (feature.bridge && feature.bridge.type === "vertical") {
5627
- const bridgeIndicator = new import_fabric4.Rect({
5628
- width: visualWidth,
5629
- height: 100 * featureScale,
5630
- // Arbitrary long length to show direction
5631
- fill: "transparent",
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;
6256
+ }
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,
6271
+ width: visualWidth,
6272
+ height: bridgeHeight,
6273
+ fill: "transparent",
5632
6274
  stroke: "#888",
5633
6275
  strokeWidth: 1,
5634
6276
  strokeDashArray: [2, 2],
5635
- originX: "center",
5636
- originY: "bottom",
5637
- // Anchor at bottom so it extends up
5638
- left: pos.x,
5639
- top: pos.y - visualHeight / 2,
5640
- // Start from top of feature
5641
6277
  opacity: 0.5,
5642
- selectable: false,
5643
- evented: false
5644
- });
5645
- const group = new import_fabric4.Group([bridgeIndicator, shape], {
5646
6278
  originX: "center",
5647
- originY: "center",
5648
- left: pos.x,
5649
- top: pos.y
6279
+ originY: "bottom",
6280
+ left: position.x,
6281
+ top: position.y - visualHeight / 2
6282
+ }
6283
+ });
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;
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
5650
6341
  });
5651
- return group;
6342
+ });
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;
5652
6357
  }
5653
- return shape;
5654
- };
5655
- singles.forEach(({ feature, index }) => {
5656
- const geometry2 = this.getGeometryForFeature(
5657
- this.currentGeometry,
5658
- feature
5659
- );
5660
- const pos = resolveFeaturePosition(feature, geometry2);
5661
- const marker = createMarkerShape(feature, pos);
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);
5662
6363
  marker.set({
5663
- visible: this.isToolActive,
5664
- selectable: this.isToolActive,
5665
- evented: this.isToolActive,
5666
- hasControls: false,
5667
- hasBorders: false,
5668
- hoverCursor: "move",
5669
- lockRotation: true,
5670
- lockScalingX: true,
5671
- lockScalingY: true,
5672
- data: { type: "feature-marker", index, isGroup: false }
6364
+ left: center.x + offsetX,
6365
+ top: center.y + offsetY
5673
6366
  });
5674
- canvas.add(marker);
5675
- canvas.bringObjectToFront(marker);
5676
- });
5677
- Object.keys(groups).forEach((groupId) => {
5678
- const members = groups[groupId];
5679
- if (members.length === 0) return;
5680
- const shapes = members.map(({ feature }) => {
5681
- const geometry2 = this.getGeometryForFeature(
5682
- this.currentGeometry,
5683
- feature
5684
- );
5685
- const pos = resolveFeaturePosition(feature, geometry2);
5686
- return createMarkerShape(feature, pos);
5687
- });
5688
- const groupObj = new import_fabric4.Group(shapes, {
5689
- visible: this.isToolActive,
5690
- selectable: this.isToolActive,
5691
- evented: this.isToolActive,
5692
- hasControls: false,
5693
- hasBorders: false,
5694
- hoverCursor: "move",
5695
- lockRotation: true,
5696
- lockScalingX: true,
5697
- lockScalingY: true,
5698
- subTargetCheck: true,
5699
- // Allow events to pass through if needed, but we treat as one
5700
- interactive: false,
5701
- // Children not interactive
5702
- // @ts-ignore
5703
- data: {
5704
- type: "feature-marker",
5705
- isGroup: true,
5706
- groupId,
5707
- indices: members.map((m) => m.index)
5708
- }
5709
- });
5710
- canvas.add(groupObj);
5711
- canvas.bringObjectToFront(groupObj);
6367
+ marker.setCoords();
5712
6368
  });
5713
- this.canvasService.requestRenderAll();
5714
6369
  }
5715
6370
  enforceConstraints() {
5716
6371
  if (!this.canvasService || !this.currentGeometry) return;
5717
- const canvas = this.canvasService.canvas;
5718
- const markers = canvas.getObjects().filter((obj) => {
5719
- var _a;
5720
- return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
5721
- });
5722
- markers.forEach((marker) => {
5723
- var _a, _b, _c;
5724
- let feature;
5725
- if ((_a = marker.data) == null ? void 0 : _a.isGroup) {
5726
- const indices = (_b = marker.data) == null ? void 0 : _b.indices;
5727
- if (indices && indices.length > 0) {
5728
- feature = this.workingFeatures[indices[0]];
5729
- }
5730
- } else {
5731
- const index = (_c = marker.data) == null ? void 0 : _c.index;
5732
- if (index !== void 0) {
5733
- feature = this.workingFeatures[index];
5734
- }
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";
5735
6376
  }
5736
- const geometry = this.getGeometryForFeature(
5737
- this.currentGeometry,
5738
- feature
5739
- );
5740
- const markerStrokeWidth = (marker.strokeWidth || 2) * (marker.scaleX || 1);
5741
- const minDim = Math.min(
5742
- marker.getScaledWidth(),
5743
- marker.getScaledHeight()
5744
- );
5745
- 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);
5746
6382
  const snapped = this.constrainPosition(
5747
- new import_fabric4.Point(marker.left, marker.top),
6383
+ {
6384
+ x: Number(marker.left || 0),
6385
+ y: Number(marker.top || 0)
6386
+ },
5748
6387
  geometry,
5749
- limit,
5750
6388
  feature
5751
6389
  );
5752
6390
  marker.set({ left: snapped.x, top: snapped.y });
5753
6391
  marker.setCoords();
6392
+ this.syncMarkerVisualsByTarget(marker, snapped);
5754
6393
  });
5755
- canvas.requestRenderAll();
6394
+ this.canvasService.canvas.requestRenderAll();
5756
6395
  }
5757
6396
  };
5758
6397
 
5759
6398
  // src/extensions/film.ts
5760
6399
  var import_core6 = require("@pooder/core");
5761
- 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;
5762
6405
  var FilmTool = class {
5763
6406
  constructor(options) {
5764
6407
  this.id = "pooder.kit.film";
@@ -5767,17 +6410,38 @@ var FilmTool = class {
5767
6410
  };
5768
6411
  this.url = "";
5769
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
+ };
5770
6421
  if (options) {
5771
6422
  Object.assign(this, options);
5772
6423
  }
5773
6424
  }
5774
6425
  activate(context) {
6426
+ var _a;
5775
6427
  this.canvasService = context.services.get("CanvasService");
5776
6428
  if (!this.canvasService) {
5777
6429
  console.warn("CanvasService not found for FilmTool");
5778
6430
  return;
5779
6431
  }
5780
- 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
+ );
5781
6445
  if (configService) {
5782
6446
  this.url = configService.get("film.url", this.url);
5783
6447
  this.opacity = configService.get("film.opacity", this.opacity);
@@ -5794,21 +6458,21 @@ var FilmTool = class {
5794
6458
  }
5795
6459
  });
5796
6460
  }
5797
- this.initLayer();
6461
+ context.eventBus.on("canvas:resized", this.onCanvasResized);
5798
6462
  this.updateFilm();
5799
6463
  }
5800
6464
  deactivate(context) {
5801
- if (this.canvasService) {
5802
- const layer = this.canvasService.getLayer("overlay");
5803
- if (layer) {
5804
- const img = this.canvasService.getObject("film-image", "overlay");
5805
- if (img) {
5806
- layer.remove(img);
5807
- this.canvasService.requestRenderAll();
5808
- }
5809
- }
5810
- this.canvasService = void 0;
5811
- }
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;
5812
6476
  }
5813
6477
  contribute() {
5814
6478
  return {
@@ -5844,73 +6508,108 @@ var FilmTool = class {
5844
6508
  ]
5845
6509
  };
5846
6510
  }
5847
- initLayer() {
5848
- if (!this.canvasService) return;
5849
- let overlayLayer = this.canvasService.getLayer("overlay");
5850
- if (!overlayLayer) {
5851
- const width = this.canvasService.canvas.width || 800;
5852
- const height = this.canvasService.canvas.height || 600;
5853
- const layer = this.canvasService.createLayer("overlay", {
5854
- width,
5855
- height,
5856
- left: 0,
5857
- top: 0,
5858
- originX: "left",
5859
- originY: "top",
5860
- selectable: false,
5861
- evented: false,
5862
- subTargetCheck: false,
5863
- interactive: false
5864
- });
5865
- this.canvasService.canvas.bringObjectToFront(layer);
5866
- }
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
+ };
5867
6519
  }
5868
- async updateFilm() {
5869
- if (!this.canvasService) return;
5870
- const layer = this.canvasService.getLayer("overlay");
5871
- if (!layer) {
5872
- console.warn("[FilmTool] Overlay layer not found");
5873
- return;
5874
- }
5875
- const { url, opacity } = this;
5876
- if (!url) {
5877
- const img2 = this.canvasService.getObject("film-image", "overlay");
5878
- if (img2) {
5879
- layer.remove(img2);
5880
- this.canvasService.requestRenderAll();
5881
- }
5882
- return;
6520
+ clampOpacity(value) {
6521
+ return Math.max(0, Math.min(1, Number(value)));
6522
+ }
6523
+ buildFilmSpecs(imageUrl, opacity) {
6524
+ if (!imageUrl) {
6525
+ return [];
5883
6526
  }
5884
- const width = this.canvasService.canvas.width || 800;
5885
- const height = this.canvasService.canvas.height || 600;
5886
- let img = this.canvasService.getObject("film-image", "overlay");
5887
- try {
5888
- if (img) {
5889
- if (img.getSrc() !== url) {
5890
- await img.setSrc(url);
5891
- }
5892
- img.set({ opacity });
5893
- } else {
5894
- img = await import_fabric5.FabricImage.fromURL(url, { crossOrigin: "anonymous" });
5895
- img.scaleToWidth(width);
5896
- if (img.getScaledHeight() < height) img.scaleToHeight(height);
5897
- img.set({
5898
- originX: "left",
5899
- 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: {
5900
6544
  left: 0,
5901
6545
  top: 0,
5902
- opacity,
6546
+ originX: "left",
6547
+ originY: "top",
6548
+ opacity: this.clampOpacity(opacity),
6549
+ scaleX: coverScale,
6550
+ scaleY: coverScale,
5903
6551
  selectable: false,
5904
6552
  evented: false,
5905
- data: { id: "film-image" }
5906
- });
5907
- 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;
5908
6587
  }
5909
- this.canvasService.requestRenderAll();
5910
6588
  } catch (error) {
5911
- 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
+ }
5912
6608
  }
5913
- 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);
5914
6613
  this.canvasService.requestRenderAll();
5915
6614
  }
5916
6615
  };
@@ -6007,19 +6706,37 @@ var MirrorTool = class {
6007
6706
 
6008
6707
  // src/extensions/ruler.ts
6009
6708
  var import_core8 = require("@pooder/core");
6010
- 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;
6011
6725
  var RulerTool = class {
6012
6726
  constructor(options) {
6013
6727
  this.id = "pooder.kit.ruler";
6014
6728
  this.metadata = {
6015
6729
  name: "RulerTool"
6016
6730
  };
6017
- this.thickness = 20;
6018
- this.gap = 15;
6019
- this.backgroundColor = "#f0f0f0";
6020
- this.textColor = "#333333";
6021
- this.lineColor = "#999999";
6022
- 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 = [];
6023
6740
  this.onCanvasResized = () => {
6024
6741
  this.updateRuler();
6025
6742
  };
@@ -6028,50 +6745,73 @@ var RulerTool = class {
6028
6745
  }
6029
6746
  }
6030
6747
  activate(context) {
6748
+ var _a;
6031
6749
  this.context = context;
6032
6750
  this.canvasService = context.services.get("CanvasService");
6033
6751
  if (!this.canvasService) {
6034
- console.warn("CanvasService not found for RulerTool");
6752
+ console.warn("[RulerTool] CanvasService not found.");
6035
6753
  return;
6036
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
+ );
6037
6766
  const configService = context.services.get(
6038
6767
  "ConfigurationService"
6039
6768
  );
6040
6769
  if (configService) {
6041
- this.thickness = configService.get("ruler.thickness", this.thickness);
6042
- this.gap = configService.get("ruler.gap", this.gap);
6043
- this.backgroundColor = configService.get(
6044
- "ruler.backgroundColor",
6045
- this.backgroundColor
6046
- );
6047
- this.textColor = configService.get("ruler.textColor", this.textColor);
6048
- this.lineColor = configService.get("ruler.lineColor", this.lineColor);
6049
- this.fontSize = configService.get("ruler.fontSize", this.fontSize);
6770
+ this.syncConfig(configService);
6050
6771
  configService.onAnyChange((e) => {
6051
6772
  let shouldUpdate = false;
6052
6773
  if (e.key.startsWith("ruler.")) {
6053
6774
  const prop = e.key.split(".")[1];
6054
6775
  if (prop && prop in this) {
6055
- 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
+ }
6056
6784
  shouldUpdate = true;
6785
+ this.log("config:update", {
6786
+ key: e.key,
6787
+ raw: e.value,
6788
+ normalized: this[prop]
6789
+ });
6057
6790
  }
6058
6791
  } else if (e.key.startsWith("size.")) {
6059
6792
  shouldUpdate = true;
6793
+ this.log("size:update", { key: e.key, value: e.value });
6060
6794
  }
6061
6795
  if (shouldUpdate) {
6062
6796
  this.updateRuler();
6063
6797
  }
6064
6798
  });
6065
6799
  }
6066
- this.createLayer();
6067
6800
  context.eventBus.on("canvas:resized", this.onCanvasResized);
6068
6801
  this.updateRuler();
6069
6802
  }
6070
6803
  deactivate(context) {
6804
+ var _a;
6071
6805
  context.eventBus.off("canvas:resized", this.onCanvasResized);
6072
- 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
+ }
6073
6812
  this.canvasService = void 0;
6074
6813
  this.context = void 0;
6814
+ this.renderSeq = 0;
6075
6815
  }
6076
6816
  contribute() {
6077
6817
  return {
@@ -6080,43 +6820,43 @@ var RulerTool = class {
6080
6820
  id: "ruler.thickness",
6081
6821
  type: "number",
6082
6822
  label: "Thickness",
6083
- min: 10,
6084
- max: 100,
6085
- default: 20
6823
+ min: RULER_THICKNESS_MIN,
6824
+ max: RULER_THICKNESS_MAX,
6825
+ default: DEFAULT_THICKNESS
6086
6826
  },
6087
6827
  {
6088
6828
  id: "ruler.gap",
6089
6829
  type: "number",
6090
6830
  label: "Gap",
6091
- min: 0,
6092
- max: 100,
6093
- default: 15
6831
+ min: RULER_GAP_MIN,
6832
+ max: RULER_GAP_MAX,
6833
+ default: DEFAULT_GAP
6094
6834
  },
6095
6835
  {
6096
6836
  id: "ruler.backgroundColor",
6097
6837
  type: "color",
6098
6838
  label: "Background Color",
6099
- default: "#f0f0f0"
6839
+ default: DEFAULT_BACKGROUND_COLOR
6100
6840
  },
6101
6841
  {
6102
6842
  id: "ruler.textColor",
6103
6843
  type: "color",
6104
6844
  label: "Text Color",
6105
- default: "#333333"
6845
+ default: DEFAULT_TEXT_COLOR
6106
6846
  },
6107
6847
  {
6108
6848
  id: "ruler.lineColor",
6109
6849
  type: "color",
6110
6850
  label: "Line Color",
6111
- default: "#999999"
6851
+ default: DEFAULT_LINE_COLOR
6112
6852
  },
6113
6853
  {
6114
6854
  id: "ruler.fontSize",
6115
6855
  type: "number",
6116
6856
  label: "Font Size",
6117
- min: 8,
6118
- max: 24,
6119
- default: 10
6857
+ min: RULER_FONT_SIZE_MIN,
6858
+ max: RULER_FONT_SIZE_MAX,
6859
+ default: DEFAULT_FONT_SIZE
6120
6860
  }
6121
6861
  ],
6122
6862
  [import_core8.ContributionPointIds.COMMANDS]: [
@@ -6129,12 +6869,23 @@ var RulerTool = class {
6129
6869
  textColor: this.textColor,
6130
6870
  lineColor: this.lineColor,
6131
6871
  fontSize: this.fontSize,
6132
- thickness: this.thickness
6872
+ thickness: this.thickness,
6873
+ gap: this.gap
6133
6874
  };
6134
6875
  const newState = { ...oldState, ...theme };
6135
- if (JSON.stringify(newState) === JSON.stringify(oldState))
6876
+ if (JSON.stringify(newState) === JSON.stringify(oldState)) {
6136
6877
  return true;
6878
+ }
6137
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
+ );
6138
6889
  this.updateRuler();
6139
6890
  return true;
6140
6891
  }
@@ -6142,225 +6893,367 @@ var RulerTool = class {
6142
6893
  ]
6143
6894
  };
6144
6895
  }
6145
- getLayer() {
6146
- var _a;
6147
- 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}`);
6148
6902
  }
6149
- createLayer() {
6150
- if (!this.canvasService) return;
6151
- const canvas = this.canvasService.canvas;
6152
- const width = canvas.width || 800;
6153
- const height = canvas.height || 600;
6154
- const layer = this.canvasService.createLayer("ruler-overlay", {
6155
- width,
6156
- height,
6157
- selectable: false,
6158
- evented: false,
6159
- left: 0,
6160
- top: 0,
6161
- originX: "left",
6162
- 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
6163
6932
  });
6164
- canvas.bringObjectToFront(layer);
6165
6933
  }
6166
- destroyLayer() {
6167
- if (!this.canvasService) return;
6168
- const layer = this.getLayer();
6169
- if (layer) {
6170
- this.canvasService.canvas.remove(layer);
6171
- }
6934
+ toFiniteNumber(value, fallback) {
6935
+ const numeric = Number(value);
6936
+ return Number.isFinite(numeric) ? numeric : fallback;
6172
6937
  }
6173
- createArrowLine(x1, y1, x2, y2, color) {
6174
- const line = new import_fabric6.Line([x1, y1, x2, y2], {
6175
- stroke: color,
6176
- strokeWidth: this.thickness / 20,
6177
- // Scale stroke width relative to thickness (default 1)
6178
- selectable: false,
6179
- evented: false
6180
- });
6181
- const arrowSize = Math.max(4, this.thickness * 0.3);
6182
- const angle = Math.atan2(y2 - y1, x2 - x1);
6183
- const endArrow = new import_fabric6.Polygon(
6184
- [
6185
- { x: 0, y: 0 },
6186
- { x: -arrowSize, y: -arrowSize / 2 },
6187
- { x: -arrowSize, y: arrowSize / 2 }
6188
- ],
6189
- {
6190
- fill: color,
6191
- left: x2,
6192
- top: y2,
6193
- originX: "right",
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",
6978
+ selectable: false,
6979
+ evented: false,
6980
+ excludeFromExport: true
6981
+ }
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",
6194
7001
  originY: "center",
6195
- angle: angle * 180 / Math.PI,
7002
+ backgroundColor: this.backgroundColor,
6196
7003
  selectable: false,
6197
- evented: false
7004
+ evented: false,
7005
+ excludeFromExport: true
6198
7006
  }
7007
+ };
7008
+ }
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))
7014
+ );
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)
7020
+ );
7021
+ const strokeWidth = Math.max(
7022
+ this.toSceneDisplayLength(1),
7023
+ this.toSceneDisplayLength(
7024
+ this.thickness / THICKNESS_TO_STROKE_WIDTH_RATIO
7025
+ )
7026
+ );
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 },
7045
+ {
7046
+ stroke: this.lineColor,
7047
+ strokeWidth,
7048
+ strokeLineCap: "butt"
7049
+ }
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
7062
+ }
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
+ })
6199
7105
  );
6200
- const startArrow = new import_fabric6.Polygon(
6201
- [
6202
- { x: 0, y: 0 },
6203
- { x: arrowSize, y: -arrowSize / 2 },
6204
- { x: arrowSize, y: arrowSize / 2 }
6205
- ],
6206
- {
6207
- fill: color,
6208
- left: x1,
6209
- top: y1,
6210
- originX: "left",
6211
- originY: "center",
6212
- angle: angle * 180 / Math.PI,
6213
- selectable: false,
6214
- evented: false
6215
- }
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"
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
7178
+ )
6216
7179
  );
6217
- return new import_fabric6.Group([line, startArrow, endArrow], {
6218
- selectable: false,
6219
- evented: false
6220
- });
7180
+ return specs;
6221
7181
  }
6222
7182
  updateRuler() {
6223
- var _a;
7183
+ void this.updateRulerAsync();
7184
+ }
7185
+ async updateRulerAsync() {
7186
+ var _a, _b;
6224
7187
  if (!this.canvasService) return;
6225
- const layer = this.getLayer();
6226
- if (!layer) return;
6227
- layer.remove(...layer.getObjects());
6228
- const { backgroundColor, lineColor, textColor, fontSize } = this;
6229
7188
  const configService = (_a = this.context) == null ? void 0 : _a.services.get(
6230
7189
  "ConfigurationService"
6231
7190
  );
6232
7191
  if (!configService) return;
7192
+ const seq = ++this.renderSeq;
6233
7193
  const sizeState = readSizeState(configService);
6234
7194
  const layout = computeSceneLayout(this.canvasService, sizeState);
6235
- if (!layout) return;
6236
- const trimRect = layout.trimRect;
6237
- const cutRect = layout.cutRect;
6238
- const useCutAsRuler = layout.cutMode === "outset";
6239
- const rulerRect = useCutAsRuler ? cutRect : trimRect;
6240
- const gap = this.gap || 15;
6241
- const rulerLeft = rulerRect.left;
6242
- const rulerTop = rulerRect.top;
6243
- const rulerRight = rulerRect.left + rulerRect.width;
6244
- const rulerBottom = rulerRect.top + rulerRect.height;
6245
- const displayWidthMm = useCutAsRuler ? layout.cutWidthMm : layout.trimWidthMm;
6246
- const displayHeightMm = useCutAsRuler ? layout.cutHeightMm : layout.trimHeightMm;
6247
- const displayUnit = sizeState.unit;
6248
- const topRulerY = rulerTop - gap;
6249
- const topRulerXStart = rulerLeft;
6250
- const topRulerXEnd = rulerRight;
6251
- const leftRulerX = rulerLeft - gap;
6252
- const leftRulerYStart = rulerTop;
6253
- const leftRulerYEnd = rulerBottom;
6254
- const topDimLine = this.createArrowLine(
6255
- topRulerXStart,
6256
- topRulerY,
6257
- topRulerXEnd,
6258
- topRulerY,
6259
- lineColor
6260
- );
6261
- layer.add(topDimLine);
6262
- const extLen = 5;
6263
- layer.add(
6264
- new import_fabric6.Line(
6265
- [
6266
- topRulerXStart,
6267
- topRulerY - extLen,
6268
- topRulerXStart,
6269
- topRulerY + extLen
6270
- ],
6271
- {
6272
- stroke: lineColor,
6273
- strokeWidth: 1,
6274
- selectable: false,
6275
- evented: false
6276
- }
6277
- )
6278
- );
6279
- layer.add(
6280
- new import_fabric6.Line(
6281
- [topRulerXEnd, topRulerY - extLen, topRulerXEnd, topRulerY + extLen],
6282
- {
6283
- stroke: lineColor,
6284
- strokeWidth: 1,
6285
- selectable: false,
6286
- evented: false
6287
- }
6288
- )
6289
- );
6290
- const widthStr = formatMm(displayWidthMm, displayUnit);
6291
- const topTextContent = `${widthStr} ${displayUnit}`;
6292
- const topText = new import_fabric6.Text(topTextContent, {
6293
- left: topRulerXStart + (rulerRight - rulerLeft) / 2,
6294
- top: topRulerY,
6295
- fontSize,
6296
- fill: textColor,
6297
- fontFamily: "Arial",
6298
- originX: "center",
6299
- originY: "center",
6300
- backgroundColor,
6301
- // Background mask for readability
6302
- selectable: false,
6303
- evented: false
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
6304
7203
  });
6305
- layer.add(topText);
6306
- const leftDimLine = this.createArrowLine(
6307
- leftRulerX,
6308
- leftRulerYStart,
6309
- leftRulerX,
6310
- leftRulerYEnd,
6311
- lineColor
6312
- );
6313
- layer.add(leftDimLine);
6314
- layer.add(
6315
- new import_fabric6.Line(
6316
- [
6317
- leftRulerX - extLen,
6318
- leftRulerYStart,
6319
- leftRulerX + extLen,
6320
- leftRulerYStart
6321
- ],
6322
- {
6323
- stroke: lineColor,
6324
- strokeWidth: 1,
6325
- selectable: false,
6326
- evented: false
6327
- }
6328
- )
6329
- );
6330
- layer.add(
6331
- new import_fabric6.Line(
6332
- [
6333
- leftRulerX - extLen,
6334
- leftRulerYEnd,
6335
- leftRulerX + extLen,
6336
- leftRulerYEnd
6337
- ],
6338
- {
6339
- stroke: lineColor,
6340
- strokeWidth: 1,
6341
- selectable: false,
6342
- evented: false
6343
- }
6344
- )
6345
- );
6346
- const heightStr = formatMm(displayHeightMm, displayUnit);
6347
- const leftTextContent = `${heightStr} ${displayUnit}`;
6348
- const leftText = new import_fabric6.Text(leftTextContent, {
6349
- left: leftRulerX,
6350
- top: leftRulerYStart + (rulerBottom - rulerTop) / 2,
6351
- angle: -90,
6352
- fontSize,
6353
- fill: textColor,
6354
- fontFamily: "Arial",
6355
- originX: "center",
6356
- originY: "center",
6357
- backgroundColor,
6358
- selectable: false,
6359
- evented: false
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
6360
7218
  });
6361
- layer.add(leftText);
6362
- this.canvasService.canvas.bringObjectToFront(layer);
6363
- this.canvasService.canvas.requestRenderAll();
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 });
6364
7257
  }
6365
7258
  };
6366
7259
 
@@ -6397,6 +7290,9 @@ var WhiteInkTool = class {
6397
7290
  this.printWithWhiteInk = true;
6398
7291
  this.previewImageVisible = true;
6399
7292
  this.renderSeq = 0;
7293
+ this.whiteSpecs = [];
7294
+ this.coverSpecs = [];
7295
+ this.overlaySpecs = [];
6400
7296
  this.onToolActivated = (event) => {
6401
7297
  const before = this.isToolActive;
6402
7298
  this.syncToolActiveFromWorkbench(event.id);
@@ -6434,12 +7330,25 @@ var WhiteInkTool = class {
6434
7330
  };
6435
7331
  }
6436
7332
  activate(context) {
7333
+ var _a;
6437
7334
  this.context = context;
6438
7335
  this.canvasService = context.services.get("CanvasService");
6439
7336
  if (!this.canvasService) {
6440
7337
  console.warn("CanvasService not found for WhiteInkTool");
6441
7338
  return;
6442
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
+ );
6443
7352
  context.eventBus.on("tool:activated", this.onToolActivated);
6444
7353
  context.eventBus.on("scene:layout:change", this.onSceneLayoutChanged);
6445
7354
  context.eventBus.on("object:added", this.onObjectAdded);
@@ -6505,7 +7414,7 @@ var WhiteInkTool = class {
6505
7414
  this.updateWhiteInks();
6506
7415
  }
6507
7416
  deactivate(context) {
6508
- var _a;
7417
+ var _a, _b;
6509
7418
  context.eventBus.off("tool:activated", this.onToolActivated);
6510
7419
  context.eventBus.off("scene:layout:change", this.onSceneLayoutChanged);
6511
7420
  context.eventBus.off("object:added", this.onObjectAdded);
@@ -6516,6 +7425,11 @@ var WhiteInkTool = class {
6516
7425
  this.dirtyTrackerDisposable = void 0;
6517
7426
  this.clearRenderedWhiteInks();
6518
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
+ }
6519
7433
  this.canvasService = void 0;
6520
7434
  this.context = void 0;
6521
7435
  }
@@ -6938,11 +7852,20 @@ var WhiteInkTool = class {
6938
7852
  if (!layout) {
6939
7853
  return { left: 0, top: 0, width: 0, height: 0 };
6940
7854
  }
6941
- return {
7855
+ return this.canvasService.toSceneRect({
6942
7856
  left: layout.cutRect.left,
6943
7857
  top: layout.cutRect.top,
6944
7858
  width: layout.cutRect.width,
6945
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"
6946
7869
  };
6947
7870
  }
6948
7871
  getImageObjects() {
@@ -6965,7 +7888,7 @@ var WhiteInkTool = class {
6965
7888
  return (_a = obj == null ? void 0 : obj._originalElement) == null ? void 0 : _a.src;
6966
7889
  }
6967
7890
  getImageSnapshot(obj) {
6968
- var _a;
7891
+ var _a, _b;
6969
7892
  if (!obj) return null;
6970
7893
  const src = this.getCurrentSrc(obj);
6971
7894
  if (!src) return null;
@@ -6973,14 +7896,18 @@ var WhiteInkTool = class {
6973
7896
  const width = Number((obj == null ? void 0 : obj.width) || 0);
6974
7897
  const height = Number((obj == null ? void 0 : obj.height) || 0);
6975
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 };
6976
7903
  return {
6977
- 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"),
6978
7905
  src,
6979
7906
  element,
6980
- left: Number.isFinite(obj == null ? void 0 : obj.left) ? Number(obj.left) : 0,
6981
- top: Number.isFinite(obj == null ? void 0 : obj.top) ? Number(obj.top) : 0,
6982
- scaleX: Number.isFinite(obj == null ? void 0 : obj.scaleX) ? Number(obj.scaleX) : 1,
6983
- 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,
6984
7911
  angle: Number.isFinite(obj == null ? void 0 : obj.angle) ? Number(obj.angle) : 0,
6985
7912
  originX: typeof (obj == null ? void 0 : obj.originX) === "string" ? obj.originX : "center",
6986
7913
  originY: typeof (obj == null ? void 0 : obj.originY) === "string" ? obj.originY : "center",
@@ -7155,8 +8082,11 @@ var WhiteInkTool = class {
7155
8082
  var _a, _b;
7156
8083
  if (!this.isToolActive || !this.canvasService) return [];
7157
8084
  if (frame.width <= 0 || frame.height <= 0) return [];
7158
- const canvasW = this.canvasService.canvas.width || 0;
7159
- 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;
7160
8090
  const strokeColor = this.getConfig("image.frame.strokeColor", "#808080") || "#808080";
7161
8091
  const strokeWidthRaw = Number(
7162
8092
  (_a = this.getConfig("image.frame.strokeWidth", 2)) != null ? _a : 2
@@ -7168,21 +8098,42 @@ var WhiteInkTool = class {
7168
8098
  const innerBackground = this.getConfig("image.frame.innerBackground", "rgba(0,0,0,0)") || "rgba(0,0,0,0)";
7169
8099
  const strokeWidth = Number.isFinite(strokeWidthRaw) ? Math.max(0, strokeWidthRaw) : 2;
7170
8100
  const dashLength = Number.isFinite(dashLengthRaw) ? Math.max(1, dashLengthRaw) : 8;
7171
- const frameLeft = Math.max(0, Math.min(canvasW, frame.left));
7172
- 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
+ );
7173
8111
  const frameRight = Math.max(
7174
8112
  frameLeft,
7175
- Math.min(canvasW, frame.left + frame.width)
8113
+ Math.min(canvasLeft + canvasW, frame.left + frame.width)
7176
8114
  );
7177
8115
  const frameBottom = Math.max(
7178
8116
  frameTop,
7179
- Math.min(canvasH, frame.top + frame.height)
8117
+ Math.min(canvasTop + canvasH, frame.top + frame.height)
7180
8118
  );
7181
8119
  const visibleFrameH = Math.max(0, frameBottom - frameTop);
7182
- const topH = frameTop;
7183
- const bottomH = Math.max(0, canvasH - frameBottom);
7184
- const leftW = frameLeft;
7185
- 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);
7186
8137
  const maskSpecs = [
7187
8138
  {
7188
8139
  id: "white-ink.cropMask.top",
@@ -7192,13 +8143,17 @@ var WhiteInkTool = class {
7192
8143
  layerId: WHITE_INK_OVERLAY_LAYER_ID,
7193
8144
  type: "white-ink-mask"
7194
8145
  },
8146
+ layout: {
8147
+ reference: "custom",
8148
+ referenceRect: viewportRect,
8149
+ alignX: "start",
8150
+ alignY: "start",
8151
+ width: "100%",
8152
+ height: topH
8153
+ },
7195
8154
  props: {
7196
- left: canvasW / 2,
7197
- top: topH / 2,
7198
- width: canvasW,
7199
- height: topH,
7200
- originX: "center",
7201
- originY: "center",
8155
+ originX: "left",
8156
+ originY: "top",
7202
8157
  fill: outerBackground,
7203
8158
  selectable: false,
7204
8159
  evented: false,
@@ -7213,13 +8168,17 @@ var WhiteInkTool = class {
7213
8168
  layerId: WHITE_INK_OVERLAY_LAYER_ID,
7214
8169
  type: "white-ink-mask"
7215
8170
  },
8171
+ layout: {
8172
+ reference: "custom",
8173
+ referenceRect: viewportRect,
8174
+ alignX: "start",
8175
+ alignY: "end",
8176
+ width: "100%",
8177
+ height: bottomH
8178
+ },
7216
8179
  props: {
7217
- left: canvasW / 2,
7218
- top: frameBottom + bottomH / 2,
7219
- width: canvasW,
7220
- height: bottomH,
7221
- originX: "center",
7222
- originY: "center",
8180
+ originX: "left",
8181
+ originY: "top",
7223
8182
  fill: outerBackground,
7224
8183
  selectable: false,
7225
8184
  evented: false,
@@ -7234,13 +8193,17 @@ var WhiteInkTool = class {
7234
8193
  layerId: WHITE_INK_OVERLAY_LAYER_ID,
7235
8194
  type: "white-ink-mask"
7236
8195
  },
7237
- props: {
7238
- left: leftW / 2,
7239
- top: frameTop + visibleFrameH / 2,
8196
+ layout: {
8197
+ reference: "custom",
8198
+ referenceRect: visibleFrameBandRect,
8199
+ alignX: "start",
8200
+ alignY: "start",
7240
8201
  width: leftW,
7241
- height: visibleFrameH,
7242
- originX: "center",
7243
- originY: "center",
8202
+ height: "100%"
8203
+ },
8204
+ props: {
8205
+ originX: "left",
8206
+ originY: "top",
7244
8207
  fill: outerBackground,
7245
8208
  selectable: false,
7246
8209
  evented: false,
@@ -7255,13 +8218,17 @@ var WhiteInkTool = class {
7255
8218
  layerId: WHITE_INK_OVERLAY_LAYER_ID,
7256
8219
  type: "white-ink-mask"
7257
8220
  },
7258
- props: {
7259
- left: frameRight + rightW / 2,
7260
- top: frameTop + visibleFrameH / 2,
8221
+ layout: {
8222
+ reference: "custom",
8223
+ referenceRect: visibleFrameBandRect,
8224
+ alignX: "end",
8225
+ alignY: "start",
7261
8226
  width: rightW,
7262
- height: visibleFrameH,
7263
- originX: "center",
7264
- originY: "center",
8227
+ height: "100%"
8228
+ },
8229
+ props: {
8230
+ originX: "left",
8231
+ originY: "top",
7265
8232
  fill: outerBackground,
7266
8233
  selectable: false,
7267
8234
  evented: false,
@@ -7279,17 +8246,21 @@ var WhiteInkTool = class {
7279
8246
  layerId: WHITE_INK_OVERLAY_LAYER_ID,
7280
8247
  type: "white-ink-frame"
7281
8248
  },
8249
+ layout: {
8250
+ reference: "custom",
8251
+ referenceRect: frameRect,
8252
+ alignX: "start",
8253
+ alignY: "start",
8254
+ width: "100%",
8255
+ height: "100%"
8256
+ },
7282
8257
  props: {
7283
- left: frame.left + frame.width / 2,
7284
- top: frame.top + frame.height / 2,
7285
- width: frame.width,
7286
- height: frame.height,
7287
- originX: "center",
7288
- originY: "center",
8258
+ originX: "left",
8259
+ originY: "top",
7289
8260
  fill: innerBackground,
7290
8261
  stroke: strokeColor,
7291
- strokeWidth,
7292
- strokeDashArray: [dashLength, dashLength],
8262
+ strokeWidth: strokeWidthScene,
8263
+ strokeDashArray: [dashLengthScene, dashLengthScene],
7293
8264
  selectable: false,
7294
8265
  evented: false,
7295
8266
  excludeFromExport: true
@@ -7365,50 +8336,30 @@ var WhiteInkTool = class {
7365
8336
  }
7366
8337
  ).filter((index) => index >= 0);
7367
8338
  let whiteInsertIndex = imageIndexes.length ? Math.min(...imageIndexes) : this.resolveDefaultInsertIndex(currentObjects);
7368
- whiteObjects.forEach((obj) => {
7369
- canvas.moveObjectTo(obj, whiteInsertIndex);
7370
- whiteInsertIndex += 1;
7371
- });
7372
- const afterWhiteObjects = canvas.getObjects();
7373
- const afterImageIndexes = afterWhiteObjects.map(
7374
- (obj, index) => {
7375
- var _a;
7376
- return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === IMAGE_OBJECT_LAYER_ID3 ? index : -1;
7377
- }
7378
- ).filter((index) => index >= 0);
7379
- let coverInsertIndex = afterImageIndexes.length ? Math.max(...afterImageIndexes) + 1 : whiteInsertIndex;
8339
+ let coverInsertIndex = whiteInsertIndex;
7380
8340
  coverObjects.forEach((obj) => {
7381
8341
  canvas.moveObjectTo(obj, coverInsertIndex);
7382
8342
  coverInsertIndex += 1;
7383
8343
  });
8344
+ whiteInsertIndex = coverInsertIndex;
8345
+ whiteObjects.forEach((obj) => {
8346
+ canvas.moveObjectTo(obj, whiteInsertIndex);
8347
+ whiteInsertIndex += 1;
8348
+ });
7384
8349
  frameObjects.forEach((obj) => canvas.bringObjectToFront(obj));
7385
8350
  canvas.getObjects().filter((obj) => {
7386
8351
  var _a;
7387
8352
  return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === IMAGE_OVERLAY_LAYER_ID2;
7388
8353
  }).forEach((obj) => canvas.bringObjectToFront(obj));
7389
- const dielineOverlay = this.canvasService.getLayer("dieline-overlay");
7390
- if (dielineOverlay) {
7391
- canvas.bringObjectToFront(dielineOverlay);
7392
- }
7393
- const rulerOverlay = this.canvasService.getLayer("ruler-overlay");
7394
- if (rulerOverlay) {
7395
- canvas.bringObjectToFront(rulerOverlay);
7396
- }
8354
+ this.canvasService.bringLayerToFront("dieline-overlay");
8355
+ this.canvasService.bringLayerToFront("ruler-overlay");
7397
8356
  }
7398
8357
  clearRenderedWhiteInks() {
7399
8358
  if (!this.canvasService) return;
7400
- void this.canvasService.applyObjectSpecsToRootLayer(
7401
- WHITE_INK_OBJECT_LAYER_ID,
7402
- []
7403
- );
7404
- void this.canvasService.applyObjectSpecsToRootLayer(
7405
- WHITE_INK_COVER_LAYER_ID,
7406
- []
7407
- );
7408
- void this.canvasService.applyObjectSpecsToRootLayer(
7409
- WHITE_INK_OVERLAY_LAYER_ID,
7410
- []
7411
- );
8359
+ this.whiteSpecs = [];
8360
+ this.coverSpecs = [];
8361
+ this.overlaySpecs = [];
8362
+ this.canvasService.requestRenderFromProducers();
7412
8363
  }
7413
8364
  purgeSourceCaches(item) {
7414
8365
  const sourceUrl = this.resolveSourceUrl(item);
@@ -7475,20 +8426,12 @@ var WhiteInkTool = class {
7475
8426
  }
7476
8427
  }
7477
8428
  }
7478
- await this.canvasService.applyObjectSpecsToRootLayer(
7479
- WHITE_INK_OBJECT_LAYER_ID,
7480
- whiteSpecs
7481
- );
8429
+ this.whiteSpecs = whiteSpecs;
7482
8430
  if (seq !== this.renderSeq) return;
7483
- await this.canvasService.applyObjectSpecsToRootLayer(
7484
- WHITE_INK_COVER_LAYER_ID,
7485
- coverSpecs
7486
- );
8431
+ this.coverSpecs = coverSpecs;
7487
8432
  if (seq !== this.renderSeq) return;
7488
- await this.canvasService.applyObjectSpecsToRootLayer(
7489
- WHITE_INK_OVERLAY_LAYER_ID,
7490
- frameSpecs
7491
- );
8433
+ this.overlaySpecs = frameSpecs;
8434
+ await this.canvasService.flushRenderFromProducers();
7492
8435
  if (seq !== this.renderSeq) return;
7493
8436
  this.syncZOrder();
7494
8437
  this.canvasService.requestRenderAll();
@@ -7718,23 +8661,16 @@ var SceneVisibilityService = class {
7718
8661
  const dielineLayer = this.canvasService.getLayer("dieline-overlay");
7719
8662
  if (dielineLayer) {
7720
8663
  const visible = !HIDDEN_DIELINE_TOOLS.has(this.activeToolId || "");
7721
- if (dielineLayer.visible !== visible) {
7722
- dielineLayer.set({ visible });
7723
- }
7724
- }
7725
- const rulerLayer = this.canvasService.getLayer("ruler-overlay");
7726
- if (rulerLayer) {
7727
- const visible = !HIDDEN_RULER_TOOLS.has(this.activeToolId || "");
7728
- if (rulerLayer.visible !== visible) {
7729
- rulerLayer.set({ visible });
7730
- }
8664
+ this.canvasService.setLayerVisibility("dieline-overlay", visible);
7731
8665
  }
8666
+ const rulerVisible = !HIDDEN_RULER_TOOLS.has(this.activeToolId || "");
8667
+ this.canvasService.setLayerVisibility("ruler-overlay", rulerVisible);
7732
8668
  this.canvasService.requestRenderAll();
7733
8669
  }
7734
8670
  };
7735
8671
 
7736
8672
  // src/services/CanvasService.ts
7737
- var import_fabric7 = require("fabric");
8673
+ var import_fabric5 = require("fabric");
7738
8674
 
7739
8675
  // src/services/ViewportSystem.ts
7740
8676
  var ViewportSystem = class {
@@ -7812,10 +8748,17 @@ var ViewportSystem = class {
7812
8748
  // src/services/CanvasService.ts
7813
8749
  var CanvasService = class {
7814
8750
  constructor(el, options) {
7815
- 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) {
7816
8759
  this.canvas = el;
7817
8760
  } else {
7818
- this.canvas = new import_fabric7.Canvas(el, {
8761
+ this.canvas = new import_fabric5.Canvas(el, {
7819
8762
  preserveObjectStacking: true,
7820
8763
  ...options
7821
8764
  });
@@ -7844,8 +8787,156 @@ var CanvasService = class {
7844
8787
  this.canvas.on("object:removed", forward("object:removed"));
7845
8788
  }
7846
8789
  dispose() {
8790
+ this.renderProducers.clear();
8791
+ this.managedProducerLayerIds.clear();
8792
+ this.managedProducerRootLayerIds.clear();
8793
+ this.producerFlushRequested = false;
7847
8794
  this.canvas.dispose();
7848
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
+ }
7849
8940
  /**
7850
8941
  * Get a layer (Group) by its ID.
7851
8942
  * We assume layers are Groups directly on the canvas with a data.id property.
@@ -7868,7 +8959,7 @@ var CanvasService = class {
7868
8959
  ...options,
7869
8960
  data: { ...options.data, id }
7870
8961
  };
7871
- layer = new import_fabric7.Group([], defaultOptions);
8962
+ layer = new import_fabric5.Group([], defaultOptions);
7872
8963
  this.canvas.add(layer);
7873
8964
  }
7874
8965
  return layer;
@@ -7900,13 +8991,206 @@ var CanvasService = class {
7900
8991
  (_a = this.eventBus) == null ? void 0 : _a.emit("canvas:resized", { width, height });
7901
8992
  this.requestRenderAll();
7902
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
+ }
7903
9187
  async applyLayerSpec(spec) {
7904
9188
  const layer = this.createLayer(spec.id, spec.props || {});
7905
9189
  await this.applyObjectSpecsToContainer(layer, spec.objects);
7906
9190
  }
7907
- async applyObjectSpecsToLayer(layerId, objects) {
9191
+ async applyObjectSpecsToLayer(layerId, objects, options = {}) {
7908
9192
  const layer = this.createLayer(layerId, {});
7909
- await this.applyObjectSpecsToContainer(layer, objects);
9193
+ await this.applyObjectSpecsToContainer(layer, objects, options);
7910
9194
  }
7911
9195
  getRootLayerObjects(layerId) {
7912
9196
  return this.canvas.getObjects().filter((obj) => {
@@ -7914,7 +9198,7 @@ var CanvasService = class {
7914
9198
  return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === layerId;
7915
9199
  });
7916
9200
  }
7917
- async applyObjectSpecsToRootLayer(layerId, specs) {
9201
+ async applyObjectSpecsToRootLayer(layerId, specs, options = {}) {
7918
9202
  const desiredIds = new Set(specs.map((s) => s.id));
7919
9203
  const existing = this.getRootLayerObjects(layerId);
7920
9204
  existing.forEach((obj) => {
@@ -7948,9 +9232,11 @@ var CanvasService = class {
7948
9232
  }
7949
9233
  this.patchFabricObject(current, spec, { layerId });
7950
9234
  }
7951
- this.requestRenderAll();
9235
+ if (options.render !== false) {
9236
+ this.requestRenderAll();
9237
+ }
7952
9238
  }
7953
- async applyObjectSpecsToContainer(container, specs) {
9239
+ async applyObjectSpecsToContainer(container, specs, options = {}) {
7954
9240
  const desiredIds = new Set(specs.map((s) => s.id));
7955
9241
  const existing = container.getObjects();
7956
9242
  existing.forEach((obj) => {
@@ -7986,7 +9272,9 @@ var CanvasService = class {
7986
9272
  this.moveObjectInContainer(container, current, index);
7987
9273
  }
7988
9274
  container.dirty = true;
7989
- this.requestRenderAll();
9275
+ if (options.render !== false) {
9276
+ this.requestRenderAll();
9277
+ }
7990
9278
  }
7991
9279
  patchFabricObject(obj, spec, extraData) {
7992
9280
  const nextData = {
@@ -7995,9 +9283,33 @@ var CanvasService = class {
7995
9283
  ...extraData || {},
7996
9284
  id: spec.id
7997
9285
  };
7998
- obj.set({ ...spec.props || {}, data: nextData });
9286
+ const props = this.resolveFabricProps(spec, spec.props || {});
9287
+ obj.set({ ...props, data: nextData });
7999
9288
  obj.setCoords();
8000
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
+ }
8001
9313
  moveObjectInContainer(container, obj, index) {
8002
9314
  if (!obj) return;
8003
9315
  const moveObjectTo = container.moveObjectTo;
@@ -8017,10 +9329,11 @@ var CanvasService = class {
8017
9329
  }
8018
9330
  }
8019
9331
  async createFabricObject(spec) {
8020
- var _a, _b;
9332
+ var _a, _b, _c, _d;
8021
9333
  if (spec.type === "rect") {
8022
- const rect = new import_fabric7.Rect({
8023
- ...spec.props || {},
9334
+ const props = this.resolveFabricProps(spec, spec.props || {});
9335
+ const rect = new import_fabric5.Rect({
9336
+ ...props,
8024
9337
  data: { ...spec.data || {}, id: spec.id }
8025
9338
  });
8026
9339
  rect.setCoords();
@@ -8029,8 +9342,9 @@ var CanvasService = class {
8029
9342
  if (spec.type === "path") {
8030
9343
  const pathData = ((_a = spec.props) == null ? void 0 : _a.path) || ((_b = spec.props) == null ? void 0 : _b.pathData);
8031
9344
  if (!pathData) return void 0;
8032
- const path = new import_fabric7.Path(pathData, {
8033
- ...spec.props || {},
9345
+ const props = this.resolveFabricProps(spec, spec.props || {});
9346
+ const path = new import_fabric5.Path(pathData, {
9347
+ ...props,
8034
9348
  data: { ...spec.data || {}, id: spec.id }
8035
9349
  });
8036
9350
  path.setCoords();
@@ -8038,14 +9352,25 @@ var CanvasService = class {
8038
9352
  }
8039
9353
  if (spec.type === "image") {
8040
9354
  if (!spec.src) return void 0;
8041
- 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 || {});
8042
9357
  image.set({
8043
- ...spec.props || {},
9358
+ ...props,
8044
9359
  data: { ...spec.data || {}, id: spec.id }
8045
9360
  });
8046
9361
  image.setCoords();
8047
9362
  return image;
8048
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
+ }
8049
9374
  return void 0;
8050
9375
  }
8051
9376
  };