Move pathfinding into routinglib; call routinglib for pathfinding jobs

This commit is contained in:
Manuel Vögele
2022-09-30 21:52:27 +02:00
parent 4b30d52da1
commit c21e4c91d6
18 changed files with 94 additions and 1809 deletions
-50
View File
@@ -1,50 +0,0 @@
#!/usr/bin/env python3
import json
from pathlib import PurePath, Path
import subprocess
import tempfile
import zipfile
wasm_pack = Path("~/.cargo/bin/wasm-pack").expanduser()
root_files = ["module.json", "README.md", "CHANGELOG.md", "LICENSE"]
wasm_files = ["gridless_pathfinding_bg.wasm", "gridless_pathfinding.js"]
output_dir = Path("artifact")
copy_everything_directories = ["js", "lang", "templates"]
wasm_dir = Path("wasm")
root_dir = Path(".")
rust_dir = Path("rust")
build_dir_tmp = tempfile.TemporaryDirectory()
build_dir = Path(build_dir_tmp.name)
with open("module.json", "r") as file:
manifest = json.load(file)
zip_root = PurePath(f'{manifest["name"]}')
filename = f'{manifest["name"]}-{manifest["version"]}.zip'
result = subprocess.run([wasm_pack, "build", "--target", "web", "--out-dir", build_dir, root_dir / rust_dir])
if result.returncode != 0:
raise Exception("Wasm build failed")
output_dir.mkdir(parents=True, exist_ok=True)
def write_directory(archive, d):
for f in (root_dir / d).iterdir():
if f.is_dir():
write_directory(archive, f)
else:
assert(f.is_file())
archive.write(f, arcname=zip_root / d / f.name)
with zipfile.ZipFile(output_dir / filename, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
for f in root_files:
archive.write(root_dir / f, arcname=zip_root / f)
for d in copy_everything_directories:
write_directory(archive, d)
for f in wasm_files:
archive.write(build_dir / f, arcname=zip_root / wasm_dir / f)
print(f"Successfully built {output_dir / filename}")
-13
View File
@@ -1,13 +0,0 @@
#!/usr/bin/env python3
import sys
import subprocess
from pathlib import Path
root_dir = Path(".")
wasm_dir = root_dir / Path("wasm")
rust_dir = root_dir / Path("rust")
debug = " --debug" if len(sys.argv) >= 2 and sys.argv[1] == "--debug" else ""
result = subprocess.run(["cargo", "watch", "-C" , rust_dir, "-s", f"wasm-pack build --target web --out-dir {wasm_dir.resolve()}{debug}"])
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
cargo install cargo-watch
cargo install wasm-pack
+10 -8
View File
@@ -65,14 +65,16 @@ export function registerKeybindings() {
precedence: -1, precedence: -1,
}); });
game.keybindings.register(settingsKey, "togglePathfinding", { if (game.modules.get("routinglib")?.active) {
name: "drag-ruler.keybindings.togglePathfinding.name", game.keybindings.register(settingsKey, "togglePathfinding", {
hint: "drag-ruler.keybindings.togglePathfinding.hint", name: "drag-ruler.keybindings.togglePathfinding.name",
onDown: handleTogglePathfinding, hint: "drag-ruler.keybindings.togglePathfinding.hint",
onUp: handleTogglePathfinding, onDown: handleTogglePathfinding,
precedence: -1, onUp: handleTogglePathfinding,
restricted: !game.settings.get(settingsKey, "allowPathfinding"), precedence: -1,
}); restricted: !game.settings.get(settingsKey, "allowPathfinding"),
});
}
} }
function handleDeleteWaypoint() { function handleDeleteWaypoint() {
-14
View File
@@ -14,29 +14,15 @@ import {disableSnap, registerKeybindings} from "./keybindings.js";
import {libWrapper} from "./libwrapper_shim.js"; import {libWrapper} from "./libwrapper_shim.js";
import {performMigrations} from "./migration.js"; import {performMigrations} from "./migration.js";
import {removeLastHistoryEntryIfAt, resetMovementHistory} from "./movement_tracking.js"; import {removeLastHistoryEntryIfAt, resetMovementHistory} from "./movement_tracking.js";
import {wipePathfindingCache, initializePathfinding} from "./pathfinding.js";
import {extendRuler} from "./ruler.js"; import {extendRuler} from "./ruler.js";
import {registerSettings, RightClickAction, settingsKey} from "./settings.js"; import {registerSettings, RightClickAction, settingsKey} from "./settings.js";
import {recalculate} from "./socket.js"; import {recalculate} from "./socket.js";
import {SpeedProvider} from "./speed_provider.js"; import {SpeedProvider} from "./speed_provider.js";
import {setSnapParameterOnOptions} from "./util.js"; import {setSnapParameterOnOptions} from "./util.js";
import initGridlessPathfinding, * as GridlessPathfinding from "../wasm/gridless_pathfinding.js";
CONFIG.debug.dragRuler = false; CONFIG.debug.dragRuler = false;
export let debugGraphics = undefined; export let debugGraphics = undefined;
initGridlessPathfinding().then(() => {
Hooks.on("canvasInit", wipePathfindingCache);
Hooks.on("canvasReady", () => {
wipePathfindingCache();
initializePathfinding();
});
Hooks.on("createWall", wipePathfindingCache);
Hooks.on("updateWall", wipePathfindingCache);
Hooks.on("deleteWall", wipePathfindingCache);
});
Hooks.once("init", () => { Hooks.once("init", () => {
registerSettings(); registerSettings();
registerKeybindings(); registerKeybindings();
-539
View File
@@ -1,539 +0,0 @@
import {
getCenterFromGridPositionObj,
getGridPositionFromPixelsObj,
getPixelsFromGridPositionObj,
} from "./foundry_fixes.js";
import {moveWithoutAnimation, togglePathfinding} from "./keybindings.js";
import {debugGraphics} from "./main.js";
import {settingsKey} from "./settings.js";
import {
buildSnapPointTokenData,
getSnapPointForTokenDataObj,
getTokenShape,
getTokenShapeForTokenData,
isModuleActive,
iterPairs,
} from "./util.js";
import * as GridlessPathfinding from "../wasm/gridless_pathfinding.js";
import {PriorityQueueSet, ProcessOnceQueue} from "./data_structures.js";
import {buildCostFunction} from "./api.js";
class CacheLayer {
constructor(tokenData, cacheId) {
this.tokenData = tokenData;
this.cacheId = cacheId;
this.queue = new ProcessOnceQueue();
this.buildNodes();
this.registerUse();
}
buildNodes() {
this.nodes = new Array(gridHeight);
for (let y = 0; y < gridHeight; y++) {
this.nodes[y] = new Array(gridWidth);
for (let x = 0; x < gridWidth; x++) {
this.nodes[y][x] = {x, y};
}
}
}
registerUse() {
this.lastUsed = Date.now();
}
}
/**
* Class to hold all the cached node data, and functions to deal with caching.
*
* Since pathfinding can depend on several factors, e.g. the token's size, we keep
* several caches, keyed by all the data relevant to pathfinding. If we already have
* the maximum number of caches and we need to create another one, we discard the
* one not used for the longest.
*
* When we select a token, or a token we have selected updates, we start caching
* in the background so, when we do start pathfinding, it's very performant.
*
* Background caching starts by trying to run an idle process (when the browser is
* otherwise not busy), but if it can't do that after an amount of time (e.g. the
* CPU is very slow and is busy) then we instead start caching a few nodes each
* frame.
*/
class Cache {
static maxCacheLayers = 5;
static maxBackgroundCachingMillis = 10;
static maxAnimationCachingMillis = 5;
static backgroundCachingTimeoutMillis = 200;
constructor() {
this.layers = new Map();
this.background = {
nextJobId: null,
nextTimeoutId: null,
nextAnimationFrameId: null,
};
}
clear() {
this.layers.clear();
if (this.background.nextJobId) {
window.cancelIdleCallback(this.background.nextJobId);
this.background.nextJobId = null;
}
this.cancelTimeout();
this.cancelAnimationFrame();
}
/**
* Retrieve the cache layer for this token, using information that can make a difference to the pathfinding algorithm
* If a layer that suits this token doesn't exist, create one
*/
getCacheLayer(token) {
const tokenData = buildTokenData(token);
// TODO Request this from the speed providers so they can set their own options
let terrainData = canvas.terrain.listAllTerrain({token});
terrainData = terrainData.map(data => {
return {
x: data.object.x,
y: data.object.y,
cost: data.cost,
shape: data.shape,
};
});
const cacheIdData = {tokenData, terrainData};
const cacheId = GridlessPathfinding.sha1(JSON.stringify(cacheIdData));
let cacheLayer = this.layers.get(cacheId);
// If we don't already have a cache layer for this cache ID, create one now
if (!cacheLayer) {
// Check if we already have the max number of layers. If we do,
// get rid of the one that hasn't been used for the longest
if (this.layers.size >= Cache.maxCacheLayers) {
const oldestCache = Array.from(this.layers.values()).reduce(
(layer1, layer2) => (layer1?.lastUsed < layer2.lastUsed ? layer1 : layer2),
null,
);
this.layers.delete(oldestCache.cacheId);
}
// Create the new cache
cacheLayer = new CacheLayer(tokenData, cacheId);
this.layers.set(cacheId, cacheLayer);
} else {
// Register that we're using this cache right now
cacheLayer.registerUse();
}
return cacheLayer;
}
/**
* Start background caching from the token's current position
*/
startBackgroundCaching(token) {
const cacheLayer = this.getCacheLayer(token);
const tokenPosition = getGridPositionFromPixelsObj(token.position);
cacheLayer.queue.push(cacheLayer.nodes[tokenPosition.y][tokenPosition.x]);
this.scheduleBackgroundCache();
}
/**
* Find if any of the caches have more nodes to background cache. If there is, then schedule a background
* caching job for that queue
*/
scheduleBackgroundCache() {
// If we already have a nextJobId, then don't start another one
if (this.background.nextJobId) return;
// Find the latest-used cache that has nodes left to cache
const latestCache = this.getLatestCacheWithNonEmptyQueue();
if (latestCache) {
this.background.nextJobId = window.requestIdleCallback(() =>
this.runBackgroundCache(latestCache),
);
this.resetAnimationFrameTimeout();
}
}
/**
* Start a timeout which, if we reach the timeout time, will schedule a small amount of caching
* to be performed every frame. This timeout will be reset every time we perform background caching.
*/
resetAnimationFrameTimeout() {
this.cancelTimeout();
this.cancelAnimationFrame();
this.background.nextTimeoutId = window.setTimeout(() => {
this.scheduleAnimationFrameCache();
this.background.nextTimeoutId = null;
}, Cache.backgroundCachingTimeoutMillis);
}
/**
* Schedule a small amount of caching to be done just before the next frame renders
*/
scheduleAnimationFrameCache() {
const latestCache = this.getLatestCacheWithNonEmptyQueue();
if (latestCache) {
this.background.nextAnimationFrameId = window.requestAnimationFrame(() =>
this.runAnimationCache(latestCache),
);
}
}
/**
* Find which cache was last used and get its cache ID
*/
getLatestCacheWithNonEmptyQueue() {
return Array.from(this.layers.values())
.filter(layer => layer.queue.hasNext())
.reduce((layer1, layer2) => (layer1?.lastUsed > layer2.lastUsed ? layer1 : layer2), null);
}
/**
* Cache nodes for a short time, and then schedule another idle job to cache more nodes
*/
runBackgroundCache(cacheLayer) {
const endTime = performance.now() + Cache.maxBackgroundCachingMillis;
while (cacheLayer.queue.hasNext() && performance.now() < endTime) {
this.cacheNextNode(cacheLayer);
}
this.background.nextJobId = null;
this.scheduleBackgroundCache();
}
/**
* Cache nodes for a very short time, then schedule to cache more nodes next frame
*/
runAnimationCache(cacheLayer) {
const endTime = performance.now() + Cache.maxAnimationCachingMillis;
while (cacheLayer.queue.hasNext() && performance.now() < endTime) {
this.cacheNextNode(cacheLayer);
}
this.background.nextAnimationFrameId = null;
this.scheduleAnimationFrameCache();
}
cacheNextNode(cacheLayer) {
let node = cacheLayer.queue.pop();
getNode(node, cacheLayer);
for (let edge of node.edges) {
cacheLayer.queue.push(edge.target);
}
}
cancelTimeout() {
if (this.background.nextTimeoutId) {
window.clearTimeout(this.background.nextTimeoutId);
this.background.nextTimeoutId = null;
}
}
cancelAnimationFrame() {
if (this.background.nextAnimationFrameId) {
window.cancelAnimationFrame(this.background.nextAnimationFrameId);
this.background.nextAnimationFrameId = null;
}
}
}
const cache = new Cache();
let use5105 = false;
let gridlessPathfinders = new Map();
let gridWidth, gridHeight;
export function isPathfindingEnabled() {
if (this.user !== game.user) return false;
if (!game.user.isGM && !game.settings.get(settingsKey, "allowPathfinding")) return false;
if (moveWithoutAnimation) return false;
return game.settings.get(settingsKey, "autoPathfinding") != togglePathfinding;
}
export function findPath(from, to, token, previousWaypoints) {
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
let tokenSize = Math.max(token.data.width, token.data.height) * canvas.dimensions.size;
let pathfinder = gridlessPathfinders.get(tokenSize);
if (!pathfinder) {
let radiusMultiplier = game.settings.get(settingsKey, "pathfindingRadius");
pathfinder = GridlessPathfinding.initialize(
canvas.walls.placeables,
tokenSize * radiusMultiplier,
token.data.elevation,
Boolean(game.modules.get("wall-height")?.active),
);
gridlessPathfinders.set(tokenSize, pathfinder);
}
paintGridlessPathfindingDebug(pathfinder);
return GridlessPathfinding.findPath(pathfinder, from, to);
} else {
const cacheLayer = cache.getCacheLayer(token);
const firstNode = calculatePath(from, to, cacheLayer, previousWaypoints);
if (!firstNode) return null;
paintGriddedPathfindingDebug(firstNode, cacheLayer.tokenData);
const path = [];
let currentNode = firstNode;
while (currentNode) {
if (
path.length >= 2 &&
!stepCollidesWithWall(path[path.length - 2], currentNode.node, cacheLayer.tokenData)
) {
// Replace last waypoint if the current waypoint leads to a valid path that isn't longer than the old path
if (window.terrainRuler) {
let startNode = getCenterFromGridPositionObj(path[path.length - 2]);
let middleNode = getCenterFromGridPositionObj(path[path.length - 1]);
let endNode = getCenterFromGridPositionObj(currentNode.node);
let oldPath = [
{ray: new Ray(startNode, middleNode)},
{ray: new Ray(middleNode, endNode)},
];
let newPath = [{ray: new Ray(startNode, endNode)}];
let costFunction = buildCostFunction(token, getTokenShape(token));
// TODO Cache the used measurement for use in the next loop to improve performance
let oldDistance = terrainRuler
.measureDistances(oldPath, {costFunction})
.reduce((a, b) => a + b);
let newDistance = terrainRuler.measureDistances(newPath, {costFunction})[0];
// TODO We might need to check if the diagonal count has increased on 5-10-5
if (newDistance < oldDistance) {
path.pop();
} else if (newDistance === oldDistance) {
let oldNoDiagonals = oldPath[1].ray.terrainRulerFinalState?.noDiagonals;
let newNoDiagonals = newPath[0].ray.terrainRulerFinalState?.noDiagonals;
// This uses === && < instead of <= because the variables might be undefined (which shall lead to a true result)
if (oldNoDiagonals === newNoDiagonals || newNoDiagonals < oldNoDiagonals) {
path.pop();
}
}
} else {
path.pop();
}
}
path.push({x: currentNode.node.x, y: currentNode.node.y});
currentNode = currentNode.next;
}
return path;
}
}
function buildTokenData(token) {
// Almost all the information we need is for calculating the snap point
const tokenData = buildSnapPointTokenData(token);
// If Wall Height is enabled, which walls matter depends on the token's elevation.
// Depending on the settings in Wall Height, the height we care about is either their
// foot height (elevation) or eye height (losHeight).
if (isModuleActive("wall-height")) {
const blockSightMovement = game.settings.get("wall-height", "blockSightMovement");
tokenData.elevation = blockSightMovement ? token.losHeight : token.data.elevation;
}
return tokenData;
}
function getNode(pos, cacheLayer, initialize = true) {
const node = cacheLayer.nodes[pos.y][pos.x];
if (initialize && !node.edges) {
node.edges = [];
for (const neighborPos of canvas.grid.grid.getNeighbors(pos.y, pos.x).map(([y, x]) => {
return {x, y};
})) {
if (
neighborPos.x < 0 ||
neighborPos.y < 0 ||
neighborPos.x >= gridWidth ||
neighborPos.y >= gridHeight
) {
continue;
}
// TODO Work with pixels instead of grid locations
if (!stepCollidesWithWall(pos, neighborPos, cacheLayer.tokenData)) {
const isDiagonal =
node.x !== neighborPos.x &&
node.y !== neighborPos.y &&
canvas.grid.type === CONST.GRID_TYPES.SQUARE;
let edgeCost;
if (window.terrainRuler) {
let ray = new Ray(
getCenterFromGridPositionObj(pos),
getCenterFromGridPositionObj(neighborPos),
);
let measuredDistance = terrainRuler.measureDistances([{ray}], {
costFunction: buildCostFunction(
cacheLayer.tokenData,
getTokenShapeForTokenData(cacheLayer.tokenData),
),
})[0];
edgeCost = Math.round(measuredDistance / canvas.dimensions.distance);
if (ray.terrainRulerFinalState?.noDiagonals === 1) {
edgeCost = 1.5;
}
// Charge 1.0001 instead of 1 for diagonals to discourage unnecessary diagonals
if (isDiagonal && edgeCost == 1) {
edgeCost = 1.0001;
}
} else {
// Count 5-10-5 diagonals as 1.5 (so two add up to 3) and 5-5-5 diagonals as 1.0001 (to discourage unnecessary diagonals)
// TODO Account for difficult terrain
edgeCost = isDiagonal ? (use5105 ? 1.5 : 1.0001) : 1;
}
const neighbor = getNode(neighborPos, cacheLayer, false);
node.edges.push({target: neighbor, cost: edgeCost});
}
}
}
return node;
}
function calculatePath(from, to, cacheLayer, previousWaypoints) {
use5105 = game.system.id === "pf2e" || canvas.grid.diagonalRule === "5105";
let startCost = 0;
if (use5105 && canvas.grid.type === CONST.GRID_TYPES.SQUARE) {
previousWaypoints = previousWaypoints.map(w => getGridPositionFromPixelsObj(w));
startCost = (calcNoDiagonals(previousWaypoints) % 2) * 0.5;
}
const nextNodes = new PriorityQueueSet(
(node1, node2) => node1.node === node2.node,
node => node.estimated,
);
const previousNodes = new Set();
nextNodes.pushWithPriority({
node: getNode(from, cacheLayer),
cost: startCost,
estimated: startCost + estimateCost(from, to),
previous: null,
});
while (nextNodes.hasNext()) {
// Get node with cheapest estimate
const currentNode = nextNodes.pop();
if (currentNode.node.x === to.x && currentNode.node.y === to.y) {
return buildPathNodes(currentNode);
}
previousNodes.add(currentNode.node);
for (const edge of currentNode.node.edges) {
const neighborNode = getNode(edge.target, cacheLayer);
if (previousNodes.has(neighborNode)) {
continue;
}
const neighbor = {
node: neighborNode,
cost: currentNode.cost + edge.cost,
estimated: currentNode.cost + edge.cost + estimateCost(neighborNode, to),
previous: currentNode,
};
nextNodes.pushWithPriority(neighbor);
}
}
}
/**
* Now we've found the path, we know the final node, and each node links to the previous one.
* Reverse this list and return the first node in the path, with each node linking to the next
*/
function buildPathNodes(lastNode) {
let currentNode = lastNode;
let previousNode = null;
while (currentNode) {
const pathNode = {
node: currentNode.node,
cost: currentNode.cost,
next: previousNode,
};
previousNode = pathNode;
currentNode = currentNode.previous;
}
return previousNode;
}
function calcNoDiagonals(waypoints) {
let diagonals = 0;
for (const [p1, p2] of iterPairs(waypoints)) {
diagonals += Math.min(Math.abs(p1.x - p2.x), Math.abs(p1.y - p2.y));
}
return diagonals;
}
/**
* Estimate the travel distance between two points, as the crow flies. Most of the time, this is 1
* per space, but for a square grid using 5-10-5 diagonals, count each diagonal as an extra 0.5
*/
function estimateCost(pos, target) {
const distX = Math.abs(pos.x - target.x);
const distY = Math.abs(pos.y - target.y);
return Math.max(distX, distY) + (use5105 ? Math.min(distX, distY) * 0.5 : 0);
}
function stepCollidesWithWall(from, to, tokenData) {
const stepStart = getSnapPointForTokenDataObj(getPixelsFromGridPositionObj(from), tokenData);
const stepEnd = getSnapPointForTokenDataObj(getPixelsFromGridPositionObj(to), tokenData);
if (isModuleActive("levels")) {
stepStart.z = tokenData.elevation;
stepEnd.z = tokenData.elevation;
return _levels.testCollision(stepStart, stepEnd, "collision");
} else {
return canvas.walls.checkCollision(new Ray(stepStart, stepEnd));
}
}
export function wipePathfindingCache() {
cache.clear();
for (const pathfinder of gridlessPathfinders.values()) {
GridlessPathfinding.free(pathfinder);
}
gridlessPathfinders.clear();
if (debugGraphics) debugGraphics.removeChildren().forEach(c => c.destroy());
}
export function initializePathfinding() {
gridWidth = Math.ceil(canvas.dimensions.width / canvas.grid.w);
gridHeight = Math.ceil(canvas.dimensions.height / canvas.grid.h);
}
export function startBackgroundCaching(token) {
// Background caching isn't yet supported for gridless scenes
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) return;
if (game.user.isGM || game.settings.get(settingsKey, "allowPathfinding")) {
cache.startBackgroundCaching(token);
}
}
function paintGriddedPathfindingDebug(firstNode, tokenData) {
if (!CONFIG.debug.dragRuler) return;
debugGraphics.removeChildren().forEach(c => c.destroy());
let currentNode = firstNode;
while (currentNode) {
let text = new PIXI.Text(currentNode.cost.toFixed(1));
let pixels = getSnapPointForTokenDataObj(
getPixelsFromGridPositionObj(currentNode.node),
tokenData,
);
text.anchor.set(0.5, 1.0);
text.x = pixels.x;
text.y = pixels.y;
debugGraphics.addChild(text);
currentNode = currentNode.next;
}
}
function paintGridlessPathfindingDebug(pathfinder) {
if (!CONFIG.debug.dragRuler) return;
debugGraphics.removeChildren().forEach(c => c.destroy());
let graphic = new PIXI.Graphics();
graphic.lineStyle(2, 0x440000);
for (const point of GridlessPathfinding.debugGetPathfindingPoints(pathfinder)) {
graphic.drawCircle(point.x, point.y, 5);
}
debugGraphics.addChild(graphic);
}
+55 -2
View File
@@ -8,11 +8,18 @@ import {
highlightMeasurementTerrainRuler, highlightMeasurementTerrainRuler,
measureDistances, measureDistances,
} from "./compatibility.js"; } from "./compatibility.js";
import {getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
import {cancelScheduledMeasurement, highlightMeasurementNative} from "./foundry_imports.js"; import {cancelScheduledMeasurement, highlightMeasurementNative} from "./foundry_imports.js";
import {disableSnap} from "./keybindings.js"; import {disableSnap} from "./keybindings.js";
import {getMovementHistory} from "./movement_tracking.js"; import {getMovementHistory} from "./movement_tracking.js";
import {settingsKey} from "./settings.js"; import {settingsKey} from "./settings.js";
import {applyTokenSizeOffset, getSnapPointForEntity, getTokenShape} from "./util.js"; import {
applyTokenSizeOffset,
getSnapPointForEntity,
getSnapPointForTokenObj,
getTokenShape,
isPathfindingEnabled,
} from "./util.js";
export function extendRuler() { export function extendRuler() {
class DragRulerRuler extends CONFIG.Canvas.rulerClass { class DragRulerRuler extends CONFIG.Canvas.rulerClass {
@@ -102,6 +109,52 @@ export function extendRuler() {
const d = this._getMeasurementDestination(destination); const d = this._getMeasurementDestination(destination);
if (d.x === this.destination.x && d.y === this.destination.y) return; if (d.x === this.destination.x && d.y === this.destination.y) return;
this.destination = d; this.destination = d;
// TODO Cancel running pathfinding operations
// TODO Check if we can reuse the old path
this.dragRulerRemovePathfindingWaypoints();
if (isToken && isPathfindingEnabled.call(this)) {
// TODO Show a busy indicator
const from = getGridPositionFromPixelsObj(this.waypoints[this.waypoints.length - 1]);
const to = getGridPositionFromPixelsObj(destination);
return routinglib
.calculatePath(from, to, {token: this.draggedEntity})
.then(result => this.addPathToWaypoints(result.path))
.then(() => this.performPostPathfindingActions(options));
}
return this.performPostPathfindingActions(options);
}
addPathToWaypoints(path) {
path = path.map(point =>
getSnapPointForTokenObj(getPixelsFromGridPositionObj(point), this.draggedEntity),
);
// If the token is snapped to the grid, the first point of the path is already handled by the ruler
if (
path[0].x === this.waypoints[this.waypoints.length - 1].x &&
path[0].y === this.waypoints[this.waypoints.length - 1].y
) {
path = path.slice(1);
}
// If snapping is enabled, the last point of the path is already handled by the ruler
if (this.dragRulerSnap) {
path = path.slice(0, path.length - 1);
}
for (const point of path) {
point.isPathfinding = true;
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
}
this.waypoints = this.waypoints.concat(path);
}
performPostPathfindingActions(options) {
// TODO Clear pathfinding busy indicator
this.segments = this._getMeasurementSegments(); this.segments = this._getMeasurementSegments();
this._computeDistance(options.gridSpaces); this._computeDistance(options.gridSpaces);
@@ -111,6 +164,7 @@ export function extendRuler() {
// Draw grid highlight // Draw grid highlight
this.highlightLayer.clear(); this.highlightLayer.clear();
const isToken = this.draggedEntity instanceof Token;
if (isToken && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS && this.dragRulerGridSpaces) { if (isToken && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS && this.dragRulerGridSpaces) {
const shape = getTokenShape(this.draggedEntity); const shape = getTokenShape(this.draggedEntity);
if (!this.dragRulerEnableTerrainRuler) { if (!this.dragRulerEnableTerrainRuler) {
@@ -154,7 +208,6 @@ export function extendRuler() {
} }
_getMeasurementSegments() { _getMeasurementSegments() {
// TODO Recalculate pathfinding, if necessary
if (this.isDragRuler) { if (this.isDragRuler) {
const unsnappedWaypoints = this.waypoints.concat([this.destination]); const unsnappedWaypoints = this.waypoints.concat([this.destination]);
const waypoints = const waypoints =
+19 -26
View File
@@ -5,7 +5,6 @@ import {
updateSpeedProvider, updateSpeedProvider,
} from "./api.js"; } from "./api.js";
import {SpeedProvider} from "./speed_provider.js"; import {SpeedProvider} from "./speed_provider.js";
import {wipePathfindingCache} from "./pathfinding.js";
import {early_isGM} from "./util.js"; import {early_isGM} from "./util.js";
export const settingsKey = "drag-ruler"; export const settingsKey = "drag-ruler";
@@ -93,32 +92,26 @@ export function registerSettings() {
default: true, default: true,
}); });
game.settings.register(settingsKey, "allowPathfinding", { if (game.modules.get("routinglib")?.active) {
name: "drag-ruler.settings.allowPathfinding.name", game.settings.register(settingsKey, "allowPathfinding", {
hint: "drag-ruler.settings.allowPathfinding.hint", name: "drag-ruler.settings.allowPathfinding.name",
scope: "world", hint: "drag-ruler.settings.allowPathfinding.hint",
config: true, scope: "world",
type: Boolean, config: true,
default: false, type: Boolean,
onChange: delayedReload, default: false,
}); onChange: delayedReload,
});
game.settings.register(settingsKey, "autoPathfinding", { game.settings.register(settingsKey, "autoPathfinding", {
name: "drag-ruler.settings.autoPathfinding.name", name: "drag-ruler.settings.autoPathfinding.name",
hint: "drag-ruler.settings.autoPathfinding.hint", hint: "drag-ruler.settings.autoPathfinding.hint",
scope: "client", scope: "client",
config: early_isGM() || game.settings.get(settingsKey, "allowPathfinding"), config: early_isGM() || game.settings.get(settingsKey, "allowPathfinding"),
type: Boolean, type: Boolean,
default: false, default: false,
}); });
}
game.settings.register(settingsKey, "pathfindingRadius", {
scope: "world",
config: false,
type: Number,
default: 0.9,
onChange: wipePathfindingCache,
});
game.settings.register(settingsKey, "lastTerrainRulerHintTime", { game.settings.register(settingsKey, "lastTerrainRulerHintTime", {
config: false, config: false,
+10 -1
View File
@@ -1,6 +1,7 @@
import {getPixelsFromGridPosition} from "./foundry_fixes.js"; import {getPixelsFromGridPosition} from "./foundry_fixes.js";
import {findVertexSnapPoint} from "./hex_support.js"; import {findVertexSnapPoint} from "./hex_support.js";
import {disableSnap} from "./keybindings.js"; import {disableSnap, moveWithoutAnimation, togglePathfinding} from "./keybindings.js";
import {settingsKey} from "./settings.js";
export function* zip(it1, it2) { export function* zip(it1, it2) {
for (let i = 0; i < Math.min(it1.length, it2.length); i++) { for (let i = 0; i < Math.min(it1.length, it2.length); i++) {
@@ -292,3 +293,11 @@ export function early_isGM() {
export function isModuleActive(moduleName) { export function isModuleActive(moduleName) {
return game.modules.get(moduleName)?.active; return game.modules.get(moduleName)?.active;
} }
export function isPathfindingEnabled() {
if (!window.routinglib) return false;
if (this.user !== game.user) return false;
if (!game.user.isGM && !game.settings.get(settingsKey, "allowPathfinding")) return false;
if (moveWithoutAnimation) return false;
return game.settings.get(settingsKey, "autoPathfinding") != togglePathfinding;
}
-1
View File
@@ -1 +0,0 @@
target/
-232
View File
@@ -1,232 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "block-buffer"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if",
"wasm-bindgen",
]
[[package]]
name = "cpufeatures"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "generic-array"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "gridless-pathfinding"
version = "1.12.8"
dependencies = [
"console_error_panic_hook",
"js-sys",
"rustc-hash",
"sha1",
"wasm-bindgen",
]
[[package]]
name = "js-sys"
version = "0.3.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b"
[[package]]
name = "log"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
]
[[package]]
name = "proc-macro2"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "sha1"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77f4e7f65455545c2153c1253d25056825e77ee2533f0e41deb65a93a34852f"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "syn"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "typenum"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasm-bindgen"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca"
dependencies = [
"bumpalo",
"lazy_static",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2"
-20
View File
@@ -1,20 +0,0 @@
[package]
name = "gridless-pathfinding"
version = "1.12.8"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib"]
[profile.release]
lto = true
#debug = true
[dependencies]
console_error_panic_hook = "0.1.7"
js-sys = "0.3.56"
rustc-hash = "1.1.0"
sha1 = "0.10.1"
wasm-bindgen = "0.2.79"
-1
View File
@@ -1 +0,0 @@
hard_tabs = true
-187
View File
@@ -1,187 +0,0 @@
use std::hash::{Hash, Hasher};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
pub type JsPoint;
#[wasm_bindgen(method, getter)]
fn x(this: &JsPoint) -> f64;
#[wasm_bindgen(method, getter)]
fn y(this: &JsPoint) -> f64;
}
#[wasm_bindgen]
#[derive(Debug, Copy, Clone)]
pub struct Point {
pub x: f64,
pub y: f64,
}
impl Point {
pub fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
pub fn from_line_x(line: &Line, x: f64) -> Self {
let y = line.calc_y(x);
Self { x, y }
}
pub fn distance_to(&self, to: Point) -> f64 {
(self.y - to.y).hypot(self.x - to.x)
}
pub fn is_same_as(&self, other: &Self) -> bool {
let e = 0.000001;
(self.x - other.x).abs() < e && (self.y - other.y).abs() < e
}
}
impl Eq for Point {}
impl PartialEq for Point {
fn eq(&self, other: &Self) -> bool {
self.x == other.x && self.y == other.y
}
}
impl Hash for Point {
fn hash<H: Hasher>(&self, hasher: &mut H) {
self.x.to_bits().hash(hasher);
self.y.to_bits().hash(hasher);
}
}
impl From<&JsPoint> for Point {
fn from(point: &JsPoint) -> Self {
Self::new(point.x(), point.y())
}
}
#[derive(Debug, Copy, Clone)]
pub struct Line {
pub m: f64,
pub b: f64,
pub p1: Point,
}
impl Line {
pub fn new(m: f64, b: f64, p1: Point) -> Self {
Self { m, b, p1 }
}
pub fn from_points(p1: Point, p2: Point) -> Self {
let m = (p1.y - p2.y) / (p1.x - p2.x);
let b = p1.y - m * p1.x;
Self { m, b, p1 }
}
pub fn from_point_and_angle(p1: Point, angle: f64) -> Self {
let p2 = Point {
x: p1.x - angle.cos(),
y: p1.y - angle.sin(),
};
Line::from_points(p1, p2)
}
pub fn is_vertical(&self) -> bool {
self.m.is_infinite()
}
pub fn is_horizontal(&self) -> bool {
self.m == 0.0
}
pub fn calc_x(&self, y: f64) -> f64 {
(y - self.b) / self.m
}
pub fn calc_y(&self, x: f64) -> f64 {
self.m * x + self.b
}
pub fn intersection(&self, other: &Line) -> Option<Point> {
// Are both lines vertical?
if self.is_vertical() && other.is_vertical() {
return None;
}
// Are the lines paralell?
if (self.m - other.m).abs() < 0.00000005 {
return None;
}
// Is one of the lines vertical?
if self.is_vertical() || other.is_vertical() {
let vertical;
let regular;
if self.is_vertical() {
vertical = self;
regular = other;
} else {
vertical = other;
regular = self;
}
return Some(Point::from_line_x(regular, vertical.p1.x));
}
// Calculate x coordinate of intersection point between both lines
// Find intersection point: x * m1 + b1 = x * m2 + b2
// Solve for x: x = (b1 - b2) / (m2 - m1)
let x = (self.b - other.b) / (other.m - self.m);
if self.m.abs() < other.m.abs() {
Some(Point::from_line_x(self, x))
} else {
Some(Point::from_line_x(other, x))
}
}
pub fn get_perpendicular_through_point(&self, p: Point) -> Self {
let m = -1.0 / self.m;
let b = p.y - m * p.x;
Self { m, b, p1: p }
}
}
#[derive(Debug, Clone, Copy)]
pub struct LineSegment {
pub p1: Point,
pub p2: Point,
pub line: Line,
}
impl LineSegment {
pub fn new(p1: Point, p2: Point) -> Self {
Self {
p1,
p2,
line: Line::from_points(p1, p2),
}
}
pub fn intersection(&self, other: &LineSegment) -> Option<Point> {
let intersection = self.line.intersection(&other.line);
intersection.filter(|intersection| {
self.is_intersection_on_segment(*intersection)
&& other.is_intersection_on_segment(*intersection)
})
}
fn is_intersection_on_segment(&self, intersection: Point) -> bool {
if intersection.is_same_as(&self.p1) || intersection.is_same_as(&self.p2) {
return true;
}
if self.line.is_vertical() || self.line.m.abs() > 1.0 {
return between(intersection.y, self.p1.y, self.p2.y);
}
between(intersection.x, self.p1.x, self.p2.x)
}
}
pub fn between<T: Copy + PartialOrd>(num: T, a: T, b: T) -> bool {
let (min, max) = if a < b { (a, b) } else { (b, a) };
num >= min && num <= max
}
-302
View File
@@ -1,302 +0,0 @@
use js_sys::Array;
use sha1::{Sha1, Digest};
use wasm_bindgen::prelude::*;
use crate::{
geometry::Point,
pathfinder::{DiscoveredNodePtr, Pathfinder},
};
#[allow(unused)]
macro_rules! log {
( $( $t:tt )* ) => {
log(&format!( $( $t )* ));
};
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console, js_name=warn)]
pub fn log(s: &str);
}
#[wasm_bindgen]
extern "C" {
pub type JsWall;
pub type JsWallData;
pub type JsWallFlags;
pub type JsWallHeight;
#[wasm_bindgen(method, getter)]
fn data(this: &JsWall) -> JsWallData;
#[wasm_bindgen(method, getter)]
fn c(this: &JsWallData) -> Vec<f64>;
#[wasm_bindgen(method, getter, js_name = "door")]
fn door_type(this: &JsWallData) -> DoorType;
#[wasm_bindgen(method, getter, js_name = "ds")]
fn door_state(this: &JsWallData) -> DoorState;
#[wasm_bindgen(method, getter, js_name = "move")]
fn move_type(this: &JsWallData) -> WallSenseType;
#[wasm_bindgen(method, getter)]
fn flags(this: &JsWallData) -> JsWallFlags;
#[wasm_bindgen(method, getter, js_name = "wallHeight")]
fn wall_height(this: &JsWallFlags) -> Option<JsWallHeight>;
#[wasm_bindgen(method, getter, js_name = "wallHeightTop")]
fn top(this: &JsWallHeight) -> Option<f64>;
#[wasm_bindgen(method, getter, js_name = "wallHeightBottom")]
fn bottom(this: &JsWallHeight) -> Option<f64>;
}
#[wasm_bindgen]
extern "C" {
pub type JsPoint;
#[wasm_bindgen(method, getter)]
fn x(this: &JsPoint) -> f64;
#[wasm_bindgen(method, getter)]
fn y(this: &JsPoint) -> f64;
}
impl From<JsPoint> for Point {
fn from(point: JsPoint) -> Self {
Point {
x: point.x(),
y: point.y(),
}
}
}
#[wasm_bindgen]
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum DoorState {
CLOSED = 0,
OPEN = 1,
LOCKED = 2,
}
impl TryFrom<usize> for DoorState {
type Error = ();
fn try_from(value: usize) -> Result<Self, Self::Error> {
match value {
x if x == Self::CLOSED as usize => Ok(Self::CLOSED),
x if x == Self::OPEN as usize => Ok(Self::OPEN),
x if x == Self::LOCKED as usize => Ok(Self::LOCKED),
_ => Err(()),
}
}
}
#[wasm_bindgen]
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum DoorType {
NONE = 0,
DOOR = 1,
SECRET = 2,
}
impl TryFrom<usize> for DoorType {
type Error = ();
fn try_from(value: usize) -> Result<Self, Self::Error> {
match value {
x if x == Self::NONE as usize => Ok(Self::NONE),
x if x == Self::DOOR as usize => Ok(Self::DOOR),
x if x == Self::SECRET as usize => Ok(Self::SECRET),
_ => Err(()),
}
}
}
#[wasm_bindgen]
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum WallSenseType {
NONE = 0,
LIMITED = 10,
NORMAL = 20,
}
impl TryFrom<usize> for WallSenseType {
type Error = ();
fn try_from(value: usize) -> Result<Self, Self::Error> {
match value {
x if x == Self::NONE as usize => Ok(Self::NONE),
x if x == Self::LIMITED as usize => Ok(Self::LIMITED),
x if x == Self::NORMAL as usize => Ok(Self::NORMAL),
_ => Err(()),
}
}
}
#[derive(Debug, Copy, Clone)]
pub struct WallHeight {
pub top: f64,
pub bottom: f64,
}
impl Default for WallHeight {
fn default() -> Self {
Self {
top: f64::INFINITY,
bottom: f64::NEG_INFINITY,
}
}
}
impl From<Option<JsWallHeight>> for WallHeight {
fn from(height: Option<JsWallHeight>) -> Self {
let height = height
.map(|height| (height.top(), height.bottom()))
.unwrap_or((None, None));
let top = height.0.unwrap_or(WallHeight::default().top);
let bottom = height.1.unwrap_or(WallHeight::default().bottom);
Self { top, bottom }
}
}
impl WallHeight {
pub fn contains(&self, height: f64) -> bool {
self.top >= height && self.bottom <= height
}
}
#[derive(Debug, Clone, Copy)]
pub struct Wall {
pub p1: Point,
pub p2: Point,
pub door_type: DoorType,
pub door_state: DoorState,
pub move_type: WallSenseType,
pub height: WallHeight,
}
impl Wall {
pub fn new(
p1: Point,
p2: Point,
door_type: DoorType,
door_state: DoorState,
move_type: WallSenseType,
height: WallHeight,
) -> Self {
Self {
p1,
p2,
door_type,
door_state,
move_type,
height,
}
}
pub fn is_door(&self) -> bool {
self.door_type != DoorType::NONE
}
pub fn is_open(&self) -> bool {
self.door_state == DoorState::OPEN
}
}
impl Wall {
fn from_js(wall: &JsWall, enable_height: bool) -> Self {
let data = wall.data();
let mut c = data.c();
c.iter_mut().for_each(|val| *val = val.round());
let height = if enable_height {
data.flags().wall_height().into()
}
else {
WallHeight::default()
};
Self::new(
Point::new(c[0], c[1]),
Point::new(c[2], c[3]),
data.door_type(),
data.door_state(),
data.move_type(),
height,
)
}
}
#[allow(dead_code)]
#[wasm_bindgen]
pub fn initialize(js_walls: Vec<JsValue>, token_size: f64, token_elevation: f64, enable_height: bool) -> Pathfinder {
let mut walls = Vec::with_capacity(js_walls.len());
for wall in js_walls {
let wall = JsWall::from(wall);
walls.push(Wall::from_js(&wall, enable_height));
}
Pathfinder::initialize(walls, token_size, token_elevation)
}
#[allow(dead_code)]
#[wasm_bindgen]
pub fn free(pathfinder: Pathfinder) {
drop(pathfinder);
}
#[allow(dead_code)]
#[wasm_bindgen(js_name=findPath)]
pub fn find_path(pathfinder: &mut Pathfinder, from: JsPoint, to: JsPoint) -> Option<Array> {
pathfinder
.find_path(from.into(), to.into())
.map(|first_node| first_node.iter_path().map(JsValue::from).collect())
}
#[allow(dead_code)]
#[wasm_bindgen(js_name=debugGetPathfindingPoints)]
pub fn debug_get_pathfinding_points(pathfinder: &Pathfinder) -> Array {
pathfinder
.nodes
.iter()
.map(|node| node.borrow().point)
.map(JsValue::from)
.collect()
}
#[allow(dead_code)]
#[wasm_bindgen]
pub fn sha1(input: &str) -> String {
let mut hasher = Sha1::new();
hasher.update(input);
format!("{:x}", hasher.finalize())
}
trait IteratePath {
fn iter_path(&self) -> PathIterator;
}
impl IteratePath for DiscoveredNodePtr {
fn iter_path(&self) -> PathIterator {
PathIterator {
current_node: Some(self.clone()),
}
}
}
struct PathIterator {
current_node: Option<DiscoveredNodePtr>,
}
impl Iterator for PathIterator {
type Item = Point;
fn next(&mut self) -> Option<Self::Item> {
if let Some(node) = self.current_node.clone() {
let point = node.borrow().node.borrow().point;
self.current_node = node.borrow().previous.clone();
Some(point)
} else {
None
}
}
}
-12
View File
@@ -1,12 +0,0 @@
mod geometry;
#[macro_use]
mod js_api;
mod pathfinder;
mod ptr_indexed_hash_set;
use wasm_bindgen::prelude::*;
#[wasm_bindgen(start)]
pub fn main() {
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
}
-330
View File
@@ -1,330 +0,0 @@
use std::{cell::RefCell, f64::consts::PI, rc::Rc};
use wasm_bindgen::prelude::*;
use rustc_hash::FxHashMap;
use crate::{
geometry::{LineSegment, Point},
js_api::{Wall, WallSenseType},
ptr_indexed_hash_set::PtrIndexedHashSet,
};
pub struct Edge {
target: NodePtr,
cost: f64,
}
pub struct Node {
pub point: Point,
edges: Option<Vec<Edge>>,
final_edge: Option<Option<Edge>>,
}
impl Node {
pub fn new(point: Point) -> Self {
Self {
point,
edges: None,
final_edge: None,
}
}
fn iter_edges(
&self,
) -> std::iter::Chain<std::slice::Iter<'_, Edge>, std::option::Iter<'_, Edge>> {
self.edges
.as_ref()
.unwrap()
.iter()
.chain(self.final_edge.as_ref().unwrap().iter())
}
}
type NodePtr = Rc<RefCell<Node>>;
impl From<Node> for NodePtr {
fn from(node: Node) -> Self {
Rc::new(RefCell::new(node))
}
}
pub struct DiscoveredNode {
pub node: NodePtr,
cost: f64,
estimated: f64,
pub previous: Option<DiscoveredNodePtr>,
}
pub type DiscoveredNodePtr = Rc<RefCell<DiscoveredNode>>;
impl From<DiscoveredNode> for DiscoveredNodePtr {
fn from(node: DiscoveredNode) -> Self {
Rc::new(RefCell::new(node))
}
}
#[derive(Default, Clone)]
pub struct NodeStorage {
regular_nodes: Vec<NodePtr>,
final_node: Option<NodePtr>,
}
pub type NodeStorageIterator<'a> = std::iter::Chain<
std::slice::Iter<'a, Rc<RefCell<Node>>>,
std::option::Iter<'a, Rc<RefCell<Node>>>,
>;
impl NodeStorage {
fn new() -> Self {
Self::default()
}
fn push(&mut self, node: NodePtr) {
self.regular_nodes.push(node);
}
fn initialize_edges(&mut self, node: &NodePtr, walls: &[LineSegment]) {
if node.borrow().final_edge.is_none() {
let final_edge = self
.final_node
.as_ref()
.filter(|neighbor| {
!self.collides_with_wall(
&LineSegment::new(node.borrow().point, neighbor.borrow().point),
walls,
)
})
.map(|neighbor| Edge {
target: neighbor.clone(),
cost: node.borrow().point.distance_to(neighbor.borrow().point),
});
node.borrow_mut().final_edge = Some(final_edge);
}
if node.borrow().edges.is_some() {
return;
}
let point = node.borrow().point;
let mut edges = Vec::new();
for neighbor in &self.regular_nodes {
if Rc::ptr_eq(neighbor, node) {
continue;
}
let neighbor_point = neighbor.borrow().point;
if !self.collides_with_wall(&LineSegment::new(point, neighbor_point), walls) {
let cost = point.distance_to(neighbor_point);
edges.push(Edge {
target: neighbor.clone(),
cost,
});
}
}
node.borrow_mut().edges = Some(edges);
}
fn collides_with_wall(&self, line: &LineSegment, walls: &[LineSegment]) -> bool {
walls.iter().any(|wall| line.intersection(wall).is_some())
}
pub fn cleanup_final_edges(&mut self) {
for node in &self.regular_nodes {
node.borrow_mut().final_edge = None;
}
}
pub fn iter(&self) -> NodeStorageIterator {
self.regular_nodes.iter().chain(self.final_node.iter())
}
}
#[wasm_bindgen]
pub struct Pathfinder {
#[wasm_bindgen(skip)]
pub nodes: NodeStorage,
#[wasm_bindgen(skip)]
pub walls: Vec<LineSegment>,
}
impl Pathfinder {
pub fn initialize<I>(walls: I, token_size: f64, token_elevation: f64) -> Self
where
I: IntoIterator<Item = Wall>,
{
let distance_from_walls = token_size / 2.0;
let mut endpoints = FxHashMap::<Point, Vec<f64>>::default();
let mut line_segments = Vec::new();
for wall in walls {
if wall.move_type == WallSenseType::NONE {
continue;
}
if wall.is_door() && wall.is_open() {
continue;
}
if !wall.height.contains(token_elevation) {
continue;
}
let x_diff = wall.p2.x - wall.p1.x;
let y_diff = wall.p2.y - wall.p1.y;
let p1_angle = y_diff.atan2(x_diff).rem_euclid(2.0 * PI);
let p2_angle = (p1_angle + PI).rem_euclid(2.0 * PI);
for (point, angle) in [(wall.p1, p1_angle), (wall.p2, p2_angle)] {
let angles = endpoints.entry(point).or_insert_with(Vec::new);
angles.push(angle);
}
line_segments.push(LineSegment::new(wall.p1, wall.p2));
}
endpoints
.values_mut()
.for_each(|angles| angles.sort_by(|a, b| a.partial_cmp(b).unwrap()));
let mut nodes = NodeStorage::new();
for (point, angles) in endpoints {
assert!(!angles.is_empty());
for i in 1..angles.len() {
let angle1 = angles[i - 1];
let angle2 = angles[i];
if angle1 == angle2 {
continue;
}
let angle_diff = angle2 - angle1;
if angle_diff <= PI {
continue;
}
let angle_between = angle_diff / 2.0 + angle1;
nodes.push(calc_pathfinding_node(
point,
angle_between,
distance_from_walls,
&mut line_segments,
));
nodes.push(calc_pathfinding_node(
point,
angle1 + 0.5 * PI,
distance_from_walls,
&mut line_segments,
));
nodes.push(calc_pathfinding_node(
point,
angle2 - 0.5 * PI,
distance_from_walls,
&mut line_segments,
));
}
let angle1 = angles.last().unwrap();
let angle2 = angles.first().unwrap() + 2.0 * PI;
let angle_diff = angle2 - angle1;
if angle_diff <= PI {
continue;
}
let angle_between = angle_diff / 2.0 + angle1;
nodes.push(calc_pathfinding_node(
point,
angle_between,
distance_from_walls,
&mut line_segments,
));
nodes.push(calc_pathfinding_node(
point,
angle1 + 0.5 * PI,
distance_from_walls,
&mut line_segments,
));
nodes.push(calc_pathfinding_node(
point,
angle2 - 0.5 * PI,
distance_from_walls,
&mut line_segments,
));
}
// TODO Eliminating nodes close to each other may improve performance
Self {
nodes,
walls: line_segments,
}
}
pub fn find_path(&mut self, from: Point, to: Point) -> Option<DiscoveredNodePtr> {
self.nodes.cleanup_final_edges();
let mut nodes = self.nodes.clone();
nodes.final_node = Some(NodePtr::from(Node::new(from)));
let to_node = NodePtr::from(Node::new(to));
nodes.initialize_edges(&to_node, &self.walls);
let to = DiscoveredNode {
node: to_node,
cost: 0.0,
estimated: to.distance_to(from),
previous: None,
};
// TODO Use a sorted set for next_nodes for better performance
let mut next_nodes = vec![DiscoveredNodePtr::from(to)];
let mut previous_nodes = PtrIndexedHashSet::new();
while !next_nodes.is_empty() {
// Sort by estimated cost, high to low
// TODO Maybe tere's a faster way to do this than re-sorting every iteration?
next_nodes.sort_by(|a, b| {
b.borrow()
.estimated
.partial_cmp(&a.borrow().estimated)
.unwrap()
});
// Get node with cheapest estimate
let current_node = next_nodes.pop().unwrap();
if current_node.borrow().node.borrow().point.x == from.x
&& current_node.borrow().node.borrow().point.y == from.y
{
return Some(current_node);
}
previous_nodes.insert(current_node.borrow().node.clone());
for edge in current_node.borrow().node.borrow().iter_edges() {
let neighbor = &edge.target;
if previous_nodes.contains(neighbor) {
continue;
}
nodes.initialize_edges(neighbor, &self.walls);
// Add a flat 0.00001 cost per node to discurage creation of unnecessary waypoints
let cost = current_node.borrow().cost + edge.cost + 0.00001;
let discovered_neighbor = DiscoveredNode {
node: neighbor.clone(),
cost,
estimated: cost + neighbor.borrow().point.distance_to(from),
previous: Some(current_node.clone()),
};
let neighbor_entry = next_nodes
.iter()
.find(|node| Rc::ptr_eq(&node.borrow().node, neighbor));
if let Some(entry) = neighbor_entry {
// If the neighbor is cheaper to reach via the current route than through previously discovered routes, replace it
if entry.borrow().cost > cost {
*entry.borrow_mut() = discovered_neighbor;
}
} else {
next_nodes.push(discovered_neighbor.into());
}
}
}
None
}
}
fn calc_pathfinding_node(
p: Point,
angle: f64,
distance_from_walls: f64,
line_segments: &mut Vec<LineSegment>,
) -> NodePtr {
let offset_x = angle.cos() * distance_from_walls;
let offset_y = angle.sin() * distance_from_walls;
line_segments.push(LineSegment::new(
p,
Point {
x: p.x + offset_x * 0.99,
y: p.y + offset_y * 0.99,
},
));
NodePtr::from(Node::new(Point {
x: p.x + offset_x,
y: p.y + offset_y,
}))
}
-68
View File
@@ -1,68 +0,0 @@
use rustc_hash::FxHashSet;
use std::collections::hash_set;
use std::hash::{Hash, Hasher};
use std::rc::Rc;
#[derive(Default)]
pub struct PtrIndexedHashSet<T>(FxHashSet<PtrIndexedRc<T>>);
impl<T> PtrIndexedHashSet<T> {
pub fn new() -> Self {
PtrIndexedHashSet(FxHashSet::default())
}
pub fn insert(&mut self, value: Rc<T>) -> bool {
self.0.insert(PtrIndexedRc(value))
}
pub fn remove(&mut self, value: &Rc<T>) -> bool {
self.0.remove(&PtrIndexedRc(Rc::clone(value)))
}
pub fn contains(&mut self, value: &Rc<T>) -> bool {
self.0.contains(&PtrIndexedRc(Rc::clone(value)))
}
}
impl<T: std::fmt::Debug> std::fmt::Debug for PtrIndexedHashSet<T> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_set().entries(self.0.iter().map(|e| &e.0)).finish()
}
}
struct PtrIndexedRc<T>(Rc<T>);
impl<T> Hash for PtrIndexedRc<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
Rc::as_ptr(&self.0).hash(state)
}
}
impl<T> PartialEq for PtrIndexedRc<T> {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.0, &other.0)
}
}
impl<T> Eq for PtrIndexedRc<T> {}
pub struct PtrIndexedHashSetIterator<'a, T>(hash_set::Iter<'a, PtrIndexedRc<T>>);
impl<'a, T> Iterator for PtrIndexedHashSetIterator<'a, T> {
type Item = &'a Rc<T>;
fn next(&mut self) -> Option<Self::Item> {
match self.0.next() {
Some(item) => Some(&item.0),
None => None,
}
}
}
impl<'a, T> IntoIterator for &'a PtrIndexedHashSet<T> {
type Item = &'a Rc<T>;
type IntoIter = PtrIndexedHashSetIterator<'a, T>;
fn into_iter(self) -> Self::IntoIter {
PtrIndexedHashSetIterator::<T>((&self.0).iter())
}
}