mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 04:52:10 +00:00
Merge pull request #2302 from brett19/fix/2223-section-end-crease-trim
fix: a round's tangent-continuation section end is trimmed at its creases, opt-in (#2223)
This commit is contained in:
@@ -49,6 +49,7 @@
|
||||
//! | [`continued_surface`], [`ContinuedSurface`] | `sm_ContinuedSurface` (`db4a655`) |
|
||||
//! | [`close_band`] | `sm_CloseBand` (`db4a655`) |
|
||||
//! | [`close_section_ended_round`] | none (WR-674, not the fork's) |
|
||||
//! | [`section_end`], [`section_end_face`], [`section_edge`] | none (WR-674, WR-706, not the fork's) |
|
||||
//!
|
||||
//! Deviations:
|
||||
//! - **Errors.** `GetFilletErrorInfo()->NoteError(code, comment)` followed by `SM_ERR`
|
||||
@@ -2003,42 +2004,132 @@ pub fn close_section_ended_round(
|
||||
|
||||
// a flat face at each section
|
||||
for (end, section) in ends.iter().zip([s0, s1]) {
|
||||
let (a, b) = edge::start_end_vertices(region, section)?;
|
||||
let a = vertex::point(region, a.ok_or_else(|| SolidError::null_pointer(OP))?)?;
|
||||
let b = vertex::point(region, b.ok_or_else(|| SolidError::null_pointer(OP))?)?;
|
||||
let mut lp: Vec<LoopCurve> = Vec::new();
|
||||
add_curve(k, &mut lp, edge_curve(k, region, section)?, a, b)?;
|
||||
for (from, to) in [(b, end.at), (end.at, a)] {
|
||||
if !same(from, to, on_tol) {
|
||||
add_curve(k, &mut lp, line(from, to), from, to)?;
|
||||
}
|
||||
}
|
||||
let [_, x, y] = end.normal.make_unit_ortho_vectors(None)?;
|
||||
let mut bbox: Option<Extent2d> = None;
|
||||
for c in &lp {
|
||||
let mut points = Vec::new();
|
||||
samples(k, &c.curve, 8, &mut points)?;
|
||||
for p in points {
|
||||
let d = p - end.at;
|
||||
let uv = Point2::new(d.dot(x), d.dot(y));
|
||||
match bbox.as_mut() {
|
||||
None => bbox = Some(Extent2d::from_points(uv, uv)),
|
||||
Some(bx) => bx.add_point(uv),
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some(bbox) = bbox else {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
};
|
||||
let margin = 0.01 * bbox.size().length() + on_tol;
|
||||
let domain = Extent2d::from_bounds(
|
||||
bbox.min().x - margin,
|
||||
bbox.min().y - margin,
|
||||
bbox.max().x + margin,
|
||||
bbox.max().y + margin,
|
||||
);
|
||||
let plane = Surface::from(Plane::new(end.at, x, y, Vec2::new(1.0, 1.0), &domain));
|
||||
face_of_loop(k, region, plane, lp)?;
|
||||
section_end_face(k, region, end, section, on_tol)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The section's flat face of a round cut off at `end` (WR-674, [`close_section_ended_round`];
|
||||
/// with `fixes.fillet_section_end_trim_at_creases`, WR-706, the trim's): on the section's
|
||||
/// plane, bounded by the lamina edge `section` of `region` (the round's section) and the lines
|
||||
/// from its ends to the end's vertex (a line shorter than `on_tol` left out), added to
|
||||
/// `region`.
|
||||
///
|
||||
/// # Errors
|
||||
/// Those of the samples and the face construction.
|
||||
pub fn section_end_face(
|
||||
k: &mut Kernel,
|
||||
region: &mut Brep,
|
||||
end: &SectionEnd,
|
||||
section: EdgeId,
|
||||
on_tol: f64,
|
||||
) -> Result<()> {
|
||||
let line = |from: Point3, to: Point3| Curve::from(Line::from_points(from, to, 3));
|
||||
let (a, b) = edge::start_end_vertices(region, section)?;
|
||||
let a = vertex::point(region, a.ok_or_else(|| SolidError::null_pointer(OP))?)?;
|
||||
let b = vertex::point(region, b.ok_or_else(|| SolidError::null_pointer(OP))?)?;
|
||||
let mut lp: Vec<LoopCurve> = Vec::new();
|
||||
add_curve(k, &mut lp, edge_curve(k, region, section)?, a, b)?;
|
||||
for (from, to) in [(b, end.at), (end.at, a)] {
|
||||
if !same(from, to, on_tol) {
|
||||
add_curve(k, &mut lp, line(from, to), from, to)?;
|
||||
}
|
||||
}
|
||||
let [_, x, y] = end.normal.make_unit_ortho_vectors(None)?;
|
||||
let mut bbox: Option<Extent2d> = None;
|
||||
for c in &lp {
|
||||
let mut points = Vec::new();
|
||||
samples(k, &c.curve, 8, &mut points)?;
|
||||
for p in points {
|
||||
let d = p - end.at;
|
||||
let uv = Point2::new(d.dot(x), d.dot(y));
|
||||
match bbox.as_mut() {
|
||||
None => bbox = Some(Extent2d::from_points(uv, uv)),
|
||||
Some(bx) => bx.add_point(uv),
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some(bbox) = bbox else {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
};
|
||||
let margin = 0.01 * bbox.size().length() + on_tol;
|
||||
let domain = Extent2d::from_bounds(
|
||||
bbox.min().x - margin,
|
||||
bbox.min().y - margin,
|
||||
bbox.max().x + margin,
|
||||
bbox.max().y + margin,
|
||||
);
|
||||
let plane = Surface::from(Plane::new(end.at, x, y, Vec2::new(1.0, 1.0), &domain));
|
||||
face_of_loop(k, region, plane, lp)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The section end of the round on `fil_edge` (of `target`) at its vertex `v` (WR-674): the
|
||||
/// plane through the vertex normal to the filleted edge there; `None` where the edge has no
|
||||
/// tangent at the vertex.
|
||||
///
|
||||
/// # Errors
|
||||
/// Those of the evaluation.
|
||||
pub fn section_end(
|
||||
k: &Kernel,
|
||||
target: &Brep,
|
||||
fil_edge: EdgeId,
|
||||
v: VertexId,
|
||||
) -> Result<Option<SectionEnd>> {
|
||||
let vertex_point = vertex::point(target, v)?;
|
||||
let interval = edge::interval(target, fil_edge)?;
|
||||
let t = if edge::start_vertex(target, fil_edge)? == Some(v) {
|
||||
interval.min()
|
||||
} else {
|
||||
interval.max()
|
||||
};
|
||||
let derivs = edge::curve(target, fil_edge)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP))?
|
||||
.evaluate(k, t, 1, true, true)?;
|
||||
let tangent = derivs
|
||||
.get(1)
|
||||
.copied()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP))?;
|
||||
let length = tangent.length();
|
||||
if length <= EFF_ZERO {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(SectionEnd {
|
||||
vertex: v,
|
||||
at: vertex_point,
|
||||
normal: tangent * (1.0 / length),
|
||||
}))
|
||||
}
|
||||
|
||||
/// The lamina edge of `region` that lies on the plane of `end` (four samples within
|
||||
/// `on_tol`): the round's section there, once the round is cut by it. `None` when no lamina
|
||||
/// edge or more than one does (WR-706).
|
||||
///
|
||||
/// # Errors
|
||||
/// Those of the samples.
|
||||
pub fn section_edge(
|
||||
k: &Kernel,
|
||||
region: &Brep,
|
||||
end: &SectionEnd,
|
||||
on_tol: f64,
|
||||
) -> Result<Option<EdgeId>> {
|
||||
let mut found = None;
|
||||
for e in crate::topology::brep::p1::edges(region, None)? {
|
||||
if !edge::is_lamina(region, e)? {
|
||||
continue;
|
||||
}
|
||||
let curve = edge_curve(k, region, e)?;
|
||||
let mut points = Vec::new();
|
||||
samples(k, &curve, 4, &mut points)?;
|
||||
if points
|
||||
.iter()
|
||||
.all(|&p| on_section_plane(p, end.at, end.normal, on_tol))
|
||||
{
|
||||
if found.is_some() {
|
||||
return Ok(None);
|
||||
}
|
||||
found = Some(e);
|
||||
}
|
||||
}
|
||||
Ok(found)
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ use std::collections::BTreeMap;
|
||||
|
||||
use super::{invalid, FilletBody, FilletObject, FilletSession, SolverObject};
|
||||
use crate::blend::fillet_cliff_overflow::{
|
||||
close_removal_region, close_section_ended_round, cut_fillet_at_open_end, Blended, Refusal,
|
||||
SectionEnd,
|
||||
close_removal_region, close_section_ended_round, cut_fillet_at_open_end, section_end, Blended,
|
||||
Refusal,
|
||||
};
|
||||
use crate::blend::fillet_executive::p3::NoRoomReason;
|
||||
use crate::blend::fillet_geom::p1::{FilletErrorType, FilletGeomId, FilletSurfaceStorage};
|
||||
@@ -404,29 +404,10 @@ impl FilletSession<'_> {
|
||||
let vertex_point = vertex::point(target, v)?;
|
||||
let end_surface = if end.section {
|
||||
// WR-674: the section through the vertex, normal to the filleted edge
|
||||
let interval = edge::interval(target, fil_edge)?;
|
||||
let t = if edge::start_vertex(target, fil_edge)? == Some(v) {
|
||||
interval.min()
|
||||
} else {
|
||||
interval.max()
|
||||
};
|
||||
let derivs = edge::curve(target, fil_edge)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP))?
|
||||
.evaluate(k, t, 1, true, true)?;
|
||||
let tangent = derivs
|
||||
.get(1)
|
||||
.copied()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP))?;
|
||||
let length = tangent.length();
|
||||
if length <= EFF_ZERO {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
}
|
||||
let normal = tangent * (1.0 / length);
|
||||
section_ends.push(SectionEnd {
|
||||
vertex: v,
|
||||
at: vertex_point,
|
||||
normal,
|
||||
});
|
||||
let end =
|
||||
section_end(k, target, fil_edge, v)?.ok_or_else(|| SolidError::sm_err(OP))?;
|
||||
let normal = end.normal;
|
||||
section_ends.push(end);
|
||||
Surface::from(Plane::from_normal(vertex_point, normal))
|
||||
} else {
|
||||
face::p2::surface(target, end_face)?
|
||||
|
||||
@@ -654,11 +654,17 @@ impl FilletSession<'_> {
|
||||
// way, by subtraction
|
||||
// WR-695 (the fork's TrimAtCreases, smlib#79 step 6): with
|
||||
// `fillet_overflow_trim_at_creases` a cliff overflow is trimmed at its creases in
|
||||
// place instead (a WR-674 section end is still subtracted)
|
||||
if session.executive.cliff_overflow
|
||||
&& k.fixes.fillet_overflow_trim_at_creases
|
||||
&& !session.executive.open_ends.iter().any(|end| end.section)
|
||||
{
|
||||
// place instead; a WR-674 section end is still subtracted, unless
|
||||
// `fillet_section_end_trim_at_creases` (WR-706) trims it too
|
||||
let sectioned = session.executive.open_ends.iter().any(|end| end.section);
|
||||
let trimmed = if sectioned {
|
||||
k.fixes.fillet_section_end_trim_at_creases
|
||||
&& k.fixes.fillet_overflow_trim_at_creases
|
||||
&& k.fixes.fillet_round_ends_at_tangent_continuation
|
||||
} else {
|
||||
session.executive.cliff_overflow && k.fixes.fillet_overflow_trim_at_creases
|
||||
};
|
||||
if trimmed {
|
||||
let result = session.trim_at_creases(k);
|
||||
session.objects.target.set_editing_enabled(editing);
|
||||
if let Some(output) = error_out {
|
||||
|
||||
@@ -22,13 +22,18 @@
|
||||
//! edge and the extension, `NoteFitRadius` from `sm_CornerTakesPerRadius` after the
|
||||
//! whole edge; tangency-labs/smlib a3d6d81, 49f393d) are ported as refusals; their fit
|
||||
//! sizes are not (`FIT(#2278)` at each).
|
||||
//! - **Section ends.** A round with WR-674's section ends (not the fork's) is not trimmed
|
||||
//! here: the caller subtracts it, as before.
|
||||
//! - **Section ends (not the fork's).** A round with WR-674's section ends is trimmed here
|
||||
//! only with `fixes.fillet_section_end_trim_at_creases` (WR-706, #2223): each such end is
|
||||
//! cut by its section's plane and closed by the section's flat face, and a side of that
|
||||
//! face that lies along an edge of the body is sewn to the edge's piece the rail's
|
||||
//! imprint cuts off, not imprinted. Without it the caller subtracts the round, as before.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use super::{invalid, FilletBody, FilletObject, FilletSession, SessionObjects};
|
||||
use crate::blend::fillet_cliff_overflow::{cut_fillet_at_open_end, Refusal};
|
||||
use crate::blend::fillet_cliff_overflow::{
|
||||
cut_fillet_at_open_end, section_edge, section_end, section_end_face, Refusal, SectionEnd,
|
||||
};
|
||||
use crate::blend::fillet_executive::p3::NoRoomReason;
|
||||
use crate::blend::fillet_geom::p1::{FilletErrorType, FilletGeomId};
|
||||
use crate::blend::fillet_overflow::{
|
||||
@@ -43,6 +48,7 @@ use crate::geom::object::ObjectRef;
|
||||
use crate::kernel::Kernel;
|
||||
use crate::math::{Point3, BIG_DOUBLE};
|
||||
use crate::query::brep::BrepIdentity;
|
||||
use crate::surface::plane::Plane;
|
||||
use crate::surface::surface::Surface;
|
||||
use crate::topology::brep::p3::{self, delete_face, with_kernel_callbacks, DeleteFaceOptions};
|
||||
use crate::topology::brep_cache::CacheObject;
|
||||
@@ -214,6 +220,54 @@ impl FilletSession<'_> {
|
||||
})?
|
||||
}
|
||||
|
||||
/// WR-706: whether the lamina edge `e` of `sheet` is a side of a section's flat face
|
||||
/// (`section_ends`) that runs from the end's vertex along an edge of the target at that
|
||||
/// vertex (its samples on the section's plane and its middle on the edge, within ten
|
||||
/// times `tol`): its two end points if so.
|
||||
fn seam_side(
|
||||
&self,
|
||||
k: &Kernel,
|
||||
sheet: &Brep,
|
||||
e: EdgeId,
|
||||
section_ends: &[SectionEnd],
|
||||
tol: f64,
|
||||
) -> Result<Option<(Point3, Point3)>> {
|
||||
let on_tol = 10.0 * tol;
|
||||
let (sv, ev) = (
|
||||
edge::start_vertex(sheet, e)?.ok_or_else(|| SolidError::null_pointer(OP))?,
|
||||
edge::end_vertex(sheet, e)?.ok_or_else(|| SolidError::null_pointer(OP))?,
|
||||
);
|
||||
let (a, b) = (vertex::point(sheet, sv)?, vertex::point(sheet, ev)?);
|
||||
let curve = edge_curve(k, sheet, e)?;
|
||||
let ivl = curve.natural_interval();
|
||||
let mid = curve.evaluate_point(k, ivl.evaluate(0.5))?;
|
||||
let target = &*self.objects.target;
|
||||
for end in section_ends {
|
||||
if a.distance(end.at) > on_tol && b.distance(end.at) > on_tol {
|
||||
continue;
|
||||
}
|
||||
let on_plane = |p: Point3| (p - end.at).dot(end.normal).abs() <= on_tol;
|
||||
if !(on_plane(a) && on_plane(b) && on_plane(mid)) {
|
||||
continue;
|
||||
}
|
||||
for te in crate::topology::brep::p1::edges(target, None)? {
|
||||
if edge::start_vertex(target, te)? != Some(end.vertex)
|
||||
&& edge::end_vertex(target, te)? != Some(end.vertex)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let t_curve =
|
||||
edge::curve(target, te)?.ok_or_else(|| SolidError::null_pointer(OP))?;
|
||||
if let Some((_, dist)) = drop_on(k, t_curve, &edge::interval(target, te)?, mid)? {
|
||||
if dist <= on_tol {
|
||||
return Ok(Some((a, b)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// The fork's `SmFilletExecutive::TrimAtCreases` (WR-695): in a cliff overflow, trim
|
||||
/// the blends along their creases with the faces beyond the cliffs and sew them into
|
||||
/// the target: the blends built on their faces' continuations, the sharp edges moved
|
||||
@@ -280,7 +334,11 @@ impl FilletSession<'_> {
|
||||
faces_of.insert(solver, self.fillet_faces(solver)?);
|
||||
}
|
||||
let open_ends = self.executive.open_ends.clone();
|
||||
if open_ends.iter().any(|end| end.section) {
|
||||
// WR-706: a section end (WR-674) is trimmed here only with its own fix
|
||||
let section_trim = k.fixes.fillet_section_end_trim_at_creases
|
||||
&& k.fixes.fillet_overflow_trim_at_creases
|
||||
&& k.fixes.fillet_round_ends_at_tangent_continuation;
|
||||
if !section_trim && open_ends.iter().any(|end| end.section) {
|
||||
return Err(invalid("a section end is not trimmed at its creases"));
|
||||
}
|
||||
|
||||
@@ -294,6 +352,7 @@ impl FilletSession<'_> {
|
||||
// (an end's event: the end face cuts the blend whatever lies past it, a lip over a
|
||||
// wall's top)
|
||||
let mut note = None;
|
||||
let mut section_ends: Vec<SectionEnd> = Vec::new();
|
||||
for end in &open_ends {
|
||||
let FilletObject::Solver(id) = end.solver else {
|
||||
return Err(invalid("open end solver is not a solver"));
|
||||
@@ -316,9 +375,18 @@ impl FilletSession<'_> {
|
||||
.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 end_surface = face::p2::surface(target, end_face)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP))?
|
||||
.clone_for_evaluation()?;
|
||||
let end_surface = if end.section {
|
||||
// WR-706: a section end (WR-674) is cut by the section through the vertex,
|
||||
// normal to the filleted edge
|
||||
let section =
|
||||
section_end(k, target, fil_edge, v)?.ok_or_else(|| SolidError::sm_err(OP))?;
|
||||
section_ends.push(section);
|
||||
Surface::from(Plane::from_normal(vertex_point, section.normal))
|
||||
} else {
|
||||
face::p2::surface(target, end_face)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP))?
|
||||
.clone_for_evaluation()?
|
||||
};
|
||||
let entry = faces_of
|
||||
.get_mut(&end.solver)
|
||||
.ok_or_else(|| SolidError::null_pointer(OP))?;
|
||||
@@ -339,6 +407,27 @@ impl FilletSession<'_> {
|
||||
self.noted(note.take(), cut)?;
|
||||
}
|
||||
|
||||
// WR-706: each section end closed by its flat face, from the round's section to the
|
||||
// vertex (WR-674's, as the subtraction closes it)
|
||||
if !section_ends.is_empty() {
|
||||
let mut tol = 0.0;
|
||||
for &a in &approx {
|
||||
tol = smos_max(tol, a);
|
||||
}
|
||||
for end in §ion_ends {
|
||||
let Some(section) = section_edge(k, &sheet, end, 10.0 * tol)? else {
|
||||
self.objects.intermediate = Some(sheet);
|
||||
self.note_refusal(refusal(
|
||||
FilletErrorType::NoRoom,
|
||||
Some(NoRoomReason::TangentContinuationEndUnsupported),
|
||||
"Fillet Error: a round ending where its edge runs on tangentially has no single section there",
|
||||
));
|
||||
return Err(SolidError::sm_err(OP));
|
||||
};
|
||||
section_end_face(k, &mut sheet, end, section, 10.0 * tol)?;
|
||||
}
|
||||
}
|
||||
|
||||
// the target is trimmed in place; a failed trim puts it back as it was
|
||||
let kept = (*self.objects.target).clone();
|
||||
let trimmed = self.trim_sheet_into(
|
||||
@@ -349,6 +438,7 @@ impl FilletSession<'_> {
|
||||
&blended_edges,
|
||||
&approx,
|
||||
tangency,
|
||||
§ion_ends,
|
||||
&mut note,
|
||||
);
|
||||
if let Some(refused) = note.take() {
|
||||
@@ -372,6 +462,7 @@ impl FilletSession<'_> {
|
||||
blended_edges: &[EdgeId],
|
||||
approx: &[f64],
|
||||
tangency: f64,
|
||||
section_ends: &[SectionEnd],
|
||||
note: &mut Option<Refusal>,
|
||||
) -> Result<()> {
|
||||
let refuse = |note: &mut Option<Refusal>, r: Refusal| -> Result<()> {
|
||||
@@ -566,6 +657,21 @@ impl FilletSession<'_> {
|
||||
lamina_edges.push(e);
|
||||
}
|
||||
}
|
||||
// WR-706: a side of a section's flat face that runs from the end's vertex along an
|
||||
// edge of the body (the seam where the corner's cylinder runs on tangentially into
|
||||
// the flat side) is sewn to that edge's piece, not imprinted on its faces
|
||||
let mut seam_sides: Vec<(Point3, Point3)> = Vec::new();
|
||||
if !section_ends.is_empty() {
|
||||
let mut kept_lamina = Vec::new();
|
||||
for &e in &lamina_edges {
|
||||
let side = self.seam_side(k, &sheet, e, section_ends, tol)?;
|
||||
match side {
|
||||
Some(ends) => seam_sides.push(ends),
|
||||
None => kept_lamina.push(e),
|
||||
}
|
||||
}
|
||||
lamina_edges = kept_lamina;
|
||||
}
|
||||
let mut extensions: Vec<Extension> = Vec::new();
|
||||
let mut along: Vec<Curve> = Vec::new();
|
||||
for &f in &target_faces {
|
||||
@@ -628,6 +734,35 @@ impl FilletSession<'_> {
|
||||
self.with_target_env(k, |k, b, env, _| merge_curves(k, b, env, f, tol, on, None))?;
|
||||
imprinted.extend(on_edges);
|
||||
}
|
||||
// WR-706: the pieces of the body's edges a section's side lies along, cut off at
|
||||
// the rails' ends by the imprint
|
||||
for &(a, b) in &seam_sides {
|
||||
let target = &*self.objects.target;
|
||||
let mut found = false;
|
||||
for e in crate::topology::brep::p1::edges(target, None)? {
|
||||
let (sv, ev) = (
|
||||
edge::start_vertex(target, e)?.ok_or_else(|| SolidError::null_pointer(OP))?,
|
||||
edge::end_vertex(target, e)?.ok_or_else(|| SolidError::null_pointer(OP))?,
|
||||
);
|
||||
let (s_pt, e_pt) = (vertex::point(target, sv)?, vertex::point(target, ev)?);
|
||||
if (s_pt.distance(a) <= on_tol && e_pt.distance(b) <= on_tol)
|
||||
|| (s_pt.distance(b) <= on_tol && e_pt.distance(a) <= on_tol)
|
||||
{
|
||||
imprinted.insert(e);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return refuse(
|
||||
note,
|
||||
refusal(
|
||||
FilletErrorType::BadMerge,
|
||||
None,
|
||||
"Fillet Error: a round's section end lies along an edge of the body that its rail does not cut",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// the faces round the blended edges, up to the imprint and the extensions' edges
|
||||
// along the body: the material the blends take off
|
||||
|
||||
@@ -1392,6 +1392,8 @@ pub struct UpstreamFixes {
|
||||
pub fillet_gn_section_generator: bool,
|
||||
/// With `fillet_cliff_overflow` (and its prerequisites), a blend past its faces is trimmed at its creases where it is built instead of being closed into the region it removes and subtracted (the fork's `SmFilletExecutive::TrimAtCreases`, `SmFilletOverflow.cpp`, tangency-labs/smlib#79 step 6 at 5e8c9c1, which deletes `SubtractRemovalRegion`): the open ends are cut by their end faces' planes as before; each sheet face is sectioned by every body face near it that holds none of its boundary, exactly by a plane, and split along each crease where it lies inside both faces; a piece is kept only when its inner point lies inside the body; a planar support face whose kept rail runs past its boundary into the material is carried on along its plane to the rail; the kept sheet's boundary is imprinted on the body's faces, the faces it encloses round the blended edges are deleted, the sheet is merged in and stitched, the body oriented, and the edges between faces sewn in on one surface removed. The blend keeps its radius and the sharp edge moves. Refused with `FilletErrorType::NoRoom`: a crease on a face that runs on tangentially from a face the blend rolls on (`NoRoomReason::CreaseAcrossTangentEdge`), a blend whose corners take its whole edge (`CornersTakeWholeEdge`), and a rail into the material that the support face cannot be carried on to (`RailRunsPastFaceIntoMaterial`); `BadMerge` for a sheet that does not sew into a manifold solid. Off: the subtraction, as before (WR-695, #2273).
|
||||
pub fillet_overflow_trim_at_creases: bool,
|
||||
/// With both `fillet_overflow_trim_at_creases` and `fillet_round_ends_at_tangent_continuation`, a round with a section end (WR-674: its filleted edge runs on tangentially at the vertex) is trimmed at its creases and sewn into the body as a cliff overflow is, instead of being closed into the region it removes and subtracted: each section end is cut by its plane through the vertex normal to the filleted edge and given its flat end face (the section's corner, from the section's ends to the vertex), and the sheet is trimmed and sewn in as `fillet_overflow_trim_at_creases` does it; a side of an end face that lies along an edge of the body (the seam where a corner's cylinder runs on tangentially into a flat side) is sewn to that edge's piece instead of being imprinted. So a round with a section end is built together with other rounds (an R25 rim and an R2 corner arc of a tabbed plate rounded together, which the subtraction refuses with `FilletErrorType::NoRoom`: it builds only a lone round both of whose ends are section ends). Off: the subtraction, as before (WR-706, #2223; own design, not in the fork, which at 5e8c9c1 leaves such a corner arc unrounded).
|
||||
pub fillet_section_end_trim_at_creases: bool,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
//! tangency-labs/solidcore#2223: the R25 rim and an R2 corner arc of a tabbed plate rounded
|
||||
//! r 0.5 together (two edges apart, sharing no vertex), WR-706
|
||||
//! (`UpstreamFixes::fillet_section_end_trim_at_creases`).
|
||||
//!
|
||||
//! The plate is #2176's: an R25 disc with a tab (sides x = +-4, R2 corners, top y = 280/9 + 2),
|
||||
//! extruded 2. Each end of the corner arc runs on tangentially into a straight top edge that
|
||||
//! is not picked, so WR-674 (`fillet_round_ends_at_tangent_continuation`) ends that round at
|
||||
//! its section through the vertex. The subtraction that builds WR-674's section ends builds
|
||||
//! only a lone round both of whose ends are section ends, so with the rim picked too the
|
||||
//! fillet was refused (`FilletErrorType::NoRoom`, 1001 in `cliff_overflow`). With WR-706 the
|
||||
//! section-ended round is trimmed at its creases and sewn in with the rim's round
|
||||
//! (`fillet_overflow_trim_at_creases`, WR-695).
|
||||
//!
|
||||
//! Expected values are closed form, not the fork's: at tangency-labs/smlib 5e8c9c1 the fork
|
||||
//! leaves the corner arc unrounded (rim alone, corner alone and both remove 7.95867347,
|
||||
//! -3.3e-9 and 7.95867350), so SMLib is known to differ here.
|
||||
//!
|
||||
//! **The corner arc's wedge (Pappus).** In each half-plane through the corner's vertical axis
|
||||
//! (radius R = 2) the round removes the corner of the r x r square at the filleted edge
|
||||
//! outside the ball's circle: the square less the quarter disc about the square's far corner,
|
||||
//! of area A = r^2 - (pi/4) r^2 = (1 - pi/4) r^2. Its centroid lies off each wall by
|
||||
//! c = (r^2 (r/2) - (pi r^2/4)(r - 4r/(3 pi))) / A = r (10 - 3 pi) / (3 (4 - pi)), so at
|
||||
//! rho = R - c from the axis. The two section planes contain the axis, so the removed solid
|
||||
//! is A revolved through theta = pi/2, and by Pappus V = A theta rho
|
||||
//! = (1 - pi/4) r^2 (pi/2) (2 - c) = 0.159135839721916.
|
||||
//!
|
||||
//! **Both.** The rim's round ends on the tab's flat sides at y = sqrt(609) = 24.68, the corner
|
||||
//! arc's round starts at y = 280/9 = 31.11, so the two removed regions are disjoint and the
|
||||
//! fillet removes the rim's 7.958674274526857 (the rim alone, as #2176's test pins it) plus
|
||||
//! the wedge: 8.117810114248773.
|
||||
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::panic,
|
||||
clippy::indexing_slicing
|
||||
)] // test helpers
|
||||
|
||||
use solidcore::blend::fillet_executive::api::fillet_edges;
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::error::SolidError;
|
||||
use solidcore::geom::attribute::{AttributeStore, ObjectKey};
|
||||
use solidcore::kernel::{Kernel, KernelOptions, UpstreamFixes};
|
||||
use solidcore::mass::integrator::properties::{brep_volume, PropertyAttributes};
|
||||
use solidcore::math::{Point3, Vec3};
|
||||
use solidcore::primitive::api_curves::{create_arc, create_line_segment};
|
||||
use solidcore::query::api_queries::edge_closest_point;
|
||||
use solidcore::sweep::api_brep::create_linear_sweep;
|
||||
use solidcore::topology::brep::p2::is_manifold_solid;
|
||||
use solidcore::topology::brep::validation::{check_brep, CheckLevel};
|
||||
use solidcore::topology::{Brep, EdgeId};
|
||||
use std::f64::consts::PI;
|
||||
|
||||
const RADIUS: f64 = 0.5;
|
||||
/// A point on the top rim arc.
|
||||
const RIM: [f64; 3] = [0.0, -25.0, 2.0];
|
||||
/// A point on the R2 corner arc at x > 0.
|
||||
const CORNER: [f64; 3] = [3.414_213_56, 32.525_324_7, 2.0];
|
||||
/// What the rim's round removes alone (#2176's `the_neighbours_keep_their_rounds`).
|
||||
const RIM_REMOVED: f64 = 7.958_674_274_526_857;
|
||||
|
||||
/// Every opt-in fix on (as in the issue), with WR-706 as given.
|
||||
fn kernel(wr706: bool) -> Kernel {
|
||||
let mut k = Kernel::new(&KernelOptions::default());
|
||||
let mut f = UpstreamFixes::default();
|
||||
for &name in UpstreamFixes::NAMES {
|
||||
*f.flag_mut(name).unwrap() = true;
|
||||
}
|
||||
f.fillet_section_end_trim_at_creases = wr706;
|
||||
k.fixes = f;
|
||||
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-8, &attrs).unwrap()
|
||||
}
|
||||
|
||||
/// The edge nearest `p`.
|
||||
fn pick(k: &Kernel, b: &Brep, p: [f64; 3]) -> EdgeId {
|
||||
let q = Point3::new(p[0], p[1], p[2]);
|
||||
let mut best = (f64::INFINITY, None);
|
||||
for &e in b.edge_list() {
|
||||
if let Ok(c) = edge_closest_point(k, b, e, q) {
|
||||
if c.closest.distance < best.0 {
|
||||
best = (c.closest.distance, Some(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
best.1.unwrap()
|
||||
}
|
||||
|
||||
fn line(a: (f64, f64), b: (f64, f64)) -> Curve {
|
||||
Curve::from(create_line_segment(Vec3::new(a.0, a.1, 0.0), Vec3::new(b.0, b.1, 0.0)).unwrap())
|
||||
}
|
||||
|
||||
fn arc(c: (f64, f64), r: f64, from: f64, to: f64) -> Curve {
|
||||
Curve::from(
|
||||
create_arc(
|
||||
Vec3::new(c.0, c.1, 0.0),
|
||||
r,
|
||||
from.to_degrees(),
|
||||
to.to_degrees(),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
/// R25 disc with a tab (sides x = +-4, R2 corners, top y = 280/9 + 2), 2 thick.
|
||||
fn plate(k: &mut Kernel) -> Brep {
|
||||
let t = (4.0_f64 / 25.0).acos();
|
||||
let y = (625.0_f64 - 16.0).sqrt();
|
||||
let cy = 280.0 / 9.0;
|
||||
let curves = [
|
||||
arc((0.0, 0.0), 25.0, PI - t - 2.0 * PI, t),
|
||||
line((4.0, y), (4.0, cy)),
|
||||
arc((2.0, cy), 2.0, 0.0, PI / 2.0),
|
||||
line((2.0, cy + 2.0), (-2.0, cy + 2.0)),
|
||||
arc((-2.0, cy), 2.0, PI / 2.0, PI),
|
||||
line((-4.0, cy), (-4.0, y)),
|
||||
];
|
||||
let refs: Vec<Option<&Curve>> = curves.iter().map(Some).collect();
|
||||
create_linear_sweep(k, &refs, Vec3::new(0.0, 0.0, 1.0), 2.0, true).unwrap()
|
||||
}
|
||||
|
||||
/// The corner arc's wedge, by Pappus (the file header).
|
||||
fn pappus() -> f64 {
|
||||
let c = RADIUS * (10.0 - 3.0 * PI) / (3.0 * (4.0 - PI));
|
||||
(1.0 - PI / 4.0) * RADIUS * RADIUS * (PI / 2.0) * (2.0 - c)
|
||||
}
|
||||
|
||||
/// The fillet of the edges nearest `picks`: the volume it removes and the body's face count,
|
||||
/// the body checked sound.
|
||||
fn removed(wr706: bool, picks: &[[f64; 3]]) -> Result<(f64, usize), SolidError> {
|
||||
let mut k = kernel(wr706);
|
||||
let mut b = plate(&mut k);
|
||||
let before = volume(&k, &mut b);
|
||||
let es: Vec<EdgeId> = picks.iter().map(|p| pick(&k, &b, *p)).collect();
|
||||
fillet_edges(&mut k, &mut b, &es, RADIUS, 1, 1, 1.0)?;
|
||||
b.refresh_cached_topology();
|
||||
assert!(is_manifold_solid(&b).unwrap());
|
||||
let check = check_brep(&mut k, &mut b, CheckLevel::Level2).unwrap();
|
||||
assert!(check.is_valid(), "{:?}", check.lines());
|
||||
Ok((before - volume(&k, &mut b), b.face_list().len()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_pappus_wedge_is_the_closed_form() {
|
||||
assert!(
|
||||
(pappus() - 0.159_135_839_721_916).abs() <= 1e-15,
|
||||
"{}",
|
||||
pappus()
|
||||
);
|
||||
}
|
||||
|
||||
/// The issue's case: both rounds built, the rim's volume plus the corner arc's wedge.
|
||||
#[test]
|
||||
fn rim_and_corner_arc_round_together() {
|
||||
let (both, faces) = removed(true, &[RIM, CORNER]).unwrap();
|
||||
let expected = RIM_REMOVED + pappus();
|
||||
assert!(
|
||||
(both - expected).abs() <= 1e-7,
|
||||
"removed {both}, rim + wedge {expected}"
|
||||
);
|
||||
// the plate's 8 faces, the rim's round, the corner arc's round and its two flat ends
|
||||
assert_eq!(faces, 12);
|
||||
}
|
||||
|
||||
/// The two edges picked the other way round build the same body.
|
||||
#[test]
|
||||
fn corner_arc_and_rim_round_together() {
|
||||
let (both, faces) = removed(true, &[CORNER, RIM]).unwrap();
|
||||
assert!(
|
||||
(both - RIM_REMOVED - pappus()).abs() <= 1e-7,
|
||||
"removed {both}"
|
||||
);
|
||||
assert_eq!(faces, 12);
|
||||
}
|
||||
|
||||
/// Each edge alone, through the trim, removes what it removes by the subtraction.
|
||||
#[test]
|
||||
fn each_edge_alone_is_unchanged() {
|
||||
let (rim, faces) = removed(true, &[RIM]).unwrap();
|
||||
assert!((rim - RIM_REMOVED).abs() <= 1e-9, "rim {rim}");
|
||||
assert_eq!(faces, 9);
|
||||
let (corner, faces) = removed(true, &[CORNER]).unwrap();
|
||||
assert!(
|
||||
(corner - pappus()).abs() <= 1e-7,
|
||||
"corner {corner}, Pappus {}",
|
||||
pappus()
|
||||
);
|
||||
// the plate's 8 faces, the round and its two flat ends
|
||||
assert_eq!(faces, 11);
|
||||
}
|
||||
|
||||
/// Without WR-706 the subtraction refuses the round with a section end next to another
|
||||
/// round (the issue's 1001), and still builds the corner arc alone.
|
||||
#[test]
|
||||
fn without_the_fix_the_subtraction_refuses_both() {
|
||||
let err = removed(false, &[RIM, CORNER]).unwrap_err();
|
||||
assert!(
|
||||
matches!(err, SolidError::Upstream { code: 1001, .. }),
|
||||
"{err:?}"
|
||||
);
|
||||
let (corner, faces) = removed(false, &[CORNER]).unwrap();
|
||||
assert!((corner - pappus()).abs() <= 1e-7, "corner {corner}");
|
||||
assert_eq!(faces, 11);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#### WR-706. A round ended at its sections (WR-674) is built only alone
|
||||
- **Severity:** Wrong result (refusal)
|
||||
- **Area:** Blend (fillet executive)
|
||||
- **Location:** not upstream: solidcore's WR-674 (`fixes.fillet_round_ends_at_tangent_continuation`, #2185), whose section-ended round is closed into the region it removes and subtracted (`close_section_ended_round`, `crates/solidcore/src/blend/fillet_cliff_overflow.rs`; `subtract_removal_region`, `crates/solidcore/src/blend/fillet_executive/session/cliff_overflow.rs`).
|
||||
- **What happens:** the subtraction builds only one round both of whose ends are section ends. Any other selection with a section end is refused with `FilletErrorType::NoRoom` (`NoRoomReason::TangentContinuationEndUnsupported`; `fillet_edges` returns `Upstream { code: 1001 }`), even when the other rounds are far away.
|
||||
- **Trigger:** #2223: an R25 disc with a tab (sides x = +-4, R2 corners, top y = 280/9 + 2), extruded 2, rounded r 0.5 on the top rim arc and the tab's R2 corner arc at x > 0 together, with every `UpstreamFixes` flag on. Each edge rounds alone (7.958674274526857 and 0.15913583445853874 removed).
|
||||
- **How found:** SolidCraft (`fillet_tabbed_plate_outer_loop_and_partial_selection_after_hole`), reported in #2223.
|
||||
- **Confirmed:** in solidcore (the regression test below). Not reachable in the pinned C++ (`3fd5b95`), which fails the corner arc alone (`SM_FILERR_VERTEX_PROBLEM`, 1001). The fork at tangency-labs/smlib 5e8c9c1 (local build, `SmApiFilletEdges` r 0.5) leaves the corner arc unrounded: rim alone 7.9586734663816969 removed, corner arc alone -3.3e-9, both 7.9586734967351731 (10 faces). So the fork is known to differ here, and the fix is checked against the closed form.
|
||||
- **Rust port:** `Kernel::fixes.fillet_section_end_trim_at_creases`, effective only with both `fillet_overflow_trim_at_creases` (WR-695) and `fillet_round_ends_at_tangent_continuation` (WR-674); own design, not in the fork. A round with a section end goes to the crease trim (`FilletSession::trim_at_creases`) instead of the subtraction: each section end is cut by the section's plane and closed by the section's flat face (`section_end_face`, the face the subtraction builds), and a side of that face that lies along an edge of the body (the vertical seam where the corner's cylinder runs on tangentially into the flat side) is sewn to that edge's piece, which the rail's imprint cuts off, instead of being imprinted. Off: the subtraction, as before.
|
||||
|
||||
Tests: `crates/solidcore/tests/issue_2223_section_end_trim_at_creases.rs`: the rim and corner arc together (either pick order) remove the rim's 7.958674274526857 plus the corner arc's Pappus wedge (1 - pi/4) r^2 (pi/2) (2 - c), c = r (10 - 3 pi) / (3 (4 - pi)), = 0.159135839721916, to 1e-7 (8.117810109458333 against 8.117810114248773), 12 faces, `check_brep` Level2 valid; each edge alone unchanged (9 and 11 faces); with the flag off the pair is still refused with 1001.
|
||||
Reference in New Issue
Block a user