- 좌측 패널 · 아래 단추 넷 · 종단도 · 구조물 배치 폼을 페이지에 한 번만 만듦(design_shared) — 두 보기가 같은 노드 - 보기는 가운데 위 칸만: 종단 보기 = 3D(design_view_profile) · 횡단 보기 = 횡단 카드(design_view_cross · 처음 보일 때 그림) - 옛 B05_Profile_UI_Page · B06_Section_UI_Page 삭제 · 한 벌이 받은 조작은 지금 보기로, 관 목록 · 횡단 재계산 알림은 두 보기 모두로 - 진행 단계 종단설계 · 횡단설계 두 칸이 보기를 따라 켜짐 · 누르면 주소만 바꿈 · 제목도 따라감(setTitle · setActiveStep) - [저장] · [확정] 한 벌 — 두 보기 몫을 모아 saveDesign 한 번 · 종단 보기 [확정] 켬(횡단 보기를 숨긴 채 세워 횡단 자료를 얻음) - [초기화] · 계획노선 편집은 같은 페이지라도 다시 세움(remountRoute) - 덧붙임: 방향키는 초점 있는 쪽만(조정창 ↔ 종단도 계획고) · 횡단 보기 계획고 편집에도 [3D 업데이트] 대기 표시 · 종단 보기 폼 관 옵션을 횡단 캐시에도 얹음 · 종단 보기에서 횡단 조정 칸 가림 - 나눔: Cross_View 756→658(_Cross_View_Frame) · Page_Structures_Panel 700→547(_Structures_Spec) · 옛 B05 Page 786 → design_view_profile 638(_Page_Latest · _Page_Stations) - 시험: test_design_view_parity 한 벌 기준(한 패널 · 보기 오가도 같은 노드) · page_single · profile_single · save_channels · save_recompute 등 옮긴 자리로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
118 lines
4.6 KiB
TypeScript
118 lines
4.6 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;
|
|
}
|
|
|
|
/** 십자선은 도형 레이어(줌·팬 따라감), 축선 · 축 이름은 틀 레이어(제자리)에 붙인다. */
|
|
export function appendCardFrame(
|
|
svg: SVGElement,
|
|
plotLayer: SVGElement,
|
|
input: CardFrameInput,
|
|
): 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" : ""}`;
|
|
plotLayer.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,
|
|
}),
|
|
);
|
|
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",
|
|
}),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 구조물이 만든 폐회로 면적을 측점 설계에 반영한다(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));
|
|
}
|
|
}
|