feat(abut-rebar): 교대 날개벽 배근 규칙 WR1–WR20 (AD7)
- 연직 WR1 · 2 · 11 · 12 (기준선 뒷굽 끝 − c, 등단면 올림 δ, 변화단면 끝 여유), 수평 WR3–5 · 13–15 (띠 3개, 맨 위 2단 경사와 나란히 Q-WR1, 교대 쪽 갈고리) - WR6 경사 하단, WR7-1 · 7-2 · 7-3 헌치 꺾임, WR9 헌치 대각 @2·s_h 자동 개수 (Q-WR3), WR10 모서리 꺾임, WR20 간격재. 간섭은 보지 않음 (Q-WR2 · Q-WR4) - 단축형: WR1 · WR11 · WR6 없음. 좌우는 ② 형상 그대로 (skew로 배면 길이 ± 두께 · cot φ) - 테스트: 교대날개벽철근.md §11 (fck 27, 벽체 높이 2500), 삽도 = 우측 날개벽 치수 재현 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3800a75b78
commit
06220fa312
2 files changed
+565
No files matched your search
@@ -0,0 +1,473 @@
|
||||
/**
|
||||
* 교대 날개벽 배근 · AD7 (2026-11-04, 교대날개벽철근.md, 결정 2026-11-02 · 2026-11-04). 순수 함수, 길이는 mm.
|
||||
*
|
||||
* - 연직: WR1 · WR2 배면 (@s_h), WR11 · WR12 전면 (@2·s_h). 기준선 = 면 위 뒷굽 끝 − c. 등단면(WR2 · WR12)은 교대 쪽으로
|
||||
* ceil((L_heel − δ)/s) + 1개, 저판 하면망 위 갈고리. 변화단면(WR1 · WR11)은 끝 쪽으로 floor((끝 − c − 기준선 − e_min)/s)개, 아래 끝 경사 하단 + c.
|
||||
* - 수평: WR3–5 배면 (@s_v), WR13–15 전면 (@2·s_v), 띠 = 등단면 / 변화단면1 / 변화단면2. 맨 위 2단은 상단 경사와 나란히
|
||||
* (사용자 결정 2026-11-04, Q-WR1), 일반 단은 그 아래 s 이상. 교대 쪽 끝은 교대 단면을 지나 반대쪽 면 − c, 90° 갈고리.
|
||||
* - WR6 경사 하단 2개, WR7-1 · 7-2 · 7-3 헌치 꺾임 (전면 수평과 같은 단), WR9 헌치 대각 위 연직 (@2·s_h 자동 개수, Q-WR3),
|
||||
* WR10 모서리 꺾임 (@s_v), WR20 간격재. 교대 철근과의 간섭은 보지 않는다 (Q-WR2 · Q-WR4 결정).
|
||||
* - 단축형 (QA-14 · §9): WR1 · WR11 · WR6 없음, 연직은 모두 등단면.
|
||||
* - 좌표: 교대 (s, t, z) — s 교대축 (IP1에서), t 교대축 직각 (전면 +), z 저판 하면에서. 날개벽 거리 d는 교대축 선에서 날개벽 방향.
|
||||
*/
|
||||
import { DEFAULT_MATERIAL, developmentLength, fyOf, hookLength, NOMINAL_DIAMETER, STANDARD_BAR, type BarSize, type Material } from '../../rebar/common';
|
||||
import type { Issue } from '../../types';
|
||||
import { lenText } from './skew-layout';
|
||||
import { splicePlan, type SplicePlan } from './splice';
|
||||
|
||||
export interface AbutWingRebarSettings {
|
||||
c: number;
|
||||
/** 날개벽 기본간격: 수평 (연직철근이 늘어서는 간격) / 연직 (수평 단 간격). 배면 s, 전면 · 헌치 2s. */
|
||||
sH: number;
|
||||
sV: number;
|
||||
/** 등단면 개수 올림 허용차. */
|
||||
delta: number;
|
||||
eMin: number;
|
||||
/** WR20 배수 (수평 · 연직). */
|
||||
k20: number;
|
||||
/** 묶음별 간격 수정 (배면 값, 전면은 2배): 연직 WR1 · 2 · 11 · 12, 수평 WR3–5 · 13–15. */
|
||||
sVert?: number;
|
||||
sHoriz?: number;
|
||||
}
|
||||
|
||||
export const ABUT_WING_REBAR_DEFAULT: AbutWingRebarSettings = { c: 100, sH: 125, sV: 125, delta: 25, eMin: 150, k20: 8 };
|
||||
|
||||
/** 규격 (기획서 §1.2). WR8 · WR16–19는 결번. */
|
||||
export const ABUT_WING_SIZES = {
|
||||
WR1: 'H16', WR2: 'H16', WR11: 'H16', WR12: 'H16', WR6: 'H16', WR9: 'H16',
|
||||
WR3: 'H25', WR4: 'H25', WR5: 'H25', WR13: 'H25', WR14: 'H25', WR15: 'H25',
|
||||
'WR7-1': 'H25', 'WR7-2': 'H25', 'WR7-3': 'H25', WR10: 'H25',
|
||||
WR20: 'H13',
|
||||
} as const satisfies Record<string, BarSize>;
|
||||
export type WingMark = keyof typeof ABUT_WING_SIZES;
|
||||
|
||||
type V2 = [number, number];
|
||||
|
||||
/** 한 측 날개벽 형상 (mm, 교대 s · t, z는 저판 하면에서). */
|
||||
export interface AbutWingGeom {
|
||||
side: 'left' | 'right';
|
||||
form: 'general' | 'short';
|
||||
/** 전면(바깥)면이 교대축 선과 만나는 점 (s, t). */
|
||||
p0: V2;
|
||||
/** 날개벽 방향 단위 벡터 (s, t), 교대에서 멀어지는 쪽. */
|
||||
w: V2;
|
||||
/** 전면 → 배면(흙 쪽) 면 이동 (s, t): 교대축 방향으로 두께만큼. */
|
||||
inward: V2;
|
||||
/** 날개벽 길이 (교대축 선에서), 끝 높이, 헌치 다리. */
|
||||
L: number;
|
||||
endH: number;
|
||||
haunch: number;
|
||||
/** 교대축 선에서 날개벽 방향으로 잰 뒷굽 끝 · 벽체 배면 · 흉벽 배면 거리. */
|
||||
heelD: number;
|
||||
wallBackD: number;
|
||||
parapetBackD: number;
|
||||
/** 교대 단면 t: 흉벽 배면 · 벽체 배면 · 벽체 전면. */
|
||||
parapetBackT: number;
|
||||
wallBackT: number;
|
||||
wallFrontT: number;
|
||||
/** z (저판 하면에서): 저판 상면, 날개벽 상단 시작 (교대축 선) · 경사, 단부 하단, 교좌면 (가장 가까운 받침), 브라켓 상면. */
|
||||
footTop: number;
|
||||
topStart: number;
|
||||
slope: number;
|
||||
bottomEnd: number;
|
||||
seat: number;
|
||||
bracketTop: number;
|
||||
/** 저판 하면망 B6 · B8 높이 (z). */
|
||||
zB68: number;
|
||||
}
|
||||
|
||||
export type WingRole = '연직' | '수평' | '헌치' | '모서리' | '간격재' | '경사 하단';
|
||||
|
||||
export interface AbutWingBar {
|
||||
mark: WingMark;
|
||||
size: BarSize;
|
||||
role: WingRole;
|
||||
spacing: string;
|
||||
actual: string;
|
||||
shape: string;
|
||||
length: number;
|
||||
lengthMin: number;
|
||||
lengthMax: number;
|
||||
count: number;
|
||||
position: string;
|
||||
note: string;
|
||||
basis: '규칙' | '가정';
|
||||
modified: boolean;
|
||||
splice?: SplicePlan;
|
||||
}
|
||||
|
||||
export interface WingBarPath {
|
||||
mark: WingMark;
|
||||
/** (s, t, z) mm. */
|
||||
pts: [number, number, number][];
|
||||
}
|
||||
|
||||
export interface AbutWingRebar {
|
||||
side: 'left' | 'right';
|
||||
settings: AbutWingRebarSettings;
|
||||
/** 면별 연직 배치 (면 위 거리, 삽도 치수 기준): 기준선, 등단면 칸 수, 변화단면 칸 수, 끝 여유, 마지막 등단면 위치, 치수 문자열. */
|
||||
vertical: Record<'front' | 'back', { base: number; nE: number; nV: number; endGap: number; lastE: number; text: string }>;
|
||||
/** 수평 단 수: 교대 쪽 (상단 높이 H_s) · 끝 (H_e), 면별. */
|
||||
levels: Record<'front' | 'back', { start: number; end: number }>;
|
||||
haunchLevels: { 'WR7-1': number; 'WR7-2': number; 'WR7-3': number };
|
||||
ld: Record<'H13' | 'H16' | 'H25', number>;
|
||||
paths: WingBarPath[];
|
||||
bars: AbutWingBar[];
|
||||
issues: Issue[];
|
||||
}
|
||||
|
||||
const d = (size: BarSize) => NOMINAL_DIAMETER[size];
|
||||
|
||||
export function resolveWingRebar(changes?: Partial<AbutWingRebarSettings>): AbutWingRebarSettings {
|
||||
return { ...ABUT_WING_REBAR_DEFAULT, ...(changes ?? {}) };
|
||||
}
|
||||
|
||||
export function wingRebarIssues(s: AbutWingRebarSettings): string[] {
|
||||
const out: string[] = [];
|
||||
for (const key of ['c', 'sH', 'sV', 'eMin'] as const) if (!(s[key] > 0)) out.push(`${key}은(는) 0보다 커야 합니다.`);
|
||||
if (!(s.delta >= 0)) out.push('등단면 올림 허용차는 0 이상이어야 합니다.');
|
||||
for (const key of ['sVert', 'sHoriz'] as const) if (s[key] !== undefined && !((s[key] as number) > 0)) out.push('철근 간격은 0보다 커야 합니다.');
|
||||
if (!(Number.isInteger(s.k20) && s.k20 >= 1)) out.push('WR20 배수는 1 이상의 정수여야 합니다.');
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 일반 단 + 맨 위 경사 2단의 단 수 at 저판 상면 위 높이 H (면 간격 s): floor((H − c − 2s − c)/s) + 1 + 2. */
|
||||
export function wingLevelCount(H: number, c: number, s: number): number {
|
||||
return Math.max(0, Math.floor((H - 2 * c - 2 * s) / s + 1e-9) + 1) + 2;
|
||||
}
|
||||
|
||||
/** 한 측 날개벽 형상 from the ② wing solid (m, plan E · N) and the abutment (mm, z from the 기초 하단). */
|
||||
export function abutWingGeom(
|
||||
model: { ip1: { e: number; n: number }; u: { e: number; n: number }; nLeft: { e: number; n: number }; frontSide: number },
|
||||
members: { shape: { parapetBackM: number; wallBackM: number; wallFrontM: number; bracketHeightM: number }; footingTopEl: number; footingBottomEl: number | undefined },
|
||||
wing: {
|
||||
side: 'left' | 'right';
|
||||
form: 'general' | 'short';
|
||||
corners: { e: number; n: number }[];
|
||||
lengthM: number;
|
||||
endHeightM: number;
|
||||
haunchM: number;
|
||||
heelDistanceM: number;
|
||||
topStartEl: number;
|
||||
topEndEl: number;
|
||||
bottomEndEl: number;
|
||||
bands: { name: string; zBottomEl: number }[];
|
||||
},
|
||||
zB68: number,
|
||||
): AbutWingGeom {
|
||||
const front = { e: model.nLeft.e * model.frontSide, n: model.nLeft.n * model.frontSide };
|
||||
const st = (p: { e: number; n: number }): V2 => {
|
||||
const d0 = { e: p.e - model.ip1.e, n: p.n - model.ip1.n };
|
||||
return [(d0.e * model.u.e + d0.n * model.u.n) * 1000, (d0.e * front.e + d0.n * front.n) * 1000];
|
||||
};
|
||||
const [c0, c1, , c3] = wing.corners;
|
||||
const p0 = st(c0);
|
||||
const p1 = st(c1);
|
||||
const len = Math.hypot(p1[0] - p0[0], p1[1] - p0[1]);
|
||||
const w: V2 = [(p1[0] - p0[0]) / len, (p1[1] - p0[1]) / len];
|
||||
const q3 = st(c3);
|
||||
const k = Math.abs(w[1]);
|
||||
const bottom = members.footingBottomEl ?? 0;
|
||||
const z = (el: number) => (el - bottom) * 1000;
|
||||
const L = wing.lengthM * 1000;
|
||||
const seatBand = wing.bands.find((b) => b.name === '변화단면2');
|
||||
return {
|
||||
side: wing.side,
|
||||
form: wing.form,
|
||||
p0,
|
||||
w,
|
||||
inward: [q3[0] - p0[0], q3[1] - p0[1]],
|
||||
L,
|
||||
endH: wing.endHeightM * 1000,
|
||||
haunch: wing.haunchM * 1000,
|
||||
heelD: wing.heelDistanceM * 1000,
|
||||
wallBackD: (Math.abs(members.shape.wallBackM) * 1000) / k,
|
||||
parapetBackD: (Math.abs(members.shape.parapetBackM) * 1000) / k,
|
||||
parapetBackT: members.shape.parapetBackM * 1000,
|
||||
wallBackT: members.shape.wallBackM * 1000,
|
||||
wallFrontT: members.shape.wallFrontM * 1000,
|
||||
footTop: z(members.footingTopEl),
|
||||
topStart: z(wing.topStartEl),
|
||||
slope: (wing.topEndEl - wing.topStartEl) / wing.lengthM,
|
||||
bottomEnd: z(wing.bottomEndEl),
|
||||
seat: seatBand ? z(wing.bands.find((b) => b.name === '변화단면1')!.zBottomEl) : z(members.footingTopEl),
|
||||
bracketTop: z(wing.topStartEl - members.shape.bracketHeightM),
|
||||
zB68,
|
||||
};
|
||||
}
|
||||
|
||||
export function abutWingRebar(g: AbutWingGeom, changes?: Partial<AbutWingRebarSettings>, material: Material = DEFAULT_MATERIAL, who = ''): AbutWingRebar {
|
||||
const s = resolveWingRebar(changes);
|
||||
const issues: Issue[] = [];
|
||||
const side = g.side === 'left' ? '좌' : '우';
|
||||
const add = (code: string, level: Issue['level'], message: string) => issues.push({ code, level, pier: who || undefined, message: `${side} 날개벽 · ${message}` });
|
||||
const SZ = ABUT_WING_SIZES;
|
||||
const c = s.c;
|
||||
const sBack = s.sVert ?? s.sH;
|
||||
const sBackV = s.sHoriz ?? s.sV;
|
||||
if (s.sVert !== undefined || s.sHoriz !== undefined || s.k20 !== 8 || s.delta !== 25) add('RB-AW03', 'I', '간격을 기본간격과 따로 고쳤거나 δ · WR20 배수가 기본값이 아닙니다.');
|
||||
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'), H25: ldOf('H25') };
|
||||
const hook = (size: BarSize) => Math.round(hookLength('main90', d(size)));
|
||||
|
||||
// 평면 틀: 전면 위 점 = p0 + d·w, 배면 위 점 = p0 + inward + d·w. n = 전면 → 배면 단위 (w에 수직), T = 수직 두께.
|
||||
const [ws, wt] = g.w;
|
||||
const iw = g.inward[0] * ws + g.inward[1] * wt;
|
||||
const nv: V2 = [g.inward[0] - iw * ws, g.inward[1] - iw * wt];
|
||||
const T = Math.hypot(nv[0], nv[1]);
|
||||
const n: V2 = [nv[0] / T, nv[1] / T];
|
||||
/** Point at axis-based distance d, `depth` from the front face toward the back. */
|
||||
const at = (dd: number, depth: number): V2 => [g.p0[0] + dd * ws + depth * n[0], g.p0[1] + dd * wt + depth * n[1]];
|
||||
// 배면은 면 위 거리를 수선의 발에서 잰다 (기획서 §4.1: 면 길이 차 = 두께 · cot φ). o = 배면 거리 − 축선 기준 거리.
|
||||
const o = iw;
|
||||
const top = (dd: number) => g.topStart + g.slope * dd;
|
||||
const general = g.form === 'general' && g.heelD < g.L;
|
||||
const bottom = (dd: number) => (!general || dd <= g.heelD ? g.footTop : g.footTop + ((g.bottomEnd - g.footTop) * (dd - g.heelD)) / (g.L - g.heelD));
|
||||
|
||||
const paths: WingBarPath[] = [];
|
||||
const lengths = new Map<WingMark, number[]>();
|
||||
const put = (mark: WingMark, pts: [number, number, number][], length: number) => {
|
||||
paths.push({ mark, pts });
|
||||
lengths.set(mark, [...(lengths.get(mark) ?? []), length]);
|
||||
};
|
||||
const P3 = (p: V2, z: number): [number, number, number] => [p[0], p[1], z];
|
||||
|
||||
// 연직 (§4.1 · §4.2). 면 위 거리 df: 전면은 축선 기준, 배면은 수선의 발 기준 (df = d + o).
|
||||
const zHook16 = g.zB68 + (d('H16') + d(SZ.WR2)) / 2;
|
||||
const h16 = hook(SZ.WR2);
|
||||
const vertical = {} as AbutWingRebar['vertical'];
|
||||
for (const face of ['front', 'back'] as const) {
|
||||
const sp = face === 'front' ? 2 * sBack : sBack;
|
||||
const off = face === 'front' ? 0 : o;
|
||||
const depth = face === 'front' ? c : T - c;
|
||||
const out = face === 'front' ? -1 : 1; // 갈고리는 날개벽 바깥쪽
|
||||
const heelF = Math.min(g.heelD, g.L) + off;
|
||||
const endF = g.L + off;
|
||||
const base = heelF - c;
|
||||
const nE = Math.max(0, Math.ceil((heelF - g.wallBackD - s.delta) / sp - 1e-9));
|
||||
const nV = general ? Math.max(0, Math.floor((endF - c - base - s.eMin) / sp + 1e-9)) : 0;
|
||||
const endGap = endF - c - base - nV * sp;
|
||||
const markE: WingMark = face === 'front' ? 'WR12' : 'WR2';
|
||||
const markV: WingMark = face === 'front' ? 'WR11' : 'WR1';
|
||||
for (let i = 0; i <= nE; i++) {
|
||||
const dd = base - i * sp - off;
|
||||
const p = at(dd, depth);
|
||||
const zt = top(dd) - c;
|
||||
const ph: V2 = [p[0] + out * n[0] * h16, p[1] + out * n[1] * h16];
|
||||
put(markE, [P3(ph, zHook16), P3(p, zHook16), P3(p, zt)], zt - zHook16 + h16);
|
||||
}
|
||||
for (let j = 1; j <= nV; j++) {
|
||||
const dd = base + j * sp - off;
|
||||
const p = at(dd, depth);
|
||||
const zb = bottom(dd) + c;
|
||||
const zt = top(dd) - c;
|
||||
if (zt - zb < 2 * c) continue;
|
||||
put(markV, [P3(p, zb), P3(p, zt)], zt - zb);
|
||||
}
|
||||
const lastE = base - nE * sp;
|
||||
vertical[face] = {
|
||||
base,
|
||||
nE,
|
||||
nV,
|
||||
endGap,
|
||||
lastE,
|
||||
text: `${lenText(c)} · ${lenText(endGap)} · ${nV}@${lenText(sp)}=${lenText(nV * sp)} · ${nE}@${lenText(sp)}=${lenText(nE * sp)} · ${lenText(lastE)}`,
|
||||
};
|
||||
}
|
||||
if (g.endH < 2 * c + sBack) add('RB-AW04', 'W', `끝 높이 ${lenText(g.endH)}가 2c + s보다 작아 연직철근 놓을 자리가 없습니다.`);
|
||||
|
||||
// 수평 (§5): 일반 단 h_k = c + k·s (저판 상면 위), 맨 위 2단은 상단 경사와 나란히. 띠는 높이로.
|
||||
const bandOf = (z: number) => (z >= g.bottomEnd - 1e-6 || !general ? 0 : z > g.seat + 1e-6 ? 1 : 2);
|
||||
const h25 = hook('H25');
|
||||
// 교대 쪽 끝: 흉벽(+브라켓) 전면 − c (t = −c) 또는 벽체 전면 − c, d로.
|
||||
const dAt = (t: number) => -t / -wt; // t = d·wt along the front face (p0 on the axis, t0 = 0)
|
||||
const dInner = [dAt(-c), dAt(-c), dAt(g.wallFrontT - c)];
|
||||
const abutEdge = (z: number) => (z > g.bracketTop + 1e-6 ? g.parapetBackD : g.wallBackD);
|
||||
let shortAnchor = false;
|
||||
const levels = { front: { start: 0, end: 0 }, back: { start: 0, end: 0 } };
|
||||
for (const face of ['front', 'back'] as const) {
|
||||
const sp = face === 'front' ? 2 * sBackV : sBackV;
|
||||
const depth = face === 'front' ? c + (d('H16') + d('H25')) / 2 : T - c - (d('H16') + d('H25')) / 2;
|
||||
const marks: WingMark[] = face === 'front' ? ['WR13', 'WR14', 'WR15'] : ['WR3', 'WR4', 'WR5'];
|
||||
const dEnd = g.L - c;
|
||||
// 맨 위 2단: 경사와 나란히, 교대 단면을 지나 흉벽 전면 − c.
|
||||
for (let m = 0; m < 2; m++) {
|
||||
const za = top(dInner[0]) - c - m * sp;
|
||||
const zb = top(dEnd) - c - m * sp;
|
||||
const a = at(dInner[0], depth);
|
||||
const b = at(dEnd, depth);
|
||||
put(marks[0], [P3(a, za - h25), P3(a, za), P3(b, zb)], Math.hypot(dEnd - dInner[0], zb - za) + h25);
|
||||
}
|
||||
// 일반 단.
|
||||
const cutTop = (dd: number) => top(dd) - c - 2 * sp; // 일반 단은 이 높이 이하 (맨 위 2단 아래 s 이상)
|
||||
for (let k = 0; ; k++) {
|
||||
const z = g.footTop + c + k * sp;
|
||||
if (z > Math.max(cutTop(0), cutTop(g.L)) + 1e-6) break;
|
||||
// d 범위: 상단 끊김 (경사로 교대 쪽이 낮음) · 경사 하단 (끝 쪽에서 올라옴).
|
||||
let dA = 0;
|
||||
if (z > cutTop(0) + 1e-6) dA = (z - (g.topStart - c - 2 * sp)) / g.slope;
|
||||
let dB = dEnd;
|
||||
if (general && z < bottom(dEnd) + c - 1e-6) dB = g.heelD + ((z - c - g.footTop) / (g.bottomEnd - g.footTop)) * (g.L - g.heelD);
|
||||
else if (general && z > bottom(dEnd) + c - 1e-6) dB = dEnd;
|
||||
if (dB - dA < c) continue;
|
||||
const band = bandOf(z);
|
||||
const mark = marks[band];
|
||||
const startsAtAbut = dA <= abutEdge(z) + 1e-6;
|
||||
const b = at(dB, depth);
|
||||
if (startsAtAbut) {
|
||||
const di = dInner[band];
|
||||
const a = at(di, depth);
|
||||
if (band === 0 && abutEdge(z) - di + h25 < ld.H25) shortAnchor = true;
|
||||
put(mark, [P3(a, z - h25), P3(a, z), P3(b, z)], dB - di + h25);
|
||||
} else {
|
||||
const a = at(dA, depth);
|
||||
put(mark, [P3(a, z), P3(b, z)], dB - dA);
|
||||
}
|
||||
}
|
||||
levels[face] = { start: wingLevelCount(g.topStart - g.footTop, c, sp), end: wingLevelCount(top(g.L) - g.footTop, c, sp) };
|
||||
}
|
||||
if (shortAnchor) add('RB-AW02', 'W', `등단면 수평철근(WR3 · WR13)의 교대 쪽 정착이 l_d ${ld.H25}보다 짧습니다 (흉벽 두께).`);
|
||||
|
||||
// WR6 (§6): 경사 하단과 나란히 c 띄워 전면 · 배면 각 1, 끝면 − c → 뒷굽 끝 → 저판 안 l_d.
|
||||
if (general) {
|
||||
const slopeLen = Math.hypot(g.L - g.heelD, g.bottomEnd - g.footTop);
|
||||
const ux = (g.L - g.heelD) / slopeLen;
|
||||
const uz = (g.bottomEnd - g.footTop) / slopeLen;
|
||||
for (const depth of [c, T - c]) {
|
||||
const dTop = g.L - c;
|
||||
const zTop = bottom(dTop) + c;
|
||||
const zH = g.footTop + c;
|
||||
const dH = g.heelD; // 뒷굽 끝 위 c
|
||||
const dIn = dH - ld.H16 * ux;
|
||||
const zIn = zH - ld.H16 * uz;
|
||||
put('WR6', [P3(at(dTop, depth), zTop), P3(at(dH, depth), zH), P3(at(dIn, depth), zIn)], Math.hypot(dTop - dH, zTop - zH) + ld.H16);
|
||||
}
|
||||
}
|
||||
|
||||
// 헌치 · 모서리 (§6 · §7): 평면에서 배면(흙 쪽) 면과 교대 배면이 만나는 모서리.
|
||||
const eIn: V2 = [-n[0], -n[1]]; // 배면에서 날개벽 안쪽 (전면 쪽)
|
||||
const tUnit: V2 = [0, 1]; // 교대 안쪽 (전면 쪽)
|
||||
const uOut: V2 = [Math.sign(g.inward[0]), 0]; // 날개벽 배면에서 교대 배면을 따라 반대쪽 날개벽 쪽
|
||||
const lineX = (p: V2, u: V2, q: V2, v: V2): V2 => {
|
||||
const den = u[0] * v[1] - u[1] * v[0];
|
||||
const f = ((q[0] - p[0]) * v[1] - (q[1] - p[1]) * v[0]) / den;
|
||||
return [p[0] + u[0] * f, p[1] + u[1] * f];
|
||||
};
|
||||
const add2 = (p: V2, u: V2, f: number): V2 => [p[0] + u[0] * f, p[1] + u[1] * f];
|
||||
const haunchLevels = { 'WR7-1': 0, 'WR7-2': 0, 'WR7-3': 0 };
|
||||
const wDir: V2 = [ws, wt];
|
||||
if (g.haunch > 0) {
|
||||
const sp = 2 * sBackV;
|
||||
const legIn = T - c - (d('H16') + d('H25')) / 2 - (d('H25') + d('H25')) / 2; // WR3–5 안쪽 25.4
|
||||
for (let k = 0; ; k++) {
|
||||
const z = g.footTop + c + k * sp;
|
||||
if (z > g.topStart - c + 1e-6) break;
|
||||
const mark: WingMark = z > g.bracketTop + 1e-6 ? 'WR7-1' : z > g.seat + 1e-6 ? 'WR7-2' : 'WR7-3';
|
||||
haunchLevels[mark]++;
|
||||
const backT = mark === 'WR7-1' ? g.parapetBackT : g.wallBackT;
|
||||
const tEnd = mark === 'WR7-3' ? g.wallFrontT - c : -c;
|
||||
// 모서리 = 배면 선 ∩ 교대 배면 선; 헌치 대각은 날개벽 쪽 점 ~ 교대 쪽 점, 철근은 그 안쪽 c.
|
||||
const backFace = at(0, T);
|
||||
const corner = lineX(backFace, wDir, [0, backT], [1, 0]);
|
||||
const hw = add2(corner, wDir, g.haunch);
|
||||
const ha = add2(corner, uOut, g.haunch);
|
||||
const dv: V2 = [ha[0] - hw[0], ha[1] - hw[1]];
|
||||
const dl = Math.hypot(dv[0], dv[1]);
|
||||
const du: V2 = [dv[0] / dl, dv[1] / dl];
|
||||
const toCorner: V2 = [corner[0] - hw[0], corner[1] - hw[1]];
|
||||
const nrm: V2 = [-du[1], du[0]];
|
||||
const sgn = nrm[0] * toCorner[0] + nrm[1] * toCorner[1] > 0 ? 1 : -1;
|
||||
const diagP = add2(hw, nrm, sgn * c);
|
||||
const legLine = at(0, legIn);
|
||||
const A = lineX(legLine, wDir, diagP, du);
|
||||
const B = lineX(diagP, du, [0, backT + c], [1, 0]);
|
||||
const P0 = add2(A, wDir, ld.H25);
|
||||
const C: V2 = [B[0], tEnd];
|
||||
const len = ld.H25 + Math.hypot(B[0] - A[0], B[1] - A[1]) + Math.abs(tEnd - B[1]) + h25;
|
||||
put(mark, [P3(P0, z), P3(A, z), P3(B, z), P3(C, z), P3(C, z - h25)], len);
|
||||
}
|
||||
// WR9 (§6, 결정 2026-11-04): 아래 헌치 대각 위 연직 @2·s_h, 개수 = floor(대각 / 간격) + 1, 가운데 맞춤. 위 끝 브라켓 상면 − c (그 위는 헌치 없음).
|
||||
const corner = lineX(at(0, T), wDir, [0, g.wallBackT], [1, 0]);
|
||||
const hw = add2(corner, wDir, g.haunch);
|
||||
const ha = add2(corner, uOut, g.haunch);
|
||||
const mid: V2 = [(hw[0] + ha[0]) / 2, (hw[1] + ha[1]) / 2];
|
||||
const toC: V2 = [corner[0] - mid[0], corner[1] - mid[1]];
|
||||
const tc = Math.hypot(toC[0], toC[1]);
|
||||
const diagLen = Math.hypot(ha[0] - hw[0], ha[1] - hw[1]);
|
||||
const sp9 = 2 * sBack;
|
||||
const n9 = Math.floor(diagLen / sp9 + 1e-9) + 1;
|
||||
const du: V2 = [(ha[0] - hw[0]) / diagLen, (ha[1] - hw[1]) / diagLen];
|
||||
for (let i = 0; i < n9; i++) {
|
||||
const p = add2(add2(mid, du, (i - (n9 - 1) / 2) * sp9), [toC[0] / tc, toC[1] / tc], c);
|
||||
const ph = add2(p, tUnit, h16);
|
||||
put('WR9', [P3(ph, zHook16), P3(p, zHook16), P3(p, g.bracketTop - c)], g.bracketTop - c - zHook16 + h16);
|
||||
}
|
||||
}
|
||||
// WR10 (§7): 모서리 L 꺾임 @s_v, 저판 상면 + c ~ 흉벽 상단 − c. 교대 배면 · 날개벽 배면에서 각 120.65 안, 모서리 대각 300, 양끝 l_d.
|
||||
{
|
||||
const depth10 = c + (d('H16') + d('H25')) / 2;
|
||||
const r = 300;
|
||||
for (let z = g.footTop + c; z <= g.topStart - c + 1e-6; z += sBackV) {
|
||||
const backT = z > g.bracketTop + 1e-6 ? g.parapetBackT : g.wallBackT;
|
||||
const wingLine = at(0, T - depth10);
|
||||
const X = lineX(wingLine, wDir, [0, backT + depth10], [1, 0]);
|
||||
const X1 = add2(X, uOut, r);
|
||||
const X2 = add2(X, wDir, r);
|
||||
put('WR10', [P3(add2(X1, uOut, ld.H25), z), P3(X1, z), P3(X2, z), P3(add2(X2, wDir, ld.H25), z)], 2 * ld.H25 + Math.hypot(X2[0] - X1[0], X2[1] - X1[1]));
|
||||
}
|
||||
}
|
||||
// WR20 (§8): 간격재, 수평 @k·s_h × 연직 @k·s_v, 단마다 반 칸 엇갈림, 전면 · 배면 수평철근 사이 + 양끝 갈고리.
|
||||
{
|
||||
const h13 = hook(SZ.WR20);
|
||||
const dx = s.k20 * sBack;
|
||||
const dz = s.k20 * sBackV;
|
||||
const dF = c + (d('H16') + d('H25')) / 2;
|
||||
const dB = T - dF;
|
||||
let j = 0;
|
||||
for (let z = g.footTop + c; z <= g.topStart + g.slope * g.L - c + 1e-6; z += dz, j++) {
|
||||
for (let dd = g.wallBackD + c + (j % 2 ? dx / 2 : 0); dd <= g.L - c + 1e-6; dd += dx) {
|
||||
if (z > top(dd) - c || z < bottom(dd) + c) continue;
|
||||
const a = at(dd, dF);
|
||||
const b = at(dd, dB);
|
||||
put('WR20', [P3(a, z - h13), P3(a, z), P3(b, z), P3(b, z - h13)], dB - dF + 2 * h13);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 표 (시안 순서).
|
||||
const bars: AbutWingBar[] = [];
|
||||
const push = (b: Omit<AbutWingBar, 'length' | 'lengthMin' | '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;
|
||||
if (splice) add('RB-AW01', splice.over ? 'W' : 'I', `${b.mark} ${Math.round(max)} > 정척 ${STANDARD_BAR}: ${splice.pieces}조각 이음 l_s ${splice.lap}.`);
|
||||
bars.push({ ...b, note: splice ? `${b.note} · ${splice.pieces}조각 이음 l_s ${splice.lap}` : b.note, length: Math.round(max), lengthMin: Math.round(Math.min(...all)), lengthMax: Math.round(max), count: all.length, splice });
|
||||
};
|
||||
const vMod = s.sVert !== undefined;
|
||||
const hMod = s.sHoriz !== undefined;
|
||||
const v2 = lenText(2 * sBack);
|
||||
const v1 = lenText(sBack);
|
||||
const hz2 = lenText(2 * sBackV);
|
||||
const hz1 = lenText(sBackV);
|
||||
push({ mark: 'WR11', size: SZ.WR11, role: '연직', spacing: v2, actual: v2, shape: '직선 · 아래 경사 하단 + c', position: '전면 · 변화단면', note: `기준선 (뒷굽 끝 − ${lenText(c)}) → 끝 쪽 ${vertical.front.nV}@${v2} · 끝 여유 ${lenText(vertical.front.endGap)}`, basis: '규칙', modified: vMod });
|
||||
push({ mark: 'WR12', size: SZ.WR12, role: '연직', spacing: v2, actual: v2, shape: `아래 90° 갈고리 ${h16} (저판 B6 · B8 위)`, position: '전면 · 등단면', note: `기준선 + 교대 쪽 ${vertical.front.nE}@${v2} · 마지막 ${lenText(vertical.front.lastE)}`, basis: '규칙', modified: vMod });
|
||||
push({ mark: 'WR1', size: SZ.WR1, role: '연직', spacing: v1, actual: v1, shape: '직선 · 아래 경사 하단 + c', position: '배면 · 변화단면', note: `${vertical.back.nV}@${v1} · 끝 여유 ${lenText(vertical.back.endGap)}`, basis: '규칙', modified: vMod });
|
||||
push({ mark: 'WR2', size: SZ.WR2, role: '연직', spacing: v1, actual: v1, shape: `아래 90° 갈고리 ${h16}`, position: '배면 · 등단면', note: `기준선 + ${vertical.back.nE}@${v1} · 마지막 ${lenText(vertical.back.lastE)}`, basis: '규칙', modified: vMod });
|
||||
push({ mark: 'WR13', size: SZ.WR13, role: '수평', spacing: hz2, actual: hz2, shape: `교대 쪽 흉벽 지나 90° 갈고리 ${h25} · 맨 위 2단 경사와 나란히`, position: '전면 수평 · 등단면 띠', note: '흉벽 전면 − c까지 (Q-WR1: 맨 위 2단 경사)', basis: '규칙', modified: hMod });
|
||||
push({ mark: 'WR14', size: SZ.WR14, role: '수평', spacing: hz2, actual: hz2, shape: `흉벽 + 브라켓 지나 90° 갈고리 ${h25}`, position: '전면 수평 · 변화단면1 (~ 교좌면)', note: '흉벽 전면 − c까지', basis: '규칙', modified: hMod });
|
||||
push({ mark: 'WR15', size: SZ.WR15, role: '수평', spacing: hz2, actual: hz2, shape: `벽체 지나 90° 갈고리 ${h25}`, position: '전면 수평 · 변화단면2 (교좌면 아래)', note: '벽체 전면 − c까지', basis: '규칙', modified: hMod });
|
||||
push({ mark: 'WR3', size: SZ.WR3, role: '수평', spacing: hz1, actual: hz1, shape: `흉벽 지나 90° 갈고리 ${h25} · 맨 위 2단 경사와 나란히`, position: '배면 수평 · 등단면 띠', note: `교대 쪽 ${levels.back.start}단 · 끝 ${levels.back.end}단`, basis: '규칙', modified: hMod });
|
||||
push({ mark: 'WR4', size: SZ.WR4, role: '수평', spacing: hz1, actual: hz1, shape: `흉벽 + 브라켓 지나 90° 갈고리 ${h25}`, position: '배면 수평 · 변화단면1', note: '', basis: '규칙', modified: hMod });
|
||||
push({ mark: 'WR5', size: SZ.WR5, role: '수평', spacing: hz1, actual: hz1, shape: `벽체 지나 90° 갈고리 ${h25}`, position: '배면 수평 · 변화단면2', note: '', basis: '규칙', modified: hMod });
|
||||
push({ mark: 'WR6', size: SZ.WR6, role: '경사 하단', spacing: '—', actual: '—', shape: `경사 하단과 나란히 · 아래 저판 안 l_d ${ld.H16}`, position: '경사 하단 · 전면 · 배면 각 1', note: '"cycle-2" 삭제 (결정)', basis: '규칙', modified: false });
|
||||
push({ mark: 'WR7-1', size: SZ['WR7-1'], role: '헌치', spacing: hz2, actual: hz2, shape: `꺾임 · 날개벽 l_d ${ld.H25} · 헌치 대각 · 흉벽 지나 갈고리`, position: '위 헌치 · 브라켓 상면 ~ 상단', note: '날개벽 다리 WR3–5 안쪽 25.4 (Q-WR7)', basis: '규칙', modified: hMod });
|
||||
push({ mark: 'WR7-2', size: SZ['WR7-2'], role: '헌치', spacing: hz2, actual: hz2, shape: '꺾임 · 흉벽 + 브라켓 지나 갈고리', position: '아래 헌치 윗부분 · 교좌면 ~ 브라켓 상면', note: '띠 경계 브라켓 상면 / 교좌면 (Q-WR6)', basis: '규칙', modified: hMod });
|
||||
push({ mark: 'WR7-3', size: SZ['WR7-3'], role: '헌치', spacing: hz2, actual: hz2, shape: '꺾임 · 벽체 지나 갈고리', position: '아래 헌치 아랫부분 · 저판 상면 ~ 교좌면', note: 'WR8 결번', basis: '규칙', modified: hMod });
|
||||
push({ mark: 'WR9', size: SZ.WR9, role: '헌치', spacing: lenText(2 * sBack), actual: '—', shape: `아래 90° 갈고리 ${h16} (저판 안)`, position: '아래 헌치 대각 위 연직 · 위 끝 브라켓 상면 − c', note: `@${lenText(2 * sBack)} 자동 개수 (사용자 결정 2026-11-04, Q-WR3)`, basis: '규칙', modified: vMod });
|
||||
push({ mark: 'WR10', size: SZ.WR10, role: '모서리', spacing: hz1, actual: hz1, shape: `꺾임 · 모서리 대각 300 · 양끝 l_d ${ld.H25}`, position: '벽체 · 흉벽 배면 ↔ 날개벽 배면 모서리 · 저판 상면 ~ 흉벽 상단', note: '헌치 철근과의 간섭은 보지 않음 (Q-WR4)', basis: '규칙', modified: hMod });
|
||||
push({ mark: 'WR20', size: SZ.WR20, role: '간격재', spacing: `${lenText(s.k20 * sBack)} × ${lenText(s.k20 * sBackV)}`, actual: '—', shape: `양끝 90° 갈고리 ${hook(SZ.WR20)}`, position: `내측 간격재 · 두께 ${lenText(T - 2 * (c + (d('H16') + d('H25')) / 2))} 걸침`, note: '철근표 H20은 오기 → H13', basis: '규칙', modified: s.k20 !== 8 });
|
||||
|
||||
return { side: g.side, settings: s, vertical, levels, haunchLevels, ld, paths, bars, issues };
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
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 { buildWings } from '../src/core/abut/wings';
|
||||
import { abutFootingGeom, abutFootingRebar } from '../src/core/abut/rebar/footing';
|
||||
import { skewAngle } from '../src/core/abut/rebar/skew-layout';
|
||||
import { abutWingGeom, abutWingRebar, wingLevelCount } from '../src/core/abut/rebar/wing';
|
||||
import { DEFAULT_MATERIAL } from '../src/core/rebar/common';
|
||||
|
||||
// 예제 A1 좌측 날개벽 (교대날개벽철근.md §11). 기획서 값은 fck 27 · 벽체 높이 2500 기준 (흉벽 상단 46.835 / 47.180).
|
||||
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 }, { parapetHeightMm: 2500 });
|
||||
const wings = buildWings(model, members);
|
||||
const footing = abutFootingRebar(abutFootingGeom(model.length, members.shape, skewAngle(rec.skewDeg)));
|
||||
const fck27 = { ...DEFAULT_MATERIAL, fck: 27 };
|
||||
const geomOf = (side: 'left' | 'right') => abutWingGeom(model, members, wings.find((w) => w.side === side)!, footing.z.B68);
|
||||
const left = geomOf('left');
|
||||
const L1 = abutWingRebar(left, undefined, fck27, 'A1');
|
||||
const bar = (r: typeof L1, mark: string) => r.bars.find((b) => b.mark === mark);
|
||||
|
||||
describe('교대 날개벽 배근 A1 좌 (AD7, 교대날개벽철근.md §11)', () => {
|
||||
it('geometry: 길이 6500, 뒷굽 끝 1807, 상단 시작 46.835 (저판 상면 위 4840), 일반형', () => {
|
||||
expect(left.form).toBe('general');
|
||||
expect(left.L).toBeCloseTo(6500, 3);
|
||||
expect(left.heelD).toBeCloseTo(1807, 0);
|
||||
expect(left.topStart - left.footTop).toBeCloseTo(4840, 0);
|
||||
});
|
||||
|
||||
it('삽도 재현 (삽도 = 우측 날개벽): 전면 100 · 193 · 18@250 · 4@250 · 707, 배면 100 · 193 · 36@125 · 9@125 · 622; 연직 WR12 5 + WR11 18, WR2 10 + WR1 36', () => {
|
||||
const R = abutWingRebar(geomOf('right'), undefined, fck27);
|
||||
expect(R.vertical.front.text).toBe('100 · 193 · 18@250=4.500 · 4@250=1.000 · 707');
|
||||
expect(R.vertical.back.text).toBe('100 · 193 · 36@125=4.500 · 9@125=1.125 · 621.7'); // 삽도 622 (두께 · cot φ 39.7로 반올림)
|
||||
expect(['WR12', 'WR11', 'WR2', 'WR1'].map((m) => bar(R, m)?.count)).toEqual([5, 18, 10, 36]);
|
||||
});
|
||||
|
||||
it('좌측은 skew로 배면이 두께 · cot φ = 39.6 짧음: 배면 기준선 1667.4 → 8@125 · 667.4, WR2 9 (전면은 같음)', () => {
|
||||
expect(L1.vertical.front.text).toBe('100 · 193 · 18@250=4.500 · 4@250=1.000 · 707');
|
||||
expect(L1.vertical.back.text).toBe('100 · 193 · 36@125=4.500 · 8@125=1.000 · 667.4');
|
||||
expect(['WR12', 'WR11', 'WR2', 'WR1'].map((m) => bar(L1, m)?.count)).toEqual([5, 18, 9, 36]);
|
||||
});
|
||||
|
||||
it('수평 단 (맨 위 2단 경사, 사용자 결정 2026-11-04): 전면 19 (교대 쪽) / 20 (끝), 배면 38 / 39', () => {
|
||||
expect([L1.levels.front.start, L1.levels.front.end, L1.levels.back.start, L1.levels.back.end]).toEqual([19, 20, 38, 39]);
|
||||
expect(wingLevelCount(4840, 100, 250)).toBe(19);
|
||||
});
|
||||
|
||||
it('WR6 2개, WR7-1 · 7-2 · 7-3 3 / 6 / 10단, WR10 38단', () => {
|
||||
expect(bar(L1, 'WR6')?.count).toBe(2);
|
||||
expect(L1.haunchLevels).toEqual({ 'WR7-1': 3, 'WR7-2': 6, 'WR7-3': 10 });
|
||||
expect(bar(L1, 'WR10')?.count).toBe(38);
|
||||
});
|
||||
|
||||
it('WR9: 아래 헌치 대각 (300 · √2 = 424) 위 @250 → 2개 (자동, Q-WR3)', () => {
|
||||
expect(bar(L1, 'WR9')?.count).toBe(Math.floor((300 * Math.SQRT2) / 250) + 1);
|
||||
});
|
||||
|
||||
it('우측: 상단은 우측 흉벽 상단 47.180 기준 (저판 상면 위 5185)', () => {
|
||||
const right = geomOf('right');
|
||||
expect(right.topStart - right.footTop).toBeCloseTo(5185, 0);
|
||||
});
|
||||
|
||||
it('단축형: WR1 · WR11 · WR6 없음 (QA-14)', () => {
|
||||
const shortW = buildWings(model, members, { left: 'short', right: 'general' }).find((w) => w.side === 'left')!;
|
||||
const S = abutWingRebar(abutWingGeom(model, members, shortW, footing.z.B68), undefined, fck27);
|
||||
expect(['WR1', 'WR11', 'WR6'].map((m) => bar(S, m))).toEqual([undefined, undefined, undefined]);
|
||||
expect(bar(S, 'WR2')!.count).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('모든 철근이 날개벽 · 교대 범위 안 (t: 벽체 전면 ~ 날개벽 끝, z: 저판 하면망 위 ~ 상단)', () => {
|
||||
const tMin = left.p0[1] + left.L * left.w[1] - 50;
|
||||
for (const p of L1.paths) {
|
||||
for (const [, t, z] of p.pts) {
|
||||
expect(t, p.mark).toBeGreaterThan(tMin);
|
||||
expect(t, p.mark).toBeLessThan(left.wallFrontT + 1);
|
||||
expect(z, p.mark).toBeLessThan(left.topStart + left.slope * left.L);
|
||||
expect(z, p.mark).toBeGreaterThan(footing.z.B68 - (p.mark === 'WR6' ? 800 : 400));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('간격 수정 → RB-AW03', () => {
|
||||
const r = abutWingRebar(left, { sHoriz: 150 }, fck27);
|
||||
expect(r.issues.map((i) => i.code)).toContain('RB-AW03');
|
||||
expect(bar(r, 'WR3')!.modified).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user