From 3d037d869eb9b1272c44a06d0d7360e04da925b9 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 3 Sep 2026 20:18:51 +0900 Subject: [PATCH] =?UTF-8?q?feat(B05):=20=EC=A2=85=EB=8B=A8=203D=20?= =?UTF-8?q?=EB=B7=B0=EC=96=B4=EC=97=90=20=EB=B0=A9=EC=9C=84=20=EB=82=98?= =?UTF-8?q?=EC=B9=A8=EB=B0=98=20=EC=B6=94=EA=B0=80=20+=20=EB=82=98?= =?UTF-8?q?=EC=B9=A8=EB=B0=98=20=EC=9C=84=EC=A0=AF=20=EA=B3=B5=EC=9A=A9=20?= =?UTF-8?q?=EC=9D=B4=EA=B4=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B05 3D 뷰어에는 방위 표시가 없어 방향을 읽을 수 없었음(사용자 지적, 메인 창 경유). B04 가 오늘 새로 만든 「지면에 누운 나침반 링」을 그대로 쓰되, 두 페이지가 함께 쓰도록 공용으로 옮김. - `B04_PreProcess_UI_Compass.ts` → `ui_template/ui_template_compass.ts` (git mv). `createTerrainCompass({ sizePx, className })` 로 크기·자리를 호출부가 정함. - 스타일도 `ui_template/ui_template_compass.css` 로 옮기고 모듈이 직접 물고 옴. **놓을 자리만** 페이지 CSS 몫 — B04 우하단(`b04-surface__compass`), B05 는 ISO 버튼 아래(`b05-route__compass`). - B05 뷰어는 104px 로 쓰고 렌더 루프에서 카메라 오프셋으로 갱신, 지형이 없으면 감춤. 좌표 규약이 B04 와 같음(북 −z · 동 +x · 표고 +y, `Viewer.ts:295` 정점 변환) — 그대로 맞음. 화면 검증(5174): B05 나침반 104×104, ISO 버튼 아래 8px. 도북 화살이 뷰 전환에 반응 — TOP 에서 `M0.00 -33.30`(수직), ISO 에서 `M23.56 -13.60`. 후자는 카메라 오프셋 (d,d,d) 에 대한 투영 이론값(x=R/√2=23.56, y=−R/√6=−13.60)과 일치. B04 쪽도 그대로(84×84, 우하단 16px, 링·화살 색 토큰 유지). `tsc --noEmit` 통과, 뷰어 693줄로 제한 안. Co-Authored-By: Claude Opus 5 (1M context) --- B04_PreProcess/B04_PreProcess_UI_Style.css | 61 +--------------- .../B04_PreProcess_UI_TerrainViewer.ts | 4 +- B05_Profile/B05_Profile_UI_Style.css | 8 +++ B05_Profile/B05_Profile_UI_Viewer.ts | 17 ++++- ui_template/ui_template_compass.css | 71 +++++++++++++++++++ .../ui_template_compass.ts | 40 +++++++---- 6 files changed, 126 insertions(+), 75 deletions(-) create mode 100644 ui_template/ui_template_compass.css rename B04_PreProcess/B04_PreProcess_UI_Compass.ts => ui_template/ui_template_compass.ts (83%) diff --git a/B04_PreProcess/B04_PreProcess_UI_Style.css b/B04_PreProcess/B04_PreProcess_UI_Style.css index e44dcf7b..62e63d40 100644 --- a/B04_PreProcess/B04_PreProcess_UI_Style.css +++ b/B04_PreProcess/B04_PreProcess_UI_Style.css @@ -557,68 +557,11 @@ text-shadow: 0 1px 2px var(--color-surface-raised); } -/* 방위 콤파스 — 3D 뷰어 우하단. 축척 막대(좌하단)와 짝. */ +/* 방위 콤파스를 놓을 자리 — 3D 뷰어 우하단. 축척 막대(좌하단)와 짝. + 위젯 모양·색은 공용(`ui_template/ui_template_compass.css`)이고 여기서는 위치만 준다. */ .b04-surface__compass { - position: absolute; right: var(--spacing-16); bottom: var(--spacing-16); - z-index: 2; - pointer-events: none; - filter: drop-shadow(0 1px 2px var(--color-surface-raised)); -} - -/* inline SVG의 baseline 여백을 없앤다 — 안 그러면 아래로 5px 떠 모서리 여백이 어긋난다. */ -.b04-surface__compass svg { - display: block; -} - -/* 지면에 누운 나침반 링 — 좌표는 카메라 자세대로 매 프레임 다시 쓰인다. - 링 안쪽 면은 지형이 비치도록 아주 옅게만 깔아 도면 위 방위표처럼 읽힌다. */ -.b04-surface__compass-face { - fill: var(--color-surface-raised); - fill-opacity: 0.5; - stroke: none; -} - -.b04-surface__compass-ring { - fill: none; - stroke: var(--color-text-secondary); - stroke-width: 1.4; -} - -.b04-surface__compass-tick { - stroke: var(--color-border); - stroke-width: 1; -} - -.b04-surface__compass-tick--major { - stroke: var(--color-text-secondary); - stroke-width: 1.6; -} - -/* 도북 화살 — 이 위젯에서 가장 먼저 읽혀야 하는 하나. */ -.b04-surface__compass-arrow { - fill: var(--color-danger, #d64545); -} - -/* 표고축 — 내려다볼수록 짧아져 시점 기울기를 한 번 더 알린다. */ -.b04-surface__compass-pole { - stroke: var(--color-text-body); - stroke-width: 1.6; - stroke-linecap: round; - opacity: 0.75; -} - -.b04-surface__compass-label { - fill: var(--color-text-secondary); - font-family: var(--font-body); - font-size: 11px; - font-weight: var(--font-weight-semibold); - text-anchor: middle; -} - -.b04-surface__compass-label--north { - fill: var(--color-danger, #d64545); } /* --- 하단 2D 지도 --- */ diff --git a/B04_PreProcess/B04_PreProcess_UI_TerrainViewer.ts b/B04_PreProcess/B04_PreProcess_UI_TerrainViewer.ts index c3f278e6..86e879a3 100644 --- a/B04_PreProcess/B04_PreProcess_UI_TerrainViewer.ts +++ b/B04_PreProcess/B04_PreProcess_UI_TerrainViewer.ts @@ -7,7 +7,7 @@ import { fetchCachedBytes, fetchCachedJson } from "../A00_Common/b_asset_cache"; import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale"; import { createProgressCircle } from "@ui/ui_template_progress"; // 계획선 색은 2D 지도·B05 배수유역도와 한 곳에서 나온다 — 같은 선을 다른 색으로 그리지 않는다. -import { createTerrainCompass } from "./B04_PreProcess_UI_Compass"; +import { createTerrainCompass } from "@ui/ui_template_compass"; import { routeLineColor } from "./B04_PreProcess_UI_MapRender"; import type { SurfaceBounds, SurfaceModelSummary } from "./B04_PreProcess_Api_Fetch"; import { @@ -168,7 +168,7 @@ export function createSurfaceTerrainViewer(): SurfaceTerrainViewer { viewerArea.append(scaleBar); // 방위 콤파스 — 축척 막대 반대편(우하단). 바늘 각도는 애니메이션 루프가 맞춘다. - const compass = createTerrainCompass(); + const compass = createTerrainCompass({ className: "b04-surface__compass" }); viewerArea.append(compass.root); // Elevation bounds legend bar overlay (I-403) diff --git a/B05_Profile/B05_Profile_UI_Style.css b/B05_Profile/B05_Profile_UI_Style.css index 94062e8f..7233f768 100644 --- a/B05_Profile/B05_Profile_UI_Style.css +++ b/B05_Profile/B05_Profile_UI_Style.css @@ -192,6 +192,14 @@ gap: var(--spacing-8); } +/* 방위 나침반을 놓을 자리 — ISO 버튼 바로 아래(2026-09-03 사용자 지시). + 버튼 줄 높이 34px + 위 여백 16px 아래에 붙인다. 위젯 모양·색은 공용 + (`ui_template/ui_template_compass.css`)이고 여기서는 위치만 준다. */ +.b05-route__compass { + top: calc(var(--spacing-16) + 34px + var(--spacing-8)); + left: var(--spacing-16); +} + .b05-route__view-group { display: flex; gap: var(--spacing-4); diff --git a/B05_Profile/B05_Profile_UI_Viewer.ts b/B05_Profile/B05_Profile_UI_Viewer.ts index 51cd5ea1..d098c6df 100644 --- a/B05_Profile/B05_Profile_UI_Viewer.ts +++ b/B05_Profile/B05_Profile_UI_Viewer.ts @@ -1,5 +1,6 @@ import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; +import { createTerrainCompass } from "@ui/ui_template_compass"; import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; import { PLYLoader } from "three/examples/jsm/loaders/PLYLoader.js"; import { API_BASE_URL } from "@config/config_frontend"; @@ -125,7 +126,10 @@ export function createRouteViewer(): RouteViewer { status.className = "b05-route__viewer-status"; status.textContent = "확정 지표면을 불러오는 중입니다."; const canvas = document.createElement("canvas"); - root.append(canvas, status); + // 방위 나침반 — ISO 버튼 아래(2026-09-03 사용자 지시). 지면에 누운 링이라 사시도에서도 + // 화면 북쪽과 어긋나지 않는다. 위젯은 B04 지표면 뷰어와 **같은 공용 모듈**을 쓴다. + const compass = createTerrainCompass({ sizePx: 104, className: "b05-route__compass" }); + root.append(canvas, status, compass.root); const scene = new THREE.Scene(); const systemDarkTheme = window.matchMedia("(prefers-color-scheme: dark)"); @@ -320,6 +324,17 @@ export function createRouteViewer(): RouteViewer { function animate(): void { frame = requestAnimationFrame(animate); controls.update(); + // 나침반은 시선 방향이 실제로 바뀔 때만 다시 그린다(모듈 안에서 걸러낸다). + if (terrain) { + compass.setVisible(true); + compass.update( + camera.position.x - controls.target.x, + camera.position.y - controls.target.y, + camera.position.z - controls.target.z, + ); + } else { + compass.setVisible(false); + } renderer.render(scene, camera); } animate(); diff --git a/ui_template/ui_template_compass.css b/ui_template/ui_template_compass.css new file mode 100644 index 00000000..9cd28c73 --- /dev/null +++ b/ui_template/ui_template_compass.css @@ -0,0 +1,71 @@ +/* ============================================================================= + * ui_template_compass.css + * 3D 뷰어 방위 나침반(`ui_template_compass.ts`) 전용 스타일. + * + * B04 지표면 뷰어에만 있던 것을 B05 종단 3D 뷰어와 함께 쓰려고 공용으로 옮겼다 + * (2026-09-03). 색상 하드코딩 금지 원칙대로 전부 토큰을 쓴다. + * ========================================================================== */ + +/* 3D 뷰어 방위 나침반 — 모양·색만 여기서 정한다. + **놓을 자리는 페이지 CSS 몫**이다(B04 우하단, B05 는 ISO 버튼 아래). 위치를 여기 박으면 + 페이지마다 덮어써야 해서 규칙이 엉킨다. */ +.ui-compass { + position: absolute; + z-index: 2; + pointer-events: none; + filter: drop-shadow(0 1px 2px var(--color-surface-raised)); +} + +/* inline SVG의 baseline 여백을 없앤다 — 안 그러면 아래로 5px 떠 모서리 여백이 어긋난다. */ +.ui-compass svg { + display: block; +} + +/* 지면에 누운 나침반 링 — 좌표는 카메라 자세대로 매 프레임 다시 쓰인다. + 링 안쪽 면은 지형이 비치도록 아주 옅게만 깔아 도면 위 방위표처럼 읽힌다. */ +.ui-compass-face { + fill: var(--color-surface-raised); + fill-opacity: 0.5; + stroke: none; +} + +.ui-compass-ring { + fill: none; + stroke: var(--color-text-secondary); + stroke-width: 1.4; +} + +.ui-compass-tick { + stroke: var(--color-border); + stroke-width: 1; +} + +.ui-compass-tick--major { + stroke: var(--color-text-secondary); + stroke-width: 1.6; +} + +/* 도북 화살 — 이 위젯에서 가장 먼저 읽혀야 하는 하나. */ +.ui-compass-arrow { + fill: var(--color-danger, #d64545); +} + +/* 표고축 — 내려다볼수록 짧아져 시점 기울기를 한 번 더 알린다. */ +.ui-compass-pole { + stroke: var(--color-text-body); + stroke-width: 1.6; + stroke-linecap: round; + opacity: 0.75; +} + +.ui-compass-label { + fill: var(--color-text-secondary); + font-family: var(--font-body); + font-size: 11px; + font-weight: var(--font-weight-semibold); + text-anchor: middle; +} + +.ui-compass-label--north { + fill: var(--color-danger, #d64545); +} diff --git a/B04_PreProcess/B04_PreProcess_UI_Compass.ts b/ui_template/ui_template_compass.ts similarity index 83% rename from B04_PreProcess/B04_PreProcess_UI_Compass.ts rename to ui_template/ui_template_compass.ts index 09113347..9bd58b68 100644 --- a/B04_PreProcess/B04_PreProcess_UI_Compass.ts +++ b/ui_template/ui_template_compass.ts @@ -1,4 +1,8 @@ -/* B04 지표면 3D 뷰어 방위 표시 — **지면에 누운 나침반 링**(2026-09-03 사용자 선택). +/* 3D 뷰어 방위 표시 — **지면에 누운 나침반 링**(2026-09-03 사용자 선택). + * + * B04 지표면 뷰어 전용이었으나 B05 종단 3D 뷰어도 같은 표시가 필요해져 공용으로 옮겼다 + * (2026-09-03). 스타일은 이 모듈이 직접 물고 오고(`ui_template_compass.css`), **놓을 자리만** + * 페이지가 클래스로 준다 — B04 는 우하단(축척 막대와 짝), B05 는 ISO 버튼 아래다. * * 종전에는 방위각만으로 도는 평면 콤파스였다. 3D 화면은 기울어 있어 남북이 화면에서 * 눌리는데(사시도 앙각 27.7° → 0.47배) 바늘은 안 눌린 각도로 돌아, 실제 화면 북쪽과 @@ -15,6 +19,8 @@ * 뷰어 파일이 이미 900줄을 넘어 여기로 뺐다(CLAUDE.md 4장 700줄 제한). */ +import "./ui_template_compass.css"; + export interface TerrainCompass { root: HTMLElement; /** 카메라 오프셋(카메라 위치 − 타깃)으로 링·화살·표고침을 맞춘다. */ @@ -22,7 +28,8 @@ export interface TerrainCompass { setVisible(visible: boolean): void; } -const SIZE_PX = 84; +/** 기본 크기(px). 호출부가 `sizePx` 로 덮을 수 있다 — B05 는 조금 크게 쓴다. */ +const DEFAULT_SIZE_PX = 84; const VIEW_BOX = 104; const CENTER = 0; const RING_RADIUS = 34; @@ -56,25 +63,32 @@ function svg(tag: string, attributes: Record): SVGElement { return element; } -export function createTerrainCompass(): TerrainCompass { +export interface TerrainCompassOptions { + /** 위젯 한 변의 크기(px). 기본 84. */ + sizePx?: number; + /** 놓을 자리를 정하는 페이지 클래스 — 위치 지정은 페이지 CSS 몫이다. */ + className?: string; +} + +export function createTerrainCompass(options: TerrainCompassOptions = {}): TerrainCompass { const root = document.createElement("div"); - root.className = "b04-surface__compass"; + root.className = options.className ? `ui-compass ${options.className}` : "ui-compass"; root.hidden = true; root.title = "도북(N) — 링이 지면에 누워 시점 기울기를 함께 보여 준다"; const half = VIEW_BOX / 2; const canvas = svg("svg", { viewBox: `${-half} ${-half} ${VIEW_BOX} ${VIEW_BOX}`, - width: String(SIZE_PX), - height: String(SIZE_PX), + width: String(options.sizePx ?? DEFAULT_SIZE_PX), + height: String(options.sizePx ?? DEFAULT_SIZE_PX), "aria-hidden": "true", }); - const face = svg("path", { class: "b04-surface__compass-face", d: "" }); - const ring = svg("path", { class: "b04-surface__compass-ring", d: "" }); + const face = svg("path", { class: "ui-compass-face", d: "" }); + const ring = svg("path", { class: "ui-compass-ring", d: "" }); const ticks: SVGElement[] = []; for (let bearing = 0; bearing < 360; bearing += TICK_STEP_DEG) { const tick = svg("line", { - class: `b04-surface__compass-tick${bearing % 90 === 0 ? " b04-surface__compass-tick--major" : ""}`, + class: `ui-compass-tick${bearing % 90 === 0 ? " ui-compass-tick--major" : ""}`, x1: "0", y1: "0", x2: "0", @@ -82,9 +96,9 @@ export function createTerrainCompass(): TerrainCompass { }); ticks.push(tick); } - const arrow = svg("path", { class: "b04-surface__compass-arrow", d: "" }); + const arrow = svg("path", { class: "ui-compass-arrow", d: "" }); const pole = svg("line", { - class: "b04-surface__compass-pole", + class: "ui-compass-pole", x1: "0", y1: "0", x2: "0", @@ -92,7 +106,7 @@ export function createTerrainCompass(): TerrainCompass { }); const labels = CARDINALS.map(([name]) => svg("text", { - class: `b04-surface__compass-label${name === "N" ? " b04-surface__compass-label--north" : ""}`, + class: `ui-compass-label${name === "N" ? " ui-compass-label--north" : ""}`, x: "0", y: "0", }), @@ -100,7 +114,7 @@ export function createTerrainCompass(): TerrainCompass { labels.forEach((label, index) => { label.textContent = CARDINALS[index][0]; }); - const poleLabel = svg("text", { class: "b04-surface__compass-label", x: "0", y: "0" }); + const poleLabel = svg("text", { class: "ui-compass-label", x: "0", y: "0" }); poleLabel.textContent = "Z"; canvas.append(face, ring, ...ticks, arrow, pole, ...labels, poleLabel);