Files
Pier_v1/tests/abut-rebar-slab.test.ts
T
ypshimandClaude Haiku 5.5 9ae6cf0945 feat(abut): 접속슬래브 배근 입력 저장
- AbutRecord.rebar.slab (바꾼 값만), applySlabRebar, 프로젝트 파일 검증

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-05 09:23:56 +09:00

127 lines
6.9 KiB
TypeScript
Raw 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 { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import * as XLSX from 'xlsx';
import { describe, expect, it } from 'vitest';
import { buildAbut } from '../src/core/abut/geometry';
import { parseAbutments, toAbutInput } from '../src/core/abut/parse';
import { buildMembers } from '../src/core/abut/members';
import { buildWings } from '../src/core/abut/wings';
import { resolveSlab, slabGradePct, slabSection } from '../src/core/abut/slab';
import { abutSlabGeom, abutSlabRebar, type AbutSlabGeom } from '../src/core/abut/rebar/slab';
import { abutPlanOutline } from '../src/view/abut-scene';
import { DEFAULT_MATERIAL } from '../src/core/rebar/common';
// 예제 A1 (교대접속슬래브철근.md §11), fck 27.
const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url));
const rec = parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abutments[0];
const model = buildAbut(toAbutInput(rec));
const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm });
const geomFor = (forms?: { left: 'general' | 'short' | 'none'; right: 'general' | 'short' | 'none' }, slabIn?: Parameters<typeof resolveSlab>[0]): AbutSlabGeom => {
const wings = buildWings(model, members, forms);
const slab = resolveSlab(slabIn);
const grade = slabGradePct(slab, model.wing?.slopePct ?? 0);
const ch = (sEnd: number) => (wings.find((w) => Math.abs(w.sEnd - sEnd) < 1e-9)?.haunchM ?? 0) * 1000;
return abutSlabGeom(
slab.form,
abutPlanOutline(model, members, wings, slab).slab,
slabSection(slab, grade, members.shape),
{ startEl: members.parapetTopStartEl, endEl: members.parapetTopEndEl, L: model.length, bottomEl: members.footingBottomEl! },
[ch(0), ch(model.length)],
slab.lengthMm,
grade,
);
};
const geom = geomFor();
const fck27 = { ...DEFAULT_MATERIAL, fck: 27 };
const A1 = abutSlabRebar(geom, undefined, fck27, 'A1');
const bar = (r: typeof A1, m: string) => r.bars.find((b) => b.mark === m);
describe('교대 접속슬래브 배근 A1 (AD8, 교대접속슬래브철근.md §11)', () => {
it('geometry: 폭 ≈ 11253 (② 평면 측면선 사이 11257.3 — 날개벽 두께를 교대축 방향으로 띄운 skew 차이), 길이 7000, 모따기 양측 300', () => {
expect(Math.abs(geom.B - 11253)).toBeLessThan(5);
expect(geom.near - geom.far).toBeCloseTo(7000, 6);
expect([geom.chamfer0, geom.chamferL]).toEqual([300, 300]);
});
it('A1: R1 36 · R3 71 (35@300, e ≈ 276.5), R2 22 · R4 43 (21@300, e 250)', () => {
expect(['R1', 'R3', 'R2', 'R4'].map((m) => bar(A1, m)?.count)).toEqual([36, 71, 22, 43]);
expect(A1.across.e).toBeCloseTo((geom.B - 200 - 10500) / 2, 6);
expect(A1.along.text).toBe('100 · 250 · 21@300=6.300 · 250 · 100');
});
it('삽도 재현 (B 11670): 100 · 185 · 37@300=11.100 · 185 · 100, 양끝 첫 R1 · R3이 모따기에 걸려 짧아짐 (RB-AS02)', () => {
const g = { ...geom, sideL: { ...geom.sideL, s: geom.side0.s + 11670 }, B: 11670 };
const r = abutSlabRebar(g, undefined, fck27);
expect(r.across.text).toBe('100 · 185 · 37@300=11.100 · 185 · 100');
expect(r.issues.map((i) => i.code)).toContain('RB-AS02');
});
it('높이: 상면 R1 = 상면 − 80, R2 = 상면 − 97.45, 하면 R4 = 하면 + 97.45, R3 = 하면 + 80 (t_s 450 → 370 / 352.55 / 97.45 / 80)', () => {
expect([A1.z.R1, A1.z.R2, A1.z.R4, A1.z.R3]).toEqual([80, expect.closeTo(97.45, 6), expect.closeTo(97.45, 6), 80]);
const r1 = A1.paths.find((p) => p.mark === 'R1')!;
const r3 = A1.paths.find((p) => p.mark === 'R3' && Math.abs(p.pts[0][0] - r1.pts[0][0]) < 1)!;
expect(r1.pts[0][2] - r3.pts[0][2]).toBeCloseTo(370 - 80, 6);
});
it('길이: R1 · R3 = 6800 (측면 skew와 경사만큼 김), R2 · R4 = B − 2c / sin θ ≈ 11052', () => {
const r1 = bar(A1, 'R1')!.length;
expect(r1).toBeGreaterThanOrEqual(6800);
expect(r1).toBeLessThan(6800 * 1.006);
// 3D 길이: 흉벽 상단이 교대축을 따라 기울어 (345 / 12.2 m) 평면 길이보다 약 4.5 김.
expect(Math.abs(bar(A1, 'R2')!.length - (geom.B - 200 / geom.sin))).toBeLessThan(6);
});
it('모따기: A1은 짧아지는 철근 없음', () => {
expect(A1.trimmed).toBe(0);
});
it('R8 54개 · 627 (= 290 + (22.2 + 22.2)/2 … 135° 갈고리 151 × 2)', () => {
expect(bar(A1, 'R8')!.count).toBe(54);
});
it('R7: 양측 날개벽 4 · 한쪽 없음 2 · 양측 없음 0, 길이 2·l_d + 341 = 1515 (skew로 대각이 조금 길어 +25 이내)', () => {
expect(bar(A1, 'R7')!.count).toBe(4);
expect(bar(A1, 'R7')!.length - 1515).toBeGreaterThanOrEqual(0);
expect(bar(A1, 'R7')!.length - 1515).toBeLessThan(25);
const one = abutSlabRebar(geomFor({ left: 'general', right: 'none' }), undefined, fck27);
expect(bar(one, 'R7')!.count).toBe(2);
const none = abutSlabRebar(geomFor({ left: 'none', right: 'none' }), undefined, fck27);
expect(bar(none, 'R7')).toBeUndefined();
});
it('날개벽 형식: 한쪽 없음 B ≈ 11725 · 양측 없음 B ≈ 12197 → 개수 다시 계산', () => {
expect(Math.abs(geomFor({ left: 'general', right: 'none' }).B - 11725)).toBeLessThan(5);
expect(Math.abs(geomFor({ left: 'none', right: 'none' }).B - 12197)).toBeLessThan(5);
});
it('기본간격 연동: s_a 200 → R1 @400, R3 @200, R8 @1600', () => {
const r = abutSlabRebar(geom, { sA: 200 }, fck27);
expect([bar(r, 'R1')!.spacing, bar(r, 'R3')!.spacing, bar(r, 'R8')!.spacing.split(' × ')[0]]).toEqual(['400', '200', '1.600']);
});
it('노출형도 같은 규칙: 상면 철근은 상면, 하면 철근은 하면을 따라 (사용자 결정 2026-11-05)', () => {
const ex = abutSlabRebar(geomFor(undefined, { form: 'exposed' }), undefined, fck27);
expect(['R1', 'R3', 'R2', 'R4'].map((m) => bar(ex, m)?.count)).toEqual([36, 71, 22, 43]);
const r3 = ex.paths.find((p) => p.mark === 'R3')!;
expect(r3.pts.length).toBeGreaterThan(2); // 헌치 · 벽체 배면에서 꺾임
});
});
import { applySlabRebar } from '../src/core/abut/edit';
import { checkStoredAbutments } from '../src/core/project';
describe('③ 접속슬래브 배근 입력 저장', () => {
it('stores only the changed values; undefined links a group back; refuses bad values', () => {
const a = applySlabRebar(rec, { sMain: 200, k8: 6 });
expect(a.rebar?.slab).toEqual({ sMain: 200, k8: 6 });
expect(applySlabRebar(a, { sMain: undefined }).rebar?.slab).toEqual({ k8: 6 });
expect(() => applySlabRebar(rec, { k8: 1.5 })).toThrow('R8 배수');
expect(() => applySlabRebar(rec, { cv: 0 })).toThrow();
});
it('the project file keeps the 접속슬래브 input and refuses a non-number', () => {
expect(() => checkStoredAbutments([applySlabRebar(rec, { c: 90 })])).not.toThrow();
expect(() => checkStoredAbutments([{ ...rec, rebar: { slab: { c: 'x' } } as never }])).toThrow('접속슬래브 배근');
});
});