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/dsl/Cargo.toml
Normal file
11
cimery/crates/dsl/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "cimery-dsl"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
cimery-core = { workspace = true }
|
||||
cimery-ir = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = { workspace = true }
|
||||
332
cimery/crates/dsl/src/girder.rs
Normal file
332
cimery/crates/dsl/src/girder.rs
Normal file
@@ -0,0 +1,332 @@
|
||||
//! Girder Feature builder.
|
||||
|
||||
use cimery_core::{FeatureError, MaterialGrade, Mm, M, SectionType, UnitExt};
|
||||
use cimery_ir::{
|
||||
FeatureId, GirderIR, PscISectionParams, SteelBoxParams, SectionParams,
|
||||
};
|
||||
|
||||
// ─── Girder ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/// A resolved Girder Feature — immutable after construction.
|
||||
///
|
||||
/// Build with [`GirderBuilder`]:
|
||||
/// ```rust
|
||||
/// use cimery_core::{MaterialGrade, UnitExt};
|
||||
/// use cimery_dsl::Girder;
|
||||
///
|
||||
/// let g = Girder::builder()
|
||||
/// .station_start(100.0.m())
|
||||
/// .station_end(140.0.m())
|
||||
/// .section_psc_i_default()
|
||||
/// .count(5)
|
||||
/// .spacing(2500.0.mm())
|
||||
/// .material(MaterialGrade::C50)
|
||||
/// .build()
|
||||
/// .expect("valid girder");
|
||||
///
|
||||
/// assert!((g.ir.span_m() - 40.0).abs() < f64::EPSILON);
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct Girder {
|
||||
pub ir: GirderIR,
|
||||
}
|
||||
|
||||
impl Girder {
|
||||
pub fn builder() -> GirderBuilder {
|
||||
GirderBuilder::default()
|
||||
}
|
||||
}
|
||||
|
||||
// ─── GirderBuilder ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Builder for a [`Girder`] Feature.
|
||||
///
|
||||
/// Every `set_*` method returns `Self` for chaining.
|
||||
/// Call [`GirderBuilder::build`] to validate and produce a [`Girder`].
|
||||
///
|
||||
/// **Defaults (when not set):**
|
||||
/// - `count` = 1
|
||||
/// - `spacing` = 2500 mm
|
||||
/// - `material` = C50
|
||||
/// - `offset` = 0 mm
|
||||
///
|
||||
/// **Validation rules:**
|
||||
/// - `station_start`, `station_end`, `section` are required.
|
||||
/// - `station_end > station_start`.
|
||||
/// - Span in practical range [5 m, 120 m].
|
||||
/// - `count ≥ 1`; `spacing > 0` when `count > 1`.
|
||||
#[derive(Default)]
|
||||
pub struct GirderBuilder {
|
||||
station_start: Option<M>,
|
||||
station_end: Option<M>,
|
||||
offset: Option<Mm>,
|
||||
section: Option<SectionParams>,
|
||||
section_type: Option<SectionType>,
|
||||
count: Option<u32>,
|
||||
spacing: Option<Mm>,
|
||||
material: Option<MaterialGrade>,
|
||||
}
|
||||
|
||||
impl GirderBuilder {
|
||||
// ── Alignment ──────────────────────────────────────────────────────────
|
||||
|
||||
/// #[param(unit="m", range=0.0..=100_000.0)] station along alignment
|
||||
pub fn station_start(mut self, v: M) -> Self {
|
||||
self.station_start = Some(v); self
|
||||
}
|
||||
|
||||
/// #[param(unit="m", range=0.0..=100_000.0)] station along alignment
|
||||
pub fn station_end(mut self, v: M) -> Self {
|
||||
self.station_end = Some(v); self
|
||||
}
|
||||
|
||||
/// #[param(unit="mm", range=-10_000.0..=10_000.0, default=0.0)]
|
||||
/// Lateral offset from alignment centreline. Positive = left.
|
||||
pub fn offset(mut self, v: Mm) -> Self {
|
||||
self.offset = Some(v); self
|
||||
}
|
||||
|
||||
// ── Section ────────────────────────────────────────────────────────────
|
||||
|
||||
/// PSC I-girder section with explicit parameters.
|
||||
pub fn section_psc_i(mut self, p: PscISectionParams) -> Self {
|
||||
self.section = Some(SectionParams::PscI(p));
|
||||
self.section_type = Some(SectionType::PscI);
|
||||
self
|
||||
}
|
||||
|
||||
/// PSC I-girder with KDS standard defaults (40 m span).
|
||||
pub fn section_psc_i_default(self) -> Self {
|
||||
self.section_psc_i(PscISectionParams::kds_standard())
|
||||
}
|
||||
|
||||
/// Steel box girder.
|
||||
pub fn section_steel_box(mut self, p: SteelBoxParams) -> Self {
|
||||
self.section = Some(SectionParams::SteelBox(p));
|
||||
self.section_type = Some(SectionType::SteelBox);
|
||||
self
|
||||
}
|
||||
|
||||
// ── Placement ──────────────────────────────────────────────────────────
|
||||
|
||||
/// #[param(unit="count", range=1..=20, default=1)]
|
||||
pub fn count(mut self, n: u32) -> Self {
|
||||
self.count = Some(n); self
|
||||
}
|
||||
|
||||
/// #[param(unit="mm", range=1500.0..=4000.0, default=2500.0)]
|
||||
/// Centre-to-centre girder spacing.
|
||||
pub fn spacing(mut self, v: Mm) -> Self {
|
||||
self.spacing = Some(v); self
|
||||
}
|
||||
|
||||
/// #[param(enum=MaterialGrade, default=C50)]
|
||||
pub fn material(mut self, m: MaterialGrade) -> Self {
|
||||
self.material = Some(m); self
|
||||
}
|
||||
|
||||
// ── Build ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// Validate and produce a [`Girder`].
|
||||
pub fn build(self) -> Result<Girder, FeatureError> {
|
||||
// Required fields
|
||||
let station_start = self.station_start
|
||||
.ok_or_else(|| FeatureError::missing("girder.station_start"))?
|
||||
.value();
|
||||
let station_end = self.station_end
|
||||
.ok_or_else(|| FeatureError::missing("girder.station_end"))?
|
||||
.value();
|
||||
let section = self.section
|
||||
.ok_or_else(|| FeatureError::missing("girder.section"))?;
|
||||
let section_type = self.section_type
|
||||
.ok_or_else(|| FeatureError::missing("girder.section_type"))?;
|
||||
|
||||
// Span validation
|
||||
if station_end <= station_start {
|
||||
return Err(FeatureError::validation(
|
||||
"girder.station_end",
|
||||
format!(
|
||||
"must be > station_start ({:.3} m); got {:.3} m",
|
||||
station_start, station_end
|
||||
),
|
||||
));
|
||||
}
|
||||
let span = station_end - station_start;
|
||||
if span < 5.0 {
|
||||
return Err(FeatureError::validation(
|
||||
"girder.span",
|
||||
format!("span {:.1} m is below minimum 5 m", span),
|
||||
));
|
||||
}
|
||||
if span > 120.0 {
|
||||
return Err(FeatureError::validation(
|
||||
"girder.span",
|
||||
format!("span {:.1} m exceeds maximum 120 m", span),
|
||||
));
|
||||
}
|
||||
|
||||
// Count / spacing
|
||||
let count = self.count.unwrap_or(1);
|
||||
if count == 0 {
|
||||
return Err(FeatureError::validation(
|
||||
"girder.count",
|
||||
"must be ≥ 1",
|
||||
));
|
||||
}
|
||||
let spacing = self.spacing.unwrap_or(2500.0.mm()).value();
|
||||
if count > 1 && spacing <= 0.0 {
|
||||
return Err(FeatureError::validation(
|
||||
"girder.spacing",
|
||||
"spacing must be > 0 mm when count > 1",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Girder {
|
||||
ir: GirderIR {
|
||||
id: FeatureId::new(),
|
||||
station_start,
|
||||
station_end,
|
||||
offset_from_alignment: self.offset.unwrap_or(0.0.mm()).value(),
|
||||
section_type,
|
||||
section,
|
||||
count,
|
||||
spacing,
|
||||
material: self.material.unwrap_or(MaterialGrade::C50),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cimery_core::{MaterialGrade, UnitExt};
|
||||
use cimery_ir::SteelBoxParams;
|
||||
|
||||
fn psc_i_girder() -> Result<Girder, FeatureError> {
|
||||
Girder::builder()
|
||||
.station_start(100.0.m())
|
||||
.station_end(140.0.m())
|
||||
.section_psc_i_default()
|
||||
.count(5)
|
||||
.spacing(2500.0.mm())
|
||||
.material(MaterialGrade::C50)
|
||||
.build()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_valid_psc_i() {
|
||||
let g = psc_i_girder().unwrap();
|
||||
assert!((g.ir.span_m() - 40.0).abs() < f64::EPSILON);
|
||||
assert_eq!(g.ir.count, 5);
|
||||
assert_eq!(g.ir.material, MaterialGrade::C50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_valid_steel_box() {
|
||||
let g = Girder::builder()
|
||||
.station_start(0.0.m())
|
||||
.station_end(50.0.m())
|
||||
.section_steel_box(SteelBoxParams {
|
||||
total_height: 2000.0,
|
||||
top_width: 3000.0,
|
||||
bottom_width: 2000.0,
|
||||
web_thickness: 18.0,
|
||||
top_flange_thickness: 24.0,
|
||||
bottom_flange_thickness: 20.0,
|
||||
})
|
||||
.build()
|
||||
.unwrap();
|
||||
assert!((g.ir.span_m() - 50.0).abs() < f64::EPSILON);
|
||||
assert_eq!(g.ir.section_type, SectionType::SteelBox);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_station_start_fails() {
|
||||
let e = Girder::builder()
|
||||
.station_end(40.0.m())
|
||||
.section_psc_i_default()
|
||||
.build()
|
||||
.unwrap_err();
|
||||
assert!(matches!(e, FeatureError::MissingField { ref path } if path == "girder.station_start"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_section_fails() {
|
||||
let e = Girder::builder()
|
||||
.station_start(0.0.m())
|
||||
.station_end(40.0.m())
|
||||
.build()
|
||||
.unwrap_err();
|
||||
assert!(matches!(e, FeatureError::MissingField { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_span_fails() {
|
||||
let e = Girder::builder()
|
||||
.station_start(100.0.m())
|
||||
.station_end(100.0.m())
|
||||
.section_psc_i_default()
|
||||
.build()
|
||||
.unwrap_err();
|
||||
assert!(matches!(e, FeatureError::Validation { ref path, .. } if path == "girder.station_end"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn span_too_short_fails() {
|
||||
let e = Girder::builder()
|
||||
.station_start(0.0.m())
|
||||
.station_end(3.0.m()) // 3 m < 5 m minimum
|
||||
.section_psc_i_default()
|
||||
.build()
|
||||
.unwrap_err();
|
||||
assert!(matches!(e, FeatureError::Validation { ref path, .. } if path == "girder.span"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn span_too_long_fails() {
|
||||
let e = Girder::builder()
|
||||
.station_start(0.0.m())
|
||||
.station_end(150.0.m()) // 150 m > 120 m maximum
|
||||
.section_psc_i_default()
|
||||
.build()
|
||||
.unwrap_err();
|
||||
assert!(matches!(e, FeatureError::Validation { ref path, .. } if path == "girder.span"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_count_fails() {
|
||||
let e = Girder::builder()
|
||||
.station_start(0.0.m())
|
||||
.station_end(40.0.m())
|
||||
.section_psc_i_default()
|
||||
.count(0)
|
||||
.build()
|
||||
.unwrap_err();
|
||||
assert!(matches!(e, FeatureError::Validation { ref path, .. } if path == "girder.count"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_girder_requires_spacing() {
|
||||
let e = Girder::builder()
|
||||
.station_start(0.0.m())
|
||||
.station_end(40.0.m())
|
||||
.section_psc_i_default()
|
||||
.count(3)
|
||||
.spacing(0.0.mm())
|
||||
.build()
|
||||
.unwrap_err();
|
||||
assert!(matches!(e, FeatureError::Validation { ref path, .. } if path == "girder.spacing"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ir_serializes_to_json() {
|
||||
let g = psc_i_girder().unwrap();
|
||||
let json = serde_json::to_string_pretty(&g.ir).unwrap();
|
||||
let ir2: cimery_ir::GirderIR = serde_json::from_str(&json).unwrap();
|
||||
assert!((g.ir.span_m() - ir2.span_m()).abs() < f64::EPSILON);
|
||||
assert_eq!(g.ir.count, ir2.count);
|
||||
}
|
||||
}
|
||||
9
cimery/crates/dsl/src/lib.rs
Normal file
9
cimery/crates/dsl/src/lib.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
//! cimery-dsl — fluent builder DSL for cimery features.
|
||||
//!
|
||||
//! # Design
|
||||
//! Pure builder pattern, no proc macros (ADR-002 J).
|
||||
//! Comments starting with `/// #[param(...)` are documentation hints
|
||||
//! for a future macro-upgrade path and do not affect compilation.
|
||||
|
||||
pub mod girder;
|
||||
pub use girder::{Girder, GirderBuilder};
|
||||
Reference in New Issue
Block a user