feat(rebar): 주철근 기본간격·끝단 여유를 입력과 연동, CP04 홀수 개수 경고
- widthLayout이 코핑 배근 입력(mainSpacing, mainEndGap)을 받는다. 결과에 spacing 필드 추가 - 3D 주철근 위치의 125 하드코딩 제거 (철근표와 3D가 같은 간격을 씀) - CP04: 주철근 개수 n+3이 홀수면 cycle-1·cycle-2 분할 불가 경고 (적용은 막지 않음) - P1 기본값(125 · 80)에서는 결과와 경고 없음 (테스트로 확인) Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d2c5f90092
commit
1e6b8db7e9
4 files changed
+44
-12
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { DEFAULT_MATERIAL, fyOf, hookLength, Material, NOMINAL_DIAMETER, spliceLength } from './common';
|
||||
import { DEFAULT_COPING_REBAR } from './coping-settings';
|
||||
import { type CopingRebarSettings, DEFAULT_COPING_REBAR } from './coping-settings';
|
||||
|
||||
/**
|
||||
* C1–C8 상·하면 주철근 (plan C3 continued). Source: RAG/코핑철근.md §5, 2026-10-22 최종 정리.
|
||||
@@ -8,9 +8,6 @@ import { DEFAULT_COPING_REBAR } from './coping-settings';
|
||||
export const MAIN = {
|
||||
/** 피복 (최외측 철근 중심까지). */
|
||||
cover: 100,
|
||||
/** 기본 간격 s_m and minimum end gap g_min. */
|
||||
spacing: 125,
|
||||
endGapMin: 80,
|
||||
/** 상면 단 가로 간격 = d (H29, mm). Fixed; the vertical step is the 상면 주철근 단 간격 input. */
|
||||
layHorizontal: NOMINAL_DIAMETER.H29,
|
||||
/** 90° 갈고리 l_h (mm, as in the document). */
|
||||
@@ -31,18 +28,24 @@ export interface WidthLayout {
|
||||
g: number;
|
||||
/** Bars in all: n + 3. */
|
||||
count: number;
|
||||
/** 기본간격 s_m (mm) used for this layout (the 주철근 기본간격 input). */
|
||||
spacing: number;
|
||||
/** Bars per cycle (alternating): 12 / 12 for 24. */
|
||||
cyclePerKind: number;
|
||||
/** Dimension string, e.g. "127 · 110.5 · 21@125 · 110.5 · 127". */
|
||||
text: string;
|
||||
}
|
||||
|
||||
/** 폭 방향 배치 (코핑철근.md §5.1). d_st = H25 stirrup, d_m = H29 main. */
|
||||
export function widthLayout(B: number, dSt = NOMINAL_DIAMETER.H25, dM = NOMINAL_DIAMETER.H29): WidthLayout {
|
||||
/**
|
||||
* 폭 방향 배치 (코핑철근.md §5.1). d_st = H25 stirrup, d_m = H29 main.
|
||||
* s_m (주철근 기본간격) and g_min (끝단 최소 여유) come from the 코핑 배근 input.
|
||||
*/
|
||||
export function widthLayout(B: number, s: Pick<CopingRebarSettings, 'mainSpacing' | 'mainEndGap'> = DEFAULT_COPING_REBAR, dSt = NOMINAL_DIAMETER.H25, dM = NOMINAL_DIAMETER.H29): WidthLayout {
|
||||
const yE = MAIN.cover + (dSt + dM) / 2;
|
||||
const S = B - 2 * yE;
|
||||
const n = Math.floor((S - 2 * MAIN.endGapMin) / MAIN.spacing);
|
||||
const g = (S - n * MAIN.spacing) / 2;
|
||||
const spacing = s.mainSpacing;
|
||||
const n = Math.floor((S - 2 * s.mainEndGap) / spacing);
|
||||
const g = (S - n * spacing) / 2;
|
||||
const count = n + 3;
|
||||
const r = (v: number) => Math.round(v * 10) / 10;
|
||||
return {
|
||||
@@ -51,8 +54,9 @@ export function widthLayout(B: number, dSt = NOMINAL_DIAMETER.H25, dM = NOMINAL_
|
||||
n,
|
||||
g: r(g),
|
||||
count,
|
||||
spacing,
|
||||
cyclePerKind: count / 2,
|
||||
text: `${r(yE)} · ${r(g)} · ${n}@${MAIN.spacing} · ${r(g)} · ${r(yE)}`,
|
||||
text: `${r(yE)} · ${r(g)} · ${n}@${spacing} · ${r(g)} · ${r(yE)}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -152,8 +152,12 @@ 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 width = widthLayout(B);
|
||||
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.
|
||||
if (width.count % 2 !== 0) {
|
||||
warnings.push({ code: 'CP04', message: `주철근 개수 ${width.count}(n+3)은 홀수라 cycle-1·cycle-2를 나눌 수 없습니다 (기본간격 ${width.spacing}, 끝단 여유 ${model.copingRebar.mainEndGap}). 기본간격을 확인하세요.`, target: 'coping' });
|
||||
}
|
||||
// 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: '상면, 이음 무시' });
|
||||
|
||||
+2
-2
@@ -141,8 +141,8 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
|
||||
|
||||
// Coping (straight coping only; a bent coping is not drawn in 3D yet).
|
||||
if (straight) {
|
||||
const w = widthLayout(B * 1000);
|
||||
const xs = [w.yE, ...Array.from({ length: w.n + 1 }, (_, i) => w.yE + w.g + 125 * i), B * 1000 - w.yE].map((v) => mm(v) - B / 2);
|
||||
const w = widthLayout(B * 1000, m.copingRebar);
|
||||
const xs = [w.yE, ...Array.from({ length: w.n + 1 }, (_, i) => w.yE + w.g + w.spacing * i), B * 1000 - w.yE].map((v) => mm(v) - B / 2);
|
||||
const cover = 0.1;
|
||||
// Top rows C1–C6 (코핑철근.md §5.1, §5.3): counted from the left, the even (1-based) positions are cycle 1 (C1–C3),
|
||||
// the odd cycle 2 (C4–C6). In 0-based j that is odd j → cycle 1. Each row turns down 472 at both ends, and the
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { developmentLength, hookLength, NOMINAL_DIAMETER, spliceLength } from '../src/core/rebar/common';
|
||||
import { bottomPiece, topBarLength, topRowEl, widthLayout } from '../src/core/rebar/coping-main';
|
||||
import { buildPier } from '../src/core/geometry';
|
||||
import { rebarSummary } from '../src/core/rebar/summary';
|
||||
import { DEFAULT_COPING_REBAR } from '../src/core/rebar/coping-settings';
|
||||
import { loadP1 } from './helpers';
|
||||
|
||||
describe('C1–C8 상·하면 주철근 (코핑철근.md §5, plan C3)', () => {
|
||||
it('nominal diameters: H29 28.6, S32 31.8 (documents use these, not the round numbers)', () => {
|
||||
@@ -19,6 +23,26 @@ describe('C1–C8 상·하면 주철근 (코핑철근.md §5, plan C3)', () => {
|
||||
expect(w.text).toBe('127 · 110.5 · 21@125 · 110.5 · 127');
|
||||
});
|
||||
|
||||
it('주철근 기본간격 input drives the layout: 140 → 19@140 (22 bars, even), 120 → 22@120 (25, odd)', () => {
|
||||
const w140 = widthLayout(3100, { ...DEFAULT_COPING_REBAR, mainSpacing: 140 });
|
||||
expect(w140.spacing).toBe(140);
|
||||
expect(w140.n).toBe(19);
|
||||
expect(w140.count).toBe(22);
|
||||
expect(w140.text).toBe(`${w140.yE} · ${w140.g} · 19@140 · ${w140.g} · ${w140.yE}`);
|
||||
const w120 = widthLayout(3100, { ...DEFAULT_COPING_REBAR, mainSpacing: 120 });
|
||||
expect(w120.n).toBe(22);
|
||||
expect(w120.count).toBe(25);
|
||||
});
|
||||
|
||||
it('CP04: the default P1 has an even count (no warning); 기본간격 120 gives an odd count and a CP04 warning', () => {
|
||||
const base = rebarSummary(buildPier(loadP1())).warnings.filter((w) => w.code === 'CP04');
|
||||
expect(base).toEqual([]);
|
||||
const odd = buildPier({ ...loadP1(), copingRebar: { mainSpacing: 120 } });
|
||||
const warnings = rebarSummary(odd).warnings.filter((w) => w.code === 'CP04');
|
||||
expect(warnings).toHaveLength(1);
|
||||
expect(warnings[0].target).toBe('coping');
|
||||
});
|
||||
|
||||
it('heights: 43.284 / 43.184 / 43.084 for the three top rows (상면 단 간격 100)', () => {
|
||||
expect(topRowEl(43.284, 1)).toBe(43.284);
|
||||
expect(topRowEl(43.284, 2)).toBe(43.184);
|
||||
|
||||
Reference in new issue
Block a user