/* ============================================================================= * B06_Section_UI_Cross_Ford_Panel.ts * 세월교 측벽 **조정 오버레이 창**(2026-08-25 사용자 확정). * * 배수관 조정창(`_Cross_Structure_Panel.ts`)과 같은 템플릿(공용 부품 * `_Cross_Panel_Base.ts`)을 쓴다 — 머리줄(제목·닫기), 2행 항목 구조, * **십자 조작 세트는 창 맨 아래**, 키보드 방향키 제어 포함(2026-08-25 일원화). * * 조작: 측벽 좌우(노견 확장) · 측벽 상하(1:1.2 대각) — 십자 · 그림 끌기. 측벽 높이는 폼 날개벽 * 칸으로 옮겨 붙는 줄 · 관경 · 수량 · 날개벽 제원은 폼 칸 하나(계획서 24-4a). * 바닥판 연장은 날개벽 길이×cos(각도)가 정하므로 읽기 전용으로 보여 준다. * ========================================================================== */ import type { FordAdjust, FordWallAdjust, FordWallRole } from "./B06_Section_UI_Cross_Ford_Geom"; import { bindArrowKeys, bindDragHandle, buildPanelShell, dpadButton, dragReadout, makeButton, makeRow, } from "./B06_Section_UI_Cross_Panel_Base"; /** 조정창이 쓰는 값·동작. 카드가 자기 상태에 맞춰 물려 준다. */ export interface FordPanelDeps { /** 지금 조작값(적용된 결과 — 한계에 잘린 뒤 값). */ adjustFor: (role: FordWallRole) => FordWallAdjust; /** 실제 그려진 벽 높이(m) — 자동일 때도 숫자를 보여 준다. */ heightFor: (role: FordWallRole) => number; /** 높이 ±0.1m. */ nudgeHeight: (role: FordWallRole, deltaM: number) => void; /** 좌우: 화면 좌(+)/우(−)로 미는 양(m). 부호 환산은 카드가 한다. */ nudge: (role: FordWallRole, screenDeltaM: number) => void; /** 상하: 1:1.2 사면을 타는 대각 이동(수평 성분 m, + = 사면 아래). */ nudgeSlope: (role: FordWallRole, deltaM: number) => void; /** 자동 자리로 되돌린다(세 축 모두). */ reset: (role: FordWallRole) => void; /** 바닥판 길이(m) — 날개벽 투영이 정한 값을 읽기 전용으로 보여 준다. */ slabLengthM: () => number; /** 창을 닫는다 = 구조물 선택 해제. */ close: () => void; } export interface FordPanelHandle { root: HTMLElement; /** null이면 숨긴다. 값이 오면 그 측벽 기준으로 다시 그린다. */ show: (role: FordWallRole | null) => void; } /** 한 걸음 — 집수정 9키와 같은 0.1m 눈금. */ const STEP_M = 0.1; /** 세월교 측벽 조정 오버레이 창을 만든다. 처음에는 숨어 있다. * 관경 · 수량 · 날개벽은 좌측 폼 칸이 정본이라 창에 두지 않는다(계획서 24-4a). * `opts.dock` — 좌측 폼에 옮겨 붙을 줄(읽기 · 측벽 높이)만 만든다 — 폼에 없던 칸이다. * 오버레이는 읽기 + 십자 + 그림 끌기 손잡이. */ export function buildFordPanel(deps: FordPanelDeps, opts?: { dock?: boolean }): FordPanelHandle { 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"; if (opts?.dock) value.classList.add("b06-structure-panel__struct--readout"); const heightLine = document.createElement("span"); const moveLine = document.createElement("span"); const slabLine = document.createElement("span"); const dragLine = dragReadout(); value.append(heightLine, moveLine, slabLine, dragLine); let current: FordWallRole | null = null; const act = (run: (role: FordWallRole) => void) => () => { if (current) run(current); }; // 측벽 높이 — 폼에 칸이 없던 값이라 폼 날개벽 칸으로 옮겨 붙는 줄(24-4a). const heightRow = makeRow("측벽 높이"); heightRow.row.classList.add("b06-structure-panel__struct--wall-height"); const heightValue = document.createElement("span"); heightValue.className = "b06-structure-panel__hval"; heightRow.controls.append( makeButton( "−", "측벽 높이 0.1m 낮추기", act((role) => deps.nudgeHeight(role, -STEP_M)), ), heightValue, makeButton( "+", "측벽 높이 0.1m 높이기", act((role) => deps.nudgeHeight(role, STEP_M)), ), ); // 십자 조작 세트 — **창 맨 아래**(템플릿 규약). const moveRow = makeRow("이동"); moveRow.controls.classList.add("b06-structure-panel__buttons"); moveRow.controls.append( dpadButton( "▲", "up", "사면 위로(1:1.2 대각)", act((role) => deps.nudgeSlope(role, -STEP_M)), ), dpadButton( "◀", "left", "화면 왼쪽으로", act((role) => deps.nudge(role, STEP_M)), ), dpadButton("↺", "reset", "이 측벽 조정값 초기화", act(deps.reset)), dpadButton( "▶", "right", "화면 오른쪽으로", act((role) => deps.nudge(role, -STEP_M)), ), dpadButton( "▼", "down", "사면 아래로(1:1.2 대각)", act((role) => deps.nudgeSlope(role, STEP_M)), ), ); root.append(value, opts?.dock ? heightRow.row : moveRow.row); const arrows = opts?.dock ? { attach: (): void => undefined, detach: (): void => undefined } : bindArrowKeys(root, () => [moveRow.controls]); // 그림 위 끌기 손잡이 — 십자와 같은 걸음 · 같은 동작(24-4a). const drag = opts?.dock ? { attach: (): void => undefined, detach: (): void => undefined } : bindDragHandle(root, { stepM: () => (current ? STEP_M : null), preview: (text) => (dragLine.textContent = text), commit: (left, down) => { if (!current) return; if (left) deps.nudge(current, left * STEP_M); if (down) deps.nudgeSlope(current, down * STEP_M); }, }); function render(): void { if (!current) return; const role = current === "inlet" ? "유입" : "유출"; title.textContent = `세월교 ${role} 측벽`; const adjust: FordWallAdjust = deps.adjustFor(current); heightLine.textContent = `높이 ${deps.heightFor(current).toFixed(2)}m` + (adjust.heightM === null ? " (자동)" : ""); moveLine.textContent = `좌우 ${adjust.lateralM.toFixed(1)}m · 상하 ${adjust.slopeM.toFixed(1)}m`; slabLine.textContent = `바닥판 ${deps.slabLengthM().toFixed(2)}m`; heightValue.textContent = `${deps.heightFor(current).toFixed(1)}m`; } return { root, show(role) { current = role; root.classList.toggle("is-hidden", role === null); // 좌측 폼에 어느 칸을 세울지 알린다 — 세월교는 그 측 날개벽 칸이다 // (배관 조정창이 유입구·유출구 칸을 가리키는 것과 같은 규약, 2026-08-30 지시 1). root.dataset.side = role ?? ""; // 칸 이름이 이미 "날개벽(유입)"이라 괄호에는 구조물 이름만 붙인다. root.dataset.panelTitle = role === null ? "" : "세월교"; arrows.detach(); drag.detach(); if (role !== null) { arrows.attach(); drag.attach(); } render(); }, }; } /** 측점별 세월교 조작값 제어 — 페이지(세션·정본)가 구현한다. */ export interface FordControl { adjustFor: (chainageM: number) => FordAdjust; update: (chainageM: number, role: FordWallRole, patch: Partial) => void; reset: (chainageM: number, role: FordWallRole) => void; /** 관경(mm)·수량(련)·관종·월류 폭 저장 — B05 정본(`pipe_points`)으로 간다. * 좌측 「구조물 배치」 폼만 보낸다(조정창에 칸 없음 · 계획서 24-4a). */ setPipe: ( chainageM: number, patch: { pipe_diameter_mm?: number; pipe_count?: number; pipe_kind?: string; ford_width_m?: number; }, ) => void; /** 날개벽 제원 저장 — 좌측 폼이 보낸다(계획서 24-4a). 같은 정본으로 간다. */ setWing: ( chainageM: number, role: FordWallRole, patch: Partial<{ installed: boolean; height_m: number; length_m: number; angle_deg: number }>, ) => void; /** 지금 선택된 측벽(측점별). */ selectedFor: (chainageM: number) => FordWallRole | null; select: (chainageM: number, role: FordWallRole | null) => void; }