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:
MqriusandClaude Opus 5.5 authored and GitHub committed 2026-10-12 20:25:46 -04:00
1 parent ddd528abaf
commit 7f92093cc8
2 files changed
+49 -1

No files matched your search

+32
View File
@@ -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();
+17 -1
View File
@@ -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)