- B05 좌측 패널(714줄)을 Fields · Settings · Route · View · Sections 로 나누고 createRoutePanel 은 등록부로 세움 - B06 좌측 칸(횡단 보기 설정 · 표준 횡단면 설정)을 등록부로 · 페이지는 createDesignPanel 만 부름 - 「구조물 배치」는 design_sections 한 정의로 두 보기 - 아래 단추 [초기화][저장][확정][보기 전환] — B05 [확정]은 저장 흐름 이음 전까지 꺼 둠 - [초기화] 는 design_reset 한 함수를 두 보기가 부름 - 칸 DOM 은 고치기 전과 같음(전후 DOM 대조) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EauA2ATmqNRbeV48s9CcBr
132 lines
5.7 KiB
TypeScript
132 lines
5.7 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_Panel_Route.ts
|
|
* 좌측 패널의 **노선 쪽 칸 재료** — 계획노선 편집 단추 · 경로 계산 설정(포인트 팔레트 · 알고리즘 ·
|
|
* 조건) · 선택 포인트 상세. 칸 틀은 `B05_Profile_UI_Panel_Sections.ts` 가 씌운다(등록부).
|
|
* `B05_Profile_UI_Panel.ts` 에서 떼어냈다(계획서 24-2b). 본문 로직과 수치는 그대로다.
|
|
* ========================================================================== */
|
|
|
|
import { createSelectField } from "@ui/ui_template_elements";
|
|
import type { PlacedRoutePoint, RoutePointKind } from "./B05_Profile_UI_Markers";
|
|
import { button, checkbox, numberField } from "./B05_Profile_UI_Panel_Fields";
|
|
|
|
export interface RoutePartCallbacks {
|
|
onSolve: () => void;
|
|
/** [계획노선 편집] — 큰 모달을 열어 노선을 고친다(계산은 모달 [확인]에서만 돈다). */
|
|
onEditPlannedRoute: () => void;
|
|
onMovePoint: () => void;
|
|
onDeletePoint: () => void;
|
|
onRadiusChange: (radius: number) => void;
|
|
}
|
|
|
|
export function createRoutePart(callbacks: RoutePartCallbacks) {
|
|
// 계획노선 편집 — 노선을 고치는 유일한 입구(2026-09-06 사용자 확정). 자동탐색
|
|
// (「경로 계산 설정」)은 사용 중단이라 이 자리가 노선을 바꾸는 길이다.
|
|
const plannedRouteBtn = document.createElement("button");
|
|
plannedRouteBtn.type = "button";
|
|
plannedRouteBtn.className = "b05-route__btn";
|
|
plannedRouteBtn.textContent = "계획노선 편집";
|
|
plannedRouteBtn.title = "예상노선 위에서 계획노선을 고칩니다. [확인] 때만 다시 계산합니다.";
|
|
plannedRouteBtn.addEventListener("click", () => callbacks.onEditPlannedRoute());
|
|
|
|
const paletteGrid = document.createElement("div");
|
|
paletteGrid.className = "b05-route__palette";
|
|
const pointLabels: Record<RoutePointKind, string> = {
|
|
bp: "BP 시작점",
|
|
ep: "EP 종료점",
|
|
cp: "CP 경유점",
|
|
ap: "AP 회피구역",
|
|
fp: "FP 금지구역",
|
|
};
|
|
(Object.keys(pointLabels) as RoutePointKind[]).forEach((kind) => {
|
|
const chip = document.createElement("div");
|
|
chip.className = `b05-route__chip is-${kind}`;
|
|
chip.draggable = true;
|
|
chip.textContent = pointLabels[kind];
|
|
chip.addEventListener("dragstart", (event) => event.dataTransfer?.setData("pointType", kind));
|
|
paletteGrid.append(chip);
|
|
});
|
|
|
|
// 드롭다운은 공통 컴포넌트(createSelectField) 재사용. `.select`로 받아 이하 로직 불변.
|
|
const algorithmField = createSelectField({
|
|
label: "알고리즘",
|
|
options: [
|
|
{ value: "dijkstra", text: "Dijkstra" },
|
|
{ value: "ridge_valley", text: "능선·계곡" },
|
|
],
|
|
});
|
|
const algorithm = algorithmField.select;
|
|
const minCurveRadius = numberField("최소 곡선반경 (m)");
|
|
const maxUphillGrade = numberField("오르막 경사 상한 (%)");
|
|
const maxDownhillGrade = numberField("내리막 경사 상한 (%)");
|
|
const minUphillGrade = numberField("오르막 경사 하한 (%)");
|
|
const minDownhillGrade = numberField("내리막 경사 하한 (%)");
|
|
const paved = checkbox("포장 임도", false);
|
|
const avoidPass = checkbox("회피구역 통과 허용", false);
|
|
const details = document.createElement("details");
|
|
const summary = document.createElement("summary");
|
|
summary.textContent = "사용한 조건";
|
|
details.append(
|
|
summary,
|
|
minCurveRadius.wrapper,
|
|
maxUphillGrade.wrapper,
|
|
maxDownhillGrade.wrapper,
|
|
minUphillGrade.wrapper,
|
|
minDownhillGrade.wrapper,
|
|
);
|
|
// [최적 경로 계산] 입구는 2026-09-06 사용자 확정으로 화면에서 내렸다 — 평면만 보고
|
|
// 노선을 정하는 방식이라 실제 판단(평면·종단·횡단·유토곡선을 함께 봄)과 맞지 않는다.
|
|
// 코드·API 는 그대로 두고 버튼만 뺀다. 노선 변경은 계획노선 편집이 대신한다.
|
|
const solveButton = button("최적 경로 계산", callbacks.onSolve, "filled");
|
|
solveButton.hidden = true;
|
|
|
|
const selectedName = document.createElement("strong");
|
|
const radius = numberField("회피/금지 반경 (m)", "25");
|
|
radius.addEventListener("change", () => callbacks.onRadiusChange(Number(radius.value) || 1));
|
|
const selectedActions = document.createElement("div");
|
|
selectedActions.className = "b05-route__actions";
|
|
selectedActions.append(
|
|
button("위치 이동", callbacks.onMovePoint),
|
|
button("삭제", callbacks.onDeletePoint, "danger"),
|
|
);
|
|
// 칸 뿌리는 등록부가 만든 뒤 알려 준다 — 평소 숨김(포인트를 고를 때만 보인다).
|
|
let selectedRoot: HTMLElement | null = null;
|
|
|
|
return {
|
|
plannedRouteButton: plannedRouteBtn,
|
|
/** 「경로 계산 설정」 본문 — 화면 순서대로. */
|
|
calcNodes: [
|
|
paletteGrid,
|
|
algorithmField.root,
|
|
paved.wrapper,
|
|
avoidPass.wrapper,
|
|
details,
|
|
solveButton,
|
|
] as HTMLElement[],
|
|
/** 「선택 포인트 상세 설정」 본문 — 화면 순서대로. */
|
|
selectedNodes: [selectedName, radius.wrapper, selectedActions] as HTMLElement[],
|
|
fields: {
|
|
algorithm,
|
|
paved,
|
|
avoidPass,
|
|
minCurveRadius,
|
|
maxUphillGrade,
|
|
maxDownhillGrade,
|
|
minUphillGrade,
|
|
minDownhillGrade,
|
|
},
|
|
bindSelectedRoot(root: HTMLElement): void {
|
|
selectedRoot = root;
|
|
root.hidden = true;
|
|
},
|
|
setSelected(point: PlacedRoutePoint | null): void {
|
|
if (selectedRoot) selectedRoot.hidden = !point;
|
|
if (!point) return;
|
|
selectedName.textContent = `${point.type.toUpperCase()} (${point.x.toFixed(2)}, ${point.y.toFixed(2)})`;
|
|
radius.wrapper.hidden = point.type !== "ap" && point.type !== "fp";
|
|
radius.value = String(point.radius_m ?? 25);
|
|
},
|
|
};
|
|
}
|
|
|
|
export type RoutePart = ReturnType<typeof createRoutePart>;
|