use std::collections::BTreeMap;
use super::assembly::{StepAssemblyExport, StepExportOccurrence, StepExportProduct};
use super::Mat4;
use crate::feature_pipeline::features::common::{compose_trs_matrix, vec3_from_value};
use crate::feature_pipeline::parts_library::{self, PartsLibraryEntry, PartsLibraryMap};
use crate::feature_pipeline::{Env, HistoryRequest};
use crate::{is_component_reference, restore_solids, BrepSolid};
const MAX_DEPTH: usize = 64;
fn component_prefix(name: &str) -> Option<&str> {
let (head, _) = name.split_once(':')?;
is_component_reference(head).then_some(head)
}
pub fn assembly_export_tree(
document_name: &str,
root_bodies: Vec<(String, BrepSolid)>,
root_components: &[(String, String, Mat4)],
) -> Result<StepAssemblyExport, String> {
let own: Vec<(String, BrepSolid)> = root_bodies
.into_iter()
.filter(|(name, _)| component_prefix(name).is_none())
.collect();
let mut assembly = StepAssemblyExport {
products: vec![StepExportProduct {
name: document_name.to_string(),
id: String::new(),
bodies: own,
}],
occurrences: Vec::new(),
};
let library = parts_library::parts_library_map();
place_children(&mut assembly, 0, &library, root_components, 0)?;
Ok(assembly)
}
fn place_children(
assembly: &mut StepAssemblyExport,
parent: usize,
library: &PartsLibraryMap,
instances: &[(String, String, Mat4)],
depth: usize,
) -> Result<(), String> {
let mut seen: BTreeMap<&str, usize> = BTreeMap::new();
for (designator, part_name, placement) in instances {
let child = match seen.get(part_name.as_str()) {
Some(index) => *index,
None => {
let entry = library.get(part_name).ok_or_else(|| {
format!(
"export_step: part '{part_name}' (placed by {designator}) is not in \
the parts library of '{}'",
assembly.products[parent].name
)
})?;
let index = add_part(assembly, part_name, entry, depth)?;
seen.insert(part_name.as_str(), index);
index
}
};
assembly.occurrences.push(StepExportOccurrence {
designator: designator.clone(),
parent,
child,
placement: *placement,
});
}
Ok(())
}
fn add_part(
assembly: &mut StepAssemblyExport,
part_name: &str,
entry: &PartsLibraryEntry,
depth: usize,
) -> Result<usize, String> {
if depth >= MAX_DEPTH {
return Err(format!(
"export_step: assembly nests deeper than {MAX_DEPTH} levels at part '{part_name}'"
));
}
let restored = match restore_solids(&entry.snapshot) {
Ok(restored) if !entry.dirty => restored,
_ => {
let (snapshot, _) = parts_library::rebuild_snapshot(&entry.document)
.map_err(|error| format!("export_step: part '{part_name}': {error}"))?;
restore_solids(&snapshot)
.map_err(|error| format!("export_step: part '{part_name}': {error}"))?
}
};
let bodies: Vec<(String, BrepSolid)> = restored
.solids
.into_iter()
.filter(|solid| component_prefix(&solid.name).is_none())
.map(|solid| (solid.name, solid.solid))
.collect();
let index = assembly.products.len();
assembly.products.push(StepExportProduct {
name: part_name.to_string(),
id: entry.source_key.clone(),
bodies,
});
let (library, instances) = document_components(&entry.document, part_name)?;
place_children(assembly, index, &library, &instances, depth + 1)?;
Ok(index)
}
fn document_components(
document: &serde_json::Value,
part_name: &str,
) -> Result<(PartsLibraryMap, Vec<(String, String, Mat4)>), String> {
let request: HistoryRequest = serde_json::from_value(document.clone()).map_err(|error| {
format!("export_step: part '{part_name}' document does not parse: {error}")
})?;
let env = Env::build(&request.expressions, &request.configurator).unwrap_or_else(Env::poisoned);
let mut instances = Vec::new();
for feature in &request.features {
if !parts_library::is_acomp_type(&feature.feature_type) {
continue;
}
let params = &feature.input_params;
let string = |key: &str| {
params
.get(key)
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|text| !text.is_empty())
.map(str::to_string)
};
let Some(id) = string("id") else {
return Err(format!(
"export_step: part '{part_name}' has a component with no id"
));
};
let Some(child) = string("partName") else {
return Err(format!(
"export_step: part '{part_name}' component {id} names no part"
));
};
let transform = params.get("transform");
let translate = vec3_from_value(
&env,
transform.and_then(|value| value.get("translate")),
"transform.translate",
[0.0, 0.0, 0.0],
)?;
let rotate_deg = vec3_from_value(
&env,
transform.and_then(|value| value.get("rotateEulerDeg")),
"transform.rotateEulerDeg",
[0.0, 0.0, 0.0],
)?;
let placement = compose_trs_matrix(
translate,
[
rotate_deg[0].to_radians(),
rotate_deg[1].to_radians(),
rotate_deg[2].to_radians(),
],
[1.0, 1.0, 1.0],
[0.0, 0.0, 0.0],
);
instances.push((id, child, placement));
}
Ok((request.parts_library, instances))
}