@rxflow/manhattan 0.0.2-alpha.8 → 0.0.3

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 (116) hide show
  1. package/README.md +214 -10
  2. package/cjs/getManHattanPath.d.ts.map +1 -1
  3. package/cjs/getManHattanPath.js +77 -46
  4. package/cjs/obstacle/ObstacleMap.d.ts +41 -9
  5. package/cjs/obstacle/ObstacleMap.d.ts.map +1 -1
  6. package/cjs/obstacle/ObstacleMap.js +201 -96
  7. package/cjs/obstacle/QuadTree.d.ts +119 -0
  8. package/cjs/obstacle/QuadTree.d.ts.map +1 -0
  9. package/cjs/obstacle/QuadTree.js +334 -0
  10. package/cjs/options/defaults.d.ts +1 -1
  11. package/cjs/options/defaults.d.ts.map +1 -1
  12. package/cjs/options/resolver.d.ts.map +1 -1
  13. package/cjs/options/resolver.js +145 -17
  14. package/cjs/options/types.d.ts +41 -0
  15. package/cjs/options/types.d.ts.map +1 -1
  16. package/cjs/pathfinder/PathCache.d.ts +92 -0
  17. package/cjs/pathfinder/PathCache.d.ts.map +1 -0
  18. package/cjs/pathfinder/PathCache.js +249 -0
  19. package/cjs/pathfinder/findRoute.d.ts.map +1 -1
  20. package/cjs/pathfinder/findRoute.js +96 -31
  21. package/cjs/pathfinder/index.d.ts +1 -0
  22. package/cjs/pathfinder/index.d.ts.map +1 -1
  23. package/cjs/pathfinder/index.js +26 -1
  24. package/cjs/svg/pathConverter.d.ts +13 -0
  25. package/cjs/svg/pathConverter.d.ts.map +1 -1
  26. package/cjs/svg/pathConverter.js +170 -1
  27. package/cjs/utils/AdaptiveStepCalculator.d.ts +90 -0
  28. package/cjs/utils/AdaptiveStepCalculator.d.ts.map +1 -0
  29. package/cjs/utils/AdaptiveStepCalculator.js +224 -0
  30. package/cjs/utils/ErrorRecovery.d.ts +182 -0
  31. package/cjs/utils/ErrorRecovery.d.ts.map +1 -0
  32. package/cjs/utils/ErrorRecovery.js +413 -0
  33. package/cjs/utils/GlobalGrid.d.ts +99 -0
  34. package/cjs/utils/GlobalGrid.d.ts.map +1 -0
  35. package/cjs/utils/GlobalGrid.js +224 -0
  36. package/cjs/utils/PerformanceMonitor.d.ts +139 -0
  37. package/cjs/utils/PerformanceMonitor.d.ts.map +1 -0
  38. package/cjs/utils/PerformanceMonitor.js +305 -0
  39. package/cjs/utils/getAnchorPoints.d.ts.map +1 -1
  40. package/cjs/utils/getAnchorPoints.js +0 -4
  41. package/cjs/utils/grid.d.ts +15 -0
  42. package/cjs/utils/grid.d.ts.map +1 -1
  43. package/cjs/utils/grid.js +19 -12
  44. package/cjs/utils/heuristics.d.ts +61 -0
  45. package/cjs/utils/heuristics.d.ts.map +1 -0
  46. package/cjs/utils/heuristics.js +141 -0
  47. package/cjs/utils/index.d.ts +6 -0
  48. package/cjs/utils/index.d.ts.map +1 -1
  49. package/cjs/utils/index.js +66 -0
  50. package/cjs/utils/pathProcessing.d.ts +45 -0
  51. package/cjs/utils/pathProcessing.d.ts.map +1 -0
  52. package/cjs/utils/pathProcessing.js +270 -0
  53. package/cjs/utils/pathValidation.d.ts.map +1 -1
  54. package/cjs/utils/pathValidation.js +0 -1
  55. package/cjs/utils/rect.d.ts.map +1 -1
  56. package/cjs/utils/rect.js +7 -0
  57. package/cjs/utils/route.d.ts.map +1 -1
  58. package/cjs/utils/route.js +18 -2
  59. package/esm/getManHattanPath.d.ts.map +1 -1
  60. package/esm/getManHattanPath.js +92 -69
  61. package/esm/obstacle/ObstacleMap.d.ts +41 -9
  62. package/esm/obstacle/ObstacleMap.d.ts.map +1 -1
  63. package/esm/obstacle/ObstacleMap.js +218 -99
  64. package/esm/obstacle/QuadTree.d.ts +119 -0
  65. package/esm/obstacle/QuadTree.d.ts.map +1 -0
  66. package/esm/obstacle/QuadTree.js +488 -0
  67. package/esm/options/defaults.d.ts +1 -1
  68. package/esm/options/defaults.d.ts.map +1 -1
  69. package/esm/options/resolver.d.ts.map +1 -1
  70. package/esm/options/resolver.js +147 -18
  71. package/esm/options/types.d.ts +41 -0
  72. package/esm/options/types.d.ts.map +1 -1
  73. package/esm/pathfinder/PathCache.d.ts +92 -0
  74. package/esm/pathfinder/PathCache.d.ts.map +1 -0
  75. package/esm/pathfinder/PathCache.js +278 -0
  76. package/esm/pathfinder/findRoute.d.ts.map +1 -1
  77. package/esm/pathfinder/findRoute.js +98 -44
  78. package/esm/pathfinder/index.d.ts +1 -0
  79. package/esm/pathfinder/index.d.ts.map +1 -1
  80. package/esm/pathfinder/index.js +2 -1
  81. package/esm/svg/pathConverter.d.ts +13 -0
  82. package/esm/svg/pathConverter.d.ts.map +1 -1
  83. package/esm/svg/pathConverter.js +170 -1
  84. package/esm/utils/AdaptiveStepCalculator.d.ts +90 -0
  85. package/esm/utils/AdaptiveStepCalculator.d.ts.map +1 -0
  86. package/esm/utils/AdaptiveStepCalculator.js +252 -0
  87. package/esm/utils/ErrorRecovery.d.ts +182 -0
  88. package/esm/utils/ErrorRecovery.d.ts.map +1 -0
  89. package/esm/utils/ErrorRecovery.js +499 -0
  90. package/esm/utils/GlobalGrid.d.ts +99 -0
  91. package/esm/utils/GlobalGrid.d.ts.map +1 -0
  92. package/esm/utils/GlobalGrid.js +259 -0
  93. package/esm/utils/PerformanceMonitor.d.ts +139 -0
  94. package/esm/utils/PerformanceMonitor.d.ts.map +1 -0
  95. package/esm/utils/PerformanceMonitor.js +360 -0
  96. package/esm/utils/getAnchorPoints.d.ts.map +1 -1
  97. package/esm/utils/getAnchorPoints.js +0 -4
  98. package/esm/utils/grid.d.ts +15 -0
  99. package/esm/utils/grid.d.ts.map +1 -1
  100. package/esm/utils/grid.js +18 -13
  101. package/esm/utils/heuristics.d.ts +61 -0
  102. package/esm/utils/heuristics.d.ts.map +1 -0
  103. package/esm/utils/heuristics.js +144 -0
  104. package/esm/utils/index.d.ts +6 -0
  105. package/esm/utils/index.d.ts.map +1 -1
  106. package/esm/utils/index.js +7 -1
  107. package/esm/utils/pathProcessing.d.ts +45 -0
  108. package/esm/utils/pathProcessing.d.ts.map +1 -0
  109. package/esm/utils/pathProcessing.js +270 -0
  110. package/esm/utils/pathValidation.d.ts.map +1 -1
  111. package/esm/utils/pathValidation.js +0 -1
  112. package/esm/utils/rect.d.ts.map +1 -1
  113. package/esm/utils/rect.js +11 -4
  114. package/esm/utils/route.d.ts.map +1 -1
  115. package/esm/utils/route.js +18 -2
  116. package/package.json +10 -2
