diff --git a/B05_Profile/B05_Profile_UI_Corridor_Tin_Design.ts b/B05_Profile/B05_Profile_UI_Corridor_Tin_Design.ts index 44b9c2c9b..548b15e96 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Tin_Design.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Tin_Design.ts @@ -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); diff --git a/resources/tester/helper_115_6_corridor_tin.cjs b/resources/tester/helper_115_6_corridor_tin.cjs index b1aa8229b..13d11319e 100644 --- a/resources/tester/helper_115_6_corridor_tin.cjs +++ b/resources/tester/helper_115_6_corridor_tin.cjs @@ -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), diff --git a/resources/tester/test_115_6_corridor_tin.py b/resources/tester/test_115_6_corridor_tin.py index e0ad94d7d..e5d3479a8 100644 --- a/resources/tester/test_115_6_corridor_tin.py +++ b/resources/tester/test_115_6_corridor_tin.py @@ -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 # 원본 정점 버퍼 그대로