mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 21:57:04 +00:00
While the Selection, Direct Selection or Group Selection tool moves the selection, or the Gradient tool draws a vector, W and H show the change in x and y, with the distance and the angle under them. A Scale tool drag shows its percentages; a Rotate, Reflect or Shear tool drag shows the center of the selection's box and its angles. The Zoom tool adds the magnification and the Paintbrush the name of its brush. W and H follow Use Preview Bounds. With several objects selected, the panel shows only the fill, stroke and type values they share, and Show Options adds a tint's swatch and percentage and a gradient's swatch name. - Tool::readout and Readout (crates/tools/src/readout.rs). - info.readout returns the selection's size and the tool's readout. - Docs: docs/mcp.md, docs/ui-parity.md.
164 lines
7.2 KiB
Rust
164 lines
7.2 KiB
Rust
//! Window › Info (`info.readout`): the selection's size and the active tool's readout, as the Info
|
||
//! panel shows them.
|
||
|
||
use serde_json::{Value, json};
|
||
use vectorcraft_render::effects;
|
||
|
||
use super::{CommandSpec, Result, Session, always, cmd};
|
||
|
||
pub fn specs() -> Vec<CommandSpec> {
|
||
vec![cmd!(
|
||
query "info.readout",
|
||
"Info",
|
||
[],
|
||
None,
|
||
"{} → {tool: the active tool's id, width?, height?: the sides of the selection's bounding box in pt (the box around the anchors selected with Direct Selection, when there are some), measured as prefs usePreviewBounds says; left out with nothing selected, readout?: what the Info panel shows for the tool in use: {kind: \"drag\", dx, dy, distance, angle} while the Selection, Direct Selection or Group Selection tool moves the selection or the Gradient tool draws a gradient vector on the art; {kind: \"scale\", width, height} (percent) during a Scale tool drag; {kind: \"rotate\", center: [x, y], angle: deg counter-clockwise, as object.rotate takes it} and {kind: \"reflect\", center, angle: the axis in deg counter-clockwise from horizontal, as object.reflect takes it} during a Rotate or Reflect tool drag; {kind: \"shear\", center, axis: 0 for a horizontal shear and 90 for a vertical one, angle: deg, as object.shear takes it} during a Shear tool drag; {kind: \"brush\", brush: name|null} while the Paintbrush is the tool; left out for any other tool and between drags. Lengths are in pt, a drag's angle is in deg counter-clockwise from the x axis, and center is the center of the selection's bounding box as the drag has carried it, in document coordinates}",
|
||
always,
|
||
readout
|
||
)]
|
||
}
|
||
|
||
impl Session {
|
||
/// The sides of the selection's bounding box as the Info panel shows them: of the box around
|
||
/// the anchors selected with Direct Selection when there are some, else of the bounding box
|
||
/// rotated with rotated objects ([`Session::transform_box`]), measured as Use Preview Bounds
|
||
/// says. None with nothing selected.
|
||
pub fn info_size(&self) -> Option<(f64, f64)> {
|
||
let st = self.active()?;
|
||
let anchors = effects::preview::anchor_bounds_with(&st.selection, &st.doc, self.bounds_measure());
|
||
let r = anchors.or_else(|| self.transform_box(&st.selection.objects).map(|b| b.rect))?;
|
||
Some((r.width(), r.height()))
|
||
}
|
||
}
|
||
|
||
fn readout(s: &mut Session, _: &Value) -> Result<Value> {
|
||
let mut out = json!({ "tool": s.tool_id() });
|
||
if let Some((w, h)) = s.info_size() {
|
||
out["width"] = json!(w);
|
||
out["height"] = json!(h);
|
||
}
|
||
if let Some(r) = s.tool_readout() {
|
||
out["readout"] = r.to_json();
|
||
}
|
||
Ok(out)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use serde_json::json;
|
||
use vectorcraft_tools::{PointerEvent, PointerKind};
|
||
|
||
use crate::{Session, ViewInfo};
|
||
|
||
fn view() -> ViewInfo {
|
||
ViewInfo { smart_guides: false, snap_to_point: false, ..Default::default() }
|
||
}
|
||
|
||
/// A 100 by 60 rectangle at (50, 50), selected, with a 10 pt stroke.
|
||
fn session() -> Session {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||
let id = s.execute("shape.rectangle", &json!({"x": 50, "y": 50, "width": 100, "height": 60})).unwrap()["id"].clone();
|
||
s.execute("select.set", &json!({ "ids": [id] })).unwrap();
|
||
s.execute("stroke.set", &json!({"weight": 10})).unwrap();
|
||
s
|
||
}
|
||
|
||
fn tool(s: &mut Session, id: &str) {
|
||
s.select_tool(id, view()).unwrap();
|
||
}
|
||
|
||
fn pointer(s: &mut Session, kind: PointerKind, x: f64, y: f64) {
|
||
s.pointer(&PointerEvent::new(kind, x, y), view()).unwrap();
|
||
}
|
||
|
||
fn info(s: &mut Session) -> serde_json::Value {
|
||
s.execute("info.readout", &json!({})).unwrap()
|
||
}
|
||
|
||
fn near(v: &serde_json::Value, want: f64) -> bool {
|
||
v.as_f64().is_some_and(|v| (v - want).abs() < 1e-6)
|
||
}
|
||
|
||
#[test]
|
||
fn the_size_follows_use_preview_bounds() {
|
||
let mut s = session();
|
||
let i = info(&mut s);
|
||
assert!(near(&i["width"], 100.0) && near(&i["height"], 60.0), "{i}");
|
||
assert!(i.get("readout").is_none(), "{i}");
|
||
s.execute("prefs.set", &json!({"key": "usePreviewBounds", "value": true})).unwrap();
|
||
let i = info(&mut s);
|
||
assert!(near(&i["width"], 110.0) && near(&i["height"], 70.0), "{i}");
|
||
s.execute("select.none", &json!({})).unwrap();
|
||
let i = info(&mut s);
|
||
assert!(i.get("width").is_none() && i.get("height").is_none(), "{i}");
|
||
}
|
||
|
||
#[test]
|
||
fn a_move_reads_how_far_it_has_gone_until_the_release() {
|
||
let mut s = session();
|
||
tool(&mut s, "selection");
|
||
pointer(&mut s, PointerKind::Down, 100.0, 80.0);
|
||
assert!(info(&mut s).get("readout").is_none());
|
||
pointer(&mut s, PointerKind::Drag, 130.0, 40.0);
|
||
let i = info(&mut s);
|
||
let r = &i["readout"];
|
||
assert_eq!(r["kind"], "drag", "{i}");
|
||
assert!(near(&r["dx"], 30.0) && near(&r["dy"], -40.0) && near(&r["distance"], 50.0), "{i}");
|
||
assert!(near(&r["angle"], 53.13010235415598), "{i}");
|
||
pointer(&mut s, PointerKind::Up, 130.0, 40.0);
|
||
let i = info(&mut s);
|
||
assert!(i.get("readout").is_none(), "{i}");
|
||
assert!(near(&i["width"], 100.0), "{i}");
|
||
}
|
||
|
||
#[test]
|
||
fn each_transform_tool_reads_its_drag() {
|
||
for (id, check) in [
|
||
("scale", (|r| r["kind"] == "scale" && near(&r["width"], 200.0) && near(&r["height"], 200.0)) as fn(&serde_json::Value) -> bool),
|
||
("rotate", |r| r["kind"] == "rotate" && near(&r["center"][0], 100.0) && near(&r["center"][1], 80.0) && near(&r["angle"], -90.0)),
|
||
("reflect", |r| r["kind"] == "reflect" && near(&r["center"][0], 100.0) && near(&r["angle"], 0.0)),
|
||
("shear", |r| {
|
||
r["kind"] == "shear"
|
||
&& near(&r["center"][0], 100.0)
|
||
&& near(&r["center"][1], 80.0)
|
||
&& near(&r["axis"], 0.0)
|
||
&& near(&r["angle"], 45.0)
|
||
}),
|
||
] {
|
||
let mut s = session();
|
||
tool(&mut s, id);
|
||
// The reference point is the box's center (100, 80).
|
||
let from = if id == "shear" { (100.0, 60.0) } else { (120.0, 100.0) };
|
||
pointer(&mut s, PointerKind::Down, from.0, from.1);
|
||
let to = match id {
|
||
"scale" => (140.0, 120.0),
|
||
"rotate" => (80.0, 100.0),
|
||
"reflect" => (160.0, 80.0),
|
||
_ => (120.0, 60.0),
|
||
};
|
||
pointer(&mut s, PointerKind::Drag, to.0, to.1);
|
||
let i = info(&mut s);
|
||
assert_eq!(i["tool"], id);
|
||
assert!(check(&i["readout"]), "{id}: {i}");
|
||
pointer(&mut s, PointerKind::Up, to.0, to.1);
|
||
assert!(info(&mut s).get("readout").is_none(), "{id}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn the_paintbrush_reads_its_brush() {
|
||
let mut s = session();
|
||
tool(&mut s, "paintbrush");
|
||
let i = info(&mut s);
|
||
assert_eq!(i["readout"], json!({"kind": "brush", "brush": null}), "{i}");
|
||
}
|
||
|
||
#[test]
|
||
fn a_parameter_is_refused() {
|
||
let mut s = session();
|
||
let e = s.execute("info.readout", &json!({"tool": "scale"})).unwrap_err().to_string();
|
||
assert!(e.contains("tool"), "{e}");
|
||
}
|
||
}
|