From 80881ea2d8db3059a2dd82863a65ba4e37d161ad Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sun, 21 Jan 2024 12:10:21 -0800 Subject: [PATCH 01/10] Add settings to API ,mostly for debuggin --- scripts/module.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/module.js b/scripts/module.js index 80a0e66..d9a6f72 100644 --- a/scripts/module.js +++ b/scripts/module.js @@ -47,7 +47,9 @@ Hooks.once("init", function() { SCENE_GRAPH }, - WallTracer, WallTracerEdge, WallTracerVertex + WallTracer, WallTracerEdge, WallTracerVertex, + + Settings }; }); From b2b7a1ac3b93db17d5ce24e2f80776d73540de53 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Mon, 22 Jan 2024 03:37:21 -0800 Subject: [PATCH 02/10] Fix for displaying pathfinding points from other users Pass segments through the socket. Only measure for the current user; use socket segments for others. Only highlight speed for current user, to avoid giving away private information. --- scripts/Ruler.js | 89 +++++++++++++++++++++++++++++++++++++++++++-- scripts/Token.js | 8 +++- scripts/patching.js | 2 + scripts/segments.js | 3 ++ scripts/settings.js | 20 +--------- 5 files changed, 98 insertions(+), 24 deletions(-) diff --git a/scripts/Ruler.js b/scripts/Ruler.js index b41ea50..9d148b1 100644 --- a/scripts/Ruler.js +++ b/scripts/Ruler.js @@ -6,7 +6,6 @@ duplicate, game, getProperty, PIXI, -Ruler, ui */ /* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */ @@ -84,6 +83,7 @@ UX goals: function clear(wrapper) { // User increments/decrements to the elevation for the current destination this.destination._userElevationIncrements = 0; + this._movementToken = undefined; return wrapper(); } @@ -95,6 +95,20 @@ function toJSON(wrapper) { // If debugging, log will not display on user's console // console.log("constructing ruler json!") const obj = wrapper(); + + // Segment information + // Simplify the ray. + obj._segments = this.segments.map(s => { + const newObj = { ...s }; + newObj.ray = { + A: s.ray.A, + B: s.ray.B + }; + + newObj.label = Boolean(s.label); + return newObj; + }); + obj._userElevationIncrements = this._userElevationIncrements; obj._unsnap = this._unsnap; obj._unsnappedOrigin = this._unsnappedOrigin; @@ -112,6 +126,14 @@ function update(wrapper, data) { this._userElevationIncrements = data._userElevationIncrements; this._unsnap = data._unsnap; this._unsnappedOrigin = data._unsnappedOrigin; + + // Reconstruct segments. + this.segments = data._segments.map(s => { + s.ray = new Ray3d(s.ray.A, s.ray.B); + return s; + }); + + wrapper(data); if ( triggerMeasure ) { @@ -439,6 +461,31 @@ function _onMouseUp(wrapped, event) { return wrapped(event); } +/** + * Add cached movement token. + * Mixed to avoid error if waypoints have no length. + */ +function _getMovementToken(wrapped) { + if ( !this.waypoints.length ) { + console.debug("Waypoints length 0"); + return undefined; + } + + if ( typeof this._movementToken !== "undefined" ) return this._movementToken; + this._movementToken = wrapped(); + if ( !this._movementToken ) this._movementToken = null; // So we can skip next time. + return this._movementToken; +} + +/** + * Override measure to break it into two parts. + * If this ruler is not the current user's ruler, only draw. + */ +function measure(destination, opts) { + if ( this.user === game.user ) this.measureSegments(destination, opts); + this.drawSegments(); +} + PATCHES.BASIC.WRAPS = { clear, @@ -464,9 +511,9 @@ PATCHES.BASIC.WRAPS = { _canMove }; -PATCHES.BASIC.MIXES = { _animateSegment }; +PATCHES.BASIC.MIXES = { _animateSegment, _getMovementToken }; -PATCHES.BASIC.OVERRIDES = { _computeDistance }; +PATCHES.BASIC.OVERRIDES = { _computeDistance, measure }; PATCHES.SPEED_HIGHLIGHTING.WRAPS = { _highlightMeasurementSegment }; @@ -572,6 +619,38 @@ function measureDistance(start, end, gridless = false) { return d; } +/** + * Break apart measure so we can avoid recalculating segments. + * See https://github.com/foundryvtt/foundryvtt/issues/10361 + */ +function drawSegments() { + // Reconstruct labels if necessary. + let labelIndex = 0; + for ( const s of this.segments ) { + if ( !s.label ) continue; // Not every segment has a label. + s.label = this.labels.children[labelIndex++]; + } + + // Following is from Ruler.prototype.measure + + // Draw the ruler graphic + this.ruler.clear(); + this._drawMeasuredPath(); + + // Draw grid highlight + this.highlightLayer.clear(); + for ( const segment of this.segments ) this._highlightMeasurementSegment(segment); +} + +function measureSegments(destination, {gridSpaces=true, force=false}={}) { + // Compute the measurement destination, segments, and distance + const d = this._getMeasurementDestination(destination); + if ( ( d.x === this.destination.x ) && ( d.y === this.destination.y ) && !force ) return; + this.destination = d; + this.segments = this._getMeasurementSegments(); + this._computeDistance(gridSpaces); +} + PATCHES.BASIC.METHODS = { incrementElevation, decrementElevation, @@ -581,7 +660,9 @@ PATCHES.BASIC.METHODS = { terrainElevationAtPoint, terrainElevationAtDestination, - _computeTokenSpeed + _computeTokenSpeed, + drawSegments, + measureSegments }; PATCHES.BASIC.STATIC_METHODS = { diff --git a/scripts/Token.js b/scripts/Token.js index 61484d6..7c025fd 100644 --- a/scripts/Token.js +++ b/scripts/Token.js @@ -5,6 +5,7 @@ canvas import { elevationAtWaypoint } from "./segments.js"; import { ConstrainedTokenBorder } from "./ConstrainedTokenBorder.js"; +import { Settings } from "./settings.js"; // Patches for the Token class export const PATCHES = {}; @@ -18,7 +19,8 @@ PATCHES.ConstrainedTokenBorder = {}; function _onDragLeftStart(wrapped, event) { wrapped(event); - // Start a Ruler measurement. + // If Token Ruler, start a ruler measurement. + if ( !Settings.get(Settings.KEYS.TOKEN_RULER.ENABLED) ) return; canvas.controls.ruler._onDragStart(event); } @@ -31,6 +33,7 @@ function _onDragLeftCancel(wrapped, event) { // Cancel a Ruler measurement. // If moving, handled by the drag left drop. + if ( !Settings.get(Settings.KEYS.TOKEN_RULER.ENABLED) ) return; const ruler = canvas.controls.ruler; if ( ruler._state !== Ruler.STATES.MOVING ) canvas.controls.ruler._onMouseUp(event); } @@ -43,6 +46,7 @@ function _onDragLeftMove(wrapped, event) { wrapped(event); // Continue a Ruler measurement. + if ( !Settings.get(Settings.KEYS.TOKEN_RULER.ENABLED) ) return; const ruler = canvas.controls.ruler; if ( ruler._state > 0 ) ruler._onMouseMove(event); } @@ -54,7 +58,7 @@ function _onDragLeftMove(wrapped, event) { async function _onDragLeftDrop(wrapped, event) { // End the ruler measurement const ruler = canvas.controls.ruler; - if ( !ruler.active ) return wrapped(event); + if ( !ruler.active || !Settings.get(Settings.KEYS.TOKEN_RULER.ENABLED) ) return wrapped(event); const destination = event.interactionData.destination; // Ensure the cursor destination is within bounds diff --git a/scripts/patching.js b/scripts/patching.js index fb663fe..616024f 100644 --- a/scripts/patching.js +++ b/scripts/patching.js @@ -38,5 +38,7 @@ export function initializePatching() { PATCHER.registerGroup("BASIC"); PATCHER.registerGroup("ConstrainedTokenBorder"); PATCHER.registerGroup("PATHFINDING"); + PATCHER.registerGroup("TOKEN_RULER"); + PATCHER.registerGroup("SPEED_HIGHLIGHTING"); } diff --git a/scripts/segments.js b/scripts/segments.js index b64409b..1828ffd 100644 --- a/scripts/segments.js +++ b/scripts/segments.js @@ -181,6 +181,9 @@ export function hasSegmentCollision(token, segments) { * Wrap Ruler.prototype._highlightMeasurementSegment */ export function _highlightMeasurementSegment(wrapped, segment) { + if ( !(this.user === game.user + && Settings.get(Settings.KEYS.TOKEN_RULER.SPEED_HIGHLIGHTING)) ) return wrapped(segment); + const token = this._getMovementToken(); if ( !token ) return wrapped(segment); diff --git a/scripts/settings.js b/scripts/settings.js index 096ff92..081505d 100644 --- a/scripts/settings.js +++ b/scripts/settings.js @@ -7,7 +7,6 @@ canvas import { MODULE_ID, MODULES_ACTIVE, SPEED } from "./const.js"; import { ModuleSettingsAbstract } from "./ModuleSettingsAbstract.js"; -import { PATCHER } from "./patching.js"; const SETTINGS = { CONTROLS: { @@ -147,8 +146,7 @@ export class Settings extends ModuleSettingsAbstract { config: true, default: false, type: Boolean, - requiresReload: false, - onChange: value => this.toggleTokenRuler(value) + requiresReload: false }); register(KEYS.TOKEN_RULER.SPEED_HIGHLIGHTING, { @@ -158,8 +156,7 @@ export class Settings extends ModuleSettingsAbstract { config: true, default: false, type: Boolean, - requiresReload: false, - onChange: value => this.toggleSpeedHighlighting(value) + requiresReload: false }); register(KEYS.TOKEN_RULER.SPEED_PROPERTY, { @@ -187,8 +184,6 @@ export class Settings extends ModuleSettingsAbstract { }); // Initialize the Token Ruler. - if ( this.get(KEYS.TOKEN_RULER.ENABLED) ) this.toggleTokenRuler(true); - if ( this.get(KEYS.TOKEN_RULER.SPEED_HIGHLIGHTING) ) this.toggleSpeedHighlighting(true); this.setSpeedProperty(this.get(KEYS.TOKEN_RULER.SPEED_PROPERTY)); } @@ -234,18 +229,7 @@ export class Settings extends ModuleSettingsAbstract { }); } - static toggleTokenRuler(value) { - if ( value ) PATCHER.registerGroup("TOKEN_RULER"); - else PATCHER.deregisterGroup("TOKEN_RULER"); - } - - static toggleSpeedHighlighting(value) { - if ( value ) PATCHER.registerGroup("SPEED_HIGHLIGHTING"); - else PATCHER.deregisterGroup("SPEED_HIGHLIGHTING"); - } - static setSpeedProperty(value) { SPEED.ATTRIBUTE = value; } - } /** From bc27251114360ceb385088a0cc0dbc5e2ce1882c Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Mon, 22 Jan 2024 08:13:07 -0800 Subject: [PATCH 03/10] Use individual wrappers instead of overriding measure Measure is too important to just override. Instead, use mixed wrap to return early from the measure methods. Remove debug message for last segment --- scripts/Ruler.js | 54 +++++---------------------------------------- scripts/segments.js | 36 +++++++++++++++++++----------- 2 files changed, 29 insertions(+), 61 deletions(-) diff --git a/scripts/Ruler.js b/scripts/Ruler.js index 9d148b1..03b7e8f 100644 --- a/scripts/Ruler.js +++ b/scripts/Ruler.js @@ -223,6 +223,9 @@ function _canMove(wrapper, token) { * @param {boolean} gridSpaces Base distance on the number of grid spaces moved? */ function _computeDistance(gridSpaces) { + // If not this ruler's user, use the segments already calculated and passed via socket. + if ( this.user !== game.user ) return; + const gridless = !gridSpaces; const token = this._getMovementToken(); const { measureDistance, modifiedMoveDistance } = this.constructor; @@ -477,16 +480,6 @@ function _getMovementToken(wrapped) { return this._movementToken; } -/** - * Override measure to break it into two parts. - * If this ruler is not the current user's ruler, only draw. - */ -function measure(destination, opts) { - if ( this.user === game.user ) this.measureSegments(destination, opts); - this.drawSegments(); -} - - PATCHES.BASIC.WRAPS = { clear, toJSON, @@ -496,7 +489,6 @@ PATCHES.BASIC.WRAPS = { _getMeasurementDestination, // Wraps related to segments - _getMeasurementSegments, _getSegmentLabel, // Move token methods @@ -511,9 +503,9 @@ PATCHES.BASIC.WRAPS = { _canMove }; -PATCHES.BASIC.MIXES = { _animateSegment, _getMovementToken }; +PATCHES.BASIC.MIXES = { _animateSegment, _getMovementToken, _getMeasurementSegments }; -PATCHES.BASIC.OVERRIDES = { _computeDistance, measure }; +PATCHES.BASIC.OVERRIDES = { _computeDistance }; PATCHES.SPEED_HIGHLIGHTING.WRAPS = { _highlightMeasurementSegment }; @@ -619,38 +611,6 @@ function measureDistance(start, end, gridless = false) { return d; } -/** - * Break apart measure so we can avoid recalculating segments. - * See https://github.com/foundryvtt/foundryvtt/issues/10361 - */ -function drawSegments() { - // Reconstruct labels if necessary. - let labelIndex = 0; - for ( const s of this.segments ) { - if ( !s.label ) continue; // Not every segment has a label. - s.label = this.labels.children[labelIndex++]; - } - - // Following is from Ruler.prototype.measure - - // Draw the ruler graphic - this.ruler.clear(); - this._drawMeasuredPath(); - - // Draw grid highlight - this.highlightLayer.clear(); - for ( const segment of this.segments ) this._highlightMeasurementSegment(segment); -} - -function measureSegments(destination, {gridSpaces=true, force=false}={}) { - // Compute the measurement destination, segments, and distance - const d = this._getMeasurementDestination(destination); - if ( ( d.x === this.destination.x ) && ( d.y === this.destination.y ) && !force ) return; - this.destination = d; - this.segments = this._getMeasurementSegments(); - this._computeDistance(gridSpaces); -} - PATCHES.BASIC.METHODS = { incrementElevation, decrementElevation, @@ -660,9 +620,7 @@ PATCHES.BASIC.METHODS = { terrainElevationAtPoint, terrainElevationAtDestination, - _computeTokenSpeed, - drawSegments, - measureSegments + _computeTokenSpeed }; PATCHES.BASIC.STATIC_METHODS = { diff --git a/scripts/segments.js b/scripts/segments.js index 1828ffd..d2f7dd1 100644 --- a/scripts/segments.js +++ b/scripts/segments.js @@ -25,22 +25,32 @@ export function elevationAtWaypoint(waypoint) { } /** - * Wrap Ruler.prototype._getMeasurementSegments - * Add elevation information to the segments + * Mixed wrap of Ruler.prototype._getMeasurementSegments + * Add elevation information to the segments. + * Add pathfinding segments. */ export function _getMeasurementSegments(wrapped) { + // If not the user's ruler, segments calculated by original user and copied via socket. + if ( this.user !== game.user ) { + // Reconstruct labels if necessary. + let labelIndex = 0; + for ( const s of this.segments ) { + if ( !s.label ) continue; // Not every segment has a label. + s.label = this.labels.children[labelIndex++]; + } + return this.segments; + } + + // Elevate the segments const segments = elevateSegments(this, wrapped()); const token = this._getMovementToken(); - if ( !(token && Settings.get(Settings.KEYS.CONTROLS.PATHFINDING)) ) return segments; - // Test for a collision; if none, no pathfinding. + // No segments are possible if dragging back to the origin point. const lastSegment = segments.at(-1); - if ( !lastSegment ) { - // console.debug("No last segment found", [...segments]); - return segments; - } + if ( !lastSegment ) return segments; + // Test for a collision; if none, no pathfinding. const { A, B } = lastSegment.ray; if ( !token.checkCollision(B, { origin: A, type: "move", mode: "any" }) ) return segments; @@ -51,19 +61,19 @@ export function _getMeasurementSegments(wrapped) { const path = pf.runPath(A, B); let pathPoints = Pathfinder.getPathPoints(path); const t1 = performance.now(); - // console.debug(`Found ${pathPoints.length} path points between ${A.x},${A.y} -> ${B.x},${B.y} in ${t1 - t0} ms.`); + console.debug(`Found ${pathPoints.length} path points between ${A.x},${A.y} -> ${B.x},${B.y} in ${t1 - t0} ms.`); const t4 = performance.now(); pathPoints = Pathfinder.cleanPath(pathPoints); const t5 = performance.now(); if ( !pathPoints ) { - // console.debug("No path points after cleaning"); + console.debug("No path points after cleaning"); return segments; } - // console.debug(`Cleaned to ${pathPoints?.length} path points between ${A.x},${A.y} -> ${B.x},${B.y} in ${t5 - t4} ms.`); + console.debug(`Cleaned to ${pathPoints?.length} path points between ${A.x},${A.y} -> ${B.x},${B.y} in ${t5 - t4} ms.`); if ( pathPoints.length < 2 ) { - // console.debug(`Only ${pathPoints.length} path points found.`, [...pathPoints]); + console.debug(`Only ${pathPoints.length} path points found.`, [...pathPoints]); return segments; } @@ -78,7 +88,7 @@ export function _getMeasurementSegments(wrapped) { // For each segment, replace with path sub-segment if pathfinding was used. const newSegments = constructPathfindingSegments(segments, segmentMap); const t3 = performance.now(); - // console.debug(`${newSegments.length} segments processed in ${t3-t2} ms.`); + console.debug(`${newSegments.length} segments processed in ${t3-t2} ms.`); return newSegments; } From bd9229168b86f7ed1d741455f6643c9916ad08de Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Mon, 22 Jan 2024 08:18:19 -0800 Subject: [PATCH 04/10] Catch when segments are not yet defined for ruler --- scripts/Ruler.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/Ruler.js b/scripts/Ruler.js index 03b7e8f..73047d3 100644 --- a/scripts/Ruler.js +++ b/scripts/Ruler.js @@ -98,7 +98,7 @@ function toJSON(wrapper) { // Segment information // Simplify the ray. - obj._segments = this.segments.map(s => { + if ( this.segments ) obj._segments = this.segments.map(s => { const newObj = { ...s }; newObj.ray = { A: s.ray.A, @@ -128,12 +128,11 @@ function update(wrapper, data) { this._unsnappedOrigin = data._unsnappedOrigin; // Reconstruct segments. - this.segments = data._segments.map(s => { + if ( data._segments ) this.segments = data._segments.map(s => { s.ray = new Ray3d(s.ray.A, s.ray.B); return s; }); - wrapper(data); if ( triggerMeasure ) { From 27f34efd855b97204bf985638181e6e439e689d1 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Mon, 22 Jan 2024 10:24:19 -0800 Subject: [PATCH 05/10] Fixes for replacing segment with pathfinding points Make sure to store previous waypoint pathfinding points --- scripts/Ruler.js | 33 ++++++++++++++-------- scripts/segments.js | 67 +++++++++++++++++++++++++++------------------ 2 files changed, 62 insertions(+), 38 deletions(-) diff --git a/scripts/Ruler.js b/scripts/Ruler.js index 73047d3..1ed6a17 100644 --- a/scripts/Ruler.js +++ b/scripts/Ruler.js @@ -15,7 +15,7 @@ export const PATCHES = {}; PATCHES.BASIC = {}; PATCHES.SPEED_HIGHLIGHTING = {}; -import { SPEED } from "./const.js"; +import { SPEED, MODULE_ID } from "./const.js"; import { Settings } from "./settings.js"; import { Ray3d } from "./geometry/3d/Ray3d.js"; import { @@ -90,28 +90,32 @@ function clear(wrapper) { /** * Wrap Ruler.prototype.toJSON * Store the current userElevationIncrements for the destination. + * Store segment information, possibly including pathfinding. */ function toJSON(wrapper) { // If debugging, log will not display on user's console // console.log("constructing ruler json!") const obj = wrapper(); + const myObj = obj[MODULE_ID] = {}; + // Segment information // Simplify the ray. - if ( this.segments ) obj._segments = this.segments.map(s => { + if ( this.segments ) myObj._segments = this.segments.map(s => { const newObj = { ...s }; newObj.ray = { A: s.ray.A, B: s.ray.B }; - newObj.label = Boolean(s.label); return newObj; }); - obj._userElevationIncrements = this._userElevationIncrements; - obj._unsnap = this._unsnap; - obj._unsnappedOrigin = this._unsnappedOrigin; + myObj._userElevationIncrements = this._userElevationIncrements; + myObj._unsnap = this._unsnap; + myObj._unsnappedOrigin = this._unsnappedOrigin; + myObj._totalDistance = this.totalDistance; + myObj._totalMoveDistance = this.totalMoveDistance; return obj; } @@ -121,18 +125,25 @@ function toJSON(wrapper) { * Retrieve the current snap status. */ function update(wrapper, data) { + const myData = data[MODULE_ID]; + if ( !myData ) return wrapper(data); // Just in case. + // Fix for displaying user elevation increments as they happen. - const triggerMeasure = this._userElevationIncrements !== data._userElevationIncrements; - this._userElevationIncrements = data._userElevationIncrements; - this._unsnap = data._unsnap; - this._unsnappedOrigin = data._unsnappedOrigin; + const triggerMeasure = this._userElevationIncrements !== myData._userElevationIncrements; + this._userElevationIncrements = myData._userElevationIncrements; + this._unsnap = myData._unsnap; + this._unsnappedOrigin = myData._unsnappedOrigin; // Reconstruct segments. - if ( data._segments ) this.segments = data._segments.map(s => { + if ( myData._segments ) this.segments = myData._segments.map(s => { s.ray = new Ray3d(s.ray.A, s.ray.B); return s; }); + // Add the calculated distance totals. + this.totalDistance = myData._totalDistance; + this.totalMoveDistance = myData._totalMoveDistance; + wrapper(data); if ( triggerMeasure ) { diff --git a/scripts/segments.js b/scripts/segments.js index d2f7dd1..42bfc7c 100644 --- a/scripts/segments.js +++ b/scripts/segments.js @@ -44,15 +44,42 @@ export function _getMeasurementSegments(wrapped) { // Elevate the segments const segments = elevateSegments(this, wrapped()); const token = this._getMovementToken(); - if ( !(token && Settings.get(Settings.KEYS.CONTROLS.PATHFINDING)) ) return segments; - // No segments are possible if dragging back to the origin point. + // If no movement token, then no pathfinding. + if ( !token ) return segments; + + // If no segments present, clear the path map and return. + // No segments are present if dragging back to the origin point. + const segmentMap = this._pathfindingSegmentMap ??= new Map(); + if ( !segments || !segments.length ) { + segmentMap.clear(); + return segments; + } + + // If currently pathfinding, set path for the last segment, overriding any prior path. const lastSegment = segments.at(-1); - if ( !lastSegment ) return segments; + const pathPoints = Settings.get(Settings.KEYS.CONTROLS.PATHFINDING) + ? calculatePathPointsForSegment(lastSegment, token) + : []; + if ( pathPoints.length > 2 ) segmentMap.set(lastSegment.ray.A.to2d().key, pathPoints); + else segmentMap.delete(lastSegment.ray.A.to2d().key); + + // For each segment, replace with path sub-segment if pathfinding was used for that segment. + const t2 = performance.now(); + const newSegments = constructPathfindingSegments(segments, segmentMap); + const t3 = performance.now(); + console.debug(`${newSegments.length} segments processed in ${t3-t2} ms.`); + return newSegments; +} - // Test for a collision; if none, no pathfinding. - const { A, B } = lastSegment.ray; - if ( !token.checkCollision(B, { origin: A, type: "move", mode: "any" }) ) return segments; +/** + * Calculate a path to get from points A to B on the segment. + * @param {RulerMeasurementSegment} segment + * @returns {PIXI.Point[]} + */ +function calculatePathPointsForSegment(segment, token) { + const { A, B } = segment.ray; + if ( !token.checkCollision(B, { origin: A, type: "move", mode: "any" }) ) return []; // Find path between last waypoint and destination. const t0 = performance.now(); @@ -63,33 +90,19 @@ export function _getMeasurementSegments(wrapped) { const t1 = performance.now(); console.debug(`Found ${pathPoints.length} path points between ${A.x},${A.y} -> ${B.x},${B.y} in ${t1 - t0} ms.`); - const t4 = performance.now(); + // Clean the path + const t2 = performance.now(); pathPoints = Pathfinder.cleanPath(pathPoints); - const t5 = performance.now(); - if ( !pathPoints ) { - console.debug("No path points after cleaning"); - return segments; - } + const t3 = performance.now(); + console.debug(`Cleaned to ${pathPoints?.length} path points between ${A.x},${A.y} -> ${B.x},${B.y} in ${t3 - t2} ms.`); - console.debug(`Cleaned to ${pathPoints?.length} path points between ${A.x},${A.y} -> ${B.x},${B.y} in ${t5 - t4} ms.`); + // If less than 3 points after cleaning, just use the original segment. if ( pathPoints.length < 2 ) { console.debug(`Only ${pathPoints.length} path points found.`, [...pathPoints]); - return segments; + return []; } - - - // Store points in case a waypoint is added. - // Overwrite the last calculated path from this waypoint. - const t2 = performance.now(); - const segmentMap = this._pathfindingSegmentMap ??= new Map(); - segmentMap.set(A.to2d().key, pathPoints); - - // For each segment, replace with path sub-segment if pathfinding was used. - const newSegments = constructPathfindingSegments(segments, segmentMap); - const t3 = performance.now(); - console.debug(`${newSegments.length} segments processed in ${t3-t2} ms.`); - return newSegments; + return pathPoints; } /** From b2432824aff4ce37231142dca58062400d5e8c89 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Mon, 22 Jan 2024 11:47:09 -0800 Subject: [PATCH 06/10] Fix summing segment distance after waypoints Keep separate count of segment distances after each waypoint. --- Changelog.md | 6 ++++++ scripts/Ruler.js | 31 ++++++++++++++++++++++++------- scripts/segments.js | 6 +++++- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/Changelog.md b/Changelog.md index 2f9c597..b432a8d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,9 @@ +# 0.8.1 + +Fix toggling on/off the pathfinding between waypoints. +Various fixes to display of ruler for other users. + + # 0.8.0 Added pathfinding toggle. Pathfinding works on gridded (both hex and square) and gridless maps. Works when dragging tokens, if Token Ruler is enabled, or when using the Ruler control and you start at a token. diff --git a/scripts/Ruler.js b/scripts/Ruler.js index 1ed6a17..b683891 100644 --- a/scripts/Ruler.js +++ b/scripts/Ruler.js @@ -114,8 +114,8 @@ function toJSON(wrapper) { myObj._userElevationIncrements = this._userElevationIncrements; myObj._unsnap = this._unsnap; myObj._unsnappedOrigin = this._unsnappedOrigin; - myObj._totalDistance = this.totalDistance; - myObj._totalMoveDistance = this.totalMoveDistance; + myObj.totalDistance = this.totalDistance; + myObj.totalMoveDistance = this.totalMoveDistance; return obj; } @@ -141,8 +141,8 @@ function update(wrapper, data) { }); // Add the calculated distance totals. - this.totalDistance = myData._totalDistance; - this.totalMoveDistance = myData._totalMoveDistance; + this.totalDistance = myData.totalDistance; + this.totalMoveDistance = myData.totalMoveDistance; wrapper(data); @@ -265,7 +265,20 @@ function _computeDistance(gridSpaces) { console.error("Segment is undefined."); } - + // Compute the waypoint distances for labeling. (Distance to immediately previous waypoint.) + const waypointKeys = new Set(this.waypoints.map(w => w.key)); + let waypointDistance = 0; + let waypointMoveDistance = 0; + for ( const segment of this.segments ) { + if ( waypointKeys.has(segment.ray.A.to2d().key) ) { + waypointDistance = 0; + waypointMoveDistance = 0; + } + waypointDistance += segment.distance; + waypointMoveDistance += segment.moveDistance; + segment.waypointDistance = waypointDistance; + segment.waypointMoveDistance = waypointMoveDistance; + } } function _computeTokenSpeed(token, tokenSpeed, gridless = false) { @@ -410,8 +423,12 @@ function splitSegment(segment, splitMoveDistance, token, gridless) { if ( breakPoint.almostEqual(A) ) return []; // Split the segment into two at the break point. - const segment0 = { ray: new Ray3d(A, breakPoint) }; - const segment1 = { ray: new Ray3d(breakPoint, B) }; + const segment0 = {...segment}; + const segment1 = {...segment}; + segment0.ray = new Ray3d(A, breakPoint); + segment1.ray = new Ray3d(breakPoint, B); + segment0.distance = rulerClass.measureDistance(segment0.ray.A, segment0.ray.B, gridless); + segment1.distance = rulerClass.measureDistance(segment1.ray.A, segment1.ray.B, gridless); segment0.moveDistance = rulerClass.modifiedMoveDistance(segment0, token); segment1.moveDistance = rulerClass.modifiedMoveDistance(segment1, token); return [segment0, segment1]; diff --git a/scripts/segments.js b/scripts/segments.js index 42bfc7c..d01a8e6 100644 --- a/scripts/segments.js +++ b/scripts/segments.js @@ -60,7 +60,7 @@ export function _getMeasurementSegments(wrapped) { const lastSegment = segments.at(-1); const pathPoints = Settings.get(Settings.KEYS.CONTROLS.PATHFINDING) ? calculatePathPointsForSegment(lastSegment, token) - : []; + : []; if ( pathPoints.length > 2 ) segmentMap.set(lastSegment.ray.A.to2d().key, pathPoints); else segmentMap.delete(lastSegment.ray.A.to2d().key); @@ -150,7 +150,11 @@ function constructPathfindingSegments(segments, segmentMap) { * Add elevation information to the label */ export function _getSegmentLabel(wrapped, segment, totalDistance) { + // Force distance to be between waypoints instead of (possibly pathfinding) segments. + const origSegmentDistance = segment.distance; + segment.distance = segment.waypointDistance; const orig_label = wrapped(segment, totalDistance); + segment.distance = origSegmentDistance; let elevation_label = segmentElevationLabel(segment); const level_name = levelNameAtElevation(CONFIG.GeometryLib.utils.pixelsToGridUnits(segment.ray.B.z)); if ( level_name ) elevation_label += `\n${level_name}`; From db2f3271d040bcd0f5634081caf26205972b94e0 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Mon, 22 Jan 2024 12:35:09 -0800 Subject: [PATCH 07/10] Temporary fix for issue #34 Don't test token bounds if bounds is undefined. --- scripts/terrain_elevation.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/terrain_elevation.js b/scripts/terrain_elevation.js index 835478c..f4baec4 100644 --- a/scripts/terrain_elevation.js +++ b/scripts/terrain_elevation.js @@ -93,7 +93,8 @@ function elevationAtLocation(location, measuringToken, startingElevation = Numbe // Prioritize the highest token at the location const max_token_elevation = retrieveVisibleTokens().reduce((e, t) => { // Is the point within the token control area? - if ( !t.bounds.contains(location.x, location.y) ) return e; + // Issue #34: bounds can apparently be undefined in some systems? + if ( !t.bounds || !t.bounds.contains(location.x, location.y) ) return e; return Math.max(tokenElevation(t), e); }, Number.NEGATIVE_INFINITY); if ( isFinite(max_token_elevation) && max_token_elevation >= ignoreBelow ) return max_token_elevation; From 33c361127ca59d5419544e385370cde32ed146c1 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Mon, 22 Jan 2024 13:06:35 -0800 Subject: [PATCH 08/10] Comment out console.debug --- scripts/Ruler.js | 2 +- scripts/segments.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/Ruler.js b/scripts/Ruler.js index b683891..cc75989 100644 --- a/scripts/Ruler.js +++ b/scripts/Ruler.js @@ -497,7 +497,7 @@ function _onMouseUp(wrapped, event) { */ function _getMovementToken(wrapped) { if ( !this.waypoints.length ) { - console.debug("Waypoints length 0"); + // console.debug("Waypoints length 0"); return undefined; } diff --git a/scripts/segments.js b/scripts/segments.js index d01a8e6..f3f81d3 100644 --- a/scripts/segments.js +++ b/scripts/segments.js @@ -68,7 +68,7 @@ export function _getMeasurementSegments(wrapped) { const t2 = performance.now(); const newSegments = constructPathfindingSegments(segments, segmentMap); const t3 = performance.now(); - console.debug(`${newSegments.length} segments processed in ${t3-t2} ms.`); + //console.debug(`${newSegments.length} segments processed in ${t3-t2} ms.`); return newSegments; } @@ -88,17 +88,17 @@ function calculatePathPointsForSegment(segment, token) { const path = pf.runPath(A, B); let pathPoints = Pathfinder.getPathPoints(path); const t1 = performance.now(); - console.debug(`Found ${pathPoints.length} path points between ${A.x},${A.y} -> ${B.x},${B.y} in ${t1 - t0} ms.`); + //console.debug(`Found ${pathPoints.length} path points between ${A.x},${A.y} -> ${B.x},${B.y} in ${t1 - t0} ms.`); // Clean the path const t2 = performance.now(); pathPoints = Pathfinder.cleanPath(pathPoints); const t3 = performance.now(); - console.debug(`Cleaned to ${pathPoints?.length} path points between ${A.x},${A.y} -> ${B.x},${B.y} in ${t3 - t2} ms.`); + //console.debug(`Cleaned to ${pathPoints?.length} path points between ${A.x},${A.y} -> ${B.x},${B.y} in ${t3 - t2} ms.`); // If less than 3 points after cleaning, just use the original segment. if ( pathPoints.length < 2 ) { - console.debug(`Only ${pathPoints.length} path points found.`, [...pathPoints]); + //console.debug(`Only ${pathPoints.length} path points found.`, [...pathPoints]); return []; } From 4ea68f271c76d860049818f293c269ebfe024aea Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Mon, 22 Jan 2024 13:44:03 -0800 Subject: [PATCH 09/10] Use log with CONFIG toggle for debug logging --- scripts/Ruler.js | 5 +++-- scripts/module.js | 5 +++++ scripts/pathfinding/Wall.js | 3 --- scripts/segments.js | 10 +++++----- scripts/settings.js | 5 +++-- scripts/util.js | 4 +--- 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/scripts/Ruler.js b/scripts/Ruler.js index cc75989..7d18ebe 100644 --- a/scripts/Ruler.js +++ b/scripts/Ruler.js @@ -37,7 +37,8 @@ import { tokenIsSnapped, iterateGridUnderLine, squareGridShape, - hexGridShape } from "./util.js"; + hexGridShape, + log } from "./util.js"; /** * Modified Ruler @@ -497,7 +498,7 @@ function _onMouseUp(wrapped, event) { */ function _getMovementToken(wrapped) { if ( !this.waypoints.length ) { - // console.debug("Waypoints length 0"); + log("Waypoints length 0"); return undefined; } diff --git a/scripts/module.js b/scripts/module.js index d9a6f72..87ca13d 100644 --- a/scripts/module.js +++ b/scripts/module.js @@ -28,6 +28,11 @@ Hooks.once("init", function() { // Cannot access localization until init. PREFER_TOKEN_CONTROL.title = game.i18n.localize(PREFER_TOKEN_CONTROL.title); registerGeometry(); + + // Configuration + CONFIG[MODULE_ID] = { debug: false }; + + game.modules.get(MODULE_ID).api = { iterateGridUnderLine, PATCHER, diff --git a/scripts/pathfinding/Wall.js b/scripts/pathfinding/Wall.js index bece227..fdd2421 100644 --- a/scripts/pathfinding/Wall.js +++ b/scripts/pathfinding/Wall.js @@ -19,9 +19,6 @@ PATCHES.PATHFINDING = {}; // When canvas is ready, the existing walls are not created, so must re-do here. Hooks.on("canvasReady", async function() { - // console.debug(`outerBounds: ${canvas.walls.outerBounds.length}`); - // console.debug(`innerBounds: ${canvas.walls.innerBounds.length}`); - const t0 = performance.now(); SCENE_GRAPH.clear(); const walls = [ diff --git a/scripts/segments.js b/scripts/segments.js index f3f81d3..2ab1cfd 100644 --- a/scripts/segments.js +++ b/scripts/segments.js @@ -11,7 +11,7 @@ import { SPEED, MODULES_ACTIVE, MODULE_ID } from "./const.js"; import { Settings } from "./settings.js"; import { Ray3d } from "./geometry/3d/Ray3d.js"; import { Point3d } from "./geometry/3d/Point3d.js"; -import { perpendicularPoints } from "./util.js"; +import { perpendicularPoints, log } from "./util.js"; import { Pathfinder } from "./pathfinding/pathfinding.js"; /** @@ -68,7 +68,7 @@ export function _getMeasurementSegments(wrapped) { const t2 = performance.now(); const newSegments = constructPathfindingSegments(segments, segmentMap); const t3 = performance.now(); - //console.debug(`${newSegments.length} segments processed in ${t3-t2} ms.`); + log(`${newSegments.length} segments processed in ${t3-t2} ms.`); return newSegments; } @@ -88,17 +88,17 @@ function calculatePathPointsForSegment(segment, token) { const path = pf.runPath(A, B); let pathPoints = Pathfinder.getPathPoints(path); const t1 = performance.now(); - //console.debug(`Found ${pathPoints.length} path points between ${A.x},${A.y} -> ${B.x},${B.y} in ${t1 - t0} ms.`); + log(`Found ${pathPoints.length} path points between ${A.x},${A.y} -> ${B.x},${B.y} in ${t1 - t0} ms.`); // Clean the path const t2 = performance.now(); pathPoints = Pathfinder.cleanPath(pathPoints); const t3 = performance.now(); - //console.debug(`Cleaned to ${pathPoints?.length} path points between ${A.x},${A.y} -> ${B.x},${B.y} in ${t3 - t2} ms.`); + log(`Cleaned to ${pathPoints?.length} path points between ${A.x},${A.y} -> ${B.x},${B.y} in ${t3 - t2} ms.`); // If less than 3 points after cleaning, just use the original segment. if ( pathPoints.length < 2 ) { - //console.debug(`Only ${pathPoints.length} path points found.`, [...pathPoints]); + log(`Only ${pathPoints.length} path points found.`, [...pathPoints]); return []; } diff --git a/scripts/settings.js b/scripts/settings.js index 081505d..acb3bb9 100644 --- a/scripts/settings.js +++ b/scripts/settings.js @@ -7,6 +7,7 @@ canvas import { MODULE_ID, MODULES_ACTIVE, SPEED } from "./const.js"; import { ModuleSettingsAbstract } from "./ModuleSettingsAbstract.js"; +import { log } from "./util.js"; const SETTINGS = { CONTROLS: { @@ -252,7 +253,7 @@ function toggleTokenRulerWaypoint(context, add = true) { const position = canvas.mousePosition; const ruler = canvas.controls.ruler; if ( !canvas.tokens.active || !ruler || !ruler.active ) return; - // console.debug(`${add ? "add" : "remove"}TokenRulerWaypoint`); + log(`${add ? "add" : "remove"}TokenRulerWaypoint`); // Keep track of when we last added/deleted a waypoint. const now = Date.now(); @@ -260,7 +261,7 @@ function toggleTokenRulerWaypoint(context, add = true) { if ( delta < 100 ) return true; // Throttle keyboard movement once per 100ms MOVE_TIME = now; - // console.debug(`${add ? "adding" : "removing"}TokenRulerWaypoint`); + log(`${add ? "adding" : "removing"}TokenRulerWaypoint`); if ( add ) ruler._addWaypoint(position); else if ( ruler.waypoints.length > 1 ) ruler._removeWaypoint(position); // Removing the last waypoint throws errors. } diff --git a/scripts/util.js b/scripts/util.js index 9f67ca9..907781e 100644 --- a/scripts/util.js +++ b/scripts/util.js @@ -10,9 +10,7 @@ import { MODULE_ID } from "./const.js"; export function log(...args) { try { - const isDebugging = game.modules.get("_dev-mode")?.api?.getPackageDebugValue(MODULE_ID); - if (isDebugging) console.log(MODULE_ID, "|", ...args); - + if ( CONFIG[MODULE_ID].debug ) console.debug(MODULE_ID, "|", ...args); } catch(e) { // Empty } From a8e3986c2a16058e5aa1e962a5c95659f53c1d82 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Mon, 22 Jan 2024 13:51:37 -0800 Subject: [PATCH 10/10] Update changelog --- Changelog.md | 6 +++--- scripts/terrain_elevation.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Changelog.md b/Changelog.md index b432a8d..d76a1d3 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,8 +1,8 @@ # 0.8.1 - -Fix toggling on/off the pathfinding between waypoints. +Fix toggling pathfinding between waypoints. Various fixes to display of ruler for other users. - +Fixes for display of distance calculations between waypoints. +Possible fix for issue #33 (undefined `token.bounds`). # 0.8.0 Added pathfinding toggle. Pathfinding works on gridded (both hex and square) and gridless maps. Works when dragging tokens, if Token Ruler is enabled, or when using the Ruler control and you start at a token. diff --git a/scripts/terrain_elevation.js b/scripts/terrain_elevation.js index f4baec4..b561df4 100644 --- a/scripts/terrain_elevation.js +++ b/scripts/terrain_elevation.js @@ -93,7 +93,7 @@ function elevationAtLocation(location, measuringToken, startingElevation = Numbe // Prioritize the highest token at the location const max_token_elevation = retrieveVisibleTokens().reduce((e, t) => { // Is the point within the token control area? - // Issue #34: bounds can apparently be undefined in some systems? + // Issue #33: bounds can apparently be undefined in some systems? if ( !t.bounds || !t.bounds.contains(location.x, location.y) ) return e; return Math.max(tokenElevation(t), e); }, Number.NEGATIVE_INFINITY);