Files
Aislo/B06_Section/B06_Section_UI_Cross_Culvert_Wire.ts
T
eomsangdonandClaude Opus 5 ff3e3b734b feat(B06): 기슭막이 4축 조작 — 좌우(노견 연장)·상하(사면 대각)·높이·재질
- 좌우 ◀▶ = 노견(추가 벽은 윗단 0.5m 기준선) 수평 연장 평행이동 —
  성토선 물매 1:1.2·상단 표고 불변, 지반에 못 닿으면 바닥 뜸 허용
- 상하 ▲▼ = 성토선을 타는 대각 이동(수평 성분 1m), 위 한계 =
  최소 성토고·토피(역경사), 추가 벽은 윗단 하단 관통 금지
- 높이 ±0.1m: 배관 벽 2.0~재질한계(기본 2.5, 메쌓기면 2.0로 절삭),
  일반 벽 0.5~재질한계(기본 1.5). 높이 기준 = 근입 0.5 위 기준선~상단
- 재질 선택(메쌓기 2.0/찰쌓기 3.0/콘크리트 5.0m) — 한계 초과 시
  토스트로 재질 변경 안내, 자동 높이는 기본값 고정(사용자 확정)
- 다단 단 수: 숫자 입력 + +/- 버튼 병행, 입력기 스피너 제거
- 저장 스키마: revetx 값이 {x,d,h,m} 객체로 확장(구 숫자 형식 호환)
- 인터페이스 3종을 Wire로 이전(Cross_View 700줄 제한)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 14:21:30 +09:00

144 lines
6.7 KiB
TypeScript

