상부 구조물: - DeckSlabIR + DeckSlabBuilder + 기하학 (직사각형 슬래브 스위프) 연결부: - BearingIR + BearingBuilder (카탈로그 기반, KDS 기본값 포함) 하부 구조물: - PierIR + PierBuilder + 기하학 (다주 지원, 코핑 포함) - AbutmentIR + AbutmentBuilder + 기하학 (흉벽 + 기초 + 날개벽) core에 BearingType·PierType·ColumnShape·AbutmentType 열거형 추가 kernel: sweep.rs 공유 모듈 (sweep_profile_flat·box·prism·merge) psc_i.rs → sweep.rs 의존으로 리팩터 GeomKernel trait에 4개 메서드 추가 (상부→하부 문서화 주석) cargo test 57개 전부 통과 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
123 lines
5.3 KiB
Rust
123 lines
5.3 KiB
Rust
//! Abutment (교대) Feature builder.
|
|
|
|
use cimery_core::{AbutmentType, FeatureError, MaterialGrade, Mm, M, UnitExt};
|
|
use cimery_ir::{AbutmentIR, FeatureId, WingWallIR};
|
|
|
|
#[derive(Debug)]
|
|
pub struct Abutment {
|
|
pub ir: AbutmentIR,
|
|
}
|
|
|
|
impl Abutment {
|
|
pub fn builder() -> AbutmentBuilder { AbutmentBuilder::default() }
|
|
}
|
|
|
|
/// Builder for an Abutment Feature.
|
|
#[derive(Default)]
|
|
pub struct AbutmentBuilder {
|
|
station: Option<M>,
|
|
skew_angle: Option<f64>,
|
|
abutment_type: Option<AbutmentType>,
|
|
breast_wall_height: Option<Mm>,
|
|
breast_wall_thickness: Option<Mm>,
|
|
breast_wall_width: Option<Mm>,
|
|
footing_length: Option<Mm>,
|
|
footing_width: Option<Mm>,
|
|
footing_thickness: Option<Mm>,
|
|
wing_length: Option<Mm>, // simplified: same left & right
|
|
material: Option<MaterialGrade>,
|
|
}
|
|
|
|
impl AbutmentBuilder {
|
|
/// #[param(unit="m")] Station along alignment
|
|
pub fn station(mut self, v: M) -> Self { self.station = Some(v); self }
|
|
/// #[param(unit="deg", range=-45.0..=45.0, default=0.0)]
|
|
pub fn skew_angle(mut self, v: f64) -> Self { self.skew_angle = Some(v); self }
|
|
/// #[param(enum=AbutmentType, default=ReverseT)]
|
|
pub fn abutment_type(mut self, t: AbutmentType) -> Self { self.abutment_type = Some(t); self }
|
|
/// #[param(unit="mm", range=2000.0..=12_000.0)] Breast wall height
|
|
pub fn breast_wall_height(mut self, v: Mm) -> Self { self.breast_wall_height = Some(v); self }
|
|
/// #[param(unit="mm", range=500.0..=3000.0, default=800.0)]
|
|
pub fn breast_wall_thickness(mut self, v: Mm) -> Self { self.breast_wall_thickness = Some(v); self }
|
|
/// #[param(unit="mm")] Breast wall transverse width
|
|
pub fn breast_wall_width(mut self, v: Mm) -> Self { self.breast_wall_width = Some(v); self }
|
|
/// #[param(unit="mm", range=2000.0..=8000.0, default=4000.0)] Footing along span
|
|
pub fn footing_length(mut self, v: Mm) -> Self { self.footing_length = Some(v); self }
|
|
/// #[param(unit="mm")] Footing transverse width
|
|
pub fn footing_width(mut self, v: Mm) -> Self { self.footing_width = Some(v); self }
|
|
/// #[param(unit="mm", range=500.0..=2000.0, default=1000.0)]
|
|
pub fn footing_thickness(mut self, v: Mm) -> Self { self.footing_thickness = Some(v); self }
|
|
/// #[param(unit="mm")] Wing wall length (both sides equal)
|
|
pub fn wing_length(mut self, v: Mm) -> Self { self.wing_length = Some(v); self }
|
|
/// #[param(enum=MaterialGrade, default=C30)]
|
|
pub fn material(mut self, m: MaterialGrade) -> Self { self.material = Some(m); self }
|
|
|
|
pub fn build(self) -> Result<Abutment, FeatureError> {
|
|
let station = self.station
|
|
.ok_or_else(|| FeatureError::missing("abutment.station"))?.value();
|
|
let bw_h = self.breast_wall_height
|
|
.ok_or_else(|| FeatureError::missing("abutment.breast_wall_height"))?.value();
|
|
let bw_w = self.breast_wall_width
|
|
.ok_or_else(|| FeatureError::missing("abutment.breast_wall_width"))?.value();
|
|
let bw_t = self.breast_wall_thickness.unwrap_or(800.0.mm()).value();
|
|
let foot_l = self.footing_length.unwrap_or(4000.0.mm()).value();
|
|
let foot_w = self.footing_width.unwrap_or_else(|| Mm(bw_w + 1000.0)).value();
|
|
let foot_t = self.footing_thickness.unwrap_or(1000.0.mm()).value();
|
|
let wing_l = self.wing_length.unwrap_or_else(|| Mm(bw_h * 0.8)).value();
|
|
let wing_h = bw_h * 0.5; // simplified: half the breast wall height
|
|
let wing_t = bw_t;
|
|
|
|
if bw_h < 1000.0 {
|
|
return Err(FeatureError::validation("abutment.breast_wall_height",
|
|
format!("minimum 1000 mm, got {bw_h:.0} mm")));
|
|
}
|
|
|
|
let wing = WingWallIR { length: wing_l, height: wing_h, thickness: wing_t };
|
|
|
|
Ok(Abutment { ir: AbutmentIR {
|
|
id: FeatureId::new(),
|
|
station,
|
|
skew_angle: self.skew_angle.unwrap_or(0.0),
|
|
abutment_type: self.abutment_type.unwrap_or(AbutmentType::ReverseT),
|
|
breast_wall_height: bw_h,
|
|
breast_wall_thickness: bw_t,
|
|
breast_wall_width: bw_w,
|
|
footing_length: foot_l,
|
|
footing_width: foot_w,
|
|
footing_thickness: foot_t,
|
|
wing_wall_left: wing.clone(),
|
|
wing_wall_right: wing,
|
|
material: self.material.unwrap_or(MaterialGrade::C40),
|
|
}})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use cimery_core::{AbutmentType, UnitExt};
|
|
|
|
#[test]
|
|
fn build_reverse_t() {
|
|
let a = Abutment::builder()
|
|
.station(100.0.m())
|
|
.abutment_type(AbutmentType::ReverseT)
|
|
.breast_wall_height(5000.0.mm())
|
|
.breast_wall_width(12000.0.mm())
|
|
.build()
|
|
.unwrap();
|
|
assert_eq!(a.ir.abutment_type, AbutmentType::ReverseT);
|
|
assert!((a.ir.breast_wall_height - 5000.0).abs() < f64::EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn too_short_breast_wall_fails() {
|
|
let e = Abutment::builder()
|
|
.station(0.0.m())
|
|
.breast_wall_height(500.0.mm()) // < 1000
|
|
.breast_wall_width(10000.0.mm())
|
|
.build().unwrap_err();
|
|
assert!(matches!(e, FeatureError::Validation { .. }));
|
|
}
|
|
}
|