Files
Aislo/B06_Section/B06_Section_UI_Cross_Culvert_Basin.ts
T
eomsangdonandClaude Opus 5 cd855e32d3 feat(B06): 관 접속 절·성토선 3도 물매 + 교차점 보간 + 절성토 교차 기준 자동 줌아웃
- 관 유도 접속선을 0도 → **3도**로: 유입측은 계류 쪽 오름(물이 관으로),
  유출측 매몰 절토선은 계류 쪽 내림(물이 밖으로). 집수정 I형 되메움선도 동일
- 모든 유도선·성토부선 끝점을 격자점이 아니라 **선형 보간 교차점**에 붙여
  선이 원지반을 미세하게 벗어나던 문제 해소(2026-08-22 사용자 지적)
- 카드 자동 줌아웃: **구조물 없는 상태**의 설계 절·성토선이 원지반과 만나는
  지점(+0.5m)이 표시 반폭 밖이면 그 카드만 반폭을 넓혀 그린다. 보유 샘플
  범위를 넘는 경우는 기존 전역 반폭 재생성 경로(applyPanelToAll)가 담당

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

289 lines
13 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_Culvert_Basin.ts
* 유입 **집수정** 단면 구성 — 기하 본체(`_Cross_Culvert_Geom.ts`)에서 분리(700줄 제한).
*
* 집수정은 기슭막이와 달리 **물을 모으려고 만든 구조물**이다(2026-08-22 사용자 확정):
* · I형(공유벽뿐): 배관은 원지반 자리 유지. 기본 높이(관경+여유)로 노견 끝 접점이
* 안 되면 벽을 노견 표고까지 키운다.
* · ㄴ(L)형(= I형 + 바닥판, 기본값): 내부 높이 1.2m(임시값) 등 사이즈를 유지한 채
* 통째로 노견 끝점에 일치한다 — 바닥(=관 유입 invert)이 따라 올라 관 물매가 바뀐다.
* · ㄷ(U)형: ㄴ형 + 반대측(도로측) 막음벽(I형 수직반전).
* 부재(벽·바닥판) 두께 0.2m(2026-08-22 사용자 지정). 벽 자리 = 상단 도로측 꼭짓점이
* **노견 끝 지점**에 닿는 자리(우측 집수정이면 상단 좌측점).
* ========================================================================== */
import {
PIPE_CONNECT_GRADE,
BASIN_INNER_HEIGHT_M,
BASIN_MEMBER_THICKNESS_M,
REVET_FREEBOARD_M,
REVET_LEAN_RATIO,
} from "./B06_Section_UI_Cross_Culvert_Const";
import { minShoulderWallOffset, slopeToeOffset } from "./B06_Section_UI_Cross_Culvert_Solve";
import type {
BasinLayout,
BasinShape,
EndFace,
OffsetPoint,
} from "./B06_Section_UI_Cross_Culvert_Types";
/** 집수정 구성 입력 — 기하 본체가 자기 상태를 명시적으로 물려 준다. */
export interface BasinBuildInput {
/** 관 유입 기준점(노견 끝 offset, invert 표고 — ㄴ·ㄷ형은 상승분 반영 후). */
anchor: OffsetPoint;
/** 계류측 방향 부호(+ = 좌). */
outward: number;
shape: BasinShape;
reason: BasinLayout["reason"];
/** 관 외경 지름(m). */
diameterM: number;
/** 이 측의 노견 끝 점 — 벽 상단 접점·I형 높이 성장 기준. */
edge: { offset_m: number; elevation_m: number };
groundAt: (offset: number) => number;
/** 표준단면 절토 경사(1:n) — 절토 계획선용. */
cutSlopeRatio: number;
}
/** 집수정 구성 결과 — 본체가 트림·관 끝단면·관 시작점에 나눠 꽂는다. */
export interface BasinBuildResult {
basin: BasinLayout;
/** 관 끝단 마감면 = I형 벽의 안쪽(물받는 쪽) 변 복사(2026-08-22 사용자 확정). */
endFace: EndFace;
/** 관 시작점(하단 기준) — ㄴ·ㄷ형은 측벽 안쪽 변×바닥 윗면 교차점, I형은 내공 1.0m. */
pipeEnd: OffsetPoint;
/** 성토 사면선 트림 경계(벽 상단 도로측 꼭짓점)와 그 표고. */
trimOffset: number;
trimElevation: number;
}
/** 실무 내공 폭(m) — 울진 돌집수정. I형 관 시작점(노견+1.0m) 계산에도 쓴다. */
export const BASIN_INNER_WIDTH_M = 1.0;
const INNER_WIDTH_M = BASIN_INNER_WIDTH_M;
/** 집수정 주변 성토(되메움) 표면 경사(도) — 2026-08-22 사용자 지정 "일단 5도". */
const BASIN_FILL_ANGLE_DEG = 5;
/** 집수정 단면을 만든다. 형태별 규칙은 파일 머리말 참조. */
export function buildBasin(input: BasinBuildInput): BasinBuildResult {
const { anchor, outward, shape, reason, diameterM, edge, groundAt, cutSlopeRatio } = input;
const memberT = BASIN_MEMBER_THICKNESS_M;
const floorThickness = memberT;
// 벽 높이: I형은 노견 접점까지 성장, ㄴ·ㄷ형은 내부 높이 1.2m 고정(임시값).
const wallHeight =
shape === "I"
? Math.max(diameterM + REVET_FREEBOARD_M, edge.elevation_m - anchor.elevation)
: BASIN_INNER_HEIGHT_M;
// 기움은 **수직 기준 반전** — 상단이 도로측(내공 쪽)으로 1:0.3 물러난다(2026-08-20).
// bottomElevation을 내리면 면 기울기를 그대로 연장한다(I형 근입용).
const defaultBottom = anchor.elevation - floorThickness;
const wallOf = (
baseOffset: number,
dir: number,
bottomElevation = defaultBottom,
): OffsetPoint[] => {
const topShift = -dir * REVET_LEAN_RATIO * wallHeight;
const spanZ = wallHeight + floorThickness;
const xAt = (elevation: number): number =>
baseOffset + topShift * ((elevation - defaultBottom) / spanZ);
return [
{ offset: xAt(bottomElevation), elevation: bottomElevation },
{ offset: baseOffset + topShift, elevation: anchor.elevation + wallHeight },
{
offset: baseOffset + topShift + dir * memberT,
elevation: anchor.elevation + wallHeight,
},
{
offset: xAt(bottomElevation) + dir * memberT,
elevation: bottomElevation,
},
];
};
// I형 벽 자리 = **상단 도로측 꼭짓점이 노견 끝 지점에 닿는다**(2026-08-22 사용자
// 확정). 벽 상단이 도로측으로 1:0.3 물러나므로 하단(base)은 그만큼 계류측 바깥.
const wallBase = anchor.offset + outward * (REVET_LEAN_RATIO * wallHeight);
// I형 벽 하단은 **원지반까지 묻는다**(2026-08-22 사용자 — 지반 아래 부재두께만큼
// 근입 후 배관을 둔다). ㄴ·ㄷ형은 바닥판이 원지반을 대체하므로 현행 유지.
const iWallBottom =
shape === "I"
? Math.min(anchor.elevation, groundAt(wallBase), groundAt(wallBase + outward * memberT)) -
floorThickness
: defaultBottom;
const parts: BasinLayout["parts"] = [
{ kind: "wall", points: wallOf(wallBase, outward, iWallBottom) },
];
const iWallPoints = parts[0].points;
// ㄴ형 = I형 + 바닥판. 바닥은 벽 바깥(계류측) 변에 맞대고 그 너머로 뻗는다
// (2026-08-20 확정). 벽이 기울어 있어 안쪽 변도 벽 바깥면 선을 그대로 따른다.
const wallOuterAt = (elevation: number): number => {
const bottom = anchor.elevation - floorThickness;
const t = (elevation - bottom) / (wallHeight + floorThickness);
return wallBase + outward * memberT - outward * REVET_LEAN_RATIO * wallHeight * t;
};
const floorTopInner = wallOuterAt(anchor.elevation);
const floorBottomInner = wallOuterAt(anchor.elevation - floorThickness);
const floorOuter = floorTopInner + outward * (INNER_WIDTH_M + memberT);
const floorTaper = REVET_LEAN_RATIO * floorThickness;
if (shape !== "I") {
parts.push({
kind: "floor",
points: [
{ offset: floorTopInner, elevation: anchor.elevation },
{ offset: floorOuter, elevation: anchor.elevation },
{
offset: floorOuter - outward * floorTaper,
elevation: anchor.elevation - floorThickness,
},
{ offset: floorBottomInner, elevation: anchor.elevation - floorThickness },
],
});
}
// ㄷ형 = ㄴ형 + 막음벽 — I형 벽을 **바닥부 중심 기준 수직 반전**한 자리·형상
// (2026-08-22 사용자 ① — 종전에는 노견 쪽에 세워 방향이 틀렸다).
const floorCenter = (floorTopInner + floorOuter) / 2;
const mirroredWallBase = 2 * floorCenter - wallBase;
if (shape === "U") {
parts.push({ kind: "wall", points: wallOf(mirroredWallBase, -outward) });
}
// 구조물 계류측 끝(ㄴ형 = 바닥 바깥 끝 상단, ㄷ형 = 막음벽 계류측 상단 꼭짓점)과
// 원지반의 관계(2026-08-22 사용자 ①):
// · 원지반 안쪽에 박히면 → 표준단면 절토경사(1:n)로 **절토선**
// · 원지반 밖으로 나오면 → **성토(되메움)선** — 경사 5도(임시값)로 지반까지.
let basinCut: BasinLayout["cutLine"] = null;
let basinFill: BasinLayout["cutLine"] = null;
if (shape !== "I") {
const refTop =
shape === "U"
? {
offset: mirroredWallBase + outward * REVET_LEAN_RATIO * wallHeight,
elevation: anchor.elevation + wallHeight,
}
: { offset: floorOuter, elevation: anchor.elevation };
const groundHere = groundAt(refTop.offset);
if (groundHere > refTop.elevation + 0.05) {
for (let h = 0.05; h <= 20; h += 0.05) {
const probe = refTop.offset + outward * cutSlopeRatio * h;
if (groundAt(probe) <= refTop.elevation + h) {
basinCut = { from: refTop, to: { offset: probe, elevation: refTop.elevation + h } };
break;
}
}
} else if (groundHere < refTop.elevation - 0.05) {
const dropPerM = Math.tan((BASIN_FILL_ANGLE_DEG * Math.PI) / 180);
for (let run = 0.05; run <= 30; run += 0.05) {
const probe = refTop.offset + outward * run;
const elevation = refTop.elevation - dropPerM * run;
if (elevation <= groundAt(probe)) {
basinFill = { from: refTop, to: { offset: probe, elevation } };
break;
}
}
}
}
return {
basin: {
shape,
parts,
// 라벨 자리 = I형 벽 하단(2026-08-20 사용자) — 그리기 쪽에서 아래로 내려 붙인다.
label: {
offset: wallBase + outward * (memberT / 2),
elevation: anchor.elevation - floorThickness,
},
reason,
cutLine: basinCut,
fillLine: basinFill,
},
// 관 끝선 = I형 벽의 안쪽(물받는 쪽 = 내공측) 변 복사(2026-08-22 사용자 확정).
// 벽 폴리곤의 하단-전면(3)~상단-전면(2) 꼭짓점을 그대로 쓴다 — I형 근입 연장도 반영.
endFace: {
base: iWallPoints[3],
direction: {
offset: iWallPoints[2].offset - iWallPoints[3].offset,
elevation: iWallPoints[2].elevation - iWallPoints[3].elevation,
},
},
pipeEnd:
shape === "I"
? { offset: anchor.offset + outward * INNER_WIDTH_M, elevation: anchor.elevation }
: { offset: floorTopInner, elevation: anchor.elevation },
trimOffset: wallBase - outward * REVET_LEAN_RATIO * wallHeight,
trimElevation: anchor.elevation + wallHeight,
};
}
/**
* 조정창 드롭다운 선택지 가용성(2026-08-22 사용자 — 상황에 안 맞는 선택지 숨김):
* · 기슭막이+배관: 자동 자리의 원지반이 토피 상한(노면−관경−토피)보다 높으면 배관이
* 원지반에 묻힐 수밖에 없다 → 숨김.
* · 집수정 ㄴ·ㄷ형: ㄴ형으로 가정해 세워 봤을 때 구조물이 원지반으로 밀고 들어가지
* 않으면(절토선이 안 생기면) → 숨김.
*/
export function inletChoiceAvailability(input: {
designAt: ((offset: number) => number) | null;
groundAt: (offset: number) => number;
edge: { offset_m: number; elevation_m: number };
outward: number;
limitOffset: number;
wallHeight: number;
invertCapM: number;
diameterM: number;
cutSlopeRatio: number;
}): { revetAllowed: boolean; basinLUAllowed: boolean; revetAutoOffset: number } {
const { designAt, groundAt, edge, outward, limitOffset, wallHeight, invertCapM } = input;
let revetAllowed = true;
// 기슭막이 자동 자리(노견 최소 지점 ?? 사면 끝) — 본체가 벽 배치에도 재사용한다.
let revetAutoOffset = edge.offset_m;
if (designAt) {
const baseAt = (offset: number): number => Math.min(groundAt(offset), invertCapM);
const toe = slopeToeOffset(designAt, groundAt, edge.offset_m, limitOffset);
revetAutoOffset =
minShoulderWallOffset(edge, outward, wallHeight, baseAt, toe, limitOffset) ?? toe;
revetAllowed = groundAt(revetAutoOffset) <= invertCapM + 0.02;
}
const probe = buildBasin({
anchor: { offset: edge.offset_m, elevation: edge.elevation_m - BASIN_INNER_HEIGHT_M },
outward,
shape: "L",
reason: "short",
diameterM: input.diameterM,
edge,
groundAt,
cutSlopeRatio: input.cutSlopeRatio,
});
return { revetAllowed, basinLUAllowed: probe.basin.cutLine != null, revetAutoOffset };
}
/**
* I형 집수정의 **관 하단 수평 되메움(성토)선**(2026-08-22 사용자). 관 유입 하단
* 꼭짓점이 원지반 위에 뜨면 그 표고 그대로 계류측으로 나가 원지반과 만나는 점까지
* 잇고, 그 아래를 성토로 본다. 지반 교점 절단 방식은 보고 후 철회.
*/
export function applyBasinPipeFill(
basin: BasinLayout,
corner: OffsetPoint,
outward: number,
groundAt: (offset: number) => number,
): void {
// 되메움선 물매 3도(2026-08-22 사용자) — 유입측이라 계류 쪽이 높다(물이 관으로).
if (basin.shape !== "I" || corner.elevation <= groundAt(corner.offset) + 0.02) return;
let previousGap = corner.elevation - groundAt(corner.offset); // 선 지반
for (let run = 0.05; run <= 30; run += 0.05) {
const probe = corner.offset + outward * run;
const elevation = corner.elevation + PIPE_CONNECT_GRADE * run;
const gap = elevation - groundAt(probe);
if (gap <= 0) {
// 끝점은 보간 교차점 — 격자점이면 지반을 미세하게 벗어난다(2026-08-22 사용자).
const fraction = previousGap > 1e-12 ? previousGap / (previousGap - gap) : 1;
const runCross = run - 0.05 * (1 - fraction);
basin.fillLine = {
from: corner,
to: {
offset: corner.offset + outward * runCross,
elevation: corner.elevation + PIPE_CONNECT_GRADE * runCross,
},
};
return;
}
previousGap = gap;
}
}