fix(B06): 세월교 관 끝 꼭짓점 = 그 높이 흙막이 전면 · 3D 도 같은 자리 함수(89-1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
This commit is contained in:
2026-10-08 12:01:35 +09:00
co-authored by Claude Opus 5.5
parent e25b9ddf2f
commit 93fe8876c6
4 changed files with 44 additions and 20 deletions
@@ -49,6 +49,7 @@ import {
type WallPath,
} from "./B05_Profile_UI_Corridor_Structures_Path";
import { computeRevetmentLayout } from "../B06_Section/B06_Section_UI_Cross_Revetment";
import { withoutGuardedFill } from "../B06_Section/B06_Section_UI_Cross_Body_Guard";
/** 스윕 프레임 하나 — 노선 위 한 지점의 중심 XY·좌향 단위벡터와 종단 표고 오프셋. */
export interface StructureFrame {
@@ -274,7 +275,8 @@ export function buildCorridorStructures(
const solids: CorridorStructure[] = [];
for (const section of crossSections) {
const layout = culvertLayoutOf(section);
const fordLayout = fordLayoutOf(section);
// 흙막이가 선 쪽은 횡단과 같은 자리 함수로 — 측벽 걷고 관 · 바닥판이 흙막이까지(89-1)
const fordLayout = withoutGuardedFill(fordLayoutOf(section), layout);
// 독립 기슭막이(옛 D군 경로) — 측점에 배관/숨김 기슭막이 세트가 붙으면 그쪽(위
// culvertLayoutOf)이 그린다. 아직 이관 안 된 정본만 이 옛 경로로 스윕한다
// (2026-08-28 이관 이중그리기 방지 — 횡단도 가드와 같은 규칙).
@@ -449,12 +451,11 @@ export function buildCorridorStructures(
if (fordLayout) {
// 구체는 월류 폭만큼 도로 방향으로 이어지고 기준 측점 전후로 절반씩 걸친다.
const half = Math.max(fordLayout.ford.span_m, 0.5) / 2;
const invertPoints = fordLayout.pipe.outer;
const fordBore = pipeBore(
fordLayout.ford.diameter_m,
{ offset: invertPoints[3].offset, elevation: invertPoints[3].elevation },
{ offset: invertPoints[2].offset, elevation: invertPoints[2].elevation },
);
// 관 끝 = 윗 꼭짓점 자리(흙막이 사면에서 안 튀어나옴 · 89-1) · 높이 = invert
const [topLeft, topRight, invertRight, invertLeft] = fordLayout.pipe.outer;
const pipeStart = { offset: topLeft.offset, elevation: invertLeft.elevation };
const pipeEnd = { offset: topRight.offset, elevation: invertRight.elevation };
const fordBore = pipeBore(fordLayout.ford.diameter_m, pipeStart, pipeEnd);
// 관은 련수만큼 폭 안에 등간격 — 구멍도 관마다 하나씩 낸다(2026-08-25 사용자 ⑥).
const count = Math.max(fordLayout.ford.pipe_count, 1);
const pipeChainages = Array.from(
@@ -518,7 +519,6 @@ export function buildCorridorStructures(
),
);
// 관은 련수만큼 폭 안에 등간격으로 놓는다(단면엔 1개만 보이지만 실물은 여러 련).
const invert = fordLayout.pipe.outer;
for (const at of pipeChainages) {
const frame = frameAt(at);
solids.push({
@@ -526,8 +526,8 @@ export function buildCorridorStructures(
kind: "pipe",
key: "pipe",
pipe: {
start: [invert[3].offset, invert[3].elevation],
end: [invert[2].offset, invert[2].elevation],
start: [pipeStart.offset, pipeStart.elevation],
end: [pipeEnd.offset, pipeEnd.elevation],
diameterM: fordLayout.ford.diameter_m,
wallThicknessM: pipeWallThicknessM(
fordLayout.ford.pipe_kind,
+14 -10
View File
@@ -210,8 +210,12 @@ export function withoutGuardedFill<T extends BoxLayout | FordLayout>(
if (!layout || !culvert?.culvert.body) return layout;
const guarded = (outward: number): boolean => culvert.walls.some((w) => w.outward === outward);
const mid = layout.label.at.offset;
const reach = (points: OffsetPoint[], outward: number, offset: number): OffsetPoint[] =>
points.map((p) => ((p.offset - mid) * outward > 0 ? { ...p, offset } : p));
const reach = (
points: OffsetPoint[],
outward: number,
offset: (p: OffsetPoint) => number,
): OffsetPoint[] =>
points.map((p) => ((p.offset - mid) * outward > 0 ? { ...p, offset: offset(p) } : p));
if ("box" in layout && "topSlab" in layout) {
const sides = layout.sides.map((side) =>
guarded(side.role === "left" ? 1 : -1)
@@ -226,7 +230,7 @@ export function withoutGuardedFill<T extends BoxLayout | FordLayout>(
const to = wall?.bottomBack.offset ?? side.offset;
if ((to - side.offset) * outward < 1e-6) continue;
[topSlab, bottomSlab, barrel] = [topSlab, bottomSlab, barrel].map((p) =>
reach(p, outward, to),
reach(p, outward, () => to),
);
}
const run =
@@ -237,19 +241,19 @@ export function withoutGuardedFill<T extends BoxLayout | FordLayout>(
const sides = ford.sides.map((side) =>
guarded(side.outward) ? { ...side, parts: [], fillSegments: [], cutLine: null } : side,
);
// 관 · 바닥판이 흙막이까지 따라감 — 관 끝 = 흙막이 전면(관 invert 높이) · 바닥판 끝 = 배면(80)
// 관 · 바닥판이 흙막이까지 따라감 — 관 끝 꼭짓점 = 그 높이의 흙막이 전면(사면 이용 · 89-1) ·
// 아래 꼭짓점(invert)은 종전 자리라 1 m 맞춤 그대로 · 바닥판 끝 = 배면(80)
let { pipe, slabBridge } = ford;
for (const side of ford.sides) {
const wall = culvert.walls.find((w) => w.role === side.role && w.outward === side.outward);
if (!wall) continue;
const front =
wall.points[2].offset +
wall.outward * wall.lean * (wall.topJoint.elevation - side.slabTopElevation);
const faceAt = (p: OffsetPoint): number =>
wall.points[2].offset + wall.outward * wall.lean * (wall.topJoint.elevation - p.elevation);
pipe = {
outer: reach(pipe.outer, side.outward, front),
inner: reach(pipe.inner, side.outward, front),
outer: reach(pipe.outer, side.outward, faceAt),
inner: reach(pipe.inner, side.outward, faceAt),
};
slabBridge = reach(slabBridge, side.outward, wall.bottomBack.offset);
slabBridge = reach(slabBridge, side.outward, () => wall.bottomBack.offset);
}
if (pipe === ford.pipe) return { ...ford, sides } as T;
const [right, left] = pipe.outer.slice(2);
+9
View File
@@ -65,6 +65,8 @@ function pipeFit(edit) {
const { ford, culvert } = computeStoredLayouts(section, list);
const [right, left] = ford.pipe.outer.slice(2);
const ends = { 1: left.offset, [-1]: right.offset };
const [topLeft, topRight] = ford.pipe.outer;
const tops = { 1: topLeft, [-1]: topRight };
const slab = ford.slabBridge.map((p) => p.offset);
const slabEnd = { 1: Math.max(...slab), [-1]: Math.min(...slab) };
return {
@@ -80,6 +82,13 @@ function pipeFit(edit) {
role: side.role,
guard: true,
pipeGap: r3(ends[side.outward] - front),
// 관 윗모서리 ↔ 그 높이의 흙막이 전면(사면 이용 · 89-1)
topGap: r3(
tops[side.outward].offset -
(wall.points[2].offset +
wall.outward * wall.lean * (wall.topJoint.elevation - tops[side.outward].elevation)),
),
lean: wall.lean,
slabGap: r3(slabEnd[side.outward] - wall.bottomBack.offset),
};
}),
+11
View File
@@ -47,6 +47,17 @@ def test_관은_흙막이_전면까지_바닥판은_배면까지(fit: dict) -> N
assert side["slabGap"] == pytest.approx(0, abs=1e-3)
def test_관_윗모서리도_흙막이_전면_사면에(fit: dict) -> None:
"""89-1 — 흙막이 사면 이용: 윗모서리가 그 높이 전면 자리라 벽 밖으로 안 튀어나감."""
leaned = 0
for case in (fit[k] for k in ("both", "inletPlus1", "outletPlus2", "inletOnly")):
for side in case["sides"]:
if side["guard"]:
assert side["topGap"] == pytest.approx(0, abs=1e-3)
leaned += side["lean"] > 0
assert leaned
def test_흙막이를_옮기면_관이_그만큼_따라감(fit: dict) -> None:
base = fit["both"]["lengthM"]
assert fit["inletPlus1"]["lengthM"] == pytest.approx(base + 1, abs=1e-3)