feat(abut-rebar): 교대 벽체 배근 규칙 W1–W11 (AD4)
- 연직 W1 · W3 · W4 (저판 격자 − 17.5, B6·B8 위 갈고리, W3 교좌면 + l_d / 브라켓 상면 − c 제한) - W2 교좌면 상면, W9 · W10 계단 상면 (낮은 이웃 구간으로 l_d), W5 · W6 계단에서 끊음, W7 간격재, W8 끝면 ㄷ형, W11 끝면 연직 - 묶음별 간격 수정(연직 · 수평 · 계단 상면), W7 배수, RB-AB01–05 - 철근표 행 + 철근별 경로 (u, t, z) — 삽도 · 3D 공용 - H13 규격 추가 - 테스트: 교대벽체철근.md §11 기대값 전체 (fck 27), l_d · 기본간격 연동 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8361487b6b
commit
1da8dd74e2
3 files changed
+491
No files matched your search
@@ -0,0 +1,351 @@
|
||||
/**
|
||||
* 교대 벽체 배근 · AD4 (2026-11-04, 교대벽체철근.md, 결정 2026-11-02). 순수 함수, 길이는 mm.
|
||||
*
|
||||
* - W1 전면 연직 (cycle-1만) / W3 · W4 배면 연직 (cycle-1 / 2): 저판 격자 − (d_B13 + d_W)/2 (§4.3).
|
||||
* - W2 교좌면 상면 (교대축 직각, skew) / W9 · W10 계단 상면 (교대축 방향, 끝 계단 / 중간 계단).
|
||||
* - W5 · W6 전면 · 배면 수평 (계단에서 끊음) / W7 간격재 / W8 끝면 수평 (ㄷ형) / W11 끝면 연직.
|
||||
* - 간격은 벽체 기본간격(s_L, s_W, s_v)에서 계산한다. 묶음별로 고치면 그 묶음만 연동이 끊긴다(RB-AB04).
|
||||
* - 좌표: u = 교대축을 따라 IP1 쪽 끝면에서 잰 skew 격자 위치 (끝면과 나란한 선이 u 일정), t = 교대축에서 전면 +,
|
||||
* z = 저판 하면에서. 저판 배근(footing.ts)과 같은 u를 쓴다.
|
||||
*/
|
||||
import { DEFAULT_MATERIAL, developmentLength, fyOf, hookLength, NOMINAL_DIAMETER, STANDARD_BAR, type BarSize, type Material } from '../../rebar/common';
|
||||
import type { Issue } from '../../types';
|
||||
import { ABUT_FOOTING_SIZES, type AbutFootingRebar } from './footing';
|
||||
import { gridLayout, gridText, lenText, type GridLayout, type SkewAngle } from './skew-layout';
|
||||
import { splicePlan, type SplicePlan } from './splice';
|
||||
|
||||
export interface AbutWallRebarSettings {
|
||||
/** 피복 (전 면, 철근 중심까지). */
|
||||
c: number;
|
||||
/** 첫 철근 최소 여유 (끝면 피복 다음). */
|
||||
eMin: number;
|
||||
/** 벽체 기본간격: 교대축 / 교대축 직각 / 연직. */
|
||||
sL: number;
|
||||
sW: number;
|
||||
sV: number;
|
||||
/** W7 간격 배수 (교대축 / 연직), 정수 ≥ 1. */
|
||||
k7L: number;
|
||||
k7V: number;
|
||||
/** 묶음별 간격 수정 (없으면 기본간격에 연동): 연직 W1–W4 · W2 (교대축 실간격), 수평 W5 · W6 · W8 (연직), 계단 상면 W9 · W10 (폭). */
|
||||
sVert?: number;
|
||||
sHoriz?: number;
|
||||
sTop?: number;
|
||||
}
|
||||
|
||||
export const ABUT_WALL_REBAR_DEFAULT: AbutWallRebarSettings = { c: 100, eMin: 150, sL: 125, sW: 125, sV: 125, k7L: 8, k7V: 4 };
|
||||
|
||||
/** 규격 (기획서 §1.2). W11 H16은 제안값 (삽도에 규격 표기 없음). */
|
||||
export const ABUT_WALL_SIZES = {
|
||||
W1: 'H19', W3: 'H19', W4: 'H19', W5: 'H19',
|
||||
W2: 'H16', W6: 'H16', W9: 'H16', W10: 'H16', W11: 'H16',
|
||||
W7: 'H13', W8: 'H13',
|
||||
} as const satisfies Record<string, BarSize>;
|
||||
export type WallMark = keyof typeof ABUT_WALL_SIZES;
|
||||
|
||||
/** 형상 (②에서 받음). */
|
||||
export interface AbutWallGeom {
|
||||
L: number;
|
||||
/** 벽체 배면 / 전면 t, 흉벽 배면 t (교대축 기준, 전면 +). */
|
||||
xb: number;
|
||||
xf: number;
|
||||
parapetBack: number;
|
||||
/** 저판 두께. */
|
||||
HF: number;
|
||||
angle: SkewAngle;
|
||||
/** 교좌면 계단 구간, u 순서. no = 구간 번호 (IP3 끝이 1). H = 벽체 높이 (저판 상면에서 교좌면까지). */
|
||||
segments: { from: number; to: number; H: number; no: number }[];
|
||||
/** 브라켓 상면 z (저판 하면에서) at u = 0 and u = L, straight between. */
|
||||
bracketTop: [number, number];
|
||||
}
|
||||
|
||||
export type WallRole = '연직' | '수평' | '간격재' | '계단 상면';
|
||||
|
||||
export interface AbutWallBar {
|
||||
mark: WallMark;
|
||||
size: BarSize;
|
||||
role: WallRole;
|
||||
spacing: string;
|
||||
actual: string;
|
||||
shape: string;
|
||||
/** 대표 길이 (구간 1) and the longest, when they differ by step. */
|
||||
length: number;
|
||||
lengthMax: number;
|
||||
count: number;
|
||||
position: string;
|
||||
note: string;
|
||||
/** 근거: 규칙 (철근표 · 결정) / 가정 (제안값 · fck 가정 정착). */
|
||||
basis: '규칙' | '가정';
|
||||
modified: boolean;
|
||||
splice?: SplicePlan;
|
||||
}
|
||||
|
||||
/** One bar as a polyline in (u, t, z), for the drawings and 3D. */
|
||||
export interface WallBarPath {
|
||||
mark: WallMark;
|
||||
pts: [number, number, number][];
|
||||
}
|
||||
|
||||
export interface AbutWallRebar {
|
||||
settings: AbutWallRebarSettings;
|
||||
/** 단면 위치 (t). */
|
||||
x: { W1: number; W34: number; W5: number; W6: number; W9: number[] };
|
||||
/** 연직철근 격자 (u) and its text; W1 · W3 = 짝수, W4 = 홀수. */
|
||||
vertical: { positions: number[]; layout: GridLayout; text: string; shift: number };
|
||||
zHook: { H19: number; H16: number };
|
||||
/** 구간별 수평 단 수와 치수 문자열 ("17@125=2.125 · 147"). */
|
||||
levels: { no: number; H: number; count: number; text: string }[];
|
||||
ld: Record<'H13' | 'H16' | 'H19', number>;
|
||||
paths: WallBarPath[];
|
||||
bars: AbutWallBar[];
|
||||
issues: Issue[];
|
||||
}
|
||||
|
||||
const d = (size: BarSize) => NOMINAL_DIAMETER[size];
|
||||
|
||||
export function resolveWallRebar(changes?: Partial<AbutWallRebarSettings>): AbutWallRebarSettings {
|
||||
return { ...ABUT_WALL_REBAR_DEFAULT, ...(changes ?? {}) };
|
||||
}
|
||||
|
||||
/** Input problems in Korean (the edit refuses them). */
|
||||
export function wallRebarIssues(s: AbutWallRebarSettings): string[] {
|
||||
const out: string[] = [];
|
||||
for (const key of ['c', 'eMin', 'sL', 'sW', 'sV'] as const) if (!(s[key] > 0)) out.push(`${key}은(는) 0보다 커야 합니다.`);
|
||||
for (const key of ['sVert', 'sHoriz', 'sTop'] as const) if (s[key] !== undefined && !((s[key] as number) > 0)) out.push('철근 간격은 0보다 커야 합니다.');
|
||||
if (!(Number.isInteger(s.k7L) && s.k7L >= 1 && Number.isInteger(s.k7V) && s.k7V >= 1)) out.push('W7 배수는 1 이상의 정수여야 합니다.');
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 벽체 형상 from the abutment members (m) → mm. */
|
||||
export function abutWallGeom(
|
||||
lengthM: number,
|
||||
members: {
|
||||
shape: { wallFrontM: number; wallBackM: number; parapetBackM: number; footingThicknessM: number; bracketHeightM: number };
|
||||
wallSegments: { sFrom: number; sTo: number; heightM: number }[];
|
||||
parapetTopStartEl: number;
|
||||
parapetTopEndEl: number;
|
||||
footingBottomEl: number | undefined;
|
||||
},
|
||||
angle: SkewAngle,
|
||||
): AbutWallGeom {
|
||||
const mm = (m: number) => Math.round(m * 1e6) / 1e3;
|
||||
const segs = [...members.wallSegments].sort((a, b) => a.sFrom - b.sFrom);
|
||||
const bottom = members.footingBottomEl ?? 0;
|
||||
const bracket = (el: number) => mm(el - members.shape.bracketHeightM - bottom);
|
||||
return {
|
||||
L: mm(lengthM),
|
||||
xb: mm(members.shape.wallBackM),
|
||||
xf: mm(members.shape.wallFrontM),
|
||||
parapetBack: mm(members.shape.parapetBackM),
|
||||
HF: mm(members.shape.footingThicknessM),
|
||||
angle,
|
||||
segments: segs.map((sg, i) => ({ from: mm(sg.sFrom), to: mm(sg.sTo), H: mm(sg.heightM), no: segs.length - i })),
|
||||
bracketTop: [bracket(members.parapetTopStartEl), bracket(members.parapetTopEndEl)],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The wall rebar of one abutment. `footing` gives the 저판 grid (B1 · B3) and the height of B6 · B8 (연직철근 갈고리).
|
||||
* `who` names the abutment in the issues.
|
||||
*/
|
||||
export function abutWallRebar(g: AbutWallGeom, footing: AbutFootingRebar, changes?: Partial<AbutWallRebarSettings>, material: Material = DEFAULT_MATERIAL, who = ''): AbutWallRebar {
|
||||
const s = resolveWallRebar(changes);
|
||||
const issues: Issue[] = [];
|
||||
const add = (code: string, level: Issue['level'], message: string) => issues.push({ code, level, pier: who || undefined, message });
|
||||
const SZ = ABUT_WALL_SIZES;
|
||||
const sin = g.angle.sin;
|
||||
const c = s.c;
|
||||
const sVert = s.sVert ?? s.sL;
|
||||
const sHoriz = s.sHoriz ?? s.sV;
|
||||
const sTop = s.sTop ?? 2 * s.sW;
|
||||
if (s.sVert !== undefined || s.sHoriz !== undefined || s.sTop !== undefined || s.k7L !== 8 || s.k7V !== 4) {
|
||||
add('RB-AB04', 'I', '철근 간격을 기본간격과 따로 고쳤거나 W7 배수가 기본값(8 × 4)이 아닙니다. [되돌리기]로 다시 연동합니다.');
|
||||
}
|
||||
const fy = (size: BarSize) => fyOf(material, size);
|
||||
const ldOf = (size: BarSize) => Math.ceil(developmentLength(d(size), fy(size), material.fck) - 1e-9);
|
||||
const ld = { H13: ldOf('H13'), H16: ldOf('H16'), H19: ldOf('H19') };
|
||||
const hook = (size: BarSize) => Math.round(hookLength('main90', d(size)));
|
||||
|
||||
const segs = g.segments;
|
||||
const last = segs.length - 1;
|
||||
const segIndex = (u: number) => {
|
||||
const i = segs.findIndex((sg) => u < sg.to);
|
||||
return i < 0 ? last : i;
|
||||
};
|
||||
const zTop = (u: number) => g.HF + segs[segIndex(u)].H;
|
||||
const bracketTop = (u: number) => g.bracketTop[0] + ((g.bracketTop[1] - g.bracketTop[0]) * u) / g.L;
|
||||
const one = segs.find((sg) => sg.no === 1) ?? segs[0];
|
||||
const inOne = (u: number) => segs[segIndex(u)] === one;
|
||||
|
||||
// 단면 위치 (§3).
|
||||
const xW5 = g.xf - c;
|
||||
const xW1 = xW5 - (d(SZ.W5) + d(SZ.W1)) / 2;
|
||||
const xW34 = g.xb + c;
|
||||
const xW6 = xW34 + (d(SZ.W3) + d(SZ.W6)) / 2;
|
||||
const xW9: number[] = [];
|
||||
for (let x = xW1 - (d(SZ.W1) + d(SZ.W9)) / 2; x >= c - 1e-9; x -= sTop) xW9.push(x);
|
||||
|
||||
// 연직철근 격자 (§4.1 · §4.3): the wall's own grid, moved off the 저판 상면 철근.
|
||||
const layout = gridLayout(g.L, c / sin, s.eMin, sVert);
|
||||
const shift = (d(ABUT_FOOTING_SIZES.top) + d(SZ.W1)) / 2;
|
||||
const us = layout.positions.map((p) => p - shift);
|
||||
if (us.length < 2) add('RB-AB01', 'E', '연직철근을 놓을 수 없습니다 (길이 · 간격 확인).');
|
||||
if (footing.dir === 'skew') {
|
||||
const fTop = footing.main.positions.map((p, i) => (i % 2 === 0 ? p + footing.sideShift.B1 : p));
|
||||
const gap = (d(ABUT_FOOTING_SIZES.top) + d(SZ.W1)) / 2;
|
||||
const hits = us.filter((u) => fTop.some((f) => Math.abs(u - f) < gap - 1e-6)).length;
|
||||
if (hits) add('RB-AB03', 'W', `벽체 연직철근 ${hits}개가 저판 상면 철근과 중심 거리 ${lenText(gap)} 미만입니다 (벽체 · 저판 기본간격 확인).`);
|
||||
}
|
||||
|
||||
// 갈고리 높이 (§4.2): 저판 하면 철근망 B6 · B8 위.
|
||||
const zHookOf = (size: BarSize) => footing.z.B68 + (d(ABUT_FOOTING_SIZES.dist) + d(size)) / 2;
|
||||
const zHook = { H19: zHookOf('H19'), H16: zHookOf('H16') };
|
||||
|
||||
const paths: WallBarPath[] = [];
|
||||
const lengths = new Map<WallMark, number[]>();
|
||||
const repr = new Map<WallMark, number>();
|
||||
const put = (mark: WallMark, pts: [number, number, number][], length: number, representative: boolean) => {
|
||||
paths.push({ mark, pts });
|
||||
lengths.set(mark, [...(lengths.get(mark) ?? []), length]);
|
||||
if (representative && !repr.has(mark)) repr.set(mark, length);
|
||||
};
|
||||
|
||||
// W1 · W3 · W4 (§4.2): hooks outward (W1 앞굽 쪽, W3 · W4 뒷굽 쪽). W3 위 끝 = 교좌면 + l_d, 브라켓 상면 − c 이하.
|
||||
const h19 = hook(SZ.W1);
|
||||
let capped = 0;
|
||||
us.forEach((u, i) => {
|
||||
const top = zTop(u);
|
||||
const zh = zHook.H19;
|
||||
if (i % 2 === 0) {
|
||||
put('W1', [[u, xW1 + h19, zh], [u, xW1, zh], [u, xW1, top - c]], top - c - zh + h19, inOne(u));
|
||||
let up = top + ld.H19;
|
||||
if (up > bracketTop(u) - c) {
|
||||
up = bracketTop(u) - c;
|
||||
capped++;
|
||||
}
|
||||
put('W3', [[u, xW34 - h19, zh], [u, xW34, zh], [u, xW34, up]], up - zh + h19, inOne(u));
|
||||
} else {
|
||||
put('W4', [[u, xW34 - h19, zh], [u, xW34, zh], [u, xW34, top - c]], top - c - zh + h19, inOne(u));
|
||||
}
|
||||
});
|
||||
if (capped) add('RB-AB02', 'W', `W3 ${capped}개의 위 끝(교좌면 + l_d)이 브라켓 상면 − ${lenText(c)}를 넘어 그 높이에서 끊었습니다.`);
|
||||
|
||||
// W2 (§5.1): W1 위치 + 옆붙임, 뒤끝 90° 갈고리 아래로, 앞 다리 l_d 아래로.
|
||||
const h16 = hook(SZ.W2);
|
||||
const shiftW2 = (d(SZ.W1) + d(SZ.W2)) / 2;
|
||||
us.forEach((u, i) => {
|
||||
if (i % 2 !== 0) return;
|
||||
const z = zTop(u) - c;
|
||||
const u2 = u + shiftW2;
|
||||
put('W2', [[u2, xW34, z - h16], [u2, xW34, z], [u2, xW1, z], [u2, xW1, z - ld.H16]], (xW1 - xW34) / sin + h16 + ld.H16, inOne(u));
|
||||
});
|
||||
|
||||
// 끝 여유 (§6.1): W5 · W6 · W9 · W10 끝은 W11 안쪽.
|
||||
const endIn = (size: BarSize) => (c + (d(SZ.W11) + d(size)) / 2) / sin;
|
||||
|
||||
// W9 · W10 (§5.2): W2 바로 아래, 낮은 이웃 구간으로 l_d 연장, 높은 이웃 쪽은 경계에서 끝.
|
||||
segs.forEach((sg, k) => {
|
||||
const mark: WallMark = k === 0 || k === last ? 'W9' : 'W10';
|
||||
const z = g.HF + sg.H - c - (d(SZ.W2) + d(SZ.W9)) / 2;
|
||||
const a = k === 0 ? endIn(SZ.W9) : segs[k - 1].H < sg.H ? sg.from - ld.H16 : sg.from;
|
||||
const b = k === last ? g.L - endIn(SZ.W9) : segs[k + 1].H < sg.H ? sg.to + ld.H16 : sg.to;
|
||||
for (const x of xW9) put(mark, [[a, x, z], [b, x, z]], b - a, sg === one || mark === 'W10');
|
||||
});
|
||||
|
||||
// W5 · W6 (§5.3): 단 z_k = H_F + c + k·s_v, 들어가는 구간이 이어진 범위마다 한 줄. 계단 쪽 끝 = 경계 − c.
|
||||
const levelsOf = (H: number) => Math.floor((H - 2 * c) / sHoriz + 1e-9) + 1;
|
||||
const zMax = g.HF + Math.max(...segs.map((sg) => sg.H)) - c;
|
||||
for (let k = 0; g.HF + c + k * sHoriz <= zMax + 1e-9; k++) {
|
||||
const z = g.HF + c + k * sHoriz;
|
||||
const ok = segs.map((sg) => z <= g.HF + sg.H - c + 1e-9);
|
||||
for (let a = 0; a <= last; a++) {
|
||||
if (!ok[a] || (a > 0 && ok[a - 1])) continue;
|
||||
let b = a;
|
||||
while (b < last && ok[b + 1]) b++;
|
||||
for (const [mark, x] of [['W5', xW5], ['W6', xW6]] as const) {
|
||||
const ua = a === 0 ? endIn(SZ[mark]) : segs[a].from + c;
|
||||
const ub = b === last ? g.L - endIn(SZ[mark]) : segs[b].to - c;
|
||||
put(mark, [[ua, x, z], [ub, x, z]], ub - ua, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
const levels = [...segs]
|
||||
.sort((p, q) => p.no - q.no)
|
||||
.map((sg) => {
|
||||
const n = levelsOf(sg.H);
|
||||
const gap = sg.H - 2 * c - (n - 1) * sHoriz;
|
||||
return { no: sg.no, H: sg.H, count: n, text: `${n - 1}@${lenText(sHoriz)}=${lenText((n - 1) * sHoriz)} · ${lenText(c + gap)}` };
|
||||
});
|
||||
if (segs.length > 1 && levels.every((lv) => lv.count === levels[0].count)) add('RB-AB05', 'I', '계단 높이 차가 작아 수평철근 단 수가 구간마다 같습니다 (W5 · W6 끊김 없음).');
|
||||
|
||||
// W7 (§6): 교대축 @k_7L·s_L, 연직 @k_7v·s_v, W3 선 ~ W5 선 + 양끝 90° 갈고리.
|
||||
const h13 = hook(SZ.W7);
|
||||
const g7 = gridLayout(g.L, c / sin, s.eMin, s.k7L * sVert);
|
||||
for (const u of g7.positions) {
|
||||
for (let z = g.HF + c; z <= zTop(u) - c + 1e-9; z += s.k7V * sHoriz) {
|
||||
put('W7', [[u, xW34, z - h13], [u, xW34, z], [u, xW5, z], [u, xW5, z - h13]], xW5 - xW34 + 2 * h13, inOne(u));
|
||||
}
|
||||
}
|
||||
|
||||
// W8 (§6): 양 끝면, W11 안쪽, ㄷ형 다리 l_d. 단 수는 그 끝 구간 높이.
|
||||
const in8 = (c + (d(SZ.W11) + d(SZ.W8)) / 2) / sin;
|
||||
for (const [u, dir, sg] of [[in8, 1, segs[0]], [g.L - in8, -1, segs[last]]] as const) {
|
||||
for (let z = g.HF + c; z <= g.HF + sg.H - c + 1e-9; z += sHoriz) {
|
||||
const leg = u + dir * ld.H13;
|
||||
put('W8', [[leg, g.xb + c, z], [u, g.xb + c, z], [u, g.xf - c, z], [leg, g.xf - c, z]], (g.xf - g.xb - 2 * c) / sin + 2 * ld.H13, true);
|
||||
}
|
||||
}
|
||||
|
||||
// W11 (§6.1): 끝면 최외측 연직. 교좌면 구간 x = 0 … x_f − c @s_W, 흉벽 · 브라켓 아래 x_b + c · 흉벽 배면 + c · −c.
|
||||
const xs11: number[] = [g.xb + c, g.parapetBack + c, -c];
|
||||
const seat11: number[] = [];
|
||||
for (let x = 0; x <= xW5 + 1e-9; x += s.sW) seat11.push(x);
|
||||
const zh16 = zHook.H16;
|
||||
for (const [u, dir, sg] of [[c / sin, 1, segs[0]], [g.L - c / sin, -1, segs[last]]] as const) {
|
||||
const top = g.HF + sg.H - c;
|
||||
// The hook turns inward along the axis (the end face has no concrete outside).
|
||||
for (const x of [...xs11, ...seat11]) put('W11', [[u + dir * h16, x, zh16], [u, x, zh16], [u, x, top]], top - zh16 + h16, sg === one);
|
||||
}
|
||||
|
||||
// 표 (시안 순서).
|
||||
const bars: AbutWallBar[] = [];
|
||||
const push = (b: Omit<AbutWallBar, 'length' | 'lengthMax' | 'count' | 'splice'>) => {
|
||||
const all = lengths.get(b.mark) ?? [];
|
||||
if (all.length === 0) return;
|
||||
const max = Math.max(...all);
|
||||
const splice = splicePlan(max, d(b.size), fy(b.size), material.fck) ?? undefined;
|
||||
const note = splice ? `${b.note} · ${splice.pieces}조각 이음 l_s ${splice.lap}` : b.note;
|
||||
if (splice) add('RB-AB01', splice.over ? 'W' : 'I', `${b.mark} ${Math.round(max)} > 정척 ${STANDARD_BAR}: ${splice.pieces}조각 이음 l_s ${splice.lap}.`);
|
||||
bars.push({ ...b, note, length: Math.round(repr.get(b.mark) ?? Math.min(...all)), lengthMax: Math.round(max), count: all.length, splice });
|
||||
};
|
||||
const vMod = s.sVert !== undefined;
|
||||
const hMod = s.sHoriz !== undefined;
|
||||
const tMod = s.sTop !== undefined;
|
||||
const vSp = lenText(2 * sVert);
|
||||
const vText = gridText(layout);
|
||||
push({ mark: 'W1', size: SZ.W1, role: '연직', spacing: vSp, actual: vSp, shape: `아래 90° 갈고리 ${h19} (앞굽 쪽)`, position: `전면 x ${lenText(xW1)} (W5 안쪽) · cycle-1만`, note: `격자 = 저판 − ${lenText(shift)} · 위 교좌면 − ${lenText(c)} · 길이는 구간 1 (구간마다 다름)`, basis: '규칙', modified: vMod });
|
||||
push({ mark: 'W3', size: SZ.W3, role: '연직', spacing: vSp, actual: lenText(sVert), shape: `아래 갈고리 (뒷굽 쪽) · 위 교좌면 + l_d ${ld.H19}`, position: `배면 x ${lenText(xW34)} · cycle-1`, note: `l_d ${ld.H19} (fck ${material.fck}) · 브라켓 상면 − ${lenText(c)} 이하`, basis: '가정', modified: vMod });
|
||||
push({ mark: 'W4', size: SZ.W4, role: '연직', spacing: vSp, actual: lenText(sVert), shape: '아래 갈고리 (뒷굽 쪽)', position: `배면 x ${lenText(xW34)} · cycle-2`, note: `위 교좌면 − ${lenText(c)} · 교대축 ${vText}`, basis: '규칙', modified: vMod });
|
||||
push({ mark: 'W2', size: SZ.W2, role: '수평', spacing: vSp, actual: vSp, shape: `⊓ 뒤 갈고리 ${h16} · 앞 다리 l_d ${ld.H16}`, position: `교좌면 − ${lenText(c)} · 교대축 직각 (skew) · x ${lenText(xW34)} ~ ${lenText(xW1)}`, note: `W1 위치 +${lenText(shiftW2)} 옆붙임`, basis: '가정', modified: vMod });
|
||||
const hSp = lenText(sHoriz);
|
||||
const lvText = levels.map((lv) => `구간 ${lv.no} ${lv.count}단`).join(' · ');
|
||||
push({ mark: 'W5', size: SZ.W5, role: '수평', spacing: hSp, actual: hSp, shape: '직선 · 계단에서 끊음', position: `전면 최외측 x ${lenText(xW5)} · 교대축`, note: `${lvText} · 끝은 W11 안쪽`, basis: '규칙', modified: hMod });
|
||||
push({ mark: 'W6', size: SZ.W6, role: '수평', spacing: hSp, actual: hSp, shape: '직선 · 계단에서 끊음', position: `배면 W3 · W4 안쪽 x ${lenText(xW6)} · 교대축`, note: 'W5와 같은 단', basis: '규칙', modified: hMod });
|
||||
push({ mark: 'W7', size: SZ.W7, role: '간격재', spacing: `${lenText(s.k7L * sVert)} × ${lenText(s.k7V * sHoriz)}`, actual: '—', shape: `양끝 90° 갈고리 ${h13}`, position: `내측 간격재 · W3 선 ~ W5 선`, note: `${s.k7L}·s × ${s.k7V}·s · 철근표 H13 · 길이는 기획서 §6 값 (skew 1/sin θ 미반영)`, basis: '규칙', modified: s.k7L !== 8 || s.k7V !== 4 });
|
||||
push({ mark: 'W8', size: SZ.W8, role: '수평', spacing: hSp, actual: hSp, shape: `ㄷ형 · 다리 l_d ${ld.H13}`, position: `양 끝면 (skew) · W11 안쪽 ${lenText(in8 * sin)}`, note: `낮은 끝 · 높은 끝 구간 높이로 단 수`, basis: '규칙', modified: hMod });
|
||||
const tSp = lenText(sTop);
|
||||
push({ mark: 'W9', size: SZ.W9, role: '계단 상면', spacing: tSp, actual: tSp, shape: `직선 · 낮은 쪽 이웃 구간으로 l_d ${ld.H16}`, position: `끝 계단 상면 · 교대축 · 폭 ${xW9.length}개`, note: `W2 바로 아래 · 첫 줄 ${lenText(xW9[0] ?? 0)} (W1 선 피함)`, basis: '규칙', modified: tMod });
|
||||
push({ mark: 'W10', size: SZ.W10, role: '계단 상면', spacing: tSp, actual: tSp, shape: `직선 · 낮은 쪽 이웃 구간으로 l_d ${ld.H16}`, position: `중간 계단 상면 · 교대축 · 폭 ${xW9.length}개`, note: '결정: W9 끝 계단 / W10 중간 계단', basis: '규칙', modified: tMod });
|
||||
push({ mark: 'W11', size: SZ.W11, role: '연직', spacing: `${lenText(s.sW)} / 300`, actual: '—', shape: `아래 90° 갈고리 ${h16}`, position: `끝면 최외측 연직 · x 0 … ${lenText(xW5)} @${lenText(s.sW)} + ${xs11.map(lenText).join(' · ')}`, note: '규격 H16 제안 (삽도 표기 없음) · 흉벽 P11로 이어짐', basis: '가정', modified: false });
|
||||
|
||||
return {
|
||||
settings: s,
|
||||
x: { W1: xW1, W34: xW34, W5: xW5, W6: xW6, W9: xW9 },
|
||||
vertical: { positions: us, layout, text: vText, shift },
|
||||
zHook,
|
||||
levels,
|
||||
ld,
|
||||
paths,
|
||||
bars,
|
||||
issues,
|
||||
};
|
||||
}
|
||||
@@ -137,6 +137,8 @@ export function spliceLength(db: number, fy: number, fck: number, lambda = MATER
|
||||
/** Nominal diameters d (mm) of the bar sizes used in the documents (코핑철근.md, 기둥철근.md). */
|
||||
export const NOMINAL_DIAMETER = {
|
||||
D13: 12.7,
|
||||
/** 교대 벽체 W7 · W8 (교대벽체철근.md §1.2, 2026-11-04). */
|
||||
H13: 12.7,
|
||||
H16: 15.9,
|
||||
H19: 19.1,
|
||||
D22: 22.2,
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildAbut } from '../src/core/abut/geometry';
|
||||
import { parseAbutments, toAbutInput } from '../src/core/abut/parse';
|
||||
import { buildMembers } from '../src/core/abut/members';
|
||||
import { abutFootingGeom, abutFootingRebar } from '../src/core/abut/rebar/footing';
|
||||
import { skewAngle } from '../src/core/abut/rebar/skew-layout';
|
||||
import { abutWallGeom, abutWallRebar, type AbutWallGeom } from '../src/core/abut/rebar/wall';
|
||||
import { DEFAULT_MATERIAL } from '../src/core/rebar/common';
|
||||
|
||||
// 예제 A1 (교대벽체철근.md §11). 기획서 기대값은 fck 27 · fy 400.
|
||||
const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url));
|
||||
const rec = parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abutments[0];
|
||||
const model = buildAbut(toAbutInput(rec));
|
||||
const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm });
|
||||
const angle = skewAngle(rec.skewDeg);
|
||||
const fGeom = abutFootingGeom(model.length, members.shape, angle);
|
||||
const footing = abutFootingRebar(fGeom);
|
||||
const geom = abutWallGeom(model.length, members, angle);
|
||||
const fck27 = { ...DEFAULT_MATERIAL, fck: 27 };
|
||||
const A1 = abutWallRebar(geom, footing, undefined, fck27, 'A1');
|
||||
const bar = (r: typeof A1, mark: string) => r.bars.find((b) => b.mark === mark)!;
|
||||
|
||||
describe('교대 벽체 배근 A1 (AD4, 교대벽체철근.md §11)', () => {
|
||||
it('geometry: 5 steps, 구간 1 (IP3 끝) H 2372, 구간 5 H 2656, 벽체 −800 ~ +1600', () => {
|
||||
expect(geom.segments.length).toBe(5);
|
||||
expect([geom.xb, geom.xf, geom.HF]).toEqual([-800, 1600, 1100]);
|
||||
const byNo = (no: number) => geom.segments.find((s) => s.no === no)!;
|
||||
expect(byNo(1).H).toBeCloseTo(2372, 0);
|
||||
expect(byNo(5).H).toBeCloseTo(2656, 0);
|
||||
});
|
||||
|
||||
it('위치: W5 1500, W1 1480.9, W3 · W4 −700, W6 −682.5, W9 첫 줄 1463.4 · 폭 6개', () => {
|
||||
expect(A1.x.W5).toBeCloseTo(1500, 6);
|
||||
expect(A1.x.W1).toBeCloseTo(1480.9, 6);
|
||||
expect(A1.x.W34).toBeCloseTo(-700, 6);
|
||||
expect(A1.x.W6).toBeCloseTo(-682.5, 6);
|
||||
expect(A1.x.W9[0]).toBeCloseTo(1463.4, 6);
|
||||
expect(A1.x.W9.length).toBe(6);
|
||||
});
|
||||
|
||||
it('교대축 배치: 93@125, e 185.8, 저판 격자 − 17.5 → W1 47, W3 47, W4 47', () => {
|
||||
expect([A1.vertical.layout.n, A1.vertical.layout.positions.length]).toEqual([93, 94]);
|
||||
expect(A1.vertical.layout.e).toBeCloseTo(185.8, 1);
|
||||
expect(A1.vertical.shift).toBeCloseTo(17.5, 6);
|
||||
expect(A1.vertical.positions[0]).toBeCloseTo(footing.main.positions[0] - 17.5, 6);
|
||||
expect(['W1', 'W3', 'W4'].map((m) => bar(A1, m).count)).toEqual([47, 47, 47]);
|
||||
});
|
||||
|
||||
it('높이 (구간 1): z_hook 186.55, W1 · W4 3481, W3 4464', () => {
|
||||
expect(A1.zHook.H19).toBeCloseTo(186.55, 2);
|
||||
expect([bar(A1, 'W1').length, bar(A1, 'W4').length, bar(A1, 'W3').length]).toEqual([3481, 3481, 4464]);
|
||||
});
|
||||
|
||||
it('W2: 2189.4 + 246 + 735 = 3170, W1 위치 +17.5', () => {
|
||||
expect(bar(A1, 'W2').length).toBe(3170);
|
||||
const w1 = A1.paths.find((p) => p.mark === 'W1')!.pts[0][0];
|
||||
const w2 = A1.paths.find((p) => p.mark === 'W2')!.pts[0][0];
|
||||
expect(w2 - w1).toBeCloseTo(17.5, 6);
|
||||
});
|
||||
|
||||
it('수평 단: 구간 1 17@125=2.125 · 147 (18단), 구간 5 19@125=2.375 · 181 (20단)', () => {
|
||||
const lv = (no: number) => A1.levels.find((l) => l.no === no)!;
|
||||
expect([lv(1).count, lv(1).text]).toEqual([18, '17@125=2.125 · 147']);
|
||||
expect([lv(5).count, lv(5).text]).toEqual([20, '19@125=2.375 · 181']);
|
||||
});
|
||||
|
||||
it('W9 · W10: 5구간 → W9 2구간 × 6, W10 3구간 × 6; 구간 수에 따라 W9만 / W10 (m − 2)', () => {
|
||||
expect(bar(A1, 'W9').count).toBe(12);
|
||||
expect(bar(A1, 'W10').count).toBe(18);
|
||||
const withSegs = (n: number): AbutWallGeom => {
|
||||
const step = geom.L / n;
|
||||
return { ...geom, segments: Array.from({ length: n }, (_, i) => ({ from: i * step, to: (i + 1) * step, H: 2372 + 50 * i, no: n - i })) };
|
||||
};
|
||||
const marks = (n: number) => abutWallRebar(withSegs(n), footing, undefined, fck27).bars.map((b) => b.mark);
|
||||
expect(marks(1)).toContain('W9');
|
||||
expect(marks(1)).not.toContain('W10');
|
||||
expect(marks(2)).not.toContain('W10');
|
||||
expect(bar(abutWallRebar(withSegs(4), footing, undefined, fck27), 'W10').count).toBe(12);
|
||||
});
|
||||
|
||||
it('W9 extends l_d 735 into the lower neighbour only', () => {
|
||||
const k = 2; // a middle step, neighbours on both sides
|
||||
const sg = geom.segments[k];
|
||||
const z = 1100 + sg.H - 100 - 15.9;
|
||||
const p = A1.paths.find((q) => q.mark === 'W10' && Math.abs(q.pts[0][2] - z) < 1e-6)!;
|
||||
const lowLeft = geom.segments[k - 1].H < sg.H;
|
||||
const lowRight = geom.segments[k + 1].H < sg.H;
|
||||
expect(p.pts[0][0]).toBeCloseTo(lowLeft ? sg.from - 735 : sg.from, 6);
|
||||
expect(p.pts[1][0]).toBeCloseTo(lowRight ? sg.to + 735 : sg.to, 6);
|
||||
});
|
||||
|
||||
it('W7: 12 × 5 = 60, 2594', () => {
|
||||
expect([bar(A1, 'W7').count, bar(A1, 'W7').length]).toEqual([60, 2594]);
|
||||
});
|
||||
|
||||
it('W8: 3383, 낮은 끝 18 + 높은 끝 20 = 38, 끝면에서 114.3', () => {
|
||||
expect([bar(A1, 'W8').count, bar(A1, 'W8').length]).toEqual([38, 3383]);
|
||||
const w8 = A1.paths.find((p) => p.mark === 'W8')!;
|
||||
expect(w8.pts[1][0] * angle.sin).toBeCloseTo(114.3, 6);
|
||||
});
|
||||
|
||||
it('W11: 끝면당 13 + 3 = 16, 양끝 32, 길이 3433 / 3717', () => {
|
||||
const w = bar(A1, 'W11');
|
||||
expect([w.count, w.length, w.lengthMax]).toEqual([32, 3433, 3717]);
|
||||
const xs = A1.paths.filter((p) => p.mark === 'W11').slice(0, 16).map((p) => Math.round(p.pts[1][1]));
|
||||
expect(xs).toEqual([-700, -400, -100, ...Array.from({ length: 13 }, (_, i) => i * 125)]);
|
||||
});
|
||||
|
||||
it('A1 has no warning (RB-AB02 · RB-AB03 없음)', () => {
|
||||
expect(A1.issues.filter((i) => i.level !== 'I').map((i) => i.code)).toEqual([]);
|
||||
});
|
||||
|
||||
it('l_d 연동: fck 30 → W2 앞 다리 · W3 위 끝이 짧아짐', () => {
|
||||
const r30 = abutWallRebar(geom, footing, undefined, { ...DEFAULT_MATERIAL, fck: 30 });
|
||||
expect(r30.ld.H16).toBeLessThan(A1.ld.H16);
|
||||
expect(bar(r30, 'W2').length).toBe(3170 - (A1.ld.H16 - r30.ld.H16));
|
||||
expect(bar(r30, 'W3').length).toBe(4464 - (A1.ld.H19 - r30.ld.H19));
|
||||
expect(bar(r30, 'W1').length).toBe(3481);
|
||||
});
|
||||
|
||||
it('기본간격 연동: 벽체 s_L 150 → 벽체만 다시 계산, 저판 그대로, RB-AB03 검사', () => {
|
||||
const r = abutWallRebar(geom, footing, { sL: 150 }, fck27);
|
||||
expect(r.vertical.layout.s).toBe(150);
|
||||
expect(footing.main.layout!.s).toBe(125);
|
||||
expect(r.issues.map((i) => i.code)).toContain('RB-AB03');
|
||||
});
|
||||
|
||||
it('묶음 수정 · W7 배수 → RB-AB04 "수정됨"', () => {
|
||||
const r = abutWallRebar(geom, footing, { sHoriz: 150, k7L: 6 }, fck27);
|
||||
expect(r.issues.map((i) => i.code)).toContain('RB-AB04');
|
||||
expect(bar(r, 'W5').modified).toBe(true);
|
||||
expect(bar(r, 'W7').modified).toBe(true);
|
||||
expect(bar(r, 'W1').modified).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user