@pooder/kit 4.2.0 → 4.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/index.d.mts +12 -5
  3. package/dist/index.d.ts +12 -5
  4. package/dist/index.js +249 -77
  5. package/dist/index.mjs +249 -77
  6. package/package.json +1 -1
  7. package/src/CanvasService.ts +96 -96
  8. package/src/ViewportSystem.ts +92 -92
  9. package/src/background.ts +230 -230
  10. package/src/constraints.ts +157 -42
  11. package/src/coordinate.ts +106 -106
  12. package/src/dieline.ts +973 -955
  13. package/src/feature.ts +88 -68
  14. package/src/featureComplete.ts +3 -2
  15. package/src/film.ts +194 -194
  16. package/src/geometry.ts +157 -30
  17. package/src/image.ts +512 -512
  18. package/src/index.ts +10 -10
  19. package/src/mirror.ts +128 -128
  20. package/src/ruler.ts +508 -508
  21. package/src/tracer.ts +570 -570
  22. package/src/units.ts +27 -27
  23. package/src/white-ink.ts +373 -373
  24. package/tsconfig.test.json +15 -15
  25. package/.test-dist/src/CanvasService.js +0 -83
  26. package/.test-dist/src/ViewportSystem.js +0 -75
  27. package/.test-dist/src/background.js +0 -203
  28. package/.test-dist/src/constraints.js +0 -153
  29. package/.test-dist/src/coordinate.js +0 -74
  30. package/.test-dist/src/dieline.js +0 -758
  31. package/.test-dist/src/feature.js +0 -687
  32. package/.test-dist/src/featureComplete.js +0 -31
  33. package/.test-dist/src/featureDraft.js +0 -31
  34. package/.test-dist/src/film.js +0 -167
  35. package/.test-dist/src/geometry.js +0 -292
  36. package/.test-dist/src/image.js +0 -421
  37. package/.test-dist/src/index.js +0 -31
  38. package/.test-dist/src/mirror.js +0 -104
  39. package/.test-dist/src/ruler.js +0 -383
  40. package/.test-dist/src/tracer.js +0 -448
  41. package/.test-dist/src/units.js +0 -30
  42. package/.test-dist/src/white-ink.js +0 -310
  43. package/.test-dist/tests/run.js +0 -60
  44. package/tests/run.ts +0 -81
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @pooder/kit
2
2
 
3
+ ## 4.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - bugfix
8
+
9
+ ## 4.3.0
10
+
11
+ ### Minor Changes
12
+
13
+ - bridge and constraints
14
+
3
15
  ## 4.2.0
4
16
 
5
17
  ### Minor Changes
