use crate::panels::parts_library::{document_signature, refresh_library_entry};
use crate::store::ModelStore;
use brep_render::engine_state::EngineState;
use serde_json::Value;
#[derive(Default)]
pub struct UpdateComponents {
outdated: Vec<String>,
missing: Vec<String>,
checked: Option<(u64, u64)>,
}
impl UpdateComponents {
pub fn new() -> Self {
Self::default()
}
pub fn ensure_current(
&mut self,
state: &mut EngineState,
model_store: &dyn ModelStore,
save_generation: u64,
) {
let key = (state.applied_generation(), save_generation);
if self.checked == Some(key) {
return;
}
let (outdated, missing) = compute(state, model_store);
self.outdated = outdated;
self.missing = missing;
self.checked = Some(key);
}
pub fn invalidate(&mut self) {
self.checked = None;
}
pub fn outdated_count(&self) -> usize {
self.outdated.len()
}
pub fn is_outdated(&self, part_name: &str) -> bool {
self.outdated.iter().any(|part| part == part_name)
}
pub fn missing(&self) -> &[String] {
&self.missing
}
pub fn run(
&mut self,
state: &mut EngineState,
model_store: &dyn ModelStore,
) -> Result<usize, String> {
let (outdated, missing) = compute(state, model_store);
if !missing.is_empty() {
state.push_notice(format!(
"Update components: no source document for {} — skipped",
missing.join(", ")
));
}
if outdated.is_empty() {
self.invalidate();
return Ok(0);
}
let mut document: Value = serde_json::from_str(&state.history_request_json())
.map_err(|error| format!("assembly document unreadable: {error}"))?;
let mut refreshed = 0usize;
for part_name in &outdated {
let source_key = document["partsLibrary"][part_name]["sourceKey"]
.as_str()
.unwrap_or_default()
.to_string();
let Some(contents) = model_store.read(&source_key) else {
state.push_notice(format!(
"Update components: '{part_name}' source '{source_key}' missing — skipped"
));
continue;
};
match refresh_library_entry(&mut document, part_name, &contents) {
Ok(()) => refreshed += 1,
Err(error) => state.push_notice(format!(
"Update components: '{part_name}': {error} — skipped"
)),
}
}
if refreshed > 0 {
state
.set_history_json(&document.to_string())
.map_err(|error| format!("assembly reload failed: {error}"))?;
state.push_notice(format!(
"Updated {refreshed} part(s) from source — every instance rebuilt"
));
}
self.invalidate();
Ok(refreshed)
}
}
fn compute(state: &mut EngineState, model_store: &dyn ModelStore) -> (Vec<String>, Vec<String>) {
let mut outdated = Vec::new();
let mut missing = Vec::new();
if !state.history_has_assembly() {
return (outdated, missing);
}
state.ensure_assembly_synced();
let Ok(document) = serde_json::from_str::<Value>(&state.history_request_json()) else {
return (outdated, missing);
};
let Some(library) = document.get("partsLibrary").and_then(Value::as_object) else {
return (outdated, missing);
};
for (part_name, entry) in library {
let Some(source_key) = entry
.get("sourceKey")
.and_then(Value::as_str)
.filter(|key| !key.is_empty())
else {
continue; };
match model_store.read(source_key) {
None => missing.push(part_name.clone()),
Some(contents) => {
let signature = entry
.get("sourceSignature")
.and_then(Value::as_str)
.unwrap_or_default();
if document_signature(&contents) != signature {
outdated.push(part_name.clone());
}
}
}
}
(outdated, missing)
}