Files
Aislo/B06_Section/B06_Section_UI_Cross_Box_Geom.ts
T
eomsangdonandClaude Opus 5 d4c8de738a feat(B05·B06): BOX암거 횡단도 · 3D 중공 셸·날개벽·관통 보어
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>
2026-08-25 12:45:32 +09:00

117 lines
5.4 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_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)}`,
},
};
}