- 벽 · 무늬를 덮개와 함께 맨 위로 · 관 · 클릭 판정면 · 글은 그 위로 다시(지금 차례 그대로) - 원지반 · 설계선 · 비탈 · 다른 외곽이 벽 안에서 안 보임 · 벽 자기 외곽 · 무늬는 그대로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
198 lines
7.9 KiB
TypeScript
198 lines
7.9 KiB
TypeScript
/* =============================================================================
|
||
* 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_MIN_PURE_HEIGHT_M,
|
||
} from "./B06_Section_UI_Cross_Culvert_Const";
|
||
import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const";
|
||
import type { OffsetPoint, WallLayout } from "./B06_Section_UI_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 },
|
||
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"),
|
||
),
|
||
);
|
||
}
|