From c21e4c91d6fd8b0d840be715ee21c0261eecfbad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20V=C3=B6gele?= Date: Fri, 30 Sep 2022 21:52:27 +0200 Subject: [PATCH] Move pathfinding into routinglib; call routinglib for pathfinding jobs --- build_release.py | 50 --- build_wasm.py | 13 - install_dev_dependencies.sh | 3 - js/keybindings.js | 18 +- js/main.js | 14 - js/pathfinding.js | 539 ------------------------------- js/ruler.js | 57 +++- js/settings.js | 45 ++- js/util.js | 11 +- rust/.gitignore | 1 - rust/Cargo.lock | 232 ------------- rust/Cargo.toml | 20 -- rust/rustfmt.toml | 1 - rust/src/geometry.rs | 187 ----------- rust/src/js_api.rs | 302 ----------------- rust/src/lib.rs | 12 - rust/src/pathfinder.rs | 330 ------------------- rust/src/ptr_indexed_hash_set.rs | 68 ---- 18 files changed, 94 insertions(+), 1809 deletions(-) delete mode 100755 build_release.py delete mode 100755 build_wasm.py delete mode 100755 install_dev_dependencies.sh delete mode 100644 js/pathfinding.js delete mode 100644 rust/.gitignore delete mode 100644 rust/Cargo.lock delete mode 100644 rust/Cargo.toml delete mode 100644 rust/rustfmt.toml delete mode 100644 rust/src/geometry.rs delete mode 100644 rust/src/js_api.rs delete mode 100644 rust/src/lib.rs delete mode 100644 rust/src/pathfinder.rs delete mode 100644 rust/src/ptr_indexed_hash_set.rs diff --git a/build_release.py b/build_release.py deleted file mode 100755 index bd555a1..0000000 --- a/build_release.py +++ /dev/null @@ -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}") diff --git a/build_wasm.py b/build_wasm.py deleted file mode 100755 index 33ae107..0000000 --- a/build_wasm.py +++ /dev/null @@ -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}"]) diff --git a/install_dev_dependencies.sh b/install_dev_dependencies.sh deleted file mode 100755 index d3534f1..0000000 --- a/install_dev_dependencies.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -cargo install cargo-watch -cargo install wasm-pack diff --git a/js/keybindings.js b/js/keybindings.js index d3831bc..fb2107d 100644 --- a/js/keybindings.js +++ b/js/keybindings.js @@ -65,14 +65,16 @@ export function registerKeybindings() { precedence: -1, }); - game.keybindings.register(settingsKey, "togglePathfinding", { - name: "drag-ruler.keybindings.togglePathfinding.name", - hint: "drag-ruler.keybindings.togglePathfinding.hint", - onDown: handleTogglePathfinding, - onUp: handleTogglePathfinding, - precedence: -1, - restricted: !game.settings.get(settingsKey, "allowPathfinding"), - }); + if (game.modules.get("routinglib")?.active) { + game.keybindings.register(settingsKey, "togglePathfinding", { + name: "drag-ruler.keybindings.togglePathfinding.name", + hint: "drag-ruler.keybindings.togglePathfinding.hint", + onDown: handleTogglePathfinding, + onUp: handleTogglePathfinding, + precedence: -1, + restricted: !game.settings.get(settingsKey, "allowPathfinding"), + }); + } } function handleDeleteWaypoint() { diff --git a/js/main.js b/js/main.js index d3c462d..23a2a7a 100644 --- a/js/main.js +++ b/js/main.js @@ -14,29 +14,15 @@ import {disableSnap, registerKeybindings} from "./keybindings.js"; import {libWrapper} from "./libwrapper_shim.js"; import {performMigrations} from "./migration.js"; import {removeLastHistoryEntryIfAt, resetMovementHistory} from "./movement_tracking.js"; -import {wipePathfindingCache, initializePathfinding} from "./pathfinding.js"; import {extendRuler} from "./ruler.js"; import {registerSettings, RightClickAction, settingsKey} from "./settings.js"; import {recalculate} from "./socket.js"; import {SpeedProvider} from "./speed_provider.js"; import {setSnapParameterOnOptions} from "./util.js"; -import initGridlessPathfinding, * as GridlessPathfinding from "../wasm/gridless_pathfinding.js"; - CONFIG.debug.dragRuler = false; 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", () => { registerSettings(); registerKeybindings(); diff --git a/js/pathfinding.js b/js/pathfinding.js deleted file mode 100644 index 58d4d29..0000000 --- a/js/pathfinding.js +++ /dev/null @@ -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); -} diff --git a/js/ruler.js b/js/ruler.js index c02bea9..b0715e1 100644 --- a/js/ruler.js +++ b/js/ruler.js @@ -8,11 +8,18 @@ import { highlightMeasurementTerrainRuler, measureDistances, } from "./compatibility.js"; +import {getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js"; import {cancelScheduledMeasurement, highlightMeasurementNative} from "./foundry_imports.js"; import {disableSnap} from "./keybindings.js"; import {getMovementHistory} from "./movement_tracking.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() { class DragRulerRuler extends CONFIG.Canvas.rulerClass { @@ -102,6 +109,52 @@ export function extendRuler() { const d = this._getMeasurementDestination(destination); if (d.x === this.destination.x && d.y === this.destination.y) return; 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._computeDistance(options.gridSpaces); @@ -111,6 +164,7 @@ export function extendRuler() { // Draw grid highlight this.highlightLayer.clear(); + const isToken = this.draggedEntity instanceof Token; if (isToken && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS && this.dragRulerGridSpaces) { const shape = getTokenShape(this.draggedEntity); if (!this.dragRulerEnableTerrainRuler) { @@ -154,7 +208,6 @@ export function extendRuler() { } _getMeasurementSegments() { - // TODO Recalculate pathfinding, if necessary if (this.isDragRuler) { const unsnappedWaypoints = this.waypoints.concat([this.destination]); const waypoints = diff --git a/js/settings.js b/js/settings.js index 4b5a64a..3eb4151 100644 --- a/js/settings.js +++ b/js/settings.js @@ -5,7 +5,6 @@ import { updateSpeedProvider, } from "./api.js"; import {SpeedProvider} from "./speed_provider.js"; -import {wipePathfindingCache} from "./pathfinding.js"; import {early_isGM} from "./util.js"; export const settingsKey = "drag-ruler"; @@ -93,32 +92,26 @@ export function registerSettings() { default: true, }); - game.settings.register(settingsKey, "allowPathfinding", { - name: "drag-ruler.settings.allowPathfinding.name", - hint: "drag-ruler.settings.allowPathfinding.hint", - scope: "world", - config: true, - type: Boolean, - default: false, - onChange: delayedReload, - }); + if (game.modules.get("routinglib")?.active) { + game.settings.register(settingsKey, "allowPathfinding", { + name: "drag-ruler.settings.allowPathfinding.name", + hint: "drag-ruler.settings.allowPathfinding.hint", + scope: "world", + config: true, + type: Boolean, + default: false, + onChange: delayedReload, + }); - game.settings.register(settingsKey, "autoPathfinding", { - name: "drag-ruler.settings.autoPathfinding.name", - hint: "drag-ruler.settings.autoPathfinding.hint", - scope: "client", - config: early_isGM() || game.settings.get(settingsKey, "allowPathfinding"), - type: Boolean, - default: false, - }); - - game.settings.register(settingsKey, "pathfindingRadius", { - scope: "world", - config: false, - type: Number, - default: 0.9, - onChange: wipePathfindingCache, - }); + game.settings.register(settingsKey, "autoPathfinding", { + name: "drag-ruler.settings.autoPathfinding.name", + hint: "drag-ruler.settings.autoPathfinding.hint", + scope: "client", + config: early_isGM() || game.settings.get(settingsKey, "allowPathfinding"), + type: Boolean, + default: false, + }); + } game.settings.register(settingsKey, "lastTerrainRulerHintTime", { config: false, diff --git a/js/util.js b/js/util.js index 00e3eca..500d9b8 100644 --- a/js/util.js +++ b/js/util.js @@ -1,6 +1,7 @@ import {getPixelsFromGridPosition} from "./foundry_fixes.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) { for (let i = 0; i < Math.min(it1.length, it2.length); i++) { @@ -292,3 +293,11 @@ export function early_isGM() { export function isModuleActive(moduleName) { 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; +} diff --git a/rust/.gitignore b/rust/.gitignore deleted file mode 100644 index 2f7896d..0000000 --- a/rust/.gitignore +++ /dev/null @@ -1 +0,0 @@ -target/ diff --git a/rust/Cargo.lock b/rust/Cargo.lock deleted file mode 100644 index 1a86120..0000000 --- a/rust/Cargo.lock +++ /dev/null @@ -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" diff --git a/rust/Cargo.toml b/rust/Cargo.toml deleted file mode 100644 index 02b8521..0000000 --- a/rust/Cargo.toml +++ /dev/null @@ -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" diff --git a/rust/rustfmt.toml b/rust/rustfmt.toml deleted file mode 100644 index 218e203..0000000 --- a/rust/rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -hard_tabs = true diff --git a/rust/src/geometry.rs b/rust/src/geometry.rs deleted file mode 100644 index 0c219d0..0000000 --- a/rust/src/geometry.rs +++ /dev/null @@ -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(&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 { - // 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 { - 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(num: T, a: T, b: T) -> bool { - let (min, max) = if a < b { (a, b) } else { (b, a) }; - num >= min && num <= max -} diff --git a/rust/src/js_api.rs b/rust/src/js_api.rs deleted file mode 100644 index d2fbc8c..0000000 --- a/rust/src/js_api.rs +++ /dev/null @@ -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; - - #[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; - - #[wasm_bindgen(method, getter, js_name = "wallHeightTop")] - fn top(this: &JsWallHeight) -> Option; - - #[wasm_bindgen(method, getter, js_name = "wallHeightBottom")] - fn bottom(this: &JsWallHeight) -> Option; -} - -#[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 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 for DoorState { - type Error = (); - fn try_from(value: usize) -> Result { - 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 for DoorType { - type Error = (); - fn try_from(value: usize) -> Result { - 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 for WallSenseType { - type Error = (); - fn try_from(value: usize) -> Result { - 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> for WallHeight { - fn from(height: Option) -> 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, 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 { - 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, -} - -impl Iterator for PathIterator { - type Item = Point; - - fn next(&mut self) -> Option { - 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 - } - } -} diff --git a/rust/src/lib.rs b/rust/src/lib.rs deleted file mode 100644 index 0d6ae2b..0000000 --- a/rust/src/lib.rs +++ /dev/null @@ -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)); -} diff --git a/rust/src/pathfinder.rs b/rust/src/pathfinder.rs deleted file mode 100644 index 89b37b8..0000000 --- a/rust/src/pathfinder.rs +++ /dev/null @@ -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>, - final_edge: Option>, -} - -impl Node { - pub fn new(point: Point) -> Self { - Self { - point, - edges: None, - final_edge: None, - } - } - - fn iter_edges( - &self, - ) -> std::iter::Chain, std::option::Iter<'_, Edge>> { - self.edges - .as_ref() - .unwrap() - .iter() - .chain(self.final_edge.as_ref().unwrap().iter()) - } -} - -type NodePtr = Rc>; - -impl From 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, -} - -pub type DiscoveredNodePtr = Rc>; - -impl From for DiscoveredNodePtr { - fn from(node: DiscoveredNode) -> Self { - Rc::new(RefCell::new(node)) - } -} - -#[derive(Default, Clone)] -pub struct NodeStorage { - regular_nodes: Vec, - final_node: Option, -} - -pub type NodeStorageIterator<'a> = std::iter::Chain< - std::slice::Iter<'a, Rc>>, - std::option::Iter<'a, Rc>>, ->; - -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, -} - -impl Pathfinder { - pub fn initialize(walls: I, token_size: f64, token_elevation: f64) -> Self - where - I: IntoIterator, - { - let distance_from_walls = token_size / 2.0; - let mut endpoints = FxHashMap::>::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 { - 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, -) -> 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, - })) -} diff --git a/rust/src/ptr_indexed_hash_set.rs b/rust/src/ptr_indexed_hash_set.rs deleted file mode 100644 index c12aa2d..0000000 --- a/rust/src/ptr_indexed_hash_set.rs +++ /dev/null @@ -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(FxHashSet>); - -impl PtrIndexedHashSet { - pub fn new() -> Self { - PtrIndexedHashSet(FxHashSet::default()) - } - - pub fn insert(&mut self, value: Rc) -> bool { - self.0.insert(PtrIndexedRc(value)) - } - - pub fn remove(&mut self, value: &Rc) -> bool { - self.0.remove(&PtrIndexedRc(Rc::clone(value))) - } - - pub fn contains(&mut self, value: &Rc) -> bool { - self.0.contains(&PtrIndexedRc(Rc::clone(value))) - } -} - -impl std::fmt::Debug for PtrIndexedHashSet { - 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(Rc); - -impl Hash for PtrIndexedRc { - fn hash(&self, state: &mut H) { - Rc::as_ptr(&self.0).hash(state) - } -} - -impl PartialEq for PtrIndexedRc { - fn eq(&self, other: &Self) -> bool { - Rc::ptr_eq(&self.0, &other.0) - } -} - -impl Eq for PtrIndexedRc {} - -pub struct PtrIndexedHashSetIterator<'a, T>(hash_set::Iter<'a, PtrIndexedRc>); - -impl<'a, T> Iterator for PtrIndexedHashSetIterator<'a, T> { - type Item = &'a Rc; - - fn next(&mut self) -> Option { - match self.0.next() { - Some(item) => Some(&item.0), - None => None, - } - } -} - -impl<'a, T> IntoIterator for &'a PtrIndexedHashSet { - type Item = &'a Rc; - type IntoIter = PtrIndexedHashSetIterator<'a, T>; - fn into_iter(self) -> Self::IntoIter { - PtrIndexedHashSetIterator::((&self.0).iter()) - } -}