Files
Aislo/B06_Section/B06_Section_UI_Cross_Box_Geom.ts
T
eomsangdonandClaude Opus 5 2fe8f617da fix(B06): BOX암거 성토선을 박스 최상단 모서리에서 끝냄
모서리 밖은 구체 개구부·날개벽이라 성토가 아니다(2026-08-25 사용자). 원지반까지
잇던 구간을 지운다 — 성토선은 노견 → (수평 연장) → 박스 최상단까지만이다.

검증(10+0.9): 좌 [(2, 535.821) → (2.744, 535.201)],
우 [(−2, 535.701) → 수평 → (−2.744, 535.201)] — 두 선 모두 상단 표고에서 종료.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 12:55:00 +09:00

159 lines
8.0 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각)만큼 계류 상·하류로 길어진다 — 세월교와 같은 산식.
* · 내공(유로)은 비워 둔다 — 관 위 성토 채움처럼 면을 칠하지 않는다.
* · **성토선은 박스 최상단 모서리에서 끝난다**(2026-08-25 사용자). 물매 1:1.2 고정이라
* 박스가 노견 밖으로 나간 만큼 노견을 수평 연장하고 거기서 사면을 시작한다 —
* 기슭막이 "벽이 밖으로 나간 만큼 노폭 연장" 규칙과 같다. 그래서 박스 길이를
* 늘리면 노견부가 저절로 길어지고 사면 각도는 그대로다.
* 부재 두께는 세월교 승계(측벽 0.2·상판 0.3·저판 0.3)이며 백엔드가 실어 보낸다.
* ========================================================================== */
import type { BoxSet, CrossSection, SectionSample } from "./B06_Section_Api_Fetch";
import type { CulvertDesignTrim, OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
import { FILL_SLOPE_RATIO_MIN } from "./B06_Section_UI_Cross_Culvert_Const";
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;
/**
* 양측 성토선 — 노견 → (박스가 나간 만큼 수평 연장) → 박스 최상단 모서리에서 끝.
* 물매 1:1.2 고정이며, 모서리 밖은 구체 개구부라 선을 잇지 않는다.
*/
fillLines: OffsetPoint[][];
label: { at: OffsetPoint; text: string };
/** 노견 밖 설계선은 이 성토선이 대신한다 — 노견에서 끊는다. */
designTrim: CulvertDesignTrim;
}
/** 노면 표고 — 설계선이 있으면 그 곡선, 없으면 노견 직선(세월교와 같은 규칙). */
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;
// 구체 길이 — 성토선이 박스 최상단을 지나려면 (노면 − 박스 상단) × 물매만큼은 노견
// 밖으로 나가야 한다. 노면이 크라운이라 좌·우 필요량이 다르므로 **큰 쪽에 맞춰**
// 대칭으로 잡고, 남는 쪽은 노견을 수평 연장해 물매 1:1.2를 지킨다(2026-08-25 사용자).
const reach =
Math.max(roadTopAt(leftEdge), roadTopAt(rightEdge)) - topElevation > 0
? (Math.max(roadTopAt(leftEdge), roadTopAt(rightEdge)) - topElevation) * FILL_SLOPE_RATIO_MIN
: box.cover_m * FILL_SLOPE_RATIO_MIN;
const bodyLeft = leftEdge + reach;
const bodyRight = rightEdge - reach;
// 저판 연장 — 유입·유출 날개벽 각도가 만든 투영량(백엔드 계산값).
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 = bodyLeft + leftExtend;
const slabRight = bodyRight - rightExtend;
// 계류 하상과의 고저차 — 터파기·성토 판정은 후속이라 값만 넘긴다.
let bed = Math.min(groundAt(leftEdge), groundAt(rightEdge));
for (let offset = rightEdge; offset <= leftEdge; offset += 0.25) {
bed = Math.min(bed, groundAt(offset));
}
// 성토선 — 노견 표고에서 수평으로 나간 뒤 박스 최상단 모서리를 지나 지반까지.
const fillLine = (edgeOffset: number, bodyOffset: number, outward: number): OffsetPoint[] => {
const roadZ = roadTopAt(edgeOffset);
// 사면이 박스 모서리를 지나려면 시작점이 모서리에서 (높이차 × 물매)만큼 안쪽이다.
const start = bodyOffset - outward * (roadZ - topElevation) * FILL_SLOPE_RATIO_MIN;
const corner: OffsetPoint = { offset: bodyOffset, elevation: topElevation };
const points: OffsetPoint[] = [{ offset: edgeOffset, elevation: roadZ }];
// 박스가 더 길면 노견을 수평으로 늘린다(각도 불변 — 2026-08-25 사용자).
if ((start - edgeOffset) * outward > 0.01) points.push({ offset: start, elevation: roadZ });
// 모서리에서 끝난다 — 그 밖은 구체 개구부·날개벽이라 성토가 아니다
// (2026-08-25 사용자: 박스와 만난 뒤의 성토선은 필요 없다).
points.push(corner);
return points;
};
return {
box,
topSlab: rect(bodyLeft, bodyRight, topElevation, barrelTop),
bottomSlab: rect(slabLeft, slabRight, barrelBottom, bottomElevation),
barrel: rect(bodyLeft, bodyRight, barrelTop, barrelBottom),
fillLines: [fillLine(leftEdge, bodyLeft, 1), fillLine(rightEdge, bodyRight, -1)],
designTrim: {
minOffset: rightEdge,
maxOffset: leftEdge,
minElevation: roadTopAt(rightEdge),
maxElevation: roadTopAt(leftEdge),
},
topElevation,
bottomElevation,
slabLengthM: Math.abs(slabLeft - slabRight),
bedGapM: Number((bottomElevation - bed).toFixed(3)),
label: {
at: { offset: (bodyLeft + bodyRight) / 2, elevation: (barrelTop + barrelBottom) / 2 },
text: `BOX ${box.inner_width_m.toFixed(1)}×${box.inner_height_m.toFixed(1)}`,
},
};
}