feat(rebar): piece lengths for C9–C13 and P1·P2 so the 재료표 has no missing bars

A row can now carry pieces (length, count). C9, C10, C11 and C9·C11 사이 have U and 삿갓 pieces per position (3 loops each);
C12 and C13 have 일자 and ㄷ pieces per bar; P1 and P2 have their splice pieces, each counting all the bars. The
재료표 totals from the pieces, so no bar is left out. 개수 stays the 철근표 count; the piece count is kept beside it. The
삿갓 length h − 100 is an assumption (Q-K5) and is noted on the ④ screen.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-23 10:27:17 +09:00
1 parent 99c71b7277
commit e999b11a21
6 files changed
+98 -23

No files matched your search

+16 -6
View File
@@ -19,9 +19,13 @@ export interface QuantityLine {
name: string;
size: string;
member: RebarRow['member'];
/** 개수 as in the 철근표 row (bars; for P1 each bar is several pieces). */
count: number;
/** Length of one bar (mm). */
length: number;
/** Pieces in the row: the sum of the piece counts (C9 108 = 18 positions × 6 pieces). */
pieceCount: number;
/** Length of one bar (mm), or null when the row has several lengths (see pieces). */
length: number | null;
pieces: { length: number; count: number }[];
/** Total length (m) = count × length. */
totalM: number;
kgPerM: number;
@@ -67,15 +71,21 @@ export function buildQuantity(rows: RebarRow[], material: Material): Quantity {
continue;
}
if (!isBarSize(r.size)) continue;
if (r.length === null) {
// A row with several lengths carries pieces; a row with one length is one piece (count bars of that length).
const pieces = r.pieces ?? (r.length === null ? null : [{ length: r.length, count: r.count }]);
if (!pieces) {
missing.push(r.name);
continue;
}
const grade = fyOf(material, r.size);
const dMm = NOMINAL_DIAMETER[r.size];
const kgPerM = unitWeight(dMm);
const totalM = (r.count * r.length) / 1000;
const line: QuantityLine = { name: r.name, size: r.size, member: r.member, count: r.count, length: r.length, totalM, kgPerM, kg: totalM * kgPerM, shape: r.shape };
const pieceCount = pieces.reduce((s, p) => s + p.count, 0);
const totalM = pieces.reduce((s, p) => s + (p.count * p.length) / 1000, 0);
const line: QuantityLine = {
name: r.name, size: r.size, member: r.member, count: r.count, pieceCount,
length: pieces.length === 1 ? pieces[0].length : null, pieces, totalM, kgPerM, kg: totalM * kgPerM, shape: r.shape,
};
const diameters = byGrade.get(grade) ?? new Map<string, QuantityDiameter>();
const d = diameters.get(r.size) ?? { size: r.size, dMm, lines: [], totalM: 0, kg: 0 };
d.lines.push(line);
@@ -103,7 +113,7 @@ export function quantityRows(q: Quantity): (string | number)[][] {
for (const g of q.groups) {
for (const d of g.diameters) {
for (const l of d.lines) {
out.push([`SD${g.grade}`, l.name, l.shape, l.size, l.count, l.length, round(l.totalM), round(l.kgPerM, 3), round(l.kg)]);
out.push([`SD${g.grade}`, l.name, l.shape, l.size, l.count, l.length ?? '여러 길이', round(l.totalM), round(l.kgPerM, 3), round(l.kg)]);
}
out.push(['', `${d.size} 소계`, '', d.size, '', '', round(d.totalM), '', round(d.kg)]);
}
+18 -8
View File
@@ -15,7 +15,10 @@ export interface RebarRow {
name: string;
size: string;
count: number;
/** One length (mm) when the row has a single length; null when the row has several (see pieces). */
length: number | null;
/** Lengths of the bars in this row, as pieces: count bars of length (mm). Their sum is the row's total (재료표). */
pieces?: { length: number; count: number }[];
note: string;
/** 간격 (설계 간격, mm or text), 실간격 (cycle 묶음 기준), 위치, 방향, 형상, 근거. */
spacing: string;
@@ -170,11 +173,16 @@ export function rebarSummary(
};
const shearNote = (label: string, xs: number[]) =>
`${label}, 하부 U ${uRange(xs)}, 삿갓 ${capRange(xs)} (l_cap 추천안, Q-K5), 조각 ${xs.length * PIECES_PER_POSITION}`;
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) });
// Each position has three 폐합, each with one 하부 U and one 삿갓 (6 pieces). Pieces: U and 삿갓 lengths at each position.
const loopPieces = (xs: number[]) => xs.flatMap((x) => [
{ length: shearULength(x, L), count: 3 },
{ length: capLength(x, L), count: 3 },
]);
rows.push({ member: 'coping', name: 'C9', size: 'H25', count: shear.c9.length, length: null, pieces: loopPieces(shear.c9), note: shearNote('전단 좌 @250', shear.c9) });
rows.push({ member: 'coping', name: 'C10', size: 'H25', count: shear.c10.length, length: null, pieces: loopPieces(shear.c10), note: shearNote('전단 Level @300', shear.c10) });
rows.push({ member: 'coping', name: 'C11', size: 'H25', count: shear.c11.length, length: null, pieces: loopPieces(shear.c11), 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}` });
rows.push({ member: 'coping', name: 'C9·C11 사이', size: 'H25', count: shear.between.length, length: null, pieces: loopPieces(shear.between), 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' });
@@ -183,8 +191,10 @@ export function rebarSummary(
const side = copingSideBars(model.copingL * 1000, 41.288, 42.538);
const hook = model.copingB * 1000 - 2 * 127 + 2 * SIDE_HOOK_LEG; // ㄷ body across the width + two legs
const c12Range = side.c12.length ? `${Math.round(side.c12[0].straight)}–${Math.round(side.c12[side.c12.length - 1].straight)}` : '—';
rows.push({ member: 'coping', name: 'C12', size: 'H25', count: 4 * side.c12.length, length: null, note: `측면, 경사 하면에서 끊김, 일자 ${c12Range} (${2 * side.c12.length}) · ㄷ ${Math.round(hook)} (${2 * side.c12.length})` });
rows.push({ member: 'coping', name: 'C13', size: 'H25', count: 4 * side.c13.length, length: side.c13.length ? side.c13[0].straight : null, note: `측면, 끝면까지, 일자 (${2 * side.c13.length}) · ㄷ ${Math.round(hook)} (${2 * side.c13.length})` });
// Per bar: 2 일자 (straight) and 2 ㄷ (hook body + legs), so 4 bars per row position.
const sidePieces = (bars: { straight: number }[]) => bars.flatMap((b) => [{ length: b.straight, count: 2 }, { length: hook, count: 2 }]);
rows.push({ member: 'coping', name: 'C12', size: 'H25', count: 4 * side.c12.length, length: null, pieces: sidePieces(side.c12), note: `측면, 경사 하면에서 끊김, 일자 ${c12Range} (${2 * side.c12.length}) · ㄷ ${Math.round(hook)} (${2 * side.c12.length})` });
rows.push({ member: 'coping', name: 'C13', size: 'H25', count: 4 * side.c13.length, length: null, pieces: sidePieces(side.c13), note: `측면, 끝면까지, 일자 (${2 * side.c13.length}) · ㄷ ${Math.round(hook)} (${2 * side.c13.length})` });
// C14 / C15 / C16 are derived from the model (코핑철근.md §7–8); the document's 38 / 11 / 22 and 3852 / 4070 / 4069 are checked in the tests.
const L14 = model.copingL * 1000;
const c14 = c14Bars(L14, model.copingRefTop, [...side.c12, ...side.c13], (x) => undersideAt(L14, x, model.endBottom, model.levelBottom));
@@ -197,8 +207,8 @@ export function rebarSummary(
// Column (기둥철근.md · plan C4), P1 defaults
const col = columnRebar(COLUMN_P1, mColumn);
rows.push({ member: 'column', name: 'P1', size: 'S32', count: col.mainP1, length: null, note: `외측 r ${col.r1}, 마디 ${col.p1Pieces.join(' / ')} (첫 마디에 갈고리 ${col.p1Hook}, 기둥철근.md §3.2)` });
rows.push({ member: 'column', name: 'P2', size: 'S32', count: col.mainP2, length: null, note: `내측 r ${col.r2}, 마디 ${col.p2Pieces.join(' / ')}` });
rows.push({ member: 'column', name: 'P1', size: 'S32', count: col.mainP1, length: null, pieces: col.p1Pieces.map((length) => ({ length, count: col.mainP1 })), note: `외측 r ${col.r1}, 마디 ${col.p1Pieces.join(' / ')} (첫 마디에 갈고리 ${col.p1Hook}, 기둥철근.md §3.2)` });
rows.push({ member: 'column', name: 'P2', size: 'S32', count: col.mainP2, length: null, pieces: col.p2Pieces.map((length) => ({ length, count: col.mainP2 })), note: `내측 r ${col.r2}, 마디 ${col.p2Pieces.join(' / ')}` });
rows.push({ member: 'column', name: '주철근 커플러', size: 'D32용 (L150 Ø60)', count: col.couplers, length: 150, note: '@3000, P2는 +150 mm 상향 엇갈림' });
const tieCouplerCount = col.p5Count + col.p4Count * 2;
rows.push({ member: 'column', name: '띠철근 커플러', size: 'D19용 (L100 Ø40)', count: tieCouplerCount, length: 100, note: `단부 P5 1개(${col.p5Count}) · 기초안 P4 2개(${col.p4Count * 2}) 폐합용` });