/* ============================================================================= * B06_Section_UI_Cross_Body_Guard.ts * 세월교 · 박스 유입 · 유출 흙막이(73-4) — 관보호공 흙막이와 **같은 벽 · 같은 자리 규칙**. * * 세트는 배관 흙막이와 같은 `section.culvert`(관 숨김 · `body`)로 얹혀 그림 · 연동 · 경사 · 3D · * 면적 길을 그대로 탐. 다른 점은 자동 자리 하나 — 벽 **배면(수직)이 본체 끝**에 붙어 벽 몸이 본체 * 바깥에 섬 · 벽 윗면 = 본체 윗면(세월교 상판 · 박스 상판) · 높이 기본 = 세월교 본체 끝 높이 · * 박스 높이 값(2 m 박스 → 2 m · 79b · 바닥 = 윗면 − 높이)(79-7 · 그림 * `tmp/79_세월교_기본위치.png`). 좌우 x 는 그 자리 기준 · 상하 d 는 배관과 같은 절대값. * 본체가 이 측점에 없으면(연동 옆 측점) 배관 규칙 그대로 — 소유 측점 값을 절대값으로 넘겨 받음. * ========================================================================== */ import type { CrossSection, SectionSample } from "./B06_Section_Api_Fetch"; import { computeBoxLayout, DEFAULT_BOX_SIDE_ADJUST } from "./B06_Section_UI_Cross_Box_Geom"; import type { BoxLayout } from "./B06_Section_UI_Cross_Box_Geom"; import { FILL_SLOPE_RATIO_MIN } from "./B06_Section_UI_Cross_Culvert_Const"; import type { CulvertDesignTrim, CulvertLayout, OffsetPoint, WallAdjust, WallLayout, } from "./B06_Section_UI_Cross_Culvert_Types"; import { computeFordLayout, DEFAULT_FORD_WALL_ADJUST } from "./B06_Section_UI_Cross_Ford_Geom"; import type { FordLayout } from "./B06_Section_UI_Cross_Ford_Geom"; import { buildRevetWallGeometry } from "./B06_Section_UI_Cross_Wall"; type Role = "inlet" | "outlet"; /** 본체 끝 — 전면 자리(offset) · 윗면(top) · 바닥(bottom) · 관 invert(세월교 바닥판 상면) 표고 · * 흙막이 기본 순수 높이(79-7 · 79b · 80). */ export interface BodyEnd { offset: number; top: number; bottom: number; invert: number; height: number; } /** 본체 끝(세월교 관 끝 · 박스 끝) + 본체 설계선 트림 — 본체가 이 측점에 없으면 null. */ export interface BodyGuardBase { ends: Record; trim: CulvertDesignTrim; } export function bodyGuardBase( section: CrossSection, samples: SectionSample[], ): BodyGuardBase | null { const body = section.culvert?.body; const adjust = section.design; if (body === "box" && section.box) { const layout = computeBoxLayout(section, samples, { left: { ...DEFAULT_BOX_SIDE_ADJUST, ...(adjust?.box_adjust?.left ?? {}) }, right: { ...DEFAULT_BOX_SIDE_ADJUST, ...(adjust?.box_adjust?.right ?? {}) }, }); if (!layout) return null; const inletOnLeft = (section.uphill_side ?? "left") === "left"; const endOf = (role: Role): BodyEnd | null => { const side = layout.sides.find( (s) => (s.role === "left") === ((role === "inlet") === inletOnLeft), ); return side ? { offset: side.offset, top: side.topElevation, bottom: side.bottomElevation, invert: side.bottomElevation, // 박스 높이 값 그대로(사용자 원문 2 m → 2 m · 79b) height: layout.box.inner_height_m, } : null; }; return { ends: { inlet: endOf("inlet"), outlet: endOf("outlet") }, trim: layout.designTrim }; } if (body === "ford" && section.ford) { const layout = computeFordLayout(section, samples, { inlet: { ...DEFAULT_FORD_WALL_ADJUST, ...(adjust?.ford_adjust?.inlet ?? {}) }, outlet: { ...DEFAULT_FORD_WALL_ADJUST, ...(adjust?.ford_adjust?.outlet ?? {}) }, }); if (!layout) return null; // 세월교 끝 = 관 끝(측벽 바깥면) · 윗면 = 측벽 상단(상판 높이) · 바닥 = 바닥판 밑 const endOf = (role: Role): BodyEnd | null => { const side = layout.sides.find((s) => s.role === role); return side ? { offset: side.pipeEnd.offset, top: side.top.elevation, bottom: side.slabTopElevation - layout.ford.slab_thickness_m, invert: side.slabTopElevation, height: side.top.elevation - (side.slabTopElevation - layout.ford.slab_thickness_m), } : null; }; return { ends: { inlet: endOf("inlet"), outlet: endOf("outlet") }, trim: layout.designTrim }; } return null; } /** 흙막이 기본 순수 높이(근입 포함) — 세월교 = 본체 끝 높이 · 박스 = 박스 높이 값(79-7 · 79b). */ export const bodyGuardHeight = (end: BodyEnd): number => end.height; /** * 벽 배면을 본체 끝에 붙인 배관 벽 4축(x · d) — `placePipeWall` 역산. * 상단 = 본체 윗면 = 노견 − d/1.2 · 바닥 기준(invert) = 상단 − 높이 · 자리 = 0점 + (x + d). */ export function bodyGuardAxes( end: BodyEnd, wall: { autoOffset: number; outward: number; height: number; edgeElevation: number; lean: number; thickness: number; }, ): BodyAxes { const d = FILL_SLOPE_RATIO_MIN * (wall.edgeElevation - end.top); // 배면 위치는 자리(offset)에 평행이동만 함 — 0 에 세워 재고 옮김(노면 멈춤도 같게). const probe = buildRevetWallGeometry({ anchor: { offset: 0, elevation: end.top - wall.height }, outward: wall.outward, height: wall.height, material: "dry", role: "outlet", thickness: wall.thickness, lean: wall.lean, roadElevation: wall.edgeElevation, }); const anchorOffset = end.offset - probe.bottomBack.offset; // 배면 → 관 invert 높이 전면 — 세월교 관이 흙막이 안으로 드는 길이(80) const front = probe.points[2].offset + wall.outward * probe.lean * (probe.topJoint.elevation - end.invert); const through = (front - probe.bottomBack.offset) * wall.outward; return { x: (anchorOffset - wall.autoOffset) * wall.outward - d, d, through, outward: wall.outward, }; } export type BodyAxes = { x: number; d: number; through: number; outward: number }; /** 세월교 관은 1 m 단위(80) — 흙막이 전면까지 든 관 길이를 올림하고 모자란 만큼 흙막이를 바깥으로 * 미는 기본 x(유출 먼저 · 배관 정수 맞춤과 같은 쪽). 손으로 민 x 는 그 위에 1 m 걸음으로 얹힘. */ export function fordPipeFit( base: BodyGuardBase | null, culvert: { body?: string | null; inlet: { structure?: string }; outlet: { structure?: string } }, axesOf: (role: Role) => BodyAxes | null, ): [BodyAxes | null, BodyAxes | null] { const axes: [BodyAxes | null, BodyAxes | null] = [axesOf("inlet"), axesOf("outlet")]; if (!base || culvert.body !== "ford") return axes; const roles = ["inlet", "outlet"] as const; const guarded = roles.map((role, i) => (culvert[role].structure ? axes[i] : null)); const tips = roles.map((role, i) => { const end = base.ends[role]; const reach = guarded[i] ? guarded[i]!.outward * guarded[i]!.through : 0; return end && { offset: end.offset + reach, z: end.invert }; }); if (!tips[0] || !tips[1]) return axes; // ponytail: 관 기울기는 작아 바깥 수평 이동으로 길이를 채움 const length = Math.hypot(tips[0].offset - tips[1].offset, tips[0].z - tips[1].z); const gap = Math.ceil(length - 1e-6) - length; const at = guarded[1] ? 1 : 0; const fit = guarded[at]; if (!fit || gap < 1e-6) return axes; axes[at] = { ...fit, x: fit.x + gap }; return axes; } /** 벽이 선 쪽은 흙막이 트림 · 안 선 쪽은 본체 트림(73-4). 벽이 하나도 없으면 null. */ export function mergeGuardTrim( guard: CulvertDesignTrim, body: CulvertDesignTrim, walls: readonly WallLayout[], ): CulvertDesignTrim | null { if (!walls.length) return null; const max = walls.some((w) => w.outward > 0) ? guard : body; const min = walls.some((w) => w.outward < 0) ? guard : body; return { minOffset: min.minOffset, maxOffset: max.maxOffset, minElevation: min.minElevation, maxElevation: max.maxElevation, minSlope: min.minSlope, maxSlope: max.maxSlope, }; } /** 소유 측점 적용값(x = 본체 기준) → 옆 측점이 받을 절대값 — 옆 측점엔 본체가 없음. */ export function bodyAbsolute(layout: CulvertLayout): CulvertLayout["revetShift"] { const shift = layout.revetShift; const bodyX = layout.bodyX; if (!bodyX) return shift; const plus = (adjust: WallAdjust, dx: number): WallAdjust => ({ ...adjust, x: adjust.x + dx }); return { ...shift, inlet: plus(shift.inlet, bodyX.inlet), outlet: plus(shift.outlet, bodyX.outlet), }; } /** 흙막이가 선 쪽의 본체 성토 · 절토선을 걷음 — 그 쪽 선은 흙막이 벽이 냄(두 줄 방지). * 세월교는 그 쪽 ㄴ형 측벽도 걷음 — 흙막이가 측벽 자리를 대신함(79-7). */ export function withoutGuardedFill( layout: T | null, culvert: CulvertLayout | null, ): T | null { if (!layout || !culvert?.culvert.body) return layout; const guarded = (outward: number): boolean => culvert.walls.some((w) => w.outward === outward); const mid = layout.label.at.offset; const reach = ( points: OffsetPoint[], outward: number, offset: (p: OffsetPoint) => number, ): OffsetPoint[] => points.map((p) => ((p.offset - mid) * outward > 0 ? { ...p, offset: offset(p) } : p)); if ("box" in layout && "topSlab" in layout) { const sides = layout.sides.map((side) => guarded(side.role === "left" ? 1 : -1) ? { ...side, fillLine: [], fillSegments: [], cutLine: null } : side, ); // 박스 본체가 흙막이 배면까지 따라감 — 흙막이를 바깥으로 민 만큼만(80) let { topSlab, bottomSlab, barrel } = layout; for (const side of layout.sides) { const outward = side.role === "left" ? 1 : -1; const wall = culvert.walls.find((w) => w.role !== "extra" && w.outward === outward); const to = wall?.bottomBack.offset ?? side.offset; if ((to - side.offset) * outward < 1e-6) continue; [topSlab, bottomSlab, barrel] = [topSlab, bottomSlab, barrel].map((p) => reach(p, outward, () => to), ); } const run = Math.max(...topSlab.map((p) => p.offset)) - Math.min(...topSlab.map((p) => p.offset)); return { ...layout, sides, topSlab, bottomSlab, barrel, bodyLengthM: run }; } const ford = layout as FordLayout; const sides = ford.sides.map((side) => guarded(side.outward) ? { ...side, parts: [], fillSegments: [], cutLine: null } : side, ); // 관 · 바닥판이 흙막이까지 따라감 — 관 끝 꼭짓점 = 그 높이의 흙막이 전면(사면 이용 · 89-1) · // 아래 꼭짓점(invert)은 종전 자리라 1 m 맞춤 그대로 · 바닥판 끝 = 배면(80) let { pipe, slabBridge } = ford; for (const side of ford.sides) { const wall = culvert.walls.find((w) => w.role === side.role && w.outward === side.outward); if (!wall) continue; const faceAt = (p: OffsetPoint): number => wall.points[2].offset + wall.outward * wall.lean * (wall.topJoint.elevation - p.elevation); pipe = { outer: reach(pipe.outer, side.outward, faceAt), inner: reach(pipe.inner, side.outward, faceAt), }; slabBridge = reach(slabBridge, side.outward, () => wall.bottomBack.offset); } if (pipe === ford.pipe) return { ...ford, sides } as T; const [right, left] = pipe.outer.slice(2); const lengthM = Math.hypot(left.offset - right.offset, left.elevation - right.elevation); const slabOffsets = [ ...slabBridge, ...sides.flatMap((s) => s.parts.flatMap((p) => p.points)), ].map((p) => p.offset); return { ...ford, sides, pipe, slabBridge, slabLengthM: Math.max(...slabOffsets) - Math.min(...slabOffsets), label: { ...ford.label, text: `${ford.label.text} L${Math.ceil(lengthM - 1e-6)}m` }, } as T; }