package/README.md CHANGED
@@ -2,33 +2,237 @@
2
2
 
3
3
  Manhattan 路由算法,用于 ReactFlow 生成正交路径,支持障碍物避让。
4
4
 
5
+ ## 特性
6
+
7
+ - **A* 寻路算法** - 高效的路径搜索,支持早期终止优化
8
+ - **正交路径生成** - 生成符合 Manhattan 风格的直角路径
9
+ - **障碍物避让** - 自动绕过节点障碍物
10
+ - **四叉树优化** - 使用 QuadTree 加速空间查询
11
+ - **自适应步长** - 根据节点密度和距离自动调整网格步长
12
+ - **路径缓存** - LRU 缓存机制提升重复查询性能
13
+ - **全局网格对齐** - 确保所有路径点对齐到全局网格
14
+ - **圆角支持** - 可配置的路径转角圆角半径
15
+ - **性能监控** - 内置性能指标收集和日志
16
+
5
17
  ## 安装
6
18
 
7
19
  ```bash
8
20
  npm install @rxflow/manhattan
9
21
  # or
10
22
  pnpm add @rxflow/manhattan
23
+ yarn add @rxflow/manhattan
11
24
  ```
12
25
 
13
- ## 使用
26
+ ## 基本使用
14
27
 
15
28
  ```tsx
16
29
  import { getManHattanPath } from '@rxflow/manhattan';
30
+ import { useReactFlow } from '@xyflow/react';
31
+
32
+ function CustomEdge({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, source, target }) {
33
+ const { getNodes } = useReactFlow();
34
+ const nodeLookup = new Map(getNodes().map(n => [n.id, n]));
35
+
36
+ const path = getManHattanPath({
37
+ sourceNodeId: source,
38
+ targetNodeId: target,
39
+ sourceX,
40
+ sourceY,
41
+ targetX,
42
+ targetY,
43
+ sourcePosition,
44
+ targetPosition,
45
+ nodeLookup,
46
+ options: {
47
+ step: 10,
48
+ borderRadius: 5,
49
+ }
50
+ });
51
+
52
+ return <path d={path} />;
53
+ }
54
+ ```
55
+
56
+ ## API
57
+
58
+ ### getManHattanPath(params)
59
+
60
+ 生成 Manhattan 风格的 SVG 路径字符串。
61
+
62
+ #### 参数
63
+
64
+ | 参数 | 类型 | 必填 | 描述 |
65
+ |------|------|------|------|
66
+ | sourceNodeId | string | ✓ | 源节点 ID |
67
+ | targetNodeId | string | ✓ | 目标节点 ID |
68
+ | sourceX | number | ✓ | 源锚点 X 坐标 |
69
+ | sourceY | number | ✓ | 源锚点 Y 坐标 |
70
+ | targetX | number | ✓ | 目标锚点 X 坐标 |
71
+ | targetY | number | ✓ | 目标锚点 Y 坐标 |
72
+ | sourcePosition | Position | ✓ | 源锚点位置 (top/right/bottom/left) |
73
+ | targetPosition | Position | ✓ | 目标锚点位置 |
74
+ | nodeLookup | NodeLookup | ✓ | ReactFlow 节点查找 Map |
75
+ | options | ManhattanRouterOptions | - | 路由配置选项 |
76
+
77
+ #### 返回值
78
+
79
+ 返回 SVG 路径字符串,可直接用于 `<path d={...} />` 元素。
17
80
 
