OcctKernel 구현 — PSC-I B-rep sweep + BRepMesh 테셀레이션

- cimery-kernel/src/occt.rs: OcctKernel (--features occt 전용)
  - Workplane::xy().sketch()로 PSC-I 14-vertex 2D 프로파일 생성
  - Face::extrude(DVec3) → Solid (OCCT B-rep)
  - Mesher::try_new() + mesh() → 테셀레이션
  - cimery Mesh로 변환 (vertices/normals/indices)
  - 기타 Feature: PureRustKernel 위임
- kernel/Cargo.toml: glam 0.24 (opencascade-rs 동일 버전)
- cargo build --features occt 빌드 확인 완료

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
minsung
2026-04-14 19:37:51 +09:00
parent bdacea5253
commit 0bc3f12688
4 changed files with 187 additions and 1 deletions

View File

@@ -12,6 +12,7 @@
## 타임라인 ## 타임라인
### 2026-04-14 ### 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 파이프라인 검증. - 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 — Revit API 가이드 Output/guides/revit-api-guide.md 추가됨.
- meta — PLAN.md · PROGRESS.md 도입. 에이전트 간 작업 조정 프로토콜 확립. - meta — PLAN.md · PROGRESS.md 도입. 에이전트 간 작업 조정 프로토콜 확립.

View File

@@ -7,7 +7,7 @@ edition.workspace = true
# Enable the full OpenCASCADE kernel backend. # Enable the full OpenCASCADE kernel backend.
# Requires OCCT installed/compiled — see cimery/CLAUDE.md for setup. # Requires OCCT installed/compiled — see cimery/CLAUDE.md for setup.
# Build: cargo build -p cimery-kernel --features occt # Build: cargo build -p cimery-kernel --features occt
occt = ["dep:opencascade"] occt = ["dep:opencascade", "dep:glam"]
[dependencies] [dependencies]
cimery-core = { workspace = true } cimery-core = { workspace = true }
@@ -16,6 +16,7 @@ thiserror = { workspace = true }
log = { workspace = true } log = { workspace = true }
# opencascade is OPTIONAL — only compiled with --features occt # opencascade is OPTIONAL — only compiled with --features occt
opencascade = { git = "https://github.com/bschwind/opencascade-rs", optional = true } opencascade = { git = "https://github.com/bschwind/opencascade-rs", optional = true }
glam = { version = "0.24", optional = true } # must match opencascade-rs glam version
[dev-dependencies] [dev-dependencies]
cimery-core = { workspace = true } cimery-core = { workspace = true }

View File

@@ -16,6 +16,10 @@ pub mod deck_slab;
pub mod bearing; pub mod bearing;
pub mod pier; pub mod pier;
pub mod abutment; pub mod abutment;
pub mod occt;
#[cfg(feature = "occt")]
pub use occt::OcctKernel;
use cimery_ir::{ use cimery_ir::{
AbutmentIR, BearingIR, DeckSlabIR, GirderIR, PierIR, SectionParams, AbutmentIR, BearingIR, DeckSlabIR, GirderIR, PierIR, SectionParams,

View File

@@ -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<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();
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<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)
}
}
}