@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.mjs CHANGED
@@ -677,6 +677,29 @@ var Coordinate = class {
677
677
  }
678
678
  };
679
679
 
680
+ // src/units.ts
681
+ function parseLengthToMm(input, defaultUnit) {
682
+ var _a, _b;
683
+ if (typeof input === "number") {
684
+ if (!Number.isFinite(input)) return 0;
685
+ return Coordinate.convertUnit(input, defaultUnit, "mm");
686
+ }
687
+ const raw = input.trim();
688
+ if (!raw) return 0;
689
+ const match = raw.match(/^([+-]?\d+(?:\.\d+)?)\s*(px|mm|cm|in)?$/i);
690
+ if (!match) return 0;
691
+ const value = Number(match[1]);
692
+ if (!Number.isFinite(value)) return 0;
693
+ const unit = (_b = (_a = match[2]) == null ? void 0 : _a.toLowerCase()) != null ? _b : defaultUnit;
694
+ return Coordinate.convertUnit(value, unit, "mm");
695
+ }
696
+ function formatMm(valueMm, displayUnit, fractionDigits = 2) {
697
+ if (!Number.isFinite(valueMm)) return "0";
698
+ const value = Coordinate.convertUnit(valueMm, "mm", displayUnit);
699
+ const rounded = Number(value.toFixed(fractionDigits));
700
+ return rounded.toString();
701
+ }
702
+
680
703
  // src/geometry.ts
681
704
  import paper2 from "paper";
682
705
  function resolveFeaturePosition(feature, geometry) {
@@ -900,105 +923,6 @@ function getPathBounds(pathData) {
900
923
  };
901
924
  }
902
925
 
903
- // src/constraints.ts
904
- var ConstraintRegistry = class {
905
- static register(type, handler) {
906
- this.handlers.set(type, handler);
907
- }
908
- static apply(x, y, feature, context) {
909
- if (!feature.constraints || !feature.constraints.type) {
910
- return { x, y };
911
- }
912
- const handler = this.handlers.get(feature.constraints.type);
913
- if (handler) {
914
- return handler(x, y, feature, context);
915
- }
916
- return { x, y };
917
- }
918
- };
919
- ConstraintRegistry.handlers = /* @__PURE__ */ new Map();
920
- var edgeConstraint = (x, y, feature, context) => {
921
- var _a;
922
- const { dielineWidth, dielineHeight } = context;
923
- const params = ((_a = feature.constraints) == null ? void 0 : _a.params) || {};
924
- const allowedEdges = params.allowedEdges || [
925
- "top",
926
- "bottom",
927
- "left",
928
- "right"
929
- ];
930
- const confine = params.confine || false;
931
- const offset = params.offset || 0;
932
- const distances = [];
933
- if (allowedEdges.includes("top"))
934
- distances.push({ edge: "top", dist: y * dielineHeight });
935
- if (allowedEdges.includes("bottom"))
936
- distances.push({ edge: "bottom", dist: (1 - y) * dielineHeight });
937
- if (allowedEdges.includes("left"))
938
- distances.push({ edge: "left", dist: x * dielineWidth });
939
- if (allowedEdges.includes("right"))
940
- distances.push({ edge: "right", dist: (1 - x) * dielineWidth });
941
- if (distances.length === 0) return { x, y };
942
- distances.sort((a, b) => a.dist - b.dist);
943
- const nearest = distances[0].edge;
944
- let newX = x;
945
- let newY = y;
946
- const fw = feature.width || 0;
947
- const fh = feature.height || 0;
948
- switch (nearest) {
949
- case "top":
950
- newY = 0 + offset / dielineHeight;
951
- if (confine) {
952
- const minX = fw / 2 / dielineWidth;
953
- const maxX = 1 - minX;
954
- newX = Math.max(minX, Math.min(newX, maxX));
955
- }
956
- break;
957
- case "bottom":
958
- newY = 1 - offset / dielineHeight;
959
- if (confine) {
960
- const minX = fw / 2 / dielineWidth;
961
- const maxX = 1 - minX;
962
- newX = Math.max(minX, Math.min(newX, maxX));
963
- }
964
- break;
965
- case "left":
966
- newX = 0 + offset / dielineWidth;
967
- if (confine) {
968
- const minY = fh / 2 / dielineHeight;
969
- const maxY = 1 - minY;
970
- newY = Math.max(minY, Math.min(newY, maxY));
971
- }
972
- break;
973
- case "right":
974
- newX = 1 - offset / dielineWidth;
975
- if (confine) {
976
- const minY = fh / 2 / dielineHeight;
977
- const maxY = 1 - minY;
978
- newY = Math.max(minY, Math.min(newY, maxY));
979
- }
980
- break;
981
- }
982
- return { x: newX, y: newY };
983
- };
984
- var internalConstraint = (x, y, feature, context) => {
985
- var _a;
986
- const { dielineWidth, dielineHeight } = context;
987
- const params = ((_a = feature.constraints) == null ? void 0 : _a.params) || {};
988
- const margin = params.margin || 0;
989
- const fw = feature.width || 0;
990
- const fh = feature.height || 0;
991
- const minX = (margin + fw / 2) / dielineWidth;
992
- const maxX = 1 - (margin + fw / 2) / dielineWidth;
993
- const minY = (margin + fh / 2) / dielineHeight;
994
- const maxY = 1 - (margin + fh / 2) / dielineHeight;
995
- const clampedX = minX > maxX ? 0.5 : Math.max(minX, Math.min(x, maxX));
996
- const clampedY = minY > maxY ? 0.5 : Math.max(minY, Math.min(y, maxY));
997
- return { x: clampedX, y: clampedY };
998
- };
999
- ConstraintRegistry.register("edge", edgeConstraint);
1000
- ConstraintRegistry.register("internal", internalConstraint);
1001
-
1002
926
  // src/dieline.ts
