- 좌측 패널 · 아래 단추 넷 · 종단도 · 구조물 배치 폼을 페이지에 한 번만 만듦(design_shared) — 두 보기가 같은 노드 - 보기는 가운데 위 칸만: 종단 보기 = 3D(design_view_profile) · 횡단 보기 = 횡단 카드(design_view_cross · 처음 보일 때 그림) - 옛 B05_Profile_UI_Page · B06_Section_UI_Page 삭제 · 한 벌이 받은 조작은 지금 보기로, 관 목록 · 횡단 재계산 알림은 두 보기 모두로 - 진행 단계 종단설계 · 횡단설계 두 칸이 보기를 따라 켜짐 · 누르면 주소만 바꿈 · 제목도 따라감(setTitle · setActiveStep) - [저장] · [확정] 한 벌 — 두 보기 몫을 모아 saveDesign 한 번 · 종단 보기 [확정] 켬(횡단 보기를 숨긴 채 세워 횡단 자료를 얻음) - [초기화] · 계획노선 편집은 같은 페이지라도 다시 세움(remountRoute) - 덧붙임: 방향키는 초점 있는 쪽만(조정창 ↔ 종단도 계획고) · 횡단 보기 계획고 편집에도 [3D 업데이트] 대기 표시 · 종단 보기 폼 관 옵션을 횡단 캐시에도 얹음 · 종단 보기에서 횡단 조정 칸 가림 - 나눔: Cross_View 756→658(_Cross_View_Frame) · Page_Structures_Panel 700→547(_Structures_Spec) · 옛 B05 Page 786 → design_view_profile 638(_Page_Latest · _Page_Stations) - 시험: test_design_view_parity 한 벌 기준(한 패널 · 보기 오가도 같은 노드) · page_single · profile_single · save_channels · save_recompute 등 옮긴 자리로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
226 lines
11 KiB
TypeScript
226 lines
11 KiB
TypeScript
/* =============================================================================
|
|
* 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<void>;
|
|
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<void>;
|
|
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<RoutePanelValues>) {
|
|
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<typeof createRoutePanel>;
|