mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 14:33:34 +00:00
Path type brackets: 8 px reach, hover highlight, and the nearest of a bracket and a port takes the press
Squashed from #5332 onto main.
This commit is contained in:
@@ -57,7 +57,7 @@ use crate::bbox::move_delta;
|
||||
use crate::corners::{self, CornerDrag, over_widget};
|
||||
use crate::guides::{HandleSnap, Leave, PointSnap, Targets};
|
||||
use crate::meshedit::{MeshEdit, grid_of};
|
||||
use crate::pathtype::{self, BracketDrag, over_bracket};
|
||||
use crate::pathtype::{self, BracketDrag, near_bracket};
|
||||
use crate::rulerguide::GuideEdit;
|
||||
use crate::select::{MoveSnap, hold_point, is_area_type, is_path_type, matrix_json, release_guide_at};
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
|
||||
@@ -227,6 +227,8 @@ pub struct DirectSelectionTool {
|
||||
/// The already selected frame anchor the press landed on without Shift: a click (no drag)
|
||||
/// selects it alone, as a drag moves every selected frame anchor with it.
|
||||
frame_click: Option<(NodeId, AnchorRef)>,
|
||||
/// Where the pointer last hovered: the type on a path bracket under it draws thicker.
|
||||
bracket_hover: Option<Point>,
|
||||
}
|
||||
|
||||
impl DirectSelectionTool {
|
||||
@@ -253,6 +255,7 @@ impl DirectSelectionTool {
|
||||
frame: BTreeMap::new(),
|
||||
frame_revision: (0, 0),
|
||||
frame_click: None,
|
||||
bracket_hover: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,6 +646,11 @@ fn segment_at(cx: &ToolContext, id: NodeId, p: Point) -> Option<SegmentHit> {
|
||||
}
|
||||
|
||||
impl Tool for DirectSelectionTool {
|
||||
/// The pointer hovers nothing for the tool: no bracket is highlighted.
|
||||
fn hover_ended(&mut self, _cx: &ToolContext) {
|
||||
self.bracket_hover = None;
|
||||
}
|
||||
|
||||
fn id(&self) -> &'static str {
|
||||
if self.group { "groupSelection" } else { "directSelection" }
|
||||
}
|
||||
@@ -703,6 +711,7 @@ impl Tool for DirectSelectionTool {
|
||||
}
|
||||
}
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
self.bracket_hover = (ev.kind == PointerKind::Move).then_some(ev.pos);
|
||||
if let Some(out) = self.guide.pointer(cx, ev) {
|
||||
return out;
|
||||
}
|
||||
@@ -727,7 +736,7 @@ impl Tool for DirectSelectionTool {
|
||||
return vec![];
|
||||
}
|
||||
if !over_path_type_anchor(cx, p)
|
||||
&& let Some(b) = BracketDrag::hit(cx, ev)
|
||||
&& let Some(b) = BracketDrag::hit_any(cx, ev)
|
||||
{
|
||||
self.state = State::Bracket(b);
|
||||
return vec![];
|
||||
@@ -915,7 +924,8 @@ impl Tool for DirectSelectionTool {
|
||||
out.extend(self.mesh.overlays(cx, &|id| cx.selection.contains(id)));
|
||||
if !self.group {
|
||||
out.extend(frame_overlays(cx, &self.frame_anchors(cx)));
|
||||
out.extend(pathtype::overlays(cx));
|
||||
let drag = if let State::Bracket(b) = &self.state { Some(b) } else { None };
|
||||
out.extend(pathtype::overlays(cx, self.bracket_hover, drag));
|
||||
}
|
||||
if matches!(self.state, State::Handle { .. } | State::MoveAnchors { .. } | State::MoveObject { .. }) {
|
||||
out.extend(self.guides.iter().cloned());
|
||||
@@ -932,7 +942,7 @@ impl Tool for DirectSelectionTool {
|
||||
if !self.group && (matches!(self.state, State::Corner(_)) || over_widget(cx, p, true)) {
|
||||
return Cursor::CornerRadius;
|
||||
}
|
||||
if !self.group && (matches!(self.state, State::Bracket(_)) || over_bracket(cx, p) && !over_path_type_anchor(cx, p)) {
|
||||
if !self.group && (matches!(self.state, State::Bracket(_)) || near_bracket(cx, p) && !over_path_type_anchor(cx, p)) {
|
||||
return Cursor::PathBracket;
|
||||
}
|
||||
let rest = if self.group { Cursor::GroupSelection } else { Cursor::ArrowHollow };
|
||||
|
||||
+160
-25
@@ -3,8 +3,12 @@
|
||||
//! type begins or ends along its path; dragging the centre bracket slides the type along the path
|
||||
//! (its brackets with it) and, dragged across the path, flips the type to the other side
|
||||
//! (Cmd/Ctrl held: it only slides). Each drag is one undo step of `type.pathOptions`. The tools
|
||||
//! draw the brackets from the same geometry they hit-test. With more than 10,000 type on a path
|
||||
//! objects selected, they neither draw nor hit-test any bracket.
|
||||
//! draw the brackets from the same geometry they hit-test. A press within [`HIT_PX`] of a bracket
|
||||
//! takes the nearest one, and the bracket a press would take draws thicker under the pointer. In
|
||||
//! the Selection tool, which loads text from ports, the nearer of a bracket and a text port takes a
|
||||
//! press (and the highlight); Direct Selection has no ports, so a bracket within reach always takes
|
||||
//! it there. With more than 10,000 type on a path objects selected, they neither draw nor hit-test
|
||||
//! any bracket.
|
||||
|
||||
use serde_json::{Map, Value, json};
|
||||
use vectorcraft_doc::{NodeId, NodeKind, TextKind};
|
||||
@@ -20,6 +24,10 @@ const DOWN_PX: f64 = 4.0;
|
||||
const FOOT_PX: f64 = 4.0;
|
||||
/// How far past the path (screen px) the centre bracket goes before the type flips.
|
||||
const FLIP_PX: f64 = 6.0;
|
||||
/// How near (screen px) a press must be to a bracket to take it.
|
||||
const HIT_PX: f64 = 8.0;
|
||||
/// The stroke width (screen px) of the bracket under the pointer; the others are hairlines.
|
||||
const HOT_WIDTH: f32 = 2.5;
|
||||
/// The most points the tools draw for the brackets of a selection, the same number as the canvas's
|
||||
/// limit on the anchor points it outlines (`OVERLAY_MAX_ANCHORS` in `crates/ui-egui/src/canvas.rs`).
|
||||
/// Past it the tools neither draw nor hit-test any bracket.
|
||||
@@ -107,9 +115,14 @@ impl Brackets {
|
||||
}
|
||||
}
|
||||
|
||||
/// The bracket within `tol` (document units) of `p`.
|
||||
fn hit(&self, p: Point, zoom: f64, tol: f64) -> Option<Bracket> {
|
||||
Bracket::ALL.into_iter().find(|b| self.lines(*b, zoom).iter().any(|(a, c)| Line::new(*a, *c).nearest(p, 1e-9).distance_sq <= tol * tol))
|
||||
/// How far (document units) bracket `b` is from `p`.
|
||||
fn distance(&self, b: Bracket, p: Point, zoom: f64) -> f64 {
|
||||
self.lines(b, zoom).into_iter().map(|(a, c)| Line::new(a, c).nearest(p, 1e-9).distance_sq.sqrt()).fold(f64::INFINITY, f64::min)
|
||||
}
|
||||
|
||||
/// The bracket nearest `p` within `tol` (document units), and how far it is.
|
||||
fn hit(&self, p: Point, zoom: f64, tol: f64) -> Option<(Bracket, f64)> {
|
||||
Bracket::ALL.into_iter().map(|b| (b, self.distance(b, p, zoom))).filter(|(_, d)| *d <= tol).min_by(|a, b| a.1.total_cmp(&b.1))
|
||||
}
|
||||
|
||||
/// The fraction of the path's length nearest document point `p`.
|
||||
@@ -125,29 +138,64 @@ fn too_many(cx: &ToolContext) -> bool {
|
||||
cx.selection.objects.iter().filter(on_path).nth(MAX_POINTS / POINTS_EACH).is_some()
|
||||
}
|
||||
|
||||
/// The selected type on a path's bracket under `p`, if any.
|
||||
fn bracket_at(cx: &ToolContext, p: Point) -> Option<(Brackets, Bracket)> {
|
||||
Brackets::of(cx).into_iter().find_map(|b| {
|
||||
let which = b.hit(p, cx.zoom, cx.tol(5.0))?;
|
||||
Some((b, which))
|
||||
})
|
||||
/// The selected type on a path's bracket nearest `p` within [`HIT_PX`], and how far it is.
|
||||
fn bracket_at(cx: &ToolContext, p: Point) -> Option<(Brackets, Bracket, f64)> {
|
||||
Brackets::of(cx)
|
||||
.into_iter()
|
||||
.filter_map(|b| {
|
||||
let (which, d) = b.hit(p, cx.zoom, cx.tol(HIT_PX))?;
|
||||
Some((b, which, d))
|
||||
})
|
||||
.min_by(|a, b| a.2.total_cmp(&b.2))
|
||||
}
|
||||
|
||||
/// Is `p` over a bracket of the selected type on a path?
|
||||
/// How far (document units) the bracket of the selected type on a path under `p` is from it; None
|
||||
/// when no bracket is within reach.
|
||||
pub fn bracket_distance(cx: &ToolContext, p: Point) -> Option<f64> {
|
||||
if too_many(cx) {
|
||||
return None;
|
||||
}
|
||||
bracket_at(cx, p).map(|(_, _, d)| d)
|
||||
}
|
||||
|
||||
/// Is `p` within reach of a bracket of the selected type on a path, ports or not (Direct
|
||||
/// Selection, which takes no clicks on ports)?
|
||||
pub fn near_bracket(cx: &ToolContext, p: Point) -> bool {
|
||||
bracket_distance(cx, p).is_some()
|
||||
}
|
||||
|
||||
/// Is `p` over a bracket of the selected type on a path, nearer than any text port (a start
|
||||
/// bracket and an in port sit side by side: the nearer takes the press)? For the Selection tool,
|
||||
/// which loads text from ports.
|
||||
pub fn over_bracket(cx: &ToolContext, p: Point) -> bool {
|
||||
!too_many(cx) && bracket_at(cx, p).is_some()
|
||||
bracket_distance(cx, p).is_some_and(|d| crate::text::port_near(cx, p).is_none_or(|(_, _, port)| d <= port))
|
||||
}
|
||||
|
||||
/// The brackets of the selected type on a path, in their layer's colour.
|
||||
pub fn overlays(cx: &ToolContext) -> Vec<Overlay> {
|
||||
/// The brackets of the selected type on a path, in their layer's colour: the one under `hover`
|
||||
/// thicker (the caller passes only a hover that would take the bracket), or the one `drag` moves.
|
||||
pub fn overlays(cx: &ToolContext, hover: Option<Point>, drag: Option<&BracketDrag>) -> Vec<Overlay> {
|
||||
if too_many(cx) {
|
||||
return vec![];
|
||||
}
|
||||
let hot = match drag {
|
||||
Some(d) => Some((d.brackets.id, d.which)),
|
||||
None => hover.and_then(|p| bracket_at(cx, p)).map(|(b, which, _)| (b.id, which)),
|
||||
};
|
||||
let mut out = vec![];
|
||||
for b in Brackets::of(cx) {
|
||||
let color = cx.doc.layer_color(b.id);
|
||||
for which in Bracket::ALL {
|
||||
out.extend(b.lines(which, cx.zoom).into_iter().map(|(a, b)| Overlay::Line { a, b, color, dashed: false }));
|
||||
let lines = b.lines(which, cx.zoom);
|
||||
if hot == Some((b.id, which)) {
|
||||
let mut path = vectorcraft_geom::kurbo::BezPath::new();
|
||||
for (a, c) in lines {
|
||||
path.move_to(a);
|
||||
path.line_to(c);
|
||||
}
|
||||
out.push(Overlay::Path { path, color, width: HOT_WIDTH, dashed: false });
|
||||
} else {
|
||||
out.extend(lines.into_iter().map(|(a, b)| Overlay::Line { a, b, color, dashed: false }));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
@@ -165,12 +213,22 @@ pub struct BracketDrag {
|
||||
}
|
||||
|
||||
impl BracketDrag {
|
||||
/// Start a drag when the press `ev` is on a bracket of the selected type on a path.
|
||||
/// Start a drag when the press `ev` is on a bracket of the selected type on a path, nearer than
|
||||
/// any text port (the Selection tool, [`over_bracket`]).
|
||||
pub fn hit(cx: &ToolContext, ev: &PointerEvent) -> Option<Self> {
|
||||
if !over_bracket(cx, ev.pos) {
|
||||
return None;
|
||||
}
|
||||
Self::hit_any(cx, ev)
|
||||
}
|
||||
|
||||
/// Start a drag when the press `ev` is within reach of a bracket, ports or not (Direct
|
||||
/// Selection, [`near_bracket`]).
|
||||
pub fn hit_any(cx: &ToolContext, ev: &PointerEvent) -> Option<Self> {
|
||||
if too_many(cx) {
|
||||
return None;
|
||||
}
|
||||
let (brackets, which) = bracket_at(cx, ev.pos)?;
|
||||
let (brackets, which, _) = bracket_at(cx, ev.pos)?;
|
||||
let grab = brackets.fraction_at(ev.pos)?;
|
||||
Some(Self { brackets, which, start: ev.pos, grab, began: false })
|
||||
}
|
||||
@@ -271,12 +329,13 @@ mod tests {
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let b = Brackets::of(&cx).pop().unwrap();
|
||||
assert_eq!(b.hit(Point::new(160.0, 290.0), 1.0, 2.0), Some(Bracket::Start));
|
||||
assert_eq!(b.hit(Point::new(280.0, 290.0), 1.0, 2.0), Some(Bracket::Centre));
|
||||
assert_eq!(b.hit(Point::new(400.0, 290.0), 1.0, 2.0), Some(Bracket::End));
|
||||
assert_eq!(b.hit(Point::new(220.0, 290.0), 1.0, 2.0), None);
|
||||
let which = |p: Point| b.hit(p, 1.0, 2.0).map(|(w, _)| w);
|
||||
assert_eq!(which(Point::new(160.0, 290.0)), Some(Bracket::Start));
|
||||
assert_eq!(which(Point::new(280.0, 290.0)), Some(Bracket::Centre));
|
||||
assert_eq!(which(Point::new(400.0, 290.0)), Some(Bracket::End));
|
||||
assert_eq!(which(Point::new(220.0, 290.0)), None);
|
||||
// Drawn over the type's side of the path, with feet pointing into the span.
|
||||
let o = overlays(&cx);
|
||||
let o = overlays(&cx, None, None);
|
||||
assert_eq!(o.len(), 5);
|
||||
assert!(o.contains(&Overlay::Line { a: Point::new(160.0, 286.0), b: Point::new(164.0, 286.0), color: d.layer_color(id), dashed: false }));
|
||||
// Nothing for unselected or other type.
|
||||
@@ -316,13 +375,13 @@ mod tests {
|
||||
};
|
||||
{
|
||||
let cx = cx(&d, &s, &p);
|
||||
assert_eq!(overlays(&cx).len(), 50_000);
|
||||
assert_eq!(overlays(&cx, None, None).len(), 50_000);
|
||||
assert!(over_bracket(&cx, at) && BracketDrag::hit(&cx, &press).is_some());
|
||||
assert_eq!(tools(&cx), vec![(50_000, Cursor::PathBracket); 2]);
|
||||
}
|
||||
add(&mut d, &mut s, 1);
|
||||
let cx = cx(&d, &s, &p);
|
||||
assert!(overlays(&cx).is_empty());
|
||||
assert!(overlays(&cx, None, None).is_empty());
|
||||
assert!(!over_bracket(&cx, at) && BracketDrag::hit(&cx, &press).is_none());
|
||||
for (n, cursor) in tools(&cx) {
|
||||
assert_eq!(n, 0);
|
||||
@@ -364,4 +423,80 @@ mod tests {
|
||||
let v = preview(&drag(&cx, Point::new(400.0, 295.0), Point::new(110.0, 300.0), Mods::default())).clone();
|
||||
assert!((v["end"].as_f64().unwrap() - 0.2).abs() < 1e-6, "{v}");
|
||||
}
|
||||
|
||||
/// A press takes a bracket within 8 screen px at any zoom, and not further.
|
||||
#[test]
|
||||
fn brackets_are_hit_within_eight_screen_pixels_at_any_zoom() {
|
||||
let (d, id) = doc_with_path_type();
|
||||
let s = selected(id);
|
||||
let p = paint();
|
||||
for zoom in [0.25, 1.0, 8.0] {
|
||||
let mut cx = cx(&d, &s, &p);
|
||||
cx.zoom = zoom;
|
||||
let px = 1.0 / zoom;
|
||||
// The start bracket's stem stands at x 160, from 4 px below the path to 14 px above it.
|
||||
let near = |dx: f64| near_bracket(&cx, Point::new(160.0 - dx * px, 300.0 - 7.0 * px));
|
||||
assert!(near(7.5), "7.5 px beside the stem at zoom {zoom}");
|
||||
assert!(!near(9.0), "9 px beside the stem at zoom {zoom}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The bracket a press would take draws thicker under the pointer; elsewhere they are all
|
||||
/// hairlines.
|
||||
#[test]
|
||||
fn the_hovered_bracket_draws_thicker() {
|
||||
let (d, id) = doc_with_path_type();
|
||||
let s = selected(id);
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let hot = |o: &[Overlay]| o.iter().filter(|o| matches!(o, Overlay::Path { width, .. } if *width == HOT_WIDTH)).count();
|
||||
assert_eq!(hot(&overlays(&cx, Some(Point::new(400.0, 293.0)), None)), 1);
|
||||
assert_eq!(hot(&overlays(&cx, Some(Point::new(220.0, 250.0)), None)), 0);
|
||||
let mut t = crate::select::SelectionTool::default();
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Move, 400.0, 293.0));
|
||||
assert_eq!(hot(&t.overlays(&cx)), 1);
|
||||
t.hover_ended(&cx);
|
||||
assert_eq!(hot(&t.overlays(&cx)), 0, "the highlight goes with the hover");
|
||||
}
|
||||
|
||||
/// A press on a bracket takes it rather than moving the type; where the in port sits beside the
|
||||
/// start bracket, the Selection tool gives a press nearer the port to the port (no bracket drag,
|
||||
/// cursor or highlight), while Direct Selection, which has no ports, takes the bracket.
|
||||
#[test]
|
||||
fn the_nearer_of_a_bracket_and_a_port_takes_a_selection_press() {
|
||||
let (d, id) = doc_with_path_type();
|
||||
let s = selected(id);
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let mut t = crate::select::SelectionTool::default();
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 280.0, 296.0));
|
||||
let out = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 310.0, 296.0));
|
||||
assert!(out.iter().any(|a| matches!(a, Action::Preview(c, _) if c == "type.pathOptions")), "{out:?}");
|
||||
// A point nearer the in port with the start bracket in reach.
|
||||
let mut between = None;
|
||||
for i in 0..400 {
|
||||
for j in 0..120 {
|
||||
let q = Point::new(130.0 + i as f64 * 0.1, 282.0 + j as f64 * 0.2);
|
||||
let Some((_, crate::text::Port::In, port)) = crate::text::port_near(&cx, q) else { continue };
|
||||
if bracket_distance(&cx, q).is_some_and(|b| port < b) {
|
||||
between = Some(q);
|
||||
}
|
||||
}
|
||||
}
|
||||
let q = between.expect("a point nearer the in port with the start bracket in reach");
|
||||
let down = PointerEvent::new(PointerKind::Down, q.x, q.y);
|
||||
let hot = |o: &[Overlay]| o.iter().any(|o| matches!(o, Overlay::Path { width, .. } if *width == HOT_WIDTH));
|
||||
assert!(!over_bracket(&cx, q) && BracketDrag::hit(&cx, &down).is_none());
|
||||
let mut select = crate::select::SelectionTool::default();
|
||||
select.pointer(&cx, &PointerEvent::new(PointerKind::Move, q.x, q.y));
|
||||
assert!(!hot(&select.overlays(&cx)));
|
||||
assert_ne!(select.cursor(&cx, q, Mods::default()), Cursor::PathBracket);
|
||||
let mut direct = crate::direct::DirectSelectionTool::new(false);
|
||||
assert_eq!(direct.cursor(&cx, q, Mods::default()), Cursor::PathBracket);
|
||||
direct.pointer(&cx, &PointerEvent::new(PointerKind::Move, q.x, q.y));
|
||||
assert!(hot(&direct.overlays(&cx)));
|
||||
direct.pointer(&cx, &down);
|
||||
let out = direct.pointer(&cx, &PointerEvent::new(PointerKind::Drag, q.x + 30.0, q.y));
|
||||
assert!(out.iter().any(|a| matches!(a, Action::Preview(c, _) if c == "type.pathOptions")), "{out:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +153,8 @@ pub struct SelectionTool {
|
||||
/// The text the pointer was loaded with when the button went down: a dropped press loads it
|
||||
/// again.
|
||||
loaded_at_press: Option<(NodeId, bool)>,
|
||||
/// Where the pointer last hovered: the type on a path bracket under it draws thicker.
|
||||
bracket_hover: Option<Point>,
|
||||
}
|
||||
|
||||
/// What Distance Guides measure the selection to.
|
||||
@@ -713,6 +715,7 @@ impl Tool for SelectionTool {
|
||||
self.hover = None;
|
||||
self.distance = None;
|
||||
self.alt_hover = false;
|
||||
self.bracket_hover = None;
|
||||
}
|
||||
|
||||
fn wants_rehover(&self) -> bool {
|
||||
@@ -735,6 +738,7 @@ impl Tool for SelectionTool {
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
self.note_loaded_at_press(ev.kind);
|
||||
let hovering = ev.kind == PointerKind::Move && matches!(self.state, State::Idle);
|
||||
self.bracket_hover = hovering.then_some(ev.pos);
|
||||
self.alt_hover = hovering && ev.mods.alt;
|
||||
self.distance = hovering.then(|| distance_target(cx, ev.pos, ev.mods)).flatten();
|
||||
if let State::Loaded { from, before, press } = self.state
|
||||
@@ -930,7 +934,9 @@ impl Tool for SelectionTool {
|
||||
}
|
||||
|
||||
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
|
||||
let mut o = pathtype::overlays(cx);
|
||||
// Highlighted only where a press would take the bracket, not a nearer port.
|
||||
let drag = if let State::Bracket(b) = &self.state { Some(b) } else { None };
|
||||
let mut o = pathtype::overlays(cx, self.bracket_hover.filter(|p| over_bracket(cx, *p)), drag);
|
||||
if let Some(source) = cx.isolation.and_then(|id| cx.doc.node(id)).filter(|n| n.name.as_deref() == Some(vectorcraft_doc::shaper::SOURCES)) {
|
||||
for n in source.children().into_iter().flatten() {
|
||||
if let Some(path) = n.path_data() {
|
||||
@@ -1345,7 +1351,7 @@ impl SelectionTool {
|
||||
fn press_widgets(&mut self, cx: &ToolContext, ev: &PointerEvent, p: Point) -> Option<Vec<Action>> {
|
||||
// 0. A port of selected area type or type on a path: the pointer takes its text,
|
||||
// to thread on from the out port or back from the in port.
|
||||
if let Some((from, port)) = crate::text::port_at(cx, p) {
|
||||
if let Some((from, port)) = crate::text::port_at(cx, p).filter(|_| !over_bracket(cx, p)) {
|
||||
self.state = State::Loaded { from, before: port == crate::text::Port::In, press: None };
|
||||
return Some(vec![]);
|
||||
}
|
||||
|
||||
@@ -51,8 +51,8 @@ mod tool;
|
||||
use plain::char_range_to_bytes;
|
||||
use plain::common_affixes;
|
||||
pub use plain::step_key;
|
||||
pub(crate) use ports::port_at;
|
||||
pub use ports::{PORT_RISE_PX, PORTS_MAX, Port, PortMarks, PortState, port_centre, port_states, ported, ports_shown, push_port, thread_ports};
|
||||
pub(crate) use ports::{port_at, port_near};
|
||||
|
||||
/// Which of the Type tools this is.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
|
||||
@@ -145,15 +145,25 @@ pub fn ports_shown(d: &vectorcraft_doc::Document, selected: &[NodeId]) -> bool {
|
||||
/// on the in port to thread into one before it. Every such frame has both ports, threaded,
|
||||
/// overflowing or not, while their ports show ([`PORTS_MAX`]).
|
||||
pub(crate) fn port_at(cx: &ToolContext, p: Point) -> Option<(NodeId, Port)> {
|
||||
port_near(cx, p).map(|(id, port, _)| (id, port))
|
||||
}
|
||||
|
||||
/// [`port_at`], with how far (document units) the port's centre is from `p`: the nearest port
|
||||
/// within reach, of every selected frame (ports of overlapping frames can both be in reach).
|
||||
pub(crate) fn port_near(cx: &ToolContext, p: Point) -> Option<(NodeId, Port, f64)> {
|
||||
if !ports_shown(cx.doc, &cx.selection.objects) {
|
||||
return None;
|
||||
}
|
||||
let h = cx.tol(PORT_HALF_PX + 1.0);
|
||||
let near = |c: Point| (c.x - p.x).abs() <= h && (c.y - p.y).abs() <= h;
|
||||
cx.selection.objects.iter().find_map(|id| {
|
||||
let t = ported(cx.doc, *id)?;
|
||||
[Port::Out, Port::In].into_iter().find(|port| port_centre(t, *port, cx.tol(1.0)).is_some_and(near)).map(|port| (*id, port))
|
||||
})
|
||||
cx.selection
|
||||
.objects
|
||||
.iter()
|
||||
.filter_map(|id| ported(cx.doc, *id).map(|t| (*id, t)))
|
||||
.flat_map(|(id, t)| [Port::Out, Port::In].into_iter().filter_map(move |port| port_centre(t, port, cx.tol(1.0)).map(|c| (id, port, c))))
|
||||
.filter(|(_, _, c)| near(*c))
|
||||
.map(|(id, port, c)| (id, port, c.distance(p)))
|
||||
.min_by(|a, b| a.2.total_cmp(&b.2))
|
||||
}
|
||||
|
||||
/// Which selected text overflows, for [`thread_ports`]: each text object's layout verdict kept
|
||||
|
||||
Reference in New Issue
Block a user