From a756fd11ce3ffb9c705fa43e89420c8e9e20f77b Mon Sep 17 00:00:00 2001 From: ypshim Date: Tue, 3 Nov 2026 17:40:54 +0900 Subject: [PATCH] =?UTF-8?q?feat(abut):=20=EC=A0=80=ED=8C=90=20=ED=8F=89?= =?UTF-8?q?=EB=A9=B4=EC=9D=84=20skew=20=ED=8F=89=ED=96=89=EC=82=AC?= =?UTF-8?q?=EB=B3=80=ED=98=95=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 저판 양 끝을 날개벽 바깥 선(s = sEnd + (ws/wt)·t)을 따라 잘라, 평면이 직사각형이 아니라 skew 평행사변형이 되도록 함 - 날개벽 하면이 저판 가장자리와 맞닿음 - 테스트: 저판 뒷면 모서리가 날개벽 바깥 선 위에 있음. 전체 641건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시 Co-Authored-By: Claude Haiku 5.5 --- src/view/abut-scene.ts | 11 +++++++---- tests/abut-blocks-slab.test.ts | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/view/abut-scene.ts b/src/view/abut-scene.ts index 5c84b99..75a1494 100644 --- a/src/view/abut-scene.ts +++ b/src/view/abut-scene.ts @@ -199,11 +199,14 @@ export function buildAbutGroup( if (footingBottom !== undefined) { // 저판: s 0 … L, t −1.8 … +2.8, from the 기초 하단 to the 저판 상면. + // Skew (2026-11-03): the 저판 ends follow the wing outer lines (s = sEnd + (ws/wt)·t), so the plan is a parallelogram. + const skewAxis = wingDirectionAxis(model); + const skewK = skewAxis.ws / skewAxis.wt; const footProfile: [number, number][] = [ - [0, members.shape.footingBackM], - [L, members.shape.footingBackM], - [L, members.shape.footingFrontM], - [0, members.shape.footingFrontM], + [skewK * members.shape.footingBackM, members.shape.footingBackM], + [L + skewK * members.shape.footingBackM, members.shape.footingBackM], + [L + skewK * members.shape.footingFrontM, members.shape.footingFrontM], + [skewK * members.shape.footingFrontM, members.shape.footingFrontM], ]; add('footing', prism(footProfile, members.shape.footingThicknessM, new THREE.Vector3(0, footingBottom, 0), ex, ez, ey), 0x5a6a78); } diff --git a/tests/abut-blocks-slab.test.ts b/tests/abut-blocks-slab.test.ts index 73f8de7..fbf4461 100644 --- a/tests/abut-blocks-slab.test.ts +++ b/tests/abut-blocks-slab.test.ts @@ -126,4 +126,23 @@ describe('교대 3D: 받침블럭과 접속슬래브 (spec §4.2, §4.8)', () => } } }); + + it('the 저판 plan is a skew parallelogram: its back corners sit on the wing outer lines, so the 날개벽 sits flush on the 저판 edge', () => { + const axis = (p: { e: number; n: number }) => { + const d = { e: p.e - model.ip1.e, n: p.n - model.ip1.n }; + const f = { e: model.nLeft.e * model.frontSide, n: model.nLeft.n * model.frontSide }; + return { s: d.e * model.u.e + d.n * model.u.n, t: d.e * f.e + d.n * f.n }; + }; + const p0 = axis(wings[0].corners[0]); + const p1 = axis(wings[0].corners[1]); + const k = (p1.s - p0.s) / (p1.t - p0.t); // s per m of t along the wing + const pos = ((named('footing') as THREE.Group).children[0] as THREE.Mesh).geometry.getAttribute('position'); + const back = [...Array(pos.count).keys()].map((i) => ({ x: pos.getX(i), z: pos.getZ(i) })).filter((p) => Math.abs(p.z - members.shape.footingBackM) < 1e-6); + expect(back.length).toBeGreaterThanOrEqual(2); + for (const p of back) { + const onLeft = Math.abs(p.x - k * p.z) < 1e-6; + const onRight = Math.abs(p.x - (model.length + k * p.z)) < 1e-6; + expect(onLeft || onRight).toBe(true); + } + }); });