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 <[email protected]>
This commit is contained in:
1 parent
dd084ec9e1
commit
c8d668108a
6 files changed
+437
-31
No files matched your search
@@ -9,6 +9,17 @@ macro_rules! created_doc {
|
||||
};
|
||||
}
|
||||
|
||||
/// A command that is designed and registered (id, toolbar place, icon, params doc) but not built
|
||||
/// yet. It is disabled with the reason, so the toolbar greys it and says why, and its `run`
|
||||
/// refuses too, however it is reached. `$issue` is the GitHub issue that tracks building it.
|
||||
macro_rules! planned {
|
||||
($id:literal, $label:literal, $issue:literal) => {
|
||||
CommandSpec::new($id, $label, |_, _| Err(crate::EngineError::Other(concat!("not supported yet: ", $label, " (issue #", $issue, ")").into())))
|
||||
.enabled(|_| Err(concat!("not supported yet (issue #", $issue, ")").into()))
|
||||
.planned($issue)
|
||||
};
|
||||
}
|
||||
|
||||
mod browser;
|
||||
mod clipboard;
|
||||
mod component;
|
||||
@@ -89,6 +100,8 @@ pub struct CommandSpec {
|
||||
/// Reads the evaluated model (the default). A command that reads only the document and the
|
||||
/// session's history runs on a model left stale by a live gesture without evaluating it first.
|
||||
pub reads_model: bool,
|
||||
/// Registered but not built yet (see `planned!`): the GitHub issue number that tracks it.
|
||||
pub planned: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl CommandSpec {
|
||||
@@ -106,8 +119,14 @@ impl CommandSpec {
|
||||
undoable: true,
|
||||
live: None,
|
||||
reads_model: true,
|
||||
planned: None,
|
||||
}
|
||||
}
|
||||
/// Not built yet; `issue` tracks building it.
|
||||
pub const fn planned(mut self, issue: &'static str) -> Self {
|
||||
self.planned = Some(issue);
|
||||
self
|
||||
}
|
||||
/// Takes `live: true`, run by `run`.
|
||||
pub const fn live(mut self, run: LiveRun) -> Self {
|
||||
self.live = Some(run);
|
||||
@@ -161,6 +180,7 @@ impl CommandSpec {
|
||||
params: self.params,
|
||||
enabled: e.is_ok(),
|
||||
disabled_reason: e.err(),
|
||||
planned: self.planned,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,6 +196,8 @@ pub struct CommandInfo {
|
||||
pub params: &'static str,
|
||||
pub enabled: bool,
|
||||
pub disabled_reason: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub planned: Option<&'static str>,
|
||||
}
|
||||
|
||||
pub fn always(_: &Session) -> std::result::Result<(), String> {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -34,6 +34,11 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.icon("curvature_map")
|
||||
.noundo()
|
||||
.params("radius?: mm shown red (default: a tenth of the model's size); clear?: true turns the analysis off"),
|
||||
planned!("inspect.continuity", "Continuity Analysis", "770")
|
||||
.at("SURFACE", "INSPECT")
|
||||
.icon("continuity")
|
||||
.noundo()
|
||||
.params("planned: body?; edges?: [[x,y,z]] (default: every edge of the body); returns each edge's worst gap (G0), normal angle (G1) and curvature jump (G2); clear?: true turns the analysis off"),
|
||||
];
|
||||
|
||||
fn set(s: &mut Session, p: &Value, a: SurfaceAnalysis) -> Result<Value> {
|
||||
|
||||
Reference in new issue
Block a user