@pooder/kit 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/index.d.mts +12 -5
  3. package/dist/index.d.ts +12 -5
  4. package/dist/index.js +236 -75
  5. package/dist/index.mjs +236 -75
  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 +955 -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 +161 -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,11 @@
1
1
  # @pooder/kit
2
2
 
3
+ ## 4.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - bridge and constraints
8
+
3
9
  ## 4.2.0
4
10
 
5
11
  ### 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,78 @@ 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 startY = bridgeBottom + 1;
841
+ const bridgeRect = new import_paper2.default.Path.Rectangle({
842
+ from: [itemBounds.left, bridgeTop],
843
+ to: [itemBounds.right, startY],
844
+ insert: false
845
+ });
846
+ const gaps = bridgeRect.subtract(mainShape);
847
+ bridgeRect.remove();
848
+ let bridgePart = null;
849
+ const isBottomPart = (part) => {
850
+ return Math.abs(part.bounds.bottom - startY) < 2;
851
+ };
852
+ if (gaps instanceof import_paper2.default.CompoundPath) {
853
+ const children = gaps.children;
854
+ let maxBottom = -Infinity;
855
+ let bestChild = null;
856
+ for (const child of children) {
857
+ if (child.bounds.bottom > maxBottom) {
858
+ maxBottom = child.bounds.bottom;
859
+ bestChild = child;
860
+ }
861
+ }
862
+ if (bestChild && isBottomPart(bestChild)) {
863
+ bridgePart = bestChild.clone();
864
+ }
865
+ } else if (gaps instanceof import_paper2.default.Path) {
866
+ if (isBottomPart(gaps)) {
867
+ bridgePart = gaps.clone();
868
+ }
869
+ }
870
+ gaps.remove();
871
+ if (bridgePart) {
872
+ const bounds = bridgePart.bounds;
873
+ if (bounds.height > 0) {
874
+ const overlap = 1;
875
+ const scaleY = (bounds.height + overlap) / bounds.height;
876
+ bridgePart.scale(1, scaleY, new import_paper2.default.Point(bounds.center.x, bounds.bottom));
877
+ }
878
+ const unitedItem = item.unite(bridgePart);
879
+ item.remove();
880
+ bridgePart.remove();
881
+ if (f.operation === "add") {
882
+ adds.push(unitedItem);
883
+ } else {
884
+ subtracts.push(unitedItem);
885
+ }
886
+ } else {
887
+ if (f.operation === "add") {
888
+ adds.push(item);
889
+ } else {
890
+ subtracts.push(item);
891
+ }
892
+ }
893
+ } else {
894
+ if (f.operation === "add") {
895
+ adds.push(item);
896
+ } else {
897
+ subtracts.push(item);
898
+ }
899
+ }
836
900
  } else {
837
- subtracts.push(item);
901
+ if (f.operation === "add") {
902
+ adds.push(item);
903
+ } else {
904
+ subtracts.push(item);
905
+ }
838
906
  }
839
907
  });
