- 옹벽 · 세월교 · BOX 조정 창의 값 칸(셀렉트 · −/+) 걷음 — 읽기 · 십자 · 그림 끌기 손잡이만(input 0) - 그림 끌기 손잡이: 고른 구조물을 끌면 십자와 같은 걸음 · 같은 동작 · 끄는 동안 미리 보기 - 좌측 [횡단 조정] 사본 상자 없앰 — 폼에 없던 줄(읽기 · 단 수 · 옵션 · 집수정 내공 · 세월교 측벽 높이 · 다단 벽)만 폼 칸에 옮겨 붙음 - 집수정 기준측점 전 · 후를 폼 칸으로(옵션 보이기 따름) · 옮겨 붙은 줄도 옵션 보이기 규칙 - 폼 값 = 칸 전부: 비운 칸은 그림 자동 · 가게(pipes)에서도 지움 · 조정 예약(culvertopt)에 따로 담지 않음 - 시험 test_b06_adjust_single_form 6건 · 거울 시험(집수정 전 · 후 폼 칸) 고침 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ACSGaHdLgnkEkoA4LDtMU
169 lines
7.0 KiB
TypeScript
169 lines
7.0 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_UI_Cross_Box_Panel.ts
|
|
* BOX암거 **조정 오버레이 창**(2026-08-25 사용자 확정).
|
|
*
|
|
* 좌·우 끝을 따로 잡는다 — 도면에서 그쪽 절반을 누르면 그 측이 선택되고,
|
|
* ◀▶는 **구체 길이**를, ▲▼는 **그 끝의 구체 표고**를 0.1m씩 움직인다. 좌·우 표고가
|
|
* 달라지면 구체가 기울고, 날개벽 구간 바닥(에이프런)은 각 끝에서 수평을 유지한다.
|
|
* CSS 클래스와 조작 관례는 세월교·배수관 조정창과 같다.
|
|
* ========================================================================== */
|
|
|
|
import type { BoxSideAdjust, BoxSideRole } from "./B06_Section_UI_Cross_Box_Geom";
|
|
import {
|
|
bindArrowKeys,
|
|
bindDragHandle,
|
|
buildPanelShell,
|
|
dpadButton,
|
|
dragReadout,
|
|
makeRow,
|
|
} from "./B06_Section_UI_Cross_Panel_Base";
|
|
|
|
/** 조정창이 쓰는 값·동작. 카드가 자기 상태에 맞춰 물려 준다. */
|
|
export interface BoxPanelDeps {
|
|
/** 지금 조작값(한계에 걸린 뒤 실제 적용값). */
|
|
adjustFor: (role: BoxSideRole) => BoxSideAdjust;
|
|
/** 구체 길이 ±0.1m — 화면 좌(◀)/우(▶) 부호 환산은 카드가 한다. */
|
|
nudgeLength: (role: BoxSideRole, screenDeltaM: number) => void;
|
|
/** 그 끝의 구체 표고 ±0.1m. */
|
|
nudgeRise: (role: BoxSideRole, deltaM: number) => void;
|
|
/** 그 측을 자동 자리로 되돌린다. */
|
|
reset: (role: BoxSideRole) => void;
|
|
/** 이 측이 좌측 「구조물 배치」의 어느 칸인가 — 좌·우는 측점 지형이 정하고, 폼은
|
|
* 유입/유출로만 부른다(2026-08-30 사용자: 조정창은 결국 지우고 폼 이름을 쓴다). */
|
|
wingRoleFor?: (role: BoxSideRole) => "inlet" | "outlet";
|
|
/** 지금 그려진 구체 길이(m)와 물매(1:n, 수평이면 null). */
|
|
bodyLengthM: () => number;
|
|
slopeRatio: () => number | null;
|
|
/** 창을 닫는다 = 선택 해제. */
|
|
close: () => void;
|
|
}
|
|
|
|
export interface BoxPanelHandle {
|
|
root: HTMLElement;
|
|
/** null이면 숨긴다. 값이 오면 그 측 기준으로 다시 그린다. */
|
|
show: (role: BoxSideRole | null) => void;
|
|
}
|
|
|
|
/** 한 걸음(m) — 사용자 지정 0.1m. */
|
|
const STEP_M = 0.1;
|
|
|
|
/** BOX암거 조정 오버레이 창을 만든다. 처음에는 숨어 있다. 값 칸은 없다(계획서 24-4a) —
|
|
* 오버레이 = 읽기 + 십자 + 그림 끌기 손잡이 · `opts.dock` = 좌측 폼 날개벽 칸으로 옮겨 붙는 읽기 줄. */
|
|
export function buildBoxPanel(deps: BoxPanelDeps, opts?: { dock?: boolean }): BoxPanelHandle {
|
|
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 lengthLine = document.createElement("span");
|
|
const riseLine = document.createElement("span");
|
|
const slopeLine = document.createElement("span");
|
|
const dragLine = dragReadout();
|
|
value.append(lengthLine, riseLine, slopeLine, dragLine);
|
|
|
|
let current: BoxSideRole | null = null;
|
|
const act = (run: (role: BoxSideRole) => void) => () => {
|
|
if (current) run(current);
|
|
};
|
|
|
|
// 십자 조작 세트 — **창 맨 아래**(템플릿 규약 — 2026-08-25 일원화).
|
|
const moveRow = makeRow("길이 ◀▶ · 표고 ▲▼");
|
|
moveRow.controls.classList.add("b06-structure-panel__buttons");
|
|
const dpad = dpadButton;
|
|
moveRow.controls.append(
|
|
dpad(
|
|
"▲",
|
|
"up",
|
|
"이쪽 끝 구체 표고 0.1m 올리기",
|
|
act((role) => deps.nudgeRise(role, STEP_M)),
|
|
),
|
|
dpad(
|
|
"◀",
|
|
"left",
|
|
"화면 왼쪽으로 구체 0.1m 늘리기",
|
|
act((role) => deps.nudgeLength(role, STEP_M)),
|
|
),
|
|
dpad("↺", "reset", "이 측 조정값 초기화", act(deps.reset)),
|
|
dpad(
|
|
"▶",
|
|
"right",
|
|
"화면 오른쪽으로 구체 0.1m 늘리기",
|
|
act((role) => deps.nudgeLength(role, -STEP_M)),
|
|
),
|
|
dpad(
|
|
"▼",
|
|
"down",
|
|
"이쪽 끝 구체 표고 0.1m 내리기",
|
|
act((role) => deps.nudgeRise(role, -STEP_M)),
|
|
),
|
|
);
|
|
|
|
root.append(value, ...(opts?.dock ? [] : [moveRow.row]));
|
|
|
|
const off = { attach: (): void => undefined, detach: (): void => undefined };
|
|
const arrows = opts?.dock ? off : bindArrowKeys(root, () => [moveRow.controls]);
|
|
// 그림 위 끌기 손잡이 — ◀▶ 구체 길이 · ▲▼ 표고(십자와 같은 걸음 · 24-4a).
|
|
const drag = opts?.dock
|
|
? off
|
|
: bindDragHandle(root, {
|
|
stepM: () => (current ? STEP_M : null),
|
|
preview: (text) => (dragLine.textContent = text),
|
|
commit: (left, down) => {
|
|
if (!current) return;
|
|
if (left) deps.nudgeLength(current, left * STEP_M);
|
|
if (down) deps.nudgeRise(current, -down * STEP_M);
|
|
},
|
|
});
|
|
|
|
function render(): void {
|
|
if (!current) return;
|
|
const side = current === "left" ? "좌" : "우";
|
|
title.textContent = `BOX암거 ${side}측 끝`;
|
|
const adjust = deps.adjustFor(current);
|
|
lengthLine.textContent =
|
|
`구체 길이 ${deps.bodyLengthM().toFixed(2)}m` +
|
|
(adjust.lengthM > 0 ? ` (이 측 +${adjust.lengthM.toFixed(1)}m)` : "");
|
|
riseLine.textContent = `표고 ${adjust.riseM >= 0 ? "+" : ""}${adjust.riseM.toFixed(1)}m`;
|
|
const ratio = deps.slopeRatio();
|
|
slopeLine.textContent = ratio ? `구체 물매 1:${ratio.toFixed(1)}` : "구체 수평";
|
|
}
|
|
|
|
return {
|
|
root,
|
|
show(role) {
|
|
current = role;
|
|
root.classList.toggle("is-hidden", role === null);
|
|
// 좌측 [횡단 조정]에 어느 칸을 세울지 알린다 — 날개벽(유입)/(유출) 칸이다
|
|
// (세월교와 같은 규약, 2026-08-30 사용자).
|
|
root.dataset.side = role === null ? "" : (deps.wingRoleFor?.(role) ?? "");
|
|
root.dataset.panelTitle = role === null ? "" : "BOX암거";
|
|
arrows.detach();
|
|
drag.detach();
|
|
if (role !== null) {
|
|
arrows.attach();
|
|
drag.attach();
|
|
}
|
|
render();
|
|
},
|
|
};
|
|
}
|
|
|
|
/** 측점별 BOX암거 조작값 제어 — 페이지(세션·정본)가 구현한다. */
|
|
export interface BoxControl {
|
|
adjustFor: (chainageM: number) => { left: BoxSideAdjust; right: BoxSideAdjust };
|
|
update: (chainageM: number, role: BoxSideRole, patch: Partial<BoxSideAdjust>) => void;
|
|
reset: (chainageM: number, role: BoxSideRole) => void;
|
|
/** 본체 규격 저장 — 좌측 폼이 보낸다. B05 정본(`pipe_points`)으로 간다. */
|
|
setBody: (chainageM: number, patch: { body_width_m?: number; body_height_m?: number }) => void;
|
|
/** 날개벽 제원 저장(유입·유출) — 세월교와 같은 옵션 키를 쓴다. */
|
|
setWing: (
|
|
chainageM: number,
|
|
role: "inlet" | "outlet",
|
|
patch: Partial<{ installed: boolean; height_m: number; length_m: number; angle_deg: number }>,
|
|
) => void;
|
|
selectedFor: (chainageM: number) => BoxSideRole | null;
|
|
select: (chainageM: number, role: BoxSideRole | null) => void;
|
|
}
|