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 { stepBlockBars } from '../src/core/rebar/step-bars'; import { rebarSummary } from '../src/core/rebar/summary'; 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); }); }); describe('전·후열 단차 블록 보강 C17 · C18 (결정 2026-11-10)', () => { const raised = (dz: number, extra: Partial = {}) => { const p1 = loadP1(); return buildPier({ ...p1, ...extra, bearings: p1.bearings.map((b) => (b.row === '후열' && b.no === 7 ? { ...b, z: b.z + dz, seatEl: b.seatEl! + dz } : b)) }); }; const opts = (m: ReturnType) => ({ cover: 100, d: 15.9, ld: 600, tieSpacing: m.copingRebar.spacingS, skinSpacing: m.copingRebar.sideSpacing }); it('a 400 mm step: C17 ㄷ ties across the higher (back) block @전단간격, legs l_d below the lower top; one C18 row', () => { const m = raised(0.4); const bars = stepBlockBars(m, opts(m)); const c17 = bars.filter((b) => b.name === 'C17'), c18 = bars.filter((b) => b.name === 'C18'); expect(c17.length).toBe(10); const [, back] = m.copingParts.filter((p) => p.k === 0); const hi = m.backSegments![1].top, lo = m.segments[1].top; for (const b of c17) { for (const p of b.pts) { expect(p[2]).toBeGreaterThanOrEqual(back.nFrom - 1e-9); expect(p[2]).toBeLessThanOrEqual(back.nTo + 1e-9); } expect(b.pts[1][1]).toBeCloseTo(hi - 0.1, 9); expect(b.pts[0][1]).toBeCloseTo(lo - 0.6, 9); expect(b.pts[0][0]).toBeGreaterThan(2.196 + 0.1 - 1e-6); expect(b.pts[0][0]).toBeLessThan(4.701 - 0.1 + 1e-6); } expect(Math.abs(c17[1].pts[0][0] - c17[0].pts[0][0])).toBeCloseTo(0.25, 9); expect(c18.length).toBe(1); expect(c18[0].pts[0][1]).toBeCloseTo(lo + 0.25, 9); }); it('below 300 mm no block bars', () => { const m = raised(0.25); expect(stepBlockBars(m, opts(m))).toEqual([]); }); it('they reach the T형 철근표 and the multi-column bars', () => { const rows = rebarSummary(raised(0.4)).rows; expect(rows.find((r) => r.name === 'C17')!.count).toBe(10); expect(rows.find((r) => r.name === 'C18')!.count).toBe(1); const multi = multicolCopingInfo(raised(0.4, { type: '2주형', columnCount: 2, foundationType: '말뚝기초', groundEl: 30, pileLengthMm: 24000 })); expect(multi.bars.some((b) => b.name === 'C17')).toBe(true); expect(multi.bars.some((b) => b.name === 'C18')).toBe(true); }); });