mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-14 19:27:07 -04:00
Merge pull request #79 from aiongg/fix/34-transform-dialogs
Transform: Rotate and Shear open their dialogs (#34)
This commit is contained in:
4 files changed
+90
-5
No files matched your search
@@ -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);
|
||||
|
||||
@@ -147,13 +147,27 @@ pub fn paint(p: &Painter, r: Rect, name: &str, c: Color32) {
|
||||
pen.line(&[(7.5, 12.5), (14.0, 3.0)]);
|
||||
pen.line(&[(12.5, 12.5), (6.0, 3.0)]);
|
||||
}
|
||||
"tool-free-transform" | "tool-scale" | "tool-shear" => {
|
||||
"tool-free-transform" => {
|
||||
pen.rect(4.0, 4.0, 13.0, 13.0);
|
||||
pen.frect(2.8, 2.8, 5.2, 5.2);
|
||||
pen.frect(11.8, 11.8, 14.2, 14.2);
|
||||
pen.line(&[(14.0, 14.0), (18.0, 18.0)]);
|
||||
pen.line(&[(15.0, 18.0), (18.0, 18.0), (18.0, 15.0)]);
|
||||
}
|
||||
"tool-scale" => {
|
||||
// A small square grown into a larger one from their shared corner.
|
||||
Pen { p, r, c: c.gamma_multiply(0.45), w: 1.0 }.rect(3.0, 3.0, 17.0, 17.0);
|
||||
pen.rect(3.0, 10.0, 10.0, 17.0);
|
||||
pen.line(&[(10.0, 10.0), (15.0, 5.0)]);
|
||||
pen.line(&[(11.5, 5.0), (15.0, 5.0), (15.0, 8.5)]);
|
||||
}
|
||||
"tool-shear" => {
|
||||
// A rectangle slanted into a parallelogram along its base.
|
||||
Pen { p, r, c: c.gamma_multiply(0.45), w: 1.0 }.rect(3.0, 8.0, 13.0, 17.0);
|
||||
pen.closed(&[(7.0, 8.0), (17.0, 8.0), (13.0, 17.0), (3.0, 17.0)]);
|
||||
pen.line(&[(7.0, 4.0), (16.0, 4.0)]);
|
||||
pen.line(&[(13.5, 2.0), (16.0, 4.0), (13.5, 6.0)]);
|
||||
}
|
||||
"tool-rotate" => {
|
||||
let pts: Vec<(f32, f32)> = (0..24)
|
||||
.map(|i| {
|
||||
@@ -791,3 +805,31 @@ pub fn button(ui: &mut egui::Ui, name: &str, size: f32, selected: bool, tip: &st
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// What icon `name` paints into a 20×20 rect, as the painter's shape list.
|
||||
fn drawing(name: &str) -> String {
|
||||
let ctx = egui::Context::default();
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
paint(ui.painter(), Rect::from_min_size(pos2(0.0, 0.0), vec2(20.0, 20.0)), name, Color32::WHITE);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
format!("{:?}", out.shapes.iter().map(|s| &s.shape).collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_tools_have_their_own_icons() {
|
||||
let names = ["tool-free-transform", "tool-scale", "tool-shear"];
|
||||
let unknown = drawing("no-such-icon");
|
||||
let drawn: Vec<String> = names.iter().map(|n| drawing(n)).collect();
|
||||
for (n, d) in names.iter().zip(&drawn) {
|
||||
assert_ne!(*d, unknown, "{n} falls back to the placeholder");
|
||||
}
|
||||
assert_ne!(drawn[0], drawn[1], "Scale draws like Free Transform");
|
||||
assert_ne!(drawn[0], drawn[2], "Shear draws like Free Transform");
|
||||
assert_ne!(drawn[1], drawn[2], "Scale and Shear draw alike");
|
||||
}
|
||||
}
|
||||
@@ -2007,6 +2007,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());
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user