Transform dialogs: Rotate and Shear open their dialogs (#34)

A parameter written as `key (description)` was dropped by the command
form parser, so Object › Transform › Rotate… and Shear… (and Basic
Feather) had no required field, opened no dialog and ran with defaults.
The key is now the leading word and the parenthetical becomes its hint.
Double-clicking the Selection, Rotate, Scale or Shear tool opens Move,
Rotate, Scale or Shear.
This commit is contained in:
aiongg
2026-10-06 18:13:29 -04:00
parent d582277cc7
commit b6162dd11d
3 changed files with 47 additions and 4 deletions
+19 -3
View File
@@ -2380,10 +2380,14 @@ pub fn command_fields(doc: &str) -> Vec<CommandField> {
let ident = |s: &str| !s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') && !s.starts_with(|c: char| c.is_ascii_digit());
let mut out = Vec::new();
for part in parts {
// `key`, `key?`, `key: spec` or `key (spec)`: the key is the leading word.
let part = part.trim();
let (k, spec) = match part.split_once(':') {
Some((k, v)) => (k.trim(), v.trim()),
None => (part, ""),
let k_end = part.find(|c: char| c.is_whitespace() || c == ':' || c == '(').unwrap_or(part.len());
let (k, rest) = part.split_at(k_end);
let rest = rest.trim_start();
let spec = match rest.strip_prefix(':') {
Some(v) => v.trim(),
None => rest.strip_prefix('(').and_then(|r| r.strip_suffix(')')).unwrap_or(rest).trim(),
};
let optional = k.ends_with('?');
let key = k.trim_end_matches('?');
@@ -2643,6 +2647,18 @@ mod tests {
assert!(f[4].boolean);
assert!(command_fields("{}").is_empty());
assert!(command_fields("no params").is_empty());
// A parenthetical after the key describes the value.
let f = command_fields("{angle (degrees, CCW), ids?}");
let keys: Vec<&str> = f.iter().map(|f| f.key.as_str()).collect();
assert_eq!(keys, ["angle", "ids"]);
assert!(!f[0].optional && f[1].optional);
assert_eq!(f[0].hint, "degrees, CCW");
let f = command_fields("{scaleX? (%), ref?: 0..8, ids?: parent items (default: all)}");
let keys: Vec<&str> = f.iter().map(|f| f.key.as_str()).collect();
assert_eq!(keys, ["scaleX", "ref", "ids"]);
assert!(f.iter().all(|f| f.optional));
assert_eq!(f[0].hint, "%");
assert_eq!(f[2].hint, "parent items (default: all)");
// Every command with a "…" label parses without panicking.
for c in designcraft_engine::command_specs() {
let _ = command_fields(c.params);
+10
View File
@@ -1996,6 +1996,16 @@ mod tests {
}
}
#[test]
fn transform_menu_items_open_their_dialogs() {
for id in ["transform.move", "transform.scale", "transform.rotate", "transform.shear"] {
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
activate(&mut app, id, &Value::Null);
let dialog = app.ui.dialog.as_ref().map(|d| d.id.clone());
assert_eq!(dialog.as_deref(), Some(format!("cmd:{id}").as_str()), "{id} opens its dialog");
}
}
#[test]
fn snap_preferences_are_a_command() {
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
+18 -1
View File
@@ -3,7 +3,7 @@
use designcraft_tools::TOOL_GROUPS;
use egui::{Color32, Sense, Stroke, StrokeKind, vec2};
use serde_json::json;
use serde_json::{Value, json};
use crate::theme::Tokens;
use crate::{DesignApp, icons};
@@ -91,6 +91,12 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
let cur = app.session.execute("tool.polygonSettings", &json!({})).unwrap_or_default();
app.ui.dialog = Some(crate::dialogs::Dialog::new("polygonSettings", cur));
}
// Double-click a transform tool: its Object › Transform dialog.
if resp.double_clicked()
&& let Some(cmd) = transform_dialog_of(shown.id)
{
crate::menus::activate(app, cmd, &Value::Null);
}
if g.len() > 1
&& (resp.secondary_clicked()
|| resp.long_touched()
@@ -343,6 +349,17 @@ fn fill_stroke_proxy(app: &mut DesignApp, ui: &mut egui::Ui, width: f32) {
});
}
/// The Object › Transform command a double-clicked tool opens (the Selection tool opens Move).
fn transform_dialog_of(tool: &str) -> Option<&'static str> {
match tool {
"selection" => Some("transform.move"),
"rotate" => Some("transform.rotate"),
"scale" => Some("transform.scale"),
"shear" => Some("transform.shear"),
_ => None,
}
}
fn flyout_triangle(p: &egui::Painter, r: egui::Rect, c: Color32) {
let q = r.right_bottom() - vec2(2.0, 2.0);
p.add(egui::Shape::convex_polygon(vec![q, q - vec2(4.0, 0.0), q - vec2(0.0, 4.0)], c, Stroke::NONE));