mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 18:45:55 +00:00
kernel-cases: a continuity check, and ladder 07/015 graded by it
expected.json `continuity`: over the tangent seams wholly within `within_mm` of `near`, the curvature jump across them (exact surfaces, 65 stations a seam) at most `curvature_jump_per_mm` clear (`clear_mm` along the seam) of the vertices where two of them turn, where a neighbour's own G1 seam ends. 07/015, the box corner's three r5 rounds with corner: g2: one valid body of 10 faces within the box, its patch's twelve seams jumping at most 1e-3 per mm clear of the hole's six corners by 1.2 mm (2.7e-4 measured; the ball corner of 009 jumps 0.2). Its golden entry is the passing run's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -114,6 +114,60 @@ fn open_edges(positions: &[solidcraft_engine::geom::Vec3], triangles: &[[u32; 3]
|
||||
}
|
||||
|
||||
/// An error that means "this kernel can't do it yet", not a wrong answer.
|
||||
/// Stations along each seam the `continuity` check samples.
|
||||
const SEAM_SAMPLES: usize = 65;
|
||||
/// A seam is tangent (G1), and two seams meeting at a vertex turn there, past this angle (°).
|
||||
const SEAM_ANGLE_DEG: f64 = 1.0;
|
||||
|
||||
/// The `continuity` check's measure of `body`: over the tangent seams lying wholly within
|
||||
/// `within` of `near`, the worst curvature jump (1/mm) at every station and at the stations at
|
||||
/// least `clear` (mm, along the seam) from each vertex where two of those seams turn (a corner of
|
||||
/// the patch's boundary, where its neighbours' own G1 seams end and no surface is G2 to both),
|
||||
/// with how many seams and stations clear there were. An error where the seams can't be measured.
|
||||
fn curvature_jumps(body: &solidcraft_engine::kernel::Body, near: [f64; 3], within: f64, clear: f64) -> Result<Value, String> {
|
||||
use solidcraft_engine::geom::Vec3;
|
||||
let tol = solidcraft_engine::kernel::ContinuityTolerances { gap_mm: 1e-3, angle_deg: SEAM_ANGLE_DEG, curvature_per_mm: 0.0 };
|
||||
let report = solidcraft_engine::kernel::seam_diagnostics(body, &[], SEAM_SAMPLES, tol).map_err(|e| e.to_string())?;
|
||||
let at = Vec3::new(near[0], near[1], near[2]);
|
||||
let seams: Vec<_> = report
|
||||
.seams
|
||||
.iter()
|
||||
.filter(|s| s.normal_angle_deg.value <= SEAM_ANGLE_DEG && s.points.len() >= 2 && s.points.iter().all(|p| p.dist(at) <= within))
|
||||
.collect();
|
||||
// Each seam's end directions, pointing into it.
|
||||
let ends = |s: &solidcraft_engine::kernel::SeamReport| -> Option<[(Vec3, Vec3); 2]> {
|
||||
let (p0, p1) = (*s.points.first()?, *s.points.last()?);
|
||||
let (q0, q1) = (*s.points.get(1)?, *s.points.get(s.points.len() - 2)?);
|
||||
Some([(p0, (q0 - p0).normalized()?), (p1, (q1 - p1).normalized()?)])
|
||||
};
|
||||
let all_ends: Vec<(Vec3, Vec3)> = seams.iter().filter_map(|s| ends(s)).flatten().collect();
|
||||
let weld = 1e-6 * within.max(1.0);
|
||||
let cos = SEAM_ANGLE_DEG.to_radians().cos();
|
||||
// A vertex turns where another seam leaves it other than straight on.
|
||||
let turns = |p: Vec3, d: Vec3| all_ends.iter().any(|(q, e)| q.dist(p) <= weld && (*e - d).len() > 1e-9 && -e.dot(d) < cos);
|
||||
let (mut worst, mut worst_clear, mut stations) = (0.0f64, 0.0f64, 0usize);
|
||||
for s in &seams {
|
||||
let Some(e) = ends(s) else { continue };
|
||||
let turning: Vec<Vec3> = e.iter().filter(|(p, d)| turns(*p, *d)).map(|(p, _)| *p).collect();
|
||||
// Arc length along the seam to each station, from its start.
|
||||
let mut along = vec![0.0f64; s.points.len()];
|
||||
for k in 1..s.points.len() {
|
||||
along[k] = along[k - 1] + s.points[k].dist(s.points[k - 1]);
|
||||
}
|
||||
let length = along.last().copied().unwrap_or(0.0);
|
||||
for (k, st) in s.stations.iter().enumerate() {
|
||||
worst = worst.max(st.curvature_jump_per_mm);
|
||||
let a = along.get(k).copied().unwrap_or(0.0);
|
||||
let from_turn = turning.iter().map(|p| if p.dist(s.points[0]) <= weld { a } else { length - a }).fold(f64::INFINITY, f64::min);
|
||||
if from_turn >= clear {
|
||||
worst_clear = worst_clear.max(st.curvature_jump_per_mm);
|
||||
stations += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(json!({"seams": seams.len(), "stations_clear": stations, "worst_per_mm": worst, "worst_clear_per_mm": worst_clear}))
|
||||
}
|
||||
|
||||
fn unsupported(e: &str) -> bool {
|
||||
e.contains("not supported yet")
|
||||
}
|
||||
@@ -256,6 +310,24 @@ fn check_case(dir: &str, dumps: Option<&std::path::Path>) -> Value {
|
||||
open == 0 && over == 0,
|
||||
);
|
||||
}
|
||||
// Curvature continuity across the seams round a point: the worst jump clear of the
|
||||
// seams' turns must be within the case's bound.
|
||||
if let Some(cont) = want.get("continuity").filter(|c| !c.is_null()) {
|
||||
let (near, within, clear, bound) =
|
||||
(v3(&cont["near"]), cont["within_mm"].as_f64(), cont["clear_mm"].as_f64(), cont["curvature_jump_per_mm"].as_f64());
|
||||
match (near, within, clear, bound) {
|
||||
(Some(near), Some(within), Some(clear), Some(bound)) => match curvature_jumps(&b.body, near, within, clear) {
|
||||
Ok(got) => {
|
||||
let ok = got["seams"].as_u64().unwrap_or(0) > 0
|
||||
&& got["stations_clear"].as_u64().unwrap_or(0) > 0
|
||||
&& got["worst_clear_per_mm"].as_f64().is_some_and(|w| w <= bound);
|
||||
c.push("curvature_jump", got, json!({"worst_clear_per_mm": format!("<= {bound}")}), ok);
|
||||
}
|
||||
Err(e) => c.push("curvature_jump", json!(format!("{}: {e}", b.name)), json!("measurable"), false),
|
||||
},
|
||||
_ => c.push("curvature_jump", json!("continuity needs near, within_mm, clear_mm and curvature_jump_per_mm"), json!("a bound"), false),
|
||||
}
|
||||
}
|
||||
if want_valid {
|
||||
// Closed shells, and every validity check this build has.
|
||||
if want_shells.is_none() {
|
||||
|
||||
@@ -88,6 +88,7 @@ the frozen truck-geometry PRs, a `kernel-paused` PR), for the report's coverage
|
||||
| `body_count`, `faces`, `edges`, `vertices` | Exact counts, where the geometry fixes them. Faces and edges count whole analytic surfaces and curves once (a full cylinder side is one face). |
|
||||
| `volume_mm3`, `area_mm2`, `center_of_mass`, `bbox` | As in `tests/oracle`: totals over every body, relative tolerance 1e-3 for volume and area, 1e-3 of the box diagonal for positions. |
|
||||
| `faces`, `edges`, `vertices`, `shells`, `body_count` as `[min, max]` | An inclusive range, where the geometry only bounds a count (a modelled thread's faces). |
|
||||
| `continuity` | `{near, within_mm, clear_mm, curvature_jump_per_mm}`: over every tangent seam lying wholly within `within_mm` of `near` (a corner's patch and its neighbours), the curvature jump across it (the faces' normal curvatures square to the seam, from their exact surfaces, at 65 stations a seam) is at most `curvature_jump_per_mm` at every station at least `clear_mm` along the seam from a vertex where two of those seams turn (where a neighbour's own G1 seam ends, which no surface is G2 to both sides of). A rolling ball's corner jumps 1 / r everywhere. |
|
||||
| `watertight` | `true`: each body's display mesh (chord tolerance `tolerance.mesh_mm`, default 0.01 mm) has no open and no over-shared edges, with vertices closer than `tolerance.weld_mm` (default 1e-4 mm) taken as one. |
|
||||
| `timeout_s` | A longer timeout for this case than the run's (at most 1800 s), for cases that are slow by nature. A case that only needs it on a loaded machine shouldn't set it. |
|
||||
| `tolerance` | Optional overrides: `rel`, `abs_mm`, `mesh_mm`, `weld_mm`. |
|
||||
|
||||
@@ -2,7 +2,29 @@
|
||||
"sources": [],
|
||||
"what": "three r5 rounds at a corner with the g2 corner patch",
|
||||
"source": "invariants",
|
||||
"method": "the g2 patch replaces the sphere octant; no closed-form volume; one valid body, the same faces as 009",
|
||||
"method": "the corner set back 1.5 r = 7.5 and filled with one patch G2 to the three rounds and the three faces; no closed-form volume; one valid body, the same faces as 009, within the box; across the patch's seams the curvature jump is at most 1e-3 per mm (the ball's corner, 009, jumps 1/r = 0.2) clear of the six points where a round's section ends on a face, where the round's own G1 seam with the face ends (the patch passes from one curvature to the other within 15% of each side: 1.2 mm covers it)",
|
||||
"body_count": 1,
|
||||
"faces": 10
|
||||
"faces": 10,
|
||||
"bbox": {
|
||||
"min": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"max": [
|
||||
60,
|
||||
40,
|
||||
20
|
||||
]
|
||||
},
|
||||
"continuity": {
|
||||
"near": [
|
||||
60,
|
||||
40,
|
||||
20
|
||||
],
|
||||
"within_mm": 15,
|
||||
"clear_mm": 1.2,
|
||||
"curvature_jump_per_mm": 0.001
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1189,8 +1189,16 @@
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/07-fillets/015-corner-g2-patch": {
|
||||
"seconds": 0.00302384,
|
||||
"status": "unsupported"
|
||||
"measured": {
|
||||
"area_mm2": 8520.38925544218,
|
||||
"body_count": 1,
|
||||
"edges": 24,
|
||||
"faces": 10,
|
||||
"vertices": 16,
|
||||
"volume_mm3": 47370.10741440046
|
||||
},
|
||||
"seconds": 2.184099933,
|
||||
"status": "pass"
|
||||
},
|
||||
"ladder/07-fillets/016-corner-three-sequential-r5-r3-r7": {
|
||||
"seconds": 0.045599545,
|
||||
|
||||
@@ -863,8 +863,14 @@ case(T7, "014-curved-edge-r2", "an r2 round on the top edge of a cylinder r10 h2
|
||||
{"body_count": 1, "volume_mm3": PI * 100 * 20 - SPANDREL * 4 * 2 * PI * (10 - XBAR * 2), "faces": 4},
|
||||
"a torus round: Pappus on the removed section (1-pi/4) r^2 at radius 10 - r(10-3pi)/(12-3pi)")
|
||||
case(T7, "015-corner-g2-patch", "three r5 rounds at a corner with the g2 corner patch",
|
||||
[F(corner, 5, corner="g2")], {"body_count": 1, "faces": 10},
|
||||
"the g2 patch replaces the sphere octant; no closed-form volume; one valid body, the same faces as 009", base=B1, source="invariants")
|
||||
[F(corner, 5, corner="g2")],
|
||||
{"body_count": 1, "faces": 10, "bbox": {"min": [0, 0, 0], "max": [60, 40, 20]},
|
||||
"continuity": {"near": [60, 40, 20], "within_mm": 15, "clear_mm": 1.2, "curvature_jump_per_mm": 0.001}},
|
||||
"the corner set back 1.5 r = 7.5 and filled with one patch G2 to the three rounds and the three faces; no closed-form volume; "
|
||||
"one valid body, the same faces as 009, within the box; across the patch's seams the curvature jump is at most 1e-3 per mm "
|
||||
"(the ball's corner, 009, jumps 1/r = 0.2) clear of the six points where a round's section ends on a face, where the round's "
|
||||
"own G1 seam with the face ends (the patch passes from one curvature to the other within 15% of each side: 1.2 mm covers it)",
|
||||
base=B1, source="invariants")
|
||||
V7 = V5 - 2 * SPANDREL * 25 * 20 - SPANDREL * 9 * 60 + SPANDREL * 1 * 2 * PI * (8 + XBAR * 1)
|
||||
case(T7, "017-part-rounds", "the running part: r5 on the front vertical edges, r3 on the back wall's top front edge, r1 where the boss meets the top",
|
||||
[F([[0, 0, 10], [60, 0, 10]], 5), F([[30, 30, 35]], 3), F([[23, 15, 20]], 1)],
|
||||
|
||||
Reference in New Issue
Block a user