From 52708e50c54ddabd36941c5dfef96845edbe4ad2 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Fri, 13 Mar 2026 02:39:40 +0100 Subject: [PATCH] feat: Update SceneModule to manage camera state and viewport, refactor WebGL layer to utilize Scene methods --- ...bootstrap-scene-container-and-defs-host.md | 6 +- ...dd-scene-module-for-shared-camera-state.md | 40 +++++---- ...egistry-as-the-ordering-source-of-truth.md | 2 + ...4-add-layer-surface-lifecycle-ownership.md | 2 + ...y-lookups-for-legacy-single-svg-callers.md | 2 + ...ed-defs-resources-to-the-dedicated-host.md | 2 + .../sprint-status.yaml | 2 +- public/main.js | 2 +- src/modules/scene.ts | 82 ++++++++++++++++++- src/modules/webgl-layer.ts | 14 ++-- 10 files changed, 128 insertions(+), 26 deletions(-) diff --git a/_bmad-output/implementation-artifacts/1-1-bootstrap-scene-container-and-defs-host.md b/_bmad-output/implementation-artifacts/1-1-bootstrap-scene-container-and-defs-host.md index fe2c3656..ceea6f52 100644 --- a/_bmad-output/implementation-artifacts/1-1-bootstrap-scene-container-and-defs-host.md +++ b/_bmad-output/implementation-artifacts/1-1-bootstrap-scene-container-and-defs-host.md @@ -50,12 +50,15 @@ so that split layer surfaces can share resources without depending on one map SV - Keep the runtime compatible with the current global-variable model. New TypeScript code must use ambient globals directly, not `window` or `globalThis` wrappers. - New TypeScript modules must follow the project Global Module Pattern: declare global, implement a class, then assign `window.ModuleName = new ModuleClass()`. - Avoid grouped SVG buckets or speculative abstractions here. The goal is bootstrap ownership only. +- Keep the implementation lean. Do not expand this story into a broad runtime framework, compatibility bridge, or camera API beyond the host ownership needed for Story 1.1. +- Prefer direct code over wrappers. Do not add helpers, comments, or indirection unless at least two concrete call sites need them. ### Architecture Compliance - This story is the Phase 1 foundation from the layered-map architecture: create one scene container, one defs host, and stable references before any layer split begins. - `svg` and `viewbox` remain compatibility-era globals after this story. They must stop being the architectural source of truth, but they are not removed yet. - The runtime defs host must live outside individual layer surfaces so later split SVG shells can reference shared resources by stable ID. +- Keep ownership narrow: this story covers bootstrap ownership only. Do not fold in layer registry, compatibility lookups, export behavior, or transform semantics. ### Project Structure Notes @@ -73,7 +76,8 @@ so that split layer surfaces can share resources without depending on one map SV - The architecture document explicitly defers formal test work for this tranche. - Do manual verification for fresh load, saved-map load, and relief visibility. -- If a pure helper is extracted while implementing bootstrap ownership, keep it testable, but do not expand scope into a new test suite in this story. +- Do not add Playwright coverage, browser harnesses, or new automated regression suites in this story. +- If a pure helper is extracted while implementing bootstrap ownership, keep it simple and locally testable later, but do not expand scope into a new test suite in this story. ### References diff --git a/_bmad-output/implementation-artifacts/1-2-add-scene-module-for-shared-camera-state.md b/_bmad-output/implementation-artifacts/1-2-add-scene-module-for-shared-camera-state.md index b71f1101..c7f4c776 100644 --- a/_bmad-output/implementation-artifacts/1-2-add-scene-module-for-shared-camera-state.md +++ b/_bmad-output/implementation-artifacts/1-2-add-scene-module-for-shared-camera-state.md @@ -1,6 +1,6 @@ # Story 1.2: Add Scene Module for Shared Camera State -Status: ready-for-dev +Status: in-progress @@ -17,18 +17,18 @@ so that all layer surfaces consume one authoritative transform contract. ## Tasks / Subtasks -- [ ] Create a narrow Scene runtime module. - - [ ] Expose shared camera and viewport getters derived from the authoritative globals. - - [ ] Expose scene-host references established in Story 1.1 through the same contract. -- [ ] Centralize transform ownership. - - [ ] Move reusable camera-bound calculations behind Scene methods instead of ad hoc DOM reads. - - [ ] Keep `scale`, `viewX`, `viewY`, `graphWidth`, and `graphHeight` as the underlying truth during migration. -- [ ] Update runtime consumers that already operate across surfaces. - - [ ] Rewire the current WebGL layer framework to consume the Scene contract instead of reading transform meaning from DOM structure. - - [ ] Ensure zoom and pan updates publish one consistent state for all registered surfaces. -- [ ] Keep compatibility intact. - - [ ] Preserve existing `viewbox` transform application while older code still depends on it. - - [ ] Do not break callers that still read the existing globals directly. +- [x] Create a narrow Scene runtime module. + - [x] Expose shared camera and viewport getters derived from the authoritative globals. + - [x] Expose scene-host references established in Story 1.1 through the same contract. +- [x] Centralize transform ownership. + - [x] Move reusable camera-bound calculations behind Scene methods instead of ad hoc DOM reads. + - [x] Keep `scale`, `viewX`, `viewY`, `graphWidth`, and `graphHeight` as the underlying truth during migration. +- [x] Update runtime consumers that already operate across surfaces. + - [x] Rewire the current WebGL layer framework to consume the Scene contract instead of reading transform meaning from DOM structure. + - [x] Ensure zoom and pan updates publish one consistent state for all registered surfaces. +- [x] Keep compatibility intact. + - [x] Preserve existing `viewbox` transform application while older code still depends on it. + - [x] Do not break callers that still read the existing globals directly. - [ ] Perform manual smoke verification. - [ ] Zoom and pan keep SVG and WebGL content aligned. - [ ] Startup and resize continue to use the correct viewport bounds. @@ -47,12 +47,14 @@ so that all layer surfaces consume one authoritative transform contract. - Use bare ambient globals declared in `src/types/global.ts`. Do not introduce `window.scale` or `globalThis.viewX` usage. - Keep the abstraction narrow: Scene owns shared camera and viewport state, not layer ordering, defs ownership, or export assembly. - Prefer pure helper methods for transform math so later stories can reuse them without DOM coupling. +- Keep the module terse. Do not let `Scene` accumulate bootstrap, compatibility, registry, or export responsibilities in this story. ### Architecture Compliance - This story implements the architecture decision that transform ownership moves from `#viewbox` to scene state. - `viewbox` remains a compatibility-era render target, not the source of truth. - The Scene API should be sufficient for both SVG and WebGL consumers once split surfaces arrive. +- Developer productivity is architecture here: expose only the few scene methods current consumers actually need. ### Previous Story Intelligence @@ -75,6 +77,7 @@ so that all layer surfaces consume one authoritative transform contract. - Formal automated test work is out of scope for this tranche. - Keep transform calculation logic pure enough for later coverage. - Manual verification should cover zoom, pan, resize, and relief alignment. +- Do not add Playwright coverage or new browser-driven tests in this story. ### Dependencies @@ -92,12 +95,21 @@ so that all layer surfaces consume one authoritative transform contract. ### Agent Model Used -TBD +GPT-5.4 ### Debug Log References ### Completion Notes List - Story context prepared on 2026-03-13. +- Scene now exposes shared camera, viewport, and compatibility transform access while reusing the stable scene-host references from Story 1.1. +- WebGL camera sync now consumes `Scene.getCameraBounds()` and legacy zoom handling routes `viewbox` transform application through `Scene.applyViewboxTransform()`. +- Automated tests were removed and no tests or Playwright checks were run per user instruction. +- Manual smoke verification remains pending before the story can move to review. ### File List + +- public/main.js +- src/modules/scene.ts +- src/modules/webgl-layer.ts +- src/modules/scene.test.ts (removed) diff --git a/_bmad-output/implementation-artifacts/1-3-add-layers-registry-as-the-ordering-source-of-truth.md b/_bmad-output/implementation-artifacts/1-3-add-layers-registry-as-the-ordering-source-of-truth.md index e2dedba6..a316f741 100644 --- a/_bmad-output/implementation-artifacts/1-3-add-layers-registry-as-the-ordering-source-of-truth.md +++ b/_bmad-output/implementation-artifacts/1-3-add-layers-registry-as-the-ordering-source-of-truth.md @@ -48,6 +48,7 @@ so that visibility, order, and surface ownership are managed consistently instea - Keep the public contract minimal. Do not add export metadata yet; that belongs to Epic 4. - Store actual surface handles, not just selectors, so later stories can mount independent SVG shells and WebGL surfaces under one contract. - Ensure reorder application is atomic from the user perspective. Avoid partial states where one surface has moved and another has not. +- Keep the registry boring. No factory layers, schema systems, or speculative metadata beyond `id`, `kind`, `order`, `visible`, and `surface`. ### Architecture Compliance @@ -77,6 +78,7 @@ so that visibility, order, and surface ownership are managed consistently instea - Manual verification is sufficient for this tranche. - Verify the existing sortable Layers UI still works and that no layer disappears from the visible stack after a reorder. +- Do not add Playwright coverage or new automated test infrastructure in this story. ### Dependencies diff --git a/_bmad-output/implementation-artifacts/1-4-add-layer-surface-lifecycle-ownership.md b/_bmad-output/implementation-artifacts/1-4-add-layer-surface-lifecycle-ownership.md index 88954f32..ce5dcebf 100644 --- a/_bmad-output/implementation-artifacts/1-4-add-layer-surface-lifecycle-ownership.md +++ b/_bmad-output/implementation-artifacts/1-4-add-layer-surface-lifecycle-ownership.md @@ -47,6 +47,7 @@ so that individual layers can be created, mounted, updated, and disposed without - Avoid generic factories or strategy trees. One lifecycle owner with SVG and WebGL-capable implementations is enough for this phase. - Do not force feature modules to pass renderer flags around. If two surface kinds need separate logic, isolate that inside the lifecycle owner. - Preserve the current `WebGLLayer` canvas and shared context budget. +- Keep the API compact. Do not add lifecycle hooks or extension points that this migration does not use yet. ### Architecture Compliance @@ -75,6 +76,7 @@ so that individual layers can be created, mounted, updated, and disposed without - Manual validation is sufficient. - Focus on mount, redraw, hide/show, and cleanup behavior for relief because that is the current live mixed-render surface. +- Do not add Playwright coverage or new automated test harnesses in this story. ### Dependencies diff --git a/_bmad-output/implementation-artifacts/1-5-add-compatibility-lookups-for-legacy-single-svg-callers.md b/_bmad-output/implementation-artifacts/1-5-add-compatibility-lookups-for-legacy-single-svg-callers.md index 820e241a..8c8fde2c 100644 --- a/_bmad-output/implementation-artifacts/1-5-add-compatibility-lookups-for-legacy-single-svg-callers.md +++ b/_bmad-output/implementation-artifacts/1-5-add-compatibility-lookups-for-legacy-single-svg-callers.md @@ -48,6 +48,7 @@ so that existing workflows keep working while code migrates to the new scene and - Return real layer surfaces or layer-local SVG roots where available. Do not fake a new canonical SVG document. - `queryMap(selector)` should be controlled and predictable. It must not silently reintroduce global DOM coupling as a hidden permanent pattern. - Preserve current IDs and selectors where possible so callers can migrate incrementally. +- Keep the bridge thin and temporary. Do not add convenience helpers beyond the three architecture-approved lookups. ### Architecture Compliance @@ -78,6 +79,7 @@ so that existing workflows keep working while code migrates to the new scene and - Manual verification is sufficient. - Validate save/export, labels/text paths, and at least one legacy selector-heavy workflow after the helpers are introduced. +- Do not add Playwright coverage or new automated regression suites in this story. ### Dependencies 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 ebde12bb..f4ea661a 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 @@ -52,6 +52,7 @@ so that split surfaces can keep using stable IDs for filters, masks, symbols, ma - The dedicated defs host must be reachable by all layer surfaces after the map DOM is split. - Prefer one focused defs owner module over scattered DOM writes. - Avoid changing export behavior in this story beyond what is necessary to keep runtime resources consistent for later work. +- Keep defs ownership isolated. Do not use this story to introduce broader compatibility or export abstractions. ### Architecture Compliance @@ -83,6 +84,7 @@ so that split surfaces can keep using stable IDs for filters, masks, symbols, ma - Manual validation is sufficient. - Check at least one feature path mask, one text-path label case, one marker or symbol reference, and fogging/filter behavior. +- Do not add Playwright coverage or new automated browser tests in this story. ### Dependencies diff --git a/_bmad-output/implementation-artifacts/sprint-status.yaml b/_bmad-output/implementation-artifacts/sprint-status.yaml index fe1f4a9d..fd2aa80f 100644 --- a/_bmad-output/implementation-artifacts/sprint-status.yaml +++ b/_bmad-output/implementation-artifacts/sprint-status.yaml @@ -42,7 +42,7 @@ 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: ready-for-dev + 1-2-add-scene-module-for-shared-camera-state: in-progress 1-3-add-layers-registry-as-the-ordering-source-of-truth: ready-for-dev 1-4-add-layer-surface-lifecycle-ownership: ready-for-dev 1-5-add-compatibility-lookups-for-legacy-single-svg-callers: ready-for-dev diff --git a/public/main.js b/public/main.js index d71db237..f578a715 100644 --- a/public/main.js +++ b/public/main.js @@ -450,7 +450,7 @@ function findBurgForMFCG(params) { } function handleZoom(isScaleChanged, isPositionChanged) { - viewbox.attr("transform", `translate(${viewX} ${viewY}) scale(${scale})`); + Scene.applyViewboxTransform(); if (isPositionChanged) { if (layerIsOn("toggleCoordinates")) drawCoordinates(); diff --git a/src/modules/scene.ts b/src/modules/scene.ts index b36f4e51..067871df 100644 --- a/src/modules/scene.ts +++ b/src/modules/scene.ts @@ -6,6 +6,58 @@ const WEBGL_CANVAS_ID = "webgl-canvas"; const RUNTIME_DEFS_HOST_ID = "runtime-defs-host"; const RUNTIME_DEFS_ID = "runtime-defs"; +export interface SceneCameraState { + scale: number; + viewX: number; + viewY: number; +} + +export interface SceneViewportState { + graphWidth: number; + graphHeight: number; + svgWidth: number; + svgHeight: number; +} + +export interface SceneCameraBounds { + left: number; + right: number; + top: number; + bottom: number; +} + +export function buildCameraBounds( + viewX: number, + viewY: number, + scale: number, + graphWidth: number, + graphHeight: number, +): SceneCameraBounds { + const left = normalizeSceneValue(-viewX / scale); + const top = normalizeSceneValue(-viewY / scale); + const width = graphWidth / scale; + const height = graphHeight / scale; + + return { + left, + right: left + width, + top, + bottom: top + height, + }; +} + +export function buildViewboxTransform({ + scale, + viewX, + viewY, +}: SceneCameraState) { + return `translate(${viewX} ${viewY}) scale(${scale})`; +} + +function normalizeSceneValue(value: number) { + return Object.is(value, -0) ? 0 : value; +} + export class SceneModule { private mapContainer: HTMLElement | null = null; private sceneContainer: HTMLDivElement | null = null; @@ -98,6 +150,32 @@ export class SceneModule { return this.runtimeDefs!; } + getCamera() { + return { scale, viewX, viewY }; + } + + getViewport() { + return { graphWidth, graphHeight, svgWidth, svgHeight }; + } + + getCameraBounds() { + const { scale, viewX, viewY } = this.getCamera(); + const { graphWidth, graphHeight } = this.getViewport(); + return buildCameraBounds(viewX, viewY, scale, graphWidth, graphHeight); + } + + getViewboxTransform() { + return buildViewboxTransform(this.getCamera()); + } + + applyViewboxTransform() { + if (typeof viewbox === "undefined") { + return; + } + + viewbox.attr("transform", this.getViewboxTransform()); + } + private requireMapContainer() { const mapContainer = document.getElementById(MAP_CONTAINER_ID); if (!(mapContainer instanceof HTMLElement)) { @@ -173,4 +251,6 @@ declare global { var Scene: SceneModule; } -window.Scene = new SceneModule(); +if (typeof window !== "undefined") { + window.Scene = new SceneModule(); +} diff --git a/src/modules/webgl-layer.ts b/src/modules/webgl-layer.ts index 4beda1c8..b7928741 100644 --- a/src/modules/webgl-layer.ts +++ b/src/modules/webgl-layer.ts @@ -26,6 +26,7 @@ export class WebGL2LayerClass { init(): boolean { const canvas = Scene.getCanvas(); + const { graphWidth, graphHeight } = Scene.getViewport(); this.renderer = new WebGLRenderer({ canvas, @@ -53,15 +54,12 @@ export class WebGL2LayerClass { private syncTransform() { if (!this.camera) return; - const x = -viewX / scale; - const y = -viewY / scale; - const w = graphWidth / scale; - const h = graphHeight / scale; + const { bottom, left, right, top } = Scene.getCameraBounds(); - this.camera.left = x; - this.camera.right = x + w; - this.camera.top = y; - this.camera.bottom = y + h; + this.camera.left = left; + this.camera.right = right; + this.camera.top = top; + this.camera.bottom = bottom; this.camera.updateProjectionMatrix(); }