feat(core): rebar for 2·3주형 — shear bars per column, column bars per column (plan)
- Coping §9: C10 at each column centre (@300, k_max as for one column), C9 / C11 outside, and the gaps between columns filled @250 from both C10 groups with the remaining length shared evenly (추천안, Q-K2). One column gives the T형 result. - Column bars and couplers repeat per column (P1 230 × 2 for 2주형, × 3 for 3주형). - Footings: each footing's bars are summed per name; the longest length governs. - C14 is kept as the T형 layout with an RB-M01 note (per-column rule open, Q-K11). - Tests: counts for 2주형 and 3주형; T형 unchanged; too-close columns refused. - Not yet: the ③ shear-segment table for multi columns (still single column), the C14 per column, and a drawing for multi columns. Not verified in a browser. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8ac5d2973c
commit
13318e9393
3 files changed
+139
-19
No files matched your search
@@ -131,3 +131,44 @@ export function shearSegments(coping: number): { segment: string; from: number;
|
||||
seg('우 경사', b.c11, 'C11', COPING_REBAR.spacingS),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Shear bars for a multi-column coping (코핑철근.md §9, 다주형 확장 원칙).
|
||||
* C10: each column's centre, @300 out to the first position beyond its Level edge.
|
||||
* C9 / C11: outward from the outermost C10 on each end, as for one column.
|
||||
* Between columns: @250 in from both C10 groups, the remaining length shared evenly (추천안, Q-K2).
|
||||
* With one column this is the same as shearBars.
|
||||
*/
|
||||
export function shearBarsMulti(coping: number, columns: number[]): ShearBars & { between: number[] } {
|
||||
if (columns.length <= 1) {
|
||||
const one = shearBars(coping, columns[0] ?? coping / 2);
|
||||
return { ...one, between: [] };
|
||||
}
|
||||
const { spacingL, spacingS, levelWidth } = COPING_REBAR;
|
||||
const kMax = Math.floor(levelWidth / 2 / spacingL) + 1;
|
||||
const groups = columns.map((xc) =>
|
||||
Array.from({ length: 2 * kMax + 1 }, (_, i) => Math.round(xc + (i - kMax) * spacingL)),
|
||||
);
|
||||
for (let i = 1; i < groups.length; i++) {
|
||||
if (groups[i][0] <= groups[i - 1][groups[i - 1].length - 1]) {
|
||||
throw new Error('기둥 간격이 좁아 C10 구간이 겹칩니다. 다주형 코핑 철근을 만들 수 없습니다.');
|
||||
}
|
||||
}
|
||||
const c10 = groups.flat();
|
||||
const left = groups[0][0];
|
||||
const right = groups[groups.length - 1][groups[groups.length - 1].length - 1];
|
||||
const n9 = Math.floor((left - COPING_END_GAP) / spacingS);
|
||||
if (n9 < 0) throw new Error('RB-K01: C10 바깥 구간이 끝 여유보다 짧아 C9를 놓을 수 없습니다.');
|
||||
const c9 = Array.from({ length: n9 }, (_, j) => Math.round(left - (n9 - j) * spacingS));
|
||||
const c11 = Array.from({ length: n9 }, (_, j) => Math.round(right + (j + 1) * spacingS));
|
||||
const between: number[] = [];
|
||||
for (let i = 1; i < groups.length; i++) {
|
||||
const a = groups[i - 1][groups[i - 1].length - 1];
|
||||
const b = groups[i][0];
|
||||
const gap = b - a;
|
||||
const pitches = Math.floor(gap / spacingS);
|
||||
for (let j = 1; j < pitches; j++) between.push(Math.round(a + (gap * j) / pitches));
|
||||
}
|
||||
const endGap = Math.min(left - c9[0], coping - c11[c11.length - 1]);
|
||||
return { c10, c9, c11, between, endGap };
|
||||
}
|
||||
+53
-19
@@ -1,7 +1,7 @@
|
||||
import type { PierModel } from '../geometry';
|
||||
import { bearingBlockRebar, totals } from './bearing-block';
|
||||
import { bottomPiece, topBarLength, widthLayout } from './coping-main';
|
||||
import { shearBars, sideRows, C14_COUNTS, c14Total, STEP_BAR_COUNTS, shearULength, SIDE_LENGTHS, STEP_LENGTHS, capLength, PIECES_PER_POSITION } from './coping';
|
||||
import { shearBars, shearBarsMulti, sideRows, C14_COUNTS, c14Total, STEP_BAR_COUNTS, shearULength, SIDE_LENGTHS, STEP_LENGTHS, capLength, PIECES_PER_POSITION } from './coping';
|
||||
import { columnRebar, COLUMN_P1 } from './column';
|
||||
import { bottomBars, crossBars, sideBars, assemblyBars, shearB9 } from './footing';
|
||||
import { DEFAULT_MATERIAL, isAssumedMaterial, Material, splitPieces } from './common';
|
||||
@@ -74,6 +74,7 @@ const META: Record<string, Pick<RebarRow, 'spacing' | 'actual' | 'position' | 'd
|
||||
'B10-2': { spacing: '200', actual: '200', position: '길이 L 양끝 면, 다리 안쪽', direction: '교각축 직각', shape: '양끝 90° 갈고리', basis: 'calc' },
|
||||
B11: { spacing: '500 (4s)', actual: '500', position: '조립 단 (B10-2 높이)', direction: '교각축 직각', shape: 'U 양끝 아래로', basis: 'calc' },
|
||||
B12: { spacing: '500 (4s)', actual: '500', position: '조립 단 (B10-1 높이)', direction: '교각축', shape: 'U 양끝 아래로', basis: 'calc' },
|
||||
'C9·C11 사이': { spacing: '250', actual: '250', position: '기둥 사이 (양쪽 C10에서 안쪽)', direction: '연직', shape: '폐합 (추천안, Q-K2)', basis: 'assumed' },
|
||||
B9: { spacing: '500 (4s)', actual: '250 (2s 엇갈림)', position: '연직, B5–B2·B4 감쌈', direction: '연직', shape: '양끝 135° 갈고리', basis: 'assumed' },
|
||||
};
|
||||
|
||||
@@ -88,6 +89,26 @@ export const STANDARD_BAR = 12000;
|
||||
* 철근표 for one pier (③ screen v1): bearing blocks, coping, column, footing. Built from the generated model.
|
||||
* Values come from the document rules in src/core/rebar/*. B9 (전단철근) is not computed yet.
|
||||
*/
|
||||
interface FootingBar {
|
||||
name: string;
|
||||
size: string;
|
||||
count: number;
|
||||
length: number;
|
||||
note: string;
|
||||
}
|
||||
|
||||
/** The footing bars of one footing (B1–B12 and B9), with the notes used in the 철근표. */
|
||||
function footingBarList(Lf: number, Wf: number, Hf: number): FootingBar[] {
|
||||
const out: FootingBar[] = [];
|
||||
for (const b of bottomBars(Lf, Wf, Hf)) out.push({ ...b, note: '교각축' });
|
||||
for (const b of crossBars(Lf, Wf, Hf)) out.push({ ...b, note: '교각축 직각' });
|
||||
for (const b of sideBars(Lf, Wf)) out.push({ ...b, note: '측면 수평' });
|
||||
for (const b of assemblyBars(Lf, Wf)) out.push({ ...b, note: '조립 수평' });
|
||||
const b9 = shearB9(Lf, Wf, Hf);
|
||||
out.push({ name: 'B9', size: 'H16', count: b9.count, length: b9.length, note: `전단 @500 엇갈림, ${b9.rows}행 (위치·갈고리는 Q-B18·B19 제안)` });
|
||||
return out;
|
||||
}
|
||||
|
||||
type RawRow = Omit<RebarRow, 'spacing' | 'actual' | 'position' | 'direction' | 'shape' | 'basis'>;
|
||||
|
||||
export function rebarSummary(model: PierModel, m: Material = DEFAULT_MATERIAL): { rows: RebarRow[]; warnings: RebarWarning[] } {
|
||||
@@ -114,7 +135,9 @@ export function rebarSummary(model: PierModel, m: Material = DEFAULT_MATERIAL):
|
||||
const bottom = bottomPiece(m);
|
||||
rows.push({ member: 'coping', name: 'C7', size: 'H29', count: perRow, length: bottom.piece, note: '하면, 2조각 겹침이음' });
|
||||
rows.push({ member: 'coping', name: 'C8', size: 'H29', count: perRow, length: bottom.piece, note: '하면, 2조각 겹침이음' });
|
||||
const shear = shearBars(L);
|
||||
const colS = model.columns.map((c) => c.s * 1000);
|
||||
const nCol = Math.max(1, colS.length);
|
||||
const shear = nCol > 1 ? shearBarsMulti(L, colS) : { ...shearBars(L), between: [] as number[] };
|
||||
const uRange = (xs: number[]) => {
|
||||
const u = xs.map((x) => Math.round(shearULength(x, L)));
|
||||
return `${Math.min(...u)}–${Math.max(...u)}`;
|
||||
@@ -128,6 +151,12 @@ export function rebarSummary(model: PierModel, m: Material = DEFAULT_MATERIAL):
|
||||
rows.push({ member: 'coping', name: 'C9', size: 'H25', count: shear.c9.length, length: null, note: shearNote('전단 좌 @250', shear.c9) });
|
||||
rows.push({ member: 'coping', name: 'C10', size: 'H25', count: shear.c10.length, length: null, note: shearNote('전단 Level @300', shear.c10) });
|
||||
rows.push({ member: 'coping', name: 'C11', size: 'H25', count: shear.c11.length, length: null, note: shearNote('전단 우 @250', shear.c11) });
|
||||
if (shear.between.length > 0) {
|
||||
rows.push({ member: 'coping', name: 'C9·C11 사이', size: 'H25', count: shear.between.length, length: null, note: `기둥 사이, 양쪽 C10에서 안쪽으로 (추천안, Q-K2), 조각 ${shear.between.length * PIECES_PER_POSITION}` });
|
||||
}
|
||||
if (nCol > 1) {
|
||||
warnings.push({ code: 'RB-M01', message: `다주형 ${nCol}기둥: C14는 T형 기준으로 두었습니다 (기둥마다 반복 규칙 미정, Q-K11).`, target: 'coping' });
|
||||
}
|
||||
const side = sideRows(41.288, 42.538);
|
||||
rows.push({ member: 'coping', name: 'C12', size: 'H25', count: side.c12.length, length: null, note: `측면, 경사 하면에서 끊김, 일자 ${SIDE_LENGTHS.C12[0]}–${SIDE_LENGTHS.C12[4]}` });
|
||||
rows.push({ member: 'coping', name: 'C13', size: 'H25', count: side.c13.length, length: SIDE_LENGTHS.C13, note: '측면, 끝면까지' });
|
||||
@@ -145,28 +174,33 @@ export function rebarSummary(model: PierModel, m: Material = DEFAULT_MATERIAL):
|
||||
rows.push({ member: 'column', name: 'P4', size: 'H19', count: col.p4Count, length: col.p4SemicircleLength * 2, note: '확대기초 안, 반원 2개 (길이는 반원 2개 합) · B11 간섭 무시' });
|
||||
rows.push({ member: 'column', name: 'P6', size: 'H16', count: col.p6Count, length: col.p6Length, note: '크로스타이, 8단 × 4' });
|
||||
|
||||
// Column bars and couplers repeat for each column (기둥철근 is per column; 다주형은 기둥마다).
|
||||
for (const r of rows) if (r.member === 'column') r.count *= nCol;
|
||||
// Footing (확대기초철근 (1).md · plan C5). Only when the footing exists (not 현장타설말뚝).
|
||||
const f = model.footings[0];
|
||||
if (f) {
|
||||
const Lf = f.length * 1000;
|
||||
const Wf = f.width * 1000;
|
||||
const Hf = (f.top - f.bottom) * 1000;
|
||||
// Bars longer than the standard length are split in two (추천안, Q-B6). The count is still the bar count.
|
||||
const addBar = (b: { name: string; size: string; count: number; length: number }, note: string) => {
|
||||
if (model.footings.length > 0) {
|
||||
// Each footing (분리 or 일체) gives the same bar set. Several footings add their counts per bar name;
|
||||
// the length is the longest of them (the largest footing governs the bar).
|
||||
const agg = new Map<string, FootingBar>();
|
||||
for (const f of model.footings) {
|
||||
for (const b of footingBarList(f.length * 1000, f.width * 1000, (f.top - f.bottom) * 1000)) {
|
||||
const cur = agg.get(b.name);
|
||||
if (cur) {
|
||||
cur.count += b.count;
|
||||
cur.length = Math.max(cur.length, b.length);
|
||||
} else {
|
||||
agg.set(b.name, { ...b });
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const b of agg.values()) {
|
||||
const split = splitPieces(b.length, 25.4, m.fyS, m.fck, STANDARD_BAR);
|
||||
if (split) {
|
||||
rows.push({ member: 'footing', name: b.name, size: b.size, count: b.count, length: split.pieceLength, note: `${note}, 2조각 이음 (추천안, 겹침 ${Math.round(split.lap)})` });
|
||||
if (split && b.length > STANDARD_BAR && b.name !== 'B9') {
|
||||
rows.push({ member: 'footing', name: b.name, size: b.size, count: b.count, length: split.pieceLength, note: `${b.note}, 2조각 이음 (추천안, 겹침 ${Math.round(split.lap)})` });
|
||||
warnings.push({ code: 'RB-F08', message: `${b.name} 길이 ${Math.round(b.length)} mm > 정척 ${STANDARD_BAR} mm: 2조각 이음을 추천안으로 적용 (Q-B6).`, target: 'footing', bar: b.name });
|
||||
} else {
|
||||
rows.push({ member: 'footing', name: b.name, size: b.size, count: b.count, length: b.length, note });
|
||||
rows.push({ member: 'footing', name: b.name, size: b.size, count: b.count, length: b.length, note: b.note });
|
||||
}
|
||||
};
|
||||
for (const b of bottomBars(Lf, Wf, Hf)) addBar(b, '교각축');
|
||||
for (const b of crossBars(Lf, Wf, Hf)) addBar(b, '교각축 직각');
|
||||
for (const b of sideBars(Lf, Wf)) rows.push({ member: 'footing', name: b.name, size: b.size, count: b.count, length: b.length, note: '측면 수평' });
|
||||
for (const b of assemblyBars(Lf, Wf)) rows.push({ member: 'footing', name: b.name, size: b.size, count: b.count, length: b.length, note: '조립 수평' });
|
||||
const b9 = shearB9(Lf, Wf, Hf);
|
||||
rows.push({ member: 'footing', name: 'B9', size: 'H16', count: b9.count, length: b9.length, note: `전단 @500 엇갈림, ${b9.rows}행 (위치·갈고리는 Q-B18·B19 제안)` });
|
||||
}
|
||||
} else {
|
||||
warnings.push({ code: 'RB-F00', message: '현장타설말뚝은 확대기초가 없어 확대기초 철근을 만들지 않습니다.', target: 'footing' });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildPier } from '../src/core/geometry';
|
||||
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).
|
||||
const two = buildPier({ ...loadP1(), type: '2주형', columnCount: 2, copingLengthMm: 12000 });
|
||||
const three = buildPier({ ...loadP1(), type: '3주형', columnCount: 3, copingLengthMm: 12000 });
|
||||
const count = (rows: ReturnType<typeof rebarSummary>['rows'], name: string) => rows.find((r) => r.name === name)!.count;
|
||||
|
||||
describe('다주형 철근 (2주형·3주형, 코핑철근.md §9)', () => {
|
||||
it('2주형: C10 2 × 9 = 18; C9 and C11 3 each outside; 18 between (추천안, Q-K2)', () => {
|
||||
const rows = rebarSummary(two).rows;
|
||||
expect(count(rows, 'C10')).toBe(18);
|
||||
expect(count(rows, 'C9')).toBe(3);
|
||||
expect(count(rows, 'C11')).toBe(3);
|
||||
expect(count(rows, 'C9·C11 사이')).toBe(18);
|
||||
});
|
||||
|
||||
it('3주형: C10 3 × 9 = 27; two gaps of 3 between', () => {
|
||||
const rows = rebarSummary(three).rows;
|
||||
expect(count(rows, 'C10')).toBe(27);
|
||||
expect(count(rows, 'C9·C11 사이')).toBe(6);
|
||||
});
|
||||
|
||||
it('column bars repeat per column: P1 230 × 2 and 230 × 3', () => {
|
||||
expect(count(rebarSummary(two).rows, 'P1')).toBe(460);
|
||||
expect(count(rebarSummary(three).rows, 'P1')).toBe(690);
|
||||
});
|
||||
|
||||
it('a multi-column pier gets the RB-M01 note about C14', () => {
|
||||
expect(rebarSummary(two).warnings.map((w) => w.code)).toContain('RB-M01');
|
||||
});
|
||||
|
||||
it('the single column (T형) has no between row and no RB-M01', () => {
|
||||
const t = rebarSummary(buildPier(loadP1()));
|
||||
expect(t.rows.find((r) => r.name === 'C9·C11 사이')).toBeUndefined();
|
||||
expect(t.warnings.map((w) => w.code)).not.toContain('RB-M01');
|
||||
});
|
||||
|
||||
it('shear bars for a columns too close to share a Level are refused', () => {
|
||||
expect(() => shearBarsMulti(12000, [3000, 3500])).toThrow('겹칩니다');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user