81
+ ### ManhattanRouterOptions
82
+
83
+ | 选项 | 类型 | 默认值 | 描述 |
84
+ |------|------|--------|------|
85
+ | step | number | 10 | 网格步长(像素) |
86
+ | maxLoopCount | number | 2000 | 最大迭代次数 |
87
+ | precision | number | 1 | 坐标精度(小数位数) |
88
+ | borderRadius | number | 5 | 转角圆角半径 |
89
+ | extensionDistance | number | 20 | 路径延伸距离 |
90
+ | padding | number \| object | 20 | 节点边界框内边距 |
91
+ | startDirections | Direction[] | ['top', 'right', 'bottom', 'left'] | 允许的起始方向 |
92
+ | endDirections | Direction[] | ['top', 'right', 'bottom', 'left'] | 允许的结束方向 |
93
+ | excludeNodes | string[] | [] | 排除的节点 ID |
94
+ | excludeShapes | string[] | [] | 排除的节点类型 |
95
+ | adaptiveStep | AdaptiveStepConfig | - | 自适应步长配置 |
96
+ | performance | PerformanceConfig | - | 性能优化配置 |
97
+ | debug | DebugConfig | - | 调试配置 |
98
+
99
+ ### AdaptiveStepConfig
100
+
101
+ ```typescript
102
+ interface AdaptiveStepConfig {
103
+ enabled: boolean; // 是否启用自适应步长
104
+ minStep: number; // 最小步长 (默认: 5)
105
+ maxStep: number; // 最大步长 (默认: 50)
106
+ densityThreshold: number; // 密度阈值 (默认: 0.3)
107
+ distanceThreshold: number; // 距离阈值 (默认: 500)
108
+ }
109
+ ```
110
+
111
+ ### PerformanceConfig
112
+
113
+ ```typescript
114
+ interface PerformanceConfig {
115
+ enableCache: boolean; // 启用路径缓存
116
+ cacheSize: number; // 缓存大小 (默认: 100)
117
+ enableQuadTree: boolean; // 启用四叉树优化
118
+ earlyTermination: boolean; // 启用早期终止
119
+ }
120
+ ```
121
+
122
+ ### DebugConfig
123
+
124
+ ```typescript
125
+ interface DebugConfig {
126
+ enableLogging: boolean; // 启用日志
127
+ enableMetrics: boolean; // 启用性能指标
128
+ logLevel: 'error' | 'warn' | 'info' | 'debug';
129
+ }
130
+ ```
131
+
132
+ ## 高级用法
133
+
134
+ ### 自定义障碍物排除
135
+
136
+ ```typescript
18
137
  const path = getManHattanPath({
19
- sourceNodeId: 'node-1',
20
- targetNodeId: 'node-2',
21
- nodeLookup,
22
- // ... other options
138
+ // ...基本参数
139
+ options: {
140
+ excludeNodes: ['node-3', 'node-4'], // 排除特定节点
141
+ excludeShapes: ['group'], // 排除特定类型
142
+ excludeTerminals: ['source'], // 排除源/目标节点
143
+ }
23
144
  });
24
145
  ```
25
146
 
26
- ## 特性
147
+ ### 性能优化配置
27
148
 
