// 세월교 측벽 수직화(B06_Section_UI_Cross_Culvert_Basin `leanRatio`) 자체검증 — 로직 복제. // 2026-08-30 사용자 확정: 세월교 측벽은 수직(교본 그림 3-18). 1:0.3 기움은 ㄴ형 // 집수정 부재를 재사용하며 딸려 온 값이라 세월교에서만 끈다(집수정은 그대로). // 실행: node tmp/tests/test_ford_wall_vertical.mjs import assert from "node:assert/strict"; const REVET_LEAN_RATIO = 0.3; /* ── 복제: buildBasin의 벽·바닥판 좌표 산식 ─────────────────────────── */ function basinParts({ anchorOffset, anchorElevation, outward, wallHeight, memberT, floorT, innerWidthM, leanRatio }) { const lean = leanRatio ?? REVET_LEAN_RATIO; const defaultBottom = anchorElevation - floorT; const wallBase = anchorOffset + outward * (lean * wallHeight); const wallOf = (baseOffset, dir, bottomElevation = defaultBottom) => { const topShift = -dir * lean * wallHeight; const spanZ = wallHeight + floorT; const xAt = (elevation) => baseOffset + topShift * ((elevation - defaultBottom) / spanZ); return [ { offset: xAt(bottomElevation), elevation: bottomElevation }, { offset: baseOffset + topShift, elevation: anchorElevation + wallHeight }, { offset: baseOffset + topShift + dir * memberT, elevation: anchorElevation + wallHeight }, { offset: xAt(bottomElevation) + dir * memberT, elevation: bottomElevation }, ]; }; const wallOuterAt = (elevation) => { const bottom = anchorElevation - floorT; const t = (elevation - bottom) / (wallHeight + floorT); return wallBase + outward * memberT - outward * lean * wallHeight * t; }; const floorTopInner = wallOuterAt(anchorElevation); const floorOuter = floorTopInner + outward * (innerWidthM + memberT); const floorTaper = lean * floorT; return { wall: wallOf(wallBase, outward), floor: [ { offset: floorTopInner, elevation: anchorElevation }, { offset: floorOuter, elevation: anchorElevation }, { offset: floorOuter - outward * floorTaper, elevation: anchorElevation - floorT }, { offset: wallOuterAt(anchorElevation - floorT), elevation: anchorElevation - floorT }, ], trimOffset: wallBase - outward * lean * wallHeight, floorLengthM: Math.abs(floorOuter - floorTopInner), }; } const base = { anchorOffset: 3.0, // 노견 끝 anchorElevation: 100.0, // 바닥판 상면 outward: 1, // 좌측 wallHeight: 1.5, memberT: 0.3, // 벽 두께 floorT: 0.3, // 물받이 두께(교본 최소 0.3m) innerWidthM: 1.2, // 날개벽 투영 연장 }; /* ── 세월교(leanRatio 0) — 벽이 수직이어야 한다 ─────────────────────── */ const ford = basinParts({ ...base, leanRatio: 0 }); const [b0, t0, t1, b1] = ford.wall; assert.ok(Math.abs(t0.offset - b0.offset) < 1e-9, `벽 도로측 면이 안 섰다: ${t0.offset} vs ${b0.offset}`); assert.ok(Math.abs(t1.offset - b1.offset) < 1e-9, `벽 계류측 면이 안 섰다: ${t1.offset} vs ${b1.offset}`); // 벽은 노견 끝에서 그대로 내려간다 — 상단 자리(설계선 트림 경계)는 그대로. assert.ok(Math.abs(t0.offset - base.anchorOffset) < 1e-9); assert.ok(Math.abs(ford.trimOffset - base.anchorOffset) < 1e-9); // 두께는 유지. assert.ok(Math.abs(Math.abs(t1.offset - t0.offset) - base.memberT) < 1e-9); // 바닥판 taper 0 = 직사각형(위·아래 바깥 모서리가 같은 offset). assert.ok(Math.abs(ford.floor[1].offset - ford.floor[2].offset) < 1e-9, "바닥판 taper가 남았다"); assert.ok(Math.abs(ford.floor[0].offset - ford.floor[3].offset) < 1e-9, "바닥판 안쪽 변이 기울었다"); // 바닥 길이 = 날개벽 투영 연장 + 벽 두께(사용자 유지 지시). assert.ok( Math.abs(ford.floorLengthM - (base.innerWidthM + base.memberT)) < 1e-9, `바닥 길이 어긋남: ${ford.floorLengthM}`, ); /* ── 집수정(기본값) — 종전 1:0.3 기움 그대로여야 한다 ───────────────── */ const basin = basinParts(base); const lean = REVET_LEAN_RATIO * base.wallHeight; assert.ok( Math.abs(basin.wall[1].offset - basin.wall[0].offset - -lean) < 1e-9, "집수정 기움이 바뀌었다 — 집수정은 유지가 원칙", ); assert.ok( Math.abs(basin.floor[1].offset - basin.floor[2].offset) > 1e-9, "집수정 바닥판 taper가 사라졌다", ); // 집수정도 상단 도로측 꼭짓점은 노견 끝(트림 경계)에 있다 — 이 규칙은 공통. assert.ok(Math.abs(basin.trimOffset - base.anchorOffset) < 1e-9); /* ── 우측(outward -1)도 같은 규칙 ──────────────────────────────────── */ const right = basinParts({ ...base, outward: -1, leanRatio: 0 }); assert.ok(Math.abs(right.wall[1].offset - right.wall[0].offset) < 1e-9, "우측 벽이 안 섰다"); assert.ok(Math.abs(right.floorLengthM - (base.innerWidthM + base.memberT)) < 1e-9); console.log("test_ford_wall_vertical.mjs OK");