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": {},
});
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.ensure_assembly_synced();
let request: HistoryRequest = serde_json::from_value(self.history.prefix_request())
.map_err(|e| format!("export STEP: history request: {e}"))?;
let named = crate::pipeline::resident_solid_handles(&request);
if named.is_empty() {
return Err("nothing to export: the model has no solids".into());
}
let report = request
.pmi
.as_ref()
.map(|_| brep_kernel::execute_history(&request).pmi)
.flatten();
let pmi = match (request.pmi.as_ref(), report.as_ref()) {
(Some(state), Some(report)) => Some(brep_kernel::StepPmi { state, report }),
_ => None,
};
if self.assembly_components.is_empty() {
return brep_kernel::export_step_named_handles(
&named,
document_name,
"MM",
"",
pmi.as_ref(),
)
.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.elements,
)
})
.collect();
brep_kernel::export_step_assembly_handles(
document_name,
&named,
&components,
"MM",
"",
pmi.as_ref(),
)
.map(|report| report.text)
}
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
}