cimery Sprint 1 — Rust 워크스페이스 + 전 계층 파이프라인
8개 크레이트 구현, cargo test 32개 전부 통과: - core: Mm/M 단위 newtype, UnitExt 리터럴, FeatureError - ir: GirderIR + 전 단면 파라미터(PSC-I/U/SteelBox/PlateI) serde JSON - dsl: Girder builder + 검증 (경간 범위·count·spacing) - kernel: GeomKernel trait + StubKernel (box mesh, AABB) - incremental: dirty-tracking IncrementalDb (salsa 업그레이드 경로 주석) - evaluator: 상태 없는 IR→kernel 브리지 - usd: USDA 1.0 텍스트 익스포트 (CimeryBridgeAPI·GirderAPI schema) - viewer: wgpu 22 + winit 0.30 컬러 삼각형 (Sprint 1 proof-of-concept) Sprint 2 다음 단계: - opencascade-rs로 StubKernel 교체 (실제 PSC-I sweep) - viewer에서 Girder Mesh 렌더 + 카메라 orbit Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
11
cimery/crates/kernel/Cargo.toml
Normal file
11
cimery/crates/kernel/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "cimery-kernel"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
cimery-ir = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
cimery-core = { workspace = true }
|
||||
160
cimery/crates/kernel/src/lib.rs
Normal file
160
cimery/crates/kernel/src/lib.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
//! cimery-kernel — GeomKernel trait + mesh types + StubKernel.
|
||||
//!
|
||||
//! ADR-001: Two production backends (Sprint 2+):
|
||||
//! - OpenCascade.js (WASM, web)
|
||||
//! - opencascade-rs (native FFI, desktop)
|
||||
//! Both accessed via `GeomKernel` trait.
|
||||
//! Sprint 1: `StubKernel` returns simple box geometry for architecture validation.
|
||||
|
||||
use cimery_ir::GirderIR;
|
||||
|
||||
// ─── Mesh ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Triangle mesh produced by a geometry kernel.
|
||||
///
|
||||
/// Coordinate convention: X = width, Y = height, Z = along-span axis.
|
||||
/// Units: millimetres.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Mesh {
|
||||
/// Interleaved [x, y, z] vertex positions in mm.
|
||||
pub vertices: Vec<[f32; 3]>,
|
||||
/// Triangle indices into `vertices`, 3 entries per triangle.
|
||||
pub indices: Vec<u32>,
|
||||
/// Per-vertex normals (unit vectors).
|
||||
pub normals: Vec<[f32; 3]>,
|
||||
}
|
||||
|
||||
impl Mesh {
|
||||
pub fn vertex_count(&self) -> usize { self.vertices.len() }
|
||||
pub fn triangle_count(&self) -> usize { self.indices.len() / 3 }
|
||||
|
||||
/// Axis-aligned bounding box: ([min_x, min_y, min_z], [max_x, max_y, max_z])
|
||||
pub fn aabb(&self) -> ([f32; 3], [f32; 3]) {
|
||||
let mut mn = [f32::INFINITY; 3];
|
||||
let mut mx = [f32::NEG_INFINITY; 3];
|
||||
for v in &self.vertices {
|
||||
for i in 0..3 {
|
||||
if v[i] < mn[i] { mn[i] = v[i]; }
|
||||
if v[i] > mx[i] { mx[i] = v[i]; }
|
||||
}
|
||||
}
|
||||
(mn, mx)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Error ────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum KernelError {
|
||||
#[error("geometry computation failed: {0}")]
|
||||
Computation(String),
|
||||
#[error("invalid input for kernel: {0}")]
|
||||
InvalidInput(String),
|
||||
}
|
||||
|
||||
// ─── Trait ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Backend-agnostic geometry kernel.
|
||||
///
|
||||
/// All implementations MUST be deterministic: same IR → same Mesh topology.
|
||||
/// Floating-point values may differ within kernel tolerance (< 1 µm).
|
||||
pub trait GeomKernel: Send + Sync {
|
||||
fn girder_mesh(&self, ir: &GirderIR) -> Result<Mesh, KernelError>;
|
||||
}
|
||||
|
||||
// ─── StubKernel ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Stub geometry backend for Sprint 1.
|
||||
///
|
||||
/// Returns a simple rectangular box for any girder.
|
||||
/// - X = 600 mm (fixed width stub)
|
||||
/// - Y = 1800 mm (fixed height stub)
|
||||
/// - Z = girder span in mm
|
||||
///
|
||||
/// Replace with `OcctKernel` in Sprint 2.
|
||||
pub struct StubKernel;
|
||||
|
||||
impl GeomKernel for StubKernel {
|
||||
fn girder_mesh(&self, 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()),
|
||||
));
|
||||
}
|
||||
|
||||
let len = ir.span_mm() as f32;
|
||||
let w = 600.0_f32;
|
||||
let h = 1800.0_f32;
|
||||
|
||||
// 8 corners: indices 0-3 at Z=0, 4-7 at Z=len
|
||||
let vertices: Vec<[f32; 3]> = vec![
|
||||
[0.0, 0.0, 0.0], [w, 0.0, 0.0], [w, h, 0.0], [0.0, h, 0.0],
|
||||
[0.0, 0.0, len], [w, 0.0, len], [w, h, len], [0.0, h, len],
|
||||
];
|
||||
|
||||
// 12 triangles (2 per face × 6 faces), CCW winding from outside
|
||||
let indices: Vec<u32> = vec![
|
||||
// -Z face
|
||||
0, 2, 1, 0, 3, 2,
|
||||
// +Z face
|
||||
4, 5, 6, 4, 6, 7,
|
||||
// -X face
|
||||
0, 4, 7, 0, 7, 3,
|
||||
// +X face
|
||||
1, 2, 6, 1, 6, 5,
|
||||
// -Y face (bottom)
|
||||
0, 1, 5, 0, 5, 4,
|
||||
// +Y face (top)
|
||||
3, 7, 6, 3, 6, 2,
|
||||
];
|
||||
|
||||
let normals = vec![[0.0_f32, 1.0, 0.0]; vertices.len()];
|
||||
|
||||
Ok(Mesh { vertices, indices, normals })
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cimery_core::{MaterialGrade, SectionType};
|
||||
use cimery_ir::{FeatureId, GirderIR, PscISectionParams, SectionParams};
|
||||
|
||||
fn test_girder(span_m: f64) -> GirderIR {
|
||||
GirderIR {
|
||||
id: FeatureId::new(),
|
||||
station_start: 0.0,
|
||||
station_end: span_m,
|
||||
offset_from_alignment: 0.0,
|
||||
section_type: SectionType::PscI,
|
||||
section: SectionParams::PscI(PscISectionParams::kds_standard()),
|
||||
count: 1,
|
||||
spacing: 0.0,
|
||||
material: MaterialGrade::C50,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stub_produces_box_mesh() {
|
||||
let mesh = StubKernel.girder_mesh(&test_girder(40.0)).unwrap();
|
||||
assert_eq!(mesh.vertex_count(), 8);
|
||||
assert_eq!(mesh.triangle_count(), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aabb_spans_correctly() {
|
||||
let ir = test_girder(40.0);
|
||||
let mesh = StubKernel.girder_mesh(&ir).unwrap();
|
||||
let (mn, mx) = mesh.aabb();
|
||||
assert!((mx[2] - ir.span_mm() as f32).abs() < 0.01);
|
||||
assert!(mn[2] < 0.001); // Z min ≈ 0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_span_fails() {
|
||||
let err = StubKernel.girder_mesh(&test_girder(0.0));
|
||||
assert!(matches!(err, Err(KernelError::InvalidInput(_))));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user