STEP import: a whole sphere bounded by one seam has its frame on that seam

A SPHERICAL_SURFACE face bounded by one meridian used both ways is a whole ball, but the
file's placement need not have its poles at the meridian's ends: a sphere's frame is any
right-handed frame about its centre (the same surface, its natural normal outward). With
the poles elsewhere, the seam is a slit across the face's form, not the form's own seam;
SMLib's export refused the record ("a closed shell bounds two regions and encloses one"),
and the ball was not imported
(seam_only_sphere_whose_placement_axis_misses_its_poles_imports_as_a_whole_ball).

Before anything is measured on the frame, such a sphere's frame is put on its seam: the
axis through the seam's ends (which must be antipodes on the sphere, within the
tolerances), the x axis through the seam's middle. A seam already between the frame's
poles keeps the file's frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-28 18:14:38 -04:00
committed by Irakli Kourtis
parent 4688c0f20a
commit 44b859fe6e
2 changed files with 52 additions and 1 deletions
+3
View File
@@ -943,6 +943,9 @@ impl<'b, 'a> Builder<'b, 'a> {
fn finish(mut self, item: u64, name: String, shells: Vec<Vec<(u32, bool)>>, kind: Kind) -> R<Solid> {
let _span = solidcraft_profile::span("import", "step.solid_record");
let mut notes = Vec::new();
// A whole sphere bounded by one seam has its frame on that seam, before anything is
// measured on the frame.
self.seat_spheres_on_seams()?;
let at_poles = self.split_at_poles()?;
if at_poles > 0 {
notes.push(format!("{at_poles} edges split where they pass through a face's pole"));
+49 -1
View File
@@ -19,7 +19,8 @@
//! group as the file has it.
use solidcraft_geom::Vec3;
use solidcraft_geom::brep::{self, Curve as BCurve};
use solidcraft_geom::brep::eval::CurveEval;
use solidcraft_geom::brep::{self, Curve as BCurve, Frame, Surface as BSurface};
use super::{Builder, LoopB, R};
@@ -178,3 +179,50 @@ impl Builder<'_, '_> {
Ok(turned)
}
}
impl Builder<'_, '_> {
/// Put the frame of each whole sphere bounded by one seam alone (a meridian from pole to
/// pole, used both ways) on that seam: its axis through the seam's ends, its x axis through
/// the seam's middle. A sphere's frame is the writer's choice (any right-handed frame about
/// the centre is the same surface, its natural normal outward); one whose poles are not the
/// seam's leaves the seam a slit across the form, not the form's own seam, and no kernel
/// takes that as the whole ball. Returns how many frames were moved.
pub(super) fn seat_spheres_on_seams(&mut self) -> R<usize> {
let mut seated = 0;
for fi in 0..self.faces.len() {
let Some(face) = self.faces.get(fi) else { continue };
let BSurface::Sphere { frame, radius } = &face.surface else { continue };
let (frame, radius) = (frame.clone(), *radius);
let [LoopB::Edges(uses)] = face.loops.as_slice() else { continue };
let [(e, a), (f, b)] = uses.as_slice() else { continue };
if e != f || a == b {
continue;
}
let edge = self.edges.get(*e as usize).ok_or("edge reference")?;
let point = |v: u32| self.vertices.get(v as usize).map(|x| (Vec3::from(x.point), x.tolerance));
let (Some((start, t0)), Some((end, t1))) = (point(edge.vertices[0]), point(edge.vertices[1])) else { continue };
let centre = Vec3::from(frame.origin);
let reach = self.tol.max(edge.tolerance).max(t0).max(t1);
// The seam runs between antipodes: its ends on the sphere, opposite each other.
if (start.dist(centre) - radius.abs()).abs() > reach || (start - centre).dist(centre - end) > 2.0 * reach {
continue;
}
let Some(z) = (start - end).normalized() else { continue };
let axis = Vec3::from(frame.x_axis).cross(Vec3::from(frame.y_axis));
// Already the frame's poles.
if axis.cross(z).len() * radius.abs() <= reach {
continue;
}
let c = CurveEval::new(&edge.curve).map_err(|m| m.to_string())?;
let middle = c.point(0.5 * (edge.interval[0] + edge.interval[1])).ok_or("edge point")?;
let off = middle - centre;
let Some(x) = (off - z * off.dot(z)).normalized() else { continue };
let y = z.cross(x);
let Some(face) = self.faces.get_mut(fi) else { continue };
face.surface = BSurface::Sphere { frame: Frame { origin: frame.origin, x_axis: [x.x, x.y, x.z], y_axis: [y.x, y.y, y.z] }, radius };
face.extent = None;
seated += 1;
}
Ok(seated)
}
}