@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/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) {
@@ -940,105 +965,6 @@ function getPathBounds(pathData) {
940
965
  };
941
966
  }
942
967
 
943
- // src/constraints.ts
944
- var ConstraintRegistry = class {
945
- static register(type, handler) {
946
- this.handlers.set(type, handler);
947
- }
948
- static apply(x, y, feature, context) {
949
- if (!feature.constraints || !feature.constraints.type) {
950
- return { x, y };
951
- }
952
- const handler = this.handlers.get(feature.constraints.type);
953
- if (handler) {
954
- return handler(x, y, feature, context);
955
- }
956
- return { x, y };
957
- }
958
- };
959
- ConstraintRegistry.handlers = /* @__PURE__ */ new Map();
960
- var edgeConstraint = (x, y, feature, context) => {
961
- var _a;
962
- const { dielineWidth, dielineHeight } = context;
963
- const params = ((_a = feature.constraints) == null ? void 0 : _a.params) || {};
964
- const allowedEdges = params.allowedEdges || [
965
- "top",
966
- "bottom",
967
- "left",
968
- "right"
969
- ];
970
- const confine = params.confine || false;
971
- const offset = params.offset || 0;
972
- const distances = [];
973
- if (allowedEdges.includes("top"))
974
- distances.push({ edge: "top", dist: y * dielineHeight });
975
- if (allowedEdges.includes("bottom"))
976
- distances.push({ edge: "bottom", dist: (1 - y) * dielineHeight });
977
- if (allowedEdges.includes("left"))
978
- distances.push({ edge: "left", dist: x * dielineWidth });
979
- if (allowedEdges.includes("right"))
980
- distances.push({ edge: "right", dist: (1 - x) * dielineWidth });
981
- if (distances.length === 0) return { x, y };
982
- distances.sort((a, b) => a.dist - b.dist);
983
- const nearest = distances[0].edge;
984
- let newX = x;
985
- let newY = y;
986
- const fw = feature.width || 0;
987
- const fh = feature.height || 0;
988
- switch (nearest) {
989
- case "top":
990
- newY = 0 + offset / dielineHeight;
991
- if (confine) {
992
- const minX = fw / 2 / dielineWidth;
993
- const maxX = 1 - minX;
994
- newX = Math.max(minX, Math.min(newX, maxX));
995
- }
996
- break;
997
- case "bottom":
998
- newY = 1 - offset / dielineHeight;
999
- if (confine) {
1000
- const minX = fw / 2 / dielineWidth;
1001
- const maxX = 1 - minX;
1002
- newX = Math.max(minX, Math.min(newX, maxX));
1003
- }
1004
- break;
1005
- case "left":
1006
- newX = 0 + offset / dielineWidth;
1007
- if (confine) {
1008
- const minY = fh / 2 / dielineHeight;
1009
- const maxY = 1 - minY;
1010
- newY = Math.max(minY, Math.min(newY, maxY));
1011
- }
1012
- break;
1013
- case "right":
1014
- newX = 1 - offset / dielineWidth;
1015
- if (confine) {
1016
- const minY = fh / 2 / dielineHeight;
1017
- const maxY = 1 - minY;
1018
- newY = Math.max(minY, Math.min(newY, maxY));
1019
- }
1020
- break;
1021
- }
1022
- return { x: newX, y: newY };
1023
- };
1024
- var internalConstraint = (x, y, feature, context) => {
1025
- var _a;
1026
- const { dielineWidth, dielineHeight } = context;
1027
- const params = ((_a = feature.constraints) == null ? void 0 : _a.params) || {};
1028
- const margin = params.margin || 0;
1029
- const fw = feature.width || 0;
1030
- const fh = feature.height || 0;
1031
- const minX = (margin + fw / 2) / dielineWidth;
1032
- const maxX = 1 - (margin + fw / 2) / dielineWidth;
1033
- const minY = (margin + fh / 2) / dielineHeight;
1034
- const maxY = 1 - (margin + fh / 2) / dielineHeight;
1035
- const clampedX = minX > maxX ? 0.5 : Math.max(minX, Math.min(x, maxX));
1036
- const clampedY = minY > maxY ? 0.5 : Math.max(minY, Math.min(y, maxY));
1037
- return { x: clampedX, y: clampedY };
1038
- };
1039
- ConstraintRegistry.register("edge", edgeConstraint);
1040
- ConstraintRegistry.register("internal", internalConstraint);
1041
-
1042
968
  // src/dieline.ts
