//! 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 { cimery_kernel::sweep::merge_meshes(meshes) } // ─── Scene builder ──────────────────────────────────────────────────────────── /// Build a complete bridge scene mesh using the provided kernel. pub fn build_bridge_scene(kernel: &K) -> Result { 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 = 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]) }