/* ============================================================================= * B06_Section_UI_Cross_Body_Guard.ts * 세월교 · 박스 유입 · 유출 흙막이(73-4) — 관보호공 흙막이와 **같은 벽 · 같은 자리 규칙**. * * 세트는 배관 흙막이와 같은 `section.culvert`(관 숨김 · `body`)로 얹혀 그림 · 연동 · 경사 · 3D · * 면적 길을 그대로 탐. 다른 점은 자동 자리 하나 — 벽 전면이 관(세월교) · 본체(박스) 끝에 옴 * (배관 벽 전면이 관 끝인 것과 같음). 좌우 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 { outletPipeEnd } from "./B06_Section_UI_Cross_Culvert_Tiers"; 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"; /** 본체 끝(관 invert · 박스 저판 밑) + 본체 설계선 트림 — 본체가 이 측점에 없으면 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): OffsetPoint | null => { const side = layout.sides.find( (s) => (s.role === "left") === ((role === "inlet") === inletOnLeft), ); return side ? { offset: side.offset, elevation: side.bottomElevation } : 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): OffsetPoint | null => layout.sides.find((s) => s.role === role)?.pipeEnd ?? null; return { ends: { inlet: endOf("inlet"), outlet: endOf("outlet") }, trim: layout.designTrim }; } return null; } /** * 벽 전면을 본체 끝에 맞춘 배관 벽 4축(x · d) — `placePipeWall` 역산. * 바닥 기준(invert) = 본체 끝 표고 · 상단 = 노견 − d/1.2 · 자리 = 0점 + (x + d). */ export function bodyGuardAxes( end: OffsetPoint, wall: { autoOffset: number; outward: number; height: number; edgeElevation: number; lean: number; thickness: number; }, ): { x: number; d: number } { const d = FILL_SLOPE_RATIO_MIN * (wall.edgeElevation - wall.height - end.elevation); // 전면 위치는 자리(offset)에 평행이동만 함 — 0 에 세워 재고 옮김(노면 멈춤도 같게). const probe = buildRevetWallGeometry({ anchor: { offset: 0, elevation: end.elevation }, outward: wall.outward, height: wall.height, material: "dry", role: "outlet", thickness: wall.thickness, lean: wall.lean, roadElevation: wall.edgeElevation, }); const anchorOffset = end.offset - outletPipeEnd(probe).offset; return { x: (anchorOffset - wall.autoOffset) * wall.outward - d, d }; } /** 벽이 선 쪽은 흙막이 트림 · 안 선 쪽은 본체 트림(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), }; } /** 흙막이가 선 쪽의 본체 성토선을 걷음 — 그 쪽 성토선은 흙막이 벽이 냄(두 줄 방지). */ 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); if ("box" in layout && "topSlab" in layout) { const sides = layout.sides.map((side) => guarded(side.role === "left" ? 1 : -1) ? { ...side, fillLine: [] } : side, ); return { ...layout, sides }; } const ford = layout as FordLayout; const sides = ford.sides.map((side) => guarded(side.outward) ? { ...side, fillSegments: [] } : side, ); return { ...ford, sides } as T; }