fix(kernel): revolve with edges on the axis is built around the origin, then moved (#452)
A full revolve of a profile with more than one edge on the axis (a spherical shell's half ring: both radial lines lie on it) took the generic sweep, built in place. Truck tessellates the degenerate faces those edges sweep reliably only with the axis through the world origin, so on a sketch plane off it the body measured about a third too large (a shell from r 18.5 to 40, 10 mm above XY: 324 339 mm³ instead of 241 561), and booleans with it failed or removed the wrong side. Such profiles are now swept about an axis through the origin and translated into place, as revolve_touching_axis already does for profiles with one edge on the axis. Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
ddd528abaf
commit
7f92093cc8
2 files changed
+49
-1
No files matched your search
@@ -331,6 +331,38 @@ fn revolve_and_primitives() {
|
||||
assert!(rel(m["total"]["volume_mm3"].as_f64().unwrap(), PI * 125.0 * 5.0 / 2.0) < 5e-4);
|
||||
}
|
||||
|
||||
/// A spherical shell (a half ring between two arcs, both radial lines on the axis) revolved on a
|
||||
/// sketch plane that does not pass through the origin: the body is the shell, as it is on XY.
|
||||
/// Before, built in place, it measured a third too large (truck tessellates the degenerate faces
|
||||
/// on the axis reliably only with the axis through the origin).
|
||||
#[test]
|
||||
fn revolve_a_spherical_shell_off_the_origin() {
|
||||
for (plane, z) in [(json!("XY"), 0.0), (json!({"origin": [0, 0, 10], "x_dir": [1, 0, 0], "y_dir": [0, 1, 0]}), 10.0)] {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": plane, "name": "Ring"}));
|
||||
run(&mut s, "sketch.line", json!({"points": [[-40, 0], [40, 0]]}));
|
||||
run(&mut s, "sketch.arc.center_point", json!({"center": [0, 0], "start": [40, 0], "end": [-40, 0]}));
|
||||
run(&mut s, "sketch.arc.center_point", json!({"center": [0, 0], "start": [18.5, 0], "end": [-18.5, 0]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
// The half ring: the larger of the two regions above the axis.
|
||||
let si = run(&mut s, "sketch.inspect", json!({"sketch": "Ring"}));
|
||||
let ring = si["profiles"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.max_by(|a, b| a["area_mm2"].as_f64().unwrap().total_cmp(&b["area_mm2"].as_f64().unwrap()))
|
||||
.unwrap()["index"]
|
||||
.clone();
|
||||
run(&mut s, "solid.revolve", json!({"sketch": "Ring", "profiles": [ring], "axis": "l1", "body_name": "Shell"}));
|
||||
let m = run(&mut s, "inspect.measure", json!({"bodies": ["Shell"]}));
|
||||
let want = 4.0 / 3.0 * PI * (40.0f64.powi(3) - 18.5f64.powi(3));
|
||||
let v = m["total"]["volume_mm3"].as_f64().unwrap();
|
||||
assert!(rel(v, want) < 5e-4, "plane at z {z}: {v} vs {want}");
|
||||
let b = &m["bodies"][0]["bbox"];
|
||||
assert!((b["min"][2].as_f64().unwrap() - (z - 40.0)).abs() < 1e-6 && (b["max"][2].as_f64().unwrap() - (z + 40.0)).abs() < 1e-6, "{b}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sketch_tools_and_constraints() {
|
||||
let mut s = Session::default();
|
||||
|
||||
@@ -384,9 +384,25 @@ pub fn revolve(plane: &Plane, regions: &[Region2], axis_origin: Vec2, axis_dir:
|
||||
out.push(b);
|
||||
continue;
|
||||
}
|
||||
let solid: Solid = guard("revolve", || Ok(builder::rsweep(&f, p3(o), v3(axis), mt::Rad(angle))))?;
|
||||
// Edges on the axis (a spherical shell's two radial lines, say) sweep into degenerate faces
|
||||
// there, which truck only tessellates reliably with the axis through the world origin. Off
|
||||
// it, the body measured too large and booleans with it failed or removed the wrong side.
|
||||
// Build such a profile there and move it into place, as `revolve_touching_axis` does.
|
||||
let on_axis = |p: Vec2| d.cross(p - axis_origin).abs() < 1e-9;
|
||||
let axis_edges = r.outer.segs.iter().filter(|s| matches!(**s, Seg2::Line { a, b } if on_axis(a) && on_axis(b))).count();
|
||||
let shift = if axis_edges > 0 { o } else { Vec3::ZERO };
|
||||
let (profile, f) = if shift.len() > 0.0 {
|
||||
let local = Plane { origin: plane.origin - shift, ..*plane };
|
||||
let profile = region_wires(&local, &given.oriented())?;
|
||||
let f = attach_profile(&wires_of(&profile))?;
|
||||
(profile, f)
|
||||
} else {
|
||||
(profile, f)
|
||||
};
|
||||
let solid: Solid = guard("revolve", || Ok(builder::rsweep(&f, p3(o - shift), v3(axis), mt::Rad(angle))))?;
|
||||
let closed = angle.abs() >= std::f64::consts::TAU;
|
||||
let history = guard("revolve history", || Ok(record::swept_face(&solid, &f, &seeds(&profile), closed)))?;
|
||||
let solid = if shift.len() > 0.0 { builder::translated(&solid, v3(shift)) } else { solid };
|
||||
out.push(record::attach(Body::new(solid)?, &[], history, Default::default())?);
|
||||
}
|
||||
Ok(out)
|
||||
|
||||
Reference in new issue
Block a user