/* ============================================================================= * B05_Profile_UI_Panel.ts * 종단 보기(B05) 좌측 칸 재료 — 한 벌 좌측 패널(`design_shared`)에 종단 보기 몫을 넣는다. * * 칸 정의는 `_Panel_Sections`(종단 보기 전용) · `design_sections`(두 보기 공용), 재료는 * `_Panel_Settings` · `_Panel_Route` · `_Panel_View`, 값 · 상수 · 입력 칸 만들기는 `_Panel_Fields` * 에 있다(700줄 제한으로 나눔, 계획서 24-2b). 값을 읽고 되살리는 창구(`values` · `restore`)만 * 여기 남는다. 아래 고정 영역(단추 넷) · 「구조물 배치」 폼은 한 벌이 두 보기에 같은 노드로 준다. * ========================================================================== */ import type { PlacedRoutePoint } from "./B05_Profile_UI_Markers"; import type { StructureInstance } from "./B05_Profile_Api_Structures"; import type { FacilityAttributes } from "./B05_Profile_UI_Drainage_Facility"; import type { StructuresSection } from "./B05_Profile_UI_Structures_Panel"; import { bindDesignView, sharedPanel, sharedStructures, type DesignPageShared, } from "../A00_Common/design/design_shared"; import "./B05_Profile_UI_Panel_Sections"; import { STATION_INTERVAL_M, parseOptional, type RoutePanelValues, } from "./B05_Profile_UI_Panel_Fields"; import { createRoutePart } from "./B05_Profile_UI_Panel_Route"; import { createSettingsPart } from "./B05_Profile_UI_Panel_Settings"; import { createViewControls } from "./B05_Profile_UI_Panel_View"; export type { RoutePanelValues } from "./B05_Profile_UI_Panel_Fields"; interface PanelCallbacks { onSolve: () => void; /** [계획노선 편집] — 큰 모달을 열어 노선을 고친다(계산은 모달 [확인]에서만 돈다). */ onEditPlannedRoute: () => void; onContourApply: (interval: number) => void | Promise; onSurfaceVisible: (visible: boolean) => void; /** [예상형상] — 계획 코리도 서피스 + 클리핑 지형(공사 후) ↔ 원지반 완전체 전환. */ onCorridorVisible: (visible: boolean) => void; onContoursVisible: (visible: boolean) => void; onAxesVisible: (visible: boolean) => void; onStationLinesVisible: (visible: boolean) => void; /** 측점 번호·이름 라벨 표시 토글(기본 켜짐 — BP·EP·5측점 배수·구조물). */ onStationLabelsVisible: (visible: boolean) => void; /** 지표면 흑백 표시 토글(기본 꺼짐 — 무지개 고도색). */ onSurfaceGrayscale: (grayscale: boolean) => void; onView: (view: "iso" | "top" | "front" | "side") => void; /** 직교/원근 전환 — 탑뷰에서 크기를 정밀 대조할 때만 직교로 본다(2026-09-04 사용자 지시). */ onProjection: (kind: "perspective" | "ortho") => void; onResetView: () => void; /** [3D 업데이트] — 계획선 편집을 3D 예상형상·측점선에 한 번에 반영(2026-09-01 사용자 * 지시). 편집마다 따라오던 자동 갱신을 없애고 이 버튼으로만 돌린다. */ onCorridorRefresh: () => void | Promise; onMovePoint: () => void; onDeletePoint: () => void; onRadiusChange: (radius: number) => void; onInputChange: () => void; /** 구조물 목록(구조물군 B~G)이 바뀔 때 — 정본 저장·그래프 반영은 Page가 맡는다. */ onStructuresChange: (structures: StructureInstance[]) => void; /** 구조물을 목록에서 선택/해제할 때 해당 구조물(또는 null). */ onStructureSelect: (structure: StructureInstance | null) => void; /** 계곡 통과 시설 추가·수정·삭제·선택 — 관 지점 정본(배수유역 패널) 경유(2026-08-17 통합). */ onPipeFacilityAdd: (chainageM: number, attributes: FacilityAttributes) => void; onPipeFacilityUpdate: ( fromChainageM: number, toChainageM: number, attributes: FacilityAttributes, ) => void; onPipeFacilityRemove: (chainageM: number) => void; /** null = 사이드 목록 재클릭 해제 — 전 화면 선택도 함께 푼다(2026-08-18). */ onPipeFacilitySelect: (chainageM: number | null) => void; /** 이어 공사 시작 기준(시작 측점·누가거리 시작)이 바뀔 때. */ onStationDisplayChange: (offset: { station: number; cumulative: number }) => void; } export function createRoutePanel(callbacks: PanelCallbacks, shared: DesignPageShared) { const { viewControls, setCorridorPending } = createViewControls(callbacks); const settings = createSettingsPart(callbacks); const route = createRoutePart(callbacks); const { fields: routeFields } = route; const { contourInterval, gradeClass, designSpeed, terrainType, crossSampleInterval, longSampleInterval, maxGradePct, minVerticalRadius, minTangentLength, startElevationOffset, endElevationOffset, enforcePipeClearance, } = settings.fields; const { algorithm, paved, avoidPass, minCurveRadius, maxUphillGrade, maxDownhillGrade, minUphillGrade, minDownhillGrade, } = routeFields; // 「구조물 배치」 — 두 보기가 같은 폼 한 벌을 쓴다(계획서 24-3b). 종단 보기가 보일 때 폼 알림을 // 받는 동작만 여기서 건다. 계곡 통과 시설(A군)은 관 지점 정본(배수유역도)과 연동한다. const structures = sharedStructures(shared); bindDesignView(shared, "profile", { structures: { onChange: callbacks.onStructuresChange, onSelect: callbacks.onStructureSelect, getInterval: () => STATION_INTERVAL_M, onPipeAdd: callbacks.onPipeFacilityAdd, onPipeUpdate: callbacks.onPipeFacilityUpdate, onPipeRemove: callbacks.onPipeFacilityRemove, onPipeSelect: callbacks.onPipeFacilitySelect, }, }); const inputElements = [ algorithm, gradeClass, paved, avoidPass, minCurveRadius, maxUphillGrade, maxDownhillGrade, minUphillGrade, minDownhillGrade, crossSampleInterval, longSampleInterval, designSpeed, terrainType, maxGradePct, minVerticalRadius, minTangentLength, startElevationOffset, endElevationOffset, enforcePipeClearance, ]; inputElements.forEach((input) => input.addEventListener("change", callbacks.onInputChange)); // 좌측 패널 = 한 벌 등록부(`design_shared`) — 종단 보기 칸 재료만 넣는다. 아래 단추 넷 · 저장 · // [확정]은 한 벌이 두 보기 몫을 모아 낸다. Object.assign(shared.parts, { settings, route }); sharedPanel(shared); return { viewControls, /** 「구조물 배치」 섹션 API(타입 주입·목록 교체·계곡 시설 병합·선택·추가/이동/삭제). */ structures: structures as StructuresSection, /** 이어 공사 시작 기준(측점번호·누가거리 오프셋) 현재값. */ stationDisplayOffset: settings.stationDisplayOffset, /** 지금 적용되는 종단기울기 상한(%) — 그래프 위반 판정 즉시 반영용(2026-08-19). */ gradeLimitPct: settings.gradeLimitPct, /** [3D 업데이트] 대기 표시 — 편집이 3D에 아직 안 들어갔음을 버튼에 알린다. */ setCorridorPending, values(): RoutePanelValues { return { contourInterval: Number(contourInterval.value) || 1, algorithm: algorithm.value as RoutePanelValues["algorithm"], gradeClass: gradeClass.value as RoutePanelValues["gradeClass"], paved: paved.checked, minCurveRadius: parseOptional(minCurveRadius), maxUphillGrade: parseOptional(maxUphillGrade), maxDownhillGrade: parseOptional(maxDownhillGrade), minUphillGrade: parseOptional(minUphillGrade), minDownhillGrade: parseOptional(minDownhillGrade), allowAvoidPassThrough: avoidPass.checked, stationInterval: STATION_INTERVAL_M, crossSampleInterval: parseOptional(crossSampleInterval), longSampleInterval: parseOptional(longSampleInterval), designSpeed: settings.currentSpeed(), terrainType: terrainType.value as RoutePanelValues["terrainType"], maxGradePct: parseOptional(maxGradePct), minVerticalRadius: parseOptional(minVerticalRadius), minTangentLength: parseOptional(minTangentLength), startElevationOffset: parseOptional(startElevationOffset), endElevationOffset: parseOptional(endElevationOffset), enforcePipeClearance: enforcePipeClearance.checked, }; }, restore(values: Partial) { if (values.contourInterval != null) contourInterval.value = String(values.contourInterval); if (values.algorithm) algorithm.value = values.algorithm; if (values.gradeClass) gradeClass.value = values.gradeClass; if (values.paved != null) paved.checked = values.paved; if (values.minCurveRadius != null) minCurveRadius.value = String(values.minCurveRadius); if (values.maxUphillGrade != null) maxUphillGrade.value = String(values.maxUphillGrade); if (values.maxDownhillGrade != null) maxDownhillGrade.value = String(values.maxDownhillGrade); if (values.minUphillGrade != null) minUphillGrade.value = String(values.minUphillGrade); if (values.minDownhillGrade != null) minDownhillGrade.value = String(values.minDownhillGrade); if (values.allowAvoidPassThrough != null) avoidPass.checked = values.allowAvoidPassThrough; // stationInterval은 복원하지 않는다 — 20m 고정(2026-08-18). if (values.crossSampleInterval != null) crossSampleInterval.value = String(values.crossSampleInterval); if (values.longSampleInterval != null) longSampleInterval.value = String(values.longSampleInterval); if (values.gradeClass) { gradeClass.value = values.gradeClass; settings.syncSpeedChoices(); } if (values.designSpeed) designSpeed.value = String(values.designSpeed); if (values.terrainType) terrainType.value = values.terrainType; if (values.maxGradePct != null) maxGradePct.value = String(values.maxGradePct); if (values.minVerticalRadius != null) minVerticalRadius.value = String(values.minVerticalRadius); if (values.minTangentLength != null) minTangentLength.value = String(values.minTangentLength); if (values.startElevationOffset != null) startElevationOffset.value = String(values.startElevationOffset); if (values.endElevationOffset != null) endElevationOffset.value = String(values.endElevationOffset); if (values.enforcePipeClearance != null) enforcePipeClearance.checked = values.enforcePipeClearance; settings.syncCriteria(); }, setSelected(point: PlacedRoutePoint | null) { route.setSelected(point); }, }; } export type RoutePanel = ReturnType;