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>
117 lines
6.9 KiB
TypeScript
117 lines
6.9 KiB
TypeScript
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');
|
||
});
|
||
});
|