From 0d1d54242106c83661d9f6bfd9ad5d30398fa9e7 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 16 Oct 2026 20:42:28 +0900 Subject: [PATCH] =?UTF-8?q?feat(3D):=20353-3=203D=20=EB=93=B1=EA=B3=A0?= =?UTF-8?q?=EC=84=A0=20=EC=A3=BC=EA=B3=A1=EC=84=A0=205=20m=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95=20=C2=B7=201=20m=20=EC=98=85=EA=B2=8C=20=E2=80=94=20?= =?UTF-8?q?=ED=8F=89=EB=A9=B4=20=EC=83=89=20=ED=86=A0=ED=81=B0=20=C2=B7=20?= =?UTF-8?q?B05=20=C2=B7=20B04=20=EA=B0=99=EC=9D=80=20=EC=9E=AC=EC=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 공용 재질 ui_template_contour3d(isMajorContour · contourMaterials) — 주곡선 = 5 m 배수(간격 무관) - 색 = --map-contour-major(진하게 0.95) · --map-contour-minor(토큰 알파 0.45 · 깊이 안 씀) - B05 3D 뷰어 · B04 3D 지형 뷰어 둘 다 옛 주황 두 색 · 간격 × 5 갈래 걷음 · B04 높이 글도 5 m 선에만 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf --- ...04_PreProcess_UI_TerrainViewer_Contours.ts | 18 +++---- B05_Profile/B05_Profile_UI_Viewer.ts | 16 +++--- .../tester/test_353_3_contour3d_style.py | 30 ++++++++++++ ui_template/ui_template_contour3d.ts | 49 +++++++++++++++++++ 4 files changed, 95 insertions(+), 18 deletions(-) create mode 100644 resources/tester/test_353_3_contour3d_style.py create mode 100644 ui_template/ui_template_contour3d.ts diff --git a/B04_PreProcess/B04_PreProcess_UI_TerrainViewer_Contours.ts b/B04_PreProcess/B04_PreProcess_UI_TerrainViewer_Contours.ts index febb8b805..32c90eea2 100644 --- a/B04_PreProcess/B04_PreProcess_UI_TerrainViewer_Contours.ts +++ b/B04_PreProcess/B04_PreProcess_UI_TerrainViewer_Contours.ts @@ -10,6 +10,7 @@ import * as THREE from "three"; import { contourAssetUrl, fetchCachedJson } from "../A00_Common/b_asset_cache"; +import { contourMaterials, isMajorContour } from "@ui/ui_template_contour3d"; /** 화면에 띄우는 등고 라벨 상한 — 긴 등고선부터 채운다(분리 전 상수 그대로). */ const MAX_CONTOUR_LABELS = 40; @@ -93,7 +94,7 @@ export async function loadContourLinesInto( const points = transform(c.coordinates); if (points.length < 2) return; - const isMajor = c.level % (interval * 5) === 0; + const isMajor = isMajorContour(c.level); const bucket = isMajor ? majorPoints : minorPoints; for (let i = 0; i < points.length - 1; i++) { bucket.push(points[i], points[i + 1]); @@ -156,18 +157,15 @@ export async function loadContourLinesInto( ctx.setLabelsDirty(true); } - // 합쳐 둔 점들을 주곡선·보조곡선 각각 한 덩어리로 올린다. + // 합쳐 둔 점들을 주곡선·보조곡선 각각 한 덩어리로 올린다 — 주곡선 = 5 m 고정 · 1 m 는 + // 옅게(평면 그림과 같은 색 토큰 · 353-3). + const materials = contourMaterials(); [ - { points: minorPoints, color: 0xf59e0b }, - { points: majorPoints, color: 0xd97706 }, - ].forEach(({ points, color }) => { + { points: minorPoints, material: materials.minor }, + { points: majorPoints, material: materials.major }, + ].forEach(({ points, material }) => { if (points.length === 0) return; const geometry = new THREE.BufferGeometry().setFromPoints(points); - const material = new THREE.LineBasicMaterial({ - color, - transparent: true, - opacity: 0.8, - }); ctx.contourGroup.add(new THREE.LineSegments(geometry, material)); }); diff --git a/B05_Profile/B05_Profile_UI_Viewer.ts b/B05_Profile/B05_Profile_UI_Viewer.ts index 619242cc5..00eb84fa7 100644 --- a/B05_Profile/B05_Profile_UI_Viewer.ts +++ b/B05_Profile/B05_Profile_UI_Viewer.ts @@ -1,6 +1,7 @@ import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import { createTerrainCompass } from "@ui/ui_template_compass"; +import { contourMaterials, isMajorContour } from "@ui/ui_template_contour3d"; 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"; @@ -339,21 +340,20 @@ export function createRouteViewer(): RouteViewer { ([x, y, z]) => new THREE.Vector3(x - cx, z - cz + 0.15, -(y - cy)), ); if (points.length < 2) return; - const bucket = contour.level % (interval * 5) === 0 ? majorPoints : minorPoints; + const bucket = isMajorContour(contour.level) ? majorPoints : minorPoints; for (let index = 0; index < points.length - 1; index += 1) { bucket.push(points[index], points[index + 1]); } }); + // 주곡선 = 5 m 고정 · 1 m 는 옅게(평면 그림과 같은 색 토큰 · 353-3) + const materials = contourMaterials(); [ - { points: minorPoints, color: 0xf59e0b }, - { points: majorPoints, color: 0xd97706 }, - ].forEach(({ points, color }) => { + { points: minorPoints, material: materials.minor }, + { points: majorPoints, material: materials.major }, + ].forEach(({ points, material }) => { if (points.length === 0) return; contours.add( - new THREE.LineSegments( - new THREE.BufferGeometry().setFromPoints(points), - new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.75 }), - ), + new THREE.LineSegments(new THREE.BufferGeometry().setFromPoints(points), material), ); }); } diff --git a/resources/tester/test_353_3_contour3d_style.py b/resources/tester/test_353_3_contour3d_style.py new file mode 100644 index 000000000..6164ac994 --- /dev/null +++ b/resources/tester/test_353_3_contour3d_style.py @@ -0,0 +1,30 @@ +"""353-3 3D 등고선 — 주곡선 5 m 고정 · 1 m 옅게 · 평면 색 토큰(B05 · B04 3D 같은 재질). + +./venv/Scripts/python.exe -m pytest resources/tester/test_353_3_contour3d_style.py -q +""" + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +STYLE = ROOT / "ui_template" / "ui_template_contour3d.ts" +VIEWERS = [ + ROOT / "B05_Profile" / "B05_Profile_UI_Viewer.ts", + ROOT / "B04_PreProcess" / "B04_PreProcess_UI_TerrainViewer_Contours.ts", +] + + +def test_재질은_평면_색_토큰과_5m_고정() -> None: + src = STYLE.read_text("utf-8") + assert '"--map-contour-major"' in src and '"--map-contour-minor"' in src + assert "CONTOUR_MAJOR_STEP_M = 5;" in src + # 1 m 선 불투명 = 토큰 알파(0.45) · 주곡선은 진하게 + assert "opacity: minor.alpha" in src and "opacity: 0.95 * major.alpha" in src + + +def test_두_3D_뷰어가_같은_재질을_씀() -> None: + for path in VIEWERS: + src = path.read_text("utf-8") + assert 'from "@ui/ui_template_contour3d"' in src, path.name + assert "isMajorContour(" in src and "contourMaterials()" in src, path.name + # 옛 갈래(간격 × 5 · 주황 두 색) 없음 + assert "interval * 5" not in src and "0xf59e0b" not in src, path.name diff --git a/ui_template/ui_template_contour3d.ts b/ui_template/ui_template_contour3d.ts new file mode 100644 index 000000000..d8411d452 --- /dev/null +++ b/ui_template/ui_template_contour3d.ts @@ -0,0 +1,49 @@ +/* ============================================================================= + * ui_template_contour3d.ts + * 3D 등고선 선 재질 — 평면 그림(노선 편집)과 같은 갈래 · 같은 색 토큰(353-3). + * + * 주곡선 = 5 m 배수 고정(간격 무관 · 평면 `B05_Profile_UI_RouteEdit_Render` 와 같음) · + * 색 `--map-contour-major` 진하게 · 나머지(1 m) = `--map-contour-minor` 옅게 · 낮은 불투명. + * B05 3D(`B05_Profile_UI_Viewer`) · B04 3D(`B04_PreProcess_UI_TerrainViewer_Contours`) 가 함께 씀. + * ========================================================================== */ + +import * as THREE from "three"; +import { themeColor } from "./ui_template_palette"; + +/** 주곡선 간격(m) — 평면 그림과 같은 5 m. */ +export const CONTOUR_MAJOR_STEP_M = 5; + +/** 5 m 배수 높이면 주곡선. */ +export function isMajorContour(level: number): boolean { + return Math.abs(level - Math.round(level / CONTOUR_MAJOR_STEP_M) * CONTOUR_MAJOR_STEP_M) < 1e-6; +} + +/** CSS 색(#rrggbb · rgba(...)) → 색 + 불투명(알파 없으면 1). */ +function parseColor(value: string): { color: THREE.Color; alpha: number } { + const rgba = value.match(/rgba?\(([^)]+)\)/); + if (!rgba) return { color: new THREE.Color(value), alpha: 1 }; + const [r, g, b, a] = rgba[1].split(",").map((part) => Number(part.trim())); + return { color: new THREE.Color(r / 255, g / 255, b / 255), alpha: Number.isFinite(a) ? a : 1 }; +} + +/** 주곡선 · 1 m 선 재질 — 1 m 는 주곡선보다 옅게. */ +export function contourMaterials(): { + major: THREE.LineBasicMaterial; + minor: THREE.LineBasicMaterial; +} { + const major = parseColor(themeColor("--map-contour-major", "#818cf8")); + const minor = parseColor(themeColor("--map-contour-minor", "rgba(165, 180, 252, 0.45)")); + return { + major: new THREE.LineBasicMaterial({ + color: major.color, + transparent: true, + opacity: 0.95 * major.alpha, + }), + minor: new THREE.LineBasicMaterial({ + color: minor.color, + transparent: true, + opacity: minor.alpha, + depthWrite: false, + }), + }; +}