feat(rebar): 전·후열 단차 300 mm 이상 블록 보강 C17·C18

전열·후열 상면 차가 300 mm 이상인 구간의 높은 쪽 블록에 (결정 2026-11-10, step-bars.ts):
- C17 ㄷ형 띠 H16 @전단간격: 단차면 + c ~ 측면 − c, 위 = 높은 상면 − c, 다리 낮은 상면 아래 l_d까지
- C18 수평 피부철근 H16 @측면간격: 단차면 안쪽, 낮은 상면 + 측면간격마다
T형(철근표·3D)과 다주형(편 좌표 → 꺾임 접기) 모두에 넣는다. 구간마다 따로 두고 이웃 구간과 잇지 않는다 (가정).

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-10 17:17:23 +09:00
1 parent 8f808f453e
commit 33aa2d9a61
5 files changed
+142 -4

No files matched your search

+4 -1
View File
@@ -8,7 +8,7 @@
*/ */
import type { PierModel } from '../geometry'; import type { PierModel } from '../geometry';
import { DEFAULT_MATERIAL, developmentLength, fyOf, hookLength, type Material, NOMINAL_DIAMETER, spliceLength } from './common'; import { DEFAULT_MATERIAL, developmentLength, fyOf, hookLength, type Material, NOMINAL_DIAMETER, spliceLength } from './common';
import { stepBars } from './step-bars'; import { stepBars, stepBlockBars } from './step-bars';
import { copingFold, foldBar, type FoldPiece, inWedge, isTransverse, onMitre } from './coping-fold'; import { copingFold, foldBar, type FoldPiece, inWedge, isTransverse, onMitre } from './coping-fold';
export type CopingPoint = [number, number, number]; export type CopingPoint = [number, number, number];
@@ -296,6 +296,9 @@ export function multicolCopingInfo(model: PierModel, material: Material = DEFAUL
stepMin: MULTI_REBAR.stepMin * 1000, across: top.filter((_, i) => i % 2 === 0) }); stepMin: MULTI_REBAR.stepMin * 1000, across: top.filter((_, i) => i % 2 === 0) });
for (const b of stairs.bars) add(b.name, 'H19', b.pts.map(([x, z]): CopingPoint => [x / 1000, z, b.n])); for (const b of stairs.bars) add(b.name, 'H19', b.pts.map(([x, z]): CopingPoint => [x / 1000, z, b.n]));
for (const message of stairs.warnings) warnings.push({ code: 'RB-S01', message }); for (const message of stairs.warnings) warnings.push({ code: 'RB-S01', message });
// 전·후열 단차 300 이상: 높은 쪽 블록에 C17 ㄷ형 띠 (전단간격) · C18 피부철근 (측면간격), H16 (결정 2026-11-10).
for (const b of stepBlockBars(model, { cover: c * 1000, d: d('H16'), ld: Math.round(developmentLength(d('H16'), fy('H16'), material.fck)),
tieSpacing: settings.spacingS, skinSpacing: settings.sideSpacing })) add(b.name, 'H16', b.pts);
// Bent coping (결정 2026-11-10, coping-fold.ts): runs along the axis turn at their mitre point; bars across the width that // Bent coping (결정 2026-11-10, coping-fold.ts): runs along the axis turn at their mitre point; bars across the width that
// would cut the mitre are left out, and one shear set (the position nearest the kink) is laid on the mitre. // would cut the mitre are left out, and one shear set (the position nearest the kink) is laid on the mitre.
+70
View File
@@ -110,3 +110,73 @@ export function stepBars(model: PierModel, opt: StepOptions): { bars: StepBar[];
} }
return { bars, warnings }; return { bars, warnings };
} }
/** 전·후열 단차 블록 보강의 최소 단차 (mm, 결정 2026-11-10). */
export const STEP_BLOCK_MIN_MM = 300;
export interface StepBlockBar {
name: 'C17' | 'C18';
/** Unfolded centre line (g m, EL m, n m). */
pts: [number, number, number][];
/** Developed length (mm). */
length: number;
}
export interface StepBlockOptions {
/** Cover c (mm). */
cover: number;
/** H16 diameter and development length l_d (mm). */
d: number;
ld: number;
/** C17 spacing along the axis = 전단간격, C18 vertical spacing = 측면간격 (mm). */
tieSpacing: number;
skinSpacing: number;
}
/**
* 전·후열 단차 블록 보강 (결정 2026-11-10): 전열·후열 상면 차가 300 mm 이상인 구간에서, 높은 쪽 블록에
* - C17 ㄷ형 띠철근 (H16): 블록 폭 방향 (코핑 안 단차면 + c ~ 측면 − c), 위 = 높은 상면 − c, 다리 양쪽을 낮은 상면 아래 l_d까지.
* 교축 방향으로 전단간격, 구간 시작 + c ~ 끝 − c에 고르게.
* - C18 수평 피부철근 (H16): 단차면 안쪽 (C17 다리 안), 낮은 상면 + 측면간격마다 높은 상면 − c 아래까지, 구간 시작 + c ~ 끝 − c.
* 구간 = 전열·후열 계단 경계를 합친 사이. 구간마다 따로 두고 이웃 구간과 잇지 않는다 (가정).
*/
export function stepBlockBars(model: PierModel, opt: StepBlockOptions): StepBlockBar[] {
const parts = model.copingParts.filter((p) => p.k === 0);
if (parts.length !== 2 || !model.backSegments) return [];
const half = model.copingB / 2;
const c = opt.cover / 1000, ld = opt.ld / 1000, d = opt.d / 1000;
const [front] = parts;
const split = front.nFrom > -half + 1e-9 ? front.nFrom : front.nTo;
const frontDir = Math.sign((front.nFrom + front.nTo) / 2 - split);
const topAt = (segs: { s0: number; s1: number; top: number }[], x: number) =>
(segs.find((s) => x >= s.s0 && x < s.s1) ?? segs[segs.length - 1]).top;
const bounds = [...new Set([0, model.copingL, ...model.segments.map((s) => s.s0), ...model.backSegments.map((s) => s.s0)].map((v) => Math.round(v * 1000) / 1000))].sort((a, b) => a - b);
const len = (pts: [number, number, number][]) => pts.slice(1).reduce((s, p, i) => s + Math.hypot(p[0] - pts[i][0], p[1] - pts[i][1], p[2] - pts[i][2]), 0) * 1000;
const out: StepBlockBar[] = [];
for (let i = 1; i < bounds.length; i++) {
const a = bounds[i - 1], b = bounds[i], mid = (a + b) / 2;
const fTop = topAt(model.segments, mid), bTop = topAt(model.backSegments, mid);
if (Math.abs(fTop - bTop) * 1000 < STEP_BLOCK_MIN_MM - 1e-6) continue;
const backHigh = bTop > fTop;
const dir = backHigh ? -frontDir : frontDir;
const hi = Math.max(fTop, bTop), lo = Math.min(fTop, bTop);
const nA = split + dir * c, nB = dir * (half - c);
const zt = hi - c, legEnd = lo - ld;
const span = b - a - 2 * c;
if (span <= 0) continue;
const s = opt.tieSpacing / 1000;
const count = Math.floor(span / s + 1e-9) + 1;
const start = a + c + (span - (count - 1) * s) / 2;
for (let k = 0; k < count; k++) {
const x = start + k * s;
const pts: [number, number, number][] = [[x, legEnd, nA], [x, zt, nA], [x, zt, nB], [x, legEnd, nB]];
out.push({ name: 'C17', pts, length: len(pts) });
}
const nSkin = nA + dir * d;
for (let z = lo + opt.skinSpacing / 1000; z < zt - 1e-9; z += opt.skinSpacing / 1000) {
const pts: [number, number, number][] = [[a + c, z, nSkin], [b - c, z, nSkin]];
out.push({ name: 'C18', pts, length: len(pts) });
}
}
return out;
}
+18 -2
View File
@@ -4,14 +4,15 @@ import { copingDefaults } from './coping-settings';
import { DEFAULT_COLUMN_REBAR } from './column-settings'; import { DEFAULT_COLUMN_REBAR } from './column-settings';
import { DEFAULT_FOOTING_REBAR, type FootingRebarSettings } from './footing-settings'; import { DEFAULT_FOOTING_REBAR, type FootingRebarSettings } from './footing-settings';
import { bottomPiece, topBarLength, widthLayout, topSplicePieces } from './coping-main'; import { bottomPiece, topBarLength, widthLayout, topSplicePieces } from './coping-main';
import { shearBars, shearBarsMulti, copingSideBars, SIDE_HOOK_LEG, c14Bars, c14Length, tStepBars, stepDevLength, undersideAt, shearULength, capLength, PIECES_PER_POSITION } from './coping'; import { shearBars, shearBarsMulti, copingSideBars, SIDE_HOOK_LEG, c14Bars, c14Length, COPING_REBAR, tStepBars, stepDevLength, undersideAt, shearULength, capLength, PIECES_PER_POSITION } from './coping';
import { sideRowLevels } from '../coping-geom'; import { sideRowLevels } from '../coping-geom';
import { CAST_PENDING } from './cast-in-place'; import { CAST_PENDING } from './cast-in-place';
import { castRebarLayout } from './cast-layout'; import { castRebarLayout } from './cast-layout';
import { multicolCopingInfo } from './multicol-coping'; import { multicolCopingInfo } from './multicol-coping';
import { stepBlockBars } from './step-bars';
import { columnRebar, columnInputFor } from './column'; import { columnRebar, columnInputFor } from './column';
import { bottomBars, crossBars, sideBars, assemblyBars, shearB9 } from './footing'; import { bottomBars, crossBars, sideBars, assemblyBars, shearB9 } from './footing';
import { DEFAULT_MATERIAL, FckByMember, fyOf, isAssumedMaterial, Material, materialFor, splitPieces, STANDARD_BAR } from './common'; import { DEFAULT_MATERIAL, developmentLength, FckByMember, fyOf, isAssumedMaterial, Material, materialFor, NOMINAL_DIAMETER, splitPieces, STANDARD_BAR } from './common';
/** 근거 badge (spec §6.1): where the value comes from. */ /** 근거 badge (spec §6.1): where the value comes from. */
export type Basis = 'input' | 'default' | 'calc' | 'assumed'; export type Basis = 'input' | 'default' | 'calc' | 'assumed';
@@ -57,6 +58,7 @@ const MULTI_NOTE: Record<string, string> = {
C9: '전단 · 테이퍼·헌치', C10: '전단 · 기둥 Level', C11: '전단 · 기둥 사이 오목', C9: '전단 · 테이퍼·헌치', C10: '전단 · 기둥 Level', C11: '전단 · 기둥 사이 오목',
C12: '측면 상단 (전 길이), 다리 위치마다', C13: '측면 하단 변화구간 (하면에서 끊김), 다리 위치마다', C12: '측면 상단 (전 길이), 다리 위치마다', C13: '측면 하단 변화구간 (하면에서 끊김), 다리 위치마다',
C14: '크로스타이 연직 600 · 교각축 1200 엇갈림', C15: '계단부 ⊓ (가장 높은 계단)', C16: '계단부 L형 (그 밖의 계단, 낮은 쪽 다리)', C14: '크로스타이 연직 600 · 교각축 1200 엇갈림', C15: '계단부 ⊓ (가장 높은 계단)', C16: '계단부 L형 (그 밖의 계단, 낮은 쪽 다리)',
C17: '전·후열 단차 300 이상 블록 ㄷ형 띠 (전단간격)', C18: '전·후열 단차 블록 수평 피부철근 (측면간격)',
}; };
/** Table metadata per bar (코핑철근.md, 확대기초철근 (1).md, 기둥철근 (1).md, 받침블럭철근.md). */ /** Table metadata per bar (코핑철근.md, 확대기초철근 (1).md, 기둥철근 (1).md, 받침블럭철근.md). */
@@ -79,6 +81,8 @@ const META: Record<string, Pick<RebarRow, 'spacing' | 'actual' | 'position' | 'd
C14: { spacing: '1000 / 2000', actual: '1000', position: '직각 수평 (위 2줄 · 아래 엇갈림)', direction: '교각축 직각', shape: '⊓ (폭 방향)', basis: 'calc' }, C14: { spacing: '1000 / 2000', actual: '1000', position: '직각 수평 (위 2줄 · 아래 엇갈림)', direction: '교각축 직각', shape: '⊓ (폭 방향)', basis: 'calc' },
C15: { spacing: '250', actual: '250', position: '계단부 가장 높은 구간', direction: '교각축', shape: '거꾸로 된 U', basis: 'calc' }, C15: { spacing: '250', actual: '250', position: '계단부 가장 높은 구간', direction: '교각축', shape: '거꾸로 된 U', basis: 'calc' },
C16: { spacing: '250', actual: '250', position: '계단부 그 밖의 구간 (최저 상면 + 100 이상)', direction: '교각축', shape: 'L형', basis: 'calc' }, C16: { spacing: '250', actual: '250', position: '계단부 그 밖의 구간 (최저 상면 + 100 이상)', direction: '교각축', shape: 'L형', basis: 'calc' },
C17: { spacing: '250', actual: '250', position: '전·후열 단차 300 이상 블록', direction: '교각축 직각', shape: 'ㄷ형', basis: 'calc' },
C18: { spacing: '250', actual: '250', position: '전·후열 단차 블록 단차면', direction: '교각축', shape: '직선', basis: 'calc' },
P1: { spacing: '원주 46등분', actual: '—', position: '외측 r 875', direction: '연직', shape: '아래 끝 90° 갈고리, 커플러 이음', 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' }, 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: '@3000 (P2 +150)', actual: '—', position: 'P1·P2 각 줄', direction: '원통', shape: 'L 150 · Ø60', basis: 'default' },
@@ -311,6 +315,18 @@ export function rebarSummary(
note: `${name === 'C15' ? '계단부, 가장 높은 구간 (거꾸로 U)' : '계단부, 그 밖의 계단 구간 (L형, 낮은 쪽 다리)'} · ${pieces.map((q) => `${q.length} × ${q.count}`).join(' / ')}` }); note: `${name === 'C15' ? '계단부, 가장 높은 구간 (거꾸로 U)' : '계단부, 그 밖의 계단 구간 (L형, 낮은 쪽 다리)'} · ${pieces.map((q) => `${q.length} × ${q.count}`).join(' / ')}` });
} }
for (const message of steps.warnings) warnings.push({ code: 'RB-S01', target: 'coping', message }); for (const message of steps.warnings) warnings.push({ code: 'RB-S01', target: 'coping', message });
// 전·후열 단차 블록 보강 C17 · C18 (결정 2026-11-10): 단차 300 mm 이상인 구간만.
const block = stepBlockBars(model, { cover: COPING_REBAR.cover, d: NOMINAL_DIAMETER.H16, ld: Math.round(developmentLength(NOMINAL_DIAMETER.H16, fyOf(mCoping, 'H16'), mCoping.fck)),
tieSpacing: model.copingRebar.spacingS, skinSpacing: model.copingRebar.sideSpacing });
for (const name of ['C17', 'C18'] as const) {
const bars = block.filter((b) => b.name === name);
if (!bars.length) continue;
const byLength = new Map<number, number>();
for (const b of bars) byLength.set(Math.round(b.length), (byLength.get(Math.round(b.length)) ?? 0) + 1);
const pieces = [...byLength].map(([length, count]) => ({ length, count }));
rows.push({ member: 'coping', name, size: 'H16', count: bars.length, length: pieces.length === 1 ? pieces[0].length : null, ...(pieces.length > 1 ? { pieces } : {}),
note: name === 'C17' ? '전·후열 단차 300 이상 블록, ㄷ형 띠 (다리 낮은 상면 아래 l_d)' : '전·후열 단차 블록, 단차면 수평 피부철근' });
}
} }
// Column (기둥철근.md · plan C4), P1 defaults // Column (기둥철근.md · plan C4), P1 defaults
+7 -1
View File
@@ -8,10 +8,11 @@ import { columnRebar, columnInputFor } from '../core/rebar/column';
import { castRebarLayout } from '../core/rebar/cast-layout'; import { castRebarLayout } from '../core/rebar/cast-layout';
import { multicolCopingBars } from '../core/rebar/multicol-coping'; import { multicolCopingBars } from '../core/rebar/multicol-coping';
import { copingFold, foldBar } from '../core/rebar/coping-fold'; import { copingFold, foldBar } from '../core/rebar/coping-fold';
import { stepBlockBars } from '../core/rebar/step-bars';
import { barDepths, barsAcross } from '../core/rebar/bearing-block'; import { barDepths, barsAcross } from '../core/rebar/bearing-block';
import { blockFramePoint } from '../core/geometry'; import { blockFramePoint } from '../core/geometry';
import { INTEGRATED_BLOCK } from '../core/integrated-block'; import { INTEGRATED_BLOCK } from '../core/integrated-block';
import { bendRadius, DEFAULT_MATERIAL, FckByMember, hookLength, Material, materialFor, NOMINAL_DIAMETER } from '../core/rebar/common'; import { bendRadius, DEFAULT_MATERIAL, developmentLength, FckByMember, fyOf, hookLength, Material, materialFor, NOMINAL_DIAMETER } from '../core/rebar/common';
import { diameterGroupIndex, DIAMETER_GROUPS, memberOfBar, type RebarMemberKey } from '../core/rebar/color'; import { diameterGroupIndex, DIAMETER_GROUPS, memberOfBar, type RebarMemberKey } from '../core/rebar/color';
import { stretchMatrix } from './scene'; import { stretchMatrix } from './scene';
@@ -307,6 +308,11 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
tBar(b.name, 0, [mm(b.pts[i - 1][0]), b.pts[i - 1][1], n], [mm(b.pts[i][0]), b.pts[i][1], n], NOMINAL_DIAMETER.H19, COLOR.coping); tBar(b.name, 0, [mm(b.pts[i - 1][0]), b.pts[i - 1][1], n], [mm(b.pts[i][0]), b.pts[i][1], n], NOMINAL_DIAMETER.H19, COLOR.coping);
} }
} }
// 전·후열 단차 300 이상 블록 보강 C17 · C18 (결정 2026-11-10, step-bars.ts).
const blockLd = Math.round(developmentLength(NOMINAL_DIAMETER.H16, fyOf(mCoping, 'H16'), mCoping.fck));
for (const b of stepBlockBars(m, { cover: COPING_REBAR.cover, d: NOMINAL_DIAMETER.H16, ld: blockLd, tieSpacing: m.copingRebar.spacingS, skinSpacing: m.copingRebar.sideSpacing })) {
for (let i = 1; i < b.pts.length; i++) tBar(b.name, 0, b.pts[i - 1], b.pts[i], NOMINAL_DIAMETER.H16, COLOR.coping);
}
} }
// Columns: P1 / P2 main bars on their radii, ties P3 / P5 / P4 as rings, P6 crossties across the column. // Columns: P1 / P2 main bars on their radii, ties P3 / P5 / P4 as rings, P6 crossties across the column.
+43
View File
@@ -2,6 +2,8 @@ import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry'; import { buildPier } from '../src/core/geometry';
import { tStepBars } from '../src/core/rebar/coping'; import { tStepBars } from '../src/core/rebar/coping';
import { multicolCopingInfo } from '../src/core/rebar/multicol-coping'; import { multicolCopingInfo } from '../src/core/rebar/multicol-coping';
import { stepBlockBars } from '../src/core/rebar/step-bars';
import { rebarSummary } from '../src/core/rebar/summary';
import type { PierInput } from '../src/core/types'; import type { PierInput } from '../src/core/types';
import { loadP1 } from './helpers'; import { loadP1 } from './helpers';
@@ -84,3 +86,44 @@ describe('계단부 철근 일반 규칙', () => {
expect(info.warnings.some((w) => w.code === 'RB-M04')).toBe(false); expect(info.warnings.some((w) => w.code === 'RB-M04')).toBe(false);
}); });
}); });
describe('전·후열 단차 블록 보강 C17 · C18 (결정 2026-11-10)', () => {
const raised = (dz: number, extra: Partial<PierInput> = {}) => {
const p1 = loadP1();
return buildPier({ ...p1, ...extra, bearings: p1.bearings.map((b) => (b.row === '후열' && b.no === 7 ? { ...b, z: b.z + dz, seatEl: b.seatEl! + dz } : b)) });
};
const opts = (m: ReturnType<typeof buildPier>) => ({ cover: 100, d: 15.9, ld: 600, tieSpacing: m.copingRebar.spacingS, skinSpacing: m.copingRebar.sideSpacing });
it('a 400 mm step: C17 ㄷ ties across the higher (back) block @전단간격, legs l_d below the lower top; one C18 row', () => {
const m = raised(0.4);
const bars = stepBlockBars(m, opts(m));
const c17 = bars.filter((b) => b.name === 'C17'), c18 = bars.filter((b) => b.name === 'C18');
expect(c17.length).toBe(10);
const [, back] = m.copingParts.filter((p) => p.k === 0);
const hi = m.backSegments![1].top, lo = m.segments[1].top;
for (const b of c17) {
for (const p of b.pts) { expect(p[2]).toBeGreaterThanOrEqual(back.nFrom - 1e-9); expect(p[2]).toBeLessThanOrEqual(back.nTo + 1e-9); }
expect(b.pts[1][1]).toBeCloseTo(hi - 0.1, 9);
expect(b.pts[0][1]).toBeCloseTo(lo - 0.6, 9);
expect(b.pts[0][0]).toBeGreaterThan(2.196 + 0.1 - 1e-6);
expect(b.pts[0][0]).toBeLessThan(4.701 - 0.1 + 1e-6);
}
expect(Math.abs(c17[1].pts[0][0] - c17[0].pts[0][0])).toBeCloseTo(0.25, 9);
expect(c18.length).toBe(1);
expect(c18[0].pts[0][1]).toBeCloseTo(lo + 0.25, 9);
});
it('below 300 mm no block bars', () => {
const m = raised(0.25);
expect(stepBlockBars(m, opts(m))).toEqual([]);
});
it('they reach the T형 철근표 and the multi-column bars', () => {
const rows = rebarSummary(raised(0.4)).rows;
expect(rows.find((r) => r.name === 'C17')!.count).toBe(10);
expect(rows.find((r) => r.name === 'C18')!.count).toBe(1);
const multi = multicolCopingInfo(raised(0.4, { type: '2주형', columnCount: 2, foundationType: '말뚝기초', groundEl: 30, pileLengthMm: 24000 }));
expect(multi.bars.some((b) => b.name === 'C17')).toBe(true);
expect(multi.bars.some((b) => b.name === 'C18')).toBe(true);
});
});