refactor(B04): 지표면 3D 뷰어 700줄 분리 (동작 불변)
B04_PreProcess_UI_TerrainViewer.ts 958 -> 698줄. - _TerrainViewer_Chrome.ts(209줄) — 상태줄·표시 토글·등고선 간격 폼·뷰포트·축척·컴파스· 표고 범례·로딩 서클 DOM 생성. 만드는 순서·클래스·인라인 스타일 그대로. - _TerrainViewer_Contours.ts(189줄) — 등고선 적재(fetch·라인 생성·라벨·범례). 뷰어 클로저가 쥐던 값만 ctx 로 받고 본문 수치·로직은 그대로. 호출부는 얇은 래퍼로 유지. 검증: export 2개(SurfaceTerrainViewer · createSurfaceTerrainViewer) 분리 전과 동일, tsc --noEmit 통과, prettier 적용. 공용 브라우저(5174) B04 실조작 — 뷰어 2개·캔버스 2개 렌더, 상태줄 CLASSIFICATION · DTM · 등고선 1m, 등고 라벨 40개(상한), 범례 940m/760m, 등고선 토글을 끄면 라벨 0 → 켜면 40 복귀, 간격 5m 적용 시 상태가 5m 로 바뀌고 1m 로 되돌리면 원상 복귀. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
/* =============================================================================
|
||||
* 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<typeof buildTerrainViewerChrome>;
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user