28
- - A* 寻路算法
29
- - 正交路径生成
30
- - 障碍物避让
31
- - 高性能计算
149
+ ```typescript
150
+ const path = getManHattanPath({
151
+ // ...基本参数
152
+ options: {
153
+ performance: {
154
+ enableCache: true,
155
+ cacheSize: 200,
156
+ enableQuadTree: true,
157
+ earlyTermination: true,
158
+ },
159
+ adaptiveStep: {
160
+ enabled: true,
161
+ minStep: 5,
162
+ maxStep: 30,
163
+ }
164
+ }
165
+ });
166
+ ```
167
+
168
+ ### 调试模式
169
+
170
+ ```typescript
171
+ const path = getManHattanPath({
172
+ // ...基本参数
173
+ options: {
174
+ debug: {
175
+ enableLogging: true,
176
+ enableMetrics: true,
177
+ logLevel: 'debug',
178
+ }
179
+ }
180
+ });
181
+ ```
182
+
183
+ ## 工具类
184
+
185
+ ### GlobalGrid
186
+
187
+ 全局网格对齐工具,确保所有路径点对齐到统一网格。
188
+
189
+ ```typescript
190
+ import { GlobalGrid } from '@rxflow/manhattan';
191
+
192
+ const grid = new GlobalGrid(10);
193
+ const snapped = grid.snapToGrid({ x: 15, y: 23 }); // { x: 20, y: 20 }
194
+ ```
195
+
196
+ ### ObstacleMap
197
+
198
+ 障碍物地图,用于空间查询和碰撞检测。
199
+
200
+ ```typescript
201
+ import { ObstacleMap } from '@rxflow/manhattan';
202
+
203
+ const map = new ObstacleMap(options);
204
+ map.build(nodeLookup, sourceId, targetId, sourceAnchor, targetAnchor);
205
+ const accessible = map.isAccessible(point);
206
+ ```
207
+
208
+ ### ErrorRecovery
209
+
210
+ 错误恢复工具,提供回退路径生成。
211
+
212
+ ```typescript
213
+ import { ErrorRecovery } from '@rxflow/manhattan';
214
+
215
+ const fallback = ErrorRecovery.generateFallbackPath(start, end);
216
+ const validated = ErrorRecovery.validateAndFixConfig(config);
217
+ ```
218
+
219
+ ## 架构
220
+
221
+ ```
222
+ @rxflow/manhattan
223
+ ├── getManHattanPath.ts # 主入口函数
224
+ ├── geometry/ # 几何类 (Point, Rectangle)
225
+ ├── obstacle/ # 障碍物处理 (ObstacleMap, QuadTree)
226
+ ├── pathfinder/ # A* 寻路算法
227
+ ├── options/ # 配置解析
228
+ ├── svg/ # SVG 路径转换
229
+ └── utils/ # 工具函数
230
+ ├── GlobalGrid.ts # 全局网格
231
+ ├── AdaptiveStepCalculator.ts # 自适应步长
232
+ ├── PerformanceMonitor.ts # 性能监控
233
+ ├── ErrorRecovery.ts # 错误恢复
234
+ └── heuristics.ts # 启发式函数
235
+ ```
32
236
 
33
237
  ## License
34
238
 
