From 3aa7cc06b65a268b0c552647b4d9818f1665bdd8 Mon Sep 17 00:00:00 2001 From: ypshim Date: Tue, 3 Nov 2026 16:42:10 +0900 Subject: [PATCH] =?UTF-8?q?fix(abut):=20=EB=82=A0=EA=B0=9C=EB=B2=BD?= =?UTF-8?q?=EC=9D=84=20=EC=82=BD=EB=8F=84=EB=8C=80=EB=A1=9C=20=ED=9D=89?= =?UTF-8?q?=EB=B2=BD=C2=B7=EB=B2=BD=EC=B2=B4=20=EB=B0=B0=EB=A9=B4=EC=97=90?= =?UTF-8?q?=20=EB=A7=9E=EB=8B=BF=EA=B2=8C=20=EB=82=98=EB=88=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 날개벽을 상·하 두 부분으로 나눔. 브라켓 상면 위는 흉벽 배면(−0.5)에, 아래는 벽체 배면(−0.8)에 맞닿음 (삽도 A-A와 평면) - 브라켓 상면이 s 방향으로 기울어 있으므로, 두께 0.45 m 안에서 가장 높은 브라켓 상면을 기준으로 자름 - 헌치는 기존 위치 유지: 위 헌치는 흉벽 배면 모서리, 아래 헌치는 벽체 배면 모서리 - 테스트 갱신: 날개벽 상부·하부 경계, 날개벽 개수 4. 전체 638건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시 Co-Authored-By: Claude Haiku 5.5 --- src/view/abut-scene.ts | 32 +++++++++++++++++++++++++++++--- tests/abut-blocks-slab.test.ts | 12 ++++++++++-- tests/abut-scene.test.ts | 2 +- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/view/abut-scene.ts b/src/view/abut-scene.ts index 91047f5..98e46d6 100644 --- a/src/view/abut-scene.ts +++ b/src/view/abut-scene.ts @@ -79,6 +79,23 @@ function toAxis(model: AbutModel, p: AbutPoint): { s: number; t: number } { return { s: d.e * model.u.e + d.n * model.u.n, t: d.e * front.e + d.n * front.n }; } +/** Keeps the part of a 2-D profile where g(x, y) ≥ 0 (Sutherland–Hodgman; g is linear, so the cut is a straight line). */ +function clipByLine(poly: [number, number][], g: (x: number, y: number) => number): [number, number][] { + const out: [number, number][] = []; + for (let i = 0; i < poly.length; i++) { + const p = poly[i]; + const q = poly[(i + 1) % poly.length]; + const gp = g(p[0], p[1]); + const gq = g(q[0], q[1]); + if (gp >= 0) out.push(p); + if (gp * gq < 0) { + const f = gp / (gp - gq); + out.push([p[0] + (q[0] - p[0]) * f, p[1] + (q[1] - p[1]) * f]); + } + } + return out; +} + /** Keeps the part of a 2-D profile with x ≥ x0 (Sutherland–Hodgman against one line). */ function clipBelowX(poly: [number, number][], x0: number): [number, number][] { const out: [number, number][] = []; @@ -255,9 +272,18 @@ export function buildAbutGroup( ...[...wing.bottomLine].reverse().map((p) => [p.dM, p.el] as [number, number]), ]; const ax = new THREE.Vector3(dir.ws, 0, dir.wt); - // The wing starts behind the 벽체 back face (t = 벽체 배면 −0.8): the part in front of it lies inside the 벽체 and 흉벽, so it is cut off. - const geom = prism(clipBelowX(profile, members.shape.wallBackM / dir.wt), 0.45, new THREE.Vector3(sEnd, 0, 0), ax, ey, new THREE.Vector3(a, 0, 0)); - add('wings', geom, 0xc9a77c); + // 삽도 (A-A, 평면): above the 브라켓 상면 the wing meets the 흉벽 back face (t = −0.5); below it, the wing meets the 벽체 back face (t = −0.8). + // The part in front of those faces lies inside the 벽체 or 흉벽, so it is cut off. The bracket falls along s, so the cut follows it. + // The wing is 0.45 thick along s, so its footprint spans two s values at each dM; the cut uses the end where the 브라켓 is highest. + const slopeS = (members.parapetTopEndEl - members.parapetTopStartEl) / L; + const highEnd = slopeS < 0 ? Math.min(0, a * WING_THICKNESS_M) : Math.max(0, a * WING_THICKNESS_M); + const bracketAtDm = (dM: number) => members.parapetTopStartEl + slopeS * (sEnd + dir.ws * dM + highEnd) - members.shape.bracketHeightM; + const above = clipByLine(profile, (x, y) => y - bracketAtDm(x)); + const below = clipByLine(profile, (x, y) => bracketAtDm(x) - y); + const upper = prism(clipBelowX(above, members.shape.parapetBackM / dir.wt), WING_THICKNESS_M, new THREE.Vector3(sEnd, 0, 0), ax, ey, new THREE.Vector3(a, 0, 0)); + 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); for (const h of wing.haunches) { const shape: [number, number][] = h.corners.map((c) => { const p = toAxis(model, c); diff --git a/tests/abut-blocks-slab.test.ts b/tests/abut-blocks-slab.test.ts index 06d26db..d6a337c 100644 --- a/tests/abut-blocks-slab.test.ts +++ b/tests/abut-blocks-slab.test.ts @@ -69,7 +69,7 @@ describe('교대 3D: 받침블럭과 접속슬래브 (spec §4.2, §4.8)', () => expect(Math.min(...backs)).toBeCloseTo(members.shape.wallBackM, 6); }); - it('the 날개벽 stays behind the 벽체 back face, so it does not enter the 벽체 or the 흉벽', () => { + it('the 날개벽 meets the 흉벽 back face above the 브라켓 and the 벽체 back face below it, and never enters them', () => { // All 벽체 and 흉벽 parts lie at t ≥ 벽체 배면 (−0.8). A wing vertex with t ≤ −0.8 therefore has no volume in common with them. const z = (g: THREE.Object3D) => { const pos = (g as THREE.Mesh).geometry.getAttribute('position'); @@ -77,7 +77,15 @@ describe('교대 3D: 받침블럭과 접속슬래브 (spec §4.2, §4.8)', () => for (let i = 0; i < pos.count; i++) max = Math.max(max, pos.getZ(i)); return max; }; - for (const w of (named('wings') as THREE.Group).children) expect(z(w)).toBeLessThanOrEqual(members.shape.wallBackM + 1e-6); + // Upper part (above the 브라켓 상면) meets the 흉벽 back face (−0.5); the lower part meets the 벽체 back face (−0.8). + const backTop = (s: number) => members.parapetTopStartEl + ((members.parapetTopEndEl - members.parapetTopStartEl) * s) / model.length - members.shape.bracketHeightM; + for (const w of (named('wings') as THREE.Group).children) { + expect(z(w)).toBeLessThanOrEqual(members.shape.parapetBackM + 1e-6); + const pos = (w as THREE.Mesh).geometry.getAttribute('position'); + for (let i = 0; i < pos.count; i++) { + if (pos.getZ(i) > members.shape.wallBackM + 1e-6) expect(pos.getY(i)).toBeGreaterThanOrEqual(backTop(pos.getX(i)) - 1e-6); + } + } for (const w of (named('wall') as THREE.Group).children) expect(new THREE.Box3().setFromObject(w).min.z).toBeGreaterThanOrEqual(members.shape.wallBackM - 1e-6); }); diff --git a/tests/abut-scene.test.ts b/tests/abut-scene.test.ts index 316288d..3c97686 100644 --- a/tests/abut-scene.test.ts +++ b/tests/abut-scene.test.ts @@ -26,7 +26,7 @@ describe('교대 A1 3D 장면 (AA3·AA3b·AA3c)', () => { 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(2); + expect(count('wings')).toBe(4); // upper and lower part per wing expect(count('haunches')).toBe(4); });