feat(core,view): footing rebar B1–B12 (plan C5) and the ③ rebar screen v1

- Footing (확대기초철근 (1).md): grid counts from W and L, layer heights,
  B1–B4 and B5–B8 counts and lengths (B2 13646, B6 11396), B10-1/B10-2,
  B11 (60) and B12 (47) with their lengths. B9 is not computed yet.
- rebarSummary: one 철근표 per pier from the model (bearing, coping, column,
  footing), with alerts: RB-F08 (B2 needs a splice, Q-B6), RB-F09 (B9 not
  computed).
- ③ screen v1: stage nav button, member tabs (받침블럭 · 코핑 · 기둥 · 확대기초),
  rebar table with 번호 · 규격 · 개수 · 길이 · 비고, alert list, and the open
  questions (Q-B5, Q-B6, Q-C9). Drawings and the 강조 연동 are not in v1.
- Tests: footing values from the document; P1 summary totals; warnings.
- Not verified in a browser.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-22 11:33:47 +09:00
1 parent a403ab9641
commit 3af8729e9f
7 files changed
+464 -3

No files matched your search

+70
View File
@@ -0,0 +1,70 @@
import { describe, expect, it } from 'vitest';
import { assemblyBars, bottomBars, crossBars, grid, layers, legB2, sideBars } from '../src/core/rebar/footing';
// P1 footing: L 10400 × W 8150 × H 2000.
const L = 10400;
const W = 8150;
const H = 2000;
describe('확대기초 철근 (확대기초철근 (1).md, plan C5)', () => {
it('grid across W: n 61, G 7625, e 162.5, 62 positions (§3.3)', () => {
const g = grid(W);
expect(g.n).toBe(61);
expect(g.G).toBe(7625);
expect(g.e).toBeCloseTo(162.5, 6);
expect(g.positions).toBe(62);
});
it('배근도 재현 grid: W 7800 → n 58, e 175', () => {
const g = grid(7800);
expect(g.n).toBe(58);
expect(g.e).toBeCloseTo(175, 6);
});
it('layer heights: B5 1900, B1 1873, B2·B4 150, B6·B8 177, B3 250, B7 277', () => {
const z = layers(H);
expect(z.B5).toBe(1900);
expect(z.B1).toBeCloseTo(1873, 6);
expect(z.B24).toBe(150);
expect(z.B68).toBeCloseTo(177, 6);
expect(z.B3).toBe(250);
expect(z.B7).toBeCloseTo(277, 6);
});
it('B2 leg 1723, B2 length 13646 (over the 12000 bar, splice needed)', () => {
expect(legB2(H)).toBeCloseTo(1723, 6);
const b2 = bottomBars(L, W, H).find((b) => b.name === 'B2')!;
expect(b2.length).toBeCloseTo(13646, 0);
expect(b2.count).toBe(31);
});
it('B1–B4 counts 31 / 31 / 31 / 31 and B4 length 10200', () => {
const bars = bottomBars(L, W, H);
expect(bars.map((b) => b.count)).toEqual([31, 31, 31, 31]);
expect(bars.find((b) => b.name === 'B4')!.length).toBeCloseTo(10200, 6);
});
it('B5–B8 counts 40 each (grid along L: n 79), B8 length 7950, B6 length 11396', () => {
const bars = crossBars(L, W, H);
expect(bars.map((b) => b.count)).toEqual([40, 40, 40, 40]);
expect(bars.find((b) => b.name === 'B8')!.length).toBeCloseTo(7950, 6);
expect(bars.find((b) => b.name === 'B6')!.length).toBeCloseTo(11396, 0);
});
it('B10-1 18 bars ≈ 10748, B10-2 18 bars ≈ 8494', () => {
const [b101, b102] = sideBars(L, W);
expect(b101.count).toBe(18);
expect(b101.length).toBeCloseTo(10748, 0);
expect(b102.count).toBe(18);
expect(b102.length).toBeCloseTo(8494, 0);
});
it('B11 60 bars (20 / 20 / 20) ≈ 8359, B12 47 bars (16 / 15 / 16) ≈ 10613', () => {
const [b11, b12] = assemblyBars(L, W);
expect(b11.count).toBe(60);
// The document rounds the 90° hook to 246 mm; the formula gives 246.45, so B11 is 1.2 mm longer.
expect(Math.abs(b11.length - 8359)).toBeLessThan(1.5);
expect(b12.count).toBe(47);
expect(b12.length).toBeCloseTo(10613, 0);
});
});
+50
View File
@@ -0,0 +1,50 @@
import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry';
import { rebarSummary } from '../src/core/rebar/summary';
import { loadP1 } from './helpers';
const { rows, warnings } = rebarSummary(buildPier(loadP1()));
const count = (name: string) => rows.find((r) => r.name === name)!.count;
describe('③ 철근표 요약 (P1)', () => {
it('bearing blocks: S1 and S2 80 bars each', () => {
expect(count('S1')).toBe(80);
expect(count('S2')).toBe(80);
});
it('coping: C1–C8 12 each, C9 18, C10 9, C11 18, C12 5 rows, C13 2 rows, C14 38, C15 11, C16 22', () => {
for (const n of ['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8']) expect(count(n)).toBe(12);
expect(count('C9')).toBe(18);
expect(count('C10')).toBe(9);
expect(count('C11')).toBe(18);
expect(count('C12')).toBe(5);
expect(count('C13')).toBe(2);
expect(count('C14')).toBe(38);
expect(count('C15')).toBe(11);
expect(count('C16')).toBe(22);
});
it('column: P1 230, P2 230, couplers 368, P3 38, P5 19, P4 8, P6 32', () => {
expect(count('P1')).toBe(230);
expect(count('P2')).toBe(230);
expect(count('커플러')).toBe(368);
expect(count('P3')).toBe(38);
expect(count('P5')).toBe(19);
expect(count('P4')).toBe(8);
expect(count('P6')).toBe(32);
});
it('footing: B1–B4 31 each, B5–B8 40 each, B10-1 18, B10-2 18, B11 60, B12 47', () => {
for (const n of ['B1', 'B2', 'B3', 'B4']) expect(count(n)).toBe(31);
for (const n of ['B5', 'B6', 'B7', 'B8']) expect(count(n)).toBe(40);
expect(count('B10-1')).toBe(18);
expect(count('B10-2')).toBe(18);
expect(count('B11')).toBe(60);
expect(count('B12')).toBe(47);
});
it('warnings: B2 needs a splice (13646 > 12000), and B9 is not computed yet', () => {
expect(warnings.map((w) => w.code)).toEqual(expect.arrayContaining(['RB-F08', 'RB-F09']));
expect(rows.find((r) => r.name === 'B2')!.length!).toBeGreaterThan(12000);
});
});