mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-04-04 14:37:24 +02:00
feat: Implement RuntimeDefsModule for managing shared runtime definitions and update related components
This commit is contained in:
parent
f928f9d101
commit
73d6d664fc
14 changed files with 126 additions and 43 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
# Story 1.6: Move Shared Defs Resources to the Dedicated Host
|
# Story 1.6: Move Shared Defs Resources to the Dedicated Host
|
||||||
|
|
||||||
Status: ready-for-dev
|
Status: review
|
||||||
|
|
||||||
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
||||||
|
|
||||||
|
|
@ -17,19 +17,19 @@ so that split surfaces can keep using stable IDs for filters, masks, symbols, ma
|
||||||
|
|
||||||
## Tasks / Subtasks
|
## Tasks / Subtasks
|
||||||
|
|
||||||
- [ ] Establish one runtime defs owner.
|
- [x] 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.
|
- [x] 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`.
|
- [x] Distinguish runtime-generated defs from the static asset library already stored in `#defElements`.
|
||||||
- [ ] Migrate the current runtime writers for shared defs-backed resources.
|
- [x] Migrate the current runtime writers for shared defs-backed resources.
|
||||||
- [ ] Move feature paths and masks now written through `defs.select(...)` to the dedicated host.
|
- [x] 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.
|
- [x] 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.
|
- [x] Move runtime masks, markers, or other shared resources that must survive split surfaces.
|
||||||
- [ ] Preserve stable IDs and references.
|
- [x] Preserve stable IDs and references.
|
||||||
- [ ] Keep existing IDs intact wherever possible so current `url(#id)` and `href="#id"` references continue to resolve.
|
- [x] 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.
|
- [x] Avoid duplicating identical resources into per-layer surfaces.
|
||||||
- [ ] Keep export work out of scope for this story.
|
- [x] Keep export work out of scope for this story.
|
||||||
- [ ] Do not redesign the export assembler here.
|
- [x] Do not redesign the export assembler here.
|
||||||
- [ ] Only make the runtime defs placement compatible with later export assembly.
|
- [x] Only make the runtime defs placement compatible with later export assembly.
|
||||||
- [ ] Perform manual smoke verification.
|
- [ ] Perform manual smoke verification.
|
||||||
- [ ] Filters, masks, symbols, markers, patterns, and text-path-backed labels still render.
|
- [ ] 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.
|
- [ ] 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
|
### Agent Model Used
|
||||||
|
|
||||||
TBD
|
Claude Sonnet 4.6
|
||||||
|
|
||||||
### Debug Log References
|
### Debug Log References
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
### Completion Notes List
|
### Completion Notes List
|
||||||
|
|
||||||
- Story context prepared on 2026-03-13.
|
- 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
|
### 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 `<g id="featurePaths">`, `<g id="textPaths">`, `<mask id="land">`, `<mask id="water">` 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`. |
|
||||||
|
|
|
||||||
|
|
@ -41,12 +41,12 @@ story_location: /Users/azgaar/Fantasy-Map-Generator/_bmad-output/implementation-
|
||||||
|
|
||||||
development_status:
|
development_status:
|
||||||
epic-1: in-progress
|
epic-1: in-progress
|
||||||
1-1-bootstrap-scene-container-and-defs-host: in-progress
|
1-1-bootstrap-scene-container-and-defs-host: done
|
||||||
1-2-add-scene-module-for-shared-camera-state: in-progress
|
1-2-add-scene-module-for-shared-camera-state: review
|
||||||
1-3-add-layers-registry-as-the-ordering-source-of-truth: in-progress
|
1-3-add-layers-registry-as-the-ordering-source-of-truth: review
|
||||||
1-4-add-layer-surface-lifecycle-ownership: done
|
1-4-add-layer-surface-lifecycle-ownership: review
|
||||||
1-5-add-compatibility-lookups-for-legacy-single-svg-callers: ready-for-dev
|
1-5-add-compatibility-lookups-for-legacy-single-svg-callers: review
|
||||||
1-6-move-shared-defs-resources-to-the-dedicated-host: ready-for-dev
|
1-6-move-shared-defs-resources-to-the-dedicated-host: review
|
||||||
epic-1-retrospective: optional
|
epic-1-retrospective: optional
|
||||||
|
|
||||||
epic-2: backlog
|
epic-2: backlog
|
||||||
|
|
|
||||||
|
|
@ -197,8 +197,8 @@ export function resolveVersionConflicts(mapVersion) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// v1.1 features stores more data
|
// v1.1 features stores more data
|
||||||
defs.select("#land").selectAll("path").remove();
|
RuntimeDefs.getLandMask().selectAll("path").remove();
|
||||||
defs.select("#water").selectAll("path").remove();
|
RuntimeDefs.getWaterMask().selectAll("path").remove();
|
||||||
coastline.selectAll("path").remove();
|
coastline.selectAll("path").remove();
|
||||||
lakes.selectAll("path").remove();
|
lakes.selectAll("path").remove();
|
||||||
|
|
||||||
|
|
@ -936,10 +936,9 @@ export function resolveVersionConflicts(mapVersion) {
|
||||||
|
|
||||||
if (isOlderThan("1.106.0")) {
|
if (isOlderThan("1.106.0")) {
|
||||||
// v1.104.0 introduced bugs with coastlines. Redraw features
|
// v1.104.0 introduced bugs with coastlines. Redraw features
|
||||||
defs.select("#featurePaths").remove();
|
RuntimeDefs.getFeaturePaths().html("");
|
||||||
defs.append("g").attr("id", "featurePaths");
|
RuntimeDefs.getLandMask().selectAll("path, use").remove();
|
||||||
defs.select("#land").selectAll("path, use").remove();
|
RuntimeDefs.getWaterMask().selectAll("path, use").remove();
|
||||||
defs.select("#water").selectAll("path, use").remove();
|
|
||||||
viewbox.select("#coastline").selectAll("path, use").remove();
|
viewbox.select("#coastline").selectAll("path, use").remove();
|
||||||
|
|
||||||
// v1.104.0 introduced bugs with state borders
|
// v1.104.0 introduced bugs with state borders
|
||||||
|
|
|
||||||
|
|
@ -367,6 +367,8 @@ async function parseLoadedData(data, mapVersion) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RuntimeDefs.purgeMapDefStubs();
|
||||||
|
|
||||||
{
|
{
|
||||||
grid = JSON.parse(data[6]);
|
grid = JSON.parse(data[6]);
|
||||||
const {cells, vertices} = calculateVoronoi(grid.points, grid.boundary);
|
const {cells, vertices} = calculateVoronoi(grid.points, grid.boundary);
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ function editCoastline() {
|
||||||
const feature = features[featureId];
|
const feature = features[featureId];
|
||||||
|
|
||||||
// change coastline path
|
// 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
|
// update area
|
||||||
const points = feature.vertices.map(vertex => vertices.p[vertex]);
|
const points = feature.vertices.map(vertex => vertices.p[vertex]);
|
||||||
|
|
|
||||||
|
|
@ -75,8 +75,9 @@ function editHeightmap(options) {
|
||||||
viewbox.selectAll("#landmass, #lakes").style("display", "none");
|
viewbox.selectAll("#landmass, #lakes").style("display", "none");
|
||||||
changeOnlyLand.checked = true;
|
changeOnlyLand.checked = true;
|
||||||
} else if (mode === "risk") {
|
} else if (mode === "risk") {
|
||||||
defs.selectAll("#land, #water").selectAll("path").remove();
|
RuntimeDefs.getLandMask().selectAll("path").remove();
|
||||||
defs.select("#featurePaths").selectAll("path").remove();
|
RuntimeDefs.getWaterMask().selectAll("path").remove();
|
||||||
|
RuntimeDefs.getFeaturePaths().selectAll("path").remove();
|
||||||
viewbox.selectAll("#coastline use, #lakes path, #oceanLayers path").remove();
|
viewbox.selectAll("#coastline use, #lakes path, #oceanLayers path").remove();
|
||||||
changeOnlyLand.checked = false;
|
changeOnlyLand.checked = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ function editLake() {
|
||||||
const feature = getLake();
|
const feature = getLake();
|
||||||
|
|
||||||
// update lake path
|
// 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
|
// update area
|
||||||
const points = feature.vertices.map(vertex => pack.vertices.p[vertex]);
|
const points = feature.vertices.map(vertex => pack.vertices.p[vertex]);
|
||||||
|
|
|
||||||
|
|
@ -649,8 +649,7 @@ function addLabelOnClick() {
|
||||||
.attr("x", 0)
|
.attr("x", 0)
|
||||||
.text(name);
|
.text(name);
|
||||||
|
|
||||||
defs
|
RuntimeDefs.getTextPaths()
|
||||||
.select("#textPaths")
|
|
||||||
.append("path")
|
.append("path")
|
||||||
.attr("id", "textPath_" + id)
|
.attr("id", "textPath_" + id)
|
||||||
.attr("d", `M${point[0] - width},${point[1]} h${width * 2}`);
|
.attr("d", `M${point[0] - width},${point[1]} h${width * 2}`);
|
||||||
|
|
|
||||||
|
|
@ -370,12 +370,8 @@
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<g id="deftemp">
|
<g id="deftemp">
|
||||||
<g id="featurePaths"></g>
|
|
||||||
<g id="textPaths"></g>
|
|
||||||
<g id="statePaths"></g>
|
<g id="statePaths"></g>
|
||||||
<g id="defs-emblems"></g>
|
<g id="defs-emblems"></g>
|
||||||
<mask id="land"></mask>
|
|
||||||
<mask id="water"></mask>
|
|
||||||
<mask id="fog" style="stroke-width: 10; stroke: black; stroke-linejoin: round; stroke-opacity: 0.1">
|
<mask id="fog" style="stroke-width: 10; stroke: black; stroke-linejoin: round; stroke-opacity: 0.1">
|
||||||
<rect x="0" y="0" width="100%" height="100%" fill="white" stroke="none" />
|
<rect x="0" y="0" width="100%" height="100%" fill="white" stroke="none" />
|
||||||
</mask>
|
</mask>
|
||||||
|
|
|
||||||
57
src/modules/defs.ts
Normal file
57
src/modules/defs.ts
Normal file
|
|
@ -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<SVGGElement, unknown, null, undefined> {
|
||||||
|
return select<SVGGElement, unknown>(this.ensureGroup("featurePaths"));
|
||||||
|
}
|
||||||
|
|
||||||
|
getLandMask(): Selection<SVGMaskElement, unknown, null, undefined> {
|
||||||
|
return select<SVGMaskElement, unknown>(this.ensureMask("land"));
|
||||||
|
}
|
||||||
|
|
||||||
|
getWaterMask(): Selection<SVGMaskElement, unknown, null, undefined> {
|
||||||
|
return select<SVGMaskElement, unknown>(this.ensureMask("water"));
|
||||||
|
}
|
||||||
|
|
||||||
|
getTextPaths(): Selection<SVGGElement, unknown, null, undefined> {
|
||||||
|
return select<SVGGElement, unknown>(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();
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import "./biomes";
|
import "./biomes";
|
||||||
import "./burgs-generator";
|
import "./burgs-generator";
|
||||||
import "./cultures-generator";
|
import "./cultures-generator";
|
||||||
|
import "./defs";
|
||||||
import "./emblem";
|
import "./emblem";
|
||||||
import "./features";
|
import "./features";
|
||||||
import "./fonts";
|
import "./fonts";
|
||||||
|
|
|
||||||
|
|
@ -65,9 +65,9 @@ const featuresRenderer = (): void => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defs.select("#featurePaths").html(html.paths.join(""));
|
RuntimeDefs.getFeaturePaths().html(html.paths.join(""));
|
||||||
defs.select("#land").html(html.landMask.join(""));
|
RuntimeDefs.getLandMask().html(html.landMask.join(""));
|
||||||
defs.select("#water").html(html.waterMask.join(""));
|
RuntimeDefs.getWaterMask().html(html.waterMask.join(""));
|
||||||
|
|
||||||
coastline.selectAll<SVGGElement, unknown>("g").each(function () {
|
coastline.selectAll<SVGGElement, unknown>("g").each(function () {
|
||||||
const paths = html.coastline[this.id] || [];
|
const paths = html.coastline[this.id] || [];
|
||||||
|
|
|
||||||
|
|
@ -117,9 +117,7 @@ const stateLabelsRenderer = (list?: number[]): void => {
|
||||||
const lineGen = line<[number, number]>().curve(curveNatural);
|
const lineGen = line<[number, number]>().curve(curveNatural);
|
||||||
|
|
||||||
const textGroup = select<SVGGElement, unknown>("g#labels > g#states");
|
const textGroup = select<SVGGElement, unknown>("g#labels > g#states");
|
||||||
const pathGroup = select<SVGGElement, unknown>(
|
const pathGroup = RuntimeDefs.getTextPaths();
|
||||||
queryMap("defs > g#deftemp > g#textPaths") as SVGGElement | null,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const [stateId, pathPoints] of labelPaths) {
|
for (const [stateId, pathPoints] of labelPaths) {
|
||||||
const state = states[stateId];
|
const state = states[stateId];
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { Selection } from "d3";
|
import type { Selection } from "d3";
|
||||||
|
import type { RuntimeDefsModule } from "../modules/defs";
|
||||||
import type { LayersModule } from "../modules/layers";
|
import type { LayersModule } from "../modules/layers";
|
||||||
import type { NameBase } from "../modules/names-generator";
|
import type { NameBase } from "../modules/names-generator";
|
||||||
import type { SceneModule } from "../modules/scene";
|
import type { SceneModule } from "../modules/scene";
|
||||||
|
|
@ -94,5 +95,6 @@ declare global {
|
||||||
var changeFont: () => void;
|
var changeFont: () => void;
|
||||||
var getFriendlyHeight: (coords: [number, number]) => string;
|
var getFriendlyHeight: (coords: [number, number]) => string;
|
||||||
var Layers: LayersModule;
|
var Layers: LayersModule;
|
||||||
|
var RuntimeDefs: RuntimeDefsModule;
|
||||||
var Scene: SceneModule;
|
var Scene: SceneModule;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue