fix(B06): 성토 물매 1:1.2 하한을 자동 자리에도 강제
기슭막이 기본 자리를 사면 끝으로 바꾼 뒤 물매가 규정보다 급해지는 측점이 생겼다 (13+15.7 실측 1:0.865 — 성토_비탈면.md §1의 1:1.2~2.0 위반). 1:1.2는 지켜야 하는 하한이므로, 사면 끝 자리의 물매가 그보다 급하면 **1:1.2에 닿을 때까지 바깥으로** 민다. 지반이 1:1.2보다 급해 끝내 못 맞추면 사면 끝을 그대로 쓰고 경고로 알린다. 물매 계산의 이음선 위치도 바로잡았다 — 자리 기준이 하단선 중점으로 바뀐 뒤에도 `REVET_TRAP_TOP_M`을 그대로 더해 실제보다 0.3m 바깥으로 보고 있었다. `fillWallBaseWidth()`로 중점↔이음선 환산을 한곳에 모았다. 검증(계획고 스윕 24케이스, 새 계획노선): 물매 최소 1:1.202 — **1:1.2 미만 0건** (종전 0.4~0.9까지 나왔다). 페이지 실데이터로도 13+15.7 1:1.206 확인. tsc 통과, pytest 148 pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user