Files
Aislo/B06_Section/B06_Section_UI_Cross_Box_Panel.ts
T
eomsangdonandClaude Opus 5 f6296fa7c5 feat(B05·B06): 구조물 3D 투영 표기 · 직교 카메라 · 조정창 템플릿 일원화
3D 구조물과 성토면의 관계를 "잘라내기"에서 "투영 표기"로 바꾸고, 세월교·BOX암거
조정창을 기슭막이 템플릿으로 통일했다.

B05 코리도
- 구조물 탑뷰 투영 산출(_Corridor_Carve.ts 신규): 구조물 솔리드 링 사이 실좌표
  사각형 + 변형 성토면 영역. 노선 파라미터가 아니라 행 횡단선과 실좌표로 교차시켜
  곡선 구간·날개벽 벌어짐이 그대로 잡힌다.
- 사각형 합집합 경계(_Corridor_Union.ts 신규): 변을 교점에서 토막 내고 내부·공유
  토막을 버려 최외곽 윤곽만 남긴다(측정 133 -> 13 루프).
- 표기 모드(CARVE_APPLY=false): 서피스를 자르지 않고 ① 성토면 위 절단 투영선
  ② 원지반 최고 표고 위 Z평면에 구조물 윤곽(노랑)과 유입·유출 성토부 윤곽
  (하늘·주황)을 따로 그린다. 구조물 커브는 성토면 없다고 보고, 성토부 커브는
  구조물 없다고 보고 만든다.
- 구조물 실루엣 공용화(_Corridor_Station_Structure.ts 신규): 기슭막이·BOX암거·
  세월교 실루엣을 한 자료형으로. 성토면 강제 변형은 스위치로 끄고
  (STRUCTURE_SILHOUETTE_OVERRIDE), 노견 확장만 상시 적용(shoulderSpans).
- 평면 영역 분리(_Corridor_Region.ts), 절단기 일반화(clipGeometry가 영역 배열·
  전체 정점 속성 보간 — 리본 UV 유지). 지형은 코리도 스트립만 도려낸다.
- BOX암거 3D를 측점 프레임 직선 압출로(노선 추종 금지), 날개벽 계산 공용화
  (_Corridor_Structures_Wing.ts 신규) 후 세월교에도 날개벽 4매 추가.
- 세월교 보어를 관 련수만큼 뚫고, 보어 밴드를 단면 표고로 클램프해 구멍 찌그러짐
  제거. 날개벽 뿌리를 벽 외측면 모서리로 교정.
- 직교 카메라(_Viewer_Camera.ts 신규) — 원근 왜곡 없이 탑뷰 판독. 커서 피벗 줌이
  직교에서 camera.zoom을 함께 조인다.

B06 횡단
- 조정창 공용 부품(_Cross_Panel_Base.ts 신규): 머리줄·2행 항목·D-pad·키보드
  방향키. 세월교·BOX 조정창을 같은 템플릿으로 옮기고 십자 세트를 창 맨 아래로.
- 세월교 조정창에 날개벽 항목 추가(설치·짧은쪽 높이·길이·각도) — 정본
  pipe_points 되쓰기, 바닥판 연장은 길이×cos각으로 즉시 재계산.
- 세월교 좌우 이동을 집수정 규칙으로: 노견 수평 확장 + 상하 이동분만 성토선.

BUILD_VERSION 19 -> 31(저장 코리도 캐시 무효화). typecheck·prettier 통과.

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

131 lines
4.9 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,
buildPanelShell,
dpadButton,
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;
/** 지금 그려진 구체 길이(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암거 조정 오버레이 창을 만든다. 처음에는 숨어 있다. */
export function buildBoxPanel(deps: BoxPanelDeps): BoxPanelHandle {
const shell = buildPanelShell(() => deps.close());
const { root, title } = shell;
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);
};
// 십자 조작 세트 — **창 맨 아래**(템플릿 규약 — 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, moveRow.row);
const arrows = bindArrowKeys(root, () => [moveRow.controls]);
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);
arrows.detach();
if (role !== null) arrows.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;
selectedFor: (chainageM: number) => BoxSideRole | null;
select: (chainageM: number, role: BoxSideRole | null) => void;
}