mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-06 20:02:16 +00:00
Profiler: kernel operations and STEP/IGES import stages are spans
Booleans, blends, sweeps, lofts, shell/draft/offset, healing and sewing, SMLib conversions, meshing and measuring; the import as parse, index, per-solid read, placement and mesh check, convert batches and finish. The existing per-step boolean/tessellate capture names its spans too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Generated
+1
@@ -4212,6 +4212,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-profile",
|
||||
"solidcraft-smlib",
|
||||
"solidcraft-step",
|
||||
"spade",
|
||||
|
||||
@@ -17,6 +17,7 @@ smlib = ["solidcraft-smlib/native"]
|
||||
[dependencies]
|
||||
solidcraft-geom = { workspace = true }
|
||||
solidcraft-smlib = { workspace = true }
|
||||
solidcraft-profile = { workspace = true }
|
||||
solidcraft-step = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
@@ -29,4 +30,5 @@ web-time = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
solidcraft-profile = { workspace = true, features = ["enabled"] }
|
||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||
|
||||
@@ -173,6 +173,7 @@ pub(crate) fn curve_derivative(c: &Curve, t: f64) -> Option<Vec3> {
|
||||
/// lines the round of `radius` has (zero curvature at the contacts on planar faces), not a
|
||||
/// circle. A configuration SMLib cannot blend so is refused, never given a circular round.
|
||||
pub fn fillet_g2(body: &Body, edges: &[Vec3], radius: f64) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "fillet_g2");
|
||||
body.require_brep("G2 fillet")?;
|
||||
if !(radius.is_finite() && radius > 1e-6 && radius < 1e6) {
|
||||
return Err(KernelError::Invalid("G2 fillet size must be positive and finite".into()));
|
||||
@@ -208,6 +209,7 @@ fn closed(input: &Body, b: Body) -> Result<Body> {
|
||||
/// faces around the edges is refused with [`KernelError::TooLarge`] ([`fillet_max`] finds the
|
||||
/// largest that works).
|
||||
pub fn fillet(body: &Body, edges: &[Vec3], radius: f64) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "fillet");
|
||||
body.require_brep("fillet")?;
|
||||
crate::blend_smlib::fillet(body, edges, radius).and_then(|b| closed(body, b))
|
||||
}
|
||||
@@ -221,6 +223,7 @@ pub fn fillet(body: &Body, edges: &[Vec3], radius: f64) -> Result<Body> {
|
||||
/// where some edge is not rounded is refused as not supported, a setback as long as an edge at
|
||||
/// its corner as too large.
|
||||
pub fn fillet_setback(body: &Body, edges: &[Vec3], radius: f64, setbacks: &[(Vec3, f64)]) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "fillet_setback");
|
||||
body.require_brep("fillet")?;
|
||||
crate::blend_smlib::fillet_setback(body, edges, radius, setbacks).and_then(|b| closed(body, b))
|
||||
}
|
||||
@@ -229,6 +232,7 @@ pub fn fillet_setback(body: &Body, edges: &[Vec3], radius: f64, setbacks: &[(Vec
|
||||
/// corners are a sphere or a Coons patch that meets the rounds tangentially, so this is refused
|
||||
/// rather than quietly given those corners; [`fillet`] (the default) is unchanged.
|
||||
pub fn fillet_g2_corners(body: &Body, edges: &[Vec3], radius: f64) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "fillet_g2_corners");
|
||||
body.require_brep("fillet")?;
|
||||
if !(radius.is_finite() && radius > 0.0) {
|
||||
return Err(KernelError::Invalid("the blend radius must be finite and positive".into()));
|
||||
@@ -249,6 +253,7 @@ fn radius_step(radius: f64) -> f64 {
|
||||
/// ([`KernelError::TooLarge`]) narrows the search: any other failure of a candidate is the
|
||||
/// search's error, since a smaller radius would not be the largest that fits.
|
||||
pub fn fillet_max(body: &Body, edges: &[Vec3], radius: f64) -> Result<Option<(f64, Body)>> {
|
||||
let _span = solidcraft_profile::span("kernel", "fillet_max");
|
||||
if !(radius.is_finite() && radius > 1e-6) {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -283,6 +288,7 @@ pub fn fillet_max(body: &Body, edges: &[Vec3], radius: f64) -> Result<Option<(f6
|
||||
/// up to 16 fillets, and this runs in the live preview). Any failure other than a refusal as too
|
||||
/// large is the error.
|
||||
pub fn fillet_max_each(body: &Body, edges: &[Vec3], radius: f64) -> Result<Option<(Vec<f64>, Body)>> {
|
||||
let _span = solidcraft_profile::span("kernel", "fillet_max_each");
|
||||
const MAX_GROUPS: usize = 8;
|
||||
const MAX_SEARCHES: usize = 2;
|
||||
let one = || -> Result<Option<(Vec<f64>, Body)>> { Ok(fillet_max(body, edges, radius)?.map(|(r, b)| (vec![r; edges.len()], b))) };
|
||||
@@ -319,6 +325,7 @@ pub fn fillet_max_each(body: &Body, edges: &[Vec3], radius: f64) -> Result<Optio
|
||||
/// The fillet of each edge at its own radius (`radii` in `edges`' order), all in one SMLib
|
||||
/// blend.
|
||||
pub fn fillet_each(body: &Body, edges: &[Vec3], radii: &[f64]) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "fillet_each");
|
||||
if radii.len() != edges.len() {
|
||||
return Err(KernelError::Invalid("one radius per edge".into()));
|
||||
}
|
||||
@@ -365,6 +372,7 @@ fn interacting_groups(body: &Body, pts: &[Vec3]) -> Option<Vec<Vec<usize>>> {
|
||||
/// Equal-distance chamfer of the edges nearest to the given points, `distance` back along each
|
||||
/// face, neighbouring chamfers meeting in a mitre.
|
||||
pub fn chamfer(body: &Body, edges: &[Vec3], distance: f64) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "chamfer");
|
||||
body.require_brep("chamfer")?;
|
||||
if !(distance.is_finite() && distance > 1e-6 && distance < 1e6) {
|
||||
return Err(KernelError::Invalid("chamfer size must be positive".into()));
|
||||
@@ -386,6 +394,7 @@ pub enum ChamferSide {
|
||||
/// A chamfer with different setbacks on the two faces of each edge: `d1` along the first face
|
||||
/// (the one facing up most, the other with `flip`), the second side by distance or angle.
|
||||
pub fn chamfer_sides(body: &Body, edges: &[Vec3], d1: f64, side: ChamferSide, flip: bool) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "chamfer_sides");
|
||||
body.require_brep("chamfer")?;
|
||||
let ok = |x: f64| x.is_finite() && x > 1e-6 && x < 1e6;
|
||||
let valid = ok(d1)
|
||||
|
||||
@@ -48,6 +48,7 @@ fn edge_frame(rec: &Faces, e: usize) -> Result<EdgeFrame> {
|
||||
|
||||
/// Fillet one straight edge with radius `r0` at its end nearest `start` and `r1` at the other.
|
||||
pub fn fillet_variable(body: &Body, edge_at: Vec3, start: Vec3, r0: f64, r1: f64) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "fillet_variable");
|
||||
fillet_variable_points(body, edge_at, start, &[(0.0, r0), (1.0, r1)], false)
|
||||
}
|
||||
|
||||
@@ -182,6 +183,7 @@ impl RadiusSpline {
|
||||
/// positions 0 (the end nearest `start`) to 1 (the other end) and any between, the radius
|
||||
/// straight between them or `smooth`. One end's radius may be zero.
|
||||
pub fn fillet_variable_points(body: &Body, edge_at: Vec3, start: Vec3, points: &[(f64, f64)], smooth: bool) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "fillet_variable_points");
|
||||
if points.len() < 2 || points.len() > 100 {
|
||||
return Err(KernelError::Invalid("a variable radius needs 2…100 points".into()));
|
||||
}
|
||||
@@ -201,6 +203,7 @@ pub fn fillet_variable_points(body: &Body, edge_at: Vec3, start: Vec3, points: &
|
||||
/// `start`. One end's radius may be zero. A law whose round would leave the faces beside the
|
||||
/// edge is SMLib's refusal ([`KernelError::TooLarge`]).
|
||||
pub fn fillet_variable_spline(body: &Body, edge_at: Vec3, start: Vec3, spline: &RadiusSpline) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "fillet_variable_spline");
|
||||
body.require_brep("fillet")?;
|
||||
spline.check()?;
|
||||
if !(edge_at.is_finite() && start.is_finite()) {
|
||||
@@ -438,6 +441,7 @@ pub fn taper(body: &Body, edge_at: Vec3, radius: f64) -> Result<Taper> {
|
||||
/// on alone gets a [`taper`], and the rest the constant radius. The result and the taper of each
|
||||
/// edge (in `edges`' order; None where the radius is constant).
|
||||
pub fn fillet_tapered(body: &Body, edges: &[Vec3], radius: f64) -> Result<(Body, Vec<Option<Taper>>)> {
|
||||
let _span = solidcraft_profile::span("kernel", "fillet_tapered");
|
||||
let why = match crate::fillet(body, edges, radius) {
|
||||
Err(KernelError::TooLarge(why)) => why,
|
||||
other => return other.map(|b| (b, vec![None; edges.len()])),
|
||||
@@ -473,6 +477,7 @@ pub fn fillet_tapered(body: &Body, edges: &[Vec3], radius: f64) -> Result<(Body,
|
||||
/// to contact at every station (SMLib's constant-chord round), its radius following the face
|
||||
/// angle along the edge.
|
||||
pub fn fillet_chord(body: &Body, edges: &[Vec3], chord: f64) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "fillet_chord");
|
||||
body.require_brep("fillet")?;
|
||||
if !(chord.is_finite() && chord > 1e-6 && chord < 1e6) {
|
||||
return Err(KernelError::Invalid("the chord length must be positive".into()));
|
||||
|
||||
@@ -915,7 +915,7 @@ impl Body {
|
||||
/// watertight wherever the B-rep is. A mesh body gives its own triangles. A face the planner
|
||||
/// cannot mesh is an error that names it and why.
|
||||
pub fn mesh(&self, t: &MeshTolerance) -> Result<Mesh> {
|
||||
crate::profile::timed(crate::profile::Kind::Tessellate, || self.mesh_untimed(t))
|
||||
crate::profile::timed(crate::profile::Kind::Tessellate, "mesh", || self.mesh_untimed(t))
|
||||
}
|
||||
|
||||
fn mesh_untimed(&self, t: &MeshTolerance) -> Result<Mesh> {
|
||||
@@ -942,7 +942,7 @@ impl Body {
|
||||
/// edge sampled once so faces meet exactly. For an evaluator of its own (the GPU viewport);
|
||||
/// [`Body::mesh_of`] gives its mesh without planning again.
|
||||
pub fn mesh_plan(&self, t: &MeshTolerance) -> Result<solidcraft_geom::tess::TessPlan> {
|
||||
crate::profile::timed(crate::profile::Kind::Tessellate, || self.mesh_plan_untimed(t))
|
||||
crate::profile::timed(crate::profile::Kind::Tessellate, "mesh_plan", || self.mesh_plan_untimed(t))
|
||||
}
|
||||
|
||||
fn mesh_plan_untimed(&self, t: &MeshTolerance) -> Result<solidcraft_geom::tess::TessPlan> {
|
||||
|
||||
@@ -66,6 +66,7 @@ pub(crate) fn frame_ok(plane: &Plane) -> bool {
|
||||
|
||||
/// Extrude each region from `start` to `end` along the plane normal (offsets in mm, `end > start`).
|
||||
pub fn extrude(plane: &Plane, regions: &[Region2], start: f64, end: f64) -> Result<Vec<Body>> {
|
||||
let _span = solidcraft_profile::span("kernel", "extrude");
|
||||
extrude_with(plane, regions, start, end, false)
|
||||
}
|
||||
|
||||
@@ -75,6 +76,7 @@ pub fn extrude(plane: &Plane, regions: &[Region2], start: f64, end: f64) -> Resu
|
||||
/// history lists every piece under it. For the Extrude feature; other builders (sheet metal's
|
||||
/// flat pattern, tools) keep a face per segment.
|
||||
pub fn extrude_whole_sides(plane: &Plane, regions: &[Region2], start: f64, end: f64) -> Result<Vec<Body>> {
|
||||
let _span = solidcraft_profile::span("kernel", "extrude_whole_sides");
|
||||
extrude_with(plane, regions, start, end, true)
|
||||
}
|
||||
|
||||
@@ -129,6 +131,7 @@ pub fn canonical_revolve_axis(plane: &Plane, region: &Region2, axis_origin: Vec2
|
||||
/// Revolve each region about the sketch axis by `angle` radians.
|
||||
/// This legacy API preserves its historical sweep sign behavior.
|
||||
pub fn revolve(plane: &Plane, regions: &[Region2], axis_origin: Vec2, axis_dir: Vec2, angle: f64) -> Result<Vec<Body>> {
|
||||
let _span = solidcraft_profile::span("kernel", "revolve");
|
||||
revolved(plane, regions, axis_origin, axis_dir, angle)
|
||||
}
|
||||
|
||||
@@ -136,6 +139,7 @@ pub fn revolve(plane: &Plane, regions: &[Region2], axis_origin: Vec2, axis_dir:
|
||||
/// face whole (#726): SMLib's revolution makes one face of each profile segment and of each
|
||||
/// cap, so this is [`revolve`].
|
||||
pub fn revolve_whole(plane: &Plane, regions: &[Region2], axis_origin: Vec2, axis_dir: Vec2, angle: f64) -> Result<Vec<Body>> {
|
||||
let _span = solidcraft_profile::span("kernel", "revolve_whole");
|
||||
revolved(plane, regions, axis_origin, axis_dir, angle)
|
||||
}
|
||||
|
||||
@@ -198,6 +202,7 @@ fn revolved(plane: &Plane, regions: &[Region2], axis_origin: Vec2, axis_dir: Vec
|
||||
|
||||
/// Axis-aligned box between two corners.
|
||||
pub fn box_solid(a: Vec3, b: Vec3) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "box_solid");
|
||||
let (lo, hi) = (a.min(b), a.max(b));
|
||||
let s = hi - lo;
|
||||
if !(s.x > 1e-6 && s.y > 1e-6 && s.z > 1e-6 && s.is_finite() && s.len() < 1e7) {
|
||||
@@ -245,6 +250,7 @@ pub(crate) const SEAM_ANGLE: f64 = 0.6131;
|
||||
|
||||
/// Cylinder with base centre `base`, axis direction `axis`, radius and height.
|
||||
pub fn cylinder(base: Vec3, axis: Vec3, radius: f64, height: f64) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "cylinder");
|
||||
let plane = Plane::from_normal(base, axis).ok_or_else(|| KernelError::Invalid("cylinder axis".into()))?;
|
||||
let radius = finite(radius, "radius")?;
|
||||
if radius <= 1e-6 {
|
||||
@@ -260,6 +266,7 @@ pub fn cylinder(base: Vec3, axis: Vec3, radius: f64, height: f64) -> Result<Body
|
||||
/// Sphere: SMLib's own (smlib_make_sphere), each face swept from the arc (segment 0) of the half
|
||||
/// disc a sphere revolves.
|
||||
pub fn sphere(center: Vec3, radius: f64) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "sphere");
|
||||
let radius = finite(radius, "radius")?;
|
||||
if radius <= 1e-6 || !center.is_finite() || center.len() >= 1e7 {
|
||||
return Err(KernelError::Invalid("radius must be positive".into()));
|
||||
@@ -269,6 +276,7 @@ pub fn sphere(center: Vec3, radius: f64) -> Result<Body> {
|
||||
|
||||
/// Torus about the Z axis through `center`.
|
||||
pub fn torus(center: Vec3, major: f64, minor: f64) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "torus");
|
||||
let (major, minor) = (finite(major, "major radius")?, finite(minor, "minor radius")?);
|
||||
if !(minor > 1e-6 && major > minor) {
|
||||
return Err(KernelError::Invalid("torus needs major > minor > 0".into()));
|
||||
@@ -755,6 +763,7 @@ const MEET_SAMPLES: usize = 48;
|
||||
/// (radians; positive grows the profile), its sharp corners closed by `corner`
|
||||
/// ([`crate::draft_corners`] says what each rule gives at each corner).
|
||||
pub fn extrude_tapered(plane: &Plane, region: &Region2, length: f64, dir_sign: f64, taper: f64, corner: DraftCorner) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "extrude_tapered");
|
||||
extrude_tapered_between(plane, region, 0.0, length, 0.0, dir_sign, taper, corner)
|
||||
}
|
||||
|
||||
@@ -775,6 +784,7 @@ pub fn extrude_tapered_between(
|
||||
taper: f64,
|
||||
corner: DraftCorner,
|
||||
) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "extrude_tapered_between");
|
||||
let (from, to, foot, taper) = (finite(from, "start")?, finite(to, "distance")?, finite(foot, "foot")?, finite(taper, "taper")?);
|
||||
if to - from <= 1e-6 {
|
||||
return Err(KernelError::Invalid("extrude distance is zero".into()));
|
||||
@@ -897,6 +907,7 @@ pub fn planar_face_record(plane: &Plane, r: &Region2) -> Result<solidcraft_geom:
|
||||
/// Ruled loft through two or more planar sections (outer loops only). The sections and the
|
||||
/// same sections in reverse order give the same body.
|
||||
pub fn loft(sections: &[(Plane, Loop2)]) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "loft");
|
||||
loft_with_rails(sections, &[], RailFit::Scale)
|
||||
}
|
||||
|
||||
@@ -904,6 +915,7 @@ pub fn loft(sections: &[(Plane, Loop2)]) -> Result<Body> {
|
||||
/// through every section's curve; see [`crate::loft_rails`]), `fit` saying how they reshape the
|
||||
/// sections between. Without rails, the plain loft.
|
||||
pub fn loft_with_rails(sections: &[(Plane, Loop2)], rails: &[Vec<Vec3>], fit: RailFit) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "loft_with_rails");
|
||||
let reversed = loft_reversed(sections)?;
|
||||
let numbered = sections.iter().map(|(p, _)| *p).zip(numbered::number_loops(sections.iter().map(|(_, l)| l))).collect::<Vec<_>>();
|
||||
let loops = loft_numbered_loops(&numbered, reversed)?;
|
||||
@@ -920,6 +932,7 @@ pub fn loft_with_rails(sections: &[(Plane, Loop2)], rails: &[Vec<Vec3>], fit: Ra
|
||||
/// Holes are paired by their positions relative to the outline, independently of sketch
|
||||
/// curve order. A hole cannot appear or disappear partway through a loft.
|
||||
pub fn loft_regions(sections: &[(Plane, Region2)]) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "loft_regions");
|
||||
if !(2..=50).contains(§ions.len()) {
|
||||
return Err(KernelError::Invalid("a loft needs 2…50 sections".into()));
|
||||
}
|
||||
@@ -1291,11 +1304,13 @@ pub fn path_polyline(path: &[PathSeg]) -> Result<Vec<Vec3>> {
|
||||
/// [`sweep`] with sweep options. The default options are exactly [`sweep`]; a twist, an end
|
||||
/// scale, guide rails or an orientation direction place the profile at stations along the path.
|
||||
pub fn sweep_with(plane: &Plane, region: &Region2, path: &[PathSeg], ctl: &crate::SweepControl) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "sweep_with");
|
||||
smlib_sweep(plane, region, path, ctl)
|
||||
}
|
||||
|
||||
/// Sweep a planar region along a chain of path segments, keeping the profile perpendicular to
|
||||
/// the path; at sharp corners the walls meet in mitres.
|
||||
pub fn sweep(plane: &Plane, region: &Region2, path: &[PathSeg]) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "sweep");
|
||||
smlib_sweep(plane, region, path, &crate::SweepControl::default())
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ fn position(asm: &Assembly, moved: &HashMap<u32, Vec3>, v: u32) -> Result<Vec3>
|
||||
/// Delete faces (indices in the body's face order, as in its mesh's `tri_face`) and heal the
|
||||
/// gap by extending the neighbouring planes. The result's history is over `[body]`.
|
||||
pub fn delete_faces(body: &Body, faces: &[usize]) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "delete_faces");
|
||||
body.require_brep("Delete Face")?;
|
||||
let rec = crate::native::brep_record(body)?;
|
||||
let n = rec.faces.len();
|
||||
|
||||
@@ -45,6 +45,7 @@ pub(crate) fn edge_points(b: &Brep, e: u32) -> Vec<Vec3> {
|
||||
/// result comes from those of `body` as SMLib's history says (a merged face from every face
|
||||
/// merged into it), over `[body]`.
|
||||
pub(crate) fn heal(body: &Body, keep: &[Vec<Vec3>]) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "heal");
|
||||
let record = crate::native::brep_record(body)?;
|
||||
// Each edge along a split line, by the point halfway along it.
|
||||
let mut kept: Vec<[f64; 3]> = Vec::new();
|
||||
|
||||
@@ -15,6 +15,7 @@ fn rot(v: Vec3, k: Vec3, ang: f64) -> Vec3 {
|
||||
/// its attitude relative to the axis, as a screw motion does: SMLib's screw sweep, one wall per
|
||||
/// profile segment.
|
||||
pub fn sweep_helix(plane: &Plane, region: &Region2, axis_origin: Vec3, axis: Vec3, pitch: f64, turns: f64) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "sweep_helix");
|
||||
let (k, profile) = helix_profile(plane, region, axis_origin, axis, pitch, turns)?;
|
||||
crate::smlib_sweeps::screw(plane, &crate::smlib_sweeps::pieces([&profile]), axis_origin, k, pitch, turns)
|
||||
}
|
||||
@@ -32,6 +33,7 @@ pub fn sweep_helix_radial(
|
||||
turns: f64,
|
||||
law: &[(f64, f64)],
|
||||
) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "sweep_helix_radial");
|
||||
if law.is_empty() {
|
||||
return sweep_helix(plane, region, axis_origin, axis, pitch, turns);
|
||||
}
|
||||
@@ -78,6 +80,7 @@ fn helix_profile(
|
||||
/// wound outward: `stations` sections per turn, flat end caps. Watertight by construction, for
|
||||
/// faceted booleans (modelled threads).
|
||||
pub fn sweep_helix_mesh(plane: &Plane, polygon: &[Vec2], axis_origin: Vec3, axis: Vec3, pitch: f64, turns: f64, stations: usize) -> Result<Mesh> {
|
||||
let _span = solidcraft_profile::span("kernel", "sweep_helix_mesh");
|
||||
let k = axis.normalized().ok_or_else(|| KernelError::Invalid("helix axis".into()))?;
|
||||
let m = polygon.len();
|
||||
if !(pitch.is_finite() && turns.is_finite() && turns > 0.0 && turns <= 200.0 && (3..=64).contains(&m) && (8..=720).contains(&stations)) {
|
||||
@@ -116,6 +119,7 @@ pub fn sweep_helix_mesh(plane: &Plane, polygon: &[Vec2], axis_origin: Vec3, axis
|
||||
/// A solid from a planar loop of straight segments to an apex point off its plane (a pyramid):
|
||||
/// SMLib's loft to a point.
|
||||
pub fn loft_to_point(plane: &Plane, lp: &Loop2, apex: Vec3) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "loft_to_point");
|
||||
if !apex.is_finite() || lp.segs.is_empty() || lp.segs.len() > 10_000 {
|
||||
return Err(KernelError::Invalid("loft to a point: bad section or apex".into()));
|
||||
}
|
||||
|
||||
@@ -1312,6 +1312,7 @@ fn write_shell(o: &mut Out, sw: &Sewer, faces: &[FaceB], styles: &mut Vec<(u64,
|
||||
/// This was the one importer stage outside every panic guard, so a panic here reached the CLI
|
||||
/// and the wasm build, where nothing catches it.
|
||||
pub fn iges_to_step(text: &str) -> std::result::Result<(String, Vec<String>), String> {
|
||||
let _span = solidcraft_profile::span("import", "iges.to_step");
|
||||
crate::guard("IGES import", || translate(text).map_err(crate::KernelError::Invalid)).map_err(|e| match e {
|
||||
crate::KernelError::Invalid(m) => m,
|
||||
other => other.to_string(),
|
||||
@@ -1322,7 +1323,7 @@ fn translate(text: &str) -> std::result::Result<(String, Vec<String>), String> {
|
||||
if text.len() > MAX_IGES_BYTES {
|
||||
return Err("IGES file too large".into());
|
||||
}
|
||||
let f = parse(text)?;
|
||||
let f = solidcraft_profile::scope("import", "iges.parse", || parse(text))?;
|
||||
let tol = (f.resolution * f.unit).clamp(1e-6, 0.05).max(1e-5);
|
||||
let mut t = Tr {
|
||||
f: &f,
|
||||
@@ -1871,6 +1872,7 @@ fn brep_edge(t: &mut Tr, sw: &mut Sewer, list: i64, ndx: i64, xf: &Xf) -> R<(usi
|
||||
|
||||
/// Read IGES text: its solids, or its surfaces sewn into shells, as the STEP reader gives them.
|
||||
pub fn iges_import(text: &str) -> crate::Result<crate::StepImport> {
|
||||
let _span = solidcraft_profile::span("import", "iges.import");
|
||||
let (step, warnings) = iges_to_step(text).map_err(crate::KernelError::Invalid)?;
|
||||
let mut imp = crate::step_import(&step)?;
|
||||
imp.warnings.splice(0..0, warnings);
|
||||
|
||||
@@ -53,6 +53,7 @@ const ACCURACY: f64 = 1e-4;
|
||||
/// and its box is SMLib's exact box; a body SMLib cannot measure is an error. A mesh body is
|
||||
/// measured from its triangles, exactly.
|
||||
pub fn measure(b: &Body) -> Result<BodyMeasure> {
|
||||
let _span = solidcraft_profile::span("kernel", "measure");
|
||||
if let Some((pos, tris)) = b.triangle_mesh() {
|
||||
// Exact for a mesh body; topology counts are facets, sides and vertices.
|
||||
let m = b.mesh(&crate::MeshTolerance::chord(1.0))?;
|
||||
@@ -94,6 +95,7 @@ pub fn measure(b: &Body) -> Result<BodyMeasure> {
|
||||
/// [`measure`]'s volume, area, centre and box alone (for callers that need no topology counts,
|
||||
/// such as an interference check).
|
||||
pub fn measure_mass(b: &Body) -> Result<MassMeasure> {
|
||||
let _span = solidcraft_profile::span("kernel", "measure_mass");
|
||||
if b.triangle_mesh().is_some() {
|
||||
let m = measure(b)?;
|
||||
return Ok(MassMeasure { volume: m.volume, area: m.area, centroid: m.centroid, bbox: m.bbox, measured_by: m.measured_by, notes: m.notes });
|
||||
|
||||
@@ -63,6 +63,7 @@ impl TriMesh {
|
||||
/// A mesh body from triangles (`positions` in mm). Coincident vertices are merged, degenerate
|
||||
/// triangles dropped, and a closed mesh with inward winding is turned outward.
|
||||
pub fn mesh_body(positions: &[Vec3], triangles: &[[u32; 3]]) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "mesh_body");
|
||||
if triangles.len() > MAX_TRIANGLES {
|
||||
return Err(KernelError::Invalid(format!("mesh with {} triangles (limit {MAX_TRIANGLES})", triangles.len())));
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ fn invalid(detail: impl std::fmt::Display) -> KernelError {
|
||||
/// body's own record and what it carries. Failure is a cache miss at the IO boundary, never a
|
||||
/// failed document save.
|
||||
pub fn encode_native_body(body: &Body) -> Result<Vec<u8>> {
|
||||
let _span = solidcraft_profile::span("kernel", "encode_native_body");
|
||||
guard("encode native B-rep", || {
|
||||
for source in body.face_sources.iter().flat_map(|s| s.iter()).flatten() {
|
||||
let mut at = source;
|
||||
@@ -81,6 +82,7 @@ pub fn encode_native_body(body: &Body) -> Result<Vec<u8>> {
|
||||
/// The schema 4 record of a solid body, in canonical order with its pcurves: the body itself.
|
||||
/// Mesh bodies have none.
|
||||
pub fn brep_record(body: &Body) -> Result<solidcraft_geom::brep::Brep> {
|
||||
let _span = solidcraft_profile::span("kernel", "brep_record");
|
||||
if body.mesh.is_some() {
|
||||
return Err(invalid("mesh bodies are source blobs, not B-rep"));
|
||||
}
|
||||
@@ -89,12 +91,14 @@ pub fn brep_record(body: &Body) -> Result<solidcraft_geom::brep::Brep> {
|
||||
|
||||
/// The body in SMLib (see [`Body::smlib`]), as an owned copy.
|
||||
pub fn smlib_body(body: &Body) -> Result<solidcraft_smlib::Brep> {
|
||||
let _span = solidcraft_profile::span("kernel", "smlib_body");
|
||||
body.smlib()?.try_clone().map_err(|e| KernelError::Failed(format!("SMLib: {e}")))
|
||||
}
|
||||
|
||||
/// Decode bounded untrusted data. Validate indices before topology extraction.
|
||||
/// Only a caller that has checked who produced the data may treat it as evaluated geometry.
|
||||
pub fn decode_native_body(bytes: &[u8]) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "decode_native_body");
|
||||
if bytes.len() > MAX_BYTES || bytes.get(..8) != Some(MAGIC.as_slice()) {
|
||||
return Err(invalid("version or size"));
|
||||
}
|
||||
@@ -124,12 +128,14 @@ pub fn decode_native_body(bytes: &[u8]) -> Result<Body> {
|
||||
/// An SMLib body as a kernel body: SMLib's export mapped by `solidcraft_smlib` (canonical order,
|
||||
/// radians, pcurves) is the body's record, and the SMLib body is kept with it.
|
||||
pub fn body_from_smlib(brep: &solidcraft_smlib::Brep) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "body_from_smlib");
|
||||
body_from_smlib_ordered(brep).map(|(body, _)| body)
|
||||
}
|
||||
|
||||
/// [`body_from_smlib`], with where each of SMLib's faces and edges is in the body's face and
|
||||
/// edge order ([`crate::history`]).
|
||||
pub(crate) fn body_from_smlib_ordered(brep: &solidcraft_smlib::Brep) -> Result<(Body, Option<solidcraft_smlib::history::Order>)> {
|
||||
let _span = solidcraft_profile::span("kernel", "body_from_smlib_ordered");
|
||||
let (record, order) = brep.to_native_ordered().map_err(|e| KernelError::Failed(e.to_string()))?;
|
||||
let handle = brep.try_clone().map_err(|e| KernelError::Failed(format!("SMLib: {e}")))?;
|
||||
Ok((Body::from_smlib_record(handle, record)?, Some(order)))
|
||||
@@ -148,6 +154,7 @@ pub(crate) fn smlib_body_ordered(body: &Body) -> Result<(solidcraft_smlib::Brep,
|
||||
pub(crate) fn smlib_record_ordered(
|
||||
record: &solidcraft_geom::brep::Brep,
|
||||
) -> Result<(solidcraft_smlib::Brep, Option<solidcraft_smlib::history::Order>)> {
|
||||
let _span = solidcraft_profile::span("kernel", "smlib_record_ordered");
|
||||
let brep = solidcraft_smlib::Brep::from_record(record, true).map_err(|e| KernelError::Failed(format!("SMLib: {e}")))?;
|
||||
let order = numbering(record, &brep);
|
||||
Ok((brep, order))
|
||||
|
||||
@@ -23,7 +23,7 @@ pub enum BoolOp {
|
||||
/// The result carries its exact history ([`Body::history`]) over the operands `[a, b]`, from
|
||||
/// SMLib's own tags, or none where a body's order cannot be matched to SMLib's.
|
||||
pub fn boolean(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
crate::profile::timed(crate::profile::Kind::Boolean, || {
|
||||
crate::profile::timed(crate::profile::Kind::Boolean, "boolean", || {
|
||||
let r = guard("SMLib boolean", || crate::smlib_boolean::smlib_boolean(a, b, op));
|
||||
with_operand_roles(a, b, op, r)
|
||||
})
|
||||
@@ -36,7 +36,7 @@ pub fn boolean(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
/// what the operands' faces stood for (their regularization protection and the planar roles on
|
||||
/// supports it kept).
|
||||
pub fn union_all(bodies: &[&Body]) -> Result<Body> {
|
||||
crate::profile::timed(crate::profile::Kind::Boolean, || {
|
||||
crate::profile::timed(crate::profile::Kind::Boolean, "union_all", || {
|
||||
if bodies.is_empty() {
|
||||
return Err(KernelError::Invalid("a union of no bodies".into()));
|
||||
}
|
||||
@@ -61,7 +61,7 @@ fn with_operand_roles(a: &Body, b: &Body, op: BoolOp, r: Result<Option<Body>>) -
|
||||
/// [`join_apart`]); `None` when fewer than two are given, one is a mesh, or two of them are not
|
||||
/// apart (their exact boxes, grown by the bodies' tolerances, overlap).
|
||||
pub fn union_apart(bodies: &[Body]) -> Result<Option<Body>> {
|
||||
crate::profile::timed(crate::profile::Kind::Boolean, || {
|
||||
crate::profile::timed(crate::profile::Kind::Boolean, "union_apart", || {
|
||||
if bodies.len() < 2 || bodies.len() > 10_000 || bodies.iter().any(Body::is_mesh) {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -132,6 +132,7 @@ fn joined(bodies: &[Body]) -> Result<(Body, part::Part)> {
|
||||
/// other matrix goes through SMLib's transform. Either way the result's faces and edges are
|
||||
/// the body's, in its history.
|
||||
pub fn transform_matrix(body: &Body, m: [[f64; 4]; 4]) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "transform_matrix");
|
||||
if m.iter().flatten().any(|x| !x.is_finite()) {
|
||||
return Err(KernelError::Invalid("non-finite transform".into()));
|
||||
}
|
||||
@@ -196,6 +197,7 @@ pub fn transform(body: &Body, translate: Vec3, origin: Vec3, axis: Vec3, angle:
|
||||
/// Split a body with a plane: the parts on the positive and the negative side of the plane
|
||||
/// (only the non-empty ones).
|
||||
pub fn split_by_plane(body: &Body, plane: &solidcraft_geom::Plane) -> Result<Vec<Body>> {
|
||||
let _span = solidcraft_profile::span("kernel", "split_by_plane");
|
||||
let mut out = Vec::new();
|
||||
for positive in [true, false] {
|
||||
out.extend(half_space(body, plane, positive)?.0);
|
||||
@@ -208,6 +210,7 @@ pub fn split_by_plane(body: &Body, plane: &solidcraft_geom::Plane) -> Result<Vec
|
||||
/// carries, over the body and the box. The box's face in the plane, the part's cut face, is its
|
||||
/// start cap on the positive side and its end cap on the negative side.
|
||||
pub fn half_space(body: &Body, plane: &solidcraft_geom::Plane, positive: bool) -> Result<(Option<Body>, Body)> {
|
||||
let _span = solidcraft_profile::span("kernel", "half_space");
|
||||
// The box's own construction roles describe no face of the design: the cut face has none.
|
||||
let tool = half_box(body, plane, positive)?.with_face_sources(None);
|
||||
Ok((boolean(body, &tool, BoolOp::Intersect)?, tool))
|
||||
|
||||
@@ -28,7 +28,7 @@ fn failed(what: &str) -> KernelError {
|
||||
/// no edges): a face of the result lies on faces of `a` or on triangles of `b`; an edge where
|
||||
/// `a` meets the mesh meets the triangles of the face it lies along.
|
||||
pub fn faceted_boolean(a: &Body, b: &Mesh, op: BoolOp, tol: f64) -> Result<Option<Body>> {
|
||||
crate::profile::timed(crate::profile::Kind::Boolean, || faceted_boolean_untimed(a, b, op, tol))
|
||||
crate::profile::timed(crate::profile::Kind::Boolean, "faceted_boolean", || faceted_boolean_untimed(a, b, op, tol))
|
||||
}
|
||||
|
||||
fn faceted_boolean_untimed(a: &Body, b: &Mesh, op: BoolOp, tol: f64) -> Result<Option<Body>> {
|
||||
|
||||
@@ -187,12 +187,14 @@ fn polyhedron_record(hs: &[HalfSpace], verts: &[Vec3], faces: &[(usize, Vec<usiz
|
||||
/// Hollow a body: walls of `thickness` inside, open at the faces under `open` (SMLib's shell).
|
||||
/// The result's history is over `[b]`: the inner walls are offsets of the faces they line.
|
||||
pub fn shell(b: &Body, open: &[Vec3], thickness: f64) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "shell");
|
||||
crate::offset_backend::shell(b, open, thickness)
|
||||
}
|
||||
|
||||
/// Tilt the faces near `faces` by `angle` about their line on the neutral plane, so that they
|
||||
/// lean in (positive angle) going along `pull` (SMLib's draft).
|
||||
pub fn draft(b: &Body, faces: &[Vec3], neutral: &solidcraft_geom::Plane, pull: Vec3, angle: f64) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "draft");
|
||||
b.require_brep("draft")?;
|
||||
crate::offset_backend::draft(b, faces, neutral, pull, angle)
|
||||
}
|
||||
@@ -200,6 +202,7 @@ pub fn draft(b: &Body, faces: &[Vec3], neutral: &solidcraft_geom::Plane, pull: V
|
||||
/// Move the faces at the given points along their outward normals by `distance` (negative: into
|
||||
/// the body), SMLib's offset of faces: the faces around them are extended or trimmed to meet them.
|
||||
pub fn offset_faces(b: &Body, at: &[Vec3], distance: f64) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "offset_faces");
|
||||
b.require_brep("offset faces")?;
|
||||
if !distance.is_finite() || distance.abs() > 1e6 {
|
||||
return Err(KernelError::Invalid("offset distance".into()));
|
||||
|
||||
@@ -74,8 +74,10 @@ pub fn credit(times: KernelTimes) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Time `f` as `kind` when a capture is running and no timed call is.
|
||||
pub(crate) fn timed<T>(kind: Kind, f: impl FnOnce() -> T) -> T {
|
||||
/// Time `f` as `kind` when a capture is running and no timed call is; it is also the
|
||||
/// profiler's span `name` (category `kernel`) whenever the profiler records.
|
||||
pub(crate) fn timed<T>(kind: Kind, name: &'static str, f: impl FnOnce() -> T) -> T {
|
||||
let _span = solidcraft_profile::span("kernel", name);
|
||||
let st = STATE.with(Cell::get);
|
||||
if !st.on || st.busy {
|
||||
return f();
|
||||
@@ -116,7 +118,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn nothing_is_timed_outside_a_capture() {
|
||||
timed(Kind::Boolean, || spin(2));
|
||||
timed(Kind::Boolean, "test", || spin(2));
|
||||
let ((), t) = capture(|| ());
|
||||
assert_eq!(t, KernelTimes::default());
|
||||
}
|
||||
@@ -125,11 +127,11 @@ mod tests {
|
||||
fn only_the_outermost_call_counts() {
|
||||
let started = web_time::Instant::now();
|
||||
let ((), t) = capture(|| {
|
||||
timed(Kind::Boolean, || {
|
||||
timed(Kind::Boolean, "test", || {
|
||||
spin(3);
|
||||
timed(Kind::Tessellate, || spin(3));
|
||||
timed(Kind::Tessellate, "test", || spin(3));
|
||||
});
|
||||
timed(Kind::Tessellate, || spin(2));
|
||||
timed(Kind::Tessellate, "test", || spin(2));
|
||||
});
|
||||
let elapsed = started.elapsed().as_secs_f64() * 1000.0;
|
||||
assert!(t.boolean_ms >= 6.0 && t.tessellate_ms >= 2.0, "{t:?}");
|
||||
@@ -145,7 +147,7 @@ mod tests {
|
||||
credit(t);
|
||||
credit(KernelTimes { boolean_ms: f64::NAN, tessellate_ms: -3.0 });
|
||||
// Inside a timed call it would count twice: it is the call's time already.
|
||||
timed(Kind::Boolean, || credit(t));
|
||||
timed(Kind::Boolean, "test", || credit(t));
|
||||
});
|
||||
assert_eq!((got.boolean_ms >= 4.0, got.boolean_ms < 8.0, got.tessellate_ms), (true, true, 1.0), "{got:?}");
|
||||
}
|
||||
@@ -153,9 +155,9 @@ mod tests {
|
||||
#[test]
|
||||
fn inner_captures_add_to_the_outer_one_and_a_panic_restores_it() {
|
||||
let ((), outer) = capture(|| {
|
||||
let ((), inner) = capture(|| timed(Kind::Boolean, || spin(2)));
|
||||
let ((), inner) = capture(|| timed(Kind::Boolean, "test", || spin(2)));
|
||||
assert!(inner.boolean_ms >= 2.0);
|
||||
let _ = std::panic::catch_unwind(|| capture(|| timed(Kind::Tessellate, || panic!("kernel"))));
|
||||
let _ = std::panic::catch_unwind(|| capture(|| timed(Kind::Tessellate, "test", || panic!("kernel"))));
|
||||
});
|
||||
assert!(outer.boolean_ms >= 2.0, "{outer:?}");
|
||||
let st = STATE.with(Cell::get);
|
||||
@@ -175,4 +177,24 @@ mod tests {
|
||||
assert!(!m.unwrap().triangles.is_empty());
|
||||
assert!(t.tessellate_ms > 0.0 && t.boolean_ms == 0.0, "{t:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kernel_operations_are_profiler_spans_with_smlib_calls_inside() {
|
||||
use crate::{BoolOp, boolean, box_solid};
|
||||
use solidcraft_geom::Vec3;
|
||||
solidcraft_profile::set_enabled(true);
|
||||
let mark = solidcraft_profile::mark();
|
||||
let a = box_solid(Vec3::ZERO, Vec3::new(10.0, 10.0, 10.0)).unwrap();
|
||||
let b = box_solid(Vec3::new(5.0, 5.0, 5.0), Vec3::new(15.0, 15.0, 15.0)).unwrap();
|
||||
let r = boolean(&a, &b, BoolOp::Cut);
|
||||
let rep = mark.report();
|
||||
let row = |cat: &str, name: &str| rep.rows.iter().find(|r| r.category == cat && r.name == name).copied();
|
||||
let k = row("kernel", "boolean").unwrap_or_else(|| panic!("{rep:?}"));
|
||||
assert!(k.count >= 1 && row("kernel", "box_solid").is_some(), "{rep:?}");
|
||||
if r.is_ok() {
|
||||
// With SMLib built, its calls are children of the boolean: their time is not its self time.
|
||||
let smlib: std::time::Duration = rep.rows.iter().filter(|r| r.category == "smlib").map(|r| r.total).sum();
|
||||
assert!(smlib > std::time::Duration::ZERO && k.self_time < k.total, "{rep:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -727,6 +727,7 @@ fn arc_curve(a: Vec3, b: Vec3, mid: Vec3) -> Result<(b::Curve, [f64; 2])> {
|
||||
|
||||
/// Sew the faces into a closed solid.
|
||||
pub fn sew(faces: &[FaceSpec]) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "sew");
|
||||
sew_impl(faces, false, None).map(|(body, _)| body)
|
||||
}
|
||||
|
||||
@@ -737,6 +738,7 @@ pub fn sew(faces: &[FaceSpec]) -> Result<Body> {
|
||||
/// own; and a face whose loops meet at a vertex (a hole's corner on its face's outline) has them
|
||||
/// as one loop through it (see [`join_touching_loops`]).
|
||||
pub fn sew_touching(faces: &[FaceSpec]) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "sew_touching");
|
||||
sew_impl(faces, true, None).map(|(body, _)| body)
|
||||
}
|
||||
|
||||
@@ -1329,6 +1331,7 @@ pub(crate) struct Assembled {
|
||||
/// shells (sheets) in the infinite region; everything in canonical order and validated.
|
||||
/// `solid`: every shell must be closed.
|
||||
pub(crate) fn assemble(parts: Parts, solid: bool) -> Result<Assembled> {
|
||||
let _span = solidcraft_profile::span("kernel", "assemble");
|
||||
let fail = |m: String| KernelError::Invalid(format!("sew: {m}"));
|
||||
let Parts { vertices, edges, faces, tolerance: tol } = parts;
|
||||
let nf = faces.len();
|
||||
|
||||
@@ -455,6 +455,7 @@ pub fn split_faces(body: &Body, faces: &[usize], tool: &SplitTool) -> Result<Bod
|
||||
|
||||
/// Like [`split_faces`], with the body face each face of the result came from (in face order).
|
||||
pub fn split_faces_with_map(body: &Body, faces: &[usize], tool: &SplitTool) -> Result<(Body, Vec<usize>)> {
|
||||
let _span = solidcraft_profile::span("kernel", "split_faces_with_map");
|
||||
let (b, map, made) = split(body, faces, tool)?;
|
||||
if let Some((missed, _)) = faces.iter().zip(&made).find(|(_, m)| **m == 0) {
|
||||
return Err(invalid(format!("the tool does not cross face {missed}")));
|
||||
@@ -538,6 +539,7 @@ fn new_lines(split: &Body, h: &History) -> Result<Vec<Vec<Vec3>>> {
|
||||
/// face order. A body the tool misses comes back whole. Each part's history is over `[body]`:
|
||||
/// its faces the body's (or pieces of them), the section new.
|
||||
pub fn split_body(body: &Body, tool: &SplitTool) -> Result<Vec<Body>> {
|
||||
let _span = solidcraft_profile::span("kernel", "split_body");
|
||||
const NAME: &str = "split body";
|
||||
body.require_brep(NAME)?;
|
||||
let reach = reach_of(body);
|
||||
|
||||
@@ -1045,6 +1045,7 @@ pub fn step_import(text: &str) -> Result<StepImport> {
|
||||
}
|
||||
|
||||
fn step_import_with_workers(text: &str, workers: usize) -> Result<StepImport> {
|
||||
let _span = solidcraft_profile::span("import", "step.import");
|
||||
let mut reading = StepReading::with_workers(text, workers)?;
|
||||
while reading.step() {}
|
||||
reading.finish()
|
||||
@@ -1071,10 +1072,12 @@ impl StepReading {
|
||||
}
|
||||
|
||||
fn with_workers(text: &str, workers: usize) -> Result<StepReading> {
|
||||
let _span = solidcraft_profile::span("import", "step.read_structure");
|
||||
let key = text_key(text);
|
||||
let watch = crate::watch::current();
|
||||
let cancelled = || watch.as_ref().is_some_and(|w| w.cancelled());
|
||||
let ex = match p21::parse(text) {
|
||||
let parsed = solidcraft_profile::scope("import", "step.parse", || p21::parse(text));
|
||||
let ex = match parsed {
|
||||
Ok(ex) => Arc::new(ex),
|
||||
Err(_) if cancelled() => return Err(crate::watch::cancelled_error("STEP import")),
|
||||
Err(e) => return Err(KernelError::Invalid(e)),
|
||||
@@ -1086,7 +1089,7 @@ impl StepReading {
|
||||
.filter(|e| matches!(e.name(), "MANIFOLD_SOLID_BREP" | "BREP_WITH_VOIDS" | "FACETED_BREP" | "SHELL_BASED_SURFACE_MODEL"))
|
||||
.count();
|
||||
rd.watch = watch.clone();
|
||||
rd.index();
|
||||
solidcraft_profile::scope("import", "step.index", || rd.index());
|
||||
// Roots: product definitions that are nobody's component.
|
||||
let child_pds: HashSet<u64> =
|
||||
rd.children.values().flatten().filter_map(|n| ex.get(*n).and_then(|e| e.params().get(4)).and_then(Param::as_ref_id)).collect();
|
||||
@@ -1128,6 +1131,7 @@ impl StepReading {
|
||||
/// Convert the next solid (or batch of solids); false once all are converted (or the
|
||||
/// watch cancelled the import).
|
||||
pub fn step(&mut self) -> bool {
|
||||
let _span = solidcraft_profile::span("import", "step.convert_batch");
|
||||
self.rd.convert_next(self.single)
|
||||
}
|
||||
|
||||
@@ -1139,6 +1143,7 @@ impl StepReading {
|
||||
/// The import, once [`Self::step`] has converted every solid.
|
||||
pub fn finish(mut self) -> Result<StepImport> {
|
||||
while self.step() {}
|
||||
let _span = solidcraft_profile::span("import", "step.finish");
|
||||
let rd = &mut self.rd;
|
||||
// Cancelled: what was read so far is dropped here, not cached.
|
||||
if rd.cancelled || rd.watch.as_ref().is_some_and(|w| w.cancelled()) {
|
||||
|
||||
@@ -59,6 +59,7 @@ pub(super) fn workers() -> usize {
|
||||
}
|
||||
|
||||
fn prepare(ex: &Exchange, job: &Job) -> Prepared {
|
||||
let _span = solidcraft_profile::span("import", "step.body");
|
||||
let (part, read) = match &job.source {
|
||||
Source::Oriented(part) => (part.clone(), None),
|
||||
Source::Item { item, units } => {
|
||||
@@ -66,9 +67,10 @@ fn prepare(ex: &Exchange, job: &Job) -> Prepared {
|
||||
(Arc::new(part), Some(read))
|
||||
}
|
||||
};
|
||||
let body = placed(&part.body, &job.placement).map_err(|e| Failure { read: false, msg: e.to_string() })?;
|
||||
let body = solidcraft_profile::scope("import", "step.place", || placed(&part.body, &job.placement))
|
||||
.map_err(|e| Failure { read: false, msg: e.to_string() })?;
|
||||
// Counted on the part (a placement meshes the same faces).
|
||||
let missing = if job.check_mesh { part.body.unmeshed_faces() } else { 0 };
|
||||
let missing = if job.check_mesh { solidcraft_profile::scope("import", "step.mesh_check", || part.body.unmeshed_faces()) } else { 0 };
|
||||
Ok(Made { body, missing, part, read })
|
||||
}
|
||||
|
||||
@@ -85,8 +87,10 @@ fn placed(body: &Body, m: &M4) -> crate::Result<Body> {
|
||||
/// and brought back as a body: the part SMLib made of the file. A solid the reader or SMLib
|
||||
/// refuses is not imported, with the reason.
|
||||
pub(super) fn smlib_part(ex: &Exchange, item: u64, units: Units) -> std::result::Result<(Part, Vec<String>), Failure> {
|
||||
let solid = solidcraft_step::topology::read_solid_with(ex, item, units, &solidcraft_smlib::pcurves::SmlibTrims)
|
||||
.map_err(|msg| Failure { read: true, msg })?;
|
||||
let solid = solidcraft_profile::scope("import", "step.read_solid", || {
|
||||
solidcraft_step::topology::read_solid_with(ex, item, units, &solidcraft_smlib::pcurves::SmlibTrims)
|
||||
})
|
||||
.map_err(|msg| Failure { read: true, msg })?;
|
||||
let failed = |e: KernelError| Failure { read: false, msg: e.to_string().trim_start_matches("the operation failed: ").to_string() };
|
||||
let (brep, smlib_order) = crate::native::smlib_record_ordered(&solid.brep).map_err(failed)?;
|
||||
let (body, body_order) = crate::native::body_from_smlib_ordered(&brep).map_err(failed)?;
|
||||
|
||||
@@ -73,6 +73,7 @@ fn rigid_frame(m: &[[f64; 4]; 4]) -> Result<([f64; 3], [f64; 3], [f64; 3])> {
|
||||
|
||||
/// STEP text (AP242) for products with bodies and assembly placements; `root` is the top product.
|
||||
pub fn step_export_products(products: &[ExportProduct], root: usize, header: &StepHeader) -> Result<String> {
|
||||
let _span = solidcraft_profile::span("kernel", "step_export_products");
|
||||
if products.is_empty() || root >= products.len() {
|
||||
return Err(KernelError::Invalid("nothing to export".into()));
|
||||
}
|
||||
@@ -290,6 +291,7 @@ fn style(w: &mut Writer, item: u64, c: [f32; 3], opacity: f32) -> u64 {
|
||||
|
||||
/// STEP text for bodies of one part (names `Body1`…).
|
||||
pub fn step_export_bodies(bodies: &[ExportBody], header: &StepHeader) -> Result<String> {
|
||||
let _span = solidcraft_profile::span("kernel", "step_export_bodies");
|
||||
let name = if header.file_name.is_empty() {
|
||||
"Part".to_string()
|
||||
} else {
|
||||
|
||||
@@ -403,11 +403,13 @@ pub(crate) fn rail_frame(along: Vec3, across: Vec3, along_to: Vec3, across_to: V
|
||||
/// Sweep a planar region along a 3D polyline path. The profile keeps its relation to the path
|
||||
/// (it need not touch it); the path should start near the profile.
|
||||
pub fn sweep_path(plane: &Plane, region: &Region2, path: &[Vec3]) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "sweep_path");
|
||||
sweep_path_with(plane, region, path, &SweepControl::default())
|
||||
}
|
||||
|
||||
/// [`sweep_path`] with sweep options: orientation, twist and end scale.
|
||||
pub fn sweep_path_with(plane: &Plane, region: &Region2, path: &[Vec3], ctl: &SweepControl) -> Result<Body> {
|
||||
let _span = solidcraft_profile::span("kernel", "sweep_path_with");
|
||||
ctl.check()?;
|
||||
smlib_sweep_points(plane, region, &crate::PointPath::polyline(path.to_vec()), ctl)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user