/* ============================================================================= * B06_Section_UI_Cross_Box_Panel.ts * BOX암거 **조정 오버레이 창**(2026-08-25 사용자 확정). * * 좌·우 끝을 따로 잡는다 — 도면에서 그쪽 절반을 누르면 그 측이 선택되고, * ◀▶는 **구체 길이**를, ▲▼는 **그 끝의 구체 표고**를 0.1m씩 움직인다. 좌·우 표고가 * 달라지면 구체가 기울고, 날개벽 구간 바닥(에이프런)은 각 끝에서 수평을 유지한다. * CSS 클래스와 조작 관례는 세월교·배수관 조정창과 같다. * ========================================================================== */ import type { BoxSideAdjust, BoxSideRole } from "./B06_Section_UI_Cross_Box_Geom"; import { bindArrowKeys, buildPanelShell, dpadButton, makeRow, } from "./B06_Section_UI_Cross_Panel_Base"; /** 조정창이 쓰는 값·동작. 카드가 자기 상태에 맞춰 물려 준다. */ export interface BoxPanelDeps { /** 지금 조작값(한계에 걸린 뒤 실제 적용값). */ adjustFor: (role: BoxSideRole) => BoxSideAdjust; /** 구체 길이 ±0.1m — 화면 좌(◀)/우(▶) 부호 환산은 카드가 한다. */ nudgeLength: (role: BoxSideRole, screenDeltaM: number) => void; /** 그 끝의 구체 표고 ±0.1m. */ nudgeRise: (role: BoxSideRole, deltaM: number) => void; /** 그 측을 자동 자리로 되돌린다. */ reset: (role: BoxSideRole) => void; /** 이 측이 좌측 「구조물 배치」의 어느 칸인가 — 좌·우는 측점 지형이 정하고, 폼은 * 유입/유출로만 부른다(2026-08-30 사용자: 조정창은 결국 지우고 폼 이름을 쓴다). */ wingRoleFor?: (role: BoxSideRole) => "inlet" | "outlet"; /** 지금 그려진 구체 길이(m)와 물매(1:n, 수평이면 null). */ bodyLengthM: () => number; slopeRatio: () => number | null; /** 창을 닫는다 = 선택 해제. */ close: () => void; } export interface BoxPanelHandle { root: HTMLElement; /** null이면 숨긴다. 값이 오면 그 측 기준으로 다시 그린다. */ show: (role: BoxSideRole | null) => void; } /** 한 걸음(m) — 사용자 지정 0.1m. */ const STEP_M = 0.1; /** BOX암거 조정 오버레이 창을 만든다. 처음에는 숨어 있다. * `opts.dock` — 좌측 [횡단 조정] 사본(2026-08-29): 흐름 배치 + 방향키 미장착. */ export function buildBoxPanel(deps: BoxPanelDeps, opts?: { dock?: boolean }): BoxPanelHandle { const shell = buildPanelShell(() => deps.close()); const { root, title } = shell; if (opts?.dock) root.classList.add("b06-structure-panel--dock"); const value = document.createElement("div"); value.className = "b06-structure-panel__value"; const lengthLine = document.createElement("span"); const riseLine = document.createElement("span"); const slopeLine = document.createElement("span"); value.append(lengthLine, riseLine, slopeLine); let current: BoxSideRole | null = null; const act = (run: (role: BoxSideRole) => void) => () => { if (current) run(current); }; // 십자 조작 세트 — **창 맨 아래**(템플릿 규약 — 2026-08-25 일원화). const moveRow = makeRow("길이 ◀▶ · 표고 ▲▼"); moveRow.controls.classList.add("b06-structure-panel__buttons"); const dpad = dpadButton; moveRow.controls.append( dpad( "▲", "up", "이쪽 끝 구체 표고 0.1m 올리기", act((role) => deps.nudgeRise(role, STEP_M)), ), dpad( "◀", "left", "화면 왼쪽으로 구체 0.1m 늘리기", act((role) => deps.nudgeLength(role, STEP_M)), ), dpad("↺", "reset", "이 측 조정값 초기화", act(deps.reset)), dpad( "▶", "right", "화면 오른쪽으로 구체 0.1m 늘리기", act((role) => deps.nudgeLength(role, -STEP_M)), ), dpad( "▼", "down", "이쪽 끝 구체 표고 0.1m 내리기", act((role) => deps.nudgeRise(role, -STEP_M)), ), ); root.append(value, moveRow.row); const arrows = opts?.dock ? { attach: (): void => undefined, detach: (): void => undefined } : bindArrowKeys(root, () => [moveRow.controls]); function render(): void { if (!current) return; const side = current === "left" ? "좌" : "우"; title.textContent = `BOX암거 ${side}측 끝`; const adjust = deps.adjustFor(current); lengthLine.textContent = `구체 길이 ${deps.bodyLengthM().toFixed(2)}m` + (adjust.lengthM > 0 ? ` (이 측 +${adjust.lengthM.toFixed(1)}m)` : ""); riseLine.textContent = `표고 ${adjust.riseM >= 0 ? "+" : ""}${adjust.riseM.toFixed(1)}m`; const ratio = deps.slopeRatio(); slopeLine.textContent = ratio ? `구체 물매 1:${ratio.toFixed(1)}` : "구체 수평"; } return { root, show(role) { current = role; root.classList.toggle("is-hidden", role === null); // 좌측 [횡단 조정]에 어느 칸을 세울지 알린다 — 날개벽(유입)/(유출) 칸이다 // (세월교와 같은 규약, 2026-08-30 사용자). root.dataset.side = role === null ? "" : (deps.wingRoleFor?.(role) ?? ""); root.dataset.panelTitle = role === null ? "" : "BOX암거"; arrows.detach(); if (role !== null) arrows.attach(); render(); }, }; } /** 측점별 BOX암거 조작값 제어 — 페이지(세션·정본)가 구현한다. */ export interface BoxControl { adjustFor: (chainageM: number) => { left: BoxSideAdjust; right: BoxSideAdjust }; update: (chainageM: number, role: BoxSideRole, patch: Partial) => void; reset: (chainageM: number, role: BoxSideRole) => void; /** 본체 규격 저장 — 좌측 폼이 보낸다. B05 정본(`pipe_points`)으로 간다. */ setBody: (chainageM: number, patch: { body_width_m?: number; body_height_m?: number }) => void; /** 날개벽 제원 저장(유입·유출) — 세월교와 같은 옵션 키를 쓴다. */ setWing: ( chainageM: number, role: "inlet" | "outlet", patch: Partial<{ installed: boolean; height_m: number; length_m: number; angle_deg: number }>, ) => void; selectedFor: (chainageM: number) => BoxSideRole | null; select: (chainageM: number, role: BoxSideRole | null) => void; }