Skip to content

Commit 365cb7c

Browse files
Refactored edge pathfinding methods to their own file. Changed helper files to static classes.
1 parent 75773af commit 365cb7c

23 files changed

+780
-713
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Canvas, Position, Side } from "src/@types/Canvas"
2+
import AdvancedCanvasPlugin from "src/main"
3+
4+
export default abstract class EdgePathfindingMethod {
5+
abstract getPath(plugin: AdvancedCanvasPlugin, canvas: Canvas, fromPos: Position, fromSide: Side, toPos: Position, toSide: Side, isDragging: boolean): EdgePath | null
6+
}
7+
8+
export interface EdgePath {
9+
svgPath: string
10+
center: Position
11+
rotateArrows: boolean
12+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { 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+
6+
export default class EdgePathfindingDirect extends EdgePathfindingMethod {
7+
getPath(_plugin: AdvancedCanvasPlugin, _canvas: Canvas, fromPos: Position, _fromSide: Side, toPos: Position, _toSide: Side, _isDragging: boolean): EdgePath {
8+
return {
9+
svgPath: SvgPathHelper.pathArrayToSvgPath([fromPos, toPos], false),
10+
center: {
11+
x: (fromPos.x + toPos.x) / 2,
12+
y: (fromPos.y + toPos.y) / 2
13+
},
14+
rotateArrows: true
15+
}
16+
}
17+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { 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+
6+
export default class EdgePathfindingSquare extends EdgePathfindingMethod {
7+
getPath(_plugin: AdvancedCanvasPlugin, _canvas: Canvas, fromPos: Position, fromSide: Side, toPos: Position, _toSide: Side, _isDragging: boolean): EdgePath {
8+
let pathArray: Position[] = []
9+
if (fromSide === 'bottom' || fromSide === 'top') {
10+
pathArray = [
11+
fromPos,
12+
{ x: fromPos.x, y: fromPos.y + (toPos.y - fromPos.y) / 2 },
13+
{ x: toPos.x, y: fromPos.y + (toPos.y - fromPos.y) / 2 },
14+
toPos
15+
]
16+
} else {
17+
pathArray = [
18+
fromPos,
19+
{ x: fromPos.x + (toPos.x - fromPos.x) / 2, y: fromPos.y },
20+
{ x: fromPos.x + (toPos.x - fromPos.x) / 2, y: toPos.y },
21+
toPos
22+
]
23+
}
24+
25+
return {
26+
svgPath: SvgPathHelper.pathArrayToSvgPath(pathArray, false),
27+
center: {
28+
x: (fromPos.x + toPos.x) / 2,
29+
y: (fromPos.y + toPos.y) / 2
30+
},
31+
rotateArrows: false
32+
}
33+
}
34+
}

0 commit comments

Comments
 (0)