Files
Aislo/B06_Section/B06_Section_UI_Cross_View_Frame.ts
T
eomsangdonandClaude Opus 5.5 6a3f9647f3 refactor(387-4): 횡단 다시 셈 창구 하나 · 면적 얹기 한 곳 — 화면은 묶음에 부탁
- 다시 셈(refreshCrossDesigns) 부르는 곳 = 묶음 cross 단계 하나(design_shared savedCross)
- 종단 미리 셈 · B06 설계 맞춤 · 3D 업데이트 = pipelineCross · reconcilePagePipeline(▲ 한 번 = cross 한 번)
- 입력 칸 밖 세션 고침(노견 · 소단 · 단면 유형 · 조정창)은 forceCross 로 같은 입력이어도 한 번 더
- 카드 그리기 · 횡단 보기 면적 쓰기 지움 · 다시 셈 창구가 면적을 늘 얹음(바뀐 측점만 알림)
- 저장 안 한 조정창 값을 상세 가게가 읽을 때 얹음(새 Section_Adjusts · B05 만 열어도 같은 면적)
- 새 test_384_4_cross_window · 옛 약속 시험 넷 새 약속으로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
2026-10-17 18:19:21 +09:00

129 lines
4.9 KiB
TypeScript

/* =============================================================================
* B06_Section_UI_Cross_View_Frame.ts
* 횡단 카드의 **틀 그리기** — 십자선(계획 노선 자리) · 아래 · 왼쪽 축선 · 높이 축 이름, 그리고
* 구조물 폐회로 면적 반영. `_Cross_View` 가 700줄을 넘어 여기로 뗐다(계획서 24-3b).
* ========================================================================== */
import type { CrossSection } from "./B06_Section_Api_Fetch";
import { CROSS_PAD, L, svgElement, svgText, validElevation } from "./B06_Section_UI_Section_Common";
type Sample = CrossSection["samples"][number];
export interface CardFrameInput {
/** 표고가 있는 샘플 — 계획고가 없으면 중심에 가장 가까운 지반고로 십자선을 세운다. */
valid: Sample[];
centerX: number;
/** 계획고(없으면 null — 옛 자료는 지반고 폴백). */
designElevation: number | null;
/** 표고 → 화면 y(높이 배율 반영). */
toY: (elevation: number) => number;
widthPx: number;
heightPx: number;
}
/** 십자선은 도형 레이어(줌·팬 따라감), 축선 · 축 이름은 틀 레이어(제자리)에 붙인다.
* 돌려준 함수 = 배율이 바뀔 때 부름 — 십자선은 자리만 따라가고 크기는 그대로(PLAN 79-3). */
export function appendCardFrame(
svg: SVGElement,
plotLayer: SVGElement,
input: CardFrameInput,
): (scale: number) => void {
const { valid, centerX, designElevation, toY, widthPx, heightPx } = input;
const hasDesign = designElevation !== null;
const centerSample = valid.reduce<Sample | null>((nearest, sample) => {
if (!nearest || Math.abs(sample.offset_m ?? 0) < Math.abs(nearest.offset_m ?? 0)) return sample;
return nearest;
}, null);
// 십자선 = 계획 노선 자리(구 데이터는 지반고 폴백).
const centerElevation = hasDesign ? designElevation : (centerSample?.elevation_m ?? null);
const centerY = centerElevation !== null ? toY(centerElevation) : heightPx / 2;
const centerMarkerClass = `b06-chart__center-marker${hasDesign ? " b06-chart__center-marker--design" : ""}`;
const marker = svgElement("g", { class: "b06-chart__center" });
marker.append(
svgElement("line", {
x1: centerX,
y1: centerY - 18,
x2: centerX,
y2: centerY + 18,
class: centerMarkerClass,
}),
svgElement("line", {
x1: centerX - 18,
y1: centerY,
x2: centerX + 18,
y2: centerY,
class: centerMarkerClass,
}),
);
plotLayer.append(marker);
svg.append(
svgElement("line", {
x1: CROSS_PAD.left,
y1: heightPx - CROSS_PAD.bottom,
x2: widthPx - CROSS_PAD.right,
y2: heightPx - CROSS_PAD.bottom,
class: "b06-chart__axis",
}),
svgElement("line", {
x1: CROSS_PAD.left,
y1: CROSS_PAD.top,
x2: CROSS_PAD.left,
y2: heightPx - CROSS_PAD.bottom,
class: "b06-chart__axis",
}),
svgText(L("B06_Profile_View_ElevationAxis"), {
x: 13,
y: heightPx / 2,
"text-anchor": "middle",
transform: `rotate(-90 13 ${heightPx / 2})`,
class: "b06-chart__axis-label",
}),
);
// 도형 레이어 배율을 십자선 중심에서 되돌림 — 자리는 따라가고 길이 18px 은 그대로.
return (scale) =>
marker.setAttribute(
"transform",
`translate(${centerX} ${centerY}) scale(${1 / Math.max(scale, 1e-6)}) translate(${-centerX} ${-centerY})`,
);
}
/** 원지반선 — 끊긴 샘플(무효 표고)마다 폴리선을 나눠 그린다. `toY` = 표고 → 화면 y(배율 반영). */
export function appendGroundProfile(
plotLayer: SVGElement,
samples: Sample[],
x: (offset: number) => number,
toY: (elevation: number) => number,
): void {
const segments: string[] = [];
let current: string[] = [];
for (const sample of samples) {
if (!validElevation(sample)) {
if (current.length > 1) segments.push(current.join(" "));
current = [];
continue;
}
current.push(`${x(sample.offset_m ?? 0)},${toY(sample.elevation_m)}`);
}
if (current.length > 1) segments.push(current.join(" "));
segments.forEach((points) =>
plotLayer.append(svgElement("polyline", { points, class: "b06-chart__cross-profile" })),
);
}
/** 팬 한계로 쓸 **그려진 범위** — 캐시 전 구간의 좌우 끝과 표고 상 · 하단(없으면 undefined). */
export function drawnContentBounds(
samples: Sample[],
x: (offset: number) => number,
toY: (elevation: number) => number,
): { x: number; width: number; y: number; height: number } | undefined {
const offsets = samples.map((sample) => sample.offset_m ?? 0);
const displayY = samples.filter(validElevation).map((sample) => toY(sample.elevation_m));
if (!offsets.length || !displayY.length) return undefined;
return {
x: x(Math.max(...offsets)),
width: x(Math.min(...offsets)) - x(Math.max(...offsets)),
y: Math.min(...displayY),
height: Math.max(...displayY) - Math.min(...displayY),
};
}