fix(B06): BOX암거 성토선 추가 — 박스 최상단을 지나고 노견은 수평 연장
박스 위 성토선이 빠져 있었다. 물매 1:1.2를 고정하고 성토선이 **박스 최상단 모서리**를 지나게 하면, 박스를 길게 잡을수록 노견이 저절로 수평 연장되고 각도는 그대로다(기슭막이 "벽이 밖으로 나간 만큼 노폭 연장"과 같은 규칙, 2026-08-25 사용자). - 구체 길이 = 노폭 + 2 × (노면 − 박스 상단) × 1.2. 노면이 크라운이라 좌·우 필요량이 다르므로 큰 쪽에 맞춰 대칭으로 잡고, 남는 쪽은 노견을 수평 연장한다 - 성토선 = 노견 → (수평 연장) → 박스 최상단 모서리 → 1:1.2로 원지반까지 - 노견 밖 설계선은 이 성토선이 대신하므로 다시 트림한다 검증(10+0.9): 구체 ±2.744m, 저판 8.32m, 좌 성토선 물매 1.200·1.200, 우 성토선 수평 0.144m 뒤 1.200·1.200. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
* 기하 계산은 `B06_Section_UI_Cross_Box_Geom.ts`가 맡는다(700줄 제한 분리).
|
||||
* 여기서는 계산 결과(`BoxLayout`)를 SVG 도형으로 옮기기만 한다 — 치수 결정 금지.
|
||||
*
|
||||
* 그리는 순서 = 시공 순서: ① 저판 → ② 상판 → ③ 내공 윤곽·라벨.
|
||||
* 그리는 순서 = 시공 순서: ① 저판 → ② 상판 → ③ 내공 윤곽 → ④ 성토선 → 라벨.
|
||||
* 콘크리트 색은 세월교 바닥판 클래스를 그대로 쓴다.
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -82,6 +82,21 @@ export function appendBoxOverlay(
|
||||
);
|
||||
layer.append(barrel);
|
||||
|
||||
// ④ 성토선 — 노견에서 (연장 후) 박스 최상단 모서리를 지나 원지반까지, 물매 1:1.2.
|
||||
for (const line of layout.fillLines) {
|
||||
if (line.length < 2) continue;
|
||||
const polyline = document.createElementNS(SVG_NS, "polyline");
|
||||
polyline.setAttribute("points", line.map((point) => toXY(point).join(",")).join(" "));
|
||||
polyline.setAttribute("class", "b06-chart__design-cross");
|
||||
const title = document.createElementNS(SVG_NS, "title");
|
||||
const height = line[0].elevation - line[line.length - 1].elevation;
|
||||
title.textContent =
|
||||
`BOX암거 성토선 — 물매 1:1.2 · 성토고 ${height.toFixed(2)}m` +
|
||||
" (박스 최상단 모서리를 지난다)";
|
||||
polyline.append(title);
|
||||
layer.append(polyline);
|
||||
}
|
||||
|
||||
const label = document.createElementNS(SVG_NS, "text");
|
||||
const [labelX, labelY] = toXY(layout.label.at);
|
||||
label.setAttribute("x", String(labelX));
|
||||
|
||||
@@ -8,13 +8,16 @@
|
||||
* · 상판 윗면 = **노면 − 복토 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 { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types";
|
||||
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 {
|
||||
@@ -32,7 +35,14 @@ export interface BoxLayout {
|
||||
slabLengthM: number;
|
||||
/** 구체 바닥과 계류 하상(원지반 최저)의 고저차(m). +면 구체가 하상 위로 떠 있다. */
|
||||
bedGapM: number;
|
||||
/**
|
||||
* 양측 성토선 — 노견 → (박스가 나간 만큼 수평 연장) → 박스 최상단 모서리 →
|
||||
* 1:1.2로 원지반까지. 물매는 고정이고 길이만 지형이 정한다.
|
||||
*/
|
||||
fillLines: OffsetPoint[][];
|
||||
label: { at: OffsetPoint; text: string };
|
||||
/** 노견 밖 설계선은 이 성토선이 대신한다 — 노견에서 끊는다. */
|
||||
designTrim: CulvertDesignTrim;
|
||||
}
|
||||
|
||||
/** 노면 표고 — 설계선이 있으면 그 곡선, 없으면 노견 직선(세월교와 같은 규칙). */
|
||||
@@ -87,11 +97,21 @@ export function computeBoxLayout(
|
||||
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 = leftEdge + leftExtend;
|
||||
const slabRight = rightEdge - rightExtend;
|
||||
const slabLeft = bodyLeft + leftExtend;
|
||||
const slabRight = bodyRight - rightExtend;
|
||||
|
||||
// 계류 하상과의 고저차 — 터파기·성토 판정은 후속이라 값만 넘긴다.
|
||||
let bed = Math.min(groundAt(leftEdge), groundAt(rightEdge));
|
||||
@@ -99,17 +119,48 @@ export function computeBoxLayout(
|
||||
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 });
|
||||
points.push(corner);
|
||||
// 모서리 밖은 같은 물매로 원지반과 만날 때까지.
|
||||
let toe = corner;
|
||||
for (let drop = 0.05; drop <= 30; drop += 0.05) {
|
||||
const probe: OffsetPoint = {
|
||||
offset: corner.offset + outward * drop * FILL_SLOPE_RATIO_MIN,
|
||||
elevation: corner.elevation - drop,
|
||||
};
|
||||
toe = probe;
|
||||
if (probe.elevation <= groundAt(probe.offset)) break;
|
||||
}
|
||||
points.push(toe);
|
||||
return points;
|
||||
};
|
||||
|
||||
return {
|
||||
box,
|
||||
topSlab: rect(leftEdge, rightEdge, topElevation, barrelTop),
|
||||
topSlab: rect(bodyLeft, bodyRight, topElevation, barrelTop),
|
||||
bottomSlab: rect(slabLeft, slabRight, barrelBottom, bottomElevation),
|
||||
barrel: rect(leftEdge, rightEdge, barrelTop, barrelBottom),
|
||||
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: (leftEdge + rightEdge) / 2, elevation: (barrelTop + barrelBottom) / 2 },
|
||||
at: { offset: (bodyLeft + bodyRight) / 2, elevation: (barrelTop + barrelBottom) / 2 },
|
||||
text: `BOX ${box.inner_width_m.toFixed(1)}×${box.inner_height_m.toFixed(1)}`,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -454,7 +454,7 @@ export function createCrossSectionCard(
|
||||
x,
|
||||
toDisplayY,
|
||||
drawSamples,
|
||||
culvertLayout?.designTrim ?? fordLayout?.designTrim ?? undefined,
|
||||
culvertLayout?.designTrim ?? fordLayout?.designTrim ?? boxLayout?.designTrim,
|
||||
);
|
||||
// 암 경계선 = 지면선 복사 + 오프셋(계획선 기준 아님).
|
||||
if (rockBoundary && section.design.geometry_preset === "rock") {
|
||||
|
||||
Reference in New Issue
Block a user