Files
Aislo/B06_Section/B06_Section_UI_Cross_View_Frame.ts
T
eomsangdonandClaude Opus 5.5 fc1068a9cd fix(B05·B06): 유토곡선 토량 분배 때 종단 · 표 스크롤 유지 · 횡단 보기 = 보기 반폭 그대로 · 십자선 크기 고정 · 보기 반폭 기본 10 m(79-1~4)
- 토량 분배(전체 맞춤) 중에는 유토곡선 ↔ 종단 스크롤 동기화를 끊음 — 0 으로 되돌던 것 없앰
- 횡단 카드는 사면 교차점 · 배수관 세트 폭으로 줌아웃하지 않음 · 밖은 가운데 버튼 팬 · ⤢ 는 보기값으로
- 줌인해도 중심 십자선 길이 그대로(도형 레이어 배율을 십자선 중심에서 되돌림)
- 횡단 보기 반폭 기본 12 → 10 m(계산 반폭과 따로)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019EMfGERKUg5nMMNQsyVZ2j
2026-10-07 23:50:13 +09:00

127 lines
5.1 KiB
TypeScript

/* =============================================================================
* B06_Section_UI_Cross_View_Frame.ts
* 횡단 카드의 **틀 그리기** — 십자선(계획 노선 자리) · 아래 · 왼쪽 축선 · 높이 축 이름, 그리고
* 구조물 폐회로 면적 반영. `_Cross_View` 가 700줄을 넘어 여기로 뗐다(계획서 24-3b).
* ========================================================================== */
import type { CrossSection } from "./B06_Section_Api_Fetch";
import { computeStructureAreas, type DesignTrim } from "@util/common_util_cross_structure_areas";
import { CROSS_PAD, L, svgElement, svgText } 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})`,
);
}
/**
* 구조물이 만든 폐회로 면적을 측점 설계에 반영한다(2026-09-06 사용자 확정).
*
* 표준 설계선만 보는 기본 계산은 구조물이 있는지 모른다 — 기슭막이·세월교·BOX가 서면
* 성토 사면이 벽에서 끊기고 그 바깥은 벽·성토부선이 대신 그리므로 폐회로가 달라진다.
* 트림이 없으면(구조물 없는 측점) 아무것도 하지 않는다.
*
* 조작 중 즉시 반영용이다. 정본은 [저장]·[확정] 때 서버가 같은 코드
* (`B06_Section_Structure_Areas_Node.ts`)로 다시 계산해 얹는다(2026-09-06).
*/
export function applyStructureAreas(
section: CrossSection,
trim: DesignTrim | undefined | null,
): void {
const design = section.design;
if (!trim || !design || !Array.isArray(design.design_line) || design.design_line.length < 2) {
return;
}
const ground = section.samples
.filter((sample) => sample.valid !== false && typeof sample.elevation_m === "number")
.map((sample) => ({ offset: sample.offset_m ?? 0, elevation: sample.elevation_m as number }))
.sort((a, b) => a.offset - b.offset);
const areas = computeStructureAreas({
designLine: design.design_line as Array<{ offset_m: number; elevation_m: number }>,
ground,
trim,
rockBoundaryOffsetM:
typeof design.rock_boundary_offset_m === "number" ? design.rock_boundary_offset_m : null,
});
if (!areas) return;
design.cut_area_m2 = Number(areas.cutAreaM2.toFixed(4));
design.fill_area_m2 = Number(areas.fillAreaM2.toFixed(4));
if (typeof design.cut_soil_area_m2 === "number") {
design.cut_soil_area_m2 = Number(areas.cutSoilAreaM2.toFixed(4));
}
}