mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 04:52:10 +00:00
surface: every caller extends a surface exactly (extended_sides): analytic kinds, ball patches and blends as themselves, offsets as their base extended
extended_sides now covers every kind the kernel extends, each exactly and certified: - a cone, sphere or torus by its own NURBS net continued Natural, still on its analytic surface, round its axis up to a quarter of the turn it leaves open and along its generator never past its apex, pole or full turn; - a ball patch by its own net, still on its sphere, its pole's side collapsed; - a procedural blend by its definition (a G^n blend's bound grown over its section's weight in closed form; a side inside its spine by the definition's spans there); - a plane widened as a plane, or by its own net for a caller keeping its trims (extended_sides_keeping_parameters); an offset surface as the offset of its base extended; an exact round's definition carried on a Natural continuation; - a straight direction's continuation widens its one span (joint knot removed exactly); the Natural continuation is built in a local frame per row. The callers switch to it: the offset executive's extended surfaces, the fillet side and end faces, the B-rep face extensions (lamina, seam). An offset joint at multiplicity p is a crease only where its one-sided normals differ. Tests: wr698's toy-net blend, issue_2611 (flag off closes), issue_398 and wr_344 pinned on the exact extensions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NvoxKMbMFSQU8tzRtoJhtT
This commit is contained in:
@@ -96,7 +96,6 @@ use crate::geom::a_object::BooleanOperation;
|
||||
use crate::geom::context::{with_mark_lock, MarkType, ALL_MARKS};
|
||||
use crate::geom::extent1d::Extent1d;
|
||||
use crate::geom::extent3d::Extent3d;
|
||||
use crate::geom::geom_utility::p2::ContinuityType;
|
||||
use crate::geom::global_solver::SolverOperation;
|
||||
use crate::geom::tol::{self, SmallTopoType};
|
||||
use crate::intersect::point_class::{PointClassType, PointClassification};
|
||||
@@ -1624,7 +1623,13 @@ fn ext_surface_rebuild(
|
||||
.clone()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP))?;
|
||||
let new_surf = surface
|
||||
.create_extended_surface(k, ext, ContinuityType::G1)
|
||||
.extended_sides(
|
||||
k,
|
||||
&crate::surface::side::BoxSide::ALL,
|
||||
crate::surface::extend::Continuity::Tangent,
|
||||
ext,
|
||||
)
|
||||
.map(|e| e.surface)
|
||||
.map_err(|e| {
|
||||
if e.is_unported_or_cancelled() {
|
||||
e
|
||||
|
||||
@@ -109,7 +109,6 @@ use crate::blend::fillet_standard_solver::p1::{FilletLaw, VariableRadiusFs};
|
||||
use crate::curve::curve::p1::GlobalPointSolveOptions;
|
||||
use crate::curve::curve::{Curve, CurveKind};
|
||||
use crate::error::{ignored, EntityRef, Result, SolidError, SM_ERR_INVALID_INPUT};
|
||||
use crate::geom::geom_utility::p2::ContinuityType;
|
||||
use crate::geom::global_solver::{SolutionRequested, SolverOperation};
|
||||
use crate::geom::solution_array::SolutionObject;
|
||||
use crate::geom::vector2d::Point2;
|
||||
@@ -433,8 +432,14 @@ impl FilletCorner {
|
||||
.surface
|
||||
.as_ref()
|
||||
.ok_or_else(|| null_deref("side face without surface"))?;
|
||||
let new_surface =
|
||||
side_surface.create_extended_surface(k, 10.0 * gap_dist, ContinuityType::G1)?;
|
||||
let new_surface = side_surface
|
||||
.extended_sides(
|
||||
k,
|
||||
&crate::surface::side::BoxSide::ALL,
|
||||
crate::surface::extend::Continuity::Tangent,
|
||||
10.0 * gap_dist,
|
||||
)?
|
||||
.surface;
|
||||
|
||||
// Copy pNewSurface, when possible as an analytic surface.
|
||||
let new_analytic_surface = new_surface.copy_and_add_analytics(k)?;
|
||||
|
||||
@@ -37,7 +37,6 @@ use crate::blend::fillet_geom::p1::{
|
||||
};
|
||||
use crate::blend::fillet_solver::p1::FilletSolver;
|
||||
use crate::error::{EntityRef, Result, SolidError};
|
||||
use crate::geom::geom_utility::p2::ContinuityType;
|
||||
use crate::intersect::point_class::PointClassType;
|
||||
use crate::kernel::Kernel;
|
||||
use crate::nurbs::b_spline_surface_n_lib::p2::OrientType;
|
||||
@@ -361,7 +360,14 @@ impl FilletCorner {
|
||||
.surface
|
||||
.as_ref()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP))?;
|
||||
let new_surface = surface.create_extended_surface(k, ext_dist, ContinuityType::G1)?;
|
||||
let new_surface = surface
|
||||
.extended_sides(
|
||||
k,
|
||||
&crate::surface::side::BoxSide::ALL,
|
||||
crate::surface::extend::Continuity::Tangent,
|
||||
ext_dist,
|
||||
)?
|
||||
.surface;
|
||||
|
||||
// Copy pNewSurface, when possible as an analytic surface
|
||||
let new_analytic_surface = new_surface.copy_and_add_analytics(k)?;
|
||||
|
||||
@@ -28,7 +28,6 @@ use crate::blend::fillet_geom::p1::{
|
||||
FilletBrep, FilletCornerId, FilletEdgeType, FilletGeomId, FilletVertex, FilletVertexType,
|
||||
};
|
||||
use crate::error::{EntityRef, Result, SolidError, SM_ERR, SM_ERR_INVALID_INPUT};
|
||||
use crate::geom::geom_utility::p2::ContinuityType;
|
||||
use crate::intersect::point_class::PointClassType;
|
||||
use crate::kernel::Kernel;
|
||||
use crate::math::{Point3, EFF_ZERO};
|
||||
@@ -268,11 +267,13 @@ impl FilletCorner {
|
||||
// there depends on how its curvature differs from the rail's face across the
|
||||
// seam, which a linear (G1) extension would not keep (tangency-labs/smlib@856b54e).
|
||||
let extension = if mixed && (tangent_side[0] || tangent_side[1]) {
|
||||
ContinuityType::CInfinity
|
||||
crate::surface::extend::Continuity::Natural
|
||||
} else {
|
||||
ContinuityType::G1
|
||||
crate::surface::extend::Continuity::Tangent
|
||||
};
|
||||
let new_surface = surface.create_extended_surface(k, ext_dist, extension)?;
|
||||
let new_surface = surface
|
||||
.extended_sides(k, &crate::surface::side::BoxSide::ALL, extension, ext_dist)?
|
||||
.surface;
|
||||
// Copy pNewSurface, when possible as an analytic surface
|
||||
let new_extend_surface = new_surface.copy_and_add_analytics(k)?;
|
||||
// If AddExtendedOriginalSurface() deleted pNewExtendSurface, then
|
||||
|
||||
@@ -1426,10 +1426,15 @@ pub struct GnVarying {
|
||||
pub r: [f64; 2],
|
||||
/// `pCells`: I's enclosures, computed once per cell.
|
||||
pub cells: GnAreaCells,
|
||||
/// The thumbweight's proved enclosure, `[lo, hi]`, over every span certified with this
|
||||
/// ([`gn_certify_span`]: the union of its pieces' [`GnRowTerms::w`] at each level whose
|
||||
/// bound it returns); `[inf, -inf]` before any. Not upstream.
|
||||
pub w: [f64; 2],
|
||||
}
|
||||
|
||||
/// `sm_GnRowTerms`: the largest `|(sum_ik dg_ikj c'_ik)|` (`x`) and `|W_j|` (`y`),
|
||||
/// the least `W_j` (`w_lo`) per row, and the largest `|S - O|` (`r`).
|
||||
/// the least `W_j` (`w_lo`) per row, the largest `|S - O|` (`r`), and the equal area
|
||||
/// thumbweight's proved enclosure over the pieces (`w`).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct GnRowTerms {
|
||||
/// `X`.
|
||||
@@ -1440,6 +1445,10 @@ pub struct GnRowTerms {
|
||||
pub w_lo: Vec<f64>,
|
||||
/// `R`.
|
||||
pub r: f64,
|
||||
/// The equal area thumbweight over every piece folded in, `[lo, hi]`: the union of the
|
||||
/// interval Newton roots `w0` (each holds every piece's root, for every s in it); `[inf,
|
||||
/// -inf]` before any. Not upstream: the fork discards `w0` after the inversion.
|
||||
pub w: [f64; 2],
|
||||
}
|
||||
|
||||
/// `sm_Centred`: the coefficients `0..=SM_GN_TAY` of a series over a piece in their
|
||||
@@ -1548,6 +1557,8 @@ pub fn gn_row_terms_over(
|
||||
if !root {
|
||||
return false;
|
||||
}
|
||||
let [w_lo, w_hi] = rt.w;
|
||||
rt.w = [cmin(w_lo, w0.lo), cmax(w_hi, w0.hi)];
|
||||
let over_w0 = vary.cells.over(k, w0);
|
||||
let sw = gn_invert(&tg, &over_w0, w0);
|
||||
let form = gn_form_normalized(n, sw);
|
||||
@@ -1906,6 +1917,7 @@ pub fn gn_certify_span(
|
||||
y: vec![0.0; ld],
|
||||
w_lo: vec![f64::INFINITY; ld],
|
||||
r: 0.0,
|
||||
w: [f64::INFINITY, f64::NEG_INFINITY],
|
||||
};
|
||||
for pair in ends.windows(2) {
|
||||
if !proved {
|
||||
@@ -2053,7 +2065,15 @@ pub fn gn_certify_span(
|
||||
continue;
|
||||
}
|
||||
// certified by each parameter's own term, never larger than the fork's
|
||||
bound_out = cmin(bound_out, round_up(level_bound(&uv4) * (1.0 + 1.0e-12)));
|
||||
let bound = round_up(level_bound(&uv4) * (1.0 + 1.0e-12));
|
||||
if bound < bound_out {
|
||||
// the thumbweight over the span, proved at the level whose bound is returned
|
||||
if let Some(v) = vary.as_deref_mut() {
|
||||
let ([lo, hi], [rlo, rhi]) = (v.w, rows.w);
|
||||
v.w = [cmin(lo, rlo), cmax(hi, rhi)];
|
||||
}
|
||||
}
|
||||
bound_out = cmin(bound_out, bound);
|
||||
if bound_out <= target {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -453,16 +453,35 @@ fn pieces(pb: &ProceduralBlend, ld: usize) -> Result<Vec<Piece>> {
|
||||
/// net's distance from its rows and the floor follow the round's size, not where it lies;
|
||||
/// the move's own rounding ([`frame_rounding`]) is added to the net's distance.
|
||||
pub fn recertify(k: &Kernel, pb: &ProceduralBlend) -> Result<f64> {
|
||||
Ok(recertify_with_thumbweight(k, pb)?.0)
|
||||
}
|
||||
|
||||
/// [`recertify`], with the equal area thumbweight's proved enclosure `[lo, hi]` over every
|
||||
/// span recertified ([`GnVarying::w`](super::certificate::GnVarying::w), from the certificate's interval Newton roots); `None`
|
||||
/// for any other section (a constant thumbweight is its own).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Those of [`recertify`].
|
||||
pub fn recertify_with_thumbweight(
|
||||
k: &Kernel,
|
||||
pb: &ProceduralBlend,
|
||||
) -> Result<(f64, Option<[f64; 2]>)> {
|
||||
crate::profile::profile_scope!(k, "blend::certificate::recertify");
|
||||
let centre = frame_centre(pb.net())?;
|
||||
let local = pb.translated(k, centre * -1.0)?;
|
||||
recertify_in_frame(k, &local, pb.bound(), frame_rounding(pb.net())?)
|
||||
}
|
||||
|
||||
/// [`recertify`] of a blend already in its own frame, against `bound`, its net's
|
||||
/// distance from its rows taken as at least `shift` more.
|
||||
/// [`recertify_with_thumbweight`] of a blend already in its own frame, against `bound`,
|
||||
/// its net's distance from its rows taken as at least `shift` more.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn recertify_in_frame(k: &Kernel, pb: &ProceduralBlend, bound: f64, shift: f64) -> Result<f64> {
|
||||
fn recertify_in_frame(
|
||||
k: &Kernel,
|
||||
pb: &ProceduralBlend,
|
||||
bound: f64,
|
||||
shift: f64,
|
||||
) -> Result<(f64, Option<[f64; 2]>)> {
|
||||
let d = pb.definition();
|
||||
let n = d.order as usize;
|
||||
let equal_area = n >= 2 && d.thumbweight == GN_THUMBWEIGHT_EQUAL_AREA;
|
||||
@@ -636,7 +655,8 @@ fn recertify_in_frame(k: &Kernel, pb: &ProceduralBlend, bound: f64, shift: f64)
|
||||
}
|
||||
achieved = achieved.max(total);
|
||||
}
|
||||
Ok(achieved)
|
||||
let [lo, hi] = vary.w;
|
||||
Ok((achieved, (equal_area && lo <= hi).then_some([lo, hi])))
|
||||
}
|
||||
|
||||
/// The stations of a definition at `sigma`, its seeds' knots, where `needed`: each solved
|
||||
@@ -759,20 +779,18 @@ pub(crate) struct DefinitionSpan {
|
||||
pub(crate) stations: Vec<GnStation>,
|
||||
}
|
||||
|
||||
/// The span of a definition's net between its last two stations (`high`) or its first two,
|
||||
/// solved and certified as [`net_of_definition`] does each.
|
||||
/// The span of a definition's net from its station `first` to the next, solved and
|
||||
/// certified as [`net_of_definition`] does each.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Those of [`net_of_definition`].
|
||||
pub(crate) fn end_span_of_definition(
|
||||
pub(crate) fn span_of_definition(
|
||||
k: &Kernel,
|
||||
pb: &ProceduralBlend,
|
||||
high: bool,
|
||||
first: usize,
|
||||
target: f64,
|
||||
) -> Result<DefinitionSpan> {
|
||||
let count = pb.definition().seeds[0].knots(None)?.0.len();
|
||||
let first = if high { count.saturating_sub(2) } else { 0 };
|
||||
let (_, mut spans, certified, ld, stations) = spans_of_definition(k, pb, target, Some(first))?;
|
||||
let (homog, weights) = spans.pop().ok_or_else(|| SolidError::sm_err(OP))?;
|
||||
Ok(DefinitionSpan {
|
||||
|
||||
@@ -458,6 +458,7 @@ pub(crate) fn varying(
|
||||
n,
|
||||
r,
|
||||
cells: GnAreaCells::default(),
|
||||
w: [f64::INFINITY, f64::NEG_INFINITY],
|
||||
};
|
||||
if equal_area {
|
||||
let mut w_lo = f64::INFINITY;
|
||||
|
||||
@@ -117,7 +117,6 @@ use crate::error::{ignore_status, Result, SolidError};
|
||||
use crate::geom::axis2_placement::Axis2Placement;
|
||||
use crate::geom::extent1d::{Extent1d, ExtentNd};
|
||||
use crate::geom::extent2d::Extent2d;
|
||||
use crate::geom::geom_utility::p2::ContinuityType;
|
||||
use crate::geom::global_solver::{SolutionRequested, SolverOperation};
|
||||
use crate::geom::local_solve_nd::{BoundaryHandler, EvalNFunctions, LocalSolveNd};
|
||||
use crate::geom::matrix::Matrix;
|
||||
@@ -2593,7 +2592,14 @@ impl FilletSolver {
|
||||
adj_surface
|
||||
} else {
|
||||
let adj = adj_surface.ok_or_else(nul)?;
|
||||
let new_surface = adj.create_extended_surface(k, extension_dist, ContinuityType::G1)?;
|
||||
let new_surface = adj
|
||||
.extended_sides(
|
||||
k,
|
||||
&crate::surface::side::BoxSide::ALL,
|
||||
crate::surface::extend::Continuity::Tangent,
|
||||
extension_dist,
|
||||
)?
|
||||
.surface;
|
||||
// Copy pNewSurface, when possible as an analytic surface
|
||||
let copy = new_surface.copy_and_add_analytics(k)?;
|
||||
pseudo
|
||||
|
||||
@@ -53,7 +53,7 @@ use crate::error::{Result, SolidError};
|
||||
use crate::kernel::Kernel;
|
||||
use crate::math::Vec3;
|
||||
use crate::nurbs::knots::{KnotVector, Side};
|
||||
use crate::nurbs::math::{CPoint, NOW, NOZ};
|
||||
use crate::nurbs::math::{gamma, CPoint, NOW, NOZ};
|
||||
use crate::nurbs::nurbs_srf::decompose_srf;
|
||||
use crate::nurbs::srf_basic::Surface as Nurb;
|
||||
|
||||
@@ -339,23 +339,38 @@ pub struct CertifiedOffset {
|
||||
|
||||
/// The knots of `G` in one direction: the base's breakpoints `breaks` (an interior one
|
||||
/// of multiplicity `m` in the base's degree `deg` taken `3 - min(deg - m - 1, 2)` times,
|
||||
/// so `G` keeps the normal's continuity) and the inserted `extra` knots (simple).
|
||||
fn offset_knots(breaks: &[f64], mults: &[usize], deg: usize, extra: &[f64]) -> Result<Vec<f64>> {
|
||||
/// so `G` keeps the normal's continuity) and the inserted `extra` knots (simple). An
|
||||
/// interior breakpoint of multiplicity `deg` (the net only `C0` in its parameter) is a
|
||||
/// crease unless `smooth` says its joint was proved tangent continuous ([`joint_meets`]):
|
||||
/// then the normal is `C0` there and `G` takes it `3` times.
|
||||
fn offset_knots(
|
||||
breaks: &[f64],
|
||||
mults: &[usize],
|
||||
smooth: &[bool],
|
||||
deg: usize,
|
||||
extra: &[f64],
|
||||
) -> Result<Vec<f64>> {
|
||||
let (Some(&lo), Some(&hi)) = (breaks.first(), breaks.last()) else {
|
||||
return Err(invalid("no breakpoints"));
|
||||
};
|
||||
let mut interior: Vec<(f64, usize)> = Vec::new();
|
||||
for (&x, &m) in breaks
|
||||
for ((&x, &m), at) in breaks
|
||||
.iter()
|
||||
.skip(1)
|
||||
.take(breaks.len().saturating_sub(2))
|
||||
.zip(mults)
|
||||
.zip(0..)
|
||||
{
|
||||
// the normal is C^(deg - m - 1) there
|
||||
let Some(cont) = deg.checked_sub(m + 1) else {
|
||||
return Err(invalid(
|
||||
"a crease inside the face: its offset is not one surface",
|
||||
));
|
||||
// the normal is C^(deg - m - 1) there; at multiplicity deg C0 where the joint
|
||||
// was proved tangent continuous
|
||||
let cont = match deg.checked_sub(m + 1) {
|
||||
Some(c) => c,
|
||||
None if m == deg && smooth.get(at) == Some(&true) => 0,
|
||||
None => {
|
||||
return Err(invalid(
|
||||
"a crease inside the face: its offset is not one surface",
|
||||
))
|
||||
}
|
||||
};
|
||||
interior.push((x, DEGREE - cont.min(DEGREE - 1)));
|
||||
}
|
||||
@@ -369,6 +384,146 @@ fn offset_knots(breaks: &[f64], mults: &[usize], deg: usize, extra: &[f64]) -> R
|
||||
Ok(k)
|
||||
}
|
||||
|
||||
/// The most pieces [`joint_meets`] splits one joint's edge into.
|
||||
const MAX_JOINT_PIECES: usize = 256;
|
||||
|
||||
/// Whether the joint of `ps` at the interior breakpoint `at` of the u (`along_u`) or v
|
||||
/// breakpoints, a knot of multiplicity the base's degree (where its net is only `C0` in the
|
||||
/// parameter), is tangent continuous closely enough that the offsets of its two sides meet
|
||||
/// there within half the offset's tolerance `tol` at the distance `d`: then its offset is
|
||||
/// one surface, `C0` across the joint, and [`certified_offset`] certifies it as any other
|
||||
/// breakpoint. A knot's multiplicity is not the joint's geometry: an exact extension's joint
|
||||
/// (Natural, Tangent, Ruled) is of multiplicity `p` and tangent continuous.
|
||||
///
|
||||
/// The comparison is of the two one-sided normals along the joint's edge, `n_L` and `n_R`
|
||||
/// (the patches' reduced normals [`reduced_normal`], of the one-sided derivatives across
|
||||
/// the joint and the common one along it), as Bernstein polynomials of the edge's parameter:
|
||||
/// `D = n_L . n_R`, `C = n_L x n_R` and `Y = |n_L|^2 |n_R|^2`. Where every coefficient of
|
||||
/// `D` is positive the angle `theta` between the unit normals is below a right angle, and
|
||||
/// where `|C| <= s sqrt(Y)` on the piece (the norm of `C`'s components' largest coefficients
|
||||
/// against `Y`'s smallest, each with its rounding) `sin theta <= s`, so the offset points'
|
||||
/// gap `|d| |n_L - n_R| = 2 |d| sin(theta / 2) <= sqrt 2 |d| sin theta`; `s = tol / (2 sqrt
|
||||
/// 2 |d|)` holds the gap to `tol / 2`. An edge piece not proved so is halved (an explicit
|
||||
/// stack, at most [`MAX_JOINT_PIECES`] pieces); a piece whose end shows the jump (`D` not
|
||||
/// positive, or `|C| > s sqrt(Y)` there) ends the test: a crease.
|
||||
fn joint_meets(
|
||||
ps: &[Vec<Patch>],
|
||||
along_u: bool,
|
||||
at: usize,
|
||||
d: f64,
|
||||
tol: f64,
|
||||
b: &Binomials,
|
||||
) -> Result<bool> {
|
||||
let before = at
|
||||
.checked_sub(1)
|
||||
.ok_or_else(|| invalid("a joint at the domain's end"))?;
|
||||
let s = tol / (2.0 * core::f64::consts::SQRT_2 * d.abs());
|
||||
if !(s.is_finite() && positive(s)) {
|
||||
return Ok(false);
|
||||
}
|
||||
let pairs: Vec<(&Patch, &Patch)> = if along_u {
|
||||
let (Some(left), Some(right)) = (ps.get(before), ps.get(at)) else {
|
||||
return Err(invalid("no patch row at the joint"));
|
||||
};
|
||||
left.iter().zip(right.iter()).collect()
|
||||
} else {
|
||||
ps.iter()
|
||||
.map(|row| match (row.get(before), row.get(at)) {
|
||||
(Some(l), Some(r)) => Ok((l, r)),
|
||||
_ => Err(invalid("no patch column at the joint")),
|
||||
})
|
||||
.collect::<Result<_>>()?
|
||||
};
|
||||
for (left, right) in pairs {
|
||||
let nl = reduced_normal(left, b)?;
|
||||
let nr = reduced_normal(right, b)?;
|
||||
// the edge: the left patch's high end, the right patch's low end, in the joint's
|
||||
// direction (a polynomial constant in it, of the edge's parameter in the other)
|
||||
let (el, er) = if along_u {
|
||||
(
|
||||
restrict3(&nl, 1.0, 1.0, 0.0, 1.0),
|
||||
restrict3(&nr, 0.0, 0.0, 0.0, 1.0),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
restrict3(&nl, 0.0, 1.0, 1.0, 1.0),
|
||||
restrict3(&nr, 0.0, 1.0, 0.0, 0.0),
|
||||
)
|
||||
};
|
||||
let mut stack = vec![(el, er)];
|
||||
let mut pieces = 1_usize;
|
||||
while let Some((a, c)) = stack.pop() {
|
||||
let dd = dot(&a, &c, b)?;
|
||||
let x = cross(&a, &c, b)?;
|
||||
let y = dot(&a, &a, b)?.mul(&dot(&c, &c, b)?, b)?;
|
||||
// every coefficient of these forms is a weighted mean of products of the normals'
|
||||
// coefficients (the Bernstein product's weights sum to 1): at most 2 M for a
|
||||
// component of the cross product, 3 M for D and 9 M^2 for Y, with M the product of
|
||||
// the two normals' largest coefficients; each is formed in a few dozen operations
|
||||
// and the splits are convex combinations: gamma_64 of that bounds their rounding
|
||||
let m = a
|
||||
.iter()
|
||||
.map(Bernstein2::max_abs_coefficient)
|
||||
.fold(0.0_f64, f64::max)
|
||||
* c.iter()
|
||||
.map(Bernstein2::max_abs_coefficient)
|
||||
.fold(0.0_f64, f64::max);
|
||||
let (rc, rd, ry) = (
|
||||
gamma(64) * 2.0 * m,
|
||||
gamma(64) * 3.0 * m,
|
||||
gamma(64) * 9.0 * m * m,
|
||||
);
|
||||
// |n_L x n_R| over the piece: at most the norm of the components' largest
|
||||
// coefficients (each component within the hull of its coefficients)
|
||||
let cmax = x
|
||||
.iter()
|
||||
.map(|f| f.max_abs_coefficient().powi(2))
|
||||
.sum::<f64>()
|
||||
.sqrt();
|
||||
let floor = |v: f64| (v - ry).max(0.0).sqrt();
|
||||
if dd.min_coefficient() > rd && cmax + rc <= s * floor(y.min_coefficient()) {
|
||||
continue;
|
||||
}
|
||||
// a jump at an end of the piece (its end values are coefficients): a crease, no
|
||||
// split proves it smooth
|
||||
let first = |f: &Bernstein2| f.coefficients().first().copied().unwrap_or(0.0);
|
||||
let last = |f: &Bernstein2| f.coefficients().last().copied().unwrap_or(0.0);
|
||||
let norm = |pick: &dyn Fn(&Bernstein2) -> f64| {
|
||||
x.iter().map(|f| pick(f).powi(2)).sum::<f64>().sqrt()
|
||||
};
|
||||
let creased = |dv: f64, cv: f64, yv: f64| dv <= -rd || cv - rc > s * (yv + ry).sqrt();
|
||||
if creased(first(&dd), norm(&first), first(&y))
|
||||
|| creased(last(&dd), norm(&last), last(&y))
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
pieces = pieces.saturating_add(1);
|
||||
if pieces > MAX_JOINT_PIECES {
|
||||
return Ok(false);
|
||||
}
|
||||
let halves = |f: &Bernstein3| -> (Bernstein3, Bernstein3) {
|
||||
let [x, yy, zz] = f;
|
||||
let sp = |g: &Bernstein2| {
|
||||
if along_u {
|
||||
g.split_v(0.5)
|
||||
} else {
|
||||
g.split_u(0.5)
|
||||
}
|
||||
};
|
||||
let (x0, x1) = sp(x);
|
||||
let (y0, y1) = sp(yy);
|
||||
let (z0, z1) = sp(zz);
|
||||
([x0, y0, z0], [x1, y1, z1])
|
||||
};
|
||||
let (a0, a1) = halves(&a);
|
||||
let (c0, c1) = halves(&c);
|
||||
stack.push((a0, c0));
|
||||
stack.push((a1, c1));
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// The Greville abscissae of the knots `k` of degree [`DEGREE`].
|
||||
fn greville(k: &[f64]) -> Vec<f64> {
|
||||
k.windows(DEGREE + 2)
|
||||
@@ -622,12 +777,28 @@ pub fn certified_offset(k: &Kernel, s: &Nurb, d: f64, tol: f64) -> Result<Certif
|
||||
let (sub, sub_mults) = breakpoints(s.u_knots(), p)?;
|
||||
let (svb, svb_mults) = breakpoints(s.v_knots(), q)?;
|
||||
let (ub, vb, ps) = patches(s)?;
|
||||
// the joints at multiplicity p (q) proved tangent continuous (an exact extension's)
|
||||
let smooth = |mults: &[usize], deg: usize, along_u: bool| -> Result<Vec<bool>> {
|
||||
mults
|
||||
.iter()
|
||||
.zip(1..)
|
||||
.map(|(&m, at)| {
|
||||
if m == deg {
|
||||
joint_meets(&ps, along_u, at, d, tol, &b)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let smooth_u = smooth(&sub_mults, p, true)?;
|
||||
let smooth_v = smooth(&svb_mults, q, false)?;
|
||||
let mut extra_u: Vec<f64> = Vec::new();
|
||||
let mut extra_v: Vec<f64> = Vec::new();
|
||||
for _round in 0..MAX_ROUNDS {
|
||||
k.check_cancelled(OP)?;
|
||||
let ku = offset_knots(&sub, &sub_mults, p, &extra_u)?;
|
||||
let kv = offset_knots(&svb, &svb_mults, q, &extra_v)?;
|
||||
let ku = offset_knots(&sub, &sub_mults, &smooth_u, p, &extra_u)?;
|
||||
let kv = offset_knots(&svb, &svb_mults, &smooth_v, q, &extra_v)?;
|
||||
let gu = greville(&ku);
|
||||
let gv = greville(&kv);
|
||||
let mut grid: Vec<Vec<Vec3>> = Vec::new();
|
||||
@@ -1043,6 +1214,100 @@ mod tests {
|
||||
worst
|
||||
}
|
||||
|
||||
/// `s` (a B-spline net) extended `how` past its u max by `e`: its net, the old end now an
|
||||
/// interior knot of multiplicity `p` (the joint).
|
||||
fn extended_net(s: &Nurb, how: crate::surface::extend::Continuity, e: f64) -> Nurb {
|
||||
use crate::nurbs::b_spline_surface::BSplineSurface;
|
||||
use crate::surface::side::BoxSide;
|
||||
use crate::surface::surface::{Surface, SurfaceKind};
|
||||
let k = Kernel::default();
|
||||
let src = Surface::from(BSplineSurface::from_gw_nurb(s));
|
||||
let dom = src.natural_uv_domain_with_nurb(&k).unwrap();
|
||||
let out = src
|
||||
.extended_by_distance(&k, &dom, BoxSide::UMax, how, e)
|
||||
.unwrap()
|
||||
.surface;
|
||||
let SurfaceKind::BSpline(net) = out.kind() else {
|
||||
panic!("not a B-spline: {out:?}");
|
||||
};
|
||||
net.gw_nurb().unwrap().clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tangent_continuous_extension_joint_at_multiplicity_p_is_no_crease() {
|
||||
use crate::surface::extend::Continuity;
|
||||
let s = parabola(false);
|
||||
for how in [Continuity::Natural, Continuity::Tangent, Continuity::Ruled] {
|
||||
let g = extended_net(&s, how, 6.0);
|
||||
let (p, _) = g.degrees();
|
||||
// the joint: the old end, u = 1, now inside at multiplicity p
|
||||
let at = g.u_knots().iter().filter(|&&x| x == 1.0).count();
|
||||
assert_eq!(at, p, "{how:?}: {:?}", g.u_knots());
|
||||
for d in [2.0, -2.0] {
|
||||
let tol = 1e-7;
|
||||
let made = certified_offset(&Kernel::default(), &g, d, tol).unwrap();
|
||||
assert!(made.bound <= tol, "{how:?} d {d}: bound {}", made.bound);
|
||||
let err = sampled_error(&g, &made.surface, d);
|
||||
assert!(
|
||||
err <= made.bound * 1.000_001 + 1e-12,
|
||||
"{how:?} d {d}: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_kink_at_multiplicity_p_is_a_crease() {
|
||||
// two quadratic spans meeting at (0, 10) at multiplicity 2, their tangents there
|
||||
// (10, 5) and (10, -5): a real crease
|
||||
let pts = [
|
||||
(-10.0, 0.0),
|
||||
(-5.0, 7.5),
|
||||
(0.0, 10.0),
|
||||
(5.0, 7.5),
|
||||
(10.0, 0.0),
|
||||
];
|
||||
let net = pts
|
||||
.iter()
|
||||
.map(|&(x, y)| vec![CPoint::new(x, y, 0.0, NOW), CPoint::new(x, y, 20.0, NOW)])
|
||||
.collect();
|
||||
let s = Nurb::new(
|
||||
net,
|
||||
2,
|
||||
1,
|
||||
vec![0., 0., 0., 0.5, 0.5, 1., 1., 1.],
|
||||
vec![0., 0., 1., 1.],
|
||||
)
|
||||
.unwrap();
|
||||
let err = certified_offset(&Kernel::default(), &s, 2.0, 1e-7).unwrap_err();
|
||||
assert!(
|
||||
matches!(err, SolidError::InvalidInput { reason, .. } if reason.starts_with("a crease")),
|
||||
"{err:?}"
|
||||
);
|
||||
// the same joint made smooth (its neighbours on the tangent through it) is none
|
||||
let pts = [
|
||||
(-10.0, 0.0),
|
||||
(-5.0, 10.0),
|
||||
(0.0, 10.0),
|
||||
(5.0, 10.0),
|
||||
(10.0, 0.0),
|
||||
];
|
||||
let net = pts
|
||||
.iter()
|
||||
.map(|&(x, y)| vec![CPoint::new(x, y, 0.0, NOW), CPoint::new(x, y, 20.0, NOW)])
|
||||
.collect();
|
||||
let s = Nurb::new(
|
||||
net,
|
||||
2,
|
||||
1,
|
||||
vec![0., 0., 0., 0.5, 0.5, 1., 1., 1.],
|
||||
vec![0., 0., 1., 1.],
|
||||
)
|
||||
.unwrap();
|
||||
let made = certified_offset(&Kernel::default(), &s, 2.0, 1e-7).unwrap();
|
||||
assert!(made.bound <= 1e-7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn radius_classes() {
|
||||
assert_eq!(classify_radius(1.0, 1e-6), SheetClass::Regular);
|
||||
|
||||
@@ -83,7 +83,6 @@ use crate::geom::context::{Context, MarkType};
|
||||
use crate::geom::extent1d::Extent1d;
|
||||
use crate::geom::extent2d::Extent2d;
|
||||
use crate::geom::extent3d::Extent3d;
|
||||
use crate::geom::geom_utility::p2::ContinuityType;
|
||||
use crate::geom::global_solver::{SolutionRequested, SolverOperation};
|
||||
use crate::geom::object::{NotifyOperation, ObjectRef};
|
||||
use crate::geom::solution_array::{Solution, SolutionArray};
|
||||
@@ -93,7 +92,7 @@ use crate::heal::stitch::p2 as stitch;
|
||||
use crate::intersect::topology_solver::p1::TopologySolver;
|
||||
use crate::intersect::topology_solver::p2::TopologySolveOptions;
|
||||
use crate::kernel::Kernel;
|
||||
use crate::math::{min2, Vec3, BIG_DOUBLE, EFF_ZERO, EFF_ZERO_SQRT, PI};
|
||||
use crate::math::{Vec3, BIG_DOUBLE, EFF_ZERO, EFF_ZERO_SQRT, PI};
|
||||
use crate::nurbs::b_spline_surface_n_lib::p1::OffsetParamType;
|
||||
use crate::nurbs::b_spline_surface_n_lib::p2::OrientType;
|
||||
use crate::nurbs::knots::Side;
|
||||
@@ -101,6 +100,8 @@ use crate::offset::offset_geometry_creation::{OffsetGeometryCreation, SsiData};
|
||||
|
||||
use super::edgeuse::{CapNotifications, MarkSource};
|
||||
use super::p2::OffsetNotifications;
|
||||
use crate::surface::extend::Continuity;
|
||||
use crate::surface::side::BoxSide;
|
||||
use crate::surface::surface::p4::NoObject;
|
||||
use crate::surface::surface::{Surface, SurfaceKind};
|
||||
use crate::topology::brep::p1::{self as bp1, SetRegionVoidType};
|
||||
@@ -184,222 +185,6 @@ pub struct VertexOffset {
|
||||
pub curve_orientations: Vec<OrientType>,
|
||||
}
|
||||
|
||||
/// Not upstream (WR-532; the fork's tangency-labs/smlib@b283ea2, `sm_ExtendConeAsItself`):
|
||||
/// cone (or cylinder) `surface` extended by `dist` along its rulings and around its axis
|
||||
/// as itself, each direction's end spans carried on (`SM_CT_CINFINITY`), which stays on
|
||||
/// the circle where a tangent extension (`SM_CT_G1R`) leaves it. Around the axis it goes
|
||||
/// no farther than half the turn the cone leaves open on each side, so it never laps
|
||||
/// itself; a closed cone is extended along its rulings only. Around the axis each
|
||||
/// side is extended on its own and each old end made a knot of multiplicity equal to
|
||||
/// the degree again ([`full_knot`]; tangency-labs/smlib@3626d4b).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Those of the extensions and knot insertions.
|
||||
fn extend_cone_as_itself(
|
||||
kernel: &Kernel,
|
||||
surface: &Surface,
|
||||
cone: &crate::surface::cone::Cone,
|
||||
dist: f64,
|
||||
) -> Result<Surface> {
|
||||
use crate::nurbs::b_spline_surface::SurfParamType;
|
||||
let (around, along) = if cone.rev().swap_uv() {
|
||||
(SurfParamType::V, SurfParamType::U)
|
||||
} else {
|
||||
(SurfParamType::U, SurfParamType::V)
|
||||
};
|
||||
let along_srf =
|
||||
surface.create_extended_surface_dir(kernel, along, dist, ContinuityType::CInfinity)?;
|
||||
|
||||
// around: half the open turn on each side, in arc length on the smaller end circle
|
||||
let sweep_rad = cone.rev().step_uv_domain().u_interval().length() * PI / 180.0;
|
||||
let radius = min2(cone.bot_radius(), cone.top_radius());
|
||||
let open = 2.0 * PI - sweep_rad;
|
||||
let around_dist = min2(dist, 0.5 * 0.5 * open * radius);
|
||||
// (`SM_CAST_PTR(SmBSplineSurface, ..)`: the B-spline surface and its subclasses)
|
||||
let is_bspline = !matches!(
|
||||
along_srf.kind(),
|
||||
SurfaceKind::OffsetSurface(_)
|
||||
| SurfaceKind::CurveBounded(_)
|
||||
| SurfaceKind::Step(_)
|
||||
| SurfaceKind::SrfInVolume(_)
|
||||
);
|
||||
if !is_bspline || open <= 0.0 || around_dist <= EFF_ZERO_SQRT * (1.0 + radius) {
|
||||
return Ok(along_srf);
|
||||
}
|
||||
// (tangency-labs/smlib@3626d4b) each side on its own, each old end then a knot of
|
||||
// multiplicity equal to the degree again: SMLib's own form for a circle
|
||||
// (`SmCone::IsNurbSurfaceCone` takes only a triple knot at each end and double knots
|
||||
// inside); both sides in one call extend the first side's extended span again and
|
||||
// leave a single knot at the old end, and a face on that surface is no longer found
|
||||
// to be a cone
|
||||
let around_ivl = if around == SurfParamType::U {
|
||||
along_srf.natural_uv_domain_with_nurb(kernel)?.u_interval()
|
||||
} else {
|
||||
along_srf.natural_uv_domain_with_nurb(kernel)?.v_interval()
|
||||
};
|
||||
let (min_side, max_side) = if around == SurfParamType::U {
|
||||
(SurfParamType::UMin, SurfParamType::UMax)
|
||||
} else {
|
||||
(SurfParamType::VMin, SurfParamType::VMax)
|
||||
};
|
||||
let mut min_srf = along_srf.create_extended_surface_dir(
|
||||
kernel,
|
||||
min_side,
|
||||
around_dist,
|
||||
ContinuityType::CInfinity,
|
||||
)?;
|
||||
full_knot(&mut min_srf, around, around_ivl.min())?;
|
||||
let mut both = min_srf.create_extended_surface_dir(
|
||||
kernel,
|
||||
max_side,
|
||||
around_dist,
|
||||
ContinuityType::CInfinity,
|
||||
)?;
|
||||
full_knot(&mut both, around, around_ivl.max())?;
|
||||
Ok(both)
|
||||
}
|
||||
|
||||
/// Not upstream (WR-728, #2484): ball patch `patch` (the face's surface `surface`)
|
||||
/// extended by `dist` as itself, a ball patch on the same sphere. The Ball's patch is the
|
||||
/// sphere's triangle with one corner, its pole, on a side of the patch collapsed to a point
|
||||
/// (a singularity, as the extension finds it); its iso-curves through the pole are great
|
||||
/// circles, and the third side is a great circle's arc. The patch is carried on with
|
||||
/// `SM_CT_CINFINITY`, the polynomial continuation of its homogeneous net, so the identity
|
||||
/// `|X - C W|^2 = R^2 W^2` that puts it on the sphere holds on the extension too, where a
|
||||
/// tangent extension (`SM_CT_G1R`) leaves the sphere at once. Around the pole (across
|
||||
/// its two sides through the pole) each side goes no farther than a quarter of the turn
|
||||
/// the third side's great circle leaves open, as a cone's turn is bounded
|
||||
/// ([`extend_cone_as_itself`]); away from the pole (across the third side) no farther
|
||||
/// than half the arc left from the third side's farthest point to the pole's antipode,
|
||||
/// where the great circles through the pole meet again. The pole's side is not extended.
|
||||
/// A patch with no singular side (not the Ball's triangle) is extended as its net is, a
|
||||
/// plain B-spline.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Those of the singularity tests, evaluations and extensions, and
|
||||
/// [`SolidError::InvalidInput`] when the extended net has a weight that is not positive.
|
||||
fn extend_ball_patch_as_itself(
|
||||
kernel: &Kernel,
|
||||
surface: &Surface,
|
||||
patch: &crate::surface::ball_patch::BallPatch,
|
||||
dist: f64,
|
||||
) -> Result<Surface> {
|
||||
use crate::math::Point2;
|
||||
use crate::nurbs::b_spline_surface::SurfParamType;
|
||||
let net = patch.net();
|
||||
let dom = net.natural_uv_domain();
|
||||
let (lo, hi) = (dom.min(), dom.max());
|
||||
// the pole's side as the extension finds it (`CreateExtendedSurface`'s singularity
|
||||
// test at the domain's corners): the pole, the third side's ends, the direction round
|
||||
// the pole and the side across the third side
|
||||
let at_lo = surface.is_singularity(kernel, lo, EFF_ZERO, false)?;
|
||||
let at_hi = surface.is_singularity(kernel, hi, EFF_ZERO, false)?;
|
||||
let (pole, ends, around, away) = if at_lo.singular && at_lo.direction == SurfParamType::U {
|
||||
(
|
||||
lo,
|
||||
[Point2::new(lo.x, hi.y), hi],
|
||||
SurfParamType::U,
|
||||
SurfParamType::VMax,
|
||||
)
|
||||
} else if at_lo.singular {
|
||||
(
|
||||
lo,
|
||||
[Point2::new(hi.x, lo.y), hi],
|
||||
SurfParamType::V,
|
||||
SurfParamType::UMax,
|
||||
)
|
||||
} else if at_hi.singular && at_hi.direction == SurfParamType::U {
|
||||
(
|
||||
hi,
|
||||
[lo, Point2::new(hi.x, lo.y)],
|
||||
SurfParamType::U,
|
||||
SurfParamType::VMin,
|
||||
)
|
||||
} else if at_hi.singular {
|
||||
(
|
||||
hi,
|
||||
[lo, Point2::new(lo.x, hi.y)],
|
||||
SurfParamType::V,
|
||||
SurfParamType::UMin,
|
||||
)
|
||||
} else {
|
||||
return Ok(Surface::from(net.create_extended_surface(
|
||||
kernel,
|
||||
dist,
|
||||
ContinuityType::G1R,
|
||||
)?));
|
||||
};
|
||||
let centre = patch.centre();
|
||||
let radius = patch.radius();
|
||||
let [e0, e1] = ends;
|
||||
let p = net.evaluate_point(pole)? - centre;
|
||||
let a = net.evaluate_point(e0)? - centre;
|
||||
let b = net.evaluate_point(e1)? - centre;
|
||||
let angle = |x: Vec3, y: Vec3| x.cross(y).length().atan2(x.dot(y));
|
||||
// the third side's farthest point from the pole: the foot of the pole's antipode on
|
||||
// the great circle's plane, where it is on the arc, else an end
|
||||
let m = a.cross(b);
|
||||
let foot = m * m.dot(p) - p * m.dot(m);
|
||||
let mut farthest = angle(p, a).max(angle(p, b));
|
||||
if foot != Vec3::new(0.0, 0.0, 0.0)
|
||||
&& a.cross(foot).dot(m) >= 0.0
|
||||
&& foot.cross(b).dot(m) >= 0.0
|
||||
{
|
||||
farthest = farthest.max(angle(p, foot));
|
||||
}
|
||||
let around_dist = min2(dist, 0.25 * (2.0 * PI - angle(a, b)) * radius);
|
||||
let away_dist = min2(dist, 0.5 * (PI - farthest) * radius);
|
||||
let round =
|
||||
net.create_extended_surface_dir(kernel, around, around_dist, ContinuityType::CInfinity)?;
|
||||
let both =
|
||||
round.create_extended_surface_dir(kernel, away, away_dist, ContinuityType::CInfinity)?;
|
||||
Ok(Surface::from(
|
||||
crate::surface::ball_patch::BallPatch::create(&both, centre, radius)?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Not upstream (WR-532; tangency-labs/smlib@3626d4b): makes the knot of B-spline
|
||||
/// `surface` at `at` in direction `dir` of multiplicity equal to the degree
|
||||
/// (`InsertOneKnot` of the missing count; a knot within `EFF_ZERO` of the interval's
|
||||
/// length counts as the same).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `SM_ERR_NULL_POINTER` for a surface that is not a B-spline surface (upstream's
|
||||
/// `NER` of the cast); those of the knot query and insertion.
|
||||
fn full_knot(
|
||||
surface: &mut Surface,
|
||||
dir: crate::nurbs::b_spline_surface::SurfParamType,
|
||||
at: f64,
|
||||
) -> Result<()> {
|
||||
use crate::nurbs::b_spline_surface::SurfParamType;
|
||||
const OP: &str = "offset::offset_executive::p1::extend_cone_as_itself";
|
||||
let SurfaceKind::BSpline(bs) = surface.kind_mut() else {
|
||||
return Err(SolidError::null_pointer(OP));
|
||||
};
|
||||
let (knots, mults) = bs.knots(dir, None)?;
|
||||
let domain = bs.natural_uv_domain();
|
||||
let ivl = if dir == SurfParamType::U {
|
||||
domain.u_interval()
|
||||
} else {
|
||||
domain.v_interval()
|
||||
};
|
||||
let same = EFF_ZERO * (1.0 + ivl.length());
|
||||
let mut have = 0;
|
||||
for (&knot, &mult) in knots.iter().zip(&mults) {
|
||||
if (knot - at).abs() <= same {
|
||||
have = mult;
|
||||
}
|
||||
}
|
||||
let degree = bs.degree(dir);
|
||||
if have < degree {
|
||||
bs.insert_one_knot(at, degree - have, dir)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The 27-bit object namespace under which the lamina clean-up's `DeleteFaces`
|
||||
/// (merge p4's arena adapter) records its notifications; distinct from the Boolean
|
||||
/// kernels' namespaces, which share no attribute store with an offset executive.
|
||||
@@ -836,8 +621,17 @@ impl OffsetExecutive {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Returns a cached extension, creating it with distance `offset_distance * 5.0`
|
||||
/// and G1-ruled continuity. A hit never re-extends or replaces the cached surface.
|
||||
/// Returns a cached extension, creating it with distance `offset_distance * 5.0`, every
|
||||
/// side exactly ruled ([`Surface::extended_sides_keeping_parameters`], [`Continuity::Ruled`]): a plane, cone,
|
||||
/// sphere, torus or ball patch as itself, a procedural blend by its definition, an
|
||||
/// offset surface as the offset of its base extended. A hit never re-extends or replaces
|
||||
/// the cached surface.
|
||||
///
|
||||
/// Not upstream: upstream's `CreateExtendedSurface(.., SM_CT_G1R, ..)` fits a ruled
|
||||
/// extension and removes knots within a tolerance (the WR-532 / WR-728 opt-ins
|
||||
/// `offset_extends_cones_as_themselves` and `offset_extends_ball_patches_as_themselves`
|
||||
/// extended cones and ball patches as themselves; the exact path always does, so the
|
||||
/// flags no longer change this).
|
||||
///
|
||||
/// `base_key` represents upstream pointer identity, including across Rust clones.
|
||||
/// Distinct source identities must use distinct keys. Failed extensions are not cached.
|
||||
@@ -853,19 +647,14 @@ impl OffsetExecutive {
|
||||
// The obsolete offset-specific `else` at cpp:2158 is unreachable:
|
||||
// NER(pBase) already established pOrigSurf == pBase != NULL.
|
||||
let dist = self.offset_distance * 5.0;
|
||||
let extension = match base.kind() {
|
||||
// Not upstream (WR-532), opt-in `offset_extends_cones_as_themselves`
|
||||
SurfaceKind::Cone(cone) if kernel.fixes.offset_extends_cones_as_themselves => {
|
||||
extend_cone_as_itself(kernel, base, cone, dist)?
|
||||
}
|
||||
// Not upstream (WR-728), opt-in `offset_extends_ball_patches_as_themselves`
|
||||
SurfaceKind::BallPatch(patch)
|
||||
if kernel.fixes.offset_extends_ball_patches_as_themselves =>
|
||||
{
|
||||
extend_ball_patch_as_itself(kernel, base, patch, dist)?
|
||||
}
|
||||
_ => base.create_extended_surface(kernel, dist, ContinuityType::G1R)?,
|
||||
};
|
||||
let extension = base
|
||||
.extended_sides_keeping_parameters(
|
||||
kernel,
|
||||
&BoxSide::ALL,
|
||||
Continuity::Ruled,
|
||||
dist,
|
||||
)?
|
||||
.surface;
|
||||
Ok(entry.insert(extension))
|
||||
}
|
||||
}
|
||||
@@ -3979,8 +3768,14 @@ mod tests {
|
||||
let mut executive = executive();
|
||||
let base = plane(0.0);
|
||||
let expected = base
|
||||
.create_extended_surface(&kernel, 0.125 * 5.0, ContinuityType::G1R)
|
||||
.unwrap();
|
||||
.extended_sides_keeping_parameters(
|
||||
&kernel,
|
||||
&BoxSide::ALL,
|
||||
Continuity::Ruled,
|
||||
0.125 * 5.0,
|
||||
)
|
||||
.unwrap()
|
||||
.surface;
|
||||
let first = executive
|
||||
.get_create_extended_surface(&kernel, 41, &base)
|
||||
.unwrap();
|
||||
|
||||
@@ -789,6 +789,12 @@ struct Local {
|
||||
/// A bound on how far each extension point's homogeneous coordinates are from the exact
|
||||
/// construction's (0 for the source's own points, which are kept bit for bit).
|
||||
err: f64,
|
||||
/// The degree across the rows (the other direction's): `q + 1` consecutive rows make a
|
||||
/// point of the surface.
|
||||
q: usize,
|
||||
/// The 3D bound of a construction made in a local frame ([`Self::natural`]), which
|
||||
/// [`Self::bound`] returns in place of the one from [`Self::err`].
|
||||
frame: Option<f64>,
|
||||
}
|
||||
|
||||
fn op_err() -> SolidError {
|
||||
@@ -806,6 +812,7 @@ impl Local {
|
||||
[c.x, c.y, c.z, c.w]
|
||||
}
|
||||
};
|
||||
let q = if side.is_u() { net.q() } else { net.p() };
|
||||
let (rows, knots, p): (Vec<Vec<H>>, &[f64], usize) = if side.is_u() {
|
||||
let cols = cpts.first().map_or(0, Vec::len);
|
||||
let rows = (0..cols)
|
||||
@@ -822,6 +829,8 @@ impl Local {
|
||||
p,
|
||||
rational,
|
||||
err: 0.0,
|
||||
q,
|
||||
frame: None,
|
||||
};
|
||||
if !side.is_high() {
|
||||
l.reverse();
|
||||
@@ -956,6 +965,8 @@ impl Local {
|
||||
p: self.p,
|
||||
rational: self.rational,
|
||||
err,
|
||||
q: self.q,
|
||||
frame: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -977,6 +988,9 @@ impl Local {
|
||||
/// from the homogeneous bound [`Self::err`]: `S = sum N H / sum N w`, so a change of at
|
||||
/// most `err` in each coordinate moves it at most `err (sqrt 3 + |S|) / w_min`.
|
||||
fn bound(&self) -> f64 {
|
||||
if let Some(b) = self.frame {
|
||||
return b;
|
||||
}
|
||||
if self.err == 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
@@ -1056,23 +1070,64 @@ impl Local {
|
||||
p: self.p,
|
||||
rational: self.rational,
|
||||
err: self.err,
|
||||
q: self.q,
|
||||
frame: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Natural: the last span's Bezier points (from a copy with its start knot at multiplicity
|
||||
/// `p`) continued past `b` by de Casteljau at `t = 1 + e / (b - c)`: the points
|
||||
/// `blossom(1^(p-i), t^i)` are the polynomial's Bezier points over `[b, b + e]`.
|
||||
///
|
||||
/// In a local frame: each row is translated by its own origin `O_j` (its end point), `H'
|
||||
/// = H - w O_j` (the weights unchanged), continued, and its new points translated back,
|
||||
/// `H = H' + w O_j`; the source's rows are kept as they are. Translation commutes with the
|
||||
/// continuation (both are linear in the homogeneous points and keep the weights), so the
|
||||
/// construction is the same; only its rounding changes. The continuation amplifies its
|
||||
/// input's and its own rounding by `a^p` (`a = |1 - t| + |t|`), which is now relative to
|
||||
/// the rows' local size, not to their absolute coordinates; the translations round once
|
||||
/// each, the way back not amplified. The 3D bound ([`Self::bound`]) is then that of the
|
||||
/// local construction ([`Self::frame`]).
|
||||
fn natural(&self, e: f64) -> Result<Self> {
|
||||
let b = self.end()?;
|
||||
let c = self.before_end(0)?;
|
||||
let p = self.p;
|
||||
let span = if c > self.start()? {
|
||||
self.with_knot(c)?
|
||||
// each row's origin: its end point (any point would do: it is exact by choice)
|
||||
let origins: Vec<[f64; 3]> = self
|
||||
.rows
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let x = r.last().ok_or_else(op_err)?;
|
||||
let w = if self.rational { x[3] } else { 1.0 };
|
||||
if !(w.is_finite() && w > 0.0) {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP,
|
||||
"a weight that is not positive",
|
||||
));
|
||||
}
|
||||
Ok([x[0] / w, x[1] / w, x[2] / w])
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
// the rows translated: one product and one difference a coordinate
|
||||
let mut local = self.clone();
|
||||
let mut moved = 0.0_f64;
|
||||
for (r, o) in local.rows.iter_mut().zip(&origins) {
|
||||
for x in r.iter_mut() {
|
||||
let w = x[3];
|
||||
for (v, &oc) in x.iter_mut().zip(o) {
|
||||
moved = moved.max(v.abs() + (w * oc).abs());
|
||||
*v -= w * oc;
|
||||
}
|
||||
}
|
||||
}
|
||||
local.err = self.err + gamma(2) * moved;
|
||||
let span = if c > local.start()? {
|
||||
local.with_knot(c)?
|
||||
} else {
|
||||
self.clone()
|
||||
local.clone()
|
||||
};
|
||||
let t = 1.0 + e / (b - c);
|
||||
let ext: Vec<Vec<H>> = span
|
||||
let ext_local: Vec<Vec<H>> = span
|
||||
.rows
|
||||
.iter()
|
||||
.map(|r| {
|
||||
@@ -1091,15 +1146,63 @@ impl Local {
|
||||
out
|
||||
})
|
||||
.collect();
|
||||
let knots = vec![b + e; p + 1];
|
||||
// de Casteljau past the span: each level `(1 - t) x + t y` scales the errors and the
|
||||
// points by `a = |1 - t| + |t|` and rounds by gamma_3 of that, over p levels (the
|
||||
// rounding of t itself only reparametrises the continuation, its points stay on it)
|
||||
let a = (1.0 - t).abs() + t.abs();
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
|
||||
let amp = a.powi(p as i32);
|
||||
let err_local = amp * (span.err + gamma(3 * p + 3) * span.size());
|
||||
// the local frame's extent: its points' largest l1 size over their weight, the
|
||||
// smallest weight (the source's rows and the new ones), and the new rows translated
|
||||
// back, each coordinate one product and one sum
|
||||
let (mut wmin, mut pmax, mut back) = (f64::INFINITY, 0.0_f64, 0.0_f64);
|
||||
for x in local
|
||||
.rows
|
||||
.iter()
|
||||
.flatten()
|
||||
.chain(ext_local.iter().flatten())
|
||||
{
|
||||
let w = if self.rational { x[3] } else { 1.0 };
|
||||
wmin = wmin.min(w);
|
||||
pmax = pmax.max((x[0].abs() + x[1].abs() + x[2].abs()) / w.abs());
|
||||
}
|
||||
let ext: Vec<Vec<H>> = ext_local
|
||||
.into_iter()
|
||||
.zip(&origins)
|
||||
.map(|(r, o)| {
|
||||
r.into_iter()
|
||||
.map(|mut x| {
|
||||
let w = x[3];
|
||||
for (v, &oc) in x.iter_mut().zip(o) {
|
||||
back = back.max(v.abs() + (w * oc).abs());
|
||||
*v += w * oc;
|
||||
}
|
||||
x
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
// rows of different origins mix at a point: the q + 1 rows of a span across, so the
|
||||
// largest l1 distance between two origins within q + 1 consecutive rows
|
||||
let mut diam = 0.0_f64;
|
||||
for (i, oi) in origins.iter().enumerate() {
|
||||
for oj in origins.iter().skip(i + 1).take(self.q) {
|
||||
let dist = (oi[0] - oj[0]).abs() + (oi[1] - oj[1]).abs() + (oi[2] - oj[2]).abs();
|
||||
diam = diam.max(dist);
|
||||
}
|
||||
}
|
||||
let knots = vec![b + e; p + 1];
|
||||
let mut out = self.joined(ext, &knots)?;
|
||||
out.err = amp * (span.err + gamma(3 * p + 3) * span.size());
|
||||
out.err = err_local;
|
||||
// `S = sum N (H' + w O_j) / sum N w`: an error of at most `err_local` in each local
|
||||
// coordinate and weight moves it at most `err_local (sqrt 3 + |S - O_j| ...)`, with
|
||||
// `|S - O_j| <= pmax + diam` for every row j at the point; the way back's rounding
|
||||
// `gamma_2 back` in each coordinate moves it at most `sqrt 3` of that over the weight
|
||||
out.frame = Some(
|
||||
(err_local * (2.0 + pmax + diam) * (1.0 + gamma(4)) + 2.0 * gamma(2) * back) / wmin
|
||||
* (1.0 + gamma(8)),
|
||||
);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
@@ -1331,6 +1434,8 @@ fn keeps(net: &Net, out: &Net, side: BoxSide) -> Result<bool> {
|
||||
Ok(points_kept && a.len() <= b.len())
|
||||
}
|
||||
|
||||
mod analytic;
|
||||
mod ball;
|
||||
mod blend;
|
||||
mod sides;
|
||||
pub use sides::{ExtendedSides, SideExtension};
|
||||
|
||||
@@ -0,0 +1,596 @@
|
||||
//! A plane, cone, sphere or torus extended past the sides of its box by its own NURBS net
|
||||
//! continued Natural ([`Surface::extended_sides`] on an analytic kind), still on its analytic
|
||||
//! surface.
|
||||
//!
|
||||
//! Not upstream: upstream extends an analytic's NURB as any B-spline's. Here (owner,
|
||||
//! 2026-11-04, the ball patch's rule) the analytic's OWN net is continued Natural past each
|
||||
//! side (the homogeneous polynomial of its end span carried on), so the source's net, knots
|
||||
//! and parameters stay bit for bit (a straight direction's end points to rounding, below): a face keeps its UV trims on the result (the shell's
|
||||
//! extended offset replaces the face's surface with UV trim creation off). Widening the
|
||||
//! analytic itself ([`Surface::continued`]) rebuilds its canonical form over the wider STEP
|
||||
//! box, which moves every kept UV trim. The implicit equation composed with the net is a
|
||||
//! polynomial identity, so it holds on its continuation: the result is certified on the
|
||||
//! analytic on its net ([`lies_on`]), within its rounding. It is a B-spline, carrying the
|
||||
//! source's kept exact round where it has one (#2487); an offset surface of an analytic is
|
||||
//! the offset of its base so extended.
|
||||
//!
|
||||
//! A plane follows the same rule for a caller keeping its trims (owner, 2026-11-04;
|
||||
//! [`Surface::extended_sides_keeping_parameters`]): its bilinear net continued, a B-spline
|
||||
//! whose parameters over the source's box are the plane's own bit for bit, where widening the
|
||||
//! plane ([`Surface::continued`]) handed back a plane kind the shell's offset takes other
|
||||
//! branches for. Its form is straight both ways, so every continuity is its continuation.
|
||||
//!
|
||||
//! **Straight directions.** Across a side where the form is of degree 1 (a plane either way,
|
||||
//! a cone along its rulings) the continuation is the end span's own line, so its joint knot
|
||||
//! is removed exactly and the span is widened (`joint_removed`): the result is again one
|
||||
//! straight span there, as the kind's canonical form is and its recognisers read it (a cone's
|
||||
//! form must have its two end circles only), on the same parameters. The source's end control
|
||||
//! points are then the widened span's points at the old ends, to their rounding (the plane's
|
||||
//! corners, a ruling's ends); its other points, its knots and its parameters stay bit for
|
||||
//! bit.
|
||||
//!
|
||||
//! A side is left where the analytic closes in its direction (a full turn), where the side
|
||||
//! is not proved longer than a point (a pole or an apex), or a plane's side at
|
||||
//! `+-INFINITE_PARAMETER`, as [`Surface::extended_sides`] leaves them; where no side is
|
||||
//! extended the result is the source itself. Every other side asked is extended `distance` across it, capped (said so,
|
||||
//! [`SideExtension::UpToLimit`]):
|
||||
//! - **round the axis**, no farther than a quarter of the turn the source leaves open (the
|
||||
//! shell's rule for a round, WR-532), so it never laps itself;
|
||||
//! - **along the generator**, never past the cone's apex or the sphere's pole, and never past
|
||||
//! the torus's full turn of its tube.
|
||||
//!
|
||||
//! **The distance** across a side is measured at the side's middle on the iso across it, in
|
||||
//! closed form on the analytic: the iso is certified (on its homogeneous control points, within
|
||||
//! their rounding) in a plane normal to the axis (round it: a parallel, a circle about the
|
||||
//! axis), in a plane through the axis (a sphere's meridian, a great circle; a torus's tube, the
|
||||
//! circle of the minor radius about its centre), or straight (a cone's ruling or a plane's
|
||||
//! iso, of degree 1).
|
||||
//! Its length is the circle's radius times the angle swept from the side through the
|
||||
//! extension's middle point to its end, or the ruling's chord. The extension parameter is
|
||||
//! solved for the distance (or the cap) by bisection on that closed form, met to `1e-12` of it.
|
||||
//!
|
||||
//! **Weights.** An extension whose weights are not all positive is refused
|
||||
//! ([`ExtendRefusal::WeightVanishes`]) naming the longest distance found whose weights are.
|
||||
//!
|
||||
//! Deviations: none (no upstream counterpart). No recursion.
|
||||
|
||||
use super::ball::{knot_box, reach};
|
||||
use super::sides::{round_the_axis, ExtendedSides, SideExtension};
|
||||
use super::{keeps, Continuity, ExtendRefusal, Local};
|
||||
use crate::error::{Result, SolidError};
|
||||
use crate::geom::vector2d::Point2;
|
||||
use crate::geom::vector3d::Vec3;
|
||||
use crate::kernel::Kernel;
|
||||
use crate::math::INFINITE_PARAMETER;
|
||||
use crate::nurbs::b_spline_surface::{BSplineSurface, SurfParamType};
|
||||
use crate::nurbs::math::{gamma, NOW};
|
||||
use crate::nurbs::srf_basic::{Direction, Surface as Net};
|
||||
use crate::nurbs::srf_tool::p1::extract_iso_crv;
|
||||
use crate::surface::implicit::{lies_on, AnalyticSupport, Implicit};
|
||||
use crate::surface::offset_surface::{OffsetSurface, SS_UNKNOWN};
|
||||
use crate::surface::side::{Across, BoxSide, Bracket};
|
||||
use crate::surface::surface::{Surface, SurfaceKind};
|
||||
use std::f64::consts::PI;
|
||||
|
||||
const OP: &str = "surface::extend::analytic::analytic_extended_sides";
|
||||
|
||||
/// A full turn, radians.
|
||||
const TURN: f64 = 2.0 * PI;
|
||||
|
||||
fn refused(refusal: ExtendRefusal, distance: Option<f64>) -> SolidError {
|
||||
SolidError::ExtendRefused {
|
||||
operation: OP,
|
||||
refusal,
|
||||
distance,
|
||||
}
|
||||
}
|
||||
|
||||
/// The iso across a side, by the analytic's closed form.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Iso {
|
||||
/// A circle about the axis, in a plane normal to it.
|
||||
Parallel,
|
||||
/// A sphere's great circle through its poles, of the sphere's radius.
|
||||
Meridian { radius: f64 },
|
||||
/// A torus's tube circle, of its minor radius about the point at its major radius.
|
||||
Tube { major: f64, minor: f64 },
|
||||
/// A cone's straight ruling.
|
||||
Ruling,
|
||||
}
|
||||
|
||||
/// The analytic's axis and the data its caps need.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Frame {
|
||||
/// A point of the axis (the sphere's or torus's centre).
|
||||
origin: Vec3,
|
||||
/// The unit axis.
|
||||
axis: Vec3,
|
||||
/// A cone's apex, if it has one (not a cylinder).
|
||||
apex: Option<Vec3>,
|
||||
/// The points' absolute size, for the rounding.
|
||||
size: f64,
|
||||
/// A quarter of the turn the source leaves open round the axis, radians.
|
||||
quarter: f64,
|
||||
/// The turn a torus's tube leaves open, radians (0 for the other kinds).
|
||||
tube_left: f64,
|
||||
}
|
||||
|
||||
/// `s`, an analytic kind (cone, sphere, torus; a plane with `planes`) or an offset surface of one, extended past
|
||||
/// the sides `asked` of its natural box by `distance` (module docs); `None` for any other
|
||||
/// kind.
|
||||
pub(super) fn analytic_extended_sides(
|
||||
k: &Kernel,
|
||||
s: &Surface,
|
||||
asked: &[BoxSide],
|
||||
distance: f64,
|
||||
planes: bool,
|
||||
) -> Result<Option<ExtendedSides>> {
|
||||
let mut offsets = Vec::new();
|
||||
let mut carrier = s;
|
||||
while let SurfaceKind::OffsetSurface(o) = carrier.kind() {
|
||||
offsets.push(o.offset_distance());
|
||||
carrier = o.base_surface();
|
||||
}
|
||||
let plane = matches!(carrier.kind(), SurfaceKind::Plane(_));
|
||||
if !(plane && planes
|
||||
|| matches!(
|
||||
carrier.kind(),
|
||||
SurfaceKind::Cone(_) | SurfaceKind::Sphere(_) | SurfaceKind::Torus(_)
|
||||
))
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
if !(distance.is_finite() && distance > 0.0) {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP,
|
||||
"the distance is not finite and positive",
|
||||
));
|
||||
}
|
||||
// the form proved on its label once; its continuation is certified at the end
|
||||
let support = AnalyticSupport::check(k, carrier)?;
|
||||
let implicit = support.implicit();
|
||||
let form = crate::blend::fillet_section::station::as_b_spline(k, carrier)?
|
||||
.ok_or_else(|| SolidError::sm_err(OP))?
|
||||
.into_owned();
|
||||
let step = carrier.step_uv_domain();
|
||||
let open = TURN - step.u_interval().length().to_radians();
|
||||
let (frame, along) = match implicit {
|
||||
Implicit::Cone {
|
||||
origin,
|
||||
axis,
|
||||
radius,
|
||||
tan_angle,
|
||||
} => (
|
||||
Frame {
|
||||
origin,
|
||||
axis,
|
||||
apex: (tan_angle != 0.0).then(|| origin + axis * (-radius / tan_angle)),
|
||||
size: 0.0,
|
||||
quarter: 0.25 * open,
|
||||
tube_left: 0.0,
|
||||
},
|
||||
Iso::Ruling,
|
||||
),
|
||||
Implicit::Sphere { centre, radius } => {
|
||||
let AnalyticSupport::Sphere { placement, .. } = support else {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
};
|
||||
(
|
||||
Frame {
|
||||
origin: centre,
|
||||
axis: placement.x_axis().cross(placement.y_axis()),
|
||||
apex: None,
|
||||
size: radius,
|
||||
quarter: 0.25 * open,
|
||||
tube_left: 0.0,
|
||||
},
|
||||
Iso::Meridian { radius },
|
||||
)
|
||||
}
|
||||
Implicit::Torus {
|
||||
centre,
|
||||
axis,
|
||||
major,
|
||||
minor,
|
||||
} => (
|
||||
Frame {
|
||||
origin: centre,
|
||||
axis,
|
||||
apex: None,
|
||||
size: major + minor,
|
||||
quarter: 0.25 * open,
|
||||
tube_left: TURN - step.v_interval().length().to_radians(),
|
||||
},
|
||||
Iso::Tube { major, minor },
|
||||
),
|
||||
// straight both ways (its form is bilinear), never capped
|
||||
Implicit::Plane { origin, normal } => (
|
||||
Frame {
|
||||
origin,
|
||||
axis: normal,
|
||||
apex: None,
|
||||
size: 0.0,
|
||||
quarter: 0.0,
|
||||
tube_left: 0.0,
|
||||
},
|
||||
Iso::Ruling,
|
||||
),
|
||||
};
|
||||
// the size of the net's points from the origin, for the rounding of the closed forms
|
||||
let net0 = form.gw_nurb().ok_or_else(|| SolidError::sm_err(OP))?;
|
||||
let mut size = frame.size;
|
||||
for c in net0.cpts().iter().flatten() {
|
||||
let w = if c.w == NOW { 1.0 } else { c.w };
|
||||
size = size.max(c.x.abs().max(c.y.abs()).max(c.z.abs()) / w.abs());
|
||||
}
|
||||
let o = frame.origin;
|
||||
let frame = Frame {
|
||||
size: size + o.x.abs().max(o.y.abs()).max(o.z.abs()),
|
||||
..frame
|
||||
};
|
||||
let dom_src = carrier.natural_uv_domain_with_nurb(k)?;
|
||||
let mut cur = form;
|
||||
let mut sides = [SideExtension::NotAsked; 4];
|
||||
let mut extension = 0.0_f64;
|
||||
for (&side, done) in BoxSide::ALL.iter().zip(sides.iter_mut()) {
|
||||
if !asked.contains(&side) {
|
||||
continue;
|
||||
}
|
||||
let param = if side.is_u() {
|
||||
SurfParamType::U
|
||||
} else {
|
||||
SurfParamType::V
|
||||
};
|
||||
if carrier.is_closed(k, &dom_src, param, None, false)?.0 {
|
||||
*done = SideExtension::Closed;
|
||||
continue;
|
||||
}
|
||||
if carrier.side_diameter(k, &dom_src, side)?.lower <= 0.0 {
|
||||
*done = SideExtension::Collapsed;
|
||||
continue;
|
||||
}
|
||||
if matches!(implicit, Implicit::Plane { .. })
|
||||
&& side.at(&dom_src).abs() >= INFINITE_PARAMETER
|
||||
{
|
||||
*done = SideExtension::Unbounded;
|
||||
continue;
|
||||
}
|
||||
let iso = if round_the_axis(carrier, side) == Some(true) {
|
||||
if open <= 0.0 {
|
||||
*done = SideExtension::Closed;
|
||||
continue;
|
||||
}
|
||||
Iso::Parallel
|
||||
} else {
|
||||
along
|
||||
};
|
||||
let (grown, across, capped, bound) = extend_side(&cur, side, &frame, iso, distance)?;
|
||||
cur = grown;
|
||||
extension = extension.max(bound);
|
||||
*done = if capped {
|
||||
SideExtension::UpToLimit(across)
|
||||
} else {
|
||||
SideExtension::Extended(across)
|
||||
};
|
||||
}
|
||||
// nothing extended: the source itself, as [`Surface::extended_sides`] leaves it
|
||||
if !sides
|
||||
.iter()
|
||||
.any(|x| matches!(x, SideExtension::Extended(_) | SideExtension::UpToLimit(_)))
|
||||
{
|
||||
return Ok(Some(ExtendedSides {
|
||||
surface: s.copy(k)?,
|
||||
sides,
|
||||
extension,
|
||||
}));
|
||||
}
|
||||
if !lies_on(&cur, &implicit)? {
|
||||
return Err(refused(ExtendRefusal::NotExact, None));
|
||||
}
|
||||
let mut surface = carrier.carry_round_on_continuation(Surface::from(cur));
|
||||
for &d in offsets.iter().rev() {
|
||||
surface = Surface::from(OffsetSurface::new(k, d, surface, true, SS_UNKNOWN)?);
|
||||
}
|
||||
Ok(Some(ExtendedSides {
|
||||
surface,
|
||||
sides,
|
||||
extension,
|
||||
}))
|
||||
}
|
||||
|
||||
/// The unit vector along `v`, refused where it has no direction.
|
||||
fn unit(v: Vec3) -> Result<Vec3> {
|
||||
let l = v.length();
|
||||
if l.is_nan() || l <= 0.0 {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
}
|
||||
Ok(v * (1.0 / l))
|
||||
}
|
||||
|
||||
/// `x`'s part normal to the axis.
|
||||
fn radial(f: &Frame, x: Vec3) -> Vec3 {
|
||||
let d = x - f.origin;
|
||||
d - f.axis * f.axis.dot(d)
|
||||
}
|
||||
|
||||
/// The angle between `a` and `b`, in radians.
|
||||
fn angle(a: Vec3, b: Vec3) -> f64 {
|
||||
a.cross(b).length().atan2(a.dot(b))
|
||||
}
|
||||
|
||||
/// The circle an iso runs (its centre, its plane's normal and its radius) through `x0`;
|
||||
/// `None` for a ruling.
|
||||
fn circle(f: &Frame, iso: Iso, x0: Vec3) -> Result<Option<(Vec3, Vec3, f64)>> {
|
||||
let d = x0 - f.origin;
|
||||
Ok(match iso {
|
||||
Iso::Parallel => {
|
||||
let centre = f.origin + f.axis * f.axis.dot(d);
|
||||
Some((centre, f.axis, (x0 - centre).length()))
|
||||
}
|
||||
Iso::Meridian { radius } => Some((f.origin, unit(f.axis.cross(d))?, radius)),
|
||||
Iso::Tube { major, minor } => {
|
||||
let centre = f.origin + unit(radial(f, x0))? * major;
|
||||
Some((centre, unit(f.axis.cross(d))?, minor))
|
||||
}
|
||||
Iso::Ruling => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The length along the iso from `x0` through `xm` to `x1`, in closed form.
|
||||
fn length(f: &Frame, iso: Iso, x0: Vec3, xm: Vec3, x1: Vec3) -> Result<f64> {
|
||||
let Some((centre, n, radius)) = circle(f, iso, x0)? else {
|
||||
return Ok((x1 - x0).length());
|
||||
};
|
||||
// the swept angle, the way round through the middle point
|
||||
let n = if (xm - x0).cross(x1 - xm).dot(n) < 0.0 {
|
||||
n * -1.0
|
||||
} else {
|
||||
n
|
||||
};
|
||||
let (u0, u1) = (x0 - centre, x1 - centre);
|
||||
let mut phi = u0.cross(u1).dot(n).atan2(u0.dot(u1));
|
||||
if phi < 0.0 {
|
||||
phi += TURN;
|
||||
}
|
||||
Ok(radius * phi)
|
||||
}
|
||||
|
||||
/// `cur` continued Natural past `side` by `distance` across it, capped (module docs): the
|
||||
/// result, its across length, whether the cap stopped it, and its construction bound.
|
||||
fn extend_side(
|
||||
cur: &BSplineSurface,
|
||||
side: BoxSide,
|
||||
f: &Frame,
|
||||
iso: Iso,
|
||||
distance: f64,
|
||||
) -> Result<(BSplineSurface, Across, bool, f64)> {
|
||||
let net = cur.gw_nurb().ok_or_else(|| SolidError::sm_err(OP))?;
|
||||
if matches!(iso, Iso::Ruling) && (if side.is_u() { net.p() } else { net.q() }) != 1 {
|
||||
return Err(SolidError::Unsupported {
|
||||
operation: OP,
|
||||
reason: "a cone's form is not straight along its rulings",
|
||||
});
|
||||
}
|
||||
let source = Local::of(net, side)?;
|
||||
let dom = knot_box(cur)?;
|
||||
let straight = (if side.is_u() { net.p() } else { net.q() }) == 1;
|
||||
let middle = side.point(&dom, 0.5);
|
||||
let made = |e: f64| -> Result<Option<(BSplineSurface, f64)>> {
|
||||
let Some(g) = source.extended(Continuity::Natural, e)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
if !g.is_finite() {
|
||||
return Ok(None);
|
||||
}
|
||||
let bound = g.bound();
|
||||
let out = g.surface(net, side)?;
|
||||
if !keeps(net, &out, side)? {
|
||||
return Err(SolidError::port_invariant(
|
||||
OP,
|
||||
"the extension moved the source's net",
|
||||
));
|
||||
}
|
||||
let out = if straight {
|
||||
joint_removed(&out, side, side.at(&dom))?
|
||||
} else {
|
||||
out
|
||||
};
|
||||
Ok(Some((BSplineSurface::from_gw_nurb(&out), bound)))
|
||||
};
|
||||
// the iso across the side at its middle, from the side (0) to the extension's end (1)
|
||||
let from = side.at(&dom);
|
||||
let at = |s: &BSplineSurface, t: f64| -> Point2 {
|
||||
let end = knot_box(s).map_or(from, |d| side.at(&d));
|
||||
let x = from + t * (end - from);
|
||||
if side.is_u() {
|
||||
Point2::new(x, middle.y)
|
||||
} else {
|
||||
Point2::new(middle.x, x)
|
||||
}
|
||||
};
|
||||
let measure = |s: &BSplineSurface| -> Result<f64> {
|
||||
let x0 = s.evaluate_point(at(s, 0.0))?;
|
||||
let xm = s.evaluate_point(at(s, 0.5))?;
|
||||
let x1 = s.evaluate_point(at(s, 1.0))?;
|
||||
length(f, iso, x0, xm, x1)
|
||||
};
|
||||
let value = |e: f64| -> Result<Option<f64>> { made(e)?.map(|(s, _)| measure(&s)).transpose() };
|
||||
// the cap, from the side's middle point and the iso's other end inside the source
|
||||
let x_side = cur.evaluate_point(middle)?;
|
||||
let inside = {
|
||||
let (lo, hi) = (dom.min(), dom.max());
|
||||
match side {
|
||||
BoxSide::UMin => Point2::new(hi.x, middle.y),
|
||||
BoxSide::UMax => Point2::new(lo.x, middle.y),
|
||||
BoxSide::VMin => Point2::new(middle.x, hi.y),
|
||||
BoxSide::VMax => Point2::new(middle.x, lo.y),
|
||||
}
|
||||
};
|
||||
let x_in = cur.evaluate_point(inside)?;
|
||||
let cap = match iso {
|
||||
Iso::Parallel => radial(f, x_side).length() * f.quarter,
|
||||
Iso::Meridian { radius } => {
|
||||
// toward the pole the latitude climbs to
|
||||
let (h0, h_in) = (f.axis.dot(x_side - f.origin), f.axis.dot(x_in - f.origin));
|
||||
let pole = if h0 > h_in { f.axis } else { f.axis * -1.0 };
|
||||
radius * angle(x_side - f.origin, pole)
|
||||
}
|
||||
Iso::Tube { minor, .. } => minor * f.tube_left,
|
||||
Iso::Ruling => match f.apex {
|
||||
Some(apex) if (x_side - apex).length() < (x_in - apex).length() => {
|
||||
(x_side - apex).length()
|
||||
}
|
||||
_ => f64::INFINITY,
|
||||
},
|
||||
};
|
||||
let target = distance.min(cap);
|
||||
let capped = target < distance;
|
||||
let e = reach(&value, target, source.range()?)?;
|
||||
let (grown, bound) = made(e)?.ok_or_else(|| SolidError::sm_err(OP))?;
|
||||
let length = measure(&grown)?;
|
||||
certify_iso(&grown, side, middle, f, iso)?;
|
||||
// the closed form's rounding (the evaluations at the points' size, the products, the
|
||||
// atan2), and the points' construction bound through the angle
|
||||
let rounding = gamma(64) * (length + f.size) + 4.0 * bound;
|
||||
let across = Across {
|
||||
enclosure: Bracket {
|
||||
lower: (length - rounding).max(0.0),
|
||||
upper: length + rounding,
|
||||
},
|
||||
truncation: 0.0,
|
||||
rounding,
|
||||
converged: true,
|
||||
};
|
||||
Ok((grown, across, capped, bound))
|
||||
}
|
||||
|
||||
/// `out`, a net of degree 1 across `side` continued Natural past it, with the joint knot
|
||||
/// `joint` (the source's old end, now of multiplicity 1) removed: the span widened. Exact:
|
||||
/// the continuation of a straight row is its own line, so the row at the joint lies on the
|
||||
/// segment between its neighbours at the knots' ratio (on the homogeneous rows, so a rational
|
||||
/// row too), and dropping it leaves the same surface on the same parameters. The analytic's
|
||||
/// canonical form is one straight span (a plane's 2 x 2 net, a cone's two end circles), which
|
||||
/// is what the kind's recognisers read; a joint left would make it two.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `SM_ERR` where the joint is not a single interior knot of `out`; those of the net.
|
||||
fn joint_removed(out: &Net, side: BoxSide, joint: f64) -> Result<Net> {
|
||||
let (p, q) = out.degrees();
|
||||
let knots = if side.is_u() {
|
||||
out.u_knots()
|
||||
} else {
|
||||
out.v_knots()
|
||||
};
|
||||
let last = knots
|
||||
.len()
|
||||
.checked_sub(1)
|
||||
.ok_or_else(|| SolidError::sm_err(OP))?;
|
||||
let at: Vec<usize> = (0..knots.len())
|
||||
.filter(|&i| knots.get(i) == Some(&joint))
|
||||
.collect();
|
||||
let &[i] = at.as_slice() else {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
};
|
||||
// degree 1, clamped: an interior knot has two knots on each side, and the control
|
||||
// point at knot index i is point i - 1
|
||||
if i < 2 || i.checked_add(2).is_none_or(|j| j > last) {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
}
|
||||
let row = i.checked_sub(1).ok_or_else(|| SolidError::sm_err(OP))?;
|
||||
let kept: Vec<f64> = knots
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|&(j, _)| j != i)
|
||||
.map(|(_, &x)| x)
|
||||
.collect();
|
||||
let mut net = out.cpts();
|
||||
if side.is_u() {
|
||||
if row >= net.len() {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
}
|
||||
net = net
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter(|&(j, _)| j != row)
|
||||
.map(|(_, r)| r)
|
||||
.collect();
|
||||
Net::new(net, p, q, kept, out.v_knots().to_vec())
|
||||
} else {
|
||||
for r in &mut net {
|
||||
if row >= r.len() {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
}
|
||||
*r = std::mem::take(r)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter(|&(j, _)| j != row)
|
||||
.map(|(_, x)| x)
|
||||
.collect();
|
||||
}
|
||||
Net::new(net, p, q, out.u_knots().to_vec(), kept)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the iso across `side` of `s` at the side's middle `middle` is the closed form's
|
||||
/// curve: every homogeneous control point in a plane normal to the axis (a parallel) or
|
||||
/// through it (a meridian, a tube), within its rounding; a ruling is straight by its
|
||||
/// degree. Refused as not exact otherwise.
|
||||
fn certify_iso(
|
||||
s: &BSplineSurface,
|
||||
side: BoxSide,
|
||||
middle: Point2,
|
||||
f: &Frame,
|
||||
iso: Iso,
|
||||
) -> Result<()> {
|
||||
if matches!(iso, Iso::Ruling) {
|
||||
return Ok(());
|
||||
}
|
||||
let net = s.gw_nurb().ok_or_else(|| SolidError::sm_err(OP))?;
|
||||
let (t, dir) = if side.is_u() {
|
||||
(middle.y, Direction::U)
|
||||
} else {
|
||||
(middle.x, Direction::V)
|
||||
};
|
||||
let curve = extract_iso_crv(net, t, dir)?;
|
||||
let points: Vec<(Vec3, f64, f64)> = curve
|
||||
.cpts()
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let w = if p.w == NOW { 1.0 } else { p.w };
|
||||
let h = Vec3::new(p.x, p.y, p.z);
|
||||
// X W from the origin, and the size of its terms
|
||||
let mag = p.x.abs().max(p.y.abs()).max(p.z.abs()) + f.size * w.abs();
|
||||
(h - f.origin * w, w, mag)
|
||||
})
|
||||
.collect();
|
||||
let Some(&(x0, w0, _)) = points.first() else {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
};
|
||||
// the plane's normal and its offset from the origin, per unit weight
|
||||
let (n, height) = if matches!(iso, Iso::Parallel) {
|
||||
(f.axis, f.axis.dot(x0) / w0)
|
||||
} else {
|
||||
// through the axis and the iso: its farthest point from the axis sets it
|
||||
let far = points
|
||||
.iter()
|
||||
.map(|&(x, w, _)| {
|
||||
let d = x * (1.0 / w);
|
||||
d - f.axis * f.axis.dot(d)
|
||||
})
|
||||
.fold(
|
||||
Vec3::ZERO,
|
||||
|m, d| if d.length() > m.length() { d } else { m },
|
||||
);
|
||||
(unit(f.axis.cross(far))?, 0.0)
|
||||
};
|
||||
for &(x, w, mag) in &points {
|
||||
// the dot product's rounding and the plane's own (its normal and height from
|
||||
// stored points), a few ulps of the terms' size each
|
||||
let off = n.dot(x) - height * w;
|
||||
if off.abs() > 8.0 * gamma(32) * (mag + height.abs() * w.abs()) {
|
||||
return Err(refused(ExtendRefusal::NotExact, None));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
//! A ball patch extended as itself ([`Surface::extended_sides`] on a [`BallPatch`]): its
|
||||
//! own net continued Natural, still on its sphere.
|
||||
//!
|
||||
//! Not upstream: upstream extends a ball patch's net as any B-spline's (the base class),
|
||||
//! which leaves the sphere. Here (owner, 2026-11-04) the patch's OWN net is continued
|
||||
//! Natural past each side (the homogeneous polynomial of its end span carried on), so the
|
||||
//! source's net and knots stay bit for bit (a face keeps its UV trims on it: the shell's
|
||||
//! extended offset replaces the face's surface with UV trim creation off) and every point
|
||||
//! stays on the sphere: the identity `|H - C w|^2 = R^2 w^2` that puts the net on the
|
||||
//! sphere is a polynomial identity, so it holds on its continuation. The result is
|
||||
//! certified on the sphere on its net ([`lies_on`]), within its rounding, and is the ball
|
||||
//! patch on the same sphere.
|
||||
//!
|
||||
//! The patch is the sphere's triangle with one side collapsed to its pole `P` (proved: the
|
||||
//! side's certified diameter's lower bound is 0), the two sides through the pole great
|
||||
//! circles (meridians about the axis `C -> P`) and the third side a great circle's arc
|
||||
//! across from the pole. The pole's side is collapsed (left). Each other side asked is
|
||||
//! extended `distance` across it, capped (said so, [`SideExtension::UpToLimit`]):
|
||||
//! - **round the pole** (a side through the pole), no farther than the sphere's radius
|
||||
//! times a quarter of the turn the patch leaves open round its axis (the shell's rule,
|
||||
//! WR-532 / WR-728: its gaps close on the rounds' ends there), and never past half that
|
||||
//! turn in longitude, so the two sides never meet;
|
||||
//! - **away from the pole** (the third side), no farther than half the arc left from the
|
||||
//! third side's farthest point to the antipodal pole, where the meridians meet again
|
||||
//! (WR-728).
|
||||
//!
|
||||
//! **The distance** across a side is measured at the side's middle on the iso across it,
|
||||
//! in closed form: that iso lies on the sphere and (certified on its homogeneous control
|
||||
//! points: every 4 x 4 determinant of three of them and a fourth zero within its rounding)
|
||||
//! in a plane, so it is a circle, and its length from the side to the end is the circle's
|
||||
//! radius times the angle swept (away from the pole the isos are meridians: the sphere's
|
||||
//! radius times the angle along the great circle). The angle is the one from the side's
|
||||
//! point through the extension's middle point to its end: the arc the iso runs. The Ball's
|
||||
//! patch is the inverse stereographic image of a rational quadratic arc's sector (blend
|
||||
//! corner `ball_triangle_patch`), so its isos run their circles monotonically, and Natural
|
||||
//! carries on the same rational map. The extension parameter is solved for the distance
|
||||
//! (or the cap) by bisection on that closed form, met to `1e-12` of it.
|
||||
//!
|
||||
//! **Weights.** An extension whose weights are not all positive is refused
|
||||
//! ([`ExtendRefusal::WeightVanishes`]) naming the longest distance found whose weights are.
|
||||
//!
|
||||
//! Deviations: none (no upstream counterpart). No recursion.
|
||||
|
||||
use super::sides::{ExtendedSides, SideExtension};
|
||||
use super::{keeps, Continuity, ExtendRefusal, Local};
|
||||
use crate::error::{Result, SolidError};
|
||||
use crate::geom::extent2d::Extent2d;
|
||||
use crate::geom::vector2d::Point2;
|
||||
use crate::geom::vector3d::Vec3;
|
||||
use crate::kernel::Kernel;
|
||||
use crate::nurbs::b_spline_surface::BSplineSurface;
|
||||
use crate::nurbs::math::{gamma, NOW};
|
||||
use crate::nurbs::srf_basic::Direction;
|
||||
use crate::nurbs::srf_tool::p1::extract_iso_crv;
|
||||
use crate::surface::ball_patch::BallPatch;
|
||||
use crate::surface::implicit::{lies_on, Implicit};
|
||||
use crate::surface::side::{Across, BoxSide, Bracket};
|
||||
use crate::surface::surface::Surface;
|
||||
use std::f64::consts::PI;
|
||||
|
||||
const OP: &str = "surface::extend::ball::ball_extended_sides";
|
||||
|
||||
/// Doublings of the extension parameter that bracket the distance.
|
||||
const BRACKET_TRIES: usize = 60;
|
||||
/// Halvings of the bracket: the length met to `MEET` of it, or the bracket a few ulps wide.
|
||||
const BISECTIONS: usize = 200;
|
||||
/// The relative accuracy the closed-form length is met to.
|
||||
const MEET: f64 = 1e-12;
|
||||
|
||||
/// The angle between `a` and `b`, in radians.
|
||||
fn angle(a: Vec3, b: Vec3) -> f64 {
|
||||
a.cross(b).length().atan2(a.dot(b))
|
||||
}
|
||||
|
||||
fn refused(refusal: ExtendRefusal, distance: Option<f64>) -> SolidError {
|
||||
SolidError::ExtendRefused {
|
||||
operation: OP,
|
||||
refusal,
|
||||
distance,
|
||||
}
|
||||
}
|
||||
|
||||
/// How far a side may go: round the pole by a length across it (the sphere's radius times
|
||||
/// a quarter of the open turn) and no more than an angle of longitude (half the open
|
||||
/// turn: the two sides never meet), or away from it by an arc along the meridians
|
||||
/// (radians).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Cap {
|
||||
Round { length: f64, turn: f64 },
|
||||
Arc(f64),
|
||||
}
|
||||
|
||||
/// `patch` (the surface `s`) extended past the sides `asked` of its natural box by
|
||||
/// `distance` as itself (module docs); `None` for a patch with no collapsed side (not the
|
||||
/// sphere's triangle: extended as its net is).
|
||||
pub(super) fn ball_extended_sides(
|
||||
k: &Kernel,
|
||||
s: &Surface,
|
||||
patch: &BallPatch,
|
||||
asked: &[BoxSide],
|
||||
distance: f64,
|
||||
) -> Result<Option<ExtendedSides>> {
|
||||
if !(distance.is_finite() && distance > 0.0) {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP,
|
||||
"the distance is not finite and positive",
|
||||
));
|
||||
}
|
||||
// the net's knot box: the extension is past the net's ends, whatever box the patch keeps
|
||||
let dom = knot_box(patch.net())?;
|
||||
let mut pole_side = None;
|
||||
for side in BoxSide::ALL {
|
||||
if s.side_diameter(k, &dom, side)?.lower <= 0.0 {
|
||||
pole_side = Some(side);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let Some(pole_side) = pole_side else {
|
||||
return Ok(None);
|
||||
};
|
||||
let away = match pole_side {
|
||||
BoxSide::UMin => BoxSide::UMax,
|
||||
BoxSide::UMax => BoxSide::UMin,
|
||||
BoxSide::VMin => BoxSide::VMax,
|
||||
BoxSide::VMax => BoxSide::VMin,
|
||||
};
|
||||
let net = patch.net();
|
||||
let (c, r) = (patch.centre(), patch.radius());
|
||||
let p = net.evaluate_point(pole_side.point(&dom, 0.5))? - c;
|
||||
let a = net.evaluate_point(away.point(&dom, 0.0))? - c;
|
||||
let b = net.evaluate_point(away.point(&dom, 1.0))? - c;
|
||||
let z = p * (1.0 / p.length());
|
||||
let (ax, bx) = (a - z * a.dot(z), b - z * b.dot(z));
|
||||
if !(ax.length() > 0.0 && bx.length() > 0.0) {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP,
|
||||
"the third side's end is at the pole or its antipode",
|
||||
));
|
||||
}
|
||||
// the turn round the pole the patch spans, and the third side's farthest point from it
|
||||
let lambda = angle(ax, bx);
|
||||
let m = a.cross(b);
|
||||
let foot = m * m.dot(p) - p * m.dot(m);
|
||||
let mut theta = angle(p, a).max(angle(p, b));
|
||||
if foot.length() > 0.0 && a.cross(foot).dot(m) >= 0.0 && foot.cross(b).dot(m) >= 0.0 {
|
||||
theta = theta.max(angle(p, foot));
|
||||
}
|
||||
if !(lambda > 0.0 && lambda < PI && theta > 0.0 && theta < PI) {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP,
|
||||
"the patch is not a triangle under a half turn of its sphere",
|
||||
));
|
||||
}
|
||||
let mut cur = net.copy();
|
||||
let mut sides = [SideExtension::NotAsked; 4];
|
||||
let mut extension = 0.0_f64;
|
||||
for (&side, done) in BoxSide::ALL.iter().zip(sides.iter_mut()) {
|
||||
if !asked.contains(&side) {
|
||||
continue;
|
||||
}
|
||||
if side == pole_side {
|
||||
*done = SideExtension::Collapsed;
|
||||
continue;
|
||||
}
|
||||
let cap = if side == away {
|
||||
Cap::Arc(0.5 * (PI - theta))
|
||||
} else {
|
||||
Cap::Round {
|
||||
length: r * 0.25 * (2.0 * PI - lambda),
|
||||
turn: 0.5 * (2.0 * PI - lambda),
|
||||
}
|
||||
};
|
||||
let (grown, across, capped, bound) = extend_side(&cur, side, c, r, z, cap, distance)?;
|
||||
cur = grown;
|
||||
extension = extension.max(bound);
|
||||
*done = if capped {
|
||||
SideExtension::UpToLimit(across)
|
||||
} else {
|
||||
SideExtension::Extended(across)
|
||||
};
|
||||
}
|
||||
// every point on the sphere, certified on the net
|
||||
let on = Implicit::Sphere {
|
||||
centre: c,
|
||||
radius: r,
|
||||
};
|
||||
if !lies_on(&cur, &on)? {
|
||||
return Err(refused(ExtendRefusal::NotExact, None));
|
||||
}
|
||||
Ok(Some(ExtendedSides {
|
||||
surface: Surface::from(BallPatch::create(&cur, c, r)?),
|
||||
sides,
|
||||
extension,
|
||||
}))
|
||||
}
|
||||
|
||||
/// The box of `s`'s knots (its B-spline form's ends).
|
||||
pub(super) fn knot_box(s: &BSplineSurface) -> Result<Extent2d> {
|
||||
let net = s.gw_nurb().ok_or_else(|| SolidError::sm_err(OP))?;
|
||||
let (u, v) = (net.u_knots(), net.v_knots());
|
||||
match (u.first(), u.last(), v.first(), v.last()) {
|
||||
(Some(&u0), Some(&u1), Some(&v0), Some(&v1)) => Ok(Extent2d::from_bounds(u0, v0, u1, v1)),
|
||||
_ => Err(SolidError::sm_err(OP)),
|
||||
}
|
||||
}
|
||||
|
||||
/// What one ask's extension measures: its length across the side and its turn round the
|
||||
/// pole's axis.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Measure {
|
||||
length: f64,
|
||||
turn: f64,
|
||||
}
|
||||
|
||||
/// `cur` continued Natural past `side` by `distance` across it, capped by `cap`: the
|
||||
/// result, its across length, whether the cap stopped it, and its construction bound.
|
||||
fn extend_side(
|
||||
cur: &BSplineSurface,
|
||||
side: BoxSide,
|
||||
c: Vec3,
|
||||
r: f64,
|
||||
z: Vec3,
|
||||
cap: Cap,
|
||||
distance: f64,
|
||||
) -> Result<(BSplineSurface, Across, bool, f64)> {
|
||||
let net = cur.gw_nurb().ok_or_else(|| SolidError::sm_err(OP))?;
|
||||
let source = Local::of(net, side)?;
|
||||
let dom = knot_box(cur)?;
|
||||
let middle = side.point(&dom, 0.5);
|
||||
let made = |e: f64| -> Result<Option<(BSplineSurface, f64)>> {
|
||||
let Some(g) = source.extended(Continuity::Natural, e)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
if !g.is_finite() {
|
||||
return Ok(None);
|
||||
}
|
||||
let bound = g.bound();
|
||||
let out = g.surface(net, side)?;
|
||||
if !keeps(net, &out, side)? {
|
||||
return Err(SolidError::port_invariant(
|
||||
OP,
|
||||
"the extension moved the source's net",
|
||||
));
|
||||
}
|
||||
Ok(Some((BSplineSurface::from_gw_nurb(&out), bound)))
|
||||
};
|
||||
// the iso across the side at its middle, from the side (0) to the extension's end (1)
|
||||
let from_end = side.at(&dom);
|
||||
let at = |s: &BSplineSurface, f: f64| -> Point2 {
|
||||
let end = knot_box(s).map_or(from_end, |d| side.at(&d));
|
||||
let from = side.at(&dom);
|
||||
let x = from + f * (end - from);
|
||||
if side.is_u() {
|
||||
Point2::new(x, middle.y)
|
||||
} else {
|
||||
Point2::new(middle.x, x)
|
||||
}
|
||||
};
|
||||
let measure = |s: &BSplineSurface| -> Result<Measure> {
|
||||
let x0 = s.evaluate_point(at(s, 0.0))? - c;
|
||||
let xm = s.evaluate_point(at(s, 0.5))? - c;
|
||||
let x1 = s.evaluate_point(at(s, 1.0))? - c;
|
||||
let n = (xm - x0).cross(x1 - xm);
|
||||
let len = n.length();
|
||||
if len.is_nan() || len <= 0.0 {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
}
|
||||
let n = n * (1.0 / len);
|
||||
let cc = n * n.dot(x0);
|
||||
let (u0, u1) = (x0 - cc, x1 - cc);
|
||||
let mut phi = u0.cross(u1).dot(n).atan2(u0.dot(u1));
|
||||
if phi < 0.0 {
|
||||
phi += 2.0 * PI;
|
||||
}
|
||||
let (t0, t1) = (x0 - z * x0.dot(z), x1 - z * x1.dot(z));
|
||||
Ok(Measure {
|
||||
length: u0.length() * phi,
|
||||
turn: angle(t0, t1),
|
||||
})
|
||||
};
|
||||
let value = |e: f64, turn: bool| -> Result<Option<f64>> {
|
||||
Ok(match made(e)? {
|
||||
Some((s, _)) => {
|
||||
let m = measure(&s)?;
|
||||
Some(if turn { m.turn } else { m.length })
|
||||
}
|
||||
None => None,
|
||||
})
|
||||
};
|
||||
let range = source.range()?;
|
||||
let (e, capped) = match cap {
|
||||
Cap::Arc(arc) => {
|
||||
let target = distance.min(r * arc);
|
||||
(
|
||||
reach(&|e| value(e, false), target, range)?,
|
||||
target < distance,
|
||||
)
|
||||
}
|
||||
Cap::Round { length, turn } => {
|
||||
// within the length cap and the turn's (none where the turn never gets there)
|
||||
let at_turn = match solve(&|e| value(e, true), turn, range)? {
|
||||
Reach::At(e_turn) => Some((
|
||||
e_turn,
|
||||
value(e_turn, false)?.ok_or_else(|| SolidError::sm_err(OP))?,
|
||||
)),
|
||||
Reach::Short(_) => None,
|
||||
};
|
||||
let target = distance.min(length);
|
||||
match at_turn {
|
||||
Some((e_turn, l_turn)) if l_turn <= target => (e_turn, true),
|
||||
_ => (
|
||||
reach(&|e| value(e, false), target, range)?,
|
||||
target < distance,
|
||||
),
|
||||
}
|
||||
}
|
||||
};
|
||||
let (grown, bound) = made(e)?.ok_or_else(|| SolidError::sm_err(OP))?;
|
||||
let length = measure(&grown)?.length;
|
||||
// the closed form's rounding (the evaluations at the points' size, the cross products,
|
||||
// the atan2), and the points' construction bound through the angle
|
||||
let size = c.length() + r;
|
||||
let rounding = gamma(64) * (length + size) + 4.0 * bound;
|
||||
certify_circle(&grown, side, middle, c, size)?;
|
||||
let across = Across {
|
||||
enclosure: Bracket {
|
||||
lower: (length - rounding).max(0.0),
|
||||
upper: length + rounding,
|
||||
},
|
||||
truncation: 0.0,
|
||||
rounding,
|
||||
converged: true,
|
||||
};
|
||||
Ok((grown, across, capped, bound))
|
||||
}
|
||||
|
||||
/// Where `f` reaches its target ([`solve`]).
|
||||
pub(super) enum Reach {
|
||||
/// At this extension parameter.
|
||||
At(f64),
|
||||
/// Never: `f` stops growing (its curve's continuation tends to a point) short of the
|
||||
/// target, at this value.
|
||||
Short(f64),
|
||||
}
|
||||
|
||||
/// The extension parameter where `f` (increasing from 0, `None` where a weight is not
|
||||
/// positive) reaches `target`: bracketed by doubling from `e0`, then bisected. A target past
|
||||
/// every positive ask is refused naming the longest value found.
|
||||
pub(super) fn solve(f: &dyn Fn(f64) -> Result<Option<f64>>, target: f64, e0: f64) -> Result<Reach> {
|
||||
let (mut lo, mut lo_v) = (0.0_f64, 0.0_f64);
|
||||
let mut hi = None;
|
||||
let mut bad = None;
|
||||
let mut e = e0;
|
||||
for _ in 0..BRACKET_TRIES {
|
||||
match f(e)? {
|
||||
Some(v) if v >= target => {
|
||||
hi = Some(e);
|
||||
break;
|
||||
}
|
||||
// no longer growing: the continuation tends to its limit point
|
||||
Some(v) if lo > 0.0 && v - lo_v <= MEET * v => return Ok(Reach::Short(v)),
|
||||
Some(v) => {
|
||||
lo = e;
|
||||
lo_v = v;
|
||||
e *= 2.0;
|
||||
}
|
||||
None => {
|
||||
bad = Some(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let hi = match (hi, bad) {
|
||||
(Some(h), _) => h,
|
||||
(None, Some(mut b)) => {
|
||||
// the longest positive ask below the first whose weights are not
|
||||
for _ in 0..BISECTIONS {
|
||||
let m = 0.5 * (lo + b);
|
||||
if !(m > lo && m < b) {
|
||||
break;
|
||||
}
|
||||
match f(m)? {
|
||||
Some(v) if v >= target => return Ok(Reach::At(bisect(f, target, lo, m)?)),
|
||||
Some(v) => {
|
||||
lo = m;
|
||||
lo_v = v;
|
||||
}
|
||||
None => b = m,
|
||||
}
|
||||
}
|
||||
return Err(refused(
|
||||
ExtendRefusal::WeightVanishes,
|
||||
(lo_v > 0.0).then_some(lo_v),
|
||||
));
|
||||
}
|
||||
(None, None) => return Ok(Reach::Short(lo_v)),
|
||||
};
|
||||
Ok(Reach::At(bisect(f, target, lo, hi)?))
|
||||
}
|
||||
|
||||
/// The parameter where `f` reaches `target`, or refused as not exact naming the longest
|
||||
/// value reached.
|
||||
pub(super) fn reach(f: &dyn Fn(f64) -> Result<Option<f64>>, target: f64, e0: f64) -> Result<f64> {
|
||||
match solve(f, target, e0)? {
|
||||
Reach::At(e) => Ok(e),
|
||||
Reach::Short(v) => Err(refused(ExtendRefusal::NotExact, (v > 0.0).then_some(v))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Bisection of `[lo, hi]` (`f(lo) < target <= f(hi)`) until `f` is within `MEET` of the
|
||||
/// target.
|
||||
fn bisect(f: &dyn Fn(f64) -> Result<Option<f64>>, target: f64, lo: f64, hi: f64) -> Result<f64> {
|
||||
let (mut lo, mut hi) = (lo, hi);
|
||||
for _ in 0..BISECTIONS {
|
||||
let m = 0.5 * (lo + hi);
|
||||
if !(m > lo && m < hi) {
|
||||
break;
|
||||
}
|
||||
let v = f(m)?.ok_or_else(|| SolidError::sm_err(OP))?;
|
||||
if (v - target).abs() <= MEET * target {
|
||||
return Ok(m);
|
||||
}
|
||||
if v < target {
|
||||
lo = m;
|
||||
} else {
|
||||
hi = m;
|
||||
}
|
||||
}
|
||||
Ok(hi)
|
||||
}
|
||||
|
||||
/// Whether the iso across `side` of `s` at the side's middle `middle` lies in a plane:
|
||||
/// every determinant of three spread homogeneous control points (from the centre `c`) and
|
||||
/// a fourth is zero within its rounding (`size` the points' absolute size), and the three
|
||||
/// span a plane. Refused as not exact otherwise.
|
||||
fn certify_circle(
|
||||
s: &BSplineSurface,
|
||||
side: BoxSide,
|
||||
middle: Point2,
|
||||
c: Vec3,
|
||||
size: f64,
|
||||
) -> Result<()> {
|
||||
let net = s.gw_nurb().ok_or_else(|| SolidError::sm_err(OP))?;
|
||||
// the iso along the side's across direction: a u-curve across a u side
|
||||
let (t, dir) = if side.is_u() {
|
||||
(middle.y, Direction::U)
|
||||
} else {
|
||||
(middle.x, Direction::V)
|
||||
};
|
||||
let iso = extract_iso_crv(net, t, dir)?;
|
||||
let mut rows: Vec<[f64; 4]> = Vec::new();
|
||||
let mut mags: Vec<f64> = Vec::new();
|
||||
for p in iso.cpts() {
|
||||
let w = if p.w == NOW { 1.0 } else { p.w };
|
||||
rows.push([p.x - c.x * w, p.y - c.y * w, p.z - c.z * w, w]);
|
||||
mags.push(p.x.abs().max(p.y.abs()).max(p.z.abs()) + (size + 1.0) * w.abs());
|
||||
}
|
||||
let n = rows.len();
|
||||
let (Some(&r0), Some(&rm), Some(&rl)) = (rows.first(), rows.get(n / 2), rows.last()) else {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
};
|
||||
let (Some(&m0), Some(&mm), Some(&ml)) = (mags.first(), mags.get(n / 2), mags.last()) else {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
};
|
||||
let g = |row: [f64; 4], x: usize| row.get(x).copied().unwrap_or(0.0);
|
||||
// the 3 x 3 minor of the three spread rows on columns i, j, l
|
||||
let minor = |i: usize, j: usize, l: usize| -> f64 {
|
||||
g(r0, i) * (g(rm, j) * g(rl, l) - g(rm, l) * g(rl, j))
|
||||
- g(r0, j) * (g(rm, i) * g(rl, l) - g(rm, l) * g(rl, i))
|
||||
+ g(r0, l) * (g(rm, i) * g(rl, j) - g(rm, j) * g(rl, i))
|
||||
};
|
||||
let three = m0 * mm * ml;
|
||||
// the three span a plane: a minor well past its rounding
|
||||
let spans = [(0, 1, 2), (0, 1, 3), (0, 2, 3), (1, 2, 3)]
|
||||
.iter()
|
||||
.any(|&(i, j, l)| minor(i, j, l).abs() > 1e3 * 6.0 * gamma(16) * three);
|
||||
if !spans {
|
||||
return Err(refused(ExtendRefusal::NotExact, None));
|
||||
}
|
||||
let (m123, m023, m013, m012) = (
|
||||
minor(1, 2, 3),
|
||||
minor(0, 2, 3),
|
||||
minor(0, 1, 3),
|
||||
minor(0, 1, 2),
|
||||
);
|
||||
for (&row, &mag) in rows.iter().zip(&mags) {
|
||||
// expansion along the fourth row; |fl(det) - det| <= gamma_K per(|A|) <= gamma_K 4!
|
||||
// times the product of the rows' sizes
|
||||
let det = -g(row, 0) * m123 + g(row, 1) * m023 - g(row, 2) * m013 + g(row, 3) * m012;
|
||||
if det.abs() > gamma(32) * 24.0 * three * mag {
|
||||
return Err(refused(ExtendRefusal::NotExact, None));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -25,7 +25,13 @@
|
||||
//! convex hull: every weight positive, as the Natural extension proves). The result's
|
||||
//! bound is `max(eps, eps G + the extension's rounding)`, the growth `G` rounded up.
|
||||
//!
|
||||
//! A G^n section (order 2 and up) has no growth factor here and is refused across.
|
||||
//! A G^n section (order `n >= 2`) is the same argument with `d_S = 3 n + 1` and its weight
|
||||
//! `W = A B^n C^n` (`A = (1 - t)^(n+1) + t^(n+1)`, `B = 1 - w + w t`, `C = 1 - w t`), bounded
|
||||
//! in closed form over a proved range of its thumbweight `w`: a constant one, or the equal
|
||||
//! area one's enclosure from its certificate's interval Newton roots (the stations' own w's
|
||||
//! padded would be no proof). `A` grows with `|t - 1/2|`; `B` and `C` are bilinear in `(t,
|
||||
//! w)`, their extremes at the corners of the (t range) x (w range) box. A corner past which
|
||||
//! `B` or `C` is not positive is a weight that vanishes: refused.
|
||||
//!
|
||||
//! **Along the spine (s).** Past the definition's end station (the side its last or first
|
||||
//! station, the net on its whole section `t in [0, 1]`, s along u): the definition is
|
||||
@@ -33,14 +39,19 @@
|
||||
//! its seeds by a span to the new station, as the construction's seeds are the feet's
|
||||
//! Hermite curves on the stations), and the net gains that span of the construction's:
|
||||
//! the stations at the end and at `s_new` solved, the span's Hermite rows
|
||||
//! ([`end_span_of_definition`], the construction's `NetForm::span`) certified against the definition ([`gn_certify_span`]'s bound), and
|
||||
//! joined after the source's net at multiplicity 3 (the construction's own joints), scaled
|
||||
//! to its weights. The source's net is kept bit for bit; the piece's first row is the
|
||||
//! source's own, so its bound against the span's Hermite rows is the joint's row
|
||||
//! difference ([`net_distance`]). The result's bound: `max(eps, certificate + joint +
|
||||
//! three floors + rounding)`. The distance is solved as any extension's
|
||||
//! ([`span_of_definition`], the construction's `NetForm::span`) certified against the
|
||||
//! definition ([`gn_certify_span`]'s bound), and joined after the source's net at
|
||||
//! multiplicity 3 (the construction's own joints), scaled to its weights. A side INSIDE the
|
||||
//! spine (a piece of the blend cut across it) runs the definition's spans already there
|
||||
//! first: each span the extension crosses solved and certified the same way, the part of
|
||||
//! it past the side (or up to `s_new`) its Hermite rows' cubic Bezier cut there (blossoms),
|
||||
//! each part joined at multiplicity 3 and scaled to the row before it; then past the end
|
||||
//! as above. The source's net is kept bit for bit; each part's first row is the row before
|
||||
//! it, so its bound against its span's rows is the joint's row difference
|
||||
//! ([`net_distance`]). The result's bound: `max(eps, the largest part's certificate +
|
||||
//! joint, + three floors + rounding)`. The distance is solved as any extension's
|
||||
//! ([`Surface::extended_by_distance`]), measured on the net; an ask whose station does not
|
||||
//! solve, whose foot leaves its support's box, or whose span is not proved, works as one
|
||||
//! solve, whose foot leaves a B-spline support's box (an analytic support continued is still itself), or whose span is not proved, works as one
|
||||
//! whose weights are not positive: the refusal names the longest that does.
|
||||
//!
|
||||
//! [`gn_certify_span`]: crate::blend::fillet_section::certificate::gn_certify_span
|
||||
@@ -48,10 +59,11 @@
|
||||
//! Deviations: none (no upstream counterpart). No recursion.
|
||||
|
||||
use super::{keeps, op_err, solve, Continuity, Extended, Local, Made, H, OP, REL};
|
||||
use crate::blend::fillet_section::recertify::recertify_with_thumbweight;
|
||||
use crate::blend::fillet_section::recertify::{
|
||||
end_span_of_definition, net_distance, precision_floor, DefinitionSpan,
|
||||
net_distance, precision_floor, span_of_definition, DefinitionSpan,
|
||||
};
|
||||
use crate::blend::fillet_section::section::{param, param_in};
|
||||
use crate::blend::fillet_section::section::{param, param_in, GN_THUMBWEIGHT_EQUAL_AREA};
|
||||
use crate::blend::fillet_section::station::GnStation;
|
||||
use crate::error::{Result, SolidError};
|
||||
use crate::geom::extent2d::Extent2d;
|
||||
@@ -61,6 +73,7 @@ use crate::nurbs::b_spline_curve::BSplineCurve;
|
||||
use crate::nurbs::b_spline_surface::BSplineSurface;
|
||||
use crate::nurbs::math::{gamma, CPoint, NOW};
|
||||
use crate::nurbs::srf_basic::Surface as Net;
|
||||
use crate::surface::implicit::AnalyticSupport;
|
||||
use crate::surface::procedural_blend::ProceduralBlend;
|
||||
use crate::surface::side::BoxSide;
|
||||
use crate::surface::surface::{Surface, SurfaceKind};
|
||||
@@ -79,15 +92,12 @@ pub(super) fn blend_extended(
|
||||
return along(k, pb, dom, side, distance);
|
||||
}
|
||||
let order = pb.order();
|
||||
let n = order as usize;
|
||||
// the section's degree in t: the chord's 1, the circle's 2, a G^n section's 3 n + 1
|
||||
let section_degree = match order {
|
||||
0 => 1,
|
||||
1 => 2,
|
||||
_ => {
|
||||
return Err(SolidError::Unsupported {
|
||||
operation: OP,
|
||||
reason: "a G^n section has no growth factor past its source's t range here",
|
||||
})
|
||||
}
|
||||
_ => 3 * n + 1,
|
||||
};
|
||||
let source_net = pb.net.gw_nurb().ok_or_else(|| SolidError::sm_err(OP))?;
|
||||
let grown = Surface::from(pb.net.clone()).extended_by_distance(
|
||||
@@ -129,14 +139,19 @@ pub(super) fn blend_extended(
|
||||
let m = section_degree + net_degree;
|
||||
let x = (2.0 * te - t0 - t1).abs() / (t1 - t0);
|
||||
let cheb = chebyshev(m, x);
|
||||
let (w_hi, w_lo) = if order == 0 {
|
||||
(1.0, 1.0)
|
||||
} else {
|
||||
let off = |t: f64| (t * (t - 1.0)).max(0.0);
|
||||
let hi = 1.0 + 2.0 * off(t0).max(off(t1));
|
||||
let (lo_t, hi_t) = (ts.min(te), ts.max(te));
|
||||
let lo = if lo_t < 1.0 && hi_t > 0.0 { 0.5 } else { 1.0 };
|
||||
(hi, lo)
|
||||
let (w_hi, w_lo) = match order {
|
||||
0 => (1.0, 1.0),
|
||||
1 => {
|
||||
let off = |t: f64| (t * (t - 1.0)).max(0.0);
|
||||
let hi = 1.0 + 2.0 * off(t0).max(off(t1));
|
||||
let (lo_t, hi_t) = (ts.min(te), ts.max(te));
|
||||
let lo = if lo_t < 1.0 && hi_t > 0.0 { 0.5 } else { 1.0 };
|
||||
(hi, lo)
|
||||
}
|
||||
_ => {
|
||||
let w = thumbweight(k, pb)?;
|
||||
gn_weight_range(n, [t0, t1], [ts.min(te), ts.max(te)], w)?
|
||||
}
|
||||
};
|
||||
let (v_hi, _) = weights(source_net);
|
||||
let (_, v_lo) = weights(out_net);
|
||||
@@ -168,6 +183,87 @@ pub(super) fn blend_extended(
|
||||
})
|
||||
}
|
||||
|
||||
/// The thumbweight of `pb`'s G^n section over its face, `[lo, hi]`: a constant one, or the
|
||||
/// equal area one's proved enclosure from its certificate's interval Newton roots
|
||||
/// ([`recertify_with_thumbweight`]).
|
||||
fn thumbweight(k: &Kernel, pb: &ProceduralBlend) -> Result<[f64; 2]> {
|
||||
let tw = pb.definition().thumbweight;
|
||||
if tw != GN_THUMBWEIGHT_EQUAL_AREA {
|
||||
return Ok([tw, tw]);
|
||||
}
|
||||
recertify_with_thumbweight(k, pb)?
|
||||
.1
|
||||
.ok_or(SolidError::Unsupported {
|
||||
operation: OP,
|
||||
reason: "the equal area thumbweight is not enclosed over the blend",
|
||||
})
|
||||
}
|
||||
|
||||
/// A G^n section's weight `W(t) = A(t) B(t)^n C(t)^n` (`A = (1 - t)^(n+1) + t^(n+1)`,
|
||||
/// `B = 1 - w + w t`, `C = 1 - w t`, `w` the thumbweight in `w`): its largest value over the
|
||||
/// source's t range `src` and its least over the extension's `ext`, rounded outward. `A`
|
||||
/// grows with `|t - 1/2|` (`A' = (n + 1) (t^n - (1 - t)^n)`, of the sign of `t - 1/2`), so
|
||||
/// its extremes on an interval are at the point farthest from and nearest to 1/2; `B` and
|
||||
/// `C` are bilinear in `(t, w)`, so their extremes on a box are at its corners. A corner of
|
||||
/// the extension's box where `B` or `C` is not positive is a weight that vanishes there:
|
||||
/// refused ([`super::ExtendRefusal::WeightVanishes`]).
|
||||
fn gn_weight_range(n: usize, src: [f64; 2], ext: [f64; 2], w: [f64; 2]) -> Result<(f64, f64)> {
|
||||
let exp = i32::try_from(n).map_err(|_| SolidError::sm_err(OP))?;
|
||||
let a = |t: f64| (1.0 - t).powi(exp + 1) + t.powi(exp + 1);
|
||||
let far = |[x, y]: [f64; 2]| {
|
||||
if (x - 0.5).abs() >= (y - 0.5).abs() {
|
||||
x
|
||||
} else {
|
||||
y
|
||||
}
|
||||
};
|
||||
let near = |[x, y]: [f64; 2]| {
|
||||
if x <= 0.5 && y >= 0.5 {
|
||||
0.5
|
||||
} else if (x - 0.5).abs() <= (y - 0.5).abs() {
|
||||
x
|
||||
} else {
|
||||
y
|
||||
}
|
||||
};
|
||||
let corners = |[t0, t1]: [f64; 2]| {
|
||||
let mut b = [0.0_f64; 4];
|
||||
let mut c = [0.0_f64; 4];
|
||||
for (i, (&t, &wt)) in [t0, t0, t1, t1]
|
||||
.iter()
|
||||
.zip(&[w[0], w[1], w[0], w[1]])
|
||||
.enumerate()
|
||||
{
|
||||
if let (Some(bi), Some(ci)) = (b.get_mut(i), c.get_mut(i)) {
|
||||
*bi = 1.0 - wt + wt * t;
|
||||
*ci = 1.0 - wt * t;
|
||||
}
|
||||
}
|
||||
(b, c)
|
||||
};
|
||||
let (bs, cs) = corners(src);
|
||||
let mag = |x: &[f64; 4]| x.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
|
||||
let hi = a(far(src)) * mag(&bs).powi(exp) * mag(&cs).powi(exp);
|
||||
let (be, ce) = corners(ext);
|
||||
let least = |x: &[f64; 4]| x.iter().fold(f64::INFINITY, |m, &v| m.min(v));
|
||||
let (bl, cl) = (least(&be), least(&ce));
|
||||
if !(bl > 0.0 && cl > 0.0) {
|
||||
return Err(SolidError::ExtendRefused {
|
||||
operation: OP,
|
||||
refusal: super::ExtendRefusal::WeightVanishes,
|
||||
distance: None,
|
||||
});
|
||||
}
|
||||
let lo = a(near(ext)) * bl.powi(exp) * cl.powi(exp);
|
||||
// each a product of 3 n + 2 roundings at most: up and down by gamma_(4 n + 8)
|
||||
let g = gamma(4 * n + 8);
|
||||
let (hi, lo) = (hi * (1.0 + g), lo * (1.0 - g));
|
||||
if !(hi.is_finite() && lo > 0.0) {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
}
|
||||
Ok((hi, lo))
|
||||
}
|
||||
|
||||
/// `T_m(x)` for `x >= 1` (`|x|` taken), by its recurrence (every term positive and
|
||||
/// increasing there), rounded up.
|
||||
fn chebyshev(m: usize, x: f64) -> f64 {
|
||||
@@ -222,11 +318,16 @@ fn along(
|
||||
let s_side = a[0] + b[0] * side.at(dom);
|
||||
// outward from the side, s grows (`up`) or falls
|
||||
let up = side.is_high() == (b[0] > 0.0);
|
||||
let end = if up { sigma.last() } else { sigma.first() };
|
||||
if end.copied() != Some(s_side) {
|
||||
// the side on the definition's spine: at its end station, or inside it (extended by
|
||||
// the definition's spans already there, then past its end)
|
||||
let (Some(&first), Some(&last)) = (sigma.first(), sigma.last()) else {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
};
|
||||
let end = if up { last } else { first };
|
||||
if !(s_side == end || (s_side > first && s_side < last)) {
|
||||
return Err(SolidError::Unsupported {
|
||||
operation: OP,
|
||||
reason: "the side is not the definition's end station",
|
||||
reason: "the side is not on the definition's spine",
|
||||
});
|
||||
}
|
||||
let source_net = pb.net.gw_nurb().ok_or_else(|| SolidError::sm_err(OP))?;
|
||||
@@ -238,34 +339,66 @@ fn along(
|
||||
});
|
||||
}
|
||||
let target = pb.bound();
|
||||
// the definition and its end span for the ask e (in the net's parameter)
|
||||
let piece_of = |e: f64| -> Result<Option<(ProceduralBlend, DefinitionSpan)>> {
|
||||
let ds = e * b[0].abs();
|
||||
let rate = b[0].abs();
|
||||
// the definition and its spans from the side to s_new for the ask e (in the net's
|
||||
// parameter), outward, each with its part `[x, y]` in s
|
||||
let pieces_of = |e: f64| -> Result<Option<(ProceduralBlend, Vec<Piece>)>> {
|
||||
let ds = e * rate;
|
||||
let s_new = if up { s_side + ds } else { s_side - ds };
|
||||
let next = continued_definition(pb, s_new, up)?;
|
||||
// a station that does not solve, a foot off its support or a span not proved: an
|
||||
// ask that does not work, as a weight not positive
|
||||
let span = match end_span_of_definition(k, &next, up, target) {
|
||||
Ok(span) if span.certified.is_finite() && feet_on(k, &next, &span)? => span,
|
||||
Ok(_) | Err(SolidError::InvalidInput { .. } | SolidError::Upstream { .. }) => {
|
||||
return Ok(None)
|
||||
let sig = next
|
||||
.definition()
|
||||
.seeds
|
||||
.first()
|
||||
.ok_or_else(op_err)?
|
||||
.knots(None)?
|
||||
.0;
|
||||
let (lo, hi) = if up { (s_side, s_new) } else { (s_new, s_side) };
|
||||
let mut pieces = Vec::new();
|
||||
for (idx, (sa, sb)) in sig.iter().zip(sig.iter().skip(1)).enumerate() {
|
||||
let (x, y) = (sa.max(lo), sb.min(hi));
|
||||
if y <= x {
|
||||
continue;
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
Ok(Some((next, span)))
|
||||
};
|
||||
let made = |e: f64| -> Result<Option<Made>> {
|
||||
let Some((_, span)) = piece_of(e)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let (ext, piece, scale) = piece_rows(&source, &span, up)?;
|
||||
// the piece's own bound waits for the met ask: here only that it is finite
|
||||
if !piece_distance(&piece, scale)?.is_finite() {
|
||||
// a station that does not solve, a foot off its support or a span not proved:
|
||||
// an ask that does not work, as a weight not positive
|
||||
let span = match span_of_definition(k, &next, idx, target) {
|
||||
Ok(span) if span.certified.is_finite() && feet_on(k, &next, &span)? => span,
|
||||
Ok(_) | Err(SolidError::InvalidInput { .. } | SolidError::Upstream { .. }) => {
|
||||
return Ok(None)
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
pieces.push(Piece {
|
||||
span,
|
||||
part: [(x - sa) / (sb - sa), (y - sa) / (sb - sa)],
|
||||
length: (y - x) / rate,
|
||||
});
|
||||
}
|
||||
if !up {
|
||||
pieces.reverse();
|
||||
}
|
||||
if pieces.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut grown = source.joined(ext, &[source.end()? + e; 4])?;
|
||||
// the scaled points' rounding, one product each
|
||||
grown.err = gamma(1) * grown.size();
|
||||
Ok(Some((next, pieces)))
|
||||
};
|
||||
let made = |e: f64| -> Result<Option<Made>> {
|
||||
let Some((_, pieces)) = pieces_of(e)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let (ext, rows) = pieces_rows(&source, &pieces, up)?;
|
||||
// the pieces' own bounds wait for the met ask: here only that they are finite
|
||||
for (piece, scale) in &rows {
|
||||
if !piece_distance(piece, *scale)?.is_finite() {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
let grown = source.joined(ext, &joints(source.end()?, e, &pieces))?;
|
||||
let mut grown = grown;
|
||||
// the scaled points' rounding, one product each, and each part's subdivision (three
|
||||
// levels of convex combinations, the part's ends a few more)
|
||||
grown.err = gamma(16) * grown.size();
|
||||
let bound = grown.bound();
|
||||
let out = grown.surface(source_net, side)?;
|
||||
Ok(Some((
|
||||
@@ -294,14 +427,26 @@ fn along(
|
||||
"the extension moved the source's net",
|
||||
));
|
||||
}
|
||||
// the met ask's definition again (the same solve and certificate, deterministic), its
|
||||
// seeds the feet's Hermite curves on its stations, and the piece's bound against it
|
||||
// the met ask's definition again (the same solves and certificates, deterministic);
|
||||
// past the end station its seeds the feet's Hermite curves on its new span's
|
||||
// stations; each piece's bound against its span
|
||||
let grown_end = Local::of(out_net, side)?.end()?;
|
||||
let e = grown_end - source.end()?;
|
||||
let (mut next, span) = piece_of(e)?.ok_or_else(op_err)?;
|
||||
hermite_seeds(&mut next, &span.stations, up)?;
|
||||
let (_, rows, scale) = piece_rows(&source, &span, up)?;
|
||||
let piece = span.certified + piece_distance(&rows, scale)?;
|
||||
let (mut next, pieces) = pieces_of(e)?.ok_or_else(op_err)?;
|
||||
let past_end = if up {
|
||||
s_side + e * rate > last
|
||||
} else {
|
||||
s_side - e * rate < first
|
||||
};
|
||||
if past_end {
|
||||
let outer = pieces.last().ok_or_else(op_err)?;
|
||||
hermite_seeds(&mut next, &outer.span.stations, up)?;
|
||||
}
|
||||
let (_, rows) = pieces_rows(&source, &pieces, up)?;
|
||||
let mut piece = 0.0_f64;
|
||||
for (p, (r, scale)) in pieces.iter().zip(&rows) {
|
||||
piece = piece.max(p.span.certified + piece_distance(r, *scale)?);
|
||||
}
|
||||
let floor = 3.0 * precision_floor(k, next.supports(), next.order(), net)?;
|
||||
let mut out = ProceduralBlend {
|
||||
net: net.clone(),
|
||||
@@ -319,6 +464,123 @@ fn along(
|
||||
})
|
||||
}
|
||||
|
||||
/// One span of a definition an extension along its spine runs: the span, the part of it
|
||||
/// it runs (`[x, y]` of its own parameter in `[0, 1]`, in s's direction) and that part's
|
||||
/// length in the net's parameter.
|
||||
struct Piece {
|
||||
span: DefinitionSpan,
|
||||
part: [f64; 2],
|
||||
length: f64,
|
||||
}
|
||||
|
||||
/// The knots after the source's joint for `pieces` from `end` by `e` in all: each part's
|
||||
/// end at multiplicity 3 (the construction's own joints), the last clamped (4), at exactly
|
||||
/// `end + e`.
|
||||
fn joints(end: f64, e: f64, pieces: &[Piece]) -> Vec<f64> {
|
||||
let mut knots = Vec::new();
|
||||
let mut at = end;
|
||||
let n = pieces.len();
|
||||
for (i, p) in pieces.iter().enumerate() {
|
||||
if i + 1 == n {
|
||||
knots.extend([end + e; 4]);
|
||||
} else {
|
||||
at += p.length;
|
||||
knots.extend([at; 3]);
|
||||
}
|
||||
}
|
||||
knots
|
||||
}
|
||||
|
||||
/// The cubic Bezier `p` over `[t0, t1]` of its parameter: its blossoms `B(t0^(3-i), t1^i)`.
|
||||
fn sub_bezier(p: [H; 4], t0: f64, t1: f64) -> [H; 4] {
|
||||
let blossom = |u: [f64; 3]| -> H {
|
||||
let mut q = p;
|
||||
for (level, &t) in u.iter().enumerate() {
|
||||
for i in 0..3 - level {
|
||||
if let (Some(&a), Some(&b)) = (q.get(i), q.get(i + 1)) {
|
||||
if let Some(x) = q.get_mut(i) {
|
||||
*x = super::lin(1.0 - t, a, t, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
q.first().copied().unwrap_or([f64::NAN; 4])
|
||||
};
|
||||
[
|
||||
blossom([t0, t0, t0]),
|
||||
blossom([t0, t0, t1]),
|
||||
blossom([t0, t1, t1]),
|
||||
blossom([t1, t1, t1]),
|
||||
]
|
||||
}
|
||||
|
||||
/// The extension's rows past the source's joint for `pieces` (outward), each part's rows
|
||||
/// scaled to the weight of the row before it (the source's joint row for the first), and
|
||||
/// for each piece its rows against its span's ([`piece_distance`]) with its scale.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn pieces_rows(
|
||||
source: &Local,
|
||||
pieces: &[Piece],
|
||||
up: bool,
|
||||
) -> Result<(Vec<Vec<H>>, Vec<(Vec<(Vec<H>, Vec<H>)>, f64)>)> {
|
||||
let mut ext: Vec<Vec<H>> = vec![Vec::new(); source.rows.len()];
|
||||
let mut out = Vec::new();
|
||||
// the row before each piece, across: the source's joint row first
|
||||
let mut before: Vec<H> = source
|
||||
.rows
|
||||
.iter()
|
||||
.map(|r| r.last().copied().ok_or_else(op_err))
|
||||
.collect::<Result<_>>()?;
|
||||
for p in pieces {
|
||||
let ld = p.span.ld;
|
||||
if source.rows.len() != ld {
|
||||
return Err(SolidError::port_invariant(
|
||||
OP,
|
||||
"the net is not the section's width",
|
||||
));
|
||||
}
|
||||
// the span's part at across index j, outward from the joint
|
||||
let part = |j: usize| -> Result<[H; 4]> {
|
||||
let mut c = [[0.0; 4]; 4];
|
||||
for (i, ci) in c.iter_mut().enumerate() {
|
||||
let at = i
|
||||
.checked_mul(ld)
|
||||
.and_then(|x| x.checked_add(j))
|
||||
.ok_or_else(op_err)?;
|
||||
let (q, w) = (
|
||||
p.span.homog.get(at).ok_or_else(op_err)?,
|
||||
p.span.weights.get(at).ok_or_else(op_err)?,
|
||||
);
|
||||
*ci = [q.x, q.y, q.z, *w];
|
||||
}
|
||||
let mut sub = sub_bezier(c, p.part[0], p.part[1]);
|
||||
if !up {
|
||||
sub.reverse();
|
||||
}
|
||||
Ok(sub)
|
||||
};
|
||||
let (Some(b0), Ok(h0)) = (before.first(), part(0)) else {
|
||||
return Err(op_err());
|
||||
};
|
||||
let scale = b0[3] / h0[0][3];
|
||||
let mut rows = Vec::new();
|
||||
for (j, (e, bj)) in ext.iter_mut().zip(before.iter_mut()).enumerate() {
|
||||
let h = part(j)?;
|
||||
let (mut hs, mut bs) = (vec![h[0]], vec![*bj]);
|
||||
for x in h.iter().skip(1) {
|
||||
let y = x.map(|c| scale * c);
|
||||
e.push(y);
|
||||
hs.push(*x);
|
||||
bs.push(y);
|
||||
}
|
||||
*bj = *bs.last().ok_or_else(op_err)?;
|
||||
rows.push((bs, hs));
|
||||
}
|
||||
out.push((rows, scale));
|
||||
}
|
||||
Ok((ext, out))
|
||||
}
|
||||
|
||||
/// `|B - H|` over the piece: `piece[j]` the net's rows along s at across index j (the
|
||||
/// source's joint row, then the scaled span rows) and the span's own rows (scaled by
|
||||
/// `scale`, the same surface), as [`net_distance`] bounds two nets.
|
||||
@@ -381,6 +643,8 @@ fn continued_curve(c: &BSplineCurve, s_new: f64, up: bool) -> Result<BSplineCurv
|
||||
p,
|
||||
rational,
|
||||
err: 0.0,
|
||||
q: 0,
|
||||
frame: None,
|
||||
};
|
||||
if !up {
|
||||
l.reverse();
|
||||
@@ -475,57 +739,24 @@ fn hermite_seeds(next: &mut ProceduralBlend, stations: &[GnStation], up: bool) -
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The span's rows joined to the source past its side: per across index j the three
|
||||
/// points after the joint, scaled to the source's weights (the source's net is the
|
||||
/// stations' rows scaled by one factor, which the joint's weights give); the piece's net
|
||||
/// (the source's joint row, then those) beside the span's own rows; and the scale.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn piece_rows(
|
||||
source: &Local,
|
||||
span: &DefinitionSpan,
|
||||
up: bool,
|
||||
) -> Result<(Vec<Vec<H>>, Vec<(Vec<H>, Vec<H>)>, f64)> {
|
||||
let ld = span.ld;
|
||||
if source.rows.len() != ld {
|
||||
return Err(SolidError::port_invariant(
|
||||
OP,
|
||||
"the net is not the section's width",
|
||||
));
|
||||
}
|
||||
// the span's row i outward from the joint at across index j
|
||||
let row = |i: usize, j: usize| -> Option<H> {
|
||||
let i = if up { i } else { 3_usize.checked_sub(i)? };
|
||||
let at = i.checked_mul(ld)?.checked_add(j)?;
|
||||
let (p, w) = (span.homog.get(at)?, span.weights.get(at)?);
|
||||
Some([p.x, p.y, p.z, *w])
|
||||
};
|
||||
let (Some(joint), Some(src)) = (row(0, 0), source.rows.first().and_then(|r| r.last())) else {
|
||||
return Err(op_err());
|
||||
};
|
||||
let scale = src[3] / joint[3];
|
||||
let mut ext = Vec::new();
|
||||
let mut piece = Vec::new();
|
||||
for (j, srow) in source.rows.iter().enumerate() {
|
||||
let (Some(&sj), Some(hj)) = (srow.last(), row(0, j)) else {
|
||||
return Err(op_err());
|
||||
};
|
||||
let (mut q, mut h, mut b) = (Vec::new(), vec![hj], vec![sj]);
|
||||
for i in 1..4 {
|
||||
let x = row(i, j).ok_or_else(op_err)?;
|
||||
let y = x.map(|c| scale * c);
|
||||
q.push(y);
|
||||
h.push(x);
|
||||
b.push(y);
|
||||
}
|
||||
ext.push(q);
|
||||
piece.push((b, h));
|
||||
}
|
||||
Ok((ext, piece, scale))
|
||||
}
|
||||
|
||||
/// Whether every foot of the span's stations is on its support's natural box.
|
||||
/// Whether every foot of the span's stations is on its support: on its natural box, or
|
||||
/// anywhere on an analytic support (plane, cone, sphere, torus). The stations are solved on
|
||||
/// the support's net continued past its knots ([`crate::blend::fillet_section::station`]);
|
||||
/// an analytic's net continued is still on the analytic (its implicit equation composed
|
||||
/// with the homogeneous net is a polynomial identity, so it holds on the continuation,
|
||||
/// wherever the weight is not zero), so a foot off its box is still on the surface. A
|
||||
/// B-spline's continuation is no longer its face's surface: a foot off its box is not on it.
|
||||
fn feet_on(k: &Kernel, pb: &ProceduralBlend, span: &DefinitionSpan) -> Result<bool> {
|
||||
for (f, support) in pb.supports().into_iter().enumerate() {
|
||||
if AnalyticSupport::of(support).is_some() {
|
||||
let finite = span.stations.iter().all(|st| {
|
||||
param(st, 2 * f, 0, 0).is_finite() && param(st, 2 * f + 1, 0, 0).is_finite()
|
||||
});
|
||||
if !finite {
|
||||
return Ok(false);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let b = support.natural_uv_domain_with_nurb(k)?;
|
||||
for st in &span.stations {
|
||||
let (u, v) = (param(st, 2 * f, 0, 0), param(st, 2 * f + 1, 0, 0));
|
||||
|
||||
@@ -17,18 +17,26 @@
|
||||
//! Two more rules (owner, 2026-11-04):
|
||||
//! - **Unbounded.** A plane's side at `+-INFINITE_PARAMETER` (the kernel's infinite plane) is
|
||||
//! left: the plane has no side there to extend past.
|
||||
//! - **Analytic kinds as themselves.** A cone, sphere or torus is extended as its own exact
|
||||
//! shape whatever the continuity asked ([`Surface::continued`], Natural): along the
|
||||
//! generator up to its apex, pole or full turn where the distance asked runs past it (the
|
||||
//! refusal's named distance, the longest that works), and said so. A surface of
|
||||
//! revolution is extended along its generator as asked, on its B-spline form row by row:
|
||||
//! its rows are one generator turned and weighted, every construction here commutes with
|
||||
//! both, so the result is the revolution of the generator extended. Round the axis is a
|
||||
//! closed direction for all of them, so a side round the axis is left.
|
||||
//! - **Analytic kinds as themselves.** A cone, sphere or torus (or an offset surface of one;
|
||||
//! a plane too for a caller keeping its trims, [`Surface::extended_sides_keeping_parameters`];
|
||||
//! otherwise a plane is widened as a plane) is extended by its own NURBS net continued Natural whatever the continuity asked, the
|
||||
//! source's net and parameters bit for bit, still on its analytic surface (certified):
|
||||
//! round its axis up to a quarter of the turn the source leaves open, along its generator
|
||||
//! never past its apex, pole or full turn, and said so where that caps the distance
|
||||
//! (`analytic`). A surface of revolution is extended along its generator as asked, on its
|
||||
//! B-spline form row by row: its rows are one generator turned and weighted, every
|
||||
//! construction here commutes with both, so the result is the revolution of the generator
|
||||
//! extended; its side round its axis is left.
|
||||
//!
|
||||
//! - **Ball patches as themselves.** A ball patch (the sphere's triangle with a pole) is
|
||||
//! extended by its own net continued Natural (the source bit for bit, every point still on
|
||||
//! the sphere, certified), round the pole up to a quarter of the turn it leaves open and
|
||||
//! away from it up to half the arc left to the antipode, its pole's side collapsed
|
||||
//! (`ball`).
|
||||
//!
|
||||
//! Deviations: none (no upstream counterpart). No recursion.
|
||||
|
||||
use super::{Continuity, ExtendRefusal, Extended};
|
||||
use super::{Continuity, Extended};
|
||||
use crate::error::{Result, SolidError};
|
||||
use crate::kernel::Kernel;
|
||||
use crate::math::INFINITE_PARAMETER;
|
||||
@@ -59,7 +67,7 @@ pub enum SideExtension {
|
||||
|
||||
/// For a cone, sphere, torus or surface of revolution, whether side `side` runs round its
|
||||
/// axis (its NURBS u is the sweep unless swapped); `None` for any other kind.
|
||||
fn round_the_axis(s: &Surface, side: BoxSide) -> Option<bool> {
|
||||
pub(super) fn round_the_axis(s: &Surface, side: BoxSide) -> Option<bool> {
|
||||
let swap = match s.kind() {
|
||||
SurfaceKind::Cone(c) => c.rev().swap_uv(),
|
||||
SurfaceKind::Sphere(sp) => sp.base.swap_uv,
|
||||
@@ -99,6 +107,48 @@ impl Surface {
|
||||
how: Continuity,
|
||||
distance: f64,
|
||||
) -> Result<ExtendedSides> {
|
||||
self.extended_sides_with(k, asked, how, distance, false)
|
||||
}
|
||||
|
||||
/// [`Surface::extended_sides`] for a caller that keeps the face's UV trims on the result
|
||||
/// (the shell's extended offset replaces a face's surface with UV trim creation off): a
|
||||
/// plane too is extended by its own net, a B-spline on the plane's own parameters (owner,
|
||||
/// 2026-11-04: the cone, sphere and torus rule), where [`Surface::extended_sides`] widens
|
||||
/// it as a plane (its kind kept, which a caller intersecting with it reads).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// As [`Surface::extended_sides`].
|
||||
pub fn extended_sides_keeping_parameters(
|
||||
&self,
|
||||
k: &Kernel,
|
||||
asked: &[BoxSide],
|
||||
how: Continuity,
|
||||
distance: f64,
|
||||
) -> Result<ExtendedSides> {
|
||||
self.extended_sides_with(k, asked, how, distance, true)
|
||||
}
|
||||
|
||||
fn extended_sides_with(
|
||||
&self,
|
||||
k: &Kernel,
|
||||
asked: &[BoxSide],
|
||||
how: Continuity,
|
||||
distance: f64,
|
||||
keeping: bool,
|
||||
) -> Result<ExtendedSides> {
|
||||
// a ball patch as its sphere (`ball`)
|
||||
if let SurfaceKind::BallPatch(patch) = self.kind() {
|
||||
if let Some(e) = super::ball::ball_extended_sides(k, self, patch, asked, distance)? {
|
||||
return Ok(e);
|
||||
}
|
||||
}
|
||||
// a cone, sphere or torus (a plane too when keeping) by its own net (`analytic`)
|
||||
if let Some(e) =
|
||||
super::analytic::analytic_extended_sides(k, self, asked, distance, keeping)?
|
||||
{
|
||||
return Ok(e);
|
||||
}
|
||||
let mut current: Option<Extended> = None;
|
||||
// the side of the current result each side of the source's box is on
|
||||
let mut on = BoxSide::ALL;
|
||||
@@ -135,44 +185,24 @@ impl Surface {
|
||||
*done = SideExtension::Unbounded;
|
||||
continue;
|
||||
}
|
||||
let revolution = round_the_axis(carrier, side);
|
||||
if revolution == Some(true) {
|
||||
// a revolution's side round its axis is left; along it, its B-spline form row by
|
||||
// row as asked, which is again a revolution (its rows are one generator turned
|
||||
// and weighted, and every construction commutes with both)
|
||||
if round_the_axis(carrier, side) == Some(true) {
|
||||
*done = SideExtension::AroundAxis;
|
||||
continue;
|
||||
}
|
||||
// an analytic label is extended as itself (Natural, its continuation); a
|
||||
// revolution's B-spline form row by row as asked, which is again a revolution
|
||||
// (its rows are one generator turned and weighted, and every construction
|
||||
// commutes with both)
|
||||
let analytic =
|
||||
revolution.is_some() && !matches!(carrier.kind(), SurfaceKind::SurfOfRevolution(_));
|
||||
let how = if analytic { Continuity::Natural } else { how };
|
||||
let (grown, capped) = match surface.extended_by_distance(k, &dom, side, how, distance) {
|
||||
Ok(g) => (g, false),
|
||||
// as itself up to its apex, pole or turn: the longest distance that works
|
||||
Err(SolidError::ExtendRefused {
|
||||
refusal:
|
||||
ExtendRefusal::PastApex | ExtendRefusal::PastPole | ExtendRefusal::OverATurn,
|
||||
distance: Some(limit),
|
||||
..
|
||||
}) if analytic && limit > 0.0 && limit < distance => (
|
||||
surface.extended_by_distance(k, &dom, side, how, limit)?,
|
||||
true,
|
||||
),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
let grown = surface.extended_by_distance(k, &dom, side, how, distance)?;
|
||||
for s in &mut on {
|
||||
*s = grown.side_of(*s);
|
||||
}
|
||||
extension = extension.max(grown.extension);
|
||||
*done = if capped {
|
||||
SideExtension::UpToLimit(grown.across)
|
||||
} else {
|
||||
SideExtension::Extended(grown.across)
|
||||
};
|
||||
*done = SideExtension::Extended(grown.across);
|
||||
current = Some(grown);
|
||||
}
|
||||
let surface = match current {
|
||||
// a Natural continuation of the net is still on an exact round's S (#2487)
|
||||
Some(e) if how == Continuity::Natural => self.carry_round_on_continuation(e.surface),
|
||||
Some(e) => e.surface,
|
||||
None => self.copy(k)?,
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::nurbs::srf_tool::p1::extract_iso_crv;
|
||||
use crate::surface::cone::Cone;
|
||||
use crate::surface::plane::Plane;
|
||||
use crate::surface::procedural_blend::ProceduralBlend;
|
||||
use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_2};
|
||||
use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_2, PI};
|
||||
|
||||
fn k() -> Kernel {
|
||||
Kernel::default()
|
||||
@@ -817,7 +817,8 @@ fn every_side_extends_in_turn_and_closed_or_collapsed_sides_are_left() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_infinite_planes_sides_are_unbounded_and_a_finite_planes_extend() {
|
||||
fn an_infinite_planes_sides_are_unbounded_and_a_finite_planes_extend_its_own_net() {
|
||||
use crate::surface::implicit::{lies_on, AnalyticSupport};
|
||||
let kk = k();
|
||||
let inf = Surface::from(Plane::from_normal(Vec3::new(0.0, 0.0, 3.0), Vec3::Z));
|
||||
let e = inf
|
||||
@@ -837,9 +838,22 @@ fn an_infinite_planes_sides_are_unbounded_and_a_finite_planes_extend() {
|
||||
Vec2::new(1.0, 1.0),
|
||||
&Extent2d::from_bounds(0.0, 0.0, 10.0, 10.0),
|
||||
));
|
||||
// widened as a plane
|
||||
let e = p
|
||||
.extended_sides(&kk, &BoxSide::ALL, Continuity::Tangent, 4.33)
|
||||
.unwrap();
|
||||
assert!(matches!(e.surface.kind(), SurfaceKind::Plane(_)));
|
||||
let kept = inf
|
||||
.extended_sides_keeping_parameters(&kk, &BoxSide::ALL, Continuity::Ruled, 4.33)
|
||||
.unwrap();
|
||||
assert!(kept
|
||||
.sides
|
||||
.iter()
|
||||
.all(|x| matches!(x, SideExtension::Unbounded)));
|
||||
// keeping its parameters
|
||||
let e = p
|
||||
.extended_sides_keeping_parameters(&kk, &BoxSide::ALL, Continuity::Tangent, 4.33)
|
||||
.unwrap();
|
||||
assert!(
|
||||
e.sides
|
||||
.iter()
|
||||
@@ -847,11 +861,45 @@ fn an_infinite_planes_sides_are_unbounded_and_a_finite_planes_extend() {
|
||||
"{:?}",
|
||||
e.sides
|
||||
);
|
||||
assert!(matches!(e.surface.kind(), SurfaceKind::Plane(_)));
|
||||
// its own net continued: a B-spline on the plane's own parameters, on the plane
|
||||
let SurfaceKind::BSpline(grown) = e.surface.kind() else {
|
||||
panic!("not a B-spline: {:?}", e.surface.kind());
|
||||
};
|
||||
let on = AnalyticSupport::check(&kk, &p).unwrap().implicit();
|
||||
assert!(lies_on(grown, &on).unwrap());
|
||||
// one straight span each way (its joint knots removed): the plane's own 2 x 2 form
|
||||
assert_eq!(grown.gw_nurb().unwrap().cpts().len(), 2);
|
||||
assert_eq!(grown.gw_nurb().unwrap().u_knots().len(), 4);
|
||||
for (u, v) in [(0.0, 0.0), (10.0, 0.0), (3.5, 7.25), (-4.0, 14.0)] {
|
||||
let uv = Point2::new(u, v);
|
||||
// the plane's own parameters: origin + u X + v Y at scale 1
|
||||
let at = Vec3::new(u, v, 0.0);
|
||||
assert!(
|
||||
(grown.evaluate_point(uv).unwrap() - at).length() <= 1e-13,
|
||||
"{uv:?}"
|
||||
);
|
||||
}
|
||||
// the across length is the distance on every side, in the plane's own parameters
|
||||
let d = grown.gw_nurb().unwrap();
|
||||
// (the extension parameter solved to 1e-12 of the distance)
|
||||
let ends = |k: &[f64]| (k[0], *k.last().unwrap());
|
||||
for (lo, hi) in [ends(d.u_knots()), ends(d.v_knots())] {
|
||||
assert!(
|
||||
(lo + 4.33).abs() <= 1e-9 && (hi - 14.33).abs() <= 1e-9,
|
||||
"{lo} {hi}"
|
||||
);
|
||||
}
|
||||
for a in e.sides {
|
||||
let SideExtension::Extended(a) = a else {
|
||||
unreachable!()
|
||||
};
|
||||
assert!((a.enclosure.mid() - 4.33).abs() <= 1e-9 * 4.33, "{a:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cone_extends_as_itself_along_up_to_its_apex_and_is_left_round_its_axis() {
|
||||
fn a_cone_extends_its_own_net_along_up_to_its_apex_and_round_its_axis_a_quarter_of_its_open_turn() {
|
||||
use crate::surface::implicit::{lies_on, AnalyticSupport};
|
||||
let kk = k();
|
||||
// radius 2 at the bottom, 1 at height 1: the apex at height 2, sqrt 2 slant past the top
|
||||
let pl = Axis2Placement::new(Vec3::ZERO, Vec3::Z, Vec3::X);
|
||||
@@ -859,31 +907,73 @@ fn a_cone_extends_as_itself_along_up_to_its_apex_and_is_left_round_its_axis() {
|
||||
c.adjust_step_uv_domain(&kk, &Extent2d::from_bounds(0.0, 0.0, 90.0, 1.0))
|
||||
.unwrap();
|
||||
let c = Surface::from(c);
|
||||
let on = AnalyticSupport::check(&kk, &c).unwrap().implicit();
|
||||
let src = crate::blend::fillet_section::station::as_b_spline(&kk, &c)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into_owned();
|
||||
let src_net = src.gw_nurb().unwrap().clone();
|
||||
for how in [Continuity::Tangent, Continuity::Natural] {
|
||||
let e = c.extended_sides(&kk, &BoxSide::ALL, how, 5.0).unwrap();
|
||||
assert!(matches!(e.surface.kind(), SurfaceKind::Cone(_)), "{how:?}");
|
||||
let around = e
|
||||
.sides
|
||||
.iter()
|
||||
.filter(|x| matches!(x, SideExtension::AroundAxis))
|
||||
.count();
|
||||
assert_eq!(around, 2, "{:?}", e.sides);
|
||||
// one side along runs the whole 5, the other stops short of the apex
|
||||
let mut lengths: Vec<(bool, f64)> = e
|
||||
.sides
|
||||
.iter()
|
||||
.filter_map(|x| match x {
|
||||
SideExtension::Extended(a) => Some((false, a.enclosure.mid())),
|
||||
SideExtension::UpToLimit(a) => Some((true, a.enclosure.mid())),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let SurfaceKind::BSpline(grown) = e.surface.kind() else {
|
||||
panic!("{how:?}: not a B-spline");
|
||||
};
|
||||
// the source's circles' points and knots bit for bit (along the rulings the span is
|
||||
// widened, one straight span: its end circles to rounding), the result on the cone
|
||||
let out = grown.gw_nurb().unwrap();
|
||||
let (src_u, src_v) = (src_net.u_knots(), src_net.v_knots());
|
||||
assert!(src_u.iter().all(|x| out.u_knots().contains(x)), "{how:?}");
|
||||
assert_eq!(out.v_knots().len(), 4, "{how:?}: one straight span along");
|
||||
assert_eq!(out.cpts().len(), src_net.cpts().len() + 4, "{how:?}");
|
||||
for i in 0..=4 {
|
||||
for j in 0..=4 {
|
||||
let uv = Point2::new(
|
||||
src_u[0] + f64::from(i) / 4.0 * (src_u.last().unwrap() - src_u[0]),
|
||||
src_v[0] + f64::from(j) / 4.0 * (src_v.last().unwrap() - src_v[0]),
|
||||
);
|
||||
let d = grown.evaluate_point(uv).unwrap() - src.evaluate_point(uv).unwrap();
|
||||
assert!(d.length() <= 1e-14, "{how:?} {uv:?}: {d:?}");
|
||||
}
|
||||
}
|
||||
assert!(lies_on(grown, &on).unwrap());
|
||||
// round the axis (u): each side a quarter of the 270 degrees left open, said so: the
|
||||
// radius at the side's middle (1.5) times 3 pi / 8
|
||||
for i in [0, 1] {
|
||||
let SideExtension::UpToLimit(a) = e.sides[i] else {
|
||||
panic!("{:?}", e.sides);
|
||||
};
|
||||
let want = 1.5 * 0.375 * PI;
|
||||
assert!((a.enclosure.mid() - want).abs() <= 1e-9 * want, "{a:?}");
|
||||
}
|
||||
// round the axis each end turned 67.5 degrees from the source's
|
||||
let (d, sd) = (grown.gw_nurb().unwrap(), src_net.clone());
|
||||
let ends =
|
||||
|n: &crate::nurbs::srf_basic::Surface| (n.u_knots()[0], *n.u_knots().last().unwrap());
|
||||
let vm = 0.5 * (sd.v_knots()[0] + *sd.v_knots().last().unwrap());
|
||||
let ((u0, u1), (s0, s1)) = (ends(d), ends(&sd));
|
||||
let crate::surface::implicit::Implicit::Cone { origin, axis, .. } = on else {
|
||||
panic!("not a cone");
|
||||
};
|
||||
let flat = |s: &BSplineSurface, u: f64| {
|
||||
let p = s.evaluate_point(Point2::new(u, vm)).unwrap() - origin;
|
||||
p - axis * axis.dot(p)
|
||||
};
|
||||
let deg = |a: Vec3, b: Vec3| a.cross(b).length().atan2(a.dot(b)).to_degrees();
|
||||
assert!((deg(flat(grown, u0), flat(&src, s0)) - 67.5).abs() <= 1e-6);
|
||||
assert!((deg(flat(grown, u1), flat(&src, s1)) - 67.5).abs() <= 1e-6);
|
||||
// along: one side runs the whole 5, the other stops at the apex
|
||||
let along = |x: &SideExtension| match x {
|
||||
SideExtension::Extended(a) => Some((false, a.enclosure.mid())),
|
||||
SideExtension::UpToLimit(a) => Some((true, a.enclosure.mid())),
|
||||
_ => None,
|
||||
};
|
||||
let mut lengths: Vec<(bool, f64)> = e.sides[2..].iter().filter_map(along).collect();
|
||||
lengths.sort_by(|a, b| a.1.total_cmp(&b.1));
|
||||
let [(true, short), (false, long)] = lengths[..] else {
|
||||
panic!("{:?}", e.sides);
|
||||
};
|
||||
assert!((long - 5.0).abs() <= 1e-8, "{long}");
|
||||
assert!(short < 2f64.sqrt() && short > 2f64.sqrt() - 1e-6, "{short}");
|
||||
assert!((long - 5.0).abs() <= 1e-9 * 5.0, "{long}");
|
||||
assert!((short - 2f64.sqrt()).abs() <= 1e-9, "{short}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1012,6 +1102,11 @@ fn ruled_with_end_weight_ratios_that_differ_is_c1_and_straight_up_to_its_reach()
|
||||
/// 1e-3); with `certified`, on its own net and bound ([`net_of_definition`]). The planes
|
||||
/// run to x = 20, so the feet have room along.
|
||||
fn round_blend(order: u32, certified: bool) -> (Kernel, ProceduralBlend) {
|
||||
round_blend_with(order, certified, 0.5)
|
||||
}
|
||||
|
||||
/// [`round_blend`] with the G^n section's thumbweight rule `thumbweight`.
|
||||
fn round_blend_with(order: u32, certified: bool, thumbweight: f64) -> (Kernel, ProceduralBlend) {
|
||||
use crate::blend::fillet_section::recertify::net_of_definition;
|
||||
use crate::blend::fillet_section::section::GnSectionFsg;
|
||||
use crate::geom::vector3d::Point3;
|
||||
@@ -1051,7 +1146,7 @@ fn round_blend(order: u32, certified: bool) -> (Kernel, ProceduralBlend) {
|
||||
offsets: [r, r],
|
||||
law: None,
|
||||
order,
|
||||
thumbweight: if order >= 2 { 0.5 } else { 0.0 },
|
||||
thumbweight: if order >= 2 { thumbweight } else { 0.0 },
|
||||
leader: &leader,
|
||||
seeds: [&seed, &seed],
|
||||
bound: 1e-3,
|
||||
@@ -1168,16 +1263,148 @@ fn a_round_continues_along_its_spine_by_a_span_of_its_definition() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gn_section_across_is_refused() {
|
||||
let (kk, pb) = round_blend(2, false);
|
||||
let s = Surface::from(pb);
|
||||
fn a_side_inside_the_spine_runs_the_definitions_spans_then_past_its_end() {
|
||||
use crate::nurbs::b_spline_surface::SurfParamType;
|
||||
let (kk, pb) = round_blend(1, true);
|
||||
// the half of the round up to x = 5 (u = 1/2, inside the spine's one span)
|
||||
let (left, _) = pb.split_at(&kk, 0.5, SurfParamType::U).unwrap();
|
||||
let s = Surface::from(left.clone());
|
||||
let d0 = s.natural_uv_domain_with_nurb(&kk).unwrap();
|
||||
match s.extended_by_distance(&kk, &d0, BoxSide::VMax, Continuity::Tangent, 0.1) {
|
||||
Err(SolidError::Unsupported { .. }) => {}
|
||||
other => panic!("{other:?}"),
|
||||
assert!((d0.max().x - 0.5).abs() < 1e-15, "{d0:?}");
|
||||
for (d, ue, stations) in [(3.0, 0.8, 2), (8.0, 1.3, 3)] {
|
||||
let e = s
|
||||
.extended_by_distance(&kk, &d0, BoxSide::UMax, Continuity::Tangent, d)
|
||||
.unwrap();
|
||||
meets(&e, d);
|
||||
let SurfaceKind::ProceduralBlend(out) = e.surface.kind() else {
|
||||
panic!("not a blend: {:?}", e.surface.kind());
|
||||
};
|
||||
let dd = e.surface.natural_uv_domain_with_nurb(&kk).unwrap();
|
||||
assert!((dd.max().x - ue).abs() < 1e-6, "{d}: {dd:?}");
|
||||
assert!(
|
||||
out.bound() >= left.bound() && out.bound() < 1e-6,
|
||||
"{}",
|
||||
out.bound()
|
||||
);
|
||||
for i in 0..=8 {
|
||||
for j in 0..=8 {
|
||||
let u = 0.5 + (dd.max().x - 0.5) * f64::from(i) / 8.0;
|
||||
let v = f64::from(j) / 8.0;
|
||||
let p = e.surface.evaluate_point(&kk, Vec2::new(u, v)).unwrap();
|
||||
let st = out.definition_parameters(Point2::new(u, v));
|
||||
let def = out.evaluate_definition(&kk, st.x, st.y, 0, 0).unwrap()[0][0];
|
||||
assert!(p.distance(def) <= out.bound(), "{u} {v}: {p:?} {def:?}");
|
||||
let rad = (p.y - 2.0).hypot(p.z - 2.0);
|
||||
assert!((rad - 2.0).abs() <= out.bound() + 1e-12, "{p:?}");
|
||||
}
|
||||
}
|
||||
// inside the spine the definition is its own; past its end it gains a station
|
||||
let sigma = out.definition().seeds[0].knots(None).unwrap().0;
|
||||
assert_eq!(sigma.len(), stations, "{d}: {sigma:?}");
|
||||
let src = left.net.gw_nurb().unwrap();
|
||||
assert!(keeps(src, out.net.gw_nurb().unwrap(), BoxSide::UMax).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gn_section_continues_past_a_foot_with_its_bound_grown_over_its_weight_in_closed_form() {
|
||||
// the G^2 section at the constant thumbweight 1/2
|
||||
let (kk, pb) = round_blend(2, true);
|
||||
let s = Surface::from(pb.clone());
|
||||
let d0 = s.natural_uv_domain_with_nurb(&kk).unwrap();
|
||||
let e = s
|
||||
.extended_by_distance(&kk, &d0, BoxSide::VMax, Continuity::Tangent, 0.3)
|
||||
.unwrap();
|
||||
meets(&e, 0.3);
|
||||
let SurfaceKind::ProceduralBlend(out) = e.surface.kind() else {
|
||||
panic!("not a blend: {:?}", e.surface.kind());
|
||||
};
|
||||
let d = e.surface.natural_uv_domain_with_nurb(&kk).unwrap();
|
||||
let te = d.max().y;
|
||||
assert!(te > 1.0 && te < 2.0, "{d:?}");
|
||||
// the growth in closed form: T_m(2 te - 1), m = 3 n + 1 + the net's degree across,
|
||||
// times the net's weight ratio, over W's least past the foot: A >= 1, B >= 1 and
|
||||
// C = 1 - te / 2 at the end (W at most 1 on the source)
|
||||
let src = pb.net.gw_nurb().unwrap();
|
||||
let m = 7 + src.q();
|
||||
let x = 2.0 * te - 1.0;
|
||||
let (mut prev, mut cur) = (1.0_f64, x);
|
||||
for _ in 1..m {
|
||||
let next = 2.0 * x * cur - prev;
|
||||
prev = cur;
|
||||
cur = next;
|
||||
}
|
||||
let ws = |n: &crate::nurbs::srf_basic::Surface| {
|
||||
let w: Vec<f64> = n
|
||||
.cpts()
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|c| if c.w == NOW { 1.0 } else { c.w })
|
||||
.collect();
|
||||
(
|
||||
w.iter().copied().fold(0.0_f64, f64::max),
|
||||
w.iter().copied().fold(f64::INFINITY, f64::min),
|
||||
)
|
||||
};
|
||||
let (vhi, _) = ws(src);
|
||||
let (_, vlo) = ws(out.net.gw_nurb().unwrap());
|
||||
let c = 1.0 - 0.5 * te;
|
||||
let want = pb.bound() * cur * vhi / vlo / (c * c);
|
||||
assert!(
|
||||
out.bound() >= want && out.bound() <= want * (1.0 + 1e-9) + 1e-15 + e.extension,
|
||||
"{} vs {want}",
|
||||
out.bound()
|
||||
);
|
||||
// the continued part within the bound of the definition continued
|
||||
for i in 0..=8 {
|
||||
for j in 0..=8 {
|
||||
let u = f64::from(i) / 8.0;
|
||||
let v = 1.0 + (te - 1.0) * f64::from(j) / 8.0;
|
||||
let p = e.surface.evaluate_point(&kk, Vec2::new(u, v)).unwrap();
|
||||
let st = out.definition_parameters(Point2::new(u, v));
|
||||
let def = out.evaluate_definition(&kk, st.x, st.y, 0, 0).unwrap()[0][0];
|
||||
assert!(p.distance(def) <= out.bound(), "{u} {v}: {p:?} {def:?}");
|
||||
}
|
||||
}
|
||||
// the source part is the source's net, bit for bit
|
||||
assert!(keeps(src, out.net.gw_nurb().unwrap(), BoxSide::VMax).unwrap());
|
||||
// a long ask stays short of the section's weight's zero (C = 0 at t = 2): met there
|
||||
// with the bound grown by its closed form, or refused
|
||||
match s.extended_by_distance(&kk, &d0, BoxSide::VMax, Continuity::Tangent, 50.0) {
|
||||
Ok(long) => {
|
||||
let d = long.surface.natural_uv_domain_with_nurb(&kk).unwrap();
|
||||
assert!(d.max().y < 2.0, "{d:?}");
|
||||
}
|
||||
Err(SolidError::ExtendRefused { .. }) => {}
|
||||
Err(other) => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_equal_area_gn_section_continues_over_its_thumbweights_proved_enclosure() {
|
||||
use crate::blend::fillet_section::recertify::recertify_with_thumbweight;
|
||||
use crate::blend::fillet_section::section::GN_THUMBWEIGHT_EQUAL_AREA;
|
||||
let (kk, pb) = round_blend_with(2, true, GN_THUMBWEIGHT_EQUAL_AREA);
|
||||
// the certificate's interval Newton roots enclose the thumbweight: one w along this
|
||||
// straight round, below 1
|
||||
let (_, w) = recertify_with_thumbweight(&kk, &pb).unwrap();
|
||||
let [lo, hi] = w.unwrap();
|
||||
assert!(lo <= hi && hi < 1.0 && hi - lo < 1e-6, "{lo} {hi}");
|
||||
let s = Surface::from(pb.clone());
|
||||
let d0 = s.natural_uv_domain_with_nurb(&kk).unwrap();
|
||||
let e = s
|
||||
.extended_by_distance(&kk, &d0, BoxSide::VMax, Continuity::Tangent, 0.3)
|
||||
.unwrap();
|
||||
meets(&e, 0.3);
|
||||
let SurfaceKind::ProceduralBlend(out) = e.surface.kind() else {
|
||||
panic!("not a blend");
|
||||
};
|
||||
assert!(out.bound().is_finite() && out.bound() >= pb.bound());
|
||||
// a constant thumbweight is its own
|
||||
let (kk, pb) = round_blend(2, true);
|
||||
assert!(recertify_with_thumbweight(&kk, &pb).unwrap().1.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_round_along_past_its_supports_is_refused_naming_a_distance_that_works() {
|
||||
// the planes run to x = 20 and the spine ends at x = 10: past x = 20 a foot leaves its
|
||||
@@ -1197,3 +1424,66 @@ fn a_round_along_past_its_supports_is_refused_naming_a_distance_that_works() {
|
||||
.unwrap();
|
||||
meets(&e, named);
|
||||
}
|
||||
|
||||
/// The octant ball patch of radius 2 about `(1, -2, 0.5)`, its pole on +x.
|
||||
fn octant_ball() -> (Surface, Vec3, f64) {
|
||||
let (c, r) = (Vec3::new(1.0, -2.0, 0.5), 2.0);
|
||||
let dirs = [Vec3::X, Vec3::Y, Vec3::Z];
|
||||
let (net, _, _) =
|
||||
crate::blend::blend_corner::ball::ball_triangle_patch(c, r, &dirs, 0, false).unwrap();
|
||||
let patch = crate::surface::ball_patch::BallPatch::create(&net, c, r).unwrap();
|
||||
(Surface::from(patch), c, r)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_ball_patch_extends_its_own_net_on_its_sphere_up_to_its_caps() {
|
||||
use crate::surface::implicit::{lies_on, Implicit};
|
||||
let kk = k();
|
||||
let (s, c, r) = octant_ball();
|
||||
let SurfaceKind::BallPatch(src) = s.kind() else {
|
||||
panic!("not a ball patch");
|
||||
};
|
||||
let src_net = src.net().gw_nurb().unwrap().clone();
|
||||
// a short ask is met on every side but the pole's; a long one stops at the caps: round
|
||||
// the pole r times a quarter of the 270 degrees left open, away half the 90 degrees
|
||||
// left from the third side to the antipode
|
||||
for (d, round, away) in [(0.1, 0.1, 0.1), (100.0, r * 0.375 * PI, r * 0.25 * PI)] {
|
||||
let e = s
|
||||
.extended_sides(&kk, &BoxSide::ALL, Continuity::Ruled, d)
|
||||
.unwrap();
|
||||
let SurfaceKind::BallPatch(out) = e.surface.kind() else {
|
||||
panic!("{d}: not a ball patch");
|
||||
};
|
||||
assert_eq!(e.sides[2], SideExtension::Collapsed, "{:?}", e.sides);
|
||||
for (i, want) in [(0, round), (1, round), (3, away)] {
|
||||
let a = match e.sides[i] {
|
||||
SideExtension::Extended(a) if d < 1.0 => a,
|
||||
SideExtension::UpToLimit(a) if d > 1.0 => a,
|
||||
other => panic!("{d}: side {i}: {other:?}"),
|
||||
};
|
||||
assert!(
|
||||
(a.enclosure.mid() - want).abs() <= 1e-9 * want,
|
||||
"{d}: {i}: {a:?}"
|
||||
);
|
||||
}
|
||||
// the source's net bit for bit, the whole result on the sphere
|
||||
let out_net = out.net().gw_nurb().unwrap();
|
||||
let kept = src_net.cpts().iter().flatten().all(|p| {
|
||||
out_net
|
||||
.cpts()
|
||||
.iter()
|
||||
.flatten()
|
||||
.any(|q| p.x == q.x && p.y == q.y && p.z == q.z && p.w == q.w)
|
||||
});
|
||||
assert!(kept, "{d}: the source's net moved");
|
||||
assert!(lies_on(
|
||||
out.net(),
|
||||
&Implicit::Sphere {
|
||||
centre: c,
|
||||
radius: r
|
||||
}
|
||||
)
|
||||
.unwrap());
|
||||
assert!((out.radius() - r).abs() == 0.0 && out.centre() == c);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ use crate::geom::vector2d::Point2;
|
||||
use crate::geom::vector3d::Vec3;
|
||||
use crate::kernel::Kernel;
|
||||
use crate::nurbs::b_spline_surface::{BSplineSurface, IsoCurveOptions, SurfParamType};
|
||||
use crate::nurbs::math::{gamma, NOW};
|
||||
use crate::nurbs::math::{gamma, CPoint, NOW};
|
||||
use crate::nurbs::srf_basic::Surface as Net;
|
||||
use crate::surface::implicit::AnalyticSupport;
|
||||
use crate::surface::surface::{Surface, SurfaceKind};
|
||||
|
||||
@@ -137,6 +138,46 @@ impl Bracket {
|
||||
}
|
||||
}
|
||||
|
||||
/// `form`'s net translated by `P0` (the iso's first point, as computed), and the
|
||||
/// translation's rounding: each homogeneous point `(X w - P0 w, w)`, its coordinates within
|
||||
/// `gamma_2 (|x| + |P0| |w|)` of the exact translation's (a product and a difference; the
|
||||
/// weights exact). A length is the same on the translated surface (`S - P0`, `P0` a fixed
|
||||
/// vector); measured there, the points' and their rounding's magnitudes are the net's size
|
||||
/// about `P0`, not its distance from the origin.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `SM_ERR` for a form without a NURBS net; those of the net.
|
||||
fn translated(form: &BSplineSurface, p0: Vec3, op: &'static str) -> Result<(BSplineSurface, f64)> {
|
||||
let nurb = form.gw_nurb().ok_or_else(|| SolidError::sm_err(op))?;
|
||||
let cpts = nurb.cpts();
|
||||
let size = p0.x.abs().max(p0.y.abs()).max(p0.z.abs());
|
||||
let g = gamma(2);
|
||||
let mut shift = 0.0_f64;
|
||||
let net: Vec<Vec<CPoint>> = cpts
|
||||
.iter()
|
||||
.map(|row| {
|
||||
row.iter()
|
||||
.map(|c| {
|
||||
let w = if c.w == NOW { 1.0 } else { c.w };
|
||||
let mag = c.x.abs().max(c.y.abs()).max(c.z.abs());
|
||||
shift = shift.max(g * (mag + size * w.abs()));
|
||||
CPoint::new(c.x - p0.x * w, c.y - p0.y * w, c.z - p0.z * w, c.w)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
let (p, q) = nurb.degrees();
|
||||
let out = Net::new(net, p, q, nurb.u_knots().to_vec(), nurb.v_knots().to_vec())?;
|
||||
if !shift.is_finite() {
|
||||
return Err(SolidError::invalid_input(
|
||||
op,
|
||||
"the net's translation is not bounded",
|
||||
));
|
||||
}
|
||||
Ok((BSplineSurface::from_gw_nurb(&out), shift))
|
||||
}
|
||||
|
||||
/// A rational Bezier piece in homogeneous coordinates `(w x, w y, w z, w)`.
|
||||
type Piece = Vec<[f64; 4]>;
|
||||
|
||||
@@ -180,21 +221,156 @@ struct Rounding {
|
||||
homogeneous: f64,
|
||||
}
|
||||
|
||||
/// The largest knot magnitude and the least nonzero span of the knots of `kv` (the full
|
||||
/// vector) from `margin` indices under the last knot below `a` to `margin` over the first
|
||||
/// above `b`: what the knot insertions and the Bezier decomposition that make the pieces
|
||||
/// over `[a, b]` read of `kv` (a segment's coefficients `(t - u_i) / (u_(i+p) - u_i)` take
|
||||
/// `i` from its start group's last index, the one before it for its first points, to `p`
|
||||
/// past it; the iso's extraction the knots within `q` of its span).
|
||||
fn knot_window(kv: &[f64], a: f64, b: f64, margin: usize) -> (f64, f64) {
|
||||
let lo = kv
|
||||
.iter()
|
||||
.position(|&x| x > a)
|
||||
.unwrap_or(kv.len())
|
||||
.saturating_sub(margin);
|
||||
let hi = kv
|
||||
.iter()
|
||||
.rposition(|&x| x < b)
|
||||
.unwrap_or(0)
|
||||
.saturating_add(margin);
|
||||
let (mut kmax, mut smin) = (0.0_f64, f64::INFINITY);
|
||||
let window: Vec<f64> = kv
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|&(i, _)| i >= lo && i <= hi)
|
||||
.map(|(_, &x)| x)
|
||||
.collect();
|
||||
for (&x, &y) in window.iter().zip(window.iter().skip(1)) {
|
||||
kmax = kmax.max(x.abs()).max(y.abs());
|
||||
if y > x {
|
||||
smin = smin.min(y - x);
|
||||
}
|
||||
}
|
||||
(kmax, smin)
|
||||
}
|
||||
|
||||
/// [`knot_window`] for the iso's extraction at `t` of a direction of degree `q` (full knot
|
||||
/// vector `kv`): knot insertion at `t` in its span `[u_s, u_(s+1))` forms its coefficients
|
||||
/// `(t - u_i) / (u_(i+q-j+1) - u_i)` from the knots `u_(s-q+1) .. u_(s+q)` only.
|
||||
fn extraction_window(kv: &[f64], t: f64, q: usize) -> (f64, f64) {
|
||||
let s = kv.iter().rposition(|&x| x <= t).unwrap_or(0);
|
||||
let (lo, hi) = (s.saturating_sub(q), s.saturating_add(q));
|
||||
let window: Vec<f64> = kv
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|&(i, _)| i >= lo && i <= hi)
|
||||
.map(|(_, &x)| x)
|
||||
.collect();
|
||||
let (mut kmax, mut smin) = (t.abs(), f64::INFINITY);
|
||||
for (&x, &y) in window.iter().zip(window.iter().skip(1)) {
|
||||
kmax = kmax.max(x.abs()).max(y.abs());
|
||||
if y > x {
|
||||
smin = smin.min(y - x);
|
||||
}
|
||||
}
|
||||
(kmax, smin)
|
||||
}
|
||||
|
||||
/// [`knot_window`] for the Bezier decomposition along the pieces over `[a, b]` of a curve of
|
||||
/// degree `p` (full knot vector `kv`): the decomposition forms a coefficient only where it
|
||||
/// inserts, at an interior knot of multiplicity below `p`, and each of its denominators
|
||||
/// `u_(i+j) - u_i` there is at least the length of the segment that knot ends. So the least
|
||||
/// span read is the least such segment's in the window; a window whose interior knots are all
|
||||
/// of multiplicity `p` or more (Bezier joints already) forms none (`INFINITY`).
|
||||
fn decomposed_window(kv: &[f64], a: f64, b: f64, p: usize) -> (f64, f64) {
|
||||
let margin = p.saturating_add(2);
|
||||
let (kmax, _) = knot_window(kv, a, b, margin);
|
||||
let lo = kv
|
||||
.iter()
|
||||
.position(|&x| x > a)
|
||||
.unwrap_or(kv.len())
|
||||
.saturating_sub(margin);
|
||||
let hi = kv
|
||||
.iter()
|
||||
.rposition(|&x| x < b)
|
||||
.unwrap_or(0)
|
||||
.saturating_add(margin);
|
||||
// the distinct knots in the window with their multiplicities in the whole vector
|
||||
let mut groups: Vec<(f64, usize)> = Vec::new();
|
||||
for (i, &x) in kv.iter().enumerate() {
|
||||
match groups.last_mut() {
|
||||
Some((y, m)) if *y == x => *m = m.saturating_add(1),
|
||||
_ if i >= lo && i <= hi => groups.push((x, 1)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let (first, last) = (kv.first().copied(), kv.last().copied());
|
||||
let mut smin = f64::INFINITY;
|
||||
for (&(x, _), &(y, my)) in groups.iter().zip(groups.iter().skip(1)) {
|
||||
let interior = Some(y) != first && Some(y) != last;
|
||||
if interior && my < p && y > x {
|
||||
smin = smin.min(y - x);
|
||||
}
|
||||
}
|
||||
(kmax, smin)
|
||||
}
|
||||
|
||||
/// The control indices of degree `deg` on the knots `k` whose basis function's support
|
||||
/// `[k_i, k_(i + deg + 1)]` meets `[lo, hi]` (closed: a support ending at `lo` is kept).
|
||||
fn support_meets(k: &[f64], deg: usize, lo: f64, hi: f64) -> std::ops::RangeInclusive<usize> {
|
||||
let count = k.len().saturating_sub(deg + 1);
|
||||
let meets = |i: usize| matches!((k.get(i), k.get(i + deg + 1)), (Some(&a), Some(&b)) if a <= hi && b >= lo);
|
||||
let first = (0..count).find(|&i| meets(i));
|
||||
let last = (0..count).rev().find(|&i| meets(i));
|
||||
match (first, last) {
|
||||
(Some(f), Some(l)) => f..=l,
|
||||
// none: every point, as the whole net's bound
|
||||
_ => 0..=count.saturating_sub(1),
|
||||
}
|
||||
}
|
||||
|
||||
impl Rounding {
|
||||
/// [`Rounding::of`] for the pieces of the iso at `fixed` (constant v for `along_u`)
|
||||
/// over `[from, to]` alone: the knots read are only those near them. Knot insertion and
|
||||
/// the Bezier decomposition are local: a segment's points are combinations whose
|
||||
/// coefficients take the knots from its start's segment's start to `p` past its end
|
||||
/// (`(t - u_i) / (u_(i+p) - u_i)`, i from the segment's start index), and the iso's
|
||||
/// extraction at `fixed` the knots within `q` of it. A span far from the pieces (a sliver
|
||||
/// at the net's other end) does not touch them.
|
||||
fn local(
|
||||
form: &BSplineSurface,
|
||||
along_u: bool,
|
||||
fixed: f64,
|
||||
from: f64,
|
||||
to: f64,
|
||||
op: &'static str,
|
||||
) -> Result<Self> {
|
||||
let nurb = form.gw_nurb().ok_or_else(|| SolidError::sm_err(op))?;
|
||||
let (p, q) = nurb.degrees();
|
||||
let (held, along, dp, dq) = if along_u {
|
||||
(nurb.v_knots(), nurb.u_knots(), p, q)
|
||||
} else {
|
||||
(nurb.u_knots(), nurb.v_knots(), q, p)
|
||||
};
|
||||
let (a, b) = (from.min(to), from.max(to));
|
||||
// each direction's coefficients read its own knots and parameters
|
||||
let (k1, s1) = decomposed_window(along, a, b, dp);
|
||||
let (k2, s2) = extraction_window(held, fixed, dq);
|
||||
let ratio = (k1.max(from.abs()).max(to.abs()) / s1).max(k2 / s2);
|
||||
// and only the control points the pieces are made of: those whose basis function's
|
||||
// support meets `[from, to]` along and holds `fixed` across (a far row's size, the
|
||||
// other end of a long spine, enters no piece)
|
||||
let along_ix = support_meets(along, dp, a, b);
|
||||
let held_ix = support_meets(held, dq, fixed, fixed);
|
||||
let keep = |i: usize, j: usize| {
|
||||
let (ia, ih) = if along_u { (i, j) } else { (j, i) };
|
||||
along_ix.contains(&ia) && held_ix.contains(&ih)
|
||||
};
|
||||
Self::with_ratio_in(nurb, ratio, op, &keep)
|
||||
}
|
||||
|
||||
fn of(form: &BSplineSurface, extra: &[f64], op: &'static str) -> Result<Self> {
|
||||
let nurb = form.gw_nurb().ok_or_else(|| SolidError::sm_err(op))?;
|
||||
let (mut h, mut wmin, mut wmax) = (0.0_f64, f64::INFINITY, 0.0_f64);
|
||||
for row in nurb.rows() {
|
||||
for c in row {
|
||||
let w = if c.w == NOW { 1.0 } else { c.w };
|
||||
if w.is_nan() || w <= 0.0 {
|
||||
return Err(SolidError::invalid_input(op, "a weight is not positive"));
|
||||
}
|
||||
h = h.max(c.x.abs()).max(c.y.abs()).max(c.z.abs());
|
||||
wmin = wmin.min(w);
|
||||
wmax = wmax.max(w);
|
||||
}
|
||||
}
|
||||
let (mut kmax, mut smin) = (0.0_f64, f64::INFINITY);
|
||||
for kv in [nurb.u_knots(), nurb.v_knots()] {
|
||||
for (&a, &b) in kv.iter().zip(kv.iter().skip(1)) {
|
||||
@@ -207,7 +383,45 @@ impl Rounding {
|
||||
for &x in extra {
|
||||
kmax = kmax.max(x.abs());
|
||||
}
|
||||
let level = gamma(3) * (1.0 + 6.0 * kmax / smin) * 2.0;
|
||||
Self::with_ratio(nurb, kmax / smin, op)
|
||||
}
|
||||
|
||||
/// The rounding of a net whose coefficients' knots and parameters are at most `ratio`
|
||||
/// times the least span they divide by (`K / S`).
|
||||
fn with_ratio(
|
||||
nurb: &crate::nurbs::srf_basic::Surface,
|
||||
ratio: f64,
|
||||
op: &'static str,
|
||||
) -> Result<Self> {
|
||||
Self::with_ratio_in(nurb, ratio, op, &|_, _| true)
|
||||
}
|
||||
|
||||
/// [`Rounding::with_ratio`] over the control points `keep` names (`[i along u][j along
|
||||
/// v]`): the pieces' points are combinations of those alone.
|
||||
fn with_ratio_in(
|
||||
nurb: &crate::nurbs::srf_basic::Surface,
|
||||
ratio: f64,
|
||||
op: &'static str,
|
||||
keep: &dyn Fn(usize, usize) -> bool,
|
||||
) -> Result<Self> {
|
||||
let (mut h, mut wmin, mut wmax) = (0.0_f64, f64::INFINITY, 0.0_f64);
|
||||
for (i, row) in nurb.rows().enumerate() {
|
||||
for c in row
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|&(j, _)| keep(i, j))
|
||||
.map(|(_, c)| c)
|
||||
{
|
||||
let w = if c.w == NOW { 1.0 } else { c.w };
|
||||
if w.is_nan() || w <= 0.0 {
|
||||
return Err(SolidError::invalid_input(op, "a weight is not positive"));
|
||||
}
|
||||
h = h.max(c.x.abs()).max(c.y.abs()).max(c.z.abs());
|
||||
wmin = wmin.min(w);
|
||||
wmax = wmax.max(w);
|
||||
}
|
||||
}
|
||||
let level = gamma(3) * (1.0 + 6.0 * ratio) * 2.0;
|
||||
let scale = h * wmax / (wmin * wmin);
|
||||
if level.is_finite() && scale.is_finite() {
|
||||
Ok(Self {
|
||||
@@ -565,10 +779,18 @@ impl Surface {
|
||||
converged: true,
|
||||
});
|
||||
}
|
||||
let rounding = Rounding::of(&form, &[fixed, from, to], OP_LENGTH)?;
|
||||
let start = if side.is_u() {
|
||||
Point2::new(from, fixed)
|
||||
} else {
|
||||
Point2::new(fixed, from)
|
||||
};
|
||||
let (form, shift) = translated(&form, form.evaluate_point(start)?, OP_LENGTH)?;
|
||||
let rounding = Rounding::local(&form, side.is_u(), fixed, from, to, OP_LENGTH)?;
|
||||
let (pieces, levels) = iso_pieces(k, &form, side.is_u(), fixed, from, to)?;
|
||||
// the pieces' homogeneous points as computed, within this of the exact ones
|
||||
let made = rounding.homogeneous(levels);
|
||||
// the pieces' homogeneous points as computed, within this of the exact ones (of the
|
||||
// net translated exactly), the translation's rounding carried through the levels'
|
||||
// convex combinations unchanged
|
||||
let made = rounding.homogeneous(levels) + shift;
|
||||
let (mut chords, mut polygons, mut leaves) = (0.0_f64, 0.0_f64, 0_usize);
|
||||
let (mut chord_cuts, mut polygon_cuts, mut moved) = (0.0_f64, 0.0_f64, 0.0_f64);
|
||||
let mut converged = true;
|
||||
|
||||
@@ -3826,6 +3826,13 @@ impl Surface {
|
||||
}
|
||||
}
|
||||
|
||||
/// An exact round's definition carried onto `continuation`, this surface's own NURBS
|
||||
/// net continued Natural (`surface::extend`'s analytic kinds): still on S, as
|
||||
/// [`Surface::create_extended_surface`]'s `CInfinity` carries it (#2487).
|
||||
pub(crate) fn carry_round_on_continuation(&self, continuation: Self) -> Self {
|
||||
self.carry_exact_round_on_extension(continuation, ContinuityType::CInfinity)
|
||||
}
|
||||
|
||||
/// [`Surface::create_extended_surface`] of the kind alone.
|
||||
fn create_extended_surface_of_kind(
|
||||
&self,
|
||||
|
||||
@@ -2912,7 +2912,13 @@ pub fn mate_lamina_edge_to_surface(
|
||||
"negative surface extension distance",
|
||||
));
|
||||
}
|
||||
surf.create_extended_surface(k, ext_dist, ContinuityType::G1R)?
|
||||
surf.extended_sides(
|
||||
k,
|
||||
&crate::surface::side::BoxSide::ALL,
|
||||
crate::surface::extend::Continuity::Ruled,
|
||||
ext_dist,
|
||||
)?
|
||||
.surface
|
||||
};
|
||||
let ext_domain = ext_surface.natural_uv_domain_with_nurb(k)?;
|
||||
|
||||
|
||||
@@ -124,7 +124,6 @@ use crate::geom::context::{
|
||||
};
|
||||
use crate::geom::extent1d::Extent1d;
|
||||
use crate::geom::extent3d::Extent3d;
|
||||
use crate::geom::geom_utility::p2::ContinuityType;
|
||||
use crate::geom::object::{self, NotifyOperation, ObjectRef};
|
||||
use crate::geom::tol::{self, SmallTopoType};
|
||||
use crate::kernel::Kernel;
|
||||
@@ -4264,21 +4263,16 @@ fn turn_to_nurbs_in(
|
||||
size = 1.0;
|
||||
}
|
||||
let extend_distance = 0.02 * size;
|
||||
let SurfaceKind::BSpline(new_bss) = new_surface.kind() else {
|
||||
return Err(ner());
|
||||
};
|
||||
let ext = new_bss.create_extended_surface(k, extend_distance, ContinuityType::G1)?;
|
||||
let ext = new_surface
|
||||
.extended_sides(
|
||||
k,
|
||||
&crate::surface::side::BoxSide::ALL,
|
||||
crate::surface::extend::Continuity::Tangent,
|
||||
extend_distance,
|
||||
)?
|
||||
.surface;
|
||||
let face_ref = (env.key_of)(EntityRef::Face(f));
|
||||
p2::replace_surface(
|
||||
k,
|
||||
b,
|
||||
env,
|
||||
f,
|
||||
Surface::from(ext),
|
||||
face_ref,
|
||||
OldSurface::Delete,
|
||||
true,
|
||||
)?;
|
||||
p2::replace_surface(k, b, env, f, ext, face_ref, OldSurface::Delete, true)?;
|
||||
continue;
|
||||
};
|
||||
if surface.type_code() == BSplineSurface::TYPE {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
//! eight corner faces is on the sphere of radius 1 concentric with its outer corner.
|
||||
//! The shell's extended offset (`SmOffsetExecutive::GetCreateExtendedSurface`) used to
|
||||
//! extend those patches tangentially (`SM_CT_G1R`) into plain B-splines, and 7 of the 8
|
||||
//! inner corners lost their sphere; with every fix on, each is a ball patch on its
|
||||
//! sphere, the volume is the closed form's and the body checks clean.
|
||||
//! inner corners lost their sphere; now each is a ball patch on its sphere (its own net
|
||||
//! continued, flag or not), the volume is the closed form's and the body checks clean.
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
@@ -209,12 +209,13 @@ fn the_shell_has_the_closed_form_volume_and_checks_clean() {
|
||||
assert!(lines.is_empty(), "{lines:?}");
|
||||
}
|
||||
|
||||
/// Without `offset_extends_ball_patches_as_themselves` the extended offset extends a ball
|
||||
/// patch tangentially, a plain B-spline: only the inner corner whose trims stay inside
|
||||
/// its patch's domain to `EFF_ZERO` (corner 1, at (27, 3, 3)) keeps its patch; the other
|
||||
/// seven reach 1.6e-12 to 2.4e-11 past it in their trims' rounding and keep the B-spline.
|
||||
/// `offset_extends_ball_patches_as_themselves` no longer changes the extended offset: it
|
||||
/// always extends a ball patch as itself, its own net continued on its sphere
|
||||
/// (`Surface::extended_sides`), so with the flag off every inner corner still keeps its
|
||||
/// patch, on its sphere (before, a tangent extension into a plain B-spline kept only
|
||||
/// corner 1's).
|
||||
#[test]
|
||||
fn without_the_fix_seven_inner_corners_lose_their_sphere() {
|
||||
fn without_the_fix_every_inner_corner_still_keeps_its_sphere() {
|
||||
let mut k = all_fixes_but("offset_extends_ball_patches_as_themselves");
|
||||
let shell = shelled_box(&mut k);
|
||||
let mut kept = Vec::new();
|
||||
@@ -222,9 +223,10 @@ fn without_the_fix_seven_inner_corners_lose_their_sphere() {
|
||||
let faces = corner_faces(&k, &shell, corner, R - WALL);
|
||||
assert_eq!(faces.len(), 1, "corner {corner}");
|
||||
let s = face2::surface(&shell, faces[0]).unwrap().unwrap();
|
||||
if matches!(s.kind(), SurfaceKind::BallPatch(_)) {
|
||||
if let SurfaceKind::BallPatch(p) = s.kind() {
|
||||
assert!(off_sphere(p) <= 1e-12, "corner {corner}");
|
||||
kept.push(corner);
|
||||
}
|
||||
}
|
||||
assert_eq!(kept, [1]);
|
||||
assert_eq!(kept, (0..8).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
@@ -256,8 +256,11 @@ fn issue_398_open_top_shell_with_edges_on_analytics_exact() {
|
||||
assert_eq!(got.face_list().len(), 51);
|
||||
let want = closed_form(Some(30.0));
|
||||
assert!((v - want).abs() <= 1e-6 * want, "{v} vs {want}");
|
||||
// 2.3e-8 above the closed form; 13701.735330849975 (8.4e-8 above) with the flag off
|
||||
let pinned = 13701.73449509504;
|
||||
// 2.8e-9 above the closed form: the offset's face extensions are exact (the source bit
|
||||
// for bit, no least squares fit or knot removal moving a face's own part); it was
|
||||
// 13701.73449509504 (2.3e-8 above) on the fitted extensions, 13701.735330849975 (8.4e-8
|
||||
// above) with the flag off
|
||||
let pinned = 13701.73421803366;
|
||||
assert!((v - pinned).abs() <= 1e-9 * pinned, "{v} vs {pinned}");
|
||||
}
|
||||
|
||||
|
||||
@@ -422,12 +422,14 @@ fn partials_past_the_series_are_refused() {
|
||||
/// evaluators and box (fork 0136260's values on the same definitions, whose net is the
|
||||
/// unit square z = 0): the box is the net's grown by the bound, so the blend is not
|
||||
/// planar within 1e-6 although its net is; the degeneracy tests, the largest analytic
|
||||
/// domain and the extension follow the fork's.
|
||||
/// domain follow the fork's; the extension is the exact one (`extended_sides`).
|
||||
#[test]
|
||||
fn the_base_class_paths_take_the_blend_s_box_and_points() {
|
||||
use solidcore::error::SolidError;
|
||||
use solidcore::geom::extent2d::Extent2d;
|
||||
use solidcore::geom::geom_utility::p2::ContinuityType;
|
||||
use solidcore::math::EFF_ZERO_DEG;
|
||||
use solidcore::surface::extend::{Continuity, SideExtension};
|
||||
use solidcore::surface::side::BoxSide;
|
||||
let k = kernel();
|
||||
for (bumpy, n, w, law) in [
|
||||
(false, 1, 0.0, false),
|
||||
@@ -456,13 +458,47 @@ fn the_base_class_paths_take_the_blend_s_box_and_points() {
|
||||
(m.min().x, m.min().y, m.max().x, m.max().y),
|
||||
(-100.0, -100.0, 101.0, 101.0)
|
||||
);
|
||||
let ext = s
|
||||
.create_extended_surface(&k, 0.1, ContinuityType::G1)
|
||||
.unwrap();
|
||||
let ed = ext.natural_uv_domain_with_nurb(&k).unwrap();
|
||||
assert!(close(ed.min().x, -0.1) && close(ed.max().y, 1.1));
|
||||
let p0 = ext.evaluate_point(&k, ed.min()).unwrap();
|
||||
assert!((p0 - Point3::new(-0.1, -0.1, 0.0)).length() < 1e-15);
|
||||
// the exact extension (`extended_sides`): across (v, the section's t) the toy net
|
||||
// continued with its bound grown in closed form; along (u) the toy net is not the
|
||||
// construction's cubic Hermite net, so no span of the definition joins it
|
||||
// (refused); the equal area G^n section's thumbweight is proved only on a net of
|
||||
// its definition (refused)
|
||||
let across = s.extended_sides(
|
||||
&k,
|
||||
&[BoxSide::VMin, BoxSide::VMax],
|
||||
Continuity::Tangent,
|
||||
0.1,
|
||||
);
|
||||
if n == 2 {
|
||||
assert!(
|
||||
matches!(across, Err(SolidError::InvalidInput { .. })),
|
||||
"{across:?}"
|
||||
);
|
||||
} else {
|
||||
let ext = across.unwrap();
|
||||
assert!(matches!(ext.sides[2], SideExtension::Extended(_)));
|
||||
assert!(matches!(ext.sides[3], SideExtension::Extended(_)));
|
||||
let ed = ext.surface.natural_uv_domain_with_nurb(&k).unwrap();
|
||||
assert!(close(ed.min().y, -0.1) && close(ed.max().y, 1.1), "{ed:?}");
|
||||
assert!(ed.min().x == 0.0 && ed.max().x == 1.0, "{ed:?}");
|
||||
// the net continued: the unit square's corner moved by 0.1 across
|
||||
let solidcore::surface::surface::SurfaceKind::ProceduralBlend(pb) = ext.surface.kind()
|
||||
else {
|
||||
panic!("not a blend");
|
||||
};
|
||||
let p0 = pb.net().evaluate_point(ed.min()).unwrap();
|
||||
assert!(
|
||||
(p0 - Point3::new(0.0, -0.1, 0.0)).length() < 1e-12,
|
||||
"{p0:?}"
|
||||
);
|
||||
}
|
||||
for side in [BoxSide::UMin, BoxSide::UMax] {
|
||||
let along = s.extended_sides(&k, &[side], Continuity::Tangent, 0.1);
|
||||
assert!(
|
||||
matches!(along, Err(SolidError::Unsupported { .. })),
|
||||
"{along:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
//! built as two half faces (`create_partial_cone` 0..180 and 180..360, united) is
|
||||
//! shelled inward, open at its top. With `offset_extended_edges_mated` the mated pairs
|
||||
//! are related and the shell has its seven faces, twelve edges and eight vertices; by
|
||||
//! default these frustums come out with extra edges and vertices on the rim.
|
||||
//! default these frustums came out with extra edges and vertices on the rim; on the exact
|
||||
//! surface extensions the default keeps the rim too.
|
||||
//!
|
||||
//! The shell's volume is held to 1e-6 of the closed form: the frustum less the cavity
|
||||
//! bounded by the inner cone (the wall moved `t` along its normal, its radius smaller by
|
||||
@@ -81,13 +82,21 @@ fn wr_344_mated_rims_are_not_split_with_the_fix() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Upstream intersects the mated pairs again and splits the rim.
|
||||
/// Upstream intersects the mated pairs again and splits the rim. On the exact extensions
|
||||
/// (a cone continued as its own net, its rulings one straight span, the offsets' extended
|
||||
/// bases meeting along the mated edges) the default path no longer splits it: the same seven
|
||||
/// faces, twelve edges and eight vertices as the fix, a manifold solid of the closed form's
|
||||
/// volume to 1e-6.
|
||||
#[test]
|
||||
fn wr_344_default_splits_the_rim() {
|
||||
fn wr_344_default_keeps_the_rim_on_exact_extensions() {
|
||||
for case in CASES {
|
||||
let (_, b) = shelled(false, case);
|
||||
let (f, e, v) = counts(&b);
|
||||
assert_eq!(f, 7, "{case:?}");
|
||||
assert!(e > 12 && v > 8, "{case:?}: {e} edges, {v} vertices");
|
||||
let (k, mut b) = shelled(false, case);
|
||||
assert!(brep_is_manifold_solid(&b).unwrap(), "{case:?}");
|
||||
assert_eq!(counts(&b), (7, 12, 8), "{case:?}");
|
||||
let (v, exact) = (volume(&k, &mut b).unwrap(), closed_form(case));
|
||||
assert!(
|
||||
((v - exact) / exact).abs() < 1e-6,
|
||||
"{case:?}: {v} against {exact}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user