diff --git a/B06_Section/B06_Section_UI_Cross_Ford_Geom.ts b/B06_Section/B06_Section_UI_Cross_Ford_Geom.ts index c33ac5554..1d21de895 100644 --- a/B06_Section/B06_Section_UI_Cross_Ford_Geom.ts +++ b/B06_Section/B06_Section_UI_Cross_Ford_Geom.ts @@ -30,7 +30,7 @@ import { designInterpolator, groundInterpolator } from "./B06_Section_UI_Cross_C /** 측벽 한 매의 조작값 — 집수정 9키와 같은 축(높이·좌우·상하 대각). */ export interface FordWallAdjust { - /** 벽 높이(m, 노견 상단 ~ 바닥판 상면). null = 자동(바닥판이 계류 하상에 놓임). */ + /** 벽 높이(m, 노견 상단 ~ 바닥판 상면). null = 기본 = 관경 + 최소 토피(원지반 안 봄 · 107-2). */ heightM: number | null; /** 좌우(m, + = 노견 바깥). 안쪽으로는 못 간다. */ lateralM: number; @@ -103,7 +103,8 @@ export interface FordLayout { adjust: FordAdjust; } -/** 측벽 높이 하한 = 관경 + 최소 토피. 관이 노면 위로 솟지 않게 막는다. */ +/** 측벽 높이 하한 · 기본 = 관경 + 최소 토피 — 바닥 = 노면 − (관경 + 토피)(박스 「노면 − 복토」 꼴 · + * 원지반 안 봄 · 깊은 계류면 ▲▼ 로 내림 · 107-2). 관이 노면 위로 솟지 않게 막는다. */ function minWallHeight(ford: FordSet): number { return ford.diameter_m + ford.min_cover_m; } @@ -149,15 +150,6 @@ export function computeFordLayout( const inletOnLeft = uphill === "left"; const roadTopAt = roadTopFactory(section, edges.left, edges.right); - // 바닥판이 놓일 계류 하상 — 두 노견 사이 원지반 최저점. - const bedElevation = (() => { - let low = Math.min(groundAt(edges.left.offset_m), groundAt(edges.right.offset_m)); - for (let offset = edges.right.offset_m; offset <= edges.left.offset_m; offset += 0.25) { - low = Math.min(low, groundAt(offset)); - } - return low; - })(); - const buildSide = (role: FordWallRole): FordSideLayout | null => { const onLeft = role === "inlet" ? inletOnLeft : !inletOnLeft; const edge = onLeft ? edges.left : edges.right; @@ -166,8 +158,7 @@ export function computeFordLayout( const wing = role === "inlet" ? ford.wing_in : ford.wing_out; const wanted = role === "inlet" ? adjust.inlet : adjust.outlet; const lateralM = Math.max(wanted.lateralM, 0); - const autoHeight = roadTopAt(edge.offset_m) - (bedElevation + ford.slab_thickness_m); - const heightM = Math.max(wanted.heightM ?? autoHeight, minWallHeight(ford)); + const heightM = Math.max(wanted.heightM ?? minWallHeight(ford), minWallHeight(ford)); // 벽 상단이 노견에 붙도록 바닥판 상면을 역산한다. 상하 이동은 buildBasin이 얹는다. const anchor: OffsetPoint = { offset: edge.offset_m, diff --git a/resources/tester/helper_107_ford_free.cjs b/resources/tester/helper_107_ford_free.cjs index 59aef59ac..c8c132c23 100644 --- a/resources/tester/helper_107_ford_free.cjs +++ b/resources/tester/helper_107_ford_free.cjs @@ -69,4 +69,88 @@ bodies.ford.control.select(173.09, null); bodies.box.control.select(352.14, "left"); out.released = released; +// 107-2 — 원지반을 바꾼 사본: 세월교 바닥 = 노면 − (관경 + 토피) · 박스 본체 · 끝 · 날개 · 흙막이 자리 그대로 +const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts.ts")); +const fixture = JSON.parse( + fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"), +); +const sections = fixture.cross_sections; +const at = (c) => sections.find((x) => Math.abs(x.chainage_m - c) < 0.6); +const clone = (v) => JSON.parse(JSON.stringify(v)); +const r3 = (v) => Math.round(v * 1000) / 1000; +const pts = (list) => (list ?? []).map((p) => [r3(p.offset), r3(p.elevation)]); +/** 원지반 사본 — [from, to] 안 샘플을 dz 만큼 옮김. */ +const shiftGround = (x, dz, from = -Infinity, to = Infinity) => { + for (const p of x.samples) + if (p.offset_m >= from && p.offset_m <= to && p.elevation_m != null) p.elevation_m += dz; +}; +const GROUNDS = { + base: () => undefined, + bedDown2: (x) => shiftGround(x, -2, -2, 2), + allUp05: (x) => shiftGround(x, 0.5), + allDown1: (x) => shiftGround(x, -1), +}; +const walls = (culvert) => + (culvert?.walls ?? []).filter((w) => w.role !== "extra").map((w) => pts(w.points)); +function fordShape(base, edit, ground) { + const x = clone(base); + edit(x); + ground(x); + const { ford, culvert } = computeStoredLayouts( + x, + sections.map((y) => (y === base ? x : y)), + ); + return { + sides: ford.sides.map((side) => ({ + role: side.role, + parts: side.parts.map((part) => pts(part.points)), + slabTop: r3(side.slabTopElevation), + // 노면(벽 상단) − 바닥판 상면 = 관경 + 최소 토피 + depth: r3(side.top.elevation - side.slabTopElevation), + })), + pipeCover: r3(ford.ford.diameter_m + ford.ford.min_cover_m), + slab: pts(ford.slabBridge), + pipe: pts(ford.pipe.outer), + walls: walls(culvert), + }; +} +function boxShape(base, edit, ground) { + const x = clone(base); + edit(x); + ground(x); + const { box, culvert } = computeStoredLayouts( + x, + sections.map((y) => (y === base ? x : y)), + ); + return { + top: pts(box.topSlab), + bottom: pts(box.bottomSlab), + barrel: pts(box.barrel), + sides: box.sides.map((side) => ({ + role: side.role, + offset: r3(side.offset), + top: r3(side.topElevation), + bottom: r3(side.bottomElevation), + apron: pts(side.apron), + })), + walls: walls(culvert), + }; +} +const FORD = at(173.087); +const BOX = at(352.14); +const keep = () => undefined; +const inletWing = (x) => { + x.culvert.inlet = { role: "inlet", structure: "" }; +}; +// 날개벽 박스 — 흙막이 없이 양쪽 날개벽(에이프런 1.414 m) +const noGuard = (x) => { + delete x.culvert; + for (const wing of [x.box.wing_in, x.box.wing_out]) + Object.assign(wing, { installed: true, slab_extend_m: 1.414 }); +}; +const run = (shape, base, edit) => + Object.fromEntries(Object.entries(GROUNDS).map(([k, g]) => [k, shape(base, edit, g)])); +out.ford = { guards: run(fordShape, FORD, keep), inletWing: run(fordShape, FORD, inletWing) }; +out.box = { guards: run(boxShape, BOX, keep), bare: run(boxShape, BOX, noGuard) }; + process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/helper_91_ground_free.cjs b/resources/tester/helper_91_ground_free.cjs index 3a1e864a9..5cb2a0ec8 100644 --- a/resources/tester/helper_91_ground_free.cjs +++ b/resources/tester/helper_91_ground_free.cjs @@ -68,7 +68,7 @@ const layoutsOf = (s, base) => sections.map((x) => (x === base ? s : x)), ); -// ① 91-2 세월교 바닥 — 자동은 하상을 따라감 · 손 값(측벽 높이)은 원지반을 바꿔도 그대로 · 흙막이 기본 높이도 +// ① 91-2 · 107-2 세월교 바닥 — 기본 · 손 값(측벽 높이)은 원지반을 바꿔도 그대로 · 흙막이 기본 높이도 const FORD = at(173.087); function fordFloor(edit) { const s = clone(FORD); diff --git a/resources/tester/test_107_ford_free.py b/resources/tester/test_107_ford_free.py index 9fb762f2a..0872cb6eb 100644 --- a/resources/tester/test_107_ford_free.py +++ b/resources/tester/test_107_ford_free.py @@ -51,3 +51,24 @@ def test_벽을_고르면_세월교_박스_고름을_풂() -> None: # 카드 안 — 벽을 고르면 본체 창을 닫고 · 본체를 고르면 벽 창을 닫음 assert "bodies.clearSelection(); // 세월교 · 박스 고름을 풂" in view assert "if (activeRevet !== null) toggleRevet(activeRevet);" in view + + +@pytest.mark.parametrize("variant", ["guards", "inletWing"]) +def test_세월교_원지반을_바꿔도_바닥_그대로(out: dict, variant: str) -> None: + """107-2 — 바닥 = 노면 − (관경 + 최소 토피) · 하상 −2 m · 전체 +0.5 / −1 m 사본도 같은 모양.""" + runs = out["ford"][variant] + for name, shape in runs.items(): + assert shape == runs["base"], name + base = runs["base"] + assert all(side["depth"] == pytest.approx(base["pipeCover"]) for side in base["sides"]) + + +@pytest.mark.parametrize("variant", ["guards", "bare"]) +def test_박스_원지반을_바꿔도_본체_끝_날개_흙막이_그대로(out: dict, variant: str) -> None: + runs = out["box"][variant] + for name, shape in runs.items(): + assert shape == runs["base"], name + if variant == "bare": + assert all(side["apron"] for side in runs["base"]["sides"]) + else: + assert len(runs["base"]["walls"]) == 2 diff --git a/resources/tester/test_91_ground_free.py b/resources/tester/test_91_ground_free.py index 29d5db9de..821be3e19 100644 --- a/resources/tester/test_91_ground_free.py +++ b/resources/tester/test_91_ground_free.py @@ -29,9 +29,9 @@ def fit() -> dict: return json.loads(done.stdout) -def test_세월교_바닥_자동은_하상을_따라감(fit: dict) -> None: - for before, after in zip(fit["fordAuto"], fit["fordAutoBedDown"]): - assert after["slabTop"] < before["slabTop"] - 1.0 +def test_세월교_바닥_기본도_원지반을_바꿔도_그대로(fit: dict) -> None: + """107-2 — 기본 바닥 = 노면 − (관경 + 토피) · 하상을 안 따라감(옛 91-2 「자동은 하상」 뒤집음).""" + assert fit["fordAuto"] == fit["fordAutoBedDown"] def test_세월교_바닥_손_값은_원지반을_바꿔도_그대로(fit: dict) -> None: