/** * 교대 받침블럭 배근 · AD6 (2026-11-05, 받침블럭철근.md §6.6 · 교대 기획서 §10, 사용자 결정 2026-11-05). 순수 함수, 길이는 mm. * * - 배치: 받침마다 블럭 1개 (분리). 블럭 평면 1200 × 1200, 몰탈패드 50 포함 높이 125 → 콘크리트 75. * - S1 (아래, 교대축 직각 간격, 교대축 방향으로 뻗음), S2 (위, 교대축 간격, 교대축 직각으로 뻗음). 규격 H13 (기획서 §6.6). * - 본수 · 다리 · 여유는 교각 받침블럭 규칙(`bearing-block.ts`의 barsAcross · barLength)을 그대로 쓴다. 교각과 달리 깊이만 * 교대 규격(H13)으로 계산한다: S2 = 상면 피복, S1 = 상면 피복 + S1/2 + S2/2 (H13 두 개 = 62.7). * - 다리 길이 700은 철근 중심에서 아래로. 묻힘 = 다리 − (블럭 콘크리트 높이 − 깊이): S2 675, S1 688. * - 경고만 (사용자 결정 2026-11-05): S1 다리가 벽체 W2 층(교좌면 − c)을 지나면 RB-AK01 (Q-S4, 확인 후 결정 기록). * - 좌표: 교대 (s, t, z) mm — s 교대축, t 전면 +, z 저판 하면에서. 블럭은 받침 방위각으로 돌려 놓는다 (abut-scene과 같은 회전). */ import { barLength, barsAcross, DEFAULT_BLOCK_REBAR, type BarRow, type BlockRebarSettings } from '../../rebar/bearing-block'; import { NOMINAL_DIAMETER, type BarSize } from '../../rebar/common'; import type { Issue } from '../../types'; export type AbutBlockSettings = BlockRebarSettings; /** 교대 블럭 규격 (기획서 §6.6): S1 · S2 모두 H13. */ export const ABUT_BLOCK_SIZES = { S1: 'H13', S2: 'H13' } as const satisfies Record; /** 몰탈패드 높이 (mm): 블럭 높이 125에 포함, 콘크리트 높이 = 블럭 높이 − 50. */ export const ABUT_BLOCK_PAD_MM = 50; /** 기본값 = 교각 P1 값 (받침블럭철근.md): 상면 피복 50 · 측면 50 · 여유 75 · @125 · 다리 700. */ export const ABUT_BLOCK_REBAR_DEFAULT: AbutBlockSettings = DEFAULT_BLOCK_REBAR; export function resolveBlockRebar(changes?: Partial): AbutBlockSettings { return { ...ABUT_BLOCK_REBAR_DEFAULT, ...(changes ?? {}) }; } export function blockRebarIssues(s: AbutBlockSettings): string[] { const out: string[] = []; for (const key of ['coverTop', 'coverSide', 'firstGap', 'spacingS1', 'spacingS2', 'legLength'] as const) { if (!(s[key] > 0)) out.push(`${key}은(는) 0보다 커야 합니다.`); } return out; } /** 블럭 상면 ~ 철근 중심 (mm): S2 = 상면 피복, S1 = 상면 피복 + S1/2 + S2/2 (교대 H13). */ export function abutBarDepths(s: AbutBlockSettings): { S1: number; S2: number } { const h = NOMINAL_DIAMETER[ABUT_BLOCK_SIZES.S1]; return { S2: s.coverTop, S1: s.coverTop + h / 2 + h / 2 }; } /** 한 받침 블럭의 배근 (블럭 콘크리트 높이 mm). */ export function abutBlockBars(concreteH: number, s: AbutBlockSettings = ABUT_BLOCK_REBAR_DEFAULT): BarRow[] { const depth = abutBarDepths(s); const length = barLength(1200, s); return [ { name: 'S1', ...barsAcross(1200, s.spacingS1, s), length, embedment: s.legLength - (concreteH - depth.S1) }, { name: 'S2', ...barsAcross(1200, s.spacingS2, s), length, embedment: s.legLength - (concreteH - depth.S2) }, ]; } export interface AbutBlockInput { no: number; /** 교대축 위치 (m, IP1 기준), 받침 전면 쪽 t (m), 방위각 회전 (deg), 교좌면 EL (m), 블럭 높이 (mm, 패드 포함). */ s: number; t: number; rotDeg: number; seatEl: number; blockHeightMm: number; } export interface AbutBlockBar { mark: 'S1' | 'S2'; size: BarSize; role: '주철근' | '보조'; spacing: string; actual: string; shape: string; length: number; count: number; embedment: number; position: string; note: string; basis: '규칙' | '가정'; modified: boolean; } /** One bar as a polyline in (s, t, z) mm from the 저판 하면 (z = seatEl − bottom + local height). */ export interface AbutBlockPath { mark: 'S1' | 'S2'; block: number; pts: [number, number, number][]; } export interface AbutBlockRebar { settings: AbutBlockSettings; concreteH: number; /** 교좌면 높이 (mm, 저판 하면 기준). 경로 z에서 빼면 교좌면 기준. */ seatZ: number; depth: { S1: number; S2: number }; embedment: { S1: number; S2: number }; paths: AbutBlockPath[]; bars: AbutBlockBar[]; issues: Issue[]; } /** W2 층 (교좌면 − c, H16 철근 중심 깊이): 간섭 판정에 쓴다 (wall.ts W2, 교대벽체철근.md §5.1). */ export const W2_LAYER_DEPTH_MM = (wallCover: number) => wallCover + NOMINAL_DIAMETER.H16 / 2; export function abutBlockRebar( blocks: AbutBlockInput[], bottomEl: number, changes?: Partial, wallCover = 100, who = '', ): AbutBlockRebar { const s = resolveBlockRebar(changes); const issues: Issue[] = []; const add = (code: string, level: Issue['level'], message: string) => issues.push({ code, level, pier: who || undefined, message }); if (s.spacingS1 !== ABUT_BLOCK_REBAR_DEFAULT.spacingS1 || s.spacingS2 !== ABUT_BLOCK_REBAR_DEFAULT.spacingS2) { add('RB-AK03', 'I', '블럭 철근 간격을 기본간격과 따로 고쳤습니다. [되돌리기]로 다시 연동합니다.'); } const concreteH = (blocks[0]?.blockHeightMm ?? 125) - ABUT_BLOCK_PAD_MM; const rows = abutBlockBars(concreteH, s); const depth = abutBarDepths(s); const embedment = { S1: rows[0].embedment, S2: rows[1].embedment }; const paths: AbutBlockPath[] = []; const half = 1200 / 2; const straight = 1200 - 2 * s.coverSide; const h = straight / 2; const yS1 = concreteH - depth.S1; // 블럭 밑면 위 높이 const yS2 = concreteH - depth.S2; for (const b of blocks) { const cos = Math.cos((b.rotDeg * Math.PI) / 180); const sin = Math.sin((b.rotDeg * Math.PI) / 180); const sb = b.s * 1000; const tb = b.t * 1000; const zb = (b.seatEl - bottomEl) * 1000; // 블럭 지역 (x 교대축, zt 교대축 직각, y 높이) → 교대 (s, t, z): abut-scene의 rotateY와 같은 회전. const toAbut = (x: number, y: number, zt: number): [number, number, number] => [sb + x * cos + zt * sin, tb - x * sin + zt * cos, zb + y]; const side = (key: 'S1' | 'S2') => (key === 'S1' ? rows[0] : rows[1]); // S1: 교대축 방향으로 뻗고, 교대축 직각 간격으로 놓인다. const r1 = side('S1'); for (let i = 0; i < r1.count; i++) { const zc = r1.firstEnd + i * r1.spacing - half; // 블럭 가운데 기준 (firstEnd는 블럭 모서리부터) paths.push({ mark: 'S1', block: b.no, pts: [toAbut(-h, yS1 - s.legLength, zc), toAbut(-h, yS1, zc), toAbut(h, yS1, zc), toAbut(h, yS1 - s.legLength, zc)] }); } // S2: 교대축 직각으로 뻗고, 교대축 간격으로 놓인다 (위 층). const r2 = side('S2'); for (let i = 0; i < r2.count; i++) { const x = r2.firstEnd + i * r2.spacing - half; paths.push({ mark: 'S2', block: b.no, pts: [toAbut(x, yS2 - s.legLength, -h), toAbut(x, yS2, -h), toAbut(x, yS2, h), toAbut(x, yS2 - s.legLength, h)] }); } } // 간섭 (Q-S4, 결정 2026-11-05 경고만): S1 다리 아래끝이 W2 층 (교좌면 − c − 철근 중심)보다 아래로 내려가면 경고. const w2 = W2_LAYER_DEPTH_MM(wallCover); if (embedment.S1 > w2) { add('RB-AK01', 'W', `블럭 S1 다리(묻힘 ${Math.round(embedment.S1)})가 벽체 W2 층(교좌면 − ${Math.round(w2)})을 지납니다 (Q-S4, 경고만 — 확인 후 결정을 기록).`); } const bars: AbutBlockBar[] = []; const count = (mark: 'S1' | 'S2') => blocks.length * (mark === 'S1' ? rows[0].count : rows[1].count); const spacingText = (mark: 'S1' | 'S2') => `${(mark === 'S1' ? s.spacingS1 : s.spacingS2)}`; const shape = (mark: 'S1' | 'S2') => `직선 ${Math.round(straight)} + 아래 다리 ${s.legLength} × 2 (${mark === 'S1' ? '교대축 방향' : '교대축 직각'})`; bars.push({ mark: 'S1', size: ABUT_BLOCK_SIZES.S1, role: '주철근', spacing: `${spacingText('S1')}`, actual: spacingText('S1'), shape: shape('S1'), length: rows[0].length, count: count('S1'), embedment: embedment.S1, position: `블럭 아래 (상면 ${Math.round(depth.S1)}) · 교대축 직각 간격 · 블럭 ${blocks.length}개 × ${rows[0].count}`, note: `묻힘 ${Math.round(embedment.S1)} (교좌면 아래) · 블럭 콘크리트 ${concreteH}`, basis: '규칙', modified: s.spacingS1 !== ABUT_BLOCK_REBAR_DEFAULT.spacingS1, }); bars.push({ mark: 'S2', size: ABUT_BLOCK_SIZES.S2, role: '보조', spacing: `${spacingText('S2')}`, actual: spacingText('S2'), shape: shape('S2'), length: rows[1].length, count: count('S2'), embedment: embedment.S2, position: `블럭 위 (상면 ${Math.round(depth.S2)}) · 교대축 간격 · 블럭 ${blocks.length}개 × ${rows[1].count}`, note: `묻힘 ${Math.round(embedment.S2)} · 블럭 상면 쪽 (교좌면 아래)`, basis: '규칙', modified: s.spacingS2 !== ABUT_BLOCK_REBAR_DEFAULT.spacingS2, }); const seatZ = blocks[0] ? (blocks[0].seatEl - bottomEl) * 1000 : 0; return { settings: s, concreteH, seatZ, depth, embedment, paths, bars, issues }; }