diff --git a/ROADMAP.md b/ROADMAP.md index d69d7c21..bb426ad9 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -110,6 +110,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist - **Live Corners:** drag a live rectangle's corner widgets (Selection or Direct Selection) to round all corners, with a radius readout. - **Graphs:** Column, Stacked Column, Bar, Stacked Bar, Line, Area, Scatter, Pie and Radar graph tools with Graph Data and Graph Type. - **Other:** Eyedropper, Gradient annotator, Artboard, Measure, Type, Hand, Zoom, Rotate View. + - **Tool double-clicks:** double-clicking a tool's button opens its options: Hand fits the artboard in the window, Zoom shows 100 %, Rotate, Scale, Reflect and Shear open their Object › Transform dialogs, Gradient, Eyedropper, Blend and the Liquify tools their panels or options. - **Drawing aids:** Smart Guides and snapping, guides dragged out of the rulers, and Draw Normal / Behind / Inside modes. - **Geometry and effects:** - Pathfinder (10 exact curve booleans), Offset, Outline Stroke, Simplify, Clean Up, Split Into Grid, Divide Objects Below. diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 125113ee..1f6d8d44 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -239,7 +239,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "tool.options", "Tool Options…", "", - "{tool: id} what double-clicking a tool button opens: gradient → the Gradient panel (window.panel), eyedropper → Eyedropper Options (dialog `eyedropperOptions`, fields sampleSize, pickUp, apply; OK runs eyedropper.setOptions), printTiling → resets the print tiling (print.tiling.set {reset: true}), warp|twirl|pucker|bloat|scallop|crystallize|wrinkle → that tool's Tool Options (dialog `liquifyOptions`, fields tool, width, height, angle, intensity %, usePressure, detail, simplify, simplifyOn, rate, complexity, horizontal %, vertical %, affectAnchors, affectIn, affectOut, showBrush; OK runs tool.setOption {tool, values})", + "{tool: id} what double-clicking a tool button opens: hand → fits the artboard in the window (view.fitArtboard), zoom → 100% (view.actualSize), rotate|scale|reflect|shear → that Object › Transform dialog (dialog `rotate`, `scale`, `reflect` or `shear`; error `nothing selected` without a selection), gradient → the Gradient panel (window.panel), eyedropper → Eyedropper Options (dialog `eyedropperOptions`, fields sampleSize, pickUp, apply; OK runs eyedropper.setOptions), printTiling → resets the print tiling (print.tiling.set {reset: true}), warp|twirl|pucker|bloat|scallop|crystallize|wrinkle → that tool's Tool Options (dialog `liquifyOptions`, fields tool, width, height, angle, intensity %, usePressure, detail, simplify, simplifyOn, rate, complexity, horizontal %, vertical %, affectAnchors, affectIn, affectOut, showBrush; OK runs tool.setOption {tool, values})", ), ( "ui.colorGuideOptions", diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 8707bc27..459d3333 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -208,9 +208,21 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { /// Open a tool's options (`tool.options`, a double-click on its button): the Gradient tool's are /// the Gradient panel, the Eyedropper's the Eyedropper Options dialog, a Liquify tool's its Tool /// Options dialog, the Blend tool's Blend -/// Options; the Print Tiling tool's resets the print tiling. +/// Options; the Print Tiling tool's resets the print tiling. As in the reference app, the Hand +/// tool's fits the artboard in the window, the Zoom tool's shows it at 100%, and the Rotate, Scale, +/// Reflect and Shear tools' are their Object › Transform dialogs. pub fn open_options(app: &mut VectorcraftApp, tool: &str) -> Result { match tool { + "hand" => app.run("view.fitArtboard", json!({})), + "zoom" => app.run("view.actualSize", json!({})), + "rotate" | "scale" | "reflect" | "shear" => { + let id = format!("object.{tool}"); + if let Some(c) = vectorcraft_engine::find_command(&id) { + (c.enabled)(&app.session)?; + } + crate::menus::invoke(app, &id, json!({})); + Ok(json!({ "dialog": tool })) + } "gradient" if app.ui.open_panel.as_deref() == Some("gradient") => Ok(json!({ "open": "gradient" })), "gradient" => app.run("window.panel", json!({ "panel": "gradient" })), "eyedropper" => { @@ -486,7 +498,35 @@ pub(crate) mod tests { crate::dialogs::confirm(&mut app).unwrap(); let o = app.session.prefs.eyedropper; assert!(o.pick_up.appearance.transparency && !o.apply.appearance.transparency && o.apply.appearance.fill.color); - assert!(app.run("tool.options", json!({"tool": "zoom"})).is_err()); + assert!(app.run("tool.options", json!({"tool": "lasso"})).is_err()); + } + + #[test] + fn double_clicking_the_hand_zoom_and_transform_tools() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 300, "height": 200})).unwrap(); + app.canvas_rect = Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(660.0, 460.0))); + app.run("view.setZoom", json!({"zoom": 333, "center": [10, 10]})).unwrap(); + // Hand: the artboard fits the window. Zoom: 100%. + app.run("tool.options", json!({"tool": "hand"})).unwrap(); + let v = *app.view().unwrap(); + assert_eq!((v.center.x, v.center.y, v.zoom), (150.0, 100.0, 2.0)); + app.run("tool.options", json!({"tool": "zoom"})).unwrap(); + assert_eq!(app.view().unwrap().zoom, 1.0); + // The transform tools open their dialogs, like Object › Transform: not with nothing selected. + assert_eq!(app.run("tool.options", json!({"tool": "rotate"})), Err("nothing selected".into())); + assert!(app.ui.dialog.is_none()); + let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 40, "height": 20})).unwrap()["id"].as_u64().unwrap(); + for tool in ["rotate", "scale", "reflect", "shear"] { + app.run("tool.options", json!({"tool": tool})).unwrap(); + assert_eq!(app.ui.dialog.take().map(|d| d.kind), Some(tool.to_string())); + } + // OK in Rotate turns the selection about its centre. + app.run("tool.options", json!({"tool": "rotate"})).unwrap(); + app.ui.dialog.as_mut().unwrap().fields.insert("angle".into(), json!(90)); + crate::dialogs::confirm(&mut app).unwrap(); + let b = app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().geometric_bounds().unwrap(); + assert!((b.x0 - 20.0).abs() < 1e-9 && (b.y0 - 0.0).abs() < 1e-9 && (b.width() - 20.0).abs() < 1e-9, "{b:?}"); } #[test] diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 53e78d8c..a4c19aa1 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -87,7 +87,9 @@ point (`paint.freeform.selectPoint`); its fields are `color` (hex), `opacity` an Tool options: double-clicking a tool button runs `tool.options {tool}`. For `gradient` it opens the Gradient panel; for `eyedropper` it opens Eyedropper Options, an `eyedropperOptions` dialog (fields `sampleSize` 1/3/5, `pickUp` and `apply`, the attribute trees of `eyedropper.setOptions`) whose `ui.dialog.confirm` runs `eyedropper.setOptions` (what -`appearance.copyFrom` copies). Gradient tool handles snap to +`appearance.copyFrom` copies). For `hand` it fits the artboard in the window (`view.fitArtboard`) and for `zoom` it +shows 100% (`view.actualSize`). For `rotate`, `scale`, `reflect` and `shear` it opens the same dialog as Object › +Transform (dialog kind = the tool id), or fails with `nothing selected`. Gradient tool handles snap to anchors, edges and smart guides; Shift constrains them to 45° steps from the `constrainAngle` preference. Effect dialogs: `engine.execute {command: "effect.dialog", params: {effect, index?, item?}}` opens the `effect` dialog