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:
ypshimandClaude Haiku 5.5 committed 2026-10-22 12:20:49 +09:00
1 parent 8ac5d2973c
commit 13318e9393
3 files changed
+139 -19

No files matched your search

+41
View File
@@ -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
View File
@@ -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' });
}