From acd2e74064e67bd43a0eea4be51c7aef030042a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20V=C3=B6gele?= Date: Tue, 18 May 2021 13:53:02 +0200 Subject: [PATCH] Implement measurement template support (resolves #13) --- CHANGELOG.md | 1 + src/compatibility.js | 7 ++-- src/foundry_imports.js | 75 +++++++++++++++++++++++------------------- src/main.js | 60 +++++++++++++++++---------------- src/util.js | 23 +++++++++++++ 5 files changed, 101 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a69d383..e87cd87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ **BREAKING** This update is incompatible with previous Terrain Ruler versions. If you're using Terrain Ruler, make sure you update Terrain Ruler to at least version 1.3.0. ### New features +- A ruler will now be shown when dragging measurement templates over the map ([#13](https://github.com/manuelVo/foundryvtt-drag-ruler/issues/13)) - Drag Ruler can now measure difficult terrain on gridless maps (if the Terrain Ruler module is installed and enabled) - Improved the positioning of the labels around the ruler. The labels should now never overlap with the waypoint. diff --git a/src/compatibility.js b/src/compatibility.js index 9ce5ab4..666d1d7 100644 --- a/src/compatibility.js +++ b/src/compatibility.js @@ -15,11 +15,12 @@ export function highlightMeasurementTerrainRuler(ray, startDistance, tokenShape= } } -export function measureDistances(segments, token, shape, options={}) { +export function measureDistances(segments, entity, shape, options={}) { + const isToken = entity instanceof Token; const opts = duplicate(options) if (!opts.gridSpaces) opts.gridSpaces = true; - const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active && (!game.modules.get("TerrainLayer")?.active || canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS); + const terrainRulerAvailable = isToken && game.modules.get("terrain-ruler")?.active && (!game.modules.get("TerrainLayer")?.active || canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS); if (terrainRulerAvailable) { const firstNewSegmentIndex = segments.findIndex(segment => !segment.ray.dragRulerVisitedSpaces); @@ -27,7 +28,7 @@ export function measureDistances(segments, token, shape, options={}) { const newSegments = segments.slice(firstNewSegmentIndex); const distances = previousSegments.map(segment => segment.ray.dragRulerVisitedSpaces[segment.ray.dragRulerVisitedSpaces.length - 1].distance); previousSegments.forEach(segment => segment.ray.terrainRulerVisitedSpaces = duplicate(segment.ray.dragRulerVisitedSpaces)); - opts.costFunction = (x, y) => getCostFromSpeedProvider(token, getAreaFromPositionAndShape({x, y}, shape), {x, y}); + opts.costFunction = (x, y) => getCostFromSpeedProvider(entity, getAreaFromPositionAndShape({x, y}, shape), {x, y}); if (previousSegments.length > 0) opts.terrainRulerInitialState = previousSegments[previousSegments.length - 1].ray.dragRulerFinalState; return distances.concat(terrainRuler.measureDistances(newSegments, opts)); diff --git a/src/foundry_imports.js b/src/foundry_imports.js index 18490b7..fec35bb 100644 --- a/src/foundry_imports.js +++ b/src/foundry_imports.js @@ -4,10 +4,10 @@ import {Line} from "./geometry.js"; import {getColorForDistance} from "./main.js" import {trackRays} from "./movement_tracking.js" import {recalculate} from "./socket.js"; -import {applyTokenSizeOffset, getSnapPointForToken, getTokenShape, highlightTokenShape, zip} from "./util.js"; +import {applyTokenSizeOffset, getSnapPointForMeasuredTemplate, getSnapPointForToken, getTokenShape, highlightTokenShape, zip} from "./util.js"; // This is a modified version of Ruler.moveToken from foundry 0.7.9 -export async function moveTokens(draggedEntity, selectedTokens) { +export async function moveEntities(draggedEntity, selectedEntities) { let wasPaused = game.paused; if (wasPaused && !game.user.isGM) { ui.notifications.warn(game.i18n.localize("GAME.PausedWarning")); @@ -19,8 +19,8 @@ export async function moveTokens(draggedEntity, selectedTokens) { // Get the movement rays and check collision along each Ray // These rays are center-to-center for the purposes of collision checking const rays = this.constructor.dragRulerGetRaysFromWaypoints(this.waypoints, this.destination); - if (!game.user.isGM) { - const hasCollision = selectedTokens.some(token => { + if (!game.user.isGM && draggedEntity instanceof Token) { + const hasCollision = selectedEntities.some(token => { const offset = calculateTokenOffset(token, draggedEntity); const offsetRays = rays.filter(ray => !ray.isPrevious).map(ray => applyOffsetToRay(ray, offset)) return offsetRays.some(r => canvas.walls.checkCollision(r)); @@ -35,7 +35,7 @@ export async function moveTokens(draggedEntity, selectedTokens) { // Execute the movement path. // Transform each center-to-center ray into a top-left to top-left ray using the prior token offsets. this._state = Ruler.STATES.MOVING; - await animateTokens.call(this, selectedTokens, draggedEntity, rays, wasPaused); + await animateEntities.call(this, selectedEntities, draggedEntity, rays, wasPaused); // Once all animations are complete we can clear the ruler if (this.draggedEntity?.id === draggedEntity.id) @@ -43,51 +43,53 @@ export async function moveTokens(draggedEntity, selectedTokens) { } // This is a modified version code extracted from Ruler.moveToken from foundry 0.7.9 -async function animateTokens(tokens, draggedEntity, draggedRays, wasPaused) { +async function animateEntities(entities, draggedEntity, draggedRays, wasPaused) { const newRays = draggedRays.filter(r => !r.isPrevious); - const tokenAnimationData = tokens.map(token => { - const tokenOffset = calculateTokenOffset(token, draggedEntity); - const offsetRays = newRays.map(ray => applyOffsetToRay(ray, tokenOffset)); + const entityAnimationData = entities.map(entity => { + const entityOffset = calculateEntityOffset(entity, draggedEntity); + const offsetRays = newRays.map(ray => applyOffsetToRay(ray, entityOffset)); // Determine offset relative to the Token top-left. // This is important so we can position the token relative to the ruler origin for non-1x1 tokens. const firstWaypoint = this.waypoints.find(w => !w.isPrevious); - const origin = [firstWaypoint.x + tokenOffset.x, firstWaypoint.y + tokenOffset.y]; + const origin = [firstWaypoint.x + entityOffset.x, firstWaypoint.y + entityOffset.y]; let dx, dy; if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) { - dx = token.data.x - origin[0]; - dy = token.data.y - origin[1]; + dx = entity.data.x - origin[0]; + dy = entity.data.y - origin[1]; } else { - dx = token.data.x - origin[0]; - dy = token.data.y - origin[1]; + dx = entity.data.x - origin[0]; + dy = entity.data.y - origin[1]; } - return {token, rays: offsetRays, dx, dy}; + return {entity, rays: offsetRays, dx, dy}; }); - const animate = !game.keyboard.isDown("Alt"); - const startWaypoint = animate ? 0 : tokenAnimationData[0].rays.length - 1; - for (let i = startWaypoint;i < tokenAnimationData[0].rays.length; i++) { + const isToken = draggedEntity instanceof Token; + const animate = isToken && !game.keyboard.isDown("Alt"); + const startWaypoint = animate ? 0 : entityAnimationData[0].rays.length - 1; + for (let i = startWaypoint;i < entityAnimationData[0].rays.length; i++) { if (!wasPaused && game.paused) break; - const tokenPaths = tokenAnimationData.map(({token, rays, dx, dy}) => { + const entityPaths = entityAnimationData.map(({entity, rays, dx, dy}) => { const ray = rays[i]; const dest = [ray.B.x, ray.B.y]; - const path = new Ray({x: token.x, y: token.y}, {x: dest[0] + dx, y: dest[1] + dy}); - return {token, path}; + const path = new Ray({x: entity.x, y: entity.y}, {x: dest[0] + dx, y: dest[1] + dy}); + return {entity, path}; }); - const updates = tokenPaths.map(({token, path}) => { - return {x: path.B.x, y: path.B.y, _id: token.id}; + const updates = entityPaths.map(({entity, path}) => { + return {x: path.B.x, y: path.B.y, _id: entity.id}; }); await draggedEntity.scene.updateEmbeddedEntity(draggedEntity.constructor.embeddedName, updates, {animate}); if (animate) - await Promise.all(tokenPaths.map(({token, path}) => token.animateMovement(path))); + await Promise.all(entityPaths.map(({entity, path}) => entity.animateMovement(path))); } - trackRays(tokens, tokenAnimationData.map(({rays}) => rays)).then(() => recalculate(tokens)); + if (isToken) + trackRays(entities, entityAnimationData.map(({rays}) => rays)).then(() => recalculate(entities)); } -function calculateTokenOffset(tokenA, tokenB) { - return {x: tokenA.data.x - tokenB.data.x, y: tokenA.data.y - tokenB.data.y} +function calculateEntityOffset(entityA, entityB) { + return {x: entityA.data.x - entityB.data.x, y: entityA.data.y - entityB.data.y}; } function applyOffsetToRay(ray, offset) { @@ -119,17 +121,22 @@ export function onMouseMove(event) { // This is a modified version of Ruler.measure form foundry 0.7.9 export function measure(destination, {gridSpaces=true, snap=false} = {}) { - if (this.isDragRuler && !this.draggedEntity.isVisible) + const isToken = this.draggedEntity instanceof Token; + if (isToken && !this.draggedEntity.isVisible) return [] - if (snap) - destination = getSnapPointForToken(destination.x, destination.y, this.draggedEntity); + if (snap) { + if (isToken) + destination = getSnapPointForToken(destination.x, destination.y, this.draggedEntity); + else + destination = getSnapPointForMeasuredTemplate(destination.x, destination.y); + } - const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active && (!game.modules.get("TerrainLayer")?.active || canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS); + const terrainRulerAvailable = isToken && game.modules.get("terrain-ruler")?.active && (!game.modules.get("TerrainLayer")?.active || canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS); const waypoints = this.waypoints.concat([destination]); // Move the waypoints to the center of the grid if a size is used that measures from edge to edge - const centeredWaypoints = applyTokenSizeOffset(waypoints, this.draggedEntity); + const centeredWaypoints = isToken ? applyTokenSizeOffset(waypoints, this.draggedEntity) : duplicate(waypoints); // Foundries native ruler requires the waypoints to sit in the dead center of the square to work properly if (!terrainRulerAvailable) centeredWaypoints.forEach(w => [w.x, w.y] = canvas.grid.getCenter(w.x, w.y)); @@ -162,7 +169,7 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) { } - const shape = getTokenShape(this.draggedEntity) + const shape = isToken ? getTokenShape(this.draggedEntity) : null; // Compute measured distance const distances = measureDistances(centeredSegments, this.draggedEntity, shape, {gridSpaces}); @@ -220,7 +227,7 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) { } // Highlight grid positions - if (canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS) { + if (isToken && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS) { if (terrainRulerAvailable) highlightMeasurementTerrainRuler.call(this, cs.ray, cs.startDistance, shape, opacityMultiplier) else diff --git a/src/main.js b/src/main.js index 11fb69a..11036c9 100644 --- a/src/main.js +++ b/src/main.js @@ -2,7 +2,7 @@ import {currentSpeedProvider, getMovedDistanceFromToken, getRangesFromSpeedProvider, getUnreachableColorFromSpeedProvider, initApi, registerModule, registerSystem} from "./api.js" import {checkDependencies, getHexSizeSupportTokenGridCenter} from "./compatibility.js"; -import {moveTokens, onMouseMove} from "./foundry_imports.js" +import {moveEntities, onMouseMove} from "./foundry_imports.js" import {performMigrations} from "./migration.js" import {DragRulerRuler} from "./ruler.js"; import {getMovementHistory, resetMovementHistory} from "./movement_tracking.js"; @@ -13,7 +13,8 @@ import {SpeedProvider} from "./speed_provider.js" Hooks.once("init", () => { registerSettings() initApi() - hookTokenDragHandlers() + hookDragHandlers(Token); + hookDragHandlers(MeasuredTemplate); hookKeyboardManagerFunctions() Ruler = DragRulerRuler; @@ -54,29 +55,29 @@ Hooks.on("getCombatTrackerEntryContext", function (html, menu) { menu.splice(1, 0, entry); }); -function hookTokenDragHandlers() { - const originalDragLeftStartHandler = Token.prototype._onDragLeftStart - Token.prototype._onDragLeftStart = function(event) { +function hookDragHandlers(entityType) { + const originalDragLeftStartHandler = entityType.prototype._onDragLeftStart + entityType.prototype._onDragLeftStart = function(event) { originalDragLeftStartHandler.call(this, event) - onTokenLeftDragStart.call(this, event) + onEntityLeftDragStart.call(this, event) } - const originalDragLeftMoveHandler = Token.prototype._onDragLeftMove - Token.prototype._onDragLeftMove = function (event) { + const originalDragLeftMoveHandler = entityType.prototype._onDragLeftMove + entityType.prototype._onDragLeftMove = function (event) { originalDragLeftMoveHandler.call(this, event) - onTokenLeftDragMove.call(this, event) + onEntityLeftDragMove.call(this, event) } - const originalDragLeftDropHandler = Token.prototype._onDragLeftDrop - Token.prototype._onDragLeftDrop = function (event) { - const eventHandled = onTokenDragLeftDrop.call(this, event) + const originalDragLeftDropHandler = entityType.prototype._onDragLeftDrop + entityType.prototype._onDragLeftDrop = function (event) { + const eventHandled = onEntityDragLeftDrop.call(this, event) if (!eventHandled) originalDragLeftDropHandler.call(this, event) } - const originalDragLeftCancelHandler = Token.prototype._onDragLeftCancel - Token.prototype._onDragLeftCancel = function (event) { - const eventHandled = onTokenDragLeftCancel.call(this, event) + const originalDragLeftCancelHandler = entityType.prototype._onDragLeftCancel + entityType.prototype._onDragLeftCancel = function (event) { + const eventHandled = onEntityDragLeftCancel.call(this, event) if (!eventHandled) originalDragLeftCancelHandler.call(this, event) } @@ -124,45 +125,48 @@ function onKeyShift(up) { ruler.measure(measurePosition, {snap: up}) } -function onTokenLeftDragStart(event) { - if (!currentSpeedProvider.usesRuler(this)) +function onEntityLeftDragStart(event) { + const isToken = this instanceof Token; + if (isToken && !currentSpeedProvider.usesRuler(this)) return const ruler = canvas.controls.ruler ruler.draggedEntity = this; - let tokenCenter - if (canvas.grid.isHex && game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(this)) - tokenCenter = getHexSizeSupportTokenGridCenter(this) + let entityCenter; + if (isToken && canvas.grid.isHex && game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(this)) + entityCenter = getHexSizeSupportTokenGridCenter(this); else - tokenCenter = this.center + entityCenter = this.center; ruler.clear(); ruler._state = Ruler.STATES.STARTING; - ruler.rulerOffset = {x: tokenCenter.x - event.data.origin.x, y: tokenCenter.y - event.data.origin.y} - if (game.settings.get(settingsKey, "enableMovementHistory")) + ruler.rulerOffset = {x: entityCenter.x - event.data.origin.x, y: entityCenter.y - event.data.origin.y}; + if (isToken && game.settings.get(settingsKey, "enableMovementHistory")) ruler.dragRulerAddWaypointHistory(getMovementHistory(this)); - ruler.dragRulerAddWaypoint(tokenCenter, false); + ruler.dragRulerAddWaypoint(entityCenter, false); } -function onTokenLeftDragMove(event) { +function onEntityLeftDragMove(event) { const ruler = canvas.controls.ruler if (ruler.isDragRuler) onMouseMove.call(ruler, event) } -function onTokenDragLeftDrop(event) { +function onEntityDragLeftDrop(event) { const ruler = canvas.controls.ruler if (!ruler.isDragRuler) return false onMouseMove.call(ruler, event); + // When we're dragging a measured template no token will ever be selected, + // resulting in only the dragged template to be moved as would be expected const selectedTokens = canvas.tokens.controlled // This can happen if the user presses ESC during drag (maybe there are other ways too) if (selectedTokens.length === 0) selectedTokens.push(ruler.draggedEntity); ruler._state = Ruler.STATES.MOVING - moveTokens.call(ruler, ruler.draggedEntity, selectedTokens); + moveEntities.call(ruler, ruler.draggedEntity, selectedTokens); return true } -function onTokenDragLeftCancel(event) { +function onEntityDragLeftCancel(event) { // This function is invoked by right clicking const ruler = canvas.controls.ruler if (!ruler.isDragRuler || ruler._state === Ruler.STATES.MOVING) diff --git a/src/util.js b/src/util.js index 6f63beb..fd61907 100644 --- a/src/util.js +++ b/src/util.js @@ -39,6 +39,29 @@ export function getSnapPointForToken(x, y, token) { return new PIXI.Point(snappedX + canvas.grid.w / 2, snappedY + canvas.grid.h / 2) } +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 highlightTokenShape(position, shape, color, alpha) { const layer = canvas.grid.highlightLayers[this.name]; if ( !layer )