mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-10 18:55:14 +00:00
fix(boolean): a proto edge's 3D curve and UV curves are trimmed to one interval (#13555)
Fixes #3214. create_proto_edge trimmed a copy of the 3D curve to the curve interval, then passed the same interval to each UV curve's trim. A trim snaps an end within 100 scaled zeros onto a knot of its own curve and writes it back, so a UV curve's knot a few ulps from the 3D trim parameter moved the edge's interval after the 3D curve had been cut: the new edge ran up to 2.4e-12 past its curve's natural interval, and the result failed SmEdge test 2 (Bad Domain). Reported in tangency-labs/solidcraft#13462: a rod cut by two counter-wound screw sweeps of one channel profile. Each sweep's rail on the rod's cylinder is a helix, the rails cross, and the crossing is solved from more than one face pair a few 1e-12 apart. The 3D curve and the UV curves whose trims accept the interval are now trimmed to the one interval none of their trims snaps (exact_pieces' common_trim_interval and trim_unsnapped, as the offset does since #3013). A UV curve whose trim refuses the interval is trimmed alone, as before. Where no trim snaps, the interval and the curves are unchanged bit for bit. Tests: the corpus case booleans/13462-rod-cut-by-two-counter-wound-channel-coils (closed-form volume, derivation in its expected.method) and solidcore-ops boolean::a_rod_cut_by_two_counter_wound_channel_sweeps_is_valid, which fails without the change (NotSolid) and passes with it, cut in turn and by the sweeps' union. Goldens: topology.proto_topology_p1-p3, boolean.merge_p1-p5 and boolean.topology_intersector_p1-p3 unchanged; boolean.api_brep's one change (issue1519_bench_tubes_union_measured, 1e-18) is the same on main without the change. The C++ defect is recorded as WR-767 (plan/upstream-issues/). Co-authored-by: Seth LaForge <sethml@ofb.net> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+71
-3
@@ -1,8 +1,12 @@
|
||||
//! The boolean family on boxes whose answers are known.
|
||||
#![allow(clippy::unwrap_used)]
|
||||
//! The boolean family on bodies whose answers are known.
|
||||
#![allow(clippy::unwrap_used, clippy::indexing_slicing)] // test
|
||||
|
||||
use std::io::Read as _;
|
||||
|
||||
use serde_json::Value;
|
||||
use solidcore_ops::boolean::{boolean, imprint, union_all};
|
||||
use solidcore_ops::{make_box, session, Body, BooleanOp, Error};
|
||||
use solidcore_ops::{make_box, session, Body, BooleanOp, CheckLevel, Error};
|
||||
use solidcore_record::brep::Brep as Record;
|
||||
|
||||
fn all_fixes() {
|
||||
session::set_thread_fixes(solidcore::kernel::UpstreamFixes::NAMES.iter().copied()).unwrap();
|
||||
@@ -76,3 +80,67 @@ fn an_imprint_cuts_the_faces_the_tool_crosses() {
|
||||
assert_eq!(r.counts().unwrap(), [6, 12, 8]);
|
||||
assert!(near(volume(&r), 1.0), "{}", volume(&r));
|
||||
}
|
||||
|
||||
/// The kernel rig's case `booleans/<case>`: its entry, and its operand bodies by name, read
|
||||
/// under the corpus's fix set.
|
||||
fn corpus_case(case: &str) -> (Value, impl Fn(&str) -> Body) {
|
||||
let corpus = format!(
|
||||
"{}/../solidcore/tests/kernel_cases",
|
||||
env!("CARGO_MANIFEST_DIR")
|
||||
);
|
||||
let fixes: Vec<String> =
|
||||
serde_json::from_str(&std::fs::read_to_string(format!("{corpus}/fixes.json")).unwrap())
|
||||
.unwrap();
|
||||
session::set_fixes(fixes.iter().map(String::as_str)).unwrap();
|
||||
let entry: Value = serde_json::from_str(
|
||||
&std::fs::read_to_string(format!("{corpus}/booleans/{case}.json")).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let gz = std::fs::File::open(format!("{corpus}/booleans/{case}.bodies.json.gz")).unwrap();
|
||||
let mut text = String::new();
|
||||
flate2::read::GzDecoder::new(gz)
|
||||
.read_to_string(&mut text)
|
||||
.unwrap();
|
||||
let records: Value = serde_json::from_str(&text).unwrap();
|
||||
let trusted = entry["bodies"].clone();
|
||||
let body = move |name: &str| {
|
||||
let record: Record = serde_json::from_value(records[name].clone()).unwrap();
|
||||
Body::from_record(&record, trusted[name]["trusted"].as_bool().unwrap())
|
||||
.unwrap()
|
||||
.0
|
||||
};
|
||||
(entry, body)
|
||||
}
|
||||
|
||||
/// A rod cut by two one-turn screw sweeps of one channel profile, one of each hand, from its
|
||||
/// top face: the profile's radial walls meet the rod's cylinder, so each sweep's rail there is
|
||||
/// a helix on it, and the two rails cross. The crossing is found more than once, a few 1e-12
|
||||
/// apart in the rail's parameter; the new edge along the second rail took the interval its
|
||||
/// UV curve's trim had snapped onto a knot of its own, 2.4e-12 before the 3D curve's end
|
||||
/// (`SmEdge` test 2). Cut in turn and cut by the two sweeps' union, the result is one valid
|
||||
/// solid of the case's closed form (its `expected.method`).
|
||||
#[test]
|
||||
fn a_rod_cut_by_two_counter_wound_channel_sweeps_is_valid() {
|
||||
let (entry, body) = corpus_case("13462-rod-cut-by-two-counter-wound-channel-coils");
|
||||
let want = entry["expected"]["volume_mm3"].as_f64().unwrap();
|
||||
let rel = entry["expected"]["tolerance"]["rel"].as_f64().unwrap();
|
||||
// b6 the rod, b4 and b5 the right- and left-handed sweeps, b14 the rod cut by b4 as the
|
||||
// case's run read it back from its record
|
||||
let in_turn = boolean(body("b14"), body("b5"), BooleanOp::Subtract)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let both = union_all(vec![body("b4"), body("b5")]).unwrap();
|
||||
let at_once = boolean(body("b6"), both, BooleanOp::Subtract)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
for (how, r) in [("in turn", &in_turn), ("by the union", &at_once)] {
|
||||
let report = r.check(CheckLevel::All).unwrap();
|
||||
assert!(report.is_valid(), "cut {how}: {}", report.text);
|
||||
assert_eq!(r.solid_count().unwrap(), 1, "cut {how}");
|
||||
let v = volume(r);
|
||||
assert!(
|
||||
(v - want).abs() <= rel * want,
|
||||
"cut {how}: volume {v}, closed form {want}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+45
-12
@@ -107,6 +107,10 @@
|
||||
//! `Kernel::fixes.imprint_extends_edge_to_proto_vertex`, [`ProtoTopologyManager::imprint_edge_definition`]
|
||||
//! first fits each end of the definition's interval to its proto vertex (`imprint_ends`: the SMLib
|
||||
//! fork's thread-groove fixes, tangency-labs/smlib b43876f, a8caccd).
|
||||
//! - **Not upstream: one trim interval** (WR-767, tangency-labs/solidcore#3214).
|
||||
//! [`ProtoTopologyManager::create_proto_edge`] trims the 3D curve and its uv curves to the
|
||||
//! one interval none of their trims snaps (`exact_pieces::common_trim_interval`); upstream
|
||||
//! trims them in turn with one interval, and the edge can end past its 3D curve.
|
||||
//! - `UpdateEdges` trims a definition at the split point; its alternative of splitting
|
||||
//! the proto edge (`SmProtoEdge::Split`) sits behind `static constexpr SmBoolean
|
||||
//! cbiDoSplit = FALSE` and is not ported.
|
||||
@@ -140,6 +144,7 @@ use crate::math::{max2, max3, max4, min2, Point2, Point3, Vec3, EFF_ZERO};
|
||||
use crate::nurbs::b_spline_curve_n_lib::TrimMode;
|
||||
use crate::nurbs::b_spline_surface_n_lib::p2::OrientType;
|
||||
use crate::query::face::p2::proto_vertex_containment_radius;
|
||||
use crate::surface::surface::exact_pieces::{common_trim_interval, trim_unsnapped};
|
||||
use crate::topology::brep::p5::{make_edge_in_face_in, EdgeInFace};
|
||||
use crate::topology::VertexId;
|
||||
|
||||
@@ -2211,26 +2216,54 @@ impl<O: ClassObject> ProtoTopologyManager<O> {
|
||||
let p3d_curve = cc1.curve();
|
||||
let _ = cc2.curve() == p3d_curve;
|
||||
|
||||
// Create a copy of the input curve and trim it to this CurveInterval.
|
||||
// Create a copy of the input curve and of its uv curves (copy statuses ignored), and
|
||||
// trim them to this CurveInterval.
|
||||
let mut copy = curve_of(brep, p3d_curve)?.copy(k)?;
|
||||
let mut domain = ci1.interval;
|
||||
copy.trim(k, &mut domain, TrimMode::Notify)?;
|
||||
|
||||
// See about uv curves (copy statuses ignored; the trims' passed on with
|
||||
// `ssi_proto_topology_statuses_pass_on`, fork bcc6f01).
|
||||
let pass = k.fixes.ssi_proto_topology_statuses_pass_on;
|
||||
let mut uv1 = None;
|
||||
if let Some(c) = cc1.uv_curve1() {
|
||||
uv1 = ignore_soft(curve_of(brep, Some(c))?.copy(k))?;
|
||||
if let Some(u) = uv1.as_mut() {
|
||||
ignore_soft(passed_on(u.trim(k, &mut domain, TrimMode::Notify), pass)?)?;
|
||||
}
|
||||
}
|
||||
let mut uv2 = None;
|
||||
if let Some(c) = cc2.uv_curve1() {
|
||||
uv2 = ignore_soft(curve_of(brep, Some(c))?.copy(k))?;
|
||||
if let Some(u) = uv2.as_mut() {
|
||||
ignore_soft(passed_on(u.trim(k, &mut domain, TrimMode::Notify), pass)?)?;
|
||||
}
|
||||
|
||||
// The uv curves' trim statuses are ignored, and passed on with
|
||||
// `ssi_proto_topology_statuses_pass_on` (fork bcc6f01). Not upstream (WR-767): the
|
||||
// 3D curve and the uv curves whose trims accept the interval are trimmed to the one
|
||||
// interval none of their trims snaps ([`common_trim_interval`]). Upstream trims them
|
||||
// in turn with one interval, each trim snapping it onto a knot of its own curve, and
|
||||
// the edge takes the interval the last trim left: a uv curve's knot a few ulps from
|
||||
// the 3D curve's trim parameter leaves the edge's interval outside its 3D curve's
|
||||
// (`SmEdge` test 2). A uv curve whose trim refuses the interval is trimmed alone, as
|
||||
// upstream.
|
||||
let pass = k.fixes.ssi_proto_topology_statuses_pass_on;
|
||||
let mut joins = [false; 2];
|
||||
for (u, join) in [&uv1, &uv2].into_iter().zip(&mut joins) {
|
||||
if let Some(u) = u {
|
||||
let mut probe = ci1.interval;
|
||||
let trimmed = u.copy(k)?.trim(k, &mut probe, TrimMode::IntervalOnly);
|
||||
*join = ignore_soft(passed_on(trimmed, pass)?)?.is_some();
|
||||
}
|
||||
}
|
||||
let joined: Vec<&Curve> = std::iter::once(©)
|
||||
.chain(
|
||||
[&uv1, &uv2]
|
||||
.into_iter()
|
||||
.zip(joins)
|
||||
.filter_map(|(u, join)| u.as_ref().filter(|_| join)),
|
||||
)
|
||||
.collect();
|
||||
let domain = common_trim_interval(k, &joined, ci1.interval, OP_CREATE_PE)?;
|
||||
trim_unsnapped(k, &mut copy, domain, OP_CREATE_PE)?;
|
||||
for (u, join) in [&mut uv1, &mut uv2].into_iter().zip(joins) {
|
||||
if let Some(u) = u.as_mut() {
|
||||
let trimmed = if join {
|
||||
trim_unsnapped(k, u, domain, OP_CREATE_PE)
|
||||
} else {
|
||||
u.trim(k, &mut domain.clone(), TrimMode::Notify)
|
||||
};
|
||||
ignore_soft(passed_on(trimmed, pass)?)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
Binary file not shown.
+595
@@ -0,0 +1,595 @@
|
||||
{
|
||||
"area": "booleans",
|
||||
"bodies": {
|
||||
"b14": {
|
||||
"keeping_splines": false,
|
||||
"trusted": true
|
||||
},
|
||||
"b4": {
|
||||
"keeping_splines": false,
|
||||
"trusted": true
|
||||
},
|
||||
"b5": {
|
||||
"keeping_splines": false,
|
||||
"trusted": true
|
||||
},
|
||||
"b6": {
|
||||
"keeping_splines": false,
|
||||
"trusted": true
|
||||
}
|
||||
},
|
||||
"calls": [
|
||||
{
|
||||
"inputs": [],
|
||||
"op": "extrude",
|
||||
"outputs": [
|
||||
"b1"
|
||||
],
|
||||
"params": {
|
||||
"cap": true,
|
||||
"corner": "curvature",
|
||||
"direction": [
|
||||
0.0,
|
||||
0.0,
|
||||
1.0
|
||||
],
|
||||
"distance": 124.0,
|
||||
"draft": 0.0,
|
||||
"profile": [
|
||||
{
|
||||
"arc": {
|
||||
"axis": [
|
||||
0.0,
|
||||
0.0,
|
||||
1.0
|
||||
],
|
||||
"center": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"end": 6.283185307179586,
|
||||
"radius": 16.5,
|
||||
"reference": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"start": 0.0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"op": "screw_sweep",
|
||||
"outputs": [
|
||||
"b2"
|
||||
],
|
||||
"params": {
|
||||
"direction": [
|
||||
0.0,
|
||||
0.0,
|
||||
1.0
|
||||
],
|
||||
"origin": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"pitch": -30.0,
|
||||
"profile": [
|
||||
{
|
||||
"line": {
|
||||
"end": [
|
||||
15.500000000033173,
|
||||
1.9916037001588853,
|
||||
124.0
|
||||
],
|
||||
"start": [
|
||||
16.082051957790647,
|
||||
2.5654660460962315,
|
||||
124.0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"arc": {
|
||||
"axis": [
|
||||
-0.0,
|
||||
-0.0,
|
||||
-1.0
|
||||
],
|
||||
"center": [
|
||||
0.0,
|
||||
0.0,
|
||||
124.0
|
||||
],
|
||||
"end": 0.1277903655261984,
|
||||
"radius": 15.627427341040864,
|
||||
"reference": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"start": -0.12779036552232945
|
||||
}
|
||||
},
|
||||
{
|
||||
"line": {
|
||||
"end": [
|
||||
16.08205195777746,
|
||||
-2.5654660462046173,
|
||||
124.0
|
||||
],
|
||||
"start": [
|
||||
15.50000000002603,
|
||||
-1.9916037002189337,
|
||||
124.0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"line": {
|
||||
"end": [
|
||||
16.222606424802095,
|
||||
-3.0128127705345857,
|
||||
124.0
|
||||
],
|
||||
"start": [
|
||||
16.08205195777746,
|
||||
-2.5654660462046173,
|
||||
124.0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"line": {
|
||||
"end": [
|
||||
17.205794692955955,
|
||||
-3.1954074838129767,
|
||||
124.0
|
||||
],
|
||||
"start": [
|
||||
16.222606424802095,
|
||||
-3.0128127705345857,
|
||||
124.0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"arc": {
|
||||
"axis": [
|
||||
0.0,
|
||||
0.0,
|
||||
1.0
|
||||
],
|
||||
"center": [
|
||||
0.0,
|
||||
0.0,
|
||||
124.0
|
||||
],
|
||||
"end": 0.1836248887672353,
|
||||
"radius": 17.50000000010735,
|
||||
"reference": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"start": -0.18362488876723534
|
||||
}
|
||||
},
|
||||
{
|
||||
"line": {
|
||||
"end": [
|
||||
16.222606424880645,
|
||||
3.012812770469558,
|
||||
124.0
|
||||
],
|
||||
"start": [
|
||||
17.205794692955955,
|
||||
3.1954074838129767,
|
||||
124.0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"line": {
|
||||
"end": [
|
||||
16.082051957790647,
|
||||
2.5654660460962315,
|
||||
124.0
|
||||
],
|
||||
"start": [
|
||||
16.222606424880645,
|
||||
3.012812770469558,
|
||||
124.0
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"tolerance": 0.003,
|
||||
"turns": 1.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"op": "screw_sweep",
|
||||
"outputs": [
|
||||
"b3"
|
||||
],
|
||||
"params": {
|
||||
"direction": [
|
||||
-0.0,
|
||||
-0.0,
|
||||
-1.0
|
||||
],
|
||||
"origin": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"pitch": 30.0,
|
||||
"profile": [
|
||||
{
|
||||
"line": {
|
||||
"end": [
|
||||
15.500000000033173,
|
||||
1.9916037001588853,
|
||||
124.0
|
||||
],
|
||||
"start": [
|
||||
16.082051957790647,
|
||||
2.5654660460962315,
|
||||
124.0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"arc": {
|
||||
"axis": [
|
||||
-0.0,
|
||||
-0.0,
|
||||
-1.0
|
||||
],
|
||||
"center": [
|
||||
0.0,
|
||||
0.0,
|
||||
124.0
|
||||
],
|
||||
"end": 0.1277903655261984,
|
||||
"radius": 15.627427341040864,
|
||||
"reference": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"start": -0.12779036552232945
|
||||
}
|
||||
},
|
||||
{
|
||||
"line": {
|
||||
"end": [
|
||||
16.08205195777746,
|
||||
-2.5654660462046173,
|
||||
124.0
|
||||
],
|
||||
"start": [
|
||||
15.50000000002603,
|
||||
-1.9916037002189337,
|
||||
124.0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"line": {
|
||||
"end": [
|
||||
16.222606424802095,
|
||||
-3.0128127705345857,
|
||||
124.0
|
||||
],
|
||||
"start": [
|
||||
16.08205195777746,
|
||||
-2.5654660462046173,
|
||||
124.0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"line": {
|
||||
"end": [
|
||||
17.205794692955955,
|
||||
-3.1954074838129767,
|
||||
124.0
|
||||
],
|
||||
"start": [
|
||||
16.222606424802095,
|
||||
-3.0128127705345857,
|
||||
124.0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"arc": {
|
||||
"axis": [
|
||||
0.0,
|
||||
0.0,
|
||||
1.0
|
||||
],
|
||||
"center": [
|
||||
0.0,
|
||||
0.0,
|
||||
124.0
|
||||
],
|
||||
"end": 0.1836248887672353,
|
||||
"radius": 17.50000000010735,
|
||||
"reference": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"start": -0.18362488876723534
|
||||
}
|
||||
},
|
||||
{
|
||||
"line": {
|
||||
"end": [
|
||||
16.222606424880645,
|
||||
3.012812770469558,
|
||||
124.0
|
||||
],
|
||||
"start": [
|
||||
17.205794692955955,
|
||||
3.1954074838129767,
|
||||
124.0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"line": {
|
||||
"end": [
|
||||
16.082051957790647,
|
||||
2.5654660460962315,
|
||||
124.0
|
||||
],
|
||||
"start": [
|
||||
16.222606424880645,
|
||||
3.012812770469558,
|
||||
124.0
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"tolerance": 0.003,
|
||||
"turns": 1.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
"b6"
|
||||
],
|
||||
"op": "copy",
|
||||
"outputs": [
|
||||
"b7"
|
||||
],
|
||||
"params": {}
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
"b7"
|
||||
],
|
||||
"op": "copy",
|
||||
"outputs": [
|
||||
"b8"
|
||||
],
|
||||
"params": {}
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
"b4"
|
||||
],
|
||||
"op": "copy",
|
||||
"outputs": [
|
||||
"b9"
|
||||
],
|
||||
"params": {}
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
"b9"
|
||||
],
|
||||
"op": "copy",
|
||||
"outputs": [
|
||||
"b10"
|
||||
],
|
||||
"params": {}
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
"b8"
|
||||
],
|
||||
"op": "tagged",
|
||||
"outputs": [
|
||||
"b11"
|
||||
],
|
||||
"params": {
|
||||
"operand": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
"b10"
|
||||
],
|
||||
"op": "tagged",
|
||||
"outputs": [
|
||||
"b12"
|
||||
],
|
||||
"params": {
|
||||
"operand": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
"b11",
|
||||
"b12"
|
||||
],
|
||||
"op": "boolean",
|
||||
"outputs": [
|
||||
"b13"
|
||||
],
|
||||
"params": {
|
||||
"op": "subtract"
|
||||
}
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
"b14"
|
||||
],
|
||||
"op": "copy",
|
||||
"outputs": [
|
||||
"b15"
|
||||
],
|
||||
"params": {}
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
"b15"
|
||||
],
|
||||
"op": "copy",
|
||||
"outputs": [
|
||||
"b16"
|
||||
],
|
||||
"params": {}
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
"b5"
|
||||
],
|
||||
"op": "copy",
|
||||
"outputs": [
|
||||
"b17"
|
||||
],
|
||||
"params": {}
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
"b17"
|
||||
],
|
||||
"op": "copy",
|
||||
"outputs": [
|
||||
"b18"
|
||||
],
|
||||
"params": {}
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
"b16"
|
||||
],
|
||||
"op": "tagged",
|
||||
"outputs": [
|
||||
"b19"
|
||||
],
|
||||
"params": {
|
||||
"operand": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
"b18"
|
||||
],
|
||||
"op": "tagged",
|
||||
"outputs": [
|
||||
"b20"
|
||||
],
|
||||
"params": {
|
||||
"operand": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
"b19",
|
||||
"b20"
|
||||
],
|
||||
"op": "boolean",
|
||||
"outputs": [
|
||||
"b21"
|
||||
],
|
||||
"params": {
|
||||
"op": "subtract"
|
||||
}
|
||||
}
|
||||
],
|
||||
"case": "13462-rod-cut-by-two-counter-wound-channel-coils",
|
||||
"expected": {
|
||||
"body_count": 1,
|
||||
"method": "Each horizontal section of a screw sweep of a profile D in a plane normal to the axis is one rotated copy of D. With D_in = D n disk(16.5), A_in its area, one sweep removes A_in*|p|; the two sweeps' sections at one height are D_in turned by +t and -t, whose overlap integrates over the turn to (|p|/2pi)*Int s(r)^2 r dr, s(r) the angle D_in covers on the circle of radius r. s(r) found exactly from the circle's crossings with the profile's lines and axis-centred arcs, integrated by adaptive Gauss-Kronrod: A_in = 4.214046963094, Int s^2 r dr = 1.279023422464. V = pi*16.5^2*124 - 2*A_in*30 + (30/2pi)*Int s^2 r dr. The one-sweep form agrees with a single cut to 3e-12. Tolerance 1e-6: the sweeps' walls are within 3e-3 of the screw surfaces (|p|*1e-4), which moves the two sweeps' overlap by about 2e-3 mm^3 (1.6e-8 here)",
|
||||
"source": "analytic",
|
||||
"sources": [
|
||||
"tangency-labs/solidcraft#13462",
|
||||
"tangency-labs/solidcore#3214"
|
||||
],
|
||||
"tolerance": {
|
||||
"rel": 1e-06
|
||||
},
|
||||
"volume_mm3": 105810.29046175262,
|
||||
"what": "a rod (r 16.5, 124 long) cut by two one-turn screw sweeps of one channel profile, one right-handed and one left-handed, pitch 30, both starting on the rod's top face: the channel's two radial walls meet the rod's surface, so each sweep's rail on it is a helix there and the two rails cross. The cut's new edge along a crossing rail took an interval its UV curve's trim had snapped 2.4e-12 past its 3D curve's end ('SmEdge test 2, Bad Domain')"
|
||||
},
|
||||
"expressible": true,
|
||||
"operation_expressible": true,
|
||||
"outcome": {
|
||||
"ran": true,
|
||||
"raw_counts": {
|
||||
"edges": 60,
|
||||
"faces": 30,
|
||||
"vertices": 32
|
||||
}
|
||||
},
|
||||
"records": "13462-rod-cut-by-two-counter-wound-channel-coils.bodies.json.gz",
|
||||
"result": [
|
||||
{
|
||||
"body": "Rod",
|
||||
"made": "b21"
|
||||
}
|
||||
],
|
||||
"rig": {
|
||||
"error": null,
|
||||
"known_failing": null,
|
||||
"measured": {
|
||||
"area_mm2": 14815.578874890229,
|
||||
"body_count": 1,
|
||||
"edges": 55,
|
||||
"faces": 25,
|
||||
"vertices": 32,
|
||||
"volume_mm3": 105810.28872357338
|
||||
},
|
||||
"refused_with": null,
|
||||
"status": "pass"
|
||||
},
|
||||
"sources": [
|
||||
"tangency-labs/solidcraft#13462",
|
||||
"tangency-labs/solidcore#3214"
|
||||
],
|
||||
"traced_ops": [
|
||||
"extrude",
|
||||
"screw_sweep",
|
||||
"screw_sweep",
|
||||
"copy",
|
||||
"copy",
|
||||
"copy",
|
||||
"copy",
|
||||
"tagged",
|
||||
"tagged",
|
||||
"boolean",
|
||||
"copy",
|
||||
"copy",
|
||||
"copy",
|
||||
"copy",
|
||||
"tagged",
|
||||
"tagged",
|
||||
"boolean"
|
||||
],
|
||||
"ungraded": [],
|
||||
"what": "a rod (r 16.5, 124 long) cut by two one-turn screw sweeps of one channel profile, one right-handed and one left-handed, pitch 30, both starting on the rod's top face: the channel's two radial walls meet the rod's surface, so each sweep's rail on it is a helix there and the two rails cross. The cut's new edge along a crossing rail took an interval its UV curve's trim had snapped 2.4e-12 past its 3D curve's end ('SmEdge test 2, Bad Domain')",
|
||||
"why_not": []
|
||||
}
|
||||
@@ -20,12 +20,13 @@
|
||||
}
|
||||
},
|
||||
"booleans": {
|
||||
"cases": 124,
|
||||
"cases": 125,
|
||||
"chain": {
|
||||
"1": 9,
|
||||
"121": 1,
|
||||
"14": 2,
|
||||
"16": 3,
|
||||
"17": 1,
|
||||
"193": 1,
|
||||
"28": 1,
|
||||
"3": 2,
|
||||
@@ -39,9 +40,9 @@
|
||||
"8": 6,
|
||||
"9": 2
|
||||
},
|
||||
"expressible": 109,
|
||||
"expressible": 110,
|
||||
"not_expressible": 12,
|
||||
"operation_expressible": 112,
|
||||
"operation_expressible": 113,
|
||||
"reasons": {
|
||||
"a final body built or changed by SolidCraft (its own geometry or a change to the kernel's record)": 10,
|
||||
"refused or failed in SolidCraft, not in a kernel operation": 2
|
||||
|
||||
solidcore/plan/upstream-issues/WR-767--createprotoedge-trims-its-curves-in-turn-with-one-interval.md
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
#### WR-767. `SmProtoTopologyManager::CreateProtoEdge` trims the 3D curve and its UV curves in turn with one interval, so the new edge's interval can end past its 3D curve
|
||||
|
||||
- **Severity:** Wrong result (the Boolean's result fails `SmEdge::AssertValid` test 2, "Bad Domain")
|
||||
- **Area:** Boolean
|
||||
- **Location:** at v1.0.1 (`3fd5b9548d19aa8c12c29bc9637a96d42a06f8a5`):
|
||||
- `source/SMLib/src/SmProtoTopology.cpp`, `SmProtoTopologyManager::CreateProtoEdge` (`:4234`-`4299`). The copy of the 3D curve is trimmed with `sCurveDomain` (`:4255`), then each UV curve with the same `sCurveDomain` (`:4264`, `:4273`), each call commented "may snap sIvl by tol to existing knots". The proto edge and its edge definition take `sCurveDomain` as the last trim left it (`:4277`).
|
||||
- `source/SMLib/src/SmBSplineCurveNLib.cpp`, `SmBSplineCurve::Trim` (`:4469`). It snaps an end within `100 * SmTol::GetScaledZero` onto a knot of its own curve (`:4541`-`4561`) and writes the snapped ends back to its argument (`:4593`).
|
||||
- `source/SMLib/src/SmEdge.cpp`, `SmEdge::AssertValid` test 2 (`:7508`): `m_vInterval.IsContainedBy(m_pCurve->GetNaturalInterval(), SM_EFF_ZERO)`.
|
||||
- **What happens:** The 3D curve is cut at the classified interval's ends. A UV curve whose own knot lies within 100 scaled zeros of an end moves that end onto its knot, and the edge keeps the moved end. Where the UV curve's knot lies outside the 3D curve's cut end by more than `SM_EFF_ZERO`, the edge's interval runs past its curve's natural interval.
|
||||
- **Trigger:** A rod cut by two one-turn screw sweeps of one channel profile, one of each hand, starting on the rod's top face: tangency-labs/solidcore#3214, from SolidCraft's tangency-labs/solidcraft#13462. The profile's radial walls meet the rod's cylinder, so each sweep's rail there is a helix on it, and the two rails cross. The crossing is found from more than one face pair, a few 1e-12 apart in the rail's parameter; the second cut's UV curve already holds a knot from an earlier split at one of those values. In the Rust port, the 3D rail is cut at t = 3.32525827878329050, and the UV curve's trim snaps the interval to its knot at 3.32525827878092617. The new edge's interval [3.325258278780926, 2π] then starts 2.4e-12 before its curve's natural interval [3.325258278783290, 2π], which test 2 refuses.
|
||||
- **How found:** tangency-labs/solidcore#3214, tracing the failing edge in the Rust port, which kept the C++ sequence.
|
||||
- **Confirmed:** yes, from the pinned source. The three trims share `sCurveDomain` and the proto edge takes its last value, as above. No C++ build was run, so the C++ outcome on the case's operands is not known: its intersection may place the crossing differently.
|
||||
|
||||
The Rust fix in `topology/proto_topology/p2.rs` trims the 3D curve and its UV curves to the one interval none of their trims snaps (`exact_pieces::common_trim_interval`, then `trim_unsnapped` for each), as the offset has done since tangency-labs/solidcore#3013. The regression is the corpus case `booleans/13462-rod-cut-by-two-counter-wound-channel-coils` and `crates/solidcore-ops/tests/boolean.rs::a_rod_cut_by_two_counter_wound_channel_sweeps_is_valid`.
|
||||
Reference in New Issue
Block a user