@rxflow/manhattan 0.0.1-alpha.11 → 0.0.1-alpha.13

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 (49) hide show
  1. package/cjs/geometry/Rectangle.d.ts +1 -1
  2. package/cjs/geometry/Rectangle.js +2 -2
  3. package/cjs/getManHattanPath.d.ts.map +1 -1
  4. package/cjs/getManHattanPath.js +153 -20
  5. package/cjs/obstacle/ObstacleMap.d.ts +7 -1
  6. package/cjs/obstacle/ObstacleMap.d.ts.map +1 -1
  7. package/cjs/obstacle/ObstacleMap.js +53 -1
  8. package/cjs/options/defaults.d.ts +1 -1
  9. package/cjs/options/defaults.d.ts.map +1 -1
  10. package/cjs/options/defaults.js +1 -1
  11. package/cjs/options/resolver.d.ts.map +1 -1
  12. package/cjs/options/resolver.js +4 -2
  13. package/cjs/options/types.d.ts +19 -6
  14. package/cjs/options/types.d.ts.map +1 -1
  15. package/cjs/pathfinder/findRoute.d.ts.map +1 -1
  16. package/cjs/pathfinder/findRoute.js +239 -15
  17. package/cjs/svg/pathConverter.d.ts.map +1 -1
  18. package/cjs/svg/pathConverter.js +23 -12
  19. package/cjs/utils/getAnchorPoints.d.ts +15 -0
  20. package/cjs/utils/getAnchorPoints.d.ts.map +1 -0
  21. package/cjs/utils/getAnchorPoints.js +75 -0
  22. package/cjs/utils/index.d.ts +1 -0
  23. package/cjs/utils/index.d.ts.map +1 -1
  24. package/cjs/utils/index.js +11 -0
  25. package/esm/geometry/Rectangle.d.ts +1 -1
  26. package/esm/geometry/Rectangle.js +2 -2
  27. package/esm/getManHattanPath.d.ts.map +1 -1
  28. package/esm/getManHattanPath.js +162 -22
  29. package/esm/obstacle/ObstacleMap.d.ts +7 -1
  30. package/esm/obstacle/ObstacleMap.d.ts.map +1 -1
  31. package/esm/obstacle/ObstacleMap.js +78 -0
  32. package/esm/options/defaults.d.ts +1 -1
  33. package/esm/options/defaults.d.ts.map +1 -1
  34. package/esm/options/defaults.js +1 -1
  35. package/esm/options/resolver.d.ts.map +1 -1
  36. package/esm/options/resolver.js +5 -3
  37. package/esm/options/types.d.ts +19 -6
  38. package/esm/options/types.d.ts.map +1 -1
  39. package/esm/pathfinder/findRoute.d.ts.map +1 -1
  40. package/esm/pathfinder/findRoute.js +262 -23
  41. package/esm/svg/pathConverter.d.ts.map +1 -1
  42. package/esm/svg/pathConverter.js +23 -12
  43. package/esm/utils/getAnchorPoints.d.ts +15 -0
  44. package/esm/utils/getAnchorPoints.d.ts.map +1 -0
  45. package/esm/utils/getAnchorPoints.js +69 -0
  46. package/esm/utils/index.d.ts +1 -0
  47. package/esm/utils/index.d.ts.map +1 -1
  48. package/esm/utils/index.js +2 -1
  49. package/package.json +1 -1
