cargo doc --workspace --no-deps 에서 55 → 0 warnings.
## 변경
- crates/{ir, kernel, incremental, viewer}/src/lib.rs 최상단에
#![allow(rustdoc::broken_intra_doc_links)] 추가.
· 이유: doc comment 내 단위 표기 [mm] [m] [deg] [rad] [radians] 가
intra-doc link 문법과 충돌 (40+ 건). 실제 intra-doc link 가 아님.
· 각 crate 에 주석으로 이유 명시.
- crates/ifc/src/bridge_export.rs: camber_mid_mm doc comment 의 \[mm\] escape.
- crates/dsl/src/{cross_beam,expansion_joint}.rs: station doc comment \[m\] escape.
- crates/viewer/src/camera.rs: radius/yaw/pitch doc \[mm\]/\[radians\] escape.
- crates/incremental/src/lib.rs: `HashSet<FeatureId>` → backtick 래핑
(unclosed HTML tag 경고 해소).
cargo check --workspace --all-targets: 0 warnings.
테스트 회귀 없음: cimery-ifc 20 + cimery-viewer 13 passed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
685 lines
27 KiB
Rust
685 lines
27 KiB
Rust
#![allow(rustdoc::broken_intra_doc_links)] // doc comment 단위 표기 false positive.
|
|
|
|
//! cimery-incremental — incremental computation layer.
|
|
//!
|
|
//! ## 백엔드 선택
|
|
//!
|
|
//! | Feature flag | 백엔드 | 타겟 |
|
|
//! |-----------------------|----------------------------|------------------|
|
|
//! | (없음, 기본값) | 수동 dirty tracking | 모든 타겟 (WASM) |
|
|
//! | `salsa-backend` | salsa 0.16 query group | 데스크톱 전용 |
|
|
//!
|
|
//! 두 백엔드 모두 동일한 공개 API를 제공한다. 테스트 4층 전부 양쪽에서 통과.
|
|
//!
|
|
//! ## Sprint 8: manual dirty-tracking
|
|
//! HashMap cache + `HashSet<FeatureId>` dirty set. Feature 단위 쿼리.
|
|
//!
|
|
//! ## Sprint 15: 전 Feature 타입으로 확장
|
|
//! Girder 전용 → 5종 MVP Feature (Girder·DeckSlab·Bearing·Pier·Abutment).
|
|
//!
|
|
//! ## Sprint 24: salsa 0.16 optional backend (ADR-002 D)
|
|
//! `--features salsa-backend`로 활성화. WASM 호환 확인 후 기본값으로 승격 예정.
|
|
|
|
// ── Feature-gated salsa backend ───────────────────────────────────────────────
|
|
#[cfg(feature = "salsa-backend")]
|
|
pub mod salsa_db;
|
|
#[cfg(feature = "salsa-backend")]
|
|
pub use salsa_db::SalsaIncrementalDb;
|
|
|
|
use cimery_ir::{AbutmentIR, BearingIR, DeckSlabIR, FeatureId, GirderIR, PierIR};
|
|
use cimery_kernel::{GeomKernel, KernelError, Mesh};
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::sync::Arc;
|
|
|
|
// ─── IncrementalDb ────────────────────────────────────────────────────────────
|
|
|
|
/// Incremental computation database.
|
|
///
|
|
/// Holds one geometry kernel and per-kind caches for all MVP feature types.
|
|
/// In the future this becomes a salsa `Database` impl (ADR-002 D).
|
|
///
|
|
/// ## Invariants
|
|
/// - `dirty` tracks only IDs that have a corresponding IR entry.
|
|
/// - Removing a feature also removes its cache entry and dirty mark.
|
|
pub struct IncrementalDb<K: GeomKernel> {
|
|
kernel: Arc<K>,
|
|
|
|
// ── IR stores ─────────────────────────────────────────────────────────
|
|
girders: HashMap<FeatureId, GirderIR>,
|
|
decks: HashMap<FeatureId, DeckSlabIR>,
|
|
bearings: HashMap<FeatureId, BearingIR>,
|
|
piers: HashMap<FeatureId, PierIR>,
|
|
abutments: HashMap<FeatureId, AbutmentIR>,
|
|
|
|
// ── Mesh caches ───────────────────────────────────────────────────────
|
|
girder_cache: HashMap<FeatureId, Arc<Mesh>>,
|
|
deck_cache: HashMap<FeatureId, Arc<Mesh>>,
|
|
bearing_cache: HashMap<FeatureId, Arc<Mesh>>,
|
|
pier_cache: HashMap<FeatureId, Arc<Mesh>>,
|
|
abutment_cache: HashMap<FeatureId, Arc<Mesh>>,
|
|
|
|
// ── Dirty sets (per kind) ─────────────────────────────────────────────
|
|
dirty_girder: HashSet<FeatureId>,
|
|
dirty_deck: HashSet<FeatureId>,
|
|
dirty_bearing: HashSet<FeatureId>,
|
|
dirty_pier: HashSet<FeatureId>,
|
|
dirty_abutment: HashSet<FeatureId>,
|
|
}
|
|
|
|
impl<K: GeomKernel> IncrementalDb<K> {
|
|
pub fn new(kernel: K) -> Self {
|
|
Self {
|
|
kernel: Arc::new(kernel),
|
|
girders: HashMap::new(),
|
|
decks: HashMap::new(),
|
|
bearings: HashMap::new(),
|
|
piers: HashMap::new(),
|
|
abutments: HashMap::new(),
|
|
girder_cache: HashMap::new(),
|
|
deck_cache: HashMap::new(),
|
|
bearing_cache: HashMap::new(),
|
|
pier_cache: HashMap::new(),
|
|
abutment_cache:HashMap::new(),
|
|
dirty_girder: HashSet::new(),
|
|
dirty_deck: HashSet::new(),
|
|
dirty_bearing: HashSet::new(),
|
|
dirty_pier: HashSet::new(),
|
|
dirty_abutment:HashSet::new(),
|
|
}
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
// Girder
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
|
|
/// Insert or update a Girder. Marks dirty, evicts cache.
|
|
pub fn set_girder(&mut self, ir: GirderIR) {
|
|
let id = ir.id;
|
|
self.girders.insert(id, ir);
|
|
self.girder_cache.remove(&id);
|
|
self.dirty_girder.insert(id);
|
|
}
|
|
|
|
/// Remove a Girder and clear its cache/dirty state.
|
|
pub fn remove_girder(&mut self, id: &FeatureId) {
|
|
self.girders.remove(id);
|
|
self.girder_cache.remove(id);
|
|
self.dirty_girder.remove(id);
|
|
}
|
|
|
|
/// Query mesh for a Girder (cache-first).
|
|
pub fn girder_mesh(&mut self, id: &FeatureId) -> Result<Arc<Mesh>, KernelError> {
|
|
if !self.dirty_girder.contains(id) {
|
|
if let Some(cached) = self.girder_cache.get(id) {
|
|
return Ok(Arc::clone(cached));
|
|
}
|
|
}
|
|
let ir = self.girders.get(id).ok_or_else(|| {
|
|
KernelError::InvalidInput(format!("unknown Girder FeatureId: {}", id))
|
|
})?;
|
|
let mesh = Arc::new(self.kernel.girder_mesh(ir)?);
|
|
self.girder_cache.insert(*id, Arc::clone(&mesh));
|
|
self.dirty_girder.remove(id);
|
|
Ok(mesh)
|
|
}
|
|
|
|
/// Raw IR lookup (no computation).
|
|
pub fn get_girder(&self, id: &FeatureId) -> Option<&GirderIR> {
|
|
self.girders.get(id)
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
// DeckSlab
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
|
|
pub fn set_deck_slab(&mut self, ir: DeckSlabIR) {
|
|
let id = ir.id;
|
|
self.decks.insert(id, ir);
|
|
self.deck_cache.remove(&id);
|
|
self.dirty_deck.insert(id);
|
|
}
|
|
|
|
pub fn remove_deck_slab(&mut self, id: &FeatureId) {
|
|
self.decks.remove(id);
|
|
self.deck_cache.remove(id);
|
|
self.dirty_deck.remove(id);
|
|
}
|
|
|
|
pub fn deck_slab_mesh(&mut self, id: &FeatureId) -> Result<Arc<Mesh>, KernelError> {
|
|
if !self.dirty_deck.contains(id) {
|
|
if let Some(cached) = self.deck_cache.get(id) {
|
|
return Ok(Arc::clone(cached));
|
|
}
|
|
}
|
|
let ir = self.decks.get(id).ok_or_else(|| {
|
|
KernelError::InvalidInput(format!("unknown DeckSlab FeatureId: {}", id))
|
|
})?;
|
|
let mesh = Arc::new(self.kernel.deck_slab_mesh(ir)?);
|
|
self.deck_cache.insert(*id, Arc::clone(&mesh));
|
|
self.dirty_deck.remove(id);
|
|
Ok(mesh)
|
|
}
|
|
|
|
pub fn get_deck_slab(&self, id: &FeatureId) -> Option<&DeckSlabIR> {
|
|
self.decks.get(id)
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
// Bearing
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
|
|
pub fn set_bearing(&mut self, ir: BearingIR) {
|
|
let id = ir.id;
|
|
self.bearings.insert(id, ir);
|
|
self.bearing_cache.remove(&id);
|
|
self.dirty_bearing.insert(id);
|
|
}
|
|
|
|
pub fn remove_bearing(&mut self, id: &FeatureId) {
|
|
self.bearings.remove(id);
|
|
self.bearing_cache.remove(id);
|
|
self.dirty_bearing.remove(id);
|
|
}
|
|
|
|
pub fn bearing_mesh(&mut self, id: &FeatureId) -> Result<Arc<Mesh>, KernelError> {
|
|
if !self.dirty_bearing.contains(id) {
|
|
if let Some(cached) = self.bearing_cache.get(id) {
|
|
return Ok(Arc::clone(cached));
|
|
}
|
|
}
|
|
let ir = self.bearings.get(id).ok_or_else(|| {
|
|
KernelError::InvalidInput(format!("unknown Bearing FeatureId: {}", id))
|
|
})?;
|
|
let mesh = Arc::new(self.kernel.bearing_mesh(ir)?);
|
|
self.bearing_cache.insert(*id, Arc::clone(&mesh));
|
|
self.dirty_bearing.remove(id);
|
|
Ok(mesh)
|
|
}
|
|
|
|
pub fn get_bearing(&self, id: &FeatureId) -> Option<&BearingIR> {
|
|
self.bearings.get(id)
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
// Pier
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
|
|
pub fn set_pier(&mut self, ir: PierIR) {
|
|
let id = ir.id;
|
|
self.piers.insert(id, ir);
|
|
self.pier_cache.remove(&id);
|
|
self.dirty_pier.insert(id);
|
|
}
|
|
|
|
pub fn remove_pier(&mut self, id: &FeatureId) {
|
|
self.piers.remove(id);
|
|
self.pier_cache.remove(id);
|
|
self.dirty_pier.remove(id);
|
|
}
|
|
|
|
pub fn pier_mesh(&mut self, id: &FeatureId) -> Result<Arc<Mesh>, KernelError> {
|
|
if !self.dirty_pier.contains(id) {
|
|
if let Some(cached) = self.pier_cache.get(id) {
|
|
return Ok(Arc::clone(cached));
|
|
}
|
|
}
|
|
let ir = self.piers.get(id).ok_or_else(|| {
|
|
KernelError::InvalidInput(format!("unknown Pier FeatureId: {}", id))
|
|
})?;
|
|
let mesh = Arc::new(self.kernel.pier_mesh(ir)?);
|
|
self.pier_cache.insert(*id, Arc::clone(&mesh));
|
|
self.dirty_pier.remove(id);
|
|
Ok(mesh)
|
|
}
|
|
|
|
pub fn get_pier(&self, id: &FeatureId) -> Option<&PierIR> {
|
|
self.piers.get(id)
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
// Abutment
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
|
|
pub fn set_abutment(&mut self, ir: AbutmentIR) {
|
|
let id = ir.id;
|
|
self.abutments.insert(id, ir);
|
|
self.abutment_cache.remove(&id);
|
|
self.dirty_abutment.insert(id);
|
|
}
|
|
|
|
pub fn remove_abutment(&mut self, id: &FeatureId) {
|
|
self.abutments.remove(id);
|
|
self.abutment_cache.remove(id);
|
|
self.dirty_abutment.remove(id);
|
|
}
|
|
|
|
pub fn abutment_mesh(&mut self, id: &FeatureId) -> Result<Arc<Mesh>, KernelError> {
|
|
if !self.dirty_abutment.contains(id) {
|
|
if let Some(cached) = self.abutment_cache.get(id) {
|
|
return Ok(Arc::clone(cached));
|
|
}
|
|
}
|
|
let ir = self.abutments.get(id).ok_or_else(|| {
|
|
KernelError::InvalidInput(format!("unknown Abutment FeatureId: {}", id))
|
|
})?;
|
|
let mesh = Arc::new(self.kernel.abutment_mesh(ir)?);
|
|
self.abutment_cache.insert(*id, Arc::clone(&mesh));
|
|
self.dirty_abutment.remove(id);
|
|
Ok(mesh)
|
|
}
|
|
|
|
pub fn get_abutment(&self, id: &FeatureId) -> Option<&AbutmentIR> {
|
|
self.abutments.get(id)
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
// Status / diagnostics
|
|
// ──────────────────────────────────────────────────────────────────────────
|
|
|
|
/// Total features awaiting recomputation (all kinds).
|
|
pub fn dirty_count(&self) -> usize {
|
|
self.dirty_girder.len()
|
|
+ self.dirty_deck.len()
|
|
+ self.dirty_bearing.len()
|
|
+ self.dirty_pier.len()
|
|
+ self.dirty_abutment.len()
|
|
}
|
|
|
|
/// Dirty Girders only (kept for backward-compat).
|
|
pub fn dirty_girder_count(&self) -> usize { self.dirty_girder.len() }
|
|
|
|
pub fn girder_count(&self) -> usize { self.girders.len() }
|
|
pub fn deck_count(&self) -> usize { self.decks.len() }
|
|
pub fn bearing_count(&self) -> usize { self.bearings.len() }
|
|
pub fn pier_count(&self) -> usize { self.piers.len() }
|
|
pub fn abutment_count(&self) -> usize { self.abutments.len() }
|
|
|
|
/// Clear all caches (force full recompute on next access).
|
|
pub fn invalidate_all(&mut self) {
|
|
for id in self.girders.keys() { self.dirty_girder.insert(*id); }
|
|
for id in self.decks.keys() { self.dirty_deck.insert(*id); }
|
|
for id in self.bearings.keys() { self.dirty_bearing.insert(*id); }
|
|
for id in self.piers.keys() { self.dirty_pier.insert(*id); }
|
|
for id in self.abutments.keys() { self.dirty_abutment.insert(*id); }
|
|
self.girder_cache.clear();
|
|
self.deck_cache.clear();
|
|
self.bearing_cache.clear();
|
|
self.pier_cache.clear();
|
|
self.abutment_cache.clear();
|
|
}
|
|
}
|
|
|
|
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use cimery_core::{AbutmentType, BearingType, MaterialGrade, PierType, ColumnShape, SectionType};
|
|
use cimery_ir::{
|
|
AbutmentIR, BearingIR, CapBeamIR, DeckSlabIR, FeatureId, GirderIR,
|
|
PierIR, PscISectionParams, SectionParams, WingWallIR,
|
|
};
|
|
use cimery_kernel::StubKernel;
|
|
|
|
fn make_girder(station_start: f64, station_end: f64) -> GirderIR {
|
|
GirderIR {
|
|
id: FeatureId::new(), station_start, station_end,
|
|
offset_from_alignment: 0.0, section_type: SectionType::PscI,
|
|
section: SectionParams::PscI(PscISectionParams::kds_standard()),
|
|
count: 1, spacing: 0.0, material: MaterialGrade::C50,
|
|
}
|
|
}
|
|
|
|
fn make_deck() -> DeckSlabIR {
|
|
DeckSlabIR {
|
|
id: FeatureId::new(), station_start: 0.0, station_end: 40.0,
|
|
width_left: 6_000.0, width_right: 6_000.0,
|
|
thickness: 220.0, haunch_depth: 0.0, cross_slope: 2.0,
|
|
material: MaterialGrade::C40,
|
|
}
|
|
}
|
|
|
|
fn make_bearing() -> BearingIR {
|
|
BearingIR {
|
|
id: FeatureId::new(), station: 0.0,
|
|
bearing_type: BearingType::Elastomeric,
|
|
plan_length: 350.0, plan_width: 450.0,
|
|
total_height: 60.0, capacity_vertical: 1_500.0,
|
|
}
|
|
}
|
|
|
|
fn make_pier() -> PierIR {
|
|
PierIR {
|
|
id: FeatureId::new(), station: 20.0, skew_angle: 0.0,
|
|
pier_type: PierType::SingleColumn,
|
|
column_shape: ColumnShape::Circular,
|
|
column_count: 2, column_spacing: 3_000.0,
|
|
column_diameter: 1_500.0, column_depth: 0.0,
|
|
column_height: 8_000.0,
|
|
cap_beam: CapBeamIR {
|
|
length: 7_000.0, width: 1_500.0, depth: 1_200.0,
|
|
cantilever_left: 500.0, cantilever_right: 500.0,
|
|
},
|
|
material: MaterialGrade::C40,
|
|
}
|
|
}
|
|
|
|
fn make_abutment() -> AbutmentIR {
|
|
AbutmentIR {
|
|
id: FeatureId::new(), station: 0.0, skew_angle: 0.0,
|
|
abutment_type: AbutmentType::ReverseT,
|
|
breast_wall_height: 3_000.0, breast_wall_thickness: 800.0,
|
|
breast_wall_width: 12_000.0, footing_length: 4_000.0,
|
|
footing_width: 13_000.0, footing_thickness: 1_000.0,
|
|
wing_wall_left: WingWallIR { length: 5_000.0, height: 2_500.0, thickness: 500.0 },
|
|
wing_wall_right: WingWallIR { length: 5_000.0, height: 2_500.0, thickness: 500.0 },
|
|
material: MaterialGrade::C40,
|
|
}
|
|
}
|
|
|
|
// ── Girder (backward-compat tests) ────────────────────────────────────────
|
|
#[test]
|
|
fn dirty_after_set_girder() {
|
|
let mut db = IncrementalDb::new(StubKernel);
|
|
let ir = make_girder(0.0, 40.0);
|
|
let id = ir.id;
|
|
db.set_girder(ir);
|
|
assert_eq!(db.dirty_count(), 1);
|
|
assert_eq!(db.girder_count(), 1);
|
|
assert!(db.get_girder(&id).is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn clean_after_compute_girder() {
|
|
let mut db = IncrementalDb::new(StubKernel);
|
|
let ir = make_girder(0.0, 40.0);
|
|
let id = ir.id;
|
|
db.set_girder(ir);
|
|
db.girder_mesh(&id).unwrap();
|
|
assert_eq!(db.dirty_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn cache_hit_girder() {
|
|
let mut db = IncrementalDb::new(StubKernel);
|
|
let ir = make_girder(0.0, 40.0);
|
|
let id = ir.id;
|
|
db.set_girder(ir);
|
|
let m1 = db.girder_mesh(&id).unwrap();
|
|
let m2 = db.girder_mesh(&id).unwrap();
|
|
assert!(Arc::ptr_eq(&m1, &m2));
|
|
}
|
|
|
|
#[test]
|
|
fn invalidation_on_update_girder() {
|
|
let mut db = IncrementalDb::new(StubKernel);
|
|
let ir = make_girder(0.0, 40.0);
|
|
let id = ir.id;
|
|
db.set_girder(ir.clone());
|
|
db.girder_mesh(&id).unwrap();
|
|
assert_eq!(db.dirty_count(), 0);
|
|
let mut ir2 = ir;
|
|
ir2.station_end = 50.0;
|
|
db.set_girder(ir2);
|
|
assert_eq!(db.dirty_count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_id_girder_error() {
|
|
let mut db = IncrementalDb::new(StubKernel);
|
|
let err = db.girder_mesh(&FeatureId::new());
|
|
assert!(matches!(err, Err(KernelError::InvalidInput(_))));
|
|
}
|
|
|
|
// ── DeckSlab ───────────────────────────────────────────────────────────────
|
|
#[test]
|
|
fn deck_slab_dirty_compute_cache() {
|
|
let mut db = IncrementalDb::new(StubKernel);
|
|
let ir = make_deck();
|
|
let id = ir.id;
|
|
db.set_deck_slab(ir);
|
|
assert_eq!(db.dirty_count(), 1);
|
|
let m1 = db.deck_slab_mesh(&id).unwrap();
|
|
assert_eq!(db.dirty_count(), 0);
|
|
let m2 = db.deck_slab_mesh(&id).unwrap();
|
|
assert!(Arc::ptr_eq(&m1, &m2));
|
|
}
|
|
|
|
// ── Bearing ────────────────────────────────────────────────────────────────
|
|
#[test]
|
|
fn bearing_dirty_compute_cache() {
|
|
let mut db = IncrementalDb::new(StubKernel);
|
|
let ir = make_bearing();
|
|
let id = ir.id;
|
|
db.set_bearing(ir);
|
|
assert_eq!(db.dirty_count(), 1);
|
|
db.bearing_mesh(&id).unwrap();
|
|
assert_eq!(db.dirty_count(), 0);
|
|
}
|
|
|
|
// ── Pier ───────────────────────────────────────────────────────────────────
|
|
#[test]
|
|
fn pier_dirty_compute_cache() {
|
|
let mut db = IncrementalDb::new(StubKernel);
|
|
let ir = make_pier();
|
|
let id = ir.id;
|
|
db.set_pier(ir);
|
|
assert_eq!(db.dirty_count(), 1);
|
|
db.pier_mesh(&id).unwrap();
|
|
assert_eq!(db.dirty_count(), 0);
|
|
}
|
|
|
|
// ── Abutment ───────────────────────────────────────────────────────────────
|
|
#[test]
|
|
fn abutment_dirty_compute_cache() {
|
|
let mut db = IncrementalDb::new(StubKernel);
|
|
let ir = make_abutment();
|
|
let id = ir.id;
|
|
db.set_abutment(ir);
|
|
assert_eq!(db.dirty_count(), 1);
|
|
db.abutment_mesh(&id).unwrap();
|
|
assert_eq!(db.dirty_count(), 0);
|
|
}
|
|
|
|
// ── Multi-feature dirty count ──────────────────────────────────────────────
|
|
#[test]
|
|
fn total_dirty_count_all_features() {
|
|
let mut db = IncrementalDb::new(StubKernel);
|
|
db.set_girder(make_girder(0.0, 40.0));
|
|
db.set_deck_slab(make_deck());
|
|
db.set_bearing(make_bearing());
|
|
db.set_pier(make_pier());
|
|
db.set_abutment(make_abutment());
|
|
assert_eq!(db.dirty_count(), 5);
|
|
}
|
|
|
|
#[test]
|
|
fn invalidate_all_re_dirties() {
|
|
let mut db = IncrementalDb::new(StubKernel);
|
|
let g = make_girder(0.0, 40.0);
|
|
let gid = g.id;
|
|
db.set_girder(g);
|
|
db.girder_mesh(&gid).unwrap();
|
|
assert_eq!(db.dirty_count(), 0);
|
|
|
|
db.invalidate_all();
|
|
assert_eq!(db.dirty_count(), 1);
|
|
}
|
|
|
|
// ── Remove feature ─────────────────────────────────────────────────────────
|
|
#[test]
|
|
fn remove_girder_clears_all() {
|
|
let mut db = IncrementalDb::new(StubKernel);
|
|
let ir = make_girder(0.0, 40.0);
|
|
let id = ir.id;
|
|
db.set_girder(ir);
|
|
db.remove_girder(&id);
|
|
assert_eq!(db.girder_count(), 0);
|
|
assert_eq!(db.dirty_count(), 0);
|
|
assert!(matches!(db.girder_mesh(&id), Err(KernelError::InvalidInput(_))));
|
|
}
|
|
}
|
|
|
|
// ─── SalsaIncrementalDb tests (salsa-backend feature) ────────────────────────
|
|
//
|
|
// Mirror of the manual-tracking tests above.
|
|
// Both backends must satisfy the same behavioural contract.
|
|
#[cfg(all(test, feature = "salsa-backend"))]
|
|
mod salsa_tests {
|
|
use super::*;
|
|
use crate::salsa_db::SalsaIncrementalDb;
|
|
use cimery_core::{AbutmentType, BearingType, ColumnShape, MaterialGrade, PierType, SectionType};
|
|
use cimery_ir::{
|
|
AbutmentIR, BearingIR, CapBeamIR, DeckSlabIR, FeatureId, GirderIR,
|
|
PierIR, PscISectionParams, SectionParams, WingWallIR,
|
|
};
|
|
use cimery_kernel::StubKernel;
|
|
use std::sync::Arc;
|
|
|
|
fn make_girder(station_start: f64, station_end: f64) -> GirderIR {
|
|
GirderIR {
|
|
id: FeatureId::new(), station_start, station_end,
|
|
offset_from_alignment: 0.0, section_type: SectionType::PscI,
|
|
section: SectionParams::PscI(PscISectionParams::kds_standard()),
|
|
count: 1, spacing: 0.0, material: MaterialGrade::C50,
|
|
}
|
|
}
|
|
|
|
fn make_deck() -> DeckSlabIR {
|
|
DeckSlabIR {
|
|
id: FeatureId::new(), station_start: 0.0, station_end: 40.0,
|
|
width_left: 6_000.0, width_right: 6_000.0,
|
|
thickness: 220.0, haunch_depth: 0.0, cross_slope: 2.0,
|
|
material: MaterialGrade::C40,
|
|
}
|
|
}
|
|
|
|
fn make_bearing() -> BearingIR {
|
|
BearingIR {
|
|
id: FeatureId::new(), station: 0.0,
|
|
bearing_type: BearingType::Elastomeric,
|
|
plan_length: 350.0, plan_width: 450.0,
|
|
total_height: 60.0, capacity_vertical: 1_500.0,
|
|
}
|
|
}
|
|
|
|
fn make_pier() -> PierIR {
|
|
PierIR {
|
|
id: FeatureId::new(), station: 20.0, skew_angle: 0.0,
|
|
pier_type: PierType::SingleColumn,
|
|
column_shape: ColumnShape::Circular,
|
|
column_count: 2, column_spacing: 3_000.0,
|
|
column_diameter: 1_500.0, column_depth: 0.0,
|
|
column_height: 8_000.0,
|
|
cap_beam: CapBeamIR {
|
|
length: 7_000.0, width: 1_500.0, depth: 1_200.0,
|
|
cantilever_left: 500.0, cantilever_right: 500.0,
|
|
},
|
|
material: MaterialGrade::C40,
|
|
}
|
|
}
|
|
|
|
fn make_abutment() -> AbutmentIR {
|
|
AbutmentIR {
|
|
id: FeatureId::new(), station: 0.0, skew_angle: 0.0,
|
|
abutment_type: AbutmentType::ReverseT,
|
|
breast_wall_height: 3_000.0, breast_wall_thickness: 800.0,
|
|
breast_wall_width: 12_000.0, footing_length: 4_000.0,
|
|
footing_width: 13_000.0, footing_thickness: 1_000.0,
|
|
wing_wall_left: WingWallIR { length: 5_000.0, height: 2_500.0, thickness: 500.0 },
|
|
wing_wall_right: WingWallIR { length: 5_000.0, height: 2_500.0, thickness: 500.0 },
|
|
material: MaterialGrade::C40,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn salsa_dirty_after_set_girder() {
|
|
let mut db = SalsaIncrementalDb::new(StubKernel);
|
|
let ir = make_girder(0.0, 40.0);
|
|
let id = ir.id;
|
|
db.set_girder(ir);
|
|
assert_eq!(db.dirty_count(), 1);
|
|
assert_eq!(db.girder_count(), 1);
|
|
assert!(db.get_girder(&id).is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn salsa_clean_after_compute_girder() {
|
|
let mut db = SalsaIncrementalDb::new(StubKernel);
|
|
let ir = make_girder(0.0, 40.0);
|
|
let id = ir.id;
|
|
db.set_girder(ir);
|
|
db.girder_mesh(&id).unwrap();
|
|
assert_eq!(db.dirty_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn salsa_cache_hit_girder() {
|
|
let mut db = SalsaIncrementalDb::new(StubKernel);
|
|
let ir = make_girder(0.0, 40.0);
|
|
let id = ir.id;
|
|
db.set_girder(ir);
|
|
let m1 = db.girder_mesh(&id).unwrap();
|
|
let m2 = db.girder_mesh(&id).unwrap();
|
|
assert!(Arc::ptr_eq(&m1, &m2));
|
|
}
|
|
|
|
#[test]
|
|
fn salsa_invalidation_on_update_girder() {
|
|
let mut db = SalsaIncrementalDb::new(StubKernel);
|
|
let ir = make_girder(0.0, 40.0);
|
|
let id = ir.id;
|
|
db.set_girder(ir.clone());
|
|
db.girder_mesh(&id).unwrap();
|
|
assert_eq!(db.dirty_count(), 0);
|
|
let mut ir2 = ir;
|
|
ir2.station_end = 50.0;
|
|
db.set_girder(ir2);
|
|
assert_eq!(db.dirty_count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn salsa_unknown_id_error() {
|
|
let mut db = SalsaIncrementalDb::new(StubKernel);
|
|
// Query against unregistered id (salsa input is never set)
|
|
// We avoid calling girder_mesh with a salsa-unknown id to prevent panic.
|
|
// Instead verify via girder_count.
|
|
assert_eq!(db.girder_count(), 0);
|
|
assert_eq!(db.dirty_count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn salsa_total_dirty_count_all_features() {
|
|
let mut db = SalsaIncrementalDb::new(StubKernel);
|
|
db.set_girder(make_girder(0.0, 40.0));
|
|
db.set_deck_slab(make_deck());
|
|
db.set_bearing(make_bearing());
|
|
db.set_pier(make_pier());
|
|
db.set_abutment(make_abutment());
|
|
assert_eq!(db.dirty_count(), 5);
|
|
}
|
|
|
|
#[test]
|
|
fn salsa_invalidate_all_re_dirties() {
|
|
let mut db = SalsaIncrementalDb::new(StubKernel);
|
|
let g = make_girder(0.0, 40.0);
|
|
let gid = g.id;
|
|
db.set_girder(g);
|
|
db.girder_mesh(&gid).unwrap();
|
|
assert_eq!(db.dirty_count(), 0);
|
|
db.invalidate_all();
|
|
assert_eq!(db.dirty_count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn salsa_remove_girder_clears_all() {
|
|
let mut db = SalsaIncrementalDb::new(StubKernel);
|
|
let ir = make_girder(0.0, 40.0);
|
|
let id = ir.id;
|
|
db.set_girder(ir);
|
|
db.remove_girder(&id);
|
|
assert_eq!(db.girder_count(), 0);
|
|
assert_eq!(db.dirty_count(), 0);
|
|
}
|
|
}
|