/* ============================================================================= * B06_Section_UI_Cross_Structure_Panel.ts * 횡단도 안에 뜨는 **구조물 위치 조정 오버레이 창**(2026-08-21 사용자 확정). * * 조정 조작구는 카드 하단이 아니라 **해당 횡단도 안에** 있어야 한다 — 어느 도면의 * 구조물을 만지는지 눈으로 붙어 있어야 하고, 카드 하단은 표시 반폭 조작구와 섞여 * 구분이 안 됐다. 앞으로 구조물 위치 조정은 모두 이 창을 통한다. * * 창은 그래프 영역(`.b06-cross-card__chart-wrap`) 안에 절대 위치로 얹힌다. 줌 버튼은 * 우측 상단, 면적표는 중상단이라 이 창은 **좌측 하단**에 둔다. * ========================================================================== */ import type { InletStructureChoice, RevetKey } from "./B06_Section_UI_Cross_Culvert"; import { L } from "./B06_Section_UI_Section_Common"; /** 조정창이 쓰는 값·동작. 카드가 자기 상태에 맞춰 물려 준다. */ export interface StructurePanelDeps { /** 지금 이동량(m, + = 계류측 바깥). 창을 열거나 값이 바뀔 때마다 읽는다. */ shiftFor: (key: RevetKey) => number; /** 화면 좌(+)/우(−) 방향으로 미는 양(m). 부호 환산은 카드가 한다. */ nudge: (key: RevetKey, screenDeltaM: number) => void; /** 자동 자리로 되돌린다. */ reset: (key: RevetKey) => void; /** 지금 관 길이(m) — 조정 단위가 관 길이 1m이라 창에 같이 적는다. */ pipeLengthM: () => number | null; /** 창을 닫는다 = 구조물 선택 해제. */ close: () => void; /** 유입측 구조물 형식(드롭다운 값 — 2026-08-22 사용자). */ structureFor: () => InletStructureChoice; /** 유입측 구조물 형식 변경 — 카드를 다시 그린다. */ setStructure: (value: InletStructureChoice) => void; /** ◀/▶ 이동 가능 여부 — 집수정(자리 고정)은 숨긴다. */ canNudge: (key: RevetKey) => boolean; /** 상황에 안 맞는 선택지 숨김(2026-08-22 사용자) — 기하가 판정한 가용성. */ optionsFor: () => { revetAllowed: boolean; basinLUAllowed: boolean }; /** 다단 기슭막이 상태 — 유출 벽 선택 시 개수 입력 행 표시 판단에 쓴다. */ extraState: () => { canAdd: boolean; count: number }; /** 다단 기슭막이 단 수 지정(2026-08-22 사용자 — 숫자 입력). 지형이 허락하는 * 단 수보다 크면 기하가 되는 만큼만 세우고 토스트로 알린다. */ setExtraCount: (count: number) => void; } export interface StructurePanelHandle { root: HTMLElement; /** null이면 숨긴다. 값이 오면 그 구조물 기준으로 다시 그린다. */ show: (key: RevetKey | null) => void; } /** * 한 걸음 이동량(m) — **관 길이 1m**에 맞춘다(2026-08-21 사용자 확정). * 관은 m 단위로 설치하므로 0.1m씩 밀면 그 끝수를 벽 폭이 흡수하게 되어 미는 동안 * 벽 단면이 변한다. 한 걸음을 1m로 두면 벽은 자리만 옮기고 형상은 그대로다. */ const STEP_M = 1.0; function makeButton(label: string, title: string, onClick: () => void): HTMLButtonElement { const button = document.createElement("button"); button.type = "button"; button.className = "b06-structure-panel__btn"; button.textContent = label; button.title = title; button.addEventListener("click", (event) => { // 카드 선택·팬으로 번지면 도면이 다시 그려져 방금 맞춘 배율이 날아간다. event.stopPropagation(); onClick(); }); return button; } /** 구조물 위치 조정 오버레이 창을 만든다. 처음에는 숨어 있다. */ export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHandle { const root = document.createElement("div"); root.className = "b06-structure-panel is-hidden"; // 창 안에서의 클릭이 카드 선택으로 번지지 않게 한 번에 막는다. root.addEventListener("click", (event) => event.stopPropagation()); const title = document.createElement("span"); title.className = "b06-structure-panel__title"; const value = document.createElement("span"); value.className = "b06-structure-panel__value"; let current: RevetKey | null = null; const act = (run: (key: RevetKey) => void) => () => { if (current) run(current); }; const buttons = document.createElement("div"); buttons.className = "b06-structure-panel__buttons"; buttons.append( makeButton( "◀", L("B06_Cross_Revet_Left"), act((key) => deps.nudge(key, STEP_M)), ), makeButton( "▶", L("B06_Cross_Revet_Right"), act((key) => deps.nudge(key, -STEP_M)), ), makeButton( "↺", L("B06_Cross_Revet_Reset"), act((key) => deps.reset(key)), ), ); // 다단 기슭막이 단 수 입력(2026-08-22 사용자) — 유출 벽 기준 숫자 입력. const extraRow = document.createElement("label"); extraRow.className = "b06-structure-panel__struct"; const extraLabel = document.createElement("span"); extraLabel.textContent = L("B06_Cross_Extra_Count"); const countInput = document.createElement("input"); countInput.type = "number"; countInput.min = "0"; countInput.max = "9"; countInput.step = "1"; countInput.className = "b06-structure-panel__count"; countInput.addEventListener("change", () => { const requested = Math.max(0, Math.min(9, Math.round(Number(countInput.value) || 0))); countInput.value = String(requested); deps.setExtraCount(requested); }); extraRow.append(extraLabel, countInput); // 유입측 구조물 형식 드롭다운(2026-08-22 사용자) — 자동/기슭막이/집수정 I·ㄴ·ㄷ. const structureRow = document.createElement("label"); structureRow.className = "b06-structure-panel__struct"; const structureLabel = document.createElement("span"); structureLabel.textContent = L("B06_Cross_Struct_Label"); const select = document.createElement("select"); select.className = "b06-structure-panel__select"; const OPTIONS: Array<[InletStructureChoice, string]> = [ ["auto", L("B06_Cross_Struct_Auto")], ["revet", L("B06_Cross_Struct_Revet")], ["I", L("B06_Cross_Struct_I")], ["L", L("B06_Cross_Struct_L")], ["U", L("B06_Cross_Struct_U")], ]; for (const [optionValue, label] of OPTIONS) { const option = document.createElement("option"); option.value = optionValue; option.textContent = label; select.append(option); } select.addEventListener("change", () => { deps.setStructure(select.value as InletStructureChoice); }); structureRow.append(structureLabel, select); const closeButton = makeButton("✕", L("B06_Cross_Revet_Close"), () => deps.close()); closeButton.classList.add("b06-structure-panel__close"); const head = document.createElement("div"); head.className = "b06-structure-panel__head"; head.append(title, closeButton); root.append(head, value, structureRow, extraRow, buttons); return { root, show: (key) => { current = key; root.classList.toggle("is-hidden", key === null); if (!key) return; const isExtra = key.startsWith("extra"); title.textContent = isExtra ? `${L("B06_Cross_Revet_Extra")} ${Number(key.slice(5)) + 1}` : key === "outlet" ? L("B06_Cross_Revet_Outlet") : deps.canNudge("inlet") ? L("B06_Cross_Revet_Inlet") : L("B06_Cross_Struct_InletBasin"); // 형식 선택은 유입측에서만, ◀/▶/↺는 자리를 옮길 수 있는 구조물(기슭막이)만. structureRow.classList.toggle("is-hidden", key !== "inlet"); if (key === "inlet") { const current = deps.structureFor(); // 상황에 안 맞는 선택지는 숨긴다(2026-08-22 사용자). 단 지금 선택된 값은 // 남긴다 — 숨기면 셀렉트가 빈 값이 된다. const allow = deps.optionsFor(); for (const option of select.options) { const value = option.value as InletStructureChoice; const hidden = (value === "revet" && !allow.revetAllowed) || ((value === "L" || value === "U") && !allow.basinLUAllowed); option.hidden = hidden && value !== current; } select.value = current; } buttons.classList.toggle("is-hidden", !deps.canNudge(key)); // 단 수 입력은 **유출 벽에서만** — 성토부가 5m 이상(의무)이거나 이미 단이 있을 때. const extra = deps.extraState(); extraRow.classList.toggle( "is-hidden", !(key === "outlet" && (extra.canAdd || extra.count > 0)), ); countInput.value = String(extra.count); const shift = deps.shiftFor(key); // 자동 자리 기준 이동량 — 0이면 "자동"으로 적어 손을 안 댔음을 바로 알린다. // 부호(+/−)만 적으면 좌·우 벽에서 어느 쪽인지 읽히지 않아 **안/바깥**으로 적는다. const direction = L(shift > 0 ? "B06_Cross_Revet_Outward" : "B06_Cross_Revet_Inward"); const moved = Math.abs(shift) < 1e-9 ? L("B06_Cross_Revet_Auto") : `${direction} ${Math.abs(shift).toFixed(1)}m`; const pipeLength = deps.pipeLengthM(); value.textContent = pipeLength === null ? moved : `${L("B06_Cross_Revet_Pipe")} ${pipeLength}m · ${moved}`; }, }; }