2026-08-28 사용자 지시 4건.
① 물넘이 파임을 3D 코리도에도 반영
classifyStation 이 노면·노견 조각 표고를 횡단도와 같은 산식(fordDeckElevationAt)
으로 내린다. 노면 밖 비탈은 그대로라 노견 끝에 수직 단차가 선다. BUILD_VERSION 85.
② 표준횡단면 저장분을 포장 재계산에 싣는다
확정 때 종단 정본 options 에 실린 standard_cross_section 을 읽어
enforce_pavement_ranges / attach_default_designs 에 넘긴다. 없으면 config 기본값.
③ 포장 구간 기본값 10 / 5 / 5 (기슭막이와 같은 출발값).
④ 독립 기슭막이
- 측점: is_station_planting_type() 규칙 신설(A군 + D군 구간형). 구간형은
시작·기준·종료에 측점을 심는다 — 길이가 길수록 횡단도가 여러 장 나온다.
구조물 재이관 차단도 같은 규칙을 본다(기타 고스트 방지).
- 서버: B06_Section_Engine_Revetment 신설 — 구간 안 측점 전부에 section.revetment.
- 횡단도: B06_Section_UI_Cross_Revetment 신설 — 벽 상단 = 성토면 끝(설계선-지반
교차점), 아래로 높이 + 근입 0.5m, 전면 1:0.3(교본 7-3). 설치 측은 사용자가
좌/우로 고른다(레지스트리 side 옵션 신설 — 자동 판정 없음).
- 3D: 같은 폴리곤을 기준측점 전/후만큼 스윕. BUILD_VERSION 86 + 구조물 해시에
물넘이·기슭막이 제원 추가.
검증: pytest 241 passed / 7 skipped(신규 4건). 공용 브라우저 실측 —
물넘이 240m 투입 시 3D 노면이 계획고보다 0.38~0.42m 아래(대조군 200m 정상),
기슭막이 225~255 투입 시 측점 23→25·횡단 카드 3장·3D revet 솔리드 3개(31링).
임시 데이터는 원래 정본으로 복구했다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
136 lines
5.4 KiB
TypeScript
136 lines
5.4 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_UI_Cross_Ford_Pavement.ts
|
|
* 물넘이포장 — 계획고를 파낸 노면을 횡단도에 그린다(2026-08-28 사용자 확정).
|
|
*
|
|
* 다른 시설은 구조물을 얹지만 물넘이는 **노면 자체가 내려앉는다**. 그래서 그림도 다르다.
|
|
* · 파임 범위 = 노견까지 **전폭**.
|
|
* · 깊이 = **노선 중심**에서 잰 월류 높이(`depth_m`).
|
|
* · 바닥은 **유입(상류)이 높고 유출이 낮게** 기운다. 경사를 비우면 그 측점의 노면
|
|
* 횡단경사(`cross_slope_pct`)를 쓴다 — 사용자가 폼에서 바꿀 수 있다.
|
|
* · 기존 계획고는 **점선**, 물넘이 바닥은 **실선**.
|
|
* · 포장 필수 — 일반 포장층과 구분되게 진한 회색 + 빗금으로 채운다.
|
|
*
|
|
* 유입측 판정은 `section_mode`(상단측 절토 = 등고가 높은 쪽 = 상류)를 따른다.
|
|
* ========================================================================== */
|
|
|
|
import type { CrossDesign } from "./B06_Section_Api_Fetch";
|
|
|
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
/** 빗금 패턴 id는 문서 안에서 유일해야 한다 — 카드마다 하나씩 번호를 준다. */
|
|
let hatchSeq = 0;
|
|
|
|
/** 백엔드 `section.ford_pavement` 제원(치수 결정은 서버 몫 — 여기서는 좌표만 만든다). */
|
|
export interface FordPavementSpec {
|
|
span_m: number;
|
|
/** 노선 중심에서 잰 파임 깊이(m). 없으면 그리지 않는다 — 수치를 지어내지 않는다. */
|
|
depth_m: number | null;
|
|
/** 유입 → 유출 바닥 경사(%). 비우면 노면 횡단경사를 쓴다. */
|
|
slope_pct: number | null;
|
|
}
|
|
|
|
interface Edge {
|
|
offset_m: number;
|
|
elevation_m: number;
|
|
}
|
|
|
|
/** 유입측(상류) 부호 — 그 방향으로 갈수록 바닥이 높아진다. */
|
|
function inflowSign(design: CrossDesign, offsetM: number): number {
|
|
const inflowLeft = design.section_mode !== "right_cut";
|
|
const towardLeft = offsetM > 0;
|
|
return towardLeft === inflowLeft ? 1 : -1;
|
|
}
|
|
|
|
/**
|
|
* 파인 노면의 표고(m). 횡단도와 3D 코리도가 **같은 식**을 써야 두 그림이 어긋나지 않는다.
|
|
* 중심에서 깊이만큼 내리고, 유입측으로 갈수록 경사만큼 올린다.
|
|
*/
|
|
export function fordDeckElevationAt(
|
|
design: CrossDesign,
|
|
spec: FordPavementSpec,
|
|
offsetM: number,
|
|
fallbackElevationM = 0,
|
|
): number {
|
|
const depth = spec.depth_m ?? 0;
|
|
const slope = (spec.slope_pct ?? design.cross_slope_pct) / 100;
|
|
const center = design.design_elevation_m ?? fallbackElevationM;
|
|
return center - depth + slope * Math.abs(offsetM) * inflowSign(design, offsetM);
|
|
}
|
|
|
|
function bottomAt(design: CrossDesign, spec: FordPavementSpec, edge: Edge): number {
|
|
return fordDeckElevationAt(design, spec, edge.offset_m, edge.elevation_m);
|
|
}
|
|
|
|
function line(points: string[], className: string): SVGPolylineElement {
|
|
const polyline = document.createElementNS(SVG_NS, "polyline");
|
|
polyline.setAttribute("points", points.join(" "));
|
|
polyline.setAttribute("class", className);
|
|
return polyline;
|
|
}
|
|
|
|
/**
|
|
* 물넘이 파임을 그린다. 그렸으면 true — 호출부는 일반 포장층 박스를 건너뛴다
|
|
* (같은 자리에 두 겹으로 깔리면 색 구분이 사라진다).
|
|
*/
|
|
export function appendFordPavementOverlay(
|
|
svg: SVGElement,
|
|
spec: FordPavementSpec | undefined,
|
|
design: CrossDesign,
|
|
x: (offset: number) => number,
|
|
y: (elevation: number) => number,
|
|
): boolean {
|
|
if (!spec || !spec.depth_m || !design.road_edges) return false;
|
|
const { left, right } = design.road_edges;
|
|
const bottomLeft = bottomAt(design, spec, left);
|
|
const bottomRight = bottomAt(design, spec, right);
|
|
|
|
// ① 기존 계획고(점선) — 파기 전 노면이 어디였는지 남긴다.
|
|
svg.append(
|
|
line(
|
|
[
|
|
`${x(left.offset_m)},${y(left.elevation_m)}`,
|
|
`${x(right.offset_m)},${y(right.elevation_m)}`,
|
|
],
|
|
"b06-chart__ford-deck-plan",
|
|
),
|
|
);
|
|
|
|
// ② 포장층 — 바닥선에서 두께만큼 아래로. 일반 포장과 색·빗금으로 구분한다.
|
|
const thickness = design.pavement_thickness_m ?? 0.2;
|
|
const hatchId = `b06-ford-hatch-${(hatchSeq += 1)}`;
|
|
const defs = document.createElementNS(SVG_NS, "defs");
|
|
const pattern = document.createElementNS(SVG_NS, "pattern");
|
|
pattern.setAttribute("id", hatchId);
|
|
pattern.setAttribute("patternUnits", "userSpaceOnUse");
|
|
pattern.setAttribute("width", "6");
|
|
pattern.setAttribute("height", "6");
|
|
const stroke = document.createElementNS(SVG_NS, "path");
|
|
stroke.setAttribute("d", "M0,6 L6,0");
|
|
stroke.setAttribute("class", "b06-chart__ford-pavement-hatch");
|
|
pattern.append(stroke);
|
|
defs.append(pattern);
|
|
svg.append(defs);
|
|
|
|
const polygon = document.createElementNS(SVG_NS, "polygon");
|
|
polygon.setAttribute(
|
|
"points",
|
|
[
|
|
`${x(left.offset_m)},${y(bottomLeft)}`,
|
|
`${x(right.offset_m)},${y(bottomRight)}`,
|
|
`${x(right.offset_m)},${y(bottomRight - thickness)}`,
|
|
`${x(left.offset_m)},${y(bottomLeft - thickness)}`,
|
|
].join(" "),
|
|
);
|
|
polygon.setAttribute("class", "b06-chart__ford-pavement");
|
|
polygon.setAttribute("fill", `url(#${hatchId})`);
|
|
svg.append(polygon);
|
|
|
|
// ③ 물넘이 바닥(실선) — 횡단 기준선이라 가장 위에 올린다.
|
|
svg.append(
|
|
line(
|
|
[`${x(left.offset_m)},${y(bottomLeft)}`, `${x(right.offset_m)},${y(bottomRight)}`],
|
|
"b06-chart__ford-deck",
|
|
),
|
|
);
|
|
return true;
|
|
}
|