/* ============================================================================= * 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); }