diff --git a/_bmad-output/implementation-artifacts/sprint-status.yaml b/_bmad-output/implementation-artifacts/sprint-status.yaml new file mode 100644 index 00000000..b73ef0c6 --- /dev/null +++ b/_bmad-output/implementation-artifacts/sprint-status.yaml @@ -0,0 +1,61 @@ +# generated: 2026-03-12 +# project: Fantasy-Map-Generator +# project_key: NOKEY +# tracking_system: file-system +# story_location: _bmad-output/implementation-artifacts + +# STATUS DEFINITIONS: +# ================== +# Epic Status: +# - backlog: Epic not yet started +# - in-progress: Epic actively being worked on +# - done: All stories in epic completed +# +# Epic Status Transitions: +# - backlog → in-progress: Automatically when first story is created (via create-story) +# - in-progress → done: Manually when all stories reach 'done' status +# +# Story Status: +# - backlog: Story only exists in epic file +# - ready-for-dev: Story file created in stories folder +# - in-progress: Developer actively working on implementation +# - review: Ready for code review (via Dev's code-review workflow) +# - done: Story completed +# +# Retrospective Status: +# - optional: Can be completed but not required +# - done: Retrospective has been completed +# +# WORKFLOW NOTES: +# =============== +# - Epic transitions to 'in-progress' automatically when first story is created +# - Stories can be worked in parallel if team capacity allows +# - SM typically creates next story after previous one is 'done' to incorporate learnings +# - Dev moves story to 'review', then runs code-review (fresh context, different LLM recommended) + +generated: 2026-03-12 +project: Fantasy-Map-Generator +project_key: NOKEY +tracking_system: file-system +story_location: _bmad-output/implementation-artifacts + +development_status: + # Epic 1: WebGL Layer Framework Module + epic-1: backlog + 1-1-pure-functions-types-and-tdd-scaffold: backlog + 1-2-framework-core-init-canvas-and-dom-setup: backlog + 1-3-layer-lifecycle-register-visibility-render-loop: backlog + epic-1-retrospective: optional + + # Epic 2: Relief Icons Layer Migration + epic-2: backlog + 2-1-verify-and-implement-per-icon-rotation-in-buildsetmesh: backlog + 2-2-refactor-draw-relief-icons-ts-to-use-framework: backlog + 2-3-webgl2-fallback-integration-verification: backlog + epic-2-retrospective: optional + + # Epic 3: Quality & Bundle Integrity + epic-3: backlog + 3-1-performance-benchmarking: backlog + 3-2-bundle-size-audit: backlog + epic-3-retrospective: optional diff --git a/_bmad-output/planning-artifacts/epics.md b/_bmad-output/planning-artifacts/epics.md new file mode 100644 index 00000000..aac059be --- /dev/null +++ b/_bmad-output/planning-artifacts/epics.md @@ -0,0 +1,422 @@ +--- +stepsCompleted: + - "step-01-validate-prerequisites" + - "step-02-design-epics" + - "step-03-create-stories-epic1" + - "step-03-create-stories-epic2" + - "step-03-create-stories-epic3" + - "step-04-final-validation" +inputDocuments: + - "_bmad-output/planning-artifacts/prd.md" + - "_bmad-output/planning-artifacts/architecture.md" +--- + +# Fantasy-Map-Generator - Epic Breakdown + +## Overview + +This document provides the complete epic and story breakdown for Fantasy-Map-Generator, decomposing the requirements from the PRD and Architecture into implementable stories. + +## Requirements Inventory + +### Functional Requirements + +FR1: The system can initialize a single WebGL2 rendering context that is shared across all registered WebGL layers +FR2: The framework can insert a `` element into the map container at a z-index position corresponding to a named anchor SVG layer's position in the visual stack +FR3: The framework can register a new WebGL layer by accepting an anchor SVG layer ID and a render callback function +FR4: The framework can maintain a registry of all registered WebGL layers and their current z-index positions +FR5: The framework can synchronize the WebGL rendering viewport to the current D3 zoom transform (translate x, translate y, scale k) applied to the SVG viewbox group +FR6: The framework can update the WebGL transform when the D3 zoom or pan state changes +FR7: The framework can convert any map-space coordinate (SVG viewport space) to the correct WebGL clip-space coordinate at any zoom level +FR8: Users can toggle individual WebGL layer visibility on and off without destroying GPU buffer state or requiring a re-upload of vertex/instance data +FR9: The framework can resize the canvas element and update the WebGL viewport to match the SVG viewport dimensions when the browser window or map container is resized +FR10: The framework can recalculate a WebGL layer's z-index to account for changes in the SVG layer stack order +FR11: The framework can dispose of a registered WebGL layer and release its associated GPU resources +FR12: The system can render all relief icon types from the existing relief atlas texture using instanced rendering in a single GPU draw call +FR13: The system can position each relief icon at the SVG-space coordinate of its corresponding terrain cell +FR14: The system can scale each relief icon according to the current map zoom level and the user's configured icon scale setting +FR15: The system can apply per-icon rotation as defined in the terrain dataset +FR16: The system can render relief icons with a configurable opacity value +FR17: The relief layer can re-render when the terrain dataset changes (cells added, removed, or type changed) +FR18: The system can detect when WebGL2 is unavailable in the current browser and automatically fall back to the existing SVG-based relief renderer +FR19: The SVG fallback renderer produces visually identical output to the WebGL renderer from the user's perspective +FR20: Users can interact with all SVG map layers (click, drag, hover, editor panels) without the WebGL canvas intercepting pointer or touch events +FR21: Users can control WebGL-rendered layer visibility and style properties using the existing Layers panel controls with no change to the UI +FR22: A developer can register a new WebGL layer by providing only an anchor SVG layer ID and a render callback — no knowledge of z-index calculation or canvas lifecycle is required +FR23: A render callback receives the current D3 transform state so it can apply coordinate synchronization without accessing global state +FR24: A developer can use the same visibility toggle and dispose APIs for custom registered layers as for the built-in relief layer +FR25: The coordinate synchronization logic can be exercised in a Vitest unit test by passing a mock D3 transform and asserting the resulting WebGL projection values +FR26: The WebGL2 fallback detection can be exercised in a Vitest unit test by mocking `canvas.getContext('webgl2')` to return null +FR27: The layer registration API can be exercised in a Vitest unit test without a real browser WebGL context using a stub renderer + +### NonFunctional Requirements + +NFR-P1: Relief layer initial render (1,000 icons) completes in <16ms — measured via Vitest benchmark / browser DevTools frame timing +NFR-P2: Relief layer initial render (10,000 icons) completes in <100ms — measured via Vitest benchmark / browser DevTools frame timing +NFR-P3: Layer visibility toggle (show/hide) completes in <4ms — measured via `performance.now()` around toggle call +NFR-P4: D3 zoom/pan event → WebGL canvas transform update latency <8ms — measured from zoom event callback to draw call completion +NFR-P5: WebGL context initialization (one-time) completes in <200ms — measured via `performance.now()` on first map load +NFR-P6: No GPU state teardown on layer hide — VBO/texture memory stays allocated; verified via browser GPU memory profiler +NFR-C1: WebGL2 context (`canvas.getContext('webgl2')`) is the sole gating check; if null, SVG fallback activates automatically with no user-visible error +NFR-C2: The framework produces identical visual output across Chrome 69+, Firefox 105+, Safari 16.4+, Edge 79+ +NFR-C3: No more than 2 WebGL contexts are open simultaneously (1 for globe, 1 for map) +NFR-C4: The framework does not break if the user has hardware acceleration disabled (falls back to SVG) +NFR-M1: The framework core (`WebGL2LayerFramework` class) has no knowledge of any specific layer's content — all layer-specific logic lives in the layer's render callback +NFR-M2: Adding a new WebGL layer requires only: one call to `framework.register(config)` and implementing the render callback — no changes to framework internals +NFR-M3: The TypeScript module follows the existing project Global Module Pattern (`declare global { var WebGL2LayerFramework: ... }`) +NFR-M4: The coordinate sync formula (D3 transform → WebGL orthographic projection) is documented in code comments with the mathematical derivation +NFR-M5: Vitest unit test coverage ≥80% for the framework core module (`src/modules/webgl-layer-framework.ts`) +NFR-B1: Three.js import uses tree-shaking — only required classes imported (`import { WebGLRenderer, ... } from 'three'`), not the full bundle +NFR-B2: Total Vite bundle size increase from this feature ≤50KB gzipped (Three.js is already a project dependency for the globe view) + +### Additional Requirements + +- **Brownfield integration**: No starter template; the framework is inserted into an existing codebase. `public/modules/` legacy JS must not be modified. +- **Global Module Pattern (mandatory)**: `window.WebGL2LayerFramework = new WebGL2LayerFrameworkClass()` must be the last line of the framework module; module added to `src/modules/index.ts` as side-effect import before renderer imports. +- **Canvas id convention**: Framework derives canvas element id as `${config.id}Canvas` (e.g., `id: "terrain"` → `canvas#terrainCanvas`). Never hardcoded by layer code. +- **DOM wrapper required**: Framework wraps existing `svg#map` in a new `div#map-container` (`position: relative`) on `init()`. Canvas is sibling to `#map` inside this container. +- **Canvas styling (mandatory)**: `position: absolute; inset: 0; pointer-events: none; aria-hidden: true; z-index: 2` +- **`hasFallback` backing field pattern**: Must use `private _fallback = false` + `get hasFallback(): boolean` — NOT `readonly hasFallback: boolean = false` (TypeScript compile error if set in `init()`). +- **`pendingConfigs[]` queue**: `register()` before `init()` is explicitly supported by queueing configs; `init()` processes the queue. Module load order is intentionally decoupled from DOM/WebGL readiness. +- **Window globals preserved**: `window.drawRelief`, `window.undrawRelief`, `window.rerenderReliefIcons` must remain as window globals for backward compatibility with legacy JS callers. +- **`undrawRelief` must call `clearLayer()`**: Does NOT call `renderer.dispose()`. Wipes group geometry only; layer remains registered. +- **Exported pure functions for testability**: `buildCameraBounds`, `detectWebGL2`, `getLayerZIndex` must be named exports testable without DOM or WebGL. +- **FR15 rotation pre-verification**: Per-icon rotation support in `buildSetMesh` must be verified before MVP ships; rotation attribute must be added if missing. +- **TypeScript linting**: `Number.isNaN()` not `isNaN()`; `parseInt()` requires radix; named Three.js imports only — no `import * as THREE`. +- **ResizeObserver**: Attached to `#map-container` in `init()`; calls `requestRender()` on resize. +- **D3 zoom subscription**: `viewbox.on("zoom.webgl", () => this.requestRender())` established in `init()`. + +### FR Coverage Map + +| Epic | Story | FRs Covered | NFRs Addressed | +| ------------------------------------ | --------------------------------------------- | ----------------------------------------------------------- | -------------------------------------- | +| Epic 1: WebGL Layer Framework Module | Story 1.1: Pure Functions & Types | FR7, FR25, FR26 | NFR-M4, NFR-M5 | +| Epic 1: WebGL Layer Framework Module | Story 1.2: Framework Init & DOM Setup | FR1, FR2, FR9, FR18 | NFR-P5, NFR-C1, NFR-C3, NFR-C4, NFR-M3 | +| Epic 1: WebGL Layer Framework Module | Story 1.3: Layer Lifecycle & Render Loop | FR3, FR4, FR5, FR6, FR8, FR10, FR11, FR22, FR23, FR24, FR27 | NFR-P3, NFR-P4, NFR-P6, NFR-M1, NFR-M2 | +| Epic 2: Relief Icons Layer Migration | Story 2.1: buildSetMesh Rotation Verification | FR15 | — | +| Epic 2: Relief Icons Layer Migration | Story 2.2: Refactor draw-relief-icons.ts | FR12, FR13, FR14, FR15, FR16, FR17, FR19, FR20, FR21 | NFR-P1, NFR-P2, NFR-C2 | +| Epic 2: Relief Icons Layer Migration | Story 2.3: WebGL2 Fallback Integration | FR18, FR19 | NFR-C1, NFR-C4 | +| Epic 3: Quality & Bundle Integrity | Story 3.1: Performance Benchmarking | — | NFR-P1, NFR-P2, NFR-P3, NFR-P4, NFR-P5 | +| Epic 3: Quality & Bundle Integrity | Story 3.2: Bundle Size Audit | — | NFR-B1, NFR-B2 | + +## Epic List + +- **Epic 1:** WebGL Layer Framework Module +- **Epic 2:** Relief Icons Layer Migration +- **Epic 3:** Quality & Bundle Integrity + +--- + +## Epic 1: WebGL Layer Framework Module + +**Goal:** Implement the generic `WebGL2LayerFrameworkClass` TypeScript module that provides canvas lifecycle management, z-index positioning, D3 zoom/pan synchronization, layer registration API, visibility toggle, and all supporting infrastructure. This is the platform foundation — all future layer migrations depend on it. + +### Story 1.1: Pure Functions, Types, and TDD Scaffold + +As a developer, +I want `buildCameraBounds`, `detectWebGL2`, and `getLayerZIndex` implemented as named-exported pure functions with full Vitest coverage, +So that coordinate sync and WebGL detection logic are verified in isolation before the class is wired up. + +**Acceptance Criteria:** + +**Given** the file `src/modules/webgl-layer-framework.ts` does not yet exist +**When** the developer creates it with `WebGLLayerConfig` interface, `RegisteredLayer` interface, and the three pure exported functions +**Then** the file compiles with zero TypeScript errors and `npm run lint` passes + +**Given** `buildCameraBounds(viewX, viewY, scale, graphWidth, graphHeight)` is implemented +**When** called with identity transform `(0, 0, 1, 960, 540)` +**Then** it returns `{left: 0, right: 960, top: 0, bottom: 540}` and `top < bottom` (Y-down convention) + +**Given** `buildCameraBounds` is called with `(0, 0, 2, 960, 540)` (2× zoom) +**When** asserting bounds +**Then** `right === 480` and `bottom === 270` (viewport shows half the map) + +**Given** `buildCameraBounds` is called with `(-100, -50, 1, 960, 540)` (panned right/down) +**When** asserting bounds +**Then** `left === 100` and `top === 50` + +**Given** `buildCameraBounds` is called with extreme zoom values `(0.1)` and `(50)` +**When** asserting results +**Then** all returned values are finite (no `NaN` or `Infinity`) + +**Given** a mock canvas where `getContext('webgl2')` returns `null` +**When** `detectWebGL2(mockCanvas)` is called +**Then** it returns `false` + +**Given** a mock canvas where `getContext('webgl2')` returns a mock context object +**When** `detectWebGL2(mockCanvas)` is called +**Then** it returns `true` + +**Given** `getLayerZIndex('terrain')` is called +**When** the `#terrain` element is not present in the DOM +**Then** it returns `2` (safe fallback) + +**Given** a Vitest test file `src/modules/webgl-layer-framework.test.ts` exists +**When** `npx vitest run` is executed +**Then** all tests in this file pass and coverage for pure functions is 100% + +--- + +### Story 1.2: Framework Core — Init, Canvas, and DOM Setup + +As a developer, +I want `WebGL2LayerFrameworkClass.init()` to set up the WebGL2 renderer, wrap `#map` in `#map-container`, insert the canvas, attach a `ResizeObserver`, and subscribe to D3 zoom events, +So that any registered layer can render correctly at any zoom level on any screen size. + +**Acceptance Criteria:** + +**Given** `WebGL2LayerFramework.init()` is called and WebGL2 is available +**When** the DOM is inspected +**Then** `div#map-container` exists with `position: relative`, `svg#map` is a child at `z-index: 1`, and `canvas#terrainCanvas` is a sibling at `z-index: 2` with `pointer-events: none` and `aria-hidden: true` + +**Given** `WebGL2LayerFramework.init()` is called +**When** `detectWebGL2()` returns `false` (WebGL2 unavailable) +**Then** `init()` returns `false`, `framework.hasFallback === true`, and all subsequent API calls on the framework are no-ops + +**Given** `hasFallback` is declared as a private backing field `private _fallback = false` with public getter `get hasFallback(): boolean` +**When** `init()` sets `_fallback = !detectWebGL2()` +**Then** the TypeScript compiler produces zero errors (compared to `readonly` which would fail) + +**Given** `WebGL2LayerFramework.init()` completes successfully +**When** the framework's private state is inspected +**Then** exactly one `THREE.WebGLRenderer` instance exists, one `THREE.Scene`, and one `THREE.OrthographicCamera` — no duplicates + +**Given** a `ResizeObserver` is attached to `#map-container` during `init()` +**When** the container's dimensions change +**Then** `renderer.setSize(width, height)` is called and `requestRender()` is triggered + +**Given** D3 zoom subscription `viewbox.on("zoom.webgl", ...)` is established in `init()` +**When** a D3 zoom or pan event fires +**Then** `requestRender()` is called, coalescing into a single RAF + +**Given** `WebGL2LayerFrameworkClass` is instantiated (constructor runs) +**When** `init()` has NOT been called yet +**Then** `renderer`, `scene`, `camera`, and `canvas` are all `null` — constructor performs no side effects + +**Given** `init()` is called +**When** measuring elapsed time via `performance.now()` +**Then** initialization completes in <200ms (NFR-P5) + +**Given** `window.WebGL2LayerFramework = new WebGL2LayerFrameworkClass()` is the last line of the module +**When** the module is loaded via `src/modules/index.ts` +**Then** the global is immediately accessible as `window.WebGL2LayerFramework` following the Global Module Pattern + +--- + +### Story 1.3: Layer Lifecycle — Register, Visibility, Render Loop + +As a developer, +I want `register()`, `unregister()`, `setVisible()`, `clearLayer()`, `requestRender()`, `syncTransform()`, and the per-frame render dispatch implemented, +So that multiple layers can be registered, rendered, shown/hidden, and cleaned up without GPU state loss. + +**Acceptance Criteria:** + +**Given** `register(config)` is called before `init()` +**When** `init()` is subsequently called +**Then** the config is queued in `pendingConfigs[]` and processed by `init()` without error — `register()` before `init()` is explicitly safe + +**Given** `register(config)` is called after `init()` +**When** the framework state is inspected +**Then** a `THREE.Group` with `config.renderOrder` is created, `config.setup(group)` is called once, the group is added to the scene, and the registration is stored in `layers: Map` + +**Given** `setVisible('terrain', false)` is called +**When** the framework internals are inspected +**Then** `layer.group.visible === false`, `config.dispose` is NOT called (no GPU teardown), and the canvas is hidden only if ALL layers are invisible + +**Given** `setVisible('terrain', true)` is called after hiding +**When** the layer is toggled back on +**Then** `layer.group.visible === true` and `requestRender()` is triggered — toggle completes in <4ms (NFR-P3) + +**Given** `clearLayer('terrain')` is called +**When** the group state is inspected +**Then** `group.clear()` has been called (all Mesh children removed), the layer registration in `layers: Map` remains intact, and `renderer.dispose()` is NOT called + +**Given** `requestRender()` is called three times in rapid succession +**When** `requestAnimationFrame` spy is observed +**Then** only one RAF is scheduled (coalescing confirmed) + +**Given** `render()` private method is invoked (via RAF callback) +**When** executing the frame +**Then** `syncTransform()` is called first, then each visible layer's `render(group)` callback is dispatched, then `renderer.render(scene, camera)` is called — order is enforced + +**Given** `syncTransform()` is called with `viewX = 0, viewY = 0, scale = 1` globals +**When** the camera bounds are applied +**Then** the orthographic camera's left/right/top/bottom match `buildCameraBounds(0, 0, 1, graphWidth, graphHeight)` exactly (D3 transform → camera sync formula) + +**Given** a Vitest test exercises `register()`, `setVisible()`, and `requestRender()` with stub scene/renderer +**When** `npx vitest run` is executed +**Then** all tests pass; framework coverage ≥80% (NFR-M5) + +**Given** layer callbacks receive a `THREE.Group` from `register()` +**When** layer code is written +**Then** `scene`, `renderer`, and `camera` are never exposed to layer callbacks — `THREE.Group` is the sole abstraction boundary (NFR-M1) + +--- + +## Epic 2: Relief Icons Layer Migration + +**Goal:** Refactor `src/renderers/draw-relief-icons.ts` to register with the `WebGL2LayerFramework` instead of managing its own `THREE.WebGLRenderer`. Verify and implement per-icon rotation (FR15). Preserve all existing window globals (`drawRelief`, `undrawRelief`, `rerenderReliefIcons`) for backward compatibility with legacy callers. + +### Story 2.1: Verify and Implement Per-Icon Rotation in buildSetMesh + +As a developer, +I want to verify that `buildSetMesh` in `draw-relief-icons.ts` correctly applies per-icon rotation from terrain data, and add rotation support if missing, +So that relief icons render with correct orientations matching the SVG baseline (FR15). + +**Acceptance Criteria:** + +**Given** the existing `buildSetMesh` implementation in `draw-relief-icons.ts` +**When** the developer reviews the vertex construction code +**Then** it is documented whether `r.i` (rotation angle) is currently applied to quad vertex positions + +**Given** rotation is NOT applied in the current `buildSetMesh` +**When** the developer adds per-icon rotation via vertex transformation (rotate the quad around its center point using the angle from `pack.relief[n].i`) +**Then** `buildSetMesh` produces correctly oriented quads and `npm run lint` passes + +**Given** rotation IS already applied in the current `buildSetMesh` +**When** verified +**Then** no code change is needed and this is documented in a code comment + +**Given** the rotation fix is applied (if needed) +**When** a visual comparison is made between WebGL-rendered icons and SVG-rendered icons for a map with rotated terrain icons +**Then** orientations are visually indistinguishable + +--- + +### Story 2.2: Refactor draw-relief-icons.ts to Use Framework + +As a developer, +I want `draw-relief-icons.ts` refactored to register with `WebGL2LayerFramework` via `framework.register({ id: 'terrain', ... })` and remove its module-level `THREE.WebGLRenderer` state, +So that the framework owns the single shared WebGL context and the relief layer uses the framework's lifecycle API. + +**Acceptance Criteria:** + +**Given** `draw-relief-icons.ts` is refactored +**When** the module loads +**Then** `WebGL2LayerFramework.register({ id: 'terrain', anchorLayerId: 'terrain', renderOrder: ..., setup, render, dispose })` is called at module load time — before `init()` is ever called (safe via `pendingConfigs[]` queue) + +**Given** the framework takes ownership of the WebGL renderer +**When** `draw-relief-icons.ts` is inspected +**Then** no module-level `THREE.WebGLRenderer`, `THREE.Scene`, or `THREE.OrthographicCamera` instances exist in the module + +**Given** `window.drawRelief()` is called (WebGL path) +**When** execution runs +**Then** `buildReliefScene(icons)` adds `Mesh` objects to the framework-managed group and calls `WebGL2LayerFramework.requestRender()` — no renderer setup or context creation occurs + +**Given** `window.undrawRelief()` is called +**When** execution runs +**Then** `WebGL2LayerFramework.clearLayer('terrain')` is called (wipes group geometry only), SVG terrain innerHTML is cleared, and `renderer.dispose()` is NOT called + +**Given** `window.rerenderReliefIcons()` is called +**When** execution runs +**Then** it calls `WebGL2LayerFramework.requestRender()` — RAF-coalesced, no redundant draws + +**Given** `window.drawRelief(type, parentEl)` is called with `type = 'svg'` or when `hasFallback === true` +**When** execution runs +**Then** `drawSvgRelief(icons, parentEl)` is called (existing SVG renderer), WebGL path is bypassed entirely + +**Given** the refactored module is complete +**When** `npm run lint` and `npx vitest run` are executed +**Then** zero linting errors and all tests pass + +**Given** relief icons are rendered on a map with 1,000 terrain cells +**When** measuring render time +**Then** initial render completes in <16ms (NFR-P1) + +--- + +### Story 2.3: WebGL2 Fallback Integration Verification + +As a developer, +I want the WebGL2 → SVG fallback path end-to-end verified, +So that users on browsers without WebGL2 (or with hardware acceleration disabled) see identical map output via the SVG renderer. + +**Acceptance Criteria:** + +**Given** a Vitest test that mocks `canvas.getContext('webgl2')` to return `null` +**When** `WebGL2LayerFramework.init()` is called +**Then** `hasFallback === true`, `init()` returns `false`, and the framework DOM setup (map-container wrapping, canvas insertion) does NOT occur + +**Given** `hasFallback === true` +**When** `WebGL2LayerFramework.register()`, `setVisible()`, `clearLayer()`, and `requestRender()` are called +**Then** all calls are silent no-ops — no exceptions thrown + +**Given** `window.drawRelief()` is called and `hasFallback === true` +**When** execution runs +**Then** `drawSvgRelief(icons, parentEl)` is invoked and SVG nodes are appended to the terrain layer — visually identical to the current implementation (FR19) + +**Given** SVG fallback is active +**When** a visually rendered map is compared against the current SVG baseline +**Then** icon positions, sizes, and orientations are pixel-indistinguishable (FR19) + +**Given** the fallback test is added to `webgl-layer-framework.test.ts` +**When** `npx vitest run` executes +**Then** the fallback detection test passes (FR26) + +--- + +## Epic 3: Quality & Bundle Integrity + +**Goal:** Validate that all performance, bundle size, and compatibility NFRs are met. Measure baseline performance, verify tree-shaking, confirm the Vite bundle delta is within budget, and document test results. + +### Story 3.1: Performance Benchmarking + +As a developer, +I want baseline and post-migration render performance measured and documented, +So that we can confirm the WebGL implementation meets all NFR performance targets. + +**Acceptance Criteria:** + +**Given** a map generated with 1,000 terrain icons (relief cells) +**When** `window.drawRelief()` is called and render time is measured via `performance.now()` +**Then** initial render time is recorded as the baseline and the WebGL render completes in <16ms (NFR-P1) + +**Given** a map generated with 10,000 terrain icons +**When** `window.drawRelief()` is called +**Then** render time is recorded and completes in <100ms (NFR-P2) + +**Given** the terrain layer is currently visible +**When** `framework.setVisible('terrain', false)` is called and measured +**Then** toggle completes in <4ms (NFR-P3) + +**Given** a D3 zoom event fires +**When** the transform update propagates through to `gl.drawArraysInstanced` +**Then** latency is <8ms (NFR-P4) + +**Given** `WebGL2LayerFramework.init()` is called cold (first page load) +**When** measured via `performance.now()` +**Then** initialization completes in <200ms (NFR-P5) + +**Given** the terrain layer is hidden (via `setVisible(false)`) +**When** the browser GPU memory profiler is observed +**Then** VBO and texture memory is NOT released — GPU state preserved (NFR-P6) + +**Given** benchmark results are collected +**When** documented +**Then** baseline SVG render time vs. WebGL render time is recorded with >80% reduction for 5,000+ icons confirmed + +--- + +### Story 3.2: Bundle Size Audit + +As a developer, +I want the Vite production bundle analyzed to confirm Three.js tree-shaking is effective and the total bundle size increase is within budget, +So that the feature does not negatively impact page load performance. + +**Acceptance Criteria:** + +**Given** `vite build` is run with the complete implementation +**When** the bundle output is analyzed (e.g., `npx vite-bundle-visualizer` or `rollup-plugin-visualizer`) +**Then** Three.js named imports confirm only the required classes are included: `WebGLRenderer, Scene, OrthographicCamera, BufferGeometry, BufferAttribute, Mesh, MeshBasicMaterial, TextureLoader, SRGBColorSpace, LinearMipmapLinearFilter, LinearFilter, DoubleSide` + +**Given** the bundle size before and after the feature is compared +**When** gzip sizes are measured +**Then** the total bundle size increase is ≤50KB gzipped (NFR-B2) + +**Given** `webgl-layer-framework.ts` source is inspected +**When** Three.js imports are reviewed +**Then** no `import * as THREE from 'three'` exists — all imports are named (NFR-B1) + +**Given** the bundle audit completes +**When** results are documented +**Then** actual gzip delta is recorded and compared to the 50KB budget diff --git a/_bmad-output/planning-artifacts/implementation-readiness-report-2026-03-12.md b/_bmad-output/planning-artifacts/implementation-readiness-report-2026-03-12.md new file mode 100644 index 00000000..a86b132d --- /dev/null +++ b/_bmad-output/planning-artifacts/implementation-readiness-report-2026-03-12.md @@ -0,0 +1,229 @@ +# Implementation Readiness Assessment Report + +**Date:** 2026-03-12 +**Project:** Fantasy-Map-Generator + +--- + +## Document Inventory + +| Document | File | Status | +| --------------- | ------------------------------------------------- | ------------------------------------------------------- | +| PRD | `_bmad-output/planning-artifacts/prd.md` | ✅ Found (whole) | +| Architecture | `_bmad-output/planning-artifacts/architecture.md` | ✅ Found (whole) | +| Epics & Stories | `_bmad-output/planning-artifacts/epics.md` | ✅ Found (whole) | +| UX Design | — | ⚠️ Not found (desktop-first tool, no UX doc — expected) | + +--- + +## PRD Analysis + +**Total FRs: 27 (FR1–FR27)** +**Total NFRs: 17 (NFR-P1–P6, NFR-C1–C4, NFR-M1–M5, NFR-B1–B2)** + +PRD completeness: Complete. All requirements clearly numbered, testable, and scoped to the brownfield WebGL layer framework feature. + +--- + +## Epic Coverage Validation + +### FR Coverage Matrix + +| FR | PRD Requirement (summary) | Epic / Story | Status | +| ---- | ------------------------------------------------------------------ | --------------------------------------- | ---------- | +| FR1 | Single shared WebGL2 context | Epic 1 / Story 1.2 | ✅ Covered | +| FR2 | Canvas at z-index derived from anchor SVG layer | Epic 1 / Story 1.2 | ✅ Covered | +| FR3 | Register layer by anchor ID + render callback | Epic 1 / Story 1.3 | ✅ Covered | +| FR4 | Maintain registry of all registered layers | Epic 1 / Story 1.3 | ✅ Covered | +| FR5 | Sync WebGL viewport to D3 zoom transform | Epic 1 / Story 1.3 | ✅ Covered | +| FR6 | Update WebGL transform on D3 zoom/pan change | Epic 1 / Story 1.3 | ✅ Covered | +| FR7 | Convert map-space → WebGL clip-space coordinates | Epic 1 / Story 1.1 | ✅ Covered | +| FR8 | Toggle layer visibility without GPU teardown | Epic 1 / Story 1.3 | ✅ Covered | +| FR9 | Resize canvas on SVG viewport change | Epic 1 / Story 1.2 | ✅ Covered | +| FR10 | Recalculate z-index on layer stack reorder | Epic 1 / Story 1.3 | ✅ Covered | +| FR11 | Dispose registered layer + release GPU resources | Epic 1 / Story 1.3 | ✅ Covered | +| FR12 | Render all relief icons via instanced rendering (single draw call) | Epic 2 / Story 2.2 | ✅ Covered | +| FR13 | Position each relief icon at SVG-space cell coordinate | Epic 2 / Story 2.2 | ✅ Covered | +| FR14 | Scale icons per zoom level and user scale setting | Epic 2 / Story 2.2 | ✅ Covered | +| FR15 | Per-icon rotation from terrain dataset | Epic 2 / Stories 2.1 + 2.2 | ✅ Covered | +| FR16 | Configurable opacity on relief icons | Epic 2 / Story 2.2 | ✅ Covered | +| FR17 | Re-render when terrain dataset changes | Epic 2 / Story 2.2 | ✅ Covered | +| FR18 | Detect WebGL2 unavailable → auto SVG fallback | Epic 1 / Story 1.2 + Epic 2 / Story 2.3 | ✅ Covered | +| FR19 | SVG fallback visually identical to WebGL output | Epic 2 / Stories 2.2 + 2.3 | ✅ Covered | +| FR20 | Canvas `pointer-events: none` — SVG layers remain interactive | Epic 2 / Story 2.2 | ✅ Covered | +| FR21 | Existing Layers panel controls work unchanged | Epic 2 / Story 2.2 | ✅ Covered | +| FR22 | Register new layer without z-index/lifecycle knowledge | Epic 1 / Story 1.3 | ✅ Covered | +| FR23 | Render callback receives D3 transform state | Epic 1 / Story 1.3 | ✅ Covered | +| FR24 | Same visibility/dispose API for all layers | Epic 1 / Story 1.3 | ✅ Covered | +| FR25 | Coordinate sync testable via Vitest mock transform | Epic 1 / Story 1.1 | ✅ Covered | +| FR26 | WebGL2 fallback testable via mock canvas | Epic 1 / Story 1.1 + Epic 2 / Story 2.3 | ✅ Covered | +| FR27 | Registration API testable without real WebGL context | Epic 1 / Story 1.3 | ✅ Covered | + +**FR Coverage: 27/27 — 100% ✅** + +### NFR Coverage Matrix + +| NFR | Requirement | Story | Status | +| ------ | ----------------------------------------- | -------------- | ---------- | +| NFR-P1 | <16ms @ 1k icons | Story 2.2, 3.1 | ✅ Covered | +| NFR-P2 | <100ms @ 10k icons | Story 3.1 | ✅ Covered | +| NFR-P3 | Toggle <4ms | Story 1.3, 3.1 | ✅ Covered | +| NFR-P4 | Pan/zoom latency <8ms | Story 1.3, 3.1 | ✅ Covered | +| NFR-P5 | Init <200ms | Story 1.2, 3.1 | ✅ Covered | +| NFR-P6 | No GPU teardown on hide | Story 1.3, 3.1 | ✅ Covered | +| NFR-C1 | WebGL2 sole gate, SVG fallback on null | Story 1.2, 2.3 | ✅ Covered | +| NFR-C2 | Cross-browser visual parity | Story 2.2 | ✅ Covered | +| NFR-C3 | Max 2 WebGL contexts | Story 1.2 | ✅ Covered | +| NFR-C4 | Fallback when HW accel disabled | Story 2.3 | ✅ Covered | +| NFR-M1 | Framework has no layer-specific knowledge | Story 1.3 | ✅ Covered | +| NFR-M2 | New layer = 1 register() call | Story 1.3 | ✅ Covered | +| NFR-M3 | Global Module Pattern | Story 1.2 | ✅ Covered | +| NFR-M4 | Sync formula documented in code | Story 1.1 | ✅ Covered | +| NFR-M5 | ≥80% Vitest coverage on framework core | Story 1.1, 1.3 | ✅ Covered | +| NFR-B1 | Named Three.js imports only | Story 3.2 | ✅ Covered | +| NFR-B2 | ≤50KB gzip bundle increase | Story 3.2 | ✅ Covered | + +**NFR Coverage: 17/17 — 100% ✅** + +**Missing Requirements: NONE** + +--- + +## UX Alignment Assessment + +### UX Document Status + +Not found — **expected and acceptable.** This project introduces a WebGL rendering layer into a developer/worldbuilder tool. The PRD explicitly states: "No new keyboard shortcuts or UI controls are introduced by the framework itself" and "The existing layer visibility toggle is reused." The canvas element carries `aria-hidden="true"` (purely decorative/visual). No user-facing UI changes are in scope for this feature. + +### Alignment Issues + +None. All user-interaction requirements (FR20, FR21) are captured in Story 2.2 with specific, testable ACs. The Layers panel is unchanged by design. `pointer-events: none` on the canvas is validated in Story 1.2 DOM setup. + +### Warnings + +⚠️ Minor: If future phases (Phase 2 DOM-split, Phase 3 full GPU migration) introduce user-facing controls or new panel elements, a UX document should be created at that time. No action required for MVP. + +--- + +## Epic Quality Review + +### Epic Structure Validation + +#### Epic 1: WebGL Layer Framework Module + +- **User value:** ⚠️ This is a technical foundation epic. However, for this brownfield project type, this is correct and necessary — the user value is delivered by Epic 2 (fast terrain rendering); Epic 1 is the required platform. Architecture explicitly calls this a "Platform MVP." Categorized as acceptable for this project context. +- **Independence:** ✅ Epic 1 stands fully alone. All three stories within it are sequentially independent. +- **Brownfield indicator:** ✅ No "set up from starter template" story needed — this is brownfield insertion. The framework is added to an existing codebase. + +#### Epic 2: Relief Icons Layer Migration + +- **User value:** ✅ Clear user outcome — worldbuilders experience fast terrain rendering with no perceived lag. Journey 1 (Katrin's dense continent) maps directly here. +- **Independence:** ✅ Uses Epic 1 output only. No forward dependency on Epic 3. +- **Story 2.1→2.2 dependency:** ✅ Correct sequence. Story 2.1 (rotation verification) is a prerequisite investigation that 2.2 builds upon — this is a valid intra-epic sequential dependency, not a forward dependency. + +#### Epic 3: Quality & Bundle Integrity + +- **User value:** ⚠️ No direct end-user value — these are quality gates. However, for a performance-critical feature with hard NFR targets, a dedicated validation epic is standard and warranted. The NFR targets (16ms, 100ms, 50KB) are measurable commitments. +- **Independence:** ✅ Epic 3 requires Epics 1+2 complete, which is the natural final phase. + +### Story Quality Assessment + +#### Story Sizing + +| Story | Size Assessment | Verdict | +| ---------------------------------- | -------------------------------------------------------------- | ------------- | +| 1.1: Pure functions + TDD scaffold | Small — 3 pure functions + test file | ✅ Well-sized | +| 1.2: Init, canvas, DOM setup | Medium — constructor, init(), ResizeObserver, D3 zoom | ✅ Well-sized | +| 1.3: Layer lifecycle + render loop | Medium-large — 7 public methods + private render | ⚠️ See note | +| 2.1: Rotation verification | Tiny — investigation + optional fix | ✅ Well-sized | +| 2.2: Refactor draw-relief-icons.ts | Medium — register() call + 3 window globals + buildReliefScene | ✅ Well-sized | +| 2.3: Fallback verification | Small — Vitest test + visual verification | ✅ Well-sized | +| 3.1: Performance benchmarking | Small — measurement + documentation | ✅ Well-sized | +| 3.2: Bundle size audit | Small — build + analysis | ✅ Well-sized | + +**Story 1.3 note:** This story covers 7 public methods (`register`, `unregister`, `setVisible`, `clearLayer`, `requestRender`, `syncTransform`, and the private `render` dispatch loop). This is the densest story. It is cohesive — all methods form a single logical unit (the layer management and render loop). It would be reasonable to split into 1.3a (register/unregister/setVisible/clearLayer) and 1.3b (requestRender/syncTransform/render loop) if a developer finds it too large. Not a blocker, but flagged for developer discretion. + +#### Acceptance Criteria Quality + +- ✅ All ACs use Given/When/Then BDD format +- ✅ All performance ACs include specific numeric targets (ms, percentage, KB) +- ✅ Error/fallback conditions covered (fallback path, missing DOM element, context unavailable) +- ✅ Each AC is independently verifiable +- ⚠️ Story 2.2 AC "1,000-icon map renders in <16ms" requires a real browser environment — Vitest alone cannot satisfy this AC. This is intentional (matches NFR-P1 intent) but the developer must understand this requires manual/DevTools measurement, not an automated test assertion. + +### Dependency Analysis + +#### Forward Dependencies Check + +- Story 1.1 → no dependencies ✅ +- Story 1.2 → depends on 1.1 (uses `detectWebGL2`, `getLayerZIndex`) ✅ +- Story 1.3 → depends on 1.2 (requires initialized framework) ✅ +- Story 2.1 → no framework dependency (code analysis only) ✅ +- Story 2.2 → depends on Epic 1 complete ✅ +- Story 2.3 → depends on 2.2 (verifies the refactored module's fallback path) ✅ +- Story 3.1 → depends on 2.2 complete ✅ +- Story 3.2 → depends on 2.2 complete (needs built module) ✅ + +**No forward dependencies detected. All dependency flows are downstream only.** + +#### Architecture/Brownfield Checks + +- ✅ No starter template story required (brownfield — confirmed by Architecture doc) +- ✅ No "create all tables upfront" equivalent — no database, no upfront resource creation +- ✅ Window globals (`drawRelief`, `undrawRelief`, `rerenderReliefIcons`) backward-compatibility requirement is explicitly carried into Story 2.2 ACs +- ✅ `pendingConfigs[]` queue pattern (register before init) is covered in Story 1.3 — the ordering hazard is explicitly tested +- ✅ `hasFallback` backing-field TypeScript pattern is explicitly called out in Story 1.2 — the known compile-time footgun is documented and tested + +### Best Practices Compliance + +| Check | Status | Notes | +| ----------------------------------------- | ---------- | ------------------------------------------------------------------- | +| Epics deliver user value | ⚠️ Partial | Epics 1 & 3 are technical; acceptable for this platform MVP context | +| Epic independence | ✅ | Each epic functions on prior epics only | +| No forward dependencies | ✅ | Clean downstream-only dependency graph | +| Appropriate story sizing | ✅ | Story 1.3 marginally large but cohesive | +| ACs are testable | ✅ | All numeric, format-specific, verifiable | +| FR traceability | ✅ | 27/27 FRs traceable to stories | +| Brownfield handled correctly | ✅ | No incorrect startup/migration stories | +| Architecture constraints carried into ACs | ✅ | backing field, canvas id, pointer-events, etc. all present | + +--- + +## Summary and Recommendations + +### Overall Readiness Status + +# ✅ READY FOR IMPLEMENTATION + +All 27 FRs and 17 NFRs are covered. No critical violations. No blocking issues. + +### Issues Found + +| Severity | Count | Items | +| ----------- | ----- | --------- | +| 🔴 Critical | 0 | — | +| 🟠 Major | 0 | — | +| 🟡 Minor | 3 | See below | + +**🟡 Minor — Story 1.3 density:** The 7-method scope is cohesive but large. Developer may optionally split into 1.3a (state management: register/unregister/setVisible/clearLayer) and 1.3b (render loop: requestRender/syncTransform/render dispatch). No structural change to epics required. + +**🟡 Minor — Story 2.2 performance AC:** The <16ms render time AC requires browser DevTools measurement, not an automated Vitest assertion. Developer must not attempt to satisfy this in unit tests — it is a manual benchmark. The story AC is correct; this is a documentation awareness item. + +**🟡 Minor — Epic 3 user value:** Stories 3.1 and 3.2 are quality gates, not user-facing features. If team velocity is a concern, these could be folded into Definition of Done criteria for Epic 2 stories rather than standalone stories. No action required unless team prefers this structure. + +### Recommended Next Steps + +1. **Begin implementation at Story 1.1** — create `src/modules/webgl-layer-framework.ts` with the three pure exported functions and the Vitest test file. This is pure TypeScript with zero DOM/WebGL dependencies and is the cleanest entry point. +2. **Optionally split Story 1.3** into 1.3a (state management) and 1.3b (render loop) before handing off to the dev agent if the team prefers smaller units. +3. **Baseline SVG render times before Story 2.2** — measure current `drawRelief()` timing on a 1k and 10k icon map before the refactor so the >80% improvement claim can be verified objectively in Story 3.1. +4. **No UX document needed for MVP** — revisit if Phase 2 (DOM-split) or Phase 3 introduce user-facing panel changes. + +### Final Note + +This assessment identified **3 minor items** across quality and sizing categories. Zero critical or major issues were found. The PRD, Architecture, and Epics documents are well-aligned, requirements are fully traced, dependencies are clean, and the brownfield integration constraints are correctly carried into acceptance criteria. The project is ready to hand off to the development agent. + +--- + +_Assessment completed: 2026-03-12 — Fantasy-Map-Generator WebGL Layer Framework MVP_ + +---