1+ import { BBox , Canvas , Position , Side } from "src/@types/Canvas"
2+ import EdgePathfindingMethod , { EdgePath } from "./edge-pathfinding-method"
3+ import SvgPathHelper from "src/utils/svg-path-helper"
4+ import AdvancedCanvasPlugin from "src/main"
5+ import BBoxHelper from "src/utils/bbox-helper"
6+
7+ const DIRECTIONS = [
8+ { dx : 1 , dy : 0 } , { dx : - 1 , dy : 0 } , { dx : 0 , dy : 1 } , { dx : 0 , dy : - 1 } ,
9+ { dx : 1 , dy : 1 } , { dx : - 1 , dy : 1 } , { dx : 1 , dy : - 1 } , { dx : - 1 , dy : - 1 } ,
10+ ] as const
11+ const DIAGONAL_COST = Math . sqrt ( 2 )
12+
13+ class Node {
14+ x : number
15+ y : number
16+ gCost : number
17+ hCost : number
18+ fCost : number
19+ parent : Node | null
20+
21+ constructor ( x : number , y : number ) {
22+ this . x = x
23+ this . y = y
24+
25+ this . gCost = 0
26+ this . hCost = 0
27+ this . fCost = 0
28+ this . parent = null
29+ }
30+
31+ // Only check for x and y, not gCost, hCost, fCost, or parent
32+ inList ( nodes : Node [ ] ) : boolean {
33+ return nodes . some ( n => n . x === this . x && n . y === this . y )
34+ }
35+ }
36+
37+ export default class EdgePathfindingAStar extends EdgePathfindingMethod {
38+ getPath ( plugin : AdvancedCanvasPlugin , canvas : Canvas , fromPos : Position , fromSide : Side , toPos : Position , toSide : Side , isDragging : boolean ) : EdgePath | null {
39+ if ( isDragging && ! plugin . settings . getSetting ( 'edgeStylePathfinderPathLiveUpdate' ) ) return null
40+
41+ const nodeBBoxes = [ ...canvas . nodes . values ( ) ]
42+ . filter ( node => {
43+ const nodeData = node . getData ( )
44+
45+ const isGroup = nodeData . type === 'group' // Exclude group nodes
46+ const isOpenPortal = nodeData . portalToFile !== undefined // Exclude open portals
47+
48+ return ! isGroup && ! isOpenPortal
49+ } ) . map ( node => node . getBBox ( ) )
50+
51+ const fromPosWithMargin = BBoxHelper . moveInDirection ( fromPos , fromSide , 10 )
52+ const toPosWithMargin = BBoxHelper . moveInDirection ( toPos , toSide , 10 )
53+
54+ const gridResolution = plugin . settings . getSetting ( 'edgeStylePathfinderGridResolution' )
55+ const pathArray = this . aStarAlgorithm ( fromPosWithMargin , fromSide , toPosWithMargin , toSide , nodeBBoxes , gridResolution )
56+ if ( ! pathArray ) return null // No path found - use default path
57+
58+ // Make connection points to the node removing the margin
59+ pathArray . splice ( 0 , 0 , fromPos )
60+ pathArray . splice ( pathArray . length , 0 , toPos )
61+
62+ const roundedPath = plugin . settings . getSetting ( 'edgeStylePathfinderPathRounded' )
63+ const svgPath = SvgPathHelper . pathArrayToSvgPath ( pathArray , roundedPath )
64+
65+ return {
66+ svgPath : svgPath ,
67+ center : pathArray [ Math . floor ( pathArray . length / 2 ) ] ,
68+ rotateArrows : false
69+ }
70+ }
71+
72+ private aStarAlgorithm ( fromPos : Position , fromSide : Side , toPos : Position , toSide : Side , obstacles : BBox [ ] , gridResolution : number ) : Position [ ] | null {
73+ const start : Node = new Node (
74+ Math . floor ( fromPos . x / gridResolution ) * gridResolution ,
75+ Math . floor ( fromPos . y / gridResolution ) * gridResolution
76+ )
77+ // Round start and end positions to the nearest grid cell outside of the nodes to connect
78+ if ( fromSide === 'right' && fromPos . x !== start . x ) start . x += gridResolution
79+ if ( fromSide === 'bottom' && fromPos . y !== start . y ) start . y += gridResolution
80+
81+ const end : Node = new Node (
82+ Math . floor ( toPos . x / gridResolution ) * gridResolution ,
83+ Math . floor ( toPos . y / gridResolution ) * gridResolution
84+ )
85+ // Round start and end positions to the nearest grid cell outside of the nodes to connect
86+ if ( toSide === 'right' && toPos . x !== end . x ) end . x += gridResolution
87+ if ( toSide === 'bottom' && toPos . y !== end . y ) end . y += gridResolution
88+
89+ // Check if start and end positions are valid
90+ if ( this . isInsideObstacle ( start , obstacles ) || this . isInsideObstacle ( end , obstacles ) ) return null
91+
92+ const openSet : Node [ ] = [ start ]
93+ const closedSet : Node [ ] = [ ]
94+
95+ while ( openSet . length > 0 ) {
96+ // Find the node with the lowest fCost in the open set
97+ let current : Node | null = null
98+ let lowestFCost = Infinity
99+
100+ for ( const node of openSet ) {
101+ if ( node . fCost < lowestFCost ) {
102+ current = node
103+ lowestFCost = node . fCost
104+ }
105+ }
106+
107+ // No path found
108+ if ( ! current ) return null
109+
110+ // Remove the current node from the open set and add it to the closed set
111+ openSet . splice ( openSet . indexOf ( current ) , 1 )
112+ closedSet . push ( current )
113+
114+ // Check if we have reached the end
115+ if ( current . x === end . x && current . y === end . y ) {
116+ return [ fromPos , ...this . reconstructPath ( current ) , toPos ] . map ( node => ( { x : node . x , y : node . y } ) )
117+ }
118+
119+ // Location is not start or end, all touching positions are invalid
120+ if ( ! ( current . x === start . x && current . y === start . y ) && this . isTouchingObstacle ( current , obstacles ) ) continue
121+
122+ // Expand neighbors
123+ for ( const neighbor of this . getPossibleNeighbors ( current , obstacles , gridResolution ) ) {
124+ if ( neighbor . inList ( closedSet ) ) continue
125+
126+ // Calculate tentative gCost
127+ const tentativeGCost = current . gCost + this . getMovementCost ( {
128+ dx : neighbor . x - current . x ,
129+ dy : neighbor . y - current . y ,
130+ } )
131+
132+ // Check if neighbor is not already in the open set or if the new gCost is lower
133+ if ( ! neighbor . inList ( openSet ) || tentativeGCost < neighbor . gCost ) {
134+ neighbor . parent = current
135+ neighbor . gCost = tentativeGCost
136+ neighbor . hCost = this . heuristic ( neighbor , end )
137+ neighbor . fCost = neighbor . gCost + neighbor . hCost
138+
139+ // Add neighbor to the open set
140+ openSet . push ( neighbor )
141+ }
142+ }
143+ }
144+
145+ // No path found
146+ return null
147+ }
148+
149+ // Manhattan distance
150+ private heuristic ( node : Node , end : Node ) : number {
151+ return Math . abs ( node . x - end . x ) + Math . abs ( node . y - end . y )
152+ }
153+
154+ // Define a function to check if a position isn't inside any obstacle
155+ private isTouchingObstacle ( node : Position , obstacles : BBox [ ] ) : boolean {
156+ return obstacles . some ( obstacle => BBoxHelper . insideBBox ( node , obstacle , true ) )
157+ }
158+
159+ private isInsideObstacle ( node : Position , obstacles : BBox [ ] ) : boolean {
160+ return obstacles . some ( obstacle => BBoxHelper . insideBBox ( node , obstacle , false ) )
161+ }
162+
163+ // Define a function to calculate movement cost based on direction
164+ private getMovementCost ( direction : { dx : number ; dy : number } ) : number {
165+ return direction . dx !== 0 && direction . dy !== 0 ? DIAGONAL_COST : 1
166+ }
167+
168+ private getPossibleNeighbors ( node : Node , obstacles : BBox [ ] , gridResolution : number ) : Node [ ] {
169+ const neighbors : Node [ ] = [ ]
170+
171+ for ( const direction of DIRECTIONS ) {
172+ const neighbor = new Node (
173+ node . x + direction . dx * gridResolution ,
174+ node . y + direction . dy * gridResolution
175+ )
176+ neighbor . gCost = node . gCost + this . getMovementCost ( direction )
177+
178+ if ( this . isInsideObstacle ( neighbor , obstacles ) ) continue
179+
180+ neighbors . push ( neighbor )
181+ }
182+
183+ return neighbors
184+ }
185+
186+ private reconstructPath ( node : Node ) : Node [ ] {
187+ const path : Node [ ] = [ ]
188+ while ( node ) {
189+ path . push ( node )
190+ node = node . parent !
191+ }
192+ return path . reverse ( )
193+ }
194+ }
0 commit comments