feat(rebar): add downward 90° hooks (135 mm) to P6 crossties in 3D rebar model

This commit is contained in:
ypshim committed 2026-10-22 17:45:22 +09:00
1 parent 4aa8dbb3cb
commit a2317caf6c
15 files changed
+740 -133

No files matched your search

+18 -7
View File
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry';
import { barsAcross, bearingBlockRebar, blockBars, totals } from '../src/core/rebar/bearing-block';
import { rebarSummary } from '../src/core/rebar/summary';
import { loadP1 } from './helpers';
describe('받침블럭 철근 (spec §6.3, plan C2)', () => {
@@ -16,13 +17,14 @@ describe('받침블럭 철근 (spec §6.3, plan C2)', () => {
for (const bar of blockBars(150)) expect(bar.length).toBe(2500);
});
it('coping embedment: S2 600 (front) / 550 (back), S1 619.1 (front) / 569.1 (back), as in the spec', () => {
const front = blockBars(150);
const back = blockBars(200);
expect(front.find((b) => b.name === 'S2')!.embedment).toBeCloseTo(600, 6);
expect(back.find((b) => b.name === 'S2')!.embedment).toBeCloseTo(550, 6);
expect(front.find((b) => b.name === 'S1')!.embedment).toBeCloseTo(619.1, 6);
expect(back.find((b) => b.name === 'S1')!.embedment).toBeCloseTo(569.1, 6);
it('coping embedment from the model block height (block top − coping top: front 100, back 150): S2 650 / 600, S1 669.1 / 619.1', () => {
const m = buildPier(loadP1());
const rebar = bearingBlockRebar(m);
const emb = (row: '전열' | '후열', name: 'S1' | 'S2') => rebar.find((b) => b.row === row)!.bars.find((r) => r.name === name)!.embedment;
expect(emb('전열', 'S2')).toBeCloseTo(650, 6);
expect(emb('후열', 'S2')).toBeCloseTo(600, 6);
expect(emb('전열', 'S1')).toBeCloseTo(669.1, 6);
expect(emb('후열', 'S1')).toBeCloseTo(619.1, 6);
});
it('P1: 10 blocks, 80 bars each of S1 and S2 (spec table: 8 本 per block)', () => {
@@ -47,3 +49,12 @@ describe('받침블럭 도면 재현 (spec §6.3)', () => {
expect(across.firstEnd).toBeCloseTo(162.5, 6); // 50 + 75 + 37.5, printed as 50 + 113 in the drawing
});
});
describe('받침블럭 묻힘 검토 (spec §9 RB-S04)', () => {
it('P1: embedments 619–669 mm are shorter than the H19 development length 882, so RB-S04 is raised and the rows show the range', () => {
const { rows, warnings } = rebarSummary(buildPier(loadP1()));
expect(warnings.some((w) => w.code === 'RB-S04')).toBe(true);
expect(rows.find((r) => r.name === 'S1')!.note).toContain('묻힘 619–669');
expect(rows.find((r) => r.name === 'S2')!.note).toContain('묻힘 600–650');
});
});
+16
View File
@@ -84,3 +84,19 @@ describe('RB-C07 P4 삭제 규칙 (기둥철근.md §6.2)', () => {
expect(r.kept).toEqual([-60]);
});
});
describe('기둥철근 P1·P2 마디와 바닥 (기둥철근.md §3.2)', () => {
it('bottom at −1697.5 (−H_f + z_B7 + (d_B7 + d_P1)/2), P1 pieces 4697.5 + 525 / 3000 ×3 / 3147', () => {
expect(c.zBot).toBeCloseTo(-1697.55, 1);
expect(c.p1Hook).toBe(525);
expect(c.p1Pieces.map((v) => Math.round(v))).toEqual([5223, 3000, 3000, 3000, 3147]);
});
it('P2 pieces 4847.5 / 3000 ×3 / 2389', () => {
expect(c.p2Pieces.map((v) => Math.round(v))).toEqual([4848, 3000, 3000, 3000, 2389]);
});
it('P6 hook 135 each, so the length is 2071 (1800.9 + 2 × 135)', () => {
expect(c.p6Length).toBeCloseTo(2070.9, 1);
});
});
+12 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { c14Total, COPING_END_GAP, sideRows, shearBars, STEP_BAR_COUNTS } from '../src/core/rebar/coping';
import { c14Total, COPING_END_GAP, copingSideBars, sideRows, shearBars, STEP_BAR_COUNTS } from '../src/core/rebar/coping';
// P1 coping: L 12000, one column at the centre (T형).
const L = 12000;
@@ -43,6 +43,17 @@ describe('코핑 철근 (코핑철근.md, plan C3)', () => {
expect(r.c13).toEqual([42.538, 42.788]);
});
it('C12 일자 lengths 3717 / 5333 / 6950 / 8567 / 10183 and C13 11746 for L 12000 (코핑철근.md §6.2 table)', () => {
const s = copingSideBars(12000, 41.288, 42.538);
const expected = [3717, 5333, 6950, 8567, 10183];
s.c12.forEach((r, i) => expect(r.straight).toBeCloseTo(expected[i], 0));
expect(s.c12[0].xw).toBeCloseTo(4141.7, 1);
s.c13.forEach((r) => {
expect(r.xw).toBe(127);
expect(r.straight).toBe(11746);
});
});
it('C14: 38 bars = 2 × 11 upper + 5 + 6 + 3 + 2 lower', () => {
expect(c14Total()).toBe(38);
});
+24 -12
View File
@@ -52,32 +52,44 @@ describe('확대기초 철근 (확대기초철근 (1).md, plan C5)', () => {
});
it('B10-1 18 bars ≈ 10748, B10-2 18 bars ≈ 8494', () => {
const [b101, b102] = sideBars(L, W);
const [b101, b102] = sideBars(L, W, H);
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);
it('B10 levels from the height: H 2000 → m 8 (9 levels), H 2200 → m 9 (10 levels), as in the document', () => {
expect(b10Levels(2000)).toBe(9);
expect(b10Levels(2200)).toBe(10);
expect(assemblyJs(2000)).toEqual([2, 4, 6]);
expect(assemblyJs(2200)).toEqual([2, 4, 6, 8]);
});
it('B9 shear: 16 rows, 20 per row, 320 in all, length ≈ 2128 (l_v 1750 + 2 × 189)', () => {
const b9 = shearB9(L, W, H);
it('B11 60 bars (20 / 20 / 20) ≈ 8359, B12 47 bars (16 / 15 / 16) ≈ 10613', () => {
const [b11, b12] = assemblyBars(L, W, H);
expect(b11.count).toBe(60);
expect(b11.length).toBeCloseTo(8359, 0);
expect(b12.count).toBe(47);
expect(b12.length).toBeCloseTo(10612.5, 1); // the document prints ≈ 10613
});
it('B9 shear: 16 rows, 20 per row on the grid, 320 in all, length ≈ 2128 (l_v 1750 + 2 × 189)', () => {
const b9 = shearB9(L, W, H, []);
expect(b9.rows).toBe(16);
expect(b9.perRow.every((c) => c === 20)).toBe(true);
expect(b9.count).toBe(320);
expect(b9.length).toBeCloseTo(2128, 0);
});
it('B9 column zone: bars whose centre is within D/2 + d/2 of a column axis are left out (P1: 14 bars)', () => {
const b9 = shearB9(L, W, H, [0], 2000);
expect(b9.count).toBe(306);
for (const b of b9.bars) expect(Math.hypot(b.x, b.n)).toBeGreaterThan(1000 + 7.95);
});
});
import { assemblyHeights, legOffsets } from '../src/core/rebar/footing';
import { assemblyHeights, assemblyJs, b10Levels, legOffsets } from '../src/core/rebar/footing';
describe('확대기초 배근도 재현 (확대기초철근 (1).md §13)', () => {
it('leg offsets: B1 +25.4, B3 +22.25, B5 +28.6, B7 +23.85', () => {
@@ -89,7 +101,7 @@ describe('확대기초 배근도 재현 (확대기초철근 (1).md §13)', () =>
});
it('B11 at 550 / 950 / 1350 and B12 at 569.1 / 969.1 / 1369.1', () => {
const h = assemblyHeights();
const h = assemblyHeights(H);
expect(h.B11).toEqual([550, 950, 1350]);
expect(h.B12.map((z) => Math.round(z * 10) / 10)).toEqual([569.1, 969.1, 1369.1]);
});
+67
View File
@@ -0,0 +1,67 @@
import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry';
import { c14Bars, c14Length, copingSideBars, stairBars, undersideAt } from '../src/core/rebar/coping';
import { loadP1 } from './helpers';
const m = buildPier(loadP1());
const L = m.copingL * 1000;
const B = m.copingB * 1000;
const side = copingSideBars(L, 41.288, 42.538);
const rows = [...side.c12, ...side.c13];
const under = (x: number) => undersideAt(L, x, m.endBottom, m.levelBottom);
describe('C14 / C15 / C16 (코핑철근.md §7–8)', () => {
it('C14: 2 upper lines of 11 (C2, C3) and lower rows 5 / 6 / 3 / 2 = 38', () => {
const bars = c14Bars(L, m.copingRefTop, rows, under);
const count = (line: number) => bars.filter((b) => b.line === line).length;
expect(count(1)).toBe(11);
expect(count(2)).toBe(11);
expect([3, 4, 5, 6].map(count)).toEqual([5, 6, 3, 2]);
expect(bars.length).toBe(38);
});
it('C14 heights: upper 43.251 / 43.194, lower 42.810 / 42.310 / 41.810 / 41.310', () => {
const bars = c14Bars(L, m.copingRefTop, rows, under);
const z = (line: number) => bars.find((b) => b.line === line)!.z;
expect(z(1)).toBeCloseTo(43.251, 3);
expect(z(2)).toBeCloseTo(43.194, 3);
expect([3, 4, 5, 6].map(z)).toEqual([42.81, 42.31, 41.81, 41.31].map((v) => expect.closeTo(v, 2)));
});
it('C14 length 3447.5 (2855.5 run + 2 × 296 legs)', () => {
expect(c14Length(B)).toBeCloseTo(3447.5, 0);
});
it('C15: 11 bars, z 43.598, length 3852 (2088 + 2 × 882)', () => {
const { c15 } = stairBars(L, B, m.copingRefTop);
expect(c15.length).toBe(11);
expect(c15[0].z).toBeCloseTo(43.598, 3);
expect(c15[0].length).toBeCloseTo(3852, 0);
expect(c15[0].x0).toBeCloseTo(9812, 0);
});
it('C16: 22 bars (3·4구간), z 43.436 / 43.507, lengths 4070 / 4069', () => {
const { c16 } = stairBars(L, B, m.copingRefTop);
expect(c16.length).toBe(22);
expect(c16[0].z).toBeCloseTo(43.436, 3);
expect(c16[11].z).toBeCloseTo(43.507, 3);
expect(c16[0].length).toBeCloseTo(4070, 0);
expect(c16[11].length).toBeCloseTo(4069, 0);
});
it('C15 / C16 across positions: 137 from the cover line, then @250, 11 bars', () => {
const { c15 } = stairBars(L, B, m.copingRefTop);
expect(c15[0].n).toBeCloseTo(-B / 2 + 237, 0);
expect(c15[10].n - c15[0].n).toBeCloseTo(2500, 0);
});
});
describe('C14 연직 발 (코핑철근.md §8)', () => {
it('upper lines keep their legs; a lower bar keeps its legs only if they end above the underside', () => {
const bars = c14Bars(L, m.copingRefTop, rows, under);
expect(bars.filter((b) => b.line <= 2).every((b) => b.legs)).toBe(true);
// The leg of the C12 5단 bar (line 4) at the ends (x 1 m, 11 m) would end below the sloped underside, so it is left out.
const omitted = bars.filter((b) => !b.legs).map((b) => [b.line, Math.round(b.x / 1000)]);
expect(omitted).toEqual([[4, 1], [4, 11]]);
});
});
+6 -6
View File
@@ -12,13 +12,13 @@ describe('③ 철근표 요약 (P1)', () => {
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', () => {
it('coping: C1–C8 12 each, C9 18, C10 9, C11 18, C12 20 (5 rows × 일자 2 + ㄷ 2), C13 8 (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('C12')).toBe(20);
expect(count('C13')).toBe(8);
expect(count('C14')).toBe(38);
expect(count('C15')).toBe(11);
expect(count('C16')).toBe(22);
@@ -43,9 +43,9 @@ describe('③ 철근표 요약 (P1)', () => {
expect(count('B12')).toBe(47);
});
it('B9: 320 bars; warnings: B2 needs a splice (13646 > 12000)', () => {
expect(count('B9')).toBe(320);
expect(warnings.map((w) => w.code)).toEqual(['I02', 'RB-F08']); // defaults are assumed values (I02)
it('B9: 306 bars (320 on the grid less 14 in the column zone); warnings: B2 needs a splice (13646 > 12000)', () => {
expect(count('B9')).toBe(306);
expect(warnings.map((w) => w.code)).toEqual(['I02', 'RB-S04', 'RB-F08']); // defaults are assumed values (I02); bearing embedment 619–669 < 882 (RB-S04)
expect(warnings.find((w) => w.code === 'RB-F08')!.bar).toBe('B2');
// B2 is split into two pieces (추천안): each piece is about half the 13646 bar plus half the lap.
expect(rows.find((r) => r.name === 'B2')!.length!).toBeCloseTo(7776, 0);
+72 -2
View File
@@ -13,11 +13,60 @@ describe('3D 철근 모델 (③)', () => {
}
});
it('P1 has one bar per 원주 등분 (46) and P2 likewise', () => {
expect(rb.byName.get('P1')!.length).toBe(46);
it('B1–B4 and B5–B8 all exist in 3D (B2 was missing)', () => {
for (const n of ['B1', 'B2', 'B3', 'B4', 'B5', 'B6', 'B7', 'B8']) {
expect(rb.byName.get(n)?.length ?? 0, n).toBeGreaterThan(0);
}
});
it('B2 legs reach the B1 height (1873 above the footing bottom), B1 legs hang 304.8 below B1', () => {
const f = buildPier(loadP1()).footings[0];
const box = (name: string) => {
const b = new THREE.Box3();
for (const mesh of rb.byName.get(name)!) b.expandByObject(mesh);
return b;
};
expect(box('B2').max.y - f.bottom).toBeCloseTo(1.873, 2);
expect(box('B1').min.y - f.bottom).toBeCloseTo(1.873 - 0.3048, 2);
});
it('C12 / C13 in 3D: per row two 일자 and two ㄷ (body + 2 legs each end)', () => {
expect(rb.byName.get('C12')!.length).toBe(5 * (2 + 2 * 3));
expect(rb.byName.get('C13')!.length).toBe(2 * (2 + 2 * 3));
});
it('C1 legs turn down 472 at the ends (to EL 42.812 at the end face)', () => {
const model = buildPier(loadP1());
const b = new THREE.Box3();
for (const mesh of rb.byName.get('C1')!) b.expandByObject(mesh);
expect(b.min.y).toBeCloseTo(model.copingRefTop - 0.127 - 0.472, 2);
});
it('C9 / C10 / C11: three 1740-wide 폐합 per position, each a U plus a cap (18 pieces each)', () => {
expect(rb.byName.get('C10')!.length).toBe(9 * 18);
expect(rb.byName.get('C9')!.length).toBe(18 * 18);
expect(rb.byName.get('C11')!.length).toBe(18 * 18);
});
it('C7 / C8 follow the underside: lowest at the Level (+127 − r), vertical legs up to the C1 level', () => {
const model = buildPier(loadP1());
const b = new THREE.Box3();
for (const n of ['C7', 'C8']) for (const mesh of rb.byName.get(n)!) b.expandByObject(mesh);
const r = 28.6 / 2000;
expect(b.min.y).toBeCloseTo(model.levelBottom + 0.127 - r, 2);
// Vertical legs end flat, so no radius on top. C1 is the top row at copingRefTop − 0.127.
expect(b.max.y).toBeCloseTo(model.copingRefTop - 0.127, 2);
});
it('P1 has one bar per 원주 등분 (46) plus its outward hook tail, P2 one per 등분 (46)', () => {
expect(rb.byName.get('P1')!.length).toBe(46 * 2);
expect(rb.byName.get('P2')!.length).toBe(46);
});
it('P6 crossties in 3D: 8 levels × 4 bars each with main bar + 2 downward hooks (3 pieces per bar = 96 pieces)', () => {
expect(rb.byName.get('P6')!.length).toBe(8 * 4 * 3);
});
it('highlight dims the other bars and keeps the chosen one', () => {
rb.highlight('P3');
const p3 = (rb.byName.get('P3')![0].material as THREE.MeshStandardMaterial).opacity;
@@ -28,3 +77,24 @@ describe('3D 철근 모델 (③)', () => {
expect((rb.byName.get('C1')![0].material as THREE.MeshStandardMaterial).opacity).toBe(1);
});
});
describe('받침블럭 S1 · S2 in 3D (받침블럭철근.md §3–5)', () => {
it('per block: 8 S1 and 8 S2, each a bar with two 700 legs (3 pieces)', () => {
const blocks = buildPier(loadP1()).blocks.length;
expect(rb.byName.get('S1')!.length).toBe(blocks * 8 * 3);
expect(rb.byName.get('S2')!.length).toBe(blocks * 8 * 3);
});
});
describe('확대기초 B9 · B10 · B11 · B12 in 3D (확대기초철근 (1).md §7–9)', () => {
it('B9: 306 vertical bars (column zone removed), each three pieces (bar + two hook tails)', () => {
expect(rb.byName.get('B9')!.length).toBe(306 * 3);
});
it('B10-1 / B10-2: 18 each (9 levels × 2 faces), each three pieces; B11 60, B12 47', () => {
expect(rb.byName.get('B10-1')!.length).toBe(18 * 3);
expect(rb.byName.get('B10-2')!.length).toBe(18 * 3);
expect(rb.byName.get('B11')!.length).toBe(60 * 3);
expect(rb.byName.get('B12')!.length).toBe(47 * 3);
});
});