@@ -25,7 +25,7 @@ export declare class Rectangle {
25
25
  */
26
26
  getCorner(): Point;
27
27
  /**
28
- * Check if a point is contained within this rectangle
28
+ * Check if a point is contained within this rectangle (interior only, excluding edges)
29
29
  */
30
30
  containsPoint(point: Point): boolean;
31
31
  /**
@@ -49,10 +49,10 @@ class Rectangle {
49
49
  }
50
50
 
51
51
  /**
52
- * Check if a point is contained within this rectangle
52
+ * Check if a point is contained within this rectangle (interior only, excluding edges)
53
53
  */
54
54
  containsPoint(point) {
55
- return point.x >= this.x && point.x <= this.x + this.width && point.y >= this.y && point.y <= this.y + this.height;
55
+ return point.x > this.x && point.x < this.x + this.width && point.y > this.y && point.y < this.y + this.height;
56
56
  }
57
57
 
58
58
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"getManHattanPath.d.ts","sourceRoot":"","sources":["../src/getManHattanPath.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,QAAQ,EAAE,MAAM,eAAe,CAAA;AAG3D,OAAO,KAAK,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAMnE;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;OAEG;IACH,YAAY,EAAE,MAAM,CAAA;IAEpB;;OAEG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAEhB,cAAc,EAAE,QAAQ,CAAC;IACzB,cAAc,EAAE,QAAQ,CAAC;IACzB;;OAEG;IACH,UAAU,EAAE,UAAU,CAAA;IAEtB;;OAEG;IACH,OAAO,CAAC,EAAE,sBAAsB,CAAA;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAwRvE"}
1
+ {"version":3,"file":"getManHattanPath.d.ts","sourceRoot":"","sources":["../src/getManHattanPath.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,QAAQ,EAAE,MAAM,eAAe,CAAA;AAG3D,OAAO,KAAK,EAAE,sBAAsB,EAAE,UAAU,EAAa,MAAM,WAAW,CAAA;AAM9E;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;OAEG;IACH,YAAY,EAAE,MAAM,CAAA;IAEpB;;OAEG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAEhB,cAAc,EAAE,QAAQ,CAAC;IACzB,cAAc,EAAE,QAAQ,CAAC;IACzB;;OAEG;IACH,UAAU,EAAE,UAAU,CAAA;IAEtB;;OAEG;IACH,OAAO,CAAC,EAAE,sBAAsB,CAAA;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAgbvE"}
@@ -51,8 +51,18 @@ function getManHattanPath(params) {
51
51
  options: userOptions = {}
52
52
  } = params;
53
53
 
54
- // Resolve options
55
- const options = (0, _options.resolveOptions)(userOptions);
54
+ // Resolve options and add position information
55
+ const options = (0, _options.resolveOptions)({
56
+ ...userOptions,
57
+ sourcePosition,
58
+ targetPosition
59
+ });
60
+
61
+ // Direction control is automatically handled by getRectPoints:
62
+ // - When anchor is on an edge, only outward directions are allowed (via isDirectionOutward)
63
+ // - For sourcePosition="right": anchor on right edge -> only extends right
64
+ // - For targetPosition="left": anchor on left edge -> path approaches from right (outward from left)
65
+ // This ensures paths follow the sourcePosition and targetPosition constraints
56
66
 
57
67
  // Get source and target nodes
58
68
  const sourceNode = nodeLookup.get(sourceNodeId);
@@ -117,6 +127,17 @@ function getManHattanPath(params) {
117
127
  console.log('[getManHattanPath] Source anchor:', `(${sourceAnchor.x}, ${sourceAnchor.y})`);
118
128
  console.log('[getManHattanPath] Target anchor:', `(${targetAnchor.x}, ${targetAnchor.y})`);
119
129
 
130
+ // If using smart point generation (sourcePosition/targetPosition specified),
131
+ // the route already contains the correct extension points, so skip manual processing
132
+ const useSmartPoints = sourcePosition || targetPosition;
133
+ if (useSmartPoints) {
134
+ console.log('[getManHattanPath] Using smart points, skipping manual extension point processing');
135
+ // Add source and target anchors to route
136
+ const finalRoute = [sourceAnchor, ...route, targetAnchor];
137
+ console.log('[getManHattanPath] Final route:', finalRoute.map(p => `(${p.x}, ${p.y})`));
138
+ return (0, _svg.pointsToPath)(finalRoute, options.precision, options.borderRadius);
139
+ }
140
+
120
141
  // Remove extension points from route that were added by getRectPoints
121
142
  // We will add our own with fixed step distance
122
143
  const step = options.step;
@@ -160,10 +181,9 @@ function getManHattanPath(params) {
160
181
  }
161
182
  }
162
183
 
163
- // Insert extension point at source - always extend away from node edge by fixed step distance
164
- // Add borderRadius to extension distance to account for rounded corners
184
+ // Insert extension point at source - always extend away from node edge by fixed distance
165
185
  if (route.length > 0) {
166
- const extensionDistance = step + options.borderRadius;
186
+ const extensionDistance = options.extensionDistance;
167
187
  const firstPoint = route[0];
168
188
 
169
189
  // Determine which edge the source anchor is on
@@ -213,10 +233,75 @@ function getManHattanPath(params) {
213
233
  }
214
234
  }
215
235
 
216
- // Insert extension point at target - always extend away from node edge by fixed step distance
217
- // Add borderRadius to extension distance to account for rounded corners
236
+ // Remove redundant points after source extension
237
+ // If the first route point has the same x or y coordinate as the source anchor, it's redundant
238
+ if (route.length > 2) {
239
+ const firstRoutePoint = route[0]; // Extension point
240
+ const secondRoutePoint = route[1]; // Corner point (if exists)
241
+ const thirdRoutePoint = route[2]; // Original A* point
242
+
243
+ // Check if the third point (original A* point) is redundant
244
+ // It's redundant if it's on the same line as the corner point and can be skipped
245
+ const sameX = Math.abs(thirdRoutePoint.x - sourceAnchor.x) < tolerance;
246
+ const sameY = Math.abs(thirdRoutePoint.y - sourceAnchor.y) < tolerance;
247
+ if (sameX || sameY) {
248
+ // The third point is aligned with the source anchor, likely redundant
249
+ // Check if we can skip it by connecting corner point directly to the next point
250
+ if (route.length > 3) {
251
+ const fourthPoint = route[3];
252
+ // If corner point and fourth point form a straight line, remove the third point
253
+ const cornerToThird = Math.abs(secondRoutePoint.x - thirdRoutePoint.x) < tolerance || Math.abs(secondRoutePoint.y - thirdRoutePoint.y) < tolerance;
254
+ const thirdToFourth = Math.abs(thirdRoutePoint.x - fourthPoint.x) < tolerance || Math.abs(thirdRoutePoint.y - fourthPoint.y) < tolerance;
255
+ if (cornerToThird && thirdToFourth) {
256
+ console.log('[getManHattanPath] Removing redundant point:', `(${thirdRoutePoint.x}, ${thirdRoutePoint.y})`);
257
+ route.splice(2, 1); // Remove the third point
258
+ }
259
+ }
260
+ }
261
+ }
262
+
263
+ // Optimize zigzag patterns BEFORE inserting target extension
264
+ // Check for patterns like: (1360, 16) -> (815, 16) -> (815, -134)
265
+ // where the middle segment goes to target edge and then moves along it
266
+ // Use target bbox with padding (same as ObstacleMap)
267
+ const targetBBoxWithPadding = targetBBox.moveAndExpand(options.paddingBox);
268
+ console.log('[getManHattanPath] Route before zigzag check:', route.map(p => `(${p.x}, ${p.y})`));
269
+ console.log('[getManHattanPath] Target BBox with padding:', `x=${targetBBoxWithPadding.x}, y=${targetBBoxWithPadding.y}`);
270
+ if (route.length >= 3) {
271
+ let i = 0;
272
+ while (i < route.length - 2) {
273
+ const p1 = route[i];
274
+ const p2 = route[i + 1];
275
+ const p3 = route[i + 2];
276
+
277
+ // Check if p2 is on the target bbox edge (with padding)
278
+ const p2OnTargetLeftEdge = Math.abs(p2.x - targetBBoxWithPadding.x) < tolerance;
279
+ const p2OnTargetRightEdge = Math.abs(p2.x - (targetBBoxWithPadding.x + targetBBoxWithPadding.width)) < tolerance;
280
+ const p2OnTargetTopEdge = Math.abs(p2.y - targetBBoxWithPadding.y) < tolerance;
281
+ const p2OnTargetBottomEdge = Math.abs(p2.y - (targetBBoxWithPadding.y + targetBBoxWithPadding.height)) < tolerance;
282
+ const p2OnTargetEdge = p2OnTargetLeftEdge || p2OnTargetRightEdge || p2OnTargetTopEdge || p2OnTargetBottomEdge;
283
+ console.log(`[getManHattanPath] Checking i=${i}: p2=(${p2.x}, ${p2.y}), onEdge=${p2OnTargetEdge}`);
284
+ if (p2OnTargetEdge) {
285
+ // Check if p1 -> p2 -> p3 forms a zigzag
286
+ const p1ToP2Horizontal = Math.abs(p1.y - p2.y) < tolerance;
287
+ const p2ToP3Vertical = Math.abs(p2.x - p3.x) < tolerance;
288
+ const p1ToP2Vertical = Math.abs(p1.x - p2.x) < tolerance;
289
+ const p2ToP3Horizontal = Math.abs(p2.y - p3.y) < tolerance;
290
+ console.log(`[getManHattanPath] Zigzag pattern: H->V=${p1ToP2Horizontal && p2ToP3Vertical}, V->H=${p1ToP2Vertical && p2ToP3Horizontal}`);
291
+ if (p1ToP2Horizontal && p2ToP3Vertical || p1ToP2Vertical && p2ToP3Horizontal) {
292
+ // We have a zigzag at target edge, remove p2 and p3
293
+ console.log('[getManHattanPath] Removing zigzag at target edge:', `(${p2.x}, ${p2.y})`, `and (${p3.x}, ${p3.y})`);
294
+ route.splice(i + 1, 2); // Remove p2 and p3
295
+ continue;
296
+ }
297
+ }
298
+ i++;
299
+ }
300
+ }
301
+
302
+ // Insert extension point at target - always extend away from node edge by fixed distance
218
303
  if (route.length > 0) {
219
- const extensionDistance = step + options.borderRadius;
304
+ const extensionDistance = options.extensionDistance;
220
305
  const lastPoint = route[route.length - 1];
221
306
 
222
307
  // Determine which edge the target anchor is on
@@ -265,20 +350,68 @@ function getManHattanPath(params) {
265
350
  }
266
351
  }
267
352
 
268
- // Add source and target anchors to route
269
- const fullRoute = [sourceAnchor, ...route, targetAnchor];
270
- console.log('[getManHattanPath] Full route:', fullRoute.map(p => `(${p.x}, ${p.y})`));
271
-
272
- // Snap to grid if enabled
273
- let finalRoute = fullRoute;
274
- if (options.snapToGrid) {
275
- // Use step size as grid size for snapping
276
- finalRoute = (0, _svg.snapPathToGrid)(fullRoute, options.step);
353
+ // Remove redundant points before target extension
354
+ // Similar logic for target side
355
+ if (route.length > 2) {
356
+ const lastIdx = route.length - 1;
357
+ const lastRoutePoint = route[lastIdx]; // Extension point
358
+ const secondLastPoint = route[lastIdx - 1]; // Corner point (if exists)
359
+ const thirdLastPoint = route[lastIdx - 2]; // Original A* point
360
+
361
+ // Check if the third-to-last point is redundant
362
+ const sameX = Math.abs(thirdLastPoint.x - targetAnchor.x) < tolerance;
363
+ const sameY = Math.abs(thirdLastPoint.y - targetAnchor.y) < tolerance;
364
+ if (sameX || sameY) {
365
+ if (route.length > 3) {
366
+ const fourthLastPoint = route[lastIdx - 3];
367
+ const fourthToThird = Math.abs(fourthLastPoint.x - thirdLastPoint.x) < tolerance || Math.abs(fourthLastPoint.y - thirdLastPoint.y) < tolerance;
368
+ const thirdToSecond = Math.abs(thirdLastPoint.x - secondLastPoint.x) < tolerance || Math.abs(thirdLastPoint.y - secondLastPoint.y) < tolerance;
369
+ if (fourthToThird && thirdToSecond) {
370
+ console.log('[getManHattanPath] Removing redundant point:', `(${thirdLastPoint.x}, ${thirdLastPoint.y})`);
371
+ route.splice(lastIdx - 2, 1); // Remove the third-to-last point
372
+ }
373
+ }
374
+ }
277
375
  }
278
376
 
279
- // Simplify path by removing collinear points
280
- finalRoute = (0, _svg.simplifyPath)(finalRoute);
281
- console.log('[getManHattanPath] Simplified route:', finalRoute.map(p => `(${p.x}, ${p.y})`));
377
+ // Additional optimization: Remove unnecessary zigzag patterns near target
378
+ // Check for patterns like: (1360, 16) -> (815, 16) -> (815, -134) -> (800, -134)
379
+ // where (815, 16) and (815, -134) form a zigzag at the target edge
380
+ let i = 0;
381
+ while (i < route.length - 2) {
382
+ const p1 = route[i];
383
+ const p2 = route[i + 1];
384
+ const p3 = route[i + 2];
385
+
386
+ // Check if p2 is on the target bbox edge
387
+ const p2OnTargetLeftEdge = Math.abs(p2.x - targetBBox.x) < tolerance;
388
+ const p2OnTargetRightEdge = Math.abs(p2.x - (targetBBox.x + targetBBox.width)) < tolerance;
389
+ const p2OnTargetTopEdge = Math.abs(p2.y - targetBBox.y) < tolerance;
390
+ const p2OnTargetBottomEdge = Math.abs(p2.y - (targetBBox.y + targetBBox.height)) < tolerance;
391
+ const p2OnTargetEdge = p2OnTargetLeftEdge || p2OnTargetRightEdge || p2OnTargetTopEdge || p2OnTargetBottomEdge;
392
+ if (p2OnTargetEdge) {
393
+ // Check if p1 -> p2 -> p3 forms a zigzag
394
+ const p1ToP2Horizontal = Math.abs(p1.y - p2.y) < tolerance;
395
+ const p2ToP3Vertical = Math.abs(p2.x - p3.x) < tolerance;
396
+ if (p1ToP2Horizontal && p2ToP3Vertical && i < route.length - 3) {
397
+ // We have horizontal -> vertical at target edge
398
+ // Check if we can skip p2 and p3
399
+ const p4 = route[i + 3];
400
+ const p3ToP4Horizontal = Math.abs(p3.y - p4.y) < tolerance;
401
+ if (p3ToP4Horizontal) {
402
+ // Pattern: horizontal -> vertical -> horizontal (zigzag)
403
+ console.log('[getManHattanPath] Removing zigzag at target edge:', `(${p2.x}, ${p2.y})`, `and (${p3.x}, ${p3.y})`);
404
+ route.splice(i + 1, 2); // Remove p2 and p3
405
+ continue;
406
+ }
407
+ }
408
+ }
409
+ i++;
410
+ }
411
+
412
+ // Add source and target anchors to route
413
+ const finalRoute = [sourceAnchor, ...route, targetAnchor];
414
+ console.log('[getManHattanPath] Final route:', finalRoute.map(p => `(${p.x}, ${p.y})`));
282
415
 
283
416
  // Convert to SVG path string
284
417
  return (0, _svg.pointsToPath)(finalRoute, options.precision, options.borderRadius);
@@ -22,7 +22,13 @@ export declare class ObstacleMap {
22
22
  private shrinkBBoxAroundAnchor;
23
23
  /**
24
24
  * Check if a point is accessible (not inside any obstacle)
25
+ * Uses binary search optimization: step -> step/2 -> step/4 -> ... -> 1px
25
26
  */
26
- isAccessible(point: Point): boolean;
27
+ isAccessible(point: Point, checkRadius?: number): boolean;
28
+ /**
29
+ * Check accessibility using binary search optimization
30
+ * Tries step -> step/2 -> step/4 -> ... -> 1px
31
+ */
32
+ private isAccessibleWithBinarySearch;
27
33
  }
28
34
  //# sourceMappingURL=ObstacleMap.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ObstacleMap.d.ts","sourceRoot":"","sources":["../../src/obstacle/ObstacleMap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAa,MAAM,aAAa,CAAA;AAC9C,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAG7D;;;GAGG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,GAAG,CAA0B;IACrC,OAAO,CAAC,YAAY,CAAC,CAAO;IAC5B,OAAO,CAAC,YAAY,CAAC,CAAO;gBAEhB,OAAO,EAAE,eAAe;IAMpC;;OAEG;IACH,KAAK,CACH,UAAU,EAAE,UAAU,EACtB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,KAAK,EACpB,YAAY,CAAC,EAAE,KAAK,GACnB,WAAW;IAiEd;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IA6D9B;;OAEG;IACH,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO;CA2CpC"}
1
+ {"version":3,"file":"ObstacleMap.d.ts","sourceRoot":"","sources":["../../src/obstacle/ObstacleMap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAa,MAAM,aAAa,CAAA;AAC9C,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAG7D;;;GAGG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,GAAG,CAA0B;IACrC,OAAO,CAAC,YAAY,CAAC,CAAO;IAC5B,OAAO,CAAC,YAAY,CAAC,CAAO;gBAEhB,OAAO,EAAE,eAAe;IAMpC;;OAEG;IACH,KAAK,CACH,UAAU,EAAE,UAAU,EACtB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,KAAK,EACpB,YAAY,CAAC,EAAE,KAAK,GACnB,WAAW;IAiEd;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IA6D9B;;;OAGG;IACH,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,WAAW,GAAE,MAAU,GAAG,OAAO;IAiD5D;;;OAGG;IACH,OAAO,CAAC,4BAA4B;CAqCrC"}
@@ -128,8 +128,9 @@ class ObstacleMap {
128
128
 
129
129
  /**
130
130
  * Check if a point is accessible (not inside any obstacle)
131
+ * Uses binary search optimization: step -> step/2 -> step/4 -> ... -> 1px
131
132
  */
132
- isAccessible(point) {
133
+ isAccessible(point, checkRadius = 0) {
133
134
  const key = point.clone().snapToGrid(this.mapGridSize).toString();
134
135
  const rects = this.map.get(key);
135
136
  if (!rects) {
@@ -152,6 +153,11 @@ class ObstacleMap {
152
153
  return true;
153
154
  }
154
155
  }
156
+
157
+ // If checkRadius is specified, use binary search to find accessible points
158
+ if (checkRadius > 0) {
159
+ return this.isAccessibleWithBinarySearch(point, checkRadius, rects);
160
+ }
155
161
  const accessible = rects.every(rect => !rect.containsPoint(point));
156
162
 
157
163
  // Debug: log points on the direct path
@@ -167,5 +173,51 @@ class ObstacleMap {
167
173
  }
168
174
  return accessible;
169
175
  }
176
+
177
+ /**
178
+ * Check accessibility using binary search optimization
179
+ * Tries step -> step/2 -> step/4 -> ... -> 1px
180
+ */
181
+ isAccessibleWithBinarySearch(point, maxRadius, rects) {
182
+ // First check the point itself
183
+ if (rects.every(rect => !rect.containsPoint(point))) {
184
+ return true;
185
+ }
186
+
187
+ // Binary search: start with step, then halve until we reach 1px
188
+ let radius = maxRadius;
189
+ const offsets = [{
190
+ dx: 1,
191
+ dy: 0
192
+ },
193
+ // right
194
+ {
195
+ dx: -1,
196
+ dy: 0
197
+ },
198
+ // left
199
+ {
200
+ dx: 0,
201
+ dy: 1
202
+ },
203
+ // down
204
+ {
205
+ dx: 0,
206
+ dy: -1
207
+ } // up
208
+ ];
209
+ while (radius >= 1) {
210
+ for (const offset of offsets) {
211
+ const testPoint = new _geometry.Point(point.x + offset.dx * radius, point.y + offset.dy * radius);
212
+ if (rects.every(rect => !rect.containsPoint(testPoint))) {
213
+ return true;
214
+ }
215
+ }
216
+
217
+ // Halve the radius for next iteration
218
+ radius = Math.floor(radius / 2);
219
+ }
220
+ return false;
221
+ }
170
222
  }
171
223
  exports.ObstacleMap = ObstacleMap;
@@ -3,7 +3,7 @@ import type { ManhattanRouterOptions } from './types';
3
3
  /**
4
4
  * Default configuration for Manhattan router
5
5
  */
6
- export declare const defaults: Required<Omit<ManhattanRouterOptions, 'fallbackRoute'>>;
6
+ export declare const defaults: Required<Omit<ManhattanRouterOptions, 'fallbackRoute' | 'sourcePosition' | 'targetPosition'>>;
7
7
  /**
8
8
  * Direction map - maps direction names to unit vectors
9
9
  */
@@ -1 +1 @@
1
- {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../src/options/defaults.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AACnC,OAAO,KAAK,EAAE,sBAAsB,EAAa,MAAM,SAAS,CAAA;AAEhE;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,sBAAsB,EAAE,eAAe,CAAC,CAkB5E,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;CAKxB,CAAA"}
1
+ {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../src/options/defaults.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AACnC,OAAO,KAAK,EAAE,sBAAsB,EAAa,MAAM,SAAS,CAAA;AAEhE;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,sBAAsB,EAAE,eAAe,GAAG,gBAAgB,GAAG,gBAAgB,CAAC,CAkBlH,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;CAKxB,CAAA"}
@@ -19,8 +19,8 @@ const defaults = exports.defaults = {
19
19
  excludeShapes: [],
20
20
  excludeTerminals: [],
21
21
  padding: 15,
22
- snapToGrid: true,
23
22
  borderRadius: 5,
23
+ extensionDistance: 20,
24
24
  penalties: {
25
25
  0: 0,
26
26
  45: 5,
@@ -1 +1 @@
1
- {"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../src/options/resolver.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAetE;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQpD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,sBAA2B,GAAG,eAAe,CA+DpF"}
1
+ {"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../src/options/resolver.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAetE;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQpD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,sBAA2B,GAAG,eAAe,CAiEpF"}
@@ -48,8 +48,8 @@ function resolveOptions(options = {}) {
48
48
  const excludeNodes = options.excludeNodes ?? _defaults.defaults.excludeNodes;
49
49
  const excludeShapes = options.excludeShapes ?? _defaults.defaults.excludeShapes;
50
50
  const excludeTerminals = options.excludeTerminals ?? _defaults.defaults.excludeTerminals;
51
- const snapToGrid = options.snapToGrid ?? _defaults.defaults.snapToGrid;
52
51
  const borderRadius = options.borderRadius ?? _defaults.defaults.borderRadius;
52
+ const extensionDistance = options.extensionDistance ?? _defaults.defaults.extensionDistance;
53
53
  const penalties = options.penalties ?? _defaults.defaults.penalties;
54
54
  const fallbackRoute = options.fallbackRoute;
55
55
 
@@ -106,8 +106,10 @@ function resolveOptions(options = {}) {
106
106
  excludeShapes,
107
107
  excludeTerminals,
108
108
  paddingBox,
109
- snapToGrid,
110
109
  borderRadius,
110
+ extensionDistance,
111
+ sourcePosition: options.sourcePosition,
112
+ targetPosition: options.targetPosition,
111
113
  directionMap: _defaults.directionMap,
112
114
  directions,
113
115
  penalties,
@@ -89,17 +89,28 @@ export interface ManhattanRouterOptions {
89
89
  bottom: number;
90
90
  left: number;
91
91
  };
92
- /**
93
- * Whether the calculation results are aligned with the grid
94
- * @default true
95
- */
96
- snapToGrid?: boolean;
97
92
  /**
98
93
  * Border radius for rounded corners at path turns (in pixels)
99
94
  * Set to 0 for sharp corners
100
95
  * @default 5
101
96
  */
102
97
  borderRadius?: number;
98
+ /**
99
+ * Extension distance from node edge for path start/end points (in pixels)
100
+ * This controls how far the path extends away from the node before turning
101
+ * @default 20
102
+ */
103
+ extensionDistance?: number;
104
+ /**
105
+ * Source position (from ReactFlow)
106
+ * Used for smart point generation
107
+ */
108
+ sourcePosition?: string;
109
+ /**
110
+ * Target position (from ReactFlow)
111
+ * Used for smart point generation
112
+ */
113
+ targetPosition?: string;
103
114
  /**
104
115
  * A penalty received for direction change
105
116
  */
@@ -130,8 +141,10 @@ export interface ResolvedOptions {
130
141
  width: number;
131
142
  height: number;
132
143
  };
133
- snapToGrid: boolean;
134
144
  borderRadius: number;
145
+ extensionDistance: number;
146
+ sourcePosition?: string;
147
+ targetPosition?: string;
135
148
  directionMap: {
136
149
  top: Point;
137
150
  right: Point;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/options/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/options/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"findRoute.d.ts","sourceRoot":"","sources":["../../src/pathfinder/findRoute.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAe9C;;GAEG;AACH,wBAAgB,SAAS,CACvB,UAAU,EAAE,SAAS,EACrB,UAAU,EAAE,SAAS,EACrB,YAAY,EAAE,KAAK,EACnB,YAAY,EAAE,KAAK,EACnB,GAAG,EAAE,WAAW,EAChB,OAAO,EAAE,eAAe,GACvB,KAAK,EAAE,GAAG,IAAI,CAsNhB"}
1
+ {"version":3,"file":"findRoute.d.ts","sourceRoot":"","sources":["../../src/pathfinder/findRoute.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAyN9C;;GAEG;AACH,wBAAgB,SAAS,CACvB,UAAU,EAAE,SAAS,EACrB,UAAU,EAAE,SAAS,EACrB,YAAY,EAAE,KAAK,EACnB,YAAY,EAAE,KAAK,EACnB,GAAG,EAAE,WAAW,EAChB,OAAO,EAAE,eAAe,GACvB,KAAK,EAAE,GAAG,IAAI,CAqThB"}