mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-11 03:01:58 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6de14d4add | |||
| 7549156435 | |||
| 6843398d2c | |||
| 0908520966 | |||
| e806792e31 | |||
| 229d720a2c | |||
| 35f1c4d7df | |||
| fb98759f24 |
@@ -174,6 +174,11 @@ pub enum Unsoundness {
|
||||
NotManifoldSolid,
|
||||
/// The body is a manifold solid whose volume is not positive.
|
||||
VolumeNotPositive,
|
||||
/// An edge the operation kept as it was is not proved clear of a face it moved: a
|
||||
/// certified distance bound between the edge's whole curve and the moved face's
|
||||
/// trimmed region does not exceed their tolerances (the edge may cross the face, which
|
||||
/// the body's check cannot see, or the bound is unavailable).
|
||||
KeptEdgeNotClear,
|
||||
}
|
||||
|
||||
/// How loops of an offset face touch without crossing ([`OffsetFailure::WallsTouch`]).
|
||||
@@ -449,6 +454,10 @@ impl core::fmt::Display for OffsetFailure {
|
||||
"the local operation's body is a manifold solid whose volume is not \
|
||||
positive: refused, the input unchanged",
|
||||
),
|
||||
Unsoundness::KeptEdgeNotClear => f.write_str(
|
||||
"an edge the local operation kept as it was is not proved clear of a face \
|
||||
it moved: refused, the input unchanged",
|
||||
),
|
||||
},
|
||||
Self::ExactCurveUncut => f.write_str(
|
||||
"the extended faces beside an edge meet on an exact curve that could not be \
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
mod attributes;
|
||||
mod cavity;
|
||||
pub(crate) mod clearance;
|
||||
mod crossing_loops;
|
||||
mod edgeuse;
|
||||
mod face;
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
//! Not upstream: proving that the edges a local operation kept are clear of the faces it
|
||||
//! moved.
|
||||
//!
|
||||
//! | Rust | Upstream |
|
||||
//! |------|----------|
|
||||
//! | [`separated`], [`all_clear`] | none |
|
||||
//!
|
||||
//! What is tested (issue #2971's certified bound): a kept edge's whole curve against the
|
||||
//! trimmed domain of each moved result face, the surface restricted to the box of the
|
||||
//! face's trim curves in its parameters (`surface_distance_lower_bound`'s `uv_box`), never
|
||||
//! the surface's whole extension: a plane's net is hundreds of thousands of units wide and
|
||||
//! crosses edges of solids the face never reaches. A curve interval is enclosed by rounded interval evaluation, then by a ball B(c,r).
|
||||
//! Distance to a set is 1-Lipschitz: d(C,S) >= d(c,S) - r. The surface distance is the
|
||||
//! existing certified bound (positive-weight Bernstein hulls, including the definition's
|
||||
//! deviation and arithmetic rounding), not a projection or a sample. Thus every accepted
|
||||
//! cell proves all its curve points clear, including between evaluated parameters.
|
||||
//!
|
||||
//! Deviations: no recursion; at most 255 curve cells and twelve halvings per branch (a cell is split only while its
|
||||
//! ball is not proved clear, so the cells gather where the curve passes near the face). A kind
|
||||
//! without an enclosure, a nonfinite enclosure, or an exhausted budget proves nothing.
|
||||
//! NURBS degree is at most 18 here: the factorials in `curve_derivs_over` are then exact
|
||||
//! binary64 integers. Lines are evaluated on their definition, not a rounded fitted form.
|
||||
|
||||
use crate::blend::fillet_section::certificate::{curve_derivs_over, NurbsCurve};
|
||||
use crate::blend::fillet_section::interval::Ivl;
|
||||
use crate::curve::curve::{Curve, CurveKind};
|
||||
use crate::curve::line::Line;
|
||||
use crate::geom::extent1d::Extent1d;
|
||||
use crate::geom::extent2d::Extent2d;
|
||||
use crate::kernel::Kernel;
|
||||
use crate::math::Point3;
|
||||
use crate::nurbs::math::{NOW, NOZ};
|
||||
use crate::query::distance_bound::surface_distance_lower_bound;
|
||||
use crate::surface::surface::Surface;
|
||||
use crate::{Result, SolidError};
|
||||
|
||||
const OP: &str = "offset::offset_executive::clearance";
|
||||
|
||||
/// An edge a local operation kept as it was: its curve over its interval, and the
|
||||
/// tolerance (its own, at least the body's) it must stay clear within.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct KeptEdge {
|
||||
pub(crate) curve: Curve,
|
||||
pub(crate) interval: Extent1d,
|
||||
pub(crate) tolerance: f64,
|
||||
}
|
||||
|
||||
/// A face a local operation moved, as the result has it: its surface, the box of its trim
|
||||
/// curves in the surface's parameters (`None` when a trim curve has no box: the whole
|
||||
/// surface is then tested), and its tolerance (at least the body's).
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct MovedFace {
|
||||
pub(crate) surface: Surface,
|
||||
pub(crate) uv_box: Option<Extent2d>,
|
||||
pub(crate) tolerance: f64,
|
||||
}
|
||||
|
||||
/// Every kept edge is proved clear of every moved face by more than their summed
|
||||
/// tolerances (rounded up). `false` means some pair is unproved, not necessarily crossing.
|
||||
///
|
||||
/// # Errors
|
||||
/// Cancellation.
|
||||
pub(crate) fn all_clear(kernel: &Kernel, kept: &[KeptEdge], moved: &[MovedFace]) -> Result<bool> {
|
||||
for edge in kept {
|
||||
for face in moved {
|
||||
let tolerance = (Ivl::from(edge.tolerance) + Ivl::from(face.tolerance)).hi;
|
||||
if !separated(
|
||||
kernel,
|
||||
&edge.curve,
|
||||
&edge.interval,
|
||||
&face.surface,
|
||||
face.uv_box.as_ref(),
|
||||
tolerance,
|
||||
)? {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
const MAX_DEPTH: usize = 12;
|
||||
const MAX_CELLS: usize = 255;
|
||||
|
||||
enum EnclosedCurve<'a> {
|
||||
Line(&'a Line),
|
||||
Nurbs(NurbsCurve),
|
||||
}
|
||||
|
||||
impl<'a> EnclosedCurve<'a> {
|
||||
fn of(curve: &'a Curve) -> Option<Self> {
|
||||
let base = match curve.kind() {
|
||||
CurveKind::Line(line) => return Some(Self::Line(line)),
|
||||
CurveKind::BSpline(c) => c,
|
||||
// Curve::evaluate_point reads these conics' stored rational forms too.
|
||||
CurveKind::Ellipse(c) => c.base(),
|
||||
CurveKind::Parabola(c) => c.base(),
|
||||
CurveKind::Hyperbola(c) => c.base(),
|
||||
_ => return None,
|
||||
};
|
||||
let net = base.gw_nurb()?;
|
||||
let (p, n) = (net.degree(), net.cpts().len());
|
||||
if !(1..=18).contains(&p) || n <= p || net.knots().len() != n + p + 1 {
|
||||
return None;
|
||||
}
|
||||
// Read the stored homogeneous coefficients directly: control_polygon followed
|
||||
// by NurbsCurve::of would divide and multiply rational coordinates again.
|
||||
let mut pw = Vec::new();
|
||||
for q in net.cpts() {
|
||||
let w = if q.w == NOW { 1.0 } else { q.w };
|
||||
if !(q.x.is_finite() && q.y.is_finite() && q.z.is_finite() && w.is_finite())
|
||||
|| q.z == NOZ
|
||||
|| w <= 0.0
|
||||
{
|
||||
return None;
|
||||
}
|
||||
pw.extend_from_slice(&[q.x, q.y, q.z, w]);
|
||||
}
|
||||
let u = net.knots().to_vec();
|
||||
if u.iter().any(|x| !x.is_finite()) || u.iter().zip(u.iter().skip(1)).any(|(a, b)| a > b) {
|
||||
return None;
|
||||
}
|
||||
Some(Self::Nurbs(NurbsCurve { p, n, u, pw }))
|
||||
}
|
||||
|
||||
fn over(&self, t: Ivl) -> Result<Option<[Ivl; 3]>> {
|
||||
match self {
|
||||
Self::Line(line) => {
|
||||
// The affine NURBS-to-STEP parameter map, with every subtraction,
|
||||
// division and multiplication enclosed (Line::evaluate_point).
|
||||
let mut step = t;
|
||||
if line.is_bounded() {
|
||||
let nurb = line.natural_interval();
|
||||
let domain = line.step_interval();
|
||||
let length = Ivl::from(nurb.max()) - Ivl::from(nurb.min());
|
||||
if length.lo.is_nan()
|
||||
|| length.lo <= 0.0
|
||||
|| crate::geom::tol::is_zero(nurb.length())
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
let along = if line.inside_out() {
|
||||
(Ivl::from(nurb.max()) - t) / length
|
||||
} else {
|
||||
(t - Ivl::from(nurb.min())) / length
|
||||
};
|
||||
step = Ivl::from(domain.min())
|
||||
+ along * (Ivl::from(domain.max()) - Ivl::from(domain.min()));
|
||||
} else if line.inside_out() {
|
||||
step = Ivl::from(0.0) - step;
|
||||
}
|
||||
let p = line.line_point();
|
||||
let v = line.line_vector();
|
||||
let s = step * Ivl::from(line.line_scale());
|
||||
Ok(Some([
|
||||
Ivl::from(p.x) + s * Ivl::from(v.x),
|
||||
Ivl::from(p.y) + s * Ivl::from(v.y),
|
||||
Ivl::from(p.z) + s * Ivl::from(v.z),
|
||||
]))
|
||||
}
|
||||
Self::Nurbs(net) => {
|
||||
let (Some(&lo), Some(&hi)) = (net.u.get(net.p), net.u.get(net.n)) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if !(t.lo >= lo && t.hi <= hi) {
|
||||
return Ok(None);
|
||||
}
|
||||
let derivatives = curve_derivs_over(net, t, 0, None)?;
|
||||
Ok(derivatives.first().map(|p| [p.x, p.y, p.z]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Every point of `curve` over `interval` is proved farther than `tolerance` from the
|
||||
/// part of `surface` over `uv_box` (the whole finite image when `None`).
|
||||
/// `false` means unproved, not necessarily intersecting. No curve or surface samples can
|
||||
/// make this return true.
|
||||
///
|
||||
/// # Errors
|
||||
/// Cancellation is propagated. An unavailable certificate returns `false`, so the
|
||||
/// executive takes the ordinary re-intersection path instead of keeping this edge.
|
||||
pub(super) fn separated(
|
||||
kernel: &Kernel,
|
||||
curve: &Curve,
|
||||
interval: &Extent1d,
|
||||
surface: &Surface,
|
||||
uv_box: Option<&Extent2d>,
|
||||
tolerance: f64,
|
||||
) -> Result<bool> {
|
||||
let Some(curve) = EnclosedCurve::of(curve) else {
|
||||
return Ok(false);
|
||||
};
|
||||
let (lo, hi) = (interval.min(), interval.max());
|
||||
if !(lo.is_finite() && hi.is_finite() && lo < hi && tolerance.is_finite() && tolerance >= 0.0) {
|
||||
return Ok(false);
|
||||
}
|
||||
let mut stack = vec![(Ivl::new(lo, hi), 0_usize)];
|
||||
let mut cells = 0;
|
||||
while let Some((t, depth)) = stack.pop() {
|
||||
kernel.check_cancelled(OP)?;
|
||||
cells += 1;
|
||||
if cells > MAX_CELLS {
|
||||
return Ok(false);
|
||||
}
|
||||
let box_ = match curve.over(t) {
|
||||
Ok(Some(box_)) => box_,
|
||||
Err(e @ SolidError::Cancelled { .. }) => return Err(e),
|
||||
_ => return Ok(false),
|
||||
};
|
||||
if box_
|
||||
.iter()
|
||||
.any(|x| !(x.lo.is_finite() && x.hi.is_finite() && x.lo <= x.hi))
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
// The centre need not lie on the curve. Its computed binary64 value is exact
|
||||
// data for both bounds: interval subtraction encloses the ball's radius.
|
||||
let centre = box_.map(|x| 0.5 * x.lo + 0.5 * x.hi);
|
||||
let mut radius2 = Ivl::from(0.0);
|
||||
for (x, c) in box_.into_iter().zip(centre) {
|
||||
let delta = x - Ivl::from(c);
|
||||
radius2 = radius2 + delta * delta;
|
||||
}
|
||||
let radius = radius2.sqrt().hi;
|
||||
let want = (Ivl::from(radius) + Ivl::from(tolerance)).hi;
|
||||
if !want.is_finite() {
|
||||
return Ok(false);
|
||||
}
|
||||
let [cx, cy, cz] = centre;
|
||||
let lower = match surface_distance_lower_bound(
|
||||
kernel,
|
||||
surface,
|
||||
uv_box,
|
||||
Point3::new(cx, cy, cz),
|
||||
want,
|
||||
) {
|
||||
Ok(lower) => lower,
|
||||
Err(e @ SolidError::Cancelled { .. }) => return Err(e),
|
||||
Err(_) => return Ok(false),
|
||||
};
|
||||
if (Ivl::from(lower) - Ivl::from(radius)).lo > tolerance {
|
||||
continue;
|
||||
}
|
||||
if depth >= MAX_DEPTH {
|
||||
return Ok(false);
|
||||
}
|
||||
let mid = 0.5 * t.lo + 0.5 * t.hi;
|
||||
if !(mid > t.lo && mid < t.hi) {
|
||||
return Ok(false);
|
||||
}
|
||||
stack.push((Ivl::new(mid, t.hi), depth + 1));
|
||||
stack.push((Ivl::new(t.lo, mid), depth + 1));
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::math::{Point2, Vec3};
|
||||
use crate::nurbs::b_spline_curve::p1::{BSplineCurveForm, CanonicalData, KnotType};
|
||||
use crate::nurbs::b_spline_curve::BSplineCurve;
|
||||
use crate::surface::offset_surface::OffsetSurface;
|
||||
use crate::surface::plane::Plane;
|
||||
|
||||
fn floor() -> Surface {
|
||||
Surface::from(Plane::from_normal(Vec3::ZERO, Vec3::Z))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_long_clear_edge_is_proved_after_subdivision() {
|
||||
let k = Kernel::default();
|
||||
let interval = Extent1d::from_min_max(0.0, 1.0);
|
||||
for curve in [
|
||||
Curve::from(Line::from_points(
|
||||
Point3::new(-10.0, 0.0, 1.0),
|
||||
Point3::new(10.0, 0.0, 1.0),
|
||||
3,
|
||||
)),
|
||||
Curve::from(
|
||||
BSplineCurve::create_line_segment(
|
||||
3,
|
||||
Point3::new(-10.0, 0.0, 1.0),
|
||||
Point3::new(10.0, 0.0, 1.0),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
] {
|
||||
assert!(separated(&k, &curve, &interval, &floor(), None, 0.01).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_crossing_contact_or_tolerance_band_is_never_certified_clear() {
|
||||
let k = Kernel::default();
|
||||
let interval = Extent1d::from_min_max(0.0, 1.0);
|
||||
for (a, b, tolerance) in [
|
||||
(-1.0, 1.0, 0.01),
|
||||
(0.0, 0.0, 0.01),
|
||||
(0.005, 0.005, 0.01),
|
||||
(0.01, 0.01, 0.01),
|
||||
] {
|
||||
let curve = Curve::from(Line::from_points(
|
||||
Point3::new(-1.0, 0.0, a),
|
||||
Point3::new(1.0, 0.0, b),
|
||||
3,
|
||||
));
|
||||
assert!(!separated(&k, &curve, &interval, &floor(), None, tolerance).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rational_curve_interiors_and_the_trim_interval_are_bounded() {
|
||||
let k = Kernel::default();
|
||||
let curve = Curve::from(
|
||||
BSplineCurve::create_canonical(&CanonicalData {
|
||||
dim: 3,
|
||||
degree: 2,
|
||||
control_points: &[
|
||||
Point3::new(-1.0, 0.0, 1.0),
|
||||
Point3::new(0.0, 0.0, -2.0),
|
||||
Point3::new(1.0, 0.0, 1.0),
|
||||
],
|
||||
form: BSplineCurveForm::Unspecified,
|
||||
knot_multiplicities: &[3, 3],
|
||||
knots: &[0.0, 1.0],
|
||||
knot_type: KnotType::Unspecified,
|
||||
weights: Some(&[1.0, 2.0, 1.0]),
|
||||
trim: None,
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
// The endpoints are above the plane but C(1/2).z = -1: it crosses.
|
||||
assert!(!separated(
|
||||
&k,
|
||||
&curve,
|
||||
&Extent1d::from_min_max(0.0, 1.0),
|
||||
&floor(),
|
||||
None,
|
||||
0.01,
|
||||
)
|
||||
.unwrap());
|
||||
assert!(separated(
|
||||
&k,
|
||||
&curve,
|
||||
&Extent1d::from_min_max(0.0, 0.03125),
|
||||
&floor(),
|
||||
None,
|
||||
0.01,
|
||||
)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// An edge through the plane far outside the face's parameter box is clear of the face
|
||||
/// (the plane's net is far wider than any face on it), and an edge through the plane
|
||||
/// inside the box is not. Tested over the whole surface instead, the far edge would
|
||||
/// be reported crossed.
|
||||
#[test]
|
||||
fn only_the_trimmed_region_of_a_surface_is_tested() {
|
||||
let k = Kernel::default();
|
||||
let plane = floor();
|
||||
let vertical_through = |u: f64, v: f64| {
|
||||
let p = plane.evaluate_point(&k, Point2::new(u, v)).unwrap();
|
||||
Curve::from(Line::from_points(
|
||||
Point3::new(p.x, p.y, p.z - 1.0),
|
||||
Point3::new(p.x, p.y, p.z + 1.0),
|
||||
3,
|
||||
))
|
||||
};
|
||||
let interval = Extent1d::from_min_max(0.0, 1.0);
|
||||
let face = Extent2d::from_bounds(-1.0, -1.0, 1.0, 1.0);
|
||||
let inside = vertical_through(0.25, 0.5);
|
||||
let outside = vertical_through(500.0, 0.0);
|
||||
assert!(!separated(&k, &inside, &interval, &plane, Some(&face), 0.01).unwrap());
|
||||
assert!(separated(&k, &outside, &interval, &plane, Some(&face), 0.01).unwrap());
|
||||
assert!(!separated(&k, &outside, &interval, &plane, None, 0.01).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unavailable_or_nonfinite_bounds_do_not_keep_an_edge() {
|
||||
let k = Kernel::default();
|
||||
let curve = Curve::from(Line::from_points(Vec3::ZERO, Vec3::X, 3));
|
||||
let interval = Extent1d::from_min_max(0.0, 1.0);
|
||||
let implicit = Surface::from(OffsetSurface::new(&k, 2.0, floor(), true, 0).unwrap());
|
||||
assert!(!separated(&k, &curve, &interval, &implicit, None, 0.01).unwrap());
|
||||
assert!(!separated(&k, &curve, &interval, &floor(), None, f64::NAN).unwrap());
|
||||
assert!(!separated(
|
||||
&k,
|
||||
&curve,
|
||||
&Extent1d::from_min_max(-1.0, f64::INFINITY),
|
||||
&floor(),
|
||||
None,
|
||||
0.01,
|
||||
)
|
||||
.unwrap());
|
||||
}
|
||||
}
|
||||
@@ -453,6 +453,31 @@ impl OffsetExecutive {
|
||||
self.kept_edgeuses.contains(&edgeuse)
|
||||
}
|
||||
|
||||
/// Not upstream: the edges [`Self::keep_unmoved_edges`] kept, as their curves over
|
||||
/// their intervals with the tolerance each must stay clear within (its own, at least
|
||||
/// the body's), for the local operation's check that the faces it moved stay clear of
|
||||
/// them ([`super::clearance::all_clear`]). `original` is the B-rep the executive ran on.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::NotFound`] for a dangling id; a null pointer for a kept edge without
|
||||
/// a curve.
|
||||
pub(crate) fn kept_edge_curves(
|
||||
&self,
|
||||
original: &Brep,
|
||||
) -> Result<Vec<super::clearance::KeptEdge>> {
|
||||
let mut kept = Vec::new();
|
||||
for &e in &self.kept_edges {
|
||||
let curve = edge::curve(original, e)?.ok_or_else(|| SolidError::null_pointer(OP))?;
|
||||
kept.push(super::clearance::KeptEdge {
|
||||
curve: curve.clone(),
|
||||
interval: edge::interval(original, e)?,
|
||||
tolerance: edge::tolerance(original, e)?.max(original.tolerance()),
|
||||
});
|
||||
}
|
||||
Ok(kept)
|
||||
}
|
||||
|
||||
/// Sets `m_eOffsetOperation`.
|
||||
pub fn set_operation(&mut self, operation: OffsetOperation) {
|
||||
self.operation = operation;
|
||||
|
||||
+214
-5
@@ -31,12 +31,14 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use crate::error::offset::OffsetFailure;
|
||||
use crate::error::{EntityRef, Failure, Result, SolidError};
|
||||
use crate::geom::attribute::ObjectKey;
|
||||
use crate::geom::attribute::{Attribute, Behavior, ObjectKey};
|
||||
use crate::geom::axis2_placement::Axis2Placement;
|
||||
use crate::geom::extent2d::Extent2d;
|
||||
use crate::kernel::Kernel;
|
||||
use crate::math::Vec3;
|
||||
use crate::nurbs::b_spline_surface::p1::SurfParamType;
|
||||
use crate::nurbs::knots::Side;
|
||||
use crate::offset::offset_executive::clearance::{self, KeptEdge, MovedFace};
|
||||
use crate::offset::offset_executive::p1::{OffsetExecutive, OffsetOperation};
|
||||
use crate::offset::offset_executive::p2::OffsetNotifications;
|
||||
use crate::query::brep::{calculate_bounding_box, BoundingBoxOptions};
|
||||
@@ -244,14 +246,27 @@ fn local_operation_on(
|
||||
// A face's surface is extended as far as its neighbours' new surfaces meet it, and as
|
||||
// far again as they moved beside it (not upstream, whose margin is five half-radii of
|
||||
// the sphere bounding the box: `local_extension_reach`). The edges whose faces and
|
||||
// vertices' faces all keep their surfaces are kept as they are, not intersected again
|
||||
// (`OffsetExecutive::keeps_edge`).
|
||||
// vertices' faces all keep their surfaces, and whose whole curve is certified clear of
|
||||
// every moved surface (a ball enclosure of each curve cell against the surface's
|
||||
// certified distance bound, rounding counted: `clearance::separated`, #2971), are
|
||||
// kept as they are, not intersected again (`OffsetExecutive::keeps_edge`).
|
||||
executive.set_local_surfaces(&all_faces, new_list, &moved);
|
||||
// the shell faces get copied without being offset: none
|
||||
executive.set_shell_faces(&[]);
|
||||
// extend/intersect to fill new convex edge gaps rather than adding a fillet
|
||||
executive.set_extend_convex_edges(true);
|
||||
|
||||
// (not upstream) each moved face carries a private mark through the operation, as a
|
||||
// caller's mark does (`Behavior::Copy`, found on the result's faces below): the
|
||||
// faces of the result that lie where the operation moved, with their trimmed regions,
|
||||
// are what the kept edges must stay clear of (`refuse_kept_edges_not_clear`)
|
||||
for &face in &moved {
|
||||
b.add_attribute(
|
||||
EntityRef::Face(face),
|
||||
Attribute::long(MOVED_FACE_MARK, 1, Behavior::Copy),
|
||||
)?;
|
||||
}
|
||||
|
||||
// (not upstream) `b`'s own attributes go to the offset under the original
|
||||
// identities, as upstream's live on the faces it copies (`SM_NO_COPY` of each
|
||||
// source face); the caller's store does not keep them afterwards
|
||||
@@ -270,6 +285,11 @@ fn local_operation_on(
|
||||
// whose status is then `SM_ERR`) the offset's error is the operation's, with its own
|
||||
// status and reason, as the reach's and the final gate's refusals are
|
||||
let offset = executive.do_extended_offset(k, Some(&mut *b), notifications);
|
||||
// the edges it kept, as they were on `b` (the faces of `b` go with `move_faces`)
|
||||
let kept = match &offset {
|
||||
Ok(_) => executive.kept_edge_curves(b),
|
||||
Err(_) => Ok(Vec::new()),
|
||||
};
|
||||
drop(executive);
|
||||
let new_brep = offset.and_then(|mut new_brep| {
|
||||
// the offset B-rep's objects carry what the offset gave them
|
||||
@@ -286,6 +306,7 @@ fn local_operation_on(
|
||||
.retain_rekeyed(&|key| (!seeded.contains(&key)).then_some(key))?;
|
||||
}
|
||||
let mut new_brep = new_brep?;
|
||||
let kept = kept?;
|
||||
|
||||
// remove all faces from this Brep, then move all faces of the new Brep into it
|
||||
// (`CopyFaces` with a merge, which also grows this Brep's tolerance to the new one's);
|
||||
@@ -298,9 +319,83 @@ fn local_operation_on(
|
||||
// (not upstream: the port's cached topology fields, true again for the caller)
|
||||
b.refresh_cached_topology();
|
||||
moved?;
|
||||
refuse_kept_edges_not_clear(k, b, &kept)?;
|
||||
refuse_unsound(k, b, &input)
|
||||
}
|
||||
|
||||
/// The id of the private mark the moved faces carry through a local operation (below the
|
||||
/// application range, which starts above 10000).
|
||||
const MOVED_FACE_MARK: u64 = 9_999;
|
||||
|
||||
/// (not upstream) The kept edges are proved clear of the faces the operation moved, or the
|
||||
/// operation is refused ([`Unsoundness::KeptEdgeNotClear`](crate::error::offset::Unsoundness)),
|
||||
/// the input as it came in.
|
||||
///
|
||||
/// The body's check has no face-face intersection test, so a face that crosses an edge
|
||||
/// the operation kept (an edge whose faces and vertices' faces all keep their surfaces: the
|
||||
/// executive does not intersect it again) leaves a self-intersecting body it cannot see. The
|
||||
/// certified bound (issue #2971): over the whole curve of each kept edge, a ball enclosure
|
||||
/// of each cell, rounding counted, and a surface distance lower bound to the part of each
|
||||
/// moved face's surface over the box of its trim curves in its parameters
|
||||
/// ([`clearance::all_clear`], `surface_distance_lower_bound`), must exceed the sum of the
|
||||
/// edge's and the face's tolerances. A moved face is found by its mark on the result; its
|
||||
/// trimmed region is the surface's part over its loops' boxes (the whole surface when a
|
||||
/// loop has none), not the surface's extension, which a plane's net makes wider than any
|
||||
/// part. No edge is intersected again: an edge not proved clear refuses the operation.
|
||||
fn refuse_kept_edges_not_clear(k: &mut Kernel, b: &mut Brep, kept: &[KeptEdge]) -> Result<()> {
|
||||
use crate::error::offset::Unsoundness;
|
||||
use crate::topology::loopuse;
|
||||
// the marks come off the result whatever follows
|
||||
let mut moved_faces = Vec::new();
|
||||
for face in bp1::faces(b, None)? {
|
||||
let handles: Vec<_> = b
|
||||
.entity_attributes(EntityRef::Face(face))
|
||||
.into_iter()
|
||||
.filter(|(_, a)| a.attribute_id() == MOVED_FACE_MARK)
|
||||
.map(|(h, _)| h)
|
||||
.collect();
|
||||
for h in &handles {
|
||||
b.remove_attribute(EntityRef::Face(face), *h)?;
|
||||
}
|
||||
if !handles.is_empty() {
|
||||
moved_faces.push(face);
|
||||
}
|
||||
}
|
||||
if kept.is_empty() || moved_faces.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
// (the trim curves a face lacks are made on a private copy)
|
||||
let mut copy = b.clone_standing_for_self()?;
|
||||
let mut faces = Vec::new();
|
||||
for face in moved_faces {
|
||||
let surface = face_ops::surface(©, face)?.ok_or_else(null)?.clone();
|
||||
let mut uv_box: Option<Extent2d> = None;
|
||||
for l in face_ops::loops(©, face, false, None)? {
|
||||
let Some(span) = loopuse::loop_uv_span(k, &mut copy, l)? else {
|
||||
uv_box = None;
|
||||
break;
|
||||
};
|
||||
uv_box = Some(uv_box.map_or(span, |u| u.union(&span)));
|
||||
}
|
||||
faces.push(MovedFace {
|
||||
surface,
|
||||
uv_box,
|
||||
tolerance: copy.face(face)?.tolerance.max(copy.tolerance()),
|
||||
});
|
||||
}
|
||||
if clearance::all_clear(k, kept, &faces)? {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Failure::new(
|
||||
OP,
|
||||
OffsetFailure::Unsound {
|
||||
what: Unsoundness::KeptEdgeNotClear,
|
||||
},
|
||||
)
|
||||
.into_error())
|
||||
}
|
||||
}
|
||||
|
||||
/// (not upstream) The local operation's final gate: the body it built is refused when it
|
||||
/// fails what its input did not ([`validation::soundness`] of both, `input` measured
|
||||
/// before the edit, the body on a private copy):
|
||||
@@ -584,7 +679,7 @@ fn seam_gap(
|
||||
k: &Kernel,
|
||||
surface: &Surface,
|
||||
in_u: bool,
|
||||
domain: &crate::geom::extent2d::Extent2d,
|
||||
domain: &Extent2d,
|
||||
) -> Result<Option<crate::geom::certified::Bounded>> {
|
||||
use crate::geom::certified::{distance, gamma, Bounded};
|
||||
use crate::geom::vector3d::Point3;
|
||||
@@ -696,7 +791,7 @@ fn cone_seam_gap(
|
||||
surface: &Surface,
|
||||
cone: &crate::surface::cone::Cone,
|
||||
in_u: bool,
|
||||
domain: &crate::geom::extent2d::Extent2d,
|
||||
domain: &Extent2d,
|
||||
) -> Result<Option<crate::geom::certified::Bounded>> {
|
||||
use crate::geom::certified::{distance, gamma, Bounded};
|
||||
use crate::geom::vector2d::Point2;
|
||||
@@ -1285,6 +1380,120 @@ mod tests {
|
||||
assert!(manifold && (v - 10000.0).abs() <= 1e-9 * 10000.0, "{v}");
|
||||
}
|
||||
|
||||
/// Issue 2976: the top of a 2 x 2 x 2 box becomes the plane z = x + y - 1. It cuts the
|
||||
/// bottom edges y = 0 and x = 0 at (1, 0, 0) and (0, 1, 0), away from their vertices;
|
||||
/// every face at those vertices is the bottom or a side, never the top, so the
|
||||
/// vertex-neighbourhood rule keeps both crossed edges whole through the cut-off corner
|
||||
/// (the check of the kept edges, `refuse_kept_edges_not_clear`, is tested on its own
|
||||
/// below). The corner (0, 0, 0) lies above the plane, and the vertical edge above it
|
||||
/// meets the plane's line only at (0, 0, -1), outside the edge: the corner's sound
|
||||
/// solid needs that edge to vanish, which the extended offset does not build. The
|
||||
/// operation therefore refuses the body, and `b` stays as it came in.
|
||||
#[test]
|
||||
fn a_tilted_top_that_crosses_unmoved_edges_is_refused_not_returned_with_them_kept() {
|
||||
let mut k = Kernel::default();
|
||||
let mut b = create_box(&mut k, Point3::ZERO, 2.0, 2.0, 2.0).unwrap();
|
||||
let top = bp1::faces(&b, None)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.find(|&f| {
|
||||
let s = face_ops::surface(&b, f).unwrap().unwrap();
|
||||
let uv = s
|
||||
.natural_uv_domain_with_nurb(&k)
|
||||
.unwrap()
|
||||
.evaluate(0.5, 0.5);
|
||||
(s.evaluate_point(&k, uv).unwrap().z - 2.0).abs() < 1e-9
|
||||
})
|
||||
.unwrap();
|
||||
let tilted = Surface::from(Plane::from_normal(
|
||||
Vec3::new(0.0, 0.0, -1.0),
|
||||
Vec3::new(-1.0, -1.0, 1.0),
|
||||
));
|
||||
let why = refused_unchanged_or_returned(&mut k, &mut b, &[top], vec![tilted]);
|
||||
assert!(
|
||||
matches!(why, Some(OffsetFailure::Unsound { .. })),
|
||||
"{why:?}"
|
||||
);
|
||||
let (volume, manifold) = volume_and_manifold(&k, &mut b);
|
||||
assert!(manifold && (volume - 8.0).abs() <= 1e-9 * 8.0, "{volume}");
|
||||
}
|
||||
|
||||
/// The check of the kept edges against the faces moved (issue #2976, certified bound
|
||||
/// #2971), on the result of raising a 2-cube's top to the plane z = 3 given as
|
||||
/// `Plane::from_normal`, whose net is about 1.2e6 wide while the face on it is 2 x 2.
|
||||
/// The moved face carries the mark. A vertical edge through the face's region at
|
||||
/// (1, 1) refuses the operation (`KeptEdgeNotClear`); one through the plane's net far
|
||||
/// outside the face, at (50, 50), is clear of the face (tested against the whole
|
||||
/// surface instead of the face's trimmed box, it would be called crossed). The marks
|
||||
/// are off the body after either.
|
||||
#[test]
|
||||
fn a_kept_edge_through_a_moved_face_refuses_and_one_through_its_plane_beside_it_does_not() {
|
||||
use crate::curve::curve::Curve;
|
||||
use crate::curve::line::Line;
|
||||
use crate::error::offset::Unsoundness;
|
||||
use crate::geom::extent1d::Extent1d;
|
||||
let kept_through = |x: f64, y: f64| KeptEdge {
|
||||
curve: Curve::from(Line::from_points(
|
||||
Point3::new(x, y, 2.5),
|
||||
Point3::new(x, y, 3.5),
|
||||
3,
|
||||
)),
|
||||
interval: Extent1d::from_min_max(0.0, 1.0),
|
||||
tolerance: 1e-5,
|
||||
};
|
||||
for (x, y, refused) in [(1.0, 1.0, true), (50.0, 50.0, false)] {
|
||||
let mut k = Kernel::default();
|
||||
let mut b = create_box(&mut k, Point3::ZERO, 2.0, 2.0, 2.0).unwrap();
|
||||
let top = bp1::faces(&b, None)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.find(|&f| {
|
||||
let s = face_ops::surface(&b, f).unwrap().unwrap();
|
||||
let uv = s
|
||||
.natural_uv_domain_with_nurb(&k)
|
||||
.unwrap()
|
||||
.evaluate(0.5, 0.5);
|
||||
(s.evaluate_point(&k, uv).unwrap().z - 2.0).abs() < 1e-9
|
||||
})
|
||||
.unwrap();
|
||||
let raised = Surface::from(Plane::from_normal(Point3::new(0.0, 0.0, 3.0), Vec3::Z));
|
||||
with_notifications(&mut b, |b, notes| {
|
||||
local_operation(&mut k, b, &[top], vec![raised], notes)
|
||||
})
|
||||
.unwrap();
|
||||
let moved = bp1::faces(&b, None)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.find(|&f| {
|
||||
let s = face_ops::surface(&b, f).unwrap().unwrap();
|
||||
let uv = b.face(f).unwrap().uv_domain.evaluate(0.5, 0.5);
|
||||
(s.evaluate_point(&k, uv).unwrap().z - 3.0).abs() < 1e-9
|
||||
})
|
||||
.unwrap();
|
||||
b.add_attribute(
|
||||
EntityRef::Face(moved),
|
||||
Attribute::long(MOVED_FACE_MARK, 1, Behavior::Copy),
|
||||
)
|
||||
.unwrap();
|
||||
let r = refuse_kept_edges_not_clear(&mut k, &mut b, &[kept_through(x, y)]);
|
||||
if refused {
|
||||
let e = r.unwrap_err();
|
||||
assert!(
|
||||
matches!(
|
||||
e.failure().map(|f| &f.reason),
|
||||
Some(crate::error::Reason::Offset(OffsetFailure::Unsound {
|
||||
what: Unsoundness::KeptEdgeNotClear
|
||||
}))
|
||||
),
|
||||
"{e}"
|
||||
);
|
||||
} else {
|
||||
r.unwrap();
|
||||
}
|
||||
assert!(b.entity_attributes(EntityRef::Face(moved)).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moves_the_top_of_a_box_and_keeps_it_a_solid() {
|
||||
let mut k = Kernel::default();
|
||||
|
||||
Reference in New Issue
Block a user