diff --git a/src/view/abut-scene.ts b/src/view/abut-scene.ts index 5532a16..776e6e8 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, type WingSolid } from '../core/abut/wings'; +import { buildWings, WING_THICKNESS_M, type WingSolid } from '../core/abut/wings'; import type { TerrainGrid } from '../core/ground-model'; import { terrainGeometry } from './terrain'; @@ -19,15 +19,16 @@ const SLAB_THICKNESS_M = 0.45; const SLAB_LENGTH_M = 7; /** 접속슬래브 하향 경사 (배면 쪽으로), spec_abut §4.8 "하향 4 %". */ const SLAB_FALL = 0.04; +/** 접속슬래브와 날개벽 안쪽면 사이 간격 (m), spec_abut §4.8 "날개벽 20". */ +const SLAB_CLEARANCE_M = 0.02; /** Eight-corner slab with its top sloping away from the wall. Indexed by (s end, t end, bottom/top). Double-sided: the winding is not critical. */ -function slabGeometry(s0: number, s1: number, tNear: number, tFar: number, bottomEl: (s: number, t: number) => number): THREE.BufferGeometry { - const corners = [s0, s1].flatMap((s) => [tNear, tFar].flatMap((t) => [bottomEl(s, t), bottomEl(s, t) + SLAB_THICKNESS_M].map((y) => [s, y, t]))); - // index = is * 4 + it * 2 + ih (ih 0 = bottom, 1 = top) +function slabGeometry(plan: [number, number][], bottomEl: (s: number, t: number) => number): THREE.BufferGeometry { + // plan: four (s, t) corners in order; vertices 0–3 are the bottom, 4–7 the top. + const corners = [...plan.map(([s, t]) => [s, bottomEl(s, t), t]), ...plan.map(([s, t]) => [s, bottomEl(s, t) + SLAB_THICKNESS_M, t])]; const quads = [ - [0, 2, 3, 1], [4, 5, 7, 6], // s0 and s1 ends - [0, 4, 6, 2], [1, 3, 7, 5], // bottom and top - [0, 1, 5, 4], [2, 6, 7, 3], // near and far faces (t ends) + [0, 1, 2, 3], [4, 5, 6, 7], // bottom and top + [0, 1, 5, 4], [1, 2, 6, 5], [2, 3, 7, 6], [3, 0, 4, 7], // sides ]; const positions: number[] = []; const indices: number[] = []; @@ -173,12 +174,29 @@ export function buildAbutGroup( } // 접속슬래브 (spec §4.8): bottom on the 브라켓 top (흉벽 상단 − 0.85), 5 mm behind the 흉벽 back face, 7 m back, falling 4 % away from the wall. - // Centred on the axis over the wing-to-wing width (slabWidthMm); the 흉벽 slope along s is followed. - const slabW = (model.wing?.slabWidthMm ?? L * 1000) / 1000; + // Plan: between the wing inner faces, each 20 mm inside (spec §4.8 "측면 = 날개벽 안쪽면 + 20"), so it never meets a wing. + // The wing is drawn as a prism over the wing direction with its 0.45 m thickness along s, so its inner face at t is s = sEnd + a·0.45 + (ws/wt)·t. const slabNear = members.shape.parapetBackM - 0.005; + const slabFar = slabNear - SLAB_LENGTH_M; + const innerS = (sEnd: number, t: number): number => { + const w = wings.find((x) => x.sEnd === sEnd); + if (!w) return sEnd; // no wing on this side: the slab ends at the 교좌면 끝선 + const a = w.sEnd === 0 ? 1 : -1; + const p0 = toAxis(model, w.corners[0]); + const p1 = toAxis(model, w.corners[1]); + return sEnd + a * (WING_THICKNESS_M + SLAB_CLEARANCE_M) + ((p1.s - p0.s) / (p1.t - p0.t)) * t; + }; const bracketTopEl = (s: number) => members.parapetTopStartEl + ((members.parapetTopEndEl - members.parapetTopStartEl) * s) / L - members.shape.bracketHeightM; const slabMesh = new THREE.Mesh( - slabGeometry((L - slabW) / 2, (L + slabW) / 2, slabNear, slabNear - SLAB_LENGTH_M, (s, t) => bracketTopEl(s) + SLAB_FALL * (t - slabNear)), + slabGeometry( + [ + [innerS(0, slabNear), slabNear], + [innerS(L, slabNear), slabNear], + [innerS(L, slabFar), slabFar], + [innerS(0, slabFar), slabFar], + ], + (s, t) => bracketTopEl(s) + SLAB_FALL * (t - slabNear), + ), new THREE.MeshStandardMaterial({ color: 0xd8c8a0, roughness: 0.9, side: THREE.DoubleSide }), ); slabMesh.name = 'slab'; diff --git a/tests/abut-blocks-slab.test.ts b/tests/abut-blocks-slab.test.ts index 4f40e9e..f243edc 100644 --- a/tests/abut-blocks-slab.test.ts +++ b/tests/abut-blocks-slab.test.ts @@ -14,7 +14,8 @@ const rec = parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abu const input = toAbutInput(rec); const model = buildAbut(input); const members = buildMembers(model, { bottomEl: input.foundationBottomEl, pileLengthMm: input.pileLengthMm }); -const group = buildAbutGroup(model, members, buildWings(model, members)); +const wings = buildWings(model, members); +const group = buildAbutGroup(model, members, wings); const named = (name: string) => group.getObjectByName(name) as THREE.Group | undefined; @@ -23,12 +24,30 @@ describe('교대 3D: 받침블럭과 접속슬래브 (spec §4.2, §4.8)', () => expect(named('blocks')?.children).toHaveLength(2 * model.bearings.length); }); - it('draws the 접속슬래브 as one slab over the wing-to-wing width', () => { - const slab = named('slab'); - expect(slab?.children).toHaveLength(1); - const box = new THREE.Box3().setFromObject(slab as THREE.Group); - const width = box.max.x - box.min.x; - expect(width).toBeCloseTo((model.wing?.slabWidthMm ?? 0) / 1000, 2); + it('draws the 접속슬래브 as one slab', () => { + expect(named('slab')?.children).toHaveLength(1); + }); + + it('the slab does not overlap either wing in plan (spec §4.8: 날개벽 안쪽면 + 20)', () => { + // Axis frame: s along IP1 → IP3, t positive to the 전면, as toAxis() in abut-scene.ts. + 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 inside = (x: number, y: number, poly: { s: number; t: number }[]) => { + let c = false; + for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) { + if (poly[i].t > y !== poly[j].t > y && x < ((poly[j].s - poly[i].s) * (y - poly[i].t)) / (poly[j].t - poly[i].t) + poly[i].s) c = !c; + } + return c; + }; + const pos = (named('slab') as THREE.Group).children[0] as THREE.Mesh; + const attr = pos.geometry.getAttribute('position'); + const polys = wings.map((w) => w.corners.map(axis)); + for (let i = 0; i < attr.count; i++) { + for (const poly of polys) expect(inside(attr.getX(i), attr.getZ(i), poly)).toBe(false); + } }); it('the slab reaches 7 m behind the 흉벽 back face and is 450 thick', () => {