mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-14 19:27:07 -04:00
Snap: dimensions on resize and create
This commit is contained in:
1 parent
b0797f2b47
commit
40f3b6454a
6 files changed
+746
-73
No files matched your search
@@ -106,6 +106,28 @@ fn cmd_move_does_not_snap() {
|
||||
assert!(b.x0 > 80.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmd_resize_still_snaps_to_a_guide() {
|
||||
use designcraft_tools::{PointerEvent, PointerKind};
|
||||
let mut s = session();
|
||||
s.execute("frame.create", &json!({"rect": [100.0, 100.0, 200.0, 180.0], "content": "unassigned"})).unwrap();
|
||||
s.execute("guide.add", &json!({"orientation": "vertical", "position": 250.0, "page": 0})).unwrap();
|
||||
s.set_tool("selection");
|
||||
let v = ViewInfo::at_zoom(1.0);
|
||||
let b0 = s.doc().unwrap().doc.spreads[0].items[0].bounds();
|
||||
let right = s.layout().to_canvas(designcraft_doc::SpreadRef::Doc(0), designcraft_geom::Point::new(b0.x1, b0.center().y));
|
||||
let cmd = designcraft_tools::Mods { cmd: true, ..Default::default() };
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, right.x, right.y).with_mods(cmd), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, right.x + 47.0, right.y).with_mods(cmd), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, right.x + 47.0, right.y).with_mods(cmd), v).unwrap();
|
||||
let it = &s.doc().unwrap().doc.spreads[0].items[0];
|
||||
let b = it.bounds();
|
||||
assert!((b.x1 - 250.0).abs() < 1e-6, "cmd resize did not snap the moving edge: {b:?}");
|
||||
assert!((b.x0 - b0.x0).abs() < 1e-6 && (b.y0 - b0.y0).abs() < 1e-6 && (b.y1 - b0.y1).abs() < 1e-6, "{b:?}");
|
||||
let expect = (b.x1 - b.x0).abs() / b0.width() * (b.y1 - b.y0).abs() / b0.height();
|
||||
assert!((it.stroke.weight - expect.sqrt()).abs() < 1e-6, "content scale weight {} want {}", it.stroke.weight, expect.sqrt());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_move_on_a_turned_spread_stays_on_the_line() {
|
||||
use designcraft_tools::{PointerEvent, PointerKind};
|
||||
|
||||
+111
-11
@@ -4,7 +4,7 @@
|
||||
use designcraft_geom::{Point, Rect};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, rect_json, spread_json};
|
||||
use crate::{Action, Cursor, Gesture, Mods, Overlay, PointerEvent, PointerKind, SnapRequest, Tool, ToolContext, rect_json, spread_json};
|
||||
|
||||
pub struct FrameTool {
|
||||
id: &'static str,
|
||||
@@ -63,6 +63,52 @@ pub fn drag_rect(a: Point, b: Point, m: Mods) -> Rect {
|
||||
}
|
||||
}
|
||||
|
||||
/// Edges the drag is setting. Alt (drawn from the centre) snaps the pointer side.
|
||||
fn moving_edges(anchor: Point, end: Point, rect: Rect, from_center: bool) -> ([bool; 3], [bool; 3]) {
|
||||
if from_center {
|
||||
let x = if (end.x - anchor.x).abs() <= 1e-9 {
|
||||
[false, false, false]
|
||||
} else if end.x >= anchor.x {
|
||||
[false, false, true]
|
||||
} else {
|
||||
[true, false, false]
|
||||
};
|
||||
let y = if (end.y - anchor.y).abs() <= 1e-9 {
|
||||
[false, false, false]
|
||||
} else if end.y >= anchor.y {
|
||||
[false, false, true]
|
||||
} else {
|
||||
[true, false, false]
|
||||
};
|
||||
return (x, y);
|
||||
}
|
||||
(anchored_edge(anchor.x, rect.x0, rect.x1), anchored_edge(anchor.y, rect.y0, rect.y1))
|
||||
}
|
||||
|
||||
fn anchored_edge(anchor: f64, a0: f64, a1: f64) -> [bool; 3] {
|
||||
let on0 = (a0 - anchor).abs() <= 1e-4;
|
||||
let on1 = (a1 - anchor).abs() <= 1e-4;
|
||||
if on0 && !on1 {
|
||||
[false, false, true]
|
||||
} else if on1 && !on0 {
|
||||
[true, false, false]
|
||||
} else if on0 && on1 {
|
||||
[false, false, false]
|
||||
} else if (a0 - anchor).abs() <= (a1 - anchor).abs() {
|
||||
[false, false, true]
|
||||
} else {
|
||||
[true, false, false]
|
||||
}
|
||||
}
|
||||
|
||||
fn length_on(edges: [bool; 3], size: f64) -> Option<f64> {
|
||||
if edges == [false, false, false] {
|
||||
return None;
|
||||
}
|
||||
let len = size.abs();
|
||||
len.is_finite().then_some(len)
|
||||
}
|
||||
|
||||
impl Tool for FrameTool {
|
||||
fn id(&self) -> &'static str {
|
||||
self.id
|
||||
@@ -90,14 +136,7 @@ impl Tool for FrameTool {
|
||||
return vec![];
|
||||
}
|
||||
let Some((sr, sa)) = cx.layout.spread_at(a) else { return vec![] };
|
||||
let mut ev = *ev;
|
||||
self.guides.clear();
|
||||
if cx.snap.any() && !ev.mods.shift {
|
||||
let (p, g) = crate::snap::snap_point(cx, sr, cx.layout.to_spread(sr, ev.pos));
|
||||
ev.pos = cx.layout.to_canvas(sr, p);
|
||||
self.guides = g;
|
||||
}
|
||||
let ev = &ev;
|
||||
let mut out = vec![];
|
||||
if !self.active {
|
||||
self.active = true;
|
||||
@@ -107,15 +146,76 @@ impl Tool for FrameTool {
|
||||
)));
|
||||
}
|
||||
if shape == "line" {
|
||||
// Shift constrains the angle first. The free endpoint still snaps.
|
||||
let mut b = cx.layout.to_spread(sr, ev.pos);
|
||||
if ev.mods.shift {
|
||||
let v = b - sa;
|
||||
let v = designcraft_geom::constrain_angle(v, 45.0);
|
||||
b = sa + v;
|
||||
b = sa + designcraft_geom::constrain_angle(v, 45.0);
|
||||
}
|
||||
if cx.snap.any() {
|
||||
let len = (b - sa).hypot();
|
||||
let hit = crate::snap::snap(
|
||||
cx,
|
||||
SnapRequest {
|
||||
spread: sr,
|
||||
gesture: Gesture::Create,
|
||||
rect: Rect::from_points(b, b),
|
||||
x_edges: [true, true, true],
|
||||
y_edges: [true, true, true],
|
||||
exclude: &[],
|
||||
copying: false,
|
||||
lengths: [len.is_finite().then_some(len), None],
|
||||
angle: None,
|
||||
radius: 0.0,
|
||||
pointer: b,
|
||||
},
|
||||
);
|
||||
if hit.length_delta[1].is_none() {
|
||||
b.y += hit.delta.y;
|
||||
}
|
||||
if hit.length_delta[0].is_none() {
|
||||
b.x += hit.delta.x;
|
||||
}
|
||||
if let Some(diff) = hit.length_delta[0]
|
||||
&& diff.is_finite()
|
||||
{
|
||||
let v = b - sa;
|
||||
let cur = v.hypot();
|
||||
if cur > 1e-9 {
|
||||
let next = cur + diff;
|
||||
if next.is_finite() {
|
||||
b = sa + v * (next / cur);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.guides = hit.guides;
|
||||
}
|
||||
out.push(Action::Preview("line.create".into(), json!({"spread": spread_json(sr), "a": [sa.x, sa.y], "b": [b.x, b.y]})));
|
||||
} else {
|
||||
let r = drag_rect(sa, cx.layout.to_spread(sr, ev.pos), ev.mods);
|
||||
// Shift constrains the shape first. The free edges still snap.
|
||||
let end = cx.layout.to_spread(sr, ev.pos);
|
||||
let mut r = drag_rect(sa, end, ev.mods);
|
||||
if cx.snap.any() {
|
||||
let (x_edges, y_edges) = moving_edges(sa, end, r, ev.mods.alt);
|
||||
let hit = crate::snap::snap(
|
||||
cx,
|
||||
SnapRequest {
|
||||
spread: sr,
|
||||
gesture: Gesture::Create,
|
||||
rect: r,
|
||||
x_edges,
|
||||
y_edges,
|
||||
exclude: &[],
|
||||
copying: false,
|
||||
lengths: [length_on(x_edges, r.width()), length_on(y_edges, r.height())],
|
||||
angle: None,
|
||||
radius: 0.0,
|
||||
pointer: end,
|
||||
},
|
||||
);
|
||||
r = crate::snap::nudge_edges(r, x_edges, y_edges, hit.delta);
|
||||
self.guides = hit.guides;
|
||||
}
|
||||
self.last = Some((spread_json(sr), r));
|
||||
out.push(self.preview(spread_json(sr), r));
|
||||
}
|
||||
|
||||
@@ -302,6 +302,9 @@ pub struct Snap {
|
||||
pub delta: Vec2,
|
||||
pub angle: Option<f64>,
|
||||
pub guides: Vec<Overlay>,
|
||||
/// Set when the dimensions pass won that axis. The value is the matched length minus the
|
||||
/// proposed length (positive grows the size). `delta` on that axis is the moving edge's shift.
|
||||
pub length_delta: [Option<f64>; 2],
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
||||
+162
-44
@@ -102,6 +102,11 @@ fn rotate_zone(cx: &ToolContext, p: Point) -> Option<Point> {
|
||||
|
||||
/// New rect when dragging `handle` of `from` to `p`.
|
||||
pub fn resize_rect(from: Rect, handle: usize, p: Point, m: Mods) -> Rect {
|
||||
finish_resize(from, handle, resize_edges_rect(from, handle, p), m)
|
||||
}
|
||||
|
||||
/// Edges the pointer sets, before Shift or Alt.
|
||||
fn resize_edges_rect(from: Rect, handle: usize, p: Point) -> Rect {
|
||||
let (mut x0, mut y0, mut x1, mut y1) = (from.x0, from.y0, from.x1, from.y1);
|
||||
match handle {
|
||||
0 => {
|
||||
@@ -125,53 +130,125 @@ pub fn resize_rect(from: Rect, handle: usize, p: Point, m: Mods) -> Rect {
|
||||
}
|
||||
_ => x0 = p.x,
|
||||
}
|
||||
if m.shift && from.width() > 1e-9 && from.height() > 1e-9 {
|
||||
let sx = (x1 - x0) / from.width();
|
||||
let sy = (y1 - y0) / from.height();
|
||||
let s = if matches!(handle, 1 | 5) {
|
||||
sy
|
||||
} else if matches!(handle, 3 | 7) || sx.abs() > sy.abs() {
|
||||
sx
|
||||
} else {
|
||||
sy
|
||||
};
|
||||
let (w, h) = (from.width() * s, from.height() * s);
|
||||
match handle {
|
||||
0 => {
|
||||
x0 = x1 - w;
|
||||
y0 = y1 - h;
|
||||
}
|
||||
2 => {
|
||||
x1 = x0 + w;
|
||||
y0 = y1 - h;
|
||||
}
|
||||
3..=5 => {
|
||||
x1 = x0 + w;
|
||||
y1 = y0 + h;
|
||||
}
|
||||
6 | 7 => {
|
||||
x0 = x1 - w;
|
||||
y1 = y0 + h;
|
||||
}
|
||||
_ => {
|
||||
x0 = x1 - w;
|
||||
y0 = y1 - h;
|
||||
}
|
||||
Rect::new(x0, y0, x1, y1)
|
||||
}
|
||||
|
||||
fn finish_resize(from: Rect, handle: usize, rect: Rect, m: Mods) -> Rect {
|
||||
let rect = if m.shift && from.width() > 1e-9 && from.height() > 1e-9 {
|
||||
apply_aspect(from, handle, rect, aspect_scale(from, handle, rect))
|
||||
} else {
|
||||
rect
|
||||
};
|
||||
if m.alt { apply_alt(from, handle, rect) } else { rect }
|
||||
}
|
||||
|
||||
fn aspect_scale(from: Rect, handle: usize, rect: Rect) -> f64 {
|
||||
let sx = (rect.x1 - rect.x0) / from.width();
|
||||
let sy = (rect.y1 - rect.y0) / from.height();
|
||||
if matches!(handle, 1 | 5) {
|
||||
sy
|
||||
} else if matches!(handle, 3 | 7) || sx.abs() > sy.abs() {
|
||||
sx
|
||||
} else {
|
||||
sy
|
||||
}
|
||||
}
|
||||
|
||||
/// True when the width, not the height, drives a Shift constraint.
|
||||
fn drives_x(handle: usize, from: Rect, rect: Rect) -> bool {
|
||||
if matches!(handle, 1 | 5) {
|
||||
return false;
|
||||
}
|
||||
if matches!(handle, 3 | 7) {
|
||||
return true;
|
||||
}
|
||||
if from.width() <= 1e-9 || from.height() <= 1e-9 {
|
||||
return true;
|
||||
}
|
||||
let sx = ((rect.x1 - rect.x0) / from.width()).abs();
|
||||
let sy = ((rect.y1 - rect.y0) / from.height()).abs();
|
||||
sx > sy
|
||||
}
|
||||
|
||||
fn apply_aspect(from: Rect, handle: usize, rect: Rect, s: f64) -> Rect {
|
||||
let (mut x0, mut y0, mut x1, mut y1) = (rect.x0, rect.y0, rect.x1, rect.y1);
|
||||
let (w, h) = (from.width() * s, from.height() * s);
|
||||
match handle {
|
||||
0 => {
|
||||
x0 = x1 - w;
|
||||
y0 = y1 - h;
|
||||
}
|
||||
2 => {
|
||||
x1 = x0 + w;
|
||||
y0 = y1 - h;
|
||||
}
|
||||
3..=5 => {
|
||||
x1 = x0 + w;
|
||||
y1 = y0 + h;
|
||||
}
|
||||
6 | 7 => {
|
||||
x0 = x1 - w;
|
||||
y1 = y0 + h;
|
||||
}
|
||||
_ => {
|
||||
x0 = x1 - w;
|
||||
y0 = y1 - h;
|
||||
}
|
||||
}
|
||||
if m.alt {
|
||||
let c = from.center();
|
||||
let (hw, hh) = ((x1 - x0) / 2.0, (y1 - y0) / 2.0);
|
||||
let (hw, hh) = match handle {
|
||||
1 | 5 => (from.width() / 2.0, (if handle == 1 { c.y - y0 } else { y1 - c.y })),
|
||||
3 | 7 => ((if handle == 7 { c.x - x0 } else { x1 - c.x }), from.height() / 2.0),
|
||||
_ => (hw.abs().max((c.x - x0).abs()).max((x1 - c.x).abs()), hh.abs().max((c.y - y0).abs()).max((y1 - c.y).abs())),
|
||||
};
|
||||
return Rect::new(c.x - hw, c.y - hh, c.x + hw, c.y + hh);
|
||||
}
|
||||
Rect::new(x0, y0, x1, y1)
|
||||
}
|
||||
|
||||
fn apply_alt(from: Rect, handle: usize, rect: Rect) -> Rect {
|
||||
let (x0, y0, x1, y1) = (rect.x0, rect.y0, rect.x1, rect.y1);
|
||||
let c = from.center();
|
||||
let (hw, hh) = ((x1 - x0) / 2.0, (y1 - y0) / 2.0);
|
||||
let (hw, hh) = match handle {
|
||||
1 | 5 => (from.width() / 2.0, (if handle == 1 { c.y - y0 } else { y1 - c.y })),
|
||||
3 | 7 => ((if handle == 7 { c.x - x0 } else { x1 - c.x }), from.height() / 2.0),
|
||||
_ => (hw.abs().max((c.x - x0).abs()).max((x1 - c.x).abs()), hh.abs().max((c.y - y0).abs()).max((y1 - c.y).abs())),
|
||||
};
|
||||
Rect::new(c.x - hw, c.y - hh, c.x + hw, c.y + hh)
|
||||
}
|
||||
|
||||
/// Left/center/right and top/center/bottom for a handle. Centers stay off.
|
||||
fn resize_edge_flags(handle: usize) -> ([bool; 3], [bool; 3]) {
|
||||
let x = match handle {
|
||||
0 | 6 | 7 => [true, false, false],
|
||||
2..=4 => [false, false, true],
|
||||
_ => [false, false, false],
|
||||
};
|
||||
let y = match handle {
|
||||
0..=2 => [true, false, false],
|
||||
4..=6 => [false, false, true],
|
||||
_ => [false, false, false],
|
||||
};
|
||||
(x, y)
|
||||
}
|
||||
|
||||
fn axis_length(edges: [bool; 3], size: f64, pad: f64) -> Option<f64> {
|
||||
if edges == [false, false, false] {
|
||||
return None;
|
||||
}
|
||||
let len = size.abs() + pad;
|
||||
len.is_finite().then_some(len)
|
||||
}
|
||||
|
||||
/// Visible size minus path size, for one selected item. Centered stroke then matches the outer edge.
|
||||
fn single_stroke_pad(cx: &ToolContext) -> (f64, f64) {
|
||||
if cx.selection.items.len() != 1 {
|
||||
return (0.0, 0.0);
|
||||
}
|
||||
let Some(id) = cx.selection.items.first() else { return (0.0, 0.0) };
|
||||
let Some(it) = cx.doc.item(*id) else { return (0.0, 0.0) };
|
||||
let path = it.bounds();
|
||||
let vis = it.visible_bounds();
|
||||
(positive_pad(vis.width() - path.width()), positive_pad(vis.height() - path.height()))
|
||||
}
|
||||
|
||||
fn positive_pad(v: f64) -> f64 {
|
||||
if v.is_finite() && v > 0.0 { v } else { 0.0 }
|
||||
}
|
||||
|
||||
impl Tool for SelectionTool {
|
||||
fn id(&self) -> &'static str {
|
||||
if self.direct { "directSelection" } else { "selection" }
|
||||
@@ -327,10 +404,51 @@ impl Tool for SelectionTool {
|
||||
vec![Action::Preview("transform.move".into(), params)]
|
||||
}
|
||||
Drag::Resize { handle, start, from, spread } => {
|
||||
let to = resize_rect(from, handle, cx.layout.to_spread(spread, p), ev.mods);
|
||||
let _ = start;
|
||||
let pointer = cx.layout.to_spread(spread, p);
|
||||
let raw = resize_edges_rect(from, handle, pointer);
|
||||
let mut to = resize_rect(from, handle, pointer, ev.mods);
|
||||
// Space while dragging: Live Distribute (several objects keep their size).
|
||||
let distribute = ev.mods.space && cx.selection.items.len() > 1;
|
||||
self.guides.clear();
|
||||
// Command still scales content. It does not skip the snap.
|
||||
if !distribute && cx.snap.any() {
|
||||
let (mut x_edges, mut y_edges) = resize_edge_flags(handle);
|
||||
let driver_x = drives_x(handle, from, raw);
|
||||
if ev.mods.shift {
|
||||
if driver_x {
|
||||
y_edges = [false, false, false];
|
||||
} else {
|
||||
x_edges = [false, false, false];
|
||||
}
|
||||
}
|
||||
let exclude = cx.selection.items.clone();
|
||||
let (pad_x, pad_y) = single_stroke_pad(cx);
|
||||
let hit = crate::snap::snap(
|
||||
cx,
|
||||
SnapRequest {
|
||||
spread,
|
||||
gesture: Gesture::Resize,
|
||||
rect: to,
|
||||
x_edges,
|
||||
y_edges,
|
||||
exclude: &exclude,
|
||||
copying: false,
|
||||
lengths: [axis_length(x_edges, to.width(), pad_x), axis_length(y_edges, to.height(), pad_y)],
|
||||
angle: None,
|
||||
radius: 0.0,
|
||||
pointer,
|
||||
},
|
||||
);
|
||||
to = crate::snap::nudge_edges(to, x_edges, y_edges, hit.delta);
|
||||
if ev.mods.shift && !ev.mods.alt && from.width() > 1e-9 && from.height() > 1e-9 {
|
||||
let s = if driver_x { (to.x1 - to.x0) / from.width() } else { (to.y1 - to.y0) / from.height() };
|
||||
if s.is_finite() {
|
||||
to = apply_aspect(from, handle, to, s);
|
||||
}
|
||||
}
|
||||
self.guides = hit.guides;
|
||||
}
|
||||
vec![Action::Preview(
|
||||
"transform.resize".into(),
|
||||
json!({"from": rect_json(from), "to": rect_json(to), "content": ev.mods.cmd, "distribute": distribute}),
|
||||
@@ -423,7 +541,7 @@ impl Tool for SelectionTool {
|
||||
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
|
||||
match &self.drag {
|
||||
Drag::Marquee { start, cur } => vec![Overlay::Marquee(Rect::from_points(*start, *cur))],
|
||||
Drag::Move { .. } => self.guides.clone(),
|
||||
Drag::Move { .. } | Drag::Resize { .. } => self.guides.clone(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
+319
-9
@@ -6,9 +6,9 @@
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use designcraft_doc::{Item, ItemId, Orientation, PageSide, Selection, SpreadId, SpreadRef};
|
||||
use designcraft_doc::{Item, ItemId, Orientation, PageSide, Selection, SpreadId, SpreadRef, StrokeAlign};
|
||||
use designcraft_geom::snap::snap_to_grid;
|
||||
use designcraft_geom::{Point, Rect};
|
||||
use designcraft_geom::{Point, Rect, Vec2, format_measure};
|
||||
|
||||
pub use crate::{Gesture, Snap, SnapRequest, SnapView};
|
||||
|
||||
@@ -149,33 +149,71 @@ pub fn snap(cx: &ToolContext, req: SnapRequest<'_>) -> Snap {
|
||||
let mut out = Snap::default();
|
||||
if let Some(hit) = &x_hit {
|
||||
out.delta.x = hit.delta;
|
||||
out.length_delta[0] = hit.length_delta;
|
||||
}
|
||||
if let Some(hit) = &y_hit {
|
||||
out.delta.y = hit.delta;
|
||||
out.length_delta[1] = hit.length_delta;
|
||||
}
|
||||
let placed = Rect::new(req.rect.x0 + out.delta.x, req.rect.y0 + out.delta.y, req.rect.x1 + out.delta.x, req.rect.y1 + out.delta.y);
|
||||
if let Some(hit) = &x_hit {
|
||||
push_alignment_guide(&mut out.guides, cx, req.spread, Axis::X, hit, placed);
|
||||
push_hit_guides(&mut out.guides, cx, &req, Axis::X, hit, placed);
|
||||
}
|
||||
if let Some(hit) = &y_hit {
|
||||
push_alignment_guide(&mut out.guides, cx, req.spread, Axis::Y, hit, placed);
|
||||
push_hit_guides(&mut out.guides, cx, &req, Axis::Y, hit, placed);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Move the flagged edges by `delta`.
|
||||
///
|
||||
/// One edge takes the whole delta. Both edges of an axis split it, low edge one way and high
|
||||
/// edge the other, so a length match keeps the centre. A non-finite component is ignored.
|
||||
pub(crate) fn nudge_edges(mut rect: Rect, x_edges: [bool; 3], y_edges: [bool; 3], delta: Vec2) -> Rect {
|
||||
nudge_axis(&mut rect.x0, &mut rect.x1, x_edges, delta.x);
|
||||
nudge_axis(&mut rect.y0, &mut rect.y1, y_edges, delta.y);
|
||||
rect
|
||||
}
|
||||
|
||||
fn nudge_axis(lo: &mut f64, hi: &mut f64, flags: [bool; 3], delta: f64) {
|
||||
if !delta.is_finite() {
|
||||
return;
|
||||
}
|
||||
let [low_edge, _, high_edge] = flags;
|
||||
if low_edge && high_edge {
|
||||
*lo -= delta;
|
||||
*hi += delta;
|
||||
} else if low_edge {
|
||||
*lo += delta;
|
||||
} else if high_edge {
|
||||
*hi += delta;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Axis {
|
||||
X,
|
||||
Y,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone)]
|
||||
struct AxisHit {
|
||||
delta: f64,
|
||||
/// Position of the winning line on this axis.
|
||||
at: f64,
|
||||
/// Target box for an alignment guide. None when the hit draws nothing.
|
||||
span: Option<Rect>,
|
||||
/// Matched length minus the proposed length, when this hit is a dimension match.
|
||||
length_delta: Option<f64>,
|
||||
/// Spread-space segment along the matched side, plus the measure text.
|
||||
dimension: Option<DimMark>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct DimMark {
|
||||
a: Point,
|
||||
b: Point,
|
||||
text: String,
|
||||
}
|
||||
|
||||
struct Best {
|
||||
@@ -343,8 +381,232 @@ fn pass_spacing(_cx: &ToolContext, _req: &SnapRequest<'_>, _axis: Axis, _tol: f6
|
||||
None
|
||||
}
|
||||
|
||||
fn pass_dimensions(_cx: &ToolContext, _req: &SnapRequest<'_>, _axis: Axis, _tol: f64) -> Option<AxisHit> {
|
||||
None
|
||||
/// Other items' own side lengths, on resize and create.
|
||||
///
|
||||
/// Own size is the path box in item space, inflated the way [`Item::visible_bounds`] inflates,
|
||||
/// then multiplied by the absolute scale (ancestors included, rotation dropped). Width and height
|
||||
/// are both candidates for whichever length is set. The closest length inside the zone wins.
|
||||
/// Delta is the length difference applied from the moving edge. A rotated rectangle contributes
|
||||
/// its side lengths, not the axis-aligned box.
|
||||
fn pass_dimensions(cx: &ToolContext, req: &SnapRequest<'_>, axis: Axis, tol: f64) -> Option<AxisHit> {
|
||||
if !matches!(req.gesture, Gesture::Resize | Gesture::Create) || !cx.snap.smart_guides || !cx.snap.smart_dimensions {
|
||||
return None;
|
||||
}
|
||||
let proposed = match axis {
|
||||
Axis::X => req.lengths[0],
|
||||
Axis::Y => req.lengths[1],
|
||||
}?;
|
||||
if !proposed.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let mut best: Option<(f64, f64)> = None;
|
||||
for len in dimension_lengths(cx, req) {
|
||||
if !len.is_finite() || len < 0.0 {
|
||||
continue;
|
||||
}
|
||||
let dist = (len - proposed).abs();
|
||||
if dist <= tol && best.is_none_or(|(have, _)| dist < have) {
|
||||
best = Some((dist, len));
|
||||
}
|
||||
}
|
||||
let (_, length) = best?;
|
||||
let diff = length - proposed;
|
||||
if !diff.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let flags = axis_flags(req, axis);
|
||||
let text = format_measure(length, cx.unit);
|
||||
Some(AxisHit {
|
||||
delta: length_edge_delta(flags, diff),
|
||||
at: 0.0,
|
||||
span: None,
|
||||
length_delta: Some(diff),
|
||||
dimension: Some(dimension_mark(req, axis, diff, text)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Edge shift so adding it to the moving edge grows the length by `diff`.
|
||||
fn length_edge_delta(flags: [bool; 3], diff: f64) -> f64 {
|
||||
let [low_edge, mid, high_edge] = flags;
|
||||
if low_edge && high_edge && !mid {
|
||||
diff / 2.0
|
||||
} else if high_edge && !low_edge {
|
||||
diff
|
||||
} else if low_edge && !high_edge {
|
||||
-diff
|
||||
} else {
|
||||
diff
|
||||
}
|
||||
}
|
||||
|
||||
/// Guide along the side whose length matched, after the edge has moved.
|
||||
fn dimension_mark(req: &SnapRequest<'_>, axis: Axis, diff: f64, text: String) -> DimMark {
|
||||
let rect = req.rect;
|
||||
let flags = axis_flags(req, axis);
|
||||
let (a, b) = match axis {
|
||||
Axis::X => {
|
||||
let (x0, x1) = grown_span(rect.x0, rect.x1, flags, diff);
|
||||
let y = rect.y0.max(rect.y1);
|
||||
(Point::new(x0, y), Point::new(x1, y))
|
||||
}
|
||||
Axis::Y => {
|
||||
let (y0, y1) = grown_span(rect.y0, rect.y1, flags, diff);
|
||||
let x = rect.x0.max(rect.x1);
|
||||
(Point::new(x, y0), Point::new(x, y1))
|
||||
}
|
||||
};
|
||||
DimMark { a, b, text }
|
||||
}
|
||||
|
||||
fn grown_span(a0: f64, a1: f64, flags: [bool; 3], diff: f64) -> (f64, f64) {
|
||||
let (lo, hi) = if a0 <= a1 { (a0, a1) } else { (a1, a0) };
|
||||
let [low_edge, mid, high_edge] = flags;
|
||||
if low_edge && high_edge && !mid {
|
||||
let mid_v = (lo + hi) / 2.0;
|
||||
let half = (hi - lo) / 2.0 + diff / 2.0;
|
||||
(mid_v - half, mid_v + half)
|
||||
} else if low_edge && !high_edge {
|
||||
(lo - diff, hi)
|
||||
} else {
|
||||
(lo, hi + diff)
|
||||
}
|
||||
}
|
||||
|
||||
fn dimension_lengths(cx: &ToolContext, req: &SnapRequest<'_>) -> Vec<f64> {
|
||||
let mut out = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
if let Some(sp) = cx.doc.spread(req.spread) {
|
||||
for item in &sp.items {
|
||||
push_own_lengths(&mut out, cx.selection, req, item, 1.0, 1.0, false, 0, &mut seen);
|
||||
}
|
||||
}
|
||||
if !layout_edits_parents(cx) {
|
||||
push_parent_lengths(&mut out, cx, req, &mut seen);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn push_own_lengths(
|
||||
out: &mut Vec<f64>,
|
||||
selection: &Selection,
|
||||
req: &SnapRequest<'_>,
|
||||
item: &Item,
|
||||
ancestor_sx: f64,
|
||||
ancestor_sy: f64,
|
||||
skipped: bool,
|
||||
depth: usize,
|
||||
seen: &mut HashSet<ItemId>,
|
||||
) {
|
||||
if depth >= ALIGN_GROUP_DEPTH || !seen.insert(item.id) || item.hidden {
|
||||
return;
|
||||
}
|
||||
let skip = skipped || align_skips_item(selection, req, item.id);
|
||||
if item.is_group() && !item.children().is_empty() {
|
||||
let decomposed = designcraft_geom::decompose::decompose(item.xf);
|
||||
let sx = ancestor_sx * decomposed.scale_x.abs();
|
||||
let sy = ancestor_sy * decomposed.scale_y.abs();
|
||||
if !sx.is_finite() || !sy.is_finite() {
|
||||
return;
|
||||
}
|
||||
for child in item.children() {
|
||||
push_own_lengths(out, selection, req, child, sx, sy, skip, depth + 1, seen);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if skip {
|
||||
return;
|
||||
}
|
||||
let Some((w, h)) = leaf_own_size(item, ancestor_sx, ancestor_sy) else { return };
|
||||
out.push(w);
|
||||
out.push(h);
|
||||
}
|
||||
|
||||
/// Path bounds in item space, stroke outset, then absolute scale. Rotation is not applied.
|
||||
fn leaf_own_size(item: &Item, ancestor_sx: f64, ancestor_sy: f64) -> Option<(f64, f64)> {
|
||||
let local = item.path.bounds()?;
|
||||
if !rect_finite(local) {
|
||||
return None;
|
||||
}
|
||||
let outset = stroke_outset(item);
|
||||
if !outset.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let local = if outset > 0.0 { local.inflate(outset, outset) } else { local };
|
||||
if !rect_finite(local) {
|
||||
return None;
|
||||
}
|
||||
let decomposed = designcraft_geom::decompose::decompose(item.xf);
|
||||
let sx = ancestor_sx * decomposed.scale_x.abs();
|
||||
let sy = ancestor_sy * decomposed.scale_y.abs();
|
||||
if !sx.is_finite() || !sy.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let w = (local.x1 - local.x0).abs() * sx;
|
||||
let h = (local.y1 - local.y0).abs() * sy;
|
||||
(w.is_finite() && h.is_finite()).then_some((w, h))
|
||||
}
|
||||
|
||||
/// Same outset as [`Item::visible_bounds`]: half the weight when centered, the whole weight when
|
||||
/// outside, nothing when inside or when the stroke is none. Open paths also count arrowheads.
|
||||
fn stroke_outset(item: &Item) -> f64 {
|
||||
if item.stroke.is_none() {
|
||||
return 0.0;
|
||||
}
|
||||
let w = match item.stroke.align {
|
||||
StrokeAlign::Center => item.stroke.weight / 2.0,
|
||||
StrokeAlign::Inside => 0.0,
|
||||
StrokeAlign::Outside => item.stroke.weight,
|
||||
};
|
||||
if item.path.is_closed() { w } else { w.max(item.stroke.extent() - item.stroke.weight) }
|
||||
}
|
||||
|
||||
fn push_parent_lengths(out: &mut Vec<f64>, cx: &ToolContext, req: &SnapRequest<'_>, seen_items: &mut HashSet<ItemId>) {
|
||||
let SpreadRef::Doc(si) = req.spread else { return };
|
||||
let page_count = cx.doc.spreads.get(si).map(|sp| sp.pages.len()).unwrap_or(0);
|
||||
let first = cx.doc.first_page_of_spread(si);
|
||||
for pi in 0..page_count {
|
||||
let Some(abs) = first.checked_add(pi) else { continue };
|
||||
let Some(page) = cx.doc.page(abs) else { continue };
|
||||
if !page.show_parent_items {
|
||||
continue;
|
||||
}
|
||||
let doc_side = page.side;
|
||||
let overridden = page.overridden.clone();
|
||||
let Some((ppi, _)) = cx.doc.parent_page_for(abs) else { continue };
|
||||
let mut seen_spreads = HashSet::new();
|
||||
push_parent_length_chain(out, cx, req, doc_side, &overridden, ppi, seen_items, &mut seen_spreads);
|
||||
}
|
||||
}
|
||||
|
||||
fn push_parent_length_chain(
|
||||
out: &mut Vec<f64>,
|
||||
cx: &ToolContext,
|
||||
req: &SnapRequest<'_>,
|
||||
doc_side: PageSide,
|
||||
overridden: &[ItemId],
|
||||
index: usize,
|
||||
seen_items: &mut HashSet<ItemId>,
|
||||
seen_spreads: &mut HashSet<SpreadId>,
|
||||
) {
|
||||
let Some(parent) = cx.doc.parents.get(index) else { return };
|
||||
if !seen_spreads.insert(parent.id) {
|
||||
return;
|
||||
}
|
||||
let next = parent.parent.as_ref().and_then(|info| info.based_on);
|
||||
if let Some(page_idx) = shown_parent_page_index(parent.pages.len(), doc_side) {
|
||||
for item in &parent.items {
|
||||
if overridden.contains(&item.id) {
|
||||
continue;
|
||||
}
|
||||
if parent.pages.len() > 1 && parent.page_at_x(item.bounds().center().x) != Some(page_idx) {
|
||||
continue;
|
||||
}
|
||||
push_own_lengths(out, cx.selection, req, item, 1.0, 1.0, false, 0, seen_items);
|
||||
}
|
||||
}
|
||||
let Some(next_id) = next else { return };
|
||||
let Some(next_index) = cx.doc.parent_index(next_id) else { return };
|
||||
push_parent_length_chain(out, cx, req, doc_side, overridden, next_index, seen_items, seen_spreads);
|
||||
}
|
||||
|
||||
/// Ruler guides, margins, and column sides. No overlay.
|
||||
@@ -626,7 +888,20 @@ fn offer(best: &mut Option<Best>, moving: f64, target: f64, tol: f64, span: Opti
|
||||
let delta = target - moving;
|
||||
let dist = delta.abs();
|
||||
if dist <= tol && best.as_ref().is_none_or(|b| dist < b.dist) {
|
||||
*best = Some(Best { dist, hit: AxisHit { delta, at: target, span } });
|
||||
*best = Some(Best { dist, hit: AxisHit { delta, at: target, span, length_delta: None, dimension: None } });
|
||||
}
|
||||
}
|
||||
|
||||
fn push_hit_guides(out: &mut Vec<Overlay>, cx: &ToolContext, req: &SnapRequest<'_>, axis: Axis, hit: &AxisHit, moving: Rect) {
|
||||
push_alignment_guide(out, cx, req.spread, axis, hit, moving);
|
||||
let Some(mark) = &hit.dimension else { return };
|
||||
let xf = cx.layout.xf(req.spread);
|
||||
if mark.a.x.is_finite() && mark.a.y.is_finite() && mark.b.x.is_finite() && mark.b.y.is_finite() {
|
||||
out.push(Overlay::Guide { a: xf * mark.a, b: xf * mark.b });
|
||||
}
|
||||
let p = xf * req.pointer;
|
||||
if p.x.is_finite() && p.y.is_finite() {
|
||||
out.push(Overlay::Measure { p, text: mark.text.clone() });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -659,8 +934,8 @@ mod tests {
|
||||
use designcraft_compose::Cache;
|
||||
use designcraft_doc::build::NewDocument;
|
||||
use designcraft_doc::{Document, Guide, Item, Margins, Selection, Shape, Stroke};
|
||||
use designcraft_geom::Unit;
|
||||
use designcraft_geom::shapes;
|
||||
use designcraft_geom::{Affine, Unit};
|
||||
|
||||
use crate::layout::CanvasLayout;
|
||||
|
||||
@@ -968,4 +1243,39 @@ mod tests {
|
||||
let hit = snap(&cx, y_req(Rect::new(100.0, 49.0, 140.0, 80.0)));
|
||||
assert_eq!(hit.delta.y, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotated_item_matches_its_own_side() {
|
||||
let mut doc = Document::new(&NewDocument::default());
|
||||
let layer = doc.default_layer();
|
||||
let id = ItemId(doc.alloc());
|
||||
let mut item = Item::new(id, layer, Shape::Rectangle, shapes::rectangle(Rect::new(0.0, 0.0, 200.0, 40.0)));
|
||||
item.xf = Affine::rotate((-30.0_f64).to_radians());
|
||||
doc.insert_item(SpreadRef::Doc(0), item, None).unwrap();
|
||||
let cache = Cache::new();
|
||||
let layout = CanvasLayout::new(&doc, false);
|
||||
let sel = Selection::default();
|
||||
let mut cx = ctx_on(&doc, &sel, &cache, &layout);
|
||||
cx.snap.snap_to_guides = false;
|
||||
cx.snap.snap_to_document_grid = false;
|
||||
// Width 193.2 is the axis-aligned box. 200 is the side. Zone 4 reaches 200 from 197, not from 193.
|
||||
let hit = snap(
|
||||
&cx,
|
||||
SnapRequest {
|
||||
spread: SpreadRef::Doc(0),
|
||||
gesture: Gesture::Resize,
|
||||
rect: Rect::new(80.0, 200.0, 277.0, 280.0),
|
||||
x_edges: [false, false, true],
|
||||
y_edges: [false, false, false],
|
||||
exclude: &[],
|
||||
copying: false,
|
||||
lengths: [Some(197.0), None],
|
||||
angle: None,
|
||||
radius: 0.0,
|
||||
pointer: Point::new(277.0, 240.0),
|
||||
},
|
||||
);
|
||||
assert!((hit.delta.x - 3.0).abs() < 1e-6);
|
||||
assert!(matches!(hit.guides.last(), Some(Overlay::Measure { text, .. }) if text.contains("200")));
|
||||
}
|
||||
}
|
||||
+129
-9
@@ -2,22 +2,55 @@
|
||||
//! the reference point elsewhere in a later iteration); Eyedropper (I): click an object to apply
|
||||
//! its attributes to the selection.
|
||||
|
||||
use designcraft_geom::Point;
|
||||
use designcraft_doc::SpreadRef;
|
||||
use designcraft_geom::{Point, Rect};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{Action, Cursor, Mods, PointerEvent, PointerKind, Tool, ToolContext};
|
||||
use crate::{Action, Cursor, Gesture, Mods, Overlay, PointerEvent, PointerKind, SnapRequest, Tool, ToolContext};
|
||||
|
||||
pub struct XformTool {
|
||||
id: &'static str,
|
||||
drag: Option<(Point, Point)>,
|
||||
guides: Vec<Overlay>,
|
||||
}
|
||||
|
||||
impl XformTool {
|
||||
pub fn new(id: &'static str) -> Self {
|
||||
Self { id, drag: None }
|
||||
Self { id, drag: None, guides: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Top-level selection on one spread, in that spread's coordinates.
|
||||
fn selection_spread_bounds(cx: &ToolContext) -> Option<(SpreadRef, Rect)> {
|
||||
let mut spread = None;
|
||||
let mut rect: Option<Rect> = None;
|
||||
for id in &cx.selection.items {
|
||||
let loc = cx.doc.find(*id)?;
|
||||
if loc.path.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
if let Some(have) = spread
|
||||
&& have != loc.spread
|
||||
{
|
||||
return None;
|
||||
}
|
||||
spread = Some(loc.spread);
|
||||
let b = cx.doc.item(*id)?.bounds();
|
||||
if !(b.x0.is_finite() && b.y0.is_finite() && b.x1.is_finite() && b.y1.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
rect = Some(match rect {
|
||||
Some(r) => r.union(b),
|
||||
None => b,
|
||||
});
|
||||
}
|
||||
Some((spread?, rect?))
|
||||
}
|
||||
|
||||
fn edge_toward(pointer: f64, center: f64) -> [bool; 3] {
|
||||
if pointer >= center { [false, false, true] } else { [true, false, false] }
|
||||
}
|
||||
|
||||
impl Tool for XformTool {
|
||||
fn id(&self) -> &'static str {
|
||||
self.id
|
||||
@@ -25,6 +58,7 @@ impl Tool for XformTool {
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
match ev.kind {
|
||||
PointerKind::Down => {
|
||||
self.guides.clear();
|
||||
let Some(b) = cx.selection_bounds() else { return vec![] };
|
||||
self.drag = Some((b.center(), ev.pos));
|
||||
vec![Action::Begin(match self.id {
|
||||
@@ -36,6 +70,7 @@ impl Tool for XformTool {
|
||||
PointerKind::Drag => {
|
||||
let Some((c, start)) = self.drag else { return vec![] };
|
||||
let (v0, v1) = (start - c, ev.pos - c);
|
||||
self.guides.clear();
|
||||
match self.id {
|
||||
"rotate" => {
|
||||
let mut a = (v1.atan2() - v0.atan2()).to_degrees();
|
||||
@@ -45,12 +80,74 @@ impl Tool for XformTool {
|
||||
vec![Action::Preview("transform.rotate".into(), json!({"angle": -a}))]
|
||||
}
|
||||
"scale" => {
|
||||
let (mut sx, mut sy) = (v1.x / v0.x.abs().max(1e-6) * v0.x.signum(), v1.y / v0.y.abs().max(1e-6) * v0.y.signum());
|
||||
let (sx0, sy0) = (v1.x / v0.x.abs().max(1e-6) * v0.x.signum(), v1.y / v0.y.abs().max(1e-6) * v0.y.signum());
|
||||
let x_drives = !ev.mods.shift || sx0.abs() >= sy0.abs();
|
||||
let (mut sx, mut sy) = (sx0, sy0);
|
||||
if ev.mods.shift {
|
||||
let s = sx.abs().max(sy.abs());
|
||||
sx = s;
|
||||
sy = s;
|
||||
}
|
||||
if cx.snap.any()
|
||||
&& sx.is_finite()
|
||||
&& sy.is_finite()
|
||||
&& let Some((spread, from)) = selection_spread_bounds(cx)
|
||||
{
|
||||
let (sign_x, sign_y) = (if sx < 0.0 { -1.0 } else { 1.0 }, if sy < 0.0 { -1.0 } else { 1.0 });
|
||||
let (w0, h0) = (from.width().abs(), from.height().abs());
|
||||
if w0 > 1e-9 && h0 > 1e-9 {
|
||||
let center = from.center();
|
||||
let (mut w, mut h) = (w0 * sx.abs(), h0 * sy.abs());
|
||||
let to = Rect::new(center.x - w / 2.0, center.y - h / 2.0, center.x + w / 2.0, center.y + h / 2.0);
|
||||
let pointer = cx.layout.to_spread(spread, ev.pos);
|
||||
let x_edges = if x_drives { edge_toward(pointer.x, center.x) } else { [false, false, false] };
|
||||
let y_edges = if x_drives && ev.mods.shift { [false, false, false] } else { edge_toward(pointer.y, center.y) };
|
||||
let exclude = cx.selection.items.clone();
|
||||
let (pad_x, pad_y) = scale_stroke_pad(cx);
|
||||
let hit = crate::snap::snap(
|
||||
cx,
|
||||
SnapRequest {
|
||||
spread,
|
||||
gesture: Gesture::Resize,
|
||||
rect: to,
|
||||
x_edges,
|
||||
y_edges,
|
||||
exclude: &exclude,
|
||||
copying: false,
|
||||
lengths: [length_on(x_edges, w, pad_x), length_on(y_edges, h, pad_y)],
|
||||
angle: None,
|
||||
radius: 0.0,
|
||||
pointer,
|
||||
},
|
||||
);
|
||||
if let Some(diff) = hit.length_delta[0] {
|
||||
w += diff;
|
||||
} else if x_edges[2] {
|
||||
w += 2.0 * hit.delta.x;
|
||||
} else if x_edges[0] {
|
||||
w -= 2.0 * hit.delta.x;
|
||||
}
|
||||
if let Some(diff) = hit.length_delta[1] {
|
||||
h += diff;
|
||||
} else if y_edges[2] {
|
||||
h += 2.0 * hit.delta.y;
|
||||
} else if y_edges[0] {
|
||||
h -= 2.0 * hit.delta.y;
|
||||
}
|
||||
if ev.mods.shift {
|
||||
if x_drives {
|
||||
h = h0 * (w / w0);
|
||||
} else {
|
||||
w = w0 * (h / h0);
|
||||
}
|
||||
}
|
||||
if w.is_finite() && h.is_finite() && w.abs() > 1e-9 && h.abs() > 1e-9 {
|
||||
sx = (w / w0) * sign_x;
|
||||
sy = (h / h0) * sign_y;
|
||||
}
|
||||
self.guides = hit.guides;
|
||||
}
|
||||
}
|
||||
vec![Action::Preview("transform.scale".into(), json!({"sx": sx, "sy": sy}))]
|
||||
}
|
||||
_ => {
|
||||
@@ -60,15 +157,15 @@ impl Tool for XformTool {
|
||||
}
|
||||
}
|
||||
PointerKind::Up => {
|
||||
if self.drag.take().is_some() {
|
||||
vec![Action::Commit]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
self.guides.clear();
|
||||
if self.drag.take().is_some() { vec![Action::Commit] } else { vec![] }
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
|
||||
self.guides.clone()
|
||||
}
|
||||
fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor {
|
||||
if self.id == "rotate" { Cursor::Rotate } else { Cursor::Crosshair }
|
||||
}
|
||||
@@ -77,6 +174,29 @@ impl Tool for XformTool {
|
||||
}
|
||||
}
|
||||
|
||||
fn length_on(edges: [bool; 3], size: f64, pad: f64) -> Option<f64> {
|
||||
if edges == [false, false, false] {
|
||||
return None;
|
||||
}
|
||||
let len = size.abs() + pad;
|
||||
len.is_finite().then_some(len)
|
||||
}
|
||||
|
||||
fn scale_stroke_pad(cx: &ToolContext) -> (f64, f64) {
|
||||
if cx.selection.items.len() != 1 {
|
||||
return (0.0, 0.0);
|
||||
}
|
||||
let Some(id) = cx.selection.items.first() else { return (0.0, 0.0) };
|
||||
let Some(it) = cx.doc.item(*id) else { return (0.0, 0.0) };
|
||||
let path = it.bounds();
|
||||
let vis = it.visible_bounds();
|
||||
(positive_pad(vis.width() - path.width()), positive_pad(vis.height() - path.height()))
|
||||
}
|
||||
|
||||
fn positive_pad(v: f64) -> f64 {
|
||||
if v.is_finite() && v > 0.0 { v } else { 0.0 }
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EyedropperTool;
|
||||
|
||||
|
||||
Reference in new issue
Block a user