- 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>
319 lines
19 KiB
TypeScript
319 lines
19 KiB
TypeScript
/**
|
||
* 교대 확대기초(저판) 배근 · 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,
|
||
};
|
||
}
|