mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-11 11:11:59 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6678f4ee9c |
+20
-19
@@ -49,6 +49,7 @@ use crate::{FacePlan, TessPlan};
|
||||
use solidcore::eval::EvalSurface;
|
||||
use solidcore_record::Vec3;
|
||||
use solidcore_record::brep::{self, Curve, Loop, Shell};
|
||||
pub(crate) use surface::FaceSurface;
|
||||
|
||||
use crate::{Result, TessError};
|
||||
|
||||
@@ -225,12 +226,12 @@ impl std::fmt::Display for Unclassified {
|
||||
/// Whether `s` is collapsed along `d` at `q`, to within `reach` (a pole, or a patch side
|
||||
/// collapsed to within a tolerance), as the kernel's classifier says; its refusal the error.
|
||||
pub(crate) fn pole_at(
|
||||
s: &EvalSurface,
|
||||
s: &FaceSurface,
|
||||
q: [f64; 2],
|
||||
d: usize,
|
||||
reach: f64,
|
||||
) -> std::result::Result<bool, Unclassified> {
|
||||
solidcore::eval::uv::degenerate_within(s, q, d, reach).map_err(|e| Unclassified {
|
||||
s.collapsed(q, d, reach).map_err(|e| Unclassified {
|
||||
at: q,
|
||||
why: crate::eval_message(e),
|
||||
})
|
||||
@@ -238,7 +239,7 @@ pub(crate) fn pole_at(
|
||||
|
||||
/// The first direction (u, then v) `s` is collapsed along at `q` ([`pole_at`]), if any.
|
||||
pub(crate) fn pole_dir(
|
||||
s: &EvalSurface,
|
||||
s: &FaceSurface,
|
||||
q: [f64; 2],
|
||||
reach: f64,
|
||||
) -> std::result::Result<Option<usize>, Unclassified> {
|
||||
@@ -445,10 +446,10 @@ fn plan_at(b: &brep::Brep, tol: f64, turn: Option<size::Turn>) -> Result<(TessPl
|
||||
// one about a hundred, so the pool is used only where curved faces (or intersection
|
||||
// curves) make up the work. A face refused at one step (its surface, an edge of it, its
|
||||
// boundary) is passed over by the steps after, and left out of the plan, named.
|
||||
let mut evals: Vec<Option<EvalSurface>> = Vec::new();
|
||||
let mut evals: Vec<Option<FaceSurface>> = Vec::new();
|
||||
for (fi, f) in faces.iter().enumerate() {
|
||||
let s = match surface::eval_surface(f.parameters()) {
|
||||
Ok(s) => Some(s),
|
||||
Ok(s) => Some(FaceSurface::new(s)),
|
||||
Err(e) => {
|
||||
refused.face(
|
||||
fi,
|
||||
@@ -462,7 +463,7 @@ fn plan_at(b: &brep::Brep, tol: f64, turn: Option<size::Turn>) -> Result<(TessPl
|
||||
let curved = evals
|
||||
.iter()
|
||||
.flatten()
|
||||
.filter(|e| !matches!(e, EvalSurface::Plane { .. }))
|
||||
.filter(|e| !matches!(***e, EvalSurface::Plane { .. }))
|
||||
.count();
|
||||
let sizes: Vec<Option<size::FaceSize>> =
|
||||
crate::par::map("face sizes", &evals, curved >= PARALLEL_SIZES, |s| {
|
||||
@@ -636,7 +637,7 @@ fn plan_at(b: &brep::Brep, tol: f64, turn: Option<size::Turn>) -> Result<(TessPl
|
||||
let surface = match eval {
|
||||
Some(eval) => {
|
||||
let i = u32::try_from(plan.surfaces.len()).map_err(|_| too_many())?;
|
||||
plan.surfaces.push(eval);
|
||||
plan.surfaces.push(eval.into_eval());
|
||||
Some(i)
|
||||
}
|
||||
None => None,
|
||||
@@ -790,7 +791,7 @@ fn boundary(
|
||||
b: &brep::Brep,
|
||||
fi: usize,
|
||||
face: &brep::Face,
|
||||
s: &EvalSurface,
|
||||
s: &FaceSurface,
|
||||
nodes: &[edges::Nodes],
|
||||
rules: &size::Rules,
|
||||
) -> Result<Boundary> {
|
||||
@@ -1133,7 +1134,7 @@ struct Boundary {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn untangle(
|
||||
b: &brep::Brep,
|
||||
evals: &[Option<EvalSurface>],
|
||||
evals: &[Option<FaceSurface>],
|
||||
edge_faces: &[Vec<usize>],
|
||||
tol: f64,
|
||||
nodes: &mut [edges::Nodes],
|
||||
@@ -1155,7 +1156,7 @@ fn untangle(
|
||||
let curved = evals
|
||||
.iter()
|
||||
.flatten()
|
||||
.filter(|e| !matches!(e, EvalSurface::Plane { .. }))
|
||||
.filter(|e| !matches!(***e, EvalSurface::Plane { .. }))
|
||||
.count();
|
||||
let found: Vec<Option<Vec<(u32, u32)>>> = crate::par::map(
|
||||
"boundary crossings",
|
||||
@@ -1254,7 +1255,7 @@ fn untangle(
|
||||
/// emptied, so it crosses nothing.
|
||||
fn rebuild(
|
||||
fi: usize,
|
||||
evals: &[Option<EvalSurface>],
|
||||
evals: &[Option<FaceSurface>],
|
||||
nodes: &[edges::Nodes],
|
||||
boundaries: &mut [Boundary],
|
||||
face_boundary: &(dyn Fn(usize, &[edges::Nodes]) -> Result<Boundary> + Sync),
|
||||
@@ -1309,7 +1310,7 @@ const MAX_SNAP: u32 = 8;
|
||||
/// (in `refused`), which are passed over from then on.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn snap_touches(
|
||||
evals: &[Option<EvalSurface>],
|
||||
evals: &[Option<FaceSurface>],
|
||||
edge_faces: &[Vec<usize>],
|
||||
tol: f64,
|
||||
nodes: &mut [edges::Nodes],
|
||||
@@ -1660,7 +1661,7 @@ fn area2(uv: &[[f64; 2]]) -> f64 {
|
||||
/// second pole back to the first is moved one period along, whichever way keeps the loop
|
||||
/// within one period with a positive area: the seam's two uses then lie a period apart.
|
||||
fn open_pole_to_pole(
|
||||
s: &EvalSurface,
|
||||
s: &FaceSurface,
|
||||
dom: &solidcore::eval::uv::Domain,
|
||||
reach: f64,
|
||||
l: &mut seam::Loop,
|
||||
@@ -1751,7 +1752,7 @@ fn open_pole_to_pole(
|
||||
/// The poles of a loop where it turns straight back along a seam: the nodes either side have
|
||||
/// the same parameters.
|
||||
fn turning_poles(
|
||||
s: &EvalSurface,
|
||||
s: &FaceSurface,
|
||||
reach: f64,
|
||||
per: f64,
|
||||
uv: &[[f64; 2]],
|
||||
@@ -1780,7 +1781,7 @@ fn turning_poles(
|
||||
/// their nodes as every other edge does. Kept as it was unless the opened loop spans at most a
|
||||
/// period with a positive area.
|
||||
fn open_at_pole(
|
||||
s: &EvalSurface,
|
||||
s: &FaceSurface,
|
||||
dom: &solidcore::eval::uv::Domain,
|
||||
reach: f64,
|
||||
l: &mut seam::Loop,
|
||||
@@ -1913,7 +1914,7 @@ fn holes_beside_outer(dom: &solidcore::eval::uv::Domain, loops: &mut [seam::Loop
|
||||
fn plan_or_fill(
|
||||
fi: u32,
|
||||
kind: &str,
|
||||
s: &EvalSurface,
|
||||
s: &FaceSurface,
|
||||
loops: &[seam::Loop],
|
||||
rules: &size::Rules,
|
||||
size: &size::FaceSize,
|
||||
@@ -1976,7 +1977,7 @@ fn plan_or_fill(
|
||||
fn plan_face(
|
||||
fi: u32,
|
||||
kind: &str,
|
||||
s: &EvalSurface,
|
||||
s: &FaceSurface,
|
||||
loops: &[seam::Loop],
|
||||
rules: &size::Rules,
|
||||
size: &size::FaceSize,
|
||||
@@ -2137,7 +2138,7 @@ fn plan_face(
|
||||
let fill = cdt::Fill {
|
||||
seeds: &seeds,
|
||||
refine: Some((sag, MAX_REFINE)),
|
||||
shape: !matches!(s, EvalSurface::Plane { .. }),
|
||||
shape: !matches!(**s, EvalSurface::Plane { .. }),
|
||||
lenient: lenient == Lenient::Yes,
|
||||
};
|
||||
cdt::triangulate(&uvs, &ranges, fill)
|
||||
@@ -2146,7 +2147,7 @@ fn plan_face(
|
||||
};
|
||||
// Planar and curved faces alike: a structured grid where the boundary runs along the
|
||||
// parameter box (a rectangle, a cylinder half), a Coons grid on four corners, else Delaunay.
|
||||
let planar = matches!(s, EvalSurface::Plane { .. });
|
||||
let planar = matches!(**s, EvalSurface::Plane { .. });
|
||||
// The lenient fill is the constrained Delaunay alone: the caller names the face in the stats.
|
||||
let (extra, triangles) = if lenient == Lenient::Yes {
|
||||
delaunay()?
|
||||
|
||||
+84
-11
@@ -3,6 +3,7 @@
|
||||
//! needs, a node of a seam cut, or a point a size rule probes between the nodes it places).
|
||||
//! Nothing is substituted for the point, and no point is skipped.
|
||||
|
||||
use super::FaceSurface;
|
||||
use solidcore::eval::{EvalCurve, EvalSurface};
|
||||
use solidcore_convert as kernel;
|
||||
use solidcore_record::Vec3;
|
||||
@@ -100,7 +101,16 @@ fn a_face_whose_surface_refuses_an_inner_point_is_refused_whole() {
|
||||
let s = cylinder_without(0.5);
|
||||
let loops = [quarter(&s, 8, 1.0)];
|
||||
let rules = Rules::new(0.01, 40.0, None);
|
||||
let r = plan_or_fill(0, "revolution", &s, &loops, &rules, &size(1.0), 0.0, None);
|
||||
let r = plan_or_fill(
|
||||
0,
|
||||
"revolution",
|
||||
&FaceSurface::new(s.clone()),
|
||||
&loops,
|
||||
&rules,
|
||||
&size(1.0),
|
||||
0.0,
|
||||
None,
|
||||
);
|
||||
match r {
|
||||
Err(e) => assert!(
|
||||
e.to_string()
|
||||
@@ -156,7 +166,16 @@ fn a_face_whose_surface_refuses_a_point_between_its_nodes_is_refused_whole() {
|
||||
let s = cylinder_without(0.25);
|
||||
let loops = [quarter(&s, 16, 1.0)];
|
||||
let rules = Rules::new(0.01, 40.0, None);
|
||||
let r = plan_or_fill(0, "revolution", &s, &loops, &rules, &size(50.0), 0.0, None);
|
||||
let r = plan_or_fill(
|
||||
0,
|
||||
"revolution",
|
||||
&FaceSurface::new(s.clone()),
|
||||
&loops,
|
||||
&rules,
|
||||
&size(50.0),
|
||||
0.0,
|
||||
None,
|
||||
);
|
||||
match r {
|
||||
Err(e) => assert!(
|
||||
e.to_string()
|
||||
@@ -224,7 +243,16 @@ fn a_size_sample_the_surface_refuses_refuses_only_a_face_it_lies_in() {
|
||||
);
|
||||
// A face beside the circle: the samples on it lie outside, and it meshes.
|
||||
let low = [quarter(&s, 16, 0.25)];
|
||||
match plan_or_fill(0, "revolution", &s, &low, &rules, &size, 0.0, None) {
|
||||
match plan_or_fill(
|
||||
0,
|
||||
"revolution",
|
||||
&FaceSurface::new(s.clone()),
|
||||
&low,
|
||||
&rules,
|
||||
&size,
|
||||
0.0,
|
||||
None,
|
||||
) {
|
||||
Ok((fp, stats)) => assert!(
|
||||
!fp.triangles.is_empty() && stats.lenient.is_empty(),
|
||||
"{stats:?}"
|
||||
@@ -241,7 +269,16 @@ fn a_size_sample_the_surface_refuses_refuses_only_a_face_it_lies_in() {
|
||||
let Some(inside) = inside else {
|
||||
panic!("a sample inside the quarter turn: {size:?}")
|
||||
};
|
||||
match plan_or_fill(0, "revolution", &s, &whole, &rules, &size, 0.0, None) {
|
||||
match plan_or_fill(
|
||||
0,
|
||||
"revolution",
|
||||
&FaceSurface::new(s.clone()),
|
||||
&whole,
|
||||
&rules,
|
||||
&size,
|
||||
0.0,
|
||||
None,
|
||||
) {
|
||||
Err(e) => assert!(
|
||||
e.to_string()
|
||||
.contains(&format!("does not evaluate at {inside:?}")),
|
||||
@@ -319,7 +356,16 @@ fn a_point_in_a_faces_notch_the_surface_does_not_evaluate_at_does_not_refuse_it(
|
||||
[0.0, 1.0],
|
||||
],
|
||||
)];
|
||||
match plan_or_fill(0, "spline", &s, ¬ched, &rules, &size, 0.0, None) {
|
||||
match plan_or_fill(
|
||||
0,
|
||||
"spline",
|
||||
&FaceSurface::new(s.clone()),
|
||||
¬ched,
|
||||
&rules,
|
||||
&size,
|
||||
0.0,
|
||||
None,
|
||||
) {
|
||||
Ok((fp, stats)) => assert!(
|
||||
!fp.triangles.is_empty() && stats.lenient.is_empty(),
|
||||
"{stats:?}"
|
||||
@@ -330,7 +376,16 @@ fn a_point_in_a_faces_notch_the_surface_does_not_evaluate_at_does_not_refuse_it(
|
||||
&s,
|
||||
&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
|
||||
)];
|
||||
match plan_or_fill(0, "spline", &s, &square, &rules, &size, 0.0, None) {
|
||||
match plan_or_fill(
|
||||
0,
|
||||
"spline",
|
||||
&FaceSurface::new(s.clone()),
|
||||
&square,
|
||||
&rules,
|
||||
&size,
|
||||
0.0,
|
||||
None,
|
||||
) {
|
||||
Err(e) => assert!(
|
||||
e.to_string()
|
||||
.contains("the surface does not evaluate at [0.5, 0.5]"),
|
||||
@@ -382,7 +437,7 @@ fn the_flips_name_a_middle_the_surface_does_not_evaluate_at() {
|
||||
match super::sag::flip(
|
||||
&s,
|
||||
0.01,
|
||||
&mut super::sag::Poles::new(&s, 1e-9),
|
||||
&mut super::sag::Poles::new(&FaceSurface::new(s.clone()), 1e-9),
|
||||
&fixed,
|
||||
&uv,
|
||||
&mut tris,
|
||||
@@ -482,7 +537,16 @@ fn a_division_point_too_many_periods_away_to_place_refuses_the_face_for_that() {
|
||||
let s = cylinder_without(0.5);
|
||||
let loops = [long_strip(&s)];
|
||||
let rules = Rules::new(0.01, 40.0, None);
|
||||
match plan_or_fill(0, "revolution", &s, &loops, &rules, &size(1.0), 0.0, None) {
|
||||
match plan_or_fill(
|
||||
0,
|
||||
"revolution",
|
||||
&FaceSurface::new(s.clone()),
|
||||
&loops,
|
||||
&rules,
|
||||
&size(1.0),
|
||||
0.0,
|
||||
None,
|
||||
) {
|
||||
Err(e) => assert!(e.to_string().contains("periods, more than the 4096"), "{e}"),
|
||||
Ok((fp, _)) => panic!("meshed with {} triangles", fp.triangles.len()),
|
||||
}
|
||||
@@ -514,7 +578,7 @@ fn a_size_sample_too_many_periods_away_to_place_refuses_the_face_for_that() {
|
||||
unevaluated: vec![[0.5, 0.3]],
|
||||
..size(1.0)
|
||||
};
|
||||
match plan_or_fill(0, "revolution", &s, &loops, &rules, &size, 0.0, None) {
|
||||
match plan_or_fill(0, "revolution", &FaceSurface::new(s.clone()), &loops, &rules, &size, 0.0, None) {
|
||||
Err(e) => assert!(
|
||||
e.to_string().ends_with(
|
||||
"periods, more than the 4096 a point the surface does not evaluate at ([0.5, 0.3]) is looked for in"
|
||||
@@ -553,7 +617,7 @@ fn the_worst_sag_is_measured_where_the_surface_evaluates() {
|
||||
let uv = [[0.0, 0.0], [0.75, -0.5], [0.75, 0.5]];
|
||||
let got = super::sag::worst(
|
||||
&s,
|
||||
&mut super::sag::Poles::new(&s, 1e-9),
|
||||
&mut super::sag::Poles::new(&FaceSurface::new(s.clone()), 1e-9),
|
||||
&[],
|
||||
&uv,
|
||||
&[[0, 1, 2]],
|
||||
@@ -597,7 +661,16 @@ fn a_face_whose_poles_cannot_be_told_is_refused_with_the_classifiers_reason() {
|
||||
speed: [1.0, 1.0],
|
||||
unevaluated: Vec::new(),
|
||||
};
|
||||
match plan_or_fill(3, "spline", &s, &loops, &rules, &size, 0.0, None) {
|
||||
match plan_or_fill(
|
||||
3,
|
||||
"spline",
|
||||
&FaceSurface::new(s.clone()),
|
||||
&loops,
|
||||
&rules,
|
||||
&size,
|
||||
0.0,
|
||||
None,
|
||||
) {
|
||||
Err(e) => {
|
||||
let why = e.to_string();
|
||||
assert!(
|
||||
|
||||
+22
-8
@@ -14,7 +14,7 @@ use crate::vec::VecExt as _;
|
||||
use solidcore::eval::EvalSurface;
|
||||
use solidcore_record::Vec3;
|
||||
|
||||
use super::{NoValue, Unclassified};
|
||||
use super::{FaceSurface, NoValue, Unclassified};
|
||||
|
||||
/// Split the triangles `tris` of a face (indices into `uv`; the first `fixed.len()` points sit
|
||||
/// at `fixed`, the rest on the surface at their parameters) until none of them lies farther
|
||||
@@ -192,7 +192,7 @@ pub(crate) fn refine(
|
||||
/// triangulation, as the kernel's classifier says (within `reach`): a property of the node's
|
||||
/// parameters, so it is asked once per node however many triangles and rounds look at it.
|
||||
pub(crate) struct Poles<'a> {
|
||||
s: &'a EvalSurface,
|
||||
s: &'a FaceSurface,
|
||||
reach: f64,
|
||||
/// Per node index; `None` until asked.
|
||||
flags: Vec<Option<[bool; 2]>>,
|
||||
@@ -202,7 +202,7 @@ pub(crate) struct Poles<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Poles<'a> {
|
||||
pub(crate) fn new(s: &'a EvalSurface, reach: f64) -> Self {
|
||||
pub(crate) fn new(s: &'a FaceSurface, reach: f64) -> Self {
|
||||
Poles {
|
||||
s,
|
||||
reach,
|
||||
@@ -648,7 +648,7 @@ mod tests {
|
||||
refine(
|
||||
&s,
|
||||
0.01,
|
||||
&mut Poles::new(&s, 1e-9),
|
||||
&mut Poles::new(&FaceSurface::new(s.clone()), 1e-9),
|
||||
&fixed,
|
||||
&mut uv,
|
||||
&mut tris,
|
||||
@@ -677,7 +677,7 @@ mod tests {
|
||||
refine(
|
||||
&s,
|
||||
0.01,
|
||||
&mut Poles::new(&s, 1e-9),
|
||||
&mut Poles::new(&FaceSurface::new(s.clone()), 1e-9),
|
||||
&fixed,
|
||||
&mut uv,
|
||||
&mut tris,
|
||||
@@ -686,7 +686,14 @@ mod tests {
|
||||
Err(Stop::NoValue(NoValue([0.5, 0.5])))
|
||||
);
|
||||
assert_eq!(
|
||||
flip(&s, 0.01, &mut Poles::new(&s, 1e-9), &fixed, &uv, &mut tris),
|
||||
flip(
|
||||
&s,
|
||||
0.01,
|
||||
&mut Poles::new(&FaceSurface::new(s.clone()), 1e-9),
|
||||
&fixed,
|
||||
&uv,
|
||||
&mut tris
|
||||
),
|
||||
Err(Stop::NoValue(NoValue([0.5, 0.5])))
|
||||
);
|
||||
}
|
||||
@@ -715,7 +722,13 @@ mod tests {
|
||||
};
|
||||
let uv = [[0.5, -0.1], [0.5, 0.1], [0.6, 0.0]];
|
||||
let side = 3.0 * (1.0 - 0.1f64.cos());
|
||||
let got = worst(&s, &mut Poles::new(&s, 0.0), &[], &uv, &[[0, 1, 2]]);
|
||||
let got = worst(
|
||||
&s,
|
||||
&mut Poles::new(&FaceSurface::new(s.clone()), 0.0),
|
||||
&[],
|
||||
&uv,
|
||||
&[[0, 1, 2]],
|
||||
);
|
||||
assert!(
|
||||
got.as_ref()
|
||||
.is_ok_and(|(w, unmeasured)| (w - side).abs() < 1e-9 * side && *unmeasured == 0),
|
||||
@@ -725,7 +738,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
worst(
|
||||
&s,
|
||||
&mut Poles::new(&s, 0.0),
|
||||
&mut Poles::new(&FaceSurface::new(s.clone()), 0.0),
|
||||
&[],
|
||||
&uv,
|
||||
&[[0, 1, 2], [0, 1, 3]]
|
||||
@@ -774,6 +787,7 @@ mod tests {
|
||||
tris.push([a, d, b]);
|
||||
}
|
||||
}
|
||||
let s = FaceSurface::new(s);
|
||||
let mut poles = Poles::new(&s, 1e-9);
|
||||
let start = uv.len();
|
||||
// Refinement is capped (it may or may not run out of room): the count holds either way.
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
//! nothing of its own: [`SurfaceEval`] makes the evaluator's form, and a label in place of its
|
||||
//! form is the record's error.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use solidcore::eval::{EvalSurface, SurfaceEval};
|
||||
use solidcore_convert as kernel;
|
||||
use solidcore_record::brep;
|
||||
@@ -33,11 +36,133 @@ pub(crate) fn eval_surface(surface: &brep::Surface) -> Result<EvalSurface> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A face's surface with the kernel's pole classifier's answers kept per iso-line.
|
||||
///
|
||||
/// Whether a surface is collapsed along `d` at `q` is decided on the iso-line through `q` along
|
||||
/// `d` alone ([`solidcore::eval::collapse`]): it depends on `q` only through the other
|
||||
/// parameter (wrapped into the surface's period, [`EvalSurface::wrap`]) and on the reach. One
|
||||
/// classification builds that iso-line's whole control row (a de Boor evaluation per column of
|
||||
/// the net), so on a spline of many poles it costs far more than a point evaluation, while the
|
||||
/// planner asks it at every node, and the nodes of a face share iso-lines: every
|
||||
/// node of an edge on a patch side, a structured grid's rows and columns, a cut grid's lines.
|
||||
/// Each answer is kept here under its iso-line and asked of the kernel once. A refusal is not
|
||||
/// kept (its message names the point), so it is asked again and comes out the same.
|
||||
pub(crate) struct FaceSurface {
|
||||
eval: EvalSurface,
|
||||
/// `(d, other parameter, reach)`, the two numbers by their bits, to the kernel's answer.
|
||||
collapsed: Mutex<HashMap<(usize, u64, u64), bool>>,
|
||||
/// Calls of the kernel's classifier made, for the tests.
|
||||
#[cfg(test)]
|
||||
asked: std::sync::atomic::AtomicUsize,
|
||||
}
|
||||
|
||||
impl FaceSurface {
|
||||
pub(crate) fn new(eval: EvalSurface) -> FaceSurface {
|
||||
FaceSurface {
|
||||
eval,
|
||||
collapsed: Mutex::new(HashMap::new()),
|
||||
#[cfg(test)]
|
||||
asked: std::sync::atomic::AtomicUsize::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// The surface, for the plan.
|
||||
pub(crate) fn into_eval(self) -> EvalSurface {
|
||||
self.eval
|
||||
}
|
||||
|
||||
/// Whether the surface is collapsed along `d` at `q` to within `reach`, as the kernel's
|
||||
/// classifier says ([`solidcore::eval::uv::degenerate_within`]), asked once per iso-line.
|
||||
pub(crate) fn collapsed(
|
||||
&self,
|
||||
q: [f64; 2],
|
||||
d: usize,
|
||||
reach: f64,
|
||||
) -> std::result::Result<bool, solidcore::eval::EvalError> {
|
||||
let ask = || {
|
||||
#[cfg(test)]
|
||||
self.asked
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
solidcore::eval::uv::degenerate_within(&self.eval, q, d, reach)
|
||||
};
|
||||
// A plane's answer is its axis alone, cheaper than the lookup; a parameter that is not
|
||||
// finite, or a direction past v, is the kernel's refusal.
|
||||
if matches!(self.eval, EvalSurface::Plane { .. })
|
||||
|| d > 1
|
||||
|| !(q[0].is_finite() && q[1].is_finite())
|
||||
{
|
||||
return ask();
|
||||
}
|
||||
let Some(&other) = self.eval.wrap(q).get(1 - d) else {
|
||||
return ask();
|
||||
};
|
||||
let key = (d, other.to_bits(), reach.to_bits());
|
||||
if let Some(&known) = self.lock().get(&key) {
|
||||
return Ok(known);
|
||||
}
|
||||
let answer = ask()?;
|
||||
self.lock().insert(key, answer);
|
||||
Ok(answer)
|
||||
}
|
||||
|
||||
fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<(usize, u64, u64), bool>> {
|
||||
// The map holds only finished answers, so one left by a panicking thread is sound.
|
||||
self.collapsed
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
/// Calls of the kernel's classifier made so far.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn asked(&self) -> usize {
|
||||
self.asked.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for FaceSurface {
|
||||
type Target = EvalSurface;
|
||||
|
||||
fn deref(&self) -> &EvalSurface {
|
||||
&self.eval
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::planner::tests::fixture;
|
||||
|
||||
/// The memo gives the kernel's own answer at every node, and asks the kernel once per
|
||||
/// iso-line: on a 9 x 9 grid of each curved face of the coil and of a sphere (whose poles
|
||||
/// collapse a whole row), 18 questions per reach, not 162.
|
||||
#[test]
|
||||
fn the_pole_classifier_is_asked_once_per_iso_line() {
|
||||
for name in ["smlib_coil_lower_half", "sphere_r4"] {
|
||||
for face in &fixture(name).faces {
|
||||
let s = FaceSurface::new(eval_surface(face.parameters()).unwrap());
|
||||
if matches!(*s, EvalSurface::Plane { .. }) {
|
||||
continue;
|
||||
}
|
||||
let dom = solidcore::eval::uv::domain(&s);
|
||||
let [(u0, u1), (v0, v1)] = dom.range.map(|r| r.unwrap_or((0.0, 1.0)));
|
||||
let at = |a: f64, b: f64, i: usize| a + (b - a) * i as f64 / 8.0;
|
||||
for reach in [0.0, 1e-3] {
|
||||
for i in 0..9 {
|
||||
for j in 0..9 {
|
||||
let q = [at(u0, u1, i), at(v0, v1, j)];
|
||||
for d in 0..2 {
|
||||
let want = solidcore::eval::uv::degenerate_within(&s, q, d, reach)
|
||||
.unwrap();
|
||||
assert_eq!(s.collapsed(q, d, reach).unwrap(), want, "{name} {q:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(s.asked(), 2 * 2 * 9, "{name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Every face of the primitives evaluates on its own parameters (a plane, or the NURBS form
|
||||
/// the record holds), and a cylinder, sphere or torus label in place of its form is refused
|
||||
/// as the record's error: tess makes no form of its own.
|
||||
|
||||
+1
-3
@@ -4,8 +4,6 @@
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use solidcore::eval::EvalSurface;
|
||||
|
||||
use super::seam;
|
||||
|
||||
/// Two chords cross only where each one's ends lie strictly either side of the other, by more
|
||||
@@ -49,7 +47,7 @@ fn shifted(q: [f64; 2], d: usize, by: f64) -> [f64; 2] {
|
||||
/// [`solidcore::eval::uv::degenerate_within`]); a node whose poles the kernel's classifier
|
||||
/// cannot tell is the error.
|
||||
pub(crate) fn crossing(
|
||||
s: &EvalSurface,
|
||||
s: &super::FaceSurface,
|
||||
reach: f64,
|
||||
loops: &[seam::Loop],
|
||||
) -> Result<Vec<(usize, usize)>, super::Unclassified> {
|
||||
|
||||
+21
-9
@@ -982,7 +982,7 @@ fn a_curved_face_crossing_itself_is_filled_on_its_surface() {
|
||||
plan_face(
|
||||
0,
|
||||
"spline",
|
||||
&s,
|
||||
&FaceSurface::new(s.clone()),
|
||||
loops,
|
||||
&rules,
|
||||
&size,
|
||||
@@ -995,7 +995,7 @@ fn a_curved_face_crossing_itself_is_filled_on_its_surface() {
|
||||
let Ok(fp) = plan_face(
|
||||
0,
|
||||
"spline",
|
||||
&s,
|
||||
&FaceSurface::new(s.clone()),
|
||||
loops,
|
||||
&rules,
|
||||
&size,
|
||||
@@ -1038,7 +1038,7 @@ fn a_face_filled_leniently_is_named_in_the_stats() {
|
||||
let Ok((fp, stats)) = plan_or_fill(
|
||||
7,
|
||||
"spline",
|
||||
&s,
|
||||
&FaceSurface::new(s.clone()),
|
||||
std::slice::from_ref(&l),
|
||||
&rules,
|
||||
&size,
|
||||
@@ -1072,7 +1072,7 @@ fn a_face_untangling_cannot_resolve_is_filled_leniently_and_named() {
|
||||
let Ok((fp, stats)) = plan_or_fill(
|
||||
7,
|
||||
"spline",
|
||||
&s,
|
||||
&FaceSurface::new(s.clone()),
|
||||
std::slice::from_ref(&l),
|
||||
&rules,
|
||||
&size,
|
||||
@@ -1348,7 +1348,7 @@ fn sag_refinement_across_an_inflection_ends_without_slivers() {
|
||||
sag::refine(
|
||||
&s,
|
||||
tol,
|
||||
&mut sag::Poles::new(&s, 1e-9),
|
||||
&mut sag::Poles::new(&FaceSurface::new(s.clone()), 1e-9),
|
||||
&fixed,
|
||||
&mut uv,
|
||||
&mut tris,
|
||||
@@ -1409,7 +1409,15 @@ fn triangles_standing_across_a_curved_sheet_are_flipped_along_it() {
|
||||
.sum()
|
||||
};
|
||||
let before = area(&tris);
|
||||
sag::flip(&s, 0.1, &mut sag::Poles::new(&s, 1e-9), &[], &uv, &mut tris).unwrap();
|
||||
sag::flip(
|
||||
&s,
|
||||
0.1,
|
||||
&mut sag::Poles::new(&FaceSurface::new(s.clone()), 1e-9),
|
||||
&[],
|
||||
&uv,
|
||||
&mut tris,
|
||||
)
|
||||
.unwrap();
|
||||
let after = area(&tris);
|
||||
let sides: Vec<(u32, u32)> = tris
|
||||
.iter()
|
||||
@@ -1868,7 +1876,7 @@ fn a_face_with_a_loop_of_one_node_is_refused_naming_the_loop() {
|
||||
assert_eq!(nodes[e as usize].points.len(), 2);
|
||||
let face = &b.faces[4];
|
||||
let s = ok(surface::eval_surface(face.parameters()));
|
||||
match boundary(&b, 4, face, &s, &nodes, &rules) {
|
||||
match boundary(&b, 4, face, &FaceSurface::new(s.clone()), &nodes, &rules) {
|
||||
Err(e) => assert!(
|
||||
e.to_string()
|
||||
.ends_with("face 4 (plane): loop 1 has 1 node (1 edge uses)"),
|
||||
@@ -1937,10 +1945,14 @@ fn a_thin_loops_edge_that_cannot_be_split_is_returned_with_why() {
|
||||
fn an_edge_untangling_cannot_split_refuses_its_faces() {
|
||||
let b = fixture("box_10x6x4");
|
||||
let rules = size::Rules::new(0.01, 12.0, None);
|
||||
let evals: Vec<Option<EvalSurface>> = b
|
||||
let evals: Vec<Option<FaceSurface>> = b
|
||||
.faces
|
||||
.iter()
|
||||
.map(|f| surface::eval_surface(f.parameters()).ok())
|
||||
.map(|f| {
|
||||
surface::eval_surface(f.parameters())
|
||||
.ok()
|
||||
.map(FaceSurface::new)
|
||||
})
|
||||
.collect();
|
||||
let mut edge_faces: Vec<Vec<usize>> = vec![Vec::new(); b.edges.len()];
|
||||
for (fi, f) in b.faces.iter().enumerate() {
|
||||
|
||||
+1
-1
@@ -135,7 +135,7 @@ pub(crate) fn limit_normal(
|
||||
/// which refuses the face there.
|
||||
pub(crate) fn plan_poles(
|
||||
fi: usize,
|
||||
s: &EvalSurface,
|
||||
s: &crate::planner::FaceSurface,
|
||||
f: &FacePlan,
|
||||
reach: f64,
|
||||
) -> (Vec<PoleNode>, Vec<AveragedPole>) {
|
||||
|
||||
Reference in New Issue
Block a user