From 93fe8876c695f7e02b9ca8d49b7e9a54aebdf1ef Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 8 Oct 2026 12:01:35 +0900 Subject: [PATCH] =?UTF-8?q?fix(B06):=20=EC=84=B8=EC=9B=94=EA=B5=90=20?= =?UTF-8?q?=EA=B4=80=20=EB=81=9D=20=EA=BC=AD=EC=A7=93=EC=A0=90=20=3D=20?= =?UTF-8?q?=EA=B7=B8=20=EB=86=92=EC=9D=B4=20=ED=9D=99=EB=A7=89=EC=9D=B4=20?= =?UTF-8?q?=EC=A0=84=EB=A9=B4=20=C2=B7=203D=20=EB=8F=84=20=EA=B0=99?= =?UTF-8?q?=EC=9D=80=20=EC=9E=90=EB=A6=AC=20=ED=95=A8=EC=88=98(89-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7 --- .../B05_Profile_UI_Corridor_Structures.ts | 20 ++++++++-------- .../B06_Section_UI_Cross_Body_Guard.ts | 24 +++++++++++-------- resources/tester/helper_80_ford_pipe.cjs | 9 +++++++ resources/tester/test_80_ford_pipe.py | 11 +++++++++ 4 files changed, 44 insertions(+), 20 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_Corridor_Structures.ts b/B05_Profile/B05_Profile_UI_Corridor_Structures.ts index 9ddf07da9..c85bb404a 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Structures.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Structures.ts @@ -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, diff --git a/B06_Section/B06_Section_UI_Cross_Body_Guard.ts b/B06_Section/B06_Section_UI_Cross_Body_Guard.ts index be2f43718..640af0277 100644 --- a/B06_Section/B06_Section_UI_Cross_Body_Guard.ts +++ b/B06_Section/B06_Section_UI_Cross_Body_Guard.ts @@ -210,8 +210,12 @@ export function withoutGuardedFill( 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( 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( 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); diff --git a/resources/tester/helper_80_ford_pipe.cjs b/resources/tester/helper_80_ford_pipe.cjs index 552e817d0..4957556cc 100644 --- a/resources/tester/helper_80_ford_pipe.cjs +++ b/resources/tester/helper_80_ford_pipe.cjs @@ -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), }; }), diff --git a/resources/tester/test_80_ford_pipe.py b/resources/tester/test_80_ford_pipe.py index eead57fc1..f101a302f 100644 --- a/resources/tester/test_80_ford_pipe.py +++ b/resources/tester/test_80_ford_pipe.py @@ -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)