Files
Pier_v1/src/core/abut/rebar/block.ts
T
ypshimandClaude Haiku 5.5 a68e4a0366 feat(abut-rebar): 교대 받침블럭 배근 삽도 (평면 · 단면)
- 평면: 블럭 1200 × 1200, S2 실선 (위) · S1 파선 (아래), 번호 풍선
- 단면: 교좌면 기준, 몰탈패드 50, S1 다리 · S2 본, 묻힘 치수 문자열
- 결과에 교좌면 높이(seatZ) 추가 — 단면 축을 교좌면 기준으로 맞춤
- verify/dump: 받침블럭 삽도 덤프

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-05 09:38:29 +09:00

192 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 교대 받침블럭 배근 · 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<string, BarSize>;
/** 몰탈패드 높이 (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>): 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<AbutBlockSettings>,
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 };
}