Files
Aislo/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts
T
eomsangdonandClaude Opus 5 933f513da0 feat(B06): 세월교 노면을 월류 높이만큼 내려 앉힌다
횡단도에 그려지던 노면은 사실 월류부 가장 아래를 자른 자리다 — 구체 위 노면은
월류 높이만큼 낮게 앉아야 한다(2026-08-30 사용자 확정). 계획고를 통째로 내려
잡아 측벽·바닥판·십자선·절성토 면적이 한 번에 따라오게 했다. 전폭 평행 하강이라
횡단경사·측구·사면 규칙은 그대로다.

백엔드
- `_ford_set`이 `ford_height_m`을 `overflow_depth_m`으로 스펙에 싣는다.
- `compute_cross_design(surface_drop_m=)` — 계획고를 그만큼 내려 계산하고, 내린
  양을 `surface_drop_m`으로 echo한다(프론트가 점선을 되그리는 데 쓴다).
- `ford_surface_drops()`·`ford_drop_at()` — 관 지점 정본에서 측점별 하강량을 읽는다
  (포장 구간과 같은 방식). 설계 계산 경로 전부(기본값·재계산·프리뷰·종횡단 확정)에
  물렸다.
- `enforce_ford_surface_drops()` — 저장분이 옛 계획고면 상세 조회에서 다시 계산한다.
  월류 높이를 지우면 같은 경로로 원래 계획고로 되돌아온다.

프론트
- 십자선을 하강량만큼 내려 찍는다(`cardDesignElevation`). 종단 계획고를 그대로
  쓰던 자리다.
- `appendFordSurfaceDropPlan()` — "월류가 없었다면" 노면을 점선으로 남긴다.
  물넘이포장의 「기존 계획고 점선」과 같은 표기라 한 규칙으로 읽힌다.
- 측벽·바닥판은 내려간 설계선(`design_line`)에 그대로 붙으므로 따로 손대지 않았다.

자체검증(공용 브라우저 5174 + 워크트리 백엔드 8001, 측점 7+9.7, 월류 높이 0.39m):
- `tmp/tests/test_ford_surface_drop.py` 2건 통과 — 계획고·좌우 노견이 정확히
  0.39m 내려가고, 절토는 늘고 성토는 줄며, 하강이 없으면 `surface_drop_m` 키 자체가
  없다.
- 화면 실측: y축 눈금 1m = 24.375px에서 십자선이 점선보다 9.50px 아래
  (= 0.390m, 오차 0.001m). 수정 전에는 두 선이 같은 자리였다.
- 세월교 측벽 자동 높이 1.70m → 1.50m. 이론값 1.31m이나 하한(관경 1.0 + 최소 토피
  0.5 = 1.5m)에 걸린 값이다 — 기존 규칙 그대로다.
- 성토 면적 18.46 → 13.85 ㎡. 이웃 일반 측점에는 점선이 그려지지 않는다.
- 검증에 쓴 `pipe_points.json` 임시 옵션은 백업본으로 되돌렸다(정본 무변경).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 12:54:29 +09:00

162 lines
6.3 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;
}
/**
* 세월교로 **내려 앉힌 노면** 위에 "월류가 없었다면" 노면을 점선으로 되그린다
* (2026-08-30 사용자 확정). 계획고는 이미 백엔드가 월류 높이만큼 내려 잡았으므로
* (`design.surface_drop_m`) 여기서는 그 양만큼 위로 올린 선 한 줄만 얹는다.
* 물넘이포장의 「기존 계획고 점선」과 같은 표기라 사용자가 한 규칙으로 읽는다.
*/
export function appendFordSurfaceDropPlan(
svg: SVGElement,
design: CrossDesign,
x: (offset: number) => number,
y: (elevation: number) => number,
): void {
const drop = design.surface_drop_m ?? 0;
if (!(drop > 0) || !design.road_edges) return;
const { left, right } = design.road_edges;
svg.append(
line(
[
`${x(left.offset_m)},${y(left.elevation_m + drop)}`,
`${x(right.offset_m)},${y(right.elevation_m + drop)}`,
],
"b06-chart__ford-deck-plan",
),
);
}
/**
* 물넘이 파임을 그린다. 그렸으면 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;
}