diff --git a/PROGRESS.md b/PROGRESS.md index e48b8ac..6f3972d 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -12,6 +12,7 @@ ## 타임라인 ### 2026-04-14 +- code — OcctKernel 구현 완료 (`--features occt`). PSC-I B-rep sweep + BRepMesh 테셀레이션. 빌드 확인. - code — cimery Sprint 1 구현 완료. 8 crates (core/ir/dsl/kernel/incremental/evaluator/usd/viewer), `cargo test --workspace` 32개 전부 통과. DSL→IR→salsa-style-db→evaluator→StubKernel→USD 파이프라인 검증. - meta — Revit API 가이드 Output/guides/revit-api-guide.md 추가됨. - meta — PLAN.md · PROGRESS.md 도입. 에이전트 간 작업 조정 프로토콜 확립. diff --git a/cimery/crates/kernel/Cargo.toml b/cimery/crates/kernel/Cargo.toml index 7871f16..1de4967 100644 --- a/cimery/crates/kernel/Cargo.toml +++ b/cimery/crates/kernel/Cargo.toml @@ -7,7 +7,7 @@ edition.workspace = true # Enable the full OpenCASCADE kernel backend. # Requires OCCT installed/compiled — see cimery/CLAUDE.md for setup. # Build: cargo build -p cimery-kernel --features occt -occt = ["dep:opencascade"] +occt = ["dep:opencascade", "dep:glam"] [dependencies] cimery-core = { workspace = true } @@ -16,6 +16,7 @@ thiserror = { workspace = true } log = { workspace = true } # opencascade is OPTIONAL — only compiled with --features occt opencascade = { git = "https://github.com/bschwind/opencascade-rs", optional = true } +glam = { version = "0.24", optional = true } # must match opencascade-rs glam version [dev-dependencies] cimery-core = { workspace = true } diff --git a/cimery/crates/kernel/src/lib.rs b/cimery/crates/kernel/src/lib.rs index 0e165f9..6d8effc 100644 --- a/cimery/crates/kernel/src/lib.rs +++ b/cimery/crates/kernel/src/lib.rs @@ -16,6 +16,10 @@ pub mod deck_slab; pub mod bearing; pub mod pier; pub mod abutment; +pub mod occt; + +#[cfg(feature = "occt")] +pub use occt::OcctKernel; use cimery_ir::{ AbutmentIR, BearingIR, DeckSlabIR, GirderIR, PierIR, SectionParams, diff --git a/cimery/crates/kernel/src/occt.rs b/cimery/crates/kernel/src/occt.rs new file mode 100644 index 0000000..b4ec817 --- /dev/null +++ b/cimery/crates/kernel/src/occt.rs @@ -0,0 +1,180 @@ +//! 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(); + + Ok(Mesh { vertices, normals, indices }) + } +} + +// ── 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) + } + } +}