Files
solidcraft-upstream/crates/kernel/src/step.rs
T
bherbruckandClaude Opus 5.5 9330ee5698 STEP export: AP242 file of our own, names, colours, components as assemblies
Our writer assembles the file: AP242 header and contexts, a product per part
(or per component, with NEXT_ASSEMBLY_USAGE_OCCURRENCE and item-defined
placements per occurrence), named solids, surface colours as styled items,
ADVANCED_FACE for advanced breps. truck now writes only each body's B-rep,
which we copy in; identical geometric entities are written once and shared.
Designs with components export as assemblies (bodies in component frames).

step_validate also rejects unreferenced non-root entities. Tests: the sample
plate and a component assembly export, validate and read back with no
warnings and the same volume, area and face counts; the oracle replay checks
area and faces too (39 rebuildable cases pass). An independent Part 21
parser (step-p21, run outside the repo) accepts our exports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
2026-10-08 00:16:52 +00:00

89 lines
3.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use truck_modeling as mt;
use truck_stepio::out;
use crate::body::Body;
use crate::{KernelError, Result, guard};
/// Intersection-curve edges (from booleans) as cubic B-splines through the exact curve: STEP
/// readers get plain 3D geometry that lies on both faces, and the file does not repeat the two
/// surfaces for every such edge.
fn plain_curves(b: &Body) -> mt::Solid {
let s = b.deep_copy();
let size = b.size();
let mut seen = std::collections::HashSet::new();
for e in s.edge_iter() {
if !seen.insert(e.id()) {
continue;
}
if let mt::Curve::IntersectionCurve(ic) = e.curve()
&& let Some(c) = hermite_through(&ic, size * 1e-4)
{
e.set_curve(mt::Curve::BSplineCurve(c));
}
}
s
}
/// A C¹ cubic B-spline through exact points of `c` spaced so that chords deviate at most `tol`
/// (Catmull–Rom tangents over chord length; the cubic is far closer than the chords).
fn hermite_through<C>(c: &C, tol: f64) -> Option<mt::BSplineCurve<mt::Point3>>
where
C: mt::ParametricCurve<Point = mt::Point3, Vector = mt::Vector3> + mt::BoundedCurve + mt::ParameterDivision1D<Point = mt::Point3>,
{
use mt::{InnerSpace, MetricSpace};
let (_, pts) = c.parameter_division(c.range_tuple(), tol);
let pts: Vec<mt::Point3> = pts.into_iter().fold(Vec::new(), |mut v: Vec<mt::Point3>, p| {
if v.last().is_none_or(|q| q.distance(p) > tol * 1e-3) {
v.push(p);
}
v
});
let n = pts.len().checked_sub(1).filter(|n| *n >= 1 && *n < 1_000_000)?;
let mut u = vec![0.0; n + 1];
for i in 1..=n {
u[i] = u[i - 1] + pts[i].distance(pts[i - 1]);
}
let tangent = |i: usize| {
let (a, b) = (i.saturating_sub(1), (i + 1).min(n));
(pts[b] - pts[a]) / (u[b] - u[a])
};
let mut ctrl = vec![pts[0]];
let mut knots = vec![u[0]; 4];
for i in 0..n {
let h = u[i + 1] - u[i];
ctrl.push(pts[i] + tangent(i) * (h / 3.0));
ctrl.push(pts[i + 1] - tangent(i + 1) * (h / 3.0));
ctrl.push(pts[i + 1]);
knots.extend(std::iter::repeat_n(u[i + 1], if i + 1 == n { 4 } else { 3 }));
}
if ctrl.iter().any(|p| !(p.x.is_finite() && p.y.is_finite() && p.z.is_finite())) || tangent(0).magnitude() == 0.0 {
return None;
}
mt::BSplineCurve::try_new(mt::KnotVec::from(knots), ctrl).ok()
}
/// truck's STEP text for the given bodies (geometry and topology; product data is ours).
pub(crate) fn truck_step(bodies: &[&Body], system: &str) -> Result<String> {
if bodies.is_empty() {
return Err(KernelError::Invalid("nothing to export".into()));
}
for b in bodies {
b.require_brep("STEP export")?;
}
guard("step export", || {
let cs: Vec<_> = bodies.iter().map(|b| plain_curves(b).compress()).collect();
let models: out::StepModels<_, _, _> = cs.iter().collect();
let header = out::StepHeaderDescriptor { organization_system: system.to_owned(), ..Default::default() };
Ok(out::CompleteStepDisplay::new(models, header).to_string())
})
}
/// STEP (ISO 10303-21, AP242) text for the given bodies as one part (`Body1`…, with the
/// bodies' colours).
pub fn step_export(bodies: &[&Body], system: &str) -> Result<String> {
let list: Vec<crate::ExportBody> =
bodies.iter().enumerate().map(|(i, b)| crate::ExportBody { name: format!("Body{}", i + 1), body: b, color: b.color() }).collect();
let header = crate::StepHeader { organization: system.to_string(), ..Default::default() };
crate::step_out::step_export_bodies(&list, &header)
}