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>
182 lines
6.8 KiB
Rust
182 lines
6.8 KiB
Rust
//! 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<Mesh, KernelError> {
|
|
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<Mesh, KernelError> {
|
|
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<Mesh, KernelError> {
|
|
crate::deck_slab::build_deck_slab_mesh(ir)
|
|
}
|
|
|
|
pub fn bearing_mesh(ir: &BearingIR) -> Result<Mesh, KernelError> {
|
|
crate::bearing::build_bearing_mesh(ir)
|
|
}
|
|
|
|
pub fn pier_mesh(ir: &PierIR) -> Result<Mesh, KernelError> {
|
|
crate::pier::build_pier_mesh(ir)
|
|
}
|
|
|
|
pub fn abutment_mesh(ir: &AbutmentIR) -> Result<Mesh, KernelError> {
|
|
crate::abutment::build_abutment_mesh(ir)
|
|
}
|
|
|
|
// ── Conversion ────────────────────────────────────────────────────────────────
|
|
|
|
fn occt_mesh_to_cimery(m: opencascade::mesh::Mesh) -> Result<Mesh, KernelError> {
|
|
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<u32> = 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<Mesh, KernelError> {
|
|
inner::girder_mesh(ir)
|
|
}
|
|
fn deck_slab_mesh(&self, ir: &DeckSlabIR) -> Result<Mesh, KernelError> {
|
|
inner::deck_slab_mesh(ir)
|
|
}
|
|
fn bearing_mesh(&self, ir: &BearingIR) -> Result<Mesh, KernelError> {
|
|
inner::bearing_mesh(ir)
|
|
}
|
|
fn pier_mesh(&self, ir: &PierIR) -> Result<Mesh, KernelError> {
|
|
inner::pier_mesh(ir)
|
|
}
|
|
fn abutment_mesh(&self, ir: &AbutmentIR) -> Result<Mesh, KernelError> {
|
|
inner::abutment_mesh(ir)
|
|
}
|
|
}
|
|
}
|