@pooder/kit 4.1.0 → 4.3.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) {
@@ -758,7 +781,7 @@ function getPerimeterShape(options) {
758
781
  const { features } = options;
759
782
  if (features && features.length > 0) {
760
783
  const edgeFeatures = features.filter(
761
- (f) => !f.placement || f.placement === "edge"
784
+ (f) => !f.renderBehavior || f.renderBehavior === "edge"
762
785
  );
763
786
  const adds = [];
764
787
  const subtracts = [];
@@ -766,10 +789,78 @@ function getPerimeterShape(options) {
766
789
  const pos = resolveFeaturePosition(f, options);
767
790
  const center = new paper2.Point(pos.x, pos.y);
768
791
  const item = createFeatureItem(f, center);
769
- if (f.operation === "add") {
770
- adds.push(item);
792
+ if (f.bridge && f.bridge.type === "vertical") {
793
+ const itemBounds = item.bounds;
794
+ const mainBounds = mainShape.bounds;
795
+ const bridgeTop = mainBounds.top;
796
+ const bridgeBottom = itemBounds.top;
797
+ if (bridgeBottom > bridgeTop) {
798
+ const startY = bridgeBottom + 1;
799
+ const bridgeRect = new paper2.Path.Rectangle({
800
+ from: [itemBounds.left, bridgeTop],
801
+ to: [itemBounds.right, startY],
802
+ insert: false
803
+ });
804
+ const gaps = bridgeRect.subtract(mainShape);
805
+ bridgeRect.remove();
806
+ let bridgePart = null;
807
+ const isBottomPart = (part) => {
808
+ return Math.abs(part.bounds.bottom - startY) < 2;
809
+ };
810
+ if (gaps instanceof paper2.CompoundPath) {
811
+ const children = gaps.children;
812
+ let maxBottom = -Infinity;
813
+ let bestChild = null;
814
+ for (const child of children) {
815
+ if (child.bounds.bottom > maxBottom) {
816
+ maxBottom = child.bounds.bottom;
817
+ bestChild = child;
818
+ }
819
+ }
820
+ if (bestChild && isBottomPart(bestChild)) {
821
+ bridgePart = bestChild.clone();
822
+ }
823
+ } else if (gaps instanceof paper2.Path) {
824
+ if (isBottomPart(gaps)) {
825
+ bridgePart = gaps.clone();
826
+ }
827
+ }
828
+ gaps.remove();
829
+ if (bridgePart) {
830
+ const bounds = bridgePart.bounds;
831
+ if (bounds.height > 0) {
832
+ const overlap = 1;
833
+ const scaleY = (bounds.height + overlap) / bounds.height;
834
+ bridgePart.scale(1, scaleY, new paper2.Point(bounds.center.x, bounds.bottom));
835
+ }
836
+ const unitedItem = item.unite(bridgePart);
837
+ item.remove();
838
+ bridgePart.remove();
839
+ if (f.operation === "add") {
840
+ adds.push(unitedItem);
841
+ } else {
842
+ subtracts.push(unitedItem);
843
+ }
844
+ } else {
845
+ if (f.operation === "add") {
846
+ adds.push(item);
847
+ } else {
848
+ subtracts.push(item);
849
+ }
850
+ }
851
+ } else {
852
+ if (f.operation === "add") {
853
+ adds.push(item);
854
+ } else {
855
+ subtracts.push(item);
856
+ }
857
+ }
771
858
  } else {
772
- subtracts.push(item);
859
+ if (f.operation === "add") {
860
+ adds.push(item);
861
+ } else {
862
+ subtracts.push(item);
863
+ }
773
864
  }
774
865
  });
775
866
  if (adds.length > 0) {
@@ -802,10 +893,12 @@ function getPerimeterShape(options) {
802
893
  return mainShape;
803
894
  }
804
895
  function applySurfaceFeatures(shape, features, options) {
805
- const internalFeatures = features.filter((f) => f.placement === "internal");
806
- if (internalFeatures.length === 0) return shape;
896
+ const surfaceFeatures = features.filter(
897
+ (f) => f.renderBehavior === "surface"
898
+ );
899
+ if (surfaceFeatures.length === 0) return shape;
807
900
  let result = shape;
808
- for (const f of internalFeatures) {
901
+ for (const f of surfaceFeatures) {
809
902
  const pos = resolveFeaturePosition(f, options);
810
903
  const center = new paper2.Point(pos.x, pos.y);
811
904
  const item = createFeatureItem(f, center);
@@ -862,9 +955,17 @@ function generateBleedZonePath(originalOptions, offsetOptions, offset) {
862
955
  ensurePaper(paperWidth, paperHeight);
863
956
  paper2.project.activeLayer.removeChildren();
864
957
  const pOriginal = getPerimeterShape(originalOptions);
865
- const shapeOriginal = applySurfaceFeatures(pOriginal, originalOptions.features, originalOptions);
958
+ const shapeOriginal = applySurfaceFeatures(
959
+ pOriginal,
960
+ originalOptions.features,
961
+ originalOptions
962
+ );
866
963
  const pOffset = getPerimeterShape(offsetOptions);
867
- const shapeOffset = applySurfaceFeatures(pOffset, offsetOptions.features, offsetOptions);
964
+ const shapeOffset = applySurfaceFeatures(
965
+ pOffset,
966
+ offsetOptions.features,
967
+ offsetOptions
968
+ );
868
969
  let bleedZone;
869
970
  if (offset > 0) {
870
971
  bleedZone = shapeOffset.subtract(shapeOriginal);
@@ -877,13 +978,29 @@ function generateBleedZonePath(originalOptions, offsetOptions, offset) {
877
978
  bleedZone.remove();
878
979
  return pathData;
879
980
  }
981
+ function getLowestPointOnDieline(options) {
982
+ ensurePaper(options.width * 2, options.height * 2);
983
+ paper2.project.activeLayer.removeChildren();
984
+ const shape = createBaseShape(options);
985
+ const bounds = shape.bounds;
986
+ const result = {
987
+ x: bounds.center.x,
988
+ y: bounds.bottom
989
+ };
990
+ shape.remove();
991
+ return result;
992
+ }
880
993
  function getNearestPointOnDieline(point, options) {
881
994
  ensurePaper(options.width * 2, options.height * 2);
882
995
  paper2.project.activeLayer.removeChildren();
883
996
  const shape = createBaseShape(options);
884
997
  const p = new paper2.Point(point.x, point.y);
885
- const nearest = shape.getNearestPoint(p);
886
- const result = { x: nearest.x, y: nearest.y };
998
+ const location = shape.getNearestLocation(p);
999
+ const result = {
1000
+ x: location.point.x,
1001
+ y: location.point.y,
1002
+ normal: location.normal ? { x: location.normal.x, y: location.normal.y } : void 0
1003
+ };
887
1004
  shape.remove();
888
1005
  return result;
889
1006
  }
@@ -900,105 +1017,6 @@ function getPathBounds(pathData) {
900
1017
  };
901
1018
  }
902
1019
 
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
1020
  // src/dieline.ts
1003
1021
  var DielineTool = class {
1004
1022
  constructor(options) {
@@ -1007,7 +1025,7 @@ var DielineTool = class {
1007
1025
  name: "DielineTool"
1008
1026
  };
1009
1027
  this.state = {
1010
- unit: "mm",
1028
+ displayUnit: "mm",
1011
1029
  shape: "rect",
1012
1030
  width: 500,
1013
1031
  height: 500,
@@ -1053,50 +1071,88 @@ var DielineTool = class {
1053
1071
  const configService = context.services.get("ConfigurationService");
1054
1072
  if (configService) {
1055
1073
  const s = this.state;
1056
- s.unit = configService.get("dieline.unit", s.unit);
1074
+ s.displayUnit = configService.get("dieline.displayUnit", s.displayUnit);
1057
1075
  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);
1076
+ s.width = parseLengthToMm(
1077
+ configService.get("dieline.width", s.width),
1078
+ "mm"
1079
+ );
1080
+ s.height = parseLengthToMm(
1081
+ configService.get("dieline.height", s.height),
1082
+ "mm"
1083
+ );
1084
+ s.radius = parseLengthToMm(
1085
+ configService.get("dieline.radius", s.radius),
1086
+ "mm"
1087
+ );
1061
1088
  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);
1089
+ s.offset = parseLengthToMm(
1090
+ configService.get("dieline.offset", s.offset),
1091
+ "mm"
1092
+ );
1093
+ s.mainLine.width = configService.get(
1094
+ "dieline.strokeWidth",
1095
+ s.mainLine.width
1096
+ );
1097
+ s.mainLine.color = configService.get(
1098
+ "dieline.strokeColor",
1099
+ s.mainLine.color
1100
+ );
1101
+ s.mainLine.dashLength = configService.get(
1102
+ "dieline.dashLength",
1103
+ s.mainLine.dashLength
1104
+ );
1066
1105
  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);
1106
+ s.offsetLine.width = configService.get(
1107
+ "dieline.offsetStrokeWidth",
1108
+ s.offsetLine.width
1109
+ );
1110
+ s.offsetLine.color = configService.get(
1111
+ "dieline.offsetStrokeColor",
1112
+ s.offsetLine.color
1113
+ );
1114
+ s.offsetLine.dashLength = configService.get(
1115
+ "dieline.offsetDashLength",
1116
+ s.offsetLine.dashLength
1117
+ );
1118
+ s.offsetLine.style = configService.get(
1119
+ "dieline.offsetStyle",
1120
+ s.offsetLine.style
1121
+ );
1071
1122
  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);
1123
+ s.outsideColor = configService.get(
1124
+ "dieline.outsideColor",
1125
+ s.outsideColor
1126
+ );
1127
+ s.showBleedLines = configService.get(
1128
+ "dieline.showBleedLines",
1129
+ s.showBleedLines
1130
+ );
1074
1131
  s.features = configService.get("dieline.features", s.features);
1075
1132
  s.pathData = configService.get("dieline.pathData", s.pathData);
1076
1133
  configService.onAnyChange((e) => {
1077
1134
  if (e.key.startsWith("dieline.")) {
1078
- console.log(`[DielineTool] Config change detected: ${e.key} -> ${e.value}`);
1079
1135
  switch (e.key) {
1080
- case "dieline.unit":
1081
- s.unit = e.value;
1136
+ case "dieline.displayUnit":
1137
+ s.displayUnit = e.value;
1082
1138
  break;
1083
1139
  case "dieline.shape":
1084
1140
  s.shape = e.value;
1085
1141
  break;
1086
1142
  case "dieline.width":
1087
- s.width = e.value;
1143
+ s.width = parseLengthToMm(e.value, "mm");
1088
1144
  break;
1089
1145
  case "dieline.height":
1090
- s.height = e.value;
1146
+ s.height = parseLengthToMm(e.value, "mm");
1091
1147
  break;
1092
1148
  case "dieline.radius":
1093
- s.radius = e.value;
1149
+ s.radius = parseLengthToMm(e.value, "mm");
1094
1150
  break;
1095
1151
  case "dieline.padding":
1096
1152
  s.padding = e.value;
1097
1153
  break;
1098
1154
  case "dieline.offset":
1099
- s.offset = e.value;
1155
+ s.offset = parseLengthToMm(e.value, "mm");
1100
1156
  break;
1101
1157
  case "dieline.strokeWidth":
1102
1158
  s.mainLine.width = e.value;
@@ -1155,11 +1211,11 @@ var DielineTool = class {
1155
1211
  return {
1156
1212
  [ContributionPointIds2.CONFIGURATIONS]: [
1157
1213
  {
1158
- id: "dieline.unit",
1214
+ id: "dieline.displayUnit",
1159
1215
  type: "select",
1160
- label: "Unit",
1161
- options: ["px", "mm", "cm", "in"],
1162
- default: s.unit
1216
+ label: "Display Unit",
1217
+ options: ["mm", "cm", "in"],
1218
+ default: s.displayUnit
1163
1219
  },
1164
1220
  {
1165
1221
  id: "dieline.shape",
@@ -1171,7 +1227,7 @@ var DielineTool = class {
1171
1227
  {
1172
1228
  id: "dieline.width",
1173
1229
  type: "number",
1174
- label: "Width",
1230
+ label: "Width (mm)",
1175
1231
  min: 10,
1176
1232
  max: 2e3,
1177
1233
  default: s.width
@@ -1179,7 +1235,7 @@ var DielineTool = class {
1179
1235
  {
1180
1236
  id: "dieline.height",
1181
1237
  type: "number",
1182
- label: "Height",
1238
+ label: "Height (mm)",
1183
1239
  min: 10,
1184
1240
  max: 2e3,
1185
1241
  default: s.height
@@ -1187,7 +1243,7 @@ var DielineTool = class {
1187
1243
  {
1188
1244
  id: "dieline.radius",
1189
1245
  type: "number",
1190
- label: "Corner Radius",
1246
+ label: "Corner Radius (mm)",
1191
1247
  min: 0,
1192
1248
  max: 500,
1193
1249
  default: s.radius
@@ -1202,7 +1258,7 @@ var DielineTool = class {
1202
1258
  {
1203
1259
  id: "dieline.offset",
1204
1260
  type: "number",
1205
- label: "Bleed Offset",
1261
+ label: "Bleed Offset (mm)",
1206
1262
  min: -100,
1207
1263
  max: 100,
1208
1264
  default: s.offset
@@ -1303,18 +1359,12 @@ var DielineTool = class {
1303
1359
  );
1304
1360
  if (!configService) return;
1305
1361
  const features = configService.get("dieline.features") || [];
1306
- const dielineWidth = configService.get("dieline.width") || 500;
1307
- const dielineHeight = configService.get("dieline.height") || 500;
1308
1362
  let changed = false;
1309
1363
  const newFeatures = features.map((f) => {
1310
1364
  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) {
1365
+ if (f.x !== x || f.y !== y) {
1316
1366
  changed = true;
1317
- return { ...f, x: constrained.x, y: constrained.y };
1367
+ return { ...f, x, y };
1318
1368
  }
1319
1369
  }
1320
1370
  return f;
@@ -1429,7 +1479,7 @@ var DielineTool = class {
1429
1479
  const layer = this.getLayer();
1430
1480
  if (!layer) return;
1431
1481
  const {
1432
- unit,
1482
+ displayUnit,
1433
1483
  shape,
1434
1484
  radius,
1435
1485
  offset,
@@ -1440,15 +1490,13 @@ var DielineTool = class {
1440
1490
  showBleedLines,
1441
1491
  features
1442
1492
  } = this.state;
1443
- let { width, height } = this.state;
1493
+ const { width, height } = this.state;
1444
1494
  const canvasW = this.canvasService.canvas.width || 800;
1445
1495
  const canvasH = this.canvasService.canvas.height || 600;
1446
1496
  const paddingPx = this.resolvePadding(canvasW, canvasH);
1447
- const layout = Coordinate.calculateLayout(
1448
- { width: canvasW, height: canvasH },
1449
- { width, height },
1450
- paddingPx
1451
- );
1497
+ this.canvasService.viewport.setPadding(paddingPx);
1498
+ this.canvasService.viewport.updatePhysical(width, height);
1499
+ const layout = this.canvasService.viewport.layout;
1452
1500
  const scale = layout.scale;
1453
1501
  const cx = layout.offsetX + layout.width / 2;
1454
1502
  const cy = layout.offsetY + layout.height / 2;
@@ -1635,15 +1683,22 @@ var DielineTool = class {
1635
1683
  }
1636
1684
  getGeometry() {
1637
1685
  if (!this.canvasService) return null;
1638
- const { unit, shape, width, height, radius, offset, mainLine, pathData } = this.state;
1686
+ const {
1687
+ displayUnit,
1688
+ shape,
1689
+ width,
1690
+ height,
1691
+ radius,
1692
+ offset,
1693
+ mainLine,
1694
+ pathData
1695
+ } = this.state;
1639
1696
  const canvasW = this.canvasService.canvas.width || 800;
1640
1697
  const canvasH = this.canvasService.canvas.height || 600;
1641
1698
  const paddingPx = this.resolvePadding(canvasW, canvasH);
1642
- const layout = Coordinate.calculateLayout(
1643
- { width: canvasW, height: canvasH },
1644
- { width, height },
1645
- paddingPx
1646
- );
1699
+ this.canvasService.viewport.setPadding(paddingPx);
1700
+ this.canvasService.viewport.updatePhysical(width, height);
1701
+ const layout = this.canvasService.viewport.layout;
1647
1702
  const scale = layout.scale;
1648
1703
  const cx = layout.offsetX + layout.width / 2;
1649
1704
  const cy = layout.offsetY + layout.height / 2;
@@ -1651,14 +1706,14 @@ var DielineTool = class {
1651
1706
  const visualHeight = layout.height;
1652
1707
  return {
1653
1708
  shape,
1654
- unit,
1709
+ unit: "mm",
1710
+ displayUnit,
1655
1711
  x: cx,
1656
1712
  y: cy,
1657
1713
  width: visualWidth,
1658
1714
  height: visualHeight,
1659
1715
  radius: radius * scale,
1660
1716
  offset: offset * scale,
1661
- // Pass scale to help other tools (like FeatureTool) convert units
1662
1717
  scale,
1663
1718
  strokeWidth: mainLine.width,
1664
1719
  pathData
@@ -1668,15 +1723,13 @@ var DielineTool = class {
1668
1723
  if (!this.canvasService) return null;
1669
1724
  const userLayer = this.canvasService.getLayer("user");
1670
1725
  if (!userLayer) return null;
1671
- const { shape, width, height, radius, features, unit, pathData } = this.state;
1726
+ const { shape, width, height, radius, features, pathData } = this.state;
1672
1727
  const canvasW = this.canvasService.canvas.width || 800;
1673
1728
  const canvasH = this.canvasService.canvas.height || 600;
1674
1729
  const paddingPx = this.resolvePadding(canvasW, canvasH);
1675
- const layout = Coordinate.calculateLayout(
1676
- { width: canvasW, height: canvasH },
1677
- { width, height },
1678
- paddingPx
1679
- );
1730
+ this.canvasService.viewport.setPadding(paddingPx);
1731
+ this.canvasService.viewport.updatePhysical(width, height);
1732
+ const layout = this.canvasService.viewport.layout;
1680
1733
  const scale = layout.scale;
1681
1734
  const cx = layout.offsetX + layout.width / 2;
1682
1735
  const cy = layout.offsetY + layout.height / 2;
@@ -1895,13 +1948,212 @@ import {
1895
1948
  ContributionPointIds as ContributionPointIds4
1896
1949
  } from "@pooder/core";
1897
1950
  import { Circle, Group, Point, Rect as Rect2 } from "fabric";
1951
+
1952
+ // src/constraints.ts
1953
+ var ConstraintRegistry = class {
1954
+ static register(type, handler) {
1955
+ this.handlers.set(type, handler);
1956
+ }
1957
+ static apply(x, y, feature, context, constraints) {
1958
+ const list = constraints || feature.constraints;
1959
+ if (!list || list.length === 0) {
1960
+ return { x, y };
1961
+ }
1962
+ let currentX = x;
1963
+ let currentY = y;
1964
+ for (const constraint of list) {
1965
+ const handler = this.handlers.get(constraint.type);
1966
+ if (handler) {
1967
+ const result = handler(currentX, currentY, feature, context, constraint.params || {});
1968
+ currentX = result.x;
1969
+ currentY = result.y;
1970
+ }
1971
+ }
1972
+ return { x: currentX, y: currentY };
1973
+ }
1974
+ };
1975
+ ConstraintRegistry.handlers = /* @__PURE__ */ new Map();
1976
+ var pathConstraint = (x, y, feature, context, params) => {
1977
+ const { dielineWidth, dielineHeight, geometry } = context;
1978
+ if (!geometry) return { x, y };
1979
+ const minX = geometry.x - geometry.width / 2;
1980
+ const minY = geometry.y - geometry.height / 2;
1981
+ const absX = minX + x * geometry.width;
1982
+ const absY = minY + y * geometry.height;
1983
+ const nearest = getNearestPointOnDieline(
1984
+ { x: absX, y: absY },
1985
+ geometry
1986
+ );
1987
+ let finalX = nearest.x;
1988
+ let finalY = nearest.y;
1989
+ const hasOffsetParams = params.minOffset !== void 0 || params.maxOffset !== void 0;
1990
+ if (hasOffsetParams && nearest.normal) {
1991
+ const dx = absX - nearest.x;
1992
+ const dy = absY - nearest.y;
1993
+ const nx2 = nearest.normal.x;
1994
+ const ny2 = nearest.normal.y;
1995
+ const dist = dx * nx2 + dy * ny2;
1996
+ const scale = dielineWidth > 0 ? geometry.width / dielineWidth : 1;
1997
+ const rawMin = params.minOffset !== void 0 ? params.minOffset : 0;
1998
+ const rawMax = params.maxOffset !== void 0 ? params.maxOffset : 0;
1999
+ const minOffset = rawMin * scale;
2000
+ const maxOffset = rawMax * scale;
2001
+ const clampedDist = Math.max(minOffset, Math.min(dist, maxOffset));
2002
+ finalX = nearest.x + nx2 * clampedDist;
2003
+ finalY = nearest.y + ny2 * clampedDist;
2004
+ }
2005
+ const nx = geometry.width > 0 ? (finalX - minX) / geometry.width : 0.5;
2006
+ const ny = geometry.height > 0 ? (finalY - minY) / geometry.height : 0.5;
2007
+ return { x: nx, y: ny };
2008
+ };
2009
+ var edgeConstraint = (x, y, feature, context, params) => {
2010
+ const { dielineWidth, dielineHeight } = context;
2011
+ const allowedEdges = params.allowedEdges || [
2012
+ "top",
2013
+ "bottom",
2014
+ "left",
2015
+ "right"
2016
+ ];
2017
+ const confine = params.confine || false;
2018
+ const offset = params.offset || 0;
2019
+ const distances = [];
2020
+ if (allowedEdges.includes("top"))
2021
+ distances.push({ edge: "top", dist: y * dielineHeight });
2022
+ if (allowedEdges.includes("bottom"))
2023
+ distances.push({ edge: "bottom", dist: (1 - y) * dielineHeight });
2024
+ if (allowedEdges.includes("left"))
2025
+ distances.push({ edge: "left", dist: x * dielineWidth });
2026
+ if (allowedEdges.includes("right"))
2027
+ distances.push({ edge: "right", dist: (1 - x) * dielineWidth });
2028
+ if (distances.length === 0) return { x, y };
2029
+ distances.sort((a, b) => a.dist - b.dist);
2030
+ const nearest = distances[0].edge;
2031
+ let newX = x;
2032
+ let newY = y;
2033
+ const fw = feature.width || 0;
2034
+ const fh = feature.height || 0;
2035
+ switch (nearest) {
2036
+ case "top":
2037
+ newY = 0 + offset / dielineHeight;
2038
+ if (confine) {
2039
+ const minX = fw / 2 / dielineWidth;
2040
+ const maxX = 1 - minX;
2041
+ newX = Math.max(minX, Math.min(newX, maxX));
2042
+ }
2043
+ break;
2044
+ case "bottom":
2045
+ newY = 1 - offset / dielineHeight;
2046
+ if (confine) {
2047
+ const minX = fw / 2 / dielineWidth;
2048
+ const maxX = 1 - minX;
2049
+ newX = Math.max(minX, Math.min(newX, maxX));
2050
+ }
2051
+ break;
2052
+ case "left":
2053
+ newX = 0 + offset / dielineWidth;
2054
+ if (confine) {
2055
+ const minY = fh / 2 / dielineHeight;
2056
+ const maxY = 1 - minY;
2057
+ newY = Math.max(minY, Math.min(newY, maxY));
2058
+ }
2059
+ break;
2060
+ case "right":
2061
+ newX = 1 - offset / dielineWidth;
2062
+ if (confine) {
2063
+ const minY = fh / 2 / dielineHeight;
2064
+ const maxY = 1 - minY;
2065
+ newY = Math.max(minY, Math.min(newY, maxY));
2066
+ }
2067
+ break;
2068
+ }
2069
+ return { x: newX, y: newY };
2070
+ };
2071
+ var internalConstraint = (x, y, feature, context, params) => {
2072
+ const { dielineWidth, dielineHeight } = context;
2073
+ const margin = params.margin || 0;
2074
+ const fw = feature.width || 0;
2075
+ const fh = feature.height || 0;
2076
+ const minX = (margin + fw / 2) / dielineWidth;
2077
+ const maxX = 1 - (margin + fw / 2) / dielineWidth;
2078
+ const minY = (margin + fh / 2) / dielineHeight;
2079
+ const maxY = 1 - (margin + fh / 2) / dielineHeight;
2080
+ const clampedX = minX > maxX ? 0.5 : Math.max(minX, Math.min(x, maxX));
2081
+ const clampedY = minY > maxY ? 0.5 : Math.max(minY, Math.min(y, maxY));
2082
+ return { x: clampedX, y: clampedY };
2083
+ };
2084
+ var tangentBottomConstraint = (x, y, feature, context, params) => {
2085
+ const { dielineWidth, dielineHeight } = context;
2086
+ const gap = params.gap || 0;
2087
+ const confineX = params.confineX !== false;
2088
+ const extentY = feature.shape === "circle" ? feature.radius || 0 : (feature.height || 0) / 2;
2089
+ const newY = 1 + (extentY + gap) / dielineHeight;
2090
+ let newX = x;
2091
+ if (confineX) {
2092
+ const extentX = feature.shape === "circle" ? feature.radius || 0 : (feature.width || 0) / 2;
2093
+ const minX = extentX / dielineWidth;
2094
+ const maxX = 1 - extentX / dielineWidth;
2095
+ newX = minX > maxX ? 0.5 : Math.max(minX, Math.min(newX, maxX));
2096
+ }
2097
+ return { x: newX, y: newY };
2098
+ };
2099
+ var lowestTangentConstraint = (x, y, feature, context, params) => {
2100
+ const { dielineWidth, dielineHeight, geometry } = context;
2101
+ if (!geometry) return { x, y };
2102
+ const lowest = getLowestPointOnDieline(geometry);
2103
+ const minY = geometry.y - geometry.height / 2;
2104
+ const normY = (lowest.y - minY) / geometry.height;
2105
+ const gap = params.gap || 0;
2106
+ const confineX = params.confineX !== false;
2107
+ const extentY = feature.shape === "circle" ? feature.radius || 0 : (feature.height || 0) / 2;
2108
+ const newY = normY + (extentY + gap) / dielineHeight;
2109
+ let newX = x;
2110
+ if (confineX) {
2111
+ const extentX = feature.shape === "circle" ? feature.radius || 0 : (feature.width || 0) / 2;
2112
+ const minX = extentX / dielineWidth;
2113
+ const maxX = 1 - extentX / dielineWidth;
2114
+ newX = minX > maxX ? 0.5 : Math.max(minX, Math.min(newX, maxX));
2115
+ }
2116
+ return { x: newX, y: newY };
2117
+ };
2118
+ ConstraintRegistry.register("path", pathConstraint);
2119
+ ConstraintRegistry.register("edge", edgeConstraint);
2120
+ ConstraintRegistry.register("internal", internalConstraint);
2121
+ ConstraintRegistry.register("tangent-bottom", tangentBottomConstraint);
2122
+ ConstraintRegistry.register("lowest-tangent", lowestTangentConstraint);
2123
+
2124
+ // src/featureComplete.ts
2125
+ function validateFeaturesStrict(features, context) {
2126
+ const eps = 1e-6;
2127
+ const issues = [];
2128
+ for (const f of features) {
2129
+ if (!f.constraints || f.constraints.length === 0) continue;
2130
+ const constrained = ConstraintRegistry.apply(f.x, f.y, f, context, f.constraints);
2131
+ if (Math.abs(constrained.x - f.x) > eps || Math.abs(constrained.y - f.y) > eps) {
2132
+ issues.push({
2133
+ featureId: f.id,
2134
+ groupId: f.groupId,
2135
+ reason: "Position violates constraint strategy"
2136
+ });
2137
+ }
2138
+ }
2139
+ return { ok: issues.length === 0, issues: issues.length ? issues : void 0 };
2140
+ }
2141
+ function completeFeaturesStrict(features, context, update) {
2142
+ const validation = validateFeaturesStrict(features, context);
2143
+ if (!validation.ok) return validation;
2144
+ const next = JSON.parse(JSON.stringify(features || []));
2145
+ update(next);
2146
+ return { ok: true };
2147
+ }
2148
+
2149
+ // src/feature.ts
1898
2150
  var FeatureTool = class {
1899
2151
  constructor(options) {
1900
2152
  this.id = "pooder.kit.feature";
1901
2153
  this.metadata = {
1902
2154
  name: "FeatureTool"
1903
2155
  };
1904
- this.features = [];
2156
+ this.workingFeatures = [];
1905
2157
  this.isUpdatingConfig = false;
1906
2158
  this.isToolActive = false;
1907
2159
  this.handleMoving = null;
@@ -1927,12 +2179,15 @@ var FeatureTool = class {
1927
2179
  "ConfigurationService"
1928
2180
  );
1929
2181
  if (configService) {
1930
- this.features = configService.get("dieline.features", []);
2182
+ const features = configService.get("dieline.features", []) || [];
2183
+ this.workingFeatures = this.cloneFeatures(features);
1931
2184
  configService.onAnyChange((e) => {
1932
2185
  if (this.isUpdatingConfig) return;
1933
2186
  if (e.key === "dieline.features") {
1934
- this.features = e.value || [];
2187
+ const next = e.value || [];
2188
+ this.workingFeatures = this.cloneFeatures(next);
1935
2189
  this.redraw();
2190
+ this.emitWorkingChange();
1936
2191
  }
1937
2192
  });
1938
2193
  }
@@ -1990,57 +2245,172 @@ var FeatureTool = class {
1990
2245
  command: "clearFeatures",
1991
2246
  title: "Clear Features",
1992
2247
  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
- }
2248
+ this.setWorkingFeatures([]);
2249
+ this.redraw();
2250
+ this.emitWorkingChange();
2000
2251
  return true;
2001
2252
  }
2253
+ },
2254
+ {
2255
+ command: "getWorkingFeatures",
2256
+ title: "Get Working Features",
2257
+ handler: () => {
2258
+ return this.cloneFeatures(this.workingFeatures);
2259
+ }
2260
+ },
2261
+ {
2262
+ command: "setWorkingFeatures",
2263
+ title: "Set Working Features",
2264
+ handler: async (features) => {
2265
+ await this.refreshGeometry();
2266
+ this.setWorkingFeatures(this.cloneFeatures(features || []));
2267
+ this.redraw();
2268
+ this.emitWorkingChange();
2269
+ return { ok: true };
2270
+ }
2271
+ },
2272
+ {
2273
+ command: "updateWorkingGroupPosition",
2274
+ title: "Update Working Group Position",
2275
+ handler: (groupId, x, y) => {
2276
+ return this.updateWorkingGroupPosition(groupId, x, y);
2277
+ }
2278
+ },
2279
+ {
2280
+ command: "completeFeatures",
2281
+ title: "Complete Features",
2282
+ handler: () => {
2283
+ return this.completeFeatures();
2284
+ }
2002
2285
  }
2003
2286
  ]
2004
2287
  };
2005
2288
  }
2006
- addFeature(type) {
2289
+ cloneFeatures(features) {
2290
+ return JSON.parse(JSON.stringify(features || []));
2291
+ }
2292
+ emitWorkingChange() {
2007
2293
  var _a;
2008
- if (!this.canvasService) return false;
2009
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
2010
- "ConfigurationService"
2294
+ (_a = this.context) == null ? void 0 : _a.eventBus.emit("feature:working:change", {
2295
+ features: this.cloneFeatures(this.workingFeatures)
2296
+ });
2297
+ }
2298
+ async refreshGeometry() {
2299
+ if (!this.context) return;
2300
+ const commandService = this.context.services.get("CommandService");
2301
+ if (!commandService) return;
2302
+ try {
2303
+ const g = await Promise.resolve(commandService.executeCommand("getGeometry"));
2304
+ if (g) this.currentGeometry = g;
2305
+ } catch (e) {
2306
+ }
2307
+ }
2308
+ setWorkingFeatures(next) {
2309
+ this.workingFeatures = next;
2310
+ }
2311
+ updateWorkingGroupPosition(groupId, x, y) {
2312
+ var _a, _b, _c;
2313
+ if (!groupId) return { ok: false };
2314
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get("ConfigurationService");
2315
+ if (!configService) return { ok: false };
2316
+ const dielineWidth = parseLengthToMm(
2317
+ (_b = configService.get("dieline.width")) != null ? _b : 500,
2318
+ "mm"
2319
+ );
2320
+ const dielineHeight = parseLengthToMm(
2321
+ (_c = configService.get("dieline.height")) != null ? _c : 500,
2322
+ "mm"
2323
+ );
2324
+ let changed = false;
2325
+ const next = this.workingFeatures.map((f) => {
2326
+ if (f.groupId !== groupId) return f;
2327
+ let nx = x;
2328
+ let ny = y;
2329
+ if (f.constraints && dielineWidth > 0 && dielineHeight > 0) {
2330
+ const constrained = ConstraintRegistry.apply(nx, ny, f, {
2331
+ dielineWidth,
2332
+ dielineHeight
2333
+ });
2334
+ nx = constrained.x;
2335
+ ny = constrained.y;
2336
+ }
2337
+ if (f.x !== nx || f.y !== ny) {
2338
+ changed = true;
2339
+ return { ...f, x: nx, y: ny };
2340
+ }
2341
+ return f;
2342
+ });
2343
+ if (!changed) return { ok: true };
2344
+ this.setWorkingFeatures(next);
2345
+ this.redraw();
2346
+ this.enforceConstraints();
2347
+ this.emitWorkingChange();
2348
+ return { ok: true };
2349
+ }
2350
+ completeFeatures() {
2351
+ var _a, _b, _c;
2352
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get("ConfigurationService");
2353
+ if (!configService) {
2354
+ return {
2355
+ ok: false,
2356
+ issues: [
2357
+ { featureId: "unknown", reason: "ConfigurationService not found" }
2358
+ ]
2359
+ };
2360
+ }
2361
+ const dielineWidth = parseLengthToMm(
2362
+ (_b = configService.get("dieline.width")) != null ? _b : 500,
2363
+ "mm"
2011
2364
  );
2012
- const unit = (configService == null ? void 0 : configService.get("dieline.unit", "mm")) || "mm";
2013
- const defaultSize = Coordinate.convertUnit(10, "mm", unit);
2365
+ const dielineHeight = parseLengthToMm(
2366
+ (_c = configService.get("dieline.height")) != null ? _c : 500,
2367
+ "mm"
2368
+ );
2369
+ const result = completeFeaturesStrict(
2370
+ this.workingFeatures,
2371
+ { dielineWidth, dielineHeight },
2372
+ (next) => {
2373
+ this.isUpdatingConfig = true;
2374
+ try {
2375
+ configService.update("dieline.features", next);
2376
+ } finally {
2377
+ this.isUpdatingConfig = false;
2378
+ }
2379
+ this.workingFeatures = this.cloneFeatures(next);
2380
+ this.emitWorkingChange();
2381
+ }
2382
+ );
2383
+ if (!result.ok) {
2384
+ return {
2385
+ ok: false,
2386
+ issues: result.issues
2387
+ };
2388
+ }
2389
+ return { ok: true };
2390
+ }
2391
+ addFeature(type) {
2392
+ if (!this.canvasService) return false;
2014
2393
  const newFeature = {
2015
2394
  id: Date.now().toString(),
2016
2395
  operation: type,
2017
- placement: "edge",
2018
2396
  shape: "rect",
2019
2397
  x: 0.5,
2020
2398
  y: 0,
2021
2399
  // Top edge
2022
- width: defaultSize,
2023
- height: defaultSize,
2024
- rotation: 0
2400
+ width: 10,
2401
+ height: 10,
2402
+ rotation: 0,
2403
+ renderBehavior: "edge",
2404
+ // Default constraint: path (snap to edge)
2405
+ constraints: [{ type: "path" }]
2025
2406
  };
2026
- if (configService) {
2027
- const current = configService.get(
2028
- "dieline.features",
2029
- []
2030
- );
2031
- configService.update("dieline.features", [...current, newFeature]);
2032
- }
2407
+ this.setWorkingFeatures([...this.workingFeatures || [], newFeature]);
2408
+ this.redraw();
2409
+ this.emitWorkingChange();
2033
2410
  return true;
2034
2411
  }
2035
2412
  addDoubleLayerHole() {
2036
- var _a;
2037
2413
  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
2414
  const groupId = Date.now().toString();
2045
2415
  const timestamp = Date.now();
2046
2416
  const lug = {
@@ -2048,32 +2418,28 @@ var FeatureTool = class {
2048
2418
  groupId,
2049
2419
  operation: "add",
2050
2420
  shape: "circle",
2051
- placement: "edge",
2052
2421
  x: 0.5,
2053
2422
  y: 0,
2054
- radius: lugRadius,
2055
- // 20mm
2056
- rotation: 0
2423
+ radius: 20,
2424
+ rotation: 0,
2425
+ renderBehavior: "edge",
2426
+ constraints: [{ type: "path" }]
2057
2427
  };
2058
2428
  const hole = {
2059
2429
  id: `${timestamp}-hole`,
2060
2430
  groupId,
2061
2431
  operation: "subtract",
2062
2432
  shape: "circle",
2063
- placement: "edge",
2064
2433
  x: 0.5,
2065
2434
  y: 0,
2066
- radius: holeRadius,
2067
- // 15mm
2068
- rotation: 0
2435
+ radius: 15,
2436
+ rotation: 0,
2437
+ renderBehavior: "edge",
2438
+ constraints: [{ type: "path" }]
2069
2439
  };
2070
- if (configService) {
2071
- const current = configService.get(
2072
- "dieline.features",
2073
- []
2074
- );
2075
- configService.update("dieline.features", [...current, lug, hole]);
2076
- }
2440
+ this.setWorkingFeatures([...this.workingFeatures || [], lug, hole]);
2441
+ this.redraw();
2442
+ this.emitWorkingChange();
2077
2443
  return true;
2078
2444
  }
2079
2445
  getGeometryForFeature(geometry, feature) {
@@ -2117,12 +2483,12 @@ var FeatureTool = class {
2117
2483
  if ((_b = target.data) == null ? void 0 : _b.isGroup) {
2118
2484
  const indices = (_c = target.data) == null ? void 0 : _c.indices;
2119
2485
  if (indices && indices.length > 0) {
2120
- feature = this.features[indices[0]];
2486
+ feature = this.workingFeatures[indices[0]];
2121
2487
  }
2122
2488
  } else {
2123
2489
  const index = (_d = target.data) == null ? void 0 : _d.index;
2124
2490
  if (index !== void 0) {
2125
- feature = this.features[index];
2491
+ feature = this.workingFeatures[index];
2126
2492
  }
2127
2493
  }
2128
2494
  const geometry = this.getGeometryForFeature(
@@ -2143,7 +2509,7 @@ var FeatureTool = class {
2143
2509
  }
2144
2510
  if (!this.handleModified) {
2145
2511
  this.handleModified = (e) => {
2146
- var _a, _b, _c, _d;
2512
+ var _a, _b, _c;
2147
2513
  const target = e.target;
2148
2514
  if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return;
2149
2515
  if ((_b = target.data) == null ? void 0 : _b.isGroup) {
@@ -2151,11 +2517,11 @@ var FeatureTool = class {
2151
2517
  const indices = (_c = groupObj.data) == null ? void 0 : _c.indices;
2152
2518
  if (!indices) return;
2153
2519
  const groupCenter = new Point(groupObj.left, groupObj.top);
2154
- const newFeatures = [...this.features];
2520
+ const newFeatures = [...this.workingFeatures];
2155
2521
  const { x, y } = this.currentGeometry;
2156
2522
  groupObj.getObjects().forEach((child, i) => {
2157
2523
  const originalIndex = indices[i];
2158
- const feature = this.features[originalIndex];
2524
+ const feature = this.workingFeatures[originalIndex];
2159
2525
  const geometry = this.getGeometryForFeature(
2160
2526
  this.currentGeometry,
2161
2527
  feature
@@ -2173,18 +2539,8 @@ var FeatureTool = class {
2173
2539
  y: normalizedY
2174
2540
  };
2175
2541
  });
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
- }
2542
+ this.setWorkingFeatures(newFeatures);
2543
+ this.emitWorkingChange();
2188
2544
  } else {
2189
2545
  this.syncFeatureFromCanvas(target);
2190
2546
  }
@@ -2218,56 +2574,41 @@ var FeatureTool = class {
2218
2574
  this.canvasService.requestRenderAll();
2219
2575
  }
2220
2576
  constrainPosition(p, geometry, limit, feature) {
2221
- if (feature && feature.constraints) {
2222
- const minX = geometry.x - geometry.width / 2;
2223
- const minY = geometry.y - geometry.height / 2;
2224
- const nx = geometry.width > 0 ? (p.x - minX) / geometry.width : 0.5;
2225
- const ny = geometry.height > 0 ? (p.y - minY) / geometry.height : 0.5;
2226
- const scale2 = geometry.scale || 1;
2227
- const dielineWidth = geometry.width / scale2;
2228
- const dielineHeight = geometry.height / scale2;
2229
- const constrained = ConstraintRegistry.apply(nx, ny, feature, {
2230
- dielineWidth,
2231
- dielineHeight
2232
- });
2233
- return {
2234
- x: minX + constrained.x * geometry.width,
2235
- y: minY + constrained.y * geometry.height
2236
- };
2237
- }
2238
- if (feature && feature.placement === "internal") {
2239
- const minX = geometry.x - geometry.width / 2;
2240
- const maxX = geometry.x + geometry.width / 2;
2241
- const minY = geometry.y - geometry.height / 2;
2242
- const maxY = geometry.y + geometry.height / 2;
2243
- return {
2244
- x: Math.max(minX, Math.min(maxX, p.x)),
2245
- y: Math.max(minY, Math.min(maxY, p.y))
2246
- };
2247
- }
2248
- const nearest = getNearestPointOnDieline({ x: p.x, y: p.y }, {
2249
- ...geometry,
2250
- features: []
2251
- });
2252
- const dx = p.x - nearest.x;
2253
- const dy = p.y - nearest.y;
2254
- const dist = Math.sqrt(dx * dx + dy * dy);
2255
- if (dist <= limit) {
2577
+ var _a;
2578
+ if (!feature) {
2256
2579
  return { x: p.x, y: p.y };
2257
2580
  }
2258
- const scale = limit / dist;
2581
+ const minX = geometry.x - geometry.width / 2;
2582
+ const minY = geometry.y - geometry.height / 2;
2583
+ const nx = geometry.width > 0 ? (p.x - minX) / geometry.width : 0.5;
2584
+ const ny = geometry.height > 0 ? (p.y - minY) / geometry.height : 0.5;
2585
+ const scale = geometry.scale || 1;
2586
+ const dielineWidth = geometry.width / scale;
2587
+ const dielineHeight = geometry.height / scale;
2588
+ const activeConstraints = (_a = feature.constraints) == null ? void 0 : _a.filter((c) => !c.validateOnly);
2589
+ const constrained = ConstraintRegistry.apply(
2590
+ nx,
2591
+ ny,
2592
+ feature,
2593
+ {
2594
+ dielineWidth,
2595
+ dielineHeight,
2596
+ geometry
2597
+ },
2598
+ activeConstraints
2599
+ );
2259
2600
  return {
2260
- x: nearest.x + dx * scale,
2261
- y: nearest.y + dy * scale
2601
+ x: minX + constrained.x * geometry.width,
2602
+ y: minY + constrained.y * geometry.height
2262
2603
  };
2263
2604
  }
2264
2605
  syncFeatureFromCanvas(target) {
2265
2606
  var _a;
2266
2607
  if (!this.currentGeometry || !this.context) return;
2267
2608
  const index = (_a = target.data) == null ? void 0 : _a.index;
2268
- if (index === void 0 || index < 0 || index >= this.features.length)
2609
+ if (index === void 0 || index < 0 || index >= this.workingFeatures.length)
2269
2610
  return;
2270
- const feature = this.features[index];
2611
+ const feature = this.workingFeatures[index];
2271
2612
  const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
2272
2613
  const { width, height, x, y } = geometry;
2273
2614
  const left = x - width / 2;
@@ -2280,20 +2621,10 @@ var FeatureTool = class {
2280
2621
  y: normalizedY
2281
2622
  // Could also update rotation if we allowed rotating markers
2282
2623
  };
2283
- const newFeatures = [...this.features];
2624
+ const newFeatures = [...this.workingFeatures];
2284
2625
  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
- }
2626
+ this.setWorkingFeatures(newFeatures);
2627
+ this.emitWorkingChange();
2297
2628
  }
2298
2629
  redraw() {
2299
2630
  if (!this.canvasService || !this.currentGeometry) return;
@@ -2304,7 +2635,7 @@ var FeatureTool = class {
2304
2635
  return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
2305
2636
  });
2306
2637
  existing.forEach((obj) => canvas.remove(obj));
2307
- if (!this.features || this.features.length === 0) {
2638
+ if (!this.workingFeatures || this.workingFeatures.length === 0) {
2308
2639
  this.canvasService.requestRenderAll();
2309
2640
  return;
2310
2641
  }
@@ -2312,7 +2643,7 @@ var FeatureTool = class {
2312
2643
  const finalScale = scale;
2313
2644
  const groups = {};
2314
2645
  const singles = [];
2315
- this.features.forEach((f, i) => {
2646
+ this.workingFeatures.forEach((f, i) => {
2316
2647
  if (f.groupId) {
2317
2648
  if (!groups[f.groupId]) groups[f.groupId] = [];
2318
2649
  groups[f.groupId].push({ feature: f, index: i });
@@ -2359,6 +2690,33 @@ var FeatureTool = class {
2359
2690
  if (feature.rotation) {
2360
2691
  shape.rotate(feature.rotation);
2361
2692
  }
2693
+ if (feature.bridge && feature.bridge.type === "vertical") {
2694
+ const bridgeIndicator = new Rect2({
2695
+ width: visualWidth,
2696
+ height: 100 * featureScale,
2697
+ // Arbitrary long length to show direction
2698
+ fill: "transparent",
2699
+ stroke: "#888",
2700
+ strokeWidth: 1,
2701
+ strokeDashArray: [2, 2],
2702
+ originX: "center",
2703
+ originY: "bottom",
2704
+ // Anchor at bottom so it extends up
2705
+ left: pos.x,
2706
+ top: pos.y - visualHeight / 2,
2707
+ // Start from top of feature
2708
+ opacity: 0.5,
2709
+ selectable: false,
2710
+ evented: false
2711
+ });
2712
+ const group = new Group([bridgeIndicator, shape], {
2713
+ originX: "center",
2714
+ originY: "center",
2715
+ left: pos.x,
2716
+ top: pos.y
2717
+ });
2718
+ return group;
2719
+ }
2362
2720
  return shape;
2363
2721
  };
2364
2722
  singles.forEach(({ feature, index }) => {
@@ -2380,25 +2738,6 @@ var FeatureTool = class {
2380
2738
  lockScalingY: true,
2381
2739
  data: { type: "feature-marker", index, isGroup: false }
2382
2740
  });
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
2741
  canvas.add(marker);
2403
2742
  canvas.bringObjectToFront(marker);
2404
2743
  });
@@ -2435,25 +2774,6 @@ var FeatureTool = class {
2435
2774
  indices: members.map((m) => m.index)
2436
2775
  }
2437
2776
  });
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
2777
  canvas.add(groupObj);
2458
2778
  canvas.bringObjectToFront(groupObj);
2459
2779
  });
@@ -2472,12 +2792,12 @@ var FeatureTool = class {
2472
2792
  if ((_a = marker.data) == null ? void 0 : _a.isGroup) {
2473
2793
  const indices = (_b = marker.data) == null ? void 0 : _b.indices;
2474
2794
  if (indices && indices.length > 0) {
2475
- feature = this.features[indices[0]];
2795
+ feature = this.workingFeatures[indices[0]];
2476
2796
  }
2477
2797
  } else {
2478
2798
  const index = (_c = marker.data) == null ? void 0 : _c.index;
2479
2799
  if (index !== void 0) {
2480
- feature = this.features[index];
2800
+ feature = this.workingFeatures[index];
2481
2801
  }
2482
2802
  }
2483
2803
  const geometry = this.getGeometryForFeature(
@@ -3207,7 +3527,7 @@ var RulerTool = class {
3207
3527
  // Dieline context for sync
3208
3528
  this.dielineWidth = 500;
3209
3529
  this.dielineHeight = 500;
3210
- this.dielineUnit = "mm";
3530
+ this.dielineDisplayUnit = "mm";
3211
3531
  this.dielinePadding = 40;
3212
3532
  this.dielineOffset = 0;
3213
3533
  if (options) {
@@ -3231,7 +3551,10 @@ var RulerTool = class {
3231
3551
  this.textColor = configService.get("ruler.textColor", this.textColor);
3232
3552
  this.lineColor = configService.get("ruler.lineColor", this.lineColor);
3233
3553
  this.fontSize = configService.get("ruler.fontSize", this.fontSize);
3234
- this.dielineUnit = configService.get("dieline.unit", this.dielineUnit);
3554
+ this.dielineDisplayUnit = configService.get(
3555
+ "dieline.displayUnit",
3556
+ this.dielineDisplayUnit
3557
+ );
3235
3558
  this.dielineWidth = configService.get("dieline.width", this.dielineWidth);
3236
3559
  this.dielineHeight = configService.get(
3237
3560
  "dieline.height",
@@ -3254,7 +3577,8 @@ var RulerTool = class {
3254
3577
  shouldUpdate = true;
3255
3578
  }
3256
3579
  } else if (e.key.startsWith("dieline.")) {
3257
- if (e.key === "dieline.unit") this.dielineUnit = e.value;
3580
+ if (e.key === "dieline.displayUnit")
3581
+ this.dielineDisplayUnit = e.value;
3258
3582
  if (e.key === "dieline.width") this.dielineWidth = e.value;
3259
3583
  if (e.key === "dieline.height") this.dielineHeight = e.value;
3260
3584
  if (e.key === "dieline.padding") this.dielinePadding = e.value;
@@ -3441,26 +3765,27 @@ var RulerTool = class {
3441
3765
  const width = this.canvasService.canvas.width || 800;
3442
3766
  const height = this.canvasService.canvas.height || 600;
3443
3767
  const paddingPx = this.resolvePadding(width, height);
3444
- const layout = Coordinate.calculateLayout(
3445
- { width, height },
3446
- { width: this.dielineWidth, height: this.dielineHeight },
3447
- paddingPx
3768
+ this.canvasService.viewport.setPadding(paddingPx);
3769
+ this.canvasService.viewport.updatePhysical(
3770
+ this.dielineWidth,
3771
+ this.dielineHeight
3448
3772
  );
3773
+ const layout = this.canvasService.viewport.layout;
3449
3774
  const scale = layout.scale;
3450
3775
  const offsetX = layout.offsetX;
3451
3776
  const offsetY = layout.offsetY;
3452
3777
  const visualWidth = layout.width;
3453
3778
  const visualHeight = layout.height;
3454
- const rawOffset = this.dielineOffset || 0;
3455
- const effectiveOffset = rawOffset > 0 ? rawOffset : 0;
3456
- const expandPixels = effectiveOffset * scale;
3779
+ const rawOffsetMm = this.dielineOffset || 0;
3780
+ const effectiveOffsetMm = rawOffsetMm > 0 ? rawOffsetMm : 0;
3781
+ const expandPixels = effectiveOffsetMm * scale;
3457
3782
  const gap = this.gap || 15;
3458
3783
  const rulerLeft = offsetX - expandPixels;
3459
3784
  const rulerTop = offsetY - expandPixels;
3460
3785
  const rulerRight = offsetX + visualWidth + expandPixels;
3461
3786
  const rulerBottom = offsetY + visualHeight + expandPixels;
3462
- const displayWidth = this.dielineWidth + effectiveOffset * 2;
3463
- const displayHeight = this.dielineHeight + effectiveOffset * 2;
3787
+ const displayWidthMm = this.dielineWidth + effectiveOffsetMm * 2;
3788
+ const displayHeightMm = this.dielineHeight + effectiveOffsetMm * 2;
3464
3789
  const topRulerY = rulerTop - gap;
3465
3790
  const topRulerXStart = rulerLeft;
3466
3791
  const topRulerXEnd = rulerRight;
@@ -3503,8 +3828,8 @@ var RulerTool = class {
3503
3828
  }
3504
3829
  )
3505
3830
  );
3506
- const widthStr = parseFloat(displayWidth.toFixed(2)).toString();
3507
- const topTextContent = `${widthStr} ${this.dielineUnit}`;
3831
+ const widthStr = formatMm(displayWidthMm, this.dielineDisplayUnit);
3832
+ const topTextContent = `${widthStr} ${this.dielineDisplayUnit}`;
3508
3833
  const topText = new Text(topTextContent, {
3509
3834
  left: topRulerXStart + (rulerRight - rulerLeft) / 2,
3510
3835
  top: topRulerY,
@@ -3559,8 +3884,8 @@ var RulerTool = class {
3559
3884
  }
3560
3885
  )
3561
3886
  );
3562
- const heightStr = parseFloat(displayHeight.toFixed(2)).toString();
3563
- const leftTextContent = `${heightStr} ${this.dielineUnit}`;
3887
+ const heightStr = formatMm(displayHeightMm, this.dielineDisplayUnit);
3888
+ const leftTextContent = `${heightStr} ${this.dielineDisplayUnit}`;
3564
3889
  const leftText = new Text(leftTextContent, {
3565
3890
  left: leftRulerX,
3566
3891
  top: leftRulerYStart + (rulerBottom - rulerTop) / 2,
@@ -3674,6 +3999,81 @@ var MirrorTool = class {
3674
3999
 
3675
4000
  // src/CanvasService.ts
3676
4001
  import { Canvas, Group as Group3 } from "fabric";
4002
+
4003
+ // src/ViewportSystem.ts
4004
+ var ViewportSystem = class {
4005
+ constructor(containerSize = { width: 0, height: 0 }, physicalSize = { width: 0, height: 0 }, padding = 40) {
4006
+ this._containerSize = { width: 0, height: 0 };
4007
+ this._physicalSize = { width: 0, height: 0 };
4008
+ this._padding = 0;
4009
+ this._layout = {
4010
+ scale: 1,
4011
+ offsetX: 0,
4012
+ offsetY: 0,
4013
+ width: 0,
4014
+ height: 0
4015
+ };
4016
+ this._containerSize = containerSize;
4017
+ this._physicalSize = physicalSize;
4018
+ this._padding = padding;
4019
+ this.updateLayout();
4020
+ }
4021
+ get layout() {
4022
+ return this._layout;
4023
+ }
4024
+ get scale() {
4025
+ return this._layout.scale;
4026
+ }
4027
+ get offset() {
4028
+ return { x: this._layout.offsetX, y: this._layout.offsetY };
4029
+ }
4030
+ updateContainer(width, height) {
4031
+ if (this._containerSize.width === width && this._containerSize.height === height)
4032
+ return;
4033
+ this._containerSize = { width, height };
4034
+ this.updateLayout();
4035
+ }
4036
+ updatePhysical(width, height) {
4037
+ if (this._physicalSize.width === width && this._physicalSize.height === height)
4038
+ return;
4039
+ this._physicalSize = { width, height };
4040
+ this.updateLayout();
4041
+ }
4042
+ setPadding(padding) {
4043
+ if (this._padding === padding) return;
4044
+ this._padding = padding;
4045
+ this.updateLayout();
4046
+ }
4047
+ updateLayout() {
4048
+ this._layout = Coordinate.calculateLayout(
4049
+ this._containerSize,
4050
+ this._physicalSize,
4051
+ this._padding
4052
+ );
4053
+ }
4054
+ toPixel(value) {
4055
+ return value * this._layout.scale;
4056
+ }
4057
+ toPhysical(value) {
4058
+ return this._layout.scale === 0 ? 0 : value / this._layout.scale;
4059
+ }
4060
+ toPixelPoint(point) {
4061
+ return {
4062
+ x: point.x * this._layout.scale + this._layout.offsetX,
4063
+ y: point.y * this._layout.scale + this._layout.offsetY
4064
+ };
4065
+ }
4066
+ // Convert screen coordinate (e.g. mouse event) to physical coordinate (relative to content origin)
4067
+ toPhysicalPoint(point) {
4068
+ if (this._layout.scale === 0) return { x: 0, y: 0 };
4069
+ return {
4070
+ x: (point.x - this._layout.offsetX) / this._layout.scale,
4071
+ y: (point.y - this._layout.offsetY) / this._layout.scale
4072
+ };
4073
+ }
4074
+ };
4075
+
4076
+ // src/CanvasService.ts
3677
4077
  var CanvasService = class {
3678
4078
  constructor(el, options) {
3679
4079
  if (el instanceof Canvas) {
@@ -3684,6 +4084,10 @@ var CanvasService = class {
3684
4084
  ...options
3685
4085
  });
3686
4086
  }
4087
+ this.viewport = new ViewportSystem();
4088
+ if (this.canvas.width !== void 0 && this.canvas.height !== void 0) {
4089
+ this.viewport.updateContainer(this.canvas.width, this.canvas.height);
4090
+ }
3687
4091
  if (options == null ? void 0 : options.eventBus) {
3688
4092
  this.setEventBus(options.eventBus);
3689
4093
  }
@@ -3763,5 +4167,7 @@ export {
3763
4167
  ImageTool,
3764
4168
  MirrorTool,
3765
4169
  RulerTool,
3766
- WhiteInkTool
4170
+ WhiteInkTool,
4171
+ formatMm,
4172
+ parseLengthToMm
3767
4173
  };