Compare commits

...
Author SHA1 Message Date
bherbruckandClaude Opus 5.5 2b4489f52b Update the solidcore kernel to main 99ed57c4d4 for rc.3
The pin and its API changes, nine retired flags dropped, chamfers through
prepare_chamfers_along at the sizes asked, the whole-body section join, the
test and list updates this pin needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 09:34:22 -05:00
17 changed files with 201 additions and 343 deletions

No files matched your search

Generated
+3 -2
View File
@@ -4051,15 +4051,16 @@ dependencies = [
[[package]]
name = "solidcore"
version = "1.0.1"
source = "git+https://github.com/tangency-labs/solidcore?rev=8ddce901671f1cebd9214a31d21895d725e886c8#8ddce901671f1cebd9214a31d21895d725e886c8"
source = "git+https://github.com/tangency-labs/solidcore?rev=99ed57c4d4f4cd77d942ac6cd2e4d09f612acb20#99ed57c4d4f4cd77d942ac6cd2e4d09f612acb20"
dependencies = [
"serde",
"thiserror 2.0.21",
]
[[package]]
name = "solidcore-record"
version = "0.1.0"
source = "git+https://github.com/tangency-labs/solidcore?rev=f5e8e9a29e0c9b7cf6c4040a5bc8878382b3c62f#f5e8e9a29e0c9b7cf6c4040a5bc8878382b3c62f"
source = "git+https://github.com/tangency-labs/solidcore?rev=99ed57c4d4f4cd77d942ac6cd2e4d09f612acb20#99ed57c4d4f4cd77d942ac6cd2e4d09f612acb20"
dependencies = [
"serde",
"serde_json",
+2 -2
View File
@@ -36,10 +36,10 @@ solidcraft-3dx-client = { path = "crates/threedx-client" }
# move the rev on its own commit. For a
# local checkout, add `[patch."https://github.com/tangency-labs/solidcore"] solidcore = { path =
# "../solidcore/crates/solidcore" }` to a `.cargo/config.toml` outside the repository.
solidcore = { git = "https://github.com/tangency-labs/solidcore", rev = "8ddce901671f1cebd9214a31d21895d725e886c8" }
solidcore = { git = "https://github.com/tangency-labs/solidcore", rev = "99ed57c4d4f4cd77d942ac6cd2e4d09f612acb20" }
# Geometry as data (solidcore's layers §1, "Record"): the B-rep record, its structural validation
# and the native body file. Pinned like solidcore; for a local checkout, patch it as above.
solidcore-record = { git = "https://github.com/tangency-labs/solidcore", rev = "f5e8e9a29e0c9b7cf6c4040a5bc8878382b3c62f" }
solidcore-record = { git = "https://github.com/tangency-labs/solidcore", rev = "99ed57c4d4f4cd77d942ac6cd2e4d09f612acb20" }
# test tooling: a dev-dependency only (xtask/src/layers.rs)
solidcraft-testkit = { path = "crates/testkit" }
+2 -2
View File
@@ -318,7 +318,7 @@ fn a_model_saved_under_another_check_mode_is_evaluated_again() {
/// opened from its model or evaluated anew. Two edges of a 20 mm box rounded r2 (a mitre at the
/// corner (20, 0, 20)), saved; opened, the upright edge there rounded r3: the corner all three
/// rounded at once make (SMLib's setback corner, kernel `three_rounds_at_a_corner`), 15 faces and
/// 7928.142563938 mm³, the same body as the design evaluated anew.
/// 7927.72428251348 mm³, the same body as the design evaluated anew.
#[test]
fn a_later_round_after_opening_blends_the_saved_rounds_again() {
let mut built = Session::default();
@@ -336,7 +336,7 @@ fn a_later_round_after_opening_blends_the_saved_rounds_again() {
let b = &st.body("Box").unwrap().body;
let v = solidcraft_engine::kernel::measure(b).unwrap().volume;
assert_eq!(b.face_count(), 15);
assert!((v - 7928.142563938).abs() < 1e-6, "{v}");
assert!((v - 7927.72428251348).abs() < 1e-6, "{v}"); // solidcore#2382's setback corner
}
assert_eq!(model("later round", &cached), model("later round", &rebuilt));
let _ = std::fs::remove_file(&path);
+51 -1
View File
@@ -1627,7 +1627,57 @@ pub(crate) fn section(plane: &Plane, m: &Mesh, face: Option<u32>) -> Vec<LinkGeo
let tight = size * 1e-4;
let checkable = exact.len().saturating_mul(close.len()) <= 4_000_000;
let strays = |v: &[LinkGeom]| exact.iter().any(|p| !v.iter().any(|g| plain_distance(g, *p).is_none_or(|d| d <= tight)));
if plain(&close) && (close.len() <= lossy.len() || (checkable && strays(&lossy) && !strays(&close))) { close } else { lossy }
let fitted = if plain(&close) && (close.len() <= lossy.len() || (checkable && strays(&lossy) && !strays(&close))) { close } else { lossy };
join_rounds(fitted, size * 1e-5)
}
/// Where the fit left a line and an arc ends apart by at most `tol`, both end at one point. A
/// line can swallow an arc's first chord (a mesh crossing just past the tangent line), and
/// [`refine_round`] moves an arc's ends but not the lines': the outline would not close.
fn join_rounds(mut fitted: Vec<LinkGeom>, tol: f64) -> Vec<LinkGeom> {
for i in 0..fitted.len() {
let Some(&LinkGeom::Arc { c, mut a, mut b }) = fitted.get(i) else { continue };
let r = c.dist(a);
for end in [&mut a, &mut b] {
for curve in &mut fitted {
let LinkGeom::Line(p, q) = curve else { continue };
let (near, far) = if p.dist(*end) <= q.dist(*end) { (p, q) } else { (q, p) };
if near.dist(*end) <= tol {
let joint = line_meets_round(*far, *near, c, r, tol);
*near = joint;
*end = joint;
}
}
}
if let Some(arc) = fitted.get_mut(i) {
*arc = LinkGeom::Arc { c, a, b };
}
}
fitted
}
/// Where a line from `far` ending near `near` meets the circle (`c`, `r`): the tangent point when
/// the line runs into the circle there, within `tol` of it over the arc's first chord, else its
/// crossing nearest `near`, else the circle's point nearest `near`.
fn line_meets_round(far: Vec2, near: Vec2, c: Vec2, r: f64, tol: f64) -> Vec2 {
let nearest = |[x, y]: [Vec2; 2]| if x.dist(near) <= y.dist(near) { x } else { y };
let to_centre = c - far;
let span = to_centre.len();
if let Some(unit) = to_centre.normalized().filter(|_| span > r) {
let reach = (span * span - r * r).sqrt();
let (along, side) = (unit * (reach * reach / span), unit.perp() * (reach * r / span));
let tangent = nearest([far + along + side, far + along - side]);
if tangent.dist(near) <= tol + (2.0 * r * tol).sqrt() {
return tangent;
}
}
let Some(dir) = (near - far).normalized() else { return near };
let foot = far + dir * (c - far).dot(dir);
let half = r * r - (foot - c).len2();
if half >= 0.0 {
return nearest([foot + dir * half.sqrt(), foot - dir * half.sqrt()]);
}
(near - c).normalized().map_or(near, |unit| c + unit * r)
}
/// How far `p` lies from a line, arc or circle; None for any other curve.
+8 -9
View File
@@ -109,11 +109,10 @@ fn fillet_as_wide_as_a_face_rounds_it_off() {
/// The owner's ruling of 2026-10-30: two edges of a 20 mm box rounded r2 in one fillet (they
/// meet in a mitre at the corner (20, 0, 20), the upright edge there sharp), then the upright edge
/// r3 in a second fillet, whose tangent chain takes the mitre's seam with it. The corner is the
/// one solidcore 8ddce9016 builds for three rounds of different radii there: one patch, 10
/// faces, 7928.743041456134 mm³ (the fork's setback corner, 15 faces and 7928.142563938, waits
/// for solidcore#2310). The r2 rounds keep their names, and the r3 round and the corner's patch
/// are the second fillet's.
/// r3 in a second fillet, whose tangent chain takes the mitre's seam with it. The corner is
/// solidcore's setback corner for three rounds of different radii there (solidcore#2382): 15
/// faces, 7927.724282512234 mm³. The r2 rounds keep their names, and the r3 round and the
/// corner's six patches are the second fillet's.
#[test]
fn a_later_round_at_a_mitred_corner_is_blended_with_the_rounds_there() {
let mut s = Session::default();
@@ -135,17 +134,17 @@ fn a_later_round_at_a_mitred_corner_is_blended_with_the_rounds_there() {
let second = run(&mut s, "solid.fillet", json!({"edges": [[20, 0, 10]], "radius": 3}))["feature"].as_u64().unwrap();
assert!(s.model.results.iter().all(|r| r.error.is_none()), "{:?}", s.model.results);
let v = volume(&mut s);
assert!((v - 7928.743041456134).abs() < 1e-6, "{v}");
assert!((v - 7927.724282512234).abs() < 1e-6, "{v}");
let faces = run(&mut s, "model.faces", json!({"body": "Box"}))["faces"].as_array().cloned().unwrap_or_default();
assert_eq!(faces.len(), 10);
assert_eq!(faces.len(), 15);
let after: Vec<String> = rounds.iter().map(|p| named(&mut s, *p)).collect();
assert_eq!(after, before);
// The r3 round (halfway up, at 45°) and the corner's patches: the faces the second fillet made.
let k3 = 3.0 - 4.5_f64.sqrt();
assert!(named(&mut s, solidcore_record::Vec3::new(20.0 - k3, k3, 10.0)).starts_with(&format!("F{second}:blend:")));
let made = faces.iter().filter(|f| f["name"].as_str().is_some_and(|n| n.starts_with(&format!("F{second}:")))).count();
// the r3 round and the corner's patch
assert_eq!(made, 2, "{faces:?}");
// the r3 round and the corner's six patches
assert_eq!(made, 7, "{faces:?}");
}
#[test]
+31 -34
View File
@@ -6,7 +6,6 @@
//! fork fixed in its own sources; each one turned on here says which fork commit it matches.
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Mutex, PoisonError};
use solidcore::geom::attribute::{AttributeStore, ObjectKey};
@@ -108,7 +107,6 @@ pub(crate) fn fixes() -> UpstreamFixes {
f.offset_resolve_crossing_loops = true;
f.offset_walls_touch_refused = true;
f.offset_analytic_mate_edges = true;
f.precise_properties_split_at_surface_knots = true;
f.nurb_cylinder_inside_out_frame = true;
f.shell_inner_shell_of_statuses = true;
f.local_operation_offset_status = true;
@@ -147,13 +145,10 @@ pub(crate) fn fixes() -> UpstreamFixes {
f.torus_signed_gen_curve_interval = true;
f.sheet_sweep_joins_profile_wires = true;
f.point_solve_keeps_triangle_guess = true;
f.fillet_setback_from_start_vertex = true;
f.imprint_trims_edge_to_proto_vertex = true;
f.imprint_extends_edge_to_proto_vertex = true;
f.fillet_point_step_per_point = true;
f.analytic_copy_keeps_parameterization = true;
f.fillet_point_bigger_step_retry = true;
f.fillet_through_extended_rail_end = true;
f.precise_properties_body_length_scale = true;
f.offset_extension_from_base = true;
f.corner_blend_stays_inside_neighbours = true;
@@ -174,11 +169,9 @@ pub(crate) fn fixes() -> UpstreamFixes {
f.boolean_skips_faces_apart = true;
f.uv_trim_checks_on_surface = true;
f.loop_orientation_floor_from_loop_box = true;
f.offset_extends_cones_as_themselves = true;
f.local_operation_box_holds_new_surfaces = true;
f.uv_trim_joins_drop_pieces = true;
f.shell_edges_on_analytics_exact = true;
f.ssi_no_reverse_of_empty_trace = true;
f.ssi_pole_point_keeps_its_tangent = true;
f.iso_curve_sliver_domain_is_degenerate = true;
f.corner_blend_coons_patch_tangent_to_sides = true;
@@ -230,7 +223,6 @@ pub(crate) fn fixes() -> UpstreamFixes {
f.fillet_cleanup_removes_input_slits = true;
f.fillet_rail_end_on_continued_surface = true;
f.glue_vertices_joins_every_region = true;
f.fillet_nx1_tangent_side_edge_same_convexity = true;
f.fillet_removal_takes_ring_fillets = true;
// The fork's commits that reached `SMLIB_REV` at eaa998a (tangency-labs/smlib#68, #69, #70).
f.fillet_trim_inside_by_best_dot = true;
@@ -274,9 +266,7 @@ pub(crate) fn fixes() -> UpstreamFixes {
f.offset_keep_cavity_regions = true;
f.offset_four_face_vertex_split = true;
f.gap_samples_non_bspline_v_stride = true;
f.gap_array_append_updates_longest = true;
f.imprint_skips_collapsed_definition = true;
f.precise_properties_trim_base_v = true;
f.fillet_blend_curve_revolved_on_arc = true;
f.temporary_radius_setup_errors = true;
f.ellipse_step_interval_checks = true;
@@ -319,7 +309,6 @@ pub(crate) fn fixes() -> UpstreamFixes {
f.circle_silhouette_perspective_center = true;
f.ellipse_convert_to_2d_uses_both_axes = true;
f.circle_offset_antiparallel_normal = true;
f.masked_array_uses_bit_shift = true;
f.global_solver_identical_prefers_larger_when_maximizing = true;
f.surface_continuity_checks_last_v_knot = true;
f.surface_point_solver_propagates_bbox_failure = true;
@@ -331,7 +320,6 @@ pub(crate) fn fixes() -> UpstreamFixes {
f.crv_in_volume_mults_by_break_index = true;
f.curve_bounded_surface_read_uv_type_and_implicit_outer = true;
f.polar_box_region_to_arc_transposes_domain = true;
f.point_set_centroid_all_blocks = true;
f.local_solve1d_unbounded_newton_step = true;
f.composite_curve_bbox_segment_intervals = true;
f.knot_vector_valid_requires_span = true;
@@ -339,14 +327,12 @@ pub(crate) fn fixes() -> UpstreamFixes {
f.curve_property_gs_branch_bounds = true;
f.volume_representational_end_continuities = true;
f.integrator_propagates_failure = true;
f.precise_properties_span_end_from_below = true;
f.offset_open_edge_second_trim_point = true;
f.close_loop_relates_only_coincident_edges = true;
f.ssi_split_curves_at_singular_points = true;
f.surface_parallel_to_vector_checks = true;
f.fillet_round_ends_at_tangent_continuation = true;
f.geometric_g2_continuity = true;
f.hash_table_grow_preserves_removed_records = true;
f.bend_volume_bbox_contains_wide_angle_and_inf_u = true;
f.bend_volume_trim_param_box_across_period = true;
f.point_set_tight_pbox_projects_points = true;
@@ -377,7 +363,6 @@ pub(crate) fn fixes() -> UpstreamFixes {
f.fillet_section_end_trim_at_creases = true;
f.fillet_refuse_mixed_corner_crossing_rails = true;
f.fillet_keep_features = true;
f.fillet_blend_corner = true;
f.fillet_exact_arc_sections = true;
f.fillet_exact_arc_spans = true;
// Held back: see `HELD_BACK`.
@@ -385,11 +370,21 @@ pub(crate) fn fixes() -> UpstreamFixes {
f.cone_unbounded_parameterized_as_nurb = true;
f.ellipse_open_arc_keeps_end_angle = true;
f.fillet_round_as_definition = true;
f.fillet_round_on_round_as_definition = true;
f.fillet_overflow_crease_along_edge = true;
f.plane_first_torus_section_exact = true;
f.keep_exact_pcurves = true;
f.shell_one_construction = true;
f.approximate_surface_checks_every_row = true;
f.topological_vertex_has_only_edge_uses = true;
f.planar_section_face_tolerance = true;
f.face_swap_uv_transposes_trims = true;
f.closed_smooth_pole_tangent_limit = true;
// Held back: see `HELD_BACK`.
f.fillet_round_ends_on_outline = false;
f.fillet_setback_beside_curved_faces = true;
f.fillet_chamfer_corner = true;
f.brep_ray_classifies_current_trims = true;
f.revolution_frame_from_profile_origin = true;
f
}
@@ -399,29 +394,31 @@ pub(crate) fn fixes() -> UpstreamFixes {
/// the fork doesn't have stays on, and where solidcore builds what SMLib refuses, a result right
/// against its closed form is a win.
///
/// `fillet_refuse_rounds_breaking_through` (WR-707, solidcore#2311): sound geometry refused. With it
/// on, 11 rig cases that pass with it off, each graded against its closed form or invariants, are
/// refused as breaking through the part (190-fillet-disc-rim-past-its-2mm-wall fails status 1001
/// instead): every-hole-top-rim-r12-past-plate-bottom, 1080-chamfer-runs-into-openings-cut-by-them
/// (3350, its closed form), 1080-fillet-notched-block-top-outline-r8,
/// 324-fillet-over-hole-cut-by-overlong-cylinder, 324-fillet-over-hole-overlap,
/// 5158-fillet-adjacent-plate-edges-as-thick-as-plate and the sloped and equal-slope basin rims of
/// #1080 and #5835. A fix under the same flag is in progress on solidcore.
/// `fillet_refuse_rounds_breaking_through` (WR-707, solidcore#2311): sound geometry refused. At
/// solidcore 9856ffee5, with it on, the fillet rig's 1668-fillet-both-sides-of-thin-wall-r7 is
/// refused as breaking through the wall into the hollow behind it ("the largest radius that fits is
/// 6.828 mm"), where with it off it builds the closed form, 39509.938 (at 8ddce9016 it refused 11
/// rig cases that pass with it off; at this pin the other 270 fillet rig cases come out as with it
/// off).
///
/// `fillet_round_ends_on_outline` (WR-730, solidcore#2541): volumes wrong against their closed
/// forms, and rounds too big for their faces built instead of refused. With it on, each of these
/// passes with it off: `fillet_over_a_cliff` measures 721.832 against the closed form's 693.205;
/// `a_round_wider_than_its_faces_overflows_onto_its_tangent_sides` 6125.719 against 6107.529;
/// `taper_follows_the_room_along_the_edge` 4210.926 against 4195.579; the round larger than the
/// cube in `hostile_inputs_are_errors` builds (4773.168 against the 1094.853 its refusal leaves),
/// as do the fillets the CLI's `run_strict` and `json_out` tests need to fail; and
/// `a_corner_round_wider_than_its_faces_overflows_and_takes_the_corners_other_edges`,
/// `overflow_goes_first`, `corners_refuse_what_they_cannot_build` and
/// `fillet_edge_removed_by_a_larger_overflowing_round_is_noted` are refused (too large, no room,
/// status 1001), where with it off they build. Measured again at solidcore 9856ffee5: the same.
#[cfg(test)]
const HELD_BACK: &[&str] = &["fillet_refuse_rounds_breaking_through"];
const HELD_BACK: &[&str] = &["fillet_refuse_rounds_breaking_through", "fillet_round_ends_on_outline"];
/// How far apart two kernels' procedural blend serials start. A body keeps the serials of the
/// kernel that made it and may be used on any other, whose memo of solved stations is keyed by
/// serial alone: the ranges keep one definition's stations from answering for another's. A serial
/// only names a definition, so which range one comes from changes no answer.
pub(crate) const BLEND_SERIALS_PER_KERNEL: u64 = 1 << 40;
/// A new kernel. Each takes the next range of blend serials, the first starting at 0.
/// A new kernel, with this backend's fixes.
fn fresh() -> Kernel {
static MADE: AtomicU64 = AtomicU64::new(0);
let mut k = Kernel::new(&KernelOptions::default());
k.fixes = fixes();
k.blend_last_serial.set(MADE.fetch_add(1, Ordering::Relaxed).wrapping_mul(BLEND_SERIALS_PER_KERNEL));
k
}
+53 -102
View File
@@ -1,7 +1,7 @@
//! Fillets and chamfers on solidcore: the C API's composition round SMLib's fillet executive,
//! ported from the fork's `capi/src/smlib_ops.cpp` (`blend_call`, `round_edges`,
//! `chamfer_offsets`, `chamfer_edges`, `chamfer_radius`, `outer_faceuse`, `face_frame`,
//! `sides_offsets`, `blend_failure`, `setbacks_fit`, `overflow_first`, `Picked`,
//! `chamfer_edges`, `outer_faceuse`, `face_frame`,
//! `blend_failure`, `setbacks_fit`, `overflow_first`, `Picked`,
//! `size_ok`, `points_ok`, `tolerance_ok`, `law_ok`, `law_pinches`, `law_along`, `finish`,
//! `smlib_fillet`, `smlib_chamfer`, `smlib_blend`, `smlib_blend_dropping`, `smlib_blend_setback`,
//! `smlib_blend_setback_open`, `smlib_fillet_variable`, `smlib_chamfer_sides`) and
@@ -31,7 +31,9 @@ use solidcore::blend::fillet_executive::p3::{ErrorInfo, FitKind, NoRoomReason};
use solidcore::blend::fillet_executive::session::{FilletObject, FilletSession};
use solidcore::blend::fillet_geom::p1::{FilletCornerType, FilletErrorType};
use solidcore::blend::fillet_section::section::GN_THUMBWEIGHT_EQUAL_AREA;
use solidcore::blend::chamfer_side::faces_along;
use solidcore::blend::fillet_solver::p1::FilletSurfaceGeneratorType;
use solidcore::surface::procedural_blend::chamfer::ChamferSize;
use solidcore::geom::attribute::{Attribute, AttributeHandlers, Behavior, ids as attribute_ids};
use solidcore::geom::vector3d::{Point3, Vec3};
use solidcore::kernel::Kernel;
@@ -222,30 +224,7 @@ fn mid(b: &Brep, e: EdgeId) -> solidcore::Result<f64> {
Ok(b.edge(e)?.interval.evaluate(0.5))
}
/// `chamfer_radius`: the radius of the ball whose tangent points on the two faces of `e` lie
/// `distance` back from the edge at its midpoint, `distance * tan(a / 2)` with `a` the angle
/// between the directions into the faces; at a cusp (a knife edge) `distance` itself, the ball
/// whose contacts the chamfer joins. `None` where the capi's returns false.
fn chamfer_radius(k: &Kernel, b: &mut Brep, e: EdgeId, distance: f64) -> solidcore::Result<Option<f64>> {
let Some(u0) = edge::blend_edgeuse(k, b, e)? else { return Ok(None) };
let Some(u1) = eu_p1::radial(b, u0)? else { return Ok(None) };
let t = mid(b, e)?;
let options = BinormalOptions::default();
let Some(e0) = answer(evaluate_binormal(k, b, u0, t, false, &options))? else { return Ok(None) };
let Some(e1) = answer(evaluate_binormal(k, b, u1, t, false, &options))? else { return Ok(None) };
// Unitize's failure (a zero vector) is the capi's `return false`.
let (Ok(n0), Ok(b0), Ok(b1)) = (e0.face_normal.unitize(), e0.binormal.unitize(), e1.binormal.unitize()) else { return Ok(None) };
let c = b0.dot(b1).clamp(-1.0, 1.0);
// At a cusp (a knife edge: the faces tangent within the half degree `GetBlendEdgeuse` calls
// tangent, folded back on each other, the binormals the same way) a is 0 and no ball lies
// `distance` back along both: the chamfer is the chord between the contacts of the ball of
// radius `distance` (fork e96898d).
let cusp = c > 0.0 && b1.dot(n0).abs() < (0.5 * std::f64::consts::PI / 180.0).sin();
let radius = if cusp { distance } else { distance * ((1.0 - c) / (1.0 + c)).sqrt() };
Ok((radius.is_finite() && radius > 0.0).then_some(radius))
}
/// What `round_edges` makes: the capi's kinds less the chamfer, which `chamfer_offsets` makes.
/// What `round_edges` makes.
#[derive(Clone, Copy, PartialEq)]
enum Section<'a> {
/// A circular section of constant radius (or of a variable radius's law), the turns of two
@@ -256,9 +235,10 @@ enum Section<'a> {
/// `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).
Chamfer(&'a [f64]),
/// A chamfer of the sizes asked: one for all the edges or one per edge, and for each edge
/// the face its first size is along (empty: the blend edge use's). The kernel makes their
/// offsets, one pair for a tangent chain of one size.
Chamfer { sizes: &'a [ChamferSize], along: &'a [FaceId] },
}
/// The faces of `b`, for `made_faces`: a face whose id is new after the operation was made by
@@ -373,13 +353,16 @@ fn blend_failure_unsized(info: &ErrorInfo<FilletObject>, e: SolidError, corner:
}
/// `e` ([`Error::TooLarge`]) with the point at the middle of `edge` of `b`, the edge its size was
/// found for; an equal chamfer's size (its ball's radius, [`chamfer_radius`]) as its distance.
/// Where the edge or its point can't be read the size stays as it is, with no point (a chamfer's
/// size then is not a distance, so it is left a plain refusal).
fn sized_on(k: &Kernel, b: &mut Brep, edge: Option<EdgeId>, section: Section<'_>, e: Error) -> solidcore::Result<Error> {
/// found for; an equal chamfer's size (an offset, [`Section::Chamfer`]) as its distance, by the
/// edge's own ratio of the distance asked to the offset the kernel made of it (`chamfered`: per
/// edge of an equal chamfer, the two). Where the edge or its point can't be read the size stays
/// as it is, with no point (a chamfer's size then is not a distance, so it is left a plain
/// refusal).
fn sized_on(k: &Kernel, b: &mut Brep, edge: Option<EdgeId>, section: Section<'_>, chamfered: &[(EdgeId, f64, f64)], e: Error) -> solidcore::Result<Error> {
let Error::TooLarge { size, kind, why, .. } = e else { return Ok(e) };
let chamfer = matches!(section, Section::Chamfer { .. }) && kind == crate::FitKind::Radius;
let Some(edge) = edge else {
return Ok(if matches!(section, Section::Chamfer(_)) && kind == crate::FitKind::Radius { Error::NoRoom } else { e });
return Ok(if chamfer { Error::NoRoom } else { e });
};
let at = match b.edge(edge) {
Ok(rec) => match rec.curve.as_ref() {
@@ -388,11 +371,13 @@ fn sized_on(k: &Kernel, b: &mut Brep, edge: Option<EdgeId>, section: Section<'_>
},
Err(_) => None,
};
if matches!(section, Section::Chamfer(_)) && kind == crate::FitKind::Radius {
// The ball's radius is linear in the chamfer's distance along an edge.
return Ok(match chamfer_radius(k, b, edge, 1.0)? {
Some(per_mm) => Error::TooLarge { size: size / per_mm, kind: crate::FitKind::Distance, at, why },
None => Error::NoRoom,
if chamfer {
// The offset is linear in the distance along an edge.
return Ok(match chamfered.iter().find(|(e, ..)| *e == edge) {
Some(&(_, distance, offset)) if offset > 0.0 => {
Error::TooLarge { size: size / offset * distance, kind: crate::FitKind::Distance, at, why }
}
_ => Error::NoRoom,
});
}
Ok(Error::TooLarge { size, kind, at, why })
@@ -450,6 +435,8 @@ fn blend_edges_stopping(
let before = face_set(b)?;
// The edge a sized refusal names ([`blend_failure`]), read after the session lets `b` go.
let mut fit_edge = None;
// An equal chamfer's edges, each with its distance and the offset the kernel made of it.
let mut chamfered: Vec<(EdgeId, f64, f64)> = Vec::new();
let done = {
let mut s = FilletSession::new(k, b)?;
// `SetSweptTurns`: read as the corners are made (`fixes.fillet_blend_corner`).
@@ -471,12 +458,21 @@ fn blend_edges_stopping(
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),
(Section::Chamfer { sizes, along }, _) => s.prepare_chamfers_along(k, edges, sizes, along).map(|offsets| {
for (i, (&e, [r, _])) in edges.iter().zip(offsets).enumerate() {
let size = sizes.get(if sizes.len() == 1 { 0 } else { i });
if let Some(&ChamferSize::Distances([d, d2])) = size
&& d == d2
{
chamfered.push((e, d, r));
}
}
}),
};
// `AddSetBackVertex` for each set-back vertex, before the corners are made: a set-back
// vertex is no Ball (`IsSetBackVertex`, read by `ClassifyBall`), so its corner is the one
// the setback is set on.
if !matches!(section, Section::Chamfer(_)) {
if !matches!(section, Section::Chamfer { .. }) {
for sb in setbacks {
s.add_set_back_vertex(sb.vertex);
}
@@ -529,7 +525,7 @@ fn blend_edges_stopping(
Err(refusal) => {
b.refresh_cached_topology();
return Ok(Err(match refusal {
Err(e @ Error::TooLarge { .. }) => Err(sized_on(k, b, fit_edge, section, e)?),
Err(e @ Error::TooLarge { .. }) => Err(sized_on(k, b, fit_edge, section, &chamfered, e)?),
r => r,
}));
}
@@ -571,16 +567,6 @@ fn blend_edges_stopping(
Ok(Ok(()))
}
/// `chamfer_offsets`' `SetFilletParameters(b, es, kinds, solvers, first, NULL, -1, -1, -1, -1,
/// 1, 1.0, &second)` with every kind `SM_FSG_LINEAR` and every solver `SM_FS_CONST_RADIUS`, as the
/// fork's `SetFilletParameters` runs it (its `pOptSecondRadii`, fork f653ac9).
fn chamfer_parameters(k: &mut Kernel, s: &mut FilletSession<'_>, edges: &[EdgeId], first: &[f64], second: &[f64]) -> solidcore::Result<()> {
if edges.is_empty() {
return Ok(());
}
s.prepare_per_edge_constant_radius_with_second(k, edges, first, second, &[FilletSurfaceGeneratorType::Linear], 1, 1.0)
}
/// The corners' part of `round_edges` / `chamfer_offsets`: a chamfer's corners bevelled; each
/// setback set on its vertex's corner (`GetFilletCornerOfVertex`), which must be an
/// `SM_FCR_N_x_N_CONVEX` corner or a blend corner whose event is the setback (`SM_FCR_BLEND` with
@@ -592,7 +578,7 @@ fn corners(
setbacks: &[SetBack],
) -> solidcore::Result<std::result::Result<Vec<usize>, std::result::Result<usize, Error>>> {
let ids = s.fillet_corners()?;
if matches!(section, Section::Chamfer(_)) {
if matches!(section, Section::Chamfer { .. }) {
for id in ids {
s.set_corner_bevel(id, true)?;
}
@@ -936,16 +922,11 @@ fn blend_call(
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.
// `chamfer_edges`: the sizes asked; the kernel makes each edge's offsets.
Blend::Chamfer => {
let mut radii = Vec::with_capacity(picked.len());
for (&e, &d) in picked.iter().zip(&each) {
match chamfer_radius(k, &mut w, e, d)? {
Some(r) => radii.push(r),
None => return Ok(Err(Error::Status(INVALID_INPUT))),
}
}
return Ok(match blend_edges(k, &mut w, &picked, &radii, Section::Chamfer(&radii), None, &[], None)? {
let chamfers: Vec<ChamferSize> = each.iter().map(|&d| ChamferSize::Distances([d, d])).collect();
let section = Section::Chamfer { sizes: &chamfers, along: &[] };
return Ok(match blend_edges(k, &mut w, &picked, &[], section, None, &[], None)? {
Ok(()) => finish(w, solid_in)?,
Err(Ok(_)) => Err(Error::Unsupported),
Err(Err(e)) => Err(e),
@@ -1165,39 +1146,6 @@ fn face_frame(k: &Kernel, b: &mut Brep, u: EdgeuseId) -> solidcore::Result<Optio
Ok(Some((normal * (sign / len), at.binormal)))
}
/// `sides_offsets`: the surface offsets of `e` (its blend edge use's surface first) at which
/// `chamfer_offsets` cuts a chamfer `d1` back along the face whose outward normal at the edge's
/// midpoint leans most along `up`, and `second` back along the other (or at the angle `second`
/// to the first face). With b0, b1 the directions into the faces and θ the angle between them,
/// setbacks s0, s1 lie at offsets r0 = (s1 - s0 cos θ) / sin θ and r1 = (s0 - s1 cos θ) / sin θ;
/// an angle φ to the first face gives s1 = s0 sin φ / sin(θ + φ).
fn sides_offsets(k: &Kernel, b: &mut Brep, e: EdgeId, d1: f64, second: ChamferSecond, up: Vec3) -> solidcore::Result<Option<(f64, f64)>> {
let Some(u0) = edge::blend_edgeuse(k, b, e)? else { return Ok(None) };
let Some(u1) = eu_p1::radial(b, u0)? else { return Ok(None) };
let (Some((n0, b0)), Some((n1, b1))) = (face_frame(k, b, u0)?, face_frame(k, b, u1)?) else { return Ok(None) };
let c = b0.dot(b1).clamp(-1.0, 1.0);
let sn = (1.0 - c * c).max(0.0).sqrt();
if !(sn > 1e-6) {
return Ok(None);
}
let mut s0 = d1;
let mut s1 = match second {
ChamferSecond::Distance(d) => d,
ChamferSecond::Angle(phi) => {
let d = (c.acos() + phi).sin();
if !(d > 1e-9) {
return Ok(None);
}
d1 * phi.sin() / d
}
};
if n1.dot(up) > n0.dot(up) {
std::mem::swap(&mut s0, &mut s1);
}
let (r0, r1) = ((s1 - s0 * c) / sn, (s0 - s1 * c) / sn);
Ok((r0.is_finite() && r1.is_finite() && r0 > 0.0 && r1 > 0.0).then_some((r0, r1)))
}
/// `smlib_chamfer_sides`: the edges nearest `points` chamfered `distance` back along the face
/// whose outward normal leans most along `first`, and by `second` on the other, with bevelled
/// corners.
@@ -1219,17 +1167,20 @@ pub(crate) fn chamfer_sides(body: &Body, points: &[[f64; 3]], tolerance: f64, di
Err(m) => return Ok(Err(edge_miss(m))),
};
let solid_in = brep_p2::is_manifold_solid(&w)?;
let (mut r0, mut r1) = (Vec::with_capacity(picked.len()), Vec::with_capacity(picked.len()));
// The first size is along the face leaning most along `up`.
let mut along = Vec::with_capacity(picked.len());
for &e in &picked {
match sides_offsets(k, &mut w, e, distance, second, up)? {
Some((a, b)) => {
r0.push(a);
r1.push(b);
}
match answer(faces_along(k, &mut w, e, up))? {
Some([(f0, l0), (f1, l1)]) => along.push(if l1 > l0 { f1 } else { f0 }),
None => return Ok(Err(Error::Status(INVALID_INPUT))),
}
}
Ok(match blend_edges(k, &mut w, &picked, &r0, Section::Chamfer(&r1), None, &[], None)? {
let size = match second {
ChamferSecond::Distance(d) => ChamferSize::Distances([distance, d]),
ChamferSecond::Angle(angle) => ChamferSize::DistanceAngle { distance, angle },
};
let section = Section::Chamfer { sizes: std::slice::from_ref(&size), along: &along };
Ok(match blend_edges(k, &mut w, &picked, &[], section, None, &[], None)? {
Ok(()) => finish(w, solid_in)?,
Err(Ok(_)) => Err(Error::Unsupported),
Err(Err(e)) => Err(e),
+14 -11
View File
@@ -18,6 +18,8 @@
use solidcore::EntityRef;
use solidcore::SolidError;
use solidcore::error::offset::OffsetFailure;
use solidcore::error::{ErrorKind, Reason};
use solidcore::geom::attribute::{Attribute, Behavior, ids};
use solidcore::geom::axis2_placement::Axis2Placement;
use solidcore::geom::object::Shape;
@@ -29,7 +31,6 @@ use solidcore::nurbs::b_spline_curve::ControlPointForm;
use solidcore::nurbs::b_spline_surface::p1::SurfParamType;
use solidcore::nurbs::b_spline_surface_n_lib::p2::OrientType;
use solidcore::nurbs::knots::Side;
use solidcore::offset::construction::ShellRefusal;
use solidcore::offset::offset_executive::p2::{OffsetOptions, ShellOptions, local_operation_simple, offset_brep_simple, shell_brep_simple};
use solidcore::offset::offset_faces::{OffsetFacesExecutive, OffsetFacesRefusal};
use solidcore::query::api_queries::{face_bounding_box, face_classify_uv, face_closest_point_in_shape, face_internal_point};
@@ -50,21 +51,23 @@ type Out<T> = std::result::Result<T, Error>;
/// `SM_ERR_INVALID_INPUT`.
const INVALID_INPUT: i32 = 1007;
/// `SM_ERR_OFFSET_WALLS_TOUCH` (fork a759622), which `status` (smlib_shim.h) reports as
/// `SMLIB_ERR_WALLS_TOUCH`.
const OFFSET_WALLS_TOUCH: i32 = 1042;
/// `status` (smlib_shim.h): an offset whose walls touch is [`Error::WallsTouch`]; a vertex moved
/// past an edge's other end (`SM_ERR_OFFSET_EDGE_VANISHES`, fork 34318f4) is
/// `SMLIB_ERR_UNSUPPORTED`; a wall whose face's offset folds, which solidcore's one construction
/// refuses (`fixes.shell_one_construction`, solidcore#2367), is [`Error::WallFolds`] with the
/// largest wall it names; any other status as the blends pass it on.
/// largest wall it names; any other status as the blends pass it on. solidcore names an offset's
/// refusals by their kind and reason (its errors in its own terms, solidcore#2654), whichever
/// status the pinned C++ gives them.
fn status(e: SolidError) -> Error {
match e {
SolidError::Upstream { code: OFFSET_WALLS_TOUCH, .. } => Error::WallsTouch,
SolidError::ShellRefused { refusal: ShellRefusal::Fold, size, .. } => Error::WallFolds { largest: size },
e if e.is_offset_edge_vanishes() => Error::Unsupported,
e => blend_status(e),
if let SolidError::Failed(f) = &e
&& let Reason::Offset(OffsetFailure::Folds { largest, .. }) = f.reason
{
return Error::WallFolds { largest: Some(largest) };
}
match e.kind() {
ErrorKind::OffsetWallsTouch => Error::WallsTouch,
ErrorKind::OffsetEdgeVanishes => Error::Unsupported,
_ => blend_status(e),
}
}
+3 -4
View File
@@ -546,9 +546,8 @@ fn offset_faces_close_a_step_wall() {
/// Rig case shell/4492's rounded box: four mitred rounds, r 3, come off a 40 x 30 x 10 box's top
/// together and follow it. From height 0 the outline is the 34 x 24 top moved out by the round's
/// w(z) = sqrt(9 - (z - axis)^2), or 3 below the axis. With the fork's values at +19, and bodies
/// whose rounds go all the way down from -7 and are cut short from -7.5 (solidcore#2027). At -9
/// solidcore refuses to put the rounds back (solidcore#2020, open): SolidCraft says it does not
/// make that move.
/// whose rounds go all the way down from -7 and are cut short from -7.5 (solidcore#2027). At -9,
/// which solidcore refused before 9856ffee5 (solidcore#2020), it builds the closed form too.
#[test]
fn mitred_rounds_follow_a_moved_top() {
use super::blend::{Corners, blend};
@@ -574,6 +573,7 @@ fn mitred_rounds_follow_a_moved_top() {
(-7.0, [6, 12, 8]),
(-7.5, [6, 12, 8]),
(-8.0, [6, 12, 8]),
(-9.0, [6, 12, 8]),
(-9.5, [6, 12, 8]),
];
for (d, n) in moves {
@@ -582,7 +582,6 @@ fn mitred_rounds_follow_a_moved_top() {
assert_eq!(counts(&moved).unwrap(), n, "d {d}");
assert!(rel(v, analytic(d)) < 1e-6, "d {d}: {v} vs {}", analytic(d));
}
assert!(matches!(offset_faces(&rounded, &[top], 0.1, -9.0), Err(Error::Unsupported)));
}
/// `moved_faces`: drafts so small they leave a plane or cylinder where it was still moved a face
+23 -140
View File
@@ -24,21 +24,20 @@
use std::sync::PoisonError;
use solidcore::EntityRef;
use solidcore::curve::curve::Curve;
use solidcore::geom::extent2d::Extent2d;
use solidcore::geom::geom_utility::p2::ContinuityType;
use solidcore::geom::object::Shape;
use solidcore::geom::tol::USE_DEFAULT;
use solidcore::geom::vector2d::Point2;
use solidcore::geom::vector3d::{Point3, Vec3};
use solidcore::kernel::Kernel;
use solidcore::nurbs::b_spline_surface::p1::SurfParamType;
use solidcore::nurbs::b_spline_surface_n_lib::p2::EvaluateFlags;
use solidcore::nurbs::b_spline_surface_n_lib::p2::OrientType;
use solidcore::query::api_queries::{edge_closest_point, face_closest_point_in_shape, face_internal_point};
use solidcore::query::brep::{BrepQuery, LiveBrepObjects};
use solidcore::surface::cone::p1::Cone;
use solidcore::surface::extend::Continuity;
use solidcore::surface::side::BoxSide;
use solidcore::surface::surface::Surface;
use solidcore::surface::surface::SurfaceKind;
use solidcore::topology::brep::p1 as brep_p1;
@@ -54,6 +53,7 @@ use solidcore::topology::face::{p1 as face_p1, p2 as face_p2, p4 as face_p4};
use solidcore::topology::loopuse::{loop_edges, loop_loopuses, notify_ref};
use solidcore::topology::topology_traverser::Marks;
use solidcore::topology::{Brep, BrepId, EdgeId, FaceId, LoopId, edge};
use solidcore::{EntityRef, SolidError};
use super::blend::{p3, status};
use super::sweep::build_faces;
@@ -454,17 +454,17 @@ pub(crate) fn untrim_face(body: &Body, point: [f64; 3], tol: f64, holes: Option<
/// grows in u), and whether it is at the box's max.
#[derive(Clone, Copy)]
struct Side {
which: SurfParamType,
which: BoxSide,
u: bool,
high: bool,
}
/// `SIDES`.
const SIDES: [Side; 4] = [
Side { which: SurfParamType::UMin, u: true, high: false },
Side { which: SurfParamType::UMax, u: true, high: true },
Side { which: SurfParamType::VMin, u: false, high: false },
Side { which: SurfParamType::VMax, u: false, high: true },
Side { which: BoxSide::UMin, u: true, high: false },
Side { which: BoxSide::UMax, u: true, high: true },
Side { which: BoxSide::VMin, u: false, high: false },
Side { which: BoxSide::VMax, u: false, high: true },
];
/// `SAMPLES`.
@@ -546,55 +546,6 @@ fn side_edge(k: &Kernel, b: &Brep, surf: &Surface, dom: &Extent2d, s: Side, edge
e.filter(|_| d <= on)
}
/// `across`: how far `ext` (the face's surface extended past side `s` of its box `dom`, on the
/// same parameters) runs past that side, measured across it through the side's middle, by
/// 16-point Gauss-Legendre quadrature on 64 pieces. Negative when it can't be evaluated.
fn across(k: &Kernel, ext: &Surface, dom: &Extent2d, s: Side) -> f64 {
const X: [f64; 8] = [
0.0950125098376374,
0.2816035507792589,
0.4580167776572274,
0.6178762444026438,
0.755404408355003,
0.8656312023878318,
0.9445750230732326,
0.9894009349916499,
];
const W: [f64; 8] = [
0.1894506104550685,
0.1826034150449236,
0.1691565193950025,
0.1495959888165767,
0.1246289712555339,
0.0951585116824928,
0.0622535239386479,
0.0271524594117541,
];
const PIECES: usize = 64;
let Ok(bx) = ext.natural_uv_domain_with_nurb(k) else { return -1.0 };
let mid = on_side(dom, s, 0.5);
let from = if s.u { mid.x } else { mid.y };
let to = match (s.u, s.high) {
(true, true) => bx.max().x,
(true, false) => bx.min().x,
(false, true) => bx.max().y,
(false, false) => bx.min().y,
};
let h = (to - from) / PIECES as f64;
let mut length = 0.0;
for piece in 0..PIECES {
let c = from + (piece as f64 + 0.5) * h;
for i in 0..16 {
let (x, w) = if i < 8 { (-X[i], W[i]) } else { (X[i - 8], W[i - 8]) };
let t = c + 0.5 * h * x;
let uv = if s.u { Point2::new(t, mid.y) } else { Point2::new(mid.x, t) };
let Some((_, du, dv)) = eval(k, ext, uv) else { return -1.0 };
length += 0.5 * h.abs() * w * if s.u { du } else { dv }.length();
}
}
length
}
/// `cone_continued`: a cone's or cylinder's face continued naturally `distance` past side `s` of
/// its box `dom`, as the analytic itself over a wider STEP domain (degrees round the axis,
/// height along it). `None` where the cone can't be widened so (more than a turn, or past the
@@ -656,82 +607,16 @@ fn same_patch(k: &Kernel, p: &Surface, q: &Surface, tol: f64) -> bool {
}
/// `extended_by`: the surface `surf` (whose face is its whole box `dom`) extended past side `s` as
/// `ct` says, so that it runs `distance` past that side measured across it ([`across`]), within
/// 1e-9 of it. `CreateExtendedSurface` takes its distance as an estimate, and how far it then runs
/// is increasing in what it is asked: a bracketed solve on that (each ask scaled by the distance
/// over what it gave until two asks fall either side, then Illinois false position). An extension
/// that moves the face's side off its parameters, or a target not met, is [`Error::NotExact`],
/// never the surface as it came out.
fn extended_by(k: &Kernel, surf: &Surface, dom: &Extent2d, s: Side, ct: ContinuityType, distance: f64, model_tol: f64) -> Result<Surface> {
const BRACKET_TRIES: usize = 40;
const SOLVE_TRIES: usize = 100;
let tol = 1e-9 * distance;
// How far asking `ask` runs, and the surface it makes.
let try_ask = |ask: f64| -> Result<(Surface, f64)> {
let tried = surf.create_extended_surface_dir(k, s.which, ask, ct).map_err(status)?;
// The face's side stays where it was on the extended surface's parameters.
let mid = on_side(dom, s, 0.5);
let stays = matches!((point_at(k, surf, mid), point_at(k, &tried, mid)), (Some(was), Some(now)) if (was - now).length() <= model_tol);
if !stays {
return Err(Error::NotExact);
}
let got = across(k, &tried, dom, s);
if got.is_finite() && got > 0.0 { Ok((tried, got)) } else { Err(Error::NotExact) }
};
// Asks that run short of and past `distance`, by f = got - distance.
let (mut short, mut past) = (None, None);
let mut ask = distance;
for _ in 0..BRACKET_TRIES {
let (made, got) = try_ask(ask)?;
let f = got - distance;
if f.abs() <= tol {
return Ok(made);
}
if f < 0.0 {
short = Some((ask, f));
} else {
past = Some((ask, f));
}
if short.is_some() && past.is_some() {
break;
}
ask *= distance / got;
/// `how` says, so that it runs `distance` past that side measured across it, exactly: solidcore's
/// one extension ([`Surface::extended_by_distance`]), which keeps the source's own part bit for bit
/// and meets the distance within 1e-9 of it. A distance it cannot meet exactly is
/// [`Error::NotExact`], never a surface that falls short or runs past.
fn extended_by(k: &Kernel, surf: &Surface, dom: &Extent2d, s: Side, how: Continuity, distance: f64) -> Result<Surface> {
match surf.extended_by_distance(k, dom, s.which, how, distance) {
Ok(e) => Ok(e.surface),
Err(SolidError::ExtendRefused { .. } | SolidError::NotConverged { .. }) => Err(Error::NotExact),
Err(e) => Err(status(e)),
}
let (Some((mut lo, mut flo)), Some((mut hi, mut fhi))) = (short, past) else { return Err(Error::NotExact) };
// Illinois: false position, halving the value at the end that stays when the same end moves
// twice running.
#[derive(PartialEq)]
enum Moved {
Neither,
Lo,
Hi,
}
let mut moved = Moved::Neither;
for _ in 0..SOLVE_TRIES {
let x = hi - fhi * (hi - lo) / (fhi - flo);
if !(x > lo.min(hi) && x < lo.max(hi)) {
break;
}
let (made, got) = try_ask(x)?;
let f = got - distance;
if f.abs() <= tol {
return Ok(made);
}
if f < 0.0 {
(lo, flo) = (x, f);
if moved == Moved::Lo {
fhi *= 0.5;
}
moved = Moved::Lo;
} else {
(hi, fhi) = (x, f);
if moved == Moved::Hi {
flo *= 0.5;
}
moved = Moved::Hi;
}
}
Err(Error::NotExact)
}
/// `smlib_extend_sheet`: the one-face sheet `sheet`, whose face is its surface's whole parameter
@@ -787,15 +672,13 @@ pub(crate) fn extend_sheet(sheet: &Body, point: [f64; 3], tol: f64, distance: f6
if (nl.x - dl.x).abs() > ptol || (nl.y - dl.y).abs() > ptol || (nh.x - dh.x).abs() > ptol || (nh.y - dh.y).abs() > ptol {
return Ok(Err(Error::Unsupported));
}
let ct = match how {
Continuation::Tangent => ContinuityType::G1,
Continuation::Curvature => ContinuityType::G1G2,
Continuation::Natural => ContinuityType::CInfinity,
let continuity = match how {
Continuation::Tangent => Continuity::Tangent,
Continuation::Curvature => Continuity::Curvature,
Continuation::Natural => Continuity::Natural,
};
// CreateExtendedSurface takes its distance as an estimate (a natural parabola falls 2.5%
// short, a G2 run-off of a circle runs more than twice as far): the distance asked of it
// is solved for until the extension measures `distance` across the side's middle.
let extended = match extended_by(k, &surf, &dom, grow, ct, distance, model_tol) {
// The extension runs `distance` across the side, exactly (solidcore's one extension).
let extended = match extended_by(k, &surf, &dom, grow, continuity, distance) {
Ok(e) => e,
Err(e) => return Ok(Err(e)),
};
+1 -1
View File
@@ -55,7 +55,7 @@ pub enum Error {
/// middle of the edge it was found for, and why there is no room, when the refusal names them.
TooLarge { size: f64, kind: FitKind, at: Option<[f64; 3]>, why: Option<RoomReason> },
/// A shell, offset or thicken whose wall is thicker than a face curves: the face's offset
/// folds (solidcore's `ShellRefused`, `ShellRefusal::Fold`). The largest wall that works,
/// folds (solidcore's `OffsetFailure::Folds`). The largest wall that works,
/// when solidcore names one.
WallFolds { largest: Option<f64> },
/// SMLib cannot build this exactly yet (a variable radius whose law has a corner).
-13
View File
@@ -486,19 +486,6 @@ fn bodies_move_between_kernels_and_calls_run_side_by_side() {
}
}
/// Each kernel has its own range of blend serials: a body keeps the serials of the kernel that
/// made it, and another kernel's memo of solved stations must not take them for its own.
#[test]
fn kernels_never_issue_the_same_blend_serial() {
let range = core::BLEND_SERIALS_PER_KERNEL as usize;
let ranges = core::run("outer", |outer| {
let inner = core::run("inner", |inner| Ok(inner.blend_last_serial.get() as usize)).unwrap_or_default();
Ok([outer.blend_last_serial.get() as usize / range, inner / range])
});
let [outer, inner] = ranges.unwrap();
assert_ne!(outer, inner);
}
/// A panic in a call running beside another costs neither: the panicking call's kernel is thrown
/// away and the other call and later ones go on.
#[test]
+2 -10
View File
@@ -7,9 +7,8 @@
# (`gh run download <run> -R tangency-labs/solidcraft -n <artifact> -D <dir>/<artifact>` for junit
# and junit-gpu-1..4), run `cargo xtask known-failing --junit <dir>`, then drop each FIXED line and
# add each REGRESSION line with its cause, from the test's failure message.
solidcraft-cli::action_matrix::action_matrix_sample 3 baseline actions fail, the same 3 as on C++: 2 shells (Bad Loop Orient on issue-616-lid-ribs; status 1001 on filleted-shelled-box), 1 fillet (SmSurfOfRevolution Test #7 on oblique_cut_of_cylinder)
solidcraft-cli::model_cache::a_later_round_after_opening_blends_the_saved_rounds_again the later r3 round at the r2 mitre builds solidcore's 10-face corner (7928.743041), the test wants SMLib f916a8d's setback corner (smlib#104), which solidcore ports in solidcore#2310: 15 faces, 7928.142563938
solidcraft-cli::naming_corpus::corpus_designs_name_alike_every_build_and_fail_where_a_reference_is_gone oracle/83-boundary-loop-thicken does not load: step 8, thicken status 1001, as on C++
solidcraft-cli::action_matrix::action_matrix_sample 2 baseline actions fail: 2 shells (Bad Loop Orient on issue-616-lid-ribs; status 1001 on filleted-shelled-box)
solidcraft-cli::naming_corpus::corpus_designs_name_alike_every_build_and_fail_where_a_reference_is_gone oracle/83-boundary-loop-thicken does not load: step 8, the offset of face 0 by -1 is not proved regular, so thicken is refused with no size named
solidcraft-doc::eval::cost_tests::a_tap_marking_searches_only_the_bodies_near_it main's mesh-based version (#10303) not taken; the exact replacement is to come
solidcraft-doc::eval::cost_tests::a_tap_marking_still_searches_the_bodies_within_its_reach main's mesh-based version (#10303) not taken; the exact replacement is to come
solidcraft-engine::cmd::edit::tests::inserting_variable_fillet_control_preserves_parameters_at_existing_positions solidcore refuses a variable fillet whose law has a corner (solidcore#430)
@@ -21,19 +20,12 @@ solidcraft-engine::measure_centre_tests::a_fillet_round_a_boss_reports_its_radiu
solidcraft-engine::tests::chord_and_variable_fillets its 2 → 4 → 2 waist: solidcore refuses a variable fillet whose law has a corner (solidcore#430)
solidcraft-engine::tests::fillet_fit_taper_closes_to_a_point fillet status 1001 on the taper fit (radius 16 closing to 0), as on C++
solidcraft-engine::tests::mirror_placed::dragging_a_mirrored_part_meshes_its_copies_once a copy of a copy drawn from its first copy's triangles (#10101) is triangulated differently from a fresh mesh of it, where truck's meshes are the same under a rigid move, as on SMLib
solidcraft-engine::tests::shell_tangent_chain_that_opens_every_face_explains_how_to_keep_walls the 2 mm shell of the box with every edge rounded r 1.5, its front face open, fails SM_ERR_INVALID_INPUT at solidcore 8ddce9016: the wall is thicker than the rounds and their Ball corners (BallPatch, fillet_blend_corner), whose inward offsets collapse
solidcraft-kernel::blend::corner_tests::a_round_ending_in_an_earlier_round_matches_both_together fillet status 1001 on the round that ends in the earlier one, as on C++
solidcraft-kernel::blend::corner_tests::corner_blends_hold_at_any_scale at 0.01x the arc case's rounds remove 72.783 (scaled back) against 72.713 at 1x, as on C++
solidcraft-kernel::blend_corner::tests::issue2422_vertical_after_separate_bottom_rounds the vertical edge after the separate bottom rounds is refused as smooth ("its faces meet tangentially, so there is nothing to round"): the rounds leave it tangent at solidcore 8ddce9016
solidcraft-kernel::blend_corner::tests::three_rounds_at_a_corner the corner's variable-radius transition between rounds of different radii meets its neighbours at 0.144 degrees once its sections are the balls' touch circles (fillet_gn_section_generator, solidcore#2262; 0 with the great circles), as on SMLib d01d7b0; the corner step replaces that transition
solidcraft-kernel::blend_variable::tests::variable_radius_on_a_box_edge the variable round r 2 to 5 along a box edge fails status 1001 at solidcore 8ddce9016
solidcraft-kernel::blend_variable::tests::variable_radius_with_a_corner_through_a_middle_point solidcore refuses a variable fillet whose law has a corner (solidcore#430)
solidcraft-kernel::blend_wrong_way_tests::a_chamfer_that_would_grow_the_part_on_a_convex_edge_is_refused at solidcore 8ddce9016 the 0.5 chamfer of the ball cut's rim is no longer refused as too large: the session builds no face (made no change), its tangent chain running into an unblended edge
solidcraft-kernel::corner_g2::tests::a_long_setback_keeps_the_g2_patch_inside_the_box at set backs 11 to 15 the G2 corner patch bulges past the box, as on C++
solidcraft-kernel::corner_g2::tests::curvature_rounds_meet_at_a_g2_corner the corner between curvature rounds jumps 2.07 in curvature over 538 stations on solidcore; the test wants it curvature continuous, as SMLib f916a8d's setback corner (smlib#104), which solidcore ports in solidcore#2310 builds it
solidcraft-kernel::corner_g2::tests::the_box_corner_g2_patch_is_a_closed_solid_curvature_continuous_to_its_neighbours the fitted G2 corner patch bulges 1.7e-5 past the box, as on C++
solidcraft-kernel::fillet_setback_tests::a_setback_not_past_the_radius_is_refused_naming_the_least solidcore builds setback 5.5 (15 faces) where SMLib f916a8d's setback corner (smlib#104), which solidcore ports in solidcore#2310 refuses it naming the least that builds; nothing names the least setback on solidcore until then
solidcraft-kernel::modify_history_tests::fillet_variable_and_chord the variable round r 2 to 5 along the cube's edge fails status 1001 at solidcore 8ddce9016, as variable_radius_on_a_box_edge
solidcraft-kernel::native::tests::box_and_fillet_have_stable_wire_records the box-edge round's wire record differs from the fixture at solidcore 8ddce9016 (a value 3 is 5): the fixture is re-blessed (NATIVE_BREP_BLESS=1) once the change is shown right
solidcraft-smlib::ops::tests::a_variable_round_with_a_corner_in_its_law_is_its_balls_union solidcore refuses a variable fillet whose law has a corner (solidcore#430)
solidcraft-ui-egui::preselect_tests::variable_fillet_round_trip the dialog's variable round fails status 1001 at solidcore 8ddce9016, as variable_radius_on_a_box_edge
+1 -4
View File
@@ -4,8 +4,6 @@
1080-fillet-equal-slope-basin-1-mm-rim-loop-4 [fail] continuity_order: got {'body': 'Slab', 'edges': 'blend', 'order': 'g0', want >= g1 at every blend seam
1080-fillet-sloped-basin-1-mm-rim-r4-2-edges [fail] continuity_order: got {'body': 'Slab', 'edges': 'blend', 'order': 'g0', want >= g1 at every blend seam
1080-fillet-sloped-basin-1-mm-rim-r4-4-edges [fail] continuity_order: got {'body': 'Slab', 'edges': 'blend', 'order': 'g0', want >= g1 at every blend seam
2893-chamfer-distance-and-angle-past-the-wall-refused [fail] refused, but not because "angle": step 2 (solid.chamfer): SMLib chamfer: operation: SMLib rejected the input (SM_ERR_INVALID_INPUT)
2893-chamfer-two-distances-elliptical-rim [fail] refused, but not because "not supported yet": step 3 (solid.chamfer): SMLib chamfer: operation: SMLib failed (status 1001) (tangent propagation blended the picked edges' tangent chain with them; with it off, tangent_chain:
4782-fillet-bar-edge-wider-than-both-faces [fail] continuity_order: got Box: no null seams want >= g1 at every blend seam
9-fillet-corner-curvature-with-reduce-fit [unsupported] solid.fillet builds `corner: g2` with `fit: exact` only
9-fillet-corner-curvature-with-taper-fit [unsupported] solid.fillet builds `corner: g2` with `fit: exact` only
@@ -16,9 +14,8 @@ cov-fillet-g2-mixed-corner [unsupported] refused as on SMLib d01d7b0: a G2 corne
cov-fillet-g2-oblique-three-edge-corner [unsupported] refused on solidcore: its three edges meet at different wedge angles, and one curvature fillet takes the first edge's equal-area thumbweight for every edge (solidcore#2250, issuecomment-6563419897); SMLib d01d7b0 fails it too
cov-fillet-g2-pocket-corner [fail] continuity_order: G1 at one seam where the corner patch meets a contact face (g2 wanted), as on SMLib d01d7b0
cov-junction-boss-tilt-20-root-round [fail] fillet status 1001 on a cylinder tilted 20 degrees into the plate, as on SMLib
cov-junction-boss-tilt-40-root-round [fail] fillet status 1001 on a cylinder tilted 40 degrees into the plate, as on SMLib
cov-junction-tee-equal-round [fail] the result is not a closed solid, as on SMLib: equal pipes' intersection is two plane ellipses crossing, a corner the old corner code cannot join (the corner step)
cov-junction-y-junction-120-round [unsupported] refused as on SMLib: the faces between the three intersection curves narrow to nothing at the two triple points, so the room check trips (the corner step)
report-drafted-peg-front-and-concave-rounds-together-r2 [unsupported] step 13 (solid.fillet): invalid input: not supported yet: the fillet meets a corner of this kind, which SMLib's blends do not handle yet (where its ed
report-fillet-curvature-three-edges-at-bar-corner [fail] continuity_order: G1 at one seam where the corner patch meets a contact face (g2 wanted), as on SMLib d01d7b0 (see KNOWN_FAILING)
report-fillet-curvature-three-edges-at-bar-corner-solver-residues [fail] continuity_order: G1 at one seam where the corner patch meets a contact face (g2 wanted), as on SMLib d01d7b0 (see KNOWN_FAILING)
report-fillet-curvature-three-edges-at-bar-corner-solver-residues-outside-setback [unsupported] solidcore#2697: solidcore's own filled corner builds this to its closed form (7e-11); SolidCraft's least-squares fit misses curvature by 4.02e-3 against 2.5e-3 at this pin
@@ -2,9 +2,9 @@
"sources": [],
"what": "the running part with every tier: back wall, boss, slot, spline window, tapped holes, rounds, chamfers, shell, vents, and a modelled M16 thread on the boss",
"source": "analytic",
"method": "10/008 less the M16 groove on the boss: its cylinder runs from the r1 root round (z 21, a shoulder: the groove stops w = 0.932735 clear of it) to the 1 mm top chamfer (z 29, run out through it, a free end); the groove's root r 6.917 stays outside the boss's 1.5 wall's inside (r 6.5), so it cuts the wall only. Bbox: the part's",
"method": "10/008 less the M16 groove on the boss: its cylinder runs down to the r1 root round's shoulder (End::Shoulder), where it is trimmed at z 21, up to the 1 mm top chamfer (z 29, run out through it, a free end); the groove's root r 6.917 stays outside the boss's 1.5 wall's inside (r 6.5), so it cuts the wall only. Bbox: the part's",
"body_count": 1,
"volume_mm3": 15138.532607554,
"volume_mm3": 15111.5575,
"bbox": {
"min": [
0,
+5 -5
View File
@@ -1822,12 +1822,12 @@
},
"ladder/11-cumulative-parts/004-housing": {
"measured": {
"area_mm2": 21105.287553425696,
"area_mm2": 21156.778657103445,
"body_count": 1,
"edges": 210,
"faces": 78,
"vertices": 139,
"volume_mm3": 15138.519861452509
"edges": 218,
"faces": 81,
"vertices": 144,
"volume_mm3": 15111.545095719644
},
"seconds": 4.128625132,
"status": "pass"
@@ -1,6 +1,5 @@
{
"source": "analytic",
"known_failure": "the variable round r 2 to 5 along a box edge fails status 1001 at solidcore 8ddce9016 (as blend_variable::tests::variable_radius_on_a_box_edge in tests/KNOWN_FAILING_TESTS)",
"method": "box 40x30x20 with its top front edge (y=0, z=20) rounded at a radius running straight from 2 (x=0) to 5 (x=40). The round is the union of the balls rolled along the edge, ball s touching both faces at r(s), the law continued past the edge's ends along its end slopes: each section loses the corner points no ball reaches, up to rho(x, t), the nearest entry into a ball along the ray from the edge at the angle t. V, moments and areas integrated by Gauss-Legendre panels in x and t (tools/oracle-analytic/cases_fillets.py)",
"body_count": 1,
"volume_mm3": 23888.613525715446,