diff --git a/src/core/abut/rebar/wing.ts b/src/core/abut/rebar/wing.ts index 9f56e97..c925fbd 100644 --- a/src/core/abut/rebar/wing.ts +++ b/src/core/abut/rebar/wing.ts @@ -5,7 +5,7 @@ * 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), + * - WR6 경사 하단 2개, WR7-1 · 7-2 · 7-3 헌치 꺾임 (전면 수평과 같은 단), WR9 저판 상면 헌치 보강 꺾임 (@2·s_h 자동 개수, 사용자 도면), * WR10 모서리 꺾임 (@s_v), WR20 간격재. 교대 철근과의 간섭은 보지 않는다 (Q-WR2 · Q-WR4 결정). * - 단축형 (QA-14 · §9): WR1 · WR11 · WR6 없음, 연직은 모두 등단면. * - 좌표: 교대 (s, t, z) — s 교대축 (IP1에서), t 교대축 직각 (전면 +), z 저판 하면에서. 날개벽 거리 d는 교대축 선에서 날개벽 방향. @@ -196,7 +196,8 @@ export function abutWingGeom( }; } -export function abutWingRebar(g: AbutWingGeom, changes?: Partial, material: Material = DEFAULT_MATERIAL, who = ''): AbutWingRebar { +/** @param la9 WR9 연직 다리 = 벽체 W9 · W10 정착장 (벽체 카드 입력, 기본 330). */ +export function abutWingRebar(g: AbutWingGeom, changes?: Partial, material: Material = DEFAULT_MATERIAL, who = '', la9 = 330): AbutWingRebar { const s = resolveWingRebar(changes); const issues: Issue[] = []; const side = g.side === 'left' ? '좌' : '우'; @@ -396,21 +397,33 @@ export function abutWingRebar(g: AbutWingGeom, changes?: Partial 0) { 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]; + const k9 = g.haunch - c * Math.SQRT2; // (깊이 − T) + (z − 저판 상면) = k9 on the bar line + const zTopLeg = g.footTop + k9 + c; // at depth T − c + const zLow = zHook16; + const depthLow = T + k9 - (zLow - g.footTop); + // 대각 · 갈고리가 날개벽 옆으로 ~1.4 m 가며 skew로 뒤쪽(t −)으로도 밀리므로, 갈고리 끝이 저판 배면 + c 안에 드는 범위까지만. + const footBackT = -g.heelD * Math.abs(wt); + const hookT0 = at(0, depthLow + h16)[1]; + const dKeep = wt < 0 ? (footBackT + c - hookT0) / wt : Infinity; + const from = g.wallBackD + c; + const to = Math.min(Math.min(g.heelD, g.L) - c, dKeep); + const n9 = to > from ? Math.floor((to - from) / sp9 + 1e-9) + 1 : 0; + const off = (to - from - (n9 - 1) * sp9) / 2; 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); + const dd = from + off + i * sp9; + const pts: [number, number, number][] = [ + P3(at(dd, T - c), zTopLeg + la9), + P3(at(dd, T - c), zTopLeg), + P3(at(dd, depthLow), zLow), + P3(at(dd, depthLow + h16), zLow), + ]; + put('WR9', pts, la9 + Math.hypot(depthLow - (T - c), zTopLeg - zLow) + h16); } } // WR10 (§7): 모서리 L 꺾임 @s_v, 저판 상면 + c ~ 흉벽 상단 − c. 교대 배면 · 날개벽 배면에서 각 120.65 안, 모서리 대각 300, 양끝 l_d. @@ -475,7 +488,7 @@ export function abutWingRebar(g: AbutWingGeom, changes?: Partial { const geom = abutWingGeom(model, members, w, footing.z.B68); - return { geom, result: abutWingRebar(geom, rec.rebar?.wing, material, rec.name) }; + // WR9 연직 다리 = 벽체 W9 · W10 정착장 (사용자 결정 2026-11-04). + return { geom, result: abutWingRebar(geom, rec.rebar?.wing, material, rec.name, resolveWallRebar(rec.rebar?.wall).la) }; }); } catch { return []; @@ -3604,7 +3605,7 @@ function fillAbutWingRebarCard(box: HTMLDivElement, rec: AbutRecord, wings: { ge ['배면 s · 전면 · 헌치 2s', `${lenText(s.sVert ?? s.sH)} · ${lenText(2 * (s.sVert ?? s.sH))}`], ['연직 기준선 (뒷굽 끝 − c)', `전면 ${lenText(r.vertical.front.base)} · 배면 ${lenText(r.vertical.back.base)}`], ['수평 단 (교대 쪽 · 끝)', `전면 ${r.levels.front.start} · ${r.levels.front.end} / 배면 ${r.levels.back.start} · ${r.levels.back.end}`], - ['WR9 개수 (헌치 대각, 자동)', String(r.bars.find((b) => b.mark === 'WR9')?.count ?? 0)], + ['WR9 (저판 상면 헌치 보강, 자동)', `${r.bars.find((b) => b.mark === 'WR9')?.count ?? 0}개 @${lenText(2 * (s.sVert ?? s.sH))} · 연직 다리 = 벽체 정착장 ${lenText(resolveWallRebar(rec.rebar?.wall).la)}`], [`l_d (fck ${fck})`, `H13 ${r.ld.H13} · H16 ${r.ld.H16} · H25 ${r.ld.H25}`], ], bars: r.bars, diff --git a/tests/abut-rebar-wing.test.ts b/tests/abut-rebar-wing.test.ts index 3014091..02f02a4 100644 --- a/tests/abut-rebar-wing.test.ts +++ b/tests/abut-rebar-wing.test.ts @@ -56,8 +56,18 @@ describe('교대 날개벽 배근 A1 좌 (AD7, 교대날개벽철근.md §11)', 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('WR9 (사용자 도면 2026-11-04): 저판 상면 헌치 보강, 벽체 배면 ~ 뒷굽 끝 @250 → 우 4개 · 좌 3개 (skew로 갈고리가 저판 배면 쪽으로 밀려 범위가 짧음); 연직 다리 330 → 헌치 빗변과 나란한 대각 → 하면망에서 갈고리 246', () => { + expect(bar(abutWingRebar(geomOf('right'), undefined, fck27), 'WR9')?.count).toBe(4); + expect(bar(L1, 'WR9')?.count).toBe(3); + const p = L1.paths.find((q) => q.mark === 'WR9')!; + expect(p.pts.length).toBe(4); + expect(p.pts[0][2] - p.pts[1][2]).toBeCloseTo(330, 6); // 연직 다리 + expect(p.pts[2][2]).toBeCloseTo(p.pts[3][2], 6); // 갈고리는 수평 + expect(Math.hypot(p.pts[3][0] - p.pts[2][0], p.pts[3][1] - p.pts[2][1])).toBeCloseTo(246, 6); + // 대각은 45° (헌치 빗변과 나란히): 수평 이동 = 높이 차. + const run = Math.hypot(p.pts[2][0] - p.pts[1][0], p.pts[2][1] - p.pts[1][1]); + expect(run).toBeCloseTo(p.pts[1][2] - p.pts[2][2], 3); + expect(abutWingRebar(left, undefined, fck27, '', 400).paths.find((q) => q.mark === 'WR9')!.pts[0][2] - p.pts[1][2]).toBeCloseTo(400, 6); }); it('우측: 상단은 우측 흉벽 상단 47.180 기준 (저판 상면 위 5185)', () => {