From 4b3ed62da5fd0c98fd497982277662e233277aa5 Mon Sep 17 00:00:00 2001 From: Keiichiro Ui Date: Sun, 12 Jul 2026 15:15:36 +0900 Subject: [PATCH] Audit src/lib/, src/types/, and src/prefabs/main.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 for #225. Eighteen files, 121 flagged comments. Declaration-level rationale moved to JSDoc across the types module (all Prefab / HighlightedPrefab / mesh / district / glyph shapes), data lookup libraries (prefab-tooltip, prefab-hit-index, labels, footprint-color, utils, prefabs, prefab-added-versions), and UI helpers (url-state, prefab-link-tooltip, prefabs-filter-controls, glyph-sprite). In-body rationale kept as single-clause WHY: / INVARIANT: markers: - prefab-tooltip: popover top-layer contract, global Shift tracking. - prefab-hit-index: rotation-swap of world-aligned width and depth. - footprint-color: lattice-origin derivation from any tile. - utils: content-encoding length-check condition. - prefabs: malformed-position fail-fast. - storage: pipeTo close-once INVARIANT; NotFoundError idempotency. - throttled-invoker: pending-flag coalescing INVARIANT. - url-state: input+change dual dispatch rationales. - dom-utils, glyph-sprite: rotation-swap and scale rationale. - label-handler: synchronous initial-language commit. - oneshot-worker: preventDefault suppresses double-logging. Deleted where variable names or idiom already convey the intent: - dom-utils "in-game scale" / "center offset" section labels. - prefabs/main.ts "// init" above the IIFE. Style: single-clause sentences using because / so / since for causation, no ; / — / colon-elaboration per the #235-#236 memo. EXCLUDED_PATHS shrinks from 48 to 30. --- src/lib/dom-utils.ts | 5 +- src/lib/footprint-color.ts | 20 ++++--- src/lib/glyph-sprite.ts | 10 ++-- src/lib/label-handler.ts | 5 +- src/lib/labels.ts | 24 ++++---- src/lib/oneshot-worker.ts | 3 +- src/lib/prefab-added-versions.ts | 15 +++-- src/lib/prefab-hit-index.ts | 26 +++++---- src/lib/prefab-link-tooltip.ts | 11 ++-- src/lib/prefab-tooltip.ts | 55 ++++++++++++------- src/lib/prefabs-filter-controls.ts | 11 ++-- src/lib/prefabs.ts | 12 ++-- src/lib/storage.ts | 6 +- src/lib/throttled-invoker.ts | 3 +- src/lib/url-state.ts | 10 +--- src/lib/utils.ts | 20 ++++--- src/prefabs/main.ts | 1 - src/types/7dtdmap.ts | 54 ++++++++++++------ .../lint-plugins/require-comment-rationale.ts | 18 ------ 19 files changed, 170 insertions(+), 139 deletions(-) diff --git a/src/lib/dom-utils.ts b/src/lib/dom-utils.ts index 832906a46..a5807ba58 100644 --- a/src/lib/dom-utils.ts +++ b/src/lib/dom-utils.ts @@ -101,8 +101,7 @@ export function prefabFootprintCssRect( }; } - // 90°/270° rotations swap the world-aligned width/depth; same formula as - // the footprint pass in src/worker/lib/map-renderer.ts. + // WHY: rotations by 90° or 270° swap the world-aligned width and depth. const odd = ((prefab.rotation ?? 0) & 1) === 1; let width = (odd ? size[1] : size[0]) * scaleX; let height = (odd ? size[0] : size[1]) * scaleY; @@ -125,14 +124,12 @@ export function canvasEventToGameCoords( mapSize: GameMapSize, canvasSize: HTMLCanvasElement, ): GameCoords | null { - // in-game scale coords with left-top offset const gx = (event.offsetX * mapSize.width) / canvasSize.width; const gz = (event.offsetY * mapSize.height) / canvasSize.height; if (gx < 0 || gx >= mapSize.width || gz < 0 || gz >= mapSize.height) { return null; } - // in-game coords (center offset) const x = gx - Math.floor(mapSize.width / 2); const z = Math.floor(mapSize.height / 2) - gz; return gameCoords({ x: Math.round(x), z: Math.round(z) }); diff --git a/src/lib/footprint-color.ts b/src/lib/footprint-color.ts index f797c40a2..e44721588 100644 --- a/src/lib/footprint-color.ts +++ b/src/lib/footprint-color.ts @@ -4,18 +4,20 @@ import type { PrefabDensityScores, } from "../types/7dtdmap.ts"; -// Edge length of an `rwg_tile_*` prefab in game blocks. +/** Edge length of an `rwg_tile_*` prefab in game blocks. */ const TILE_SIZE = 150; export interface TileIndex { offsetX: number; offsetZ: number; - // Key: `${gridX},${gridZ}` → district name (extracted from tile filename). + /** Key `${gridX},${gridZ}` maps to a district name extracted from the tile filename. */ cells: Map; } -// Tile prefabs are 150×150 `rwg_tile__*` blocks on a regular lattice; -// index each by grid cell for O(1) per-POI district lookups. +/** + * Tile prefabs are 150×150 `rwg_tile__*` blocks on a regular + * lattice. Indexes each by grid cell for O(1) per-POI district lookups. + */ export function buildTileIndex(allPrefabs: Prefab[]): TileIndex { const tiles: { x: number; z: number; district: string }[] = []; for (const p of allPrefabs) { @@ -24,8 +26,7 @@ export function buildTileIndex(allPrefabs: Prefab[]): TileIndex { // deno-lint-ignore no-non-null-assertion tiles.push({ x: p.x, z: p.z, district: m[1]! }); } - // The lattice origin is the world-centre offset, not a multiple of 150 in - // world coords. Derive it from any tile (mod 150 normalised to [0, 150)). + // WHY: the lattice origin is the world-centre offset, not a multiple of 150 in world coords. Derive it from any tile (mod 150 normalised to [0, 150)). const mod = (n: number) => ((n % TILE_SIZE) + TILE_SIZE) % TILE_SIZE; // deno-lint-ignore no-non-null-assertion const offsetX = tiles.length > 0 ? mod(tiles[0]!.x) : 0; @@ -40,8 +41,11 @@ export function buildTileIndex(allPrefabs: Prefab[]): TileIndex { return { offsetX, offsetZ, cells }; } -// Mirrors WorldGenerationEngineFinal.StreetTile.SpawnMarkerPartsAndPrefabs: -// preview_color × (remnant/abandoned 0.75, low-density 0.4), trader #994d4d, wilderness white. +/** + * Mirrors `WorldGenerationEngineFinal.StreetTile.SpawnMarkerPartsAndPrefabs`. + * Returns `preview_color × (remnant/abandoned 0.75, low-density 0.4)`, + * trader `#994d4d`, wilderness white. + */ export function footprintColorRgb( prefab: Prefab, tileIndex: TileIndex, diff --git a/src/lib/glyph-sprite.ts b/src/lib/glyph-sprite.ts index 6504ef128..6d813436a 100644 --- a/src/lib/glyph-sprite.ts +++ b/src/lib/glyph-sprite.ts @@ -4,8 +4,11 @@ import { GLYPH_MARKER_VIEWPORT, } from "../../lib/glyph-marker.ts"; -// Stamps a glyph path baked by tools/generate-glyph-markers.ts into a square -// sprite twice the target pixel size, matching the baked SVGs' layered look. +/** + * Stamps a glyph path baked by `tools/generate-glyph-markers.ts` into + * a square sprite twice the target pixel size, matching the baked + * SVGs' layered look. + */ export function buildGlyphSprite( path2D: Path2D, pixelSize: number, @@ -14,8 +17,7 @@ export function buildGlyphSprite( const sprite = new OffscreenCanvas(spriteSize, spriteSize); const sc = sprite.getContext("2d"); if (sc) { - // Scale the baked path's viewport to `pixelSize`, then centre it on the - // sprite; the path itself is built centred on VIEWPORT/2 at build time. + // WHY: scale the baked path's viewport to pixelSize, then centre it on the sprite. The path itself is built centred on VIEWPORT/2 at build time. const k = pixelSize / GLYPH_MARKER_FONT_SIZE; sc.lineJoin = "round"; sc.lineCap = "round"; diff --git a/src/lib/label-handler.ts b/src/lib/label-handler.ts index 84ace224e..8bda303e1 100644 --- a/src/lib/label-handler.ts +++ b/src/lib/label-handler.ts @@ -22,10 +22,7 @@ export class LabelHandler { this.#doms = doms; this.#holder = new LabelHolder(labelsBaseUrl, navigatorLanguages); this.#buildSelectOptions(); - // Resolve and apply the initial language synchronously so that callers - // can pull it via `labelHandler.language` / `holder` right after - // construction. The initial value is delivered by pull, not push; - // subscribers only receive subsequent user changes via `addListener`. + // WHY: resolve and apply the initial language synchronously so callers can pull it via labelHandler.language / holder right after construction. Subscribers via addListener() only receive subsequent user changes. this.#commitLanguage( (localStorage.getItem("language") as Language | null) ?? resolveLanguage(navigatorLanguages), diff --git a/src/lib/labels.ts b/src/lib/labels.ts index 14b7ba747..db74df954 100644 --- a/src/lib/labels.ts +++ b/src/lib/labels.ts @@ -17,10 +17,12 @@ export const LANGUAGES = [ ] as const; export type Language = (typeof LANGUAGES)[number]; -// Maps a BCP47 primary language subtag to a 7 Days to Die label set. -// Chinese is resolved separately because its label set depends on the script -// subtag (Hans vs Hant), which we derive via Intl.Locale's CLDR-backed -// Likely Subtags expansion. +/** + * Maps a BCP47 primary language subtag to a 7 Days to Die label set. + * Chinese is resolved separately because its label set depends on the + * script subtag (Hans vs Hant), derived via `Intl.Locale`'s CLDR-backed + * Likely Subtags expansion. + */ const PRIMARY_SUBTAG_LANGUAGES: { [primary: string]: Language } = { en: "english", de: "german", @@ -131,12 +133,14 @@ function matchLanguage(clientTag: string): Language | undefined { return PRIMARY_SUBTAG_LANGUAGES[locale.language]; } -// Use Intl.Locale's `maximize()` (backed by CLDR Likely Subtags) to fill in the -// script subtag from language/region — so `zh-CN`, `zh-SG` expand to Hans and -// `zh-TW`, `zh-HK`, `zh-MO` expand to Hant without us hand-maintaining the -// region list. Bare `zh` expands to `zh-Hans-CN`, matching the upstream -// 7 Days to Die / Steam convention where `schinese` is the canonical Chinese -// label set. +/** + * Uses `Intl.Locale.maximize()` (backed by CLDR Likely Subtags) to + * fill in the script subtag from language and region. `zh-CN` and + * `zh-SG` expand to Hans, while `zh-TW`, `zh-HK`, and `zh-MO` expand + * to Hant without a hand-maintained region list. Bare `zh` expands to + * `zh-Hans-CN`, matching the upstream 7 Days to Die / Steam convention + * where `schinese` is the canonical Chinese label set. + */ function resolveChinese(locale: Intl.Locale): Language { return locale.maximize().script === "Hant" ? "tchinese" : "schinese"; } diff --git a/src/lib/oneshot-worker.ts b/src/lib/oneshot-worker.ts index 8849c62a5..30bf15399 100644 --- a/src/lib/oneshot-worker.ts +++ b/src/lib/oneshot-worker.ts @@ -40,8 +40,7 @@ function awaitOneshotWorker(worker: Worker): Promise { settle(() => resolve(event.data)); }); worker.addEventListener("error", (event) => { - // Prevent the browser from also logging an "Uncaught" notice for an - // error we are about to surface through the promise. + // WHY: prevent the browser from also logging an "Uncaught" notice for an error we are about to surface through the promise. event.preventDefault(); const { message, filename, lineno } = event; const detail = message || filename diff --git a/src/lib/prefab-added-versions.ts b/src/lib/prefab-added-versions.ts index 85a9e24f8..28ec6c366 100644 --- a/src/lib/prefab-added-versions.ts +++ b/src/lib/prefab-added-versions.ts @@ -1,10 +1,15 @@ import type { PrefabAddedVersions } from "../types/7dtdmap.ts"; -// Parses each dot-separated version string (e.g. "3.10") into a numeric -// segment array and keeps the numerically greatest one, so "3.10" is treated -// as newer than "3.2" (a plain lexicographic sort would get this backwards). -// Kept separate from lib/prefabs.ts (which uses DOMParser) so this is -// importable from the filter worker, where DOM types are unavailable. +/** + * Parses each dot-separated version string (e.g. `"3.10"`) into a + * numeric segment array and keeps the numerically greatest one, so + * `"3.10"` is treated as newer than `"3.2"`. Plain lexicographic sort + * would order them backwards. + * + * Kept separate from `lib/prefabs.ts` (which uses `DOMParser`) so it + * is importable from the filter worker, where DOM types are + * unavailable. + */ export function latestAddedVersion( addedVersions: PrefabAddedVersions, ): string { diff --git a/src/lib/prefab-hit-index.ts b/src/lib/prefab-hit-index.ts index 533735eb0..6be61315e 100644 --- a/src/lib/prefab-hit-index.ts +++ b/src/lib/prefab-hit-index.ts @@ -1,19 +1,23 @@ import type { GameCoords, Prefab, PrefabMeshSizes } from "../types/7dtdmap.ts"; -// Mirrors the exclusions in map-renderer's footprint draw so that hit-tests -// target exactly the rectangles a user can see on the map. Tiles are the -// placement framework; part_driveway overlaps its parent POI and is never the -// thing the user is asking about. +/** + * Mirrors the exclusions in `map-renderer`'s footprint draw so hit-tests + * target exactly the rectangles a user can see on the map. `rwg_tile_` + * is the placement framework and `part_driveway_` overlaps its parent + * POI, so neither is ever what a user is asking about. + */ export const EXCLUDED_NAME_FRAGMENTS = [ "rwg_tile_", "part_driveway_", ] as const; -// AABB stored in four parallel Int32Arrays so the hit-test loop is pure -// integer compares (no string `.includes`, no PrefabMeshSizes lookup, no -// rotation parity per sample). Rows are sorted ascending by footprint area -// so the first hit is the smallest, naturally preferring the more specific -// POI on overlapping footprints. +/** + * AABB stored in four parallel `Int32Array`s so the hit-test loop is + * pure integer compares (no string `.includes`, no `PrefabMeshSizes` + * lookup, no rotation parity per sample). Rows are sorted ascending by + * footprint area so the first hit is the smallest, naturally preferring + * the more specific POI on overlapping footprints. + */ export class PrefabHitIndex { readonly source: Prefab[]; readonly prefabs: Prefab[]; @@ -38,9 +42,7 @@ export class PrefabHitIndex { const size = meshSizes[p.name]; if (!size) continue; const [sx, sz] = size; - // decoration.position is the SW corner of the rotated AABB; 90°/270° - // rotations swap world-aligned width/depth. Matches the renderer's - // footprint formula in src/worker/lib/map-renderer.ts. + // WHY: decoration.position is the SW corner of the rotated AABB. Rotations by 90° or 270° swap world-aligned width and depth. const odd = ((p.rotation ?? 0) & 1) === 1; const w = odd ? sz : sx; const d = odd ? sx : sz; diff --git a/src/lib/prefab-link-tooltip.ts b/src/lib/prefab-link-tooltip.ts index 72cf50968..69c4307cc 100644 --- a/src/lib/prefab-link-tooltip.ts +++ b/src/lib/prefab-link-tooltip.ts @@ -1,4 +1,4 @@ -// Pixels offset from the cursor so the tooltip does not sit on top of it. +/** Pixels offset from the cursor so the tooltip does not sit on top of it. */ const CURSOR_OFFSET = 16; const HREF_PATTERN = /(?:^|\/)prefabs\/([^/]+)\.html(?:[?#]|$)/; @@ -8,9 +8,12 @@ interface Doms { image: HTMLImageElement; } -// Shows a preview image popover when the user hovers a link to an individual -// prefab page. The popover element is placed in the top layer so it overlays -// modal `` content (e.g. the prefab inspector). +/** + * Shows a preview image popover when the user hovers a link to an + * individual prefab page. The popover element is placed in the top + * layer so it overlays modal `` content (e.g. the prefab + * inspector). + */ export function installPrefabLinkTooltip(doms: Doms): void { let currentLink: HTMLAnchorElement | null = null; let lastEvent: MouseEvent | null = null; diff --git a/src/lib/prefab-tooltip.ts b/src/lib/prefab-tooltip.ts index 5d6cf58b3..a29185e65 100644 --- a/src/lib/prefab-tooltip.ts +++ b/src/lib/prefab-tooltip.ts @@ -7,12 +7,16 @@ import type { LabelHandler } from "./label-handler.ts"; import { escapeHtml } from "./utils.ts"; import { latestAddedVersion } from "./prefab-added-versions.ts"; -// Pixels between the anchor (footprint box edge, or the cursor on the fallback -// path) and the tooltip, so it does not sit on top of the crosshair. +/** + * Pixels between the anchor (footprint box edge, or the cursor on the + * fallback path) and the tooltip. Keeps the tooltip off the crosshair. + */ export const CURSOR_OFFSET = 16; -// Which action hints to list. The 2D map wires all three; the 3D terrain -// viewer only supports Shift+Click, so it passes just that one. +/** + * Which action hints to list. The 2D map wires all three. The 3D + * terrain viewer only supports Shift+Click and passes just that one. + */ type TooltipHint = "click" | "dblclick" | "shift-click"; interface TooltipContent { @@ -31,12 +35,18 @@ const HINT_HTML: Record = { `
🔗 Shift+Click: Open prefab page
`, }; -// Shared prefab popover (image, badges, name, hints) for the 2D map and 3D -// hover; each caller computes its own anchor and passes viewport coords to showAt(). +/** + * Shared prefab popover (image, badges, name, hints) for the 2D map + * and 3D hover. Each caller computes its own anchor and passes viewport + * coords to `showAt()`. + */ class PrefabTooltip { #dom: HTMLElement; - // Rebuild innerHTML only when the shown prefab changes so hovering within - // one POI does not re-request the (and flash) on every sample. + /** + * Rebuild `innerHTML` only when the shown prefab changes so hovering + * within one POI does not re-request the `` (and flash) on + * every sample. + */ #shownKey: string | null = null; constructor(dom: HTMLElement) { @@ -75,8 +85,7 @@ class PrefabTooltip { showAt(left: number, top: number): void { this.#dom.style.left = `${left.toString()}px`; this.#dom.style.top = `${top.toString()}px`; - // showPopover() throws if already open, so only enter the top layer once - // and let subsequent calls just reposition (e.g. while the camera pans). + // WHY: showPopover() throws if already open, so only enter the top layer once and let subsequent calls just reposition (e.g. while the camera pans). if (!this.#dom.matches(":popover-open")) this.#dom.showPopover(); } @@ -86,9 +95,12 @@ class PrefabTooltip { } } -// Single owner of the shared tooltip DOM for both the 2D map and the terrain -// viewer. Each caller supplies only hit detection and an anchor; this resolves -// the prefab to content, owns the Shift-hint state, and positions the popover. +/** + * Single owner of the shared tooltip DOM for both the 2D map and the + * terrain viewer. Each caller supplies hit detection and an anchor. + * This class resolves the prefab to content, owns the Shift-hint + * state, and positions the popover. + */ export class PrefabTooltipController { #view: PrefabTooltip; #labelHandler: LabelHandler; @@ -96,8 +108,11 @@ export class PrefabTooltipController { #addedVersions: Promise; #latestAddedVersion: Promise; #shiftActive = false; - // Bumped by every showFor and hide so a late-resolving content lookup that - // has since been superseded (newer hover, or a hide) never renders. + /** + * Bumped by every `showFor` and `hide` so a late-resolving content + * lookup that has since been superseded (newer hover or a hide) + * never renders. + */ #token = 0; constructor( @@ -112,8 +127,7 @@ export class PrefabTooltipController { this.#addedVersions = addedVersions; this.#latestAddedVersion = addedVersions.then(latestAddedVersion); - // Track Shift globally so the hint highlight flips the moment the modifier - // is pressed or released, without needing pointer movement. + // WHY: track Shift globally so the hint highlight flips the moment the modifier is pressed or released, without needing pointer movement. globalThis.addEventListener("keydown", (e) => { if (e.key === "Shift") this.#setShiftActive(true); }); @@ -123,8 +137,11 @@ export class PrefabTooltipController { globalThis.addEventListener("blur", () => this.#setShiftActive(false)); } - // `anchor` is evaluated after the content lookup resolves, so it reflects the - // freshest pointer/camera state for the request that wins the token race. + /** + * `anchor` is evaluated after the content lookup resolves so it + * reflects the freshest pointer or camera state for the request that + * wins the token race. + */ async showFor( prefab: Pick, hints: TooltipHint[], diff --git a/src/lib/prefabs-filter-controls.ts b/src/lib/prefabs-filter-controls.ts index e127ad787..d7592b546 100644 --- a/src/lib/prefabs-filter-controls.ts +++ b/src/lib/prefabs-filter-controls.ts @@ -31,10 +31,13 @@ export function readPreExcludes( return doms.preExcludes.flatMap((i) => (i.checked ? [i.value] : [])); } -// Wire the controls common to every PrefabsHandler variant: text filters, -// tier range inputs with change-suppression, pre-exclude checkboxes, and -// the language switch. Callers handle the variant-specific concerns -// (initial postMessage, marker/file integration, output dispatch). +/** + * Wires the controls common to every `PrefabsHandler` variant: text + * filters, tier range inputs with change-suppression, pre-exclude + * checkboxes, and the language switch. Callers handle the + * variant-specific concerns (initial `postMessage`, marker or file + * integration, output dispatch). + */ export function bindPrefabsFilterControls( doms: PrefabsFilterControlsDoms, worker: PrefabsFilterWorker, diff --git a/src/lib/prefabs.ts b/src/lib/prefabs.ts index e9cdbf2b3..b0fe2ef10 100644 --- a/src/lib/prefabs.ts +++ b/src/lib/prefabs.ts @@ -15,7 +15,10 @@ export function assertDifficultyIndex( } } -// Note: This logic can not be moved to a worker because DOM API like `DOMParser` is not available in workers. +/** + * Runs on the main thread because it uses `DOMParser`, which is not + * available in workers. + */ export async function loadPrefabsXml(): Promise { const workspace = await storage.workspaceDir(); const prefabsXml = await workspace.get("prefabs.xml"); @@ -30,17 +33,16 @@ function parseXml(xml: string): Prefab[] { }); } -// Exported for testing without a DOM; accepts anything with `getAttribute`. +/** Exported for testing without a DOM. Accepts anything with `getAttribute`. */ export function decorationToPrefab( e: { getAttribute(name: string): string | null }, ): Prefab | null { const positionAttr = e.getAttribute("position"); const name = e.getAttribute("name"); - // A decoration without a name or position is not a placed prefab; skip it. + // WHY: a decoration without a name or position is not a placed prefab, so skip it. if (name === null || positionAttr === null) return null; const nums = positionAttr.split(",").map((s) => parseInt(s, 10)); - // A malformed position is a corrupt World File, so fail loudly instead of - // coercing to NaN or silently dropping the prefab. + // WHY: a malformed position is a corrupt World File. Fail loudly instead of coercing to NaN or silently dropping the prefab. if (nums.length !== 3 || !nums.every((n) => Number.isFinite(n))) { throw new Error( `Invalid decoration position "${positionAttr}" for prefab "${name}"`, diff --git a/src/lib/storage.ts b/src/lib/storage.ts index eb688e2f9..ef18f45dd 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -71,8 +71,7 @@ export class MapDir { await writable.close(); } } else { - // pipeTo closes the writable on its own (preventClose defaults to false), - // so we must not call close() again here. + // INVARIANT: do not close() again here because pipeTo already closes the writable (preventClose defaults to false). await data.pipeTo(writable); } } @@ -114,8 +113,7 @@ export class MapDir { try { await this.#dir.removeEntry(name); } catch (e: unknown) { - // Treat removal as idempotent: ignore missing entries so that bulk - // clears (e.g. FileHandler#clear) don't abort halfway through. + // WHY: treat removal as idempotent so bulk clears (e.g. FileHandler#clear) do not abort halfway through when an entry is already gone. if (e instanceof DOMException && e.name === "NotFoundError") { return; } diff --git a/src/lib/throttled-invoker.ts b/src/lib/throttled-invoker.ts index 5f84f4ce6..629b3badb 100644 --- a/src/lib/throttled-invoker.ts +++ b/src/lib/throttled-invoker.ts @@ -15,8 +15,7 @@ export function throttledInvoker( const cycle = pending; const wait = Math.max(10, lastFinishedAt + intervalMs - Date.now()); await sleep(wait); - // Cleared just before invoking: calls arriving while asyncFunc runs - // must schedule one more cycle; calls during the wait above must not. + // INVARIANT: clear pending just before invoking so calls arriving while asyncFunc runs schedule one more cycle, while calls during the wait above coalesce into this one. pending = null; try { await asyncFunc(); diff --git a/src/lib/url-state.ts b/src/lib/url-state.ts index ecbcec848..98505588c 100644 --- a/src/lib/url-state.ts +++ b/src/lib/url-state.ts @@ -3,7 +3,7 @@ interface StateElement { element: HTMLInputElement; } -// Store values of input elements in the URL query string. +/** Stores values of input elements in the URL query string. */ export class UrlState { #url: URL; #inputs: Map; @@ -20,9 +20,7 @@ export class UrlState { for (const [input, { defaultValue }] of this.#inputs.entries()) { if (this.#url.searchParams.has(input.id)) { setValue(input, this.#url.searchParams.get(input.id) ?? defaultValue); - // Dispatch both events so listeners that subscribe to either one - // observe the restored value. Both are bubbling to match how - // browsers fire native user-driven events. + // WHY: dispatch both events so listeners that subscribe to either one observe the restored value. Both bubble to match how browsers fire native user-driven events. input.dispatchEvent(new Event("input", { bubbles: true })); input.dispatchEvent(new Event("change", { bubbles: true })); } @@ -37,9 +35,7 @@ export class UrlState { fn(this.#url); }); }; - // Subscribe to both events so we catch text-style typing (input) and - // checkbox/radio/select toggles (change). The value-equality guard - // above prevents the duplicate dispatch from causing double updates. + // WHY: subscribe to both events to catch text-style typing (input) and checkbox/radio/select toggles (change). The value-equality guard above prevents the duplicate dispatch from causing double updates. input.addEventListener("input", handler); input.addEventListener("change", handler); } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index c97866555..de3f6a370 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -71,14 +71,16 @@ export async function fetchJson(url: string): Promise { return (await r.json()) as T; } -// Reject empty/short bodies before they are persisted: dev/CI static servers -// occasionally return them under load. See github.com/kui/7dtd-map/issues/202. +/** + * Rejects empty or short bodies before they are persisted. Dev and CI + * static servers occasionally return them under load. See + * https://github.com/kui/7dtd-map/issues/202. + */ export async function fetchCompleteBlob(url: string): Promise { const r = await fetch(url); if (!r.ok) throw Error(`Failed to fetch ${url}: ${r.statusText}`); const blob = await r.blob(); - // Content-Encoding bodies decompress client-side, so blob.size legitimately - // differs from Content-Length; only length-check uncompressed responses. + // WHY: content-encoding bodies decompress client-side, so blob.size legitimately differs from Content-Length. Only length-check uncompressed responses. const header = r.headers.get("content-length"); const declared = header === null ? NaN : Number(header); const short = r.headers.get("content-encoding") === null && @@ -100,10 +102,12 @@ export function basename(path: string) { return tail.split(/[?#]/, 1)[0] ?? tail; } -// Escape characters that have special meaning in HTML text content or in -// double-quoted attribute values. Inputs sourced from user-supplied XML files -// or worker output must be passed through this before being interpolated into -// innerHTML strings. +/** + * Escapes characters that have special meaning in HTML text content or + * in double-quoted attribute values. Inputs sourced from user-supplied + * XML files or worker output must be passed through this before being + * interpolated into `innerHTML` strings. + */ export function escapeHtml(s: string): string { return s.replace(/[&<>"']/g, (c) => { switch (c) { diff --git a/src/prefabs/main.ts b/src/prefabs/main.ts index 6b6802724..896623bd3 100644 --- a/src/prefabs/main.ts +++ b/src/prefabs/main.ts @@ -18,7 +18,6 @@ function main() { ); }); - // init (async () => { updatePrefabLabels(await labelHolder.get("prefabs")); updateBlockLabels( diff --git a/src/types/7dtdmap.ts b/src/types/7dtdmap.ts index 70a15d6dd..530d7c5eb 100644 --- a/src/types/7dtdmap.ts +++ b/src/types/7dtdmap.ts @@ -3,53 +3,71 @@ export interface Prefab { x: number; y: number; z: number; - // Placed rotation from prefabs.xml `decoration.rotation`, 0..3 = 0/90/180/270 - // degrees clockwise applied at placement time. + /** + * Placed rotation from `prefabs.xml decoration.rotation`. Values 0..3 + * correspond to 0/90/180/270 degrees clockwise applied at placement + * time. + */ rotation?: number; - // From `y_is_groundlevel="true"`; a missing attribute is false. When true, - // y is the terrain surface rather than the prefab's bottom. + /** + * From `y_is_groundlevel="true"`. A missing attribute is `false`. + * When `true`, `y` is the terrain surface rather than the prefab's + * bottom. + */ yIsGroundLevel: boolean; } export interface PrefabMeshSizes { - // [width (X), depth (Z), height (Y), yOffset] in game blocks. yOffset is the - // number of blocks the prefab sits below ground (0 or negative). + /** + * `[width (X), depth (Z), height (Y), yOffset]` in game blocks. + * `yOffset` is the number of blocks the prefab sits below ground + * (0 or negative). + */ [prefabName: string]: [number, number, number, number]; } export interface DistrictColors { - // CSS hex (`#rrggbb`) from rwgmixer.xml `` preview_color. + /** CSS hex (`#rrggbb`) from `rwgmixer.xml preview_color`. */ [districtName: string]: string; } export interface PrefabDensityScores { - // Integer score from PrefabData.Init: floor((TotalVertices + 50000) / 100000). + /** Integer score from `PrefabData.Init`: `floor((TotalVertices + 50000) / 100000)`. */ [prefabName: string]: number; } -// Written by tools/generate-glyph-markers.ts. +/** Written by `tools/generate-glyph-markers.ts`. */ export interface GlyphMarker { d: string; - // Point in the shared VIEWPORT square (lib/glyph-marker.ts) that should - // land on the target coordinate when this marker is placed. + /** + * Point in the shared `VIEWPORT` square (`lib/glyph-marker.ts`) that + * should land on the target coordinate when this marker is placed. + */ anchor: { x: number; y: number }; } export interface HighlightedPrefab extends Prefab { - // Looked up from prefab-difficulties.json inside the filter worker — kept on - // the highlighted variant (not Prefab itself) so the type stays a pure - // representation of the prefabs.xml entry. + /** + * Looked up from `prefab-difficulties.json` inside the filter worker. + * Kept on the highlighted variant (not `Prefab` itself) so the base + * type stays a pure representation of the `prefabs.xml` entry. + */ difficulty?: number; - // Looked up from prefab-added-versions.json; absent for prefabs that - // predate that tracking (e.g. everything before 3.0). + /** + * Looked up from `prefab-added-versions.json`. Absent for prefabs + * that predate that tracking (e.g. everything before 3.0). + */ addedVersion?: string; isAddedInLatestVersion?: boolean; highlightedName?: string; highlightedLabel?: string; matchedBlocks?: HighlightedBlock[]; matchedBlockCount?: number; - // Number of matched block types before matchedBlocks was capped for display; - // larger than matchedBlocks.length when the list was truncated. + /** + * Number of matched block types before `matchedBlocks` was capped + * for display. Larger than `matchedBlocks.length` when the list was + * truncated. + */ matchedBlockTypeCount?: number; distance?: [Direction | null, number] | null; } diff --git a/tools/lint-plugins/require-comment-rationale.ts b/tools/lint-plugins/require-comment-rationale.ts index a515f8828..639eb4cdc 100644 --- a/tools/lint-plugins/require-comment-rationale.ts +++ b/tools/lint-plugins/require-comment-rationale.ts @@ -44,24 +44,6 @@ const EXCLUDED_PATHS: readonly string[] = [ "/src/index/terrain-viewer/hover-controller.ts", "/src/index/terrain-viewer/marker-sprites.ts", "/src/index/terrain-viewer/prefab-boxes.ts", - "/src/lib/dom-utils.ts", - "/src/lib/footprint-color.ts", - "/src/lib/glyph-sprite.ts", - "/src/lib/label-handler.ts", - "/src/lib/labels.ts", - "/src/lib/oneshot-worker.ts", - "/src/lib/prefab-added-versions.ts", - "/src/lib/prefab-hit-index.ts", - "/src/lib/prefab-link-tooltip.ts", - "/src/lib/prefab-tooltip.ts", - "/src/lib/prefabs-filter-controls.ts", - "/src/lib/prefabs.ts", - "/src/lib/storage.ts", - "/src/lib/throttled-invoker.ts", - "/src/lib/url-state.ts", - "/src/lib/utils.ts", - "/src/prefabs/main.ts", - "/src/types/7dtdmap.ts", "/src/worker/lib/map-renderer.ts", "/src/worker/lib/prefab-filter.ts", "/src/worker/map-renderer.ts",