mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-09 03:51:42 +00:00
Placement crosshair: kept with its page and document, gone with a pointer placement, kept when the same tool is chosen again
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
367f188b34
commit
56ad0ba14f
7 files changed
+73
-12
No files matched your search
@@ -590,9 +590,12 @@ pub struct ToolState {
|
||||
/// The widget layers whose hover styles show while the pointer is over them (a live preview).
|
||||
pub(crate) widget_hover: Vec<NodeId>,
|
||||
/// The placement crosshair: where Enter puts the next shape or frame once the arrows have
|
||||
/// shown it (in the middle of the view) and moved it (`tool.crosshair`), in page coordinates.
|
||||
/// Placing, Escape and a tool change take it away.
|
||||
/// shown it (in the middle of the view) and moved it (`tool.crosshair`), in page coordinates
|
||||
/// of the document and page in `place_on` (elsewhere it isn't shown: `Session::crosshair`).
|
||||
/// Placing, Escape and a change of tool take it away.
|
||||
pub place_at: Option<Point>,
|
||||
/// The document (uid) and page the crosshair was shown on.
|
||||
pub(crate) place_on: Option<(u64, NodeId)>,
|
||||
}
|
||||
|
||||
impl ToolState {
|
||||
|
||||
@@ -209,6 +209,8 @@ impl Session {
|
||||
crate::normalize(ds.doc_mut(), &before);
|
||||
}
|
||||
self.commit_interaction()?;
|
||||
// A placement by the pointer takes the placement crosshair with it, as Enter does.
|
||||
self.tool.place_at = None;
|
||||
if !self.prefs.keep_tool {
|
||||
self.set_tool(ToolId::Move);
|
||||
}
|
||||
|
||||
@@ -86,7 +86,8 @@ impl Session {
|
||||
}
|
||||
// The crosshair the arrows moved goes with the placement, which lands on it when no point
|
||||
// is given.
|
||||
let crosshair = self.tool.place_at.take();
|
||||
let crosshair = self.crosshair();
|
||||
self.tool.place_at = None;
|
||||
let v = &self.ds()?.view;
|
||||
let (c, zoom) = (at.or(crosshair).unwrap_or(Point::new(v.cx, v.cy)), v.zoom);
|
||||
let (w, h) = tool.click_size();
|
||||
@@ -114,8 +115,11 @@ impl Session {
|
||||
self.tool.overlay = Overlay::default();
|
||||
return self.cancel_interaction();
|
||||
}
|
||||
// The placement crosshair goes first; the tool stays.
|
||||
if self.tool.place_at.take().is_some() {
|
||||
// The placement crosshair goes first; the tool stays. One left on another page or
|
||||
// document goes quietly.
|
||||
let shown = self.crosshair().is_some();
|
||||
self.tool.place_at = None;
|
||||
if shown {
|
||||
if let Ok(ds) = self.ds_mut() {
|
||||
ds.revision += 1;
|
||||
}
|
||||
@@ -184,7 +188,14 @@ impl Session {
|
||||
/// Snapshot of tool state for the UI / inspect.
|
||||
pub fn tool_info(&self) -> serde_json::Value {
|
||||
let shapes: Vec<serde_json::Value> = self.shape_handles().into_iter().map(|(h, at)| json!({"handle": h, "at": at})).collect();
|
||||
let crosshair = self.tool.place_at.map(|p| [p.x, p.y]);
|
||||
let crosshair = self.crosshair().map(|p| [p.x, p.y]);
|
||||
json!({"tool": self.tool.tool.id(), "busy": self.tool.busy(), "overlay": self.tool.overlay, "arcHandles": self.arc_handles(), "shapeHandles": shapes, "crosshair": crosshair})
|
||||
}
|
||||
|
||||
/// The placement crosshair, while the document and page it was shown on are the ones in
|
||||
/// front (`ToolState::place_at`).
|
||||
pub fn crosshair(&self) -> Option<Point> {
|
||||
let here = self.ds().ok().map(|d| (d.uid, d.page));
|
||||
self.tool.place_at.filter(|_| here.is_some() && here == self.tool.place_on)
|
||||
}
|
||||
}
|
||||
@@ -76,10 +76,13 @@ impl Session {
|
||||
if !matches!(t, ToolId::Move | ToolId::Scale | ToolId::Hand) && self.ds().is_ok_and(|d| d.vector_edit.is_some()) {
|
||||
let _ = crate::vectoredit::end(self);
|
||||
}
|
||||
// The placement crosshair goes with a change of tool, not with the same tool chosen again.
|
||||
if self.tool.tool != t {
|
||||
self.tool.place_at = None;
|
||||
}
|
||||
self.tool.tool = t;
|
||||
self.tool.gesture = Gesture::None;
|
||||
self.tool.pick_only = false;
|
||||
self.tool.place_at = None;
|
||||
// The tool's cursor shows before the pointer moves.
|
||||
self.tool.overlay =
|
||||
Overlay { builder_remove: self.tool.builder_remove && t == ToolId::ShapeBuilder, cursor: t.cursor(), ..Overlay::default() };
|
||||
|
||||
@@ -534,13 +534,18 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
let step = if boolean(p, "big").unwrap_or(false) { s.prefs.nudge_big } else { s.prefs.nudge_small };
|
||||
let dir = |k| num(p, k).filter(|v| v.is_finite()).unwrap_or(0.0).clamp(-100.0, 100.0) * step;
|
||||
let (dx, dy) = (dir("dx"), dir("dy"));
|
||||
let v = &s.ds()?.view;
|
||||
// "It will appear in the middle of the viewport": the first arrow shows it there.
|
||||
let at = match s.tool.place_at {
|
||||
let (uid, page, centre) = {
|
||||
let ds = s.ds()?;
|
||||
(ds.uid, ds.page, figcraft_geom::Point::new(ds.view.cx, ds.view.cy))
|
||||
};
|
||||
// "It will appear in the middle of the viewport": the first arrow shows it there (a
|
||||
// crosshair left on another page or document starts over).
|
||||
let at = match s.crosshair() {
|
||||
Some(c) => figcraft_geom::Point::new(c.x + dx, c.y + dy),
|
||||
None => figcraft_geom::Point::new(v.cx, v.cy),
|
||||
None => centre,
|
||||
};
|
||||
s.tool.place_at = Some(at);
|
||||
s.tool.place_on = Some((uid, page));
|
||||
if let Ok(ds) = s.ds_mut() {
|
||||
ds.revision += 1;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
use crate::tools::{PointerEvent, PointerKind};
|
||||
|
||||
#[test]
|
||||
fn the_arrows_show_and_move_the_crosshair_and_enter_places_there() {
|
||||
@@ -43,3 +44,39 @@ fn the_arrows_show_and_move_the_crosshair_and_enter_places_there() {
|
||||
s.execute("tool.ellipse", &json!({})).unwrap();
|
||||
assert_eq!(s.tool.place_at, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_crosshair_keeps_its_page_and_goes_with_a_pointer_placement_but_not_the_same_tool_again() {
|
||||
let mut s = Session::new();
|
||||
s.execute("view.set", &json!({"zoom": 1.0, "cx": 300, "cy": 200})).unwrap();
|
||||
s.execute("tool.rectangle", &json!({})).unwrap();
|
||||
s.execute("tool.crosshair", &json!({"dx": 1})).unwrap();
|
||||
s.execute("tool.crosshair", &json!({"dx": 1})).unwrap();
|
||||
// The tool already chosen named again keeps the crosshair: `tool.place {tool}` lands on it.
|
||||
let placed = s.execute("tool.place", &json!({"tool": "rectangle"})).unwrap();
|
||||
let id = crate::params::id_of(&placed["ids"][0]).unwrap();
|
||||
assert_eq!(s.doc().unwrap().world_bounds(id).unwrap().center().x, 301.0);
|
||||
// A click places too (the tool kept by the preference) and takes the crosshair with it.
|
||||
s.prefs.keep_tool = true;
|
||||
s.execute("select.set", &json!({"ids": []})).unwrap();
|
||||
s.execute("tool.rectangle", &json!({})).unwrap();
|
||||
s.execute("tool.crosshair", &json!({})).unwrap();
|
||||
s.pointer(&PointerEvent::at(PointerKind::Down, 600.0, 600.0, 1.0)).unwrap();
|
||||
s.pointer(&PointerEvent::at(PointerKind::Up, 600.0, 600.0, 1.0)).unwrap();
|
||||
assert_eq!((s.tool.tool, s.tool.place_at), (crate::ToolId::Rectangle, None), "placed by the pointer: the crosshair goes, the tool stays");
|
||||
// Another page in front: the crosshair shown on the first isn't there, Escape has nothing to
|
||||
// take, and the next arrow starts over in the middle of this page's view.
|
||||
s.execute("select.set", &json!({"ids": []})).unwrap();
|
||||
s.execute("tool.crosshair", &json!({"dx": 5})).unwrap();
|
||||
s.execute("page.add", &json!({})).unwrap();
|
||||
s.execute("page.select", &json!({"index": 1})).unwrap();
|
||||
s.execute("tool.rectangle", &json!({})).unwrap();
|
||||
assert_eq!(s.crosshair(), None);
|
||||
assert!(s.tool_info()["crosshair"].is_null());
|
||||
s.escape().unwrap();
|
||||
assert_eq!(s.tool.tool, crate::ToolId::Move, "Escape went on to the tool");
|
||||
s.execute("tool.rectangle", &json!({})).unwrap();
|
||||
let r = s.execute("tool.crosshair", &json!({"dx": 1})).unwrap();
|
||||
let v = &s.ds().unwrap().view;
|
||||
assert_eq!((r["x"].as_f64(), r["y"].as_f64()), (Some(v.cx), Some(v.cy)), "shown afresh in the middle of this page's view");
|
||||
}
|
||||
@@ -419,7 +419,7 @@ fn attach_points(app: &FigApp, painter: &egui::Painter, points: &[([f64; 2], boo
|
||||
|
||||
/// The placement crosshair the arrows showed and moved: where Enter puts the next shape or frame.
|
||||
fn crosshair_overlay(app: &FigApp, painter: &egui::Painter) {
|
||||
let Some(p) = app.session.tool.place_at.filter(|_| app.session.tool.tool.draws_shape()) else { return };
|
||||
let Some(p) = app.session.crosshair().filter(|_| app.session.tool.tool.draws_shape()) else { return };
|
||||
let c = app.to_screen(p.x, p.y);
|
||||
let col = accent(painter);
|
||||
for d in [vec2(1.0, 0.0), vec2(0.0, 1.0)] {
|
||||
|
||||
Reference in new issue
Block a user