- 맹암거 둘레에 고름 표식(data-structure-id) · 287-5 판정면을 같이 탐(손잡이 아래 · 3 px 끌기 = 고름 아님) - 형상 누름 창구 shapePickRoute — 관 지점(물넘이) = 그 카드 시설 고름 · 맹암거 = id 알림 → 구조물 목록이 id 로 고름(알약 · 폼 · 측점선 따라옴 · 노면공 포장 범위 고름과 따로) - 목록 · 알약 고름 → 형상 강조(287-5 highlightShapes) - 시험 test_288 · 287 대조 글 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
138 lines
5.9 KiB
TypeScript
138 lines
5.9 KiB
TypeScript
/* =============================================================================
|
||
* B06_Section_UI_Cross_Underdrain.ts
|
||
* 맹암거 횡단 형상(288-1 · 요구 183-3).
|
||
*
|
||
* · 노면 = 설계 노면 — 서버 · TS 짝이 암거 경사로 지대 낮은 쪽 한 경사로 기울여 냄 · 측구 없음.
|
||
* · 옵셋선 = 노면을 암거 높이만큼 아래로 같은 각도.
|
||
* · 성토 쪽 = 옵셋선을 성토 사면과 만날 때까지 · 절토 쪽 = 절토 사면을 아래로 늘여 옵셋선과 만남.
|
||
* · 노면 · 양쪽 선 · 옵셋선이 둘러싼 안 = 돌 무늬(흙막이 돌 그리기 `drawStones`).
|
||
* 높이가 비면 그리지 않음(수치를 지어내지 않음).
|
||
* ========================================================================== */
|
||
|
||
import type { CrossDesign } from "./B06_Section_Api_Fetch";
|
||
import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
|
||
import { drawStones } from "./B06_Section_UI_Cross_Wall_Hatch";
|
||
import type { UnderdrainSpec } from "@util/common_util_structure_underdrain";
|
||
|
||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||
let clipSeq = 0;
|
||
|
||
type Edge = { offset_m: number; elevation_m: number };
|
||
|
||
/** 한쪽 둘레(노면 끝 → 옵셋선과 만나는 자리) — `outward` = 바깥 부호(+1 좌 · −1 우). */
|
||
function sidePath(
|
||
edge: Edge,
|
||
outward: 1 | -1,
|
||
line: readonly Edge[],
|
||
roadGrade: number,
|
||
heightM: number,
|
||
): OffsetPoint[] {
|
||
const start = { offset: edge.offset_m, elevation: edge.elevation_m };
|
||
const offZ = (o: number) => edge.elevation_m + roadGrade * (o - edge.offset_m) - heightM;
|
||
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 d = Math.abs(k - roadGrade) < 1e-9 ? 0 : -heightM / (k - roadGrade);
|
||
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;
|
||
}
|
||
|
||
/** 맹암거 형상 둘레 — 좌 노면 끝 → 우 노면 끝 → 우 둘레 → 옵셋선 → 좌 둘레(위로). 높이 없으면 null. */
|
||
export function underdrainOutline(
|
||
design: CrossDesign,
|
||
heightM: number | 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 grade = (left.elevation_m - right.elevation_m) / span;
|
||
const line = design.design_line ?? [];
|
||
const leftPath = sidePath(left, 1, line, grade, heightM);
|
||
const rightPath = sidePath(right, -1, line, grade, heightM);
|
||
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) : 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 height = spec.height_m!;
|
||
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 at = (u: number, v: number): OffsetPoint => {
|
||
const offset = lo + (hi - lo) * u;
|
||
return { offset, elevation: roadZ(offset) - height * (v + 0.5) };
|
||
};
|
||
const [ax, ay] = px(at(0, 0));
|
||
const [bx, by] = px(at(1, 0));
|
||
drawStones(
|
||
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,
|
||
},
|
||
false,
|
||
height,
|
||
);
|
||
group.append(body, clip, stones);
|
||
svg.append(group);
|
||
return group;
|
||
}
|