mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 04:52:10 +00:00
Merge pull request #1961 from brett19/fix/face-tight-box-knot-division
fix: a face's tight box divides its cells at the surface's knots (opt-in, WR-645, fork 326de73)
This commit is contained in:
@@ -542,7 +542,7 @@ pub struct UpstreamFixes {
|
||||
pub fillet_trace_rejects_short_step: bool,
|
||||
/// `SmCurve::CalculateTightBoundingBox` adds only the B-spline knots strictly inside the queried interval (the interval's ends are already in the box). Upstream adds every tangent-discontinuous knot and always the last knot whatever the interval, so the tight box of an edge on part of a NURBS curve (an imported sphere cap's seam, 30 deg of a quarter-circle NURBS) reaches points the edge never does (WR-474, #1054; the fork's \"a curve's tight box holds the knots inside its interval only\").
|
||||
pub curve_tight_box_interval_knots: bool,
|
||||
/// `SmFace::CalculateTightBoundingBox` searches a trimmed face's own extreme inside its trim (subdividing the UV domain from the vertex and edge extremes, bounded at depth 16 / 4096 cells), not only the surface's global extreme, so a face whose surface peaks outside the trim is not boxed too small (WR-588, #1799; tangency-labs/smlib@9d44c1b).
|
||||
/// `SmFace::CalculateTightBoundingBox` searches a trimmed face's own extreme inside its trim (subdividing the UV domain from the vertex and edge extremes, bounded at depth 16 / 4096 cells, an undecided cell divided at the surface's knot inside it nearest its middle, else at its middle), not only the surface's global extreme, so a face whose surface peaks outside the trim is not boxed too small (WR-588, #1799; tangency-labs/smlib@9d44c1b), nor one whose rational surface has knots off the domain's dyadic points too large (WR-651; tangency-labs/smlib@326de73).
|
||||
pub face_tight_box_searches_trim: bool,
|
||||
/// `SmTopologyIntersector::IntersectTopology` skips a face pair whose face boxes lie farther apart than the intersection tolerance (a box that cannot be computed, or holds nothing, rules nothing out), and `SmBrep::MakeEdgeInFaceWithVU`, on a plane, leaves in place without classifying an inner loop whose test point lies outside the box of the new face's outer loop (grown by the edges' tolerances, at the retry tolerance). Upstream pairs every face of one sheet with every face of the other through whole-surface checks and classifies every inner loop against each imprinted face, so a 2D boolean of a sheet with n holes takes time in n^2 (WR-527, #1526/#1527; the fork's tangency-labs/smlib@60b1c6a).
|
||||
pub boolean_skips_faces_apart: bool,
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
//! | Rust | Upstream |
|
||||
//! |-----------------------------------------------|---------------------------------------------|
|
||||
//! | [`calculate_tight_bounding_box`], [`calculate_tight_bounding_box_into`] | `SmFace::CalculateTightBoundingBox` |
|
||||
//! | `face_directed_max` (opt-in, WR-588), `cell_directed_max`, `cell_monotone` | `sm_FaceDirectedMax`, `sm_CellDirectedMax`, `sm_CellMonotone` (the fork tangency-labs/smlib@9d44c1b) |
|
||||
//! | `face_directed_max` (opt-in, WR-588), `cell_directed_max`, `cell_monotone`, `cell_division` (WR-651) | `sm_FaceDirectedMax`, `sm_CellDirectedMax`, `sm_CellMonotone` (the fork tangency-labs/smlib@9d44c1b), `sm_CellDivision` (tangency-labs/smlib@326de73) |
|
||||
//! | [`face_classify_uv_line`] | `sm_FaceClassifyUVLine` (file static) |
|
||||
//! | [`face_classify_uv_line_mut`] | `sm_FaceClassifyUVLine` (in place) |
|
||||
//! | [`find_and_classify_loop_bisector`], [`LoopBisector`] | `sms_FindAndClassifyLoopBisector` |
|
||||
@@ -587,10 +587,37 @@ fn eval_pos(eval: &mut SurfaceEval<'_>) -> Result<Point3> {
|
||||
/// value, so the box is never smaller than the face.
|
||||
const FACE_EXTREME_MAX_CELLS: usize = 4096;
|
||||
|
||||
/// How deep [`face_directed_max`] halves a cell (`sm_lFaceExtremeMaxDepth`), each way. A
|
||||
/// How deep [`face_directed_max`] divides a cell (`sm_lFaceExtremeMaxDepth`), each way. A
|
||||
/// cell left undecided at this depth counts with its own greatest value.
|
||||
const FACE_EXTREME_MAX_DEPTH: u32 = 16;
|
||||
|
||||
/// How near a cell's side a knot may lie and still divide it
|
||||
/// (`sm_dFaceExtremeKnotSnap`): `CopySubPatch` snaps a requested side onto a knot within
|
||||
/// this distance, so a part nearer than that is no cell of its own.
|
||||
const FACE_EXTREME_KNOT_SNAP: f64 = 1.0e-08;
|
||||
|
||||
/// Where a cell of `[lo, hi]` is divided along one way (`sm_CellDivision`, WR-651,
|
||||
/// tangency-labs/smlib@326de73): at the knot strictly inside it (by more than
|
||||
/// [`FACE_EXTREME_KNOT_SNAP`]) nearest its middle, the first such in `knots` on a tie,
|
||||
/// else at its middle. Divided at knots, a cell comes to lie within one knot span that
|
||||
/// way, as [`cell_monotone`] asks of a rational patch; halved at middles alone, a cell
|
||||
/// over a knot off the dyadic points of the domain (a screw surface's at every eighth of
|
||||
/// a turn) never does.
|
||||
fn cell_division(knots: &[f64], lo: f64, hi: f64) -> f64 {
|
||||
let mid = 0.5 * (lo + hi);
|
||||
let (mut at, mut off) = (mid, crate::math::BIG_DOUBLE);
|
||||
for &knot in knots {
|
||||
if knot > lo + FACE_EXTREME_KNOT_SNAP
|
||||
&& knot < hi - FACE_EXTREME_KNOT_SNAP
|
||||
&& (knot - mid).abs() < off
|
||||
{
|
||||
at = knot;
|
||||
off = (knot - mid).abs();
|
||||
}
|
||||
}
|
||||
at
|
||||
}
|
||||
|
||||
/// Whether `x` is above zero, false for NaN (the negation of `x > 0`, without the lint).
|
||||
fn is_positive(x: f64) -> bool {
|
||||
x > 0.0
|
||||
@@ -849,7 +876,8 @@ fn cell_monotone(k: &Kernel, surface: &Surface, cell: &Extent2d, dir: Vec3) -> R
|
||||
/// on the face's boundary or on the cell's own sides, which `best` and the neighbouring
|
||||
/// cells hold;
|
||||
///
|
||||
/// otherwise the cell is halved each way. A cell left at [`FACE_EXTREME_MAX_DEPTH`] or past
|
||||
/// otherwise the cell is divided each way ([`cell_division`]: at a knot inside it, else at
|
||||
/// its middle; WR-651). A cell left at [`FACE_EXTREME_MAX_DEPTH`] or past
|
||||
/// [`FACE_EXTREME_MAX_CELLS`] counts with its greatest value. The cells are an explicit
|
||||
/// stack (no recursion), taken last first, as the fork's.
|
||||
fn face_directed_max(
|
||||
@@ -862,6 +890,15 @@ fn face_directed_max(
|
||||
dir: Vec3,
|
||||
mut best: f64,
|
||||
) -> Result<f64> {
|
||||
// (`IsKindOf(SmBSplineSurface_TYPE)`: `GetKnots` through `GetOrCreateGwNurbPointer`)
|
||||
let (knots_u, knots_v) = if has_b_spline_box(surface) {
|
||||
(
|
||||
surface.knots_with_nurb(k, SurfParamType::U, None)?.0,
|
||||
surface.knots_with_nurb(k, SurfParamType::V, None)?.0,
|
||||
)
|
||||
} else {
|
||||
(Vec::new(), Vec::new())
|
||||
};
|
||||
let mut cells: Vec<(Extent2d, u32)> = vec![(*uv_domain, 0)];
|
||||
let mut divided = 0usize;
|
||||
while let Some((cell, depth)) = cells.pop() {
|
||||
@@ -898,13 +935,18 @@ fn face_directed_max(
|
||||
best = top.top;
|
||||
continue;
|
||||
}
|
||||
let mid = cell.mid();
|
||||
let (u0, v0, u1, v1) = (cell.min().x, cell.min().y, cell.max().x, cell.max().y);
|
||||
// WR-651 (tangency-labs/smlib@326de73): divided at a knot inside the cell, else at
|
||||
// its middle (`sm_CellDivision`).
|
||||
let at = Point2::new(
|
||||
cell_division(&knots_u, u0, u1),
|
||||
cell_division(&knots_v, v0, v1),
|
||||
);
|
||||
let child_depth = depth + 1;
|
||||
cells.push((Extent2d::from_bounds(u0, v0, mid.x, mid.y), child_depth));
|
||||
cells.push((Extent2d::from_bounds(mid.x, v0, u1, mid.y), child_depth));
|
||||
cells.push((Extent2d::from_bounds(u0, mid.y, mid.x, v1), child_depth));
|
||||
cells.push((Extent2d::from_bounds(mid.x, mid.y, u1, v1), child_depth));
|
||||
cells.push((Extent2d::from_bounds(u0, v0, at.x, at.y), child_depth));
|
||||
cells.push((Extent2d::from_bounds(at.x, v0, u1, at.y), child_depth));
|
||||
cells.push((Extent2d::from_bounds(u0, at.y, at.x, v1), child_depth));
|
||||
cells.push((Extent2d::from_bounds(at.x, at.y, u1, v1), child_depth));
|
||||
}
|
||||
Ok(best)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
//! WR-651 (tangency-labs/smlib@326de73), the second half of #1799's trimmed-face tight box
|
||||
//! (`Kernel::fixes.face_tight_box_searches_trim`, WR-588): the search divides an undecided
|
||||
//! cell at the surface's knot inside it, not only at its middle.
|
||||
//!
|
||||
//! A rational patch is read monotone only within one knot span. Halved at middles alone, a
|
||||
//! cell over a knot off the dyadic points of the face's domain never comes to lie within one
|
||||
//! span, so along the trim it stays undecided down to the depth limit and counts with its
|
||||
//! surface's top: the box overshoots. SolidCraft's case: an M10x1.5 thread cut from a shaft
|
||||
//! of radius 5 and length 12 boxed to z 12.46875; its screw walls have a knot every eighth
|
||||
//! of a turn.
|
||||
//!
|
||||
//! Here: the 270-degree cylinder of radius 1 about z (three rational quadratic quarter
|
||||
//! spans, knots at u = 1/3 and 2/3), z = v over [0, 1], trimmed to z <= 0.3. Its box is
|
||||
//! (-1, -1, 0) to (1, 1, 0.3).
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::panic,
|
||||
clippy::indexing_slicing
|
||||
)] // test helpers
|
||||
|
||||
use std::f64::consts::FRAC_1_SQRT_2;
|
||||
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::curve::line::Line;
|
||||
use solidcore::geom::extent1d::Extent1d;
|
||||
use solidcore::geom::extent3d::Extent3d;
|
||||
use solidcore::math::Point3;
|
||||
use solidcore::nurbs::b_spline_curve::BSplineCurve;
|
||||
use solidcore::nurbs::b_spline_surface::BSplineSurface;
|
||||
use solidcore::nurbs::crv_basic::Curve as NurbCurve;
|
||||
use solidcore::nurbs::math::CPoint;
|
||||
use solidcore::nurbs::srf_basic::Surface as NurbSurface;
|
||||
use solidcore::primitive::api_trim_ops::trim_surface_with_3d_curves;
|
||||
use solidcore::query::face::p3::calculate_tight_bounding_box;
|
||||
use solidcore::surface::surface::Surface;
|
||||
use solidcore::topology::brep::p1::faces;
|
||||
use solidcore::Kernel;
|
||||
|
||||
/// The arc's knots: three quarter spans, double interior knots at 1/3 and 2/3.
|
||||
fn arc_knots() -> Vec<f64> {
|
||||
let (a, b) = (1.0 / 3.0, 2.0 / 3.0);
|
||||
vec![0.0, 0.0, 0.0, a, a, b, b, 1.0, 1.0, 1.0]
|
||||
}
|
||||
|
||||
/// The arc's control points `(x, y)` and weights, from angle 0 to 270 degrees.
|
||||
fn arc_net() -> Vec<(f64, f64, f64)> {
|
||||
let s = FRAC_1_SQRT_2;
|
||||
vec![
|
||||
(1.0, 0.0, 1.0),
|
||||
(1.0, 1.0, s),
|
||||
(0.0, 1.0, 1.0),
|
||||
(-1.0, 1.0, s),
|
||||
(-1.0, 0.0, 1.0),
|
||||
(-1.0, -1.0, s),
|
||||
(0.0, -1.0, 1.0),
|
||||
]
|
||||
}
|
||||
|
||||
/// The arc at height `z`, reversed when `backwards` (the knots are symmetric).
|
||||
fn arc(z: f64, backwards: bool) -> Curve {
|
||||
let mut net = arc_net();
|
||||
if backwards {
|
||||
net.reverse();
|
||||
}
|
||||
let pw = net
|
||||
.into_iter()
|
||||
.map(|(x, y, w)| CPoint::new(x * w, y * w, z * w, w))
|
||||
.collect();
|
||||
let nurb = NurbCurve::new(pw, 2, arc_knots()).unwrap();
|
||||
Curve::from(BSplineCurve::from_nurb(nurb, 3, false))
|
||||
}
|
||||
|
||||
/// The cylinder: the arc along u, z = v linear over [0, 1].
|
||||
fn cylinder() -> Surface {
|
||||
let net: Vec<Vec<CPoint>> = arc_net()
|
||||
.into_iter()
|
||||
.map(|(x, y, w)| {
|
||||
[0.0, 1.0]
|
||||
.iter()
|
||||
.map(|&z| CPoint::new(x * w, y * w, z * w, w))
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
let nurb = NurbSurface::new(net, 2, 1, arc_knots(), vec![0.0, 0.0, 1.0, 1.0]).unwrap();
|
||||
Surface::from(BSplineSurface::from_nurb(nurb, false))
|
||||
}
|
||||
|
||||
fn line(from: Point3, to: Point3) -> Curve {
|
||||
let interval = Extent1d::from_min_max(0.0, 1.0);
|
||||
Curve::from(Line::create_line_segment(3, from, to, Some(&interval)).unwrap())
|
||||
}
|
||||
|
||||
/// The tight box of the cylinder trimmed to `0 <= z <= top`, with the trim search on or off.
|
||||
fn trimmed_cylinder_box(top: f64, searches_trim: bool) -> Extent3d {
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.face_tight_box_searches_trim = searches_trim;
|
||||
let curves = vec![
|
||||
arc(0.0, false),
|
||||
line(Point3::new(0.0, -1.0, 0.0), Point3::new(0.0, -1.0, top)),
|
||||
arc(top, true),
|
||||
line(Point3::new(1.0, 0.0, top), Point3::new(1.0, 0.0, 0.0)),
|
||||
];
|
||||
let b = trim_surface_with_3d_curves(&mut k, cylinder(), &[4], curves).unwrap();
|
||||
let fs = faces(&b, None).unwrap();
|
||||
assert_eq!(fs.len(), 1);
|
||||
calculate_tight_bounding_box(&k, &b, fs[0]).unwrap()
|
||||
}
|
||||
|
||||
fn assert_close(what: &str, got: f64, want: f64) {
|
||||
assert!((got - want).abs() <= 1e-9, "{what}: {got}, want {want}");
|
||||
}
|
||||
|
||||
/// With the search on, the cells over the knots divide there and are read monotone: the box
|
||||
/// is the face's, exactly. Halved at middles only, the cells over u = 1/3 along the trim at
|
||||
/// z = 0.3 stay undecided to the depth limit and the box reaches above 0.3.
|
||||
#[test]
|
||||
fn trimmed_rational_cylinder_box_is_exact_with_the_search() {
|
||||
let bx = trimmed_cylinder_box(0.3, true);
|
||||
assert_close("z max", bx.max().z, 0.3);
|
||||
assert_close("z min", bx.min().z, 0.0);
|
||||
assert_close("x max", bx.max().x, 1.0);
|
||||
assert_close("x min", bx.min().x, -1.0);
|
||||
assert_close("y max", bx.max().y, 1.0);
|
||||
assert_close("y min", bx.min().y, -1.0);
|
||||
}
|
||||
|
||||
/// Flag off (upstream): the surface's global solves classify outside the face, so the box
|
||||
/// is the vertices' and edges', which here is the face's too.
|
||||
#[test]
|
||||
fn trimmed_rational_cylinder_box_without_the_search_is_the_edges() {
|
||||
let bx = trimmed_cylinder_box(0.3, false);
|
||||
assert_close("z max", bx.max().z, 0.3);
|
||||
assert_close("z min", bx.min().z, 0.0);
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
#### WR-651. The trimmed-face tight box search (WR-588) leaves cells over a knot off the domain's dyadic points undecided, so the box overshoots
|
||||
- **Severity:** Wrong result (a trimmed face's tight box is too large; opt-in path only)
|
||||
- **Area:** Faces / tight bounding box
|
||||
- **Location:** `source/SMLib/src/SmFace.cpp`, `sm_FaceDirectedMax` (the SolidCraft fork's, tangency-labs/smlib@beb6061, ported for WR-588): an undecided cell is halved at its middle. Not in upstream SMLib, which has no such search; this completes WR-588's fix.
|
||||
- **What happens:** `sm_CellMonotone` reads a rational patch's monotonicity only over one knot span. Halved at middles alone, a cell over a knot that is not a dyadic point of the face's UV domain never comes to lie within one span, so every such cell along the trim stays undecided down to the depth limit (16) or the cell limit (4096) and counts with its own greatest value.
|
||||
- **Trigger:** SolidCraft's threads/1250: an M10x1.5 thread groove cut from a shaft of radius 5 and length 12 boxes to z 12.46875 (expected 12); the screw sweep's walls are rational quadratic with a knot every eighth of a turn. In the port: the 270-degree rational cylinder of radius 1 (knots at u = 1/3, 2/3) trimmed to 0 <= z <= 0.3 boxes to z 0.30859375 with the search on (expected 0.3).
|
||||
- **How found:** SolidCraft (the fork, tangency-labs/smlib@326de73); 15 SolidCraft thread cases overshoot without it.
|
||||
- **Confirmed:** in the port (`crates/solidcore/tests/issue_1799_tight_box_divides_at_knots.rs` fails with z max 0.30859375 when the knot division is removed). The pinned C++ has no such search, so there is nothing upstream to confirm.
|
||||
- **Fix (fork):** tangency-labs/smlib@326de73 divides a cell each way at the surface's knot strictly inside it (by more than `CopySubPatch`'s 1e-8 snap) nearest its middle, at its middle only where it holds none (`sm_CellDivision`), the knots read by `GetKnots` when the surface is a kind of `SmBSplineSurface`.
|
||||
- **Rust port:** under the same opt-in `Kernel::fixes.face_tight_box_searches_trim` (WR-588; default `false`, upstream's behaviour): `cell_division` in `face_directed_max` (`crates/solidcore/src/query/face/p3.rs`). It is the rest of the fork's fix for one defect, and the flag has not yet been turned on by any caller. Tests: `trimmed_rational_cylinder_box_is_exact_with_the_search` (flag on: box (-1, -1, 0) to (1, 1, 0.3)) and `trimmed_rational_cylinder_box_without_the_search_is_the_edges` (flag off).
|
||||
Reference in New Issue
Block a user