solidcore: a curvature fillet is the equal-area Gⁿ section; mixed wedges refused

Blend::Curvature now builds the capi's section, as fork 6148773's capi
does: SM_FSG_BLEND_CURVE of order 2 at the equal-area thumbweight,
passed by solidcore's name GN_THUMBWEIGHT_EQUAL_AREA (solidcore#2274
moves its value from -2 to 2), with no blend-curve-at-thirds setting.
The sync-2 refusal (e9ac0e606) comes off.

One call resolves the equal-area thumbweight once, from the first
blend's wedge, and every edge takes it, in the fork as in solidcore: an
edge of another wedge then takes off the wrong area, depending on the
edges' order (17% short, 21% over;
solidcore#2250 issuecomment-6563419897). A curvature fillet over edges
whose faces meet at different angles (at each edge's midpoint, within
1e-6 rad) is refused as Error::Unported citing that comment; the call
is not split per edge.

Against closed forms on the rig, solidcore within 1e-9 of SMLib
d01d7b0: 862 7845.48670 (7845.48668), 2708 11791.330160 (11791.330159),
every-box-edge-r3 and section box-edge-r3 23922.743338, boss-root-r2
26056.152951, pipe-on-plate-r2 40302.590788 (completes, #2264),
hole-top-rim-r1 15207.56176, crease-r2 13963.15756, l-bracket 7538.62833,
peg front 9383.62724, all within 3e-9 relative. The G2 corner cases
pocket-corner and the bar corner's three edges (two variants) build and
fail continuity G1 at one seam, as on SMLib; the oblique three-edge
corner is refused for its mixed wedges. The kernel's and engine's
curvature tests now pass.

a_curvature_round_takes_its_wedges_equal_area_and_mixed_wedges_are_refused
holds each wedge's r² (cot(α/2) - (π - α)/2) to 1e-6 on a chamfered box
(135° and 90°), both orders refused, two 90° edges built together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdcgEu4Lr3moCCisdZShKz
This commit is contained in:
brett19
2026-11-01 18:32:46 -07:00
parent 3b4adeb73b
commit f9fc681670
2 changed files with 76 additions and 19 deletions
+45 -8
View File
@@ -28,6 +28,7 @@ use solidcore::blend::fillet_executive::p1::{FilletLaw, FilletLawKind};
use solidcore::blend::fillet_executive::p3::NoRoomReason;
use solidcore::blend::fillet_executive::session::FilletSession;
use solidcore::blend::fillet_geom::p1::{FilletCornerType, FilletErrorType};
use solidcore::blend::fillet_section::section::GN_THUMBWEIGHT_EQUAL_AREA;
use solidcore::blend::fillet_solver::p1::FilletSurfaceGeneratorType;
use solidcore::geom::attribute::{Attribute, AttributeHandlers, Behavior, ids as attribute_ids};
use solidcore::geom::vector3d::{Point3, Vec3};
@@ -235,6 +236,10 @@ fn chamfer_radius(k: &Kernel, b: &mut Brep, e: EdgeId, distance: f64) -> solidco
#[derive(Clone, Copy, PartialEq)]
enum Section<'a> {
Round,
/// The curvature-continuous section (`SM_FSG_BLEND_CURVE`): the Gⁿ generator's order 2 at the
/// equal-area thumbweight (`SMLIB_BLEND_THUMBWEIGHT`, fork 6148773; solidcore#2261 under
/// `fillet_gn_section_generator`), which takes off what the ball's circle does.
Curvature,
Chord,
/// A chamfer, with each edge's offset of its second surface (`chamfer_offsets`' `second`;
/// the sizes are the first's).
@@ -340,6 +345,11 @@ fn blend_edges_stopping(
Some(&size) => s.prepare_variable_radius_edges(k, edges, size, law, 1, 1),
None => return Ok(Err(Err(Error::Argument))),
},
// The equal-area thumbweight by solidcore's name, never its value: solidcore#2274 moves
// it from -2 to 2, after which -2 is an ordinary weight.
(Section::Curvature, _) => {
s.prepare_per_edge_constant_radius(k, edges, sizes, &[FilletSurfaceGeneratorType::BlendCurve], 2, GN_THUMBWEIGHT_EQUAL_AREA)
}
(Section::Chord, _) => s.prepare_per_edge_constant_distance(k, edges, sizes, &[FilletSurfaceGeneratorType::Circular], 1, 1.0),
(Section::Chamfer(second), _) => chamfer_parameters(k, &mut s, edges, sizes, second),
};
@@ -652,13 +662,36 @@ fn untagged(mut b: Brep) -> Out<Brep> {
Ok(b)
}
/// Why [`Blend::Curvature`] is refused. The capi's curvature section is the Gⁿ generator's order 2
/// at the equal-area thumbweight (`SMLIB_BLEND_THUMBWEIGHT`, fork 6148773), which solidcore does
/// not have yet; the blend-curve quintic it does have takes off another area, so it is not built
/// in its place. The refusal comes off when the solidcore pin carries #2260 and 6148773 and its
/// `fillet_gn_section_generator` flag is on.
pub(crate) const CURVATURE_SECTION: &str = "a curvature-continuous fillet on solidcore: the curvature section's equal-area Gⁿ \
generator is not in solidcore yet (solidcore#2250, port in progress as #2260)";
/// Why a curvature fillet over edges of different wedge angles is refused. The equal-area
/// thumbweight is resolved once per call, from the first blend's wedge, and every edge takes it
/// (in the fork as in solidcore#2261): an edge of another wedge takes off another area than its
/// own, and which one depends on the edges' order.
pub(crate) const CURVATURE_MIXED_WEDGES: &str = "a curvature-continuous fillet on solidcore over edges whose faces meet at different \
angles: every edge takes the equal-area thumbweight of the first one's angle, so the others take off the wrong area \
(https://github.com/tangency-labs/solidcore/issues/2250#issuecomment-6563419897)";
/// How far apart, in radians, two edges' wedge angles may be and still share one equal-area
/// thumbweight.
const WEDGE_TOLERANCE: f64 = 1e-6;
/// Whether the faces of every edge in `edges` meet at the same angle at its midpoint (the wedge the
/// equal-area thumbweight is resolved from), within [`WEDGE_TOLERANCE`]. An edge whose angle can't
/// be read counts as another wedge.
fn one_wedge(k: &Kernel, b: &mut Brep, edges: &[EdgeId]) -> solidcore::Result<bool> {
let mut first: Option<f64> = None;
for &e in edges {
let Some(u0) = edge::blend_edgeuse(k, b, e)? else { return Ok(false) };
let Some(u1) = eu_p1::radial(b, u0)? else { return Ok(false) };
let (Some((n0, _)), Some((n1, _))) = (face_frame(k, b, u0)?, face_frame(k, b, u1)?) else { return Ok(false) };
let angle = n0.dot(n1).clamp(-1.0, 1.0).acos();
match first {
None => first = Some(angle),
Some(a) if (a - angle).abs() > WEDGE_TOLERANCE => return Ok(false),
Some(_) => {}
}
}
Ok(true)
}
/// [`blend_holes`] without its holes.
#[allow(clippy::too_many_arguments)] // the capi's blend_call
@@ -751,7 +784,11 @@ fn blend_call(
let solid_in = brep_p2::is_manifold_solid(&w)?;
let section = match kind {
Blend::Round => Section::Round,
Blend::Curvature => return Ok(Err(Error::Unported(CURVATURE_SECTION))),
// Every edge of one call takes the equal-area thumbweight of the first's wedge
// (solidcore#2250's comment 6563419897): edges of other wedges would take off another
// area than their own, so they are not built together.
Blend::Curvature if !one_wedge(k, &mut w, &picked)? => return Ok(Err(Error::Unported(CURVATURE_MIXED_WEDGES))),
Blend::Curvature => Section::Curvature,
Blend::Chord => Section::Chord,
// `chamfer_edges`: each edge's offsets from its setback at its midpoint.
Blend::Chamfer => {
+31 -11
View File
@@ -663,19 +663,39 @@ fn a_setback_corner_left_open_returns_its_hole() {
assert_eq!(refused, Some(Error::CornerRefused { corner: 0 }));
}
/// A curvature-continuous round is refused on solidcore, whose only curvature section is the
/// blend-curve quintic (113/756 r² off a square corner, not the equal-area (1 - π/4) r²), until
/// its pin has the Gⁿ section generator (solidcore#2250).
/// A curvature-continuous round at the equal-area thumbweight takes off what the ball's circle
/// does, r² (cot(α/2) - (π - α)/2) per unit length on a wedge α (solidcore#2261): on a 40 x 30 x 20
/// box with its top back edge chamfered 5, r 2 along the chamfer's top edge (α 135°) takes
/// 0.0860579227 per mm, along the bottom front edge (α 90°) 0.8584073464. Both in one call are
/// refused: every edge takes the first one's thumbweight, so the other would take 17% too little
/// or 21% too much (solidcore#2250's comment 6563419897), whichever order they come in.
#[test]
fn a_curvature_round_is_refused_until_solidcore_has_the_equal_area_section() {
let b = BLOCK.core();
let r = blend(&b, Blend::Curvature, &[TOP_FRONT], &[2.0], TOL, &[], &[], Corners::Filled).err();
assert_eq!(r, Some(Error::Unported(super::blend::CURVATURE_SECTION)));
assert!(r.is_some_and(|e| e.to_string().contains("solidcore#2250")));
let set_back = blend(&b, Blend::Curvature, &[TOP_FRONT], &[2.0], TOL, &[[0.0, 0.0, 10.0]], &[3.0], Corners::Filled).err();
assert_eq!(set_back, Some(Error::Unported(super::blend::CURVATURE_SECTION)));
fn a_curvature_round_takes_its_wedges_equal_area_and_mixed_wedges_are_refused() {
let r = 2.0;
let cut = |alpha: f64| r * r * (1.0 / (alpha / 2.0).tan() - (PI - alpha) / 2.0);
let chamfered =
blend(&Shape::Box([0.0; 3], [40.0, 30.0, 20.0]).core(), Blend::Chamfer, &[[20.0, 30.0, 20.0]], &[5.0], TOL, &[], &[], Corners::Filled)
.unwrap();
let v0 = volume(&chamfered, 1e-8).unwrap();
assert!(rel(v0, 23500.0) < 1e-9, "{v0}");
let (a, b) = ([20.0, 25.0, 20.0], [20.0, 0.0, 0.0]);
for (at, alpha) in [(a, 0.75 * PI), (b, 0.5 * PI)] {
let round = blend(&chamfered, Blend::Curvature, &[at], &[r], TOL, &[], &[], Corners::Filled).unwrap();
let (rec, _) = native(&to_arrays(&round).unwrap()).unwrap();
rec.validate().unwrap();
let removed = v0 - volume(&round, 1e-8).unwrap();
assert!(rel(removed, 40.0 * cut(alpha)) < 1e-6, "{removed} vs {}", 40.0 * cut(alpha));
}
for both in [[a, b], [b, a]] {
let refused = blend(&chamfered, Blend::Curvature, &both, &[r], TOL, &[], &[], Corners::Filled).err();
assert_eq!(refused, Some(Error::Unported(super::blend::CURVATURE_MIXED_WEDGES)));
assert!(refused.is_some_and(|e| e.to_string().contains("issuecomment-6563419897")));
}
// Edges of one wedge go together.
let two = blend(&chamfered, Blend::Curvature, &[b, [20.0, 0.0, 20.0]], &[r], TOL, &[], &[], Corners::Filled).unwrap();
assert!(rel(v0 - volume(&two, 1e-8).unwrap(), 80.0 * cut(0.5 * PI)) < 1e-6);
// A pick that misses is still named first.
assert_eq!(blend(&b, Blend::Curvature, &[[90.0; 3]], &[2.0], TOL, &[], &[], Corners::Filled).err(), Some(Error::NoMatch { point: 0 }));
assert_eq!(blend(&chamfered, Blend::Curvature, &[[90.0; 3]], &[r], TOL, &[], &[], Corners::Filled).err(), Some(Error::NoMatch { point: 0 }));
}
/// A variable round is the union of its balls (solidcore#2262, `fillet_gn_section_generator`).