feat(abut): 벽체 높이 H_w = 받침 ① 교좌면 ~ 흉벽 상단 연직 (기본 3326)

- 사용자 도면(2026-11-04): 받침 ① 위치에서 받침 ① 교좌면부터 흉벽 상단까지 연직 거리. 모든 교대 기본값 3326 (QA-4 가정 2500 대체)
- 입력 이름 "벽체 높이 · 받침 ① 기준 연직", ② 정면도에 받침 ① 기준선과 H_w 치수
- ③ 흉벽 철근표 대표 단면 = 받침 ① 위치 (P1–P3는 그 단면에서 정확히 계산)
- 테스트: 흉벽 상단 IP1 48.006 / IP3 47.661 등 + 0.826 반영, 교대흉벽철근.md §11 재현은 2500을 넣어 유지, 3326 기본 확인 (P1 3945)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-04 17:20:05 +09:00
1 parent 51a36669a8
commit 5e64a4f2ed
17 files changed
+125 -71

No files matched your search

+1 -1
View File
@@ -214,7 +214,7 @@ export function applySlabEdit(rec: AbutRecord, key: (typeof SLAB_EDIT_FIELDS)[nu
export const SHAPE_EDIT_FIELDS: { key: keyof AbutShape; label: string; group: 'wall' | 'footing'; note?: string }[] = [
// 벽체 · 흉벽 (시안 입력 격자 순서). 벽체 두께는 자동 = 브라켓 폭 + 흉벽 두께 + 교좌면 폭.
{ key: 'wallFrontMm', label: '교좌면 폭 (mm)', group: 'wall' },
{ key: 'parapetHeightMm', label: '흉벽 높이 (mm)', group: 'wall' },
{ key: 'parapetHeightMm', label: '벽체 높이 · 받침 ① 기준 연직 (mm)', group: 'wall', note: '받침 ① 교좌면 ~ 흉벽 상단' },
{ key: 'parapetThicknessMm', label: '흉벽 두께 (mm)', group: 'wall' },
{ key: 'bracketHeightMm', label: '브라켓 높이 (mm)', group: 'wall', note: '= 접속슬래브 카드 흉벽 높이' },
{ key: 'bracketWidthMm', label: '브라켓 폭 (mm)', group: 'wall' },
+1 -1
View File
@@ -2,7 +2,7 @@
* 교대 부재 · AA3 (2026-11-03, spec_abut §4.3–4.6): 벽체, 흉벽 상단, 저판, 말뚝의 치수와 위치. Pure.
*
* - 교대축 기준 t: 전면 쪽이 +. 벽체 전면 +1.6 m (교좌면 끝), 배면 −0.8 m (브라켓 배면). 두께 2.4 m.
* - 흉벽: t −0.5 ~ 0 m. 상단 EL(s) = 교좌면 EL(구간 ①) + 흉벽 높이 2.5 m + i_b × (s − s_1), i_b = 받침 z의 축 방향 경사.
* - 흉벽: t −0.5 ~ 0 m. 상단 EL(s) = 받침 ① 교좌면 EL + 벽체 높이 H_w (받침 ① 기준 연직, 기본 3.326 m) + i_b × (s − s_1), i_b = 받침 z의 축 방향 경사.
* - 저판: 하면 = 기초 하단 EL, 두께 1.1 m. 폭은 t −1.8 ~ +2.8 m (앞굽 1.2, 뒷굽 1.0, 벽체 2.4). 길이 = 교좌면 길이 L.
* - 말뚝: 열 위치 t = −1.15 / +0.85 / +2.15 m. 열 안 본수 = int((L − 1.3)/2.0) + 1, 양끝 0.65 m 띄움, 균등 간격.
* 말뚝 선단 EL = 기초 하단 EL − 말뚝길이. (skew 평행사변형에서 열 끝이 밀리는 보정은 아직 없다 — QA-5 이후.)
+23 -25
View File
@@ -47,8 +47,10 @@ export interface AbutParapetGeom {
/** 흉벽 상단 z at u = 0 and u = L (저판 하면에서), straight between. 브라켓 상면 = 흉벽 상단 − bracketH. */
top: [number, number];
bracketH: number;
/** 흉벽 높이 입력 (교좌면 → 흉벽 상단). 철근표 대표 단면 = 흉벽 높이가 이 값인 곳 (기획서 §2 "구간 1 끝 2500"과 같은 단면). */
/** 벽체 높이 H_w (받침 ① 교좌면 → 흉벽 상단, 받침 ① 위치에서 연직). */
Hp: number;
/** 받침 ① 위치 u (mm). 철근표 대표 단면 (사용자 도면 2026-11-04). */
ref: number;
/** 접속슬래브: 폭 B (교대축 방향), 다웰 선(브라켓 가운데)에서 슬래브 양 측면의 u, 슬래브 끝 t, 측별 모서리 모따기 (0 = 없음). */
slab: { B: number; a: number; b: number; nearT: number; chamferA: number; chamferB: number } | null;
}
@@ -62,7 +64,7 @@ export interface AbutParapetBar {
spacing: string;
actual: string;
shape: string;
/** 대표 길이 (흉벽 높이 = H_p 단면) and the range. */
/** 대표 길이 (받침 ① 위치 단면) and the range. */
length: number;
lengthMin: number;
lengthMax: number;
@@ -84,7 +86,7 @@ export interface AbutParapetRebar {
x: { P1: number; P2back: number; P3: number; P4: number; P5up: number; P5low: number; P10: number };
/** 연직 격자 오프셋 (저판 격자 기준). */
offsets: { P1: number; P2: number; P3: number };
/** 대표 단면 (흉벽 높이 = H_p)의 단 수, and that section's u. */
/** 대표 단면 (받침 ① 위치)의 단 수, and that section's u. */
levels: { P4: number; P5up: number; P5low: number; P6: number; P7: number };
uRef: number;
dowel: { n: number; e: number; moved: number; positions: number[] };
@@ -110,10 +112,15 @@ export function parapetRebarIssues(s: AbutParapetRebarSettings): string[] {
}
/** 흉벽 형상 from the 벽체 형상 (브라켓 상면 + 브라켓 높이 = 흉벽 상단) and the 접속슬래브 values (mm). */
export function abutParapetGeom(wall: AbutWallGeom, shape: { parapetBackM: number; bracketHeightM: number; parapetHeightM: number }, slab: AbutParapetGeom['slab']): AbutParapetGeom {
export function abutParapetGeom(
wall: AbutWallGeom,
shape: { parapetBackM: number; bracketHeightM: number; parapetHeightM: number },
bearing1SM: number,
slab: AbutParapetGeom['slab'],
): AbutParapetGeom {
const mm = (m: number) => Math.round(m * 1e6) / 1e3;
const bracketH = mm(shape.bracketHeightM);
return { wall, parapetBack: mm(shape.parapetBackM), top: [wall.bracketTop[0] + bracketH, wall.bracketTop[1] + bracketH], bracketH, Hp: mm(shape.parapetHeightM), slab };
return { wall, parapetBack: mm(shape.parapetBackM), top: [wall.bracketTop[0] + bracketH, wall.bracketTop[1] + bracketH], bracketH, Hp: mm(shape.parapetHeightM), ref: mm(bearing1SM), slab };
}
/**
@@ -160,21 +167,8 @@ export function abutParapetRebar(g: AbutParapetGeom, wall: AbutWallRebar, change
const seat = (u: number) => W.HF + segs[segIndex(u)].H;
const top = (u: number) => g.top[0] + ((g.top[1] - g.top[0]) * u) / W.L;
const br = (u: number) => top(u) - g.bracketH;
// 대표 단면: 흉벽 높이(상단 − 교좌면)가 입력값 H_p인 곳 (기획서 §2의 "구간 1 끝 2500" 단면). Linear in each step, so the
// closest point is a clamped root per step.
let uRef = W.L;
let best = Infinity;
for (const sg of segs) {
const hAt = (u: number) => top(u) - (W.HF + sg.H);
const h0 = hAt(sg.from);
const h1 = hAt(sg.to);
const f = h1 === h0 ? 0 : Math.min(1, Math.max(0, (g.Hp - h0) / (h1 - h0)));
const u = sg.from + f * (sg.to - sg.from);
if (Math.abs(hAt(u) - g.Hp) < best - 1e-9) {
best = Math.abs(hAt(u) - g.Hp);
uRef = Math.min(u, sg.to - 1e-6);
}
}
// 대표 단면 = 받침 ① 위치 (벽체 높이 H_w의 기준선, 사용자 도면 2026-11-04). 그 단면의 흉벽 높이가 H_w다.
const uRef = Math.min(Math.max(g.ref, 0), W.L - 1e-6);
// 단면 위치 (§3).
const xP4 = -c;
@@ -198,21 +192,25 @@ export function abutParapetRebar(g: AbutParapetGeom, wall: AbutWallRebar, change
lengths.set(mark, [...(lengths.get(mark) ?? []), { u, l: length }]);
};
// P1 · P2 · P3 (§4.2).
// P1 · P2 · P3 (§4.2). Lengths as functions of u, so the 철근표 대표 can be taken exactly at the 받침 ① 단면.
const lenP1 = (u: number) => top(u) - c - offP2 - (seat(u) - ld.H16);
const lenP2 = (u: number) => ld.H16 + (xP1 - xP2back) + (top(u) - c - (br(u) - ld.H16));
const lenP3 = (u: number) => br(u) - c - (seat(u) - ld.H16) + (xP4 - xP3);
let shortBack = false;
base.forEach((u, i) => {
const z0 = seat(u);
const zt = top(u);
const zb = br(u);
if (i % 2 === 0) put('P1', [[u, xP1, z0 - ld.H16], [u, xP1, zt - c - offP2]], zt - c - offP2 - (z0 - ld.H16), u);
if (i % 2 === 0) put('P1', [[u, xP1, z0 - ld.H16], [u, xP1, zt - c - offP2]], lenP1(u), u);
const u2 = u + offP2;
const zh = zt - c;
const backEnd = zb - ld.H16;
if (backEnd < seat(u2)) shortBack = true;
put('P2', [[u2, xP1, zh - ld.H16], [u2, xP1, zh], [u2, xP2back, zh], [u2, xP2back, backEnd]], ld.H16 + (xP1 - xP2back) + (zh - backEnd), u2);
put('P2', [[u2, xP1, zh - ld.H16], [u2, xP1, zh], [u2, xP2back, zh], [u2, xP2back, backEnd]], lenP2(u2), u2);
const u3 = u + offP3;
put('P3', [[u3, xP3, seat(u3) - ld.H16], [u3, xP3, br(u3) - c], [u3, xP4, br(u3) - c]], br(u3) - c - (seat(u3) - ld.H16) + (xP4 - xP3), u3);
put('P3', [[u3, xP3, seat(u3) - ld.H16], [u3, xP3, br(u3) - c], [u3, xP4, br(u3) - c]], lenP3(u3), u3);
});
const atRef: Partial<Record<ParapetMark, number>> = { P1: lenP1(uRef), P2: lenP2(uRef), P3: lenP3(uRef) };
if (shortBack) add('RB-AP01', 'W', `흉벽 높이 − 브라켓 높이가 l_d ${ld.H16}보다 작아 P2 뒤 다리가 교좌면 아래로 내려갑니다.`);
// 수평 단 (§5): 흉벽 상단에서 c + k·s_v 아래, 상단 경사와 나란함.
@@ -340,7 +338,7 @@ export function abutParapetRebar(g: AbutParapetGeom, wall: AbutWallRebar, change
if (kinds > 10) add('RB-AP04', 'I', `${b.mark} 길이가 흉벽 상단 경사 · 계단으로 ${kinds}종입니다 (철근표에는 범위).`);
const splice = splicePlan(max, d(b.size), fy(b.size), material.fck) ?? undefined;
if (splice) add('RB-AP07', 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(ref.l), lengthMin: Math.round(Math.min(...ls)), lengthMax: Math.round(max), count: all.length, splice });
bars.push({ ...b, note: splice ? `${b.note} · ${splice.pieces}조각 이음 l_s ${splice.lap}` : b.note, length: Math.round(atRef[b.mark] ?? ref.l), lengthMin: Math.round(Math.min(...ls)), lengthMax: Math.round(max), count: all.length, splice });
};
const vMod = s.sVert !== undefined;
const hMod = s.sHoriz !== undefined;
+3 -2
View File
@@ -45,7 +45,8 @@ export interface AbutShape {
export const ABUT_SHAPE_DEFAULT: AbutShape = {
wallFrontMm: 1600,
parapetHeightMm: 2500,
/** 벽체 높이 H_w: 받침 ① 위치에서 받침 ① 교좌면 ~ 흉벽 상단 연직 거리 (사용자 도면, 2026-11-04). 모든 교대 기본값. */
parapetHeightMm: 3326,
parapetThicknessMm: 500,
bracketHeightMm: 850,
bracketWidthMm: 300,
@@ -166,7 +167,7 @@ export function shapeIssues(changes: Partial<AbutShape>): string[] {
const out: string[] = [];
if (!(v.wallFrontMm > 0)) out.push('교좌면 폭은 0보다 커야 합니다.');
if (!(v.bracketWidthMm >= 0)) out.push('브라켓 폭은 0 이상이어야 합니다.');
if (!(v.parapetHeightMm > 0)) out.push('흉벽 높이는 0보다 커야 합니다.');
if (!(v.parapetHeightMm > 0)) out.push('벽체 높이(받침 ① 기준)는 0보다 커야 합니다.');
if (!(v.parapetThicknessMm > 0)) out.push('흉벽 두께는 0보다 커야 합니다.');
if (!(v.bracketHeightMm >= 0)) out.push('브라켓 높이는 0 이상이어야 합니다.');
if (!(v.footingThicknessMm > 0)) out.push('저판 두께는 0보다 커야 합니다.');
+1 -1
View File
@@ -30,7 +30,7 @@ export function abutSummaryRows(rec: AbutRecord): [string, string][] {
['받침블럭 높이 (패드 포함)', mm(first.blockHeightMm)],
['벽체 높이 (구간 ①)', m3(members.wallSegments.find((w) => Math.abs(w.seatEl - first.seatEl) < 1e-9)?.heightM ?? NaN)],
['벽체 두께 · 전면', `${Math.round(shape.wallFrontM * 1000 - shape.wallBackM * 1000)} mm · ${Math.round(shape.wallFrontM * 1000)} mm`],
['흉벽 높이 · 두께', `${Math.round(shape.parapetHeightM * 1000)} mm · ${Math.round(-shape.parapetBackM * 1000)} mm`],
['벽체 높이 (받침 ① 기준) · 흉벽 두께', `${Math.round(shape.parapetHeightM * 1000)} mm · ${Math.round(-shape.parapetBackM * 1000)} mm`],
['흉벽 상단 EL (IP1 끝 / IP3 끝)', `${members.parapetTopStartEl.toFixed(3)} / ${members.parapetTopEndEl.toFixed(3)} m`],
['브라켓 상면 EL (구간 ①)', m3(first.seatEl + shape.parapetHeightM - shape.bracketHeightM)],
['흉벽 경사', `${(members.parapetSlope * 100).toFixed(3)} %`],