diff --git a/src/core/rebar/multicol-coping.ts b/src/core/rebar/multicol-coping.ts index 59a1636..7849c49 100644 --- a/src/core/rebar/multicol-coping.ts +++ b/src/core/rebar/multicol-coping.ts @@ -8,7 +8,7 @@ */ import type { PierModel } from '../geometry'; 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'; 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) }); 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 }); + // 전·후열 단차 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 // would cut the mitre are left out, and one shear set (the position nearest the kink) is laid on the mitre. diff --git a/src/core/rebar/step-bars.ts b/src/core/rebar/step-bars.ts index d97d7b7..54ce2f0 100644 --- a/src/core/rebar/step-bars.ts +++ b/src/core/rebar/step-bars.ts @@ -110,3 +110,73 @@ export function stepBars(model: PierModel, opt: StepOptions): { bars: StepBar[]; } 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; +} diff --git a/src/core/rebar/summary.ts b/src/core/rebar/summary.ts index dcdff98..0399c94 100644 --- a/src/core/rebar/summary.ts +++ b/src/core/rebar/summary.ts @@ -4,14 +4,15 @@ 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, 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 { CAST_PENDING } from './cast-in-place'; import { castRebarLayout } from './cast-layout'; import { multicolCopingInfo } from './multicol-coping'; +import { stepBlockBars } from './step-bars'; 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'; +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. */ export type Basis = 'input' | 'default' | 'calc' | 'assumed'; @@ -57,6 +58,7 @@ const MULTI_NOTE: Record = { C9: '전단 · 테이퍼·헌치', C10: '전단 · 기둥 Level', C11: '전단 · 기둥 사이 오목', C12: '측면 상단 (전 길이), 다리 위치마다', C13: '측면 하단 변화구간 (하면에서 끊김), 다리 위치마다', C14: '크로스타이 연직 600 · 교각축 1200 엇갈림', C15: '계단부 ⊓ (가장 높은 계단)', C16: '계단부 L형 (그 밖의 계단, 낮은 쪽 다리)', + C17: '전·후열 단차 300 이상 블록 ㄷ형 띠 (전단간격)', C18: '전·후열 단차 블록 수평 피부철근 (측면간격)', }; /** Table metadata per bar (코핑철근.md, 확대기초철근 (1).md, 기둥철근 (1).md, 받침블럭철근.md). */ @@ -79,6 +81,8 @@ const META: Record `${q.length} × ${q.count}`).join(' / ')}` }); } 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(); + 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 diff --git a/src/view/rebar3d.ts b/src/view/rebar3d.ts index 0d03e89..323ec5e 100644 --- a/src/view/rebar3d.ts +++ b/src/view/rebar3d.ts @@ -8,10 +8,11 @@ import { columnRebar, columnInputFor } from '../core/rebar/column'; import { castRebarLayout } from '../core/rebar/cast-layout'; import { multicolCopingBars } from '../core/rebar/multicol-coping'; import { copingFold, foldBar } from '../core/rebar/coping-fold'; +import { stepBlockBars } from '../core/rebar/step-bars'; import { barDepths, barsAcross } from '../core/rebar/bearing-block'; import { blockFramePoint } from '../core/geometry'; 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 { 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); } } + // 전·후열 단차 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. diff --git a/tests/step-bars.test.ts b/tests/step-bars.test.ts index 3acf65c..359f858 100644 --- a/tests/step-bars.test.ts +++ b/tests/step-bars.test.ts @@ -2,6 +2,8 @@ import { describe, expect, it } from 'vitest'; import { buildPier } from '../src/core/geometry'; import { tStepBars } from '../src/core/rebar/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 { loadP1 } from './helpers'; @@ -84,3 +86,44 @@ describe('계단부 철근 일반 규칙', () => { expect(info.warnings.some((w) => w.code === 'RB-M04')).toBe(false); }); }); + +describe('전·후열 단차 블록 보강 C17 · C18 (결정 2026-11-10)', () => { + const raised = (dz: number, extra: Partial = {}) => { + 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) => ({ 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); + }); +});