@pooder/kit 4.1.0 → 4.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/src/ruler.ts CHANGED
@@ -7,7 +7,8 @@ import {
7
7
  } from "@pooder/core";
8
8
  import { Rect, Line, Text, Group, Polygon } from "fabric";
9
9
  import CanvasService from "./CanvasService";
10
- import { Coordinate, Unit } from "./coordinate";
10
+ import { Unit } from "./coordinate";
11
+ import { formatMm } from "./units";
11
12
 
12
13
  export class RulerTool implements Extension {
13
14
  id = "pooder.kit.ruler";
@@ -26,7 +27,7 @@ export class RulerTool implements Extension {
26
27
  // Dieline context for sync
27
28
  private dielineWidth: number = 500;
28
29
  private dielineHeight: number = 500;
29
- private dielineUnit: Unit = "mm";
30
+ private dielineDisplayUnit: Unit = "mm";
30
31
  private dielinePadding: number | string = 40;
31
32
  private dielineOffset: number = 0;
32
33
 
@@ -67,7 +68,10 @@ export class RulerTool implements Extension {
67
68
  this.fontSize = configService.get("ruler.fontSize", this.fontSize);
68
69
 
69
70
  // Load Dieline Config
70
- this.dielineUnit = configService.get("dieline.unit", this.dielineUnit);
71
+ this.dielineDisplayUnit = configService.get(
72
+ "dieline.displayUnit",
73
+ this.dielineDisplayUnit,
74
+ );
71
75
  this.dielineWidth = configService.get("dieline.width", this.dielineWidth);
72
76
  this.dielineHeight = configService.get(
73
77
  "dieline.height",
@@ -92,7 +96,8 @@ export class RulerTool implements Extension {
92
96
  shouldUpdate = true;
93
97
  }
94
98
  } else if (e.key.startsWith("dieline.")) {
95
- if (e.key === "dieline.unit") this.dielineUnit = e.value;
99
+ if (e.key === "dieline.displayUnit")
100
+ this.dielineDisplayUnit = e.value;
96
101
  if (e.key === "dieline.width") this.dielineWidth = e.value;
97
102
  if (e.key === "dieline.height") this.dielineHeight = e.value;
98
103
  if (e.key === "dieline.padding") this.dielinePadding = e.value;
@@ -320,12 +325,16 @@ export class RulerTool implements Extension {
320
325
  // Calculate Layout using Dieline properties
321
326
  // Add padding to match DielineTool
322
327
  const paddingPx = this.resolvePadding(width, height);
323
- const layout = Coordinate.calculateLayout(
324
- { width, height },
325
- { width: this.dielineWidth, height: this.dielineHeight },
326
- paddingPx,
328
+
329
+ // Sync Viewport (in case DielineTool hasn't updated it yet, or purely for consistency)
330
+ this.canvasService.viewport.setPadding(paddingPx);
331
+ this.canvasService.viewport.updatePhysical(
332
+ this.dielineWidth,
333
+ this.dielineHeight,
327
334
  );
328
335
 
336
+ const layout = this.canvasService.viewport.layout;
337
+
329
338
  const scale = layout.scale;
330
339
  const offsetX = layout.offsetX;
331
340
  const offsetY = layout.offsetY;
@@ -341,12 +350,12 @@ export class RulerTool implements Extension {
341
350
  // - Dimensions show original size.
342
351
  // - Bleed area is internal, so we ignore it for ruler placement.
343
352
 
344
- const rawOffset = this.dielineOffset || 0;
353
+ const rawOffsetMm = this.dielineOffset || 0;
345
354
  // Effective offset for ruler calculations (only positive offset expands the ruler)
346
- const effectiveOffset = rawOffset > 0 ? rawOffset : 0;
355
+ const effectiveOffsetMm = rawOffsetMm > 0 ? rawOffsetMm : 0;
347
356
 
348
357
  // Pixel expansion based on effective offset
349
- const expandPixels = effectiveOffset * scale;
358
+ const expandPixels = effectiveOffsetMm * scale;
350
359
  // Use gap configuration
351
360
  const gap = this.gap || 15;
352
361
 
@@ -357,8 +366,8 @@ export class RulerTool implements Extension {
357
366
  const rulerBottom = offsetY + visualHeight + expandPixels;
358
367
 
359
368
  // Display Dimensions (Physical)
360
- const displayWidth = this.dielineWidth + effectiveOffset * 2;
361
- const displayHeight = this.dielineHeight + effectiveOffset * 2;
369
+ const displayWidthMm = this.dielineWidth + effectiveOffsetMm * 2;
370
+ const displayHeightMm = this.dielineHeight + effectiveOffsetMm * 2;
362
371
 
363
372
  // Ruler Placement Coordinates
364
373
  // Top Ruler: Above the top boundary
@@ -412,9 +421,8 @@ export class RulerTool implements Extension {
412
421
  );
413
422
 
414
423
  // 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}`;
424
+ const widthStr = formatMm(displayWidthMm, this.dielineDisplayUnit);
425
+ const topTextContent = `${widthStr} ${this.dielineDisplayUnit}`;
418
426
  const topText = new Text(topTextContent, {
419
427
  left: topRulerXStart + (rulerRight - rulerLeft) / 2,
420
428
  top: topRulerY,
@@ -476,8 +484,8 @@ export class RulerTool implements Extension {
476
484
  );
477
485
 
478
486
  // Left Text (Centered, Rotated)
479
- const heightStr = parseFloat(displayHeight.toFixed(2)).toString();
480
- const leftTextContent = `${heightStr} ${this.dielineUnit}`;
487
+ const heightStr = formatMm(displayHeightMm, this.dielineDisplayUnit);
488
+ const leftTextContent = `${heightStr} ${this.dielineDisplayUnit}`;
481
489
  const leftText = new Text(leftTextContent, {
482
490
  left: leftRulerX,
483
491
  top: leftRulerYStart + (rulerBottom - rulerTop) / 2,
package/src/units.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { Coordinate, Unit } from "./coordinate";
2
+
3
+ export function parseLengthToMm(input: number | string, defaultUnit: Unit): number {
4
+ if (typeof input === "number") {
5
+ if (!Number.isFinite(input)) return 0;
6
+ return Coordinate.convertUnit(input, defaultUnit, "mm");
7
+ }
8
+
9
+ const raw = input.trim();
10
+ if (!raw) return 0;
11
+
12
+ const match = raw.match(/^([+-]?\d+(?:\.\d+)?)\s*(px|mm|cm|in)?$/i);
13
+ if (!match) return 0;
14
+
15
+ const value = Number(match[1]);
16
+ if (!Number.isFinite(value)) return 0;
17
+
18
+ const unit = (match[2]?.toLowerCase() as Unit | undefined) ?? defaultUnit;
19
+ return Coordinate.convertUnit(value, unit, "mm");
20
+ }
21
+
22
+ export function formatMm(valueMm: number, displayUnit: Unit, fractionDigits: number = 2): string {
23
+ if (!Number.isFinite(valueMm)) return "0";
24
+ const value = Coordinate.convertUnit(valueMm, "mm", displayUnit);
25
+ const rounded = Number(value.toFixed(fractionDigits));
26
+ return rounded.toString();
27
+ }
package/tests/run.ts ADDED
@@ -0,0 +1,81 @@
1
+ import { ConstraintRegistry } from "../src/constraints";
2
+ import { completeFeaturesStrict } from "../src/featureComplete";
3
+ import { ConstraintFeature } from "../src/constraints";
4
+
5
+ function assert(condition: any, message: string) {
6
+ if (!condition) throw new Error(message);
7
+ }
8
+
9
+ function closeTo(a: number, b: number, eps: number = 1e-6) {
10
+ return Math.abs(a - b) <= eps;
11
+ }
12
+
13
+ function testTangentBottom() {
14
+ const feature: ConstraintFeature = {
15
+ id: "stand",
16
+ operation: "add",
17
+ shape: "rect",
18
+ x: 0.5,
19
+ y: 0.2,
20
+ width: 40,
21
+ height: 20,
22
+ constraints: { type: "tangent-bottom", params: { gap: 0 } },
23
+ };
24
+
25
+ const out = ConstraintRegistry.apply(feature.x, feature.y, feature, {
26
+ dielineWidth: 100,
27
+ dielineHeight: 100,
28
+ });
29
+
30
+ assert(closeTo(out.y, 1.1), `tangent-bottom y expected 1.1, got ${out.y}`);
31
+ assert(closeTo(out.x, 0.5), `tangent-bottom x expected 0.5, got ${out.x}`);
32
+ }
33
+
34
+ function testCompleteFeaturesStrict() {
35
+ const updates: any[] = [];
36
+
37
+ const illegal: ConstraintFeature = {
38
+ id: "stand",
39
+ operation: "add",
40
+ shape: "rect",
41
+ x: 0.5,
42
+ y: 0.5,
43
+ width: 40,
44
+ height: 20,
45
+ constraints: { type: "tangent-bottom", params: { gap: 0 } },
46
+ };
47
+
48
+ const failed = completeFeaturesStrict(
49
+ [illegal],
50
+ { dielineWidth: 100, dielineHeight: 100 },
51
+ (next) => updates.push({ key: "dieline.features", value: next }),
52
+ );
53
+ assert(failed.ok === false, "completeFeatures should fail for illegal features");
54
+ assert(updates.length === 0, "illegal draft should not update configuration");
55
+
56
+ const legal: ConstraintFeature = {
57
+ ...illegal,
58
+ y: 1.1,
59
+ };
60
+
61
+ const ok = completeFeaturesStrict(
62
+ [legal],
63
+ { dielineWidth: 100, dielineHeight: 100 },
64
+ (next) => updates.push({ key: "dieline.features", value: next }),
65
+ );
66
+ assert(ok.ok === true, "completeFeatures should succeed for legal features");
67
+ assert(updates.length === 1, "legal draft should update configuration once");
68
+ assert(updates[0].key === "dieline.features", "should update dieline.features");
69
+ assert(
70
+ closeTo(updates[0].value[0].y, 1.1),
71
+ "saved feature y should remain 1.1",
72
+ );
73
+ }
74
+
75
+ function main() {
76
+ testTangentBottom();
77
+ testCompleteFeaturesStrict();
78
+ console.log("ok");
79
+ }
80
+
81
+ main();
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "CommonJS",
5
+ "lib": ["ES2020", "DOM"],
6
+ "moduleResolution": "Node",
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "strict": true,
10
+ "outDir": ".test-dist",
11
+ "rootDir": "."
12
+ },
13
+ "include": ["tests/**/*.ts", "src/**/*.ts"]
14
+ }
15
+