Put Move/Copy Sketch Entities on the toolbar, behind a window with distances, an angle and a copy flag (#735)

Sketch Move gets a toolbar placement in Sketch > Modify and a Move/Copy tool window with X and Y distances, an angle, an optional centre point and a Create Copy flag, replacing the context-menu route that ran the command with no parameters.
This commit is contained in:
William Dunn authored and GitHub committed 2026-10-13 13:48:13 -04:00
1 parent de1479029d
commit facff1050e
10 files changed
+319 -8

No files matched your search

+62 -4
View File
@@ -6,8 +6,11 @@
//! issue #162 item 1: "Offset does not create a constraint, the size of the offset seems
//! random"). Mirror joins them here, which ran as a bare click flow: entities until a
//! right-click, then one more click for the line, with nothing on screen saying what was
//! picked. `sketch.fillet`, the three `sketch.chamfer.*` commands, the sketch patterns and
//! `sketch.move` are the same gap and join this module later, each its own change.
//! picked. Move joins them too, which had no toolbar placement, no tool and no dialog: its only
//! route into the UI was the sketch context menu, and that went through the same
//! empty-parameter fallback and reported an engine error. `sketch.fillet`, the three
//! `sketch.chamfer.*` commands and the sketch patterns are the same gap and join this module
//! later, each its own change.
use serde_json::{Map, Value, json};
use solidcraft_engine::Sel;
@@ -32,6 +35,10 @@ pub enum Mod {
/// its own, so the window's whole job is to show what is picked and let a pick be changed
/// before anything is committed.
Mirror,
/// `sketch.move`: how far the picked entities move along the sketch's own axes, how far they
/// turn about the centre, and whether the originals stay behind. The command already
/// carries the copy flag, so the Create Copy row needs no engine change.
Move { x: String, y: String, angle: String, copy: bool },
}
impl Mod {
@@ -40,6 +47,7 @@ impl Mod {
Mod::Scale { .. } => "SKETCH SCALE",
Mod::Offset { .. } => "OFFSET",
Mod::Mirror => "MIRROR",
Mod::Move { .. } => "MOVE/COPY",
}
}
@@ -48,6 +56,7 @@ impl Mod {
match self {
Mod::Scale { factor } => Some(("Factor", ValueKind::Unitless, factor)),
Mod::Offset { distance, .. } => Some(("Distance", ValueKind::Length, distance)),
Mod::Move { x, .. } => Some(("X Distance", ValueKind::Length, x)),
Mod::Mirror => None,
}
}
@@ -59,6 +68,7 @@ fn inputs(k: &Mod) -> Vec<SelInput> {
Mod::Offset { .. } => vec![SelInput::new("Curves", CURVES, true)],
// AXES is the mask that takes a straight sketch curve (`selection::SelInput::accepts`).
Mod::Mirror => vec![SelInput::new("Entities", CURVES | POINTS, true), SelInput::new("Mirror line", AXES, false)],
Mod::Move { .. } => vec![SelInput::new("Entities", CURVES | POINTS, true), SelInput::new("Center", POINTS, false)],
}
}
@@ -68,6 +78,7 @@ pub fn start(_app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
"sketch.scale" => Mod::Scale { factor: "2".into() },
"sketch.offset" => Mod::Offset { distance: "2 mm".into(), chain: true, flip: false },
"sketch.mirror" => Mod::Mirror,
"sketch.move" => Mod::Move { x: "0 mm".into(), y: "0 mm".into(), angle: "0 deg".into(), copy: false },
_ => return None,
};
let i = inputs(&k);
@@ -96,6 +107,20 @@ pub fn rows(ui: &mut egui::Ui, k: &mut Mod) -> bool {
}
// Mirror has no value of its own: its inputs are the whole window.
Mod::Mirror => {}
Mod::Move { x, y, angle, copy } => {
row_label(ui, "X Distance");
enter |= field(ui, x);
ui.end_row();
row_label(ui, "Y Distance");
enter |= field(ui, y);
ui.end_row();
row_label(ui, "Angle");
enter |= field(ui, angle);
ui.end_row();
row_label(ui, "Create Copy");
check(ui, copy);
ui.end_row();
}
}
enter
}
@@ -142,6 +167,22 @@ fn factor(app: &SolveApp, text: &str) -> Result<f64, String> {
Ok(v)
}
/// A typed field as the command wants it: evaluated against the document's parameters, so an
/// expression works, with a bare number read in the design's own units. An empty field is zero,
/// the way an untouched row should leave the geometry where it is.
fn value(app: &SolveApp, text: &str, kind: ValueKind, label: &str) -> Result<f64, String> {
let text = text.trim();
if text.is_empty() {
return Ok(0.0);
}
let doc = &app.session.doc;
let v = doc.eval(&doc.with_design_unit(text, kind), kind).map_err(|e| format!("{label}: {e}"))?;
if !v.is_finite() {
return Err(format!("{label} must be a number"));
}
Ok(v)
}
/// A typed distance as the command wants it: the field evaluated against the document's
/// parameters, with the sign reversed when Flip is set. A bare number means the design's units,
/// as a typed sketch value does everywhere else.
@@ -155,8 +196,8 @@ fn signed_length(app: &SolveApp, text: &str, flip: bool) -> Result<f64, String>
}
/// The command a sketch modify dialog makes. `sketch.scale` takes a plain evaluated number for
/// its factor and `sketch.offset` one for its distance, not an expression string: neither has a
/// model parameter to carry one, unlike `sketch.fillet`'s radius.
/// its factor, `sketch.offset` one for its distance and `sketch.move` its distances and angle,
/// not expression strings: none has a model parameter to carry one, unlike `sketch.fillet`'s radius.
pub fn commands(app: &SolveApp, k: &Mod, inputs: &[SelInput], extra: &Map<String, Value>) -> Result<Vec<(String, Value)>, String> {
let (cmd, mut params) = match k {
Mod::Scale { factor: text } => {
@@ -196,6 +237,23 @@ pub fn commands(app: &SolveApp, k: &Mod, inputs: &[SelInput], extra: &Map<String
}
("sketch.mirror", json!({"entities": entities, "line": line}))
}
Mod::Move { x, y, angle, copy } => {
let entities = entity_ids(inputs, 0);
if entities.is_empty() {
return Err("Select the sketch curves or points to move first".into());
}
let dx = value(app, x, ValueKind::Length, "X Distance")?;
let dy = value(app, y, ValueKind::Length, "Y Distance")?;
let deg = value(app, angle, ValueKind::Angle, "Angle")?.to_degrees();
if dx.abs() < 1e-9 && dy.abs() < 1e-9 && deg.abs() < 1e-9 {
return Err("Type a distance or an angle to move by".into());
}
let mut p = json!({"entities": entities, "translate": [dx, dy], "angle": deg, "copy": copy});
if let Some(center) = items(inputs, 1).first().and_then(point_id) {
p["center"] = json!(center);
}
("sketch.move", p)
}
};
if let Value::Object(m) = &mut params {
for (key, value) in extra {