Skip to content

Commit

Permalink
Merge branch 'feature/better-measured-projection' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
caewok committed Jul 25, 2021
2 parents bdb74cc + f146c4f commit 7990bdf
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 31 deletions.
3 changes: 2 additions & 1 deletion scripts/module.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { registerSettings, registerHotkeys } from "./settings.js";
import { registerRuler } from "./patching.js";
import { iterateGridUnder3dLine, projectElevatedPoint } from "./utility.js";
import { iterateGridUnder3dLine, projectElevatedPoint, projectGridless } from "./utility.js";

export const MODULE_ID = 'elevationruler';
const FORCE_DEBUG = false; // used for logging before dev mode is set up
Expand All @@ -25,6 +25,7 @@ Hooks.once('init', async function() {
log("Initializing Elevation Ruler Options.");

window['elevationRuler'] = { projectElevatedPoint: projectElevatedPoint,
projectGridless: projectGridless,
iterateGridUnder3dLine: iterateGridUnder3dLine };

});
Expand Down
4 changes: 3 additions & 1 deletion scripts/patching.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { elevationRulerAddProperties,
elevationRulerGetText } from "./segments.js";

import { calculate3dDistance,
iterateGridUnder3dLine_wrapper } from "./utility.js";
iterateGridUnder3dLine_wrapper,
points3dAlmostEqual } from "./utility.js";

