diff --git a/Changelog.md b/Changelog.md index 350c433..50789ec 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,10 @@ +# 0.9.2 +Fix pathfinding. +Change the keybind for teleport to "F" (fast-forward) to avoid collision with arrow-key usage on the canvas. Switch to requiring "F" to be held when the user triggers the move to get teleport, to avoid weirdness with the drag still being active when using a separate trigger key. +Catch error if waypoint is not added in `_addWaypoint`. +Correct error when sending ruler data from one user to another. +Move Maximum speed category to `CONFIG.elevationruler.SPEED.CATEGORIES`. Should now be possible to define specific colors per user in the CONFIG, so long as category names are same. + # 0.9.1 Fix errors when using the ruler on gridless scenes. Correct speed highlighting on gridless scenes. diff --git a/languages/en.json b/languages/en.json index 77f8856..3d6d902 100644 --- a/languages/en.json +++ b/languages/en.json @@ -19,7 +19,7 @@ "elevationruler.keybindings.forceToGround.hint": "When measuring, press this key to get the distance to the ground. Press again to revert.", "elevationruler.keybindings.teleport.name": "Teleport along ruler", - "elevationruler.keybindings.teleport.hint": "When measuring, press this key to move the token to the ruler destination without animation", + "elevationruler.keybindings.teleport.hint": "When measuring, hold this key when you release the dragged token or hit the spacebar to move the token to the ruler destination without animation.", "elevationruler.settings.levels-use-floor-label.name": "Levels Floor Label", "elevationruler.settings.levels-use-floor-label.hint": "If Levels module is active, label the ruler with the current floor, if the Levels UI floors are named.", diff --git a/module.json b/module.json index b9cbbfe..5417691 100644 --- a/module.json +++ b/module.json @@ -8,7 +8,7 @@ "manifestPlusVersion": "1.0.0", "compatibility": { "minimum": "12", - "verified": "12.324" + "verified": "12.325" }, "authors": [ { diff --git a/scripts/Ruler.js b/scripts/Ruler.js index b37f1e8..fa9c161 100644 --- a/scripts/Ruler.js +++ b/scripts/Ruler.js @@ -1,5 +1,6 @@ /* globals canvas, +Color, CONFIG, CONST, foundry, @@ -15,7 +16,7 @@ export const PATCHES = {}; PATCHES.BASIC = {}; PATCHES.SPEED_HIGHLIGHTING = {}; -import { SPEED, MODULE_ID, MaximumSpeedCategory } from "./const.js"; +import { SPEED, MODULE_ID } from "./const.js"; import { Settings } from "./settings.js"; import { Ray3d } from "./geometry/3d/Ray3d.js"; import { Point3d } from "./geometry/3d/Point3d.js"; @@ -100,9 +101,11 @@ function _getMeasurementData(wrapper) { B: s.ray.B }; newObj.label = Boolean(s.label); + newObj.speed ??= newObj.speed.name; return newObj; }); + myObj._userElevationIncrements = this._userElevationIncrements; myObj._unsnap = this._unsnap; myObj._unsnappedOrigin = this._unsnappedOrigin; @@ -117,6 +120,7 @@ function _getMeasurementData(wrapper) { * Retrieve the current snap status. */ function update(wrapper, data) { + if ( !data || (data.state === Ruler.STATES.INACTIVE) ) return wrapper(data); const myData = data[MODULE_ID]; if ( !myData ) return wrapper(data); // Just in case. @@ -129,6 +133,7 @@ function update(wrapper, data) { // Reconstruct segments. if ( myData._segments ) this.segments = myData._segments.map(s => { s.ray = new Ray3d(s.ray.A, s.ray.B); + s.speed ??= SPEED.CATEGORIES.find(s => s.name === s.speed); return s; }); @@ -152,6 +157,10 @@ function update(wrapper, data) { function _addWaypoint(wrapper, point) { wrapper(point); + // In case the waypoint was never added. + if ( (this.state !== Ruler.STATES.STARTING) && (this.state !== Ruler.STATES.MEASURING ) ) return; + if ( !this.waypoints.length ) return; + // If shift was held, use the precise point. if ( this._unsnap ) { const lastWaypoint = this.waypoints.at(-1); @@ -397,20 +406,17 @@ function _computeTokenSpeed() { let segment; // Debugging -// if ( this.segments[0].moveDistance > 25 ) log(`${this.segments[0].moveDistance}`); -// if ( this.segments[0].moveDistance > 30 ) log(`${this.segments[0].moveDistance}`); -// if ( this.segments[0].moveDistance > 50 ) log(`${this.segments[0].moveDistance}`); -// if ( this.segments[0].moveDistance > 60 ) log(`${this.segments[0].moveDistance}`); + if ( CONFIG[MODULE_ID].debug ) { + if ( this.segments[0].moveDistance > 25 ) log(`${this.segments[0].moveDistance}`); + if ( this.segments[0].moveDistance > 30 ) log(`${this.segments[0].moveDistance}`); + if ( this.segments[0].moveDistance > 50 ) log(`${this.segments[0].moveDistance}`); + if ( this.segments[0].moveDistance > 60 ) log(`${this.segments[0].moveDistance}`); + } // Progress through each speed attribute in turn. - const categoryIter = [...SPEED.CATEGORIES, MaximumSpeedCategory].values(); - const maxDistFn = (token, speedCategory, tokenSpeed) => { - if ( speedCategory.name === "Maximum" ) return Number.POSITIVE_INFINITY; - return SPEED.maximumCategoryDistance(token, speedCategory, tokenSpeed); - }; - + const categoryIter = [...SPEED.CATEGORIES].values(); let speedCategory = categoryIter.next().value; - let maxDistance = maxDistFn(token, speedCategory, tokenSpeed); + let maxDistance = SPEED.maximumCategoryDistance(token, speedCategory, tokenSpeed); // Determine which speed category we are starting with // Add in already moved combat distance and determine the starting category @@ -424,10 +430,11 @@ function _computeTokenSpeed() { while ( (segment = this.segments[s]) ) { // Skip speed categories that do not provide a distance larger than the last. - while ( speedCategory.name !== "Maximum" && maxDistance <= minDistance ) { + while ( speedCategory && maxDistance <= minDistance ) { speedCategory = categoryIter.next().value; - maxDistance = maxDistFn(token, speedCategory, tokenSpeed); + maxDistance = SPEED.maximumCategoryDistance(token, speedCategory, tokenSpeed); } + if ( !speedCategory ) speedCategory = SPEED.CATEGORIES.at(-1); segment.speed = speedCategory; let newPrevDiagonal = _measureSegment(segment, token, numPrevDiagonal); @@ -608,6 +615,17 @@ function _onMouseUp(wrapped, event) { return wrapped(event); } +/** + * Wrap Ruler.prototype._onMoveKeyDown + * If the teleport key is held, teleport the token. + * @param {KeyboardEventContext} context + */ +function _onMoveKeyDown(wrapped, context) { + const teleportKeys = new Set(game.keybindings.get(MODULE_ID, Settings.KEYBINDINGS.TELEPORT).map(binding => binding.key)); + if ( teleportKeys.intersects(game.keyboard.downKeys) ) this.segments.forEach(s => s.teleport = true); + wrapped(context); +} + PATCHES.BASIC.WRAPS = { _getMeasurementData, update, @@ -623,7 +641,8 @@ PATCHES.BASIC.WRAPS = { _onClickLeft, _onClickRight, _onMouseMove, - _canMove + _canMove, + _onMoveKeyDown }; PATCHES.BASIC.MIXES = { _animateMovement, _getMeasurementSegments, _onMouseUp }; @@ -677,7 +696,7 @@ function decrementElevation() { * Move the token and stop the ruler measurement * @returns {boolean} False if the movement did not occur */ -async function teleport(context) { +async function teleport(_context) { if ( this._state !== this.constructor.STATES.MEASURING ) return false; if ( !this._canMove(this.token) ) return false; diff --git a/scripts/Token.js b/scripts/Token.js index ba66125..2d5b906 100644 --- a/scripts/Token.js +++ b/scripts/Token.js @@ -76,8 +76,7 @@ async function _onDragLeftDrop(wrapped, event) { // } // ruler._state = Ruler.STATES.MOVING; // Do NOT set state to MOVING here in v12, as it will break the canvas. - await ruler.moveToken(); - ruler._onMouseUp(event); + ruler._onMoveKeyDown(event); // Movement is async here but not awaited in _onMoveKeyDown. } /** diff --git a/scripts/const.js b/scripts/const.js index 1ac80a9..027d3ba 100644 --- a/scripts/const.js +++ b/scripts/const.js @@ -97,6 +97,12 @@ const DashSpeedCategory = { multiplier: 2 }; +const MaximumSpeedCategory = { + Name: "Maximum", + color: Color.from(0xff0000), + multiplier: Number.POSITIVE_INFINITY +} + export const SPEED = { /** * Object of strings indicating where on the actor to locate the given attribute. @@ -110,13 +116,7 @@ export const SPEED = { * in the first category is the next category considered. * @type {SpeedCategory[]} */ - CATEGORIES: [WalkSpeedCategory, DashSpeedCategory], - - /** - * Color to use once all SpeedCategory distances have been exceeded. - * @type {Color} - */ - MAXIMUM_COLOR: Color.from(0xff0000), + CATEGORIES: [WalkSpeedCategory, DashSpeedCategory, MaximumSpeedCategory], // Use Font Awesome font unicode instead of basic unicode for displaying terrain symbol. @@ -134,14 +134,6 @@ export const SPEED = { terrainSymbol: "🥾" }; -export const MaximumSpeedCategory = { - name: "Maximum", - multiplier: Number.POSITIVE_INFINITY -}; - -Object.defineProperty(MaximumSpeedCategory, "color", { - get: () => SPEED.MAXIMUM_COLOR -}); /** * Given a token, get the maximum distance the token can travel for a given type. @@ -179,8 +171,6 @@ Hooks.once("init", function() { DashSpeedCategory.multiplier = defaultDashMultiplier(); }); - -/* eslint-disable no-multi-spaces */ export function defaultHPAttribute() { switch ( game.system.id ) { case "dnd5e": return "actor.system.attributes.hp.value"; diff --git a/scripts/geometry b/scripts/geometry index 63cf692..e83109a 160000 --- a/scripts/geometry +++ b/scripts/geometry @@ -1 +1 @@ -Subproject commit 63cf692aec85196768f035d72ab2604ae0c66f78 +Subproject commit e83109a56265221deac7aefa8ef3186e2995bdd7 diff --git a/scripts/patching.js b/scripts/patching.js index 1d144fa..4147023 100644 --- a/scripts/patching.js +++ b/scripts/patching.js @@ -8,12 +8,12 @@ import { Patcher } from "./Patcher.js"; import { PATCHES as PATCHES_Ruler } from "./Ruler.js"; import { PATCHES as PATCHES_Token } from "./Token.js"; import { PATCHES as PATCHES_ClientKeybindings } from "./ClientKeybindings.js"; -import { PATCHES as PATCHES_TokenPF } from "./pathfinding/Token.js"; import { PATCHES as PATCHES_DrawingConfig } from "./DrawingConfig.js"; - // Pathfinding import { PATCHES as PATCHES_Wall } from "./pathfinding/Wall.js"; +import { PATCHES as PATCHES_CanvasEdges } from "./pathfinding/CanvasEdges.js"; +import { PATCHES as PATCHES_TokenPF } from "./pathfinding/Token.js"; // Movement tracking import { PATCHES as PATCHES_TokenHUD } from "./token_hud.js"; @@ -26,6 +26,7 @@ const mergeObject = foundry.utils.mergeObject; const PATCHES = { ClientKeybindings: PATCHES_ClientKeybindings, ClientSettings: PATCHES_ClientSettings, + ["foundry.canvas.edges.CanvasEdges"]: PATCHES_CanvasEdges, DrawingConfig: PATCHES_DrawingConfig, Ruler: PATCHES_Ruler, Token: mergeObject(mergeObject(PATCHES_Token, PATCHES_TokenPF), PATCHES_TokenHUD), diff --git a/scripts/pathfinding/BorderTriangle.js b/scripts/pathfinding/BorderTriangle.js index de3dfa4..6894160 100644 --- a/scripts/pathfinding/BorderTriangle.js +++ b/scripts/pathfinding/BorderTriangle.js @@ -15,6 +15,16 @@ import { PhysicalDistance } from "../PhysicalDistance.js"; import { Draw } from "../geometry/Draw.js"; import { WallTracerEdge } from "./WallTracer.js"; +const OTHER_DIRECTION = { + ccw: "cw", + cw: "ccw" +}; + +const OTHER_TRIANGLE = { + cwTriangle: "ccwTriangle", + ccwTriangle: "cwTriangle" +}; + /** * An edge that makes up the triangle-shaped polygon */ @@ -86,8 +96,14 @@ export class BorderEdge { */ findTriangleFromVertexKey(vertexKey, dir = "ccw") { const [a, b] = this.a.key === vertexKey ? [this.a, this.b] : [this.b, this.a]; - const cCCW = this._nonSharedVertex(this.ccwTriangle); - return (foundry.utils.orient2dFast(a, b, cCCW) > 0) ^ (dir !== "ccw") ? this.ccwTriangle : this.cwTriangle; + + if ( this.ccwTriangle ) { + const cCCW = this._nonSharedVertex(this.ccwTriangle); + return (foundry.utils.orient2dFast(a, b, cCCW) > 0) ^ (dir !== "ccw") ? this.ccwTriangle : this.cwTriangle; + } else { + const cCW = this._nonSharedVertex(this.cwTriangle); + return (foundry.utils.orient2dFast(a, b, cCW) < 0) ^ (dir !== "cw") ? this.cwTriangle : this.ccwTriangle; + } } /** @@ -171,12 +187,13 @@ export class BorderEdge { */ edgeBlocks(origin, elevation = 0) { if ( !origin ) { - if ( !this.ccwTriangle.center || !this.cwTriangle.center) { - console.warn("edgeBlocks|Triangle centers not defined."); - return false; - } - return this.edgeBlocks(this.ccwTriangle.center, elevation) - || this.edgeBlocks(this.cwTriangle.center, elevation); +// if ( !this.ccwTriangle || !this.cwTriangle || !this.ccwTriangle.center || !this.cwTriangle.center) { +// console.warn("edgeBlocks|Triangle centers not defined."); +// return false; +// } + const ccwBlocks = this.ccwTriangle ? this.edgeBlocks(this.ccwTriangle.center, elevation) : false; + const cwBlocks = this.cwTriangle ? this.edgeBlocks(this.cwTriangle.center, elevation) : false; + return ccwBlocks || cwBlocks; } const { moveToken, tokenBlockType } = this.constructor; @@ -199,7 +216,18 @@ export class BorderEdge { const otherEndpoint = !this.endpointKeys.has(aTri.key) ? aTri : !this.endpointKeys.has(bTri.key) ? bTri : cTri; + + // Debugging + if ( !this.endpointKeys.has(aTri.key) + && !this.endpointKeys.has(bTri.key) + && !this.endpointKeys.has(cTri.key) ) console.error(`Triangle ${triangle.id} keys not found ${aTri.key}, ${bTri.key}, ${cTri.key}`, this); + const orient2d = foundry.utils.orient2dFast; + const oABE = orient2d(a, b, otherEndpoint); + + // Debugging + if ( oABE === 0 ) console.error(`Triangle ${triangle.id} collinear to this edge at ${otherEndpoint.x},${otherEndpoint.y}`, this); + if ( orient2d(a, b, otherEndpoint) > 0 ) this.ccwTriangle = triangle; else this.cwTriangle = triangle; } @@ -215,6 +243,7 @@ export class BorderEdge { vertexBlocks(vertexKey, elevation = 0) { const iter = this.sharedVertexEdges(vertexKey); for ( const edge of iter ) { + // if ( !edge.ccwTriangle || !edge.cwTriangle ) console.warn("vertexBlocks|Edge triangles not defined."); // Debugging. if ( edge === this ) continue; // Could break here b/c this edge implicitly is always last. if ( edge.edgeBlocks(undefined, elevation) ) return true; } @@ -247,9 +276,24 @@ export class BorderEdge { * @param {string} [direction] Either ccw or c. * @returns {BorderEdge} */ - _nextEdge(vertexKey, dir = "ccw") { + _nextEdge(vertexKey, dir = "ccw", _recurse = true) { const tri = this.findTriangleFromVertexKey(vertexKey, dir); - return Object.values(tri.edges).find(e => e !== this && e.endpointKeys.has(vertexKey)); + if ( tri ) return Object.values(tri.edges).find(e => e !== this && e.endpointKeys.has(vertexKey)); + + // Edge is at a border, vertex at the corner of the border. + // Need to run the opposite direction until we get undefined in that direction. + if ( !_recurse ) return null; + const maxIter = 100; + let iter = 0; + let edge = this; + let prevEdge; + const otherDir = OTHER_DIRECTION[dir]; + do { + prevEdge = edge; + iter += 1; + edge = prevEdge._nextEdge(vertexKey, otherDir, false); + } while ( iter < maxIter && edge && edge !== this ); + return prevEdge; } /** diff --git a/scripts/pathfinding/CanvasEdges.js b/scripts/pathfinding/CanvasEdges.js new file mode 100644 index 0000000..2774451 --- /dev/null +++ b/scripts/pathfinding/CanvasEdges.js @@ -0,0 +1,48 @@ +/* globals +canvas, +Wall +*/ +/* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */ +"use strict"; + +import { MODULE_ID } from "../const.js"; +import { SCENE_GRAPH } from "./WallTracer.js"; +import { Pathfinder } from "./pathfinding.js"; +import { Settings } from "../settings.js"; + +// Track wall creation, update, and deletion, constructing WallTracerEdges as we go. +// Use to update the pathfinding triangulation. + +export const PATCHES = {}; +PATCHES.PATHFINDING = {}; + +// ----- NOTE: Hooks ----- // + +/** + * Hook initializeEdges + * Set up the SCENE GRAPH with all wall edges. + */ +function initializeEdges() { + const t0 = performance.now(); + SCENE_GRAPH.clear(); + let numWalls = 0; + for ( const edge of canvas.edges.values() ) { + if ( edge.object instanceof Wall ) { + SCENE_GRAPH.addWall(edge.object); + numWalls += 1; + } else if ( edge.type === "outerBounds" + || edge.type === "innerBounds" ) SCENE_GRAPH.addCanvasEdge(edge); + } + + Settings.setTokenBlocksPathfinding(); + const t1 = performance.now(); + + // Use the scene graph to initialize Pathfinder triangulation. + Pathfinder.initialize(); + const t2 = performance.now(); + + console.debug(`${MODULE_ID}|Tracked ${numWalls} walls in ${t1 - t0} ms.`); + console.debug(`${MODULE_ID}|Initialized pathfinding in ${t2 - t1} ms.`); +} + +PATCHES.PATHFINDING.HOOKS = { initializeEdges }; diff --git a/scripts/pathfinding/PriorityQueue.js b/scripts/pathfinding/PriorityQueue.js index 40e7c45..662ecde 100644 --- a/scripts/pathfinding/PriorityQueue.js +++ b/scripts/pathfinding/PriorityQueue.js @@ -1,5 +1,6 @@ // Priority queue using a heap // from https://www.digitalocean.com/community/tutorials/js-binary-heaps +import { radixSortObj } from "../geometry/RadixSort.js"; class Node { /** @param {object} */ diff --git a/scripts/pathfinding/Token.js b/scripts/pathfinding/Token.js index 15c91b1..bbc24fb 100644 --- a/scripts/pathfinding/Token.js +++ b/scripts/pathfinding/Token.js @@ -1,14 +1,11 @@ /* globals -canvas, -Hooks */ "use strict"; - /* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */ -import { MODULE_ID } from "../const.js"; import { SCENE_GRAPH } from "./WallTracer.js"; import { Pathfinder } from "./pathfinding.js"; +import { log } from "../util.js"; // Track wall creation, update, and deletion, constructing WallTracerEdges as we go. // Use to update the pathfinding triangulation. @@ -38,8 +35,38 @@ function updateToken(document, changes, _options, _userId) { // Only update the edges if the coordinates have changed. if ( !(Object.hasOwn(changes, "x") || Object.hasOwn(changes, "y")) ) return; + log(`updateToken hook|token moved.`); + +// // Easiest approach is to trash the edges for the wall and re-create them. +// SCENE_GRAPH.removeToken(document.id); +// +// /* Debugging: None of the edges should have this token. +// if ( CONFIG[MODULE_ID].debug ) { +// const token = document.object; +// SCENE_GRAPH.edges.forEach((edge, key) => { +// if ( edge.objects.has(token) ) console.debug(`Edge ${key} has ${token.name} ${token.id} after deletion.`); +// }) +// } +// */ +// +// SCENE_GRAPH.addToken(document.object); +// +// // Need to re-do the triangulation because the change to the wall could have added edges if intersected. +// Pathfinder.dirty = true; +} + +/** + * Hook refresh token to update the scene graph and triangulation. + * Cannot use updateToken hook b/c the token position is not correctly updated by that point. + * @param {PlaceableObject} object The object instance being refreshed + */ +function refreshToken(object, flags) { + if ( !flags.refreshPosition || object.isPreview ) return; + + log(`refreshToken hook|original token moved.`); + // Easiest approach is to trash the edges for the wall and re-create them. - SCENE_GRAPH.removeToken(document.id); + SCENE_GRAPH.removeToken(object.id); /* Debugging: None of the edges should have this token. if ( CONFIG[MODULE_ID].debug ) { @@ -50,7 +77,7 @@ function updateToken(document, changes, _options, _userId) { } */ - SCENE_GRAPH.addToken(document.object); + SCENE_GRAPH.addToken(object); // Need to re-do the triangulation because the change to the wall could have added edges if intersected. Pathfinder.dirty = true; @@ -67,4 +94,4 @@ function deleteToken(document, _options, _userId) { Pathfinder.dirty = true; } -PATCHES.PATHFINDING_TOKENS.HOOKS = { createToken, updateToken, deleteToken }; +PATCHES.PATHFINDING_TOKENS.HOOKS = { createToken, updateToken, deleteToken, refreshToken }; diff --git a/scripts/pathfinding/Wall.js b/scripts/pathfinding/Wall.js index 8317ace..397c30b 100644 --- a/scripts/pathfinding/Wall.js +++ b/scripts/pathfinding/Wall.js @@ -1,15 +1,11 @@ /* globals -canvas, -Hooks */ "use strict"; /* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */ -import { MODULE_ID } from "../const.js"; import { SCENE_GRAPH } from "./WallTracer.js"; import { Pathfinder } from "./pathfinding.js"; -import { Settings } from "../settings.js"; // Track wall creation, update, and deletion, constructing WallTracerEdges as we go. // Use to update the pathfinding triangulation. @@ -17,31 +13,6 @@ import { Settings } from "../settings.js"; export const PATCHES = {}; PATCHES.PATHFINDING = {}; - -// When canvas is ready, the existing walls are not created, so must re-do here. -Hooks.on("canvasReady", async function() { - const t0 = performance.now(); - SCENE_GRAPH.clear(); - const walls = [ - ...canvas.walls.placeables, - ...canvas.walls.outerBounds, - ...canvas.walls.innerBounds - ]; - for ( const wall of walls ) SCENE_GRAPH.addWall(wall); - - // Must happen when the canvas is set so tokens (and walls) are available. - Settings.toggleTokenBlocksPathfinding(); - const t1 = performance.now(); - - // Use the scene graph to initialize Pathfinder triangulation. - Pathfinder.initialize(); - const t2 = performance.now(); - - console.debug(`${MODULE_ID}|Tracked ${walls.length} walls in ${t1 - t0} ms.`); - console.debug(`${MODULE_ID}|Initialized pathfinding in ${t2 - t1} ms.`); -}); - - /** * Hook createWall to update the scene graph and triangulation. * @param {Document} document The new Document instance which has been created diff --git a/scripts/pathfinding/WallTracer.js b/scripts/pathfinding/WallTracer.js index bc6f54b..755818d 100644 --- a/scripts/pathfinding/WallTracer.js +++ b/scripts/pathfinding/WallTracer.js @@ -2,7 +2,7 @@ CanvasQuadtree, CONFIG, CONST, -getProperty, +foundry, PIXI, Token, Wall @@ -211,19 +211,24 @@ export class WallTracerEdge extends GraphEdge { */ get tokens() { return this.objects.filter(o => o instanceof Token); } + /** + * Filter set for CanvasEdges + */ + get canvasEdges() { return this.objects.filter(o => o instanceof foundry.canvas.edges.Edge); } + /** * Construct an edge. * To be used instead of constructor in most cases. * @param {Point} edgeA First object edge endpoint * @param {Point} edgeB Other object edge endpoint - * @param {PlaceableObject} [object[]] Object(s) that contains this edge + * @param {PlaceableObject[]} [objects] Object(s) that contains this edge, if any * @param {number} [tA=0] Where the A endpoint of this edge falls on the object * @param {number} [tB=1] Where the B endpoint of this edge falls on the object * @returns {SegmentTracerEdge} */ static fromObjects(edgeA, edgeB, objects, tA = 0, tB = 1) { - tA = Math.clamped(tA, 0, 1); - tB = Math.clamped(tB, 0, 1); + tA = Math.clamp(tA, 0, 1); + tB = Math.clamp(tB, 0, 1); edgeA = PIXI.Point.fromObject(edgeA); edgeB = PIXI.Point.fromObject(edgeB); const eA = this.pointAtEdgeRatio(edgeA, edgeB, tA); @@ -232,7 +237,7 @@ export class WallTracerEdge extends GraphEdge { const B = new WallTracerVertex(eB.x, eB.y); const dist = PIXI.Point.distanceSquaredBetween(A.point, B.point); const edge = new this(A, B, dist); - if ( objects ) objects.forEach(obj => edge.objects.add(obj)); + objects.forEach(obj => edge.objects.add(obj)); return edge; } @@ -242,7 +247,15 @@ export class WallTracerEdge extends GraphEdge { * @param {Wall} wall Wall represented by this edge * @returns {WallTracerEdge} */ - static fromWall(wall) { return this.fromObject(wall.A, wall.B, [wall]); } + static fromWall(wall) { return this.fromObject(wall.edge.a, wall.edge.b, [wall]); } + + /** + * Construct an edge from a Canvas Edge + * Used for boundary walls. + * @param {Edge} edge Canvas edge + * @returns {WallTracerEdge} + */ + static fromCanvasEdge(edge) { return this.fromObject(edge.a, edge.b, [edge]); } /** * Construct an array of edges form the constrained token border. @@ -302,7 +315,7 @@ export class WallTracerEdge extends GraphEdge { * @returns {WallTracerEdge[]|null} Array of two edge tracer edges that share t endpoint. */ splitAtT(edgeT) { - edgeT = Math.clamped(edgeT, 0, 1); + edgeT = Math.clamp(edgeT, 0, 1); if ( edgeT.almostEqual(0) || edgeT.almostEqual(1) ) return null; // Construct two new edges, divided at the edgeT location. @@ -331,9 +344,9 @@ export class WallTracerEdge extends GraphEdge { */ edgeBlocks(origin, moveToken, tokenBlockType, elevation = 0) { return this.objects.some(obj => - (obj instanceof Wall) ? this.constructor.wallBlocks(obj, origin, elevation) - : (obj instanceof Token) ? this.constructor.tokenEdgeBlocks(obj, moveToken, tokenBlockType, elevation) - : false); + (obj instanceof Wall) ? this.constructor.wallBlocks(obj, origin, elevation) + : (obj instanceof Token) ? this.constructor.tokenEdgeBlocks(obj, moveToken, tokenBlockType, elevation) + : false); } /** @@ -348,7 +361,7 @@ export class WallTracerEdge extends GraphEdge { if ( !wall.document.move || wall.isOpen ) return false; // Ignore one-directional walls which are facing away from the center - const side = wall.orientPoint(origin); + const side = wall.edge.orientPoint(origin); /* Unneeded? const wdm = PointSourcePolygon.WALL_DIRECTION_MODES; @@ -379,7 +392,7 @@ export class WallTracerEdge extends GraphEdge { // Don't block dead tokens (HP <= 0). const { tokenHPAttribute, pathfindingIgnoreStatuses } = CONFIG[MODULE_ID]; - const tokenHP = Number(getProperty(token, tokenHPAttribute)); + const tokenHP = Number(foundry.utils.getProperty(token, tokenHPAttribute)); if ( Number.isFinite(tokenHP) && tokenHP <= 0 ) return false; // Don't block tokens with certain status. @@ -431,6 +444,12 @@ export class WallTracer extends Graph { */ objectEdges = new Map(); + /** + * Set of canvas edge ids represented in this graph. + * @type {Set} + */ + canvasEdgeIds = new Set(); + /** * Set of wall ids represented in this graph. * @type {Set} @@ -454,6 +473,7 @@ export class WallTracer extends Graph { this.objectEdges.clear(); this.wallIds.clear(); this.tokenIds.clear(); + this.canvasEdgeIds.clear(); super.clear(); } @@ -527,7 +547,8 @@ export class WallTracer extends Graph { // If no collisions, then a single edge can represent this edge object. const collisions = this.findEdgeCollisions(edgeA, edgeB); if ( !collisions.size ) { - const edge = WallTracerEdge.fromObjects(edgeA, edgeB, [object]); + const objects = object ? [object] : []; + const edge = WallTracerEdge.fromObjects(edgeA, edgeB, objects); this.addEdge(edge); return; } @@ -539,7 +560,7 @@ export class WallTracer extends Graph { * @param {SegmentIntersection[]} collisions * @param {PIXI.Point} edgeA First edge endpoint * @param {PIXI.Point} edgeB Other edge endpoint - * @param {Wall|Token} object + * @param {Wall|Token|Edge} object */ #processCollisions(collisions, edgeA, edgeB, object) { // Sort the keys so we can progress from A --> B along the edge. @@ -652,10 +673,23 @@ export class WallTracer extends Graph { if ( this.edges.has(wallId) ) return; // Construct a new wall edge set. - this.addObjectEdge(PIXI.Point.fromObject(wall.A), PIXI.Point.fromObject(wall.B), wall); + this.addObjectEdge(PIXI.Point.fromObject(wall.edge.a), PIXI.Point.fromObject(wall.edge.b), wall); this.wallIds.add(wallId); } + /** + * Split the canvas edge by edges already in this graph. + * @param {Edge} edge Canvas edge to convert to edge(s) + */ + addCanvasEdge(edge) { + const id = edge.id; + if ( this.edges.has(id) ) return; + + // Construct a new canvas edge set + this.addObjectEdge(PIXI.Point.fromObject(edge.a), PIXI.Point.fromObject(edge.b), edge); + this.canvasEdgeIds.add(id); + } + /** * Remove all associated edges with this edge set and object id. * @param {string} id Id of the edge object to remove @@ -685,7 +719,6 @@ export class WallTracer extends Graph { // This will remove unnecessary vertices and recombine edges. if ( _recurse ) { const remainingObjects = edgesArr.reduce((acc, curr) => acc = acc.union(curr.objects), new Set()); - if ( !remainingObjects.size ) return; remainingObjects.forEach(obj => obj instanceof Wall ? this.removeWall(obj.id, false) : this.removeToken(obj.id, false)); remainingObjects.forEach(obj => obj instanceof Wall @@ -713,6 +746,16 @@ export class WallTracer extends Graph { return this.removeObject(tokenId, _recurse); } + /** + * Remove all associated edges with this canvas edge. + * @param {string|Edge} edgeId + */ + removeCanvasEdge(edgeId, _recurse = true) { + if ( edgeId instanceof foundry.canvas.edges.Edge ) edgeId = edgeId.id; + this.canvasEdgeIds.delete(edgeId); + return this.removeObject(edgeId, _recurse); + } + /** * Locate collision points for any edges that collide with this edge. * Skips edges that simply share a single endpoint. @@ -767,6 +810,17 @@ canvas.tokens.placeables.filter(t => !SCENE_GRAPH.tokenIds.has(t.id)) // do we have all the walls? canvas.walls.placeables.filter(w => !SCENE_GRAPH.wallIds.has(w.id)) +// Every object edge id should be in one of the three sets and vice versa. +objectEdgeKeys = new Set(SCENE_GRAPH.objectEdges.keys()) +SCENE_GRAPH.canvasEdgeIds.difference(objectEdgeKeys).size +SCENE_GRAPH.tokenIds.difference(objectEdgeKeys).size +SCENE_GRAPH.wallIds.difference(objectEdgeKeys).size +objectEdgeKeys.equals(SCENE_GRAPH.canvasEdgeIds.union(SCENE_GRAPH.tokenIds).union(SCENE_GRAPH.wallIds)) + + + + + // Draw all edges SCENE_GRAPH.drawEdges() diff --git a/scripts/pathfinding/pathfinding.js b/scripts/pathfinding/pathfinding.js index f5c3cd3..1ea136d 100644 --- a/scripts/pathfinding/pathfinding.js +++ b/scripts/pathfinding/pathfinding.js @@ -26,6 +26,7 @@ api = game.modules.get("elevationruler").api Pathfinder = api.pathfinding.Pathfinder SCENE_GRAPH = api.pathfinding.SCENE_GRAPH BorderEdge = api.pathfinding.BorderEdge +BorderTriangle = api.pathfinding.BorderTriangle PriorityQueueArray = api.pathfinding.PriorityQueueArray; PriorityQueue = api.pathfinding.PriorityQueue; @@ -44,14 +45,47 @@ pq.enqueue({"C": 3}, 3); pq.enqueue({"B": 2}, 2); pq.data +// Test SCENE_GRAPH +SCENE_GRAPH.drawEdges() + +// Ensure the token edges get updated after moving +SCENE_GRAPH.drawEdges() + // Test pathfinding Pathfinder.initialize() - Draw.clearDrawings() - BorderEdge.moveToken = _token; + + Pathfinder.drawTriangles(); +for ( const tri of Pathfinder.borderTriangles ) { + for ( const edgeLabel of ["AB", "BC", "CA"] ) { + const edge = tri.edges[edgeLabel]; + if ( !(edge instanceof BorderEdge) ) { + console.log(`Tri ${tri.id}, edge ${edgeLabel} is not a BorderEdge.`); + continue; + } + if ( !(edge.ccwTriangle instanceof BorderTriangle) + || !(edge.cwTriangle instanceof BorderTriangle) ) console.log(`Tri ${tri.id}, edge ${edgeLabel} cw/ccw Triangle is not a BorderTriangle.`); + } +} + +edges = [] +for ( const edge of Pathfinder.triangleEdges ) { + if ( !edge.ccwTriangle ) { + console.log(`ccw Triangle is not a BorderTriangle.`, edge); + edges.push(edge); + } else if ( !edge.cwTriangle ) { + console.log(`cw Triangle is not a BorderTriangle.`, edge); + edges.push(edge); + } +} + + +pf = _token.elevationruler.pathfinder + + endPoint = _token.center diff --git a/scripts/segments.js b/scripts/segments.js index bc78a5a..3a4d2e7 100644 --- a/scripts/segments.js +++ b/scripts/segments.js @@ -229,6 +229,8 @@ export async function _animateSegment(token, segment, destination) { log(`Updating ${token.name} destination from ({${token.document.x},${token.document.y}) to (${destination.x},${destination.y}) for segment (${segment.ray.A.x},${segment.ray.A.y})|(${segment.ray.B.x},${segment.ray.B.y})`); // If the segment is teleporting and the segment destination is not a waypoint or ruler destination, skip. + // Doesn't work because _animateMovement stops the movement if the token does not make it to the + // next waypoint. // if ( segment.teleport // && !(segment.B.x === this.destination.x && segment.B.y === this.destination.y ) // && !this.waypoints.some(w => segment.B.x === w.x && segment.B.y === w.y) ) return; diff --git a/scripts/settings.js b/scripts/settings.js index 179713d..12e7558 100644 --- a/scripts/settings.js +++ b/scripts/settings.js @@ -125,7 +125,7 @@ export class Settings extends ModuleSettingsAbstract { [KEYS.PATHFINDING.TOKENS_BLOCK_CHOICES.HOSTILE]: localize(`${KEYS.PATHFINDING.TOKENS_BLOCK_CHOICES.HOSTILE}`), [KEYS.PATHFINDING.TOKENS_BLOCK_CHOICES.ALL]: localize(`${KEYS.PATHFINDING.TOKENS_BLOCK_CHOICES.ALL}`) }, - onChange: value => this.toggleTokenBlocksPathfinding(value) + onChange: value => this.setTokenBlocksPathfinding(value) }); register(KEYS.PATHFINDING.LIMIT_TOKEN_LOS, { @@ -302,25 +302,13 @@ export class Settings extends ModuleSettingsAbstract { name: game.i18n.localize(`${MODULE_ID}.keybindings.${KEYBINDINGS.TELEPORT}.name`), hint: game.i18n.localize(`${MODULE_ID}.keybindings.${KEYBINDINGS.TELEPORT}.hint`), editable: [ - { key: "ArrowRight" } + { key: "KeyF" } ], - onDown: async function(context) { - const ruler = canvas.controls.ruler; - if ( !ruler.active ) return; - canvas.mouseInteractionManager.cancel(context.event); // Unclear if this is doing anything. - const token = ruler.token; - await ruler.teleport(context); - if ( token ) { - token._preview?._onDragEnd(); // Unclear if this is doing anything. - token._onDragEnd(); // Unclear if this is doing anything. - } - canvas.mouseInteractionManager.reset(); // Unclear if this is doing anything. - }, precedence: CONST.KEYBINDING_PRECEDENCE.NORMAL }); } - static toggleTokenBlocksPathfinding(blockSetting) { + static setTokenBlocksPathfinding(blockSetting) { const C = this.KEYS.PATHFINDING.TOKENS_BLOCK_CHOICES; blockSetting ??= Settings.get(Settings.KEYS.PATHFINDING.TOKENS_BLOCK); if ( blockSetting === C.NO ) { // Disable