Ruled B-spline sheets that are circular cylinders or cones are written as CYLINDRICAL_SURFACE and CONICAL_SURFACE, so other readers no longer bound the part by sheets that reach past its faces.
This commit is contained in:
1 parent
2c754c430b
commit
2e6951135a
2 files changed
+191
-64
No files matched your search
@@ -863,6 +863,66 @@ fn glued_halves_export_as_whole_faces() {
|
||||
assert_eq!(imp.bodies[0].file_faces, 8);
|
||||
}
|
||||
|
||||
/// A slot plate with a round on its top outline (issue #612): the extruded arc ends and the
|
||||
/// rounds along the straight edges are lone B-spline sheets, written as cylinders bounded by
|
||||
/// their faces. The rounds' sheets reach far past their edges, and a reader bounding the part by
|
||||
/// its surfaces saw a 60 mm part as 130 mm long.
|
||||
#[test]
|
||||
fn rounded_slot_exports_cylinders() {
|
||||
use solidcraft_geom::Seg2;
|
||||
use std::f64::consts::{FRAC_PI_2, PI};
|
||||
let outer = Loop2 {
|
||||
segs: vec![
|
||||
Seg2::Line { a: Vec2::new(10.0, -10.0), b: Vec2::new(50.0, -10.0) },
|
||||
Seg2::Arc { center: Vec2::new(50.0, 0.0), radius: 10.0, start: -FRAC_PI_2, sweep: PI },
|
||||
Seg2::Line { a: Vec2::new(50.0, 10.0), b: Vec2::new(10.0, 10.0) },
|
||||
Seg2::Arc { center: Vec2::new(10.0, 0.0), radius: 10.0, start: FRAC_PI_2, sweep: PI },
|
||||
],
|
||||
};
|
||||
let plate = extrude(&Plane::XY, &[Region2 { outer, holes: vec![] }], 0.0, 4.0).unwrap().pop().unwrap();
|
||||
let edges = [Vec3::new(30.0, -10.0, 4.0), Vec3::new(30.0, 10.0, 4.0), Vec3::new(0.0, 0.0, 4.0), Vec3::new(60.0, 0.0, 4.0)];
|
||||
let b = fillet(&plate, &edges, 1.0).unwrap();
|
||||
let text = step_export(&[&b], "t").unwrap();
|
||||
assert!(!text.contains("B_SPLINE_SURFACE"), "{text}");
|
||||
assert_eq!(text.matches("CYLINDRICAL_SURFACE(").count(), 4);
|
||||
assert_eq!(text.matches("TOROIDAL_SURFACE(").count(), 2);
|
||||
// No point of the file (surface placements included) lies outside the part.
|
||||
let imp = round_trip(&b);
|
||||
for line in text.lines().filter(|l| l.contains("CARTESIAN_POINT")) {
|
||||
let Some(xyz) = line.split("((").nth(1).and_then(|r| r.split("))").next()) else { continue };
|
||||
let x: f64 = xyz.split(',').next().unwrap().trim().parse().unwrap();
|
||||
assert!((-1e-6..=60.0 + 1e-6).contains(&x), "{line}");
|
||||
}
|
||||
// The volume analytically: the plate less the round's corner section swept along the edges
|
||||
// (Pappus: the section's centroid runs 0.2234 r inside the edge round the arcs).
|
||||
let section = 1.0 - PI / 4.0;
|
||||
let centroid = (10.0 - 3.0 * PI) / (3.0 * (4.0 - PI));
|
||||
let want = (800.0 + PI * 100.0) * 4.0 - section * (80.0 + std::f64::consts::TAU * (10.0 - centroid));
|
||||
assert!(rel(measure(&imp.bodies[0].body).unwrap().volume, want) < 1e-3);
|
||||
}
|
||||
|
||||
/// A countersink cut into a plate (issue #612): the cone is a tapered extrusion of a disc, a
|
||||
/// B-spline sheet per half, written as one CONICAL_SURFACE face (the sheets reached below the
|
||||
/// plate).
|
||||
#[test]
|
||||
fn countersink_exports_one_cone() {
|
||||
use std::f64::consts::FRAC_PI_4;
|
||||
let plate = box_solid(Vec3::new(-10.0, -10.0, 0.0), Vec3::new(10.0, 10.0, 4.0)).unwrap();
|
||||
// A 90° countersink, radius 4 at the top face, down from 1 mm above it to radius 1.8, and the
|
||||
// 2 mm drill through.
|
||||
let disc = Region2 { outer: Loop2::circle(Vec2::ZERO, 5.0), holes: vec![] };
|
||||
let cone = crate::extrude_tapered(&Plane::XY.offset(5.0), &disc, 3.2, -1.0, -FRAC_PI_4).unwrap();
|
||||
let drill = cylinder(Vec3::new(0.0, 0.0, -1.0), Vec3::Z, 2.0, 6.0).unwrap();
|
||||
let b = boolean(&boolean(&plate, &cone, BoolOp::Cut).unwrap().unwrap(), &drill, BoolOp::Cut).unwrap().unwrap();
|
||||
let text = step_export(&[&b], "t").unwrap();
|
||||
assert!(!text.contains("B_SPLINE_SURFACE"), "{text}");
|
||||
assert_eq!(text.matches("CONICAL_SURFACE(").count(), 1, "{text}");
|
||||
assert_eq!(text.matches("CYLINDRICAL_SURFACE(").count(), 1, "{text}");
|
||||
let imp = round_trip(&b);
|
||||
// Six box faces, the countersink and the drill.
|
||||
assert_eq!(imp.bodies[0].file_faces, 8);
|
||||
}
|
||||
|
||||
/// Knot multiplicities are expanded into one value each before the knot count is compared with
|
||||
/// the control points, so the sum has to be checked first: 1,000,000 knots of multiplicity 64
|
||||
/// against two control points asked for a 500 MB vector (20,000,000 for 10 GB) only to reject
|
||||
|
||||
+131
-64
@@ -151,8 +151,8 @@ struct Out {
|
||||
shared: HashMap<String, u64>,
|
||||
/// Surfaces written with the opposite normal to the one they replace.
|
||||
flipped: std::collections::HashSet<u64>,
|
||||
/// B-spline cylinder sheets of the body being copied under merged faces, with the direction
|
||||
/// of the cylinder's parameter seam.
|
||||
/// B-spline cylinder and cone sheets of the body being copied to write as analytic surfaces,
|
||||
/// with the direction of their parameter seam.
|
||||
merged: HashMap<u64, V3>,
|
||||
/// Faces of the body being copied: exchange id → written id.
|
||||
faces_out: HashMap<u64, u64>,
|
||||
@@ -270,13 +270,13 @@ impl Out {
|
||||
map.insert(id, new);
|
||||
continue;
|
||||
}
|
||||
// Flat B-spline sheets are planes; cylindrical ones become cylinders when their
|
||||
// pieces were merged into one face (a lone piece stays as written).
|
||||
// Flat B-spline sheets are planes; ruled ones become cylinders and cones when
|
||||
// `analytic_sheets` chose them (their pieces merged, or a lone piece's edges allow).
|
||||
if !done
|
||||
&& let Some(a) = planar_bspline(ex, e).map(|kind| Analytic { kind, flip: false }).or_else(|| {
|
||||
let seam = self.merged.get(&id)?;
|
||||
let mut a = cylindrical_bspline(ex, e)?;
|
||||
if let AnalyticKind::Cylinder { x, .. } = &mut a.kind {
|
||||
let mut a = ruled_bspline(ex, e)?;
|
||||
if let AnalyticKind::Cylinder { x, .. } | AnalyticKind::Cone { x, .. } = &mut a.kind {
|
||||
*x = *seam;
|
||||
}
|
||||
Some(a)
|
||||
@@ -609,10 +609,11 @@ fn planar_bspline(ex: &p21::Exchange, e: &p21::Entity) -> Option<AnalyticKind> {
|
||||
Some(AnalyticKind::Plane { o, z, x })
|
||||
}
|
||||
|
||||
/// A B-spline surface that is a circular cylinder (a circle or arc swept along a line, as truck
|
||||
/// makes for extruded round profiles), with its normal direction relative to the cylinder's
|
||||
/// outward normal.
|
||||
fn cylindrical_bspline(ex: &p21::Exchange, e: &p21::Entity) -> Option<Analytic> {
|
||||
/// A B-spline surface that is a circular cylinder or cone: straight rulings between two coaxial
|
||||
/// circles or arcs (or an arc and the cone's apex), as truck makes for extruded round profiles,
|
||||
/// rounds along straight edges and countersinks. With its normal direction relative to the
|
||||
/// surface's outward normal.
|
||||
fn ruled_bspline(ex: &p21::Exchange, e: &p21::Entity) -> Option<Analytic> {
|
||||
use truck_modeling::{ParametricSurface, ParametricSurface3D};
|
||||
if !(e.has("B_SPLINE_SURFACE") || e.name() == "B_SPLINE_SURFACE_WITH_KNOTS") {
|
||||
return None;
|
||||
@@ -624,45 +625,98 @@ fn cylindrical_bspline(ex: &p21::Exchange, e: &p21::Entity) -> Option<Analytic>
|
||||
let p = s.subs(u, v);
|
||||
[p.x, p.y, p.z]
|
||||
};
|
||||
// Which parameter sweeps along a line: the same translation from one end to the other at
|
||||
// every point of the profile.
|
||||
// Which parameter runs along the rulings: straight from one end to the other at every point
|
||||
// of the profile.
|
||||
for along_v in [true, false] {
|
||||
let point = |t: f64, w: f64| if along_v { at(u0 + (u1 - u0) * t, v0 + (v1 - v0) * w) } else { at(u0 + (u1 - u0) * w, v0 + (v1 - v0) * t) };
|
||||
let prof: Vec<V3> = (0..=8).map(|k| point(k as f64 / 8.0, 0.0)).collect();
|
||||
let d = sub(point(0.0, 1.0), point(0.0, 0.0));
|
||||
let len = norm(d);
|
||||
let scale = prof.iter().map(|p| norm(sub(*p, prof[0]))).fold(len, f64::max);
|
||||
let grid: Vec<Vec<V3>> = (0..=4).map(|m| (0..=8).map(|k| point(k as f64 / 8.0, m as f64 / 4.0)).collect()).collect();
|
||||
let (Some(p0), Some(p1)) = (grid.first(), grid.last()) else { continue };
|
||||
let Some(&first) = p0.first() else { continue };
|
||||
let scale = grid.iter().flatten().map(|p| norm(sub(*p, first))).fold(0.0, f64::max);
|
||||
let tol = 1e-7 * (1.0 + scale);
|
||||
if len < tol
|
||||
|| (0..=8).any(|k| {
|
||||
let t = k as f64 / 8.0;
|
||||
(0..=4).any(|m| norm(sub(sub(point(t, m as f64 / 4.0), point(t, 0.0)), mul(d, m as f64 / 4.0))) > tol)
|
||||
})
|
||||
{
|
||||
let straight = (0..=8).all(|k| {
|
||||
let (Some(&a), Some(&b)) = (p0.get(k), p1.get(k)) else { return false };
|
||||
let Some(d) = unit(sub(b, a)) else { return false };
|
||||
grid.iter().all(|row| row.get(k).is_some_and(|q| norm(cross(sub(*q, a), d)) <= tol))
|
||||
});
|
||||
if !straight {
|
||||
continue;
|
||||
}
|
||||
let (Some(&a), Some(&b), Some(&c)) = (prof.first(), prof.get(4), prof.get(8)) else { continue };
|
||||
let (centre, r, n) = circle3(a, b, c).or_else(|| circle3(a, prof[2], b))?;
|
||||
let z = unit(d)?;
|
||||
if norm(cross(n, z)) > 1e-7 || prof.iter().any(|p| (norm(sub(*p, centre)) - r).abs() > tol) {
|
||||
// Each end: a circle (centre, radius, axis) or a single point (a cone's apex).
|
||||
let ring = |prof: &[V3]| -> Option<(V3, f64, Option<V3>)> {
|
||||
let &a = prof.first()?;
|
||||
if prof.iter().all(|p| norm(sub(*p, a)) <= tol) {
|
||||
return Some((a, 0.0, None));
|
||||
}
|
||||
let (&b, &c, &h) = (prof.get(4)?, prof.get(8)?, prof.get(2)?);
|
||||
let (centre, r, n) = circle3(a, b, c).or_else(|| circle3(a, h, b))?;
|
||||
prof.iter().all(|p| (norm(sub(*p, centre)) - r).abs() <= tol).then_some((centre, r, Some(n)))
|
||||
};
|
||||
let ((c0, r0, n0), (c1, r1, n1)) = (ring(p0)?, ring(p1)?);
|
||||
let axis = unit(sub(c1, c0))?;
|
||||
if [n0, n1].iter().flatten().any(|n| norm(cross(*n, axis)) > 1e-7) || (n0.is_none() && n1.is_none()) {
|
||||
return None;
|
||||
}
|
||||
let height = norm(sub(c1, c0));
|
||||
// The radius grows along z (a cylinder keeps the sweep's direction).
|
||||
let (o, z, r, angle) = if (r1 - r0).abs() <= tol {
|
||||
(c0, axis, r0, 0.0)
|
||||
} else if r1 > r0 {
|
||||
(c1, axis, r1, ((r1 - r0) / height).atan())
|
||||
} else {
|
||||
(c0, mul(axis, -1.0), r0, ((r0 - r1) / height).atan())
|
||||
};
|
||||
let radius_at = |q: V3| r + dot(sub(q, o), z) * angle.tan();
|
||||
let radial = |q: V3| {
|
||||
let v = sub(q, o);
|
||||
sub(v, mul(z, dot(v, z)))
|
||||
};
|
||||
if grid.iter().flatten().any(|q| (norm(radial(*q)) - radius_at(*q)).abs() > tol) {
|
||||
return None;
|
||||
}
|
||||
// The surface's own normal against the outward one, mid-face.
|
||||
let (um, vm) = ((u0 + u1) / 2.0, (v0 + v1) / 2.0);
|
||||
let nn = s.normal(um, vm);
|
||||
let q = at(um, vm);
|
||||
let radial = sub(sub(q, centre), mul(z, dot(sub(q, centre), z)));
|
||||
let flip = dot([nn.x, nn.y, nn.z], radial) < 0.0;
|
||||
let eq = unit(radial(q))?;
|
||||
let outward = sub(mul(eq, angle.cos()), mul(z, angle.sin()));
|
||||
let flip = dot([nn.x, nn.y, nn.z], outward) < 0.0;
|
||||
// The parameter seam opposite the middle of the sheet, away from its edges.
|
||||
let x = unit(mul(radial, -1.0)).or_else(|| unit(sub(a, centre)))?;
|
||||
return Some(Analytic { kind: AnalyticKind::Cylinder { o: centre, z, x, r }, flip });
|
||||
let x = mul(eq, -1.0);
|
||||
let kind = if angle == 0.0 { AnalyticKind::Cylinder { o, z, x, r } } else { AnalyticKind::Cone { o, z, x, r, angle } };
|
||||
return Some(Analytic { kind, flip });
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// A B-spline surface that is really a plane or a cylinder.
|
||||
/// A B-spline surface that is really a plane, a cylinder or a cone.
|
||||
fn analytic_bspline(ex: &p21::Exchange, e: &p21::Entity) -> Option<Analytic> {
|
||||
planar_bspline(ex, e).map(|kind| Analytic { kind, flip: false }).or_else(|| cylindrical_bspline(ex, e))
|
||||
planar_bspline(ex, e).map(|kind| Analytic { kind, flip: false }).or_else(|| ruled_bspline(ex, e))
|
||||
}
|
||||
|
||||
/// Every edge of a face on a cylinder or cone is a circle square to its axis or one of its
|
||||
/// rulings (a line along the axis, or through a cone's apex). Faces bounded by other curves (a
|
||||
/// hole crossing a tube wall) keep their B-spline sheet: readers lay those curves out far more
|
||||
/// slowly on the analytic surface than on the sheet they came with.
|
||||
fn square_or_ruled(ex: &p21::Exchange, fe: &p21::Entity, kind: &AnalyticKind) -> bool {
|
||||
let (z, apex) = match *kind {
|
||||
AnalyticKind::Cylinder { z, .. } => (z, None),
|
||||
AnalyticKind::Cone { o, z, r, angle, .. } if angle.tan() > 1e-12 => (z, Some(sub(o, mul(z, r / angle.tan())))),
|
||||
_ => return false,
|
||||
};
|
||||
face_edges(ex, fe).is_some_and(|ls| {
|
||||
ls.iter().flatten().all(|(x, _)| {
|
||||
edge_samples(ex, *x).is_some_and(|pts| {
|
||||
let Some(&q0) = pts.first() else { return false };
|
||||
let tol = 1e-7 * (1.0 + norm(q0));
|
||||
let ruling = match apex {
|
||||
None => pts.iter().all(|q| norm(cross(sub(*q, q0), z)) <= tol),
|
||||
Some(a) => pts.iter().all(|q| norm(cross(sub(*q, a), sub(q0, a))) <= tol * (1.0 + norm(sub(q0, a)))),
|
||||
};
|
||||
ruling || pts.iter().all(|q| dot(sub(*q, q0), z).abs() <= tol)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Points along an edge's curve (its ends for a line, None for a conic it cannot sample).
|
||||
@@ -833,8 +887,9 @@ fn face_edges(ex: &p21::Exchange, face: &p21::Entity) -> Option<Vec<Vec<(u64, bo
|
||||
|
||||
/// Faces that truck splits (a revolution in two or three turns) joined back into one face per
|
||||
/// analytic surface, by dropping the edges between them. Faces whose union would lose all its
|
||||
/// edges, or whose leftover edges do not close into loops, stay apart.
|
||||
fn merge_split_faces(ex: &mut p21::Exchange, paint: &mut HashMap<u64, FaceLook>, keep: &[Vec<Vec3>]) -> HashMap<u64, V3> {
|
||||
/// edges, or whose leftover edges do not close into loops, stay apart. Returns the surfaces of
|
||||
/// merged faces.
|
||||
fn merge_split_faces(ex: &mut p21::Exchange, paint: &mut HashMap<u64, FaceLook>, keep: &[Vec<Vec3>]) -> std::collections::HashSet<u64> {
|
||||
// An edge on a line the user split along: every sample within a hair of the polyline.
|
||||
let on_split = |ex: &p21::Exchange, e: u64| -> bool {
|
||||
let Some(pts) = edge_samples(ex, e) else { return false };
|
||||
@@ -876,21 +931,10 @@ fn merge_split_faces(ex: &mut p21::Exchange, paint: &mut HashMap<u64, FaceLook>,
|
||||
let v = |a: truck_modeling::Vector3| [a.x, a.y, a.z];
|
||||
Some(Analytic { kind: AnalyticKind::Plane { o: [f.o.x, f.o.y, f.o.z], z: v(f.z), x: v(f.x) }, flip: false })
|
||||
}
|
||||
// Pieces of a B-spline cylinder or cone merge only when their edges
|
||||
// are circles square to the axis and rulings (an extruded hole).
|
||||
_ => analytic_bspline(ex, se).filter(|a| match a.kind {
|
||||
// Pieces of a B-spline cylinder merge only when their curved edges
|
||||
// are circles square to the axis (an extruded hole): crossing
|
||||
// curves (a hole through a tube wall) read far more slowly on a
|
||||
// cylinder than on the sheet they came with.
|
||||
AnalyticKind::Cylinder { z, .. } => face_edges(ex, fe).is_some_and(|ls| {
|
||||
ls.iter().flatten().all(|(x, _)| {
|
||||
edge_samples(ex, *x).is_some_and(|pts| {
|
||||
let Some(&q0) = pts.first() else { return false };
|
||||
let tol = 1e-7 * (1.0 + norm(q0));
|
||||
pts.iter().all(|q| dot(sub(*q, q0), z).abs() <= tol)
|
||||
|| pts.iter().all(|q| norm(cross(sub(*q, q0), z)) <= tol)
|
||||
})
|
||||
})
|
||||
}),
|
||||
AnalyticKind::Cylinder { .. } | AnalyticKind::Cone { .. } => square_or_ruled(ex, fe, &a.kind),
|
||||
_ => true,
|
||||
}),
|
||||
}
|
||||
@@ -1137,10 +1181,16 @@ fn merge_split_faces(ex: &mut p21::Exchange, paint: &mut HashMap<u64, FaceLook>,
|
||||
}
|
||||
}
|
||||
}
|
||||
// B-spline cylinder sheets under merged faces are written as cylinders (the sheet covers only
|
||||
// its own piece), with the parameter seam on the face's seam edge for a full turn, else
|
||||
// opposite the face's middle. (Faces replaced by merges stay in the exchange unreferenced:
|
||||
// only the shells' faces count.)
|
||||
merged_surfaces
|
||||
}
|
||||
|
||||
/// B-spline cylinder and cone sheets written as CYLINDRICAL_SURFACE and CONICAL_SURFACE (the
|
||||
/// sheet covers only its own piece, and a fillet's may reach far past it), with the direction of
|
||||
/// the parameter seam: on the face's seam edge for a full turn, else opposite the face's middle.
|
||||
/// Sheets under faces merged from pieces always; a lone piece when its edges are circles square
|
||||
/// to the axis and rulings. (Faces replaced by merges stay in the exchange unreferenced: only the
|
||||
/// shells' faces count.)
|
||||
fn analytic_sheets(ex: &p21::Exchange, merged_surfaces: &std::collections::HashSet<u64>) -> HashMap<u64, V3> {
|
||||
let live: Vec<u64> = ex
|
||||
.entities
|
||||
.values()
|
||||
@@ -1151,12 +1201,16 @@ fn merge_split_faces(ex: &mut p21::Exchange, paint: &mut HashMap<u64, FaceLook>,
|
||||
let mut out = HashMap::new();
|
||||
for e in live.iter().filter_map(|f| ex.get(*f)) {
|
||||
let Some(sid) = e.params().get(2).and_then(Param::as_ref_id) else { continue };
|
||||
if !merged_surfaces.contains(&sid) || out.contains_key(&sid) {
|
||||
if out.contains_key(&sid) {
|
||||
continue;
|
||||
}
|
||||
let Some(Analytic { kind: AnalyticKind::Cylinder { o, z, .. }, .. }) = ex.get(sid).and_then(|se| cylindrical_bspline(ex, se)) else {
|
||||
let Some(Analytic { kind, .. }) = ex.get(sid).and_then(|se| ruled_bspline(ex, se)) else {
|
||||
continue;
|
||||
};
|
||||
let (AnalyticKind::Cylinder { o, z, .. } | AnalyticKind::Cone { o, z, .. }) = kind else { continue };
|
||||
if !merged_surfaces.contains(&sid) && !square_or_ruled(ex, e, &kind) {
|
||||
continue;
|
||||
}
|
||||
let Some(loops) = face_edges(ex, e) else { continue };
|
||||
let radial = |q: V3| {
|
||||
let v = sub(q, o);
|
||||
@@ -1167,8 +1221,13 @@ fn merge_split_faces(ex: &mut p21::Exchange, paint: &mut HashMap<u64, FaceLook>,
|
||||
.flat_map(|l| l.iter().map(move |(x, _)| (l, *x)))
|
||||
.find(|(l, x)| l.iter().filter(|(y, _)| y == x).count() > 1)
|
||||
.map(|(_, x)| x);
|
||||
let x = match seam.and_then(|edge| ex.get(edge)?.params().get(1)?.as_ref_id()).and_then(|v| vertex_point(ex, v)) {
|
||||
Some(q) => unit(radial(q)),
|
||||
// (A cone tip's seam has one end at the apex, on the axis.)
|
||||
let seam_dir = seam.and_then(|edge| {
|
||||
let p = ex.get(edge)?.params();
|
||||
[p.get(1), p.get(2)].into_iter().filter_map(|v| vertex_point(ex, v?.as_ref_id()?)).find_map(|q| unit(radial(q)))
|
||||
});
|
||||
let x = match seam_dir {
|
||||
Some(x) => Some(x),
|
||||
None => {
|
||||
let mean = loops
|
||||
.iter()
|
||||
@@ -1187,8 +1246,8 @@ fn merge_split_faces(ex: &mut p21::Exchange, paint: &mut HashMap<u64, FaceLook>,
|
||||
out
|
||||
}
|
||||
|
||||
/// The B-rep entities of one body as truck writes them, parsed, and the surfaces of faces
|
||||
/// merged from pieces.
|
||||
/// The B-rep entities of one body as truck writes them, parsed, and the B-spline sheets to write
|
||||
/// as cylinders and cones (see `analytic_sheets`). Without `merge` the file is truck's, as is.
|
||||
fn brep_exchange(b: &Body, merge: bool) -> Result<(p21::Exchange, HashMap<u64, V3>, HashMap<u64, FaceLook>)> {
|
||||
let text = crate::step::truck_step(&[b], "SolidCraft")?;
|
||||
let mut ex = p21::parse(&text).map_err(|e| KernelError::Failed(format!("STEP export: {e}")))?;
|
||||
@@ -1209,9 +1268,17 @@ fn brep_exchange(b: &Body, merge: bool) -> Result<(p21::Exchange, HashMap<u64, V
|
||||
}
|
||||
let before = shell_face_count(&ex);
|
||||
let keep = b.split_lines().map(|l| l.as_slice()).unwrap_or(&[]);
|
||||
let merged = if merge { merge_split_faces(&mut ex, &mut paint, keep) } else { Default::default() };
|
||||
let sheets = if merge {
|
||||
let merged = merge_split_faces(&mut ex, &mut paint, keep);
|
||||
let sheets = analytic_sheets(&ex, &merged);
|
||||
// Lone sheets rewritten as analytic surfaces count as changes to check too.
|
||||
MERGES.with(|m| m.set(m.get() + sheets.keys().filter(|s| !merged.contains(s)).count()));
|
||||
sheets
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
MERGES.with(|m| m.set(m.get() + before.saturating_sub(shell_face_count(&ex))));
|
||||
Ok((ex, merged, paint))
|
||||
Ok((ex, sheets, paint))
|
||||
}
|
||||
|
||||
/// The separate pieces of a body for export, each keeping its face colours: a body of several
|
||||
@@ -1277,9 +1344,9 @@ fn rigid_frame(m: &[[f64; 4]; 4]) -> Result<([f64; 3], [f64; 3], [f64; 3])> {
|
||||
pub fn step_export_products(products: &[ExportProduct], root: usize, header: &StepHeader) -> Result<String> {
|
||||
MERGES.with(|m| m.set(0));
|
||||
let text = products_text(products, root, header, true)?;
|
||||
// Faces merged per surface must read back as cleanly as the pieces they came from (a merge
|
||||
// can leave a loop the reader cannot lay out); otherwise the pieces are written. Without a
|
||||
// merge there is nothing to check.
|
||||
// Faces merged per surface, and sheets rewritten as cylinders and cones, must read back as
|
||||
// cleanly as truck's pieces (a merge can leave a loop the reader cannot lay out); otherwise
|
||||
// the pieces are written as truck made them. Without a change there is nothing to check.
|
||||
let clean = |t: &str| crate::step_in::step_import(t).is_ok_and(|i| i.warnings.is_empty());
|
||||
if MERGES.with(|m| m.get()) > 0 && !clean(&text) {
|
||||
let plain = products_text(products, root, header, false)?;
|
||||
@@ -1291,7 +1358,7 @@ pub fn step_export_products(products: &[ExportProduct], root: usize, header: &St
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// Faces merged while writing the current export.
|
||||
/// Faces merged and sheets rewritten while writing the current export.
|
||||
static MERGES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user