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