@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.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) {
@@ -908,7 +931,7 @@ var DielineTool = class {
908
931
  name: "DielineTool"
909
932
  };
910
933
  this.state = {
911
- unit: "mm",
934
+ displayUnit: "mm",
912
935
  shape: "rect",
913
936
  width: 500,
914
937
  height: 500,
@@ -954,50 +977,88 @@ var DielineTool = class {
954
977
  const configService = context.services.get("ConfigurationService");
955
978
  if (configService) {
956
979
  const s = this.state;
957
- s.unit = configService.get("dieline.unit", s.unit);
980
+ s.displayUnit = configService.get("dieline.displayUnit", s.displayUnit);
958
981
  s.shape = configService.get("dieline.shape", s.shape);
959
- s.width = configService.get("dieline.width", s.width);
960
- s.height = configService.get("dieline.height", s.height);
961
- 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
+ );
962
994
  s.padding = configService.get("dieline.padding", s.padding);
963
- s.offset = configService.get("dieline.offset", s.offset);
964
- s.mainLine.width = configService.get("dieline.strokeWidth", s.mainLine.width);
965
- s.mainLine.color = configService.get("dieline.strokeColor", s.mainLine.color);
966
- 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
+ );
967
1011
  s.mainLine.style = configService.get("dieline.style", s.mainLine.style);
968
- s.offsetLine.width = configService.get("dieline.offsetStrokeWidth", s.offsetLine.width);
969
- s.offsetLine.color = configService.get("dieline.offsetStrokeColor", s.offsetLine.color);
970
- s.offsetLine.dashLength = configService.get("dieline.offsetDashLength", s.offsetLine.dashLength);
971
- 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
+ );
972
1028
  s.insideColor = configService.get("dieline.insideColor", s.insideColor);
973
- s.outsideColor = configService.get("dieline.outsideColor", s.outsideColor);
974
- 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
+ );
975
1037
  s.features = configService.get("dieline.features", s.features);
976
1038
  s.pathData = configService.get("dieline.pathData", s.pathData);
