Files
Aislo/B06_Section/B06_Section_UI_Cross_Ford_Panel.ts
T
eomsangdon 2e28af6e86 feat(B05,B06): 구조물 배치 컨테이너를 B06에 장착하고 [횡단 조정] 사본을 세운다
B05/B06 인터페이스 일원화 1차 (2026-08-29 사용자 확정 계획):
- A00_Common/b_structures_section.ts: B05 구조물 배치 폼·목록 공용 진입점
  (재수출 + 스타일 동봉)
- B06 좌측 패널에 [구조물 배치] 섹션 + 하단 dock [목록][구분선][저장버튼]
  (B05 동일 템플릿). 구조물 조작은 세션 pending으로 쌓여 기존
  flushPendingStructures([임시저장]·[확정])로 정본 반영
- 선택 연동: 목록·폼 → 최근접 카드 focusStation, 횡단도 벽·세월교·BOX →
  좌측 폼에 소유 측점 시설 로드(wireStructureSelection)
- [횡단 조정] 하위 컨테이너(Adjust_Dock): 오버레이 조정창을 같은 deps로
  사본 인스턴스 생성해 세움 — 로직 이동 0, 카드 재렌더로 자동 동기.
  9키·십자 행은 사본에서 숨김(사용자 지시 7), 방향키 이중 입력 방지
- A군 시설 추가·이동·삭제는 B06에서 안내 토스트(배수유역 체인은 B05 몫)

검증: 공용 브라우저 — 구간값 길이 사본 + 10.0→11.0m 양측 동기,
오버레이 - 원복, ✕ 닫기 시 사본 해제. B05 회귀 없음. typecheck 통과.
2026-08-29 14:20:49 +09:00

