@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.js CHANGED
@@ -434,6 +434,24 @@ var ImageTracer = class {
434
434
 
435
435
  // src/coordinate.ts
436
436
  var Coordinate = class {
437
+ /**
438
+ * Calculate layout to fit content within container while preserving aspect ratio.
439
+ */
440
+ static calculateLayout(container, content, padding = 0) {
441
+ const availableWidth = Math.max(0, container.width - padding * 2);
442
+ const availableHeight = Math.max(0, container.height - padding * 2);
443
+ if (content.width === 0 || content.height === 0) {
444
+ return { scale: 1, offsetX: 0, offsetY: 0, width: 0, height: 0 };
445
+ }
446
+ const scaleX = availableWidth / content.width;
447
+ const scaleY = availableHeight / content.height;
448
+ const scale = Math.min(scaleX, scaleY);
449
+ const width = content.width * scale;
450
+ const height = content.height * scale;
451
+ const offsetX = (container.width - width) / 2;
452
+ const offsetY = (container.height - height) / 2;
453
+ return { scale, offsetX, offsetY, width, height };
454
+ }
437
455
  /**
438
456
  * Convert an absolute value to a normalized value (0-1).
439
457
  * @param value Absolute value (e.g., pixels)
@@ -468,6 +486,21 @@ var Coordinate = class {
468
486
  y: this.toAbsolute(point.y, size.height)
469
487
  };
470
488
  }
489
+ static convertUnit(value, from, to) {
490
+ if (from === to) return value;
491
+ const toMM = {
492
+ px: 0.264583,
493
+ // 1px = 0.264583mm (96 DPI)
494
+ mm: 1,
495
+ cm: 10,
496
+ in: 25.4
497
+ };
498
+ const mmValue = value * (from === "px" ? toMM.px : toMM[from] || 1);
499
+ if (to === "px") {
500
+ return mmValue / toMM.px;
501
+ }
502
+ return mmValue / (toMM[to] || 1);
503
+ }
471
504
  };
472
505
 
473
506
  // src/geometry.ts
@@ -524,9 +557,10 @@ function resolveHolePosition(hole, geometry, canvasSize) {
524
557
  y: by + (hole.offsetY || 0)
525
558
  };
526
559
  } else if (hole.x !== void 0 && hole.y !== void 0) {
560
+ const { x, width, y, height } = geometry;
527
561
  return {
528
- x: hole.x * canvasSize.width,
529
- y: hole.y * canvasSize.height
562
+ x: hole.x * width + (x - width / 2) + (hole.offsetX || 0),
563
+ y: hole.y * height + (y - height / 2) + (hole.offsetY || 0)
530
564
  };
531
565
  }
532
566
  return { x: 0, y: 0 };
@@ -632,7 +666,10 @@ function getDielineShape(options) {
632
666
  cutsPath.remove();
633
667
  mainShape = temp;
634
668
  } catch (e) {
635
- console.error("Geometry: Failed to subtract cutsPath from mainShape", e);
669
+ console.error(
670
+ "Geometry: Failed to subtract cutsPath from mainShape",
671
+ e
672
+ );
636
673
  }
637
674
  }
638
675
  }
@@ -725,7 +762,12 @@ function getPathBounds(pathData) {
725
762
  path.pathData = pathData;
726
763
  const bounds = path.bounds;
727
764
  path.remove();
728
- return { width: bounds.width, height: bounds.height };
765
+ return {
766
+ x: bounds.x,
767
+ y: bounds.y,
768
+ width: bounds.width,
769
+ height: bounds.height
770
+ };
729
771
  }
730
772
 
731
773
  // src/dieline.ts
@@ -735,6 +777,7 @@ var DielineTool = class {
735
777
  this.metadata = {
736
778
  name: "DielineTool"
737
779
  };
780
+ this.unit = "mm";
738
781
  this.shape = "rect";
739
782
  this.width = 500;
740
783
  this.height = 500;
@@ -745,6 +788,7 @@ var DielineTool = class {
745
788
  this.outsideColor = "#ffffff";
746
789
  this.showBleedLines = true;
747
790
  this.holes = [];
791
+ this.padding = 140;
748
792
  if (options) {
749
793
  Object.assign(this, options);
750
794
  }
@@ -758,14 +802,12 @@ var DielineTool = class {
758
802
  }
759
803
  const configService = context.services.get("ConfigurationService");
760
804
  if (configService) {
805
+ this.unit = configService.get("dieline.unit", this.unit);
761
806
  this.shape = configService.get("dieline.shape", this.shape);
762
807
  this.width = configService.get("dieline.width", this.width);
763
808
  this.height = configService.get("dieline.height", this.height);
764
809
  this.radius = configService.get("dieline.radius", this.radius);
765
- this.borderLength = configService.get(
766
- "dieline.borderLength",
767
- this.borderLength
768
- );
810
+ this.padding = configService.get("dieline.padding", this.padding);
769
811
  this.offset = configService.get("dieline.offset", this.offset);
770
812
  this.style = configService.get("dieline.style", this.style);
771
813
  this.insideColor = configService.get(
@@ -806,6 +848,13 @@ var DielineTool = class {
806
848
  contribute() {
807
849
  return {
808
850
  [import_core2.ContributionPointIds.CONFIGURATIONS]: [
851
+ {
852
+ id: "dieline.unit",
853
+ type: "select",
854
+ label: "Unit",
855
+ options: ["px", "mm", "cm", "in"],
856
+ default: this.unit
857
+ },
809
858
  {
810
859
  id: "dieline.shape",
811
860
  type: "select",
@@ -841,15 +890,14 @@ var DielineTool = class {
841
890
  id: "dieline.position",
842
891
  type: "json",
843
892
  label: "Position (Normalized)",
844
- default: this.position
893
+ default: this.radius
845
894
  },
846
895
  {
847
- id: "dieline.borderLength",
848
- type: "number",
849
- label: "Margin",
850
- min: 0,
851
- max: 500,
852
- default: this.borderLength
896
+ id: "dieline.padding",
897
+ type: "select",
898
+ label: "View Padding",
899
+ options: [0, 10, 20, 40, 60, 100, "2%", "5%", "10%", "15%", "20%"],
900
+ default: this.padding
853
901
  },
854
902
  {
855
903
  id: "dieline.offset",
@@ -918,7 +966,9 @@ var DielineTool = class {
918
966
  const scale = currentMax / Math.max(bounds.width, bounds.height);
919
967
  const newWidth = bounds.width * scale;
920
968
  const newHeight = bounds.height * scale;
921
- const configService = (_a = this.context) == null ? void 0 : _a.services.get("ConfigurationService");
969
+ const configService = (_a = this.context) == null ? void 0 : _a.services.get(
970
+ "ConfigurationService"
971
+ );
922
972
  if (configService) {
923
973
  configService.update("dieline.width", newWidth);
924
974
  configService.update("dieline.height", newHeight);
@@ -983,12 +1033,25 @@ var DielineTool = class {
983
1033
  }
984
1034
  return new import_fabric2.Pattern({ source: canvas, repetition: "repeat" });
985
1035
  }
1036
+ resolvePadding(containerWidth, containerHeight) {
1037
+ if (typeof this.padding === "number") {
1038
+ return this.padding;
1039
+ }
1040
+ if (typeof this.padding === "string") {
1041
+ if (this.padding.endsWith("%")) {
1042
+ const percent = parseFloat(this.padding) / 100;
1043
+ return Math.min(containerWidth, containerHeight) * percent;
1044
+ }
1045
+ return parseFloat(this.padding) || 0;
1046
+ }
1047
+ return 0;
1048
+ }
986
1049
  updateDieline(emitEvent = true) {
987
- var _a, _b;
988
1050
  if (!this.canvasService) return;
989
1051
  const layer = this.getLayer();
990
1052
  if (!layer) return;
991
1053
  const {
1054
+ unit,
992
1055
  shape,
993
1056
  radius,
994
1057
  offset,
@@ -996,43 +1059,60 @@ var DielineTool = class {
996
1059
  insideColor,
997
1060
  outsideColor,
998
1061
  position,
999
- borderLength,
1000
1062
  showBleedLines,
1001
1063
  holes
1002
1064
  } = this;
1003
1065
  let { width, height } = this;
1004
1066
  const canvasW = this.canvasService.canvas.width || 800;
1005
1067
  const canvasH = this.canvasService.canvas.height || 600;
1006
- let visualWidth = width;
1007
- let visualHeight = height;
1008
- if (borderLength && borderLength > 0) {
1009
- visualWidth = Math.max(0, canvasW - borderLength * 2);
1010
- visualHeight = Math.max(0, canvasH - borderLength * 2);
1011
- }
1012
- const cx = Coordinate.toAbsolute((_a = position == null ? void 0 : position.x) != null ? _a : 0.5, canvasW);
1013
- const cy = Coordinate.toAbsolute((_b = position == null ? void 0 : position.y) != null ? _b : 0.5, canvasH);
1068
+ const paddingPx = this.resolvePadding(canvasW, canvasH);
1069
+ const layout = Coordinate.calculateLayout(
1070
+ { width: canvasW, height: canvasH },
1071
+ { width, height },
1072
+ paddingPx
1073
+ );
1074
+ const scale = layout.scale;
1075
+ const cx = layout.offsetX + layout.width / 2;
1076
+ const cy = layout.offsetY + layout.height / 2;
1077
+ const visualWidth = layout.width;
1078
+ const visualHeight = layout.height;
1079
+ const visualRadius = radius * scale;
1080
+ const visualOffset = offset * scale;
1014
1081
  layer.remove(...layer.getObjects());
1015
1082
  const geometryForHoles = {
1016
1083
  x: cx,
1017
1084
  y: cy,
1018
1085
  width: visualWidth,
1019
1086
  height: visualHeight
1087
+ // Pass scale/unit context if needed by resolveHolePosition (though currently unused there)
1020
1088
  };
1021
1089
  const absoluteHoles = (holes || []).map((h) => {
1022
- const pos = resolveHolePosition(
1023
- h,
1024
- geometryForHoles,
1025
- { width: canvasW, height: canvasH }
1026
- );
1090
+ const unitScale = Coordinate.convertUnit(1, "mm", unit);
1091
+ const offsetScale = unitScale * scale;
1092
+ const hWithPixelOffsets = {
1093
+ ...h,
1094
+ offsetX: (h.offsetX || 0) * offsetScale,
1095
+ offsetY: (h.offsetY || 0) * offsetScale
1096
+ };
1097
+ const pos = resolveHolePosition(hWithPixelOffsets, geometryForHoles, {
1098
+ width: canvasW,
1099
+ height: canvasH
1100
+ });
1027
1101
  return {
1028
1102
  ...h,
1029
1103
  x: pos.x,
1030
- y: pos.y
1104
+ y: pos.y,
1105
+ // Scale hole radii: mm -> current unit -> pixels
1106
+ innerRadius: h.innerRadius * offsetScale,
1107
+ outerRadius: h.outerRadius * offsetScale,
1108
+ // Store scaled offsets in the result for consistency, though pos is already resolved
1109
+ offsetX: hWithPixelOffsets.offsetX,
1110
+ offsetY: hWithPixelOffsets.offsetY
1031
1111
  };
1032
1112
  });
1033
- const cutW = Math.max(0, width + offset * 2);
1034
- const cutH = Math.max(0, height + offset * 2);
1035
- const cutR = radius === 0 ? 0 : Math.max(0, radius + offset);
1113
+ const cutW = Math.max(0, visualWidth + visualOffset * 2);
1114
+ const cutH = Math.max(0, visualHeight + visualOffset * 2);
1115
+ const cutR = visualRadius === 0 ? 0 : Math.max(0, visualRadius + visualOffset);
1036
1116
  const maskPathData = generateMaskPath({
1037
1117
  canvasWidth: canvasW,
1038
1118
  canvasHeight: canvasH,
@@ -1082,15 +1162,15 @@ var DielineTool = class {
1082
1162
  const bleedPathData = generateBleedZonePath(
1083
1163
  {
1084
1164
  shape,
1085
- width,
1086
- height,
1087
- radius,
1165
+ width: visualWidth,
1166
+ height: visualHeight,
1167
+ radius: visualRadius,
1088
1168
  x: cx,
1089
1169
  y: cy,
1090
1170
  holes: absoluteHoles,
1091
1171
  pathData: this.pathData
1092
1172
  },
1093
- offset
1173
+ visualOffset
1094
1174
  );
1095
1175
  if (showBleedLines !== false) {
1096
1176
  const pattern = this.createHatchPattern("red");
@@ -1133,13 +1213,12 @@ var DielineTool = class {
1133
1213
  }
1134
1214
  const borderPathData = generateDielinePath({
1135
1215
  shape,
1136
- width,
1137
- height,
1138
- radius,
1216
+ width: visualWidth,
1217
+ height: visualHeight,
1218
+ radius: visualRadius,
1139
1219
  x: cx,
1140
1220
  y: cy,
1141
1221
  holes: absoluteHoles,
1142
- // FIX: Use absoluteHoles instead of holes
1143
1222
  pathData: this.pathData
1144
1223
  });
1145
1224
  const borderObj = new import_fabric2.Path(borderPathData, {
@@ -1177,115 +1256,107 @@ var DielineTool = class {
1177
1256
  }
1178
1257
  }
1179
1258
  getGeometry() {
1180
- var _a, _b;
1181
1259
  if (!this.canvasService) return null;
1182
- const { shape, width, height, radius, position, borderLength, offset } = this;
1260
+ const { unit, shape, width, height, radius, position, offset } = this;
1183
1261
  const canvasW = this.canvasService.canvas.width || 800;
1184
1262
  const canvasH = this.canvasService.canvas.height || 600;
1185
- let visualWidth = width;
1186
- let visualHeight = height;
1187
- if (borderLength && borderLength > 0) {
1188
- visualWidth = Math.max(0, canvasW - borderLength * 2);
1189
- visualHeight = Math.max(0, canvasH - borderLength * 2);
1190
- }
1191
- const cx = Coordinate.toAbsolute((_a = position == null ? void 0 : position.x) != null ? _a : 0.5, canvasW);
1192
- const cy = Coordinate.toAbsolute((_b = position == null ? void 0 : position.y) != null ? _b : 0.5, canvasH);
1263
+ const paddingPx = this.resolvePadding(canvasW, canvasH);
1264
+ const layout = Coordinate.calculateLayout(
1265
+ { width: canvasW, height: canvasH },
1266
+ { width, height },
1267
+ paddingPx
1268
+ );
1269
+ const scale = layout.scale;
1270
+ const cx = layout.offsetX + layout.width / 2;
1271
+ const cy = layout.offsetY + layout.height / 2;
1272
+ const visualWidth = layout.width;
1273
+ const visualHeight = layout.height;
1193
1274
  return {
1194
1275
  shape,
1276
+ unit,
1195
1277
  x: cx,
1196
1278
  y: cy,
1197
1279
  width: visualWidth,
1198
1280
  height: visualHeight,
1199
- radius,
1200
- offset,
1201
- borderLength,
1281
+ radius: radius * scale,
1282
+ offset: offset * scale,
1283
+ // Pass scale to help other tools (like HoleTool) convert units
1284
+ scale,
1202
1285
  pathData: this.pathData
1203
1286
  };
1204
1287
  }
1205
- exportCutImage() {
1206
- var _a, _b, _c, _d;
1288
+ async exportCutImage() {
1207
1289
  if (!this.canvasService) return null;
1208
- const canvas = this.canvasService.canvas;
1290
+ const userLayer = this.canvasService.getLayer("user");
1291
+ if (!userLayer) return null;
1209
1292
  const { shape, width, height, radius, position, holes } = this;
1210
- const canvasW = canvas.width || 800;
1211
- const canvasH = canvas.height || 600;
1212
- const cx = Coordinate.toAbsolute((_a = position == null ? void 0 : position.x) != null ? _a : 0.5, canvasW);
1213
- const cy = Coordinate.toAbsolute((_b = position == null ? void 0 : position.y) != null ? _b : 0.5, canvasH);
1293
+ const canvasW = this.canvasService.canvas.width || 800;
1294
+ const canvasH = this.canvasService.canvas.height || 600;
1295
+ const paddingPx = this.resolvePadding(canvasW, canvasH);
1296
+ const layout = Coordinate.calculateLayout(
1297
+ { width: canvasW, height: canvasH },
1298
+ { width, height },
1299
+ paddingPx
1300
+ );
1301
+ const scale = layout.scale;
1302
+ const cx = layout.offsetX + layout.width / 2;
1303
+ const cy = layout.offsetY + layout.height / 2;
1304
+ const visualWidth = layout.width;
1305
+ const visualHeight = layout.height;
1306
+ const visualRadius = radius * scale;
1214
1307
  const absoluteHoles = (holes || []).map((h) => {
1308
+ const unit = this.unit || "mm";
1309
+ const unitScale = Coordinate.convertUnit(1, "mm", unit);
1215
1310
  const pos = resolveHolePosition(
1216
- h,
1217
- { x: cx, y: cy, width, height },
1311
+ {
1312
+ ...h,
1313
+ offsetX: (h.offsetX || 0) * unitScale * scale,
1314
+ offsetY: (h.offsetY || 0) * unitScale * scale
1315
+ },
1316
+ { x: cx, y: cy, width: visualWidth, height: visualHeight },
1218
1317
  { width: canvasW, height: canvasH }
1219
1318
  );
1220
1319
  return {
1221
1320
  ...h,
1222
1321
  x: pos.x,
1223
- y: pos.y
1322
+ y: pos.y,
1323
+ innerRadius: h.innerRadius * unitScale * scale,
1324
+ outerRadius: h.outerRadius * unitScale * scale,
1325
+ offsetX: (h.offsetX || 0) * unitScale * scale,
1326
+ offsetY: (h.offsetY || 0) * unitScale * scale
1224
1327
  };
1225
1328
  });
1226
1329
  const pathData = generateDielinePath({
1227
1330
  shape,
1228
- width,
1229
- height,
1230
- radius,
1331
+ width: visualWidth,
1332
+ height: visualHeight,
1333
+ radius: visualRadius,
1231
1334
  x: cx,
1232
1335
  y: cy,
1233
1336
  holes: absoluteHoles,
1234
1337
  pathData: this.pathData
1235
1338
  });
1339
+ const clonedLayer = await userLayer.clone();
1236
1340
  const clipPath = new import_fabric2.Path(pathData, {
1237
- left: 0,
1238
- top: 0,
1239
1341
  originX: "left",
1240
1342
  originY: "top",
1241
- absolutePositioned: true
1242
- });
1243
- const layer = this.getLayer();
1244
- const wasVisible = (_c = layer == null ? void 0 : layer.visible) != null ? _c : true;
1245
- if (layer) layer.visible = false;
1246
- const holeMarkers = canvas.getObjects().filter((o) => {
1247
- var _a2;
1248
- return ((_a2 = o.data) == null ? void 0 : _a2.type) === "hole-marker";
1249
- });
1250
- holeMarkers.forEach((o) => o.visible = false);
1251
- const rulerLayer = canvas.getObjects().find((obj) => {
1252
- var _a2;
1253
- return ((_a2 = obj.data) == null ? void 0 : _a2.id) === "ruler-overlay";
1254
- });
1255
- const rulerWasVisible = (_d = rulerLayer == null ? void 0 : rulerLayer.visible) != null ? _d : true;
1256
- if (rulerLayer) rulerLayer.visible = false;
1257
- const originalClip = canvas.clipPath;
1258
- canvas.clipPath = clipPath;
1259
- const bbox = clipPath.getBoundingRect();
1260
- const clipPathCorrected = new import_fabric2.Path(pathData, {
1261
- absolutePositioned: true,
1262
1343
  left: 0,
1263
- top: 0
1264
- });
1265
- const tempPath = new import_fabric2.Path(pathData);
1266
- const tempBounds = tempPath.getBoundingRect();
1267
- clipPathCorrected.set({
1268
- left: tempBounds.left,
1269
- top: tempBounds.top,
1270
- originX: "left",
1271
- originY: "top"
1344
+ top: 0,
1345
+ absolutePositioned: true
1346
+ // Important for groups
1272
1347
  });
1273
- canvas.clipPath = clipPathCorrected;
1274
- const exportBbox = clipPathCorrected.getBoundingRect();
1275
- const dataURL = canvas.toDataURL({
1348
+ clonedLayer.clipPath = clipPath;
1349
+ const bounds = clipPath.getBoundingRect();
1350
+ const dataUrl = clonedLayer.toDataURL({
1276
1351
  format: "png",
1277
1352
  multiplier: 2,
1278
- left: exportBbox.left,
1279
- top: exportBbox.top,
1280
- width: exportBbox.width,
1281
- height: exportBbox.height
1353
+ // Better quality
1354
+ left: bounds.left,
1355
+ top: bounds.top,
1356
+ width: bounds.width,
1357
+ height: bounds.height
1282
1358
  });
1283
- canvas.clipPath = originalClip;
1284
- if (layer) layer.visible = wasVisible;
1285
- if (rulerLayer) rulerLayer.visible = rulerWasVisible;
1286
- holeMarkers.forEach((o) => o.visible = true);
1287
- canvas.requestRenderAll();
1288
- return dataURL;
1359
+ return dataUrl;
1289
1360
  }
1290
1361
  };
1291
1362
 
@@ -1558,11 +1629,19 @@ var HoleTool = class {
1558
1629
  handler: (x, y) => {
1559
1630
  var _a, _b, _c;
1560
1631
  if (!this.canvasService) return false;
1561
- const { width, height } = this.canvasService.canvas;
1562
- const normalizedHole = Coordinate.normalizePoint(
1563
- { x, y },
1564
- { width: width || 800, height: height || 600 }
1565
- );
1632
+ let normalizedX = 0.5;
1633
+ let normalizedY = 0.5;
1634
+ if (this.currentGeometry) {
1635
+ const { x: gx, y: gy, width: gw, height: gh } = this.currentGeometry;
1636
+ const left = gx - gw / 2;
1637
+ const top = gy - gh / 2;
1638
+ normalizedX = gw > 0 ? (x - left) / gw : 0.5;
1639
+ normalizedY = gh > 0 ? (y - top) / gh : 0.5;
1640
+ } else {
1641
+ const { width, height } = this.canvasService.canvas;
1642
+ normalizedX = Coordinate.toNormalized(x, width || 800);
1643
+ normalizedY = Coordinate.toNormalized(y, height || 600);
1644
+ }
1566
1645
  const configService = (_a = this.context) == null ? void 0 : _a.services.get(
1567
1646
  "ConfigurationService"
1568
1647
  );
@@ -1572,8 +1651,8 @@ var HoleTool = class {
1572
1651
  const innerRadius = (_b = lastHole == null ? void 0 : lastHole.innerRadius) != null ? _b : 15;
1573
1652
  const outerRadius = (_c = lastHole == null ? void 0 : lastHole.outerRadius) != null ? _c : 25;
1574
1653
  const newHole = {
1575
- x: normalizedHole.x,
1576
- y: normalizedHole.y,
1654
+ x: normalizedX,
1655
+ y: normalizedY,
1577
1656
  innerRadius,
1578
1657
  outerRadius
1579
1658
  };
@@ -1668,7 +1747,10 @@ var HoleTool = class {
1668
1747
  var _a;
1669
1748
  const target = e.target;
1670
1749
  if (!target || ((_a = target.data) == null ? void 0 : _a.type) !== "hole-marker") return;
1671
- this.syncHolesFromCanvas();
1750
+ const changed = this.enforceConstraints();
1751
+ if (!changed) {
1752
+ this.syncHolesFromCanvas();
1753
+ }
1672
1754
  };
1673
1755
  canvas.on("object:modified", this.handleModified);
1674
1756
  }
@@ -1726,18 +1808,21 @@ var HoleTool = class {
1726
1808
  }
1727
1809
  );
1728
1810
  const newHoles = objects.map((obj, i) => {
1729
- var _a, _b;
1811
+ var _a, _b, _c, _d;
1730
1812
  const original = this.holes[i];
1731
1813
  const newAbsX = obj.left;
1732
1814
  const newAbsY = obj.top;
1815
+ const scale = ((_a = this.currentGeometry) == null ? void 0 : _a.scale) || 1;
1816
+ const unit = ((_b = this.currentGeometry) == null ? void 0 : _b.unit) || "mm";
1817
+ const unitScale = Coordinate.convertUnit(1, "mm", unit);
1733
1818
  if (original && original.anchor && this.currentGeometry) {
1734
- const { x, y, width: width2, height: height2 } = this.currentGeometry;
1819
+ const { x, y, width, height } = this.currentGeometry;
1735
1820
  let bx = x;
1736
1821
  let by = y;
1737
- const left = x - width2 / 2;
1738
- const right = x + width2 / 2;
1739
- const top = y - height2 / 2;
1740
- const bottom = y + height2 / 2;
1822
+ const left = x - width / 2;
1823
+ const right = x + width / 2;
1824
+ const top = y - height / 2;
1825
+ const bottom = y + height / 2;
1741
1826
  switch (original.anchor) {
1742
1827
  case "top-left":
1743
1828
  bx = left;
@@ -1778,25 +1863,34 @@ var HoleTool = class {
1778
1863
  }
1779
1864
  return {
1780
1865
  ...original,
1781
- offsetX: newAbsX - bx,
1782
- offsetY: newAbsY - by,
1866
+ // Denormalize offset back to physical units (mm)
1867
+ offsetX: (newAbsX - bx) / scale / unitScale,
1868
+ offsetY: (newAbsY - by) / scale / unitScale,
1783
1869
  // Clear direct coordinates if we use anchor
1784
1870
  x: void 0,
1785
1871
  y: void 0
1786
1872
  };
1787
1873
  }
1788
- const { width, height } = this.canvasService.canvas;
1789
- const p = Coordinate.normalizePoint(
1790
- { x: newAbsX, y: newAbsY },
1791
- { width: width || 800, height: height || 600 }
1792
- );
1874
+ let normalizedX = 0.5;
1875
+ let normalizedY = 0.5;
1876
+ if (this.currentGeometry) {
1877
+ const { x, y, width, height } = this.currentGeometry;
1878
+ const left = x - width / 2;
1879
+ const top = y - height / 2;
1880
+ normalizedX = width > 0 ? (newAbsX - left) / width : 0.5;
1881
+ normalizedY = height > 0 ? (newAbsY - top) / height : 0.5;
1882
+ } else {
1883
+ const { width, height } = this.canvasService.canvas;
1884
+ normalizedX = Coordinate.toNormalized(newAbsX, width || 800);
1885
+ normalizedY = Coordinate.toNormalized(newAbsY, height || 600);
1886
+ }
1793
1887
  return {
1794
1888
  ...original,
1795
- x: p.x,
1796
- y: p.y,
1889
+ x: normalizedX,
1890
+ y: normalizedY,
1797
1891
  // Ensure radii are preserved
1798
- innerRadius: (_a = original == null ? void 0 : original.innerRadius) != null ? _a : 15,
1799
- outerRadius: (_b = original == null ? void 0 : original.outerRadius) != null ? _b : 25
1892
+ innerRadius: (_c = original == null ? void 0 : original.innerRadius) != null ? _c : 15,
1893
+ outerRadius: (_d = original == null ? void 0 : original.outerRadius) != null ? _d : 25
1800
1894
  };
1801
1895
  });
1802
1896
  this.holes = newHoles;
@@ -1834,16 +1928,28 @@ var HoleTool = class {
1834
1928
  x: (width || 800) / 2,
1835
1929
  y: (height || 600) / 2,
1836
1930
  width: width || 800,
1837
- height: height || 600
1931
+ height: height || 600,
1932
+ scale: 1
1933
+ // Default scale if no geometry loaded
1838
1934
  };
1839
1935
  holes.forEach((hole, index) => {
1936
+ const scale = geometry.scale || 1;
1937
+ const unit = geometry.unit || "mm";
1938
+ const unitScale = Coordinate.convertUnit(1, "mm", unit);
1939
+ const visualInnerRadius = hole.innerRadius * unitScale * scale;
1940
+ const visualOuterRadius = hole.outerRadius * unitScale * scale;
1840
1941
  const pos = resolveHolePosition(
1841
- hole,
1942
+ {
1943
+ ...hole,
1944
+ offsetX: (hole.offsetX || 0) * unitScale * scale,
1945
+ offsetY: (hole.offsetY || 0) * unitScale * scale
1946
+ },
1842
1947
  geometry,
1843
- { width: width || 800, height: height || 600 }
1948
+ { width: geometry.width, height: geometry.height }
1949
+ // Use geometry dims instead of canvas
1844
1950
  );
1845
1951
  const innerCircle = new import_fabric4.Circle({
1846
- radius: hole.innerRadius,
1952
+ radius: visualInnerRadius,
1847
1953
  fill: "transparent",
1848
1954
  stroke: "red",
1849
1955
  strokeWidth: 2,
@@ -1851,7 +1957,7 @@ var HoleTool = class {
1851
1957
  originY: "center"
1852
1958
  });
1853
1959
  const outerCircle = new import_fabric4.Circle({
1854
- radius: hole.outerRadius,
1960
+ radius: visualOuterRadius,
1855
1961
  fill: "transparent",
1856
1962
  stroke: "#666",
1857
1963
  strokeWidth: 1,
@@ -1931,11 +2037,16 @@ var HoleTool = class {
1931
2037
  var _a, _b;
1932
2038
  const currentPos = new import_fabric4.Point(obj.left, obj.top);
1933
2039
  const holeData = this.holes[i];
2040
+ const scale = geometry.scale || 1;
2041
+ const unit = geometry.unit || "mm";
2042
+ const unitScale = Coordinate.convertUnit(1, "mm", unit);
2043
+ const innerR = ((_a = holeData == null ? void 0 : holeData.innerRadius) != null ? _a : 15) * unitScale * scale;
2044
+ const outerR = ((_b = holeData == null ? void 0 : holeData.outerRadius) != null ? _b : 25) * unitScale * scale;
1934
2045
  const newPos = this.calculateConstrainedPosition(
1935
2046
  currentPos,
1936
2047
  constraintGeometry,
1937
- (_a = holeData == null ? void 0 : holeData.innerRadius) != null ? _a : 15,
1938
- (_b = holeData == null ? void 0 : holeData.outerRadius) != null ? _b : 25
2048
+ innerR,
2049
+ outerR
1939
2050
  );
1940
2051
  if (currentPos.distanceFrom(newPos) > 0.1) {
1941
2052
  obj.set({
@@ -1989,17 +2100,14 @@ var HoleTool = class {
1989
2100
  var import_core5 = require("@pooder/core");
1990
2101
  var import_fabric5 = require("fabric");
1991
2102
  var ImageTool = class {
1992
- constructor(options) {
2103
+ constructor() {
1993
2104
  this.id = "pooder.kit.image";
1994
2105
  this.metadata = {
1995
2106
  name: "ImageTool"
1996
2107
  };
1997
- this._loadingUrl = null;
1998
- this.url = "";
1999
- this.opacity = 1;
2000
- if (options) {
2001
- Object.assign(this, options);
2002
- }
2108
+ this.items = [];
2109
+ this.objectMap = /* @__PURE__ */ new Map();
2110
+ this.isUpdatingConfig = false;
2003
2111
  }
2004
2112
  activate(context) {
2005
2113
  this.context = context;
@@ -2010,38 +2118,33 @@ var ImageTool = class {
2010
2118
  }
2011
2119
  const configService = context.services.get("ConfigurationService");
2012
2120
  if (configService) {
2013
- this.url = configService.get("image.url", this.url);
2014
- this.opacity = configService.get("image.opacity", this.opacity);
2015
- this.width = configService.get("image.width", this.width);
2016
- this.height = configService.get("image.height", this.height);
2017
- this.angle = configService.get("image.angle", this.angle);
2018
- this.left = configService.get("image.left", this.left);
2019
- this.top = configService.get("image.top", this.top);
2121
+ this.items = configService.get("image.items", []) || [];
2020
2122
  configService.onAnyChange((e) => {
2021
- if (e.key.startsWith("image.")) {
2022
- const prop = e.key.split(".")[1];
2023
- console.log(
2024
- `[ImageTool] Config change detected: ${e.key} -> ${e.value}`
2025
- );
2026
- if (prop && prop in this) {
2027
- this[prop] = e.value;
2028
- this.updateImage();
2029
- }
2123
+ if (this.isUpdatingConfig) return;
2124
+ let shouldUpdate = false;
2125
+ if (e.key === "image.items") {
2126
+ this.items = e.value || [];
2127
+ shouldUpdate = true;
2128
+ } else if (e.key.startsWith("dieline.") && e.key !== "dieline.holes") {
2129
+ shouldUpdate = true;
2130
+ }
2131
+ if (shouldUpdate) {
2132
+ this.updateImages();
2030
2133
  }
2031
2134
  });
2032
2135
  }
2033
2136
  this.ensureLayer();
2034
- this.updateImage();
2137
+ this.updateImages();
2035
2138
  }
2036
2139
  deactivate(context) {
2037
2140
  if (this.canvasService) {
2038
2141
  const layer = this.canvasService.getLayer("user");
2039
2142
  if (layer) {
2040
- const userImage = this.canvasService.getObject("user-image", "user");
2041
- if (userImage) {
2042
- layer.remove(userImage);
2043
- this.canvasService.requestRenderAll();
2044
- }
2143
+ this.objectMap.forEach((obj) => {
2144
+ layer.remove(obj);
2145
+ });
2146
+ this.objectMap.clear();
2147
+ this.canvasService.requestRenderAll();
2045
2148
  }
2046
2149
  this.canvasService = void 0;
2047
2150
  this.context = void 0;
@@ -2051,82 +2154,103 @@ var ImageTool = class {
2051
2154
  return {
2052
2155
  [import_core5.ContributionPointIds.CONFIGURATIONS]: [
2053
2156
  {
2054
- id: "image.url",
2055
- type: "string",
2056
- label: "Image URL",
2057
- default: this.url
2058
- },
2157
+ id: "image.items",
2158
+ type: "array",
2159
+ label: "Images",
2160
+ default: []
2161
+ }
2162
+ ],
2163
+ [import_core5.ContributionPointIds.COMMANDS]: [
2059
2164
  {
2060
- id: "image.opacity",
2061
- type: "number",
2062
- label: "Opacity",
2063
- min: 0,
2064
- max: 1,
2065
- step: 0.1,
2066
- default: this.opacity
2165
+ command: "addImage",
2166
+ title: "Add Image",
2167
+ handler: (url, options) => {
2168
+ const newItem = {
2169
+ id: this.generateId(),
2170
+ url,
2171
+ opacity: 1,
2172
+ ...options
2173
+ };
2174
+ this.updateConfig([...this.items, newItem]);
2175
+ return newItem.id;
2176
+ }
2067
2177
  },
2068
2178
  {
2069
- id: "image.width",
2070
- type: "number",
2071
- label: "Width",
2072
- min: 0,
2073
- max: 5e3,
2074
- default: this.width
2179
+ command: "removeImage",
2180
+ title: "Remove Image",
2181
+ handler: (id) => {
2182
+ const newItems = this.items.filter((item) => item.id !== id);
2183
+ if (newItems.length !== this.items.length) {
2184
+ this.updateConfig(newItems);
2185
+ }
2186
+ }
2075
2187
  },
2076
2188
  {
2077
- id: "image.height",
2078
- type: "number",
2079
- label: "Height",
2080
- min: 0,
2081
- max: 5e3,
2082
- default: this.height
2189
+ command: "updateImage",
2190
+ title: "Update Image",
2191
+ handler: (id, updates) => {
2192
+ const index = this.items.findIndex((item) => item.id === id);
2193
+ if (index !== -1) {
2194
+ const newItems = [...this.items];
2195
+ newItems[index] = { ...newItems[index], ...updates };
2196
+ this.updateConfig(newItems);
2197
+ }
2198
+ }
2083
2199
  },
2084
2200
  {
2085
- id: "image.angle",
2086
- type: "number",
2087
- label: "Rotation",
2088
- min: 0,
2089
- max: 360,
2090
- default: this.angle
2201
+ command: "clearImages",
2202
+ title: "Clear Images",
2203
+ handler: () => {
2204
+ this.updateConfig([]);
2205
+ }
2091
2206
  },
2092
2207
  {
2093
- id: "image.left",
2094
- type: "number",
2095
- label: "Left (Normalized)",
2096
- min: 0,
2097
- max: 1,
2098
- default: this.left
2208
+ command: "bringToFront",
2209
+ title: "Bring Image to Front",
2210
+ handler: (id) => {
2211
+ const index = this.items.findIndex((item) => item.id === id);
2212
+ if (index !== -1 && index < this.items.length - 1) {
2213
+ const newItems = [...this.items];
2214
+ const [item] = newItems.splice(index, 1);
2215
+ newItems.push(item);
2216
+ this.updateConfig(newItems);
2217
+ }
2218
+ }
2099
2219
  },
2100
2220
  {
2101
- id: "image.top",
2102
- type: "number",
2103
- label: "Top (Normalized)",
2104
- min: 0,
2105
- max: 1,
2106
- default: this.top
2107
- }
2108
- ],
2109
- [import_core5.ContributionPointIds.COMMANDS]: [
2110
- {
2111
- command: "setUserImage",
2112
- title: "Set User Image",
2113
- handler: (url, opacity, width, height, angle, left, top) => {
2114
- if (this.url === url && this.opacity === opacity && this.width === width && this.height === height && this.angle === angle && this.left === left && this.top === top)
2115
- return true;
2116
- this.url = url;
2117
- this.opacity = opacity;
2118
- this.width = width;
2119
- this.height = height;
2120
- this.angle = angle;
2121
- this.left = left;
2122
- this.top = top;
2123
- this.updateImage();
2124
- return true;
2221
+ command: "sendToBack",
2222
+ title: "Send Image to Back",
2223
+ handler: (id) => {
2224
+ const index = this.items.findIndex((item) => item.id === id);
2225
+ if (index > 0) {
2226
+ const newItems = [...this.items];
2227
+ const [item] = newItems.splice(index, 1);
2228
+ newItems.unshift(item);
2229
+ this.updateConfig(newItems);
2230
+ }
2125
2231
  }
2126
2232
  }
2127
2233
  ]
2128
2234
  };
2129
2235
  }
2236
+ generateId() {
2237
+ return Math.random().toString(36).substring(2, 9);
2238
+ }
2239
+ updateConfig(newItems, skipCanvasUpdate = false) {
2240
+ if (!this.context) return;
2241
+ this.isUpdatingConfig = true;
2242
+ this.items = newItems;
2243
+ const configService = this.context.services.get("ConfigurationService");
2244
+ if (configService) {
2245
+ configService.update("image.items", newItems);
2246
+ }
2247
+ if (!skipCanvasUpdate) {
2248
+ this.updateImages();
2249
+ }
2250
+ setTimeout(() => {
2251
+ this.isUpdatingConfig = false;
2252
+ }, 50);
2253
+ }
2130
2254
  ensureLayer() {
2131
2255
  if (!this.canvasService) return;
2132
2256
  let userLayer = this.canvasService.getLayer("user");
@@ -2158,224 +2282,176 @@ var ImageTool = class {
2158
2282
  this.canvasService.requestRenderAll();
2159
2283
  }
2160
2284
  }
2161
- updateImage() {
2285
+ getLayoutInfo() {
2162
2286
  var _a, _b;
2287
+ const canvasW = ((_a = this.canvasService) == null ? void 0 : _a.canvas.width) || 800;
2288
+ const canvasH = ((_b = this.canvasService) == null ? void 0 : _b.canvas.height) || 600;
2289
+ let layoutScale = 1;
2290
+ let layoutOffsetX = 0;
2291
+ let layoutOffsetY = 0;
2292
+ let visualWidth = canvasW;
2293
+ let visualHeight = canvasH;
2294
+ let dielinePhysicalWidth = 500;
2295
+ let dielinePhysicalHeight = 500;
2296
+ if (this.context) {
2297
+ const configService = this.context.services.get("ConfigurationService");
2298
+ if (configService) {
2299
+ dielinePhysicalWidth = configService.get("dieline.width") || 500;
2300
+ dielinePhysicalHeight = configService.get("dieline.height") || 500;
2301
+ const padding = configService.get("dieline.padding") || 40;
2302
+ const layout = Coordinate.calculateLayout(
2303
+ { width: canvasW, height: canvasH },
2304
+ { width: dielinePhysicalWidth, height: dielinePhysicalHeight },
2305
+ padding
2306
+ );
2307
+ layoutScale = layout.scale;
2308
+ layoutOffsetX = layout.offsetX;
2309
+ layoutOffsetY = layout.offsetY;
2310
+ visualWidth = layout.width;
2311
+ visualHeight = layout.height;
2312
+ }
2313
+ }
2314
+ return {
2315
+ layoutScale,
2316
+ layoutOffsetX,
2317
+ layoutOffsetY,
2318
+ visualWidth,
2319
+ visualHeight,
2320
+ dielinePhysicalWidth,
2321
+ dielinePhysicalHeight
2322
+ };
2323
+ }
2324
+ updateImages() {
2163
2325
  if (!this.canvasService) return;
2164
- let { url, opacity, width, height, angle, left, top } = this;
2165
2326
  const layer = this.canvasService.getLayer("user");
2166
2327
  if (!layer) {
2167
2328
  console.warn("[ImageTool] User layer not found");
2168
2329
  return;
2169
2330
  }
2170
- const userImage = this.canvasService.getObject("user-image", "user");
2171
- if (this._loadingUrl === url) return;
2172
- if (userImage) {
2173
- const currentSrc = ((_a = userImage.getSrc) == null ? void 0 : _a.call(userImage)) || ((_b = userImage._element) == null ? void 0 : _b.src);
2174
- if (currentSrc !== url) {
2175
- this.loadImage(layer);
2331
+ const currentIds = new Set(this.items.map((i) => i.id));
2332
+ for (const [id, obj] of this.objectMap) {
2333
+ if (!currentIds.has(id)) {
2334
+ layer.remove(obj);
2335
+ this.objectMap.delete(id);
2336
+ }
2337
+ }
2338
+ const layout = this.getLayoutInfo();
2339
+ this.items.forEach((item, index) => {
2340
+ let obj = this.objectMap.get(item.id);
2341
+ if (!obj) {
2342
+ this.loadImage(item, layer, layout);
2176
2343
  } else {
2177
- const updates = {};
2178
- const canvasW = this.canvasService.canvas.width || 800;
2179
- const canvasH = this.canvasService.canvas.height || 600;
2180
- const centerX = canvasW / 2;
2181
- const centerY = canvasH / 2;
2182
- if (userImage.opacity !== opacity) updates.opacity = opacity;
2183
- if (angle !== void 0 && userImage.angle !== angle)
2184
- updates.angle = angle;
2185
- if (userImage.originX !== "center") {
2186
- userImage.set({
2187
- originX: "center",
2188
- originY: "center",
2189
- left: userImage.left + userImage.width * userImage.scaleX / 2,
2190
- top: userImage.top + userImage.height * userImage.scaleY / 2
2191
- });
2192
- }
2193
- if (left !== void 0) {
2194
- const globalLeft = Coordinate.toAbsolute(left, canvasW);
2195
- const localLeft = globalLeft - centerX;
2196
- if (Math.abs(userImage.left - localLeft) > 1)
2197
- updates.left = localLeft;
2198
- }
2199
- if (top !== void 0) {
2200
- const globalTop = Coordinate.toAbsolute(top, canvasH);
2201
- const localTop = globalTop - centerY;
2202
- if (Math.abs(userImage.top - localTop) > 1) updates.top = localTop;
2203
- }
2204
- if (width !== void 0 && userImage.width)
2205
- updates.scaleX = width / userImage.width;
2206
- if (height !== void 0 && userImage.height)
2207
- updates.scaleY = height / userImage.height;
2208
- if (Object.keys(updates).length > 0) {
2209
- userImage.set(updates);
2210
- layer.dirty = true;
2211
- this.canvasService.requestRenderAll();
2212
- }
2344
+ this.updateObjectProperties(obj, item, layout);
2345
+ layer.remove(obj);
2346
+ layer.add(obj);
2213
2347
  }
2214
- } else {
2215
- this.loadImage(layer);
2348
+ });
2349
+ layer.dirty = true;
2350
+ this.canvasService.requestRenderAll();
2351
+ }
2352
+ updateObjectProperties(obj, item, layout) {
2353
+ const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight } = layout;
2354
+ const updates = {};
2355
+ if (obj.opacity !== item.opacity) updates.opacity = item.opacity;
2356
+ if (item.angle !== void 0 && obj.angle !== item.angle) updates.angle = item.angle;
2357
+ if (item.left !== void 0) {
2358
+ const globalLeft = layoutOffsetX + item.left * visualWidth;
2359
+ if (Math.abs(obj.left - globalLeft) > 1) updates.left = globalLeft;
2360
+ }
2361
+ if (item.top !== void 0) {
2362
+ const globalTop = layoutOffsetY + item.top * visualHeight;
2363
+ if (Math.abs(obj.top - globalTop) > 1) updates.top = globalTop;
2364
+ }
2365
+ if (item.width !== void 0 && obj.width) {
2366
+ const targetScaleX = item.width * layoutScale / obj.width;
2367
+ if (Math.abs(obj.scaleX - targetScaleX) > 1e-3) updates.scaleX = targetScaleX;
2368
+ }
2369
+ if (item.height !== void 0 && obj.height) {
2370
+ const targetScaleY = item.height * layoutScale / obj.height;
2371
+ if (Math.abs(obj.scaleY - targetScaleY) > 1e-3) updates.scaleY = targetScaleY;
2372
+ }
2373
+ if (obj.originX !== "center") {
2374
+ updates.originX = "center";
2375
+ updates.originY = "center";
2376
+ }
2377
+ if (Object.keys(updates).length > 0) {
2378
+ obj.set(updates);
2216
2379
  }
2217
2380
  }
2218
- loadImage(layer) {
2219
- if (!this.canvasService) return;
2220
- const { url } = this;
2221
- if (!url) return;
2222
- this._loadingUrl = url;
2223
- import_fabric5.FabricImage.fromURL(url, { crossOrigin: "anonymous" }).then((image) => {
2224
- var _a, _b, _c, _d, _e, _f;
2225
- if (this._loadingUrl !== url) return;
2226
- this._loadingUrl = null;
2227
- let { opacity, width, height, angle, left, top } = this;
2228
- if (this.context) {
2229
- const configService = this.context.services.get(
2230
- "ConfigurationService"
2231
- );
2232
- const dielineWidth = configService.get("dieline.width");
2233
- const dielineHeight = configService.get("dieline.height");
2234
- console.log(
2235
- "[ImageTool] Dieline config debug:",
2236
- {
2237
- widthVal: dielineWidth,
2238
- heightVal: dielineHeight,
2239
- // Debug: dump all keys to see what is available
2240
- allKeys: Array.from(
2241
- ((_a = configService.configValues) == null ? void 0 : _a.keys()) || []
2242
- )
2243
- },
2244
- configService
2245
- );
2246
- if (width === void 0 && height === void 0) {
2247
- const scale = Math.min(
2248
- dielineWidth / (image.width || 1),
2249
- dielineHeight / (image.height || 1)
2250
- );
2251
- width = (image.width || 1) * scale;
2252
- height = (image.height || 1) * scale;
2253
- this.width = width;
2254
- this.height = height;
2255
- }
2256
- if (left === void 0 && top === void 0) {
2257
- const dielinePos = configService == null ? void 0 : configService.get("dieline.position");
2258
- if (dielinePos) {
2259
- this.left = dielinePos.x;
2260
- this.top = dielinePos.y;
2261
- } else {
2262
- this.left = 0.5;
2263
- this.top = 0.5;
2264
- }
2265
- left = this.left;
2266
- top = this.top;
2267
- }
2268
- }
2269
- const existingImage = this.canvasService.getObject(
2270
- "user-image",
2271
- "user"
2272
- );
2273
- if (existingImage) {
2274
- const defaultLeft = existingImage.left;
2275
- const defaultTop = existingImage.top;
2276
- const defaultAngle = existingImage.angle;
2277
- const defaultScaleX = existingImage.scaleX;
2278
- const defaultScaleY = existingImage.scaleY;
2279
- const canvasW = ((_b = this.canvasService) == null ? void 0 : _b.canvas.width) || 800;
2280
- const canvasH = ((_c = this.canvasService) == null ? void 0 : _c.canvas.height) || 600;
2281
- const centerX = canvasW / 2;
2282
- const centerY = canvasH / 2;
2283
- let targetLeft = left !== void 0 ? left : defaultLeft;
2284
- let targetTop = top !== void 0 ? top : defaultTop;
2285
- const configService = (_d = this.context) == null ? void 0 : _d.services.get(
2286
- "ConfigurationService"
2287
- );
2288
- console.log("[ImageTool] Loading EXISTING image...", {
2289
- canvasW,
2290
- canvasH,
2291
- centerX,
2292
- centerY,
2293
- incomingLeft: left,
2294
- incomingTop: top,
2295
- dielinePos: configService == null ? void 0 : configService.get("dieline.position"),
2296
- existingImage: !!existingImage
2297
- });
2298
- if (left !== void 0) {
2299
- const globalLeft = Coordinate.toAbsolute(left, canvasW);
2300
- targetLeft = globalLeft;
2301
- console.log("[ImageTool] Calculated targetLeft", {
2302
- globalLeft,
2303
- targetLeft
2304
- });
2305
- }
2306
- if (top !== void 0) {
2307
- const globalTop = Coordinate.toAbsolute(top, canvasH);
2308
- targetTop = globalTop;
2309
- console.log("[ImageTool] Calculated targetTop", {
2310
- globalTop,
2311
- targetTop
2312
- });
2313
- }
2314
- image.set({
2315
- originX: "center",
2316
- // Use center origin for easier positioning
2317
- originY: "center",
2318
- left: targetLeft,
2319
- top: targetTop,
2320
- angle: angle !== void 0 ? angle : defaultAngle,
2321
- scaleX: width !== void 0 && image.width ? width / image.width : defaultScaleX,
2322
- scaleY: height !== void 0 && image.height ? height / image.height : defaultScaleY
2323
- });
2324
- layer.remove(existingImage);
2325
- } else {
2326
- image.set({
2327
- originX: "center",
2328
- originY: "center"
2329
- });
2330
- if (width !== void 0 && image.width)
2331
- image.scaleX = width / image.width;
2332
- if (height !== void 0 && image.height)
2333
- image.scaleY = height / image.height;
2334
- if (angle !== void 0) image.angle = angle;
2335
- const canvasW = ((_e = this.canvasService) == null ? void 0 : _e.canvas.width) || 800;
2336
- const canvasH = ((_f = this.canvasService) == null ? void 0 : _f.canvas.height) || 600;
2337
- const centerX = canvasW / 2;
2338
- const centerY = canvasH / 2;
2339
- if (left !== void 0) {
2340
- image.left = Coordinate.toAbsolute(left, canvasW);
2341
- } else {
2342
- image.left = centerX;
2343
- }
2344
- if (top !== void 0) {
2345
- image.top = Coordinate.toAbsolute(top, canvasH);
2381
+ loadImage(item, layer, layout) {
2382
+ import_fabric5.Image.fromURL(item.url, { crossOrigin: "anonymous" }).then((image) => {
2383
+ var _a;
2384
+ if (!this.items.find((i) => i.id === item.id)) return;
2385
+ image.set({
2386
+ originX: "center",
2387
+ originY: "center",
2388
+ data: { id: item.id }
2389
+ });
2390
+ let { width, height, left, top } = item;
2391
+ const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight, dielinePhysicalWidth, dielinePhysicalHeight } = layout;
2392
+ if (width === void 0 && height === void 0) {
2393
+ const imgAspect = (image.width || 1) / (image.height || 1);
2394
+ const dielineAspect = dielinePhysicalWidth / dielinePhysicalHeight;
2395
+ if (imgAspect > dielineAspect) {
2396
+ const w = dielinePhysicalWidth;
2397
+ width = w;
2398
+ height = w / imgAspect;
2346
2399
  } else {
2347
- image.top = centerY;
2400
+ const h = dielinePhysicalHeight;
2401
+ height = h;
2402
+ width = h * imgAspect;
2348
2403
  }
2404
+ item.width = width;
2405
+ item.height = height;
2349
2406
  }
2350
- image.set({
2351
- opacity: opacity !== void 0 ? opacity : 1,
2352
- data: {
2353
- id: "user-image"
2354
- }
2355
- });
2407
+ if (left === void 0 && top === void 0) {
2408
+ left = 0.5;
2409
+ top = 0.5;
2410
+ item.left = left;
2411
+ item.top = top;
2412
+ }
2413
+ this.updateObjectProperties(image, item, layout);
2356
2414
  layer.add(image);
2415
+ this.objectMap.set(item.id, image);
2357
2416
  image.on("modified", (e) => {
2358
- var _a2, _b2;
2359
- const matrix = image.calcTransformMatrix();
2360
- const globalPoint = import_fabric5.util.transformPoint(new import_fabric5.Point(0, 0), matrix);
2361
- const canvasW = ((_a2 = this.canvasService) == null ? void 0 : _a2.canvas.width) || 800;
2362
- const canvasH = ((_b2 = this.canvasService) == null ? void 0 : _b2.canvas.height) || 600;
2363
- this.left = Coordinate.toNormalized(globalPoint.x, canvasW);
2364
- this.top = Coordinate.toNormalized(globalPoint.y, canvasH);
2365
- this.angle = e.target.angle;
2366
- if (image.width) this.width = e.target.width * e.target.scaleX;
2367
- if (image.height) this.height = e.target.height * e.target.scaleY;
2368
- if (this.context) {
2369
- this.context.eventBus.emit("update");
2370
- }
2417
+ this.handleObjectModified(item.id, image);
2371
2418
  });
2372
2419
  layer.dirty = true;
2373
- this.canvasService.requestRenderAll();
2420
+ (_a = this.canvasService) == null ? void 0 : _a.requestRenderAll();
2421
+ if (item.width !== width || item.height !== height || item.left !== left || item.top !== top) {
2422
+ this.updateImageInConfig(item.id, { width, height, left, top });
2423
+ }
2374
2424
  }).catch((err) => {
2375
- if (this._loadingUrl === url) this._loadingUrl = null;
2376
- console.error("Failed to load image", url, err);
2425
+ console.error("Failed to load image", item.url, err);
2377
2426
  });
2378
2427
  }
2428
+ handleObjectModified(id, image) {
2429
+ const layout = this.getLayoutInfo();
2430
+ const { layoutScale, layoutOffsetX, layoutOffsetY, visualWidth, visualHeight } = layout;
2431
+ const matrix = image.calcTransformMatrix();
2432
+ const globalPoint = import_fabric5.util.transformPoint(new import_fabric5.Point(0, 0), matrix);
2433
+ const updates = {};
2434
+ updates.left = (globalPoint.x - layoutOffsetX) / visualWidth;
2435
+ updates.top = (globalPoint.y - layoutOffsetY) / visualHeight;
2436
+ updates.angle = image.angle;
2437
+ if (image.width) {
2438
+ const pixelWidth = image.width * image.scaleX;
2439
+ updates.width = pixelWidth / layoutScale;
2440
+ }
2441
+ if (image.height) {
2442
+ const pixelHeight = image.height * image.scaleY;
2443
+ updates.height = pixelHeight / layoutScale;
2444
+ }
2445
+ this.updateImageInConfig(id, updates);
2446
+ }
2447
+ updateImageInConfig(id, updates) {
2448
+ const index = this.items.findIndex((i) => i.id === id);
2449
+ if (index !== -1) {
2450
+ const newItems = [...this.items];
2451
+ newItems[index] = { ...newItems[index], ...updates };
2452
+ this.updateConfig(newItems, true);
2453
+ }
2454
+ }
2379
2455
  };
2380
2456
 
2381
2457
  // src/white-ink.ts
@@ -2678,12 +2754,18 @@ var RulerTool = class {
2678
2754
  this.metadata = {
2679
2755
  name: "RulerTool"
2680
2756
  };
2681
- this.unit = "px";
2682
2757
  this.thickness = 20;
2758
+ this.gap = 15;
2683
2759
  this.backgroundColor = "#f0f0f0";
2684
2760
  this.textColor = "#333333";
2685
2761
  this.lineColor = "#999999";
2686
2762
  this.fontSize = 10;
2763
+ // Dieline context for sync
2764
+ this.dielineWidth = 500;
2765
+ this.dielineHeight = 500;
2766
+ this.dielineUnit = "mm";
2767
+ this.dielinePadding = 40;
2768
+ this.dielineOffset = 0;
2687
2769
  if (options) {
2688
2770
  Object.assign(this, options);
2689
2771
  }
@@ -2696,8 +2778,8 @@ var RulerTool = class {
2696
2778
  }
2697
2779
  const configService = context.services.get("ConfigurationService");
2698
2780
  if (configService) {
2699
- this.unit = configService.get("ruler.unit", this.unit);
2700
2781
  this.thickness = configService.get("ruler.thickness", this.thickness);
2782
+ this.gap = configService.get("ruler.gap", this.gap);
2701
2783
  this.backgroundColor = configService.get(
2702
2784
  "ruler.backgroundColor",
2703
2785
  this.backgroundColor
@@ -2705,13 +2787,38 @@ var RulerTool = class {
2705
2787
  this.textColor = configService.get("ruler.textColor", this.textColor);
2706
2788
  this.lineColor = configService.get("ruler.lineColor", this.lineColor);
2707
2789
  this.fontSize = configService.get("ruler.fontSize", this.fontSize);
2790
+ this.dielineUnit = configService.get("dieline.unit", this.dielineUnit);
2791
+ this.dielineWidth = configService.get("dieline.width", this.dielineWidth);
2792
+ this.dielineHeight = configService.get(
2793
+ "dieline.height",
2794
+ this.dielineHeight
2795
+ );
2796
+ this.dielinePadding = configService.get(
2797
+ "dieline.padding",
2798
+ this.dielinePadding
2799
+ );
2800
+ this.dielineOffset = configService.get(
2801
+ "dieline.offset",
2802
+ this.dielineOffset
2803
+ );
2708
2804
  configService.onAnyChange((e) => {
2805
+ let shouldUpdate = false;
2709
2806
  if (e.key.startsWith("ruler.")) {
2710
2807
  const prop = e.key.split(".")[1];
2711
2808
  if (prop && prop in this) {
2712
2809
  this[prop] = e.value;
2713
- this.updateRuler();
2810
+ shouldUpdate = true;
2714
2811
  }
2812
+ } else if (e.key.startsWith("dieline.")) {
2813
+ if (e.key === "dieline.unit") this.dielineUnit = e.value;
2814
+ if (e.key === "dieline.width") this.dielineWidth = e.value;
2815
+ if (e.key === "dieline.height") this.dielineHeight = e.value;
2816
+ if (e.key === "dieline.padding") this.dielinePadding = e.value;
2817
+ if (e.key === "dieline.offset") this.dielineOffset = e.value;
2818
+ shouldUpdate = true;
2819
+ }
2820
+ if (shouldUpdate) {
2821
+ this.updateRuler();
2715
2822
  }
2716
2823
  });
2717
2824
  }
@@ -2725,13 +2832,6 @@ var RulerTool = class {
2725
2832
  contribute() {
2726
2833
  return {
2727
2834
  [import_core7.ContributionPointIds.CONFIGURATIONS]: [
2728
- {
2729
- id: "ruler.unit",
2730
- type: "select",
2731
- label: "Unit",
2732
- options: ["px", "mm", "cm", "in"],
2733
- default: "px"
2734
- },
2735
2835
  {
2736
2836
  id: "ruler.thickness",
2737
2837
  type: "number",
@@ -2740,6 +2840,14 @@ var RulerTool = class {
2740
2840
  max: 100,
2741
2841
  default: 20
2742
2842
  },
2843
+ {
2844
+ id: "ruler.gap",
2845
+ type: "number",
2846
+ label: "Gap",
2847
+ min: 0,
2848
+ max: 100,
2849
+ default: 15
2850
+ },
2743
2851
  {
2744
2852
  id: "ruler.backgroundColor",
2745
2853
  type: "color",
@@ -2768,16 +2876,6 @@ var RulerTool = class {
2768
2876
  }
2769
2877
  ],
2770
2878
  [import_core7.ContributionPointIds.COMMANDS]: [
2771
- {
2772
- command: "setUnit",
2773
- title: "Set Ruler Unit",
2774
- handler: (unit) => {
2775
- if (this.unit === unit) return true;
2776
- this.unit = unit;
2777
- this.updateRuler();
2778
- return true;
2779
- }
2780
- },
2781
2879
  {
2782
2880
  command: "setTheme",
2783
2881
  title: "Set Ruler Theme",
@@ -2828,6 +2926,68 @@ var RulerTool = class {
2828
2926
  this.canvasService.canvas.remove(layer);
2829
2927
  }
2830
2928
  }
2929
+ createArrowLine(x1, y1, x2, y2, color) {
2930
+ const line = new import_fabric7.Line([x1, y1, x2, y2], {
2931
+ stroke: color,
2932
+ strokeWidth: this.thickness / 20,
2933
+ // Scale stroke width relative to thickness (default 1)
2934
+ selectable: false,
2935
+ evented: false
2936
+ });
2937
+ const arrowSize = Math.max(4, this.thickness * 0.3);
2938
+ const angle = Math.atan2(y2 - y1, x2 - x1);
2939
+ const endArrow = new import_fabric7.Polygon(
2940
+ [
2941
+ { x: 0, y: 0 },
2942
+ { x: -arrowSize, y: -arrowSize / 2 },
2943
+ { x: -arrowSize, y: arrowSize / 2 }
2944
+ ],
2945
+ {
2946
+ fill: color,
2947
+ left: x2,
2948
+ top: y2,
2949
+ originX: "right",
2950
+ originY: "center",
2951
+ angle: angle * 180 / Math.PI,
2952
+ selectable: false,
2953
+ evented: false
2954
+ }
2955
+ );
2956
+ const startArrow = new import_fabric7.Polygon(
2957
+ [
2958
+ { x: 0, y: 0 },
2959
+ { x: arrowSize, y: -arrowSize / 2 },
2960
+ { x: arrowSize, y: arrowSize / 2 }
2961
+ ],
2962
+ {
2963
+ fill: color,
2964
+ left: x1,
2965
+ top: y1,
2966
+ originX: "left",
2967
+ originY: "center",
2968
+ angle: angle * 180 / Math.PI,
2969
+ selectable: false,
2970
+ evented: false
2971
+ }
2972
+ );
2973
+ return new import_fabric7.Group([line, startArrow, endArrow], {
2974
+ selectable: false,
2975
+ evented: false
2976
+ });
2977
+ }
2978
+ resolvePadding(containerWidth, containerHeight) {
2979
+ if (typeof this.dielinePadding === "number") {
2980
+ return this.dielinePadding;
2981
+ }
2982
+ if (typeof this.dielinePadding === "string") {
2983
+ if (this.dielinePadding.endsWith("%")) {
2984
+ const percent = parseFloat(this.dielinePadding) / 100;
2985
+ return Math.min(containerWidth, containerHeight) * percent;
2986
+ }
2987
+ return parseFloat(this.dielinePadding) || 0;
2988
+ }
2989
+ return 0;
2990
+ }
2831
2991
  updateRuler() {
2832
2992
  if (!this.canvasService) return;
2833
2993
  const layer = this.getLayer();
@@ -2836,95 +2996,141 @@ var RulerTool = class {
2836
2996
  const { thickness, backgroundColor, lineColor, textColor, fontSize } = this;
2837
2997
  const width = this.canvasService.canvas.width || 800;
2838
2998
  const height = this.canvasService.canvas.height || 600;
2839
- const topBg = new import_fabric7.Rect({
2840
- left: 0,
2841
- top: 0,
2842
- width,
2843
- height: thickness,
2844
- fill: backgroundColor,
2845
- selectable: false,
2846
- evented: false
2847
- });
2848
- const leftBg = new import_fabric7.Rect({
2849
- left: 0,
2850
- top: 0,
2851
- width: thickness,
2852
- height,
2853
- fill: backgroundColor,
2854
- selectable: false,
2855
- evented: false
2856
- });
2857
- const cornerBg = new import_fabric7.Rect({
2858
- left: 0,
2859
- top: 0,
2860
- width: thickness,
2861
- height: thickness,
2862
- fill: backgroundColor,
2863
- stroke: lineColor,
2864
- strokeWidth: 1,
2999
+ const paddingPx = this.resolvePadding(width, height);
3000
+ const layout = Coordinate.calculateLayout(
3001
+ { width, height },
3002
+ { width: this.dielineWidth, height: this.dielineHeight },
3003
+ paddingPx
3004
+ );
3005
+ const scale = layout.scale;
3006
+ const offsetX = layout.offsetX;
3007
+ const offsetY = layout.offsetY;
3008
+ const visualWidth = layout.width;
3009
+ const visualHeight = layout.height;
3010
+ const rawOffset = this.dielineOffset || 0;
3011
+ const effectiveOffset = rawOffset > 0 ? rawOffset : 0;
3012
+ const expandPixels = effectiveOffset * scale;
3013
+ const gap = this.gap || 15;
3014
+ const rulerLeft = offsetX - expandPixels;
3015
+ const rulerTop = offsetY - expandPixels;
3016
+ const rulerRight = offsetX + visualWidth + expandPixels;
3017
+ const rulerBottom = offsetY + visualHeight + expandPixels;
3018
+ const displayWidth = this.dielineWidth + effectiveOffset * 2;
3019
+ const displayHeight = this.dielineHeight + effectiveOffset * 2;
3020
+ const topRulerY = rulerTop - gap;
3021
+ const topRulerXStart = rulerLeft;
3022
+ const topRulerXEnd = rulerRight;
3023
+ const leftRulerX = rulerLeft - gap;
3024
+ const leftRulerYStart = rulerTop;
3025
+ const leftRulerYEnd = rulerBottom;
3026
+ const topDimLine = this.createArrowLine(
3027
+ topRulerXStart,
3028
+ topRulerY,
3029
+ topRulerXEnd,
3030
+ topRulerY,
3031
+ lineColor
3032
+ );
3033
+ layer.add(topDimLine);
3034
+ const extLen = 5;
3035
+ layer.add(
3036
+ new import_fabric7.Line(
3037
+ [
3038
+ topRulerXStart,
3039
+ topRulerY - extLen,
3040
+ topRulerXStart,
3041
+ topRulerY + extLen
3042
+ ],
3043
+ {
3044
+ stroke: lineColor,
3045
+ strokeWidth: 1,
3046
+ selectable: false,
3047
+ evented: false
3048
+ }
3049
+ )
3050
+ );
3051
+ layer.add(
3052
+ new import_fabric7.Line(
3053
+ [topRulerXEnd, topRulerY - extLen, topRulerXEnd, topRulerY + extLen],
3054
+ {
3055
+ stroke: lineColor,
3056
+ strokeWidth: 1,
3057
+ selectable: false,
3058
+ evented: false
3059
+ }
3060
+ )
3061
+ );
3062
+ const widthStr = parseFloat(displayWidth.toFixed(2)).toString();
3063
+ const topTextContent = `${widthStr} ${this.dielineUnit}`;
3064
+ const topText = new import_fabric7.Text(topTextContent, {
3065
+ left: topRulerXStart + (rulerRight - rulerLeft) / 2,
3066
+ top: topRulerY,
3067
+ fontSize,
3068
+ fill: textColor,
3069
+ fontFamily: "Arial",
3070
+ originX: "center",
3071
+ originY: "center",
3072
+ backgroundColor,
3073
+ // Background mask for readability
2865
3074
  selectable: false,
2866
3075
  evented: false
2867
3076
  });
2868
- layer.add(topBg);
2869
- layer.add(leftBg);
2870
- layer.add(cornerBg);
2871
- const step = 100;
2872
- const subStep = 10;
2873
- const midStep = 50;
2874
- for (let x = 0; x <= width; x += subStep) {
2875
- if (x < thickness) continue;
2876
- let len = thickness * 0.25;
2877
- if (x % step === 0) len = thickness * 0.8;
2878
- else if (x % midStep === 0) len = thickness * 0.5;
2879
- const line = new import_fabric7.Line([x, thickness - len, x, thickness], {
2880
- stroke: lineColor,
2881
- strokeWidth: 1,
2882
- selectable: false,
2883
- evented: false
2884
- });
2885
- layer.add(line);
2886
- if (x % step === 0) {
2887
- const text = new import_fabric7.Text(x.toString(), {
2888
- left: x + 2,
2889
- top: 2,
2890
- fontSize,
2891
- fill: textColor,
2892
- fontFamily: "Arial",
3077
+ layer.add(topText);
3078
+ const leftDimLine = this.createArrowLine(
3079
+ leftRulerX,
3080
+ leftRulerYStart,
3081
+ leftRulerX,
3082
+ leftRulerYEnd,
3083
+ lineColor
3084
+ );
3085
+ layer.add(leftDimLine);
3086
+ layer.add(
3087
+ new import_fabric7.Line(
3088
+ [
3089
+ leftRulerX - extLen,
3090
+ leftRulerYStart,
3091
+ leftRulerX + extLen,
3092
+ leftRulerYStart
3093
+ ],
3094
+ {
3095
+ stroke: lineColor,
3096
+ strokeWidth: 1,
2893
3097
  selectable: false,
2894
3098
  evented: false
2895
- });
2896
- layer.add(text);
2897
- }
2898
- }
2899
- for (let y = 0; y <= height; y += subStep) {
2900
- if (y < thickness) continue;
2901
- let len = thickness * 0.25;
2902
- if (y % step === 0) len = thickness * 0.8;
2903
- else if (y % midStep === 0) len = thickness * 0.5;
2904
- const line = new import_fabric7.Line([thickness - len, y, thickness, y], {
2905
- stroke: lineColor,
2906
- strokeWidth: 1,
2907
- selectable: false,
2908
- evented: false
2909
- });
2910
- layer.add(line);
2911
- if (y % step === 0) {
2912
- const text = new import_fabric7.Text(y.toString(), {
2913
- angle: -90,
2914
- left: thickness / 2 - fontSize / 3,
2915
- // approximate centering
2916
- top: y + fontSize,
2917
- fontSize,
2918
- fill: textColor,
2919
- fontFamily: "Arial",
2920
- originX: "center",
2921
- originY: "center",
3099
+ }
3100
+ )
3101
+ );
3102
+ layer.add(
3103
+ new import_fabric7.Line(
3104
+ [
3105
+ leftRulerX - extLen,
3106
+ leftRulerYEnd,
3107
+ leftRulerX + extLen,
3108
+ leftRulerYEnd
3109
+ ],
3110
+ {
3111
+ stroke: lineColor,
3112
+ strokeWidth: 1,
2922
3113
  selectable: false,
2923
3114
  evented: false
2924
- });
2925
- layer.add(text);
2926
- }
2927
- }
3115
+ }
3116
+ )
3117
+ );
3118
+ const heightStr = parseFloat(displayHeight.toFixed(2)).toString();
3119
+ const leftTextContent = `${heightStr} ${this.dielineUnit}`;
3120
+ const leftText = new import_fabric7.Text(leftTextContent, {
3121
+ left: leftRulerX,
3122
+ top: leftRulerYStart + (rulerBottom - rulerTop) / 2,
3123
+ angle: -90,
3124
+ fontSize,
3125
+ fill: textColor,
3126
+ fontFamily: "Arial",
3127
+ originX: "center",
3128
+ originY: "center",
3129
+ backgroundColor,
3130
+ selectable: false,
3131
+ evented: false
3132
+ });
3133
+ layer.add(leftText);
2928
3134
  this.canvasService.canvas.bringObjectToFront(layer);
2929
3135
  this.canvasService.canvas.requestRenderAll();
2930
3136
  }