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:
@@ -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 도입. 에이전트 간 작업 조정 프로토콜 확립.
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
180
cimery/crates/kernel/src/occt.rs
Normal file
180
cimery/crates/kernel/src/occt.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user