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:
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> {
|
||||
|
||||
+284
-18
@@ -1,7 +1,32 @@
|
||||
//! Toolbar and browser icons, drawn in code (no image assets). Each icon is designed on a
|
||||
//! 24 × 24 grid and scaled to the target rectangle.
|
||||
|
||||
use egui::{Color32, Painter, Pos2, Rect, Shape, Stroke, pos2, vec2};
|
||||
use egui::{Color32, Mesh, Painter, Pos2, Rect, Shape, Stroke, pos2, vec2};
|
||||
|
||||
/// Surface sheets: a sheet, a second sheet beside it, a lighter sheet (an opening, a blend or
|
||||
/// round strip), and the iso lines and comb teeth drawn on sheets.
|
||||
const SHEET: Color32 = Color32::from_rgb(170, 200, 240);
|
||||
const SHEET_2: Color32 = Color32::from_rgb(150, 185, 235);
|
||||
const SHEET_LIGHT: Color32 = Color32::from_rgb(214, 229, 248);
|
||||
const SHEET_LINE: Color32 = Color32::from_rgb(127, 166, 218);
|
||||
|
||||
/// `n + 1` points along the cubic Bézier curve with control points `c`.
|
||||
fn bezier(c: [(f32, f32); 4], n: usize) -> Vec<(f32, f32)> {
|
||||
(0..=n)
|
||||
.map(|i| i as f32 / n as f32)
|
||||
.map(|t| {
|
||||
let u = 1.0 - t;
|
||||
let w = [u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t];
|
||||
(w.iter().zip(&c).map(|(w, q)| w * q.0).sum(), w.iter().zip(&c).map(|(w, q)| w * q.1).sum())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// `n + 1` points along an ellipse from angle `a0` to `a1` (y down: angles below the centre
|
||||
/// first).
|
||||
fn ellipse(x: f32, y: f32, rx: f32, ry: f32, a0: f32, a1: f32, n: usize) -> Vec<(f32, f32)> {
|
||||
(0..=n).map(|i| a0 + (a1 - a0) * i as f32 / n as f32).map(|a| (x + rx * a.cos(), y + ry * a.sin())).collect()
|
||||
}
|
||||
|
||||
struct Pen<'a> {
|
||||
p: &'a Painter,
|
||||
@@ -60,6 +85,30 @@ impl Pen<'_> {
|
||||
self.poly(&side, self.fill.gamma_multiply(0.7), self.ink);
|
||||
self.poly(&top, self.fill, self.ink);
|
||||
}
|
||||
/// A cubic Bézier curve through control points `c`.
|
||||
fn curve(&self, c: [(f32, f32); 4], col: Color32) {
|
||||
self.line(&bezier(c, 16), col);
|
||||
}
|
||||
fn dashed(&self, pts: &[(f32, f32)], c: Color32) {
|
||||
let v: Vec<Pos2> = pts.iter().map(|(x, y)| self.at(*x, *y)).collect();
|
||||
self.p.add(Shape::dashed_line(&v, Stroke::new(self.w, c), 2.0 * self.s(), 1.6 * self.s()));
|
||||
}
|
||||
/// Fill the strip between two polylines that run the same way, such as the front and back
|
||||
/// edges of a curved sheet. Unlike `poly`, the strip need not be convex.
|
||||
fn band(&self, a: &[(f32, f32)], b: &[(f32, f32)], fill: Color32) {
|
||||
let mut m = Mesh::default();
|
||||
for (p, q) in a.iter().zip(b) {
|
||||
m.colored_vertex(self.at(p.0, p.1), fill);
|
||||
m.colored_vertex(self.at(q.0, q.1), fill);
|
||||
}
|
||||
let n = a.len().min(b.len()) as u32;
|
||||
for i in 1..n {
|
||||
let k = 2 * i;
|
||||
m.add_triangle(k - 2, k - 1, k);
|
||||
m.add_triangle(k - 1, k + 1, k);
|
||||
}
|
||||
self.p.add(Shape::mesh(m));
|
||||
}
|
||||
}
|
||||
|
||||
/// The stroke width for the Normal/Construction/Centerline line-type icons. It is heavier than
|
||||
@@ -858,22 +907,33 @@ pub fn paint(p: &Painter, r: Rect, name: &str, ink: Color32, fill: Color32, acce
|
||||
}
|
||||
"patch" => {
|
||||
// A wire loop with a surface spanned across it.
|
||||
pen.poly(&[(4.0, 16.0), (9.0, 6.0), (19.0, 5.0), (20.0, 15.0), (12.0, 20.0)], Color32::from_rgb(170, 200, 240), ink);
|
||||
pen.poly(&[(4.0, 16.0), (9.0, 6.0), (19.0, 5.0), (20.0, 15.0), (12.0, 20.0)], SHEET, ink);
|
||||
pen.line(&[(4.0, 16.0), (9.0, 6.0), (19.0, 5.0), (20.0, 15.0), (12.0, 20.0), (4.0, 16.0)], a);
|
||||
}
|
||||
"stitch" => {
|
||||
// Two surface patches sewn along their shared edge.
|
||||
pen.poly(&[(3.0, 18.0), (11.0, 14.0), (11.0, 4.0), (3.0, 8.0)], Color32::from_rgb(170, 200, 240), ink);
|
||||
pen.poly(&[(13.0, 14.0), (21.0, 18.0), (21.0, 8.0), (13.0, 4.0)], Color32::from_rgb(150, 185, 235), ink);
|
||||
pen.poly(&[(3.0, 18.0), (11.0, 14.0), (11.0, 4.0), (3.0, 8.0)], SHEET, ink);
|
||||
pen.poly(&[(13.0, 14.0), (21.0, 18.0), (21.0, 8.0), (13.0, 4.0)], SHEET_2, ink);
|
||||
for y in [6.0, 9.0, 12.0] {
|
||||
pen.line(&[(10.0, y), (14.0, y + 1.5)], a);
|
||||
}
|
||||
}
|
||||
"thicken" => {
|
||||
// A curved sheet given thickness: its offset copy and the side walls.
|
||||
pen.arc(12.0, 20.0, 11.0, 3.6, 5.8, ink);
|
||||
pen.arc(12.0, 20.0, 7.0, 3.6, 5.8, a);
|
||||
pen.arrow((12.0, 9.0), (12.0, 13.5), a);
|
||||
// A wavy sheet (accent edges) grown downward into a slab, shaded like `iso_box`.
|
||||
let f = [(2.0, 12.5), (7.0, 9.5), (12.0, 15.5), (17.0, 12.5)];
|
||||
let b = [(6.4, 8.9), (11.4, 5.9), (16.4, 11.9), (21.4, 8.9)];
|
||||
let d = f.map(|(x, y)| (x, y + 6.0));
|
||||
pen.band(&bezier(b, 16), &bezier(f, 16), SHEET);
|
||||
pen.band(&bezier(f, 16), &bezier(d, 16), pen.fill.gamma_multiply(0.85));
|
||||
pen.poly(&[(17.0, 12.5), (21.4, 8.9), (21.4, 14.9), (17.0, 18.5)], pen.fill.gamma_multiply(0.7), ink);
|
||||
pen.line(&[(2.0, 12.5), (2.0, 18.5)], ink);
|
||||
pen.line(&[(17.0, 12.5), (17.0, 18.5)], ink);
|
||||
pen.line(&[(21.4, 8.9), (21.4, 14.9)], ink);
|
||||
pen.curve(d, ink);
|
||||
pen.line(&[(6.4, 8.9), (2.0, 12.5)], ink);
|
||||
pen.line(&[(21.4, 8.9), (17.0, 12.5)], a);
|
||||
pen.curve(b, a);
|
||||
pen.curve(f, a);
|
||||
}
|
||||
"extend" => {
|
||||
pen.line(&[(20.0, 3.0), (20.0, 21.0)], ink);
|
||||
@@ -896,19 +956,191 @@ pub fn paint(p: &Painter, r: Rect, name: &str, ink: Color32, fill: Color32, acce
|
||||
"blend" => {
|
||||
pen.line(&[(2.0, 19.0), (8.0, 15.0)], ink);
|
||||
pen.line(&[(16.0, 9.0), (22.0, 5.0)], ink);
|
||||
let c = [(8.0_f32, 15.0_f32), (12.5, 12.0), (11.5, 12.0), (16.0, 9.0)];
|
||||
let pts: Vec<(f32, f32)> = (0..=16)
|
||||
.map(|i| i as f32 / 16.0)
|
||||
.map(|t| {
|
||||
let u = 1.0 - t;
|
||||
let w = [u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t];
|
||||
(w.iter().zip(&c).map(|(w, q)| w * q.0).sum(), w.iter().zip(&c).map(|(w, q)| w * q.1).sum())
|
||||
})
|
||||
.collect();
|
||||
pen.line(&pts, a);
|
||||
pen.curve([(8.0, 15.0), (12.5, 12.0), (11.5, 12.0), (16.0, 9.0)], a);
|
||||
pen.dot(8.0, 15.0, ink);
|
||||
pen.dot(16.0, 9.0, ink);
|
||||
}
|
||||
// Surface modelling: open sheets (no caps) in the sheet colours, the input or the result
|
||||
// in the accent.
|
||||
"surface_extrude" => {
|
||||
// An open profile (accent) pulled up into a sheet.
|
||||
let top = [(4.0, 11.0), (8.0, 8.0), (13.0, 14.0), (20.0, 10.0)];
|
||||
let bottom = [(4.0, 20.0), (8.0, 17.0), (13.0, 23.0), (20.0, 19.0)];
|
||||
pen.band(&bezier(top, 16), &bezier(bottom, 16), SHEET);
|
||||
pen.line(&[(4.0, 11.0), (4.0, 20.0)], ink);
|
||||
pen.line(&[(20.0, 10.0), (20.0, 19.0)], ink);
|
||||
pen.curve(top, ink);
|
||||
pen.curve(bottom, a);
|
||||
pen.arrow((12.0, 8.5), (12.0, 2.5), a);
|
||||
}
|
||||
"surface_revolve" => {
|
||||
// An open profile (accent) turned about the axis into a hollow band.
|
||||
let left = [(5.0, 6.0), (7.0, 10.0), (5.0, 14.0), (6.0, 18.0)];
|
||||
let right = [(19.0, 6.0), (17.0, 10.0), (19.0, 14.0), (18.0, 18.0)];
|
||||
pen.band(&bezier(left, 16), &bezier(right, 16), SHEET);
|
||||
let front = ellipse(12.0, 18.0, 6.0, 2.2, 0.0, std::f32::consts::PI, 16);
|
||||
pen.poly(&front, SHEET, Color32::TRANSPARENT);
|
||||
// A closed fill takes each point once: a repeated end point spikes `poly`'s edge.
|
||||
let mut opening = ellipse(12.0, 6.0, 7.0, 2.2, 0.0, std::f32::consts::TAU, 32);
|
||||
opening.pop();
|
||||
pen.poly(&opening, SHEET_LIGHT, ink);
|
||||
pen.line(&front, ink);
|
||||
pen.curve(left, ink);
|
||||
pen.line(&[(12.0, 1.5), (12.0, 3.8)], ink);
|
||||
pen.line(&[(12.0, 20.2), (12.0, 22.5)], ink);
|
||||
pen.curve(right, a);
|
||||
}
|
||||
"surface_sweep" => {
|
||||
// A straight profile (accent) carried along a curved path: a ribbon.
|
||||
let near = [(4.0, 13.0), (8.0, 5.0), (14.0, 5.0), (20.0, 9.0)];
|
||||
let far = [(4.0, 20.0), (8.0, 12.0), (14.0, 12.0), (20.0, 16.0)];
|
||||
pen.band(&bezier(near, 16), &bezier(far, 16), SHEET);
|
||||
pen.curve(near, ink);
|
||||
pen.curve(far, ink);
|
||||
pen.line(&[(4.0, 13.0), (4.0, 20.0)], a);
|
||||
pen.line(&[(20.0, 9.0), (20.0, 16.0)], ink);
|
||||
}
|
||||
"surface_loft" => {
|
||||
// Two open sections (accent) joined by a sheet.
|
||||
let top = [(5.0, 6.0), (9.0, 3.0), (14.0, 8.0), (19.0, 5.0)];
|
||||
let bottom = [(3.0, 19.0), (8.0, 15.0), (14.0, 22.0), (21.0, 18.0)];
|
||||
pen.band(&bezier(top, 16), &bezier(bottom, 16), SHEET);
|
||||
pen.line(&[(5.0, 6.0), (3.0, 19.0)], ink);
|
||||
pen.line(&[(19.0, 5.0), (21.0, 18.0)], ink);
|
||||
pen.curve(top, a);
|
||||
pen.curve(bottom, a);
|
||||
}
|
||||
"surface_offset" => {
|
||||
// A wavy sheet and its offset copy (dashed), the distance shown by the arrow.
|
||||
let back = [(6.0, 14.0), (10.0, 11.0), (15.0, 17.0), (21.0, 14.0)];
|
||||
let front = [(3.0, 20.0), (7.0, 17.0), (12.0, 23.0), (18.0, 20.0)];
|
||||
pen.band(&bezier(back, 16), &bezier(front, 16), SHEET);
|
||||
pen.curve(back, ink);
|
||||
pen.curve(front, ink);
|
||||
pen.line(&[(6.0, 14.0), (3.0, 20.0)], ink);
|
||||
pen.line(&[(21.0, 14.0), (18.0, 20.0)], ink);
|
||||
pen.dashed(&bezier(back.map(|(x, y)| (x, y - 9.0)), 16), a);
|
||||
pen.dashed(&bezier(front.map(|(x, y)| (x, y - 9.0)), 16), a);
|
||||
pen.dashed(&[(6.0, 5.0), (3.0, 11.0)], a);
|
||||
pen.dashed(&[(21.0, 5.0), (18.0, 11.0)], a);
|
||||
pen.arrow((12.0, 17.0), (12.0, 8.5), a);
|
||||
}
|
||||
"ruled" => {
|
||||
// A strip hanging off a block's edge (accent), ruled by straight lines.
|
||||
pen.poly(&[(2.0, 12.0), (12.0, 12.0), (12.0, 22.0), (2.0, 22.0)], pen.fill, ink);
|
||||
pen.poly(&[(12.0, 12.0), (21.0, 4.0), (21.0, 14.0), (12.0, 22.0)], SHEET, ink);
|
||||
pen.line(&[(15.0, 9.3), (15.0, 19.3)], a);
|
||||
pen.line(&[(18.0, 6.7), (18.0, 16.7)], a);
|
||||
pen.line(&[(12.0, 12.0), (12.0, 22.0)], a);
|
||||
}
|
||||
"boundary" => {
|
||||
// Four boundary curves (accent) meeting at corners, the patch between them a Coons
|
||||
// patch with its iso lines.
|
||||
let (c00, c10, c11, c01) = ((4.0, 17.0), (10.0, 5.0), (20.0, 7.0), (17.0, 20.0));
|
||||
let left = [c00, (5.0, 12.0), (8.0, 8.0), c10];
|
||||
let top = [c10, (13.0, 4.0), (17.0, 4.0), c11];
|
||||
let right = [c01, (19.0, 16.0), (20.0, 11.0), c11];
|
||||
let bottom = [c00, (8.0, 20.0), (13.0, 21.0), c01];
|
||||
let n = 12;
|
||||
let lerp = |p: (f32, f32), q: (f32, f32), s: f32| (p.0 + (q.0 - p.0) * s, p.1 + (q.1 - p.1) * s);
|
||||
let (b, t) = (bezier(bottom, n), bezier(top, n));
|
||||
// Each row runs across the patch (u) at one height (v) up the left and right sides.
|
||||
let rows: Vec<Vec<(f32, f32)>> = bezier(left, n)
|
||||
.into_iter()
|
||||
.zip(bezier(right, n))
|
||||
.enumerate()
|
||||
.map(|(j, (lv, rv))| {
|
||||
let v = j as f32 / n as f32;
|
||||
b.iter()
|
||||
.zip(&t)
|
||||
.enumerate()
|
||||
.map(|(i, (bu, tu))| {
|
||||
let u = i as f32 / n as f32;
|
||||
let corners = lerp(lerp(c00, c01, u), lerp(c10, c11, u), v);
|
||||
let (ruled_v, ruled_u) = (lerp(*bu, *tu, v), lerp(lv, rv, u));
|
||||
(ruled_v.0 + ruled_u.0 - corners.0, ruled_v.1 + ruled_u.1 - corners.1)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
for (lo, hi) in rows.iter().zip(rows.iter().skip(1)) {
|
||||
pen.band(lo, hi, SHEET);
|
||||
}
|
||||
for j in [n / 3, 2 * n / 3] {
|
||||
if let Some(r) = rows.get(j) {
|
||||
pen.line(r, SHEET_LINE);
|
||||
}
|
||||
pen.line(&rows.iter().filter_map(|r| r.get(j).copied()).collect::<Vec<_>>(), SHEET_LINE);
|
||||
}
|
||||
for c in [left, top, right, bottom] {
|
||||
pen.curve(c, a);
|
||||
}
|
||||
for (x, y) in [c00, c10, c11, c01] {
|
||||
pen.dot(x, y, ink);
|
||||
}
|
||||
}
|
||||
"surface_blend" => {
|
||||
// Two separate sheets at different heights bridged by a smooth strip (accent) that
|
||||
// leaves both edges tangent.
|
||||
let near = [(13.0, 20.0), (8.5, 20.0), (10.5, 11.0), (6.0, 11.0)];
|
||||
let far = near.map(|(x, y)| (x + 4.4, y - 3.6));
|
||||
pen.poly(&[(2.0, 11.0), (6.0, 11.0), (10.4, 7.4), (6.4, 7.4)], SHEET, ink);
|
||||
pen.band(&bezier(near, 16), &bezier(far, 16), SHEET_LIGHT);
|
||||
pen.curve(far, a);
|
||||
pen.poly(&[(13.0, 20.0), (17.0, 20.0), (21.4, 16.4), (17.4, 16.4)], SHEET, ink);
|
||||
pen.curve(near, a);
|
||||
}
|
||||
"untrim" => {
|
||||
// A trimmed sheet whose hole (dashed, accent) is filled back in.
|
||||
pen.poly(&[(3.0, 17.0), (9.0, 5.0), (21.0, 7.0), (16.0, 20.0)], SHEET, ink);
|
||||
let hole = ellipse(12.2, 12.3, 4.2, 3.2, 0.0, std::f32::consts::TAU, 32);
|
||||
pen.poly(hole.get(1..).unwrap_or_default(), SHEET_LIGHT, Color32::TRANSPARENT);
|
||||
pen.dashed(&hole, a);
|
||||
}
|
||||
"unstitch" => {
|
||||
// Two sheets pulled apart from their shared edge.
|
||||
pen.poly(&[(2.0, 18.0), (9.0, 14.0), (9.0, 4.0), (2.0, 8.0)], SHEET, ink);
|
||||
pen.poly(&[(15.0, 14.0), (22.0, 18.0), (22.0, 8.0), (15.0, 4.0)], SHEET_2, ink);
|
||||
pen.arrow((10.8, 11.0), (7.0, 11.0), a);
|
||||
pen.arrow((13.2, 11.0), (17.0, 11.0), a);
|
||||
}
|
||||
"reverse_normal" => {
|
||||
// A sheet with its old normal (faded) and the flipped one (accent).
|
||||
pen.poly(&[(2.0, 14.0), (14.0, 10.0), (22.0, 13.0), (10.0, 18.0)], SHEET, ink);
|
||||
pen.arrow((12.0, 14.0), (12.0, 3.5), ink.gamma_multiply(0.5));
|
||||
pen.arrow((12.0, 14.0), (12.0, 22.5), a);
|
||||
}
|
||||
"surface_fillet" => {
|
||||
// A wall sheet and a floor sheet joined by a round strip (accent edges).
|
||||
let near = [(3.0, 16.0), (3.0, 18.8), (5.2, 21.0), (8.0, 21.0)];
|
||||
let far = near.map(|(x, y)| (x + 4.4, y - 3.6));
|
||||
pen.poly(&[(3.0, 16.0), (3.0, 8.0), (7.4, 4.4), (7.4, 12.4)], SHEET_2, ink);
|
||||
pen.poly(&[(8.0, 21.0), (16.0, 21.0), (20.4, 17.4), (12.4, 17.4)], SHEET, ink);
|
||||
pen.band(&bezier(near, 16), &bezier(far, 16), SHEET_LIGHT);
|
||||
pen.line(&[(4.5, 19.5), (8.9, 15.9)], SHEET_LINE);
|
||||
pen.curve(far, a);
|
||||
pen.curve(near, a);
|
||||
}
|
||||
"continuity" => {
|
||||
// A straight curve joining a curved one (accent) at a point, with the curved one's
|
||||
// curvature comb: teeth that grow as the curve tightens.
|
||||
pen.line(&[(2.0, 17.0), (12.0, 13.0)], ink);
|
||||
let c = [(12.0, 13.0), (16.5, 11.2), (20.0, 8.0), (21.0, 3.0)];
|
||||
let pts = bezier(c, 10);
|
||||
let mut tips = Vec::new();
|
||||
for (i, ((x, y), (x1, y1))) in pts.iter().zip(pts.iter().skip(1)).enumerate() {
|
||||
let len = (x1 - x).hypot(y1 - y).max(1e-6);
|
||||
let h = 0.8 + 4.2 * i as f32 / 10.0;
|
||||
let tip = (x + (y1 - y) / len * h, y - (x1 - x) / len * h);
|
||||
if i % 2 == 0 {
|
||||
pen.line(&[(*x, *y), tip], SHEET_LINE);
|
||||
}
|
||||
tips.push(tip);
|
||||
}
|
||||
pen.line(&tips, SHEET_LINE);
|
||||
pen.curve(c, a);
|
||||
pen.dot(12.0, 13.0, a);
|
||||
}
|
||||
// Assembly: joints (accent: the joint itself).
|
||||
"joint" => {
|
||||
pen.iso_box(2.0, 13.0, 7.0, 5.0, 5.0);
|
||||
@@ -1083,6 +1315,40 @@ mod tests {
|
||||
format!("{:?}", out.shapes)
|
||||
}
|
||||
|
||||
/// Command icons not drawn yet (the fallback square) or drawn past their square, from before
|
||||
/// this check. Remove a name when its icon is fixed; never add one.
|
||||
const KNOWN_GAPS: &[&str] =
|
||||
&["cylinder", "pipe", "offset_face", "thread", "boundary_fill", "coil", "rib", "emboss", "capture", "revert", "camera", "material"];
|
||||
|
||||
/// What is wrong with the icon `name`: not drawn (the fallback square), or drawn past its
|
||||
/// square by more than a stroke width.
|
||||
fn icon_problem(name: &str) -> Option<String> {
|
||||
if drawn(name, Color32::BLACK, Color32::WHITE, Color32::BLUE) == drawn("", Color32::BLACK, Color32::WHITE, Color32::BLUE) {
|
||||
return Some("no icon drawn".into());
|
||||
}
|
||||
let side = 48.0;
|
||||
let r = Rect::from_min_size(Pos2::ZERO, vec2(side, side));
|
||||
let ctx = egui::Context::default();
|
||||
let input = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(100.0, 100.0))), ..Default::default() };
|
||||
let out = ctx.run_ui(input, |ui| paint(ui.painter(), r, name, Color32::BLACK, Color32::WHITE, Color32::BLUE));
|
||||
let used = out.shapes.iter().fold(Rect::NOTHING, |b, s| b.union(s.shape.visual_bounding_rect()));
|
||||
(!r.expand(side / 24.0 * 1.4).contains_rect(used)).then(|| format!("drawn over {used:?}"))
|
||||
}
|
||||
|
||||
/// Every command's icon is drawn and stays inside its square.
|
||||
#[test]
|
||||
fn every_command_icon_is_drawn_inside_its_square() {
|
||||
let bad: Vec<String> = solidcraft_engine::command_specs()
|
||||
.into_iter()
|
||||
.filter(|c| !c.icon.is_empty() && !KNOWN_GAPS.contains(&c.icon))
|
||||
.filter_map(|c| icon_problem(c.icon).map(|why| format!("{} ({}): {why}", c.id, c.icon)))
|
||||
.collect();
|
||||
assert!(bad.is_empty(), "{}", bad.join("\n"));
|
||||
for gap in KNOWN_GAPS {
|
||||
assert!(icon_problem(gap).is_some(), "`{gap}` is fixed now: take it off KNOWN_GAPS");
|
||||
}
|
||||
}
|
||||
|
||||
/// The Sketch Palette and the toolbar both rely on this contract. Construction and
|
||||
/// centerline must track whatever accent colour the caller passes (both callers pass the
|
||||
/// theme's construction orange) and must not change with `ink`.
|
||||
|
||||
+16
-3
@@ -1,6 +1,6 @@
|
||||
# Features
|
||||
|
||||
SolidCraft's commands by area, generated by `cargo xtask features` from the command registry (`solidcraft-cli commands`). **335 commands.** Every command runs the same way from the toolbar, a script, `solidcraft-cli exec <id> '<json>'`, the control channel and MCP; parameters are in the [command reference](book/src/reference/commands.md) (`cargo xtask book`). Status and plans: [ROADMAP](../ROADMAP.md).
|
||||
SolidCraft's commands by area, generated by `cargo xtask features` from the command registry (`solidcraft-cli commands`). **335 commands.** Planned (registered, not built yet): 13. Every command runs the same way from the toolbar, a script, `solidcraft-cli exec <id> '<json>'`, the control channel and MCP; parameters are in the [command reference](book/src/reference/commands.md) (`cargo xtask book`). Status and plans: [ROADMAP](../ROADMAP.md).
|
||||
|
||||
## Sketch (98)
|
||||
|
||||
@@ -145,7 +145,7 @@ SolidCraft's commands by area, generated by `cargo xtask features` from the comm
|
||||
| Align | `solid.align` | SOLID › MODIFY |
|
||||
| Remove | `solid.remove` | SOLID › MODIFY |
|
||||
|
||||
## Surfaces (5)
|
||||
## Surfaces (5, 12 planned)
|
||||
|
||||
| Command | Id | Toolbar |
|
||||
|---|---|---|
|
||||
@@ -154,6 +154,18 @@ SolidCraft's commands by area, generated by `cargo xtask features` from the comm
|
||||
| Thicken | `surface.thicken` | SURFACE › CREATE |
|
||||
| Extend | `surface.extend` | SURFACE › MODIFY |
|
||||
| Trim | `surface.trim` | SURFACE › MODIFY |
|
||||
| Surface Extrude *(planned: [#757](https://github.com/bherbruck/solidcraft/issues/757))* | `surface.extrude` | SURFACE › CREATE |
|
||||
| Surface Revolve *(planned: [#757](https://github.com/bherbruck/solidcraft/issues/757))* | `surface.revolve` | SURFACE › CREATE |
|
||||
| Surface Sweep *(planned: [#757](https://github.com/bherbruck/solidcraft/issues/757))* | `surface.sweep` | SURFACE › CREATE |
|
||||
| Surface Loft *(planned: [#757](https://github.com/bherbruck/solidcraft/issues/757))* | `surface.loft` | SURFACE › CREATE |
|
||||
| Offset Surface *(planned: [#758](https://github.com/bherbruck/solidcraft/issues/758))* | `surface.offset` | SURFACE › CREATE |
|
||||
| Ruled Surface *(planned: [#762](https://github.com/bherbruck/solidcraft/issues/762))* | `surface.ruled` | SURFACE › CREATE |
|
||||
| Boundary Surface *(planned: [#763](https://github.com/bherbruck/solidcraft/issues/763))* | `surface.boundary` | SURFACE › CREATE |
|
||||
| Blend Surface *(planned: [#764](https://github.com/bherbruck/solidcraft/issues/764))* | `surface.blend` | SURFACE › CREATE |
|
||||
| Untrim *(planned: [#761](https://github.com/bherbruck/solidcraft/issues/761))* | `surface.untrim` | SURFACE › MODIFY |
|
||||
| Unstitch *(planned: [#766](https://github.com/bherbruck/solidcraft/issues/766))* | `surface.unstitch` | SURFACE › MODIFY |
|
||||
| Reverse Normal *(planned: [#766](https://github.com/bherbruck/solidcraft/issues/766))* | `surface.reverse_normal` | SURFACE › MODIFY |
|
||||
| Surface Fillet *(planned: [#769](https://github.com/bherbruck/solidcraft/issues/769))* | `surface.fillet` | SURFACE › MODIFY |
|
||||
|
||||
## Construction geometry (19)
|
||||
|
||||
@@ -281,7 +293,7 @@ SolidCraft's commands by area, generated by `cargo xtask features` from the comm
|
||||
| Copy Features | `timeline.copy` | – |
|
||||
| Paste Features | `timeline.paste` | – |
|
||||
|
||||
## Inspect (12)
|
||||
## Inspect (12, 1 planned)
|
||||
|
||||
| Command | Id | Toolbar |
|
||||
|---|---|---|
|
||||
@@ -294,6 +306,7 @@ SolidCraft's commands by area, generated by `cargo xtask features` from the comm
|
||||
| Environment Map Analysis | `inspect.environment_map` | SKETCH › INSPECT |
|
||||
| Accessibility Analysis | `inspect.accessibility` | SKETCH › INSPECT |
|
||||
| Curvature Map Analysis | `inspect.curvature_map` | SKETCH › INSPECT |
|
||||
| Continuity Analysis *(planned: [#770](https://github.com/bherbruck/solidcraft/issues/770))* | `inspect.continuity` | SURFACE › INSPECT |
|
||||
| Interference | `inspect.interference` | SOLID › INSPECT |
|
||||
| Measure (I) | `inspect.measure` | SOLID › INSPECT |
|
||||
| Section Analysis | `inspect.section` | SOLID › INSPECT |
|
||||
|
||||
+24
-9
@@ -3,7 +3,8 @@
|
||||
//! The page lists every registered command by area (the first part of its dotted id), with its
|
||||
//! label, id and toolbar place. It comes only from SolidCraft's command registry
|
||||
//! (`solidcraft-cli commands`), so it can't drift from what the program does; it compares with
|
||||
//! nothing else.
|
||||
//! nothing else. Planned commands (registered, not built yet) are listed as planned, with their
|
||||
//! issue, and are not counted.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
@@ -59,16 +60,26 @@ pub fn page(cmds: &[Value]) -> String {
|
||||
for p in prefixes.iter().filter(|p| !AREAS.iter().any(|(a, _)| a == p)) {
|
||||
order.push((p.clone(), format!("`{p}.*`")));
|
||||
}
|
||||
let planned = |c: &Value| c["planned"].as_str().map(str::to_string);
|
||||
let built = cmds.iter().filter(|c| planned(c).is_none()).count();
|
||||
let later = match cmds.len() - built {
|
||||
0 => String::new(),
|
||||
n => format!(" Planned (registered, not built yet): {n}."),
|
||||
};
|
||||
let mut md = format!(
|
||||
"# Features\n\nSolidCraft's commands by area, generated by `cargo xtask features` from the command registry \
|
||||
(`solidcraft-cli commands`). **{} commands.** Every command runs the same way from the toolbar, a script, \
|
||||
(`solidcraft-cli commands`). **{built} commands.**{later} Every command runs the same way from the toolbar, a script, \
|
||||
`solidcraft-cli exec <id> '<json>'`, the control channel and MCP; parameters are in the \
|
||||
[command reference](book/src/reference/commands.md) (`cargo xtask book`). Status and plans: [ROADMAP](../ROADMAP.md).\n",
|
||||
cmds.len()
|
||||
[command reference](book/src/reference/commands.md) (`cargo xtask book`). Status and plans: [ROADMAP](../ROADMAP.md).\n"
|
||||
);
|
||||
for (prefix, heading) in &order {
|
||||
let in_area: Vec<&Value> = cmds.iter().filter(|c| area(&s(c, "id")) == prefix).collect();
|
||||
md += &format!("\n## {heading} ({})\n\n| Command | Id | Toolbar |\n|---|---|---|\n", in_area.len());
|
||||
let n = in_area.iter().filter(|c| planned(c).is_none()).count();
|
||||
let count = match in_area.len() - n {
|
||||
0 => n.to_string(),
|
||||
p => format!("{n}, {p} planned"),
|
||||
};
|
||||
md += &format!("\n## {heading} ({count})\n\n| Command | Id | Toolbar |\n|---|---|---|\n");
|
||||
for c in in_area {
|
||||
let place = match (s(c, "tab"), s(c, "panel")) {
|
||||
(t, _) if t.is_empty() => "–".to_string(),
|
||||
@@ -76,7 +87,8 @@ pub fn page(cmds: &[Value]) -> String {
|
||||
(t, p) => format!("{t} › {p}"),
|
||||
};
|
||||
let key = c["shortcut"].as_str().map(|k| format!(" ({k})")).unwrap_or_default();
|
||||
md += &format!("| {}{key} | `{}` | {place} |\n", s(c, "label").replace('|', "/"), s(c, "id"));
|
||||
let note = planned(c).map(|i| format!(" *(planned: [#{i}](https://github.com/bherbruck/solidcraft/issues/{i}))*")).unwrap_or_default();
|
||||
md += &format!("| {}{key}{note} | `{}` | {place} |\n", s(c, "label").replace('|', "/"), s(c, "id"));
|
||||
}
|
||||
}
|
||||
md
|
||||
@@ -91,14 +103,17 @@ mod tests {
|
||||
let cmds: Value = serde_json::from_str(
|
||||
r#"[{"id":"solid.extrude","label":"Extrude","tab":"SOLID","panel":"CREATE","shortcut":"E"},
|
||||
{"id":"sketch.line","label":"Line","tab":"SKETCH","panel":"CREATE"},
|
||||
{"id":"view.fit","label":"Fit","tab":"","panel":""}]"#,
|
||||
{"id":"view.fit","label":"Fit","tab":"","panel":""},
|
||||
{"id":"solid.knit","label":"Knit","tab":"SOLID","panel":"CREATE","planned":"9"}]"#,
|
||||
)
|
||||
.unwrap();
|
||||
let md = page(cmds.as_array().unwrap());
|
||||
assert!(md.contains("**3 commands.**"));
|
||||
let (sk, so, other) = (md.find("## Sketch (1)").unwrap(), md.find("## Solid modelling (1)").unwrap(), md.find("## `view.*` (1)").unwrap());
|
||||
assert!(md.contains("**3 commands.** Planned (registered, not built yet): 1."));
|
||||
let (sk, so, other) =
|
||||
(md.find("## Sketch (1)").unwrap(), md.find("## Solid modelling (1, 1 planned)").unwrap(), md.find("## `view.*` (1)").unwrap());
|
||||
assert!(sk < so && so < other);
|
||||
assert!(md.contains("| Extrude (E) | `solid.extrude` | SOLID › CREATE |"));
|
||||
assert!(md.contains("| Fit | `view.fit` | – |"));
|
||||
assert!(md.contains("| Knit *(planned: [#9](https://github.com/bherbruck/solidcraft/issues/9))* | `solid.knit` | SOLID › CREATE |"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user