## ADR-004 (Output/reports/ADR-004-sprint-25-39-decisions.md) Sprint 25~39 기간의 **15개 아키텍처 결정** 정리: - D1~D9: 거더교 MVP 확장 (단면 분기·다경간·Skew 관례·방호벽·격벽·Camber·헌치·UI) - D10~D13: IFC4X3 Add2 익스포터 4 결정 (크레이트 분리·형상 전략 3단계·GUID·Camber 근사) - D14: proc-macro 스캐폴딩 (전면 #[param] 는 Feature 10+ 안정 후) - D15: 변단면 거더 알고리즘 (소핏 lift + Y 선형보간) - 미결 6항목 (Pset 확장·LinearPlacement·ElementAssembly·IfcPile·#[param] 전면·변단면 IFC) - 테스트 커버리지 101개 현황표 ## IFC 스냅샷 테스트 (crates/ifc/tests/snapshot_tests.rs) insta 기반 회귀 방지, 8개 baseline: - mask_guids(): 22자 IFC GUID 를 'GUID' 로 정규화 (결정적 비교 가능) - 시나리오: 기본 단경간 PSC-I / 2경간 π형 / skew 15° / camber 50mm / Rectangle 단면 / parapets off - mask_guids 자체 유닛 테스트 2개 ## Mesh helper 유닛 테스트 (crates/viewer/src/bridge_scene.rs helper_tests) 순수 함수 9개 검증: - apply_camber_mesh: zero 항등·midspan 도달값·경간 밖 미영향 - rotate_y_around_z: 0 회전 항등·90° 피봇 회전·정점 개수 보존 - apply_variable_depth: zero 항등·소핏 lift · 지점 0 lift ## clippy lib 경고 15+ → 0 - map_identity (kernel/expansion_joint.rs) - unnecessary_lazy_evaluations ×4 (dsl/abutment·pier·csv_template — auto-fix) - too_many_arguments (usd save_scene — allow with justification) - clamp-like 패턴 ×7 (viewer bridge_scene/incremental_scene 의 .max(1).min(N) → .clamp(1, N)) - redundant_closure ×2 (project_file 의 `|e| Error::other(e)` → `Error::other`) - redundant_guard ×1 (viewer KeyboardInput match guard → 패턴 내 직접 매치) cargo clippy --workspace --lib: 0 경고. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
144 lines
5.9 KiB
Rust
144 lines
5.9 KiB
Rust
//! Pier (교각) Feature builder.
|
|
|
|
use cimery_core::{
|
|
ColumnShape, FeatureError, MaterialGrade, Mm, M, PierType, UnitExt,
|
|
};
|
|
use cimery_ir::{CapBeamIR, FeatureId, PierIR};
|
|
|
|
#[derive(Debug)]
|
|
pub struct Pier {
|
|
pub ir: PierIR,
|
|
}
|
|
|
|
impl Pier {
|
|
pub fn builder() -> PierBuilder { PierBuilder::default() }
|
|
}
|
|
|
|
/// Builder for a Pier Feature.
|
|
#[derive(Default)]
|
|
pub struct PierBuilder {
|
|
station: Option<M>,
|
|
skew_angle: Option<f64>,
|
|
pier_type: Option<PierType>,
|
|
column_shape: Option<ColumnShape>,
|
|
column_count: Option<u32>,
|
|
column_spacing: Option<Mm>,
|
|
column_diameter: Option<Mm>,
|
|
column_depth: Option<Mm>,
|
|
column_height: Option<Mm>,
|
|
cap_length: Option<Mm>,
|
|
cap_width: Option<Mm>,
|
|
cap_depth: Option<Mm>,
|
|
material: Option<MaterialGrade>,
|
|
}
|
|
|
|
impl PierBuilder {
|
|
/// #[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=PierType, default=SingleColumn)]
|
|
pub fn pier_type(mut self, t: PierType) -> Self { self.pier_type = Some(t); self }
|
|
/// #[param(enum=ColumnShape, default=Circular)]
|
|
pub fn column_shape(mut self, s: ColumnShape) -> Self { self.column_shape = Some(s); self }
|
|
/// #[param(unit="count", range=1..=6, default=1)]
|
|
pub fn column_count(mut self, n: u32) -> Self { self.column_count = Some(n); self }
|
|
/// #[param(unit="mm", range=500.0..=5000.0, default=3000.0)] Column c/c spacing
|
|
pub fn column_spacing(mut self, v: Mm) -> Self { self.column_spacing = Some(v); self }
|
|
/// #[param(unit="mm", range=800.0..=5000.0, default=1500.0)] Diameter or width
|
|
pub fn column_diameter(mut self, v: Mm) -> Self { self.column_diameter = Some(v); self }
|
|
/// #[param(unit="mm", range=800.0..=5000.0, default=1500.0)] Depth (rectangular only)
|
|
pub fn column_depth(mut self, v: Mm) -> Self { self.column_depth = Some(v); self }
|
|
/// #[param(unit="mm", range=3000.0..=40_000.0)] Foundation top to cap soffit
|
|
pub fn column_height(mut self, v: Mm) -> Self { self.column_height = Some(v); self }
|
|
/// #[param(unit="mm")] Cap beam total transverse length
|
|
pub fn cap_length(mut self, v: Mm) -> Self { self.cap_length = Some(v); self }
|
|
/// #[param(unit="mm", range=800.0..=3000.0, default=1200.0)] Along span
|
|
pub fn cap_width(mut self, v: Mm) -> Self { self.cap_width = Some(v); self }
|
|
/// #[param(unit="mm", range=800.0..=3000.0, default=1500.0)]
|
|
pub fn cap_depth(mut self, v: Mm) -> Self { self.cap_depth = Some(v); self }
|
|
/// #[param(enum=MaterialGrade, default=C40)]
|
|
pub fn material(mut self, m: MaterialGrade) -> Self { self.material = Some(m); self }
|
|
|
|
pub fn build(self) -> Result<Pier, FeatureError> {
|
|
let station = self.station.ok_or_else(|| FeatureError::missing("pier.station"))?.value();
|
|
let pier_type = self.pier_type.unwrap_or(PierType::SingleColumn);
|
|
let col_shape = self.column_shape.unwrap_or(ColumnShape::Circular);
|
|
let col_count = self.column_count.unwrap_or(1);
|
|
let col_dia = self.column_diameter
|
|
.ok_or_else(|| FeatureError::missing("pier.column_diameter"))?.value();
|
|
let col_h = self.column_height
|
|
.ok_or_else(|| FeatureError::missing("pier.column_height"))?.value();
|
|
let cap_w = self.cap_width.unwrap_or(1200.0.mm()).value();
|
|
let cap_d = self.cap_depth.unwrap_or(1500.0.mm()).value();
|
|
let cap_l = self.cap_length.unwrap_or_else(|| {
|
|
let span = col_count as f64 * col_dia + 2.0 * 1000.0;
|
|
Mm(span)
|
|
}).value();
|
|
|
|
if col_dia < 500.0 {
|
|
return Err(FeatureError::validation("pier.column_diameter",
|
|
format!("minimum 500 mm, got {col_dia:.0} mm")));
|
|
}
|
|
if col_h < 2000.0 {
|
|
return Err(FeatureError::validation("pier.column_height",
|
|
format!("minimum 2000 mm, got {col_h:.0} mm")));
|
|
}
|
|
if col_count > 1 && self.column_spacing.is_none() {
|
|
return Err(FeatureError::missing("pier.column_spacing"));
|
|
}
|
|
|
|
Ok(Pier { ir: PierIR {
|
|
id: FeatureId::new(),
|
|
station,
|
|
skew_angle: self.skew_angle.unwrap_or(0.0),
|
|
pier_type,
|
|
column_shape: col_shape,
|
|
column_count: col_count,
|
|
column_spacing: self.column_spacing.unwrap_or(0.0.mm()).value(),
|
|
column_diameter: col_dia,
|
|
column_depth: self.column_depth.unwrap_or(Mm(col_dia)).value(),
|
|
column_height: col_h,
|
|
cap_beam: CapBeamIR {
|
|
length: cap_l,
|
|
width: cap_w,
|
|
depth: cap_d,
|
|
cantilever_left: 1000.0,
|
|
cantilever_right: 1000.0,
|
|
},
|
|
material: self.material.unwrap_or(MaterialGrade::C40),
|
|
}})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use cimery_core::{ColumnShape, PierType, UnitExt};
|
|
|
|
#[test]
|
|
fn single_column_pier() {
|
|
let p = Pier::builder()
|
|
.station(100.0.m())
|
|
.pier_type(PierType::SingleColumn)
|
|
.column_shape(ColumnShape::Circular)
|
|
.column_diameter(1500.0.mm())
|
|
.column_height(8000.0.mm())
|
|
.build()
|
|
.unwrap();
|
|
assert_eq!(p.ir.column_count, 1);
|
|
assert!((p.ir.column_height - 8000.0).abs() < f64::EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn multi_column_needs_spacing() {
|
|
let e = Pier::builder()
|
|
.station(100.0.m())
|
|
.column_count(3)
|
|
.column_diameter(1200.0.mm())
|
|
.column_height(8000.0.mm())
|
|
.build().unwrap_err();
|
|
assert!(matches!(e, FeatureError::MissingField { .. }));
|
|
}
|
|
}
|