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())
}
pub fn import_3mf_bytes_feature(&mut self, bytes: &[u8]) -> Result<String, String> {
self.submit_mesh_import(crate::runner::MeshImportFormat::ThreeMf, 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()
}
pub fn take_mesh_preview_phase(&mut self) -> Option<crate::runner::MeshImportPhaseReply> {
self.mesh_preview_phases.pop_front()
}
pub fn submit_region_fit(&mut self, job: crate::runner::RegionFitJob) {
if !self.runner.submit_region_fit(job.clone()) {
self.region_fit_results.push_back(crate::runner::region_fit_reply(job));
}
self.pump();
}
pub fn take_region_fit(&mut self) -> Option<crate::runner::RegionFitReply> {
self.region_fit_results.pop_front()
}
pub fn submit_apply_region_choices(&mut self, job: crate::runner::ApplyRegionChoicesJob) {
if !self.runner.submit_apply_region_choices(job.clone()) {
self.apply_choices_results.push_back(crate::runner::apply_region_choices_reply(job));
}
self.pump();
}
pub fn take_apply_region_choices(&mut self) -> Option<crate::runner::ApplyRegionChoicesReply> {
self.apply_choices_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);
let phases = destination == MeshImportDestination::Preview;
self.runner.submit_mesh_import(crate::runner::MeshImportRequest {
id, format, bytes, options, phases,
});
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": {},
});
let lifted = brep_kernel::read_step_pmi(step_text, &id).unwrap_or(None);
self.pending_fit = true;
let report = self.add_feature(&feature.to_string());
if let Some(lifted) = lifted {
self.pmi_merge_imported(lifted);
}
report
}
pub fn export_step_text(&mut self) -> Result<String, String> {
self.export_step_text_named("Part")
}
pub fn export_step_text_named(&mut self, document_name: &str) -> Result<String, String> {
self.export_step_text_selected(document_name, None)
}
pub fn export_step_text_selected(&mut self, document_name: &str, only: Option<&[String]>) -> Result<String, String> {
self.plugin_export_ready()?;
let request: HistoryRequest = serde_json::from_value(self.run_request_value())
.map_err(|e| format!("export STEP: history request: {e}"))?;
let mut named = self.plugin_resident_handles(&request)?;
if let Some(only) = only {
if !self.assembly_components.is_empty() {
return Err("export STEP: a subset of solids is exported from a part document, not an assembly".into());
}
for name in only {
if !named.iter().any(|(n, _)| n == name) {
return Err(format!("export STEP: `{name}` is not a solid of this model"));
}
}
named.retain(|(n, _)| only.contains(n));
}
let board = if only.is_some() { Default::default() } else { self.board_step_solids().unwrap_or_default() };
for notice in &board.notices {
self.push_notice_as(super::NoticeSeverity::Warning, notice.clone());
}
if let Some(first) = board.skipped.first() {
self.push_notice(format!(
"STEP export: {} board item(s) could not be made as solids and are missing from the file — first: {first}",
board.skipped.len()
));
}
if named.is_empty() && board.bodies.is_empty() {
return Err("nothing to export: the model has no solids".into());
}
let report = request
.pmi
.as_ref()
.filter(|_| only.is_none())
.map(|_| {
let _trace = crate::run_trace::span("export_pmi");
self.execute_plugin_history(&request).pmi
})
.flatten();
let annotation_errors = super::plugins::plugin_annotation_errors(report.as_ref());
if !annotation_errors.is_empty() {
return Err(format!("plugin annotation export replay failed: {}", annotation_errors.join("; ")));
}
let pmi = match (request.pmi.as_ref().filter(|_| only.is_none()), report.as_ref()) {
(Some(state), Some(report)) => Some(brep_kernel::StepPmi { state, report }),
_ => None,
};
let mut colors = self.step_export_colors(&named);
for (name, rgb) in &board.colors {
if self.metadata.attribute(name, brep_kernel::COLOR_METADATA_KEY).is_none() {
colors.set(name, *rgb);
}
}
let resident = |named: &[(String, u32)]| -> Result<Vec<(String, brep_kernel::BrepSolid)>, String> {
named
.iter()
.map(|(name, handle)| Ok((name.clone(), brep_kernel::registered_solid_clone(*handle)?)))
.collect()
};
if self.assembly_components.is_empty() && !board.bodies.is_empty() {
let mut solids = resident(&named)?;
solids.extend(board.bodies);
let borrowed: Vec<(String, &brep_kernel::BrepSolid)> =
solids.iter().map(|(name, solid)| (name.clone(), solid)).collect();
return brep_kernel::export_step_report_named(
&borrowed,
document_name,
"MM",
"",
pmi.as_ref(),
Some(&colors),
)
.map(|report| report.text);
}
if self.assembly_components.is_empty() {
return brep_kernel::export_step_named_handles(
&named,
document_name,
"MM",
"",
pmi.as_ref(),
Some(&colors),
)
.map(|report| report.text);
}
let components: Vec<(String, String, brep_kernel::Mat4)> = self
.assembly_components
.iter()
.map(|record| {
(
record.id.clone(),
record.part_name.clone(),
record.transform,
)
})
.collect();
let resident = resident(&named)?;
let mut assembly = self.with_plugin_provider(||
brep_kernel::assembly_export_tree(document_name, resident, &components))?;
if !board.bodies.is_empty() {
for (name, rgb) in &board.colors {
if self.metadata.attribute(name, brep_kernel::COLOR_METADATA_KEY).is_none() {
colors.set(&format!("{BOARD_OCCURRENCE}:{name}"), *rgb);
}
}
for (name, rgb) in self.persisted_export_colors() {
if name.starts_with(super::board_geometry::BOARD_SOLID_PREFIX) {
colors.set(&format!("{BOARD_OCCURRENCE}:{name}"), rgb);
}
}
assembly.products.push(brep_kernel::StepExportProduct {
name: BOARD_OCCURRENCE.to_string(),
id: BOARD_OCCURRENCE.to_string(),
bodies: board.bodies,
pmi: None,
});
assembly.occurrences.push(brep_kernel::StepExportOccurrence {
designator: BOARD_OCCURRENCE.to_string(),
parent: 0,
child: assembly.products.len() - 1,
placement: [
1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1.,
],
});
}
for product in &mut assembly.products {
product.id = portable_product_id(&product.id, &product.name);
}
if self.history.pcb_block().is_some() && pmi.is_none() {
self.label_occurrences_by_reference(&mut assembly, &mut colors, &named);
}
brep_kernel::export_step_assembly_report(&assembly, "MM", "", pmi.as_ref(), Some(&colors))
.map(|report| report.text)
}
fn label_occurrences_by_reference(
&self,
assembly: &mut brep_kernel::StepAssemblyExport,
colors: &mut brep_kernel::StepColors,
named: &[(String, u32)],
) {
let mut references: std::collections::BTreeMap<String, String> = Default::default();
let mut seen: std::collections::BTreeMap<String, usize> = Default::default();
let mut ids: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for feature in self.history.features() {
let is_component = feature["type"]
.as_str()
.is_some_and(super::components::is_acomp_feature_type);
let params = &feature["inputParams"];
let Some(id) = params["id"].as_str().filter(|_| is_component) else {
continue;
};
ids.insert(id.to_string());
let reference = params["bom"]["Reference_Designator"].as_str().unwrap_or("").trim();
if !reference.is_empty() && !reference.contains(':') {
references.insert(id.to_string(), reference.to_string());
*seen.entry(reference.to_string()).or_default() += 1;
}
}
references.retain(|_, reference| {
seen[reference.as_str()] == 1 && !ids.contains(reference.as_str()) && reference != BOARD_OCCURRENCE
});
if references.is_empty() {
return;
}
for occurrence in assembly.occurrences.iter_mut().filter(|o| o.parent == 0) {
if let Some(reference) = references.get(&occurrence.designator) {
occurrence.designator = reference.clone();
}
}
let hashed = self.settings.face_color_mode == crate::style::FaceColorMode::HashedBySolid;
let mut keyed: Vec<(String, [u8; 3])> = self.persisted_export_colors();
if hashed {
for (name, _) in named {
if self.metadata.attribute(name, brep_kernel::COLOR_METADATA_KEY).is_none() {
keyed.push((name.clone(), srgb_bytes(crate::color::solid_color_srgb(name))));
}
}
}
for (name, rgb) in keyed {
let Some((id, rest)) = name.split_once(':') else {
continue;
};
if let Some(reference) = references.get(id) {
colors.set(&format!("{reference}:{rest}"), rgb);
}
}
}
fn step_export_colors(&self, named: &[(String, u32)]) -> brep_kernel::StepColors {
let mut colors = brep_kernel::StepColors::new();
for (name, rgb) in self.persisted_export_colors() {
colors.set(&name, rgb);
}
if self.settings.face_color_mode == crate::style::FaceColorMode::HashedBySolid {
for (name, _) in named {
if self.metadata.attribute(name, brep_kernel::COLOR_METADATA_KEY).is_none() {
colors.set(name, srgb_bytes(crate::color::solid_color_srgb(name)));
}
}
}
colors
}
fn flat_pattern_target_handle(&self) -> Result<u32, String> {
self.plugin_export_ready()?;
let request: HistoryRequest = serde_json::from_value(self.run_request_value())
.map_err(|e| format!("export flat pattern: history request: {e}"))?;
let sheet_metal: Vec<(String, u32)> = self.plugin_resident_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> {
self.export_iges_text_selected("Part", None)
}
pub fn export_iges_text_selected(&self, document_name: &str, only: Option<&[String]>) -> Result<String, String> {
self.plugin_export_ready()?;
let request: HistoryRequest = serde_json::from_value(self.run_request_value())
.map_err(|e| format!("export IGES: history request: {e}"))?;
let mut named = self.plugin_resident_handles(&request)?;
if let Some(only) = only {
if !self.assembly_components.is_empty() {
return Err("export IGES: a subset of solids is exported from a part document, not an assembly".into());
}
for name in only {
if !named.iter().any(|(n, _)| n == name) {
return Err(format!("export IGES: `{name}` is not a solid of this model"));
}
}
named.retain(|(n, _)| only.contains(n));
}
if named.is_empty() {
return Err("nothing to export: the model has no solids".into());
}
let colors = self.step_export_colors(&named);
let resident: Vec<(String, brep_kernel::BrepSolid)> = named
.iter()
.map(|(name, handle)| Ok((name.clone(), brep_kernel::registered_solid_clone(*handle)?)))
.collect::<Result<_, String>>()?;
let assembly = if self.assembly_components.is_empty() {
brep_kernel::StepAssemblyExport {
products: vec![brep_kernel::StepExportProduct {
name: document_name.to_string(),
bodies: resident,
..Default::default()
}],
occurrences: Vec::new(),
}
} else {
let components: Vec<(String, String, brep_kernel::Mat4)> = self
.assembly_components
.iter()
.map(|record| (record.id.clone(), record.part_name.clone(), record.transform))
.collect();
self.with_plugin_provider(|| brep_kernel::assembly_export_tree(document_name, resident, &components))?
};
brep_kernel::export_iges_assembly(&assembly, &colors, document_name, "MM", "")
}
pub fn import_model_text_feature(&mut self, text: &str) -> Result<String, String> {
if is_iges_text(text) {
self.import_iges_feature(text)
} else {
self.import_step_feature(text)
}
}
pub fn export_stl_text(&self) -> Result<String, String> {
self.plugin_export_ready()?;
let mut facets = Vec::new();
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];
facets.push((triangle_normal(a, b, c), [a, b, c]));
}
}
if facets.is_empty() {
return Err("nothing to export: the scene has no triangles".into());
}
Ok(brep_kernel::write_ascii_stl("brep", facets))
}
pub fn export_obj_text(&self) -> Result<String, String> {
self.plugin_export_ready()?;
let mut mesh = brep_kernel::Mesh::default();
for solid in self.scene.solids() {
let base = (mesh.positions.len() / 3) as u32;
for (position, normal) in solid.mesh.positions.iter().zip(&solid.mesh.normals) {
mesh.positions.extend(position.iter().map(|value| f64::from(*value)));
mesh.normals.extend(normal.iter().map(|value| f64::from(*value)));
}
mesh.indices.extend(solid.mesh.indices.iter().map(|index| base + index));
}
if mesh.indices.is_empty() {
return Err("nothing to export: the scene has no triangles".into());
}
brep_kernel::write_obj(&mesh, "brep")
}
fn persisted_export_colors(&self) -> Vec<(String, [u8; 3])> {
self.metadata
.all()
.iter()
.filter_map(|(name, record)| {
let value = record.get(brep_kernel::COLOR_METADATA_KEY)?;
let rgb = crate::style::parse_css_hex(value)?;
Some((name.clone(), srgb_bytes(rgb.map(f64::from))))
})
.collect()
}
fn display_body_color(&self, name: &str) -> [u8; 3] {
match self.settings.face_color_mode {
crate::style::FaceColorMode::HashedBySolid => {
srgb_bytes(crate::color::solid_color_srgb(name))
}
crate::style::FaceColorMode::Uniform => {
let face = self.settings.face_color;
srgb_bytes([f64::from(face[0]), f64::from(face[1]), f64::from(face[2])])
}
}
}
pub fn export_glb_bytes(&self, document_name: &str) -> Result<Vec<u8>, String> {
self.plugin_export_ready()?;
let colors: std::collections::BTreeMap<String, [u8; 3]> =
self.persisted_export_colors().into_iter().collect();
let solids: Vec<brep_kernel::GlbSolid> = self
.scene
.solids()
.iter()
.map(|solid| {
let mut mesh = brep_kernel::Mesh::default();
for (position, normal) in solid.mesh.positions.iter().zip(&solid.mesh.normals) {
mesh.positions.extend(position.iter().map(|value| f64::from(*value)));
mesh.normals.extend(normal.iter().map(|value| f64::from(*value)));
}
mesh.indices = solid.mesh.indices.clone();
mesh.face_ids = solid.mesh.face_ids.clone();
let face_colors = solid
.faces
.iter()
.enumerate()
.filter_map(|(index, face)| {
colors.get(&face.name).map(|rgb| (index as u32, *rgb))
})
.collect();
brep_kernel::GlbSolid {
name: solid.name.clone(),
mesh,
color: Some(
colors
.get(&solid.name)
.copied()
.unwrap_or_else(|| self.display_body_color(&solid.name)),
),
face_colors,
}
})
.collect();
brep_kernel::write_glb(
&solids,
document_name,
brep_kernel::GlbUpAxis::YUp,
METRES_PER_MILLIMETRE,
)
.map(|(bytes, _report)| bytes)
}
pub fn export_3mf_bytes(&self, document_name: &str) -> Result<Vec<u8>, String> {
self.plugin_export_ready()?;
let colors: std::collections::BTreeMap<String, [u8; 3]> =
self.persisted_export_colors().into_iter().collect();
let hashed = self.settings.face_color_mode == crate::style::FaceColorMode::HashedBySolid;
let solids: Vec<brep_kernel::ThreeMfSolid> = self
.scene
.solids()
.iter()
.map(|solid| {
let mut mesh = brep_kernel::Mesh::default();
for (position, normal) in solid.mesh.positions.iter().zip(&solid.mesh.normals) {
mesh.positions.extend(position.iter().map(|value| f64::from(*value)));
mesh.normals.extend(normal.iter().map(|value| f64::from(*value)));
}
mesh.indices = solid.mesh.indices.clone();
mesh.face_ids = solid.mesh.face_ids.clone();
let face_colors = solid
.faces
.iter()
.enumerate()
.filter_map(|(index, face)| {
colors.get(&face.name).map(|rgb| (index as u32, *rgb))
})
.collect();
let color = colors.get(&solid.name).copied().or_else(|| {
hashed.then(|| srgb_bytes(crate::color::solid_color_srgb(&solid.name)))
});
brep_kernel::ThreeMfSolid { name: solid.name.clone(), mesh, color, face_colors }
})
.collect();
brep_kernel::write_3mf(&solids, document_name).map(|(bytes, _report)| bytes)
}
pub fn pdf3d_bodies(&self) -> Vec<crate::sheets::pdf3d::Body> {
let colors: std::collections::BTreeMap<String, [u8; 3]> =
self.persisted_export_colors().into_iter().collect();
self.scene
.solids()
.iter()
.map(|shown| self.pmi_explode_originals.get(&shown.name).unwrap_or(shown))
.filter(|solid| solid.visible && !solid.is_sketch && solid.mesh.indices.len() >= 3)
.map(|solid| {
let body = colors.get(&solid.name).copied().unwrap_or_else(|| self.display_body_color(&solid.name));
let face_colors: Vec<[u8; 3]> = solid
.faces
.iter()
.map(|face| colors.get(&face.name).copied().unwrap_or(body))
.collect();
crate::sheets::pdf3d::Body {
name: solid.name.clone(),
positions: solid.mesh.positions.clone(),
normals: solid.mesh.normals.clone(),
indices: solid.mesh.indices.clone(),
triangle_colors: solid
.mesh
.face_ids
.iter()
.map(|face| face_colors.get(*face as usize).copied().unwrap_or(body))
.collect(),
edges: solid
.edges
.iter()
.filter(|edge| !edge.aux && !edge.centerline && edge.polyline.len() >= 2)
.map(|edge| edge.polyline.clone())
.collect(),
}
})
.collect()
}
}
const METRES_PER_MILLIMETRE: f64 = 0.001;
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,
chain: Vec<usize>,
}
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);
let may_have_structure = if is_iges_text(step_text) {
iges_has_instances(step_text)
} else {
step_text.contains("NEXT_ASSEMBLY_USAGE_OCCURRENCE")
};
if !may_have_structure {
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_model_text_feature(step_text)?;
Ok(StepAssemblyReport {
failed_products,
first_error,
flat_fallback: true,
..StepAssemblyReport::default()
})
}
None => {
self.import_model_text_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;
let mut namespaces: std::collections::HashMap<Vec<usize>, String> =
std::collections::HashMap::new();
for (key, pose, chain) 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;
}
let id = self.history.next_feature_id("ACOMP");
namespaces
.entry(chain.clone())
.or_insert_with(|| format!("{id}:"));
features.push(serde_json::json!({
"type": "ACOMP",
"inputParams": {
"id": id,
"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);
let pmi_unplaced = self.merge_assembly_pmi(&assembly, &namespaces);
for line in &assembly.pmi_diagnostics {
note(&mut first_error, line.clone());
}
if pmi_unplaced > 0 {
note(
&mut first_error,
format!(
"{pmi_unplaced} PMI reference(s) name an occurrence this import placed no \
component for; the annotations are kept unresolved"
),
);
}
Consumed::Imported(StepAssemblyReport {
parts: entry_names
.values()
.collect::<std::collections::HashSet<_>>()
.len(),
instances,
baked_nonrigid,
failed_products,
first_error,
flat_fallback: false,
})
}
}
impl EngineState {
fn merge_assembly_pmi(
&mut self,
assembly: &brep_kernel::StepAssembly,
namespaces: &std::collections::HashMap<Vec<usize>, String>,
) -> usize {
let Some(lifted) = assembly.pmi.clone() else {
return 0;
};
let mut unplaced = 0usize;
let mut state = self.pmi_state();
for view in lifted.views {
let id = state.next_id("VIEW");
let mut annotations = Vec::with_capacity(view.annotations.len());
for mut annotation in view.annotations {
unplaced += brep_kernel::rewrite_occurrence_refs(&mut annotation.params, |chain| {
namespaces.get(chain).cloned()
});
let prefix = brep_kernel::pmi_type(&annotation.kind)
.map(|def| def.short_name)
.unwrap_or("PMI");
let fresh = state.next_id(prefix);
if let Some(object) = annotation.params.as_object_mut() {
object.insert("id".into(), serde_json::Value::String(fresh));
}
annotations.push(annotation);
}
state.views.push(brep_kernel::PmiView {
id,
name: view.name,
camera: view.camera,
display: view.display,
annotations,
});
}
let block = serde_json::to_value(&state).ok();
self.history.set_pmi_block_no_undo(block);
self.rerun_history();
unplaced
}
}
#[derive(Default)]
struct Plan {
wanted: Vec<(PartKey, Mat4, Vec<usize>)>,
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, placed.chain.clone()));
}
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<usize>)> = Vec::new();
if !assembly.products[root].bodies.is_empty() {
rows.push((
DocKey::Leaf((assembly.products[root].pd_ref, NO_FACTOR)),
MAT4_IDENTITY,
Vec::new(),
));
}
let mut root_level_bakes = 0usize;
build.place_children(root, &[root], &mut rows, &mut root_level_bakes);
for (key, pose, chain) 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, chain));
}
}
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<usize>)> = 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, _chain) 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()?;
let mut document =
serde_json::json!({ "partsLibrary": library, "features": features });
if let Some(attributes) = part_attributes(node) {
document[PART_ATTRIBUTES] = attributes;
}
if let Some(pmi) = &node.pmi {
if let Ok(block) = serde_json::to_value(pmi) {
document["pmi"] = block;
}
}
Ok(Some((part_name(node, self.doc_name), document)))
}
fn place_children(
&mut self,
product: usize,
ancestors: &[usize],
rows: &mut Vec<(DocKey, Mat4, Vec<usize>)>,
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.edge_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.edge_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, vec![occurrence.edge_ref]));
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, vec![occurrence.edge_ref]));
}
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.edge_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, vec![occurrence.edge_ref]));
}
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 mut document = serde_json::json!({
"features": [{
"type": "IMPORT3D",
"inputParams": { "id": "IMPORT3D1", "nativeBrep": payload },
"persistentData": {},
}]
});
if let Some(attributes) = part_attributes(product) {
document[PART_ATTRIBUTES] = attributes;
}
if let Some(pmi) = &product.pmi {
if let Ok(block) = serde_json::to_value(pmi) {
document["pmi"] = block;
}
}
Ok((name, document))
}
fn part_attributes(product: &brep_kernel::StepProduct) -> Option<serde_json::Value> {
let number = product.id.trim();
if number.is_empty() {
return None;
}
Some(serde_json::json!({ brep_kernel::PART_NUMBER: number }))
}
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_impl(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 srgb_bytes(rgb: [f64; 3]) -> [u8; 3] {
rgb.map(|channel| (channel.clamp(0.0, 1.0) * 255.0).round() as u8)
}
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,
chain: Vec::new(),
},
ancestors: vec![product],
})
.collect();
while let Some(node) = stack.pop() {
let (product, world, depth, rigid_path, chain) = (
node.placed.product,
node.placed.world,
node.placed.depth,
node.placed.rigid_path,
node.placed.chain.clone(),
);
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.edge_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);
let mut child_chain = chain.clone();
child_chain.push(occurrence.edge_ref);
stack.push(Node {
placed: PlacedProduct {
product: occurrence.child,
world: mat4_mul(&world, &occurrence.placement),
depth: depth + 1,
rigid_path: rigid_path && occurrence.rigid,
chain: child_chain,
},
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,
];
use crate::geometry3d::mat4_mul_row_major as mat4_mul;
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
}
const BOARD_OCCURRENCE: &str = "PCB";
pub(crate) fn is_iges_text(text: &str) -> bool {
!text.contains("ISO-10303-21")
&& text
.lines()
.any(|line| matches!(line.chars().nth(72), Some('S' | 'G' | 'D' | 'P' | 'T')))
}
fn iges_has_instances(text: &str) -> bool {
text.lines().any(|line| {
line.chars().nth(72) == Some('D') && line.get(..8).is_some_and(|field| field.trim() == "408")
})
}
fn portable_product_id(id: &str, name: &str) -> String {
const SEPARATORS: [char; 2] = ['/', '\\'];
if !id.contains(SEPARATORS) {
return id.to_string();
}
let file = id.rsplit(SEPARATORS).next().unwrap_or_default();
let stem = file.rsplit_once('.').map_or(file, |(stem, _)| stem);
if stem.is_empty() {
name.to_string()
} else {
stem.to_string()
}
}