Files
Aislo/B06_Section/B06_Section_UI_Cross_Wall.ts
T
eomsangdonandClaude Opus 5.5 e4befd158a refactor(472-3 B06 구조물): 관 · BOX · 세월교 · 물넘이 모양 계산을 logic 으로 옮김(518-2)
- logic 새 파일 9 — B06_Section_Cross_Culvert · _Culvert_Geom · _Culvert_Extra · _Culvert_Tiers ·
  _Culvert_Types · _Culvert_Wire · _Box_Geom · _Ford_Geom · _Ford_Pavement
- 그림 파일(UI_Cross_Culvert · _Culvert_Types · _Box · _Ford · _Ford_Pavement)은 받은 좌표로 그리기만 ·
  관 겹 꼭짓점(pipeShellCorners) · 물넘이 노면 끝 · 월류선 · 유출 성토선은 logic 으로
- 옛 순수 기하 6 파일(UI_Cross_Culvert_Geom · _Extra · _Tiers · _Wire · _Box_Geom · _Ford_Geom) 지움 ·
  부르는 쪽(B05 Corridor · Structure_Layouts · Route_Carry_Patch · Wall 등)은 import 줄만
- test_472_3 GEOMETRY_PULLS 자기 11 줄 + 더는 안 끌리는 Design_Surface 1 줄 걷음
- 시험 · helper 경로를 새 logic 파일로 — 옮긴 파일을 가리키던 낡은 경로

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

