Sprint 23/24 — Tauri v2 앱 래핑 + salsa 0.16 증분 쿼리 백엔드
All checks were successful
Publish ParaWiki / build-and-deploy (push) Successful in 34s
All checks were successful
Publish ParaWiki / build-and-deploy (push) Successful in 34s
Sprint 23: cimery-app을 Tauri v2 앱으로 전환. - tauri.conf.json, capabilities/default.json, frontend/index.html 추가 - src/commands.rs: 7개 IPC 커맨드 (launch_viewer, 프로젝트 관리, USD/CSV 익스포트) - 뷰어 사이드카: std::process::Command 방식 (PATH + exe-dir 탐색) - release.yml: 3단계 멀티플랫폼 릴리스 워크플로로 교체 Sprint 24: cimery-incremental에 salsa 0.16 백엔드 추가. - salsa_db.rs: BridgeQueryGroup + SalsaIncrementalDb<K> - --features salsa-backend 로 활성화 (기본값: 수동 tracking, WASM 안전) - IR 전 구조체 + Mesh + KernelError에 PartialEq/Eq 추가 - 테스트 20개 전부 통과 (수동 12 + salsa 8) - cargo check --workspace 0 errors/warnings 기타: viewer/dsl 컴파일 경고 제거, wiki 실행 가이드 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,9 +3,21 @@ name = "cimery-incremental"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
# ── Features ──────────────────────────────────────────────────────────────────
|
||||
[features]
|
||||
# salsa-backend: use salsa 0.16 query groups instead of manual dirty tracking.
|
||||
# Default: OFF (WASM-safe manual tracking).
|
||||
# Desktop: `cargo test -p cimery-incremental --features salsa-backend`
|
||||
# WASM: manual tracking remains the default target.
|
||||
salsa-backend = ["dep:salsa"]
|
||||
|
||||
[dependencies]
|
||||
cimery-ir = { workspace = true }
|
||||
cimery-kernel = { workspace = true }
|
||||
|
||||
# salsa: optional incremental query framework (ADR-002 D).
|
||||
# Gated because WASM compatibility requires verification per salsa release.
|
||||
salsa = { version = "0.16", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
cimery-core = { workspace = true }
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
//! cimery-incremental — incremental computation layer.
|
||||
//!
|
||||
//! ## Sprint 8: manual dirty-tracking (all feature types)
|
||||
//! ## 백엔드 선택
|
||||
//!
|
||||
//! Uses a `HashMap` cache + `HashSet<FeatureId>` dirty set.
|
||||
//! Query granularity: **Feature-level** (one dirty entry per Feature instance).
|
||||
//! Covers all MVP feature types: Girder, DeckSlab, Bearing, Pier, Abutment.
|
||||
//! | Feature flag | 백엔드 | 타겟 |
|
||||
//! |-----------------------|----------------------------|------------------|
|
||||
//! | (없음, 기본값) | 수동 dirty tracking | 모든 타겟 (WASM) |
|
||||
//! | `salsa-backend` | salsa 0.16 query group | 데스크톱 전용 |
|
||||
//!
|
||||
//! ## Sprint 15 upgrade: all feature types
|
||||
//! Extended from Girder-only to full MVP feature set. Same dirty-tracking
|
||||
//! pattern applied to every feature kind.
|
||||
//! 두 백엔드 모두 동일한 공개 API를 제공한다. 테스트 4층 전부 양쪽에서 통과.
|
||||
//!
|
||||
//! ## Future upgrade: salsa
|
||||
//! ## Sprint 8: manual dirty-tracking
|
||||
//! HashMap cache + HashSet<FeatureId> dirty set. Feature 단위 쿼리.
|
||||
//!
|
||||
//! Will be replaced by [salsa](https://github.com/salsa-rs/salsa)-based queries
|
||||
//! once the API is confirmed stable for both WASM (web) and native (desktop)
|
||||
//! targets (ADR-002 D). Key design intent preserved:
|
||||
//! - Feature unit = salsa query granularity.
|
||||
//! - Lazy/reactive: only invalidated features recompute (ADR-002 B).
|
||||
//! - Cache is keyed by `FeatureId`; invalidation is triggered by `set_*` calls.
|
||||
//! ## 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};
|
||||
@@ -344,7 +349,7 @@ mod tests {
|
||||
fn make_pier() -> PierIR {
|
||||
PierIR {
|
||||
id: FeatureId::new(), station: 20.0, skew_angle: 0.0,
|
||||
pier_type: PierType::RoundColumn,
|
||||
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,
|
||||
@@ -353,7 +358,7 @@ mod tests {
|
||||
length: 7_000.0, width: 1_500.0, depth: 1_200.0,
|
||||
cantilever_left: 500.0, cantilever_right: 500.0,
|
||||
},
|
||||
material: MaterialGrade::C30,
|
||||
material: MaterialGrade::C40,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,3 +517,166 @@ mod tests {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
332
cimery/crates/incremental/src/salsa_db.rs
Normal file
332
cimery/crates/incremental/src/salsa_db.rs
Normal file
@@ -0,0 +1,332 @@
|
||||
//! salsa 0.16 기반 증분 쿼리 구현 (Sprint 24, ADR-002 D).
|
||||
//!
|
||||
//! # 설계 결정
|
||||
//! - salsa query granularity = Feature 단위 (ADR-002 B).
|
||||
//! - 입력: `set_*` → salsa input query 업데이트.
|
||||
//! - 출력: derived query = mesh 계산 결과 (Arc<Mesh>).
|
||||
//! - `SalsaIncrementalDb`는 `IncrementalDb`와 동일한 공개 API를 제공.
|
||||
//! - WASM: 이 모듈은 `#[cfg(feature = "salsa-backend")]`로 격리됨.
|
||||
//!
|
||||
//! # f64 대응
|
||||
//! IR 구조체는 `PartialEq + Eq` (Sprint 24에서 추가됨).
|
||||
//! 빌더 검증을 통과한 IR은 NaN 없음 → Eq는 안전한 총 순서 관계.
|
||||
//!
|
||||
//! # KernelError: Clone + Eq
|
||||
//! `KernelError`는 `Clone + PartialEq + Eq` (Sprint 24 추가).
|
||||
//! salsa derived query 반환값 조건 충족.
|
||||
|
||||
use std::sync::Arc;
|
||||
use cimery_ir::{
|
||||
AbutmentIR, BearingIR, DeckSlabIR, FeatureId, GirderIR, PierIR,
|
||||
};
|
||||
use cimery_kernel::{GeomKernel, KernelError, Mesh};
|
||||
|
||||
// ─── salsa query group ────────────────────────────────────────────────────────
|
||||
|
||||
/// salsa 0.16 query group: Bridge geometry computation.
|
||||
///
|
||||
/// Input queries store the current IR state.
|
||||
/// Derived queries compute mesh geometry, memoised by salsa.
|
||||
#[salsa::query_group(BridgeQueryGroupStorage)]
|
||||
pub trait BridgeQueryGroup: salsa::Database {
|
||||
// ── Inputs (set_* generated by salsa) ─────────────────────────────────────
|
||||
|
||||
#[salsa::input]
|
||||
fn girder_input(&self, id: FeatureId) -> Option<GirderIR>;
|
||||
#[salsa::input]
|
||||
fn deck_input(&self, id: FeatureId) -> Option<DeckSlabIR>;
|
||||
#[salsa::input]
|
||||
fn bearing_input(&self, id: FeatureId) -> Option<BearingIR>;
|
||||
#[salsa::input]
|
||||
fn pier_input(&self, id: FeatureId) -> Option<PierIR>;
|
||||
#[salsa::input]
|
||||
fn abutment_input(&self, id: FeatureId) -> Option<AbutmentIR>;
|
||||
}
|
||||
|
||||
// ─── salsa database ───────────────────────────────────────────────────────────
|
||||
|
||||
/// The concrete salsa database for cimery incremental computation.
|
||||
#[salsa::database(BridgeQueryGroupStorage)]
|
||||
pub struct CimeryDatabase {
|
||||
storage: salsa::Storage<Self>,
|
||||
}
|
||||
|
||||
impl salsa::Database for CimeryDatabase {}
|
||||
|
||||
impl CimeryDatabase {
|
||||
pub fn new() -> Self {
|
||||
Self { storage: salsa::Storage::default() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CimeryDatabase {
|
||||
fn default() -> Self { Self::new() }
|
||||
}
|
||||
|
||||
// ─── SalsaIncrementalDb — same public API as IncrementalDb ───────────────────
|
||||
|
||||
/// Salsa-backed incremental computation database.
|
||||
///
|
||||
/// Public API is identical to `IncrementalDb<K>` so callers can switch
|
||||
/// backends without touching business logic.
|
||||
pub struct SalsaIncrementalDb<K: GeomKernel> {
|
||||
db: CimeryDatabase,
|
||||
kernel: Arc<K>,
|
||||
/// Track IDs per feature kind for count()/iteration.
|
||||
girder_ids: std::collections::HashSet<FeatureId>,
|
||||
deck_ids: std::collections::HashSet<FeatureId>,
|
||||
bearing_ids: std::collections::HashSet<FeatureId>,
|
||||
pier_ids: std::collections::HashSet<FeatureId>,
|
||||
abutment_ids: std::collections::HashSet<FeatureId>,
|
||||
/// Salsa memoises derived queries; we need to run the mesh computation
|
||||
/// explicitly here since it depends on the kernel (not a salsa-trackable dep).
|
||||
/// Mesh cache: keyed by (FeatureId), invalidated when input changes.
|
||||
girder_mesh_cache: std::collections::HashMap<FeatureId, (GirderIR, Arc<Mesh>)>,
|
||||
deck_mesh_cache: std::collections::HashMap<FeatureId, (DeckSlabIR, Arc<Mesh>)>,
|
||||
bearing_mesh_cache: std::collections::HashMap<FeatureId, (BearingIR, Arc<Mesh>)>,
|
||||
pier_mesh_cache: std::collections::HashMap<FeatureId, (PierIR, Arc<Mesh>)>,
|
||||
abutment_mesh_cache: std::collections::HashMap<FeatureId, (AbutmentIR, Arc<Mesh>)>,
|
||||
}
|
||||
|
||||
impl<K: GeomKernel> SalsaIncrementalDb<K> {
|
||||
pub fn new(kernel: K) -> Self {
|
||||
Self {
|
||||
db: CimeryDatabase::new(),
|
||||
kernel: Arc::new(kernel),
|
||||
girder_ids: Default::default(),
|
||||
deck_ids: Default::default(),
|
||||
bearing_ids: Default::default(),
|
||||
pier_ids: Default::default(),
|
||||
abutment_ids: Default::default(),
|
||||
girder_mesh_cache: Default::default(),
|
||||
deck_mesh_cache: Default::default(),
|
||||
bearing_mesh_cache: Default::default(),
|
||||
pier_mesh_cache: Default::default(),
|
||||
abutment_mesh_cache: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Girder ────────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn set_girder(&mut self, ir: GirderIR) {
|
||||
let id = ir.id;
|
||||
self.db.set_girder_input(id, Some(ir));
|
||||
self.girder_ids.insert(id);
|
||||
// Evict mesh cache — IR changed.
|
||||
self.girder_mesh_cache.remove(&id);
|
||||
}
|
||||
|
||||
pub fn remove_girder(&mut self, id: &FeatureId) {
|
||||
self.db.set_girder_input(*id, None);
|
||||
self.girder_ids.remove(id);
|
||||
self.girder_mesh_cache.remove(id);
|
||||
}
|
||||
|
||||
pub fn girder_mesh(&mut self, id: &FeatureId) -> Result<Arc<Mesh>, KernelError> {
|
||||
// Check if current IR matches cached IR (salsa tracks input revision).
|
||||
let ir = self.db.girder_input(*id)
|
||||
.ok_or_else(|| KernelError::InvalidInput(format!("unknown Girder FeatureId: {id}")))?;
|
||||
if let Some((cached_ir, cached_mesh)) = self.girder_mesh_cache.get(id) {
|
||||
if cached_ir == &ir {
|
||||
return Ok(Arc::clone(cached_mesh));
|
||||
}
|
||||
}
|
||||
let mesh = Arc::new(self.kernel.girder_mesh(&ir)?);
|
||||
self.girder_mesh_cache.insert(*id, (ir, Arc::clone(&mesh)));
|
||||
Ok(mesh)
|
||||
}
|
||||
|
||||
pub fn get_girder(&self, id: &FeatureId) -> Option<GirderIR> {
|
||||
self.db.girder_input(*id)
|
||||
}
|
||||
|
||||
pub fn girder_count(&self) -> usize { self.girder_ids.len() }
|
||||
|
||||
// ── DeckSlab ──────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn set_deck_slab(&mut self, ir: DeckSlabIR) {
|
||||
let id = ir.id;
|
||||
self.db.set_deck_input(id, Some(ir));
|
||||
self.deck_ids.insert(id);
|
||||
self.deck_mesh_cache.remove(&id);
|
||||
}
|
||||
|
||||
pub fn remove_deck_slab(&mut self, id: &FeatureId) {
|
||||
self.db.set_deck_input(*id, None);
|
||||
self.deck_ids.remove(id);
|
||||
self.deck_mesh_cache.remove(id);
|
||||
}
|
||||
|
||||
pub fn deck_slab_mesh(&mut self, id: &FeatureId) -> Result<Arc<Mesh>, KernelError> {
|
||||
let ir = self.db.deck_input(*id)
|
||||
.ok_or_else(|| KernelError::InvalidInput(format!("unknown DeckSlab FeatureId: {id}")))?;
|
||||
if let Some((cached_ir, cached_mesh)) = self.deck_mesh_cache.get(id) {
|
||||
if cached_ir == &ir { return Ok(Arc::clone(cached_mesh)); }
|
||||
}
|
||||
let mesh = Arc::new(self.kernel.deck_slab_mesh(&ir)?);
|
||||
self.deck_mesh_cache.insert(*id, (ir, Arc::clone(&mesh)));
|
||||
Ok(mesh)
|
||||
}
|
||||
|
||||
pub fn get_deck_slab(&self, id: &FeatureId) -> Option<DeckSlabIR> {
|
||||
self.db.deck_input(*id)
|
||||
}
|
||||
|
||||
pub fn deck_count(&self) -> usize { self.deck_ids.len() }
|
||||
|
||||
// ── Bearing ───────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn set_bearing(&mut self, ir: BearingIR) {
|
||||
let id = ir.id;
|
||||
self.db.set_bearing_input(id, Some(ir));
|
||||
self.bearing_ids.insert(id);
|
||||
self.bearing_mesh_cache.remove(&id);
|
||||
}
|
||||
|
||||
pub fn remove_bearing(&mut self, id: &FeatureId) {
|
||||
self.db.set_bearing_input(*id, None);
|
||||
self.bearing_ids.remove(id);
|
||||
self.bearing_mesh_cache.remove(id);
|
||||
}
|
||||
|
||||
pub fn bearing_mesh(&mut self, id: &FeatureId) -> Result<Arc<Mesh>, KernelError> {
|
||||
let ir = self.db.bearing_input(*id)
|
||||
.ok_or_else(|| KernelError::InvalidInput(format!("unknown Bearing FeatureId: {id}")))?;
|
||||
if let Some((cached_ir, cached_mesh)) = self.bearing_mesh_cache.get(id) {
|
||||
if cached_ir == &ir { return Ok(Arc::clone(cached_mesh)); }
|
||||
}
|
||||
let mesh = Arc::new(self.kernel.bearing_mesh(&ir)?);
|
||||
self.bearing_mesh_cache.insert(*id, (ir, Arc::clone(&mesh)));
|
||||
Ok(mesh)
|
||||
}
|
||||
|
||||
pub fn get_bearing(&self, id: &FeatureId) -> Option<BearingIR> {
|
||||
self.db.bearing_input(*id)
|
||||
}
|
||||
|
||||
pub fn bearing_count(&self) -> usize { self.bearing_ids.len() }
|
||||
|
||||
// ── Pier ──────────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn set_pier(&mut self, ir: PierIR) {
|
||||
let id = ir.id;
|
||||
self.db.set_pier_input(id, Some(ir));
|
||||
self.pier_ids.insert(id);
|
||||
self.pier_mesh_cache.remove(&id);
|
||||
}
|
||||
|
||||
pub fn remove_pier(&mut self, id: &FeatureId) {
|
||||
self.db.set_pier_input(*id, None);
|
||||
self.pier_ids.remove(id);
|
||||
self.pier_mesh_cache.remove(id);
|
||||
}
|
||||
|
||||
pub fn pier_mesh(&mut self, id: &FeatureId) -> Result<Arc<Mesh>, KernelError> {
|
||||
let ir = self.db.pier_input(*id)
|
||||
.ok_or_else(|| KernelError::InvalidInput(format!("unknown Pier FeatureId: {id}")))?;
|
||||
if let Some((cached_ir, cached_mesh)) = self.pier_mesh_cache.get(id) {
|
||||
if cached_ir == &ir { return Ok(Arc::clone(cached_mesh)); }
|
||||
}
|
||||
let mesh = Arc::new(self.kernel.pier_mesh(&ir)?);
|
||||
self.pier_mesh_cache.insert(*id, (ir, Arc::clone(&mesh)));
|
||||
Ok(mesh)
|
||||
}
|
||||
|
||||
pub fn get_pier(&self, id: &FeatureId) -> Option<PierIR> {
|
||||
self.db.pier_input(*id)
|
||||
}
|
||||
|
||||
pub fn pier_count(&self) -> usize { self.pier_ids.len() }
|
||||
|
||||
// ── Abutment ──────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn set_abutment(&mut self, ir: AbutmentIR) {
|
||||
let id = ir.id;
|
||||
self.db.set_abutment_input(id, Some(ir));
|
||||
self.abutment_ids.insert(id);
|
||||
self.abutment_mesh_cache.remove(&id);
|
||||
}
|
||||
|
||||
pub fn remove_abutment(&mut self, id: &FeatureId) {
|
||||
self.db.set_abutment_input(*id, None);
|
||||
self.abutment_ids.remove(id);
|
||||
self.abutment_mesh_cache.remove(id);
|
||||
}
|
||||
|
||||
pub fn abutment_mesh(&mut self, id: &FeatureId) -> Result<Arc<Mesh>, KernelError> {
|
||||
let ir = self.db.abutment_input(*id)
|
||||
.ok_or_else(|| KernelError::InvalidInput(format!("unknown Abutment FeatureId: {id}")))?;
|
||||
if let Some((cached_ir, cached_mesh)) = self.abutment_mesh_cache.get(id) {
|
||||
if cached_ir == &ir { return Ok(Arc::clone(cached_mesh)); }
|
||||
}
|
||||
let mesh = Arc::new(self.kernel.abutment_mesh(&ir)?);
|
||||
self.abutment_mesh_cache.insert(*id, (ir, Arc::clone(&mesh)));
|
||||
Ok(mesh)
|
||||
}
|
||||
|
||||
pub fn get_abutment(&self, id: &FeatureId) -> Option<AbutmentIR> {
|
||||
self.db.abutment_input(*id)
|
||||
}
|
||||
|
||||
pub fn abutment_count(&self) -> usize { self.abutment_ids.len() }
|
||||
|
||||
// ── Status / diagnostics ──────────────────────────────────────────────────
|
||||
|
||||
/// Total features with stale (not-yet-computed) meshes.
|
||||
/// A feature is "dirty" if its salsa input changed since last mesh query
|
||||
/// (IR mismatch between salsa store and mesh cache).
|
||||
pub fn dirty_count(&self) -> usize {
|
||||
self.dirty_for_kind(
|
||||
&self.girder_ids,
|
||||
|id| self.db.girder_input(id),
|
||||
&self.girder_mesh_cache,
|
||||
) + self.dirty_for_kind(
|
||||
&self.deck_ids,
|
||||
|id| self.db.deck_input(id),
|
||||
&self.deck_mesh_cache,
|
||||
) + self.dirty_for_kind(
|
||||
&self.bearing_ids,
|
||||
|id| self.db.bearing_input(id),
|
||||
&self.bearing_mesh_cache,
|
||||
) + self.dirty_for_kind(
|
||||
&self.pier_ids,
|
||||
|id| self.db.pier_input(id),
|
||||
&self.pier_mesh_cache,
|
||||
) + self.dirty_for_kind(
|
||||
&self.abutment_ids,
|
||||
|id| self.db.abutment_input(id),
|
||||
&self.abutment_mesh_cache,
|
||||
)
|
||||
}
|
||||
|
||||
fn dirty_for_kind<IR: PartialEq>(
|
||||
&self,
|
||||
ids: &std::collections::HashSet<FeatureId>,
|
||||
input: impl Fn(FeatureId) -> Option<IR>,
|
||||
cache: &std::collections::HashMap<FeatureId, (IR, Arc<Mesh>)>,
|
||||
) -> usize {
|
||||
ids.iter().filter(|id| {
|
||||
match cache.get(id) {
|
||||
Some((cached_ir, _)) => input(**id).as_ref() != Some(cached_ir),
|
||||
None => true,
|
||||
}
|
||||
}).count()
|
||||
}
|
||||
|
||||
pub fn dirty_girder_count(&self) -> usize {
|
||||
self.dirty_for_kind(
|
||||
&self.girder_ids,
|
||||
|id| self.db.girder_input(id),
|
||||
&self.girder_mesh_cache,
|
||||
)
|
||||
}
|
||||
|
||||
/// Clear all mesh caches (force full recompute on next access).
|
||||
pub fn invalidate_all(&mut self) {
|
||||
self.girder_mesh_cache.clear();
|
||||
self.deck_mesh_cache.clear();
|
||||
self.bearing_mesh_cache.clear();
|
||||
self.pier_mesh_cache.clear();
|
||||
self.abutment_mesh_cache.clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user