Sprint 3 — Must Feature 5종 추가 (상부→하부 순서)

상부 구조물:
- 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>
This commit is contained in:
minsung
2026-04-14 19:27:57 +09:00
parent 40857f39c5
commit bdacea5253
16 changed files with 1113 additions and 85 deletions

View File

@@ -0,0 +1,122 @@
//! 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 { .. }));
}
}