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>
This commit is contained in:
1 parent
d8fd6adcf0
commit
7cf5835689
6 files changed
+175
-18
No files matched your search
+10
-1
@@ -1,4 +1,4 @@
|
||||
import type { Material } from './rebar/common';
|
||||
import type { FckByMember, Material } from './rebar/common';
|
||||
import type { PierInput } from './types';
|
||||
|
||||
/** Project file schema (spec §8). Bump only with a migration. */
|
||||
@@ -31,6 +31,8 @@ export interface ProjectFile {
|
||||
history: { version: string; note: string; at: string }[];
|
||||
/** ③ 공통 설정 (fck · fy). Absent in older files: the defaults are used. */
|
||||
common?: Material;
|
||||
/** ② 부재별 fck (MPa). Absent in older files: every member uses the 공통 fck. */
|
||||
fckByMember?: FckByMember;
|
||||
}
|
||||
|
||||
/** Serialises a project to the pretty-printed JSON stored in <교량명>_교각설계.pzr.json. */
|
||||
@@ -55,6 +57,13 @@ export function parseProject(text: string): ProjectFile {
|
||||
throw new Error(`지원하지 않는 파일 버전입니다 (schemaVersion ${String(obj.schemaVersion)}, 지원: ${SCHEMA_VERSION}).`);
|
||||
}
|
||||
if (!Array.isArray(obj.piers)) throw new Error('piers 목록이 없습니다.');
|
||||
if (obj.fckByMember !== undefined) {
|
||||
for (const [member, value] of Object.entries(obj.fckByMember)) {
|
||||
if (!['column', 'coping', 'footing'].includes(member) || typeof value !== 'number' || !(value > 0)) {
|
||||
throw new Error(`부재별 fck 값이 맞지 않습니다 (${member}: ${String(value)}).`);
|
||||
}
|
||||
}
|
||||
}
|
||||
obj.piers.forEach((p, i) => {
|
||||
if (!p || typeof p.id !== 'string' || !p.input) throw new Error(`교각 ${i + 1}: id 또는 input이 없습니다.`);
|
||||
const input = p.input;
|
||||
|
||||
@@ -14,6 +14,19 @@ export interface Material {
|
||||
|
||||
export const DEFAULT_MATERIAL: Material = { fck: 27, fyH: 400, fyS: 500 };
|
||||
|
||||
/** Members that can take their own fck on ② (부재 카드). Other members always use the 공통 fck. */
|
||||
export type FckMember = 'column' | 'coping' | 'footing';
|
||||
/** The members of {@link FckMember}, for the ② cards. */
|
||||
export const FCK_MEMBERS: readonly FckMember[] = ['column', 'coping', 'footing'];
|
||||
|
||||
/** Per-member fck (MPa). A member without an entry uses the ③ 공통 설정 fck. */
|
||||
export type FckByMember = Partial<Record<FckMember, number>>;
|
||||
|
||||
/** The material a member is designed with: its own fck when set, otherwise the 공통 value. fy stays common. */
|
||||
export function materialFor(common: Material, byMember: FckByMember, member: FckMember): Material {
|
||||
return { ...common, fck: byMember[member] ?? common.fck };
|
||||
}
|
||||
|
||||
/** True while the values are the assumed defaults (정보 I02 on the rebar screen). */
|
||||
export function isAssumedMaterial(m: Material): boolean {
|
||||
return m.fck === DEFAULT_MATERIAL.fck && m.fyH === DEFAULT_MATERIAL.fyH && m.fyS === DEFAULT_MATERIAL.fyS;
|
||||
|
||||
@@ -4,7 +4,7 @@ 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, isAssumedMaterial, Material, splitPieces } from './common';
|
||||
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';
|
||||
@@ -113,11 +113,20 @@ function footingBarList(Lf: number, Wf: number, Hf: number, columnX: number[]):
|
||||
|
||||
type RawRow = Omit<RebarRow, 'spacing' | 'actual' | 'position' | 'direction' | 'shape' | 'basis'>;
|
||||
|
||||
export function rebarSummary(model: PierModel, m: Material = DEFAULT_MATERIAL): { rows: RebarRow[]; warnings: RebarWarning[] } {
|
||||
export function rebarSummary(
|
||||
model: PierModel,
|
||||
common: Material = DEFAULT_MATERIAL,
|
||||
byMember: FckByMember = {},
|
||||
): { rows: RebarRow[]; warnings: RebarWarning[] } {
|
||||
const rows: RawRow[] = [];
|
||||
const warnings: RebarWarning[] = [];
|
||||
if (isAssumedMaterial(m)) {
|
||||
warnings.push({ code: 'I02', message: `fck ${m.fck} MPa · fy ${m.fyH} / ${m.fyS} MPa는 가정값입니다. 정착·이음 길이에 반영됩니다. 공통 설정에서 확인하세요.`, target: 'common' });
|
||||
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;
|
||||
@@ -130,7 +139,7 @@ export function rebarSummary(model: PierModel, m: Material = DEFAULT_MATERIAL):
|
||||
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(m); // RB-S04: the embedment must reach the H19 development length
|
||||
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' });
|
||||
@@ -145,7 +154,7 @@ export function rebarSummary(model: PierModel, m: Material = DEFAULT_MATERIAL):
|
||||
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(m);
|
||||
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);
|
||||
@@ -182,12 +191,12 @@ export function rebarSummary(model: PierModel, m: Material = DEFAULT_MATERIAL):
|
||||
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, m);
|
||||
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, m);
|
||||
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 상향 엇갈림' });
|
||||
@@ -219,7 +228,7 @@ export function rebarSummary(model: PierModel, m: Material = DEFAULT_MATERIAL):
|
||||
}
|
||||
}
|
||||
for (const b of agg.values()) {
|
||||
const split = splitPieces(b.length, 25.4, m.fyS, m.fck, STANDARD_BAR);
|
||||
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 });
|
||||
|
||||
Reference in new issue
Block a user