mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 04:52:10 +00:00
offset: a face's surface is extended by its offset's reach past the face, not five offsets (#2769)
The offset executive extended every surface by |offset| * 5 before intersecting the offsets. The margin is now derived from the geometry: how far past the face the offsets of its neighbours meet its own (offset::offset_executive::reach::extension_reach, a kernel function with its tests). Across an edge whose faces are offset by dF and dG, the offsets' tangent planes meet on a line whose foot on the face's tangent plane lies r = beta (nG.m) past the edge, beta = (dG - c dF) / (1 - c^2) (d / (1 + c) for equal offsets: d cot(alpha / 2), alpha the solid's sector angle); at a corner the foot of the offset corner is |x| from the vertex, x.m1 = r1, x.m2 = r2. Each foot lies within |r| (|x|) of the face, so of its surface's box, also where an inward meeting leaves the face across another edge (a hole's offset growing past a side). A face's reach is the largest over its edges (sampled along each) and corners; a shell's open faces stay where they are (offset 0); edges whose offsets never meet (a knife edge, tangent faces offset apart) add nothing; a face with no neighbour (a sheet's lamina face) is not extended. The executive records each original face's reach as it offsets it and extends by it and one offset, so that each meeting crosses the extension rather than touching its boundary (a shell's wall meets its open face at the wall's edge, a tangent neighbour's offset at the edge's image; the shells of issue_403, 854, 855 and 380 failed on such touching meetings, and a tolerance-sized margin is below what an extension can certify). An offset face takes its face use's margin; a cap made from none, the body's largest. The step-gap segment's surface reaches the segment's far end. Normals within the tangency tolerance of tangent (or of folded back) make no meeting and no corner: their tangent lines' crossing, thousands of offsets off, is not where curved faces' offsets meet. A local operation's faces move to surfaces of their own, not by an offset: its margin stays upstream's 2.5 radii of the sphere bounding the body (set as the executive's bound; its 165 difftest cases unchanged). difftest: offset.offset_executive_p2 moves three more cases (two rounding, one geometry, pe_Fillet_cyl_on_box_w_fillet2_f1_d0_out_ext, unsound on main and the branch alike) and pe_Fillet_cyl_on_box_w_fillet1_f0_d0_out_ext returns to the C++ value; offset_executive_p1, offset_geometry_creation, assert_valid, brep_local_operation, shell and face_p1 are unchanged. Full suite on the stack: 5235 passed, 2 failed (solidcore-ops session flakes, #2776; 9/9 serially), 47 ignored. wr_578 seam 200 now shells sound (manifold, integrity clean, 1.5e-7 off the fork's closed form as seams 30 and 55 are), so it is asserted sound; seam 125 is still refused. The offset_executive_p1 and offset_geometry_creation difftest units extend face-less surfaces: they pass the margin upstream's call asks for (offset * 5, either way). A difftest test measures the p1 extension cases on their own: the base over its box, each side straight within its points' rounding (the middle row's reach against the margin reported). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NvoxKMbMFSQU8tzRtoJhtT
This commit is contained in:
@@ -177,11 +177,6 @@ unit = "offset.offset_executive_p2"
|
||||
case = "pe_Fillet_cyl_on_box_w_fillet1_f0_d0_in_ext"
|
||||
why = "the exact surface extension replaces the fitted one (rounding; plan/findings/surface.exact-extension--difftest-cases-moved-2026-11-05.md names the case and its value)"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "offset.offset_executive_p2"
|
||||
case = "pe_Fillet_cyl_on_box_w_fillet1_f0_d0_out_ext"
|
||||
why = "the exact surface extension replaces the fitted one (rounding; plan/findings/surface.exact-extension--difftest-cases-moved-2026-11-05.md names the case and its value)"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "offset.offset_executive_p2"
|
||||
case = "pe_Fillet_cyl_on_box_w_fillet2_f0_d0_in_ext"
|
||||
@@ -241,3 +236,18 @@ why = "the exact surface extension replaces the fitted one (geometry; plan/findi
|
||||
unit = "offset.offset_executive_p2"
|
||||
case = "shell_file_problemShell2_face1_extend"
|
||||
why = "the exact surface extension replaces the fitted one (geometry; plan/findings/surface.exact-extension--difftest-cases-moved-2026-11-05.md names the case and its value)"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "offset.offset_executive_p2"
|
||||
case = "pe_Fillet_cyl_on_box_f2_d0_in_ext"
|
||||
why = "the offset's extended surfaces reach as far as the offsets meet, not five offsets (rounding; plan/findings/surface.exact-extension--difftest-cases-moved-2026-11-05.md names the case and its value)"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "offset.offset_executive_p2"
|
||||
case = "pe_Fillet_cyl_on_box_w_fillet1_f2_d0_in_ext"
|
||||
why = "the offset's extended surfaces reach as far as the offsets meet, not five offsets (rounding; plan/findings/surface.exact-extension--difftest-cases-moved-2026-11-05.md names the case and its value)"
|
||||
|
||||
[[mismatch]]
|
||||
unit = "offset.offset_executive_p2"
|
||||
case = "pe_Fillet_cyl_on_box_w_fillet2_f1_d0_out_ext"
|
||||
why = "the offset's extended surfaces reach as far as the offsets meet, not five offsets (geometry; plan/findings/surface.exact-extension--difftest-cases-moved-2026-11-05.md names the case and its value)"
|
||||
|
||||
@@ -1699,6 +1699,11 @@ fn rust_extend(input: &Extension) -> RResult<Output> {
|
||||
input.offset_distance,
|
||||
input.approx_tol,
|
||||
);
|
||||
// The executive extends a face's surface by its offset's reach past the face (from the
|
||||
// original faces it records as it offsets them); these bases have no face, so each is
|
||||
// extended by the margin upstream's call asks for (`m_rGeometryCreation
|
||||
// .GetOffsetDistance() * 5.0`, either way).
|
||||
let margin = (input.offset_distance * 5.0).abs();
|
||||
let mut identities = BTreeMap::new();
|
||||
let mut next_id = 0;
|
||||
let mut output = Output::new().int("action_count", input.actions.len() as i64);
|
||||
@@ -1706,7 +1711,7 @@ fn rust_extend(input: &Extension) -> RResult<Output> {
|
||||
let base = at(&bases, base_key)?;
|
||||
let hit = identities.contains_key(&base_key);
|
||||
let (status, surface) =
|
||||
operation(executive.get_create_extended_surface(&kernel, base_key as u64, base))?;
|
||||
operation(executive.get_create_extended_surface(&kernel, base_key as u64, base, margin))?;
|
||||
let result_id = if surface.is_some() {
|
||||
*identities.entry(base_key).or_insert_with(|| {
|
||||
let id = next_id;
|
||||
@@ -2958,6 +2963,132 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{compare, CaseSource};
|
||||
|
||||
/// The extension cases measured independently of the C++ (#2769's moved cases): over the
|
||||
/// base's box the extension is the base (sampled on a 9 x 9 grid), and past each side
|
||||
/// every row runs on straight, along the base's end direction (the reach of the base's
|
||||
/// middle row against the margin asked, `|offset| * 5`, is reported: the extension
|
||||
/// measures it at the middle of the box as extended so far); a zero margin leaves the base as it is. The C++ reference fitted its
|
||||
/// extension to an estimated parameter reach, so its domains and samples past the box
|
||||
/// differ; these are the values the exact construction must give (each measured within
|
||||
/// the rounding of its points at the surface's size). Writes one line per case to
|
||||
/// `$SC_EXTEND_PROBE` when set.
|
||||
#[test]
|
||||
fn extensions_are_the_base_and_reach_the_margin_straight() {
|
||||
use solidcore::geom::vector2d::Point2;
|
||||
use std::fmt::Write as _;
|
||||
let context = CaseContext {
|
||||
root: crate::case::repo_root(),
|
||||
seeds: 0,
|
||||
};
|
||||
let kernel = Kernel::default();
|
||||
let mut report = String::new();
|
||||
let mut measured = 0;
|
||||
let mut failures = 0;
|
||||
for case in &Runner.cases(&context).unwrap() {
|
||||
let Ok(Inputs::Extend(input)) = case.parse() else {
|
||||
continue;
|
||||
};
|
||||
let margin = (input.offset_distance * 5.0).abs();
|
||||
let mut worst_on_box = 0.0_f64;
|
||||
let mut worst_reach = 0.0_f64;
|
||||
let mut worst_straight = 0.0_f64;
|
||||
let mut worst_direction = 0.0_f64;
|
||||
let mut refused = false;
|
||||
for base in &input.surfaces {
|
||||
let Ok(base) = base.rust() else { continue };
|
||||
let mut executive =
|
||||
OffsetExecutive::new(OffsetOperation::SolidOffset, input.offset_distance, input.approx_tol);
|
||||
let Ok(ext) = executive.get_create_extended_surface(&kernel, 0, &base, margin) else {
|
||||
refused = true;
|
||||
continue;
|
||||
};
|
||||
let ext = ext.clone();
|
||||
let Ok(b) = base.natural_uv_domain_with_nurb(&kernel) else { continue };
|
||||
let e = ext.natural_uv_domain_with_nurb(&kernel).unwrap();
|
||||
let scale = {
|
||||
let p = base.evaluate_point(&kernel, b.min()).unwrap();
|
||||
let q = base.evaluate_point(&kernel, b.max()).unwrap();
|
||||
p.distance(q).max(p.length()).max(q.length()).max(1e-300)
|
||||
};
|
||||
let at = |s: &Surface, u: f64, v: f64| s.evaluate_point(&kernel, Point2::new(u, v)).unwrap();
|
||||
let lerp = |a: f64, c: f64, t: f64| (1.0 - t) * a + t * c;
|
||||
for i in 0..=8 {
|
||||
for j in 0..=8 {
|
||||
let (u, v) = (lerp(b.min().x, b.max().x, f64::from(i) / 8.0), lerp(b.min().y, b.max().y, f64::from(j) / 8.0));
|
||||
// within the rounding at the extension's size (its net reaches the margin past)
|
||||
worst_on_box = worst_on_box.max(at(&base, u, v).distance(at(&ext, u, v)) / (scale + margin));
|
||||
}
|
||||
}
|
||||
// a margin within a million roundings of the surface's size is no length
|
||||
// to measure a direction on: the base over its box only
|
||||
if margin < 1e6 * f64::EPSILON * scale {
|
||||
continue;
|
||||
}
|
||||
if margin == 0.0 {
|
||||
assert_eq!(e, b, "{}: a zero margin extends nothing", case.name);
|
||||
continue;
|
||||
}
|
||||
// (side, base boundary, extended boundary) in u then v
|
||||
let sides = [
|
||||
(true, b.min().x, e.min().x),
|
||||
(true, b.max().x, e.max().x),
|
||||
(false, b.min().y, e.min().y),
|
||||
(false, b.max().y, e.max().y),
|
||||
];
|
||||
for (along_u, edge, far) in sides {
|
||||
#[allow(clippy::float_cmp)]
|
||||
if edge == far {
|
||||
continue; // a side left as it is (closed or degenerate)
|
||||
}
|
||||
for k in 0..=8 {
|
||||
let t = f64::from(k) / 8.0;
|
||||
let point = |w: f64| {
|
||||
if along_u {
|
||||
at(&ext, w, lerp(b.min().y, b.max().y, t))
|
||||
} else {
|
||||
at(&ext, lerp(b.min().x, b.max().x, t), w)
|
||||
}
|
||||
};
|
||||
let (p, q, mid) = (point(edge), point(far), point(0.5 * (edge + far)));
|
||||
let width = if along_u { b.max().x - b.min().x } else { b.max().y - b.min().y };
|
||||
let inward = if far > edge { -1.0 } else { 1.0 };
|
||||
let inner = point(edge + inward * 1e-6 * width);
|
||||
let reach = p.distance(q);
|
||||
if reach == 0.0 {
|
||||
continue; // a pole: the row is a point
|
||||
}
|
||||
// against the margin at the side's middle (where the extension
|
||||
// measures it: the rows past it reach as their end tangents go),
|
||||
// within the points' rounding at the surface's size
|
||||
if k != 4 {
|
||||
continue;
|
||||
}
|
||||
worst_reach = worst_reach.max((reach - margin).abs() / (1e-9 * margin + 1e3 * f64::EPSILON * scale));
|
||||
let chord = q - p;
|
||||
// off the chord by more than its points' rounding at the surface's size
|
||||
worst_straight = worst_straight.max((mid - p).cross(chord).length() / chord.length() / (1e-9 * chord.length() + 1e3 * f64::EPSILON * scale));
|
||||
let back = p - inner;
|
||||
if back.length() > 0.0 {
|
||||
worst_direction = worst_direction.max(back.cross(chord).length() / (back.length() * chord.length()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
measured += 1;
|
||||
let _ = writeln!(
|
||||
report,
|
||||
"{}\tmargin={margin:e}\trefused={refused}\ton_box={worst_on_box:.1e}\treach={worst_reach:.1e}\tstraight={worst_straight:.1e}\tdirection={worst_direction:.1e}",
|
||||
case.name
|
||||
);
|
||||
failures += usize::from(worst_on_box > 1e-12 || worst_straight > 1.0);
|
||||
}
|
||||
if let Ok(path) = std::env::var("SC_EXTEND_PROBE") {
|
||||
std::fs::write(path, &report).unwrap();
|
||||
}
|
||||
assert!(measured > 0);
|
||||
assert_eq!(failures, 0, "{report}");
|
||||
}
|
||||
|
||||
/// Issue #1476: every `replace` fixture (extrusion, plane and B-spline bases)
|
||||
/// replaces its offset surface by a B-spline approximation on both sides, and the
|
||||
/// sides agree; an extrusion base was refused with `SM_ERR` before.
|
||||
|
||||
@@ -1646,7 +1646,14 @@ impl OffsetExecutive<H> for Exec {
|
||||
if seeded {
|
||||
self.inner.insert_extended_surface(key, surface.clone());
|
||||
}
|
||||
let ext = self.inner.get_create_extended_surface(k, key, &surface)?;
|
||||
// The executive extends a face's surface by its offset's reach past the face, from
|
||||
// the original faces it records as it offsets them; this unit's script offsets no
|
||||
// face through it, so an unseeded base is extended by the margin upstream's call
|
||||
// asks for (`GetOffsetDistance() * 5.0`, either way).
|
||||
let margin = (self.inner.offset_distance() * 5.0).abs();
|
||||
let ext = self
|
||||
.inner
|
||||
.get_create_extended_surface(k, key, &surface, margin)?;
|
||||
let domain = ext.natural_uv_domain_with_nurb(k)?;
|
||||
if seeded {
|
||||
// the seeded entry is the base itself, as `m_vSurfExtSurf.Insert(s, s)`
|
||||
|
||||
@@ -17,6 +17,7 @@ mod face;
|
||||
mod merge;
|
||||
pub mod p1;
|
||||
pub mod p2;
|
||||
pub mod reach;
|
||||
mod step_gaps;
|
||||
mod vertex;
|
||||
|
||||
|
||||
@@ -1265,9 +1265,13 @@ impl CreatorExecutive<Handle> for Executive<'_> {
|
||||
Arena::Offset => self.offset_key_of,
|
||||
};
|
||||
let key = key_of(EntityRef::Face(f)).key.0;
|
||||
let margin = match arena {
|
||||
Arena::Original => self.inner.original_extension_margin(f),
|
||||
Arena::Offset => self.inner.offset_extension_margin(f),
|
||||
};
|
||||
let extension = self
|
||||
.inner
|
||||
.get_create_extended_surface(k, key, &surface)?
|
||||
.get_create_extended_surface(k, key, &surface, margin)?
|
||||
.clone();
|
||||
let domain = extension.natural_uv_domain_with_nurb(k)?;
|
||||
if let Some(&handle) = self.made.get(&key) {
|
||||
|
||||
@@ -213,6 +213,14 @@ pub struct OffsetExecutive {
|
||||
pub(super) vertex_to_faces: Relation<VertexId, FaceId>,
|
||||
pub(super) blend_to_faces: Relation<FaceId, FaceId>,
|
||||
extended_surfaces: BTreeMap<u64, Surface>,
|
||||
/// Not upstream: how far each original face's surface must reach past the face for
|
||||
/// its neighbours' offsets to meet its own ([`super::reach::extension_reach`]), by the
|
||||
/// face and by each of its face uses, recorded as the face is offset.
|
||||
extension_reaches: BTreeMap<FaceId, f64>,
|
||||
faceuse_extension_reaches: BTreeMap<FaceuseId, f64>,
|
||||
/// Not upstream: the margin of every face of an operation whose faces are not offset
|
||||
/// but moved to surfaces of their own (a local operation), set by its caller.
|
||||
extension_bound: Option<f64>,
|
||||
/// Not upstream (WR-346): the original faces of an inset whose offset collapses (a
|
||||
/// convex round thinner than the wall), mated across instead of offset
|
||||
/// (`m_sCollapsedFaces`; only with `Kernel::fixes.offset_collapsed_rounds_mated`).
|
||||
@@ -272,6 +280,9 @@ impl OffsetExecutive {
|
||||
vertex_to_faces: Relation::default(),
|
||||
blend_to_faces: Relation::default(),
|
||||
extended_surfaces: BTreeMap::new(),
|
||||
extension_reaches: BTreeMap::new(),
|
||||
faceuse_extension_reaches: BTreeMap::new(),
|
||||
extension_bound: None,
|
||||
collapsed_faces: Vec::new(),
|
||||
unmated_edgeuses: BTreeSet::new(),
|
||||
local_faces: Vec::new(),
|
||||
@@ -533,6 +544,97 @@ impl OffsetExecutive {
|
||||
self.edge_to_faces.remove_all();
|
||||
self.vertex_to_faces.remove_all();
|
||||
self.blend_to_faces.remove_all();
|
||||
self.extension_reaches.clear();
|
||||
self.faceuse_extension_reaches.clear();
|
||||
}
|
||||
|
||||
/// Records how far the surface of original face `face` must be extended for its
|
||||
/// neighbours' offsets to meet its offset along `faceuse`
|
||||
/// ([`super::reach::extension_reach`] at the executive's offset distance, the shell's
|
||||
/// faces staying where they are), and one offset distance, so that the meetings cross
|
||||
/// the extended surface's boundary rather than touch it.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Those of [`super::reach::extension_reach`].
|
||||
pub(super) fn record_extension_reach(
|
||||
&mut self,
|
||||
kernel: &Kernel,
|
||||
original: &mut Brep,
|
||||
face: FaceId,
|
||||
faceuse: FaceuseId,
|
||||
) -> Result<()> {
|
||||
// The meetings must cross the extended surface's boundary, not touch it (a face
|
||||
// that stays where it is meets its neighbours' offsets at its edge, a tangent
|
||||
// neighbour's offset meets at the edge's image): the margin is the reach and the
|
||||
// offset's own size, so each meeting lies at least one offset inside the extension
|
||||
// (a tolerance-sized margin is below what an extension can certify). A face with
|
||||
// no neighbour meets no offset and is not extended. A local operation's faces move to surfaces of their
|
||||
// own: its margin is the bound it sets ([`Self::set_extension_bound`]).
|
||||
let reach = if let Some(bound) = self.extension_bound {
|
||||
bound
|
||||
} else {
|
||||
super::reach::extension_reach(
|
||||
kernel,
|
||||
original,
|
||||
faceuse,
|
||||
self.offset_distance,
|
||||
&self.shell_faces,
|
||||
self.tangency_tol_radians,
|
||||
)?
|
||||
.map_or(0.0, |reach| reach + self.offset_distance.abs())
|
||||
};
|
||||
self.extension_reaches.insert(face, reach);
|
||||
let (primary, mate) = crate::topology::face::p2::faceuses(original, face)?;
|
||||
for faceuse in [Some(faceuse), primary, mate].into_iter().flatten() {
|
||||
self.faceuse_extension_reaches.insert(faceuse, reach);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the margin every face's surface is extended by, in place of its offset's reach
|
||||
/// (a local operation, whose faces move to surfaces of their own); `None` derives it.
|
||||
pub fn set_extension_bound(&mut self, bound: Option<f64>) {
|
||||
self.extension_bound = bound;
|
||||
}
|
||||
|
||||
/// The largest reach recorded: the margin of a surface no recorded face accounts for
|
||||
/// (an edge's or vertex's cap, a face the offset did not make from one).
|
||||
fn body_extension_reach(&self) -> f64 {
|
||||
if let Some(bound) = self.extension_bound {
|
||||
return bound;
|
||||
}
|
||||
self.extension_reaches.values().fold(0.0, |a, &b| a.max(b))
|
||||
}
|
||||
|
||||
/// The margin the surface of original face `face` is extended by: its recorded reach.
|
||||
pub(super) fn original_extension_margin(&self, face: FaceId) -> f64 {
|
||||
if let Some(bound) = self.extension_bound {
|
||||
return bound;
|
||||
}
|
||||
self.extension_reaches
|
||||
.get(&face)
|
||||
.copied()
|
||||
.unwrap_or_else(|| self.body_extension_reach())
|
||||
}
|
||||
|
||||
/// The margin the surface of offset face `face` is extended by: the reach of the
|
||||
/// original face use it was made from.
|
||||
pub(super) fn offset_extension_margin(&self, face: FaceId) -> f64 {
|
||||
if let Some(bound) = self.extension_bound {
|
||||
return bound;
|
||||
}
|
||||
let reaches: Vec<f64> = self
|
||||
.faceuse_to_faces
|
||||
.firsts(face)
|
||||
.iter()
|
||||
.filter_map(|faceuse| self.faceuse_extension_reaches.get(faceuse).copied())
|
||||
.collect();
|
||||
if reaches.is_empty() {
|
||||
self.body_extension_reach()
|
||||
} else {
|
||||
reaches.into_iter().fold(0.0, f64::max)
|
||||
}
|
||||
}
|
||||
|
||||
/// Synchronizes mapped edge ranges, splitting offset edges at the other side's
|
||||
@@ -621,44 +723,56 @@ impl OffsetExecutive {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Returns a cached extension, creating it with distance `|offset_distance| * 5.0` (none
|
||||
/// for a zero offset), every
|
||||
/// side exactly ruled ([`Surface::extended_sides_keeping_parameters`], [`Continuity::Ruled`]): a plane, cone,
|
||||
/// sphere, torus or ball patch as itself, a procedural blend by its definition, an
|
||||
/// Returns a cached extension, creating it `margin` past every side of `base`'s box
|
||||
/// (none for a margin of zero: the base itself), every side exactly ruled
|
||||
/// ([`Surface::extended_sides_keeping_parameters`], [`Continuity::Ruled`]): a plane,
|
||||
/// cone, sphere, torus or ball patch as itself, a procedural blend by its definition, an
|
||||
/// offset surface as the offset of its base extended. A hit never re-extends or replaces
|
||||
/// the cached surface.
|
||||
///
|
||||
/// Not upstream: upstream's `CreateExtendedSurface(.., SM_CT_G1R, ..)` fits a ruled
|
||||
/// extension and removes knots within a tolerance (the WR-532 / WR-728 opt-ins
|
||||
/// `offset_extends_cones_as_themselves` and `offset_extends_ball_patches_as_themselves`
|
||||
/// extended cones and ball patches as themselves; the exact path always does, so the
|
||||
/// flags no longer change this).
|
||||
/// The margin is how far the face's offset must reach past the face for its
|
||||
/// neighbours' offsets to meet it ([`Self::original_extension_margin`],
|
||||
/// [`Self::offset_extension_margin`]: [`super::reach::extension_reach`]).
|
||||
///
|
||||
/// Not upstream: upstream's `CreateExtendedSurface(.., SM_CT_G1R, ..)` extends by five
|
||||
/// offset distances, fits a ruled extension and removes knots within a tolerance (the
|
||||
/// WR-532 / WR-728 opt-ins `offset_extends_cones_as_themselves` and
|
||||
/// `offset_extends_ball_patches_as_themselves` extended cones and ball patches as
|
||||
/// themselves; the exact path always does, so the flags no longer change this).
|
||||
///
|
||||
/// `base_key` represents upstream pointer identity, including across Rust clones.
|
||||
/// Distinct source identities must use distinct keys. Failed extensions are not cached.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::InvalidInput`] for a margin that is negative or not finite; those of
|
||||
/// the extension.
|
||||
pub fn get_create_extended_surface(
|
||||
&mut self,
|
||||
kernel: &Kernel,
|
||||
base_key: u64,
|
||||
base: &Surface,
|
||||
margin: f64,
|
||||
) -> Result<&Surface> {
|
||||
if !(margin.is_finite() && margin >= 0.0) {
|
||||
return Err(SolidError::invalid_input(
|
||||
"offset::offset_executive::p1::get_create_extended_surface",
|
||||
"the margin is negative or not finite",
|
||||
));
|
||||
}
|
||||
match self.extended_surfaces.entry(base_key) {
|
||||
std::collections::btree_map::Entry::Occupied(entry) => Ok(entry.into_mut()),
|
||||
std::collections::btree_map::Entry::Vacant(entry) => {
|
||||
// The obsolete offset-specific `else` at cpp:2158 is unreachable:
|
||||
// NER(pBase) already established pOrigSurf == pBase != NULL.
|
||||
// The extension is a margin round the base for the offset to be trimmed in:
|
||||
// its size is five offsets either way (an inward offset's distance is
|
||||
// negative), and an offset of zero needs none.
|
||||
let dist = (self.offset_distance * 5.0).abs();
|
||||
let extension = if dist == 0.0 {
|
||||
let extension = if margin == 0.0 {
|
||||
base.clone()
|
||||
} else {
|
||||
base.extended_sides_keeping_parameters(
|
||||
kernel,
|
||||
&BoxSide::ALL,
|
||||
Continuity::Ruled,
|
||||
dist,
|
||||
margin,
|
||||
)?
|
||||
.surface
|
||||
};
|
||||
@@ -3784,7 +3898,7 @@ mod tests {
|
||||
.unwrap()
|
||||
.surface;
|
||||
let first = executive
|
||||
.get_create_extended_surface(&kernel, 41, &base)
|
||||
.get_create_extended_surface(&kernel, 41, &base, 0.625)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
first.natural_uv_domain_if_built(),
|
||||
@@ -3799,7 +3913,7 @@ mod tests {
|
||||
// A cached identity does not evaluate even a different/malformed supplied base.
|
||||
let broken = Surface::from(BSplineSurface::new());
|
||||
let cached = executive
|
||||
.get_create_extended_surface(&kernel, 41, &broken)
|
||||
.get_create_extended_surface(&kernel, 41, &broken, 0.625)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
cached
|
||||
@@ -3811,7 +3925,7 @@ mod tests {
|
||||
first_point
|
||||
);
|
||||
let other = executive
|
||||
.get_create_extended_surface(&kernel, 42, &plane(7.0))
|
||||
.get_create_extended_surface(&kernel, 42, &plane(7.0), 0.625)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
other
|
||||
@@ -3827,32 +3941,75 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_inward_offset_extends_by_five_offsets_and_a_zero_one_not_at_all() {
|
||||
fn a_surface_is_extended_by_its_margin_none_by_zero_and_a_negative_one_is_refused() {
|
||||
let kernel = Kernel::default();
|
||||
let base = plane(0.0);
|
||||
let expected = base
|
||||
.extended_sides_keeping_parameters(
|
||||
&kernel,
|
||||
&BoxSide::ALL,
|
||||
Continuity::Ruled,
|
||||
0.125 * 5.0,
|
||||
)
|
||||
.extended_sides_keeping_parameters(&kernel, &BoxSide::ALL, Continuity::Ruled, 0.3)
|
||||
.unwrap()
|
||||
.surface;
|
||||
let mut inward = OffsetExecutive::new(OffsetOperation::SolidOffset, -0.125, 1.0e-6);
|
||||
let got = inward
|
||||
.get_create_extended_surface(&kernel, 1, &base)
|
||||
let mut executive = executive();
|
||||
let got = executive
|
||||
.get_create_extended_surface(&kernel, 1, &base, 0.3)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
got.natural_uv_domain_if_built(),
|
||||
expected.natural_uv_domain_if_built()
|
||||
);
|
||||
let mut zero = OffsetExecutive::new(OffsetOperation::SolidOffset, 0.0, 1.0e-6);
|
||||
let same = zero.get_create_extended_surface(&kernel, 1, &base).unwrap();
|
||||
let same = executive
|
||||
.get_create_extended_surface(&kernel, 2, &base, 0.0)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
same.natural_uv_domain_if_built(),
|
||||
base.natural_uv_domain_if_built()
|
||||
);
|
||||
for refused in [-0.1, f64::NAN, f64::INFINITY] {
|
||||
assert!(executive
|
||||
.get_create_extended_surface(&kernel, 3, &base, refused)
|
||||
.is_err());
|
||||
}
|
||||
assert_eq!(executive.extended_surfaces.len(), 2);
|
||||
}
|
||||
|
||||
/// A box offset by `d` either way: every face's offset is met `d` past (or inside) each
|
||||
/// edge and `d·√2` from each corner, so each face's margin is `d·√2` and one offset. An offset face takes the margin of the face use it was made from; one made
|
||||
/// from none, the body's largest.
|
||||
#[test]
|
||||
fn a_box_face_margin_is_its_offset_corner_reach() {
|
||||
let mut kernel = Kernel::default();
|
||||
let mut brep = crate::primitive::api_primitives::create_box(
|
||||
&mut kernel,
|
||||
crate::math::Point3::new(0.0, 0.0, 0.0),
|
||||
1.0,
|
||||
2.0,
|
||||
3.0,
|
||||
)
|
||||
.unwrap();
|
||||
let faces = bp1::faces(&brep, None).unwrap();
|
||||
let expected = 0.125 * 2.0_f64.sqrt() + 0.125;
|
||||
for (distance, inset) in [(0.125, false), (0.125, true)] {
|
||||
let mut executive =
|
||||
OffsetExecutive::new(OffsetOperation::SolidOffset, distance, 1.0e-6);
|
||||
executive.set_inset(inset);
|
||||
for &f in &faces {
|
||||
let faceuse =
|
||||
super::super::p2::select_offset_faceuse(&brep, f, false, inset, "test")
|
||||
.unwrap();
|
||||
executive
|
||||
.record_extension_reach(&kernel, &mut brep, f, faceuse)
|
||||
.unwrap();
|
||||
assert!((executive.original_extension_margin(f) - expected).abs() < 1e-12);
|
||||
executive.relate_faceuse(faceuse, FaceId::from_index(100));
|
||||
}
|
||||
assert!(
|
||||
(executive.offset_extension_margin(FaceId::from_index(100)) - expected).abs()
|
||||
< 1e-12
|
||||
);
|
||||
assert!(
|
||||
(executive.offset_extension_margin(FaceId::from_index(101)) - expected).abs()
|
||||
< 1e-12
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3861,11 +4018,11 @@ mod tests {
|
||||
let mut executive = executive();
|
||||
let broken = Surface::from(BSplineSurface::new());
|
||||
assert!(executive
|
||||
.get_create_extended_surface(&kernel, 1, &broken)
|
||||
.get_create_extended_surface(&kernel, 1, &broken, 0.625)
|
||||
.is_err());
|
||||
assert!(executive.extended_surfaces.is_empty());
|
||||
assert!(executive
|
||||
.get_create_extended_surface(&kernel, 1, &plane(0.0))
|
||||
.get_create_extended_surface(&kernel, 1, &plane(0.0), 0.625)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
|
||||
@@ -1372,6 +1372,9 @@ impl OffsetExecutive {
|
||||
) -> Result<()> {
|
||||
let operation = if extended { OP_EXTENDED } else { OP_SOLID };
|
||||
let faceuse = select_offset_faceuse(original, id, extended, self.inset(), operation)?;
|
||||
// Not upstream: the margin this face's surface is extended by, from its edges'
|
||||
// sector angles and the offset (upstream extends by five offsets).
|
||||
self.record_extension_reach(kernel, original, id, faceuse)?;
|
||||
let (mut surface, ssi) = if self.local_faces.is_empty() {
|
||||
let creator = crate::offset::offset_geometry_creation::OffsetGeometryCreation::new(
|
||||
self.offset_distance(),
|
||||
@@ -1503,8 +1506,9 @@ impl OffsetExecutive {
|
||||
let surface = required(temporary.face(id)?.surface.as_ref(), OP_EXTENDED)?;
|
||||
let original_domain = surface.natural_uv_domain_with_nurb(kernel)?;
|
||||
// Face-owned surfaces use the caller's result-arena identities.
|
||||
let margin = self.offset_extension_margin(id);
|
||||
let extension = self
|
||||
.get_create_extended_surface(kernel, key.key.0, surface)?
|
||||
.get_create_extended_surface(kernel, key.key.0, surface, margin)?
|
||||
.clone();
|
||||
let extension_domain = extension.natural_uv_domain_with_nurb(kernel)?;
|
||||
// cpp:1639: UNSURE retains the original; FALSE suppresses UV trim
|
||||
@@ -1581,7 +1585,7 @@ impl OffsetExecutive {
|
||||
}
|
||||
}
|
||||
|
||||
fn select_offset_faceuse(
|
||||
pub(super) fn select_offset_faceuse(
|
||||
original: &Brep,
|
||||
face: FaceId,
|
||||
extended: bool,
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
//! How far past its face an offset face's surface must reach for the offsets of its
|
||||
//! neighbours to meet its own: the margin the offset executive extends each surface by before
|
||||
//! it intersects the offsets (not upstream: upstream extends every surface by five offset
|
||||
//! distances).
|
||||
//!
|
||||
//! At a point `p` of an edge between a face `F` (unit normal `nF`, binormal `bF` into `F`,
|
||||
//! offset `dF`) and its neighbour `G` (unit normal `nG`, offset `dG`), `c = nF·nG`, the
|
||||
//! offsets' tangent planes `(x − p)·nF = dF` and `(x − p)·nG = dG` meet on the line through
|
||||
//! `p + a nF + β nG`, `a + β c = dF`, `a c + β = dG`. Its foot on `F`'s tangent plane lies
|
||||
//! past the edge (along the outward binormal `m = −bF`) by the edge's signed reach
|
||||
//!
|
||||
//! ```text
|
||||
//! r = β (nG·m), β = (dG − c dF) / (1 − c²) ( = d / (1 + c) where dF = dG = d ),
|
||||
//! ```
|
||||
//!
|
||||
//! `d·cot(α / 2)` for equal offsets, `α` the sector angle of the solid between the faces:
|
||||
//! `d` across a box's edge, `0` across a tangent edge, negative where the offsets meet inside
|
||||
//! `F`. At a vertex where `F`'s edge uses meet with reaches `r1`, `r2` and outward binormals
|
||||
//! `m1`, `m2`, the offset corner's foot `x` on the tangent plane solves `x·m1 = r1`,
|
||||
//! `x·m2 = r2`, at `|x|` from the vertex.
|
||||
//!
|
||||
//! Each foot is within `|r|` (`|x|` at a corner) of a point of the face, so within that of the
|
||||
//! face's surface's box, however far the meeting runs along the face: an inward meeting that
|
||||
//! leaves the face across another of its edges (a hole's offset growing past a side) is held
|
||||
//! as well as an outward one. The reach of a face is the largest of these, sampled along each
|
||||
//! edge (its sector angle varies) and at each corner. Faces whose offsets never meet, a knife
|
||||
//! edge's (`c = −1` for equal offsets) or tangent faces offset apart (`c = 1`, `dF ≠ dG`),
|
||||
//! add nothing: no extension makes them meet.
|
||||
|
||||
use crate::error::{Result, SolidError};
|
||||
use crate::geom::vector3d::{sqrt, Vec3};
|
||||
use crate::kernel::Kernel;
|
||||
use crate::math::EFF_ZERO;
|
||||
use crate::nurbs::b_spline_surface_n_lib::p2::OrientType;
|
||||
use crate::topology::edgeuse::{p1 as eu, p3 as eu3};
|
||||
use crate::topology::{edge, loopuse, Brep, EdgeuseId, FaceId, FaceuseId};
|
||||
|
||||
const OP: &str = "offset::offset_executive::reach::extension_reach";
|
||||
|
||||
/// The number of intervals each edge is sampled at for its sector angle.
|
||||
const EDGE_SAMPLES: u32 = 8;
|
||||
|
||||
/// One end of an edge use, as the corner beside it reads it.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct End {
|
||||
/// The signed reach past the edge.
|
||||
reach: f64,
|
||||
/// The outward binormal (away from the face, perpendicular to the edge).
|
||||
outward: Vec3,
|
||||
}
|
||||
|
||||
/// The signed reach past the edge of a face offset by `own` whose neighbour is offset by
|
||||
/// `other`, from their unit normals and the face's outward binormal (module docs); zero where
|
||||
/// the offsets do not meet: faces folded back on each other (equal offsets) or tangent to
|
||||
/// each other (different offsets: their offsets run apart), each within `tangent_sine`, the
|
||||
/// sine of the tangency tolerance.
|
||||
fn signed_reach(
|
||||
n_own: Vec3,
|
||||
n_other: Vec3,
|
||||
outward: Vec3,
|
||||
own: f64,
|
||||
other: f64,
|
||||
tangent_sine: f64,
|
||||
) -> f64 {
|
||||
let c = n_own.dot(n_other);
|
||||
let sine_squared = (tangent_sine * tangent_sine).max(EFF_ZERO);
|
||||
#[allow(clippy::float_cmp)] // the same offset, not a computed value
|
||||
let beta = if own == other {
|
||||
// 1 + c = 1 - cos(θ) ≈ θ² / 2 for normals θ off opposite
|
||||
let denominator = 1.0 + c;
|
||||
if denominator <= 0.5 * sine_squared {
|
||||
return 0.0;
|
||||
}
|
||||
own / denominator
|
||||
} else {
|
||||
// 1 - c² = sin²(θ) for normals θ apart
|
||||
let denominator = 1.0 - c * c;
|
||||
if denominator <= sine_squared {
|
||||
return 0.0;
|
||||
}
|
||||
(other - c * own) / denominator
|
||||
};
|
||||
beta * n_other.dot(outward)
|
||||
}
|
||||
|
||||
/// The signed reach past `eu`'s edge at `parameter` (module docs), with `eu`'s outward
|
||||
/// binormal there, and whether the edge has a neighbour (zero reach without).
|
||||
fn edge_reach(
|
||||
k: &Kernel,
|
||||
b: &mut Brep,
|
||||
eu: EdgeuseId,
|
||||
parameter: f64,
|
||||
offset: f64,
|
||||
unmoved: &[FaceId],
|
||||
tangent_sine: f64,
|
||||
) -> Result<(End, bool)> {
|
||||
let options = eu3::BinormalOptions::default();
|
||||
let own = eu3::evaluate_binormal(k, b, eu, parameter, false, &options)?;
|
||||
let outward = -own.binormal;
|
||||
// A seam's radial is on the face itself (its offset continues across), a lamina
|
||||
// edge's is its mate on the face's other side (its offset is closed by a side wall).
|
||||
let own_face = eu::face(b, eu)?;
|
||||
let neighbour = match eu::radial(b, eu)? {
|
||||
Some(r) if r != eu && eu::face(b, r)? != own_face => Some(r),
|
||||
_ => None,
|
||||
};
|
||||
let Some(radial) = neighbour else {
|
||||
let end = End {
|
||||
reach: 0.0,
|
||||
outward,
|
||||
};
|
||||
return Ok((end, false));
|
||||
};
|
||||
let other_offset = match eu::face(b, radial)? {
|
||||
Some(f) if unmoved.contains(&f) => 0.0,
|
||||
_ => offset,
|
||||
};
|
||||
let other = eu3::evaluate_binormal(k, b, radial, parameter, false, &options)?;
|
||||
let n_own = own.face_normal.unitize()?;
|
||||
let n_other = other.face_normal.unitize()?;
|
||||
let end = End {
|
||||
reach: signed_reach(n_own, n_other, outward, offset, other_offset, tangent_sine),
|
||||
outward,
|
||||
};
|
||||
Ok((end, true))
|
||||
}
|
||||
|
||||
/// The distance from a vertex of the foot of its offset corner on the face's tangent plane
|
||||
/// (module docs), from the reaches and outward binormals of the edge uses meeting there.
|
||||
/// Where the edges run on tangent to each other (within `tangent_sine`, the sine of the
|
||||
/// tangency tolerance) the corner is no corner: the offset edges run on into each other, and
|
||||
/// the larger reach stands (the tangent lines' crossing, far off for nearly parallel lines,
|
||||
/// is not where curved edges' offsets meet).
|
||||
fn corner_reach(into: End, out: End, tangent_sine: f64) -> f64 {
|
||||
let g = into.outward.dot(out.outward);
|
||||
let det = 1.0 - g * g;
|
||||
if det <= (tangent_sine * tangent_sine).max(EFF_ZERO) {
|
||||
return into.reach.abs().max(out.reach.abs());
|
||||
}
|
||||
let a = (into.reach - g * out.reach) / det;
|
||||
let c = (out.reach - g * into.reach) / det;
|
||||
sqrt((a * a + c * c + 2.0 * a * c * g).max(0.0))
|
||||
}
|
||||
|
||||
/// How far past its face the surface of `faceuse` must be extended for the offsets of its
|
||||
/// neighbours to meet its own, the face offset by `offset` (signed, along the face use's
|
||||
/// normal) and each neighbour by the same, except the `unmoved` faces, which stay where they
|
||||
/// are (module docs): the largest distance from the face of the feet of the meetings, sampled
|
||||
/// along each edge at its sector angle and at each corner (edges meeting within
|
||||
/// `tangency_tol_radians` of tangent make no corner); `None` for a face with no
|
||||
/// neighbour (every edge a lamina edge or a seam), whose offset meets no other.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::InvalidInput`] for an offset that is not finite or a vanishing face normal;
|
||||
/// those of [`eu3::evaluate_binormal`]; [`SolidError::NotFound`] for a dangling id.
|
||||
pub fn extension_reach(
|
||||
k: &Kernel,
|
||||
b: &mut Brep,
|
||||
faceuse: FaceuseId,
|
||||
offset: f64,
|
||||
unmoved: &[FaceId],
|
||||
tangency_tol_radians: f64,
|
||||
) -> Result<Option<f64>> {
|
||||
if !offset.is_finite() {
|
||||
return Err(SolidError::invalid_input(OP, "the offset is not finite"));
|
||||
}
|
||||
let tangent_sine = tangency_tol_radians.abs().min(crate::math::PI / 2.0).sin();
|
||||
let mut reach = 0.0_f64;
|
||||
let mut met = false;
|
||||
for lu in eu::faceuse_loopuses(b, faceuse, false, None)? {
|
||||
let mut ends: Vec<(End, End)> = Vec::new();
|
||||
for edgeuse in loopuse::edgeuses(b, lu, None, None)? {
|
||||
let Some(e) = eu::edge(b, edgeuse)? else {
|
||||
continue;
|
||||
};
|
||||
let interval = edge::interval(b, e)?;
|
||||
let (lo, hi) = (interval.min(), interval.max());
|
||||
let mut first = None;
|
||||
let mut last = None;
|
||||
for i in 0..=EDGE_SAMPLES {
|
||||
let parameter = lo + (hi - lo) * f64::from(i) / f64::from(EDGE_SAMPLES);
|
||||
let (end, neighbour) =
|
||||
edge_reach(k, b, edgeuse, parameter, offset, unmoved, tangent_sine)?;
|
||||
met |= neighbour;
|
||||
reach = reach.max(end.reach.abs());
|
||||
if i == 0 {
|
||||
first = Some(end);
|
||||
}
|
||||
last = Some(end);
|
||||
}
|
||||
let (Some(first), Some(last)) = (first, last) else {
|
||||
continue;
|
||||
};
|
||||
// The edge use runs from its start to its end vertex along the loop.
|
||||
ends.push(match eu::orientation(b, edgeuse)? {
|
||||
OrientType::Opposite => (last, first),
|
||||
_ => (first, last),
|
||||
});
|
||||
}
|
||||
// Each edge use's end meets the next one's start (the loop closes).
|
||||
let next = ends.iter().cycle().skip(1);
|
||||
for (&(_, into), &(out, _)) in ends.iter().zip(next) {
|
||||
reach = reach.max(corner_reach(into, out, tangent_sine));
|
||||
}
|
||||
}
|
||||
Ok(met.then_some(reach))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::math::Point3;
|
||||
use crate::topology::brep::p1 as bp1;
|
||||
|
||||
fn box_faceuses(k: &mut Kernel, inset: bool) -> (Brep, Vec<(FaceId, FaceuseId)>) {
|
||||
let brep = crate::primitive::api_primitives::create_box(
|
||||
k,
|
||||
Point3::new(0.0, 0.0, 0.0),
|
||||
1.0,
|
||||
2.0,
|
||||
3.0,
|
||||
)
|
||||
.unwrap();
|
||||
let faceuses = bp1::faces(&brep, None)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|f| {
|
||||
let fu = super::super::p2::select_offset_faceuse(&brep, f, false, inset, OP);
|
||||
(f, fu.unwrap())
|
||||
})
|
||||
.collect();
|
||||
(brep, faceuses)
|
||||
}
|
||||
|
||||
/// Across a box's edges (sector angle a right angle) the offsets meet `d` past each
|
||||
/// edge, and at its corners `d` past both: `d·√2` from the vertex. Offset inward they meet
|
||||
/// as far inside: the same distance from the face.
|
||||
#[test]
|
||||
fn a_box_offset_either_way_reaches_its_corners_offset() {
|
||||
for inset in [false, true] {
|
||||
let mut k = Kernel::default();
|
||||
let (mut brep, faceuses) = box_faceuses(&mut k, inset);
|
||||
for (_, fu) in faceuses {
|
||||
let reach = extension_reach(&k, &mut brep, fu, 0.25, &[], 1e-3)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!((reach - 0.25 * sqrt(2.0)).abs() < 1e-12, "{reach}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A box shelled open at one face: that face stays where it is, and each wall's offset
|
||||
/// meets it at the wall's edge (a right angle, `β = 0`). A wall's corner on the open face
|
||||
/// lies `d` from its vertex, and its other corners `d·√2`.
|
||||
#[test]
|
||||
fn an_unmoved_face_is_met_where_it_stands() {
|
||||
let mut k = Kernel::default();
|
||||
let (mut brep, faceuses) = box_faceuses(&mut k, true);
|
||||
let (open, _) = faceuses[0];
|
||||
for &(_, fu) in faceuses.iter().filter(|(f, _)| *f != open) {
|
||||
let reach = extension_reach(&k, &mut brep, fu, 0.25, &[open], 1e-3)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!((reach - 0.25 * sqrt(2.0)).abs() < 1e-12, "{reach}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The signed reach: `d·cot(α/2)` for equal offsets (a right angle `d`, a tangent edge
|
||||
/// `0`), `0` for a knife edge (no meeting); an unmoved neighbour at a right angle is met at
|
||||
/// the edge, at 60° between normals `dF·c·sin / (1 − c²)` inside.
|
||||
#[test]
|
||||
fn signed_reach_is_the_foot_of_the_offsets_meeting() {
|
||||
let up = Vec3::new(0.0, 0.0, 1.0);
|
||||
let out = Vec3::X;
|
||||
assert!((signed_reach(up, Vec3::X, out, 0.5, 0.5, 1e-3) - 0.5).abs() < 1e-15);
|
||||
assert!((signed_reach(up, -Vec3::X, out, 0.5, 0.5, 1e-3) + 0.5).abs() < 1e-15);
|
||||
assert_eq!(signed_reach(up, up, out, 0.5, 0.5, 1e-3), 0.0);
|
||||
assert_eq!(signed_reach(up, -up, out, 0.5, 0.5, 1e-3), 0.0);
|
||||
assert_eq!(signed_reach(up, up, out, 0.5, 0.0, 1e-3), 0.0);
|
||||
assert!(signed_reach(up, Vec3::X, out, 0.5, 0.0, 1e-3).abs() < 1e-15);
|
||||
let half = sqrt(3.0) / 2.0;
|
||||
let tilted = Vec3::new(half, 0.0, 0.5);
|
||||
let expected = -0.5 * 0.5 * half / (1.0 - 0.25);
|
||||
assert!((signed_reach(up, tilted, out, 0.5, 0.0, 1e-3) - expected).abs() < 1e-15);
|
||||
// within the tangency tolerance of tangent, offsets 0.5 apart never meet near
|
||||
let nearly = Vec3::new(1e-4, 0.0, (1.0 - 1e-8_f64).sqrt());
|
||||
assert_eq!(signed_reach(up, nearly, out, 0.5, 0.0, 1e-3), 0.0);
|
||||
assert!(signed_reach(up, nearly, out, 0.5, 0.0, 1e-5).abs() > 1e3);
|
||||
}
|
||||
|
||||
/// The corner's foot: perpendicular edges each reached `1` and `2` past put the corner
|
||||
/// at `(1, 2)`; edges at 60° reached `d` past both put it on the bisector, `d / cos 30°`
|
||||
/// from the vertex, as reached `d` inside; parallel edges, or edges within the tangency
|
||||
/// tolerance of tangent, take the larger reach.
|
||||
#[test]
|
||||
fn corner_reach_is_the_foot_of_the_offset_corner() {
|
||||
let end = |reach: f64, outward: Vec3| End { reach, outward };
|
||||
let square = corner_reach(end(1.0, Vec3::X), end(2.0, Vec3::Y), 1e-3);
|
||||
assert!((square - sqrt(5.0)).abs() < 1e-12);
|
||||
let half = sqrt(3.0) / 2.0;
|
||||
let sixty = Vec3::new(0.5, half, 0.0);
|
||||
let acute = corner_reach(end(1.0, Vec3::X), end(1.0, sixty), 1e-3);
|
||||
assert!((acute - 1.0 / half).abs() < 1e-12, "{acute}");
|
||||
let inside = corner_reach(end(-1.0, Vec3::X), end(-1.0, sixty), 1e-3);
|
||||
assert!((inside - 1.0 / half).abs() < 1e-12, "{inside}");
|
||||
let parallel = corner_reach(end(1.0, Vec3::X), end(-3.0, Vec3::X), 1e-3);
|
||||
assert_eq!(parallel, 3.0);
|
||||
// edges 1e-4 off tangent, within a tolerance of 1e-3: no corner, not 1e4 off
|
||||
let nearly = Vec3::new((1.0 - 1e-8_f64).sqrt(), 1e-4, 0.0);
|
||||
let tangent = corner_reach(end(1.0, Vec3::X), end(2.0, nearly), 1e-3);
|
||||
assert_eq!(tangent, 2.0);
|
||||
}
|
||||
|
||||
/// A moved input is refused: an offset that is not finite.
|
||||
#[test]
|
||||
fn a_non_finite_offset_is_refused() {
|
||||
let mut k = Kernel::default();
|
||||
let (mut brep, faceuses) = box_faceuses(&mut k, false);
|
||||
assert!(extension_reach(&k, &mut brep, faceuses[0].1, f64::NAN, &[], 1e-3).is_err());
|
||||
}
|
||||
}
|
||||
@@ -394,10 +394,14 @@ impl OffsetExecutive {
|
||||
}
|
||||
|
||||
// The straight segment must lie on the face's surface (as extended for the offset:
|
||||
// the face's own surface ends at its old boundary).
|
||||
// the face's own surface ends at its old boundary). Its far end is no farther past
|
||||
// the face than its length.
|
||||
let key = (notifications.result_key_of)(EntityRef::Face(f)).key.0;
|
||||
let margin = self
|
||||
.offset_extension_margin(f)
|
||||
.max(vertex_point.distance(gap_end));
|
||||
let ext = self
|
||||
.get_create_extended_surface(kernel, key, &surface)?
|
||||
.get_create_extended_surface(kernel, key, &surface, margin)?
|
||||
.clone();
|
||||
let ext_domain = ext.natural_uv_domain_with_nurb(kernel)?;
|
||||
for ii in 1..4 {
|
||||
|
||||
@@ -439,8 +439,8 @@ pub trait OffsetExecutive<O> {
|
||||
}
|
||||
/// `GetCreateExtendedSurface(pBase, rUVDomainOfOffset, rpExtSurface)`: the extension
|
||||
/// of `base` cached by the executive (`m_vSurfExtSurf`), created on a miss by
|
||||
/// extending `base` by five offset distances
|
||||
/// (`m_rGeometryCreation.GetOffsetDistance() * 5.0`, G1-ruled) with
|
||||
/// extending `base` exactly ruled by its face's offset reach (not upstream: upstream
|
||||
/// extends by `m_rGeometryCreation.GetOffsetDistance() * 5.0`) with
|
||||
/// [`crate::offset::offset_executive::p1::OffsetExecutive::get_create_extended_surface`],
|
||||
/// and its natural UV domain. A newly made extension is stored where `brep` resolves
|
||||
/// the returned handle; a hit returns the cached handle again.
|
||||
|
||||
@@ -155,6 +155,10 @@ pub fn local_operation(
|
||||
// (the geometry creator's constructor tolerance, half a degree under WR-666's opt-in)
|
||||
executive.use_creator_tangency_tol(k);
|
||||
executive.set_local_surfaces(&all_faces, new_list);
|
||||
// A face's surface is extended to meet the moved surfaces, which the box is to hold
|
||||
// (WR-534), not to meet an offset: by upstream's five half-radii of the sphere bounding
|
||||
// it (more than its diameter, the farthest any point in it lies from a face in it)
|
||||
executive.set_extension_bound(Some(2.5 * radius));
|
||||
// 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
|
||||
|
||||
@@ -93,6 +93,21 @@ fn wr_578_box_and_cylinder_shell_closes_the_loop_gaps() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Seam 200 with the opt-in alone: each face's surface extended by its offset's reach (not
|
||||
/// five offsets), the shell is sound as at seams 30 and 55 (manifold, integrity clean), its
|
||||
/// volume 1.5e-7 off the fork's closed form as theirs are (WR-578's divergence).
|
||||
#[test]
|
||||
fn wr_578_seam_200_shells_sound() {
|
||||
let (k, result) = shell_1494(true, 200.0);
|
||||
assert!(report(
|
||||
&k,
|
||||
"wr_578 shell/1494 seam 200",
|
||||
result,
|
||||
Some(SHELL_1494),
|
||||
1e-6
|
||||
));
|
||||
}
|
||||
|
||||
/// Seams 125 and 200 degrees off the front face: the fork shells these too; the port
|
||||
/// returns a body that is not a manifold solid (WR-578).
|
||||
#[test]
|
||||
@@ -110,12 +125,12 @@ fn wr_578_box_and_cylinder_shell_other_seams() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Seams 125 and 200 with the opt-in alone: the offset is no closed solid, and the shell
|
||||
/// refuses it (`SM_ERR`) instead of handing it back as upstream's `ShellBrep` does
|
||||
/// (WR-454), so the flag never turns upstream's refusal into a broken body.
|
||||
/// Seam 125 with the opt-in alone: the offset is no closed solid, and the shell refuses it
|
||||
/// (`SM_ERR`) instead of handing it back as upstream's `ShellBrep` does (WR-454), so the
|
||||
/// flag never turns upstream's refusal into a broken body.
|
||||
#[test]
|
||||
fn wr_578_other_seams_are_refused() {
|
||||
for seam in [125.0, 200.0] {
|
||||
for seam in [125.0] {
|
||||
match shell_1494_with(kernel(true), seam).1 {
|
||||
Err(e) => assert_eq!(e.sm_status(), Some(SM_ERR), "seam {seam}"),
|
||||
other => panic!("seam {seam}: {other:?}"),
|
||||
|
||||
Reference in New Issue
Block a user