Files
Aislo/B06_Section/B06_Section_UI_Cross_Underdrain.ts
T
eomsangdonandClaude Opus 5.5 d3315da993 refactor(472-3 B06 벽 · 기슭막이 · 배치): 구조물 기하 logic 분리 · 그림 파일은 그리기만
- logic 새 파일 다섯 — Cross_Wall_Geom(벽 1매) · Cross_Wall_Hatch_Geom(무늬 띠 · 물빼기 · 도수로 띠 · 원지반선) · Cross_Excavation_Geom(벽 터파기) · Cross_Design_Surface_Geom(암선 · 포장층) · Cross_Revetment_Geom(독립 기슭막이 배치)
- logic 으로 옮김 — Body_Guard → Cross_Body_Guard · Structure_Layouts · Route_Carry_Patch(내용 그대로 · import 경로만)
- inferStationInterval → common_util_station(글자표 없는 자리 · common_util_svg 는 다시 내보냄) — Node 묶음 ui_template_locale 끌림 끊음
- 부르는 쪽 import 줄 · 등록부 ts 경로 · 시험 · 도우미 경로
- test_472_3 GEOMETRY_PULLS 다섯 줄 걷음 · Wall_Hatch · Excavation · Design_Surface 세 줄은 Culvert · Box · Ford 그림 파일(518-2)이 그리기 함수로 부르는 동안 남김

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 19:22:27 +09:00

