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:
153
cimery/crates/dsl/src/cross_beam.rs
Normal file
153
cimery/crates/dsl/src/cross_beam.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
130
cimery/crates/dsl/src/expansion_joint.rs
Normal file
130
cimery/crates/dsl/src/expansion_joint.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user