사용자 지시 3건(2026-08-30).
1) 횡단도에서 세월교 측벽을 고르면 좌측 폼의 그 측 칸이 선다 — 배관 기슭막이가
쓰던 경로를 그대로 탄다. `SideSlots`에 `wingInlet`·`wingOutlet`을 더하고
`slotOf()`가 배관 칸 → 독립 기슭막이 칸 → 날개벽 칸 순으로 지금 서 있는 쪽을
고른다. 세월교 조정창은 `dataset.side`·`dataset.panelTitle`을 세워 어느 칸을
가리키는지 알린다. 벽 선택 뒤 `refreshAdjustSlots()`도 배관과 같게 붙였다.
2) 월류 폭·높이·수량·개략 단면 결과를 관경 바로 다음으로 옮겼다(바닥 경사도 같은
묶음에 둔다 — 물넘이포장 전용이라 세월교에서는 숨는다). 개략 단면 안내문은
읽는 글이라 10px·opacity 0.75로 줄였다.
3) 세월교·날개벽 숫자 칸을 기슭막이·집수정과 같은 [-][값][+] 묶음으로 바꿨다
(월류 폭·높이·바닥 경사·수량·날개 높이·길이·각도). `stepper()`에 표시 자릿수
인자를 더해 련·각도는 정수로 보인다("1.0련"·"45.0°" 방지). 기본값 1이라 기존
호출부는 그대로다.
자체검증(공용 브라우저 5174, 측점 7+9.7 세월교):
- tsc --noEmit 통과.
- 유입 측벽 선택 → `날개벽(유입) (세월교)` 칸 강조, 유출 측벽 → `날개벽(유출)
(세월교)`. 한 번에 한 칸만 선다.
- 폼 항목 순서 실측: 관종 > 관경 > 월류 폭 > 월류 높이 > 수량 > 날개벽(유입) …
안내문 font-size 10px, 보이는 스텝 묶음 9개.
- 수량 + → 폼 "2"·조정창 "2련", 각도 + → 폼 "50"·조정창 "50°" (소수점 없음).
- 회귀: 배관 측점 기슭막이 선택 → `유출구 (기슭막이(유출))` 강조 + 높이·길이·
기준측점 전/후 행 이식 정상.
- 조작값은 원래대로(수량 1·각도 45) 되돌려 놓았다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
306 lines
12 KiB
TypeScript
306 lines
12 KiB
TypeScript
/* =============================================================================
|
||
* 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);
|
||
// 좌측 [횡단 조정]에 어느 칸을 세울지 알린다 — 세월교는 그 측 날개벽 칸이다
|
||
// (배관 조정창이 유입구·유출구 칸을 가리키는 것과 같은 규약, 2026-08-30 지시 1).
|
||
root.dataset.side = role ?? "";
|
||
// 칸 이름이 이미 "날개벽(유입)"이라 괄호에는 구조물 이름만 붙인다.
|
||
root.dataset.panelTitle = 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`)으로 간다.
|
||
* 관종·월류 폭은 조정창에 칸이 없고 좌측 「구조물 배치」 폼만 보낸다(2026-08-30 사용자). */
|
||
setPipe: (
|
||
chainageM: number,
|
||
patch: {
|
||
pipe_diameter_mm?: number;
|
||
pipe_count?: number;
|
||
pipe_kind?: string;
|
||
ford_width_m?: 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;
|
||
}
|