Sprint 14~22 — egui 리본 UI + OcctKernel B-rep + 가로보/신축이음 + 선형 좌표 + USD 익스포트 + WASM + CI/CD + 테스트 4층

Sprint 14: egui TopBottomPanel 리본 + CollapsingHeader SidePanel (상부구조·추가부재·선형·프로젝트)
Sprint 15: IncrementalDb 전 Feature 타입 확장 (girder→7종), dirty-tracking 20 unit tests
Sprint 16: Gitea + GitHub Actions CI/CD (check/test/clippy/fmt + 멀티플랫폼 릴리스)
Sprint 17: AlignmentTransform + AlignmentScene — 선형 국소 프레임 → 세계 좌표 변환
Sprint 18: OcctKernel 교각(16각형 기둥+코핑) + 교대(흉벽+푸팅+날개벽) B-rep
Sprint 19: CrossBeamIR + ExpansionJointIR — IR/DSL/kernel/scene 전 계층, sweep_profile_flat_x
Sprint 20: 테스트 4층 — Layer1 insta 스냅샷(7종), Layer2 기하 불변량(19), Layer3 두-커널(7), Layer4 proptest(7) — 61 tests pass
Sprint 21: cimery-usd PureRustKernel 실제 기하 변환 + BridgeExporter 증분 캐시
Sprint 22: viewer wasm feature + wasm-bindgen/web-sys + GitHub Actions Cloudflare Pages 배포

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
minsung
2026-04-15 08:18:06 +09:00
parent 81349c97d2
commit 1f9ca3a00f
37 changed files with 3569 additions and 259 deletions

View File

