@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.mjs CHANGED
@@ -2,7 +2,12 @@
2
2
  import {
3
3
  ContributionPointIds
4
4
  } from "@pooder/core";
5
- import { Rect, FabricImage as Image2 } from "fabric";
5
+ import { FabricImage } from "fabric";
6
+ var BACKGROUND_LAYER_ID = "background";
7
+ var BACKGROUND_RECT_ID = "background-color-rect";
8
+ var BACKGROUND_IMAGE_ID = "background-image";
9
+ var DEFAULT_WIDTH = 800;
10
+ var DEFAULT_HEIGHT = 600;
6
11
  var BackgroundTool = class {
7
12
  constructor(options) {
8
13
  this.id = "pooder.kit.background";
@@ -11,17 +16,38 @@ var BackgroundTool = class {
11
16
  };
12
17
  this.color = "";
13
18
  this.url = "";
19
+ this.specs = [];
20
+ this.renderSeq = 0;
21
+ this.renderImageUrl = "";
22
+ this.sourceSizeBySrc = /* @__PURE__ */ new Map();
23
+ this.pendingSizeBySrc = /* @__PURE__ */ new Map();
24
+ this.onCanvasResized = () => {
25
+ this.updateBackground();
26
+ };
14
27
  if (options) {
15
28
  Object.assign(this, options);
16
29
  }
17
30
  }
18
31
  activate(context) {
32
+ var _a;
19
33
  this.canvasService = context.services.get("CanvasService");
20
34
  if (!this.canvasService) {
21
35
  console.warn("CanvasService not found for BackgroundTool");
22
36
  return;
23
37
  }
24
- const configService = context.services.get("ConfigurationService");
38
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
39
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
40
+ this.id,
41
+ () => ({
42
+ layerSpecs: {
43
+ [BACKGROUND_LAYER_ID]: this.specs
44
+ }
45
+ }),
46
+ { priority: 0 }
47
+ );
48
+ const configService = context.services.get(
49
+ "ConfigurationService"
50
+ );
25
51
  if (configService) {
26
52
  this.color = configService.get("background.color", this.color);
27
53
  this.url = configService.get("background.url", this.url);
@@ -45,17 +71,25 @@ var BackgroundTool = class {
45
71
  }
46
72
  });
47
73
  }
48
- this.initLayer();
74
+ context.eventBus.on("canvas:resized", this.onCanvasResized);
49
75
  this.updateBackground();
50
76
  }
