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:
minsung
2026-04-14 17:46:14 +09:00
parent 919855c1e8
commit 62ddf3aea6
24 changed files with 1779 additions and 3 deletions

View 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 }

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