fix(B06 십자선): 십자선 = 형상과 한 높이(측점 설계 높이) · 원지반 십자선 · 평면 초안 계획선 표본도 초안 종단 (413-1)

- 카드 · 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
This commit is contained in:
2026-10-18 12:19:39 +09:00
co-authored by Claude Opus 5.5
parent 19ab7d54b9
commit c9b4f0bede
14 changed files with 210 additions and 54 deletions
+11 -6
View File
@@ -37,6 +37,7 @@ import {
type ZoomPanState,
} from "../A00_Common/b_svg_zoom_pan";
import { buildZoomControls } from "./B06_Section_UI_Cross_View_Zoom";
import { unscaleAt } from "./B06_Section_UI_Cross_View_Frame";
import { detailHomeView, layerPoints } from "./B06_Section_UI_Cross_Detail_Home";
import { attachDetailTable, designForTable, groupSpan } from "./B06_Section_UI_Cross_Detail_Table";
import { createCrossAxes, IDENTITY_VIEW, type CrossAxesOptions } from "./B06_Section_UI_Cross_Axes";
@@ -209,16 +210,20 @@ export function attachDetailZoom(
const [, , width, height] = (clone.getAttribute("viewBox") ?? "").split(/[\s,]+/).map(Number);
const axisLayer = clone.querySelector<SVGGElement>("g.b06-chart__axes");
const renderAxes = axes && axisLayer ? createCrossAxes(axisLayer, axes) : null;
// 십자 중심 = 세로 선 x1 · y1 + 18 · 계획 십자가 첫 보기 중심 · 원지반 십자도 크기 되돌림(413)
const centerOf = (marker: Element | null): [number, number] => {
const line = marker?.querySelector("line");
return [Number(line?.getAttribute("x1")), Number(line?.getAttribute("y1")) + 18];
};
const marker = layer.querySelector<SVGGElement>("g.b06-chart__center");
const markerLine = marker?.querySelector("line");
const cx = Number(markerLine?.getAttribute("x1"));
const cy = Number(markerLine?.getAttribute("y1")) + 18;
const [cx, cy] = centerOf(marker);
const ground = layer.querySelector<SVGGElement>("g.b06-chart__ground-center");
const [gx, gy] = centerOf(ground);
const follow = (state: ZoomPanState): void => {
renderAxes?.(state);
marker?.setAttribute(
"transform",
`translate(${cx} ${cy}) scale(${1 / Math.max(state.scale, 1e-6)}) translate(${-cx} ${-cy})`,
);
marker?.setAttribute("transform", unscaleAt(cx, cy, state.scale));
ground?.setAttribute("transform", unscaleAt(gx, gy, state.scale));
};
follow(initial ?? IDENTITY_VIEW);
const plot = {
@@ -2,7 +2,7 @@
* B06_Section_UI_Cross_Detail_Pick.ts
* [상세] 수량표 칸 누름 → 그림 복제 안 그 항목 강조(150-4).
*
* 칸 키 → 그림 요소: 면적 띠(깍기 · 쌓기) · 사토장 선 · 중심 십자(높이 넷) · 보라 판정 선 구역
* 칸 키 → 그림 요소: 면적 띠(깍기 · 쌓기) · 사토장 선 · 계획 · 원지반 십자(높이 넷) · 보라 판정 선 구역
* (사면 · 노면 · 측구). 판정 선은 화면 좌표라 십자(x(0)) · `data-px-per-m` 로 m 로 되돌려
* `crossZoneAt` 으로 가름 — 측구 = 단면유형을 성토 양쪽으로 둬도 절토로 남는 자리.
* 편책 · 제근 · 표토제거는 늘 빈칸 = 누름 없음(짝 표에서 뺌) · 대상이 그 카드에 없는 칸 = `presentKeys` 밖.
@@ -16,10 +16,10 @@ import { crossZoneAt, type CrossZone } from "./B06_Section_UI_Cross_Design_Pick"
type LineZone = CrossZone | "ditch" | "cut_slope";
/** 칸 키 → 강조 대상(선택자 = 그 요소 · zone = 그 구역 판정 선). 없는 키 = 누름 없음. */
export const DETAIL_TARGETS: Readonly<Record<string, { selector: string } | { zone: LineZone }>> = {
ground: { selector: "g.b06-chart__center" },
ground: { selector: "g.b06-chart__ground-center" }, // 원지반 십자(413)
planned: { selector: "g.b06-chart__center" },
fill: { selector: "g.b06-chart__center" },
cut: { selector: "g.b06-chart__center" },
fill: { selector: "g.b06-chart__center, g.b06-chart__ground-center" },
cut: { selector: "g.b06-chart__center, g.b06-chart__ground-center" },
cut_soil: { selector: "g.b06-chart__area--cut_soil" },
cut_rock: { selector: "g.b06-chart__area--cut_rock" },
embankment: { selector: "g.b06-chart__area--fill" },
+6 -3
View File
@@ -344,7 +344,9 @@ export function createCrossSectionCard(
} else {
const { sourceSamples, maxOffset, elevationMid, displayMax, pixelsPerMeter, heightPx } =
metrics;
const hasDesign = designElevation !== undefined && Number.isFinite(designElevation);
// 십자선 · 계획 상자 = 형상과 한 높이(그 측점 설계 높이 · 413) · 설계 없을 때만 넘겨받은 종단 표본
const designZ = section.design?.design_elevation_m ?? designElevation;
const hasDesign = designZ !== undefined && Number.isFinite(designZ);
const valid = sourceSamples.filter(validElevation);
const exaggeration = Math.max(verticalExaggeration, 0.1);
// 좌표 규약(+offset=좌, -offset=우)을 표준 횡단면도 관례에 맞춘다: 진행방향을 바라보는
@@ -378,7 +380,7 @@ export function createCrossSectionCard(
elevationMid,
exaggeration,
centerZ: section.center_z ?? null,
designZ: hasDesign ? (designElevation as number) : null,
designZ: hasDesign ? (designZ as number) : null,
});
renderAxes(cardZoomStates.get(section.station_id) ?? IDENTITY_VIEW);
@@ -610,7 +612,8 @@ export function createCrossSectionCard(
const scaleCenter = appendCardFrame(svg, plotLayer, {
valid,
centerX: x(0),
designElevation: hasDesign ? (designElevation as number) : null,
designElevation: hasDesign ? (designZ as number) : null,
groundElevation: section.center_z ?? null,
toY: toDisplayY,
widthPx,
heightPx,
+34 -24
View File
@@ -15,6 +15,8 @@ export interface CardFrameInput {
centerX: number;
/** 계획고(없으면 null — 옛 자료는 지반고 폴백). */
designElevation: number | null;
/** 중심 원지반고(`section.center_z` · 없으면 null = 중심에 가장 가까운 지반고). */
groundElevation?: number | null;
/** 표고 → 화면 y(높이 배율 반영). */
toY: (elevation: number) => number;
widthPx: number;
@@ -34,27 +36,30 @@ export function appendCardFrame(
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 : (centerSample?.elevation_m ?? null);
const centerElevation = hasDesign ? designElevation : groundElevation;
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,
}),
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", {
@@ -79,12 +84,17 @@ export function appendCardFrame(
class: "b06-chart__axis-label",
}),
);
// 도형 레이어 배율을 십자선 중심에서 되돌림 — 자리는 따라가고 길이 18px 은 그대로.
return (scale) =>
marker.setAttribute(
"transform",
`translate(${centerX} ${centerY}) scale(${1 / Math.max(scale, 1e-6)}) translate(${-centerX} ${-centerY})`,
);
// 도형 레이어 배율을 십자선 중심에서 되돌림 — 자리는 따라가고 길이 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(배율 반영). */
+3 -3
View File
@@ -335,13 +335,13 @@ export function createSectionView(
applySelection(previous);
};
/** 십자선이 설 계획고 — 세월교 측점은 구체 위 노면이 월류 높이만큼 낮게 앉으므로
* 그만큼 내려 찍는다(2026-08-30 사용자 확정). 하강량은 백엔드가 계획고를 내려
* 잡으며 실어 준 값(`design.surface_drop_m`)이고, 원래 노면은 점선이 남긴다. */
/** 십자선이 설 계획고 = 그 측점 설계 높이(형상과 한 값 · 413). 설계가 없을 때만 종단 표본 —
* 세월교 측점은 월류 높이(`design.surface_drop_m`)만큼 내려 찍는다(2026-08-30 사용자 확정). */
const cardDesignElevation = (
detail: SectionDetailResponse,
section: CrossSection,
): number | undefined => {
if (section.design?.design_elevation_m !== undefined) return section.design.design_elevation_m;
const planZ = designElevationAt(detail.longitudinal.design_profiles, section.chainage_m);
if (planZ === undefined) return undefined;
return planZ - (section.design?.surface_drop_m ?? 0);
@@ -85,10 +85,17 @@
}
/* 중심점 고름(356-3) — 십자 자리 투명 원 · 고르면 보라 테두리 · 끌면 좌우. 십자는 누름을 원으로 넘김. */
.b06-chart__center {
.b06-chart__center,
.b06-chart__ground-center {
pointer-events: none;
}
/* 원지반 십자선(413) = 원지반선 색 · 계획 십자선 아래(먼저 그림) */
.b06-chart__ground-center-marker {
stroke: var(--color-chart-1);
stroke-width: 2.5;
}
.b06-chart__center-hit {
fill: transparent;
stroke: transparent;
@@ -154,6 +154,7 @@
stroke-width: 9;
}
.b06-detail__cad .b06-chart__ground-center.is-active line,
.b06-detail__cad .b06-chart__center.is-active line {
stroke: var(--color-danger);
stroke-width: 3;