From 74de932eb9937e1a1680c94486bf1261b7d0d325 Mon Sep 17 00:00:00 2001 From: ypshim Date: Wed, 4 Nov 2026 16:45:20 +0900 Subject: [PATCH] =?UTF-8?q?fix(abut-rebar):=20=EB=B2=BD=EC=B2=B4=20W9=20?= =?UTF-8?q?=C2=B7=20W10=20=EA=B5=AC=EA=B0=84=EB=B3=84=20=E2=8A=93=ED=98=95?= =?UTF-8?q?=20(=EC=96=91=EB=81=9D=20=EB=8B=A4=EB=A6=AC=20l=5Fd)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자 도면(2026-11-04): 구간마다 독립된 ⊓, 양끝 계단 경계 − c (끝면 쪽은 W11 안쪽)에서 아래로 다리 l_d 735 - 낮은 이웃 구간으로의 직선 연장과 RB-AB06 피복 경고 삭제 - 계단 단면 삽도 · 안내 문구 반영 Co-Authored-By: Claude Haiku 5.5 --- src/core/abut/rebar/wall.ts | 18 +++++++----------- src/main.ts | 4 ++-- src/view/abut-wall-rebar-svg.ts | 4 +++- tests/abut-rebar-wall.test.ts | 28 +++++++++++++--------------- 4 files changed, 25 insertions(+), 29 deletions(-) diff --git a/src/core/abut/rebar/wall.ts b/src/core/abut/rebar/wall.ts index 07c8b5b..e169915 100644 --- a/src/core/abut/rebar/wall.ts +++ b/src/core/abut/rebar/wall.ts @@ -242,20 +242,16 @@ export function abutWallRebar(g: AbutWallGeom, footing: AbutFootingRebar, change // 끝 여유 (§6.1): W5 · W6 · W9 · W10 끝은 W11 안쪽. 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; + // W9 · W10 (§5.2, 사용자 도면 2026-11-04): W2 바로 아래, 구간마다 독립된 ⊓형. 양끝은 계단 경계 − c (끝면 쪽은 W11 안쪽)에서 + // 아래로 l_d 꺾는다. 이웃 구간으로는 넘어가지 않는다. 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'); + const a = k === 0 ? endIn(SZ.W9) : sg.from + c; + const b = k === last ? g.L - endIn(SZ.W9) : sg.to - c; + for (const x of xW9) put(mark, [[a, x, z - ld.H16], [a, x, z], [b, x, z], [b, x, z - ld.H16]], b - a + 2 * ld.H16, 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; @@ -338,8 +334,8 @@ export function abutWallRebar(g: AbutWallGeom, footing: AbutFootingRebar, change 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: 'W9', size: SZ.W9, role: '계단 상면', spacing: tSp, actual: tSp, shape: `⊓ · 양끝 다리 l_d ${ld.H16} (경계 − ${lenText(c)})`, 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} (경계 − ${lenText(c)})`, 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 }); return { diff --git a/src/main.ts b/src/main.ts index b66fe3b..30c6b45 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3265,7 +3265,7 @@ function fillAbutWallRebarCard(box: HTMLDivElement, rec: AbutRecord, built: { ge 'figcaption', state.abutWallView === 'sec' ? '교대축 직각 단면 · 구간 1 (IP3 끝) · 왼쪽 배면 → 오른쪽 전면 · 선 = 연직 · W2 · W7, 점 = W5 · W6 · W9 · W10' - : '교대축 단면 · 전면 철근 · 왼쪽 IP3 = 구간 1 → 오른쪽 IP1 · W5는 계단에서 끊김, W9 · W10은 낮은 쪽 이웃 구간으로 l_d 연장', + : '교대축 단면 · 전면 철근 · 왼쪽 IP3 = 구간 1 → 오른쪽 IP1 · W5는 계단에서 끊김, W9 · W10은 구간마다 ⊓ (양끝 경계 − 피복에서 다리 l_d)', 'muted small', ), ); @@ -3274,7 +3274,7 @@ function fillAbutWallRebarCard(box: HTMLDivElement, rec: AbutRecord, built: { ge box.append( el( 'p', - `벽체 높이 구간 ${[...geom.segments].sort((a, b) => a.no - b.no).map((sg) => `${sg.no} ${Math.round(sg.H)}`).join(' · ')} · 수평 W5는 높이가 들어가는 구간까지만 (위 계단 면 − ${lenText(s.c)}에서 끊음) · W9 · W10은 낮은 쪽 이웃 구간으로 l_d ${r.ld.H16} 연장`, + `벽체 높이 구간 ${[...geom.segments].sort((a, b) => a.no - b.no).map((sg) => `${sg.no} ${Math.round(sg.H)}`).join(' · ')} · 수평 W5는 높이가 들어가는 구간까지만 (위 계단 면 − ${lenText(s.c)}에서 끊음) · W9 · W10은 구간마다 ⊓, 양끝 다리 l_d ${r.ld.H16}`, 'muted small', ), ); diff --git a/src/view/abut-wall-rebar-svg.ts b/src/view/abut-wall-rebar-svg.ts index a08bd8b..52f5a7c 100644 --- a/src/view/abut-wall-rebar-svg.ts +++ b/src/view/abut-wall-rebar-svg.ts @@ -96,7 +96,9 @@ export function abutWallElevationSvg(g: AbutWallGeom, r: AbutWallRebar, pick: Ab const [u1, , z1] = p.pts[p.pts.length - 1]; 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 === 'W9' || p.mark === 'W10') && Math.abs(t0 - r.x.W9[0]) < 1e-6) { + for (let i = 1; i < p.pts.length; i++) parts.push(seg(p.mark, [p.pts[i - 1][0], p.pts[i - 1][2]], [p.pts[i][0], p.pts[i][2]], 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)); diff --git a/tests/abut-rebar-wall.test.ts b/tests/abut-rebar-wall.test.ts index 88b8997..5879cde 100644 --- a/tests/abut-rebar-wall.test.ts +++ b/tests/abut-rebar-wall.test.ts @@ -81,15 +81,19 @@ describe('교대 벽체 배근 A1 (AD4, 교대벽체철근.md §11)', () => { expect(bar(abutWallRebar(withSegs(4), footing, undefined, fck27), 'W10').count).toBe(12); }); - it('W9 extends l_d 735 into the lower neighbour only', () => { - const k = 2; // a middle step, neighbours on both sides + it('W9 · W10: ⊓ per step, ends at the boundary − 100 (끝면 쪽은 W11 안쪽), legs down l_d 735 (사용자 도면 2026-11-04)', () => { + const k = 2; // a middle step const sg = geom.segments[k]; const z = 1100 + sg.H - 100 - 15.9; - const p = A1.paths.find((q) => q.mark === 'W10' && Math.abs(q.pts[0][2] - z) < 1e-6)!; - const lowLeft = geom.segments[k - 1].H < sg.H; - const lowRight = geom.segments[k + 1].H < sg.H; - expect(p.pts[0][0]).toBeCloseTo(lowLeft ? sg.from - 735 : sg.from, 6); - expect(p.pts[1][0]).toBeCloseTo(lowRight ? sg.to + 735 : sg.to, 6); + const p = A1.paths.find((q) => q.mark === 'W10' && Math.abs(q.pts[1][2] - z) < 1e-6)!; + expect(p.pts.map((q) => Math.round(q[0] * 10) / 10)).toEqual([sg.from + 100, sg.from + 100, sg.to - 100, sg.to - 100].map((v) => Math.round(v * 10) / 10)); + expect(p.pts[0][2]).toBeCloseTo(z - 735, 6); + expect(p.pts[3][2]).toBeCloseTo(z - 735, 6); + const w = A1.bars.find((b) => b.mark === 'W10')!; + expect(w.length).toBe(Math.round(sg.to - sg.from - 200 + 2 * 735)); + // 끝 계단: 끝면 쪽은 W11 안쪽 (100 + (15.9 + 15.9)/2) / sin θ. + const w9 = A1.paths.find((q) => q.mark === 'W9')!; + expect(w9.pts[1][0]).toBeCloseTo((100 + 15.9) / angle.sin, 6); }); it('W7: 12 × 5 = 60, 2200 / sin θ + 2 × 197 = 2603 (skew, 사용자 결정 2026-11-04)', () => { @@ -113,14 +117,8 @@ describe('교대 벽체 배근 A1 (AD4, 교대벽체철근.md §11)', () => { expect(xs).toEqual([-700, -400, -100, ...Array.from({ length: 13 }, (_, i) => i * 125)]); }); - 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('A1 has no warning (RB-AB02 · RB-AB03 없음, W9 · W10은 이웃 구간으로 넘어가지 않음)', () => { + expect(A1.issues.filter((i) => i.level !== 'I').map((i) => i.code)).toEqual([]); }); it('l_d 연동: fck 30 → W2 앞 다리 · W3 위 끝이 짧아짐', () => {