@pooder/kit 4.0.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.
Files changed (44) hide show
  1. package/.test-dist/src/CanvasService.js +83 -0
  2. package/.test-dist/src/ViewportSystem.js +75 -0
  3. package/.test-dist/src/background.js +203 -0
  4. package/.test-dist/src/constraints.js +153 -0
  5. package/.test-dist/src/coordinate.js +74 -0
  6. package/.test-dist/src/dieline.js +758 -0
  7. package/.test-dist/src/feature.js +687 -0
  8. package/.test-dist/src/featureComplete.js +31 -0
  9. package/.test-dist/src/featureDraft.js +31 -0
  10. package/.test-dist/src/film.js +167 -0
  11. package/.test-dist/src/geometry.js +292 -0
  12. package/.test-dist/src/image.js +421 -0
  13. package/.test-dist/src/index.js +31 -0
  14. package/.test-dist/src/mirror.js +104 -0
  15. package/.test-dist/src/ruler.js +383 -0
  16. package/.test-dist/src/tracer.js +448 -0
  17. package/.test-dist/src/units.js +30 -0
  18. package/.test-dist/src/white-ink.js +310 -0
  19. package/.test-dist/tests/run.js +60 -0
  20. package/CHANGELOG.md +12 -0
  21. package/dist/index.d.mts +54 -5
  22. package/dist/index.d.ts +54 -5
  23. package/dist/index.js +584 -190
  24. package/dist/index.mjs +581 -189
  25. package/package.json +3 -2
  26. package/src/CanvasService.ts +7 -0
  27. package/src/ViewportSystem.ts +92 -0
  28. package/src/background.ts +230 -230
  29. package/src/constraints.ts +207 -0
  30. package/src/coordinate.ts +106 -106
  31. package/src/dieline.ts +194 -75
  32. package/src/feature.ts +239 -147
  33. package/src/featureComplete.ts +45 -0
  34. package/src/film.ts +194 -194
  35. package/src/geometry.ts +4 -0
  36. package/src/image.ts +512 -512
  37. package/src/index.ts +1 -0
  38. package/src/mirror.ts +128 -128
  39. package/src/ruler.ts +508 -500
  40. package/src/tracer.ts +570 -570
  41. package/src/units.ts +27 -0
  42. package/src/white-ink.ts +373 -373
  43. package/tests/run.ts +81 -0
  44. package/tsconfig.test.json +15 -0
package/dist/index.js CHANGED
@@ -38,7 +38,9 @@ __export(index_exports, {
38
38
  ImageTool: () => ImageTool,
39
39
  MirrorTool: () => MirrorTool,
40
40
  RulerTool: () => RulerTool,
41
- WhiteInkTool: () => WhiteInkTool
41
+ WhiteInkTool: () => WhiteInkTool,
42
+ formatMm: () => formatMm,
43
+ parseLengthToMm: () => parseLengthToMm
42
44
  });
43
45
  module.exports = __toCommonJS(index_exports);
44
46
 
@@ -717,6 +719,29 @@ var Coordinate = class {
717
719
  }
718
720
  };
719
721
 
722
+ // src/units.ts
723
+ function parseLengthToMm(input, defaultUnit) {
724
+ var _a, _b;
725
+ if (typeof input === "number") {
726
+ if (!Number.isFinite(input)) return 0;
727
+ return Coordinate.convertUnit(input, defaultUnit, "mm");
728
+ }
729
+ const raw = input.trim();
730
+ if (!raw) return 0;
731
+ const match = raw.match(/^([+-]?\d+(?:\.\d+)?)\s*(px|mm|cm|in)?$/i);
732
+ if (!match) return 0;
733
+ const value = Number(match[1]);
734
+ if (!Number.isFinite(value)) return 0;
735
+ const unit = (_b = (_a = match[2]) == null ? void 0 : _a.toLowerCase()) != null ? _b : defaultUnit;
736
+ return Coordinate.convertUnit(value, unit, "mm");
737
+ }
738
+ function formatMm(valueMm, displayUnit, fractionDigits = 2) {
739
+ if (!Number.isFinite(valueMm)) return "0";
740
+ const value = Coordinate.convertUnit(valueMm, "mm", displayUnit);
741
+ const rounded = Number(value.toFixed(fractionDigits));
742
+ return rounded.toString();
743
+ }
744
+
720
745
  // src/geometry.ts
721
746
  var import_paper2 = __toESM(require("paper"));
