bearing.rs: X 중심, Y 하방향 (-h to 0) 기하학 수정 bridge_scene.rs: 받침 X 오프셋 제거 (girder 정렬) Mesh: colors 필드 추가 + recolor() 메서드 sweep.rs / occt.rs: 기본 콘크리트 색 자동 채움 bridge_scene: 부재별 색상 (거더/슬래브/받침/교대) shader.wgsl: base_color 입력 → 조명 계산에 적용 선택(Selection) 기능은 계획대로 별도 Sprint에 구현. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
147 lines
6.7 KiB
Rust
147 lines
6.7 KiB
Rust
//! Full bridge scene compositor — Sprint 4.
|
||
//!
|
||
//! Builds a single merged mesh for a simple 40 m single-span PSC-I girder bridge:
|
||
//! - 5 × PSC-I Girder (2500 mm c/c)
|
||
//! - 1 × Deck Slab (12000 mm wide)
|
||
//! - 10 × Elastomeric Bearing (5 per abutment)
|
||
//! - 2 × Abutment (start & end)
|
||
//!
|
||
//! Positions are in the same coordinate space as the girder mesh:
|
||
//! X = transverse (right = +), Y = vertical (up = +), Z = along span.
|
||
|
||
use cimery_core::{AbutmentType, BearingType, MaterialGrade, SectionType};
|
||
use cimery_ir::{
|
||
AbutmentIR, BearingIR, DeckSlabIR, FeatureId, GirderIR,
|
||
PscISectionParams, SectionParams, WingWallIR,
|
||
};
|
||
use cimery_kernel::{GeomKernel, KernelError, Mesh};
|
||
|
||
// ── Part colours (linear sRGB) ──────────────────────────────────────────────
|
||
pub const COL_GIRDER: [f32; 3] = [0.85, 0.82, 0.72]; // light concrete
|
||
pub const COL_DECK: [f32; 3] = [0.72, 0.70, 0.62]; // slightly darker slab
|
||
pub const COL_BEARING: [f32; 3] = [0.30, 0.30, 0.35]; // dark rubber/steel
|
||
pub const COL_ABUTMENT: [f32; 3] = [0.65, 0.60, 0.50]; // brown concrete
|
||
|
||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||
|
||
fn translate(mut mesh: Mesh, dx: f32, dy: f32, dz: f32) -> Mesh {
|
||
for v in &mut mesh.vertices {
|
||
v[0] += dx;
|
||
v[1] += dy;
|
||
v[2] += dz;
|
||
}
|
||
mesh
|
||
}
|
||
|
||
fn merge(meshes: Vec<Mesh>) -> Mesh {
|
||
cimery_kernel::sweep::merge_meshes(meshes)
|
||
}
|
||
|
||
// ─── Scene builder ────────────────────────────────────────────────────────────
|
||
|
||
/// Build a complete bridge scene mesh using the provided kernel.
|
||
pub fn build_bridge_scene<K: GeomKernel>(kernel: &K) -> Result<Mesh, KernelError> {
|
||
const SPAN_M: f64 = 40.0;
|
||
const SPAN_MM: f32 = 40_000.0;
|
||
const N_GIRDERS: usize = 5;
|
||
const SPACING: f32 = 2_500.0; // mm c/c
|
||
const GIRDER_H: f32 = 1_800.0; // mm
|
||
const BEARING_H: f32 = 60.0; // mm
|
||
|
||
let mut parts: Vec<Mesh> = Vec::new();
|
||
|
||
// ── Girders ────────────────────────────────────────────────────────────────
|
||
for i in 0..N_GIRDERS {
|
||
let x = (i as f32 - (N_GIRDERS as f32 - 1.0) * 0.5) * SPACING;
|
||
let ir = GirderIR {
|
||
id: FeatureId::new(),
|
||
station_start: 0.0,
|
||
station_end: SPAN_M,
|
||
offset_from_alignment: x as f64,
|
||
section_type: SectionType::PscI,
|
||
section: SectionParams::PscI(PscISectionParams::kds_standard()),
|
||
count: 1,
|
||
spacing: 0.0,
|
||
material: MaterialGrade::C50,
|
||
};
|
||
let mut mesh = kernel.girder_mesh(&ir)?;
|
||
mesh.recolor(COL_GIRDER);
|
||
parts.push(translate(mesh, x, 0.0, 0.0));
|
||
}
|
||
|
||
// ── Deck Slab ──────────────────────────────────────────────────────────────
|
||
// KDS: min 220 mm, width = (N-1)*spacing + 2 × cantilever
|
||
let half_width = ((N_GIRDERS as f32 - 1.0) * SPACING) * 0.5 + 1_000.0; // 1 m cantilever
|
||
let deck_ir = DeckSlabIR {
|
||
id: FeatureId::new(),
|
||
station_start: 0.0,
|
||
station_end: SPAN_M,
|
||
width_left: half_width as f64,
|
||
width_right: half_width as f64,
|
||
thickness: 220.0,
|
||
haunch_depth: 0.0,
|
||
cross_slope: 2.0,
|
||
material: MaterialGrade::C40,
|
||
};
|
||
let mut deck_mesh = kernel.deck_slab_mesh(&deck_ir)?;
|
||
deck_mesh.recolor(COL_DECK);
|
||
parts.push(translate(deck_mesh, 0.0, GIRDER_H + 220.0, 0.0));
|
||
|
||
// ── Bearings ───────────────────────────────────────────────────────────────
|
||
// 5 per abutment, one under each girder
|
||
for &z in &[0.0_f32, SPAN_MM] {
|
||
for i in 0..N_GIRDERS {
|
||
let x = (i as f32 - (N_GIRDERS as f32 - 1.0) * 0.5) * SPACING;
|
||
let bearing_ir = BearingIR {
|
||
id: FeatureId::new(),
|
||
station: if z < 1.0 { 0.0 } else { SPAN_M },
|
||
bearing_type: BearingType::Elastomeric,
|
||
plan_length: 350.0,
|
||
plan_width: 450.0,
|
||
total_height: BEARING_H as f64,
|
||
capacity_vertical: 1_500.0,
|
||
};
|
||
let mut mesh = kernel.bearing_mesh(&bearing_ir)?;
|
||
mesh.recolor(COL_BEARING);
|
||
parts.push(translate(mesh, x, 0.0, z - 225.0));
|
||
}
|
||
}
|
||
|
||
// ── Abutments ──────────────────────────────────────────────────────────────
|
||
let wing = WingWallIR { length: 5_000.0, height: 2_500.0, thickness: 500.0 };
|
||
let total_w = (N_GIRDERS as f64 - 1.0) * SPACING as f64 + 3_000.0; // incl. overhangs
|
||
|
||
for &(station, z) in &[(0.0f64, -800.0_f32), (SPAN_M, SPAN_MM)] {
|
||
let abut_ir = AbutmentIR {
|
||
id: FeatureId::new(),
|
||
station,
|
||
skew_angle: 0.0,
|
||
abutment_type: AbutmentType::ReverseT,
|
||
breast_wall_height: (GIRDER_H + BEARING_H) as f64,
|
||
breast_wall_thickness: 800.0,
|
||
breast_wall_width: total_w,
|
||
footing_length: 4_000.0,
|
||
footing_width: total_w + 1_000.0,
|
||
footing_thickness: 1_000.0,
|
||
wing_wall_left: wing.clone(),
|
||
wing_wall_right: wing.clone(),
|
||
material: MaterialGrade::C40,
|
||
};
|
||
let mut mesh = kernel.abutment_mesh(&abut_ir)?;
|
||
mesh.recolor(COL_ABUTMENT);
|
||
let y = -(BEARING_H + abut_ir.breast_wall_height as f32);
|
||
parts.push(translate(mesh, 0.0, y, z));
|
||
}
|
||
|
||
Ok(merge(parts))
|
||
}
|
||
|
||
/// Bounding box of the full bridge scene (for camera setup).
|
||
pub fn scene_extents() -> ([f32; 3], [f32; 3]) {
|
||
const SPAN_MM: f32 = 40_000.0;
|
||
const HALF_W: f32 = 6_500.0;
|
||
const TOP_Y: f32 = 2_020.0; // top of slab
|
||
const BOT_Y: f32 = -3_000.0; // footing bottom approx
|
||
([-HALF_W, BOT_Y, -2_000.0], [HALF_W, TOP_Y, SPAN_MM + 2_000.0])
|
||
}
|