Skip to content

Commit

Permalink
Update rulers where the start/end points are tokens to track those to…
Browse files Browse the repository at this point in the history
…kens when they move.

Refactor the ruler layer to have `LineOfSightRulerGroup`s, which manage a set of rulers. Each ruler in the group can have a start/end point that is either a Token or a 3D point. If either start/end is a Token, then can choose whether to draw just centre-to-centre or also include edge-to-edge sub-rulers. When a token is refreshed, the rulers are re-drawn.
Add a `current` static property to LineOfSightRulerLayer and TerrainHeightLayer. These return the relevant layer from the canvas, simplifying other code and improving intellisense.
  • Loading branch information
Wibble199 committed Jan 29, 2025
1 parent 87c4929 commit 3579965
Show file tree
Hide file tree
Showing 13 changed files with 308 additions and 213 deletions.
2 changes: 1 addition & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Represents a point in 3D space.
![Available Since v0.3.0](https://img.shields.io/badge/Available%20Since-v0.3.0-blue?style=flat-square)
![Changed in v0.4.0](https://img.shields.io/badge/Changed%20In-v0.4.0-orange?style=flat-square)

Computes a line sight test between two points in 3d space.
Computes a line of sight test between two points in 3d space.

Note that this will always return an empty array if the line of sight ray is zero-length, even if the start/end point is within a shape.

Expand Down
67 changes: 31 additions & 36 deletions module/api.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import { defaultGroupName, moduleName, settings } from "./consts.mjs";
import { HeightMap } from "./geometry/height-map.mjs";
import { LineOfSightRulerLayer } from "./layers/line-of-sight-ruler-layer.mjs";
import { TerrainHeightLayer } from "./layers/terrain-height-layer.mjs";
import { getTerrainTypes } from "./utils/terrain-types.mjs";
import { calculateRaysBetweenTokensOrPoints } from "./utils/token-utils.mjs";

export { getTerrainTypes } from "./utils/terrain-types.mjs";

Expand All @@ -30,9 +32,7 @@ export function getTerrainType(terrain) {
* @returns {{ terrainTypeId: string; height: number; elevation: number; }[]}
*/
export function getCell(x, y) {
/** @type {import("./geometry/height-map.mjs").HeightMap} */
const hm = game.canvas.terrainHeightLayer._heightMap;
return hm.get(y, x);
return TerrainHeightLayer.current?._heightMap.get(y, x);
}

/**
Expand All @@ -42,9 +42,7 @@ export function getCell(x, y) {
* @param {import("./geometry/height-map.mjs").HeightMapShape | undefined}
*/
export function getShapes(x, y) {
/** @type {import("./geometry/height-map.mjs").HeightMap} */
const hm = game.canvas.terrainHeightLayer._heightMap;
return hm.getShapes(y, x);
return TerrainHeightLayer.current?._heightMap.getShapes(y, x);
}

/**
Expand Down Expand Up @@ -72,9 +70,7 @@ export function paintCells(cells, terrain, { mode = "totalReplace" } = {}) {
if (terrainType.usesHeight && typeof terrain.height !== "number")
throw new Error(`Terrain "${terrainType.name}' requires a height, but one was not provided.`);

/** @type {import("./geometry/height-map.mjs").HeightMap} */
const hm = game.canvas.terrainHeightLayer._heightMap;
return hm.paintCells(cells, terrainType.id, terrain.height ?? 0, terrain.elevation ?? 0, { mode });
return TerrainHeightLayer.current?._heightMap.paintCells(cells, terrainType.id, terrain.height ?? 0, terrain.elevation ?? 0, { mode });
}

/**
Expand All @@ -87,9 +83,7 @@ export function eraseCells(cells) {
throw new Error("Expected `cells` to be an array of arrays.");
if (cells.length === 0) return;

/** @type {import("./geometry/height-map.mjs").HeightMap} */
const hm = game.canvas.terrainHeightLayer._heightMap;
return hm.eraseCells(cells);
return TerrainHeightLayer.current?._heightMap.eraseCells(cells);
}

/**
Expand Down Expand Up @@ -117,9 +111,7 @@ export function calculateLineOfSight(p1, p2, options = {}) {
* @returns {{ shape: import('./geometry/height-map.mjs').HeightMapShape; regions: import('./geometry/height-map-shape.mjs').LineOfSightIntersectionRegion[]; }[]}
*/
export function calculateLineOfSightByShape(p1, p2, options = {}) {
/** @type {import("./geometry/height-map.mjs").HeightMap} */
const hm = game.canvas.terrainHeightLayer._heightMap;
return hm.calculateLineOfSight(p1, p2, options);
return TerrainHeightLayer.current?._heightMap.calculateLineOfSight(p1, p2, options);
}

/**
Expand All @@ -137,7 +129,7 @@ export function calculateLineOfSightByShape(p1, p2, options = {}) {
*/
export function calculateLineOfSightRaysBetweenTokens(token1, token2, { token1RelativeHeight, token2RelativeHeight } = {}) {
const defaultRelativeHeight = game.settings.get(moduleName, settings.defaultTokenLosTokenHeight);
const [left, centre, right] = LineOfSightRulerLayer._calculateRaysBetweenTokens(token1, token2, token1RelativeHeight ?? defaultRelativeHeight, token2RelativeHeight ?? defaultRelativeHeight);
const { left, centre, right } = calculateRaysBetweenTokensOrPoints(token1, token2, token1RelativeHeight ?? defaultRelativeHeight, token2RelativeHeight ?? defaultRelativeHeight);
return {
left: { p1: left[0], p2: left[1] },
centre: { p1: centre[0], p2: centre[1] },
Expand All @@ -159,24 +151,30 @@ export function calculateLineOfSightRaysBetweenTokens(token1, token2, { token1Re
* @param {} [options.showLabels=true] Whether height labels are shown at the start and end of the ruler.
*/
export function drawLineOfSightRay(p1, p2, { group = defaultGroupName, drawForOthers = true, includeNoHeightTerrain = false, showLabels = true } = {}) {
if (!LineOfSightRulerLayer._isPoint3d(p1)) throw new Error("`p1` is not a valid Point3D. Expected an object with `x`, `y`, and `h` properties.");
if (!LineOfSightRulerLayer._isPoint3d(p2)) throw new Error("`p2` is not a valid Point3D. Expected an object with `x`, `y`, and `h` properties.");
return drawLineOfSightRays([{ p1, p2, options: { includeNoHeightTerrain, showLabels } }], { group, drawForOthers });
LineOfSightRulerLayer.current?._drawLineOfSightRays([{
a: p1,
b: p2,
includeNoHeightTerrain,
showLabels
}], { group, drawForOthers });
}

/**
* Calculates and draws any number of line of sight rays between the given points.
* Note that this will clear all previously drawn lines, INCLUDING those drawn by the tools in the side bar.
* @param {({ p1: import("./layers/line-of-sight-ruler-layer.mjs").Point3D; p2: import("./layers/line-of-sight-ruler-layer.mjs").Point3D; } & import("./layers/line-of-sight-ruler-layer.mjs").RulerOptions)[]} rays
* @param {import("./layers/line-of-sight-ruler-layer.mjs").LineOfSightRulerConfiguration[]} rays
* @param {Object} [options={}] Options that change for the lines are drawn.
* @param {string} [options.group] The name for this group of rulers. It is strongly recommended to provide a value for
* this. Recommended to use something unique, e.g. `"my-module-name"` or `"my-module-name.group1"`.
* @param {boolean} [options.drawForOthers=true] Whether to draw these rays for other users connected to the game.
*/
export function drawLineOfSightRays(rays, { group = defaultGroupName, drawForOthers = true } = {}) {
/** @type {LineOfSightRulerLayer} */
const ruler = game.canvas.terrainHeightLosRulerLayer;
ruler._drawLineOfSightRays(rays.map(({ p1, p2, ...options }) => [p1, p2, options]), { group, drawForOthers });
// For legacy reasons, if a and b are not provided, use p1 and p2.
LineOfSightRulerLayer.current?._drawLineOfSightRays(rays.map(ray => ({
...ray,
a: ray.a ?? ray.p1,
b: ray.b ?? ray.p2
})), { group, drawForOthers });
}

/**
Expand All @@ -197,17 +195,16 @@ export function drawLineOfSightRays(rays, { group = defaultGroupName, drawForOth
* @param {boolean} [options.includeNoHeightTerrain=false] If true, terrain types that are configured as not using a
* height value will be included in the return list. They are treated as having infinite height.
* @param {boolean} [options.drawForOthers] Whether to draw these rays for other users connected to the game.
* @param {boolean} [options.includeEdges] Whether to include edge-to-edge rulers between tokens.
*/
export function drawLineOfSightRaysBetweenTokens(token1, token2, { group = defaultGroupName, token1RelativeHeight, token2RelativeHeight, includeNoHeightTerrain = false, drawForOthers = true } = {}) {
const { left, centre, right } = calculateLineOfSightRaysBetweenTokens(token1, token2, { token1RelativeHeight, token2RelativeHeight });

/** @type {LineOfSightRulerLayer} */
const ruler = game.canvas.terrainHeightLosRulerLayer;
ruler._drawLineOfSightRays([
[left.p1, left.p2, { includeNoHeightTerrain, showLabels: false }],
[centre.p1, centre.p2, { includeNoHeightTerrain, showLabels: true }],
[right.p1, right.p2, { includeNoHeightTerrain, showLabels: false }],
], { group, drawForOthers });
export function drawLineOfSightRaysBetweenTokens(token1, token2, { group = defaultGroupName, token1RelativeHeight, token2RelativeHeight, includeNoHeightTerrain = false, drawForOthers = true, includeEdges = true } = {}) {
const defaultRelativeHeight = game.settings.get(moduleName, settings.defaultTokenLosTokenHeight);
LineOfSightRulerLayer.current?._drawLineOfSightRays([{
a: token1, ah: token1RelativeHeight ?? defaultRelativeHeight,
b: token2, bh: token2RelativeHeight ?? defaultRelativeHeight,
includeNoHeightTerrain,
includeEdges
}], { group, drawForOthers });
}

/**
Expand All @@ -217,7 +214,5 @@ export function drawLineOfSightRaysBetweenTokens(token1, token2, { group = defau
* this. Recommended to use something unique, e.g. `"my-module-name"` or `"my-module-name.group1"`.
*/
export function clearLineOfSightRays({ group = defaultGroupName } = {}) {
/** @type {LineOfSightRulerLayer} */
const ruler = game.canvas.terrainHeightLosRulerLayer;
ruler._clearLineOfSightRays({ group, clearForOthers: true });
LineOfSightRulerLayer.current?._clearLineOfSightRays({ group, clearForOthers: true });
}
5 changes: 0 additions & 5 deletions module/applications/token-line-of-sight-config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,6 @@ export class TokenLineOfSightConfig extends withSubscriptions(Application) {
});
}

/** @type {import("../layers/line-of-sight-ruler-layer.mjs").LineOfSightRulerLayer} */
get #losLayer() {
return canvas.terrainHeightLosRulerLayer;
}

/** Whether or not the user is currently selecting a token. */
get _isSelecting() {
return typeof this.#selectingToken$.value === "number";
Expand Down
8 changes: 4 additions & 4 deletions module/config/controls.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { TerrainErasePalette } from "../applications/terrain-erase-palette.mjs";
import { TerrainPaintPalette } from "../applications/terrain-paint-palette.mjs";
import { TokenLineOfSightConfig } from "../applications/token-line-of-sight-config.mjs";
import { moduleName, settings, tools } from "../consts.mjs";
import { LineOfSightRulerLayer } from "../layers/line-of-sight-ruler-layer.mjs";
import { TerrainHeightLayer } from "../layers/terrain-height-layer.mjs";
import { Signal } from "../utils/signal.mjs";

export const sceneControls = {
Expand Down Expand Up @@ -52,9 +54,7 @@ export function registerSceneControls(controls) {
title: game.i18n.localize("CONTROLS.TerrainHeightToolsTokenLineOfSight"),
icon: "fas fa-compass-drafting",
onClick: () => {
/** @type {import("../layers/line-of-sight-ruler-layer.mjs").LineOfSightRulerLayer} */
const ruler = game.canvas.terrainHeightLosRulerLayer;
ruler._autoSelectTokenLosTargets();
LineOfSightRulerLayer.current?._autoSelectTokenLosTargets();
}
},
sceneControls.terrainHeightToolsLayerToggleControlButton = {
Expand Down Expand Up @@ -108,7 +108,7 @@ export function registerSceneControls(controls) {
onClick: () => Dialog.confirm({
title: game.i18n.localize("TERRAINHEIGHTTOOLS.ClearConfirmTitle"),
content: `<p>${game.i18n.format("TERRAINHEIGHTTOOLS.ClearConfirmContent")}</p>`,
yes: () => game.canvas.terrainHeightLayer?.clear()
yes: () => TerrainHeightLayer.current?.clear()
}),
button: true
}
Expand Down
9 changes: 3 additions & 6 deletions module/config/keybindings.mjs
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import { keybindings, moduleName, settings } from "../consts.mjs";
import { LineOfSightRulerLayer } from "../layers/line-of-sight-ruler-layer.mjs";
import { sceneControls } from "./controls.mjs";

export function registerKeybindings() {
game.keybindings.register(moduleName, keybindings.increaseLosRulerHeight, {
name: "KEYBINDINGS.IncreaseLosRulerHeight",
editable: [{ key: "Equal" }],
onDown: () => {
/** @type {import("../layers/line-of-sight-ruler-layer.mjs").LineOfSightRulerLayer} */
const ruler = canvas.terrainHeightLosRulerLayer;
ruler._handleHeightChangeKeybinding(1);
LineOfSightRulerLayer.current?._handleHeightChangeKeybinding(1);
}
});

game.keybindings.register(moduleName, keybindings.decreaseLosRulerHeight, {
name: "KEYBINDINGS.DecreaseLosRulerHeight",
editable: [{ key: "Minus" }],
onDown: () => {
/** @type {import("../layers/line-of-sight-ruler-layer.mjs").LineOfSightRulerLayer} */
const ruler = canvas.terrainHeightLosRulerLayer;
ruler._handleHeightChangeKeybinding(-1);
LineOfSightRulerLayer.current?._handleHeightChangeKeybinding(-1);
}
});

Expand Down
15 changes: 6 additions & 9 deletions module/config/settings.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TerrainTypesConfig } from "../applications/terrain-types-config.mjs";
import { flags, moduleName, settings, terrainStackViewerDisplayModes, tokenRelativeHeights } from "../consts.mjs";
import { TerrainHeightLayer } from "../layers/terrain-height-layer.mjs";
import { sceneControls } from "./controls.mjs";

export function registerSettings() {
Expand All @@ -21,7 +22,7 @@ export function registerSettings() {
config: false,
onChange: () => {
sceneControls.terrainPaintPalette?.render(false);
game.canvas.terrainHeightLayer._updateGraphics();
TerrainHeightLayer.current?._updateGraphics();
globalThis.terrainHeightTools.ui.terrainStackViewer?.render();
}
});
Expand Down Expand Up @@ -74,9 +75,7 @@ export function registerSettings() {
config: false,
default: true,
onChange: value => {
/** @type {import("../layers/terrain-height-graphics.mjs").TerrainHeightGraphics} */
const graphicsLayer = game.canvas.terrainHeightLayer._graphics;
graphicsLayer.showOnTokenLayer$.value = value;
TerrainHeightLayer.current._graphics.showOnTokenLayer$.value = value;
}
});

Expand Down Expand Up @@ -108,9 +107,7 @@ export function registerSettings() {
config: true,
default: 0,
onChange: value => {
/** @type {import("../layers/terrain-height-graphics.mjs").TerrainHeightGraphics} */
const graphicsLayer = game.canvas.terrainHeightLayer._graphics;
graphicsLayer.maskRadius$.value = value;
TerrainHeightLayer.current._graphics.maskRadius$.value = value;
}
});

Expand Down Expand Up @@ -158,7 +155,7 @@ export function registerSettings() {
type: Boolean,
config: true,
default: true,
onChange: () => game.canvas.terrainHeightLayer._updateGraphics()
onChange: () => TerrainHeightLayer.current?._updateGraphics()
});

game.settings.register(moduleName, settings.smartLabelPlacement, {
Expand All @@ -168,7 +165,7 @@ export function registerSettings() {
type: Boolean,
config: true,
default: true,
onChange: () => game.canvas.terrainHeightLayer._updateGraphics()
onChange: () => TerrainHeightLayer.current?._updateGraphics()
});
}

Expand Down
4 changes: 2 additions & 2 deletions module/hooks/token-elevation.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { moduleName, settings } from "../consts.mjs";
import { TerrainHeightLayer } from "../layers/terrain-height-layer.mjs";
import { getCellsUnderTokenPosition, toSceneUnits } from "../utils/grid-utils.mjs";
import { getTerrainType } from "../utils/terrain-types.mjs";

Expand Down Expand Up @@ -54,8 +55,7 @@ export function handleTokenPreCreation(tokenDoc, _createData, _options, userId)
* @param {boolean} isAltOrientation
*/
function getHighestTerrainUnderToken(position, isAltOrientation) {
/** @type {import("../geometry/height-map.mjs").HeightMap} */
const hm = game.canvas.terrainHeightLayer._heightMap;
const hm = TerrainHeightLayer.current?._heightMap;

let highest = 0;

Expand Down
Loading

0 comments on commit 3579965

Please sign in to comment.