@@ -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,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"}
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,CAydvE"}
@@ -69,10 +69,12 @@ function getManHattanPath(params) {
69
69
  const targetNode = nodeLookup.get(targetNodeId);
70
70
  if (!sourceNode || !targetNode) {
71
71
  // Fallback to simple straight line if nodes not found
72
- console.warn('Source or target node not found in nodeLookup');
72
+ console.warn('[getManHattanPath] Source or target node not found in nodeLookup');
73
73
  const start = new _geometry.Point(sourceX, sourceY);
74
74
  const end = new _geometry.Point(targetX, targetY);
75
- return (0, _svg.pointsToPath)([start, end], options.precision);
75
+ // Use ErrorRecovery to generate a proper fallback path
76
+ const fallbackPath = _utils.ErrorRecovery.generateFallbackPath(start, end);
77
+ return (0, _svg.pointsToPath)(fallbackPath, options.precision, options.borderRadius);
76
78
  }
77
79
 
78
80
  // Get node dimensions using ReactFlow's priority logic
@@ -107,10 +109,8 @@ function getManHattanPath(params) {
107
109
 
108
110
  // Check if smooth step path intersects with any obstacles
109
111
  if (smoothStepPoints.length > 0 && !(0, _utils.pathIntersectsObstacles)(smoothStepPoints, nodeLookup)) {
110
- console.log('[getManHattanPath] Using ReactFlow getSmoothStepPath (no obstacles)');
111
112
  return smoothStepPath;
112
113
  }
113
- console.log('[getManHattanPath] SmoothStepPath intersects obstacles, using Manhattan routing');
114
114
 
115
115
  // Build obstacle map with anchor information
116
116
  const obstacleMap = new _obstacle.ObstacleMap(options).build(nodeLookup, sourceNodeId, targetNodeId, sourceAnchor, targetAnchor);
@@ -118,23 +118,80 @@ function getManHattanPath(params) {
118
118
  // Find route
119
119
  let route = (0, _pathfinder.findRoute)(sourceBBox, targetBBox, sourceAnchor, targetAnchor, obstacleMap, options);
120
120
 
121
- // Fallback to straight line if no route found
121
+ // Fallback to Z-shaped path if no route found
122
122
  if (!route) {
123
- console.warn('Unable to find Manhattan route, using straight line fallback');
124
- route = [sourceAnchor, targetAnchor];
123
+ console.warn('[getManHattanPath] Unable to find Manhattan route, using fallback path');
124
+ route = _utils.ErrorRecovery.generateFallbackPath(sourceAnchor, targetAnchor);
125
125
  }
126
- console.log('[getManHattanPath] Route from findRoute:', route.map(p => `(${p.x}, ${p.y})`));
127
- console.log('[getManHattanPath] Source anchor:', `(${sourceAnchor.x}, ${sourceAnchor.y})`);
128
- console.log('[getManHattanPath] Target anchor:', `(${targetAnchor.x}, ${targetAnchor.y})`);
129
126
 
130
127
  // If using smart point generation (sourcePosition/targetPosition specified),
131
128
  // the route already contains the correct extension points, so skip manual processing
132
129
  const useSmartPoints = sourcePosition || targetPosition;
133
130
  if (useSmartPoints) {
134
- console.log('[getManHattanPath] Using smart points, skipping manual extension point processing');
131
+ // Post-process route to fix X coordinates for horizontal anchors
132
+ // This ensures the extension distance is fixed and not affected by grid alignment
133
+ const isSourceHorizontal = sourcePosition === _react.Position.Left || sourcePosition === _react.Position.Right;
134
+ const isTargetHorizontal = targetPosition === _react.Position.Left || targetPosition === _react.Position.Right;
135
+ if (isSourceHorizontal && isTargetHorizontal && route.length > 0) {
136
+ // Calculate expected extension X coordinates
137
+ const sourceExtensionX = sourcePosition === _react.Position.Right ? sourceAnchor.x + options.extensionDistance : sourceAnchor.x - options.extensionDistance;
138
+ const targetExtensionX = targetPosition === _react.Position.Left ? targetAnchor.x - options.extensionDistance : targetAnchor.x + options.extensionDistance;
139
+
140
+ // Find the horizontal segment (where Y stays constant but X changes significantly)
141
+ // This divides the path into source-side and target-side
142
+ let horizontalSegmentStart = -1;
143
+ let horizontalSegmentEnd = -1;
144
+ for (let i = 0; i < route.length - 1; i++) {
145
+ const curr = route[i];
146
+ const next = route[i + 1];
147
+ const isHorizontalMove = Math.abs(curr.y - next.y) < 1 && Math.abs(curr.x - next.x) > options.step;
148
+ if (isHorizontalMove) {
149
+ horizontalSegmentStart = i;
150
+ horizontalSegmentEnd = i + 1;
151
+ break;
152
+ }
153
+ }
154
+
155
+ // Fix route points
156
+ for (let i = 0; i < route.length; i++) {
157
+ const point = route[i];
158
+ if (horizontalSegmentStart >= 0) {
159
+ if (i <= horizontalSegmentStart) {
160
+ // Source side - all vertical segment points should use sourceExtensionX
161
+ route[i] = new _geometry.Point(sourceExtensionX, point.y);
162
+ } else if (i >= horizontalSegmentEnd) {
163
+ // Target side - all vertical segment points should use targetExtensionX
164
+ route[i] = new _geometry.Point(targetExtensionX, point.y);
165
+ }
166
+ }
167
+ }
168
+
169
+ // Remove redundant points (consecutive points on same line)
170
+ const optimized = [];
171
+ for (let i = 0; i < route.length; i++) {
172
+ const point = route[i];
173
+ if (optimized.length < 2) {
174
+ optimized.push(point);
175
+ } else {
176
+ const prev = optimized[optimized.length - 1];
177
+ const prevPrev = optimized[optimized.length - 2];
178
+
179
+ // Check if prev is on the same line as prevPrev and point
180
+ const sameX = Math.abs(prevPrev.x - prev.x) < 1 && Math.abs(prev.x - point.x) < 1;
181
+ const sameY = Math.abs(prevPrev.y - prev.y) < 1 && Math.abs(prev.y - point.y) < 1;
182
+ if (sameX || sameY) {
183
+ // prev is redundant, replace it with current point
184
+ optimized[optimized.length - 1] = point;
185
+ } else {
186
+ optimized.push(point);
187
+ }
188
+ }
189
+ }
190
+ route = optimized;
191
+ }
192
+
135
193
  // Add source and target anchors to route
136
194
  const finalRoute = [sourceAnchor, ...route, targetAnchor];
137
- console.log('[getManHattanPath] Final route:', finalRoute.map(p => `(${p.x}, ${p.y})`));
138
195
  return (0, _svg.pointsToPath)(finalRoute, options.precision, options.borderRadius);
139
196
  }
140
197
 
@@ -157,7 +214,6 @@ function getManHattanPath(params) {
157
214
  // This is likely an extension point, remove it
158
215
  if (onRight && firstPoint.x > sourceAnchor.x || onLeft && firstPoint.x < sourceAnchor.x || onBottom && firstPoint.y > sourceAnchor.y || onTop && firstPoint.y < sourceAnchor.y) {
159
216
  route.shift();
160
- console.log('[getManHattanPath] Removed extension point from route start');
161
217
  }
162
218
  }
163
219
  }
@@ -176,7 +232,6 @@ function getManHattanPath(params) {
176
232
  // This is likely an extension point, remove it
177
233
  if (onLeft && lastPoint.x < targetAnchor.x || onRight && lastPoint.x > targetAnchor.x || onTop && lastPoint.y < targetAnchor.y || onBottom && lastPoint.y > targetAnchor.y) {
178
234
  route.pop();
179
- console.log('[getManHattanPath] Removed extension point from route end');
180
235
  }
181
236
  }
182
237
  }
@@ -202,7 +257,6 @@ function getManHattanPath(params) {
202
257
  route.unshift(new _geometry.Point(extendX, firstPoint.y)); // Corner point
203
258
  }
204
259
  route.unshift(extensionPoint); // Extension point (fixed distance)
205
- console.log('[getManHattanPath] Inserted source extension (right):', `(${extendX}, ${sourceAnchor.y})`);
206
260
  } else if (onLeft) {
207
261
  // Anchor on left edge - extend left by step + borderRadius
208
262
  const extendX = sourceAnchor.x - extensionDistance;
@@ -211,7 +265,6 @@ function getManHattanPath(params) {
211
265
  route.unshift(new _geometry.Point(extendX, firstPoint.y));
212
266
  }
213
267
  route.unshift(extensionPoint);
214
- console.log('[getManHattanPath] Inserted source extension (left):', `(${extendX}, ${sourceAnchor.y})`);
215
268
  } else if (onBottom) {
216
269
  // Anchor on bottom edge - extend down by step + borderRadius
217
270
  const extendY = sourceAnchor.y + extensionDistance;
@@ -220,7 +273,6 @@ function getManHattanPath(params) {
220
273
  route.unshift(new _geometry.Point(firstPoint.x, extendY));
221
274
  }
222
275
  route.unshift(extensionPoint);
223
- console.log('[getManHattanPath] Inserted source extension (down):', `(${sourceAnchor.x}, ${extendY})`);
224
276
  } else if (onTop) {
225
277
  // Anchor on top edge - extend up by step + borderRadius
226
278
  const extendY = sourceAnchor.y - extensionDistance;
@@ -229,33 +281,25 @@ function getManHattanPath(params) {
229
281
  route.unshift(new _geometry.Point(firstPoint.x, extendY));
230
282
  }
231
283
  route.unshift(extensionPoint);
232
- console.log('[getManHattanPath] Inserted source extension (up):', `(${sourceAnchor.x}, ${extendY})`);
233
284
  }
