@pooder/kit 3.1.0 → 3.2.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/CHANGELOG.md +11 -0
- package/dist/index.d.mts +39 -24
- package/dist/index.d.ts +39 -24
- package/dist/index.js +740 -534
- package/dist/index.mjs +744 -538
- package/package.json +2 -2
- package/src/coordinate.ts +57 -0
- package/src/dieline.ts +186 -129
- package/src/geometry.ts +19 -10
- package/src/hole.ts +88 -31
- package/src/image.ts +334 -365
- package/src/ruler.ts +295 -120
package/src/ruler.ts
CHANGED
|
@@ -5,8 +5,9 @@ import {
|
|
|
5
5
|
CommandContribution,
|
|
6
6
|
ConfigurationContribution,
|
|
7
7
|
} from "@pooder/core";
|
|
8
|
-
import { Rect, Line, Text } from "fabric";
|
|
8
|
+
import { Rect, Line, Text, Group, Polygon } from "fabric";
|
|
9
9
|
import CanvasService from "./CanvasService";
|
|
10
|
+
import { Coordinate, Unit } from "./coordinate";
|
|
10
11
|
|
|
11
12
|
export class RulerTool implements Extension {
|
|
12
13
|
id = "pooder.kit.ruler";
|
|
@@ -15,18 +16,24 @@ export class RulerTool implements Extension {
|
|
|
15
16
|
name: "RulerTool",
|
|
16
17
|
};
|
|
17
18
|
|
|
18
|
-
private unit: "px" | "mm" | "cm" | "in" = "px";
|
|
19
19
|
private thickness: number = 20;
|
|
20
|
+
private gap: number = 15;
|
|
20
21
|
private backgroundColor: string = "#f0f0f0";
|
|
21
22
|
private textColor: string = "#333333";
|
|
22
23
|
private lineColor: string = "#999999";
|
|
23
24
|
private fontSize: number = 10;
|
|
24
25
|
|
|
26
|
+
// Dieline context for sync
|
|
27
|
+
private dielineWidth: number = 500;
|
|
28
|
+
private dielineHeight: number = 500;
|
|
29
|
+
private dielineUnit: Unit = "mm";
|
|
30
|
+
private dielinePadding: number | string = 40;
|
|
31
|
+
private dielineOffset: number = 0;
|
|
32
|
+
|
|
25
33
|
private canvasService?: CanvasService;
|
|
26
34
|
|
|
27
35
|
constructor(
|
|
28
36
|
options?: Partial<{
|
|
29
|
-
unit: "px" | "mm" | "cm" | "in";
|
|
30
37
|
thickness: number;
|
|
31
38
|
backgroundColor: string;
|
|
32
39
|
textColor: string;
|
|
@@ -49,8 +56,8 @@ export class RulerTool implements Extension {
|
|
|
49
56
|
const configService = context.services.get<any>("ConfigurationService");
|
|
50
57
|
if (configService) {
|
|
51
58
|
// Load initial config
|
|
52
|
-
this.unit = configService.get("ruler.unit", this.unit);
|
|
53
59
|
this.thickness = configService.get("ruler.thickness", this.thickness);
|
|
60
|
+
this.gap = configService.get("ruler.gap", this.gap);
|
|
54
61
|
this.backgroundColor = configService.get(
|
|
55
62
|
"ruler.backgroundColor",
|
|
56
63
|
this.backgroundColor,
|
|
@@ -59,14 +66,42 @@ export class RulerTool implements Extension {
|
|
|
59
66
|
this.lineColor = configService.get("ruler.lineColor", this.lineColor);
|
|
60
67
|
this.fontSize = configService.get("ruler.fontSize", this.fontSize);
|
|
61
68
|
|
|
69
|
+
// Load Dieline Config
|
|
70
|
+
this.dielineUnit = configService.get("dieline.unit", this.dielineUnit);
|
|
71
|
+
this.dielineWidth = configService.get("dieline.width", this.dielineWidth);
|
|
72
|
+
this.dielineHeight = configService.get(
|
|
73
|
+
"dieline.height",
|
|
74
|
+
this.dielineHeight,
|
|
75
|
+
);
|
|
76
|
+
this.dielinePadding = configService.get(
|
|
77
|
+
"dieline.padding",
|
|
78
|
+
this.dielinePadding,
|
|
79
|
+
);
|
|
80
|
+
this.dielineOffset = configService.get(
|
|
81
|
+
"dieline.offset",
|
|
82
|
+
this.dielineOffset,
|
|
83
|
+
);
|
|
84
|
+
|
|
62
85
|
// Listen for changes
|
|
63
86
|
configService.onAnyChange((e: { key: string; value: any }) => {
|
|
87
|
+
let shouldUpdate = false;
|
|
64
88
|
if (e.key.startsWith("ruler.")) {
|
|
65
89
|
const prop = e.key.split(".")[1];
|
|
66
90
|
if (prop && prop in this) {
|
|
67
91
|
(this as any)[prop] = e.value;
|
|
68
|
-
|
|
92
|
+
shouldUpdate = true;
|
|
69
93
|
}
|
|
94
|
+
} else if (e.key.startsWith("dieline.")) {
|
|
95
|
+
if (e.key === "dieline.unit") this.dielineUnit = e.value;
|
|
96
|
+
if (e.key === "dieline.width") this.dielineWidth = e.value;
|
|
97
|
+
if (e.key === "dieline.height") this.dielineHeight = e.value;
|
|
98
|
+
if (e.key === "dieline.padding") this.dielinePadding = e.value;
|
|
99
|
+
if (e.key === "dieline.offset") this.dielineOffset = e.value;
|
|
100
|
+
shouldUpdate = true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (shouldUpdate) {
|
|
104
|
+
this.updateRuler();
|
|
70
105
|
}
|
|
71
106
|
});
|
|
72
107
|
}
|
|
@@ -83,13 +118,6 @@ export class RulerTool implements Extension {
|
|
|
83
118
|
contribute() {
|
|
84
119
|
return {
|
|
85
120
|
[ContributionPointIds.CONFIGURATIONS]: [
|
|
86
|
-
{
|
|
87
|
-
id: "ruler.unit",
|
|
88
|
-
type: "select",
|
|
89
|
-
label: "Unit",
|
|
90
|
-
options: ["px", "mm", "cm", "in"],
|
|
91
|
-
default: "px",
|
|
92
|
-
},
|
|
93
121
|
{
|
|
94
122
|
id: "ruler.thickness",
|
|
95
123
|
type: "number",
|
|
@@ -98,6 +126,14 @@ export class RulerTool implements Extension {
|
|
|
98
126
|
max: 100,
|
|
99
127
|
default: 20,
|
|
100
128
|
},
|
|
129
|
+
{
|
|
130
|
+
id: "ruler.gap",
|
|
131
|
+
type: "number",
|
|
132
|
+
label: "Gap",
|
|
133
|
+
min: 0,
|
|
134
|
+
max: 100,
|
|
135
|
+
default: 15,
|
|
136
|
+
},
|
|
101
137
|
{
|
|
102
138
|
id: "ruler.backgroundColor",
|
|
103
139
|
type: "color",
|
|
@@ -126,16 +162,6 @@ export class RulerTool implements Extension {
|
|
|
126
162
|
},
|
|
127
163
|
] as ConfigurationContribution[],
|
|
128
164
|
[ContributionPointIds.COMMANDS]: [
|
|
129
|
-
{
|
|
130
|
-
command: "setUnit",
|
|
131
|
-
title: "Set Ruler Unit",
|
|
132
|
-
handler: (unit: "px" | "mm" | "cm" | "in") => {
|
|
133
|
-
if (this.unit === unit) return true;
|
|
134
|
-
this.unit = unit;
|
|
135
|
-
this.updateRuler();
|
|
136
|
-
return true;
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
165
|
{
|
|
140
166
|
command: "setTheme",
|
|
141
167
|
title: "Set Ruler Theme",
|
|
@@ -201,6 +227,85 @@ export class RulerTool implements Extension {
|
|
|
201
227
|
}
|
|
202
228
|
}
|
|
203
229
|
|
|
230
|
+
private createArrowLine(
|
|
231
|
+
x1: number,
|
|
232
|
+
y1: number,
|
|
233
|
+
x2: number,
|
|
234
|
+
y2: number,
|
|
235
|
+
color: string,
|
|
236
|
+
): Group {
|
|
237
|
+
const line = new Line([x1, y1, x2, y2], {
|
|
238
|
+
stroke: color,
|
|
239
|
+
strokeWidth: this.thickness / 20, // Scale stroke width relative to thickness (default 1)
|
|
240
|
+
selectable: false,
|
|
241
|
+
evented: false,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Arrow size proportional to thickness
|
|
245
|
+
const arrowSize = Math.max(4, this.thickness * 0.3);
|
|
246
|
+
const angle = Math.atan2(y2 - y1, x2 - x1);
|
|
247
|
+
|
|
248
|
+
// End Arrow (at x2, y2)
|
|
249
|
+
const endArrow = new Polygon(
|
|
250
|
+
[
|
|
251
|
+
{ x: 0, y: 0 },
|
|
252
|
+
{ x: -arrowSize, y: -arrowSize / 2 },
|
|
253
|
+
{ x: -arrowSize, y: arrowSize / 2 },
|
|
254
|
+
],
|
|
255
|
+
{
|
|
256
|
+
fill: color,
|
|
257
|
+
left: x2,
|
|
258
|
+
top: y2,
|
|
259
|
+
originX: "right",
|
|
260
|
+
originY: "center",
|
|
261
|
+
angle: (angle * 180) / Math.PI,
|
|
262
|
+
selectable: false,
|
|
263
|
+
evented: false,
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// Start Arrow (at x1, y1)
|
|
268
|
+
const startArrow = new Polygon(
|
|
269
|
+
[
|
|
270
|
+
{ x: 0, y: 0 },
|
|
271
|
+
{ x: arrowSize, y: -arrowSize / 2 },
|
|
272
|
+
{ x: arrowSize, y: arrowSize / 2 },
|
|
273
|
+
],
|
|
274
|
+
{
|
|
275
|
+
fill: color,
|
|
276
|
+
left: x1,
|
|
277
|
+
top: y1,
|
|
278
|
+
originX: "left",
|
|
279
|
+
originY: "center",
|
|
280
|
+
angle: (angle * 180) / Math.PI,
|
|
281
|
+
selectable: false,
|
|
282
|
+
evented: false,
|
|
283
|
+
},
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
return new Group([line, startArrow, endArrow], {
|
|
287
|
+
selectable: false,
|
|
288
|
+
evented: false,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private resolvePadding(
|
|
293
|
+
containerWidth: number,
|
|
294
|
+
containerHeight: number,
|
|
295
|
+
): number {
|
|
296
|
+
if (typeof this.dielinePadding === "number") {
|
|
297
|
+
return this.dielinePadding;
|
|
298
|
+
}
|
|
299
|
+
if (typeof this.dielinePadding === "string") {
|
|
300
|
+
if (this.dielinePadding.endsWith("%")) {
|
|
301
|
+
const percent = parseFloat(this.dielinePadding) / 100;
|
|
302
|
+
return Math.min(containerWidth, containerHeight) * percent;
|
|
303
|
+
}
|
|
304
|
+
return parseFloat(this.dielinePadding) || 0;
|
|
305
|
+
}
|
|
306
|
+
return 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
204
309
|
private updateRuler() {
|
|
205
310
|
if (!this.canvasService) return;
|
|
206
311
|
const layer = this.getLayer();
|
|
@@ -212,111 +317,181 @@ export class RulerTool implements Extension {
|
|
|
212
317
|
const width = this.canvasService.canvas.width || 800;
|
|
213
318
|
const height = this.canvasService.canvas.height || 600;
|
|
214
319
|
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
height:
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
320
|
+
// Calculate Layout using Dieline properties
|
|
321
|
+
// Add padding to match DielineTool
|
|
322
|
+
const paddingPx = this.resolvePadding(width, height);
|
|
323
|
+
const layout = Coordinate.calculateLayout(
|
|
324
|
+
{ width, height },
|
|
325
|
+
{ width: this.dielineWidth, height: this.dielineHeight },
|
|
326
|
+
paddingPx,
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const scale = layout.scale;
|
|
330
|
+
const offsetX = layout.offsetX;
|
|
331
|
+
const offsetY = layout.offsetY;
|
|
332
|
+
const visualWidth = layout.width;
|
|
333
|
+
const visualHeight = layout.height;
|
|
334
|
+
|
|
335
|
+
// Logic for Bleed Offset:
|
|
336
|
+
// 1. If offset > 0 (Expand):
|
|
337
|
+
// - Ruler expands to cover the bleed area.
|
|
338
|
+
// - Dimensions show expanded size.
|
|
339
|
+
// 2. If offset < 0 (Shrink/Cut):
|
|
340
|
+
// - Ruler stays at original Dieline boundary (does not shrink).
|
|
341
|
+
// - Dimensions show original size.
|
|
342
|
+
// - Bleed area is internal, so we ignore it for ruler placement.
|
|
343
|
+
|
|
344
|
+
const rawOffset = this.dielineOffset || 0;
|
|
345
|
+
// Effective offset for ruler calculations (only positive offset expands the ruler)
|
|
346
|
+
const effectiveOffset = rawOffset > 0 ? rawOffset : 0;
|
|
347
|
+
|
|
348
|
+
// Pixel expansion based on effective offset
|
|
349
|
+
const expandPixels = effectiveOffset * scale;
|
|
350
|
+
// Use gap configuration
|
|
351
|
+
const gap = this.gap || 15;
|
|
352
|
+
|
|
353
|
+
// New Bounding Box for Ruler
|
|
354
|
+
const rulerLeft = offsetX - expandPixels;
|
|
355
|
+
const rulerTop = offsetY - expandPixels;
|
|
356
|
+
const rulerRight = offsetX + visualWidth + expandPixels;
|
|
357
|
+
const rulerBottom = offsetY + visualHeight + expandPixels;
|
|
358
|
+
|
|
359
|
+
// Display Dimensions (Physical)
|
|
360
|
+
const displayWidth = this.dielineWidth + effectiveOffset * 2;
|
|
361
|
+
const displayHeight = this.dielineHeight + effectiveOffset * 2;
|
|
362
|
+
|
|
363
|
+
// Ruler Placement Coordinates
|
|
364
|
+
// Top Ruler: Above the top boundary
|
|
365
|
+
const topRulerY = rulerTop - gap;
|
|
366
|
+
const topRulerXStart = rulerLeft;
|
|
367
|
+
const topRulerXEnd = rulerRight;
|
|
368
|
+
|
|
369
|
+
// Left Ruler: Left of the left boundary
|
|
370
|
+
const leftRulerX = rulerLeft - gap;
|
|
371
|
+
const leftRulerYStart = rulerTop;
|
|
372
|
+
const leftRulerYEnd = rulerBottom;
|
|
373
|
+
|
|
374
|
+
// 1. Top Dimension Line (X-Axis)
|
|
375
|
+
const topDimLine = this.createArrowLine(
|
|
376
|
+
topRulerXStart,
|
|
377
|
+
topRulerY,
|
|
378
|
+
topRulerXEnd,
|
|
379
|
+
topRulerY,
|
|
380
|
+
lineColor,
|
|
381
|
+
);
|
|
382
|
+
layer.add(topDimLine);
|
|
383
|
+
|
|
384
|
+
// Top Extension Lines
|
|
385
|
+
const extLen = 5;
|
|
386
|
+
layer.add(
|
|
387
|
+
new Line(
|
|
388
|
+
[
|
|
389
|
+
topRulerXStart,
|
|
390
|
+
topRulerY - extLen,
|
|
391
|
+
topRulerXStart,
|
|
392
|
+
topRulerY + extLen,
|
|
393
|
+
],
|
|
394
|
+
{
|
|
395
|
+
stroke: lineColor,
|
|
396
|
+
strokeWidth: 1,
|
|
397
|
+
selectable: false,
|
|
398
|
+
evented: false,
|
|
399
|
+
},
|
|
400
|
+
),
|
|
401
|
+
);
|
|
402
|
+
layer.add(
|
|
403
|
+
new Line(
|
|
404
|
+
[topRulerXEnd, topRulerY - extLen, topRulerXEnd, topRulerY + extLen],
|
|
405
|
+
{
|
|
406
|
+
stroke: lineColor,
|
|
407
|
+
strokeWidth: 1,
|
|
408
|
+
selectable: false,
|
|
409
|
+
evented: false,
|
|
410
|
+
},
|
|
411
|
+
),
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
// Top Text (Centered)
|
|
415
|
+
// Format to max 2 decimal places if needed
|
|
416
|
+
const widthStr = parseFloat(displayWidth.toFixed(2)).toString();
|
|
417
|
+
const topTextContent = `${widthStr} ${this.dielineUnit}`;
|
|
418
|
+
const topText = new Text(topTextContent, {
|
|
419
|
+
left: topRulerXStart + (rulerRight - rulerLeft) / 2,
|
|
420
|
+
top: topRulerY,
|
|
421
|
+
fontSize: fontSize,
|
|
422
|
+
fill: textColor,
|
|
423
|
+
fontFamily: "Arial",
|
|
424
|
+
originX: "center",
|
|
425
|
+
originY: "center",
|
|
426
|
+
backgroundColor: backgroundColor, // Background mask for readability
|
|
244
427
|
selectable: false,
|
|
245
428
|
evented: false,
|
|
246
429
|
});
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
layer.add(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (x % step === 0) {
|
|
274
|
-
const text = new Text(x.toString(), {
|
|
275
|
-
left: x + 2,
|
|
276
|
-
top: 2,
|
|
277
|
-
fontSize: fontSize,
|
|
278
|
-
fill: textColor,
|
|
279
|
-
fontFamily: "Arial",
|
|
430
|
+
// Add small padding to text background if Fabric supports it directly or via separate rect
|
|
431
|
+
// Fabric Text backgroundColor is tight.
|
|
432
|
+
layer.add(topText);
|
|
433
|
+
|
|
434
|
+
// 2. Left Dimension Line (Y-Axis)
|
|
435
|
+
const leftDimLine = this.createArrowLine(
|
|
436
|
+
leftRulerX,
|
|
437
|
+
leftRulerYStart,
|
|
438
|
+
leftRulerX,
|
|
439
|
+
leftRulerYEnd,
|
|
440
|
+
lineColor,
|
|
441
|
+
);
|
|
442
|
+
layer.add(leftDimLine);
|
|
443
|
+
|
|
444
|
+
// Left Extension Lines
|
|
445
|
+
layer.add(
|
|
446
|
+
new Line(
|
|
447
|
+
[
|
|
448
|
+
leftRulerX - extLen,
|
|
449
|
+
leftRulerYStart,
|
|
450
|
+
leftRulerX + extLen,
|
|
451
|
+
leftRulerYStart,
|
|
452
|
+
],
|
|
453
|
+
{
|
|
454
|
+
stroke: lineColor,
|
|
455
|
+
strokeWidth: 1,
|
|
280
456
|
selectable: false,
|
|
281
457
|
evented: false,
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
stroke: lineColor,
|
|
297
|
-
strokeWidth: 1,
|
|
298
|
-
selectable: false,
|
|
299
|
-
evented: false,
|
|
300
|
-
});
|
|
301
|
-
layer.add(line);
|
|
302
|
-
|
|
303
|
-
if (y % step === 0) {
|
|
304
|
-
const text = new Text(y.toString(), {
|
|
305
|
-
angle: -90,
|
|
306
|
-
left: thickness / 2 - fontSize / 3, // approximate centering
|
|
307
|
-
top: y + fontSize,
|
|
308
|
-
fontSize: fontSize,
|
|
309
|
-
fill: textColor,
|
|
310
|
-
fontFamily: "Arial",
|
|
311
|
-
originX: "center",
|
|
312
|
-
originY: "center",
|
|
458
|
+
},
|
|
459
|
+
),
|
|
460
|
+
);
|
|
461
|
+
layer.add(
|
|
462
|
+
new Line(
|
|
463
|
+
[
|
|
464
|
+
leftRulerX - extLen,
|
|
465
|
+
leftRulerYEnd,
|
|
466
|
+
leftRulerX + extLen,
|
|
467
|
+
leftRulerYEnd,
|
|
468
|
+
],
|
|
469
|
+
{
|
|
470
|
+
stroke: lineColor,
|
|
471
|
+
strokeWidth: 1,
|
|
313
472
|
selectable: false,
|
|
314
473
|
evented: false,
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
474
|
+
},
|
|
475
|
+
),
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
// Left Text (Centered, Rotated)
|
|
479
|
+
const heightStr = parseFloat(displayHeight.toFixed(2)).toString();
|
|
480
|
+
const leftTextContent = `${heightStr} ${this.dielineUnit}`;
|
|
481
|
+
const leftText = new Text(leftTextContent, {
|
|
482
|
+
left: leftRulerX,
|
|
483
|
+
top: leftRulerYStart + (rulerBottom - rulerTop) / 2,
|
|
484
|
+
angle: -90,
|
|
485
|
+
fontSize: fontSize,
|
|
486
|
+
fill: textColor,
|
|
487
|
+
fontFamily: "Arial",
|
|
488
|
+
originX: "center",
|
|
489
|
+
originY: "center",
|
|
490
|
+
backgroundColor: backgroundColor,
|
|
491
|
+
selectable: false,
|
|
492
|
+
evented: false,
|
|
493
|
+
});
|
|
494
|
+
layer.add(leftText);
|
|
320
495
|
|
|
321
496
|
// Always bring ruler to front
|
|
322
497
|
this.canvasService.canvas.bringObjectToFront(layer);
|