Surface toolset: icons and planned commands (#808)

Icons for the surface toolset, and its planned commands registered and disabled with the issue that tracks each one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Irakli KourtisandClaude Opus 5.5 authored and GitHub committed 2026-10-13 14:28:38 -04:00
1 parent dd084ec9e1
commit c8d668108a
6 files changed
+437 -31

No files matched your search

+86 -1
View File
@@ -1,5 +1,5 @@
//! Surface commands: Patch, Stitch, Thicken and Trim (surface bodies and turning them into
//! solids).
//! solids), and the planned surface toolset (`planned!`: registered, not built yet).
use serde_json::Value;
use solidcraft_doc::{FaceAt, FeatureKind, PlaneRef, ProfileSel, expr::Kind};
@@ -30,6 +30,54 @@ pub static COMMANDS: &[CommandSpec] = &[
.at("SURFACE", "MODIFY")
.icon("trim")
.params("body: surface name; plane: XY|XZ|YZ|plane name|{origin, normal} or tool: {body, point} (a face, its surface extended); keep: [x,y,z] (a point on the side to keep)"),
planned!("surface.extrude", "Surface Extrude", "757")
.at("SURFACE", "CREATE")
.icon("surface_extrude")
.params("planned: the parameters of solid.extrude, with open curve chains as well as profiles (walls only, no caps); caps?: bool (close a closed profile)"),
planned!("surface.revolve", "Surface Revolve", "757")
.at("SURFACE", "CREATE")
.icon("surface_revolve")
.params("planned: the parameters of solid.revolve, with open curve chains as well as profiles (no caps); caps?: bool"),
planned!("surface.sweep", "Surface Sweep", "757")
.at("SURFACE", "CREATE")
.icon("surface_sweep")
.params("planned: the parameters of solid.sweep, with an open profile chain as well as a closed one (no caps); caps?: bool"),
planned!("surface.loft", "Surface Loft", "757")
.at("SURFACE", "CREATE")
.icon("surface_loft")
.params("planned: the parameters of solid.loft, with open sections as well as closed ones (no caps); caps?: bool"),
planned!("surface.offset", "Offset Surface", "758")
.at("SURFACE", "CREATE")
.icon("surface_offset")
.params("planned: faces: [{body, point}]; distance: expr (signed, along the normals; 0 copies the faces); chain?: bool (the whole tangent chain of each pick); tolerance?: expr (B-spline fit, default 0.01 mm)"),
planned!("surface.ruled", "Ruled Surface", "762")
.at("SURFACE", "CREATE")
.icon("ruled")
.params("planned: body; edges: [[x,y,z]] (points on an edge chain); distance: expr; mode?: normal|tangent|angle|direction; angle?: expr; direction?: [x,y,z]; flip?: bool; corners?: mitre|round"),
planned!("surface.boundary", "Boundary Surface", "763")
.at("SURFACE", "CREATE")
.icon("boundary")
.params("planned: u: [curves], v?: [curves] (sketch curves, 3D sketch wires or {body, point} edges; 2–4 boundaries, more make a network); continuity?: [{curve, g: 0|1|2}] for edge boundaries; operation?: new|join"),
planned!("surface.blend", "Blend Surface", "764")
.at("SURFACE", "CREATE")
.icon("surface_blend")
.params("planned: from, to: {body, edges: [[x,y,z]]} (two edge chains apart); continuity?: [g0|g1|g2, g0|g1|g2] (each side); bulge?: [number, number]; align?: auto|[[x,y,z], [x,y,z]]; stitch?: bool"),
planned!("surface.untrim", "Untrim", "761")
.at("SURFACE", "MODIFY")
.icon("untrim")
.params("planned: face: {body, point}; holes?: all|[[x,y,z]] (points on the inner loops to remove); outer?: bool (restore the natural bounds); in_place?: bool (a surface body only)"),
planned!("surface.unstitch", "Unstitch", "766")
.at("SURFACE", "MODIFY")
.icon("unstitch")
.params("planned: body; faces?: [[x,y,z]] (points on the faces to split off; default: every face its own surface body)"),
planned!("surface.reverse_normal", "Reverse Normal", "766")
.at("SURFACE", "MODIFY")
.icon("reverse_normal")
.params("planned: body (a surface); faces?: [[x,y,z]] (default: all of them)"),
planned!("surface.fillet", "Surface Fillet", "769")
.at("SURFACE", "MODIFY")
.icon("surface_fillet")
.params("planned: a, b: {body, point} (two faces of separate bodies that meet or cross; the points pick the side the ball rolls on); radius: expr; trim?: both|first|none"),
];
fn patch(s: &mut Session, p: &Value) -> Result<Value> {
@@ -129,6 +177,43 @@ mod tests {
solidcraft_kernel::measure(&st.body(name).unwrap().body).unwrap().volume
}
/// The planned surface commands are registered but refuse to run: disabled with the
/// issue that tracks them, an error (never a panic) from every caller, and the design left
/// as it was.
#[test]
fn planned_commands_refuse_and_leave_the_design_alone() {
let planned = [
"surface.extrude",
"surface.revolve",
"surface.sweep",
"surface.loft",
"surface.offset",
"surface.ruled",
"surface.boundary",
"surface.blend",
"surface.untrim",
"surface.unstitch",
"surface.reverse_normal",
"surface.fillet",
"inspect.continuity",
];
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "body_name": "B"}));
let before = s.doc.features.len();
for id in planned {
let spec = crate::find_command(id).unwrap_or_else(|| panic!("{id} is not registered"));
let info = spec.info(&s);
assert!(!info.enabled, "{id} must stay disabled until it is built");
let why = info.disabled_reason.unwrap_or_default();
assert!(why.starts_with("not supported yet (issue #"), "{id}: {why}");
assert!(!spec.icon.is_empty() && !spec.params.is_empty(), "{id}: icon and params doc");
assert!(s.execute(id, &json!({"body": "B"})).is_err(), "{id} ran");
let e = (spec.run)(&mut s, &json!({})).unwrap_err().to_string();
assert!(e.starts_with("not supported yet: ") && e.contains("(issue #"), "{id}: {e}");
}
assert_eq!(s.doc.features.len(), before, "a planned command changed the design");
}
/// A rectangle patched, trimmed by a plane and thickened; a box's open top patched; a rim
/// that is not planar patched.
#[test]