Files
Aislo/B06_Section/B06_Section_UI_Cross_Culvert_Basin.ts
T
eomsangdonandClaude Opus 5 97053ab0d2 feat(B06): I형 관 하단 수평 되메움선 — 지반 교점 절단 철회
- 지반 교점 절단(직전 커밋)은 사용자 확인 후 철회 — 관 끝은 벽 안쪽 변
  절단으로 복귀.
- 대신 관 유입 하단 꼭짓점이 원지반 위에 뜨면 그 표고 그대로 계류측으로
  **수평 되메움(성토)선**을 그어 원지반과 연결(applyBasinPipeFill —
  Basin 분리, Geom 700줄 유지).
- 검증: 13+15.7 I형 실화면 — 성토선 수평·관 꼭짓점 부착 0.4px, 스크린샷.

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

274 lines
12 KiB
TypeScript

/* =============================================================================
* 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 {
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 {
if (basin.shape !== "I" || corner.elevation <= groundAt(corner.offset) + 0.02) return;
for (let run = 0.05; run <= 30; run += 0.05) {
const probe = corner.offset + outward * run;
if (groundAt(probe) >= corner.elevation) {
basin.fillLine = { from: corner, to: { offset: probe, elevation: corner.elevation } };
return;
}
}
}