Model cache: a saved model records its body check mode and is adopted under it

Since the body check mode became part of the evaluation context (9acbcf924),
evaluate digests the context with the mode, but Model::adopt still took the
context without it and the default mode: the first evaluation after opening a
design from its saved model saw another context and recomputed every feature
(undoing_an_edit_after_opening_reuses_the_saved_results,
rolling_the_marker_back_keeps_results_after_cache_eviction). The saved model
now records the mode it was evaluated under (thinned ones too), adopt takes
it and digests the context as evaluate does, and a session opening a design
under another mode evaluates it again rather than showing that mode's results.
Model cache format 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-30 15:05:21 -04:00
committed by jyviko
parent 45823002c0
commit 709765fcae
5 changed files with 44 additions and 13 deletions
+24
View File
@@ -288,3 +288,27 @@ fn undoing_an_edit_after_opening_reuses_the_saved_results() {
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_file(format!("{}.bak", path.display()));
}
/// A saved model holds the results of the body check mode it was evaluated under: opened under
/// the same mode it is taken as it is, under another the design is evaluated again.
#[test]
fn a_model_saved_under_another_check_mode_is_evaluated_again() {
let plate = solidcraft_engine::sample::samples().into_iter().find(|s| s.name == "Sample Plate").unwrap();
let mut built = Session::default();
for c in (plate.script)()["commands"].as_array().unwrap() {
built.execute(c["command"].as_str().unwrap(), &c["params"]).unwrap();
}
let path = temp("mode");
built.execute("file.save_as", &json!({"path": path.to_string_lossy()})).unwrap();
let same = open(&path);
assert_eq!(same.model.validation(), built.model.validation());
assert_eq!(same.model.recomputed, 0, "taken from the file");
let mut other = Session::default();
other.set_validation(solidcraft_engine::kernel::Validation::EachStep);
assert_ne!(other.validation(), built.model.validation());
other.execute("doc.open", &json!({"path": path.to_string_lossy()})).unwrap();
assert_eq!(other.validation(), solidcraft_engine::kernel::Validation::EachStep);
assert!(other.model.recomputed > 0, "evaluated again under its own mode");
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_file(format!("{}.bak", path.display()));
}
+11 -5
View File
@@ -232,9 +232,10 @@ impl Model {
}
features.push(f);
}
// The check mode the results were evaluated under: they are another mode's results.
let records = json!({
"features": features, "states": w.states, "model_bodies": w.model_bodies, "names": w.names, "sketches": w.sketches,
"sheets": w.sheets,
"sheets": w.sheets, "validation": self.validation().id(),
});
Ok(SavedModel { records, bodies: w.bodies })
})
@@ -540,6 +541,7 @@ impl SavedModel {
let records = json!({
"features": out_features, "states": out_states, "model_bodies": out_bodies, "names": kept_of(names.as_slice(), &names_used),
"sketches": kept_of(sketches.as_slice(), &sketches_used), "sheets": sheets,
"validation": self.records.get("validation").cloned().unwrap_or(Json::Null),
});
let kept: Vec<usize> = (0..self.bodies.len()).filter(|i| kernel_used.get(*i) == Some(&true)).collect();
let bodies = kept.iter().filter_map(|i| self.bodies.get(*i).cloned()).collect();
@@ -550,8 +552,9 @@ impl SavedModel {
impl Model {
/// The model a design file kept for `doc` ([`Model::saved`]), taken as `doc`'s evaluation:
/// every feature's result as saved, nothing evaluated but the features whose state the file
/// did not keep ([`SavedModel::thinned`]). Refused (an error) unless every feature's
/// fingerprint is the one `doc` gives it now, and every record is well formed.
/// did not keep ([`SavedModel::thinned`]), under the check mode it was evaluated in
/// ([`Model::validation`]). Refused (an error) unless every feature's fingerprint is the one
/// `doc` gives it now, and every record is well formed.
pub fn adopt(doc: &Document, saved: &SavedModel) -> Result<Model> {
let mut states = saved.states()?;
let frame_bodies = Arc::new(std::sync::Mutex::new(FrameBodies::default()));
@@ -565,13 +568,16 @@ impl Model {
let text =
|v: Option<&Json>| v.and_then(Json::as_str).filter(|s| s.len() <= 1 << 16).map(str::to_owned).ok_or_else(|| bad("saved model: text"));
let features = records.get("features").and_then(Json::as_array).ok_or_else(|| bad("saved model: features"))?;
let validation =
records.get("validation").and_then(Json::as_str).and_then(kernel::Validation::parse).ok_or_else(|| bad("saved model: validation"))?;
if features.len() != doc.features.len() {
return Err(bad("saved model: features"));
}
let (vals, param_errors) = evaluation_values(doc);
let step_digests = StepDigests::of(doc, &StepDigests::default());
let marker = doc.marker.unwrap_or(usize::MAX);
let ctx = context_digest(doc, &vals);
// As `evaluate` digests the context under the mode, so the next evaluation reuses these.
let ctx = context_digest(doc, &vals) ^ validation_digest(validation);
let mut before = Arc::new(ModelState::default());
// Whether `before` is the state before the feature: one the file left out only stands in.
let mut exact = true;
@@ -613,7 +619,7 @@ impl Model {
}
results.push(result);
}
let mut model = Model { results, param_errors, step_digests, context: ctx, frame_bodies, ..Model::default() };
let mut model = Model { results, param_errors, step_digests, context: ctx, frame_bodies, validation, ..Model::default() };
let unkept = model.results.iter().filter(|r| r.unkept).count();
if unkept > 0 {
// The features whose state the file did not keep, each evaluated again from the state
+4 -5
View File
@@ -617,14 +617,13 @@ impl Session {
// The session keeps what is hidden (an undo step of its own); the design file carries it.
let visibility = std::mem::take(&mut doc.visibility);
let doc = Arc::new(doc);
// A model saved under another check mode holds that mode's results: evaluated again.
let adopted = saved
.filter(|_| solidcraft_geom::env::var_os("NO_MODEL_CACHE").is_none())
.and_then(|saved| Model::adopt(&doc, saved).map_err(|e| log::info!("the saved model is not used: {e}")).ok());
.and_then(|saved| Model::adopt(&doc, saved).map_err(|e| log::info!("the saved model is not used: {e}")).ok())
.filter(|model| model.validation() == validation);
let model = match adopted {
Some(mut model) => {
model.set_validation(validation);
model
}
Some(model) => model,
None => {
let mut model = Model::new();
model.set_validation(validation);
+1 -1
View File
@@ -5,7 +5,7 @@
use super::*;
use solidcraft_doc::{ModelState, SavedModel};
const FORMAT: &str = "solidcraft-model-cache/5";
const FORMAT: &str = "solidcraft-model-cache/6";
const PRODUCER: &str = "solidcraft-truck-adapter/4";
const MANIFEST: &str = "cache/model.json";
const PAYLOAD: &str = "cache/bodies.bin";
+4 -2
View File
@@ -126,7 +126,9 @@ the final state fits.
The model is the whole evaluation: every feature's result (warning, boolean
diagnostics, whether it was skipped, its fingerprint) and the state after it
(bodies, solved sketches, construction geometry, sheet metal bodies, threads and
the blend records later features read). States share what they share: a body that
the blend records later features read), and the body check mode it was evaluated
under (`each_step`, `each_feature` or `off`): a design opened under another mode is
evaluated again. States share what they share: a body that
later features pass through unchanged, its face names and a solved sketch are each
recorded once. Face names are kept as worked out (saving works out any not asked
for yet), so a body taken from the file is named exactly as when it was saved.
@@ -147,7 +149,7 @@ SHA-256 of their encoding), and a placed copy of a repeated part (an instance fr
a STEP assembly) records the part and its matrix, so it shows that part's display
again instead of being meshed on its own.
`cache/model.json` records `solidcraft-model-cache/5`, the producer revision
`cache/model.json` records `solidcraft-model-cache/6`, the producer revision
`solidcraft-truck-adapter/4`, the application version, the build (source commit,
CPU architecture and OS), the SHA-256 of the exact `document.json` (whose blob
references pin every STEP, mesh and image payload) and of `cache/bodies.bin`, the