- UI_Map: 바탕(위성 · 등고선) + 겹(그리기 · 거리재기) · 도구줄 부품(되돌리기 · 다시하기 · 3D · 좌 · 우 회전 · 거리재기) · B05 createMapRotation · 왼쪽 끌기 이동 끔(가운데 단추 · 휠 방향 = 설정) - 새 UI_Draw: 시점에서 점 찍어 직선 · 미리보기 길이 · 기울기 · 제약 칸(자유 · 길이 고정 · 기울기 고정 · 둘 다 · 20 m · 9 %) · 종점 붙기 · 갈래 · 구역 마크 · Enter · Esc · Backspace - 새 UI_Measure: 두 점 길이 · 기울기 · 높이 차(지형 heightAt) - logic/Plan: 선 · 갈래 · 조각 꼴 다루기로 다시 씀(옛 갈래 넷 · 이은 누가 걷음) - UI_Page · Api: 지형 범위 카카오 지도 미리보기 → [이 범위 받기] · [범위 밖 더 받기] · GET · PUT plan/network · [초기화] · 조각 연도 고름 · 캐시 판 2 - UI_3D: 지형 + 그은 선(선 고름) · 예상 코리도 · 체크 칸 걷음 - 지움: UI_Cross · UI_Edit · logic/Expect · logic/Edit · test_544_1(예상 횡단 · 예상 코리도 셈이 화면에서 걷혀 지킬 대상 없음) - 시험: 530_3 = 손 긋기 꼴 다루기 + 서버 derive 대조로 다시 씀 · 553_4 D03 부품 단언 새 화면으로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
130 lines
4.5 KiB
TypeScript
130 lines
4.5 KiB
TypeScript
/* =============================================================================
|
|
* D03_PlanAnalysis_UI_3D.ts
|
|
* D03 [3D] 큰 모달(PLAN 530-3 · 563-1) — 사업 전체 DEM + 그은 선 하나(지반에 붙임) + 측점 막대.
|
|
* 선 고름 = 모달 위 칸(본선 · 갈래) · 뷰어 = B05 `createRouteViewer` 그대로 · 닫으면 버림(WebGL 문맥까지).
|
|
* 예상 코리도 없음(꺾은선 · 계획고 = 지반고라 절성토 0 — 요구서 389 3장 11).
|
|
* ========================================================================== */
|
|
|
|
import "../B05_Profile/B05_Profile_UI_Style.css";
|
|
import { createSelectField } from "@ui/ui_template_input";
|
|
import { openModal } from "@ui/ui_template_modal";
|
|
import { createProgressCircle } from "@ui/ui_template_progress";
|
|
import { showToast } from "@ui/ui_template_elements";
|
|
import type { DtmGrid } from "../common_util/common_util_surface_sampler";
|
|
import { createRouteViewer, type RouteViewer } from "../B05_Profile/B05_Profile_UI_Viewer";
|
|
import { sectionsGround } from "../B05_Profile/logic/B05_Profile_Sections_Ground";
|
|
import type { Vertex } from "./logic/D03_PlanAnalysis_Plan";
|
|
|
|
export interface Plan3dLine {
|
|
name: string;
|
|
line: Vertex[];
|
|
/** 선 위 측점 누가(선마다 No.0 부터). */
|
|
stations: number[];
|
|
}
|
|
|
|
/** 격자 → 뷰어 상자(지형에 붙는 선 · 막대는 높이 원점과 무관 · x · y 만 메시와 맞으면 됨). */
|
|
function gridBounds(grid: DtmGrid): {
|
|
x: [number, number];
|
|
y: [number, number];
|
|
z: [number, number];
|
|
} {
|
|
let low = Infinity;
|
|
let high = -Infinity;
|
|
for (const z of grid.z) {
|
|
if (!Number.isFinite(z)) continue;
|
|
if (z < low) low = z;
|
|
if (z > high) high = z;
|
|
}
|
|
return {
|
|
x: [grid.x[0], grid.x[grid.x.length - 1]],
|
|
y: [grid.y[0], grid.y[grid.y.length - 1]],
|
|
z: [low, high],
|
|
};
|
|
}
|
|
|
|
export function openPlan3d(
|
|
projectId: string,
|
|
surface: { modelId: number; grid: DtmGrid },
|
|
lines: Plan3dLine[],
|
|
): void {
|
|
if (!lines.length) return void showToast("3D 로 볼 선이 없습니다.", "warning");
|
|
let viewer: RouteViewer | null = createRouteViewer();
|
|
let loaded = false;
|
|
const pick = createSelectField({
|
|
options: lines.map((item, i) => ({ value: String(i), text: item.name })),
|
|
value: "0",
|
|
size: 28,
|
|
ariaLabel: "선",
|
|
}).select;
|
|
pick.classList.add("d03-plan__3d-year");
|
|
const top = document.createElement("div");
|
|
top.className = "d03-plan__3d-top";
|
|
top.append(pick);
|
|
|
|
const load = async (index: number): Promise<void> => {
|
|
const item = lines[index] ?? lines[0];
|
|
if (!viewer) return;
|
|
const progress = createProgressCircle({ label: "3D 지형을 불러오는 중…", overlay: true });
|
|
progress.set(null);
|
|
viewer.root.append(progress.root);
|
|
const started = performance.now();
|
|
const stat: Record<string, unknown> = { line: item.name, done: false };
|
|
(window as unknown as { __d03Plan3d?: unknown }).__d03Plan3d = stat;
|
|
try {
|
|
if (!loaded) {
|
|
await viewer.loadSurface(
|
|
projectId,
|
|
surface.modelId,
|
|
"dtm",
|
|
true,
|
|
5,
|
|
gridBounds(surface.grid),
|
|
);
|
|
loaded = true;
|
|
}
|
|
if (!viewer) return;
|
|
viewer.markers.renderRoute(item.line.map(([x, y]) => ({ x, y })));
|
|
const ground = sectionsGround(item.line, item.stations, surface.grid);
|
|
viewer.renderStationLines(
|
|
ground.stations.map((station, i) => ({
|
|
station_id: String(item.stations[i] ?? station.chainage_m),
|
|
center_x: station.center_x,
|
|
center_y: station.center_y,
|
|
center_z: station.center_z,
|
|
frame: { left_xy: station.left_xy },
|
|
kind: "regular",
|
|
chainage_m: item.stations[i] ?? station.chainage_m,
|
|
})),
|
|
2.5,
|
|
);
|
|
stat.stations = ground.stations.length;
|
|
viewer.setView("iso");
|
|
} catch (error) {
|
|
showToast(error instanceof Error ? error.message : "3D 지형을 불러오지 못했습니다.", "error");
|
|
} finally {
|
|
stat.done = true;
|
|
stat.totalMs = Math.round(performance.now() - started);
|
|
progress.remove();
|
|
}
|
|
};
|
|
pick.addEventListener("change", () => void load(Number(pick.value)));
|
|
|
|
openModal({
|
|
title: "3D 보기 — 지형 + 그은 선",
|
|
closeLabel: "닫기",
|
|
wide: true,
|
|
dialogClass: "d03-plan__3d-modal",
|
|
mount: (body) => {
|
|
body.classList.add("d03-plan__3d-body");
|
|
body.append(top, viewer!.root);
|
|
viewer!.setActive(true);
|
|
},
|
|
beforeClose: () => {
|
|
viewer?.dispose();
|
|
viewer = null;
|
|
return true;
|
|
},
|
|
});
|
|
void load(0);
|
|
}
|