@ngx-km/path-finding 0.0.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.
@@ -0,0 +1,1002 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Injectable } from '@angular/core';
3
+
4
+ /**
5
+ * Path Finding Models
6
+ *
7
+ * Types and interfaces for obstacle-aware path routing between nodes.
8
+ */
9
+ /**
10
+ * Default path finding options
11
+ */
12
+ const DEFAULT_PATH_FINDING_OPTIONS = {
13
+ pathType: 'orthogonal',
14
+ obstaclePadding: 20,
15
+ cornerPadding: 10,
16
+ connectionSpacing: 15,
17
+ allowOverlap: false,
18
+ spreadAnchors: true,
19
+ anchorSpacing: 15,
20
+ };
21
+
22
+ /**
23
+ * Path Utilities
24
+ *
25
+ * Functions for path manipulation, simplification, and measurement.
26
+ */
27
+ /**
28
+ * Calculate the length of a route using Manhattan distance (orthogonal segments)
29
+ * Used for comparing route candidates
30
+ */
31
+ function calculateRouteLength(points) {
32
+ let length = 0;
33
+ for (let i = 0; i < points.length - 1; i++) {
34
+ length += Math.abs(points[i + 1].x - points[i].x) + Math.abs(points[i + 1].y - points[i].y);
35
+ }
36
+ return length;
37
+ }
38
+ /**
39
+ * Calculate total path length using Euclidean distance
40
+ * Excludes control points for bezier curves
41
+ */
42
+ function calculatePathLength(waypoints) {
43
+ let length = 0;
44
+ const points = waypoints.filter((w) => !w.isControlPoint);
45
+ for (let i = 0; i < points.length - 1; i++) {
46
+ length += Math.hypot(points[i + 1].x - points[i].x, points[i + 1].y - points[i].y);
47
+ }
48
+ return length;
49
+ }
50
+ /**
51
+ * Calculate the midpoint of a path
52
+ * Returns the point at half the total path length
53
+ */
54
+ function calculateMidpoint(waypoints) {
55
+ if (waypoints.length === 0)
56
+ return { x: 0, y: 0 };
57
+ if (waypoints.length === 1)
58
+ return { x: waypoints[0].x, y: waypoints[0].y };
59
+ const nonControlPoints = waypoints.filter((w) => !w.isControlPoint);
60
+ if (nonControlPoints.length >= 2) {
61
+ const totalLength = calculatePathLength(nonControlPoints);
62
+ const targetLength = totalLength / 2;
63
+ let accumulatedLength = 0;
64
+ for (let i = 0; i < nonControlPoints.length - 1; i++) {
65
+ const segmentLength = Math.hypot(nonControlPoints[i + 1].x - nonControlPoints[i].x, nonControlPoints[i + 1].y - nonControlPoints[i].y);
66
+ if (accumulatedLength + segmentLength >= targetLength) {
67
+ const remainingLength = targetLength - accumulatedLength;
68
+ const ratio = segmentLength > 0 ? remainingLength / segmentLength : 0;
69
+ return {
70
+ x: nonControlPoints[i].x + (nonControlPoints[i + 1].x - nonControlPoints[i].x) * ratio,
71
+ y: nonControlPoints[i].y + (nonControlPoints[i + 1].y - nonControlPoints[i].y) * ratio,
72
+ };
73
+ }
74
+ accumulatedLength += segmentLength;
75
+ }
76
+ }
77
+ const midIndex = Math.floor(nonControlPoints.length / 2);
78
+ return { x: nonControlPoints[midIndex].x, y: nonControlPoints[midIndex].y };
79
+ }
80
+ /**
81
+ * Simplify path by removing collinear points
82
+ * This reduces redundant waypoints while preserving path shape
83
+ */
84
+ function simplifyPath(waypoints) {
85
+ if (waypoints.length <= 2)
86
+ return waypoints;
87
+ const simplified = [waypoints[0]];
88
+ for (let i = 1; i < waypoints.length - 1; i++) {
89
+ const prev = simplified[simplified.length - 1];
90
+ const curr = waypoints[i];
91
+ const next = waypoints[i + 1];
92
+ const isCollinear = (Math.abs(prev.x - curr.x) < 0.001 && Math.abs(curr.x - next.x) < 0.001) ||
93
+ (Math.abs(prev.y - curr.y) < 0.001 && Math.abs(curr.y - next.y) < 0.001);
94
+ if (!isCollinear) {
95
+ simplified.push(curr);
96
+ }
97
+ }
98
+ simplified.push(waypoints[waypoints.length - 1]);
99
+ return simplified;
100
+ }
101
+
102
+ /**
103
+ * Anchor Utilities
104
+ *
105
+ * Functions for calculating connection points and anchor slot assignments.
106
+ */
107
+ /**
108
+ * Get preferred connection side based on relative position of nodes
109
+ */
110
+ function getPreferredSide(from, to) {
111
+ const fromCenterX = from.x + from.width / 2;
112
+ const fromCenterY = from.y + from.height / 2;
113
+ const toCenterX = to.x + to.width / 2;
114
+ const toCenterY = to.y + to.height / 2;
115
+ const dx = toCenterX - fromCenterX;
116
+ const dy = toCenterY - fromCenterY;
117
+ if (Math.abs(dx) > Math.abs(dy)) {
118
+ return dx > 0 ? 'right' : 'left';
119
+ }
120
+ else {
121
+ return dy > 0 ? 'bottom' : 'top';
122
+ }
123
+ }
124
+ /**
125
+ * Get connection point on a node
126
+ */
127
+ function getConnectionPoint(node, targetNode, forcedAnchor, cornerPadding, slot, anchorSpacing) {
128
+ const side = forcedAnchor || getPreferredSide(node, targetNode);
129
+ const point = getPointOnSide(node, side, cornerPadding, slot, anchorSpacing);
130
+ return { side, x: point.x, y: point.y };
131
+ }
132
+ /**
133
+ * Get point on a specific side of a node
134
+ * Uses even distribution when multiple slots are on the same side
135
+ */
136
+ function getPointOnSide(node, side, cornerPadding, slot, _anchorSpacing // kept for API compatibility but not used
137
+ ) {
138
+ // Calculate position on edge using even distribution
139
+ const isVerticalSide = side === 'left' || side === 'right';
140
+ const edgeLength = isVerticalSide ? node.height : node.width;
141
+ const usableLength = edgeLength - 2 * cornerPadding;
142
+ let edgePosition;
143
+ if (slot && slot.totalSlots > 1) {
144
+ // Distribute evenly: for N slots, divide into N+1 segments
145
+ const t = (slot.slotIndex + 1) / (slot.totalSlots + 1);
146
+ edgePosition = cornerPadding + t * usableLength;
147
+ }
148
+ else {
149
+ // Single slot or no slot - use center
150
+ edgePosition = edgeLength / 2;
151
+ }
152
+ switch (side) {
153
+ case 'top':
154
+ return {
155
+ x: node.x + edgePosition,
156
+ y: node.y,
157
+ };
158
+ case 'bottom':
159
+ return {
160
+ x: node.x + edgePosition,
161
+ y: node.y + node.height,
162
+ };
163
+ case 'left':
164
+ return {
165
+ x: node.x,
166
+ y: node.y + edgePosition,
167
+ };
168
+ case 'right':
169
+ return {
170
+ x: node.x + node.width,
171
+ y: node.y + edgePosition,
172
+ };
173
+ }
174
+ }
175
+ /**
176
+ * Get a padded point - sits at padding distance from node edge
177
+ * Uses even distribution when multiple slots are on the same side
178
+ */
179
+ function getPaddedPoint(node, side, padding, cornerPadding, slot) {
180
+ // Calculate position on edge using even distribution
181
+ const isVerticalSide = side === 'left' || side === 'right';
182
+ const edgeLength = isVerticalSide ? node.height : node.width;
183
+ const usableLength = edgeLength - 2 * cornerPadding;
184
+ let edgePosition;
185
+ if (slot && slot.totalSlots > 1) {
186
+ // Distribute evenly: for N slots, divide into N+1 segments
187
+ const t = (slot.slotIndex + 1) / (slot.totalSlots + 1);
188
+ edgePosition = cornerPadding + t * usableLength;
189
+ }
190
+ else {
191
+ // Single slot or no slot - use center
192
+ edgePosition = edgeLength / 2;
193
+ }
194
+ switch (side) {
195
+ case 'top': {
196
+ const edgeX = node.x + edgePosition;
197
+ return {
198
+ x: edgeX,
199
+ y: node.y - padding,
200
+ side,
201
+ nodeEdgeX: edgeX,
202
+ nodeEdgeY: node.y,
203
+ };
204
+ }
205
+ case 'bottom': {
206
+ const edgeX = node.x + edgePosition;
207
+ return {
208
+ x: edgeX,
209
+ y: node.y + node.height + padding,
210
+ side,
211
+ nodeEdgeX: edgeX,
212
+ nodeEdgeY: node.y + node.height,
213
+ };
214
+ }
215
+ case 'left': {
216
+ const edgeY = node.y + edgePosition;
217
+ return {
218
+ x: node.x - padding,
219
+ y: edgeY,
220
+ side,
221
+ nodeEdgeX: node.x,
222
+ nodeEdgeY: edgeY,
223
+ };
224
+ }
225
+ case 'right': {
226
+ const edgeY = node.y + edgePosition;
227
+ return {
228
+ x: node.x + node.width + padding,
229
+ y: edgeY,
230
+ side,
231
+ nodeEdgeX: node.x + node.width,
232
+ nodeEdgeY: edgeY,
233
+ };
234
+ }
235
+ }
236
+ }
237
+ /**
238
+ * Calculate actual slot positions on a node edge
239
+ * Uses even distribution across the available edge space
240
+ */
241
+ function calculateSlotPositions(node, side, totalSlots, cornerPadding = 5) {
242
+ const positions = [];
243
+ // Calculate the available range on this edge
244
+ const isVerticalSide = side === 'left' || side === 'right';
245
+ const edgeLength = isVerticalSide ? node.height : node.width;
246
+ const usableLength = edgeLength - 2 * cornerPadding;
247
+ for (let i = 0; i < totalSlots; i++) {
248
+ // Distribute evenly: for N slots, divide into N+1 segments
249
+ // and place slots at positions 1, 2, ..., N
250
+ const t = (i + 1) / (totalSlots + 1);
251
+ const offset = cornerPadding + t * usableLength;
252
+ switch (side) {
253
+ case 'top':
254
+ positions.push({ x: node.x + offset, y: node.y });
255
+ break;
256
+ case 'bottom':
257
+ positions.push({ x: node.x + offset, y: node.y + node.height });
258
+ break;
259
+ case 'left':
260
+ positions.push({ x: node.x, y: node.y + offset });
261
+ break;
262
+ case 'right':
263
+ positions.push({ x: node.x + node.width, y: node.y + offset });
264
+ break;
265
+ }
266
+ }
267
+ return positions;
268
+ }
269
+ /**
270
+ * Find optimal slot assignment to minimize crossings
271
+ * Returns an array where assignment[connIndex] = slotIndex
272
+ */
273
+ function findOptimalSlotAssignment(connections, slotPositions) {
274
+ const n = connections.length;
275
+ // For small numbers, try all permutations to find the best one
276
+ if (n <= 6) {
277
+ return findBestPermutation(connections, slotPositions);
278
+ }
279
+ // For larger numbers, use greedy assignment based on distance
280
+ return greedySlotAssignment(connections, slotPositions);
281
+ }
282
+ /**
283
+ * Try all permutations to find the one with minimum total distance
284
+ */
285
+ function findBestPermutation(connections, slotPositions) {
286
+ const n = connections.length;
287
+ const indices = Array.from({ length: n }, (_, i) => i);
288
+ let bestAssignment = indices.slice();
289
+ let bestScore = Infinity;
290
+ // Generate all permutations
291
+ const permute = (arr, start) => {
292
+ if (start === arr.length - 1) {
293
+ // Calculate score for this permutation
294
+ let totalDist = 0;
295
+ for (let i = 0; i < n; i++) {
296
+ const slot = slotPositions[arr[i]];
297
+ const conn = connections[i];
298
+ totalDist += Math.hypot(conn.otherNodeX - slot.x, conn.otherNodeY - slot.y);
299
+ }
300
+ if (totalDist < bestScore) {
301
+ bestScore = totalDist;
302
+ bestAssignment = arr.slice();
303
+ }
304
+ return;
305
+ }
306
+ for (let i = start; i < arr.length; i++) {
307
+ [arr[start], arr[i]] = [arr[i], arr[start]];
308
+ permute(arr, start + 1);
309
+ [arr[start], arr[i]] = [arr[i], arr[start]];
310
+ }
311
+ };
312
+ permute(indices, 0);
313
+ return bestAssignment;
314
+ }
315
+ /**
316
+ * Greedy slot assignment for larger numbers of connections
317
+ */
318
+ function greedySlotAssignment(connections, slotPositions) {
319
+ const n = connections.length;
320
+ const assignment = new Array(n).fill(-1);
321
+ const usedSlots = new Set();
322
+ // Create distance matrix
323
+ const distances = [];
324
+ for (let c = 0; c < n; c++) {
325
+ for (let s = 0; s < n; s++) {
326
+ const dist = Math.hypot(connections[c].otherNodeX - slotPositions[s].x, connections[c].otherNodeY - slotPositions[s].y);
327
+ distances.push({ connIdx: c, slotIdx: s, dist });
328
+ }
329
+ }
330
+ // Sort by distance and greedily assign
331
+ distances.sort((a, b) => a.dist - b.dist);
332
+ for (const { connIdx, slotIdx } of distances) {
333
+ if (assignment[connIdx] === -1 && !usedSlots.has(slotIdx)) {
334
+ assignment[connIdx] = slotIdx;
335
+ usedSlots.add(slotIdx);
336
+ }
337
+ }
338
+ return assignment;
339
+ }
340
+ /**
341
+ * Calculate anchor slot assignments based on actual path results
342
+ * This uses the sides that were actually chosen by the routing algorithm
343
+ */
344
+ function calculateAnchorSlotsFromPaths(paths, nodeMap) {
345
+ const slots = new Map();
346
+ // Group connections by node and side
347
+ const connectionsByNodeSide = new Map();
348
+ for (const path of paths) {
349
+ const sourceNode = nodeMap.get(path.sourceId);
350
+ const targetNode = nodeMap.get(path.targetId);
351
+ if (!sourceNode || !targetNode)
352
+ continue;
353
+ // Use the actual sides from the calculated path
354
+ const sourceSide = path.sourcePoint.side;
355
+ const targetSide = path.targetPoint.side;
356
+ // Source connection
357
+ const sourceKey = `${path.sourceId}:${sourceSide}`;
358
+ if (!connectionsByNodeSide.has(sourceKey)) {
359
+ connectionsByNodeSide.set(sourceKey, []);
360
+ }
361
+ connectionsByNodeSide.get(sourceKey).push({
362
+ relationshipId: path.relationshipId,
363
+ isSource: true,
364
+ otherNodeX: targetNode.x + targetNode.width / 2,
365
+ otherNodeY: targetNode.y + targetNode.height / 2,
366
+ });
367
+ // Target connection
368
+ const targetKey = `${path.targetId}:${targetSide}`;
369
+ if (!connectionsByNodeSide.has(targetKey)) {
370
+ connectionsByNodeSide.set(targetKey, []);
371
+ }
372
+ connectionsByNodeSide.get(targetKey).push({
373
+ relationshipId: path.relationshipId,
374
+ isSource: false,
375
+ otherNodeX: sourceNode.x + sourceNode.width / 2,
376
+ otherNodeY: sourceNode.y + sourceNode.height / 2,
377
+ });
378
+ }
379
+ // Assign slots for each node-side combination
380
+ for (const [key, connections] of connectionsByNodeSide) {
381
+ const [nodeId, side] = key.split(':');
382
+ const node = nodeMap.get(nodeId);
383
+ if (!node)
384
+ continue;
385
+ const totalSlots = connections.length;
386
+ if (totalSlots === 1) {
387
+ // Single connection - just assign slot 0
388
+ const conn = connections[0];
389
+ const slotKey = `${conn.relationshipId}-${conn.isSource ? 'source' : 'target'}`;
390
+ slots.set(slotKey, {
391
+ nodeId,
392
+ side,
393
+ relationshipId: conn.relationshipId,
394
+ isSource: conn.isSource,
395
+ slotIndex: 0,
396
+ totalSlots: 1,
397
+ });
398
+ continue;
399
+ }
400
+ // Calculate the actual slot positions on the node edge
401
+ const slotPositions = calculateSlotPositions(node, side, totalSlots);
402
+ // Find optimal assignment: pair each connection with a slot position
403
+ // to minimize total distance and avoid crossings
404
+ const assignment = findOptimalSlotAssignment(connections, slotPositions);
405
+ // Assign slots based on the optimal pairing
406
+ assignment.forEach((slotIndex, connIndex) => {
407
+ const conn = connections[connIndex];
408
+ const slotKey = `${conn.relationshipId}-${conn.isSource ? 'source' : 'target'}`;
409
+ slots.set(slotKey, {
410
+ nodeId,
411
+ side,
412
+ relationshipId: conn.relationshipId,
413
+ isSource: conn.isSource,
414
+ slotIndex,
415
+ totalSlots,
416
+ });
417
+ });
418
+ }
419
+ return slots;
420
+ }
421
+
422
+ /**
423
+ * Geometry Utilities
424
+ *
425
+ * Core geometric types and functions for path finding calculations.
426
+ */
427
+ /**
428
+ * Clamp a value between min and max
429
+ */
430
+ function clamp(value, min, max) {
431
+ return Math.max(min, Math.min(max, value));
432
+ }
433
+ /**
434
+ * Create a Rect from a PathNode
435
+ */
436
+ function createRect(node) {
437
+ return {
438
+ x: node.x,
439
+ y: node.y,
440
+ width: node.width,
441
+ height: node.height,
442
+ right: node.x + node.width,
443
+ bottom: node.y + node.height,
444
+ centerX: node.x + node.width / 2,
445
+ centerY: node.y + node.height / 2,
446
+ };
447
+ }
448
+ /**
449
+ * Create a padded Rect from a PathNode
450
+ */
451
+ function createPaddedRect(node, padding) {
452
+ return {
453
+ x: node.x - padding,
454
+ y: node.y - padding,
455
+ width: node.width + padding * 2,
456
+ height: node.height + padding * 2,
457
+ right: node.x + node.width + padding,
458
+ bottom: node.y + node.height + padding,
459
+ centerX: node.x + node.width / 2,
460
+ centerY: node.y + node.height / 2,
461
+ };
462
+ }
463
+ /**
464
+ * Expand a Rect by padding amount on all sides
465
+ */
466
+ function expandRect(rect, padding) {
467
+ return {
468
+ x: rect.x - padding,
469
+ y: rect.y - padding,
470
+ width: rect.width + padding * 2,
471
+ height: rect.height + padding * 2,
472
+ right: rect.right + padding,
473
+ bottom: rect.bottom + padding,
474
+ centerX: rect.centerX,
475
+ centerY: rect.centerY,
476
+ };
477
+ }
478
+ /**
479
+ * Check if a line segment passes through rectangle interior (not just touches edge)
480
+ * Used for obstacle collision detection in orthogonal routing
481
+ */
482
+ function linePassesThroughRect(p1, p2, rect) {
483
+ // Check bounding box overlap first
484
+ const lineMinX = Math.min(p1.x, p2.x);
485
+ const lineMaxX = Math.max(p1.x, p2.x);
486
+ const lineMinY = Math.min(p1.y, p2.y);
487
+ const lineMaxY = Math.max(p1.y, p2.y);
488
+ // No overlap at all
489
+ if (lineMaxX <= rect.x || lineMinX >= rect.right)
490
+ return false;
491
+ if (lineMaxY <= rect.y || lineMinY >= rect.bottom)
492
+ return false;
493
+ // For orthogonal lines, check if line passes through interior
494
+ const isVertical = Math.abs(p1.x - p2.x) < 0.001;
495
+ const isHorizontal = Math.abs(p1.y - p2.y) < 0.001;
496
+ if (isVertical) {
497
+ // Vertical line: passes through if x is strictly inside rect
498
+ return p1.x > rect.x && p1.x < rect.right;
499
+ }
500
+ if (isHorizontal) {
501
+ // Horizontal line: passes through if y is strictly inside rect
502
+ return p1.y > rect.y && p1.y < rect.bottom;
503
+ }
504
+ // Diagonal line (shouldn't happen for orthogonal routing)
505
+ return true;
506
+ }
507
+
508
+ /**
509
+ * Orthogonal Path Strategy
510
+ *
511
+ * Generates paths with right-angle turns only (Manhattan-style routing).
512
+ * Supports obstacle avoidance with multiple routing strategies.
513
+ */
514
+ /**
515
+ * Orthogonal path strategy - routes paths with right-angle turns
516
+ */
517
+ class OrthogonalStrategy {
518
+ calculatePath(context) {
519
+ const { source, target, relationship, allNodes, options, sourceSlot, targetSlot } = context;
520
+ const padding = options.obstaclePadding;
521
+ // Create rectangles for collision detection
522
+ const sourceRect = createRect(source);
523
+ const targetRect = createRect(target);
524
+ // Get obstacles (all nodes except source and target, with padding)
525
+ const obstacles = options.allowOverlap
526
+ ? []
527
+ : allNodes
528
+ .filter((n) => n.id !== source.id && n.id !== target.id)
529
+ .map((n) => createPaddedRect(n, padding));
530
+ // If we have slots from a previous pass, force those sides to maintain consistency
531
+ const forcedSourceSide = sourceSlot?.side ?? relationship.sourceAnchor;
532
+ const forcedTargetSide = targetSlot?.side ?? relationship.targetAnchor;
533
+ const bestRoute = this.findBestOrthogonalRoute(source, target, sourceRect, targetRect, obstacles, padding, forcedSourceSide, forcedTargetSide, options.cornerPadding, sourceSlot, targetSlot);
534
+ return {
535
+ sourcePoint: {
536
+ side: bestRoute.sourceSide,
537
+ x: bestRoute.waypoints[0].x,
538
+ y: bestRoute.waypoints[0].y,
539
+ },
540
+ targetPoint: {
541
+ side: bestRoute.targetSide,
542
+ x: bestRoute.waypoints[bestRoute.waypoints.length - 1].x,
543
+ y: bestRoute.waypoints[bestRoute.waypoints.length - 1].y,
544
+ },
545
+ waypoints: bestRoute.waypoints,
546
+ };
547
+ }
548
+ /**
549
+ * Find the best orthogonal route using padded-edge approach
550
+ */
551
+ findBestOrthogonalRoute(sourceNode, targetNode, sourceRect, targetRect, obstacles, padding, forcedSourceSide, forcedTargetSide, cornerPadding, sourceSlot, targetSlot) {
552
+ const candidates = [];
553
+ // Determine which sides to try
554
+ const sourceSides = forcedSourceSide
555
+ ? [forcedSourceSide]
556
+ : ['top', 'right', 'bottom', 'left'];
557
+ const targetSides = forcedTargetSide
558
+ ? [forcedTargetSide]
559
+ : ['top', 'right', 'bottom', 'left'];
560
+ // Try all side combinations
561
+ for (const sourceSide of sourceSides) {
562
+ for (const targetSide of targetSides) {
563
+ // Get padded points (at padding distance from node edge)
564
+ const sourcePadded = getPaddedPoint(sourceNode, sourceSide, padding, cornerPadding, sourceSlot);
565
+ const targetPadded = getPaddedPoint(targetNode, targetSide, padding, cornerPadding, targetSlot);
566
+ // Find route between padded points
567
+ const paddedRoute = this.findPaddedRoute(sourcePadded, targetPadded, sourceRect, targetRect, obstacles, padding);
568
+ if (paddedRoute) {
569
+ // Extend route to actual node edges
570
+ const fullRoute = this.extendRouteToEdges(paddedRoute, sourcePadded, targetPadded);
571
+ const length = calculateRouteLength(fullRoute);
572
+ candidates.push({
573
+ waypoints: fullRoute,
574
+ length,
575
+ sourceSide,
576
+ targetSide,
577
+ });
578
+ }
579
+ }
580
+ }
581
+ // Return the shortest valid route
582
+ if (candidates.length > 0) {
583
+ candidates.sort((a, b) => a.length - b.length);
584
+ return candidates[0];
585
+ }
586
+ // Fallback: simple direct route
587
+ const preferredSourceSide = forcedSourceSide || getPreferredSide(sourceNode, targetNode);
588
+ const preferredTargetSide = forcedTargetSide || getPreferredSide(targetNode, sourceNode);
589
+ const sourcePadded = getPaddedPoint(sourceNode, preferredSourceSide, padding, cornerPadding, sourceSlot);
590
+ const targetPadded = getPaddedPoint(targetNode, preferredTargetSide, padding, cornerPadding, targetSlot);
591
+ const fallbackRoute = this.createFallbackRoute(sourcePadded, targetPadded);
592
+ const fullRoute = this.extendRouteToEdges(fallbackRoute, sourcePadded, targetPadded);
593
+ return {
594
+ waypoints: fullRoute,
595
+ length: Infinity,
596
+ sourceSide: preferredSourceSide,
597
+ targetSide: preferredTargetSide,
598
+ };
599
+ }
600
+ /**
601
+ * Find route between padded points avoiding obstacles
602
+ */
603
+ findPaddedRoute(source, target, sourceRect, targetRect, obstacles, padding) {
604
+ // Create padded obstacle rects for collision detection
605
+ // Include source and target nodes as obstacles (paths shouldn't go through them)
606
+ const paddedSourceRect = expandRect(sourceRect, padding);
607
+ const paddedTargetRect = expandRect(targetRect, padding);
608
+ const allObstacles = [...obstacles, paddedSourceRect, paddedTargetRect];
609
+ // Try strategies in order of simplicity
610
+ const strategies = [
611
+ () => this.tryDirect(source, target, allObstacles),
612
+ () => this.tryLShape(source, target, allObstacles),
613
+ () => this.tryZShape(source, target, allObstacles),
614
+ () => this.tryUShape(source, target, sourceRect, targetRect, allObstacles, padding),
615
+ ];
616
+ for (const strategy of strategies) {
617
+ const route = strategy();
618
+ if (route) {
619
+ return simplifyPath(route);
620
+ }
621
+ }
622
+ return null;
623
+ }
624
+ /**
625
+ * Try direct route (straight line between padded points)
626
+ */
627
+ tryDirect(source, target, obstacles) {
628
+ // Only works if horizontally or vertically aligned
629
+ const isHorizontal = Math.abs(source.y - target.y) < 1;
630
+ const isVertical = Math.abs(source.x - target.x) < 1;
631
+ if (!isHorizontal && !isVertical)
632
+ return null;
633
+ const route = [
634
+ { x: source.x, y: source.y },
635
+ { x: target.x, y: target.y },
636
+ ];
637
+ if (this.isRouteValid(route, obstacles)) {
638
+ return route;
639
+ }
640
+ return null;
641
+ }
642
+ /**
643
+ * Try L-shaped route (one bend)
644
+ */
645
+ tryLShape(source, target, obstacles) {
646
+ const isHorizontalSource = source.side === 'left' || source.side === 'right';
647
+ const isHorizontalTarget = target.side === 'left' || target.side === 'right';
648
+ // L-route only works when source and target are on different axis
649
+ if (isHorizontalSource === isHorizontalTarget)
650
+ return null;
651
+ const routes = [];
652
+ if (isHorizontalSource) {
653
+ // Source horizontal, target vertical: go horizontal first, then vertical
654
+ routes.push([
655
+ { x: source.x, y: source.y },
656
+ { x: target.x, y: source.y },
657
+ { x: target.x, y: target.y },
658
+ ]);
659
+ }
660
+ else {
661
+ // Source vertical, target horizontal: go vertical first, then horizontal
662
+ routes.push([
663
+ { x: source.x, y: source.y },
664
+ { x: source.x, y: target.y },
665
+ { x: target.x, y: target.y },
666
+ ]);
667
+ }
668
+ for (const route of routes) {
669
+ if (this.isRouteValid(route, obstacles)) {
670
+ return route;
671
+ }
672
+ }
673
+ return null;
674
+ }
675
+ /**
676
+ * Try Z-shaped route (two bends)
677
+ */
678
+ tryZShape(source, target, obstacles) {
679
+ const isHorizontalSource = source.side === 'left' || source.side === 'right';
680
+ const isHorizontalTarget = target.side === 'left' || target.side === 'right';
681
+ const routes = [];
682
+ if (isHorizontalSource && isHorizontalTarget) {
683
+ // Both horizontal: route with vertical middle segment
684
+ const midX = (source.x + target.x) / 2;
685
+ routes.push([
686
+ { x: source.x, y: source.y },
687
+ { x: midX, y: source.y },
688
+ { x: midX, y: target.y },
689
+ { x: target.x, y: target.y },
690
+ ]);
691
+ }
692
+ else if (!isHorizontalSource && !isHorizontalTarget) {
693
+ // Both vertical: route with horizontal middle segment
694
+ const midY = (source.y + target.y) / 2;
695
+ routes.push([
696
+ { x: source.x, y: source.y },
697
+ { x: source.x, y: midY },
698
+ { x: target.x, y: midY },
699
+ { x: target.x, y: target.y },
700
+ ]);
701
+ }
702
+ // Sort by length and return first valid
703
+ routes.sort((a, b) => calculateRouteLength(a) - calculateRouteLength(b));
704
+ for (const route of routes) {
705
+ if (this.isRouteValid(route, obstacles)) {
706
+ return route;
707
+ }
708
+ }
709
+ return null;
710
+ }
711
+ /**
712
+ * Try U-shaped route (goes around nodes)
713
+ */
714
+ tryUShape(source, target, sourceRect, targetRect, obstacles, padding) {
715
+ const routes = [];
716
+ // Calculate bounds for going around
717
+ const minX = Math.min(sourceRect.x, targetRect.x) - padding * 2;
718
+ const maxX = Math.max(sourceRect.right, targetRect.right) + padding * 2;
719
+ const minY = Math.min(sourceRect.y, targetRect.y) - padding * 2;
720
+ const maxY = Math.max(sourceRect.bottom, targetRect.bottom) + padding * 2;
721
+ // Route around top
722
+ routes.push([
723
+ { x: source.x, y: source.y },
724
+ { x: source.x, y: minY },
725
+ { x: target.x, y: minY },
726
+ { x: target.x, y: target.y },
727
+ ]);
728
+ // Route around bottom
729
+ routes.push([
730
+ { x: source.x, y: source.y },
731
+ { x: source.x, y: maxY },
732
+ { x: target.x, y: maxY },
733
+ { x: target.x, y: target.y },
734
+ ]);
735
+ // Route around left
736
+ routes.push([
737
+ { x: source.x, y: source.y },
738
+ { x: minX, y: source.y },
739
+ { x: minX, y: target.y },
740
+ { x: target.x, y: target.y },
741
+ ]);
742
+ // Route around right
743
+ routes.push([
744
+ { x: source.x, y: source.y },
745
+ { x: maxX, y: source.y },
746
+ { x: maxX, y: target.y },
747
+ { x: target.x, y: target.y },
748
+ ]);
749
+ // Sort by length and return first valid
750
+ routes.sort((a, b) => calculateRouteLength(a) - calculateRouteLength(b));
751
+ for (const route of routes) {
752
+ if (this.isRouteValid(route, obstacles)) {
753
+ return route;
754
+ }
755
+ }
756
+ return null;
757
+ }
758
+ /**
759
+ * Create fallback route when no valid route is found
760
+ */
761
+ createFallbackRoute(source, target) {
762
+ const isHorizontalSource = source.side === 'left' || source.side === 'right';
763
+ if (isHorizontalSource) {
764
+ return [
765
+ { x: source.x, y: source.y },
766
+ { x: target.x, y: source.y },
767
+ { x: target.x, y: target.y },
768
+ ];
769
+ }
770
+ else {
771
+ return [
772
+ { x: source.x, y: source.y },
773
+ { x: source.x, y: target.y },
774
+ { x: target.x, y: target.y },
775
+ ];
776
+ }
777
+ }
778
+ /**
779
+ * Extend route from padded points to actual node edges
780
+ */
781
+ extendRouteToEdges(paddedRoute, source, target) {
782
+ const result = [];
783
+ // Add start point at node edge
784
+ result.push({ x: source.nodeEdgeX, y: source.nodeEdgeY });
785
+ // Add all waypoints from padded route
786
+ for (const wp of paddedRoute) {
787
+ result.push({ x: wp.x, y: wp.y });
788
+ }
789
+ // Add end point at node edge
790
+ result.push({ x: target.nodeEdgeX, y: target.nodeEdgeY });
791
+ return simplifyPath(result);
792
+ }
793
+ /**
794
+ * Check if route is valid (doesn't pass through obstacles)
795
+ */
796
+ isRouteValid(route, obstacles) {
797
+ for (let i = 0; i < route.length - 1; i++) {
798
+ const p1 = route[i];
799
+ const p2 = route[i + 1];
800
+ for (const obs of obstacles) {
801
+ if (linePassesThroughRect(p1, p2, obs)) {
802
+ return false;
803
+ }
804
+ }
805
+ }
806
+ return true;
807
+ }
808
+ }
809
+
810
+ /**
811
+ * Bezier Path Strategy
812
+ *
813
+ * Generates smooth curved paths using cubic bezier curves.
814
+ * Control points are positioned based on connection sides.
815
+ */
816
+ /**
817
+ * Bezier path strategy - creates smooth curved paths
818
+ */
819
+ class BezierStrategy {
820
+ calculatePath(context) {
821
+ const { source, target, relationship, options, sourceSlot, targetSlot } = context;
822
+ const sourcePoint = getConnectionPoint(source, target, relationship.sourceAnchor, options.cornerPadding, sourceSlot, options.anchorSpacing);
823
+ const targetPoint = getConnectionPoint(target, source, relationship.targetAnchor, options.cornerPadding, targetSlot, options.anchorSpacing);
824
+ const waypoints = this.calculateBezierPath(sourcePoint, targetPoint);
825
+ return {
826
+ sourcePoint,
827
+ targetPoint,
828
+ waypoints,
829
+ };
830
+ }
831
+ /**
832
+ * Calculate bezier path with control points
833
+ */
834
+ calculateBezierPath(source, target) {
835
+ const controlDistance = Math.max(50, Math.hypot(target.x - source.x, target.y - source.y) * 0.3);
836
+ const sourceControl = this.getControlPointForBezier(source, controlDistance);
837
+ const targetControl = this.getControlPointForBezier(target, controlDistance);
838
+ return [
839
+ { x: source.x, y: source.y },
840
+ { x: sourceControl.x, y: sourceControl.y, isControlPoint: true },
841
+ { x: targetControl.x, y: targetControl.y, isControlPoint: true },
842
+ { x: target.x, y: target.y },
843
+ ];
844
+ }
845
+ /**
846
+ * Get control point position based on connection side
847
+ */
848
+ getControlPointForBezier(connection, distance) {
849
+ switch (connection.side) {
850
+ case 'top':
851
+ return { x: connection.x, y: connection.y - distance };
852
+ case 'bottom':
853
+ return { x: connection.x, y: connection.y + distance };
854
+ case 'left':
855
+ return { x: connection.x - distance, y: connection.y };
856
+ case 'right':
857
+ return { x: connection.x + distance, y: connection.y };
858
+ }
859
+ }
860
+ }
861
+
862
+ /**
863
+ * Straight Path Strategy
864
+ *
865
+ * Generates direct line paths between nodes.
866
+ * Simple and fast, but may overlap obstacles.
867
+ */
868
+ /**
869
+ * Straight path strategy - creates direct line paths
870
+ */
871
+ class StraightStrategy {
872
+ calculatePath(context) {
873
+ const { source, target, relationship, options, sourceSlot, targetSlot } = context;
874
+ const sourcePoint = getConnectionPoint(source, target, relationship.sourceAnchor, options.cornerPadding, sourceSlot, options.anchorSpacing);
875
+ const targetPoint = getConnectionPoint(target, source, relationship.targetAnchor, options.cornerPadding, targetSlot, options.anchorSpacing);
876
+ const waypoints = [
877
+ { x: sourcePoint.x, y: sourcePoint.y },
878
+ { x: targetPoint.x, y: targetPoint.y },
879
+ ];
880
+ return {
881
+ sourcePoint,
882
+ targetPoint,
883
+ waypoints,
884
+ };
885
+ }
886
+ }
887
+
888
+ /**
889
+ * Path Finding Service
890
+ *
891
+ * Calculates obstacle-aware paths between nodes using smart routing.
892
+ * Automatically selects optimal connection sides to find shortest valid paths.
893
+ *
894
+ * Uses strategy pattern for different path types:
895
+ * - Orthogonal: Right-angle turns with obstacle avoidance
896
+ * - Bezier: Smooth curved paths
897
+ * - Straight: Direct line paths
898
+ */
899
+ class PathFindingService {
900
+ strategies = {
901
+ orthogonal: new OrthogonalStrategy(),
902
+ bezier: new BezierStrategy(),
903
+ straight: new StraightStrategy(),
904
+ };
905
+ /**
906
+ * Calculate paths for all relationships
907
+ */
908
+ calculatePaths(input) {
909
+ const options = { ...DEFAULT_PATH_FINDING_OPTIONS, ...input.options };
910
+ const nodeMap = new Map();
911
+ for (const node of input.nodes) {
912
+ nodeMap.set(node.id, node);
913
+ }
914
+ // First pass: calculate paths without anchor spreading to determine actual sides
915
+ const initialPaths = [];
916
+ for (const relationship of input.relationships) {
917
+ const sourceNode = nodeMap.get(relationship.sourceId);
918
+ const targetNode = nodeMap.get(relationship.targetId);
919
+ if (!sourceNode || !targetNode) {
920
+ console.warn(`PathFindingService: Relationship "${relationship.id}" references unknown node`);
921
+ continue;
922
+ }
923
+ const path = this.calculatePath(sourceNode, targetNode, relationship, input.nodes, options, undefined, undefined);
924
+ initialPaths.push(path);
925
+ }
926
+ // If anchor spreading is disabled, return initial paths
927
+ if (!options.spreadAnchors) {
928
+ return { paths: initialPaths };
929
+ }
930
+ // Second pass: calculate anchor slots based on actual sides used, then recalculate paths
931
+ const anchorSlots = calculateAnchorSlotsFromPaths(initialPaths, nodeMap);
932
+ const paths = [];
933
+ for (const relationship of input.relationships) {
934
+ const sourceNode = nodeMap.get(relationship.sourceId);
935
+ const targetNode = nodeMap.get(relationship.targetId);
936
+ if (!sourceNode || !targetNode) {
937
+ continue;
938
+ }
939
+ const sourceSlot = anchorSlots.get(`${relationship.id}-source`);
940
+ const targetSlot = anchorSlots.get(`${relationship.id}-target`);
941
+ const path = this.calculatePath(sourceNode, targetNode, relationship, input.nodes, options, sourceSlot, targetSlot);
942
+ paths.push(path);
943
+ }
944
+ return { paths };
945
+ }
946
+ /**
947
+ * Calculate a single path between two nodes
948
+ */
949
+ calculatePath(source, target, relationship, allNodes, options, sourceSlot, targetSlot) {
950
+ const strategy = this.strategies[options.pathType];
951
+ const context = {
952
+ source,
953
+ target,
954
+ relationship,
955
+ allNodes,
956
+ options,
957
+ sourceSlot,
958
+ targetSlot,
959
+ };
960
+ const result = strategy.calculatePath(context);
961
+ const midpoint = calculateMidpoint(result.waypoints);
962
+ const length = calculatePathLength(result.waypoints);
963
+ return {
964
+ relationshipId: relationship.id,
965
+ sourceId: relationship.sourceId,
966
+ targetId: relationship.targetId,
967
+ sourcePoint: result.sourcePoint,
968
+ targetPoint: result.targetPoint,
969
+ waypoints: result.waypoints,
970
+ midpoint,
971
+ length,
972
+ pathType: options.pathType,
973
+ };
974
+ }
975
+ /**
976
+ * Recalculate paths (convenience method)
977
+ */
978
+ recalculatePaths(input) {
979
+ return this.calculatePaths(input);
980
+ }
981
+ /**
982
+ * Calculate a single path (convenience method)
983
+ */
984
+ calculateSinglePath(source, target, relationship, obstacles, options) {
985
+ const fullOptions = { ...DEFAULT_PATH_FINDING_OPTIONS, ...options };
986
+ return this.calculatePath(source, target, relationship, obstacles, fullOptions, undefined, undefined);
987
+ }
988
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: PathFindingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
989
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: PathFindingService });
990
+ }
991
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: PathFindingService, decorators: [{
992
+ type: Injectable
993
+ }] });
994
+
995
+ // Models - Values
996
+
997
+ /**
998
+ * Generated bundle index. Do not edit.
999
+ */
1000
+
1001
+ export { BezierStrategy, DEFAULT_PATH_FINDING_OPTIONS, OrthogonalStrategy, PathFindingService, StraightStrategy, calculateAnchorSlotsFromPaths, calculateMidpoint, calculatePathLength, calculateRouteLength, calculateSlotPositions, clamp, createPaddedRect, createRect, expandRect, findOptimalSlotAssignment, getConnectionPoint, getPaddedPoint, getPointOnSide, getPreferredSide, linePassesThroughRect, simplifyPath };
1002
+ //# sourceMappingURL=ngx-km-path-finding.mjs.map