1003
927
  var DielineTool = class {
1004
928
  constructor(options) {
@@ -1007,7 +931,7 @@ var DielineTool = class {
1007
931
  name: "DielineTool"
1008
932
  };
1009
933
  this.state = {
1010
- unit: "mm",
934
+ displayUnit: "mm",
1011
935
  shape: "rect",
1012
936
  width: 500,
1013
937
  height: 500,
@@ -1053,50 +977,88 @@ var DielineTool = class {
1053
977
  const configService = context.services.get("ConfigurationService");
1054
978
  if (configService) {
1055
979
  const s = this.state;
1056
- s.unit = configService.get("dieline.unit", s.unit);
980
+ s.displayUnit = configService.get("dieline.displayUnit", s.displayUnit);
1057
981
  s.shape = configService.get("dieline.shape", s.shape);
1058
- s.width = configService.get("dieline.width", s.width);
1059
- s.height = configService.get("dieline.height", s.height);
1060
- s.radius = configService.get("dieline.radius", s.radius);
982
+ s.width = parseLengthToMm(
983
+ configService.get("dieline.width", s.width),
984
+ "mm"
985
+ );
986
+ s.height = parseLengthToMm(
987
+ configService.get("dieline.height", s.height),
988
+ "mm"
989
+ );
990
+ s.radius = parseLengthToMm(
991
+ configService.get("dieline.radius", s.radius),
992
+ "mm"
993
+ );
1061
994
  s.padding = configService.get("dieline.padding", s.padding);
1062
- s.offset = configService.get("dieline.offset", s.offset);
1063
- s.mainLine.width = configService.get("dieline.strokeWidth", s.mainLine.width);
1064
- s.mainLine.color = configService.get("dieline.strokeColor", s.mainLine.color);
1065
- s.mainLine.dashLength = configService.get("dieline.dashLength", s.mainLine.dashLength);
995
+ s.offset = parseLengthToMm(
996
+ configService.get("dieline.offset", s.offset),
997
+ "mm"
998
+ );
999
+ s.mainLine.width = configService.get(
1000
+ "dieline.strokeWidth",
1001
+ s.mainLine.width
1002
+ );
1003
+ s.mainLine.color = configService.get(
1004
+ "dieline.strokeColor",
1005
+ s.mainLine.color
1006
+ );
1007
+ s.mainLine.dashLength = configService.get(
1008
+ "dieline.dashLength",
1009
+ s.mainLine.dashLength
1010
+ );
1066
1011
  s.mainLine.style = configService.get("dieline.style", s.mainLine.style);
1067
- s.offsetLine.width = configService.get("dieline.offsetStrokeWidth", s.offsetLine.width);
1068
- s.offsetLine.color = configService.get("dieline.offsetStrokeColor", s.offsetLine.color);
1069
- s.offsetLine.dashLength = configService.get("dieline.offsetDashLength", s.offsetLine.dashLength);
1070
- s.offsetLine.style = configService.get("dieline.offsetStyle", s.offsetLine.style);
1012
+ s.offsetLine.width = configService.get(
1013
+ "dieline.offsetStrokeWidth",
1014
+ s.offsetLine.width
1015
+ );
1016
+ s.offsetLine.color = configService.get(
1017
+ "dieline.offsetStrokeColor",
1018
+ s.offsetLine.color
1019
+ );
1020
+ s.offsetLine.dashLength = configService.get(
1021
+ "dieline.offsetDashLength",
1022
+ s.offsetLine.dashLength
1023
+ );
1024
+ s.offsetLine.style = configService.get(
1025
+ "dieline.offsetStyle",
1026
+ s.offsetLine.style
1027
+ );
1071
1028
  s.insideColor = configService.get("dieline.insideColor", s.insideColor);
1072
- s.outsideColor = configService.get("dieline.outsideColor", s.outsideColor);
1073
- s.showBleedLines = configService.get("dieline.showBleedLines", s.showBleedLines);
1029
+ s.outsideColor = configService.get(
1030
+ "dieline.outsideColor",
1031
+ s.outsideColor
1032
+ );
1033
+ s.showBleedLines = configService.get(
1034
+ "dieline.showBleedLines",
1035
+ s.showBleedLines
1036
+ );
1074
1037
  s.features = configService.get("dieline.features", s.features);
1075
1038
  s.pathData = configService.get("dieline.pathData", s.pathData);
1076
1039
  configService.onAnyChange((e) => {
1077
1040
  if (e.key.startsWith("dieline.")) {
1078
- console.log(`[DielineTool] Config change detected: ${e.key} -> ${e.value}`);
1079
1041
  switch (e.key) {
1080
- case "dieline.unit":
1081
- s.unit = e.value;
1042
+ case "dieline.displayUnit":
1043
+ s.displayUnit = e.value;
1082
1044
  break;
1083
1045
  case "dieline.shape":
1084
1046
  s.shape = e.value;
1085
1047
  break;
1086
1048
  case "dieline.width":
1087
- s.width = e.value;
1049
+ s.width = parseLengthToMm(e.value, "mm");
1088
1050
  break;
1089
1051
  case "dieline.height":
1090
- s.height = e.value;
1052
+ s.height = parseLengthToMm(e.value, "mm");
1091
1053
  break;
1092
1054
  case "dieline.radius":
1093
- s.radius = e.value;
1055
+ s.radius = parseLengthToMm(e.value, "mm");
1094
1056
  break;
1095
1057
  case "dieline.padding":
1096
1058
  s.padding = e.value;
1097
1059
  break;
1098
1060
  case "dieline.offset":
1099
- s.offset = e.value;
1061
+ s.offset = parseLengthToMm(e.value, "mm");
1100
1062
  break;
1101
1063
  case "dieline.strokeWidth":
1102
1064
  s.mainLine.width = e.value;
@@ -1155,11 +1117,11 @@ var DielineTool = class {
1155
1117
  return {
1156
1118
  [ContributionPointIds2.CONFIGURATIONS]: [
1157
1119
  {
1158
- id: "dieline.unit",
1120
+ id: "dieline.displayUnit",
1159
1121
  type: "select",
1160
- label: "Unit",
1161
- options: ["px", "mm", "cm", "in"],
1162
- default: s.unit
1122
+ label: "Display Unit",
1123
+ options: ["mm", "cm", "in"],
1124
+ default: s.displayUnit
1163
1125
  },
1164
1126
  {
1165
1127
  id: "dieline.shape",
@@ -1171,7 +1133,7 @@ var DielineTool = class {
1171
1133
  {
1172
1134
  id: "dieline.width",
1173
1135
  type: "number",
1174
- label: "Width",
1136
+ label: "Width (mm)",
1175
1137
  min: 10,
1176
1138
  max: 2e3,
1177
1139
  default: s.width
@@ -1179,7 +1141,7 @@ var DielineTool = class {
1179
1141
  {
1180
1142
  id: "dieline.height",
1181
1143
  type: "number",
1182
- label: "Height",
1144
+ label: "Height (mm)",
1183
1145
  min: 10,
1184
1146
  max: 2e3,
1185
1147
  default: s.height
@@ -1187,7 +1149,7 @@ var DielineTool = class {
1187
1149
  {
1188
1150
  id: "dieline.radius",
1189
1151
  type: "number",
1190
- label: "Corner Radius",
1152
+ label: "Corner Radius (mm)",
1191
1153
  min: 0,
1192
1154
  max: 500,
1193
1155
  default: s.radius
@@ -1202,7 +1164,7 @@ var DielineTool = class {
1202
1164
  {
1203
1165
  id: "dieline.offset",
1204
1166
  type: "number",
1205
- label: "Bleed Offset",
1167
+ label: "Bleed Offset (mm)",
1206
1168
  min: -100,
1207
1169
  max: 100,
1208
1170
  default: s.offset
@@ -1303,18 +1265,12 @@ var DielineTool = class {
1303
1265
  );
1304
1266
  if (!configService) return;
1305
1267
  const features = configService.get("dieline.features") || [];
1306
- const dielineWidth = configService.get("dieline.width") || 500;
1307
- const dielineHeight = configService.get("dieline.height") || 500;
1308
1268
  let changed = false;
1309
1269
  const newFeatures = features.map((f) => {
1310
1270
  if (f.groupId === groupId) {
1311
- const constrained = ConstraintRegistry.apply(x, y, f, {
1312
- dielineWidth,
1313
- dielineHeight
1314
- });
1315
- if (f.x !== constrained.x || f.y !== constrained.y) {
1271
+ if (f.x !== x || f.y !== y) {
1316
1272
  changed = true;
1317
- return { ...f, x: constrained.x, y: constrained.y };
1273
+ return { ...f, x, y };
1318
1274
  }
1319
1275
  }
1320
1276
  return f;
@@ -1429,7 +1385,7 @@ var DielineTool = class {
1429
1385
  const layer = this.getLayer();
1430
1386
  if (!layer) return;
1431
1387
  const {
1432
- unit,
1388
+ displayUnit,
1433
1389
  shape,
1434
1390
  radius,
1435
1391
  offset,
@@ -1440,15 +1396,13 @@ var DielineTool = class {
1440
1396
  showBleedLines,
1441
1397
  features
1442
1398
  } = this.state;
1443
- let { width, height } = this.state;
1399
+ const { width, height } = this.state;
1444
1400
  const canvasW = this.canvasService.canvas.width || 800;
1445
1401
  const canvasH = this.canvasService.canvas.height || 600;
1446
1402
  const paddingPx = this.resolvePadding(canvasW, canvasH);
1447
- const layout = Coordinate.calculateLayout(
1448
- { width: canvasW, height: canvasH },
1449
- { width, height },
1450
- paddingPx
1451
- );
1403
+ this.canvasService.viewport.setPadding(paddingPx);
1404
+ this.canvasService.viewport.updatePhysical(width, height);
1405
+ const layout = this.canvasService.viewport.layout;
1452
1406
  const scale = layout.scale;
1453
1407
  const cx = layout.offsetX + layout.width / 2;
1454
1408
  const cy = layout.offsetY + layout.height / 2;
@@ -1635,15 +1589,22 @@ var DielineTool = class {
1635
1589
  }
1636
1590
  getGeometry() {
1637
1591
  if (!this.canvasService) return null;
1638
- const { unit, shape, width, height, radius, offset, mainLine, pathData } = this.state;
1592
+ const {
1593
+ displayUnit,
1594
+ shape,
1595
+ width,
1596
+ height,
1597
+ radius,
1598
+ offset,
1599
+ mainLine,
1600
+ pathData
1601
+ } = this.state;
1639
1602
  const canvasW = this.canvasService.canvas.width || 800;
1640
1603
  const canvasH = this.canvasService.canvas.height || 600;
1641
1604
  const paddingPx = this.resolvePadding(canvasW, canvasH);
1642
- const layout = Coordinate.calculateLayout(
1643
- { width: canvasW, height: canvasH },
1644
- { width, height },
1645
- paddingPx
1646
- );
1605
+ this.canvasService.viewport.setPadding(paddingPx);
1606
+ this.canvasService.viewport.updatePhysical(width, height);
1607
+ const layout = this.canvasService.viewport.layout;
1647
1608
  const scale = layout.scale;
1648
1609
  const cx = layout.offsetX + layout.width / 2;
1649
1610
  const cy = layout.offsetY + layout.height / 2;
@@ -1651,14 +1612,14 @@ var DielineTool = class {
1651
1612
  const visualHeight = layout.height;
1652
1613
  return {
1653
1614
  shape,
1654
- unit,
1615
+ unit: "mm",
1616
+ displayUnit,
1655
1617
  x: cx,
1656
1618
  y: cy,
1657
1619
  width: visualWidth,
1658
1620
  height: visualHeight,
1659
1621
  radius: radius * scale,
1660
1622
  offset: offset * scale,
1661
- // Pass scale to help other tools (like FeatureTool) convert units
1662
1623
  scale,
1663
1624
  strokeWidth: mainLine.width,
1664
1625
  pathData
@@ -1668,15 +1629,13 @@ var DielineTool = class {
1668
1629
  if (!this.canvasService) return null;
1669
1630
  const userLayer = this.canvasService.getLayer("user");
1670
1631
  if (!userLayer) return null;
1671
- const { shape, width, height, radius, features, unit, pathData } = this.state;
1632
+ const { shape, width, height, radius, features, pathData } = this.state;
1672
1633
  const canvasW = this.canvasService.canvas.width || 800;
1673
1634
  const canvasH = this.canvasService.canvas.height || 600;
1674
1635
  const paddingPx = this.resolvePadding(canvasW, canvasH);
1675
- const layout = Coordinate.calculateLayout(
1676
- { width: canvasW, height: canvasH },
1677
- { width, height },
1678
- paddingPx
1679
- );
1636
+ this.canvasService.viewport.setPadding(paddingPx);
1637
+ this.canvasService.viewport.updatePhysical(width, height);
1638
+ const layout = this.canvasService.viewport.layout;
1680
1639
  const scale = layout.scale;
1681
1640
  const cx = layout.offsetX + layout.width / 2;
1682
1641
  const cy = layout.offsetY + layout.height / 2;
@@ -1895,13 +1854,158 @@ import {
1895
1854
  ContributionPointIds as ContributionPointIds4
1896
1855
  } from "@pooder/core";
1897
1856
  import { Circle, Group, Point, Rect as Rect2 } from "fabric";
1857
+
1858
+ // src/constraints.ts
1859
+ var ConstraintRegistry = class {
1860
+ static register(type, handler) {
1861
+ this.handlers.set(type, handler);
1862
+ }
1863
+ static apply(x, y, feature, context) {
1864
+ if (!feature.constraints || !feature.constraints.type) {
1865
+ return { x, y };
1866
+ }
1867
+ const handler = this.handlers.get(feature.constraints.type);
1868
+ if (handler) {
1869
+ return handler(x, y, feature, context);
1870
+ }
1871
+ return { x, y };
1872
+ }
1873
+ };
1874
+ ConstraintRegistry.handlers = /* @__PURE__ */ new Map();
1875
+ var edgeConstraint = (x, y, feature, context) => {
1876
+ var _a;
1877
+ const { dielineWidth, dielineHeight } = context;
1878
+ const params = ((_a = feature.constraints) == null ? void 0 : _a.params) || {};
1879
+ const allowedEdges = params.allowedEdges || [
1880
+ "top",
1881
+ "bottom",
1882
+ "left",
1883
+ "right"
1884
+ ];
1885
+ const confine = params.confine || false;
1886
+ const offset = params.offset || 0;
1887
+ const distances = [];
1888
+ if (allowedEdges.includes("top"))
1889
+ distances.push({ edge: "top", dist: y * dielineHeight });
1890
+ if (allowedEdges.includes("bottom"))
1891
+ distances.push({ edge: "bottom", dist: (1 - y) * dielineHeight });
1892
+ if (allowedEdges.includes("left"))
1893
+ distances.push({ edge: "left", dist: x * dielineWidth });
1894
+ if (allowedEdges.includes("right"))
1895
+ distances.push({ edge: "right", dist: (1 - x) * dielineWidth });
1896
+ if (distances.length === 0) return { x, y };
1897
+ distances.sort((a, b) => a.dist - b.dist);
1898
+ const nearest = distances[0].edge;
1899
+ let newX = x;
1900
+ let newY = y;
1901
+ const fw = feature.width || 0;
1902
+ const fh = feature.height || 0;
1903
+ switch (nearest) {
1904
+ case "top":
1905
+ newY = 0 + offset / dielineHeight;
1906
+ if (confine) {
1907
+ const minX = fw / 2 / dielineWidth;
1908
+ const maxX = 1 - minX;
1909
+ newX = Math.max(minX, Math.min(newX, maxX));
1910
+ }
1911
+ break;
1912
+ case "bottom":
1913
+ newY = 1 - offset / dielineHeight;
1914
+ if (confine) {
1915
+ const minX = fw / 2 / dielineWidth;
1916
+ const maxX = 1 - minX;
1917
+ newX = Math.max(minX, Math.min(newX, maxX));
1918
+ }
1919
+ break;
1920
+ case "left":
1921
+ newX = 0 + offset / dielineWidth;
1922
+ if (confine) {
1923
+ const minY = fh / 2 / dielineHeight;
1924
+ const maxY = 1 - minY;
1925
+ newY = Math.max(minY, Math.min(newY, maxY));
1926
+ }
1927
+ break;
1928
+ case "right":
1929
+ newX = 1 - offset / dielineWidth;
1930
+ if (confine) {
1931
+ const minY = fh / 2 / dielineHeight;
1932
+ const maxY = 1 - minY;
1933
+ newY = Math.max(minY, Math.min(newY, maxY));
1934
+ }
1935
+ break;
1936
+ }
1937
+ return { x: newX, y: newY };
1938
+ };
1939
+ var internalConstraint = (x, y, feature, context) => {
1940
+ var _a;
1941
+ const { dielineWidth, dielineHeight } = context;
1942
+ const params = ((_a = feature.constraints) == null ? void 0 : _a.params) || {};
1943
+ const margin = params.margin || 0;
1944
+ const fw = feature.width || 0;
1945
+ const fh = feature.height || 0;
1946
+ const minX = (margin + fw / 2) / dielineWidth;
1947
+ const maxX = 1 - (margin + fw / 2) / dielineWidth;
1948
+ const minY = (margin + fh / 2) / dielineHeight;
1949
+ const maxY = 1 - (margin + fh / 2) / dielineHeight;
1950
+ const clampedX = minX > maxX ? 0.5 : Math.max(minX, Math.min(x, maxX));
1951
+ const clampedY = minY > maxY ? 0.5 : Math.max(minY, Math.min(y, maxY));
1952
+ return { x: clampedX, y: clampedY };
1953
+ };
1954
+ var tangentBottomConstraint = (x, y, feature, context) => {
1955
+ var _a;
1956
+ const { dielineWidth, dielineHeight } = context;
1957
+ const params = ((_a = feature.constraints) == null ? void 0 : _a.params) || {};
1958
+ const gap = params.gap || 0;
1959
+ const confineX = params.confineX !== false;
1960
+ const extentY = feature.shape === "circle" ? feature.radius || 0 : (feature.height || 0) / 2;
1961
+ const newY = 1 + (extentY + gap) / dielineHeight;
1962
+ let newX = x;
1963
+ if (confineX) {
1964
+ const extentX = feature.shape === "circle" ? feature.radius || 0 : (feature.width || 0) / 2;
1965
+ const minX = extentX / dielineWidth;
1966
+ const maxX = 1 - extentX / dielineWidth;
1967
+ newX = minX > maxX ? 0.5 : Math.max(minX, Math.min(newX, maxX));
1968
+ }
1969
+ return { x: newX, y: newY };
1970
+ };
1971
+ ConstraintRegistry.register("edge", edgeConstraint);
1972
+ ConstraintRegistry.register("internal", internalConstraint);
1973
+ ConstraintRegistry.register("tangent-bottom", tangentBottomConstraint);
1974
+
1975
+ // src/featureComplete.ts
1976
+ function validateFeaturesStrict(features, context) {
1977
+ var _a;
1978
+ const eps = 1e-6;
1979
+ const issues = [];
1980
+ for (const f of features) {
1981
+ if (!((_a = f.constraints) == null ? void 0 : _a.type)) continue;
1982
+ const constrained = ConstraintRegistry.apply(f.x, f.y, f, context);
1983
+ if (Math.abs(constrained.x - f.x) > eps || Math.abs(constrained.y - f.y) > eps) {
1984
+ issues.push({
1985
+ featureId: f.id,
1986
+ groupId: f.groupId,
1987
+ reason: "Position violates constraint strategy"
1988
+ });
1989
+ }
1990
+ }
1991
+ return { ok: issues.length === 0, issues: issues.length ? issues : void 0 };
1992
+ }
1993
+ function completeFeaturesStrict(features, context, update) {
1994
+ const validation = validateFeaturesStrict(features, context);
1995
+ if (!validation.ok) return validation;
1996
+ const next = JSON.parse(JSON.stringify(features || []));
1997
+ update(next);
1998
+ return { ok: true };
1999
+ }
2000
+
2001
+ // src/feature.ts
1898
2002
  var FeatureTool = class {
1899
2003
  constructor(options) {
1900
2004
  this.id = "pooder.kit.feature";
1901
2005
  this.metadata = {
1902
2006
  name: "FeatureTool"
1903
2007
  };
1904
- this.features = [];
2008
+ this.workingFeatures = [];
1905
2009
  this.isUpdatingConfig = false;
1906
2010
  this.isToolActive = false;
1907
2011
  this.handleMoving = null;
@@ -1927,12 +2031,15 @@ var FeatureTool = class {
1927
2031
  "ConfigurationService"
1928
2032
  );
1929
2033
  if (configService) {
1930
- this.features = configService.get("dieline.features", []);
2034
+ const features = configService.get("dieline.features", []) || [];
2035
+ this.workingFeatures = this.cloneFeatures(features);
1931
2036
  configService.onAnyChange((e) => {
1932
2037
  if (this.isUpdatingConfig) return;
1933
2038
  if (e.key === "dieline.features") {
1934
- this.features = e.value || [];
2039
+ const next = e.value || [];
2040
+ this.workingFeatures = this.cloneFeatures(next);
1935
2041
  this.redraw();
2042
+ this.emitWorkingChange();
1936
2043
  }
1937
2044
  });
1938
2045
  }
@@ -1990,27 +2097,151 @@ var FeatureTool = class {
1990
2097
  command: "clearFeatures",
1991
2098
  title: "Clear Features",
1992
2099
  handler: () => {
1993
- var _a;
1994
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1995
- "ConfigurationService"
1996
- );
1997
- if (configService) {
1998
- configService.update("dieline.features", []);
1999
- }
2100
+ this.setWorkingFeatures([]);
2101
+ this.redraw();
2102
+ this.emitWorkingChange();
2000
2103
  return true;
2001
2104
  }
2105
+ },
2106
+ {
2107
+ command: "getWorkingFeatures",
2108
+ title: "Get Working Features",
2109
+ handler: () => {
2110
+ return this.cloneFeatures(this.workingFeatures);
2111
+ }
2112
+ },
2113
+ {
2114
+ command: "setWorkingFeatures",
2115
+ title: "Set Working Features",
2116
+ handler: async (features) => {
2117
+ await this.refreshGeometry();
2118
+ this.setWorkingFeatures(this.cloneFeatures(features || []));
2119
+ this.redraw();
2120
+ this.emitWorkingChange();
2121
+ return { ok: true };
2122
+ }
2123
+ },
2124
+ {
2125
+ command: "updateWorkingGroupPosition",
2126
+ title: "Update Working Group Position",
2127
+ handler: (groupId, x, y) => {
2128
+ return this.updateWorkingGroupPosition(groupId, x, y);
2129
+ }
2130
+ },
2131
+ {
2132
+ command: "completeFeatures",
2133
+ title: "Complete Features",
2134
+ handler: () => {
2135
+ return this.completeFeatures();
2136
+ }
2002
2137
  }
2003
2138
  ]
2004
2139
  };
2005
2140
  }
2006
- addFeature(type) {
2141
+ cloneFeatures(features) {
2142
+ return JSON.parse(JSON.stringify(features || []));
2143
+ }
2144
+ emitWorkingChange() {
2007
2145
  var _a;
2008
- if (!this.canvasService) return false;
2009
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
2010
- "ConfigurationService"
2146
+ (_a = this.context) == null ? void 0 : _a.eventBus.emit("feature:working:change", {
2147
+ features: this.cloneFeatures(this.workingFeatures)
2148
+ });
2149
+ }
2150
+ async refreshGeometry() {
2151
+ if (!this.context) return;
2152
+ const commandService = this.context.services.get("CommandService");
2153
+ if (!commandService) return;
2154
+ try {
2155
+ const g = await Promise.resolve(commandService.executeCommand("getGeometry"));
2156
+ if (g) this.currentGeometry = g;
2157
+ } catch (e) {
2158
+ }
2159
+ }
2160
+ setWorkingFeatures(next) {
2161
+ this.workingFeatures = next;
2162
+ }
2163
+ updateWorkingGroupPosition(groupId, x, y) {
2164
+ var _a, _b, _c;
2165
+ if (!groupId) return { ok: false };
2166
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get("ConfigurationService");
2167
+ if (!configService) return { ok: false };
2168
+ const dielineWidth = parseLengthToMm(
2169
+ (_b = configService.get("dieline.width")) != null ? _b : 500,
2170
+ "mm"
2171
+ );
2172
+ const dielineHeight = parseLengthToMm(
2173
+ (_c = configService.get("dieline.height")) != null ? _c : 500,
2174
+ "mm"
2175
+ );
2176
+ let changed = false;
2177
+ const next = this.workingFeatures.map((f) => {
2178
+ if (f.groupId !== groupId) return f;
2179
+ let nx = x;
2180
+ let ny = y;
2181
+ if (f.constraints && dielineWidth > 0 && dielineHeight > 0) {
2182
+ const constrained = ConstraintRegistry.apply(nx, ny, f, {
2183
+ dielineWidth,
2184
+ dielineHeight
2185
+ });
2186
+ nx = constrained.x;
2187
+ ny = constrained.y;
2188
+ }
2189
+ if (f.x !== nx || f.y !== ny) {
2190
+ changed = true;
2191
+ return { ...f, x: nx, y: ny };
2192
+ }
2193
+ return f;
2194
+ });
2195
+ if (!changed) return { ok: true };
2196
+ this.setWorkingFeatures(next);
2197
+ this.redraw();
2198
+ this.enforceConstraints();
2199
+ this.emitWorkingChange();
2200
+ return { ok: true };
2201
+ }
2202
+ completeFeatures() {
2203
+ var _a, _b, _c;
2204
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get("ConfigurationService");
2205
+ if (!configService) {
2206
+ return {
2207
+ ok: false,
2208
+ issues: [
2209
+ { featureId: "unknown", reason: "ConfigurationService not found" }
2210
+ ]
2211
+ };
2212
+ }
2213
+ const dielineWidth = parseLengthToMm(
2214
+ (_b = configService.get("dieline.width")) != null ? _b : 500,
2215
+ "mm"
2216
+ );
2217
+ const dielineHeight = parseLengthToMm(
2218
+ (_c = configService.get("dieline.height")) != null ? _c : 500,
2219
+ "mm"
2220
+ );
2221
+ const result = completeFeaturesStrict(
2222
+ this.workingFeatures,
2223
+ { dielineWidth, dielineHeight },
2224
+ (next) => {
2225
+ this.isUpdatingConfig = true;
2226
+ try {
2227
+ configService.update("dieline.features", next);
2228
+ } finally {
2229
+ this.isUpdatingConfig = false;
2230
+ }
2231
+ this.workingFeatures = this.cloneFeatures(next);
2232
+ this.emitWorkingChange();
2233
+ }
2011
2234
  );
2012
- const unit = (configService == null ? void 0 : configService.get("dieline.unit", "mm")) || "mm";
2013
- const defaultSize = Coordinate.convertUnit(10, "mm", unit);
2235
+ if (!result.ok) {
2236
+ return {
2237
+ ok: false,
2238
+ issues: result.issues
2239
+ };
2240
+ }
2241
+ return { ok: true };
2242
+ }
2243
+ addFeature(type) {
2244
+ if (!this.canvasService) return false;
2014
2245
  const newFeature = {
2015
2246
  id: Date.now().toString(),
2016
2247
  operation: type,
@@ -2019,28 +2250,17 @@ var FeatureTool = class {
2019
2250
  x: 0.5,
2020
2251
  y: 0,
2021
2252
  // Top edge
2022
- width: defaultSize,
2023
- height: defaultSize,
2253
+ width: 10,
2254
+ height: 10,
2024
2255
  rotation: 0
2025
2256
  };
2026
- if (configService) {
2027
- const current = configService.get(
2028
- "dieline.features",
2029
- []
2030
- );
2031
- configService.update("dieline.features", [...current, newFeature]);
2032
- }
2257
+ this.setWorkingFeatures([...this.workingFeatures || [], newFeature]);
2258
+ this.redraw();
2259
+ this.emitWorkingChange();
2033
2260
  return true;
2034
2261
  }
2035
2262
  addDoubleLayerHole() {
2036
- var _a;
2037
2263
  if (!this.canvasService) return false;
2038
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
2039
- "ConfigurationService"
2040
- );
2041
- const unit = (configService == null ? void 0 : configService.get("dieline.unit", "mm")) || "mm";
2042
- const lugRadius = Coordinate.convertUnit(20, "mm", unit);
2043
- const holeRadius = Coordinate.convertUnit(15, "mm", unit);
2044
2264
  const groupId = Date.now().toString();
2045
2265
  const timestamp = Date.now();
2046
2266
  const lug = {
@@ -2051,8 +2271,7 @@ var FeatureTool = class {
2051
2271
  placement: "edge",
2052
2272
  x: 0.5,
2053
2273
  y: 0,
2054
- radius: lugRadius,
2055
- // 20mm
2274
+ radius: 20,
2056
2275
  rotation: 0
2057
2276
  };
2058
2277
  const hole = {
@@ -2063,17 +2282,12 @@ var FeatureTool = class {
2063
2282
  placement: "edge",
2064
2283
  x: 0.5,
2065
2284
  y: 0,
2066
- radius: holeRadius,
2067
- // 15mm
2285
+ radius: 15,
2068
2286
  rotation: 0
2069
2287
  };
2070
- if (configService) {
2071
- const current = configService.get(
2072
- "dieline.features",
2073
- []
2074
- );
2075
- configService.update("dieline.features", [...current, lug, hole]);
2076
- }
2288
+ this.setWorkingFeatures([...this.workingFeatures || [], lug, hole]);
2289
+ this.redraw();
2290
+ this.emitWorkingChange();
2077
2291
  return true;
2078
2292
  }
2079
2293
  getGeometryForFeature(geometry, feature) {
@@ -2117,12 +2331,12 @@ var FeatureTool = class {
2117
2331
  if ((_b = target.data) == null ? void 0 : _b.isGroup) {
2118
2332
  const indices = (_c = target.data) == null ? void 0 : _c.indices;
2119
2333
  if (indices && indices.length > 0) {
2120
- feature = this.features[indices[0]];
2334
+ feature = this.workingFeatures[indices[0]];
2121
2335
  }
2122
2336
  } else {
2123
2337
  const index = (_d = target.data) == null ? void 0 : _d.index;
2124
2338
  if (index !== void 0) {
2125
- feature = this.features[index];
2339
+ feature = this.workingFeatures[index];
2126
2340
  }
2127
2341
  }
2128
2342
  const geometry = this.getGeometryForFeature(
@@ -2143,7 +2357,7 @@ var FeatureTool = class {
2143
2357
  }
2144
2358
  if (!this.handleModified) {
2145
2359
  this.handleModified = (e) => {
2146
- var _a, _b, _c, _d;
2360
+ var _a, _b, _c;
2147
2361
  const target = e.target;
2148
2362
  if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return;
2149
2363
  if ((_b = target.data) == null ? void 0 : _b.isGroup) {
@@ -2151,11 +2365,11 @@ var FeatureTool = class {
2151
2365
  const indices = (_c = groupObj.data) == null ? void 0 : _c.indices;
2152
2366
  if (!indices) return;
2153
2367
  const groupCenter = new Point(groupObj.left, groupObj.top);
2154
- const newFeatures = [...this.features];
2368
+ const newFeatures = [...this.workingFeatures];
2155
2369
  const { x, y } = this.currentGeometry;
2156
2370
  groupObj.getObjects().forEach((child, i) => {
2157
2371
  const originalIndex = indices[i];
2158
- const feature = this.features[originalIndex];
2372
+ const feature = this.workingFeatures[originalIndex];
2159
2373
  const geometry = this.getGeometryForFeature(
2160
2374
  this.currentGeometry,
2161
2375
  feature
@@ -2173,18 +2387,8 @@ var FeatureTool = class {
2173
2387
  y: normalizedY
2174
2388
  };
2175
2389
  });
2176
- this.features = newFeatures;
2177
- const configService = (_d = this.context) == null ? void 0 : _d.services.get(
2178
- "ConfigurationService"
2179
- );
2180
- if (configService) {
2181
- this.isUpdatingConfig = true;
2182
- try {
2183
- configService.update("dieline.features", this.features);
2184
- } finally {
2185
- this.isUpdatingConfig = false;
2186
- }
2187
- }
2390
+ this.setWorkingFeatures(newFeatures);
2391
+ this.emitWorkingChange();
2188
2392
  } else {
2189
2393
  this.syncFeatureFromCanvas(target);
2190
2394
  }
@@ -2245,10 +2449,13 @@ var FeatureTool = class {
2245
2449
  y: Math.max(minY, Math.min(maxY, p.y))
2246
2450
  };
2247
2451
  }
2248
- const nearest = getNearestPointOnDieline({ x: p.x, y: p.y }, {
2249
- ...geometry,
2250
- features: []
2251
- });
2452
+ const nearest = getNearestPointOnDieline(
2453
+ { x: p.x, y: p.y },
2454
+ {
2455
+ ...geometry,
2456
+ features: []
2457
+ }
2458
+ );
2252
2459
  const dx = p.x - nearest.x;
2253
2460
  const dy = p.y - nearest.y;
2254
2461
  const dist = Math.sqrt(dx * dx + dy * dy);
@@ -2265,9 +2472,9 @@ var FeatureTool = class {
2265
2472
  var _a;
2266
2473
  if (!this.currentGeometry || !this.context) return;
2267
2474
  const index = (_a = target.data) == null ? void 0 : _a.index;
2268
- if (index === void 0 || index < 0 || index >= this.features.length)
2475
+ if (index === void 0 || index < 0 || index >= this.workingFeatures.length)
2269
2476
  return;
2270
- const feature = this.features[index];
2477
+ const feature = this.workingFeatures[index];
2271
2478
  const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
2272
2479
  const { width, height, x, y } = geometry;
2273
2480
  const left = x - width / 2;
@@ -2280,20 +2487,10 @@ var FeatureTool = class {
2280
2487
  y: normalizedY
2281
2488
  // Could also update rotation if we allowed rotating markers
2282
2489
  };
2283
- const newFeatures = [...this.features];
2490
+ const newFeatures = [...this.workingFeatures];
2284
2491
  newFeatures[index] = updatedFeature;
2285
- this.features = newFeatures;
2286
- const configService = this.context.services.get(
2287
- "ConfigurationService"
2288
- );
2289
- if (configService) {
2290
- this.isUpdatingConfig = true;
2291
- try {
2292
- configService.update("dieline.features", this.features);
2293
- } finally {
2294
- this.isUpdatingConfig = false;
2295
- }
2296
- }
2492
+ this.setWorkingFeatures(newFeatures);
2493
+ this.emitWorkingChange();
2297
2494
  }
2298
2495
  redraw() {
2299
2496
  if (!this.canvasService || !this.currentGeometry) return;
@@ -2304,7 +2501,7 @@ var FeatureTool = class {
2304
2501
  return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
2305
2502
  });
2306
2503
  existing.forEach((obj) => canvas.remove(obj));
2307
- if (!this.features || this.features.length === 0) {
2504
+ if (!this.workingFeatures || this.workingFeatures.length === 0) {
2308
2505
  this.canvasService.requestRenderAll();
2309
2506
  return;
2310
2507
  }
@@ -2312,7 +2509,7 @@ var FeatureTool = class {
2312
2509
  const finalScale = scale;
2313
2510
  const groups = {};
2314
2511
  const singles = [];
2315
- this.features.forEach((f, i) => {
2512
+ this.workingFeatures.forEach((f, i) => {
2316
2513
  if (f.groupId) {
2317
2514
  if (!groups[f.groupId]) groups[f.groupId] = [];
2318
2515
  groups[f.groupId].push({ feature: f, index: i });
@@ -2380,25 +2577,6 @@ var FeatureTool = class {
2380
2577
  lockScalingY: true,
2381
2578
  data: { type: "feature-marker", index, isGroup: false }
2382
2579
  });
2383
- marker.set("opacity", 0);
2384
- marker.on("mouseover", () => {
2385
- marker.set("opacity", 1);
2386
- canvas.requestRenderAll();
2387
- });
2388
- marker.on("mouseout", () => {
2389
- if (canvas.getActiveObject() !== marker) {
2390
- marker.set("opacity", 0);
2391
- canvas.requestRenderAll();
2392
- }
2393
- });
2394
- marker.on("selected", () => {
2395
- marker.set("opacity", 1);
2396
- canvas.requestRenderAll();
2397
- });
2398
- marker.on("deselected", () => {
2399
- marker.set("opacity", 0);
2400
- canvas.requestRenderAll();
2401
- });
2402
2580
  canvas.add(marker);
2403
2581
  canvas.bringObjectToFront(marker);
2404
2582
  });
@@ -2435,25 +2613,6 @@ var FeatureTool = class {
2435
2613
  indices: members.map((m) => m.index)
2436
2614
  }
2437
2615
  });
2438
- groupObj.set("opacity", 0);
2439
- groupObj.on("mouseover", () => {
2440
- groupObj.set("opacity", 1);
2441
- canvas.requestRenderAll();
2442
- });
2443
- groupObj.on("mouseout", () => {
2444
- if (canvas.getActiveObject() !== groupObj) {
2445
- groupObj.set("opacity", 0);
2446
- canvas.requestRenderAll();
2447
- }
2448
- });
2449
- groupObj.on("selected", () => {
2450
- groupObj.set("opacity", 1);
2451
- canvas.requestRenderAll();
2452
- });
2453
- groupObj.on("deselected", () => {
2454
- groupObj.set("opacity", 0);
2455
- canvas.requestRenderAll();
2456
- });
2457
2616
  canvas.add(groupObj);
2458
2617
  canvas.bringObjectToFront(groupObj);
2459
2618
  });
@@ -2472,12 +2631,12 @@ var FeatureTool = class {
2472
2631
  if ((_a = marker.data) == null ? void 0 : _a.isGroup) {
2473
2632
  const indices = (_b = marker.data) == null ? void 0 : _b.indices;
2474
2633
  if (indices && indices.length > 0) {
2475
- feature = this.features[indices[0]];
2634
+ feature = this.workingFeatures[indices[0]];
2476
2635
  }
2477
2636
  } else {
2478
2637
  const index = (_c = marker.data) == null ? void 0 : _c.index;
2479
2638
  if (index !== void 0) {
2480
- feature = this.features[index];
2639
+ feature = this.workingFeatures[index];
2481
2640
  }
2482
2641
  }
2483
2642
  const geometry = this.getGeometryForFeature(
@@ -3207,7 +3366,7 @@ var RulerTool = class {
3207
3366
  // Dieline context for sync
3208
3367
  this.dielineWidth = 500;
3209
3368
  this.dielineHeight = 500;
3210
- this.dielineUnit = "mm";
3369
+ this.dielineDisplayUnit = "mm";
3211
3370
  this.dielinePadding = 40;
3212
3371
  this.dielineOffset = 0;
3213
3372
  if (options) {
@@ -3231,7 +3390,10 @@ var RulerTool = class {
3231
3390
  this.textColor = configService.get("ruler.textColor", this.textColor);
3232
3391
  this.lineColor = configService.get("ruler.lineColor", this.lineColor);
3233
3392
  this.fontSize = configService.get("ruler.fontSize", this.fontSize);
3234
- this.dielineUnit = configService.get("dieline.unit", this.dielineUnit);
3393
+ this.dielineDisplayUnit = configService.get(
3394
+ "dieline.displayUnit",
3395
+ this.dielineDisplayUnit
3396
+ );
3235
3397
  this.dielineWidth = configService.get("dieline.width", this.dielineWidth);
3236
3398
  this.dielineHeight = configService.get(
3237
3399
  "dieline.height",
@@ -3254,7 +3416,8 @@ var RulerTool = class {
3254
3416
  shouldUpdate = true;
3255
3417
  }
3256
3418
  } else if (e.key.startsWith("dieline.")) {
3257
- if (e.key === "dieline.unit") this.dielineUnit = e.value;
3419
+ if (e.key === "dieline.displayUnit")
3420
+ this.dielineDisplayUnit = e.value;
3258
3421
  if (e.key === "dieline.width") this.dielineWidth = e.value;
3259
3422
  if (e.key === "dieline.height") this.dielineHeight = e.value;
3260
3423
  if (e.key === "dieline.padding") this.dielinePadding = e.value;
@@ -3441,26 +3604,27 @@ var RulerTool = class {
3441
3604
  const width = this.canvasService.canvas.width || 800;
3442
3605
  const height = this.canvasService.canvas.height || 600;
3443
3606
  const paddingPx = this.resolvePadding(width, height);
3444
- const layout = Coordinate.calculateLayout(
3445
- { width, height },
3446
- { width: this.dielineWidth, height: this.dielineHeight },
3447
- paddingPx
3607
+ this.canvasService.viewport.setPadding(paddingPx);
3608
+ this.canvasService.viewport.updatePhysical(
3609
+ this.dielineWidth,
3610
+ this.dielineHeight
3448
3611
  );
3612
+ const layout = this.canvasService.viewport.layout;
3449
3613
  const scale = layout.scale;
3450
3614
  const offsetX = layout.offsetX;
3451
3615
  const offsetY = layout.offsetY;
3452
3616
  const visualWidth = layout.width;
3453
3617
  const visualHeight = layout.height;
3454
- const rawOffset = this.dielineOffset || 0;
3455
- const effectiveOffset = rawOffset > 0 ? rawOffset : 0;
3456
- const expandPixels = effectiveOffset * scale;
3618
+ const rawOffsetMm = this.dielineOffset || 0;
3619
+ const effectiveOffsetMm = rawOffsetMm > 0 ? rawOffsetMm : 0;
3620
+ const expandPixels = effectiveOffsetMm * scale;
3457
3621
  const gap = this.gap || 15;
3458
3622
  const rulerLeft = offsetX - expandPixels;
3459
3623
  const rulerTop = offsetY - expandPixels;
3460
3624
  const rulerRight = offsetX + visualWidth + expandPixels;
3461
3625
  const rulerBottom = offsetY + visualHeight + expandPixels;
3462
- const displayWidth = this.dielineWidth + effectiveOffset * 2;
3463
- const displayHeight = this.dielineHeight + effectiveOffset * 2;
3626
+ const displayWidthMm = this.dielineWidth + effectiveOffsetMm * 2;
3627
+ const displayHeightMm = this.dielineHeight + effectiveOffsetMm * 2;
3464
3628
  const topRulerY = rulerTop - gap;
3465
3629
  const topRulerXStart = rulerLeft;
3466
3630
  const topRulerXEnd = rulerRight;
@@ -3503,8 +3667,8 @@ var RulerTool = class {
3503
3667
  }
3504
3668
  )
3505
3669
  );
3506
- const widthStr = parseFloat(displayWidth.toFixed(2)).toString();
3507
- const topTextContent = `${widthStr} ${this.dielineUnit}`;
3670
+ const widthStr = formatMm(displayWidthMm, this.dielineDisplayUnit);
3671
+ const topTextContent = `${widthStr} ${this.dielineDisplayUnit}`;
3508
3672
  const topText = new Text(topTextContent, {
3509
3673
  left: topRulerXStart + (rulerRight - rulerLeft) / 2,
3510
3674
  top: topRulerY,
@@ -3559,8 +3723,8 @@ var RulerTool = class {
3559
3723
  }
3560
3724
  )
3561
3725
  );
3562
- const heightStr = parseFloat(displayHeight.toFixed(2)).toString();
3563
- const leftTextContent = `${heightStr} ${this.dielineUnit}`;
3726
+ const heightStr = formatMm(displayHeightMm, this.dielineDisplayUnit);
3727
+ const leftTextContent = `${heightStr} ${this.dielineDisplayUnit}`;
3564
3728
  const leftText = new Text(leftTextContent, {
3565
3729
  left: leftRulerX,
3566
3730
  top: leftRulerYStart + (rulerBottom - rulerTop) / 2,
@@ -3674,6 +3838,81 @@ var MirrorTool = class {
3674
3838
 
3675
3839
  // src/CanvasService.ts
3676
3840
  import { Canvas, Group as Group3 } from "fabric";
3841
+
3842
+ // src/ViewportSystem.ts
3843
+ var ViewportSystem = class {
3844
+ constructor(containerSize = { width: 0, height: 0 }, physicalSize = { width: 0, height: 0 }, padding = 40) {
3845
+ this._containerSize = { width: 0, height: 0 };
3846
+ this._physicalSize = { width: 0, height: 0 };
3847
+ this._padding = 0;
3848
+ this._layout = {
3849
+ scale: 1,
3850
+ offsetX: 0,
3851
+ offsetY: 0,
3852
+ width: 0,
3853
+ height: 0
3854
+ };
3855
+ this._containerSize = containerSize;
3856
+ this._physicalSize = physicalSize;
3857
+ this._padding = padding;
3858
+ this.updateLayout();
3859
+ }
3860
+ get layout() {
3861
+ return this._layout;
3862
+ }
3863
+ get scale() {
3864
+ return this._layout.scale;
3865
+ }
3866
+ get offset() {
3867
+ return { x: this._layout.offsetX, y: this._layout.offsetY };
3868
+ }
3869
+ updateContainer(width, height) {
3870
+ if (this._containerSize.width === width && this._containerSize.height === height)
3871
+ return;
3872
+ this._containerSize = { width, height };
3873
+ this.updateLayout();
3874
+ }
3875
+ updatePhysical(width, height) {
3876
+ if (this._physicalSize.width === width && this._physicalSize.height === height)
3877
+ return;
3878
+ this._physicalSize = { width, height };
3879
+ this.updateLayout();
3880
+ }
3881
+ setPadding(padding) {
3882
+ if (this._padding === padding) return;
3883
+ this._padding = padding;
3884
+ this.updateLayout();
3885
+ }
3886
+ updateLayout() {
3887
+ this._layout = Coordinate.calculateLayout(
3888
+ this._containerSize,
3889
+ this._physicalSize,
3890
+ this._padding
3891
+ );
3892
+ }
3893
+ toPixel(value) {
3894
+ return value * this._layout.scale;
3895
+ }
3896
+ toPhysical(value) {
3897
+ return this._layout.scale === 0 ? 0 : value / this._layout.scale;
3898
+ }
3899
+ toPixelPoint(point) {
3900
+ return {
3901
+ x: point.x * this._layout.scale + this._layout.offsetX,
3902
+ y: point.y * this._layout.scale + this._layout.offsetY
3903
+ };
3904
+ }
3905
+ // Convert screen coordinate (e.g. mouse event) to physical coordinate (relative to content origin)
3906
+ toPhysicalPoint(point) {
3907
+ if (this._layout.scale === 0) return { x: 0, y: 0 };
3908
+ return {
3909
+ x: (point.x - this._layout.offsetX) / this._layout.scale,
3910
+ y: (point.y - this._layout.offsetY) / this._layout.scale
3911
+ };
3912
+ }
3913
+ };
3914
+
3915
+ // src/CanvasService.ts
3677
3916
  var CanvasService = class {
3678
3917
  constructor(el, options) {
3679
3918
  if (el instanceof Canvas) {
@@ -3684,6 +3923,10 @@ var CanvasService = class {
3684
3923
  ...options
3685
3924
  });
3686
3925
  }
3926
+ this.viewport = new ViewportSystem();
3927
+ if (this.canvas.width !== void 0 && this.canvas.height !== void 0) {
3928
+ this.viewport.updateContainer(this.canvas.width, this.canvas.height);
3929
+ }
3687
3930
  if (options == null ? void 0 : options.eventBus) {
3688
3931
  this.setEventBus(options.eventBus);
3689
3932
  }
@@ -3763,5 +4006,7 @@ export {
3763
4006
  ImageTool,
3764
4007
  MirrorTool,
3765
4008
  RulerTool,
3766
- WhiteInkTool
4009
+ WhiteInkTool,
4010
+ formatMm,
4011
+ parseLengthToMm
3767
4012
  };