@pooder/kit 2.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/src/dieline.ts CHANGED
@@ -1,133 +1,386 @@
1
1
  import {
2
- Command,
3
- Editor,
4
- EditorState,
5
2
  Extension,
6
- OptionSchema,
7
- Rect,
8
- Circle,
9
- Ellipse,
10
- Path,
11
- PooderLayer,
12
- Pattern,
3
+ ExtensionContext,
4
+ ContributionPointIds,
5
+ CommandContribution,
6
+ ConfigurationContribution,
13
7
  } from "@pooder/core";
8
+ import { Path, Pattern } from "fabric";
9
+ import CanvasService from "./CanvasService";
10
+ import { ImageTracer } from "./tracer";
11
+ import { Coordinate } from "./coordinate";
14
12
  import {
15
13
  generateDielinePath,
16
14
  generateMaskPath,
17
15
  generateBleedZonePath,
16
+ getPathBounds,
18
17
  HoleData,
19
18
  } from "./geometry";
20
19
 
21
- export interface DielineToolOptions {
22
- shape: "rect" | "circle" | "ellipse";
23
- width: number;
24
- height: number;
25
- radius: number; // corner radius for rect
26
- position?: { x: number; y: number };
27
- borderLength?: number;
28
- offset: number;
29
- style: "solid" | "dashed";
30
- insideColor: string;
31
- outsideColor: string;
32
- showBleedLines?: boolean;
33
- }
34
-
35
- // Alias for compatibility if needed, or just use DielineToolOptions
36
- export type DielineConfig = DielineToolOptions;
37
-
38
20
  export interface DielineGeometry {
39
- shape: "rect" | "circle" | "ellipse";
21
+ shape: "rect" | "circle" | "ellipse" | "custom";
40
22
  x: number;
41
23
  y: number;
42
24
  width: number;
43
25
  height: number;
44
26
  radius: number;
27
+ offset: number;
28
+ borderLength?: number;
29
+ pathData?: string;
45
30
  }
46
31
 
