fix(B05): 3D 제안 ② 비탈 끝 드레이프 — 115-7 로 짧아진 행의 설계면 높이 끝과 바깥 지형 턱 없앰(측점 아닌 행 · 지반 차 0.1 m 넘는 행 · 측점 행은 B06 그대로)(115-8)

- 성토 닿은 70° 삼각형 760 → 150(226) · 측점 오차 그대로
- test_115_6 드레이프 단언 하나

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
This commit is contained in:
2026-10-09 08:33:43 +09:00
co-authored by Claude Opus 5.5
parent f25f25287c
commit 338c307620
3 changed files with 37 additions and 9 deletions
@@ -38,8 +38,8 @@ export const SECTION_STEP_M = 1;
const MIN_CATCH_M = 0.02;
/** 비탈 끝에서 이 안쪽 꺾임만 점으로 둔다(m). */
const BEND_GAP_M = 0.05;
/** 비탈 끝 단면선 높이가 지반과 이만큼 안이면 지반 높이(m) — 못 찾은 끝(단면 끝)은 설계 높이. */
const DAYLIGHT_SNAP_M = 0.1;
/** 비탈 끝 높이와 지반 차가 이보다 작으면 끝 점만 지반으로(단면 안 보정 없음 · 측점 단면 그대로)(m). */
const DRAPE_MIN_M = 0.1;
/** 윗면으로 보는 높이 여유(m) · 윗면 최소 폭(m). */
const TOP_TOLERANCE_M = 0.05;
const TOP_MIN_WIDTH_M = 0.05;
@@ -169,8 +169,9 @@ export function buildTinDesign(
pslg.addEdge(anchors[0][0], anchors[1][0], EDGE_ANCHOR);
pslg.addEdge(anchors[0][n - 1], anchors[1][n - 1], EDGE_ANCHOR);
/** 그 행 · 측 단면의 설계 높이(모델) — 성토는 지반 위로 · 절토는 아래로만 · 성토 금지 고리. */
const designZ = (r: number, si: number, ki: number, s: number): number | null => {
/** 그 행 · 측 단면의 설계 높이(모델) — 성토는 지반 위로 · 절토는 아래로만 · 성토 금지 고리.
* `lift` = 드레이프 보정(비탈 끝에서 지형에 맞춘 몫 × s / 비탈 끝). */
const designZ = (r: number, si: number, ki: number, s: number, lift = 0): number | null => {
const row = rows[r];
const anchor = si === 0 ? row.left : row.right;
const shapes = source(row.chainage_m, SIDES[si]);
@@ -180,7 +181,7 @@ export function buildTinDesign(
if (rel === null) return null;
const [x, y] = field.pointAt(r, si, s);
const ground = groundAt(x, y);
let z = anchor[2] + rel;
let z = anchor[2] + rel + lift;
if (ground !== null) {
z = ki === 0 ? Math.max(z, ground) : Math.min(z, ground);
for (const limit of limits) {
@@ -200,9 +201,13 @@ export function buildTinDesign(
return reach >= MIN_CATCH_M ? { ki, reach } : null;
};
const isStation = (chainage: number): boolean =>
stationChainages.some((c) => Math.abs(c - chainage) < 1e-3);
// 비탈 끝 점 · 곡선 안쪽 겹침 청소 — 앞 행들 단면 선분에 닿으면 처음 닿는 자리에서 자른다(겹친 점 제거).
const kindOf = SIDES.map(() => new Int8Array(n).fill(-1));
const reachOf = SIDES.map(() => new Float64Array(n));
const liftOf = SIDES.map(() => new Float64Array(n));
const daylight = SIDES.map(() => new Int32Array(n).fill(-1));
SIDES.forEach((_, si) => {
const fan = new SegGrid(2);
@@ -224,8 +229,14 @@ export function buildTinDesign(
const [px, py] = field.pointAt(r, si, reach);
const line = designZ(r, si, slope.ki, reach);
const ground = groundAt(px, py);
// 드레이프(115-8 · 제안 ③ 과 같은 규칙) — 비탈 끝 높이 = 그 자리 지형 · 보정량은 단면에 s 비율로.
// 비탈 끝이 짧아진 행(115-7 튐 자름 · 못 닫힘)에서 설계면 높이 끝이 바깥 지형과 턱 지던 자리.
// 측점 행은 B06 단면 그대로(끝 점만 0.1 m 안이면 지반).
const step = ground !== null && line !== null ? ground - line : 0;
const station = isStation(rows[r].chainage_m);
if (hit >= 1 && !station && Math.abs(step) >= DRAPE_MIN_M) liftOf[si][r] = step;
const z =
hit >= 1 && ground !== null && (line === null || Math.abs(line - ground) < DAYLIGHT_SNAP_M)
hit >= 1 && ground !== null && (!station || line === null || Math.abs(step) < DRAPE_MIN_M)
? ground
: (line ?? ground ?? rows[r].left[2]);
daylight[si][r] = pslg.addPoint(sx(px), sz(py), z - oz, slope.ki === 0 ? TAG_FILL : TAG_CUT);
@@ -261,8 +272,6 @@ export function buildTinDesign(
.map((id): [number, number] => [pslg.x[id], pslg.z[id]]);
// 단면 행 — 측점 + 1 m 걸음. 횡방향 선(노면 끝 → 꺾임 → 비탈 끝) · 같은 열 꺾임 종방향 선.
const isStation = (chainage: number): boolean =>
stationChainages.some((c) => Math.abs(c - chainage) < 1e-3);
const sectionRows: number[] = [];
for (let r = 0; r < n; r += 1) {
const chainage = rows[r].chainage_m;
@@ -292,7 +301,7 @@ export function buildTinDesign(
const path = [anchors[si][r]];
for (const [col, s] of field.bendsAt(r, si, ki)) {
if (s <= MIN_CATCH_M || s >= reach - BEND_GAP_M) continue;
const z = designZ(r, si, ki, s);
const z = designZ(r, si, ki, s, (liftOf[si][r] * s) / reach);
if (z === null) continue;
const [x, y] = field.pointAt(r, si, s);
const id = pslg.addPoint(sx(x), sz(y), z - oz, ki === 0 ? TAG_FILL : TAG_CUT);
@@ -382,7 +382,20 @@ const r6 = (v) => Math.round(v * 1e6) / 1e6;
}
toeErr = Math.max(toeErr, best === Infinity ? 99 : best);
}
// 드레이프(115-8) — 측점 아닌 행의 비탈 끝(둘레 점)은 그 자리 망 지형 높이.
let offGround = 0;
let daylightRows = 0;
out.design.outer.forEach(([dx, dz]) => {
const tag = tagAt.get(fkey(dx, dz));
if (tag !== PSLG.TAG_FILL && tag !== PSLG.TAG_CUT) return;
if ([-20, 0, 20].some((x) => Math.abs(dx - x) < 1e-6)) return;
const i = [...Array(pslg.count).keys()].find((k) => pslg.x[k] === dx && pslg.z[k] === dz);
daylightRows += 1;
if (Math.abs(pslg.y[i] - heightIndex.heightAt(dx, dz)) > 1e-4) offGround += 1;
});
results.straight = {
daylightRows,
offGround,
stats: out.stats,
gaps: gap.gaps,
maxGapLen: r6(gap.maxGapLen),
@@ -41,6 +41,12 @@ def test_straight_station_section_matches_b06(results):
assert r["toeErr"] < 0.06 # 비탈 끝 자리(시험 걸음 0.05 m 안)
def test_straight_daylight_draped(results):
r = results["straight"]
assert r["daylightRows"] > 300
assert r["offGround"] == 0 # 측점 아닌 행 비탈 끝 = 망 지형 높이(드레이프 · 115-8)
def test_straight_terrain_outside_untouched(results):
r = results["straight"]
assert r["originalUntouched"] is True # 원본 정점 버퍼 그대로