Files
Aislo/B06_Section/B06_Section_UI_Cross_Box_Panel.ts
T
eomsangdonandClaude Opus 5 a87182496e feat(B06·B05): BOX암거 조정창 · 구체 기울기 · 3D 벽 구간 정리
조정창에서 좌·우 끝을 따로 잡는다 — ◀▶는 구체 길이, ▲▼는 그 끝의 구체 표고를
0.1m씩 움직인다. 좌·우 표고가 달라지면 구체가 기울고, 날개벽 구간 바닥(에이프런)은
각 끝 표고에서 수평을 지킨다(2026-08-25 사용자).

- `computeBoxLayout`이 좌·우를 따로 푼다: 끝 표고·길이·성토선·에이프런
- 성토선은 그쪽 표고 기준으로 물매 1:1.2를 지키므로 표고를 내리면 구체가 길어진다
- 조정창 `_Cross_Box_Panel.ts` + 제어 `createBoxControls` + `design.box_adjust` 저장
  (세션 → 캐시 → 확정 payload, 세월교와 같은 체계)
- 3D: 구체 밖(날개벽 구간)에 서 있던 벽을 없앴다 — 천장 없는 통로가 생기던 자리는
  날개벽 몫이다. 이제 상판·구체 저판·측벽 2매·에이프런 2매 + 날개벽 4매로 나뉜다
- 카드 렌더러의 구체 배선(선택 토글·강조·조정창)을 `_Cross_View_Bodies.ts`로 분리,
  연동 플래그 세션은 `_Page_Link_Session.ts`로 분리(700줄 제한)

검증(10+0.9): 구체 5.34m·수평 → ◀ 1회 5.44m → ▼ 2회 표고 −0.2m·물매 1:28.4,
에이프런 두 장 모두 수평 유지, ↺ 원복. 3D 솔리드 10개(측벽 링 0.2m).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 15:03:53 +09:00

163 lines
6.2 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";
/** 조정창이 쓰는 값·동작. 카드가 자기 상태에 맞춰 물려 준다. */
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;
/** 지금 그려진 구체 길이(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;
function makeButton(label: string, title: string, onClick: () => void): HTMLButtonElement {
const button = document.createElement("button");
button.type = "button";
button.className = "b06-structure-panel__btn";
button.textContent = label;
button.title = title;
button.addEventListener("click", (event) => {
// 카드 선택·팬으로 번지면 도면이 다시 그려져 맞춰 둔 배율이 날아간다.
event.stopPropagation();
onClick();
});
return button;
}
/** 항목 행 — 1행 이름 라벨 + 2행 값 조작(다른 조정창과 같은 2행 구조). */
function makeRow(labelText: string): { row: HTMLElement; controls: HTMLElement } {
const row = document.createElement("div");
row.className = "b06-structure-panel__struct";
const label = document.createElement("span");
label.className = "b06-structure-panel__label";
label.textContent = labelText;
const controls = document.createElement("div");
controls.className = "b06-structure-panel__controls";
row.append(label, controls);
return { row, controls };
}
/** BOX암거 조정 오버레이 창을 만든다. 처음에는 숨어 있다. */
export function buildBoxPanel(deps: BoxPanelDeps): BoxPanelHandle {
const root = document.createElement("div");
root.className = "b06-structure-panel is-hidden";
root.addEventListener("click", (event) => event.stopPropagation());
const title = document.createElement("span");
title.className = "b06-structure-panel__title";
const value = document.createElement("div");
value.className = "b06-structure-panel__value";
const lengthLine = document.createElement("span");
const riseLine = document.createElement("span");
const slopeLine = document.createElement("span");
value.append(lengthLine, riseLine, slopeLine);
let current: BoxSideRole | null = null;
const act = (run: (role: BoxSideRole) => void) => () => {
if (current) run(current);
};
const moveRow = makeRow("길이 ◀▶ · 표고 ▲▼");
moveRow.controls.classList.add("b06-structure-panel__buttons");
const dpad = (
label: string,
slot: "up" | "down" | "left" | "right" | "reset",
tip: string,
onClick: () => void,
): HTMLButtonElement => {
const button = makeButton(label, tip, onClick);
button.classList.add(`b06-structure-panel__btn--${slot}`);
return button;
};
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)),
),
);
const closeButton = makeButton("✕", "닫기", () => deps.close());
closeButton.classList.add("b06-structure-panel__close");
root.append(title, closeButton, value, moveRow.row);
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);
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;
selectedFor: (chainageM: number) => BoxSideRole | null;
select: (chainageM: number, role: BoxSideRole | null) => void;
}