840
908
  if (adds.length > 0) {
@@ -867,10 +935,12 @@ function getPerimeterShape(options) {
867
935
  return mainShape;
868
936
  }
869
937
  function applySurfaceFeatures(shape, features, options) {
870
- const internalFeatures = features.filter((f) => f.placement === "internal");
871
- if (internalFeatures.length === 0) return shape;
938
+ const surfaceFeatures = features.filter(
939
+ (f) => f.renderBehavior === "surface"
940
+ );
941
+ if (surfaceFeatures.length === 0) return shape;
872
942
  let result = shape;
873
- for (const f of internalFeatures) {
943
+ for (const f of surfaceFeatures) {
874
944
  const pos = resolveFeaturePosition(f, options);
875
945
  const center = new import_paper2.default.Point(pos.x, pos.y);
876
946
  const item = createFeatureItem(f, center);
@@ -927,9 +997,17 @@ function generateBleedZonePath(originalOptions, offsetOptions, offset) {
927
997
  ensurePaper(paperWidth, paperHeight);
928
998
  import_paper2.default.project.activeLayer.removeChildren();
929
999
  const pOriginal = getPerimeterShape(originalOptions);
930
- const shapeOriginal = applySurfaceFeatures(pOriginal, originalOptions.features, originalOptions);
1000
+ const shapeOriginal = applySurfaceFeatures(
1001
+ pOriginal,
1002
+ originalOptions.features,
1003
+ originalOptions
1004
+ );
931
1005
  const pOffset = getPerimeterShape(offsetOptions);
932
- const shapeOffset = applySurfaceFeatures(pOffset, offsetOptions.features, offsetOptions);
1006
+ const shapeOffset = applySurfaceFeatures(
1007
+ pOffset,
1008
+ offsetOptions.features,
1009
+ offsetOptions
1010
+ );
933
1011
  let bleedZone;
934
1012
  if (offset > 0) {
935
1013
  bleedZone = shapeOffset.subtract(shapeOriginal);
@@ -942,13 +1020,29 @@ function generateBleedZonePath(originalOptions, offsetOptions, offset) {
942
1020
  bleedZone.remove();
943
1021
  return pathData;
944
1022
  }
1023
+ function getLowestPointOnDieline(options) {
1024
+ ensurePaper(options.width * 2, options.height * 2);
1025
+ import_paper2.default.project.activeLayer.removeChildren();
1026
+ const shape = createBaseShape(options);
1027
+ const bounds = shape.bounds;
1028
+ const result = {
1029
+ x: bounds.center.x,
1030
+ y: bounds.bottom
1031
+ };
1032
+ shape.remove();
1033
+ return result;
1034
+ }
945
1035
  function getNearestPointOnDieline(point, options) {
946
1036
  ensurePaper(options.width * 2, options.height * 2);
947
1037
  import_paper2.default.project.activeLayer.removeChildren();
948
1038
  const shape = createBaseShape(options);
949
1039
  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 };
1040
+ const location = shape.getNearestLocation(p);
1041
+ const result = {
1042
+ x: location.point.x,
1043
+ y: location.point.y,
1044
+ normal: location.normal ? { x: location.normal.x, y: location.normal.y } : void 0
1045
+ };
952
1046
  shape.remove();
953
1047
  return result;
954
1048
  }
@@ -1898,22 +1992,60 @@ var ConstraintRegistry = class {
1898
1992
  static register(type, handler) {
1899
1993
  this.handlers.set(type, handler);
1900
1994
  }
1901
- static apply(x, y, feature, context) {
1902
- if (!feature.constraints || !feature.constraints.type) {
1995
+ static apply(x, y, feature, context, constraints) {
1996
+ const list = constraints || feature.constraints;
1997
+ if (!list || list.length === 0) {
1903
1998
  return { x, y };
1904
1999
  }
1905
- const handler = this.handlers.get(feature.constraints.type);
1906
- if (handler) {
1907
- return handler(x, y, feature, context);
2000
+ let currentX = x;
2001
+ let currentY = y;
2002
+ for (const constraint of list) {
2003
+ const handler = this.handlers.get(constraint.type);
2004
+ if (handler) {
2005
+ const result = handler(currentX, currentY, feature, context, constraint.params || {});
2006
+ currentX = result.x;
2007
+ currentY = result.y;
2008
+ }
1908
2009
  }
1909
- return { x, y };
2010
+ return { x: currentX, y: currentY };
1910
2011
  }
1911
2012
  };
1912
2013
  ConstraintRegistry.handlers = /* @__PURE__ */ new Map();
1913
- var edgeConstraint = (x, y, feature, context) => {
1914
- var _a;
2014
+ var pathConstraint = (x, y, feature, context, params) => {
2015
+ const { dielineWidth, dielineHeight, geometry } = context;
2016
+ if (!geometry) return { x, y };
2017
+ const minX = geometry.x - geometry.width / 2;
2018
+ const minY = geometry.y - geometry.height / 2;
2019
+ const absX = minX + x * geometry.width;
2020
+ const absY = minY + y * geometry.height;
2021
+ const nearest = getNearestPointOnDieline(
2022
+ { x: absX, y: absY },
2023
+ geometry
2024
+ );
2025
+ let finalX = nearest.x;
2026
+ let finalY = nearest.y;
2027
+ const hasOffsetParams = params.minOffset !== void 0 || params.maxOffset !== void 0;
2028
+ if (hasOffsetParams && nearest.normal) {
2029
+ const dx = absX - nearest.x;
2030
+ const dy = absY - nearest.y;
2031
+ const nx2 = nearest.normal.x;
2032
+ const ny2 = nearest.normal.y;
2033
+ const dist = dx * nx2 + dy * ny2;
2034
+ const scale = dielineWidth > 0 ? geometry.width / dielineWidth : 1;
2035
+ const rawMin = params.minOffset !== void 0 ? params.minOffset : 0;
2036
+ const rawMax = params.maxOffset !== void 0 ? params.maxOffset : 0;
2037
+ const minOffset = rawMin * scale;
2038
+ const maxOffset = rawMax * scale;
2039
+ const clampedDist = Math.max(minOffset, Math.min(dist, maxOffset));
2040
+ finalX = nearest.x + nx2 * clampedDist;
2041
+ finalY = nearest.y + ny2 * clampedDist;
2042
+ }
2043
+ const nx = geometry.width > 0 ? (finalX - minX) / geometry.width : 0.5;
2044
+ const ny = geometry.height > 0 ? (finalY - minY) / geometry.height : 0.5;
2045
+ return { x: nx, y: ny };
2046
+ };
2047
+ var edgeConstraint = (x, y, feature, context, params) => {
1915
2048
  const { dielineWidth, dielineHeight } = context;
1916
- const params = ((_a = feature.constraints) == null ? void 0 : _a.params) || {};
1917
2049
  const allowedEdges = params.allowedEdges || [
1918
2050
  "top",
1919
2051
  "bottom",
@@ -1974,10 +2106,8 @@ var edgeConstraint = (x, y, feature, context) => {
1974
2106
  }
1975
2107
  return { x: newX, y: newY };
1976
2108
  };
1977
- var internalConstraint = (x, y, feature, context) => {
1978
- var _a;
2109
+ var internalConstraint = (x, y, feature, context, params) => {
1979
2110
  const { dielineWidth, dielineHeight } = context;
1980
- const params = ((_a = feature.constraints) == null ? void 0 : _a.params) || {};
1981
2111
  const margin = params.margin || 0;
1982
2112
  const fw = feature.width || 0;
1983
2113
  const fh = feature.height || 0;
@@ -1989,10 +2119,8 @@ var internalConstraint = (x, y, feature, context) => {
1989
2119
  const clampedY = minY > maxY ? 0.5 : Math.max(minY, Math.min(y, maxY));
1990
2120
  return { x: clampedX, y: clampedY };
1991
2121
  };
1992
- var tangentBottomConstraint = (x, y, feature, context) => {
1993
- var _a;
2122
+ var tangentBottomConstraint = (x, y, feature, context, params) => {
1994
2123
  const { dielineWidth, dielineHeight } = context;
1995
- const params = ((_a = feature.constraints) == null ? void 0 : _a.params) || {};
1996
2124
  const gap = params.gap || 0;
1997
2125
  const confineX = params.confineX !== false;
1998
2126
  const extentY = feature.shape === "circle" ? feature.radius || 0 : (feature.height || 0) / 2;
@@ -2006,18 +2134,38 @@ var tangentBottomConstraint = (x, y, feature, context) => {
2006
2134
  }
2007
2135
  return { x: newX, y: newY };
2008
2136
  };
2137
+ var lowestTangentConstraint = (x, y, feature, context, params) => {
2138
+ const { dielineWidth, dielineHeight, geometry } = context;
2139
+ if (!geometry) return { x, y };
2140
+ const lowest = getLowestPointOnDieline(geometry);
2141
+ const minY = geometry.y - geometry.height / 2;
2142
+ const normY = (lowest.y - minY) / geometry.height;
2143
+ const gap = params.gap || 0;
2144
+ const confineX = params.confineX !== false;
2145
+ const extentY = feature.shape === "circle" ? feature.radius || 0 : (feature.height || 0) / 2;
2146
+ const newY = normY + (extentY + gap) / dielineHeight;
2147
+ let newX = x;
2148
+ if (confineX) {
2149
+ const extentX = feature.shape === "circle" ? feature.radius || 0 : (feature.width || 0) / 2;
2150
+ const minX = extentX / dielineWidth;
2151
+ const maxX = 1 - extentX / dielineWidth;
2152
+ newX = minX > maxX ? 0.5 : Math.max(minX, Math.min(newX, maxX));
2153
+ }
2154
+ return { x: newX, y: newY };
2155
+ };
2156
+ ConstraintRegistry.register("path", pathConstraint);
2009
2157
  ConstraintRegistry.register("edge", edgeConstraint);
2010
2158
  ConstraintRegistry.register("internal", internalConstraint);
2011
2159
  ConstraintRegistry.register("tangent-bottom", tangentBottomConstraint);
2160
+ ConstraintRegistry.register("lowest-tangent", lowestTangentConstraint);
2012
2161
 
2013
2162
  // src/featureComplete.ts
2014
2163
  function validateFeaturesStrict(features, context) {
2015
- var _a;
2016
2164
  const eps = 1e-6;
2017
2165
  const issues = [];
2018
2166
  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);
2167
+ if (!f.constraints || f.constraints.length === 0) continue;
2168
+ const constrained = ConstraintRegistry.apply(f.x, f.y, f, context, f.constraints);
2021
2169
  if (Math.abs(constrained.x - f.x) > eps || Math.abs(constrained.y - f.y) > eps) {
2022
2170
  issues.push({
2023
2171
  featureId: f.id,
@@ -2283,14 +2431,16 @@ var FeatureTool = class {
2283
2431
  const newFeature = {
2284
2432
  id: Date.now().toString(),
2285
2433
  operation: type,
2286
- placement: "edge",
2287
2434
  shape: "rect",
2288
2435
  x: 0.5,
2289
2436
  y: 0,
2290
2437
  // Top edge
2291
2438
  width: 10,
2292
2439
  height: 10,
2293
- rotation: 0
2440
+ rotation: 0,
2441
+ renderBehavior: "edge",
2442
+ // Default constraint: path (snap to edge)
2443
+ constraints: [{ type: "path" }]
2294
2444
  };
2295
2445
  this.setWorkingFeatures([...this.workingFeatures || [], newFeature]);
2296
2446
  this.redraw();
@@ -2306,22 +2456,24 @@ var FeatureTool = class {
2306
2456
  groupId,
2307
2457
  operation: "add",
2308
2458
  shape: "circle",
2309
- placement: "edge",
2310
2459
  x: 0.5,
2311
2460
  y: 0,
2312
2461
  radius: 20,
2313
- rotation: 0
2462
+ rotation: 0,
2463
+ renderBehavior: "edge",
2464
+ constraints: [{ type: "path" }]
2314
2465
  };
2315
2466
  const hole = {
2316
2467
  id: `${timestamp}-hole`,
2317
2468
  groupId,
2318
2469
  operation: "subtract",
2319
2470
  shape: "circle",
2320
- placement: "edge",
2321
2471
  x: 0.5,
2322
2472
  y: 0,
2323
2473
  radius: 15,
2324
- rotation: 0
2474
+ rotation: 0,
2475
+ renderBehavior: "edge",
2476
+ constraints: [{ type: "path" }]
2325
2477
  };
2326
2478
  this.setWorkingFeatures([...this.workingFeatures || [], lug, hole]);
2327
2479
  this.redraw();
@@ -2460,50 +2612,32 @@ var FeatureTool = class {
2460
2612
  this.canvasService.requestRenderAll();
2461
2613
  }
2462
2614
  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
- };
2615
+ var _a;
2616
+ if (!feature) {
2617
+ return { x: p.x, y: p.y };
2489
2618
  }
2490
- const nearest = getNearestPointOnDieline(
2491
- { x: p.x, y: p.y },
2619
+ const minX = geometry.x - geometry.width / 2;
2620
+ const minY = geometry.y - geometry.height / 2;
2621
+ const nx = geometry.width > 0 ? (p.x - minX) / geometry.width : 0.5;
2622
+ const ny = geometry.height > 0 ? (p.y - minY) / geometry.height : 0.5;
2623
+ const scale = geometry.scale || 1;
2624
+ const dielineWidth = geometry.width / scale;
2625
+ const dielineHeight = geometry.height / scale;
2626
+ const activeConstraints = (_a = feature.constraints) == null ? void 0 : _a.filter((c) => !c.validateOnly);
2627
+ const constrained = ConstraintRegistry.apply(
2628
+ nx,
2629
+ ny,
2630
+ feature,
2492
2631
  {
2493
- ...geometry,
2494
- features: []
2495
- }
2632
+ dielineWidth,
2633
+ dielineHeight,
2634
+ geometry
2635
+ },
2636
+ activeConstraints
2496
2637
  );
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
2638
  return {
2505
- x: nearest.x + dx * scale,
2506
- y: nearest.y + dy * scale
2639
+ x: minX + constrained.x * geometry.width,
2640
+ y: minY + constrained.y * geometry.height
2507
2641
  };
2508
2642
  }
2509
2643
  syncFeatureFromCanvas(target) {
@@ -2594,6 +2728,33 @@ var FeatureTool = class {
2594
2728
  if (feature.rotation) {
2595
2729
  shape.rotate(feature.rotation);
2596
2730
  }
2731
+ if (feature.bridge && feature.bridge.type === "vertical") {
2732
+ const bridgeIndicator = new import_fabric4.Rect({
2733
+ width: visualWidth,
2734
+ height: 100 * featureScale,
2735
+ // Arbitrary long length to show direction
2736
+ fill: "transparent",
2737
+ stroke: "#888",
2738
+ strokeWidth: 1,
2739
+ strokeDashArray: [2, 2],
2740
+ originX: "center",
2741
+ originY: "bottom",
2742
+ // Anchor at bottom so it extends up
2743
+ left: pos.x,
2744
+ top: pos.y - visualHeight / 2,
2745
+ // Start from top of feature
2746
+ opacity: 0.5,
2747
+ selectable: false,
2748
+ evented: false
2749
+ });
2750
+ const group = new import_fabric4.Group([bridgeIndicator, shape], {
2751
+ originX: "center",
2752
+ originY: "center",
2753
+ left: pos.x,
2754
+ top: pos.y
2755
+ });
2756
+ return group;
2757
+ }
2597
2758
  return shape;
2598
2759
  };
2599
2760
  singles.forEach(({ feature, index }) => {