From f9fc6816705354eecc34779ddbefaed940bdad22 Mon Sep 17 00:00:00 2001 From: brett19 Date: Sun, 1 Nov 2026 18:32:46 -0700 Subject: [PATCH] =?UTF-8?q?solidcore:=20a=20curvature=20fillet=20is=20the?= =?UTF-8?q?=20equal-area=20G=E2=81=BF=20section;=20mixed=20wedges=20refuse?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01SdcgEu4Lr3moCCisdZShKz --- crates/smlib/src/core/blend.rs | 53 +++++++++++++++++++++++----- crates/smlib/src/core/blend_tests.rs | 42 ++++++++++++++++------ 2 files changed, 76 insertions(+), 19 deletions(-) diff --git a/crates/smlib/src/core/blend.rs b/crates/smlib/src/core/blend.rs index a5294e35ea..f6a1e5035a 100644 --- a/crates/smlib/src/core/blend.rs +++ b/crates/smlib/src/core/blend.rs @@ -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 { 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 { + let mut first: Option = 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 => { diff --git a/crates/smlib/src/core/blend_tests.rs b/crates/smlib/src/core/blend_tests.rs index b7abee6334..dade9f8168 100644 --- a/crates/smlib/src/core/blend_tests.rs +++ b/crates/smlib/src/core/blend_tests.rs @@ -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`).