From e766fb14fbe4fa9e6c24efdb97897f1b1c656f16 Mon Sep 17 00:00:00 2001 From: Yuto Nakamura <78634880+yutyan0119@users.noreply.github.com> Date: Mon, 19 Oct 2026 01:11:56 +0900 Subject: [PATCH] Preserve warm assembly caches across Save As renames (#2120) Thanks @yutyan0119. --- crates/engine/src/cmd/file.rs | 11 ++++++ crates/engine/tests/save_as_cache.rs | 58 ++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 crates/engine/tests/save_as_cache.rs diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index cc137984e3..3ccca6b5bc 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -294,12 +294,23 @@ fn saved_name(name: &str, old_path: Option<&str>, path: &str) -> Option /// Rename the design, and the undo and redo states that still carry the old name, so undo /// does not bring the default name back and leave the design looking changed. fn rename_design(s: &mut Session, from: &str, to: &str) { + // A prepared world is keyed by the document's Arc. Changing only its name + // must not make an opted-in save lose warm geometry and display caches. + // Retag only a cache that already matches: cold or stale placements stay so. + let model = s.model.state(); + let keep_world = s.world_cache.lock().ok().is_some_and(|cache| cache.as_ref().is_some_and(|cache| cache.matches(s, &model))); s.doc_mut().name = to.to_string(); for snap in s.undo.iter_mut().chain(s.redo.iter_mut()) { if snap.doc.name == from { std::sync::Arc::make_mut(&mut snap.doc).name = to.to_string(); } } + if keep_world + && let Ok(mut cache) = s.world_cache.lock() + && let Some(cache) = cache.as_mut() + { + cache.doc = std::sync::Arc::clone(&s.doc); + } } fn write(s: &mut Session, path: &str, format: &str, compression: &str, preview: bool, cache: bool) -> Result { diff --git a/crates/engine/tests/save_as_cache.rs b/crates/engine/tests/save_as_cache.rs new file mode 100644 index 0000000000..5506002b4a --- /dev/null +++ b/crates/engine/tests/save_as_cache.rs @@ -0,0 +1,58 @@ +//! Optional save caches survive a filename-driven rename, without warming or +//! certifying placements that do not match the document being saved. +use serde_json::json; +use solidcraft_doc::Document; +use solidcraft_engine::Session; + +fn assembly() -> Session { + let mut session = Session::new(Document::new("Untitled")); + session.execute("component.create", &json!({"name":"PlacedPart"})).unwrap(); + session.execute("solid.box", &json!({"length":2,"width":3,"height":4})).unwrap(); + session +} + +fn warm(session: &Session) { + for body in &session.world_state().bodies { + body.mesh(); + } +} + +fn save(session: &mut Session, label: &str) -> Vec { + let path = std::env::temp_dir().join(format!("solidcraft-save-as-{label}-{}.solidcraft", std::process::id())); + session.execute("file.save_as", &json!({"path":path,"cache":true,"preview":true})).unwrap(); + let bytes = std::fs::read(&path).unwrap(); + assert_eq!(solidcraft_io::read_design(&bytes).unwrap().name, path.file_stem().unwrap().to_string_lossy()); + std::fs::remove_file(&path).unwrap(); + let _ = std::fs::remove_file(format!("{}.bak", path.display())); + bytes +} + +#[test] +fn a_filename_rename_preserves_opted_in_warm_assembly_caches() { + let mut session = assembly(); + warm(&session); + for label in ["first-warm", "renamed-warm"] { + let bytes = save(&mut session, label); + let geometry = solidcraft_io::read_design_geometry(&bytes).expect("warm geometry survives the rename"); + assert_eq!(geometry.len(), 1); + assert!((solidcraft_kernel::measure(&geometry[0].body).unwrap().volume - 24.0).abs() < 1e-9); + assert!(solidcraft_io::read_design_preview(&bytes).is_some(), "warm display meshes survive the rename"); + warm(&session); + } +} + +#[test] +fn naming_a_design_does_not_warm_cold_or_revalidate_stale_placements() { + let original = assembly(); + let mut cold = Session::new((*original.doc).clone()); + let bytes = save(&mut cold, "cold"); + assert!(solidcraft_io::read_design_geometry(&bytes).is_none()); + assert!(solidcraft_io::read_design_preview(&bytes).is_none()); + + let mut stale = assembly(); + warm(&stale); + stale.doc_mut().occurrences.first_mut().unwrap().transform[3][0] = 25.0; + let bytes = save(&mut stale, "stale"); + assert!(solidcraft_io::read_design_geometry(&bytes).is_none(), "old placements must not be certified for a changed document"); + assert!(solidcraft_io::read_design_preview(&bytes).is_none()); +}