Files
Pier_v1/tests/rebar-multi.test.ts
ypshimandClaude Opus 5.5 364b329c54 feat(rebar): 다주형 코핑 배근을 3주형형상배근.md 규칙으로 재작성
2주형 DWG 실측 규칙(§4–§8)을 생성기에 옮기고, 3D·삽도·철근표가 같은 조각을 쓴다.
- 전단 C9·C10·C11 (H22): 끝면 피복선 1개 + @300, 기둥 구간 8@300, 기둥 옆 밀집 @150
  (바깥 14 · 안쪽 16칸, 가운데 기둥 양쪽 16칸), 기둥 사이 ceil 등분. 번호는 그 자리 하면
  (Level C10 · 오목 C11 · 테이퍼·헌치 C9). 다리 8개 두 묶음(가운데 250), 폐합 4개, 위 끝 = 계단 상면 − c
- 상면 1단 C1(양끝 갈고리) · C4: 8 m 이음, 이음 중심 n등분점 ∓ l_s. 2단 C3 · C2 = 기둥 중심 + 4450까지,
  가운데 기둥 위 C3-1 · C2-1 (± 4450). 폭 27개(cycle-1 14 · cycle-2 13)
- 하면 C7: 전 길이, Level 꺾임점에서 철근을 따라 l_s/2씩 겹침, 양끝 위로 갈고리. C8: 경간마다 ± 2230 안쪽
- 측면 C12(상단 전 길이) · C13(하단, 하면에서 끊김) H16 @200, 다리 8개 위치마다
- C14 크로스타이 연직 600 · 교각축 1200 엇갈림, C15 끝단보다 100 이상 높은 계단 전체에 ⊓
- 경고 RB-M01(밀집 구간 겹침·코핑 끝 초과 → 위치를 합쳐 표시), M02, M03, M04, M05, M06, M08
- 다주형 기본값 피복 120 · 다리 8. 기둥 위치 입력 e_c · s_c 추가(Q-M10 미결, 없으면 L/5)
- §11 재현 테스트: 2주형 112위치(간격 문자열 일치), 3주형 173위치 · C9 78 · C10 41 · C11 54,
  C1/C4 8 m 이음 조각, C3 10324 · C2 9930 · C3-1/C2-1 8900, C7 5조각, C8 8540 × 26, C12 6단 36960,
  C13 4333/4400, RB-M05 위치

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 15:01:55 +09:00

