Files
Aislo/B06_Section/B06_Section_UI_Cross_Line_Cell.ts
T
eomsangdonandClaude Opus 5.5 fe025b6db2 feat(B06 횡단): 암 경계선 고름 · 방향키 · 세로 끌기 · 아래 띠 공용 칸 (370-3)
- 새 B06_Section_UI_Cross_Line_Cell.ts — 아래 띠 왼쪽 공용 칸(제목 · ◀▲↺▼▶ 한 벌 · 값 한 줄 말줄임 · 길게 누름 반복) · 숨은 버튼은 방향키 슬롯 뗌
- Design_Pick — 암 점선 위 판정 선(키 rock · ▲▼↺ · ◀▶ 막음 · 방향키 = 암만) · 떠 있는 선 조정창 · 우측 위 치수 라벨 걷음 · 안 고르면 암 칸 · 고르면 그 선(노면 · 사면 · 소단 · 중심점 · 교차점 · 암)
- Center.ts bindAxisDrag(한 축 끌기 · 놓을 때 한 번 · click 한 번 막음) — 중심점 · 암 세로 끌기 같이 씀
- buildRockBoundaryControl = 공용 칸 「안 고름」 꼴(Cross_Design) · Card_Chrome 띠 왼쪽 = 공용 칸 · Detail_Pick 표 강조에 암 판정 선 뺌
- CSS(Cross.css 공용 칸 · Areas.css 암 고름 띠 · Controls.css 주석) · 시험 test_370_3 새로 · 137 · 139_6 · 179_1 · 193_1 · 194_1 · 318 · 356 · 370_2 고쳐 씀

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
2026-10-17 12:14:43 +09:00

173 lines
6.3 KiB
TypeScript

/* =============================================================================
* 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<Record<CellSlot, CellButton>>;
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 },
},
};
}