import { describe, expect, it } from 'vitest'; import { buildPier } from '../src/core/geometry'; import { tStepBars } from '../src/core/rebar/coping'; import { multicolCopingInfo } from '../src/core/rebar/multicol-coping'; import type { PierInput } from '../src/core/types'; import { loadP1 } from './helpers'; // 계단부 C15 · C16 일반 규칙 (결정 2026-11-10, src/core/rebar/step-bars.ts). P1 문서값은 rebar-stairs.test.ts. /** P1 with the 교좌면 EL (and z with it) of bearings 1–5 / 6–10 set to `seats` (m), both rows alike. */ function withSeats(seats: number[]): PierInput { const p1 = loadP1(); return { ...p1, bearings: p1.bearings.map((b) => { const s = seats[(b.no - 1) % 5]; return { ...b, z: b.z - b.seatEl! + s, seatEl: s }; }), }; } const SEAT = [43.411, 43.483, 43.555, 43.626, 43.698]; describe('계단부 철근 일반 규칙', () => { it('steps that fall towards the end: C15 on the first step (leg at the coping start + c), C16 legs at the low (end) side', () => { const m = buildPier(withSeats([...SEAT].reverse())); const bars = tStepBars(m).bars; const c15 = bars.filter((b) => b.name === 'C15'); const c16 = bars.filter((b) => b.name === 'C16'); expect(c15.length).toBe(11); expect(c15[0].x0).toBe(100); expect(c16.length).toBe(22); expect(c16.every((b) => b.legs === 'end')).toBe(true); // Mirror of P1: the run reaches l_d into the higher (start-side) step. expect(c16.map((b) => b.length).sort()).toEqual([...Array(11).fill(4069), ...Array(11).fill(4070)].sort()); }); it('every bar stays inside the coping length (the P1 positions no longer leak past a shorter coping)', () => { const m = buildPier({ ...withSeats([...SEAT].reverse()), copingLengthMm: 12000 }); const L = m.copingL * 1000; for (const b of tStepBars(m).bars) { expect(b.x0).toBeGreaterThanOrEqual(100); expect(b.x1).toBeLessThanOrEqual(L - 100); } }); it('no step bar when no step is h_16 (100 mm) above the lowest top', () => { const m = buildPier(withSeats([43.411, 43.45, 43.48, 43.5, 43.51])); expect(tStepBars(m).bars).toEqual([]); }); it('a step with both neighbours higher gets no bar and a warning (RB-S01 규칙 밖)', () => { const m = buildPier(withSeats([43.698, 43.561, 43.711, 43.411, 43.411])); const { bars, warnings } = tStepBars(m); expect(warnings).toHaveLength(1); expect(warnings[0]).toContain('양쪽이 더 높아'); // The two peaks each get C15; the valley step none. expect(bars.filter((b) => b.name === 'C15').length).toBe(22); expect(bars.filter((b) => b.name === 'C16')).toEqual([]); }); it('with a lateral step, front and back rows are separate and stay in their own part of the width', () => { const p1 = loadP1(); const bearings = p1.bearings.map((b) => (b.row === '후열' && b.no === 7 ? { ...b, z: b.z + 0.3, seatEl: b.seatEl! + 0.3 } : b)); const m = buildPier({ ...p1, bearings }); const [front, back] = m.copingParts.filter((p) => p.k === 0); const bars = tStepBars(m).bars; expect(new Set(bars.map((b) => b.row))).toEqual(new Set(['전열', '후열'])); for (const b of bars) { const part = b.row === '전열' ? front : back; expect(b.n).toBeGreaterThanOrEqual(part.nFrom - 1e-9); expect(b.n).toBeLessThanOrEqual(part.nTo + 1e-9); } // The raised back segment 2 (⑦) is a peak: C15 there, on the back row only. const backC15 = bars.filter((b) => b.row === '후열' && b.name === 'C15'); expect(new Set(backC15.map((b) => b.x0))).toEqual(new Set([2296, 9812])); }); it('multi-column copings use the same rule: a peak gets C15, a lower step C16 (no more RB-M04)', () => { const m = buildPier({ ...withSeats(SEAT), type: '2주형', columnCount: 2, foundationType: '말뚝기초', groundEl: 30, pileLengthMm: 24000 }); const info = multicolCopingInfo(m); const names = info.bars.map((b) => b.name); expect(names).toContain('C15'); expect(names).toContain('C16'); expect(info.warnings.some((w) => w.code === 'RB-M04')).toBe(false); }); });