STEP: open/insert/drag-drop

Import base feature (FeatureKind::Import, timeline type BaseFeature) holding
the STEP text and the file's product tree; bodies named from the file.
doc.open accepts .step/.stp (any case) as a new design; Insert STEP
(FusionImportCommandFromToolbar) adds one to the current design; File menu,
SOLID > INSERT, drag-and-drop (insert into a non-empty design, else open),
'solvecraft part.step', MCP open. Features (fillet, hole, sketch-on-face
cut, shell) work on imported bodies. Imports are cached so a file is read
once per command. Measure tightens its chord tolerance for small radii.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-07 21:01:04 +00:00
1 parent ed611ed7ba
commit f3dbb12b23
18 files changed
+360 -18

No files matched your search

+1 -1
View File
@@ -31,7 +31,7 @@ pub use ops::{BoolOp, boolean, split_by_plane, transform, transform_matrix};
pub use polybool::planar_boolean;
pub use polyhedron::{HalfSpace, convex_polyhedron, draft, shell};
pub use step::step_export;
pub use step_in::{ImportedBody, StepImport, StepNode, step_import};
pub use step_in::{ImportedBody, StepImport, StepNode, step_import, step_import_shared};
pub use topo::{TopoCounts, merged_topology, seam_flags};
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
+29 -4
View File
@@ -35,13 +35,38 @@ fn face_sums(m: &Mesh, nf: usize) -> Vec<(f64, f64, Vec3)> {
out
}
/// Measure a body from a fine tessellation (chord tolerance 5e-5 of the body size). Faces whose
/// fine tessellation disagrees with a coarse one (a meshing failure) use the coarse result.
/// Smallest radius of curvature along the mesh's edge polylines (straight edges ignored).
fn min_edge_radius(m: &Mesh) -> Option<f64> {
let mut best: Option<f64> = None;
for e in &m.edges {
for w in e.windows(3) {
let (Some(&a), Some(&b), Some(&c)) = (w.first(), w.get(1), w.get(2)) else { continue };
let (ab, bc, ca) = (b - a, c - b, a - c);
let cross = ab.cross(bc).len();
if !(cross > 1e-12 * ab.len() * bc.len()) {
continue;
}
let r = ab.len() * bc.len() * ca.len() / (2.0 * cross);
if r.is_finite() && r > 0.0 && best.is_none_or(|x| r < x) {
best = Some(r);
}
}
}
best
}
/// Measure a body from a fine tessellation (chord tolerance 5e-5 of the body size, finer for
/// small radii down to 5e-6). Faces whose fine tessellation disagrees with a coarse one (a
/// meshing failure) use the coarse result.
pub fn measure(b: &Body) -> Result<BodyMeasure> {
let size = b.size();
let fine = b.tessellate((size * 5e-5).max(1e-4))?;
let medium = b.tessellate((size * 2.5e-4).max(2e-4))?;
let coarse = b.tessellate((size * 1e-3).max(1e-3))?;
// Chord error relative to a radius sets the volume error: keep it near 3e-4 of the
// smallest radius (thin tubes, small fillets).
let tight = min_edge_radius(&coarse).map(|r| r * 3e-4).unwrap_or(f64::INFINITY);
let fine_tol = (size * 5e-5).min(tight).max(size * 5e-6).max(1e-4);
let fine = b.tessellate(fine_tol)?;
let medium = b.tessellate((fine_tol * 5.0).max(2e-4))?;
let nf = b.face_count();
let (sf, sm, sc) = (face_sums(&fine, nf), face_sums(&medium, nf), face_sums(&coarse, nf));
let close = |x: f64, y: f64| (x - y).abs() <= 0.01 * x.abs().max(y.abs()) + 1e-9;
+26
View File
@@ -567,6 +567,32 @@ impl<'a> Reader<'a> {
}
}
/// Recent imports, so a file read for a command and then evaluated in the timeline is only
/// read once.
static RECENT: std::sync::Mutex<Vec<(u64, usize, std::sync::Arc<StepImport>)>> = std::sync::Mutex::new(Vec::new());
/// [`step_import`] through a small cache of recent results (keyed by the text).
pub fn step_import_shared(text: &str) -> Result<std::sync::Arc<StepImport>> {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
text.hash(&mut h);
let key = (h.finish(), text.len());
if let Ok(cache) = RECENT.lock()
&& let Some((_, _, imp)) = cache.iter().find(|(k, n, _)| (*k, *n) == key)
{
return Ok(imp.clone());
}
let imp = std::sync::Arc::new(step_import(text)?);
if let Ok(mut cache) = RECENT.lock() {
cache.retain(|(k, n, _)| (*k, *n) != key);
cache.push((key.0, key.1, imp.clone()));
if cache.len() > 4 {
cache.remove(0);
}
}
Ok(imp)
}
/// Read a STEP file: its solids (as bodies, placed by the assembly structure) and the tree.
pub fn step_import(text: &str) -> Result<StepImport> {
let ex = p21::parse(text).map_err(KernelError::Invalid)?;