diff --git a/B04_wf1_Surface/B04_wf1_Surface_UI_Camera.ts b/B04_wf1_Surface/B04_wf1_Surface_UI_Camera.ts index fb41ae3d..7b359fd7 100644 --- a/B04_wf1_Surface/B04_wf1_Surface_UI_Camera.ts +++ b/B04_wf1_Surface/B04_wf1_Surface_UI_Camera.ts @@ -1,6 +1,8 @@ import type { SurfaceBounds } from "./B04_wf1_Surface_Api_Fetch"; export const SURFACE_CAMERA_FOV = 50; +const LIGHT_VIEWER_BACKGROUND = 0xf5f7f9; +const DARK_VIEWER_BACKGROUND = 0x251f38; export interface SurfaceCameraState { direction: [number, number, number]; @@ -40,3 +42,36 @@ export function niceScaleDistance(roughMeters: number): number { const step = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10; return step * base; } + +export function bindSurfaceViewerTheme( + applyBackground: (color: string | number) => void, +): () => void { + const systemDarkTheme = window.matchMedia("(prefers-color-scheme: dark)"); + const update = (): void => { + const theme = document.documentElement.getAttribute("data-theme"); + const dark = theme === "dark" || (theme !== "light" && systemDarkTheme.matches); + if (!dark) { + applyBackground(LIGHT_VIEWER_BACKGROUND); + return; + } + const surfaceRaised = getComputedStyle(document.documentElement) + .getPropertyValue("--color-surface-raised") + .trim(); + applyBackground( + surfaceRaised && CSS.supports("color", surfaceRaised) + ? surfaceRaised + : DARK_VIEWER_BACKGROUND, + ); + }; + const observer = new MutationObserver(update); + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["data-theme"], + }); + systemDarkTheme.addEventListener("change", update); + update(); + return () => { + observer.disconnect(); + systemDarkTheme.removeEventListener("change", update); + }; +} diff --git a/B04_wf1_Surface/B04_wf1_Surface_UI_Style.css b/B04_wf1_Surface/B04_wf1_Surface_UI_Style.css index f452dcf9..eb960f2b 100644 --- a/B04_wf1_Surface/B04_wf1_Surface_UI_Style.css +++ b/B04_wf1_Surface/B04_wf1_Surface_UI_Style.css @@ -460,26 +460,31 @@ pointer-events: none; } -.b04-surface__scale { +.b04-surface__scale, +.b04-map__scale { position: absolute; bottom: var(--spacing-16); left: var(--spacing-16); z-index: 2; height: var(--spacing-8); - border: 2px solid var(--color-text); + border: 2px solid var(--color-text-body); border-top: 0; - color: var(--color-text); + color: var(--color-text-body); + filter: drop-shadow(0 1px 1px var(--color-surface-raised)); pointer-events: none; } -.b04-surface__scale span { +.b04-surface__scale span, +.b04-map__scale span { position: absolute; bottom: var(--spacing-8); left: 50%; transform: translateX(-50%); white-space: nowrap; + font-family: var(--font-body); font-size: var(--text-caption); font-weight: var(--font-weight-semibold); + text-shadow: 0 1px 2px var(--color-surface-raised); } /* --- 하단 2D 지도 --- */ @@ -619,28 +624,6 @@ background: var(--color-surface-raised); } -.b04-map__scale { - position: absolute; - bottom: var(--spacing-16); - left: var(--spacing-16); - z-index: 2; - height: var(--spacing-8); - border: 2px solid var(--color-text); - border-top: 0; - color: var(--color-text); - pointer-events: none; -} - -.b04-map__scale span { - position: absolute; - bottom: var(--spacing-8); - left: 50%; - transform: translateX(-50%); - white-space: nowrap; - font-size: var(--text-caption); - font-weight: var(--font-weight-semibold); -} - @media (max-width: 760px) { .b04-map__header { align-items: flex-start; diff --git a/B04_wf1_Surface/B04_wf1_Surface_UI_TerrainViewer.ts b/B04_wf1_Surface/B04_wf1_Surface_UI_TerrainViewer.ts index 4d34198f..87a7f162 100644 --- a/B04_wf1_Surface/B04_wf1_Surface_UI_TerrainViewer.ts +++ b/B04_wf1_Surface/B04_wf1_Surface_UI_TerrainViewer.ts @@ -5,6 +5,7 @@ import { PLYLoader } from "three/examples/jsm/loaders/PLYLoader.js"; import { API_BASE_URL } from "@config/config_frontend"; import type { SurfaceBounds, SurfaceModelSummary } from "./B04_wf1_Surface_Api_Fetch"; import { + bindSurfaceViewerTheme, getTopFitDistance, niceScaleDistance, SURFACE_CAMERA_FOV, @@ -180,7 +181,9 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer { let loadGeneration = 0; const scene = new THREE.Scene(); - scene.background = new THREE.Color(0xf5f7f9); + const releaseTheme = bindSurfaceViewerTheme((color) => { + scene.background = new THREE.Color(color); + }); const camera = new THREE.PerspectiveCamera(SURFACE_CAMERA_FOV, 1, 0.01, 100000); const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); @@ -543,6 +546,7 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer { cancelAnimationFrame(animationFrameId); clearMesh(); clearContours(); + releaseTheme(); controls.dispose(); renderer.dispose(); } @@ -670,6 +674,7 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer { dispose() { cancelAnimationFrame(animationFrameId); resizeObserver.disconnect(); + releaseTheme(); clearMesh(); clearContours(); controls.dispose(); diff --git a/B04_wf1_Surface/B04_wf1_Surface_UI_Viewer.ts b/B04_wf1_Surface/B04_wf1_Surface_UI_Viewer.ts index f3000f81..a9ad9012 100644 --- a/B04_wf1_Surface/B04_wf1_Surface_UI_Viewer.ts +++ b/B04_wf1_Surface/B04_wf1_Surface_UI_Viewer.ts @@ -3,6 +3,7 @@ import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import type { SurfaceBounds, SurfacePointCloudSampleResponse } from "./B04_wf1_Surface_Api_Fetch"; import { + bindSurfaceViewerTheme, getReferenceCenter, getTopFitDistance, niceScaleDistance, @@ -106,7 +107,9 @@ export function createSurfacePointCloudViewer(): SurfacePointCloudViewer { }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, RENDER_OPTIONS.maxPixelRatio)); const scene = new THREE.Scene(); - scene.background = new THREE.Color(0xf5f7f9); + const releaseTheme = bindSurfaceViewerTheme((color) => { + scene.background = new THREE.Color(color); + }); const camera = new THREE.PerspectiveCamera(SURFACE_CAMERA_FOV, 1, 0.1, 22000); const orbit = new OrbitControls(camera, canvas); orbit.enableDamping = true; @@ -268,6 +271,7 @@ export function createSurfacePointCloudViewer(): SurfacePointCloudViewer { if (disposed) return; disposed = true; cancelAnimationFrame(animationFrame); + releaseTheme(); clearPoints(); orbit.dispose(); renderer.dispose(); diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Viewer.ts b/B05_wf2_Route/B05_wf2_Route_UI_Viewer.ts index ee2da3df..5f3f5f47 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Viewer.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Viewer.ts @@ -12,6 +12,9 @@ import { type SectionStationMarker, } from "./B05_wf2_Route_UI_Markers"; +const LIGHT_VIEWER_BACKGROUND = 0xf5f7fa; +const DARK_VIEWER_BACKGROUND = 0x251f38; + function disposeObject(object: THREE.Object3D | null): void { object?.traverse((child) => { if ( @@ -58,7 +61,32 @@ export function createRouteViewer(): RouteViewer { root.append(canvas, status); const scene = new THREE.Scene(); - scene.background = new THREE.Color(0xf5f7fa); + const systemDarkTheme = window.matchMedia("(prefers-color-scheme: dark)"); + + function updateSceneBackground(): void { + const theme = document.documentElement.getAttribute("data-theme"); + const dark = theme === "dark" || (theme !== "light" && systemDarkTheme.matches); + if (!dark) { + scene.background = new THREE.Color(LIGHT_VIEWER_BACKGROUND); + return; + } + const surfaceRaised = getComputedStyle(document.documentElement) + .getPropertyValue("--color-surface-raised") + .trim(); + scene.background = new THREE.Color( + surfaceRaised && CSS.supports("color", surfaceRaised) + ? surfaceRaised + : DARK_VIEWER_BACKGROUND, + ); + } + + const themeObserver = new MutationObserver(updateSceneBackground); + themeObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ["data-theme"], + }); + systemDarkTheme.addEventListener("change", updateSceneBackground); + updateSceneBackground(); const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100000); camera.position.set(100, 120, 100); const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); @@ -335,6 +363,8 @@ export function createRouteViewer(): RouteViewer { dispose() { cancelAnimationFrame(frame); resizeObserver.disconnect(); + themeObserver.disconnect(); + systemDarkTheme.removeEventListener("change", updateSceneBackground); canvas.removeEventListener("pointerdown", handlePointerDown, true); canvas.removeEventListener("pointermove", handlePointerMove, true); canvas.removeEventListener("pointerup", handlePointerUp, true);