Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f6e348bf0 | |||
| 82f2a885fb | |||
| 5add0be393 | |||
| f77990420a | |||
| e84632c0d7 | |||
| 30bd3d33e0 | |||
| df4c515fcd | |||
| 02dfd8db0d | |||
| ddf89f9499 | |||
| 0dfdb23bfb | |||
| 3f2e9e1a3e | |||
| 576db2dc5a | |||
| b031acaa6e | |||
| ee956abd78 | |||
| 1334703877 | |||
| d6cccf4466 | |||
| 074d2f5052 | |||
| e5b8f896f8 | |||
| 3de350d3e9 | |||
| 8f97081236 | |||
| 18253dc42b | |||
| fb702cd850 | |||
| ad3fdf4d18 | |||
| 58946096a6 | |||
| 22112ddfd5 | |||
| f59b0f9d2d | |||
| 443cfd4317 | |||
| fb3665d758 | |||
| 96cb690431 | |||
| fb7202d5f0 | |||
| fa581ddfc3 | |||
| 56e15be4ca | |||
| 1509307313 | |||
| 68885385ea | |||
| 6748617f6a |
+4
-1
@@ -1 +1,4 @@
|
|||||||
foundry-*.js
|
/foundry*.js
|
||||||
|
artifact/
|
||||||
|
wasm/
|
||||||
|
*.lock
|
||||||
|
|||||||
@@ -1,3 +1,67 @@
|
|||||||
|
## 1.12.6
|
||||||
|
### Bugfixes
|
||||||
|
- Measured templates no longer snap to a virtual grid on gridless scenes (this was a regression introduced in 1.12.5)
|
||||||
|
|
||||||
|
|
||||||
|
## 1.12.5
|
||||||
|
### Compatibility
|
||||||
|
- Drag Ruler is now compatible with DF Template Enhancements
|
||||||
|
|
||||||
|
|
||||||
|
## 1.12.4
|
||||||
|
### Bugfixes
|
||||||
|
- Fixed a bug that could cause Drag Ruler to override the default ruler color on other player's clients
|
||||||
|
|
||||||
|
|
||||||
|
## 1.12.3
|
||||||
|
### Bugfixes
|
||||||
|
- Fixed a bug that could cause foundry to freeze indefinitely when trying to pathfind to an unreachalbe location (thanks to JDCalvert)
|
||||||
|
- Fixed a bug that caused the pathfinder to route through one-directional walls from the wrong direction (thanks to JDCalvert)
|
||||||
|
- Fixed a bug that could cause Drag Ruler to write errors into the JS console during regular usage
|
||||||
|
|
||||||
|
### Compatibility
|
||||||
|
- Drag Ruler's generic speed provider is now aware of good defaults for DnD 4th Edition
|
||||||
|
- Drag Ruler's pathfinder should now be compatible with the Wall Height and Levels modules (thanks to JDCalvert)
|
||||||
|
|
||||||
|
|
||||||
|
## 1.12.2
|
||||||
|
### Bugfixes
|
||||||
|
- Fixed a bug where the pathfinder on gridless scenes sometimes wasn't able to find a way around corners with specific angles
|
||||||
|
- Pathfinding will now be disabled when the hotkey to move tokens without animation is being pressed, to allow GMs to move their tokens through walls
|
||||||
|
|
||||||
|
|
||||||
|
## 1.12.1
|
||||||
|
### Hotfix
|
||||||
|
- Version 1.12.0 was incorrectly packaged, which caused it to fail to load
|
||||||
|
|
||||||
|
|
||||||
|
## 1.12.0
|
||||||
|
### New features
|
||||||
|
- Pathfinding is now supported on gridless scenes
|
||||||
|
|
||||||
|
|
||||||
|
## 1.11.5
|
||||||
|
### Bugfixes
|
||||||
|
- Fixed a bug that was causing Drag Ruler to spam useless warnings into the console (this was a regression introduced in 1.11.4)
|
||||||
|
|
||||||
|
|
||||||
|
## 1.11.4
|
||||||
|
### Bugfixes
|
||||||
|
- When changing the measurement mode via a keybinding (toggle snaping or toggle pathfinding) the updated ruler will now be sent to other players immediately
|
||||||
|
- Fixed a bug that incorrectly showed a ruler to be snapped to other players despite the ruler not being snapped
|
||||||
|
- Fixed a bug that could cause a token to move to an incorrect location if the token was being dragged and dropped very rapidly
|
||||||
|
- Drag Ruler's token movement animations can now be properly waited for (this improves the interaction with modules like sequencer)
|
||||||
|
|
||||||
|
### Translation
|
||||||
|
- Updated Spanish translation (thanks to Viriato139ac#342)
|
||||||
|
|
||||||
|
|
||||||
|
## 1.11.3
|
||||||
|
### Bugfixes
|
||||||
|
- The setting to automatically start pathfinding is now visible to players again (this was a regression introduced in 1.11.2)
|
||||||
|
- Fixed a bug that would show the measurements of other players as if they were using the pathfinder, even if they were not using it.
|
||||||
|
|
||||||
|
|
||||||
## 1.11.2
|
## 1.11.2
|
||||||
### Bugfixes
|
### Bugfixes
|
||||||
- Fixed a memory leak that could cause the rule to slow down after using the pathfinding functionality for a while
|
- Fixed a memory leak that could cause the rule to slow down after using the pathfinding functionality for a while
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ Drag Ruler will work with all Foundry VTT game systems. However, some game syste
|
|||||||
|
|
||||||
The game systems that offer Drag Ruler integration are:
|
The game systems that offer Drag Ruler integration are:
|
||||||
- Cypher System (starting with version 1.13.0)
|
- Cypher System (starting with version 1.13.0)
|
||||||
|
- DnD5e (via the module [DnD5e Drag Ruler Integration](https://foundryvtt.com/packages/elevation-drag-ruler))
|
||||||
- GURPS 4th Edition Game Aid (Unofficial) (starting with version 0.9.1)
|
- GURPS 4th Edition Game Aid (Unofficial) (starting with version 0.9.1)
|
||||||
- Ironclaw Second Edition (starting with version 0.2.2)
|
- Ironclaw Second Edition (starting with version 0.2.2)
|
||||||
- Lancer (via the module [Lancer Speed Provider](https://foundryvtt.com/packages/lancer-speed-provider))
|
- Lancer (via the module [Lancer Speed Provider](https://foundryvtt.com/packages/lancer-speed-provider))
|
||||||
@@ -44,6 +45,7 @@ The game systems that offer Drag Ruler integration are:
|
|||||||
- Pathfinder 1 (starting with version 0.77.3)
|
- Pathfinder 1 (starting with version 0.77.3)
|
||||||
- Pathfinder 2e (via the module [PF2E Drag Ruler Integration](https://foundryvtt.com/packages/pf2e-dragruler/))
|
- Pathfinder 2e (via the module [PF2E Drag Ruler Integration](https://foundryvtt.com/packages/pf2e-dragruler/))
|
||||||
- Shadowrun 5th Edition (via the module [Drag Ruler Integration for Shadowrun 5E](https://foundryvtt.com/packages/drag-ruler-integration-for-shadowrun-5e))
|
- Shadowrun 5th Edition (via the module [Drag Ruler Integration for Shadowrun 5E](https://foundryvtt.com/packages/drag-ruler-integration-for-shadowrun-5e))
|
||||||
|
- Shadow of the Demon Lord (starting with version 1.7.15)
|
||||||
- Starfinder (via the module [Starfinder Drag Ruler Integration](https://foundryvtt.com/packages/starfinder-drag-ruler))
|
- Starfinder (via the module [Starfinder Drag Ruler Integration](https://foundryvtt.com/packages/starfinder-drag-ruler))
|
||||||
- Stargate RPG (starting with version 1.6.0)
|
- Stargate RPG (starting with version 1.6.0)
|
||||||
- Symbaroum (via the module [Symbaroum drag ruler integration](https://foundryvtt.com/packages/symbaroum-drag-ruler-integration))
|
- Symbaroum (via the module [Symbaroum drag ruler integration](https://foundryvtt.com/packages/symbaroum-drag-ruler-integration))
|
||||||
|
|||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/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}")
|
||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
#!/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}"])
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
cargo install cargo-watch
|
||||||
|
cargo install wasm-pack
|
||||||
@@ -91,7 +91,7 @@ async function animateEntities(entities, draggedEntity, draggedRays, wasPaused)
|
|||||||
});
|
});
|
||||||
await draggedEntity.scene.updateEmbeddedDocuments(draggedEntity.constructor.embeddedName, updates, {animate});
|
await draggedEntity.scene.updateEmbeddedDocuments(draggedEntity.constructor.embeddedName, updates, {animate});
|
||||||
if (animate)
|
if (animate)
|
||||||
await Promise.all(entityPaths.map(({entity, path}) => entity.animateMovement(path)));
|
await Promise.all(entityPaths.map(({entity}) => CanvasAnimation.getAnimation(entity.movementAnimationName)?.promise));
|
||||||
|
|
||||||
// This is a flag of the "Monk's Active Tile Triggers" module that signals that the movement should be cancelled early
|
// This is a flag of the "Monk's Active Tile Triggers" module that signals that the movement should be cancelled early
|
||||||
if (this.cancelMovement) {
|
if (this.cancelMovement) {
|
||||||
@@ -168,7 +168,7 @@ export function measure(destination, options={}) {
|
|||||||
|
|
||||||
this.dragRulerRemovePathfindingWaypoints();
|
this.dragRulerRemovePathfindingWaypoints();
|
||||||
|
|
||||||
if (isToken && isPathfindingEnabled()) {
|
if (isToken && isPathfindingEnabled.call(this)) {
|
||||||
const from = getGridPositionFromPixelsObj(this.waypoints[this.waypoints.length - 1]);
|
const from = getGridPositionFromPixelsObj(this.waypoints[this.waypoints.length - 1]);
|
||||||
const to = getGridPositionFromPixelsObj(destination);
|
const to = getGridPositionFromPixelsObj(destination);
|
||||||
let path = findPath(from, to, this.draggedEntity, this.waypoints);
|
let path = findPath(from, to, this.draggedEntity, this.waypoints);
|
||||||
@@ -107,11 +107,22 @@ function handleDisableSnap(event) {
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
|
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
|
||||||
|
ruler.dragRulerSendState();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMoveWithoutAnimation(event) {
|
function handleMoveWithoutAnimation(event) {
|
||||||
moveWithoutAnimation = !event.up;
|
moveWithoutAnimation = !event.up;
|
||||||
|
|
||||||
|
const ruler = canvas.controls.ruler;
|
||||||
|
if (!ruler?.isDragRuler)
|
||||||
|
return false;
|
||||||
|
if (ruler._state !== Ruler.STATES.MEASURING)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
|
||||||
|
ruler.dragRulerSendState();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTogglePathfinding(event) {
|
function handleTogglePathfinding(event) {
|
||||||
@@ -124,5 +135,6 @@ function handleTogglePathfinding(event) {
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
|
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
|
||||||
|
ruler.dragRulerSendState();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -7,15 +7,29 @@ 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()
|
||||||
@@ -137,6 +151,9 @@ function onEntityDragLeftDrop(event) {
|
|||||||
// This can happen if the user presses ESC during drag (maybe there are other ways too)
|
// This can happen if the user presses ESC during drag (maybe there are other ways too)
|
||||||
if (selectedTokens.length === 0)
|
if (selectedTokens.length === 0)
|
||||||
selectedTokens.push(ruler.draggedEntity);
|
selectedTokens.push(ruler.draggedEntity);
|
||||||
|
// This can happen if the ruler is being dragged so rapidly that the drag move handler hasn't been called before dropping
|
||||||
|
if (ruler._state === Ruler.STATES.STARTING)
|
||||||
|
onMouseMove.call(ruler, event);
|
||||||
ruler._state = Ruler.STATES.MOVING
|
ruler._state = Ruler.STATES.MOVING
|
||||||
moveEntities.call(ruler, ruler.draggedEntity, selectedTokens);
|
moveEntities.call(ruler, ruler.draggedEntity, selectedTokens);
|
||||||
return true
|
return true
|
||||||
@@ -1,41 +1,58 @@
|
|||||||
import {getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
|
import {getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
|
||||||
import {togglePathfinding} from "./keybindings.js";
|
import {moveWithoutAnimation, togglePathfinding} from "./keybindings.js";
|
||||||
import {debugGraphics} from "./main.js";
|
import {debugGraphics} from "./main.js";
|
||||||
import {settingsKey} from "./settings.js";
|
import {settingsKey} from "./settings.js";
|
||||||
import {getSnapPointForTokenObj, iterPairs} from "./util.js";
|
import {getSnapPointForTokenObj, iterPairs} from "./util.js";
|
||||||
|
|
||||||
|
import * as GridlessPathfinding from "../wasm/gridless_pathfinding.js"
|
||||||
|
|
||||||
let cachedNodes = undefined;
|
let cachedNodes = undefined;
|
||||||
|
let cacheElevation;
|
||||||
let use5105 = false;
|
let use5105 = false;
|
||||||
|
let gridlessPathfinders = new Map();
|
||||||
|
let gridWidth, gridHeight;
|
||||||
|
|
||||||
export function isPathfindingEnabled() {
|
export function isPathfindingEnabled() {
|
||||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS)
|
if (this.user !== game.user)
|
||||||
return false;
|
return false;
|
||||||
if (!game.user.isGM && !game.settings.get(settingsKey, "allowPathfinding"))
|
if (!game.user.isGM && !game.settings.get(settingsKey, "allowPathfinding"))
|
||||||
return false;
|
return false;
|
||||||
|
if (moveWithoutAnimation)
|
||||||
|
return false;
|
||||||
return game.settings.get(settingsKey, "autoPathfinding") != togglePathfinding;
|
return game.settings.get(settingsKey, "autoPathfinding") != togglePathfinding;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findPath(from, to, token, previousWaypoints) {
|
export function findPath(from, to, token, previousWaypoints) {
|
||||||
const lastNode = calculatePath(from, to, token, previousWaypoints);
|
checkCacheValid(token);
|
||||||
if (!lastNode)
|
|
||||||
return null;
|
|
||||||
paintPathfindingDebug(lastNode, token);
|
|
||||||
const path = [];
|
|
||||||
let currentNode = lastNode;
|
|
||||||
while (currentNode) {
|
|
||||||
// TODO Check if the distance doesn't change
|
|
||||||
if (path.length >= 2 && !stepCollidesWithWall(currentNode.node, path[path.length - 2], token))
|
|
||||||
// Replace last waypoint if the current waypoint leads to a valid path
|
|
||||||
path[path.length - 1] = {x: currentNode.node.x, y: currentNode.node.y};
|
|
||||||
else
|
|
||||||
path.push({x: currentNode.node.x, y: currentNode.node.y});
|
|
||||||
currentNode = currentNode.previous;
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function wipePathfindingCache() {
|
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||||
cachedNodes = undefined;
|
let tokenSize = Math.max(token.data.width, token.data.height) * canvas.dimensions.size;
|
||||||
|
let pathfinder = gridlessPathfinders.get(tokenSize);
|
||||||
|
if (!pathfinder) {
|
||||||
|
pathfinder = GridlessPathfinding.initialize(canvas.walls.placeables, tokenSize, token.data.elevation, Boolean(game.modules.get("levels")?.active));
|
||||||
|
gridlessPathfinders.set(tokenSize, pathfinder);
|
||||||
|
}
|
||||||
|
paintGridlessPathfindingDebug(pathfinder);
|
||||||
|
return GridlessPathfinding.findPath(pathfinder, from, to);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const lastNode = calculatePath(from, to, token, previousWaypoints);
|
||||||
|
if (!lastNode)
|
||||||
|
return null;
|
||||||
|
paintGriddedPathfindingDebug(lastNode, token);
|
||||||
|
const path = [];
|
||||||
|
let currentNode = lastNode;
|
||||||
|
while (currentNode) {
|
||||||
|
// TODO Check if the distance doesn't change
|
||||||
|
if (path.length >= 2 && !stepCollidesWithWall(path[path.length - 2], currentNode.node, token))
|
||||||
|
// Replace last waypoint if the current waypoint leads to a valid path
|
||||||
|
path[path.length - 1] = {x: currentNode.node.x, y: currentNode.node.y};
|
||||||
|
else
|
||||||
|
path.push({x: currentNode.node.x, y: currentNode.node.y});
|
||||||
|
currentNode = currentNode.previous;
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNode(pos, token, initialize=true) {
|
function getNode(pos, token, initialize=true) {
|
||||||
@@ -45,7 +62,7 @@ function getNode(pos, token, initialize=true) {
|
|||||||
if (!cachedNodes[pos.layer])
|
if (!cachedNodes[pos.layer])
|
||||||
cachedNodes[pos.layer] = new Array(Math.ceil(canvas.dimensions.height / canvas.grid.h));
|
cachedNodes[pos.layer] = new Array(Math.ceil(canvas.dimensions.height / canvas.grid.h));
|
||||||
if (!cachedNodes[pos.layer][pos.y])
|
if (!cachedNodes[pos.layer][pos.y])
|
||||||
cachedNodes[pos.layer][pos.y] = new Array(Math.ceil(canvas.dimensions.width / canvas.grid.w));
|
cachedNodes[pos.layer][pos.y] = new Array(Math.ceil(canvas.dimensions.width / canvas.grid.w));
|
||||||
if (!cachedNodes[pos.layer][pos.y][pos.x]) {
|
if (!cachedNodes[pos.layer][pos.y][pos.x]) {
|
||||||
cachedNodes[pos.layer][pos.y][pos.x] = pos;
|
cachedNodes[pos.layer][pos.y][pos.x] = pos;
|
||||||
}
|
}
|
||||||
@@ -54,10 +71,12 @@ function getNode(pos, token, initialize=true) {
|
|||||||
if (initialize && !node.edges) {
|
if (initialize && !node.edges) {
|
||||||
node.edges = [];
|
node.edges = [];
|
||||||
for (const neighborPos of canvas.grid.grid.getNeighbors(pos.y, pos.x).map(([y, x]) => {return {x, y};})) {
|
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)
|
if (neighborPos.x < 0 || neighborPos.y < 0 || neighborPos.x > gridWidth || neighborPos.y > gridHeight) {
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO Work with pixels instead of grid locations
|
// TODO Work with pixels instead of grid locations
|
||||||
if (!stepCollidesWithWall(pos, neighborPos, token)) {
|
if (!stepCollidesWithWall(neighborPos, pos, token)) {
|
||||||
const isDiagonal = node.x !== neighborPos.x && node.y !== neighborPos.y && canvas.grid.type === CONST.GRID_TYPES.SQUARE;
|
const isDiagonal = node.x !== neighborPos.x && node.y !== neighborPos.y && canvas.grid.type === CONST.GRID_TYPES.SQUARE;
|
||||||
let targetLayer = pos.layer;
|
let targetLayer = pos.layer;
|
||||||
if (use5105 && isDiagonal)
|
if (use5105 && isDiagonal)
|
||||||
@@ -135,11 +154,40 @@ function stepCollidesWithWall(from, to, token) {
|
|||||||
return canvas.walls.checkCollision(new Ray(stepStart, stepEnd));
|
return canvas.walls.checkCollision(new Ray(stepStart, stepEnd));
|
||||||
}
|
}
|
||||||
|
|
||||||
function paintPathfindingDebug(lastNode, token) {
|
export function wipePathfindingCache() {
|
||||||
|
cachedNodes = undefined;
|
||||||
|
for (const pathfinder of gridlessPathfinders.values()) {
|
||||||
|
GridlessPathfinding.free(pathfinder);
|
||||||
|
}
|
||||||
|
gridlessPathfinders.clear();
|
||||||
|
if (debugGraphics)
|
||||||
|
debugGraphics.removeChildren().forEach(c => c.destroy());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current cache is still suitable for the path we're about to find. If not, clear the cache
|
||||||
|
*/
|
||||||
|
function checkCacheValid(token) {
|
||||||
|
// If levels is enabled, the cache is invalid if it was made for a
|
||||||
|
if (game.modules.get("levels")?.active) {
|
||||||
|
const tokenElevation = token.data.elevation;
|
||||||
|
if (tokenElevation !== cacheElevation) {
|
||||||
|
cacheElevation = tokenElevation;
|
||||||
|
wipePathfindingCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initializePathfinding() {
|
||||||
|
gridWidth = Math.ceil(canvas.dimensions.width / canvas.grid.w);
|
||||||
|
gridHeight = Math.ceil(canvas.dimensions.height / canvas.grid.h);
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintGriddedPathfindingDebug(lastNode, token) {
|
||||||
if (!CONFIG.debug.dragRuler)
|
if (!CONFIG.debug.dragRuler)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
debugGraphics.removeChildren();
|
debugGraphics.removeChildren().forEach(c => c.destroy());
|
||||||
let currentNode = lastNode;
|
let currentNode = lastNode;
|
||||||
while (currentNode) {
|
while (currentNode) {
|
||||||
let text = new PIXI.Text(currentNode.cost.toFixed(0));
|
let text = new PIXI.Text(currentNode.cost.toFixed(0));
|
||||||
@@ -151,3 +199,16 @@ function paintPathfindingDebug(lastNode, token) {
|
|||||||
currentNode = currentNode.previous;
|
currentNode = currentNode.previous;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ import {currentSpeedProvider, getColorForDistanceAndToken, getRangesFromSpeedPro
|
|||||||
import {getHexSizeSupportTokenGridCenter} from "./compatibility.js";
|
import {getHexSizeSupportTokenGridCenter} from "./compatibility.js";
|
||||||
import {cancelScheduledMeasurement, measure} from "./foundry_imports.js"
|
import {cancelScheduledMeasurement, measure} from "./foundry_imports.js"
|
||||||
import {getMovementHistory} from "./movement_tracking.js";
|
import {getMovementHistory} from "./movement_tracking.js";
|
||||||
import {wipePathfindingCache} from "./pathfinding.js";
|
|
||||||
import {settingsKey} from "./settings.js";
|
import {settingsKey} from "./settings.js";
|
||||||
import {getSnapPointForEntity} from "./util.js";
|
import {getSnapPointForEntity} from "./util.js";
|
||||||
|
|
||||||
@@ -55,12 +54,18 @@ export function extendRuler() {
|
|||||||
else
|
else
|
||||||
this.draggedEntity = canvas.templates.get(data.draggedEntity);
|
this.draggedEntity = canvas.templates.get(data.draggedEntity);
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
this.draggedEntity = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
super.update(data);
|
super.update(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
measure(destination, options={}) {
|
measure(destination, options={}) {
|
||||||
if (this.isDragRuler) {
|
if (this.isDragRuler) {
|
||||||
|
// If this is the ruler of a remote user take the waypoints as they were transmitted and don't apply any additional snapping to them
|
||||||
|
if (this.user !== game.user)
|
||||||
|
options.snap = false;
|
||||||
return measure.call(this, destination, options);
|
return measure.call(this, destination, options);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -184,7 +189,6 @@ export function extendRuler() {
|
|||||||
return;
|
return;
|
||||||
const ruler = canvas.controls.ruler;
|
const ruler = canvas.controls.ruler;
|
||||||
ruler.clear();
|
ruler.clear();
|
||||||
wipePathfindingCache();
|
|
||||||
ruler._state = Ruler.STATES.STARTING;
|
ruler._state = Ruler.STATES.STARTING;
|
||||||
let entityCenter;
|
let entityCenter;
|
||||||
if (isToken && canvas.grid.isHex && game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(entity))
|
if (isToken && canvas.grid.isHex && game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(entity))
|
||||||
@@ -199,6 +203,12 @@ export function extendRuler() {
|
|||||||
if (measureImmediately)
|
if (measureImmediately)
|
||||||
ruler.measure(destination, options);
|
ruler.measure(destination, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dragRulerSendState() {
|
||||||
|
game.user.broadcastActivity({
|
||||||
|
ruler: this.toJSON()
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ruler = DragRulerRuler;
|
Ruler = DragRulerRuler;
|
||||||
@@ -10,6 +10,10 @@ export const RightClickAction = Object.freeze({
|
|||||||
ABORT_DRAG:2,
|
ABORT_DRAG:2,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function delayedReload() {
|
||||||
|
window.setTimeout(() => location.reload(), 500);
|
||||||
|
}
|
||||||
|
|
||||||
export function registerSettings() {
|
export function registerSettings() {
|
||||||
game.settings.register(settingsKey, "dataVersion", {
|
game.settings.register(settingsKey, "dataVersion", {
|
||||||
scope: "world",
|
scope: "world",
|
||||||
@@ -90,14 +94,14 @@ export function registerSettings() {
|
|||||||
config: true,
|
config: true,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
onChange: () => location.reload(),
|
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",
|
||||||
scpoe: "client",
|
scpoe: "client",
|
||||||
config: early_isGM(),
|
config: early_isGM() || game.settings.get(settingsKey, "allowPathfinding"),
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
defualt: false,
|
defualt: false,
|
||||||
});
|
});
|
||||||
@@ -39,5 +39,7 @@ export function recalculate(tokens) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _socketRecalculate(tokenIds) {
|
function _socketRecalculate(tokenIds) {
|
||||||
return canvas.controls.ruler.dragRulerRecalculate(tokenIds);
|
const ruler = canvas.controls.ruler;
|
||||||
|
if (ruler.isDragRuler)
|
||||||
|
ruler.dragRulerRecalculate(tokenIds);
|
||||||
}
|
}
|
||||||
@@ -4,13 +4,15 @@ export function getDefaultSpeedAttribute() {
|
|||||||
return "actor.data.data.attribs.mov.value";
|
return "actor.data.data.attribs.mov.value";
|
||||||
case "dcc":
|
case "dcc":
|
||||||
return "actor.data.data.attributes.speed.value";
|
return "actor.data.data.attributes.speed.value";
|
||||||
|
case "dnd4e":
|
||||||
|
return "actor.data.data.movement.walk.value";
|
||||||
case "dnd5e":
|
case "dnd5e":
|
||||||
return "actor.data.data.attributes.movement.walk"
|
return "actor.data.data.attributes.movement.walk";
|
||||||
case "lancer":
|
case "lancer":
|
||||||
return "actor.data.data.derived.speed"
|
return "actor.data.data.derived.speed";
|
||||||
case "pf1":
|
case "pf1":
|
||||||
case "D35E":
|
case "D35E":
|
||||||
return "actor.data.data.attributes.speed.land.total"
|
return "actor.data.data.attributes.speed.land.total";
|
||||||
case "sfrpg":
|
case "sfrpg":
|
||||||
return "actor.data.data.attributes.speed.value";
|
return "actor.data.data.attributes.speed.value";
|
||||||
case "shadowrun5e":
|
case "shadowrun5e":
|
||||||
@@ -28,6 +30,7 @@ export function getDefaultDashMultiplier() {
|
|||||||
case "swade":
|
case "swade":
|
||||||
return 0
|
return 0
|
||||||
case "dcc":
|
case "dcc":
|
||||||
|
case "dnd4e":
|
||||||
case "dnd5e":
|
case "dnd5e":
|
||||||
case "lancer":
|
case "lancer":
|
||||||
case "pf1":
|
case "pf1":
|
||||||
@@ -35,9 +38,9 @@ export function getDefaultDashMultiplier() {
|
|||||||
case "sfrpg":
|
case "sfrpg":
|
||||||
case "shadowrun5e":
|
case "shadowrun5e":
|
||||||
case "ds4":
|
case "ds4":
|
||||||
return 2
|
return 2;
|
||||||
case "CoC7":
|
case "CoC7":
|
||||||
return 5;
|
return 5;
|
||||||
}
|
}
|
||||||
return 0
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -94,23 +94,7 @@ export function getSnapPointForMeasuredTemplate(x, y) {
|
|||||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||||
return new PIXI.Point(x, y);
|
return new PIXI.Point(x, y);
|
||||||
}
|
}
|
||||||
let subgridWidth, subgridHeight;
|
return canvas.grid.grid.getSnappedPosition(x, y, canvas.templates.gridPosition);
|
||||||
if (canvas.grid.type === CONST.GRID_TYPES.SQUARE) {
|
|
||||||
subgridWidth = subgridHeight = canvas.dimensions.size / 2;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (canvas.grid.grid.columns) {
|
|
||||||
subgridWidth = canvas.grid.w / 4;
|
|
||||||
subgridHeight = canvas.grid.h / 2;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
subgridWidth = canvas.grid.w / 2;
|
|
||||||
subgridHeight = canvas.grid.h / 4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const snappedX = Math.round(x / subgridWidth) * subgridWidth;
|
|
||||||
const snappedY = Math.round(y / subgridHeight) * subgridHeight;
|
|
||||||
return new PIXI.Point(snappedX, snappedY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSnapPointForEntity(x, y, entity) {
|
export function getSnapPointForEntity(x, y, entity) {
|
||||||
@@ -44,9 +44,17 @@
|
|||||||
"moveWithoutAnimation": {
|
"moveWithoutAnimation": {
|
||||||
"name": "Deshabilitar animación de icono",
|
"name": "Deshabilitar animación de icono",
|
||||||
"hint": "Si al soltar un icono se presiona esta tecla, se deshabilitará la animación del icono al moverse al destino"
|
"hint": "Si al soltar un icono se presiona esta tecla, se deshabilitará la animación del icono al moverse al destino"
|
||||||
|
},
|
||||||
|
"togglePathfinding": {
|
||||||
|
"name": "Conmutar búsqueda de camino",
|
||||||
|
"hint": "Cuando se presione al arrastrar un icono, la funcionalidad de búsqueda de camino será temporalmente habilitada/deshabilitada"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"allowPathfinding": {
|
||||||
|
"name": "Permitir búsqueda de camino a los jugadores",
|
||||||
|
"hint": "Permite a los jugadores usar en este mundo la funcionalidad de búsqueda de camino. Tenga cuidado porque la ruta puede transcurrir por lugares con niebla de guerra o muros invisibles, lo que podrá revelar algunos secretos a los jugadores antes de tiempo"
|
||||||
|
},
|
||||||
"alwaysShowSpeedForPCs": {
|
"alwaysShowSpeedForPCs": {
|
||||||
"name": "Mostrar velocidad de los PJs a todo el mundo",
|
"name": "Mostrar velocidad de los PJs a todo el mundo",
|
||||||
"hint": "Si se habilita, se mostrará a todo el mundo los códigos de colores de las rutas de los PJs, incluso si no tienen permisos de observador para ese personaje"
|
"hint": "Si se habilita, se mostrará a todo el mundo los códigos de colores de las rutas de los PJs, incluso si no tienen permisos de observador para ese personaje"
|
||||||
@@ -55,6 +63,10 @@
|
|||||||
"name": "Comenzar a medir automáticamente",
|
"name": "Comenzar a medir automáticamente",
|
||||||
"hint": "Si se habilita, Drag Ruler comenzará a medir en cuanto se comience a arrastrar un icono. Si se deshabilita, Drag Ruler permanecerá inactivo y comenzará a medir únicamente cuando se presione el botón configurado para añadir un nuevo punto de ruta"
|
"hint": "Si se habilita, Drag Ruler comenzará a medir en cuanto se comience a arrastrar un icono. Si se deshabilita, Drag Ruler permanecerá inactivo y comenzará a medir únicamente cuando se presione el botón configurado para añadir un nuevo punto de ruta"
|
||||||
},
|
},
|
||||||
|
"autoPathfinding": {
|
||||||
|
"name": "Búsqueda de camino por defecto",
|
||||||
|
"hint": "Si se habilita, al arrastrar un icono se usará automáticamente la regla de búsqueda de camino"
|
||||||
|
},
|
||||||
"enableMovementHistory": {
|
"enableMovementHistory": {
|
||||||
"name": "Habilitar historial de movimiento durante el combate",
|
"name": "Habilitar historial de movimiento durante el combate",
|
||||||
"hint": "Si se habilita, Drag Ruler recordará la ruta que ha seguido un icono en su turno y la mostrará al seleccionarlo de nuevo"
|
"hint": "Si se habilita, Drag Ruler recordará la ruta que ha seguido un icono en su turno y la mostrará al seleccionarlo de nuevo"
|
||||||
|
|||||||
+5
-5
@@ -2,7 +2,7 @@
|
|||||||
"name": "drag-ruler",
|
"name": "drag-ruler",
|
||||||
"title": "Drag Ruler",
|
"title": "Drag Ruler",
|
||||||
"description": "When dragging a token displays a ruler showing how far you've moved that token.",
|
"description": "When dragging a token displays a ruler showing how far you've moved that token.",
|
||||||
"version": "1.11.2",
|
"version": "1.12.6",
|
||||||
"minimumCoreVersion" : "9.245",
|
"minimumCoreVersion" : "9.245",
|
||||||
"compatibleCoreVersion" : "9",
|
"compatibleCoreVersion" : "9",
|
||||||
"authors": [
|
"authors": [
|
||||||
@@ -13,9 +13,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"esmodules": [
|
"esmodules": [
|
||||||
"src/libwrapper_shim.js",
|
"js/libwrapper_shim.js",
|
||||||
"src/main.js",
|
"js/main.js",
|
||||||
"src/socket.js"
|
"js/socket.js"
|
||||||
],
|
],
|
||||||
"templates": [
|
"templates": [
|
||||||
"speed_provider_settings.html"
|
"speed_provider_settings.html"
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
],
|
],
|
||||||
"socket": true,
|
"socket": true,
|
||||||
"url": "https://github.com/manuelVo/foundryvtt-drag-ruler",
|
"url": "https://github.com/manuelVo/foundryvtt-drag-ruler",
|
||||||
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/archive/v1.11.2.zip",
|
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/releases/download/v1.12.6/drag-ruler-1.12.6.zip",
|
||||||
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/master/module.json",
|
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/master/module.json",
|
||||||
"readme": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/README.md",
|
"readme": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/README.md",
|
||||||
"changelog": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/CHANGELOG.md",
|
"changelog": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/CHANGELOG.md",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
target/
|
||||||
Generated
+154
@@ -0,0 +1,154 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
[[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 = "gridless-pathfinding"
|
||||||
|
version = "1.12.6"
|
||||||
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
|
"js-sys",
|
||||||
|
"rustc-hash",
|
||||||
|
"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 = "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 = "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 = "unicode-xid"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
||||||
|
|
||||||
|
[[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"
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "gridless-pathfinding"
|
||||||
|
version = "1.12.6"
|
||||||
|
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"
|
||||||
|
wasm-bindgen = "0.2.79"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
hard_tabs = true
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
use js_sys::Array;
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
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,
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user