Merge pull request #1905 from brett19/fix/960-p2-cliff-construction

fix: a cliff overflow is built and subtracted from the body (#960, WR-631, WR-638)
This commit is contained in:
bherbruck
2026-10-30 23:48:09 -04:00
committed by GitHub
12 changed files with 2160 additions and 63 deletions
+1
View File
@@ -8,6 +8,7 @@
pub mod feature_executive;
pub mod fillet_classification;
pub mod fillet_cliff_overflow;
pub mod fillet_corner;
pub mod fillet_custom_solver;
pub mod fillet_executive;
File diff suppressed because it is too large Load Diff
+168 -22
View File
@@ -171,6 +171,8 @@ use crate::nurbs::b_spline_surface_n_lib::p2::OrientType;
use crate::nurbs::knots::Side;
use crate::query::brep::{BrepFaceServices, FaceCaches};
use crate::surface::plane::Plane;
use crate::surface::surface::p4::NoObject;
use crate::surface::surface::p5::DropPointOptions;
use crate::surface::surface::Surface;
use crate::topology::brep::p7::{make_face_with_curves, FaceCurvesInput};
use crate::topology::brep_cache::CacheObject;
@@ -1468,33 +1470,70 @@ impl FilletCorner {
/// The start of the fork's `SmFilletNx1Corner::CalcCornerEdgeGeom` (not upstream; opt-in
/// `fixes.fillet_nx1_end_rib_global_merge`, WR-303, tangency-labs/solidcore#841;
/// tangency-labs/smlib `967f7ab`). At a corner of at most three edges the one corner
/// edge is the end rib, from one rail's end on its side edge to the other's. When
/// one rail missed its side edge (`SM_FV_NO_INT_RAIL_EU`) because that edge ends
/// within the fillet's width, there is no end rib: it is deleted and the caller sets
/// the executive's global merge, which cuts the round off where it meets the body.
/// Not when the side edge also lies on the fillet's other side surface: it continues
/// the filleted edge between the fillet's own two surfaces, no end exists there, and
/// the end rib fails as before. Returns whether the rib was deleted.
/// tangency-labs/smlib `967f7ab`), outside a cliff overflow: see
/// [`FilletCorner::drop_end_rib`]. Returns whether the rib was deleted for the global
/// merge.
///
/// # Errors
/// `SM_ERR_NULL_POINTER` for a missing edge use, face, surface or curve (upstream
/// dereferences them); those of the surface coincidence test and the deletion.
/// Those of [`FilletCorner::drop_end_rib`].
pub fn drop_end_rib_for_global_merge(
&mut self,
k: &mut Kernel,
target: &mut Brep,
pseudo: &mut FilletBrep,
) -> Result<bool> {
const OP: &str = "blend::fillet_corner::p1::drop_end_rib_for_global_merge";
let mut no_open_ends = |_, _, _| Ok(());
Ok(
self.drop_end_rib(k, target, pseudo, false, &mut no_open_ends)?
== EndRibDrop::GlobalMerge,
)
}
/// The start of the fork's `SmFilletNx1Corner::CalcCornerEdgeGeom` (not upstream; opt-in
/// `fixes.fillet_nx1_end_rib_global_merge`, WR-303, tangency-labs/solidcore#841;
/// tangency-labs/smlib `967f7ab`; in a cliff overflow, `fixes.fillet_cliff_overflow`,
/// WR-631, tangency-labs/smlib `8ba4667` and `3f22e97`). At a corner of at most three
/// edges the one corner edge is the end rib, from one rail's end on its side edge to
/// the other's. When one rail missed its side edge (`SM_FV_NO_INT_RAIL_EU`) because
/// that edge ends within the fillet's width, there is no end rib: it is deleted and
/// the caller sets the executive's global merge, which cuts the round off where it
/// meets the body. Not when the side edge also lies on the fillet's other side
/// surface: it continues the filleted edge between the fillet's own two surfaces, no
/// end exists there, and the end rib fails as before.
///
/// In a cliff overflow (`cliff_overflow`, the executive's `m_bCliffOverflow`) a rail
/// end off the fillet surface (farther than ten times the corner's tolerance), or
/// found on its side edge's line past the edge's end (off the edge by more than the
/// tolerance), is a miss too, and its fillet vertex's status becomes
/// `SM_FV_NO_INT_RAIL_EU`; both rails may miss. A miss that does not run on leaves the
/// end uncut: `note_open_end` (the executive's `NoteOpenEnd`) takes the filleted
/// edge's edge use, the corner's vertex and the end face, then the end rib is deleted
/// ([`EndRibDrop::OpenEnd`]), without the global merge. Both rails missing outside
/// that case is no global merge either.
///
/// # Errors
/// `SM_ERR_NULL_POINTER` for a missing edge use, face, surface or curve (upstream
/// dereferences them; in a cliff overflow, the end rib's fillet surface); `SM_ERR`
/// for an open end whose end face is a face of the filleted edge; those of the
/// drops, the surface coincidence test and the deletion.
#[allow(clippy::too_many_lines)] // one fork routine
pub fn drop_end_rib(
&mut self,
k: &mut Kernel,
target: &mut Brep,
pseudo: &mut FilletBrep,
cliff_overflow: bool,
note_open_end: &mut dyn FnMut(EdgeuseId, VertexId, FaceId) -> Result<()>,
) -> Result<EndRibDrop> {
const OP: &str = "blend::fillet_corner::p1::drop_end_rib";
let [end_rib] = self.edges[..] else {
return Ok(false);
return Ok(EndRibDrop::Kept);
};
if !k.fixes.fillet_nx1_end_rib_global_merge
if !(k.fixes.fillet_nx1_end_rib_global_merge || cliff_overflow)
|| !matches!(self.kind, FilletCornerKind::NByOne | FilletCornerKind::Open)
|| pseudo.fillet_edge(end_rib)?.edge_type != FilletEdgeType::FilletXSideFace
{
return Ok(false);
return Ok(EndRibDrop::Kept);
}
let start_fv =
edge::vertex(pseudo.brep(), end_rib)?.ok_or_else(|| SolidError::null_pointer(OP))?;
@@ -1506,19 +1545,93 @@ impl FilletCorner {
&& pseudo.fillet_vertex(other)?.is_processed(),
)
};
let miss_fv = if missed(start_fv, end_fv)? {
start_fv
let mut miss_fv = if missed(start_fv, end_fv)? {
Some(start_fv)
} else if missed(end_fv, start_fv)? {
end_fv
Some(end_fv)
} else {
return Ok(false);
None
};
// The fork's (8ba4667, 3f22e97): in a cliff overflow a rail can miss its side edge
// with the intersection clamped to the side edge's far vertex, off the fillet, or
// found on the side edge's line past its end: that is a miss too; and both rails
// can miss.
let mut misses = [false, false];
if cliff_overflow {
misses = [
pseudo.fillet_vertex(start_fv)?.status() == FilStatus::NoIntRailEu,
pseudo.fillet_vertex(end_fv)?.status() == FilStatus::NoIntRailEu,
];
let rib_eu = eu1::primary_edgeuse(pseudo.brep(), end_rib)?;
let rib_geom = match rib_eu {
Some(eu) => pseudo.fillet_edgeuse(eu)?.fillet_geom(),
None => None,
};
let fillet_surface = match rib_geom {
Some(g) => pseudo.geom(g)?.fillet_surface(None)?.cloned(),
None => None,
}
.ok_or_else(|| SolidError::null_pointer(OP))?;
let domain = fillet_surface.natural_uv_domain_with_nurb(k)?;
for (miss, fv) in misses.iter_mut().zip([start_fv, end_fv]) {
if *miss || !pseudo.fillet_vertex(fv)?.is_processed() {
continue;
}
let point = pseudo.vertex_point(fv)?;
let dropped = fillet_surface.drop_point::<NoObject>(
k,
point,
&domain,
&DropPointOptions::default(),
)?;
let mut off = !dropped.success || dropped.gap > 10.0 * self.approx_tol_3d;
// The rail's end found on the side edge's line continued past the edge's
// end (SmFilletSolver::RailEdgeuseIntersect continues a straight side
// edge): the rail misses the side edge itself, however close past the end.
let record = pseudo.fillet_vertex(fv)?;
if !off && record.point_class() == PointClassType::Edgeuse {
let Some(EntityRef::Edgeuse(side_eu)) = record.point_class_object() else {
return Err(SolidError::null_pointer(OP));
};
let side_edge =
eu1::edge(target, side_eu)?.ok_or_else(|| SolidError::null_pointer(OP))?;
let curve = edge::curve(target, side_edge)?
.ok_or_else(|| SolidError::null_pointer(OP))?;
let on_side = curve.drop_point(
k,
&edge::interval(target, side_edge)?,
point,
None,
BIG_DOUBLE,
None,
SolverOperation::Minimize,
)?;
off = !on_side.success || on_side.distance > self.approx_tol_3d;
}
if off {
*miss = true;
// its rail is not trimmed there
pseudo
.fillet_vertex_mut(fv)?
.set_status(FilStatus::NoIntRailEu);
}
}
match misses {
[true, _] => miss_fv = Some(start_fv),
[false, true] => miss_fv = Some(end_fv),
[false, false] => {}
}
}
let both_miss = cliff_overflow && misses == [true, true];
let Some(miss_fv) = miss_fv else {
return Ok(EndRibDrop::Kept);
};
let fv = pseudo.fillet_vertex(miss_fv)?;
let Some(EntityRef::Edgeuse(side_eu)) = fv.point_class_object() else {
return Ok(false);
return Ok(EndRibDrop::Kept);
};
if fv.point_class() != PointClassType::Edgeuse {
return Ok(false);
return Ok(EndRibDrop::Kept);
}
// The side edge the rail missed, and the fillet's side face it does not bound.
@@ -1534,6 +1647,7 @@ impl FilletCorner {
eu1::face(target, eu)?.ok_or_else(|| SolidError::null_pointer(OP))
};
let side_faces = [face_of(side_eu)?, face_of(radial(side_eu)?)?];
let fil_faces = [face_of(fil_eu)?, face_of(radial(fil_eu)?)?];
let mut other_face = face_of(fil_eu)?;
if side_faces.contains(&other_face) {
other_face = face_of(radial(fil_eu)?)?;
@@ -1565,11 +1679,31 @@ impl FilletCorner {
)?
.is_some();
if runs_on {
return Ok(false);
return Ok(EndRibDrop::Kept);
}
if cliff_overflow {
// The end face: the side edge's face that is neither of the fillet's side faces.
let [first, second] = side_faces;
let end_face = if first == other_face || fil_faces.contains(&first) {
second
} else {
first
};
if fil_faces.contains(&end_face) {
return Err(SolidError::sm_err(OP));
}
let vertex = self.vertex.ok_or_else(|| SolidError::null_pointer(OP))?;
note_open_end(fil_eu, vertex, end_face)?;
pseudo.delete_wire_fillet_edge(k, end_rib)?;
self.edges.clear();
return Ok(EndRibDrop::OpenEnd);
}
if both_miss || !k.fixes.fillet_nx1_end_rib_global_merge {
return Ok(EndRibDrop::Kept);
}
pseudo.delete_wire_fillet_edge(k, end_rib)?;
self.edges.clear();
Ok(true)
Ok(EndRibDrop::GlobalMerge)
}
/// `CalcCornerEdgeGeom`: `SmFilletEdge::CalcCornerEdgeGeom` on every corner fillet edge
@@ -2826,6 +2960,18 @@ fn make_face(
Ok(made.face)
}
/// What [`FilletCorner::drop_end_rib`] did with an Nx1 corner's end rib.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EndRibDrop {
/// The end rib stays.
Kept,
/// The end rib was deleted; the executive does a global merge (WR-303).
GlobalMerge,
/// The end rib was deleted in a cliff overflow and the fillet left uncut there,
/// noted as an open end (the executive's `NoteOpenEnd`, WR-631).
OpenEnd,
}
#[cfg(test)]
mod tests {
use super::*;
@@ -109,6 +109,26 @@ pub enum NoRoomReason {
/// A rail leaves its face and the fillet ends on more than one face (the global merge,
/// WR-631).
OverflowWithGlobalMerge,
/// A variable round of a cliff overflow, traced on its faces' continuations, does not
/// hold its radius law (the fork's `sm_HoldsItsLaw`, WR-631).
CliffLawNotHeld,
/// A cliff overflow runs past a fillet end whose end face is not planar (WR-631).
CliffEndFaceNotPlanar,
/// The end face's plane does not cut a fillet that overflows its end in two, or does
/// not part it from its middle (WR-631).
CliffEndNotCut,
/// A cliff overflow's fillet boundary lies on the surfaces of two faces (WR-631).
CliffBoundaryOnTwoFaces,
/// A cliff overflow's fillet boundary branches on a face (WR-631).
CliffBoundaryBranches,
/// A cliff overflow's fillet boundary closes on one face (WR-631).
CliffBoundaryClosed,
/// A cliff overflow's fillet boundary leaves a face along no side edge of the filleted
/// edges (WR-631).
CliffBoundaryLeavesAlongNoSideEdge,
/// A cliff overflow's closing face meets two filleted edges at a vertex, or is not
/// bounded by filleted edges between its side edges (WR-631).
CliffClosingFaceUnbounded,
}
/// The initialized error-info members p3 reads or writes. Corner kinds use the
@@ -2456,10 +2476,8 @@ impl<O: ClassObject + SolutionObject + Ord> FilletExecutive<O> {
/// together with a rail rolling over a tangent edge; an overflow with the global
/// merge on (a fillet end on more than one face).
///
/// The cliff overflow's construction (the open ends and the removal region the
/// later #960 changes port) is not in this port yet: an accepted overflow is
/// refused too, as `fillet_refuse_rails_leaving_faces` alone refuses it, after
/// [`FilletExecutive::cliff_overflow`] is set.
/// An accepted overflow is built by the session's `DoFilleting`, which subtracts the
/// region the blends remove (`SubtractRemovalRegion`).
///
/// # Errors
/// The refusal as `SM_ERR`, noted in the error info; those of the checks.
@@ -2518,11 +2536,6 @@ impl<O: ClassObject + SolutionObject + Ord> FilletExecutive<O> {
return self.record_no_room(reason, why);
}
self.cliff_overflow = true;
// the overflow's construction is not ported yet (see the doc)
return self.record_no_room(
NoRoomReason::RailLeavesFace,
"Fillet Error: a rail leaves the face it rolls on (the blend is larger than the faces allow)",
);
}
Ok(())
}
@@ -2979,6 +2992,13 @@ impl<O: ClassObject + SolutionObject + Ord> FilletExecutive<O> {
return Err(failure());
}
self.shrink_fillet_faces(k, access)?;
// the fork's (WR-631): a cliff overflow is subtracted, on the live session only
if self.cliff_overflow {
return Err(SolidError::unported(
"SmFilletExecutive::SubtractRemovalRegion (fork)",
"blend.fillet_executive_p3",
));
}
let target = self.target.ok_or(invalid("missing target B-rep"))?;
let editing = access.editing_enabled(target)?;
access.set_editing_enabled(target, true)?;
@@ -59,9 +59,10 @@ use super::p2::{EdgeMapAccess, FilletParameters, FilletParametersAccess, SolverS
use super::p3::{
BrepCreationAccess, CornerAdjustmentAccess, CornerEdgeAccess, CornerVertexAccess, CornerView,
ErrorInfo, FaceConstructionAccess, FaceShrinkAccess, FaceStitchAccess, FilletExecutive,
RailClassificationAccess, RailTrimAccess, SolverGeometryAccess, StitchAttempt, VertexEntry,
OpenEnd, RailClassificationAccess, RailTrimAccess, SolverGeometryAccess, StitchAttempt,
VertexEntry,
};
use crate::blend::fillet_corner::p1::LawIntervalSolvers;
use crate::blend::fillet_corner::p1::{EndRibDrop, LawIntervalSolvers};
use crate::blend::fillet_corner::p3::{
CornerFilletBrep, CornerPatchEnv, CornerSolverStore, ExecutiveErrorObject, FilletCorner,
FilletCornerKind, Nx2AdjustContext, TangentSplitEnv, TangentSplitOps,
@@ -95,6 +96,7 @@ use crate::topology::{Brep, EdgeId, EdgeuseId, FaceId, VertexId};
mod big_radius;
mod classification;
mod cliff_overflow;
mod concave_nxn;
mod corner_marks;
mod crossing_rails;
@@ -2173,6 +2175,9 @@ impl<'target> FilletSession<'target> {
}
}
/// The operation of the fork's `SmFilletExecutive::NoteOpenEnd` (WR-631).
const OP_NOTE_OPEN_END: &str = "blend::fillet_executive::session::note_open_end";
impl CornerEdgeAccess<FilletObject> for SessionObjects<'_> {
fn edges_adjust_tangent(
&mut self,
@@ -2242,8 +2247,28 @@ impl CornerEdgeAccess<FilletObject> for SessionObjects<'_> {
let corner = corners
.get_mut(id.0 as usize)
.ok_or_else(|| invalid("corner is not live"))?;
// WR-303: the Nx1 corner's end rib hands the round to the global merge.
if corner.drop_end_rib_for_global_merge(k, target, pseudo)? {
// WR-303: the Nx1 corner's end rib hands the round to the global merge; in a
// cliff overflow (WR-631) the end is left open, noted for the subtraction.
let cliff_overflow = executive.cliff_overflow;
let mut open_ends = Vec::new();
let mut note_open_end = |eu, vertex, end_face| -> Result<()> {
// GetFilletSolverOfEdgeuse(pFilEdgeuse), then NoteOpenEnd
let solver = executive
.fillet_solver_of_edgeuse(FilletObject::Topology(
FilletBody::Target,
EntityRef::Edgeuse(eu),
))
.ok_or_else(|| SolidError::null_pointer(OP_NOTE_OPEN_END))?;
open_ends.push(OpenEnd {
solver,
vertex: FilletObject::Topology(FilletBody::Target, EntityRef::Vertex(vertex)),
end_face: FilletObject::Topology(FilletBody::Target, EntityRef::Face(end_face)),
});
Ok(())
};
let dropped = corner.drop_end_rib(k, target, pseudo, cliff_overflow, &mut note_open_end)?;
executive.open_ends.append(&mut open_ends);
if dropped == EndRibDrop::GlobalMerge {
executive.options.global_merge = true;
}
let mut context = SessionCornerEdgeContext {
@@ -0,0 +1,403 @@
//! The cliff overflow's subtraction on the live session (not upstream): the fork's
//! `SmFilletExecutive::SubtractRemovalRegion` and `sm_HoldsItsLaw`
//! (`source/SMLib/src/SmFilletCliffOverflow.cpp`, tangency-labs/smlib `8ba4667`, `ae2a6de`,
//! rev `68fb9162`; `fixes.fillet_cliff_overflow`, WR-631, WR-638). The construction steps
//! are in [`crate::blend::fillet_cliff_overflow`].
//!
//! | Rust | Fork |
//! |--------------------------------------------|-------------------------------------------|
//! | `FilletSession::subtract_removal_region` | `SmFilletExecutive::SubtractRemovalRegion` |
//! | `FilletSession::holds_its_law` (private) | `sm_HoldsItsLaw` |
//!
//! Deviations:
//! - **The removal region leaves the session first.** Once each round's law is checked
//! (which reads the fillet surfaces where the intermediate body owns them), the
//! intermediate body is taken out of the session, as the fork sets `m_pFilletBrep` to
//! `NULL` before the subtraction; the open ends are cut, the region closed, stitched and
//! oriented on it as a stand-alone body. The faces of each solver's fillet geometries are
//! followed through the cuts in a local map rather than by resetting the geometries'
//! fillet surfaces (`SetFilletSurface`), which nothing reads afterwards.
//! - **A failed subtraction keeps the target.** The Boolean consumes its operands, so the
//! target is copied before it and the copy put back when it fails (the fork's in-place
//! merge leaves whatever it reached).
use std::collections::BTreeMap;
use super::{invalid, FilletBody, FilletObject, FilletSession, SolverObject};
use crate::blend::fillet_cliff_overflow::{
close_removal_region, cut_fillet_at_open_end, Blended, Refusal,
};
use crate::blend::fillet_executive::p3::NoRoomReason;
use crate::blend::fillet_geom::p1::{FilletErrorType, FilletGeomId, FilletSurfaceStorage};
use crate::blend::fillet_solver::p1::FilletSurfaceGeneratorType;
use crate::boolean::api_brep::boolean;
use crate::error::{EntityRef, Result, SolidError};
use crate::geom::a_object::BooleanOperation;
use crate::geom::attribute::ids;
use crate::kernel::Kernel;
use crate::math::{Vec3, EFF_ZERO, EFF_ZERO_SQRT, PI};
use crate::surface::surface::p4::NoObject;
use crate::surface::surface::p5::DropPointOptions;
use crate::topology::brep::p3::{orient_trimmed_surfaces, with_kernel_callbacks};
use crate::topology::topology_traverser::Marks;
use crate::topology::{edge, edgeuse::p1 as eu1, face, vertex, Brep, FaceId};
const OP: &str = "blend::fillet_executive::session::cliff_overflow";
/// `smos_Max` of two doubles.
fn smos_max(a: f64, b: f64) -> f64 {
if a > b {
a
} else {
b
}
}
/// `smos_Min` of two doubles.
fn smos_min(a: f64, b: f64) -> f64 {
if a < b {
a
} else {
b
}
}
/// The `SmStatus` upstream's `SER` returns for a failed callee (a `double` function's
/// status as its value, as `SmVariableRadiusFS::GetFilletRadius` returns it).
fn status_of(e: &SolidError) -> i32 {
match e {
SolidError::Upstream { code, .. } => *code,
SolidError::InvalidInput { .. } | SolidError::PortInvariant { .. } => {
crate::error::SM_ERR_INVALID_INPUT
}
_ => crate::error::SM_ERR,
}
}
impl FilletSession<'_> {
/// `GetFilletErrorInfo()->NoteError(code, comment)` of a [`Refusal`].
fn note_refusal(&mut self, refusal: Refusal) {
self.executive.error.code = refusal.code as i32;
self.executive.error.comment = refusal.comment.into();
self.executive.error.no_room = refusal.reason;
}
/// Note `note`'s refusal, if any, and pass `result` on.
fn noted<T>(&mut self, note: Option<Refusal>, result: Result<T>) -> Result<T> {
if let Some(refusal) = note {
self.note_refusal(refusal);
}
result
}
/// The fork's `sm_HoldsItsLaw`: whether a variable round on planar faces holds its
/// radius law between the filleted edge's ends.
///
/// A rolling ball of radius r between two planes meeting at the interior angle a stands
/// off the edge by `r (1 / sin(a/2) - 1)` where its section crosses the wedge's
/// bisector. That point must lie on the fillet surface, at seven points along the edge,
/// to twenty times the solver's tolerance and a ten-thousandth of the radius; of the two
/// angles the planes make, and the bisector either way, the one that fits. Rounds of
/// other solvers, with other than a circular section, on faces that are not both
/// planes, with other than one fillet geometry, or along an edge that is not straight,
/// pass.
fn holds_its_law(&self, k: &Kernel, solver: FilletObject) -> Result<bool> {
let FilletObject::Solver(id) = solver else {
return Err(invalid("object is not a solver"));
};
let object = self.objects.solver(id)?;
let SolverObject::VariableRadius(vr) = object else {
return Ok(true);
};
let base = object.base();
if base.fsg.as_ref().map(
crate::blend::fillet_solver::p1::FilletSurfaceGenerator::fillet_surface_generator_type,
) != Some(FilletSurfaceGeneratorType::Circular)
{
return Ok(true);
}
let mut normals = [Vec3::UNDEF; 2];
for (rail, normal) in normals.iter_mut().enumerate() {
let offset = base
.surface(rail)?
.ok_or_else(|| SolidError::null_pointer(OP))?;
let planar = offset.surface.base_surface().is_planar(k, EFF_ZERO)?;
if !planar.is_planar {
return Ok(true);
}
*normal = planar.normal;
}
let [n0, n1] = normals;
let phi = smos_max(-1.0, smos_min(1.0, n0.dot(n1))).acos();
let [geom] = base.fillet_geoms[..] else {
return Ok(true);
};
let Some(fillet) = self.objects.fillet_surface(geom)? else {
return Ok(true);
};
let fillet = fillet.clone_for_evaluation()?;
let eu = base
.edgeuse(0)?
.ok_or_else(|| SolidError::null_pointer(OP))?;
let target = &*self.objects.target;
let e = eu1::edge(target, eu)?.ok_or_else(|| SolidError::null_pointer(OP))?;
let curve = edge::curve(target, e)?.ok_or_else(|| SolidError::null_pointer(OP))?;
if !curve.is_linear(k, EFF_ZERO, false)?.linear {
return Ok(true);
}
let interval = edge::interval(target, e)?;
let domain = fillet.natural_uv_domain_with_nurb(k)?;
let tol = 20.0 * base.approx_tol_3d;
// (the law evaluates through its own caches: a copy, the solver stays shared)
let mut law = vr.law.clone();
let mut holds = true;
let mut i = 1;
while i < 8 && holds {
let t = interval.evaluate(f64::from(i) / 8.0);
i += 1;
let pnt = curve.evaluate_point(k, t)?;
// SmVariableRadiusFS::GetFilletRadius: SER inside a `double` function
let radius = match law.evaluate(t, &vr.law_interval, vr.law_orientation) {
Ok(values) => values.first().copied().unwrap_or(0.0),
Err(e) if e.is_unported() => return Err(e),
Err(e) => f64::from(status_of(&e)),
}
.abs();
// The section's middle: along the wedge's bisector, r (1 / sin(a/2) - 1) off the
// edge. The planes' normals are both outward (or inward), the bisector along
// their sum and the angle pi less theirs, or one of each, the bisector along
// their difference.
let mut fits = false;
let mut c = 0;
while c < 4 && !fits {
let bisector = if c < 2 { n0 + n1 } else { n0 - n1 };
let angle = if c < 2 { PI - phi } else { phi };
let sign = if c % 2 == 1 { -1.0 } else { 1.0 };
c += 1;
let len = bisector.length();
if len <= EFF_ZERO_SQRT || angle <= EFF_ZERO_SQRT {
continue;
}
let stand_off = radius * (1.0 / (angle / 2.0).sin() - 1.0);
let middle = pnt + bisector * (sign * stand_off / len);
let dropped = fillet.drop_point::<NoObject>(
k,
middle,
&domain,
&DropPointOptions::default(),
)?;
// the solver's tolerance, and its circular sections' relative accuracy (a
// non-rational cubic across the round, within about 2e-5 of the radius)
fits = dropped.success && dropped.gap <= tol + 1.0e-4 * radius;
}
holds = fits;
}
Ok(holds)
}
/// The face of the intermediate body each of `solver`'s fillet geometries' surfaces is
/// on (`pFG->GetFilletSurface()->GetOwner()` in `m_pFilletBrep`), `None` for none.
fn fillet_faces(&self, solver: FilletObject) -> Result<Vec<(FilletGeomId, Option<FaceId>)>> {
let FilletObject::Solver(id) = solver else {
return Err(invalid("object is not a solver"));
};
let body = self
.objects
.intermediate_identity(EntityRef::Brep(crate::topology::BrepId::from_index(0)))?;
let mut out = Vec::new();
for &g in &self.objects.solver(id)?.base().fillet_geoms {
let face = match self.objects.pseudo.geom(g)?.fillet_surface {
Some(FilletSurfaceStorage::ManagedFace { body: owner, face }) if owner == body => {
Some(face)
}
_ => None,
};
out.push((g, face));
}
Ok(out)
}
/// The fork's `SmFilletExecutive::SubtractRemovalRegion` (WR-631, `8ba4667`; its errors
/// as `ae2a6de` keeps them, WR-638): in a cliff overflow, subtract the region the blends
/// remove from the target: the blends built on their faces' continuations, then cut by
/// what lies beyond the cliffs.
///
/// Each variable round on planes must hold its law (`NoRoom` otherwise); each open end
/// is cut ([`cut_fillet_at_open_end`]); the intermediate body is closed into the
/// removal region ([`close_removal_region`]), stitched, and oriented as a solid; a
/// region that is not a manifold solid without lamina edges is `BadMerge`. The fillet
/// edges' attribute leaves the region's edges, and the region is subtracted from the
/// target by the manifold Boolean (`BadMerge` when that fails). Each refusal is noted
/// in the error info.
///
/// # Errors
/// The refusals as `SM_ERR`; those of the steps.
#[allow(clippy::too_many_lines)] // one fork routine
pub(super) fn subtract_removal_region(&mut self, k: &mut Kernel) -> Result<()> {
if self.objects.intermediate.is_none() {
return Err(SolidError::null_pointer(OP));
}
let solvers = self.executive.solvers.clone();
for &solver in &solvers {
if !self.holds_its_law(k, solver)? {
self.note_refusal(Refusal {
code: FilletErrorType::NoRoom,
reason: Some(NoRoomReason::CliffLawNotHeld),
comment: "Fillet Error: a variable round traced on its faces' continuations does not hold its radius law",
});
return Err(SolidError::sm_err(OP));
}
}
// What the cuts and the closing read of the solvers, before the region leaves.
let mut faces_of: BTreeMap<FilletObject, Vec<(FilletGeomId, Option<FaceId>)>> =
BTreeMap::new();
let mut tol = 0.0;
let mut blended_edges = Vec::new();
for &solver in &solvers {
let FilletObject::Solver(id) = solver else {
return Err(invalid("object is not a solver"));
};
let base = self.objects.solver(id)?.base();
tol = smos_max(tol, base.approx_tol_3d);
let eu = base
.edgeuse(0)?
.ok_or_else(|| SolidError::null_pointer(OP))?;
blended_edges.push(
eu1::edge(self.objects.target, eu)?.ok_or_else(|| SolidError::null_pointer(OP))?,
);
faces_of.insert(solver, self.fillet_faces(solver)?);
}
let open_ends = self.executive.open_ends.clone();
let mut region = self
.objects
.intermediate
.take()
.ok_or_else(|| SolidError::null_pointer(OP))?;
let mut note = None;
for end in &open_ends {
let FilletObject::Solver(id) = end.solver else {
return Err(invalid("open end solver is not a solver"));
};
let (
FilletObject::Topology(FilletBody::Target, EntityRef::Vertex(v)),
FilletObject::Topology(FilletBody::Target, EntityRef::Face(end_face)),
) = (end.vertex, end.end_face)
else {
return Err(invalid("open end is not target topology"));
};
let base = self.objects.solver(id)?.base();
let solver_tol = base.approx_tol_3d;
let target = &*self.objects.target;
let end_surface = face::p2::surface(target, end_face)?
.ok_or_else(|| SolidError::null_pointer(OP))?
.clone_for_evaluation()?;
// the fillet's point nearest the middle of the filleted edge
let eu = base
.edgeuse(0)?
.ok_or_else(|| SolidError::null_pointer(OP))?;
let fil_edge = eu1::edge(target, eu)?.ok_or_else(|| SolidError::null_pointer(OP))?;
let edge_mid = edge::curve(target, fil_edge)?
.ok_or_else(|| SolidError::null_pointer(OP))?
.evaluate_point(k, edge::interval(target, fil_edge)?.evaluate(0.5))?;
let vertex_point = vertex::point(target, v)?;
let entry = faces_of
.get_mut(&end.solver)
.ok_or_else(|| SolidError::null_pointer(OP))?;
let mut faces: Vec<Option<FaceId>> = entry.iter().map(|&(_, f)| f).collect();
let cut = cut_fillet_at_open_end(
k,
&mut region,
&mut faces,
&end_surface,
vertex_point,
edge_mid,
solver_tol,
&mut note,
);
for (slot, f) in entry.iter_mut().zip(faces) {
slot.1 = f;
}
self.noted(note.take(), cut)?;
}
let blended = Blended::of_edges(self.objects.target, &blended_edges)?;
let closed = close_removal_region(
k,
&mut region,
self.objects.target,
&blended,
tol,
&mut note,
);
self.noted(note.take(), closed)?;
// stitch the closing faces to the sheet, and orient the closed shell as a solid
let tolerance = region.tolerance();
let stitched =
crate::topology::brep::p4::stitch_faces(k, &mut region, tolerance, None, false)?;
let mut maybe_open = false;
if stitched.lamina_edges == 0 {
let oriented = with_kernel_callbacks(k, &mut region, |k, b, env| {
let mut marks = Marks::default();
orient_trimmed_surfaces(k, b, env, &mut marks, true, false, None)
})?;
maybe_open = oriented.maybe_not_closed_solid;
}
if stitched.lamina_edges != 0
|| maybe_open
|| !crate::topology::brep::p2::is_manifold_solid(&region)?
{
self.note_refusal(Refusal {
code: FilletErrorType::BadMerge,
reason: None,
comment:
"Fillet Error: a cliff overflow's removal region does not close into a solid",
});
return Err(SolidError::sm_err(OP));
}
// the fillet edges' attribute stays with the executive's own B-reps
if let (Some(serial), Some(FilletObject::Attribute(handle))) =
(region.notify_serial(), self.executive.edge_attribute)
{
for e in crate::topology::brep::p1::edges(&region, None)? {
let key = crate::topology::brep::p3::topology_ref(serial, EntityRef::Edge(e)).key;
if self
.objects
.attributes
.find_attribute(key, ids::ALL_FILLET_EDGES)
== Some(handle)
{
self.objects
.attributes
.remove_attribute(key, handle, false)?;
}
}
}
// the region from the target, by the Boolean (the merge modifies the target in
// place; the region is deleted after)
let kept = (*self.objects.target).clone();
let taken: Brep = std::mem::take(&mut *self.objects.target);
match boolean(k, taken, region, BooleanOperation::Difference) {
Ok(outcome) => {
*self.objects.target = outcome.result;
Ok(())
}
Err(e) => {
*self.objects.target = kept;
if e.is_unported() {
return Err(e);
}
self.note_refusal(Refusal {
code: FilletErrorType::BadMerge,
reason: None,
comment: "Fillet Error: subtracting a cliff overflow's removal region failed",
});
Err(SolidError::sm_err(OP))
}
}
}
}
@@ -590,6 +590,17 @@ impl FilletSession<'_> {
session.mark_corner_patches()?;
let editing = session.objects.target.editing_enabled();
session.objects.target.set_editing_enabled(true);
// The fork's (WR-631, WR-638): a cliff overflow closes the fillet faces into the
// region the blends remove and subtracts it from the target, instead of merging
// the fillet faces in; the error a failed subtraction noted stands.
if session.executive.cliff_overflow {
let result = session.subtract_removal_region(k);
session.objects.target.set_editing_enabled(editing);
if let Some(output) = error_out {
output.assign(&session.executive.error);
}
return result;
}
let result = session.merge_fillet(k);
session.objects.target.set_editing_enabled(editing);
result?;
+1 -1
View File
@@ -472,7 +472,7 @@ pub struct UpstreamFixes {
pub fillet_trace_reaches_rails: bool,
/// `SmFilletVertex::CalcRailIntEdgeuse` places a rail's end found on a side edge at the rail's base surface evaluated with out-of-bounds evaluation on (the surface continued, as `RailEdgeuseIntersect`'s solve over the extended domain found it), not clamped to the surface's own domain, where a rail meeting a side edge's line just past the edge's end (a cliff overflow, with `fillet_trace_reaches_rails`) was put on the edge's end vertex, off the rail by the square of that distance over twice the radius, and taken as meeting the side edge (WR-630, #960/#1467; the fork's tangency-labs/smlib@3f22e97).
pub fillet_rail_end_on_continued_surface: bool,
/// SolidCraft's cliff overflow (WR-631, #960; the fork's tangency-labs/smlib@8ba4667 and later, rev 68fb9162): with `fillet_refuse_rails_leaving_faces` (which must be on too; this flag does nothing without it), a convex blend whose rails leave the faces they roll on across cliffs (an opening's rim, the body's outline) is built on the faces' continuations and the material it removes is subtracted from the body, instead of being refused. A concave overflow, a round running on into the body's material, an overflow with another blend concave, with a rollover, or with the global merge on, are refused with `FilletErrorType::NoRoom` and their own messages. In this port so far the choice and its refusals are ported (`FilletExecutive::cliff_overflow`); an accepted overflow is still refused with the `fillet_refuse_rails_leaving_faces` message until its construction lands. Use with `fillet_trace_reaches_rails` and `fillet_rail_end_on_continued_surface`, as the fork has them.
/// SolidCraft's cliff overflow (WR-631, #960; the fork's tangency-labs/smlib@8ba4667 and later, rev 68fb9162): with `fillet_refuse_rails_leaving_faces` (which must be on too; this flag does nothing without it), a convex blend whose rails leave the faces they roll on across cliffs (an opening's rim, the body's outline) is built on the faces' continuations and the material it removes is subtracted from the body, instead of being refused. A concave overflow, a round running on into the body's material, an overflow with another blend concave, with a rollover, or with the global merge on, are refused with `FilletErrorType::NoRoom` and their own messages. An accepted overflow (`FilletExecutive::cliff_overflow`) leaves the Nx1 corners' ends its rails run past uncut (`FilletCorner::drop_end_rib`, the open ends), and `DoFilleting` subtracts it instead of merging (`SubtractRemovalRegion`, `crate::blend::fillet_cliff_overflow`): each variable round on planes must hold its radius law; each open end is cut by the end face's plane; the fillet sheet is closed into the region the blends remove by faces on the hosts' surfaces (a plane continued exactly), stitched, oriented, and subtracted from the target by the Boolean. A construction that fails keeps its own error (WR-638, tangency-labs/smlib@ae2a6de): `NoRoom` where it finds no room, `NotExact` for a continuation that cannot be exact (a non-planar surface past its domain, a curved side edge past its end), `BadMerge` for a region that does not close or a failed subtraction. Use with `fillet_trace_reaches_rails` and `fillet_rail_end_on_continued_surface`, as the fork has them.
pub fillet_cliff_overflow: bool,
/// `SmMerge::BooleanLists` copies its surface operands with the virtual `Copy` (the derived surface, its natural domain and its kind), where upstream's base copy constructor slices every surface to a geometry-less `SmSurface`, so a surface list always fails with `SM_ERR` and the planar 2D path is never taken (WR-196; `plan/upstream-issues/WR-196--booleanlists-slices-derived-surfaces-into-base-smsurface.md`).
pub boolean_lists_virtual_surface_copies: bool,
@@ -9,8 +9,7 @@
//! that side and the 3 wide step; the edge at (7,5) is concave, the step's foot.
//!
//! - r6 at (10,0): the rail on the 5 high side runs past its rim (10,5), a cliff: an
//! overflow, `cliff_overflow` set. Its construction is not ported yet, so it is still
//! refused with the generic message.
//! overflow, `cliff_overflow` set, and built (`wr631_cliff_overflow_construction`).
//! - r4 at (10,5): the rail on the 3 wide step runs into the wall beyond its foot: a
//! round is refused ("into the body's material"); a chamfer is an overflow.
//! - r8 at (7,5): concave, the rail on the step runs past its rim: refused ("adds
@@ -84,8 +83,8 @@ fn edge_at(k: &Kernel, b: &Brep, x: f64, y: f64) -> EdgeId {
}
/// Rounds (`chamfer` false) or chamfers the edge at `(x, y)`: whether it was taken for a
/// cliff overflow, the fillet error code and comment.
fn blend(cliff: bool, x: f64, y: f64, r: f64, chamfer: bool) -> (bool, i32, String) {
/// cliff overflow, and the fillet error code and comment of a refusal (`None`: built).
fn blend(cliff: bool, x: f64, y: f64, r: f64, chamfer: bool) -> (bool, Option<(i32, String)>) {
let mut k = kernel(cliff);
let mut b = stepped_bar(&mut k);
let e = edge_at(&k, &b, x, y);
@@ -94,12 +93,11 @@ fn blend(cliff: bool, x: f64, y: f64, r: f64, chamfer: bool) -> (bool, i32, Stri
session
.prepare_constant_radius_edges(&mut k, &[e], r, xsect, 1, 1.0)
.unwrap();
assert!(session.do_fillet(&mut k).is_err());
let built = session.do_fillet(&mut k).is_ok();
let info = session.error_info();
(
session.executive().cliff_overflow,
info.code,
info.comment.clone(),
(!built).then(|| (info.code, info.comment.clone())),
)
}
@@ -107,14 +105,11 @@ const NO_ROOM: i32 = FilletErrorType::NoRoom as i32;
#[test]
fn a_convex_round_over_a_cliff_is_a_cliff_overflow() {
assert_eq!(
blend(true, 10.0, 0.0, 6.0, false),
(true, NO_ROOM, GENERIC.into())
);
assert_eq!(blend(true, 10.0, 0.0, 6.0, false), (true, None));
// without the flag: refused as before, no overflow
assert_eq!(
blend(false, 10.0, 0.0, 6.0, false),
(false, NO_ROOM, GENERIC.into())
(false, Some((NO_ROOM, GENERIC.into())))
);
}
@@ -124,14 +119,13 @@ fn a_round_into_the_material_is_refused_and_a_chamfer_overflows() {
blend(true, 10.0, 5.0, 4.0, false),
(
false,
NO_ROOM,
"Fillet Error: a round's rail leaves the face it rolls on into the body's material (beyond a wall)".into()
Some((
NO_ROOM,
"Fillet Error: a round's rail leaves the face it rolls on into the body's material (beyond a wall)".into()
))
)
);
assert_eq!(
blend(true, 10.0, 5.0, 4.0, true),
(true, NO_ROOM, GENERIC.into())
);
assert_eq!(blend(true, 10.0, 5.0, 4.0, true), (true, None));
}
#[test]
@@ -140,16 +134,22 @@ fn a_concave_overflow_is_refused() {
blend(true, 7.0, 5.0, 8.0, false),
(
false,
NO_ROOM,
"Fillet Error: a rail leaves the face it rolls on (the blend is larger than the faces allow, and adds material)".into()
Some((
NO_ROOM,
"Fillet Error: a rail leaves the face it rolls on (the blend is larger than the faces allow, and adds material)".into()
))
)
);
assert_eq!(blend(false, 7.0, 5.0, 8.0, false).1, NO_ROOM);
assert_eq!(
blend(false, 7.0, 5.0, 8.0, false).1.map(|e| e.0),
Some(NO_ROOM)
);
}
/// The typed reason of the refusal of the edge at `(x, y)`.
fn reason(x: f64, y: f64, r: f64, chamfer: bool) -> Option<NoRoomReason> {
let mut k = kernel(true);
/// The typed reason of the refusal of the edge at `(x, y)` (`cliff`: with the cliff
/// overflow).
fn reason(cliff: bool, x: f64, y: f64, r: f64, chamfer: bool) -> Option<NoRoomReason> {
let mut k = kernel(cliff);
let mut b = stepped_bar(&mut k);
let e = edge_at(&k, &b, x, y);
let mut session = FilletSession::new(&mut k, &mut b).unwrap();
@@ -163,15 +163,15 @@ fn reason(x: f64, y: f64, r: f64, chamfer: bool) -> Option<NoRoomReason> {
#[test]
fn each_refusal_names_its_reason() {
assert_eq!(
reason(10.0, 0.0, 6.0, false),
reason(false, 10.0, 0.0, 6.0, false),
Some(NoRoomReason::RailLeavesFace)
);
assert_eq!(
reason(10.0, 5.0, 4.0, false),
reason(true, 10.0, 5.0, 4.0, false),
Some(NoRoomReason::RoundIntoMaterial)
);
assert_eq!(
reason(7.0, 5.0, 8.0, false),
reason(true, 7.0, 5.0, 8.0, false),
Some(NoRoomReason::ConcaveOverflow)
);
}
@@ -0,0 +1,368 @@
//! WR-631 / WR-638 (#960; the fork's tangency-labs/smlib@8ba4667 and ae2a6de, rev 68fb9162):
//! with `fillet_refuse_rails_leaving_faces` and `fillet_cliff_overflow`, a convex blend
//! whose rail leaves its face across a cliff is built on the faces' continuations, closed
//! into the region it removes and subtracted from the body. Without the flag the same blends
//! are refused (`NoRoom`), as before.
//!
//! Every expected volume is the body less the blend's corner region clipped to the body, in
//! closed form: for a round of radius r on an edge between two perpendicular faces w and h
//! wide, the section removed is `{0 <= u <= min(w, r), 0 <= v <= min(h, r)}` outside the
//! circle of radius r about `(r, r)`.
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::cast_precision_loss
)]
use solidcore::blend::fillet_executive::session::FilletSession;
use solidcore::blend::fillet_geom::p1::FilletErrorType;
use solidcore::blend::fillet_standard_solver::p1::FilletLaw;
use solidcore::boolean::api_brep::boolean_difference;
use solidcore::curve::curve::Curve;
use solidcore::geom::attribute::{AttributeStore, ObjectKey};
use solidcore::mass::integrator::properties::{brep_volume, PropertyAttributes};
use solidcore::math::{Point3, Vec3};
use solidcore::primitive::api_curves::create_line_segment;
use solidcore::primitive::api_primitives::{create_box, create_cylinder};
use solidcore::sweep::api_brep::create_linear_sweep;
use solidcore::topology::brep::p2::is_manifold_solid;
use solidcore::topology::{edge, EdgeId};
use solidcore::{Brep, Kernel};
fn kernel(cliff: bool) -> Kernel {
let mut k = Kernel::default();
k.fixes.fillet_refuse_rails_leaving_faces = true;
k.fixes.fillet_trace_reaches_rails = true;
k.fixes.fillet_rail_end_on_continued_surface = true;
k.fixes.fillet_cliff_overflow = cliff;
k
}
fn volume(k: &Kernel, b: &mut Brep) -> f64 {
let store = AttributeStore::default();
let attrs = PropertyAttributes {
store: &store,
key_of: &|_| ObjectKey(0),
};
brep_volume(k, b, 1e-6, &attrs).unwrap()
}
/// The area of the section a round of radius `r` removes between faces `w` and `h` wide
/// (perpendicular), in closed form.
fn removed_area(r: f64, w: f64, h: f64) -> f64 {
let a = w.min(r);
let f = |s: f64| s / 2.0 * (r * r - s * s).max(0.0).sqrt() + r * r / 2.0 * (s / r).asin();
let u_h = if h >= r {
0.0
} else {
r - (r * r - (r - h) * (r - h)).sqrt()
};
let mut area = if h < r { h * u_h.min(a) } else { 0.0 };
if a > u_h {
area += r * (a - u_h) - (f(r - u_h) - f(r - a));
}
area
}
/// The edge from `p` to `q` (either way).
fn edge_between(k: &Kernel, b: &Brep, p: Point3, q: Point3) -> EdgeId {
b.edges()
.ids()
.find(|&e| {
let ivl = edge::interval(b, e).unwrap();
let c = edge::curve(b, e).unwrap().unwrap();
let s = c.evaluate_point(k, ivl.min()).unwrap();
let t = c.evaluate_point(k, ivl.max()).unwrap();
(s.distance(p) < 1e-9 && t.distance(q) < 1e-9)
|| (s.distance(q) < 1e-9 && t.distance(p) < 1e-9)
})
.expect("edge")
}
/// The outcome of a blend: the error code and comment of a refusal, or the volume of the
/// result (a manifold solid).
#[derive(Debug)]
enum Outcome {
Built(f64),
Refused(i32, String),
}
/// Rounds (`chamfer` false: circular section; true: linear) the edge from `p` to `q` of
/// the body `make` builds, by `r`.
fn blend(
cliff: bool,
make: fn(&mut Kernel) -> Brep,
p: Point3,
q: Point3,
r: f64,
chamfer: bool,
) -> Outcome {
let mut k = kernel(cliff);
let mut b = make(&mut k);
let e = edge_between(&k, &b, p, q);
let mut session = FilletSession::new(&mut k, &mut b).unwrap();
session
.prepare_constant_radius_edges(&mut k, &[e], r, u32::from(!chamfer), 1, 1.0)
.unwrap();
let result = session.do_fillet(&mut k);
let info = session.error_info().clone();
drop(session);
match result {
Ok(()) => {
assert!(
is_manifold_solid(&b).unwrap(),
"r {r}: not a manifold solid"
);
Outcome::Built(volume(&k, &mut b))
}
Err(_) => Outcome::Refused(info.code, info.comment),
}
}
fn assert_builds(outcome: &Outcome, expected: f64, rel: f64) {
let Outcome::Built(v) = outcome else {
panic!("expected a build at {expected}, got {outcome:?}");
};
assert!(
(v - expected).abs() <= rel * expected,
"volume {v} against {expected} (rel {})",
(v - expected).abs() / expected
);
}
fn assert_no_room(outcome: &Outcome) {
let Outcome::Refused(code, _) = outcome else {
panic!("expected a refusal, got {outcome:?}");
};
assert_eq!(*code, FilletErrorType::NoRoom as i32, "{outcome:?}");
}
fn p(x: f64, y: f64, z: f64) -> Point3 {
Point3::new(x, y, z)
}
/// The stepped bar of `wr631_cliff_overflow_choice`: its profile
/// `(0,0) (10,0) (10,5) (7,5) (7,10) (0,10)` swept 20 along z.
fn stepped_bar(k: &mut Kernel) -> Brep {
const PROFILE: [[f64; 2]; 6] = [
[0.0, 0.0],
[10.0, 0.0],
[10.0, 5.0],
[7.0, 5.0],
[7.0, 10.0],
[0.0, 10.0],
];
let cs: Vec<Curve> = PROFILE
.iter()
.zip(PROFILE.iter().cycle().skip(1))
.map(|(a, b)| {
Curve::from(
create_line_segment(Vec3::new(a[0], a[1], 0.0), Vec3::new(b[0], b[1], 0.0))
.unwrap(),
)
})
.collect();
let refs: Vec<Option<&Curve>> = cs.iter().map(Some).collect();
create_linear_sweep(k, &refs, Vec3::new(0.0, 0.0, 1.0), 20.0, true).unwrap()
}
/// An 80 x 5 x 5 bar (#1466).
fn bar(k: &mut Kernel) -> Brep {
create_box(k, Point3::ZERO, 80.0, 5.0, 5.0).unwrap()
}
/// A 40 x 20 x 10 box (#1467).
fn box_40_20_10(k: &mut Kernel) -> Brep {
create_box(k, Point3::ZERO, 40.0, 20.0, 10.0).unwrap()
}
#[test]
fn a_round_over_a_step_rim_is_subtracted() {
// r6 at (10, 0): the rail on the 5 high side runs past the step's rim
let expected = 1700.0 - 20.0 * removed_area(6.0, 10.0, 5.0);
let (a, b) = (p(10.0, 0.0, 0.0), p(10.0, 0.0, 20.0));
assert_builds(&blend(true, stepped_bar, a, b, 6.0, false), expected, 1e-8);
assert_no_room(&blend(false, stepped_bar, a, b, 6.0, false));
}
#[test]
fn a_chamfer_into_a_wall_foot_is_subtracted() {
// chamfer 4 at (10, 5): it reaches x = 6 on the 3 wide step, past the wall's foot; the
// triangle it removes lies inside the body
let (a, b) = (p(10.0, 5.0, 0.0), p(10.0, 5.0, 20.0));
assert_builds(
&blend(true, stepped_bar, a, b, 4.0, true),
1700.0 - 20.0 * 8.0,
1e-9,
);
assert_no_room(&blend(false, stepped_bar, a, b, 4.0, true));
}
#[test]
fn a_round_wider_than_a_bar_is_subtracted() {
// #1466: the long top edge of an 80 x 5 x 5 bar; r9 and r10 run past both faces
let (a, b) = (p(0.0, 5.0, 5.0), p(80.0, 5.0, 5.0));
for r in [9.0, 10.0, 12.0] {
let expected = 80.0 * 25.0 - 80.0 * removed_area(r, 5.0, 5.0);
assert_builds(&blend(true, bar, a, b, r, false), expected, 1e-8);
}
assert_no_room(&blend(false, bar, a, b, 10.0, false));
}
#[test]
fn a_round_just_past_a_box_side_is_subtracted() {
// #1467: the top edge of the 40 x 20 x 10 box on its 10 high face, r just over 10 (the
// radii of the fork's test)
let (a, b) = (p(0.0, 20.0, 10.0), p(40.0, 20.0, 10.0));
for r in [10.05, 10.07, 12.0] {
let expected = 8000.0 - 40.0 * removed_area(r, 20.0, 10.0);
assert_builds(&blend(true, box_40_20_10, a, b, r, false), expected, 1e-8);
}
assert_no_room(&blend(false, box_40_20_10, a, b, 10.05, false));
}
/// A 40 x 20 x 10 block with a through hole of radius 3 at (20, 14.5) (#422, SolidCraft
/// rig 324, `fixtures/solidcraft/324-round-beside-through-hole.json`).
fn block_with_hole(k: &mut Kernel) -> Brep {
let block = create_box(k, Point3::ZERO, 40.0, 20.0, 10.0).unwrap();
let drill = create_cylinder(k, p(20.0, 14.5, -1.0), 3.0, 12.0).unwrap();
boolean_difference(k, block, drill).unwrap()
}
#[test]
fn a_round_over_a_hole_is_subtracted() {
// #422: R4 on the top edge at y = 20; its rail on the top face runs into the hole, whose
// rim is a cliff. The block less the hole and the round's corner, plus the part of that
// corner inside the hole: O = int_16^17.5 2 sqrt(9 - (y - 14.5)^2) (4 - sqrt(16 - (y -
// 16)^2)) dy = 0.3758296360 (the fixture's formula, its value)
let (a, b) = (p(0.0, 20.0, 10.0), p(40.0, 20.0, 10.0));
let expected = 7580.287315387334;
assert_builds(
&blend(true, block_with_hole, a, b, 4.0, false),
expected,
1e-8,
);
assert_no_room(&blend(false, block_with_hole, a, b, 4.0, false));
}
/// The 10 x 20 x 30 box of `wr609_trace_reaches_rails`; its top edge at x = 0 is 20 long,
/// the top face 10 wide, the side 30 high.
fn box_10_20_30(k: &mut Kernel) -> Brep {
create_box(k, Point3::ZERO, 10.0, 20.0, 30.0).unwrap()
}
#[test]
fn a_constant_round_past_a_box_top_is_subtracted() {
// R12: the rail on the 10 wide top runs past its far edge (the fork's 5384.17828518)
let (a, b) = (p(0.0, 0.0, 30.0), p(0.0, 20.0, 30.0));
let expected = 6000.0 - 20.0 * removed_area(12.0, 10.0, 30.0);
assert!((expected - 5384.17828518).abs() < 1e-8);
assert_builds(
&blend(true, box_10_20_30, a, b, 12.0, false),
expected,
1e-8,
);
assert_no_room(&blend(false, box_10_20_30, a, b, 12.0, false));
}
/// The variable round from `r0` to `r1` (linear) of the box's top edge at x = 0.
fn variable_round(cliff: bool, r0: f64, r1: f64) -> Outcome {
let mut k = kernel(cliff);
let mut b = box_10_20_30(&mut k);
let e = edge_between(&k, &b, p(0.0, 0.0, 30.0), p(0.0, 20.0, 30.0));
let mut session = FilletSession::new(&mut k, &mut b).unwrap();
session
.prepare_variable_radius_edges(&mut k, &[e], r0, FilletLaw::linear(r0, r1, None), 1, 1)
.unwrap();
let result = session.do_fillet(&mut k);
let info = session.error_info().clone();
drop(session);
match result {
Ok(()) => {
assert!(is_manifold_solid(&b).unwrap(), "not a manifold solid");
Outcome::Built(volume(&k, &mut b))
}
Err(_) => Outcome::Refused(info.code, info.comment),
}
}
/// The box less the variable round's section along the edge, by the midpoint rule (the
/// rolling ball's section across the edge is the circle of the radius there, to the
/// accuracy of the round's cubic sections).
fn variable_round_expected(r0: f64, r1: f64) -> f64 {
let n = 20_000;
let mut removed = 0.0;
for i in 0..n {
let t = (f64::from(i) + 0.5) / f64::from(n);
removed += removed_area(r0 + (r1 - r0) * t, 10.0, 30.0);
}
6000.0 - removed * 20.0 / f64::from(n)
}
#[test]
fn a_variable_round_past_a_box_top_is_subtracted() {
// the fork's 2 -> 11, 20, 30, 50 (5789.69878, 5391.54581, 4819.37775, 3546.18768), from
// either end, to 1e-4 (the variable round's cubic sections' accuracy)
for (r1, fork) in [
(11.0, 5789.69878),
(20.0, 5391.54581),
(30.0, 4819.37775),
(50.0, 3546.18768),
] {
let expected = variable_round_expected(2.0, r1);
assert!((expected - fork).abs() < 1e-4, "{r1}: {expected}");
assert_builds(&variable_round(true, 2.0, r1), expected, 1e-4);
assert_builds(&variable_round(true, r1, 2.0), expected, 1e-4);
}
assert_no_room(&variable_round(false, 2.0, 11.0));
}
/// The 40 x 20 x 10 box with its x = 0 end cut concave by a vertical cylinder of radius 30
/// about (-25, 10): the top edge at y = 20 ends on that cylinder.
fn box_with_round_end(k: &mut Kernel) -> Brep {
let block = create_box(k, Point3::ZERO, 40.0, 20.0, 10.0).unwrap();
let cutter = create_cylinder(k, p(-25.0, 10.0, -1.0), 30.0, 12.0).unwrap();
boolean_difference(k, block, cutter).unwrap()
}
#[test]
fn an_overflow_the_construction_cannot_cut_reports_its_own_error() {
// WR-638 (ae2a6de): R12 on the top edge at y = 20 runs past the 10 high side; at the
// cylinder end its rails miss their side edges, and the end face that would cut the
// fillet off there is not planar. The refusal is the construction's own, not the generic
// "larger than the faces" (which the flag off still gives).
let x0 = -25.0 + 800.0f64.sqrt();
let (a, b) = (p(x0, 20.0, 10.0), p(40.0, 20.0, 10.0));
let Outcome::Refused(code, comment) = blend(true, box_with_round_end, a, b, 12.0, false) else {
panic!("expected a refusal");
};
assert_eq!(code, FilletErrorType::NoRoom as i32);
assert_eq!(
comment,
"Fillet Error: a cliff overflow runs past a fillet end whose end face is not planar"
);
let Outcome::Refused(code, comment) = blend(false, box_with_round_end, a, b, 12.0, false)
else {
panic!("expected a refusal");
};
assert_eq!(code, FilletErrorType::NoRoom as i32);
assert_eq!(
comment,
"Fillet Error: a rail leaves the face it rolls on (the blend is larger than the faces allow)"
);
}
#[test]
fn a_cliff_overflow_is_deterministic() {
let (a, b) = (p(10.0, 0.0, 0.0), p(10.0, 0.0, 20.0));
let runs: Vec<f64> = (0..2)
.map(|_| match blend(true, stepped_bar, a, b, 6.0, false) {
Outcome::Built(v) => v,
other @ Outcome::Refused(..) => panic!("{other:?}"),
})
.collect();
assert_eq!(runs[0].to_bits(), runs[1].to_bits());
}
@@ -0,0 +1,9 @@
Updates: WR-631
The construction of an accepted cliff overflow is ported (the rest of tangency-labs/smlib@8ba4667, rev `68fb9162`, with the Nx1 miss test of tangency-labs/smlib@3f22e97), behind the same `Kernel::fixes.fillet_cliff_overflow`. An accepted overflow builds; it is no longer refused with the WR-302 message.
- **Open ends** (`SmFilletNx1Corner::CalcCornerEdgeGeom`): in an overflow a rail end off the fillet surface by more than ten times the corner's tolerance, or found on its side edge's line past the edge's end (off the edge by more than the tolerance, 3f22e97), is a miss; both rails may miss. A miss that does not run on deletes the end rib and notes the end (`NoteOpenEnd`) instead of setting the global merge: `FilletCorner::drop_end_rib`, `blend/fillet_corner/p1.rs`; the session's `edges_calculate` records `FilletExecutive::open_ends`.
- **Subtraction** (`SmFilletExecutive::DoFilleting`, `SubtractRemovalRegion`, `sm_HoldsItsLaw`): `blend/fillet_executive/session/cliff_overflow.rs`. A variable round on planes must keep its section's middle on its surface at seven points; each open end is cut by the end face's plane (`CutFilletAtOpenEnd`); the sheet is closed (`CloseRemovalRegion`: the host test, the chain walk and the closing face are separate functions, `blend/fillet_cliff_overflow.rs`), stitched, oriented and checked to be a manifold solid, and subtracted from the target by the Boolean.
- **Not covered by the fork's 68fb9162 either:** a box edge rounded r10.02 on its 10 high face (the fillet meets the bottom face 2e-5 from its edge) builds an empty body; the fork's test uses r10.05 and up, which build.
Tests: `crates/solidcore/tests/wr631_cliff_overflow_construction.rs` (closed-form volumes: a stepped bar's round over a step rim and chamfer under a wall; the 80 x 5 x 5 bar r9/r10/r12, #1466; the 40 x 20 x 10 box r10.05/r10.07/r12, #1467; the 10 x 20 x 30 box R12, 5384.17828518, and the variable rounds 2 -> 11, 20, 30, 50 from either end; the round over a through hole, #422, 7580.28731536; each refused without the flag; determinism).
@@ -0,0 +1,11 @@
#### WR-638. A cliff overflow that cannot be built is reported as having no room
- **Severity:** Wrong result (error report)
- **Area:** Blend (fillet executive)
- **Location:** the fork's `source/SMLib/src/SmFilletExecutive.cpp`, `SmFilletExecutive::DoFilleting` and `SmFilletCliffOverflow.cpp`, `NoteUnbuiltOverflow` (tangency-labs/smlib@8ba4667).
- **What happens:** when the construction of a cliff overflow (WR-631) fails, 8ba4667 rewrote whatever the failing step noted into `SM_FILERR_NO_ROOM` with the generic "larger than the faces" comment (`NoteUnbuiltOverflow`), unless a continuation was not exact. A caller fitting the radius could not tell a removal region that did not close (`SM_FILERR_BAD_MERGE`), or a step that found no room for its own reason, from a blend too large for its faces.
- **Trigger:** a round on a box's top edge past its 10 high side whose end face is a concave cylinder: the open end cannot be cut by a plane.
- **How found:** SolidCraft (#1071, #960). The fork removes `NoteUnbuiltOverflow` in tangency-labs/smlib@ae2a6de ("a cliff overflow that fails reports its own error"): `CreateFilletBrep` and `SubtractRemovalRegion` failures keep the error their step noted.
- **Confirmed:** no C++ run of the fork was made; not reachable in the pinned C++ (`3fd5b95`), which has no cliff overflow.
- **Rust port:** follows ae2a6de with `Kernel::fixes.fillet_cliff_overflow` (the session's `DoFilleting` passes the subtraction's error on, `blend/fillet_executive/session/finish.rs`): `NoRoom` with the step's own comment and `NoRoomReason`, `NotExact`, or `BadMerge`.
Tests: `crates/solidcore/tests/wr631_cliff_overflow_construction.rs` (`an_overflow_the_construction_cannot_cut_reports_its_own_error`).