From b75f3d48558075f350a32c579b27fd8e8d72e1f2 Mon Sep 17 00:00:00 2001 From: ypshim Date: Thu, 5 Nov 2026 09:27:52 +0900 Subject: [PATCH] =?UTF-8?q?feat(abut-rebar):=20=E2=91=A2=20=EA=B5=90?= =?UTF-8?q?=EB=8C=80=20=EC=A0=91=EC=86=8D=EC=8A=AC=EB=9E=98=EB=B8=8C=203D?= =?UTF-8?q?=20=EC=B2=A0=EA=B7=BC=20(R1=E2=80=93R8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 접속슬래브 배근 경로를 교대 3D 철근에 추가 (선택 · 색 기준 공통) - 콘크리트 안 검사에 접속슬래브 포함: 모든 교대 철근 (P10 다웰 포함) 밖으로 나간 점 없음 Co-Authored-By: Claude Haiku 5.5 --- src/main.ts | 4 ++-- src/view/abut-rebar3d.ts | 12 ++++++++++-- tests/abut-rebar-inside.test.ts | 23 +++++++++++------------ verify/dump/abut-rebar-outside.dump.ts | 9 ++++++--- 4 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/main.ts b/src/main.ts index 34ad43d..98b5ded 100644 --- a/src/main.ts +++ b/src/main.ts @@ -697,7 +697,7 @@ function setupScene(): void { state.scene = scene; setView(state.view); } - // ③ 교대 (2026-11-04): with the piers hidden, the selected abutment's 저판 · 벽체 · 흉벽 · 날개벽 rebar is the ③ rebar (표 · 3D 선택 공통). + // ③ 교대 (2026-11-04): with the piers hidden, the selected abutment's 저판 · 벽체 · 흉벽 · 날개벽 · 접속슬래브 rebar is the ③ rebar (표 · 3D 선택 공통). const abutRec = piersShown(state.stage, state.abutActive) ? undefined : state.abutments.find((a) => a.name === state.abutActive); const abutGroup = abutRec ? abutGroupByName.get(abutRec.name) : undefined; const footing = abutRec ? abutFootingBuilt(abutRec) : null; @@ -705,7 +705,7 @@ function setupScene(): void { const wall = abutWallBuilt(abutRec, footing.result); const para = wall ? abutParapetBuilt(abutRec, wall) : null; const wings = abutWingsBuilt(abutRec, footing.result).map((w) => w.result); - rebar3d = buildAbutRebar3D(footing.geom, footing.result, footing.skewK, footing.backM, footing.bottomEl, wall?.result, para?.result, wings); + rebar3d = buildAbutRebar3D(footing.geom, footing.result, footing.skewK, footing.backM, footing.bottomEl, wall?.result, para?.result, wings, abutSlabBuilt(abutRec)?.result); rebar3d.group.applyMatrix4(abutGroup.matrix); rebar3d.group.visible = state.stage === 'rebar'; rebar3d.setColorMode(state.rebarColor); diff --git a/src/view/abut-rebar3d.ts b/src/view/abut-rebar3d.ts index 52201ce..90f7f1b 100644 --- a/src/view/abut-rebar3d.ts +++ b/src/view/abut-rebar3d.ts @@ -1,5 +1,5 @@ /** - * 교대 3D 철근 (③, 2026-11-04): 저판 B1–B10, 벽체 W1–W11, 흉벽 · 브라켓 P1–P11, 날개벽 WR1–WR20 (좌 · 우). Positions come from `abutFootingRebar` · + * 교대 3D 철근 (③, 2026-11-04): 저판 B1–B10, 벽체 W1–W11, 흉벽 · 브라켓 P1–P11, 날개벽 WR1–WR20 (좌 · 우), 접속슬래브 R1–R8. Positions come from `abutFootingRebar` · * `abutWallRebar` · `abutParapetRebar` (no new rules here; the 벽체 · 흉벽 bars are their paths). The 저판 shapes follow the * 배근 삽도 (abut-footing-rebar-svg.ts): B1 ⊓ / B2 ⊔ / B3 · B4 직선, B5 ⊓ / B6 ⊔ / B7 · B8 직선, B9 연직, B10 측면, B1-S · B2-S. * Hooks and the bend radii are not drawn. Each bar is named by its 철근표 number, so the table, the 삽도 and the model @@ -14,6 +14,8 @@ import { ABUT_PARAPET_SIZES, type AbutParapetRebar } from '../core/abut/rebar/pa import { ABUT_PARAPET_COLORS } from './abut-parapet-rebar-svg'; import { ABUT_WING_SIZES, type AbutWingRebar } from '../core/abut/rebar/wing'; import { ABUT_WING_COLORS } from './abut-wing-rebar-svg'; +import { ABUT_SLAB_SIZES, type AbutSlabRebar } from '../core/abut/rebar/slab'; +import { ABUT_SLAB_COLORS } from './abut-slab-rebar-svg'; import { ABUT_FOOTING_COLORS } from './abut-footing-rebar-svg'; import { ABUT_WALL_COLORS } from './abut-wall-rebar-svg'; import { rebarColor, rebarControls, type RebarScene } from './rebar3d'; @@ -33,12 +35,13 @@ export function buildAbutRebar3D( wall?: AbutWallRebar, parapet?: AbutParapetRebar, wings: AbutWingRebar[] = [], + slab?: AbutSlabRebar, ): RebarScene { const group = new THREE.Group(); group.name = 'abut-rebar'; const byName = new Map(); const C = Object.fromEntries(Object.entries(ABUT_FOOTING_COLORS).map(([k, v]) => [k, parseInt(v.slice(1), 16)])) as Record; - const sizeOf = new Map([...r.bars.map((b) => [b.mark, b.size] as const), ...Object.entries(ABUT_WALL_SIZES), ...Object.entries(ABUT_PARAPET_SIZES), ...Object.entries(ABUT_WING_SIZES)]); + const sizeOf = new Map([...r.bars.map((b) => [b.mark, b.size] as const), ...Object.entries(ABUT_WALL_SIZES), ...Object.entries(ABUT_PARAPET_SIZES), ...Object.entries(ABUT_WING_SIZES), ...Object.entries(ABUT_SLAB_SIZES)]); // (s, x, z) in mm: s along the skew grid (끝면과 나란히), x from the 배면, z from the 저판 하면. const P = (s: number, x: number, z: number): P3 => [s / 1000 + skewK * (backM + x / 1000), bottomEl + z / 1000, backM + x / 1000]; // Option ②: square to the axis, s in the 삽도's frame (abutFootingRebarPlanSvg SQ), the same for every x. @@ -131,5 +134,10 @@ export function buildAbutRebar3D( const wingColor = (mark: string) => ['WR1', 'WR2', 'WR11', 'WR12'].includes(mark) ? G.vertical : /^WR(3|4|5|13|14|15)$/.test(mark) ? G.horizontal : mark === 'WR6' || mark === 'WR20' ? G.other : G.haunch; for (const w of wings) for (const p of w.paths) add(p.mark, p.pts.map(([s, t, z]): P3 => [s / 1000, bottomEl + z / 1000, t / 1000]), wingColor(p.mark)); + if (slab) { + const S = Object.fromEntries(Object.entries(ABUT_SLAB_COLORS).map(([k, v]) => [k, parseInt(v.slice(1), 16)])) as Record; + const slabColor = (mark: string) => (mark === 'R1' || mark === 'R3' ? S.main : mark === 'R7' ? S.corner : mark === 'R8' ? S.shear : S.dist); + for (const p of slab.paths) add(p.mark, p.pts.map(([s, t, z]): P3 => [s / 1000, bottomEl + z / 1000, t / 1000]), slabColor(p.mark)); + } return { group, byName, records: [], ...rebarControls(byName) }; } diff --git a/tests/abut-rebar-inside.test.ts b/tests/abut-rebar-inside.test.ts index e2156ec..9132089 100644 --- a/tests/abut-rebar-inside.test.ts +++ b/tests/abut-rebar-inside.test.ts @@ -7,7 +7,8 @@ import { buildAbut } from '../src/core/abut/geometry'; import { parseAbutments, toAbutInput } from '../src/core/abut/parse'; import { buildMembers } from '../src/core/abut/members'; import { buildWings } from '../src/core/abut/wings'; -import { resolveSlab } from '../src/core/abut/slab'; +import { resolveSlab, slabGradePct, slabSection } from '../src/core/abut/slab'; +import { abutSlabGeom, abutSlabRebar } from '../src/core/abut/rebar/slab'; import { abutFootingGeom, abutFootingRebar } from '../src/core/abut/rebar/footing'; import { skewAngle } from '../src/core/abut/rebar/skew-layout'; import { abutWallGeom, abutWallRebar } from '../src/core/abut/rebar/wall'; @@ -39,12 +40,14 @@ const res: Record = {}; const para = abutParapetRebar(pg, wall); 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 grade = slabGradePct(slab, model.wing?.slopePct ?? 0); + const slabG = abutSlabGeom(slab.form, abutPlanOutline(model, members, wings, slab).slab, slabSection(slab, grade, members.shape), { startEl: members.parapetTopStartEl, endEl: members.parapetTopEndEl, L: model.length, bottomEl: members.footingBottomEl! }, [300, 300], slab.lengthMm, grade); + const rebar = buildAbutRebar3D(fg, footing, k, members.shape.footingBackM, members.footingBottomEl!, wall, para, wingR, abutSlabRebar(slabG)); const group = buildAbutGroup(model, members, wings); group.updateMatrixWorld(true); const solids: THREE.Mesh[] = []; group.traverse((o) => { - if (o instanceof THREE.Mesh && !['ground', 'slab', 'blinding', 'piles'].some((n) => o.parent?.name === n || o.name === n)) solids.push(o); + if (o instanceof THREE.Mesh && !['ground', 'blinding', 'piles'].some((n) => o.parent?.name === n || o.name === n)) solids.push(o); }); const ray = new THREE.Raycaster(); const inside = (p: THREE.Vector3) => @@ -74,15 +77,11 @@ const res: Record = {}; } } -describe('교대 A1 3D 철근이 콘크리트 안에 있음', () => { - it('저판 · 벽체 · 흉벽 · 날개벽 철근은 밖으로 나간 점이 없음 (P10 제외)', () => { - const outside = Object.entries(res).filter(([name, v]) => name !== 'P10' && v.out > 0).map(([name, v]) => `${name} ${v.out}/${v.total}`); +describe('교대 A1 3D 철근이 콘크리트 안에 있음 (접속슬래브 포함)', () => { + it('저판 · 벽체 · 흉벽 · 날개벽 · 접속슬래브 철근과 P10 다웰 모두 밖으로 나간 점이 없음', () => { + const outside = Object.entries(res).filter(([, v]) => v.out > 0).map(([name, v]) => `${name} ${v.out}/${v.total}`); expect(outside).toEqual([]); - expect(Object.keys(res).length).toBeGreaterThan(30); - }); - - it('P10 다웰: 슬래브 쪽 절반만 밖 (브라켓 안 300은 콘크리트 안)', () => { - expect(res.P10.out / res.P10.total).toBeLessThan(0.6); - expect(res.P10.out / res.P10.total).toBeGreaterThan(0.4); + expect(Object.keys(res).length).toBeGreaterThan(36); + for (const m of ['R1', 'R2', 'R3', 'R4', 'R7', 'R8', 'P10']) expect(res[m]?.total, m).toBeGreaterThan(0); }); }); diff --git a/verify/dump/abut-rebar-outside.dump.ts b/verify/dump/abut-rebar-outside.dump.ts index 1aea92c..7f93651 100644 --- a/verify/dump/abut-rebar-outside.dump.ts +++ b/verify/dump/abut-rebar-outside.dump.ts @@ -9,7 +9,8 @@ import { buildAbut } from '../../src/core/abut/geometry'; import { parseAbutments, toAbutInput } from '../../src/core/abut/parse'; import { buildMembers } from '../../src/core/abut/members'; import { buildWings } from '../../src/core/abut/wings'; -import { resolveSlab } from '../../src/core/abut/slab'; +import { resolveSlab, slabGradePct, slabSection } from '../../src/core/abut/slab'; +import { abutSlabGeom, abutSlabRebar } from '../../src/core/abut/rebar/slab'; import { abutFootingGeom, abutFootingRebar } from '../../src/core/abut/rebar/footing'; import { skewAngle } from '../../src/core/abut/rebar/skew-layout'; import { abutWallGeom, abutWallRebar } from '../../src/core/abut/rebar/wall'; @@ -39,12 +40,14 @@ it('dump 콘크리트 밖 철근', () => { const para = abutParapetRebar(pg, wall); 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 grade = slabGradePct(slab, model.wing?.slopePct ?? 0); + const slabG = abutSlabGeom(slab.form, abutPlanOutline(model, members, wings, slab).slab, slabSection(slab, grade, members.shape), { startEl: members.parapetTopStartEl, endEl: members.parapetTopEndEl, L: model.length, bottomEl: members.footingBottomEl! }, [300, 300], slab.lengthMm, grade); + const rebar = buildAbutRebar3D(fg, footing, k, members.shape.footingBackM, members.footingBottomEl!, wall, para, wingR, abutSlabRebar(slabG)); const group = buildAbutGroup(model, members, wings); group.updateMatrixWorld(true); const solids: THREE.Mesh[] = []; group.traverse((o) => { - if (o instanceof THREE.Mesh && !['ground', 'slab', 'blinding', 'piles'].some((n) => o.parent?.name === n || o.name === n)) solids.push(o); + if (o instanceof THREE.Mesh && !['ground', 'blinding', 'piles'].some((n) => o.parent?.name === n || o.name === n)) solids.push(o); }); const ray = new THREE.Raycaster(); const inside = (p: THREE.Vector3) =>