/* =============================================================================
* B06_Section_UI_Cross_Culvert_Wire.ts
* 횡단 카드의 배수관 세트 **계산 호출 + 이동량 되받기(토스트)** — Cross_View에서
* 분리(700줄 제한). 카드가 다시 그려질 때마다 한 번 불린다.
* ========================================================================== */
import type { CrossSection, SectionSample } from "./B06_Section_Api_Fetch";
import { computeCulvertLayout } from "./B06_Section_UI_Cross_Culvert";
import type { CulvertLayout, InletStructureChoice, RevetKey } from "./B06_Section_UI_Cross_Culvert";
import { materialLabel, materialLimit } from "./B06_Section_UI_Cross_Culvert_Const";
import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types";
/**
* 기슭막이 4축 조작 제어(2026-08-22 확정) — 벽을 눌러 고르고 조정창에서
* 좌우(x)·상하(d, 성토선 대각)·높이(h)·재질(m)을 만진다. 고른 벽은 여기 담아 두어
* 카드가 다시 그려져도 되살아난다. `select`는 값만 담고 다시 그리지 않는다.
*/
export interface RevetOffsetControl {
adjustFor: (section: CrossSection, role: RevetKey) => WallAdjust;
selectedFor: (section: CrossSection) => RevetKey | null;
select: (chainageM: number, key: RevetKey | null) => void;
/** 기하가 실제로 적용한 조작값을 되받아 담는다(한계에 걸린 요청값을 잘라 낸다). */
syncApplied: (chainageM: number, role: RevetKey, applied: WallAdjust) => void;
/** 일부 축만 바꾼다 — 나머지는 유지. h: null = 기본값 복귀. */
update: (chainageM: number, role: RevetKey, patch: Partial<WallAdjust>) => void;
reset: (chainageM: number, role: RevetKey) => void;
}
/** 유입측 구조물 형식 선택(드롭다운 — 2026-08-22 사용자). 세션 보관은 Page가 한다. */
export interface InletStructureControl {
valueFor: (section: CrossSection) => InletStructureChoice;
set: (chainageM: number, value: InletStructureChoice) => void;
}
/** 유출측 다단 기슭막이 단 수 제어(2026-08-22 사용자 — 유출 벽 기준 숫자 입력). */
export interface ExtraWallControl {
countFor: (section: CrossSection) => number;
/** 단 수 지정 — 줄이면 사라지는 단의 이동량도 함께 지운다. */
setCount: (chainageM: number, count: number) => void;
/** 기하가 실제로 세운 단 수로 잘라 동기화(지형상 못 세운 단 정리). */
syncCount: (chainageM: number, built: number) => void;
}
import { showToast } from "@ui/ui_template_elements";
import { L } from "./B06_Section_UI_Section_Common";
/**
* 배수관 세트 기하 계산 + 제어 상태 동기화.
*
* ① 세션의 이동량(기슭막이·추가 벽)·유입 구조물 선택을 모아 기하를 계산하고,
* ② 성토부가 짧아져 못 세운 추가 벽은 개수를 잘라 동기화하며(고아 이동량 방지),
* ③ 요청 이동량이 한계에 잘렸으면 실제 적용값으로 되돌려 담는다 — 안 그러면 눌러도
* 안 움직이는데 창의 숫자만 계속 커진다(2026-08-21 사용자 지적). 잘린 순간에는
* **왜 안 움직였는지 토스트**로 알린다(2026-08-22 사용자 — 13+15.7처럼 자동
* 자리가 이미 안쪽 한계라 ◀가 그대로 먹히지 않는 경우). 조작 중(그 벽이 선택된
* 상태)일 때만 띄운다 — 리로드로 복원된 옛 세션 값에는 침묵.
*/
export function computeCardCulvert(
section: CrossSection,
sourceSamples: SectionSample[],
activeRevet: RevetKey | null,
revetOffset?: RevetOffsetControl,
inletStructure?: InletStructureControl,
extraWalls?: ExtraWallControl,
): CulvertLayout | null {
const layout = computeCulvertLayout(
section,
sourceSamples,
revetOffset
? {
inlet: revetOffset.adjustFor(section, "inlet"),
outlet: revetOffset.adjustFor(section, "outlet"),
extras: Array.from({ length: extraWalls?.countFor(section) ?? 0 }, (_, i) =>
revetOffset.adjustFor(section, `extra${i}`),
),
}
: undefined,
inletStructure?.valueFor(section),
);
if (!layout) return null;
if (extraWalls && extraWalls.countFor(section) > layout.extraWalls.length) {
// 요청 단 수보다 지형이 허락하는 단이 적다(벽이 원지반에 0.5m 이상 묻히면 성토
// 불필요 — 그 아래 단은 못 세운다). 조작 중일 때만 가능한 단 수를 토스트로 알린다.
if (activeRevet === "outlet" || activeRevet?.startsWith("extra")) {
showToast(
L("B06_Cross_Extra_Limit").replace("{n}", String(layout.extraWalls.length)),
"info",
);
}
extraWalls.syncCount(section.chainage_m, layout.extraWalls.length);
}
if (revetOffset) {
const roles: Array<[RevetKey, WallAdjust]> = [
["inlet", layout.revetShift.inlet],
["outlet", layout.revetShift.outlet],
...layout.revetShift.extras.map((applied, i): [RevetKey, WallAdjust] => [
`extra${i}`,
applied,
]),
];
const wallOf = (role: RevetKey) =>
role.startsWith("extra")
? layout.extraWalls[Number(role.slice(5))]
: layout.walls.find((wall) => wall.role === role);
for (const [role, applied] of roles) {
const requested = revetOffset.adjustFor(section, role);
// 축별로 요청이 잘렸으면 왜 안 됐는지 토스트로 알린다(조작 중인 벽만).
if (activeRevet === role) {
if (Math.abs(requested.x - applied.x) > 0.05) {
showToast(
L(
requested.x < applied.x
? "B06_Cross_Revet_Limit_Inward"
: "B06_Cross_Revet_Limit_Outward",
),
"info",
);
}
if (Math.abs(requested.d - applied.d) > 0.05) {
showToast(
L(requested.d < applied.d ? "B06_Cross_Revet_Limit_Up" : "B06_Cross_Revet_Limit_Down"),
"info",
);
}
// 높이가 재질 한계에 잘렸으면 재질 변경을 안내(2026-08-22 사용자 — 경고와
// 높이 해제는 재질 선택으로).
const wall = wallOf(role);
if (requested.h != null && applied.h != null && requested.h > applied.h + 0.05 && wall) {
showToast(
L("B06_Cross_Height_Limit")
.replace("{mat}", materialLabel(wall.material))
.replace("{limit}", materialLimit(wall.material).toFixed(1)),
"info",
);
}
if (requested.h != null && applied.h != null && requested.h < applied.h - 0.05) {
showToast(L("B06_Cross_Height_Floor"), "info");
}
}
revetOffset.syncApplied(section.chainage_m, role, applied);
}
}
return layout;
}