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);