- 사용자 도면(2026-11-04): 받침 ① 위치에서 받침 ① 교좌면부터 흉벽 상단까지 연직 거리. 모든 교대 기본값 3326 (QA-4 가정 2500 대체) - 입력 이름 "벽체 높이 · 받침 ① 기준 연직", ② 정면도에 받침 ① 기준선과 H_w 치수 - ③ 흉벽 철근표 대표 단면 = 받침 ① 위치 (P1–P3는 그 단면에서 정확히 계산) - 테스트: 흉벽 상단 IP1 48.006 / IP3 47.661 등 + 0.826 반영, 교대흉벽철근.md §11 재현은 2500을 넣어 유지, 3326 기본 확인 (P1 3945) Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
168 lines
7.3 KiB
TypeScript
168 lines
7.3 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, type AbutInput } from '../src/core/abut/geometry';
|
||
import { parseAbutments, toAbutInput } from '../src/core/abut/parse';
|
||
import { buildMembers } from '../src/core/abut/members';
|
||
import { buildWings, WING_LENGTH_M } from '../src/core/abut/wings';
|
||
|
||
const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url));
|
||
const a1Input = toAbutInput(parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abutments[0]);
|
||
const SKEW = 95 + 2 / 60 + 52 / 3600;
|
||
const GIRDER = 75 + 56 / 60 + 55.9 / 3600;
|
||
|
||
// A2 합성 예제 (spec §10.2): ① 쪽이 IP1 끝.
|
||
const a2Input: AbutInput = {
|
||
name: 'A2',
|
||
type: 'A2',
|
||
seatEl: 44.367,
|
||
ip1: { e: 179161.0318, n: 259932.3259 },
|
||
ip2: { e: 179161.0417, n: 259932.3276 },
|
||
ip3: { e: 179173.0573, n: 259930.2859 },
|
||
bearings: [
|
||
{ no: 1, pos: { e: 179161.9111, n: 259931.422 }, z: 44.492 },
|
||
{ no: 2, pos: { e: 179164.3854, n: 259931.0021 }, z: 44.563 },
|
||
{ no: 3, pos: { e: 179166.8597, n: 259930.5821 }, z: 44.634 },
|
||
{ no: 4, pos: { e: 179169.334, n: 259930.1621 }, z: 44.705 },
|
||
{ no: 5, pos: { e: 179171.8084, n: 259929.7421 }, z: 44.776 },
|
||
],
|
||
skewDeg: SKEW,
|
||
slopeI: -0.0191,
|
||
girderAzimuthDeg: GIRDER,
|
||
foundationBottomEl: 40.895,
|
||
pileLengthMm: 21600,
|
||
};
|
||
|
||
describe('교대 A2 합성 · 날개벽 (AA3b, spec_abut §10.2)', () => {
|
||
const model = buildAbut(a2Input);
|
||
const members = buildMembers(model, { bottomEl: 40.895, pileLengthMm: 21600 });
|
||
const [left, right] = buildWings(model, members);
|
||
|
||
it('좌 날개벽 (① 쪽, IP1 끝): top 47.664 at the start, 47.540 at the end, bottom end 46.040', () => {
|
||
expect(left.side).toBe('left');
|
||
expect(left.sEnd).toBeCloseTo(0, 9);
|
||
expect(left.topStartEl).toBeCloseTo(47.664, 2);
|
||
expect(left.topEndEl).toBeCloseTo(47.540, 2);
|
||
expect(left.bottomEndEl).toBeCloseTo(46.040, 2);
|
||
});
|
||
|
||
it('우 날개벽 (⑤ 쪽, IP3 끝): top 48.009 → 47.885, bottom end 46.385', () => {
|
||
expect(right.side).toBe('right');
|
||
expect(right.sEnd).toBeCloseTo(model.length, 9);
|
||
expect(right.topStartEl).toBeCloseTo(48.009, 2);
|
||
expect(right.topEndEl).toBeCloseTo(47.885, 2);
|
||
expect(right.bottomEndEl).toBeCloseTo(46.385, 2);
|
||
});
|
||
|
||
it('the wing is 6.5 m long along the wing line and 0.45 m thick inward', () => {
|
||
const [p0, p1] = left.corners;
|
||
expect(Math.hypot(p1.e - p0.e, p1.n - p0.n)).toBeCloseTo(WING_LENGTH_M, 6);
|
||
const [, , q2, q3] = left.corners;
|
||
expect(Math.hypot(q2.e - p1.e, q2.n - p1.n)).toBeCloseTo(0.45, 6);
|
||
expect(Math.hypot(q3.e - p0.e, q3.n - p0.n)).toBeCloseTo(0.45, 6);
|
||
});
|
||
|
||
it('the wing starts at the axis end with the wing direction on the 배면 side (AA2 line 75.32°)', () => {
|
||
const [p0, p1] = left.corners;
|
||
const deg = (Math.atan2(p1.n - p0.n, p1.e - p0.e) * 180) / Math.PI;
|
||
const cad = ((deg % 360) + 360) % 360;
|
||
expect(cad).toBeCloseTo(75.32, 1);
|
||
});
|
||
|
||
it('a 단축형 wing bottom sits at the footing top (41.995)', () => {
|
||
const short = buildWings(model, members, { left: 'short', right: 'none' });
|
||
expect(short).toHaveLength(1);
|
||
expect(short[0].bottomEndEl).toBeCloseTo(41.995, 3);
|
||
});
|
||
|
||
it('no wing when the side is 없음', () => {
|
||
expect(buildWings(model, members, { left: 'none', right: 'none' })).toEqual([]);
|
||
});
|
||
});
|
||
|
||
describe('교대 A1 날개벽 (운곡교P1입력.xlsx)', () => {
|
||
const model = buildAbut(a1Input);
|
||
const members = buildMembers(model, { bottomEl: a1Input.foundationBottomEl, pileLengthMm: a1Input.pileLengthMm });
|
||
const wings = buildWings(model, members);
|
||
|
||
it('A1 has two general wings; ① 쪽 wing starts at the IP3 end (the bearing ① is near IP3)', () => {
|
||
expect(wings.map((w) => w.side)).toEqual(['left', 'right']);
|
||
const left = wings[0];
|
||
expect(left.sEnd).toBeCloseTo(model.length, 9);
|
||
expect(left.topStartEl).toBeCloseTo(47.661, 2);
|
||
// A1 날개벽 경사는 +1.91 % (벽체에서 멀어질수록 높아짐): 끝이 0.124 m 높다.
|
||
expect(left.topEndEl - left.topStartEl).toBeCloseTo(0.124, 2);
|
||
});
|
||
});
|
||
|
||
describe('교대 날개벽 하단 선 · 헌치 (AA3c, spec_abut §4.7)', () => {
|
||
const a1 = buildAbut(a1Input);
|
||
const a1m = buildMembers(a1, { bottomEl: a1Input.foundationBottomEl, pileLengthMm: a1Input.pileLengthMm });
|
||
const [a1Left] = buildWings(a1, a1m);
|
||
const a2 = buildAbut(a2Input);
|
||
const a2m = buildMembers(a2, { bottomEl: 40.895, pileLengthMm: 21600 });
|
||
const [a2Left] = buildWings(a2, a2m);
|
||
|
||
it('뒷굽 끝까지의 날개벽 거리 1.807 m (단축형 표와 같다)', () => {
|
||
expect(a1Left.heelDistanceM).toBeCloseTo(1.807, 2);
|
||
expect(a2Left.heelDistanceM).toBeCloseTo(1.807, 2);
|
||
});
|
||
|
||
it('하단선: 뒷굽 끝까지 저판 상면 41.995, 그 밖은 단부 하단까지 직선 (A2 좌: 46.040)', () => {
|
||
const line = a2Left.bottomLine;
|
||
expect(line[0].el).toBeCloseTo(41.995, 3);
|
||
expect(line[1].dM).toBeCloseTo(1.807, 2);
|
||
expect(line[1].el).toBeCloseTo(41.995, 3);
|
||
expect(line[2].el).toBeCloseTo(46.040, 2);
|
||
});
|
||
|
||
it('two vertical 300 × 300 haunches at the 내측면 corners: upper at the 흉벽 배면, lower at the 벽체 배면', () => {
|
||
expect(a2Left.haunches.map((h) => h.kind)).toEqual(['upper', 'lower']);
|
||
for (const h of a2Left.haunches) {
|
||
const [c0, c1, c2] = h.corners;
|
||
expect(Math.hypot(c1.e - c0.e, c1.n - c0.n)).toBeCloseTo(0.3, 6);
|
||
expect(Math.hypot(c2.e - c0.e, c2.n - c0.n)).toBeCloseTo(0.3, 6);
|
||
}
|
||
});
|
||
|
||
it('the 저판 상면 haunch runs from the 벽체 배면 to the 뒷굽 on the 저판 상면; the 뒷굽 haunch is gone', () => {
|
||
const f = a2Left.footingHaunch!;
|
||
expect(f.el).toBeCloseTo(41.995, 3);
|
||
expect(f.dMTo).toBeGreaterThan(f.dMFrom);
|
||
expect((a2Left as { heelHaunch?: unknown }).heelHaunch).toBeUndefined();
|
||
});
|
||
|
||
it('upper haunch runs from the bracket top (흉벽 상단 − 0.85) to the wing top', () => {
|
||
const [up, low] = a2Left.haunches;
|
||
// The upper haunch rests on the highest point of the 브라켓 top over its footprint (the top falls along s), never below it.
|
||
expect(up.zBottomEl).toBeGreaterThanOrEqual(47.664 - 0.85 - 1e-6);
|
||
expect(up.zBottomEl).toBeLessThan(47.664 - 0.85 + 0.05);
|
||
expect(up.zTopEl).toBeGreaterThan(up.zBottomEl);
|
||
expect(low.zBottomEl).toBeCloseTo(41.995, 3);
|
||
expect(low.zTopEl).toBeCloseTo(47.664 - 0.85, 2);
|
||
});
|
||
});
|
||
|
||
describe('날개벽 수평 단 (QA-12 초안 적용, spec_abut §4.7)', () => {
|
||
const a2 = buildAbut(a2Input);
|
||
const a2m = buildMembers(a2, { bottomEl: 40.895, pileLengthMm: 21600 });
|
||
const [a2Left] = buildWings(a2, a2m);
|
||
|
||
it('등단면 down to 1.5 m below the wing end, then 변화단면1 to the seat EL and 변화단면2 to the footing top', () => {
|
||
const [equal, v1, v2] = a2Left.bands;
|
||
expect(equal.name).toBe('등단면');
|
||
expect(equal.zBottomEl).toBeCloseTo(46.040, 2);
|
||
expect(v1.name).toBe('변화단면1');
|
||
expect(v1.zBottomEl).toBeCloseTo(44.367, 3);
|
||
expect(v2.name).toBe('변화단면2');
|
||
expect(v2.zBottomEl).toBeCloseTo(41.995, 3);
|
||
});
|
||
|
||
it('bands are contiguous from the wing top to the footing top', () => {
|
||
const b = buildWings(a2, a2m)[0].bands;
|
||
for (let i = 1; i < b.length; i++) expect(b[i].zTopEl).toBeCloseTo(b[i - 1].zBottomEl, 6);
|
||
expect(b[b.length - 1].zBottomEl).toBeCloseTo(41.995, 3);
|
||
});
|
||
});
|