독립 기슭막이(구조물 정본 D군 구간형)를 배수관 측점(pipe_points)의 시설 종류 "revetment"로 옮긴다. 관을 숨긴(hidden_pipe) 배관 세트로 얹혀 컴퓨트·렌더·패널· 연동·경사·3D·수량이 배관 경로를 그대로 탄다. - common_util_drainage_pipes: PIPE_FACILITY_REVET 추가, 여유고 0 - B06_Section_Engine_Culvert: _revet_set/_revet_side — 관 숨김 세트 생성 - B06_Section_Router: attach_revetments 제거(관 세트에 통합) - B05_Profile_Engine_Sections: 기슭막이는 시작·기준·종료 세 측점 심기 - B05_Profile_Structure_Types.json: revetment placement=point, managed_by=pipe_points, 설치 측에 "양쪽" 추가 - B05_Profile_UI_Drainage_Facility: 기슭막이 옵션 폼(설치 측·형태·높이·길이·전후·단 수) - B06_Section_UI_Cross_Culvert_Geom: 관 숨김 시 관경 하한·역경사 클램프·유출 가드 해제 - B06_Section_UI_Cross_Culvert_Wire: restrictToSide — 설치 측(좌/우)만 남기기 - B06_Section_UI_Cross_View: 옛 D군 경로 이중그리기 가드, appliedAdjust 되받기 - B06_Section_UI_Cross_Wall: 기슭막이 벽 공용 기하·그리기 층 신설 - B05_Profile_UI_Corridor_Structures: 3D도 같은 규칙(관 실린더 숨김, 관통 컷 없음) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
210 lines
8.9 KiB
TypeScript
210 lines
8.9 KiB
TypeScript
/* =============================================================================
|
||
* B06_Section_UI_Cross_Wall.ts
|
||
* 기슭막이 벽 **공용 기하 + 그리기** — 배관 세트 벽(`_Cross_Culvert_Geom`/`_Culvert`)과
|
||
* 독립 기슭막이(`_Cross_Revetment`)가 같은 산식·같은 도형을 쓰도록 뽑아낸 층이다
|
||
* (2026-08-28 사용자: "배관용에서 배관만 빼면 대부분 동일해야"). 벽 1매의 합성 단면
|
||
* (하부 사다리꼴 + 상부 평행사변형, 배면 수직·전면 1:0.3)과 그 돌쌓기 표현은 여기 하나뿐.
|
||
*
|
||
* 배관 벽 계산은 아직 `_Cross_Culvert_Geom`이 자체 인라인으로 만든다 — 결과가 이 함수와
|
||
* 동일함을 화면으로 확인한 뒤 그쪽도 이 함수로 바꾼다(계획서 후속). 지금은 독립 기슭막이가
|
||
* 이 함수를 써서 배관 벽과 같은 모양·자리로 선다.
|
||
* ========================================================================== */
|
||
|
||
import {
|
||
materialLabel,
|
||
REVET_EMBED_DEPTH_M,
|
||
REVET_LEAN_RATIO,
|
||
REVET_THICKNESS_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";
|
||
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* 합성 단면(하부 사다리꼴 + 상부 평행사변형) 꼭짓점을 만든다. 배관 벽(`buildWall`,
|
||
* `_Cross_Culvert_Geom` 335~376행)과 **같은 산식** — 배면 수직, 상단 변 = 사다리꼴
|
||
* 상단(t/2)+띠(t), 전면 1:0.3, 하단선 = 기준표고 −0.5m 수평 기초.
|
||
*/
|
||
export function buildRevetWallGeometry(input: RevetWallGeometryInput): WallLayout {
|
||
const { anchor, outward, height, material, role } = input;
|
||
const thickness = input.thickness ?? REVET_THICKNESS_M;
|
||
const baseWidth = thickness * 1.5 + REVET_LEAN_RATIO * 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 * REVET_LEAN_RATIO * (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,
|
||
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);
|
||
|
||
// 평행사변형 띠와 사다리꼴 사이 대각 이음선(내부 경계) — 상단 변 중간점에서 바닥으로.
|
||
const joint = document.createElementNS(SVG_NS, "line");
|
||
const jointBaseOffset = wall.outerOffset - wall.outward * REVET_THICKNESS_M;
|
||
const bottomSpan = wall.bottomFront.offset - wall.bottomBack.offset;
|
||
const bottomAtOffset = (offset: number): number =>
|
||
Math.abs(bottomSpan) > 1e-9
|
||
? wall.bottomBack.elevation +
|
||
(wall.bottomFront.elevation - wall.bottomBack.elevation) *
|
||
((offset - wall.bottomBack.offset) / bottomSpan)
|
||
: wall.bottomBack.elevation;
|
||
joint.setAttribute("x1", String(x(wall.topJoint.offset)));
|
||
joint.setAttribute("y1", String(toDisplayY(wall.topJoint.elevation)));
|
||
joint.setAttribute("x2", String(x(jointBaseOffset)));
|
||
joint.setAttribute("y2", String(toDisplayY(bottomAtOffset(jointBaseOffset))));
|
||
joint.setAttribute("class", "b06-chart__culvert-stone");
|
||
layer.append(joint);
|
||
|
||
// 돌 해칭 — 큰 돌이 한 줄로 쌓인 계류측 기운 띠. 벽 폴리곤 clip 안에만 그려 어떤
|
||
// 높이에서도 돌이 벽 밖으로 새지 않게 한다.
|
||
const pixelsPerMeter = Math.abs(x(1) - x(0)) || 1;
|
||
const clipId = `b06-revet-clip-${keyId}-${Math.round(wall.backOffset * 100)}`;
|
||
const clip = document.createElementNS(SVG_NS, "clipPath");
|
||
clip.setAttribute("id", clipId);
|
||
clip.append(
|
||
polygon(
|
||
wall.points.map((p) => [x(p.offset), toDisplayY(p.elevation)] as [number, number]),
|
||
"",
|
||
"",
|
||
),
|
||
);
|
||
const stoneGroup = document.createElementNS(SVG_NS, "g");
|
||
stoneGroup.setAttribute("clip-path", `url(#${clipId})`);
|
||
layer.append(clip, stoneGroup);
|
||
|
||
const centerBaseOffset = wall.outerOffset - wall.outward * (REVET_THICKNESS_M / 2);
|
||
const embedBase = bottomAtOffset(centerBaseOffset);
|
||
const stoneSpan = Math.max(0.3, wall.topBack.elevation - embedBase);
|
||
const stoneCount = Math.max(2, Math.round(stoneSpan / 0.45));
|
||
const stoneHeight = stoneSpan / stoneCount;
|
||
const centerBase = centerBaseOffset;
|
||
const centerTop = wall.topJoint.offset + wall.outward * (REVET_THICKNESS_M / 2);
|
||
const axisX = x(centerTop) - x(centerBase);
|
||
const axisY = toDisplayY(wall.topBack.elevation) - toDisplayY(embedBase);
|
||
const leanDegrees = (Math.atan2(axisX, -axisY) * 180) / Math.PI;
|
||
for (let i = 0; i < stoneCount; i += 1) {
|
||
const fraction = (i + 0.5) / stoneCount;
|
||
const centerOffset = centerBase + (centerTop - centerBase) * fraction;
|
||
const centerElevation = embedBase + stoneSpan * fraction;
|
||
const cx = x(centerOffset);
|
||
const cy = toDisplayY(centerElevation);
|
||
const stone = document.createElementNS(SVG_NS, "rect");
|
||
const widthPx = REVET_THICKNESS_M * pixelsPerMeter * 0.9;
|
||
const heightPx = stoneHeight * pixelsPerMeter * 0.86;
|
||
stone.setAttribute("x", String(cx - widthPx / 2));
|
||
stone.setAttribute("y", String(cy - heightPx / 2));
|
||
stone.setAttribute("width", String(widthPx));
|
||
stone.setAttribute("height", String(heightPx));
|
||
stone.setAttribute("rx", String(Math.min(widthPx, heightPx) * 0.3));
|
||
stone.setAttribute("transform", `rotate(${leanDegrees.toFixed(1)} ${cx} ${cy})`);
|
||
stone.setAttribute("class", "b06-chart__culvert-stone");
|
||
stoneGroup.append(stone);
|
||
}
|
||
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);
|
||
}
|