When projecting a face's boundary loop onto its surface fails from an unhinted first point, the walk is retried once from the first point an unhinted search does find; a walk that succeeds is unchanged. A fillet strip on an extended surface now meshes, so the rim cut in the reported case builds in one boolean.
This commit is contained in:
1 parent
575c037f39
commit
e519fbd14d
5 files changed
+1597
-31
No files matched your search
@@ -283,6 +283,7 @@ mod loft_spacing;
|
|||||||
mod mirror_planes;
|
mod mirror_planes;
|
||||||
mod path_scope;
|
mod path_scope;
|
||||||
mod primitive_points;
|
mod primitive_points;
|
||||||
|
mod rim_cut;
|
||||||
mod robustness;
|
mod robustness;
|
||||||
mod robustness_goldens;
|
mod robustness_goldens;
|
||||||
mod spiral_join;
|
mod spiral_join;
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,57 @@
|
|||||||
|
//! #882: a cut through the open rim of a shelled case failed once an R1 round went on the outer
|
||||||
|
//! loop of the other end face. One strip of that round (along a sloped side edge, far from the
|
||||||
|
//! cut) was never meshed: its boundary was projected onto its surface starting from a corner
|
||||||
|
//! that a search without a hint misses, so the face was dropped. Every boolean with the body
|
||||||
|
//! then failed (truck's meshes both operands to classify faces), and the exact fallback refused
|
||||||
|
//! it for the same face. The owner's design, `battery-tpu-case.solidcraft` (format 18).
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use super::run;
|
||||||
|
use crate::Session;
|
||||||
|
|
||||||
|
fn open_at(feature: &str) -> Session {
|
||||||
|
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/tests/fixtures/battery-tpu-case.solidcraft");
|
||||||
|
let mut s = Session::default();
|
||||||
|
run(&mut s, "doc.open", json!({"path": path.to_string_lossy()}));
|
||||||
|
run(&mut s, "timeline.roll_to", json!({"feature": feature}));
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The window cut through the front wall and its rim, 48 × 4 × 50 from below the rim.
|
||||||
|
fn cut_window(s: &mut Session) -> serde_json::Value {
|
||||||
|
run(s, "solid.box", json!({"corner": [-24, -0.5, -1], "length": 48, "width": 4, "height": 50, "operation": "cut", "targets": ["Case"]}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every face of the case after the round meshes (the strip had no triangles at any tolerance).
|
||||||
|
#[test]
|
||||||
|
fn every_face_of_the_rounded_case_meshes() {
|
||||||
|
let s = open_at("Fillet2");
|
||||||
|
let case = s.model.state().body("Case").map(|b| b.body.clone()).unwrap();
|
||||||
|
let faces = case.faces(0.05).unwrap();
|
||||||
|
assert_eq!(faces.len(), case.face_count());
|
||||||
|
let unmeshed: Vec<usize> = faces.iter().filter(|f| f.area <= 0.0).map(|f| f.index).collect();
|
||||||
|
assert!(unmeshed.is_empty(), "faces with no mesh: {unmeshed:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The cut takes the same window after the round as before it (the round is 120 mm away), in one
|
||||||
|
/// boolean with no retry.
|
||||||
|
#[test]
|
||||||
|
fn rim_cut_after_the_round_on_the_other_end() {
|
||||||
|
let mut before = open_at("Fillet1");
|
||||||
|
let mut after = open_at("Fillet2");
|
||||||
|
let faces = |s: &Session| s.model.state().body("Case").map(|b| b.body.face_count()).unwrap();
|
||||||
|
let (f1, f2) = (faces(&before), faces(&after));
|
||||||
|
cut_window(&mut before);
|
||||||
|
let id = cut_window(&mut after)["feature"].as_u64().unwrap();
|
||||||
|
let result = after.model.result(id).unwrap();
|
||||||
|
assert!(result.error.is_none() && result.warning.is_none(), "{result:?}");
|
||||||
|
assert!(result.boolean_diagnostics.events.is_empty(), "{:?}", result.boolean_diagnostics);
|
||||||
|
// The window's three faces (two sides and the top), at both points of the timeline.
|
||||||
|
assert_eq!(faces(&before), f1 + 3);
|
||||||
|
assert_eq!(faces(&after), f2 + 3);
|
||||||
|
let case = after.model.state().body("Case").map(|b| b.body.clone()).unwrap();
|
||||||
|
assert_eq!(case.shell_count(), 1);
|
||||||
|
}
|
||||||
Vendored
+7
@@ -59,6 +59,13 @@ Changes:
|
|||||||
This prevents huge arbitrary pole parameters from corrupting trimmed meshes (#796),
|
This prevents huge arbitrary pole parameters from corrupting trimmed meshes (#796),
|
||||||
while retaining genuine extrapolation for unbounded planes. Work is bounded to four
|
while retaining genuine extrapolation for unbounded planes. Work is bounded to four
|
||||||
endpoint evaluations per projected point; no search or tessellation tolerance is relaxed.
|
endpoint evaluations per projected point; no search or tessellation tolerance is relaxed.
|
||||||
|
- `tessellation/triangulation.rs`: a boundary loop is projected onto its surface point by point,
|
||||||
|
each searched from the one before; the first has no hint. When that walk fails, it is tried
|
||||||
|
once more from the first point an unhinted search finds. An unhinted search can miss a point
|
||||||
|
that lies on the surface, such as the corner of a fillet strip on a surface extended past it,
|
||||||
|
and the face was then left unmeshed, which also failed every boolean with the body (#882). A
|
||||||
|
walk that succeeds is kept as it was: where a loop starts decides how it is laid out across a
|
||||||
|
periodic surface's seam.
|
||||||
- `tessellation/triangulation.rs`: the inside test of a face's boundary skips loops whose
|
- `tessellation/triangulation.rs`: the inside test of a face's boundary skips loops whose
|
||||||
parameter box is clear of the point (it tested every segment of every loop, so a face with
|
parameter box is clear of the point (it tested every segment of every loop, so a face with
|
||||||
hundreds of holes meshed in quadratic time).
|
hundreds of holes meshed in quadratic time).
|
||||||
|
|||||||
+48
-31
@@ -411,43 +411,60 @@ impl PolyBoundaryPiece {
|
|||||||
) -> Option<Self> {
|
) -> Option<Self> {
|
||||||
let (up, vp) = (surface.u_period(), surface.v_period());
|
let (up, vp) = (surface.u_period(), surface.v_period());
|
||||||
let (urange, vrange) = surface.try_range_tuple();
|
let (urange, vrange) = surface.try_range_tuple();
|
||||||
let mut bdry3d: Vec<Point3> = wire
|
let bdry3d: Vec<Point3> = wire
|
||||||
.flat_map(|poly_edge| {
|
.flat_map(|poly_edge| {
|
||||||
let n = poly_edge.len() - 1;
|
let n = poly_edge.len() - 1;
|
||||||
poly_edge.into_iter().take(n)
|
poly_edge.into_iter().take(n)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
bdry3d.push(bdry3d[0]);
|
let walk = |mut bdry3d: Vec<Point3>| -> Option<Vec<Point2>> {
|
||||||
let mut previous = None;
|
bdry3d.push(bdry3d[0]);
|
||||||
let mut vec = bdry3d
|
let mut previous = None;
|
||||||
.into_iter()
|
bdry3d
|
||||||
.flat_map(|pt| {
|
.into_iter()
|
||||||
let (mut u, mut v) = match sp(surface, pt, previous) {
|
.flat_map(|pt| {
|
||||||
// SolidCraft: validate exact and nearest results alike, once per
|
let (mut u, mut v) = match sp(surface, pt, previous) {
|
||||||
// boundary point, including the bounded provisional preview.
|
// SolidCraft: validate exact and nearest results alike, once per
|
||||||
Some(hint) => sane(surface, pt, hint),
|
// boundary point, including the bounded provisional preview.
|
||||||
None => return vec![None],
|
Some(hint) => sane(surface, pt, hint),
|
||||||
};
|
None => return vec![None],
|
||||||
if let (Some(up), Some((u0, _))) = (up, previous) {
|
};
|
||||||
u = get_mindiff(u, u0, up);
|
if let (Some(up), Some((u0, _))) = (up, previous) {
|
||||||
}
|
u = get_mindiff(u, u0, up);
|
||||||
if let (Some(vp), Some((_, v0))) = (vp, previous) {
|
|
||||||
v = get_mindiff(v, v0, vp);
|
|
||||||
}
|
|
||||||
let res = (|| {
|
|
||||||
if let Some((u0, v0)) = previous {
|
|
||||||
if !u0.near(&u) && surface.uder(u0, v0).so_small() {
|
|
||||||
return vec![Some(Point2::new(u, v0)), Some(Point2::new(u, v))];
|
|
||||||
} else if !v0.near(&v) && surface.vder(u0, v0).so_small() {
|
|
||||||
return vec![Some(Point2::new(u0, v)), Some(Point2::new(u, v))];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
vec![Some(Point2::new(u, v))]
|
if let (Some(vp), Some((_, v0))) = (vp, previous) {
|
||||||
})();
|
v = get_mindiff(v, v0, vp);
|
||||||
previous = Some((u, v));
|
}
|
||||||
res
|
let res = (|| {
|
||||||
})
|
if let Some((u0, v0)) = previous {
|
||||||
.collect::<Option<Vec<Point2>>>()?;
|
if !u0.near(&u) && surface.uder(u0, v0).so_small() {
|
||||||
|
return vec![Some(Point2::new(u, v0)), Some(Point2::new(u, v))];
|
||||||
|
} else if !v0.near(&v) && surface.vder(u0, v0).so_small() {
|
||||||
|
return vec![Some(Point2::new(u0, v)), Some(Point2::new(u, v))];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vec![Some(Point2::new(u, v))]
|
||||||
|
})();
|
||||||
|
previous = Some((u, v));
|
||||||
|
res
|
||||||
|
})
|
||||||
|
.collect::<Option<Vec<Point2>>>()
|
||||||
|
};
|
||||||
|
// SolidCraft: each point is searched from the one before it, but the first has no
|
||||||
|
// hint, and a search without one can miss a point that lies on the surface (a corner of
|
||||||
|
// a fillet strip on a surface extended past it, #882): the whole face was then left
|
||||||
|
// unmeshed. When the walk fails, it is tried once more from the first point an unhinted
|
||||||
|
// search finds. A walk that succeeds is kept as it is (where a loop starts decides how
|
||||||
|
// it is laid out across a periodic surface's seam).
|
||||||
|
let mut vec = match walk(bdry3d.clone()) {
|
||||||
|
Some(vec) => vec,
|
||||||
|
None => {
|
||||||
|
let start = bdry3d.iter().skip(1).position(|pt| sp(surface, *pt, None).is_some())? + 1;
|
||||||
|
let mut rotated = bdry3d;
|
||||||
|
rotated.rotate_left(start);
|
||||||
|
walk(rotated)?
|
||||||
|
}
|
||||||
|
};
|
||||||
let grav = vec.iter().fold(Point2::origin(), |g, p| g + p.to_vec()) / vec.len() as f64;
|
let grav = vec.iter().fold(Point2::origin(), |g, p| g + p.to_vec()) / vec.len() as f64;
|
||||||
if let (Some(up), Some((u0, _))) = (up, urange) {
|
if let (Some(up), Some((u0, _))) = (up, urange) {
|
||||||
let quot = f64::floor((grav.x - u0) / up);
|
let quot = f64::floor((grav.x - u0) / up);
|
||||||
|
|||||||
Reference in new issue
Block a user