722
747
  function resolveFeaturePosition(feature, geometry) {
@@ -948,7 +973,7 @@ var DielineTool = class {
948
973
  name: "DielineTool"
949
974
  };
950
975
  this.state = {
951
- unit: "mm",
976
+ displayUnit: "mm",
952
977
  shape: "rect",
953
978
  width: 500,
954
979
  height: 500,
@@ -994,50 +1019,88 @@ var DielineTool = class {
994
1019
  const configService = context.services.get("ConfigurationService");
995
1020
  if (configService) {
996
1021
  const s = this.state;
997
- s.unit = configService.get("dieline.unit", s.unit);
1022
+ s.displayUnit = configService.get("dieline.displayUnit", s.displayUnit);
998
1023
  s.shape = configService.get("dieline.shape", s.shape);
999
- s.width = configService.get("dieline.width", s.width);
1000
- s.height = configService.get("dieline.height", s.height);
1001
- s.radius = configService.get("dieline.radius", s.radius);
1024
+ s.width = parseLengthToMm(
1025
+ configService.get("dieline.width", s.width),
1026
+ "mm"
1027
+ );
1028
+ s.height = parseLengthToMm(
1029
+ configService.get("dieline.height", s.height),
1030
+ "mm"
1031
+ );
1032
+ s.radius = parseLengthToMm(
1033
+ configService.get("dieline.radius", s.radius),
1034
+ "mm"
1035
+ );
1002
1036
  s.padding = configService.get("dieline.padding", s.padding);
1003
- s.offset = configService.get("dieline.offset", s.offset);
1004
- s.mainLine.width = configService.get("dieline.strokeWidth", s.mainLine.width);
1005
- s.mainLine.color = configService.get("dieline.strokeColor", s.mainLine.color);
1006
- s.mainLine.dashLength = configService.get("dieline.dashLength", s.mainLine.dashLength);
1037
+ s.offset = parseLengthToMm(
1038
+ configService.get("dieline.offset", s.offset),
1039
+ "mm"
1040
+ );
1041
+ s.mainLine.width = configService.get(
1042
+ "dieline.strokeWidth",
1043
+ s.mainLine.width
1044
+ );
1045
+ s.mainLine.color = configService.get(
1046
+ "dieline.strokeColor",
1047
+ s.mainLine.color
1048
+ );
1049
+ s.mainLine.dashLength = configService.get(
1050
+ "dieline.dashLength",
1051
+ s.mainLine.dashLength
1052
+ );
1007
1053
  s.mainLine.style = configService.get("dieline.style", s.mainLine.style);
1008
- s.offsetLine.width = configService.get("dieline.offsetStrokeWidth", s.offsetLine.width);
1009
- s.offsetLine.color = configService.get("dieline.offsetStrokeColor", s.offsetLine.color);
1010
- s.offsetLine.dashLength = configService.get("dieline.offsetDashLength", s.offsetLine.dashLength);
1011
- s.offsetLine.style = configService.get("dieline.offsetStyle", s.offsetLine.style);
1054
+ s.offsetLine.width = configService.get(
1055
+ "dieline.offsetStrokeWidth",
1056
+ s.offsetLine.width
1057
+ );
1058
+ s.offsetLine.color = configService.get(
1059
+ "dieline.offsetStrokeColor",
1060
+ s.offsetLine.color
1061
+ );
1062
+ s.offsetLine.dashLength = configService.get(
1063
+ "dieline.offsetDashLength",
1064
+ s.offsetLine.dashLength
1065
+ );
1066
+ s.offsetLine.style = configService.get(
1067
+ "dieline.offsetStyle",
1068
+ s.offsetLine.style
1069
+ );
1012
1070
  s.insideColor = configService.get("dieline.insideColor", s.insideColor);
1013
- s.outsideColor = configService.get("dieline.outsideColor", s.outsideColor);
1014
- s.showBleedLines = configService.get("dieline.showBleedLines", s.showBleedLines);
1071
+ s.outsideColor = configService.get(
1072
+ "dieline.outsideColor",
1073
+ s.outsideColor
1074
+ );
1075
+ s.showBleedLines = configService.get(
1076
+ "dieline.showBleedLines",
1077
+ s.showBleedLines
1078
+ );
1015
1079
  s.features = configService.get("dieline.features", s.features);
1016
1080
  s.pathData = configService.get("dieline.pathData", s.pathData);
1017
1081
  configService.onAnyChange((e) => {
1018
1082
  if (e.key.startsWith("dieline.")) {
1019
- console.log(`[DielineTool] Config change detected: ${e.key} -> ${e.value}`);
1020
1083
  switch (e.key) {
1021
- case "dieline.unit":
1022
- s.unit = e.value;
1084
+ case "dieline.displayUnit":
1085
+ s.displayUnit = e.value;
1023
1086
  break;
1024
1087
  case "dieline.shape":
1025
1088
  s.shape = e.value;
1026
1089
  break;
1027
1090
  case "dieline.width":
1028
- s.width = e.value;
1091
+ s.width = parseLengthToMm(e.value, "mm");
1029
1092
  break;
1030
1093
  case "dieline.height":
1031
- s.height = e.value;
1094
+ s.height = parseLengthToMm(e.value, "mm");
1032
1095
  break;
1033
1096
  case "dieline.radius":
1034
- s.radius = e.value;
1097
+ s.radius = parseLengthToMm(e.value, "mm");
1035
1098
  break;
1036
1099
  case "dieline.padding":
1037
1100
  s.padding = e.value;
1038
1101
  break;
1039
1102
  case "dieline.offset":
1040
- s.offset = e.value;
1103
+ s.offset = parseLengthToMm(e.value, "mm");
1041
1104
  break;
1042
1105
  case "dieline.strokeWidth":
1043
1106
  s.mainLine.width = e.value;
@@ -1096,11 +1159,11 @@ var DielineTool = class {
1096
1159
  return {
1097
1160
  [import_core2.ContributionPointIds.CONFIGURATIONS]: [
1098
1161
  {
1099
- id: "dieline.unit",
1162
+ id: "dieline.displayUnit",
1100
1163
  type: "select",
1101
- label: "Unit",
1102
- options: ["px", "mm", "cm", "in"],
1103
- default: s.unit
1164
+ label: "Display Unit",
1165
+ options: ["mm", "cm", "in"],
1166
+ default: s.displayUnit
1104
1167
  },
1105
1168
  {
1106
1169
  id: "dieline.shape",
@@ -1112,7 +1175,7 @@ var DielineTool = class {
1112
1175
  {
1113
1176
  id: "dieline.width",
1114
1177
  type: "number",
1115
- label: "Width",
1178
+ label: "Width (mm)",
1116
1179
  min: 10,
1117
1180
  max: 2e3,
1118
1181
  default: s.width
@@ -1120,7 +1183,7 @@ var DielineTool = class {
1120
1183
  {
1121
1184
  id: "dieline.height",
1122
1185
  type: "number",
1123
- label: "Height",
1186
+ label: "Height (mm)",
1124
1187
  min: 10,
1125
1188
  max: 2e3,
1126
1189
  default: s.height
@@ -1128,7 +1191,7 @@ var DielineTool = class {
1128
1191
  {
1129
1192
  id: "dieline.radius",
1130
1193
  type: "number",
1131
- label: "Corner Radius",
1194
+ label: "Corner Radius (mm)",
1132
1195
  min: 0,
1133
1196
  max: 500,
1134
1197
  default: s.radius
@@ -1143,7 +1206,7 @@ var DielineTool = class {
1143
1206
  {
1144
1207
  id: "dieline.offset",
1145
1208
  type: "number",
1146
- label: "Bleed Offset",
1209
+ label: "Bleed Offset (mm)",
1147
1210
  min: -100,
1148
1211
  max: 100,
1149
1212
  default: s.offset
@@ -1234,6 +1297,31 @@ var DielineTool = class {
1234
1297
  }
1235
1298
  ],
1236
1299
  [import_core2.ContributionPointIds.COMMANDS]: [
1300
+ {
1301
+ command: "updateFeaturePosition",
1302
+ title: "Update Feature Position",
1303
+ handler: (groupId, x, y) => {
1304
+ var _a;
1305
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1306
+ "ConfigurationService"
1307
+ );
1308
+ if (!configService) return;
1309
+ const features = configService.get("dieline.features") || [];
1310
+ let changed = false;
1311
+ const newFeatures = features.map((f) => {
1312
+ if (f.groupId === groupId) {
1313
+ if (f.x !== x || f.y !== y) {
1314
+ changed = true;
1315
+ return { ...f, x, y };
1316
+ }
1317
+ }
1318
+ return f;
1319
+ });
1320
+ if (changed) {
1321
+ configService.update("dieline.features", newFeatures);
1322
+ }
1323
+ }
1324
+ },
1237
1325
  {
1238
1326
  command: "getGeometry",
1239
1327
  title: "Get Geometry",
@@ -1339,7 +1427,7 @@ var DielineTool = class {
1339
1427
  const layer = this.getLayer();
1340
1428
  if (!layer) return;
1341
1429
  const {
1342
- unit,
1430
+ displayUnit,
1343
1431
  shape,
1344
1432
  radius,
1345
1433
  offset,
@@ -1350,15 +1438,13 @@ var DielineTool = class {
1350
1438
  showBleedLines,
1351
1439
  features
1352
1440
  } = this.state;
1353
- let { width, height } = this.state;
1441
+ const { width, height } = this.state;
1354
1442
  const canvasW = this.canvasService.canvas.width || 800;
1355
1443
  const canvasH = this.canvasService.canvas.height || 600;
1356
1444
  const paddingPx = this.resolvePadding(canvasW, canvasH);
1357
- const layout = Coordinate.calculateLayout(
1358
- { width: canvasW, height: canvasH },
1359
- { width, height },
1360
- paddingPx
1361
- );
1445
+ this.canvasService.viewport.setPadding(paddingPx);
1446
+ this.canvasService.viewport.updatePhysical(width, height);
1447
+ const layout = this.canvasService.viewport.layout;
1362
1448
  const scale = layout.scale;
1363
1449
  const cx = layout.offsetX + layout.width / 2;
1364
1450
  const cy = layout.offsetY + layout.height / 2;
@@ -1545,15 +1631,22 @@ var DielineTool = class {
1545
1631
  }
1546
1632
  getGeometry() {
1547
1633
  if (!this.canvasService) return null;
1548
- const { unit, shape, width, height, radius, offset, mainLine, pathData } = this.state;
1634
+ const {
1635
+ displayUnit,
1636
+ shape,
1637
+ width,
1638
+ height,
1639
+ radius,
1640
+ offset,
1641
+ mainLine,
1642
+ pathData
1643
+ } = this.state;
1549
1644
  const canvasW = this.canvasService.canvas.width || 800;
1550
1645
  const canvasH = this.canvasService.canvas.height || 600;
1551
1646
  const paddingPx = this.resolvePadding(canvasW, canvasH);
1552
- const layout = Coordinate.calculateLayout(
1553
- { width: canvasW, height: canvasH },
1554
- { width, height },
1555
- paddingPx
1556
- );
1647
+ this.canvasService.viewport.setPadding(paddingPx);
1648
+ this.canvasService.viewport.updatePhysical(width, height);
1649
+ const layout = this.canvasService.viewport.layout;
1557
1650
  const scale = layout.scale;
1558
1651
  const cx = layout.offsetX + layout.width / 2;
1559
1652
  const cy = layout.offsetY + layout.height / 2;
@@ -1561,14 +1654,14 @@ var DielineTool = class {
1561
1654
  const visualHeight = layout.height;
1562
1655
  return {
1563
1656
  shape,
1564
- unit,
1657
+ unit: "mm",
1658
+ displayUnit,
1565
1659
  x: cx,
1566
1660
  y: cy,
1567
1661
  width: visualWidth,
1568
1662
  height: visualHeight,
1569
1663
  radius: radius * scale,
1570
1664
  offset: offset * scale,
1571
- // Pass scale to help other tools (like FeatureTool) convert units
1572
1665
  scale,
1573
1666
  strokeWidth: mainLine.width,
1574
1667
  pathData
@@ -1578,15 +1671,13 @@ var DielineTool = class {
1578
1671
  if (!this.canvasService) return null;
1579
1672
  const userLayer = this.canvasService.getLayer("user");
1580
1673
  if (!userLayer) return null;
1581
- const { shape, width, height, radius, features, unit, pathData } = this.state;
1674
+ const { shape, width, height, radius, features, pathData } = this.state;
1582
1675
  const canvasW = this.canvasService.canvas.width || 800;
1583
1676
  const canvasH = this.canvasService.canvas.height || 600;
1584
1677
  const paddingPx = this.resolvePadding(canvasW, canvasH);
1585
- const layout = Coordinate.calculateLayout(
1586
- { width: canvasW, height: canvasH },
1587
- { width, height },
1588
- paddingPx
1589
- );
1678
+ this.canvasService.viewport.setPadding(paddingPx);
1679
+ this.canvasService.viewport.updatePhysical(width, height);
1680
+ const layout = this.canvasService.viewport.layout;
1590
1681
  const scale = layout.scale;
1591
1682
  const cx = layout.offsetX + layout.width / 2;
1592
1683
  const cy = layout.offsetY + layout.height / 2;
@@ -1801,13 +1892,158 @@ var FilmTool = class {
1801
1892
  // src/feature.ts
1802
1893
  var import_core4 = require("@pooder/core");
1803
1894
  var import_fabric4 = require("fabric");
1895
+
1896
+ // src/constraints.ts
1897
+ var ConstraintRegistry = class {
1898
+ static register(type, handler) {
1899
+ this.handlers.set(type, handler);
1900
+ }
1901
+ static apply(x, y, feature, context) {
1902
+ if (!feature.constraints || !feature.constraints.type) {
1903
+ return { x, y };
1904
+ }
1905
+ const handler = this.handlers.get(feature.constraints.type);
1906
+ if (handler) {
1907
+ return handler(x, y, feature, context);
1908
+ }
1909
+ return { x, y };
1910
+ }
1911
+ };
1912
+ ConstraintRegistry.handlers = /* @__PURE__ */ new Map();
1913
+ var edgeConstraint = (x, y, feature, context) => {
1914
+ var _a;
1915
+ const { dielineWidth, dielineHeight } = context;
1916
+ const params = ((_a = feature.constraints) == null ? void 0 : _a.params) || {};
1917
+ const allowedEdges = params.allowedEdges || [
1918
+ "top",
1919
+ "bottom",
1920
+ "left",
1921
+ "right"
1922
+ ];
1923
+ const confine = params.confine || false;
1924
+ const offset = params.offset || 0;
1925
+ const distances = [];
1926
+ if (allowedEdges.includes("top"))
1927
+ distances.push({ edge: "top", dist: y * dielineHeight });
1928
+ if (allowedEdges.includes("bottom"))
1929
+ distances.push({ edge: "bottom", dist: (1 - y) * dielineHeight });
1930
+ if (allowedEdges.includes("left"))
1931
+ distances.push({ edge: "left", dist: x * dielineWidth });
1932
+ if (allowedEdges.includes("right"))
1933
+ distances.push({ edge: "right", dist: (1 - x) * dielineWidth });
1934
+ if (distances.length === 0) return { x, y };
1935
+ distances.sort((a, b) => a.dist - b.dist);
1936
+ const nearest = distances[0].edge;
1937
+ let newX = x;
1938
+ let newY = y;
1939
+ const fw = feature.width || 0;
1940
+ const fh = feature.height || 0;
1941
+ switch (nearest) {
1942
+ case "top":
1943
+ newY = 0 + offset / dielineHeight;
1944
+ if (confine) {
1945
+ const minX = fw / 2 / dielineWidth;
1946
+ const maxX = 1 - minX;
1947
+ newX = Math.max(minX, Math.min(newX, maxX));
1948
+ }
1949
+ break;
1950
+ case "bottom":
1951
+ newY = 1 - offset / dielineHeight;
1952
+ if (confine) {
1953
+ const minX = fw / 2 / dielineWidth;
1954
+ const maxX = 1 - minX;
1955
+ newX = Math.max(minX, Math.min(newX, maxX));
1956
+ }
1957
+ break;
1958
+ case "left":
1959
+ newX = 0 + offset / dielineWidth;
1960
+ if (confine) {
1961
+ const minY = fh / 2 / dielineHeight;
1962
+ const maxY = 1 - minY;
1963
+ newY = Math.max(minY, Math.min(newY, maxY));
1964
+ }
1965
+ break;
1966
+ case "right":
1967
+ newX = 1 - offset / dielineWidth;
1968
+ if (confine) {
1969
+ const minY = fh / 2 / dielineHeight;
1970
+ const maxY = 1 - minY;
1971
+ newY = Math.max(minY, Math.min(newY, maxY));
1972
+ }
1973
+ break;
1974
+ }
1975
+ return { x: newX, y: newY };
1976
+ };
1977
+ var internalConstraint = (x, y, feature, context) => {
1978
+ var _a;
1979
+ const { dielineWidth, dielineHeight } = context;
1980
+ const params = ((_a = feature.constraints) == null ? void 0 : _a.params) || {};
1981
+ const margin = params.margin || 0;
1982
+ const fw = feature.width || 0;
1983
+ const fh = feature.height || 0;
1984
+ const minX = (margin + fw / 2) / dielineWidth;
1985
+ const maxX = 1 - (margin + fw / 2) / dielineWidth;
1986
+ const minY = (margin + fh / 2) / dielineHeight;
1987
+ const maxY = 1 - (margin + fh / 2) / dielineHeight;
1988
+ const clampedX = minX > maxX ? 0.5 : Math.max(minX, Math.min(x, maxX));
1989
+ const clampedY = minY > maxY ? 0.5 : Math.max(minY, Math.min(y, maxY));
1990
+ return { x: clampedX, y: clampedY };
1991
+ };
1992
+ var tangentBottomConstraint = (x, y, feature, context) => {
1993
+ var _a;
1994
+ const { dielineWidth, dielineHeight } = context;
1995
+ const params = ((_a = feature.constraints) == null ? void 0 : _a.params) || {};
1996
+ const gap = params.gap || 0;
1997
+ const confineX = params.confineX !== false;
1998
+ const extentY = feature.shape === "circle" ? feature.radius || 0 : (feature.height || 0) / 2;
1999
+ const newY = 1 + (extentY + gap) / dielineHeight;
2000
+ let newX = x;
2001
+ if (confineX) {
2002
+ const extentX = feature.shape === "circle" ? feature.radius || 0 : (feature.width || 0) / 2;
2003
+ const minX = extentX / dielineWidth;
2004
+ const maxX = 1 - extentX / dielineWidth;
2005
+ newX = minX > maxX ? 0.5 : Math.max(minX, Math.min(newX, maxX));
2006
+ }
2007
+ return { x: newX, y: newY };
2008
+ };
2009
+ ConstraintRegistry.register("edge", edgeConstraint);
2010
+ ConstraintRegistry.register("internal", internalConstraint);
2011
+ ConstraintRegistry.register("tangent-bottom", tangentBottomConstraint);
2012
+
2013
+ // src/featureComplete.ts
2014
+ function validateFeaturesStrict(features, context) {
2015
+ var _a;
2016
+ const eps = 1e-6;
2017
+ const issues = [];
2018
+ for (const f of features) {
2019
+ if (!((_a = f.constraints) == null ? void 0 : _a.type)) continue;
2020
+ const constrained = ConstraintRegistry.apply(f.x, f.y, f, context);
2021
+ if (Math.abs(constrained.x - f.x) > eps || Math.abs(constrained.y - f.y) > eps) {
2022
+ issues.push({
2023
+ featureId: f.id,
2024
+ groupId: f.groupId,
2025
+ reason: "Position violates constraint strategy"
2026
+ });
2027
+ }
2028
+ }
2029
+ return { ok: issues.length === 0, issues: issues.length ? issues : void 0 };
2030
+ }
2031
+ function completeFeaturesStrict(features, context, update) {
2032
+ const validation = validateFeaturesStrict(features, context);
2033
+ if (!validation.ok) return validation;
2034
+ const next = JSON.parse(JSON.stringify(features || []));
2035
+ update(next);
2036
+ return { ok: true };
2037
+ }
2038
+
2039
+ // src/feature.ts
1804
2040
  var FeatureTool = class {
1805
2041
  constructor(options) {
1806
2042
  this.id = "pooder.kit.feature";
1807
2043
  this.metadata = {
1808
2044
  name: "FeatureTool"
1809
2045
  };
1810
- this.features = [];
2046
+ this.workingFeatures = [];
1811
2047
  this.isUpdatingConfig = false;
1812
2048
  this.isToolActive = false;
1813
2049
  this.handleMoving = null;
@@ -1833,12 +2069,15 @@ var FeatureTool = class {
1833
2069
  "ConfigurationService"
1834
2070
  );
1835
2071
  if (configService) {
1836
- this.features = configService.get("dieline.features", []);
2072
+ const features = configService.get("dieline.features", []) || [];
2073
+ this.workingFeatures = this.cloneFeatures(features);
1837
2074
  configService.onAnyChange((e) => {
1838
2075
  if (this.isUpdatingConfig) return;
1839
2076
  if (e.key === "dieline.features") {
1840
- this.features = e.value || [];
2077
+ const next = e.value || [];
2078
+ this.workingFeatures = this.cloneFeatures(next);
1841
2079
  this.redraw();
2080
+ this.emitWorkingChange();
1842
2081
  }
1843
2082
  });
1844
2083
  }
@@ -1896,27 +2135,151 @@ var FeatureTool = class {
1896
2135
  command: "clearFeatures",
1897
2136
  title: "Clear Features",
1898
2137
  handler: () => {
1899
- var _a;
1900
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1901
- "ConfigurationService"
1902
- );
1903
- if (configService) {
1904
- configService.update("dieline.features", []);
1905
- }
2138
+ this.setWorkingFeatures([]);
2139
+ this.redraw();
2140
+ this.emitWorkingChange();
1906
2141
  return true;
1907
2142
  }
2143
+ },
2144
+ {
2145
+ command: "getWorkingFeatures",
2146
+ title: "Get Working Features",
2147
+ handler: () => {
2148
+ return this.cloneFeatures(this.workingFeatures);
2149
+ }
2150
+ },
2151
+ {
2152
+ command: "setWorkingFeatures",
2153
+ title: "Set Working Features",
2154
+ handler: async (features) => {
2155
+ await this.refreshGeometry();
2156
+ this.setWorkingFeatures(this.cloneFeatures(features || []));
2157
+ this.redraw();
2158
+ this.emitWorkingChange();
2159
+ return { ok: true };
2160
+ }
2161
+ },
2162
+ {
2163
+ command: "updateWorkingGroupPosition",
2164
+ title: "Update Working Group Position",
2165
+ handler: (groupId, x, y) => {
2166
+ return this.updateWorkingGroupPosition(groupId, x, y);
2167
+ }
2168
+ },
2169
+ {
2170
+ command: "completeFeatures",
2171
+ title: "Complete Features",
2172
+ handler: () => {
2173
+ return this.completeFeatures();
2174
+ }
1908
2175
  }
1909
2176
  ]
1910
2177
  };
1911
2178
  }
1912
- addFeature(type) {
2179
+ cloneFeatures(features) {
2180
+ return JSON.parse(JSON.stringify(features || []));
2181
+ }
2182
+ emitWorkingChange() {
1913
2183
  var _a;
1914
- if (!this.canvasService) return false;
1915
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1916
- "ConfigurationService"
2184
+ (_a = this.context) == null ? void 0 : _a.eventBus.emit("feature:working:change", {
2185
+ features: this.cloneFeatures(this.workingFeatures)
2186
+ });
2187
+ }
2188
+ async refreshGeometry() {
2189
+ if (!this.context) return;
2190
+ const commandService = this.context.services.get("CommandService");
2191
+ if (!commandService) return;
2192
+ try {
2193
+ const g = await Promise.resolve(commandService.executeCommand("getGeometry"));
2194
+ if (g) this.currentGeometry = g;
2195
+ } catch (e) {
2196
+ }
2197
+ }
2198
+ setWorkingFeatures(next) {
2199
+ this.workingFeatures = next;
2200
+ }
2201
+ updateWorkingGroupPosition(groupId, x, y) {
2202
+ var _a, _b, _c;
2203
+ if (!groupId) return { ok: false };
2204
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get("ConfigurationService");
2205
+ if (!configService) return { ok: false };
2206
+ const dielineWidth = parseLengthToMm(
2207
+ (_b = configService.get("dieline.width")) != null ? _b : 500,
2208
+ "mm"
2209
+ );
2210
+ const dielineHeight = parseLengthToMm(
2211
+ (_c = configService.get("dieline.height")) != null ? _c : 500,
2212
+ "mm"
2213
+ );
2214
+ let changed = false;
2215
+ const next = this.workingFeatures.map((f) => {
2216
+ if (f.groupId !== groupId) return f;
2217
+ let nx = x;
2218
+ let ny = y;
2219
+ if (f.constraints && dielineWidth > 0 && dielineHeight > 0) {
2220
+ const constrained = ConstraintRegistry.apply(nx, ny, f, {
2221
+ dielineWidth,
2222
+ dielineHeight
2223
+ });
2224
+ nx = constrained.x;
2225
+ ny = constrained.y;
2226
+ }
2227
+ if (f.x !== nx || f.y !== ny) {
2228
+ changed = true;
2229
+ return { ...f, x: nx, y: ny };
2230
+ }
2231
+ return f;
2232
+ });
2233
+ if (!changed) return { ok: true };
2234
+ this.setWorkingFeatures(next);
2235
+ this.redraw();
2236
+ this.enforceConstraints();
2237
+ this.emitWorkingChange();
2238
+ return { ok: true };
2239
+ }
2240
+ completeFeatures() {
2241
+ var _a, _b, _c;
2242
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get("ConfigurationService");
2243
+ if (!configService) {
2244
+ return {
2245
+ ok: false,
2246
+ issues: [
2247
+ { featureId: "unknown", reason: "ConfigurationService not found" }
2248
+ ]
2249
+ };
2250
+ }
2251
+ const dielineWidth = parseLengthToMm(
2252
+ (_b = configService.get("dieline.width")) != null ? _b : 500,
2253
+ "mm"
1917
2254
  );
1918
- const unit = (configService == null ? void 0 : configService.get("dieline.unit", "mm")) || "mm";
1919
- const defaultSize = Coordinate.convertUnit(10, "mm", unit);
2255
+ const dielineHeight = parseLengthToMm(
2256
+ (_c = configService.get("dieline.height")) != null ? _c : 500,
2257
+ "mm"
2258
+ );
2259
+ const result = completeFeaturesStrict(
2260
+ this.workingFeatures,
2261
+ { dielineWidth, dielineHeight },
2262
+ (next) => {
2263
+ this.isUpdatingConfig = true;
2264
+ try {
2265
+ configService.update("dieline.features", next);
2266
+ } finally {
2267
+ this.isUpdatingConfig = false;
2268
+ }
2269
+ this.workingFeatures = this.cloneFeatures(next);
2270
+ this.emitWorkingChange();
2271
+ }
2272
+ );
2273
+ if (!result.ok) {
2274
+ return {
2275
+ ok: false,
2276
+ issues: result.issues
2277
+ };
2278
+ }
2279
+ return { ok: true };
2280
+ }
2281
+ addFeature(type) {
2282
+ if (!this.canvasService) return false;
1920
2283
  const newFeature = {
1921
2284
  id: Date.now().toString(),
1922
2285
  operation: type,
@@ -1925,28 +2288,17 @@ var FeatureTool = class {
1925
2288
  x: 0.5,
1926
2289
  y: 0,
1927
2290
  // Top edge
1928
- width: defaultSize,
1929
- height: defaultSize,
2291
+ width: 10,
2292
+ height: 10,
1930
2293
  rotation: 0
1931
2294
  };
1932
- if (configService) {
1933
- const current = configService.get(
1934
- "dieline.features",
1935
- []
1936
- );
1937
- configService.update("dieline.features", [...current, newFeature]);
1938
- }
2295
+ this.setWorkingFeatures([...this.workingFeatures || [], newFeature]);
2296
+ this.redraw();
2297
+ this.emitWorkingChange();
1939
2298
  return true;
1940
2299
  }
1941
2300
  addDoubleLayerHole() {
1942
- var _a;
1943
2301
  if (!this.canvasService) return false;
1944
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1945
- "ConfigurationService"
1946
- );
1947
- const unit = (configService == null ? void 0 : configService.get("dieline.unit", "mm")) || "mm";
1948
- const lugRadius = Coordinate.convertUnit(20, "mm", unit);
1949
- const holeRadius = Coordinate.convertUnit(15, "mm", unit);
1950
2302
  const groupId = Date.now().toString();
1951
2303
  const timestamp = Date.now();
1952
2304
  const lug = {
@@ -1957,8 +2309,7 @@ var FeatureTool = class {
1957
2309
  placement: "edge",
1958
2310
  x: 0.5,
1959
2311
  y: 0,
1960
- radius: lugRadius,
1961
- // 20mm
2312
+ radius: 20,
1962
2313
  rotation: 0
1963
2314
  };
1964
2315
  const hole = {
@@ -1969,17 +2320,12 @@ var FeatureTool = class {
1969
2320
  placement: "edge",
1970
2321
  x: 0.5,
1971
2322
  y: 0,
1972
- radius: holeRadius,
1973
- // 15mm
2323
+ radius: 15,
1974
2324
  rotation: 0
1975
2325
  };
1976
- if (configService) {
1977
- const current = configService.get(
1978
- "dieline.features",
1979
- []
1980
- );
1981
- configService.update("dieline.features", [...current, lug, hole]);
1982
- }
2326
+ this.setWorkingFeatures([...this.workingFeatures || [], lug, hole]);
2327
+ this.redraw();
2328
+ this.emitWorkingChange();
1983
2329
  return true;
1984
2330
  }
1985
2331
  getGeometryForFeature(geometry, feature) {
@@ -2023,12 +2369,12 @@ var FeatureTool = class {
2023
2369
  if ((_b = target.data) == null ? void 0 : _b.isGroup) {
2024
2370
  const indices = (_c = target.data) == null ? void 0 : _c.indices;
2025
2371
  if (indices && indices.length > 0) {
2026
- feature = this.features[indices[0]];
2372
+ feature = this.workingFeatures[indices[0]];
2027
2373
  }
2028
2374
  } else {
2029
2375
  const index = (_d = target.data) == null ? void 0 : _d.index;
2030
2376
  if (index !== void 0) {
2031
- feature = this.features[index];
2377
+ feature = this.workingFeatures[index];
2032
2378
  }
2033
2379
  }
2034
2380
  const geometry = this.getGeometryForFeature(
@@ -2049,7 +2395,7 @@ var FeatureTool = class {
2049
2395
  }
2050
2396
  if (!this.handleModified) {
2051
2397
  this.handleModified = (e) => {
2052
- var _a, _b, _c, _d;
2398
+ var _a, _b, _c;
2053
2399
  const target = e.target;
2054
2400
  if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return;
2055
2401
  if ((_b = target.data) == null ? void 0 : _b.isGroup) {
@@ -2057,11 +2403,11 @@ var FeatureTool = class {
2057
2403
  const indices = (_c = groupObj.data) == null ? void 0 : _c.indices;
2058
2404
  if (!indices) return;
2059
2405
  const groupCenter = new import_fabric4.Point(groupObj.left, groupObj.top);
2060
- const newFeatures = [...this.features];
2406
+ const newFeatures = [...this.workingFeatures];
2061
2407
  const { x, y } = this.currentGeometry;
2062
2408
  groupObj.getObjects().forEach((child, i) => {
2063
2409
  const originalIndex = indices[i];
2064
- const feature = this.features[originalIndex];
2410
+ const feature = this.workingFeatures[originalIndex];
2065
2411
  const geometry = this.getGeometryForFeature(
2066
2412
  this.currentGeometry,
2067
2413
  feature
@@ -2079,18 +2425,8 @@ var FeatureTool = class {
2079
2425
  y: normalizedY
2080
2426
  };
2081
2427
  });
2082
- this.features = newFeatures;
2083
- const configService = (_d = this.context) == null ? void 0 : _d.services.get(
2084
- "ConfigurationService"
2085
- );
2086
- if (configService) {
2087
- this.isUpdatingConfig = true;
2088
- try {
2089
- configService.update("dieline.features", this.features);
2090
- } finally {
2091
- this.isUpdatingConfig = false;
2092
- }
2093
- }
2428
+ this.setWorkingFeatures(newFeatures);
2429
+ this.emitWorkingChange();
2094
2430
  } else {
2095
2431
  this.syncFeatureFromCanvas(target);
2096
2432
  }
@@ -2124,6 +2460,23 @@ var FeatureTool = class {
2124
2460
  this.canvasService.requestRenderAll();
2125
2461
  }
2126
2462
  constrainPosition(p, geometry, limit, feature) {
2463
+ if (feature && feature.constraints) {
2464
+ const minX = geometry.x - geometry.width / 2;
2465
+ const minY = geometry.y - geometry.height / 2;
2466
+ const nx = geometry.width > 0 ? (p.x - minX) / geometry.width : 0.5;
2467
+ const ny = geometry.height > 0 ? (p.y - minY) / geometry.height : 0.5;
2468
+ const scale2 = geometry.scale || 1;
2469
+ const dielineWidth = geometry.width / scale2;
2470
+ const dielineHeight = geometry.height / scale2;
2471
+ const constrained = ConstraintRegistry.apply(nx, ny, feature, {
2472
+ dielineWidth,
2473
+ dielineHeight
2474
+ });
2475
+ return {
2476
+ x: minX + constrained.x * geometry.width,
2477
+ y: minY + constrained.y * geometry.height
2478
+ };
2479
+ }
2127
2480
  if (feature && feature.placement === "internal") {
2128
2481
  const minX = geometry.x - geometry.width / 2;
2129
2482
  const maxX = geometry.x + geometry.width / 2;
@@ -2134,10 +2487,13 @@ var FeatureTool = class {
2134
2487
  y: Math.max(minY, Math.min(maxY, p.y))
2135
2488
  };
2136
2489
  }
2137
- const nearest = getNearestPointOnDieline({ x: p.x, y: p.y }, {
2138
- ...geometry,
2139
- features: []
2140
- });
2490
+ const nearest = getNearestPointOnDieline(
2491
+ { x: p.x, y: p.y },
2492
+ {
2493
+ ...geometry,
2494
+ features: []
2495
+ }
2496
+ );
2141
2497
  const dx = p.x - nearest.x;
2142
2498
  const dy = p.y - nearest.y;
2143
2499
  const dist = Math.sqrt(dx * dx + dy * dy);
@@ -2154,9 +2510,9 @@ var FeatureTool = class {
2154
2510
  var _a;
2155
2511
  if (!this.currentGeometry || !this.context) return;
2156
2512
  const index = (_a = target.data) == null ? void 0 : _a.index;
2157
- if (index === void 0 || index < 0 || index >= this.features.length)
2513
+ if (index === void 0 || index < 0 || index >= this.workingFeatures.length)
2158
2514
  return;
2159
- const feature = this.features[index];
2515
+ const feature = this.workingFeatures[index];
2160
2516
  const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
2161
2517
  const { width, height, x, y } = geometry;
2162
2518
  const left = x - width / 2;
@@ -2169,20 +2525,10 @@ var FeatureTool = class {
2169
2525
  y: normalizedY
2170
2526
  // Could also update rotation if we allowed rotating markers
2171
2527
  };
2172
- const newFeatures = [...this.features];
2528
+ const newFeatures = [...this.workingFeatures];
2173
2529
  newFeatures[index] = updatedFeature;
2174
- this.features = newFeatures;
2175
- const configService = this.context.services.get(
2176
- "ConfigurationService"
2177
- );
2178
- if (configService) {
2179
- this.isUpdatingConfig = true;
2180
- try {
2181
- configService.update("dieline.features", this.features);
2182
- } finally {
2183
- this.isUpdatingConfig = false;
2184
- }
2185
- }
2530
+ this.setWorkingFeatures(newFeatures);
2531
+ this.emitWorkingChange();
2186
2532
  }
2187
2533
  redraw() {
2188
2534
  if (!this.canvasService || !this.currentGeometry) return;
@@ -2193,7 +2539,7 @@ var FeatureTool = class {
2193
2539
  return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
2194
2540
  });
2195
2541
  existing.forEach((obj) => canvas.remove(obj));
2196
- if (!this.features || this.features.length === 0) {
2542
+ if (!this.workingFeatures || this.workingFeatures.length === 0) {
2197
2543
  this.canvasService.requestRenderAll();
2198
2544
  return;
2199
2545
  }
@@ -2201,7 +2547,7 @@ var FeatureTool = class {
2201
2547
  const finalScale = scale;
2202
2548
  const groups = {};
2203
2549
  const singles = [];
2204
- this.features.forEach((f, i) => {
2550
+ this.workingFeatures.forEach((f, i) => {
2205
2551
  if (f.groupId) {
2206
2552
  if (!groups[f.groupId]) groups[f.groupId] = [];
2207
2553
  groups[f.groupId].push({ feature: f, index: i });
@@ -2269,25 +2615,6 @@ var FeatureTool = class {
2269
2615
  lockScalingY: true,
2270
2616
  data: { type: "feature-marker", index, isGroup: false }
2271
2617
  });
2272
- marker.set("opacity", 0);
2273
- marker.on("mouseover", () => {
2274
- marker.set("opacity", 1);
2275
- canvas.requestRenderAll();
2276
- });
2277
- marker.on("mouseout", () => {
2278
- if (canvas.getActiveObject() !== marker) {
2279
- marker.set("opacity", 0);
2280
- canvas.requestRenderAll();
2281
- }
2282
- });
2283
- marker.on("selected", () => {
2284
- marker.set("opacity", 1);
2285
- canvas.requestRenderAll();
2286
- });
2287
- marker.on("deselected", () => {
2288
- marker.set("opacity", 0);
2289
- canvas.requestRenderAll();
2290
- });
2291
2618
  canvas.add(marker);
2292
2619
  canvas.bringObjectToFront(marker);
2293
2620
  });
@@ -2324,25 +2651,6 @@ var FeatureTool = class {
2324
2651
  indices: members.map((m) => m.index)
2325
2652
  }
2326
2653
  });
2327
- groupObj.set("opacity", 0);
2328
- groupObj.on("mouseover", () => {
2329
- groupObj.set("opacity", 1);
2330
- canvas.requestRenderAll();
2331
- });
2332
- groupObj.on("mouseout", () => {
2333
- if (canvas.getActiveObject() !== groupObj) {
2334
- groupObj.set("opacity", 0);
2335
- canvas.requestRenderAll();
2336
- }
2337
- });
2338
- groupObj.on("selected", () => {
2339
- groupObj.set("opacity", 1);
2340
- canvas.requestRenderAll();
2341
- });
2342
- groupObj.on("deselected", () => {
2343
- groupObj.set("opacity", 0);
2344
- canvas.requestRenderAll();
2345
- });
2346
2654
  canvas.add(groupObj);
2347
2655
  canvas.bringObjectToFront(groupObj);
2348
2656
  });
@@ -2361,12 +2669,12 @@ var FeatureTool = class {
2361
2669
  if ((_a = marker.data) == null ? void 0 : _a.isGroup) {
2362
2670
  const indices = (_b = marker.data) == null ? void 0 : _b.indices;
2363
2671
  if (indices && indices.length > 0) {
2364
- feature = this.features[indices[0]];
2672
+ feature = this.workingFeatures[indices[0]];
2365
2673
  }
2366
2674
  } else {
2367
2675
  const index = (_c = marker.data) == null ? void 0 : _c.index;
2368
2676
  if (index !== void 0) {
2369
- feature = this.features[index];
2677
+ feature = this.workingFeatures[index];
2370
2678
  }
2371
2679
  }
2372
2680
  const geometry = this.getGeometryForFeature(
@@ -3090,7 +3398,7 @@ var RulerTool = class {
3090
3398
  // Dieline context for sync
3091
3399
  this.dielineWidth = 500;
3092
3400
  this.dielineHeight = 500;
3093
- this.dielineUnit = "mm";
3401
+ this.dielineDisplayUnit = "mm";
3094
3402
  this.dielinePadding = 40;
3095
3403
  this.dielineOffset = 0;
3096
3404
  if (options) {
@@ -3114,7 +3422,10 @@ var RulerTool = class {
3114
3422
  this.textColor = configService.get("ruler.textColor", this.textColor);
3115
3423
  this.lineColor = configService.get("ruler.lineColor", this.lineColor);
3116
3424
  this.fontSize = configService.get("ruler.fontSize", this.fontSize);
3117
- this.dielineUnit = configService.get("dieline.unit", this.dielineUnit);
3425
+ this.dielineDisplayUnit = configService.get(
3426
+ "dieline.displayUnit",
3427
+ this.dielineDisplayUnit
3428
+ );
3118
3429
  this.dielineWidth = configService.get("dieline.width", this.dielineWidth);
3119
3430
  this.dielineHeight = configService.get(
3120
3431
  "dieline.height",
@@ -3137,7 +3448,8 @@ var RulerTool = class {
3137
3448
  shouldUpdate = true;
3138
3449
  }
3139
3450
  } else if (e.key.startsWith("dieline.")) {
3140
- if (e.key === "dieline.unit") this.dielineUnit = e.value;
3451
+ if (e.key === "dieline.displayUnit")
3452
+ this.dielineDisplayUnit = e.value;
3141
3453
  if (e.key === "dieline.width") this.dielineWidth = e.value;
3142
3454
  if (e.key === "dieline.height") this.dielineHeight = e.value;
3143
3455
  if (e.key === "dieline.padding") this.dielinePadding = e.value;
@@ -3324,26 +3636,27 @@ var RulerTool = class {
3324
3636
  const width = this.canvasService.canvas.width || 800;
3325
3637
  const height = this.canvasService.canvas.height || 600;
3326
3638
  const paddingPx = this.resolvePadding(width, height);
3327
- const layout = Coordinate.calculateLayout(
3328
- { width, height },
3329
- { width: this.dielineWidth, height: this.dielineHeight },
3330
- paddingPx
3639
+ this.canvasService.viewport.setPadding(paddingPx);
3640
+ this.canvasService.viewport.updatePhysical(
3641
+ this.dielineWidth,
3642
+ this.dielineHeight
3331
3643
  );
3644
+ const layout = this.canvasService.viewport.layout;
3332
3645
  const scale = layout.scale;
3333
3646
  const offsetX = layout.offsetX;
3334
3647
  const offsetY = layout.offsetY;
3335
3648
  const visualWidth = layout.width;
3336
3649
  const visualHeight = layout.height;
3337
- const rawOffset = this.dielineOffset || 0;
3338
- const effectiveOffset = rawOffset > 0 ? rawOffset : 0;
3339
- const expandPixels = effectiveOffset * scale;
3650
+ const rawOffsetMm = this.dielineOffset || 0;
3651
+ const effectiveOffsetMm = rawOffsetMm > 0 ? rawOffsetMm : 0;
3652
+ const expandPixels = effectiveOffsetMm * scale;
3340
3653
  const gap = this.gap || 15;
3341
3654
  const rulerLeft = offsetX - expandPixels;
3342
3655
  const rulerTop = offsetY - expandPixels;
3343
3656
  const rulerRight = offsetX + visualWidth + expandPixels;
3344
3657
  const rulerBottom = offsetY + visualHeight + expandPixels;
3345
- const displayWidth = this.dielineWidth + effectiveOffset * 2;
3346
- const displayHeight = this.dielineHeight + effectiveOffset * 2;
3658
+ const displayWidthMm = this.dielineWidth + effectiveOffsetMm * 2;
3659
+ const displayHeightMm = this.dielineHeight + effectiveOffsetMm * 2;
3347
3660
  const topRulerY = rulerTop - gap;
3348
3661
  const topRulerXStart = rulerLeft;
3349
3662
  const topRulerXEnd = rulerRight;
@@ -3386,8 +3699,8 @@ var RulerTool = class {
3386
3699
  }
3387
3700
  )
3388
3701
  );
3389
- const widthStr = parseFloat(displayWidth.toFixed(2)).toString();
3390
- const topTextContent = `${widthStr} ${this.dielineUnit}`;
3702
+ const widthStr = formatMm(displayWidthMm, this.dielineDisplayUnit);
3703
+ const topTextContent = `${widthStr} ${this.dielineDisplayUnit}`;
3391
3704
  const topText = new import_fabric7.Text(topTextContent, {
3392
3705
  left: topRulerXStart + (rulerRight - rulerLeft) / 2,
3393
3706
  top: topRulerY,
@@ -3442,8 +3755,8 @@ var RulerTool = class {
3442
3755
  }
3443
3756
  )
3444
3757
  );
3445
- const heightStr = parseFloat(displayHeight.toFixed(2)).toString();
3446
- const leftTextContent = `${heightStr} ${this.dielineUnit}`;
3758
+ const heightStr = formatMm(displayHeightMm, this.dielineDisplayUnit);
3759
+ const leftTextContent = `${heightStr} ${this.dielineDisplayUnit}`;
3447
3760
  const leftText = new import_fabric7.Text(leftTextContent, {
3448
3761
  left: leftRulerX,
3449
3762
  top: leftRulerYStart + (rulerBottom - rulerTop) / 2,
@@ -3555,6 +3868,81 @@ var MirrorTool = class {
3555
3868
 
3556
3869
  // src/CanvasService.ts
3557
3870
  var import_fabric8 = require("fabric");
3871
+
3872
+ // src/ViewportSystem.ts
3873
+ var ViewportSystem = class {
3874
+ constructor(containerSize = { width: 0, height: 0 }, physicalSize = { width: 0, height: 0 }, padding = 40) {
3875
+ this._containerSize = { width: 0, height: 0 };
3876
+ this._physicalSize = { width: 0, height: 0 };
3877
+ this._padding = 0;
3878
+ this._layout = {
3879
+ scale: 1,
3880
+ offsetX: 0,
3881
+ offsetY: 0,
3882
+ width: 0,
3883
+ height: 0
3884
+ };
3885
+ this._containerSize = containerSize;
3886
+ this._physicalSize = physicalSize;
3887
+ this._padding = padding;
3888
+ this.updateLayout();
3889
+ }
3890
+ get layout() {
3891
+ return this._layout;
3892
+ }
3893
+ get scale() {
3894
+ return this._layout.scale;
3895
+ }
3896
+ get offset() {
3897
+ return { x: this._layout.offsetX, y: this._layout.offsetY };
3898
+ }
3899
+ updateContainer(width, height) {
3900
+ if (this._containerSize.width === width && this._containerSize.height === height)
3901
+ return;
3902
+ this._containerSize = { width, height };
3903
+ this.updateLayout();
3904
+ }
3905
+ updatePhysical(width, height) {
3906
+ if (this._physicalSize.width === width && this._physicalSize.height === height)
3907
+ return;
3908
+ this._physicalSize = { width, height };
3909
+ this.updateLayout();
3910
+ }
3911
+ setPadding(padding) {
3912
+ if (this._padding === padding) return;
3913
+ this._padding = padding;
3914
+ this.updateLayout();
3915
+ }
3916
+ updateLayout() {
3917
+ this._layout = Coordinate.calculateLayout(
3918
+ this._containerSize,
3919
+ this._physicalSize,
3920
+ this._padding
3921
+ );
3922
+ }
3923
+ toPixel(value) {
3924
+ return value * this._layout.scale;
3925
+ }
3926
+ toPhysical(value) {
3927
+ return this._layout.scale === 0 ? 0 : value / this._layout.scale;
3928
+ }
3929
+ toPixelPoint(point) {
3930
+ return {
3931
+ x: point.x * this._layout.scale + this._layout.offsetX,
3932
+ y: point.y * this._layout.scale + this._layout.offsetY
3933
+ };
3934
+ }
3935
+ // Convert screen coordinate (e.g. mouse event) to physical coordinate (relative to content origin)
3936
+ toPhysicalPoint(point) {
3937
+ if (this._layout.scale === 0) return { x: 0, y: 0 };
3938
+ return {
3939
+ x: (point.x - this._layout.offsetX) / this._layout.scale,
3940
+ y: (point.y - this._layout.offsetY) / this._layout.scale
3941
+ };
3942
+ }
3943
+ };
3944
+
3945
+ // src/CanvasService.ts
3558
3946
  var CanvasService = class {
3559
3947
  constructor(el, options) {
3560
3948
  if (el instanceof import_fabric8.Canvas) {
@@ -3565,6 +3953,10 @@ var CanvasService = class {
3565
3953
  ...options
3566
3954
  });
3567
3955
  }
3956
+ this.viewport = new ViewportSystem();
3957
+ if (this.canvas.width !== void 0 && this.canvas.height !== void 0) {
3958
+ this.viewport.updateContainer(this.canvas.width, this.canvas.height);
3959
+ }
3568
3960
  if (options == null ? void 0 : options.eventBus) {
3569
3961
  this.setEventBus(options.eventBus);
3570
3962
  }
@@ -3645,5 +4037,7 @@ var CanvasService = class {
3645
4037
  ImageTool,
3646
4038
  MirrorTool,
3647
4039
  RulerTool,
3648
- WhiteInkTool
4040
+ WhiteInkTool,
4041
+ formatMm,
4042
+ parseLengthToMm
3649
4043
  });