/* ============================================================================= * B04_PreProcess_UI_TerrainViewer_Chrome.ts * 지표면 3D 뷰어의 **DOM 뼈대** — 상태줄·표시 토글·등고선 간격 폼·뷰포트·축척 막대· * 방위 컴파스·표고 범례·로딩 서클을 만들어 넘긴다. * * `B04_PreProcess_UI_TerrainViewer` 가 700줄을 넘겨 떼어낸 조각이다(2026-09-04). * 만드는 순서·클래스·인라인 스타일은 옮기기 전 그대로다. 이벤트 배선과 THREE 렌더링은 * 뷰어 본체에 남아 있고, 여기서는 엘리먼트만 만든다. * ========================================================================== */ import { createTerrainCompass } from "@ui/ui_template_compass"; import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale"; import { createProgressCircle } from "@ui/ui_template_progress"; function L(key: keyof typeof ui_locales): string { return ui_locales[key][currentLanguageIndex]; } /** 뷰어 본체가 이어 쓸 엘리먼트 묶음 — 이름은 분리 전 지역변수와 같다. */ export type TerrainViewerChrome = ReturnType; export function buildTerrainViewerChrome() { const root = document.createElement("div"); root.className = "terrain-model-group"; const statusSpan = document.createElement("span"); statusSpan.className = "terrain-status"; statusSpan.style.fontSize = "var(--text-caption)"; statusSpan.style.color = "var(--color-text-secondary)"; statusSpan.textContent = "모델 선택 대기 중..."; const axesCheck = document.createElement("input"); axesCheck.type = "checkbox"; axesCheck.checked = false; const rightControls = document.createElement("div"); rightControls.className = "viewer-options model-display-options"; // Surface Toggle const surfLabel = document.createElement("label"); surfLabel.className = "toggle-label toggle-button"; const surfCheck = document.createElement("input"); surfCheck.type = "checkbox"; surfCheck.checked = true; surfLabel.append(surfCheck, document.createTextNode(" 서피스")); // 스무딩(tin/dtm 전용) — 지면 필터·서피스와 함께 "지표면 분석" 컨테이너로 옮겼다. // 주변 입력과 양식을 맞추려고 버튼이 아니라 드롭다운이다(2026-08-01 사용자 지시). // 상태·재렌더 배선은 여기 그대로 두고, 페이지는 이 엘리먼트를 원하는 자리에 놓기만 한다. const smoothLabel = document.createElement("label"); smoothLabel.className = "b04-surface__field"; const smoothCaption = document.createElement("span"); smoothCaption.textContent = L("B04_Surface_Field_Smoothing"); const smoothSelect = document.createElement("select"); smoothSelect.className = "b04-surface__select"; ( [ ["on", "B04_Surface_Smoothing_On"], ["off", "B04_Surface_Smoothing_Off"], ] as const ).forEach(([value, key]) => { const option = document.createElement("option"); option.value = value; option.textContent = L(key); smoothSelect.append(option); }); smoothSelect.value = "on"; smoothLabel.append(smoothCaption, smoothSelect); // Contour Toggle const contourLabel = document.createElement("label"); contourLabel.className = "toggle-label toggle-button"; const contourCheck = document.createElement("input"); contourCheck.type = "checkbox"; contourCheck.checked = true; contourLabel.append(contourCheck, document.createTextNode(" 등고선")); // Contour Interval input form const intervalForm = document.createElement("form"); intervalForm.className = "contour-interval-form"; const intervalInput = document.createElement("input"); intervalInput.type = "number"; intervalInput.value = "1.0"; intervalInput.step = "0.5"; intervalInput.min = "0.5"; intervalInput.className = "contour-interval-input"; const intervalSubmit = document.createElement("button"); intervalSubmit.type = "submit"; intervalSubmit.textContent = "적용"; intervalSubmit.className = "contour-interval-submit"; intervalForm.append( document.createTextNode("간격 "), intervalInput, document.createTextNode("m "), intervalSubmit, ); const axesLabel = document.createElement("label"); axesLabel.className = "toggle-label toggle-button"; axesLabel.append(axesCheck, document.createTextNode(" 축")); rightControls.append(axesLabel, surfLabel, contourLabel, intervalForm); // 표시 토글 묶음 + 상태 줄. 컨테이너(제목)는 페이지가 만든다 — 포인트 옵션과 한 칸을 쓴다. const optionsContent = document.createElement("div"); optionsContent.className = "terrain-options-content"; optionsContent.append(rightControls, statusSpan); // 3D View container const viewerArea = document.createElement("div"); viewerArea.className = "three-viewer"; viewerArea.style.position = "relative"; viewerArea.style.borderRadius = "0 0 var(--radius-cards) var(--radius-cards)"; viewerArea.style.overflow = "hidden"; const canvas = document.createElement("canvas"); canvas.className = "b04-surface-viewer__canvas"; viewerArea.append(canvas); root.append(viewerArea); // Scale bar overlay const scaleBar = document.createElement("div"); scaleBar.className = "b04-surface__scale"; scaleBar.hidden = true; const scaleLabel = document.createElement("span"); scaleBar.append(scaleLabel); viewerArea.append(scaleBar); // 방위 콤파스 — 축척 막대 반대편(우하단). 바늘 각도는 애니메이션 루프가 맞춘다. const compass = createTerrainCompass({ className: "b04-surface__compass" }); viewerArea.append(compass.root); // Elevation bounds legend bar overlay (I-403) const legendBar = document.createElement("div"); legendBar.style.position = "absolute"; legendBar.style.top = "16px"; legendBar.style.right = "16px"; legendBar.style.background = "rgba(255, 255, 255, 0.9)"; legendBar.style.border = "1px solid #cbd5e1"; legendBar.style.borderRadius = "6px"; legendBar.style.padding = "8px"; legendBar.style.width = "50px"; legendBar.style.display = "none"; // hidden until contours are loaded legendBar.style.flexDirection = "column"; legendBar.style.alignItems = "center"; legendBar.style.zIndex = "10"; legendBar.style.boxShadow = "0 2px 6px rgba(0,0,0,0.08)"; legendBar.style.pointerEvents = "none"; const maxValSpan = document.createElement("span"); maxValSpan.style.fontSize = "10px"; maxValSpan.style.fontWeight = "bold"; maxValSpan.style.color = "#b91c1c"; maxValSpan.style.marginBottom = "4px"; const gradientDiv = document.createElement("div"); gradientDiv.style.width = "12px"; gradientDiv.style.height = "120px"; gradientDiv.style.background = "linear-gradient(to bottom, #d60000 0%, #ff5100 25%, #e6a100 50%, #228b22 75%, #3a85ff 100%)"; gradientDiv.style.borderRadius = "2px"; gradientDiv.style.border = "1px solid #94a3b8"; const minValSpan = document.createElement("span"); minValSpan.style.fontSize = "10px"; minValSpan.style.fontWeight = "bold"; minValSpan.style.color = "#1d4ed8"; minValSpan.style.marginTop = "4px"; legendBar.append(maxValSpan, gradientDiv, minValSpan); viewerArea.append(legendBar); // 뷰포트 정중앙 로딩 서클 — 메쉬 파일은 수십 MB라 내려받는 동안 화면이 비어 보인다. const progress = createProgressCircle({ overlay: true }); progress.root.hidden = true; viewerArea.append(progress.root); return { root, statusSpan, axesCheck, rightControls, surfLabel, surfCheck, smoothLabel, smoothCaption, smoothSelect, contourLabel, contourCheck, intervalForm, intervalInput, intervalSubmit, axesLabel, optionsContent, viewerArea, canvas, scaleBar, scaleLabel, compass, legendBar, maxValSpan, gradientDiv, minValSpan, progress, }; }