47
- export class DielineTool implements Extension<DielineToolOptions> {
48
- public name = "DielineTool";
49
- public options: DielineToolOptions = {
50
- shape: "rect",
51
- width: 300,
52
- height: 300,
53
- radius: 0,
54
- offset: 0,
55
- style: "solid",
56
- insideColor: "rgba(0,0,0,0)",
57
- outsideColor: "#ffffff",
58
- showBleedLines: true,
59
- };
60
-
61
- public schema: Record<keyof DielineToolOptions, OptionSchema> = {
62
- shape: {
63
- type: "select",
64
- options: ["rect", "circle", "ellipse"],
65
- label: "Shape",
66
- },
67
- width: { type: "number", min: 10, max: 2000, label: "Width" },
68
- height: { type: "number", min: 10, max: 2000, label: "Height" },
69
- radius: { type: "number", min: 0, max: 500, label: "Corner Radius" },
70
- position: { type: "string", label: "Position" }, // Complex object, simplified for now or need custom handler
71
- borderLength: { type: "number", min: 0, max: 500, label: "Margin" },
72
- offset: { type: "number", min: -100, max: 100, label: "Bleed Offset" },
73
- showBleedLines: { type: "boolean", label: "Show Bleed Lines" },
74
- style: {
75
- type: "select",
76
- options: ["solid", "dashed"],
77
- label: "Line Style",
78
- },
79
- insideColor: { type: "color", label: "Inside Color" },
80
- outsideColor: { type: "color", label: "Outside Color" },
32
+ export class DielineTool implements Extension {
33
+ id = "pooder.kit.dieline";
34
+ public metadata = {
35
+ name: "DielineTool",
81
36
  };
82
37
 
83
- onMount(editor: Editor) {
84
- this.createLayer(editor);
85
- this.updateDieline(editor);
38
+ private shape: "rect" | "circle" | "ellipse" | "custom" = "rect";
39
+ private width: number = 500;
40
+ private height: number = 500;
41
+ private radius: number = 0;
42
+ private offset: number = 0;
43
+ private style: "solid" | "dashed" = "solid";
44
+ private insideColor: string = "rgba(0,0,0,0)";
45
+ private outsideColor: string = "#ffffff";
46
+ private showBleedLines: boolean = true;
47
+ private holes: HoleData[] = [];
48
+ // Position is stored as normalized coordinates (0-1)
49
+ private position?: { x: number; y: number };
50
+ private borderLength?: number;
51
+ private pathData?: string;
52
+
53
+ private canvasService?: CanvasService;
54
+ private context?: ExtensionContext;
55
+
56
+ constructor(
57
+ options?: Partial<{
58
+ shape: "rect" | "circle" | "ellipse" | "custom";
59
+ width: number;
60
+ height: number;
61
+ radius: number;
62
+ // Position is normalized (0-1)
63
+ position: { x: number; y: number };
64
+ borderLength: number;
65
+ offset: number;
66
+ style: "solid" | "dashed";
67
+ insideColor: string;
68
+ outsideColor: string;
69
+ showBleedLines: boolean;
70
+ holes: HoleData[];
71
+ pathData: string;
72
+ }>,
73
+ ) {
74
+ if (options) {
75
+ Object.assign(this, options);
76
+ }
86
77
  }
87
78
 
88
- onUnmount(editor: Editor) {
89
- this.destroyLayer(editor);
79
+ activate(context: ExtensionContext) {
80
+ this.context = context;
81
+ this.canvasService = context.services.get<CanvasService>("CanvasService");
82
+ if (!this.canvasService) {
83
+ console.warn("CanvasService not found for DielineTool");
84
+ return;
85
+ }
86
+
87
+ const configService = context.services.get<any>("ConfigurationService");
88
+ if (configService) {
89
+ // Load initial config
90
+ this.shape = configService.get("dieline.shape", this.shape);
91
+ this.width = configService.get("dieline.width", this.width);
92
+ this.height = configService.get("dieline.height", this.height);
93
+ this.radius = configService.get("dieline.radius", this.radius);
94
+ this.borderLength = configService.get(
95
+ "dieline.borderLength",
96
+ this.borderLength,
97
+ );
98
+ this.offset = configService.get("dieline.offset", this.offset);
99
+ this.style = configService.get("dieline.style", this.style);
100
+ this.insideColor = configService.get(
101
+ "dieline.insideColor",
102
+ this.insideColor,
103
+ );
104
+ this.outsideColor = configService.get(
105
+ "dieline.outsideColor",
106
+ this.outsideColor,
107
+ );
108
+ this.showBleedLines = configService.get(
109
+ "dieline.showBleedLines",
110
+ this.showBleedLines,
111
+ );
112
+ this.holes = configService.get("dieline.holes", this.holes);
113
+ this.pathData = configService.get("dieline.pathData", this.pathData);
114
+
115
+ // Listen for changes
116
+ configService.onAnyChange((e: { key: string; value: any }) => {
117
+ if (e.key.startsWith("dieline.")) {
118
+ const prop = e.key.split(".")[1];
119
+ console.log(
120
+ `[DielineTool] Config change detected: ${e.key} -> ${e.value}`,
121
+ );
122
+ if (prop && prop in this) {
123
+ (this as any)[prop] = e.value;
124
+ this.updateDieline();
125
+ }
126
+ }
127
+ });
128
+ }
129
+
130
+ this.createLayer();
131
+ this.updateDieline();
90
132
  }
91
133
 
92
- onUpdate(editor: Editor, state: EditorState) {
93
- this.updateDieline(editor);
134
+ deactivate(context: ExtensionContext) {
135
+ this.destroyLayer();
136
+ this.canvasService = undefined;
137
+ this.context = undefined;
94
138
  }
95
139
 
96
- onDestroy(editor: Editor) {
97
- this.destroyLayer(editor);
140
+ contribute() {
141
+ return {
142
+ [ContributionPointIds.CONFIGURATIONS]: [
143
+ {
144
+ id: "dieline.shape",
145
+ type: "select",
146
+ label: "Shape",
147
+ options: ["rect", "circle", "ellipse", "custom"],
148
+ default: this.shape,
149
+ },
150
+ {
151
+ id: "dieline.width",
152
+ type: "number",
153
+ label: "Width",
154
+ min: 10,
155
+ max: 2000,
156
+ default: this.width,
157
+ },
158
+ {
159
+ id: "dieline.height",
160
+ type: "number",
161
+ label: "Height",
162
+ min: 10,
163
+ max: 2000,
164
+ default: this.height,
165
+ },
166
+ {
167
+ id: "dieline.radius",
168
+ type: "number",
169
+ label: "Corner Radius",
170
+ min: 0,
171
+ max: 500,
172
+ default: this.radius,
173
+ },
174
+ {
175
+ id: "dieline.position",
176
+ type: "json",
177
+ label: "Position (Normalized)",
178
+ default: this.position,
179
+ },
180
+ {
181
+ id: "dieline.borderLength",
182
+ type: "number",
183
+ label: "Margin",
184
+ min: 0,
185
+ max: 500,
186
+ default: this.borderLength,
187
+ },
188
+ {
189
+ id: "dieline.offset",
190
+ type: "number",
191
+ label: "Bleed Offset",
192
+ min: -100,
193
+ max: 100,
194
+ default: this.offset,
195
+ },
196
+ {
197
+ id: "dieline.showBleedLines",
198
+ type: "boolean",
199
+ label: "Show Bleed Lines",
200
+ default: this.showBleedLines,
201
+ },
202
+ {
203
+ id: "dieline.style",
204
+ type: "select",
205
+ label: "Line Style",
206
+ options: ["solid", "dashed"],
207
+ default: this.style,
208
+ },
209
+ {
210
+ id: "dieline.insideColor",
211
+ type: "color",
212
+ label: "Inside Color",
213
+ default: this.insideColor,
214
+ },
215
+ {
216
+ id: "dieline.outsideColor",
217
+ type: "color",
218
+ label: "Outside Color",
219
+ default: this.outsideColor,
220
+ },
221
+ {
222
+ id: "dieline.holes",
223
+ type: "json",
224
+ label: "Holes",
225
+ default: this.holes,
226
+ },
227
+ ] as ConfigurationContribution[],
228
+ [ContributionPointIds.COMMANDS]: [
229
+ {
230
+ command: "reset",
231
+ title: "Reset Dieline",
232
+ handler: () => {
233
+ this.shape = "rect";
234
+ this.width = 300;
235
+ this.height = 300;
236
+ this.radius = 0;
237
+ this.offset = 0;
238
+ this.style = "solid";
239
+ this.insideColor = "rgba(0,0,0,0)";
240
+ this.outsideColor = "#ffffff";
241
+ this.showBleedLines = true;
242
+ this.holes = [];
243
+ this.pathData = undefined;
244
+ this.updateDieline();
245
+ return true;
246
+ },
247
+ },
248
+ {
249
+ command: "setDimensions",
250
+ title: "Set Dimensions",
251
+ handler: (width: number, height: number) => {
252
+ if (this.width === width && this.height === height) return true;
253
+ this.width = width;
254
+ this.height = height;
255
+ this.updateDieline();
256
+ return true;
257
+ },
258
+ },
259
+ {
260
+ command: "setShape",
261
+ title: "Set Shape",
262
+ handler: (shape: "rect" | "circle" | "ellipse" | "custom") => {
263
+ if (this.shape === shape) return true;
264
+ this.shape = shape;
265
+ this.updateDieline();
266
+ return true;
267
+ },
268
+ },
269
+ {
270
+ command: "setBleed",
271
+ title: "Set Bleed",
272
+ handler: (bleed: number) => {
273
+ if (this.offset === bleed) return true;
274
+ this.offset = bleed;
275
+ this.updateDieline();
276
+ return true;
277
+ },
278
+ },
279
+ {
280
+ command: "setHoles",
281
+ title: "Set Holes",
282
+ handler: (holes: HoleData[]) => {
283
+ this.holes = holes;
284
+ this.updateDieline(false);
285
+ return true;
286
+ },
287
+ },
288
+ {
289
+ command: "getGeometry",
290
+ title: "Get Geometry",
291
+ handler: () => {
292
+ return this.getGeometry();
293
+ },
294
+ },
295
+ {
296
+ command: "exportCutImage",
297
+ title: "Export Cut Image",
298
+ handler: () => {
299
+ return this.exportCutImage();
300
+ },
301
+ },
302
+ {
303
+ command: "detectEdge",
304
+ title: "Detect Edge from Image",
305
+ handler: async (imageUrl: string, options?: any) => {
306
+ try {
307
+ // Pass current dimensions if we want to scale immediately?
308
+ // But wait, the user said "It should be scaled according to width and height".
309
+ // If the user already set width/height on the tool, we should respect it?
310
+ // Or should we set width/height based on the image aspect ratio?
311
+ // Usually for a new trace, we might want to respect the IMAGE aspect ratio but fit into current width/height?
312
+ // Or just replace width/height with image dimensions?
313
+ // Let's assume we want to keep the current "box" size but fit the shape inside?
314
+ // Or if options has width/height use that.
315
+
316
+ // Let's first trace to get the natural shape (and its aspect ratio)
317
+ // Then we can decide how to update this.width/this.height.
318
+
319
+ const pathData = await ImageTracer.trace(imageUrl, options);
320
+
321
+ // We need to set width/height from the path bounds to avoid distortion
322
+ const bounds = getPathBounds(pathData);
323
+
324
+ // If we want to scale the path to specific dimensions, we can do it via ImageTracer options.scaleToWidth/Height
325
+ // But here we got the raw path.
326
+ // Let's update the TOOL's dimensions to match the detected shape's aspect ratio,
327
+ // while keeping the size reasonable (e.g. max dimension 300 or current size).
328
+
329
+ // If current tool size is default 300x300, we might want to resize tool to match image ratio.
330
+ const currentMax = Math.max(this.width, this.height);
331
+ const scale = currentMax / Math.max(bounds.width, bounds.height);
332
+
333
+ this.width = bounds.width * scale;
334
+ this.height = bounds.height * scale;
335
+
336
+ this.shape = "custom";
337
+ this.pathData = pathData;
338
+
339
+ this.updateDieline();
340
+ return pathData;
341
+ } catch (e) {
342
+ console.error("Edge detection failed", e);
343
+ throw e;
344
+ }
345
+ },
346
+ },
347
+ ] as CommandContribution[],
348
+ };
98
349
  }
99
350
 
100
- private getLayer(editor: Editor, id: string) {
101
- return editor.canvas
102
- .getObjects()
103
- .find((obj: any) => obj.data?.id === id) as PooderLayer | undefined;
351
+ private getLayer() {
352
+ return this.canvasService?.getLayer("dieline-overlay");
104
353
  }
105
354
 
106
- private createLayer(editor: Editor) {
107
- let layer = this.getLayer(editor, "dieline-overlay");
355
+ private createLayer() {
356
+ if (!this.canvasService) return;
357
+ const width = this.canvasService.canvas.width || 800;
358
+ const height = this.canvasService.canvas.height || 600;
108
359
 
109
- if (!layer) {
110
- const width = editor.canvas.width || 800;
111
- const height = editor.canvas.height || 600;
360
+ const layer = this.canvasService.createLayer("dieline-overlay", {
361
+ width,
362
+ height,
363
+ selectable: false,
364
+ evented: false,
365
+ });
112
366
 
113
- layer = new PooderLayer([], {
114
- width,
115
- height,
116
- selectable: false,
117
- evented: false,
118
- data: { id: "dieline-overlay" },
119
- } as any);
367
+ this.canvasService.canvas.bringObjectToFront(layer);
120
368
 
121
- editor.canvas.add(layer);
369
+ // Ensure above user layer
370
+ const userLayer = this.canvasService.getLayer("user");
371
+ if (userLayer) {
372
+ const userIndex = this.canvasService.canvas
373
+ .getObjects()
374
+ .indexOf(userLayer);
375
+ this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
122
376
  }
123
-
124
- editor.canvas.bringObjectToFront(layer);
125
377
  }
126
378
 
127
- private destroyLayer(editor: Editor) {
128
- const layer = this.getLayer(editor, "dieline-overlay");
379
+ private destroyLayer() {
380
+ if (!this.canvasService) return;
381
+ const layer = this.getLayer();
129
382
  if (layer) {
130
- editor.canvas.remove(layer);
383
+ this.canvasService.canvas.remove(layer);
131
384
  }
132
385
  }
133
386
 
@@ -156,7 +409,11 @@ export class DielineTool implements Extension<DielineToolOptions> {
156
409
  return new Pattern({ source: canvas, repetition: "repeat" });
157
410
  }
158
411
 
159
- public updateDieline(editor: Editor) {
412
+ public updateDieline(emitEvent: boolean = true) {
413
+ if (!this.canvasService) return;
414
+ const layer = this.getLayer();
415
+ if (!layer) return;
416
+
160
417
  const {
161
418
  shape,
162
419
  radius,
@@ -167,11 +424,12 @@ export class DielineTool implements Extension<DielineToolOptions> {
167
424
  position,
168
425
  borderLength,
169
426
  showBleedLines,
170
- } = this.options;
171
- let { width, height } = this.options;
427
+ holes,
428
+ } = this;
429
+ let { width, height } = this;
172
430
 
173
- const canvasW = editor.canvas.width || 800;
174
- const canvasH = editor.canvas.height || 600;
431
+ const canvasW = this.canvasService.canvas.width || 800;
432
+ const canvasH = this.canvasService.canvas.height || 600;
175
433
 
176
434
  // Handle borderLength (Margin)
177
435
  if (borderLength && borderLength > 0) {
@@ -180,32 +438,26 @@ export class DielineTool implements Extension<DielineToolOptions> {
180
438
  }
181
439
 
182
440
  // Handle Position
183
- const cx = position?.x ?? canvasW / 2;
184
- const cy = position?.y ?? canvasH / 2;
185
-
186
- const layer = this.getLayer(editor, "dieline-overlay");
187
- if (!layer) return;
441
+ // this.position is normalized (0-1). Default to center (0.5, 0.5).
442
+ const normalizedPos = position ?? { x: 0.5, y: 0.5 };
443
+ const cx = Coordinate.toAbsolute(normalizedPos.x, canvasW);
444
+ const cy = Coordinate.toAbsolute(normalizedPos.y, canvasH);
188
445
 
189
446
  // Clear existing objects
190
447
  layer.remove(...layer.getObjects());
191
448
 
192
- // Get Hole Tool and Enforce Constraints
193
- const holeTool = editor.getExtension("HoleTool") as any;
194
- if (holeTool && typeof holeTool.enforceConstraints === "function") {
195
- holeTool.enforceConstraints(editor);
196
- }
197
-
198
- // Get Hole Data
199
- const holes = holeTool ? holeTool.options.holes || [] : [];
200
- const innerRadius = holeTool ? holeTool.options.innerRadius || 15 : 15;
201
- const outerRadius = holeTool ? holeTool.options.outerRadius || 25 : 25;
202
-
203
- const holeData: HoleData[] = holes.map((h: any) => ({
204
- x: h.x,
205
- y: h.y,
206
- innerRadius,
207
- outerRadius,
208
- }));
449
+ // Denormalize Holes for Geometry Generation
450
+ const absoluteHoles = (holes || []).map((h) => {
451
+ const p = Coordinate.denormalizePoint(
452
+ { x: h.x, y: h.y },
453
+ { width: canvasW, height: canvasH },
454
+ );
455
+ return {
456
+ ...h,
457
+ x: p.x,
458
+ y: p.y,
459
+ };
460
+ });
209
461
 
210
462
  // 1. Draw Mask (Outside)
211
463
  const cutW = Math.max(0, width + offset * 2);
@@ -222,7 +474,8 @@ export class DielineTool implements Extension<DielineToolOptions> {
222
474
  radius: cutR,
223
475
  x: cx,
224
476
  y: cy,
225
- holes: holeData,
477
+ holes: absoluteHoles,
478
+ pathData: this.pathData,
226
479
  });
227
480
 
228
481
  const mask = new Path(maskPathData, {
@@ -237,12 +490,7 @@ export class DielineTool implements Extension<DielineToolOptions> {
237
490
  });
238
491
  layer.add(mask);
239
492
 
240
- // 2. Draw Inside Fill (Dieline Shape itself, merged with holes if needed, or just the shape?)
241
- // The user wants "fusion effect" so holes should be part of the dieline visually.
242
- // If insideColor is transparent, it doesn't matter much.
243
- // If insideColor is opaque, we need to punch holes in it too.
244
- // Let's use Paper.js for this too if insideColor is not transparent.
245
-
493
+ // 2. Draw Inside Fill (Dieline Shape itself, merged with holes if needed)
246
494
  if (
247
495
  insideColor &&
248
496
  insideColor !== "transparent" &&
@@ -256,7 +504,8 @@ export class DielineTool implements Extension<DielineToolOptions> {
256
504
  radius: cutR,
257
505
  x: cx,
258
506
  y: cy,
259
- holes: holeData,
507
+ holes: absoluteHoles,
508
+ pathData: this.pathData,
260
509
  });
261
510
 
262
511
  const insideObj = new Path(productPathData, {
@@ -280,7 +529,8 @@ export class DielineTool implements Extension<DielineToolOptions> {
280
529
  radius,
281
530
  x: cx,
282
531
  y: cy,
283
- holes: holeData,
532
+ holes: absoluteHoles,
533
+ pathData: this.pathData,
284
534
  },
285
535
  offset,
286
536
  );
@@ -310,7 +560,8 @@ export class DielineTool implements Extension<DielineToolOptions> {
310
560
  radius: cutR,
311
561
  x: cx,
312
562
  y: cy,
313
- holes: holeData,
563
+ holes: absoluteHoles,
564
+ pathData: this.pathData,
314
565
  });
315
566
 
316
567
  const offsetBorderObj = new Path(offsetPathData, {
@@ -328,8 +579,8 @@ export class DielineTool implements Extension<DielineToolOptions> {
328
579
 
329
580
  // 4. Draw Dieline (Visual Border)
330
581
  // This should outline the product shape AND the holes.
331
- // Paper.js `generateDielinePath` returns exactly this (Dieline - Holes).
332
-
582
+ // NOTE: We need to use absoluteHoles (denormalized) here, NOT holes (normalized 0-1)
583
+ // generateDielinePath expects holes to be in absolute coordinates (matching width/height scale)
333
584
  const borderPathData = generateDielinePath({
334
585
  shape,
335
586
  width: width,
@@ -337,7 +588,8 @@ export class DielineTool implements Extension<DielineToolOptions> {
337
588
  radius: radius,
338
589
  x: cx,
339
590
  y: cy,
340
- holes: holeData,
591
+ holes: absoluteHoles, // FIX: Use absoluteHoles instead of holes
592
+ pathData: this.pathData,
341
593
  });
342
594
 
343
595
  const borderObj = new Path(borderPathData, {
@@ -353,209 +605,46 @@ export class DielineTool implements Extension<DielineToolOptions> {
353
605
 
354
606
  layer.add(borderObj);
355
607
 
356
- editor.canvas.requestRenderAll();
357
- }
608
+ // Enforce z-index: Dieline > User
609
+ const userLayer = this.canvasService.getLayer("user");
610
+ if (layer && userLayer) {
611
+ const layerIndex = this.canvasService.canvas.getObjects().indexOf(layer);
612
+ const userIndex = this.canvasService.canvas
613
+ .getObjects()
614
+ .indexOf(userLayer);
615
+ if (layerIndex < userIndex) {
616
+ this.canvasService.canvas.moveObjectTo(layer, userIndex + 1);
617
+ }
618
+ } else {
619
+ // If no user layer, just bring to front (safe default)
620
+ this.canvasService.canvas.bringObjectToFront(layer);
621
+ }
358
622
 
359
- commands: Record<string, Command> = {
360
- reset: {
361
- execute: (editor: Editor) => {
362
- this.options = {
363
- shape: "rect",
364
- width: 300,
365
- height: 300,
366
- radius: 0,
367
- offset: 0,
368
- style: "solid",
369
- insideColor: "rgba(0,0,0,0)",
370
- outsideColor: "#ffffff",
371
- showBleedLines: true,
372
- };
373
- this.updateDieline(editor);
374
- return true;
375
- },
376
- },
377
- destroy: {
378
- execute: (editor: Editor) => {
379
- this.destroyLayer(editor);
380
- return true;
381
- },
382
- },
383
- setDimensions: {
384
- execute: (editor: Editor, width: number, height: number) => {
385
- if (this.options.width === width && this.options.height === height)
386
- return true;
387
- this.options.width = width;
388
- this.options.height = height;
389
- this.updateDieline(editor);
390
- return true;
391
- },
392
- schema: {
393
- width: {
394
- type: "number",
395
- label: "Width",
396
- min: 10,
397
- max: 2000,
398
- required: true,
399
- },
400
- height: {
401
- type: "number",
402
- label: "Height",
403
- min: 10,
404
- max: 2000,
405
- required: true,
406
- },
407
- },
408
- },
409
- setShape: {
410
- execute: (editor: Editor, shape: "rect" | "circle" | "ellipse") => {
411
- if (this.options.shape === shape) return true;
412
- this.options.shape = shape;
413
- this.updateDieline(editor);
414
- return true;
415
- },
416
- schema: {
417
- shape: {
418
- type: "string",
419
- label: "Shape",
420
- options: ["rect", "circle", "ellipse"],
421
- required: true,
422
- },
423
- },
424
- },
425
- setBleed: {
426
- execute: (editor: Editor, bleed: number) => {
427
- if (this.options.offset === bleed) return true;
428
- this.options.offset = bleed;
429
- this.updateDieline(editor);
430
- return true;
431
- },
432
- schema: {
433
- bleed: {
434
- type: "number",
435
- label: "Bleed",
436
- min: -100,
437
- max: 100,
438
- required: true,
439
- },
440
- },
441
- },
442
- exportCutImage: {
443
- execute: (editor: Editor) => {
444
- // 1. Generate Path Data
445
- const { shape, width, height, radius, position } = this.options;
446
- const canvasW = editor.canvas.width || 800;
447
- const canvasH = editor.canvas.height || 600;
448
- const cx = position?.x ?? canvasW / 2;
449
- const cy = position?.y ?? canvasH / 2;
450
-
451
- const holeTool = editor.getExtension("HoleTool") as any;
452
- const holes = holeTool ? holeTool.options.holes || [] : [];
453
- const innerRadius = holeTool ? holeTool.options.innerRadius || 15 : 15;
454
- const outerRadius = holeTool ? holeTool.options.outerRadius || 25 : 25;
455
- const holeData = holes.map((h: any) => ({
456
- x: h.x,
457
- y: h.y,
458
- innerRadius,
459
- outerRadius,
460
- }));
461
-
462
- const pathData = generateDielinePath({
463
- shape,
464
- width,
465
- height,
466
- radius,
467
- x: cx,
468
- y: cy,
469
- holes: holeData,
470
- });
471
-
472
- // 2. Create Clip Path
473
- // @ts-ignore
474
- const clipPath = new Path(pathData, {
475
- left: 0,
476
- top: 0,
477
- originX: "left",
478
- originY: "top",
479
- absolutePositioned: true,
480
- });
481
-
482
- // 3. Hide UI Layers
483
- const layer = this.getLayer(editor, "dieline-overlay");
484
- const wasVisible = layer?.visible ?? true;
485
- if (layer) layer.visible = false;
486
-
487
- // Hide hole markers
488
- const holeMarkers = editor.canvas
489
- .getObjects()
490
- .filter((o: any) => o.data?.type === "hole-marker");
491
- holeMarkers.forEach((o) => (o.visible = false));
492
-
493
- // Hide Ruler Overlay
494
- const rulerLayer = editor.canvas
495
- .getObjects()
496
- .find((obj: any) => obj.data?.id === "ruler-overlay");
497
- const rulerWasVisible = rulerLayer?.visible ?? true;
498
- if (rulerLayer) rulerLayer.visible = false;
499
-
500
- // 4. Apply Clip & Export
501
- const originalClip = editor.canvas.clipPath;
502
- editor.canvas.clipPath = clipPath;
503
-
504
- const bbox = clipPath.getBoundingRect();
505
- // Adjust hole coordinates to be relative to the bounding box
506
- const holeDataRelative = holes.map((h: any) => ({
507
- x: h.x - bbox.left,
508
- y: h.y - bbox.top,
509
- innerRadius,
510
- outerRadius,
511
- }));
512
-
513
- const clipPathCorrected = new Path(pathData, {
514
- absolutePositioned: true,
515
- left: 0,
516
- top: 0,
517
- });
518
-
519
- const tempPath = new Path(pathData);
520
- const tempBounds = tempPath.getBoundingRect();
521
-
522
- clipPathCorrected.set({
523
- left: tempBounds.left,
524
- top: tempBounds.top,
525
- originX: "left",
526
- originY: "top",
527
- });
528
-
529
- // 4. Apply Clip & Export
530
- editor.canvas.clipPath = clipPathCorrected;
531
-
532
- const exportBbox = clipPathCorrected.getBoundingRect();
533
- const dataURL = editor.canvas.toDataURL({
534
- format: "png",
535
- multiplier: 2,
536
- left: exportBbox.left,
537
- top: exportBbox.top,
538
- width: exportBbox.width,
539
- height: exportBbox.height,
540
- });
541
-
542
- // 5. Restore
543
- editor.canvas.clipPath = originalClip;
544
- if (layer) layer.visible = wasVisible;
545
- if (rulerLayer) rulerLayer.visible = rulerWasVisible;
546
- holeMarkers.forEach((o) => (o.visible = true));
547
- editor.canvas.requestRenderAll();
548
-
549
- return dataURL;
550
- },
551
- },
552
- };
623
+ // Ensure Ruler is above Dieline if it exists
624
+ const rulerLayer = this.canvasService.getLayer("ruler-overlay");
625
+ if (rulerLayer) {
626
+ this.canvasService.canvas.bringObjectToFront(rulerLayer);
627
+ }
553
628
 
554
- public getGeometry(editor: Editor): DielineGeometry | null {
555
- const { shape, width, height, radius, position, borderLength } =
556
- this.options;
557
- const canvasW = editor.canvas.width || 800;
558
- const canvasH = editor.canvas.height || 600;
629
+ layer.dirty = true;
630
+ this.canvasService.requestRenderAll();
631
+
632
+ // Emit change event so other tools (like HoleTool) can react
633
+ // Only emit if requested (to avoid loops when updating non-geometry props like holes)
634
+ if (emitEvent && this.context) {
635
+ const geometry = this.getGeometry();
636
+ if (geometry) {
637
+ this.context.eventBus.emit("dieline:geometry:change", geometry);
638
+ }
639
+ }
640
+ }
641
+
642
+ public getGeometry(): DielineGeometry | null {
643
+ if (!this.canvasService) return null;
644
+ const { shape, width, height, radius, position, borderLength, offset } =
645
+ this;
646
+ const canvasW = this.canvasService.canvas.width || 800;
647
+ const canvasH = this.canvasService.canvas.height || 600;
559
648
 
560
649
  let visualWidth = width;
561
650
  let visualHeight = height;
@@ -565,8 +654,8 @@ export class DielineTool implements Extension<DielineToolOptions> {
565
654
  visualHeight = Math.max(0, canvasH - borderLength * 2);
566
655
  }
567
656
 
568
- const cx = position?.x ?? canvasW / 2;
569
- const cy = position?.y ?? canvasH / 2;
657
+ const cx = Coordinate.toAbsolute(position?.x ?? 0.5, canvasW);
658
+ const cy = Coordinate.toAbsolute(position?.y ?? 0.5, canvasH);
570
659
 
571
660
  return {
572
661
  shape,
@@ -575,6 +664,117 @@ export class DielineTool implements Extension<DielineToolOptions> {
575
664
  width: visualWidth,
576
665
  height: visualHeight,
577
666
  radius,
667
+ offset,
668
+ borderLength,
669
+ pathData: this.pathData,
578
670
  };
579
671
  }
672
+
673
+ public exportCutImage() {
674
+ if (!this.canvasService) return null;
675
+ const canvas = this.canvasService.canvas;
676
+
677
+ // 1. Generate Path Data
678
+ const { shape, width, height, radius, position, holes } = this;
679
+ const canvasW = canvas.width || 800;
680
+ const canvasH = canvas.height || 600;
681
+ const cx = Coordinate.toAbsolute(position?.x ?? 0.5, canvasW);
682
+ const cy = Coordinate.toAbsolute(position?.y ?? 0.5, canvasH);
683
+
684
+ // Denormalize Holes for Export
685
+ const absoluteHoles = (holes || []).map((h) => {
686
+ const p = Coordinate.denormalizePoint(
687
+ { x: h.x, y: h.y },
688
+ { width: canvasW, height: canvasH },
689
+ );
690
+ return {
691
+ ...h,
692
+ x: p.x,
693
+ y: p.y,
694
+ };
695
+ });
696
+
697
+ const pathData = generateDielinePath({
698
+ shape,
699
+ width,
700
+ height,
701
+ radius,
702
+ x: cx,
703
+ y: cy,
704
+ holes: absoluteHoles,
705
+ pathData: this.pathData,
706
+ });
707
+
708
+ // 2. Create Clip Path
709
+ // @ts-ignore
710
+ const clipPath = new Path(pathData, {
711
+ left: 0,
712
+ top: 0,
713
+ originX: "left",
714
+ originY: "top",
715
+ absolutePositioned: true,
716
+ });
717
+
718
+ // 3. Hide UI Layers
719
+ const layer = this.getLayer();
720
+ const wasVisible = layer?.visible ?? true;
721
+ if (layer) layer.visible = false;
722
+
723
+ // Hide hole markers
724
+ const holeMarkers = canvas
725
+ .getObjects()
726
+ .filter((o: any) => o.data?.type === "hole-marker");
727
+ holeMarkers.forEach((o) => (o.visible = false));
728
+
729
+ // Hide Ruler Overlay
730
+ const rulerLayer = canvas
731
+ .getObjects()
732
+ .find((obj: any) => obj.data?.id === "ruler-overlay");
733
+ const rulerWasVisible = rulerLayer?.visible ?? true;
734
+ if (rulerLayer) rulerLayer.visible = false;
735
+
736
+ // 4. Apply Clip & Export
737
+ const originalClip = canvas.clipPath;
738
+ canvas.clipPath = clipPath;
739
+
740
+ const bbox = clipPath.getBoundingRect();
741
+
742
+ const clipPathCorrected = new Path(pathData, {
743
+ absolutePositioned: true,
744
+ left: 0,
745
+ top: 0,
746
+ });
747
+
748
+ const tempPath = new Path(pathData);
749
+ const tempBounds = tempPath.getBoundingRect();
750
+
751
+ clipPathCorrected.set({
752
+ left: tempBounds.left,
753
+ top: tempBounds.top,
754
+ originX: "left",
755
+ originY: "top",
756
+ });
757
+
758
+ // 4. Apply Clip & Export
759
+ canvas.clipPath = clipPathCorrected;
760
+
761
+ const exportBbox = clipPathCorrected.getBoundingRect();
762
+ const dataURL = canvas.toDataURL({
763
+ format: "png",
764
+ multiplier: 2,
765
+ left: exportBbox.left,
766
+ top: exportBbox.top,
767
+ width: exportBbox.width,
768
+ height: exportBbox.height,
769
+ });
770
+
771
+ // 5. Restore
772
+ canvas.clipPath = originalClip;
773
+ if (layer) layer.visible = wasVisible;
774
+ if (rulerLayer) rulerLayer.visible = rulerWasVisible;
775
+ holeMarkers.forEach((o) => (o.visible = true));
776
+ canvas.requestRenderAll();
777
+
778
+ return dataURL;
779
+ }
580
780
  }