200 lines
8.0 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_Wall.ts
* 기슭막이 벽 **공용 기하 + 그리기** — 배관 세트 벽(`_Cross_Culvert_Geom`/`_Culvert`)과
* 독립 기슭막이(`_Cross_Revetment`)가 같은 산식·같은 도형을 쓰도록 뽑아낸 층이다
* (2026-08-28 사용자: "배관용에서 배관만 빼면 대부분 동일해야"). 벽 1매의 합성 단면
* (하부 사다리꼴 + 상부 평행사변형, 배면 수직·전면 1:0.3)과 그 돌쌓기 표현은 여기 하나뿐.
*
* 관 유입·유출 흙막이 · 추가 흙막이 · 사면안정 흙막이 · 옹벽이 모두 이 함수 하나로 선다.
* 형태가 바꾸는 것은 안 무늬(`_Cross_Wall_Hatch`)뿐 — 외곽은 높이·자리·기울기로만 정함.
* ========================================================================== */
import {
materialLabel,
REVET_EMBED_DEPTH_M,
REVET_LEAN_RATIO,
REVET_THICKNESS_M,
WALL_FILL_TOP_INSET_M,
WALL_MIN_PURE_HEIGHT_M,
} from "./logic/B06_Section_UI_Cross_Culvert_Const";
import type { RevetMaterial } from "./logic/B06_Section_UI_Cross_Culvert_Const";
import type { OffsetPoint, WallLayout } from "./logic/B06_Section_Cross_Culvert_Types";
import { appendWallHatch } from "./B06_Section_UI_Cross_Wall_Hatch";
const SVG_NS = "http://www.w3.org/2000/svg";
function polygon(
points: Array<[number, number]>,
className: string,
tooltip: string,
): SVGPolygonElement {
const shape = document.createElementNS(SVG_NS, "polygon");
shape.setAttribute("points", points.map(([px, py]) => `${px},${py}`).join(" "));
shape.setAttribute("class", className);
if (tooltip) {
const title = document.createElementNS(SVG_NS, "title");
title.textContent = tooltip;
shape.append(title);
}
return shape;
}
/** 벽 1매의 기하 입력 — 자리 기준은 **하단선 중점**(anchor.offset)과 그 자리 기준표고
* (anchor.elevation = 관 invert 또는 지반). 벽은 그 위로 `height`만큼 선다. */
export interface RevetWallGeometryInput {
/** 하단선 중점 offset + 기준표고(벽이 이 위로 선다). */
anchor: OffsetPoint;
/** +1 = 화면 좌측(계류측), −1 = 우측. 전면 1:0.3이 이 부호로 기운다. */
outward: number;
/** 계산용 높이(기준표고~상단, 근입 0.5 제외). */
height: number;
material: RevetMaterial;
role: "inlet" | "outlet" | "extra";
/** 표기용 형태 — 미지정이면 재질 라벨(메/찰/콘크리트). */
form?: string | null;
lengthM?: number | null;
thickness?: number;
floatGapM?: number;
extraIndex?: number;
/** 전면 기울기 1:n 의 n — 표준경사(`leanOf`). 없으면 종전 0.3. */
lean?: number;
/** 노면(계획고) — 벽 윗면이 이보다 높으면 여기서 멈춤. 높이 한도는 이것 하나. */
roadElevation?: number | null;
}
/**
* 합성 단면(하부 사다리꼴 + 상부 평행사변형) 꼭짓점을 만든다. 배관 벽(`buildWall`,
* `_Cross_Culvert_Geom` 335~376행)과 **같은 산식** — 배면 수직, 상단 변 = 사다리꼴
* 상단(t/2)+띠(t), 전면 1:0.3, 하단선 = 기준표고 −0.5m 수평 기초.
*/
export function buildRevetWallGeometry(input: RevetWallGeometryInput): WallLayout {
const { anchor, outward, material, role } = input;
const road = input.roadElevation;
const height =
road != null && anchor.elevation + input.height > road + 1e-9
? Math.max(road - anchor.elevation, WALL_MIN_PURE_HEIGHT_M - REVET_EMBED_DEPTH_M)
: input.height;
const thickness = input.thickness ?? REVET_THICKNESS_M;
const lean = input.lean ?? REVET_LEAN_RATIO;
const baseWidth = thickness * 1.5 + lean * height;
const backOffset = anchor.offset - outward * (baseWidth / 2);
const topJoint = backOffset + outward * (thickness / 2);
const topElevation = anchor.elevation + height;
const topBack: OffsetPoint = { offset: backOffset, elevation: topElevation };
const topFront = topJoint + outward * thickness;
const frontXAt = (elevation: number): number =>
topFront + outward * lean * (topElevation - elevation);
const bottomElevation = anchor.elevation - REVET_EMBED_DEPTH_M;
const bottomBack: OffsetPoint = { offset: backOffset, elevation: bottomElevation };
const bottomFront: OffsetPoint = {
offset: frontXAt(bottomElevation),
elevation: bottomElevation,
};
return {
role,
extraIndex: input.extraIndex,
form: input.form ?? materialLabel(material),
lengthM: input.lengthM ?? null,
backOffset,
outerOffset: bottomFront.offset,
base: anchor.elevation,
height,
floatGapM: input.floatGapM ?? 0,
material,
lean,
outward,
topBack,
topJoint: { offset: topJoint, elevation: topElevation },
fillTop: { offset: topFront - outward * WALL_FILL_TOP_INSET_M, elevation: topElevation },
bottomBack,
bottomFront,
points: [bottomBack, topBack, { offset: topFront, elevation: topElevation }, bottomFront],
};
}
/**
* 벽 1매를 SVG로 그린다 — 폴리곤 + 대각 이음선 + 계류측 기운 띠의 돌쌓기 해칭.
* 배관 오버레이(`_Cross_Culvert` 161~246행)와 **같은 그리기**. 선택·강조 배선은 호출자
* 몫이라 여기서는 본체 폴리곤만 돌려준다. `keyId`는 clipPath id 중복 방지용.
*/
export function drawRevetWall(
layer: SVGElement,
wall: WallLayout,
x: (offset: number) => number,
toDisplayY: (elevation: number) => number,
className: string,
tooltip: string,
keyId: string,
): SVGPolygonElement {
const revetShape = polygon(
wall.points.map((p) => [x(p.offset), toDisplayY(p.elevation)] as [number, number]),
className,
tooltip,
);
layer.append(revetShape);
// 이음선 + **형태별 표현**(돌/콘크리트/돌망태/통나무/바자)은 공용 해칭이 맡는다
// (2026-08-30 사용자 지시 3) — 배관 오버레이와 같은 함수를 쓴다.
appendWallHatch(layer, wall, x, toDisplayY, keyId);
return revetShape;
}
/** 성토·절토 접속선(공사 계획선) 폴리라인 — 설계선과 같은 보라 실선. */
export function appendPlanLine(
layer: SVGElement,
points: OffsetPoint[],
x: (offset: number) => number,
toDisplayY: (elevation: number) => number,
tooltip: string,
className = "b06-chart__design-cross",
): void {
if (points.length < 2) return;
const line = document.createElementNS(SVG_NS, "polyline");
line.setAttribute(
"points",
points.map((p) => `${x(p.offset)},${toDisplayY(p.elevation)}`).join(" "),
);
line.setAttribute("class", className);
if (tooltip) {
const title = document.createElementNS(SVG_NS, "title");
title.textContent = tooltip;
line.append(title);
}
layer.append(line);
}
/**
* 벽 안으로 지나가는 다른 선(원지반 · 설계선 · 비탈 · 다른 외곽)을 가린다(66-5) — 흙막이 · 옹벽을
* 바탕색 덮개와 함께 맨 위로 올림. 벽 자기 외곽 · 무늬는 그대로. 관 · 클릭 판정면 · 글은 그 위로
* 다시 올려 지금 차례(관이 벽을 지나감 · 판정면 맨 위)를 지킨다.
*/
export function raiseWalls(layer: SVGElement): void {
const kids = [...layer.children];
const groups = kids
.filter((el) => el.classList.contains("b06-chart__culvert-revet"))
.map((wall) => {
// 무늬 = 벽 바로 뒤의 clipPath + 잘린 묶음(`appendWallHatch`)
const hatch: Element[] = [];
let next = wall.nextElementSibling;
while (
next &&
(next.tagName === "clipPath" ||
next.getAttribute("clip-path")?.startsWith("url(#b06-revet-clip-"))
) {
hatch.push(next);
next = next.nextElementSibling;
}
const cover = document.createElementNS(SVG_NS, "polygon");
cover.setAttribute("points", wall.getAttribute("points") ?? "");
cover.setAttribute("class", "b06-chart__wall-cover");
return [cover, wall, ...hatch];
});
if (!groups.length) return;
for (const group of groups) layer.append(...group);
layer.append(
...kids.filter((el) =>
el.matches(".b06-chart__culvert-pipe, .b06-chart__culvert-revet-hit, text"),
),
);
}