diff --git a/B06_Section/B06_Section_UI_Cross_Design_Surface.ts b/B06_Section/B06_Section_UI_Cross_Design_Surface.ts index b35b73912..dca4ed947 100644 --- a/B06_Section/B06_Section_UI_Cross_Design_Surface.ts +++ b/B06_Section/B06_Section_UI_Cross_Design_Surface.ts @@ -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 = { + 콘크리트: { size: 6, marks: '' }, + 아스콘: { + size: 5, + marks: '', + }, + 쇄석: { + size: 9, + marks: + '' + + '', + }, +}; +/** 무늬 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 = `${mark.marks}`; + 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); + } } diff --git a/B06_Section/B06_Section_UI_Cross_View.ts b/B06_Section/B06_Section_UI_Cross_View.ts index df688b16b..55bc589db 100644 --- a/B06_Section/B06_Section_UI_Cross_View.ts +++ b/B06_Section/B06_Section_UI_Cross_View.ts @@ -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); diff --git a/B06_Section/B06_Section_UI_Style_Cross_Areas.css b/B06_Section/B06_Section_UI_Style_Cross_Areas.css index d240917ba..390b8c475 100644 --- a/B06_Section/B06_Section_UI_Style_Cross_Areas.css +++ b/B06_Section/B06_Section_UI_Style_Cross_Areas.css @@ -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); diff --git a/B07_DesignDetail/B07_DesignDetail_Cross_Overlay.ts b/B07_DesignDetail/B07_DesignDetail_Cross_Overlay.ts index aa57fc4ea..ff43a5cff 100644 --- a/B07_DesignDetail/B07_DesignDetail_Cross_Overlay.ts +++ b/B07_DesignDetail/B07_DesignDetail_Cross_Overlay.ts @@ -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,