- 평면: 블럭 1200 × 1200, S2 실선 (위) · S1 파선 (아래), 번호 풍선 - 단면: 교좌면 기준, 몰탈패드 50, S1 다리 · S2 본, 묻힘 치수 문자열 - 결과에 교좌면 높이(seatZ) 추가 — 단면 축을 교좌면 기준으로 맞춤 - verify/dump: 받침블럭 삽도 덤프 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
192 lines
9.0 KiB
TypeScript
192 lines
9.0 KiB
TypeScript
/**
|
||
* 교대 받침블럭 배근 · 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 };
|
||
}
|