* Add a one-surface fill of closed 3D loops to geom
A closed loop of curves that does not lie in a plane has no exact surface;
the usual answer is one smooth face fitted over it. fill_loop does that:
- the loop's best-fit plane (Newell normal), with a frame chosen from the
loop itself so the result does not depend on where the loop starts, its
direction, or where the part sits
- the loop seen along that normal must be simple and must not fold or run
along the normal; otherwise it is refused ("not supported yet"), never
filled with wrong geometry
- one non-rational bicubic B-spline height field over the loop's box,
fitted by least squares to the loop's samples (The NURBS Book 9.4.1,
9.4.3, eq. 9.62/9.65) with a thin-plate smoothness term
lambda*A*int(h_uu^2 + 2 h_uv^2 + h_vv^2), A one grid cell's area, so the
fit is the same at any scale; only planes are reproduced exactly
- span counts per direction (long narrow loops fit), refined until the
loop is within tolerance, else refused with the deviation reached
bspline: bisected span search, basis functions on a stack array, one
clamping helper shared by curves and surfaces, one binomial helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
* Patch fills non-planar loops of edges with one smooth face (#250)
surface.patch with edges refused any loop that is not planar ("not
supported yet: patching a loop that is not planar"). It now fills it:
- planar loops still get one exact planar face, but planarity is certified
conservatively: lines by their ends, B-spline/NURBS edges by their control
points, intersection curves by their samples lying on a support plane
within tolerance; a loop that can't be certified is filled instead
- other loops get one face from geom's fill_loop, bounded by the body's own
chained edges, whole (no split): its history is one generated face whose
edges are the body's, and it stitches back into a closed solid
- intersection-curve edges are sampled by a seeded, guarded double
projection shared by the planarity test and the fill (plane_fit)
- the face tolerance is 10x the planarity tolerance (documented: later
operations take the face to contain its edges within it)
- edge picking runs inside the guard; the geom->truck conversion is shared
with the T-spline sheets (spline_conv); the Newell plane fit is shared with
STEP import
Tests: 3- to 8-sided, concave (dart, arrow, plus), flat-corner, two-edge and
imported rims fill as one face and stitch closed (volumes against an
independent integral); finite mesh normals, same result for any pick order
or placement; non-height-field loops and too-fine detail are refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
* Test the Patch dialog filling a non-planar loop of picked edges
The Patch dialog's only test used a sketch profile. Picking the four
curved rim edges of a box cut by a sideways cylinder now makes one
surface.patch command whose result is a single smooth face over the rim.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
156 lines
5.4 KiB
Rust
156 lines
5.4 KiB
Rust
//! The B-rep kernel boundary.
|
|
//!
|
|
//! Everything SolidCraft does with solids goes through this crate: building bodies from planar
|
|
//! regions (extrude, revolve) and primitives, booleans, fillets and chamfers, tessellation,
|
|
//! measurement and STEP export and import. The implementation uses the `truck` crates (Apache-2.0; see
|
|
//! `plan/adr/0001-geometry-kernel.md`) plus our own local operations (edge fillets and
|
|
//! chamfers in `blend.rs`), but no truck type appears in the public API, so the kernel can be
|
|
//! replaced without touching the rest of the workspace.
|
|
//!
|
|
//! Every call into the third-party kernel runs under [`guard`]: a panic inside it becomes a
|
|
//! [`KernelError::Internal`] instead of taking the application down.
|
|
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
|
#![forbid(unsafe_code)]
|
|
|
|
mod blend;
|
|
mod blend_corner;
|
|
mod blend_variable;
|
|
mod body;
|
|
mod boolean_diagnostics;
|
|
mod boolean_record;
|
|
mod bore_guard;
|
|
mod build;
|
|
mod coplanar;
|
|
mod curveblend;
|
|
mod delete_face;
|
|
mod emboss;
|
|
#[cfg(test)]
|
|
mod face_source_tests;
|
|
mod fill;
|
|
mod freeform;
|
|
pub mod fuzz;
|
|
mod heal;
|
|
mod helix;
|
|
pub mod history;
|
|
mod iges_in;
|
|
mod iges_out;
|
|
#[cfg(test)]
|
|
mod intersection_curve_tests;
|
|
mod lip;
|
|
mod loopblend;
|
|
mod measure;
|
|
mod mesh_convert;
|
|
mod meshbody;
|
|
#[cfg(test)]
|
|
mod modify_history_tests;
|
|
mod numbered;
|
|
mod offset;
|
|
mod ops;
|
|
mod plane_fit;
|
|
mod polybool;
|
|
mod polyhedron;
|
|
mod prism;
|
|
mod prism_curve;
|
|
mod prism_measure;
|
|
pub mod provenance;
|
|
mod record;
|
|
mod sew;
|
|
mod smooth_edges;
|
|
mod spline_conv;
|
|
mod splitface;
|
|
mod step;
|
|
mod step_in;
|
|
mod step_out;
|
|
mod surface_kind;
|
|
mod surfaces;
|
|
mod sweep_path;
|
|
#[cfg(test)]
|
|
mod thread_tests;
|
|
mod topo;
|
|
mod tspline;
|
|
|
|
pub use blend::{ChamferSide, chamfer, chamfer_sides, fillet, fillet_max};
|
|
pub use blend_corner::fillet_radii;
|
|
pub use blend_variable::{fillet_chord, fillet_variable, fillet_variable_points};
|
|
pub use body::{Body, CapSignature, EdgeInfo, FaceInfo, FacePaint, FaceSource, Paint};
|
|
pub use boolean_diagnostics::{BooleanDiagnostics, BooleanFallback, BooleanStrategy, capture_boolean_diagnostics};
|
|
pub use build::{PathSeg, box_solid, canonical_revolve_axis, cylinder, extrude, extrude_tapered, loft, revolve, sphere, sweep, torus};
|
|
pub use delete_face::delete_faces;
|
|
pub use emboss::{cylinder_emboss_anchor, emboss_cylinder, emboss_cylinder_at, has_cylinder_emboss_wall, infer_cylinder_emboss};
|
|
pub use helix::{loft_to_point, sweep_helix, sweep_helix_mesh};
|
|
pub use iges_in::{MAX_IGES_BYTES, iges_import, iges_to_step};
|
|
pub use iges_out::iges_export_bodies;
|
|
pub use lip::raise_band;
|
|
pub use measure::{BodyMeasure, measure};
|
|
pub use mesh_convert::convert_prismatic_mesh;
|
|
pub use meshbody::{MAX_TRIANGLES, mesh_body};
|
|
pub use offset::with_same_surface;
|
|
pub use ops::{BoolOp, boolean, can_join_touching_faces, split_by_plane, transform, transform_matrix, union_apart};
|
|
pub use polybool::{faceted_boolean, planar_boolean};
|
|
pub use polyhedron::{HalfSpace, convex_polyhedron, draft, offset_faces, shell};
|
|
pub use prism::{extrude_planar_regions, planar_regions_boolean};
|
|
pub use prism_measure::certified_prism_equivalent;
|
|
pub use sew::{EdgeSpec, FaceSpec, SurfSpec, sew};
|
|
pub use splitface::{SplitTool, split_body, split_faces, split_faces_with_map};
|
|
pub use step::step_export;
|
|
pub use step_in::{ImportedBody, StepImport, StepNode, step_import, step_import_shared, step_orientation_errors, step_validate};
|
|
pub use step_out::{ExportBody, ExportProduct, StepHeader, step_export_bodies, step_export_products};
|
|
pub use surface_kind::{FaceSurface, face_surface};
|
|
pub use surfaces::{copy_faces, extend, patch_edges, patch_region, stitch, thicken, trim};
|
|
pub use sweep_path::sweep_path;
|
|
pub use topo::{CylinderFace, cylinder_face_at};
|
|
pub use topo::{TopoCounts, merged_topology, seam_flags};
|
|
pub use tspline::tspline_sheet;
|
|
|
|
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
|
|
pub enum KernelError {
|
|
#[error("invalid input: {0}")]
|
|
Invalid(String),
|
|
#[error("the operation failed: {0}")]
|
|
Failed(String),
|
|
/// A blend or other size too large for the geometry around it (a smaller size may work);
|
|
/// shown like any feature that isn't supported yet.
|
|
#[error("the operation failed: not supported yet: {0}")]
|
|
TooLarge(String),
|
|
/// The third-party kernel panicked; the model is unchanged.
|
|
#[error("kernel internal error: {0}")]
|
|
Internal(String),
|
|
}
|
|
|
|
pub type Result<T> = std::result::Result<T, KernelError>;
|
|
|
|
thread_local! {
|
|
static GUARDED: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
|
|
}
|
|
|
|
/// Is this thread inside [`guard`]? (A panic now will be caught and turned into an error; the
|
|
/// engine's crash reports mark it recovered.)
|
|
pub fn guarded() -> bool {
|
|
GUARDED.with(std::cell::Cell::get) > 0
|
|
}
|
|
|
|
/// Run third-party kernel code, converting a panic into an error.
|
|
pub fn guard<T>(what: &str, f: impl FnOnce() -> Result<T>) -> Result<T> {
|
|
struct Depth;
|
|
impl Drop for Depth {
|
|
fn drop(&mut self) {
|
|
GUARDED.with(|g| g.set(g.get().saturating_sub(1)));
|
|
}
|
|
}
|
|
GUARDED.with(|g| g.set(g.get() + 1));
|
|
let depth = Depth;
|
|
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
|
|
drop(depth);
|
|
match r {
|
|
Ok(r) => r,
|
|
Err(p) => {
|
|
let msg = p.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| p.downcast_ref::<String>().cloned()).unwrap_or_default();
|
|
log::warn!("kernel panic in {what}: {msg}");
|
|
Err(KernelError::Internal(format!("{what}: {msg}")))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|