Files
Pier_v1/src/core/rebar/summary.ts
T
ypshimandClaude Haiku 5.5 7cf5835689 feat(rebar): per-member fck on ② for column, coping and footing
Each member can take its own fck on its ② card. A member without one uses the ③ 공통 fck, and fy stays common.
The rebar table and the 3D model both read the resolved value. The I02 note stays until every fck member has its
own value. The project file stores the values as optional fckByMember and refuses a bad value on open.
Changing the fck is not a shape change, so it is not counted in the pending changes.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-23 08:53:39 +09:00

246 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { PierModel } from '../geometry';
import { barLength, bearingBlockRebar, totals } from './bearing-block';
import { bottomPiece, topBarLength, widthLayout } from './coping-main';
import { shearBars, shearBarsMulti, copingSideBars, SIDE_HOOK_LEG, c14Bars, c14Length, stairBars, stepDevLength, undersideAt, shearULength, capLength, PIECES_PER_POSITION } from './coping';
import { columnRebar, COLUMN_P1 } from './column';
import { bottomBars, crossBars, sideBars, assemblyBars, shearB9 } from './footing';
import { DEFAULT_MATERIAL, FckByMember, isAssumedMaterial, Material, materialFor, splitPieces } from './common';
/** 근거 badge (spec §6.1): where the value comes from. */
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';
name: string;
size: string;
count: number;
length: number | null;
note: string;
/** 간격 (설계 간격, mm or text), 실간격 (cycle 묶음 기준), 위치, 방향, 형상, 근거. */
spacing: string;
actual: string;
position: string;
direction: string;
shape: string;
basis: Basis;
}
export interface RebarWarning {
code: string;
message: string;
/** Where the alert points: a member tab, or the 공통 설정 (for I02). */
target: RebarRow['member'] | 'common';
/** The bar the alert is about, so that [보기] can highlight its row. */
bar?: string;
}
/** Table metadata per bar (코핑철근.md, 확대기초철근 (1).md, 기둥철근 (1).md, 받침블럭철근.md). */
const META: Record<string, Pick<RebarRow, 'spacing' | 'actual' | 'position' | 'direction' | 'shape' | 'basis'>> = {
S1: { spacing: '125', actual: '125', position: '블럭 상면 − 69.1', direction: '교각축 직각', shape: '양끝 다리 꺾임', basis: 'calc' },
S2: { spacing: '125', actual: '125', position: '블럭 상면 − 50', direction: '교각축', shape: '양끝 다리 꺾임', basis: 'calc' },
C1: { spacing: '250', actual: '125', position: '상면 1단 (cycle-1)', direction: '교각축', shape: '양끝 꺾임', basis: 'calc' },
C2: { spacing: '250', actual: '125', position: '상면 2단 (cycle-1)', direction: '교각축', shape: '양끝 꺾임', basis: 'calc' },
C3: { spacing: '250', actual: '125', position: '상면 3단 (cycle-1)', direction: '교각축', shape: '양끝 꺾임', basis: 'calc' },
C4: { spacing: '250', actual: '125', position: '상면 1단 (cycle-2)', direction: '교각축', shape: '양끝 꺾임', basis: 'calc' },
C5: { spacing: '250', actual: '125', position: '상면 2단 (cycle-2)', direction: '교각축', shape: '양끝 꺾임', basis: 'calc' },
C6: { spacing: '250', actual: '125', position: '상면 3단 (cycle-2)', direction: '교각축', shape: '양끝 꺾임', basis: 'calc' },
C7: { spacing: '250', actual: '125', position: '하면 1단 (cycle-1)', direction: '교각축', shape: '하면 추종, 양끝 꺾임, 2조각 겹침', basis: 'calc' },
C8: { spacing: '250', actual: '125', position: '하면 1단 (cycle-2)', direction: '교각축', shape: '하면 추종, 양끝 꺾임, 2조각 겹침', basis: 'calc' },
C9: { spacing: '250', actual: '250', position: '전단 좌 경사', direction: '연직', shape: '폐합 (하부 U + 상부 삿갓)', basis: 'assumed' },
C10: { spacing: '300', actual: '300', position: '전단 Level', direction: '연직', shape: '폐합 (하부 U + 상부 삿갓)', basis: 'assumed' },
C11: { spacing: '250', actual: '250', position: '전단 우 경사', direction: '연직', shape: '폐합 (하부 U + 상부 삿갓)', basis: 'assumed' },
C12: { spacing: '250', actual: '250', position: '측면 하단 (경사 하면에서 끊김)', direction: '교각축', shape: '일자 + 좌우 ㄷ형', basis: 'calc' },
C13: { spacing: '250', actual: '250', position: '측면 상단 (끝면까지)', direction: '교각축', shape: '일자 + 좌우 ㄷ형', basis: 'calc' },
C14: { spacing: '1000 / 2000', actual: '1000', position: '직각 수평 (위 2줄 · 아래 엇갈림)', direction: '교각축 직각', shape: '⊓ (폭 방향)', basis: 'calc' },
C15: { spacing: '250', actual: '250', position: '계단부 가장 높은 5구간', direction: '교각축', shape: '거꾸로 된 U', basis: 'calc' },
C16: { spacing: '250', actual: '250', position: '계단부 3·4구간', direction: '교각축', shape: 'L형', basis: 'calc' },
P1: { spacing: '원주 46등분', actual: '—', position: '외측 r 875', direction: '연직', shape: '아래 끝 90° 갈고리, 커플러 이음', basis: 'calc' },
P2: { spacing: '원주 46등분', actual: '—', position: '내측 r 843.2', direction: '연직', shape: '직선, 커플러 이음', basis: 'calc' },
'주철근 커플러': { spacing: '@3000 (P2 +150)', actual: '—', position: 'P1·P2 각 줄', direction: '원통', shape: 'L 150 · Ø60', basis: 'default' },
'띠철근 커플러': { spacing: '120', actual: '120', position: '단부 P5 · 기초안 P4', direction: '원통', shape: 'L 100 · Ø40', basis: 'assumed' },
커플러: { spacing: '@3000 (P2 +150)', actual: '—', position: 'P1·P2 각 줄', direction: '원통', shape: 'L 150 · Ø60', basis: 'default' },
P3: { spacing: '300', actual: '300', position: '일반 구간 (코핑 안 포함)', direction: '원형', shape: '겹침 (추천안)', basis: 'assumed' },
P5: { spacing: '120', actual: '120', position: '단부 구간 (첫 P3 아래)', direction: '원형', shape: '반원 2개가 아닌 원형 1개, 커플러 1개', basis: 'calc' },
P4: { spacing: '120', actual: '120', position: '확대기초 안 (마지막 P5 아래)', direction: '반원 2개', shape: '반원 2개, 커플러 2개', basis: 'calc' },
P6: { spacing: '1800 (단)', actual: '—', position: '첫 P4부터, 단마다 4본', direction: 'U형', shape: '양끝 90° 갈고리', basis: 'calc' },
B1: { spacing: '250', actual: '250', position: 'B5 아래 2층', direction: '교각축', shape: '⊓ 양끝 아래로', basis: 'assumed' },
B2: { spacing: '250', actual: '125', position: '하면 1단 최외측 (cycle-2)', direction: '교각축', shape: '⊔, 다리 B1 높이까지, 2조각', basis: 'calc' },
B3: { spacing: '250', actual: '250', position: '하면 2단 (B2·B4 + 100)', direction: '교각축', shape: '⊔ 양끝 위로', basis: 'assumed' },
B4: { spacing: '250', actual: '125', position: '하면 1단 최외측 (cycle-1)', direction: '교각축', shape: '직선', basis: 'calc' },
B5: { spacing: '250', actual: '250', position: '상면 최외측', direction: '교각축 직각', shape: '⊓ 양끝 아래로', basis: 'assumed' },
B6: { spacing: '250', actual: '125', position: '하면 1단 (cycle-2)', direction: '교각축 직각', shape: '⊔, 다리 B5 높이까지', basis: 'calc' },
B7: { spacing: '250', actual: '250', position: '하면 2단 (B6·B8 + 100)', direction: '교각축 직각', shape: '⊔ 양끝 위로', basis: 'assumed' },
B8: { spacing: '250', actual: '125', position: '하면 1단 (cycle-1)', direction: '교각축 직각', shape: '직선', basis: 'calc' },
'B10-1': { spacing: '200', actual: '200', position: '폭 W 양끝 면, 다리 안쪽', direction: '교각축', shape: '양끝 90° 갈고리', basis: 'calc' },
'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' },
};
const FALLBACK: Pick<RebarRow, 'spacing' | 'actual' | 'position' | 'direction' | 'shape' | 'basis'> = {
spacing: '—', actual: '—', position: '—', direction: '—', shape: '—', basis: 'calc',
};
/** 정척 L_bar (mm) from 확대기초철근.md §1.2. Bars longer than this need a splice. */
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, columnX: 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, Hf)) out.push({ ...b, note: '측면 수평' });
for (const b of assemblyBars(Lf, Wf, Hf)) out.push({ ...b, note: '조립 수평' });
const b9 = shearB9(Lf, Wf, Hf, columnX);
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,
common: Material = DEFAULT_MATERIAL,
byMember: FckByMember = {},
): { rows: RebarRow[]; warnings: RebarWarning[] } {
const rows: RawRow[] = [];
const warnings: RebarWarning[] = [];
const mCoping = materialFor(common, byMember, 'coping');
const mColumn = materialFor(common, byMember, 'column');
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);
if (isAssumedMaterial(common) && !ownFck) {
warnings.push({ code: 'I02', message: `fck ${common.fck} MPa · fy ${common.fyH} / ${common.fyS} MPa는 가정값입니다. 정착·이음 길이에 반영됩니다. 공통 설정에서 확인하세요.`, target: 'common' });
}
const L = model.copingL * 1000;
const B = model.copingB * 1000;
// Bearing blocks (§6.3 · plan C2)
// Embedment in the coping (받침블럭철근.md §4): l_leg − (block height − bar depth), with the model's block heights.
const blocks = bearingBlockRebar(model);
const blk = totals(blocks);
const embedments = (name: 'S1' | 'S2') => blocks.flatMap((b) => b.bars.filter((r) => r.name === name).map((r) => r.embedment));
const emb1 = embedments('S1');
const emb2 = embedments('S2');
const range = (xs: number[]) => `${Math.round(Math.min(...xs))}–${Math.round(Math.max(...xs))}`;
const anchorage = stepDevLength(mCoping); // RB-S04: the embedment must reach the H19 development length
const shortest = Math.min(...emb1, ...emb2);
if (shortest < anchorage) {
warnings.push({ code: 'RB-S04', message: `받침블럭 철근 묻힘 ${Math.round(shortest)} mm < 정착길이 ${anchorage} mm (코핑 안 묻힘 부족)`, target: 'bearing' });
}
rows.push({ member: 'bearing', name: 'S1', size: 'H19', count: blk.S1, length: barLength(), note: `블럭 배치 분리, 블럭당 8본 · 묻힘 ${range(emb1)} (블럭 상면 − 69.1)` });
rows.push({ member: 'bearing', name: 'S2', size: 'H19', count: blk.S2, length: barLength(), note: `블럭 배치 분리, 블럭당 8본 · 묻힘 ${range(emb2)} (블럭 상면 − 50)` });
// Coping (코핑철근.md §5–§8)
const width = widthLayout(B);
const perRow = width.count / 2; // cycle split, 12 per row
// C1–C3 and C4–C6 are the same three top rows (row 1, 2, 3), one set per cycle.
for (const [name, k] of [['C1', 1], ['C2', 2], ['C3', 3], ['C4', 1], ['C5', 2], ['C6', 3]] as const) {
rows.push({ member: 'coping', name, size: 'H29', count: perRow, length: topBarLength(L, k), note: '상면, 이음 무시' });
}
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 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)}`;
};
const capRange = (xs: number[]) => {
const c = xs.map((x) => Math.round(capLength(x, L)));
return `${Math.min(...c)}–${Math.max(...c)}`;
};
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) });
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' });
}
// Per row: two 일자 (side faces) and two ㄷ (one at each end), so 4 bars per row (코핑철근.md §6.2).
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})` });
// 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));
const lineCount = (line: number) => c14.filter((b) => b.line === line).length;
const omitted = c14.filter((b) => !b.legs).length;
rows.push({ member: 'coping', name: 'C14', size: 'H19', count: c14.length, length: c14Length(model.copingB * 1000), note: `위 2 줄 ${lineCount(1)} · ${lineCount(2)}본씩, 아래 ${[3, 4, 5, 6].map(lineCount).join(' · ')} (합 ${c14.length}) · 연직 발 생략 ${omitted}개 (하면 밖)` });
const stairs = stairBars(model.copingL * 1000, model.copingB * 1000, model.copingRefTop, mCoping);
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(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: '주철근 커플러', 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}) 폐합용` });
rows.push({ member: 'column', name: 'P3', size: 'H19', count: col.p3Count, length: col.p3Length, note: `일반 구간 띠철근 @300, 겹침 ${col.lapP3} (추천안, Q-C6)` });
rows.push({ member: 'column', name: 'P5', size: 'H19', count: col.p5Count, length: col.p5Length, note: '단부 띠철근 @120, 커플러 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' });
// 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 현장타설말뚝).
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) {
// Columns on this footing, as x mm from the footing centre (for the B9 column-zone rule).
const columnX = model.columns.filter((c) => Math.abs(c.s - f.s) <= f.length / 2).map((c) => (c.s - f.s) * 1000);
for (const b of footingBarList(f.length * 1000, f.width * 1000, (f.top - f.bottom) * 1000, columnX)) {
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, mFooting.fyS, mFooting.fck, STANDARD_BAR);
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: b.note });
}
}
} else {
warnings.push({ code: 'RB-F00', message: '현장타설말뚝은 확대기초가 없어 확대기초 철근을 만들지 않습니다.', target: 'footing' });
}
// Table metadata per bar (구분·간격·실간격·위치·방향·형상·근거).
const withMeta = rows.map((r) => ({ ...r, ...(META[r.name] ?? FALLBACK) }));
return { rows: withMeta, warnings: warnings.map((w) => ({ ...w, target: w.target ?? 'common' })) };
}