Files
Aislo/B06_Section/B06_Section_UI_Cross_Ford_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

288 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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];
/** 세월교 측벽 조정 오버레이 창을 만든다. 처음에는 숨어 있다. */
export function buildFordPanel(deps: FordPanelDeps): FordPanelHandle {
const shell = buildPanelShell(() => deps.close());
const { root, title } = shell;
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 = 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);
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`)으로 간다. */
setPipe: (chainageM: number, patch: { pipe_diameter_mm?: number; pipe_count?: 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;
}