292 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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 대각) · 관경 · 관 수량 ·
* **날개벽(설치·짧은쪽 높이·길이·각도)**(2026-08-25 사용자 — 항목 없던 것 추가).
* 바닥판 연장은 날개벽 길이×cos(각도)가 정하므로 읽기 전용으로 보여 준다.
* ========================================================================== */
import type { FordWingSpec } from "./B06_Section_Api_Fetch";
import type { FordAdjust, FordWallAdjust, FordWallRole } from "./B06_Section_UI_Cross_Ford_Geom";
import {
bindArrowKeys,
buildPanelShell,
dpadButton,
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;
/** 관경(mm)·수량(련) — 값은 B05 정본(`pipe_points`)에 되돌려 쓴다. */
pipeDiameterMm: () => number;
setPipeDiameterMm: (value: number) => void;
pipeCount: () => number;
setPipeCount: (value: number) => void;
/** 날개벽 제원(그 측) — 설치·짧은쪽 높이·길이·각도(2026-08-25 사용자). */
wingFor: (role: FordWallRole) => FordWingSpec | null;
setWing: (
role: FordWallRole,
patch: Partial<{ installed: boolean; height_m: number; length_m: number; angle_deg: number }>,
) => 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;
/** 날개벽 각도 한 걸음(°). */
const ANGLE_STEP_DEG = 5;
/** 관경 선택지(mm) — B05 레지스트리 `ford_bridge.pipe_diameter_mm` choices와 같다. */
const DIAMETER_CHOICES_MM = [800, 1000, 1200, 1500];
/** 세월교 측벽 조정 오버레이 창을 만든다. 처음에는 숨어 있다.
* `opts.dock` — 좌측 [횡단 조정] 사본(2026-08-29): 흐름 배치 + 방향키 미장착. */
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";
const heightLine = document.createElement("span");
const moveLine = document.createElement("span");
const slabLine = document.createElement("span");
value.append(heightLine, moveLine, slabLine);
let current: FordWallRole | null = null;
const act = (run: (role: FordWallRole) => void) => () => {
if (current) run(current);
};
const heightRow = makeRow("측벽 높이");
heightRow.controls.append(
makeButton(
"",
"측벽 높이 0.1m 낮추기",
act((role) => deps.nudgeHeight(role, -STEP_M)),
),
makeButton(
"",
"측벽 높이 0.1m 높이기",
act((role) => deps.nudgeHeight(role, STEP_M)),
),
);
const pipeRow = makeRow("관경 · 수량");
const diameter = document.createElement("select");
diameter.className = "b06-structure-panel__select";
for (const mm of DIAMETER_CHOICES_MM) {
const option = document.createElement("option");
option.value = String(mm);
option.textContent = ${mm}`;
diameter.append(option);
}
diameter.addEventListener("change", () => deps.setPipeDiameterMm(Number(diameter.value)));
const countValue = document.createElement("span");
countValue.className = "b06-structure-panel__count";
pipeRow.controls.append(
diameter,
makeButton("", "관 수량 1련 줄이기", () =>
deps.setPipeCount(Math.max(1, deps.pipeCount() - 1)),
),
countValue,
makeButton("", "관 수량 1련 늘리기", () => deps.setPipeCount(deps.pipeCount() + 1)),
);
// ── 날개벽(2026-08-25 사용자 — 제어 항목 추가): 설치 토글 + 높이·길이·각도.
// 각도는 관축 기준 벌어짐각 — 바닥판 연장 = 길이 × cos(각도)로 따라 바뀐다.
const wingRow = makeRow("날개벽");
const wingToggle = document.createElement("button");
wingToggle.type = "button";
wingToggle.className = "b06-structure-panel__btn b06-structure-panel__toggle";
wingToggle.textContent = "설치";
wingToggle.title = "날개벽 설치/해제 — 해제하면 바닥판 연장도 0이 된다.";
wingToggle.addEventListener("click", (event) => {
event.stopPropagation();
if (!current) return;
const wing = deps.wingFor(current);
deps.setWing(current, { installed: !(wing?.installed ?? false) });
});
wingRow.controls.append(wingToggle);
const wingValueRows: Array<{
row: HTMLElement;
value: HTMLSpanElement;
read: (wing: FordWingSpec) => string;
}> = [];
const makeWingValueRow = (
label: string,
stepText: string,
read: (wing: FordWingSpec) => string,
apply: (wing: FordWingSpec, direction: 1 | -1) => void,
): void => {
const parts = makeRow(label);
const readout = document.createElement("span");
readout.className = "b06-structure-panel__hval";
const nudgeWing = (direction: 1 | -1) => () => {
if (!current) return;
const wing = deps.wingFor(current);
if (wing) apply(wing, direction);
};
parts.controls.append(
makeButton("", `${label} ${stepText} 줄이기`, nudgeWing(-1)),
readout,
makeButton("", `${label} ${stepText} 늘리기`, nudgeWing(1)),
);
wingValueRows.push({ row: parts.row, value: readout, read });
};
makeWingValueRow(
"날개 짧은쪽 높이",
`${STEP_M}m`,
(wing) => `${(wing.height_m ?? 0).toFixed(1)}m`,
(wing, direction) => {
if (!current) return;
deps.setWing(current, {
height_m: Math.max(0.3, (wing.height_m ?? 1) + direction * STEP_M),
});
},
);
makeWingValueRow(
"날개 길이",
`${STEP_M}m`,
(wing) => `${(wing.length_m ?? 0).toFixed(1)}m`,
(wing, direction) => {
if (!current) return;
deps.setWing(current, {
length_m: Math.max(0, (wing.length_m ?? 0) + direction * STEP_M),
});
},
);
makeWingValueRow(
"날개 각도",
`${ANGLE_STEP_DEG}°`,
(wing) => `${Math.round(wing.angle_deg ?? 45)}°`,
(wing, direction) => {
if (!current) return;
deps.setWing(current, {
angle_deg: Math.min(85, Math.max(5, (wing.angle_deg ?? 45) + direction * ANGLE_STEP_DEG)),
});
},
);
// 십자 조작 세트 — **창 맨 아래**(템플릿 규약).
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,
heightRow.row,
pipeRow.row,
wingRow.row,
...wingValueRows.map((entry) => entry.row),
moveRow.row,
);
const arrows = opts?.dock
? { attach: (): void => undefined, detach: (): void => undefined }
: bindArrowKeys(root, () => [moveRow.controls]);
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 (날개벽 각도 종속)`;
diameter.value = String(deps.pipeDiameterMm());
countValue.textContent = `${deps.pipeCount()}련`;
const wing = deps.wingFor(current);
const installed = wing?.installed ?? false;
wingToggle.classList.toggle("is-on", installed);
wingToggle.setAttribute("aria-pressed", String(installed));
for (const entry of wingValueRows) {
entry.row.classList.toggle("is-hidden", !installed);
if (wing && installed) entry.value.textContent = entry.read(wing);
}
}
return {
root,
show(role) {
current = role;
root.classList.toggle("is-hidden", role === null);
arrows.detach();
if (role !== null) arrows.attach();
render();
},
};
}
/** 측점별 세월교 조작값 제어 — 페이지(세션·정본)가 구현한다. */
export interface FordControl {
adjustFor: (chainageM: number) => FordAdjust;
update: (chainageM: number, role: FordWallRole, patch: Partial<FordWallAdjust>) => void;
reset: (chainageM: number, role: FordWallRole) => void;
/** 관경(mm)·수량(련) 저장 — B05 정본(`pipe_points`)으로 간다. */
setPipe: (chainageM: number, patch: { pipe_diameter_mm?: number; pipe_count?: number }) => void;
/** 날개벽 제원 저장(2026-08-25 사용자 — 조정창에서 직접 제어). 같은 정본으로 간다. */
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;
}