diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts index b7635f5e..4c240cda 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts @@ -49,6 +49,7 @@ import type { FillSlopeSegment } from "./B06_Section_UI_Cross_Culvert_Solve"; import { clampWallOffset, cutLength, + enforceMinFillRatio, outletSlopeFactory, fillSlopeOf, groundInterpolator, @@ -125,9 +126,17 @@ export function computeCulvertLayout( // "5m 이상이면 기슭막이 의무"라는 뜻이라 여기서 자르지 않는다. let inletAutoOffset: number | null = null; if (!basinReason && designAt) { - inletAutoOffset = slopeToeOffset(designAt, groundAt, inletInfo.edge.offset_m, inletInfo.limit); const inletBaseAt = (offset: number): number => Math.min(groundAt(offset), invertCap(inletInfo.edge)); + // 사면 끝이 기본 자리지만, 그 자리 물매가 1:1.2보다 급하면 규정 위반이라 바깥으로 민다. + inletAutoOffset = enforceMinFillRatio( + inletInfo.edge, + inletInfo.outward, + wallHeightFor(culvert.inlet), + inletBaseAt, + slopeToeOffset(designAt, groundAt, inletInfo.edge.offset_m, inletInfo.limit), + inletInfo.limit, + ); const shifted = clampWallOffset( inletAutoOffset, inletAutoOffset + inletInfo.outward * (revetShift?.inlet ?? 0), @@ -404,7 +413,16 @@ export function computeCulvertLayout( const invertAt = (offset: number): number => Math.min(groundAt(offset), inlet.elevation); // 유출 벽 기본 자리도 **사면 끝**(= outletAnchorOffset)이고 그 자리가 하단선 중점이다. let outletWallAnchor = outletAnchor; - const outletAutoOffset = outletAnchorOffset; + const outletAutoOffset = designAt + ? enforceMinFillRatio( + outletInfo.edge, + outletInfo.outward, + wallHeightFor(culvert.outlet), + (offset) => Math.min(groundAt(offset), inlet.elevation), + outletAnchorOffset, + outletInfo.limit, + ) + : outletAnchorOffset; if (designAt) { const shifted = clampWallOffset( outletAutoOffset, diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Solve.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Solve.ts index 39b4e477..e279639d 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Solve.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Solve.ts @@ -10,10 +10,17 @@ import { FILL_SLOPE_MAX_LENGTH_M, FILL_SLOPE_RATIO_MAX, FILL_SLOPE_RATIO_MIN, + REVET_LEAN_RATIO, + REVET_THICKNESS_M, REVET_TRAP_TOP_M, } from "./B06_Section_UI_Cross_Culvert_Const"; import type { OffsetPoint, PipeEnd } from "./B06_Section_UI_Cross_Culvert_Types"; +/** 기슭막이 하단선 폭(m) — 사다리꼴 밑변. 자리 기준(하단선 중점) 환산에 쓴다. */ +export function fillWallBaseWidth(height: number): number { + return REVET_THICKNESS_M * 1.5 + REVET_LEAN_RATIO * height; +} + /** 유효 지반 샘플 → offset 오름차순 보간기. 범위 밖은 끝값 클램프, 샘플 없으면 null. */ export function groundInterpolator(samples: SectionSample[]): ((offset: number) => number) | null { const ground = samples @@ -175,8 +182,8 @@ export function clampWallOffset( const rise = edge.elevation_m - (baseAt(candidate) + height); if (rise < FILL_MIN_RISE_M) break; if (inward) { - const run = Math.abs(candidate + outward * REVET_TRAP_TOP_M - edge.offset_m); - if (run / rise < FILL_SLOPE_RATIO_MIN) break; + const joint = candidate - outward * (fillWallBaseWidth(height) / 2 - REVET_TRAP_TOP_M); + if (Math.abs(joint - edge.offset_m) / rise < FILL_SLOPE_RATIO_MIN) break; } allowed = candidate; } @@ -219,3 +226,36 @@ export function outletSlopeFactory( }; }; } + +/** + * 성토 물매가 **1:1.2보다 급하면** 규정 위반이다(`성토_비탈면.md` §1 — 1:1.2~2.0). + * 사면 끝에 세운 자리가 그렇다면 물매가 1:1.2에 닿을 때까지 **바깥으로** 민다. + * 지반이 1:1.2보다 급해 끝내 못 맞추면 시작 자리를 그대로 돌려준다 — 그때는 + * 기슭막이로 못 받는 자리라 옹벽·석축 검토 대상이고 경고로 알린다. + */ +export function enforceMinFillRatio( + edge: { offset_m: number; elevation_m: number }, + outward: number, + height: number, + baseAt: (offset: number) => number, + startOffset: number, + limitOffset: number, +): number { + // 자리 기준은 **하단선 중점**이라 이음선 상단점은 중점보다 안쪽에 있다. + const jointOf = (offset: number): number => + offset - outward * (fillWallBaseWidth(height) / 2 - REVET_TRAP_TOP_M); + const ratioAt = (offset: number): number => { + const rise = edge.elevation_m - (baseAt(offset) + height); + if (!(rise > 1e-6)) return Number.POSITIVE_INFINITY; + return Math.abs(jointOf(offset) - edge.offset_m) / rise; + }; + if (ratioAt(startOffset) >= FILL_SLOPE_RATIO_MIN) return startOffset; + const span = (limitOffset - startOffset) * outward; + if (!(span > 0)) return startOffset; + const steps = 200; + for (let i = 1; i <= steps; i += 1) { + const candidate = startOffset + outward * ((span * i) / steps); + if (ratioAt(candidate) >= FILL_SLOPE_RATIO_MIN) return candidate; + } + return startOffset; +}