187 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* B06_Section_UI_Cross_Underdrain.ts
* 맹암거 횡단 형상(288-1 · 요구 183-3).
*
* · 노면 = 설계 노면 — 서버 · TS 짝이 표준 경사로 지대 낮은 쪽 한 경사로 기울여 냄 · 측구 없음.
* · 옵셋선(하단 경계) = 높은 쪽 노면 끝에서 암거 높이 아래 · 암거 경사로 낮은 쪽으로 내림(371-3) ·
* 경사가 비면 노면과 같은 각도.
* · 성토 쪽 = 옵셋선을 성토 사면과 만날 때까지 · 절토 쪽 = 절토 사면을 아래로 늘여 옵셋선과 만남.
* · 양측절토(낮은 쪽 늘인 노면 `tilt_reach`) = 늘인 노면이 원지반과 만나는 점에서 수직으로 끊음(356-1).
* · 노면 · 양쪽 선 · 옵셋선이 둘러싼 안 = 돌 무늬(306-1) — 켜마다 반 칸 엇갈리고 이웃과 겹친 돌 ·
* 기울기를 번갈아 서로 교차 · 그림 전용(누름 안 받음 · CSS `.b06-chart__underdrain`).
* 높이가 비면 그리지 않음(수치를 지어내지 않음).
* ========================================================================== */
import type { CrossDesign } from "./B06_Section_Api_Fetch";
import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
import { cell, type BandFrame } from "./B06_Section_UI_Cross_Wall_Hatch";
import { courses } from "./logic/B06_Section_Cross_Wall_Hatch_Geom";
import type { UnderdrainSpec } from "@util/common_util_structure_underdrain";
const SVG_NS = "http://www.w3.org/2000/svg";
let clipSeq = 0;
/** 돌 한 칸(m · 보기용) · 이웃과 겹침 배수 · 번갈아 기울기(도). */
const STONE_M = 0.25;
const STONE_OVERLAP = 1.4;
const STONE_TILT_DEG = 22;
/** 엇갈려 겹친 돌 무늬(306-1) — 켜(가로줄)마다 반 칸 밀고 · 돌은 칸보다 커서 이웃 · 위아래와 겹침 ·
* 기울기를 번갈아 서로 교차. `frame` v = −0.5(노면) ~ 0.5(옵셋선). */
export function drawCrossedStones(group: SVGElement, frame: BandFrame, heightM: number): void {
const rows = courses(heightM, STONE_M);
const count = courses(frame.spanM, STONE_M);
const across = (heightM / rows) * STONE_OVERLAP;
const along = (frame.spanM / count) * STONE_OVERLAP;
for (let r = 0; r < rows; r += 1) {
const shift = r % 2 ? 0 : 0.5;
for (let i = 0; i + shift <= count; i += 1) {
const tilt = (i + r) % 2 ? STONE_TILT_DEG : -STONE_TILT_DEG;
const lean = { ...frame, leanDegrees: frame.leanDegrees + tilt };
group.append(cell(lean, (i + shift) / count, (r + 0.5) / rows - 0.5, across, along, 0.45));
}
}
}
type Edge = { offset_m: number; elevation_m: number };
/** 하단 경계(옵셋선) — 표고 = `z0 + grade · (o − o0)`. */
type Bottom = { o0: number; z0: number; grade: number };
/** 한쪽 둘레(노면 끝 → 옵셋선과 만나는 자리) — `outward` = 바깥 부호(+1 좌 · −1 우). */
function sidePath(
edge: Edge,
outward: 1 | -1,
line: readonly Edge[],
bottom: Bottom,
): OffsetPoint[] {
const start = { offset: edge.offset_m, elevation: edge.elevation_m };
const offZ = (o: number) => bottom.z0 + bottom.grade * (o - bottom.o0);
const outside = line
.filter((p) => (p.offset_m - edge.offset_m) * outward > 1e-6)
.sort((a, b) => (a.offset_m - b.offset_m) * outward);
const first = outside[0];
if (!first) return [start, { offset: start.offset, elevation: offZ(start.offset) }];
if (first.elevation_m >= edge.elevation_m) {
// 절토 — 사면을 안쪽 아래로 늘여 옵셋선과 만남
const k = (first.elevation_m - edge.elevation_m) / (first.offset_m - edge.offset_m);
const g = bottom.grade;
const d = Math.abs(k - g) < 1e-9 ? 0 : (offZ(edge.offset_m) - edge.elevation_m) / (k - g);
const offset = edge.offset_m + d;
return [start, { offset, elevation: offZ(offset) }];
}
// 성토 — 사면을 따라 내려가다 옵셋선을 만나는 자리
const path: OffsetPoint[] = [start];
let prev = edge;
for (const point of outside) {
const fa = prev.elevation_m - offZ(prev.offset_m);
const fb = point.elevation_m - offZ(point.offset_m);
if (fb <= 0) {
const t = fa / (fa - fb);
const offset = prev.offset_m + (point.offset_m - prev.offset_m) * t;
path.push({ offset, elevation: offZ(offset) });
return path;
}
path.push({ offset: point.offset_m, elevation: point.elevation_m });
prev = point;
}
path.push({ offset: prev.offset_m, elevation: offZ(prev.offset_m) });
return path;
}
/** 하단 경계 — 높은 쪽(상류) 노면 끝 − 높이에서 암거 경사로 낮은 쪽으로 내림(371-3) · 경사가 비면 노면과 평행. */
function bottomOf(left: Edge, right: Edge, heightM: number, slopePct: number | null): Bottom {
const roadGrade = (left.elevation_m - right.elevation_m) / (left.offset_m - right.offset_m);
const high = left.elevation_m >= right.elevation_m ? left : right;
const low = high === left ? right : left;
const grade =
slopePct && slopePct > 0
? (Math.sign(low.offset_m - high.offset_m) * -slopePct) / 100
: roadGrade;
return { o0: high.offset_m, z0: high.elevation_m - heightM, grade };
}
/** 맹암거 형상 둘레 — 좌 노면 끝 → 우 노면 끝 → 우 둘레 → 옵셋선 → 좌 둘레(위로). 높이 없으면 null. */
export function underdrainOutline(
design: CrossDesign,
heightM: number | null,
slopePct: number | null = null,
): OffsetPoint[] | null {
const edges = design.road_edges;
if (!edges || !(heightM && heightM > 0)) return null;
const { left, right } = edges;
const span = left.offset_m - right.offset_m;
if (!(Math.abs(span) > 1e-9)) return null;
const bottom = bottomOf(left, right, heightM, slopePct);
// 양측절토(356-1) — 낮은 쪽 늘인 노면이 원지반과 만나는 점(tilt_reach)에서 끊음 · 너머 점 거름 → 꼬리가 수직으로 닫음
const reach = design.tilt_reach;
const outward = reach?.side === "left" ? 1 : -1;
const line = (design.design_line ?? []).filter(
(p) => !reach || (p.offset_m - reach.offset_m) * outward <= 1e-6,
);
const leftPath = sidePath(left, 1, line, bottom);
const rightPath = sidePath(right, -1, line, bottom);
return [
{ offset: left.offset_m, elevation: left.elevation_m },
...rightPath,
...leftPath.slice(1).reverse(),
];
}
/** 맹암거 형상을 그린다(돌 무늬 · 둘레 클립). 그렸으면 그 무리 — 둘레에 고름 표식(`data-structure-id` · 288-2). */
export function appendUnderdrainOverlay(
svg: SVGElement,
spec: UnderdrainSpec | undefined,
design: CrossDesign,
x: (offset: number) => number,
y: (elevation: number) => number,
): SVGGElement | null {
const outline = spec ? underdrainOutline(design, spec.height_m, spec.slope_pct) : null;
if (!spec || !outline) return null;
const px = (p: OffsetPoint): [number, number] => [x(p.offset), y(p.elevation)];
const points = outline.map((p) => px(p).join(",")).join(" ");
const group = document.createElementNS(SVG_NS, "g") as SVGGElement;
group.setAttribute("class", "b06-chart__underdrain");
const body = document.createElementNS(SVG_NS, "polygon");
body.setAttribute("points", points);
body.setAttribute("class", "b06-chart__underdrain-body");
if (spec.structure_id) body.setAttribute("data-structure-id", spec.structure_id);
clipSeq += 1;
const clipId = `b06-underdrain-clip-${clipSeq}`;
const clip = document.createElementNS(SVG_NS, "clipPath");
clip.setAttribute("id", clipId);
const clipShape = document.createElementNS(SVG_NS, "polygon");
clipShape.setAttribute("points", points);
clip.append(clipShape);
const stones = document.createElementNS(SVG_NS, "g");
stones.setAttribute("clip-path", `url(#${clipId})`);
const offsets = outline.map((p) => p.offset);
const [lo, hi] = [Math.min(...offsets), Math.max(...offsets)];
const { left, right } = design.road_edges;
const grade = (left.elevation_m - right.elevation_m) / (left.offset_m - right.offset_m);
const roadZ = (o: number) => left.elevation_m + grade * (o - left.offset_m);
const bottom = bottomOf(left, right, spec.height_m!, spec.slope_pct);
const bottomZ = (o: number) => bottom.z0 + bottom.grade * (o - bottom.o0);
// 돌 띠 = 노면 ~ 하단 경계(두께가 자리마다 다름 · 켜 수는 두꺼운 쪽)
const height = Math.max(roadZ(lo) - bottomZ(lo), roadZ(hi) - bottomZ(hi), spec.height_m!);
const at = (u: number, v: number): OffsetPoint => {
const offset = lo + (hi - lo) * u;
const top = roadZ(offset);
return { offset, elevation: top + (bottomZ(offset) - top) * (v + 0.5) };
};
const [ax, ay] = px(at(0, 0));
const [bx, by] = px(at(1, 0));
drawCrossedStones(
stones,
{
spanM: Math.hypot(hi - lo, grade * (hi - lo)),
pixelsPerMeter: Math.abs(x(1) - x(0)) || 1,
point: (u, v) => px(at(u, v)),
leanDegrees: (Math.atan2(bx - ax, -(by - ay)) * 180) / Math.PI,
},
height,
);
group.append(body, clip, stones);
svg.append(group);
return group;
}