feat(abut-rebar): 교대 저판 배근 규칙 B1–B10 (AD3) · skew 배근 · 겹침이음 공통 (AD1 · AD2)
- skew-layout: 교대축을 따라 잰 격자, 끝면 피복 환산, 치수 문자열 - splice: l_s = 1.3 l_d (올림 1 mm), 최소 조각 수, ± l_s 엇갈림 - footing: 층 높이, B1–B4 옵션 ① skew / ② 교대축 직각 (가운데 격자 + 끝 skew 철근 B1-S · B2-S), B5–B8 (상면 벽체 밑 제외), B9 교차 배치, B10-1 · B10-2, 철근별 간격 수정, 검사 RB-AF01–10 - 교대확대기초철근.md §13 기대값 테스트 (뒷굽 짧음 합성은 §5.2 규칙에 맞춰 뒷굽 150으로 확인) Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
59c2607418
commit
e00e43ca61
5 files changed
+598
No files matched your search
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* 교대 확대기초(저판) 배근 · AD3 (2026-11-04, 교대확대기초철근.md, 결정 2026-11-02). 순수 함수, 길이는 mm.
|
||||
*
|
||||
* - B1–B4: 폭 방향 주철근, 교대축을 따라 s_L 격자에 번갈아(cycle-1 B1·B2 ⊓⊔, cycle-2 B3·B4 직선). 최외측.
|
||||
* 옵션 ① skew 방향(날개벽 선과 나란히) / ② 교대축 직각(가운데 격자 + 양끝 끝 skew 철근 B1-S·B2-S). 직사각 평면은 ② 고정.
|
||||
* - B5–B8: 길이 방향 배력철근, 폭을 따라 s_W 격자. 상면(B5·B7)은 벽체 밑에서 빼되 벽체 면 안쪽 s_W/2 이내는 남긴다.
|
||||
* - B9: 전단, B1–B4 격자와 B5–B8 격자의 교점. 행 = 폭 격자 k_9W 배수(벽체 밑 제외), 행 안 = 교대축 격자 k_9L 배수, 행마다 엇갈림.
|
||||
* - B10-1(전면·배면, 교대축 방향) · B10-2(양 끝면, skew 선과 나란히): 측면 수평, s_v 단.
|
||||
* - 간격은 저판 기본간격(s_L, s_W, s_v)에서 계산한다. 철근별로 간격을 고치면 그 철근 묶음만 연동이 끊긴다(RB-AF10).
|
||||
* - 높이 z는 저판 하면 기준, 폭 위치 x는 뒷굽 끝(배면)에서, 길이 위치 s는 교대축을 따라 IP1 쪽 끝면에서 잰다.
|
||||
*/
|
||||
import { DEFAULT_MATERIAL, fyOf, hookLength, NOMINAL_DIAMETER, STANDARD_BAR, type BarSize, type Material } from '../../rebar/common';
|
||||
import type { Issue } from '../../types';
|
||||
import { gridLayout, gridText, lenText, type GridLayout, type SkewAngle } from './skew-layout';
|
||||
import { splicePlan, type SplicePlan } from './splice';
|
||||
|
||||
export type MainDir = 'skew' | 'perp';
|
||||
|
||||
export interface AbutFootingRebarSettings {
|
||||
/** 피복 상면 / 측면 / 하면 (철근 중심까지). */
|
||||
cTop: number;
|
||||
cSide: number;
|
||||
cBottom: number;
|
||||
/** 첫 철근 최소 여유 (측면 피복 다음). */
|
||||
eMin: number;
|
||||
/** 저판 기본간격: 교대축 / 교대축 직각 / 연직. */
|
||||
sL: number;
|
||||
sW: number;
|
||||
sV: number;
|
||||
/** B9 간격 배수 (교대축 / 폭), 정수 ≥ 1. */
|
||||
k9L: number;
|
||||
k9W: number;
|
||||
/** 상면 철근 아래 최소 여유 (B10). */
|
||||
eTop: number;
|
||||
/** B1–B4 배근 방향 (skew 평면일 때). */
|
||||
dir: MainDir;
|
||||
/** 철근별 간격 수정 (없으면 기본간격에 연동): B1–B4 (교대축), B5–B8 (폭), B10 (연직). */
|
||||
sMain?: number;
|
||||
sDist?: number;
|
||||
sSide?: number;
|
||||
}
|
||||
|
||||
export const ABUT_FOOTING_REBAR_DEFAULT: AbutFootingRebarSettings = {
|
||||
cTop: 100,
|
||||
cSide: 100,
|
||||
cBottom: 150,
|
||||
eMin: 150,
|
||||
sL: 125,
|
||||
sW: 150,
|
||||
sV: 200,
|
||||
k9L: 2,
|
||||
k9W: 2,
|
||||
eTop: 150,
|
||||
dir: 'skew',
|
||||
};
|
||||
|
||||
/** 규격 (기획서 §1.2, 엑셀 Abut 시트). */
|
||||
export const ABUT_FOOTING_SIZES = { top: 'H16', bottom: 'H22', dist: 'H16', shear: 'H16', side: 'H19' } as const satisfies Record<string, BarSize>;
|
||||
|
||||
/** 형상 (②에서 받음). */
|
||||
export interface AbutFootingGeom {
|
||||
/** 길이 (교대축 방향) · 폭 (교대축 직각) · 두께. */
|
||||
L: number;
|
||||
W: number;
|
||||
H: number;
|
||||
/** 벽체 배면 / 전면 위치 (뒷굽 끝에서). */
|
||||
xb: number;
|
||||
xf: number;
|
||||
angle: SkewAngle;
|
||||
plan: 'skew' | 'rect';
|
||||
}
|
||||
|
||||
export type BarRole = '주철근' | '보조' | '전단·간격재' | '측면';
|
||||
|
||||
export interface AbutFootingBar {
|
||||
mark: string;
|
||||
size: BarSize;
|
||||
role: BarRole;
|
||||
/** 간격 (cycle별) · 실간격, as the table writes them. */
|
||||
spacing: string;
|
||||
actual: string;
|
||||
shape: string;
|
||||
length: number;
|
||||
count: number;
|
||||
position: string;
|
||||
note: string;
|
||||
/** 간격을 기본간격과 따로 고친 묶음 (RB-AF10 "수정됨"). */
|
||||
modified: boolean;
|
||||
splice?: SplicePlan;
|
||||
}
|
||||
|
||||
export interface AbutFootingRebar {
|
||||
settings: AbutFootingRebarSettings;
|
||||
dir: MainDir;
|
||||
z: { B13: number; B57: number; B68: number; B24: number };
|
||||
/** B1–B4 배치: 위치(교대축, IP1 끝면에서)와 cycle. 옵션 ②는 가운데 격자. */
|
||||
main: { positions: number[]; layout: GridLayout | null; text: string };
|
||||
/** 옵션 ② 끝 skew 철근 (한쪽): 끝면 피복선에서 잰 위치, 치수 문자열. */
|
||||
skewEnd: { offsets: number[]; D: number; k: number; r: number; text: string } | null;
|
||||
/** B5–B8 배치: x (뒷굽 끝에서), 상면에 남는지. */
|
||||
dist: { positions: number[]; topKept: boolean[]; layout: GridLayout; bottomText: string; topText: string };
|
||||
/** B10 단 높이와 위 여유, 치수 문자열. */
|
||||
side: { levels: number[]; topGap: number; text: string; depth1: number; depth2: number; hook: number };
|
||||
/** B9 행: 폭 위치 x와 그 행의 교대축 위치. */
|
||||
shear: { rows: { x: number; s: number[] }[]; length: number };
|
||||
/** 옆붙임 (B1 교대축 방향, B5 폭 방향). */
|
||||
sideShift: { B1: number; B5: number };
|
||||
bars: AbutFootingBar[];
|
||||
issues: Issue[];
|
||||
}
|
||||
|
||||
const d = (size: BarSize) => NOMINAL_DIAMETER[size];
|
||||
|
||||
export function resolveFootingRebar(changes?: Partial<AbutFootingRebarSettings>): AbutFootingRebarSettings {
|
||||
return { ...ABUT_FOOTING_REBAR_DEFAULT, ...(changes ?? {}) };
|
||||
}
|
||||
|
||||
/** Input problems in Korean (the edit refuses them). */
|
||||
export function footingRebarIssues(s: AbutFootingRebarSettings): string[] {
|
||||
const out: string[] = [];
|
||||
for (const key of ['cTop', 'cSide', 'cBottom', 'eMin', 'sL', 'sW', 'sV', 'eTop'] as const) if (!(s[key] > 0)) out.push(`${key}은(는) 0보다 커야 합니다.`);
|
||||
for (const key of ['sMain', 'sDist', 'sSide'] as const) if (s[key] !== undefined && !((s[key] as number) > 0)) out.push('철근 간격은 0보다 커야 합니다.');
|
||||
if (!(Number.isInteger(s.k9L) && s.k9L >= 1 && Number.isInteger(s.k9W) && s.k9W >= 1)) out.push('B9 배수는 1 이상의 정수여야 합니다.');
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* The footing rebar of one abutment. `who` names the abutment in the issues. Lengths are rounded to 1 mm in the table;
|
||||
* the layouts keep the exact values for the drawings.
|
||||
*/
|
||||
export function abutFootingRebar(g: AbutFootingGeom, changes?: Partial<AbutFootingRebarSettings>, material: Material = DEFAULT_MATERIAL, who = ''): AbutFootingRebar {
|
||||
const s = resolveFootingRebar(changes);
|
||||
const issues: Issue[] = [];
|
||||
const add = (code: string, level: Issue['level'], message: string) => issues.push({ code, level, pier: who || undefined, message });
|
||||
const SZ = ABUT_FOOTING_SIZES;
|
||||
const sin = g.angle.sin;
|
||||
const dir: MainDir = g.plan === 'rect' ? 'perp' : s.dir;
|
||||
if (g.plan === 'rect') add('RB-AF04', 'I', '직사각 평면이라 B1–B4 배근 방향을 ② 교대축 직각으로 고정합니다.');
|
||||
const sMain = s.sMain ?? s.sL;
|
||||
const sDist = s.sDist ?? s.sW;
|
||||
const sSide = s.sSide ?? s.sV;
|
||||
if (s.sMain !== undefined || s.sDist !== undefined || s.sSide !== undefined || s.k9L !== 2 || s.k9W !== 2) {
|
||||
add('RB-AF10', 'I', '철근 간격을 기본간격과 따로 고쳤거나 B9 배수가 기본값(2)이 아닙니다. [기본간격으로 되돌리기]로 다시 연동합니다.');
|
||||
}
|
||||
|
||||
// 높이 (§3).
|
||||
const z = {
|
||||
B13: g.H - s.cTop,
|
||||
B57: g.H - s.cTop - (d(SZ.top) + d(SZ.dist)) / 2,
|
||||
B24: s.cBottom,
|
||||
B68: s.cBottom + (d(SZ.bottom) + d(SZ.dist)) / 2,
|
||||
};
|
||||
if (!(z.B57 - z.B68 > d(SZ.dist))) add('RB-AF07', 'W', '상면과 하면 철근 층 사이 순간격이 없습니다 (규격 · 피복 확인).');
|
||||
const legMain = z.B13 - z.B24;
|
||||
const legDist = z.B57 - z.B68;
|
||||
const fy = (size: BarSize) => fyOf(material, size);
|
||||
|
||||
// B1–B4 (§4).
|
||||
let mainPositions: number[] = [];
|
||||
let mainLayout: GridLayout | null = null;
|
||||
let mainText = '';
|
||||
let l0Main: number;
|
||||
let l3: number;
|
||||
let skewEnd: AbutFootingRebar['skewEnd'] = null;
|
||||
if (dir === 'skew') {
|
||||
mainLayout = gridLayout(g.L, s.cSide / sin, s.eMin, sMain);
|
||||
mainPositions = mainLayout.positions;
|
||||
mainText = gridText(mainLayout);
|
||||
l0Main = (g.W - 2 * s.cSide) / sin;
|
||||
l3 = (g.xf - 2 * s.cSide) / sin;
|
||||
} else {
|
||||
const shift = g.W * g.angle.cotAbs;
|
||||
const C = g.L - 2 * s.cSide - shift;
|
||||
const n = Math.floor(C / sMain);
|
||||
const e = (C - Math.max(0, n) * sMain) / 2;
|
||||
const start = s.cSide + shift;
|
||||
mainPositions = n < 0 ? [] : Array.from({ length: n + 1 }, (_, i) => start + e + i * sMain);
|
||||
mainText = `${lenText(s.cSide)} · ${lenText(e)} · ${n}@${lenText(sMain)}=${lenText(n * sMain)} · ${lenText(e)} · ${lenText(s.cSide)} (가운데 격자)`;
|
||||
l0Main = g.W - 2 * s.cSide;
|
||||
l3 = g.xf - 2 * s.cSide;
|
||||
// 끝 skew 철근 (§4.3 ②).
|
||||
const D = e + shift;
|
||||
const k = Math.floor(D / sMain + 1e-9);
|
||||
const r = D - k * sMain;
|
||||
if (k >= 1) {
|
||||
const offsets: number[] = []; // from the first perpendicular bar toward the end face
|
||||
for (let i = 1; i <= k - 1; i++) offsets.push(i * sMain);
|
||||
let text: string;
|
||||
if (r < 1e-6) {
|
||||
offsets.push(k * sMain);
|
||||
text = `${lenText(s.cSide)} · ${k - 1 > 0 ? `${k - 1}@${lenText(sMain)}` : ''}`.replace(/ · $/, '');
|
||||
} else {
|
||||
const half = (sMain + r) / 2;
|
||||
const base = (k - 1) * sMain;
|
||||
offsets.push(base + half, D);
|
||||
const rest = k - 2;
|
||||
text = `${lenText(s.cSide)} · 2@${Math.round(half)}${rest > 0 ? ` · ${rest > 1 ? `${rest}@` : ''}${lenText(sMain)}` : ''}`;
|
||||
}
|
||||
skewEnd = { offsets: offsets.map((o) => D - o), D, k, r, text };
|
||||
add('RB-AF05', 'I', `옵션 ② 끝 skew 철근 B1-S · B2-S를 양끝에 한쪽 ${offsets.length}개씩 둡니다 (${text}).`);
|
||||
}
|
||||
}
|
||||
if (mainPositions.length < 2) add('RB-AF01', 'E', 'B1–B4를 놓을 수 없습니다 (길이 · 간격 확인).');
|
||||
if (mainPositions.length % 2 === 1) add('RB-AF03', 'I', `B1–B4 위치 수가 ${mainPositions.length}개(홀수)라 cycle-1이 1개 많습니다.`);
|
||||
const c1 = Math.ceil(mainPositions.length / 2);
|
||||
const c2 = Math.floor(mainPositions.length / 2);
|
||||
|
||||
// B5–B8 (§5).
|
||||
const distLayout = gridLayout(g.W, s.cSide, s.eMin, sDist);
|
||||
const xs = distLayout.positions;
|
||||
const topKept = xs.map((x) => x <= g.xb + sDist / 2 + 1e-9 || x >= g.xf - sDist / 2 - 1e-9);
|
||||
const backIdx = xs.map((x, i) => (topKept[i] && x < g.xb + sDist / 2 + 1e-9 ? i : -1)).filter((i) => i >= 0);
|
||||
const frontIdx = xs.map((x, i) => (topKept[i] && x >= g.xf - sDist / 2 - 1e-9 ? i : -1)).filter((i) => i >= 0);
|
||||
if (backIdx.length === 0 || frontIdx.length === 0) add('RB-AF02', 'W', `${backIdx.length === 0 ? '뒷굽' : '앞굽'} 상면 길이 방향 철근이 0개입니다 (${backIdx.length === 0 ? '뒷굽' : '앞굽'}이 짧음).`);
|
||||
if (xs.length < 2) add('RB-AF01', 'E', 'B5–B8을 놓을 수 없습니다 (폭 · 간격 확인).');
|
||||
if (xs.length % 2 === 1) add('RB-AF03', 'I', `B5–B8 위치 수가 ${xs.length}개(홀수)라 cycle-1이 1개 많습니다.`);
|
||||
const run = (idx: number[]) => (idx.length > 1 ? `${idx.length - 1}@${lenText(sDist)}=${lenText((idx.length - 1) * sDist)}` : '');
|
||||
const topText =
|
||||
backIdx.length && frontIdx.length
|
||||
? [lenText(s.cSide), lenText(distLayout.e), run(backIdx), lenText(xs[frontIdx[0]] - xs[backIdx[backIdx.length - 1]]), run(frontIdx), lenText(distLayout.e), lenText(s.cSide)].filter(Boolean).join(' · ')
|
||||
: gridText(distLayout);
|
||||
const evenCount = (idx: number[]) => idx.filter((i) => i % 2 === 0).length;
|
||||
const oddCount = (idx: number[]) => idx.filter((i) => i % 2 === 1).length;
|
||||
const kept = [...backIdx, ...frontIdx];
|
||||
const l0Dist = g.L - (2 * s.cSide) / sin;
|
||||
|
||||
// B10 (§6).
|
||||
const m = Math.floor((z.B13 - s.eTop - s.cBottom) / sSide);
|
||||
const levels = m < 0 ? [] : Array.from({ length: m + 1 }, (_, j) => s.cBottom + j * sSide);
|
||||
if (m < 1) add('RB-AF09', 'W', 'B10 단 수가 1 미만입니다 (두께 · 연직 간격 확인).');
|
||||
const hook10 = Math.round(hookLength('main90', d(SZ.side)));
|
||||
const depth1 = s.cSide + (d(SZ.bottom) + d(SZ.side)) / 2;
|
||||
const depth2 = s.cSide + (d(SZ.dist) + d(SZ.side)) / 2;
|
||||
const topGap = levels.length ? z.B13 - levels[levels.length - 1] : 0;
|
||||
|
||||
// B9 (§8): rows on the B5–B8 grid (k_9W multiples, not under the wall), bars on the B1–B4 grid (k_9L multiples, staggered).
|
||||
const rowIdx = xs.map((_, i) => i).filter((i) => i % s.k9W === 0 && topKept[i]);
|
||||
const rows = rowIdx.map((i, r) => {
|
||||
const shift = s.k9L >= 2 && r % 2 === 1 ? 1 : 0;
|
||||
return { x: xs[i], s: mainPositions.filter((_, j) => j >= shift && (j - shift) % s.k9L === 0) };
|
||||
});
|
||||
const b9Count = rows.reduce((a, row) => a + row.s.length, 0);
|
||||
const b9Length = z.B13 - z.B24 + 2 * hookLength('stirrup135', d(SZ.shear));
|
||||
|
||||
// 표 (시안 순서).
|
||||
const bars: AbutFootingBar[] = [];
|
||||
const push = (b: Omit<AbutFootingBar, 'length' | 'splice'> & { length: number }) => {
|
||||
const splice = splicePlan(b.length, d(b.size), fy(b.size), material.fck) ?? undefined;
|
||||
const length = Math.round(b.length);
|
||||
if (splice) {
|
||||
const text = `${splice.pieces}조각 이음 l_s ${splice.lap} (${Math.round(splice.staggered[0])} / ${Math.round(splice.staggered[1])} 엇갈림)`;
|
||||
b.note = b.note ? `${b.note} · ${text}` : text;
|
||||
add('RB-AF08', splice.over ? 'W' : 'I', `${b.mark} ${length} > 정척 ${STANDARD_BAR}: ${text}${splice.over ? ' — 조각이 정척을 넘습니다' : ''}.`);
|
||||
}
|
||||
bars.push({ ...b, length, splice });
|
||||
};
|
||||
const mainMod = s.sMain !== undefined;
|
||||
const distMod = s.sDist !== undefined;
|
||||
const sideMod = s.sSide !== undefined;
|
||||
const mainSp = `${lenText(2 * sMain)}`;
|
||||
const mainAct = `${lenText(sMain)}`;
|
||||
const dirText = dir === 'skew' ? '폭 방향 (skew 선과 나란히)' : '교대축 직각';
|
||||
const shiftB1 = (d(SZ.top) + d(SZ.bottom)) / 2;
|
||||
const shiftB5 = (d(SZ.dist) + d(SZ.dist)) / 2;
|
||||
push({ mark: 'B1', size: SZ.top, role: '주철근', spacing: mainSp, actual: mainAct, shape: `⊓ 다리 아래로 ${Math.round(legMain)} (B2 높이까지)`, length: l0Main + 2 * legMain, count: c1, position: `상면 최외측 z ${lenText(z.B13)} · ${dirText}`, note: `cycle-1 · B2 옆 +${shiftB1.toFixed(2)} (교대축 방향)`, modified: mainMod });
|
||||
push({ mark: 'B3', size: SZ.top, role: '주철근', spacing: mainSp, actual: mainAct, shape: '직선', length: l3, count: c2, position: `상면 · 배면 끝 ~ 벽체 전면 − ${lenText(s.cSide)}`, note: 'cycle-2 · 앞굽 상면은 B1만', modified: mainMod });
|
||||
push({ mark: 'B2', size: SZ.bottom, role: '주철근', spacing: mainSp, actual: mainAct, shape: `⊔ 다리 위로 ${Math.round(legMain)} (B1 높이까지)`, length: l0Main + 2 * legMain, count: c1, position: `하면 최외측 z ${lenText(z.B24)}`, note: 'cycle-1', modified: mainMod });
|
||||
push({ mark: 'B4', size: SZ.bottom, role: '주철근', spacing: mainSp, actual: mainAct, shape: '직선', length: l0Main, count: c2, position: '하면 최외측', note: 'cycle-2', modified: mainMod });
|
||||
if (skewEnd) {
|
||||
const n = skewEnd.offsets.length * 2;
|
||||
const len = (g.W - 2 * s.cSide) / sin;
|
||||
push({ mark: 'B1-S', size: SZ.top, role: '주철근', spacing: skewEnd.text, actual: '—', shape: '직선 · 끝면과 나란히 · 전체 폭', length: len, count: n, position: `상면 · 양끝 삼각형 (한쪽 ${skewEnd.offsets.length}개)`, note: `D ${lenText(skewEnd.D)} · k ${skewEnd.k} · r ${lenText(skewEnd.r)}`, modified: mainMod });
|
||||
push({ mark: 'B2-S', size: SZ.bottom, role: '주철근', spacing: skewEnd.text, actual: '—', shape: '직선 · 끝면과 나란히 · 전체 폭', length: len, count: n, position: `하면 · 양끝 삼각형 (한쪽 ${skewEnd.offsets.length}개)`, note: 'B1-S와 같은 위치', modified: mainMod });
|
||||
}
|
||||
const distSp = lenText(2 * sDist);
|
||||
const distAct = lenText(sDist);
|
||||
push({ mark: 'B5', size: SZ.dist, role: '보조', spacing: distSp, actual: distAct, shape: `⊓ 다리 ${Math.round(legDist)} (B6 높이까지)`, length: l0Dist + 2 * legDist, count: evenCount(kept), position: `상면 B1 아래 z ${lenText(z.B57)} · 뒷굽·앞굽 · 교대축 방향`, note: `+${shiftB5.toFixed(1)} 옆붙임 (폭 방향)`, modified: distMod });
|
||||
push({ mark: 'B7', size: SZ.dist, role: '보조', spacing: distSp, actual: distAct, shape: '직선', length: l0Dist, count: oddCount(kept), position: '상면 · 뒷굽·앞굽 · 교대축 방향', note: `벽체 밑 제외 (면 안쪽 s_W/2 = ${lenText(sDist / 2)} 이내는 남김)`, modified: distMod });
|
||||
push({ mark: 'B6', size: SZ.dist, role: '보조', spacing: distSp, actual: distAct, shape: `⊔ 다리 ${Math.round(legDist)} (B5 높이까지)`, length: l0Dist + 2 * legDist, count: xs.filter((_, i) => i % 2 === 0).length, position: `하면 B2 위 z ${lenText(z.B68)}`, note: 'cycle-1', modified: distMod });
|
||||
push({ mark: 'B8', size: SZ.dist, role: '보조', spacing: distSp, actual: distAct, shape: '직선', length: l0Dist, count: xs.filter((_, i) => i % 2 === 1).length, position: '하면 B2 위', note: 'cycle-2', modified: distMod });
|
||||
push({ mark: 'B9', size: SZ.shear, role: '전단·간격재', spacing: `${lenText(s.k9L * sMain)} × ${lenText(s.k9W * sDist)}`, actual: '—', shape: '양끝 135° 갈고리', length: b9Length, count: b9Count, position: `전단 · B1 ~ B2 감쌈 · 벽체 밑 제외 ${rows.length}행`, note: `교대축 ${s.k9L}·s_L × 폭 ${s.k9W}·s_W · 행마다 s_L 엇갈림 · 배수로만 수정`, modified: s.k9L !== 2 || s.k9W !== 2 });
|
||||
const sideSp = lenText(sSide);
|
||||
push({ mark: 'B10-1', size: SZ.side, role: '측면', spacing: sideSp, actual: sideSp, shape: `양끝 90° 갈고리 ${hook10}`, length: g.L - (2 * depth2) / sin + 2 * hook10, count: levels.length * 2, position: `측면 · 전면·배면 · 깊이 ${depth1.toFixed(2)} · +${d(SZ.side).toFixed(1)}`, note: `${levels.length}단 ${m}@${sideSp} · 위 여유 ${lenText(topGap)}`, modified: sideMod });
|
||||
push({ mark: 'B10-2', size: SZ.side, role: '측면', spacing: sideSp, actual: sideSp, shape: `양끝 90° 갈고리 ${hook10}`, length: (g.W - 2 * depth1) / sin + 2 * hook10, count: levels.length * 2, position: `측면 · 양 끝면 (skew) · 깊이 ${depth2.toFixed(1)}`, note: `${levels.length}단 ${m}@${sideSp}`, modified: sideMod });
|
||||
|
||||
return {
|
||||
settings: s,
|
||||
dir,
|
||||
z,
|
||||
main: { positions: mainPositions, layout: mainLayout, text: mainText },
|
||||
skewEnd,
|
||||
dist: { positions: xs, topKept, layout: distLayout, bottomText: gridText(distLayout), topText },
|
||||
side: { levels, topGap, text: `${lenText(s.cBottom)} · ${m}@${sideSp}=${lenText(m * sSide)}`, depth1, depth2, hook: hook10 },
|
||||
shear: { rows, length: Math.round(b9Length) },
|
||||
sideShift: { B1: shiftB1, B5: shiftB5 },
|
||||
bars,
|
||||
issues,
|
||||
};
|
||||
}
|
||||
|
||||
/** The 형상 of the 저판 from the generated abutment (m → mm): 길이 = 교대축 길이, 폭 · 두께 · 벽체 위치는 형상 입력, θ = skew. */
|
||||
export function abutFootingGeom(
|
||||
lengthM: number,
|
||||
shape: { footingFrontM: number; footingBackM: number; footingThicknessM: number; wallBackM: number; wallFrontM: number },
|
||||
angle: SkewAngle,
|
||||
plan: 'skew' | 'rect' = 'skew',
|
||||
): AbutFootingGeom {
|
||||
const mm = (m: number) => Math.round(m * 1e6) / 1e3;
|
||||
return {
|
||||
L: mm(lengthM),
|
||||
W: mm(shape.footingFrontM - shape.footingBackM),
|
||||
H: mm(shape.footingThicknessM),
|
||||
xb: mm(shape.wallBackM - shape.footingBackM),
|
||||
xf: mm(shape.wallFrontM - shape.footingBackM),
|
||||
angle,
|
||||
plan,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 교대 skew 배근 공통 (plan_abut AD1, spec_abut §6.1, 2026-11-04). 순수 함수, 길이는 mm.
|
||||
*
|
||||
* - 교대축 직각 철근은 날개벽(skew) 선과 나란히 뻗고, 간격은 교대축을 따라 잰다. 실제 직각 간격은 s · sin θ다.
|
||||
* - skew 끝면의 피복을 교대축 방향으로 재면 c / sin θ, 폭을 skew 선 방향으로 재면 W / sin θ다.
|
||||
* - θ는 교대축과 날개벽 선의 사잇각이다 (예제 A1 95°02′52″, sin θ = 0.99612).
|
||||
*/
|
||||
|
||||
export interface SkewAngle {
|
||||
/** sin θ (> 0). */
|
||||
sin: number;
|
||||
/** |cot θ|: the shift of the end face along the axis per unit of width. 0 for a square plan. */
|
||||
cotAbs: number;
|
||||
}
|
||||
|
||||
export function skewAngle(skewDeg: number): SkewAngle {
|
||||
const r = (skewDeg * Math.PI) / 180;
|
||||
return { sin: Math.abs(Math.sin(r)), cotAbs: Math.abs(Math.cos(r) / Math.sin(r)) };
|
||||
}
|
||||
|
||||
/** A square plan (θ = 90°). */
|
||||
export const SQUARE: SkewAngle = { sin: 1, cotAbs: 0 };
|
||||
|
||||
/** One row of bars laid out over a length with an edge on both sides (교각 확대기초 §3.1과 같은 식). */
|
||||
export interface GridLayout {
|
||||
/** 배치 가능 길이 A = total − 2·edge. */
|
||||
A: number;
|
||||
/** 격자 칸 수 n = floor((A − 2·e_min) / s); positions n + 1. Below 0 when nothing fits (RB-AF01). */
|
||||
n: number;
|
||||
s: number;
|
||||
/** 격자 길이 G = n·s and the end gap e = (A − G) / 2. */
|
||||
G: number;
|
||||
e: number;
|
||||
edge: number;
|
||||
/** Positions from the start of `total`: edge + e + i·s. */
|
||||
positions: number[];
|
||||
}
|
||||
|
||||
export function gridLayout(total: number, edge: number, eMin: number, s: number): GridLayout {
|
||||
const A = total - 2 * edge;
|
||||
const n = Math.floor((A - 2 * eMin) / s);
|
||||
const G = Math.max(0, n) * s;
|
||||
const e = (A - G) / 2;
|
||||
const positions = n < 0 ? [] : Array.from({ length: n + 1 }, (_, i) => edge + e + i * s);
|
||||
return { A, n, s, G, e, edge, positions };
|
||||
}
|
||||
|
||||
/** A length as the drawings write it: m with 3 decimals from 1000 mm ("4.050"), else mm with up to 1 decimal ("185.8", "175"). */
|
||||
export function lenText(mm: number): string {
|
||||
if (mm >= 1000 - 1e-9) return (mm / 1000).toFixed(3);
|
||||
const r = Math.round(mm * 10) / 10;
|
||||
return Number.isInteger(r) ? String(r) : r.toFixed(1);
|
||||
}
|
||||
|
||||
/** "100.4 · 185.8 · 93@125=11.625 · 185.8 · 100.4" of a grid (edge · e · n@s=G · e · edge). */
|
||||
export function gridText(g: GridLayout): string {
|
||||
return [lenText(g.edge), lenText(g.e), `${g.n}@${lenText(g.s)}=${lenText(g.G)}`, lenText(g.e), lenText(g.edge)].join(' · ');
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 교대 겹침이음 공통 (plan_abut AD2, 교대확대기초철근.md §12.1, 결정 2026-11-02 Q-AF11). 순수 함수, 길이는 mm.
|
||||
*
|
||||
* l_d = 0.6·d_b·f_y / (λ·√fck), l_s = 1.3·l_d (올림 1 mm)
|
||||
* 조각 수 p = 가장 작은 p, (l + (p − 1)·l_s) / p ≤ L_bar
|
||||
* 이음 중심은 인접 철근끼리 ± l_s 엇갈림 → 끝 조각 길이가 ± l_s 바뀐다.
|
||||
*/
|
||||
import { spliceLength, STANDARD_BAR } from '../../rebar/common';
|
||||
|
||||
/** l_s (mm), rounded up to 1 mm. */
|
||||
export function lapLength(db: number, fy: number, fck: number): number {
|
||||
return Math.ceil(spliceLength(db, fy, fck) - 1e-9);
|
||||
}
|
||||
|
||||
export interface SplicePlan {
|
||||
pieces: number;
|
||||
lap: number;
|
||||
/** Piece length with the splice in the middle (equal pieces). */
|
||||
even: number;
|
||||
/** End piece lengths with the stagger: [even − l_s, even + l_s]. */
|
||||
staggered: [number, number];
|
||||
/** True when a staggered piece is still longer than L_bar (RB-AF08 경고). */
|
||||
over: boolean;
|
||||
}
|
||||
|
||||
/** The splice of a bar longer than L_bar; null when it fits in one piece. */
|
||||
export function splicePlan(total: number, db: number, fy: number, fck: number, standard = STANDARD_BAR): SplicePlan | null {
|
||||
if (total <= standard) return null;
|
||||
const lap = lapLength(db, fy, fck);
|
||||
let p = 2;
|
||||
while ((total + (p - 1) * lap) / p > standard && p < 20) p++;
|
||||
const even = (total + (p - 1) * lap) / p;
|
||||
const staggered: [number, number] = [even - lap, even + lap];
|
||||
return { pieces: p, lap, even, staggered, over: staggered[1] > standard + 1e-9 };
|
||||
}
|
||||
@@ -140,6 +140,8 @@ export const NOMINAL_DIAMETER = {
|
||||
H16: 15.9,
|
||||
H19: 19.1,
|
||||
D22: 22.2,
|
||||
/** 교대 저판 B2·B4 (교대확대기초철근.md §1.3, 2026-11-04). */
|
||||
H22: 22.2,
|
||||
H25: 25.4,
|
||||
H29: 28.6,
|
||||
S32: 31.8,
|
||||
|
||||
Reference in new issue
Block a user