import { createIconButton } from "@ui/ui_template_elements"; import { createMapTools, mapToolButton } from "@ui/ui_template_map_tools"; import { createSidePanels } from "@ui/ui_template_side_panels"; import { bindFootprintToggle } from "./B05_Profile_UI_RouteEdit_Footprint"; import { bindMarkToggle } from "./B05_Profile_UI_RouteEdit_Marks"; import { bottomRowNarrow, infoLiftPx } from "./logic/B05_Profile_RouteEdit_InfoLift"; /* ============================================================================= * B05_Profile_UI_RouteEdit_Chrome.ts * 계획노선 편집(평면)의 **뼈대** — 칸·단추·오버레이 판을 만들고 자주 쓰는 요소를 집어 준다. * * `B05_Profile_UI_RouteEdit.ts` 가 700줄을 넘겨 떼어낸 조각이다(2026-09-12). 생김새만 있고 * 동작은 없다 — 배선은 본체와 각 조각(`_Apply`·`_Rotate`·`_History`…)이 한다. * * **배치**(352-1) — 지도가 칸 전체 · 머리 줄 · 테두리 없음. 도구는 3D 와 같은 떠 있는 유리 단추 * (지도 왼쪽 위 · ☰ 비킴), [?] 조작 설명은 도구줄 아래 2열, 상태·범례는 왼쪽 아래, 잰 값은 오른쪽 * 아래. 횡단 두 판은 지도 오른쪽을 덮는 판(아래 종단 판이 그 위). * 도구줄 차례(366-2 · 381-2) — [?] | ↶ ↷ | 3D | ⟲ ⟳ · 1m 등고선 · 측점 · 기울기 · 절/성토선 · 거리 재기 | * 측점 스냅 · 간격 | 분석 지형. 단추는 공용 지도 도구줄(568-2 · `ui_template_map_tools`) 아이콘 — 긴 설명은 마우스 글. * * 자리(345-3) — 모달이 아니라 종단 보기 가운데 위 본문(옛 3D 자리)에 붙는다. 덮개 · 전체 화면 · * 닫기 단추는 걷었다 · [취소] · [확인] · [노선 반영] · [저장 전으로]는 없앰(346-3 · 352-2 — 노선은 페이지 * [저장]이 맨 앞에서 보냄) · 오른쪽 횡단 칸은 접는 칸. * 머리 `tools` 자리에 부른 쪽 단추([3D])가 앉는다. [노선 추천] · 현재 추천 · [초기화]는 좌측 * 「노선 추천」 칸으로 옮겼다(345-4 · `_Panel_Recommend`). * ========================================================================== */ /** 회전 단추 아이콘 — **반만 도는 화살표**(2026-09-12 사용자 지시 ㉙). 한 바퀴를 다 그린 * 기호(`↺`·`↻`)는 「한 바퀴 돈다」로 읽혀 한 칸씩 도는 동작과 안 맞았다. */ export const HALF_TURN_ICON = { ccw: ``, cw: ``, }; /** 되돌리기 · 다시하기 아이콘 — **곧은 꼬리 + 갈고리**(↩ ↪ · 요구 241-9). 둥근 화살표(`↶`·`↷`)는 * 회전 단추와 헷갈렸다. 종단 도구줄도 같은 그림. */ export const HISTORY_ICON = { undo: ``, redo: ``, }; export interface RouteEditChrome { overlay: HTMLElement; canvas: HTMLCanvasElement; status: HTMLElement; busy: HTMLElement; measureBox: HTMLElement; measureText: HTMLElement; measureButton: HTMLButtonElement; minorContourButton: HTMLButtonElement; /** [측점 스냅] 켬끔(요구 224-2) — `aria-pressed` · `is-active` 는 본체가 맞춤. */ stationSnapButton: HTMLButtonElement; /** 분석 지형 고르기(PLAN 133-1) — 라이다 있는 프로젝트만 보임(감싼 label 의 hidden). */ /** 설계 지형 이름(509-5) — 고르기는 추천 창 안. */ terrainName: HTMLElement; } /** 손잡이 자리(24) + 틈(8) — 접혀도 남는 폭(`.ui-side-panels` margin-left). */ const SIDE_HANDLE_PX = 32; // 글판 접기 기억(345-5 덧 3) — 페이지 동안만(모듈 값) · 다시 열어도 그대로. let hintOpen = false; const HINT_LINES = [ "노드 끌기 = 옮기기", "노드 클릭 = R 라벨", "선 두 번 클릭 = 노드 추가", "노드 오른쪽 클릭 = 삭제", "측점 눈금 클릭 = 횡단 미리보기", "Shift+클릭 = 거리·기울기", "가운데(휠) 버튼 끌기 = 지도 이동", "휠 = 확대", "Ctrl+휠 = 돌리기(한 칸 5°)", ]; let infoCollapsed = false; /** 뼈대를 만들어 `parent` 에 붙이고, 자주 쓰는 요소를 집어 돌려준다. */ export function createRouteEditChrome( parent: HTMLElement, tools: HTMLElement[] = [], ): RouteEditChrome { const overlay = document.createElement("div"); overlay.className = "b05-routeedit"; overlay.innerHTML = `
노선을 읽는 중… 예상노선(원본) 계획노선
`; parent.append(overlay); // 스냅 간격 고르기 · 설계 지형 이름 칩 — 단추 아님(도구줄에 요소로 끼움). const extras = document.createElement("div"); extras.innerHTML = ` `; const [snapStep, terrainName] = extras.children as unknown as [HTMLElement, HTMLElement]; const mapTools = createMapTools({ label: "평면 도구", hint: { lines: HINT_LINES, open: hintOpen, onToggle: (open) => (hintOpen = open) }, groups: [ [ mapToolButton("undo", { title: "되돌리기 (Ctrl+Z)", disabled: true }), mapToolButton("redo", { title: "다시하기 (Ctrl+Y)", disabled: true }), ], tools, // 부른 쪽 단추([3D]) [ "rotate-ccw", "rotate-cw", mapToolButton("contour-minor", { title: "1m 등고선 — 1m 간격 등고선을 얇고 옅게 겹칩니다 (확대하면 보임)", disabled: true, }), mapToolButton("stations", { title: "측점 — 측점 눈금 · 번호를 평면에 보이기 · 숨기기 (시점 · 종점 이름표는 늘 보임)", }), mapToolButton("slopes", { title: "기울기 — 측점 사이 기울기(%)를 평면에 보이기 · 숨기기 (확대해야 보임)", }), mapToolButton("footprint", { title: "절/성토선 — 도로 끝 · 비탈 끝(깎기 붉음 · 쌓기 청록)을 평면에 겹칩니다", }), mapToolButton("measure", { title: "거리 재기 — 노선 위 두 점을 눌러 거리·기울기를 잽니다 (Shift+클릭도 같음)", }), ], [ mapToolButton("station-snap", { title: "측점 스냅 — 켜면 고친 곡선의 가운데(MC)를 고른 간격 배수에 맞춥니다 — 일반측점(20 m)은 그대로 · MC 가 일반측점 위가 아니면 「No.N+10 (MC)」 특별측점을 더함 · 손대지 않은 곡선은 그 자리 유지 · 고정 교각점은 그대로 · 못 맞춘 곡선은 상태줄 「측점 밖 곡선」 · [저장] 때 저장", }), snapStep, ], [terrainName], ], }); mapTools.root.classList.add("b05-routeedit__tools"); mapTools.hint!.classList.add("b05-routeedit__hint"); // 가이드 촬영 · 엿보기 숨김이 잡음 overlay.querySelector('[data-slot="map-tools"]')!.replaceWith(mapTools.root); // 횡단 미리보기 = 패널 양식(382-1 · 손잡이 · 머리 · 폭 끌기 · 열림 · 폭 기억) · 자리는 지도 위 덮개 그대로 · // 기본 접힘(393-1 · 키 -v2 = 옛 열림 기억 한 번 버림) · 열림 · 닫힘은 손잡이만(513-2) · 잰 값 창이 판을 비킴(--b05-side-open). const side = document.createElement("div"); side.className = "b05-routeedit__side"; const setSideOpen = (): void => overlay.style.setProperty( "--b05-side-open", `${(panels.current() ? panels.width() : 0) + SIDE_HANDLE_PX}px`, ); const panels = createSidePanels({ prefKey: "b05-routeedit-cross-v2", panels: [{ id: "cross", title: "횡단", body: side }], defaultOpen: null, defaultWidth: 460, follow: false, onChange: setSideOpen, onResize: setSideOpen, }); overlay.querySelector(".b05-routeedit__side-wrap")!.append(panels.root); setSideOpen(); // 상태 · 범례 접기 — 접으면 한 줄 「계획노선 N m」(상태 글에서 읽음 · 345-5 덧 3). const info = overlay.querySelector(".b05-routeedit__info")!; const infoToggle = info.querySelector('[data-act="info-toggle"]')!; const summary = info.querySelector(".b05-routeedit__summary")!; const setInfo = (collapsed: boolean): void => { infoCollapsed = collapsed; info.classList.toggle("is-collapsed", collapsed); infoToggle.textContent = collapsed ? "▸" : "▾"; infoToggle.setAttribute("aria-expanded", String(!collapsed)); }; setInfo(infoCollapsed); infoToggle.addEventListener("click", () => setInfo(!infoCollapsed)); // 뜬 종단 판(이웃 칸)이 지도 아래를 덮으면 상태 · 범례를 그 위로(408-2) — 판 높이 끌기 · 접기 · // 창 분할(판이 다른 창으로 감 = 못 찾음)을 판 크기 관찰로 따라감. 횡단 판이 넓어 아래 줄이 좁으면 잰 값 창을 // 상태 글판 위로 쌓음(408-4 · 횡단 판 · 글판 크기도 관찰). let watchedProfile: Element | null = null; const liftInfo = (): void => { if (!overlay.isConnected) return; // 붙기 전 첫 알림 · 걷힌 뒤 — 판 관찰은 판과 함께 사라짐 const profile = overlay.closest(".design-page__main")?.querySelector(".b05-route-profile") ?? null; if (profile && profile !== watchedProfile) lift.observe((watchedProfile = profile)); const map = overlay.getBoundingClientRect(); const top = profile?.isConnected ? profile.getBoundingClientRect().top : null; overlay.style.setProperty( "--b05-profile-cover", `${infoLiftPx(map.bottom, map.height, top)}px`, ); const sideOpen = parseFloat(overlay.style.getPropertyValue("--b05-side-open")) || 0; overlay.classList.toggle("is-narrow-bottom", bottomRowNarrow(map.width, sideOpen)); overlay.style.setProperty( "--b05-info-h", `${Math.round(info.getBoundingClientRect().height)}px`, ); }; const lift = new ResizeObserver(liftInfo); lift.observe(overlay); lift.observe(info); lift.observe(overlay.querySelector(".b05-routeedit__side-wrap")!); const measureCloseButton = createIconButton({ glyph: "✕", title: "닫기", size: 24 }); measureCloseButton.classList.add("b05-routeedit__measure-close"); overlay.querySelector('[data-slot="measure-close"]')!.replaceWith(measureCloseButton); const canvas = overlay.querySelector(".b05-routeedit__canvas")!; const status = overlay.querySelector(".b05-routeedit__status")!; const syncSummary = (): void => { const text = status.textContent ?? ""; const length = /계획노선 ([\d.]+)m/.exec(text)?.[1]; summary.textContent = length ? `계획노선 ${length} m` : text; }; new MutationObserver(syncSummary).observe(status, { childList: true, characterData: true }); syncSummary(); const measureBox = overlay.querySelector(".b05-routeedit__measure")!; const measureText = overlay.querySelector(".b05-routeedit__measure-text")!; const measureButton = mapTools.button("measure")!; const busy = overlay.querySelector(".b05-routeedit__busy")!; const minorContourButton = mapTools.button("contour-minor")!; const stationSnapButton = mapTools.button("station-snap")!; bindFootprintToggle(mapTools.button("footprint")!); bindMarkToggle(mapTools.button("stations")!, "stations"); bindMarkToggle(mapTools.button("slopes")!, "slopes"); return { overlay, canvas, status, busy, measureBox, measureText, measureButton, minorContourButton, stationSnapButton, terrainName, }; }