feat(shape): 다주형 코핑 하면을 오목형으로 (Level D+200 · 오목 500 · 헌치 1500)

spec §3·§4.4(2026-11-06 개정)와 3주형형상배근.md §2.2(2주형 DWG 실측)를 따른다.
- 기둥 사이 하면: Level 끝에서 헌치 수평 1500 동안 500 올라가 평탄, 다음 기둥 앞에서 내려옴
  (이전 Level + 300, 헌치 300 × 300 대체)
- 다주형 Level 폭 기본값 = D + 200 (입력값이 있으면 입력 우선). T형은 2100 유지
- 오목 평탄부가 없으면 RB-M01 오류
- 3주형 테스트는 오목형이 성립하는 20 m 코핑으로 바꿈 (P1 길이 12 m는 RB-M01)
- 2주형 DWG 치수(L 24200, D 3000) 하면 꺾임점 테스트 추가

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Opus 5.5 committed 2026-11-06 14:11:15 +09:00
1 parent 42316104df
commit 370bd3d7cd
8 files changed
+79 -34

No files matched your search

+2 -1
View File
@@ -22,7 +22,8 @@ describe('cast-in-place independent rendering and incomplete quantities', () =>
for (const [type, columnCount] of [['T형', 1], ['2주형', 2], ['3주형', 3]] as const) {
for (const foundationType of ['직접기초', '말뚝기초', '현장타설말뚝'] as const) {
it(`${type} / ${foundationType}: concrete, rebar and summary remain usable`, () => {
const m = buildPier({ ...loadP1(), type, columnCount, foundationType });
// 3주형 needs a coping long enough for the 오목형 spans (RB-M01 otherwise).
const m = buildPier({ ...loadP1(), ...(columnCount === 3 ? { copingLengthMm: 20000 } : {}), type, columnCount, foundationType });
const concrete = buildPierScene(m);
expect(concrete.parts.coping.children.length).toBeGreaterThan(0);
const rebar = buildRebar3D(m);
+1 -1
View File
@@ -100,7 +100,7 @@ describe('dedicated cast-in-place physical reinforcement', () => {
expect(result.bars.some(b=>b.name==='말뚝-P1')).toBe(true);
});
it('repeats physical pieces per actual column without multiplying piece counts again', () => {
const one=layout(), three=layout({type:'3주형',columnCount:3});
const one=layout(), three=layout({type:'3주형',columnCount:3,copingLengthMm:20000});
expect(three.bars.filter(b=>b.kind==='main')).toHaveLength(one.bars.filter(b=>b.kind==='main').length*3);
expect(new Set(three.bars.map(b=>b.columnIndex)).size).toBe(3);
});
+7 -4
View File
@@ -7,10 +7,13 @@ import { castRebarLayout } from '../src/core/rebar/cast-layout';
import { buildRebar3D } from '../src/view/rebar3d';
import { loadP1 } from './helpers';
// 3주형 on the P1 sheet needs a longer coping: with L 12.166 m the columns are 3650 apart and leave no flat 오목 part (RB-M01).
const lengthFor = (columnCount: number) => (columnCount === 3 ? { copingLengthMm: 20000 } : {});
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 model = buildPier({ ...loadP1(), ...lengthFor(columnCount), 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);
@@ -20,7 +23,7 @@ describe('multi-column drawing corrections', () => {
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 model = buildPier({ ...loadP1(), ...lengthFor(columnCount), type, columnCount, foundationType, groundEl: 30, pileLengthMm: 24000 });
const bars = multicolCopingBars(model);
expect(bars.length).toBeGreaterThan(100);
const polygon = model.copingParts[0].profile;
@@ -54,7 +57,7 @@ describe('multi-column drawing corrections', () => {
}, 15000);
}
it(`${type}: physical column pieces and local confinement height agree`, () => {
const model = buildPier({ ...loadP1(), type, columnCount });
const model = buildPier({ ...loadP1(), ...lengthFor(columnCount), type, columnCount });
const rows = rebarSummary(model).rows;
for (const name of ['P1', 'P2']) {
const row = rows.find(r => r.name === name)!;
@@ -83,7 +86,7 @@ describe('multi-column drawing corrections', () => {
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,
const model = buildPier({ ...loadP1(), copingLengthMm: 20000, type: '3주형', columnCount: 3, foundationType: '현장타설말뚝', groundEl: 40,
pileLengthMm: 24000, castInPlaceRebar: { column: { endZoneLengthMm: 3000 } } });
const layout = castRebarLayout(model);
for (let i = 0; i < 3; i++) {
+6 -4
View File
@@ -4,9 +4,10 @@ import { shearBarsMulti } from '../src/core/rebar/coping';
import { rebarSummary } from '../src/core/rebar/summary';
import { loadP1 } from './helpers';
// 2주형 and 3주형 on the P1 sheet with a 12 m coping (columns at L/5, 4L/5 and L/5, L/2, 4L/5).
// 2주형 on the P1 sheet with a 12 m coping (columns at L/5, 4L/5). 3주형 needs 20 m (columns 4 / 10 / 16): with 12 m the
// columns are 3600 apart and leave no flat 오목 part between the 1500 haunches (RB-M01).
const two = buildPier({ ...loadP1(), type: '2주형', columnCount: 2, copingLengthMm: 12000 });
const three = buildPier({ ...loadP1(), type: '3주형', columnCount: 3, copingLengthMm: 12000 });
const three = buildPier({ ...loadP1(), type: '3주형', columnCount: 3, copingLengthMm: 20000 });
const count = (rows: ReturnType<typeof rebarSummary>['rows'], name: string) => rows.find((r) => r.name === name)!.count;
describe('다주형 철근 (2주형·3주형, 코핑철근.md §9)', () => {
@@ -18,10 +19,11 @@ describe('다주형 철근 (2주형·3주형, 코핑철근.md §9)', () => {
expect(count(rows, 'C9·C11 사이')).toBe(15 * 6);
});
it('3주형: C10 27 positions and two gaps of three, each with six physical pieces', () => {
it('3주형: C10 27 positions and two gaps of eleven, each with six physical pieces', () => {
const rows = rebarSummary(three).rows;
// Per column 9 positions (±4 @300); between groups 8.8 − 5.2 = 3.6 m → floor(3600 / 300) = 12 bays → 11 positions.
expect(count(rows, 'C10')).toBe(27 * 6);
expect(count(rows, 'C9·C11 사이')).toBe(6 * 6);
expect(count(rows, 'C9·C11 사이')).toBe(22 * 6);
});
it('column bars repeat per column: P1 230 × 2 and 230 × 3', () => {
+35 -12
View File
@@ -82,27 +82,50 @@ describe('foundation types', () => {
expect(m.piles[0].diameter).toBeCloseTo(0.6096, 6);
});
it('2주형: underside between columns rises 300 above Level; column tops and T형 stay at Level', () => {
it('2주형: underside between columns is 오목형 (500 above Level); column tops and T형 stay at Level', () => {
const t = buildPier(loadP1());
const raised = t.levelBottom + 0.3;
const raised = t.levelBottom + 0.5;
expect(t.copingParts[0].profile.some(([, z]) => Math.abs(z - raised) < 1e-9)).toBe(false);
expect(t.copingGeom.levelWidth).toBe(2100);
const two = buildPier({ ...loadP1(), type: '2주형', columnCount: 2 });
expect(two.copingParts[0].profile.some(([, z]) => Math.abs(z - raised) < 1e-9)).toBe(true);
const outlineLow = Math.min(...two.copingParts[0].profile.map(([, z]) => z));
expect(outlineLow).toBeCloseTo(two.levelBottom, 9);
expect(two.columns.every((c) => c.top === two.levelBottom)).toBe(true);
// Level width default for 2·3주형 = D + 200 (D 2000 when absent).
expect(two.copingGeom.levelWidth).toBe(2200);
});
it('3주형 builds with two gaps of haunches', () => {
const m = buildPier({ ...loadP1(), type: '3주형', columnCount: 3 });
const raised = m.copingParts[0].profile.filter(([, z]) => Math.abs(z - (m.levelBottom + 0.3)) < 1e-9);
it('3주형 builds with two 오목형 spans', () => {
const m = buildPier({ ...loadP1(), type: '3주형', columnCount: 3, copingLengthMm: 20000 });
const raised = m.copingParts[0].profile.filter(([, z]) => Math.abs(z - (m.levelBottom + 0.5)) < 1e-9);
expect(raised).toHaveLength(4);
});
it('RB-M01: columns too close for two 1500 haunches → error', () => {
// P1 length 12.166 m as 3주형: columns L/5 · L/2 · 4L/5 are 3650 apart, minus Level 2200 = 1450 < 3000.
expect(() => buildPier({ ...loadP1(), type: '3주형', columnCount: 3 })).toThrow(/RB-M01/);
});
it('2주형 DWG dimensions (L 24200, D 3000): Level 3200, haunch 1500 rising 500 (3주형형상배근.md §2.2)', () => {
const m = buildPier({ ...loadP1(), type: '2주형', columnCount: 2, copingLengthMm: 24200, columnDiameterMm: 3000, copingH1Mm: 1500, copingH2Mm: 1500 });
expect(m.copingGeom.levelWidth).toBe(3200);
const profile = m.copingParts[0].profile;
const has = (x: number, z: number) => profile.some(([s, el]) => Math.abs(s - x) < 1e-9 && Math.abs(el - z) < 1e-9);
const [c1, c2] = m.columns.map((c) => c.s);
// Level ends at column ± 1.6, the haunch reaches Level + 0.5 another 1.5 inward, then the flat 오목 part.
expect(has(c1 + 1.6, m.levelBottom)).toBe(true);
expect(has(c1 + 3.1, m.levelBottom + 0.5)).toBe(true);
expect(has(c2 - 3.1, m.levelBottom + 0.5)).toBe(true);
expect(has(c2 - 1.6, m.levelBottom)).toBe(true);
// Ends: taper from the end height (h1 1500 below the top) down to Level.
expect(has(0, m.levelBottom + 1.5)).toBe(true);
});
for (const type of ['2주형', '3주형'] as const) {
for (const dims of [{}, { copingLengthMm: 15000, copingH1Mm: 1200, copingH2Mm: 1000, levelWidthMm: 2400 }]) {
it(`${type}: raised spans and 300 × 300 haunches follow every column, dimensions ${JSON.stringify(dims)}`, () => {
for (const dims of [type === '2주형' ? {} : { copingLengthMm: 20000 }, { copingLengthMm: 22000, copingH1Mm: 1200, copingH2Mm: 1000, levelWidthMm: 2400 }]) {
it(`${type}: 오목형 spans and 1500 haunches follow every column, dimensions ${JSON.stringify(dims)}`, () => {
const m = buildPier({ ...loadP1(), ...dims, type, columnCount: type === '2주형' ? 2 : 3 });
const profile = m.copingParts[0].profile;
const half = m.copingGeom.levelWidth / 2000;
@@ -115,8 +138,8 @@ describe('foundation types', () => {
for (let i = 0; i < m.columns.length - 1; i++) {
const left = m.columns[i].s + half;
const right = m.columns[i + 1].s - half;
expect(has(left + 0.3, m.levelBottom + 0.3)).toBe(true);
expect(has(right - 0.3, m.levelBottom + 0.3)).toBe(true);
expect(has(left + 1.5, m.levelBottom + 0.5)).toBe(true);
expect(has(right - 1.5, m.levelBottom + 0.5)).toBe(true);
}
expect(Math.min(...profile.map(([, z]) => z))).toBeCloseTo(m.levelBottom, 9);
});
@@ -171,10 +194,10 @@ describe('일체기초 (spec §4.6)', () => {
expect(m.derived.pileLayout).toBe('7@2314 × 3@2250 = 32본');
});
it('3주형 L 12000: three columns 3600 apart share one footing', () => {
const m = buildPier({ ...loadP1(), type: '3주형', columnCount: 3, copingLengthMm: 12000, foundationType: '직접기초' });
it('3주형 L 20000: three columns 6000 apart share one footing (outer distance + 10400)', () => {
const m = buildPier({ ...loadP1(), type: '3주형', columnCount: 3, copingLengthMm: 20000, foundationType: '직접기초' });
expect(m.footings).toHaveLength(1);
expect(m.footings[0].length).toBeCloseTo(17.6, 6);
expect(m.footings[0].length).toBeCloseTo(22.4, 6);
});
it('2주형 with columns 18000 apart: separate footings of 10400', () => {