From 34959f5b4e19f39f4b87e6144aad6716422a27be Mon Sep 17 00:00:00 2001 From: ypshim Date: Wed, 4 Nov 2026 16:40:23 +0900 Subject: [PATCH] =?UTF-8?q?fix(abut-rebar):=20=EB=B2=BD=EC=B2=B4=20W7=20sk?= =?UTF-8?q?ew=20=EA=B8=B8=EC=9D=B4=20=C2=B7=20W11=20=E3=84=B7=ED=98=95=20?= =?UTF-8?q?=C2=B7=20RB-AB06=20=ED=94=BC=EB=B3=B5=20=EA=B2=BD=EA=B3=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - W7 길이에 1/sin θ 적용 (2594 → 2603) - W11 위 갈고리 추가, 아래 · 위 모두 교대축 안쪽 ㄷ형 (3433/3717 → 3679/3963), 계단 단면 삽도에 반영 - W9 · W10 연장부 피복 < c이면 RB-AB06 경고만 (A1 44.9) Co-Authored-By: Claude Haiku 5.5 --- src/core/abut/rebar/wall.ts | 19 ++++++++++++------- src/view/abut-wall-rebar-svg.ts | 5 ++++- tests/abut-rebar-wall.test.ts | 22 ++++++++++++++++------ 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/core/abut/rebar/wall.ts b/src/core/abut/rebar/wall.ts index f0394c7..07c8b5b 100644 --- a/src/core/abut/rebar/wall.ts +++ b/src/core/abut/rebar/wall.ts @@ -3,7 +3,7 @@ * * - W1 전면 연직 (cycle-1만) / W3 · W4 배면 연직 (cycle-1 / 2): 저판 격자 − (d_B13 + d_W)/2 (§4.3). * - W2 교좌면 상면 (교대축 직각, skew) / W9 · W10 계단 상면 (교대축 방향, 끝 계단 / 중간 계단). - * - W5 · W6 전면 · 배면 수평 (계단에서 끊음) / W7 간격재 / W8 끝면 수평 (ㄷ형) / W11 끝면 연직. + * - W5 · W6 전면 · 배면 수평 (계단에서 끊음) / W7 간격재 / W8 끝면 수평 (ㄷ형) / W11 끝면 연직 (ㄷ형). * - 간격은 벽체 기본간격(s_L, s_W, s_v)에서 계산한다. 묶음별로 고치면 그 묶음만 연동이 끊긴다(RB-AB04). * - 좌표: u = 교대축을 따라 IP1 쪽 끝면에서 잰 skew 격자 위치 (끝면과 나란한 선이 u 일정), t = 교대축에서 전면 +, * z = 저판 하면에서. 저판 배근(footing.ts)과 같은 u를 쓴다. @@ -243,14 +243,19 @@ export function abutWallRebar(g: AbutWallGeom, footing: AbutFootingRebar, change const endIn = (size: BarSize) => (c + (d(SZ.W11) + d(size)) / 2) / sin; // W9 · W10 (§5.2): W2 바로 아래, 낮은 이웃 구간으로 l_d 연장, 높은 이웃 쪽은 경계에서 끝. + // 연장 부분이 그 이웃 교좌면 아래 피복 c를 지키지 못하면 경고만 한다 (RB-AB06, 사용자 결정 2026-11-04). + let thinCover = Infinity; 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; + for (const nb of [segs[k - 1], segs[k + 1]]) if (nb && nb.H < sg.H) thinCover = Math.min(thinCover, g.HF + nb.H - z); 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'); }); + if (thinCover < c - 1e-6) add('RB-AB06', 'W', `W9 · W10이 낮은 이웃 구간으로 l_d 연장되는 부분의 피복이 ${lenText(thinCover)}로 ${lenText(c)}보다 작습니다 (계단 높이 차 확인).`); + // 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; @@ -277,12 +282,12 @@ export function abutWallRebar(g: AbutWallGeom, footing: AbutFootingRebar, change }); 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° 갈고리. + // W7 (§6): 교대축 @k_7L·s_L, 연직 @k_7v·s_v, W3 선 ~ W5 선 (skew, 1/sin θ, 사용자 결정 2026-11-04) + 양끝 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)); + put('W7', [[u, xW34, z - h13], [u, xW34, z], [u, xW5, z], [u, xW5, z - h13]], (xW5 - xW34) / sin + 2 * h13, inOne(u)); } } @@ -302,8 +307,8 @@ export function abutWallRebar(g: AbutWallGeom, footing: AbutFootingRebar, change 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); + // ㄷ형 (사용자 결정 2026-11-04): 아래 · 위 90° 갈고리 모두 교대축 안쪽으로 (끝면 밖에는 콘크리트가 없음). + for (const x of [...xs11, ...seat11]) put('W11', [[u + dir * h16, x, zh16], [u, x, zh16], [u, x, top], [u + dir * h16, x, top]], top - zh16 + 2 * h16, sg === one); } // 표 (시안 순서). @@ -330,12 +335,12 @@ export function abutWallRebar(g: AbutWallGeom, footing: AbutFootingRebar, change 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: '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 · 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 }); + 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, diff --git a/src/view/abut-wall-rebar-svg.ts b/src/view/abut-wall-rebar-svg.ts index 79673e2..a08bd8b 100644 --- a/src/view/abut-wall-rebar-svg.ts +++ b/src/view/abut-wall-rebar-svg.ts @@ -97,7 +97,10 @@ export function abutWallElevationSvg(g: AbutWallGeom, r: AbutWallRebar, pick: Ab if (p.mark === 'W1') parts.push(seg('W1', [p.pts[1][0], p.pts[1][2]], [u1, z1], 0.7)); else if (p.mark === 'W5') parts.push(seg('W5', [u0, z0], [u1, z1], 0.8)); else if ((p.mark === 'W9' || p.mark === 'W10') && Math.abs(t0 - r.x.W9[0]) < 1e-6) parts.push(seg(p.mark, [u0, z0], [u1, z1], 2)); - else if (p.mark === 'W11' && Math.abs(t0 - w11Front) < 1e-6) parts.push(seg('W11', [p.pts[1][0], p.pts[1][2]], [u1, z1], 2)); + else if (p.mark === 'W11' && Math.abs(t0 - w11Front) < 1e-6) { + // ㄷ형: 연직 + 위 갈고리 (아래 갈고리는 저판 안). + parts.push(seg('W11', [p.pts[1][0], p.pts[1][2]], [p.pts[2][0], p.pts[2][2]], 2), seg('W11', [p.pts[2][0], p.pts[2][2]], [u1, z1], 2)); + } else if (p.mark === 'W8') parts.push(``); } // 단 치수 (양끝). diff --git a/tests/abut-rebar-wall.test.ts b/tests/abut-rebar-wall.test.ts index 9483d5b..88b8997 100644 --- a/tests/abut-rebar-wall.test.ts +++ b/tests/abut-rebar-wall.test.ts @@ -92,8 +92,8 @@ describe('교대 벽체 배근 A1 (AD4, 교대벽체철근.md §11)', () => { 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('W7: 12 × 5 = 60, 2200 / sin θ + 2 × 197 = 2603 (skew, 사용자 결정 2026-11-04)', () => { + expect([bar(A1, 'W7').count, bar(A1, 'W7').length]).toEqual([60, 2603]); }); it('W8: 3383, 낮은 끝 18 + 높은 끝 20 = 38, 끝면에서 114.3', () => { @@ -102,15 +102,25 @@ describe('교대 벽체 배근 A1 (AD4, 교대벽체철근.md §11)', () => { expect(w8.pts[1][0] * angle.sin).toBeCloseTo(114.3, 6); }); - it('W11: 끝면당 13 + 3 = 16, 양끝 32, 길이 3433 / 3717', () => { + it('W11: 끝면당 13 + 3 = 16, 양끝 32, ㄷ형 (위 갈고리 246 추가) 길이 3679 / 3963', () => { const w = bar(A1, 'W11'); - expect([w.count, w.length, w.lengthMax]).toEqual([32, 3433, 3717]); + expect([w.count, w.length, w.lengthMax]).toEqual([32, 3433 + 246, 3717 + 246]); + const p = A1.paths.find((q) => q.mark === 'W11')!; + expect(p.pts.length).toBe(4); + expect(p.pts[0][0]).toBeCloseTo(p.pts[3][0], 6); // both hooks turn the same way (inward) + expect(p.pts[3][0]).toBeGreaterThan(p.pts[1][0]); 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('A1 warns only RB-AB06: W9 · W10 연장부 피복 44.9 < 100 (계단 71.1)', () => { + expect(A1.issues.filter((i) => i.level !== 'I').map((i) => i.code)).toEqual(['RB-AB06']); + expect(A1.issues.find((i) => i.code === 'RB-AB06')!.message).toContain('44.9'); + }); + + it('RB-AB06 is gone when the steps are flat enough (높이 차 0)', () => { + const flat = { ...geom, segments: geom.segments.map((s) => ({ ...s, H: 2372 })) }; + expect(abutWallRebar(flat, footing, undefined, fck27).issues.map((i) => i.code)).not.toContain('RB-AB06'); }); it('l_d 연동: fck 30 → W2 앞 다리 · W3 위 끝이 짧아짐', () => {