@@ -0,0 +1,153 @@
//! Cross beam (가로보) DSL builder. Sprint 19.
//!
//! A cross beam braces multiple girder bays transversely at a given station.
//!
//! # Example
//! ```rust,ignore
//! let cb = CrossBeam::builder()
//! .station(10.0.m())
//! .section(CrossBeamSection::HSection)
//! .web_height(1300.0.mm())
//! .web_thickness(200.0.mm())
//! .flange_width(400.0.mm())
//! .flange_thickness(20.0.mm())
//! .bay_count(4)
//! .girder_spacing(2500.0.mm())
//! .build()
//! .expect("valid cross beam");
//! ```
use cimery_core::{CrossBeamSection, FeatureError, MaterialGrade, M, Mm};
use cimery_ir::{CrossBeamIR, FeatureId};
pub struct CrossBeam {
pub ir: CrossBeamIR,
}
impl CrossBeam {
pub fn builder() -> CrossBeamBuilder { CrossBeamBuilder::default() }
}
#[derive(Default)]
pub struct CrossBeamBuilder {
station: Option<f64>,
section: Option<CrossBeamSection>,
web_height: Option<f64>,
web_thickness: Option<f64>,
flange_width: Option<f64>,
flange_thickness: Option<f64>,
bay_count: Option<u32>,
girder_spacing: Option<f64>,
material: Option<MaterialGrade>,
}
impl CrossBeamBuilder {
/// Station along alignment [m].
pub fn station(mut self, v: M) -> Self {
self.station = Some(v.value()); self
}
/// Cross-section type.
pub fn section(mut self, v: CrossBeamSection) -> Self {
self.section = Some(v); self
}
/// #[param(unit="mm", range=500..=3000, default=1260)]
pub fn web_height(mut self, v: Mm) -> Self {
self.web_height = Some(v.value()); self
}
/// #[param(unit="mm", range=100..=400, default=200)]
pub fn web_thickness(mut self, v: Mm) -> Self {
self.web_thickness = Some(v.value()); self
}
/// #[param(unit="mm", range=200..=600, default=400)]
pub fn flange_width(mut self, v: Mm) -> Self {
self.flange_width = Some(v.value()); self
}
/// #[param(unit="mm", range=12..=50, default=20)]
pub fn flange_thickness(mut self, v: Mm) -> Self {
self.flange_thickness = Some(v.value()); self
}
/// Number of girder bays to span (= girder_count - 1).
pub fn bay_count(mut self, v: u32) -> Self {
self.bay_count = Some(v); self
}
/// #[param(unit="mm", range=1500..=4000, default=2500)]
pub fn girder_spacing(mut self, v: Mm) -> Self {
self.girder_spacing = Some(v.value()); self
}
pub fn material(mut self, v: MaterialGrade) -> Self {
self.material = Some(v); self
}
pub fn build(self) -> Result<CrossBeam, FeatureError> {
let station = self.station.unwrap_or(0.0);
let section = self.section.unwrap_or(CrossBeamSection::HSection);
let web_height = self.web_height.ok_or_else(|| FeatureError::missing("cross_beam.web_height"))?;
let web_thickness = self.web_thickness.unwrap_or(200.0);
let flange_width = self.flange_width.unwrap_or(400.0);
let flange_thick = self.flange_thickness.unwrap_or(20.0);
let bay_count = self.bay_count.ok_or_else(|| FeatureError::missing("cross_beam.bay_count"))?;
let girder_sp = self.girder_spacing.ok_or_else(|| FeatureError::missing("cross_beam.girder_spacing"))?;
let material = self.material.unwrap_or(MaterialGrade::C50);
if web_height <= 0.0 {
return Err(FeatureError::validation("cross_beam.web_height", "must be positive"));
}
if bay_count == 0 {
return Err(FeatureError::validation("cross_beam.bay_count", "must be ≥ 1"));
}
if girder_sp <= 0.0 {
return Err(FeatureError::validation("cross_beam.girder_spacing", "must be positive"));
}
Ok(CrossBeam {
ir: CrossBeamIR {
id: FeatureId::new(),
station,
section,
web_height,
web_thickness,
flange_width,
flange_thickness: flange_thick,
bay_count,
girder_spacing: girder_sp,
material,
},
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use cimery_core::UnitExt;
#[test]
fn builder_valid() {
let cb = CrossBeam::builder()
.station(10.0.m())
.web_height(1260.0.mm())
.bay_count(4)
.girder_spacing(2500.0.mm())
.build()
.unwrap();
assert!((cb.ir.station - 10.0).abs() < f64::EPSILON);
assert_eq!(cb.ir.bay_count, 4);
assert!((cb.ir.total_length_mm() - 10_000.0).abs() < f64::EPSILON);
}
#[test]
fn builder_missing_web_height() {
let err = CrossBeam::builder()
.bay_count(4).girder_spacing(2500.0.mm())
.build();
assert!(err.is_err());
}
#[test]
fn builder_missing_bay_count() {
let err = CrossBeam::builder()
.web_height(1260.0.mm()).girder_spacing(2500.0.mm())
.build();
assert!(err.is_err());
}
}

View File

@@ -0,0 +1,130 @@
//! Expansion joint (신축이음) DSL builder. Sprint 19.
//!
//! # Example
//! ```rust,ignore
//! let ej = ExpansionJoint::builder()
//! .station(40.0.m())
//! .joint_type(ExpansionJointType::RubberType)
//! .gap_width(50.0.mm())
//! .total_width(12000.0.mm())
//! .depth(300.0.mm())
//! .movement_range(60.0.mm())
//! .build()
//! .expect("valid expansion joint");
//! ```
use cimery_core::{ExpansionJointType, FeatureError, M, Mm};
use cimery_ir::{ExpansionJointIR, FeatureId};
pub struct ExpansionJoint {
pub ir: ExpansionJointIR,
}
impl ExpansionJoint {
pub fn builder() -> ExpansionJointBuilder { ExpansionJointBuilder::default() }
}
#[derive(Default)]
pub struct ExpansionJointBuilder {
station: Option<f64>,
joint_type: Option<ExpansionJointType>,
gap_width: Option<f64>,
total_width: Option<f64>,
depth: Option<f64>,
movement_range: Option<f64>,
}
impl ExpansionJointBuilder {
/// Station along alignment [m].
pub fn station(mut self, v: M) -> Self {
self.station = Some(v.value()); self
}
/// Type of expansion joint mechanism.
pub fn joint_type(mut self, v: ExpansionJointType) -> Self {
self.joint_type = Some(v); self
}
/// #[param(unit="mm", range=20..=200, default=50)]
pub fn gap_width(mut self, v: Mm) -> Self {
self.gap_width = Some(v.value()); self
}
/// #[param(unit="mm", range=2000..=30000, default=12000)]
pub fn total_width(mut self, v: Mm) -> Self {
self.total_width = Some(v.value()); self
}
/// #[param(unit="mm", range=100..=600, default=300)]
pub fn depth(mut self, v: Mm) -> Self {
self.depth = Some(v.value()); self
}
/// #[param(unit="mm", range=10..=500, default=60)]
pub fn movement_range(mut self, v: Mm) -> Self {
self.movement_range = Some(v.value()); self
}
pub fn build(self) -> Result<ExpansionJoint, FeatureError> {
let station = self.station.unwrap_or(0.0);
let joint_type = self.joint_type.unwrap_or(ExpansionJointType::RubberType);
let gap_width = self.gap_width.ok_or_else(|| FeatureError::missing("expansion_joint.gap_width"))?;
let total_width = self.total_width.ok_or_else(|| FeatureError::missing("expansion_joint.total_width"))?;
let depth = self.depth.unwrap_or(300.0);
let movement_range = self.movement_range.unwrap_or(60.0);
if gap_width <= 0.0 {
return Err(FeatureError::validation("expansion_joint.gap_width", "must be positive"));
}
if total_width <= 0.0 {
return Err(FeatureError::validation("expansion_joint.total_width", "must be positive"));
}
if depth <= 0.0 {
return Err(FeatureError::validation("expansion_joint.depth", "must be positive"));
}
Ok(ExpansionJoint {
ir: ExpansionJointIR {
id: FeatureId::new(),
station,
joint_type,
gap_width,
total_width,
depth,
movement_range,
},
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use cimery_core::UnitExt;
#[test]
fn builder_valid() {
let ej = ExpansionJoint::builder()
.station(40.0.m())
.gap_width(50.0.mm())
.total_width(12_000.0.mm())
.depth(300.0.mm())
.build()
.unwrap();
assert!((ej.ir.station - 40.0).abs() < f64::EPSILON);
assert!((ej.ir.gap_width - 50.0).abs() < f64::EPSILON);
}
#[test]
fn builder_missing_gap_width() {
let err = ExpansionJoint::builder()
.total_width(12_000.0.mm())
.build();
assert!(err.is_err());
}
#[test]
fn rubber_type_default() {
let ej = ExpansionJoint::builder()
.gap_width(50.0.mm())
.total_width(12_000.0.mm())
.build()
.unwrap();
assert_eq!(ej.ir.joint_type, ExpansionJointType::RubberType);
}
}

View File

@@ -11,9 +11,13 @@ pub mod deck_slab;
pub mod bearing;
pub mod pier;
pub mod abutment;
pub mod cross_beam; // Sprint 19
pub mod expansion_joint; // Sprint 19
pub use girder::{Girder, GirderBuilder};
pub use deck_slab::{DeckSlab, DeckSlabBuilder};
pub use bearing::{Bearing, BearingBuilder};
pub use pier::{Pier, PierBuilder};
pub use abutment::{Abutment, AbutmentBuilder};
pub use cross_beam::{CrossBeam, CrossBeamBuilder};
pub use expansion_joint::{ExpansionJoint, ExpansionJointBuilder};