mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-11 19:22:01 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ffbb0b050 | |||
| 1289fbc82e | |||
| 42c76341b4 | |||
| 96559e3969 |
@@ -877,6 +877,7 @@ fn rust(k: &Kernel, op: &OpIn) -> Result<Output, HarnessError> {
|
||||
[cont(k1[0])?, cont(k1[1])?],
|
||||
[cont(k2[0])?, cont(k2[1])?],
|
||||
*adj,
|
||||
0.0,
|
||||
&sol,
|
||||
);
|
||||
let keep = tri!(o, r);
|
||||
|
||||
+26
-10
@@ -476,10 +476,21 @@ fn reverse_ignored(k: &Kernel, curve: &mut Curve, domain: &Extent1d) -> Result<b
|
||||
/// `ivl1` / `ivl2` of the two curves (`start.parameters = [t1, t2]`,
|
||||
/// `start.solution_value` the gap) is kept. Exact (gap below `SM_EFF_ZERO` or the
|
||||
/// scaled zero) and tangential (gap perpendicular to both tangents) answers are
|
||||
/// kept; an answer on an interval end where the curve continues (`conts*` at least
|
||||
/// `G1`) is dropped when the next interval gets closer; one on a curve end (`conts*`
|
||||
/// at most `C0`) is kept; others are dropped. `adjacency` 1 / 2 drops the shared end
|
||||
/// of adjacent spans in self intersection (`[1st, 2nd]` / `[2nd, 1st]`).
|
||||
/// kept. The scaled zero is `SM_EFF_ZERO` times one plus the largest of the solution
|
||||
/// points, tangents and parameters and `evaluation_scale`: the largest coordinate the
|
||||
/// curves' points are evaluated from (zero for analytic curves, whose points are the
|
||||
/// solution's own magnitude). A B-spline point is a convex combination of its control
|
||||
/// points, rounded to about `SM_EFF_ZERO` times their largest coordinate (Farouki and
|
||||
/// Rajan, "On the numerical condition of polynomials in Bernstein form", CAGD 4, 1987),
|
||||
/// so a gap of that size is as exact as the curve can give: upstream scaled the zero by
|
||||
/// the solution alone and dropped such crossings, for instance on a line's B-spline,
|
||||
/// whose control points lie at the infinite parameter, 1.2e6 mm out.
|
||||
///
|
||||
/// The other answers are kept or dropped as upstream: an answer on an interval end
|
||||
/// where the curve continues (`conts*` at least `G1`) is dropped when the next interval
|
||||
/// gets closer; one on a curve end (`conts*` at most `C0`) is kept; others are dropped.
|
||||
/// `adjacency` 1 / 2 drops the shared end of adjacent spans in self intersection
|
||||
/// (`[1st, 2nd]` / `[2nd, 1st]`).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
@@ -493,6 +504,7 @@ pub fn filter_solution<C1, C2, O>(
|
||||
conts1: [ContinuityType; 2],
|
||||
conts2: [ContinuityType; 2],
|
||||
adjacency: u64,
|
||||
evaluation_scale: f64,
|
||||
solution: &Solution<O>,
|
||||
) -> Result<bool>
|
||||
where
|
||||
@@ -523,14 +535,18 @@ where
|
||||
let [p1, t1] = pv1;
|
||||
let [p2, t2] = pv2;
|
||||
|
||||
// keep answers good to ScaledZero (sensitive to parameter values [B628])
|
||||
// keep answers good to ScaledZero (sensitive to parameter values [B628]), and to the
|
||||
// rounding of the curves' evaluation (not upstream)
|
||||
let scaled_zero = EFF_ZERO
|
||||
* (1.0
|
||||
+ max4(
|
||||
max2(p1.max_dimension(), t1.max_dimension()),
|
||||
max2(p2.max_dimension(), t2.max_dimension()),
|
||||
s0,
|
||||
s1,
|
||||
+ max2(
|
||||
max4(
|
||||
max2(p1.max_dimension(), t1.max_dimension()),
|
||||
max2(p2.max_dimension(), t2.max_dimension()),
|
||||
s0,
|
||||
s1,
|
||||
),
|
||||
evaluation_scale,
|
||||
));
|
||||
if value < scaled_zero {
|
||||
return Ok(true);
|
||||
|
||||
+50
-3
@@ -86,7 +86,8 @@ use crate::geom::tree::{NodeData, Tree, TreeNode};
|
||||
use crate::geom::vector3d::{Point3, Vec3};
|
||||
use crate::kernel::Kernel;
|
||||
use crate::math::{max2, min2, rad_to_deg, BIG_DOUBLE, EFF_ZERO, EFF_ZERO_SQ, EFF_ZERO_SQRT, PI};
|
||||
use crate::nurbs::b_spline_curve::ProjectionType;
|
||||
use crate::nurbs::b_spline_curve::{BSplineCurve, ProjectionType};
|
||||
use crate::nurbs::math::{NOW, NOZ};
|
||||
|
||||
const OP_CCI: &str = "curve::curve::p7::CciGlobalSolver::local_solve_branch";
|
||||
const OP_GPA: &str = "curve::curve::p7::Curve::global_property_analysis";
|
||||
@@ -125,6 +126,28 @@ fn leaf_span<O: Copy + PartialEq>(tree: &Tree<O>, node: &TreeNode<O>) -> Result<
|
||||
Ok(*tree.curve_span(node)?)
|
||||
}
|
||||
|
||||
/// The largest coordinate a point of `curve` is evaluated from: that of its control points
|
||||
/// for a curve evaluated from them (a B-spline, a line made a B-spline by a mirror), zero for
|
||||
/// an analytic curve (a line, an ellipse, a parabola, a hyperbola), which evaluates from its
|
||||
/// own frame, and for the kinds without control points (not upstream: see `filter_solution`).
|
||||
fn evaluation_scale(curve: &Curve) -> f64 {
|
||||
if curve.is_analytic() {
|
||||
return 0.0;
|
||||
}
|
||||
let Some(Ok(points)) = curve.b_spline_base().map(BSplineCurve::control_points) else {
|
||||
return 0.0;
|
||||
};
|
||||
points.iter().fold(0.0, |m, p| {
|
||||
let w = if p.w == NOW || p.w == 0.0 {
|
||||
1.0
|
||||
} else {
|
||||
p.w.abs()
|
||||
};
|
||||
let z = if p.z == NOZ { 0.0 } else { p.z.abs() };
|
||||
max2(m, max2(max2(p.x.abs(), p.y.abs()), z) / w)
|
||||
})
|
||||
}
|
||||
|
||||
impl<'t, O: SolutionObject + 't> CciGlobalSolver<'t, O> {
|
||||
/// A two-variable single value solution between the branch's nodes naming both
|
||||
/// curves.
|
||||
@@ -181,6 +204,12 @@ impl<'t, O: SolutionObject + 't> CciGlobalSolver<'t, O> {
|
||||
let k: &Kernel = self
|
||||
.kernel
|
||||
.ok_or(SolidError::invalid_input(OP_CCI, "solver without kernel"))?;
|
||||
// the largest coordinate the two curves' points are evaluated from (not upstream:
|
||||
// see `filter_solution`)
|
||||
let evaluation_scale = max2(
|
||||
evaluation_scale(parent_curve1),
|
||||
evaluation_scale(parent_curve2),
|
||||
);
|
||||
let tol = self.base.tolerance_3d;
|
||||
let interval1 = self.interval1;
|
||||
let interval2 = self.interval2;
|
||||
@@ -716,7 +745,16 @@ impl<'t, O: SolutionObject + 't> CciGlobalSolver<'t, O> {
|
||||
|
||||
// Now check the refined solution.
|
||||
let keep = filter_solution(
|
||||
k, curve1, &ivl1, curve2, &ivl2, conts1, conts2, adjacency, sol,
|
||||
k,
|
||||
curve1,
|
||||
&ivl1,
|
||||
curve2,
|
||||
&ivl2,
|
||||
conts1,
|
||||
conts2,
|
||||
adjacency,
|
||||
evaluation_scale,
|
||||
sol,
|
||||
)?;
|
||||
|
||||
// save aSolutions, which should be kept, to m_pSolutions
|
||||
@@ -939,7 +977,16 @@ impl<'t, O: SolutionObject + 't> CciGlobalSolver<'t, O> {
|
||||
// Filter out intersections which are actually outside of the boundary
|
||||
// of the curve and are not of the tangent sort
|
||||
let keep = filter_solution(
|
||||
k, curve1, &ivl1, curve2, &ivl2, conts1, conts2, adjacency, &solution,
|
||||
k,
|
||||
curve1,
|
||||
&ivl1,
|
||||
curve2,
|
||||
&ivl2,
|
||||
conts1,
|
||||
conts2,
|
||||
adjacency,
|
||||
evaluation_scale,
|
||||
&solution,
|
||||
)?;
|
||||
|
||||
// Filter out the nasty end case where the intersection converges to
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
//! tangency-labs/solidcraft#13004: SolidCraft joins a mirrored arm to a torso with slanted
|
||||
//! sides, and the union fails (`boolean_union_refuses_solid_leak`) or, without that fix,
|
||||
//! returns no solid; joined to the arm as made it succeeds.
|
||||
//!
|
||||
//! The bodies are SolidCraft's (binary `.smb`, written by SolidCraft from the issue's
|
||||
//! script): the torso, a trapezoid extruded 8 mm with its sides slanted 13 degrees; the pin, a
|
||||
//! cylinder of radius 2 along x from (5, 0, 26) to (7, 0, 26) through the torso's slanted
|
||||
//! side; the arm, a box joined to that pin. Their straight edges are infinite lines (natural
|
||||
//! interval [-1234567, 1234567]), as SolidCraft and STEP make them.
|
||||
//!
|
||||
//! Mirroring turns each line into a degree-1 B-spline over the line's NURB, whose two
|
||||
//! control points lie about 1.2e6 mm out. A point of it near the part is a convex combination
|
||||
//! of those far points, good to about 1e-16 * 1.2e6 mm. The pin's seam crosses the torso's
|
||||
//! slanted side; the curve/curve solve found that crossing with a gap of 4.4e-11 and the
|
||||
//! solution filter dropped it (its exact zero was scaled by the solution point, 2.8e-11), so
|
||||
//! the intersection of the pin with the side was not split at the seam and the faces were
|
||||
//! misclassified.
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::panic,
|
||||
clippy::indexing_slicing
|
||||
)] // test helpers
|
||||
use solidcore::boolean::api_brep::boolean_union;
|
||||
use solidcore::geom::attribute::{AttributeStore, ObjectKey};
|
||||
use solidcore::kernel::{KernelOptions, UpstreamFixes};
|
||||
use solidcore::mass::integrator::properties::{
|
||||
brep_properties, MassPropertyBehavior, PropertyAttributes,
|
||||
};
|
||||
use solidcore::query::brep::{calculate_bounding_box, BoundingBoxOptions};
|
||||
use solidcore::topology::api_files::read_brep_from_file;
|
||||
use solidcore::topology::brep::mirror::transform_matrix;
|
||||
use solidcore::topology::brep::p10::TopologyEnv;
|
||||
use solidcore::topology::brep::p2::is_manifold_solid;
|
||||
use solidcore::topology::topology::NotifyRecords;
|
||||
use solidcore::{Brep, Kernel};
|
||||
|
||||
/// The mirror in the plane x = 0 (row-vector convention: the fourth row is the translation).
|
||||
const MIRROR_X: [[f64; 4]; 4] = [
|
||||
[-1.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 1.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 1.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
];
|
||||
|
||||
const TORSO: &[u8] = include_bytes!("data/solidcraft_13004_torso.smb");
|
||||
const PIN: &[u8] = include_bytes!("data/solidcraft_13004_pin.smb");
|
||||
const ARM: &[u8] = include_bytes!("data/solidcraft_13004_arm.smb");
|
||||
|
||||
fn read(k: &mut Kernel, bytes: &[u8]) -> Brep {
|
||||
let fresh = Brep::new();
|
||||
let mut store = AttributeStore::new();
|
||||
let mut records = NotifyRecords::new();
|
||||
let key_of = |e| fresh.owner_identity(e);
|
||||
let mut env = TopologyEnv {
|
||||
all_attributes: &[],
|
||||
attributes: &mut store,
|
||||
key_of: &key_of,
|
||||
records: &mut records,
|
||||
next_key: 1 << 50,
|
||||
};
|
||||
let mut b = read_brep_from_file(k, Some(bytes.to_vec()), false, false, &mut env).unwrap();
|
||||
b.refresh_cached_topology();
|
||||
b
|
||||
}
|
||||
|
||||
fn volume(k: &Kernel, b: &mut Brep) -> f64 {
|
||||
let bbox = calculate_bounding_box(k, &mut *b, BoundingBoxOptions::default()).unwrap();
|
||||
let store = AttributeStore::default();
|
||||
let key_of = |_| ObjectKey(0);
|
||||
brep_properties(
|
||||
k,
|
||||
b,
|
||||
1e-9,
|
||||
bbox.mid(),
|
||||
MassPropertyBehavior::default(),
|
||||
&PropertyAttributes {
|
||||
store: &store,
|
||||
key_of: &key_of,
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
.volume
|
||||
}
|
||||
|
||||
/// The union of the torso with `tool` mirrored `mirrors` times in x = 0, run with `fixes`:
|
||||
/// its volume when it is a closed solid, else why not. The torso is symmetric about x = 0,
|
||||
/// so the union's volume does not depend on `mirrors`.
|
||||
fn union_volume(tool: &[u8], mirrors: usize, fixes: &UpstreamFixes) -> Result<f64, String> {
|
||||
let mut k = Kernel::new(&KernelOptions::default());
|
||||
k.fixes = *fixes;
|
||||
let torso = read(&mut k, TORSO);
|
||||
let mut t = read(&mut k, tool);
|
||||
for _ in 0..mirrors {
|
||||
transform_matrix(&mut k, &mut t, &MIRROR_X, true).unwrap();
|
||||
}
|
||||
let mut u = boolean_union(&mut k, torso, t).map_err(|e| e.to_string())?;
|
||||
if !is_manifold_solid(&u).unwrap() {
|
||||
return Err("not a solid".into());
|
||||
}
|
||||
Ok(volume(&k, &mut u))
|
||||
}
|
||||
|
||||
/// `got` is a solid of volume `want`: to 1e-6 relative, the volume integration's error on
|
||||
/// the same body with B-spline in place of line edges (about 2e-8); the misclassified union
|
||||
/// was 1.2% off.
|
||||
fn assert_same(what: &str, got: Result<f64, String>, want: f64) {
|
||||
match got {
|
||||
Ok(v) => assert!(
|
||||
(v - want).abs() <= 1e-6 * want,
|
||||
"{what}: {v}, expected {want}"
|
||||
),
|
||||
Err(e) => panic!("{what}: {e}, expected a solid of {want}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Torso and pin: 1352 + pi 2^2 2 less the pin's part inside the torso. Mirrored twice
|
||||
/// (the same pin, its edges now B-splines) the union keeps that volume; it was 1348.66.
|
||||
#[test]
|
||||
fn torso_and_twice_mirrored_pin() {
|
||||
let made = union_volume(PIN, 0, &UpstreamFixes::default()).unwrap();
|
||||
assert!((made - 1365.533).abs() < 1e-3, "{made}");
|
||||
assert_same(
|
||||
"default fixes",
|
||||
union_volume(PIN, 2, &UpstreamFixes::default()),
|
||||
made,
|
||||
);
|
||||
assert_same(
|
||||
"all fixes",
|
||||
union_volume(PIN, 2, &UpstreamFixes::all()),
|
||||
made,
|
||||
);
|
||||
}
|
||||
|
||||
/// Torso and arm, as in the issue: the arm mirrored to the other side of the torso joins with
|
||||
/// the volume of the arm as made, 1516.80 (by symmetry). With every fix on it was refused
|
||||
/// (`boolean_union_refuses_solid_leak`, status 1001); with none it gave no solid.
|
||||
#[test]
|
||||
fn torso_and_mirrored_arm() {
|
||||
let made = union_volume(ARM, 0, &UpstreamFixes::all()).unwrap();
|
||||
assert!((made - 1516.798).abs() < 1e-3, "{made}");
|
||||
assert_same(
|
||||
"all fixes",
|
||||
union_volume(ARM, 1, &UpstreamFixes::all()),
|
||||
made,
|
||||
);
|
||||
assert_same(
|
||||
"default fixes",
|
||||
union_volume(ARM, 1, &UpstreamFixes::default()),
|
||||
made,
|
||||
);
|
||||
assert_same(
|
||||
"twice, all fixes",
|
||||
union_volume(ARM, 2, &UpstreamFixes::all()),
|
||||
made,
|
||||
);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user