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/core/Cargo.toml
Normal file
11
cimery/crates/core/Cargo.toml
Normal 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 }
|
||||
140
cimery/crates/core/src/lib.rs
Normal file
140
cimery/crates/core/src/lib.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user