// 교차점 탐색·외삽(B06_Section_UI_Cross_Fit) 자체검증 — 로직 복제. // 사면이 5m를 넘어도 실제 지반 교차점을 짚어야 한다(2026-08-23 사용자 보고). // 실행: node tmp/tests/test_toe_extrapolate.mjs import assert from "node:assert/strict"; const TREND_SPAN_M = 2; const MAX = 60; function extrapolateMeet(designAt, groundAt, limit, startOffset) { const outward = Math.sign(limit - startOffset) || 1; const back = limit - outward * TREND_SPAN_M; const diffAtLimit = designAt(limit) - groundAt(limit); const diffAtBack = designAt(back) - groundAt(back); const closingRate = (Math.abs(diffAtBack) - Math.abs(diffAtLimit)) / TREND_SPAN_M; if (!(closingRate > 1e-6)) return limit; const extra = Math.abs(diffAtLimit) / closingRate; if (!Number.isFinite(extra)) return limit; return limit + outward * Math.min(extra, MAX); } // 성토 사면 1:1.5(바깥으로 갈수록 0.667m/m 하강), 지반 평탄 → 실제 교차점 = 노견+15m. const edge = 3; const designAt = (o) => 10 - ((o - edge) * 2) / 3; const groundAt = () => 0; // 샘플 끝 15m에서 아직 안 만남(차이 2m) → 외삽이 실제 교차점 18m를 짚어야 한다. const meet = extrapolateMeet(designAt, groundAt, 15, edge); assert.ok(Math.abs(meet - 18) < 1e-6, `외삽 어긋남: ${meet}`); // 반대쪽(음의 offset)도 부호가 맞아야 한다. const designLeft = (o) => 10 - ((-edge - o) * 2) / 3; const meetLeft = extrapolateMeet(designLeft, groundAt, -15, -edge); assert.ok(Math.abs(meetLeft + 18) < 1e-6, `좌측 외삽 어긋남: ${meetLeft}`); // 벌어지기만 하는 지형(지반이 사면보다 더 빨리 내려감)은 넓히지 않는다. const divergingGround = (o) => -Math.abs(o) * 2; assert.equal(extrapolateMeet(designAt, divergingGround, 15, edge), 15); // 표시 반폭 = 올림(교차점) + 1m — 18m 교차점이면 19m. assert.equal(Math.min(Math.ceil(Math.abs(meet) + 1), MAX), 19); console.log("ok"); // ── 실제 교차점 탐색(meetOffset) — 성토 사면 5m 규칙에 잘리면 안 된다. const SCAN_STEP_M = 0.1; const MEET_TOLERANCE_M = 0.02; function meetOffset(designAt, groundAt, startOffset, limitOffset, outward) { const span = Math.abs(limitOffset - startOffset); if (!(span > 0)) return startOffset; const steps = Math.ceil(span / SCAN_STEP_M); let previousDiff = designAt(startOffset) - groundAt(startOffset); for (let i = 1; i <= steps; i += 1) { const o = startOffset + outward * Math.min(SCAN_STEP_M * i, span); const diff = designAt(o) - groundAt(o); if (Math.abs(diff) <= MEET_TOLERANCE_M) return o; if (previousDiff !== 0 && Math.sign(diff) !== Math.sign(previousDiff)) return o; previousDiff = diff; } return extrapolateMeet(designAt, groundAt, limitOffset, outward); } // 사면 길이 약 18m(1:1.5, 높이 10m) — 5m 컷오프면 노견+약 4.2m에서 멈춘다. const meetFar = meetOffset(designAt, groundAt, edge, 25, 1); assert.ok(Math.abs(meetFar - 18) < 0.15, `교차점 탐색 어긋남: ${meetFar}`); assert.ok(meetFar > edge + 5, "5m 사면 규칙에 잘렸다"); // 설계선이 교차 전에 끝나면(15m까지만) 외삽으로 같은 자리를 짚는다. const meetClipped = meetOffset(designAt, groundAt, edge, 15, 1); assert.ok(Math.abs(meetClipped - 18) < 1e-6, `외삽 어긋남: ${meetClipped}`); console.log("ok(fit)");