fix(abut-rebar): 날개벽 WR7-1 · 7-2 · 7-3 평면 형상 (긴 대각 + 양끝 갈고리 394)

- 사용자 도면(2026-11-05): 헌치 빗변과 나란한 대각이 안쪽 모서리를 c 떨어져 지나, 교대 쪽은 전면 − c (WR7-1 · 7-2 흉벽 전면,
  WR7-3 벽체 전면), 날개벽 쪽은 날개벽 바깥면 − c에서 끝남. 양끝 평면 90° 갈고리 394 (교대 안쪽 / 날개벽 끝 쪽)
- WR7-3의 긴 대각이 더 낮은 이웃 계단의 교좌면 위로 나오는 단은 WR7-2로 (교좌면 계단을 날개벽 형상에 전달)
- 테스트: 갈고리 394 · 끝점 · 대각 길이, WR7-1 · 7-2 끝, 우측 재분류, 콘크리트 안 검사 통과

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-05 09:01:33 +09:00
1 parent 89ebf852a9
commit 7b9de649e5
5 files changed
+86 -31

No files matched your search

+45 -27
View File
@@ -74,6 +74,9 @@ export interface AbutWingGeom {
bracketTop: number;
/** 저판 하면망 B6 · B8 높이 (z). */
zB68: number;
/** 교좌면 계단 (u = s − k·t 범위 mm, 교좌면 z): WR7-3이 긴 대각으로 이웃 계단까지 갈 때 그 교좌면 아래인지 본다. 없으면 seat 하나. */
seats?: { from: number; to: number; z: number }[];
skewK?: number;
}
export type WingRole = '연직' | '수평' | '헌치' | '모서리' | '간격재' | '경사 하단';
@@ -154,6 +157,8 @@ export function abutWingGeom(
bands: { name: string; zBottomEl: number }[];
},
zB68: number,
seats?: { from: number; to: number; z: number }[],
skewK?: number,
): AbutWingGeom {
const front = { e: model.nLeft.e * model.frontSide, n: model.nLeft.n * model.frontSide };
const st = (p: { e: number; n: number }): V2 => {
@@ -193,6 +198,8 @@ export function abutWingGeom(
seat: seatBand ? z(wing.bands.find((b) => b.name === '변화단면1')!.zBottomEl) : z(members.footingTopEl),
bracketTop: z(wing.topStartEl - members.shape.bracketHeightM),
zB68,
seats,
skewK,
};
}
@@ -368,34 +375,44 @@ export function abutWingRebar(g: AbutWingGeom, changes?: Partial<AbutWingRebarSe
};
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';
let mark: WingMark = z > g.bracketTop + 1e-6 ? 'WR7-1' : z > g.seat + 1e-6 ? 'WR7-2' : 'WR7-3';
// 평면 (사용자 도면 2026-11-05): 헌치 빗변과 나란한 긴 대각이 안쪽 모서리(날개벽 배면 × 교대 배면)를 콘크리트 쪽으로 c 떨어져
// 지나, 교대 쪽은 전면 − c 선(WR7-1 · 7-2 흉벽 전면, WR7-3 벽체 전면), 날개벽 쪽은 날개벽 바깥면 − c 선에서 끝난다.
// 양끝은 평면 안 90° 갈고리 394: 교대 쪽은 전면 선을 따라 교대 안쪽으로, 날개벽 쪽은 바깥면 선을 따라 날개벽 끝 쪽으로.
const plan = (m: WingMark) => {
const backT = m === 'WR7-1' ? g.parapetBackT : g.wallBackT;
const tEnd = m === 'WR7-3' ? g.wallFrontT - c : -c;
const corner = lineX(at(0, T), wDir, [0, backT], [1, 0]);
const hw = add2(corner, wDir, g.haunch);
const ha = add2(corner, uOut, g.haunch);
const dl = Math.hypot(ha[0] - hw[0], ha[1] - hw[1]);
const du: V2 = [(ha[0] - hw[0]) / dl, (ha[1] - hw[1]) / dl];
const away: V2 = [corner[0] - (hw[0] + ha[0]) / 2, corner[1] - (hw[1] + ha[1]) / 2];
const al = Math.hypot(away[0], away[1]);
const L0 = add2(corner, [away[0] / al, away[1] / al], c);
const F = lineX(L0, du, [0, tEnd], [1, 0]);
const G = lineX(L0, du, at(0, c), wDir);
const legG = Math.min(h25, legEnd(G, z));
const len = h25 + Math.hypot(G[0] - F[0], G[1] - F[1]) + legG;
return { pts: [add2(F, uOut, h25), F, G, add2(G, wDir, legG)] as V2[], len };
};
let bar = plan(mark);
// WR7-3의 긴 대각이 이웃 계단(교좌면이 더 낮음)까지 가면 그 교좌면 위로 나온다 → 그 단은 WR7-2 (흉벽 전면 − c, 브라켓 안).
if (mark === 'WR7-3' && g.seats) {
const seatAt = (q: V2) => {
const u = q[0] - (g.skewK ?? 0) * q[1];
return (g.seats!.find((x) => u < x.to) ?? g.seats![g.seats!.length - 1]).z;
};
if (bar.pts.some((q) => z > seatAt(q) + 1e-6)) {
mark = 'WR7-2';
bar = plan(mark);
}
}
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 leg = legEnd(A, z);
const P0 = add2(A, wDir, leg);
const C: V2 = [B[0], tEnd];
const len = leg + 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);
put(mark, bar.pts.map((q) => P3(q, z)), bar.len);
}
}
// WR9 (사용자 도면 2026-11-04, 연직헌치 보강철근): 저판 상면 헌치(날개벽 배면 ~ 저판 상면, 다리 g.haunch) 보강.
@@ -485,9 +502,10 @@ export function abutWingRebar(g: AbutWingGeom, changes?: Partial<AbutWingRebarSe
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 });
const wr7Shape = (front: string) => `평면 꺾임 · 갈고리 ${h25} · 안쪽 모서리 c 지나는 대각 (헌치 빗변과 나란히) · ${front} − c ~ 날개벽 바깥면 − c · 갈고리 ${h25}`;
push({ mark: 'WR7-1', size: SZ['WR7-1'], role: '헌치', spacing: hz2, actual: hz2, shape: wr7Shape('흉벽 전면'), position: '위 헌치 · 브라켓 상면 ~ 상단', note: '사용자 도면 2026-11-05: 긴 대각 + 양끝 평면 갈고리', basis: '규칙', modified: hMod });
push({ mark: 'WR7-2', size: SZ['WR7-2'], role: '헌치', spacing: hz2, actual: hz2, shape: wr7Shape('흉벽 전면'), position: '아래 헌치 윗부분 · 교좌면 ~ 브라켓 상면', note: '띠 경계 브라켓 상면 / 교좌면 (Q-WR6)', basis: '규칙', modified: hMod });
push({ mark: 'WR7-3', size: SZ['WR7-3'], role: '헌치', spacing: hz2, actual: hz2, shape: wr7Shape('벽체 전면'), position: '아래 헌치 아랫부분 · 저판 상면 ~ 교좌면', note: 'WR8 결번', basis: '규칙', modified: hMod });
push({ mark: 'WR9', size: SZ.WR9, role: '헌치', spacing: lenText(2 * sBack), actual: lenText(2 * sBack), shape: `꺾임 · 연직 다리 ${lenText(la9)} · 헌치 빗변과 나란한 대각 · 저판 하면망에서 90° 갈고리 ${h16}`, position: '저판 상면 헌치 (날개벽 배면) · 벽체 배면 ~ 뒷굽 끝', note: `연직헌치 보강 (사용자 도면 2026-11-04) · @${lenText(2 * sBack)} 자동 개수 · 연직 다리 = 벽체 W9 · W10 정착장`, 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 });
+3 -1
View File
@@ -3163,8 +3163,10 @@ function abutWingsBuilt(rec: AbutRecord, footing: AbutFootingRebar): { geom: Abu
const model = buildAbut(toAbutInput(rec));
const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, rec.shape);
const material = materialFor(state.material, state.fckByMember, 'abutBody');
const zOf = (el: number) => (el - (members.footingBottomEl ?? 0)) * 1000;
const seats = members.wallSegments.map((sg) => ({ from: sg.sFrom * 1000, to: sg.sTo * 1000, z: zOf(sg.seatEl) }));
return buildWings(model, members, rec.wings, rec.wingDims).map((w) => {
const geom = abutWingGeom(model, members, w, footing.z.B68);
const geom = abutWingGeom(model, members, w, footing.z.B68, seats, abutSkewK(model));
// WR9 연직 다리 = 벽체 W9 · W10 정착장 (사용자 결정 2026-11-04).
return { geom, result: abutWingRebar(geom, rec.rebar?.wing, material, rec.name, resolveWallRebar(rec.rebar?.wall).la) };
});
+2 -1
View File
@@ -37,7 +37,8 @@ const res: Record<string, { out: number; total: number }> = {};
const span = slabSpanAt(abutPlanOutline(model, members, wings, slab).slab, tM, k)!;
const pg = abutParapetGeom(wg, members.shape, model.bearings[0].s, { B: model.wing!.slabWidthMm, a: span.a, b: span.b, nearT: members.shape.parapetBackM * 1000 - slab.parapetGapMm, chamferA: 300, chamferB: 300 });
const para = abutParapetRebar(pg, wall);
const wingR = wings.map((w) => abutWingRebar(abutWingGeom(model, members, w, footing.z.B68)));
const seats = members.wallSegments.map((sg) => ({ from: sg.sFrom * 1000, to: sg.sTo * 1000, z: (sg.seatEl - members.footingBottomEl!) * 1000 }));
const wingR = wings.map((w) => abutWingRebar(abutWingGeom(model, members, w, footing.z.B68, seats, k)));
const rebar = buildAbutRebar3D(fg, footing, k, members.shape.footingBackM, members.footingBottomEl!, wall, para, wingR);
const group = buildAbutGroup(model, members, wings);
group.updateMatrixWorld(true);
+34 -1
View File
@@ -10,6 +10,7 @@ import { abutFootingGeom, abutFootingRebar } from '../src/core/abut/rebar/footin
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';
import { abutSkewK } from '../src/view/abut-scene';
// 예제 A1 좌측 날개벽 (교대날개벽철근.md §11). 기획서 값은 fck 27 · 벽체 높이 2500 기준 (흉벽 상단 46.835 / 47.180).
const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url));
@@ -19,7 +20,8 @@ const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLeng
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 seats = members.wallSegments.map((sg) => ({ from: sg.sFrom * 1000, to: sg.sTo * 1000, z: (sg.seatEl - members.footingBottomEl!) * 1000 }));
const geomOf = (side: 'left' | 'right') => abutWingGeom(model, members, wings.find((w) => w.side === side)!, footing.z.B68, seats, abutSkewK(model));
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);
@@ -137,3 +139,34 @@ describe('③ 날개벽 삽도', () => {
for (const m of ['WR7', 'WR10', 'WR9']) expect(svg).toContain(`data-pick="${m}"`);
});
});
describe('WR7 평면 형상 (사용자 도면 2026-11-05)', () => {
it('WR7-3: 벽체 전면 − c에서 교대 안쪽으로 갈고리 394 → 안쪽 모서리를 c 떨어져 지나는 긴 대각 → 날개벽 바깥면 − c에서 날개벽 쪽 갈고리 394', () => {
const p = L1.paths.find((q) => q.mark === 'WR7-3')!;
expect(p.pts.length).toBe(4);
const [h, F, G, e] = p.pts;
expect(F[1]).toBeCloseTo(left.wallFrontT - 100, 6);
expect(h[1]).toBeCloseTo(F[1], 6); // 갈고리는 전면 선을 따라
expect(Math.hypot(h[0] - F[0], h[1] - F[1])).toBeCloseTo(394, 6);
// G는 날개벽 바깥면에서 c 안쪽.
const [ws, wt] = left.w;
const iw = left.inward[0] * ws + left.inward[1] * wt;
const nv = [left.inward[0] - iw * ws, left.inward[1] - iw * wt];
const T = Math.hypot(nv[0], nv[1]);
const depth = ((G[0] - left.p0[0]) * nv[0] + (G[1] - left.p0[1]) * nv[1]) / T;
expect(depth).toBeCloseTo(100, 6);
expect(Math.hypot(e[0] - G[0], e[1] - G[1])).toBeCloseTo(394, 6);
// 긴 대각: 벽체 두께를 가로지름 (3 m 이상).
expect(Math.hypot(G[0] - F[0], G[1] - F[1])).toBeGreaterThan(3000);
});
it('WR7-1 · 7-2는 흉벽 전면 − c에서 끝남', () => {
for (const m of ['WR7-1', 'WR7-2']) expect(L1.paths.find((q) => q.mark === m)!.pts[1][1]).toBeCloseTo(-100, 6);
});
it('우측: 긴 대각이 더 낮은 이웃 계단까지 가는 단은 WR7-3 대신 WR7-2 (교좌면 위로 나오지 않게)', () => {
const R = abutWingRebar(geomOf('right'), undefined, fck27);
const total = R.haunchLevels['WR7-1'] + R.haunchLevels['WR7-2'] + R.haunchLevels['WR7-3'];
expect(R.haunchLevels['WR7-3']).toBeLessThan(total - R.haunchLevels['WR7-1'] - 6);
});
});
+2 -1
View File
@@ -37,7 +37,8 @@ it('dump 콘크리트 밖 철근', () => {
const span = slabSpanAt(abutPlanOutline(model, members, wings, slab).slab, tM, k)!;
const pg = abutParapetGeom(wg, members.shape, model.bearings[0].s, { B: model.wing!.slabWidthMm, a: span.a, b: span.b, nearT: members.shape.parapetBackM * 1000 - slab.parapetGapMm, chamferA: 300, chamferB: 300 });
const para = abutParapetRebar(pg, wall);
const wingR = wings.map((w) => abutWingRebar(abutWingGeom(model, members, w, footing.z.B68)));
const seats = members.wallSegments.map((sg) => ({ from: sg.sFrom * 1000, to: sg.sTo * 1000, z: (sg.seatEl - members.footingBottomEl!) * 1000 }));
const wingR = wings.map((w) => abutWingRebar(abutWingGeom(model, members, w, footing.z.B68, seats, k)));
const rebar = buildAbutRebar3D(fg, footing, k, members.shape.footingBackM, members.footingBottomEl!, wall, para, wingR);
const group = buildAbutGroup(model, members, wings);
group.updateMatrixWorld(true);