@pooder/kit 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -394,6 +394,24 @@ var ImageTracer = class {
394
394
 
395
395
  // src/coordinate.ts
396
396
  var Coordinate = class {
397
+ /**
398
+ * Calculate layout to fit content within container while preserving aspect ratio.
399
+ */
400
+ static calculateLayout(container, content, padding = 0) {
401
+ const availableWidth = Math.max(0, container.width - padding * 2);
402
+ const availableHeight = Math.max(0, container.height - padding * 2);
403
+ if (content.width === 0 || content.height === 0) {
404
+ return { scale: 1, offsetX: 0, offsetY: 0, width: 0, height: 0 };
405
+ }
406
+ const scaleX = availableWidth / content.width;
407
+ const scaleY = availableHeight / content.height;
408
+ const scale = Math.min(scaleX, scaleY);
409
+ const width = content.width * scale;
410
+ const height = content.height * scale;
411
+ const offsetX = (container.width - width) / 2;
412
+ const offsetY = (container.height - height) / 2;
413
+ return { scale, offsetX, offsetY, width, height };
414
+ }
397
415
  /**
398
416
  * Convert an absolute value to a normalized value (0-1).
399
417
  * @param value Absolute value (e.g., pixels)
@@ -428,6 +446,21 @@ var Coordinate = class {
428
446
  y: this.toAbsolute(point.y, size.height)
429
447
  };
430
448
  }
449
+ static convertUnit(value, from, to) {
450
+ if (from === to) return value;
451
+ const toMM = {
452
+ px: 0.264583,
453
+ // 1px = 0.264583mm (96 DPI)
454
+ mm: 1,
455
+ cm: 10,
456
+ in: 25.4
457
+ };
458
+ const mmValue = value * (from === "px" ? toMM.px : toMM[from] || 1);
459
+ if (to === "px") {
460
+ return mmValue / toMM.px;
461
+ }
462
+ return mmValue / (toMM[to] || 1);
463
+ }
431
464
  };
432
465
 
433
466
  // src/geometry.ts
@@ -484,9 +517,10 @@ function resolveHolePosition(hole, geometry, canvasSize) {
484
517
  y: by + (hole.offsetY || 0)
485
518
  };
486
519
  } else if (hole.x !== void 0 && hole.y !== void 0) {
520
+ const { x, width, y, height } = geometry;
487
521
  return {
488
- x: hole.x * canvasSize.width,
489
- y: hole.y * canvasSize.height
522
+ x: hole.x * width + (x - width / 2) + (hole.offsetX || 0),
523
+ y: hole.y * height + (y - height / 2) + (hole.offsetY || 0)
490
524
  };
491
525
  }
492
526
  return { x: 0, y: 0 };
@@ -592,7 +626,10 @@ function getDielineShape(options) {
592
626
  cutsPath.remove();
593
627
  mainShape = temp;
594
628
  } catch (e) {
595
- console.error("Geometry: Failed to subtract cutsPath from mainShape", e);
629
+ console.error(
630
+ "Geometry: Failed to subtract cutsPath from mainShape",
631
+ e
632
+ );
596
633
  }
597
634
  }
598
635
  }
@@ -685,7 +722,12 @@ function getPathBounds(pathData) {
685
722
  path.pathData = pathData;
686
723
  const bounds = path.bounds;
687
724
  path.remove();
688
- return { width: bounds.width, height: bounds.height };
725
+ return {
726
+ x: bounds.x,
727
+ y: bounds.y,
728
+ width: bounds.width,
729
+ height: bounds.height
730
+ };
689
731
  }
690
732
 
691
733
  // src/dieline.ts
@@ -695,6 +737,7 @@ var DielineTool = class {
695
737
  this.metadata = {
696
738
  name: "DielineTool"
697
739
  };
740
+ this.unit = "mm";
698
741
  this.shape = "rect";
699
742
  this.width = 500;
700
743
  this.height = 500;
@@ -705,6 +748,7 @@ var DielineTool = class {
705
748
  this.outsideColor = "#ffffff";
706
749
  this.showBleedLines = true;
707
750
  this.holes = [];
751
+ this.padding = 140;
708
752
  if (options) {
709
753
  Object.assign(this, options);
710
754
  }
@@ -718,14 +762,12 @@ var DielineTool = class {
718
762
  }
719
763
  const configService = context.services.get("ConfigurationService");
720
764
  if (configService) {
765
+ this.unit = configService.get("dieline.unit", this.unit);
721
766
  this.shape = configService.get("dieline.shape", this.shape);
722
767
  this.width = configService.get("dieline.width", this.width);
723
768
  this.height = configService.get("dieline.height", this.height);
724
769
  this.radius = configService.get("dieline.radius", this.radius);
725
- this.borderLength = configService.get(
726
- "dieline.borderLength",
727
- this.borderLength
728
- );
770
+ this.padding = configService.get("dieline.padding", this.padding);
729
771
  this.offset = configService.get("dieline.offset", this.offset);
730
772
  this.style = configService.get("dieline.style", this.style);
731
773
  this.insideColor = configService.get(
@@ -766,6 +808,13 @@ var DielineTool = class {
766
808
  contribute() {
767
809
  return {
768
810
  [ContributionPointIds2.CONFIGURATIONS]: [
811
+ {
812
+ id: "dieline.unit",
813
+ type: "select",
814
+ label: "Unit",
815
+ options: ["px", "mm", "cm", "in"],
816
+ default: this.unit
817
+ },
769
818
  {
770
819
  id: "dieline.shape",
771
820
  type: "select",
@@ -801,15 +850,14 @@ var DielineTool = class {
801
850
  id: "dieline.position",
802
851
  type: "json",
803
852
  label: "Position (Normalized)",
804
- default: this.position
853
+ default: this.radius
805
854
  },
806
855
  {
807
- id: "dieline.borderLength",
808
- type: "number",
809
- label: "Margin",
810
- min: 0,
811
- max: 500,
812
- default: this.borderLength
856
+ id: "dieline.padding",
857
+ type: "select",
858
+ label: "View Padding",
859
+ options: [0, 10, 20, 40, 60, 100, "2%", "5%", "10%", "15%", "20%"],
860
+ default: this.padding
813
861
  },
814
862
  {
815
863
  id: "dieline.offset",
@@ -878,7 +926,9 @@ var DielineTool = class {
878
926
  const scale = currentMax / Math.max(bounds.width, bounds.height);
879
927
  const newWidth = bounds.width * scale;
880
928
  const newHeight = bounds.height * scale;
881
- const configService = (_a = this.context) == null ? void 0 : _a.services.get("ConfigurationService");
929
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get(
930
+ "ConfigurationService"
931
+ );
882
932
  if (configService) {
883
933
  configService.update("dieline.width", newWidth);
884
934
  configService.update("dieline.height", newHeight);
@@ -943,12 +993,25 @@ var DielineTool = class {
943
993
  }
944
994
  return new Pattern({ source: canvas, repetition: "repeat" });
945
995
  }
996
+ resolvePadding(containerWidth, containerHeight) {
997
+ if (typeof this.padding === "number") {
998
+ return this.padding;
999
+ }
1000
+ if (typeof this.padding === "string") {
1001
+ if (this.padding.endsWith("%")) {
1002
+ const percent = parseFloat(this.padding) / 100;
1003
+ return Math.min(containerWidth, containerHeight) * percent;
1004
+ }
1005
+ return parseFloat(this.padding) || 0;
1006
+ }
1007
+ return 0;
1008
+ }
946
1009
  updateDieline(emitEvent = true) {
947
- var _a, _b;
948
1010
  if (!this.canvasService) return;
949
1011
  const layer = this.getLayer();
950
1012
  if (!layer) return;
951
1013
  const {
1014
+ unit,
952
1015
  shape,
953
1016
  radius,
954
1017
  offset,
@@ -956,43 +1019,60 @@ var DielineTool = class {
956
1019
  insideColor,
957
1020
  outsideColor,
958
1021
  position,
959
- borderLength,
960
1022
  showBleedLines,
961
1023
  holes
962
1024
  } = this;
963
1025
  let { width, height } = this;
964
1026
  const canvasW = this.canvasService.canvas.width || 800;
965
1027
  const canvasH = this.canvasService.canvas.height || 600;
966
- let visualWidth = width;
967
- let visualHeight = height;
968
- if (borderLength && borderLength > 0) {
969
- visualWidth = Math.max(0, canvasW - borderLength * 2);
970
- visualHeight = Math.max(0, canvasH - borderLength * 2);
971
- }
972
- const cx = Coordinate.toAbsolute((_a = position == null ? void 0 : position.x) != null ? _a : 0.5, canvasW);
973
- const cy = Coordinate.toAbsolute((_b = position == null ? void 0 : position.y) != null ? _b : 0.5, canvasH);
1028
+ const paddingPx = this.resolvePadding(canvasW, canvasH);
1029
+ const layout = Coordinate.calculateLayout(
1030
+ { width: canvasW, height: canvasH },
1031
+ { width, height },
1032
+ paddingPx
1033
+ );
1034
+ const scale = layout.scale;
1035
+ const cx = layout.offsetX + layout.width / 2;
1036
+ const cy = layout.offsetY + layout.height / 2;
1037
+ const visualWidth = layout.width;
1038
+ const visualHeight = layout.height;
1039
+ const visualRadius = radius * scale;
1040
+ const visualOffset = offset * scale;
974
1041
  layer.remove(...layer.getObjects());
975
1042
  const geometryForHoles = {
976
1043
  x: cx,
977
1044
  y: cy,
978
1045
  width: visualWidth,
979
1046
  height: visualHeight
1047
+ // Pass scale/unit context if needed by resolveHolePosition (though currently unused there)
980
1048
  };
981
1049
  const absoluteHoles = (holes || []).map((h) => {
982
- const pos = resolveHolePosition(
983
- h,
984
- geometryForHoles,
985
- { width: canvasW, height: canvasH }
986
- );
1050
+ const unitScale = Coordinate.convertUnit(1, "mm", unit);
1051
+ const offsetScale = unitScale * scale;
1052
+ const hWithPixelOffsets = {
1053
+ ...h,
1054
+ offsetX: (h.offsetX || 0) * offsetScale,
1055
+ offsetY: (h.offsetY || 0) * offsetScale
1056
+ };
1057
+ const pos = resolveHolePosition(hWithPixelOffsets, geometryForHoles, {
1058
+ width: canvasW,
1059
+ height: canvasH
1060
+ });
987
1061
  return {
988
1062
  ...h,
989
1063
  x: pos.x,
990
- y: pos.y
1064
+ y: pos.y,
1065
+ // Scale hole radii: mm -> current unit -> pixels
1066
+ innerRadius: h.innerRadius * offsetScale,
1067
+ outerRadius: h.outerRadius * offsetScale,
1068
+ // Store scaled offsets in the result for consistency, though pos is already resolved
1069
+ offsetX: hWithPixelOffsets.offsetX,
1070
+ offsetY: hWithPixelOffsets.offsetY
991
1071
  };
992
1072
  });
993
- const cutW = Math.max(0, width + offset * 2);
994
- const cutH = Math.max(0, height + offset * 2);
995
- const cutR = radius === 0 ? 0 : Math.max(0, radius + offset);
1073
+ const cutW = Math.max(0, visualWidth + visualOffset * 2);
1074
+ const cutH = Math.max(0, visualHeight + visualOffset * 2);
1075
+ const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
996
1076
  const maskPathData = generateMaskPath({
997
1077
  canvasWidth: canvasW,
998
1078
  canvasHeight: canvasH,
@@ -1042,15 +1122,15 @@ var DielineTool = class {
1042
1122
  const bleedPathData = generateBleedZonePath(
1043
1123
  {
1044
1124
  shape,
1045
- width,
1046
- height,
1047
- radius,
1125
+ width: visualWidth,
1126
+ height: visualHeight,
1127
+ radius: visualRadius,
1048
1128
  x: cx,
1049
1129
  y: cy,
1050
1130
  holes: absoluteHoles,
1051
1131
  pathData: this.pathData
1052
1132
  },
1053
- offset
1133
+ visualOffset
1054
1134
  );
1055
1135
  if (showBleedLines !== false) {
1056
1136
  const pattern = this.createHatchPattern("red");
@@ -1093,13 +1173,12 @@ var DielineTool = class {
1093
1173
  }
1094
1174
  const borderPathData = generateDielinePath({
1095
1175
  shape,
1096
- width,
1097
- height,
1098
- radius,
1176
+ width: visualWidth,
1177
+ height: visualHeight,
1178
+ radius: visualRadius,
1099
1179
  x: cx,
1100
1180
  y: cy,
1101
1181
  holes: absoluteHoles,
1102
- // FIX: Use absoluteHoles instead of holes
1103
1182
  pathData: this.pathData
1104
1183
  });
1105
1184
  const borderObj = new Path(borderPathData, {
@@ -1137,115 +1216,107 @@ var DielineTool = class {
1137
1216
  }
1138
1217
  }
1139
1218
  getGeometry() {
1140
- var _a, _b;
1141
1219
  if (!this.canvasService) return null;
1142
- const { shape, width, height, radius, position, borderLength, offset } = this;
1220
+ const { unit, shape, width, height, radius, position, offset } = this;
1143
1221
  const canvasW = this.canvasService.canvas.width || 800;
1144
1222
  const canvasH = this.canvasService.canvas.height || 600;
1145
- let visualWidth = width;
1146
- let visualHeight = height;
1147
- if (borderLength && borderLength > 0) {
1148
- visualWidth = Math.max(0, canvasW - borderLength * 2);
1149
- visualHeight = Math.max(0, canvasH - borderLength * 2);
1150
- }
1151
- const cx = Coordinate.toAbsolute((_a = position == null ? void 0 : position.x) != null ? _a : 0.5, canvasW);
1152
- const cy = Coordinate.toAbsolute((_b = position == null ? void 0 : position.y) != null ? _b : 0.5, canvasH);
1223
+ const paddingPx = this.resolvePadding(canvasW, canvasH);
1224
+ const layout = Coordinate.calculateLayout(
1225
+ { width: canvasW, height: canvasH },
1226
+ { width, height },
1227
+ paddingPx
1228
+ );
1229
+ const scale = layout.scale;
1230
+ const cx = layout.offsetX + layout.width / 2;
1231
+ const cy = layout.offsetY + layout.height / 2;
1232
+ const visualWidth = layout.width;
1233
+ const visualHeight = layout.height;
1153
1234
  return {
1154
1235
  shape,
1236
+ unit,
1155
1237
  x: cx,
1156
1238
  y: cy,
1157
1239
  width: visualWidth,
1158
1240
  height: visualHeight,
1159
- radius,
1160
- offset,
1161
- borderLength,
1241
+ radius: radius * scale,
1242
+ offset: offset * scale,
1243
+ // Pass scale to help other tools (like HoleTool) convert units
1244
+ scale,
1162
1245
  pathData: this.pathData
1163
1246
  };
1164
1247
  }
1165
- exportCutImage() {
1166
- var _a, _b, _c, _d;
1248
+ async exportCutImage() {
1167
1249
  if (!this.canvasService) return null;
1168
- const canvas = this.canvasService.canvas;
1250
+ const userLayer = this.canvasService.getLayer("user");
1251
+ if (!userLayer) return null;
1169
1252
  const { shape, width, height, radius, position, holes } = this;
1170
- const canvasW = canvas.width || 800;
1171
- const canvasH = canvas.height || 600;
1172
- const cx = Coordinate.toAbsolute((_a = position == null ? void 0 : position.x) != null ? _a : 0.5, canvasW);
1173
- const cy = Coordinate.toAbsolute((_b = position == null ? void 0 : position.y) != null ? _b : 0.5, canvasH);
1253
+ const canvasW = this.canvasService.canvas.width || 800;
1254
+ const canvasH = this.canvasService.canvas.height || 600;
1255
+ const paddingPx = this.resolvePadding(canvasW, canvasH);
1256
+ const layout = Coordinate.calculateLayout(
1257
+ { width: canvasW, height: canvasH },
1258
+ { width, height },
1259
+ paddingPx
1260
+ );
1261
+ const scale = layout.scale;
1262
+ const cx = layout.offsetX + layout.width / 2;
1263
+ const cy = layout.offsetY + layout.height / 2;
1264
+ const visualWidth = layout.width;
1265
+ const visualHeight = layout.height;
1266
+ const visualRadius = radius * scale;
1174
1267
  const absoluteHoles = (holes || []).map((h) => {
1268
+ const unit = this.unit || "mm";
1269
+ const unitScale = Coordinate.convertUnit(1, "mm", unit);
1175
1270
  const pos = resolveHolePosition(
1176
- h,
1177
- { x: cx, y: cy, width, height },
1271
+ {
1272
+ ...h,
1273
+ offsetX: (h.offsetX || 0) * unitScale * scale,
1274
+ offsetY: (h.offsetY || 0) * unitScale * scale
1275
+ },
1276
+ { x: cx, y: cy, width: visualWidth, height: visualHeight },
1178
1277
  { width: canvasW, height: canvasH }
1179
1278
  );
1180
1279
  return {
1181
1280
  ...h,
1182
1281
  x: pos.x,
1183
- y: pos.y
1282
+ y: pos.y,
1283
+ innerRadius: h.innerRadius * unitScale * scale,
1284
+ outerRadius: h.outerRadius * unitScale * scale,
1285
+ offsetX: (h.offsetX || 0) * unitScale * scale,
1286
+ offsetY: (h.offsetY || 0) * unitScale * scale
1184
1287
  };
1185
1288
  });
1186
1289
  const pathData = generateDielinePath({
1187
1290
  shape,
1188
- width,
1189
- height,
1190
- radius,
1291
+ width: visualWidth,
1292
+ height: visualHeight,
1293
+ radius: visualRadius,
1191
1294
  x: cx,
1192
1295
  y: cy,
1193
1296
  holes: absoluteHoles,
1194
1297
  pathData: this.pathData
1195
1298
  });
1299
+ const clonedLayer = await userLayer.clone();
1196
1300
  const clipPath = new Path(pathData, {
1197
- left: 0,
1198
- top: 0,
1199
1301
  originX: "left",
1200
1302
  originY: "top",
1201
- absolutePositioned: true
1202
- });
1203
- const layer = this.getLayer();
1204
- const wasVisible = (_c = layer == null ? void 0 : layer.visible) != null ? _c : true;
1205
- if (layer) layer.visible = false;
1206
- const holeMarkers = canvas.getObjects().filter((o) => {
1207
- var _a2;
1208
- return ((_a2 = o.data) == null ? void 0 : _a2.type) === "hole-marker";
1209
- });
1210
- holeMarkers.forEach((o) => o.visible = false);
1211
- const rulerLayer = canvas.getObjects().find((obj) => {
1212
- var _a2;
1213
- return ((_a2 = obj.data) == null ? void 0 : _a2.id) === "ruler-overlay";
1214
- });
1215
- const rulerWasVisible = (_d = rulerLayer == null ? void 0 : rulerLayer.visible) != null ? _d : true;
1216
- if (rulerLayer) rulerLayer.visible = false;
1217
- const originalClip = canvas.clipPath;
1218
- canvas.clipPath = clipPath;
1219
- const bbox = clipPath.getBoundingRect();
1220
- const clipPathCorrected = new Path(pathData, {
1221
- absolutePositioned: true,
1222
1303
  left: 0,
1223
- top: 0
1224
- });
1225
- const tempPath = new Path(pathData);
1226
- const tempBounds = tempPath.getBoundingRect();
1227
- clipPathCorrected.set({
1228
- left: tempBounds.left,
1229
- top: tempBounds.top,
1230
- originX: "left",
1231
- originY: "top"
1304
+ top: 0,
1305
+ absolutePositioned: true
1306
+ // Important for groups
1232
1307
  });
1233
- canvas.clipPath = clipPathCorrected;
1234
- const exportBbox = clipPathCorrected.getBoundingRect();
1235
- const dataURL = canvas.toDataURL({
1308
+ clonedLayer.clipPath = clipPath;
1309
+ const bounds = clipPath.getBoundingRect();
1310
+ const dataUrl = clonedLayer.toDataURL({
1236
1311
  format: "png",
1237
1312
  multiplier: 2,
1238
- left: exportBbox.left,
1239
- top: exportBbox.top,
1240
- width: exportBbox.width,
1241
- height: exportBbox.height
1313
+ // Better quality
1314
+ left: bounds.left,
1315
+ top: bounds.top,
1316
+ width: bounds.width,
1317
+ height: bounds.height
1242
1318
  });
1243
- canvas.clipPath = originalClip;
1244
- if (layer) layer.visible = wasVisible;
1245
- if (rulerLayer) rulerLayer.visible = rulerWasVisible;
1246
- holeMarkers.forEach((o) => o.visible = true);
1247
- canvas.requestRenderAll();
1248
- return dataURL;
1319
+ return dataUrl;
1249
1320
  }
1250
1321
  };
1251
1322
 
@@ -1522,11 +1593,19 @@ var HoleTool = class {
1522
1593
  handler: (x, y) => {
1523
1594
  var _a, _b, _c;
1524
1595
  if (!this.canvasService) return false;
1525
- const { width, height } = this.canvasService.canvas;
1526
- const normalizedHole = Coordinate.normalizePoint(
1527
- { x, y },
1528
- { width: width || 800, height: height || 600 }
1529
- );
1596
+ let normalizedX = 0.5;
1597
+ let normalizedY = 0.5;
1598
+ if (this.currentGeometry) {
1599
+ const { x: gx, y: gy, width: gw, height: gh } = this.currentGeometry;
1600
+ const left = gx - gw / 2;
1601
+ const top = gy - gh / 2;
1602
+ normalizedX = gw > 0 ? (x - left) / gw : 0.5;
1603
+ normalizedY = gh > 0 ? (y - top) / gh : 0.5;
1604
+ } else {
1605
+ const { width, height } = this.canvasService.canvas;
1606
+ normalizedX = Coordinate.toNormalized(x, width || 800);
1607
+ normalizedY = Coordinate.toNormalized(y, height || 600);
1608
+ }
1530
1609
  const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1531
1610
  "ConfigurationService"
1532
1611
  );
@@ -1536,8 +1615,8 @@ var HoleTool = class {
1536
1615
  const innerRadius = (_b = lastHole == null ? void 0 : lastHole.innerRadius) != null ? _b : 15;
1537
1616
  const outerRadius = (_c = lastHole == null ? void 0 : lastHole.outerRadius) != null ? _c : 25;
1538
1617
  const newHole = {
1539
- x: normalizedHole.x,
1540
- y: normalizedHole.y,
1618
+ x: normalizedX,
1619
+ y: normalizedY,
1541
1620
  innerRadius,
1542
1621
  outerRadius
1543
1622
  };
@@ -1632,7 +1711,10 @@ var HoleTool = class {
1632
1711
  var _a;
1633
1712
  const target = e.target;
1634
1713
  if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "hole-marker") return;
1635
- this.syncHolesFromCanvas();
1714
+ const changed = this.enforceConstraints();
1715
+ if (!changed) {
1716
+ this.syncHolesFromCanvas();
1717
+ }
1636
1718
  };
1637
1719
  canvas.on("object:modified", this.handleModified);
1638
1720
  }
@@ -1690,18 +1772,21 @@ var HoleTool = class {
1690
1772
  }
1691
1773
  );
1692
1774
  const newHoles = objects.map((obj, i) => {
1693
- var _a, _b;
1775
+ var _a, _b, _c, _d;
1694
1776
  const original = this.holes[i];
1695
1777
  const newAbsX = obj.left;
1696
1778
  const newAbsY = obj.top;
1779
+ const scale = ((_a = this.currentGeometry) == null ? void 0 : _a.scale) || 1;
1780
+ const unit = ((_b = this.currentGeometry) == null ? void 0 : _b.unit) || "mm";
1781
+ const unitScale = Coordinate.convertUnit(1, "mm", unit);
1697
1782
  if (original && original.anchor && this.currentGeometry) {
1698
- const { x, y, width: width2, height: height2 } = this.currentGeometry;
1783
+ const { x, y, width, height } = this.currentGeometry;
1699
1784
  let bx = x;
1700
1785
  let by = y;
1701
- const left = x - width2 / 2;
1702
- const right = x + width2 / 2;
1703
- const top = y - height2 / 2;
1704
- const bottom = y + height2 / 2;
1786
+ const left = x - width / 2;
1787
+ const right = x + width / 2;
1788
+ const top = y - height / 2;
1789
+ const bottom = y + height / 2;
1705
1790
  switch (original.anchor) {
1706
1791
  case "top-left":
1707
1792
  bx = left;
@@ -1742,25 +1827,34 @@ var HoleTool = class {
1742
1827
  }
1743
1828
  return {
1744
1829
  ...original,
1745
- offsetX: newAbsX - bx,
1746
- offsetY: newAbsY - by,
1830
+ // Denormalize offset back to physical units (mm)
1831
+ offsetX: (newAbsX - bx) / scale / unitScale,
1832
+ offsetY: (newAbsY - by) / scale / unitScale,
1747
1833
  // Clear direct coordinates if we use anchor
1748
1834
  x: void 0,
1749
1835
  y: void 0
1750
1836
  };
1751
1837
  }
1752
- const { width, height } = this.canvasService.canvas;
1753
- const p = Coordinate.normalizePoint(
1754
- { x: newAbsX, y: newAbsY },
1755
- { width: width || 800, height: height || 600 }
1756
- );
1838
+ let normalizedX = 0.5;
1839
+ let normalizedY = 0.5;
1840
+ if (this.currentGeometry) {
1841
+ const { x, y, width, height } = this.currentGeometry;
1842
+ const left = x - width / 2;
1843
+ const top = y - height / 2;
1844
+ normalizedX = width > 0 ? (newAbsX - left) / width : 0.5;
1845
+ normalizedY = height > 0 ? (newAbsY - top) / height : 0.5;
1846
+ } else {
1847
+ const { width, height } = this.canvasService.canvas;
1848
+ normalizedX = Coordinate.toNormalized(newAbsX, width || 800);
1849
+ normalizedY = Coordinate.toNormalized(newAbsY, height || 600);
1850
+ }
1757
1851
  return {
1758
1852
  ...original,
1759
- x: p.x,
1760
- y: p.y,
1853
+ x: normalizedX,
1854
+ y: normalizedY,
1761
1855
  // Ensure radii are preserved
1762
- innerRadius: (_a = original == null ? void 0 : original.innerRadius) != null ? _a : 15,
1763
- outerRadius: (_b = original == null ? void 0 : original.outerRadius) != null ? _b : 25
1856
+ innerRadius: (_c = original == null ? void 0 : original.innerRadius) != null ? _c : 15,
1857
+ outerRadius: (_d = original == null ? void 0 : original.outerRadius) != null ? _d : 25
1764
1858
  };
1765
1859
  });
1766
1860
  this.holes = newHoles;
@@ -1798,16 +1892,28 @@ var HoleTool = class {
1798
1892
  x: (width || 800) / 2,
1799
1893
  y: (height || 600) / 2,
1800
1894
  width: width || 800,
1801
- height: height || 600
1895
+ height: height || 600,
1896
+ scale: 1
1897
+ // Default scale if no geometry loaded
1802
1898
  };
1803
1899
  holes.forEach((hole, index) => {
1900
+ const scale = geometry.scale || 1;
1901
+ const unit = geometry.unit || "mm";
1902
+ const unitScale = Coordinate.convertUnit(1, "mm", unit);
1903
+ const visualInnerRadius = hole.innerRadius * unitScale * scale;
1904
+ const visualOuterRadius = hole.outerRadius * unitScale * scale;
1804
1905
  const pos = resolveHolePosition(
1805
- hole,
1906
+ {
1907
+ ...hole,
1908
+ offsetX: (hole.offsetX || 0) * unitScale * scale,
1909
+ offsetY: (hole.offsetY || 0) * unitScale * scale
1910
+ },
1806
1911
  geometry,
1807
- { width: width || 800, height: height || 600 }
1912
+ { width: geometry.width, height: geometry.height }
1913
+ // Use geometry dims instead of canvas
1808
1914
  );
1809
1915
  const innerCircle = new Circle({
1810
- radius: hole.innerRadius,
1916
+ radius: visualInnerRadius,
1811
1917
  fill: "transparent",
1812
1918
  stroke: "red",
1813
1919
  strokeWidth: 2,
@@ -1815,7 +1921,7 @@ var HoleTool = class {
1815
1921
  originY: "center"
1816
1922
  });
1817
1923
  const outerCircle = new Circle({
1818
- radius: hole.outerRadius,
1924
+ radius: visualOuterRadius,
1819
1925
  fill: "transparent",
1820
1926
  stroke: "#666",
1821
1927
  strokeWidth: 1,
@@ -1895,11 +2001,16 @@ var HoleTool = class {
1895
2001
  var _a, _b;
1896
2002
  const currentPos = new Point(obj.left, obj.top);
1897
2003
  const holeData = this.holes[i];
2004
+ const scale = geometry.scale || 1;
2005
+ const unit = geometry.unit || "mm";
2006
+ const unitScale = Coordinate.convertUnit(1, "mm", unit);
2007
+ const innerR = ((_a = holeData == null ? void 0 : holeData.innerRadius) != null ? _a : 15) * unitScale * scale;
2008
+ const outerR = ((_b = holeData == null ? void 0 : holeData.outerRadius) != null ? _b : 25) * unitScale * scale;
1898
2009
  const newPos = this.calculateConstrainedPosition(
1899
2010
  currentPos,
1900
2011
  constraintGeometry,
1901
- (_a = holeData == null ? void 0 : holeData.innerRadius) != null ? _a : 15,
1902
- (_b = holeData == null ? void 0 : holeData.outerRadius) != null ? _b : 25
2012
+ innerR,
2013
+ outerR
1903
2014
  );
1904
2015
  if (currentPos.distanceFrom(newPos) > 0.1) {
1905
2016
  obj.set({
@@ -1953,19 +2064,16 @@ var HoleTool = class {
1953
2064
  import {
1954
2065
  ContributionPointIds as ContributionPointIds5
1955
2066
  } from "@pooder/core";
1956
- import { FabricImage as Image4, Point as Point2, util } from "fabric";
2067
+ import { Image as Image4, Point as Point2, util } from "fabric";
1957
2068
  var ImageTool = class {
1958
- constructor(options) {
2069
+ constructor() {
1959
2070
  this.id = "pooder.kit.image";
1960
2071
  this.metadata = {
1961
2072
  name: "ImageTool"
1962
2073
  };
1963
- this._loadingUrl = null;
1964
- this.url = "";
1965
- this.opacity = 1;
1966
- if (options) {
1967
- Object.assign(this, options);
1968
- }
2074
+ this.items = [];
2075
+ this.objectMap = /* @__PURE__ */ new Map();
2076
+ this.isUpdatingConfig = false;
1969
2077
  }
1970
2078
  activate(context) {
1971
2079
  this.context = context;
@@ -1976,38 +2084,33 @@ var ImageTool = class {
1976
2084
  }
1977
2085
  const configService = context.services.get("ConfigurationService");
1978
2086
  if (configService) {
1979
- this.url = configService.get("image.url", this.url);
1980
- this.opacity = configService.get("image.opacity", this.opacity);
1981
- this.width = configService.get("image.width", this.width);
1982
- this.height = configService.get("image.height", this.height);
1983
- this.angle = configService.get("image.angle", this.angle);
1984
- this.left = configService.get("image.left", this.left);
1985
- this.top = configService.get("image.top", this.top);
2087
+ this.items = configService.get("image.items", []) || [];
1986
2088
  configService.onAnyChange((e) => {
1987
- if (e.key.startsWith("image.")) {
1988
- const prop = e.key.split(".")[1];
1989
- console.log(
1990
- `[ImageTool] Config change detected: ${e.key} -> ${e.value}`
1991
- );
1992
- if (prop && prop in this) {
1993
- this[prop] = e.value;
1994
- this.updateImage();
1995
- }
2089
+ if (this.isUpdatingConfig) return;
2090
+ let shouldUpdate = false;
2091
+ if (e.key === "image.items") {
2092
+ this.items = e.value || [];
2093
+ shouldUpdate = true;
2094
+ } else if (e.key.startsWith("dieline.") && e.key !== "dieline.holes") {
2095
+ shouldUpdate = true;
2096
+ }
2097
+ if (shouldUpdate) {
2098
+ this.updateImages();
1996
2099
  }
1997
2100
  });
1998
2101
  }
1999
2102
  this.ensureLayer();
2000
- this.updateImage();
2103
+ this.updateImages();
2001
2104
  }
2002
2105
  deactivate(context) {
2003
2106
  if (this.canvasService) {
2004
2107
  const layer = this.canvasService.getLayer("user");
2005
2108
  if (layer) {
2006
- const userImage = this.canvasService.getObject("user-image", "user");
2007
- if (userImage) {
2008
- layer.remove(userImage);
2009
- this.canvasService.requestRenderAll();
2010
- }
2109
+ this.objectMap.forEach((obj) => {
2110
+ layer.remove(obj);
2111
+ });
2112
+ this.objectMap.clear();
2113
+ this.canvasService.requestRenderAll();
2011
2114
  }
2012
2115
  this.canvasService = void 0;
2013
2116
  this.context = void 0;
@@ -2017,82 +2120,103 @@ var ImageTool = class {
2017
2120
  return {
2018
2121
  [ContributionPointIds5.CONFIGURATIONS]: [
2019
2122
  {
2020
- id: "image.url",
2021
- type: "string",
2022
- label: "Image URL",
2023
- default: this.url
2024
- },
2123
+ id: "image.items",
2124
+ type: "array",
2125
+ label: "Images",
2126
+ default: []
2127
+ }
2128
+ ],
2129
+ [ContributionPointIds5.COMMANDS]: [
2025
2130
  {
2026
- id: "image.opacity",
2027
- type: "number",
2028
- label: "Opacity",
2029
- min: 0,
2030
- max: 1,
2031
- step: 0.1,
2032
- default: this.opacity
2131
+ command: "addImage",
2132
+ title: "Add Image",
2133
+ handler: (url, options) => {
2134
+ const newItem = {
2135
+ id: this.generateId(),
2136
+ url,
2137
+ opacity: 1,
2138
+ ...options
2139
+ };
2140
+ this.updateConfig([...this.items, newItem]);
2141
+ return newItem.id;
2142
+ }
2033
2143
  },
2034
2144
  {
2035
- id: "image.width",
2036
- type: "number",
2037
- label: "Width",
2038
- min: 0,
2039
- max: 5e3,
2040
- default: this.width
2145
+ command: "removeImage",
2146
+ title: "Remove Image",
2147
+ handler: (id) => {
2148
+ const newItems = this.items.filter((item) => item.id !== id);
2149
+ if (newItems.length !== this.items.length) {
2150
+ this.updateConfig(newItems);
2151
+ }
2152
+ }
2041
2153
  },
2042
2154
  {
2043
- id: "image.height",
2044
- type: "number",
2045
- label: "Height",
2046
- min: 0,
2047
- max: 5e3,
2048
- default: this.height
2155
+ command: "updateImage",
2156
+ title: "Update Image",
2157
+ handler: (id, updates) => {
2158
+ const index = this.items.findIndex((item) => item.id === id);
2159
+ if (index !== -1) {
2160
+ const newItems = [...this.items];
2161
+ newItems[index] = { ...newItems[index], ...updates };
2162
+ this.updateConfig(newItems);
2163
+ }
2164
+ }
2049
2165
  },
2050
2166
  {
2051
- id: "image.angle",
2052
- type: "number",
2053
- label: "Rotation",
2054
- min: 0,
2055
- max: 360,
2056
- default: this.angle
2167
+ command: "clearImages",
2168
+ title: "Clear Images",
2169
+ handler: () => {
2170
+ this.updateConfig([]);
2171
+ }
2057
2172
  },
2058
2173
  {
2059
- id: "image.left",
2060
- type: "number",
2061
- label: "Left (Normalized)",
2062
- min: 0,
2063
- max: 1,
2064
- default: this.left
2174
+ command: "bringToFront",
2175
+ title: "Bring Image to Front",
2176
+ handler: (id) => {
2177
+ const index = this.items.findIndex((item) => item.id === id);
2178
+ if (index !== -1 && index < this.items.length - 1) {
2179
+ const newItems = [...this.items];
2180
+ const [item] = newItems.splice(index, 1);
2181
+ newItems.push(item);
2182
+ this.updateConfig(newItems);
2183
+ }
2184
+ }
2065
2185
  },
2066
2186
  {
2067
- id: "image.top",
2068
- type: "number",
2069
- label: "Top (Normalized)",
2070
- min: 0,
2071
- max: 1,
2072
- default: this.top
2073
- }
2074
- ],
2075
- [ContributionPointIds5.COMMANDS]: [
2076
- {
2077
- command: "setUserImage",
2078
- title: "Set User Image",
2079
- handler: (url, opacity, width, height, angle, left, top) => {
2080
- if (this.url === url && this.opacity === opacity && this.width === width && this.height === height && this.angle === angle && this.left === left && this.top === top)
2081
- return true;
2082
- this.url = url;
2083
- this.opacity = opacity;
2084
- this.width = width;
2085
- this.height = height;
2086
- this.angle = angle;
2087
- this.left = left;
2088
- this.top = top;
2089
- this.updateImage();
2090
- return true;
2187
+ command: "sendToBack",
2188
+ title: "Send Image to Back",
2189
+ handler: (id) => {
2190
+ const index = this.items.findIndex((item) => item.id === id);
2191
+ if (index > 0) {
2192
+ const newItems = [...this.items];
2193
+ const [item] = newItems.splice(index, 1);
2194
+ newItems.unshift(item);
2195
+ this.updateConfig(newItems);
2196
+ }
2091
2197
  }
2092
2198
  }
2093
2199
  ]
2094
2200
  };
2095
2201
  }
2202
+ generateId() {
2203
+ return Math.random().toString(36).substring(2, 9);
2204
+ }
2205
+ updateConfig(newItems, skipCanvasUpdate = false) {
2206
+ if (!this.context) return;
2207
+ this.isUpdatingConfig = true;
2208
+ this.items = newItems;
2209
+ const configService = this.context.services.get("ConfigurationService");
2210
+ if (configService) {
2211
+ configService.update("image.items", newItems);
2212
+ }
2213
+ if (!skipCanvasUpdate) {
2214
+ this.updateImages();
2215
+ }
2216
+ setTimeout(() => {
2217
+ this.isUpdatingConfig = false;
2218
+ }, 50);
2219
+ }
2096
2220
  ensureLayer() {
2097
2221
  if (!this.canvasService) return;
2098
2222
  let userLayer = this.canvasService.getLayer("user");
@@ -2124,224 +2248,176 @@ var ImageTool = class {
2124
2248
  this.canvasService.requestRenderAll();
2125
2249
  }
2126
2250
  }
2127
- updateImage() {
2251
+ getLayoutInfo() {
2128
2252
  var _a, _b;
2253
+ const canvasW = ((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 800;
2254
+ const canvasH = ((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 600;
2255
+ let layoutScale = 1;
2256
+ let layoutOffsetX = 0;
2257
+ let layoutOffsetY = 0;
2258
+ let visualWidth = canvasW;
2259
+ let visualHeight = canvasH;
2260
+ let dielinePhysicalWidth = 500;
2261
+ let dielinePhysicalHeight = 500;
2262
+ if (this.context) {
2263
+ const configService = this.context.services.get("ConfigurationService");
2264
+ if (configService) {
2265
+ dielinePhysicalWidth = configService.get("dieline.width") || 500;
2266
+ dielinePhysicalHeight = configService.get("dieline.height") || 500;
2267
+ const padding = configService.get("dieline.padding") || 40;
2268
+ const layout = Coordinate.calculateLayout(
2269
+ { width: canvasW, height: canvasH },
2270
+ { width: dielinePhysicalWidth, height: dielinePhysicalHeight },
2271
+ padding
2272
+ );
2273
+ layoutScale = layout.scale;
2274
+ layoutOffsetX = layout.offsetX;
2275
+ layoutOffsetY = layout.offsetY;
2276
+ visualWidth = layout.width;
2277
+ visualHeight = layout.height;
2278
+ }
2279
+ }
2280
+ return {
2281
+ layoutScale,
2282
+ layoutOffsetX,
2283
+ layoutOffsetY,
2284
+ visualWidth,
2285
+ visualHeight,
2286
+ dielinePhysicalWidth,
2287
+ dielinePhysicalHeight
2288
+ };
2289
+ }
2290
+ updateImages() {
2129
2291
  if (!this.canvasService) return;
2130
- let { url, opacity, width, height, angle, left, top } = this;
2131
2292
  const layer = this.canvasService.getLayer("user");
2132
2293
  if (!layer) {
2133
2294
  console.warn("[ImageTool] User layer not found");
2134
2295
  return;
2135
2296
  }
2136
- const userImage = this.canvasService.getObject("user-image", "user");
2137
- if (this._loadingUrl === url) return;
2138
- if (userImage) {
2139
- const currentSrc = ((_a = userImage.getSrc) == null ? void 0 : _a.call(userImage)) || ((_b = userImage._element) == null ? void 0 : _b.src);
2140
- if (currentSrc !== url) {
2141
- this.loadImage(layer);
2297
+ const currentIds = new Set(this.items.map((i) => i.id));
2298
+ for (const [id, obj] of this.objectMap) {
2299
+ if (!currentIds.has(id)) {
2300
+ layer.remove(obj);
2301
+ this.objectMap.delete(id);
2302
+ }
2303
+ }
2304
+ const layout = this.getLayoutInfo();
2305
+ this.items.forEach((item, index) => {
2306
+ let obj = this.objectMap.get(item.id);
2307
+ if (!obj) {
2308
+ this.loadImage(item, layer, layout);
2142
2309
  } else {
2143
- const updates = {};
2144
- const canvasW = this.canvasService.canvas.width || 800;
2145
- const canvasH = this.canvasService.canvas.height || 600;
2146
- const centerX = canvasW / 2;
2147
- const centerY = canvasH / 2;
2148
- if (userImage.opacity !== opacity) updates.opacity = opacity;
2149
- if (angle !== void 0 && userImage.angle !== angle)
2150
- updates.angle = angle;
2151
- if (userImage.originX !== "center") {
2152
- userImage.set({
2153
- originX: "center",
2154
- originY: "center",
2155
- left: userImage.left + userImage.width * userImage.scaleX / 2,
2156
- top: userImage.top + userImage.height * userImage.scaleY / 2
2157
- });
2158
- }
2159
- if (left !== void 0) {
2160
- const globalLeft = Coordinate.toAbsolute(left, canvasW);
2161
- const localLeft = globalLeft - centerX;
2162
- if (Math.abs(userImage.left - localLeft) > 1)
2163
- updates.left = localLeft;
2164
- }
2165
- if (top !== void 0) {
2166
- const globalTop = Coordinate.toAbsolute(top, canvasH);
2167
- const localTop = globalTop - centerY;
2168
- if (Math.abs(userImage.top - localTop) > 1) updates.top = localTop;
2169
- }
2170
- if (width !== void 0 && userImage.width)
2171
- updates.scaleX = width / userImage.width;
2172
- if (height !== void 0 && userImage.height)
2173
- updates.scaleY = height / userImage.height;
2174
- if (Object.keys(updates).length > 0) {
2175
- userImage.set(updates);
2176
- layer.dirty = true;
2177
- this.canvasService.requestRenderAll();
2178
- }
2310
+ this.updateObjectProperties(obj, item, layout);
2311
+ layer.remove(obj);
2312
+ layer.add(obj);
2179
2313
  }
2180
- } else {
2181
- this.loadImage(layer);
2314
+ });
2315
+ layer.dirty = true;
2316
+ this.canvasService.requestRenderAll();
2317
+ }
2318
+ updateObjectProperties(obj, item, layout) {
2319
+ const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight } = layout;
2320
+ const updates = {};
2321
+ if (obj.opacity !== item.opacity) updates.opacity = item.opacity;
2322
+ if (item.angle !== void 0 && obj.angle !== item.angle) updates.angle = item.angle;
2323
+ if (item.left !== void 0) {
2324
+ const globalLeft = layoutOffsetX + item.left * visualWidth;
2325
+ if (Math.abs(obj.left - globalLeft) > 1) updates.left = globalLeft;
2326
+ }
2327
+ if (item.top !== void 0) {
2328
+ const globalTop = layoutOffsetY + item.top * visualHeight;
2329
+ if (Math.abs(obj.top - globalTop) > 1) updates.top = globalTop;
2330
+ }
2331
+ if (item.width !== void 0 && obj.width) {
2332
+ const targetScaleX = item.width * layoutScale / obj.width;
2333
+ if (Math.abs(obj.scaleX - targetScaleX) > 1e-3) updates.scaleX = targetScaleX;
2334
+ }
2335
+ if (item.height !== void 0 && obj.height) {
2336
+ const targetScaleY = item.height * layoutScale / obj.height;
2337
+ if (Math.abs(obj.scaleY - targetScaleY) > 1e-3) updates.scaleY = targetScaleY;
2338
+ }
2339
+ if (obj.originX !== "center") {
2340
+ updates.originX = "center";
2341
+ updates.originY = "center";
2342
+ }
2343
+ if (Object.keys(updates).length > 0) {
2344
+ obj.set(updates);
2182
2345
  }
2183
2346
  }
2184
- loadImage(layer) {
2185
- if (!this.canvasService) return;
2186
- const { url } = this;
2187
- if (!url) return;
2188
- this._loadingUrl = url;
2189
- Image4.fromURL(url, { crossOrigin: "anonymous" }).then((image) => {
2190
- var _a, _b, _c, _d, _e, _f;
2191
- if (this._loadingUrl !== url) return;
2192
- this._loadingUrl = null;
2193
- let { opacity, width, height, angle, left, top } = this;
2194
- if (this.context) {
2195
- const configService = this.context.services.get(
2196
- "ConfigurationService"
2197
- );
2198
- const dielineWidth = configService.get("dieline.width");
2199
- const dielineHeight = configService.get("dieline.height");
2200
- console.log(
2201
- "[ImageTool] Dieline config debug:",
2202
- {
2203
- widthVal: dielineWidth,
2204
- heightVal: dielineHeight,
2205
- // Debug: dump all keys to see what is available
2206
- allKeys: Array.from(
2207
- ((_a = configService.configValues) == null ? void 0 : _a.keys()) || []
2208
- )
2209
- },
2210
- configService
2211
- );
2212
- if (width === void 0 && height === void 0) {
2213
- const scale = Math.min(
2214
- dielineWidth / (image.width || 1),
2215
- dielineHeight / (image.height || 1)
2216
- );
2217
- width = (image.width || 1) * scale;
2218
- height = (image.height || 1) * scale;
2219
- this.width = width;
2220
- this.height = height;
2221
- }
2222
- if (left === void 0 && top === void 0) {
2223
- const dielinePos = configService == null ? void 0 : configService.get("dieline.position");
2224
- if (dielinePos) {
2225
- this.left = dielinePos.x;
2226
- this.top = dielinePos.y;
2227
- } else {
2228
- this.left = 0.5;
2229
- this.top = 0.5;
2230
- }
2231
- left = this.left;
2232
- top = this.top;
2233
- }
2234
- }
2235
- const existingImage = this.canvasService.getObject(
2236
- "user-image",
2237
- "user"
2238
- );
2239
- if (existingImage) {
2240
- const defaultLeft = existingImage.left;
2241
- const defaultTop = existingImage.top;
2242
- const defaultAngle = existingImage.angle;
2243
- const defaultScaleX = existingImage.scaleX;
2244
- const defaultScaleY = existingImage.scaleY;
2245
- const canvasW = ((_b = this.canvasService) == null ? void 0 : _b.canvas.width) || 800;
2246
- const canvasH = ((_c = this.canvasService) == null ? void 0 : _c.canvas.height) || 600;
2247
- const centerX = canvasW / 2;
2248
- const centerY = canvasH / 2;
2249
- let targetLeft = left !== void 0 ? left : defaultLeft;
2250
- let targetTop = top !== void 0 ? top : defaultTop;
2251
- const configService = (_d = this.context) == null ? void 0 : _d.services.get(
2252
- "ConfigurationService"
2253
- );
2254
- console.log("[ImageTool] Loading EXISTING image...", {
2255
- canvasW,
2256
- canvasH,
2257
- centerX,
2258
- centerY,
2259
- incomingLeft: left,
2260
- incomingTop: top,
2261
- dielinePos: configService == null ? void 0 : configService.get("dieline.position"),
2262
- existingImage: !!existingImage
2263
- });
2264
- if (left !== void 0) {
2265
- const globalLeft = Coordinate.toAbsolute(left, canvasW);
2266
- targetLeft = globalLeft;
2267
- console.log("[ImageTool] Calculated targetLeft", {
2268
- globalLeft,
2269
- targetLeft
2270
- });
2271
- }
2272
- if (top !== void 0) {
2273
- const globalTop = Coordinate.toAbsolute(top, canvasH);
2274
- targetTop = globalTop;
2275
- console.log("[ImageTool] Calculated targetTop", {
2276
- globalTop,
2277
- targetTop
2278
- });
2279
- }
2280
- image.set({
2281
- originX: "center",
2282
- // Use center origin for easier positioning
2283
- originY: "center",
2284
- left: targetLeft,
2285
- top: targetTop,
2286
- angle: angle !== void 0 ? angle : defaultAngle,
2287
- scaleX: width !== void 0 && image.width ? width / image.width : defaultScaleX,
2288
- scaleY: height !== void 0 && image.height ? height / image.height : defaultScaleY
2289
- });
2290
- layer.remove(existingImage);
2291
- } else {
2292
- image.set({
2293
- originX: "center",
2294
- originY: "center"
2295
- });
2296
- if (width !== void 0 && image.width)
2297
- image.scaleX = width / image.width;
2298
- if (height !== void 0 && image.height)
2299
- image.scaleY = height / image.height;
2300
- if (angle !== void 0) image.angle = angle;
2301
- const canvasW = ((_e = this.canvasService) == null ? void 0 : _e.canvas.width) || 800;
2302
- const canvasH = ((_f = this.canvasService) == null ? void 0 : _f.canvas.height) || 600;
2303
- const centerX = canvasW / 2;
2304
- const centerY = canvasH / 2;
2305
- if (left !== void 0) {
2306
- image.left = Coordinate.toAbsolute(left, canvasW);
2307
- } else {
2308
- image.left = centerX;
2309
- }
2310
- if (top !== void 0) {
2311
- image.top = Coordinate.toAbsolute(top, canvasH);
2347
+ loadImage(item, layer, layout) {
2348
+ Image4.fromURL(item.url, { crossOrigin: "anonymous" }).then((image) => {
2349
+ var _a;
2350
+ if (!this.items.find((i) => i.id === item.id)) return;
2351
+ image.set({
2352
+ originX: "center",
2353
+ originY: "center",
2354
+ data: { id: item.id }
2355
+ });
2356
+ let { width, height, left, top } = item;
2357
+ const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight, dielinePhysicalWidth, dielinePhysicalHeight } = layout;
2358
+ if (width === void 0 && height === void 0) {
2359
+ const imgAspect = (image.width || 1) / (image.height || 1);
2360
+ const dielineAspect = dielinePhysicalWidth / dielinePhysicalHeight;
2361
+ if (imgAspect > dielineAspect) {
2362
+ const w = dielinePhysicalWidth;
2363
+ width = w;
2364
+ height = w / imgAspect;
2312
2365
  } else {
2313
- image.top = centerY;
2366
+ const h = dielinePhysicalHeight;
2367
+ height = h;
2368
+ width = h * imgAspect;
2314
2369
  }
2370
+ item.width = width;
2371
+ item.height = height;
2315
2372
  }
2316
- image.set({
2317
- opacity: opacity !== void 0 ? opacity : 1,
2318
- data: {
2319
- id: "user-image"
2320
- }
2321
- });
2373
+ if (left === void 0 && top === void 0) {
2374
+ left = 0.5;
2375
+ top = 0.5;
2376
+ item.left = left;
2377
+ item.top = top;
2378
+ }
2379
+ this.updateObjectProperties(image, item, layout);
2322
2380
  layer.add(image);
2381
+ this.objectMap.set(item.id, image);
2323
2382
  image.on("modified", (e) => {
2324
- var _a2, _b2;
2325
- const matrix = image.calcTransformMatrix();
2326
- const globalPoint = util.transformPoint(new Point2(0, 0), matrix);
2327
- const canvasW = ((_a2 = this.canvasService) == null ? void 0 : _a2.canvas.width) || 800;
2328
- const canvasH = ((_b2 = this.canvasService) == null ? void 0 : _b2.canvas.height) || 600;
2329
- this.left = Coordinate.toNormalized(globalPoint.x, canvasW);
2330
- this.top = Coordinate.toNormalized(globalPoint.y, canvasH);
2331
- this.angle = e.target.angle;
2332
- if (image.width) this.width = e.target.width * e.target.scaleX;
2333
- if (image.height) this.height = e.target.height * e.target.scaleY;
2334
- if (this.context) {
2335
- this.context.eventBus.emit("update");
2336
- }
2383
+ this.handleObjectModified(item.id, image);
2337
2384
  });
2338
2385
  layer.dirty = true;
2339
- this.canvasService.requestRenderAll();
2386
+ (_a = this.canvasService) == null ? void 0 : _a.requestRenderAll();
2387
+ if (item.width !== width || item.height !== height || item.left !== left || item.top !== top) {
2388
+ this.updateImageInConfig(item.id, { width, height, left, top });
2389
+ }
2340
2390
  }).catch((err) => {
2341
- if (this._loadingUrl === url) this._loadingUrl = null;
2342
- console.error("Failed to load image", url, err);
2391
+ console.error("Failed to load image", item.url, err);
2343
2392
  });
2344
2393
  }
2394
+ handleObjectModified(id, image) {
2395
+ const layout = this.getLayoutInfo();
2396
+ const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight } = layout;
2397
+ const matrix = image.calcTransformMatrix();
2398
+ const globalPoint = util.transformPoint(new Point2(0, 0), matrix);
2399
+ const updates = {};
2400
+ updates.left = (globalPoint.x - layoutOffsetX) / visualWidth;
2401
+ updates.top = (globalPoint.y - layoutOffsetY) / visualHeight;
2402
+ updates.angle = image.angle;
2403
+ if (image.width) {
2404
+ const pixelWidth = image.width * image.scaleX;
2405
+ updates.width = pixelWidth / layoutScale;
2406
+ }
2407
+ if (image.height) {
2408
+ const pixelHeight = image.height * image.scaleY;
2409
+ updates.height = pixelHeight / layoutScale;
2410
+ }
2411
+ this.updateImageInConfig(id, updates);
2412
+ }
2413
+ updateImageInConfig(id, updates) {
2414
+ const index = this.items.findIndex((i) => i.id === id);
2415
+ if (index !== -1) {
2416
+ const newItems = [...this.items];
2417
+ newItems[index] = { ...newItems[index], ...updates };
2418
+ this.updateConfig(newItems, true);
2419
+ }
2420
+ }
2345
2421
  };
2346
2422
 
2347
2423
  // src/white-ink.ts
@@ -2641,19 +2717,25 @@ var WhiteInkTool = class {
2641
2717
  import {
2642
2718
  ContributionPointIds as ContributionPointIds7
2643
2719
  } from "@pooder/core";
2644
- import { Rect as Rect2, Line, Text } from "fabric";
2720
+ import { Line, Text, Group as Group2, Polygon } from "fabric";
2645
2721
  var RulerTool = class {
2646
2722
  constructor(options) {
2647
2723
  this.id = "pooder.kit.ruler";
2648
2724
  this.metadata = {
2649
2725
  name: "RulerTool"
2650
2726
  };
2651
- this.unit = "px";
2652
2727
  this.thickness = 20;
2728
+ this.gap = 15;
2653
2729
  this.backgroundColor = "#f0f0f0";
2654
2730
  this.textColor = "#333333";
2655
2731
  this.lineColor = "#999999";
2656
2732
  this.fontSize = 10;
2733
+ // Dieline context for sync
2734
+ this.dielineWidth = 500;
2735
+ this.dielineHeight = 500;
2736
+ this.dielineUnit = "mm";
2737
+ this.dielinePadding = 40;
2738
+ this.dielineOffset = 0;
2657
2739
  if (options) {
2658
2740
  Object.assign(this, options);
2659
2741
  }
@@ -2666,8 +2748,8 @@ var RulerTool = class {
2666
2748
  }
2667
2749
  const configService = context.services.get("ConfigurationService");
2668
2750
  if (configService) {
2669
- this.unit = configService.get("ruler.unit", this.unit);
2670
2751
  this.thickness = configService.get("ruler.thickness", this.thickness);
2752
+ this.gap = configService.get("ruler.gap", this.gap);
2671
2753
  this.backgroundColor = configService.get(
2672
2754
  "ruler.backgroundColor",
2673
2755
  this.backgroundColor
@@ -2675,13 +2757,38 @@ var RulerTool = class {
2675
2757
  this.textColor = configService.get("ruler.textColor", this.textColor);
2676
2758
  this.lineColor = configService.get("ruler.lineColor", this.lineColor);
2677
2759
  this.fontSize = configService.get("ruler.fontSize", this.fontSize);
2760
+ this.dielineUnit = configService.get("dieline.unit", this.dielineUnit);
2761
+ this.dielineWidth = configService.get("dieline.width", this.dielineWidth);
2762
+ this.dielineHeight = configService.get(
2763
+ "dieline.height",
2764
+ this.dielineHeight
2765
+ );
2766
+ this.dielinePadding = configService.get(
2767
+ "dieline.padding",
2768
+ this.dielinePadding
2769
+ );
2770
+ this.dielineOffset = configService.get(
2771
+ "dieline.offset",
2772
+ this.dielineOffset
2773
+ );
2678
2774
  configService.onAnyChange((e) => {
2775
+ let shouldUpdate = false;
2679
2776
  if (e.key.startsWith("ruler.")) {
2680
2777
  const prop = e.key.split(".")[1];
2681
2778
  if (prop && prop in this) {
2682
2779
  this[prop] = e.value;
2683
- this.updateRuler();
2780
+ shouldUpdate = true;
2684
2781
  }
2782
+ } else if (e.key.startsWith("dieline.")) {
2783
+ if (e.key === "dieline.unit") this.dielineUnit = e.value;
2784
+ if (e.key === "dieline.width") this.dielineWidth = e.value;
2785
+ if (e.key === "dieline.height") this.dielineHeight = e.value;
2786
+ if (e.key === "dieline.padding") this.dielinePadding = e.value;
2787
+ if (e.key === "dieline.offset") this.dielineOffset = e.value;
2788
+ shouldUpdate = true;
2789
+ }
2790
+ if (shouldUpdate) {
2791
+ this.updateRuler();
2685
2792
  }
2686
2793
  });
2687
2794
  }
@@ -2695,13 +2802,6 @@ var RulerTool = class {
2695
2802
  contribute() {
2696
2803
  return {
2697
2804
  [ContributionPointIds7.CONFIGURATIONS]: [
2698
- {
2699
- id: "ruler.unit",
2700
- type: "select",
2701
- label: "Unit",
2702
- options: ["px", "mm", "cm", "in"],
2703
- default: "px"
2704
- },
2705
2805
  {
2706
2806
  id: "ruler.thickness",
2707
2807
  type: "number",
@@ -2710,6 +2810,14 @@ var RulerTool = class {
2710
2810
  max: 100,
2711
2811
  default: 20
2712
2812
  },
2813
+ {
2814
+ id: "ruler.gap",
2815
+ type: "number",
2816
+ label: "Gap",
2817
+ min: 0,
2818
+ max: 100,
2819
+ default: 15
2820
+ },
2713
2821
  {
2714
2822
  id: "ruler.backgroundColor",
2715
2823
  type: "color",
@@ -2738,16 +2846,6 @@ var RulerTool = class {
2738
2846
  }
2739
2847
  ],
2740
2848
  [ContributionPointIds7.COMMANDS]: [
2741
- {
2742
- command: "setUnit",
2743
- title: "Set Ruler Unit",
2744
- handler: (unit) => {
2745
- if (this.unit === unit) return true;
2746
- this.unit = unit;
2747
- this.updateRuler();
2748
- return true;
2749
- }
2750
- },
2751
2849
  {
2752
2850
  command: "setTheme",
2753
2851
  title: "Set Ruler Theme",
@@ -2798,6 +2896,68 @@ var RulerTool = class {
2798
2896
  this.canvasService.canvas.remove(layer);
2799
2897
  }
2800
2898
  }
2899
+ createArrowLine(x1, y1, x2, y2, color) {
2900
+ const line = new Line([x1, y1, x2, y2], {
2901
+ stroke: color,
2902
+ strokeWidth: this.thickness / 20,
2903
+ // Scale stroke width relative to thickness (default 1)
2904
+ selectable: false,
2905
+ evented: false
2906
+ });
2907
+ const arrowSize = Math.max(4, this.thickness * 0.3);
2908
+ const angle = Math.atan2(y2 - y1, x2 - x1);
2909
+ const endArrow = new Polygon(
2910
+ [
2911
+ { x: 0, y: 0 },
2912
+ { x: -arrowSize, y: -arrowSize / 2 },
2913
+ { x: -arrowSize, y: arrowSize / 2 }
2914
+ ],
2915
+ {
2916
+ fill: color,
2917
+ left: x2,
2918
+ top: y2,
2919
+ originX: "right",
2920
+ originY: "center",
2921
+ angle: angle * 180 / Math.PI,
2922
+ selectable: false,
2923
+ evented: false
2924
+ }
2925
+ );
2926
+ const startArrow = new Polygon(
2927
+ [
2928
+ { x: 0, y: 0 },
2929
+ { x: arrowSize, y: -arrowSize / 2 },
2930
+ { x: arrowSize, y: arrowSize / 2 }
2931
+ ],
2932
+ {
2933
+ fill: color,
2934
+ left: x1,
2935
+ top: y1,
2936
+ originX: "left",
2937
+ originY: "center",
2938
+ angle: angle * 180 / Math.PI,
2939
+ selectable: false,
2940
+ evented: false
2941
+ }
2942
+ );
2943
+ return new Group2([line, startArrow, endArrow], {
2944
+ selectable: false,
2945
+ evented: false
2946
+ });
2947
+ }
2948
+ resolvePadding(containerWidth, containerHeight) {
2949
+ if (typeof this.dielinePadding === "number") {
2950
+ return this.dielinePadding;
2951
+ }
2952
+ if (typeof this.dielinePadding === "string") {
2953
+ if (this.dielinePadding.endsWith("%")) {
2954
+ const percent = parseFloat(this.dielinePadding) / 100;
2955
+ return Math.min(containerWidth, containerHeight) * percent;
2956
+ }
2957
+ return parseFloat(this.dielinePadding) || 0;
2958
+ }
2959
+ return 0;
2960
+ }
2801
2961
  updateRuler() {
2802
2962
  if (!this.canvasService) return;
2803
2963
  const layer = this.getLayer();
@@ -2806,95 +2966,141 @@ var RulerTool = class {
2806
2966
  const { thickness, backgroundColor, lineColor, textColor, fontSize } = this;
2807
2967
  const width = this.canvasService.canvas.width || 800;
2808
2968
  const height = this.canvasService.canvas.height || 600;
2809
- const topBg = new Rect2({
2810
- left: 0,
2811
- top: 0,
2812
- width,
2813
- height: thickness,
2814
- fill: backgroundColor,
2815
- selectable: false,
2816
- evented: false
2817
- });
2818
- const leftBg = new Rect2({
2819
- left: 0,
2820
- top: 0,
2821
- width: thickness,
2822
- height,
2823
- fill: backgroundColor,
2824
- selectable: false,
2825
- evented: false
2826
- });
2827
- const cornerBg = new Rect2({
2828
- left: 0,
2829
- top: 0,
2830
- width: thickness,
2831
- height: thickness,
2832
- fill: backgroundColor,
2833
- stroke: lineColor,
2834
- strokeWidth: 1,
2969
+ const paddingPx = this.resolvePadding(width, height);
2970
+ const layout = Coordinate.calculateLayout(
2971
+ { width, height },
2972
+ { width: this.dielineWidth, height: this.dielineHeight },
2973
+ paddingPx
2974
+ );
2975
+ const scale = layout.scale;
2976
+ const offsetX = layout.offsetX;
2977
+ const offsetY = layout.offsetY;
2978
+ const visualWidth = layout.width;
2979
+ const visualHeight = layout.height;
2980
+ const rawOffset = this.dielineOffset || 0;
2981
+ const effectiveOffset = rawOffset > 0 ? rawOffset : 0;
2982
+ const expandPixels = effectiveOffset * scale;
2983
+ const gap = this.gap || 15;
2984
+ const rulerLeft = offsetX - expandPixels;
2985
+ const rulerTop = offsetY - expandPixels;
2986
+ const rulerRight = offsetX + visualWidth + expandPixels;
2987
+ const rulerBottom = offsetY + visualHeight + expandPixels;
2988
+ const displayWidth = this.dielineWidth + effectiveOffset * 2;
2989
+ const displayHeight = this.dielineHeight + effectiveOffset * 2;
2990
+ const topRulerY = rulerTop - gap;
2991
+ const topRulerXStart = rulerLeft;
2992
+ const topRulerXEnd = rulerRight;
2993
+ const leftRulerX = rulerLeft - gap;
2994
+ const leftRulerYStart = rulerTop;
2995
+ const leftRulerYEnd = rulerBottom;
2996
+ const topDimLine = this.createArrowLine(
2997
+ topRulerXStart,
2998
+ topRulerY,
2999
+ topRulerXEnd,
3000
+ topRulerY,
3001
+ lineColor
3002
+ );
3003
+ layer.add(topDimLine);
3004
+ const extLen = 5;
3005
+ layer.add(
3006
+ new Line(
3007
+ [
3008
+ topRulerXStart,
3009
+ topRulerY - extLen,
3010
+ topRulerXStart,
3011
+ topRulerY + extLen
3012
+ ],
3013
+ {
3014
+ stroke: lineColor,
3015
+ strokeWidth: 1,
3016
+ selectable: false,
3017
+ evented: false
3018
+ }
3019
+ )
3020
+ );
3021
+ layer.add(
3022
+ new Line(
3023
+ [topRulerXEnd, topRulerY - extLen, topRulerXEnd, topRulerY + extLen],
3024
+ {
3025
+ stroke: lineColor,
3026
+ strokeWidth: 1,
3027
+ selectable: false,
3028
+ evented: false
3029
+ }
3030
+ )
3031
+ );
3032
+ const widthStr = parseFloat(displayWidth.toFixed(2)).toString();
3033
+ const topTextContent = `${widthStr} ${this.dielineUnit}`;
3034
+ const topText = new Text(topTextContent, {
3035
+ left: topRulerXStart + (rulerRight - rulerLeft) / 2,
3036
+ top: topRulerY,
3037
+ fontSize,
3038
+ fill: textColor,
3039
+ fontFamily: "Arial",
3040
+ originX: "center",
3041
+ originY: "center",
3042
+ backgroundColor,
3043
+ // Background mask for readability
2835
3044
  selectable: false,
2836
3045
  evented: false
2837
3046
  });
2838
- layer.add(topBg);
2839
- layer.add(leftBg);
2840
- layer.add(cornerBg);
2841
- const step = 100;
2842
- const subStep = 10;
2843
- const midStep = 50;
2844
- for (let x = 0; x <= width; x += subStep) {
2845
- if (x < thickness) continue;
2846
- let len = thickness * 0.25;
2847
- if (x % step === 0) len = thickness * 0.8;
2848
- else if (x % midStep === 0) len = thickness * 0.5;
2849
- const line = new Line([x, thickness - len, x, thickness], {
2850
- stroke: lineColor,
2851
- strokeWidth: 1,
2852
- selectable: false,
2853
- evented: false
2854
- });
2855
- layer.add(line);
2856
- if (x % step === 0) {
2857
- const text = new Text(x.toString(), {
2858
- left: x + 2,
2859
- top: 2,
2860
- fontSize,
2861
- fill: textColor,
2862
- fontFamily: "Arial",
3047
+ layer.add(topText);
3048
+ const leftDimLine = this.createArrowLine(
3049
+ leftRulerX,
3050
+ leftRulerYStart,
3051
+ leftRulerX,
3052
+ leftRulerYEnd,
3053
+ lineColor
3054
+ );
3055
+ layer.add(leftDimLine);
3056
+ layer.add(
3057
+ new Line(
3058
+ [
3059
+ leftRulerX - extLen,
3060
+ leftRulerYStart,
3061
+ leftRulerX + extLen,
3062
+ leftRulerYStart
3063
+ ],
3064
+ {
3065
+ stroke: lineColor,
3066
+ strokeWidth: 1,
2863
3067
  selectable: false,
2864
3068
  evented: false
2865
- });
2866
- layer.add(text);
2867
- }
2868
- }
2869
- for (let y = 0; y <= height; y += subStep) {
2870
- if (y < thickness) continue;
2871
- let len = thickness * 0.25;
2872
- if (y % step === 0) len = thickness * 0.8;
2873
- else if (y % midStep === 0) len = thickness * 0.5;
2874
- const line = new Line([thickness - len, y, thickness, y], {
2875
- stroke: lineColor,
2876
- strokeWidth: 1,
2877
- selectable: false,
2878
- evented: false
2879
- });
2880
- layer.add(line);
2881
- if (y % step === 0) {
2882
- const text = new Text(y.toString(), {
2883
- angle: -90,
2884
- left: thickness / 2 - fontSize / 3,
2885
- // approximate centering
2886
- top: y + fontSize,
2887
- fontSize,
2888
- fill: textColor,
2889
- fontFamily: "Arial",
2890
- originX: "center",
2891
- originY: "center",
3069
+ }
3070
+ )
3071
+ );
3072
+ layer.add(
3073
+ new Line(
3074
+ [
3075
+ leftRulerX - extLen,
3076
+ leftRulerYEnd,
3077
+ leftRulerX + extLen,
3078
+ leftRulerYEnd
3079
+ ],
3080
+ {
3081
+ stroke: lineColor,
3082
+ strokeWidth: 1,
2892
3083
  selectable: false,
2893
3084
  evented: false
2894
- });
2895
- layer.add(text);
2896
- }
2897
- }
3085
+ }
3086
+ )
3087
+ );
3088
+ const heightStr = parseFloat(displayHeight.toFixed(2)).toString();
3089
+ const leftTextContent = `${heightStr} ${this.dielineUnit}`;
3090
+ const leftText = new Text(leftTextContent, {
3091
+ left: leftRulerX,
3092
+ top: leftRulerYStart + (rulerBottom - rulerTop) / 2,
3093
+ angle: -90,
3094
+ fontSize,
3095
+ fill: textColor,
3096
+ fontFamily: "Arial",
3097
+ originX: "center",
3098
+ originY: "center",
3099
+ backgroundColor,
3100
+ selectable: false,
3101
+ evented: false
3102
+ });
3103
+ layer.add(leftText);
2898
3104
  this.canvasService.canvas.bringObjectToFront(layer);
2899
3105
  this.canvasService.canvas.requestRenderAll();
2900
3106
  }
@@ -2993,7 +3199,7 @@ var MirrorTool = class {
2993
3199
  };
2994
3200
 
2995
3201
  // src/CanvasService.ts
2996
- import { Canvas, Group as Group2 } from "fabric";
3202
+ import { Canvas, Group as Group3 } from "fabric";
2997
3203
  var CanvasService = class {
2998
3204
  constructor(el, options) {
2999
3205
  if (el instanceof Canvas) {
@@ -3030,7 +3236,7 @@ var CanvasService = class {
3030
3236
  ...options,
3031
3237
  data: { ...options.data, id }
3032
3238
  };
3033
- layer = new Group2([], defaultOptions);
3239
+ layer = new Group3([], defaultOptions);
3034
3240
  this.canvas.add(layer);
3035
3241
  }
3036
3242
  return layer;