From cf74a11f18b1a86cfca1663a682147af66fc1c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20V=C3=B6gele?= Date: Fri, 30 Sep 2022 14:01:07 +0200 Subject: [PATCH] v10 compat: Split measure function into several subfunctions as suggested by Foundry v10 --- js/foundry_imports.js | 208 +----------------------------------- js/ruler.js | 239 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 233 insertions(+), 214 deletions(-) diff --git a/js/foundry_imports.js b/js/foundry_imports.js index 2dbea9f..4207246 100644 --- a/js/foundry_imports.js +++ b/js/foundry_imports.js @@ -1,25 +1,8 @@ -import {highlightMeasurementTerrainRuler, measureDistances} from "./compatibility.js"; -import { - getGridPositionFromPixels, - getGridPositionFromPixelsObj, - getPixelsFromGridPositionObj, -} from "./foundry_fixes.js"; -import {Line} from "./geometry.js"; +import {getGridPositionFromPixels} from "./foundry_fixes.js"; import {disableSnap, moveWithoutAnimation} from "./keybindings.js"; import {trackRays} from "./movement_tracking.js"; -import {findPath, isPathfindingEnabled} from "./pathfinding.js"; -import {settingsKey} from "./settings.js"; import {recalculate} from "./socket.js"; -import { - applyTokenSizeOffset, - enumeratedZip, - getSnapPointForEntity, - getSnapPointForToken, - getSnapPointForTokenObj, - getTokenShape, - highlightTokenShape, - sum, -} from "./util.js"; +import {getSnapPointForToken, highlightTokenShape, sum} from "./util.js"; // This is a modified version of Ruler.moveToken from foundry 0.7.9 export async function moveEntities(draggedEntity, selectedEntities) { @@ -195,193 +178,6 @@ export function cancelScheduledMeasurement() { this.deferredMeasurementResolve?.(); } -// This is a modified version of Ruler.measure form foundry 0.7.9 -export function measure(destination, options = {}) { - const isToken = this.draggedEntity instanceof Token; - if (isToken && !this.draggedEntity.isVisible) return []; - - options.snap = options.snap ?? !disableSnap; - - if (options.snap) { - destination = getSnapPointForEntity(destination.x, destination.y, this.draggedEntity); - } - - this.dragRulerRemovePathfindingWaypoints(); - - 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); - if (path) path.shift(); - if (path && path.length > 0) { - path = path.map(point => - getSnapPointForTokenObj(getPixelsFromGridPositionObj(point), this.draggedEntity), - ); - - // If the token is snapped to the grid, the first point of the path is already handled by the ruler - if ( - path[0].x === this.waypoints[this.waypoints.length - 1].x && - path[0].y === this.waypoints[this.waypoints.length - 1].y - ) - path = path.slice(1); - - // If snapping is enabled, the last point of the path is already handled by the ruler - if (options.snap) path = path.slice(0, path.length - 1); - - for (const point of path) { - point.isPathfinding = true; - this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle)); - } - this.waypoints = this.waypoints.concat(path); - } else { - // Don't show a path if the pathfinding yields no result to show the user that the destination is unreachable - destination = this.waypoints[this.waypoints.length - 1]; - } - } - - if (options.gridSpaces === undefined) { - options.gridSpaces = canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS; - } - - if (canvas.grid.diagonalRule === "EUCL") { - options.gridSpaces = false; - options.ignoreGrid = true; - } - - if (options.ignoreGrid === undefined) { - options.ignoreGrid = false; - } - - options.enableTerrainRuler = isToken && game.modules.get("terrain-ruler")?.active; - - 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 = 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 (!options.enableTerrainRuler && !options.ignoreGrid) - centeredWaypoints.forEach(w => ([w.x, w.y] = canvas.grid.getCenter(w.x, w.y))); - - const r = this.ruler; - this.destination = destination; - - // Iterate over waypoints and construct segment rays - const segments = []; - const centeredSegments = []; - for (let [i, dest] of waypoints.slice(1).entries()) { - const centeredDest = centeredWaypoints[i + 1]; - const origin = waypoints[i]; - const centeredOrigin = centeredWaypoints[i]; - const label = this.labels.children[i]; - const ray = new Ray(origin, dest); - const centeredRay = new Ray(centeredOrigin, centeredDest); - ray.isPrevious = Boolean(origin.isPrevious); - centeredRay.isPrevious = ray.isPrevious; - ray.dragRulerVisitedSpaces = origin.dragRulerVisitedSpaces; - centeredRay.dragRulerVisitedSpaces = ray.dragRulerVisitedSpaces; - ray.dragRulerFinalState = origin.dragRulerFinalState; - centeredRay.dragRulerFinalState = ray.dragRulerFinalState; - if (ray.distance < 10) { - if (label) label.visible = false; - continue; - } - segments.push({ray, label}); - centeredSegments.push({ray: centeredRay, label}); - } - - const shape = isToken ? getTokenShape(this.draggedEntity) : null; - - // Compute measured distance - const distances = measureDistances(centeredSegments, this.draggedEntity, shape, options); - - let totalDistance = 0; - for (let [i, d] of distances.entries()) { - let s = centeredSegments[i]; - s.startDistance = totalDistance; - totalDistance += d; - s.last = i === centeredSegments.length - 1; - s.distance = d; - s.text = this._getSegmentLabel(d, totalDistance, s.last); - } - - // Clear the grid highlight layer - const hlt = canvas.grid.highlightLayers[this.name] || canvas.grid.addHighlightLayer(this.name); - hlt.clear(); - - // Draw measured path - r.clear(); - let rulerColor; - if (!options.gridSpaces || canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) - rulerColor = this.dragRulerGetColorForDistance(totalDistance); - else rulerColor = this.color; - for (const [i, s, cs] of enumeratedZip( - [...segments].reverse(), - [...centeredSegments].reverse(), - )) { - const {label, text, last} = cs; - - // Draw line segment - const opacityMultiplier = s.ray.isPrevious ? 0.33 : 1; - r.lineStyle(6, 0x000000, 0.5 * opacityMultiplier) - .moveTo(s.ray.A.x, s.ray.A.y) - .lineTo(s.ray.B.x, s.ray.B.y) - .lineStyle(4, rulerColor, 0.25 * opacityMultiplier) - .moveTo(s.ray.A.x, s.ray.A.y) - .lineTo(s.ray.B.x, s.ray.B.y); - - // Draw the distance label just after the endpoint of the segment - if (label) { - label.text = text; - label.alpha = last ? 1.0 : 0.5; - label.visible = true; - let labelPosition = {x: s.ray.x0, y: s.ray.y0}; - labelPosition.x -= label.width / 2; - labelPosition.y -= label.height / 2; - const rayLine = Line.fromPoints(s.ray.A, s.ray.B); - const rayLabelXHitY = rayLine.calcY(labelPosition.x); - let innerDistance; - // If ray hits top or bottom side of label - if ( - rayLine.isVertical || - rayLabelXHitY < labelPosition.y || - rayLabelXHitY > labelPosition.y + label.height - ) - innerDistance = Math.abs(label.height / 2 / Math.sin(s.ray.angle)); - // If ray hits left or right side of label - else innerDistance = Math.abs(label.width / 2 / Math.cos(s.ray.angle)); - labelPosition = s.ray.project((s.ray.distance + 50 + innerDistance) / s.ray.distance); - labelPosition.x -= label.width / 2; - labelPosition.y -= label.height / 2; - label.position.set(labelPosition.x, labelPosition.y); - } - - // Highlight grid positions - if (isToken && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS && options.gridSpaces) { - if (options.enableTerrainRuler) { - highlightMeasurementTerrainRuler.call( - this, - cs.ray, - cs.startDistance, - shape, - opacityMultiplier, - ); - } else { - const previousSegments = centeredSegments.slice(0, segments.length - 1 - i); - highlightMeasurementNative.call(this, cs.ray, previousSegments, shape, opacityMultiplier); - } - } - } - - // Draw endpoints - for (let p of waypoints) { - r.lineStyle(2, 0x000000, 0.5).beginFill(rulerColor, 0.25).drawCircle(p.x, p.y, 8); - } - - // Return the measured segments - return segments; -} - export function highlightMeasurementNative( ray, previousSegments, diff --git a/js/ruler.js b/js/ruler.js index 8977f31..e4a7bcc 100644 --- a/js/ruler.js +++ b/js/ruler.js @@ -3,11 +3,16 @@ import { getColorForDistanceAndToken, getRangesFromSpeedProvider, } from "./api.js"; -import {getHexSizeSupportTokenGridCenter} from "./compatibility.js"; -import {cancelScheduledMeasurement, measure} from "./foundry_imports.js"; +import { + getHexSizeSupportTokenGridCenter, + highlightMeasurementTerrainRuler, + measureDistances, +} from "./compatibility.js"; +import {cancelScheduledMeasurement, highlightMeasurementNative} from "./foundry_imports.js"; +import {disableSnap} from "./keybindings.js"; import {getMovementHistory} from "./movement_tracking.js"; import {settingsKey} from "./settings.js"; -import {getSnapPointForEntity} from "./util.js"; +import {applyTokenSizeOffset, getSnapPointForEntity, getTokenShape} from "./util.js"; export function extendRuler() { class DragRulerRuler extends CONFIG.Canvas.rulerClass { @@ -67,13 +72,231 @@ export function extendRuler() { } 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 { + if (!this.isDragRuler) { return super.measure(destination, options); } + if (options.gridSpaces === undefined) { + options.gridSpaces = canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS; + } + this.dragRulerGridSpaces = options.gridSpaces; + const isToken = this.draggedEntity instanceof Token; + if (isToken && !this.draggedEntity.isVisible) { + return []; + } + if (canvas.grid.diagonalRule === "EUCL") { + options.gridSpaces = false; + options.ignoreGrid = true; + } + if (options.ignoreGrid === undefined) { + options.ignoreGrid = false; + } + this.dragRulerIgnoreGrid = options.ignoreGrid; + // 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; + } + this.dragRulerSnap = options.snap ?? !disableSnap; + this.dragRulerEnableTerrainRuler = isToken && window.terrainRuler; + + // Compute the measurement destination, segments, and distance + const d = this._getMeasurementDestination(destination); + if (d.x === this.destination.x && d.y === this.destination.y) return; + this.destination = d; + this.segments = this._getMeasurementSegments(); + this._computeDistance(options.gridSpaces); + + // Draw the ruler graphic + this.ruler.clear(); + this._drawMeasuredPath(); + + // Draw grid highlight + this.highlightLayer.clear(); + if (isToken && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS && this.dragRulerGridSpaces) { + const shape = getTokenShape(this.draggedEntity); + if (!this.dragRulerEnableTerrainRuler) { + for (const [i, segment] of [...this.segments].reverse().entries()) { + const opacityMultiplier = segment.ray.isPrevious ? 0.33 : 1; + const previousSegments = this.segments.slice(0, this.segments.length - 1 - i); + highlightMeasurementNative.call( + this, + segment.ray, + previousSegments, + shape, + opacityMultiplier, + ); + } + } else { + for (const segment of [...this.dragRulerUnsnappedSegments].reverse()) { + const opacityMultiplier = segment.ray.isPrevious ? 0.33 : 1; + highlightMeasurementTerrainRuler.call( + this, + segment.ray, + segment.startDistance, + shape, + opacityMultiplier, + ); + } + } + } + return this.segments; + } + + _getMeasurementDestination(destination) { + if (this.isDragRuler) { + if (this.dragRulerSnap) { + return getSnapPointForEntity(destination.x, destination.y, this.draggedEntity); + } else { + return destination; + } + } else { + return super._getMeasurementDestination(destination); + } + } + + _getMeasurementSegments() { + // TODO Recalculate pathfinding, if necessary + if (this.isDragRuler) { + const unsnappedWaypoints = this.waypoints.concat([this.destination]); + const waypoints = + this.draggedEntity instanceof Token + ? applyTokenSizeOffset(unsnappedWaypoints, this.draggedEntity) + : duplicate(unsnappedWaypoints); + const unsnappedSegments = []; + const segments = []; + for (const [i, p1] of waypoints.entries()) { + if (i === 0) continue; + const unsnappedP1 = unsnappedWaypoints[i]; + const p0 = waypoints[i - 1]; + const unsnappedP0 = unsnappedWaypoints[i - 1]; + const label = this.labels.children[i - 1]; + const ray = new Ray(p0, p1); + const unsnappedRay = new Ray(unsnappedP0, unsnappedP1); + ray.isPrevious = Boolean(unsnappedP0.isPrevious); + unsnappedRay.isPrevious = ray.isPrevious; + ray.dragRulerVisitedSpaces = unsnappedP0.dragRulerVisitedSpaces; + unsnappedRay.dragRulerVisitedSpaces = ray.dragRulerVisitedSpaces; + ray.dragRulerFinalState = unsnappedP0.dragRulerFinalState; + unsnappedRay.dragRulerFinalState = ray.dragRulerFinalState; + if (ray.distance < 10) { + if (label) label.visible = false; + continue; + } + segments.push({ray, label}); + unsnappedSegments.push({ray: unsnappedRay, label}); + } + this.dragRulerUnsnappedSegments = unsnappedSegments; + return segments; + } else { + return super._getMeasurementSegments(); + } + } + + _computeDistance(gridSpaces) { + if (!this.isDragRuler) { + return super._computeDistance(gridSpaces); + } + if (!this.dragRulerEnableTerrainRuler) { + if (!this.dragRulerIgnoreGrid) { + gridSpaces = true; + } + super._computeDistance(gridSpaces); + } else { + const shape = this.draggedEntity ? getTokenShape(this.draggedEntity) : null; + const options = { + ignoreGrid: this.dragRulerIgnoreGrid, + gridSpaces, + enableTerrainRuler: this.dragRulerEnableTerrainRuler, + }; + const distances = measureDistances(this.segments, this.draggedEntity, shape, options); + for (const [i, d] of distances.entries()) { + totalDistance += d; + let s = this.segements[i]; + s.last = i === this.segements.length - 1; + s.distance = d; + s.text = this._getSegmentLabel(s, totalDistance); + } + } + /*if (!this.dragRulerEnableTerrainRuler) { + if (!this.dragRulerIgnoreGrid) { + gridSpaces = true; + } + super._computeDistance(gridSpaces); + } else { + const unsnappedSegments = this.dragRulerUnsnappedSegments; + const firstNewSegmentIndex = unsnappedSegments.findIndex( + segment => !segment.ray.dragRulerVisitedSpaces, + ); + const previousSegments = unsnappedSegments.slice(0, firstNewSegmentIndex); + const newSegments = unsnappedSegments.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)), + ); + const shape = isToken ? getTokenShape(this.draggedEntity) : null; + const options = {}; + options.costFunction = buildCostFunction(this.draggedEntity, shape); + if (previousSegments.length > 0) + opts.terrainRulerInitialState = + previousSegments[previousSegments.length - 1].ray.dragRulerFinalState; + distances = distances.concat(terrainRuler.measureDistances(newSegments, options)); + for (const [i, d] of distances.entries()) { + totalDistance += d; + let s = this.segements[i]; + s.last = i === this.segements.length - 1; + s.distance = d; + s.text = this._getSegmentLabel(s, totalDistance); + } + }*/ + for (const [i, segment] of this.segments.entries()) { + const unsnappedSegment = this.dragRulerUnsnappedSegments[i]; + unsnappedSegment.last = segment.last; + unsnappedSegment.distance = segment.distance; + unsnappedSegment.text = segment.text; + } + } + + _drawMeasuredPath() { + if (!this.isDragRuler) { + return super._drawMeasuredPath(); + } + let rulerColor = this.color; + if (!this.dragRulerGridSpaces || canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) { + const totalDistance = this.segments.reduce((total, current) => total + current.distance, 0); + rulerColor = this.dragRulerGetColorForDistance(totalDistance); + } + const r = this.ruler.beginFill(rulerColor, 0.25); + for (const segment of this.dragRulerUnsnappedSegments) { + const opacityMultiplier = segment.ray.isPrevious ? 0.33 : 1; + const {ray, distance, label, text, last} = segment; + if (distance === 0) continue; + + // Draw Line + r.moveTo(ray.A.x, ray.A.y) + .lineStyle(6, 0x000000, 0.5 * opacityMultiplier) + .lineTo(ray.B.x, ray.B.y) + .lineStyle(4, rulerColor, 0.25 * opacityMultiplier) + .moveTo(ray.A.x, ray.A.y) + .lineTo(ray.B.x, ray.B.y); + + // Draw Waypoints + r.lineStyle(2, 0x000000, 0.5).drawCircle(ray.A.x, ray.A.y, 8); + if (last) r.drawCircle(ray.B.x, ray.B.y, 8); + + // Draw Label + if (label) { + label.text = text; + label.alpha = last ? 1.0 : 0.5; + label.visible = true; + let labelPosition = ray.project((ray.distance + 50) / ray.distance); + label.position.set(labelPosition.x, labelPosition.y); + } + } + r.endFill(); } _endMeasurement() {