feat(B06): 노면공 횡단 형상 — 두께 · 포장사양 무늬 셋 · 확폭 끔 규격 폭 · B07 따라옴(88-2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
This commit is contained in:
2026-10-08 12:03:31 +09:00
co-authored by Claude Opus 5.5
parent 6f76fe9f31
commit ebb8eb1302
4 changed files with 89 additions and 17 deletions
@@ -1,3 +1,4 @@
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";
@@ -29,26 +30,80 @@ export function appendRockBoundaryOverlay(
}
}
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)),
};
}
/**
* 포장층 — 노면선(= 포장 윗면) 아래로 두께만큼. 노면공 범위(`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 (!design.paved || !edges) return;
const thickness = design.pavement_thickness_m ?? 0.2;
const { left, right } = edges;
const polygon = document.createElementNS(SVG_NS, "polygon");
polygon.setAttribute(
"points",
[
`${x(left.offset_m)},${y(left.elevation_m)}`,
`${x(right.offset_m)},${y(right.elevation_m)}`,
`${x(right.offset_m)},${y(right.elevation_m - thickness)}`,
`${x(left.offset_m)},${y(left.elevation_m - thickness)}`,
].join(" "),
);
polygon.setAttribute("class", "b06-chart__pavement");
svg.append(polygon);
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");
svg.append(polygon);
const spec = entry?.spec;
const mark = spec ? PAVE_MARKS[spec] : undefined;
if (!spec || !mark) continue;
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}">${mark.marks}</pattern>`;
const hatch = document.createElementNS(SVG_NS, "polygon");
hatch.setAttribute("points", points);
hatch.setAttribute("class", "b06-chart__pavement-hatch");
hatch.setAttribute("fill", `url(#${id})`);
hatch.dataset.paveSpec = spec;
svg.append(defs, hatch);
}
}
+2 -1
View File
@@ -429,7 +429,8 @@ export function createCrossSectionCard(
x,
toDisplayY,
);
if (!fordPaved) appendPavementOverlay(plotLayer, section.design, x, toDisplayY);
if (!fordPaved)
appendPavementOverlay(plotLayer, section.design, x, toDisplayY, section.pavement);
// 세월교 측점 — 계획고는 이미 월류 높이만큼 내려와 있다. "월류가 없었다면"
// 노면을 점선으로 남겨 얼마나 내려앉았는지 읽히게 한다(2026-08-30 사용자).
appendFordSurfaceDropPlan(plotLayer, section.design, x, toDisplayY, fordLayout);
@@ -185,6 +185,22 @@
stroke-width: 1;
}
/* 노면공 포장사양 무늬(88): 채움 박스 위에 무늬만 한 겹 — 콘크리트 빗금 · 아스콘 점 · 쇄석 돌 */
.b06-chart__pavement-hatch {
stroke: none;
}
.b06-chart__pave-mark {
fill: none;
stroke: color-mix(in srgb, var(--color-text-body) 70%, transparent);
stroke-width: 1;
}
.b06-chart__pave-mark--dot {
fill: color-mix(in srgb, var(--color-text-body) 70%, transparent);
stroke: none;
}
/* 독립 기슭막이(2026-08-28): 성토면 끝에 서는 벽 — 배관 세트 기슭막이와 같은 색 계열 */
.b06-chart__revetment {
fill: color-mix(in srgb, #9b6bdc 22%, transparent);
@@ -415,7 +415,7 @@ function drawDesign(
): void {
const { design, culvert, box, ford, own } = layouts;
const paved = appendFordPavementOverlay(svg, section.ford_pavement, design, x, y);
if (!paved) appendPavementOverlay(svg, design, x, y);
if (!paved) appendPavementOverlay(svg, design, x, y, section.pavement);
appendCrossDesignOverlay(
svg,
design,