diff --git a/src/core/abut/rebar/wing.ts b/src/core/abut/rebar/wing.ts index c925fbd..9c7b86d 100644 --- a/src/core/abut/rebar/wing.ts +++ b/src/core/abut/rebar/wing.ts @@ -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 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 `평면 꺾임 · 갈고리 ${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 }); diff --git a/src/main.ts b/src/main.ts index 821520f..e0da349 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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) }; }); diff --git a/tests/abut-rebar-inside.test.ts b/tests/abut-rebar-inside.test.ts index 9d5c853..e2156ec 100644 --- a/tests/abut-rebar-inside.test.ts +++ b/tests/abut-rebar-inside.test.ts @@ -37,7 +37,8 @@ const res: Record = {}; 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); diff --git a/tests/abut-rebar-wing.test.ts b/tests/abut-rebar-wing.test.ts index 02f02a4..77c0e74 100644 --- a/tests/abut-rebar-wing.test.ts +++ b/tests/abut-rebar-wing.test.ts @@ -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); + }); +}); diff --git a/verify/dump/abut-rebar-outside.dump.ts b/verify/dump/abut-rebar-outside.dump.ts index 97be977..1aea92c 100644 --- a/verify/dump/abut-rebar-outside.dump.ts +++ b/verify/dump/abut-rebar-outside.dump.ts @@ -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);