feat(rebar): 현장타설 말뚝 · 다주형 코핑 형상과 철근 계산
- 현장타설 기둥·말뚝 형상(cast-in-place-shape)과 주철근·횡철근·커플러 계산(rebar/cast-in-place, cast-layout) - 다주형 코핑 철근(multicol-coping), 기둥 높이 · 양단부 연장 - 철근표 요약 · 재료 수량 · 3D 뷰 연결과 삽도(cast-rebar-svg) - 검사 기준 갱신과 회귀 테스트, 검증 스크립트 보관 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3922b330d5
commit
2e0bfb6fc8
29 files changed
+1750
-71
No files matched your search
+90
-15
@@ -1,11 +1,14 @@
|
||||
import type { PierModel } from '../geometry';
|
||||
import { barLength, bearingBlockRebar, DEFAULT_BLOCK_REBAR, totals } from './bearing-block';
|
||||
import { DEFAULT_COPING_REBAR } from './coping-settings';
|
||||
import { copingDefaults } from './coping-settings';
|
||||
import { DEFAULT_COLUMN_REBAR } from './column-settings';
|
||||
import { DEFAULT_FOOTING_REBAR, type FootingRebarSettings } from './footing-settings';
|
||||
import { bottomPiece, topBarLength, widthLayout, topSplicePieces } from './coping-main';
|
||||
import { shearBars, shearBarsMulti, copingSideBars, SIDE_HOOK_LEG, c14Bars, c14Length, stairBars, stepDevLength, undersideAt, shearULength, capLength, PIECES_PER_POSITION } from './coping';
|
||||
import { sideRowLevels } from '../coping-geom';
|
||||
import { CAST_PENDING } from './cast-in-place';
|
||||
import { castRebarLayout } from './cast-layout';
|
||||
import { multicolCopingBars } from './multicol-coping';
|
||||
import { columnRebar, columnInputFor } from './column';
|
||||
import { bottomBars, crossBars, sideBars, assemblyBars, shearB9 } from './footing';
|
||||
import { DEFAULT_MATERIAL, FckByMember, fyOf, isAssumedMaterial, Material, materialFor, splitPieces, STANDARD_BAR } from './common';
|
||||
@@ -15,7 +18,7 @@ export type Basis = 'input' | 'default' | 'calc' | 'assumed';
|
||||
|
||||
/** One line of a 철근표. length is null where the document gives no length yet. */
|
||||
export interface RebarRow {
|
||||
member: 'bearing' | 'coping' | 'column' | 'footing';
|
||||
member: 'bearing' | 'coping' | 'column' | 'footing' | 'pile';
|
||||
name: string;
|
||||
size: string;
|
||||
count: number;
|
||||
@@ -31,6 +34,9 @@ export interface RebarRow {
|
||||
direction: string;
|
||||
shape: string;
|
||||
basis: Basis;
|
||||
/** Explicit dedicated-section properties; absent for legacy rows. */
|
||||
diameterMm?: number;
|
||||
gradeMpa?: number;
|
||||
}
|
||||
|
||||
export interface RebarWarning {
|
||||
@@ -128,7 +134,7 @@ const MAIN_BARS = ['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8'];
|
||||
*/
|
||||
export function linkedSpacing(name: string, model: PierModel): Pick<RebarRow, 'spacing' | 'actual' | 'basis'> | undefined {
|
||||
const c = model.copingRebar;
|
||||
const d = DEFAULT_COPING_REBAR;
|
||||
const d = copingDefaults(model.columns.length);
|
||||
const b = model.blockRebar;
|
||||
const bd = DEFAULT_BLOCK_REBAR;
|
||||
const basis = (changed: boolean): Basis => (changed ? 'input' : 'default');
|
||||
@@ -186,11 +192,12 @@ export function rebarSummary(
|
||||
model: PierModel,
|
||||
common: Material = DEFAULT_MATERIAL,
|
||||
byMember: FckByMember = {},
|
||||
): { rows: RebarRow[]; warnings: RebarWarning[] } {
|
||||
): { rows: RebarRow[]; warnings: RebarWarning[]; incomplete: string[] } {
|
||||
const rows: RawRow[] = [];
|
||||
const warnings: RebarWarning[] = [];
|
||||
const mCoping = materialFor(common, byMember, 'coping');
|
||||
const mColumn = materialFor(common, byMember, 'column');
|
||||
const cast = model.castInput ? castRebarLayout(model, mColumn) : undefined;
|
||||
const mFooting = materialFor(common, byMember, 'footing');
|
||||
// The I02 note stays while the common values are the assumed ones, unless every fck-bearing member has its own fck.
|
||||
const ownFck = (['column', 'coping', 'footing'] as const).every((k) => byMember[k] !== undefined);
|
||||
@@ -217,6 +224,9 @@ export function rebarSummary(
|
||||
rows.push({ member: 'bearing', name: 'S2', size: 'H19', count: blk.S2, length: barLength(1200, model.blockRebar), note: `블럭 배치 분리, 블럭당 8본 · 묻힘 ${range(emb2)} (블럭 상면 − 50)` });
|
||||
|
||||
// Coping (코핑철근.md §5–§8)
|
||||
const colS = model.columns.map((c) => c.s * 1000);
|
||||
const nCol = Math.max(1, colS.length);
|
||||
if (nCol <= 1 || model.kink !== null) {
|
||||
const width = widthLayout(B, model.copingRebar);
|
||||
const perRow = width.count / 2; // cycle split, 12 per row
|
||||
// CP04 (승인 2026-10-26): the cycle split needs an even count (n + 3), so n must be odd. Warning only; the input is kept.
|
||||
@@ -236,8 +246,6 @@ export function rebarSummary(
|
||||
const bottom = bottomPiece(mCoping);
|
||||
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 colS = model.columns.map((c) => c.s * 1000);
|
||||
const nCol = Math.max(1, colS.length);
|
||||
const geom = model.copingGeom;
|
||||
const shear = nCol > 1 ? shearBarsMulti(L, colS, geom) : { ...shearBars(L, undefined, geom), between: [] as number[] };
|
||||
const uRange = (xs: number[]) => {
|
||||
@@ -262,7 +270,7 @@ export function rebarSummary(
|
||||
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' });
|
||||
warnings.push({ code: 'RB-M01', message: `다주형 ${nCol}기둥: C14는 전용 삽도의 H16·기본600 간격과 실제 하면을 사용합니다. 기둥별 반복·번호 대응 상세는 확인이 필요합니다.`, target: 'coping' });
|
||||
}
|
||||
// Per row: two 일자 (side faces) and two ㄷ (one at each end), so 4 bars per row (코핑철근.md §6.2).
|
||||
const levels = sideRowLevels(model.levelBottom, model.endBottom, model.copingRefTop);
|
||||
@@ -282,21 +290,66 @@ export function rebarSummary(
|
||||
const stairs = stairBars(model.copingL * 1000, model.copingB * 1000, model.copingRefTop, mCoping, 2 * model.copingRebar.mainSpacing);
|
||||
rows.push({ member: 'coping', name: 'C15', size: 'H19', count: stairs.c15.length, length: stairs.c15[0].length, note: '계단부, 가장 높은 구간 (거꾸로 U)' });
|
||||
rows.push({ member: 'coping', name: 'C16', size: 'H19', count: stairs.c16.length, length: stairs.c16[0].length, note: `계단부, 3·4구간 (${Math.round(stairs.c16[0].length)} / ${Math.round(stairs.c16[stairs.c16.length - 1].length)})` });
|
||||
}
|
||||
|
||||
// Column (기둥철근.md · plan C4), P1 defaults
|
||||
const col = columnRebar(columnInputFor(model), mColumn);
|
||||
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(' / ')}` });
|
||||
if (model.footings.length > 0) {
|
||||
const columnRows: RawRow[] = [];
|
||||
for (let columnIndex = 0; columnIndex < model.columns.length; columnIndex++) {
|
||||
const first = rows.length;
|
||||
const input = columnInputFor(model, undefined, columnIndex);
|
||||
const col = columnRebar(input, mColumn);
|
||||
rows.push({ member: 'column', name: 'P1', size: 'S32', count: col.mainP1, length: null, pieces: col.p1Pieces.map((length) => ({ length, count: input.nDiv })), 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: input.nDiv })), note: `내측 r ${col.r2}, 마디 ${col.p2Pieces.join(' / ')}` });
|
||||
rows.push({ member: 'column', name: '주철근 커플러', size: 'D32용 (L150 Ø60)', count: col.couplers, length: 150, note: `@${model.columnRebar.ctcCoupler}, P2는 +${model.columnRebar.dzCp2} 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}) 폐합용` });
|
||||
rows.push({ member: 'column', name: 'P3', size: 'H19', count: col.p3Count, length: col.p3Length, note: `일반 구간 띠철근 @${model.columnRebar.ctcP3}, 겹침 ${col.lapP3} (추천안, Q-C6)` });
|
||||
rows.push({ member: 'column', name: 'P5', size: 'H19', count: col.p5Count, length: col.p5Length, note: `단부 띠철근 @${model.columnRebar.ctcP5}, 커플러 1개` });
|
||||
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' });
|
||||
rows.push({ member: 'column', name: 'P6', size: 'H16', count: col.p6Count, length: col.p6Length, note: `크로스타이, ${col.p6Levels.length}단 × 4` });
|
||||
|
||||
// Column bars and couplers repeat for each column (기둥철근 is per column; 다주형은 기둥마다).
|
||||
for (const r of rows) if (r.member === 'column') r.count *= nCol;
|
||||
columnRows.push(...rows.splice(first));
|
||||
}
|
||||
for (const r of columnRows) {
|
||||
const existing = rows.find(v => v.member === 'column' && v.name === r.name);
|
||||
if (!existing) rows.push({ ...r, pieces: r.pieces ? [...r.pieces] : undefined });
|
||||
else {
|
||||
if (existing.pieces || r.pieces || existing.length !== r.length) {
|
||||
existing.pieces ??= [{ length: existing.length!, count: existing.count }];
|
||||
existing.pieces.push(...(r.pieces ?? [{ length: r.length!, count: r.count }]));
|
||||
existing.length = null;
|
||||
}
|
||||
existing.count += r.count;
|
||||
existing.note = '기둥별 실제 배치·길이 합산 · 마디별 수량은 각 기둥의 원주 등분 수';
|
||||
}
|
||||
}
|
||||
} else if (model.castInput) {
|
||||
const layout = cast!;
|
||||
const grouped = new Map<string, typeof layout.bars>();
|
||||
for (const bar of layout.bars) {
|
||||
const key = `${bar.name}|${bar.diameterMm}|${bar.gradeMpa}`;
|
||||
const group = grouped.get(key) ?? [];
|
||||
group.push(bar); grouped.set(key, group);
|
||||
}
|
||||
for (const group of grouped.values()) {
|
||||
const first = group[0];
|
||||
const byLength = new Map<number, { length: number; count: number }>();
|
||||
for (const bar of group) {
|
||||
const key = Math.round(bar.lengthMm * 1e6);
|
||||
const piece = byLength.get(key) ?? { length: bar.lengthMm, count: 0 };
|
||||
piece.count++; byLength.set(key, piece);
|
||||
}
|
||||
const pieces = [...byLength.values()];
|
||||
rows.push({ member: first.member, name: first.name, size: first.kind === 'coupler' ? `커플러 Ø${first.diameterMm.toFixed(1)}` : `Ø${first.diameterMm}`,
|
||||
count: group.length, length: pieces.length === 1 ? pieces[0].length : null, pieces,
|
||||
diameterMm: first.diameterMm, gradeMpa: first.gradeMpa, note: first.note });
|
||||
}
|
||||
layout.missing.forEach(message => warnings.push({ code: 'RB-CIP01', message, target: 'column' }));
|
||||
} else {
|
||||
warnings.push({ code: 'RB-CIP01', message: CAST_PENDING.join(' · ') + ' — 전용 입력을 저장할 수 있으며 3D·철근표·재료표 계산은 상세 확정 후 제공합니다.', target: 'column' });
|
||||
warnings.push({ code: 'RB-CIP02', message: '현장타설말뚝 철근 수량은 미계산입니다. 지원되는 다른 부재만 집계합니다.', target: 'pile' });
|
||||
}
|
||||
// Footing (확대기초철근 (1).md · plan C5). Only when the footing exists (not 현장타설말뚝).
|
||||
if (model.footings.length > 0) {
|
||||
// Each footing (분리 or 일체) gives the same bar set. Several footings add their counts per bar name;
|
||||
@@ -327,10 +380,32 @@ export function rebarSummary(
|
||||
} else {
|
||||
warnings.push({ code: 'RB-F00', message: '현장타설말뚝은 확대기초가 없어 확대기초 철근을 만들지 않습니다.', target: 'footing' });
|
||||
}
|
||||
if (nCol > 1 && model.kink === null) {
|
||||
const grouped = new Map<string, ReturnType<typeof multicolCopingBars>>();
|
||||
for (const b of multicolCopingBars(model, mCoping)) {
|
||||
const group = grouped.get(b.name) ?? [];
|
||||
group.push(b); grouped.set(b.name, group);
|
||||
}
|
||||
for (const [name, bars] of grouped) rows.push({ member: 'coping', name, size: bars[0].size,
|
||||
count: bars.length, length: null, pieces: bars.map(b => ({ length: b.length, count: 1 })),
|
||||
note: '다주형 전용 삽도 규격 · 실제 콘크리트 하면에 따른 조각별 중심선 길이. 번호·cycle별 이음 상세 확인 필요' });
|
||||
warnings.push({ code: 'RB-M02', target: 'coping', message: '다주형 전용 삽도 우선: C1~4 H25·C7 H29·C8 H25·전단 H22·수평/C14 H16. C5/C6 및 C11/C13 중복 번호·cycle별 이음 위치·분리 수평철근 갈고리는 상세 확인 필요. 해당 수량은 검토용입니다.' });
|
||||
warnings.push({ code: 'RB-M01', target: 'coping', message: '다주형 C14는 전용 삽도의 H16·기본600 간격과 실제 하면을 사용합니다. 기둥별 반복·번호 대응 상세는 확인이 필요합니다.' });
|
||||
warnings.push({ code: 'RB-M03', target: 'column', message: '다주형 단부는 삽도의 지름 이상·코핑 반높이 연장을 적용했습니다. P4/P5 번호는 기존 역할(단부 P5·기초내 P4)을 유지하며 도면 번호 대응 확인이 필요합니다.' });
|
||||
}
|
||||
// Table metadata per bar (구분·간격·실간격·위치·방향·형상·근거). The spacing rows take their values from the inputs (승인 2026-10-26).
|
||||
const withMeta = rows.map((r) => {
|
||||
if (r.diameterMm !== undefined) return { ...r, spacing: '입력 구간별', actual: '중심선 계산',
|
||||
position: r.name.startsWith('기둥·말뚝') ? '기둥~말뚝 연속' : r.member === 'pile' ? '지중 말뚝' : '지상 기둥·코핑', direction: '배근 경로',
|
||||
shape: r.name.includes('커플러') ? '커플러' : r.name.includes('나선') ? '나선' : r.name.includes('크로스타이') ? '양끝 갈고리' : /P[35]/.test(r.name) ? '원형' : '직선', basis: 'calc' as const };
|
||||
const linked = linkedSpacing(r.name, model);
|
||||
return { ...r, ...(META[r.name] ?? FALLBACK), ...(linked ?? {}) };
|
||||
const multi = nCol > 1 && r.member === 'coping';
|
||||
return { ...r, ...(META[r.name] ?? FALLBACK), ...(linked ?? {}), ...(nCol > 1 && r.member === 'column' && r.name === 'P5' ? { position: '상·하 단부 · 코핑 반높이까지 (번호 확인 중)' } : {}), ...(multi ? {
|
||||
shape: '다주형 조각별 경로 (검토용)', position: '다주형 전용 삽도 · 실제 하면',
|
||||
...(r.name === 'C9·C11 사이' ? { spacing: String(model.copingRebar.spacingS), actual: String(model.copingRebar.spacingS) } : {}) } : {}) };
|
||||
});
|
||||
return { rows: withMeta, warnings: warnings.map((w) => ({ ...w, target: w.target ?? 'common' })) };
|
||||
return { rows: withMeta, warnings: warnings.map((w) => ({ ...w, target: w.target ?? 'common' })), incomplete: [
|
||||
...(cast ? cast.missing : model.footings.length ? [] : CAST_PENDING),
|
||||
...(nCol > 1 ? ['다주형 철근 번호 대응·cycle별 이음 위치·수평철근 갈고리 상세 확인 필요'] : []),
|
||||
] };
|
||||
}
|
||||
Reference in new issue
Block a user