package/dist/index.d.mts CHANGED
@@ -53,13 +53,12 @@ interface DielineFeature {
53
53
  height?: number;
54
54
  radius?: number;
55
55
  rotation?: number;
56
- placement?: "edge" | "internal";
56
+ renderBehavior?: "edge" | "surface";
57
57
  color?: string;
58
58
  strokeDash?: number[];
59
59
  skipCut?: boolean;
60
- constraints?: {
61
- type: string;
62
- params?: any;
60
+ bridge?: {
61
+ type: "vertical";
63
62
  };
64
63
  }
65
64
 
@@ -147,6 +146,14 @@ declare class FilmTool implements Extension {
147
146
  private updateFilm;
148
147
  }
149
148
 
149
+ interface ConstraintFeature extends DielineFeature {
150
+ constraints?: Array<{
151
+ type: string;
152
+ params?: any;
153
+ validateOnly?: boolean;
154
+ }>;
155
+ }
156
+
150
157
  declare class FeatureTool implements Extension {
151
158
  id: string;
152
159
  metadata: {
@@ -162,7 +169,7 @@ declare class FeatureTool implements Extension {
162
169
  private handleDielineChange;
163
170
  private currentGeometry;
164
171
  constructor(options?: Partial<{
165
- features: DielineFeature[];
172
+ features: ConstraintFeature[];
166
173
  }>);
167
174
  activate(context: ExtensionContext): void;
168
175
  deactivate(context: ExtensionContext): void;
package/dist/index.d.ts CHANGED
@@ -53,13 +53,12 @@ interface DielineFeature {
53
53
  height?: number;
54
54
  radius?: number;
55
55
  rotation?: number;
56
- placement?: "edge" | "internal";
56
+ renderBehavior?: "edge" | "surface";
57
57
  color?: string;
58
58
  strokeDash?: number[];
59
59
  skipCut?: boolean;
60
- constraints?: {
61
- type: string;
62
- params?: any;
60
+ bridge?: {
61
+ type: "vertical";
63
62
  };
64
63
  }
65
64
 
@@ -147,6 +146,14 @@ declare class FilmTool implements Extension {
147
146
  private updateFilm;
148
147
  }
149
148
 
149
+ interface ConstraintFeature extends DielineFeature {
150
+ constraints?: Array<{
151
+ type: string;
152
+ params?: any;
153
+ validateOnly?: boolean;
154
+ }>;
155
+ }
156
+
150
157
  declare class FeatureTool implements Extension {
151
158
  id: string;
152
159
  metadata: {
@@ -162,7 +169,7 @@ declare class FeatureTool implements Extension {
162
169
  private handleDielineChange;
163
170
  private currentGeometry;
164
171
  constructor(options?: Partial<{
165
- features: DielineFeature[];
172
+ features: ConstraintFeature[];
166
173
  }>);
167
174
  activate(context: ExtensionContext): void;
168
175
  deactivate(context: ExtensionContext): void;
package/dist/index.js CHANGED
@@ -823,7 +823,7 @@ function getPerimeterShape(options) {
823
823
  const { features } = options;
824
824
  if (features && features.length > 0) {
825
825
  const edgeFeatures = features.filter(
826
- (f) => !f.placement || f.placement === "edge"
826
+ (f) => !f.renderBehavior || f.renderBehavior === "edge"
827
827
  );
828
828
  const adds = [];
829
829
  const subtracts = [];
@@ -831,10 +831,74 @@ function getPerimeterShape(options) {
831
831
  const pos = resolveFeaturePosition(f, options);
832
832
  const center = new import_paper2.default.Point(pos.x, pos.y);
833
833
  const item = createFeatureItem(f, center);
834
- if (f.operation === "add") {
835
- adds.push(item);
834
+ if (f.bridge && f.bridge.type === "vertical") {
835
+ const itemBounds = item.bounds;
836
+ const mainBounds = mainShape.bounds;
837
+ const bridgeTop = mainBounds.top;
838
+ const bridgeBottom = itemBounds.top;
839
+ if (bridgeBottom > bridgeTop) {
840
+ const centerX = itemBounds.center.x;
841
+ const ray = new import_paper2.default.Path.Line({
842
+ from: [centerX, bridgeBottom],
843
+ to: [centerX, bridgeTop - 10],
844
+ // Extend slightly past top to ensure intersection
845
+ insert: false
846
+ });
847
+ const intersections = mainShape.getIntersections(ray);
848
+ let targetY = bridgeTop;
849
+ let found = false;
850
+ if (intersections && intersections.length > 0) {
851
+ const validHits = intersections.filter((i) => i.point.y < bridgeBottom - 0.1);
852
+ if (validHits.length > 0) {
853
+ validHits.sort((a, b) => b.point.y - a.point.y);
854
+ targetY = validHits[0].point.y;
855
+ found = true;
856
+ }
857
+ }
858
+ ray.remove();
859
+ const overlap = 2;
860
+ const rectBottom = bridgeBottom;
861
+ let rectTop = found ? targetY + overlap : bridgeTop;
862
+ if (!found) {
863
+ if (mainBounds.bottom < bridgeBottom) {
864
+ targetY = mainBounds.bottom;
865
+ rectTop = targetY - overlap;
866
+ }
867
+ }
868
+ if (rectTop < rectBottom) {
869
+ const bridgeRect = new import_paper2.default.Path.Rectangle({
870
+ from: [itemBounds.left, rectTop],
871
+ to: [itemBounds.right, rectBottom],
872
+ insert: false
873
+ });
874
+ const unitedItem = item.unite(bridgeRect);
875
+ item.remove();
876
+ bridgeRect.remove();
877
+ if (f.operation === "add") {
878
+ adds.push(unitedItem);
879
+ } else {
880
+ subtracts.push(unitedItem);
881
+ }
882
+ } else {
883
+ if (f.operation === "add") {
884
+ adds.push(item);
885
+ } else {
886
+ subtracts.push(item);
887
+ }
888
+ }
889
+ } else {
890
+ if (f.operation === "add") {
891
+ adds.push(item);
892
+ } else {
893
+ subtracts.push(item);
894
+ }
895
+ }
836
896
  } else {
837
- subtracts.push(item);
897
+ if (f.operation === "add") {
898
+ adds.push(item);
899
+ } else {
900
+ subtracts.push(item);
901
+ }
838
902
  }
839
903
  });
840
904
  if (adds.length > 0) {
@@ -867,10 +931,12 @@ function getPerimeterShape(options) {
867
931
  return mainShape;
868
932
  }
869
933
  function applySurfaceFeatures(shape, features, options) {
870
- const internalFeatures = features.filter((f) => f.placement === "internal");
871
- if (internalFeatures.length === 0) return shape;
934
+ const surfaceFeatures = features.filter(
935
+ (f) => f.renderBehavior === "surface"
936
+ );
937
+ if (surfaceFeatures.length === 0) return shape;
872
938
  let result = shape;
873
- for (const f of internalFeatures) {
939
+ for (const f of surfaceFeatures) {
874
940
  const pos = resolveFeaturePosition(f, options);
875
941
  const center = new import_paper2.default.Point(pos.x, pos.y);
876
942
  const item = createFeatureItem(f, center);
@@ -927,9 +993,17 @@ function generateBleedZonePath(originalOptions, offsetOptions, offset) {
927
993
  ensurePaper(paperWidth, paperHeight);
928
994
  import_paper2.default.project.activeLayer.removeChildren();
929
995
  const pOriginal = getPerimeterShape(originalOptions);
930
- const shapeOriginal = applySurfaceFeatures(pOriginal, originalOptions.features, originalOptions);
996
+ const shapeOriginal = applySurfaceFeatures(
997
+ pOriginal,
998
+ originalOptions.features,
999
+ originalOptions
1000
+ );
931
1001
  const pOffset = getPerimeterShape(offsetOptions);
932
- const shapeOffset = applySurfaceFeatures(pOffset, offsetOptions.features, offsetOptions);
1002
+ const shapeOffset = applySurfaceFeatures(
1003
+ pOffset,
1004
+ offsetOptions.features,
1005
+ offsetOptions
1006
+ );
933
1007
  let bleedZone;
934
1008
  if (offset > 0) {
935
1009
  bleedZone = shapeOffset.subtract(shapeOriginal);
@@ -942,13 +1016,29 @@ function generateBleedZonePath(originalOptions, offsetOptions, offset) {
942
1016
  bleedZone.remove();
943
1017
  return pathData;
944
1018
  }
1019
+ function getLowestPointOnDieline(options) {
1020
+ ensurePaper(options.width * 2, options.height * 2);
1021
+ import_paper2.default.project.activeLayer.removeChildren();
1022
+ const shape = createBaseShape(options);
1023
+ const bounds = shape.bounds;
1024
+ const result = {
1025
+ x: bounds.center.x,
1026
+ y: bounds.bottom
1027
+ };
1028
+ shape.remove();
1029
+ return result;
1030
+ }
945
1031
  function getNearestPointOnDieline(point, options) {
946
1032
  ensurePaper(options.width * 2, options.height * 2);
947
1033
  import_paper2.default.project.activeLayer.removeChildren();
948
1034
  const shape = createBaseShape(options);
949
1035
  const p = new import_paper2.default.Point(point.x, point.y);
950
- const nearest = shape.getNearestPoint(p);
951
- const result = { x: nearest.x, y: nearest.y };
1036
+ const location = shape.getNearestLocation(p);
1037
+ const result = {
1038
+ x: location.point.x,
1039
+ y: location.point.y,
1040
+ normal: location.normal ? { x: location.normal.x, y: location.normal.y } : void 0
1041
+ };
952
1042
  shape.remove();
953
1043
  return result;
954
1044
  }
@@ -1341,7 +1431,19 @@ var DielineTool = class {
1341
1431
  title: "Detect Edge from Image",
1342
1432
  handler: async (imageUrl, options) => {
1343
1433
  try {
1344
- const pathData = await ImageTracer.trace(imageUrl, options);
1434
+ const loadImage = (url) => {
1435
+ return new Promise((resolve, reject) => {
1436
+ const img2 = new Image();
1437
+ img2.crossOrigin = "Anonymous";
1438
+ img2.onload = () => resolve(img2);
1439
+ img2.onerror = (e) => reject(e);
1440
+ img2.src = url;
1441
+ });
1442
+ };
1443
+ const [img, pathData] = await Promise.all([
1444
+ loadImage(imageUrl),
1445
+ ImageTracer.trace(imageUrl, options)
1446
+ ]);
1345
1447
  const bounds = getPathBounds(pathData);
1346
1448
  const currentMax = Math.max(s.width, s.height);
1347
1449
  const scale = currentMax / Math.max(bounds.width, bounds.height);
@@ -1350,7 +1452,10 @@ var DielineTool = class {
1350
1452
  return {
1351
1453
  pathData,
1352
1454
  width: newWidth,
1353
- height: newHeight
1455
+ height: newHeight,
1456
+ rawBounds: bounds,
1457
+ imageWidth: img.width,
1458
+ imageHeight: img.height
1354
1459
  };
1355
1460
  } catch (e) {
1356
1461
  console.error("Edge detection failed", e);
@@ -1898,22 +2003,60 @@ var ConstraintRegistry = class {
1898
2003
  static register(type, handler) {
1899
2004
  this.handlers.set(type, handler);
1900
2005
  }
1901
- static apply(x, y, feature, context) {
1902
- if (!feature.constraints || !feature.constraints.type) {
2006
+ static apply(x, y, feature, context, constraints) {
2007
+ const list = constraints || feature.constraints;
2008
+ if (!list || list.length === 0) {
1903
2009
  return { x, y };
1904
2010
  }
1905
- const handler = this.handlers.get(feature.constraints.type);
1906
- if (handler) {
1907
- return handler(x, y, feature, context);
2011
+ let currentX = x;
2012
+ let currentY = y;
2013
+ for (const constraint of list) {
2014
+ const handler = this.handlers.get(constraint.type);
2015
+ if (handler) {
2016
+ const result = handler(currentX, currentY, feature, context, constraint.params || {});
2017
+ currentX = result.x;
2018
+ currentY = result.y;
2019
+ }
1908
2020
  }
1909
- return { x, y };
2021
+ return { x: currentX, y: currentY };
1910
2022
  }
1911
2023
  };
1912
2024
  ConstraintRegistry.handlers = /* @__PURE__ */ new Map();
1913
- var edgeConstraint = (x, y, feature, context) => {
1914
- var _a;
2025
+ var pathConstraint = (x, y, feature, context, params) => {
2026
+ const { dielineWidth, dielineHeight, geometry } = context;
2027
+ if (!geometry) return { x, y };
2028
+ const minX = geometry.x - geometry.width / 2;
2029
+ const minY = geometry.y - geometry.height / 2;
2030
+ const absX = minX + x * geometry.width;
2031
+ const absY = minY + y * geometry.height;
2032
+ const nearest = getNearestPointOnDieline(
2033
+ { x: absX, y: absY },
2034
+ geometry
2035
+ );
2036
+ let finalX = nearest.x;
2037
+ let finalY = nearest.y;
2038
+ const hasOffsetParams = params.minOffset !== void 0 || params.maxOffset !== void 0;
2039
+ if (hasOffsetParams && nearest.normal) {
2040
+ const dx = absX - nearest.x;
2041
+ const dy = absY - nearest.y;
2042
+ const nx2 = nearest.normal.x;
2043
+ const ny2 = nearest.normal.y;
2044
+ const dist = dx * nx2 + dy * ny2;
2045
+ const scale = dielineWidth > 0 ? geometry.width / dielineWidth : 1;
2046
+ const rawMin = params.minOffset !== void 0 ? params.minOffset : 0;
2047
+ const rawMax = params.maxOffset !== void 0 ? params.maxOffset : 0;
2048
+ const minOffset = rawMin * scale;
2049
+ const maxOffset = rawMax * scale;
2050
+ const clampedDist = Math.max(minOffset, Math.min(dist, maxOffset));
2051
+ finalX = nearest.x + nx2 * clampedDist;
2052
+ finalY = nearest.y + ny2 * clampedDist;
2053
+ }
2054
+ const nx = geometry.width > 0 ? (finalX - minX) / geometry.width : 0.5;
2055
+ const ny = geometry.height > 0 ? (finalY - minY) / geometry.height : 0.5;
2056
+ return { x: nx, y: ny };
2057
+ };
2058
+ var edgeConstraint = (x, y, feature, context, params) => {
1915
2059
  const { dielineWidth, dielineHeight } = context;
1916
- const params = ((_a = feature.constraints) == null ? void 0 : _a.params) || {};
1917
2060
  const allowedEdges = params.allowedEdges || [
1918
2061
  "top",
1919
2062
  "bottom",
@@ -1974,10 +2117,8 @@ var edgeConstraint = (x, y, feature, context) => {
1974
2117
  }
1975
2118
  return { x: newX, y: newY };
1976
2119
  };
1977
- var internalConstraint = (x, y, feature, context) => {
1978
- var _a;
2120
+ var internalConstraint = (x, y, feature, context, params) => {
1979
2121
  const { dielineWidth, dielineHeight } = context;
1980
- const params = ((_a = feature.constraints) == null ? void 0 : _a.params) || {};
1981
2122
  const margin = params.margin || 0;
1982
2123
  const fw = feature.width || 0;
1983
2124
  const fh = feature.height || 0;
@@ -1989,10 +2130,8 @@ var internalConstraint = (x, y, feature, context) => {
1989
2130
  const clampedY = minY > maxY ? 0.5 : Math.max(minY, Math.min(y, maxY));
1990
2131
  return { x: clampedX, y: clampedY };
1991
2132
  };
1992
- var tangentBottomConstraint = (x, y, feature, context) => {
1993
- var _a;
2133
+ var tangentBottomConstraint = (x, y, feature, context, params) => {
1994
2134
  const { dielineWidth, dielineHeight } = context;
1995
- const params = ((_a = feature.constraints) == null ? void 0 : _a.params) || {};
1996
2135
  const gap = params.gap || 0;
1997
2136
  const confineX = params.confineX !== false;
1998
2137
  const extentY = feature.shape === "circle" ? feature.radius || 0 : (feature.height || 0) / 2;
@@ -2006,18 +2145,38 @@ var tangentBottomConstraint = (x, y, feature, context) => {
2006
2145
  }
2007
2146
  return { x: newX, y: newY };
2008
2147
  };
2148
+ var lowestTangentConstraint = (x, y, feature, context, params) => {
2149
+ const { dielineWidth, dielineHeight, geometry } = context;
2150
+ if (!geometry) return { x, y };
2151
+ const lowest = getLowestPointOnDieline(geometry);
2152
+ const minY = geometry.y - geometry.height / 2;
2153
+ const normY = (lowest.y - minY) / geometry.height;
2154
+ const gap = params.gap || 0;
2155
+ const confineX = params.confineX !== false;
2156
+ const extentY = feature.shape === "circle" ? feature.radius || 0 : (feature.height || 0) / 2;
2157
+ const newY = normY + (extentY + gap) / dielineHeight;
2158
+ let newX = x;
2159
+ if (confineX) {
2160
+ const extentX = feature.shape === "circle" ? feature.radius || 0 : (feature.width || 0) / 2;
2161
+ const minX = extentX / dielineWidth;
2162
+ const maxX = 1 - extentX / dielineWidth;
2163
+ newX = minX > maxX ? 0.5 : Math.max(minX, Math.min(newX, maxX));
2164
+ }
2165
+ return { x: newX, y: newY };
2166
+ };
2167
+ ConstraintRegistry.register("path", pathConstraint);
2009
2168
  ConstraintRegistry.register("edge", edgeConstraint);
2010
2169
  ConstraintRegistry.register("internal", internalConstraint);
2011
2170
  ConstraintRegistry.register("tangent-bottom", tangentBottomConstraint);
2171
+ ConstraintRegistry.register("lowest-tangent", lowestTangentConstraint);
2012
2172
 
2013
2173
  // src/featureComplete.ts
2014
2174
  function validateFeaturesStrict(features, context) {
2015
- var _a;
2016
2175
  const eps = 1e-6;
2017
2176
  const issues = [];
2018
2177
  for (const f of features) {
2019
- if (!((_a = f.constraints) == null ? void 0 : _a.type)) continue;
2020
- const constrained = ConstraintRegistry.apply(f.x, f.y, f, context);
2178
+ if (!f.constraints || f.constraints.length === 0) continue;
2179
+ const constrained = ConstraintRegistry.apply(f.x, f.y, f, context, f.constraints);
2021
2180
  if (Math.abs(constrained.x - f.x) > eps || Math.abs(constrained.y - f.y) > eps) {
2022
2181
  issues.push({
2023
2182
  featureId: f.id,
@@ -2283,14 +2442,16 @@ var FeatureTool = class {
2283
2442
  const newFeature = {
2284
2443
  id: Date.now().toString(),
2285
2444
  operation: type,
2286
- placement: "edge",
2287
2445
  shape: "rect",
2288
2446
  x: 0.5,
2289
2447
  y: 0,
2290
2448
  // Top edge
2291
2449
  width: 10,
2292
2450
  height: 10,
2293
- rotation: 0
2451
+ rotation: 0,
2452
+ renderBehavior: "edge",
2453
+ // Default constraint: path (snap to edge)
2454
+ constraints: [{ type: "path" }]
2294
2455
  };
2295
2456
  this.setWorkingFeatures([...this.workingFeatures || [], newFeature]);
2296
2457
  this.redraw();
@@ -2306,22 +2467,24 @@ var FeatureTool = class {
2306
2467
  groupId,
2307
2468
  operation: "add",
2308
2469
  shape: "circle",
2309
- placement: "edge",
2310
2470
  x: 0.5,
2311
2471
  y: 0,
2312
2472
  radius: 20,
2313
- rotation: 0
2473
+ rotation: 0,
2474
+ renderBehavior: "edge",
2475
+ constraints: [{ type: "path" }]
2314
2476
  };
2315
2477
  const hole = {
2316
2478
  id: `${timestamp}-hole`,
2317
2479
  groupId,
2318
2480
  operation: "subtract",
2319
2481
  shape: "circle",
2320
- placement: "edge",
2321
2482
  x: 0.5,
2322
2483
  y: 0,
2323
2484
  radius: 15,
2324
- rotation: 0
2485
+ rotation: 0,
2486
+ renderBehavior: "edge",
2487
+ constraints: [{ type: "path" }]
2325
2488
  };
2326
2489
  this.setWorkingFeatures([...this.workingFeatures || [], lug, hole]);
2327
2490
  this.redraw();
@@ -2460,50 +2623,32 @@ var FeatureTool = class {
2460
2623
  this.canvasService.requestRenderAll();
2461
2624
  }
2462
2625
  constrainPosition(p, geometry, limit, feature) {
2463
- if (feature && feature.constraints) {
2464
- const minX = geometry.x - geometry.width / 2;
2465
- const minY = geometry.y - geometry.height / 2;
2466
- const nx = geometry.width > 0 ? (p.x - minX) / geometry.width : 0.5;
2467
- const ny = geometry.height > 0 ? (p.y - minY) / geometry.height : 0.5;
2468
- const scale2 = geometry.scale || 1;
2469
- const dielineWidth = geometry.width / scale2;
2470
- const dielineHeight = geometry.height / scale2;
2471
- const constrained = ConstraintRegistry.apply(nx, ny, feature, {
2472
- dielineWidth,
2473
- dielineHeight
2474
- });
2475
- return {
2476
- x: minX + constrained.x * geometry.width,
2477
- y: minY + constrained.y * geometry.height
2478
- };
2479
- }
2480
- if (feature && feature.placement === "internal") {
2481
- const minX = geometry.x - geometry.width / 2;
2482
- const maxX = geometry.x + geometry.width / 2;
2483
- const minY = geometry.y - geometry.height / 2;
2484
- const maxY = geometry.y + geometry.height / 2;
2485
- return {
2486
- x: Math.max(minX, Math.min(maxX, p.x)),
2487
- y: Math.max(minY, Math.min(maxY, p.y))
2488
- };
2626
+ var _a;
2627
+ if (!feature) {
2628
+ return { x: p.x, y: p.y };
2489
2629
  }
2490
- const nearest = getNearestPointOnDieline(
2491
- { x: p.x, y: p.y },
2630
+ const minX = geometry.x - geometry.width / 2;
2631
+ const minY = geometry.y - geometry.height / 2;
2632
+ const nx = geometry.width > 0 ? (p.x - minX) / geometry.width : 0.5;
2633
+ const ny = geometry.height > 0 ? (p.y - minY) / geometry.height : 0.5;
2634
+ const scale = geometry.scale || 1;
2635
+ const dielineWidth = geometry.width / scale;
2636
+ const dielineHeight = geometry.height / scale;
2637
+ const activeConstraints = (_a = feature.constraints) == null ? void 0 : _a.filter((c) => !c.validateOnly);
2638
+ const constrained = ConstraintRegistry.apply(
2639
+ nx,
2640
+ ny,
2641
+ feature,
2492
2642
  {
2493
- ...geometry,
2494
- features: []
2495
- }
2643
+ dielineWidth,
2644
+ dielineHeight,
2645
+ geometry
2646
+ },
2647
+ activeConstraints
2496
2648
  );
2497
- const dx = p.x - nearest.x;
2498
- const dy = p.y - nearest.y;
2499
- const dist = Math.sqrt(dx * dx + dy * dy);
2500
- if (dist <= limit) {
2501
- return { x: p.x, y: p.y };
2502
- }
2503
- const scale = limit / dist;
2504
2649
  return {
2505
- x: nearest.x + dx * scale,
2506
- y: nearest.y + dy * scale
2650
+ x: minX + constrained.x * geometry.width,
2651
+ y: minY + constrained.y * geometry.height
2507
2652
  };
2508
2653
  }
2509
2654
  syncFeatureFromCanvas(target) {
@@ -2594,6 +2739,33 @@ var FeatureTool = class {
2594
2739
  if (feature.rotation) {
2595
2740
  shape.rotate(feature.rotation);
2596
2741
  }
2742
+ if (feature.bridge && feature.bridge.type === "vertical") {
2743
+ const bridgeIndicator = new import_fabric4.Rect({
2744
+ width: visualWidth,
2745
+ height: 100 * featureScale,
2746
+ // Arbitrary long length to show direction
2747
+ fill: "transparent",
2748
+ stroke: "#888",
2749
+ strokeWidth: 1,
2750
+ strokeDashArray: [2, 2],
2751
+ originX: "center",
2752
+ originY: "bottom",
2753
+ // Anchor at bottom so it extends up
2754
+ left: pos.x,
2755
+ top: pos.y - visualHeight / 2,
2756
+ // Start from top of feature
2757
+ opacity: 0.5,
2758
+ selectable: false,
2759
+ evented: false
2760
+ });
2761
+ const group = new import_fabric4.Group([bridgeIndicator, shape], {
2762
+ originX: "center",
2763
+ originY: "center",
2764
+ left: pos.x,
2765
+ top: pos.y
2766
+ });
2767
+ return group;
2768
+ }
2597
2769
  return shape;
2598
2770
  };
2599
2771
  singles.forEach(({ feature, index }) => {