mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 11:54:03 +00:00
Merge pull request #5196 from dotCooCoo/fix/perspective-snap-mixed-depths
Perspective grid: Snap to Grid moves objects at every depth by one plane offset
This commit is contained in:
@@ -144,7 +144,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Move in Perspective",
|
||||
[],
|
||||
None,
|
||||
"{ids?|id?, from: [x,y], to: [x,y], plane?, copy?: bool (move copies, as Alt-dragging does), perpendicular?: bool (move along the plane's normal instead, as pressing 5 while dragging does), snap?: bool (default: View › Perspective Grid › Snap to Grid), constrain?: bool (default false; constrain each in-plane move to its plane's nearest axis or 45° diagonal)} slide objects within their own planes (unattached objects attach to `plane`/the active plane first); snapping pulls the nearer edge of the first plane's joint bounds toward a gridline within a quarter cell while preserving the constrained direction; Transform Again repeats it",
|
||||
"{ids?|id?, from: [x,y], to: [x,y], plane?, copy?: bool (move copies, as Alt-dragging does), perpendicular?: bool (move along the plane's normal instead, as pressing 5 while dragging does), snap?: bool (default: View › Perspective Grid › Snap to Grid), constrain?: bool (default false; constrain each in-plane move to its plane's nearest axis or 45° diagonal)} slide objects within their own planes (unattached objects attach to `plane`/the active plane first); snapping pulls the nearer edge of the first plane's joint bounds toward a gridline within a quarter cell while preserving the constrained direction, and moves every object on that plane, at any depth, by the same plane offset; Transform Again repeats it",
|
||||
has_doc,
|
||||
persp_move
|
||||
),
|
||||
|
||||
@@ -285,7 +285,7 @@ pub(super) fn persp_move(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let constrain = switch_or(p, "constrain", false, C)?;
|
||||
// `snap_fix` uses Snap to Grid's setting for a `snap` that is not true or false.
|
||||
check_switches(p, &["snap"], C)?;
|
||||
// Snap to Grid: a correction for the objects on the first one's plane and depth.
|
||||
// Shift: the constrained end point, read on the first object's plane and depth.
|
||||
let snap_to = if constrain && !perp {
|
||||
let doc = &s.doc()?.doc;
|
||||
let g = grid_of(doc);
|
||||
@@ -300,11 +300,20 @@ pub(super) fn persp_move(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
} else {
|
||||
to
|
||||
};
|
||||
let fix = if perp { None } else { crate::cmd::perspgrid::snap_fix(&s.doc()?.doc, &ids, p, (from, snap_to)) };
|
||||
let snapped = move |pl: Plane, depth: f64, raw: vectorcraft_geom::Vec2| {
|
||||
// With Snap to Grid, every object on the first one's plane, at any depth, moves by the first
|
||||
// one's plane-space offset and its correction, so the objects keep their spacing on the plane.
|
||||
let fix = if perp {
|
||||
None
|
||||
} else {
|
||||
let doc = &s.doc()?.doc;
|
||||
crate::cmd::perspgrid::snap_fix(doc, &ids, p, (from, snap_to))
|
||||
.and_then(|(plane, depth, correction)| Some((plane, grid_of(doc).plane_delta(plane, depth, from, to)?, correction)))
|
||||
};
|
||||
let snapped = move |pl: Plane, raw: vectorcraft_geom::Vec2| {
|
||||
let raw = fix.filter(|f| f.0 == pl).map_or(raw, |f| f.1);
|
||||
let (direction, dv) =
|
||||
if constrain { vectorcraft_tools::distort::perspective::constrain_move_delta(raw) } else { (vectorcraft_geom::Vec2::ZERO, raw) };
|
||||
dv + fix.filter(|f| f.0 == pl && f.1 == depth).map_or(vectorcraft_geom::Vec2::ZERO, |f| {
|
||||
dv + fix.filter(|f| f.0 == pl).map_or(vectorcraft_geom::Vec2::ZERO, |f| {
|
||||
if !constrain {
|
||||
return f.2;
|
||||
}
|
||||
@@ -325,7 +334,7 @@ pub(super) fn persp_move(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let to_depth = g.depth_at(pl, depth, from, to).ok_or_else(beyond_horizon)?;
|
||||
Ok((g.transform_map(pl, depth, Affine::IDENTITY, to_depth - depth).ok_or_else(beyond_horizon)?, to_depth))
|
||||
} else {
|
||||
let dv = snapped(pl, depth, g.plane_delta(pl, depth, from, to).ok_or_else(beyond_horizon)?);
|
||||
let dv = snapped(pl, g.plane_delta(pl, depth, from, to).ok_or_else(beyond_horizon)?);
|
||||
Ok((g.transform_map(pl, depth, Affine::translate(dv), 0.0).ok_or_else(beyond_horizon)?, depth))
|
||||
}
|
||||
};
|
||||
@@ -336,7 +345,7 @@ pub(super) fn persp_move(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if perp {
|
||||
g.depth_at(pl, depth, from, to).is_some_and(|d| (d - depth).abs() <= 1e-9)
|
||||
} else {
|
||||
g.plane_delta(pl, depth, from, to).map(|dv| snapped(pl, depth, dv)).is_some_and(|dv| dv.x.abs() <= 1e-9 && dv.y.abs() <= 1e-9)
|
||||
g.plane_delta(pl, depth, from, to).map(|dv| snapped(pl, dv)).is_some_and(|dv| dv.x.abs() <= 1e-9 && dv.y.abs() <= 1e-9)
|
||||
}
|
||||
};
|
||||
let first_still = if copy { None } else { unmoved(s, &ids, still)? };
|
||||
@@ -350,9 +359,7 @@ pub(super) fn persp_move(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let again = match perp {
|
||||
_ if first_still.is_some() => Some(json!({"matrix": matrix_json(Affine::IDENTITY), "copy": copy})),
|
||||
true => g.depth_at(pl, depth, from, to).map(|d| json!({"matrix": matrix_json(Affine::IDENTITY), "depth": d - depth, "copy": copy})),
|
||||
false => {
|
||||
g.plane_delta(pl, depth, from, to).map(|dv| json!({"matrix": matrix_json(Affine::translate(snapped(pl, depth, dv))), "copy": copy}))
|
||||
}
|
||||
false => g.plane_delta(pl, depth, from, to).map(|dv| json!({"matrix": matrix_json(Affine::translate(snapped(pl, dv))), "copy": copy})),
|
||||
};
|
||||
if let Some(a) = again {
|
||||
record_again(s, a)?;
|
||||
|
||||
@@ -428,7 +428,7 @@ fn rulers(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
/// Snap to Grid for a move of `ids` from page point `from` to `to` (unless `p` says `snap: false`):
|
||||
/// the plane and depth of the first one and the plane-space correction that lands the edge of the
|
||||
/// joint bounds of the objects there nearer a gridline on it.
|
||||
/// joint bounds of the objects on that plane, each measured at its own depth, nearer a gridline on it.
|
||||
pub(crate) fn snap_fix(doc: &Document, ids: &[NodeId], p: &Value, (from, to): (Point, Point)) -> Option<(Plane, f64, Vec2)> {
|
||||
let g = grid_of(doc);
|
||||
if !bool_or(p, "snap", g.snap) {
|
||||
|
||||
@@ -228,6 +228,40 @@ fn diagonal_shift_snap_lands_an_edge_on_a_gridline() {
|
||||
assert!(edge.iter().any(|y| ((y / g.cell).round() * g.cell - y).abs() < 1e-6), "{edge:?}, correction {correction:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snap_moves_objects_at_different_depths_by_one_plane_offset() {
|
||||
let (mut s, _) = persp_session();
|
||||
let rect = |s: &mut Session, x: f64| {
|
||||
NodeId(s.execute("shape.rectangle", &json!({"x": x, "y": 380, "width": 40, "height": 40})).unwrap()["id"].as_u64().unwrap())
|
||||
};
|
||||
let near = rect(&mut s, 240.0);
|
||||
s.execute("perspective.attach", &json!({"ids": [near.0], "plane": "left"})).unwrap();
|
||||
s.execute("perspective.plane.move", &json!({"plane": "left", "offset": 50})).unwrap();
|
||||
let far = rect(&mut s, 150.0);
|
||||
s.execute("perspective.attach", &json!({"ids": [far.0], "plane": "left"})).unwrap();
|
||||
s.execute("perspective.plane.move", &json!({"plane": "left", "offset": 0})).unwrap();
|
||||
assert_eq!(attached(&s, near), Some((Plane::Left, 0.0)));
|
||||
assert_eq!(attached(&s, far), Some((Plane::Left, 50.0)));
|
||||
s.execute("perspective.grid.snap", &json!({"on": true})).unwrap();
|
||||
let g = grid(&s);
|
||||
let (near_before, far_before) = (plane_pts(&s, near, Plane::Left, 0.0), plane_pts(&s, far, Plane::Left, 50.0));
|
||||
let b = bounds_of(&near_before).union(bounds_of(&far_before));
|
||||
// A move that ends 0.3 pt past a gridline crossing on the near object's plane.
|
||||
let past = |v: f64| (v / g.cell).ceil() * g.cell + 0.3 - v;
|
||||
let start = bounds_of(&near_before).center();
|
||||
let h = g.homography_at(Plane::Left, 0.0).unwrap();
|
||||
let from = h.apply(start).unwrap();
|
||||
let to = h.apply(start + Vec2::new(past(b.x0), past(b.y0))).unwrap();
|
||||
s.execute("perspective.move", &json!({"ids": [near.0, far.0], "from": [from.x, from.y], "to": [to.x, to.y]})).unwrap();
|
||||
let near_delta = plane_pts(&s, near, Plane::Left, 0.0)[0] - near_before[0];
|
||||
let far_delta = plane_pts(&s, far, Plane::Left, 50.0)[0] - far_before[0];
|
||||
assert!((near_delta - far_delta).hypot() < 1e-6, "near {near_delta:?}, far {far_delta:?}");
|
||||
let on_line = |v: f64| ((v / g.cell).round() * g.cell - v).abs() < 1e-6;
|
||||
let moved = b + near_delta;
|
||||
assert!(on_line(moved.x0) || on_line(moved.x1), "{moved:?}");
|
||||
assert!(on_line(moved.y0) || on_line(moved.y1), "{moved:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constrained_move_uses_each_objects_plane() {
|
||||
let (mut s, right) = persp_session();
|
||||
|
||||
+2
-1
@@ -6528,7 +6528,8 @@ bounding-box handles scale it in plane space (`perspective.transform`; Shift kee
|
||||
centre), and `press_key` `5` during a drag switches the move to perpendicular to the plane (press again to switch
|
||||
back). Holding Shift during an in-plane move constrains each object's displacement to an axis or 45° diagonal of its
|
||||
own plane. `perspective.move` accepts `constrain: true` (default false) and adjusts Snap to Grid along the constrained
|
||||
direction on the first object's plane and depth.
|
||||
direction on the first object's plane and depth. With Snap to Grid, every object on the first object's plane moves
|
||||
by the same plane offset, whatever its depth, and the edge of their joint bounds nearer a gridline lands on it.
|
||||
`press_key` Escape during a move, a resize or a plane widget drag cancels it and puts the objects and the
|
||||
grid back. The arrow keys nudge a selection in perspective by the keyboard increment (`perspective.nudge`; Shift ×10, Alt
|
||||
copies). Object › Transform › Transform Again repeats the last perspective move or scale in plane space.
|
||||
|
||||
@@ -1413,6 +1413,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
An Alt-drag of objects in perspective with the Perspective Selection tool, and an Alt-drag of a plane widget with that tool or the Perspective Grid tool, copy the objects on the first move, and the moves after it move those copies (#1429).
|
||||
With the Perspective Selection tool, Ctrl+Alt-drag (Command+Option-drag on macOS) also copies objects on their perspective plane; Alt/Option-drag continues to do so.
|
||||
With the Perspective Selection tool, holding Shift while moving or copying constrains each object's displacement to the nearest axis or 45° diagonal of its own plane; releasing Shift resumes the raw pointer position, and 5 still moves perpendicular to the plane. Snap to Grid follows the constrained line on the first object's plane and depth.
|
||||
With Snap to Grid on, a move of objects on one perspective plane at different depths moves them all by the same offset on the plane, so they keep their spacing, and the edge of their joint bounds nearer a gridline lands on it. Regression: `snap_moves_objects_at_different_depths_by_one_plane_offset` in `crates/engine/src/tests/persp_select.rs`.
|
||||
When the desktop app opens a document on a thread of its own (#1245), that thread also reads the document's linked files and lays out its type, and the UI thread only adds the finished document: in release builds, the step left on the UI thread takes 0.02 ms instead of 1.04 s for a document with 12 linked 3000 × 2000 TIFFs, and it takes 0.1 ms instead of 0.85 s for one with 300 Auto Size area type objects, every second one in three columns.
|
||||
Laying out type keeps each font face's shaping plans, at most 32 per face, so type objects of the same font, script, direction, language and features share one plan: the layouts of a document of 5,000 short point type objects take 59 ms instead of 73 ms in a release build, and the glyphs, bounds and baselines do not change.
|
||||
Laying out type looks up the font face of each style run once and uses it for the run's glyphs and for the line metrics of a paragraph that starts in that run: the layouts of a document of 5,000 short point type objects take a median of 215 million CPU cycles instead of 236 million in a release build (11 alternating runs of each build on Windows 11, measured while other builds were running), and the glyphs, bounds and baselines do not change.
|
||||
|
||||
Reference in New Issue
Block a user