- 카드 · B05 오른쪽 판 · [상세] 창 십자선 · 계획 상자 = section.design.design_elevation_m · 설계 없을 때만 종단 표본 - 원지반 십자선(center_z · 원지반 색 · 다른 무리) 먼저 그림 · 겹치면 보라 위 · 확대 보정 둘 다 - [상세] 표 원지반고 = 원지반 십자 · 절성토고 = 둘 다 - 시험 318 · 150 · 164 고쳐 씀 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG
139 lines
5.9 KiB
TypeScript
139 lines
5.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;
|
|
/** 중심 원지반고(`section.center_z` · 없으면 null = 중심에 가장 가까운 지반고). */
|
|
groundElevation?: 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 groundElevation = input.groundElevation ?? centerSample?.elevation_m ?? null;
|
|
// 십자선 = 계획 노선 자리(구 데이터는 지반고 폴백).
|
|
const centerElevation = hasDesign ? designElevation : groundElevation;
|
|
const centerY = centerElevation !== null ? toY(centerElevation) : heightPx / 2;
|
|
const cross = (groupClass: string, y: number, lineClass: string): SVGElement => {
|
|
const group = svgElement("g", { class: groupClass });
|
|
group.append(
|
|
svgElement("line", { x1: centerX, y1: y - 18, x2: centerX, y2: y + 18, class: lineClass }),
|
|
svgElement("line", { x1: centerX - 18, y1: y, x2: centerX + 18, y2: y, class: lineClass }),
|
|
);
|
|
return group;
|
|
};
|
|
// 원지반 십자선(413) — 계획 십자선보다 먼저(겹치면 보라가 위) · 다른 무리(고름 · 강조 선택자 안 닿음)
|
|
const groundY = groundElevation !== null ? toY(groundElevation) : null;
|
|
const ground =
|
|
hasDesign && groundY !== null
|
|
? cross("b06-chart__ground-center", groundY, "b06-chart__ground-center-marker")
|
|
: null;
|
|
const marker = cross(
|
|
"b06-chart__center",
|
|
centerY,
|
|
`b06-chart__center-marker${hasDesign ? " b06-chart__center-marker--design" : ""}`,
|
|
);
|
|
if (ground) plotLayer.append(ground);
|
|
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", unscaleAt(centerX, centerY, scale));
|
|
if (ground && groundY !== null)
|
|
ground.setAttribute("transform", unscaleAt(centerX, groundY, scale));
|
|
};
|
|
}
|
|
|
|
/** (cx, cy) 를 축으로 배율을 되돌리는 transform — 카드 · [상세] 창 십자선이 같이 씀. */
|
|
export function unscaleAt(cx: number, cy: number, scale: number): string {
|
|
return `translate(${cx} ${cy}) scale(${1 / Math.max(scale, 1e-6)}) translate(${-cx} ${-cy})`;
|
|
}
|
|
|
|
/** 원지반선 — 끊긴 샘플(무효 표고)마다 폴리선을 나눠 그린다. `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),
|
|
};
|
|
}
|