BOX암거를 세월교와 같은 계류 방향 절단면으로 그리고, 3D는 속이 빈 서피스 셸로 만든다. 관통 컷·중공 튜브는 배수관 세트와 세월교에도 함께 적용한다. B06 횡단도 - `_box_set()` — 내공 폭·높이(B05 옵션), 부재 두께 세월교 승계(측벽 0.2·상하판 0.3), 복토 0.5m(별표2 교차 참조), 날개벽 투영 저판 연장 - 상판 윗면 = 노면 − 복토, 구체는 수평 설치(교본 3장). 계류 하상과의 고저차는 툴팁 표기 - 구체가 노면 아래 묻히므로 계획선은 자르지 않는다(세월교와 다른 점) 3D - 로프트가 **링별 폴리곤**을 받는다 — 구조물을 위·아래 조각으로 갈라 링마다 틈을 벌려 빈 공간을 만든다. 틈 0이면 속 찬 단면 그대로라 토폴로지가 유지된다 - BOX = 내공 구간만 틈을 벌려 측벽이 저절로 생기는 셸 + 날개벽 4매(날개 축 프레임) - 관통 보어 = 링 y에서 틈 높이 2·√(r²−(y−y₀)²) → 배관 외측 기준 원형 구멍. 기슭막이·집수정·세월교 측벽에 적용 - 배관은 중공 튜브(바깥·안쪽 원통 + 양 끝 고리)로 내부 유로를 보인다 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
117 lines
5.4 KiB
TypeScript
117 lines
5.4 KiB
TypeScript
/* =============================================================================
|
||
* B06_Section_UI_Cross_Box_Geom.ts
|
||
* BOX암거 구체 **기하 계산** — 그리기(`_Cross_Box.ts`)와 분리(700줄 제한).
|
||
* 백엔드 `section.box` 제원을 좌표로 옮긴다(치수 결정 금지).
|
||
*
|
||
* 구성(2026-08-25 사용자 확정):
|
||
* · 박스 축 = 계류 방향이라 횡단도에는 **종단 절단면**(상판·내공·저판)이 보인다.
|
||
* · 상판 윗면 = **노면 − 복토 0.5m**(별표2 교차 참조), 구체는 수평 설치(교본 3장).
|
||
* · 저판은 **날개벽 투영**(길이×cos각)만큼 계류 상·하류로 길어진다 — 세월교와 같은 산식.
|
||
* · 내공(유로)은 비워 둔다 — 관 위 성토 채움처럼 면을 칠하지 않는다.
|
||
* · 구체가 노면 아래 묻히므로 **계획선은 자르지 않는다** — 노견 밖 성토 비탈이 실제로
|
||
* 존재한다(세월교는 측벽이 비탈을 대신해 잘랐다).
|
||
* 부재 두께는 세월교 승계(측벽 0.2·상판 0.3·저판 0.3)이며 백엔드가 실어 보낸다.
|
||
* ========================================================================== */
|
||
|
||
import type { BoxSet, CrossSection, SectionSample } from "./B06_Section_Api_Fetch";
|
||
import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
|
||
import { designInterpolator, groundInterpolator } from "./B06_Section_UI_Cross_Culvert_Solve";
|
||
|
||
export interface BoxLayout {
|
||
box: BoxSet;
|
||
/** 상판 폴리곤(노견~노견). */
|
||
topSlab: OffsetPoint[];
|
||
/** 저판 폴리곤 — 날개벽 투영만큼 각 측 연장. */
|
||
bottomSlab: OffsetPoint[];
|
||
/** 내공(유로) 사각 — 비워 두고 윤곽선·라벨만 쓴다. */
|
||
barrel: OffsetPoint[];
|
||
/** 상판 윗면·저판 밑면 표고. */
|
||
topElevation: number;
|
||
bottomElevation: number;
|
||
/** 저판 전체 길이(m) — 노폭 + 유입·유출 날개벽 투영. */
|
||
slabLengthM: number;
|
||
/** 구체 바닥과 계류 하상(원지반 최저)의 고저차(m). +면 구체가 하상 위로 떠 있다. */
|
||
bedGapM: number;
|
||
label: { at: OffsetPoint; text: string };
|
||
}
|
||
|
||
/** 노면 표고 — 설계선이 있으면 그 곡선, 없으면 노견 직선(세월교와 같은 규칙). */
|
||
function roadTopFactory(
|
||
section: CrossSection,
|
||
left: { offset_m: number; elevation_m: number },
|
||
right: { offset_m: number; elevation_m: number },
|
||
): (offset: number) => number {
|
||
const designAt = designInterpolator(section.design?.design_line);
|
||
if (designAt) return designAt;
|
||
const span = left.offset_m - right.offset_m;
|
||
return (offset: number): number => {
|
||
if (Math.abs(span) < 1e-6) return left.elevation_m;
|
||
const t = (offset - right.offset_m) / span;
|
||
return right.elevation_m + (left.elevation_m - right.elevation_m) * t;
|
||
};
|
||
}
|
||
|
||
/** 사각 폴리곤(좌상 → 우상 → 우하 → 좌하). */
|
||
function rect(left: number, right: number, top: number, bottom: number): OffsetPoint[] {
|
||
return [
|
||
{ offset: left, elevation: top },
|
||
{ offset: right, elevation: top },
|
||
{ offset: right, elevation: bottom },
|
||
{ offset: left, elevation: bottom },
|
||
];
|
||
}
|
||
|
||
/** BOX암거 구체 기하. 제원·설계선·지반이 부족하면 null. */
|
||
export function computeBoxLayout(
|
||
section: CrossSection,
|
||
groundSamples: SectionSample[],
|
||
): BoxLayout | null {
|
||
const box = section.box;
|
||
if (!box) return null;
|
||
const edges = section.design?.road_edges;
|
||
if (!edges) return null;
|
||
const groundAt = groundInterpolator(groundSamples);
|
||
if (!groundAt) return null;
|
||
|
||
// 좌표 규약: +offset = 좌측. 유입 = 상단측(미상이면 좌측 폴백).
|
||
const inletOnLeft = (section.uphill_side ?? "left") === "left";
|
||
const roadTopAt = roadTopFactory(section, edges.left, edges.right);
|
||
const leftEdge = edges.left.offset_m;
|
||
const rightEdge = edges.right.offset_m;
|
||
if (!(leftEdge > rightEdge)) return null;
|
||
|
||
// 구체는 수평 설치(교본 3장) — 노면이 가장 낮은 자리에서도 복토를 확보한다.
|
||
const roadLow = Math.min(roadTopAt(leftEdge), roadTopAt(rightEdge), roadTopAt(0));
|
||
const topElevation = roadLow - box.cover_m;
|
||
const barrelTop = topElevation - box.top_thickness_m;
|
||
const barrelBottom = barrelTop - box.inner_height_m;
|
||
const bottomElevation = barrelBottom - box.slab_thickness_m;
|
||
|
||
// 저판 연장 — 유입·유출 날개벽 각도가 만든 투영량(백엔드 계산값).
|
||
const leftExtend = Math.max((inletOnLeft ? box.wing_in : box.wing_out).slab_extend_m, 0);
|
||
const rightExtend = Math.max((inletOnLeft ? box.wing_out : box.wing_in).slab_extend_m, 0);
|
||
const slabLeft = leftEdge + leftExtend;
|
||
const slabRight = rightEdge - rightExtend;
|
||
|
||
// 계류 하상과의 고저차 — 터파기·성토 판정은 후속이라 값만 넘긴다.
|
||
let bed = Math.min(groundAt(leftEdge), groundAt(rightEdge));
|
||
for (let offset = rightEdge; offset <= leftEdge; offset += 0.25) {
|
||
bed = Math.min(bed, groundAt(offset));
|
||
}
|
||
|
||
return {
|
||
box,
|
||
topSlab: rect(leftEdge, rightEdge, topElevation, barrelTop),
|
||
bottomSlab: rect(slabLeft, slabRight, barrelBottom, bottomElevation),
|
||
barrel: rect(leftEdge, rightEdge, barrelTop, barrelBottom),
|
||
topElevation,
|
||
bottomElevation,
|
||
slabLengthM: Math.abs(slabLeft - slabRight),
|
||
bedGapM: Number((bottomElevation - bed).toFixed(3)),
|
||
label: {
|
||
at: { offset: (leftEdge + rightEdge) / 2, elevation: (barrelTop + barrelBottom) / 2 },
|
||
text: `BOX ${box.inner_width_m.toFixed(1)}×${box.inner_height_m.toFixed(1)}`,
|
||
},
|
||
};
|
||
}
|