- 카드 하단 ◀/▶/↺(개별 표시 반폭) 삭제. 조작구는 우측 상단 줌 버튼 하나로. - 줌 버튼 역할 확장(원배율에서만 반폭 조작): − 배율>1이면 축소, 원배율이면 표시 반폭 +1m + 배율>1이면 확대, 원배율이면 표시 반폭 −1m(하한 2m) ⤢ 배율 원복 + 절·성토 교차점이 다 보이는 폭으로 맞춤 - 교차점 맞춤(_UI_Cross_Fit): 보유 샘플(계산 반폭 20m) 안에서 교차점을 찾고, 샘플 끝까지 안 만나면 끝단 사면·지반 기울기 차로 만나는 자리를 추세 외삽해 올림 +1m을 요청 반폭으로 쓴다(상한 60m). - 요청 반폭 > 보유 샘플이면 그 폭으로 재생성(regenerate) 후 해당 측점만 세션 반폭으로 넓게 표시. 계산 기본 20m·캐시는 그대로, 확정 시 기존 경로대로 display_half_width_m에 영구 저장. - 재생성 로직을 _UI_Page_Common.createSampleWidener로 분리(700줄 제한), [전체 측점 반영]도 같은 경로 사용. 연타 중복 요청 차단. 자체검증: node tmp/tests/test_toe_extrapolate.mjs (외삽 좌/우·발산 지형·+1m 올림), tsc --noEmit 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
75 lines
3.6 KiB
TypeScript
75 lines
3.6 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_UI_Cross_Fit.ts
|
|
* 횡단 카드 「교차점 맞춤」 — 절토·성토 사면이 원지반과 만나는 지점(교차점)이 모두
|
|
* 보이는 표시 반폭(m)을 구한다.
|
|
*
|
|
* 보유 샘플(계산 반폭, 기본 20m) 안에서 교차점을 찾으면 그 자리 + 여유로 끝난다.
|
|
* 샘플 끝까지 만나지 않으면 **추세로 외삽**한다 — 끝단의 계획 사면 기울기와 지반
|
|
* 기울기 차이로 만나는 자리를 추정하고 +1m 해서 넘긴다(2026-08-23 사용자 지시).
|
|
* 그 값이 계산 반폭을 넘으면 페이지가 백엔드 재생성으로 샘플을 넓힌다.
|
|
* ========================================================================== */
|
|
|
|
import type { CrossSection } from "./B06_Section_Api_Fetch";
|
|
import {
|
|
designInterpolator,
|
|
groundInterpolator,
|
|
slopeToeOffset,
|
|
} from "./B06_Section_UI_Cross_Culvert_Solve";
|
|
|
|
/** 교차점이 샘플 끝에 걸렸다고 볼 여유(m) — 끝 격자에서 잡히면 실제로는 더 밖이다. */
|
|
const EDGE_TOLERANCE_M = 0.05;
|
|
/** 추세 기울기를 재는 구간 길이(m). */
|
|
const TREND_SPAN_M = 2;
|
|
/** 외삽 결과의 상한(m) — 발산하는 지형에서 폭이 무한정 커지는 것을 막는다. */
|
|
const MAX_FIT_HALF_WIDTH_M = 60;
|
|
|
|
/**
|
|
* 절·성토 교차점이 모두 들어오는 표시 반폭(m). 설계선·지반선이 없으면 null.
|
|
*
|
|
* 반환값은 1m 단위 올림 — 교차점이 축선에 딱 붙지 않게 한다.
|
|
*/
|
|
export function toeFitHalfWidth(section: CrossSection): number | null {
|
|
const design = section.design;
|
|
if (!design) return null;
|
|
const groundAt = groundInterpolator(section.samples);
|
|
const designAt = designInterpolator(design.design_line);
|
|
const edges = design.road_edges;
|
|
if (!groundAt || !designAt || !edges) return null;
|
|
const offsets = section.samples.map((sample) => sample.offset_m ?? 0);
|
|
const limits = { left: Math.max(...offsets), right: Math.min(...offsets) };
|
|
let extent = 0;
|
|
for (const side of ["left", "right"] as const) {
|
|
const edge = side === "left" ? edges.left : edges.right;
|
|
const limit = limits[side];
|
|
const toe = slopeToeOffset(designAt, groundAt, edge.offset_m, limit);
|
|
// 샘플 끝에서 멈췄고 아직 지반과 만나지 않았으면 추세로 더 밖을 추정한다.
|
|
const stopped = Math.abs(toe - limit) <= EDGE_TOLERANCE_M;
|
|
const reach = stopped ? extrapolateMeet(designAt, groundAt, limit, edge.offset_m) : toe;
|
|
extent = Math.max(extent, Math.abs(reach));
|
|
}
|
|
if (!(extent > 0)) return null;
|
|
return Math.min(Math.ceil(extent + 1), MAX_FIT_HALF_WIDTH_M);
|
|
}
|
|
|
|
/**
|
|
* 샘플 끝(limit)에서 계획 사면과 지반의 기울기 차로 만나는 자리를 외삽한다.
|
|
* 벌어지기만 하면(차이가 줄지 않으면) 끝 자리를 그대로 돌려준다 — 무한정 넓히지 않는다.
|
|
*/
|
|
function extrapolateMeet(
|
|
designAt: (offset: number) => number,
|
|
groundAt: (offset: number) => number,
|
|
limit: number,
|
|
startOffset: number,
|
|
): number {
|
|
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);
|
|
// 바깥으로 갈수록 차이가 줄어드는 속도(m/m). 0 이하이면 만나지 않는다.
|
|
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_FIT_HALF_WIDTH_M);
|
|
}
|