diff --git a/src/index.ts b/src/index.ts index 78b1504d..68f2f0c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,8 +49,7 @@ import { initMapStorage } from "./lib/map-storage.ts"; import { PrefabInspectorHandler } from "./index/prefab-inspector-handler.ts"; import { installPrefabLinkTooltip } from "./lib/prefab-link-tooltip.ts"; -// Fetched once and shared via Promise so multiple handlers awaiting the -// same JSON do not issue duplicate requests. +// WHY: fetched once and shared via Promise so multiple handlers awaiting the same JSON do not issue duplicate requests. const prefabMeshSizes: Promise = fetchJson( "prefab-mesh-sizes.json", ); @@ -83,8 +82,7 @@ function main() { .catch(printError); initMapStorage(); - // Restore stored input values first so downstream init steps and handlers - // observe the restored state instead of HTML defaults. + // WHY: restore stored input values first so downstream init steps and handlers observe the restored state instead of HTML defaults. rememberValue.init(); presetButton.init(); copyButton.init(); @@ -199,8 +197,7 @@ function main() { }, fileHandler, ); - // Never appended to the DOM: the worker draws the sign/marker-free composite - // into it and the terrain viewer samples the placeholder as its texture. + // WHY: never appended to the DOM. The worker draws the sign/marker-free composite into it and the terrain viewer samples the placeholder as its texture. const mapCompositeCanvas = document.createElement("canvas"); new MapCanvasHandler( { @@ -224,8 +221,7 @@ function main() { markerStore, fileHandler, ); - // One controller owns the shared #prefab-tooltip DOM for both the 2D map - // hover and the terrain viewer hover. + // WHY: one controller owns the shared #prefab-tooltip DOM for both the 2D map hover and the terrain viewer hover. const prefabTooltipController = new PrefabTooltipController( component("prefab-tooltip", HTMLElement), labelHandler, diff --git a/src/index/controller-highlight.ts b/src/index/controller-highlight.ts index 2e6e4634..1aae3ad9 100644 --- a/src/index/controller-highlight.ts +++ b/src/index/controller-highlight.ts @@ -1,8 +1,11 @@ export const HIGHLIGHT_CLASS = "reset-target-highlight"; -// Mark each target's closest or
  • ancestor, plus any collapsed -//
    ancestor so the cue stays visible when the section is folded. -// Targets without any of these ancestors (e.g. inside a dialog) are skipped. +/** + * Marks each target's closest `` or `
  • ` ancestor plus any + * collapsed `
    ` ancestor so the cue stays visible when the + * section is folded. Targets without any of these ancestors (e.g. + * inside a dialog) are skipped. + */ export function setHighlightFor( targets: Iterable, on: boolean, diff --git a/src/index/cursor-coords-display-handler.ts b/src/index/cursor-coords-display-handler.ts index eb4e9a14..e5e03182 100644 --- a/src/index/cursor-coords-display-handler.ts +++ b/src/index/cursor-coords-display-handler.ts @@ -11,9 +11,11 @@ interface Doms { const EMPTY = "E/W: -, N/S: -, Elev: -"; -// Sink for cursor events that renders the coords/elevation line. Elevation is -// resolved here (not in CursorHandler) because it is asynchronous and not -// needed by every subscriber. +/** + * Sink for cursor events that renders the coords / elevation line. + * Elevation is resolved here rather than in `CursorHandler` because it + * is asynchronous and not needed by every subscriber. + */ export class CursorCoordsDisplayHandler { #doms: Doms; #dtmHandler: DtmHandler; diff --git a/src/index/cursor-handler.ts b/src/index/cursor-handler.ts index 9c23227f..9da5cec3 100644 --- a/src/index/cursor-handler.ts +++ b/src/index/cursor-handler.ts @@ -11,17 +11,22 @@ interface Doms { } export interface CursorEventMessage { - // null when the cursor left the canvas. + /** `null` when the cursor left the canvas. */ event: MouseEvent | null; - // null when the cursor is outside the map area or when the map size is not - // yet known (no map_info.xml loaded). + /** + * `null` when the cursor is outside the map area, or when the map + * size is not yet known (no `map_info.xml` loaded). + */ coords: GameCoords | null; } -// Streams "what is under the cursor" events to any number of subscribers -// (coords/elevation display, prefab tooltip, …). Holding the single source -// here avoids per-feature `mousemove` listeners and lets the canvas-to-game -// coord conversion happen once per cursor sample. +/** + * Streams "what is under the cursor" events to any number of + * subscribers (coords/elevation display, prefab tooltip, and so on). + * Holding the single source here avoids per-feature `mousemove` + * listeners and lets the canvas-to-game coord conversion happen once + * per cursor sample. + */ export class CursorHandler { #doms: Doms; #dtmHandler: DtmHandler; @@ -46,10 +51,13 @@ export class CursorHandler { }); } - // Source-side debounce keeps the dispatch rate near rAF so that even on a - // fast-moving cursor we don't fire dozens of subscriber chains per frame. - // Subscribers are free to layer their own throttle on top when their work - // (DOM writes, async fetches) is more expensive than the dispatch itself. + /** + * Source-side debounce keeps the dispatch rate near rAF so even a + * fast-moving cursor does not fire dozens of subscriber chains per + * frame. Subscribers can layer their own throttle on top when their + * work (DOM writes, async fetches) is more expensive than the + * dispatch itself. + */ #dispatch = throttledInvoker(() => this.#dispatchImmediately(), 16); async #dispatchImmediately() { diff --git a/src/index/dialog-handler.ts b/src/index/dialog-handler.ts index ce8e2161..1423a5de 100644 --- a/src/index/dialog-handler.ts +++ b/src/index/dialog-handler.ts @@ -57,8 +57,11 @@ export class DialogHandler { const TERMINATED_STATES = ["completed", "skipped"] as const; export class FileProgressionIndicator { - // Direct name ->
  • lookup. Avoids scanning by textContent, which is - // fragile under i18n, whitespace differences, or duplicate task names. + /** + * Direct name-to-`
  • ` lookup. Avoids scanning by `textContent`, + * which is fragile under i18n, whitespace differences, or duplicate + * task names. + */ #liByName = new Map(); #liList: HTMLLIElement[] = []; @@ -68,8 +71,7 @@ export class FileProgressionIndicator { li.textContent = taskName; li.classList.add("processing"); this.#liList.push(li); - // First occurrence wins so setState is deterministic when duplicate - // task names appear. + // INVARIANT: the first occurrence wins so setState is deterministic when duplicate task names appear. if (!this.#liByName.has(taskName)) this.#liByName.set(taskName, li); } } diff --git a/src/index/dnd-handler.ts b/src/index/dnd-handler.ts index 9d2aac76..38afb37c 100644 --- a/src/index/dnd-handler.ts +++ b/src/index/dnd-handler.ts @@ -9,13 +9,14 @@ interface Doms { dragovered: HTMLElement; } -// Tracks nested dragenter/dragleave pairs so we only react to the outermost -// boundary crossings. dragenter/dragleave fire for every child element the -// cursor crosses, so a naive close() on dragleave would fire while the cursor -// is still over the page. By incrementing on enter and decrementing on leave, -// the counter returns to 0 only when the cursor truly leaves the target. The -// previous implementation relied on the clientX/Y === 0 heuristic, which is -// unreliable across browsers and only matched the window's top-left corner. +/** + * Tracks nested dragenter/dragleave pairs so we only react to the + * outermost boundary crossings. `dragenter` and `dragleave` fire for + * every child element the cursor crosses, so a naive `close()` on + * `dragleave` would fire while the cursor is still over the page. + * Incrementing on enter and decrementing on leave returns the counter + * to 0 only when the cursor truly leaves the target. + */ export interface DragCounter { enter(): boolean; leave(): boolean; @@ -71,8 +72,7 @@ export class DndHandler { dom.dragovered.addEventListener("drop", (event) => { if (!event.dataTransfer?.types.includes("Files")) return; event.preventDefault(); - // drop consumes the dragenter without firing a matching dragleave, - // so reset the counter to keep it consistent for the next drag. + // WHY: drop consumes the dragenter without firing a matching dragleave, so reset the counter to keep it consistent for the next drag. counter.reset(); this.#listeners.dispatchNoAwait({ files: Array.from(event.dataTransfer.items).flatMap((item) => diff --git a/src/index/dtm-handler.ts b/src/index/dtm-handler.ts index 6f56a5d1..d480f089 100644 --- a/src/index/dtm-handler.ts +++ b/src/index/dtm-handler.ts @@ -15,9 +15,7 @@ export class DtmHandler { #dtmRaw: CacheHolder; #mapSize = new CacheHolder( () => getHightMapSize(), - () => { - // Do nothing - }, + () => {}, ); #listeners = new events.ListenerManager(); @@ -34,9 +32,7 @@ export class DtmHandler { return null; } }, - () => { - // Do nothing - }, + () => {}, ); fileHandler.addListener(async (fileNames) => { @@ -103,16 +99,17 @@ export class Dtm { return null; } - // In-game coords with left-top offset const x = Math.floor(width / 2) + coords.x; const z = Math.floor(height / 2) + coords.z; const elev = this.#data[x + z * width]; return elev ?? null; } - // `geo` must already be rotated into the XZ ground plane (+Y normal), - // i.e. `geo.rotateX(-Math.PI / 2)` has been applied to a fresh - // PlaneGeometry before calling this. + /** + * `geo` must already be rotated into the XZ ground plane (+Y normal). + * Callers should apply `geo.rotateX(-Math.PI / 2)` to a fresh + * `PlaneGeometry` before calling this. + */ writeY(geo: three.PlaneGeometry) { if (!this.#data) return; @@ -145,25 +142,20 @@ export class Dtm { const maxX = width - 1; const maxZ = height - 1; - // pos.array layout is interleaved (x, y, z) per vertex. Bypassing - // getX/getY/setZ avoids per-call overhead that is significant across - // ~4M vertices. + // WHY: pos.array is interleaved (x, y, z) per vertex. Bypassing getX/getY/setZ avoids per-call overhead that is significant across roughly 4M vertices. const arr = pos.array as Float32Array; for (let i = 0, j = 0; i < pos.count; i++, j += 3) { - // game axis -> webgl axis: game x -> x, game z(row) -> -z, elevation -> y + // WHY: game axis to webgl axis: game x maps to x, game z (row) maps to -z, elevation maps to y. const px = arr[j] as number; const pz = arr[j + 2] as number; let dataX = Math.round((px + halfW) * scaleFactor); let dataZ = Math.round((halfH - pz) * scaleFactor); - // Plane vertices on the +width/+height edge round to width/height, - // one past the last valid DTM index. Clamp to keep sampling in range. + // WHY: plane vertices on the +width or +height edge round to width or height, one past the last valid DTM index. Clamp to keep sampling in range. if (dataX < 0) dataX = 0; else if (dataX > maxX) dataX = maxX; if (dataZ < 0) dataZ = 0; else if (dataZ > maxZ) dataZ = maxZ; - // Elevation byte is in game meters; divide by scaleFactor to convert - // to the geometry's local units (which are already shrunk by the same - // ratio horizontally). + // WHY: elevation byte is in game meters. Divide by scaleFactor to convert to the geometry's local units, which are already shrunk by the same ratio horizontally. // deno-lint-ignore no-non-null-assertion arr[j + 1] = this.#data[dataX + dataZ * width]! / scaleFactor; } diff --git a/src/index/file-handler.ts b/src/index/file-handler.ts index 8d684195..4d843463 100644 --- a/src/index/file-handler.ts +++ b/src/index/file-handler.ts @@ -89,9 +89,7 @@ export class FileHandler { doms.files.addEventListener("change", () => { if (!doms.files.files) return; const files = Array.from(doms.files.files); - // webkitdirectory selections expose the chosen directory via the - // first file's webkitRelativePath ("/"); use it so the - // Map Name row gets the same immediate feedback as drag-and-drop. + // WHY: webkitdirectory selections expose the chosen directory via the first file's webkitRelativePath ("/"). Use it so the Map Name row gets the same immediate feedback as drag-and-drop. const dir = files[0]?.webkitRelativePath.split("/")[0]; if (dir) this.#setMapName(dir); this.#pushFiles(files).catch(printError); @@ -106,7 +104,7 @@ export class FileHandler { this.#setMapName(mapName); await this.#pushUrls( Array.from(MAP_FILE_NAMES).map((name) => `${mapDir}/${name}`), - // Bundled world files are preprocessed. See tools/copy-map-files.ts + // WHY: bundled world files are preprocessed by tools/copy-map-files.ts, so no further preprocessing is needed. true, ); }); @@ -200,7 +198,6 @@ export class FileHandler { throw new Error("Already processing"); } this.#dialogHandler.state = "processing"; - // Prevent opening the dialog because the dialog will be closed immediately. let progression = null; if (resourceList.some((r) => !("remove" in r))) { progression = this.#dialogHandler.createProgression( diff --git a/src/index/map-canvas-handler.ts b/src/index/map-canvas-handler.ts index 7056bd1e..81583385 100644 --- a/src/index/map-canvas-handler.ts +++ b/src/index/map-canvas-handler.ts @@ -6,8 +6,10 @@ import type { Prefab } from "../types/7dtdmap.ts"; interface Doms { canvas: HTMLCanvasElement; - // Off-DOM mirror of the sign/marker-free composite; its placeholder element - // is sampled by the terrain viewer as a texture. + /** + * Off-DOM mirror of the sign/marker-free composite. Its placeholder + * element is sampled by the terrain viewer as a texture. + */ compositeCanvas: HTMLCanvasElement; biomesAlpha: HTMLInputElement; splat3Alpha: HTMLInputElement; diff --git a/src/index/marker-coords-display-handler.ts b/src/index/marker-coords-display-handler.ts index 89f7b28f..bd91cd0b 100644 --- a/src/index/marker-coords-display-handler.ts +++ b/src/index/marker-coords-display-handler.ts @@ -14,9 +14,11 @@ interface Doms { const EMPTY = "E/W: -, N/S: -, Elev: -"; -// Hit index is sourced from PrefabsHandler so cursor hover and flag drop -// agree on "which prefab is here" without each consumer doing its own -// bookkeeping over PrefabMeshSizes. +/** + * Sources the hit index from `PrefabsHandler` so cursor hover and flag + * drop agree on "which prefab is here" without each consumer doing + * its own bookkeeping over `PrefabMeshSizes`. + */ export class MarkerCoordsDisplayHandler { #doms: Doms; #dtmHandler: DtmHandler; diff --git a/src/index/marker-handler.ts b/src/index/marker-handler.ts index e5213abc..6bc65f8d 100644 --- a/src/index/marker-handler.ts +++ b/src/index/marker-handler.ts @@ -10,9 +10,12 @@ interface Doms { resetMarker: HTMLButtonElement; } -// 2D map input adapter: turns canvas clicks/dblclicks, the reset button, and -// file loads into MarkerStore updates. The store owns the coordinate state and -// broadcasts to subscribers; this class only translates DOM events into calls. +/** + * 2D map input adapter. Turns canvas clicks / dblclicks, the reset + * button, and file loads into `MarkerStore` updates. The store owns + * the coordinate state and broadcasts to subscribers, so this class + * only translates DOM events into calls. + */ export class MarkerHandler { constructor( doms: Doms, @@ -29,8 +32,7 @@ export class MarkerHandler { }; doms.canvas.addEventListener("click", (e) => { - // Shift+Click is reserved for prefab page navigation in - // PrefabTooltipHandler; the two click roles are mutually exclusive. + // WHY: Shift+Click is reserved for prefab page navigation in PrefabTooltipHandler. The two click roles are mutually exclusive. if (e.shiftKey) return; setFromEvent(e).catch(printError); }); diff --git a/src/index/marker-jump-handler.ts b/src/index/marker-jump-handler.ts index 293362a7..cf2f7837 100644 --- a/src/index/marker-jump-handler.ts +++ b/src/index/marker-jump-handler.ts @@ -63,8 +63,7 @@ export class MarkerJumpHandler { async #jump(): Promise { const coords = this.#store.coords; if (!coords) return; - // On touch devices the click arrives without a preceding mouseenter, so - // position the highlight ourselves before scrolling to it. + // WHY: on touch devices the click arrives without a preceding mouseenter, so position the highlight ourselves before scrolling to it. await this.#show(coords); if (!this.#highlight.visible) return; const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches; diff --git a/src/index/marker-store.ts b/src/index/marker-store.ts index 808e0bf8..c48b6d40 100644 --- a/src/index/marker-store.ts +++ b/src/index/marker-store.ts @@ -5,9 +5,11 @@ interface EventMessage { coords: GameCoords | null; } -// Domain state for the map's flag marker. Kept DOM-free so both the 2D canvas -// input adapter (MarkerHandler) and other input sources (e.g. the terrain -// viewer's raycast) can drive the same broadcast. +/** + * Domain state for the map's flag marker. Kept DOM-free so both the + * 2D canvas input adapter (`MarkerHandler`) and other input sources + * (e.g. the terrain viewer's raycast) can drive the same broadcast. + */ export class MarkerStore { #coords: GameCoords | null = null; #listeners = new events.ListenerManager(); diff --git a/src/index/prefab-highlight-handler.ts b/src/index/prefab-highlight-handler.ts index c07c426e..f94c4ea3 100644 --- a/src/index/prefab-highlight-handler.ts +++ b/src/index/prefab-highlight-handler.ts @@ -37,8 +37,7 @@ export class PrefabHighlightHandler { this.#meshSizes = meshSizes; this.#highlight = highlight; - // Nested block rows have no data-x, so closest() resolves them to their - // parent prefab row. + // WHY: nested block rows have no data-x, so closest() resolves them to their parent prefab row. doms.list.addEventListener("mouseover", (event) => { const li = coordsLi(event.target); if (li === this.#hoveredLi) return; @@ -61,8 +60,7 @@ export class PrefabHighlightHandler { if (!li) return; this.#jump(li).catch(printError); }); - // A filter update can replace the hovered row without a mouseout; drop - // the highlight instead of leaving it pinned to a stale position. + // WHY: a filter update can replace the hovered row without a mouseout, so drop the highlight instead of leaving it pinned to a stale position. prefabsHandler.addFilterHeaderListener(() => { this.#hoveredLi = null; this.#hide(); @@ -71,7 +69,7 @@ export class PrefabHighlightHandler { async #show(li: HTMLElement): Promise { const rect = await this.#computeRect(li); - // The hover may have moved on during the awaits. + // WHY: the hover may have moved on during the awaits, so re-check identity before applying. if (this.#hoveredLi !== li) return; if (!rect) { this.#hide(); @@ -85,8 +83,7 @@ export class PrefabHighlightHandler { } async #jump(li: HTMLElement): Promise { - // On touch devices the click arrives without a preceding mouseover, so - // position the highlight ourselves before scrolling to it. + // WHY: on touch devices the click arrives without a preceding mouseover, so position the highlight ourselves before scrolling to it. this.#hoveredLi = li; await this.#show(li); if (!this.#highlight.visible) return; diff --git a/src/index/prefab-inspector-handler.ts b/src/index/prefab-inspector-handler.ts index 475122cd..3a7fd747 100644 --- a/src/index/prefab-inspector-handler.ts +++ b/src/index/prefab-inspector-handler.ts @@ -32,13 +32,9 @@ interface Doms { } const EXCLUDE_PREFAB_REGEXPS = [ - // Test prefabs /^(?:aaa_|AAA_|spacercise_|terrain_smoothing_bug)/, - // Tile /^rwg_tile_/, - // Parts /^part_/, - // Located by the other generation methods (e.g. biome, spawn point) or may not be used /^(?:deco_|desert_|departure_bridge_|departure_city_sign|player_start|rock_form|roadblock_|rwg_bridge|sign_|streets?_)/, ]; @@ -93,7 +89,7 @@ export class PrefabInspectorHandler { const newCounts = { inMap: 0, defined: 0 }; for (const name of prefabIndex) { const difficulty = difficulties[name] ?? 0; - // Should rise an error if the difficulty is not in the range of 0-5 + // SAFETY: countsPerDifficulty is built with length 6, so an out-of-range difficulty would raise before this dereference. // deno-lint-ignore no-non-null-assertion const counts = countsPerDifficulty[difficulty]!; counts.defined++; diff --git a/src/index/prefab-tooltip-handler.ts b/src/index/prefab-tooltip-handler.ts index 7f728cca..ae517625 100644 --- a/src/index/prefab-tooltip-handler.ts +++ b/src/index/prefab-tooltip-handler.ts @@ -19,9 +19,12 @@ interface Doms { canvas: HTMLCanvasElement; } -// 2D map side of the shared tooltip: turns cursor coords into a footprint hit -// and a footprint-relative anchor, feeding the shared PrefabTooltipController. -// (The terrain viewer feeds the same controller via TerrainViewerHoverController.) +/** + * 2D map side of the shared tooltip. Turns cursor coords into a + * footprint hit and a footprint-relative anchor, feeding the shared + * `PrefabTooltipController`. The terrain viewer feeds the same + * controller via `TerrainViewerHoverController`. + */ export class PrefabTooltipHandler { #doms: Doms; #controller: PrefabTooltipController; @@ -64,8 +67,7 @@ export class PrefabTooltipHandler { const hit = this.#currentHit; if (!hit) return; e.preventDefault(); - // Shift is not a "background tab" modifier for browsers, so a plain - // window.open() opens the new tab in the foreground. + // WHY: browsers do not treat Shift as a background-tab modifier, so a plain window.open() opens the new tab in the foreground. globalThis.open( `prefabs/${encodeURIComponent(hit.name)}.html`, "_blank", @@ -101,9 +103,12 @@ export class PrefabTooltipHandler { ); } - // Anchor beside the POI's footprint AABB (same game-coords-to-canvas mapping - // as the prefab-list hover highlight) so it stays put while the cursor moves - // within one POI; falls back to the cursor when the map size is unknown. + /** + * Anchors beside the POI's footprint AABB using the same + * game-coords-to-canvas mapping as the prefab-list hover highlight, + * so it stays put while the cursor moves within one POI. Falls back + * to the cursor when the map size is unknown. + */ #anchor( event: MouseEvent, prefab: Prefab, diff --git a/src/index/prefabs-handler.ts b/src/index/prefabs-handler.ts index 21ca9783..fb4fea52 100644 --- a/src/index/prefabs-handler.ts +++ b/src/index/prefabs-handler.ts @@ -85,12 +85,10 @@ export class PrefabsHandler { fileHandler.addListener(async (fileNames) => { if (!fileNames.includes("prefabs.xml")) return; const all = await loadPrefabsXml(); - // Send to the filter worker and notify "all" subscribers without - // waiting on meshSizes: neither path needs the hit index. + // WHY: send to the filter worker and notify "all" subscribers without waiting on meshSizes because neither path needs the hit index. worker.postMessage({ all }); this.#allPrefabsListeners.dispatchNoAwait({ all }); - // Hit index requires mesh sizes; emit on its own channel after the - // await so that subscribers who only want raw `all` are not delayed. + // WHY: the hit index requires mesh sizes. Emit on its own channel after the await so subscribers who only want raw `all` are not delayed. const sizes = await meshSizes; const index = new PrefabHitIndex(all, sizes); this.#hitIndexListeners.dispatchNoAwait({ index }); @@ -105,9 +103,12 @@ export class PrefabsHandler { this.#filterChunkListeners.addListener(fn); } - // Raw load output: emitted once per prefabs.xml load with the full, - // unfiltered prefab list (with rotation). Used by the map renderer to - // draw prefab footprints independently of the active filter. + /** + * Raw load output. Emitted once per `prefabs.xml` load with the + * full, unfiltered prefab list (including rotation). Used by the + * map renderer to draw prefab footprints independently of the + * active filter. + */ addAllPrefabsListener(fn: (m: AllPrefabsEventMessage) => unknown) { this.#allPrefabsListeners.addListener(fn); } diff --git a/src/index/reset-display-settings.ts b/src/index/reset-display-settings.ts index d7723b02..c6fdd56e 100644 --- a/src/index/reset-display-settings.ts +++ b/src/index/reset-display-settings.ts @@ -1,9 +1,12 @@ import { isFormValueElement } from "../lib/dom-utils.ts"; import { bindHoverHighlight } from "./controller-highlight.ts"; -// Explicit whitelist (not "all [data-remember]") so keys representing loaded -// state, like "mapName", are not erased by the reset button. Must stay in -// sync with data-remember values in public/index.html. +/** + * Explicit whitelist rather than every `[data-remember]` so keys + * representing loaded state (like `mapName`) are not erased by the + * reset button. Must stay in sync with `data-remember` values in + * `public/index.html`. + */ const RESET_KEYS: readonly string[] = [ "biomesAlpha", "splat3Alpha", @@ -34,9 +37,7 @@ function resetDisplaySettings(): void { for (const el of querySelectorAllByKey(key)) { if (!isFormValueElement(el)) continue; restoreDefault(el); - // Dispatch before removeItem: the persistence handler re-saves on - // input, so removing first would leave the just-restored default in - // localStorage. + // WHY: dispatch before removeItem so the persistence handler's input listener does not immediately re-save the just-restored default into localStorage. el.dispatchEvent(new Event("input", { bubbles: true })); } localStorage.removeItem(key); diff --git a/src/index/terrain-viewer.ts b/src/index/terrain-viewer.ts index 226a32ce..cfcabdb8 100644 --- a/src/index/terrain-viewer.ts +++ b/src/index/terrain-viewer.ts @@ -46,16 +46,20 @@ interface Doms { footprintAlpha: HTMLInputElement; } -// Base width of the terrain plane in local geometry units. +/** Base width of the terrain plane in local geometry units. */ const TERRAIN_WIDTH = 2048; -// Mesh subdivisions along the horizontal axis. Reduced from 2047 to 1024 -// to cut vertex count by ~4x; the overhead of writeY + -// computeVertexNormals dominates more than the small visual loss for a -// top-down view where texture detail carries most of the perceived -// quality. +/** + * Mesh subdivisions along the horizontal axis. Chosen at 1024 to cut + * vertex count by roughly 4× compared with a per-block 2047: `writeY` + * and `computeVertexNormals` dominate the frame budget, and a top-down + * view carries most of the perceived quality through the texture. + */ const TERRAIN_SEGMENTS = 1024; -// Measured on Pregen06k01: a decoration's y sits ~1 block above the DTM -// surface (signed mean +1.01 over 1590 POIs), so drop it one block to sit flush. +/** + * Measured on Pregen06k01. A decoration's y sits ~1 block above the + * DTM surface (signed mean +1.01 over 1590 POIs), so drop it one block + * to sit flush. + */ const DECORATION_Y_TO_DTM = -1; function prefabGroundGame(prefab: Prefab, yOffset: number): number { @@ -82,8 +86,10 @@ export class TerrainViewer { #boxes: PrefabBoxes | null = null; #hover: TerrainViewerHoverController; #highlight: BoxHighlight | null = null; - // Placement currently drawn as the hover highlight, synced each frame from - // the tooltip handler's raycast result. + /** + * Placement currently drawn as the hover highlight, synced each + * frame from the tooltip handler's raycast result. + */ #highlightedPlacement: BoxPlacement | null = null; #filteredPrefabs: Prefab[] = []; #allPrefabs: Prefab[] = []; @@ -91,11 +97,11 @@ export class TerrainViewer { #densityScores: Promise; #markerCoords: GameCoords | null = null; #markerStore: MarkerStore; - // Cached so that each terrain-build phase does not re-await the same value. + /** Cached so each terrain-build phase does not re-await the same value. */ #mapSize: GameMapSize | null = null; #clickRaycaster = new three.Raycaster(); #clickNdc = new three.Vector2(); - // Guards the trailing click of a camera drag from synthesizing a marker. + /** Guards the trailing click of a camera drag from synthesizing a marker. */ #pointerDownX = 0; #pointerDownY = 0; #pointerDragged = false; @@ -150,19 +156,19 @@ export class TerrainViewer { canvas: doms.output, antialias: false, }); - this.#renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); // cap DPR for performance + // WHY: cap DPR at 2 so downstream fill cost stays bounded on hidpi displays. + this.#renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); this.#scene = new three.Scene(); - // Neutral background shows through transparent texture pixels instead of - // the renderer's default black clear color. + // WHY: a neutral background shows through transparent texture pixels instead of the renderer's default black clear colour. this.#scene.background = new three.Color("#111111"); - const light = new three.DirectionalLight(0xffffff, 5); // bright key light to balance the dim ambient - // Only the terrain mesh is rotated -90° around X (see #updateElevations) - // to become Y-up; rotate this fixed light position the same way so it - // keeps the same angle relative to the terrain surface as before. + // WHY: bright key light balances the dim ambient below. + const light = new three.DirectionalLight(0xffffff, 5); + // WHY: the terrain mesh is rotated -90° around X in #updateElevations to become Y-up. Rotate this fixed light position the same way so it keeps the same angle relative to the terrain surface. light.position.set(1, 1, -1); this.#scene.add(light); - this.#scene.add(new three.AmbientLight(0xffffff, 0.09)); // low fill to keep terrain contrast + // WHY: low ambient fill keeps terrain contrast without crushing shadows. + this.#scene.add(new three.AmbientLight(0xffffff, 0.09)); this.#cameraController = new TerrainViewerCameraController( doms.output, @@ -178,9 +184,7 @@ export class TerrainViewer { doms.show.addEventListener("click", () => { this.#show().catch(printError); }); - // The native close event fires for Esc key, programmatic close(), - // or form submit. Use it as the single teardown point so the render loop - // stops regardless of how the dialog was dismissed. + // WHY: the native close event fires for Esc, programmatic close(), and form submit. Use it as the single teardown point so the render loop stops regardless of how the dialog was dismissed. doms.dialog.addEventListener("close", () => { this.#stopRender(); this.#disposeTerrain(); @@ -188,11 +192,7 @@ export class TerrainViewer { this.#disposeFlag(); this.#disposeBoxes(); }); - // Clicking inside the HUD (e.g. the Show/Hide Help checkbox) would - // otherwise move focus off the canvas and break keyboard camera - // controls until the user clicks back. Suppress the default focus - // shift on mousedown so the canvas keeps focus while the click event - // still toggles the control. + // WHY: clicking inside the HUD would otherwise move focus off the canvas and break keyboard camera controls. Suppress the default focus shift on mousedown so the canvas keeps focus while the click still toggles the control. doms.hud.addEventListener("mousedown", (event) => { event.preventDefault(); }); @@ -213,8 +213,7 @@ export class TerrainViewer { const placement = this.#hover.hoveredPlacement; if (!placement) return; event.preventDefault(); - // Shift is not a "background tab" modifier for browsers, so a plain - // window.open() opens the new tab in the foreground. + // WHY: browsers do not treat Shift as a background-tab modifier, so a plain window.open() opens the new tab in the foreground. globalThis.open( `prefabs/${encodeURIComponent(placement.prefab.name)}.html`, "_blank", @@ -265,10 +264,7 @@ export class TerrainViewer { ); console.time("updateElevations"); - // LOD is not implemented because it would require tiled meshes with - // distance-based switching and seam stitching. Halving subdivision - // provides the majority of the performance benefit for this top-down - // viewer with negligible visual loss. + // WHY: full LOD would require tiled meshes with distance-based switching and seam stitching. Halving subdivision provides most of the performance benefit for this top-down viewer with negligible visual loss. const segmentW = TERRAIN_SEGMENTS; const segmentH = Math.max( 1, @@ -282,8 +278,7 @@ export class TerrainViewer { segmentW, segmentH, ); - // Default PlaneGeometry lies in the XY plane (+Z normal); rotate into - // the XZ ground plane (+Y normal) to match three.js's Y-up convention. + // WHY: default PlaneGeometry lies in the XY plane with +Z normal. Rotate into the XZ ground plane with +Y normal to match three.js's Y-up convention. geo.rotateX(-Math.PI / 2); await this.#dtm.writeY(geo); geo.computeBoundingSphere(); @@ -308,8 +303,7 @@ export class TerrainViewer { const spriteScale = this.#spriteScale(); const meshSizes = await this.#meshSizes; const placements = this.#filteredPrefabs.map((prefab) => { - // Same footprint-centre shift as the 2D sign renderer: decoration - // positions are the SW corner of the rotated AABB. + // WHY: decoration positions are the SW corner of the rotated AABB, so shift by half-extents to the footprint centre. Same shift as the 2D sign renderer. const size = meshSizes[prefab.name]; const odd = ((prefab.rotation ?? 0) & 1) === 1; const halfW = size ? (odd ? size[1] : size[0]) / 2 : 0; @@ -332,9 +326,12 @@ export class TerrainViewer { if (this.#signSprites) this.#scene.add(this.#signSprites.object); } - // Cheap on its own; kept separate from #updateSigns so a marker change does - // not force a resample of every sign (which would refetch the DTM raw after - // its CacheHolder expires and briefly wipe every sign sprite). + /** + * Cheap on its own. Kept separate from `#updateSigns` so a marker + * change does not force a resample of every sign, which would + * refetch the DTM raw after its `CacheHolder` expires and briefly + * wipe every sign sprite. + */ async #updateFlag(mapSize: GameMapSize): Promise { this.#disposeFlag(); if (!this.#markerCoords) return; @@ -357,8 +354,7 @@ export class TerrainViewer { #spriteScale(): number { const glyphPx = this.#doms.signSize.valueAsNumber; const fovRad = (this.#cameraController.camera.fov * Math.PI) / 180; - // World scale s spans s / (2 * tan(fov / 2)) of the viewport height with - // sizeAttenuation off; the sprite canvas is twice the glyph box. + // WHY: with sizeAttenuation off, world scale s spans s / (2 * tan(fov / 2)) of the viewport height. The sprite canvas is twice the glyph box, so the factor of 4 combines both. return (4 * glyphPx * Math.tan(fovRad / 2)) / document.documentElement.clientHeight; } @@ -373,13 +369,15 @@ export class TerrainViewer { return { x: x / scaleFactor, y: elevation / scaleFactor, - // Game z (north positive) maps to -z in the Y-up terrain space. + // WHY: game z is north-positive, but the Y-up terrain space is south-positive, so negate. z: -z / scaleFactor, }; } - // DTM elevation (game meters) at a footprint centre, clamped to the index - // range since edge prefabs can round one block past it. + /** + * DTM elevation (game meters) at a footprint centre. Clamped to the + * index range because edge prefabs can round one block past it. + */ async #sampleGroundGame( x: number, z: number, @@ -395,8 +393,7 @@ export class TerrainViewer { async #updateBoxes(mapSize: GameMapSize): Promise { this.#disposeBoxes(); - // Boxes cover every decoration regardless of the prefab filter (which only - // drives the ✘ signs); the footprint-alpha slider is their on/off switch. + // WHY: boxes cover every decoration regardless of the prefab filter (which only drives the ✘ signs). The footprint-alpha slider is their on/off switch. const footprintAlpha = this.#doms.footprintAlpha.valueAsNumber; if (footprintAlpha <= 0 || this.#allPrefabs.length === 0) return; @@ -487,7 +484,7 @@ export class TerrainViewer { if (!hit) return; const scaleFactor = (this.#mapSize.width - 1) / this.#terrainSize.width; const gx = Math.round(hit.point.x * scaleFactor); - // Game z (north positive) is the negated world z. + // WHY: game z is north-positive, opposite of world z, so negate. const gz = Math.round(-hit.point.z * scaleFactor); const halfW = Math.floor(this.#mapSize.width / 2); const halfH = Math.floor(this.#mapSize.height / 2); @@ -506,8 +503,11 @@ export class TerrainViewer { this.#boxes = null; } - // Mirror the tooltip handler's hovered box as the two-part highlight; the - // handler owns hit detection, this owns the scene-facing visual. + /** + * Mirrors the tooltip handler's hovered box as the two-part + * highlight. The handler owns hit detection, this owns the + * scene-facing visual. + */ #syncHighlight(): void { const placement = this.#hover.hoveredPlacement; if (placement === this.#highlightedPlacement) return; @@ -539,9 +539,11 @@ export class TerrainViewer { this.#flagSprite = null; } - // three.js does not free GPU resources on GC; geometry, materials and - // textures must be disposed explicitly or VRAM accumulates each time the - // viewer is reopened with a different DTM. + /** + * three.js does not free GPU resources on GC. Geometry, materials, + * and textures must be disposed explicitly, or VRAM accumulates each + * time the viewer is reopened with a different DTM. + */ #disposeTerrain(): void { if (!this.#terrain) return; this.#scene.remove(this.#terrain); @@ -571,11 +573,7 @@ export class TerrainViewer { this.#syncHighlight(); this.#renderer.render(this.#scene, this.#cameraController.camera); }; - // Seed prev == current via rAF so the first frame's delta is 0. - // Calling r(0, 0) directly would feed a zero prevTime into the next - // rAF's high-res currentTime, producing a multi-second delta that - // snaps the camera if a movement key is already held when the dialog - // opens. + // WHY: seed prev == current via rAF so the first frame's delta is zero. Calling r(0, 0) directly would feed a zero prevTime into the next rAF's high-res currentTime, producing a multi-second delta that snaps the camera if a movement key is already held when the dialog opens. this.#animationRequestId = requestAnimationFrame((t) => { r(t, t); }); @@ -588,9 +586,11 @@ export class TerrainViewer { } } - // Drives the existing Show/Hide Help checkbox; its inline oninput - // handles updating the op-desc visibility, so we just synthesise a - // click. Bound to the "?" key by the camera controller. + /** + * Drives the existing Show/Hide Help checkbox. Its inline `oninput` + * updates the op-desc visibility, so this just synthesises a click. + * Bound to the "?" key by the camera controller. + */ #toggleHelp() { this.#doms.helpToggle.click(); } diff --git a/src/index/terrain-viewer/camera-controller.ts b/src/index/terrain-viewer/camera-controller.ts index 24b7503d..2961ac2b 100644 --- a/src/index/terrain-viewer/camera-controller.ts +++ b/src/index/terrain-viewer/camera-controller.ts @@ -10,16 +10,21 @@ const MOUSE_BUTTON_BITMASK = { const GROUND_PLANE = new three.Plane(new three.Vector3(0, 1, 0), 0); const TILT_AXIS = new three.Vector3(1, 0, 0); const TILT_REFERENCE = new three.Vector3(0, 0, 1); -const TILT_MAX_RAD = Math.PI / 2; // 90° -const TILT_MIN_RAD = Math.PI / 6; // 30° +/** 90° in radians. */ +const TILT_MAX_RAD = Math.PI / 2; +/** 30° in radians. */ +const TILT_MIN_RAD = Math.PI / 6; const MAX_ELEV = 255; -// Speed multiplier applied to horizontal pan while Shift is held. +/** Speed multiplier applied to horizontal pan while Shift is held. */ const PAN_BOOST_MULTIPLIER = 4; -// Approximate pixel-equivalent multipliers for the non-pixel wheel delta modes. -// Browsers report line/page units when a discrete wheel is used; converting to a -// pixel-like scale keeps zoom speed consistent across input devices. +/** + * Approximate pixel-equivalent multipliers for the non-pixel wheel + * delta modes. Browsers report line and page units when a discrete + * wheel is used, so converting to a pixel-like scale keeps zoom speed + * consistent across input devices. + */ const WHEEL_LINE_HEIGHT_PX = 16; const WHEEL_PAGE_HEIGHT_PX = 800; @@ -34,9 +39,11 @@ function normalizeWheelDelta(event: WheelEvent): number { } } -// Keyboard action set. Movement actions translate to a signed axis speed; -// "toggle-help" is one-shot. Extracted so the keymap can be unit-tested -// independently of the controller and the DOM. +/** + * Keyboard action set. Movement actions translate to a signed axis + * speed. `"toggle-help"` is one-shot. Extracted so the keymap can be + * unit-tested independently of the controller and the DOM. + */ export type CameraKeyAction = | "pan-left" | "pan-right" @@ -58,8 +65,7 @@ interface KeyEventLike { } export function mapCameraKey(event: KeyEventLike): CameraKeyAction { - // Defer to the browser when a modifier is held so we do not steal - // shortcuts like Ctrl+R (reload) or Cmd+Left (history back). + // WHY: defer to the browser when a modifier is held so we do not steal shortcuts like Ctrl+R (reload) or Cmd+Left (history back). if (event.ctrlKey || event.altKey || event.metaKey) return null; switch (event.code) { case "KeyA": @@ -111,7 +117,7 @@ export class TerrainViewerCameraController { wheel: 0, }; - // Reusable temporary vectors / ray to avoid per-frame heap allocation. + /** Reusable temporary vectors and ray to avoid per-frame heap allocation. */ #cameraWork = { direction: new three.Vector3(), move: new three.Vector3(), @@ -196,10 +202,7 @@ export class TerrainViewerCameraController { this.#minY = (MAX_ELEV * terrainSize.width) / mapWidth; this.#maxY = terrainSize.height * 1.2; - // Keep the far/near ratio bounded so the depth buffer retains precision - // at the terrain's far edge. The default near of 0.1 paired with a far - // proportional to terrain size produces a ratio of ~40000, which causes - // Z-fighting on distant geometry. + // WHY: keep the far/near ratio bounded so the depth buffer retains precision at the terrain's far edge. The default near of 0.1 paired with a far proportional to terrain size produces a ratio around 40000, which causes Z-fighting on distant geometry. this.camera.near = 1; this.camera.far = this.#terrainSize.height * 2; this.camera.updateProjectionMatrix(); @@ -268,9 +271,7 @@ export class TerrainViewerCameraController { this.#mouseMove.left.y = 0; if (deltaX !== 0) this.camera.position.x += deltaX; - // Z is the negated counterpart of the pre-migration ground Y axis - // (the -90° X rotation that put the plane in Y-up maps old +Y to - // new -Z), so panning subtracts here where it used to add. + // WHY: Z is the negated counterpart of the pre-migration ground Y axis. The -90° X rotation that put the plane in Y-up maps old +Y to new -Z, so panning subtracts here where it used to add. if (deltaZ !== 0) this.camera.position.z -= deltaZ; this.#syncCameraWork(); @@ -296,8 +297,7 @@ export class TerrainViewerCameraController { const wheelDelta = (this.#mouseMove.wheel * this.#terrainSize.width) / -5000; this.#mouseMove.wheel = 0; - // Keyboard zoom advances roughly 30% of the terrain width per second at - // speed 1; matches the perceived feel of the mouse wheel at moderate use. + // WHY: keyboard zoom advances roughly 30% of the terrain width per second at speed 1 to match the perceived feel of the mouse wheel at moderate use. const keyDelta = (this.#speeds.zoom * this.#terrainSize.width * 0.3 * deltaMsec) / 1000; const moveDelta = wheelDelta + keyDelta; @@ -315,11 +315,10 @@ export class TerrainViewerCameraController { #tiltCamera(deltaMsec: number) { if (this.#mouseMove.center.y === 0 && this.#speeds.tilt === 0) return; - // PI rad = 180° - // -(PI/2) rad / 1000 pixels by mouse + // WHY: mouse tilt uses -(PI/2) rad per 1000 pixels of vertical movement (about 90° per drag height). const deltaRadMouse = this.#mouseMove.center.y * (-(Math.PI / 2) / 1000); - // PI/4 rad/sec by keypress + // WHY: keypress tilt uses PI/4 rad per second (about 45°/s at speed 1). const deltaRadKey = (((this.#speeds.tilt * Math.PI) / 4) * deltaMsec) / 1000; diff --git a/src/index/terrain-viewer/hover-controller.ts b/src/index/terrain-viewer/hover-controller.ts index 712716bc..00350122 100644 --- a/src/index/terrain-viewer/hover-controller.ts +++ b/src/index/terrain-viewer/hover-controller.ts @@ -6,10 +6,13 @@ import { } from "../../lib/prefab-tooltip.ts"; import { printError } from "../../lib/utils.ts"; -// TerrainViewer's hover sub-controller (peer of TerrainViewerCameraController): -// raycasts the pointer against the footprint boxes and projects a hovered box -// to an anchor, feeding the shared PrefabTooltipController. The viewer ticks -// update() each frame and reads hoveredPlacement to drive its box highlight. +/** + * `TerrainViewer`'s hover sub-controller (peer of + * `TerrainViewerCameraController`). Raycasts the pointer against the + * footprint boxes and projects a hovered box to an anchor, feeding + * the shared `PrefabTooltipController`. The viewer ticks `update()` + * each frame and reads `hoveredPlacement` to drive its box highlight. + */ export class TerrainViewerHoverController { #controller: PrefabTooltipController; #camera: three.Camera; @@ -32,8 +35,7 @@ export class TerrainViewerHoverController { this.#camera = camera; this.#controller = controller; - // The raycast runs once per frame via update(); these listeners only track - // pointer state so a held button (camera drag) can suppress it. + // WHY: the raycast runs once per frame via update(). These listeners only track pointer state so a held button (camera drag) can suppress it. canvas.addEventListener("pointermove", (event) => { const rect = canvas.getBoundingClientRect(); this.#pointerNdc.set( @@ -61,7 +63,7 @@ export class TerrainViewerHoverController { return this.#boxes.placements[this.#hoverInstanceId] ?? null; } - // Called on each box rebuild (and with null on teardown); resets the hover. + /** Called on each box rebuild, and with `null` on teardown. Resets the hover. */ setBoxes(boxes: PrefabBoxes | null): void { this.#boxes = boxes; this.#setHover(null); @@ -82,8 +84,7 @@ export class TerrainViewerHoverController { this.#controller.hide(); return; } - // The controller's token guard drops this if the hover changes (or hides) - // before the content lookup resolves, so no per-instance recheck is needed. + // WHY: the controller's token guard drops this if the hover changes or hides before the content lookup resolves, so no per-instance recheck is needed. this.#controller.showFor( placement.prefab, ["click", "dblclick", "shift-click"], @@ -91,8 +92,11 @@ export class TerrainViewerHoverController { ).catch(printError); } - // Anchor to the right of the hovered box by projecting its 8 corners; falls - // back to the cursor when every corner is behind the camera (off-screen). + /** + * Anchors to the right of the hovered box by projecting its 8 + * corners. Falls back to the cursor when every corner is behind the + * camera (off-screen). + */ #anchor(p: BoxPlacement): { left: number; top: number } { const rect = this.#canvas.getBoundingClientRect(); const v = new three.Vector3(); diff --git a/src/index/terrain-viewer/marker-sprites.ts b/src/index/terrain-viewer/marker-sprites.ts index 47cdcfa8..dbb6d50c 100644 --- a/src/index/terrain-viewer/marker-sprites.ts +++ b/src/index/terrain-viewer/marker-sprites.ts @@ -7,11 +7,13 @@ import { GLYPH_MARKER_VIEWPORT, } from "../../../lib/glyph-marker.ts"; -// Baked glyph resolution, independent of the on-screen size (the largest -// reachable screen size is 60px, so this stays oversampled). +/** + * Baked glyph resolution, independent of on-screen size. The largest + * reachable screen size is 60px, so this stays oversampled. + */ const GLYPH_TEXTURE_PX = 128; -// Position in the terrain mesh's local (three.js) coordinate space. +/** Position in the terrain mesh's local (three.js) coordinate space. */ export interface MarkerPlacement { x: number; y: number; @@ -23,8 +25,10 @@ export interface MarkerSprites { dispose(): void; } -// `spriteScale` is the world scale that yields the desired constant -// on-screen size with `sizeAttenuation: false`. +/** + * `spriteScale` is the world scale that yields the desired constant + * on-screen size with `sizeAttenuation: false`. + */ export function buildSignSprites(opts: { marker: GlyphMarker; placements: MarkerPlacement[]; @@ -52,8 +56,7 @@ export function buildFlagSprite(opts: { spriteScale: number; }): MarkerSprites { const { material, center, disposables } = glyphMaterial(opts.marker, 1); - // Higher renderOrder than signs (2) so the flag paints last regardless of - // the transparent pass's distance sort (all sprites share depthTest: false). + // WHY: renderOrder 3 exceeds signs (2) so the flag paints last regardless of the transparent pass's distance sort, since all sprites share depthTest: false. const sprite = makeSprite( material, center, @@ -84,8 +87,7 @@ function glyphMaterial( transparent: true, opacity, sizeAttenuation: false, - // Markers show through terrain by design: with depth testing, glyph - // pixels away from the anchor row get culled by nearer terrain. + // WHY: markers show through terrain by design. With depth testing, glyph pixels away from the anchor row get culled by nearer terrain. depthTest: false, depthWrite: false, }); @@ -96,8 +98,11 @@ function glyphMaterial( }; } -// Convert a glyph anchor (a point in the shared VIEWPORT square) into -// Sprite.center units: [0, 1] with (0, 0) at the sprite's bottom-left. +/** + * Converts a glyph anchor (a point in the shared `VIEWPORT` square) + * into `Sprite.center` units, `[0, 1]` with `(0, 0)` at the sprite's + * bottom-left. + */ function anchorCenter(anchor: GlyphMarker["anchor"]): three.Vector2 { return new three.Vector2( 0.5 + (anchor.x - GLYPH_MARKER_VIEWPORT / 2) / (2 * GLYPH_MARKER_FONT_SIZE), @@ -116,8 +121,7 @@ function makeSprite( sprite.center.copy(center); sprite.scale.set(scale, scale, 1); sprite.position.set(placement.x, placement.y, placement.z); - // Above terrain, boxes (0) and highlight (1); the transparent pass's distance - // sort could otherwise paint those over these non-depth-writing sprites. + // WHY: renderOrder above terrain, boxes (0), and highlight (1). Without it, the transparent pass's distance sort could paint those over these non-depth-writing sprites. sprite.renderOrder = renderOrder; return sprite; } diff --git a/src/index/terrain-viewer/prefab-boxes.ts b/src/index/terrain-viewer/prefab-boxes.ts index 8f1d0f0c..7fc1afd0 100644 --- a/src/index/terrain-viewer/prefab-boxes.ts +++ b/src/index/terrain-viewer/prefab-boxes.ts @@ -2,8 +2,11 @@ import type { Prefab } from "../../types/7dtdmap.ts"; import * as three from "three"; -// A footprint box in the terrain's local space; 90°/270° rotations fold into -// width/depth. groundY splits it into above-ground and buried highlight slabs. +/** + * A footprint box in the terrain's local space. Rotations of 90° or + * 270° fold into width and depth. `groundY` splits it into + * above-ground and buried highlight slabs. + */ export interface BoxPlacement { prefab: Prefab; centerX: number; @@ -18,13 +21,13 @@ export interface BoxPlacement { export interface PrefabBoxes { object: three.Object3D; - // Exposed for the hover raycaster; instanceId indexes `placements`. + /** Exposed for the hover raycaster. `instanceId` indexes `placements`. */ mesh: three.InstancedMesh; placements: BoxPlacement[]; dispose(): void; } -// Amber buried-part highlight, distinct from the brightened above-ground tint. +/** Amber buried-part highlight, distinct from the brightened above-ground tint. */ const BURIED_HIGHLIGHT = new three.Color("#ffb300"); export function buildPrefabBoxes( @@ -69,8 +72,11 @@ export interface BoxHighlight { dispose(): void; } -// Two depth-test-off slabs so the highlight reads through terrain: a brightened -// above-ground slab and, when the prefab is buried, an amber below-ground slab. +/** + * Two depth-test-off slabs so the highlight reads through terrain: a + * brightened above-ground slab, plus an amber below-ground slab when + * the prefab is buried. + */ export function buildBoxHighlight(placement: BoxPlacement): BoxHighlight { const group = new three.Group(); const disposables: { dispose(): void }[] = []; diff --git a/tools/lint-plugins/require-comment-rationale.ts b/tools/lint-plugins/require-comment-rationale.ts index 7b4afe14..083ec3ba 100644 --- a/tools/lint-plugins/require-comment-rationale.ts +++ b/tools/lint-plugins/require-comment-rationale.ts @@ -21,29 +21,6 @@ const PRESERVE_PATTERNS: readonly string[] = [ is not audited in one big sweep; shrink to empty, then delete this mechanism. Match is POSIX-style substring on the absolute filename. */ const EXCLUDED_PATHS: readonly string[] = [ - "/src/index.ts", - "/src/index/controller-highlight.ts", - "/src/index/cursor-coords-display-handler.ts", - "/src/index/cursor-handler.ts", - "/src/index/dialog-handler.ts", - "/src/index/dnd-handler.ts", - "/src/index/dtm-handler.ts", - "/src/index/file-handler.ts", - "/src/index/map-canvas-handler.ts", - "/src/index/marker-coords-display-handler.ts", - "/src/index/marker-handler.ts", - "/src/index/marker-jump-handler.ts", - "/src/index/marker-store.ts", - "/src/index/prefab-highlight-handler.ts", - "/src/index/prefab-inspector-handler.ts", - "/src/index/prefab-tooltip-handler.ts", - "/src/index/prefabs-handler.ts", - "/src/index/reset-display-settings.ts", - "/src/index/terrain-viewer.ts", - "/src/index/terrain-viewer/camera-controller.ts", - "/src/index/terrain-viewer/hover-controller.ts", - "/src/index/terrain-viewer/marker-sprites.ts", - "/src/index/terrain-viewer/prefab-boxes.ts", "/e2e/index.spec.ts", "/e2e/prefabs.spec.ts", "/playwright.config.ts",