51
77
  deactivate(context) {
52
- if (this.canvasService) {
53
- const layer = this.canvasService.getLayer("background");
54
- if (layer) {
55
- this.canvasService.canvas.remove(layer);
56
- }
57
- this.canvasService = void 0;
78
+ var _a;
79
+ context.eventBus.off("canvas:resized", this.onCanvasResized);
80
+ this.renderSeq += 1;
81
+ this.specs = [];
82
+ this.renderImageUrl = "";
83
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
84
+ this.renderProducerDisposable = void 0;
85
+ if (!this.canvasService) return;
86
+ const layer = this.canvasService.getLayer(BACKGROUND_LAYER_ID);
87
+ if (layer) {
88
+ this.canvasService.canvas.remove(layer);
58
89
  }
90
+ void this.canvasService.flushRenderFromProducers();
91
+ this.canvasService.requestRenderAll();
92
+ this.canvasService = void 0;
59
93
  }
60
94
  contribute() {
61
95
  return {
@@ -115,88 +149,131 @@ var BackgroundTool = class {
115
149
  ]
116
150
  };
117
151
  }
118
- initLayer() {
119
- if (!this.canvasService) return;
120
- let backgroundLayer = this.canvasService.getLayer("background");
121
- if (!backgroundLayer) {
122
- backgroundLayer = this.canvasService.createLayer("background", {
123
- width: this.canvasService.canvas.width,
124
- height: this.canvasService.canvas.height,
125
- selectable: false,
126
- evented: false
127
- });
128
- this.canvasService.canvas.sendObjectToBack(backgroundLayer);
129
- }
152
+ getViewportSize() {
153
+ var _a, _b;
154
+ const width = Number(((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 0);
155
+ const height = Number(((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 0);
156
+ return {
157
+ width: width > 0 ? width : DEFAULT_WIDTH,
158
+ height: height > 0 ? height : DEFAULT_HEIGHT
159
+ };
130
160
  }
131
- async updateBackground() {
132
- if (!this.canvasService) return;
133
- const layer = this.canvasService.getLayer("background");
134
- if (!layer) {
135
- console.warn("[BackgroundTool] Background layer not found");
136
- return;
161
+ buildBackgroundSpecs(color, imageUrl) {
162
+ const { width, height } = this.getViewportSize();
163
+ const specs = [
164
+ {
165
+ id: BACKGROUND_RECT_ID,
166
+ type: "rect",
167
+ space: "screen",
168
+ data: {
169
+ id: BACKGROUND_RECT_ID,
170
+ layerId: BACKGROUND_LAYER_ID,
171
+ type: "background-color"
172
+ },
173
+ props: {
174
+ left: 0,
175
+ top: 0,
176
+ width,
177
+ height,
178
+ originX: "left",
179
+ originY: "top",
180
+ fill: color,
181
+ selectable: false,
182
+ evented: false,
183
+ excludeFromExport: true
184
+ }
185
+ }
186
+ ];
187
+ if (!imageUrl) {
188
+ return specs;
137
189
  }
138
- const { color, url } = this;
139
- const width = this.canvasService.canvas.width || 800;
140
- const height = this.canvasService.canvas.height || 600;
141
- let rect = this.canvasService.getObject(
142
- "background-color-rect",
143
- "background"
144
- );
145
- if (rect) {
146
- rect.set({
147
- fill: color
148
- });
149
- } else {
150
- rect = new Rect({
151
- width,
152
- height,
153
- fill: color,
190
+ const sourceSize = this.sourceSizeBySrc.get(imageUrl);
191
+ const sourceWidth = Math.max(1, Number((sourceSize == null ? void 0 : sourceSize.width) || width));
192
+ const sourceHeight = Math.max(1, Number((sourceSize == null ? void 0 : sourceSize.height) || height));
193
+ const coverScale = Math.max(width / sourceWidth, height / sourceHeight);
194
+ specs.push({
195
+ id: BACKGROUND_IMAGE_ID,
196
+ type: "image",
197
+ src: imageUrl,
198
+ space: "screen",
199
+ data: {
200
+ id: BACKGROUND_IMAGE_ID,
201
+ layerId: BACKGROUND_LAYER_ID,
202
+ type: "background-image"
203
+ },
204
+ props: {
205
+ left: 0,
206
+ top: 0,
207
+ originX: "left",
208
+ originY: "top",
209
+ scaleX: coverScale,
210
+ scaleY: coverScale,
154
211
  selectable: false,
155
212
  evented: false,
156
- data: {
157
- id: "background-color-rect"
158
- }
159
- });
160
- layer.add(rect);
161
- layer.sendObjectToBack(rect);
213
+ excludeFromExport: true
214
+ }
215
+ });
216
+ return specs;
217
+ }
218
+ async ensureImageSize(src) {
219
+ if (!src) return null;
220
+ const cached = this.sourceSizeBySrc.get(src);
221
+ if (cached) return cached;
222
+ const pending = this.pendingSizeBySrc.get(src);
223
+ if (pending) {
224
+ return pending;
162
225
  }
163
- let img = this.canvasService.getObject(
164
- "background-image",
165
- "background"
166
- );
226
+ const task = this.loadImageSize(src);
227
+ this.pendingSizeBySrc.set(src, task);
167
228
  try {
168
- if (img) {
169
- if (img.getSrc() !== url) {
170
- if (url) {
171
- await img.setSrc(url);
172
- } else {
173
- layer.remove(img);
174
- }
175
- }
176
- } else {
177
- if (url) {
178
- img = await Image2.fromURL(url, { crossOrigin: "anonymous" });
179
- img.set({
180
- originX: "left",
181
- originY: "top",
182
- left: 0,
183
- top: 0,
184
- selectable: false,
185
- evented: false,
186
- data: {
187
- id: "background-image"
188
- }
189
- });
190
- img.scaleToWidth(width);
191
- if (img.getScaledHeight() < height) img.scaleToHeight(height);
192
- layer.add(img);
193
- }
229
+ return await task;
230
+ } finally {
231
+ if (this.pendingSizeBySrc.get(src) === task) {
232
+ this.pendingSizeBySrc.delete(src);
194
233
  }
195
- this.canvasService.requestRenderAll();
196
- } catch (e) {
197
- console.error("[BackgroundTool] Failed to load image", e);
198
234
  }
199
- layer.dirty = true;
235
+ }
236
+ async loadImageSize(src) {
237
+ try {
238
+ const image = await FabricImage.fromURL(src, {
239
+ crossOrigin: "anonymous"
240
+ });
241
+ const width = Number((image == null ? void 0 : image.width) || 0);
242
+ const height = Number((image == null ? void 0 : image.height) || 0);
243
+ if (width > 0 && height > 0) {
244
+ const size = { width, height };
245
+ this.sourceSizeBySrc.set(src, size);
246
+ return size;
247
+ }
248
+ } catch (error) {
249
+ console.error("[BackgroundTool] Failed to load image", src, error);
250
+ }
251
+ return null;
252
+ }
253
+ updateBackground() {
254
+ void this.updateBackgroundAsync();
255
+ }
256
+ async updateBackgroundAsync() {
257
+ if (!this.canvasService) return;
258
+ const seq = ++this.renderSeq;
259
+ const color = this.color;
260
+ const nextUrl = String(this.url || "").trim();
261
+ if (!nextUrl) {
262
+ this.renderImageUrl = "";
263
+ } else if (nextUrl !== this.renderImageUrl) {
264
+ const loaded = await this.ensureImageSize(nextUrl);
265
+ if (seq !== this.renderSeq) return;
266
+ if (loaded) {
267
+ this.renderImageUrl = nextUrl;
268
+ }
269
+ }
270
+ this.specs = this.buildBackgroundSpecs(color, this.renderImageUrl);
271
+ await this.canvasService.flushRenderFromProducers();
272
+ if (seq !== this.renderSeq) return;
273
+ const layer = this.canvasService.getLayer(BACKGROUND_LAYER_ID);
274
+ if (layer) {
275
+ this.canvasService.canvas.sendObjectToBack(layer);
276
+ }
200
277
  this.canvasService.requestRenderAll();
201
278
  }
202
279
  };
@@ -207,11 +284,76 @@ import {
207
284
  } from "@pooder/core";
208
285
  import {
209
286
  Canvas as FabricCanvas,
210
- Image as FabricImage,
287
+ Image as FabricImage2,
211
288
  Pattern,
212
289
  Point
213
290
  } from "fabric";
214
291
 
292
+ // src/extensions/dielineShape.ts
293
+ var BUILTIN_DIELINE_SHAPES = [
294
+ "rect",
295
+ "circle",
296
+ "ellipse",
297
+ "heart"
298
+ ];
299
+ var DIELINE_SHAPES = [...BUILTIN_DIELINE_SHAPES, "custom"];
300
+ var DEFAULT_DIELINE_SHAPE = "rect";
301
+ var DEFAULT_HEART_SHAPE_PARAMS = {
302
+ lobeSpread: 0.46,
303
+ notchDepth: 0.24,
304
+ tipSharpness: 0
305
+ };
306
+ var DEFAULT_DIELINE_SHAPE_STYLE = {
307
+ fitMode: "contain",
308
+ ...DEFAULT_HEART_SHAPE_PARAMS
309
+ };
310
+ function isDielineShape(value) {
311
+ return typeof value === "string" && DIELINE_SHAPES.includes(value);
312
+ }
313
+ function normalizeFitMode(value, fallback) {
314
+ if (value === "contain" || value === "stretch") return value;
315
+ return fallback;
316
+ }
317
+ function normalizeUnitInterval(value, fallback) {
318
+ const num = Number(value);
319
+ if (!Number.isFinite(num)) return fallback;
320
+ return Math.max(0, Math.min(1, num));
321
+ }
322
+ function normalizeDielineShape(value, fallback = DEFAULT_DIELINE_SHAPE) {
323
+ return isDielineShape(value) ? value : fallback;
324
+ }
325
+ function normalizeShapeStyle(value, fallback = DEFAULT_DIELINE_SHAPE_STYLE) {
326
+ var _a, _b, _c;
327
+ const raw = value && typeof value === "object" ? value : {};
328
+ return {
329
+ ...fallback,
330
+ fitMode: normalizeFitMode(raw.fitMode, fallback.fitMode),
331
+ lobeSpread: normalizeUnitInterval(
332
+ raw.lobeSpread,
333
+ Number((_a = fallback.lobeSpread) != null ? _a : DEFAULT_HEART_SHAPE_PARAMS.lobeSpread)
334
+ ),
335
+ notchDepth: normalizeUnitInterval(
336
+ raw.notchDepth,
337
+ Number((_b = fallback.notchDepth) != null ? _b : DEFAULT_HEART_SHAPE_PARAMS.notchDepth)
338
+ ),
339
+ tipSharpness: normalizeUnitInterval(
340
+ raw.tipSharpness,
341
+ Number((_c = fallback.tipSharpness) != null ? _c : DEFAULT_HEART_SHAPE_PARAMS.tipSharpness)
342
+ )
343
+ };
344
+ }
345
+ function getShapeFitMode(style) {
346
+ return normalizeShapeStyle(style).fitMode;
347
+ }
348
+ function getHeartShapeParams(style) {
349
+ const normalized = normalizeShapeStyle(style);
350
+ return {
351
+ lobeSpread: Number(normalized.lobeSpread),
352
+ notchDepth: Number(normalized.notchDepth),
353
+ tipSharpness: Number(normalized.tipSharpness)
354
+ };
355
+ }
356
+
215
357
  // src/extensions/geometry.ts
216
358
  import paper from "paper";
217
359
 
@@ -354,45 +496,164 @@ function selectOuterChain(args) {
354
496
  if (scoreA !== scoreB) return scoreA > scoreB ? pointsA : pointsB;
355
497
  return pointsA.length <= pointsB.length ? pointsA : pointsB;
356
498
  }
357
- function createBaseShape(options) {
358
- var _a;
359
- const { shape, width, height, radius, x, y, pathData } = options;
360
- const center = new paper.Point(x, y);
361
- if (shape === "rect") {
499
+ function fitPathItemToRect(item, rect, fitMode) {
500
+ const { left, top, width, height } = rect;
501
+ const bounds = item.bounds;
502
+ if (width <= 0 || height <= 0 || !Number.isFinite(bounds.width) || !Number.isFinite(bounds.height) || bounds.width <= 0 || bounds.height <= 0) {
503
+ item.position = new paper.Point(left + width / 2, top + height / 2);
504
+ return item;
505
+ }
506
+ item.translate(new paper.Point(-bounds.left, -bounds.top));
507
+ if (fitMode === "stretch") {
508
+ item.scale(width / bounds.width, height / bounds.height, new paper.Point(0, 0));
509
+ item.translate(new paper.Point(left, top));
510
+ return item;
511
+ }
512
+ const uniformScale = Math.min(width / bounds.width, height / bounds.height);
513
+ item.scale(uniformScale, uniformScale, new paper.Point(0, 0));
514
+ const scaledWidth = bounds.width * uniformScale;
515
+ const scaledHeight = bounds.height * uniformScale;
516
+ item.translate(
517
+ new paper.Point(
518
+ left + (width - scaledWidth) / 2,
519
+ top + (height - scaledHeight) / 2
520
+ )
521
+ );
522
+ return item;
523
+ }
524
+ function createNormalizedHeartPath(params) {
525
+ const { lobeSpread, notchDepth, tipSharpness } = params;
526
+ const halfSpread = 0.22 + lobeSpread * 0.18;
527
+ const notchY = 0.06 + notchDepth * 0.2;
528
+ const shoulderY = 0.24 + notchDepth * 0.2;
529
+ const topLift = 0.12 + (1 - notchDepth) * 0.06;
530
+ const topY = notchY - topLift;
531
+ const sideCtrlY = shoulderY - (0.18 - notchDepth * 0.08);
532
+ const lowerCtrlY = 0.58 + (1 - tipSharpness) * 0.16;
533
+ const tipCtrlX = 0.34 - tipSharpness * 0.2;
534
+ const notchCtrlX = 0.06 + lobeSpread * 0.06;
535
+ const lobeCtrlX = 0.1 + lobeSpread * 0.08;
536
+ const notchCtrlY = notchY - topLift * 0.45;
537
+ const xPeakL = 0.5 - halfSpread;
538
+ const xPeakR = 0.5 + halfSpread;
539
+ const heartPath = new paper.Path({ insert: false });
540
+ heartPath.moveTo(new paper.Point(0.5, notchY));
541
+ heartPath.cubicCurveTo(
542
+ new paper.Point(0.5 - notchCtrlX, notchCtrlY),
543
+ new paper.Point(xPeakL + lobeCtrlX, topY),
544
+ new paper.Point(xPeakL, topY)
545
+ );
546
+ heartPath.cubicCurveTo(
547
+ new paper.Point(xPeakL - lobeCtrlX, topY),
548
+ new paper.Point(0, sideCtrlY),
549
+ new paper.Point(0, shoulderY)
550
+ );
551
+ heartPath.cubicCurveTo(
552
+ new paper.Point(0, lowerCtrlY),
553
+ new paper.Point(tipCtrlX, 1),
554
+ new paper.Point(0.5, 1)
555
+ );
556
+ heartPath.cubicCurveTo(
557
+ new paper.Point(1 - tipCtrlX, 1),
558
+ new paper.Point(1, lowerCtrlY),
559
+ new paper.Point(1, shoulderY)
560
+ );
561
+ heartPath.cubicCurveTo(
562
+ new paper.Point(1, sideCtrlY),
563
+ new paper.Point(xPeakR + lobeCtrlX, topY),
564
+ new paper.Point(xPeakR, topY)
565
+ );
566
+ heartPath.cubicCurveTo(
567
+ new paper.Point(xPeakR - lobeCtrlX, topY),
568
+ new paper.Point(0.5 + notchCtrlX, notchCtrlY),
569
+ new paper.Point(0.5, notchY)
570
+ );
571
+ heartPath.closed = true;
572
+ return heartPath;
573
+ }
574
+ function createHeartBaseShape(options) {
575
+ const { x, y, width, height } = options;
576
+ const w = Math.max(0, width);
577
+ const h = Math.max(0, height);
578
+ const left = x - w / 2;
579
+ const top = y - h / 2;
580
+ const fitMode = getShapeFitMode(options.shapeStyle);
581
+ const heartParams = getHeartShapeParams(options.shapeStyle);
582
+ const rawHeart = createNormalizedHeartPath(heartParams);
583
+ return fitPathItemToRect(rawHeart, { left, top, width: w, height: h }, fitMode);
584
+ }
585
+ var BUILTIN_SHAPE_BUILDERS = {
586
+ rect: (options) => {
587
+ const { x, y, width, height, radius } = options;
362
588
  return new paper.Path.Rectangle({
363
589
  point: [x - width / 2, y - height / 2],
364
590
  size: [Math.max(0, width), Math.max(0, height)],
365
591
  radius: Math.max(0, radius)
366
592
  });
367
- } else if (shape === "circle") {
593
+ },
594
+ circle: (options) => {
595
+ const { x, y, width, height } = options;
368
596
  const r = Math.min(width, height) / 2;
369
597
  return new paper.Path.Circle({
370
- center,
598
+ center: new paper.Point(x, y),
371
599
  radius: Math.max(0, r)
372
600
  });
373
- } else if (shape === "ellipse") {
601
+ },
602
+ ellipse: (options) => {
603
+ const { x, y, width, height } = options;
374
604
  return new paper.Path.Ellipse({
375
- center,
605
+ center: new paper.Point(x, y),
376
606
  radius: [Math.max(0, width / 2), Math.max(0, height / 2)]
377
607
  });
378
- } else if (shape === "custom" && pathData) {
379
- const hasMultipleSubPaths = ((_a = (pathData.match(/[Mm]/g) || []).length) != null ? _a : 0) > 1;
380
- const path = hasMultipleSubPaths ? new paper.CompoundPath(pathData) : (() => {
381
- const single = new paper.Path();
382
- single.pathData = pathData;
383
- return single;
384
- })();
608
+ },
609
+ heart: createHeartBaseShape
610
+ };
611
+ function createCustomBaseShape(options) {
612
+ var _a;
613
+ const {
614
+ pathData,
615
+ customSourceWidthPx,
616
+ customSourceHeightPx,
617
+ x,
618
+ y,
619
+ width,
620
+ height
621
+ } = options;
622
+ if (typeof pathData !== "string" || pathData.trim().length === 0) {
623
+ return null;
624
+ }
625
+ const center = new paper.Point(x, y);
626
+ const hasMultipleSubPaths = ((_a = (pathData.match(/[Mm]/g) || []).length) != null ? _a : 0) > 1;
627
+ const path = hasMultipleSubPaths ? new paper.CompoundPath(pathData) : (() => {
628
+ const single = new paper.Path();
629
+ single.pathData = pathData;
630
+ return single;
631
+ })();
632
+ const sourceWidth = Number(customSourceWidthPx != null ? customSourceWidthPx : 0);
633
+ const sourceHeight = Number(customSourceHeightPx != null ? customSourceHeightPx : 0);
634
+ if (Number.isFinite(sourceWidth) && Number.isFinite(sourceHeight) && sourceWidth > 0 && sourceHeight > 0 && width > 0 && height > 0) {
635
+ const targetLeft = x - width / 2;
636
+ const targetTop = y - height / 2;
637
+ path.scale(width / sourceWidth, height / sourceHeight, new paper.Point(0, 0));
638
+ path.translate(new paper.Point(targetLeft, targetTop));
639
+ return path;
640
+ }
641
+ if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
385
642
  path.position = center;
386
- if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
387
- path.scale(width / path.bounds.width, height / path.bounds.height);
388
- }
643
+ path.scale(width / path.bounds.width, height / path.bounds.height);
389
644
  return path;
390
- } else {
391
- return new paper.Path.Rectangle({
392
- point: [x - width / 2, y - height / 2],
393
- size: [Math.max(0, width), Math.max(0, height)]
394
- });
395
645
  }
646
+ path.position = center;
647
+ return path;
648
+ }
649
+ function createBaseShape(options) {
650
+ const { shape } = options;
651
+ if (shape === "custom") {
652
+ const customShape = createCustomBaseShape(options);
653
+ if (customShape) return customShape;
654
+ return BUILTIN_SHAPE_BUILDERS[DEFAULT_DIELINE_SHAPE](options);
655
+ }
656
+ return BUILTIN_SHAPE_BUILDERS[shape](options);
396
657
  }
397
658
  function resolveBridgeBasePath(shape, anchor) {
398
659
  if (shape instanceof paper.Path) {
@@ -716,6 +977,18 @@ function getNearestPointOnDieline(point, options) {
716
977
  shape.remove();
717
978
  return result;
718
979
  }
980
+ function getPathBounds(pathData) {
981
+ const path = new paper.Path();
982
+ path.pathData = pathData;
983
+ const bounds = path.bounds;
984
+ path.remove();
985
+ return {
986
+ x: bounds.x,
987
+ y: bounds.y,
988
+ width: bounds.width,
989
+ height: bounds.height
990
+ };
991
+ }
719
992
 
720
993
  // src/coordinate.ts
721
994
  var Coordinate = class {
@@ -804,12 +1077,6 @@ function parseLengthToMm(input, defaultUnit) {
804
1077
  const unit = (_b = (_a = match[2]) == null ? void 0 : _a.toLowerCase()) != null ? _b : defaultUnit;
805
1078
  return Coordinate.convertUnit(value, unit, "mm");
806
1079
  }
807
- function formatMm(valueMm, displayUnit, fractionDigits = 2) {
808
- if (!Number.isFinite(valueMm)) return "0";
809
- const value = Coordinate.convertUnit(valueMm, "mm", displayUnit);
810
- const rounded = Number(value.toFixed(fractionDigits));
811
- return rounded.toString();
812
- }
813
1080
 
814
1081
  // src/extensions/sceneLayoutModel.ts
815
1082
  var DEFAULT_SIZE_STATE = {
@@ -1020,10 +1287,19 @@ function buildSceneGeometry(configService, layout) {
1020
1287
  "mm"
1021
1288
  );
1022
1289
  const offset = (layout.cutRect.width - layout.trimRect.width) / 2;
1290
+ const sourceWidth = Number(configService.get("dieline.customSourceWidthPx", 0));
1291
+ const sourceHeight = Number(
1292
+ configService.get("dieline.customSourceHeightPx", 0)
1293
+ );
1294
+ const shapeStyle = normalizeShapeStyle(
1295
+ configService.get("dieline.shapeStyle", DEFAULT_DIELINE_SHAPE_STYLE)
1296
+ );
1023
1297
  return {
1024
- shape: configService.get("dieline.shape", "rect"),
1025
- unit: "mm",
1026
- displayUnit: normalizeUnit(configService.get("size.unit", "mm")),
1298
+ shape: normalizeDielineShape(
1299
+ configService.get("dieline.shape", DEFAULT_DIELINE_SHAPE)
1300
+ ),
1301
+ shapeStyle,
1302
+ unit: "px",
1027
1303
  x: layout.trimRect.centerX,
1028
1304
  y: layout.trimRect.centerY,
1029
1305
  width: layout.trimRect.width,
@@ -1031,7 +1307,9 @@ function buildSceneGeometry(configService, layout) {
1031
1307
  radius: radiusMm * layout.scale,
1032
1308
  offset,
1033
1309
  scale: layout.scale,
1034
- pathData: configService.get("dieline.pathData")
1310
+ pathData: configService.get("dieline.pathData"),
1311
+ customSourceWidthPx: Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : void 0,
1312
+ customSourceHeightPx: Number.isFinite(sourceHeight) && sourceHeight > 0 ? sourceHeight : void 0
1035
1313
  };
1036
1314
  }
1037
1315
 
@@ -1054,6 +1332,7 @@ var ImageTool = class {
1054
1332
  this.isImageSelectionActive = false;
1055
1333
  this.focusedImageId = null;
1056
1334
  this.renderSeq = 0;
1335
+ this.overlaySpecs = [];
1057
1336
  this.onToolActivated = (event) => {
1058
1337
  const before = this.isToolActive;
1059
1338
  this.syncToolActiveFromWorkbench(event.id);
@@ -1131,28 +1410,41 @@ var ImageTool = class {
1131
1410
  const frame = this.getFrameRect();
1132
1411
  if (!frame.width || !frame.height) return;
1133
1412
  const center = target.getCenterPoint ? target.getCenterPoint() : new Point((_c = target.left) != null ? _c : 0, (_d = target.top) != null ? _d : 0);
1413
+ const centerScene = this.canvasService ? this.canvasService.toScenePoint({ x: center.x, y: center.y }) : { x: center.x, y: center.y };
1134
1414
  const objectScale = Number.isFinite(target == null ? void 0 : target.scaleX) ? target.scaleX : 1;
1415
+ const objectScaleScene = this.toSceneObjectScale(objectScale || 1);
1135
1416
  const workingItem = this.workingItems.find((item) => item.id === id);
1136
1417
  const sourceKey = (workingItem == null ? void 0 : workingItem.sourceUrl) || (workingItem == null ? void 0 : workingItem.url) || "";
1137
1418
  const sourceSize = this.getSourceSize(sourceKey, target);
1138
1419
  const coverScale = this.getCoverScale(frame, sourceSize);
1139
1420
  const updates = {
1140
- left: this.clampNormalized((center.x - frame.left) / frame.width),
1141
- top: this.clampNormalized((center.y - frame.top) / frame.height),
1421
+ left: this.clampNormalized((centerScene.x - frame.left) / frame.width),
1422
+ top: this.clampNormalized((centerScene.y - frame.top) / frame.height),
1142
1423
  angle: Number.isFinite(target.angle) ? target.angle : 0,
1143
- scale: Math.max(0.05, (objectScale || 1) / coverScale)
1424
+ scale: Math.max(0.05, objectScaleScene / coverScale)
1144
1425
  };
1145
1426
  this.focusedImageId = id;
1146
1427
  this.updateImageInWorking(id, updates);
1147
1428
  };
1148
1429
  }
1149
1430
  activate(context) {
1431
+ var _a;
1150
1432
  this.context = context;
1151
1433
  this.canvasService = context.services.get("CanvasService");
1152
1434
  if (!this.canvasService) {
1153
1435
  console.warn("CanvasService not found for ImageTool");
1154
1436
  return;
1155
1437
  }
1438
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
1439
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
1440
+ this.id,
1441
+ () => ({
1442
+ rootLayerSpecs: {
1443
+ [IMAGE_OVERLAY_LAYER_ID]: this.overlaySpecs
1444
+ }
1445
+ }),
1446
+ { priority: 300 }
1447
+ );
1156
1448
  context.eventBus.on("tool:activated", this.onToolActivated);
1157
1449
  context.eventBus.on("object:modified", this.onObjectModified);
1158
1450
  context.eventBus.on("selection:created", this.onSelectionChanged);
@@ -1193,7 +1485,7 @@ var ImageTool = class {
1193
1485
  this.updateImages();
1194
1486
  }
1195
1487
  deactivate(context) {
1196
- var _a;
1488
+ var _a, _b;
1197
1489
  context.eventBus.off("tool:activated", this.onToolActivated);
1198
1490
  context.eventBus.off("object:modified", this.onObjectModified);
1199
1491
  context.eventBus.off("selection:created", this.onSelectionChanged);
@@ -1205,12 +1497,13 @@ var ImageTool = class {
1205
1497
  this.dirtyTrackerDisposable = void 0;
1206
1498
  this.cropShapeHatchPattern = void 0;
1207
1499
  this.cropShapeHatchPatternColor = void 0;
1500
+ this.cropShapeHatchPatternKey = void 0;
1501
+ this.overlaySpecs = [];
1208
1502
  this.clearRenderedImages();
1503
+ (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
1504
+ this.renderProducerDisposable = void 0;
1209
1505
  if (this.canvasService) {
1210
- void this.canvasService.applyObjectSpecsToRootLayer(
1211
- IMAGE_OVERLAY_LAYER_ID,
1212
- []
1213
- );
1506
+ void this.canvasService.flushRenderFromProducers();
1214
1507
  this.canvasService = void 0;
1215
1508
  }
1216
1509
  this.context = void 0;
@@ -1621,38 +1914,38 @@ var ImageTool = class {
1621
1914
  if (!layout) {
1622
1915
  return { left: 0, top: 0, width: 0, height: 0 };
1623
1916
  }
1624
- return {
1917
+ return this.canvasService.toSceneRect({
1625
1918
  left: layout.cutRect.left,
1626
1919
  top: layout.cutRect.top,
1627
1920
  width: layout.cutRect.width,
1628
1921
  height: layout.cutRect.height
1922
+ });
1923
+ }
1924
+ getFrameRectScreen(frame) {
1925
+ if (!this.canvasService) {
1926
+ return { left: 0, top: 0, width: 0, height: 0 };
1927
+ }
1928
+ return this.canvasService.toScreenRect(frame || this.getFrameRect());
1929
+ }
1930
+ toLayoutSceneRect(rect) {
1931
+ return {
1932
+ left: rect.left,
1933
+ top: rect.top,
1934
+ width: rect.width,
1935
+ height: rect.height,
1936
+ space: "scene"
1629
1937
  };
1630
1938
  }
1631
1939
  async resolveDefaultFitArea() {
1632
- if (!this.context || !this.canvasService) return null;
1633
- const commandService = this.context.services.get("CommandService");
1634
- if (!commandService) return null;
1635
- try {
1636
- const layout = await Promise.resolve(
1637
- commandService.executeCommand("getSceneLayout")
1638
- );
1639
- const cutRect = layout == null ? void 0 : layout.cutRect;
1640
- const width = Number(cutRect == null ? void 0 : cutRect.width);
1641
- const height = Number(cutRect == null ? void 0 : cutRect.height);
1642
- const left = Number(cutRect == null ? void 0 : cutRect.left);
1643
- const top = Number(cutRect == null ? void 0 : cutRect.top);
1644
- if (!Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(left) || !Number.isFinite(top)) {
1645
- return null;
1646
- }
1647
- return {
1648
- width: Math.max(1, width),
1649
- height: Math.max(1, height),
1650
- left: left + width / 2,
1651
- top: top + height / 2
1652
- };
1653
- } catch (e) {
1654
- return null;
1655
- }
1940
+ if (!this.canvasService) return null;
1941
+ const frame = this.getFrameRect();
1942
+ if (frame.width <= 0 || frame.height <= 0) return null;
1943
+ return {
1944
+ width: Math.max(1, frame.width),
1945
+ height: Math.max(1, frame.height),
1946
+ left: frame.left + frame.width / 2,
1947
+ top: frame.top + frame.height / 2
1948
+ };
1656
1949
  }
1657
1950
  async fitImageToDefaultArea(id) {
1658
1951
  if (!this.canvasService) return;
@@ -1661,13 +1954,14 @@ var ImageTool = class {
1661
1954
  await this.fitImageToArea(id, area);
1662
1955
  return;
1663
1956
  }
1664
- const canvasW = Math.max(1, this.canvasService.canvas.width || 0);
1665
- const canvasH = Math.max(1, this.canvasService.canvas.height || 0);
1957
+ const viewport = this.canvasService.getSceneViewportRect();
1958
+ const canvasW = Math.max(1, viewport.width || 0);
1959
+ const canvasH = Math.max(1, viewport.height || 0);
1666
1960
  await this.fitImageToArea(id, {
1667
1961
  width: canvasW,
1668
1962
  height: canvasH,
1669
- left: canvasW / 2,
1670
- top: canvasH / 2
1963
+ left: viewport.left + canvasW / 2,
1964
+ top: viewport.top + canvasH / 2
1671
1965
  });
1672
1966
  }
1673
1967
  getImageObjects() {
@@ -1755,13 +2049,17 @@ var ImageTool = class {
1755
2049
  }
1756
2050
  toSceneGeometryLike(raw) {
1757
2051
  const shape = raw == null ? void 0 : raw.shape;
1758
- if (shape !== "rect" && shape !== "circle" && shape !== "ellipse" && shape !== "custom") {
2052
+ if (!isDielineShape(shape)) {
1759
2053
  return null;
1760
2054
  }
1761
- const radius = Number(raw == null ? void 0 : raw.radius);
1762
- const offset = Number(raw == null ? void 0 : raw.offset);
2055
+ const radiusRaw = Number(raw == null ? void 0 : raw.radius);
2056
+ const offsetRaw = Number(raw == null ? void 0 : raw.offset);
2057
+ const unit = typeof (raw == null ? void 0 : raw.unit) === "string" ? raw.unit : "px";
2058
+ const radius = unit === "scene" || !this.canvasService ? radiusRaw : this.canvasService.toSceneLength(radiusRaw);
2059
+ const offset = unit === "scene" || !this.canvasService ? offsetRaw : this.canvasService.toSceneLength(offsetRaw);
1763
2060
  return {
1764
2061
  shape,
2062
+ shapeStyle: normalizeShapeStyle(raw == null ? void 0 : raw.shapeStyle),
1765
2063
  radius: Number.isFinite(radius) ? radius : 0,
1766
2064
  offset: Number.isFinite(offset) ? offset : 0
1767
2065
  };
@@ -1813,8 +2111,11 @@ var ImageTool = class {
1813
2111
  return Math.max(0, Math.min(maxRadius, rawCutRadius));
1814
2112
  }
1815
2113
  getCropShapeHatchPattern(color = "rgba(255, 0, 0, 0.6)") {
2114
+ var _a;
1816
2115
  if (typeof document === "undefined") return void 0;
1817
- if (this.cropShapeHatchPattern && this.cropShapeHatchPatternColor === color) {
2116
+ const sceneScale = ((_a = this.canvasService) == null ? void 0 : _a.getSceneScale()) || 1;
2117
+ const cacheKey = `${color}::${sceneScale.toFixed(6)}`;
2118
+ if (this.cropShapeHatchPattern && this.cropShapeHatchPatternColor === color && this.cropShapeHatchPatternKey === cacheKey) {
1818
2119
  return this.cropShapeHatchPattern;
1819
2120
  }
1820
2121
  const size = 16;
@@ -1843,11 +2144,21 @@ var ImageTool = class {
1843
2144
  // @ts-ignore: Fabric Pattern accepts canvas source here.
1844
2145
  repetition: "repeat"
1845
2146
  });
2147
+ pattern.patternTransform = [
2148
+ 1 / sceneScale,
2149
+ 0,
2150
+ 0,
2151
+ 1 / sceneScale,
2152
+ 0,
2153
+ 0
2154
+ ];
1846
2155
  this.cropShapeHatchPattern = pattern;
1847
2156
  this.cropShapeHatchPatternColor = color;
2157
+ this.cropShapeHatchPatternKey = cacheKey;
1848
2158
  return pattern;
1849
2159
  }
1850
2160
  buildCropShapeOverlaySpecs(frame, sceneGeometry) {
2161
+ var _a, _b;
1851
2162
  if (!sceneGeometry) {
1852
2163
  this.debug("overlay:shape:skip", { reason: "scene-geometry-missing" });
1853
2164
  return [];
@@ -1857,6 +2168,7 @@ var ImageTool = class {
1857
2168
  return [];
1858
2169
  }
1859
2170
  const shape = sceneGeometry.shape;
2171
+ const shapeStyle = sceneGeometry.shapeStyle;
1860
2172
  const inset = 0;
1861
2173
  const shapeWidth = Math.max(1, frame.width);
1862
2174
  const shapeHeight = Math.max(1, frame.height);
@@ -1866,6 +2178,7 @@ var ImageTool = class {
1866
2178
  frameWidth: frame.width,
1867
2179
  frameHeight: frame.height,
1868
2180
  offset: sceneGeometry.offset,
2181
+ shapeStyle,
1869
2182
  inset,
1870
2183
  shapeWidth,
1871
2184
  shapeHeight,
@@ -1887,6 +2200,7 @@ var ImageTool = class {
1887
2200
  x: frame.width / 2,
1888
2201
  y: frame.height / 2,
1889
2202
  features: [],
2203
+ shapeStyle,
1890
2204
  canvasWidth: frame.width,
1891
2205
  canvasHeight: frame.height
1892
2206
  };
@@ -1904,6 +2218,9 @@ var ImageTool = class {
1904
2218
  }
1905
2219
  const patternFill = this.getCropShapeHatchPattern();
1906
2220
  const hatchFill = patternFill || "rgba(255, 0, 0, 0.22)";
2221
+ const shapeBounds = getPathBounds(shapePathData);
2222
+ const hatchBounds = getPathBounds(hatchPathData);
2223
+ const frameRect = this.toLayoutSceneRect(frame);
1907
2224
  const hatchPathLength = hatchPathData.length;
1908
2225
  const shapePathLength = shapePathData.length;
1909
2226
  const specs = [
@@ -1911,10 +2228,16 @@ var ImageTool = class {
1911
2228
  id: "image.cropShapeHatch",
1912
2229
  type: "path",
1913
2230
  data: { id: "image.cropShapeHatch", zIndex: 5 },
2231
+ layout: {
2232
+ reference: "custom",
2233
+ referenceRect: frameRect,
2234
+ alignX: "start",
2235
+ alignY: "start",
2236
+ offsetX: hatchBounds.x,
2237
+ offsetY: hatchBounds.y
2238
+ },
1914
2239
  props: {
1915
2240
  pathData: hatchPathData,
1916
- left: frame.left,
1917
- top: frame.top,
1918
2241
  originX: "left",
1919
2242
  originY: "top",
1920
2243
  fill: hatchFill,
@@ -1931,15 +2254,21 @@ var ImageTool = class {
1931
2254
  id: "image.cropShapePath",
1932
2255
  type: "path",
1933
2256
  data: { id: "image.cropShapePath", zIndex: 6 },
2257
+ layout: {
2258
+ reference: "custom",
2259
+ referenceRect: frameRect,
2260
+ alignX: "start",
2261
+ alignY: "start",
2262
+ offsetX: shapeBounds.x,
2263
+ offsetY: shapeBounds.y
2264
+ },
1934
2265
  props: {
1935
2266
  pathData: shapePathData,
1936
- left: frame.left,
1937
- top: frame.top,
1938
2267
  originX: "left",
1939
2268
  originY: "top",
1940
2269
  fill: "rgba(0,0,0,0)",
1941
2270
  stroke: "rgba(255, 0, 0, 0.9)",
1942
- strokeWidth: 1,
2271
+ strokeWidth: (_b = (_a = this.canvasService) == null ? void 0 : _a.toSceneLength(1)) != null ? _b : 1,
1943
2272
  selectable: false,
1944
2273
  evented: false,
1945
2274
  excludeFromExport: true,
@@ -1956,6 +2285,8 @@ var ImageTool = class {
1956
2285
  fillRule: "evenodd",
1957
2286
  shapePathLength,
1958
2287
  hatchPathLength,
2288
+ shapeBounds,
2289
+ hatchBounds,
1959
2290
  hatchFillType: hatchFill && typeof hatchFill === "object" ? "pattern" : "color",
1960
2291
  ids: specs.map((spec) => spec.id)
1961
2292
  });
@@ -2018,6 +2349,28 @@ var ImageTool = class {
2018
2349
  opacity: render.opacity
2019
2350
  };
2020
2351
  }
2352
+ toScreenObjectProps(props) {
2353
+ if (!this.canvasService) return props;
2354
+ const next = { ...props };
2355
+ if (Number.isFinite(next.left) || Number.isFinite(next.top)) {
2356
+ const mapped = this.canvasService.toScreenPoint({
2357
+ x: Number.isFinite(next.left) ? Number(next.left) : 0,
2358
+ y: Number.isFinite(next.top) ? Number(next.top) : 0
2359
+ });
2360
+ if (Number.isFinite(next.left)) next.left = mapped.x;
2361
+ if (Number.isFinite(next.top)) next.top = mapped.y;
2362
+ }
2363
+ const sceneScale = this.canvasService.getSceneScale();
2364
+ const sx = Number.isFinite(next.scaleX) ? Number(next.scaleX) : 1;
2365
+ const sy = Number.isFinite(next.scaleY) ? Number(next.scaleY) : 1;
2366
+ next.scaleX = sx * sceneScale;
2367
+ next.scaleY = sy * sceneScale;
2368
+ return next;
2369
+ }
2370
+ toSceneObjectScale(value) {
2371
+ if (!this.canvasService) return value;
2372
+ return value / this.canvasService.getSceneScale();
2373
+ }
2021
2374
  getCurrentSrc(obj) {
2022
2375
  var _a;
2023
2376
  if (!obj) return void 0;
@@ -2050,7 +2403,7 @@ var ImageTool = class {
2050
2403
  obj = void 0;
2051
2404
  }
2052
2405
  if (!obj) {
2053
- const created = await FabricImage.fromURL(render.src, {
2406
+ const created = await FabricImage2.fromURL(render.src, {
2054
2407
  crossOrigin: "anonymous"
2055
2408
  });
2056
2409
  if (seq !== this.renderSeq) return;
@@ -2067,8 +2420,9 @@ var ImageTool = class {
2067
2420
  this.rememberSourceSize(render.src, obj);
2068
2421
  const sourceSize = this.getSourceSize(render.src, obj);
2069
2422
  const props = this.computeCanvasProps(render, sourceSize, frame);
2423
+ const screenProps = this.toScreenObjectProps(props);
2070
2424
  obj.set({
2071
- ...props,
2425
+ ...screenProps,
2072
2426
  data: {
2073
2427
  ...obj.data || {},
2074
2428
  id: item.id,
@@ -2134,37 +2488,67 @@ var ImageTool = class {
2134
2488
  });
2135
2489
  return [];
2136
2490
  }
2137
- const canvasW = this.canvasService.canvas.width || 0;
2138
- const canvasH = this.canvasService.canvas.height || 0;
2491
+ const viewport = this.canvasService.getSceneViewportRect();
2492
+ const canvasW = viewport.width || 0;
2493
+ const canvasH = viewport.height || 0;
2494
+ const canvasLeft = viewport.left || 0;
2495
+ const canvasTop = viewport.top || 0;
2139
2496
  const visual = this.getFrameVisualConfig();
2140
- const frameLeft = Math.max(0, Math.min(canvasW, frame.left));
2141
- const frameTop = Math.max(0, Math.min(canvasH, frame.top));
2497
+ const strokeWidthScene = this.canvasService.toSceneLength(
2498
+ visual.strokeWidth
2499
+ );
2500
+ const dashLengthScene = this.canvasService.toSceneLength(visual.dashLength);
2501
+ const frameLeft = Math.max(
2502
+ canvasLeft,
2503
+ Math.min(canvasLeft + canvasW, frame.left)
2504
+ );
2505
+ const frameTop = Math.max(
2506
+ canvasTop,
2507
+ Math.min(canvasTop + canvasH, frame.top)
2508
+ );
2142
2509
  const frameRight = Math.max(
2143
2510
  frameLeft,
2144
- Math.min(canvasW, frame.left + frame.width)
2511
+ Math.min(canvasLeft + canvasW, frame.left + frame.width)
2145
2512
  );
2146
2513
  const frameBottom = Math.max(
2147
2514
  frameTop,
2148
- Math.min(canvasH, frame.top + frame.height)
2515
+ Math.min(canvasTop + canvasH, frame.top + frame.height)
2149
2516
  );
2150
2517
  const visibleFrameH = Math.max(0, frameBottom - frameTop);
2151
- const topH = frameTop;
2152
- const bottomH = Math.max(0, canvasH - frameBottom);
2153
- const leftW = frameLeft;
2154
- const rightW = Math.max(0, canvasW - frameRight);
2518
+ const topH = Math.max(0, frameTop - canvasTop);
2519
+ const bottomH = Math.max(0, canvasTop + canvasH - frameBottom);
2520
+ const leftW = Math.max(0, frameLeft - canvasLeft);
2521
+ const rightW = Math.max(0, canvasLeft + canvasW - frameRight);
2522
+ const viewportRect = this.toLayoutSceneRect({
2523
+ left: canvasLeft,
2524
+ top: canvasTop,
2525
+ width: canvasW,
2526
+ height: canvasH
2527
+ });
2528
+ const visibleFrameBandRect = this.toLayoutSceneRect({
2529
+ left: canvasLeft,
2530
+ top: frameTop,
2531
+ width: canvasW,
2532
+ height: visibleFrameH
2533
+ });
2534
+ const frameRect = this.toLayoutSceneRect(frame);
2155
2535
  const shapeOverlay = this.buildCropShapeOverlaySpecs(frame, sceneGeometry);
2156
2536
  const mask = [
2157
2537
  {
2158
2538
  id: "image.cropMask.top",
2159
2539
  type: "rect",
2160
2540
  data: { id: "image.cropMask.top", zIndex: 1 },
2541
+ layout: {
2542
+ reference: "custom",
2543
+ referenceRect: viewportRect,
2544
+ alignX: "start",
2545
+ alignY: "start",
2546
+ width: "100%",
2547
+ height: topH
2548
+ },
2161
2549
  props: {
2162
- left: canvasW / 2,
2163
- top: topH / 2,
2164
- width: canvasW,
2165
- height: topH,
2166
- originX: "center",
2167
- originY: "center",
2550
+ originX: "left",
2551
+ originY: "top",
2168
2552
  fill: visual.outerBackground,
2169
2553
  selectable: false,
2170
2554
  evented: false
@@ -2174,13 +2558,17 @@ var ImageTool = class {
2174
2558
  id: "image.cropMask.bottom",
2175
2559
  type: "rect",
2176
2560
  data: { id: "image.cropMask.bottom", zIndex: 2 },
2561
+ layout: {
2562
+ reference: "custom",
2563
+ referenceRect: viewportRect,
2564
+ alignX: "start",
2565
+ alignY: "end",
2566
+ width: "100%",
2567
+ height: bottomH
2568
+ },
2177
2569
  props: {
2178
- left: canvasW / 2,
2179
- top: frameBottom + bottomH / 2,
2180
- width: canvasW,
2181
- height: bottomH,
2182
- originX: "center",
2183
- originY: "center",
2570
+ originX: "left",
2571
+ originY: "top",
2184
2572
  fill: visual.outerBackground,
2185
2573
  selectable: false,
2186
2574
  evented: false
@@ -2190,13 +2578,17 @@ var ImageTool = class {
2190
2578
  id: "image.cropMask.left",
2191
2579
  type: "rect",
2192
2580
  data: { id: "image.cropMask.left", zIndex: 3 },
2193
- props: {
2194
- left: leftW / 2,
2195
- top: frameTop + visibleFrameH / 2,
2581
+ layout: {
2582
+ reference: "custom",
2583
+ referenceRect: visibleFrameBandRect,
2584
+ alignX: "start",
2585
+ alignY: "start",
2196
2586
  width: leftW,
2197
- height: visibleFrameH,
2198
- originX: "center",
2199
- originY: "center",
2587
+ height: "100%"
2588
+ },
2589
+ props: {
2590
+ originX: "left",
2591
+ originY: "top",
2200
2592
  fill: visual.outerBackground,
2201
2593
  selectable: false,
2202
2594
  evented: false
@@ -2206,13 +2598,17 @@ var ImageTool = class {
2206
2598
  id: "image.cropMask.right",
2207
2599
  type: "rect",
2208
2600
  data: { id: "image.cropMask.right", zIndex: 4 },
2209
- props: {
2210
- left: frameRight + rightW / 2,
2211
- top: frameTop + visibleFrameH / 2,
2601
+ layout: {
2602
+ reference: "custom",
2603
+ referenceRect: visibleFrameBandRect,
2604
+ alignX: "end",
2605
+ alignY: "start",
2212
2606
  width: rightW,
2213
- height: visibleFrameH,
2214
- originX: "center",
2215
- originY: "center",
2607
+ height: "100%"
2608
+ },
2609
+ props: {
2610
+ originX: "left",
2611
+ originY: "top",
2216
2612
  fill: visual.outerBackground,
2217
2613
  selectable: false,
2218
2614
  evented: false
@@ -2223,17 +2619,21 @@ var ImageTool = class {
2223
2619
  id: "image.cropFrame",
2224
2620
  type: "rect",
2225
2621
  data: { id: "image.cropFrame", zIndex: 7 },
2622
+ layout: {
2623
+ reference: "custom",
2624
+ referenceRect: frameRect,
2625
+ alignX: "start",
2626
+ alignY: "start",
2627
+ width: "100%",
2628
+ height: "100%"
2629
+ },
2226
2630
  props: {
2227
- left: frame.left + frame.width / 2,
2228
- top: frame.top + frame.height / 2,
2229
- width: frame.width,
2230
- height: frame.height,
2231
- originX: "center",
2232
- originY: "center",
2631
+ originX: "left",
2632
+ originY: "top",
2233
2633
  fill: visual.innerBackground,
2234
2634
  stroke: visual.strokeStyle === "hidden" ? "rgba(0,0,0,0)" : visual.strokeColor,
2235
- strokeWidth: visual.strokeStyle === "hidden" ? 0 : visual.strokeWidth,
2236
- strokeDashArray: visual.strokeStyle === "dashed" ? [visual.dashLength, visual.dashLength] : void 0,
2635
+ strokeWidth: visual.strokeStyle === "hidden" ? 0 : strokeWidthScene,
2636
+ strokeDashArray: visual.strokeStyle === "dashed" ? [dashLengthScene, dashLengthScene] : void 0,
2237
2637
  selectable: false,
2238
2638
  evented: false
2239
2639
  }
@@ -2284,10 +2684,8 @@ var ImageTool = class {
2284
2684
  const sceneGeometry = await this.resolveSceneGeometryForOverlay();
2285
2685
  if (seq !== this.renderSeq) return;
2286
2686
  const overlaySpecs = this.buildOverlaySpecs(frame, sceneGeometry);
2287
- await this.canvasService.applyObjectSpecsToRootLayer(
2288
- IMAGE_OVERLAY_LAYER_ID,
2289
- overlaySpecs
2290
- );
2687
+ this.overlaySpecs = overlaySpecs;
2688
+ await this.canvasService.flushRenderFromProducers();
2291
2689
  this.syncImageZOrder(renderItems);
2292
2690
  const overlayCanvasCount = this.getOverlayObjects().length;
2293
2691
  this.debug("render:done", {
@@ -2378,7 +2776,7 @@ var ImageTool = class {
2378
2776
  const source = this.getSourceSize(render.src, obj);
2379
2777
  const frame = this.getFrameRect();
2380
2778
  const coverScale = this.getCoverScale(frame, source);
2381
- const currentScale = obj.scaleX || 1;
2779
+ const currentScale = this.toSceneObjectScale(obj.scaleX || 1);
2382
2780
  const zoom = Math.max(0.05, currentScale / coverScale);
2383
2781
  const updated = {
2384
2782
  scale: Number.isFinite(zoom) ? zoom : 1,
@@ -2415,12 +2813,13 @@ var ImageTool = class {
2415
2813
  Math.max(1, area.width) / Math.max(1, source.width),
2416
2814
  Math.max(1, area.height) / Math.max(1, source.height)
2417
2815
  );
2418
- const canvasW = this.canvasService.canvas.width || 1;
2419
- const canvasH = this.canvasService.canvas.height || 1;
2816
+ const viewport = this.canvasService.getSceneViewportRect();
2817
+ const canvasW = viewport.width || 1;
2818
+ const canvasH = viewport.height || 1;
2420
2819
  const areaLeftInput = (_a = area.left) != null ? _a : 0.5;
2421
2820
  const areaTopInput = (_b = area.top) != null ? _b : 0.5;
2422
- const areaLeftPx = areaLeftInput <= 1.5 ? areaLeftInput * canvasW : areaLeftInput;
2423
- const areaTopPx = areaTopInput <= 1.5 ? areaTopInput * canvasH : areaTopInput;
2821
+ const areaLeftPx = areaLeftInput <= 1.5 ? viewport.left + areaLeftInput * canvasW : areaLeftInput;
2822
+ const areaTopPx = areaTopInput <= 1.5 ? viewport.top + areaTopInput * canvasH : areaTopInput;
2424
2823
  const updates = {
2425
2824
  scale: Math.max(0.05, desiredScale / baseCover),
2426
2825
  left: this.clampNormalized(
@@ -2458,6 +2857,8 @@ var ImageTool = class {
2458
2857
  this.normalizeItem({
2459
2858
  ...item,
2460
2859
  url,
2860
+ // Keep original source for next image-tool session editing,
2861
+ // and use committedUrl as non-image-tools render source.
2461
2862
  sourceUrl,
2462
2863
  committedUrl: url
2463
2864
  })
@@ -2483,7 +2884,8 @@ var ImageTool = class {
2483
2884
  if (!normalizedIds.length) {
2484
2885
  throw new Error("image-ids-required");
2485
2886
  }
2486
- const frame = this.getFrameRect();
2887
+ const frameScene = this.getFrameRect();
2888
+ const frame = this.getFrameRectScreen(frameScene);
2487
2889
  const multiplier = Math.max(1, (_a = options.multiplier) != null ? _a : 2);
2488
2890
  const format = options.format === "jpeg" ? "jpeg" : "png";
2489
2891
  const width = Math.max(1, Math.round(frame.width * multiplier));
@@ -3365,14 +3767,10 @@ var ImageTracer = class {
3365
3767
  };
3366
3768
  }
3367
3769
  const baseUnpaddedContours = baseContours.map(
3368
- (contour) => this.clampPointsToImageBounds(
3369
- contour.map((p) => ({
3370
- x: p.x - padding,
3371
- y: p.y - padding
3372
- })),
3373
- width,
3374
- height
3375
- )
3770
+ (contour) => contour.map((p) => ({
3771
+ x: p.x - padding,
3772
+ y: p.y - padding
3773
+ }))
3376
3774
  ).filter((contour) => contour.length > 2);
3377
3775
  if (!baseUnpaddedContours.length) {
3378
3776
  const w = (_h = options.scaleToWidth) != null ? _h : width;
@@ -3461,13 +3859,46 @@ var ImageTracer = class {
3461
3859
  this.flattenContours(baseScaledContours)
3462
3860
  );
3463
3861
  }
3464
- debugLog("traceWithBounds:contours", {
3465
- baseContourCount: baseContoursRaw.length,
3466
- baseSelectedCount: baseContours.length,
3467
- expandedContourCount: expandedContoursRaw.length,
3468
- expandedSelectedCount: expandedContours.length,
3469
- baseBounds,
3470
- expandedBounds: globalBounds,
3862
+ if (expand > 0) {
3863
+ const expectedExpandedBounds = {
3864
+ x: baseBounds.x - expand,
3865
+ y: baseBounds.y - expand,
3866
+ width: baseBounds.width + expand * 2,
3867
+ height: baseBounds.height + expand * 2
3868
+ };
3869
+ if (expectedExpandedBounds.width > 0 && expectedExpandedBounds.height > 0 && globalBounds.width > 0 && globalBounds.height > 0) {
3870
+ 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;
3871
+ if (shouldNormalizeExpandBounds) {
3872
+ const beforeNormalize = globalBounds;
3873
+ finalContours = this.translateContours(
3874
+ this.scaleContours(
3875
+ finalContours,
3876
+ expectedExpandedBounds.width,
3877
+ expectedExpandedBounds.height,
3878
+ globalBounds
3879
+ ),
3880
+ expectedExpandedBounds.x,
3881
+ expectedExpandedBounds.y
3882
+ );
3883
+ globalBounds = this.boundsFromPoints(
3884
+ this.flattenContours(finalContours)
3885
+ );
3886
+ debugLog("traceWithBounds:expand-normalized", {
3887
+ expand,
3888
+ expectedExpandedBounds,
3889
+ beforeNormalize,
3890
+ afterNormalize: globalBounds
3891
+ });
3892
+ }
3893
+ }
3894
+ }
3895
+ debugLog("traceWithBounds:contours", {
3896
+ baseContourCount: baseContoursRaw.length,
3897
+ baseSelectedCount: baseContours.length,
3898
+ expandedContourCount: expandedContoursRaw.length,
3899
+ expandedSelectedCount: expandedContours.length,
3900
+ baseBounds,
3901
+ expandedBounds: globalBounds,
3471
3902
  expandedDeltaX: globalBounds.width - baseBounds.width,
3472
3903
  expandedDeltaY: globalBounds.height - baseBounds.height,
3473
3904
  expandedMayOverflowImageBounds: expand > 0,
@@ -3823,13 +4254,13 @@ var ImageTracer = class {
3823
4254
  (points) => this.scalePoints(points, targetWidth, targetHeight, bounds)
3824
4255
  );
3825
4256
  }
3826
- static clampPointsToImageBounds(points, width, height) {
3827
- const maxX = Math.max(0, width);
3828
- const maxY = Math.max(0, height);
3829
- return points.map((p) => ({
3830
- x: Math.max(0, Math.min(maxX, p.x)),
3831
- y: Math.max(0, Math.min(maxY, p.y))
3832
- }));
4257
+ static translateContours(contours, offsetX, offsetY) {
4258
+ return contours.map(
4259
+ (points) => points.map((p) => ({
4260
+ x: p.x + offsetX,
4261
+ y: p.y + offsetY
4262
+ }))
4263
+ );
3833
4264
  }
3834
4265
  static pointsToSVG(points) {
3835
4266
  if (points.length === 0) return "";
@@ -3882,6 +4313,7 @@ var ImageTracer = class {
3882
4313
 
3883
4314
  // src/extensions/dieline.ts
3884
4315
  var IMAGE_OBJECT_LAYER_ID2 = "image.user";
4316
+ var DIELINE_LAYER_ID = "dieline-overlay";
3885
4317
  var DielineTool = class {
3886
4318
  constructor(options) {
3887
4319
  this.id = "pooder.kit.dieline";
@@ -3889,8 +4321,8 @@ var DielineTool = class {
3889
4321
  name: "DielineTool"
3890
4322
  };
3891
4323
  this.state = {
3892
- displayUnit: "mm",
3893
- shape: "rect",
4324
+ shape: DEFAULT_DIELINE_SHAPE,
4325
+ shapeStyle: { ...DEFAULT_DIELINE_SHAPE_STYLE },
3894
4326
  width: 500,
3895
4327
  height: 500,
3896
4328
  radius: 0,
@@ -3913,6 +4345,8 @@ var DielineTool = class {
3913
4345
  showBleedLines: true,
3914
4346
  features: []
3915
4347
  };
4348
+ this.specs = [];
4349
+ this.renderSeq = 0;
3916
4350
  this.onCanvasResized = () => {
3917
4351
  this.updateDieline();
3918
4352
  };
@@ -3925,24 +4359,50 @@ var DielineTool = class {
3925
4359
  Object.assign(this.state.offsetLine, options.offsetLine);
3926
4360
  delete options.offsetLine;
3927
4361
  }
4362
+ if (options.shapeStyle) {
4363
+ this.state.shapeStyle = normalizeShapeStyle(
4364
+ options.shapeStyle,
4365
+ this.state.shapeStyle
4366
+ );
4367
+ delete options.shapeStyle;
4368
+ }
3928
4369
  Object.assign(this.state, options);
4370
+ this.state.shape = normalizeDielineShape(options.shape, this.state.shape);
3929
4371
  }
3930
4372
  }
3931
4373
  activate(context) {
4374
+ var _a;
3932
4375
  this.context = context;
3933
4376
  this.canvasService = context.services.get("CanvasService");
3934
4377
  if (!this.canvasService) {
3935
4378
  console.warn("CanvasService not found for DielineTool");
3936
4379
  return;
3937
4380
  }
4381
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
4382
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
4383
+ this.id,
4384
+ () => ({
4385
+ layerSpecs: {
4386
+ [DIELINE_LAYER_ID]: this.specs
4387
+ },
4388
+ replaceLayerIds: [DIELINE_LAYER_ID]
4389
+ }),
4390
+ { priority: 250 }
4391
+ );
3938
4392
  const configService = context.services.get(
3939
4393
  "ConfigurationService"
3940
4394
  );
3941
4395
  if (configService) {
3942
4396
  const s = this.state;
3943
4397
  const sizeState = readSizeState(configService);
3944
- s.displayUnit = sizeState.unit;
3945
- s.shape = configService.get("dieline.shape", s.shape);
4398
+ s.shape = normalizeDielineShape(
4399
+ configService.get("dieline.shape", s.shape),
4400
+ s.shape
4401
+ );
4402
+ s.shapeStyle = normalizeShapeStyle(
4403
+ configService.get("dieline.shapeStyle", s.shapeStyle),
4404
+ s.shapeStyle
4405
+ );
3946
4406
  s.width = sizeState.actualWidthMm;
3947
4407
  s.height = sizeState.actualHeightMm;
3948
4408
  s.radius = parseLengthToMm(
@@ -3991,10 +4451,17 @@ var DielineTool = class {
3991
4451
  );
3992
4452
  s.features = configService.get("dieline.features", s.features);
3993
4453
  s.pathData = configService.get("dieline.pathData", s.pathData);
4454
+ const sourceWidth = Number(
4455
+ configService.get("dieline.customSourceWidthPx", 0)
4456
+ );
4457
+ const sourceHeight = Number(
4458
+ configService.get("dieline.customSourceHeightPx", 0)
4459
+ );
4460
+ s.customSourceWidthPx = Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : void 0;
4461
+ s.customSourceHeightPx = Number.isFinite(sourceHeight) && sourceHeight > 0 ? sourceHeight : void 0;
3994
4462
  configService.onAnyChange((e) => {
3995
4463
  if (e.key.startsWith("size.")) {
3996
4464
  const nextSize = readSizeState(configService);
3997
- s.displayUnit = nextSize.unit;
3998
4465
  s.width = nextSize.actualWidthMm;
3999
4466
  s.height = nextSize.actualHeightMm;
4000
4467
  s.padding = nextSize.viewPadding;
@@ -4005,7 +4472,10 @@ var DielineTool = class {
4005
4472
  if (e.key.startsWith("dieline.")) {
4006
4473
  switch (e.key) {
4007
4474
  case "dieline.shape":
4008
- s.shape = e.value;
4475
+ s.shape = normalizeDielineShape(e.value, s.shape);
4476
+ break;
4477
+ case "dieline.shapeStyle":
4478
+ s.shapeStyle = normalizeShapeStyle(e.value, s.shapeStyle);
4009
4479
  break;
4010
4480
  case "dieline.radius":
4011
4481
  s.radius = parseLengthToMm(e.value, "mm");
@@ -4049,18 +4519,30 @@ var DielineTool = class {
4049
4519
  case "dieline.pathData":
4050
4520
  s.pathData = e.value;
4051
4521
  break;
4522
+ case "dieline.customSourceWidthPx":
4523
+ s.customSourceWidthPx = Number.isFinite(Number(e.value)) && Number(e.value) > 0 ? Number(e.value) : void 0;
4524
+ break;
4525
+ case "dieline.customSourceHeightPx":
4526
+ s.customSourceHeightPx = Number.isFinite(Number(e.value)) && Number(e.value) > 0 ? Number(e.value) : void 0;
4527
+ break;
4052
4528
  }
4053
4529
  this.updateDieline();
4054
4530
  }
4055
4531
  });
4056
4532
  }
4057
4533
  context.eventBus.on("canvas:resized", this.onCanvasResized);
4058
- this.createLayer();
4059
4534
  this.updateDieline();
4060
4535
  }
4061
4536
  deactivate(context) {
4537
+ var _a;
4062
4538
  context.eventBus.off("canvas:resized", this.onCanvasResized);
4063
- this.destroyLayer();
4539
+ this.renderSeq += 1;
4540
+ this.specs = [];
4541
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
4542
+ this.renderProducerDisposable = void 0;
4543
+ if (this.canvasService) {
4544
+ void this.canvasService.flushRenderFromProducers();
4545
+ }
4064
4546
  this.canvasService = void 0;
4065
4547
  this.context = void 0;
4066
4548
  }
@@ -4083,7 +4565,7 @@ var DielineTool = class {
4083
4565
  id: "dieline.shape",
4084
4566
  type: "select",
4085
4567
  label: "Shape",
4086
- options: ["rect", "circle", "ellipse", "custom"],
4568
+ options: Array.from(DIELINE_SHAPES),
4087
4569
  default: s.shape
4088
4570
  },
4089
4571
  {
@@ -4094,6 +4576,12 @@ var DielineTool = class {
4094
4576
  max: 500,
4095
4577
  default: s.radius
4096
4578
  },
4579
+ {
4580
+ id: "dieline.shapeStyle",
4581
+ type: "json",
4582
+ label: "Shape Style",
4583
+ default: s.shapeStyle
4584
+ },
4097
4585
  {
4098
4586
  id: "dieline.showBleedLines",
4099
4587
  type: "boolean",
@@ -4269,34 +4757,6 @@ var DielineTool = class {
4269
4757
  ]
4270
4758
  };
4271
4759
  }
4272
- getLayer() {
4273
- var _a;
4274
- return (_a = this.canvasService) == null ? void 0 : _a.getLayer("dieline-overlay");
4275
- }
4276
- createLayer() {
4277
- if (!this.canvasService) return;
4278
- const width = this.canvasService.canvas.width || 800;
4279
- const height = this.canvasService.canvas.height || 600;
4280
- const layer = this.canvasService.createLayer("dieline-overlay", {
4281
- width,
4282
- height,
4283
- selectable: false,
4284
- evented: false
4285
- });
4286
- this.canvasService.canvas.bringObjectToFront(layer);
4287
- const userLayer = this.canvasService.getLayer("user");
4288
- if (userLayer) {
4289
- const userIndex = this.canvasService.canvas.getObjects().indexOf(userLayer);
4290
- this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
4291
- }
4292
- }
4293
- destroyLayer() {
4294
- if (!this.canvasService) return;
4295
- const layer = this.getLayer();
4296
- if (layer) {
4297
- this.canvasService.canvas.remove(layer);
4298
- }
4299
- }
4300
4760
  createHatchPattern(color = "rgba(0, 0, 0, 0.3)") {
4301
4761
  if (typeof document === "undefined") {
4302
4762
  return void 0;
@@ -4325,7 +4785,6 @@ var DielineTool = class {
4325
4785
  }
4326
4786
  syncSizeState(configService) {
4327
4787
  const sizeState = readSizeState(configService);
4328
- this.state.displayUnit = sizeState.unit;
4329
4788
  this.state.width = sizeState.actualWidthMm;
4330
4789
  this.state.height = sizeState.actualHeightMm;
4331
4790
  this.state.padding = sizeState.viewPadding;
@@ -4339,20 +4798,26 @@ var DielineTool = class {
4339
4798
  return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.type) === "feature-marker";
4340
4799
  }).forEach((obj) => canvas.bringObjectToFront(obj));
4341
4800
  }
4342
- updateDieline(_emitEvent = true) {
4801
+ ensureLayerStacking() {
4343
4802
  if (!this.canvasService) return;
4344
- const layer = this.getLayer();
4803
+ const layer = this.canvasService.getLayer(DIELINE_LAYER_ID);
4345
4804
  if (!layer) return;
4346
- const configService = this.getConfigService();
4347
- if (!configService) return;
4348
- this.syncSizeState(configService);
4349
- const sceneLayout = computeSceneLayout(
4350
- this.canvasService,
4351
- readSizeState(configService)
4352
- );
4353
- if (!sceneLayout) return;
4805
+ const userLayer = this.canvasService.getLayer("user");
4806
+ if (userLayer) {
4807
+ const layerIndex = this.canvasService.canvas.getObjects().indexOf(layer);
4808
+ const userIndex = this.canvasService.canvas.getObjects().indexOf(userLayer);
4809
+ if (layerIndex < userIndex) {
4810
+ this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
4811
+ }
4812
+ return;
4813
+ }
4814
+ this.canvasService.canvas.bringObjectToFront(layer);
4815
+ }
4816
+ buildDielineSpecs(sceneLayout) {
4817
+ var _a, _b;
4354
4818
  const {
4355
4819
  shape,
4820
+ shapeStyle,
4356
4821
  radius,
4357
4822
  mainLine,
4358
4823
  offsetLine,
@@ -4361,8 +4826,8 @@ var DielineTool = class {
4361
4826
  showBleedLines,
4362
4827
  features
4363
4828
  } = this.state;
4364
- const canvasW = sceneLayout.canvasWidth || this.canvasService.canvas.width || 800;
4365
- const canvasH = sceneLayout.canvasHeight || this.canvasService.canvas.height || 600;
4829
+ const canvasW = sceneLayout.canvasWidth || ((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 800;
4830
+ const canvasH = sceneLayout.canvasHeight || ((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 600;
4366
4831
  const scale = sceneLayout.scale;
4367
4832
  const cx = sceneLayout.trimRect.centerX;
4368
4833
  const cy = sceneLayout.trimRect.centerY;
@@ -4373,7 +4838,6 @@ var DielineTool = class {
4373
4838
  const cutH = sceneLayout.cutRect.height;
4374
4839
  const visualOffset = (cutW - visualWidth) / 2;
4375
4840
  const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
4376
- layer.remove(...layer.getObjects());
4377
4841
  const absoluteFeatures = (features || []).map((f) => ({
4378
4842
  ...f,
4379
4843
  x: f.x,
@@ -4393,19 +4857,30 @@ var DielineTool = class {
4393
4857
  x: cx,
4394
4858
  y: cy,
4395
4859
  features: cutFeatures,
4396
- pathData: this.state.pathData
4397
- });
4398
- const mask = new Path(maskPathData, {
4399
- fill: outsideColor,
4400
- stroke: null,
4401
- selectable: false,
4402
- evented: false,
4403
- originX: "left",
4404
- originY: "top",
4405
- left: 0,
4406
- top: 0
4860
+ shapeStyle,
4861
+ pathData: this.state.pathData,
4862
+ customSourceWidthPx: this.state.customSourceWidthPx,
4863
+ customSourceHeightPx: this.state.customSourceHeightPx
4407
4864
  });
4408
- layer.add(mask);
4865
+ const specs = [
4866
+ {
4867
+ id: "dieline.mask",
4868
+ type: "path",
4869
+ space: "screen",
4870
+ data: { id: "dieline.mask", type: "dieline" },
4871
+ props: {
4872
+ pathData: maskPathData,
4873
+ fill: outsideColor,
4874
+ stroke: null,
4875
+ selectable: false,
4876
+ evented: false,
4877
+ originX: "left",
4878
+ originY: "top",
4879
+ left: 0,
4880
+ top: 0
4881
+ }
4882
+ }
4883
+ ];
4409
4884
  if (insideColor && insideColor !== "transparent" && insideColor !== "rgba(0,0,0,0)") {
4410
4885
  const productPathData = generateDielinePath({
4411
4886
  shape,
@@ -4415,19 +4890,28 @@ var DielineTool = class {
4415
4890
  x: cx,
4416
4891
  y: cy,
4417
4892
  features: cutFeatures,
4893
+ shapeStyle,
4418
4894
  pathData: this.state.pathData,
4895
+ customSourceWidthPx: this.state.customSourceWidthPx,
4896
+ customSourceHeightPx: this.state.customSourceHeightPx,
4419
4897
  canvasWidth: canvasW,
4420
4898
  canvasHeight: canvasH
4421
4899
  });
4422
- const insideObj = new Path(productPathData, {
4423
- fill: insideColor,
4424
- stroke: null,
4425
- selectable: false,
4426
- evented: false,
4427
- originX: "left",
4428
- originY: "top"
4900
+ specs.push({
4901
+ id: "dieline.inside",
4902
+ type: "path",
4903
+ space: "screen",
4904
+ data: { id: "dieline.inside", type: "dieline" },
4905
+ props: {
4906
+ pathData: productPathData,
4907
+ fill: insideColor,
4908
+ stroke: null,
4909
+ selectable: false,
4910
+ evented: false,
4911
+ originX: "left",
4912
+ originY: "top"
4913
+ }
4429
4914
  });
4430
- layer.add(insideObj);
4431
4915
  }
4432
4916
  if (Math.abs(visualOffset) > 1e-4) {
4433
4917
  const bleedPathData = generateBleedZonePath(
@@ -4439,7 +4923,10 @@ var DielineTool = class {
4439
4923
  x: cx,
4440
4924
  y: cy,
4441
4925
  features: cutFeatures,
4926
+ shapeStyle,
4442
4927
  pathData: this.state.pathData,
4928
+ customSourceWidthPx: this.state.customSourceWidthPx,
4929
+ customSourceHeightPx: this.state.customSourceHeightPx,
4443
4930
  canvasWidth: canvasW,
4444
4931
  canvasHeight: canvasH
4445
4932
  },
@@ -4451,7 +4938,10 @@ var DielineTool = class {
4451
4938
  x: cx,
4452
4939
  y: cy,
4453
4940
  features: cutFeatures,
4941
+ shapeStyle,
4454
4942
  pathData: this.state.pathData,
4943
+ customSourceWidthPx: this.state.customSourceWidthPx,
4944
+ customSourceHeightPx: this.state.customSourceHeightPx,
4455
4945
  canvasWidth: canvasW,
4456
4946
  canvasHeight: canvasH
4457
4947
  },
@@ -4460,16 +4950,22 @@ var DielineTool = class {
4460
4950
  if (showBleedLines !== false) {
4461
4951
  const pattern = this.createHatchPattern(mainLine.color);
4462
4952
  if (pattern) {
4463
- const bleedObj = new Path(bleedPathData, {
4464
- fill: pattern,
4465
- stroke: null,
4466
- selectable: false,
4467
- evented: false,
4468
- objectCaching: false,
4469
- originX: "left",
4470
- originY: "top"
4953
+ specs.push({
4954
+ id: "dieline.bleed-zone",
4955
+ type: "path",
4956
+ space: "screen",
4957
+ data: { id: "dieline.bleed-zone", type: "dieline" },
4958
+ props: {
4959
+ pathData: bleedPathData,
4960
+ fill: pattern,
4961
+ stroke: null,
4962
+ selectable: false,
4963
+ evented: false,
4964
+ objectCaching: false,
4965
+ originX: "left",
4966
+ originY: "top"
4967
+ }
4471
4968
  });
4472
- layer.add(bleedObj);
4473
4969
  }
4474
4970
  }
4475
4971
  const offsetPathData = generateDielinePath({
@@ -4480,21 +4976,30 @@ var DielineTool = class {
4480
4976
  x: cx,
4481
4977
  y: cy,
4482
4978
  features: cutFeatures,
4979
+ shapeStyle,
4483
4980
  pathData: this.state.pathData,
4981
+ customSourceWidthPx: this.state.customSourceWidthPx,
4982
+ customSourceHeightPx: this.state.customSourceHeightPx,
4484
4983
  canvasWidth: canvasW,
4485
4984
  canvasHeight: canvasH
4486
4985
  });
4487
- const offsetBorderObj = new Path(offsetPathData, {
4488
- fill: null,
4489
- stroke: offsetLine.style === "hidden" ? null : offsetLine.color,
4490
- strokeWidth: offsetLine.width,
4491
- strokeDashArray: offsetLine.style === "dashed" ? [offsetLine.dashLength, offsetLine.dashLength] : void 0,
4492
- selectable: false,
4493
- evented: false,
4494
- originX: "left",
4495
- originY: "top"
4986
+ specs.push({
4987
+ id: "dieline.offset-border",
4988
+ type: "path",
4989
+ space: "screen",
4990
+ data: { id: "dieline.offset-border", type: "dieline" },
4991
+ props: {
4992
+ pathData: offsetPathData,
4993
+ fill: null,
4994
+ stroke: offsetLine.style === "hidden" ? null : offsetLine.color,
4995
+ strokeWidth: offsetLine.width,
4996
+ strokeDashArray: offsetLine.style === "dashed" ? [offsetLine.dashLength, offsetLine.dashLength] : void 0,
4997
+ selectable: false,
4998
+ evented: false,
4999
+ originX: "left",
5000
+ originY: "top"
5001
+ }
4496
5002
  });
4497
- layer.add(offsetBorderObj);
4498
5003
  }
4499
5004
  const borderPathData = generateDielinePath({
4500
5005
  shape,
@@ -4504,37 +5009,59 @@ var DielineTool = class {
4504
5009
  x: cx,
4505
5010
  y: cy,
4506
5011
  features: absoluteFeatures,
5012
+ shapeStyle,
4507
5013
  pathData: this.state.pathData,
5014
+ customSourceWidthPx: this.state.customSourceWidthPx,
5015
+ customSourceHeightPx: this.state.customSourceHeightPx,
4508
5016
  canvasWidth: canvasW,
4509
5017
  canvasHeight: canvasH
4510
5018
  });
4511
- const borderObj = new Path(borderPathData, {
4512
- fill: "transparent",
4513
- stroke: mainLine.style === "hidden" ? null : mainLine.color,
4514
- strokeWidth: mainLine.width,
4515
- strokeDashArray: mainLine.style === "dashed" ? [mainLine.dashLength, mainLine.dashLength] : void 0,
4516
- selectable: false,
4517
- evented: false,
4518
- originX: "left",
4519
- originY: "top"
4520
- });
4521
- layer.add(borderObj);
4522
- const userLayer = this.canvasService.getLayer("user");
4523
- if (layer && userLayer) {
4524
- const layerIndex = this.canvasService.canvas.getObjects().indexOf(layer);
4525
- const userIndex = this.canvasService.canvas.getObjects().indexOf(userLayer);
4526
- if (layerIndex < userIndex) {
4527
- this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
5019
+ specs.push({
5020
+ id: "dieline.border",
5021
+ type: "path",
5022
+ space: "screen",
5023
+ data: { id: "dieline.border", type: "dieline" },
5024
+ props: {
5025
+ pathData: borderPathData,
5026
+ fill: "transparent",
5027
+ stroke: mainLine.style === "hidden" ? null : mainLine.color,
5028
+ strokeWidth: mainLine.width,
5029
+ strokeDashArray: mainLine.style === "dashed" ? [mainLine.dashLength, mainLine.dashLength] : void 0,
5030
+ selectable: false,
5031
+ evented: false,
5032
+ originX: "left",
5033
+ originY: "top"
4528
5034
  }
4529
- } else {
4530
- this.canvasService.canvas.bringObjectToFront(layer);
5035
+ });
5036
+ return specs;
5037
+ }
5038
+ updateDieline(_emitEvent = true) {
5039
+ void this.updateDielineAsync();
5040
+ }
5041
+ async updateDielineAsync() {
5042
+ if (!this.canvasService) return;
5043
+ const configService = this.getConfigService();
5044
+ if (!configService) return;
5045
+ const seq = ++this.renderSeq;
5046
+ this.syncSizeState(configService);
5047
+ const sceneLayout = computeSceneLayout(
5048
+ this.canvasService,
5049
+ readSizeState(configService)
5050
+ );
5051
+ if (!sceneLayout) {
5052
+ if (seq !== this.renderSeq) return;
5053
+ this.specs = [];
5054
+ await this.canvasService.flushRenderFromProducers();
5055
+ return;
4531
5056
  }
5057
+ const nextSpecs = this.buildDielineSpecs(sceneLayout);
5058
+ if (seq !== this.renderSeq) return;
5059
+ this.specs = nextSpecs;
5060
+ await this.canvasService.flushRenderFromProducers();
5061
+ if (seq !== this.renderSeq) return;
5062
+ this.ensureLayerStacking();
4532
5063
  this.bringFeatureMarkersToFront();
4533
- const rulerLayer = this.canvasService.getLayer("ruler-overlay");
4534
- if (rulerLayer) {
4535
- this.canvasService.canvas.bringObjectToFront(rulerLayer);
4536
- }
4537
- layer.dirty = true;
5064
+ this.canvasService.bringLayerToFront("ruler-overlay");
4538
5065
  this.canvasService.requestRenderAll();
4539
5066
  }
4540
5067
  getGeometry() {
@@ -4550,7 +5077,9 @@ var DielineTool = class {
4550
5077
  return {
4551
5078
  ...sceneGeometry,
4552
5079
  strokeWidth: this.state.mainLine.width,
4553
- pathData: this.state.pathData
5080
+ pathData: this.state.pathData,
5081
+ customSourceWidthPx: this.state.customSourceWidthPx,
5082
+ customSourceHeightPx: this.state.customSourceHeightPx
4554
5083
  };
4555
5084
  }
4556
5085
  async exportCutImage(options) {
@@ -4580,7 +5109,7 @@ var DielineTool = class {
4580
5109
  );
4581
5110
  return null;
4582
5111
  }
4583
- const { shape, radius, features, pathData } = this.state;
5112
+ const { shape, shapeStyle, radius, features, pathData } = this.state;
4584
5113
  const canvasW = sceneLayout.canvasWidth || this.canvasService.canvas.width || 800;
4585
5114
  const canvasH = sceneLayout.canvasHeight || this.canvasService.canvas.height || 600;
4586
5115
  const scale = sceneLayout.scale;
@@ -4608,7 +5137,10 @@ var DielineTool = class {
4608
5137
  x: cx,
4609
5138
  y: cy,
4610
5139
  features: cutFeatures,
5140
+ shapeStyle,
4611
5141
  pathData,
5142
+ customSourceWidthPx: this.state.customSourceWidthPx,
5143
+ customSourceHeightPx: this.state.customSourceHeightPx,
4612
5144
  canvasWidth: canvasW,
4613
5145
  canvasHeight: canvasH
4614
5146
  });
@@ -4720,7 +5252,6 @@ var DielineTool = class {
4720
5252
  import {
4721
5253
  ContributionPointIds as ContributionPointIds5
4722
5254
  } from "@pooder/core";
4723
- import { Circle, Group, Point as Point2, Rect as Rect2 } from "fabric";
4724
5255
 
4725
5256
  // src/extensions/constraints.ts
4726
5257
  var ConstraintRegistry = class {
@@ -4920,6 +5451,10 @@ function completeFeaturesStrict(features, context, update) {
4920
5451
  }
4921
5452
 
4922
5453
  // src/extensions/feature.ts
5454
+ var FEATURE_OVERLAY_LAYER_ID = "feature-overlay";
5455
+ var FEATURE_STROKE_WIDTH = 2;
5456
+ var DEFAULT_RECT_SIZE = 10;
5457
+ var DEFAULT_CIRCLE_RADIUS = 5;
4923
5458
  var FeatureTool = class {
4924
5459
  constructor(options) {
4925
5460
  this.id = "pooder.kit.feature";
@@ -4932,6 +5467,8 @@ var FeatureTool = class {
4932
5467
  this.isFeatureSessionActive = false;
4933
5468
  this.sessionOriginalFeatures = null;
4934
5469
  this.hasWorkingChanges = false;
5470
+ this.specs = [];
5471
+ this.renderSeq = 0;
4935
5472
  this.handleMoving = null;
4936
5473
  this.handleModified = null;
4937
5474
  this.handleSceneGeometryChange = null;
@@ -4948,12 +5485,23 @@ var FeatureTool = class {
4948
5485
  }
4949
5486
  }
4950
5487
  activate(context) {
5488
+ var _a;
4951
5489
  this.context = context;
4952
5490
  this.canvasService = context.services.get("CanvasService");
4953
5491
  if (!this.canvasService) {
4954
5492
  console.warn("CanvasService not found for FeatureTool");
4955
5493
  return;
4956
5494
  }
5495
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
5496
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
5497
+ this.id,
5498
+ () => ({
5499
+ rootLayerSpecs: {
5500
+ [FEATURE_OVERLAY_LAYER_ID]: this.specs
5501
+ }
5502
+ }),
5503
+ { priority: 350 }
5504
+ );
4957
5505
  const configService = context.services.get(
4958
5506
  "ConfigurationService"
4959
5507
  );
@@ -4992,21 +5540,7 @@ var FeatureTool = class {
4992
5540
  this.context = void 0;
4993
5541
  }
4994
5542
  updateVisibility() {
4995
- if (!this.canvasService) return;
4996
- const canvas = this.canvasService.canvas;
4997
- const markers = canvas.getObjects().filter((obj) => {
4998
- var _a;
4999
- return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
5000
- });
5001
- markers.forEach((marker) => {
5002
- marker.set({
5003
- visible: this.isToolActive,
5004
- // Or just selectable: false if we want them visible but locked
5005
- selectable: this.isToolActive,
5006
- evented: this.isToolActive
5007
- });
5008
- });
5009
- canvas.requestRenderAll();
5543
+ this.redraw();
5010
5544
  }
5011
5545
  contribute() {
5012
5546
  return {
@@ -5238,8 +5772,7 @@ var FeatureTool = class {
5238
5772
  if (!changed) return { ok: true };
5239
5773
  this.setWorkingFeatures(next);
5240
5774
  this.hasWorkingChanges = true;
5241
- this.redraw();
5242
- this.enforceConstraints();
5775
+ this.redraw({ enforceConstraints: true });
5243
5776
  this.emitWorkingChange();
5244
5777
  return { ok: true };
5245
5778
  }
@@ -5287,12 +5820,10 @@ var FeatureTool = class {
5287
5820
  shape: "rect",
5288
5821
  x: 0.5,
5289
5822
  y: 0,
5290
- // Top edge
5291
5823
  width: 10,
5292
5824
  height: 10,
5293
5825
  rotation: 0,
5294
5826
  renderBehavior: "edge",
5295
- // Default constraint: path (snap to edge)
5296
5827
  constraints: [{ type: "path" }]
5297
5828
  };
5298
5829
  this.setWorkingFeatures([...this.workingFeatures || [], newFeature]);
@@ -5335,7 +5866,7 @@ var FeatureTool = class {
5335
5866
  this.emitWorkingChange();
5336
5867
  return true;
5337
5868
  }
5338
- getGeometryForFeature(geometry, feature) {
5869
+ getGeometryForFeature(geometry, _feature) {
5339
5870
  return geometry;
5340
5871
  }
5341
5872
  setup() {
@@ -5344,8 +5875,7 @@ var FeatureTool = class {
5344
5875
  if (!this.handleSceneGeometryChange) {
5345
5876
  this.handleSceneGeometryChange = (geometry) => {
5346
5877
  this.currentGeometry = geometry;
5347
- this.redraw();
5348
- this.enforceConstraints();
5878
+ this.redraw({ enforceConstraints: true });
5349
5879
  };
5350
5880
  this.context.eventBus.on(
5351
5881
  "scene:geometry:change",
@@ -5368,76 +5898,34 @@ var FeatureTool = class {
5368
5898
  }
5369
5899
  if (!this.handleMoving) {
5370
5900
  this.handleMoving = (e) => {
5371
- var _a, _b, _c, _d;
5372
- const target = e.target;
5373
- if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return;
5374
- if (!this.currentGeometry) return;
5375
- let feature;
5376
- if ((_b = target.data) == null ? void 0 : _b.isGroup) {
5377
- const indices = (_c = target.data) == null ? void 0 : _c.indices;
5378
- if (indices && indices.length > 0) {
5379
- feature = this.workingFeatures[indices[0]];
5380
- }
5381
- } else {
5382
- const index = (_d = target.data) == null ? void 0 : _d.index;
5383
- if (index !== void 0) {
5384
- feature = this.workingFeatures[index];
5385
- }
5386
- }
5387
- const geometry = this.getGeometryForFeature(
5388
- this.currentGeometry,
5901
+ const target = this.getDraggableMarkerTarget(e == null ? void 0 : e.target);
5902
+ if (!target || !this.currentGeometry) return;
5903
+ const feature = this.getFeatureForMarker(target);
5904
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
5905
+ const snapped = this.constrainPosition(
5906
+ {
5907
+ x: Number(target.left || 0),
5908
+ y: Number(target.top || 0)
5909
+ },
5910
+ geometry,
5389
5911
  feature
5390
5912
  );
5391
- const p = new Point2(target.left, target.top);
5392
- const markerStrokeWidth = (target.strokeWidth || 2) * (target.scaleX || 1);
5393
- const minDim = Math.min(
5394
- target.getScaledWidth(),
5395
- target.getScaledHeight()
5396
- );
5397
- const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
5398
- const snapped = this.constrainPosition(p, geometry, limit, feature);
5399
5913
  target.set({
5400
5914
  left: snapped.x,
5401
5915
  top: snapped.y
5402
5916
  });
5917
+ target.setCoords();
5918
+ this.syncMarkerVisualsByTarget(target, snapped);
5403
5919
  };
5404
5920
  canvas.on("object:moving", this.handleMoving);
5405
5921
  }
5406
5922
  if (!this.handleModified) {
5407
5923
  this.handleModified = (e) => {
5408
- var _a, _b, _c;
5409
- const target = e.target;
5410
- if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return;
5411
- if ((_b = target.data) == null ? void 0 : _b.isGroup) {
5412
- const groupObj = target;
5413
- const indices = (_c = groupObj.data) == null ? void 0 : _c.indices;
5414
- if (!indices) return;
5415
- const groupCenter = new Point2(groupObj.left, groupObj.top);
5416
- const newFeatures = [...this.workingFeatures];
5417
- const { x, y } = this.currentGeometry;
5418
- groupObj.getObjects().forEach((child, i) => {
5419
- const originalIndex = indices[i];
5420
- const feature = this.workingFeatures[originalIndex];
5421
- const geometry = this.getGeometryForFeature(
5422
- this.currentGeometry,
5423
- feature
5424
- );
5425
- const { width, height } = geometry;
5426
- const layoutLeft = x - width / 2;
5427
- const layoutTop = y - height / 2;
5428
- const absX = groupCenter.x + (child.left || 0);
5429
- const absY = groupCenter.y + (child.top || 0);
5430
- const normalizedX = width > 0 ? (absX - layoutLeft) / width : 0.5;
5431
- const normalizedY = height > 0 ? (absY - layoutTop) / height : 0.5;
5432
- newFeatures[originalIndex] = {
5433
- ...newFeatures[originalIndex],
5434
- x: normalizedX,
5435
- y: normalizedY
5436
- };
5437
- });
5438
- this.setWorkingFeatures(newFeatures);
5439
- this.hasWorkingChanges = true;
5440
- this.emitWorkingChange();
5924
+ var _a;
5925
+ const target = this.getDraggableMarkerTarget(e == null ? void 0 : e.target);
5926
+ if (!target) return;
5927
+ if ((_a = target.data) == null ? void 0 : _a.isGroup) {
5928
+ this.syncGroupFromCanvas(target);
5441
5929
  } else {
5442
5930
  this.syncFeatureFromCanvas(target);
5443
5931
  }
@@ -5446,6 +5934,7 @@ var FeatureTool = class {
5446
5934
  }
5447
5935
  }
5448
5936
  teardown() {
5937
+ var _a;
5449
5938
  if (!this.canvasService) return;
5450
5939
  const canvas = this.canvasService.canvas;
5451
5940
  if (this.handleMoving) {
@@ -5463,14 +5952,25 @@ var FeatureTool = class {
5463
5952
  );
5464
5953
  this.handleSceneGeometryChange = null;
5465
5954
  }
5466
- const objects = canvas.getObjects().filter((obj) => {
5467
- var _a;
5468
- return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
5469
- });
5470
- objects.forEach((obj) => canvas.remove(obj));
5471
- this.canvasService.requestRenderAll();
5955
+ this.renderSeq += 1;
5956
+ this.specs = [];
5957
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
5958
+ this.renderProducerDisposable = void 0;
5959
+ void this.canvasService.flushRenderFromProducers();
5960
+ }
5961
+ getDraggableMarkerTarget(target) {
5962
+ var _a, _b;
5963
+ if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return null;
5964
+ if (((_b = target.data) == null ? void 0 : _b.markerRole) !== "handle") return null;
5965
+ return target;
5966
+ }
5967
+ getFeatureForMarker(target) {
5968
+ const data = (target == null ? void 0 : target.data) || {};
5969
+ const index = data.isGroup ? this.toFeatureIndex(data.anchorIndex) : this.toFeatureIndex(data.index);
5970
+ if (index === null) return void 0;
5971
+ return this.workingFeatures[index];
5472
5972
  }
5473
- constrainPosition(p, geometry, limit, feature) {
5973
+ constrainPosition(p, geometry, feature) {
5474
5974
  var _a;
5475
5975
  if (!feature) {
5476
5976
  return { x: p.x, y: p.y };
@@ -5501,225 +6001,364 @@ var FeatureTool = class {
5501
6001
  y: minY + constrained.y * geometry.height
5502
6002
  };
5503
6003
  }
6004
+ toNormalizedPoint(point, geometry) {
6005
+ const left = geometry.x - geometry.width / 2;
6006
+ const top = geometry.y - geometry.height / 2;
6007
+ return {
6008
+ x: geometry.width > 0 ? (point.x - left) / geometry.width : 0.5,
6009
+ y: geometry.height > 0 ? (point.y - top) / geometry.height : 0.5
6010
+ };
6011
+ }
5504
6012
  syncFeatureFromCanvas(target) {
5505
6013
  var _a;
5506
- if (!this.currentGeometry || !this.context) return;
5507
- const index = (_a = target.data) == null ? void 0 : _a.index;
5508
- if (index === void 0 || index < 0 || index >= this.workingFeatures.length)
5509
- return;
6014
+ if (!this.currentGeometry) return;
6015
+ const index = this.toFeatureIndex((_a = target.data) == null ? void 0 : _a.index);
6016
+ if (index === null || index >= this.workingFeatures.length) return;
5510
6017
  const feature = this.workingFeatures[index];
5511
6018
  const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
5512
- const { width, height, x, y } = geometry;
5513
- const left = x - width / 2;
5514
- const top = y - height / 2;
5515
- const normalizedX = width > 0 ? (target.left - left) / width : 0.5;
5516
- const normalizedY = height > 0 ? (target.top - top) / height : 0.5;
6019
+ const normalized = this.toNormalizedPoint(
6020
+ {
6021
+ x: Number(target.left || 0),
6022
+ y: Number(target.top || 0)
6023
+ },
6024
+ geometry
6025
+ );
5517
6026
  const updatedFeature = {
5518
6027
  ...feature,
5519
- x: normalizedX,
5520
- y: normalizedY
5521
- // Could also update rotation if we allowed rotating markers
6028
+ x: normalized.x,
6029
+ y: normalized.y
5522
6030
  };
5523
- const newFeatures = [...this.workingFeatures];
5524
- newFeatures[index] = updatedFeature;
5525
- this.setWorkingFeatures(newFeatures);
6031
+ const next = [...this.workingFeatures];
6032
+ next[index] = updatedFeature;
6033
+ this.setWorkingFeatures(next);
5526
6034
  this.hasWorkingChanges = true;
5527
6035
  this.emitWorkingChange();
5528
6036
  }
5529
- redraw() {
5530
- if (!this.canvasService || !this.currentGeometry) return;
5531
- const canvas = this.canvasService.canvas;
5532
- const geometry = this.currentGeometry;
5533
- const existing = canvas.getObjects().filter((obj) => {
5534
- var _a;
5535
- return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
6037
+ syncGroupFromCanvas(target) {
6038
+ var _a, _b;
6039
+ if (!this.currentGeometry) return;
6040
+ const indices = this.readGroupIndices((_a = target.data) == null ? void 0 : _a.indices);
6041
+ if (indices.length === 0) return;
6042
+ const offsets = this.readGroupMemberOffsets((_b = target.data) == null ? void 0 : _b.memberOffsets, indices);
6043
+ const anchorCenter = {
6044
+ x: Number(target.left || 0),
6045
+ y: Number(target.top || 0)
6046
+ };
6047
+ const next = [...this.workingFeatures];
6048
+ let changed = false;
6049
+ offsets.forEach((entry) => {
6050
+ const index = entry.index;
6051
+ if (index < 0 || index >= next.length) return;
6052
+ const feature = next[index];
6053
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
6054
+ const normalized = this.toNormalizedPoint(
6055
+ {
6056
+ x: anchorCenter.x + entry.dx,
6057
+ y: anchorCenter.y + entry.dy
6058
+ },
6059
+ geometry
6060
+ );
6061
+ if (feature.x !== normalized.x || feature.y !== normalized.y) {
6062
+ next[index] = {
6063
+ ...feature,
6064
+ x: normalized.x,
6065
+ y: normalized.y
6066
+ };
6067
+ changed = true;
6068
+ }
5536
6069
  });
5537
- existing.forEach((obj) => canvas.remove(obj));
5538
- if (!this.workingFeatures || this.workingFeatures.length === 0) {
5539
- this.canvasService.requestRenderAll();
5540
- return;
6070
+ if (!changed) return;
6071
+ this.setWorkingFeatures(next);
6072
+ this.hasWorkingChanges = true;
6073
+ this.emitWorkingChange();
6074
+ }
6075
+ redraw(options = {}) {
6076
+ void this.redrawAsync(options);
6077
+ }
6078
+ async redrawAsync(options = {}) {
6079
+ if (!this.canvasService) return;
6080
+ const seq = ++this.renderSeq;
6081
+ this.specs = this.buildFeatureSpecs();
6082
+ if (seq !== this.renderSeq) return;
6083
+ await this.canvasService.flushRenderFromProducers();
6084
+ if (seq !== this.renderSeq) return;
6085
+ this.syncOverlayOrder();
6086
+ if (options.enforceConstraints) {
6087
+ this.enforceConstraints();
5541
6088
  }
5542
- const scale = geometry.scale || 1;
5543
- const finalScale = scale;
5544
- const groups = {};
6089
+ }
6090
+ syncOverlayOrder() {
6091
+ if (!this.canvasService) return;
6092
+ this.canvasService.bringLayerToFront(FEATURE_OVERLAY_LAYER_ID);
6093
+ this.canvasService.bringLayerToFront("ruler-overlay");
6094
+ }
6095
+ buildFeatureSpecs() {
6096
+ if (!this.currentGeometry || this.workingFeatures.length === 0) {
6097
+ return [];
6098
+ }
6099
+ const groups = /* @__PURE__ */ new Map();
5545
6100
  const singles = [];
5546
- this.workingFeatures.forEach((f, i) => {
5547
- if (f.groupId) {
5548
- if (!groups[f.groupId]) groups[f.groupId] = [];
5549
- groups[f.groupId].push({ feature: f, index: i });
5550
- } else {
5551
- singles.push({ feature: f, index: i });
6101
+ this.workingFeatures.forEach((feature, index) => {
6102
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
6103
+ const position = resolveFeaturePosition(feature, geometry);
6104
+ const scale = geometry.scale || 1;
6105
+ const marker = {
6106
+ feature,
6107
+ index,
6108
+ position,
6109
+ geometry,
6110
+ scale
6111
+ };
6112
+ if (feature.groupId) {
6113
+ const list = groups.get(feature.groupId) || [];
6114
+ list.push(marker);
6115
+ groups.set(feature.groupId, list);
6116
+ return;
5552
6117
  }
6118
+ singles.push(marker);
6119
+ });
6120
+ const specs = [];
6121
+ singles.forEach((marker) => {
6122
+ this.appendMarkerSpecs(specs, marker, {
6123
+ markerRole: "handle",
6124
+ isGroup: false
6125
+ });
6126
+ });
6127
+ groups.forEach((members, groupId) => {
6128
+ if (!members.length) return;
6129
+ const anchor = members[0];
6130
+ const memberOffsets = members.map((member) => ({
6131
+ index: member.index,
6132
+ dx: member.position.x - anchor.position.x,
6133
+ dy: member.position.y - anchor.position.y
6134
+ }));
6135
+ const indices = members.map((member) => member.index);
6136
+ members.filter((member) => member.index !== anchor.index).forEach((member) => {
6137
+ this.appendMarkerSpecs(specs, member, {
6138
+ markerRole: "member",
6139
+ isGroup: false,
6140
+ groupId
6141
+ });
6142
+ });
6143
+ this.appendMarkerSpecs(specs, anchor, {
6144
+ markerRole: "handle",
6145
+ isGroup: true,
6146
+ groupId,
6147
+ indices,
6148
+ anchorIndex: anchor.index,
6149
+ memberOffsets
6150
+ });
5553
6151
  });
5554
- const createMarkerShape = (feature, pos) => {
5555
- const featureScale = scale;
5556
- const visualWidth = (feature.width || 10) * featureScale;
5557
- const visualHeight = (feature.height || 10) * featureScale;
5558
- const visualRadius = (feature.radius || 0) * featureScale;
5559
- const color = feature.color || (feature.operation === "add" ? "#00FF00" : "#FF0000");
5560
- const strokeDash = feature.strokeDash || (feature.operation === "subtract" ? [4, 4] : void 0);
5561
- let shape;
5562
- if (feature.shape === "rect") {
5563
- shape = new Rect2({
6152
+ return specs;
6153
+ }
6154
+ appendMarkerSpecs(specs, marker, options) {
6155
+ var _a, _b, _c, _d, _e;
6156
+ const { feature, index, position, scale, geometry } = marker;
6157
+ const baseRadius = feature.shape === "circle" ? (_a = feature.radius) != null ? _a : DEFAULT_CIRCLE_RADIUS : (_b = feature.radius) != null ? _b : 0;
6158
+ const baseWidth = feature.shape === "circle" ? baseRadius * 2 : (_c = feature.width) != null ? _c : DEFAULT_RECT_SIZE;
6159
+ const baseHeight = feature.shape === "circle" ? baseRadius * 2 : (_d = feature.height) != null ? _d : DEFAULT_RECT_SIZE;
6160
+ const visualWidth = baseWidth * scale;
6161
+ const visualHeight = baseHeight * scale;
6162
+ const visualRadius = baseRadius * scale;
6163
+ const color = feature.color || (feature.operation === "add" ? "#00FF00" : "#FF0000");
6164
+ const strokeDash = feature.strokeDash || (feature.operation === "subtract" ? [4, 4] : void 0);
6165
+ const interactive = options.markerRole === "handle";
6166
+ const baseData = this.buildMarkerData(marker, options);
6167
+ const commonProps = {
6168
+ visible: this.isToolActive,
6169
+ selectable: interactive && this.isToolActive,
6170
+ evented: interactive && this.isToolActive,
6171
+ hasControls: false,
6172
+ hasBorders: false,
6173
+ hoverCursor: interactive ? "move" : "default",
6174
+ lockRotation: true,
6175
+ lockScalingX: true,
6176
+ lockScalingY: true,
6177
+ fill: "transparent",
6178
+ stroke: color,
6179
+ strokeWidth: FEATURE_STROKE_WIDTH,
6180
+ strokeDashArray: strokeDash,
6181
+ originX: "center",
6182
+ originY: "center",
6183
+ left: position.x,
6184
+ top: position.y,
6185
+ angle: feature.rotation || 0
6186
+ };
6187
+ const markerId = this.markerId(index);
6188
+ if (feature.shape === "rect") {
6189
+ specs.push({
6190
+ id: markerId,
6191
+ type: "rect",
6192
+ space: "screen",
6193
+ data: baseData,
6194
+ props: {
6195
+ ...commonProps,
5564
6196
  width: visualWidth,
5565
6197
  height: visualHeight,
5566
6198
  rx: visualRadius,
5567
- ry: visualRadius,
5568
- fill: "transparent",
5569
- stroke: color,
5570
- strokeWidth: 2,
5571
- strokeDashArray: strokeDash,
5572
- originX: "center",
5573
- originY: "center",
5574
- left: pos.x,
5575
- top: pos.y
5576
- });
5577
- } else {
5578
- shape = new Circle({
5579
- radius: visualRadius || 5 * finalScale,
5580
- fill: "transparent",
5581
- stroke: color,
5582
- strokeWidth: 2,
5583
- strokeDashArray: strokeDash,
5584
- originX: "center",
5585
- originY: "center",
5586
- left: pos.x,
5587
- top: pos.y
5588
- });
5589
- }
5590
- if (feature.rotation) {
5591
- shape.rotate(feature.rotation);
5592
- }
5593
- if (feature.bridge && feature.bridge.type === "vertical") {
5594
- const bridgeIndicator = new Rect2({
5595
- width: visualWidth,
5596
- height: 100 * featureScale,
5597
- // Arbitrary long length to show direction
5598
- fill: "transparent",
5599
- stroke: "#888",
6199
+ ry: visualRadius
6200
+ }
6201
+ });
6202
+ } else {
6203
+ specs.push({
6204
+ id: markerId,
6205
+ type: "rect",
6206
+ space: "screen",
6207
+ data: baseData,
6208
+ props: {
6209
+ ...commonProps,
6210
+ width: visualWidth,
6211
+ height: visualHeight,
6212
+ rx: visualRadius,
6213
+ ry: visualRadius
6214
+ }
6215
+ });
6216
+ }
6217
+ if (((_e = feature.bridge) == null ? void 0 : _e.type) === "vertical") {
6218
+ const featureTopY = position.y - visualHeight / 2;
6219
+ const dielineTopY = geometry.y - geometry.height / 2;
6220
+ const bridgeHeight = Math.max(0, featureTopY - dielineTopY);
6221
+ if (bridgeHeight <= 1e-3) {
6222
+ return;
6223
+ }
6224
+ specs.push({
6225
+ id: this.bridgeIndicatorId(index),
6226
+ type: "rect",
6227
+ space: "screen",
6228
+ data: {
6229
+ ...baseData,
6230
+ markerRole: "indicator",
6231
+ markerOffsetX: 0,
6232
+ markerOffsetY: -visualHeight / 2
6233
+ },
6234
+ props: {
6235
+ visible: this.isToolActive,
6236
+ selectable: false,
6237
+ evented: false,
6238
+ width: visualWidth,
6239
+ height: bridgeHeight,
6240
+ fill: "transparent",
6241
+ stroke: "#888",
5600
6242
  strokeWidth: 1,
5601
6243
  strokeDashArray: [2, 2],
5602
- originX: "center",
5603
- originY: "bottom",
5604
- // Anchor at bottom so it extends up
5605
- left: pos.x,
5606
- top: pos.y - visualHeight / 2,
5607
- // Start from top of feature
5608
6244
  opacity: 0.5,
5609
- selectable: false,
5610
- evented: false
5611
- });
5612
- const group = new Group([bridgeIndicator, shape], {
5613
6245
  originX: "center",
5614
- originY: "center",
5615
- left: pos.x,
5616
- top: pos.y
6246
+ originY: "bottom",
6247
+ left: position.x,
6248
+ top: position.y - visualHeight / 2
6249
+ }
6250
+ });
6251
+ }
6252
+ }
6253
+ buildMarkerData(marker, options) {
6254
+ const data = {
6255
+ type: "feature-marker",
6256
+ index: marker.index,
6257
+ featureId: marker.feature.id,
6258
+ markerRole: options.markerRole,
6259
+ markerOffsetX: 0,
6260
+ markerOffsetY: 0,
6261
+ isGroup: options.isGroup
6262
+ };
6263
+ if (options.groupId) data.groupId = options.groupId;
6264
+ if (options.indices) data.indices = options.indices;
6265
+ if (options.anchorIndex !== void 0) data.anchorIndex = options.anchorIndex;
6266
+ if (options.memberOffsets) data.memberOffsets = options.memberOffsets;
6267
+ return data;
6268
+ }
6269
+ markerId(index) {
6270
+ return `feature.marker.${index}`;
6271
+ }
6272
+ bridgeIndicatorId(index) {
6273
+ return `feature.marker.${index}.bridge`;
6274
+ }
6275
+ toFeatureIndex(value) {
6276
+ const numeric = Number(value);
6277
+ if (!Number.isInteger(numeric) || numeric < 0) return null;
6278
+ return numeric;
6279
+ }
6280
+ readGroupIndices(raw) {
6281
+ if (!Array.isArray(raw)) return [];
6282
+ return raw.map((value) => this.toFeatureIndex(value)).filter((value) => value !== null);
6283
+ }
6284
+ readGroupMemberOffsets(raw, fallbackIndices = []) {
6285
+ if (Array.isArray(raw)) {
6286
+ const parsed = raw.map((entry) => {
6287
+ const index = this.toFeatureIndex(entry == null ? void 0 : entry.index);
6288
+ const dx = Number(entry == null ? void 0 : entry.dx);
6289
+ const dy = Number(entry == null ? void 0 : entry.dy);
6290
+ if (index === null || !Number.isFinite(dx) || !Number.isFinite(dy)) {
6291
+ return null;
6292
+ }
6293
+ return { index, dx, dy };
6294
+ }).filter((value) => !!value);
6295
+ if (parsed.length > 0) return parsed;
6296
+ }
6297
+ return fallbackIndices.map((index) => ({ index, dx: 0, dy: 0 }));
6298
+ }
6299
+ syncMarkerVisualsByTarget(target, center) {
6300
+ var _a, _b, _c, _d, _e, _f;
6301
+ if ((_a = target.data) == null ? void 0 : _a.isGroup) {
6302
+ const indices = this.readGroupIndices((_b = target.data) == null ? void 0 : _b.indices);
6303
+ const offsets = this.readGroupMemberOffsets((_c = target.data) == null ? void 0 : _c.memberOffsets, indices);
6304
+ offsets.forEach((entry) => {
6305
+ this.syncMarkerVisualObjectsToCenter(entry.index, {
6306
+ x: center.x + entry.dx,
6307
+ y: center.y + entry.dy
5617
6308
  });
5618
- return group;
6309
+ });
6310
+ (_d = this.canvasService) == null ? void 0 : _d.requestRenderAll();
6311
+ return;
6312
+ }
6313
+ const index = this.toFeatureIndex((_e = target.data) == null ? void 0 : _e.index);
6314
+ if (index === null) return;
6315
+ this.syncMarkerVisualObjectsToCenter(index, center);
6316
+ (_f = this.canvasService) == null ? void 0 : _f.requestRenderAll();
6317
+ }
6318
+ syncMarkerVisualObjectsToCenter(index, center) {
6319
+ if (!this.canvasService) return;
6320
+ const markers = this.canvasService.canvas.getObjects().filter(
6321
+ (obj) => {
6322
+ var _a, _b;
6323
+ return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.type) === "feature-marker" && this.toFeatureIndex((_b = obj == null ? void 0 : obj.data) == null ? void 0 : _b.index) === index;
5619
6324
  }
5620
- return shape;
5621
- };
5622
- singles.forEach(({ feature, index }) => {
5623
- const geometry2 = this.getGeometryForFeature(
5624
- this.currentGeometry,
5625
- feature
5626
- );
5627
- const pos = resolveFeaturePosition(feature, geometry2);
5628
- const marker = createMarkerShape(feature, pos);
6325
+ );
6326
+ markers.forEach((marker) => {
6327
+ var _a, _b;
6328
+ const offsetX = Number(((_a = marker == null ? void 0 : marker.data) == null ? void 0 : _a.markerOffsetX) || 0);
6329
+ const offsetY = Number(((_b = marker == null ? void 0 : marker.data) == null ? void 0 : _b.markerOffsetY) || 0);
5629
6330
  marker.set({
5630
- visible: this.isToolActive,
5631
- selectable: this.isToolActive,
5632
- evented: this.isToolActive,
5633
- hasControls: false,
5634
- hasBorders: false,
5635
- hoverCursor: "move",
5636
- lockRotation: true,
5637
- lockScalingX: true,
5638
- lockScalingY: true,
5639
- data: { type: "feature-marker", index, isGroup: false }
5640
- });
5641
- canvas.add(marker);
5642
- canvas.bringObjectToFront(marker);
5643
- });
5644
- Object.keys(groups).forEach((groupId) => {
5645
- const members = groups[groupId];
5646
- if (members.length === 0) return;
5647
- const shapes = members.map(({ feature }) => {
5648
- const geometry2 = this.getGeometryForFeature(
5649
- this.currentGeometry,
5650
- feature
5651
- );
5652
- const pos = resolveFeaturePosition(feature, geometry2);
5653
- return createMarkerShape(feature, pos);
5654
- });
5655
- const groupObj = new Group(shapes, {
5656
- visible: this.isToolActive,
5657
- selectable: this.isToolActive,
5658
- evented: this.isToolActive,
5659
- hasControls: false,
5660
- hasBorders: false,
5661
- hoverCursor: "move",
5662
- lockRotation: true,
5663
- lockScalingX: true,
5664
- lockScalingY: true,
5665
- subTargetCheck: true,
5666
- // Allow events to pass through if needed, but we treat as one
5667
- interactive: false,
5668
- // Children not interactive
5669
- // @ts-ignore
5670
- data: {
5671
- type: "feature-marker",
5672
- isGroup: true,
5673
- groupId,
5674
- indices: members.map((m) => m.index)
5675
- }
6331
+ left: center.x + offsetX,
6332
+ top: center.y + offsetY
5676
6333
  });
5677
- canvas.add(groupObj);
5678
- canvas.bringObjectToFront(groupObj);
6334
+ marker.setCoords();
5679
6335
  });
5680
- this.canvasService.requestRenderAll();
5681
6336
  }
5682
6337
  enforceConstraints() {
5683
6338
  if (!this.canvasService || !this.currentGeometry) return;
5684
- const canvas = this.canvasService.canvas;
5685
- const markers = canvas.getObjects().filter((obj) => {
5686
- var _a;
5687
- return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
5688
- });
5689
- markers.forEach((marker) => {
5690
- var _a, _b, _c;
5691
- let feature;
5692
- if ((_a = marker.data) == null ? void 0 : _a.isGroup) {
5693
- const indices = (_b = marker.data) == null ? void 0 : _b.indices;
5694
- if (indices && indices.length > 0) {
5695
- feature = this.workingFeatures[indices[0]];
5696
- }
5697
- } else {
5698
- const index = (_c = marker.data) == null ? void 0 : _c.index;
5699
- if (index !== void 0) {
5700
- feature = this.workingFeatures[index];
5701
- }
6339
+ const handles = this.canvasService.canvas.getObjects().filter(
6340
+ (obj) => {
6341
+ var _a, _b;
6342
+ return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.type) === "feature-marker" && ((_b = obj == null ? void 0 : obj.data) == null ? void 0 : _b.markerRole) === "handle";
5702
6343
  }
5703
- const geometry = this.getGeometryForFeature(
5704
- this.currentGeometry,
5705
- feature
5706
- );
5707
- const markerStrokeWidth = (marker.strokeWidth || 2) * (marker.scaleX || 1);
5708
- const minDim = Math.min(
5709
- marker.getScaledWidth(),
5710
- marker.getScaledHeight()
5711
- );
5712
- const limit = Math.max(0, minDim / 2 - markerStrokeWidth);
6344
+ );
6345
+ handles.forEach((marker) => {
6346
+ const feature = this.getFeatureForMarker(marker);
6347
+ if (!feature) return;
6348
+ const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
5713
6349
  const snapped = this.constrainPosition(
5714
- new Point2(marker.left, marker.top),
6350
+ {
6351
+ x: Number(marker.left || 0),
6352
+ y: Number(marker.top || 0)
6353
+ },
5715
6354
  geometry,
5716
- limit,
5717
6355
  feature
5718
6356
  );
5719
6357
  marker.set({ left: snapped.x, top: snapped.y });
5720
6358
  marker.setCoords();
6359
+ this.syncMarkerVisualsByTarget(marker, snapped);
5721
6360
  });
5722
- canvas.requestRenderAll();
6361
+ this.canvasService.canvas.requestRenderAll();
5723
6362
  }
5724
6363
  };
5725
6364
 
@@ -5727,7 +6366,11 @@ var FeatureTool = class {
5727
6366
  import {
5728
6367
  ContributionPointIds as ContributionPointIds6
5729
6368
  } from "@pooder/core";
5730
- import { FabricImage as Image3 } from "fabric";
6369
+ import { FabricImage as FabricImage3 } from "fabric";
6370
+ var FILM_LAYER_ID = "overlay";
6371
+ var FILM_IMAGE_ID = "film-image";
6372
+ var DEFAULT_WIDTH2 = 800;
6373
+ var DEFAULT_HEIGHT2 = 600;
5731
6374
  var FilmTool = class {
5732
6375
  constructor(options) {
5733
6376
  this.id = "pooder.kit.film";
@@ -5736,17 +6379,38 @@ var FilmTool = class {
5736
6379
  };
5737
6380
  this.url = "";
5738
6381
  this.opacity = 0.5;
6382
+ this.specs = [];
6383
+ this.renderSeq = 0;
6384
+ this.renderImageUrl = "";
6385
+ this.sourceSizeBySrc = /* @__PURE__ */ new Map();
6386
+ this.pendingSizeBySrc = /* @__PURE__ */ new Map();
6387
+ this.onCanvasResized = () => {
6388
+ this.updateFilm();
6389
+ };
5739
6390
  if (options) {
5740
6391
  Object.assign(this, options);
5741
6392
  }
5742
6393
  }
5743
6394
  activate(context) {
6395
+ var _a;
5744
6396
  this.canvasService = context.services.get("CanvasService");
5745
6397
  if (!this.canvasService) {
5746
6398
  console.warn("CanvasService not found for FilmTool");
5747
6399
  return;
5748
6400
  }
5749
- const configService = context.services.get("ConfigurationService");
6401
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
6402
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
6403
+ this.id,
6404
+ () => ({
6405
+ layerSpecs: {
6406
+ [FILM_LAYER_ID]: this.specs
6407
+ }
6408
+ }),
6409
+ { priority: 500 }
6410
+ );
6411
+ const configService = context.services.get(
6412
+ "ConfigurationService"
6413
+ );
5750
6414
  if (configService) {
5751
6415
  this.url = configService.get("film.url", this.url);
5752
6416
  this.opacity = configService.get("film.opacity", this.opacity);
@@ -5763,21 +6427,21 @@ var FilmTool = class {
5763
6427
  }
5764
6428
  });
5765
6429
  }
5766
- this.initLayer();
6430
+ context.eventBus.on("canvas:resized", this.onCanvasResized);
5767
6431
  this.updateFilm();
5768
6432
  }
5769
6433
  deactivate(context) {
5770
- if (this.canvasService) {
5771
- const layer = this.canvasService.getLayer("overlay");
5772
- if (layer) {
5773
- const img = this.canvasService.getObject("film-image", "overlay");
5774
- if (img) {
5775
- layer.remove(img);
5776
- this.canvasService.requestRenderAll();
5777
- }
5778
- }
5779
- this.canvasService = void 0;
5780
- }
6434
+ var _a;
6435
+ context.eventBus.off("canvas:resized", this.onCanvasResized);
6436
+ this.renderSeq += 1;
6437
+ this.specs = [];
6438
+ this.renderImageUrl = "";
6439
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
6440
+ this.renderProducerDisposable = void 0;
6441
+ if (!this.canvasService) return;
6442
+ void this.canvasService.flushRenderFromProducers();
6443
+ this.canvasService.requestRenderAll();
6444
+ this.canvasService = void 0;
5781
6445
  }
5782
6446
  contribute() {
5783
6447
  return {
@@ -5813,73 +6477,108 @@ var FilmTool = class {
5813
6477
  ]
5814
6478
  };
5815
6479
  }
5816
- initLayer() {
5817
- if (!this.canvasService) return;
5818
- let overlayLayer = this.canvasService.getLayer("overlay");
5819
- if (!overlayLayer) {
5820
- const width = this.canvasService.canvas.width || 800;
5821
- const height = this.canvasService.canvas.height || 600;
5822
- const layer = this.canvasService.createLayer("overlay", {
5823
- width,
5824
- height,
5825
- left: 0,
5826
- top: 0,
5827
- originX: "left",
5828
- originY: "top",
5829
- selectable: false,
5830
- evented: false,
5831
- subTargetCheck: false,
5832
- interactive: false
5833
- });
5834
- this.canvasService.canvas.bringObjectToFront(layer);
5835
- }
6480
+ getViewportSize() {
6481
+ var _a, _b;
6482
+ const width = Number(((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 0);
6483
+ const height = Number(((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 0);
6484
+ return {
6485
+ width: width > 0 ? width : DEFAULT_WIDTH2,
6486
+ height: height > 0 ? height : DEFAULT_HEIGHT2
6487
+ };
5836
6488
  }
5837
- async updateFilm() {
5838
- if (!this.canvasService) return;
5839
- const layer = this.canvasService.getLayer("overlay");
5840
- if (!layer) {
5841
- console.warn("[FilmTool] Overlay layer not found");
5842
- return;
5843
- }
5844
- const { url, opacity } = this;
5845
- if (!url) {
5846
- const img2 = this.canvasService.getObject("film-image", "overlay");
5847
- if (img2) {
5848
- layer.remove(img2);
5849
- this.canvasService.requestRenderAll();
5850
- }
5851
- return;
6489
+ clampOpacity(value) {
6490
+ return Math.max(0, Math.min(1, Number(value)));
6491
+ }
6492
+ buildFilmSpecs(imageUrl, opacity) {
6493
+ if (!imageUrl) {
6494
+ return [];
5852
6495
  }
5853
- const width = this.canvasService.canvas.width || 800;
5854
- const height = this.canvasService.canvas.height || 600;
5855
- let img = this.canvasService.getObject("film-image", "overlay");
5856
- try {
5857
- if (img) {
5858
- if (img.getSrc() !== url) {
5859
- await img.setSrc(url);
5860
- }
5861
- img.set({ opacity });
5862
- } else {
5863
- img = await Image3.fromURL(url, { crossOrigin: "anonymous" });
5864
- img.scaleToWidth(width);
5865
- if (img.getScaledHeight() < height) img.scaleToHeight(height);
5866
- img.set({
5867
- originX: "left",
5868
- originY: "top",
6496
+ const { width, height } = this.getViewportSize();
6497
+ const sourceSize = this.sourceSizeBySrc.get(imageUrl);
6498
+ const sourceWidth = Math.max(1, Number((sourceSize == null ? void 0 : sourceSize.width) || width));
6499
+ const sourceHeight = Math.max(1, Number((sourceSize == null ? void 0 : sourceSize.height) || height));
6500
+ const coverScale = Math.max(width / sourceWidth, height / sourceHeight);
6501
+ return [
6502
+ {
6503
+ id: FILM_IMAGE_ID,
6504
+ type: "image",
6505
+ src: imageUrl,
6506
+ space: "screen",
6507
+ data: {
6508
+ id: FILM_IMAGE_ID,
6509
+ layerId: FILM_LAYER_ID,
6510
+ type: "film-image"
6511
+ },
6512
+ props: {
5869
6513
  left: 0,
5870
6514
  top: 0,
5871
- opacity,
6515
+ originX: "left",
6516
+ originY: "top",
6517
+ opacity: this.clampOpacity(opacity),
6518
+ scaleX: coverScale,
6519
+ scaleY: coverScale,
5872
6520
  selectable: false,
5873
6521
  evented: false,
5874
- data: { id: "film-image" }
5875
- });
5876
- layer.add(img);
6522
+ excludeFromExport: true
6523
+ }
6524
+ }
6525
+ ];
6526
+ }
6527
+ async ensureImageSize(src) {
6528
+ if (!src) return null;
6529
+ const cached = this.sourceSizeBySrc.get(src);
6530
+ if (cached) return cached;
6531
+ const pending = this.pendingSizeBySrc.get(src);
6532
+ if (pending) {
6533
+ return pending;
6534
+ }
6535
+ const task = this.loadImageSize(src);
6536
+ this.pendingSizeBySrc.set(src, task);
6537
+ try {
6538
+ return await task;
6539
+ } finally {
6540
+ if (this.pendingSizeBySrc.get(src) === task) {
6541
+ this.pendingSizeBySrc.delete(src);
6542
+ }
6543
+ }
6544
+ }
6545
+ async loadImageSize(src) {
6546
+ try {
6547
+ const image = await FabricImage3.fromURL(src, {
6548
+ crossOrigin: "anonymous"
6549
+ });
6550
+ const width = Number((image == null ? void 0 : image.width) || 0);
6551
+ const height = Number((image == null ? void 0 : image.height) || 0);
6552
+ if (width > 0 && height > 0) {
6553
+ const size = { width, height };
6554
+ this.sourceSizeBySrc.set(src, size);
6555
+ return size;
5877
6556
  }
5878
- this.canvasService.requestRenderAll();
5879
6557
  } catch (error) {
5880
- console.error("[FilmTool] Failed to load film image", url, error);
6558
+ console.error("[FilmTool] Failed to load film image", src, error);
6559
+ }
6560
+ return null;
6561
+ }
6562
+ updateFilm() {
6563
+ void this.updateFilmAsync();
6564
+ }
6565
+ async updateFilmAsync() {
6566
+ if (!this.canvasService) return;
6567
+ const seq = ++this.renderSeq;
6568
+ const nextUrl = String(this.url || "").trim();
6569
+ if (!nextUrl) {
6570
+ this.renderImageUrl = "";
6571
+ } else if (nextUrl !== this.renderImageUrl) {
6572
+ const loaded = await this.ensureImageSize(nextUrl);
6573
+ if (seq !== this.renderSeq) return;
6574
+ if (loaded) {
6575
+ this.renderImageUrl = nextUrl;
6576
+ }
5881
6577
  }
5882
- layer.dirty = true;
6578
+ this.specs = this.buildFilmSpecs(this.renderImageUrl, this.opacity);
6579
+ await this.canvasService.flushRenderFromProducers();
6580
+ if (seq !== this.renderSeq) return;
6581
+ this.canvasService.bringLayerToFront(FILM_LAYER_ID);
5883
6582
  this.canvasService.requestRenderAll();
5884
6583
  }
5885
6584
  };
@@ -5980,19 +6679,37 @@ var MirrorTool = class {
5980
6679
  import {
5981
6680
  ContributionPointIds as ContributionPointIds8
5982
6681
  } from "@pooder/core";
5983
- import { Line, Text, Group as Group2, Polygon } from "fabric";
6682
+ var RULER_LAYER_ID = "ruler-overlay";
6683
+ var EXTENSION_LINE_LENGTH = 5;
6684
+ var MIN_ARROW_SIZE = 4;
6685
+ var THICKNESS_TO_STROKE_WIDTH_RATIO = 20;
6686
+ var DEFAULT_THICKNESS = 20;
6687
+ var DEFAULT_GAP = 45;
6688
+ var DEFAULT_FONT_SIZE = 10;
6689
+ var DEFAULT_BACKGROUND_COLOR = "#f0f0f0";
6690
+ var DEFAULT_TEXT_COLOR = "#333333";
6691
+ var DEFAULT_LINE_COLOR = "#999999";
6692
+ var RULER_THICKNESS_MIN = 10;
6693
+ var RULER_THICKNESS_MAX = 100;
6694
+ var RULER_GAP_MIN = 0;
6695
+ var RULER_GAP_MAX = 100;
6696
+ var RULER_FONT_SIZE_MIN = 8;
6697
+ var RULER_FONT_SIZE_MAX = 24;
5984
6698
  var RulerTool = class {
5985
6699
  constructor(options) {
5986
6700
  this.id = "pooder.kit.ruler";
5987
6701
  this.metadata = {
5988
6702
  name: "RulerTool"
5989
6703
  };
5990
- this.thickness = 20;
5991
- this.gap = 15;
5992
- this.backgroundColor = "#f0f0f0";
5993
- this.textColor = "#333333";
5994
- this.lineColor = "#999999";
5995
- this.fontSize = 10;
6704
+ this.thickness = DEFAULT_THICKNESS;
6705
+ this.gap = DEFAULT_GAP;
6706
+ this.backgroundColor = DEFAULT_BACKGROUND_COLOR;
6707
+ this.textColor = DEFAULT_TEXT_COLOR;
6708
+ this.lineColor = DEFAULT_LINE_COLOR;
6709
+ this.fontSize = DEFAULT_FONT_SIZE;
6710
+ this.renderSeq = 0;
6711
+ this.numericProps = /* @__PURE__ */ new Set(["thickness", "gap", "fontSize"]);
6712
+ this.specs = [];
5996
6713
  this.onCanvasResized = () => {
5997
6714
  this.updateRuler();
5998
6715
  };
@@ -6001,50 +6718,73 @@ var RulerTool = class {
6001
6718
  }
6002
6719
  }
6003
6720
  activate(context) {
6721
+ var _a;
6004
6722
  this.context = context;
6005
6723
  this.canvasService = context.services.get("CanvasService");
6006
6724
  if (!this.canvasService) {
6007
- console.warn("CanvasService not found for RulerTool");
6725
+ console.warn("[RulerTool] CanvasService not found.");
6008
6726
  return;
6009
6727
  }
6728
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
6729
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
6730
+ this.id,
6731
+ () => ({
6732
+ rootLayerSpecs: {
6733
+ [RULER_LAYER_ID]: this.specs
6734
+ },
6735
+ replaceRootLayerIds: [RULER_LAYER_ID]
6736
+ }),
6737
+ { priority: 400 }
6738
+ );
6010
6739
  const configService = context.services.get(
6011
6740
  "ConfigurationService"
6012
6741
  );
6013
6742
  if (configService) {
6014
- this.thickness = configService.get("ruler.thickness", this.thickness);
6015
- this.gap = configService.get("ruler.gap", this.gap);
6016
- this.backgroundColor = configService.get(
6017
- "ruler.backgroundColor",
6018
- this.backgroundColor
6019
- );
6020
- this.textColor = configService.get("ruler.textColor", this.textColor);
6021
- this.lineColor = configService.get("ruler.lineColor", this.lineColor);
6022
- this.fontSize = configService.get("ruler.fontSize", this.fontSize);
6743
+ this.syncConfig(configService);
6023
6744
  configService.onAnyChange((e) => {
6024
6745
  let shouldUpdate = false;
6025
6746
  if (e.key.startsWith("ruler.")) {
6026
6747
  const prop = e.key.split(".")[1];
6027
6748
  if (prop && prop in this) {
6028
- this[prop] = e.value;
6749
+ if (this.numericProps.has(prop)) {
6750
+ this[prop] = this.toFiniteNumber(
6751
+ e.value,
6752
+ this[prop]
6753
+ );
6754
+ } else {
6755
+ this[prop] = e.value;
6756
+ }
6029
6757
  shouldUpdate = true;
6758
+ this.log("config:update", {
6759
+ key: e.key,
6760
+ raw: e.value,
6761
+ normalized: this[prop]
6762
+ });
6030
6763
  }
6031
6764
  } else if (e.key.startsWith("size.")) {
6032
6765
  shouldUpdate = true;
6766
+ this.log("size:update", { key: e.key, value: e.value });
6033
6767
  }
6034
6768
  if (shouldUpdate) {
6035
6769
  this.updateRuler();
6036
6770
  }
6037
6771
  });
6038
6772
  }
6039
- this.createLayer();
6040
6773
  context.eventBus.on("canvas:resized", this.onCanvasResized);
6041
6774
  this.updateRuler();
6042
6775
  }
6043
6776
  deactivate(context) {
6777
+ var _a;
6044
6778
  context.eventBus.off("canvas:resized", this.onCanvasResized);
6045
- this.destroyLayer();
6779
+ this.specs = [];
6780
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
6781
+ this.renderProducerDisposable = void 0;
6782
+ if (this.canvasService) {
6783
+ void this.canvasService.flushRenderFromProducers();
6784
+ }
6046
6785
  this.canvasService = void 0;
6047
6786
  this.context = void 0;
6787
+ this.renderSeq = 0;
6048
6788
  }
6049
6789
  contribute() {
6050
6790
  return {
@@ -6053,43 +6793,43 @@ var RulerTool = class {
6053
6793
  id: "ruler.thickness",
6054
6794
  type: "number",
6055
6795
  label: "Thickness",
6056
- min: 10,
6057
- max: 100,
6058
- default: 20
6796
+ min: RULER_THICKNESS_MIN,
6797
+ max: RULER_THICKNESS_MAX,
6798
+ default: DEFAULT_THICKNESS
6059
6799
  },
6060
6800
  {
6061
6801
  id: "ruler.gap",
6062
6802
  type: "number",
6063
6803
  label: "Gap",
6064
- min: 0,
6065
- max: 100,
6066
- default: 15
6804
+ min: RULER_GAP_MIN,
6805
+ max: RULER_GAP_MAX,
6806
+ default: DEFAULT_GAP
6067
6807
  },
6068
6808
  {
6069
6809
  id: "ruler.backgroundColor",
6070
6810
  type: "color",
6071
6811
  label: "Background Color",
6072
- default: "#f0f0f0"
6812
+ default: DEFAULT_BACKGROUND_COLOR
6073
6813
  },
6074
6814
  {
6075
6815
  id: "ruler.textColor",
6076
6816
  type: "color",
6077
6817
  label: "Text Color",
6078
- default: "#333333"
6818
+ default: DEFAULT_TEXT_COLOR
6079
6819
  },
6080
6820
  {
6081
6821
  id: "ruler.lineColor",
6082
6822
  type: "color",
6083
6823
  label: "Line Color",
6084
- default: "#999999"
6824
+ default: DEFAULT_LINE_COLOR
6085
6825
  },
6086
6826
  {
6087
6827
  id: "ruler.fontSize",
6088
6828
  type: "number",
6089
6829
  label: "Font Size",
6090
- min: 8,
6091
- max: 24,
6092
- default: 10
6830
+ min: RULER_FONT_SIZE_MIN,
6831
+ max: RULER_FONT_SIZE_MAX,
6832
+ default: DEFAULT_FONT_SIZE
6093
6833
  }
6094
6834
  ],
6095
6835
  [ContributionPointIds8.COMMANDS]: [
@@ -6102,12 +6842,23 @@ var RulerTool = class {
6102
6842
  textColor: this.textColor,
6103
6843
  lineColor: this.lineColor,
6104
6844
  fontSize: this.fontSize,
6105
- thickness: this.thickness
6845
+ thickness: this.thickness,
6846
+ gap: this.gap
6106
6847
  };
6107
6848
  const newState = { ...oldState, ...theme };
6108
- if (JSON.stringify(newState) === JSON.stringify(oldState))
6849
+ if (JSON.stringify(newState) === JSON.stringify(oldState)) {
6109
6850
  return true;
6851
+ }
6110
6852
  Object.assign(this, newState);
6853
+ this.thickness = this.toFiniteNumber(
6854
+ this.thickness,
6855
+ DEFAULT_THICKNESS
6856
+ );
6857
+ this.gap = this.toFiniteNumber(this.gap, DEFAULT_GAP);
6858
+ this.fontSize = this.toFiniteNumber(
6859
+ this.fontSize,
6860
+ DEFAULT_FONT_SIZE
6861
+ );
6111
6862
  this.updateRuler();
6112
6863
  return true;
6113
6864
  }
@@ -6115,225 +6866,367 @@ var RulerTool = class {
6115
6866
  ]
6116
6867
  };
6117
6868
  }
6118
- getLayer() {
6119
- var _a;
6120
- return (_a = this.canvasService) == null ? void 0 : _a.getLayer("ruler-overlay");
6869
+ log(step, payload) {
6870
+ if (payload) {
6871
+ console.debug(`[RulerTool] ${step}`, payload);
6872
+ return;
6873
+ }
6874
+ console.debug(`[RulerTool] ${step}`);
6121
6875
  }
6122
- createLayer() {
6123
- if (!this.canvasService) return;
6124
- const canvas = this.canvasService.canvas;
6125
- const width = canvas.width || 800;
6126
- const height = canvas.height || 600;
6127
- const layer = this.canvasService.createLayer("ruler-overlay", {
6128
- width,
6129
- height,
6130
- selectable: false,
6131
- evented: false,
6132
- left: 0,
6133
- top: 0,
6134
- originX: "left",
6135
- originY: "top"
6876
+ syncConfig(configService) {
6877
+ this.thickness = this.toFiniteNumber(
6878
+ configService.get("ruler.thickness", this.thickness),
6879
+ DEFAULT_THICKNESS
6880
+ );
6881
+ this.gap = Math.max(
6882
+ 0,
6883
+ this.toFiniteNumber(
6884
+ configService.get("ruler.gap", this.gap),
6885
+ DEFAULT_GAP
6886
+ )
6887
+ );
6888
+ this.backgroundColor = configService.get(
6889
+ "ruler.backgroundColor",
6890
+ this.backgroundColor
6891
+ );
6892
+ this.textColor = configService.get("ruler.textColor", this.textColor);
6893
+ this.lineColor = configService.get("ruler.lineColor", this.lineColor);
6894
+ this.fontSize = this.toFiniteNumber(
6895
+ configService.get("ruler.fontSize", this.fontSize),
6896
+ DEFAULT_FONT_SIZE
6897
+ );
6898
+ this.log("config:loaded", {
6899
+ thickness: this.thickness,
6900
+ gap: this.gap,
6901
+ fontSize: this.fontSize,
6902
+ backgroundColor: this.backgroundColor,
6903
+ textColor: this.textColor,
6904
+ lineColor: this.lineColor
6136
6905
  });
6137
- canvas.bringObjectToFront(layer);
6138
6906
  }
6139
- destroyLayer() {
6140
- if (!this.canvasService) return;
6141
- const layer = this.getLayer();
6142
- if (layer) {
6143
- this.canvasService.canvas.remove(layer);
6144
- }
6907
+ toFiniteNumber(value, fallback) {
6908
+ const numeric = Number(value);
6909
+ return Number.isFinite(numeric) ? numeric : fallback;
6145
6910
  }
6146
- createArrowLine(x1, y1, x2, y2, color) {
6147
- const line = new Line([x1, y1, x2, y2], {
6148
- stroke: color,
6149
- strokeWidth: this.thickness / 20,
6150
- // Scale stroke width relative to thickness (default 1)
6151
- selectable: false,
6152
- evented: false
6153
- });
6154
- const arrowSize = Math.max(4, this.thickness * 0.3);
6155
- const angle = Math.atan2(y2 - y1, x2 - x1);
6156
- const endArrow = new Polygon(
6157
- [
6158
- { x: 0, y: 0 },
6159
- { x: -arrowSize, y: -arrowSize / 2 },
6160
- { x: -arrowSize, y: arrowSize / 2 }
6161
- ],
6162
- {
6163
- fill: color,
6164
- left: x2,
6165
- top: y2,
6166
- originX: "right",
6911
+ toSceneDisplayLength(value) {
6912
+ if (!this.canvasService) return value;
6913
+ return this.canvasService.toSceneLength(value);
6914
+ }
6915
+ formatLengthMm(valueMm, unit) {
6916
+ const converted = fromMm(valueMm, unit);
6917
+ const fractionDigits = unit === "in" ? 3 : 2;
6918
+ return Number(converted.toFixed(fractionDigits)).toString();
6919
+ }
6920
+ buildLinePath(start, end) {
6921
+ const dx = end.x - start.x;
6922
+ const dy = end.y - start.y;
6923
+ return `M 0 0 L ${dx} ${dy}`;
6924
+ }
6925
+ buildStartArrowPath(size) {
6926
+ return `M 0 0 L ${size} ${-size / 2} L ${size} ${size / 2} Z`;
6927
+ }
6928
+ buildEndArrowPath(size) {
6929
+ return `M 0 0 L ${-size} ${-size / 2} L ${-size} ${size / 2} Z`;
6930
+ }
6931
+ createPathSpec(id, pathData, position, options) {
6932
+ var _a, _b, _c, _d, _e, _f, _g;
6933
+ return {
6934
+ id,
6935
+ type: "path",
6936
+ data: {
6937
+ id,
6938
+ type: "ruler"
6939
+ },
6940
+ props: {
6941
+ pathData,
6942
+ left: position.x,
6943
+ top: position.y,
6944
+ originX: (_a = options.originX) != null ? _a : "left",
6945
+ originY: (_b = options.originY) != null ? _b : "top",
6946
+ angle: (_c = options.angle) != null ? _c : 0,
6947
+ stroke: (_d = options.stroke) != null ? _d : null,
6948
+ fill: (_e = options.fill) != null ? _e : null,
6949
+ strokeWidth: (_f = options.strokeWidth) != null ? _f : 1,
6950
+ strokeLineCap: (_g = options.strokeLineCap) != null ? _g : "butt",
6951
+ selectable: false,
6952
+ evented: false,
6953
+ excludeFromExport: true
6954
+ }
6955
+ };
6956
+ }
6957
+ createTextSpec(id, text, position, angle = 0) {
6958
+ return {
6959
+ id,
6960
+ type: "text",
6961
+ data: {
6962
+ id,
6963
+ type: "ruler"
6964
+ },
6965
+ props: {
6966
+ text,
6967
+ left: position.x,
6968
+ top: position.y,
6969
+ angle,
6970
+ fontSize: this.toSceneDisplayLength(this.fontSize),
6971
+ fill: this.textColor,
6972
+ fontFamily: "Arial",
6973
+ originX: "center",
6167
6974
  originY: "center",
6168
- angle: angle * 180 / Math.PI,
6975
+ backgroundColor: this.backgroundColor,
6169
6976
  selectable: false,
6170
- evented: false
6977
+ evented: false,
6978
+ excludeFromExport: true
6171
6979
  }
6980
+ };
6981
+ }
6982
+ buildRulerSpecs(input) {
6983
+ const { left, top, right, bottom, widthLabel, heightLabel } = input;
6984
+ const gap = Math.max(
6985
+ 0,
6986
+ this.toSceneDisplayLength(this.toFiniteNumber(this.gap, DEFAULT_GAP))
6987
+ );
6988
+ const topY = top - gap;
6989
+ const leftX = left - gap;
6990
+ const arrowSize = Math.max(
6991
+ this.toSceneDisplayLength(MIN_ARROW_SIZE),
6992
+ this.toSceneDisplayLength(this.thickness * 0.3)
6993
+ );
6994
+ const strokeWidth = Math.max(
6995
+ this.toSceneDisplayLength(1),
6996
+ this.toSceneDisplayLength(
6997
+ this.thickness / THICKNESS_TO_STROKE_WIDTH_RATIO
6998
+ )
6999
+ );
7000
+ const extensionLength = this.toSceneDisplayLength(EXTENSION_LINE_LENGTH);
7001
+ const topLineAngleDeg = 0;
7002
+ const leftLineAngleDeg = 90;
7003
+ const topMidX = left + (right - left) / 2;
7004
+ const leftMidY = top + (bottom - top) / 2;
7005
+ const topLineStartX = Math.min(left + arrowSize, topMidX);
7006
+ const topLineEndX = Math.max(right - arrowSize, topMidX);
7007
+ const leftLineStartY = Math.min(top + arrowSize, leftMidY);
7008
+ const leftLineEndY = Math.max(bottom - arrowSize, leftMidY);
7009
+ const specs = [];
7010
+ specs.push(
7011
+ this.createPathSpec(
7012
+ "ruler.top.line",
7013
+ this.buildLinePath(
7014
+ { x: topLineStartX, y: topY },
7015
+ { x: topLineEndX, y: topY }
7016
+ ),
7017
+ { x: topLineStartX, y: topY },
7018
+ {
7019
+ stroke: this.lineColor,
7020
+ strokeWidth,
7021
+ strokeLineCap: "butt"
7022
+ }
7023
+ ),
7024
+ this.createPathSpec(
7025
+ "ruler.top.arrow.start",
7026
+ this.buildStartArrowPath(arrowSize),
7027
+ { x: left, y: topY },
7028
+ {
7029
+ fill: this.lineColor,
7030
+ stroke: this.lineColor,
7031
+ strokeWidth: this.toSceneDisplayLength(1),
7032
+ originX: "left",
7033
+ originY: "center",
7034
+ angle: topLineAngleDeg
7035
+ }
7036
+ ),
7037
+ this.createPathSpec(
7038
+ "ruler.top.arrow.end",
7039
+ this.buildEndArrowPath(arrowSize),
7040
+ { x: right, y: topY },
7041
+ {
7042
+ fill: this.lineColor,
7043
+ stroke: this.lineColor,
7044
+ strokeWidth: this.toSceneDisplayLength(1),
7045
+ originX: "right",
7046
+ originY: "center",
7047
+ angle: topLineAngleDeg
7048
+ }
7049
+ ),
7050
+ this.createPathSpec(
7051
+ "ruler.top.ext.start",
7052
+ this.buildLinePath(
7053
+ { x: left, y: topY - extensionLength },
7054
+ { x: left, y: topY + extensionLength }
7055
+ ),
7056
+ { x: left, y: topY - extensionLength },
7057
+ {
7058
+ stroke: this.lineColor,
7059
+ strokeWidth: this.toSceneDisplayLength(1)
7060
+ }
7061
+ ),
7062
+ this.createPathSpec(
7063
+ "ruler.top.ext.end",
7064
+ this.buildLinePath(
7065
+ { x: right, y: topY - extensionLength },
7066
+ { x: right, y: topY + extensionLength }
7067
+ ),
7068
+ { x: right, y: topY - extensionLength },
7069
+ {
7070
+ stroke: this.lineColor,
7071
+ strokeWidth: this.toSceneDisplayLength(1)
7072
+ }
7073
+ ),
7074
+ this.createTextSpec("ruler.top.label", widthLabel, {
7075
+ x: left + (right - left) / 2,
7076
+ y: topY
7077
+ })
6172
7078
  );
6173
- const startArrow = new Polygon(
6174
- [
6175
- { x: 0, y: 0 },
6176
- { x: arrowSize, y: -arrowSize / 2 },
6177
- { x: arrowSize, y: arrowSize / 2 }
6178
- ],
6179
- {
6180
- fill: color,
6181
- left: x1,
6182
- top: y1,
6183
- originX: "left",
6184
- originY: "center",
6185
- angle: angle * 180 / Math.PI,
6186
- selectable: false,
6187
- evented: false
6188
- }
7079
+ specs.push(
7080
+ this.createPathSpec(
7081
+ "ruler.left.line",
7082
+ this.buildLinePath(
7083
+ { x: leftX, y: leftLineStartY },
7084
+ { x: leftX, y: leftLineEndY }
7085
+ ),
7086
+ { x: leftX, y: leftLineStartY },
7087
+ {
7088
+ stroke: this.lineColor,
7089
+ strokeWidth,
7090
+ strokeLineCap: "butt"
7091
+ }
7092
+ ),
7093
+ this.createPathSpec(
7094
+ "ruler.left.arrow.start",
7095
+ this.buildStartArrowPath(arrowSize),
7096
+ { x: leftX, y: top },
7097
+ {
7098
+ fill: this.lineColor,
7099
+ stroke: this.lineColor,
7100
+ strokeWidth: this.toSceneDisplayLength(1),
7101
+ originX: "left",
7102
+ originY: "center",
7103
+ angle: leftLineAngleDeg
7104
+ }
7105
+ ),
7106
+ this.createPathSpec(
7107
+ "ruler.left.arrow.end",
7108
+ this.buildEndArrowPath(arrowSize),
7109
+ { x: leftX, y: bottom },
7110
+ {
7111
+ fill: this.lineColor,
7112
+ stroke: this.lineColor,
7113
+ strokeWidth: this.toSceneDisplayLength(1),
7114
+ originX: "right",
7115
+ originY: "center",
7116
+ angle: leftLineAngleDeg
7117
+ }
7118
+ ),
7119
+ this.createPathSpec(
7120
+ "ruler.left.ext.start",
7121
+ this.buildLinePath(
7122
+ { x: leftX - extensionLength, y: top },
7123
+ { x: leftX + extensionLength, y: top }
7124
+ ),
7125
+ { x: leftX - extensionLength, y: top },
7126
+ {
7127
+ stroke: this.lineColor,
7128
+ strokeWidth: this.toSceneDisplayLength(1)
7129
+ }
7130
+ ),
7131
+ this.createPathSpec(
7132
+ "ruler.left.ext.end",
7133
+ this.buildLinePath(
7134
+ { x: leftX - extensionLength, y: bottom },
7135
+ { x: leftX + extensionLength, y: bottom }
7136
+ ),
7137
+ { x: leftX - extensionLength, y: bottom },
7138
+ {
7139
+ stroke: this.lineColor,
7140
+ strokeWidth: this.toSceneDisplayLength(1)
7141
+ }
7142
+ ),
7143
+ this.createTextSpec(
7144
+ "ruler.left.label",
7145
+ heightLabel,
7146
+ {
7147
+ x: leftX,
7148
+ y: top + (bottom - top) / 2
7149
+ },
7150
+ -90
7151
+ )
6189
7152
  );
6190
- return new Group2([line, startArrow, endArrow], {
6191
- selectable: false,
6192
- evented: false
6193
- });
7153
+ return specs;
6194
7154
  }
6195
7155
  updateRuler() {
6196
- var _a;
7156
+ void this.updateRulerAsync();
7157
+ }
7158
+ async updateRulerAsync() {
7159
+ var _a, _b;
6197
7160
  if (!this.canvasService) return;
6198
- const layer = this.getLayer();
6199
- if (!layer) return;
6200
- layer.remove(...layer.getObjects());
6201
- const { backgroundColor, lineColor, textColor, fontSize } = this;
6202
7161
  const configService = (_a = this.context) == null ? void 0 : _a.services.get(
6203
7162
  "ConfigurationService"
6204
7163
  );
6205
7164
  if (!configService) return;
7165
+ const seq = ++this.renderSeq;
6206
7166
  const sizeState = readSizeState(configService);
6207
7167
  const layout = computeSceneLayout(this.canvasService, sizeState);
6208
- if (!layout) return;
6209
- const trimRect = layout.trimRect;
6210
- const cutRect = layout.cutRect;
6211
- const useCutAsRuler = layout.cutMode === "outset";
6212
- const rulerRect = useCutAsRuler ? cutRect : trimRect;
6213
- const gap = this.gap || 15;
6214
- const rulerLeft = rulerRect.left;
6215
- const rulerTop = rulerRect.top;
6216
- const rulerRight = rulerRect.left + rulerRect.width;
6217
- const rulerBottom = rulerRect.top + rulerRect.height;
6218
- const displayWidthMm = useCutAsRuler ? layout.cutWidthMm : layout.trimWidthMm;
6219
- const displayHeightMm = useCutAsRuler ? layout.cutHeightMm : layout.trimHeightMm;
6220
- const displayUnit = sizeState.unit;
6221
- const topRulerY = rulerTop - gap;
6222
- const topRulerXStart = rulerLeft;
6223
- const topRulerXEnd = rulerRight;
6224
- const leftRulerX = rulerLeft - gap;
6225
- const leftRulerYStart = rulerTop;
6226
- const leftRulerYEnd = rulerBottom;
6227
- const topDimLine = this.createArrowLine(
6228
- topRulerXStart,
6229
- topRulerY,
6230
- topRulerXEnd,
6231
- topRulerY,
6232
- lineColor
6233
- );
6234
- layer.add(topDimLine);
6235
- const extLen = 5;
6236
- layer.add(
6237
- new Line(
6238
- [
6239
- topRulerXStart,
6240
- topRulerY - extLen,
6241
- topRulerXStart,
6242
- topRulerY + extLen
6243
- ],
6244
- {
6245
- stroke: lineColor,
6246
- strokeWidth: 1,
6247
- selectable: false,
6248
- evented: false
6249
- }
6250
- )
6251
- );
6252
- layer.add(
6253
- new Line(
6254
- [topRulerXEnd, topRulerY - extLen, topRulerXEnd, topRulerY + extLen],
6255
- {
6256
- stroke: lineColor,
6257
- strokeWidth: 1,
6258
- selectable: false,
6259
- evented: false
6260
- }
6261
- )
6262
- );
6263
- const widthStr = formatMm(displayWidthMm, displayUnit);
6264
- const topTextContent = `${widthStr} ${displayUnit}`;
6265
- const topText = new Text(topTextContent, {
6266
- left: topRulerXStart + (rulerRight - rulerLeft) / 2,
6267
- top: topRulerY,
6268
- fontSize,
6269
- fill: textColor,
6270
- fontFamily: "Arial",
6271
- originX: "center",
6272
- originY: "center",
6273
- backgroundColor,
6274
- // Background mask for readability
6275
- selectable: false,
6276
- evented: false
7168
+ this.log("render:start", {
7169
+ seq,
7170
+ unit: sizeState.unit,
7171
+ gap: this.gap,
7172
+ thickness: this.thickness,
7173
+ fontSize: this.fontSize,
7174
+ hasLayout: !!layout,
7175
+ scale: (_b = layout == null ? void 0 : layout.scale) != null ? _b : null
6277
7176
  });
6278
- layer.add(topText);
6279
- const leftDimLine = this.createArrowLine(
6280
- leftRulerX,
6281
- leftRulerYStart,
6282
- leftRulerX,
6283
- leftRulerYEnd,
6284
- lineColor
6285
- );
6286
- layer.add(leftDimLine);
6287
- layer.add(
6288
- new Line(
6289
- [
6290
- leftRulerX - extLen,
6291
- leftRulerYStart,
6292
- leftRulerX + extLen,
6293
- leftRulerYStart
6294
- ],
6295
- {
6296
- stroke: lineColor,
6297
- strokeWidth: 1,
6298
- selectable: false,
6299
- evented: false
6300
- }
6301
- )
6302
- );
6303
- layer.add(
6304
- new Line(
6305
- [
6306
- leftRulerX - extLen,
6307
- leftRulerYEnd,
6308
- leftRulerX + extLen,
6309
- leftRulerYEnd
6310
- ],
6311
- {
6312
- stroke: lineColor,
6313
- strokeWidth: 1,
6314
- selectable: false,
6315
- evented: false
6316
- }
6317
- )
6318
- );
6319
- const heightStr = formatMm(displayHeightMm, displayUnit);
6320
- const leftTextContent = `${heightStr} ${displayUnit}`;
6321
- const leftText = new Text(leftTextContent, {
6322
- left: leftRulerX,
6323
- top: leftRulerYStart + (rulerBottom - rulerTop) / 2,
6324
- angle: -90,
6325
- fontSize,
6326
- fill: textColor,
6327
- fontFamily: "Arial",
6328
- originX: "center",
6329
- originY: "center",
6330
- backgroundColor,
6331
- selectable: false,
6332
- evented: false
7177
+ if (!layout || layout.scale <= 0) {
7178
+ if (seq !== this.renderSeq) return;
7179
+ this.log("render:skip", { seq, reason: "invalid-layout" });
7180
+ this.specs = [];
7181
+ await this.canvasService.flushRenderFromProducers();
7182
+ return;
7183
+ }
7184
+ const geometry = buildSceneGeometry(configService, layout);
7185
+ if (geometry.unit !== "px") {
7186
+ console.warn("[RulerTool] Unexpected geometry unit.", geometry.unit);
7187
+ }
7188
+ const centerScene = this.canvasService.toScenePoint({
7189
+ x: geometry.x,
7190
+ y: geometry.y
6333
7191
  });
6334
- layer.add(leftText);
6335
- this.canvasService.canvas.bringObjectToFront(layer);
6336
- this.canvasService.canvas.requestRenderAll();
7192
+ const widthScene = this.canvasService.toSceneLength(geometry.width);
7193
+ const heightScene = this.canvasService.toSceneLength(geometry.height);
7194
+ const rulerLeft = centerScene.x - widthScene / 2;
7195
+ const rulerTop = centerScene.y - heightScene / 2;
7196
+ const rulerRight = rulerLeft + widthScene;
7197
+ const rulerBottom = rulerTop + heightScene;
7198
+ const widthMm = widthScene;
7199
+ const heightMm = heightScene;
7200
+ const unit = sizeState.unit;
7201
+ const widthLabel = `${this.formatLengthMm(widthMm, unit)} ${unit}`;
7202
+ const heightLabel = `${this.formatLengthMm(heightMm, unit)} ${unit}`;
7203
+ const specs = this.buildRulerSpecs({
7204
+ left: rulerLeft,
7205
+ top: rulerTop,
7206
+ right: rulerRight,
7207
+ bottom: rulerBottom,
7208
+ widthLabel,
7209
+ heightLabel
7210
+ });
7211
+ this.log("render:geometry", {
7212
+ seq,
7213
+ left: rulerLeft,
7214
+ top: rulerTop,
7215
+ right: rulerRight,
7216
+ bottom: rulerBottom,
7217
+ widthScene,
7218
+ heightScene,
7219
+ widthMm,
7220
+ heightMm,
7221
+ specCount: specs.length
7222
+ });
7223
+ if (seq !== this.renderSeq) return;
7224
+ this.specs = specs;
7225
+ await this.canvasService.flushRenderFromProducers();
7226
+ if (seq !== this.renderSeq) return;
7227
+ this.canvasService.bringLayerToFront(RULER_LAYER_ID);
7228
+ this.canvasService.requestRenderAll();
7229
+ this.log("render:done", { seq });
6337
7230
  }
6338
7231
  };
6339
7232
 
@@ -6372,6 +7265,9 @@ var WhiteInkTool = class {
6372
7265
  this.printWithWhiteInk = true;
6373
7266
  this.previewImageVisible = true;
6374
7267
  this.renderSeq = 0;
7268
+ this.whiteSpecs = [];
7269
+ this.coverSpecs = [];
7270
+ this.overlaySpecs = [];
6375
7271
  this.onToolActivated = (event) => {
6376
7272
  const before = this.isToolActive;
6377
7273
  this.syncToolActiveFromWorkbench(event.id);
@@ -6409,12 +7305,25 @@ var WhiteInkTool = class {
6409
7305
  };
6410
7306
  }
6411
7307
  activate(context) {
7308
+ var _a;
6412
7309
  this.context = context;
6413
7310
  this.canvasService = context.services.get("CanvasService");
6414
7311
  if (!this.canvasService) {
6415
7312
  console.warn("CanvasService not found for WhiteInkTool");
6416
7313
  return;
6417
7314
  }
7315
+ (_a = this.renderProducerDisposable) == null ? void 0 : _a.dispose();
7316
+ this.renderProducerDisposable = this.canvasService.registerRenderProducer(
7317
+ this.id,
7318
+ () => ({
7319
+ rootLayerSpecs: {
7320
+ [WHITE_INK_OBJECT_LAYER_ID]: this.whiteSpecs,
7321
+ [WHITE_INK_COVER_LAYER_ID]: this.coverSpecs,
7322
+ [WHITE_INK_OVERLAY_LAYER_ID]: this.overlaySpecs
7323
+ }
7324
+ }),
7325
+ { priority: 260 }
7326
+ );
6418
7327
  context.eventBus.on("tool:activated", this.onToolActivated);
6419
7328
  context.eventBus.on("scene:layout:change", this.onSceneLayoutChanged);
6420
7329
  context.eventBus.on("object:added", this.onObjectAdded);
@@ -6480,7 +7389,7 @@ var WhiteInkTool = class {
6480
7389
  this.updateWhiteInks();
6481
7390
  }
6482
7391
  deactivate(context) {
6483
- var _a;
7392
+ var _a, _b;
6484
7393
  context.eventBus.off("tool:activated", this.onToolActivated);
6485
7394
  context.eventBus.off("scene:layout:change", this.onSceneLayoutChanged);
6486
7395
  context.eventBus.off("object:added", this.onObjectAdded);
@@ -6491,6 +7400,11 @@ var WhiteInkTool = class {
6491
7400
  this.dirtyTrackerDisposable = void 0;
6492
7401
  this.clearRenderedWhiteInks();
6493
7402
  this.applyImageVisibilityForWhiteInk(false);
7403
+ (_b = this.renderProducerDisposable) == null ? void 0 : _b.dispose();
7404
+ this.renderProducerDisposable = void 0;
7405
+ if (this.canvasService) {
7406
+ void this.canvasService.flushRenderFromProducers();
7407
+ }
6494
7408
  this.canvasService = void 0;
6495
7409
  this.context = void 0;
6496
7410
  }
@@ -6913,11 +7827,20 @@ var WhiteInkTool = class {
6913
7827
  if (!layout) {
6914
7828
  return { left: 0, top: 0, width: 0, height: 0 };
6915
7829
  }
6916
- return {
7830
+ return this.canvasService.toSceneRect({
6917
7831
  left: layout.cutRect.left,
6918
7832
  top: layout.cutRect.top,
6919
7833
  width: layout.cutRect.width,
6920
7834
  height: layout.cutRect.height
7835
+ });
7836
+ }
7837
+ toLayoutSceneRect(rect) {
7838
+ return {
7839
+ left: rect.left,
7840
+ top: rect.top,
7841
+ width: rect.width,
7842
+ height: rect.height,
7843
+ space: "scene"
6921
7844
  };
6922
7845
  }
6923
7846
  getImageObjects() {
@@ -6940,7 +7863,7 @@ var WhiteInkTool = class {
6940
7863
  return (_a = obj == null ? void 0 : obj._originalElement) == null ? void 0 : _a.src;
6941
7864
  }
6942
7865
  getImageSnapshot(obj) {
6943
- var _a;
7866
+ var _a, _b;
6944
7867
  if (!obj) return null;
6945
7868
  const src = this.getCurrentSrc(obj);
6946
7869
  if (!src) return null;
@@ -6948,14 +7871,18 @@ var WhiteInkTool = class {
6948
7871
  const width = Number((obj == null ? void 0 : obj.width) || 0);
6949
7872
  const height = Number((obj == null ? void 0 : obj.height) || 0);
6950
7873
  this.rememberSourceSize(src, { width, height });
7874
+ const sceneScale = ((_a = this.canvasService) == null ? void 0 : _a.getSceneScale()) || 1;
7875
+ const leftScreen = Number.isFinite(obj == null ? void 0 : obj.left) ? Number(obj.left) : 0;
7876
+ const topScreen = Number.isFinite(obj == null ? void 0 : obj.top) ? Number(obj.top) : 0;
7877
+ const scenePoint = this.canvasService ? this.canvasService.toScenePoint({ x: leftScreen, y: topScreen }) : { x: leftScreen, y: topScreen };
6951
7878
  return {
6952
- id: String(((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.id) || "image"),
7879
+ id: String(((_b = obj == null ? void 0 : obj.data) == null ? void 0 : _b.id) || "image"),
6953
7880
  src,
6954
7881
  element,
6955
- left: Number.isFinite(obj == null ? void 0 : obj.left) ? Number(obj.left) : 0,
6956
- top: Number.isFinite(obj == null ? void 0 : obj.top) ? Number(obj.top) : 0,
6957
- scaleX: Number.isFinite(obj == null ? void 0 : obj.scaleX) ? Number(obj.scaleX) : 1,
6958
- scaleY: Number.isFinite(obj == null ? void 0 : obj.scaleY) ? Number(obj.scaleY) : 1,
7882
+ left: scenePoint.x,
7883
+ top: scenePoint.y,
7884
+ scaleX: (Number.isFinite(obj == null ? void 0 : obj.scaleX) ? Number(obj.scaleX) : 1) / sceneScale,
7885
+ scaleY: (Number.isFinite(obj == null ? void 0 : obj.scaleY) ? Number(obj.scaleY) : 1) / sceneScale,
6959
7886
  angle: Number.isFinite(obj == null ? void 0 : obj.angle) ? Number(obj.angle) : 0,
6960
7887
  originX: typeof (obj == null ? void 0 : obj.originX) === "string" ? obj.originX : "center",
6961
7888
  originY: typeof (obj == null ? void 0 : obj.originY) === "string" ? obj.originY : "center",
@@ -7130,8 +8057,11 @@ var WhiteInkTool = class {
7130
8057
  var _a, _b;
7131
8058
  if (!this.isToolActive || !this.canvasService) return [];
7132
8059
  if (frame.width <= 0 || frame.height <= 0) return [];
7133
- const canvasW = this.canvasService.canvas.width || 0;
7134
- const canvasH = this.canvasService.canvas.height || 0;
8060
+ const viewport = this.canvasService.getSceneViewportRect();
8061
+ const canvasW = viewport.width || 0;
8062
+ const canvasH = viewport.height || 0;
8063
+ const canvasLeft = viewport.left || 0;
8064
+ const canvasTop = viewport.top || 0;
7135
8065
  const strokeColor = this.getConfig("image.frame.strokeColor", "#808080") || "#808080";
7136
8066
  const strokeWidthRaw = Number(
7137
8067
  (_a = this.getConfig("image.frame.strokeWidth", 2)) != null ? _a : 2
@@ -7143,21 +8073,42 @@ var WhiteInkTool = class {
7143
8073
  const innerBackground = this.getConfig("image.frame.innerBackground", "rgba(0,0,0,0)") || "rgba(0,0,0,0)";
7144
8074
  const strokeWidth = Number.isFinite(strokeWidthRaw) ? Math.max(0, strokeWidthRaw) : 2;
7145
8075
  const dashLength = Number.isFinite(dashLengthRaw) ? Math.max(1, dashLengthRaw) : 8;
7146
- const frameLeft = Math.max(0, Math.min(canvasW, frame.left));
7147
- const frameTop = Math.max(0, Math.min(canvasH, frame.top));
8076
+ const strokeWidthScene = this.canvasService.toSceneLength(strokeWidth);
8077
+ const dashLengthScene = this.canvasService.toSceneLength(dashLength);
8078
+ const frameLeft = Math.max(
8079
+ canvasLeft,
8080
+ Math.min(canvasLeft + canvasW, frame.left)
8081
+ );
8082
+ const frameTop = Math.max(
8083
+ canvasTop,
8084
+ Math.min(canvasTop + canvasH, frame.top)
8085
+ );
7148
8086
  const frameRight = Math.max(
7149
8087
  frameLeft,
7150
- Math.min(canvasW, frame.left + frame.width)
8088
+ Math.min(canvasLeft + canvasW, frame.left + frame.width)
7151
8089
  );
7152
8090
  const frameBottom = Math.max(
7153
8091
  frameTop,
7154
- Math.min(canvasH, frame.top + frame.height)
8092
+ Math.min(canvasTop + canvasH, frame.top + frame.height)
7155
8093
  );
7156
8094
  const visibleFrameH = Math.max(0, frameBottom - frameTop);
7157
- const topH = frameTop;
7158
- const bottomH = Math.max(0, canvasH - frameBottom);
7159
- const leftW = frameLeft;
7160
- const rightW = Math.max(0, canvasW - frameRight);
8095
+ const topH = Math.max(0, frameTop - canvasTop);
8096
+ const bottomH = Math.max(0, canvasTop + canvasH - frameBottom);
8097
+ const leftW = Math.max(0, frameLeft - canvasLeft);
8098
+ const rightW = Math.max(0, canvasLeft + canvasW - frameRight);
8099
+ const viewportRect = this.toLayoutSceneRect({
8100
+ left: canvasLeft,
8101
+ top: canvasTop,
8102
+ width: canvasW,
8103
+ height: canvasH
8104
+ });
8105
+ const visibleFrameBandRect = this.toLayoutSceneRect({
8106
+ left: canvasLeft,
8107
+ top: frameTop,
8108
+ width: canvasW,
8109
+ height: visibleFrameH
8110
+ });
8111
+ const frameRect = this.toLayoutSceneRect(frame);
7161
8112
  const maskSpecs = [
7162
8113
  {
7163
8114
  id: "white-ink.cropMask.top",
@@ -7167,13 +8118,17 @@ var WhiteInkTool = class {
7167
8118
  layerId: WHITE_INK_OVERLAY_LAYER_ID,
7168
8119
  type: "white-ink-mask"
7169
8120
  },
8121
+ layout: {
8122
+ reference: "custom",
8123
+ referenceRect: viewportRect,
8124
+ alignX: "start",
8125
+ alignY: "start",
8126
+ width: "100%",
8127
+ height: topH
8128
+ },
7170
8129
  props: {
7171
- left: canvasW / 2,
7172
- top: topH / 2,
7173
- width: canvasW,
7174
- height: topH,
7175
- originX: "center",
7176
- originY: "center",
8130
+ originX: "left",
8131
+ originY: "top",
7177
8132
  fill: outerBackground,
7178
8133
  selectable: false,
7179
8134
  evented: false,
@@ -7188,13 +8143,17 @@ var WhiteInkTool = class {
7188
8143
  layerId: WHITE_INK_OVERLAY_LAYER_ID,
7189
8144
  type: "white-ink-mask"
7190
8145
  },
8146
+ layout: {
8147
+ reference: "custom",
8148
+ referenceRect: viewportRect,
8149
+ alignX: "start",
8150
+ alignY: "end",
8151
+ width: "100%",
8152
+ height: bottomH
8153
+ },
7191
8154
  props: {
7192
- left: canvasW / 2,
7193
- top: frameBottom + bottomH / 2,
7194
- width: canvasW,
7195
- height: bottomH,
7196
- originX: "center",
7197
- originY: "center",
8155
+ originX: "left",
8156
+ originY: "top",
7198
8157
  fill: outerBackground,
7199
8158
  selectable: false,
7200
8159
  evented: false,
@@ -7209,13 +8168,17 @@ var WhiteInkTool = class {
7209
8168
  layerId: WHITE_INK_OVERLAY_LAYER_ID,
7210
8169
  type: "white-ink-mask"
7211
8170
  },
7212
- props: {
7213
- left: leftW / 2,
7214
- top: frameTop + visibleFrameH / 2,
8171
+ layout: {
8172
+ reference: "custom",
8173
+ referenceRect: visibleFrameBandRect,
8174
+ alignX: "start",
8175
+ alignY: "start",
7215
8176
  width: leftW,
7216
- height: visibleFrameH,
7217
- originX: "center",
7218
- originY: "center",
8177
+ height: "100%"
8178
+ },
8179
+ props: {
8180
+ originX: "left",
8181
+ originY: "top",
7219
8182
  fill: outerBackground,
7220
8183
  selectable: false,
7221
8184
  evented: false,
@@ -7230,13 +8193,17 @@ var WhiteInkTool = class {
7230
8193
  layerId: WHITE_INK_OVERLAY_LAYER_ID,
7231
8194
  type: "white-ink-mask"
7232
8195
  },
7233
- props: {
7234
- left: frameRight + rightW / 2,
7235
- top: frameTop + visibleFrameH / 2,
8196
+ layout: {
8197
+ reference: "custom",
8198
+ referenceRect: visibleFrameBandRect,
8199
+ alignX: "end",
8200
+ alignY: "start",
7236
8201
  width: rightW,
7237
- height: visibleFrameH,
7238
- originX: "center",
7239
- originY: "center",
8202
+ height: "100%"
8203
+ },
8204
+ props: {
8205
+ originX: "left",
8206
+ originY: "top",
7240
8207
  fill: outerBackground,
7241
8208
  selectable: false,
7242
8209
  evented: false,
@@ -7254,17 +8221,21 @@ var WhiteInkTool = class {
7254
8221
  layerId: WHITE_INK_OVERLAY_LAYER_ID,
7255
8222
  type: "white-ink-frame"
7256
8223
  },
8224
+ layout: {
8225
+ reference: "custom",
8226
+ referenceRect: frameRect,
8227
+ alignX: "start",
8228
+ alignY: "start",
8229
+ width: "100%",
8230
+ height: "100%"
8231
+ },
7257
8232
  props: {
7258
- left: frame.left + frame.width / 2,
7259
- top: frame.top + frame.height / 2,
7260
- width: frame.width,
7261
- height: frame.height,
7262
- originX: "center",
7263
- originY: "center",
8233
+ originX: "left",
8234
+ originY: "top",
7264
8235
  fill: innerBackground,
7265
8236
  stroke: strokeColor,
7266
- strokeWidth,
7267
- strokeDashArray: [dashLength, dashLength],
8237
+ strokeWidth: strokeWidthScene,
8238
+ strokeDashArray: [dashLengthScene, dashLengthScene],
7268
8239
  selectable: false,
7269
8240
  evented: false,
7270
8241
  excludeFromExport: true
@@ -7340,50 +8311,30 @@ var WhiteInkTool = class {
7340
8311
  }
7341
8312
  ).filter((index) => index >= 0);
7342
8313
  let whiteInsertIndex = imageIndexes.length ? Math.min(...imageIndexes) : this.resolveDefaultInsertIndex(currentObjects);
7343
- whiteObjects.forEach((obj) => {
7344
- canvas.moveObjectTo(obj, whiteInsertIndex);
7345
- whiteInsertIndex += 1;
7346
- });
7347
- const afterWhiteObjects = canvas.getObjects();
7348
- const afterImageIndexes = afterWhiteObjects.map(
7349
- (obj, index) => {
7350
- var _a;
7351
- return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === IMAGE_OBJECT_LAYER_ID3 ? index : -1;
7352
- }
7353
- ).filter((index) => index >= 0);
7354
- let coverInsertIndex = afterImageIndexes.length ? Math.max(...afterImageIndexes) + 1 : whiteInsertIndex;
8314
+ let coverInsertIndex = whiteInsertIndex;
7355
8315
  coverObjects.forEach((obj) => {
7356
8316
  canvas.moveObjectTo(obj, coverInsertIndex);
7357
8317
  coverInsertIndex += 1;
7358
8318
  });
8319
+ whiteInsertIndex = coverInsertIndex;
8320
+ whiteObjects.forEach((obj) => {
8321
+ canvas.moveObjectTo(obj, whiteInsertIndex);
8322
+ whiteInsertIndex += 1;
8323
+ });
7359
8324
  frameObjects.forEach((obj) => canvas.bringObjectToFront(obj));
7360
8325
  canvas.getObjects().filter((obj) => {
7361
8326
  var _a;
7362
8327
  return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === IMAGE_OVERLAY_LAYER_ID2;
7363
8328
  }).forEach((obj) => canvas.bringObjectToFront(obj));
7364
- const dielineOverlay = this.canvasService.getLayer("dieline-overlay");
7365
- if (dielineOverlay) {
7366
- canvas.bringObjectToFront(dielineOverlay);
7367
- }
7368
- const rulerOverlay = this.canvasService.getLayer("ruler-overlay");
7369
- if (rulerOverlay) {
7370
- canvas.bringObjectToFront(rulerOverlay);
7371
- }
8329
+ this.canvasService.bringLayerToFront("dieline-overlay");
8330
+ this.canvasService.bringLayerToFront("ruler-overlay");
7372
8331
  }
7373
8332
  clearRenderedWhiteInks() {
7374
8333
  if (!this.canvasService) return;
7375
- void this.canvasService.applyObjectSpecsToRootLayer(
7376
- WHITE_INK_OBJECT_LAYER_ID,
7377
- []
7378
- );
7379
- void this.canvasService.applyObjectSpecsToRootLayer(
7380
- WHITE_INK_COVER_LAYER_ID,
7381
- []
7382
- );
7383
- void this.canvasService.applyObjectSpecsToRootLayer(
7384
- WHITE_INK_OVERLAY_LAYER_ID,
7385
- []
7386
- );
8334
+ this.whiteSpecs = [];
8335
+ this.coverSpecs = [];
8336
+ this.overlaySpecs = [];
8337
+ this.canvasService.requestRenderFromProducers();
7387
8338
  }
7388
8339
  purgeSourceCaches(item) {
7389
8340
  const sourceUrl = this.resolveSourceUrl(item);
@@ -7450,20 +8401,12 @@ var WhiteInkTool = class {
7450
8401
  }
7451
8402
  }
7452
8403
  }
7453
- await this.canvasService.applyObjectSpecsToRootLayer(
7454
- WHITE_INK_OBJECT_LAYER_ID,
7455
- whiteSpecs
7456
- );
8404
+ this.whiteSpecs = whiteSpecs;
7457
8405
  if (seq !== this.renderSeq) return;
7458
- await this.canvasService.applyObjectSpecsToRootLayer(
7459
- WHITE_INK_COVER_LAYER_ID,
7460
- coverSpecs
7461
- );
8406
+ this.coverSpecs = coverSpecs;
7462
8407
  if (seq !== this.renderSeq) return;
7463
- await this.canvasService.applyObjectSpecsToRootLayer(
7464
- WHITE_INK_OVERLAY_LAYER_ID,
7465
- frameSpecs
7466
- );
8408
+ this.overlaySpecs = frameSpecs;
8409
+ await this.canvasService.flushRenderFromProducers();
7467
8410
  if (seq !== this.renderSeq) return;
7468
8411
  this.syncZOrder();
7469
8412
  this.canvasService.requestRenderAll();
@@ -7696,23 +8639,16 @@ var SceneVisibilityService = class {
7696
8639
  const dielineLayer = this.canvasService.getLayer("dieline-overlay");
7697
8640
  if (dielineLayer) {
7698
8641
  const visible = !HIDDEN_DIELINE_TOOLS.has(this.activeToolId || "");
7699
- if (dielineLayer.visible !== visible) {
7700
- dielineLayer.set({ visible });
7701
- }
7702
- }
7703
- const rulerLayer = this.canvasService.getLayer("ruler-overlay");
7704
- if (rulerLayer) {
7705
- const visible = !HIDDEN_RULER_TOOLS.has(this.activeToolId || "");
7706
- if (rulerLayer.visible !== visible) {
7707
- rulerLayer.set({ visible });
7708
- }
8642
+ this.canvasService.setLayerVisibility("dieline-overlay", visible);
7709
8643
  }
8644
+ const rulerVisible = !HIDDEN_RULER_TOOLS.has(this.activeToolId || "");
8645
+ this.canvasService.setLayerVisibility("ruler-overlay", rulerVisible);
7710
8646
  this.canvasService.requestRenderAll();
7711
8647
  }
7712
8648
  };
7713
8649
 
7714
8650
  // src/services/CanvasService.ts
7715
- import { Canvas, Group as Group3, Rect as Rect4, Path as Path2, Image as Image4 } from "fabric";
8651
+ import { Canvas, Group, Rect, Path as Path2, Image as Image2, Text } from "fabric";
7716
8652
 
7717
8653
  // src/services/ViewportSystem.ts
7718
8654
  var ViewportSystem = class {
@@ -7790,6 +8726,13 @@ var ViewportSystem = class {
7790
8726
  // src/services/CanvasService.ts
7791
8727
  var CanvasService = class {
7792
8728
  constructor(el, options) {
8729
+ this.renderProducers = /* @__PURE__ */ new Map();
8730
+ this.producerOrder = 0;
8731
+ this.producerFlushRequested = false;
8732
+ this.producerLoopPending = false;
8733
+ this.producerLoopPromise = null;
8734
+ this.managedProducerLayerIds = /* @__PURE__ */ new Set();
8735
+ this.managedProducerRootLayerIds = /* @__PURE__ */ new Set();
7793
8736
  if (el instanceof Canvas) {
7794
8737
  this.canvas = el;
7795
8738
  } else {
@@ -7822,8 +8765,156 @@ var CanvasService = class {
7822
8765
  this.canvas.on("object:removed", forward("object:removed"));
7823
8766
  }
7824
8767
  dispose() {
8768
+ this.renderProducers.clear();
8769
+ this.managedProducerLayerIds.clear();
8770
+ this.managedProducerRootLayerIds.clear();
8771
+ this.producerFlushRequested = false;
7825
8772
  this.canvas.dispose();
7826
8773
  }
8774
+ registerRenderProducer(toolId, producer, options = {}) {
8775
+ const normalizedToolId = String(toolId || "").trim();
8776
+ if (!normalizedToolId) {
8777
+ throw new Error(
8778
+ "[CanvasService] registerRenderProducer requires a toolId."
8779
+ );
8780
+ }
8781
+ if (typeof producer !== "function") {
8782
+ throw new Error(
8783
+ `[CanvasService] registerRenderProducer("${normalizedToolId}") requires a producer function.`
8784
+ );
8785
+ }
8786
+ const entry = {
8787
+ toolId: normalizedToolId,
8788
+ producer,
8789
+ priority: Number.isFinite(options.priority) ? Number(options.priority) : 0,
8790
+ order: this.producerOrder++
8791
+ };
8792
+ this.renderProducers.set(normalizedToolId, entry);
8793
+ this.requestRenderFromProducers();
8794
+ return {
8795
+ dispose: () => {
8796
+ this.unregisterRenderProducer(normalizedToolId);
8797
+ }
8798
+ };
8799
+ }
8800
+ unregisterRenderProducer(toolId) {
8801
+ const normalizedToolId = String(toolId || "").trim();
8802
+ if (!normalizedToolId) return false;
8803
+ const removed = this.renderProducers.delete(normalizedToolId);
8804
+ if (removed) {
8805
+ this.requestRenderFromProducers();
8806
+ }
8807
+ return removed;
8808
+ }
8809
+ requestRenderFromProducers() {
8810
+ this.producerFlushRequested = true;
8811
+ this.scheduleProducerLoop();
8812
+ }
8813
+ async flushRenderFromProducers() {
8814
+ this.requestRenderFromProducers();
8815
+ if (this.producerLoopPromise) {
8816
+ await this.producerLoopPromise;
8817
+ }
8818
+ }
8819
+ scheduleProducerLoop() {
8820
+ if (this.producerLoopPending) return;
8821
+ this.producerLoopPending = true;
8822
+ this.producerLoopPromise = Promise.resolve().then(() => this.runProducerLoop()).catch((error) => {
8823
+ console.error("[CanvasService] render producer loop failed.", error);
8824
+ }).finally(() => {
8825
+ this.producerLoopPending = false;
8826
+ if (this.producerFlushRequested) {
8827
+ this.scheduleProducerLoop();
8828
+ }
8829
+ });
8830
+ }
8831
+ async runProducerLoop() {
8832
+ while (this.producerFlushRequested) {
8833
+ this.producerFlushRequested = false;
8834
+ await this.collectAndApplyProducerSpecs();
8835
+ }
8836
+ }
8837
+ sortedRenderProducerEntries() {
8838
+ return Array.from(this.renderProducers.values()).sort((a, b) => {
8839
+ if (a.priority !== b.priority) {
8840
+ return a.priority - b.priority;
8841
+ }
8842
+ if (a.order !== b.order) {
8843
+ return a.order - b.order;
8844
+ }
8845
+ return a.toolId.localeCompare(b.toolId);
8846
+ });
8847
+ }
8848
+ appendLayerSpecMap(map, source) {
8849
+ if (!source) return;
8850
+ Object.entries(source).forEach(([layerId, specs]) => {
8851
+ if (!Array.isArray(specs)) return;
8852
+ const list = map.get(layerId) || [];
8853
+ list.push(...specs);
8854
+ map.set(layerId, list);
8855
+ });
8856
+ }
8857
+ async collectAndApplyProducerSpecs() {
8858
+ const groupLayerSpecs = /* @__PURE__ */ new Map();
8859
+ const rootLayerSpecs = /* @__PURE__ */ new Map();
8860
+ const replaceLayerIds = /* @__PURE__ */ new Set();
8861
+ const replaceRootLayerIds = /* @__PURE__ */ new Set();
8862
+ const entries = this.sortedRenderProducerEntries();
8863
+ for (const entry of entries) {
8864
+ try {
8865
+ const result = await entry.producer();
8866
+ if (!result) continue;
8867
+ this.appendLayerSpecMap(groupLayerSpecs, result.layerSpecs);
8868
+ this.appendLayerSpecMap(rootLayerSpecs, result.rootLayerSpecs);
8869
+ if (Array.isArray(result.replaceLayerIds)) {
8870
+ result.replaceLayerIds.forEach((layerId) => {
8871
+ if (layerId) replaceLayerIds.add(layerId);
8872
+ });
8873
+ }
8874
+ if (Array.isArray(result.replaceRootLayerIds)) {
8875
+ result.replaceRootLayerIds.forEach((layerId) => {
8876
+ if (layerId) replaceRootLayerIds.add(layerId);
8877
+ });
8878
+ }
8879
+ } catch (error) {
8880
+ console.error(
8881
+ `[CanvasService] render producer "${entry.toolId}" failed.`,
8882
+ error
8883
+ );
8884
+ }
8885
+ }
8886
+ const nextLayerIds = new Set(groupLayerSpecs.keys());
8887
+ const nextRootLayerIds = new Set(rootLayerSpecs.keys());
8888
+ for (const [layerId, specs] of groupLayerSpecs.entries()) {
8889
+ if (replaceLayerIds.has(layerId)) {
8890
+ const layer = this.getLayer(layerId);
8891
+ if (layer) {
8892
+ layer.getObjects().forEach((obj) => layer.remove(obj));
8893
+ }
8894
+ }
8895
+ await this.applyObjectSpecsToLayer(layerId, specs, { render: false });
8896
+ }
8897
+ for (const layerId of this.managedProducerLayerIds) {
8898
+ if (nextLayerIds.has(layerId)) continue;
8899
+ const layer = this.getLayer(layerId);
8900
+ if (!layer) continue;
8901
+ await this.applyObjectSpecsToContainer(layer, [], { render: false });
8902
+ }
8903
+ for (const [layerId, specs] of rootLayerSpecs.entries()) {
8904
+ if (replaceRootLayerIds.has(layerId)) {
8905
+ const existing = this.getRootLayerObjects(layerId);
8906
+ existing.forEach((obj) => this.canvas.remove(obj));
8907
+ }
8908
+ await this.applyObjectSpecsToRootLayer(layerId, specs, { render: false });
8909
+ }
8910
+ for (const layerId of this.managedProducerRootLayerIds) {
8911
+ if (nextRootLayerIds.has(layerId)) continue;
8912
+ await this.applyObjectSpecsToRootLayer(layerId, [], { render: false });
8913
+ }
8914
+ this.managedProducerLayerIds = nextLayerIds;
8915
+ this.managedProducerRootLayerIds = nextRootLayerIds;
8916
+ this.requestRenderAll();
8917
+ }
7827
8918
  /**
7828
8919
  * Get a layer (Group) by its ID.
7829
8920
  * We assume layers are Groups directly on the canvas with a data.id property.
@@ -7846,7 +8937,7 @@ var CanvasService = class {
7846
8937
  ...options,
7847
8938
  data: { ...options.data, id }
7848
8939
  };
7849
- layer = new Group3([], defaultOptions);
8940
+ layer = new Group([], defaultOptions);
7850
8941
  this.canvas.add(layer);
7851
8942
  }
7852
8943
  return layer;
@@ -7878,13 +8969,206 @@ var CanvasService = class {
7878
8969
  (_a = this.eventBus) == null ? void 0 : _a.emit("canvas:resized", { width, height });
7879
8970
  this.requestRenderAll();
7880
8971
  }
8972
+ getSceneScale() {
8973
+ const scale = Number(this.viewport.scale);
8974
+ return Number.isFinite(scale) && scale > 0 ? scale : 1;
8975
+ }
8976
+ getSceneOffset() {
8977
+ const offset = this.viewport.offset;
8978
+ const x = Number(offset.x);
8979
+ const y = Number(offset.y);
8980
+ return {
8981
+ x: Number.isFinite(x) ? x : 0,
8982
+ y: Number.isFinite(y) ? y : 0
8983
+ };
8984
+ }
8985
+ toScreenPoint(point) {
8986
+ const scale = this.getSceneScale();
8987
+ const offset = this.getSceneOffset();
8988
+ return {
8989
+ x: point.x * scale + offset.x,
8990
+ y: point.y * scale + offset.y
8991
+ };
8992
+ }
8993
+ toScenePoint(point) {
8994
+ const scale = this.getSceneScale();
8995
+ const offset = this.getSceneOffset();
8996
+ return {
8997
+ x: (point.x - offset.x) / scale,
8998
+ y: (point.y - offset.y) / scale
8999
+ };
9000
+ }
9001
+ toScreenLength(value) {
9002
+ return value * this.getSceneScale();
9003
+ }
9004
+ toSceneLength(value) {
9005
+ return value / this.getSceneScale();
9006
+ }
9007
+ toScreenRect(rect) {
9008
+ const start = this.toScreenPoint({ x: rect.left, y: rect.top });
9009
+ return {
9010
+ left: start.x,
9011
+ top: start.y,
9012
+ width: this.toScreenLength(rect.width),
9013
+ height: this.toScreenLength(rect.height)
9014
+ };
9015
+ }
9016
+ toSceneRect(rect) {
9017
+ const start = this.toScenePoint({ x: rect.left, y: rect.top });
9018
+ return {
9019
+ left: start.x,
9020
+ top: start.y,
9021
+ width: this.toSceneLength(rect.width),
9022
+ height: this.toSceneLength(rect.height)
9023
+ };
9024
+ }
9025
+ getSceneViewportRect() {
9026
+ const width = Number(this.canvas.width || 0);
9027
+ const height = Number(this.canvas.height || 0);
9028
+ return this.toSceneRect({ left: 0, top: 0, width, height });
9029
+ }
9030
+ getScreenViewportRect() {
9031
+ return {
9032
+ left: 0,
9033
+ top: 0,
9034
+ width: Number(this.canvas.width || 0),
9035
+ height: Number(this.canvas.height || 0)
9036
+ };
9037
+ }
9038
+ toSpaceRect(rect, from, to) {
9039
+ if (from === to) return { ...rect };
9040
+ if (from === "scene") {
9041
+ return this.toScreenRect(rect);
9042
+ }
9043
+ return this.toSceneRect(rect);
9044
+ }
9045
+ resolveLayoutLength(value, base) {
9046
+ if (typeof value === "number") {
9047
+ return Number.isFinite(value) ? value : void 0;
9048
+ }
9049
+ if (typeof value !== "string") {
9050
+ return void 0;
9051
+ }
9052
+ const raw = value.trim();
9053
+ if (!raw) return void 0;
9054
+ if (raw.endsWith("%")) {
9055
+ const percent = parseFloat(raw.slice(0, -1));
9056
+ if (!Number.isFinite(percent)) return void 0;
9057
+ return base * percent / 100;
9058
+ }
9059
+ const parsed = parseFloat(raw);
9060
+ return Number.isFinite(parsed) ? parsed : void 0;
9061
+ }
9062
+ resolveLayoutInsets(inset, reference) {
9063
+ var _a, _b, _c, _d, _e;
9064
+ if (typeof inset === "number" || typeof inset === "string") {
9065
+ const all = (_a = this.resolveLayoutLength(
9066
+ inset,
9067
+ Math.min(reference.width, reference.height)
9068
+ )) != null ? _a : 0;
9069
+ return { top: all, right: all, bottom: all, left: all };
9070
+ }
9071
+ const source = inset || {};
9072
+ const top = (_b = this.resolveLayoutLength(source.top, reference.height)) != null ? _b : 0;
9073
+ const right = (_c = this.resolveLayoutLength(source.right, reference.width)) != null ? _c : 0;
9074
+ const bottom = (_d = this.resolveLayoutLength(source.bottom, reference.height)) != null ? _d : 0;
9075
+ const left = (_e = this.resolveLayoutLength(source.left, reference.width)) != null ? _e : 0;
9076
+ return { top, right, bottom, left };
9077
+ }
9078
+ resolveLayoutReferenceRect(layout, space) {
9079
+ if (layout.referenceRect) {
9080
+ const sourceSpace = layout.referenceRect.space || space;
9081
+ return this.toSpaceRect(layout.referenceRect, sourceSpace, space);
9082
+ }
9083
+ const reference = layout.reference || "sceneViewport";
9084
+ if (reference === "screenViewport") {
9085
+ const screenRect = this.getScreenViewportRect();
9086
+ return space === "screen" ? screenRect : this.toSceneRect(screenRect);
9087
+ }
9088
+ const sceneRect = this.getSceneViewportRect();
9089
+ return space === "scene" ? sceneRect : this.toScreenRect(sceneRect);
9090
+ }
9091
+ alignFactor(value) {
9092
+ if (value === "end") return 1;
9093
+ if (value === "center") return 0.5;
9094
+ return 0;
9095
+ }
9096
+ normalizeOriginX(value) {
9097
+ if (value === "center") return "center";
9098
+ if (value === "right") return "right";
9099
+ return "left";
9100
+ }
9101
+ normalizeOriginY(value) {
9102
+ if (value === "center") return "center";
9103
+ if (value === "bottom") return "bottom";
9104
+ return "top";
9105
+ }
9106
+ originFactor(value) {
9107
+ if (value === "center") return 0.5;
9108
+ if (value === "right" || value === "bottom") return 1;
9109
+ return 0;
9110
+ }
9111
+ resolveLayoutProps(spec, props) {
9112
+ var _a, _b, _c, _d;
9113
+ const layout = spec.layout;
9114
+ if (!layout) {
9115
+ return { ...props };
9116
+ }
9117
+ const space = spec.space || "scene";
9118
+ const reference = this.resolveLayoutReferenceRect(layout, space);
9119
+ const inset = this.resolveLayoutInsets(layout.inset, reference);
9120
+ const area = {
9121
+ left: reference.left + inset.left,
9122
+ top: reference.top + inset.top,
9123
+ width: Math.max(0, reference.width - inset.left - inset.right),
9124
+ height: Math.max(0, reference.height - inset.top - inset.bottom)
9125
+ };
9126
+ const next = { ...props };
9127
+ const width = (_a = this.resolveLayoutLength(layout.width, area.width)) != null ? _a : Number.isFinite(next.width) ? Number(next.width) : void 0;
9128
+ const height = (_b = this.resolveLayoutLength(layout.height, area.height)) != null ? _b : Number.isFinite(next.height) ? Number(next.height) : void 0;
9129
+ if (width !== void 0) next.width = width;
9130
+ if (height !== void 0) next.height = height;
9131
+ const alignX = this.alignFactor(layout.alignX);
9132
+ const alignY = this.alignFactor(layout.alignY);
9133
+ const offsetX = (_c = this.resolveLayoutLength(layout.offsetX, area.width)) != null ? _c : 0;
9134
+ const offsetY = (_d = this.resolveLayoutLength(layout.offsetY, area.height)) != null ? _d : 0;
9135
+ const objectWidth = Number.isFinite(next.width) ? Number(next.width) : 0;
9136
+ const objectHeight = Number.isFinite(next.height) ? Number(next.height) : 0;
9137
+ const objectLeft = area.left + (area.width - objectWidth) * alignX + offsetX;
9138
+ const objectTop = area.top + (area.height - objectHeight) * alignY + offsetY;
9139
+ const originX = this.normalizeOriginX(next.originX);
9140
+ const originY = this.normalizeOriginY(next.originY);
9141
+ next.left = objectLeft + objectWidth * this.originFactor(originX);
9142
+ next.top = objectTop + objectHeight * this.originFactor(originY);
9143
+ return next;
9144
+ }
9145
+ setLayerVisibility(layerId, visible) {
9146
+ const layer = this.getLayer(layerId);
9147
+ if (layer) {
9148
+ layer.set({ visible });
9149
+ }
9150
+ const objects = this.getRootLayerObjects(layerId);
9151
+ objects.forEach((obj) => {
9152
+ var _a, _b;
9153
+ (_a = obj.set) == null ? void 0 : _a.call(obj, { visible });
9154
+ (_b = obj.setCoords) == null ? void 0 : _b.call(obj);
9155
+ });
9156
+ }
9157
+ bringLayerToFront(layerId) {
9158
+ const layer = this.getLayer(layerId);
9159
+ if (layer) {
9160
+ this.canvas.bringObjectToFront(layer);
9161
+ }
9162
+ const objects = this.getRootLayerObjects(layerId);
9163
+ objects.forEach((obj) => this.canvas.bringObjectToFront(obj));
9164
+ }
7881
9165
  async applyLayerSpec(spec) {
7882
9166
  const layer = this.createLayer(spec.id, spec.props || {});
7883
9167
  await this.applyObjectSpecsToContainer(layer, spec.objects);
7884
9168
  }
7885
- async applyObjectSpecsToLayer(layerId, objects) {
9169
+ async applyObjectSpecsToLayer(layerId, objects, options = {}) {
7886
9170
  const layer = this.createLayer(layerId, {});
7887
- await this.applyObjectSpecsToContainer(layer, objects);
9171
+ await this.applyObjectSpecsToContainer(layer, objects, options);
7888
9172
  }
7889
9173
  getRootLayerObjects(layerId) {
7890
9174
  return this.canvas.getObjects().filter((obj) => {
@@ -7892,7 +9176,7 @@ var CanvasService = class {
7892
9176
  return ((_a = obj == null ? void 0 : obj.data) == null ? void 0 : _a.layerId) === layerId;
7893
9177
  });
7894
9178
  }
7895
- async applyObjectSpecsToRootLayer(layerId, specs) {
9179
+ async applyObjectSpecsToRootLayer(layerId, specs, options = {}) {
7896
9180
  const desiredIds = new Set(specs.map((s) => s.id));
7897
9181
  const existing = this.getRootLayerObjects(layerId);
7898
9182
  existing.forEach((obj) => {
@@ -7926,9 +9210,11 @@ var CanvasService = class {
7926
9210
  }
7927
9211
  this.patchFabricObject(current, spec, { layerId });
7928
9212
  }
7929
- this.requestRenderAll();
9213
+ if (options.render !== false) {
9214
+ this.requestRenderAll();
9215
+ }
7930
9216
  }
7931
- async applyObjectSpecsToContainer(container, specs) {
9217
+ async applyObjectSpecsToContainer(container, specs, options = {}) {
7932
9218
  const desiredIds = new Set(specs.map((s) => s.id));
7933
9219
  const existing = container.getObjects();
7934
9220
  existing.forEach((obj) => {
@@ -7964,7 +9250,9 @@ var CanvasService = class {
7964
9250
  this.moveObjectInContainer(container, current, index);
7965
9251
  }
7966
9252
  container.dirty = true;
7967
- this.requestRenderAll();
9253
+ if (options.render !== false) {
9254
+ this.requestRenderAll();
9255
+ }
7968
9256
  }
7969
9257
  patchFabricObject(obj, spec, extraData) {
7970
9258
  const nextData = {
@@ -7973,9 +9261,33 @@ var CanvasService = class {
7973
9261
  ...extraData || {},
7974
9262
  id: spec.id
7975
9263
  };
7976
- obj.set({ ...spec.props || {}, data: nextData });
9264
+ const props = this.resolveFabricProps(spec, spec.props || {});
9265
+ obj.set({ ...props, data: nextData });
7977
9266
  obj.setCoords();
7978
9267
  }
9268
+ resolveFabricProps(spec, props) {
9269
+ const space = spec.space || "scene";
9270
+ const next = this.resolveLayoutProps(spec, props);
9271
+ if (space === "screen") {
9272
+ return next;
9273
+ }
9274
+ const hasLeft = Number.isFinite(next.left);
9275
+ const hasTop = Number.isFinite(next.top);
9276
+ if (hasLeft || hasTop) {
9277
+ const mapped = this.toScreenPoint({
9278
+ x: hasLeft ? Number(next.left) : 0,
9279
+ y: hasTop ? Number(next.top) : 0
9280
+ });
9281
+ if (hasLeft) next.left = mapped.x;
9282
+ if (hasTop) next.top = mapped.y;
9283
+ }
9284
+ const rawScaleX = Number.isFinite(next.scaleX) ? Number(next.scaleX) : 1;
9285
+ const rawScaleY = Number.isFinite(next.scaleY) ? Number(next.scaleY) : 1;
9286
+ const sceneScale = this.getSceneScale();
9287
+ next.scaleX = rawScaleX * sceneScale;
9288
+ next.scaleY = rawScaleY * sceneScale;
9289
+ return next;
9290
+ }
7979
9291
  moveObjectInContainer(container, obj, index) {
7980
9292
  if (!obj) return;
7981
9293
  const moveObjectTo = container.moveObjectTo;
@@ -7995,10 +9307,11 @@ var CanvasService = class {
7995
9307
  }
7996
9308
  }
7997
9309
  async createFabricObject(spec) {
7998
- var _a, _b;
9310
+ var _a, _b, _c, _d;
7999
9311
  if (spec.type === "rect") {
8000
- const rect = new Rect4({
8001
- ...spec.props || {},
9312
+ const props = this.resolveFabricProps(spec, spec.props || {});
9313
+ const rect = new Rect({
9314
+ ...props,
8002
9315
  data: { ...spec.data || {}, id: spec.id }
8003
9316
  });
8004
9317
  rect.setCoords();
@@ -8007,8 +9320,9 @@ var CanvasService = class {
8007
9320
  if (spec.type === "path") {
8008
9321
  const pathData = ((_a = spec.props) == null ? void 0 : _a.path) || ((_b = spec.props) == null ? void 0 : _b.pathData);
8009
9322
  if (!pathData) return void 0;
9323
+ const props = this.resolveFabricProps(spec, spec.props || {});
8010
9324
  const path = new Path2(pathData, {
8011
- ...spec.props || {},
9325
+ ...props,
8012
9326
  data: { ...spec.data || {}, id: spec.id }
8013
9327
  });
8014
9328
  path.setCoords();
@@ -8016,14 +9330,25 @@ var CanvasService = class {
8016
9330
  }
8017
9331
  if (spec.type === "image") {
8018
9332
  if (!spec.src) return void 0;
8019
- const image = await Image4.fromURL(spec.src, { crossOrigin: "anonymous" });
9333
+ const image = await Image2.fromURL(spec.src, { crossOrigin: "anonymous" });
9334
+ const props = this.resolveFabricProps(spec, spec.props || {});
8020
9335
  image.set({
8021
- ...spec.props || {},
9336
+ ...props,
8022
9337
  data: { ...spec.data || {}, id: spec.id }
8023
9338
  });
8024
9339
  image.setCoords();
8025
9340
  return image;
8026
9341
  }
9342
+ if (spec.type === "text") {
9343
+ const content = String((_d = (_c = spec.props) == null ? void 0 : _c.text) != null ? _d : "");
9344
+ const props = this.resolveFabricProps(spec, spec.props || {});
9345
+ const text = new Text(content, {
9346
+ ...props,
9347
+ data: { ...spec.data || {}, id: spec.id }
9348
+ });
9349
+ text.setCoords();
9350
+ return text;
9351
+ }
8027
9352
  return void 0;
8028
9353
  }
8029
9354
  };