234
285
  }
235
286
 
236
287
  // Remove redundant points after source extension
237
288
  // 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)
289
+ if (route.length > 3) {
241
290
  const thirdRoutePoint = route[2]; // Original A* point
242
-
243
291
  // Check if the third point (original A* point) is redundant
244
292
  // It's redundant if it's on the same line as the corner point and can be skipped
245
293
  const sameX = Math.abs(thirdRoutePoint.x - sourceAnchor.x) < tolerance;
246
294
  const sameY = Math.abs(thirdRoutePoint.y - sourceAnchor.y) < tolerance;
247
295
  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
- }
296
+ const secondRoutePoint = route[1]; // Corner point (if exists)
297
+ const fourthPoint = route[3];
298
+ // If corner point and fourth point form a straight line, remove the third point
299
+ const cornerToThird = Math.abs(secondRoutePoint.x - thirdRoutePoint.x) < tolerance || Math.abs(secondRoutePoint.y - thirdRoutePoint.y) < tolerance;
300
+ const thirdToFourth = Math.abs(thirdRoutePoint.x - fourthPoint.x) < tolerance || Math.abs(thirdRoutePoint.y - fourthPoint.y) < tolerance;
301
+ if (cornerToThird && thirdToFourth) {
302
+ route.splice(2, 1); // Remove the third point
259
303
  }
260
304
  }
261
305
  }
