Files
Pier_v1/tests/abut-rebar-wall.test.ts
T
ypshimandClaude Haiku 5.5 079748e5e7 feat(abut-rebar): 교대 벽체 배근 삽도 (교대축 직각 단면 · 계단 단면)
- 배근 규칙 경로로 그림: 구간 1 단면 (W1–W7 · W9 · W10, 단 치수), 계단 단면 (W1 · W5 · W9 · W10 · W11 · W8, 구간 높이)
- 번호 풍선 · 선택 강조 · 선택만 보기는 저판 삽도와 같은 함수 (공용으로 export)
- verify/dump: 삽도 SVG 덤프 전용 설정 (기본 테스트에 포함 안 됨)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-04 16:27:57 +09:00

177 lines
8.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 { abutFootingGeom, abutFootingRebar } from '../src/core/abut/rebar/footing';
import { skewAngle } from '../src/core/abut/rebar/skew-layout';
import { abutWallGeom, abutWallRebar, type AbutWallGeom } from '../src/core/abut/rebar/wall';
import { DEFAULT_MATERIAL } from '../src/core/rebar/common';
// 예제 A1 (교대벽체철근.md §11). 기획서 기대값은 fck 27 · fy 400.
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 angle = skewAngle(rec.skewDeg);
const fGeom = abutFootingGeom(model.length, members.shape, angle);
const footing = abutFootingRebar(fGeom);
const geom = abutWallGeom(model.length, members, angle);
const fck27 = { ...DEFAULT_MATERIAL, fck: 27 };
const A1 = abutWallRebar(geom, footing, undefined, fck27, 'A1');
const bar = (r: typeof A1, mark: string) => r.bars.find((b) => b.mark === mark)!;
describe('교대 벽체 배근 A1 (AD4, 교대벽체철근.md §11)', () => {
it('geometry: 5 steps, 구간 1 (IP3 끝) H 2372, 구간 5 H 2656, 벽체 −800 ~ +1600', () => {
expect(geom.segments.length).toBe(5);
expect([geom.xb, geom.xf, geom.HF]).toEqual([-800, 1600, 1100]);
const byNo = (no: number) => geom.segments.find((s) => s.no === no)!;
expect(byNo(1).H).toBeCloseTo(2372, 0);
expect(byNo(5).H).toBeCloseTo(2656, 0);
});
it('위치: W5 1500, W1 1480.9, W3 · W4 −700, W6 −682.5, W9 첫 줄 1463.4 · 폭 6개', () => {
expect(A1.x.W5).toBeCloseTo(1500, 6);
expect(A1.x.W1).toBeCloseTo(1480.9, 6);
expect(A1.x.W34).toBeCloseTo(-700, 6);
expect(A1.x.W6).toBeCloseTo(-682.5, 6);
expect(A1.x.W9[0]).toBeCloseTo(1463.4, 6);
expect(A1.x.W9.length).toBe(6);
});
it('교대축 배치: 93@125, e 185.8, 저판 격자 − 17.5 → W1 47, W3 47, W4 47', () => {
expect([A1.vertical.layout.n, A1.vertical.layout.positions.length]).toEqual([93, 94]);
expect(A1.vertical.layout.e).toBeCloseTo(185.8, 1);
expect(A1.vertical.shift).toBeCloseTo(17.5, 6);
expect(A1.vertical.positions[0]).toBeCloseTo(footing.main.positions[0] - 17.5, 6);
expect(['W1', 'W3', 'W4'].map((m) => bar(A1, m).count)).toEqual([47, 47, 47]);
});
it('높이 (구간 1): z_hook 186.55, W1 · W4 3481, W3 4464', () => {
expect(A1.zHook.H19).toBeCloseTo(186.55, 2);
expect([bar(A1, 'W1').length, bar(A1, 'W4').length, bar(A1, 'W3').length]).toEqual([3481, 3481, 4464]);
});
it('W2: 2189.4 + 246 + 735 = 3170, W1 위치 +17.5', () => {
expect(bar(A1, 'W2').length).toBe(3170);
const w1 = A1.paths.find((p) => p.mark === 'W1')!.pts[0][0];
const w2 = A1.paths.find((p) => p.mark === 'W2')!.pts[0][0];
expect(w2 - w1).toBeCloseTo(17.5, 6);
});
it('수평 단: 구간 1 17@125=2.125 · 147 (18단), 구간 5 19@125=2.375 · 181 (20단)', () => {
const lv = (no: number) => A1.levels.find((l) => l.no === no)!;
expect([lv(1).count, lv(1).text]).toEqual([18, '17@125=2.125 · 147']);
expect([lv(5).count, lv(5).text]).toEqual([20, '19@125=2.375 · 181']);
});
it('W9 · W10: 5구간 → W9 2구간 × 6, W10 3구간 × 6; 구간 수에 따라 W9만 / W10 (m − 2)', () => {
expect(bar(A1, 'W9').count).toBe(12);
expect(bar(A1, 'W10').count).toBe(18);
const withSegs = (n: number): AbutWallGeom => {
const step = geom.L / n;
return { ...geom, segments: Array.from({ length: n }, (_, i) => ({ from: i * step, to: (i + 1) * step, H: 2372 + 50 * i, no: n - i })) };
};
const marks = (n: number) => abutWallRebar(withSegs(n), footing, undefined, fck27).bars.map((b) => b.mark);
expect(marks(1)).toContain('W9');
expect(marks(1)).not.toContain('W10');
expect(marks(2)).not.toContain('W10');
expect(bar(abutWallRebar(withSegs(4), footing, undefined, fck27), 'W10').count).toBe(12);
});
it('W9 extends l_d 735 into the lower neighbour only', () => {
const k = 2; // a middle step, neighbours on both sides
const sg = geom.segments[k];
const z = 1100 + sg.H - 100 - 15.9;
const p = A1.paths.find((q) => q.mark === 'W10' && Math.abs(q.pts[0][2] - z) < 1e-6)!;
const lowLeft = geom.segments[k - 1].H < sg.H;
const lowRight = geom.segments[k + 1].H < sg.H;
expect(p.pts[0][0]).toBeCloseTo(lowLeft ? sg.from - 735 : sg.from, 6);
expect(p.pts[1][0]).toBeCloseTo(lowRight ? sg.to + 735 : sg.to, 6);
});
it('W7: 12 × 5 = 60, 2594', () => {
expect([bar(A1, 'W7').count, bar(A1, 'W7').length]).toEqual([60, 2594]);
});
it('W8: 3383, 낮은 끝 18 + 높은 끝 20 = 38, 끝면에서 114.3', () => {
expect([bar(A1, 'W8').count, bar(A1, 'W8').length]).toEqual([38, 3383]);
const w8 = A1.paths.find((p) => p.mark === 'W8')!;
expect(w8.pts[1][0] * angle.sin).toBeCloseTo(114.3, 6);
});
it('W11: 끝면당 13 + 3 = 16, 양끝 32, 길이 3433 / 3717', () => {
const w = bar(A1, 'W11');
expect([w.count, w.length, w.lengthMax]).toEqual([32, 3433, 3717]);
const xs = A1.paths.filter((p) => p.mark === 'W11').slice(0, 16).map((p) => Math.round(p.pts[1][1]));
expect(xs).toEqual([-700, -400, -100, ...Array.from({ length: 13 }, (_, i) => i * 125)]);
});
it('A1 has no warning (RB-AB02 · RB-AB03 없음)', () => {
expect(A1.issues.filter((i) => i.level !== 'I').map((i) => i.code)).toEqual([]);
});
it('l_d 연동: fck 30 → W2 앞 다리 · W3 위 끝이 짧아짐', () => {
const r30 = abutWallRebar(geom, footing, undefined, { ...DEFAULT_MATERIAL, fck: 30 });
expect(r30.ld.H16).toBeLessThan(A1.ld.H16);
expect(bar(r30, 'W2').length).toBe(3170 - (A1.ld.H16 - r30.ld.H16));
expect(bar(r30, 'W3').length).toBe(4464 - (A1.ld.H19 - r30.ld.H19));
expect(bar(r30, 'W1').length).toBe(3481);
});
it('기본간격 연동: 벽체 s_L 150 → 벽체만 다시 계산, 저판 그대로, RB-AB03 검사', () => {
const r = abutWallRebar(geom, footing, { sL: 150 }, fck27);
expect(r.vertical.layout.s).toBe(150);
expect(footing.main.layout!.s).toBe(125);
expect(r.issues.map((i) => i.code)).toContain('RB-AB03');
});
it('묶음 수정 · W7 배수 → RB-AB04 "수정됨"', () => {
const r = abutWallRebar(geom, footing, { sHoriz: 150, k7L: 6 }, fck27);
expect(r.issues.map((i) => i.code)).toContain('RB-AB04');
expect(bar(r, 'W5').modified).toBe(true);
expect(bar(r, 'W7').modified).toBe(true);
expect(bar(r, 'W1').modified).toBe(false);
});
});
import { applyFootingRebar, applyWallRebar } from '../src/core/abut/edit';
import { checkStoredAbutments } from '../src/core/project';
describe('③ 벽체 배근 입력 저장', () => {
it('stores only the changed values next to the 저판 ones; undefined links a group back; refuses bad values', () => {
const a = applyWallRebar(applyFootingRebar(rec, { sW: 200 }), { sHoriz: 150, k7L: 6 });
expect(a.rebar).toEqual({ footing: { sW: 200 }, wall: { sHoriz: 150, k7L: 6 } });
expect(applyWallRebar(a, { sHoriz: undefined }).rebar?.wall).toEqual({ k7L: 6 });
expect(() => applyWallRebar(rec, { k7V: 2.5 })).toThrow('W7 배수');
expect(() => applyWallRebar(rec, { c: 0 })).toThrow();
});
it('the project file keeps the 벽체 input and refuses a non-number', () => {
expect(() => checkStoredAbutments([applyWallRebar(rec, { sL: 150, eMin: 200 })])).not.toThrow();
expect(() => checkStoredAbutments([{ ...rec, rebar: { wall: { sL: 'x' } } as never }])).toThrow('벽체 배근');
});
});
import { abutWallElevationSvg, abutWallSectionSvg } from '../src/view/abut-wall-rebar-svg';
describe('③ 벽체 삽도', () => {
it('단면: 구간 1 단 치수 "100 · 17@125=2.125 · 147", W9 · W10 간격, 번호 풍선', () => {
const svg = abutWallSectionSvg(geom, A1);
expect(svg).toContain('100 · 17@125=2.125 · 147');
expect(svg).toContain('W9 · W10 5@250');
for (const m of ['W1', 'W2', 'W3', 'W4', 'W5', 'W6', 'W7', 'W9', 'W10']) expect(svg).toContain(`data-pick="${m}"`);
});
it('계단 단면: 구간별 높이, 양끝 단 치수, W8 · W11 풍선; 선택하면 나머지는 흐리게 / 숨김', () => {
const svg = abutWallElevationSvg(geom, A1);
expect(svg).toContain('구간 1 · 2372');
expect(svg).toContain('19@125=2.375 · 181');
for (const m of ['W8', 'W11']) expect(svg).toContain(`data-pick="${m}"`);
expect(abutWallElevationSvg(geom, A1, { marks: new Set(['W9']), only: false })).toContain('opacity="0.15"');
expect(abutWallElevationSvg(geom, A1, { marks: new Set(['W9']), only: true })).toContain('opacity="0"');
});
});