//! OcctKernel — geometry backend using OpenCASCADE Technology (OCCT). //! //! Only compiled with `--features occt` (ADR-001, ADR-002 optional feature). //! //! Produces proper B-rep solids: //! - Accurate PSC-I cross-section with haunches //! - Higher-quality tessellation (BRepMesh_IncrementalMesh) //! - Foundation for fillets, boolean ops in later sprints #[cfg(feature = "occt")] mod inner { use glam::DVec3; use opencascade::{ mesh::Mesher, workplane::Workplane, }; use cimery_ir::{ AbutmentIR, BearingIR, DeckSlabIR, GirderIR, PierIR, PscISectionParams, SectionParams, }; use crate::{KernelError, Mesh}; // ── Girder (PSC-I B-rep sweep) ────────────────────────────────────────────── pub fn girder_mesh(ir: &GirderIR) -> Result { if ir.span_m() <= 0.0 { return Err(KernelError::InvalidInput( format!("span must be positive, got {} m", ir.span_m()), )); } match &ir.section { SectionParams::PscI(p) => psc_i_extrude(p, ir.span_mm()), // Other sections: fall back to PureRustKernel until implemented _ => { log::warn!( "OcctKernel: section {:?} not yet implemented, using PureRust fallback", ir.section_type ); crate::psc_i::build_psc_i_mesh( match &ir.section { SectionParams::PscI(p) => p, _ => unreachable!(), }, ir.span_mm(), ) } } } fn psc_i_extrude(p: &PscISectionParams, span_mm: f64) -> Result { let hw = p.top_flange_width / 2.0; let hbw = p.bottom_flange_width / 2.0; let hwb = p.web_thickness / 2.0; let h = p.total_height; let tft = p.top_flange_thickness; let bft = p.bottom_flange_thickness; let hch = p.haunch; // PSC-I 14-vertex profile in XY plane (mm), CCW let wire = Workplane::xy() .sketch() .move_to(-hbw, 0.0 ) .line_to( hbw, 0.0 ) .line_to( hbw, bft ) .line_to( hwb, bft ) .line_to( hwb, h - tft - hch ) .line_to( hwb + hch, h - tft ) .line_to( hw, h - tft ) .line_to( hw, h ) .line_to(-hw, h ) .line_to(-hw, h - tft ) .line_to(-(hwb + hch), h - tft ) .line_to(-hwb, h - tft - hch ) .line_to(-hwb, bft ) .line_to(-hbw, bft ) .close() .to_face(); // Extrude along Z axis by span_mm let solid = wire.extrude(DVec3::new(0.0, 0.0, span_mm)); // Tessellate with engineering-grade tolerance (1 mm) let shape = solid.into(); let occt_mesh = Mesher::try_new(&shape, 1.0) .map_err(|e| KernelError::Computation(format!("OCCT mesher init: {e}")))? .mesh() .map_err(|e| KernelError::Computation(format!("OCCT tessellation: {e}")))?; occt_mesh_to_cimery(occt_mesh) } // ── Other features (use PureRustKernel geometry for now) ───────────────────── pub fn deck_slab_mesh(ir: &DeckSlabIR) -> Result { crate::deck_slab::build_deck_slab_mesh(ir) } pub fn bearing_mesh(ir: &BearingIR) -> Result { crate::bearing::build_bearing_mesh(ir) } pub fn pier_mesh(ir: &PierIR) -> Result { crate::pier::build_pier_mesh(ir) } pub fn abutment_mesh(ir: &AbutmentIR) -> Result { crate::abutment::build_abutment_mesh(ir) } // ── Conversion ──────────────────────────────────────────────────────────────── fn occt_mesh_to_cimery(m: opencascade::mesh::Mesh) -> Result { if m.vertices.is_empty() { return Err(KernelError::Computation( "OCCT tessellation produced no vertices".into(), )); } let vertices: Vec<[f32; 3]> = m.vertices.iter() .map(|v| [v.x as f32, v.y as f32, v.z as f32]) .collect(); // normals array may be shorter (known OCCT API quirk, see mesh.rs) let normals: Vec<[f32; 3]> = if m.normals.len() == vertices.len() { m.normals.iter() .map(|n| { let len = (n.x*n.x + n.y*n.y + n.z*n.z).sqrt(); if len < 1e-10 { [0.0, 1.0, 0.0] } else { [(n.x/len) as f32, (n.y/len) as f32, (n.z/len) as f32] } }) .collect() } else { // Fallback: flat normals per vertex (less ideal but correct) vec![[0.0_f32, 1.0, 0.0]; vertices.len()] }; let indices: Vec = m.indices.iter() .map(|&i| i as u32) .collect(); let colors = vec![crate::COLOR_CONCRETE; vertices.len()]; Ok(Mesh { vertices, normals, indices, colors }) } } // ── Public struct (only when feature is active) ──────────────────────────────── #[cfg(feature = "occt")] pub use self::occt_kernel::OcctKernel; #[cfg(feature = "occt")] mod occt_kernel { use cimery_ir::{AbutmentIR, BearingIR, DeckSlabIR, GirderIR, PierIR}; use crate::{GeomKernel, KernelError, Mesh}; use super::inner; /// Full B-rep geometry backend using OpenCASCADE Technology. /// Enabled with `--features occt`. See ADR-001. pub struct OcctKernel; impl GeomKernel for OcctKernel { fn girder_mesh(&self, ir: &GirderIR) -> Result { inner::girder_mesh(ir) } fn deck_slab_mesh(&self, ir: &DeckSlabIR) -> Result { inner::deck_slab_mesh(ir) } fn bearing_mesh(&self, ir: &BearingIR) -> Result { inner::bearing_mesh(ir) } fn pier_mesh(&self, ir: &PierIR) -> Result { inner::pier_mesh(ir) } fn abutment_mesh(&self, ir: &AbutmentIR) -> Result { inner::abutment_mesh(ir) } } }