- 평면: 왼쪽 절반 하면 (R3 · R4 · R8) · 오른쪽 절반 상면 (R1 · R2), 모따기 모서리 R7, 교대축 · 길이 배치 치수 - 단면 (길이 방향, 폭 가운데): 브라켓 위 평탄 + 경사, 층 순서, R8은 R1 · R3 사이 - verify/dump: 접속슬래브 삽도 덤프 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
145 lines
7.7 KiB
TypeScript
145 lines
7.7 KiB
TypeScript
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"');
|
||
});
|
||
});
|