977
1039
  configService.onAnyChange((e) => {
978
1040
  if (e.key.startsWith("dieline.")) {
979
- console.log(`[DielineTool] Config change detected: ${e.key} -> ${e.value}`);
980
1041
  switch (e.key) {
981
- case "dieline.unit":
982
- s.unit = e.value;
1042
+ case "dieline.displayUnit":
1043
+ s.displayUnit = e.value;
983
1044
  break;
984
1045
  case "dieline.shape":
985
1046
  s.shape = e.value;
986
1047
  break;
987
1048
  case "dieline.width":
988
- s.width = e.value;
1049
+ s.width = parseLengthToMm(e.value, "mm");
989
1050
  break;
990
1051
  case "dieline.height":
991
- s.height = e.value;
1052
+ s.height = parseLengthToMm(e.value, "mm");
992
1053
  break;
993
1054
  case "dieline.radius":
994
- s.radius = e.value;
1055
+ s.radius = parseLengthToMm(e.value, "mm");
995
1056
  break;
996
1057
  case "dieline.padding":
997
1058
  s.padding = e.value;
998
1059
  break;
999
1060
  case "dieline.offset":
1000
- s.offset = e.value;
1061
+ s.offset = parseLengthToMm(e.value, "mm");
1001
1062
  break;
1002
1063
  case "dieline.strokeWidth":
1003
1064
  s.mainLine.width = e.value;
@@ -1056,11 +1117,11 @@ var DielineTool = class {
1056
1117
  return {
1057
1118
  [ContributionPointIds2.CONFIGURATIONS]: [
1058
1119
  {
1059
- id: "dieline.unit",
1120
+ id: "dieline.displayUnit",
1060
1121
  type: "select",
1061
- label: "Unit",
1062
- options: ["px", "mm", "cm", "in"],
1063
- default: s.unit
1122
+ label: "Display Unit",
1123
+ options: ["mm", "cm", "in"],
1124
+ default: s.displayUnit
1064
1125
  },
1065
1126
  {
1066
1127
  id: "dieline.shape",
@@ -1072,7 +1133,7 @@ var DielineTool = class {
1072
1133
  {
1073
1134
  id: "dieline.width",
1074
1135
  type: "number",
1075
- label: "Width",
1136
+ label: "Width (mm)",
1076
1137
  min: 10,
1077
1138
  max: 2e3,
1078
1139
  default: s.width
@@ -1080,7 +1141,7 @@ var DielineTool = class {
1080
1141
  {
1081
1142
  id: "dieline.height",
1082
1143
  type: "number",
1083
- label: "Height",
1144
+ label: "Height (mm)",
1084
1145
  min: 10,
1085
1146
  max: 2e3,
1086
1147
  default: s.height
@@ -1088,7 +1149,7 @@ var DielineTool = class {
1088
1149
  {
1089
1150
  id: "dieline.radius",
1090
1151
  type: "number",
1091
- label: "Corner Radius",
1152
+ label: "Corner Radius (mm)",
1092
1153
  min: 0,
1093
1154
  max: 500,
1094
1155
  default: s.radius
@@ -1103,7 +1164,7 @@ var DielineTool = class {
1103
1164
  {
1104
1165
  id: "dieline.offset",
1105
1166
  type: "number",
1106
- label: "Bleed Offset",
1167
+ label: "Bleed Offset (mm)",
1107
1168
  min: -100,
1108
1169
  max: 100,
1109
1170
  default: s.offset
@@ -1194,6 +1255,31 @@ var DielineTool = class {
1194
1255
  }
1195
1256
  ],
1196
1257
  [ContributionPointIds2.COMMANDS]: [
1258
+ {
1259
+ command: "updateFeaturePosition",
1260
+ title: "Update Feature Position",
1261
+ handler: (groupId, x, y) => {
1262
+ var _a;
1263
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1264
+ "ConfigurationService"
1265
+ );
1266
+ if (!configService) return;
1267
+ const features = configService.get("dieline.features") || [];
1268
+ let changed = false;
1269
+ const newFeatures = features.map((f) => {
1270
+ if (f.groupId === groupId) {
1271
+ if (f.x !== x || f.y !== y) {
1272
+ changed = true;
1273
+ return { ...f, x, y };
1274
+ }
1275
+ }
1276
+ return f;
1277
+ });
1278
+ if (changed) {
1279
+ configService.update("dieline.features", newFeatures);
1280
+ }
1281
+ }
1282
+ },
1197
1283
  {
1198
1284
  command: "getGeometry",
1199
1285
  title: "Get Geometry",
@@ -1299,7 +1385,7 @@ var DielineTool = class {
1299
1385
  const layer = this.getLayer();
1300
1386
  if (!layer) return;
1301
1387
  const {
1302
- unit,
1388
+ displayUnit,
1303
1389
  shape,
1304
1390
  radius,
1305
1391
  offset,
@@ -1310,15 +1396,13 @@ var DielineTool = class {
1310
1396
  showBleedLines,
1311
1397
  features
1312
1398
  } = this.state;
1313
- let { width, height } = this.state;
1399
+ const { width, height } = this.state;
1314
1400
  const canvasW = this.canvasService.canvas.width || 800;
1315
1401
  const canvasH = this.canvasService.canvas.height || 600;
1316
1402
  const paddingPx = this.resolvePadding(canvasW, canvasH);
1317
- const layout = Coordinate.calculateLayout(
1318
- { width: canvasW, height: canvasH },
1319
- { width, height },
1320
- paddingPx
1321
- );
1403
+ this.canvasService.viewport.setPadding(paddingPx);
1404
+ this.canvasService.viewport.updatePhysical(width, height);
1405
+ const layout = this.canvasService.viewport.layout;
1322
1406
  const scale = layout.scale;
1323
1407
  const cx = layout.offsetX + layout.width / 2;
1324
1408
  const cy = layout.offsetY + layout.height / 2;
@@ -1505,15 +1589,22 @@ var DielineTool = class {
1505
1589
  }
1506
1590
  getGeometry() {
1507
1591
  if (!this.canvasService) return null;
1508
- 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;
1509
1602
  const canvasW = this.canvasService.canvas.width || 800;
1510
1603
  const canvasH = this.canvasService.canvas.height || 600;
1511
1604
  const paddingPx = this.resolvePadding(canvasW, canvasH);
1512
- const layout = Coordinate.calculateLayout(
1513
- { width: canvasW, height: canvasH },
1514
- { width, height },
1515
- paddingPx
1516
- );
1605
+ this.canvasService.viewport.setPadding(paddingPx);
1606
+ this.canvasService.viewport.updatePhysical(width, height);
1607
+ const layout = this.canvasService.viewport.layout;
1517
1608
  const scale = layout.scale;
1518
1609
  const cx = layout.offsetX + layout.width / 2;
1519
1610
  const cy = layout.offsetY + layout.height / 2;
@@ -1521,14 +1612,14 @@ var DielineTool = class {
1521
1612
  const visualHeight = layout.height;
1522
1613
  return {
1523
1614
  shape,
1524
- unit,
1615
+ unit: "mm",
1616
+ displayUnit,
1525
1617
  x: cx,
1526
1618
  y: cy,
1527
1619
  width: visualWidth,
1528
1620
  height: visualHeight,
1529
1621
  radius: radius * scale,
1530
1622
  offset: offset * scale,
1531
- // Pass scale to help other tools (like FeatureTool) convert units
1532
1623
  scale,
1533
1624
  strokeWidth: mainLine.width,
1534
1625
  pathData
@@ -1538,15 +1629,13 @@ var DielineTool = class {
1538
1629
  if (!this.canvasService) return null;
1539
1630
  const userLayer = this.canvasService.getLayer("user");
1540
1631
  if (!userLayer) return null;
1541
- const { shape, width, height, radius, features, unit, pathData } = this.state;
1632
+ const { shape, width, height, radius, features, pathData } = this.state;
1542
1633
  const canvasW = this.canvasService.canvas.width || 800;
1543
1634
  const canvasH = this.canvasService.canvas.height || 600;
1544
1635
  const paddingPx = this.resolvePadding(canvasW, canvasH);
1545
- const layout = Coordinate.calculateLayout(
1546
- { width: canvasW, height: canvasH },
1547
- { width, height },
1548
- paddingPx
1549
- );
1636
+ this.canvasService.viewport.setPadding(paddingPx);
1637
+ this.canvasService.viewport.updatePhysical(width, height);
1638
+ const layout = this.canvasService.viewport.layout;
1550
1639
  const scale = layout.scale;
1551
1640
  const cx = layout.offsetX + layout.width / 2;
1552
1641
  const cy = layout.offsetY + layout.height / 2;
@@ -1765,13 +1854,158 @@ import {
1765
1854
  ContributionPointIds as ContributionPointIds4
1766
1855
  } from "@pooder/core";
1767
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
1768
2002
  var FeatureTool = class {
1769
2003
  constructor(options) {
1770
2004
  this.id = "pooder.kit.feature";
1771
2005
  this.metadata = {
1772
2006
  name: "FeatureTool"
1773
2007
  };
1774
- this.features = [];
2008
+ this.workingFeatures = [];
1775
2009
  this.isUpdatingConfig = false;
1776
2010
  this.isToolActive = false;
1777
2011
  this.handleMoving = null;
@@ -1797,12 +2031,15 @@ var FeatureTool = class {
1797
2031
  "ConfigurationService"
1798
2032
  );
1799
2033
  if (configService) {
1800
- this.features = configService.get("dieline.features", []);
2034
+ const features = configService.get("dieline.features", []) || [];
2035
+ this.workingFeatures = this.cloneFeatures(features);
1801
2036
  configService.onAnyChange((e) => {
1802
2037
  if (this.isUpdatingConfig) return;
1803
2038
  if (e.key === "dieline.features") {
1804
- this.features = e.value || [];
2039
+ const next = e.value || [];
2040
+ this.workingFeatures = this.cloneFeatures(next);
1805
2041
  this.redraw();
2042
+ this.emitWorkingChange();
1806
2043
  }
1807
2044
  });
1808
2045
  }
@@ -1860,27 +2097,151 @@ var FeatureTool = class {
1860
2097
  command: "clearFeatures",
1861
2098
  title: "Clear Features",
1862
2099
  handler: () => {
1863
- var _a;
1864
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1865
- "ConfigurationService"
1866
- );
1867
- if (configService) {
1868
- configService.update("dieline.features", []);
1869
- }
2100
+ this.setWorkingFeatures([]);
2101
+ this.redraw();
2102
+ this.emitWorkingChange();
1870
2103
  return true;
1871
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
+ }
1872
2137
  }
1873
2138
  ]
1874
2139
  };
1875
2140
  }
1876
- addFeature(type) {
2141
+ cloneFeatures(features) {
2142
+ return JSON.parse(JSON.stringify(features || []));
2143
+ }
2144
+ emitWorkingChange() {
1877
2145
  var _a;
1878
- if (!this.canvasService) return false;
1879
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1880
- "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"
1881
2216
  );
1882
- const unit = (configService == null ? void 0 : configService.get("dieline.unit", "mm")) || "mm";
1883
- const defaultSize = Coordinate.convertUnit(10, "mm", unit);
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
+ }
2234
+ );
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;
1884
2245
  const newFeature = {
1885
2246
  id: Date.now().toString(),
1886
2247
  operation: type,
@@ -1889,28 +2250,17 @@ var FeatureTool = class {
1889
2250
  x: 0.5,
1890
2251
  y: 0,
1891
2252
  // Top edge
1892
- width: defaultSize,
1893
- height: defaultSize,
2253
+ width: 10,
2254
+ height: 10,
1894
2255
  rotation: 0
1895
2256
  };
1896
- if (configService) {
1897
- const current = configService.get(
1898
- "dieline.features",
1899
- []
1900
- );
1901
- configService.update("dieline.features", [...current, newFeature]);
1902
- }
2257
+ this.setWorkingFeatures([...this.workingFeatures || [], newFeature]);
2258
+ this.redraw();
2259
+ this.emitWorkingChange();
1903
2260
  return true;
1904
2261
  }
1905
2262
  addDoubleLayerHole() {
1906
- var _a;
1907
2263
  if (!this.canvasService) return false;
1908
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1909
- "ConfigurationService"
1910
- );
1911
- const unit = (configService == null ? void 0 : configService.get("dieline.unit", "mm")) || "mm";
1912
- const lugRadius = Coordinate.convertUnit(20, "mm", unit);
1913
- const holeRadius = Coordinate.convertUnit(15, "mm", unit);
1914
2264
  const groupId = Date.now().toString();
1915
2265
  const timestamp = Date.now();
1916
2266
  const lug = {
@@ -1921,8 +2271,7 @@ var FeatureTool = class {
1921
2271
  placement: "edge",
1922
2272
  x: 0.5,
1923
2273
  y: 0,
1924
- radius: lugRadius,
1925
- // 20mm
2274
+ radius: 20,
1926
2275
  rotation: 0
1927
2276
  };
1928
2277
  const hole = {
@@ -1933,17 +2282,12 @@ var FeatureTool = class {
1933
2282
  placement: "edge",
1934
2283
  x: 0.5,
1935
2284
  y: 0,
1936
- radius: holeRadius,
1937
- // 15mm
2285
+ radius: 15,
1938
2286
  rotation: 0
1939
2287
  };
1940
- if (configService) {
1941
- const current = configService.get(
1942
- "dieline.features",
1943
- []
1944
- );
1945
- configService.update("dieline.features", [...current, lug, hole]);
1946
- }
2288
+ this.setWorkingFeatures([...this.workingFeatures || [], lug, hole]);
2289
+ this.redraw();
2290
+ this.emitWorkingChange();
1947
2291
  return true;
1948
2292
  }
1949
2293
  getGeometryForFeature(geometry, feature) {
@@ -1987,12 +2331,12 @@ var FeatureTool = class {
1987
2331
  if ((_b = target.data) == null ? void 0 : _b.isGroup) {
1988
2332
  const indices = (_c = target.data) == null ? void 0 : _c.indices;
1989
2333
  if (indices && indices.length > 0) {
1990
- feature = this.features[indices[0]];
2334
+ feature = this.workingFeatures[indices[0]];
1991
2335
  }
1992
2336
  } else {
1993
2337
  const index = (_d = target.data) == null ? void 0 : _d.index;
1994
2338
  if (index !== void 0) {
1995
- feature = this.features[index];
2339
+ feature = this.workingFeatures[index];
1996
2340
  }
1997
2341
  }
1998
2342
  const geometry = this.getGeometryForFeature(
@@ -2013,7 +2357,7 @@ var FeatureTool = class {
2013
2357
  }
2014
2358
  if (!this.handleModified) {
2015
2359
  this.handleModified = (e) => {
2016
- var _a, _b, _c, _d;
2360
+ var _a, _b, _c;
2017
2361
  const target = e.target;
2018
2362
  if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return;
2019
2363
  if ((_b = target.data) == null ? void 0 : _b.isGroup) {
@@ -2021,11 +2365,11 @@ var FeatureTool = class {
2021
2365
  const indices = (_c = groupObj.data) == null ? void 0 : _c.indices;
2022
2366
  if (!indices) return;
2023
2367
  const groupCenter = new Point(groupObj.left, groupObj.top);
2024
- const newFeatures = [...this.features];
2368
+ const newFeatures = [...this.workingFeatures];
2025
2369
  const { x, y } = this.currentGeometry;
2026
2370
  groupObj.getObjects().forEach((child, i) => {
2027
2371
  const originalIndex = indices[i];
2028
- const feature = this.features[originalIndex];
2372
+ const feature = this.workingFeatures[originalIndex];
2029
2373
  const geometry = this.getGeometryForFeature(
2030
2374
  this.currentGeometry,
2031
2375
  feature
@@ -2043,18 +2387,8 @@ var FeatureTool = class {
2043
2387
  y: normalizedY
2044
2388
  };
2045
2389
  });
2046
- this.features = newFeatures;
2047
- const configService = (_d = this.context) == null ? void 0 : _d.services.get(
2048
- "ConfigurationService"
2049
- );
2050
- if (configService) {
2051
- this.isUpdatingConfig = true;
2052
- try {
2053
- configService.update("dieline.features", this.features);
2054
- } finally {
2055
- this.isUpdatingConfig = false;
2056
- }
2057
- }
2390
+ this.setWorkingFeatures(newFeatures);
2391
+ this.emitWorkingChange();
2058
2392
  } else {
2059
2393
  this.syncFeatureFromCanvas(target);
2060
2394
  }
@@ -2088,6 +2422,23 @@ var FeatureTool = class {
2088
2422
  this.canvasService.requestRenderAll();
2089
2423
  }
2090
2424
  constrainPosition(p, geometry, limit, feature) {
2425
+ if (feature && feature.constraints) {
2426
+ const minX = geometry.x - geometry.width / 2;
2427
+ const minY = geometry.y - geometry.height / 2;
2428
+ const nx = geometry.width > 0 ? (p.x - minX) / geometry.width : 0.5;
2429
+ const ny = geometry.height > 0 ? (p.y - minY) / geometry.height : 0.5;
2430
+ const scale2 = geometry.scale || 1;
2431
+ const dielineWidth = geometry.width / scale2;
2432
+ const dielineHeight = geometry.height / scale2;
2433
+ const constrained = ConstraintRegistry.apply(nx, ny, feature, {
2434
+ dielineWidth,
2435
+ dielineHeight
2436
+ });
2437
+ return {
2438
+ x: minX + constrained.x * geometry.width,
2439
+ y: minY + constrained.y * geometry.height
2440
+ };
2441
+ }
2091
2442
  if (feature && feature.placement === "internal") {
2092
2443
  const minX = geometry.x - geometry.width / 2;
2093
2444
  const maxX = geometry.x + geometry.width / 2;
@@ -2098,10 +2449,13 @@ var FeatureTool = class {
2098
2449
  y: Math.max(minY, Math.min(maxY, p.y))
2099
2450
  };
2100
2451
  }
2101
- const nearest = getNearestPointOnDieline({ x: p.x, y: p.y }, {
2102
- ...geometry,
2103
- features: []
2104
- });
2452
+ const nearest = getNearestPointOnDieline(
2453
+ { x: p.x, y: p.y },
2454
+ {
2455
+ ...geometry,
2456
+ features: []
2457
+ }
2458
+ );
2105
2459
  const dx = p.x - nearest.x;
2106
2460
  const dy = p.y - nearest.y;
2107
2461
  const dist = Math.sqrt(dx * dx + dy * dy);
@@ -2118,9 +2472,9 @@ var FeatureTool = class {
2118
2472
  var _a;
2119
2473
  if (!this.currentGeometry || !this.context) return;
2120
2474
  const index = (_a = target.data) == null ? void 0 : _a.index;
2121
- if (index === void 0 || index < 0 || index >= this.features.length)
2475
+ if (index === void 0 || index < 0 || index >= this.workingFeatures.length)
2122
2476
  return;
2123
- const feature = this.features[index];
2477
+ const feature = this.workingFeatures[index];
2124
2478
  const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
2125
2479
  const { width, height, x, y } = geometry;
2126
2480
  const left = x - width / 2;
@@ -2133,20 +2487,10 @@ var FeatureTool = class {
2133
2487
  y: normalizedY
2134
2488
  // Could also update rotation if we allowed rotating markers
2135
2489
  };
2136
- const newFeatures = [...this.features];
2490
+ const newFeatures = [...this.workingFeatures];
2137
2491
  newFeatures[index] = updatedFeature;
2138
- this.features = newFeatures;
2139
- const configService = this.context.services.get(
2140
- "ConfigurationService"
2141
- );
2142
- if (configService) {
2143
- this.isUpdatingConfig = true;
2144
- try {
2145
- configService.update("dieline.features", this.features);
2146
- } finally {
2147
- this.isUpdatingConfig = false;
2148
- }
2149
- }
2492
+ this.setWorkingFeatures(newFeatures);
2493
+ this.emitWorkingChange();
2150
2494
  }
2151
2495
  redraw() {
2152
2496
  if (!this.canvasService || !this.currentGeometry) return;
@@ -2157,7 +2501,7 @@ var FeatureTool = class {
2157
2501
  return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
2158
2502
  });
2159
2503
  existing.forEach((obj) => canvas.remove(obj));
2160
- if (!this.features || this.features.length === 0) {
2504
+ if (!this.workingFeatures || this.workingFeatures.length === 0) {
2161
2505
  this.canvasService.requestRenderAll();
2162
2506
  return;
2163
2507
  }
@@ -2165,7 +2509,7 @@ var FeatureTool = class {
2165
2509
  const finalScale = scale;
2166
2510
  const groups = {};
2167
2511
  const singles = [];
2168
- this.features.forEach((f, i) => {
2512
+ this.workingFeatures.forEach((f, i) => {
2169
2513
  if (f.groupId) {
2170
2514
  if (!groups[f.groupId]) groups[f.groupId] = [];
2171
2515
  groups[f.groupId].push({ feature: f, index: i });
@@ -2233,25 +2577,6 @@ var FeatureTool = class {
2233
2577
  lockScalingY: true,
2234
2578
  data: { type: "feature-marker", index, isGroup: false }
2235
2579
  });
2236
- marker.set("opacity", 0);
2237
- marker.on("mouseover", () => {
2238
- marker.set("opacity", 1);
2239
- canvas.requestRenderAll();
2240
- });
2241
- marker.on("mouseout", () => {
2242
- if (canvas.getActiveObject() !== marker) {
2243
- marker.set("opacity", 0);
2244
- canvas.requestRenderAll();
2245
- }
2246
- });
2247
- marker.on("selected", () => {
2248
- marker.set("opacity", 1);
2249
- canvas.requestRenderAll();
2250
- });
2251
- marker.on("deselected", () => {
2252
- marker.set("opacity", 0);
2253
- canvas.requestRenderAll();
2254
- });
2255
2580
  canvas.add(marker);
2256
2581
  canvas.bringObjectToFront(marker);
2257
2582
  });
@@ -2288,25 +2613,6 @@ var FeatureTool = class {
2288
2613
  indices: members.map((m) => m.index)
2289
2614
  }
2290
2615
  });
2291
- groupObj.set("opacity", 0);
2292
- groupObj.on("mouseover", () => {
2293
- groupObj.set("opacity", 1);
2294
- canvas.requestRenderAll();
2295
- });
2296
- groupObj.on("mouseout", () => {
2297
- if (canvas.getActiveObject() !== groupObj) {
2298
- groupObj.set("opacity", 0);
2299
- canvas.requestRenderAll();
2300
- }
2301
- });
2302
- groupObj.on("selected", () => {
2303
- groupObj.set("opacity", 1);
2304
- canvas.requestRenderAll();
2305
- });
2306
- groupObj.on("deselected", () => {
2307
- groupObj.set("opacity", 0);
2308
- canvas.requestRenderAll();
2309
- });
2310
2616
  canvas.add(groupObj);
2311
2617
  canvas.bringObjectToFront(groupObj);
2312
2618
  });
@@ -2325,12 +2631,12 @@ var FeatureTool = class {
2325
2631
  if ((_a = marker.data) == null ? void 0 : _a.isGroup) {
2326
2632
  const indices = (_b = marker.data) == null ? void 0 : _b.indices;
2327
2633
  if (indices && indices.length > 0) {
2328
- feature = this.features[indices[0]];
2634
+ feature = this.workingFeatures[indices[0]];
2329
2635
  }
2330
2636
  } else {
2331
2637
  const index = (_c = marker.data) == null ? void 0 : _c.index;
2332
2638
  if (index !== void 0) {
2333
- feature = this.features[index];
2639
+ feature = this.workingFeatures[index];
2334
2640
  }
2335
2641
  }
2336
2642
  const geometry = this.getGeometryForFeature(
@@ -3060,7 +3366,7 @@ var RulerTool = class {
3060
3366
  // Dieline context for sync
3061
3367
  this.dielineWidth = 500;
3062
3368
  this.dielineHeight = 500;
3063
- this.dielineUnit = "mm";
3369
+ this.dielineDisplayUnit = "mm";
3064
3370
  this.dielinePadding = 40;
3065
3371
  this.dielineOffset = 0;
3066
3372
  if (options) {
@@ -3084,7 +3390,10 @@ var RulerTool = class {
3084
3390
  this.textColor = configService.get("ruler.textColor", this.textColor);
3085
3391
  this.lineColor = configService.get("ruler.lineColor", this.lineColor);
3086
3392
  this.fontSize = configService.get("ruler.fontSize", this.fontSize);
3087
- this.dielineUnit = configService.get("dieline.unit", this.dielineUnit);
3393
+ this.dielineDisplayUnit = configService.get(
3394
+ "dieline.displayUnit",
3395
+ this.dielineDisplayUnit
3396
+ );
3088
3397
  this.dielineWidth = configService.get("dieline.width", this.dielineWidth);
3089
3398
  this.dielineHeight = configService.get(
3090
3399
  "dieline.height",
@@ -3107,7 +3416,8 @@ var RulerTool = class {
3107
3416
  shouldUpdate = true;
3108
3417
  }
3109
3418
  } else if (e.key.startsWith("dieline.")) {
3110
- if (e.key === "dieline.unit") this.dielineUnit = e.value;
3419
+ if (e.key === "dieline.displayUnit")
3420
+ this.dielineDisplayUnit = e.value;
3111
3421
  if (e.key === "dieline.width") this.dielineWidth = e.value;
3112
3422
  if (e.key === "dieline.height") this.dielineHeight = e.value;
3113
3423
  if (e.key === "dieline.padding") this.dielinePadding = e.value;
@@ -3294,26 +3604,27 @@ var RulerTool = class {
3294
3604
  const width = this.canvasService.canvas.width || 800;
3295
3605
  const height = this.canvasService.canvas.height || 600;
3296
3606
  const paddingPx = this.resolvePadding(width, height);
3297
- const layout = Coordinate.calculateLayout(
3298
- { width, height },
3299
- { width: this.dielineWidth, height: this.dielineHeight },
3300
- paddingPx
3607
+ this.canvasService.viewport.setPadding(paddingPx);
3608
+ this.canvasService.viewport.updatePhysical(
3609
+ this.dielineWidth,
3610
+ this.dielineHeight
3301
3611
  );
3612
+ const layout = this.canvasService.viewport.layout;
3302
3613
  const scale = layout.scale;
3303
3614
  const offsetX = layout.offsetX;
3304
3615
  const offsetY = layout.offsetY;
3305
3616
  const visualWidth = layout.width;
3306
3617
  const visualHeight = layout.height;
3307
- const rawOffset = this.dielineOffset || 0;
3308
- const effectiveOffset = rawOffset > 0 ? rawOffset : 0;
3309
- const expandPixels = effectiveOffset * scale;
3618
+ const rawOffsetMm = this.dielineOffset || 0;
3619
+ const effectiveOffsetMm = rawOffsetMm > 0 ? rawOffsetMm : 0;
3620
+ const expandPixels = effectiveOffsetMm * scale;
3310
3621
  const gap = this.gap || 15;
3311
3622
  const rulerLeft = offsetX - expandPixels;
3312
3623
  const rulerTop = offsetY - expandPixels;
3313
3624
  const rulerRight = offsetX + visualWidth + expandPixels;
3314
3625
  const rulerBottom = offsetY + visualHeight + expandPixels;
3315
- const displayWidth = this.dielineWidth + effectiveOffset * 2;
3316
- const displayHeight = this.dielineHeight + effectiveOffset * 2;
3626
+ const displayWidthMm = this.dielineWidth + effectiveOffsetMm * 2;
3627
+ const displayHeightMm = this.dielineHeight + effectiveOffsetMm * 2;
3317
3628
  const topRulerY = rulerTop - gap;
3318
3629
  const topRulerXStart = rulerLeft;
3319
3630
  const topRulerXEnd = rulerRight;
@@ -3356,8 +3667,8 @@ var RulerTool = class {
3356
3667
  }
3357
3668
  )
3358
3669
  );
3359
- const widthStr = parseFloat(displayWidth.toFixed(2)).toString();
3360
- const topTextContent = `${widthStr} ${this.dielineUnit}`;
3670
+ const widthStr = formatMm(displayWidthMm, this.dielineDisplayUnit);
3671
+ const topTextContent = `${widthStr} ${this.dielineDisplayUnit}`;
3361
3672
  const topText = new Text(topTextContent, {
3362
3673
  left: topRulerXStart + (rulerRight - rulerLeft) / 2,
3363
3674
  top: topRulerY,
@@ -3412,8 +3723,8 @@ var RulerTool = class {
3412
3723
  }
3413
3724
  )
3414
3725
  );
3415
- const heightStr = parseFloat(displayHeight.toFixed(2)).toString();
3416
- const leftTextContent = `${heightStr} ${this.dielineUnit}`;
3726
+ const heightStr = formatMm(displayHeightMm, this.dielineDisplayUnit);
3727
+ const leftTextContent = `${heightStr} ${this.dielineDisplayUnit}`;
3417
3728
  const leftText = new Text(leftTextContent, {
3418
3729
  left: leftRulerX,
3419
3730
  top: leftRulerYStart + (rulerBottom - rulerTop) / 2,
@@ -3527,6 +3838,81 @@ var MirrorTool = class {
3527
3838
 
3528
3839
  // src/CanvasService.ts
3529
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
3530
3916
  var CanvasService = class {
3531
3917
  constructor(el, options) {
3532
3918
  if (el instanceof Canvas) {
@@ -3537,6 +3923,10 @@ var CanvasService = class {
3537
3923
  ...options
3538
3924
  });
3539
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
+ }
3540
3930
  if (options == null ? void 0 : options.eventBus) {
3541
3931
  this.setEventBus(options.eventBus);
3542
3932
  }
@@ -3616,5 +4006,7 @@ export {
3616
4006
  ImageTool,
3617
4007
  MirrorTool,
3618
4008
  RulerTool,
3619
- WhiteInkTool
4009
+ WhiteInkTool,
4010
+ formatMm,
4011
+ parseLengthToMm
3620
4012
  };