From 804bd5c8f8a7df88453009dee918ed296b5cdf5d Mon Sep 17 00:00:00 2001 From: ypshim Date: Tue, 3 Nov 2026 17:32:21 +0900 Subject: [PATCH] =?UTF-8?q?Revert=20"Reapply=20"fix(abut):=20=EB=82=A0?= =?UTF-8?q?=EA=B0=9C=EB=B2=BD=EC=9D=98=20=EC=97=B0=EC=A7=81=20=ED=97=8C?= =?UTF-8?q?=EC=B9=98=20=EC=82=AD=EC=A0=9C""?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 61ef0010348db1389de0111cd9f6afce3a00935c. --- src/core/abut/wings.ts | 33 +++++++++++++++++++++++++++++++++ src/view/abut-scene.ts | 9 +++++++++ tests/abut-blocks-slab.test.ts | 7 ++++++- tests/abut-scene.test.ts | 4 ++-- tests/abut-wings.test.ts | 18 ++++++++++++++++++ 5 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/core/abut/wings.ts b/src/core/abut/wings.ts index da73ddf..fd1da01 100644 --- a/src/core/abut/wings.ts +++ b/src/core/abut/wings.ts @@ -38,6 +38,8 @@ export interface WingSolid { topLine: { dM: number; el: number }[]; /** 하단선: 저판 상면 → 뒷굽 끝 → 단부 하단 (일반형), 또는 저판 상면 평탄 (단축형). */ bottomLine: { dM: number; el: number }[]; + /** 헌치 300 × 300 (위 헌치, 흉벽 배면 모서리). 평면 삼각형과 높이 범위. */ + haunches: Haunch[]; /** * 저판 상면 헌치 (수평, 2026-11-03): between the 저판 상면 and the 날개벽 내측면. The triangle lies in the plane EL–s, * with legs HAUNCH_LEG_M along the 저판 상면 and along the 내측면; it runs along the wing from dMFrom (벽체 배면) to dMTo (뒷굽). @@ -54,6 +56,14 @@ export interface WingBand { zBottomEl: number; } +export interface Haunch { + kind: 'upper' | 'lower'; + /** 평면 삼각형 꼭짓점 (E, N): 모서리, 날개벽 쪽 점, 벽체·흉벽 쪽 점. */ + corners: [AbutPoint, AbutPoint, AbutPoint]; + zBottomEl: number; + zTopEl: number; +} + export const HAUNCH_LEG_M = 0.3; export function buildWings(model: AbutModel, members: AbutMembers, forms?: { left: WingForm; right: WingForm }): WingSolid[] { @@ -101,6 +111,28 @@ export function buildWings(model: AbutModel, members: AbutMembers, forms?: { lef { dM: WING_LENGTH_M, el: form === 'general' ? bottomEnd : footingTop }, ]; + // 헌치: 위 헌치는 흉벽 배면(−0.5), 아래 헌치는 벽체 배면(−0.8)의 모서리에 둔다. + const pointAt = (d: number) => ({ e: p0.e + w.e * d, n: p0.n + w.n * d }); + const shift = (q: AbutPoint, v: AbutPoint, f: number) => ({ e: q.e + v.e * f, n: q.n + v.n * f }); + const bracketTop = topStart - members.shape.bracketHeightM; + // The 브라켓 상면 falls along s (the 흉벽 top slopes), so the upper haunch rests on the highest point of its own footprint. + const sAt = (q: AbutPoint) => (q.e - model.ip1.e) * model.u.e + (q.n - model.ip1.n) * model.u.n; + const backTopAt = (s: number) => members.parapetTopStartEl + ((members.parapetTopEndEl - members.parapetTopStartEl) * s) / model.length - members.shape.bracketHeightM; + const haunchAt = (kind: 'upper' | 'lower', t: number): Haunch => { + const d = Math.abs(t) / k; + const c = pointAt(d); + const inwardUnit = { e: model.u.e * a, n: model.u.n * a }; + const corners: [AbutPoint, AbutPoint, AbutPoint] = [c, shift(c, w, HAUNCH_LEG_M), shift(c, inwardUnit, HAUNCH_LEG_M)]; + return { + kind, + corners, + zBottomEl: kind === 'upper' ? Math.max(...corners.map((q) => backTopAt(sAt(q)))) : footingTop, + zTopEl: kind === 'upper' ? topAt(d) : bracketTop, + }; + }; + // 연직 헌치 (2026-11-03, 내측면 모서리): upper at the 흉벽 배면, lower at the 벽체 배면. + const haunches = [haunchAt('upper', members.shape.parapetBackM), haunchAt('lower', members.shape.wallBackM)]; + // 저판 상면 헌치 (수평): from the 벽체 배면 to the 뒷굽, on the 저판 상면 next to the 날개벽 내측면. const footingHaunch = { dMFrom: Math.abs(members.shape.wallBackM) / k, @@ -131,6 +163,7 @@ export function buildWings(model: AbutModel, members: AbutMembers, forms?: { lef heelDistanceM, topLine, bottomLine, + haunches, footingHaunch, bands, }; diff --git a/src/view/abut-scene.ts b/src/view/abut-scene.ts index 94d8f41..5c84b99 100644 --- a/src/view/abut-scene.ts +++ b/src/view/abut-scene.ts @@ -293,6 +293,15 @@ export function buildAbutGroup( const origin = new THREE.Vector3(sEnd + a * WING_THICKNESS_M + dir.ws * f.dMFrom, f.el, dir.wt * f.dMFrom); add('haunches', prism(triangle, f.dMTo - f.dMFrom, origin, new THREE.Vector3(a, 0, 0), ey, new THREE.Vector3(dir.ws, 0, dir.wt)), 0xc9a77c); } + for (const h of wing.haunches) { + const shape: [number, number][] = h.corners.map((c) => { + const p = toAxis(model, c); + return [p.s, p.t]; + }); + const zBottom = h.zBottomEl; + const height = h.zTopEl - h.zBottomEl; + if (height > 0) add('haunches', prism(shape, height, new THREE.Vector3(0, zBottom, 0), ex, ez, ey), 0xc9a77c); + } } // 지형: when the ground model's terrain grid covers the abutment footprint, the terrain is drawn (the same frame as the diff --git a/tests/abut-blocks-slab.test.ts b/tests/abut-blocks-slab.test.ts index a7d5841..d6a337c 100644 --- a/tests/abut-blocks-slab.test.ts +++ b/tests/abut-blocks-slab.test.ts @@ -89,8 +89,13 @@ describe('교대 3D: 받침블럭과 접속슬래브 (spec §4.2, §4.8)', () => for (const w of (named('wall') as THREE.Group).children) expect(new THREE.Box3().setFromObject(w).min.z).toBeGreaterThanOrEqual(members.shape.wallBackM - 1e-6); }); - it('the 접속슬래브 stays on the 브라켓 상면 over the back strip, not in it', () => { + it('the upper 헌치 and the 접속슬래브 stay on the 브라켓 상면 over the back strip, 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) continue; // lower 헌치, behind the 벽체 + expect(b.min.y).toBeGreaterThanOrEqual(Math.max(backTop(b.min.x), backTop(b.max.x)) - 1e-6); + } const pos = ((named('slab') as THREE.Group).children[0] as THREE.Mesh).geometry.getAttribute('position'); for (let i = 0; i < pos.count; i++) { if (pos.getZ(i) >= members.shape.wallBackM) expect(pos.getY(i)).toBeGreaterThanOrEqual(backTop(pos.getX(i)) - 1e-6); diff --git a/tests/abut-scene.test.ts b/tests/abut-scene.test.ts index e26ad5c..8fff2b3 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, 6 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(6); // per wing: upper and lower 연직 헌치, and the 저판 상면 헌치 }); it('spans from the pile tip (19.295 m) up to the highest parapet end (47.18 m)', () => { diff --git a/tests/abut-wings.test.ts b/tests/abut-wings.test.ts index c7481e9..9b269e2 100644 --- a/tests/abut-wings.test.ts +++ b/tests/abut-wings.test.ts @@ -117,6 +117,15 @@ describe('교대 날개벽 하단 선 · 헌치 (AA3c, spec_abut §4.7)', () => expect(line[2].el).toBeCloseTo(45.214, 2); }); + it('two vertical 300 × 300 haunches at the 내측면 corners: upper at the 흉벽 배면, lower at the 벽체 배면', () => { + expect(a2Left.haunches.map((h) => h.kind)).toEqual(['upper', 'lower']); + for (const h of a2Left.haunches) { + const [c0, c1, c2] = h.corners; + expect(Math.hypot(c1.e - c0.e, c1.n - c0.n)).toBeCloseTo(0.3, 6); + expect(Math.hypot(c2.e - c0.e, c2.n - c0.n)).toBeCloseTo(0.3, 6); + } + }); + it('the 저판 상면 haunch runs from the 벽체 배면 to the 뒷굽 on the 저판 상면; the 뒷굽 haunch is gone', () => { const f = a2Left.footingHaunch!; expect(f.el).toBeCloseTo(41.995, 3); @@ -124,6 +133,15 @@ describe('교대 날개벽 하단 선 · 헌치 (AA3c, spec_abut §4.7)', () => expect((a2Left as { heelHaunch?: unknown }).heelHaunch).toBeUndefined(); }); + it('upper haunch runs from the bracket top (흉벽 상단 − 0.85) to the wing top', () => { + const [up, low] = a2Left.haunches; + // The upper haunch rests on the highest point of the 브라켓 top over its footprint (the top falls along s), never below it. + expect(up.zBottomEl).toBeGreaterThanOrEqual(46.838 - 0.85 - 1e-6); + expect(up.zBottomEl).toBeLessThan(46.838 - 0.85 + 0.05); + expect(up.zTopEl).toBeGreaterThan(up.zBottomEl); + expect(low.zBottomEl).toBeCloseTo(41.995, 3); + expect(low.zTopEl).toBeCloseTo(46.838 - 0.85, 2); + }); }); describe('날개벽 수평 단 (QA-12 초안 적용, spec_abut §4.7)', () => {