/* ============================================================================= * B06_Section_UI_Cross_Line_Cell.ts * 횡단 카드 아래 띠 왼쪽 **공용 칸**(370-3) — 고른 선의 제목 · 축에 맞는 버튼 · 값 한 줄. * * · 안 고르면 암 경계선 ▲▼↺ · 값(암 지반만 · 토사는 빈 칸 · 띠 높이 그대로) * · 고르면 그 선(노면 · 사면 · 소단 · 중심점 · 교차점 · 암) — `_Cross_Design_Pick` 이 상태를 넣음 * · 버튼 다섯 칸(◀ ▲ ↺ ▼ ▶)은 한 벌 · 상태마다 보임 · 툴팁 · 동작만 바꿈 · 방향키는 보이는 슬롯만 찾음 * · ▲▼ 길게 누름 = 0.5 초 뒤 0.1 초마다 반복(암 · B05 종단 선 제어 꼴) * ========================================================================== */ export type CellSlot = "left" | "up" | "reset" | "down" | "right"; const SLOTS: Array<[CellSlot, string]> = [ ["left", "◀"], ["up", "▲"], ["reset", "↺"], ["down", "▼"], ["right", "▶"], ]; export interface CellButton { title: string; act: () => void; /** 길게 누르면 반복. */ hold?: boolean; } export interface LineCellState { title: string; buttons: Partial>; value: string; } export interface LineCell { root: HTMLElement; /** 버튼 줄 — 방향키 묶음(`bindArrowKeys`)이 슬롯 버튼을 찾는 그릇. */ buttons: HTMLElement; /** null = 빈 칸(자리는 남음). */ show: (state: LineCellState | null) => void; /** 값 글만 바꿈(끄는 동안 미리 보기). */ setValue: (text: string) => void; } /** * 길게 누르는 동안 같은 동작을 반복한다(B05 종단 선 제어 패턴 재사용). 0.5초 유지하면 * 0.1초 간격 반복이 시작된다. 버튼이 재빌드로 사라져도 반복이 끊기지 않도록 * 타이머·종료 감지를 버튼이 아니라 window 이벤트로 들고 있는다. */ function createHoldRepeater(): { start: (action: () => void) => void; stop: () => void } { let delayTimer = 0; let repeatTimer = 0; function stop(): void { window.clearTimeout(delayTimer); window.clearInterval(repeatTimer); delayTimer = 0; repeatTimer = 0; window.removeEventListener("pointerup", stop); window.removeEventListener("pointercancel", stop); window.removeEventListener("blur", stop); } function start(action: () => void): void { stop(); delayTimer = window.setTimeout(() => { repeatTimer = window.setInterval(action, 100); }, 500); window.addEventListener("pointerup", stop); window.addEventListener("pointercancel", stop); window.addEventListener("blur", stop); } return { start, stop }; } /** 버튼 칸 표시(보임과 무관 · 시험이 찾음) — 슬롯 클래스(`b06-structure-panel__btn--*`)는 보일 때만. */ export const CELL_BTN = "b06-cross-card__cell-btn--"; const HOLD_HINT = "\n(길게 누르면 0.5초 뒤부터 연속 조정)"; export function buildLineCell(): LineCell { const root = document.createElement("div"); root.className = "b06-design__seg b06-design__rockb b06-cross-card__linecell"; root.addEventListener("click", (event) => event.stopPropagation()); // 카드 고름 · 팬으로 안 번짐 const legend = document.createElement("span"); legend.className = "b06-design__seg-legend"; const group = document.createElement("div"); group.className = "b06-design__seg-buttons"; const readout = document.createElement("span"); readout.className = "b06-design__rockb-readout"; const repeater = createHoldRepeater(); let current: LineCellState | null = null; const run = (slot: CellSlot) => current?.buttons[slot]?.act(); const buttons = SLOTS.map(([slot, text]) => { const button = document.createElement("button"); button.type = "button"; button.className = `b06-design__rockb-btn ${CELL_BTN}${slot}`; if (slot === "reset") button.classList.add("is-reset"); button.textContent = text; // 길게 누름 — 누르면 즉시 1회 · 반복 예약 · 이어지는 click 은 삼킴 let swallowClick = false; button.addEventListener("pointerdown", (event) => { if (!current?.buttons[slot]?.hold) return; event.stopPropagation(); swallowClick = true; run(slot); repeater.start(() => run(slot)); }); button.addEventListener("click", (event) => { event.stopPropagation(); if (swallowClick) { swallowClick = false; return; } run(slot); }); group.append(button); return [slot, button] as const; }); group.append(readout); root.append(legend, group); const cell: LineCell = { root, buttons: group, show: (state) => { current = state; root.classList.toggle("is-empty", !state); legend.textContent = state?.title ?? ""; readout.textContent = state?.value ?? ""; readout.title = state?.value ?? ""; for (const [slot, button] of buttons) { const spec = state?.buttons[slot]; button.hidden = !spec; // 방향키 묶음은 슬롯 클래스로 찾음 — 숨은 버튼은 슬롯을 떼어 키를 안 삼킴(노면 ▲▼ = 종단도) button.classList.toggle(`b06-structure-panel__btn--${slot}`, !!spec); button.title = spec ? spec.title + (spec.hold ? HOLD_HINT : "") : ""; } }, setValue: (text) => { readout.textContent = text; readout.title = text; }, }; cell.show(null); return cell; } /** 암 경계선 칸 상태(안 고름 · 암 고름 둘 다) — ▲ = 위로 · ▼ = 아래로 · ↺ = 기본. */ export interface RockCellSpec { legend: string; offsetM: number; stepM: number; upTitle: string; downTitle: string; resetTitle: string; adjust: (deltaM: number) => void; reset: () => void; } export const rockValueText = (offsetM: number): string => `${offsetM >= 0 ? "+" : ""}${offsetM.toFixed(1)}m`; export function rockCellState(rock: RockCellSpec): LineCellState { return { title: rock.legend, value: rockValueText(rock.offsetM), buttons: { up: { title: `${rock.upTitle} (+${rock.stepM}m)`, act: () => rock.adjust(rock.stepM), hold: true, }, down: { title: `${rock.downTitle} (-${rock.stepM}m)`, act: () => rock.adjust(-rock.stepM), hold: true, }, reset: { title: rock.resetTitle, act: rock.reset }, }, }; }