@@ -265,8 +309,6 @@ function getManHattanPath(params) {
265
309
  // where the middle segment goes to target edge and then moves along it
266
310
  // Use target bbox with padding (same as ObstacleMap)
267
311
  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
312
  if (route.length >= 3) {
271
313
  let i = 0;
272
314
  while (i < route.length - 2) {
@@ -280,17 +322,14 @@ function getManHattanPath(params) {
280
322
  const p2OnTargetTopEdge = Math.abs(p2.y - targetBBoxWithPadding.y) < tolerance;
281
323
  const p2OnTargetBottomEdge = Math.abs(p2.y - (targetBBoxWithPadding.y + targetBBoxWithPadding.height)) < tolerance;
282
324
  const p2OnTargetEdge = p2OnTargetLeftEdge || p2OnTargetRightEdge || p2OnTargetTopEdge || p2OnTargetBottomEdge;
283
- console.log(`[getManHattanPath] Checking i=${i}: p2=(${p2.x}, ${p2.y}), onEdge=${p2OnTargetEdge}`);
284
325
  if (p2OnTargetEdge) {
285
326
  // Check if p1 -> p2 -> p3 forms a zigzag
286
327
  const p1ToP2Horizontal = Math.abs(p1.y - p2.y) < tolerance;
287
328
  const p2ToP3Vertical = Math.abs(p2.x - p3.x) < tolerance;
288
329
  const p1ToP2Vertical = Math.abs(p1.x - p2.x) < tolerance;
289
330
  const p2ToP3Horizontal = Math.abs(p2.y - p3.y) < tolerance;
290
- console.log(`[getManHattanPath] Zigzag pattern: H->V=${p1ToP2Horizontal && p2ToP3Vertical}, V->H=${p1ToP2Vertical && p2ToP3Horizontal}`);
291
331
  if (p1ToP2Horizontal && p2ToP3Vertical || p1ToP2Vertical && p2ToP3Horizontal) {
292
332
  // 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
333
  route.splice(i + 1, 2); // Remove p2 and p3
295
334
  continue;
296
335
  }
@@ -319,7 +358,6 @@ function getManHattanPath(params) {
319
358
  route.push(new _geometry.Point(extendX, lastPoint.y)); // Corner point
320
359
  }
321
360
  route.push(extensionPoint); // Extension point (fixed distance)
322
- console.log('[getManHattanPath] Inserted target extension (left):', `(${extendX}, ${targetAnchor.y})`);
323
361
  } else if (onRight) {
324
362
  // Anchor on right edge - extend right by step + borderRadius
325
363
  const extendX = targetAnchor.x + extensionDistance;
@@ -328,7 +366,6 @@ function getManHattanPath(params) {
328
366
  route.push(new _geometry.Point(extendX, lastPoint.y));
329
367
  }
330
368
  route.push(extensionPoint);
331
- console.log('[getManHattanPath] Inserted target extension (right):', `(${extendX}, ${targetAnchor.y})`);
332
369
  } else if (onTop) {
333
370
  // Anchor on top edge - extend up by step + borderRadius
334
371
  const extendY = targetAnchor.y - extensionDistance;
@@ -337,7 +374,6 @@ function getManHattanPath(params) {
337
374
  route.push(new _geometry.Point(lastPoint.x, extendY));
338
375
  }
339
376
  route.push(extensionPoint);
340
- console.log('[getManHattanPath] Inserted target extension (up):', `(${targetAnchor.x}, ${extendY})`);
341
377
  } else if (onBottom) {
342
378
  // Anchor on bottom edge - extend down by step + borderRadius
343
379
  const extendY = targetAnchor.y + extensionDistance;
@@ -346,7 +382,6 @@ function getManHattanPath(params) {
346
382
  route.push(new _geometry.Point(lastPoint.x, extendY));
347
383
  }
348
384
  route.push(extensionPoint);
349
- console.log('[getManHattanPath] Inserted target extension (down):', `(${targetAnchor.x}, ${extendY})`);
350
385
  }
351
386
  }
352
387
 
@@ -354,7 +389,6 @@ function getManHattanPath(params) {
354
389
  // Similar logic for target side
355
390
  if (route.length > 2) {
356
391
  const lastIdx = route.length - 1;
357
- const lastRoutePoint = route[lastIdx]; // Extension point
358
392
  const secondLastPoint = route[lastIdx - 1]; // Corner point (if exists)
359
393
  const thirdLastPoint = route[lastIdx - 2]; // Original A* point
360
394
 
@@ -367,7 +401,6 @@ function getManHattanPath(params) {
367
401
  const fourthToThird = Math.abs(fourthLastPoint.x - thirdLastPoint.x) < tolerance || Math.abs(fourthLastPoint.y - thirdLastPoint.y) < tolerance;
368
402
  const thirdToSecond = Math.abs(thirdLastPoint.x - secondLastPoint.x) < tolerance || Math.abs(thirdLastPoint.y - secondLastPoint.y) < tolerance;
369
403
  if (fourthToThird && thirdToSecond) {
370
- console.log('[getManHattanPath] Removing redundant point:', `(${thirdLastPoint.x}, ${thirdLastPoint.y})`);
371
404
  route.splice(lastIdx - 2, 1); // Remove the third-to-last point
372
405
  }
373
406
  }
@@ -400,7 +433,6 @@ function getManHattanPath(params) {
400
433
  const p3ToP4Horizontal = Math.abs(p3.y - p4.y) < tolerance;
401
434
  if (p3ToP4Horizontal) {
402
435
  // Pattern: horizontal -> vertical -> horizontal (zigzag)
403
- console.log('[getManHattanPath] Removing zigzag at target edge:', `(${p2.x}, ${p2.y})`, `and (${p3.x}, ${p3.y})`);
404
436
  route.splice(i + 1, 2); // Remove p2 and p3
405
437
  continue;
406
438
  }
@@ -411,7 +443,6 @@ function getManHattanPath(params) {
411
443
 
412
444
  // Add source and target anchors to route
413
445
  const finalRoute = [sourceAnchor, ...route, targetAnchor];
414
- console.log('[getManHattanPath] Final route:', finalRoute.map(p => `(${p.x}, ${p.y})`));
415
446
 
416
447
  // Convert to SVG path string
417
448
  return (0, _svg.pointsToPath)(finalRoute, options.precision, options.borderRadius);
@@ -2,29 +2,61 @@ import { Point } from '../geometry';
2
2
  import type { ResolvedOptions, NodeLookup } from '../options';
3
3
  /**
4
4
  * ObstacleMap class for managing obstacles in pathfinding
5
- * Uses a grid-based spatial partitioning for efficient queries
5
+ * Feature: manhattan-optimization
6
+ *
7
+ * Uses QuadTree for efficient spatial queries (O(log n) instead of O(n))
8
+ * Includes query caching for repeated accessibility checks
6
9
  */
7
10
  export declare class ObstacleMap {
8
11
  private options;
9
- private mapGridSize;
10
- private map;
12
+ private quadTree;
13
+ private obstacles;
11
14
  private sourceAnchor?;
12
15
  private targetAnchor?;
16
+ private accessibilityCache;
17
+ private cacheHits;
18
+ private cacheMisses;
13
19
  constructor(options: ResolvedOptions);
14
20
  /**
15
- * Build obstacle map from node lookup
21
+ * Get cache statistics for performance monitoring
16
22
  */
17
- build(nodeLookup: NodeLookup, sourceNodeId: string, targetNodeId: string, sourceAnchor?: Point, targetAnchor?: Point): ObstacleMap;
23
+ getCacheStats(): {
24
+ hits: number;
25
+ misses: number;
26
+ hitRate: number;
27
+ };
28
+ /**
29
+ * Clear the accessibility cache
30
+ */
31
+ clearCache(): void;
18
32
  /**
19
- * Shrink bbox to exclude the area around the anchor point
20
- * This allows paths to start/end at the anchor but prevents crossing the node
33
+ * Build obstacle map from node lookup using QuadTree
21
34
  */
22
- private shrinkBBoxAroundAnchor;
35
+ build(nodeLookup: NodeLookup, sourceNodeId: string, targetNodeId: string, sourceAnchor?: Point, targetAnchor?: Point): ObstacleMap;
23
36
  /**
24
37
  * Check if a point is accessible (not inside any obstacle)
25
- * Uses binary search optimization: step -> step/2 -> step/4 -> ... -> 1px
38
+ * Uses QuadTree for efficient spatial queries with caching
26
39
  */
27
40
  isAccessible(point: Point, checkRadius?: number): boolean;
41
+ /**
42
+ * Check if an anchor point has sufficient clearance around it
43
+ * This ensures the path can start/end at the anchor without being blocked
44
+ */
45
+ hasAnchorClearance(anchor: Point, direction: 'top' | 'right' | 'bottom' | 'left'): boolean;
46
+ /**
47
+ * Check accessibility without using cache (for internal use)
48
+ */
49
+ private isAccessibleWithoutCache;
50
+ /**
51
+ * Find the nearest accessible point from an anchor in a given direction
52
+ * Uses binary search for efficiency
53
+ */
54
+ findNearestAccessiblePoint(anchor: Point, direction: 'top' | 'right' | 'bottom' | 'left', maxDistance: number): Point | null;
55
+ /**
56
+ * Batch check accessibility for multiple points (optimized)
57
+ * Returns array of booleans in same order as input points
58
+ */
59
+ areAccessible(points: Point[]): boolean[];
28
60
  /**
29
61
  * Check accessibility using binary search optimization
30
62
  * Tries step -> step/2 -> step/4 -> ... -> 1px
@@ -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;;;OAGG;IACH,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,WAAW,GAAE,MAAU,GAAG,OAAO;IAiD5D;;;OAGG;IACH,OAAO,CAAC,4BAA4B;CAqCrC"}
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;AAI7D;;;;;;GAMG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,QAAQ,CAAgC;IAChD,OAAO,CAAC,SAAS,CAAmD;IACpE,OAAO,CAAC,YAAY,CAAC,CAAO;IAC5B,OAAO,CAAC,YAAY,CAAC,CAAO;IAG5B,OAAO,CAAC,kBAAkB,CAAkC;IAC5D,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,WAAW,CAAY;gBAEnB,OAAO,EAAE,eAAe;IAIpC;;OAEG;IACH,aAAa,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE;IASlE;;OAEG;IACH,UAAU,IAAI,IAAI;IAMlB;;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;IA8Cd;;;OAGG;IACH,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,WAAW,GAAE,MAAU,GAAG,OAAO;IAoD5D;;;OAGG;IACH,kBAAkB,CAAC,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO;IA0C1F;;OAEG;IACH,OAAO,CAAC,wBAAwB;IAShC;;;OAGG;IACH,0BAA0B,CACxB,MAAM,EAAE,KAAK,EACb,SAAS,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,EAC9C,WAAW,EAAE,MAAM,GAClB,KAAK,GAAG,IAAI;IAyDf;;;OAGG;IACH,aAAa,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE;IAIzC;;;OAGG;IACH,OAAO,CAAC,4BAA4B;CAqCrC"}