feat(B06): 기슭막이 자리·하단선 규칙 재정립
2026-08-21 사용자 확정: - 기본 자리 = **성토 사면이 원지반과 만나는 지점**(사면 끝). 사면이 5m를 넘으면 기슭막이가 사면을 5m에서 끊는다 — 5m는 자리 한계가 아니라 "5m 이상이면 기슭막이 의무"라는 규정(성토_비탈면.md §2)의 표현이다. 물매 1:1.2~2.0으로 자리를 풀던 방식은 걷어냈다(자유도 축소였다). - 벽 하단선 = **원지반을 수직 0.5m 내린 선**. 수평 바닥이라 사면에서 앞 모서리가 뜨던 문제가 사라진다(배면·전면 모두 근입 0.500 확인). - 자리 기준(이동 중심) = **하단선 중점**. 배면 기준이면 벽이 기울 때 체감 위치가 어긋난다. - 관 유출단 invert = 그 자리 원지반 → 관 하단 경사가 지반과 만난다. - structureRequired = 사면길이 5m 이상(의무 발생), 종전의 "받을 수 없는 자리" 판정 대체. 그리기도 기운 하단선에 맞췄다 — 이음선 바닥·돌 해칭이 하단선 위에서 잡힌다. 검증: 두 측점 이동 10케이스 근입 배면·전면 모두 0.500, 하단선 중점이 원지반과 일치 (관 수평 클램프 2건 제외), 계획고 스윕 24케이스 문제 0. tsc 통과, pytest 148 pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,6 @@ import {
|
||||
FILL_SLOPE_MAX_LENGTH_M,
|
||||
FILL_SLOPE_RATIO_MAX,
|
||||
FILL_SLOPE_RATIO_MIN,
|
||||
FILL_STRUCTURE_HEIGHT_M,
|
||||
REVET_TRAP_TOP_M,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Const";
|
||||
import type { OffsetPoint, PipeEnd } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
@@ -80,7 +79,8 @@ export function intersect(
|
||||
export const STRAY_LIMIT_M = 2.5;
|
||||
|
||||
/** 노견에서 바깥으로 훑어 설계선(사면)이 지반과 만나는 첫 지점(사면 끝)을 찾는다.
|
||||
* 사면 경사길이 5m 한계(별표2)에 걸리면 그 지점에서 멈춘다 — 거기가 구조물 자리다. */
|
||||
* 5m는 자리 한계가 아니다 — 사면이 5m 이상이면 **기슭막이가 의무**라는 뜻일 뿐이라
|
||||
* 여기서 끊지 않는다(2026-08-21 사용자 정정). */
|
||||
export function slopeToeOffset(
|
||||
designAt: (offset: number) => number,
|
||||
groundAt: (offset: number) => number,
|
||||
@@ -93,6 +93,10 @@ export function slopeToeOffset(
|
||||
let previousDiff = designAt(startOffset) - groundAt(startOffset);
|
||||
let previousElevation = designAt(startOffset);
|
||||
let slopeLength = 0;
|
||||
// 자리 = **성토선이 지반과 만나는 점**과 **사면길이 5m 지점** 중 안쪽. 사면이 5m를
|
||||
// 넘으면 기슭막이가 있어야 한다는 규정(성토_비탈면.md §2)은 곧 기슭막이가 사면을
|
||||
// 5m에서 끊어 준다는 뜻이다. 교차점이 5m 안이면 거기가 자연스러운 사면 끝이다.
|
||||
// 더 밖으로 두고 싶으면 조정창으로 민다(2026-08-21 사용자 확정).
|
||||
for (let i = 1; i <= stepCount; i += 1) {
|
||||
const offset = startOffset + step * i;
|
||||
const elevation = designAt(offset);
|
||||
@@ -107,82 +111,6 @@ export function slopeToeOffset(
|
||||
return limitOffset;
|
||||
}
|
||||
|
||||
/** 성토측 구조물 자리 풀이 결과 — 벽 offset과 그 자리에서 성립하는 사면 물매. */
|
||||
export interface FillWallPlacement {
|
||||
offset: number;
|
||||
/** 사면선이 벽 이음선 상단점을 지나는 물매(1:n). 범위 밖이면 끝값으로 고정된 값. */
|
||||
ratio: number;
|
||||
slopeLengthM: number;
|
||||
withinLimit: boolean;
|
||||
ratioClamped: boolean;
|
||||
/** 그 자리의 성토 전체고(노견 − 벽 상단). 3m 이상이면 벽을 5m 쪽으로 끌어올린다. */
|
||||
fillHeightM: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 성토측 기슭막이 자리 풀이 — 노견에서 바깥으로 훑으며 **성토 사면선이 벽 이음선
|
||||
* 상단점(평행사변형↔사다리꼴 상단 교차점)을 지나는** 자리를 찾는다.
|
||||
*
|
||||
* 종전에는 사면선(백엔드 1:1.2 고정)과의 교차로 벽 **높이**를 정했으나, 높이가
|
||||
* 관경+여유고로 고정된 뒤로는 그 방식이 성립하지 않는다. 물매는 고정값이 아니라
|
||||
* **1:1.2~2.0 범위**(`성토_비탈면.md` §1)이므로, 벽 자리를 정하면 물매가 따라 정해진다.
|
||||
* 노견에서 멀어질수록 물매는 급 → 완만으로 단조 증가하므로 **범위에 처음 드는 자리**
|
||||
* (= 사면이 가장 짧은 1:1.2 쪽)를 고른다. 사면길이 5m(별표2)를 넘기 전까지만 훑는다.
|
||||
*
|
||||
* 범위 안에 드는 자리가 없으면(급경사 지형) 5m 한계 직전 자리를 쓰고 물매를 끝값으로
|
||||
* 고정한다 — `ratioClamped`로 알린다.
|
||||
*/
|
||||
export function solveFillWallOffset(
|
||||
edge: { offset_m: number; elevation_m: number },
|
||||
outward: number,
|
||||
height: number,
|
||||
baseAt: (offset: number) => number,
|
||||
limitOffset: number,
|
||||
): FillWallPlacement | null {
|
||||
const span = (limitOffset - edge.offset_m) * outward;
|
||||
if (!(span > 0)) return null;
|
||||
const steps = 400;
|
||||
const inRange: FillWallPlacement[] = [];
|
||||
let last: FillWallPlacement | null = null;
|
||||
for (let i = 0; i <= steps; i += 1) {
|
||||
const offset = edge.offset_m + outward * ((span * i) / steps);
|
||||
const joint = offset + outward * REVET_TRAP_TOP_M;
|
||||
const rise = edge.elevation_m - (baseAt(offset) + height);
|
||||
if (!(rise > 1e-6)) continue;
|
||||
const run = Math.abs(joint - edge.offset_m);
|
||||
const slopeLengthM = Math.hypot(run, rise);
|
||||
// 사면길이 5m를 넘는 자리는 구조물 의무 구간이라 벽 자리 후보가 아니다(별표2).
|
||||
if (slopeLengthM > FILL_SLOPE_MAX_LENGTH_M) break;
|
||||
const candidate: FillWallPlacement = {
|
||||
offset,
|
||||
ratio: run / rise,
|
||||
slopeLengthM,
|
||||
withinLimit: true,
|
||||
ratioClamped: false,
|
||||
fillHeightM: rise,
|
||||
};
|
||||
last = candidate;
|
||||
if (candidate.ratio >= FILL_SLOPE_RATIO_MIN && candidate.ratio <= FILL_SLOPE_RATIO_MAX) {
|
||||
inRange.push(candidate);
|
||||
}
|
||||
}
|
||||
if (!last) return null;
|
||||
if (inRange.length) {
|
||||
// 성토 전체고가 3m 이상이면 1:1.2로도 사면이 5m에 육박한다 — 벽을 **사면길이 5m
|
||||
// 쪽(가장 바깥 후보)**으로 끌어올린다(2026-08-21 사용자 ①). 낮은 성토는 사면을
|
||||
// 짧게 두는 **가장 안쪽 후보**를 쓴다.
|
||||
return last.fillHeightM >= FILL_STRUCTURE_HEIGHT_M ? inRange[inRange.length - 1] : inRange[0];
|
||||
}
|
||||
// 범위 안에 드는 자리가 없다 = 지형이 1:1.2보다 급하거나 1:2.0보다 완만하다.
|
||||
// 5m 한계 직전 자리를 쓰고 물매를 가까운 끝값으로 고정한다 — 도면에 경고로 알린다.
|
||||
return {
|
||||
...last,
|
||||
ratio: last.ratio < FILL_SLOPE_RATIO_MIN ? FILL_SLOPE_RATIO_MIN : FILL_SLOPE_RATIO_MAX,
|
||||
withinLimit: last.slopeLengthM <= FILL_SLOPE_MAX_LENGTH_M + 1e-6,
|
||||
ratioClamped: true,
|
||||
};
|
||||
}
|
||||
|
||||
/** 노견 → 벽 이음선 상단점 성토 사면 구간(단일 각도)과 그 물매·경고. */
|
||||
export interface FillSlopeSegment {
|
||||
ratio: number;
|
||||
|
||||
Reference in New Issue
Block a user