@pooder/kit 1.0.0 → 3.0.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.
package/dist/index.mjs CHANGED
@@ -1,111 +1,147 @@
1
1
  // src/background.ts
2
- import { Image, PooderLayer, Rect } from "@pooder/core";
2
+ import {
3
+ ContributionPointIds
4
+ } from "@pooder/core";
5
+ import { Rect, FabricImage as Image2 } from "fabric";
3
6
  var BackgroundTool = class {
4
- constructor() {
5
- this.name = "BackgroundTool";
6
- this.options = {
7
- color: "",
8
- url: ""
9
- };
10
- this.schema = {
11
- color: {
12
- type: "color",
13
- label: "Background Color"
14
- },
15
- url: {
16
- type: "string",
17
- label: "Image URL"
18
- }
7
+ constructor(options) {
8
+ this.id = "pooder.kit.background";
9
+ this.metadata = {
10
+ name: "BackgroundTool"
19
11
  };
20
- this.commands = {
21
- reset: {
22
- execute: (editor) => {
23
- this.updateBackground(editor, this.options);
24
- return true;
12
+ this.color = "";
13
+ this.url = "";
14
+ if (options) {
15
+ Object.assign(this, options);
16
+ }
17
+ }
18
+ activate(context) {
19
+ this.canvasService = context.services.get("CanvasService");
20
+ if (!this.canvasService) {
21
+ console.warn("CanvasService not found for BackgroundTool");
22
+ return;
23
+ }
24
+ const configService = context.services.get("ConfigurationService");
25
+ if (configService) {
26
+ this.color = configService.get("background.color", this.color);
27
+ this.url = configService.get("background.url", this.url);
28
+ configService.onAnyChange((e) => {
29
+ if (e.key.startsWith("background.")) {
30
+ const prop = e.key.split(".")[1];
31
+ console.log(
32
+ `[BackgroundTool] Config change detected: ${e.key} -> ${e.value}, prop: ${prop}`
33
+ );
34
+ if (prop && prop in this) {
35
+ console.log(
36
+ `[BackgroundTool] Updating option ${prop} to ${e.value}`
37
+ );
38
+ this[prop] = e.value;
39
+ this.updateBackground();
40
+ } else {
41
+ console.warn(
42
+ `[BackgroundTool] Property ${prop} not found in options`
43
+ );
44
+ }
25
45
  }
26
- },
27
- clear: {
28
- execute: (editor) => {
29
- this.options = {
30
- color: "transparent",
31
- url: ""
32
- };
33
- this.updateBackground(editor, this.options);
34
- return true;
46
+ });
47
+ }
48
+ this.initLayer();
49
+ this.updateBackground();
50
+ }
51
+ 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;
58
+ }
59
+ }
60
+ contribute() {
61
+ return {
62
+ [ContributionPointIds.CONFIGURATIONS]: [
63
+ {
64
+ id: "background.color",
65
+ type: "color",
66
+ label: "Background Color",
67
+ default: ""
68
+ },
69
+ {
70
+ id: "background.url",
71
+ type: "string",
72
+ label: "Image URL",
73
+ default: ""
35
74
  }
36
- },
37
- setBackgroundColor: {
38
- execute: (editor, color) => {
39
- if (this.options.color === color) return true;
40
- this.options.color = color;
41
- this.updateBackground(editor, this.options);
42
- return true;
75
+ ],
76
+ [ContributionPointIds.COMMANDS]: [
77
+ {
78
+ command: "reset",
79
+ title: "Reset Background",
80
+ handler: () => {
81
+ this.updateBackground();
82
+ return true;
83
+ }
43
84
  },
44
- schema: {
45
- color: {
46
- type: "string",
47
- // Should be 'color' if supported by CommandArgSchema, but using 'string' for now as per previous plan
48
- label: "Background Color",
49
- required: true
85
+ {
86
+ command: "clear",
87
+ title: "Clear Background",
88
+ handler: () => {
89
+ this.color = "transparent";
90
+ this.url = "";
91
+ this.updateBackground();
92
+ return true;
50
93
  }
51
- }
52
- },
53
- setBackgroundImage: {
54
- execute: (editor, url) => {
55
- if (this.options.url === url) return true;
56
- this.options.url = url;
57
- this.updateBackground(editor, this.options);
58
- return true;
59
94
  },
60
- schema: {
61
- url: {
62
- type: "string",
63
- label: "Image URL",
64
- required: true
95
+ {
96
+ command: "setBackgroundColor",
97
+ title: "Set Background Color",
98
+ handler: (color) => {
99
+ if (this.color === color) return true;
100
+ this.color = color;
101
+ this.updateBackground();
102
+ return true;
103
+ }
104
+ },
105
+ {
106
+ command: "setBackgroundImage",
107
+ title: "Set Background Image",
108
+ handler: (url) => {
109
+ if (this.url === url) return true;
110
+ this.url = url;
111
+ this.updateBackground();
112
+ return true;
65
113
  }
66
114
  }
67
- }
115
+ ]
68
116
  };
69
117
  }
70
- initLayer(editor) {
71
- let backgroundLayer = editor.getLayer("background");
118
+ initLayer() {
119
+ if (!this.canvasService) return;
120
+ let backgroundLayer = this.canvasService.getLayer("background");
72
121
  if (!backgroundLayer) {
73
- backgroundLayer = new PooderLayer([], {
74
- width: editor.canvas.width,
75
- height: editor.canvas.height,
122
+ backgroundLayer = this.canvasService.createLayer("background", {
123
+ width: this.canvasService.canvas.width,
124
+ height: this.canvasService.canvas.height,
76
125
  selectable: false,
77
- evented: false,
78
- data: {
79
- id: "background"
80
- }
126
+ evented: false
81
127
  });
82
- editor.canvas.add(backgroundLayer);
83
- editor.canvas.sendObjectToBack(backgroundLayer);
84
- }
85
- }
86
- onMount(editor) {
87
- this.initLayer(editor);
88
- this.updateBackground(editor, this.options);
89
- }
90
- onUnmount(editor) {
91
- const layer = editor.getLayer("background");
92
- if (layer) {
93
- editor.canvas.remove(layer);
128
+ this.canvasService.canvas.sendObjectToBack(backgroundLayer);
94
129
  }
95
130
  }
96
- onUpdate(editor, state) {
97
- this.updateBackground(editor, this.options);
98
- }
99
- async updateBackground(editor, options) {
100
- const layer = editor.getLayer("background");
131
+ async updateBackground() {
132
+ if (!this.canvasService) return;
133
+ const layer = this.canvasService.getLayer("background");
101
134
  if (!layer) {
102
135
  console.warn("[BackgroundTool] Background layer not found");
103
136
  return;
104
137
  }
105
- const { color, url } = options;
106
- const width = editor.state.width;
107
- const height = editor.state.height;
108
- let rect = editor.getObject("background-color-rect", "background");
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
+ );
109
145
  if (rect) {
110
146
  rect.set({
111
147
  fill: color
@@ -124,7 +160,10 @@ var BackgroundTool = class {
124
160
  layer.add(rect);
125
161
  layer.sendObjectToBack(rect);
126
162
  }
127
- let img = editor.getObject("background-image", "background");
163
+ let img = this.canvasService.getObject(
164
+ "background-image",
165
+ "background"
166
+ );
128
167
  try {
129
168
  if (img) {
130
169
  if (img.getSrc() !== url) {
@@ -136,7 +175,7 @@ var BackgroundTool = class {
136
175
  }
137
176
  } else {
138
177
  if (url) {
139
- img = await Image.fromURL(url, { crossOrigin: "anonymous" });
178
+ img = await Image2.fromURL(url, { crossOrigin: "anonymous" });
140
179
  img.set({
141
180
  originX: "left",
142
181
  originY: "top",
@@ -149,30 +188,259 @@ var BackgroundTool = class {
149
188
  }
150
189
  });
151
190
  img.scaleToWidth(width);
152
- if (img.getScaledHeight() < height)
153
- img.scaleToHeight(height);
191
+ if (img.getScaledHeight() < height) img.scaleToHeight(height);
154
192
  layer.add(img);
155
193
  }
156
194
  }
157
- editor.canvas.requestRenderAll();
195
+ this.canvasService.requestRenderAll();
158
196
  } catch (e) {
159
197
  console.error("[BackgroundTool] Failed to load image", e);
160
198
  }
199
+ layer.dirty = true;
200
+ this.canvasService.requestRenderAll();
161
201
  }
162
202
  };
163
203
 
164
204
  // src/dieline.ts
165
- import { Path, PooderLayer as PooderLayer2, Pattern } from "@pooder/core";
205
+ import {
206
+ ContributionPointIds as ContributionPointIds2
207
+ } from "@pooder/core";
208
+ import { Path, Pattern } from "fabric";
209
+
210
+ // src/tracer.ts
211
+ var ImageTracer = class {
212
+ /**
213
+ * Main entry point: Traces an image URL to an SVG path string.
214
+ * @param imageUrl The URL or Base64 string of the image.
215
+ * @param options Configuration options.
216
+ */
217
+ static async trace(imageUrl, options = {}) {
218
+ var _a, _b;
219
+ const img = await this.loadImage(imageUrl);
220
+ const width = img.width;
221
+ const height = img.height;
222
+ const canvas = document.createElement("canvas");
223
+ canvas.width = width;
224
+ canvas.height = height;
225
+ const ctx = canvas.getContext("2d");
226
+ if (!ctx) throw new Error("Could not get 2D context");
227
+ ctx.drawImage(img, 0, 0);
228
+ const imageData = ctx.getImageData(0, 0, width, height);
229
+ const points = this.marchingSquares(imageData, (_a = options.threshold) != null ? _a : 10);
230
+ let finalPoints = points;
231
+ if (options.scaleToWidth && options.scaleToHeight && points.length > 0) {
232
+ finalPoints = this.scalePoints(
233
+ points,
234
+ options.scaleToWidth,
235
+ options.scaleToHeight
236
+ );
237
+ }
238
+ const simplifiedPoints = this.douglasPeucker(
239
+ finalPoints,
240
+ (_b = options.simplifyTolerance) != null ? _b : 0.5
241
+ );
242
+ return this.pointsToSVG(simplifiedPoints);
243
+ }
244
+ static loadImage(url) {
245
+ return new Promise((resolve, reject) => {
246
+ const img = new Image();
247
+ img.crossOrigin = "Anonymous";
248
+ img.onload = () => resolve(img);
249
+ img.onerror = (e) => reject(e);
250
+ img.src = url;
251
+ });
252
+ }
253
+ /**
254
+ * Moore-Neighbor Tracing Algorithm
255
+ * More robust for irregular shapes than simple Marching Squares walker.
256
+ */
257
+ static marchingSquares(imageData, alphaThreshold) {
258
+ const width = imageData.width;
259
+ const height = imageData.height;
260
+ const data = imageData.data;
261
+ const isSolid = (x, y) => {
262
+ if (x < 0 || x >= width || y < 0 || y >= height) return false;
263
+ const index = (y * width + x) * 4;
264
+ const r = data[index];
265
+ const g = data[index + 1];
266
+ const b = data[index + 2];
267
+ const a = data[index + 3];
268
+ if (a <= alphaThreshold) return false;
269
+ if (r > 240 && g > 240 && b > 240) return false;
270
+ return true;
271
+ };
272
+ let startX = -1;
273
+ let startY = -1;
274
+ searchLoop: for (let y = 0; y < height; y++) {
275
+ for (let x = 0; x < width; x++) {
276
+ if (isSolid(x, y)) {
277
+ startX = x;
278
+ startY = y;
279
+ break searchLoop;
280
+ }
281
+ }
282
+ }
283
+ if (startX === -1) return [];
284
+ const points = [];
285
+ let cx = startX;
286
+ let cy = startY;
287
+ const neighbors = [
288
+ { x: 0, y: -1 },
289
+ { x: 1, y: -1 },
290
+ { x: 1, y: 0 },
291
+ { x: 1, y: 1 },
292
+ { x: 0, y: 1 },
293
+ { x: -1, y: 1 },
294
+ { x: -1, y: 0 },
295
+ { x: -1, y: -1 }
296
+ ];
297
+ let backtrack = 6;
298
+ const maxSteps = width * height * 3;
299
+ let steps = 0;
300
+ do {
301
+ points.push({ x: cx, y: cy });
302
+ let found = false;
303
+ for (let i = 0; i < 8; i++) {
304
+ const idx = (backtrack + 1 + i) % 8;
305
+ const nx = cx + neighbors[idx].x;
306
+ const ny = cy + neighbors[idx].y;
307
+ if (isSolid(nx, ny)) {
308
+ cx = nx;
309
+ cy = ny;
310
+ backtrack = (idx + 4) % 8;
311
+ backtrack = (idx + 4 + 1) % 8;
312
+ backtrack = (idx + 4 + 1) % 8;
313
+ found = true;
314
+ break;
315
+ }
316
+ }
317
+ if (!found) {
318
+ break;
319
+ }
320
+ steps++;
321
+ } while ((cx !== startX || cy !== startY) && steps < maxSteps);
322
+ return points;
323
+ }
324
+ /**
325
+ * Douglas-Peucker Line Simplification
326
+ */
327
+ static douglasPeucker(points, tolerance) {
328
+ if (points.length <= 2) return points;
329
+ const sqTolerance = tolerance * tolerance;
330
+ let maxSqDist = 0;
331
+ let index = 0;
332
+ const first = points[0];
333
+ const last = points[points.length - 1];
334
+ for (let i = 1; i < points.length - 1; i++) {
335
+ const sqDist = this.getSqSegDist(points[i], first, last);
336
+ if (sqDist > maxSqDist) {
337
+ index = i;
338
+ maxSqDist = sqDist;
339
+ }
340
+ }
341
+ if (maxSqDist > sqTolerance) {
342
+ const left = this.douglasPeucker(points.slice(0, index + 1), tolerance);
343
+ const right = this.douglasPeucker(points.slice(index), tolerance);
344
+ return left.slice(0, left.length - 1).concat(right);
345
+ } else {
346
+ return [first, last];
347
+ }
348
+ }
349
+ static getSqSegDist(p, p1, p2) {
350
+ let x = p1.x;
351
+ let y = p1.y;
352
+ let dx = p2.x - x;
353
+ let dy = p2.y - y;
354
+ if (dx !== 0 || dy !== 0) {
355
+ const t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy);
356
+ if (t > 1) {
357
+ x = p2.x;
358
+ y = p2.y;
359
+ } else if (t > 0) {
360
+ x += dx * t;
361
+ y += dy * t;
362
+ }
363
+ }
364
+ dx = p.x - x;
365
+ dy = p.y - y;
366
+ return dx * dx + dy * dy;
367
+ }
368
+ static scalePoints(points, targetWidth, targetHeight) {
369
+ if (points.length === 0) return points;
370
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
371
+ for (const p of points) {
372
+ if (p.x < minX) minX = p.x;
373
+ if (p.y < minY) minY = p.y;
374
+ if (p.x > maxX) maxX = p.x;
375
+ if (p.y > maxY) maxY = p.y;
376
+ }
377
+ const srcW = maxX - minX;
378
+ const srcH = maxY - minY;
379
+ if (srcW === 0 || srcH === 0) return points;
380
+ const scaleX = targetWidth / srcW;
381
+ const scaleY = targetHeight / srcH;
382
+ return points.map((p) => ({
383
+ x: (p.x - minX) * scaleX,
384
+ y: (p.y - minY) * scaleY
385
+ }));
386
+ }
387
+ static pointsToSVG(points) {
388
+ if (points.length === 0) return "";
389
+ const head = points[0];
390
+ const tail = points.slice(1);
391
+ return `M ${head.x} ${head.y} ` + tail.map((p) => `L ${p.x} ${p.y}`).join(" ") + " Z";
392
+ }
393
+ };
394
+
395
+ // src/coordinate.ts
396
+ var Coordinate = class {
397
+ /**
398
+ * Convert an absolute value to a normalized value (0-1).
399
+ * @param value Absolute value (e.g., pixels)
400
+ * @param total Total dimension size (e.g., canvas width)
401
+ */
402
+ static toNormalized(value, total) {
403
+ return total === 0 ? 0 : value / total;
404
+ }
405
+ /**
406
+ * Convert a normalized value (0-1) to an absolute value.
407
+ * @param normalized Normalized value (0-1)
408
+ * @param total Total dimension size (e.g., canvas width)
409
+ */
410
+ static toAbsolute(normalized, total) {
411
+ return normalized * total;
412
+ }
413
+ /**
414
+ * Normalize a point's coordinates.
415
+ */
416
+ static normalizePoint(point, size) {
417
+ return {
418
+ x: this.toNormalized(point.x, size.width),
419
+ y: this.toNormalized(point.y, size.height)
420
+ };
421
+ }
422
+ /**
423
+ * Denormalize a point's coordinates to absolute pixels.
424
+ */
425
+ static denormalizePoint(point, size) {
426
+ return {
427
+ x: this.toAbsolute(point.x, size.width),
428
+ y: this.toAbsolute(point.y, size.height)
429
+ };
430
+ }
431
+ };
166
432
 
167
433
  // src/geometry.ts
168
434
  import paper from "paper";
169
435
  function ensurePaper(width, height) {
170
436
  if (!paper.project) {
171
437
  paper.setup(new paper.Size(width, height));
438
+ } else {
439
+ paper.view.viewSize = new paper.Size(width, height);
172
440
  }
173
441
  }
174
442
  function createBaseShape(options) {
175
- const { shape, width, height, radius, x, y } = options;
443
+ const { shape, width, height, radius, x, y, pathData } = options;
176
444
  const center = new paper.Point(x, y);
177
445
  if (shape === "rect") {
178
446
  return new paper.Path.Rectangle({
@@ -186,11 +454,24 @@ function createBaseShape(options) {
186
454
  center,
187
455
  radius: Math.max(0, r)
188
456
  });
189
- } else {
457
+ } else if (shape === "ellipse") {
190
458
  return new paper.Path.Ellipse({
191
459
  center,
192
460
  radius: [Math.max(0, width / 2), Math.max(0, height / 2)]
193
461
  });
462
+ } else if (shape === "custom" && pathData) {
463
+ const path = new paper.Path();
464
+ path.pathData = pathData;
465
+ path.position = center;
466
+ if (width > 0 && height > 0 && path.bounds.width > 0 && path.bounds.height > 0) {
467
+ path.scale(width / path.bounds.width, height / path.bounds.height);
468
+ }
469
+ return path;
470
+ } else {
471
+ return new paper.Path.Rectangle({
472
+ point: [x - width / 2, y - height / 2],
473
+ size: [Math.max(0, width), Math.max(0, height)]
474
+ });
194
475
  }
195
476
  }
196
477
  function getDielineShape(options) {
@@ -204,10 +485,6 @@ function getDielineShape(options) {
204
485
  center: [hole.x, hole.y],
205
486
  radius: hole.outerRadius
206
487
  });
207
- if (!mainShape.intersects(lug) && !mainShape.contains(lug.position)) {
208
- lug.remove();
209
- return;
210
- }
211
488
  const cut = new paper.Path.Circle({
212
489
  center: [hole.x, hole.y],
213
490
  radius: hole.innerRadius
@@ -215,31 +492,49 @@ function getDielineShape(options) {
215
492
  if (!lugsPath) {
216
493
  lugsPath = lug;
217
494
  } else {
218
- const temp = lugsPath.unite(lug);
219
- lugsPath.remove();
220
- lug.remove();
221
- lugsPath = temp;
495
+ try {
496
+ const temp = lugsPath.unite(lug);
497
+ lugsPath.remove();
498
+ lug.remove();
499
+ lugsPath = temp;
500
+ } catch (e) {
501
+ console.error("Geometry: Failed to unite lug", e);
502
+ lug.remove();
503
+ }
222
504
  }
223
505
  if (!cutsPath) {
224
506
  cutsPath = cut;
225
507
  } else {
226
- const temp = cutsPath.unite(cut);
227
- cutsPath.remove();
228
- cut.remove();
229
- cutsPath = temp;
508
+ try {
509
+ const temp = cutsPath.unite(cut);
510
+ cutsPath.remove();
511
+ cut.remove();
512
+ cutsPath = temp;
513
+ } catch (e) {
514
+ console.error("Geometry: Failed to unite cut", e);
515
+ cut.remove();
516
+ }
230
517
  }
231
518
  });
232
519
  if (lugsPath) {
233
- const temp = mainShape.unite(lugsPath);
234
- mainShape.remove();
235
- lugsPath.remove();
236
- mainShape = temp;
520
+ try {
521
+ const temp = mainShape.unite(lugsPath);
522
+ mainShape.remove();
523
+ lugsPath.remove();
524
+ mainShape = temp;
525
+ } catch (e) {
526
+ console.error("Geometry: Failed to unite lugsPath to mainShape", e);
527
+ }
237
528
  }
238
529
  if (cutsPath) {
239
- const temp = mainShape.subtract(cutsPath);
240
- mainShape.remove();
241
- cutsPath.remove();
242
- mainShape = temp;
530
+ try {
531
+ const temp = mainShape.subtract(cutsPath);
532
+ mainShape.remove();
533
+ cutsPath.remove();
534
+ mainShape = temp;
535
+ } catch (e) {
536
+ console.error("Geometry: Failed to subtract cutsPath from mainShape", e);
537
+ }
243
538
  }
244
539
  }
245
540
  return mainShape;
@@ -273,13 +568,37 @@ function generateBleedZonePath(options, offset) {
273
568
  ensurePaper(maxDim, maxDim);
274
569
  paper.project.activeLayer.removeChildren();
275
570
  const shapeOriginal = getDielineShape(options);
276
- const offsetOptions = {
277
- ...options,
278
- width: Math.max(0, options.width + offset * 2),
279
- height: Math.max(0, options.height + offset * 2),
280
- radius: options.radius === 0 ? 0 : Math.max(0, options.radius + offset)
281
- };
282
- const shapeOffset = getDielineShape(offsetOptions);
571
+ let shapeOffset;
572
+ if (options.shape === "custom") {
573
+ const stroker = shapeOriginal.clone();
574
+ stroker.strokeColor = new paper.Color("black");
575
+ stroker.strokeWidth = Math.abs(offset) * 2;
576
+ stroker.strokeJoin = "round";
577
+ stroker.strokeCap = "round";
578
+ let expanded;
579
+ try {
580
+ expanded = stroker.expand({ stroke: true, fill: false, insert: false });
581
+ } catch (e) {
582
+ stroker.remove();
583
+ shapeOffset = shapeOriginal.clone();
584
+ return shapeOffset.pathData;
585
+ }
586
+ stroker.remove();
587
+ if (offset > 0) {
588
+ shapeOffset = shapeOriginal.unite(expanded);
589
+ } else {
590
+ shapeOffset = shapeOriginal.subtract(expanded);
591
+ }
592
+ expanded.remove();
593
+ } else {
594
+ const offsetOptions = {
595
+ ...options,
596
+ width: Math.max(0, options.width + offset * 2),
597
+ height: Math.max(0, options.height + offset * 2),
598
+ radius: options.radius === 0 ? 0 : Math.max(0, options.radius + offset)
599
+ };
600
+ shapeOffset = getDielineShape(offsetOptions);
601
+ }
283
602
  let bleedZone;
284
603
  if (offset > 0) {
285
604
  bleedZone = shapeOffset.subtract(shapeOriginal);
@@ -302,164 +621,301 @@ function getNearestPointOnDieline(point, options) {
302
621
  shape.remove();
303
622
  return result;
304
623
  }
624
+ function getPathBounds(pathData) {
625
+ const path = new paper.Path();
626
+ path.pathData = pathData;
627
+ const bounds = path.bounds;
628
+ path.remove();
629
+ return { width: bounds.width, height: bounds.height };
630
+ }
305
631
 
306
632
  // src/dieline.ts
307
633
  var DielineTool = class {
308
- constructor() {
309
- this.name = "DielineTool";
310
- this.options = {
311
- shape: "rect",
312
- width: 300,
313
- height: 300,
314
- radius: 0,
315
- offset: 0,
316
- style: "solid",
317
- insideColor: "rgba(0,0,0,0)",
318
- outsideColor: "#ffffff"
634
+ constructor(options) {
635
+ this.id = "pooder.kit.dieline";
636
+ this.metadata = {
637
+ name: "DielineTool"
319
638
  };
320
- this.schema = {
321
- shape: {
322
- type: "select",
323
- options: ["rect", "circle", "ellipse"],
324
- label: "Shape"
325
- },
326
- width: { type: "number", min: 10, max: 2e3, label: "Width" },
327
- height: { type: "number", min: 10, max: 2e3, label: "Height" },
328
- radius: { type: "number", min: 0, max: 500, label: "Corner Radius" },
329
- position: { type: "string", label: "Position" },
330
- // Complex object, simplified for now or need custom handler
331
- borderLength: { type: "number", min: 0, max: 500, label: "Margin" },
332
- offset: { type: "number", min: -100, max: 100, label: "Bleed Offset" },
333
- style: {
334
- type: "select",
335
- options: ["solid", "dashed"],
336
- label: "Line Style"
337
- },
338
- insideColor: { type: "color", label: "Inside Color" },
339
- outsideColor: { type: "color", label: "Outside Color" }
340
- };
341
- this.commands = {
342
- reset: {
343
- execute: (editor) => {
344
- this.options = {
345
- shape: "rect",
346
- width: 300,
347
- height: 300,
348
- radius: 0,
349
- offset: 0,
350
- style: "solid",
351
- insideColor: "rgba(0,0,0,0)",
352
- outsideColor: "#ffffff"
353
- };
354
- this.updateDieline(editor);
355
- return true;
639
+ this.shape = "rect";
640
+ this.width = 500;
641
+ this.height = 500;
642
+ this.radius = 0;
643
+ this.offset = 0;
644
+ this.style = "solid";
645
+ this.insideColor = "rgba(0,0,0,0)";
646
+ this.outsideColor = "#ffffff";
647
+ this.showBleedLines = true;
648
+ this.holes = [];
649
+ if (options) {
650
+ Object.assign(this, options);
651
+ }
652
+ }
653
+ activate(context) {
654
+ this.context = context;
655
+ this.canvasService = context.services.get("CanvasService");
656
+ if (!this.canvasService) {
657
+ console.warn("CanvasService not found for DielineTool");
658
+ return;
659
+ }
660
+ const configService = context.services.get("ConfigurationService");
661
+ if (configService) {
662
+ this.shape = configService.get("dieline.shape", this.shape);
663
+ this.width = configService.get("dieline.width", this.width);
664
+ this.height = configService.get("dieline.height", this.height);
665
+ this.radius = configService.get("dieline.radius", this.radius);
666
+ this.borderLength = configService.get(
667
+ "dieline.borderLength",
668
+ this.borderLength
669
+ );
670
+ this.offset = configService.get("dieline.offset", this.offset);
671
+ this.style = configService.get("dieline.style", this.style);
672
+ this.insideColor = configService.get(
673
+ "dieline.insideColor",
674
+ this.insideColor
675
+ );
676
+ this.outsideColor = configService.get(
677
+ "dieline.outsideColor",
678
+ this.outsideColor
679
+ );
680
+ this.showBleedLines = configService.get(
681
+ "dieline.showBleedLines",
682
+ this.showBleedLines
683
+ );
684
+ this.holes = configService.get("dieline.holes", this.holes);
685
+ this.pathData = configService.get("dieline.pathData", this.pathData);
686
+ configService.onAnyChange((e) => {
687
+ if (e.key.startsWith("dieline.")) {
688
+ const prop = e.key.split(".")[1];
689
+ console.log(
690
+ `[DielineTool] Config change detected: ${e.key} -> ${e.value}`
691
+ );
692
+ if (prop && prop in this) {
693
+ this[prop] = e.value;
694
+ this.updateDieline();
695
+ }
356
696
  }
357
- },
358
- destroy: {
359
- execute: (editor) => {
360
- this.destroyLayer(editor);
361
- return true;
697
+ });
698
+ }
699
+ this.createLayer();
700
+ this.updateDieline();
701
+ }
702
+ deactivate(context) {
703
+ this.destroyLayer();
704
+ this.canvasService = void 0;
705
+ this.context = void 0;
706
+ }
707
+ contribute() {
708
+ return {
709
+ [ContributionPointIds2.CONFIGURATIONS]: [
710
+ {
711
+ id: "dieline.shape",
712
+ type: "select",
713
+ label: "Shape",
714
+ options: ["rect", "circle", "ellipse", "custom"],
715
+ default: this.shape
716
+ },
717
+ {
718
+ id: "dieline.width",
719
+ type: "number",
720
+ label: "Width",
721
+ min: 10,
722
+ max: 2e3,
723
+ default: this.width
724
+ },
725
+ {
726
+ id: "dieline.height",
727
+ type: "number",
728
+ label: "Height",
729
+ min: 10,
730
+ max: 2e3,
731
+ default: this.height
732
+ },
733
+ {
734
+ id: "dieline.radius",
735
+ type: "number",
736
+ label: "Corner Radius",
737
+ min: 0,
738
+ max: 500,
739
+ default: this.radius
740
+ },
741
+ {
742
+ id: "dieline.position",
743
+ type: "json",
744
+ label: "Position (Normalized)",
745
+ default: this.position
746
+ },
747
+ {
748
+ id: "dieline.borderLength",
749
+ type: "number",
750
+ label: "Margin",
751
+ min: 0,
752
+ max: 500,
753
+ default: this.borderLength
754
+ },
755
+ {
756
+ id: "dieline.offset",
757
+ type: "number",
758
+ label: "Bleed Offset",
759
+ min: -100,
760
+ max: 100,
761
+ default: this.offset
762
+ },
763
+ {
764
+ id: "dieline.showBleedLines",
765
+ type: "boolean",
766
+ label: "Show Bleed Lines",
767
+ default: this.showBleedLines
768
+ },
769
+ {
770
+ id: "dieline.style",
771
+ type: "select",
772
+ label: "Line Style",
773
+ options: ["solid", "dashed"],
774
+ default: this.style
775
+ },
776
+ {
777
+ id: "dieline.insideColor",
778
+ type: "color",
779
+ label: "Inside Color",
780
+ default: this.insideColor
781
+ },
782
+ {
783
+ id: "dieline.outsideColor",
784
+ type: "color",
785
+ label: "Outside Color",
786
+ default: this.outsideColor
787
+ },
788
+ {
789
+ id: "dieline.holes",
790
+ type: "json",
791
+ label: "Holes",
792
+ default: this.holes
362
793
  }
363
- },
364
- setDimensions: {
365
- execute: (editor, width, height) => {
366
- if (this.options.width === width && this.options.height === height) return true;
367
- this.options.width = width;
368
- this.options.height = height;
369
- this.updateDieline(editor);
370
- return true;
794
+ ],
795
+ [ContributionPointIds2.COMMANDS]: [
796
+ {
797
+ command: "reset",
798
+ title: "Reset Dieline",
799
+ handler: () => {
800
+ this.shape = "rect";
801
+ this.width = 300;
802
+ this.height = 300;
803
+ this.radius = 0;
804
+ this.offset = 0;
805
+ this.style = "solid";
806
+ this.insideColor = "rgba(0,0,0,0)";
807
+ this.outsideColor = "#ffffff";
808
+ this.showBleedLines = true;
809
+ this.holes = [];
810
+ this.pathData = void 0;
811
+ this.updateDieline();
812
+ return true;
813
+ }
371
814
  },
372
- schema: {
373
- width: {
374
- type: "number",
375
- label: "Width",
376
- min: 10,
377
- max: 2e3,
378
- required: true
379
- },
380
- height: {
381
- type: "number",
382
- label: "Height",
383
- min: 10,
384
- max: 2e3,
385
- required: true
815
+ {
816
+ command: "setDimensions",
817
+ title: "Set Dimensions",
818
+ handler: (width, height) => {
819
+ if (this.width === width && this.height === height) return true;
820
+ this.width = width;
821
+ this.height = height;
822
+ this.updateDieline();
823
+ return true;
386
824
  }
387
- }
388
- },
389
- setShape: {
390
- execute: (editor, shape) => {
391
- if (this.options.shape === shape) return true;
392
- this.options.shape = shape;
393
- this.updateDieline(editor);
394
- return true;
395
825
  },
396
- schema: {
397
- shape: {
398
- type: "string",
399
- label: "Shape",
400
- options: ["rect", "circle", "ellipse"],
401
- required: true
826
+ {
827
+ command: "setShape",
828
+ title: "Set Shape",
829
+ handler: (shape) => {
830
+ if (this.shape === shape) return true;
831
+ this.shape = shape;
832
+ this.updateDieline();
833
+ return true;
402
834
  }
403
- }
404
- },
405
- setBleed: {
406
- execute: (editor, bleed) => {
407
- if (this.options.offset === bleed) return true;
408
- this.options.offset = bleed;
409
- this.updateDieline(editor);
410
- return true;
411
835
  },
412
- schema: {
413
- bleed: {
414
- type: "number",
415
- label: "Bleed",
416
- min: -100,
417
- max: 100,
418
- required: true
836
+ {
837
+ command: "setBleed",
838
+ title: "Set Bleed",
839
+ handler: (bleed) => {
840
+ if (this.offset === bleed) return true;
841
+ this.offset = bleed;
842
+ this.updateDieline();
843
+ return true;
844
+ }
845
+ },
846
+ {
847
+ command: "setHoles",
848
+ title: "Set Holes",
849
+ handler: (holes) => {
850
+ this.holes = holes;
851
+ this.updateDieline(false);
852
+ return true;
853
+ }
854
+ },
855
+ {
856
+ command: "getGeometry",
857
+ title: "Get Geometry",
858
+ handler: () => {
859
+ return this.getGeometry();
860
+ }
861
+ },
862
+ {
863
+ command: "exportCutImage",
864
+ title: "Export Cut Image",
865
+ handler: () => {
866
+ return this.exportCutImage();
867
+ }
868
+ },
869
+ {
870
+ command: "detectEdge",
871
+ title: "Detect Edge from Image",
872
+ handler: async (imageUrl, options) => {
873
+ try {
874
+ const pathData = await ImageTracer.trace(imageUrl, options);
875
+ const bounds = getPathBounds(pathData);
876
+ const currentMax = Math.max(this.width, this.height);
877
+ const scale = currentMax / Math.max(bounds.width, bounds.height);
878
+ this.width = bounds.width * scale;
879
+ this.height = bounds.height * scale;
880
+ this.shape = "custom";
881
+ this.pathData = pathData;
882
+ this.updateDieline();
883
+ return pathData;
884
+ } catch (e) {
885
+ console.error("Edge detection failed", e);
886
+ throw e;
887
+ }
419
888
  }
420
889
  }
421
- }
890
+ ]
422
891
  };
423
892
  }
424
- onMount(editor) {
425
- this.createLayer(editor);
426
- this.updateDieline(editor);
427
- }
428
- onUnmount(editor) {
429
- this.destroyLayer(editor);
430
- }
431
- onUpdate(editor, state) {
432
- this.updateDieline(editor);
433
- }
434
- onDestroy(editor) {
435
- this.destroyLayer(editor);
893
+ getLayer() {
894
+ var _a;
895
+ return (_a = this.canvasService) == null ? void 0 : _a.getLayer("dieline-overlay");
436
896
  }
437
- getLayer(editor, id) {
438
- return editor.canvas.getObjects().find((obj) => {
439
- var _a;
440
- return ((_a = obj.data) == null ? void 0 : _a.id) === id;
897
+ createLayer() {
898
+ if (!this.canvasService) return;
899
+ const width = this.canvasService.canvas.width || 800;
900
+ const height = this.canvasService.canvas.height || 600;
901
+ const layer = this.canvasService.createLayer("dieline-overlay", {
902
+ width,
903
+ height,
904
+ selectable: false,
905
+ evented: false
441
906
  });
442
- }
443
- createLayer(editor) {
444
- let layer = this.getLayer(editor, "dieline-overlay");
445
- if (!layer) {
446
- const width = editor.canvas.width || 800;
447
- const height = editor.canvas.height || 600;
448
- layer = new PooderLayer2([], {
449
- width,
450
- height,
451
- selectable: false,
452
- evented: false,
453
- data: { id: "dieline-overlay" }
454
- });
455
- editor.canvas.add(layer);
907
+ this.canvasService.canvas.bringObjectToFront(layer);
908
+ const userLayer = this.canvasService.getLayer("user");
909
+ if (userLayer) {
910
+ const userIndex = this.canvasService.canvas.getObjects().indexOf(userLayer);
911
+ this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
456
912
  }
457
- editor.canvas.bringObjectToFront(layer);
458
913
  }
459
- destroyLayer(editor) {
460
- const layer = this.getLayer(editor, "dieline-overlay");
914
+ destroyLayer() {
915
+ if (!this.canvasService) return;
916
+ const layer = this.getLayer();
461
917
  if (layer) {
462
- editor.canvas.remove(layer);
918
+ this.canvasService.canvas.remove(layer);
463
919
  }
464
920
  }
465
921
  createHatchPattern(color = "rgba(0, 0, 0, 0.3)") {
@@ -482,31 +938,44 @@ var DielineTool = class {
482
938
  }
483
939
  return new Pattern({ source: canvas, repetition: "repeat" });
484
940
  }
485
- updateDieline(editor) {
486
- var _a, _b;
487
- const { shape, radius, offset, style, insideColor, outsideColor, position, borderLength } = this.options;
488
- let { width, height } = this.options;
489
- const canvasW = editor.canvas.width || 800;
490
- const canvasH = editor.canvas.height || 600;
941
+ updateDieline(emitEvent = true) {
942
+ if (!this.canvasService) return;
943
+ const layer = this.getLayer();
944
+ if (!layer) return;
945
+ const {
946
+ shape,
947
+ radius,
948
+ offset,
949
+ style,
950
+ insideColor,
951
+ outsideColor,
952
+ position,
953
+ borderLength,
954
+ showBleedLines,
955
+ holes
956
+ } = this;
957
+ let { width, height } = this;
958
+ const canvasW = this.canvasService.canvas.width || 800;
959
+ const canvasH = this.canvasService.canvas.height || 600;
491
960
  if (borderLength && borderLength > 0) {
492
961
  width = Math.max(0, canvasW - borderLength * 2);
493
962
  height = Math.max(0, canvasH - borderLength * 2);
494
963
  }
495
- const cx = (_a = position == null ? void 0 : position.x) != null ? _a : canvasW / 2;
496
- const cy = (_b = position == null ? void 0 : position.y) != null ? _b : canvasH / 2;
497
- const layer = this.getLayer(editor, "dieline-overlay");
498
- if (!layer) return;
964
+ const normalizedPos = position != null ? position : { x: 0.5, y: 0.5 };
965
+ const cx = Coordinate.toAbsolute(normalizedPos.x, canvasW);
966
+ const cy = Coordinate.toAbsolute(normalizedPos.y, canvasH);
499
967
  layer.remove(...layer.getObjects());
500
- const holeTool = editor.getExtension("HoleTool");
501
- const holes = holeTool ? holeTool.options.holes || [] : [];
502
- const innerRadius = holeTool ? holeTool.options.innerRadius || 15 : 15;
503
- const outerRadius = holeTool ? holeTool.options.outerRadius || 25 : 25;
504
- const holeData = holes.map((h) => ({
505
- x: h.x,
506
- y: h.y,
507
- innerRadius,
508
- outerRadius
509
- }));
968
+ const absoluteHoles = (holes || []).map((h) => {
969
+ const p = Coordinate.denormalizePoint(
970
+ { x: h.x, y: h.y },
971
+ { width: canvasW, height: canvasH }
972
+ );
973
+ return {
974
+ ...h,
975
+ x: p.x,
976
+ y: p.y
977
+ };
978
+ });
510
979
  const cutW = Math.max(0, width + offset * 2);
511
980
  const cutH = Math.max(0, height + offset * 2);
512
981
  const cutR = radius === 0 ? 0 : Math.max(0, radius + offset);
@@ -519,7 +988,8 @@ var DielineTool = class {
519
988
  radius: cutR,
520
989
  x: cx,
521
990
  y: cy,
522
- holes: holeData
991
+ holes: absoluteHoles,
992
+ pathData: this.pathData
523
993
  });
524
994
  const mask = new Path(maskPathData, {
525
995
  fill: outsideColor,
@@ -540,7 +1010,8 @@ var DielineTool = class {
540
1010
  radius: cutR,
541
1011
  x: cx,
542
1012
  y: cy,
543
- holes: holeData
1013
+ holes: absoluteHoles,
1014
+ pathData: this.pathData
544
1015
  });
545
1016
  const insideObj = new Path(productPathData, {
546
1017
  fill: insideColor,
@@ -554,27 +1025,33 @@ var DielineTool = class {
554
1025
  layer.add(insideObj);
555
1026
  }
556
1027
  if (offset !== 0) {
557
- const bleedPathData = generateBleedZonePath({
558
- shape,
559
- width,
560
- height,
561
- radius,
562
- x: cx,
563
- y: cy,
564
- holes: holeData
565
- }, offset);
566
- const pattern = this.createHatchPattern("red");
567
- if (pattern) {
568
- const bleedObj = new Path(bleedPathData, {
569
- fill: pattern,
570
- stroke: null,
571
- selectable: false,
572
- evented: false,
573
- objectCaching: false,
574
- originX: "left",
575
- originY: "top"
576
- });
577
- layer.add(bleedObj);
1028
+ const bleedPathData = generateBleedZonePath(
1029
+ {
1030
+ shape,
1031
+ width,
1032
+ height,
1033
+ radius,
1034
+ x: cx,
1035
+ y: cy,
1036
+ holes: absoluteHoles,
1037
+ pathData: this.pathData
1038
+ },
1039
+ offset
1040
+ );
1041
+ if (showBleedLines !== false) {
1042
+ const pattern = this.createHatchPattern("red");
1043
+ if (pattern) {
1044
+ const bleedObj = new Path(bleedPathData, {
1045
+ fill: pattern,
1046
+ stroke: null,
1047
+ selectable: false,
1048
+ evented: false,
1049
+ objectCaching: false,
1050
+ originX: "left",
1051
+ originY: "top"
1052
+ });
1053
+ layer.add(bleedObj);
1054
+ }
578
1055
  }
579
1056
  const offsetPathData = generateDielinePath({
580
1057
  shape,
@@ -583,7 +1060,8 @@ var DielineTool = class {
583
1060
  radius: cutR,
584
1061
  x: cx,
585
1062
  y: cy,
586
- holes: holeData
1063
+ holes: absoluteHoles,
1064
+ pathData: this.pathData
587
1065
  });
588
1066
  const offsetBorderObj = new Path(offsetPathData, {
589
1067
  fill: null,
@@ -606,7 +1084,9 @@ var DielineTool = class {
606
1084
  radius,
607
1085
  x: cx,
608
1086
  y: cy,
609
- holes: holeData
1087
+ holes: absoluteHoles,
1088
+ // FIX: Use absoluteHoles instead of holes
1089
+ pathData: this.pathData
610
1090
  });
611
1091
  const borderObj = new Path(borderPathData, {
612
1092
  fill: "transparent",
@@ -619,103 +1099,238 @@ var DielineTool = class {
619
1099
  originY: "top"
620
1100
  });
621
1101
  layer.add(borderObj);
622
- editor.canvas.requestRenderAll();
1102
+ const userLayer = this.canvasService.getLayer("user");
1103
+ if (layer && userLayer) {
1104
+ const layerIndex = this.canvasService.canvas.getObjects().indexOf(layer);
1105
+ const userIndex = this.canvasService.canvas.getObjects().indexOf(userLayer);
1106
+ if (layerIndex < userIndex) {
1107
+ this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
1108
+ }
1109
+ } else {
1110
+ this.canvasService.canvas.bringObjectToFront(layer);
1111
+ }
1112
+ const rulerLayer = this.canvasService.getLayer("ruler-overlay");
1113
+ if (rulerLayer) {
1114
+ this.canvasService.canvas.bringObjectToFront(rulerLayer);
1115
+ }
1116
+ layer.dirty = true;
1117
+ this.canvasService.requestRenderAll();
1118
+ if (emitEvent && this.context) {
1119
+ const geometry = this.getGeometry();
1120
+ if (geometry) {
1121
+ this.context.eventBus.emit("dieline:geometry:change", geometry);
1122
+ }
1123
+ }
623
1124
  }
624
- getGeometry(editor) {
1125
+ getGeometry() {
625
1126
  var _a, _b;
626
- const { shape, width, height, radius, position, borderLength } = this.options;
627
- const canvasW = editor.canvas.width || 800;
628
- const canvasH = editor.canvas.height || 600;
1127
+ if (!this.canvasService) return null;
1128
+ const { shape, width, height, radius, position, borderLength, offset } = this;
1129
+ const canvasW = this.canvasService.canvas.width || 800;
1130
+ const canvasH = this.canvasService.canvas.height || 600;
629
1131
  let visualWidth = width;
630
1132
  let visualHeight = height;
631
1133
  if (borderLength && borderLength > 0) {
632
1134
  visualWidth = Math.max(0, canvasW - borderLength * 2);
633
1135
  visualHeight = Math.max(0, canvasH - borderLength * 2);
634
1136
  }
635
- const cx = (_a = position == null ? void 0 : position.x) != null ? _a : canvasW / 2;
636
- const cy = (_b = position == null ? void 0 : position.y) != null ? _b : canvasH / 2;
1137
+ const cx = Coordinate.toAbsolute((_a = position == null ? void 0 : position.x) != null ? _a : 0.5, canvasW);
1138
+ const cy = Coordinate.toAbsolute((_b = position == null ? void 0 : position.y) != null ? _b : 0.5, canvasH);
637
1139
  return {
638
1140
  shape,
639
1141
  x: cx,
640
1142
  y: cy,
641
1143
  width: visualWidth,
642
1144
  height: visualHeight,
643
- radius
1145
+ radius,
1146
+ offset,
1147
+ borderLength,
1148
+ pathData: this.pathData
644
1149
  };
645
1150
  }
1151
+ exportCutImage() {
1152
+ var _a, _b, _c, _d;
1153
+ if (!this.canvasService) return null;
1154
+ const canvas = this.canvasService.canvas;
1155
+ const { shape, width, height, radius, position, holes } = this;
1156
+ const canvasW = canvas.width || 800;
1157
+ const canvasH = canvas.height || 600;
1158
+ const cx = Coordinate.toAbsolute((_a = position == null ? void 0 : position.x) != null ? _a : 0.5, canvasW);
1159
+ const cy = Coordinate.toAbsolute((_b = position == null ? void 0 : position.y) != null ? _b : 0.5, canvasH);
1160
+ const absoluteHoles = (holes || []).map((h) => {
1161
+ const p = Coordinate.denormalizePoint(
1162
+ { x: h.x, y: h.y },
1163
+ { width: canvasW, height: canvasH }
1164
+ );
1165
+ return {
1166
+ ...h,
1167
+ x: p.x,
1168
+ y: p.y
1169
+ };
1170
+ });
1171
+ const pathData = generateDielinePath({
1172
+ shape,
1173
+ width,
1174
+ height,
1175
+ radius,
1176
+ x: cx,
1177
+ y: cy,
1178
+ holes: absoluteHoles,
1179
+ pathData: this.pathData
1180
+ });
1181
+ const clipPath = new Path(pathData, {
1182
+ left: 0,
1183
+ top: 0,
1184
+ originX: "left",
1185
+ originY: "top",
1186
+ absolutePositioned: true
1187
+ });
1188
+ const layer = this.getLayer();
1189
+ const wasVisible = (_c = layer == null ? void 0 : layer.visible) != null ? _c : true;
1190
+ if (layer) layer.visible = false;
1191
+ const holeMarkers = canvas.getObjects().filter((o) => {
1192
+ var _a2;
1193
+ return ((_a2 = o.data) == null ? void 0 : _a2.type) === "hole-marker";
1194
+ });
1195
+ holeMarkers.forEach((o) => o.visible = false);
1196
+ const rulerLayer = canvas.getObjects().find((obj) => {
1197
+ var _a2;
1198
+ return ((_a2 = obj.data) == null ? void 0 : _a2.id) === "ruler-overlay";
1199
+ });
1200
+ const rulerWasVisible = (_d = rulerLayer == null ? void 0 : rulerLayer.visible) != null ? _d : true;
1201
+ if (rulerLayer) rulerLayer.visible = false;
1202
+ const originalClip = canvas.clipPath;
1203
+ canvas.clipPath = clipPath;
1204
+ const bbox = clipPath.getBoundingRect();
1205
+ const clipPathCorrected = new Path(pathData, {
1206
+ absolutePositioned: true,
1207
+ left: 0,
1208
+ top: 0
1209
+ });
1210
+ const tempPath = new Path(pathData);
1211
+ const tempBounds = tempPath.getBoundingRect();
1212
+ clipPathCorrected.set({
1213
+ left: tempBounds.left,
1214
+ top: tempBounds.top,
1215
+ originX: "left",
1216
+ originY: "top"
1217
+ });
1218
+ canvas.clipPath = clipPathCorrected;
1219
+ const exportBbox = clipPathCorrected.getBoundingRect();
1220
+ const dataURL = canvas.toDataURL({
1221
+ format: "png",
1222
+ multiplier: 2,
1223
+ left: exportBbox.left,
1224
+ top: exportBbox.top,
1225
+ width: exportBbox.width,
1226
+ height: exportBbox.height
1227
+ });
1228
+ canvas.clipPath = originalClip;
1229
+ if (layer) layer.visible = wasVisible;
1230
+ if (rulerLayer) rulerLayer.visible = rulerWasVisible;
1231
+ holeMarkers.forEach((o) => o.visible = true);
1232
+ canvas.requestRenderAll();
1233
+ return dataURL;
1234
+ }
646
1235
  };
647
1236
 
648
1237
  // src/film.ts
649
- import { Image as Image2, PooderLayer as PooderLayer3 } from "@pooder/core";
1238
+ import {
1239
+ ContributionPointIds as ContributionPointIds3
1240
+ } from "@pooder/core";
1241
+ import { FabricImage as Image3 } from "fabric";
650
1242
  var FilmTool = class {
651
- constructor() {
652
- this.name = "FilmTool";
653
- this.options = {
654
- url: "",
655
- opacity: 0.5
1243
+ constructor(options) {
1244
+ this.id = "pooder.kit.film";
1245
+ this.metadata = {
1246
+ name: "FilmTool"
656
1247
  };
657
- this.schema = {
658
- url: {
659
- type: "string",
660
- label: "Film Image URL"
661
- },
662
- opacity: {
663
- type: "number",
664
- min: 0,
665
- max: 1,
666
- step: 0.1,
667
- label: "Opacity"
668
- }
669
- };
670
- this.commands = {
671
- setFilmImage: {
672
- execute: (editor, url, opacity) => {
673
- if (this.options.url === url && this.options.opacity === opacity) return true;
674
- this.options.url = url;
675
- this.options.opacity = opacity;
676
- this.updateFilm(editor, this.options);
677
- return true;
678
- },
679
- schema: {
680
- url: {
681
- type: "string",
682
- label: "Image URL",
683
- required: true
684
- },
685
- opacity: {
686
- type: "number",
687
- label: "Opacity",
688
- min: 0,
689
- max: 1,
690
- required: true
1248
+ this.url = "";
1249
+ this.opacity = 0.5;
1250
+ if (options) {
1251
+ Object.assign(this, options);
1252
+ }
1253
+ }
1254
+ activate(context) {
1255
+ this.canvasService = context.services.get("CanvasService");
1256
+ if (!this.canvasService) {
1257
+ console.warn("CanvasService not found for FilmTool");
1258
+ return;
1259
+ }
1260
+ const configService = context.services.get("ConfigurationService");
1261
+ if (configService) {
1262
+ this.url = configService.get("film.url", this.url);
1263
+ this.opacity = configService.get("film.opacity", this.opacity);
1264
+ configService.onAnyChange((e) => {
1265
+ if (e.key.startsWith("film.")) {
1266
+ const prop = e.key.split(".")[1];
1267
+ console.log(
1268
+ `[FilmTool] Config change detected: ${e.key} -> ${e.value}`
1269
+ );
1270
+ if (prop && prop in this) {
1271
+ this[prop] = e.value;
1272
+ this.updateFilm();
691
1273
  }
692
1274
  }
1275
+ });
1276
+ }
1277
+ this.initLayer();
1278
+ this.updateFilm();
1279
+ }
1280
+ deactivate(context) {
1281
+ if (this.canvasService) {
1282
+ const layer = this.canvasService.getLayer("overlay");
1283
+ if (layer) {
1284
+ const img = this.canvasService.getObject("film-image", "overlay");
1285
+ if (img) {
1286
+ layer.remove(img);
1287
+ this.canvasService.requestRenderAll();
1288
+ }
693
1289
  }
694
- };
695
- }
696
- onMount(editor) {
697
- this.initLayer(editor);
698
- this.updateFilm(editor, this.options);
699
- }
700
- onUnmount(editor) {
701
- const layer = editor.getLayer("overlay");
702
- if (layer) {
703
- const img = editor.getObject("film-image", "overlay");
704
- if (img) {
705
- layer.remove(img);
706
- editor.canvas.requestRenderAll();
707
- }
1290
+ this.canvasService = void 0;
708
1291
  }
709
1292
  }
710
- onUpdate(editor, state) {
711
- this.updateFilm(editor, this.options);
1293
+ contribute() {
1294
+ return {
1295
+ [ContributionPointIds3.CONFIGURATIONS]: [
1296
+ {
1297
+ id: "film.url",
1298
+ type: "string",
1299
+ label: "Film Image URL",
1300
+ default: ""
1301
+ },
1302
+ {
1303
+ id: "film.opacity",
1304
+ type: "number",
1305
+ label: "Opacity",
1306
+ min: 0,
1307
+ max: 1,
1308
+ step: 0.1,
1309
+ default: 0.5
1310
+ }
1311
+ ],
1312
+ [ContributionPointIds3.COMMANDS]: [
1313
+ {
1314
+ command: "setFilmImage",
1315
+ title: "Set Film Image",
1316
+ handler: (url, opacity) => {
1317
+ if (this.url === url && this.opacity === opacity) return true;
1318
+ this.url = url;
1319
+ this.opacity = opacity;
1320
+ this.updateFilm();
1321
+ return true;
1322
+ }
1323
+ }
1324
+ ]
1325
+ };
712
1326
  }
713
- initLayer(editor) {
714
- let overlayLayer = editor.getLayer("overlay");
1327
+ initLayer() {
1328
+ if (!this.canvasService) return;
1329
+ let overlayLayer = this.canvasService.getLayer("overlay");
715
1330
  if (!overlayLayer) {
716
- const width = editor.state.width;
717
- const height = editor.state.height;
718
- const layer = new PooderLayer3([], {
1331
+ const width = this.canvasService.canvas.width || 800;
1332
+ const height = this.canvasService.canvas.height || 600;
1333
+ const layer = this.canvasService.createLayer("overlay", {
719
1334
  width,
720
1335
  height,
721
1336
  left: 0,
@@ -725,33 +1340,30 @@ var FilmTool = class {
725
1340
  selectable: false,
726
1341
  evented: false,
727
1342
  subTargetCheck: false,
728
- interactive: false,
729
- data: {
730
- id: "overlay"
731
- }
1343
+ interactive: false
732
1344
  });
733
- editor.canvas.add(layer);
734
- editor.canvas.bringObjectToFront(layer);
1345
+ this.canvasService.canvas.bringObjectToFront(layer);
735
1346
  }
736
1347
  }
737
- async updateFilm(editor, options) {
738
- const layer = editor.getLayer("overlay");
1348
+ async updateFilm() {
1349
+ if (!this.canvasService) return;
1350
+ const layer = this.canvasService.getLayer("overlay");
739
1351
  if (!layer) {
740
1352
  console.warn("[FilmTool] Overlay layer not found");
741
1353
  return;
742
1354
  }
743
- const { url, opacity } = options;
1355
+ const { url, opacity } = this;
744
1356
  if (!url) {
745
- const img2 = editor.getObject("film-image", "overlay");
1357
+ const img2 = this.canvasService.getObject("film-image", "overlay");
746
1358
  if (img2) {
747
1359
  layer.remove(img2);
748
- editor.canvas.requestRenderAll();
1360
+ this.canvasService.requestRenderAll();
749
1361
  }
750
1362
  return;
751
1363
  }
752
- const width = editor.state.width;
753
- const height = editor.state.height;
754
- let img = editor.getObject("film-image", "overlay");
1364
+ const width = this.canvasService.canvas.width || 800;
1365
+ const height = this.canvasService.canvas.height || 600;
1366
+ let img = this.canvasService.getObject("film-image", "overlay");
755
1367
  try {
756
1368
  if (img) {
757
1369
  if (img.getSrc() !== url) {
@@ -759,10 +1371,9 @@ var FilmTool = class {
759
1371
  }
760
1372
  img.set({ opacity });
761
1373
  } else {
762
- img = await Image2.fromURL(url, { crossOrigin: "anonymous" });
1374
+ img = await Image3.fromURL(url, { crossOrigin: "anonymous" });
763
1375
  img.scaleToWidth(width);
764
- if (img.getScaledHeight() < height)
765
- img.scaleToHeight(height);
1376
+ if (img.getScaledHeight() < height) img.scaleToHeight(height);
766
1377
  img.set({
767
1378
  originX: "left",
768
1379
  originY: "top",
@@ -775,219 +1386,363 @@ var FilmTool = class {
775
1386
  });
776
1387
  layer.add(img);
777
1388
  }
778
- editor.canvas.requestRenderAll();
1389
+ this.canvasService.requestRenderAll();
779
1390
  } catch (error) {
780
1391
  console.error("[FilmTool] Failed to load film image", url, error);
781
1392
  }
1393
+ layer.dirty = true;
1394
+ this.canvasService.requestRenderAll();
782
1395
  }
783
1396
  };
784
1397
 
785
1398
  // src/hole.ts
786
- import { Circle as Circle2, Group, Point } from "@pooder/core";
1399
+ import {
1400
+ ContributionPointIds as ContributionPointIds4
1401
+ } from "@pooder/core";
1402
+ import { Circle, Group, Point } from "fabric";
787
1403
  var HoleTool = class {
788
- constructor() {
789
- this.name = "HoleTool";
790
- this.options = {
791
- innerRadius: 15,
792
- outerRadius: 25,
793
- style: "solid",
794
- holes: []
795
- };
796
- this.schema = {
797
- innerRadius: {
798
- type: "number",
799
- min: 1,
800
- max: 100,
801
- label: "Inner Radius"
802
- },
803
- outerRadius: {
804
- type: "number",
805
- min: 1,
806
- max: 100,
807
- label: "Outer Radius"
808
- },
809
- style: {
810
- type: "select",
811
- options: ["solid", "dashed"],
812
- label: "Line Style"
813
- },
814
- holes: {
815
- type: "json",
816
- label: "Holes"
817
- }
1404
+ constructor(options) {
1405
+ this.id = "pooder.kit.hole";
1406
+ this.metadata = {
1407
+ name: "HoleTool"
818
1408
  };
1409
+ this.innerRadius = 15;
1410
+ this.outerRadius = 25;
1411
+ this.style = "solid";
1412
+ this.holes = [];
1413
+ this.constraintTarget = "bleed";
1414
+ this.isUpdatingConfig = false;
819
1415
  this.handleMoving = null;
820
1416
  this.handleModified = null;
821
- this.commands = {
822
- reset: {
823
- execute: (editor) => {
824
- let defaultPos = { x: editor.canvas.width / 2, y: 50 };
825
- const g = this.getDielineGeometry(editor);
826
- if (g) {
827
- const topCenter = { x: g.x, y: g.y - g.height / 2 };
828
- defaultPos = getNearestPointOnDieline(topCenter, { ...g, holes: [] });
1417
+ this.handleDielineChange = null;
1418
+ // Cache geometry to enforce constraints during drag
1419
+ this.currentGeometry = null;
1420
+ if (options) {
1421
+ Object.assign(this, options);
1422
+ }
1423
+ }
1424
+ activate(context) {
1425
+ this.context = context;
1426
+ this.canvasService = context.services.get("CanvasService");
1427
+ if (!this.canvasService) {
1428
+ console.warn("CanvasService not found for HoleTool");
1429
+ return;
1430
+ }
1431
+ const configService = context.services.get(
1432
+ "ConfigurationService"
1433
+ );
1434
+ if (configService) {
1435
+ this.innerRadius = configService.get(
1436
+ "hole.innerRadius",
1437
+ this.innerRadius
1438
+ );
1439
+ this.outerRadius = configService.get(
1440
+ "hole.outerRadius",
1441
+ this.outerRadius
1442
+ );
1443
+ this.style = configService.get("hole.style", this.style);
1444
+ this.constraintTarget = configService.get(
1445
+ "hole.constraintTarget",
1446
+ this.constraintTarget
1447
+ );
1448
+ const dielineHoles = configService.get("dieline.holes", []);
1449
+ if (this.canvasService) {
1450
+ const { width, height } = this.canvasService.canvas;
1451
+ this.holes = dielineHoles.map((h) => {
1452
+ const p = Coordinate.denormalizePoint(h, {
1453
+ width: width || 800,
1454
+ height: height || 600
1455
+ });
1456
+ return { x: p.x, y: p.y };
1457
+ });
1458
+ }
1459
+ configService.onAnyChange((e) => {
1460
+ if (this.isUpdatingConfig) return;
1461
+ if (e.key.startsWith("hole.")) {
1462
+ const prop = e.key.split(".")[1];
1463
+ if (prop && prop in this) {
1464
+ this[prop] = e.value;
1465
+ this.redraw();
1466
+ this.syncHolesToDieline();
829
1467
  }
830
- this.options = {
831
- innerRadius: 15,
832
- outerRadius: 25,
833
- style: "solid",
834
- holes: [defaultPos]
835
- };
836
- this.redraw(editor);
837
- const dielineTool = editor.getExtension("DielineTool");
838
- if (dielineTool && dielineTool.updateDieline) {
839
- dielineTool.updateDieline(editor);
1468
+ }
1469
+ if (e.key === "dieline.holes") {
1470
+ const holes = e.value || [];
1471
+ if (this.canvasService) {
1472
+ const { width, height } = this.canvasService.canvas;
1473
+ this.holes = holes.map((h) => {
1474
+ const p = Coordinate.denormalizePoint(h, {
1475
+ width: width || 800,
1476
+ height: height || 600
1477
+ });
1478
+ return { x: p.x, y: p.y };
1479
+ });
1480
+ this.redraw();
840
1481
  }
841
- return true;
842
1482
  }
843
- },
844
- addHole: {
845
- execute: (editor, x, y) => {
846
- if (!this.options.holes) this.options.holes = [];
847
- this.options.holes.push({ x, y });
848
- this.redraw(editor);
849
- const dielineTool = editor.getExtension("DielineTool");
850
- if (dielineTool && dielineTool.updateDieline) {
851
- dielineTool.updateDieline(editor);
1483
+ });
1484
+ }
1485
+ this.setup();
1486
+ }
1487
+ deactivate(context) {
1488
+ this.teardown();
1489
+ this.canvasService = void 0;
1490
+ this.context = void 0;
1491
+ }
1492
+ contribute() {
1493
+ return {
1494
+ [ContributionPointIds4.CONFIGURATIONS]: [
1495
+ {
1496
+ id: "hole.innerRadius",
1497
+ type: "number",
1498
+ label: "Inner Radius",
1499
+ min: 1,
1500
+ max: 100,
1501
+ default: 15
1502
+ },
1503
+ {
1504
+ id: "hole.outerRadius",
1505
+ type: "number",
1506
+ label: "Outer Radius",
1507
+ min: 1,
1508
+ max: 100,
1509
+ default: 25
1510
+ },
1511
+ {
1512
+ id: "hole.style",
1513
+ type: "select",
1514
+ label: "Line Style",
1515
+ options: ["solid", "dashed"],
1516
+ default: "solid"
1517
+ },
1518
+ {
1519
+ id: "hole.constraintTarget",
1520
+ type: "select",
1521
+ label: "Constraint Target",
1522
+ options: ["original", "bleed"],
1523
+ default: "bleed"
1524
+ }
1525
+ ],
1526
+ [ContributionPointIds4.COMMANDS]: [
1527
+ {
1528
+ command: "resetHoles",
1529
+ title: "Reset Holes",
1530
+ handler: () => {
1531
+ if (!this.canvasService) return false;
1532
+ let defaultPos = { x: this.canvasService.canvas.width / 2, y: 50 };
1533
+ if (this.currentGeometry) {
1534
+ const g = this.currentGeometry;
1535
+ const topCenter = { x: g.x, y: g.y - g.height / 2 };
1536
+ defaultPos = getNearestPointOnDieline(topCenter, {
1537
+ ...g,
1538
+ holes: []
1539
+ });
1540
+ }
1541
+ this.innerRadius = 15;
1542
+ this.outerRadius = 25;
1543
+ this.style = "solid";
1544
+ this.holes = [defaultPos];
1545
+ this.redraw();
1546
+ this.syncHolesToDieline();
1547
+ return true;
852
1548
  }
853
- return true;
854
1549
  },
855
- schema: {
856
- x: {
857
- type: "number",
858
- label: "X Position",
859
- required: true
860
- },
861
- y: {
862
- type: "number",
863
- label: "Y Position",
864
- required: true
1550
+ {
1551
+ command: "addHole",
1552
+ title: "Add Hole",
1553
+ handler: (x, y) => {
1554
+ if (!this.holes) this.holes = [];
1555
+ this.holes.push({ x, y });
1556
+ this.redraw();
1557
+ this.syncHolesToDieline();
1558
+ return true;
865
1559
  }
866
- }
867
- },
868
- clearHoles: {
869
- execute: (editor) => {
870
- this.options.holes = [];
871
- this.redraw(editor);
872
- const dielineTool = editor.getExtension("DielineTool");
873
- if (dielineTool && dielineTool.updateDieline) {
874
- dielineTool.updateDieline(editor);
1560
+ },
1561
+ {
1562
+ command: "clearHoles",
1563
+ title: "Clear Holes",
1564
+ handler: () => {
1565
+ this.holes = [];
1566
+ this.redraw();
1567
+ this.syncHolesToDieline();
1568
+ return true;
875
1569
  }
876
- return true;
877
1570
  }
878
- }
1571
+ ]
879
1572
  };
880
1573
  }
881
- onMount(editor) {
882
- this.setup(editor);
883
- }
884
- onUnmount(editor) {
885
- this.teardown(editor);
886
- }
887
- onDestroy(editor) {
888
- this.teardown(editor);
889
- }
890
- getDielineGeometry(editor) {
891
- const dielineTool = editor.getExtension("DielineTool");
892
- if (!dielineTool) return null;
893
- const geometry = dielineTool.getGeometry(editor);
894
- if (!geometry) return null;
895
- const offset = dielineTool.options.offset || 0;
896
- return {
897
- ...geometry,
898
- width: Math.max(0, geometry.width + offset * 2),
899
- height: Math.max(0, geometry.height + offset * 2),
900
- radius: Math.max(0, geometry.radius + offset)
901
- };
902
- }
903
- setup(editor) {
1574
+ setup() {
1575
+ if (!this.canvasService || !this.context) return;
1576
+ const canvas = this.canvasService.canvas;
1577
+ if (!this.handleDielineChange) {
1578
+ this.handleDielineChange = (geometry) => {
1579
+ this.currentGeometry = geometry;
1580
+ const changed = this.enforceConstraints();
1581
+ if (changed) {
1582
+ this.syncHolesToDieline();
1583
+ }
1584
+ };
1585
+ this.context.eventBus.on(
1586
+ "dieline:geometry:change",
1587
+ this.handleDielineChange
1588
+ );
1589
+ }
1590
+ const commandService = this.context.services.get("CommandService");
1591
+ if (commandService) {
1592
+ try {
1593
+ const geometry = commandService.executeCommand("getGeometry");
1594
+ if (geometry) {
1595
+ Promise.resolve(geometry).then((g) => {
1596
+ if (g) {
1597
+ this.currentGeometry = g;
1598
+ this.enforceConstraints();
1599
+ this.initializeHoles();
1600
+ }
1601
+ });
1602
+ }
1603
+ } catch (e) {
1604
+ }
1605
+ }
904
1606
  if (!this.handleMoving) {
905
1607
  this.handleMoving = (e) => {
906
1608
  var _a;
907
1609
  const target = e.target;
908
1610
  if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "hole-marker") return;
909
- const geometry = this.getDielineGeometry(editor);
910
- if (!geometry) return;
1611
+ if (!this.currentGeometry) return;
1612
+ const effectiveOffset = this.constraintTarget === "original" ? 0 : this.currentGeometry.offset;
1613
+ const constraintGeometry = {
1614
+ ...this.currentGeometry,
1615
+ width: Math.max(0, this.currentGeometry.width + effectiveOffset * 2),
1616
+ height: Math.max(
1617
+ 0,
1618
+ this.currentGeometry.height + effectiveOffset * 2
1619
+ ),
1620
+ radius: Math.max(0, this.currentGeometry.radius + effectiveOffset)
1621
+ };
911
1622
  const p = new Point(target.left, target.top);
912
- const newPos = this.calculateConstrainedPosition(p, geometry);
1623
+ const newPos = this.calculateConstrainedPosition(p, constraintGeometry);
913
1624
  target.set({
914
1625
  left: newPos.x,
915
1626
  top: newPos.y
916
1627
  });
917
1628
  };
918
- editor.canvas.on("object:moving", this.handleMoving);
1629
+ canvas.on("object:moving", this.handleMoving);
919
1630
  }
920
1631
  if (!this.handleModified) {
921
1632
  this.handleModified = (e) => {
922
1633
  var _a;
923
1634
  const target = e.target;
924
1635
  if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "hole-marker") return;
925
- this.syncHolesFromCanvas(editor);
1636
+ this.syncHolesFromCanvas();
926
1637
  };
927
- editor.canvas.on("object:modified", this.handleModified);
1638
+ canvas.on("object:modified", this.handleModified);
928
1639
  }
929
- const opts = this.options;
930
- if (!opts.holes || opts.holes.length === 0) {
931
- let defaultPos = { x: editor.canvas.width / 2, y: 50 };
932
- const g = this.getDielineGeometry(editor);
933
- if (g) {
1640
+ this.initializeHoles();
1641
+ }
1642
+ initializeHoles() {
1643
+ if (!this.canvasService) return;
1644
+ if (!this.holes || this.holes.length === 0) {
1645
+ let defaultPos = { x: this.canvasService.canvas.width / 2, y: 50 };
1646
+ if (this.currentGeometry) {
1647
+ const g = this.currentGeometry;
934
1648
  const topCenter = { x: g.x, y: g.y - g.height / 2 };
935
- const snapped = getNearestPointOnDieline(topCenter, { ...g, holes: [] });
1649
+ const snapped = getNearestPointOnDieline(topCenter, {
1650
+ ...g,
1651
+ holes: []
1652
+ });
936
1653
  defaultPos = snapped;
937
1654
  }
938
- opts.holes = [defaultPos];
939
- }
940
- this.options = { ...opts };
941
- this.redraw(editor);
942
- const dielineTool = editor.getExtension("DielineTool");
943
- if (dielineTool && dielineTool.updateDieline) {
944
- dielineTool.updateDieline(editor);
1655
+ this.holes = [defaultPos];
945
1656
  }
1657
+ this.redraw();
1658
+ this.syncHolesToDieline();
946
1659
  }
947
- teardown(editor) {
1660
+ teardown() {
1661
+ if (!this.canvasService) return;
1662
+ const canvas = this.canvasService.canvas;
948
1663
  if (this.handleMoving) {
949
- editor.canvas.off("object:moving", this.handleMoving);
1664
+ canvas.off("object:moving", this.handleMoving);
950
1665
  this.handleMoving = null;
951
1666
  }
952
1667
  if (this.handleModified) {
953
- editor.canvas.off("object:modified", this.handleModified);
1668
+ canvas.off("object:modified", this.handleModified);
954
1669
  this.handleModified = null;
955
1670
  }
956
- const objects = editor.canvas.getObjects().filter((obj) => {
1671
+ if (this.handleDielineChange && this.context) {
1672
+ this.context.eventBus.off(
1673
+ "dieline:geometry:change",
1674
+ this.handleDielineChange
1675
+ );
1676
+ this.handleDielineChange = null;
1677
+ }
1678
+ const objects = canvas.getObjects().filter((obj) => {
957
1679
  var _a;
958
1680
  return ((_a = obj.data) == null ? void 0 : _a.type) === "hole-marker";
959
1681
  });
960
- objects.forEach((obj) => editor.canvas.remove(obj));
961
- editor.canvas.requestRenderAll();
962
- }
963
- onUpdate(editor, state) {
1682
+ objects.forEach((obj) => canvas.remove(obj));
1683
+ if (this.context) {
1684
+ const commandService = this.context.services.get("CommandService");
1685
+ if (commandService) {
1686
+ try {
1687
+ commandService.executeCommand("setHoles", []);
1688
+ } catch (e) {
1689
+ }
1690
+ }
1691
+ }
1692
+ this.canvasService.requestRenderAll();
964
1693
  }
965
- syncHolesFromCanvas(editor) {
966
- const objects = editor.canvas.getObjects().filter((obj) => {
1694
+ syncHolesFromCanvas() {
1695
+ if (!this.canvasService) return;
1696
+ const objects = this.canvasService.canvas.getObjects().filter((obj) => {
967
1697
  var _a;
968
1698
  return ((_a = obj.data) == null ? void 0 : _a.type) === "hole-marker";
969
1699
  });
970
1700
  const holes = objects.map((obj) => ({ x: obj.left, y: obj.top }));
971
- this.options.holes = holes;
972
- const dielineTool = editor.getExtension("DielineTool");
973
- if (dielineTool && dielineTool.updateDieline) {
974
- dielineTool.updateDieline(editor);
1701
+ this.holes = holes;
1702
+ this.syncHolesToDieline();
1703
+ }
1704
+ syncHolesToDieline() {
1705
+ if (!this.context || !this.canvasService) return;
1706
+ const { holes, innerRadius, outerRadius } = this;
1707
+ const currentHoles = holes || [];
1708
+ const width = this.canvasService.canvas.width || 800;
1709
+ const height = this.canvasService.canvas.height || 600;
1710
+ const configService = this.context.services.get(
1711
+ "ConfigurationService"
1712
+ );
1713
+ if (configService) {
1714
+ this.isUpdatingConfig = true;
1715
+ try {
1716
+ const normalizedHoles = currentHoles.map((h) => {
1717
+ const p = Coordinate.normalizePoint(h, { width, height });
1718
+ return {
1719
+ x: p.x,
1720
+ y: p.y,
1721
+ innerRadius,
1722
+ outerRadius
1723
+ };
1724
+ });
1725
+ configService.update("dieline.holes", normalizedHoles);
1726
+ } finally {
1727
+ this.isUpdatingConfig = false;
1728
+ }
975
1729
  }
976
1730
  }
977
- redraw(editor) {
978
- const canvas = editor.canvas;
1731
+ redraw() {
1732
+ if (!this.canvasService) return;
1733
+ const canvas = this.canvasService.canvas;
979
1734
  const existing = canvas.getObjects().filter((obj) => {
980
1735
  var _a;
981
1736
  return ((_a = obj.data) == null ? void 0 : _a.type) === "hole-marker";
982
1737
  });
983
1738
  existing.forEach((obj) => canvas.remove(obj));
984
- const { innerRadius, outerRadius, style, holes } = this.options;
1739
+ const { innerRadius, outerRadius, style, holes } = this;
985
1740
  if (!holes || holes.length === 0) {
986
- canvas.requestRenderAll();
1741
+ this.canvasService.requestRenderAll();
987
1742
  return;
988
1743
  }
989
1744
  holes.forEach((hole, index) => {
990
- const innerCircle = new Circle2({
1745
+ const innerCircle = new Circle({
991
1746
  radius: innerRadius,
992
1747
  fill: "transparent",
993
1748
  stroke: "red",
@@ -995,7 +1750,7 @@ var HoleTool = class {
995
1750
  originX: "center",
996
1751
  originY: "center"
997
1752
  });
998
- const outerCircle = new Circle2({
1753
+ const outerCircle = new Circle({
999
1754
  radius: outerRadius,
1000
1755
  fill: "transparent",
1001
1756
  stroke: "#666",
@@ -1041,7 +1796,68 @@ var HoleTool = class {
1041
1796
  canvas.add(holeGroup);
1042
1797
  canvas.bringObjectToFront(holeGroup);
1043
1798
  });
1044
- canvas.requestRenderAll();
1799
+ const markers = canvas.getObjects().filter((o) => {
1800
+ var _a;
1801
+ return ((_a = o.data) == null ? void 0 : _a.type) === "hole-marker";
1802
+ });
1803
+ markers.forEach((m) => canvas.bringObjectToFront(m));
1804
+ this.canvasService.requestRenderAll();
1805
+ }
1806
+ enforceConstraints() {
1807
+ const geometry = this.currentGeometry;
1808
+ if (!geometry || !this.canvasService) {
1809
+ console.log(
1810
+ "[HoleTool] Skipping enforceConstraints: No geometry or canvas service"
1811
+ );
1812
+ return false;
1813
+ }
1814
+ const effectiveOffset = this.constraintTarget === "original" ? 0 : geometry.offset;
1815
+ const constraintGeometry = {
1816
+ ...geometry,
1817
+ width: Math.max(0, geometry.width + effectiveOffset * 2),
1818
+ height: Math.max(0, geometry.height + effectiveOffset * 2),
1819
+ radius: Math.max(0, geometry.radius + effectiveOffset)
1820
+ };
1821
+ const objects = this.canvasService.canvas.getObjects().filter((obj) => {
1822
+ var _a;
1823
+ return ((_a = obj.data) == null ? void 0 : _a.type) === "hole-marker";
1824
+ });
1825
+ console.log(
1826
+ `[HoleTool] Enforcing constraints on ${objects.length} markers`
1827
+ );
1828
+ let changed = false;
1829
+ objects.sort(
1830
+ (a, b) => {
1831
+ var _a, _b, _c, _d;
1832
+ return ((_b = (_a = a.data) == null ? void 0 : _a.index) != null ? _b : 0) - ((_d = (_c = b.data) == null ? void 0 : _c.index) != null ? _d : 0);
1833
+ }
1834
+ );
1835
+ const newHoles = [];
1836
+ objects.forEach((obj) => {
1837
+ const currentPos = new Point(obj.left, obj.top);
1838
+ const newPos = this.calculateConstrainedPosition(
1839
+ currentPos,
1840
+ constraintGeometry
1841
+ );
1842
+ if (currentPos.distanceFrom(newPos) > 0.1) {
1843
+ console.log(
1844
+ `[HoleTool] Moving hole from (${currentPos.x}, ${currentPos.y}) to (${newPos.x}, ${newPos.y})`
1845
+ );
1846
+ obj.set({
1847
+ left: newPos.x,
1848
+ top: newPos.y
1849
+ });
1850
+ obj.setCoords();
1851
+ changed = true;
1852
+ }
1853
+ newHoles.push({ x: obj.left, y: obj.top });
1854
+ });
1855
+ if (changed) {
1856
+ this.holes = newHoles;
1857
+ this.canvasService.requestRenderAll();
1858
+ return true;
1859
+ }
1860
+ return false;
1045
1861
  }
1046
1862
  calculateConstrainedPosition(p, g) {
1047
1863
  const options = {
@@ -1049,7 +1865,10 @@ var HoleTool = class {
1049
1865
  holes: []
1050
1866
  // We don't need holes for boundary calculation
1051
1867
  };
1052
- const nearest = getNearestPointOnDieline({ x: p.x, y: p.y }, options);
1868
+ const nearest = getNearestPointOnDieline(
1869
+ { x: p.x, y: p.y },
1870
+ options
1871
+ );
1053
1872
  const nearestP = new Point(nearest.x, nearest.y);
1054
1873
  const dist = p.distanceFrom(nearestP);
1055
1874
  const v = p.subtract(nearestP);
@@ -1063,12 +1882,11 @@ var HoleTool = class {
1063
1882
  }
1064
1883
  let clampedDist = signedDist;
1065
1884
  if (signedDist > 0) {
1066
- clampedDist = Math.min(signedDist, this.options.innerRadius);
1885
+ clampedDist = Math.min(signedDist, this.innerRadius);
1067
1886
  } else {
1068
- clampedDist = Math.max(signedDist, -this.options.outerRadius);
1887
+ clampedDist = Math.max(signedDist, -this.outerRadius);
1069
1888
  }
1070
1889
  if (dist < 1e-3) return nearestP;
1071
- const dir = v.scalarDivide(dist);
1072
1890
  const scale = Math.abs(clampedDist) / (dist || 1);
1073
1891
  const offset = v.scalarMultiply(scale);
1074
1892
  return nearestP.add(offset);
@@ -1076,76 +1894,156 @@ var HoleTool = class {
1076
1894
  };
1077
1895
 
1078
1896
  // src/image.ts
1079
- import { Image as Image3, PooderLayer as PooderLayer4 } from "@pooder/core";
1897
+ import {
1898
+ ContributionPointIds as ContributionPointIds5
1899
+ } from "@pooder/core";
1900
+ import { FabricImage as Image4, Point as Point2, util } from "fabric";
1080
1901
  var ImageTool = class {
1081
- constructor() {
1082
- this.name = "ImageTool";
1083
- this.options = {
1084
- url: "",
1085
- opacity: 1
1086
- };
1087
- this.schema = {
1088
- url: {
1089
- type: "string",
1090
- label: "Image URL"
1091
- },
1092
- opacity: {
1093
- type: "number",
1094
- min: 0,
1095
- max: 1,
1096
- step: 0.1,
1097
- label: "Opacity"
1098
- }
1902
+ constructor(options) {
1903
+ this.id = "pooder.kit.image";
1904
+ this.metadata = {
1905
+ name: "ImageTool"
1099
1906
  };
1100
- this.commands = {
1101
- setUserImage: {
1102
- execute: (editor, url, opacity) => {
1103
- if (this.options.url === url && this.options.opacity === opacity) return true;
1104
- this.options.url = url;
1105
- this.options.opacity = opacity;
1106
- this.updateImage(editor, this.options);
1107
- return true;
1108
- },
1109
- schema: {
1110
- url: {
1111
- type: "string",
1112
- label: "Image URL",
1113
- required: true
1114
- },
1115
- opacity: {
1116
- type: "number",
1117
- label: "Opacity",
1118
- min: 0,
1119
- max: 1,
1120
- required: true
1907
+ this._loadingUrl = null;
1908
+ this.url = "";
1909
+ this.opacity = 1;
1910
+ if (options) {
1911
+ Object.assign(this, options);
1912
+ }
1913
+ }
1914
+ activate(context) {
1915
+ this.context = context;
1916
+ this.canvasService = context.services.get("CanvasService");
1917
+ if (!this.canvasService) {
1918
+ console.warn("CanvasService not found for ImageTool");
1919
+ return;
1920
+ }
1921
+ const configService = context.services.get("ConfigurationService");
1922
+ if (configService) {
1923
+ this.url = configService.get("image.url", this.url);
1924
+ this.opacity = configService.get("image.opacity", this.opacity);
1925
+ this.width = configService.get("image.width", this.width);
1926
+ this.height = configService.get("image.height", this.height);
1927
+ this.angle = configService.get("image.angle", this.angle);
1928
+ this.left = configService.get("image.left", this.left);
1929
+ this.top = configService.get("image.top", this.top);
1930
+ configService.onAnyChange((e) => {
1931
+ if (e.key.startsWith("image.")) {
1932
+ const prop = e.key.split(".")[1];
1933
+ console.log(
1934
+ `[ImageTool] Config change detected: ${e.key} -> ${e.value}`
1935
+ );
1936
+ if (prop && prop in this) {
1937
+ this[prop] = e.value;
1938
+ this.updateImage();
1121
1939
  }
1122
1940
  }
1941
+ });
1942
+ }
1943
+ this.ensureLayer();
1944
+ this.updateImage();
1945
+ }
1946
+ deactivate(context) {
1947
+ if (this.canvasService) {
1948
+ const layer = this.canvasService.getLayer("user");
1949
+ if (layer) {
1950
+ const userImage = this.canvasService.getObject("user-image", "user");
1951
+ if (userImage) {
1952
+ layer.remove(userImage);
1953
+ this.canvasService.requestRenderAll();
1954
+ }
1123
1955
  }
1124
- };
1125
- }
1126
- onMount(editor) {
1127
- this.ensureLayer(editor);
1128
- this.updateImage(editor, this.options);
1129
- }
1130
- onUnmount(editor) {
1131
- const layer = editor.getLayer("user");
1132
- if (layer) {
1133
- const userImage = editor.getObject("user-image", "user");
1134
- if (userImage) {
1135
- layer.remove(userImage);
1136
- editor.canvas.requestRenderAll();
1137
- }
1956
+ this.canvasService = void 0;
1957
+ this.context = void 0;
1138
1958
  }
1139
1959
  }
1140
- onUpdate(editor, state) {
1141
- this.updateImage(editor, this.options);
1960
+ contribute() {
1961
+ return {
1962
+ [ContributionPointIds5.CONFIGURATIONS]: [
1963
+ {
1964
+ id: "image.url",
1965
+ type: "string",
1966
+ label: "Image URL",
1967
+ default: this.url
1968
+ },
1969
+ {
1970
+ id: "image.opacity",
1971
+ type: "number",
1972
+ label: "Opacity",
1973
+ min: 0,
1974
+ max: 1,
1975
+ step: 0.1,
1976
+ default: this.opacity
1977
+ },
1978
+ {
1979
+ id: "image.width",
1980
+ type: "number",
1981
+ label: "Width",
1982
+ min: 0,
1983
+ max: 5e3,
1984
+ default: this.width
1985
+ },
1986
+ {
1987
+ id: "image.height",
1988
+ type: "number",
1989
+ label: "Height",
1990
+ min: 0,
1991
+ max: 5e3,
1992
+ default: this.height
1993
+ },
1994
+ {
1995
+ id: "image.angle",
1996
+ type: "number",
1997
+ label: "Rotation",
1998
+ min: 0,
1999
+ max: 360,
2000
+ default: this.angle
2001
+ },
2002
+ {
2003
+ id: "image.left",
2004
+ type: "number",
2005
+ label: "Left (Normalized)",
2006
+ min: 0,
2007
+ max: 1,
2008
+ default: this.left
2009
+ },
2010
+ {
2011
+ id: "image.top",
2012
+ type: "number",
2013
+ label: "Top (Normalized)",
2014
+ min: 0,
2015
+ max: 1,
2016
+ default: this.top
2017
+ }
2018
+ ],
2019
+ [ContributionPointIds5.COMMANDS]: [
2020
+ {
2021
+ command: "setUserImage",
2022
+ title: "Set User Image",
2023
+ handler: (url, opacity, width, height, angle, left, top) => {
2024
+ if (this.url === url && this.opacity === opacity && this.width === width && this.height === height && this.angle === angle && this.left === left && this.top === top)
2025
+ return true;
2026
+ this.url = url;
2027
+ this.opacity = opacity;
2028
+ this.width = width;
2029
+ this.height = height;
2030
+ this.angle = angle;
2031
+ this.left = left;
2032
+ this.top = top;
2033
+ this.updateImage();
2034
+ return true;
2035
+ }
2036
+ }
2037
+ ]
2038
+ };
1142
2039
  }
1143
- ensureLayer(editor) {
1144
- let userLayer = editor.getLayer("user");
2040
+ ensureLayer() {
2041
+ if (!this.canvasService) return;
2042
+ let userLayer = this.canvasService.getLayer("user");
1145
2043
  if (!userLayer) {
1146
- userLayer = new PooderLayer4([], {
1147
- width: editor.state.width,
1148
- height: editor.state.height,
2044
+ userLayer = this.canvasService.createLayer("user", {
2045
+ width: this.canvasService.canvas.width,
2046
+ height: this.canvasService.canvas.height,
1149
2047
  left: 0,
1150
2048
  top: 0,
1151
2049
  originX: "left",
@@ -1153,53 +2051,238 @@ var ImageTool = class {
1153
2051
  selectable: false,
1154
2052
  evented: true,
1155
2053
  subTargetCheck: true,
1156
- interactive: true,
1157
- data: {
1158
- id: "user"
1159
- }
2054
+ interactive: true
1160
2055
  });
1161
- editor.canvas.add(userLayer);
2056
+ const dielineLayer = this.canvasService.getLayer("dieline-overlay");
2057
+ if (dielineLayer) {
2058
+ const index = this.canvasService.canvas.getObjects().indexOf(dielineLayer);
2059
+ if (index >= 0) {
2060
+ this.canvasService.canvas.moveObjectTo(userLayer, index);
2061
+ }
2062
+ } else {
2063
+ const bgLayer = this.canvasService.getLayer("background");
2064
+ if (bgLayer) {
2065
+ this.canvasService.canvas.sendObjectToBack(bgLayer);
2066
+ }
2067
+ }
2068
+ this.canvasService.requestRenderAll();
1162
2069
  }
1163
2070
  }
1164
- updateImage(editor, opts) {
2071
+ updateImage() {
1165
2072
  var _a, _b;
1166
- let { url, opacity } = opts;
1167
- const layer = editor.getLayer("user");
2073
+ if (!this.canvasService) return;
2074
+ let { url, opacity, width, height, angle, left, top } = this;
2075
+ const layer = this.canvasService.getLayer("user");
1168
2076
  if (!layer) {
1169
2077
  console.warn("[ImageTool] User layer not found");
1170
2078
  return;
1171
2079
  }
1172
- const userImage = editor.getObject("user-image", "user");
2080
+ const userImage = this.canvasService.getObject("user-image", "user");
2081
+ if (this._loadingUrl === url) return;
1173
2082
  if (userImage) {
1174
2083
  const currentSrc = ((_a = userImage.getSrc) == null ? void 0 : _a.call(userImage)) || ((_b = userImage._element) == null ? void 0 : _b.src);
1175
2084
  if (currentSrc !== url) {
1176
- this.loadImage(editor, layer, url, opacity, userImage);
2085
+ this.loadImage(layer);
1177
2086
  } else {
1178
- if (userImage.opacity !== opacity) {
1179
- userImage.set({ opacity });
1180
- editor.canvas.requestRenderAll();
2087
+ const updates = {};
2088
+ const canvasW = this.canvasService.canvas.width || 800;
2089
+ const canvasH = this.canvasService.canvas.height || 600;
2090
+ const centerX = canvasW / 2;
2091
+ const centerY = canvasH / 2;
2092
+ if (userImage.opacity !== opacity) updates.opacity = opacity;
2093
+ if (angle !== void 0 && userImage.angle !== angle)
2094
+ updates.angle = angle;
2095
+ if (userImage.originX !== "center") {
2096
+ userImage.set({
2097
+ originX: "center",
2098
+ originY: "center",
2099
+ left: userImage.left + userImage.width * userImage.scaleX / 2,
2100
+ top: userImage.top + userImage.height * userImage.scaleY / 2
2101
+ });
2102
+ }
2103
+ if (left !== void 0) {
2104
+ const globalLeft = Coordinate.toAbsolute(left, canvasW);
2105
+ const localLeft = globalLeft - centerX;
2106
+ if (Math.abs(userImage.left - localLeft) > 1)
2107
+ updates.left = localLeft;
2108
+ }
2109
+ if (top !== void 0) {
2110
+ const globalTop = Coordinate.toAbsolute(top, canvasH);
2111
+ const localTop = globalTop - centerY;
2112
+ if (Math.abs(userImage.top - localTop) > 1) updates.top = localTop;
2113
+ }
2114
+ if (width !== void 0 && userImage.width)
2115
+ updates.scaleX = width / userImage.width;
2116
+ if (height !== void 0 && userImage.height)
2117
+ updates.scaleY = height / userImage.height;
2118
+ if (Object.keys(updates).length > 0) {
2119
+ userImage.set(updates);
2120
+ layer.dirty = true;
2121
+ this.canvasService.requestRenderAll();
1181
2122
  }
1182
2123
  }
1183
2124
  } else {
1184
- this.loadImage(editor, layer, url, opacity);
2125
+ this.loadImage(layer);
1185
2126
  }
1186
2127
  }
1187
- loadImage(editor, layer, url, opacity, oldImage) {
1188
- Image3.fromURL(url).then((image) => {
1189
- if (oldImage) {
1190
- const { left, top, scaleX, scaleY, angle } = oldImage;
1191
- image.set({ left, top, scaleX, scaleY, angle });
1192
- layer.remove(oldImage);
2128
+ loadImage(layer) {
2129
+ if (!this.canvasService) return;
2130
+ const { url } = this;
2131
+ if (!url) return;
2132
+ this._loadingUrl = url;
2133
+ Image4.fromURL(url, { crossOrigin: "anonymous" }).then((image) => {
2134
+ var _a, _b, _c, _d, _e, _f;
2135
+ if (this._loadingUrl !== url) return;
2136
+ this._loadingUrl = null;
2137
+ let { opacity, width, height, angle, left, top } = this;
2138
+ if (this.context) {
2139
+ const configService = this.context.services.get(
2140
+ "ConfigurationService"
2141
+ );
2142
+ const dielineWidth = configService.get("dieline.width");
2143
+ const dielineHeight = configService.get("dieline.height");
2144
+ console.log(
2145
+ "[ImageTool] Dieline config debug:",
2146
+ {
2147
+ widthVal: dielineWidth,
2148
+ heightVal: dielineHeight,
2149
+ // Debug: dump all keys to see what is available
2150
+ allKeys: Array.from(
2151
+ ((_a = configService.configValues) == null ? void 0 : _a.keys()) || []
2152
+ )
2153
+ },
2154
+ configService
2155
+ );
2156
+ if (width === void 0 && height === void 0) {
2157
+ const scale = Math.min(
2158
+ dielineWidth / (image.width || 1),
2159
+ dielineHeight / (image.height || 1)
2160
+ );
2161
+ width = (image.width || 1) * scale;
2162
+ height = (image.height || 1) * scale;
2163
+ this.width = width;
2164
+ this.height = height;
2165
+ }
2166
+ if (left === void 0 && top === void 0) {
2167
+ const dielinePos = configService == null ? void 0 : configService.get("dieline.position");
2168
+ if (dielinePos) {
2169
+ this.left = dielinePos.x;
2170
+ this.top = dielinePos.y;
2171
+ } else {
2172
+ this.left = 0.5;
2173
+ this.top = 0.5;
2174
+ }
2175
+ left = this.left;
2176
+ top = this.top;
2177
+ }
2178
+ }
2179
+ const existingImage = this.canvasService.getObject(
2180
+ "user-image",
2181
+ "user"
2182
+ );
2183
+ if (existingImage) {
2184
+ const defaultLeft = existingImage.left;
2185
+ const defaultTop = existingImage.top;
2186
+ const defaultAngle = existingImage.angle;
2187
+ const defaultScaleX = existingImage.scaleX;
2188
+ const defaultScaleY = existingImage.scaleY;
2189
+ const canvasW = ((_b = this.canvasService) == null ? void 0 : _b.canvas.width) || 800;
2190
+ const canvasH = ((_c = this.canvasService) == null ? void 0 : _c.canvas.height) || 600;
2191
+ const centerX = canvasW / 2;
2192
+ const centerY = canvasH / 2;
2193
+ let targetLeft = left !== void 0 ? left : defaultLeft;
2194
+ let targetTop = top !== void 0 ? top : defaultTop;
2195
+ const configService = (_d = this.context) == null ? void 0 : _d.services.get(
2196
+ "ConfigurationService"
2197
+ );
2198
+ console.log("[ImageTool] Loading EXISTING image...", {
2199
+ canvasW,
2200
+ canvasH,
2201
+ centerX,
2202
+ centerY,
2203
+ incomingLeft: left,
2204
+ incomingTop: top,
2205
+ dielinePos: configService == null ? void 0 : configService.get("dieline.position"),
2206
+ existingImage: !!existingImage
2207
+ });
2208
+ if (left !== void 0) {
2209
+ const globalLeft = Coordinate.toAbsolute(left, canvasW);
2210
+ targetLeft = globalLeft;
2211
+ console.log("[ImageTool] Calculated targetLeft", {
2212
+ globalLeft,
2213
+ targetLeft
2214
+ });
2215
+ }
2216
+ if (top !== void 0) {
2217
+ const globalTop = Coordinate.toAbsolute(top, canvasH);
2218
+ targetTop = globalTop;
2219
+ console.log("[ImageTool] Calculated targetTop", {
2220
+ globalTop,
2221
+ targetTop
2222
+ });
2223
+ }
2224
+ image.set({
2225
+ originX: "center",
2226
+ // Use center origin for easier positioning
2227
+ originY: "center",
2228
+ left: targetLeft,
2229
+ top: targetTop,
2230
+ angle: angle !== void 0 ? angle : defaultAngle,
2231
+ scaleX: width !== void 0 && image.width ? width / image.width : defaultScaleX,
2232
+ scaleY: height !== void 0 && image.height ? height / image.height : defaultScaleY
2233
+ });
2234
+ layer.remove(existingImage);
2235
+ } else {
2236
+ image.set({
2237
+ originX: "center",
2238
+ originY: "center"
2239
+ });
2240
+ if (width !== void 0 && image.width)
2241
+ image.scaleX = width / image.width;
2242
+ if (height !== void 0 && image.height)
2243
+ image.scaleY = height / image.height;
2244
+ if (angle !== void 0) image.angle = angle;
2245
+ const canvasW = ((_e = this.canvasService) == null ? void 0 : _e.canvas.width) || 800;
2246
+ const canvasH = ((_f = this.canvasService) == null ? void 0 : _f.canvas.height) || 600;
2247
+ const centerX = canvasW / 2;
2248
+ const centerY = canvasH / 2;
2249
+ if (left !== void 0) {
2250
+ image.left = Coordinate.toAbsolute(left, canvasW);
2251
+ } else {
2252
+ image.left = centerX;
2253
+ }
2254
+ if (top !== void 0) {
2255
+ image.top = Coordinate.toAbsolute(top, canvasH);
2256
+ } else {
2257
+ image.top = centerY;
2258
+ }
1193
2259
  }
1194
2260
  image.set({
1195
- opacity,
2261
+ opacity: opacity !== void 0 ? opacity : 1,
1196
2262
  data: {
1197
2263
  id: "user-image"
1198
2264
  }
1199
2265
  });
1200
2266
  layer.add(image);
1201
- editor.canvas.requestRenderAll();
2267
+ image.on("modified", (e) => {
2268
+ var _a2, _b2;
2269
+ const matrix = image.calcTransformMatrix();
2270
+ const globalPoint = util.transformPoint(new Point2(0, 0), matrix);
2271
+ const canvasW = ((_a2 = this.canvasService) == null ? void 0 : _a2.canvas.width) || 800;
2272
+ const canvasH = ((_b2 = this.canvasService) == null ? void 0 : _b2.canvas.height) || 600;
2273
+ this.left = Coordinate.toNormalized(globalPoint.x, canvasW);
2274
+ this.top = Coordinate.toNormalized(globalPoint.y, canvasH);
2275
+ this.angle = e.target.angle;
2276
+ if (image.width) this.width = e.target.width * e.target.scaleX;
2277
+ if (image.height) this.height = e.target.height * e.target.scaleY;
2278
+ if (this.context) {
2279
+ this.context.eventBus.emit("update");
2280
+ }
2281
+ });
2282
+ layer.dirty = true;
2283
+ this.canvasService.requestRenderAll();
1202
2284
  }).catch((err) => {
2285
+ if (this._loadingUrl === url) this._loadingUrl = null;
1203
2286
  console.error("Failed to load image", url, err);
1204
2287
  });
1205
2288
  }
@@ -1207,72 +2290,110 @@ var ImageTool = class {
1207
2290
 
1208
2291
  // src/white-ink.ts
1209
2292
  import {
1210
- Image as Image4,
1211
- filters,
1212
- PooderLayer as PooderLayer5
2293
+ ContributionPointIds as ContributionPointIds6
1213
2294
  } from "@pooder/core";
2295
+ import { FabricImage as Image5, filters } from "fabric";
1214
2296
  var WhiteInkTool = class {
1215
- constructor() {
1216
- this.name = "WhiteInkTool";
1217
- this.options = {
1218
- customMask: "",
1219
- opacity: 1,
1220
- enableClip: false
2297
+ constructor(options) {
2298
+ this.id = "pooder.kit.white-ink";
2299
+ this.metadata = {
2300
+ name: "WhiteInkTool"
1221
2301
  };
1222
- this.schema = {
1223
- customMask: { type: "string", label: "Custom Mask URL" },
1224
- opacity: { type: "number", min: 0, max: 1, step: 0.01, label: "Opacity" },
1225
- enableClip: { type: "boolean", label: "Enable Clip" }
1226
- };
1227
- this.commands = {
1228
- setWhiteInkImage: {
1229
- execute: (editor, customMask, opacity, enableClip = true) => {
1230
- if (this.options.customMask === customMask && this.options.opacity === opacity && this.options.enableClip === enableClip) return true;
1231
- this.options.customMask = customMask;
1232
- this.options.opacity = opacity;
1233
- this.options.enableClip = enableClip;
1234
- this.updateWhiteInk(editor, this.options);
1235
- return true;
1236
- },
1237
- schema: {
1238
- customMask: {
1239
- type: "string",
1240
- label: "Custom Mask URL",
1241
- required: true
1242
- },
1243
- opacity: {
1244
- type: "number",
1245
- label: "Opacity",
1246
- min: 0,
1247
- max: 1,
1248
- required: true
1249
- },
1250
- enableClip: {
1251
- type: "boolean",
1252
- label: "Enable Clip",
1253
- default: true,
1254
- required: false
2302
+ this.customMask = "";
2303
+ this.opacity = 1;
2304
+ this.enableClip = false;
2305
+ this._loadingUrl = null;
2306
+ if (options) {
2307
+ Object.assign(this, options);
2308
+ }
2309
+ }
2310
+ activate(context) {
2311
+ this.canvasService = context.services.get("CanvasService");
2312
+ if (!this.canvasService) {
2313
+ console.warn("CanvasService not found for WhiteInkTool");
2314
+ return;
2315
+ }
2316
+ const configService = context.services.get("ConfigurationService");
2317
+ if (configService) {
2318
+ this.customMask = configService.get(
2319
+ "whiteInk.customMask",
2320
+ this.customMask
2321
+ );
2322
+ this.opacity = configService.get("whiteInk.opacity", this.opacity);
2323
+ this.enableClip = configService.get(
2324
+ "whiteInk.enableClip",
2325
+ this.enableClip
2326
+ );
2327
+ configService.onAnyChange((e) => {
2328
+ if (e.key.startsWith("whiteInk.")) {
2329
+ const prop = e.key.split(".")[1];
2330
+ console.log(
2331
+ `[WhiteInkTool] Config change detected: ${e.key} -> ${e.value}`
2332
+ );
2333
+ if (prop && prop in this) {
2334
+ this[prop] = e.value;
2335
+ this.updateWhiteInk();
1255
2336
  }
1256
2337
  }
1257
- }
1258
- };
1259
- }
1260
- onMount(editor) {
1261
- this.setup(editor);
1262
- this.updateWhiteInk(editor, this.options);
2338
+ });
2339
+ }
2340
+ this.setup();
2341
+ this.updateWhiteInk();
1263
2342
  }
1264
- onUnmount(editor) {
1265
- this.teardown(editor);
2343
+ deactivate(context) {
2344
+ this.teardown();
2345
+ this.canvasService = void 0;
1266
2346
  }
1267
- onDestroy(editor) {
1268
- this.teardown(editor);
2347
+ contribute() {
2348
+ return {
2349
+ [ContributionPointIds6.CONFIGURATIONS]: [
2350
+ {
2351
+ id: "whiteInk.customMask",
2352
+ type: "string",
2353
+ label: "Custom Mask URL",
2354
+ default: ""
2355
+ },
2356
+ {
2357
+ id: "whiteInk.opacity",
2358
+ type: "number",
2359
+ label: "Opacity",
2360
+ min: 0,
2361
+ max: 1,
2362
+ step: 0.01,
2363
+ default: 1
2364
+ },
2365
+ {
2366
+ id: "whiteInk.enableClip",
2367
+ type: "boolean",
2368
+ label: "Enable Clip",
2369
+ default: false
2370
+ }
2371
+ ],
2372
+ [ContributionPointIds6.COMMANDS]: [
2373
+ {
2374
+ command: "setWhiteInkImage",
2375
+ title: "Set White Ink Image",
2376
+ handler: (customMask, opacity, enableClip = true) => {
2377
+ if (this.customMask === customMask && this.opacity === opacity && this.enableClip === enableClip)
2378
+ return true;
2379
+ this.customMask = customMask;
2380
+ this.opacity = opacity;
2381
+ this.enableClip = enableClip;
2382
+ this.updateWhiteInk();
2383
+ return true;
2384
+ }
2385
+ }
2386
+ ]
2387
+ };
1269
2388
  }
1270
- setup(editor) {
1271
- let userLayer = editor.getLayer("user");
2389
+ setup() {
2390
+ if (!this.canvasService) return;
2391
+ const canvas = this.canvasService.canvas;
2392
+ let userLayer = this.canvasService.getLayer("user");
1272
2393
  if (!userLayer) {
1273
- userLayer = new PooderLayer5([], {
1274
- width: editor.state.width,
1275
- height: editor.state.height,
2394
+ userLayer = this.canvasService.createLayer("user", {
2395
+ width: canvas.width,
2396
+ height: canvas.height,
1276
2397
  left: 0,
1277
2398
  top: 0,
1278
2399
  originX: "left",
@@ -1280,61 +2401,58 @@ var WhiteInkTool = class {
1280
2401
  selectable: false,
1281
2402
  evented: true,
1282
2403
  subTargetCheck: true,
1283
- interactive: true,
1284
- data: {
1285
- id: "user"
1286
- }
2404
+ interactive: true
1287
2405
  });
1288
- editor.canvas.add(userLayer);
2406
+ canvas.add(userLayer);
1289
2407
  }
1290
2408
  if (!this.syncHandler) {
1291
2409
  this.syncHandler = (e) => {
1292
2410
  var _a;
1293
2411
  const target = e.target;
1294
2412
  if (target && ((_a = target.data) == null ? void 0 : _a.id) === "user-image") {
1295
- this.syncWithUserImage(editor);
2413
+ this.syncWithUserImage();
1296
2414
  }
1297
2415
  };
1298
- editor.canvas.on("object:moving", this.syncHandler);
1299
- editor.canvas.on("object:scaling", this.syncHandler);
1300
- editor.canvas.on("object:rotating", this.syncHandler);
1301
- editor.canvas.on("object:modified", this.syncHandler);
2416
+ canvas.on("object:moving", this.syncHandler);
2417
+ canvas.on("object:scaling", this.syncHandler);
2418
+ canvas.on("object:rotating", this.syncHandler);
2419
+ canvas.on("object:modified", this.syncHandler);
1302
2420
  }
1303
2421
  }
1304
- teardown(editor) {
2422
+ teardown() {
2423
+ if (!this.canvasService) return;
2424
+ const canvas = this.canvasService.canvas;
1305
2425
  if (this.syncHandler) {
1306
- editor.canvas.off("object:moving", this.syncHandler);
1307
- editor.canvas.off("object:scaling", this.syncHandler);
1308
- editor.canvas.off("object:rotating", this.syncHandler);
1309
- editor.canvas.off("object:modified", this.syncHandler);
2426
+ canvas.off("object:moving", this.syncHandler);
2427
+ canvas.off("object:scaling", this.syncHandler);
2428
+ canvas.off("object:rotating", this.syncHandler);
2429
+ canvas.off("object:modified", this.syncHandler);
1310
2430
  this.syncHandler = void 0;
1311
2431
  }
1312
- const layer = editor.getLayer("user");
2432
+ const layer = this.canvasService.getLayer("user");
1313
2433
  if (layer) {
1314
- const whiteInk = editor.getObject("white-ink", "user");
2434
+ const whiteInk = this.canvasService.getObject("white-ink", "user");
1315
2435
  if (whiteInk) {
1316
2436
  layer.remove(whiteInk);
1317
2437
  }
1318
2438
  }
1319
- const userImage = editor.getObject("user-image", "user");
2439
+ const userImage = this.canvasService.getObject("user-image", "user");
1320
2440
  if (userImage && userImage.clipPath) {
1321
2441
  userImage.set({ clipPath: void 0 });
1322
2442
  }
1323
- editor.canvas.requestRenderAll();
1324
- }
1325
- onUpdate(editor, state) {
1326
- this.updateWhiteInk(editor, this.options);
2443
+ this.canvasService.requestRenderAll();
1327
2444
  }
1328
- updateWhiteInk(editor, opts) {
2445
+ updateWhiteInk() {
1329
2446
  var _a, _b;
1330
- const { customMask, opacity, enableClip } = opts;
1331
- const layer = editor.getLayer("user");
2447
+ if (!this.canvasService) return;
2448
+ const { customMask, opacity, enableClip } = this;
2449
+ const layer = this.canvasService.getLayer("user");
1332
2450
  if (!layer) {
1333
2451
  console.warn("[WhiteInkTool] User layer not found");
1334
2452
  return;
1335
2453
  }
1336
- const whiteInk = editor.getObject("white-ink", "user");
1337
- const userImage = editor.getObject("user-image", "user");
2454
+ const whiteInk = this.canvasService.getObject("white-ink", "user");
2455
+ const userImage = this.canvasService.getObject("user-image", "user");
1338
2456
  if (!customMask) {
1339
2457
  if (whiteInk) {
1340
2458
  layer.remove(whiteInk);
@@ -1342,45 +2460,55 @@ var WhiteInkTool = class {
1342
2460
  if (userImage && userImage.clipPath) {
1343
2461
  userImage.set({ clipPath: void 0 });
1344
2462
  }
1345
- editor.canvas.requestRenderAll();
2463
+ layer.dirty = true;
2464
+ this.canvasService.requestRenderAll();
1346
2465
  return;
1347
2466
  }
1348
2467
  if (whiteInk) {
1349
2468
  const currentSrc = ((_a = whiteInk.getSrc) == null ? void 0 : _a.call(whiteInk)) || ((_b = whiteInk._element) == null ? void 0 : _b.src);
1350
2469
  if (currentSrc !== customMask) {
1351
- this.loadWhiteInk(editor, layer, customMask, opacity, enableClip, whiteInk);
2470
+ this.loadWhiteInk(layer, customMask, opacity, enableClip, whiteInk);
1352
2471
  } else {
1353
2472
  if (whiteInk.opacity !== opacity) {
1354
2473
  whiteInk.set({ opacity });
1355
- editor.canvas.requestRenderAll();
2474
+ layer.dirty = true;
2475
+ this.canvasService.requestRenderAll();
1356
2476
  }
1357
2477
  }
1358
2478
  } else {
1359
- this.loadWhiteInk(editor, layer, customMask, opacity, enableClip);
2479
+ this.loadWhiteInk(layer, customMask, opacity, enableClip);
1360
2480
  }
1361
2481
  if (userImage) {
1362
2482
  if (enableClip) {
1363
2483
  if (!userImage.clipPath) {
1364
- this.applyClipPath(editor, customMask);
2484
+ this.applyClipPath(customMask);
1365
2485
  }
1366
2486
  } else {
1367
2487
  if (userImage.clipPath) {
1368
2488
  userImage.set({ clipPath: void 0 });
1369
- editor.canvas.requestRenderAll();
2489
+ layer.dirty = true;
2490
+ this.canvasService.requestRenderAll();
1370
2491
  }
1371
2492
  }
1372
2493
  }
1373
2494
  }
1374
- loadWhiteInk(editor, layer, url, opacity, enableClip, oldImage) {
1375
- Image4.fromURL(url, { crossOrigin: "anonymous" }).then((image) => {
2495
+ loadWhiteInk(layer, url, opacity, enableClip, oldImage) {
2496
+ if (!this.canvasService) return;
2497
+ if (this._loadingUrl === url) return;
2498
+ this._loadingUrl = url;
2499
+ Image5.fromURL(url, { crossOrigin: "anonymous" }).then((image) => {
1376
2500
  var _a;
2501
+ if (this._loadingUrl !== url) return;
2502
+ this._loadingUrl = null;
1377
2503
  if (oldImage) {
1378
2504
  layer.remove(oldImage);
1379
2505
  }
1380
- (_a = image.filters) == null ? void 0 : _a.push(new filters.BlendColor({
1381
- color: "#FFFFFF",
1382
- mode: "add"
1383
- }));
2506
+ (_a = image.filters) == null ? void 0 : _a.push(
2507
+ new filters.BlendColor({
2508
+ color: "#FFFFFF",
2509
+ mode: "add"
2510
+ })
2511
+ );
1384
2512
  image.applyFilters();
1385
2513
  image.set({
1386
2514
  opacity,
@@ -1391,26 +2519,29 @@ var WhiteInkTool = class {
1391
2519
  }
1392
2520
  });
1393
2521
  layer.add(image);
1394
- const userImage = editor.getObject("user-image", "user");
2522
+ const userImage = this.canvasService.getObject("user-image", "user");
1395
2523
  if (userImage) {
1396
2524
  layer.remove(userImage);
1397
2525
  layer.add(userImage);
1398
2526
  }
1399
2527
  if (enableClip) {
1400
- this.applyClipPath(editor, url);
2528
+ this.applyClipPath(url);
1401
2529
  } else if (userImage) {
1402
2530
  userImage.set({ clipPath: void 0 });
1403
2531
  }
1404
- this.syncWithUserImage(editor);
1405
- editor.canvas.requestRenderAll();
2532
+ this.syncWithUserImage();
2533
+ layer.dirty = true;
2534
+ this.canvasService.requestRenderAll();
1406
2535
  }).catch((err) => {
1407
2536
  console.error("Failed to load white ink mask", url, err);
2537
+ this._loadingUrl = null;
1408
2538
  });
1409
2539
  }
1410
- applyClipPath(editor, url) {
1411
- const userImage = editor.getObject("user-image", "user");
2540
+ applyClipPath(url) {
2541
+ if (!this.canvasService) return;
2542
+ const userImage = this.canvasService.getObject("user-image", "user");
1412
2543
  if (!userImage) return;
1413
- Image4.fromURL(url, { crossOrigin: "anonymous" }).then((maskImage) => {
2544
+ Image5.fromURL(url, { crossOrigin: "anonymous" }).then((maskImage) => {
1414
2545
  maskImage.set({
1415
2546
  originX: "center",
1416
2547
  originY: "center",
@@ -1421,14 +2552,17 @@ var WhiteInkTool = class {
1421
2552
  scaleY: userImage.height / maskImage.height
1422
2553
  });
1423
2554
  userImage.set({ clipPath: maskImage });
1424
- editor.canvas.requestRenderAll();
2555
+ const layer = this.canvasService.getLayer("user");
2556
+ if (layer) layer.dirty = true;
2557
+ this.canvasService.requestRenderAll();
1425
2558
  }).catch((err) => {
1426
2559
  console.error("Failed to load clip path", url, err);
1427
2560
  });
1428
2561
  }
1429
- syncWithUserImage(editor) {
1430
- const userImage = editor.getObject("user-image", "user");
1431
- const whiteInk = editor.getObject("white-ink", "user");
2562
+ syncWithUserImage() {
2563
+ if (!this.canvasService) return;
2564
+ const userImage = this.canvasService.getObject("user-image", "user");
2565
+ const whiteInk = this.canvasService.getObject("white-ink", "user");
1432
2566
  if (userImage && whiteInk) {
1433
2567
  whiteInk.set({
1434
2568
  left: userImage.left,
@@ -1448,114 +2582,175 @@ var WhiteInkTool = class {
1448
2582
  };
1449
2583
 
1450
2584
  // src/ruler.ts
1451
- import { PooderLayer as PooderLayer6, Rect as Rect3, Line, Text } from "@pooder/core";
2585
+ import {
2586
+ ContributionPointIds as ContributionPointIds7
2587
+ } from "@pooder/core";
2588
+ import { Rect as Rect2, Line, Text } from "fabric";
1452
2589
  var RulerTool = class {
1453
- constructor() {
1454
- this.name = "RulerTool";
1455
- this.options = {
1456
- unit: "px",
1457
- thickness: 20,
1458
- backgroundColor: "#f0f0f0",
1459
- textColor: "#333333",
1460
- lineColor: "#999999",
1461
- fontSize: 10
1462
- };
1463
- this.schema = {
1464
- unit: {
1465
- type: "select",
1466
- options: ["px", "mm", "cm", "in"],
1467
- label: "Unit"
1468
- },
1469
- thickness: { type: "number", min: 10, max: 100, label: "Thickness" },
1470
- backgroundColor: { type: "color", label: "Background Color" },
1471
- textColor: { type: "color", label: "Text Color" },
1472
- lineColor: { type: "color", label: "Line Color" },
1473
- fontSize: { type: "number", min: 8, max: 24, label: "Font Size" }
2590
+ constructor(options) {
2591
+ this.id = "pooder.kit.ruler";
2592
+ this.metadata = {
2593
+ name: "RulerTool"
1474
2594
  };
1475
- this.commands = {
1476
- setUnit: {
1477
- execute: (editor, unit) => {
1478
- if (this.options.unit === unit) return true;
1479
- this.options.unit = unit;
1480
- this.updateRuler(editor);
1481
- return true;
1482
- },
1483
- schema: {
1484
- unit: {
1485
- type: "string",
1486
- label: "Unit",
1487
- options: ["px", "mm", "cm", "in"],
1488
- required: true
2595
+ this.unit = "px";
2596
+ this.thickness = 20;
2597
+ this.backgroundColor = "#f0f0f0";
2598
+ this.textColor = "#333333";
2599
+ this.lineColor = "#999999";
2600
+ this.fontSize = 10;
2601
+ if (options) {
2602
+ Object.assign(this, options);
2603
+ }
2604
+ }
2605
+ activate(context) {
2606
+ this.canvasService = context.services.get("CanvasService");
2607
+ if (!this.canvasService) {
2608
+ console.warn("CanvasService not found for RulerTool");
2609
+ return;
2610
+ }
2611
+ const configService = context.services.get("ConfigurationService");
2612
+ if (configService) {
2613
+ this.unit = configService.get("ruler.unit", this.unit);
2614
+ this.thickness = configService.get("ruler.thickness", this.thickness);
2615
+ this.backgroundColor = configService.get(
2616
+ "ruler.backgroundColor",
2617
+ this.backgroundColor
2618
+ );
2619
+ this.textColor = configService.get("ruler.textColor", this.textColor);
2620
+ this.lineColor = configService.get("ruler.lineColor", this.lineColor);
2621
+ this.fontSize = configService.get("ruler.fontSize", this.fontSize);
2622
+ configService.onAnyChange((e) => {
2623
+ if (e.key.startsWith("ruler.")) {
2624
+ const prop = e.key.split(".")[1];
2625
+ if (prop && prop in this) {
2626
+ this[prop] = e.value;
2627
+ this.updateRuler();
1489
2628
  }
1490
2629
  }
1491
- },
1492
- setTheme: {
1493
- execute: (editor, theme) => {
1494
- const newOptions = { ...this.options, ...theme };
1495
- if (JSON.stringify(newOptions) === JSON.stringify(this.options)) return true;
1496
- this.options = newOptions;
1497
- this.updateRuler(editor);
1498
- return true;
2630
+ });
2631
+ }
2632
+ this.createLayer();
2633
+ this.updateRuler();
2634
+ }
2635
+ deactivate(context) {
2636
+ this.destroyLayer();
2637
+ this.canvasService = void 0;
2638
+ }
2639
+ contribute() {
2640
+ return {
2641
+ [ContributionPointIds7.CONFIGURATIONS]: [
2642
+ {
2643
+ id: "ruler.unit",
2644
+ type: "select",
2645
+ label: "Unit",
2646
+ options: ["px", "mm", "cm", "in"],
2647
+ default: "px"
2648
+ },
2649
+ {
2650
+ id: "ruler.thickness",
2651
+ type: "number",
2652
+ label: "Thickness",
2653
+ min: 10,
2654
+ max: 100,
2655
+ default: 20
2656
+ },
2657
+ {
2658
+ id: "ruler.backgroundColor",
2659
+ type: "color",
2660
+ label: "Background Color",
2661
+ default: "#f0f0f0"
2662
+ },
2663
+ {
2664
+ id: "ruler.textColor",
2665
+ type: "color",
2666
+ label: "Text Color",
2667
+ default: "#333333"
2668
+ },
2669
+ {
2670
+ id: "ruler.lineColor",
2671
+ type: "color",
2672
+ label: "Line Color",
2673
+ default: "#999999"
2674
+ },
2675
+ {
2676
+ id: "ruler.fontSize",
2677
+ type: "number",
2678
+ label: "Font Size",
2679
+ min: 8,
2680
+ max: 24,
2681
+ default: 10
2682
+ }
2683
+ ],
2684
+ [ContributionPointIds7.COMMANDS]: [
2685
+ {
2686
+ command: "setUnit",
2687
+ title: "Set Ruler Unit",
2688
+ handler: (unit) => {
2689
+ if (this.unit === unit) return true;
2690
+ this.unit = unit;
2691
+ this.updateRuler();
2692
+ return true;
2693
+ }
1499
2694
  },
1500
- schema: {
1501
- theme: {
1502
- type: "object",
1503
- label: "Theme",
1504
- required: true
2695
+ {
2696
+ command: "setTheme",
2697
+ title: "Set Ruler Theme",
2698
+ handler: (theme) => {
2699
+ const oldState = {
2700
+ backgroundColor: this.backgroundColor,
2701
+ textColor: this.textColor,
2702
+ lineColor: this.lineColor,
2703
+ fontSize: this.fontSize,
2704
+ thickness: this.thickness
2705
+ };
2706
+ const newState = { ...oldState, ...theme };
2707
+ if (JSON.stringify(newState) === JSON.stringify(oldState))
2708
+ return true;
2709
+ Object.assign(this, newState);
2710
+ this.updateRuler();
2711
+ return true;
1505
2712
  }
1506
2713
  }
1507
- }
2714
+ ]
1508
2715
  };
1509
2716
  }
1510
- onMount(editor) {
1511
- this.createLayer(editor);
1512
- this.updateRuler(editor);
1513
- }
1514
- onUnmount(editor) {
1515
- this.destroyLayer(editor);
1516
- }
1517
- onUpdate(editor, state) {
1518
- this.updateRuler(editor);
2717
+ getLayer() {
2718
+ var _a;
2719
+ return (_a = this.canvasService) == null ? void 0 : _a.getLayer("ruler-overlay");
1519
2720
  }
1520
- onDestroy(editor) {
1521
- this.destroyLayer(editor);
1522
- }
1523
- getLayer(editor) {
1524
- return editor.canvas.getObjects().find((obj) => {
1525
- var _a;
1526
- return ((_a = obj.data) == null ? void 0 : _a.id) === "ruler-overlay";
2721
+ createLayer() {
2722
+ if (!this.canvasService) return;
2723
+ const canvas = this.canvasService.canvas;
2724
+ const width = canvas.width || 800;
2725
+ const height = canvas.height || 600;
2726
+ const layer = this.canvasService.createLayer("ruler-overlay", {
2727
+ width,
2728
+ height,
2729
+ selectable: false,
2730
+ evented: false,
2731
+ left: 0,
2732
+ top: 0,
2733
+ originX: "left",
2734
+ originY: "top"
1527
2735
  });
2736
+ canvas.bringObjectToFront(layer);
1528
2737
  }
1529
- createLayer(editor) {
1530
- let layer = this.getLayer(editor);
1531
- if (!layer) {
1532
- const width = editor.canvas.width || 800;
1533
- const height = editor.canvas.height || 600;
1534
- layer = new PooderLayer6([], {
1535
- width,
1536
- height,
1537
- selectable: false,
1538
- evented: false,
1539
- data: { id: "ruler-overlay" }
1540
- });
1541
- editor.canvas.add(layer);
1542
- }
1543
- editor.canvas.bringObjectToFront(layer);
1544
- }
1545
- destroyLayer(editor) {
1546
- const layer = this.getLayer(editor);
2738
+ destroyLayer() {
2739
+ if (!this.canvasService) return;
2740
+ const layer = this.getLayer();
1547
2741
  if (layer) {
1548
- editor.canvas.remove(layer);
2742
+ this.canvasService.canvas.remove(layer);
1549
2743
  }
1550
2744
  }
1551
- updateRuler(editor) {
1552
- const layer = this.getLayer(editor);
2745
+ updateRuler() {
2746
+ if (!this.canvasService) return;
2747
+ const layer = this.getLayer();
1553
2748
  if (!layer) return;
1554
2749
  layer.remove(...layer.getObjects());
1555
- const { thickness, backgroundColor, lineColor, textColor, fontSize } = this.options;
1556
- const width = editor.canvas.width || 800;
1557
- const height = editor.canvas.height || 600;
1558
- const topBg = new Rect3({
2750
+ const { thickness, backgroundColor, lineColor, textColor, fontSize } = this;
2751
+ const width = this.canvasService.canvas.width || 800;
2752
+ const height = this.canvasService.canvas.height || 600;
2753
+ const topBg = new Rect2({
1559
2754
  left: 0,
1560
2755
  top: 0,
1561
2756
  width,
@@ -1564,7 +2759,7 @@ var RulerTool = class {
1564
2759
  selectable: false,
1565
2760
  evented: false
1566
2761
  });
1567
- const leftBg = new Rect3({
2762
+ const leftBg = new Rect2({
1568
2763
  left: 0,
1569
2764
  top: 0,
1570
2765
  width: thickness,
@@ -1573,7 +2768,7 @@ var RulerTool = class {
1573
2768
  selectable: false,
1574
2769
  evented: false
1575
2770
  });
1576
- const cornerBg = new Rect3({
2771
+ const cornerBg = new Rect2({
1577
2772
  left: 0,
1578
2773
  top: 0,
1579
2774
  width: thickness,
@@ -1584,7 +2779,9 @@ var RulerTool = class {
1584
2779
  selectable: false,
1585
2780
  evented: false
1586
2781
  });
1587
- layer.add(topBg, leftBg, cornerBg);
2782
+ layer.add(topBg);
2783
+ layer.add(leftBg);
2784
+ layer.add(cornerBg);
1588
2785
  const step = 100;
1589
2786
  const subStep = 10;
1590
2787
  const midStep = 50;
@@ -1642,16 +2839,175 @@ var RulerTool = class {
1642
2839
  layer.add(text);
1643
2840
  }
1644
2841
  }
1645
- editor.canvas.bringObjectToFront(layer);
1646
- editor.canvas.requestRenderAll();
2842
+ this.canvasService.canvas.bringObjectToFront(layer);
2843
+ this.canvasService.canvas.requestRenderAll();
2844
+ }
2845
+ };
2846
+
2847
+ // src/mirror.ts
2848
+ import {
2849
+ ContributionPointIds as ContributionPointIds8
2850
+ } from "@pooder/core";
2851
+ var MirrorTool = class {
2852
+ constructor(options) {
2853
+ this.id = "pooder.kit.mirror";
2854
+ this.metadata = {
2855
+ name: "MirrorTool"
2856
+ };
2857
+ this.enabled = false;
2858
+ if (options) {
2859
+ Object.assign(this, options);
2860
+ }
2861
+ }
2862
+ toJSON() {
2863
+ return {
2864
+ enabled: this.enabled
2865
+ };
2866
+ }
2867
+ loadFromJSON(json) {
2868
+ this.enabled = json.enabled;
2869
+ }
2870
+ activate(context) {
2871
+ this.canvasService = context.services.get("CanvasService");
2872
+ if (!this.canvasService) {
2873
+ console.warn("CanvasService not found for MirrorTool");
2874
+ return;
2875
+ }
2876
+ const configService = context.services.get("ConfigurationService");
2877
+ if (configService) {
2878
+ this.enabled = configService.get("mirror.enabled", this.enabled);
2879
+ configService.onAnyChange((e) => {
2880
+ if (e.key === "mirror.enabled") {
2881
+ this.applyMirror(e.value);
2882
+ }
2883
+ });
2884
+ }
2885
+ if (this.enabled) {
2886
+ this.applyMirror(true);
2887
+ }
2888
+ }
2889
+ deactivate(context) {
2890
+ this.applyMirror(false);
2891
+ this.canvasService = void 0;
2892
+ }
2893
+ contribute() {
2894
+ return {
2895
+ [ContributionPointIds8.CONFIGURATIONS]: [
2896
+ {
2897
+ id: "mirror.enabled",
2898
+ type: "boolean",
2899
+ label: "Enable Mirror",
2900
+ default: false
2901
+ }
2902
+ ],
2903
+ [ContributionPointIds8.COMMANDS]: [
2904
+ {
2905
+ command: "setMirror",
2906
+ title: "Set Mirror",
2907
+ handler: (enabled) => {
2908
+ this.applyMirror(enabled);
2909
+ return true;
2910
+ }
2911
+ }
2912
+ ]
2913
+ };
2914
+ }
2915
+ applyMirror(enabled) {
2916
+ if (!this.canvasService) return;
2917
+ const canvas = this.canvasService.canvas;
2918
+ if (!canvas) return;
2919
+ const width = canvas.width || 800;
2920
+ let vpt = canvas.viewportTransform || [1, 0, 0, 1, 0, 0];
2921
+ vpt = [...vpt];
2922
+ const isFlipped = vpt[0] < 0;
2923
+ if (enabled && !isFlipped) {
2924
+ vpt[0] = -vpt[0];
2925
+ vpt[4] = width - vpt[4];
2926
+ canvas.setViewportTransform(vpt);
2927
+ canvas.requestRenderAll();
2928
+ this.enabled = true;
2929
+ } else if (!enabled && isFlipped) {
2930
+ vpt[0] = -vpt[0];
2931
+ vpt[4] = width - vpt[4];
2932
+ canvas.setViewportTransform(vpt);
2933
+ canvas.requestRenderAll();
2934
+ this.enabled = false;
2935
+ }
2936
+ }
2937
+ };
2938
+
2939
+ // src/CanvasService.ts
2940
+ import { Canvas, Group as Group2 } from "fabric";
2941
+ var CanvasService = class {
2942
+ constructor(el, options) {
2943
+ if (el instanceof Canvas) {
2944
+ this.canvas = el;
2945
+ } else {
2946
+ this.canvas = new Canvas(el, {
2947
+ preserveObjectStacking: true,
2948
+ ...options
2949
+ });
2950
+ }
2951
+ }
2952
+ dispose() {
2953
+ this.canvas.dispose();
2954
+ }
2955
+ /**
2956
+ * Get a layer (Group) by its ID.
2957
+ * We assume layers are Groups directly on the canvas with a data.id property.
2958
+ */
2959
+ getLayer(id) {
2960
+ return this.canvas.getObjects().find((obj) => {
2961
+ var _a;
2962
+ return ((_a = obj.data) == null ? void 0 : _a.id) === id;
2963
+ });
2964
+ }
2965
+ /**
2966
+ * Create a layer (Group) with the given ID if it doesn't exist.
2967
+ */
2968
+ createLayer(id, options = {}) {
2969
+ let layer = this.getLayer(id);
2970
+ if (!layer) {
2971
+ const defaultOptions = {
2972
+ selectable: false,
2973
+ evented: false,
2974
+ ...options,
2975
+ data: { ...options.data, id }
2976
+ };
2977
+ layer = new Group2([], defaultOptions);
2978
+ this.canvas.add(layer);
2979
+ }
2980
+ return layer;
2981
+ }
2982
+ /**
2983
+ * Find an object by ID, optionally within a specific layer.
2984
+ */
2985
+ getObject(id, layerId) {
2986
+ if (layerId) {
2987
+ const layer = this.getLayer(layerId);
2988
+ if (!layer) return void 0;
2989
+ return layer.getObjects().find((obj) => {
2990
+ var _a;
2991
+ return ((_a = obj.data) == null ? void 0 : _a.id) === id;
2992
+ });
2993
+ }
2994
+ return this.canvas.getObjects().find((obj) => {
2995
+ var _a;
2996
+ return ((_a = obj.data) == null ? void 0 : _a.id) === id;
2997
+ });
2998
+ }
2999
+ requestRenderAll() {
3000
+ this.canvas.requestRenderAll();
1647
3001
  }
1648
3002
  };
1649
3003
  export {
1650
3004
  BackgroundTool,
3005
+ CanvasService,
1651
3006
  DielineTool,
1652
3007
  FilmTool,
1653
3008
  HoleTool,
1654
3009
  ImageTool,
3010
+ MirrorTool,
1655
3011
  RulerTool,
1656
3012
  WhiteInkTool
1657
3013
  };