From 43edcdc3c534dab515fcc6695cc70ca396417993 Mon Sep 17 00:00:00 2001 From: ypshim Date: Tue, 3 Nov 2026 16:24:43 +0900 Subject: [PATCH] =?UTF-8?q?fix(abut):=20=ED=9D=89=EB=B2=BD=EA=B3=BC=20?= =?UTF-8?q?=EB=B2=BD=EC=B2=B4=EA=B0=80=20=EA=B0=84=EC=84=AD=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=EB=B2=BD=EC=B2=B4=20?= =?UTF-8?q?=EB=B0=B0=EB=A9=B4=20=EB=8B=A8=EC=B0=A8=EB=A5=BC=20=EB=82=98?= =?UTF-8?q?=EB=88=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 벽체를 전면부(t −0.5 … 1.6, 교좌면 단차까지)와 배면 단차부(t −0.8 … −0.5, 브라켓 상면까지)로 나눔. 배면 단차 위가 접속슬래브 받침 (흉벽 상단 − 0.85) - 흉벽은 교좌면 단차 위(t −0.5 … 0)에서 흉벽 상단까지. 벽체와 겹치지 않음 - 배면은 연직 단차 없이 같은 면(−0.8, −0.5)을 따라 올라가고, 바뀌는 곳은 수평 단차 1개 - 테스트: 벽체와 흉벽 사이 공통 체적 0, 벽체 배면 위치. 전체 636건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시 Co-Authored-By: Claude Haiku 5.5 --- src/view/abut-scene.ts | 34 +++++++++++++++++++++++----------- tests/abut-blocks-slab.test.ts | 13 +++++++++++++ tests/abut-scene.test.ts | 6 +++--- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/view/abut-scene.ts b/src/view/abut-scene.ts index 776e6e8..9829e66 100644 --- a/src/view/abut-scene.ts +++ b/src/view/abut-scene.ts @@ -133,25 +133,37 @@ export function buildAbutGroup( const footingTop = members.footingTopEl; const footingBottom = members.footingBottomEl; - // 벽체: one segment per 교좌면 step, from the footing top to the 교좌면, t −0.8 … +1.6. + // 벽체 (spec §4.3, 도면 type-1): the front part (t −0.5 … 1.6) stops at the 교좌면 step; the back ledge (t −0.8 … −0.5) + // rises to the 브라켓 상면 (흉벽 상단 − 0.85), where the 접속슬래브 sits. The two parts only touch the 흉벽 faces. + const parapetTopAt = (s: number) => members.parapetTopStartEl + ((members.parapetTopEndEl - members.parapetTopStartEl) * s) / L; + const bracketAt = (s: number) => parapetTopAt(s) - members.shape.bracketHeightM; for (const seg of members.wallSegments) { - const profile: [number, number][] = [ + const front: [number, number][] = [ [seg.sFrom, footingTop], [seg.sTo, footingTop], [seg.sTo, seg.seatEl], [seg.sFrom, seg.seatEl], ]; - add('wall', prism(profile, members.shape.wallFrontM - members.shape.wallBackM, new THREE.Vector3(0, 0, members.shape.wallBackM), ex, ey, ez), 0xb7c7d3); + add('wall', prism(front, members.shape.wallFrontM - members.shape.parapetBackM, new THREE.Vector3(0, 0, members.shape.parapetBackM), ex, ey, ez), 0xb7c7d3); + const ledge: [number, number][] = [ + [seg.sFrom, footingTop], + [seg.sTo, footingTop], + [seg.sTo, bracketAt(seg.sTo)], + [seg.sFrom, bracketAt(seg.sFrom)], + ]; + add('wall', prism(ledge, members.shape.parapetBackM - members.shape.wallBackM, new THREE.Vector3(0, 0, members.shape.wallBackM), ex, ey, ez), 0xb7c7d3); } - // 흉벽: t −0.5 … 0, from the footing top up to the parapet top line. - const parapetProfile: [number, number][] = [ - [0, footingTop], - [L, footingTop], - [L, members.parapetTopEndEl], - [0, members.parapetTopStartEl], - ]; - add('parapet', prism(parapetProfile, -members.shape.parapetBackM, new THREE.Vector3(0, 0, members.shape.parapetBackM), ex, ey, ez), 0x8898aa); + // 흉벽: t −0.5 … 0, from the 교좌면 step (the top of the front wall) up to the parapet top line, so it never overlaps the wall. + for (const seg of members.wallSegments) { + const parapet: [number, number][] = [ + [seg.sFrom, seg.seatEl], + [seg.sTo, seg.seatEl], + [seg.sTo, parapetTopAt(seg.sTo)], + [seg.sFrom, parapetTopAt(seg.sFrom)], + ]; + add('parapet', prism(parapet, -members.shape.parapetBackM, new THREE.Vector3(0, 0, members.shape.parapetBackM), ex, ey, ez), 0x8898aa); + } if (footingBottom !== undefined) { // 저판: s 0 … L, t −1.8 … +2.8, from the 기초 하단 to the 저판 상면. diff --git a/tests/abut-blocks-slab.test.ts b/tests/abut-blocks-slab.test.ts index f243edc..b67e8b7 100644 --- a/tests/abut-blocks-slab.test.ts +++ b/tests/abut-blocks-slab.test.ts @@ -55,4 +55,17 @@ describe('교대 3D: 받침블럭과 접속슬래브 (spec §4.2, §4.8)', () => expect(box.min.z).toBeCloseTo(members.shape.parapetBackM - 0.005 - 7, 2); expect(box.max.y - box.min.y).toBeGreaterThan(0.45); }); + + it('the 흉벽 and the 벽체 only touch (no shared volume) and the back ledge is vertical-step free', () => { + const volume = (a: THREE.Object3D, b: THREE.Object3D) => { + const box = new THREE.Box3().setFromObject(a).intersect(new THREE.Box3().setFromObject(b)); + return Math.max(0, box.max.x - box.min.x) * Math.max(0, box.max.y - box.min.y) * Math.max(0, box.max.z - box.min.z); + }; + for (const w of (named('wall') as THREE.Group).children) { + for (const p of (named('parapet') as THREE.Group).children) expect(volume(w, p)).toBeLessThan(1e-9); + } + // back face: the ledge (t −0.8 … −0.5) and the 흉벽 back (−0.5) meet at one horizontal edge, not a vertical offset + const backs = (named('wall') as THREE.Group).children.map((w) => new THREE.Box3().setFromObject(w).min.z); + expect(Math.min(...backs)).toBeCloseTo(members.shape.wallBackM, 6); + }); }); diff --git a/tests/abut-scene.test.ts b/tests/abut-scene.test.ts index ea2d300..545d69c 100644 --- a/tests/abut-scene.test.ts +++ b/tests/abut-scene.test.ts @@ -21,9 +21,9 @@ const group = buildAbutGroup(model, members, wings); const count = (name: string) => (group.getObjectByName(name) as THREE.Group | undefined)?.children.length ?? 0; describe('교대 A1 3D 장면 (AA3·AA3b·AA3c)', () => { - it('has the members: 5 wall segments, 1 parapet, 1 footing, 18 piles, 2 wings, 4 haunches', () => { - expect(count('wall')).toBe(5); - expect(count('parapet')).toBe(1); + it('has the members: 5 wall segments (front + back ledge), 5 parapet parts, 1 footing, 18 piles, 2 wings, 4 haunches', () => { + expect(count('wall')).toBe(10); // 5 front parts + 5 back ledges (도면 type-1) + expect(count('parapet')).toBe(5); // one per 교좌면 step, from the step up expect(count('footing')).toBe(1); expect(count('piles')).toBe(18); expect(count('wings')).toBe(2);