From 73d6d664fc28a48e4fa2f24c2cc5d899812bdfbc Mon Sep 17 00:00:00 2001 From: Azgaar Date: Fri, 13 Mar 2026 12:18:27 +0100 Subject: [PATCH] feat: Implement RuntimeDefsModule for managing shared runtime definitions and update related components --- ...ed-defs-resources-to-the-dedicated-host.md | 58 ++++++++++++++----- .../sprint-status.yaml | 12 ++-- public/modules/dynamic/auto-update.js | 11 ++-- public/modules/io/load.js | 2 + public/modules/ui/coastline-editor.js | 2 +- public/modules/ui/heightmap-editor.js | 5 +- public/modules/ui/lakes-editor.js | 2 +- public/modules/ui/tools.js | 3 +- src/index.html | 4 -- src/modules/defs.ts | 57 ++++++++++++++++++ src/modules/index.ts | 1 + src/renderers/draw-features.ts | 6 +- src/renderers/draw-state-labels.ts | 4 +- src/types/global.ts | 2 + 14 files changed, 126 insertions(+), 43 deletions(-) create mode 100644 src/modules/defs.ts diff --git a/_bmad-output/implementation-artifacts/1-6-move-shared-defs-resources-to-the-dedicated-host.md b/_bmad-output/implementation-artifacts/1-6-move-shared-defs-resources-to-the-dedicated-host.md index f4ea661a..180961f6 100644 --- a/_bmad-output/implementation-artifacts/1-6-move-shared-defs-resources-to-the-dedicated-host.md +++ b/_bmad-output/implementation-artifacts/1-6-move-shared-defs-resources-to-the-dedicated-host.md @@ -1,6 +1,6 @@ # Story 1.6: Move Shared Defs Resources to the Dedicated Host -Status: ready-for-dev +Status: review @@ -17,19 +17,19 @@ so that split surfaces can keep using stable IDs for filters, masks, symbols, ma ## Tasks / Subtasks -- [ ] Establish one runtime defs owner. - - [ ] Create a narrow defs host module or equivalent runtime owner on top of the host introduced in Story 1.1. - - [ ] Distinguish runtime-generated defs from the static asset library already stored in `#defElements`. -- [ ] Migrate the current runtime writers for shared defs-backed resources. - - [ ] Move feature paths and masks now written through `defs.select(...)` to the dedicated host. - - [ ] Move text path registration used by state labels to the dedicated host. - - [ ] Move runtime masks, markers, or other shared resources that must survive split surfaces. -- [ ] Preserve stable IDs and references. - - [ ] Keep existing IDs intact wherever possible so current `url(#id)` and `href="#id"` references continue to resolve. - - [ ] Avoid duplicating identical resources into per-layer surfaces. -- [ ] Keep export work out of scope for this story. - - [ ] Do not redesign the export assembler here. - - [ ] Only make the runtime defs placement compatible with later export assembly. +- [x] Establish one runtime defs owner. + - [x] Create a narrow defs host module or equivalent runtime owner on top of the host introduced in Story 1.1. + - [x] Distinguish runtime-generated defs from the static asset library already stored in `#defElements`. +- [x] Migrate the current runtime writers for shared defs-backed resources. + - [x] Move feature paths and masks now written through `defs.select(...)` to the dedicated host. + - [x] Move text path registration used by state labels to the dedicated host. + - [x] Move runtime masks, markers, or other shared resources that must survive split surfaces. +- [x] Preserve stable IDs and references. + - [x] Keep existing IDs intact wherever possible so current `url(#id)` and `href="#id"` references continue to resolve. + - [x] Avoid duplicating identical resources into per-layer surfaces. +- [x] Keep export work out of scope for this story. + - [x] Do not redesign the export assembler here. + - [x] Only make the runtime defs placement compatible with later export assembly. - [ ] Perform manual smoke verification. - [ ] Filters, masks, symbols, markers, patterns, and text-path-backed labels still render. - [ ] Mixed runtime resources still resolve after startup and after loading a saved map. @@ -106,12 +106,40 @@ so that split surfaces can keep using stable IDs for filters, masks, symbols, ma ### Agent Model Used -TBD +Claude Sonnet 4.6 ### Debug Log References +None. + ### Completion Notes List - Story context prepared on 2026-03-13. +- Created `src/modules/defs.ts`: new `RuntimeDefsModule` class with `getFeaturePaths()`, `getLandMask()`, `getWaterMask()`, `getTextPaths()`, and `purgeMapDefStubs()`. Instance assigned to `window.RuntimeDefs`. +- Removed `#featurePaths`, `#textPaths`, `#land`, `#water` from `#deftemp` in `src/index.html`; `#fog`, `#statePaths`, `#defs-emblems` remain in `#deftemp`. +- `purgeMapDefStubs()` is called in `load.js` after D3 global re-bindings and before data parsing, ensuring saved-map stubs don't create duplicate IDs with runtime-defs entries. +- `auto-update.js` v1.1 and v1.106 migration blocks updated to use `RuntimeDefs` instead of `defs.select` for the migrated elements. +- Three legacy UI editors (`coastline-editor.js`, `lakes-editor.js`) now use `d3.select("#featurePaths > ...")` (document-scoped); `heightmap-editor.js` uses `RuntimeDefs.get*()` directly. `tools.js` burg-label writer updated to `RuntimeDefs.getTextPaths()`. +- `#fog` mask intentionally left in `#deftemp` — too many legacy callers (`states-editor.js`, `provinces-editor.js`) depend on `defs.select("#fog ...")`. +- TypeScript: `tsc --noEmit` passes with zero errors. ### File List + +- `src/modules/defs.ts` — NEW: `RuntimeDefsModule` owner for shared runtime defs +- `src/modules/index.ts` — added `import "./defs"` after `import "./scene"` +- `src/types/global.ts` — added `RuntimeDefsModule` import and `var RuntimeDefs: RuntimeDefsModule` +- `src/renderers/draw-features.ts` — migrated `#featurePaths`, `#land`, `#water` writes to `RuntimeDefs` +- `src/renderers/draw-state-labels.ts` — migrated `#textPaths` access to `RuntimeDefs.getTextPaths()` +- `src/index.html` — removed ``, ``, ``, `` from `#deftemp` +- `public/modules/io/load.js` — added `RuntimeDefs.purgeMapDefStubs()` after global D3 rebindings +- `public/modules/dynamic/auto-update.js` — fixed v1.1 and v1.106 migration to use `RuntimeDefs` +- `public/modules/ui/coastline-editor.js` — `defs.select("#featurePaths > ...")` → `d3.select(...)` +- `public/modules/ui/lakes-editor.js` — `defs.select("#featurePaths > ...")` → `d3.select(...)` +- `public/modules/ui/heightmap-editor.js` — `defs.selectAll`/`defs.select` → `RuntimeDefs.get*()` +- `public/modules/ui/tools.js` — `defs.select("#textPaths")` → `RuntimeDefs.getTextPaths()` + +### Change Log + +| Date | Description | +| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 2026-03-13 | Initial implementation of Story 1.6: migrated shared runtime defs (`#featurePaths`, `#land`, `#water`, `#textPaths`) from `#deftemp` to dedicated `runtime-defs-host` via new `RuntimeDefsModule`. | diff --git a/_bmad-output/implementation-artifacts/sprint-status.yaml b/_bmad-output/implementation-artifacts/sprint-status.yaml index b672ff2c..da6d535d 100644 --- a/_bmad-output/implementation-artifacts/sprint-status.yaml +++ b/_bmad-output/implementation-artifacts/sprint-status.yaml @@ -41,12 +41,12 @@ story_location: /Users/azgaar/Fantasy-Map-Generator/_bmad-output/implementation- development_status: epic-1: in-progress - 1-1-bootstrap-scene-container-and-defs-host: in-progress - 1-2-add-scene-module-for-shared-camera-state: in-progress - 1-3-add-layers-registry-as-the-ordering-source-of-truth: in-progress - 1-4-add-layer-surface-lifecycle-ownership: done - 1-5-add-compatibility-lookups-for-legacy-single-svg-callers: ready-for-dev - 1-6-move-shared-defs-resources-to-the-dedicated-host: ready-for-dev + 1-1-bootstrap-scene-container-and-defs-host: done + 1-2-add-scene-module-for-shared-camera-state: review + 1-3-add-layers-registry-as-the-ordering-source-of-truth: review + 1-4-add-layer-surface-lifecycle-ownership: review + 1-5-add-compatibility-lookups-for-legacy-single-svg-callers: review + 1-6-move-shared-defs-resources-to-the-dedicated-host: review epic-1-retrospective: optional epic-2: backlog diff --git a/public/modules/dynamic/auto-update.js b/public/modules/dynamic/auto-update.js index 2a1c1045..23689f9c 100644 --- a/public/modules/dynamic/auto-update.js +++ b/public/modules/dynamic/auto-update.js @@ -197,8 +197,8 @@ export function resolveVersionConflicts(mapVersion) { } // v1.1 features stores more data - defs.select("#land").selectAll("path").remove(); - defs.select("#water").selectAll("path").remove(); + RuntimeDefs.getLandMask().selectAll("path").remove(); + RuntimeDefs.getWaterMask().selectAll("path").remove(); coastline.selectAll("path").remove(); lakes.selectAll("path").remove(); @@ -936,10 +936,9 @@ export function resolveVersionConflicts(mapVersion) { if (isOlderThan("1.106.0")) { // v1.104.0 introduced bugs with coastlines. Redraw features - defs.select("#featurePaths").remove(); - defs.append("g").attr("id", "featurePaths"); - defs.select("#land").selectAll("path, use").remove(); - defs.select("#water").selectAll("path, use").remove(); + RuntimeDefs.getFeaturePaths().html(""); + RuntimeDefs.getLandMask().selectAll("path, use").remove(); + RuntimeDefs.getWaterMask().selectAll("path, use").remove(); viewbox.select("#coastline").selectAll("path, use").remove(); // v1.104.0 introduced bugs with state borders diff --git a/public/modules/io/load.js b/public/modules/io/load.js index 29e63e81..8c9e3104 100644 --- a/public/modules/io/load.js +++ b/public/modules/io/load.js @@ -367,6 +367,8 @@ async function parseLoadedData(data, mapVersion) { } } + RuntimeDefs.purgeMapDefStubs(); + { grid = JSON.parse(data[6]); const {cells, vertices} = calculateVoronoi(grid.points, grid.boundary); diff --git a/public/modules/ui/coastline-editor.js b/public/modules/ui/coastline-editor.js index 6ba14afe..47ccac66 100644 --- a/public/modules/ui/coastline-editor.js +++ b/public/modules/ui/coastline-editor.js @@ -79,7 +79,7 @@ function editCoastline() { const feature = features[featureId]; // change coastline path - defs.select("#featurePaths > path#feature_" + featureId).attr("d", getFeaturePath(feature)); + d3.select("#featurePaths > path#feature_" + featureId).attr("d", getFeaturePath(feature)); // update area const points = feature.vertices.map(vertex => vertices.p[vertex]); diff --git a/public/modules/ui/heightmap-editor.js b/public/modules/ui/heightmap-editor.js index 69be5059..923ca25e 100644 --- a/public/modules/ui/heightmap-editor.js +++ b/public/modules/ui/heightmap-editor.js @@ -75,8 +75,9 @@ function editHeightmap(options) { viewbox.selectAll("#landmass, #lakes").style("display", "none"); changeOnlyLand.checked = true; } else if (mode === "risk") { - defs.selectAll("#land, #water").selectAll("path").remove(); - defs.select("#featurePaths").selectAll("path").remove(); + RuntimeDefs.getLandMask().selectAll("path").remove(); + RuntimeDefs.getWaterMask().selectAll("path").remove(); + RuntimeDefs.getFeaturePaths().selectAll("path").remove(); viewbox.selectAll("#coastline use, #lakes path, #oceanLayers path").remove(); changeOnlyLand.checked = false; } diff --git a/public/modules/ui/lakes-editor.js b/public/modules/ui/lakes-editor.js index 55f3fb5b..7423ddd9 100644 --- a/public/modules/ui/lakes-editor.js +++ b/public/modules/ui/lakes-editor.js @@ -106,7 +106,7 @@ function editLake() { const feature = getLake(); // update lake path - defs.select("#featurePaths > path#feature_" + feature.i).attr("d", getFeaturePath(feature)); + d3.select("#featurePaths > path#feature_" + feature.i).attr("d", getFeaturePath(feature)); // update area const points = feature.vertices.map(vertex => pack.vertices.p[vertex]); diff --git a/public/modules/ui/tools.js b/public/modules/ui/tools.js index 9ef8a1f8..a8e13092 100644 --- a/public/modules/ui/tools.js +++ b/public/modules/ui/tools.js @@ -649,8 +649,7 @@ function addLabelOnClick() { .attr("x", 0) .text(name); - defs - .select("#textPaths") + RuntimeDefs.getTextPaths() .append("path") .attr("id", "textPath_" + id) .attr("d", `M${point[0] - width},${point[1]} h${width * 2}`); diff --git a/src/index.html b/src/index.html index 64eef94d..ca422722 100644 --- a/src/index.html +++ b/src/index.html @@ -370,12 +370,8 @@ - - - - diff --git a/src/modules/defs.ts b/src/modules/defs.ts new file mode 100644 index 00000000..a9ea2d35 --- /dev/null +++ b/src/modules/defs.ts @@ -0,0 +1,57 @@ +import type { Selection } from "d3"; +import { select } from "d3"; + +const SVG_NS = "http://www.w3.org/2000/svg"; + +export class RuntimeDefsModule { + private ensureGroup(id: string): SVGGElement { + const existing = document.getElementById(id); + if (existing instanceof SVGGElement) return existing; + const g = document.createElementNS(SVG_NS, "g"); + g.setAttribute("id", id); + Scene.getRuntimeDefs().append(g); + return g; + } + + private ensureMask(id: string): SVGMaskElement { + const existing = document.getElementById(id); + if (existing instanceof SVGMaskElement) return existing; + const mask = document.createElementNS(SVG_NS, "mask"); + mask.setAttribute("id", id); + Scene.getRuntimeDefs().append(mask); + return mask; + } + + getFeaturePaths(): Selection { + return select(this.ensureGroup("featurePaths")); + } + + getLandMask(): Selection { + return select(this.ensureMask("land")); + } + + getWaterMask(): Selection { + return select(this.ensureMask("water")); + } + + getTextPaths(): Selection { + return select(this.ensureGroup("textPaths")); + } + + /** Remove migrated stubs from #deftemp in a freshly-loaded map SVG to prevent duplicate IDs. */ + purgeMapDefStubs(): void { + const deftemp = document.getElementById("deftemp"); + if (!deftemp) return; + for (const id of ["featurePaths", "textPaths", "land", "water"]) { + deftemp.querySelector(`#${id}`)?.remove(); + } + } +} + +declare global { + var RuntimeDefs: RuntimeDefsModule; +} + +if (typeof window !== "undefined") { + window.RuntimeDefs = new RuntimeDefsModule(); +} diff --git a/src/modules/index.ts b/src/modules/index.ts index 07a2a1d2..a5070e65 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -1,6 +1,7 @@ import "./biomes"; import "./burgs-generator"; import "./cultures-generator"; +import "./defs"; import "./emblem"; import "./features"; import "./fonts"; diff --git a/src/renderers/draw-features.ts b/src/renderers/draw-features.ts index 5a6801d8..a8239632 100644 --- a/src/renderers/draw-features.ts +++ b/src/renderers/draw-features.ts @@ -65,9 +65,9 @@ const featuresRenderer = (): void => { } } - defs.select("#featurePaths").html(html.paths.join("")); - defs.select("#land").html(html.landMask.join("")); - defs.select("#water").html(html.waterMask.join("")); + RuntimeDefs.getFeaturePaths().html(html.paths.join("")); + RuntimeDefs.getLandMask().html(html.landMask.join("")); + RuntimeDefs.getWaterMask().html(html.waterMask.join("")); coastline.selectAll("g").each(function () { const paths = html.coastline[this.id] || []; diff --git a/src/renderers/draw-state-labels.ts b/src/renderers/draw-state-labels.ts index 1fd582c5..517e9a7c 100644 --- a/src/renderers/draw-state-labels.ts +++ b/src/renderers/draw-state-labels.ts @@ -117,9 +117,7 @@ const stateLabelsRenderer = (list?: number[]): void => { const lineGen = line<[number, number]>().curve(curveNatural); const textGroup = select("g#labels > g#states"); - const pathGroup = select( - queryMap("defs > g#deftemp > g#textPaths") as SVGGElement | null, - ); + const pathGroup = RuntimeDefs.getTextPaths(); for (const [stateId, pathPoints] of labelPaths) { const state = states[stateId]; diff --git a/src/types/global.ts b/src/types/global.ts index 3e093fa1..66c5b488 100644 --- a/src/types/global.ts +++ b/src/types/global.ts @@ -1,4 +1,5 @@ import type { Selection } from "d3"; +import type { RuntimeDefsModule } from "../modules/defs"; import type { LayersModule } from "../modules/layers"; import type { NameBase } from "../modules/names-generator"; import type { SceneModule } from "../modules/scene"; @@ -94,5 +95,6 @@ declare global { var changeFont: () => void; var getFriendlyHeight: (coords: [number, number]) => string; var Layers: LayersModule; + var RuntimeDefs: RuntimeDefsModule; var Scene: SceneModule; }