- 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
142 lines
6.5 KiB
TypeScript
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 };
|
|
}
|