use crate::panels::parts_library::{document_signature, refresh_library_entry};
use crate::store::ModelStore;
use brep_render::engine_state::{EngineState, NoticeSeverity};
use crate::store::{ReadNow, Residency};
use serde_json::Value;
#[derive(Default)]
pub struct UpdateComponents {
outdated: Vec<String>,
missing: Vec<String>,
checked: Option<(u64, u64, 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(),
state.history.revision(),
save_generation,
model_store.mutation_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 outdated(&self) -> &[String] {
&self.outdated
}
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_as(NoticeSeverity::Warning, 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 contents = match crate::store::read_now(model_store, &source_key) {
ReadNow::Ready(contents) => contents,
ReadNow::Loading => {
state.push_notice_as(NoticeSeverity::Info, format!(
"Update components: '{part_name}' is still loading from the store — run the update again in a moment"
));
continue;
}
ReadNow::Absent => {
state.push_notice_as(NoticeSeverity::Warning, format!(
"Update components: '{part_name}' source '{source_key}' missing — skipped"
));
continue;
}
};
match refresh_library_entry(&mut document, part_name, &contents) {
Ok(unlinked) => {
refreshed += 1;
for line in unlinked {
state.push_notice_as(NoticeSeverity::Warning, format!("Update components: '{part_name}': {line}"));
}
}
Err(error) => state.push_notice(format!(
"Update components: '{part_name}': {error} — skipped"
)),
}
}
if refreshed > 0 {
state
.edit_document_json(&document.to_string())
.map_err(|error| format!("assembly reload failed: {error}"))?;
state.push_notice_as(NoticeSeverity::Info, format!(
"Updated {refreshed} part(s) from source — instances and eCAD snapshots refreshed"
));
}
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);
}
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 crate::store::read_now(model_store, source_key) {
ReadNow::Loading => {}
ReadNow::Absent => missing.push(part_name.clone()),
ReadNow::Ready(contents) => {
let signature = entry
.get("sourceSignature")
.and_then(Value::as_str)
.unwrap_or_default();
let stale_sheet = ["diagram", "pcb"].iter().any(|block| {
document[*block]["components"].as_array().is_some_and(|components| {
components.iter().any(|component| component["part"]["key"].as_str() == Some(part_name.as_str())
&& !component["part"]["instance"].as_str().is_some_and(|id| id.contains(':'))
&& component["part"]["signature"].as_str() != Some(signature))
})
});
if document_signature(&contents) != signature || stale_sheet {
outdated.push(part_name.clone());
}
}
}
}
(outdated, missing)
}