cimery Sprint 1 — Rust 워크스페이스 + 전 계층 파이프라인
8개 크레이트 구현, cargo test 32개 전부 통과: - core: Mm/M 단위 newtype, UnitExt 리터럴, FeatureError - ir: GirderIR + 전 단면 파라미터(PSC-I/U/SteelBox/PlateI) serde JSON - dsl: Girder builder + 검증 (경간 범위·count·spacing) - kernel: GeomKernel trait + StubKernel (box mesh, AABB) - incremental: dirty-tracking IncrementalDb (salsa 업그레이드 경로 주석) - evaluator: 상태 없는 IR→kernel 브리지 - usd: USDA 1.0 텍스트 익스포트 (CimeryBridgeAPI·GirderAPI schema) - viewer: wgpu 22 + winit 0.30 컬러 삼각형 (Sprint 1 proof-of-concept) Sprint 2 다음 단계: - opencascade-rs로 StubKernel 교체 (실제 PSC-I sweep) - viewer에서 Girder Mesh 렌더 + 카메라 orbit Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
176
cimery/crates/incremental/src/lib.rs
Normal file
176
cimery/crates/incremental/src/lib.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
//! cimery-incremental — incremental computation layer.
|
||||
//!
|
||||
//! ## Sprint 1: manual dirty-tracking
|
||||
//!
|
||||
//! Uses a `HashMap` cache + `HashSet<FeatureId>` dirty set.
|
||||
//! Query granularity: **Feature-level** (one dirty entry per Feature instance).
|
||||
//!
|
||||
//! ## Sprint 2 upgrade: salsa
|
||||
//!
|
||||
//! 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.
|
||||
|
||||
use cimery_ir::{FeatureId, GirderIR};
|
||||
use cimery_kernel::{GeomKernel, KernelError, Mesh};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
// ─── IncrementalDb ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Incremental computation database.
|
||||
///
|
||||
/// Holds one geometry kernel and one cache per Feature type.
|
||||
/// In Sprint 2 this becomes a salsa `Database` impl.
|
||||
pub struct IncrementalDb<K: GeomKernel> {
|
||||
kernel: Arc<K>,
|
||||
girders: HashMap<FeatureId, GirderIR>,
|
||||
mesh_cache: HashMap<FeatureId, Arc<Mesh>>,
|
||||
dirty: HashSet<FeatureId>,
|
||||
}
|
||||
|
||||
impl<K: GeomKernel> IncrementalDb<K> {
|
||||
pub fn new(kernel: K) -> Self {
|
||||
Self {
|
||||
kernel: Arc::new(kernel),
|
||||
girders: HashMap::new(),
|
||||
mesh_cache: HashMap::new(),
|
||||
dirty: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Writers ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Insert or update a Girder. Marks the feature dirty and evicts mesh cache.
|
||||
pub fn set_girder(&mut self, ir: GirderIR) {
|
||||
let id = ir.id;
|
||||
self.girders.insert(id, ir);
|
||||
self.mesh_cache.remove(&id);
|
||||
self.dirty.insert(id);
|
||||
}
|
||||
|
||||
// ── Readers ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Query the mesh for a Girder.
|
||||
///
|
||||
/// - Cache hit (not dirty) → returns `Arc<Mesh>` without recomputation.
|
||||
/// - Cache miss or dirty → calls kernel, updates cache, clears dirty.
|
||||
pub fn girder_mesh(
|
||||
&mut self,
|
||||
id: &FeatureId,
|
||||
) -> Result<Arc<Mesh>, KernelError> {
|
||||
// Cache hit path (not dirty)
|
||||
if !self.dirty.contains(id) {
|
||||
if let Some(cached) = self.mesh_cache.get(id) {
|
||||
return Ok(Arc::clone(cached));
|
||||
}
|
||||
}
|
||||
|
||||
// Compute path
|
||||
let ir = self.girders.get(id).ok_or_else(|| {
|
||||
KernelError::InvalidInput(format!("unknown FeatureId: {}", id))
|
||||
})?;
|
||||
let mesh = Arc::new(self.kernel.girder_mesh(ir)?);
|
||||
self.mesh_cache.insert(*id, Arc::clone(&mesh));
|
||||
self.dirty.remove(id);
|
||||
Ok(mesh)
|
||||
}
|
||||
|
||||
/// Raw IR lookup (no computation).
|
||||
pub fn get_girder(&self, id: &FeatureId) -> Option<&GirderIR> {
|
||||
self.girders.get(id)
|
||||
}
|
||||
|
||||
// ── Status ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// Number of Features awaiting recomputation.
|
||||
pub fn dirty_count(&self) -> usize { self.dirty.len() }
|
||||
|
||||
/// Total number of stored Girder Features.
|
||||
pub fn girder_count(&self) -> usize { self.girders.len() }
|
||||
}
|
||||
|
||||
// ─── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cimery_core::{MaterialGrade, SectionType};
|
||||
use cimery_ir::{FeatureId, GirderIR, PscISectionParams, SectionParams};
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dirty_after_set() {
|
||||
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() {
|
||||
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_on_second_call() {
|
||||
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(); // must be same Arc
|
||||
assert!(Arc::ptr_eq(&m1, &m2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalidation_on_update() {
|
||||
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);
|
||||
|
||||
// Update the same girder (longer span)
|
||||
let mut ir2 = ir;
|
||||
ir2.station_end = 50.0;
|
||||
db.set_girder(ir2);
|
||||
assert_eq!(db.dirty_count(), 1); // re-dirtied
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_id_returns_error() {
|
||||
let mut db = IncrementalDb::new(StubKernel);
|
||||
let missing_id = FeatureId::new();
|
||||
let err = db.girder_mesh(&missing_id);
|
||||
assert!(matches!(err, Err(KernelError::InvalidInput(_))));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user