export function registerRuler() {

Expand All @@ -30,6 +31,7 @@ export function registerRuler() {

// utilities
libWrapper.register(MODULE_ID, 'window.libRuler.RulerUtilities.calculateDistance', calculate3dDistance, 'MIXED');
libWrapper.register(MODULE_ID, 'window.libRuler.RulerUtilities.pointsAlmostEqual', points3dAlmostEqual, 'WRAPPER');
libWrapper.register(MODULE_ID, 'window.libRuler.RulerUtilities.iterateGridUnderLine', iterateGridUnder3dLine_wrapper, 'WRAPPER');


Expand Down
24 changes: 6 additions & 18 deletions scripts/segments.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export function elevationRulerConstructPhysicalPath(wrapped, ...args) {
// --> this is done in AddProperties function
log("Constructing the physical path.");
const default_path = wrapped(...args);
log("Default path: (${default_path.origin.x}, ${default_path.origin.y}), (${default_path.destination.x}, ${default_path.destination.y})", default_path);
log(`Default path: (${default_path.origin.x}, ${default_path.origin.y}), (${default_path.destination.x}, ${default_path.destination.y})`, default_path);

const starting_elevation = this.getFlag(MODULE_ID, "starting_elevation");
const ending_elevation = this.getFlag(MODULE_ID, "ending_elevation");
Expand All @@ -173,7 +173,7 @@ export function elevationRulerConstructPhysicalPath(wrapped, ...args) {
default_path.origin.z = starting_elevation_grid_units;
default_path.destination.z = (starting_elevation_grid_units + elevation_delta) * ratio;

log("Default path: (${default_path.origin.x}, ${default_path.origin.y}, ${default_path.origin.z}), (${default_path.destination.x}, ${default_path.destination.y}, ${default_path.destination.z})", default_path);
log(`Default path: (${default_path.origin.x}, ${default_path.origin.y}, ${default_path.origin.z}), (${default_path.destination.x}, ${default_path.destination.y}, ${default_path.destination.z})`, default_path);

return default_path;
}
Expand All @@ -196,22 +196,10 @@ export function elevationRulerMeasurePhysicalPath(wrapped, physical_path) {
if(!("z" in physical_path.origin)) physical_path.origin.z = 0;
if(!("z" in physical_path.destination)) physical_path.destination.z = 0;

if(!window.libRuler.RulerUtilities.almostEqual(physical_path.origin.z, physical_path.destination.z)) {
// Project the 3-D path to 2-D canvas
log(`Projecting physical_path from origin ${physical_path.origin.x}, ${physical_path.origin.y}, ${physical_path.origin.z} to dest ${physical_path.destination.x}, ${physical_path.destination.y}, ${physical_path.destination.z}`);
physical_path.origin = projectElevatedPoint(physical_path.origin, physical_path.destination);

// if we are using grid spaces, the destination needs to be re-centered to the grid.
// otherwise, when a token moves in 2-D diagonally, the 3-D measure will be inconsistent
// depending on cardinality of the move, as rounding will increase/decrease to the nearest gridspace
if(this.options?.gridSpaces) {
// canvas.grid.getCenter returns an array [x, y];
const snapped = canvas.grid.getCenter(physical_path.origin.x, physical_path.origin.y);
log(`Snapping ${physical_path.origin.x}, ${physical_path.origin.y} to ${snapped[0]}, ${snapped[1]}`);
physical_path.origin = { x: snapped[0], y: snapped[1] };
log(`Projected physical_path from origin ${physical_path.origin.x}, ${physical_path.origin.y} to dest ${physical_path.destination.x}, ${physical_path.destination.y}`);
}
}
// Project the 3-D path to 2-D canvas
// projectElevatedPoint will return origin/destination w/o z.
// projectElevatedPoint will not modify unless necessary.
[physical_path.origin, physical_path.destination] = projectElevatedPoint(physical_path.origin, physical_path.destination);
}

return wrapped(physical_path);
Expand Down
169 changes: 158 additions & 11 deletions scripts/utility.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,23 +60,152 @@ export function iterateGridUnder3dLine_wrapper(wrapped, origin, destination) {
* and you also move 'height' distance orthogonal to the plane, the distance is the
* hypotenuse of the triangle formed by A, B, and C, where C is orthogonal to B.
* Project by rotating the vertical triangle 90º, then calculate the new point C.
*
* Cx = { height * (By - Ay) / dist(A to B) } + Bx
* Cy = { height * (Bx - Ax) / dist(A to B) } + By
* @param {{x: number, y: number}} A
* @param {{x: number, y: number}} B
* For gridded maps, project A such that A <-> projected_A is straight on the grid.
* @param {{x: number, y: number, z: number}} A
* @param {{x: number, y: number, z: number}} B
*/
export function projectElevatedPoint(A, B) {
const height = A.z - B.z;
const distance = window.libRuler.RulerUtilities.calculateDistance(A, B);
if(window.libRuler.RulerUtilities.pointsAlmostEqual(A, B)) { return [{ x: A.x, y: A.y }, { x: B.x, y: B.y }]; }
if(B.z === undefined || B.z === NaN) { B.z = A.z; }
if(A.z === undefined || A.z === NaN) { A.z = B.z; }
if(A.z === B.z) { return [{ x: A.x, y: A.y }, { x: B.x, y: B.y }]; }
if(window.libRuler.RulerUtilities.almostEqual(A.z, B.z)) { return [{ x: A.x, y: A.y }, { x: B.x, y: B.y }]; }

switch(canvas.grid.type) {
case CONST.GRID_TYPES.GRIDLESS: return projectGridless(A, B);
case CONST.GRID_TYPES.SQUARE: return projectSquareGrid(A, B);
case CONST.GRID_TYPES.HEXODDR:
case CONST.GRID_TYPES.HEXEVENR: return projectEast(A, B);
case CONST.GRID_TYPES.HEXODDQ:
case CONST.GRID_TYPES.HEXEVENQ: return projectSouth(A, B);
}

// catch-all
return projectGridless(A, B);
}

/*
* Project A and B in a square grid.
* move A vertically or horizontally by the total height different
* If the points are already on a line, don't change B.
* So if B is to the west or east, set A to the south.
* Otherwise, set A to the east and B to the south.
* Represents the 90º rotation of the right triangle from height
*/
function projectSquareGrid(A, B) {
// if the points are already on a line, don't change B.
// Otherwise, set A to the east and B to the south
// Represents the 90º rotation of the right triangle from height
const height = Math.abs(A.z - B.z);
let projected_A, projected_B;

if(window.libRuler.RulerUtilities.almostEqual(A.x, B.x)) {
// points are on vertical line
// set A to the east
// B is either north or south from A
// (quicker than calling projectEast b/c no distance calc req'd)
projected_A = {x: A.x + height, y: A.y}; // east
projected_B = {x: B.x, y: B.y};
} else if(window.libRuler.RulerUtilities.almostEqual(A.y, B.y)) {
// points are on horizontal line
// B is either west or east from A
// set A to the south
// (quicker than calling projectSouth b/c no distance calc req'd)
projected_A = {x: A.x, y: A.y + height}; // south
projected_B = {x: B.x, y: B.y};
} else {
// set B to point south, A pointing east
[projected_A, projected_B] = projectEast(A, B, height);
}

log(`Projecting Square: A: (${A.x}, ${A.y}, ${A.z})->(${projected_A.x}, ${projected_A.y}); B: (${B.x}, ${B.y}, ${B.z})->(${projected_B.x}, ${projected_B.y})`);

return [projected_A, projected_B];
}

function projectSouth(A, B, height, distance) {
if(height === undefined) height = A.z - B.z;
if(distance === undefined) distance = gridDistance(A, B);

// set A pointing south; B pointing west
const projected_A = {x: A.x, y: A.y + height};
const projected_B = {x: A.x - distance, y: A.y};

log(`Projecting South: A: (${A.x}, ${A.y}, ${A.z})->(${projected_A.x}, ${projected_A.y}); B: (${B.x}, ${B.y}, ${B.z})->(${projected_B.x}, ${projected_B.y})`);

// if dnd5e 5-5-5 or 5-10-5, snap to nearest grid point
// origin should be fine if elevation is in increments. Otherwise, may need to be snapped. Leave for now.
// if((canvas.grid.diagonalRule === "555" || canvas.grid.diagonalRule === "5105" || game.system.id === "pf2e")) {
// log(`Snapping ${projected_B.x}, ${projected_B.y}`);
// [projected_B.x, projected_B.y] = canvas.grid.getCenter(projected_B.x, projected_B.y);
// log(`Snapped to ${projected_B.x}, ${projected_B.y}`);
// }
//
return [projected_A, projected_B];
}

function projectEast(A, B, height, distance) {
if(height === undefined) height = A.z - B.z;
if(distance === undefined) distance = gridDistance(A, B);

// set A pointing east; B pointing south
const projected_A = {x: A.x + height, y: A.y};
const projected_B = {x: A.x, y: A.y + distance};

log(`Projecting East: A: (${A.x}, ${A.y}, ${A.z})->(${projected_A.x}, ${projected_A.y}); B: (${B.x}, ${B.y}, ${B.z})->(${projected_B.x}, ${projected_B.y})`);

// if dnd5e 5-5-5 or 5-10-5, snap to nearest grid point
// origin should be fine if elevation is in increments. Otherwise, may need to be snapped. Leave for now.
// if((canvas.grid.diagonalRule === "555" || canvas.grid.diagonalRule === "5105" || game.system.id === "pf2e")) {
// log(`Snapping ${projected_B.x}, ${projected_B.y}`);
// [projected_B.x, projected_B.y] = canvas.grid.getCenter(projected_B.x, projected_B.y);
// log(`Snapped to ${projected_B.x}, ${projected_B.y}`);
// }

return [projected_A, projected_B];
}

function gridDistance(A, B) {
const use_grid = canvas.grid.diagonalRule === "555" ||
canvas.grid.diagonalRule === "5105" ||
game.system.id === "pf2e";

if(use_grid) {
const distance_segments = [{ray: new Ray(A, B)}];
return canvas.grid.measureDistances(distance_segments, { gridSpaces: true })[0] * canvas.scene.data.grid / canvas.scene.data.gridDistance;
}

return window.libRuler.RulerUtilities.calculateDistance({x: A.x, y: A.y},
{x: B.x, y: B.y})
}


/**
* Calculate a new point by projecting the elevated point back onto the 2-D surface
* If the movement on the plane is represented by moving from point A to point B,
* and you also move 'height' distance orthogonal to the plane, the distance is the
* hypotenuse of the triangle formed by A, B, and C, where C is orthogonal to B.
* Project by rotating the vertical triangle 90º, then calculate the new point C.
*
* Cx = { height * (By - Ay) / dist(A to B) } + Bx
* Cy = { height * (Bx - Ax) / dist(A to B) } + By
* @param {{x: number, y: number}} A
* @param {{x: number, y: number}} B
*/
export function projectGridless(A, B, height, distance) {
if(height === undefined) height = A.z - B.z;
if(distance === undefined) distance = window.libRuler.RulerUtilities.calculateDistance({x: A.x, y: A.y},
{x: B.x, y: B.y});

const projected_x = A.x + ((height / distance) * (B.y - A.y));
const projected_y = A.y - ((height / distance) * (B.x - A.x));

// for square grids, rotate so that the origin point A is vertical or horizontal from original A?
// for hex grids, rotate so that the origin point A is in a straight line from original A?
// this will give correct results for diagonal moves, b/c A should always be straight line to projected A, as it is a vertical move
log(`Projecting Gridless: A: (${A.x}, ${A.y}, ${A.z})->(${projected_x}, ${projected_y}); B: (${B.x}, ${B.y}, ${B.z})->(${B.x}, ${B.y})`);


return new PIXI.Point(projected_x, projected_y);
return [{ x: projected_x, y: projected_y },
{ x: B.x, y: B.y }];
}

/*
Expand All @@ -102,3 +231,21 @@ export function calculate3dDistance(wrapped, A, B, EPSILON = 1e-6) {
}


/*
* Test if two points are almost equal, given a small error window.
* @param {PIXI.Point} p1 Point in {x, y, z} format. z optional
* @param {PIXI.Point} p2 Point in {x, y, z} format.
* @return {Boolean} True if the points are within the error of each other
*/
export function points3dAlmostEqual(wrapped, p1, p2, EPSILON = 1e-6) {
const equal2d = wrapped(p1, p2, EPSILON);
if(!equal2d) return false;

if(p1.z === undefined ||
p2.z === undefined ||
p1.z === NaN ||
p2.z === NaN) return true;

return window.libRuler.RulerUtilities.almostEqual(p1.z, p2.z, EPSILON);
}

0 comments on commit 7990bdf

Please sign in to comment.