@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.js CHANGED
@@ -38,7 +38,9 @@ __export(index_exports, {
38
38
  ImageTool: () => ImageTool,
39
39
  MirrorTool: () => MirrorTool,
40
40
  RulerTool: () => RulerTool,
41
- WhiteInkTool: () => WhiteInkTool
41
+ WhiteInkTool: () => WhiteInkTool,
42
+ formatMm: () => formatMm,
43
+ parseLengthToMm: () => parseLengthToMm
42
44
  });
43
45
  module.exports = __toCommonJS(index_exports);
44
46
 
@@ -717,6 +719,29 @@ var Coordinate = class {
717
719
  }
718
720
  };
719
721
 
722
+ // src/units.ts
723
+ function parseLengthToMm(input, defaultUnit) {
724
+ var _a, _b;
725
+ if (typeof input === "number") {
726
+ if (!Number.isFinite(input)) return 0;
727
+ return Coordinate.convertUnit(input, defaultUnit, "mm");
728
+ }
729
+ const raw = input.trim();
730
+ if (!raw) return 0;
731
+ const match = raw.match(/^([+-]?\d+(?:\.\d+)?)\s*(px|mm|cm|in)?$/i);
732
+ if (!match) return 0;
733
+ const value = Number(match[1]);
734
+ if (!Number.isFinite(value)) return 0;
735
+ const unit = (_b = (_a = match[2]) == null ? void 0 : _a.toLowerCase()) != null ? _b : defaultUnit;
736
+ return Coordinate.convertUnit(value, unit, "mm");
737
+ }
738
+ function formatMm(valueMm, displayUnit, fractionDigits = 2) {
739
+ if (!Number.isFinite(valueMm)) return "0";
740
+ const value = Coordinate.convertUnit(valueMm, "mm", displayUnit);
741
+ const rounded = Number(value.toFixed(fractionDigits));
742
+ return rounded.toString();
743
+ }
744
+
720
745
  // src/geometry.ts
721
746
  var import_paper2 = __toESM(require("paper"));
