This commit is contained in:
2026-07-19 13:08:34 +09:00
parent cfb7a443af
commit 0ba6110dbf
5 changed files with 86 additions and 29 deletions
@@ -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);
};
}
+9 -26
View File
@@ -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;
@@ -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();
+5 -1
View File
@@ -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();
+31 -1
View File
@@ -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);