1043
969
  var DielineTool = class {
1044
970
  constructor(options) {
@@ -1047,7 +973,7 @@ var DielineTool = class {
1047
973
  name: "DielineTool"
1048
974
  };
1049
975
  this.state = {
1050
- unit: "mm",
976
+ displayUnit: "mm",
1051
977
  shape: "rect",
1052
978
  width: 500,
1053
979
  height: 500,
@@ -1093,50 +1019,88 @@ var DielineTool = class {
1093
1019
  const configService = context.services.get("ConfigurationService");
1094
1020
  if (configService) {
1095
1021
  const s = this.state;
1096
- s.unit = configService.get("dieline.unit", s.unit);
1022
+ s.displayUnit = configService.get("dieline.displayUnit", s.displayUnit);
1097
1023
  s.shape = configService.get("dieline.shape", s.shape);
1098
- s.width = configService.get("dieline.width", s.width);
1099
- s.height = configService.get("dieline.height", s.height);
1100
- 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
+ );
1101
1036
  s.padding = configService.get("dieline.padding", s.padding);
1102
- s.offset = configService.get("dieline.offset", s.offset);
1103
- s.mainLine.width = configService.get("dieline.strokeWidth", s.mainLine.width);
1104
- s.mainLine.color = configService.get("dieline.strokeColor", s.mainLine.color);
1105
- 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
+ );
1106
1053
  s.mainLine.style = configService.get("dieline.style", s.mainLine.style);
1107
- s.offsetLine.width = configService.get("dieline.offsetStrokeWidth", s.offsetLine.width);
1108
- s.offsetLine.color = configService.get("dieline.offsetStrokeColor", s.offsetLine.color);
1109
- s.offsetLine.dashLength = configService.get("dieline.offsetDashLength", s.offsetLine.dashLength);
1110
- 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
+ );
1111
1070
  s.insideColor = configService.get("dieline.insideColor", s.insideColor);
1112
- s.outsideColor = configService.get("dieline.outsideColor", s.outsideColor);
1113
- 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
+ );
1114
1079
  s.features = configService.get("dieline.features", s.features);
1115
1080
  s.pathData = configService.get("dieline.pathData", s.pathData);
1116
1081
  configService.onAnyChange((e) => {
1117
1082
  if (e.key.startsWith("dieline.")) {
1118
- console.log(`[DielineTool] Config change detected: ${e.key} -> ${e.value}`);
1119
1083
  switch (e.key) {
1120
- case "dieline.unit":
1121
- s.unit = e.value;
1084
+ case "dieline.displayUnit":
1085
+ s.displayUnit = e.value;
1122
1086
  break;
1123
1087
  case "dieline.shape":
1124
1088
  s.shape = e.value;
1125
1089
  break;
1126
1090
  case "dieline.width":
1127
- s.width = e.value;
1091
+ s.width = parseLengthToMm(e.value, "mm");
1128
1092
  break;
1129
1093
  case "dieline.height":
1130
- s.height = e.value;
1094
+ s.height = parseLengthToMm(e.value, "mm");
1131
1095
  break;
1132
1096
  case "dieline.radius":
1133
- s.radius = e.value;
1097
+ s.radius = parseLengthToMm(e.value, "mm");
1134
1098
  break;
1135
1099
  case "dieline.padding":
1136
1100
  s.padding = e.value;
1137
1101
  break;
1138
1102
  case "dieline.offset":
1139
- s.offset = e.value;
1103
+ s.offset = parseLengthToMm(e.value, "mm");
1140
1104
  break;
1141
1105
  case "dieline.strokeWidth":
1142
1106
  s.mainLine.width = e.value;
@@ -1195,11 +1159,11 @@ var DielineTool = class {
1195
1159
  return {
1196
1160
  [import_core2.ContributionPointIds.CONFIGURATIONS]: [
1197
1161
  {
1198
- id: "dieline.unit",
1162
+ id: "dieline.displayUnit",
1199
1163
  type: "select",
1200
- label: "Unit",
1201
- options: ["px", "mm", "cm", "in"],
1202
- default: s.unit
1164
+ label: "Display Unit",
1165
+ options: ["mm", "cm", "in"],
1166
+ default: s.displayUnit
1203
1167
  },
1204
1168
  {
1205
1169
  id: "dieline.shape",
@@ -1211,7 +1175,7 @@ var DielineTool = class {
1211
1175
  {
1212
1176
  id: "dieline.width",
1213
1177
  type: "number",
1214
- label: "Width",
1178
+ label: "Width (mm)",
1215
1179
  min: 10,
1216
1180
  max: 2e3,
1217
1181
  default: s.width
@@ -1219,7 +1183,7 @@ var DielineTool = class {
1219
1183
  {
1220
1184
  id: "dieline.height",
1221
1185
  type: "number",
1222
- label: "Height",
1186
+ label: "Height (mm)",
1223
1187
  min: 10,
1224
1188
  max: 2e3,
1225
1189
  default: s.height
@@ -1227,7 +1191,7 @@ var DielineTool = class {
1227
1191
  {
1228
1192
  id: "dieline.radius",
1229
1193
  type: "number",
1230
- label: "Corner Radius",
1194
+ label: "Corner Radius (mm)",
1231
1195
  min: 0,
1232
1196
  max: 500,
1233
1197
  default: s.radius
@@ -1242,7 +1206,7 @@ var DielineTool = class {
1242
1206
  {
1243
1207
  id: "dieline.offset",
1244
1208
  type: "number",
1245
- label: "Bleed Offset",
1209
+ label: "Bleed Offset (mm)",
1246
1210
  min: -100,
1247
1211
  max: 100,
1248
1212
  default: s.offset
@@ -1343,18 +1307,12 @@ var DielineTool = class {
1343
1307
  );
1344
1308
  if (!configService) return;
1345
1309
  const features = configService.get("dieline.features") || [];
1346
- const dielineWidth = configService.get("dieline.width") || 500;
1347
- const dielineHeight = configService.get("dieline.height") || 500;
1348
1310
  let changed = false;
1349
1311
  const newFeatures = features.map((f) => {
1350
1312
  if (f.groupId === groupId) {
1351
- const constrained = ConstraintRegistry.apply(x, y, f, {
1352
- dielineWidth,
1353
- dielineHeight
1354
- });
1355
- if (f.x !== constrained.x || f.y !== constrained.y) {
1313
+ if (f.x !== x || f.y !== y) {
1356
1314
  changed = true;
1357
- return { ...f, x: constrained.x, y: constrained.y };
1315
+ return { ...f, x, y };
1358
1316
  }
1359
1317
  }
1360
1318
  return f;
@@ -1469,7 +1427,7 @@ var DielineTool = class {
1469
1427
  const layer = this.getLayer();
1470
1428
  if (!layer) return;
1471
1429
  const {
1472
- unit,
1430
+ displayUnit,
1473
1431
  shape,
1474
1432
  radius,
1475
1433
  offset,
@@ -1480,15 +1438,13 @@ var DielineTool = class {
1480
1438
  showBleedLines,
1481
1439
  features
1482
1440
  } = this.state;
1483
- let { width, height } = this.state;
1441
+ const { width, height } = this.state;
1484
1442
  const canvasW = this.canvasService.canvas.width || 800;
1485
1443
  const canvasH = this.canvasService.canvas.height || 600;
1486
1444
  const paddingPx = this.resolvePadding(canvasW, canvasH);
1487
- const layout = Coordinate.calculateLayout(
1488
- { width: canvasW, height: canvasH },
1489
- { width, height },
1490
- paddingPx
1491
- );
1445
+ this.canvasService.viewport.setPadding(paddingPx);
1446
+ this.canvasService.viewport.updatePhysical(width, height);
1447
+ const layout = this.canvasService.viewport.layout;
1492
1448
  const scale = layout.scale;
1493
1449
  const cx = layout.offsetX + layout.width / 2;
1494
1450
  const cy = layout.offsetY + layout.height / 2;
@@ -1675,15 +1631,22 @@ var DielineTool = class {
1675
1631
  }
1676
1632
  getGeometry() {
1677
1633
  if (!this.canvasService) return null;
1678
- 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;
1679
1644
  const canvasW = this.canvasService.canvas.width || 800;
1680
1645
  const canvasH = this.canvasService.canvas.height || 600;
1681
1646
  const paddingPx = this.resolvePadding(canvasW, canvasH);
1682
- const layout = Coordinate.calculateLayout(
1683
- { width: canvasW, height: canvasH },
1684
- { width, height },
1685
- paddingPx
1686
- );
1647
+ this.canvasService.viewport.setPadding(paddingPx);
1648
+ this.canvasService.viewport.updatePhysical(width, height);
1649
+ const layout = this.canvasService.viewport.layout;
1687
1650
  const scale = layout.scale;
1688
1651
  const cx = layout.offsetX + layout.width / 2;
1689
1652
  const cy = layout.offsetY + layout.height / 2;
@@ -1691,14 +1654,14 @@ var DielineTool = class {
1691
1654
  const visualHeight = layout.height;
1692
1655
  return {
1693
1656
  shape,
1694
- unit,
1657
+ unit: "mm",
1658
+ displayUnit,
1695
1659
  x: cx,
1696
1660
  y: cy,
1697
1661
  width: visualWidth,
1698
1662
  height: visualHeight,
1699
1663
  radius: radius * scale,
1700
1664
  offset: offset * scale,
1701
- // Pass scale to help other tools (like FeatureTool) convert units
1702
1665
  scale,
1703
1666
  strokeWidth: mainLine.width,
1704
1667
  pathData
@@ -1708,15 +1671,13 @@ var DielineTool = class {
1708
1671
  if (!this.canvasService) return null;
1709
1672
  const userLayer = this.canvasService.getLayer("user");
1710
1673
  if (!userLayer) return null;
1711
- const { shape, width, height, radius, features, unit, pathData } = this.state;
1674
+ const { shape, width, height, radius, features, pathData } = this.state;
1712
1675
  const canvasW = this.canvasService.canvas.width || 800;
1713
1676
  const canvasH = this.canvasService.canvas.height || 600;
1714
1677
  const paddingPx = this.resolvePadding(canvasW, canvasH);
1715
- const layout = Coordinate.calculateLayout(
1716
- { width: canvasW, height: canvasH },
1717
- { width, height },
1718
- paddingPx
1719
- );
1678
+ this.canvasService.viewport.setPadding(paddingPx);
1679
+ this.canvasService.viewport.updatePhysical(width, height);
1680
+ const layout = this.canvasService.viewport.layout;
1720
1681
  const scale = layout.scale;
1721
1682
  const cx = layout.offsetX + layout.width / 2;
1722
1683
  const cy = layout.offsetY + layout.height / 2;
@@ -1931,13 +1892,158 @@ var FilmTool = class {
1931
1892
  // src/feature.ts
1932
1893
  var import_core4 = require("@pooder/core");
1933
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
1934
2040
  var FeatureTool = class {
1935
2041
  constructor(options) {
1936
2042
  this.id = "pooder.kit.feature";
1937
2043
  this.metadata = {
1938
2044
  name: "FeatureTool"
1939
2045
  };
1940
- this.features = [];
2046
+ this.workingFeatures = [];
1941
2047
  this.isUpdatingConfig = false;
1942
2048
  this.isToolActive = false;
1943
2049
  this.handleMoving = null;
@@ -1963,12 +2069,15 @@ var FeatureTool = class {
1963
2069
  "ConfigurationService"
1964
2070
  );
1965
2071
  if (configService) {
1966
- this.features = configService.get("dieline.features", []);
2072
+ const features = configService.get("dieline.features", []) || [];
2073
+ this.workingFeatures = this.cloneFeatures(features);
1967
2074
  configService.onAnyChange((e) => {
1968
2075
  if (this.isUpdatingConfig) return;
1969
2076
  if (e.key === "dieline.features") {
1970
- this.features = e.value || [];
2077
+ const next = e.value || [];
2078
+ this.workingFeatures = this.cloneFeatures(next);
1971
2079
  this.redraw();
2080
+ this.emitWorkingChange();
1972
2081
  }
1973
2082
  });
1974
2083
  }
@@ -2026,27 +2135,151 @@ var FeatureTool = class {
2026
2135
  command: "clearFeatures",
2027
2136
  title: "Clear Features",
2028
2137
  handler: () => {
2029
- var _a;
2030
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
2031
- "ConfigurationService"
2032
- );
2033
- if (configService) {
2034
- configService.update("dieline.features", []);
2035
- }
2138
+ this.setWorkingFeatures([]);
2139
+ this.redraw();
2140
+ this.emitWorkingChange();
2036
2141
  return true;
2037
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
+ }
2038
2175
  }
2039
2176
  ]
2040
2177
  };
2041
2178
  }
2042
- addFeature(type) {
2179
+ cloneFeatures(features) {
2180
+ return JSON.parse(JSON.stringify(features || []));
2181
+ }
2182
+ emitWorkingChange() {
2043
2183
  var _a;
2044
- if (!this.canvasService) return false;
2045
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
2046
- "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"
2254
+ );
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
+ }
2047
2272
  );
2048
- const unit = (configService == null ? void 0 : configService.get("dieline.unit", "mm")) || "mm";
2049
- const defaultSize = Coordinate.convertUnit(10, "mm", unit);
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;
2050
2283
  const newFeature = {
2051
2284
  id: Date.now().toString(),
2052
2285
  operation: type,
@@ -2055,28 +2288,17 @@ var FeatureTool = class {
2055
2288
  x: 0.5,
2056
2289
  y: 0,
2057
2290
  // Top edge
2058
- width: defaultSize,
2059
- height: defaultSize,
2291
+ width: 10,
2292
+ height: 10,
2060
2293
  rotation: 0
2061
2294
  };
2062
- if (configService) {
2063
- const current = configService.get(
2064
- "dieline.features",
2065
- []
2066
- );
2067
- configService.update("dieline.features", [...current, newFeature]);
2068
- }
2295
+ this.setWorkingFeatures([...this.workingFeatures || [], newFeature]);
2296
+ this.redraw();
2297
+ this.emitWorkingChange();
2069
2298
  return true;
2070
2299
  }
2071
2300
  addDoubleLayerHole() {
2072
- var _a;
2073
2301
  if (!this.canvasService) return false;
2074
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
2075
- "ConfigurationService"
2076
- );
2077
- const unit = (configService == null ? void 0 : configService.get("dieline.unit", "mm")) || "mm";
2078
- const lugRadius = Coordinate.convertUnit(20, "mm", unit);
2079
- const holeRadius = Coordinate.convertUnit(15, "mm", unit);
2080
2302
  const groupId = Date.now().toString();
2081
2303
  const timestamp = Date.now();
2082
2304
  const lug = {
@@ -2087,8 +2309,7 @@ var FeatureTool = class {
2087
2309
  placement: "edge",
2088
2310
  x: 0.5,
2089
2311
  y: 0,
2090
- radius: lugRadius,
2091
- // 20mm
2312
+ radius: 20,
2092
2313
  rotation: 0
2093
2314
  };
2094
2315
  const hole = {
@@ -2099,17 +2320,12 @@ var FeatureTool = class {
2099
2320
  placement: "edge",
2100
2321
  x: 0.5,
2101
2322
  y: 0,
2102
- radius: holeRadius,
2103
- // 15mm
2323
+ radius: 15,
2104
2324
  rotation: 0
2105
2325
  };
2106
- if (configService) {
2107
- const current = configService.get(
2108
- "dieline.features",
2109
- []
2110
- );
2111
- configService.update("dieline.features", [...current, lug, hole]);
2112
- }
2326
+ this.setWorkingFeatures([...this.workingFeatures || [], lug, hole]);
2327
+ this.redraw();
2328
+ this.emitWorkingChange();
2113
2329
  return true;
2114
2330
  }
2115
2331
  getGeometryForFeature(geometry, feature) {
@@ -2153,12 +2369,12 @@ var FeatureTool = class {
2153
2369
  if ((_b = target.data) == null ? void 0 : _b.isGroup) {
2154
2370
  const indices = (_c = target.data) == null ? void 0 : _c.indices;
2155
2371
  if (indices && indices.length > 0) {
2156
- feature = this.features[indices[0]];
2372
+ feature = this.workingFeatures[indices[0]];
2157
2373
  }
2158
2374
  } else {
2159
2375
  const index = (_d = target.data) == null ? void 0 : _d.index;
2160
2376
  if (index !== void 0) {
2161
- feature = this.features[index];
2377
+ feature = this.workingFeatures[index];
2162
2378
  }
2163
2379
  }
2164
2380
  const geometry = this.getGeometryForFeature(
@@ -2179,7 +2395,7 @@ var FeatureTool = class {
2179
2395
  }
2180
2396
  if (!this.handleModified) {
2181
2397
  this.handleModified = (e) => {
2182
- var _a, _b, _c, _d;
2398
+ var _a, _b, _c;
2183
2399
  const target = e.target;
2184
2400
  if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return;
2185
2401
  if ((_b = target.data) == null ? void 0 : _b.isGroup) {
@@ -2187,11 +2403,11 @@ var FeatureTool = class {
2187
2403
  const indices = (_c = groupObj.data) == null ? void 0 : _c.indices;
2188
2404
  if (!indices) return;
2189
2405
  const groupCenter = new import_fabric4.Point(groupObj.left, groupObj.top);
2190
- const newFeatures = [...this.features];
2406
+ const newFeatures = [...this.workingFeatures];
2191
2407
  const { x, y } = this.currentGeometry;
2192
2408
  groupObj.getObjects().forEach((child, i) => {
2193
2409
  const originalIndex = indices[i];
2194
- const feature = this.features[originalIndex];
2410
+ const feature = this.workingFeatures[originalIndex];
2195
2411
  const geometry = this.getGeometryForFeature(
2196
2412
  this.currentGeometry,
2197
2413
  feature
@@ -2209,18 +2425,8 @@ var FeatureTool = class {
2209
2425
  y: normalizedY
2210
2426
  };
2211
2427
  });
2212
- this.features = newFeatures;
2213
- const configService = (_d = this.context) == null ? void 0 : _d.services.get(
2214
- "ConfigurationService"
2215
- );
2216
- if (configService) {
2217
- this.isUpdatingConfig = true;
2218
- try {
2219
- configService.update("dieline.features", this.features);
2220
- } finally {
2221
- this.isUpdatingConfig = false;
2222
- }
2223
- }
2428
+ this.setWorkingFeatures(newFeatures);
2429
+ this.emitWorkingChange();
2224
2430
  } else {
2225
2431
  this.syncFeatureFromCanvas(target);
2226
2432
  }
@@ -2281,10 +2487,13 @@ var FeatureTool = class {
2281
2487
  y: Math.max(minY, Math.min(maxY, p.y))
2282
2488
  };
2283
2489
  }
2284
- const nearest = getNearestPointOnDieline({ x: p.x, y: p.y }, {
2285
- ...geometry,
2286
- features: []
2287
- });
2490
+ const nearest = getNearestPointOnDieline(
2491
+ { x: p.x, y: p.y },
2492
+ {
2493
+ ...geometry,
2494
+ features: []
2495
+ }
2496
+ );
2288
2497
  const dx = p.x - nearest.x;
2289
2498
  const dy = p.y - nearest.y;
2290
2499
  const dist = Math.sqrt(dx * dx + dy * dy);
@@ -2301,9 +2510,9 @@ var FeatureTool = class {
2301
2510
  var _a;
2302
2511
  if (!this.currentGeometry || !this.context) return;
2303
2512
  const index = (_a = target.data) == null ? void 0 : _a.index;
2304
- if (index === void 0 || index < 0 || index >= this.features.length)
2513
+ if (index === void 0 || index < 0 || index >= this.workingFeatures.length)
2305
2514
  return;
2306
- const feature = this.features[index];
2515
+ const feature = this.workingFeatures[index];
2307
2516
  const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
2308
2517
  const { width, height, x, y } = geometry;
2309
2518
  const left = x - width / 2;
@@ -2316,20 +2525,10 @@ var FeatureTool = class {
2316
2525
  y: normalizedY
2317
2526
  // Could also update rotation if we allowed rotating markers
2318
2527
  };
2319
- const newFeatures = [...this.features];
2528
+ const newFeatures = [...this.workingFeatures];
2320
2529
  newFeatures[index] = updatedFeature;
2321
- this.features = newFeatures;
2322
- const configService = this.context.services.get(
2323
- "ConfigurationService"
2324
- );
2325
- if (configService) {
2326
- this.isUpdatingConfig = true;
2327
- try {
2328
- configService.update("dieline.features", this.features);
2329
- } finally {
2330
- this.isUpdatingConfig = false;
2331
- }
2332
- }
2530
+ this.setWorkingFeatures(newFeatures);
2531
+ this.emitWorkingChange();
2333
2532
  }
2334
2533
  redraw() {
2335
2534
  if (!this.canvasService || !this.currentGeometry) return;
@@ -2340,7 +2539,7 @@ var FeatureTool = class {
2340
2539
  return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
2341
2540
  });
2342
2541
  existing.forEach((obj) => canvas.remove(obj));
2343
- if (!this.features || this.features.length === 0) {
2542
+ if (!this.workingFeatures || this.workingFeatures.length === 0) {
2344
2543
  this.canvasService.requestRenderAll();
2345
2544
  return;
2346
2545
  }
@@ -2348,7 +2547,7 @@ var FeatureTool = class {
2348
2547
  const finalScale = scale;
2349
2548
  const groups = {};
2350
2549
  const singles = [];
2351
- this.features.forEach((f, i) => {
2550
+ this.workingFeatures.forEach((f, i) => {
2352
2551
  if (f.groupId) {
2353
2552
  if (!groups[f.groupId]) groups[f.groupId] = [];
2354
2553
  groups[f.groupId].push({ feature: f, index: i });
@@ -2416,25 +2615,6 @@ var FeatureTool = class {
2416
2615
  lockScalingY: true,
2417
2616
  data: { type: "feature-marker", index, isGroup: false }
2418
2617
  });
2419
- marker.set("opacity", 0);
2420
- marker.on("mouseover", () => {
2421
- marker.set("opacity", 1);
2422
- canvas.requestRenderAll();
2423
- });
2424
- marker.on("mouseout", () => {
2425
- if (canvas.getActiveObject() !== marker) {
2426
- marker.set("opacity", 0);
2427
- canvas.requestRenderAll();
2428
- }
2429
- });
2430
- marker.on("selected", () => {
2431
- marker.set("opacity", 1);
2432
- canvas.requestRenderAll();
2433
- });
2434
- marker.on("deselected", () => {
2435
- marker.set("opacity", 0);
2436
- canvas.requestRenderAll();
2437
- });
2438
2618
  canvas.add(marker);
2439
2619
  canvas.bringObjectToFront(marker);
2440
2620
  });
@@ -2471,25 +2651,6 @@ var FeatureTool = class {
2471
2651
  indices: members.map((m) => m.index)
2472
2652
  }
2473
2653
  });
2474
- groupObj.set("opacity", 0);
2475
- groupObj.on("mouseover", () => {
2476
- groupObj.set("opacity", 1);
2477
- canvas.requestRenderAll();
2478
- });
2479
- groupObj.on("mouseout", () => {
2480
- if (canvas.getActiveObject() !== groupObj) {
2481
- groupObj.set("opacity", 0);
2482
- canvas.requestRenderAll();
2483
- }
2484
- });
2485
- groupObj.on("selected", () => {
2486
- groupObj.set("opacity", 1);
2487
- canvas.requestRenderAll();
2488
- });
2489
- groupObj.on("deselected", () => {
2490
- groupObj.set("opacity", 0);
2491
- canvas.requestRenderAll();
2492
- });
2493
2654
  canvas.add(groupObj);
2494
2655
  canvas.bringObjectToFront(groupObj);
2495
2656
  });
@@ -2508,12 +2669,12 @@ var FeatureTool = class {
2508
2669
  if ((_a = marker.data) == null ? void 0 : _a.isGroup) {
2509
2670
  const indices = (_b = marker.data) == null ? void 0 : _b.indices;
2510
2671
  if (indices && indices.length > 0) {
2511
- feature = this.features[indices[0]];
2672
+ feature = this.workingFeatures[indices[0]];
2512
2673
  }
2513
2674
  } else {
2514
2675
  const index = (_c = marker.data) == null ? void 0 : _c.index;
2515
2676
  if (index !== void 0) {
2516
- feature = this.features[index];
2677
+ feature = this.workingFeatures[index];
2517
2678
  }
2518
2679
  }
2519
2680
  const geometry = this.getGeometryForFeature(
@@ -3237,7 +3398,7 @@ var RulerTool = class {
3237
3398
  // Dieline context for sync
3238
3399
  this.dielineWidth = 500;
3239
3400
  this.dielineHeight = 500;
3240
- this.dielineUnit = "mm";
3401
+ this.dielineDisplayUnit = "mm";
3241
3402
  this.dielinePadding = 40;
3242
3403
  this.dielineOffset = 0;
3243
3404
  if (options) {
@@ -3261,7 +3422,10 @@ var RulerTool = class {
3261
3422
  this.textColor = configService.get("ruler.textColor", this.textColor);
3262
3423
  this.lineColor = configService.get("ruler.lineColor", this.lineColor);
3263
3424
  this.fontSize = configService.get("ruler.fontSize", this.fontSize);
3264
- this.dielineUnit = configService.get("dieline.unit", this.dielineUnit);
3425
+ this.dielineDisplayUnit = configService.get(
3426
+ "dieline.displayUnit",
3427
+ this.dielineDisplayUnit
3428
+ );
3265
3429
  this.dielineWidth = configService.get("dieline.width", this.dielineWidth);
3266
3430
  this.dielineHeight = configService.get(
3267
3431
  "dieline.height",
@@ -3284,7 +3448,8 @@ var RulerTool = class {
3284
3448
  shouldUpdate = true;
3285
3449
  }
3286
3450
  } else if (e.key.startsWith("dieline.")) {
3287
- if (e.key === "dieline.unit") this.dielineUnit = e.value;
3451
+ if (e.key === "dieline.displayUnit")
3452
+ this.dielineDisplayUnit = e.value;
3288
3453
  if (e.key === "dieline.width") this.dielineWidth = e.value;
3289
3454
  if (e.key === "dieline.height") this.dielineHeight = e.value;
3290
3455
  if (e.key === "dieline.padding") this.dielinePadding = e.value;
@@ -3471,26 +3636,27 @@ var RulerTool = class {
3471
3636
  const width = this.canvasService.canvas.width || 800;
3472
3637
  const height = this.canvasService.canvas.height || 600;
3473
3638
  const paddingPx = this.resolvePadding(width, height);
3474
- const layout = Coordinate.calculateLayout(
3475
- { width, height },
3476
- { width: this.dielineWidth, height: this.dielineHeight },
3477
- paddingPx
3639
+ this.canvasService.viewport.setPadding(paddingPx);
3640
+ this.canvasService.viewport.updatePhysical(
3641
+ this.dielineWidth,
3642
+ this.dielineHeight
3478
3643
  );
3644
+ const layout = this.canvasService.viewport.layout;
3479
3645
  const scale = layout.scale;
3480
3646
  const offsetX = layout.offsetX;
3481
3647
  const offsetY = layout.offsetY;
3482
3648
  const visualWidth = layout.width;
3483
3649
  const visualHeight = layout.height;
3484
- const rawOffset = this.dielineOffset || 0;
3485
- const effectiveOffset = rawOffset > 0 ? rawOffset : 0;
3486
- const expandPixels = effectiveOffset * scale;
3650
+ const rawOffsetMm = this.dielineOffset || 0;
3651
+ const effectiveOffsetMm = rawOffsetMm > 0 ? rawOffsetMm : 0;
3652
+ const expandPixels = effectiveOffsetMm * scale;
3487
3653
  const gap = this.gap || 15;
3488
3654
  const rulerLeft = offsetX - expandPixels;
3489
3655
  const rulerTop = offsetY - expandPixels;
3490
3656
  const rulerRight = offsetX + visualWidth + expandPixels;
3491
3657
  const rulerBottom = offsetY + visualHeight + expandPixels;
3492
- const displayWidth = this.dielineWidth + effectiveOffset * 2;
3493
- const displayHeight = this.dielineHeight + effectiveOffset * 2;
3658
+ const displayWidthMm = this.dielineWidth + effectiveOffsetMm * 2;
3659
+ const displayHeightMm = this.dielineHeight + effectiveOffsetMm * 2;
3494
3660
  const topRulerY = rulerTop - gap;
3495
3661
  const topRulerXStart = rulerLeft;
3496
3662
  const topRulerXEnd = rulerRight;
@@ -3533,8 +3699,8 @@ var RulerTool = class {
3533
3699
  }
3534
3700
  )
3535
3701
  );
3536
- const widthStr = parseFloat(displayWidth.toFixed(2)).toString();
3537
- const topTextContent = `${widthStr} ${this.dielineUnit}`;
3702
+ const widthStr = formatMm(displayWidthMm, this.dielineDisplayUnit);
3703
+ const topTextContent = `${widthStr} ${this.dielineDisplayUnit}`;
3538
3704
  const topText = new import_fabric7.Text(topTextContent, {
3539
3705
  left: topRulerXStart + (rulerRight - rulerLeft) / 2,
3540
3706
  top: topRulerY,
@@ -3589,8 +3755,8 @@ var RulerTool = class {
3589
3755
  }
3590
3756
  )
3591
3757
  );
3592
- const heightStr = parseFloat(displayHeight.toFixed(2)).toString();
3593
- const leftTextContent = `${heightStr} ${this.dielineUnit}`;
3758
+ const heightStr = formatMm(displayHeightMm, this.dielineDisplayUnit);
3759
+ const leftTextContent = `${heightStr} ${this.dielineDisplayUnit}`;
3594
3760
  const leftText = new import_fabric7.Text(leftTextContent, {
3595
3761
  left: leftRulerX,
3596
3762
  top: leftRulerYStart + (rulerBottom - rulerTop) / 2,
@@ -3702,6 +3868,81 @@ var MirrorTool = class {
3702
3868
 
3703
3869
  // src/CanvasService.ts
3704
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
3705
3946
  var CanvasService = class {
3706
3947
  constructor(el, options) {
3707
3948
  if (el instanceof import_fabric8.Canvas) {
@@ -3712,6 +3953,10 @@ var CanvasService = class {
3712
3953
  ...options
3713
3954
  });
3714
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
+ }
3715
3960
  if (options == null ? void 0 : options.eventBus) {
3716
3961
  this.setEventBus(options.eventBus);
3717
3962
  }
@@ -3792,5 +4037,7 @@ var CanvasService = class {
3792
4037
  ImageTool,
3793
4038
  MirrorTool,
3794
4039
  RulerTool,
3795
- WhiteInkTool
4040
+ WhiteInkTool,
4041
+ formatMm,
4042
+ parseLengthToMm
3796
4043
  });