mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Fix #1408: the Transform tools and commands act on the anchors selected with Direct Selection
This commit is contained in:
1 parent
003234a056
commit
a41d59604b
5 files changed
+135
-37
No files matched your search
@@ -4,7 +4,7 @@ use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Document, NodeId};
|
||||
use crate::{Document, NodeId, NodeKind};
|
||||
|
||||
/// (subpath index, anchor index) inside a path.
|
||||
pub type AnchorRef = (usize, usize);
|
||||
@@ -125,6 +125,25 @@ impl Selection {
|
||||
pub fn partial(&self, id: NodeId) -> Option<&BTreeSet<AnchorRef>> {
|
||||
self.anchors.get(&id)
|
||||
}
|
||||
/// The objects selected whole: those with no direct-selected anchors.
|
||||
pub fn whole(&self) -> Vec<NodeId> {
|
||||
self.objects.iter().copied().filter(|id| !self.anchors.contains_key(id)).collect()
|
||||
}
|
||||
/// With direct-selected anchors, what a transform of them measures from: the box around those
|
||||
/// anchors and the objects selected whole (`visual`: with their strokes). `None` without any.
|
||||
pub fn anchor_bounds(&self, doc: &Document, visual: bool) -> Option<vectorcraft_geom::Rect> {
|
||||
if self.anchors.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut acc = doc.bounds_of(&self.whole(), visual);
|
||||
for (id, set) in &self.anchors {
|
||||
let Some(NodeKind::Path { path, .. }) = doc.node(*id).map(|n| &n.kind) else { continue };
|
||||
for a in set.iter().filter_map(|&(si, ai)| path.subpaths.get(si)?.anchors.get(ai)) {
|
||||
acc = vectorcraft_geom::union_opt(acc, Some(vectorcraft_geom::Rect::from_points(a.p, a.p)));
|
||||
}
|
||||
}
|
||||
acc
|
||||
}
|
||||
/// Which direction handles belong to selected segments; both for an anchor selection.
|
||||
pub fn handle_sides(&self, id: NodeId, si: usize, ai: usize, sp: &vectorcraft_geom::SubPath) -> (bool, bool) {
|
||||
let Some(segments) = self.segments.get(&id) else { return (true, true) };
|
||||
|
||||
@@ -23,7 +23,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Transform",
|
||||
[],
|
||||
None,
|
||||
"{matrix: [a,b,c,d,e,f], copy?: bool, ids?, strokes?: bool, corners?: bool, typeAreas?: bool, patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)} apply an affine to the selection (or ids); strokes/corners: Scale Strokes & Effects / Scale Corners (default: the preferences); typeAreas: area type among them (not type inside a group) reshapes its frame by the matrix and its text reflows at its size, as a bounding-box handle drag does (default: the type transforms too)",
|
||||
"{matrix: [a,b,c,d,e,f], copy?: bool, ids?, strokes?: bool, corners?: bool, typeAreas?: bool, anchors?: bool, patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)} apply an affine to the selection (or ids); anchors (without ids or copy): paths with anchors selected with Direct Selection have only those anchors and their handles transformed, as the Transform tools and object.move/rotate/scale/reflect/shear do (default: the whole objects); strokes/corners: Scale Strokes & Effects / Scale Corners (default: the preferences); typeAreas: area type among them (not type inside a group) reshapes its frame by the matrix and its text reflows at its size, as a bounding-box handle drag does (default: the type transforms too)",
|
||||
has_doc,
|
||||
transform
|
||||
),
|
||||
@@ -32,7 +32,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Move…",
|
||||
["Object", "Transform"],
|
||||
Some("Cmd+Shift+M"),
|
||||
"{dx, dy, copy?, patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)}",
|
||||
"{dx, dy, copy?, patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)}. With anchors selected with Direct Selection (and no copy), only those anchors and their handles transform, about the centre of their own box.",
|
||||
has_selection,
|
||||
move_cmd
|
||||
),
|
||||
@@ -41,7 +41,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Rotate…",
|
||||
["Object", "Transform"],
|
||||
None,
|
||||
"{angle: deg (counter-clockwise), absolute?: bool (angle is the bounding box's new angle, not an amount), origin?: [x,y] (default: the bounding box centre), copy?, patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)} → {ids}",
|
||||
"{angle: deg (counter-clockwise), absolute?: bool (angle is the bounding box's new angle, not an amount), origin?: [x,y] (default: the bounding box centre), copy?, patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)} → {ids}. With anchors selected with Direct Selection (and no copy), only those anchors and their handles transform, about the centre of their own box.",
|
||||
has_selection,
|
||||
rotate
|
||||
),
|
||||
@@ -50,7 +50,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Scale…",
|
||||
["Object", "Transform"],
|
||||
None,
|
||||
"{sx: %, sy?: %, origin?: [x,y], copy?, strokes?: bool (Scale Strokes & Effects: stroke weights, dashes and effect distances scale; off keeps them, type strokes included), corners?: bool (Scale Corners: live corner radii scale), patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)} (strokes/corners default to the preferences)",
|
||||
"{sx: %, sy?: %, origin?: [x,y], copy?, strokes?: bool (Scale Strokes & Effects: stroke weights, dashes and effect distances scale; off keeps them, type strokes included), corners?: bool (Scale Corners: live corner radii scale), patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)} (strokes/corners default to the preferences). With anchors selected with Direct Selection (and no copy), only those anchors and their handles transform, about the centre of their own box.",
|
||||
has_selection,
|
||||
scale
|
||||
),
|
||||
@@ -59,7 +59,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Reflect…",
|
||||
["Object", "Transform"],
|
||||
None,
|
||||
"{axis: \"vertical\"|\"horizontal\"|deg, origin?, copy?, patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)}",
|
||||
"{axis: \"vertical\"|\"horizontal\"|deg, origin?, copy?, patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)}. With anchors selected with Direct Selection (and no copy), only those anchors and their handles transform, about the centre of their own box.",
|
||||
has_selection,
|
||||
reflect
|
||||
),
|
||||
@@ -68,7 +68,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Shear…",
|
||||
["Object", "Transform"],
|
||||
None,
|
||||
"{angle: deg, axis?: \"horizontal\"|\"vertical\", origin?, copy?, patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)}",
|
||||
"{angle: deg, axis?: \"horizontal\"|\"vertical\", origin?, copy?, patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)}. With anchors selected with Direct Selection (and no copy), only those anchors and their handles transform, about the centre of their own box.",
|
||||
has_selection,
|
||||
shear
|
||||
),
|
||||
@@ -223,6 +223,13 @@ fn origin_of(s: &Session, p: &Value, ids: &[NodeId]) -> Result<Point> {
|
||||
if let Some(o) = point_param(p, "origin") {
|
||||
return Ok(o);
|
||||
}
|
||||
// Direct-selected anchors transform alone (see [`apply_transform`]), about their own centre.
|
||||
let st = s.doc()?;
|
||||
if !bool_or(p, "copy", false)
|
||||
&& let Some(b) = st.selection.anchor_bounds(&st.doc, s.prefs.use_preview_bounds)
|
||||
{
|
||||
return Ok(b.center());
|
||||
}
|
||||
s.transform_box(ids).map(|b| b.center()).ok_or_else(|| EngineError::Other("selection has no bounds".into()))
|
||||
}
|
||||
|
||||
@@ -263,7 +270,10 @@ pub(crate) fn transform_patterns(s: &mut Session, p: &Value, ids: &[NodeId]) ->
|
||||
/// Apply `xf` to `ids` (`copy` param: duplicate first; `strokes`/`corners`: see [`scaling`];
|
||||
/// `patterns`: see [`transform_patterns`]; `typeAreas`: see [`resize_type_area`]). Outside an
|
||||
/// interaction, records `xf` and the options it used for Transform Again.
|
||||
pub(crate) fn apply_transform(s: &mut Session, label: &str, ids: Vec<NodeId>, xf: Affine, p: &Value) -> Result<Value> {
|
||||
///
|
||||
/// `anchors` (`ids` being the selection): with anchors selected with Direct Selection and no
|
||||
/// copy, only those anchors transform, their handles with them, and the objects selected whole.
|
||||
pub(crate) fn apply_transform(s: &mut Session, label: &str, ids: Vec<NodeId>, xf: Affine, p: &Value, anchors: bool) -> Result<Value> {
|
||||
let copy = bool_or(p, "copy", false);
|
||||
let areas = bool_or(p, "typeAreas", false);
|
||||
let mut sc = if Scaling::factor(xf).is_some() { scaling(s, p) } else { Scaling::default() };
|
||||
@@ -273,7 +283,12 @@ pub(crate) fn apply_transform(s: &mut Session, label: &str, ids: Vec<NodeId>, xf
|
||||
// With a copy, the reply lists the copies, and there are none.
|
||||
let d = &s.doc()?.doc;
|
||||
let none = d.index_paths(ids.iter().copied()).is_empty();
|
||||
let ids = if none || (!copy && xf == Affine::IDENTITY) {
|
||||
let ids = if anchors && !copy && !s.doc()?.selection.anchors.is_empty() {
|
||||
if xf != Affine::IDENTITY {
|
||||
super::path::transform_anchors(s, label, xf, sc)?;
|
||||
}
|
||||
ids
|
||||
} else if none || (!copy && xf == Affine::IDENTITY) {
|
||||
if copy { vec![] } else { ids }
|
||||
} else {
|
||||
s.edit(label, |d, sel| {
|
||||
@@ -316,16 +331,16 @@ fn resize_type_area(n: &mut Node, xf: Affine) -> bool {
|
||||
|
||||
fn transform(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let m = matrix_param(p, "matrix").ok_or_else(|| bad("object.transform", "missing matrix [a,b,c,d,e,f]"))?;
|
||||
let ids = match ids_param(p, "ids") {
|
||||
Some(v) => v,
|
||||
None => selected_roots(s)?,
|
||||
let (ids, anchors) = match ids_param(p, "ids") {
|
||||
Some(v) => (v, false),
|
||||
None => (selected_roots(s)?, bool_or(p, "anchors", false)),
|
||||
};
|
||||
apply_transform(s, "Transform", ids, m, p)
|
||||
apply_transform(s, "Transform", ids, m, p, anchors)
|
||||
}
|
||||
|
||||
fn move_cmd(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ids = selected_roots(s)?;
|
||||
apply_transform(s, "Move", ids, Affine::translate((f64_or(p, "dx", 0.0), f64_or(p, "dy", 0.0))), p)
|
||||
apply_transform(s, "Move", ids, Affine::translate((f64_or(p, "dx", 0.0), f64_or(p, "dy", 0.0))), p, true)
|
||||
}
|
||||
|
||||
fn nudge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -348,7 +363,7 @@ fn nudge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return super::docmenu::guide_move(s, &json!({ "dx": dx, "dy": dy, "copy": copy }));
|
||||
}
|
||||
let ids = selected_roots(s)?;
|
||||
apply_transform(s, "Move", ids, Affine::translate((dx, dy)), p)
|
||||
apply_transform(s, "Move", ids, Affine::translate((dx, dy)), p, true)
|
||||
}
|
||||
|
||||
fn about(o: Point, a: Affine) -> Affine {
|
||||
@@ -364,7 +379,7 @@ fn rotate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
// Angles are counter-clockwise; y is down, so negate.
|
||||
let a = about(o, Affine::rotate(-angle.to_radians()));
|
||||
apply_transform(s, "Rotate", ids, a, p)
|
||||
apply_transform(s, "Rotate", ids, a, p, true)
|
||||
}
|
||||
|
||||
fn scale(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -375,7 +390,7 @@ fn scale(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if sx == 0.0 || sy == 0.0 {
|
||||
return Err(bad("object.scale", "scale must be non-zero"));
|
||||
}
|
||||
apply_transform(s, "Scale", ids, about(o, Affine::scale_non_uniform(sx, sy)), p)
|
||||
apply_transform(s, "Scale", ids, about(o, Affine::scale_non_uniform(sx, sy)), p, true)
|
||||
}
|
||||
|
||||
fn reflect(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -394,7 +409,7 @@ fn reflect(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad("object.reflect", format!("axis must be \"vertical\", \"horizontal\" or an angle in degrees, not `{given}`")));
|
||||
}
|
||||
};
|
||||
apply_transform(s, "Reflect", ids, about(o, m), p)
|
||||
apply_transform(s, "Reflect", ids, about(o, m), p, true)
|
||||
}
|
||||
|
||||
fn shear(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -403,7 +418,7 @@ fn shear(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let t = f64_or(p, "angle", 0.0).clamp(-89.0, 89.0).to_radians().tan();
|
||||
let vertical = choice(p, "object.shear", "axis", &["horizontal", "vertical"])? == Some("vertical");
|
||||
let m = if vertical { Affine::new([1.0, t, 0.0, 1.0, 0.0, 0.0]) } else { Affine::new([1.0, 0.0, -t, 1.0, 0.0, 0.0]) };
|
||||
apply_transform(s, "Shear", ids, about(o, m), p)
|
||||
apply_transform(s, "Shear", ids, about(o, m), p, true)
|
||||
}
|
||||
|
||||
/// The last transform's matrix on the selection, with the options `last_transform` keeps for it; an
|
||||
@@ -417,7 +432,7 @@ fn transform_again(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
o.extend(given.iter().filter(|(_, v)| v.is_boolean()).map(|(k, v)| (k.clone(), v.clone())));
|
||||
}
|
||||
let ids = selected_roots(s)?;
|
||||
apply_transform(s, "Transform Again", ids, m, &options)
|
||||
apply_transform(s, "Transform Again", ids, m, &options, true)
|
||||
}
|
||||
|
||||
enum Arrange {
|
||||
@@ -1311,10 +1326,10 @@ fn set_bounds(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// leave a rounding error in it on a rotated object or with Use Preview Bounds.
|
||||
let current = |k: &str, now: f64| p.get(k).and_then(Value::as_f64).is_none_or(|x| x == now);
|
||||
if current("x", page.x) && current("y", page.y) && current("width", v.width()) && current("height", v.height()) {
|
||||
return apply_transform(s, "Transform", ids, Affine::IDENTITY, p);
|
||||
return apply_transform(s, "Transform", ids, Affine::IDENTITY, p, false);
|
||||
}
|
||||
let to = frame.to_local(Point::new(f64_or(p, "x", page.x), f64_or(p, "y", page.y)));
|
||||
apply_transform(s, "Transform", ids, frame.conjugate(Affine::translate(to - new_rp) * scale), p)
|
||||
apply_transform(s, "Transform", ids, frame.conjugate(Affine::translate(to - new_rp) * scale), p, false)
|
||||
}
|
||||
|
||||
fn expand_shape(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
|
||||
@@ -168,35 +168,38 @@ fn close(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
pub(crate) fn move_anchors(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let d = Vec2::new(f64_or(p, "dx", 0.0), f64_or(p, "dy", 0.0));
|
||||
let st = s.doc()?;
|
||||
let sel = st.selection.clone();
|
||||
let scale_strokes = s.prefs.scale_strokes;
|
||||
let _ = scale_strokes;
|
||||
// Objects selected whole (with no entry in `sel.anchors`) move as objects, and their pattern
|
||||
// tiles move with them when `patterns` (default: General › Transform Pattern Tiles) is on.
|
||||
let whole: Vec<NodeId> = sel.objects.iter().copied().filter(|id| !sel.anchors.contains_key(id)).collect();
|
||||
// Objects selected whole move as objects, and their pattern tiles move with them when
|
||||
// `patterns` (default: General › Transform Pattern Tiles) is on.
|
||||
let whole = s.doc()?.selection.whole();
|
||||
let sc = Scaling { patterns: super::object::transform_patterns(s, p, &whole)?, ..Scaling::default() };
|
||||
s.edit("Move", |doc, _| {
|
||||
transform_anchors(s, "Move", vectorcraft_geom::Affine::translate(d), sc)?;
|
||||
ok()
|
||||
}
|
||||
|
||||
/// Apply `xf` to the direct-selected anchors, their handles with them, and (with `sc`) to the
|
||||
/// objects selected whole (those with no entry in the selection's `anchors`).
|
||||
pub(crate) fn transform_anchors(s: &mut Session, label: &str, xf: vectorcraft_geom::Affine, sc: Scaling) -> Result<()> {
|
||||
let sel = s.doc()?.selection.clone();
|
||||
s.edit(label, |doc, _| {
|
||||
for id in &sel.objects {
|
||||
match sel.anchors.get(id) {
|
||||
Some(set) => {
|
||||
let path = path_mut(doc, *id)?;
|
||||
for &(si, ai) in set {
|
||||
if let Some(a) = path.anchor_mut(si, ai) {
|
||||
a.translate(d);
|
||||
*a = a.transform(xf);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if let Some(n) = doc.node_mut(*id) {
|
||||
n.transform(vectorcraft_geom::Affine::translate(d), sc);
|
||||
n.transform(xf, sc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
ok()
|
||||
})
|
||||
}
|
||||
|
||||
fn set_handle(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
@@ -764,6 +764,60 @@ fn a_transform_that_moves_nothing_records_no_undo_step() {
|
||||
assert_eq!(count(&s), n + 1);
|
||||
}
|
||||
|
||||
/// #1408: with anchors selected with Direct Selection, the Transform commands and tools transform
|
||||
/// those anchors alone, about the centre of their own box; a copy still copies the whole object.
|
||||
#[test]
|
||||
fn transforms_act_on_direct_selected_anchors() {
|
||||
let mut s = session();
|
||||
let square = json!({"anchors": [{"x": 0, "y": 0}, {"x": 100, "y": 0}, {"x": 100, "y": 100}, {"x": 0, "y": 100}], "closed": true});
|
||||
let id = NodeId(s.execute("path.create", &square).unwrap()["id"].as_u64().unwrap());
|
||||
let points = |s: &Session| -> Vec<(f64, f64)> {
|
||||
let NodeKind::Path { path, .. } = &s.doc().unwrap().doc.node(id).unwrap().kind else { panic!("a path") };
|
||||
path.subpaths[0].anchors.iter().map(|a| (a.p.x.round(), a.p.y.round())).collect()
|
||||
};
|
||||
let top = json!({"id": id.0, "anchors": [[0, 0], [0, 1]]});
|
||||
s.execute("select.anchors", &top).unwrap();
|
||||
let cases = [
|
||||
("object.scale", json!({"sx": 50}), [(25.0, 0.0), (75.0, 0.0)]),
|
||||
("object.scale", json!({"sx": 50, "origin": [0, 100]}), [(0.0, 50.0), (50.0, 50.0)]),
|
||||
("object.rotate", json!({"angle": 90}), [(50.0, 50.0), (50.0, -50.0)]),
|
||||
("object.reflect", json!({"axis": "vertical"}), [(100.0, 0.0), (0.0, 0.0)]),
|
||||
("object.shear", json!({"angle": 45, "axis": "vertical", "origin": [0, 0]}), [(0.0, 0.0), (100.0, 100.0)]),
|
||||
("object.move", json!({"dx": 10, "dy": -10}), [(10.0, -10.0), (110.0, -10.0)]),
|
||||
("object.transform", json!({"matrix": [1, 0, 0, 1, 5, 5], "anchors": true}), [(5.0, 5.0), (105.0, 5.0)]),
|
||||
];
|
||||
for (cmd, p, [a, b]) in cases {
|
||||
s.execute(cmd, &p).unwrap();
|
||||
assert_eq!(points(&s), [a, b, (100.0, 100.0), (0.0, 100.0)], "{cmd} {p}");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("select.anchors", &top).unwrap();
|
||||
}
|
||||
// Transform Again repeats it on the anchors.
|
||||
s.execute("object.move", &json!({"dx": 10, "dy": 0})).unwrap();
|
||||
s.execute("object.transformAgain", &json!({})).unwrap();
|
||||
assert_eq!(points(&s), [(20.0, 0.0), (120.0, 0.0), (100.0, 100.0), (0.0, 100.0)]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("select.anchors", &top).unwrap();
|
||||
// Without `anchors`, object.transform (the Selection tool's) takes the whole path, and so does a copy.
|
||||
s.execute("object.transform", &json!({"matrix": [1, 0, 0, 1, 5, 5]})).unwrap();
|
||||
assert_eq!(points(&s), [(5.0, 5.0), (105.0, 5.0), (105.0, 105.0), (5.0, 105.0)]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("select.anchors", &top).unwrap();
|
||||
let copies = s.execute("object.scale", &json!({"sx": 50, "copy": true})).unwrap();
|
||||
assert_eq!(copies["ids"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(points(&s), [(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)], "the original stays");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("select.anchors", &top).unwrap();
|
||||
// The Scale tool: a drag from (100, 50) to (75, 50) halves the top edge about its centre (50, 0).
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("scale", v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 100.0, 50.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 75.0, 50.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 75.0, 50.0), v).unwrap();
|
||||
assert_eq!(points(&s), [(25.0, 0.0), (75.0, 0.0), (100.0, 100.0), (0.0, 100.0)]);
|
||||
}
|
||||
|
||||
/// Arrange commands that leave the stacking order as it is record no undo step: Bring to Front and
|
||||
/// Bring Forward on the front objects, Send to Back and Send Backward on the back ones, and Send to
|
||||
/// Current Layer on objects already on top of the current layer.
|
||||
|
||||
@@ -87,6 +87,12 @@ fn about(o: Point, a: Affine) -> Affine {
|
||||
Affine::translate(o.to_vec2()) * a * Affine::translate(-o.to_vec2())
|
||||
}
|
||||
|
||||
/// The transform `m` of the selection (of a copy with `copy`): of the anchors selected with Direct
|
||||
/// Selection alone, when there are some.
|
||||
fn preview(m: Affine, copy: bool) -> Action {
|
||||
Action::Preview("object.transform".into(), json!({ "matrix": matrix_json(m), "copy": copy, "anchors": true }))
|
||||
}
|
||||
|
||||
/// Reflection across the line through the origin at angle `theta` (radians, screen space).
|
||||
pub fn reflect_matrix(o: Point, theta: f64) -> Affine {
|
||||
about(o, Affine::rotate(theta) * Affine::scale_non_uniform(1.0, -1.0) * Affine::rotate(-theta))
|
||||
@@ -106,7 +112,8 @@ impl TransformTool {
|
||||
{
|
||||
return Some(*p);
|
||||
}
|
||||
selection_box(cx).map(|b| b.center())
|
||||
// Anchors selected with Direct Selection transform alone, about their own centre.
|
||||
cx.selection.anchor_bounds(cx.doc, cx.preview_bounds).map(|b| b.center()).or_else(|| selection_box(cx).map(|b| b.center()))
|
||||
}
|
||||
|
||||
/// Transform for dragging from `start` to `p` about `o`; returns the matrix and a readout.
|
||||
@@ -209,14 +216,14 @@ impl Tool for TransformTool {
|
||||
d.last = m;
|
||||
self.drag = Some(d);
|
||||
self.measure = Some((p, text));
|
||||
out.push(Action::Preview("object.transform".into(), json!({ "matrix": matrix_json(m), "copy": ev.mods.alt })));
|
||||
out.push(preview(m, ev.mods.alt));
|
||||
out
|
||||
}
|
||||
PointerKind::Up => {
|
||||
let Some(d) = self.end_drag() else { return vec![] };
|
||||
if d.began {
|
||||
// The Alt state at release decides whether the result is a copy.
|
||||
vec![Action::Preview("object.transform".into(), json!({ "matrix": matrix_json(d.last), "copy": ev.mods.alt })), Action::Commit]
|
||||
vec![preview(d.last, ev.mods.alt), Action::Commit]
|
||||
} else {
|
||||
// A click sets the reference point; with Alt, it opens the dialog too.
|
||||
self.origin = Some((d.pick, cx.selection.objects.clone()));
|
||||
|
||||
Reference in new issue
Block a user