use super::*;
impl EngineState {
pub fn import_stl_feature(&mut self, bytes: &[u8]) -> Result<String, String> {
self.submit_mesh_import(crate::runner::MeshImportFormat::Stl, bytes.to_vec())
}
pub fn import_obj_feature(&mut self, text: &str) -> Result<String, String> {
self.import_obj_bytes_feature(text.as_bytes())
}
pub fn import_obj_bytes_feature(&mut self, bytes: &[u8]) -> Result<String, String> {
self.submit_mesh_import(crate::runner::MeshImportFormat::Obj, bytes.to_vec())
}
fn submit_mesh_import(
&mut self,
format: crate::runner::MeshImportFormat,
bytes: Vec<u8>,
) -> Result<String, String> {
let id = self.submit_mesh_reconstruction(
format, bytes, Default::default(), MeshImportDestination::Document,
)?;
Ok(serde_json::json!({ "meshImport": "submitted", "id": id }).to_string())
}
pub fn reconstruct_mesh_preview(
&mut self,
format: crate::runner::MeshImportFormat,
bytes: Vec<u8>,
options: crate::runner::StlConversionOptions,
) -> Result<u64, String> {
self.submit_mesh_reconstruction(format, bytes, options, MeshImportDestination::Preview)
}
pub fn take_mesh_preview(&mut self) -> Option<crate::runner::MeshImportReply> {
self.mesh_preview_results.pop_front()
}
fn submit_mesh_reconstruction(
&mut self,
format: crate::runner::MeshImportFormat,
bytes: Vec<u8>,
options: crate::runner::StlConversionOptions,
destination: MeshImportDestination,
) -> Result<u64, String> {
if bytes.is_empty() {
return Err("mesh import failed: file is empty".into());
}
let id = self.next_mesh_import_id;
self.next_mesh_import_id = self.next_mesh_import_id.wrapping_add(1);
self.pending_mesh_imports.insert(id, destination);
self.runner.submit_mesh_import(crate::runner::MeshImportRequest {
id, format, bytes, options,
});
self.pump();
Ok(id)
}
pub fn import_step_feature(&mut self, step_text: &str) -> Result<String, String> {
if !step_text.contains("ISO-10303-21") {
return Err("not a STEP file (missing the ISO-10303-21 header)".into());
}
let id = self.next_feature_id(&crate::features::feature_short_name("IMPORT3D"));
let feature = serde_json::json!({
"type": "IMPORT3D",
"inputParams": { "id": id, "stepText": step_text },
"persistentData": {},
});
self.pending_fit = true;
self.add_feature(&feature.to_string())
}
pub fn export_step_text(&self) -> Result<String, String> {
let request: HistoryRequest = serde_json::from_value(self.history.prefix_request())
.map_err(|e| format!("export STEP: history request: {e}"))?;
let handles: Vec<u32> = crate::pipeline::resident_solid_handles(&request)
.into_iter()
.map(|(_, handle)| handle)
.collect();
if handles.is_empty() {
return Err("nothing to export: the model has no solids".into());
}
brep_kernel::export_step_handles(&handles, "Part", "MM", "")
}
fn flat_pattern_target_handle(&self) -> Result<u32, String> {
let request: HistoryRequest = serde_json::from_value(self.history.prefix_request())
.map_err(|e| format!("export flat pattern: history request: {e}"))?;
let sheet_metal: Vec<(String, u32)> = crate::pipeline::resident_solid_handles(&request)
.into_iter()
.filter(|(_, handle)| brep_kernel::is_sheet_metal_handle(*handle))
.collect();
if sheet_metal.is_empty() {
return Err("no sheet-metal body in the part".into());
}
let selected: Vec<u32> = sheet_metal
.iter()
.filter(|(name, _)| self.emphasis.selected_solids.contains(name))
.map(|(_, handle)| *handle)
.collect();
if let [handle] = selected.as_slice() {
return Ok(*handle);
}
match sheet_metal.as_slice() {
[(_, handle)] => Ok(*handle),
_ => Err(
"several sheet-metal bodies in the part — select the one to export".into(),
),
}
}
pub fn export_flat_pattern_dxf(&self) -> Result<String, String> {
brep_kernel::flat_pattern_dxf(self.flat_pattern_target_handle()?)
}
pub fn export_flat_pattern_svg(&self) -> Result<String, String> {
brep_kernel::flat_pattern_svg(self.flat_pattern_target_handle()?)
}
pub fn import_iges_feature(&mut self, iges_text: &str) -> Result<String, String> {
if iges_text.contains("ISO-10303-21") {
return Err("not an IGES file (this looks like a STEP document)".into());
}
let looks_like_iges = iges_text.lines().any(|line| {
matches!(line.chars().nth(72), Some('S' | 'G' | 'D' | 'P' | 'T'))
});
if !looks_like_iges {
return Err("not an IGES file (no S/G/D/P/T section records found)".into());
}
let id = self.next_feature_id(&crate::features::feature_short_name("IMPORT3D"));
let feature = serde_json::json!({
"type": "IMPORT3D",
"inputParams": { "id": id, "igesText": iges_text },
"persistentData": {},
});
self.pending_fit = true;
self.add_feature(&feature.to_string())
}
pub fn export_iges_text(&self) -> Result<String, String> {
let request: HistoryRequest = serde_json::from_value(self.history.prefix_request())
.map_err(|e| format!("export IGES: history request: {e}"))?;
let handles: Vec<u32> = crate::pipeline::resident_solid_handles(&request)
.into_iter()
.map(|(_, handle)| handle)
.collect();
if handles.is_empty() {
return Err("nothing to export: the model has no solids".into());
}
brep_kernel::export_iges_handles(&handles, "Part", "MM", "")
}
pub fn export_stl_text(&self) -> Result<String, String> {
let mut out = String::from("solid brep\n");
let mut triangles = 0usize;
for solid in self.scene.solids() {
let positions = &solid.mesh.positions;
for tri in solid.mesh.indices.chunks_exact(3) {
let a = positions[tri[0] as usize];
let b = positions[tri[1] as usize];
let c = positions[tri[2] as usize];
let normal = triangle_normal(a, b, c);
out.push_str(&format!(
" facet normal {} {} {}\n outer loop\n",
normal[0], normal[1], normal[2]
));
for v in [a, b, c] {
out.push_str(&format!(" vertex {} {} {}\n", v[0], v[1], v[2]));
}
out.push_str(" endloop\n endfacet\n");
triangles += 1;
}
}
out.push_str("endsolid brep\n");
if triangles == 0 {
return Err("nothing to export: the scene has no triangles".into());
}
Ok(out)
}
}
fn triangle_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] {
let u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
let v = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
let n = [
u[1] * v[2] - u[2] * v[1],
u[2] * v[0] - u[0] * v[2],
u[0] * v[1] - u[1] * v[0],
];
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
if len > 0.0 {
[n[0] / len, n[1] / len, n[2] / len]
} else {
[0.0, 0.0, 0.0]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StepAssemblyProbe {
pub parts: usize,
pub instances: usize,
pub nested_depth: usize,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct StepAssemblyImport {
pub nested: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct StepAssemblyReport {
pub parts: usize,
pub instances: usize,
pub baked_nonrigid: usize,
pub failed_products: usize,
pub first_error: Option<String>,
pub flat_fallback: bool,
}
enum Consumed {
Imported(StepAssemblyReport),
NoComponents {
failed_products: usize,
first_error: Option<String>,
},
}
type Mat4 = [f64; 16];
struct PlacedProduct {
product: usize,
world: Mat4,
depth: usize,
rigid_path: bool,
}
type PartKey = (usize, [u64; 9]);
const NO_FACTOR: [u64; 9] = [0; 9];
impl EngineState {
pub fn probe_step_assembly(
&mut self,
step_text: &str,
) -> Result<Option<StepAssemblyProbe>, String> {
let id = self.submit_step_probe(step_text);
match self.take_step_probe() {
Some((answered, outcome)) if answered == id => match outcome {
super::StepProbeOutcome::Structure(probe) => Ok(Some(probe)),
super::StepProbeOutcome::Flat => Ok(None),
super::StepProbeOutcome::Failed(error) => Err(error),
},
_ => Err(
"the STEP probe is still running on the background runner — use \
submit_step_probe / take_step_probe"
.into(),
),
}
}
pub fn submit_step_probe(&mut self, step_text: &str) -> u64 {
self.pending_step_assembly = None;
let id = self.next_step_probe_id;
self.next_step_probe_id = self.next_step_probe_id.wrapping_add(1);
if !step_text.contains("NEXT_ASSEMBLY_USAGE_OCCURRENCE") {
self.step_probe_results
.push_back((id, super::StepProbeOutcome::Flat));
return id;
}
self.pending_step_probes.insert(id);
self.runner.submit_step_probe(crate::runner::StepProbeRequest {
id,
text: step_text.to_string(),
});
self.pump();
id
}
pub fn take_step_probe(&mut self) -> Option<(u64, super::StepProbeOutcome)> {
self.step_probe_results.pop_front()
}
pub fn step_probes_pending(&self) -> bool {
!self.pending_step_probes.is_empty()
}
pub fn import_probed_step_assembly(
&mut self,
doc_name: &str,
opts: StepAssemblyImport,
sink: &mut dyn PartSink,
) -> Result<StepAssemblyReport, String> {
let assembly = self.pending_step_assembly.take().ok_or_else(|| {
"import STEP assembly: nothing probed (call probe_step_assembly first)".to_string()
})?;
match self.consume_step_assembly(assembly, doc_name, opts.nested, sink) {
Consumed::Imported(report) => Ok(report),
Consumed::NoComponents { first_error, .. } => Err(format!(
"import STEP assembly: no part of the assembly could be built{}",
first_error
.map(|error| format!(" ({error})"))
.unwrap_or_default()
)),
}
}
pub fn discard_probed_step_assembly(&mut self) {
self.pending_step_assembly = None;
}
pub fn import_step_assembly(
&mut self,
step_text: &str,
doc_name: &str,
opts: StepAssemblyImport,
) -> Result<StepAssemblyReport, String> {
let structured = self.probe_step_assembly(step_text)?.is_some();
let outcome = structured.then(|| {
let assembly = self
.pending_step_assembly
.take()
.expect("a Some probe stashed the assembly it counted");
self.consume_step_assembly(assembly, doc_name, opts.nested, &mut EmbeddedOnly)
});
match outcome {
Some(Consumed::Imported(report)) => Ok(report),
Some(Consumed::NoComponents {
failed_products,
first_error,
}) => {
self.import_step_feature(step_text)?;
Ok(StepAssemblyReport {
failed_products,
first_error,
flat_fallback: true,
..StepAssemblyReport::default()
})
}
None => {
self.import_step_feature(step_text)?;
Ok(StepAssemblyReport {
flat_fallback: true,
..StepAssemblyReport::default()
})
}
}
}
fn consume_step_assembly(
&mut self,
assembly: brep_kernel::StepAssembly,
doc_name: &str,
nested: bool,
sink: &mut dyn PartSink,
) -> Consumed {
let mut first_error = assembly.first_error.clone();
let mut writer = PartWriter::new(sink);
let plan = if nested {
plan_nested(&assembly, doc_name, &mut first_error, &mut writer)
} else {
plan_flat(&assembly, &mut first_error)
};
let Plan {
wanted,
factors,
documents,
mut failed_products,
baked_below_root,
} = plan;
let mut keys: Vec<PartKey> = wanted.iter().map(|(key, _)| *key).collect();
keys.sort_unstable();
keys.dedup();
let mut entry_names: std::collections::HashMap<PartKey, String> =
std::collections::HashMap::new();
{
let _isolation = brep_kernel::IsolatedSceneMetadata::begin();
for key in &keys {
let built = match documents.get(key) {
Some((name, document)) => install_part(name, document, &mut writer),
None => {
let product = assembly
.products
.iter()
.find(|product| product.pd_ref == key.0)
.expect("every key names a product of this assembly");
build_library_entry(product, factors.get(key), doc_name, &mut writer)
}
};
match built {
Ok(name) => {
entry_names.insert(*key, name);
}
Err(error) => {
failed_products += 1;
note(&mut first_error, error);
}
}
}
}
if entry_names.is_empty() {
return Consumed::NoComponents {
failed_products,
first_error,
};
}
let mut ground_next = !(0..self.history.len()).any(|index| {
matches!(
self.history.feature_type(index).as_deref(),
Some("ACOMP") | Some("ASSEMBLY COMPONENT")
)
});
let mut features: Vec<serde_json::Value> = Vec::with_capacity(wanted.len());
let mut baked_nonrigid = 0usize;
for (key, pose) in &wanted {
let Some(part_name) = entry_names.get(key) else {
continue; };
let transform = match brep_kernel::AffineTransform::new(*pose) {
Ok(transform) => transform,
Err(error) => {
note(&mut first_error, format!("occurrence pose: {error}"));
continue;
}
};
if key.1 != NO_FACTOR {
baked_nonrigid += 1;
}
features.push(serde_json::json!({
"type": "ACOMP",
"inputParams": {
"id": self.history.next_feature_id("ACOMP"),
"partName": part_name,
"transform": brep_kernel::transform_to_pose_params(&transform),
"isFixed": ground_next,
},
"persistentData": {}
}));
ground_next = false;
}
if features.is_empty() {
return Consumed::NoComponents {
failed_products,
first_error,
};
}
if let Ok(library) =
serde_json::from_str::<serde_json::Value>(&brep_kernel::parts_library_json())
{
self.history.set_parts_library(library);
}
self.pending_fit = true;
let instances = features.len();
let baked_nonrigid = baked_nonrigid + baked_below_root;
self.add_features(&features);
Consumed::Imported(StepAssemblyReport {
parts: entry_names
.values()
.collect::<std::collections::HashSet<_>>()
.len(),
instances,
baked_nonrigid,
failed_products,
first_error,
flat_fallback: false,
})
}
}
#[derive(Default)]
struct Plan {
wanted: Vec<(PartKey, Mat4)>,
factors: std::collections::HashMap<PartKey, Mat4>,
documents: std::collections::HashMap<PartKey, (String, serde_json::Value)>,
failed_products: usize,
baked_below_root: usize,
}
fn plan_flat(assembly: &brep_kernel::StepAssembly, first_error: &mut Option<String>) -> Plan {
let mut plan = Plan::default();
for placed in &compose_world_occurrences(assembly) {
let product = &assembly.products[placed.product];
if product.bodies.is_empty() {
continue; }
let (key, pose) = if placed.rigid_path {
((product.pd_ref, NO_FACTOR), placed.world)
} else {
match split_rigid(&placed.world) {
Ok((rigid, factor)) if is_identity(&factor) => {
((product.pd_ref, NO_FACTOR), rigid)
}
Ok((rigid, factor)) => {
let key = (product.pd_ref, factor_key(&factor));
plan.factors.insert(key, factor);
(key, rigid)
}
Err(error) => {
note(first_error, error);
continue;
}
}
};
plan.wanted.push((key, pose));
}
plan
}
fn plan_nested(
assembly: &brep_kernel::StepAssembly,
doc_name: &str,
first_error: &mut Option<String>,
writer: &mut PartWriter<'_>,
) -> Plan {
let _isolation = brep_kernel::IsolatedSceneMetadata::begin();
let mut build = NestedBuild {
assembly,
doc_name,
writer,
memo: std::collections::HashMap::new(),
factors: std::collections::HashMap::new(),
entries: 0,
bytes: 0,
failed_products: 0,
baked_nonrigid: 0,
first_error: None,
};
let mut plan = Plan::default();
for &root in &assembly.roots {
let mut rows: Vec<(DocKey, Mat4)> = Vec::new();
if !assembly.products[root].bodies.is_empty() {
rows.push((
DocKey::Leaf((assembly.products[root].pd_ref, NO_FACTOR)),
MAT4_IDENTITY,
));
}
let mut root_level_bakes = 0usize;
build.place_children(root, &[root], &mut rows, &mut root_level_bakes);
for (key, pose) in rows {
let part = match build.document(key, &mut vec![root]) {
Ok(Some(document)) => document,
Ok(None) => continue,
Err(error) => {
build.failed_products += 1;
note(&mut build.first_error, error);
continue;
}
};
let part_key = key.part_key(assembly);
plan.documents.insert(part_key, part);
plan.wanted.push((part_key, pose));
}
}
plan.failed_products = build.failed_products;
plan.baked_below_root = build.baked_nonrigid;
if let Some(error) = build.first_error {
note(first_error, error);
}
plan
}
const MAX_NESTED_DEPTH: usize = 64;
const MAX_NESTED_ENTRIES: usize = 10_000;
const MAX_NESTED_BYTES: usize = 256 * 1024 * 1024;
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
enum DocKey {
Leaf(PartKey),
Assembly(usize),
}
impl DocKey {
fn part_key(self, assembly: &brep_kernel::StepAssembly) -> PartKey {
match self {
DocKey::Leaf(key) => key,
DocKey::Assembly(product) => (assembly.products[product].pd_ref, NO_FACTOR),
}
}
}
struct NestedBuild<'a, 'w> {
assembly: &'a brep_kernel::StepAssembly,
doc_name: &'a str,
writer: &'a mut PartWriter<'w>,
memo: std::collections::HashMap<DocKey, Option<(String, serde_json::Value)>>,
factors: std::collections::HashMap<PartKey, Mat4>,
entries: usize,
bytes: usize,
failed_products: usize,
baked_nonrigid: usize,
first_error: Option<String>,
}
impl NestedBuild<'_, '_> {
fn document(
&mut self,
key: DocKey,
ancestors: &mut Vec<usize>,
) -> Result<Option<(String, serde_json::Value)>, String> {
if let Some(hit) = self.memo.get(&key) {
return Ok(hit.clone());
}
if ancestors.len() >= MAX_NESTED_DEPTH {
return Err(format!(
"nested import: sub-assembly nesting deeper than {MAX_NESTED_DEPTH} levels \
(import as bodies, or import flat)"
));
}
let built = match key {
DocKey::Leaf(part) => self.leaf_document(part),
DocKey::Assembly(product) => {
ancestors.push(product);
let built = self.assembly_document(product, ancestors);
ancestors.pop();
built
}
}?;
self.memo.insert(key, built.clone());
Ok(built)
}
fn leaf_document(
&mut self,
key: PartKey,
) -> Result<Option<(String, serde_json::Value)>, String> {
let product = self
.assembly
.products
.iter()
.find(|product| product.pd_ref == key.0)
.expect("every key names a product of this assembly");
if product.bodies.is_empty() {
return Ok(None);
}
let factor = self.factors.get(&key).copied();
self.spend_entry()?;
native_part_document(product, factor.as_ref(), self.doc_name).map(Some)
}
fn assembly_document(
&mut self,
product: usize,
ancestors: &mut Vec<usize>,
) -> Result<Option<(String, serde_json::Value)>, String> {
let node = &self.assembly.products[product];
let mut library = serde_json::Map::new();
let mut features: Vec<serde_json::Value> = Vec::new();
if !node.bodies.is_empty() {
let payload = brep_kernel::native_import_payload_with_appearance(
"IMPORT3D1",
&node.bodies,
&node.appearances,
)
.map_err(|error| format!("part '{}': {error}", part_name(node, self.doc_name)))?;
features.push(serde_json::json!({
"type": "IMPORT3D",
"inputParams": { "id": "IMPORT3D1", "nativeBrep": payload },
"persistentData": {},
}));
}
let mut rows: Vec<(DocKey, Mat4)> = Vec::new();
let mut bakes = 0usize;
self.place_children(product, ancestors, &mut rows, &mut bakes);
self.baked_nonrigid += bakes;
let mut names: std::collections::HashMap<DocKey, String> =
std::collections::HashMap::new();
let mut by_signature: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
let mut components = 0usize;
for (key, pose) in rows {
let name = match names.get(&key) {
Some(name) => name.clone(),
None => {
let built = match self.document(key, ancestors) {
Ok(Some(built)) => built,
Ok(None) => continue,
Err(error) => {
self.failed_products += 1;
note(&mut self.first_error, error);
continue;
}
};
let serialized = built.1.to_string();
let signature = document_signature(&serialized);
let name = match by_signature.get(&signature) {
Some(name) => name.clone(),
None => {
self.spend_bytes(serialized.len())?;
let name = unique_entry_name(&library, &built.0);
let source_key =
self.writer.key_for(&name, &serialized, &signature);
library.insert(
name.clone(),
serde_json::json!({
"sourceKey": source_key,
"sourceSignature": signature.clone(),
"document": built.1,
"snapshot": "",
}),
);
by_signature.insert(signature, name.clone());
name
}
};
names.insert(key, name.clone());
name
}
};
let Ok(transform) = brep_kernel::AffineTransform::new(pose) else {
note(
&mut self.first_error,
format!("sub-assembly '{name}': occurrence pose is not an affine"),
);
continue;
};
components += 1;
features.push(serde_json::json!({
"type": "ACOMP",
"inputParams": {
"id": format!("ACOMP{components}"),
"partName": name,
"transform": brep_kernel::transform_to_pose_params(&transform),
"isFixed": components == 1,
},
"persistentData": {},
}));
}
if features.is_empty() {
return Ok(None);
}
self.spend_entry()?;
Ok(Some((
part_name(node, self.doc_name),
serde_json::json!({ "partsLibrary": library, "features": features }),
)))
}
fn place_children(
&mut self,
product: usize,
ancestors: &[usize],
rows: &mut Vec<(DocKey, Mat4)>,
bakes: &mut usize,
) {
let mut children: Vec<&brep_kernel::StepOccurrence> = self
.assembly
.occurrences
.iter()
.filter(|occurrence| occurrence.parent == product)
.collect();
children.sort_by_key(|occurrence| occurrence.nauo_ref);
for occurrence in children {
if ancestors.contains(&occurrence.child) {
note(
&mut self.first_error,
format!(
"occurrence #{} closes a cycle in the product structure and was skipped",
occurrence.nauo_ref
),
);
continue;
}
let child = &self.assembly.products[occurrence.child];
let is_assembly = self
.assembly
.occurrences
.iter()
.any(|edge| edge.parent == occurrence.child);
if occurrence.rigid {
let key = if is_assembly {
DocKey::Assembly(occurrence.child)
} else {
DocKey::Leaf((child.pd_ref, NO_FACTOR))
};
rows.push((key, occurrence.placement));
continue;
}
match split_rigid(&occurrence.placement) {
Ok((rigid, factor)) if is_identity(&factor) => {
let key = if is_assembly {
DocKey::Assembly(occurrence.child)
} else {
DocKey::Leaf((child.pd_ref, NO_FACTOR))
};
rows.push((key, rigid));
}
Ok(_) if is_assembly => {
note(
&mut self.first_error,
format!(
"occurrence #{} places sub-assembly '{}' with a non-rigid transform, \
which a nested import cannot represent — import flat instead",
occurrence.nauo_ref,
part_name(child, self.doc_name)
),
);
}
Ok((rigid, factor)) => {
*bakes += 1;
let key = (child.pd_ref, factor_key(&factor));
self.factors.insert(key, factor);
rows.push((DocKey::Leaf(key), rigid));
}
Err(error) => note(&mut self.first_error, error),
}
}
}
fn spend_entry(&mut self) -> Result<(), String> {
self.entries += 1;
if self.entries > MAX_NESTED_ENTRIES {
return Err(format!(
"nested import: more than {MAX_NESTED_ENTRIES} distinct parts \
(import as bodies, or import flat)"
));
}
Ok(())
}
fn spend_bytes(&mut self, bytes: usize) -> Result<(), String> {
self.bytes = self.bytes.saturating_add(bytes);
if self.bytes > MAX_NESTED_BYTES {
return Err(format!(
"nested import: the embedded sub-assembly documents exceed \
{} MB (import as bodies, or import flat)",
MAX_NESTED_BYTES / (1024 * 1024)
));
}
Ok(())
}
}
fn unique_entry_name(library: &serde_json::Map<String, serde_json::Value>, requested: &str) -> String {
if !library.contains_key(requested) {
return requested.to_string();
}
(2..)
.map(|counter| format!("{requested}-{counter}"))
.find(|candidate| !library.contains_key(candidate))
.expect("the counter loop is unbounded")
}
fn note(slot: &mut Option<String>, error: String) {
if slot.is_none() {
*slot = Some(error);
}
}
fn build_library_entry(
product: &brep_kernel::StepProduct,
factor: Option<&Mat4>,
doc_name: &str,
writer: &mut PartWriter<'_>,
) -> Result<String, String> {
let (name, document) = native_part_document(product, factor, doc_name)?;
install_part(&name, &document, writer)
}
fn native_part_document(
product: &brep_kernel::StepProduct,
factor: Option<&Mat4>,
doc_name: &str,
) -> Result<(String, serde_json::Value), String> {
let mut name = part_name(product, doc_name);
let bodies = match factor {
None => product.bodies.clone(),
Some(factor) => {
let transform = brep_kernel::AffineTransform::new(*factor)
.map_err(|error| format!("part '{name}': non-rigid factor: {error}"))?;
let mirrored = transform.determinant3() < 0.0;
name.push_str(if mirrored { " (mirrored)" } else { " (scaled)" });
product
.bodies
.iter()
.map(|body| {
brep_kernel::transform_brep(body, transform, mirrored)
.map_err(|error| format!("part '{name}': {error}"))
})
.collect::<Result<Vec<_>, _>>()?
}
};
let payload = brep_kernel::native_import_payload_with_appearance(
"IMPORT3D1",
&bodies,
&product.appearances,
)
.map_err(|error| format!("part '{name}': {error}"))?;
let document = serde_json::json!({
"features": [{
"type": "IMPORT3D",
"inputParams": { "id": "IMPORT3D1", "nativeBrep": payload },
"persistentData": {},
}]
});
Ok((name, document))
}
fn install_part(
name: &str,
document: &serde_json::Value,
writer: &mut PartWriter<'_>,
) -> Result<String, String> {
let document = document.to_string();
let signature = document_signature(&document);
let source_key = writer.key_for(name, &document, &signature);
brep_kernel::add_part_to_library(name, &source_key, &signature, &document)
.map_err(|error| format!("part '{name}': {error:?}"))
}
pub trait PartSink {
fn store_part(&mut self, part_name: &str, document_json: &str) -> Option<String>;
}
pub struct EmbeddedOnly;
impl PartSink for EmbeddedOnly {
fn store_part(&mut self, _part_name: &str, _document_json: &str) -> Option<String> {
None
}
}
struct PartWriter<'a> {
sink: &'a mut dyn PartSink,
by_signature: std::collections::HashMap<String, String>,
}
impl<'a> PartWriter<'a> {
fn new(sink: &'a mut dyn PartSink) -> Self {
Self {
sink,
by_signature: std::collections::HashMap::new(),
}
}
fn key_for(&mut self, name: &str, document_json: &str, signature: &str) -> String {
if let Some(key) = self.by_signature.get(signature) {
return key.clone();
}
let key = self
.sink
.store_part(name, document_json)
.unwrap_or_default();
self.by_signature.insert(signature.to_string(), key.clone());
key
}
}
fn part_name(product: &brep_kernel::StepProduct, doc_name: &str) -> String {
let named = product.name.trim();
if !named.is_empty() {
return named.to_string();
}
match doc_name.trim() {
"" => format!("part-{}", product.pd_ref),
stem => format!("{stem}-part-{}", product.pd_ref),
}
}
pub(super) fn probe_counts(assembly: &brep_kernel::StepAssembly) -> StepAssemblyProbe {
let mut parts = std::collections::HashSet::new();
let mut instances = 0usize;
let mut nested_depth = 0usize;
for placed in compose_world_occurrences(assembly) {
let product = &assembly.products[placed.product];
if product.bodies.is_empty() {
continue;
}
parts.insert(product.pd_ref);
instances += 1;
nested_depth = nested_depth.max(placed.depth);
}
StepAssemblyProbe {
parts: parts.len(),
instances,
nested_depth,
}
}
fn compose_world_occurrences(assembly: &brep_kernel::StepAssembly) -> Vec<PlacedProduct> {
struct Node {
placed: PlacedProduct,
ancestors: Vec<usize>,
}
let mut out = Vec::new();
let mut stack: Vec<Node> = assembly
.roots
.iter()
.rev()
.map(|&product| Node {
placed: PlacedProduct {
product,
world: MAT4_IDENTITY,
depth: 0,
rigid_path: true,
},
ancestors: vec![product],
})
.collect();
while let Some(node) = stack.pop() {
let (product, world, depth, rigid_path) = (
node.placed.product,
node.placed.world,
node.placed.depth,
node.placed.rigid_path,
);
out.push(node.placed);
let mut children: Vec<&brep_kernel::StepOccurrence> = assembly
.occurrences
.iter()
.filter(|occurrence| occurrence.parent == product)
.collect();
children.sort_by_key(|occurrence| occurrence.nauo_ref);
for occurrence in children.into_iter().rev() {
if node.ancestors.contains(&occurrence.child) {
continue; }
let mut ancestors = node.ancestors.clone();
ancestors.push(occurrence.child);
stack.push(Node {
placed: PlacedProduct {
product: occurrence.child,
world: mat4_mul(&world, &occurrence.placement),
depth: depth + 1,
rigid_path: rigid_path && occurrence.rigid,
},
ancestors,
});
}
}
out
}
const MAT4_IDENTITY: Mat4 = [
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
];
fn mat4_mul(a: &Mat4, b: &Mat4) -> Mat4 {
let mut out = [0.0; 16];
for row in 0..4 {
for column in 0..4 {
out[row * 4 + column] = (0..4)
.map(|k| a[row * 4 + k] * b[k * 4 + column])
.sum();
}
}
out
}
fn split_rigid(world: &Mat4) -> Result<(Mat4, Mat4), String> {
let column = |index: usize| [world[index], world[4 + index], world[8 + index]];
let dot = |a: [f64; 3], b: [f64; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
let axpy = |a: [f64; 3], scale: f64, b: [f64; 3]| {
[a[0] - scale * b[0], a[1] - scale * b[1], a[2] - scale * b[2]]
};
let (a1, a2, a3) = (column(0), column(1), column(2));
let r11 = dot(a1, a1).sqrt();
let mut q1 = normalize(a1, r11)?;
let r12 = dot(q1, a2);
let v2 = axpy(a2, r12, q1);
let r22 = dot(v2, v2).sqrt();
let q2 = normalize(v2, r22)?;
let r13 = dot(q1, a3);
let r23 = dot(q2, a3);
let v3 = axpy(axpy(a3, r13, q1), r23, q2);
let r33 = dot(v3, v3).sqrt();
let q3 = normalize(v3, r33)?;
let cross = [
q2[1] * q3[2] - q2[2] * q3[1],
q2[2] * q3[0] - q2[0] * q3[2],
q2[0] * q3[1] - q2[1] * q3[0],
];
let (mut r11, mut r12, mut r13) = (r11, r12, r13);
if dot(q1, cross) < 0.0 {
q1 = [-q1[0], -q1[1], -q1[2]];
r11 = -r11;
r12 = -r12;
r13 = -r13;
}
let rigid = [
q1[0], q2[0], q3[0], world[3], q1[1], q2[1], q3[1], world[7], q1[2], q2[2], q3[2], world[11], 0.0, 0.0, 0.0, 1.0,
];
let factor = [
r11, r12, r13, 0.0, 0.0, r22, r23, 0.0, 0.0, 0.0, r33, 0.0, 0.0, 0.0, 0.0, 1.0,
];
Ok((rigid, factor))
}
fn normalize(vector: [f64; 3], length: f64) -> Result<[f64; 3], String> {
if !(length > 1e-12) || !length.is_finite() {
return Err("occurrence placement is singular (a degenerate axis)".into());
}
Ok([vector[0] / length, vector[1] / length, vector[2] / length])
}
fn is_identity(matrix: &Mat4) -> bool {
matrix
.iter()
.zip(MAT4_IDENTITY.iter())
.all(|(value, want)| (value - want).abs() <= 1e-9)
}
fn factor_key(factor: &Mat4) -> [u64; 9] {
let mut key = [0u64; 9];
for (slot, index) in key.iter_mut().zip([0, 1, 2, 4, 5, 6, 8, 9, 10]) {
*slot = factor[index].to_bits();
}
key
}
#[cfg(test)]
mod io_tests {
use super::*;
use crate::engine_state::StepProbeOutcome;
fn cube_history(id: &str, size: f64) -> String {
serde_json::json!({
"expressions": "",
"configurator": {},
"features": [{
"type": "P.CU",
"inputParams": {
"id": id,
"sizeX": size, "sizeY": size, "sizeZ": size,
"transform": {
"position": [0.0, 0.0, 0.0],
"rotationEuler": [0.0, 0.0, 0.0],
"scale": [1.0, 1.0, 1.0]
},
"boolean": { "targets": [], "operation": "NONE" }
},
"persistentData": {}
}]
})
.to_string()
}
fn box_step(sx: f64, sy: f64, sz: f64) -> String {
let solid =
brep_kernel::make_box_brep(brep_kernel::Vec3::new(0.0, 0.0, 0.0), sx, sy, sz)
.unwrap();
brep_kernel::export_step(&[solid], "part", "MM", "").unwrap()
}
fn imported_volume(step_text: &str) -> f64 {
let solids = brep_kernel::import_step(step_text).unwrap();
assert_eq!(solids.len(), 1, "STEP round-trips to one solid");
brep_kernel::solid_mass_properties(&solids[0]).unwrap().volume
}
#[test]
fn import_step_feature_adds_the_body_to_the_model() {
let step = box_step(4.0, 3.0, 2.0);
let mut state = EngineState::new();
state.import_step_feature(&step).unwrap();
assert_eq!(state.scene.solids().len(), 1, "one imported body");
let size = state.scene.solids()[0].bbox.size();
assert!(
(size[0] - 4.0).abs() < 1e-4
&& (size[1] - 3.0).abs() < 1e-4
&& (size[2] - 2.0).abs() < 1e-4,
"imported bbox {size:?} != 4x3x2"
);
let mut empty = EngineState::new();
assert!(empty.import_step_feature("not a step file").is_err());
assert_eq!(empty.history_len(), 0, "a bad import adds no feature");
}
#[test]
fn imported_step_colour_reaches_the_engine_metadata_store() {
let step = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../BREP_kernel/tests/fixtures/step-import/freecad_partdesign_body.step"
));
let mut state = EngineState::new();
state.import_step_feature(step).expect("fixture imports");
let name = state.scene.solids()[0].name.clone();
assert_eq!(
state.metadata.attribute(&name, "color"),
Some("#CCCCCC"),
"the imported body colour must reach the store the Info window reads"
);
state.set_metadata_attribute(&name, "color", "#123456");
state.roll_to(0);
state.roll_to(state.history_len());
assert_eq!(
state.metadata.attribute(&name, "color"),
Some("#123456"),
"a user-edited colour must win over the re-stamped import"
);
}
#[test]
fn import_obj_feature_reconstructs_mesh_into_a_cad_body() {
let cube = r#"
v 0 0 0
v 1 0 0
v 1 1 0
v 0 1 0
v 0 0 1
v 1 0 1
v 1 1 1
v 0 1 1
f 1 3 2
f 1 4 3
f 5 6 7
f 5 7 8
f 1 2 6
f 1 6 5
f 2 3 7
f 2 7 6
f 3 4 8
f 3 8 7
f 4 1 5
f 4 5 8
"#;
let mut state = EngineState::new();
state.import_obj_feature(cube).unwrap();
assert_eq!(
state.history_len(),
1,
"mesh import adds one undoable feature"
);
assert_eq!(
state.scene.solids().len(),
1,
"reconstruction yields one body"
);
let size = state.scene.solids()[0].bbox.size();
assert!(
size.iter().all(|axis| (*axis - 1.0).abs() < 1e-4),
"bbox: {size:?}"
);
assert!(
state.history_request_json().contains("ISO-10303-21"),
"history stores the validated reconstructed BREP as STEP"
);
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn import_binary_stl_feature_reconstructs_mesh_into_a_cad_body() {
let bytes = include_bytes!("../../../tests/fixtures/stl/PartDesignExample-Body.stl");
let mut state = EngineState::new();
state.set_runner(Box::new(crate::runner::ThreadRunner::new()));
let submitted = std::time::Instant::now();
state.import_stl_feature(bytes).unwrap();
assert!(state.mesh_imports_pending(), "RANSAC is running off-thread");
assert_eq!(
state.history_len(),
0,
"no feature is added before reconstruction"
);
assert!(
submitted.elapsed() < std::time::Duration::from_secs(1),
"submission must not wait for RANSAC"
);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20);
while state.mesh_imports_pending() || state.run_pending() {
assert!(
std::time::Instant::now() < deadline,
"background import timed out"
);
state.pump();
std::thread::sleep(std::time::Duration::from_millis(2));
}
assert_eq!(state.history_len(), 1);
assert_eq!(state.scene.solids().len(), 1);
assert!(state.history_request_json().contains("ISO-10303-21"));
}
#[test]
fn export_step_text_round_trips_a_box_model() {
let mut state = EngineState::new();
state.set_history_json(&cube_history("Box", 10.0)).unwrap();
let step = state.export_step_text().unwrap();
assert!(step.contains("ISO-10303-21"), "STEP header present");
let volume = imported_volume(&step);
assert!((volume - 1000.0).abs() < 1e-3, "exported volume {volume} != 1000");
let empty = EngineState::new();
assert!(empty.export_step_text().is_err(), "empty model errs on export");
}
#[test]
fn import_export_import_preserves_count_and_volume() {
let step_in = box_step(5.0, 4.0, 3.0); let mut state = EngineState::new();
state.import_step_feature(&step_in).unwrap();
assert_eq!(state.scene.solids().len(), 1);
let step_out = state.export_step_text().unwrap();
let solids = brep_kernel::import_step(&step_out).unwrap();
assert_eq!(solids.len(), 1, "solid count preserved");
let volume = brep_kernel::solid_mass_properties(&solids[0]).unwrap().volume;
assert!((volume - 60.0).abs() < 1e-3, "round-trip volume {volume} != 60");
}
#[test]
fn export_iges_text_round_trips_a_box_model() {
let mut state = EngineState::new();
state.set_history_json(&cube_history("Box", 5.0)).unwrap(); let iges = state.export_iges_text().unwrap();
assert_eq!(iges.chars().nth(72), Some('S'), "first record is the start section");
let solids = brep_kernel::import_iges(&iges).unwrap();
assert_eq!(solids.len(), 1, "one solid round-trips through IGES");
let volume = brep_kernel::solid_mass_properties(&solids[0]).unwrap().volume;
assert!((volume - 125.0).abs() < 1e-3, "IGES round-trip volume {volume} != 125");
let mut other = EngineState::new();
other.import_iges_feature(&iges).unwrap();
assert_eq!(other.scene.solids().len(), 1, "imported one body via the feature");
let empty = EngineState::new();
assert!(empty.export_iges_text().is_err(), "empty model errs on IGES export");
assert!(
other.import_iges_feature("not an iges file").is_err(),
"a non-IGES payload is refused"
);
}
fn sheet_metal_tab_history() -> String {
serde_json::json!({
"expressions": "", "configurator": {},
"features": [
{
"type": "S",
"inputParams": { "id": "SkTab" },
"persistentData": {
"basis": { "origin": [0,0,0], "x": [1,0,0], "y": [0,1,0], "z": [0,0,1] },
"sketch": {
"points": [
{"id":1,"x":0.0,"y":0.0,"fixed":true},
{"id":2,"x":40.0,"y":0.0,"fixed":true},
{"id":3,"x":40.0,"y":25.0,"fixed":true},
{"id":4,"x":0.0,"y":25.0,"fixed":true}
],
"geometries": [
{"id":10,"type":"line","points":[1,2]},
{"id":11,"type":"line","points":[2,3]},
{"id":12,"type":"line","points":[3,4]},
{"id":13,"type":"line","points":[4,1]}
],
"constraints": []
}
},
"timestamp": null
},
{
"type": "SM.TAB",
"inputParams": { "id": "tab1", "profile": "SkTab", "thickness": 2.0, "placementMode": "midplane" },
"persistentData": {},
"timestamp": null
}
]
})
.to_string()
}
#[test]
fn export_flat_pattern_dxf_and_svg_for_a_sheet_metal_part() {
let mut state = EngineState::new();
state.set_history_json(&sheet_metal_tab_history()).unwrap();
let dxf = state.export_flat_pattern_dxf().unwrap();
assert!(dxf.contains("AC1009"), "DXF R12 header present");
assert!(dxf.contains("\nPOLYLINE\n"), "DXF has a polyline entity");
assert!(dxf.trim_end().ends_with("EOF"), "DXF terminates with EOF");
let svg = state.export_flat_pattern_svg().unwrap();
assert!(svg.starts_with("<svg"), "SVG opens with the svg root");
assert!(svg.contains("<path"), "SVG has a path per loop");
assert_eq!(state.history_len(), 2, "flat-pattern export adds no feature");
let mut box_model = EngineState::new();
box_model.set_history_json(&cube_history("Box", 10.0)).unwrap();
let err = box_model.export_flat_pattern_dxf().unwrap_err();
assert_eq!(err, "no sheet-metal body in the part", "clear no-target error");
}
#[test]
fn is_sheet_metal_object_marks_the_sheet_body_not_a_box() {
let mut state = EngineState::new();
state.set_history_json(&sheet_metal_tab_history()).unwrap();
let sheet = state
.scene
.solids()
.iter()
.find(|s| s.is_sheet_metal)
.expect("the SM.TAB body carries the sheet-metal marker");
let solid_name = sheet.name.clone();
let face_name = sheet.faces.iter().find(|f| !f.name.is_empty()).map(|f| f.name.clone());
let edge_name = sheet.edges.iter().find(|e| !e.name.is_empty()).map(|e| e.name.clone());
assert!(state.is_sheet_metal_object(&solid_name), "the solid is sheet metal");
if let Some(face) = face_name {
assert!(state.is_sheet_metal_object(&face), "a face of it is sheet metal");
}
if let Some(edge) = edge_name {
assert!(state.is_sheet_metal_object(&edge), "an edge of it is sheet metal");
}
assert!(!state.is_sheet_metal_object(""), "empty name is not sheet metal");
assert!(!state.is_sheet_metal_object("nope"), "unknown name is not sheet metal");
let mut box_model = EngineState::new();
box_model.set_history_json(&cube_history("Box", 10.0)).unwrap();
assert!(!box_model.is_sheet_metal_object("Box"), "a plain box is not sheet metal");
}
#[test]
fn export_stl_text_emits_ascii_facets() {
let mut state = EngineState::new();
state.set_history_json(&cube_history("Box", 6.0)).unwrap();
let stl = state.export_stl_text().unwrap();
assert!(stl.starts_with("solid brep"), "STL opens with the solid header");
assert!(stl.trim_end().ends_with("endsolid brep"), "STL closes the solid");
assert_eq!(
stl.matches("facet normal").count(),
12,
"a box tessellates to 12 triangles"
);
assert_eq!(stl.matches("vertex").count(), 36, "3 vertices per triangle");
let empty = EngineState::new();
assert!(empty.export_stl_text().is_err(), "empty scene errs on STL export");
}
fn step_fixture(name: &str) -> String {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../BREP_kernel/tests/fixtures/step-import")
.join(name);
std::fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("read fixture {}: {error}", path.display()))
}
fn library() -> serde_json::Map<String, serde_json::Value> {
serde_json::from_str::<serde_json::Value>(&brep_kernel::parts_library_json())
.expect("the parts library serializes as JSON")
.as_object()
.cloned()
.expect("the parts library is an object")
}
fn component_part_names(state: &EngineState) -> Vec<String> {
serde_json::from_str::<serde_json::Value>(&state.history_request_json())
.expect("history JSON")["features"]
.as_array()
.expect("features array")
.iter()
.filter(|feature| feature["type"] == "ACOMP")
.map(|feature| feature["inputParams"]["partName"].as_str().unwrap().to_string())
.collect()
}
fn instance_counts(state: &EngineState) -> std::collections::BTreeMap<String, usize> {
let mut counts = std::collections::BTreeMap::new();
for name in component_part_names(state) {
*counts.entry(name).or_insert(0usize) += 1;
}
counts
}
fn entry_payload(entry: &serde_json::Value) -> String {
entry["document"]["features"][0]["inputParams"]["nativeBrep"]
.as_str()
.expect("the part document is one native IMPORT3D")
.to_string()
}
fn placed_bboxes(state: &EngineState) -> Vec<[i64; 6]> {
let mut out: Vec<[i64; 6]> = state
.scene
.solids()
.iter()
.map(|solid| {
let (center, size) = (solid.bbox.center(), solid.bbox.size());
let q = |value: f64| (value * 1.0e4).round() as i64;
[
q(center[0]),
q(center[1]),
q(center[2]),
q(size[0]),
q(size[1]),
q(size[2]),
]
})
.collect();
out.sort_unstable();
out
}
fn synthetic_assembly(
bodies: Vec<brep_kernel::BrepSolid>,
placements: &[([f64; 16], bool)],
) -> brep_kernel::StepAssembly {
brep_kernel::StepAssembly {
products: vec![
brep_kernel::StepProduct {
pd_ref: 1,
name: "root".into(),
id: "root".into(),
bodies: Vec::new(),
appearances: Vec::new(),
failed_bodies: 0,
},
brep_kernel::StepProduct {
pd_ref: 2,
name: "widget".into(),
id: "widget".into(),
bodies,
appearances: Vec::new(),
failed_bodies: 0,
},
],
occurrences: placements
.iter()
.enumerate()
.map(|(index, (placement, rigid))| brep_kernel::StepOccurrence {
nauo_ref: 10 + index,
parent: 0,
child: 1,
designator: format!("widget-{index}"),
placement: *placement,
rigid: *rigid,
})
.collect(),
roots: vec![0],
first_error: None,
}
}
#[test]
fn import_step_assembly_runs_one_rebuild() {
let text = step_fixture("as1-ug-214.stp");
let mut state = EngineState::new();
let before = state.applied_generation();
let report = state
.import_step_assembly(&text, "as1-ug", StepAssemblyImport::default())
.expect("as1-ug-214 imports as an assembly");
assert!(!report.flat_fallback, "as1-ug-214 carries a product structure");
assert!(
report.instances > 5,
"the fixture is a real assembly: {} instances",
report.instances
);
assert_eq!(
state.applied_generation(),
before + 1,
"ONE rebuild for {} instances, not one per instance",
report.instances
);
assert_eq!(
component_part_names(&state).len(),
report.instances,
"every reported instance is an ACOMP feature"
);
assert!(state.can_undo(), "the import is undoable");
state.undo();
assert!(
component_part_names(&state).is_empty(),
"the whole import undoes in ONE step"
);
}
#[test]
fn import_step_assembly_parses_once() {
let text = step_fixture("as1-ug-214.stp");
let mut state = EngineState::new();
let probe = state
.probe_step_assembly(&text)
.expect("as1-ug-214 parses")
.expect("as1-ug-214 carries structure");
assert!(
state.pending_step_assembly.is_some(),
"the probe stashes THE parse for the consume"
);
assert!(probe.parts > 0 && probe.instances >= probe.parts);
assert!(
probe.nested_depth > 1,
"as1-ug-214 has sub-assemblies: depth {}",
probe.nested_depth
);
let report = state
.import_probed_step_assembly("as1-ug", StepAssemblyImport::default(), &mut EmbeddedOnly)
.expect("the probed assembly imports");
assert!(
state.pending_step_assembly.is_none(),
"the consume TAKES the stash"
);
assert_eq!(
(report.parts, report.instances),
(probe.parts, probe.instances),
"the dialog's counts are the import's counts"
);
assert_eq!(report.baked_nonrigid, 0, "every as1 occurrence is rigid");
assert_eq!(report.failed_products, 0, "every product encodes");
assert!(
state
.import_probed_step_assembly("as1-ug", StepAssemblyImport::default(), &mut EmbeddedOnly)
.is_err(),
"a second consume has nothing to import — never a double insert"
);
}
#[test]
fn probe_submit_and_take_pair_answers_by_id() {
let assembly = step_fixture("as1-ug-214.stp");
let part = step_fixture("analytic_cube.step");
let mut state = EngineState::new();
assert!(state.take_step_probe().is_none());
let flat = state.submit_step_probe(&part);
assert!(!state.step_probes_pending(), "no NAUO text: answered without the runner");
assert_eq!(state.take_step_probe(), Some((flat, StepProbeOutcome::Flat)));
let structured = state.submit_step_probe(&assembly);
assert_ne!(structured, flat, "ids are distinct");
assert!(!state.step_probes_pending());
match state.take_step_probe() {
Some((id, StepProbeOutcome::Structure(probe))) => {
assert_eq!(id, structured);
assert_eq!(probe.instances, 18);
}
other => panic!("expected the as1 structure, got {other:?}"),
}
assert!(state.pending_step_assembly.is_some(), "stashed for the import");
assert!(state.take_step_probe().is_none(), "each answer is taken once");
let broken = state.submit_step_probe("NEXT_ASSEMBLY_USAGE_OCCURRENCE but no header");
assert!(matches!(
state.take_step_probe(),
Some((id, StepProbeOutcome::Failed(_))) if id == broken
));
assert!(state.pending_step_assembly.is_none(), "a failed probe clears the stash");
state.submit_step_probe(&assembly);
state.set_history_json(&cube_history("Box", 4.0)).unwrap();
assert!(state.take_step_probe().is_none());
assert!(state.pending_step_assembly.is_none());
}
#[test]
fn probing_replaces_the_stash_and_never_accumulates() {
let assembly = step_fixture("as1-ug-214.stp");
let part = step_fixture("analytic_cube.step");
let mut state = EngineState::new();
assert!(state.probe_step_assembly(&assembly).unwrap().is_some());
assert!(
state.probe_step_assembly(&part).unwrap().is_none(),
"a part file has no structure"
);
assert!(
state.pending_step_assembly.is_none(),
"a structureless probe must CLEAR the stash, or the next consume \
imports the previous file"
);
assert!(state.probe_step_assembly(&assembly).unwrap().is_some());
state.discard_probed_step_assembly();
assert!(state.pending_step_assembly.is_none(), "Cancel drops the parse");
assert!(state.probe_step_assembly(&assembly).unwrap().is_some());
state.set_history_json(&cube_history("Box", 4.0)).unwrap();
assert!(
state.pending_step_assembly.is_none(),
"a document switch drops a parse that belonged to the old document"
);
}
#[test]
fn import_step_assembly_falls_back_to_flat_for_a_part_file() {
let text = step_fixture("analytic_cube.step");
let mut state = EngineState::new();
assert!(
state.probe_step_assembly(&text).unwrap().is_none(),
"the probe reports no structure, so the app never offers the dialog"
);
let report = state
.import_step_assembly(&text, "analytic_cube", StepAssemblyImport::default())
.expect("the part file still imports");
assert!(report.flat_fallback, "the flat lane ran");
assert_eq!((report.parts, report.instances), (0, 0));
assert!(library().is_empty(), "no parts-library entry for a flat import");
assert_eq!(state.history_len(), 1, "one IMPORT3D feature");
assert!(
state.history_request_json().contains("stepText"),
"the flat lane stores the STEP text, exactly as before"
);
assert!(!state.scene.solids().is_empty(), "the bodies are in the model");
}
#[test]
fn structured_import_dedups_parts() {
let text = step_fixture("as1-ug-214.stp");
let mut state = EngineState::new();
let report = state
.import_step_assembly(&text, "as1-ug", StepAssemblyImport::default())
.expect("as1-ug-214 imports as an assembly");
let library = library();
assert_eq!(
library.len(),
report.parts,
"one entry per unique geometry-bearing product"
);
let counts = instance_counts(&state);
assert_eq!(
counts.values().sum::<usize>(),
report.instances,
"one ACOMP per geometry-bearing occurrence"
);
assert_eq!(
counts.len(),
library.len(),
"every entry is instanced, and every instance names an entry"
);
assert_eq!(
counts.get("bolt").copied(),
Some(6),
"the six-bolt classic: ONE stored bolt, six instances — {counts:?}"
);
assert_eq!(
counts.get("nut").copied(),
Some(8),
"eight nuts (six on the bolts, two on the rods): {counts:?}"
);
assert_eq!(
counts.get("l_bracket").copied(),
Some(2),
"two L-brackets: {counts:?}"
);
assert_eq!(
(report.parts, report.instances),
(5, 18),
"as1-ug-214: 5 distinct parts in 18 places"
);
for (name, entry) in &library {
assert_eq!(
entry["sourceKey"], "",
"'{name}': with no sink there is no file, so the entry must \
stay embedded-only or update-components badges it as falsely \
outdated"
);
assert_eq!(
entry["sourceSignature"],
serde_json::Value::String(document_signature(&entry["document"].to_string())),
"'{name}' signature is the ONE signature fn over its document"
);
assert!(
!entry_payload(entry).is_empty(),
"'{name}' carries a native payload"
);
assert!(
!entry["document"].to_string().contains("ISO-10303-21"),
"'{name}' must store NATIVE geometry, never the STEP text"
);
}
}
#[derive(Default)]
struct RecordingSink {
written: std::collections::BTreeMap<String, String>,
offers: usize,
}
impl PartSink for RecordingSink {
fn store_part(&mut self, part_name: &str, document_json: &str) -> Option<String> {
self.offers += 1;
let key = format!("/models/{part_name}.BREP.json");
self.written.insert(key.clone(), document_json.to_string());
Some(key)
}
}
#[test]
fn a_sink_gives_every_unique_part_a_real_source_key_without_losing_dedup() {
let text = step_fixture("as1-ug-214.stp");
let mut state = EngineState::new();
let mut sink = RecordingSink::default();
state.probe_step_assembly(&text).unwrap();
let report = state
.import_probed_step_assembly("as1-ug", StepAssemblyImport::default(), &mut sink)
.expect("structured import");
assert_eq!(
(report.parts, report.instances),
(5, 18),
"still 5 distinct parts in 18 places — the sink must not split them"
);
assert_eq!(
sink.written.len(),
5,
"one file per DISTINCT part, not per occurrence: {:?}",
sink.written.keys().collect::<Vec<_>>()
);
assert_eq!(
sink.offers, 5,
"and the store is offered each part exactly once — identical \
content is written once, not written and then deduped"
);
let library: serde_json::Map<String, serde_json::Value> = serde_json::from_str(
&brep_kernel::parts_library_json(),
)
.unwrap();
assert_eq!(library.len(), 5, "five entries, one per part");
for (name, entry) in &library {
let key = entry["sourceKey"].as_str().unwrap_or_default();
assert!(!key.is_empty(), "'{name}' must carry a real sourceKey");
let stored = sink
.written
.get(key)
.unwrap_or_else(|| panic!("'{name}' key '{key}' names a written file"));
assert_eq!(
entry["sourceSignature"],
serde_json::Value::String(document_signature(stored)),
"'{name}': the entry's signature and the stored file must \
describe the same content"
);
}
}
#[test]
fn a_declining_sink_leaves_that_part_embedded_and_imports_the_rest() {
struct PickySink;
impl PartSink for PickySink {
fn store_part(&mut self, part_name: &str, _document: &str) -> Option<String> {
(part_name != "bolt").then(|| format!("/models/{part_name}.BREP.json"))
}
}
let text = step_fixture("as1-ug-214.stp");
let mut state = EngineState::new();
state.probe_step_assembly(&text).unwrap();
let report = state
.import_probed_step_assembly("as1-ug", StepAssemblyImport::default(), &mut PickySink)
.expect("structured import");
assert_eq!(
(report.parts, report.instances),
(5, 18),
"the import is unaffected by one refused write"
);
let library: serde_json::Map<String, serde_json::Value> =
serde_json::from_str(&brep_kernel::parts_library_json()).unwrap();
assert_eq!(
library["bolt"]["sourceKey"], "",
"the refused part falls back to embedded-only"
);
for (name, entry) in library.iter().filter(|(name, _)| name.as_str() != "bolt") {
assert!(
!entry["sourceKey"].as_str().unwrap_or_default().is_empty(),
"'{name}' still got its file"
);
}
}
#[test]
fn structured_import_matches_the_flat_lane_geometry() {
let text = step_fixture("as1-ug-214.stp");
let mut flat = EngineState::new();
flat.import_step_feature(&text).expect("flat import");
let mut structured = EngineState::new();
structured
.import_step_assembly(&text, "as1-ug", StepAssemblyImport::default())
.expect("structured import");
assert_eq!(
structured.scene.solids().len(),
flat.scene.solids().len(),
"same body count"
);
assert_eq!(
placed_bboxes(&structured),
placed_bboxes(&flat),
"every component must sit where the flat lane's baked body sits"
);
}
#[test]
fn native_part_document_heals_and_converges() {
let text = step_fixture("as1-ug-214.stp");
let mut state = EngineState::new();
state
.import_step_assembly(&text, "as1-ug", StepAssemblyImport::default())
.expect("as1-ug-214 imports as an assembly");
let inserted = library();
let heal_once = |state: &mut EngineState| -> serde_json::Map<String, serde_json::Value> {
let mut document: serde_json::Value =
serde_json::from_str(&state.history_request_json()).expect("document JSON");
for (_, entry) in document["partsLibrary"]
.as_object_mut()
.expect("the document carries the library")
.iter_mut()
{
entry["snapshot"] = serde_json::Value::String(String::new());
}
state.set_history_json(&document.to_string()).expect("reopen");
let healed = library();
for (name, entry) in &healed {
assert!(
!entry["snapshot"].as_str().unwrap_or_default().is_empty(),
"'{name}' must have healed its cleared snapshot"
);
}
healed
};
let first = heal_once(&mut state);
let second = heal_once(&mut state);
assert_eq!(
first, second,
"a second heal must reproduce the first BYTE for byte"
);
assert_eq!(first.len(), inserted.len(), "the heal keeps the same entries");
for (name, entry) in &first {
assert_eq!(
entry["snapshot"],
inserted[name.as_str()]["snapshot"],
"'{name}': the heal must reproduce what the INSERT stored — the \
whole reason the import goes through add_part_to_library"
);
let payload = brep_kernel::restore_solids(&entry_payload(entry))
.expect("the stored payload decodes");
let healed = brep_kernel::restore_solids(entry["snapshot"].as_str().unwrap())
.expect("the healed snapshot decodes");
let names = |snapshot: &brep_kernel::RestoredSnapshot| -> Vec<String> {
snapshot.solids.iter().map(|solid| solid.name.clone()).collect()
};
assert_eq!(names(&healed), names(&payload), "'{name}': identical body names");
for (healed, stored) in healed.solids.iter().zip(payload.solids.iter()) {
let (healed_data, healed_names) = brep_kernel::encode_solid(&healed.solid).unwrap();
let (stored_data, stored_names) = brep_kernel::encode_solid(&stored.solid).unwrap();
assert_eq!(healed_data, stored_data, "'{name}': identical geometry");
assert_eq!(
healed_names.faces, stored_names.faces,
"'{name}': identical face names"
);
assert_eq!(
healed_names.edges, stored_names.edges,
"'{name}': identical edge names"
);
}
assert!(
healed.metadata.len() >= payload.metadata.len(),
"'{name}': the heal's snapshot is a superset (sourceFeatureId records)"
);
}
}
#[test]
fn imported_part_payloads_never_capture_the_live_documents_metadata() {
let step = box_step(4.0, 3.0, 2.0);
let bodies = brep_kernel::import_step(&step).expect("the box imports");
let mut state = EngineState::new();
state.import_step_feature(&step).expect("flat import");
let leaked = brep_kernel::restore_solids(
&brep_kernel::native_import_payload("IMPORT3D1", &bodies).unwrap(),
)
.unwrap();
assert!(
!leaked.metadata.is_empty(),
"the live document must actually hold records under these names"
);
state.pending_step_assembly = Some(synthetic_assembly(bodies.clone(), &[(MAT4_IDENTITY, true)]));
state
.import_probed_step_assembly("collide", StepAssemblyImport::default(), &mut EmbeddedOnly)
.expect("the synthetic assembly imports");
let entry = library().into_iter().next().expect("one entry").1;
let stored = brep_kernel::restore_solids(&entry_payload(&entry)).expect("payload decodes");
assert!(
stored.metadata.is_empty(),
"the part's payload must carry the PART's metadata (it has none), \
never the live document's: {:?}",
stored.metadata
);
let after = brep_kernel::restore_solids(
&brep_kernel::native_import_payload("IMPORT3D1", &bodies).unwrap(),
)
.unwrap();
assert_eq!(
after.metadata, leaked.metadata,
"the live document's scene metadata must survive the import"
);
}
#[test]
fn nonrigid_occurrence_bakes_a_distinct_part() {
let bodies = brep_kernel::import_step(&box_step(4.0, 3.0, 2.0)).expect("box imports");
let mirrored = [
-1.0, 0.0, 0.0, 20.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
];
let mut state = EngineState::new();
state.pending_step_assembly = Some(synthetic_assembly(
bodies,
&[(MAT4_IDENTITY, true), (mirrored, false)],
));
let report = state
.import_probed_step_assembly("mirror", StepAssemblyImport::default(), &mut EmbeddedOnly)
.expect("the mirrored assembly imports");
assert_eq!(report.instances, 2, "both occurrences become components");
assert_eq!(report.baked_nonrigid, 1, "one of them baked its factor");
assert_eq!(report.parts, 2, "the mirrored instance is its OWN part");
let counts = instance_counts(&state);
assert_eq!(
counts.get("widget").copied(),
Some(1),
"the plain instance keeps the plain part: {counts:?}"
);
assert_eq!(
counts.get("widget (mirrored)").copied(),
Some(1),
"the mirrored instance gets its own entry: {counts:?}"
);
assert_eq!(state.scene.solids().len(), 2);
let mut centers: Vec<f64> = state
.scene
.solids()
.iter()
.map(|solid| solid.bbox.center()[0])
.collect();
centers.sort_by(|a, b| a.partial_cmp(b).expect("finite"));
assert!(
(centers[0] + centers[1] - 20.0).abs() < 1e-3
&& (centers[1] - centers[0]).abs() > 1e-3,
"the mirror must land at 20 − x̄, not on top of its twin: {centers:?}"
);
}
const NESTED: StepAssemblyImport = StepAssemblyImport { nested: true };
fn child_library(document: &serde_json::Value) -> serde_json::Map<String, serde_json::Value> {
document["partsLibrary"]
.as_object()
.cloned()
.unwrap_or_default()
}
fn child_components(document: &serde_json::Value) -> Vec<String> {
document["features"]
.as_array()
.map(Vec::as_slice)
.unwrap_or_default()
.iter()
.filter(|feature| feature["type"] == "ACOMP")
.map(|feature| feature["inputParams"]["partName"].as_str().unwrap().to_string())
.collect()
}
fn world_placed(state: &mut EngineState) -> Vec<[f64; 10]> {
state.ensure_assembly_synced();
let library = library();
let mut out = Vec::new();
for record in state.assembly_components() {
let entry = &library[record.part_name.as_str()];
let restored = brep_kernel::restore_solids(entry["snapshot"].as_str().unwrap())
.expect("every entry's snapshot decodes");
let mirrored = record.transform.determinant3() < 0.0;
for solid in &restored.solids {
let posed = brep_kernel::transform_brep(&solid.solid, record.transform, mirrored)
.expect("a component pose is rigid");
let mass =
brep_kernel::solid_mass_properties_full(&posed).expect("mass properties");
let mut lo = [f64::INFINITY; 3];
let mut hi = [f64::NEG_INFINITY; 3];
for vertex in &posed.vertices {
for axis in 0..3 {
let value = [vertex.point.x, vertex.point.y, vertex.point.z][axis];
lo[axis] = lo[axis].min(value);
hi[axis] = hi[axis].max(value);
}
}
out.push([
mass.volume,
mass.centroid.x,
mass.centroid.y,
mass.centroid.z,
lo[0],
lo[1],
lo[2],
hi[0],
hi[1],
hi[2],
]);
}
}
out.sort_by_key(|row| row.map(|value| (value * 1.0e6).round() as i64));
out
}
fn assert_placed_eq(a: &[[f64; 10]], b: &[[f64; 10]], tolerance: f64, what: &str) {
assert_eq!(a.len(), b.len(), "{what}: solid count");
for (index, (left, right)) in a.iter().zip(b.iter()).enumerate() {
for (column, (l, r)) in left.iter().zip(right.iter()).enumerate() {
assert!(
(l - r).abs() <= tolerance,
"{what}: solid {index} column {column}: {l} != {r}"
);
}
}
}
#[test]
fn nested_import_builds_child_libraries() {
let text = step_fixture("as1-ug-214.stp");
let mut state = EngineState::new();
let report = state
.import_step_assembly(&text, "as1-ug", NESTED)
.expect("as1-ug-214 imports as a nested assembly");
assert!(!report.flat_fallback, "the structured lane ran");
assert_eq!(
(report.parts, report.instances),
(3, 4),
"the ROOT's children: plate, lb_assem ×2, rod_assem — a sub-assembly \
is ONE component (build-spec §2.2), not one per leaf body"
);
assert_eq!(report.failed_products, 0, "every product encodes");
assert_eq!(report.baked_nonrigid, 0, "every as1 occurrence is rigid");
assert_eq!(
instance_counts(&state),
[("lb_assem".to_string(), 2), ("plate".to_string(), 1), ("rod_assem".to_string(), 1)]
.into_iter()
.collect::<std::collections::BTreeMap<_, _>>(),
);
let library = library();
let lb = &library["lb_assem"]["document"];
assert_eq!(
child_library(lb).keys().cloned().collect::<Vec<_>>(),
vec!["l_bracket".to_string(), "nba".to_string()],
"the sub-document's library is its own (build-spec §2.2: a part \
reused across levels is stored once PER level)"
);
assert_eq!(
child_components(lb),
vec!["l_bracket", "nba", "nba", "nba"],
"one ACOMP per child occurrence — three nut-bolt assemblies"
);
let nba = &child_library(lb)["nba"]["document"];
assert_eq!(
child_library(nba).keys().cloned().collect::<Vec<_>>(),
vec!["bolt".to_string(), "nut".to_string()],
);
assert_eq!(child_components(nba), vec!["bolt", "nut"]);
for (name, entry) in child_library(nba) {
assert_eq!(
entry["sourceKey"], "",
"'{name}': no sink, so the nested child stays embedded"
);
assert!(!entry_payload(&entry).is_empty(), "'{name}' is a native part");
}
let names: Vec<&str> = state
.scene
.solids()
.iter()
.map(|solid| solid.name.as_str())
.collect();
assert!(
names.iter().any(|name| name.matches("ACOMP").count() == 3),
"a three-level chain must appear in the scene names: {names:?}"
);
assert!(
names.iter().any(|name| name.starts_with("ACOMP2:ACOMP2:ACOMP1:")),
"the chained prefix the structure tree reads back: {names:?}"
);
assert_eq!(
names.len(),
18,
"the same 18 bodies the flat lane produces, reached through the tree"
);
}
#[test]
fn nested_matches_flat_for_a_depth_one_tree() {
let text = step_fixture("AssemblyExample-Assembly.step");
let mut state = EngineState::new();
let probe = state
.probe_step_assembly(&text)
.unwrap()
.expect("the fixture carries structure");
assert_eq!(probe.nested_depth, 1, "the fixture must be depth 1");
let mut flat = EngineState::new();
let flat_report = flat
.import_step_assembly(&text, "example", StepAssemblyImport::default())
.expect("flat");
let flat_document = flat.history_request_json();
let mut nested = EngineState::new();
let nested_report = nested
.import_step_assembly(&text, "example", NESTED)
.expect("nested");
assert_eq!(nested_report, flat_report, "identical report");
assert_eq!(
nested.history_request_json(),
flat_document,
"a depth-1 nested import must produce the FLAT document, byte for byte"
);
let bodies = brep_kernel::import_step(&box_step(4.0, 3.0, 2.0)).expect("box imports");
let placed = [
1.0, 0.0, 0.0, 12.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
];
let mirrored = [
-1.0, 0.0, 0.0, 30.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
];
let with_root_bodies = || {
let mut assembly =
synthetic_assembly(bodies.clone(), &[(placed, true), (mirrored, false)]);
assembly.products[0].bodies = bodies.clone();
assembly
};
let mut flat = EngineState::new();
flat.pending_step_assembly = Some(with_root_bodies());
flat.import_probed_step_assembly("root", StepAssemblyImport::default(), &mut EmbeddedOnly)
.expect("flat");
let flat_document = flat.history_request_json();
let mut nested = EngineState::new();
nested.pending_step_assembly = Some(with_root_bodies());
let report = nested
.import_probed_step_assembly("root", NESTED, &mut EmbeddedOnly)
.expect("nested");
assert_eq!(report.baked_nonrigid, 1, "the mirrored occurrence baked");
assert_eq!(
nested.history_request_json(),
flat_document,
"interior geometry AT THE ROOT, and a mirrored leaf, are the same \
components in both lanes"
);
}
#[test]
fn interior_node_geometry_lives_inside_its_own_document() {
let mid_bodies = brep_kernel::import_step(&box_step(6.0, 6.0, 1.0)).expect("plate");
let leaf_bodies = brep_kernel::import_step(&box_step(2.0, 2.0, 2.0)).expect("stud");
let shift = |x: f64, z: f64| {
[
1.0, 0.0, 0.0, x, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, z, 0.0, 0.0, 0.0, 1.0,
]
};
let assembly = || brep_kernel::StepAssembly {
products: vec![
brep_kernel::StepProduct {
pd_ref: 1,
name: "root".into(),
id: "root".into(),
bodies: Vec::new(),
appearances: Vec::new(),
failed_bodies: 0,
},
brep_kernel::StepProduct {
pd_ref: 2,
name: "mid".into(),
id: "mid".into(),
bodies: mid_bodies.clone(),
appearances: Vec::new(),
failed_bodies: 0,
},
brep_kernel::StepProduct {
pd_ref: 3,
name: "stud".into(),
id: "stud".into(),
bodies: leaf_bodies.clone(),
appearances: Vec::new(),
failed_bodies: 0,
},
],
occurrences: vec![
brep_kernel::StepOccurrence {
nauo_ref: 10,
parent: 0,
child: 1,
designator: "mid-1".into(),
placement: shift(20.0, 0.0),
rigid: true,
},
brep_kernel::StepOccurrence {
nauo_ref: 11,
parent: 1,
child: 2,
designator: "stud-1".into(),
placement: shift(2.0, 1.0),
rigid: true,
},
],
roots: vec![0],
first_error: None,
};
let mut nested = EngineState::new();
nested.pending_step_assembly = Some(assembly());
let report = nested
.import_probed_step_assembly("interior", NESTED, &mut EmbeddedOnly)
.expect("nested import");
assert_eq!(
(report.parts, report.instances),
(1, 1),
"ONE component — `mid` and everything under it"
);
let document = &library()["mid"]["document"];
let kinds: Vec<&str> = document["features"]
.as_array()
.unwrap()
.iter()
.map(|feature| feature["type"].as_str().unwrap())
.collect();
assert_eq!(
kinds,
vec!["IMPORT3D", "ACOMP"],
"the node's OWN bodies ride in ITS document, alongside its children"
);
assert_eq!(child_components(document), vec!["stud"]);
assert!(
!document["features"][0]["inputParams"]["nativeBrep"]
.as_str()
.unwrap_or_default()
.is_empty(),
"the interior geometry is a native payload, not STEP text"
);
let names: Vec<&str> = nested
.scene
.solids()
.iter()
.map(|solid| solid.name.as_str())
.collect();
assert!(names.contains(&"ACOMP1:IMPORT3D1"), "mid's own body: {names:?}");
assert!(
names.contains(&"ACOMP1:ACOMP1:IMPORT3D1"),
"the stud, one level deeper: {names:?}"
);
let nested_geometry = world_placed(&mut nested);
let mut flat = EngineState::new();
flat.pending_step_assembly = Some(assembly());
flat.import_probed_step_assembly("interior", StepAssemblyImport::default(), &mut EmbeddedOnly)
.expect("flat import");
assert_eq!(
placed_bboxes(&nested),
placed_bboxes(&flat),
"interior geometry must sit where the flat lane's composed pose puts it"
);
assert_placed_eq(
&nested_geometry,
&world_placed(&mut flat),
1.0e-9,
"interior node, nested vs flat",
);
}
#[test]
fn a_mirrored_sub_assembly_is_reported_not_silently_mis_handed() {
let bodies = brep_kernel::import_step(&box_step(4.0, 3.0, 2.0)).expect("box imports");
let mirror = [
-1.0, 0.0, 0.0, 30.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
];
let assembly = || brep_kernel::StepAssembly {
products: vec![
brep_kernel::StepProduct {
pd_ref: 1,
name: "root".into(),
id: "root".into(),
bodies: Vec::new(),
appearances: Vec::new(),
failed_bodies: 0,
},
brep_kernel::StepProduct {
pd_ref: 2,
name: "subasm".into(),
id: "subasm".into(),
bodies: Vec::new(),
appearances: Vec::new(),
failed_bodies: 0,
},
brep_kernel::StepProduct {
pd_ref: 3,
name: "widget".into(),
id: "widget".into(),
bodies: bodies.clone(),
appearances: Vec::new(),
failed_bodies: 0,
},
],
occurrences: vec![
brep_kernel::StepOccurrence {
nauo_ref: 10,
parent: 0,
child: 1,
designator: "sub".into(),
placement: mirror,
rigid: false,
},
brep_kernel::StepOccurrence {
nauo_ref: 11,
parent: 0,
child: 2,
designator: "loose".into(),
placement: MAT4_IDENTITY,
rigid: true,
},
brep_kernel::StepOccurrence {
nauo_ref: 12,
parent: 1,
child: 2,
designator: "inner".into(),
placement: MAT4_IDENTITY,
rigid: true,
},
],
roots: vec![0],
first_error: None,
};
let mut state = EngineState::new();
state.pending_step_assembly = Some(assembly());
let report = state
.import_probed_step_assembly("mirror-sub", NESTED, &mut EmbeddedOnly)
.expect("the rest of the file still imports");
assert_eq!(report.instances, 1, "only the plain leaf lands");
assert_eq!(report.baked_nonrigid, 0, "a sub-assembly is never baked");
assert!(
report
.first_error
.as_deref()
.is_some_and(|error| error.contains("import flat instead")),
"the user is told what to do instead: {:?}",
report.first_error
);
let mut flat = EngineState::new();
flat.pending_step_assembly = Some(assembly());
let flat_report = flat
.import_probed_step_assembly("mirror-sub", StepAssemblyImport::default(), &mut EmbeddedOnly)
.expect("flat");
assert_eq!(
(flat_report.instances, flat_report.baked_nonrigid),
(2, 1),
"flat places both and bakes the mirrored one"
);
}
#[test]
fn nested_import_matches_the_flat_lane_geometry() {
let text = step_fixture("as1-ug-214.stp");
let mut flat = EngineState::new();
flat.import_step_assembly(&text, "as1-ug", StepAssemblyImport::default())
.expect("flat import");
let flat_geometry = world_placed(&mut flat);
let mut nested = EngineState::new();
nested
.import_step_assembly(&text, "as1-ug", NESTED)
.expect("nested import");
let nested_geometry = world_placed(&mut nested);
assert_eq!(flat_geometry.len(), 18, "as1-ug-214 places 18 bodies");
assert_placed_eq(&nested_geometry, &flat_geometry, 1.0e-9, "nested vs flat");
assert_eq!(placed_bboxes(&nested), placed_bboxes(&flat));
}
#[test]
fn nested_part_documents_heal_and_converge() {
let text = step_fixture("as1-ug-214.stp");
let mut state = EngineState::new();
state
.import_step_assembly(&text, "as1-ug", NESTED)
.expect("nested import");
let inserted = library();
let heal_once = |state: &mut EngineState| {
let mut document: serde_json::Value =
serde_json::from_str(&state.history_request_json()).expect("document JSON");
for (_, entry) in document["partsLibrary"].as_object_mut().unwrap().iter_mut() {
entry["snapshot"] = serde_json::Value::String(String::new());
}
state.set_history_json(&document.to_string()).expect("reopen");
library()
};
let first = heal_once(&mut state);
let second = heal_once(&mut state);
assert_eq!(first, second, "a second heal reproduces the first");
assert_eq!(
first, inserted,
"a heal of a SUB-ASSEMBLY entry reproduces what the insert stored — \
the inner entries carry no snapshot, so this is the whole recursive \
re-execution converging"
);
for (name, entry) in &first {
assert!(
!entry["snapshot"].as_str().unwrap_or_default().is_empty(),
"'{name}' healed its cleared snapshot"
);
}
}
#[test]
fn nested_import_guards_cycles_and_depth() {
let bodies = brep_kernel::import_step(&box_step(2.0, 2.0, 2.0)).expect("box imports");
let shift = |x: f64| {
[
1.0, 0.0, 0.0, x, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
]
};
let chain = |levels: usize, cycle: bool| {
let mut products: Vec<brep_kernel::StepProduct> = (0..=levels)
.map(|index| brep_kernel::StepProduct {
pd_ref: index + 1,
name: format!("n{index}"),
id: format!("n{index}"),
bodies: (index == levels).then(|| bodies.clone()).unwrap_or_default(),
appearances: Vec::new(),
failed_bodies: 0,
})
.collect();
products[0].name = "root".into();
let mut occurrences: Vec<brep_kernel::StepOccurrence> = (0..levels)
.map(|index| brep_kernel::StepOccurrence {
nauo_ref: 100 + index,
parent: index,
child: index + 1,
designator: format!("link{index}"),
placement: shift(1.0),
rigid: true,
})
.collect();
if cycle {
occurrences.push(brep_kernel::StepOccurrence {
nauo_ref: 90,
parent: levels,
child: 1,
designator: "back".into(),
placement: shift(1.0),
rigid: true,
});
}
brep_kernel::StepAssembly {
products,
occurrences,
roots: vec![0],
first_error: None,
}
};
let mut state = EngineState::new();
state.pending_step_assembly = Some(chain(3, true));
let report = state
.import_probed_step_assembly("cyclic", NESTED, &mut EmbeddedOnly)
.expect("a cyclic structure still imports what it can");
assert_eq!(report.instances, 1, "the root places its one child");
assert!(
report
.first_error
.as_deref()
.is_some_and(|error| error.contains("cycle")),
"the skipped back-edge is reported: {:?}",
report.first_error
);
assert!(!state.scene.solids().is_empty(), "the geometry still arrives");
let mut state = EngineState::new();
state.pending_step_assembly = Some(chain(MAX_NESTED_DEPTH + 40, false));
let error = state
.import_probed_step_assembly("deep", NESTED, &mut EmbeddedOnly)
.expect_err("a 100-level nesting has no usable document");
assert!(
error.contains("no part of the assembly could be built"),
"the dialog's cue to fall back to the flat import: {error}"
);
let mut state = EngineState::new();
state.pending_step_assembly = Some(chain(MAX_NESTED_DEPTH + 40, false));
assert!(
state
.import_probed_step_assembly("deep", StepAssemblyImport::default(), &mut EmbeddedOnly)
.is_ok(),
"the FLAT lane composes instead of embedding, so depth costs it nothing"
);
}
#[test]
fn add_features_appends_a_batch_in_one_rebuild() {
let mut state = EngineState::new();
state.set_history_json(&cube_history("Box", 4.0)).unwrap();
let before = state.applied_generation();
state.add_features(&[]);
assert_eq!(
(state.applied_generation(), state.history_len()),
(before, 1),
"an empty batch neither re-runs nor appends"
);
let features: Vec<serde_json::Value> = (0..3)
.map(|index| {
serde_json::json!({
"type": "P.CU",
"inputParams": {
"id": format!("Cube{index}"),
"sizeX": 2.0, "sizeY": 2.0, "sizeZ": 2.0,
"transform": {
"position": [10.0 * index as f64, 0.0, 0.0],
"rotationEuler": [0.0, 0.0, 0.0],
"scale": [1.0, 1.0, 1.0]
},
"boolean": { "targets": [], "operation": "NONE" }
},
"persistentData": {}
})
})
.collect();
state.add_features(&features);
assert_eq!(state.history_len(), 4, "all three appended");
assert_eq!(
state.applied_generation(),
before + 1,
"ONE rebuild for the batch"
);
assert_eq!(state.scene.solids().len(), 4);
state.undo();
assert_eq!(state.history_len(), 1, "the batch undoes in ONE step");
}
}