fix(abut): 접속슬래브를 날개벽 안쪽면 사이에 두어 간섭 제거
- 슬래브 평면을 날개벽 안쪽면에서 20 mm 안쪽으로 잡음 (spec §4.8 "측면 = 날개벽 안쪽면 + 20"). 날개벽은 축 방향 프리즘(두께 0.45 m, s 방향)으로 그리므로 안쪽면 s = sEnd + a·0.45 + (ws/wt)·t - 배면 끝은 흉벽 배면 −0.505에서 7 m (t 기준) - 간섭 검사 테스트: 슬래브 꼭짓점이 어느 날개벽 평면 영역에도 들어가지 않음 - 테스트 635건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8f75c6dc9f
commit
fbc315fdba
2 files changed
+54
-17
No files matched your search
+28
-10
@@ -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';
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in new issue
Block a user