Files
Pier_v1/tests/abut-rebar-slab.test.ts
T
ypshimandClaude Haiku 5.5 e9d79957c8 feat(abut-rebar): 교대 접속슬래브 배근 삽도 (평면 · 단면)
- 평면: 왼쪽 절반 하면 (R3 · R4 · R8) · 오른쪽 절반 상면 (R1 · R2), 모따기 모서리 R7, 교대축 · 길이 배치 치수
- 단면 (길이 방향, 폭 가운데): 브라켓 위 평탄 + 경사, 층 순서, R8은 R1 · R3 사이
- verify/dump: 접속슬래브 삽도 덤프

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

145 lines
7.7 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('접속슬래브 배근');
});
});
import { abutSlabPlanSvg, abutSlabSectionSvg } from '../src/view/abut-slab-rebar-svg';
describe('③ 접속슬래브 삽도', () => {
it('평면: 교대축 · 길이 배치 치수, 하면 · 상면 반쪽, 번호 풍선 R1–R4 · R7 · R8', () => {
const svg = abutSlabPlanSvg(geom, A1);
expect(svg).toContain(A1.across.text);
expect(svg).toContain('100 · 250 · 21@300=6.300 · 250 · 100');
for (const m of ['R1', 'R2', 'R3', 'R4', 'R7', 'R8']) expect(svg).toContain(`data-pick="${m}"`);
});
it('단면: 매립형 · 경사 −4 %, 선택하면 나머지 흐리게 / 숨김', () => {
const svg = abutSlabSectionSvg(geom, A1);
expect(svg).toContain('매립형');
expect(svg).toContain('−4.0 %'.replace('−', '-'));
expect(abutSlabSectionSvg(geom, A1, { marks: new Set(['R8']), only: true })).toContain('opacity="0"');
});
});