Files
Aislo/B06_Section/B06_Section_UI_Cross_Wall.ts
T
eomsangdonandClaude Opus 5 f916aa52a1 feat(B05,B06): 독립 기슭막이를 배수관 시설 종류로 이관
독립 기슭막이(구조물 정본 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>
2026-08-28 19:09:28 +09:00

210 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_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);
}