@joshrp/react-flow-smart-edge 4.0.2
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.
- package/LICENSE +21 -0
- package/README.md +355 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.mjs +409 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +108 -0
- package/src/SmartBezierEdge/index.tsx +26 -0
- package/src/SmartEdge/index.tsx +92 -0
- package/src/SmartStepEdge/index.tsx +29 -0
- package/src/SmartStraightEdge/index.tsx +29 -0
- package/src/functions/createGrid.ts +81 -0
- package/src/functions/drawSvgPath.ts +72 -0
- package/src/functions/generatePath.ts +60 -0
- package/src/functions/getBoundingBoxes.ts +138 -0
- package/src/functions/guaranteeWalkablePath.ts +38 -0
- package/src/functions/index.ts +7 -0
- package/src/functions/pointConversion.ts +49 -0
- package/src/functions/utils.ts +15 -0
- package/src/getSmartEdge/index.ts +160 -0
- package/src/index.tsx +18 -0
- package/src/internal/SmartEdgeDebug.tsx +43 -0
- package/src/internal/SmartEdgeDebugOverlay.tsx +24 -0
- package/src/internal/useSmartEdgeDebug.ts +26 -0
- package/src/pathfinding/aStar.ts +134 -0
- package/src/pathfinding/grid.ts +141 -0
- package/src/pathfinding/types.ts +3 -0
- package/src/stories/CustomLabel.tsx +94 -0
- package/src/stories/DummyData.ts +194 -0
- package/src/stories/GraphWrapper.tsx +23 -0
- package/src/stories/SmartEdge.stories.tsx +67 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createGrid,
|
|
3
|
+
getBoundingBoxes,
|
|
4
|
+
gridToGraphPoint,
|
|
5
|
+
pathfindingAStarDiagonal,
|
|
6
|
+
svgDrawSmoothLinePath,
|
|
7
|
+
toInteger,
|
|
8
|
+
} from "../functions";
|
|
9
|
+
import type {
|
|
10
|
+
PointInfo,
|
|
11
|
+
PathFindingFunction,
|
|
12
|
+
SVGDrawFunction,
|
|
13
|
+
} from "../functions";
|
|
14
|
+
import type { Node, EdgeProps } from "@xyflow/react";
|
|
15
|
+
|
|
16
|
+
export type EdgeParams = Pick<
|
|
17
|
+
EdgeProps,
|
|
18
|
+
| "sourceX"
|
|
19
|
+
| "sourceY"
|
|
20
|
+
| "targetX"
|
|
21
|
+
| "targetY"
|
|
22
|
+
| "sourcePosition"
|
|
23
|
+
| "targetPosition"
|
|
24
|
+
>;
|
|
25
|
+
|
|
26
|
+
export interface GetSmartEdgeOptions {
|
|
27
|
+
gridRatio?: number;
|
|
28
|
+
nodePadding?: number;
|
|
29
|
+
drawEdge?: SVGDrawFunction;
|
|
30
|
+
generatePath?: PathFindingFunction;
|
|
31
|
+
// Internal-only debug hook. Not intended for public consumption.
|
|
32
|
+
debug?: {
|
|
33
|
+
enabled?: boolean;
|
|
34
|
+
setGraphBox?: (box: {
|
|
35
|
+
x: number;
|
|
36
|
+
y: number;
|
|
37
|
+
width: number;
|
|
38
|
+
height: number;
|
|
39
|
+
}) => void;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type GetSmartEdgeParams<
|
|
44
|
+
NodeDataType extends Record<string, unknown> = Record<string, unknown>,
|
|
45
|
+
> = EdgeParams & {
|
|
46
|
+
options?: GetSmartEdgeOptions;
|
|
47
|
+
nodes: Node<NodeDataType>[];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export interface GetSmartEdgeReturn {
|
|
51
|
+
svgPathString: string;
|
|
52
|
+
edgeCenterX: number;
|
|
53
|
+
edgeCenterY: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const getSmartEdge = <
|
|
57
|
+
NodeDataType extends Record<string, unknown> = Record<string, unknown>,
|
|
58
|
+
>({
|
|
59
|
+
options = {},
|
|
60
|
+
nodes = [],
|
|
61
|
+
sourceX,
|
|
62
|
+
sourceY,
|
|
63
|
+
targetX,
|
|
64
|
+
targetY,
|
|
65
|
+
sourcePosition,
|
|
66
|
+
targetPosition,
|
|
67
|
+
}: GetSmartEdgeParams<NodeDataType>): GetSmartEdgeReturn | Error => {
|
|
68
|
+
try {
|
|
69
|
+
const {
|
|
70
|
+
drawEdge = svgDrawSmoothLinePath,
|
|
71
|
+
generatePath = pathfindingAStarDiagonal,
|
|
72
|
+
} = options;
|
|
73
|
+
|
|
74
|
+
let { gridRatio = 10, nodePadding = 10 } = options;
|
|
75
|
+
gridRatio = toInteger(gridRatio);
|
|
76
|
+
nodePadding = toInteger(nodePadding);
|
|
77
|
+
|
|
78
|
+
// We use the node's information to generate bounding boxes for them
|
|
79
|
+
// and the graph
|
|
80
|
+
const { graphBox, nodeBoxes } = getBoundingBoxes(
|
|
81
|
+
nodes,
|
|
82
|
+
nodePadding,
|
|
83
|
+
gridRatio,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Internal: publish computed bounding box for debugging visualization
|
|
87
|
+
if (options.debug?.enabled && options.debug.setGraphBox) {
|
|
88
|
+
options.debug.setGraphBox({
|
|
89
|
+
x: graphBox.topLeft.x,
|
|
90
|
+
y: graphBox.topLeft.y,
|
|
91
|
+
width: graphBox.width,
|
|
92
|
+
height: graphBox.height,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const source: PointInfo = {
|
|
97
|
+
x: sourceX,
|
|
98
|
+
y: sourceY,
|
|
99
|
+
position: sourcePosition,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const target: PointInfo = {
|
|
103
|
+
x: targetX,
|
|
104
|
+
y: targetY,
|
|
105
|
+
position: targetPosition,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// With this information, we can create a 2D grid representation of
|
|
109
|
+
// our graph, that tells us where in the graph there is a "free" space or not
|
|
110
|
+
const { grid, start, end } = createGrid(
|
|
111
|
+
graphBox,
|
|
112
|
+
nodeBoxes,
|
|
113
|
+
source,
|
|
114
|
+
target,
|
|
115
|
+
gridRatio,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// We then can use the grid representation to do pathfinding
|
|
119
|
+
const generatePathResult = generatePath(grid, start, end);
|
|
120
|
+
|
|
121
|
+
const fullPath = generatePathResult;
|
|
122
|
+
|
|
123
|
+
// Here we convert the grid path to a sequence of graph coordinates.
|
|
124
|
+
const graphPath = fullPath.map((gridPoint) => {
|
|
125
|
+
const [x, y] = gridPoint;
|
|
126
|
+
const graphPoint = gridToGraphPoint(
|
|
127
|
+
{ x, y },
|
|
128
|
+
graphBox.xMin,
|
|
129
|
+
graphBox.yMin,
|
|
130
|
+
gridRatio,
|
|
131
|
+
);
|
|
132
|
+
return [graphPoint.x, graphPoint.y];
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Finally, we can use the graph path to draw the edge
|
|
136
|
+
const svgPathString = drawEdge(source, target, graphPath);
|
|
137
|
+
|
|
138
|
+
// Compute the edge's middle point using the full path, so users can use
|
|
139
|
+
// it to position their custom labels
|
|
140
|
+
const index = Math.floor(fullPath.length / 2);
|
|
141
|
+
const middlePoint = fullPath[index];
|
|
142
|
+
const [middleX, middleY] = middlePoint;
|
|
143
|
+
const { x: edgeCenterX, y: edgeCenterY } = gridToGraphPoint(
|
|
144
|
+
{ x: middleX, y: middleY },
|
|
145
|
+
graphBox.xMin,
|
|
146
|
+
graphBox.yMin,
|
|
147
|
+
gridRatio,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return { svgPathString, edgeCenterX, edgeCenterY };
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (error instanceof Error) {
|
|
153
|
+
return error;
|
|
154
|
+
} else {
|
|
155
|
+
return new Error(`Unknown error: ${String(error)}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export type GetSmartEdgeFunction = typeof getSmartEdge;
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { SmartBezierEdge } from "./SmartBezierEdge";
|
|
2
|
+
export { SmartStraightEdge } from "./SmartStraightEdge";
|
|
3
|
+
export { SmartStepEdge } from "./SmartStepEdge";
|
|
4
|
+
export { getSmartEdge } from "./getSmartEdge";
|
|
5
|
+
export {
|
|
6
|
+
svgDrawSmoothLinePath,
|
|
7
|
+
svgDrawStraightLinePath,
|
|
8
|
+
} from "./functions/drawSvgPath";
|
|
9
|
+
export {
|
|
10
|
+
pathfindingAStarDiagonal,
|
|
11
|
+
pathfindingAStarNoDiagonal,
|
|
12
|
+
} from "./functions/generatePath";
|
|
13
|
+
|
|
14
|
+
export type { GetSmartEdgeOptions } from "./getSmartEdge";
|
|
15
|
+
export type { SVGDrawFunction } from "./functions/drawSvgPath";
|
|
16
|
+
export type { PathFindingFunction } from "./functions/generatePath";
|
|
17
|
+
export type { Grid, GridNode } from "./pathfinding/grid";
|
|
18
|
+
export type { XYPosition } from "@xyflow/react";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import type { PropsWithChildren } from "react";
|
|
3
|
+
import type {
|
|
4
|
+
SmartEdgeGraphBox,
|
|
5
|
+
SmartEdgeDebugContextValue,
|
|
6
|
+
} from "./useSmartEdgeDebug";
|
|
7
|
+
import { SmartEdgeDebugContext } from "./useSmartEdgeDebug";
|
|
8
|
+
|
|
9
|
+
interface SmartEdgeDebugProviderProps {
|
|
10
|
+
value?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const SmartEdgeDebugProvider = ({
|
|
14
|
+
value = true,
|
|
15
|
+
children,
|
|
16
|
+
}: PropsWithChildren<SmartEdgeDebugProviderProps>) => {
|
|
17
|
+
const [graphBox, setGraphBoxState] = useState<SmartEdgeGraphBox>(null);
|
|
18
|
+
|
|
19
|
+
const setGraphBox = (next: SmartEdgeGraphBox) => {
|
|
20
|
+
setGraphBoxState((prev) => {
|
|
21
|
+
if (
|
|
22
|
+
prev?.x === next?.x &&
|
|
23
|
+
prev?.y === next?.y &&
|
|
24
|
+
prev?.width === next?.width &&
|
|
25
|
+
prev?.height === next?.height
|
|
26
|
+
) {
|
|
27
|
+
return prev;
|
|
28
|
+
}
|
|
29
|
+
return next;
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const contextValue = useMemo<SmartEdgeDebugContextValue>(
|
|
34
|
+
() => ({ enabled: value, graphBox, setGraphBox }),
|
|
35
|
+
[value, graphBox],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<SmartEdgeDebugContext.Provider value={contextValue}>
|
|
40
|
+
{children}
|
|
41
|
+
</SmartEdgeDebugContext.Provider>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { memo } from "react";
|
|
2
|
+
import type { CSSProperties } from "react";
|
|
3
|
+
import { useSmartEdgeDebug } from "./useSmartEdgeDebug";
|
|
4
|
+
|
|
5
|
+
export const SmartEdgeDebugOverlay = memo(() => {
|
|
6
|
+
const { enabled, graphBox } = useSmartEdgeDebug();
|
|
7
|
+
|
|
8
|
+
if (!enabled || !graphBox) return null;
|
|
9
|
+
|
|
10
|
+
const style: CSSProperties = {
|
|
11
|
+
position: "absolute",
|
|
12
|
+
left: graphBox.x,
|
|
13
|
+
top: graphBox.y,
|
|
14
|
+
width: graphBox.width,
|
|
15
|
+
height: graphBox.height,
|
|
16
|
+
pointerEvents: "none",
|
|
17
|
+
border: "1px solid red",
|
|
18
|
+
backgroundColor: "transparent",
|
|
19
|
+
boxSizing: "border-box",
|
|
20
|
+
zIndex: 1,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return <div style={style} data-testid="smart-edge-debug-overlay" />;
|
|
24
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
|
|
3
|
+
export type SmartEdgeGraphBox = {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
} | null;
|
|
9
|
+
|
|
10
|
+
export interface SmartEdgeDebugContextValue {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
graphBox: SmartEdgeGraphBox;
|
|
13
|
+
setGraphBox: (next: SmartEdgeGraphBox) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const SmartEdgeDebugContext = createContext<SmartEdgeDebugContextValue>({
|
|
17
|
+
enabled: false,
|
|
18
|
+
graphBox: null,
|
|
19
|
+
setGraphBox: () => {
|
|
20
|
+
// Do nothing
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const useSmartEdgeDebug = (): SmartEdgeDebugContextValue => {
|
|
25
|
+
return useContext(SmartEdgeDebugContext);
|
|
26
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Based on https://github.com/qiao/PathFinding.js
|
|
2
|
+
|
|
3
|
+
import type { Grid, GridNode } from "./grid";
|
|
4
|
+
import type { DiagonalMovement } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
export interface AStarOptions {
|
|
7
|
+
diagonalMovement?: DiagonalMovement;
|
|
8
|
+
heuristic?: (dx: number, dy: number) => number;
|
|
9
|
+
weight?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const manhattan = (dx: number, dy: number): number => dx + dy;
|
|
13
|
+
|
|
14
|
+
const octile = (dx: number, dy: number): number => {
|
|
15
|
+
const F = Math.SQRT2 - 1;
|
|
16
|
+
return dx < dy ? F * dx + dy : F * dy + dx;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const reconstructPath = (endNode: GridNode): number[][] => {
|
|
20
|
+
const path: number[][] = [];
|
|
21
|
+
let node: GridNode | undefined = endNode;
|
|
22
|
+
|
|
23
|
+
while (node) {
|
|
24
|
+
path.push([node.x, node.y]);
|
|
25
|
+
node = node.parent;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return path.reverse();
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getHeuristic = (
|
|
32
|
+
diagonalMovement: DiagonalMovement,
|
|
33
|
+
): ((dx: number, dy: number) => number) => {
|
|
34
|
+
if (diagonalMovement === "Never") return manhattan;
|
|
35
|
+
return octile;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const selectNodeWithLowestEstimatedTotalCost = (
|
|
39
|
+
openList: GridNode[],
|
|
40
|
+
): GridNode => {
|
|
41
|
+
let bestIdx = 0;
|
|
42
|
+
|
|
43
|
+
for (let i = 1; i < openList.length; i++) {
|
|
44
|
+
if (
|
|
45
|
+
(openList[i].estimatedTotalCost ?? Infinity) <
|
|
46
|
+
(openList[bestIdx].estimatedTotalCost ?? Infinity)
|
|
47
|
+
) {
|
|
48
|
+
bestIdx = i;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return openList.splice(bestIdx, 1)[0];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const processNeighbor = (
|
|
56
|
+
neighbor: GridNode,
|
|
57
|
+
current: GridNode,
|
|
58
|
+
end: GridNode,
|
|
59
|
+
openList: GridNode[],
|
|
60
|
+
heuristic: (dx: number, dy: number) => number,
|
|
61
|
+
weight: number,
|
|
62
|
+
): void => {
|
|
63
|
+
if (neighbor.closed) return;
|
|
64
|
+
|
|
65
|
+
const dx = Math.abs(neighbor.x - current.x);
|
|
66
|
+
const dy = Math.abs(neighbor.y - current.y);
|
|
67
|
+
|
|
68
|
+
const tentativeG =
|
|
69
|
+
(current.costFromStart ?? 0) + (dx === 0 || dy === 0 ? 1 : Math.SQRT2);
|
|
70
|
+
|
|
71
|
+
if (!neighbor.opened || tentativeG < (neighbor.costFromStart ?? Infinity)) {
|
|
72
|
+
neighbor.costFromStart = tentativeG;
|
|
73
|
+
|
|
74
|
+
neighbor.heuristicCostToGoal =
|
|
75
|
+
neighbor.heuristicCostToGoal ??
|
|
76
|
+
weight *
|
|
77
|
+
heuristic(Math.abs(neighbor.x - end.x), Math.abs(neighbor.y - end.y));
|
|
78
|
+
|
|
79
|
+
neighbor.estimatedTotalCost =
|
|
80
|
+
(neighbor.costFromStart ?? 0) + (neighbor.heuristicCostToGoal ?? 0);
|
|
81
|
+
|
|
82
|
+
neighbor.parent = current;
|
|
83
|
+
|
|
84
|
+
if (!neighbor.opened) {
|
|
85
|
+
neighbor.opened = true;
|
|
86
|
+
openList.push(neighbor);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const createAStarFinder = (opts: AStarOptions = {}) => {
|
|
92
|
+
const diagonalMovement: DiagonalMovement = opts.diagonalMovement ?? "Never";
|
|
93
|
+
const heuristic = opts.heuristic ?? getHeuristic(diagonalMovement);
|
|
94
|
+
const weight = opts.weight ?? 1;
|
|
95
|
+
|
|
96
|
+
const findPath = (
|
|
97
|
+
startX: number,
|
|
98
|
+
startY: number,
|
|
99
|
+
endX: number,
|
|
100
|
+
endY: number,
|
|
101
|
+
grid: Grid,
|
|
102
|
+
): number[][] => {
|
|
103
|
+
const start = grid.getNodeAt(startX, startY);
|
|
104
|
+
const end = grid.getNodeAt(endX, endY);
|
|
105
|
+
|
|
106
|
+
// Open list implemented as a simple array with linear min search for clarity
|
|
107
|
+
const openList: GridNode[] = [];
|
|
108
|
+
|
|
109
|
+
start.costFromStart = 0;
|
|
110
|
+
start.heuristicCostToGoal = 0;
|
|
111
|
+
start.estimatedTotalCost = 0;
|
|
112
|
+
start.opened = true;
|
|
113
|
+
openList.push(start);
|
|
114
|
+
|
|
115
|
+
while (openList.length > 0) {
|
|
116
|
+
const node = selectNodeWithLowestEstimatedTotalCost(openList);
|
|
117
|
+
node.closed = true;
|
|
118
|
+
|
|
119
|
+
if (node === end) {
|
|
120
|
+
return reconstructPath(end);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const neighbors = grid.getNeighbors(node, diagonalMovement);
|
|
124
|
+
for (const neighbor of neighbors) {
|
|
125
|
+
processNeighbor(neighbor, node, end, openList, heuristic, weight);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// no path found
|
|
130
|
+
return [];
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return { findPath };
|
|
134
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Based on https://github.com/qiao/PathFinding.js
|
|
2
|
+
import type { DiagonalMovement } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
// A modern, typed, functional replacement for PathFinding.js Grid
|
|
5
|
+
// Provides the same runtime API shape used by finders/utilities:
|
|
6
|
+
// - width, height, nodes[][]
|
|
7
|
+
// - getNodeAt, isWalkableAt, setWalkableAt, getNeighbors, clone
|
|
8
|
+
|
|
9
|
+
export interface GridNode {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
walkable: boolean;
|
|
13
|
+
// A* search metadata (set during pathfinding)
|
|
14
|
+
costFromStart?: number;
|
|
15
|
+
heuristicCostToGoal?: number;
|
|
16
|
+
estimatedTotalCost?: number;
|
|
17
|
+
opened?: boolean;
|
|
18
|
+
closed?: boolean;
|
|
19
|
+
parent?: GridNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Grid {
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
nodes: GridNode[][]; // nodes[row][col] i.e., nodes[y][x]
|
|
26
|
+
|
|
27
|
+
getNodeAt: (x: number, y: number) => GridNode;
|
|
28
|
+
isWalkableAt: (x: number, y: number) => boolean;
|
|
29
|
+
setWalkableAt: (x: number, y: number, walkable: boolean) => void;
|
|
30
|
+
getNeighbors: (
|
|
31
|
+
node: GridNode,
|
|
32
|
+
diagonalMovement: DiagonalMovement,
|
|
33
|
+
) => GridNode[];
|
|
34
|
+
isInside: (x: number, y: number) => boolean;
|
|
35
|
+
clone: () => Grid;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const createNodes = (
|
|
39
|
+
width: number,
|
|
40
|
+
height: number,
|
|
41
|
+
matrix?: (number | boolean)[][],
|
|
42
|
+
): GridNode[][] => {
|
|
43
|
+
const rows: GridNode[][] = new Array<GridNode[]>(height);
|
|
44
|
+
for (let y = 0; y < height; y++) {
|
|
45
|
+
const row: GridNode[] = new Array<GridNode>(width);
|
|
46
|
+
for (let x = 0; x < width; x++) {
|
|
47
|
+
// PathFinding.js semantics: a truthy matrix cell means non-walkable
|
|
48
|
+
// (e.g., 1 indicates obstacle). Falsy (0) means walkable.
|
|
49
|
+
const cell = matrix ? matrix[y]?.[x] : undefined;
|
|
50
|
+
const isBlocked = !!cell;
|
|
51
|
+
const walkable = matrix ? !isBlocked : true;
|
|
52
|
+
row[x] = { x, y, walkable };
|
|
53
|
+
}
|
|
54
|
+
rows[y] = row;
|
|
55
|
+
}
|
|
56
|
+
return rows;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const withinBounds = (width: number, height: number, x: number, y: number) =>
|
|
60
|
+
x >= 0 && x < width && y >= 0 && y < height;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create a grid with the given width/height. Optionally accepts a matrix
|
|
64
|
+
* of booleans/numbers indicating obstacles (truthy = blocked, falsy/0 = walkable).
|
|
65
|
+
*/
|
|
66
|
+
export const createGrid = (
|
|
67
|
+
width: number,
|
|
68
|
+
height: number,
|
|
69
|
+
matrix?: (number | boolean)[][],
|
|
70
|
+
): Grid => {
|
|
71
|
+
const nodes = createNodes(width, height, matrix);
|
|
72
|
+
|
|
73
|
+
const getNodeAt = (x: number, y: number): GridNode => nodes[y][x];
|
|
74
|
+
|
|
75
|
+
const isWalkableAt = (x: number, y: number): boolean =>
|
|
76
|
+
withinBounds(width, height, x, y) && nodes[y][x].walkable;
|
|
77
|
+
|
|
78
|
+
const setWalkableAt = (x: number, y: number, walkable: boolean): void => {
|
|
79
|
+
if (!withinBounds(width, height, x, y)) return;
|
|
80
|
+
nodes[y][x].walkable = walkable;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Diagonal movement policy using string literal union values:
|
|
84
|
+
// "Always", "Never", "IfAtMostOneObstacle", "OnlyWhenNoObstacles"
|
|
85
|
+
const getNeighbors = (
|
|
86
|
+
node: GridNode,
|
|
87
|
+
diagonalMovement: import("./types.ts").DiagonalMovement,
|
|
88
|
+
): GridNode[] => {
|
|
89
|
+
const x = node.x;
|
|
90
|
+
const y = node.y;
|
|
91
|
+
const neighbors: GridNode[] = [];
|
|
92
|
+
|
|
93
|
+
// ↑, →, ↓, ←
|
|
94
|
+
const s0 = isWalkableAt(x, y - 1);
|
|
95
|
+
const s1 = isWalkableAt(x + 1, y);
|
|
96
|
+
const s2 = isWalkableAt(x, y + 1);
|
|
97
|
+
const s3 = isWalkableAt(x - 1, y);
|
|
98
|
+
|
|
99
|
+
if (s0) neighbors.push(getNodeAt(x, y - 1));
|
|
100
|
+
if (s1) neighbors.push(getNodeAt(x + 1, y));
|
|
101
|
+
if (s2) neighbors.push(getNodeAt(x, y + 1));
|
|
102
|
+
if (s3) neighbors.push(getNodeAt(x - 1, y));
|
|
103
|
+
|
|
104
|
+
// Diagonals: ↗, ↘, ↙, ↖
|
|
105
|
+
const d0Walkable = isWalkableAt(x + 1, y - 1);
|
|
106
|
+
const d1Walkable = isWalkableAt(x + 1, y + 1);
|
|
107
|
+
const d2Walkable = isWalkableAt(x - 1, y + 1);
|
|
108
|
+
const d3Walkable = isWalkableAt(x - 1, y - 1);
|
|
109
|
+
|
|
110
|
+
if (diagonalMovement === "Never") {
|
|
111
|
+
return neighbors;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// default: "Always"
|
|
115
|
+
if (d0Walkable) neighbors.push(getNodeAt(x + 1, y - 1));
|
|
116
|
+
if (d1Walkable) neighbors.push(getNodeAt(x + 1, y + 1));
|
|
117
|
+
if (d2Walkable) neighbors.push(getNodeAt(x - 1, y + 1));
|
|
118
|
+
if (d3Walkable) neighbors.push(getNodeAt(x - 1, y - 1));
|
|
119
|
+
return neighbors;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const clone = (): Grid => {
|
|
123
|
+
// Recreate the original matrix semantics: truthy = blocked
|
|
124
|
+
const clonedMatrix: number[][] = nodes.map((row) =>
|
|
125
|
+
row.map((node) => (node.walkable ? 0 : 1)),
|
|
126
|
+
);
|
|
127
|
+
return createGrid(width, height, clonedMatrix);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
width,
|
|
132
|
+
height,
|
|
133
|
+
nodes,
|
|
134
|
+
getNodeAt,
|
|
135
|
+
isWalkableAt,
|
|
136
|
+
setWalkableAt,
|
|
137
|
+
getNeighbors,
|
|
138
|
+
isInside: (x: number, y: number) => withinBounds(width, height, x, y),
|
|
139
|
+
clone,
|
|
140
|
+
};
|
|
141
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useNodes, BezierEdge } from "@xyflow/react";
|
|
2
|
+
import { getSmartEdge } from "../getSmartEdge";
|
|
3
|
+
import type { EdgeProps } from "@xyflow/react";
|
|
4
|
+
|
|
5
|
+
const size = 20;
|
|
6
|
+
|
|
7
|
+
export function SmartEdgeCustomLabel(props: EdgeProps) {
|
|
8
|
+
const {
|
|
9
|
+
id,
|
|
10
|
+
sourcePosition,
|
|
11
|
+
targetPosition,
|
|
12
|
+
sourceX,
|
|
13
|
+
sourceY,
|
|
14
|
+
targetX,
|
|
15
|
+
targetY,
|
|
16
|
+
style,
|
|
17
|
+
markerStart,
|
|
18
|
+
markerEnd,
|
|
19
|
+
} = props;
|
|
20
|
+
|
|
21
|
+
const nodes = useNodes();
|
|
22
|
+
|
|
23
|
+
const getSmartEdgeResponse = getSmartEdge({
|
|
24
|
+
sourcePosition,
|
|
25
|
+
targetPosition,
|
|
26
|
+
sourceX,
|
|
27
|
+
sourceY,
|
|
28
|
+
targetX,
|
|
29
|
+
targetY,
|
|
30
|
+
nodes,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (getSmartEdgeResponse instanceof Error) {
|
|
34
|
+
return <BezierEdge {...props} />;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { edgeCenterX, edgeCenterY, svgPathString } = getSmartEdgeResponse;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
<path
|
|
42
|
+
style={style}
|
|
43
|
+
className="react-flow__edge-path"
|
|
44
|
+
d={svgPathString}
|
|
45
|
+
markerEnd={markerEnd}
|
|
46
|
+
markerStart={markerStart}
|
|
47
|
+
/>
|
|
48
|
+
<foreignObject
|
|
49
|
+
width={size}
|
|
50
|
+
height={size}
|
|
51
|
+
x={edgeCenterX - size / 2}
|
|
52
|
+
y={edgeCenterY - size / 2}
|
|
53
|
+
style={{
|
|
54
|
+
background: "transparent",
|
|
55
|
+
width: "minContent",
|
|
56
|
+
height: "minContent",
|
|
57
|
+
display: "flex",
|
|
58
|
+
justifyContent: "center",
|
|
59
|
+
alignItems: "center",
|
|
60
|
+
minHeight: "40px",
|
|
61
|
+
}}
|
|
62
|
+
requiredExtensions="http://www.w3.org/1999/xhtml"
|
|
63
|
+
>
|
|
64
|
+
<div
|
|
65
|
+
style={{
|
|
66
|
+
display: "flex",
|
|
67
|
+
justifyContent: "center",
|
|
68
|
+
alignItems: "center",
|
|
69
|
+
flexDirection: "column",
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
<button
|
|
73
|
+
style={{
|
|
74
|
+
width: "20px",
|
|
75
|
+
height: "20px",
|
|
76
|
+
background: "#eee",
|
|
77
|
+
border: "1px solid #fff",
|
|
78
|
+
cursor: "pointer",
|
|
79
|
+
borderRadius: "50%",
|
|
80
|
+
fontSize: "12px",
|
|
81
|
+
lineHeight: "1",
|
|
82
|
+
}}
|
|
83
|
+
onClick={() => {
|
|
84
|
+
alert(`Clicked on edge with id ${id}`);
|
|
85
|
+
}}
|
|
86
|
+
type="button"
|
|
87
|
+
>
|
|
88
|
+
x
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
</foreignObject>
|
|
92
|
+
</>
|
|
93
|
+
);
|
|
94
|
+
}
|