From e5134cdb68cf13af5d1db86d9c7fb4bd8ee71cf1 Mon Sep 17 00:00:00 2001 From: ypshim Date: Tue, 3 Nov 2026 17:16:06 +0900 Subject: [PATCH] =?UTF-8?q?fix(abut):=20=EB=82=A0=EA=B0=9C=EB=B2=BD=20?= =?UTF-8?q?=ED=97=8C=EC=B9=98=EB=A5=BC=20=EB=82=B4=EC=B8=A1=EB=A9=B4=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20=EC=9E=AC=EB=B0=B0?= =?UTF-8?q?=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 뒷굽 헌치 삭제 - 연직 헌치 2개(흉벽 배면, 벽체 배면)를 날개벽 내측면 모서리에 평면 삼각형 기둥으로 둠 - 저판 상면 헌치 추가: 저판 상면과 날개벽 내측면 사이의 수평 헌치 (벽체 배면 → 뒷굽 구간, 다리 0.3 m) - 테스트 갱신 (헌치 6개, 저판 상면 헌치 범위). 전체 639건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시 Co-Authored-By: Claude Haiku 5.5 --- src/core/abut/wings.ts | 32 ++++++++++++-------------------- src/view/abut-scene.ts | 14 +++++++++----- tests/abut-scene.test.ts | 4 ++-- tests/abut-wings.test.ts | 28 +++++++++++++++++----------- 4 files changed, 40 insertions(+), 38 deletions(-) diff --git a/src/core/abut/wings.ts b/src/core/abut/wings.ts index 375000f..fd1da01 100644 --- a/src/core/abut/wings.ts +++ b/src/core/abut/wings.ts @@ -41,10 +41,10 @@ export interface WingSolid { /** 헌치 300 × 300 (위 헌치, 흉벽 배면 모서리). 평면 삼각형과 높이 범위. */ haunches: Haunch[]; /** - * 뒷굽 헌치 (삽도, 날개벽과 확대기초): (t, EL) 직각삼각형. 꼭짓점은 뒷굽 끝 H, 날개벽 하면을 따라 0.3 m, 뒷굽 면을 따라 아래로 0.3 m. - * 날개벽 두께(0.45 m)만큼 s 방향으로 민다. 단축형 날개벽에는 없다. + * 저판 상면 헌치 (수평, 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 (뒷굽). */ - heelHaunch?: [number, number][]; + footingHaunch?: { dMFrom: number; dMTo: number; el: number }; /** 수평 단 (QA-12, spec_abut §4.7): 등단면 / 변화단면1 / 변화단면2. 경계는 단부 높이 1.5 m와 교좌면 EL. */ bands: WingBand[]; } @@ -130,23 +130,15 @@ export function buildWings(model: AbutModel, members: AbutMembers, forms?: { lef zTopEl: kind === 'upper' ? topAt(d) : bracketTop, }; }; - const haunches = [haunchAt('upper', members.shape.parapetBackM)]; + // 연직 헌치 (2026-11-03, 내측면 모서리): upper at the 흉벽 배면, lower at the 벽체 배면. + const haunches = [haunchAt('upper', members.shape.parapetBackM), haunchAt('lower', members.shape.wallBackM)]; - // 뒷굽 헌치 (삽도): at the heel H = (footing back, footing top). The 300 legs run along the wing underside and down the 뒷굽 face. - const tOf = (dM: number) => (members.shape.footingBackM * dM) / heelDistanceM; - const underside = bottomLine.find((p) => p.dM > heelDistanceM + 1e-9); - const heelHaunch: [number, number][] | undefined = - underside === undefined - ? undefined - : (() => { - const H: [number, number] = [members.shape.footingBackM, footingTop]; - const ux = tOf(underside.dM) - H[0]; - const uy = underside.el - H[1]; - const len = Math.hypot(ux, uy); - const A: [number, number] = [H[0] + (0.3 * ux) / len, H[1] + (0.3 * uy) / len]; - const B: [number, number] = [H[0], H[1] - 0.3]; - return [H, A, B]; - })(); + // 저판 상면 헌치 (수평): from the 벽체 배면 to the 뒷굽, on the 저판 상면 next to the 날개벽 내측면. + const footingHaunch = { + dMFrom: Math.abs(members.shape.wallBackM) / k, + dMTo: heelDistanceM, + el: footingTop, + }; // 수평 단 (QA-12): 등단면 = 끝 상단 − 1.5 m 위, 변화단면1 = 그 아래 ~ 교좌면 EL, 변화단면2 = 교좌면 EL ~ 저판 상면. // 교좌면 EL은 날개벽 끝에 가장 가까운 받침의 교좌면이다. @@ -172,7 +164,7 @@ export function buildWings(model: AbutModel, members: AbutMembers, forms?: { lef topLine, bottomLine, haunches, - heelHaunch, + footingHaunch, bands, }; }; diff --git a/src/view/abut-scene.ts b/src/view/abut-scene.ts index 8c7fe9a..5c84b99 100644 --- a/src/view/abut-scene.ts +++ b/src/view/abut-scene.ts @@ -8,7 +8,7 @@ import * as THREE from 'three'; import type { AbutModel, AbutPoint } from '../core/abut/geometry'; import { buildMembers, type AbutMembers } from '../core/abut/members'; -import { buildWings, WING_THICKNESS_M, type WingSolid } from '../core/abut/wings'; +import { buildWings, HAUNCH_LEG_M, WING_THICKNESS_M, type WingSolid } from '../core/abut/wings'; import type { TerrainGrid } from '../core/ground-model'; import { terrainGeometry } from './terrain'; @@ -284,10 +284,14 @@ 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); - // 뒷굽 헌치 (삽도): (t, EL) triangle, extruded across the wing thickness along s. - if (wing.heelHaunch) { - const profile = wing.heelHaunch; - add('haunches', prism(profile, WING_THICKNESS_M, new THREE.Vector3(sEnd, 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) { + const f = wing.footingHaunch; + const leg = HAUNCH_LEG_M; + const triangle: [number, number][] = [[0, 0], [leg, 0], [0, leg]]; + 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) => { diff --git a/tests/abut-scene.test.ts b/tests/abut-scene.test.ts index 3c97686..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, 4 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(4); + 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 dde2676..9b269e2 100644 --- a/tests/abut-wings.test.ts +++ b/tests/abut-wings.test.ts @@ -117,24 +117,30 @@ describe('교대 날개벽 하단 선 · 헌치 (AA3c, spec_abut §4.7)', () => expect(line[2].el).toBeCloseTo(45.214, 2); }); - it('the upper 300 × 300 haunch sits at the 흉벽 배면 (legs 0.3 m); the 뒷굽 haunch is a (t, EL) triangle with 0.3 m legs at the heel', () => { - expect(a2Left.haunches.map((h) => h.kind)).toEqual(['upper']); - const tri = a2Left.heelHaunch!; - expect(tri).toHaveLength(3); - expect(tri[0][0]).toBeCloseTo(-1.8, 6); // 뒷굽 끝 (t) - expect(Math.hypot(tri[1][0] - tri[0][0], tri[1][1] - tri[0][1])).toBeCloseTo(0.3, 6); - expect(tri[2][1]).toBeCloseTo(tri[0][1] - 0.3, 6); // leg down the 뒷굽 face - const [c0, c1, c2] = a2Left.haunches[0].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('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); + expect(f.dMTo).toBeGreaterThan(f.dMFrom); + expect((a2Left as { heelHaunch?: unknown }).heelHaunch).toBeUndefined(); }); it('upper haunch runs from the bracket top (흉벽 상단 − 0.85) to the wing top', () => { - const [up] = a2Left.haunches; + 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); }); });