feat(abut): 교대 3D에 받침 본체 · 솔플레이트 (교각과 같은 개념)
교대 3D는 받침블럭 콘크리트와 몰탈패드만 그렸다. ② 받침 카드와 같은 솔플레이트(abutSoles)로 교각처럼 그린다. - 받침 본체: 솔플레이트 평면에서 사방 100 mm 뺀 상자, 높이 h3, 패드 상면에서 - 솔플레이트: L은 거더 방향(블럭 회전 = 방위각, 적용한 계산 방위각 포함), W는 그 직각, T1은 오르막 쪽 - 거더 하면 고정: T ≠ T_ref이면 블럭 상면 · 패드가 (T − T_ref)만큼 내려감 (받침 ELEVATION TABLE과 일치) - 부재 표시: '받침·받침블럭'에 교대 bearings, '솔플레이트'에 교대 solplates 연결 - 테스트: A1(짝 P1) 본체 높이 245 · 솔플 상면 z + 245 + 38, 두꺼운 끝이 오르막(A1) 쪽, S −3 % → 패드 −4 mm · 상면 유지, soles 없이 부를 때 기존 형상. 브라우저: 교대만 · 교각+교대 가져오기 예외 0, 받침 쌓임 확인 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c7acfccd51
commit
1fda705b75
3 files changed
+113
-6
No files matched your search
+9
-3
@@ -64,6 +64,7 @@ import {
|
||||
abutElevationRows,
|
||||
abutSolplateRows,
|
||||
resolveBearingRule,
|
||||
abutSoles,
|
||||
} from './core/abut/bearing-table';
|
||||
import { abutMemberRows, ABUT_MEMBER_EDITABLE, type AbutMemberId } from './core/abut/member-list';
|
||||
import type { AbutModel, WingForm } from './core/abut/geometry';
|
||||
@@ -147,8 +148,8 @@ type Stage = 'shape' | 'plan' | 'rebar' | 'quantity';
|
||||
|
||||
/** 부재 표시 toggles (2026-11-03). `pier` parts follow the pier scenes; `abut` names follow the abutment groups (abut-scene.ts). */
|
||||
const PART_GROUPS: { label: string; pier: PartName[]; abut: string[] }[] = [
|
||||
{ label: '받침·받침블럭', pier: ['blocks', 'pads', 'bearings'], abut: ['blocks'] },
|
||||
{ label: '솔플레이트', pier: ['solplates'], abut: [] },
|
||||
{ label: '받침·받침블럭', pier: ['blocks', 'pads', 'bearings'], abut: ['blocks', 'bearings'] },
|
||||
{ label: '솔플레이트', pier: ['solplates'], abut: ['solplates'] },
|
||||
{ label: '코핑', pier: ['coping'], abut: [] },
|
||||
{ label: '기둥', pier: ['columns'], abut: [] },
|
||||
{ label: '확대기초', pier: ['footings'], abut: ['footing'] },
|
||||
@@ -659,7 +660,12 @@ function setupScene(): void {
|
||||
try {
|
||||
const model = buildAbut(toAbutInput(rec));
|
||||
const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, rec.shape);
|
||||
const group = buildAbutGroup(model, members, buildWings(model, members, rec.wings, rec.wingDims), pivot, rec.groundEl, groundModel?.terrainGrid, rec.slab);
|
||||
// 받침 본체 · 솔플레이트 (2026-11-06): the same solplates as the ② 받침 카드 (짝 받침 S, T, 거더 하면 고정).
|
||||
const soles = abutSoles(model, resolveBearingRule(rec.bearingRule), rec.bearingHeightMm, {
|
||||
pairs: state.bridgePairs, slopeI: rec.slopeI,
|
||||
inputAzimuthDeg: Object.fromEntries(rec.bearings.filter((x) => x.azimuthDeg !== undefined).map((x) => [String(x.no), x.azimuthDeg!])),
|
||||
});
|
||||
const group = buildAbutGroup(model, members, buildWings(model, members, rec.wings, rec.wingDims), pivot, rec.groundEl, groundModel?.terrainGrid, rec.slab, soles);
|
||||
group.visible = abutShown(state.stage, state.abutActive, rec.name);
|
||||
// The 부재 표시 toggles hold for a new abutment group too.
|
||||
for (const [name, on] of Object.entries(state.abutVisible)) {
|
||||
|
||||
+36
-3
@@ -5,6 +5,7 @@
|
||||
* 날개벽 평면(E, N)은 축 기준 (s, t)로 바꿔 쓴다: s = (P − IP1)·u, t = (P − IP1)·(전면 법선).
|
||||
* 흉벽·벽체 단면은 모식도다. 흉벽은 저판 상면에서 상단까지 그린다(교좌면 아래 세부는 배근 단계에서).
|
||||
*/
|
||||
import type { BearingSole } from '../core/sole-plate';
|
||||
import * as THREE from 'three';
|
||||
import type { AbutModel, AbutPoint } from '../core/abut/geometry';
|
||||
import { buildMembers, type AbutMembers } from '../core/abut/members';
|
||||
@@ -13,7 +14,7 @@ import { resolveSlab, slabGradePct, slabLevelAt, slabSection, type AbutSlab } fr
|
||||
import type { TerrainGrid } from '../core/ground-model';
|
||||
import { terrainGeometry } from './terrain';
|
||||
|
||||
export const ABUT_GROUP_NAMES = ['wall', 'parapet', 'footing', 'blinding', 'piles', 'wings', 'haunches', 'ground', 'blocks', 'slab'] as const;
|
||||
export const ABUT_GROUP_NAMES = ['wall', 'parapet', 'footing', 'blinding', 'piles', 'wings', 'haunches', 'ground', 'blocks', 'bearings', 'solplates', 'slab'] as const;
|
||||
/** 말뚝 두부 매입 (m): the drawn pile head goes this far into the 저판 (drawing only). */
|
||||
const PILE_HEAD_EMBED_M = 0.1;
|
||||
|
||||
@@ -100,6 +101,20 @@ function prism(profile: [number, number][], depth: number, origin: THREE.Vector3
|
||||
return geom;
|
||||
}
|
||||
|
||||
/** Solplate wedge in the block frame: W along x, L along z, bottom at y0; thickness T1 at z = side·L/2, T2 at the other end. */
|
||||
function solplateWedge(L: number, W: number, T1: number, T2: number, side: 1 | -1, y0: number): THREE.BufferGeometry {
|
||||
const g = new THREE.BoxGeometry(W, 1, L);
|
||||
const pos = g.getAttribute('position') as THREE.BufferAttribute;
|
||||
for (let i = 0; i < pos.count; i++) {
|
||||
const z = pos.getZ(i), top = pos.getY(i) > 0;
|
||||
const thick = T1 + ((T2 - T1) * (L / 2 - side * z)) / L;
|
||||
pos.setY(i, y0 + (top ? thick : 0));
|
||||
}
|
||||
pos.needsUpdate = true;
|
||||
g.computeVertexNormals();
|
||||
return g;
|
||||
}
|
||||
|
||||
function material(color: number): THREE.MeshStandardMaterial {
|
||||
return new THREE.MeshStandardMaterial({ color, side: THREE.DoubleSide, roughness: 0.85 });
|
||||
}
|
||||
@@ -228,6 +243,8 @@ export function buildAbutGroup(
|
||||
groundEl?: number,
|
||||
terrain?: TerrainGrid,
|
||||
slabInput?: Partial<AbutSlab>,
|
||||
/** Solplate of each bearing (abutSoles, 2026-11-06): the bearing body and the solplate wedge, and the block-top drop. */
|
||||
soles?: BearingSole[],
|
||||
): THREE.Group {
|
||||
const root = new THREE.Group();
|
||||
root.name = model.name;
|
||||
@@ -328,13 +345,29 @@ export function buildAbutGroup(
|
||||
// 받침블럭 (spec §4.2, §6.6): concrete 1200 × 1200 from the 교좌면 to 50 below z, and the 몰탈패드 1000 × 1000 × 50 up to z.
|
||||
// The plan follows the 교대축 (받침 방위각 = 축 방향). t is the 전면 +, so the bearing's lateral offset is taken with the front side.
|
||||
// The block turns by the bearing azimuth (rotDeg, from the 방위각 input) about its own centre.
|
||||
// 교량받침 본체 · 솔플레이트 (2026-11-06, 교각과 같은 개념): the body is the solplate plan less 100 mm each side, height h3,
|
||||
// on the pad; the solplate tapers along the girder (L) with T1 on the 오르막 쪽. 거더 하면 고정: the block top and the pad
|
||||
// drop by T − T_ref (as the 받침 ELEVATION TABLE).
|
||||
const soleOf = new Map((soles ?? []).map((s) => [s.no, s]));
|
||||
for (const b of model.bearings) {
|
||||
const t = model.frontSide * b.lateral;
|
||||
const blockTop = b.z - 0.05;
|
||||
const sole = soleOf.get(b.no);
|
||||
const padTop = b.z - (sole?.dropMm ?? 0) / 1000;
|
||||
const blockTop = padTop - 0.05;
|
||||
const turn = (b.rotDeg * Math.PI) / 180;
|
||||
const placed = (g: THREE.BufferGeometry) => g.rotateY(turn).translate(b.s, 0, t);
|
||||
add('blocks', placed(prism([[-0.6, b.seatEl], [0.6, b.seatEl], [0.6, blockTop], [-0.6, blockTop]], 1.2, new THREE.Vector3(0, 0, -0.6), ex, ey, ez)), 0xa9b4bf);
|
||||
add('blocks', placed(prism([[-0.5, blockTop], [0.5, blockTop], [0.5, b.z], [-0.5, b.z]], 1.0, new THREE.Vector3(0, 0, -0.5), ex, ey, ez)), 0x6e7a86);
|
||||
add('blocks', placed(prism([[-0.5, blockTop], [0.5, blockTop], [0.5, padTop], [-0.5, padTop]], 1.0, new THREE.Vector3(0, 0, -0.5), ex, ey, ez)), 0x6e7a86);
|
||||
if (!sole) continue;
|
||||
const L = sole.L / 1000, W = sole.W / 1000, h3 = sole.h3 / 1000;
|
||||
// Local block frame after rotateY(turn): +z (the girder, L) is (sin turn)·u + (cos turn)·front in (E, N).
|
||||
const front = { e: model.nLeft.e * model.frontSide, n: model.nLeft.n * model.frontSide };
|
||||
const zDir = { e: Math.sin(turn) * model.u.e + Math.cos(turn) * front.e, n: Math.sin(turn) * model.u.n + Math.cos(turn) * front.n };
|
||||
const t1Side = sole.uphill && zDir.e * sole.uphill.e + zDir.n * sole.uphill.n > 0 ? 1 : -1;
|
||||
const body = new THREE.BoxGeometry(W - 0.2, h3, L - 0.2);
|
||||
body.translate(0, padTop + h3 / 2, 0);
|
||||
add('bearings', placed(body), 0x3d4a57);
|
||||
add('solplates', placed(solplateWedge(L, W, sole.T1 / 1000, sole.T2 / 1000, t1Side, padTop + h3)), 0xc08a3e);
|
||||
}
|
||||
|
||||
// 접속슬래브 (spec §4.8, 2026-11-04 매립형 / 노출형): the section (core/abut/slab.ts) is measured from the 흉벽 상단 at each s.
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import * as THREE from 'three';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildAbut } from '../src/core/abut/geometry';
|
||||
import { parseAbutments, toAbutInput } from '../src/core/abut/parse';
|
||||
import { abutSoles, resolveBearingRule } from '../src/core/abut/bearing-table';
|
||||
import { buildAbutGroup } from '../src/view/abut-scene';
|
||||
import { loadInputWorkbook, p1Pairs } from './helpers';
|
||||
|
||||
// 교대 받침 본체 · 솔플레이트 3D (2026-11-06, 교각과 같은 개념). A1 with its P1 pair: S −1.908 %, T 30, h3 245.
|
||||
// Vertex positions are 32-bit floats: at EL 44 m they carry ~1.5 µm, so heights are compared to 5 decimals.
|
||||
const rec = parseAbutments(loadInputWorkbook()).abutments.find((a) => a.name === 'A1')!;
|
||||
const model = buildAbut(toAbutInput(rec));
|
||||
const ctx = { pairs: p1Pairs(), slopeI: rec.slopeI };
|
||||
const soles = abutSoles(model, resolveBearingRule(), rec.bearingHeightMm, ctx);
|
||||
const group = buildAbutGroup(model, undefined, undefined, undefined, undefined, undefined, undefined, soles);
|
||||
const meshes = (name: string) => (group.getObjectByName(name) as THREE.Group).children as THREE.Mesh[];
|
||||
const box = (m: THREE.Object3D) => new THREE.Box3().setFromObject(m);
|
||||
|
||||
describe('교대 받침 본체 · 솔플레이트 3D', () => {
|
||||
it('one bearing body per bearing, height h3 (245), on the pad top (z)', () => {
|
||||
expect(meshes('bearings')).toHaveLength(5);
|
||||
model.bearings.forEach((b, i) => {
|
||||
const bb = box(meshes('bearings')[i]);
|
||||
expect(bb.max.y - bb.min.y).toBeCloseTo(0.245, 5);
|
||||
expect(bb.min.y).toBeCloseTo(b.z, 5);
|
||||
});
|
||||
});
|
||||
|
||||
it('one solplate wedge per bearing: from z + h3 up to T1 (38 mm) at its thick end', () => {
|
||||
expect(meshes('solplates')).toHaveLength(5);
|
||||
model.bearings.forEach((b, i) => {
|
||||
const bb = box(meshes('solplates')[i]);
|
||||
expect(bb.min.y).toBeCloseTo(b.z + 0.245, 5);
|
||||
expect(bb.max.y).toBeCloseTo(b.z + 0.245 + 0.038, 5);
|
||||
});
|
||||
});
|
||||
|
||||
it('T1 sits on the 오르막 쪽: A1 is higher than P1, so the thick end points away from P1 (against the azimuth)', () => {
|
||||
model.bearings.forEach((b, i) => {
|
||||
const pos = meshes('solplates')[i].geometry.getAttribute('position') as THREE.BufferAttribute;
|
||||
const pts = Array.from({ length: pos.count }, (_, k) => new THREE.Vector3(pos.getX(k), pos.getY(k), pos.getZ(k)));
|
||||
const topY = Math.max(...pts.map((p) => p.y));
|
||||
const thick = pts.filter((p) => topY - p.y < 1e-6), thin = pts.filter((p) => p.y > b.z + 0.245 + 1e-6 && topY - p.y > 1e-3);
|
||||
const mid = (ps: THREE.Vector3[]) => ps.reduce((a, p) => a.add(p), new THREE.Vector3()).multiplyScalar(1 / ps.length);
|
||||
const d = mid(thick).sub(mid(thin));
|
||||
// Group without a pivot: local x = s along u, local z = t along the front (nLeft · frontSide).
|
||||
const front = { e: model.nLeft.e * model.frontSide, n: model.nLeft.n * model.frontSide };
|
||||
const dir = { e: d.x * model.u.e + d.z * front.e, n: d.x * model.u.n + d.z * front.n };
|
||||
const az = (b.azimuthDeg! * Math.PI) / 180;
|
||||
expect(dir.e * Math.sin(az) + dir.n * Math.cos(az)).toBeLessThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('거더 하면 고정: S −3 % typed → T 34, the pad and block top drop 4 mm and the solplate top stays', () => {
|
||||
const steep = abutSoles(model, resolveBearingRule({ slopePct: -3 }), rec.bearingHeightMm, ctx);
|
||||
const g = buildAbutGroup(model, undefined, undefined, undefined, undefined, undefined, undefined, steep);
|
||||
const body = box((g.getObjectByName('bearings') as THREE.Group).children[0]);
|
||||
expect(body.min.y).toBeCloseTo(model.bearings[0].z - 0.004, 5);
|
||||
// Centre thickness T: the top at the middle of the wedge is pad top + h3 + T, the same as with T 30.
|
||||
expect(body.min.y + 0.245 + 0.034).toBeCloseTo(model.bearings[0].z + 0.245 + 0.03, 5);
|
||||
});
|
||||
|
||||
it('without soles the group keeps only the block and pad (old callers)', () => {
|
||||
const plain = buildAbutGroup(model);
|
||||
expect(plain.getObjectByName('bearings')).toBeUndefined();
|
||||
expect(plain.getObjectByName('solplates')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user