Compare commits

..

38 Commits

Author SHA1 Message Date
Manuel Vögele 5add0be393 Release v1.12.5 2022-03-08 22:06:14 +01:00
Manuel Vögele f77990420a Replace getSnapPointForMeasuredTemplate with foundry native function (fixes #181) 2022-03-08 20:17:25 +01:00
Manuel Vögele e84632c0d7 Release v1.12.4 2022-03-08 12:49:56 +01:00
Manuel Vögele 30bd3d33e0 If no dragged entity is being transmitted, unset it (fixes #179) 2022-03-08 12:48:17 +01:00
Manuel Vögele df4c515fcd Release v1.12.3 2022-03-07 16:53:48 +01:00
Manuel Vögele 02dfd8db0d Only reclaculate the ruler if it's a Drag Ruler (fixes #161) 2022-03-07 14:39:31 +01:00
Manuel Vögele ddf89f9499 Add support for the wall height module in the gridless pathfinder 2022-03-07 12:40:16 +01:00
Jonathan Calvert 0dfdb23bfb Levels Compatibility: Clear pathfinding cache after changing elevation (#173) 2022-03-07 12:39:16 +01:00
Jonathan Calvert 3f2e9e1a3e Fix one-way wall collision detection (fixes #167) (#172) 2022-03-07 12:38:30 +01:00
Jonathan Calvert 576db2dc5a Pathfinding Improvements: Bound Algorithm to Canvas (#168) 2022-02-28 09:52:12 +01:00
EndlesNights b031acaa6e dnd 4e system support (#162) 2022-02-28 09:42:10 +01:00
Manuel Vögele ee956abd78 Update .gitignore 2022-02-17 11:15:40 +01:00
Manuel Vögele 1334703877 Release v1.12.2 2022-02-17 00:58:33 +01:00
Manuel Vögele d6cccf4466 Ensure pathfinding on gridless is possible by always generating a pathfinding point when a virtual wall is being inserted 2022-02-17 00:53:51 +01:00
Manuel Vögele 074d2f5052 Disable pathfinding when the hotkey for moving tokens without pathfinding is enabled (fixes #158) 2022-02-17 00:47:13 +01:00
Manuel Vögele e5b8f896f8 Release v1.12.1 2022-02-16 12:48:59 +01:00
Manuel Vögele 3de350d3e9 Remove wasm snippets from release builds 2022-02-16 12:47:23 +01:00
Manuel Vögele 8f97081236 Release v1.12.0 2022-02-16 11:57:33 +01:00
Manuel Vögele 18253dc42b Delay reload to make sure all settings are being applied 2022-02-16 11:46:46 +01:00
Manuel Vögele fb702cd850 Add support for gridless pathfinding 2022-02-15 17:56:08 +01:00
Manuel Vögele ad3fdf4d18 Release v1.11.5 2022-02-12 18:29:52 +01:00
Manuel Vögele 58946096a6 Remove debugging output 2022-02-12 18:25:50 +01:00
Manuel Vögele 22112ddfd5 Release v1.11.4 2022-02-09 10:58:02 +01:00
Manuel Vögele f59b0f9d2d When changing the measurement mode via a keybinding send the updated ruler state to other players immediately (fixes #152) 2022-02-09 10:51:19 +01:00
Manuel Vögele 443cfd4317 Don't apply snapping to rulers received from other players (fixes #150) 2022-02-09 10:41:01 +01:00
Manuel Vögele fb3665d758 If no measurement has been performed yet before dropping the token, schedule a measurement on drop (fixes #153) 2022-02-09 10:30:17 +01:00
Manuel Vögele 96cb690431 Use foundries new, improved way of wainting for animations when moving tokens (fixes #156) 2022-02-09 08:57:30 +01:00
José Lozano fb7202d5f0 Spanish updated (#155) 2022-02-07 10:07:07 +01:00
Manuel Vögele fa581ddfc3 Release v1.11.3 2022-02-03 18:23:35 +01:00
Manuel Vögele 56e15be4ca Don't perform pathfinding on other player's rulers 2022-02-03 17:59:35 +01:00
Manuel Vögele 1509307313 Show the setting for automatic pathfinding to players if they are allowed to use pathfinding 2022-02-03 17:59:09 +01:00
Manuel Vögele 68885385ea Formatting fix 2022-02-03 17:42:35 +01:00
Manuel Vögele 6748617f6a Add "DnD5e Drag Ruler Integration" and "Shadow of the Demon Lord" to list of Drag Ruler integraion 2022-02-01 20:57:44 +01:00
Manuel Vögele 9d5dccb504 Release v1.11.2 2022-02-01 11:35:46 +01:00
Manuel Vögele a252da620a Always allow the GM to use pathfinding 2022-02-01 11:33:33 +01:00
Johannes Loher 41c8979925 Add support for Dungeonslayers 4 (#146) 2022-02-01 10:32:54 +01:00
Manuel Vögele 39f9204fa7 Destroy labels that are no longer used 2022-02-01 02:58:55 +01:00
Manuel Vögele 10633e4e2a Add .gitignore 2022-01-30 14:28:02 +01:00
36 changed files with 1397 additions and 82 deletions
+4
View File
@@ -0,0 +1,4 @@
/foundry*.js
artifact/
wasm/
*.lock
+70
View File
@@ -1,3 +1,73 @@
## 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
### Bugfixes
- Fixed a memory leak that could cause the rule to slow down after using the pathfinding functionality for a while
### Misc
- GMs are now always allowed to use the pathfinding tool. The setting now only prevents players from using it.
### Compatibility
- Drag Ruler's generic speed provider is now aware of good defaults for Dungeonslayers 4
## 1.11.1
### Bugfixes
- Fixed a bug that would cause the pathfinding algorithm to make tokens of size 2 and 4 to take an unnecessary step
+2
View File
@@ -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:
- 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)
- Ironclaw Second Edition (starting with version 0.2.2)
- 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 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))
- 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))
- Stargate RPG (starting with version 1.6.0)
- Symbaroum (via the module [Symbaroum drag ruler integration](https://foundryvtt.com/packages/symbaroum-drag-ruler-integration))
+50
View File
@@ -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
View File
@@ -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}"])
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
cargo install cargo-watch
cargo install wasm-pack
View File
@@ -91,7 +91,7 @@ async function animateEntities(entities, draggedEntity, draggedRays, wasPaused)
});
await draggedEntity.scene.updateEmbeddedDocuments(draggedEntity.constructor.embeddedName, updates, {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
if (this.cancelMovement) {
@@ -168,7 +168,7 @@ export function measure(destination, options={}) {
this.dragRulerRemovePathfindingWaypoints();
if (isToken && isPathfindingEnabled()) {
if (isToken && isPathfindingEnabled.call(this)) {
const from = getGridPositionFromPixelsObj(this.waypoints[this.waypoints.length - 1]);
const to = getGridPositionFromPixelsObj(destination);
let path = findPath(from, to, this.draggedEntity, this.waypoints);
View File
+20 -9
View File
@@ -52,15 +52,14 @@ export function registerKeybindings() {
precedence: -1,
});
if (game.settings.get(settingsKey, "allowPathfinding")) {
game.keybindings.register(settingsKey, "togglePathfinding", {
name: "drag-ruler.keybindings.togglePathfinding.name",
hint: "drag-ruler.keybindings.togglePathfinding.hint",
onDown: handleTogglePathfinding,
onUp: handleTogglePathfinding,
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"),
});
}
function handleDeleteWaypoint() {
@@ -108,11 +107,22 @@ function handleDisableSnap(event) {
return false;
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
ruler.dragRulerSendState();
return false;
}
function handleMoveWithoutAnimation(event) {
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) {
@@ -125,5 +135,6 @@ function handleTogglePathfinding(event) {
return false;
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
ruler.dragRulerSendState();
return false;
}
+17
View File
@@ -7,15 +7,29 @@ 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()
@@ -137,6 +151,9 @@ function onEntityDragLeftDrop(event) {
// This can happen if the user presses ESC during drag (maybe there are other ways too)
if (selectedTokens.length === 0)
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
moveEntities.call(ruler, ruler.draggedEntity, selectedTokens);
return true
+88 -27
View File
@@ -1,41 +1,58 @@
import {getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
import {togglePathfinding} from "./keybindings.js";
import {moveWithoutAnimation, togglePathfinding} from "./keybindings.js";
import {debugGraphics} from "./main.js";
import {settingsKey} from "./settings.js";
import {getSnapPointForTokenObj, iterPairs} from "./util.js";
import * as GridlessPathfinding from "../wasm/gridless_pathfinding.js"
let cachedNodes = undefined;
let cacheElevation;
let use5105 = false;
let gridlessPathfinders = new Map();
let gridWidth, gridHeight;
export function isPathfindingEnabled() {
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS)
if (this.user !== game.user)
return false;
if (!game.settings.get(settingsKey, "allowPathfinding"))
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) {
const lastNode = calculatePath(from, to, token, previousWaypoints);
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;
}
checkCacheValid(token);
export function wipePathfindingCache() {
cachedNodes = undefined;
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) {
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) {
@@ -45,7 +62,7 @@ function getNode(pos, token, initialize=true) {
if (!cachedNodes[pos.layer])
cachedNodes[pos.layer] = new Array(Math.ceil(canvas.dimensions.height / canvas.grid.h));
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]) {
cachedNodes[pos.layer][pos.y][pos.x] = pos;
}
@@ -54,10 +71,12 @@ function getNode(pos, token, initialize=true) {
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)
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, token)) {
if (!stepCollidesWithWall(neighborPos, pos, token)) {
const isDiagonal = node.x !== neighborPos.x && node.y !== neighborPos.y && canvas.grid.type === CONST.GRID_TYPES.SQUARE;
let targetLayer = pos.layer;
if (use5105 && isDiagonal)
@@ -135,11 +154,40 @@ function stepCollidesWithWall(from, to, token) {
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)
return;
debugGraphics.removeChildren();
debugGraphics.removeChildren().forEach(c => c.destroy());
let currentNode = lastNode;
while (currentNode) {
let text = new PIXI.Text(currentNode.cost.toFixed(0));
@@ -151,3 +199,16 @@ function paintPathfindingDebug(lastNode, token) {
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);
}
+13 -3
View File
@@ -2,7 +2,6 @@ import {currentSpeedProvider, getColorForDistanceAndToken, getRangesFromSpeedPro
import {getHexSizeSupportTokenGridCenter} from "./compatibility.js";
import {cancelScheduledMeasurement, measure} from "./foundry_imports.js"
import {getMovementHistory} from "./movement_tracking.js";
import {wipePathfindingCache} from "./pathfinding.js";
import {settingsKey} from "./settings.js";
import {getSnapPointForEntity} from "./util.js";
@@ -55,12 +54,18 @@ export function extendRuler() {
else
this.draggedEntity = canvas.templates.get(data.draggedEntity);
}
else {
this.draggedEntity = undefined;
}
super.update(data);
}
measure(destination, options={}) {
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);
}
else {
@@ -116,7 +121,7 @@ export function extendRuler() {
}
dragRulerRemovePathfindingWaypoints() {
this.waypoints.filter(waypoint => waypoint.isPathfinding).forEach(_ => this.labels.removeChild(this.labels.children.pop()));
this.waypoints.filter(waypoint => waypoint.isPathfinding).forEach(_ => this.labels.removeChild(this.labels.children[this.labels.children.length - 1]).destroy());
this.waypoints = this.waypoints.filter(waypoint => !waypoint.isPathfinding);
}
@@ -184,7 +189,6 @@ export function extendRuler() {
return;
const ruler = canvas.controls.ruler;
ruler.clear();
wipePathfindingCache();
ruler._state = Ruler.STATES.STARTING;
let entityCenter;
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)
ruler.measure(destination, options);
}
dragRulerSendState() {
game.user.broadcastActivity({
ruler: this.toJSON()
});
}
}
Ruler = DragRulerRuler;
+7 -2
View File
@@ -1,5 +1,6 @@
import {availableSpeedProviders, currentSpeedProvider, getDefaultSpeedProvider, updateSpeedProvider} from "./api.js";
import {SpeedProvider} from "./speed_provider.js"
import { early_isGM } from "./util.js";
export const settingsKey = "drag-ruler";
@@ -9,6 +10,10 @@ export const RightClickAction = Object.freeze({
ABORT_DRAG:2,
});
function delayedReload() {
window.setTimeout(() => location.reload(), 500);
}
export function registerSettings() {
game.settings.register(settingsKey, "dataVersion", {
scope: "world",
@@ -89,14 +94,14 @@ export function registerSettings() {
config: true,
type: Boolean,
default: false,
onChange: () => location.reload(),
onChange: delayedReload,
});
game.settings.register(settingsKey, "autoPathfinding", {
name: "drag-ruler.settings.autoPathfinding.name",
hint: "drag-ruler.settings.autoPathfinding.hint",
scpoe: "client",
config: true,
config: early_isGM() || game.settings.get(settingsKey, "allowPathfinding"),
type: Boolean,
defualt: false,
});
+3 -1
View File
@@ -39,5 +39,7 @@ export function recalculate(tokens) {
}
function _socketRecalculate(tokenIds) {
return canvas.controls.ruler.dragRulerRecalculate(tokenIds);
const ruler = canvas.controls.ruler;
if (ruler.isDragRuler)
ruler.dragRulerRecalculate(tokenIds);
}
+11 -5
View File
@@ -4,19 +4,23 @@ export function getDefaultSpeedAttribute() {
return "actor.data.data.attribs.mov.value";
case "dcc":
return "actor.data.data.attributes.speed.value";
case "dnd4e":
return "actor.data.data.movement.walk.value";
case "dnd5e":
return "actor.data.data.attributes.movement.walk"
return "actor.data.data.attributes.movement.walk";
case "lancer":
return "actor.data.data.derived.speed"
return "actor.data.data.derived.speed";
case "pf1":
case "D35E":
return "actor.data.data.attributes.speed.land.total"
return "actor.data.data.attributes.speed.land.total";
case "sfrpg":
return "actor.data.data.attributes.speed.value";
case "shadowrun5e":
return "actor.data.data.movement.walk.value";
case "swade":
return "actor.data.data.stats.speed.adjusted";
case "ds4":
return "actor.data.data.combatValues.movement.total";
}
return ""
}
@@ -26,15 +30,17 @@ export function getDefaultDashMultiplier() {
case "swade":
return 0
case "dcc":
case "dnd4e":
case "dnd5e":
case "lancer":
case "pf1":
case "D35E":
case "sfrpg":
case "shadowrun5e":
return 2
case "ds4":
return 2;
case "CoC7":
return 5;
}
return 0
return 0;
}
+8 -24
View File
@@ -90,35 +90,12 @@ export function getSnapPointForTokenObj(pos, token) {
return getSnapPointForToken(pos.x, pos.y, token);
}
export function getSnapPointForMeasuredTemplate(x, y) {
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
return new PIXI.Point(x, y);
}
let subgridWidth, subgridHeight;
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) {
const isToken = entity instanceof Token;
if (isToken)
return getSnapPointForToken(x, y, entity);
else
return getSnapPointForMeasuredTemplate(x, y);
return canvas.grid.grid.getSnappedPosition(x, y, canvas.templates.gridPosition);
}
export function highlightTokenShape(position, shape, color, alpha) {
@@ -272,3 +249,10 @@ export function getMeasurePosition() {
const measurePosition = {x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y};
return measurePosition;
}
// isGM function for use during loading when game.user isn't available yet
export function early_isGM() {
const level = game.data.users.find(u => u._id == game.data.userId).role;
const gmLevel = CONST.USER_ROLES.ASSISTANT;
return level >= gmLevel;
}
+2 -2
View File
@@ -48,8 +48,8 @@
},
"settings": {
"allowPathfinding": {
"name": "Wegfindung aktivieren",
"hint": "Aktiviert Drag Ruler's Wegfindungsfunktion. Bitte beachte, dass die Wegfindung Wege durch unerkundeten Nebel des Kriegs und Ätherische Wände berechnen kann. Dies kann deinen Spielern Geheimnisse lüften, von denen sie noch nicht erfahren sollten."
"name": "Wegfindung für Spieler erlauben",
"hint": "Erlaubt es Spielern die Wegfindungs zu benutzen. Bitte beachte, dass die Wegfindung Wege durch unerkundeten Nebel des Kriegs und Ätherische Wände berechnen kann. Dies kann deinen Spielern Geheimnisse lüften, von denen sie noch nicht erfahren sollten."
},
"alwaysShowSpeedForPCs": {
"name": "Geschwindigkeit von Spielercharakteren für jeden anzeigen",
+2 -2
View File
@@ -48,8 +48,8 @@
},
"settings": {
"allowPathfinding": {
"name": "Enable pathfinding feature",
"hint": "Enables Drag Ruler's pathfinding functionality in this world. Be aware that pathfinding can route through unexplored fog of war and Ethereal Walls, which might reveal secrets to your players ahead of time."
"name": "Allow pathfinding for players",
"hint": "Allows players to use Drag Ruler's pathfinding functionality in this world. Be aware that pathfinding can route through unexplored fog of war and Ethereal Walls, which might reveal secrets to your players ahead of time."
},
"alwaysShowSpeedForPCs": {
"name": "Show PC speed to everyone",
+12
View File
@@ -44,9 +44,17 @@
"moveWithoutAnimation": {
"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"
},
"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": {
"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": {
"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"
@@ -55,6 +63,10 @@
"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"
},
"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": {
"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"
+5 -5
View File
@@ -2,7 +2,7 @@
"name": "drag-ruler",
"title": "Drag Ruler",
"description": "When dragging a token displays a ruler showing how far you've moved that token.",
"version": "1.11.1",
"version": "1.12.5",
"minimumCoreVersion" : "9.245",
"compatibleCoreVersion" : "9",
"authors": [
@@ -13,9 +13,9 @@
}
],
"esmodules": [
"src/libwrapper_shim.js",
"src/main.js",
"src/socket.js"
"js/libwrapper_shim.js",
"js/main.js",
"js/socket.js"
],
"templates": [
"speed_provider_settings.html"
@@ -65,7 +65,7 @@
],
"socket": true,
"url": "https://github.com/manuelVo/foundryvtt-drag-ruler",
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/archive/v1.11.1.zip",
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/releases/download/v1.12.5/drag-ruler-1.12.5.zip",
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/master/module.json",
"readme": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/README.md",
"changelog": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/CHANGELOG.md",
+1
View File
@@ -0,0 +1 @@
target/
+154
View File
@@ -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.5"
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"
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "gridless-pathfinding"
version = "1.12.5"
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"
+1
View File
@@ -0,0 +1 @@
hard_tabs = true
+187
View File
@@ -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
}
+293
View File
@@ -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
}
}
}
+12
View File
@@ -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));
}
+330
View File
@@ -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,
}))
}
+68
View File
@@ -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())
}
}