mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
STEP import: a face bounded by a slit alone is the whole of its closed surface
A loop that runs out along a chain of edges and straight back (a meridian from a sphere's pole stopping short of the other, out and back) encloses no (u, v) area, yet on a closed surface it bounds all of it less a cut of none. The reader only read such a face when the slit already ran pole to pole (and moved the sphere's frame onto it only then), so the 2 mm ball whose meridian stops at 2.5 rad was refused for its empty loop. The slit is now made the face's seams, so its loop bounds the surface's whole domain: on a sphere, a revolution whose profile ends on its axis, or a spline closed round u that collapses at both ends, the slit's meridian is run on to both poles, out and back (a sphere's frame put on the slit first, whichever two points it joins); on a torus, the circle the slit lies along is closed and the other circle through its start is the second seam. The seams on a sphere are its own meridians (angle 0 on the equator), so their pcurves are exact lines on its form. Tests: non_antipodal_seam_sphere_imports_as_a_whole_ball passes (its known-failing line goes); seam_only_torus_imports_as_a_whole_ring adds a torus bounded by a slit along its tube. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -146,12 +146,15 @@ fn non_antipodal_seam_sphere_imports_as_a_whole_ball() {
|
||||
|
||||
/// A whole ring torus (major radius 10, minor 3, about Z) written as one face bounded only by
|
||||
/// seams: its tube circle at u = 0 and its ring circle at v = 0, each run out and back, in
|
||||
/// either order round the loop. Each is the whole torus, volume 2π²Rr² and area 4π²Rr.
|
||||
/// either order round the loop; or only a slit along the tube circle, from the outer equator 2.5
|
||||
/// rad towards the top and back, the torus less a slit of no area. Each is the whole torus,
|
||||
/// volume 2π²Rr² and area 4π²Rr.
|
||||
#[test]
|
||||
fn seam_only_torus_imports_as_a_whole_ring() {
|
||||
use std::f64::consts::PI;
|
||||
let (major, minor) = (10.0_f64, 3.0_f64);
|
||||
let outer = major + minor;
|
||||
let (end_x, end_z) = (major + minor * 2.5_f64.cos(), minor * 2.5_f64.sin());
|
||||
let tube = format!(
|
||||
"#14=CARTESIAN_POINT('',({major:.16},0.,0.));\n#15=DIRECTION('',(0.,-1.,0.));\n#16=AXIS2_PLACEMENT_3D('',#14,#15,#3);\n#17=CIRCLE('',#16,{minor:.16});\n"
|
||||
);
|
||||
@@ -159,11 +162,19 @@ fn seam_only_torus_imports_as_a_whole_ring() {
|
||||
let cases = [
|
||||
("tube then ring", format!("{tube}#20=EDGE_CURVE('',#11,#11,#17,.T.);\n{ring}#30=EDGE_LOOP('',(#21,#22,#23,#24));\n")),
|
||||
("ring then tube", format!("{tube}#20=EDGE_CURVE('',#11,#11,#17,.T.);\n{ring}#30=EDGE_LOOP('',(#22,#21,#24,#23));\n")),
|
||||
(
|
||||
"a slit along the tube",
|
||||
format!(
|
||||
"#12=CARTESIAN_POINT('',({end_x:.16},0.,{end_z:.16}));\n#13=VERTEX_POINT('',#12);\n{tube}#20=EDGE_CURVE('',#11,#13,#17,.T.);\n#30=EDGE_LOOP('',(#21,#23));\n"
|
||||
),
|
||||
),
|
||||
];
|
||||
for (what, edges) in cases {
|
||||
let mut t = String::from(HEAD);
|
||||
t += "#1=CARTESIAN_POINT('',(0.,0.,0.));\n#2=DIRECTION('',(0.,0.,1.));\n#3=DIRECTION('',(1.,0.,0.));\n#4=AXIS2_PLACEMENT_3D('',#1,#2,#3);\n";
|
||||
t += &format!("#5=TOROIDAL_SURFACE('',#4,{major:.16},{minor:.16});\n#10=CARTESIAN_POINT('',({outer:.16},0.,0.));\n#11=VERTEX_POINT('',#10);\n");
|
||||
t += &format!(
|
||||
"#5=TOROIDAL_SURFACE('',#4,{major:.16},{minor:.16});\n#10=CARTESIAN_POINT('',({outer:.16},0.,0.));\n#11=VERTEX_POINT('',#10);\n"
|
||||
);
|
||||
t += &edges;
|
||||
t += "#21=ORIENTED_EDGE('',*,*,#20,.T.);\n#22=ORIENTED_EDGE('',*,*,#19,.T.);\n#23=ORIENTED_EDGE('',*,*,#20,.F.);\n#24=ORIENTED_EDGE('',*,*,#19,.F.);\n";
|
||||
t += "#31=FACE_OUTER_BOUND('',#30,.T.);\n#32=ADVANCED_FACE('',(#31),#5,.T.);\n#33=CLOSED_SHELL('',(#32));\n#34=MANIFOLD_SOLID_BREP('Ring',#33);\n";
|
||||
|
||||
@@ -943,9 +943,10 @@ 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()?;
|
||||
// A face bounded by a slit alone is the whole of its closed surface, closed by its seams
|
||||
// (a sphere's frame put on the slit), before anything is measured on the frame or split
|
||||
// at its poles.
|
||||
self.complete_slits()?;
|
||||
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"));
|
||||
|
||||
@@ -28,8 +28,7 @@
|
||||
use std::f64::consts::TAU;
|
||||
|
||||
use solidcraft_geom::Vec3;
|
||||
use solidcraft_geom::brep::eval::CurveEval;
|
||||
use solidcraft_geom::brep::{self, Curve as BCurve, Frame, Surface as BSurface};
|
||||
use solidcraft_geom::brep::{self, Curve as BCurve};
|
||||
|
||||
use super::{Builder, LoopB, R};
|
||||
|
||||
@@ -257,50 +256,3 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
//! column (u at the start of its knots), the loops split there where they have no vertex on it;
|
||||
//! the period is the width of its knots. A torus face winding without a seam, and a spline
|
||||
//! closed in v or both ways, are refused for now.
|
||||
//!
|
||||
//! A face bounded by a slit alone (a loop out along edges and straight back, of no (u, v) area)
|
||||
//! on a closed surface is all of that surface less a cut of none: its slit is made its seams
|
||||
//! ([`Builder::complete_slits`]), so the loop bounds the surface's whole (u, v) domain.
|
||||
|
||||
use std::f64::consts::{PI, TAU};
|
||||
|
||||
@@ -37,6 +41,21 @@ fn wrap_by(x: f64, period: f64) -> f64 {
|
||||
x - (x / period).round() * period
|
||||
}
|
||||
|
||||
/// The path a slit runs out along: a loop that runs out along a chain of edges and straight back
|
||||
/// along it, each edge used once each way, started at one of its two turns; `None` for any
|
||||
/// other loop.
|
||||
pub(super) fn slit_path(es: &[(u32, bool)]) -> Option<Vec<(u32, bool)>> {
|
||||
let n = es.len();
|
||||
if n < 2 || !n.is_multiple_of(2) || !super::seam_only(es) {
|
||||
return None;
|
||||
}
|
||||
// A turn: a use followed by its edge the other way.
|
||||
let k = (0..n).find(|&k| matches!((es.get((k + n - 1) % n), es.get(k)), (Some(a), Some(b)) if a.0 == b.0))?;
|
||||
let ring: Vec<(u32, bool)> = es.iter().skip(k).chain(es.iter().take(k)).copied().collect();
|
||||
let (out, back) = ring.split_at(n / 2);
|
||||
back.iter().rev().zip(out).all(|(b, o)| b.0 == o.0 && b.1 != o.1).then(|| out.to_vec())
|
||||
}
|
||||
|
||||
/// The axis frame of a surface of revolution: origin, unit axis, and the pole points at the
|
||||
/// ends of its v range that have one (min v, max v).
|
||||
struct Axis {
|
||||
@@ -457,14 +476,23 @@ impl Builder<'_, '_> {
|
||||
let (curve, interval, along) = match &ax.kind {
|
||||
Kind::Ruled => (BCurve::Line { start: arr(pa), end: arr(pb) }, [0.0, 1.0], true),
|
||||
Kind::Sphere(r) => {
|
||||
let x = (pa - ax.origin).normalized().ok_or("a seam from the sphere's centre")?;
|
||||
let to = pb - ax.origin;
|
||||
let y = (to - x * to.dot(x)).normalized().ok_or("a seam between opposite points")?;
|
||||
let theta = to.dot(y).atan2(to.dot(x));
|
||||
if !(theta > 0.0 && theta <= PI) {
|
||||
return Err("a degenerate seam".into());
|
||||
// The sphere's own meridian through the end further from the axis: angle 0 on the
|
||||
// equator, π/2 at the upper pole, so its angle is the surface's latitude and its
|
||||
// quarters the surface's.
|
||||
let (da, db) = (pa - ax.origin, pb - ax.origin);
|
||||
let (ra, rb) = (da - ax.z * da.dot(ax.z), db - ax.z * db.dot(ax.z));
|
||||
let x = if ra.len() >= rb.len() { ra } else { rb }.normalized().ok_or("a seam along the sphere's axis")?;
|
||||
let latitude = |d: Vec3| d.dot(ax.z).atan2(d.dot(x));
|
||||
let (ta, tb) = (latitude(da), latitude(db));
|
||||
if !(ta.abs() <= 0.5 * PI + 1e-9 && tb.abs() <= 0.5 * PI + 1e-9) {
|
||||
return Err(unsupported("a seam between loops at opposite angles"));
|
||||
}
|
||||
(BCurve::Circle { frame: Frame { origin: arr(ax.origin), x_axis: arr(x), y_axis: arr(y) }, radius: *r }, [0.0, theta], true)
|
||||
let along = ta < tb;
|
||||
(
|
||||
BCurve::Circle { frame: Frame { origin: arr(ax.origin), x_axis: arr(x), y_axis: arr(ax.z) }, radius: *r },
|
||||
if along { [ta, tb] } else { [tb, ta] },
|
||||
along,
|
||||
)
|
||||
}
|
||||
Kind::Iso { column, .. } => {
|
||||
// Each vertex's v on the surface (a pole's from its end of the v range).
|
||||
@@ -507,7 +535,14 @@ impl Builder<'_, '_> {
|
||||
return Err(unsupported("a seam off its surface: loops with no vertices at one angle"));
|
||||
}
|
||||
}
|
||||
let vertices = if along { [a, b] } else { [b, a] };
|
||||
let i = self.new_edge(curve, interval, if along { [a, b] } else { [b, a] })?;
|
||||
Ok((i, along))
|
||||
}
|
||||
|
||||
/// A new edge on `curve` over `interval` between `vertices`, each vertex's tolerance raised to
|
||||
/// cover its gap to the curve's end.
|
||||
fn new_edge(&mut self, curve: BCurve, interval: [f64; 2], vertices: [u32; 2]) -> R<u32> {
|
||||
let c = CurveEval::new(&curve).map_err(|m| m.to_string())?;
|
||||
for (t, v) in interval.into_iter().zip(vertices) {
|
||||
let gap = c.point(t).ok_or("seam point")?.dist(self.vpoint(v)?);
|
||||
if let Some(x) = self.vertices.get_mut(v as usize) {
|
||||
@@ -516,7 +551,242 @@ impl Builder<'_, '_> {
|
||||
}
|
||||
let i = index(self.edges.len())?;
|
||||
self.edges.push(EdgeB { curve, interval, vertices, tolerance: self.tol, form: None });
|
||||
Ok((i, along))
|
||||
Ok(i)
|
||||
}
|
||||
|
||||
/// Complete every face bounded by a slit alone into the whole of its surface. A slit is a
|
||||
/// loop that runs out along a chain of edges and straight back ([`slit_path`]): it encloses
|
||||
/// no (u, v) area, yet on a closed surface it bounds all of it less a cut of none (a ball
|
||||
/// whose one face is bounded by part of a meridian, out and back). Schema 4 keeps that face
|
||||
/// as the surface's whole (u, v) domain, closed by its seams, so the slit is made one: on a
|
||||
/// surface of revolution closed at both poles (a sphere, a revolution whose profile ends on
|
||||
/// its axis, a spline closed round u that collapses at both v ends), the meridian the slit
|
||||
/// lies along is run on from its ends to the poles, out and back; on a torus, the circle the
|
||||
/// slit lies along (round the tube or round the axis) is closed and the other circle through
|
||||
/// its start is the second seam. Returns how many faces were completed.
|
||||
pub(super) fn complete_slits(&mut self) -> R<usize> {
|
||||
let mut completed = 0;
|
||||
for fi in 0..self.faces.len() {
|
||||
let Some(face) = self.faces.get(fi) else { continue };
|
||||
let [LoopB::Edges(es)] = face.loops.as_slice() else { continue };
|
||||
let Some(path) = slit_path(es) else { continue };
|
||||
let (step, surface) = (face.step, face.surface.clone());
|
||||
let (merged, seated) = match &surface {
|
||||
BSurface::Torus { frame, major_radius, .. } => (self.torus_slit(&path, frame, *major_radius)?, None),
|
||||
_ => match self.closed_at_poles(&path, &surface)? {
|
||||
// An open surface: the slit bounds nothing, and is refused for that later.
|
||||
None => continue,
|
||||
Some(Some(m)) => (Some(m), None),
|
||||
// A sphere's frame is the writer's choice (any right-handed frame about its
|
||||
// centre is the same surface, its natural normal outward): one whose
|
||||
// meridians miss the slit has its pole put at the slit's start instead, its x
|
||||
// axis through the slit's middle.
|
||||
Some(None) => match &surface {
|
||||
BSurface::Sphere { frame, radius } => match self.seated(&path, frame, *radius)? {
|
||||
Some(s) => (self.closed_at_poles(&path, &s)?.flatten(), Some(s)),
|
||||
None => (None, None),
|
||||
},
|
||||
_ => (None, None),
|
||||
},
|
||||
},
|
||||
};
|
||||
let merged = merged.ok_or_else(|| format!("face #{step}: {}", unsupported("a face bounded by a slit off its surface's seam lines")))?;
|
||||
let Some(face) = self.faces.get_mut(fi) else { continue };
|
||||
if let Some(s) = seated {
|
||||
face.surface = s;
|
||||
face.extent = None;
|
||||
}
|
||||
if let Some(LoopB::Edges(es)) = face.loops.first_mut() {
|
||||
*es = merged;
|
||||
}
|
||||
completed += 1;
|
||||
}
|
||||
Ok(completed)
|
||||
}
|
||||
|
||||
/// The slit made the meridian seam of `surface` when it is a surface of revolution closed
|
||||
/// at both poles ([`Builder::meridian_slit`]); `None` for any other surface.
|
||||
fn closed_at_poles(&mut self, path: &[(u32, bool)], surface: &BSurface) -> R<Option<Option<Vec<(u32, bool)>>>> {
|
||||
let eval = SurfaceEval::new(surface).map_err(|e| e.to_string())?;
|
||||
match axis(surface).or_else(|| spline_axis(surface, &eval)) {
|
||||
Some(ax) if ax.poles.iter().all(Option::is_some) => Ok(Some(self.meridian_slit(path, &ax, &eval)?)),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// The sphere about `frame`'s centre with its upper pole at the slit's start and its x axis
|
||||
/// through the middle of the slit's middle edge; `None` where those coincide.
|
||||
fn seated(&self, path: &[(u32, bool)], frame: &Frame, radius: f64) -> R<Option<BSurface>> {
|
||||
let centre = Vec3::from(frame.origin);
|
||||
let first = *path.first().ok_or("empty slit")?;
|
||||
let Some(z) = (self.vpoint(self.start(first)?)? - centre).normalized() else { return Ok(None) };
|
||||
let &(e, _) = path.get(path.len() / 2).ok_or("empty slit")?;
|
||||
let edge = self.edges.get(e as usize).ok_or("edge reference")?;
|
||||
let middle =
|
||||
CurveEval::new(&edge.curve).map_err(|m| m.to_string())?.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 { return Ok(None) };
|
||||
Ok(Some(BSurface::Sphere { frame: Frame { origin: frame.origin, x_axis: arr(x), y_axis: arr(z.cross(x)) }, radius }))
|
||||
}
|
||||
|
||||
/// The vertex use `u` ends at.
|
||||
fn end(&self, (e, forward): (u32, bool)) -> R<u32> {
|
||||
self.start((e, !forward))
|
||||
}
|
||||
|
||||
/// Vertex `v` is at `p`, within its tolerance.
|
||||
fn at(&self, v: u32, p: Vec3) -> R<bool> {
|
||||
let x = self.vertices.get(v as usize).ok_or("vertex reference")?;
|
||||
Ok(Vec3::from(x.point).dist(p) <= self.tol.max(x.tolerance))
|
||||
}
|
||||
|
||||
/// Every point of the path's edges lies on one meridian of the surface (the spline's
|
||||
/// boundary column for [`Kind::Iso`]), the poles apart.
|
||||
fn on_meridian(&self, path: &[(u32, bool)], ax: &Axis, eval: &SurfaceEval) -> R<bool> {
|
||||
let mut u_ref = match ax.kind {
|
||||
Kind::Iso { u0, .. } => Some(u0),
|
||||
_ => None,
|
||||
};
|
||||
for &(e, _) in path {
|
||||
let edge = self.edges.get(e as usize).ok_or("edge reference")?;
|
||||
let c = CurveEval::new(&edge.curve).map_err(|m| m.to_string())?;
|
||||
let ([lo, hi], reach) = (edge.interval, self.tol.max(edge.tolerance));
|
||||
for k in 0..=SAMPLES {
|
||||
let p = c.point(lo + (hi - lo) * k as f64 / SAMPLES as f64).ok_or("edge point")?;
|
||||
if ax.poles.iter().flatten().any(|q| q.dist(p) <= reach) {
|
||||
continue;
|
||||
}
|
||||
let Some(q) = eval.project(p, None, f64::INFINITY) else { return Ok(false) };
|
||||
let u = *u_ref.get_or_insert(q[0]);
|
||||
if !eval.point([u, q[1]]).is_some_and(|m| m.dist(p) <= reach) {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(u_ref.is_some())
|
||||
}
|
||||
|
||||
/// A slit along a meridian of a surface closed at both poles, run on to them: the meridian
|
||||
/// from the lower pole to the upper (the slit's path, with a seam edge from each end of it
|
||||
/// that is not at its pole), then back. `None` when the slit is off every meridian.
|
||||
fn meridian_slit(&mut self, path: &[(u32, bool)], ax: &Axis, eval: &SurfaceEval) -> R<Option<Vec<(u32, bool)>>> {
|
||||
let (Some(south), Some(north)) = (ax.poles[0], ax.poles[1]) else { return Ok(None) };
|
||||
if !self.on_meridian(path, ax, eval)? {
|
||||
return Ok(None);
|
||||
}
|
||||
// How far up the meridian a vertex is: a pole's from its end, else its v.
|
||||
let height = |s: &Self, v: u32| -> R<f64> {
|
||||
Ok(if s.at(v, south)? {
|
||||
f64::NEG_INFINITY
|
||||
} else if s.at(v, north)? {
|
||||
f64::INFINITY
|
||||
} else {
|
||||
eval.project(s.vpoint(v)?, None, f64::INFINITY).ok_or("a slit vertex off its surface")?[1]
|
||||
})
|
||||
};
|
||||
let (first, last) = (*path.first().ok_or("empty slit")?, *path.last().ok_or("empty slit")?);
|
||||
let (ha, hb) = (height(self, self.start(first)?)?, height(self, self.end(last)?)?);
|
||||
if !(ha != hb) {
|
||||
return Ok(None);
|
||||
}
|
||||
// Up the meridian.
|
||||
let mut up = path.to_vec();
|
||||
if ha > hb {
|
||||
super::reverse(&mut up);
|
||||
}
|
||||
let (a, b) = (self.start(*up.first().ok_or("empty slit")?)?, self.end(*up.last().ok_or("empty slit")?)?);
|
||||
let pole_vertex = |s: &mut Self, p: Vec3| -> R<u32> {
|
||||
let v = index(s.vertices.len())?;
|
||||
s.vertices.push(Vertex { point: arr(p), tolerance: s.tol });
|
||||
Ok(v)
|
||||
};
|
||||
let mut merged = Vec::with_capacity(2 * up.len() + 4);
|
||||
if !self.at(a, south)? {
|
||||
let s = pole_vertex(self, south)?;
|
||||
let (seam, along) = self.seam_edge(a, s, ax, eval, true)?;
|
||||
merged.push((seam, !along));
|
||||
}
|
||||
merged.extend(up);
|
||||
if !self.at(b, north)? {
|
||||
let n = pole_vertex(self, north)?;
|
||||
merged.push(self.seam_edge(b, n, ax, eval, true)?);
|
||||
}
|
||||
let mut back = merged.clone();
|
||||
super::reverse(&mut back);
|
||||
merged.extend(back);
|
||||
Ok(Some(merged))
|
||||
}
|
||||
|
||||
/// A slit along a circle of a torus (round its tube, or round its axis) made the torus's two
|
||||
/// seams: that circle closed from the slit's end round to its start, the way the slit runs,
|
||||
/// and the other circle through the start; the loop runs round the first, the second, the
|
||||
/// first back and the second back. `None` when the slit is off both circles.
|
||||
fn torus_slit(&mut self, path: &[(u32, bool)], frame: &Frame, major: f64) -> R<Option<Vec<(u32, bool)>>> {
|
||||
let (o, z) = (Vec3::from(frame.origin), Vec3::from(frame.x_axis).cross(Vec3::from(frame.y_axis)));
|
||||
let (first, last) = (*path.first().ok_or("empty slit")?, *path.last().ok_or("empty slit")?);
|
||||
let (a, b) = (self.start(first)?, self.end(last)?);
|
||||
let (pa, pb) = (self.vpoint(a)?, self.vpoint(b)?);
|
||||
let off = pa - o;
|
||||
let h = off.dot(z);
|
||||
let Some(radial) = (off - z * h).normalized() else { return Ok(None) };
|
||||
// The circles through the slit's start, as centre and unit normal: round the tube in the
|
||||
// plane through the axis, and round the axis at the start's height.
|
||||
let tube = (o + radial * major, radial.cross(z));
|
||||
let ring = (o + z * h, z);
|
||||
let along = |s: &Self, (c, n): (Vec3, Vec3)| -> R<bool> {
|
||||
let r = (pa - c).len();
|
||||
for &(e, _) in path {
|
||||
let edge = s.edges.get(e as usize).ok_or("edge reference")?;
|
||||
let curve = CurveEval::new(&edge.curve).map_err(|m| m.to_string())?;
|
||||
let ([lo, hi], reach) = (edge.interval, s.tol.max(edge.tolerance));
|
||||
for k in 0..=SAMPLES {
|
||||
let d = curve.point(lo + (hi - lo) * k as f64 / SAMPLES as f64).ok_or("edge point")? - c;
|
||||
if !(d.dot(n).abs() <= reach && (d.len() - r).abs() <= reach) {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
};
|
||||
let (circle, other) = if along(self, tube)? {
|
||||
(tube, ring)
|
||||
} else if along(self, ring)? {
|
||||
(ring, tube)
|
||||
} else {
|
||||
return Ok(None);
|
||||
};
|
||||
let mut round = path.to_vec();
|
||||
if a != b {
|
||||
// On from the slit's end the way it arrives there, from a point of its last edge
|
||||
// just before.
|
||||
let (c, n) = circle;
|
||||
let edge = self.edges.get(last.0 as usize).ok_or("edge reference")?;
|
||||
let [lo, hi] = edge.interval;
|
||||
let t = if last.1 { hi - (hi - lo) / SAMPLES as f64 } else { lo + (hi - lo) / SAMPLES as f64 };
|
||||
let before = CurveEval::new(&edge.curve).map_err(|m| m.to_string())?.point(t).ok_or("edge point")?;
|
||||
let Some(x) = (pb - c).normalized() else { return Ok(None) };
|
||||
let y = n.cross(x);
|
||||
let y = if (pb - before).dot(y) >= 0.0 { y } else { -y };
|
||||
let d = pa - c;
|
||||
let theta = d.dot(y).atan2(d.dot(x)).rem_euclid(TAU);
|
||||
let radius = (pb - c).len();
|
||||
if !(theta * radius > self.tol) {
|
||||
return Ok(None);
|
||||
}
|
||||
let rest = BCurve::Circle { frame: Frame { origin: arr(c), x_axis: arr(x), y_axis: arr(y) }, radius };
|
||||
round.push((self.new_edge(rest, [0.0, theta], [b, a])?, true));
|
||||
}
|
||||
let (c, n) = other;
|
||||
let Some(x) = (pa - c).normalized() else { return Ok(None) };
|
||||
let full = BCurve::Circle { frame: Frame { origin: arr(c), x_axis: arr(x), y_axis: arr(n.cross(x)) }, radius: (pa - c).len() };
|
||||
let second = self.new_edge(full, [0.0, TAU], [a, a])?;
|
||||
let mut back = round.clone();
|
||||
super::reverse(&mut back);
|
||||
let mut merged = round;
|
||||
merged.push((second, true));
|
||||
merged.extend(back);
|
||||
merged.push((second, false));
|
||||
Ok(Some(merged))
|
||||
}
|
||||
|
||||
/// Join loop `li` of face `fi` to the pole the face covers: seam out and back.
|
||||
|
||||
@@ -16,5 +16,4 @@ solidcraft-kernel::blend::corner_tests::corner_blends_hold_at_any_scale at 0.01x
|
||||
solidcraft-kernel::blend_corner::tests::issue2422_vertical_after_separate_bottom_rounds SMLib's check refuses the fillet: edge and vertex tolerance 0.97 against the body's 0.01 (#2422)
|
||||
solidcraft-kernel::corner_g2::tests::a_long_setback_keeps_the_g2_patch_inside_the_box at set backs 11 to 15 the G2 corner patch bulges 2.6e-6 to 1.33e-3 past the box (the explicit corner is in progress)
|
||||
solidcraft-kernel::corner_g2::tests::the_box_corner_g2_patch_is_a_closed_solid_curvature_continuous_to_its_neighbours the fitted G2 corner patch bulges 1.7e-5 past the box, which SMLib's exact tight box now sees (the explicit G2 corner builds it inside)
|
||||
solidcraft-kernel::step_in::tests::non_antipodal_seam_sphere_imports_as_a_whole_ball the import refuses the ball: its face's loop, a meridian slit from the pole run out and back, encloses no (u, v) area, so the whole sphere less a slit is not read as the sphere
|
||||
solidcraft-kernel::tests::fillet_tabbed_plate_outer_loop_and_partial_selection_after_hole the arc and tab rounds together remove 9.20067 against 9.20420 (3.8e-4 off, 1e-5 allowed)
|
||||
|
||||
Reference in New Issue
Block a user