Files
Aislo/B06_Section/B06_Section_UI_Cross_Design_Surface.ts
T
eomsangdonandClaude Opus 5.5 4ae7ee25cc feat(B06): 노면공 고름 = 범위 카드 테두리 대신 포장층 강조 · 카드 누름으로 노면공 열기 끊음(152-5)
- 포장층 범위 표식(종류 · 시작 · 끝) · 고른 노면공 층만 is-active(겹침은 하나만)
- 다른 구간형은 --span 그대로
- resolveSelection ③ 덮는 구조물에 거르개 · B06 은 G군 뺌 · 덮는 것 없으면 알약 · 강조 풂

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TNMXVCZXMpHpVvqmFkeChL
2026-10-09 19:13:08 +09:00

135 lines
5.5 KiB
TypeScript

import type { PavementEntry } from "@util/common_util_structure_pavement";
import type { CrossDesign } from "./B06_Section_Api_Fetch";
const SVG_NS = "http://www.w3.org/2000/svg";
export function appendRockBoundaryOverlay(
svg: SVGElement,
samples: Array<{ offset_m?: number; elevation_m?: number | null; valid: boolean }>,
offsetM: number,
x: (offset: number) => number,
y: (elevation: number) => number,
): void {
const segments: string[][] = [];
let current: string[] = [];
for (const sample of samples) {
const elevation = sample.elevation_m;
if (sample.valid === false || elevation === null || !Number.isFinite(elevation ?? NaN)) {
if (current.length > 1) segments.push(current);
current = [];
continue;
}
current.push(`${x(sample.offset_m ?? 0)},${y((elevation as number) + offsetM)}`);
}
if (current.length > 1) segments.push(current);
for (const points of segments) {
const polyline = document.createElementNS(SVG_NS, "polyline");
polyline.setAttribute("points", points.join(" "));
polyline.setAttribute("class", "b06-chart__rock-boundary");
svg.append(polyline);
}
}
type Edge = { offset_m: number; elevation_m: number };
/** 포장사양 → 무늬(88) — 콘크리트 빗금 · 아스콘 점 · 쇄석 돌. 그 밖 = 채움만. */
const PAVE_MARKS: Record<string, { size: number; marks: string }> = {
콘크리트: { size: 6, marks: '<path d="M0,6 L6,0" class="b06-chart__pave-mark"/>' },
아스콘: {
size: 5,
marks: '<circle cx="1.5" cy="1.5" r="0.7" class="b06-chart__pave-mark--dot"/>',
},
쇄석: {
size: 9,
marks:
'<circle cx="2.5" cy="2.5" r="1.6" class="b06-chart__pave-mark"/>' +
'<ellipse cx="6.8" cy="6.5" rx="1.8" ry="1.2" class="b06-chart__pave-mark"/>',
},
};
/** 무늬 id 는 문서 안에서 유일 — 카드 · 다시 그리기마다 새 번호(물넘이 빗금과 같은 방식). */
let paveSeq = 0;
/** 확폭 끈 구간 — 규격 폭 끝(좌 = left − 확폭좌 · 우 = right + 확폭우 · 표고는 두 끝 사이 직선). */
function standardEdges(edges: { left: Edge; right: Edge }, design: CrossDesign) {
const { left, right } = edges;
const span = right.offset_m - left.offset_m || 1;
const at = (offset: number): Edge => ({
offset_m: offset,
elevation_m:
left.elevation_m + ((right.elevation_m - left.elevation_m) * (offset - left.offset_m)) / span,
});
return {
left: at(left.offset_m - (design.widening_left_m ?? 0)),
right: at(right.offset_m + (design.widening_right_m ?? 0)),
};
}
/**
* 고른 노면공의 포장층만 `is-active`(152-5) — 범위 표식(종류 · 시작 · 끝)이 맞는 층. 겹친 노면공은 하나만.
* `mark` 없음 = 모두 풂.
*/
export function highlightPavement(
root: ParentNode,
mark?: { type_id: string; start_m?: number | null; end_m?: number | null },
): void {
const [low, high] = [mark?.start_m ?? 0, mark?.end_m ?? 0].sort((a, b) => a - b);
for (const layer of root.querySelectorAll<SVGElement>(".b06-chart__pavement")) {
const { paveType, paveStart, paveEnd } = layer.dataset;
const same =
!!mark &&
paveType === mark.type_id &&
Math.abs(Number(paveStart) - low) < 0.01 &&
Math.abs(Number(paveEnd) - high) < 0.01;
layer.classList.toggle("is-active", same);
}
}
/**
* 포장층 — 노면선(= 포장 윗면) 아래로 두께만큼. 노면공 범위(`section.pavement`)면 그 두께 · 무늬 ·
* 확폭 켬/끔(여러 건은 아래로 차례로 쌓음) · 없으면 카드 포장 켬일 때 표준 두께 채움만.
*/
export function appendPavementOverlay(
svg: SVGElement,
design: CrossDesign,
x: (offset: number) => number,
y: (elevation: number) => number,
pavement?: readonly PavementEntry[] | null,
): void {
const edges = design.carriageway_edges ?? design.road_edges;
if (!edges) return;
const layers = pavement?.length ? pavement : design.paved ? [null] : [];
let depth = 0;
for (const entry of layers) {
const thickness = entry?.thickness_m ?? design.pavement_thickness_m ?? 0.2;
const { left, right } = entry && !entry.widening_on ? standardEdges(edges, design) : edges;
const points = [
`${x(left.offset_m)},${y(left.elevation_m - depth)}`,
`${x(right.offset_m)},${y(right.elevation_m - depth)}`,
`${x(right.offset_m)},${y(right.elevation_m - depth - thickness)}`,
`${x(left.offset_m)},${y(left.elevation_m - depth - thickness)}`,
].join(" ");
depth += thickness;
const polygon = document.createElementNS(SVG_NS, "polygon");
polygon.setAttribute("points", points);
polygon.setAttribute("class", "b06-chart__pavement");
if (entry) {
// 범위 표식 — 고른 노면공의 포장층만 강조(152-5 · 겹친 노면공 가름)
polygon.dataset.paveType = entry.type_id;
polygon.dataset.paveStart = String(entry.start_m);
polygon.dataset.paveEnd = String(entry.end_m);
}
const spec = entry?.spec;
const mark = spec ? PAVE_MARKS[spec] : undefined;
if (spec && mark) {
// 무늬는 바탕 채움째 한 패턴 — 다각형 한 장(B07 도면 수확이 겹선을 안 담음)
const id = `b06-pave-hatch-${(paveSeq += 1)}`;
const defs = document.createElementNS(SVG_NS, "defs");
defs.innerHTML = `<pattern id="${id}" patternUnits="userSpaceOnUse" width="${mark.size}" height="${mark.size}"><rect width="${mark.size}" height="${mark.size}" class="b06-chart__pave-bg"/>${mark.marks}</pattern>`;
svg.append(defs);
polygon.style.fill = `url(#${id})`;
polygon.dataset.paveSpec = spec;
}
svg.append(polygon);
}
}