Files
Aislo/B06_Section/B06_Section_UI_Cross_Panel_Base.ts
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

115 lines
4.4 KiB
TypeScript

/* =============================================================================
* B06_Section_UI_Cross_Panel_Base.ts
* 구조물 조정 오버레이 창 **공용 부품**(2026-08-25 사용자: 세월교·BOX암거 창을
* 기슭막이 창(`_Cross_Structure_Panel.ts`)과 같은 템플릿으로 일원화).
*
* 템플릿 규약:
* · 머리줄(`__head`) = 제목 + ✕ 닫기.
* · 항목 행 = 1행 이름 라벨 + 2행 값 조작(2026-08-22 확정 2행 구조).
* · **십자(D-pad)·9키 조작 세트는 항상 창 맨 아래**(2026-08-24·08-25 사용자).
* · 키보드 방향키 = 십자 버튼(2026-08-23) — 셀렉트·입력 포커스 중엔 양보.
* ========================================================================== */
/** 조작 버튼 하나 — 클릭이 카드 선택·팬으로 번지지 않게 막는다. */
export 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행 값 조작. */
export 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 };
}
/** D-pad 슬롯 버튼 — 방향키 바인딩이 슬롯 클래스로 버튼을 찾는다. */
export function dpadButton(
label: string,
slot: "up" | "down" | "left" | "right" | "reset" | "equal",
title: string,
onClick: () => void,
): HTMLButtonElement {
const button = makeButton(label, title, onClick);
button.classList.add(`b06-structure-panel__btn--${slot}`);
return button;
}
/** 창 뼈대 — root(.b06-structure-panel, 숨김 시작) + 머리줄(제목·닫기). */
export function buildPanelShell(onClose: () => void): {
root: HTMLElement;
head: HTMLElement;
title: HTMLSpanElement;
} {
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 closeButton = makeButton("✕", "닫기", onClose);
closeButton.classList.add("b06-structure-panel__close");
const head = document.createElement("div");
head.className = "b06-structure-panel__head";
head.append(title, closeButton);
root.append(head);
return { root, head, title };
}
const ARROW_SLOT: Record<string, string> = {
ArrowUp: "up",
ArrowDown: "down",
ArrowLeft: "left",
ArrowRight: "right",
};
/**
* 키보드 방향키 = 십자 버튼. `containers` 중 숨겨지지 않은 첫 행의 슬롯 버튼을
* 누른다. 창이 숨겨지면 리스너를 떼고, 카드가 통째로 사라지면 스스로 해제한다.
* 반환: show/hide 때 부를 (attach)와 (detach).
*/
export function bindArrowKeys(
root: HTMLElement,
containers: () => HTMLElement[],
): { attach: () => void; detach: () => void } {
const onArrowKey = (event: KeyboardEvent): void => {
if (!root.isConnected) {
document.removeEventListener("keydown", onArrowKey);
return;
}
const slot = ARROW_SLOT[event.key];
if (!slot || root.classList.contains("is-hidden")) return;
const focus = event.target as HTMLElement | null;
if (focus && (focus.tagName === "SELECT" || focus.tagName === "INPUT")) return;
for (const container of containers()) {
if (container.classList.contains("is-hidden")) continue;
const button = container.querySelector<HTMLButtonElement>(
`.b06-structure-panel__btn--${slot}`,
);
if (button) {
event.preventDefault();
button.click();
return;
}
}
};
return {
attach: () => document.addEventListener("keydown", onArrowKey),
detach: () => document.removeEventListener("keydown", onArrowKey),
};
}