fix(abut-rebar): 날개벽 철근이 콘크리트 밖으로 나가는 곳 수정
- WR12 (전면 등단면) 아래 갈고리: 전면이 저판 끝면과 같은 면이라 바깥으로 꺾으면 저판 밖 → 안쪽으로 - WR7-1 · 7-2 · 7-3 · WR10 날개벽 다리: 뒷굽 끝 너머 경사 하단이 올라오는 곳에서 c 안쪽까지만 (최대 l_d) - WR20: 갈고리까지 날개벽 안인 단만 - 테스트: 교대 A1 모든 3D 철근을 ~100 mm 간격으로 찍어 콘크리트 안인지 확인 (P10은 슬래브 쪽 절반만 밖) Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e3c9ad0953
commit
81fe1f1549
3 files changed
+179
-6
No files matched your search
@@ -0,0 +1,87 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import * as THREE from 'three';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildAbut } from '../src/core/abut/geometry';
|
||||
import { parseAbutments, toAbutInput } from '../src/core/abut/parse';
|
||||
import { buildMembers } from '../src/core/abut/members';
|
||||
import { buildWings } from '../src/core/abut/wings';
|
||||
import { resolveSlab } from '../src/core/abut/slab';
|
||||
import { abutFootingGeom, abutFootingRebar } from '../src/core/abut/rebar/footing';
|
||||
import { skewAngle } from '../src/core/abut/rebar/skew-layout';
|
||||
import { abutWallGeom, abutWallRebar } from '../src/core/abut/rebar/wall';
|
||||
import { abutParapetGeom, abutParapetRebar, slabSpanAt } from '../src/core/abut/rebar/parapet';
|
||||
import { abutWingGeom, abutWingRebar } from '../src/core/abut/rebar/wing';
|
||||
import { abutPlanOutline, abutSkewK, buildAbutGroup } from '../src/view/abut-scene';
|
||||
import { buildAbutRebar3D } from '../src/view/abut-rebar3d';
|
||||
|
||||
// 교대 A1: 모든 3D 철근이 콘크리트(저판 · 벽체 · 흉벽 · 브라켓 · 날개벽 · 헌치) 안에 있는지 (2026-11-04, 화면에서 날개벽 WR12 갈고리 ·
|
||||
// WR7-3 · WR10 다리가 밖으로 나간 것을 고친 뒤). 철근 축을 ~100 mm 간격으로 찍고 메시 안팎은 광선 교차 수 홀짝으로 본다.
|
||||
// P10 다웰은 접속슬래브로 300 올라가므로 위 절반만 밖에 있어도 된다.
|
||||
const res: Record<string, { out: number; total: number }> = {};
|
||||
{
|
||||
const xlsx = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url));
|
||||
const rec = parseAbutments(XLSX.read(readFileSync(xlsx), { type: 'buffer' })).abutments[0];
|
||||
const model = buildAbut(toAbutInput(rec));
|
||||
const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm });
|
||||
const wings = buildWings(model, members);
|
||||
const angle = skewAngle(rec.skewDeg);
|
||||
const fg = abutFootingGeom(model.length, members.shape, angle);
|
||||
const footing = abutFootingRebar(fg);
|
||||
const wg = abutWallGeom(model.length, members, angle);
|
||||
const wall = abutWallRebar(wg, footing);
|
||||
const slab = resolveSlab();
|
||||
const k = abutSkewK(model);
|
||||
const tM = (wg.xb / 1000 + members.shape.parapetBackM) / 2;
|
||||
const span = slabSpanAt(abutPlanOutline(model, members, wings, slab).slab, tM, k)!;
|
||||
const pg = abutParapetGeom(wg, members.shape, model.bearings[0].s, { B: model.wing!.slabWidthMm, a: span.a, b: span.b, nearT: members.shape.parapetBackM * 1000 - slab.parapetGapMm, chamferA: 300, chamferB: 300 });
|
||||
const para = abutParapetRebar(pg, wall);
|
||||
const wingR = wings.map((w) => abutWingRebar(abutWingGeom(model, members, w, footing.z.B68)));
|
||||
const rebar = buildAbutRebar3D(fg, footing, k, members.shape.footingBackM, members.footingBottomEl!, wall, para, wingR);
|
||||
const group = buildAbutGroup(model, members, wings);
|
||||
group.updateMatrixWorld(true);
|
||||
const solids: THREE.Mesh[] = [];
|
||||
group.traverse((o) => {
|
||||
if (o instanceof THREE.Mesh && !['ground', 'slab', 'blinding', 'piles'].some((n) => o.parent?.name === n || o.name === n)) solids.push(o);
|
||||
});
|
||||
const ray = new THREE.Raycaster();
|
||||
const inside = (p: THREE.Vector3) =>
|
||||
solids.some((m) => {
|
||||
ray.set(p, new THREE.Vector3(0.577, 0.577, 0.577));
|
||||
return ray.intersectObject(m, false).length % 2 === 1;
|
||||
});
|
||||
|
||||
for (const [name, meshes] of rebar.byName) {
|
||||
for (const mesh of meshes) {
|
||||
const geo = mesh.geometry as THREE.CylinderGeometry;
|
||||
geo.computeBoundingBox();
|
||||
// Cylinder axis: the two cap centres = bbox centre ± half length along its direction; sample the bbox diagonal's centre line instead.
|
||||
const pos = geo.getAttribute('position');
|
||||
const a = new THREE.Vector3(pos.getX(0), pos.getY(0), pos.getZ(0));
|
||||
const b = new THREE.Vector3(pos.getX(pos.count - 1), pos.getY(pos.count - 1), pos.getZ(pos.count - 1));
|
||||
const n = Math.max(2, Math.ceil(a.distanceTo(b) / 0.1));
|
||||
const r = (res[name] ??= { out: 0, total: 0 });
|
||||
for (let i = 0; i <= n; i++) {
|
||||
const p = a.clone().lerp(b, i / n);
|
||||
r.total++;
|
||||
if (!inside(p)) {
|
||||
r.out++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('교대 A1 3D 철근이 콘크리트 안에 있음', () => {
|
||||
it('저판 · 벽체 · 흉벽 · 날개벽 철근은 밖으로 나간 점이 없음 (P10 제외)', () => {
|
||||
const outside = Object.entries(res).filter(([name, v]) => name !== 'P10' && v.out > 0).map(([name, v]) => `${name} ${v.out}/${v.total}`);
|
||||
expect(outside).toEqual([]);
|
||||
expect(Object.keys(res).length).toBeGreaterThan(30);
|
||||
});
|
||||
|
||||
it('P10 다웰: 슬래브 쪽 절반만 밖 (브라켓 안 300은 콘크리트 안)', () => {
|
||||
expect(res.P10.out / res.P10.total).toBeLessThan(0.6);
|
||||
expect(res.P10.out / res.P10.total).toBeGreaterThan(0.4);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user