117 lines
6.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry';
import { multicolCopingInfo, multicolShearLayout } from '../src/core/rebar/multicol-coping';
import { rebarSummary } from '../src/core/rebar/summary';
import { loadP1 } from './helpers';
// RAG/3주형형상배근.md §11 (2026-11-06): 2주형 DWG (L 24200) and the 3주형 합성 예제 (L 37200), B 3500, D 3000,
// h1 = h2 = 1500, 기둥 끝 여유 5600 · 간격 13000 (Q-M10 input), 피복 120, 다리 8개.
const dwg = (type: '2주형' | '3주형', L: number, extra = {}) => buildPier({ ...loadP1(), type, columnCount: type === '2주형' ? 2 : 3,
copingLengthMm: L, copingWidthMm: 3500, columnDiameterMm: 3000, copingH1Mm: 1500, copingH2Mm: 1500, columnEndOffsetMm: 5600, ...extra });
const two = dwg('2주형', 24200), three = dwg('3주형', 37200);
const info2 = multicolCopingInfo(two), info3 = multicolCopingInfo(three);
const named = (info: typeof info2, name: string) => info.bars.filter((b) => b.name === name);
/** Lengths of the pieces of one bar line (the first y of that name), in order along x. */
const line = (info: typeof info2, name: string) => {
const all = named(info, name);
const y = all[0].pts[1][2];
return all.filter((b) => b.pts.some((p) => Math.abs(p[2] - y) < 1e-9)).sort((a, b) => a.pts[0][0] - b.pts[0][0]).map((b) => b.length);
};
const near = (got: number[], want: number[], tol: number) => {
expect(got).toHaveLength(want.length);
got.forEach((g, i) => expect(Math.abs(g - want[i]), `#${i}: ${g} vs ${want[i]}`).toBeLessThanOrEqual(tol));
};
describe('다주형 코핑 배근 (3주형형상배근.md §11 재현)', () => {
it('column positions from e_c 5600 / s_c 13000 (Q-M10 input) and the multi-column defaults (피복 120, 다리 8)', () => {
expect(two.columns.map((c) => c.s)).toEqual([5.6, 18.6]);
expect(three.columns.map((c) => c.s).map((x) => Math.round(x * 1000) / 1000)).toEqual([5.6, 18.6, 31.6]);
expect(three.copingRebar).toMatchObject({ cover: 120, shearLegs: 8, spacingS: 300, sideSpacing: 200 });
});
it('2주형 shear: 112 positions, 120 · 130 · 7@300 · 14@150 · 8@300 · 16@150 · 19@300 · 16@150 · 8@300 · 14@150 · 7@300 · 130 · 120', () => {
const { positions } = multicolShearLayout(24.2, [5.6, 18.6], 0.12, 0.3, 0.15);
expect(positions).toHaveLength(112);
expect(positions[0]).toBeCloseTo(0.12, 9);
const gaps = positions.slice(1).map((x, i) => Math.round((x - positions[i]) * 10000) / 10);
const want = [130, ...Array(7).fill(300), ...Array(14).fill(150), ...Array(8).fill(300), ...Array(16).fill(150), ...Array(19).fill(300),
...Array(16).fill(150), ...Array(8).fill(300), ...Array(14).fill(150), ...Array(7).fill(300), 130];
expect(gaps).toEqual(want);
});
it('3주형 shear: 173 positions, spans 20@287.5, C9 78 · C10 41 · C11 54 (8 pieces per position: 4 closures × U + 삿갓)', () => {
const { positions } = multicolShearLayout(37.2, [5.6, 18.6, 31.6], 0.12, 0.3, 0.15);
expect(positions).toHaveLength(173);
const gaps = positions.slice(1).map((x, i) => Math.round((x - positions[i]) * 10000) / 10);
expect(gaps.filter((g) => g === 287.5)).toHaveLength(40);
expect([named(info3, 'C9').length, named(info3, 'C10').length, named(info3, 'C11').length].map((n) => n / 8)).toEqual([78, 41, 54]);
});
it('3주형 상면 1단: C1 7 pieces 4911.5 · 6805 × 5 · 7961.5, C4 7567.5 · 6805 × 5 · 4517.5 (8 m 이음), 14 / 13 bars', () => {
near(line(info3, 'C1'), [4911.5, 6805, 6805, 6805, 6805, 6805, 7961.5], 1);
near(line(info3, 'C4'), [7567.5, 6805, 6805, 6805, 6805, 6805, 4517.5], 1);
expect(named(info3, 'C1')).toHaveLength(98);
expect(named(info3, 'C4')).toHaveLength(91);
});
it('2주형 상면 1단: C1 5 pieces 4423.5 · 6317 × 3 · 7473.5', () => {
near(line(info2, 'C1'), [4423.5, 6317, 6317, 6317, 7473.5], 1);
});
it('상면 2단: C3 10324 × 2 · C2 9930 × 2 per bar, 가운데 기둥 위 C3-1 / C2-1 8900', () => {
near(line(info3, 'C3'), [10324, 10324], 1);
near(line(info3, 'C2'), [9930, 9930], 1);
near(line(info3, 'C3-1'), [8900], 1);
expect(named(info3, 'C3-1')).toHaveLength(14);
expect(named(info3, 'C2-1')).toHaveLength(13);
expect(named(info2, 'C3-1')).toHaveLength(0);
});
it('RB-M05: 8 m 이음 중심 in a column zone — 3주형 C4 17.485 · C1 19.715, 2주형 C4 6.437 · C1 17.763', () => {
const m3 = info3.warnings.find((w) => w.code === 'RB-M05')!.message;
expect(m3).toContain('C4 17.485');
expect(m3).toContain('C1 19.715');
const m2 = info2.warnings.find((w) => w.code === 'RB-M05')!.message;
expect(m2).toContain('C4 6.437');
expect(m2).toContain('C1 17.763');
});
it('하면 C7: 3주형 5 pieces 8690.5 / 11679 / 4917 / 11679 / 8690.5, 2주형 3 pieces (lap l_s/2 along the bar at the Level kinks)', () => {
// The outer piece follows the taper from the cover line: 7344 + 858.5 + 472 = 8674.5 (DWG 실측 경로 7360 → 8690.5).
near(line(info3, 'C7'), [8690.5, 11679, 4917, 11679, 8690.5], 20);
near(line(info2, 'C7'), [8690.5, 11679, 8690.5], 20);
expect(named(info3, 'C7')).toHaveLength(14 * 5);
});
it('하면 C8: 8540 per span (기둥 중심 ± 2230 안쪽), 13 × 2 = 26 for 3주형', () => {
expect(named(info3, 'C8')).toHaveLength(26);
expect(named(info3, 'C8').every((b) => Math.abs(b.length - 8540) < 1e-6)).toBe(true);
});
it('측면: C12 6 rows (z 1720 … 2720 above Level) at the 8 leg lines, 36960; C13 z 320 → 4330 / 4400 / 4330 (cut at each column)', () => {
const c12 = named(info3, 'C12');
expect([...new Set(c12.map((b) => Math.round((b.pts[0][1] - three.levelBottom) * 1000)))]).toEqual([1720, 1920, 2120, 2320, 2520, 2720]);
expect(c12).toHaveLength(6 * 8);
expect(c12.every((b) => Math.abs(b.length - 36960) < 1e-6)).toBe(true);
const low = named(info3, 'C13').filter((b) => Math.abs(b.pts[0][1] - three.levelBottom - 0.32) < 1e-9).map((b) => Math.round(b.length));
expect([...new Set(low)].sort()).toEqual([4333, 4400]);
expect(low).toHaveLength(3 * 8);
});
it('RB-M01: s_c 7000 — the dense zones of neighbouring columns overlap (warning; positions merged)', () => {
const short = multicolCopingInfo(dwg('3주형', 37200, { columnSpacingMm: 7000 }));
expect(short.warnings.map((w) => w.code)).toContain('RB-M01');
expect(info3.warnings.map((w) => w.code)).not.toContain('RB-M01');
});
it('철근표 rows reuse the pieces; the multi-column pier gets RB-M06 / RB-M08 notes; T형 none', () => {
const summary = rebarSummary(three);
for (const name of ['C1', 'C4', 'C3', 'C2', 'C3-1', 'C2-1', 'C7', 'C8', 'C9', 'C10', 'C11', 'C12', 'C13', 'C14']) {
const row = summary.rows.find((r) => r.name === name)!;
expect(row.count, name).toBe(named(info3, name).length);
}
expect(summary.warnings.map((w) => w.code)).toEqual(expect.arrayContaining(['RB-M05', 'RB-M06', 'RB-M08']));
expect(rebarSummary(buildPier(loadP1())).warnings.map((w) => w.code)).not.toContain('RB-M06');
});
});