diff --git a/src/view/abut-scene.ts b/src/view/abut-scene.ts index 94d8f41..08aaede 100644 --- a/src/view/abut-scene.ts +++ b/src/view/abut-scene.ts @@ -284,6 +284,20 @@ export function buildAbutGroup( const lower = prism(clipBelowX(below, members.shape.wallBackM / dir.wt), WING_THICKNESS_M, new THREE.Vector3(sEnd, 0, 0), ax, ey, new THREE.Vector3(a, 0, 0)); add('wings', upper, 0xc9a77c); add('wings', lower, 0xc9a77c); + // 흉벽 배면 헌치 (삽도 A-A, 2026-11-03): a right triangle in the plane t–EL where the 흉벽 배면 (t −0.5) meets the 브라켓 상면. + // Legs 0.3 m: up the 흉벽 배면 and back along the 브라켓 상면 to the 벽체 배면 (t −0.8). Extruded 0.3 m from the 날개벽 내측면 toward the centre. + const sInner = sEnd + a * WING_THICKNESS_M + (dir.ws / dir.wt) * members.shape.parapetBackM; + const bracketHigh = Math.max( + members.parapetTopStartEl + ((members.parapetTopEndEl - members.parapetTopStartEl) * sInner) / L - members.shape.bracketHeightM, + members.parapetTopStartEl + ((members.parapetTopEndEl - members.parapetTopStartEl) * (sInner + a * HAUNCH_LEG_M)) / L - members.shape.bracketHeightM, + ); + const ledge: [number, number][] = [ + [members.shape.parapetBackM, bracketHigh], + [members.shape.parapetBackM, bracketHigh + HAUNCH_LEG_M], + [members.shape.wallBackM, bracketHigh], + ]; + add('haunches', prism(ledge, HAUNCH_LEG_M, new THREE.Vector3(sInner, 0, 0), ez, ey, new THREE.Vector3(a, 0, 0)), 0xc9a77c); + // 저판 상면 헌치 (수평, 2026-11-03): a right triangle in the plane EL–s on the 저판 상면, next to the 날개벽 내측면, // extruded along the wing from the 벽체 배면 to the 뒷굽. if (wing.footingHaunch) { diff --git a/tests/abut-blocks-slab.test.ts b/tests/abut-blocks-slab.test.ts index a7d5841..004a0c8 100644 --- a/tests/abut-blocks-slab.test.ts +++ b/tests/abut-blocks-slab.test.ts @@ -96,4 +96,13 @@ describe('교대 3D: 받침블럭과 접속슬래브 (spec §4.2, §4.8)', () => if (pos.getZ(i) >= members.shape.wallBackM) expect(pos.getY(i)).toBeGreaterThanOrEqual(backTop(pos.getX(i)) - 1e-6); } }); + + it('the 흉벽 배면 헌치 sits on the 브라켓 상면 against the 흉벽 back, not in it', () => { + const backTop = (s: number) => members.parapetTopStartEl + ((members.parapetTopEndEl - members.parapetTopStartEl) * s) / model.length - members.shape.bracketHeightM; + for (const h of (named('haunches') as THREE.Group).children) { + const b = new THREE.Box3().setFromObject(h); + if (b.max.z > members.shape.parapetBackM + 1e-6 || b.min.z < members.shape.wallBackM - 1e-6) continue; // not the 흉벽 배면 헌치 + expect(b.min.y).toBeGreaterThanOrEqual(Math.max(backTop(b.min.x), backTop(b.max.x)) - 1e-6); + } + }); }); diff --git a/tests/abut-scene.test.ts b/tests/abut-scene.test.ts index e26ad5c..f037148 100644 --- a/tests/abut-scene.test.ts +++ b/tests/abut-scene.test.ts @@ -21,13 +21,13 @@ 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 blocks, 10 parapet parts (2 per step), 1 footing, 18 piles, 2 wings, 2 haunches (저판 상면)', () => { + it('has the members: 5 wall blocks, 10 parapet parts (2 per step), 1 footing, 18 piles, 2 wings, 4 haunches (흉벽 배면 + 저판 상면)', () => { expect(count('wall')).toBe(5); // one 벽체 block per 교좌면 step (삽도) expect(count('parapet')).toBe(10); // per step: back strip (흉벽 배면 단차) and the 흉벽 (삽도) expect(count('footing')).toBe(1); expect(count('piles')).toBe(18); expect(count('wings')).toBe(4); // upper and lower part per wing - expect(count('haunches')).toBe(2); // per wing: the 저판 상면 헌치 + expect(count('haunches')).toBe(4); // per wing: the 흉벽 배면 헌치 (삽도 A-A) and the 저판 상면 헌치 }); it('spans from the pile tip (19.295 m) up to the highest parapet end (47.18 m)', () => {