`_UI_Cross_View` 784줄 → 698줄. 도면과 무관한 제목행·하단 정보행을 `_UI_Cross_Card_Chrome` 으로 옮기고, 실효 표시 반폭 계산은 순수 계산이라 `_UI_Cross_View_Metrics` 로, 측점별 줌·팬 상태 맵은 임자인 `_UI_Cross_View_Zoom` 으로 옮겼다. 계산·판정 규칙은 그대로다. 검증 — 공용 브라우저 B06 실측: 카드 65·제목행 65·하단행 65·단면유형 pill 65·사면 미교차 경고 13, 첫 카드 제목행 `0+0.0 / 0.0m / 토사·리핑암·발파암 / 편측 성토 / BP` 로 종전과 동일. pytest 383 passed·17 skipped, typecheck·prettier 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
103 lines
3.6 KiB
TypeScript
103 lines
3.6 KiB
TypeScript
import type { CrossSection, SectionSample } from "./B06_Section_Api_Fetch";
|
|
import { CROSS_HEIGHT, CROSS_PAD, validElevation } from "./B06_Section_UI_Section_Common";
|
|
import { culvertRequiredHalfWidth } from "./B06_Section_UI_Cross_Culvert_Wire";
|
|
import { toeFitHalfWidth } from "./B06_Section_UI_Cross_Fit";
|
|
|
|
const AREA_OVERLAY_HEADROOM_PX = 58;
|
|
|
|
interface CrossPlotMetrics {
|
|
sourceSamples: SectionSample[];
|
|
minOffset: number;
|
|
maxOffset: number;
|
|
elevationMid: number;
|
|
displaySpan: number;
|
|
displayMax: number;
|
|
pixelsPerMeter: number;
|
|
heightPx: number;
|
|
}
|
|
|
|
export function crossPlotMetrics(
|
|
section: CrossSection,
|
|
verticalExaggeration: number,
|
|
widthPx: number,
|
|
crossHalfWidth?: number,
|
|
designElevation?: number,
|
|
forcedHeightPx?: number,
|
|
): CrossPlotMetrics | null {
|
|
const sourceSamples = section.samples.filter(
|
|
(sample) =>
|
|
crossHalfWidth === undefined || Math.abs(sample.offset_m ?? 0) <= crossHalfWidth + 1e-6,
|
|
);
|
|
const valid = sourceSamples.filter(validElevation);
|
|
if (!valid.length) return null;
|
|
const offsets = sourceSamples.map((sample) => sample.offset_m ?? 0);
|
|
const minOffset = Math.min(...offsets, -1);
|
|
const maxOffset = Math.max(...offsets, 1);
|
|
const elevations = valid.map((sample) => sample.elevation_m);
|
|
if (designElevation !== undefined && Number.isFinite(designElevation))
|
|
elevations.push(designElevation);
|
|
const rawMin = Math.min(...elevations);
|
|
const rawMax = Math.max(...elevations);
|
|
const elevationMid = (rawMin + rawMax) / 2;
|
|
const padding = rawMax > rawMin ? (rawMax - rawMin) * 0.08 : 0.5;
|
|
const pixelsPerMeter =
|
|
(widthPx - CROSS_PAD.left - CROSS_PAD.right) / Math.max(maxOffset - minOffset, 1e-6);
|
|
const rawSpan = Math.max(
|
|
(rawMax - rawMin + padding * 2) * Math.max(verticalExaggeration, 0.1),
|
|
1e-6,
|
|
);
|
|
const naturalHeight =
|
|
rawSpan * pixelsPerMeter + CROSS_PAD.top + CROSS_PAD.bottom + AREA_OVERLAY_HEADROOM_PX;
|
|
const heightPx = forcedHeightPx ?? Math.max(naturalHeight, CROSS_HEIGHT);
|
|
const displaySpan = Math.max(heightPx - CROSS_PAD.top - CROSS_PAD.bottom, 1e-6) / pixelsPerMeter;
|
|
const displayMax = elevationMid + displaySpan / 2 + AREA_OVERLAY_HEADROOM_PX / pixelsPerMeter / 2;
|
|
return {
|
|
sourceSamples,
|
|
minOffset,
|
|
maxOffset,
|
|
elevationMid,
|
|
displaySpan,
|
|
displayMax,
|
|
pixelsPerMeter,
|
|
heightPx,
|
|
};
|
|
}
|
|
|
|
export function crossCardNaturalHeight(
|
|
section: CrossSection,
|
|
verticalExaggeration: number,
|
|
widthPx: number,
|
|
crossHalfWidth?: number,
|
|
designElevation?: number,
|
|
): number {
|
|
return (
|
|
crossPlotMetrics(section, verticalExaggeration, widthPx, crossHalfWidth, designElevation)
|
|
?.heightPx ?? CROSS_HEIGHT
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 카드가 실제로 그릴 표시 반폭 — 개별값 > 전역값(2026-08-06).
|
|
*
|
|
* 절·성토선이 원지반과 만나는 지점(교차점)이 반폭 밖이면 **이 카드만** 자동 줌아웃한다
|
|
* (2026-08-22, 판정 기준 개편 2026-08-23: 배수관용 5m 사면 규칙 대신 실제 교차점).
|
|
* 보유 샘플 밖까지는 넓히지 않는다 — 지반이 없어 빈 화면이 될 뿐이고, 그 경우
|
|
* [보기 반폭 적용]이 필요한 폭으로 백엔드 재생성을 건다.
|
|
*/
|
|
export function effectiveCardHalfWidth(
|
|
section: CrossSection,
|
|
baseHalfWidth: number | undefined,
|
|
): number | undefined {
|
|
const sampledExtent = Math.max(
|
|
0,
|
|
...section.samples.map((sample) => Math.abs(sample.offset_m ?? 0)),
|
|
);
|
|
const requiredHalfWidth = Math.min(
|
|
Math.max(culvertRequiredHalfWidth(section) ?? 0, toeFitHalfWidth(section) ?? 0),
|
|
sampledExtent,
|
|
);
|
|
return baseHalfWidth !== undefined && requiredHalfWidth > 0
|
|
? Math.max(baseHalfWidth, requiredHalfWidth)
|
|
: baseHalfWidth;
|
|
}
|