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-core"
version.workspace = true
edition.workspace = true
[dependencies]
serde = { workspace = true }
thiserror = { workspace = true }
[dev-dependencies]
serde_json = { workspace = true }

View File

@@ -0,0 +1,140 @@
//! cimery-core — unit system, errors, domain enums.
//!
//! ADR-002 rule: all structural dimensions in `Mm`, alignment/road in `M`.
//! Conversions MUST be explicit via `From`/`Into`. Never implicit.
use serde::{Deserialize, Serialize};
// ─── Unit types ──────────────────────────────────────────────────────────────
/// Millimetres — all structural dimensions (flange thickness, web, height, spacing…).
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct Mm(pub f64);
/// Metres — alignment stations, road offsets.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct M(pub f64);
impl Mm {
#[inline] pub fn value(self) -> f64 { self.0 }
#[inline] pub fn new(v: f64) -> Self { Self(v) }
}
impl M {
#[inline] pub fn value(self) -> f64 { self.0 }
#[inline] pub fn new(v: f64) -> Self { Self(v) }
}
// Explicit conversions only — boundary must be visible in code.
impl From<Mm> for M { fn from(mm: Mm) -> M { M(mm.0 / 1000.0) } }
impl From<M> for Mm { fn from(m: M) -> Mm { Mm(m.0 * 1000.0) } }
impl std::fmt::Display for Mm {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} mm", self.0)
}
}
impl std::fmt::Display for M {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} m", self.0)
}
}
/// Literal helpers: `1800.0_f64.mm()`, `40.0_f64.m()`
pub trait UnitExt: Sized {
fn mm(self) -> Mm;
fn m(self) -> M;
}
macro_rules! impl_unit_ext {
($($t:ty),*) => {
$(impl UnitExt for $t {
fn mm(self) -> Mm { Mm(self as f64) }
fn m(self) -> M { M(self as f64) }
})*
};
}
impl_unit_ext!(f64, f32, i32, i64, u32, u64);
// ─── Errors ──────────────────────────────────────────────────────────────────
/// All Feature build/validation errors.
#[derive(Debug, thiserror::Error)]
pub enum FeatureError {
/// A field value violated a constraint.
#[error("{path}: {message}")]
Validation { path: String, message: String },
/// A required field was not supplied.
#[error("{path}: required field is missing")]
MissingField { path: String },
/// Geometry generation failed.
#[error("geometry error: {0}")]
Geometry(String),
}
impl FeatureError {
pub fn validation(path: impl Into<String>, msg: impl Into<String>) -> Self {
Self::Validation { path: path.into(), message: msg.into() }
}
pub fn missing(path: impl Into<String>) -> Self {
Self::MissingField { path: path.into() }
}
}
// ─── Domain enums ─────────────────────────────────────────────────────────────
/// Girder cross-section family.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SectionType {
PscI,
PscU,
SteelBox,
SteelPlateI,
}
/// Structural material grade.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum MaterialGrade {
C40,
C50,
Ss400,
Sm490,
}
// ─── Tests ────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unit_conversions_are_explicit() {
let mm = Mm(1000.0);
let m: M = mm.into();
assert!((m.value() - 1.0).abs() < f64::EPSILON);
let m2 = M(0.5);
let mm2: Mm = m2.into();
assert!((mm2.value() - 500.0).abs() < f64::EPSILON);
}
#[test]
fn unit_literals() {
let h = 1800.0.mm();
let sp = 40.0.m();
assert_eq!(h.value(), 1800.0);
assert_eq!(sp.value(), 40.0);
}
#[test]
fn serde_roundtrip() {
let mm: Mm = 1234.5.mm();
let json = serde_json::to_string(&mm).unwrap();
let mm2: Mm = serde_json::from_str(&json).unwrap();
assert!((mm.value() - mm2.value()).abs() < f64::EPSILON);
}
}