Files
Pier_v1/tests/multicol-rebar.test.ts
T
ypshimandClaude Haiku 5.5 42316104df fix(rebar): 다주형 코핑에서 C5·C6 제외 (전용 삽도 근거 없음)
1106 엑셀 '2주형 3주형' 삽도(A92·A61·A38)에 C5·C6이 없어
다주형 상면 주철근 생성에서 제외했다. T형 레거시 경로는 변경하지 않는다.
경고 문구·화면 안내를 갱신하고 제외를 고정하는 테스트를 추가했다.

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

107 lines
6.7 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry';
import { columnInputFor, columnRebar } from '../src/core/rebar/column';
import { multicolCopingBars } from '../src/core/rebar/multicol-coping';
import { rebarSummary } from '../src/core/rebar/summary';
import { castRebarLayout } from '../src/core/rebar/cast-layout';
import { buildRebar3D } from '../src/view/rebar3d';
import { loadP1 } from './helpers';
describe('multi-column drawing corrections', () => {
it('excludes C5/C6 from the multi-column coping bars (대안A: the 1106 전용 삽도 has no C5/C6)', () => {
for (const [type, columnCount] of [['2주형', 2], ['3주형', 3]] as const) {
const model = buildPier({ ...loadP1(), type, columnCount, foundationType: '말뚝기초', groundEl: 30, pileLengthMm: 24000 });
const names = new Set(multicolCopingBars(model).map(b => b.name));
expect(names.has('C5')).toBe(false);
expect(names.has('C6')).toBe(false);
for (const name of ['C1', 'C2', 'C3', 'C4', 'C7', 'C8']) expect(names.has(name), `${type} ${name}`).toBe(true);
}
});
for (const [type, columnCount] of [['2주형', 2], ['3주형', 3]] as const) {
for (const foundationType of ['말뚝기초', '현장타설말뚝'] as const) {
it(`${type} ${foundationType}: every coping path stays inside the concrete lower boundary`, () => {
const model = buildPier({ ...loadP1(), type, columnCount, foundationType, groundEl: 30, pileLengthMm: 24000 });
const bars = multicolCopingBars(model);
expect(bars.length).toBeGreaterThan(100);
const polygon = model.copingParts[0].profile;
// Independent intersection against the concrete polygon; include all haunch vertices.
const lower = (x: number) => Math.min(...polygon.flatMap((a, i) => {
const b = polygon[(i + 1) % polygon.length];
return Math.abs(a[0] - b[0]) > 1e-9 && x >= Math.min(a[0], b[0]) - 1e-9 && x <= Math.max(a[0], b[0]) + 1e-9
? [a[1] + (b[1] - a[1]) * (x - a[0]) / (b[0] - a[0])] : [];
}));
for (const bar of bars.filter(b => !['C15', 'C16'].includes(b.name))) for (let i = 1; i < bar.pts.length; i++) {
const a = bar.pts[i - 1], b = bar.pts[i];
const ts = [...Array.from({ length: 21 }, (_, j) => j / 20), ...polygon.map(p => (p[0] - a[0]) / (b[0] - a[0])).filter(t => Number.isFinite(t) && t >= 0 && t <= 1)];
for (const t of ts) {
const x = a[0] + (b[0] - a[0]) * t, y = a[1] + (b[1] - a[1]) * t;
expect(y - lower(x), `${bar.name} at ${x}`).toBeGreaterThanOrEqual(0.099);
}
}
const summary = rebarSummary(model);
for (const row of summary.rows.filter(r => r.member === 'coping')) {
const pieces = bars.filter(b => b.name === row.name);
expect(row.count).toBe(pieces.length);
expect(row.pieces!.reduce((s, p) => s + p.count * p.length, 0)).toBeCloseTo(pieces.reduce((s, p) => s + p.length, 0), 6);
}
const scene = buildRebar3D(model);
for (const name of ['C7', 'C8', 'C10', 'C12', 'C14']) {
const expected = bars.filter(b => b.name === name);
expect(scene.records.filter(r => r.name === name).every(r => r.dMm === expected[0].diameterMm)).toBe(true);
expect(scene.records.some(r => r.name === name)).toBe(true);
}
scene.group.traverse(o => { const mesh = o as import('three').Mesh; mesh.geometry?.dispose(); const m = mesh.material; if (Array.isArray(m)) m.forEach(v => v.dispose()); else m?.dispose(); });
}, 15000);
}
it(`${type}: physical column pieces and local confinement height agree`, () => {
const model = buildPier({ ...loadP1(), type, columnCount });
const rows = rebarSummary(model).rows;
for (const name of ['P1', 'P2']) {
const row = rows.find(r => r.name === name)!;
expect(row.pieces!.reduce((s, p) => s + p.count, 0)).toBe(row.count);
const key = name === 'P1' ? 'p1Pieces' : 'p2Pieces';
const total = model.columns.reduce((s, _, i) => {
const input = columnInputFor(model, undefined, i);
return s + input.nDiv * columnRebar(input)[key].reduce((a, b) => a + b, 0);
}, 0);
expect(row.pieces!.reduce((s, p) => s + p.count * p.length, 0)).toBeCloseTo(total, 6);
}
for (const [i, column] of model.columns.entries()) {
const input = columnInputFor(model, undefined, i), detail = columnRebar(input);
const segment = model.segments.find(s => column.s >= s.s0 && column.s <= s.s1)!;
expect(column.bottom + detail.zTopT / 1000).toBeCloseTo((column.top + segment.top) / 2, 3);
expect(detail.p5.filter(z => z > input.Hc - input.D).length).toBeGreaterThan(10);
expect(Math.max(...detail.p5)).toBeLessThanOrEqual(detail.zTopT);
expect(detail.p3.every(z => z > detail.lE && z < input.Hc - detail.lE)).toBe(true);
}
});
}
it('short columns merge dense ends without duplicate or out-of-range rings', () => {
const detail = columnRebar({ ...columnInputFor(buildPier({ ...loadP1(), type: '2주형', columnCount: 2 })), Hc: 1000 });
expect(detail.p3).toEqual([]);
expect(new Set(detail.p5).size).toBe(detail.p5.length);
expect(detail.p5.every(z => z >= 0 && z <= detail.zTopT)).toBe(true);
});
it('cast upper dense zone includes the coping half-height and clips at ground', () => {
const model = buildPier({ ...loadP1(), type: '3주형', columnCount: 3, foundationType: '현장타설말뚝', groundEl: 40,
pileLengthMm: 24000, castInPlaceRebar: { column: { endZoneLengthMm: 3000 } } });
const layout = castRebarLayout(model);
for (let i = 0; i < 3; i++) {
const ties = layout.bars.filter(b => b.columnIndex === i && b.name === '기둥-P5');
expect(ties.length).toBeGreaterThan(10);
expect(ties.some(b => b.pts[0][1] > model.columns[i].top)).toBe(true);
expect(ties.every(b => b.pts[0][1] >= 40)).toBe(true);
const pileTies = layout.bars.filter(b => b.columnIndex === i && b.name === '말뚝-P5');
expect(pileTies.some(b => b.pts[0][1] >= model.columns[i].top - 3 && b.pts[0][1] < 40)).toBe(true);
}
});
it('drawing defaults yield to saved overrides and leave the T-shaped defaults unchanged', () => {
const multi = buildPier({ ...loadP1(), type: '2주형', columnCount: 2 });
expect(multi.copingRebar).toMatchObject({ spacingS: 300, sideSpacing: 200, c14UpperSpacing: 600 });
const edited = buildPier({ ...loadP1(), type: '2주형', columnCount: 2, copingRebar: { spacingS: 275 } });
expect(edited.copingRebar.spacingS).toBe(275);
expect(buildPier(loadP1()).copingRebar.spacingS).toBe(250);
expect(() => multicolCopingBars({ ...multi, copingRebar: { ...multi.copingRebar, sideSpacing: 0 } })).toThrow('양수');
});
});