Files
Aislo/A00_Common/design/design_view_profile_3d.ts
T
eomsangdonandClaude Opus 5.5 519b54eb73 feat(종단보기): 345-4 3D 모달 열 때 낡았으면 자동 다시 짓기 — 닫혀 있는 동안 안 지음 · 「3D 늦음」 = setCorridorPending 그대로 · 단추 · 열기 겹쳐도 한 번
- ProfileViewState.corridorPending(표시와 한 칸) · sections.setCorridorPending · 모달 onOpen(지형 선 뒤 · 그 사이 닫으면 안 부름)
- 시험: test_345_4_corridor_worker 4(워커 = 주 스레드 · transfer 9/9 · 저장본 먼저) · test_345_2_3d_modal 5(닫힌 동안 짓기 0) · 관련 310 통과 · typecheck 내 파일 0 오류

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
2026-10-16 11:47:31 +09:00

142 lines
6.5 KiB
TypeScript

/* =============================================================================
* design_view_profile_3d.ts
* 종단 보기의 **3D 몫** — 보기 조작 줄 콜백 · [3D 업데이트] · 등고선 다시 적용 · 3D 지형 올리기 ·
* 큰 모달(`design_view_profile_3d_modal` · 345-2 — 지형은 처음 열 때 올림).
* `design_view_profile.ts` 가 700줄을 넘어 뗀 조각(345-1). 상태는 본체가 쥔 `ProfileViewState`.
* ========================================================================== */
import { showToast } from "@ui/ui_template_elements";
import { updateContourInterval } from "../../B05_Profile/B05_Profile_Api_Fetch";
import { refreshCorridor } from "../../B05_Profile/B05_Profile_UI_Corridor";
import type { createLatestSource } from "../../B05_Profile/B05_Profile_UI_Page_Latest";
import { toBounds } from "../../B05_Profile/B05_Profile_UI_Page_Helpers";
import type { ViewControlCallbacks } from "../../B05_Profile/B05_Profile_UI_Panel_View";
import type { createRouteProfilePanel } from "../../B05_Profile/B05_Profile_UI_Profile_Panel";
import type { RouteViewer } from "../../B05_Profile/B05_Profile_UI_Viewer";
import type { DesignPageHost } from "./design_page";
import type { createProfileSections, ProfileViewState } from "./design_view_profile_sections";
import { createViewer3dModal } from "./design_view_profile_3d_modal";
export interface Profile3dDeps {
projectId: string;
state: ProfileViewState;
viewer: RouteViewer;
host: DesignPageHost;
latestSource: ReturnType<typeof createLatestSource>;
profilePanel: () => ReturnType<typeof createRouteProfilePanel>;
sections: () => ReturnType<typeof createProfileSections>;
}
export function createProfile3d(deps: Profile3dDeps) {
const { projectId, state, viewer } = deps;
/** 보기 조작 줄(ISO · 켬끔 · [3D 업데이트])이 부르는 것 — 좌측 패널 콜백에 섞어 넘긴다. */
const viewCallbacks: ViewControlCallbacks = {
onSurfaceVisible: viewer.setSurfaceVisible,
onCorridorVisible: (visible) => {
state.corridorVisible = visible;
viewer.setCorridorVisible(visible);
// 측점 바 기준면이 바뀐다(예상형상 위 ↔ 원지반 위) — 다시 그린다.
deps.sections().redrawStationLines();
},
onContoursVisible: viewer.setContoursVisible,
onAxesVisible: viewer.setAxesVisible,
onStationLinesVisible: viewer.setStationLinesVisible,
onStationLabelsVisible: viewer.setStationLabelsVisible,
onSurfaceGrayscale: viewer.setSurfaceGrayscale,
onView: viewer.setView,
onResetView: () => viewer.setView("top"),
onProjection: viewer.setProjection,
// [3D 업데이트](2026-09-01) — 밀린 계획선 편집을 예상형상·측점선에 한 번에 반영한다.
onCorridorRefresh: () => refreshOnce(),
};
/** 짓는 중이면 그 결과를 같이 기다린다 — 단추 · 모달 열기가 겹쳐도 한 번만 짓는다. */
let refreshing: Promise<void> | null = null;
function refreshOnce(): Promise<void> {
refreshing ??= refreshCorridorNow().finally(() => (refreshing = null));
return refreshing;
}
/** 낡은 3D 길 모형을 지금 노선 · 계획선으로 — 저장본이 맞으면 받기만 · 아니면 워커 미리보기. */
async function refreshCorridorNow(): Promise<void> {
const detail = state.detail;
const latest = state.latest;
if (!detail || !latest?.route?.id) return;
const routePoints = latest.route_points ?? [];
if (routePoints.length <= 1) return;
await refreshCorridor(
viewer,
projectId,
latest.route.id,
detail,
routePoints,
deps.profilePanel().alignmentSamples() ?? undefined,
);
// 측점 바·라벨·램프도 계획고를 따라 움직여야 한다(2026-08-23 지적 ③).
deps.sections().renderStationLines(detail);
deps.sections().setCorridorPending(false);
}
async function applyContours(interval: number): Promise<void> {
// 화면 전체를 잠그지 않는다 — 진행 표시는 [재적용] 버튼이 맡는다(CLAUDE.md 5장).
try {
await viewer.reloadContours(interval); // 지형 전(모달 안 엶)이면 그냥 지나감
await updateContourInterval(projectId, interval);
// 처음 열 때 올릴 지형도 새 간격으로(지형은 모달을 열 때 올림 · 345-2).
if (state.latest)
state.latest = {
...state.latest,
surface_params: { ...state.latest.surface_params, contour_interval_m: interval },
};
// 세션 캐시에도 반영해 다음 진입 시 옛 등고선 간격으로 복원되지 않게 한다.
const cached = deps.latestSource.read();
if (cached) {
deps.latestSource.write({
...cached,
surface_params: { ...cached.surface_params, contour_interval_m: interval },
});
}
} catch (error) {
showToast(error instanceof Error ? error.message : "등고선 조회에 실패했습니다.", "error");
}
}
/** ④ 3D 지형 — 모달을 처음 열 때 올린다(345-2). 실패해도 나머지는 그대로 쓴다. */
async function loadTerrain(): Promise<void> {
const surface = state.confirmedSurface;
const current = state.latest;
if (!surface || !current || deps.host.disposed()) return;
try {
// 범위는 ②에서 받아 둔 확정 응답에 이미 들어 있다 — 다시 부르지 않는다.
if (!surface.bounds) throw new Error("지표면 범위 정보를 찾을 수 없습니다.");
await viewer.loadSurface(
projectId,
surface.model_id as number,
current.surface_params.method,
current.surface_params.smooth,
current.surface_params.contour_interval_m,
toBounds(surface.bounds),
);
if (deps.host.disposed()) return; // 올리는 사이 페이지를 떠났다.
deps.sections().renderLatest(current); // 지형이 올라온 뒤에야 마커·측점선이 지형 위에 놓인다.
deps.sections().redrawStationLines();
showToast("3D 지형 준비 완료", "success");
} catch (error) {
showToast(error instanceof Error ? error.message : "3D 지형을 불러오지 못했습니다.", "error");
}
}
const modal = createViewer3dModal({
viewer,
loadTerrain,
visible: () => deps.host.visible(),
// 낡은 채 열면 그때 한 번 다시 짓는다(워커 · 345-4) — 닫혀 있는 동안은 안 짓는다.
onOpen: () => {
if (state.corridorPending) void refreshOnce();
},
});
return { viewCallbacks, applyContours, modal };
}