722
747
  function resolveFeaturePosition(feature, geometry) {
@@ -798,7 +823,7 @@ function getPerimeterShape(options) {
798
823
  const { features } = options;
799
824
  if (features && features.length > 0) {
800
825
  const edgeFeatures = features.filter(
801
- (f) => !f.placement || f.placement === "edge"
826
+ (f) => !f.renderBehavior || f.renderBehavior === "edge"
802
827
  );
803
828
  const adds = [];
804
829
  const subtracts = [];
@@ -806,10 +831,78 @@ function getPerimeterShape(options) {
806
831
  const pos = resolveFeaturePosition(f, options);
807
832
  const center = new import_paper2.default.Point(pos.x, pos.y);
808
833
  const item = createFeatureItem(f, center);
809
- if (f.operation === "add") {
810
- adds.push(item);
834
+ if (f.bridge && f.bridge.type === "vertical") {
835
+ const itemBounds = item.bounds;
836
+ const mainBounds = mainShape.bounds;
837
+ const bridgeTop = mainBounds.top;
838
+ const bridgeBottom = itemBounds.top;
839
+ if (bridgeBottom > bridgeTop) {
840
+ const startY = bridgeBottom + 1;
841
+ const bridgeRect = new import_paper2.default.Path.Rectangle({
842
+ from: [itemBounds.left, bridgeTop],
843
+ to: [itemBounds.right, startY],
844
+ insert: false
845
+ });
846
+ const gaps = bridgeRect.subtract(mainShape);
847
+ bridgeRect.remove();
848
+ let bridgePart = null;
849
+ const isBottomPart = (part) => {
850
+ return Math.abs(part.bounds.bottom - startY) < 2;
851
+ };
852
+ if (gaps instanceof import_paper2.default.CompoundPath) {
853
+ const children = gaps.children;
854
+ let maxBottom = -Infinity;
855
+ let bestChild = null;
856
+ for (const child of children) {
857
+ if (child.bounds.bottom > maxBottom) {
858
+ maxBottom = child.bounds.bottom;
859
+ bestChild = child;
860
+ }
861
+ }
862
+ if (bestChild && isBottomPart(bestChild)) {
863
+ bridgePart = bestChild.clone();
864
+ }
865
+ } else if (gaps instanceof import_paper2.default.Path) {
866
+ if (isBottomPart(gaps)) {
867
+ bridgePart = gaps.clone();
868
+ }
869
+ }
870
+ gaps.remove();
871
+ if (bridgePart) {
872
+ const bounds = bridgePart.bounds;
873
+ if (bounds.height > 0) {
874
+ const overlap = 1;
875
+ const scaleY = (bounds.height + overlap) / bounds.height;
876
+ bridgePart.scale(1, scaleY, new import_paper2.default.Point(bounds.center.x, bounds.bottom));
877
+ }
878
+ const unitedItem = item.unite(bridgePart);
879
+ item.remove();
880
+ bridgePart.remove();
881
+ if (f.operation === "add") {
882
+ adds.push(unitedItem);
883
+ } else {
884
+ subtracts.push(unitedItem);
885
+ }
886
+ } else {
887
+ if (f.operation === "add") {
888
+ adds.push(item);
889
+ } else {
890
+ subtracts.push(item);
891
+ }
892
+ }
893
+ } else {
894
+ if (f.operation === "add") {
895
+ adds.push(item);
896
+ } else {
897
+ subtracts.push(item);
898
+ }
899
+ }
811
900
  } else {
812
- subtracts.push(item);
901
+ if (f.operation === "add") {
902
+ adds.push(item);
903
+ } else {
904
+ subtracts.push(item);
905
+ }
813
906
  }
814
907
  });
815
908
  if (adds.length > 0) {
@@ -842,10 +935,12 @@ function getPerimeterShape(options) {
842
935
  return mainShape;
843
936
  }
844
937
  function applySurfaceFeatures(shape, features, options) {
845
- const internalFeatures = features.filter((f) => f.placement === "internal");
846
- if (internalFeatures.length === 0) return shape;
938
+ const surfaceFeatures = features.filter(
939
+ (f) => f.renderBehavior === "surface"
940
+ );
941
+ if (surfaceFeatures.length === 0) return shape;
847
942
  let result = shape;
848
- for (const f of internalFeatures) {
943
+ for (const f of surfaceFeatures) {
849
944
  const pos = resolveFeaturePosition(f, options);
850
945
  const center = new import_paper2.default.Point(pos.x, pos.y);
851
946
  const item = createFeatureItem(f, center);
@@ -902,9 +997,17 @@ function generateBleedZonePath(originalOptions, offsetOptions, offset) {
902
997
  ensurePaper(paperWidth, paperHeight);
903
998
  import_paper2.default.project.activeLayer.removeChildren();
904
999
  const pOriginal = getPerimeterShape(originalOptions);
905
- const shapeOriginal = applySurfaceFeatures(pOriginal, originalOptions.features, originalOptions);
1000
+ const shapeOriginal = applySurfaceFeatures(
1001
+ pOriginal,
1002
+ originalOptions.features,
1003
+ originalOptions
1004
+ );
906
1005
  const pOffset = getPerimeterShape(offsetOptions);
907
- const shapeOffset = applySurfaceFeatures(pOffset, offsetOptions.features, offsetOptions);
1006
+ const shapeOffset = applySurfaceFeatures(
1007
+ pOffset,
1008
+ offsetOptions.features,
1009
+ offsetOptions
1010
+ );
908
1011
  let bleedZone;
909
1012
  if (offset > 0) {
910
1013
  bleedZone = shapeOffset.subtract(shapeOriginal);
@@ -917,13 +1020,29 @@ function generateBleedZonePath(originalOptions, offsetOptions, offset) {
917
1020
  bleedZone.remove();
918
1021
  return pathData;
919
1022
  }
1023
+ function getLowestPointOnDieline(options) {
1024
+ ensurePaper(options.width * 2, options.height * 2);
1025
+ import_paper2.default.project.activeLayer.removeChildren();
1026
+ const shape = createBaseShape(options);
1027
+ const bounds = shape.bounds;
1028
+ const result = {
1029
+ x: bounds.center.x,
1030
+ y: bounds.bottom
1031
+ };
1032
+ shape.remove();
1033
+ return result;
1034
+ }
920
1035
  function getNearestPointOnDieline(point, options) {
921
1036
  ensurePaper(options.width * 2, options.height * 2);
922
1037
  import_paper2.default.project.activeLayer.removeChildren();
923
1038
  const shape = createBaseShape(options);
924
1039
  const p = new import_paper2.default.Point(point.x, point.y);
925
- const nearest = shape.getNearestPoint(p);
926
- const result = { x: nearest.x, y: nearest.y };
1040
+ const location = shape.getNearestLocation(p);
1041
+ const result = {
1042
+ x: location.point.x,
1043
+ y: location.point.y,
1044
+ normal: location.normal ? { x: location.normal.x, y: location.normal.y } : void 0
1045
+ };
927
1046
  shape.remove();
928
1047
  return result;
929
1048
  }
@@ -940,105 +1059,6 @@ function getPathBounds(pathData) {
940
1059
  };
941
1060
  }
942
1061
 
943
- // src/constraints.ts
944
- var ConstraintRegistry = class {
945
- static register(type, handler) {
946
- this.handlers.set(type, handler);
947
- }
948
- static apply(x, y, feature, context) {
949
- if (!feature.constraints || !feature.constraints.type) {
950
- return { x, y };
951
- }
952
- const handler = this.handlers.get(feature.constraints.type);
953
- if (handler) {
954
- return handler(x, y, feature, context);
955
- }
956
- return { x, y };
957
- }
958
- };
959
- ConstraintRegistry.handlers = /* @__PURE__ */ new Map();
960
- var edgeConstraint = (x, y, feature, context) => {
961
- var _a;
962
- const { dielineWidth, dielineHeight } = context;
963
- const params = ((_a = feature.constraints) == null ? void 0 : _a.params) || {};
964
- const allowedEdges = params.allowedEdges || [
965
- "top",
966
- "bottom",
967
- "left",
968
- "right"
969
- ];
970
- const confine = params.confine || false;
971
- const offset = params.offset || 0;
972
- const distances = [];
973
- if (allowedEdges.includes("top"))
974
- distances.push({ edge: "top", dist: y * dielineHeight });
975
- if (allowedEdges.includes("bottom"))
976
- distances.push({ edge: "bottom", dist: (1 - y) * dielineHeight });
977
- if (allowedEdges.includes("left"))
978
- distances.push({ edge: "left", dist: x * dielineWidth });
979
- if (allowedEdges.includes("right"))
980
- distances.push({ edge: "right", dist: (1 - x) * dielineWidth });
981
- if (distances.length === 0) return { x, y };
982
- distances.sort((a, b) => a.dist - b.dist);
983
- const nearest = distances[0].edge;
984
- let newX = x;
985
- let newY = y;
986
- const fw = feature.width || 0;
987
- const fh = feature.height || 0;
988
- switch (nearest) {
989
- case "top":
990
- newY = 0 + offset / dielineHeight;
991
- if (confine) {
992
- const minX = fw / 2 / dielineWidth;
993
- const maxX = 1 - minX;
994
- newX = Math.max(minX, Math.min(newX, maxX));
995
- }
996
- break;
997
- case "bottom":
998
- newY = 1 - offset / dielineHeight;
999
- if (confine) {
1000
- const minX = fw / 2 / dielineWidth;
1001
- const maxX = 1 - minX;
1002
- newX = Math.max(minX, Math.min(newX, maxX));
1003
- }
1004
- break;
1005
- case "left":
1006
- newX = 0 + offset / dielineWidth;
1007
- if (confine) {
1008
- const minY = fh / 2 / dielineHeight;
1009
- const maxY = 1 - minY;
1010
- newY = Math.max(minY, Math.min(newY, maxY));
1011
- }
1012
- break;
1013
- case "right":
1014
- newX = 1 - offset / dielineWidth;
1015
- if (confine) {
1016
- const minY = fh / 2 / dielineHeight;
1017
- const maxY = 1 - minY;
1018
- newY = Math.max(minY, Math.min(newY, maxY));
1019
- }
1020
- break;
1021
- }
1022
- return { x: newX, y: newY };
1023
- };
1024
- var internalConstraint = (x, y, feature, context) => {
1025
- var _a;
1026
- const { dielineWidth, dielineHeight } = context;
1027
- const params = ((_a = feature.constraints) == null ? void 0 : _a.params) || {};
1028
- const margin = params.margin || 0;
1029
- const fw = feature.width || 0;
1030
- const fh = feature.height || 0;
1031
- const minX = (margin + fw / 2) / dielineWidth;
1032
- const maxX = 1 - (margin + fw / 2) / dielineWidth;
1033
- const minY = (margin + fh / 2) / dielineHeight;
1034
- const maxY = 1 - (margin + fh / 2) / dielineHeight;
1035
- const clampedX = minX > maxX ? 0.5 : Math.max(minX, Math.min(x, maxX));
1036
- const clampedY = minY > maxY ? 0.5 : Math.max(minY, Math.min(y, maxY));
1037
- return { x: clampedX, y: clampedY };
1038
- };
1039
- ConstraintRegistry.register("edge", edgeConstraint);
1040
- ConstraintRegistry.register("internal", internalConstraint);
1041
-
1042
1062
  // src/dieline.ts
1043
1063
  var DielineTool = class {
1044
1064
  constructor(options) {
@@ -1047,7 +1067,7 @@ var DielineTool = class {
1047
1067
  name: "DielineTool"
1048
1068
  };
1049
1069
  this.state = {
1050
- unit: "mm",
1070
+ displayUnit: "mm",
1051
1071
  shape: "rect",
1052
1072
  width: 500,
1053
1073
  height: 500,
@@ -1093,50 +1113,88 @@ var DielineTool = class {
1093
1113
  const configService = context.services.get("ConfigurationService");
1094
1114
  if (configService) {
1095
1115
  const s = this.state;
1096
- s.unit = configService.get("dieline.unit", s.unit);
1116
+ s.displayUnit = configService.get("dieline.displayUnit", s.displayUnit);
1097
1117
  s.shape = configService.get("dieline.shape", s.shape);
1098
- s.width = configService.get("dieline.width", s.width);
1099
- s.height = configService.get("dieline.height", s.height);
1100
- s.radius = configService.get("dieline.radius", s.radius);
1118
+ s.width = parseLengthToMm(
1119
+ configService.get("dieline.width", s.width),
1120
+ "mm"
1121
+ );
1122
+ s.height = parseLengthToMm(
1123
+ configService.get("dieline.height", s.height),
1124
+ "mm"
1125
+ );
1126
+ s.radius = parseLengthToMm(
1127
+ configService.get("dieline.radius", s.radius),
1128
+ "mm"
1129
+ );
1101
1130
  s.padding = configService.get("dieline.padding", s.padding);
1102
- s.offset = configService.get("dieline.offset", s.offset);
1103
- s.mainLine.width = configService.get("dieline.strokeWidth", s.mainLine.width);
1104
- s.mainLine.color = configService.get("dieline.strokeColor", s.mainLine.color);
1105
- s.mainLine.dashLength = configService.get("dieline.dashLength", s.mainLine.dashLength);
1131
+ s.offset = parseLengthToMm(
1132
+ configService.get("dieline.offset", s.offset),
1133
+ "mm"
1134
+ );
1135
+ s.mainLine.width = configService.get(
1136
+ "dieline.strokeWidth",
1137
+ s.mainLine.width
1138
+ );
1139
+ s.mainLine.color = configService.get(
1140
+ "dieline.strokeColor",
1141
+ s.mainLine.color
1142
+ );
1143
+ s.mainLine.dashLength = configService.get(
1144
+ "dieline.dashLength",
1145
+ s.mainLine.dashLength
1146
+ );
1106
1147
  s.mainLine.style = configService.get("dieline.style", s.mainLine.style);
1107
- s.offsetLine.width = configService.get("dieline.offsetStrokeWidth", s.offsetLine.width);
1108
- s.offsetLine.color = configService.get("dieline.offsetStrokeColor", s.offsetLine.color);
1109
- s.offsetLine.dashLength = configService.get("dieline.offsetDashLength", s.offsetLine.dashLength);
1110
- s.offsetLine.style = configService.get("dieline.offsetStyle", s.offsetLine.style);
1148
+ s.offsetLine.width = configService.get(
1149
+ "dieline.offsetStrokeWidth",
1150
+ s.offsetLine.width
1151
+ );
1152
+ s.offsetLine.color = configService.get(
1153
+ "dieline.offsetStrokeColor",
1154
+ s.offsetLine.color
1155
+ );
1156
+ s.offsetLine.dashLength = configService.get(
1157
+ "dieline.offsetDashLength",
1158
+ s.offsetLine.dashLength
1159
+ );
1160
+ s.offsetLine.style = configService.get(
1161
+ "dieline.offsetStyle",
1162
+ s.offsetLine.style
1163
+ );
1111
1164
  s.insideColor = configService.get("dieline.insideColor", s.insideColor);
1112
- s.outsideColor = configService.get("dieline.outsideColor", s.outsideColor);
1113
- s.showBleedLines = configService.get("dieline.showBleedLines", s.showBleedLines);
1165
+ s.outsideColor = configService.get(
1166
+ "dieline.outsideColor",
1167
+ s.outsideColor
1168
+ );
1169
+ s.showBleedLines = configService.get(
1170
+ "dieline.showBleedLines",
1171
+ s.showBleedLines
1172
+ );
1114
1173
  s.features = configService.get("dieline.features", s.features);
1115
1174
  s.pathData = configService.get("dieline.pathData", s.pathData);
1116
1175
  configService.onAnyChange((e) => {
1117
1176
  if (e.key.startsWith("dieline.")) {
1118
- console.log(`[DielineTool] Config change detected: ${e.key} -> ${e.value}`);
1119
1177
  switch (e.key) {
1120
- case "dieline.unit":
1121
- s.unit = e.value;
1178
+ case "dieline.displayUnit":
1179
+ s.displayUnit = e.value;
1122
1180
  break;
1123
1181
  case "dieline.shape":
1124
1182
  s.shape = e.value;
1125
1183
  break;
1126
1184
  case "dieline.width":
1127
- s.width = e.value;
1185
+ s.width = parseLengthToMm(e.value, "mm");
1128
1186
  break;
1129
1187
  case "dieline.height":
1130
- s.height = e.value;
1188
+ s.height = parseLengthToMm(e.value, "mm");
1131
1189
  break;
1132
1190
  case "dieline.radius":
1133
- s.radius = e.value;
1191
+ s.radius = parseLengthToMm(e.value, "mm");
1134
1192
  break;
1135
1193
  case "dieline.padding":
1136
1194
  s.padding = e.value;
1137
1195
  break;
1138
1196
  case "dieline.offset":
1139
- s.offset = e.value;
1197
+ s.offset = parseLengthToMm(e.value, "mm");
1140
1198
  break;
1141
1199
  case "dieline.strokeWidth":
1142
1200
  s.mainLine.width = e.value;
@@ -1195,11 +1253,11 @@ var DielineTool = class {
1195
1253
  return {
1196
1254
  [import_core2.ContributionPointIds.CONFIGURATIONS]: [
1197
1255
  {
1198
- id: "dieline.unit",
1256
+ id: "dieline.displayUnit",
1199
1257
  type: "select",
1200
- label: "Unit",
1201
- options: ["px", "mm", "cm", "in"],
1202
- default: s.unit
1258
+ label: "Display Unit",
1259
+ options: ["mm", "cm", "in"],
1260
+ default: s.displayUnit
1203
1261
  },
1204
1262
  {
1205
1263
  id: "dieline.shape",
@@ -1211,7 +1269,7 @@ var DielineTool = class {
1211
1269
  {
1212
1270
  id: "dieline.width",
1213
1271
  type: "number",
1214
- label: "Width",
1272
+ label: "Width (mm)",
1215
1273
  min: 10,
1216
1274
  max: 2e3,
1217
1275
  default: s.width
@@ -1219,7 +1277,7 @@ var DielineTool = class {
1219
1277
  {
1220
1278
  id: "dieline.height",
1221
1279
  type: "number",
1222
- label: "Height",
1280
+ label: "Height (mm)",
1223
1281
  min: 10,
1224
1282
  max: 2e3,
1225
1283
  default: s.height
@@ -1227,7 +1285,7 @@ var DielineTool = class {
1227
1285
  {
1228
1286
  id: "dieline.radius",
1229
1287
  type: "number",
1230
- label: "Corner Radius",
1288
+ label: "Corner Radius (mm)",
1231
1289
  min: 0,
1232
1290
  max: 500,
1233
1291
  default: s.radius
@@ -1242,7 +1300,7 @@ var DielineTool = class {
1242
1300
  {
1243
1301
  id: "dieline.offset",
1244
1302
  type: "number",
1245
- label: "Bleed Offset",
1303
+ label: "Bleed Offset (mm)",
1246
1304
  min: -100,
1247
1305
  max: 100,
1248
1306
  default: s.offset
@@ -1343,18 +1401,12 @@ var DielineTool = class {
1343
1401
  );
1344
1402
  if (!configService) return;
1345
1403
  const features = configService.get("dieline.features") || [];
1346
- const dielineWidth = configService.get("dieline.width") || 500;
1347
- const dielineHeight = configService.get("dieline.height") || 500;
1348
1404
  let changed = false;
1349
1405
  const newFeatures = features.map((f) => {
1350
1406
  if (f.groupId === groupId) {
1351
- const constrained = ConstraintRegistry.apply(x, y, f, {
1352
- dielineWidth,
1353
- dielineHeight
1354
- });
1355
- if (f.x !== constrained.x || f.y !== constrained.y) {
1407
+ if (f.x !== x || f.y !== y) {
1356
1408
  changed = true;
1357
- return { ...f, x: constrained.x, y: constrained.y };
1409
+ return { ...f, x, y };
1358
1410
  }
1359
1411
  }
1360
1412
  return f;
@@ -1469,7 +1521,7 @@ var DielineTool = class {
1469
1521
  const layer = this.getLayer();
1470
1522
  if (!layer) return;
1471
1523
  const {
1472
- unit,
1524
+ displayUnit,
1473
1525
  shape,
1474
1526
  radius,
1475
1527
  offset,
@@ -1480,15 +1532,13 @@ var DielineTool = class {
1480
1532
  showBleedLines,
1481
1533
  features
1482
1534
  } = this.state;
1483
- let { width, height } = this.state;
1535
+ const { width, height } = this.state;
1484
1536
  const canvasW = this.canvasService.canvas.width || 800;
1485
1537
  const canvasH = this.canvasService.canvas.height || 600;
1486
1538
  const paddingPx = this.resolvePadding(canvasW, canvasH);
1487
- const layout = Coordinate.calculateLayout(
1488
- { width: canvasW, height: canvasH },
1489
- { width, height },
1490
- paddingPx
1491
- );
1539
+ this.canvasService.viewport.setPadding(paddingPx);
1540
+ this.canvasService.viewport.updatePhysical(width, height);
1541
+ const layout = this.canvasService.viewport.layout;
1492
1542
  const scale = layout.scale;
1493
1543
  const cx = layout.offsetX + layout.width / 2;
1494
1544
  const cy = layout.offsetY + layout.height / 2;
@@ -1675,15 +1725,22 @@ var DielineTool = class {
1675
1725
  }
1676
1726
  getGeometry() {
1677
1727
  if (!this.canvasService) return null;
1678
- const { unit, shape, width, height, radius, offset, mainLine, pathData } = this.state;
1728
+ const {
1729
+ displayUnit,
1730
+ shape,
1731
+ width,
1732
+ height,
1733
+ radius,
1734
+ offset,
1735
+ mainLine,
1736
+ pathData
1737
+ } = this.state;
1679
1738
  const canvasW = this.canvasService.canvas.width || 800;
1680
1739
  const canvasH = this.canvasService.canvas.height || 600;
1681
1740
  const paddingPx = this.resolvePadding(canvasW, canvasH);
1682
- const layout = Coordinate.calculateLayout(
1683
- { width: canvasW, height: canvasH },
1684
- { width, height },
1685
- paddingPx
1686
- );
1741
+ this.canvasService.viewport.setPadding(paddingPx);
1742
+ this.canvasService.viewport.updatePhysical(width, height);
1743
+ const layout = this.canvasService.viewport.layout;
1687
1744
  const scale = layout.scale;
1688
1745
  const cx = layout.offsetX + layout.width / 2;
1689
1746
  const cy = layout.offsetY + layout.height / 2;
@@ -1691,14 +1748,14 @@ var DielineTool = class {
1691
1748
  const visualHeight = layout.height;
1692
1749
  return {
1693
1750
  shape,
1694
- unit,
1751
+ unit: "mm",
1752
+ displayUnit,
1695
1753
  x: cx,
1696
1754
  y: cy,
1697
1755
  width: visualWidth,
1698
1756
  height: visualHeight,
1699
1757
  radius: radius * scale,
1700
1758
  offset: offset * scale,
1701
- // Pass scale to help other tools (like FeatureTool) convert units
1702
1759
  scale,
1703
1760
  strokeWidth: mainLine.width,
1704
1761
  pathData
@@ -1708,15 +1765,13 @@ var DielineTool = class {
1708
1765
  if (!this.canvasService) return null;
1709
1766
  const userLayer = this.canvasService.getLayer("user");
1710
1767
  if (!userLayer) return null;
1711
- const { shape, width, height, radius, features, unit, pathData } = this.state;
1768
+ const { shape, width, height, radius, features, pathData } = this.state;
1712
1769
  const canvasW = this.canvasService.canvas.width || 800;
1713
1770
  const canvasH = this.canvasService.canvas.height || 600;
1714
1771
  const paddingPx = this.resolvePadding(canvasW, canvasH);
1715
- const layout = Coordinate.calculateLayout(
1716
- { width: canvasW, height: canvasH },
1717
- { width, height },
1718
- paddingPx
1719
- );
1772
+ this.canvasService.viewport.setPadding(paddingPx);
1773
+ this.canvasService.viewport.updatePhysical(width, height);
1774
+ const layout = this.canvasService.viewport.layout;
1720
1775
  const scale = layout.scale;
1721
1776
  const cx = layout.offsetX + layout.width / 2;
1722
1777
  const cy = layout.offsetY + layout.height / 2;
@@ -1931,13 +1986,212 @@ var FilmTool = class {
1931
1986
  // src/feature.ts
1932
1987
  var import_core4 = require("@pooder/core");
1933
1988
  var import_fabric4 = require("fabric");
1989
+
1990
+ // src/constraints.ts
1991
+ var ConstraintRegistry = class {
1992
+ static register(type, handler) {
1993
+ this.handlers.set(type, handler);
1994
+ }
1995
+ static apply(x, y, feature, context, constraints) {
1996
+ const list = constraints || feature.constraints;
1997
+ if (!list || list.length === 0) {
1998
+ return { x, y };
1999
+ }
2000
+ let currentX = x;
2001
+ let currentY = y;
2002
+ for (const constraint of list) {
2003
+ const handler = this.handlers.get(constraint.type);
2004
+ if (handler) {
2005
+ const result = handler(currentX, currentY, feature, context, constraint.params || {});
2006
+ currentX = result.x;
2007
+ currentY = result.y;
2008
+ }
2009
+ }
2010
+ return { x: currentX, y: currentY };
2011
+ }
2012
+ };
2013
+ ConstraintRegistry.handlers = /* @__PURE__ */ new Map();
2014
+ var pathConstraint = (x, y, feature, context, params) => {
2015
+ const { dielineWidth, dielineHeight, geometry } = context;
2016
+ if (!geometry) return { x, y };
2017
+ const minX = geometry.x - geometry.width / 2;
2018
+ const minY = geometry.y - geometry.height / 2;
2019
+ const absX = minX + x * geometry.width;
2020
+ const absY = minY + y * geometry.height;
2021
+ const nearest = getNearestPointOnDieline(
2022
+ { x: absX, y: absY },
2023
+ geometry
2024
+ );
2025
+ let finalX = nearest.x;
2026
+ let finalY = nearest.y;
2027
+ const hasOffsetParams = params.minOffset !== void 0 || params.maxOffset !== void 0;
2028
+ if (hasOffsetParams && nearest.normal) {
2029
+ const dx = absX - nearest.x;
2030
+ const dy = absY - nearest.y;
2031
+ const nx2 = nearest.normal.x;
2032
+ const ny2 = nearest.normal.y;
2033
+ const dist = dx * nx2 + dy * ny2;
2034
+ const scale = dielineWidth > 0 ? geometry.width / dielineWidth : 1;
2035
+ const rawMin = params.minOffset !== void 0 ? params.minOffset : 0;
2036
+ const rawMax = params.maxOffset !== void 0 ? params.maxOffset : 0;
2037
+ const minOffset = rawMin * scale;
2038
+ const maxOffset = rawMax * scale;
2039
+ const clampedDist = Math.max(minOffset, Math.min(dist, maxOffset));
2040
+ finalX = nearest.x + nx2 * clampedDist;
2041
+ finalY = nearest.y + ny2 * clampedDist;
2042
+ }
2043
+ const nx = geometry.width > 0 ? (finalX - minX) / geometry.width : 0.5;
2044
+ const ny = geometry.height > 0 ? (finalY - minY) / geometry.height : 0.5;
2045
+ return { x: nx, y: ny };
2046
+ };
2047
+ var edgeConstraint = (x, y, feature, context, params) => {
2048
+ const { dielineWidth, dielineHeight } = context;
2049
+ const allowedEdges = params.allowedEdges || [
2050
+ "top",
2051
+ "bottom",
2052
+ "left",
2053
+ "right"
2054
+ ];
2055
+ const confine = params.confine || false;
2056
+ const offset = params.offset || 0;
2057
+ const distances = [];
2058
+ if (allowedEdges.includes("top"))
2059
+ distances.push({ edge: "top", dist: y * dielineHeight });
2060
+ if (allowedEdges.includes("bottom"))
2061
+ distances.push({ edge: "bottom", dist: (1 - y) * dielineHeight });
2062
+ if (allowedEdges.includes("left"))
2063
+ distances.push({ edge: "left", dist: x * dielineWidth });
2064
+ if (allowedEdges.includes("right"))
2065
+ distances.push({ edge: "right", dist: (1 - x) * dielineWidth });
2066
+ if (distances.length === 0) return { x, y };
2067
+ distances.sort((a, b) => a.dist - b.dist);
2068
+ const nearest = distances[0].edge;
2069
+ let newX = x;
2070
+ let newY = y;
2071
+ const fw = feature.width || 0;
2072
+ const fh = feature.height || 0;
2073
+ switch (nearest) {
2074
+ case "top":
2075
+ newY = 0 + offset / dielineHeight;
2076
+ if (confine) {
2077
+ const minX = fw / 2 / dielineWidth;
2078
+ const maxX = 1 - minX;
2079
+ newX = Math.max(minX, Math.min(newX, maxX));
2080
+ }
2081
+ break;
2082
+ case "bottom":
2083
+ newY = 1 - offset / dielineHeight;
2084
+ if (confine) {
2085
+ const minX = fw / 2 / dielineWidth;
2086
+ const maxX = 1 - minX;
2087
+ newX = Math.max(minX, Math.min(newX, maxX));
2088
+ }
2089
+ break;
2090
+ case "left":
2091
+ newX = 0 + offset / dielineWidth;
2092
+ if (confine) {
2093
+ const minY = fh / 2 / dielineHeight;
2094
+ const maxY = 1 - minY;
2095
+ newY = Math.max(minY, Math.min(newY, maxY));
2096
+ }
2097
+ break;
2098
+ case "right":
2099
+ newX = 1 - offset / dielineWidth;
2100
+ if (confine) {
2101
+ const minY = fh / 2 / dielineHeight;
2102
+ const maxY = 1 - minY;
2103
+ newY = Math.max(minY, Math.min(newY, maxY));
2104
+ }
2105
+ break;
2106
+ }
2107
+ return { x: newX, y: newY };
2108
+ };
2109
+ var internalConstraint = (x, y, feature, context, params) => {
2110
+ const { dielineWidth, dielineHeight } = context;
2111
+ const margin = params.margin || 0;
2112
+ const fw = feature.width || 0;
2113
+ const fh = feature.height || 0;
2114
+ const minX = (margin + fw / 2) / dielineWidth;
2115
+ const maxX = 1 - (margin + fw / 2) / dielineWidth;
2116
+ const minY = (margin + fh / 2) / dielineHeight;
2117
+ const maxY = 1 - (margin + fh / 2) / dielineHeight;
2118
+ const clampedX = minX > maxX ? 0.5 : Math.max(minX, Math.min(x, maxX));
2119
+ const clampedY = minY > maxY ? 0.5 : Math.max(minY, Math.min(y, maxY));
2120
+ return { x: clampedX, y: clampedY };
2121
+ };
2122
+ var tangentBottomConstraint = (x, y, feature, context, params) => {
2123
+ const { dielineWidth, dielineHeight } = context;
2124
+ const gap = params.gap || 0;
2125
+ const confineX = params.confineX !== false;
2126
+ const extentY = feature.shape === "circle" ? feature.radius || 0 : (feature.height || 0) / 2;
2127
+ const newY = 1 + (extentY + gap) / dielineHeight;
2128
+ let newX = x;
2129
+ if (confineX) {
2130
+ const extentX = feature.shape === "circle" ? feature.radius || 0 : (feature.width || 0) / 2;
2131
+ const minX = extentX / dielineWidth;
2132
+ const maxX = 1 - extentX / dielineWidth;
2133
+ newX = minX > maxX ? 0.5 : Math.max(minX, Math.min(newX, maxX));
2134
+ }
2135
+ return { x: newX, y: newY };
2136
+ };
2137
+ var lowestTangentConstraint = (x, y, feature, context, params) => {
2138
+ const { dielineWidth, dielineHeight, geometry } = context;
2139
+ if (!geometry) return { x, y };
2140
+ const lowest = getLowestPointOnDieline(geometry);
2141
+ const minY = geometry.y - geometry.height / 2;
2142
+ const normY = (lowest.y - minY) / geometry.height;
2143
+ const gap = params.gap || 0;
2144
+ const confineX = params.confineX !== false;
2145
+ const extentY = feature.shape === "circle" ? feature.radius || 0 : (feature.height || 0) / 2;
2146
+ const newY = normY + (extentY + gap) / dielineHeight;
2147
+ let newX = x;
2148
+ if (confineX) {
2149
+ const extentX = feature.shape === "circle" ? feature.radius || 0 : (feature.width || 0) / 2;
2150
+ const minX = extentX / dielineWidth;
2151
+ const maxX = 1 - extentX / dielineWidth;
2152
+ newX = minX > maxX ? 0.5 : Math.max(minX, Math.min(newX, maxX));
2153
+ }
2154
+ return { x: newX, y: newY };
2155
+ };
2156
+ ConstraintRegistry.register("path", pathConstraint);
2157
+ ConstraintRegistry.register("edge", edgeConstraint);
2158
+ ConstraintRegistry.register("internal", internalConstraint);
2159
+ ConstraintRegistry.register("tangent-bottom", tangentBottomConstraint);
2160
+ ConstraintRegistry.register("lowest-tangent", lowestTangentConstraint);
2161
+
2162
+ // src/featureComplete.ts
2163
+ function validateFeaturesStrict(features, context) {
2164
+ const eps = 1e-6;
2165
+ const issues = [];
2166
+ for (const f of features) {
2167
+ if (!f.constraints || f.constraints.length === 0) continue;
2168
+ const constrained = ConstraintRegistry.apply(f.x, f.y, f, context, f.constraints);
2169
+ if (Math.abs(constrained.x - f.x) > eps || Math.abs(constrained.y - f.y) > eps) {
2170
+ issues.push({
2171
+ featureId: f.id,
2172
+ groupId: f.groupId,
2173
+ reason: "Position violates constraint strategy"
2174
+ });
2175
+ }
2176
+ }
2177
+ return { ok: issues.length === 0, issues: issues.length ? issues : void 0 };
2178
+ }
2179
+ function completeFeaturesStrict(features, context, update) {
2180
+ const validation = validateFeaturesStrict(features, context);
2181
+ if (!validation.ok) return validation;
2182
+ const next = JSON.parse(JSON.stringify(features || []));
2183
+ update(next);
2184
+ return { ok: true };
2185
+ }
2186
+
2187
+ // src/feature.ts
1934
2188
  var FeatureTool = class {
1935
2189
  constructor(options) {
1936
2190
  this.id = "pooder.kit.feature";
1937
2191
  this.metadata = {
1938
2192
  name: "FeatureTool"
1939
2193
  };
1940
- this.features = [];
2194
+ this.workingFeatures = [];
1941
2195
  this.isUpdatingConfig = false;
1942
2196
  this.isToolActive = false;
1943
2197
  this.handleMoving = null;
@@ -1963,12 +2217,15 @@ var FeatureTool = class {
1963
2217
  "ConfigurationService"
1964
2218
  );
1965
2219
  if (configService) {
1966
- this.features = configService.get("dieline.features", []);
2220
+ const features = configService.get("dieline.features", []) || [];
2221
+ this.workingFeatures = this.cloneFeatures(features);
1967
2222
  configService.onAnyChange((e) => {
1968
2223
  if (this.isUpdatingConfig) return;
1969
2224
  if (e.key === "dieline.features") {
1970
- this.features = e.value || [];
2225
+ const next = e.value || [];
2226
+ this.workingFeatures = this.cloneFeatures(next);
1971
2227
  this.redraw();
2228
+ this.emitWorkingChange();
1972
2229
  }
1973
2230
  });
1974
2231
  }
@@ -2026,57 +2283,172 @@ var FeatureTool = class {
2026
2283
  command: "clearFeatures",
2027
2284
  title: "Clear Features",
2028
2285
  handler: () => {
2029
- var _a;
2030
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
2031
- "ConfigurationService"
2032
- );
2033
- if (configService) {
2034
- configService.update("dieline.features", []);
2035
- }
2286
+ this.setWorkingFeatures([]);
2287
+ this.redraw();
2288
+ this.emitWorkingChange();
2036
2289
  return true;
2037
2290
  }
2291
+ },
2292
+ {
2293
+ command: "getWorkingFeatures",
2294
+ title: "Get Working Features",
2295
+ handler: () => {
2296
+ return this.cloneFeatures(this.workingFeatures);
2297
+ }
2298
+ },
2299
+ {
2300
+ command: "setWorkingFeatures",
2301
+ title: "Set Working Features",
2302
+ handler: async (features) => {
2303
+ await this.refreshGeometry();
2304
+ this.setWorkingFeatures(this.cloneFeatures(features || []));
2305
+ this.redraw();
2306
+ this.emitWorkingChange();
2307
+ return { ok: true };
2308
+ }
2309
+ },
2310
+ {
2311
+ command: "updateWorkingGroupPosition",
2312
+ title: "Update Working Group Position",
2313
+ handler: (groupId, x, y) => {
2314
+ return this.updateWorkingGroupPosition(groupId, x, y);
2315
+ }
2316
+ },
2317
+ {
2318
+ command: "completeFeatures",
2319
+ title: "Complete Features",
2320
+ handler: () => {
2321
+ return this.completeFeatures();
2322
+ }
2038
2323
  }
2039
2324
  ]
2040
2325
  };
2041
2326
  }
2042
- addFeature(type) {
2327
+ cloneFeatures(features) {
2328
+ return JSON.parse(JSON.stringify(features || []));
2329
+ }
2330
+ emitWorkingChange() {
2043
2331
  var _a;
2044
- if (!this.canvasService) return false;
2045
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
2046
- "ConfigurationService"
2332
+ (_a = this.context) == null ? void 0 : _a.eventBus.emit("feature:working:change", {
2333
+ features: this.cloneFeatures(this.workingFeatures)
2334
+ });
2335
+ }
2336
+ async refreshGeometry() {
2337
+ if (!this.context) return;
2338
+ const commandService = this.context.services.get("CommandService");
2339
+ if (!commandService) return;
2340
+ try {
2341
+ const g = await Promise.resolve(commandService.executeCommand("getGeometry"));
2342
+ if (g) this.currentGeometry = g;
2343
+ } catch (e) {
2344
+ }
2345
+ }
2346
+ setWorkingFeatures(next) {
2347
+ this.workingFeatures = next;
2348
+ }
2349
+ updateWorkingGroupPosition(groupId, x, y) {
2350
+ var _a, _b, _c;
2351
+ if (!groupId) return { ok: false };
2352
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get("ConfigurationService");
2353
+ if (!configService) return { ok: false };
2354
+ const dielineWidth = parseLengthToMm(
2355
+ (_b = configService.get("dieline.width")) != null ? _b : 500,
2356
+ "mm"
2357
+ );
2358
+ const dielineHeight = parseLengthToMm(
2359
+ (_c = configService.get("dieline.height")) != null ? _c : 500,
2360
+ "mm"
2361
+ );
2362
+ let changed = false;
2363
+ const next = this.workingFeatures.map((f) => {
2364
+ if (f.groupId !== groupId) return f;
2365
+ let nx = x;
2366
+ let ny = y;
2367
+ if (f.constraints && dielineWidth > 0 && dielineHeight > 0) {
2368
+ const constrained = ConstraintRegistry.apply(nx, ny, f, {
2369
+ dielineWidth,
2370
+ dielineHeight
2371
+ });
2372
+ nx = constrained.x;
2373
+ ny = constrained.y;
2374
+ }
2375
+ if (f.x !== nx || f.y !== ny) {
2376
+ changed = true;
2377
+ return { ...f, x: nx, y: ny };
2378
+ }
2379
+ return f;
2380
+ });
2381
+ if (!changed) return { ok: true };
2382
+ this.setWorkingFeatures(next);
2383
+ this.redraw();
2384
+ this.enforceConstraints();
2385
+ this.emitWorkingChange();
2386
+ return { ok: true };
2387
+ }
2388
+ completeFeatures() {
2389
+ var _a, _b, _c;
2390
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get("ConfigurationService");
2391
+ if (!configService) {
2392
+ return {
2393
+ ok: false,
2394
+ issues: [
2395
+ { featureId: "unknown", reason: "ConfigurationService not found" }
2396
+ ]
2397
+ };
2398
+ }
2399
+ const dielineWidth = parseLengthToMm(
2400
+ (_b = configService.get("dieline.width")) != null ? _b : 500,
2401
+ "mm"
2047
2402
  );
2048
- const unit = (configService == null ? void 0 : configService.get("dieline.unit", "mm")) || "mm";
2049
- const defaultSize = Coordinate.convertUnit(10, "mm", unit);
2403
+ const dielineHeight = parseLengthToMm(
2404
+ (_c = configService.get("dieline.height")) != null ? _c : 500,
2405
+ "mm"
2406
+ );
2407
+ const result = completeFeaturesStrict(
2408
+ this.workingFeatures,
2409
+ { dielineWidth, dielineHeight },
2410
+ (next) => {
2411
+ this.isUpdatingConfig = true;
2412
+ try {
2413
+ configService.update("dieline.features", next);
2414
+ } finally {
2415
+ this.isUpdatingConfig = false;
2416
+ }
2417
+ this.workingFeatures = this.cloneFeatures(next);
2418
+ this.emitWorkingChange();
2419
+ }
2420
+ );
2421
+ if (!result.ok) {
2422
+ return {
2423
+ ok: false,
2424
+ issues: result.issues
2425
+ };
2426
+ }
2427
+ return { ok: true };
2428
+ }
2429
+ addFeature(type) {
2430
+ if (!this.canvasService) return false;
2050
2431
  const newFeature = {
2051
2432
  id: Date.now().toString(),
2052
2433
  operation: type,
2053
- placement: "edge",
2054
2434
  shape: "rect",
2055
2435
  x: 0.5,
2056
2436
  y: 0,
2057
2437
  // Top edge
2058
- width: defaultSize,
2059
- height: defaultSize,
2060
- rotation: 0
2438
+ width: 10,
2439
+ height: 10,
2440
+ rotation: 0,
2441
+ renderBehavior: "edge",
2442
+ // Default constraint: path (snap to edge)
2443
+ constraints: [{ type: "path" }]
2061
2444
  };
2062
- if (configService) {
2063
- const current = configService.get(
2064
- "dieline.features",
2065
- []
2066
- );
2067
- configService.update("dieline.features", [...current, newFeature]);
2068
- }
2445
+ this.setWorkingFeatures([...this.workingFeatures || [], newFeature]);
2446
+ this.redraw();
2447
+ this.emitWorkingChange();
2069
2448
  return true;
2070
2449
  }
2071
2450
  addDoubleLayerHole() {
2072
- var _a;
2073
2451
  if (!this.canvasService) return false;
2074
- const configService = (_a = this.context) == null ? void 0 : _a.services.get(
2075
- "ConfigurationService"
2076
- );
2077
- const unit = (configService == null ? void 0 : configService.get("dieline.unit", "mm")) || "mm";
2078
- const lugRadius = Coordinate.convertUnit(20, "mm", unit);
2079
- const holeRadius = Coordinate.convertUnit(15, "mm", unit);
2080
2452
  const groupId = Date.now().toString();
2081
2453
  const timestamp = Date.now();
2082
2454
  const lug = {
@@ -2084,32 +2456,28 @@ var FeatureTool = class {
2084
2456
  groupId,
2085
2457
  operation: "add",
2086
2458
  shape: "circle",
2087
- placement: "edge",
2088
2459
  x: 0.5,
2089
2460
  y: 0,
2090
- radius: lugRadius,
2091
- // 20mm
2092
- rotation: 0
2461
+ radius: 20,
2462
+ rotation: 0,
2463
+ renderBehavior: "edge",
2464
+ constraints: [{ type: "path" }]
2093
2465
  };
2094
2466
  const hole = {
2095
2467
  id: `${timestamp}-hole`,
2096
2468
  groupId,
2097
2469
  operation: "subtract",
2098
2470
  shape: "circle",
2099
- placement: "edge",
2100
2471
  x: 0.5,
2101
2472
  y: 0,
2102
- radius: holeRadius,
2103
- // 15mm
2104
- rotation: 0
2473
+ radius: 15,
2474
+ rotation: 0,
2475
+ renderBehavior: "edge",
2476
+ constraints: [{ type: "path" }]
2105
2477
  };
2106
- if (configService) {
2107
- const current = configService.get(
2108
- "dieline.features",
2109
- []
2110
- );
2111
- configService.update("dieline.features", [...current, lug, hole]);
2112
- }
2478
+ this.setWorkingFeatures([...this.workingFeatures || [], lug, hole]);
2479
+ this.redraw();
2480
+ this.emitWorkingChange();
2113
2481
  return true;
2114
2482
  }
2115
2483
  getGeometryForFeature(geometry, feature) {
@@ -2153,12 +2521,12 @@ var FeatureTool = class {
2153
2521
  if ((_b = target.data) == null ? void 0 : _b.isGroup) {
2154
2522
  const indices = (_c = target.data) == null ? void 0 : _c.indices;
2155
2523
  if (indices && indices.length > 0) {
2156
- feature = this.features[indices[0]];
2524
+ feature = this.workingFeatures[indices[0]];
2157
2525
  }
2158
2526
  } else {
2159
2527
  const index = (_d = target.data) == null ? void 0 : _d.index;
2160
2528
  if (index !== void 0) {
2161
- feature = this.features[index];
2529
+ feature = this.workingFeatures[index];
2162
2530
  }
2163
2531
  }
2164
2532
  const geometry = this.getGeometryForFeature(
@@ -2179,7 +2547,7 @@ var FeatureTool = class {
2179
2547
  }
2180
2548
  if (!this.handleModified) {
2181
2549
  this.handleModified = (e) => {
2182
- var _a, _b, _c, _d;
2550
+ var _a, _b, _c;
2183
2551
  const target = e.target;
2184
2552
  if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "feature-marker") return;
2185
2553
  if ((_b = target.data) == null ? void 0 : _b.isGroup) {
@@ -2187,11 +2555,11 @@ var FeatureTool = class {
2187
2555
  const indices = (_c = groupObj.data) == null ? void 0 : _c.indices;
2188
2556
  if (!indices) return;
2189
2557
  const groupCenter = new import_fabric4.Point(groupObj.left, groupObj.top);
2190
- const newFeatures = [...this.features];
2558
+ const newFeatures = [...this.workingFeatures];
2191
2559
  const { x, y } = this.currentGeometry;
2192
2560
  groupObj.getObjects().forEach((child, i) => {
2193
2561
  const originalIndex = indices[i];
2194
- const feature = this.features[originalIndex];
2562
+ const feature = this.workingFeatures[originalIndex];
2195
2563
  const geometry = this.getGeometryForFeature(
2196
2564
  this.currentGeometry,
2197
2565
  feature
@@ -2209,18 +2577,8 @@ var FeatureTool = class {
2209
2577
  y: normalizedY
2210
2578
  };
2211
2579
  });
2212
- this.features = newFeatures;
2213
- const configService = (_d = this.context) == null ? void 0 : _d.services.get(
2214
- "ConfigurationService"
2215
- );
2216
- if (configService) {
2217
- this.isUpdatingConfig = true;
2218
- try {
2219
- configService.update("dieline.features", this.features);
2220
- } finally {
2221
- this.isUpdatingConfig = false;
2222
- }
2223
- }
2580
+ this.setWorkingFeatures(newFeatures);
2581
+ this.emitWorkingChange();
2224
2582
  } else {
2225
2583
  this.syncFeatureFromCanvas(target);
2226
2584
  }
@@ -2254,56 +2612,41 @@ var FeatureTool = class {
2254
2612
  this.canvasService.requestRenderAll();
2255
2613
  }
2256
2614
  constrainPosition(p, geometry, limit, feature) {
2257
- if (feature && feature.constraints) {
2258
- const minX = geometry.x - geometry.width / 2;
2259
- const minY = geometry.y - geometry.height / 2;
2260
- const nx = geometry.width > 0 ? (p.x - minX) / geometry.width : 0.5;
2261
- const ny = geometry.height > 0 ? (p.y - minY) / geometry.height : 0.5;
2262
- const scale2 = geometry.scale || 1;
2263
- const dielineWidth = geometry.width / scale2;
2264
- const dielineHeight = geometry.height / scale2;
2265
- const constrained = ConstraintRegistry.apply(nx, ny, feature, {
2266
- dielineWidth,
2267
- dielineHeight
2268
- });
2269
- return {
2270
- x: minX + constrained.x * geometry.width,
2271
- y: minY + constrained.y * geometry.height
2272
- };
2273
- }
2274
- if (feature && feature.placement === "internal") {
2275
- const minX = geometry.x - geometry.width / 2;
2276
- const maxX = geometry.x + geometry.width / 2;
2277
- const minY = geometry.y - geometry.height / 2;
2278
- const maxY = geometry.y + geometry.height / 2;
2279
- return {
2280
- x: Math.max(minX, Math.min(maxX, p.x)),
2281
- y: Math.max(minY, Math.min(maxY, p.y))
2282
- };
2283
- }
2284
- const nearest = getNearestPointOnDieline({ x: p.x, y: p.y }, {
2285
- ...geometry,
2286
- features: []
2287
- });
2288
- const dx = p.x - nearest.x;
2289
- const dy = p.y - nearest.y;
2290
- const dist = Math.sqrt(dx * dx + dy * dy);
2291
- if (dist <= limit) {
2615
+ var _a;
2616
+ if (!feature) {
2292
2617
  return { x: p.x, y: p.y };
2293
2618
  }
2294
- const scale = limit / dist;
2619
+ const minX = geometry.x - geometry.width / 2;
2620
+ const minY = geometry.y - geometry.height / 2;
2621
+ const nx = geometry.width > 0 ? (p.x - minX) / geometry.width : 0.5;
2622
+ const ny = geometry.height > 0 ? (p.y - minY) / geometry.height : 0.5;
2623
+ const scale = geometry.scale || 1;
2624
+ const dielineWidth = geometry.width / scale;
2625
+ const dielineHeight = geometry.height / scale;
2626
+ const activeConstraints = (_a = feature.constraints) == null ? void 0 : _a.filter((c) => !c.validateOnly);
2627
+ const constrained = ConstraintRegistry.apply(
2628
+ nx,
2629
+ ny,
2630
+ feature,
2631
+ {
2632
+ dielineWidth,
2633
+ dielineHeight,
2634
+ geometry
2635
+ },
2636
+ activeConstraints
2637
+ );
2295
2638
  return {
2296
- x: nearest.x + dx * scale,
2297
- y: nearest.y + dy * scale
2639
+ x: minX + constrained.x * geometry.width,
2640
+ y: minY + constrained.y * geometry.height
2298
2641
  };
2299
2642
  }
2300
2643
  syncFeatureFromCanvas(target) {
2301
2644
  var _a;
2302
2645
  if (!this.currentGeometry || !this.context) return;
2303
2646
  const index = (_a = target.data) == null ? void 0 : _a.index;
2304
- if (index === void 0 || index < 0 || index >= this.features.length)
2647
+ if (index === void 0 || index < 0 || index >= this.workingFeatures.length)
2305
2648
  return;
2306
- const feature = this.features[index];
2649
+ const feature = this.workingFeatures[index];
2307
2650
  const geometry = this.getGeometryForFeature(this.currentGeometry, feature);
2308
2651
  const { width, height, x, y } = geometry;
2309
2652
  const left = x - width / 2;
@@ -2316,20 +2659,10 @@ var FeatureTool = class {
2316
2659
  y: normalizedY
2317
2660
  // Could also update rotation if we allowed rotating markers
2318
2661
  };
2319
- const newFeatures = [...this.features];
2662
+ const newFeatures = [...this.workingFeatures];
2320
2663
  newFeatures[index] = updatedFeature;
2321
- this.features = newFeatures;
2322
- const configService = this.context.services.get(
2323
- "ConfigurationService"
2324
- );
2325
- if (configService) {
2326
- this.isUpdatingConfig = true;
2327
- try {
2328
- configService.update("dieline.features", this.features);
2329
- } finally {
2330
- this.isUpdatingConfig = false;
2331
- }
2332
- }
2664
+ this.setWorkingFeatures(newFeatures);
2665
+ this.emitWorkingChange();
2333
2666
  }
2334
2667
  redraw() {
2335
2668
  if (!this.canvasService || !this.currentGeometry) return;
@@ -2340,7 +2673,7 @@ var FeatureTool = class {
2340
2673
  return ((_a = obj.data) == null ? void 0 : _a.type) === "feature-marker";
2341
2674
  });
2342
2675
  existing.forEach((obj) => canvas.remove(obj));
2343
- if (!this.features || this.features.length === 0) {
2676
+ if (!this.workingFeatures || this.workingFeatures.length === 0) {
2344
2677
  this.canvasService.requestRenderAll();
2345
2678
  return;
2346
2679
  }
@@ -2348,7 +2681,7 @@ var FeatureTool = class {
2348
2681
  const finalScale = scale;
2349
2682
  const groups = {};
2350
2683
  const singles = [];
2351
- this.features.forEach((f, i) => {
2684
+ this.workingFeatures.forEach((f, i) => {
2352
2685
  if (f.groupId) {
2353
2686
  if (!groups[f.groupId]) groups[f.groupId] = [];
2354
2687
  groups[f.groupId].push({ feature: f, index: i });
@@ -2395,6 +2728,33 @@ var FeatureTool = class {
2395
2728
  if (feature.rotation) {
2396
2729
  shape.rotate(feature.rotation);
2397
2730
  }
2731
+ if (feature.bridge && feature.bridge.type === "vertical") {
2732
+ const bridgeIndicator = new import_fabric4.Rect({
2733
+ width: visualWidth,
2734
+ height: 100 * featureScale,
2735
+ // Arbitrary long length to show direction
2736
+ fill: "transparent",
2737
+ stroke: "#888",
2738
+ strokeWidth: 1,
2739
+ strokeDashArray: [2, 2],
2740
+ originX: "center",
2741
+ originY: "bottom",
2742
+ // Anchor at bottom so it extends up
2743
+ left: pos.x,
2744
+ top: pos.y - visualHeight / 2,
2745
+ // Start from top of feature
2746
+ opacity: 0.5,
2747
+ selectable: false,
2748
+ evented: false
2749
+ });
2750
+ const group = new import_fabric4.Group([bridgeIndicator, shape], {
2751
+ originX: "center",
2752
+ originY: "center",
2753
+ left: pos.x,
2754
+ top: pos.y
2755
+ });
2756
+ return group;
2757
+ }
2398
2758
  return shape;
2399
2759
  };
2400
2760
  singles.forEach(({ feature, index }) => {
@@ -2416,25 +2776,6 @@ var FeatureTool = class {
2416
2776
  lockScalingY: true,
2417
2777
  data: { type: "feature-marker", index, isGroup: false }
2418
2778
  });
2419
- marker.set("opacity", 0);
2420
- marker.on("mouseover", () => {
2421
- marker.set("opacity", 1);
2422
- canvas.requestRenderAll();
2423
- });
2424
- marker.on("mouseout", () => {
2425
- if (canvas.getActiveObject() !== marker) {
2426
- marker.set("opacity", 0);
2427
- canvas.requestRenderAll();
2428
- }
2429
- });
2430
- marker.on("selected", () => {
2431
- marker.set("opacity", 1);
2432
- canvas.requestRenderAll();
2433
- });
2434
- marker.on("deselected", () => {
2435
- marker.set("opacity", 0);
2436
- canvas.requestRenderAll();
2437
- });
2438
2779
  canvas.add(marker);
2439
2780
  canvas.bringObjectToFront(marker);
2440
2781
  });
@@ -2471,25 +2812,6 @@ var FeatureTool = class {
2471
2812
  indices: members.map((m) => m.index)
2472
2813
  }
2473
2814
  });
2474
- groupObj.set("opacity", 0);
2475
- groupObj.on("mouseover", () => {
2476
- groupObj.set("opacity", 1);
2477
- canvas.requestRenderAll();
2478
- });
2479
- groupObj.on("mouseout", () => {
2480
- if (canvas.getActiveObject() !== groupObj) {
2481
- groupObj.set("opacity", 0);
2482
- canvas.requestRenderAll();
2483
- }
2484
- });
2485
- groupObj.on("selected", () => {
2486
- groupObj.set("opacity", 1);
2487
- canvas.requestRenderAll();
2488
- });
2489
- groupObj.on("deselected", () => {
2490
- groupObj.set("opacity", 0);
2491
- canvas.requestRenderAll();
2492
- });
2493
2815
  canvas.add(groupObj);
2494
2816
  canvas.bringObjectToFront(groupObj);
2495
2817
  });
@@ -2508,12 +2830,12 @@ var FeatureTool = class {
2508
2830
  if ((_a = marker.data) == null ? void 0 : _a.isGroup) {
2509
2831
  const indices = (_b = marker.data) == null ? void 0 : _b.indices;
2510
2832
  if (indices && indices.length > 0) {
2511
- feature = this.features[indices[0]];
2833
+ feature = this.workingFeatures[indices[0]];
2512
2834
  }
2513
2835
  } else {
2514
2836
  const index = (_c = marker.data) == null ? void 0 : _c.index;
2515
2837
  if (index !== void 0) {
2516
- feature = this.features[index];
2838
+ feature = this.workingFeatures[index];
2517
2839
  }
2518
2840
  }
2519
2841
  const geometry = this.getGeometryForFeature(
@@ -3237,7 +3559,7 @@ var RulerTool = class {
3237
3559
  // Dieline context for sync
3238
3560
  this.dielineWidth = 500;
3239
3561
  this.dielineHeight = 500;
3240
- this.dielineUnit = "mm";
3562
+ this.dielineDisplayUnit = "mm";
3241
3563
  this.dielinePadding = 40;
3242
3564
  this.dielineOffset = 0;
3243
3565
  if (options) {
@@ -3261,7 +3583,10 @@ var RulerTool = class {
3261
3583
  this.textColor = configService.get("ruler.textColor", this.textColor);
3262
3584
  this.lineColor = configService.get("ruler.lineColor", this.lineColor);
3263
3585
  this.fontSize = configService.get("ruler.fontSize", this.fontSize);
3264
- this.dielineUnit = configService.get("dieline.unit", this.dielineUnit);
3586
+ this.dielineDisplayUnit = configService.get(
3587
+ "dieline.displayUnit",
3588
+ this.dielineDisplayUnit
3589
+ );
3265
3590
  this.dielineWidth = configService.get("dieline.width", this.dielineWidth);
3266
3591
  this.dielineHeight = configService.get(
3267
3592
  "dieline.height",
@@ -3284,7 +3609,8 @@ var RulerTool = class {
3284
3609
  shouldUpdate = true;
3285
3610
  }
3286
3611
  } else if (e.key.startsWith("dieline.")) {
3287
- if (e.key === "dieline.unit") this.dielineUnit = e.value;
3612
+ if (e.key === "dieline.displayUnit")
3613
+ this.dielineDisplayUnit = e.value;
3288
3614
  if (e.key === "dieline.width") this.dielineWidth = e.value;
3289
3615
  if (e.key === "dieline.height") this.dielineHeight = e.value;
3290
3616
  if (e.key === "dieline.padding") this.dielinePadding = e.value;
@@ -3471,26 +3797,27 @@ var RulerTool = class {
3471
3797
  const width = this.canvasService.canvas.width || 800;
3472
3798
  const height = this.canvasService.canvas.height || 600;
3473
3799
  const paddingPx = this.resolvePadding(width, height);
3474
- const layout = Coordinate.calculateLayout(
3475
- { width, height },
3476
- { width: this.dielineWidth, height: this.dielineHeight },
3477
- paddingPx
3800
+ this.canvasService.viewport.setPadding(paddingPx);
3801
+ this.canvasService.viewport.updatePhysical(
3802
+ this.dielineWidth,
3803
+ this.dielineHeight
3478
3804
  );
3805
+ const layout = this.canvasService.viewport.layout;
3479
3806
  const scale = layout.scale;
3480
3807
  const offsetX = layout.offsetX;
3481
3808
  const offsetY = layout.offsetY;
3482
3809
  const visualWidth = layout.width;
3483
3810
  const visualHeight = layout.height;
3484
- const rawOffset = this.dielineOffset || 0;
3485
- const effectiveOffset = rawOffset > 0 ? rawOffset : 0;
3486
- const expandPixels = effectiveOffset * scale;
3811
+ const rawOffsetMm = this.dielineOffset || 0;
3812
+ const effectiveOffsetMm = rawOffsetMm > 0 ? rawOffsetMm : 0;
3813
+ const expandPixels = effectiveOffsetMm * scale;
3487
3814
  const gap = this.gap || 15;
3488
3815
  const rulerLeft = offsetX - expandPixels;
3489
3816
  const rulerTop = offsetY - expandPixels;
3490
3817
  const rulerRight = offsetX + visualWidth + expandPixels;
3491
3818
  const rulerBottom = offsetY + visualHeight + expandPixels;
3492
- const displayWidth = this.dielineWidth + effectiveOffset * 2;
3493
- const displayHeight = this.dielineHeight + effectiveOffset * 2;
3819
+ const displayWidthMm = this.dielineWidth + effectiveOffsetMm * 2;
3820
+ const displayHeightMm = this.dielineHeight + effectiveOffsetMm * 2;
3494
3821
  const topRulerY = rulerTop - gap;
3495
3822
  const topRulerXStart = rulerLeft;
3496
3823
  const topRulerXEnd = rulerRight;
@@ -3533,8 +3860,8 @@ var RulerTool = class {
3533
3860
  }
3534
3861
  )
3535
3862
  );
3536
- const widthStr = parseFloat(displayWidth.toFixed(2)).toString();
3537
- const topTextContent = `${widthStr} ${this.dielineUnit}`;
3863
+ const widthStr = formatMm(displayWidthMm, this.dielineDisplayUnit);
3864
+ const topTextContent = `${widthStr} ${this.dielineDisplayUnit}`;
3538
3865
  const topText = new import_fabric7.Text(topTextContent, {
3539
3866
  left: topRulerXStart + (rulerRight - rulerLeft) / 2,
3540
3867
  top: topRulerY,
@@ -3589,8 +3916,8 @@ var RulerTool = class {
3589
3916
  }
3590
3917
  )
3591
3918
  );
3592
- const heightStr = parseFloat(displayHeight.toFixed(2)).toString();
3593
- const leftTextContent = `${heightStr} ${this.dielineUnit}`;
3919
+ const heightStr = formatMm(displayHeightMm, this.dielineDisplayUnit);
3920
+ const leftTextContent = `${heightStr} ${this.dielineDisplayUnit}`;
3594
3921
  const leftText = new import_fabric7.Text(leftTextContent, {
3595
3922
  left: leftRulerX,
3596
3923
  top: leftRulerYStart + (rulerBottom - rulerTop) / 2,
@@ -3702,6 +4029,81 @@ var MirrorTool = class {
3702
4029
 
3703
4030
  // src/CanvasService.ts
3704
4031
  var import_fabric8 = require("fabric");
4032
+
4033
+ // src/ViewportSystem.ts
4034
+ var ViewportSystem = class {
4035
+ constructor(containerSize = { width: 0, height: 0 }, physicalSize = { width: 0, height: 0 }, padding = 40) {
4036
+ this._containerSize = { width: 0, height: 0 };
4037
+ this._physicalSize = { width: 0, height: 0 };
4038
+ this._padding = 0;
4039
+ this._layout = {
4040
+ scale: 1,
4041
+ offsetX: 0,
4042
+ offsetY: 0,
4043
+ width: 0,
4044
+ height: 0
4045
+ };
4046
+ this._containerSize = containerSize;
4047
+ this._physicalSize = physicalSize;
4048
+ this._padding = padding;
4049
+ this.updateLayout();
4050
+ }
4051
+ get layout() {
4052
+ return this._layout;
4053
+ }
4054
+ get scale() {
4055
+ return this._layout.scale;
4056
+ }
4057
+ get offset() {
4058
+ return { x: this._layout.offsetX, y: this._layout.offsetY };
4059
+ }
4060
+ updateContainer(width, height) {
4061
+ if (this._containerSize.width === width && this._containerSize.height === height)
4062
+ return;
4063
+ this._containerSize = { width, height };
4064
+ this.updateLayout();
4065
+ }
4066
+ updatePhysical(width, height) {
4067
+ if (this._physicalSize.width === width && this._physicalSize.height === height)
4068
+ return;
4069
+ this._physicalSize = { width, height };
4070
+ this.updateLayout();
4071
+ }
4072
+ setPadding(padding) {
4073
+ if (this._padding === padding) return;
4074
+ this._padding = padding;
4075
+ this.updateLayout();
4076
+ }
4077
+ updateLayout() {
4078
+ this._layout = Coordinate.calculateLayout(
4079
+ this._containerSize,
4080
+ this._physicalSize,
4081
+ this._padding
4082
+ );
4083
+ }
4084
+ toPixel(value) {
4085
+ return value * this._layout.scale;
4086
+ }
4087
+ toPhysical(value) {
4088
+ return this._layout.scale === 0 ? 0 : value / this._layout.scale;
4089
+ }
4090
+ toPixelPoint(point) {
4091
+ return {
4092
+ x: point.x * this._layout.scale + this._layout.offsetX,
4093
+ y: point.y * this._layout.scale + this._layout.offsetY
4094
+ };
4095
+ }
4096
+ // Convert screen coordinate (e.g. mouse event) to physical coordinate (relative to content origin)
4097
+ toPhysicalPoint(point) {
4098
+ if (this._layout.scale === 0) return { x: 0, y: 0 };
4099
+ return {
4100
+ x: (point.x - this._layout.offsetX) / this._layout.scale,
4101
+ y: (point.y - this._layout.offsetY) / this._layout.scale
4102
+ };
4103
+ }
4104
+ };
4105
+
4106
+ // src/CanvasService.ts
3705
4107
  var CanvasService = class {
3706
4108
  constructor(el, options) {
3707
4109
  if (el instanceof import_fabric8.Canvas) {
@@ -3712,6 +4114,10 @@ var CanvasService = class {
3712
4114
  ...options
3713
4115
  });
3714
4116
  }
4117
+ this.viewport = new ViewportSystem();
4118
+ if (this.canvas.width !== void 0 && this.canvas.height !== void 0) {
4119
+ this.viewport.updateContainer(this.canvas.width, this.canvas.height);
4120
+ }
3715
4121
  if (options == null ? void 0 : options.eventBus) {
3716
4122
  this.setEventBus(options.eventBus);
3717
4123
  }
@@ -3792,5 +4198,7 @@ var CanvasService = class {
3792
4198
  ImageTool,
3793
4199
  MirrorTool,
3794
4200
  RulerTool,
3795
- WhiteInkTool
4201
+ WhiteInkTool,
4202
+ formatMm,
4203
+ parseLengthToMm
3796
4204
  });