mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-11 03:01:58 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 61e7055185 | |||
| 4af742e6bb | |||
| e4dcfe48bd | |||
| dd88fe8057 |
+32
@@ -162,6 +162,8 @@ pub struct WorkCounters {
|
||||
face_pairs: Cell<u64>,
|
||||
loops_classified: Cell<u64>,
|
||||
wall_body_copies: Cell<u64>,
|
||||
coincident_curves_classified: Cell<u64>,
|
||||
containment_edge_drops: Cell<u64>,
|
||||
}
|
||||
|
||||
impl WorkCounters {
|
||||
@@ -190,11 +192,29 @@ impl WorkCounters {
|
||||
self.wall_body_copies.get()
|
||||
}
|
||||
|
||||
/// Edge curves of two coincident faces that `FaceIntersect` classifies on both: those
|
||||
/// of each face within reach of the other face's box. Not upstream (the fork's counters
|
||||
/// have none).
|
||||
#[must_use]
|
||||
pub fn coincident_curves_classified(&self) -> u64 {
|
||||
self.coincident_curves_classified.get()
|
||||
}
|
||||
|
||||
/// Edge curves the loop containment test (`query::loop::contains_uv_point`) drops the
|
||||
/// test point onto: those whose box, grown by the edge's tolerance, reaches the point.
|
||||
/// Not upstream (upstream drops it onto every edge of the loop it walks).
|
||||
#[must_use]
|
||||
pub fn containment_edge_drops(&self) -> u64 {
|
||||
self.containment_edge_drops.get()
|
||||
}
|
||||
|
||||
/// Zeroes every count (`SmWorkCounters::Reset`).
|
||||
pub fn reset(&self) {
|
||||
self.face_pairs.set(0);
|
||||
self.loops_classified.set(0);
|
||||
self.wall_body_copies.set(0);
|
||||
self.coincident_curves_classified.set(0);
|
||||
self.containment_edge_drops.set(0);
|
||||
}
|
||||
|
||||
/// Counts one face pair examined for intersection.
|
||||
@@ -213,6 +233,18 @@ impl WorkCounters {
|
||||
self.wall_body_copies
|
||||
.set(self.wall_body_copies.get().saturating_add(1));
|
||||
}
|
||||
|
||||
/// Counts one edge curve of coincident faces classified on both.
|
||||
pub(crate) fn add_coincident_curve_classified(&self) {
|
||||
self.coincident_curves_classified
|
||||
.set(self.coincident_curves_classified.get().saturating_add(1));
|
||||
}
|
||||
|
||||
/// Counts one drop of a containment test's point onto an edge curve.
|
||||
pub(crate) fn add_containment_edge_drop(&self) {
|
||||
self.containment_edge_drops
|
||||
.set(self.containment_edge_drops.get().saturating_add(1));
|
||||
}
|
||||
}
|
||||
|
||||
/// Emits the [`UpstreamFixes`] definition it wraps unchanged, plus
|
||||
|
||||
+91
-8
@@ -169,6 +169,80 @@ fn or_unknown<T>(k: &Kernel, r: Result<T>) -> Result<Option<T>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Not upstream: whether `point` may lie within `reach` of `curve` over `interval`, so
|
||||
/// that [`drop_on_edge`] there can find it (a drop succeeds only for a solution closer than
|
||||
/// its tolerance). `false` only when the point lies outside the box of the curve's points
|
||||
/// over the interval, grown by `reach` and by an evaluation's rounding (`1e-6` of the box's
|
||||
/// size and of the coordinates, as [`WindingHull::excludes`]), in some coordinate. That box
|
||||
/// is, for a line, the box of its points at the interval's ends (its points between lie
|
||||
/// between them); for a B-spline with positive weights whose basis is well made (as many
|
||||
/// control points as its knots and degree call for, the knots in order) over an interval
|
||||
/// inside its domain, the box of its control points (each point is a convex combination of
|
||||
/// them). Every other curve may reach every point. Neither curve's copy for evaluation can
|
||||
/// fail, so leaving out its copy and drop changes no result.
|
||||
fn curve_box_reaches(
|
||||
k: &Kernel,
|
||||
curve: &Curve,
|
||||
interval: &Extent1d,
|
||||
point: Point3,
|
||||
reach: f64,
|
||||
) -> bool {
|
||||
use crate::curve::curve::CurveKind;
|
||||
use crate::nurbs::math::NOW;
|
||||
let mut lo = Point3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
|
||||
let mut hi = Point3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
|
||||
let mut add = |e: Point3| {
|
||||
lo = Point3::new(lo.x.min(e.x), lo.y.min(e.y), lo.z.min(e.z));
|
||||
hi = Point3::new(hi.x.max(e.x), hi.y.max(e.y), hi.z.max(e.z));
|
||||
};
|
||||
match curve.kind() {
|
||||
CurveKind::Line(_) => {
|
||||
for t in [interval.min(), interval.max()] {
|
||||
let Ok(e) = curve.evaluate_point(k, t) else {
|
||||
return true;
|
||||
};
|
||||
add(e);
|
||||
}
|
||||
}
|
||||
CurveKind::BSpline(c) => {
|
||||
let Some(nurb) = c.nurb.as_ref() else {
|
||||
return true;
|
||||
};
|
||||
let p = nurb.degree();
|
||||
let cpts = nurb.cpts();
|
||||
let well_made = p >= 1
|
||||
&& cpts.len() > p
|
||||
&& nurb.knots().len() == cpts.len() + p + 1
|
||||
&& nurb
|
||||
.knots()
|
||||
.windows(2)
|
||||
.all(|w| matches!(w, [a, b] if a <= b));
|
||||
let domain = c.natural_interval();
|
||||
if !(well_made && interval.min() >= domain.min() && interval.max() <= domain.max()) {
|
||||
return true;
|
||||
}
|
||||
for c in cpts {
|
||||
if !(c.w == NOW || (c.w > 0.0 && c.w.is_finite())) {
|
||||
return true;
|
||||
}
|
||||
add(c.to_euclid());
|
||||
}
|
||||
}
|
||||
_ => return true,
|
||||
}
|
||||
if ![lo.x, lo.y, lo.z, hi.x, hi.y, hi.z]
|
||||
.iter()
|
||||
.all(|v| v.is_finite())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
let apart = |lo: f64, hi: f64, x: f64| {
|
||||
let m = reach + 1e-6 * (1.0 + (hi - lo) + lo.abs().max(hi.abs()) + x.abs());
|
||||
x < lo - m || x > hi + m
|
||||
};
|
||||
!(apart(lo.x, hi.x, point.x) || apart(lo.y, hi.y, point.y) || apart(lo.z, hi.z, point.z))
|
||||
}
|
||||
|
||||
/// `LocalPointSolve(SM_SO_MINIMIZE)` of the test point on a UV trim curve: whether it
|
||||
/// converged and the solution. A failing solve has not converged (its status is
|
||||
/// ignored upstream and the flag is reset first); with the opt-in
|
||||
@@ -421,18 +495,27 @@ pub(crate) fn contains_uv_point_with_topology(
|
||||
let this_edge = edge_of(b, this_eu)?;
|
||||
|
||||
// low work: see if TgtPoint is on this EdgeCurve
|
||||
// (not upstream) only where the curve's box, grown by the edge's tolerance, reaches
|
||||
// the point: the drop finds the point only within that tolerance of the curve
|
||||
// ([`curve_box_reaches`]). Upstream copies and drops onto every edge of the loop, so
|
||||
// a face of n edges classified at n points made n^2 drops.
|
||||
let on_edge_curve = {
|
||||
let edge = b.edge(this_edge)?;
|
||||
// the drop builds the edge curve's cache at the edge's tolerance
|
||||
// (SmTol::GetApproxTol3d(&m_crCurve) reads pEdge->GetTolerance() through the
|
||||
// owner)
|
||||
let mut curve = edge
|
||||
let curve = edge
|
||||
.curve
|
||||
.as_ref()
|
||||
.ok_or_else(|| null("ContainsUVPoint: edge without curve"))?
|
||||
.clone_for_evaluation()?;
|
||||
curve.set_owner_zone_tol_3d(Some(edge.tolerance));
|
||||
drop_on_edge(k, &curve, &edge.interval, point_3d, edge.tolerance)?
|
||||
.ok_or_else(|| null("ContainsUVPoint: edge without curve"))?;
|
||||
if curve_box_reaches(k, curve, &edge.interval, point_3d, edge.tolerance) {
|
||||
k.work_counters.add_containment_edge_drop();
|
||||
// the drop builds the edge curve's cache at the edge's tolerance
|
||||
// (SmTol::GetApproxTol3d(&m_crCurve) reads pEdge->GetTolerance() through the
|
||||
// owner)
|
||||
let mut curve = curve.clone_for_evaluation()?;
|
||||
curve.set_owner_zone_tol_3d(Some(edge.tolerance));
|
||||
drop_on_edge(k, &curve, &edge.interval, point_3d, edge.tolerance)?
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(param) = on_edge_curve {
|
||||
// TgtPoint is on EdgeCurve - set output and return
|
||||
|
||||
+37
-1
@@ -102,6 +102,7 @@ use crate::curve::curve::Curve;
|
||||
use crate::error::{ignore_status, ignored, EntityRef, Result, SolidError};
|
||||
use crate::geom::extent1d::{Extent1d, ExtentPointType};
|
||||
use crate::geom::extent2d::{Extent2d, SS_UMAX, SS_UMIN, SS_VMAX, SS_VMIN};
|
||||
use crate::geom::extent3d::Extent3d;
|
||||
use crate::geom::object::{NotifyOperation, ObjectRef};
|
||||
use crate::geom::solution_array::{ObjectKind, Solution, SolutionObject, MAX_TREES};
|
||||
use crate::geom::tol::xsect_tol_3d_from_zones;
|
||||
@@ -2520,8 +2521,26 @@ pub fn face_intersect_breps_cached(
|
||||
let mut have_curves = false;
|
||||
if are_coincident && coinc_dist < approx_tol_3d {
|
||||
have_curves = true;
|
||||
'faces: for (fb, face) in [(b, f), (ob, other)] {
|
||||
// (not upstream) an edge whose box lies apart from the other face's box lies inside
|
||||
// no part of it, and its classification on both faces keeps nothing: it is left out.
|
||||
// Upstream classifies every edge of each face on both, the other face's edges again
|
||||
// for each face it is paired with, so a 2D boolean of one face of n edges against m
|
||||
// small faces classified n·m curves, each against all n edges. A box that cannot be
|
||||
// computed, or holds nothing, rules nothing out.
|
||||
let reach = smos_max(approx_tol_3d, zone_tol);
|
||||
let face_box = |fb: &Brep, face: FaceId| {
|
||||
crate::query::face::p1::calculate_bounding_box(k, fb, face, false)
|
||||
.ok()
|
||||
.filter(|x| !x.has_negative_volume())
|
||||
};
|
||||
let boxes = [face_box(ob, other), face_box(b, f)];
|
||||
'faces: for ((fb, face), apart_from) in [(b, f), (ob, other)].into_iter().zip(boxes) {
|
||||
for e in face_edges(fb, face)? {
|
||||
if let Some(far) = &apart_from {
|
||||
if edge_box(k, fb, e).is_some_and(|x| x.are_disjoint(far, reach)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let edge_crv = edge_curve(fb, e)?;
|
||||
if edge_crv.is_degenerate(k, EFF_ZERO, None)? {
|
||||
continue;
|
||||
@@ -2541,6 +2560,7 @@ pub fn face_intersect_breps_cached(
|
||||
uv_curves1.push(None);
|
||||
uv_curves2.push(None);
|
||||
curve_types.push(TsectCurveType::RegionBoundary);
|
||||
k.work_counters.add_coincident_curve_classified();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2772,3 +2792,19 @@ pub fn face_intersect_breps_cached(
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The axis aligned box of edge `e`'s curve over its interval, grown by the edge's tolerance;
|
||||
/// `None` when it cannot be computed or holds nothing.
|
||||
fn edge_box(k: &Kernel, b: &Brep, e: EdgeId) -> Option<Extent3d> {
|
||||
let request = crate::nurbs::b_spline_curve::p2::BoundingBoxRequest {
|
||||
normal: true,
|
||||
pseudo: false,
|
||||
polar: false,
|
||||
};
|
||||
let mut x = crate::query::edge::calculate_bounding_box(k, b, e, request, false)
|
||||
.ok()?
|
||||
.normal
|
||||
.filter(|x| !x.has_negative_volume())?;
|
||||
x.expand_absolute(edge::tolerance(b, e).ok()?);
|
||||
Some(x)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
//! A plate whose sides are split into n segments each, less n x n square holes cut in 2D
|
||||
//! (`boolean_2d`, the holes united in balanced rounds first), n = 4 and 8: the work of
|
||||
//! `FaceIntersect` on the coincident faces it pairs (SolidCraft's sheet metal flat patterns,
|
||||
//! a panel of many edges less its bends' trims, spent most of each 2D boolean there).
|
||||
//!
|
||||
//! `Boolean2D` pairs the plate's face with each hole's (their boxes overlap). Upstream's
|
||||
//! `FaceIntersect` of two coincident faces copies every edge of both and classifies each on
|
||||
//! both faces: the plate's 4n edges again for every hole, (4n + 4) n^2 curves, 320 and 2304,
|
||||
//! n^3, which is what this test counts without the box test. An edge whose box lies apart
|
||||
//! from the other face's box lies inside no part of it and is left out: the plate's sides
|
||||
//! lie 18 mm from every hole, so each pair classifies its hole's 4 edges only, 4 n^2 curves,
|
||||
//! 64 and 256, four times the curves for four times the holes. The plate keeps its exact
|
||||
//! area either way.
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::panic,
|
||||
clippy::indexing_slicing
|
||||
)] // test helpers
|
||||
|
||||
use solidcore::boolean::api_brep::{boolean_2d, Boolean2d, Boolean2dOutcome};
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::mass::integrator::properties::brep_area;
|
||||
use solidcore::math::Point3;
|
||||
use solidcore::primitive::api_curves::create_line_segment;
|
||||
use solidcore::primitive::api_trim_ops::create_planar_faces;
|
||||
use solidcore::{Brep, Kernel};
|
||||
|
||||
const SIDE: f64 = 4.0;
|
||||
const PITCH: f64 = 20.0;
|
||||
|
||||
fn boolean(k: &mut Kernel, a: Brep, b: Brep, op: Boolean2d) -> Brep {
|
||||
match boolean_2d(k, a, b, op).unwrap() {
|
||||
Boolean2dOutcome::Result(r) => r,
|
||||
Boolean2dOutcome::Unassigned { .. } => panic!("2D boolean left its operands"),
|
||||
}
|
||||
}
|
||||
|
||||
/// A planar face bounded by the closed polygon through `corners`.
|
||||
fn polygon(k: &mut Kernel, corners: &[(f64, f64)]) -> Brep {
|
||||
let sides: Vec<Curve> = (0..corners.len())
|
||||
.map(|i| {
|
||||
let (a, b) = (corners[i], corners[(i + 1) % corners.len()]);
|
||||
Curve::from(
|
||||
create_line_segment(Point3::new(a.0, a.1, 0.0), Point3::new(b.0, b.1, 0.0))
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
create_planar_faces(k, &sides).unwrap()
|
||||
}
|
||||
|
||||
/// The `w` x `w` plate from the origin, each side split into `n` equal segments.
|
||||
fn plate(k: &mut Kernel, w: f64, n: u32) -> Brep {
|
||||
let step = w / f64::from(n);
|
||||
let mut corners = Vec::new();
|
||||
for (from, along) in [
|
||||
((0.0, 0.0), (1.0, 0.0)),
|
||||
((w, 0.0), (0.0, 1.0)),
|
||||
((w, w), (-1.0, 0.0)),
|
||||
((0.0, w), (0.0, -1.0)),
|
||||
] {
|
||||
for i in 0..n {
|
||||
let d = step * f64::from(i);
|
||||
corners.push((from.0 + along.0 * d, from.1 + along.1 * d));
|
||||
}
|
||||
}
|
||||
polygon(k, &corners)
|
||||
}
|
||||
|
||||
/// The plate with `n` x `n` square holes cut in 2D: checks its exact area and returns the
|
||||
/// coincident edge curves `FaceIntersect` classified.
|
||||
fn curves_classified(n: u32) -> u64 {
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.boolean_skips_faces_apart = true;
|
||||
k.work_counters.reset();
|
||||
let h = SIDE / 2.0;
|
||||
let mut holes = Vec::new();
|
||||
for i in 0..n {
|
||||
for j in 0..n {
|
||||
let (x, y) = (PITCH * f64::from(i + 1), PITCH * f64::from(j + 1));
|
||||
holes.push(polygon(
|
||||
&mut k,
|
||||
&[
|
||||
(x - h, y - h),
|
||||
(x + h, y - h),
|
||||
(x + h, y + h),
|
||||
(x - h, y + h),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
while holes.len() > 1 {
|
||||
let mut next = Vec::new();
|
||||
let mut round = holes.into_iter();
|
||||
while let Some(a) = round.next() {
|
||||
match round.next() {
|
||||
Some(b) => next.push(boolean(&mut k, a, b, Boolean2d::Union)),
|
||||
None => next.push(a),
|
||||
}
|
||||
}
|
||||
holes = next;
|
||||
}
|
||||
let w = PITCH * f64::from(n + 1);
|
||||
let plate = plate(&mut k, w, n);
|
||||
let mut cut = boolean(&mut k, plate, holes.pop().unwrap(), Boolean2d::Difference);
|
||||
let curves = k.work_counters.coincident_curves_classified();
|
||||
let area = brep_area(&k, &mut cut, 1e-8).unwrap();
|
||||
let exact = w * w - f64::from(n * n) * SIDE * SIDE;
|
||||
println!(
|
||||
"plate less {} holes: {curves} coincident curves classified, area {area}",
|
||||
n * n
|
||||
);
|
||||
assert!(
|
||||
(area - exact).abs() <= 1e-9 * exact,
|
||||
"area {area}, exact {exact}"
|
||||
);
|
||||
curves
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coincident_faces_classify_only_the_edges_near_each_other() {
|
||||
assert_eq!((curves_classified(4), curves_classified(8)), (64, 256));
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
//! A plate whose sides are split into n segments each, classified at n x n points inside it
|
||||
//! and at one point on its first side (`face_classify_uv`), n = 4 and 8: the work of the loop
|
||||
//! containment test (`query::loop::contains_uv_point`) the classification makes for each
|
||||
//! point (SolidCraft's sheet metal flat patterns, faces of many edges, spent most of each 2D
|
||||
//! boolean there: the boolean classifies each new edge's midpoint on the face it splits).
|
||||
//!
|
||||
//! Upstream's containment test drops the point onto every edge of the loop to see whether it
|
||||
//! lies on one, up to the edge it lies on: 4n drops for each point inside, 2 for the point on
|
||||
//! the first side (its segment is the second edge the walk visits), 4n^3 + 2 for the plate,
|
||||
//! 258 and 2050, which is what this test counts without the box test. A drop finds the point only within the edge's tolerance,
|
||||
//! so an edge whose box, grown by that tolerance, lies apart from the point is not dropped
|
||||
//! onto: the points inside lie 10 mm or more from every side, and the point on the first
|
||||
//! side reaches the box of the one segment it lies on, 1 drop for either plate. Every point
|
||||
//! keeps its class.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)] // test helpers
|
||||
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::intersect::point_class::PointClassType;
|
||||
use solidcore::math::Point3;
|
||||
use solidcore::primitive::api_curves::create_line_segment;
|
||||
use solidcore::primitive::api_trim_ops::create_planar_faces;
|
||||
use solidcore::query::api_queries::{face_classify_uv, surface_closest_point};
|
||||
use solidcore::topology::face::p2::surface;
|
||||
use solidcore::{Brep, Kernel};
|
||||
|
||||
const WIDTH: f64 = 100.0;
|
||||
|
||||
/// The `WIDTH` square plate from the origin, each side split into `n` equal segments.
|
||||
fn plate(k: &mut Kernel, n: u32) -> Brep {
|
||||
let step = WIDTH / f64::from(n);
|
||||
let mut corners = Vec::new();
|
||||
for (from, along) in [
|
||||
((0.0, 0.0), (1.0, 0.0)),
|
||||
((WIDTH, 0.0), (0.0, 1.0)),
|
||||
((WIDTH, WIDTH), (-1.0, 0.0)),
|
||||
((0.0, WIDTH), (0.0, -1.0)),
|
||||
] {
|
||||
for i in 0..n {
|
||||
let d = step * f64::from(i);
|
||||
corners.push((from.0 + along.0 * d, from.1 + along.1 * d));
|
||||
}
|
||||
}
|
||||
let sides: Vec<Curve> = (0..corners.len())
|
||||
.map(|i| {
|
||||
let (a, b) = (corners[i], corners[(i + 1) % corners.len()]);
|
||||
Curve::from(
|
||||
create_line_segment(Point3::new(a.0, a.1, 0.0), Point3::new(b.0, b.1, 0.0))
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
create_planar_faces(k, &sides).unwrap()
|
||||
}
|
||||
|
||||
/// The plate of `4n` edges classified at `n` x `n` points inside it and at the middle of its
|
||||
/// first segment: checks each class and returns the containment test's edge drops.
|
||||
fn edge_drops(n: u32) -> u64 {
|
||||
let mut k = Kernel::default();
|
||||
let mut b = plate(&mut k, n);
|
||||
let f = b.face_list()[0];
|
||||
let srf = surface(&b, f).unwrap().unwrap().clone();
|
||||
let pitch = (WIDTH - 20.0) / f64::from(n - 1);
|
||||
let mut points = Vec::new();
|
||||
for i in 0..n {
|
||||
for j in 0..n {
|
||||
let p = Point3::new(
|
||||
10.0 + pitch * f64::from(i),
|
||||
10.0 + pitch * f64::from(j),
|
||||
0.0,
|
||||
);
|
||||
points.push((p, PointClassType::Face));
|
||||
}
|
||||
}
|
||||
points.push((
|
||||
Point3::new(WIDTH / f64::from(2 * n), 0.0, 0.0),
|
||||
PointClassType::Edge,
|
||||
));
|
||||
let uvs: Vec<_> = points
|
||||
.iter()
|
||||
.map(|(p, _)| surface_closest_point(&k, &srf, *p).unwrap().uvs[0])
|
||||
.collect();
|
||||
k.work_counters.reset();
|
||||
for ((p, class), uv) in points.iter().zip(uvs) {
|
||||
assert_eq!(
|
||||
face_classify_uv(&k, &mut b, f, uv).unwrap(),
|
||||
*class,
|
||||
"at {p:?}"
|
||||
);
|
||||
}
|
||||
let drops = k.work_counters.containment_edge_drops();
|
||||
println!(
|
||||
"plate of {} edges at {} points: {drops} edge drops",
|
||||
4 * n,
|
||||
points.len()
|
||||
);
|
||||
drops
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn containment_drops_onto_the_edges_near_the_point_only() {
|
||||
assert_eq!((edge_drops(4), edge_drops(8)), (1, 1));
|
||||
}
|
||||
Reference in New Issue
Block a user