use super::{
finish_step_file, id_list, mat4_mul, pmi, step_string, write_file_contexts, write_product,
write_product_geometry, Mat4, StepExportReport, StepItemOwner, StepNameMaps, StepPmi,
StepWriter, MAT4_IDENTITY,
};
use crate::{BrepSolid, Vec3};
#[derive(Clone, Debug, Default)]
pub struct StepExportProduct {
pub name: String,
pub id: String,
pub bodies: Vec<(String, BrepSolid)>,
}
#[derive(Clone, Debug)]
pub struct StepExportOccurrence {
pub designator: String,
pub parent: usize,
pub child: usize,
pub placement: Mat4,
}
#[derive(Clone, Debug, Default)]
pub struct StepAssemblyExport {
pub products: Vec<StepExportProduct>,
pub occurrences: Vec<StepExportOccurrence>,
}
const MAX_OCCURRENCE_PATHS: usize = 100_000;
impl StepAssemblyExport {
pub fn root(&self) -> Result<usize, String> {
if self.products.is_empty() {
return Err("export_step: an assembly needs at least one product".into());
}
let count = self.products.len();
let mut is_child = vec![false; count];
for occurrence in &self.occurrences {
if occurrence.parent >= count || occurrence.child >= count {
return Err(format!(
"export_step: occurrence '{}' names a product outside the structure",
occurrence.designator
));
}
if occurrence.parent == occurrence.child {
return Err(format!(
"export_step: occurrence '{}' places a product inside itself",
occurrence.designator
));
}
rigid(&occurrence.placement).map_err(|error| {
format!(
"export_step: occurrence '{}' placement: {error}",
occurrence.designator
)
})?;
is_child[occurrence.child] = true;
}
let roots: Vec<usize> = (0..count).filter(|index| !is_child[*index]).collect();
let [root] = roots.as_slice() else {
return Err(format!(
"export_step: an assembly needs exactly one root product, found {}",
roots.len()
));
};
let mut reached = vec![false; count];
let mut stack = vec![*root];
reached[*root] = true;
while let Some(product) = stack.pop() {
for occurrence in &self.occurrences {
if occurrence.parent == product && !reached[occurrence.child] {
reached[occurrence.child] = true;
stack.push(occurrence.child);
}
}
}
if let Some(lost) = reached.iter().position(|hit| !hit) {
return Err(format!(
"export_step: product '{}' is not reachable from the root",
self.products[lost].name
));
}
Ok(*root)
}
fn occurrence_paths(&self, root: usize) -> Vec<Vec<(String, Mat4)>> {
let mut paths: Vec<Vec<(String, Mat4)>> = vec![Vec::new(); self.products.len()];
let mut total = 0usize;
let mut stack: Vec<(usize, String, Mat4, Vec<usize>)> =
vec![(root, String::new(), MAT4_IDENTITY, vec![root])];
while let Some((product, prefix, world, ancestors)) = stack.pop() {
if total >= MAX_OCCURRENCE_PATHS {
break;
}
total += 1;
paths[product].push((prefix.clone(), world));
for occurrence in &self.occurrences {
if occurrence.parent != product || ancestors.contains(&occurrence.child) {
continue;
}
let mut ancestors = ancestors.clone();
ancestors.push(occurrence.child);
stack.push((
occurrence.child,
format!("{prefix}{}:", occurrence.designator),
mat4_mul(&world, &occurrence.placement),
ancestors,
));
}
}
paths
}
}
fn rigid(matrix: &Mat4) -> Result<(), String> {
if matrix.iter().any(|value| !value.is_finite()) {
return Err("matrix is not finite".into());
}
let column = |index: usize| {
[
matrix[index],
matrix[4 + index],
matrix[8 + index],
]
};
let dot = |a: [f64; 3], b: [f64; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
let (x, y, z) = (column(0), column(1), column(2));
const TOLERANCE: f64 = 1e-9;
for (a, b, expected) in [
(x, x, 1.0),
(y, y, 1.0),
(z, z, 1.0),
(x, y, 0.0),
(x, z, 0.0),
(y, z, 0.0),
] {
if (dot(a, b) - expected).abs() > TOLERANCE {
return Err("matrix is not a rotation (mirrored or scaled)".into());
}
}
let cross = [
x[1] * y[2] - x[2] * y[1],
x[2] * y[0] - x[0] * y[2],
x[0] * y[1] - x[1] * y[0],
];
if (dot(cross, z) - 1.0).abs() > TOLERANCE {
return Err("matrix is left-handed (a mirrored instance)".into());
}
Ok(())
}
fn write_placement_axis(writer: &mut StepWriter, matrix: &Mat4) -> Result<usize, String> {
super::write_placement(
writer,
Vec3::new(matrix[3], matrix[7], matrix[11]),
Vec3::new(matrix[2], matrix[6], matrix[10]),
Vec3::new(matrix[0], matrix[4], matrix[8]),
)
}
pub fn export_step_assembly(
assembly: &StepAssemblyExport,
unit: &str,
timestamp: &str,
) -> Result<String, String> {
export_step_assembly_report(assembly, unit, timestamp, None).map(|report| report.text)
}
pub fn export_step_assembly_report(
assembly: &StepAssemblyExport,
unit: &str,
timestamp: &str,
pmi: Option<&StepPmi<'_>>,
) -> Result<StepExportReport, String> {
let root = assembly.root()?;
if assembly
.products
.iter()
.all(|product| product.bodies.is_empty())
{
return Err("export_step: at least one solid is required".into());
}
let mut report = StepExportReport {
products: assembly.products.len(),
occurrences: assembly.occurrences.len(),
..StepExportReport::default()
};
let mut writer = StepWriter::default();
let contexts = write_file_contexts(&mut writer, unit)?;
let mut geometries = Vec::with_capacity(assembly.products.len());
for product in &assembly.products {
let bodies: Vec<(String, &BrepSolid)> = product
.bodies
.iter()
.map(|(name, solid)| (name.clone(), solid))
.collect();
geometries.push(write_product_geometry(
&mut writer,
&contexts,
&bodies,
&mut report,
)?);
}
let mut placement_axes = Vec::with_capacity(assembly.occurrences.len());
for occurrence in &assembly.occurrences {
placement_axes.push(write_placement_axis(&mut writer, &occurrence.placement)?);
}
let geometry_context = contexts.geometry_context;
let mut representations = Vec::with_capacity(assembly.products.len());
for (index, product) in assembly.products.iter().enumerate() {
let mut items = vec![contexts.axis];
items.extend(&geometries[index].solids);
for (slot, occurrence) in assembly.occurrences.iter().enumerate() {
if occurrence.parent == index {
items.push(placement_axes[slot]);
}
}
let keyword = if geometries[index].solids.is_empty() {
"SHAPE_REPRESENTATION"
} else {
"ADVANCED_BREP_SHAPE_REPRESENTATION"
};
representations.push(writer.add(format!(
"{keyword}('{}',{},#{geometry_context})",
step_string(&product.name),
id_list(&items)
)));
}
let mut definitions = Vec::with_capacity(assembly.products.len());
for (index, product) in assembly.products.iter().enumerate() {
let places_others = assembly
.occurrences
.iter()
.any(|occurrence| occurrence.parent == index);
let category = if places_others { "assembly" } else { "part" };
let ids = write_product(&mut writer, &contexts, &product.name, &product.id, category);
let (product_shape, representation) = (ids.product_shape, representations[index]);
writer.add(format!(
"SHAPE_DEFINITION_REPRESENTATION(#{product_shape},#{representation})"
));
definitions.push(ids);
}
for (slot, occurrence) in assembly.occurrences.iter().enumerate() {
let parent = definitions[occurrence.parent].definition;
let child = definitions[occurrence.child].definition;
let label = step_string(&occurrence.designator);
let nauo = writer.add(format!(
"NEXT_ASSEMBLY_USAGE_OCCURRENCE('{label}','{label}','',#{parent},#{child},'{label}')"
));
let nauo_shape =
writer.add(format!("PRODUCT_DEFINITION_SHAPE('','NAUO PRDDFN',#{nauo})"));
let identity = contexts.axis;
let placement = placement_axes[slot];
let transformation = writer.add(format!(
"ITEM_DEFINED_TRANSFORMATION('','',#{identity},#{placement})"
));
let (child_rep, parent_rep) = (
representations[occurrence.child],
representations[occurrence.parent],
);
let relationship = writer.add(format!(
"(REPRESENTATION_RELATIONSHIP('','',#{child_rep},#{parent_rep})\
REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#{transformation})\
SHAPE_REPRESENTATION_RELATIONSHIP())"
));
writer.add(format!(
"CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#{relationship},#{nauo_shape})"
));
}
if let Some(pmi) = pmi {
let paths = assembly.occurrence_paths(root);
let mut names = StepNameMaps::default();
for (index, geometry) in geometries.iter().enumerate() {
let owner = StepItemOwner {
product_shape: definitions[index].product_shape,
representation: representations[index],
};
for (prefix, world) in &paths[index] {
names.register(geometry, owner, prefix, world);
}
}
let context = pmi::StepContext {
product_shape: definitions[root].product_shape,
representation: representations[root],
geometry_context,
length_unit: contexts.length_unit,
angle_unit: contexts.angle_unit,
faces: &names.faces,
edges: &names.edges,
vertices: &names.vertices,
};
report.pmi_unresolved_references = pmi::write_pmi(&mut writer, &context, pmi)?;
}
finish_step_file(
writer,
&assembly.products[root].name,
timestamp,
&mut report,
)?;
Ok(report)
}