mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-11 11:11:59 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd03ebd334 | |||
| 1c29ddedd6 | |||
| ab5cf3b1db | |||
| 808686d3fd |
+176
@@ -0,0 +1,176 @@
|
||||
//! solidcraft#13085 (tangency-labs/solidcore#3150): a revolve cut whose tool has a cylindrical
|
||||
//! face a hair (1e-10 to 6e-10) outside the wall it is meant to coincide with, meeting it at a
|
||||
//! torus/cylinder tangent junction. The intersection curve's hit on the cup's rim is
|
||||
//! ill-conditioned there, so the proto vertex at the corner keeps the mean of that hit and the
|
||||
//! corner, and the end fit of `SmEdgeDefinition::Imprint` (the imprint flags) stops the rim
|
||||
//! edge short of the corner vertex the B-rep already had: loops out of tolerance. With
|
||||
//! `imprint_fits_edge_end_to_final_vertex` the end goes to that vertex (WR-753).
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::panic,
|
||||
clippy::indexing_slicing
|
||||
)] // test helpers
|
||||
|
||||
use std::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||
|
||||
use solidcore_ops::boolean::{boolean, BooleanOp};
|
||||
use solidcore_ops::sweep::{extrude, revolve, Curve, DraftCorner};
|
||||
use solidcore_ops::{session, Body, CheckLevel};
|
||||
|
||||
/// SolidCraft's end-fit flags and the fix under test.
|
||||
fn imprint_fixes() {
|
||||
session::set_fixes([
|
||||
"imprint_trims_edge_to_proto_vertex",
|
||||
"imprint_extends_edge_to_proto_vertex",
|
||||
"imprint_fits_edge_end_to_final_vertex",
|
||||
])
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn line(start: [f64; 3], end: [f64; 3]) -> Curve {
|
||||
Curve::Line { start, end }
|
||||
}
|
||||
|
||||
fn arc(
|
||||
center: [f64; 3],
|
||||
axis: [f64; 3],
|
||||
reference: [f64; 3],
|
||||
radius: f64,
|
||||
start: f64,
|
||||
end: f64,
|
||||
) -> Curve {
|
||||
Curve::Arc {
|
||||
center,
|
||||
axis,
|
||||
reference,
|
||||
radius,
|
||||
start,
|
||||
end,
|
||||
}
|
||||
}
|
||||
|
||||
/// The cup: a half-disc of radius 24.75 (y <= 0) with a 7 mm lug down to y = -34.75 (its end
|
||||
/// round r 3.5 about (0, -31.25), its junctions with the ring filleted r 5 about
|
||||
/// (+-8.5, -28.5099), tangent to the ring and to the lug's sides x = +-3.5) and a 2.5 mm eye
|
||||
/// hole through the lug, extruded 20; a half-disc of radius 23.75 cut 19 deep from the top
|
||||
/// (a 1 mm wall and floor).
|
||||
fn cup() -> Body {
|
||||
const R: f64 = 24.75;
|
||||
const FY: f64 = -28.509_866_713_120_786;
|
||||
let z = [0.0, 0.0, 1.0];
|
||||
let zn = [0.0, 0.0, -1.0];
|
||||
let x = [1.0, 0.0, 0.0];
|
||||
// The ring's tangent point with a fillet: along the line from the origin through the
|
||||
// fillet's centre, 24.75 out.
|
||||
// atan2(-23.718292475621443, -7.071428571428571)
|
||||
let t = -1.860_548_028_230_959_4_f64;
|
||||
let outline = [
|
||||
line([R, 0.0, 0.0], [-R, 0.0, 0.0]),
|
||||
arc([0.0; 3], z, x, R, PI, t + TAU),
|
||||
arc([-8.5, FY, 0.0], zn, x, 5.0, -(PI + t), 0.0),
|
||||
line([-3.5, FY, 0.0], [-3.5, -31.25, 0.0]),
|
||||
arc([0.0, -31.25, 0.0], z, x, 3.5, PI, TAU),
|
||||
line([3.5, -31.25, 0.0], [3.5, FY, 0.0]),
|
||||
arc([8.5, FY, 0.0], zn, x, 5.0, -PI, t),
|
||||
arc([0.0; 3], z, x, R, -(PI + t), 0.0),
|
||||
arc([0.0, -31.25, 0.0], zn, x, 2.5, -TAU, 0.0),
|
||||
];
|
||||
let body = extrude(&outline, z, 20.0, 0.0, DraftCorner::Curvature, true).unwrap();
|
||||
let inner = [
|
||||
line([23.75, 0.0, 1.0], [-23.75, 0.0, 1.0]),
|
||||
arc([0.0, 0.0, 1.0], z, x, 23.75, PI, TAU),
|
||||
];
|
||||
let hollow = extrude(&inner, z, 19.0, 0.0, DraftCorner::Curvature, true).unwrap();
|
||||
boolean(body, hollow, BooleanOp::Subtract)
|
||||
.unwrap()
|
||||
.expect("the cup")
|
||||
}
|
||||
|
||||
/// The tool: the profile Y in [-34.75, wall], Z in [5, 20], its lower corner a quarter arc of
|
||||
/// radius 10 about (-34.75, 15) ending tangent to the vertical line at (`wall`, 15), revolved
|
||||
/// `angle` about Z, placed symmetrically about the YZ plane (the profile turned by -angle/2,
|
||||
/// whose sine and cosine `half` gives; `angle` is in radians).
|
||||
fn tool(wall: f64, angle: f64, half: (f64, f64)) -> Body {
|
||||
let (s, c) = (-half.0, half.1);
|
||||
let turn = |p: [f64; 3]| [c * p[0] - s * p[1], s * p[0] + c * p[1], p[2]];
|
||||
let turn_dir = |d: [f64; 3]| [c * d[0] - s * d[1], s * d[0] + c * d[1], d[2]];
|
||||
let profile = [
|
||||
line(turn([0.0, -34.75, 20.0]), turn([0.0, -34.75, 5.0])),
|
||||
arc(
|
||||
turn([0.0, -34.75, 15.0]),
|
||||
turn_dir([1.0, 0.0, 0.0]),
|
||||
turn_dir([0.0, 1.0, 0.0]),
|
||||
10.0,
|
||||
-FRAC_PI_2,
|
||||
0.0,
|
||||
),
|
||||
line(turn([0.0, wall, 15.0]), turn([0.0, wall, 20.0])),
|
||||
line(turn([0.0, wall, 20.0]), turn([0.0, -34.75, 20.0])),
|
||||
];
|
||||
revolve(&profile, [0.0; 3], [0.0, 0.0, 1.0], angle, true).unwrap()
|
||||
}
|
||||
|
||||
/// The two revolve angles, 28 and 36 degrees: their names, their radians, and the sine and
|
||||
/// cosine of half of each (the ops tests compute no geometry, so the values are written out).
|
||||
const ANGLES: [(&str, f64, (f64, f64)); 2] = [
|
||||
(
|
||||
"28 deg",
|
||||
0.488_692_190_558_412_3,
|
||||
(0.241_921_895_599_667_73, 0.970_295_726_275_996_5),
|
||||
),
|
||||
(
|
||||
"36 deg",
|
||||
0.628_318_530_717_958_6,
|
||||
(0.309_016_994_374_947_4, 0.951_056_516_295_153_5),
|
||||
),
|
||||
];
|
||||
|
||||
/// The cut, or why it failed: the boolean's error, or no body.
|
||||
fn cut(wall: f64, angle: (&str, f64, (f64, f64))) -> Result<Body, String> {
|
||||
match boolean(cup(), tool(wall, angle.1, angle.2), BooleanOp::Subtract) {
|
||||
Ok(Some(b)) => Ok(b),
|
||||
Ok(None) => Err("no body".to_string()),
|
||||
Err(e) => Err(format!("{e}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// The tool's vertical face exactly on the cup's outer wall: the cut is right.
|
||||
#[test]
|
||||
fn a_tool_face_on_the_wall_cuts() {
|
||||
imprint_fixes();
|
||||
for angle in ANGLES {
|
||||
let b = cut(-24.75, angle).unwrap_or_else(|e| panic!("{}: {e}", angle.0));
|
||||
let report = b.check(CheckLevel::All).unwrap();
|
||||
assert!(
|
||||
report.is_valid(),
|
||||
"{}:
|
||||
{}",
|
||||
angle.0,
|
||||
report.text
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The tool's vertical face a hair outside the wall (the reporter's sketch left it 4e-10
|
||||
/// out): at 36 degrees the tool's end planes reach into the lug's fillets and the rim edge
|
||||
/// between the fillets' tangent points is rebuilt from the intersection; at 28 degrees they
|
||||
/// clear the fillets. Both cuts are right.
|
||||
#[test]
|
||||
fn a_tool_face_a_hair_outside_the_wall_cuts_too() {
|
||||
imprint_fixes();
|
||||
for off in [1e-10, 4e-10, 6e-10] {
|
||||
for angle in ANGLES {
|
||||
let b = cut(-24.75 - off, angle)
|
||||
.unwrap_or_else(|e| panic!("offset {off}, {}: {e}", angle.0));
|
||||
let report = b.check(CheckLevel::All).unwrap();
|
||||
assert!(
|
||||
report.is_valid(),
|
||||
"offset {off}, {}:
|
||||
{}",
|
||||
angle.0,
|
||||
report.text
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -1171,6 +1171,8 @@ pub struct UpstreamFixes {
|
||||
pub imprint_extends_edge_to_proto_vertex: bool,
|
||||
/// With `imprint_extends_edge_to_proto_vertex`, a line, circle or ellipse is continued to its proto vertex over its NURB and stays that kind: a circle or ellipse keeps its frame, radii and inside-out sense, its angular domain carried on to the new end, and a line runs between the continued ends (the bar-top/boss arc of a union, an exact `SmCircle` from `SmCone::IntersectWithPlane`, stopped 5.4e-10 short of its tangent point). Without it those kinds are not continued (the fork before this change made them plain B-splines). The new curve must evaluate as the continued NURB does, or the curve is not continued (WR-601; the fork's tangency-labs/smlib@bec8a0b).
|
||||
pub imprint_extension_keeps_analytic_curve: bool,
|
||||
/// With `imprint_trims_edge_to_proto_vertex` or `imprint_extends_edge_to_proto_vertex`, an end whose proto vertex resolved to one vertex in each B-rep is fitted to that vertex's position when the two B-reps' vertices lie within the edge definition's tolerance of each other, not to the proto vertex's position. A proto vertex imprinted onto a vertex the B-rep already has keeps the mean of its definitions' points, which lies off that vertex when one definition is an ill-conditioned hit (a tool face coaxial with the wall it cuts, a hair outside it, meeting a tangent torus junction: the end of the rim edge stops 1.45e-5 to 4.6e-5 short of its vertex, loops out of tolerance or `make_faceuse` fails; WR-753, tangency-labs/solidcore#3150). Off by default: the fork's end fit (bec8a0b) targets the proto vertex.
|
||||
pub imprint_fits_edge_end_to_final_vertex: bool,
|
||||
/// `SmEdgeDefinition::Imprint` takes an edge Brep2's face already has between the definition's two vertices along its curve (`SmTools::FindCoincidentEdge` within the larger of the face's and the definition's tolerance) as the definition's edge there, where upstream makes a second edge beside it: the face then has coincident edges and the merge copies the boundary edge into Brep1 beside the curve's (`SmFace` tests 33 and 34; two equal tubes meeting end to end) (WR-580, #1519; the fork's tangency-labs/smlib@81cd61a).
|
||||
pub imprint_reuses_coincident_face_edge: bool,
|
||||
/// `SmFilletSolver::ComputePointValues` chooses its finite-difference step for each point (1e-3 at a tangent point, 1e-5 otherwise), not once per process as the function-local static `sdStaticStep`, so a blend does not depend on the blends before it ([`Kernel::fillet_point_step`] is then neither read nor set, and an existing latch is kept as it is) (ND-3, #1173; the fork's tangency-labs/smlib@9e17f64 (was 0a05860)).
|
||||
|
||||
@@ -100,15 +100,22 @@ impl<O: ClassObject> ProtoTopologyManager<O> {
|
||||
v.final_vertex(1) == Some(vtx1) && v.final_vertex(2) == Some(vtx2)
|
||||
});
|
||||
}
|
||||
let pv_pos = if one_vertex {
|
||||
proto.position()
|
||||
} else {
|
||||
let p1 = brep.vertex_point(vtx1)?;
|
||||
let p2 = brep.vertex_point(vtx2)?;
|
||||
if !(p1.distance(p2) <= self.ed_ref(ed)?.raw_tolerance()) {
|
||||
continue;
|
||||
}
|
||||
// WR-498: the proto vertex's position when it is one vertex in each B-rep, else
|
||||
// the end's own final vertex when the two B-reps agree on it. WR-753 (opt-in): the
|
||||
// final vertex first whenever the B-reps agree on it, since a proto vertex imprinted
|
||||
// onto a vertex the B-rep already had keeps the mean of its definitions' points, not
|
||||
// that vertex's position.
|
||||
let p1 = brep.vertex_point(vtx1)?;
|
||||
let p2 = brep.vertex_point(vtx2)?;
|
||||
let finals_agree = p1.distance(p2) <= self.ed_ref(ed)?.raw_tolerance();
|
||||
let pv_pos = if k.fixes.imprint_fits_edge_end_to_final_vertex && finals_agree {
|
||||
p1
|
||||
} else if one_vertex {
|
||||
proto.position()
|
||||
} else if finals_agree {
|
||||
p1
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let d = self.ed_ref(ed)?;
|
||||
let interval = d.xsect_interval();
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
#### WR-753. `SmEdgeDefinition::Imprint`'s end fit (WR-452, WR-453, WR-498) aims at the proto vertex's position when that proto vertex was imprinted onto a vertex the B-rep already had, which sits elsewhere
|
||||
- **Severity:** Wrong result (a rim edge ends 1.45e-5 to 3e-5 short of its vertex: loops out of tolerance, the ops crate's `NotSolid`; SolidCraft's Subtract fails its check)
|
||||
- **Area:** Booleans / proto topology
|
||||
- **Location:** `crates/solidcore/src/topology/proto_topology/p2/imprint_ends.rs`, `fit_edge_def_ends_to_proto_vertices`; the fork's end-fit block at the top of `SmEdgeDefinition::Imprint` (tangency-labs/smlib b43876f, a8caccd, ffaf4e8, bec8a0b). Not in upstream SMLib, which has no end fit.
|
||||
- **What happens:** WR-498 sends an end to its proto vertex's position when every vertex definition of that proto vertex resolved to the edge's own final vertex in each B-rep, assuming the vertex sits there. A proto vertex whose definition was imprinted onto a vertex the B-rep already had (`imprint_vertex_definition`'s merge within tolerance) keeps the mean of its definitions' points; the B-rep's vertex is not moved. When one definition is an ill-conditioned hit, the mean lies off the vertex by half the hit's error, and the fitted end stops there.
|
||||
- **Trigger:** a subtract whose tool has a cylindrical face coaxial with the wall it cuts and a hair (1e-10 to 6e-10) outside it, meeting the wall at a torus/cylinder tangent junction, where the wall's rim meets fillets tangentially: the intersection curve along the rim hits the fillet's rim arc tangentially, the hit lands 2.9e-5 (1e-10) to 5.8e-5 (4e-10) along the rim from the exact corner, the proto vertex's position is the mean of the hit and the corner, and the rim edge is fitted to that mean: 1.45e-5 to 2.9e-5 short of the corner vertex, over the 2e-5 loop tolerance. Without the imprint flags the edge keeps the raw hit parameter (2.9e-5 off), as upstream. SolidCraft: tangency-labs/solidcraft#13085 (a half-ring cup with a lug, a revolve cut of 36 degrees whose profile corner the sketch solver left 4e-10 off the wall; tangency-labs/solidcore#3150).
|
||||
- **How found:** SolidCraft triage of #13085; the ops-crate test `crates/solidcore-ops/tests/boolean_cut_tool_wall_a_hair_off_coincident.rs` (committed first, failing), probes of the proto vertex position and the final vertices at the fit.
|
||||
- **Confirmed:** yes, in the port: with SolidCraft's fix set the 4e-10 cut fails its check with the gaps the issue reports (5.9e-5 and 2.9e-5), and with this flag the 2e-10, 4e-10 and 6e-10 cuts pass at 28 and 36 degrees. Not confirmed against the C++ fork (no fork build here); the fork at bec8a0b carries the same WR-498 rule.
|
||||
- **Fix:** `Kernel::fixes.imprint_fits_edge_end_to_final_vertex` (opt-in, off by default): the fit's target is the end's final vertex whenever its final vertices in the two B-reps lie within the edge definition's tolerance of each other; the proto vertex's position only when they do not and the proto vertex is one vertex in each B-rep (WR-498's rule), else the end is left alone. The edge is made between those final vertices, so that is where its curve must end.
|
||||
- **Not fixed here:** from 1e-9 outside, the rulings where the tool cylinder meets the lug fillets (two cylinders internally tangent within the offset) come out of the surface/surface intersection beside the true tangent line (9.1e-5 at 1e-9), which no end fit can move, and `merge::p2::make_faceuse` fails; a second cause, recorded on tangency-labs/solidcore#3150.
|
||||
- Fixes tangency-labs/solidcore#3150 for offsets up to 6e-10 (the reporter's 4e-10).
|
||||
Reference in New Issue
Block a user