diff --git a/src/view/abut-scene.ts b/src/view/abut-scene.ts index 75a1494..6519acf 100644 --- a/src/view/abut-scene.ts +++ b/src/view/abut-scene.ts @@ -163,6 +163,19 @@ export function buildAbutGroup( }; const footingTop = members.footingTopEl; + // Skew (2026-11-03): the 벽체·흉벽 plan follows the wing lines, s = s_axis + k·t, with k = ws/wt of the wing direction. + const skewAxis = wingDirectionAxis(model); + const skewK = skewAxis.ws / skewAxis.wt; + // A prism whose profile (s, EL) is given at the face t0 and which is extruded along t with the skew (λ·(k, 0, 1)). + const skewPrism = (profile: [number, number][], depth: number, t0: number) => + prism( + profile.map(([s, y]) => [s + skewK * t0, y] as [number, number]), + depth, + new THREE.Vector3(0, 0, t0), + ex, + ey, + new THREE.Vector3(skewK, 0, 1), + ); const footingBottom = members.footingBottomEl; // 벽체 (spec §4.3, 삽도): the full 2400 block from the footing top up to the 교좌면 step (t −0.8 … 1.6). @@ -175,7 +188,7 @@ export function buildAbutGroup( [seg.sTo, seg.seatEl], [seg.sFrom, seg.seatEl], ]; - add('wall', prism(block, members.shape.wallFrontM - members.shape.wallBackM, new THREE.Vector3(0, 0, members.shape.wallBackM), ex, ey, ez), 0xb7c7d3); + add('wall', skewPrism(block, members.shape.wallFrontM - members.shape.wallBackM, members.shape.wallBackM), 0xb7c7d3); } // 흉벽 (삽도): sits on the 벽체 top, above the 교좌면 step. The back strip (t −0.8 … −0.5) rises to the 브라켓 상면 (흉벽 상단 − 0.85), @@ -187,21 +200,19 @@ export function buildAbutGroup( [seg.sTo, bracketAt(seg.sTo)], [seg.sFrom, bracketAt(seg.sFrom)], ]; - add('parapet', prism(backStrip, members.shape.parapetBackM - members.shape.wallBackM, new THREE.Vector3(0, 0, members.shape.wallBackM), ex, ey, ez), 0x8898aa); + add('parapet', skewPrism(backStrip, members.shape.parapetBackM - members.shape.wallBackM, members.shape.wallBackM), 0x8898aa); const parapet: [number, number][] = [ [seg.sFrom, seg.seatEl], [seg.sTo, seg.seatEl], [seg.sTo, parapetTopAt(seg.sTo)], [seg.sFrom, parapetTopAt(seg.sFrom)], ]; - add('parapet', prism(parapet, -members.shape.parapetBackM, new THREE.Vector3(0, 0, members.shape.parapetBackM), ex, ey, ez), 0x8898aa); + add('parapet', skewPrism(parapet, -members.shape.parapetBackM, members.shape.parapetBackM), 0x8898aa); } 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][] = [ [skewK * members.shape.footingBackM, members.shape.footingBackM], [L + skewK * members.shape.footingBackM, members.shape.footingBackM], diff --git a/tests/abut-blocks-slab.test.ts b/tests/abut-blocks-slab.test.ts index fbf4461..8f3e171 100644 --- a/tests/abut-blocks-slab.test.ts +++ b/tests/abut-blocks-slab.test.ts @@ -61,9 +61,13 @@ describe('교대 3D: 받침블럭과 접속슬래브 (spec §4.2, §4.8)', () => const box = new THREE.Box3().setFromObject(a).intersect(new THREE.Box3().setFromObject(b)); return Math.max(0, box.max.x - box.min.x) * Math.max(0, box.max.y - box.min.y) * Math.max(0, box.max.z - box.min.z); }; - for (const w of (named('wall') as THREE.Group).children) { - for (const p of (named('parapet') as THREE.Group).children) expect(volume(w, p)).toBeLessThan(1e-9); - } + // Same 교좌면 step only: the skewed prisms of neighbouring steps share a face, and their bounding boxes overlap slightly. + const walls = (named('wall') as THREE.Group).children; + const parapets = (named('parapet') as THREE.Group).children; + walls.forEach((w, k) => { + expect(volume(w, parapets[2 * k])).toBeLessThan(1e-9); + expect(volume(w, parapets[2 * k + 1])).toBeLessThan(1e-9); + }); // back face: the ledge (t −0.8 … −0.5) and the 흉벽 back (−0.5) meet at one horizontal edge, not a vertical offset const backs = (named('wall') as THREE.Group).children.map((w) => new THREE.Box3().setFromObject(w).min.z); expect(Math.min(...backs)).toBeCloseTo(members.shape.wallBackM, 6);