- 관 유도 접속선을 0도 → **3도**로: 유입측은 계류 쪽 오름(물이 관으로), 유출측 매몰 절토선은 계류 쪽 내림(물이 밖으로). 집수정 I형 되메움선도 동일 - 모든 유도선·성토부선 끝점을 격자점이 아니라 **선형 보간 교차점**에 붙여 선이 원지반을 미세하게 벗어나던 문제 해소(2026-08-22 사용자 지적) - 카드 자동 줌아웃: **구조물 없는 상태**의 설계 절·성토선이 원지반과 만나는 지점(+0.5m)이 표시 반폭 밖이면 그 카드만 반폭을 넓혀 그린다. 보유 샘플 범위를 넘는 경우는 기존 전역 반폭 재생성 경로(applyPanelToAll)가 담당 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
185 lines
8.5 KiB
TypeScript
185 lines
8.5 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";
|
|
import {
|
|
designInterpolator,
|
|
groundInterpolator,
|
|
slopeToeOffset,
|
|
} from "./B06_Section_UI_Cross_Culvert_Solve";
|
|
/**
|
|
* 기슭막이 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;
|
|
/** 다단 등간격 배치 요청(2026-08-22 ①) — 다음 계산 1회에 적용된다. */
|
|
equalize: (chainageM: number) => void;
|
|
/** 등간격 요청을 소비(1회성) — 계산 직전에 Wire가 부른다. */
|
|
consumeEqualize: (section: CrossSection) => boolean;
|
|
/** 기하가 실제로 세운 단 수로 잘라 동기화(지형상 못 세운 단 정리). */
|
|
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 equalizeExtras = extraWalls?.consumeEqualize(section) ?? false;
|
|
const layout = computeCulvertLayout(
|
|
section,
|
|
sourceSamples,
|
|
adjustsInput(section, revetOffset, extraWalls),
|
|
inletStructure?.valueFor(section),
|
|
equalizeExtras,
|
|
);
|
|
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;
|
|
}
|
|
|
|
function adjustsInput(
|
|
section: CrossSection,
|
|
revetOffset?: RevetOffsetControl,
|
|
extraWalls?: ExtraWallControl,
|
|
): { inlet: WallAdjust; outlet: WallAdjust; extras: WallAdjust[] } | undefined {
|
|
if (!revetOffset) return undefined;
|
|
return {
|
|
inlet: revetOffset.adjustFor(section, "inlet"),
|
|
outlet: revetOffset.adjustFor(section, "outlet"),
|
|
extras: Array.from({ length: extraWalls?.countFor(section) ?? 0 }, (_, i) =>
|
|
revetOffset.adjustFor(section, `extra${i}`),
|
|
),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* **구조물 없는 상태**의 설계 절·성토선이 원지반과 만나는 지점이 들어오는 데
|
|
* 필요한 표시 반폭(m, 여유 0.5 포함) — 2026-08-22 사용자 확정 줌 기준.
|
|
* 구조물(기슭막이·집수정)이 어디로 가든 화면 기준은 지형·설계선이 정한다.
|
|
*/
|
|
export function culvertRequiredHalfWidth(section: CrossSection): number | null {
|
|
const design = section.design;
|
|
if (!design) return null;
|
|
const groundAt = groundInterpolator(section.samples);
|
|
const designAt = designInterpolator(design.design_line);
|
|
const edges = design.road_edges;
|
|
if (!groundAt || !designAt || !edges) return null;
|
|
const offsets = section.samples.map((sample) => sample.offset_m ?? 0);
|
|
const limits = { left: Math.max(...offsets), right: Math.min(...offsets) };
|
|
let extent = 0;
|
|
for (const side of ["left", "right"] as const) {
|
|
const edge = side === "left" ? edges.left : edges.right;
|
|
const toe = slopeToeOffset(designAt, groundAt, edge.offset_m, limits[side]);
|
|
extent = Math.max(extent, Math.abs(toe));
|
|
}
|
|
return extent > 0 ? extent + 0.5 : null;
|
|
}
|