전열·후열 상면 차가 300 mm 이상인 구간의 높은 쪽 블록에 (결정 2026-11-10, step-bars.ts): - C17 ㄷ형 띠 H16 @전단간격: 단차면 + c ~ 측면 − c, 위 = 높은 상면 − c, 다리 낮은 상면 아래 l_d까지 - C18 수평 피부철근 H16 @측면간격: 단차면 안쪽, 낮은 상면 + 측면간격마다 T형(철근표·3D)과 다주형(편 좌표 → 꺾임 접기) 모두에 넣는다. 구간마다 따로 두고 이웃 구간과 잇지 않는다 (가정). Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
130 lines
6.3 KiB
TypeScript
130 lines
6.3 KiB
TypeScript
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<PierInput> = {}) => {
|
||
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<typeof buildPier>) => ({ 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);
|
||
});
|
||
});
|