Files
deckcraft/crates/engine/src/connect.rs
T

138 lines
5.2 KiB
Rust

//! Connectors glued to shapes: connection sites, snapping line ends to them, and rerouting attached
//! connectors whenever the shapes they connect move, resize or rotate.
use deckcraft_geom::{Point, Xfrm};
use deckcraft_model::{Presentation, Shape, ShapeId, ShapeKind};
use crate::Selection;
use crate::cmd::xfrm_of;
/// Connection sites of `s` in slide coordinates (index = site index stored on connectors).
pub fn sites(doc: &Presentation, sel: &Selection, s: &Shape) -> Vec<Point> {
if s.is_line() || matches!(s.kind, ShapeKind::Group { .. } | ShapeKind::Ink { .. }) {
return vec![];
}
let x = xfrm_of(doc, sel, s);
let mut local = deckcraft_render::shape_geometry(s, x.w, x.h).sites;
if local.is_empty() {
local = vec![Point::new(x.w / 2.0, 0.0), Point::new(0.0, x.h / 2.0), Point::new(x.w / 2.0, x.h), Point::new(x.w, x.h / 2.0)];
}
let a = x.affine();
local.into_iter().map(|p| a * p).collect()
}
/// The nearest connection site within `radius` of `p` on any visible shape except `exclude`.
pub fn nearest_site(
doc: &Presentation,
sel: &Selection,
shapes: &[Shape],
p: Point,
radius: f64,
exclude: Option<ShapeId>,
) -> Option<(ShapeId, u32, Point)> {
let mut best: Option<(f64, ShapeId, u32, Point)> = None;
for s in shapes.iter().filter(|s| Some(s.id) != exclude && !s.hidden) {
for (i, q) in sites(doc, sel, s).into_iter().enumerate() {
let d = (q - p).hypot();
if d <= radius && best.is_none_or(|b| d < b.0) {
best = Some((d, s.id, i as u32, q));
}
}
}
best.map(|b| (b.1, b.2, b.3))
}
/// The shape whose sites should show while drawing or dragging a connector end near `p`.
pub fn site_shape_at(doc: &Presentation, sel: &Selection, shapes: &[Shape], p: Point, margin: f64, exclude: Option<ShapeId>) -> Option<ShapeId> {
shapes
.iter()
.rev()
.filter(|s| Some(s.id) != exclude && !s.hidden && !s.is_line() && !matches!(s.kind, ShapeKind::Ink { .. }))
.find(|s| xfrm_of(doc, sel, s).bounds().inflate(margin, margin).contains(p))
.map(|s| s.id)
}
/// Line endpoints (start, end) of a line-like shape's box.
pub fn endpoints(x: &Xfrm) -> (Point, Point) {
let a = x.affine();
(a * Point::new(0.0, 0.0), a * Point::new(x.w, x.h))
}
/// Move the ends of connectors glued to shapes on the edited slide to the current site positions.
/// Connections to shapes that no longer exist are dropped.
pub fn reroute(doc: &mut Presentation, sel: &Selection) {
let Some(list) = crate::shapes_mut(doc, sel) else { return };
if !list.iter().any(|s| matches!(s.kind, ShapeKind::Connector { start: Some(_), .. } | ShapeKind::Connector { end: Some(_), .. })) {
return;
}
let snapshot: Vec<Shape> = list.clone();
let view = doc.clone();
let site = |id: ShapeId, idx: u32| -> Option<Point> {
let s = snapshot.iter().find(|s| s.id == id)?;
let v = sites(&view, sel, s);
v.get(idx as usize).or(v.first()).copied()
};
let mut updates: Vec<(usize, Xfrm, bool, bool)> = vec![];
for (i, s) in snapshot.iter().enumerate() {
let ShapeKind::Connector { start, end } = &s.kind else { continue };
if start.is_none() && end.is_none() {
continue;
}
let x = xfrm_of(&view, sel, s);
let (mut p0, mut p1) = endpoints(&x);
let a = start.and_then(|(id, k)| site(id, k));
let b = end.and_then(|(id, k)| site(id, k));
if let Some(q) = a {
p0 = q;
}
if let Some(q) = b {
p1 = q;
}
let nx = crate::tools::line_xfrm(p0, p1, x.rot);
let changed = (nx.x - x.x).abs() > 1e-6
|| (nx.y - x.y).abs() > 1e-6
|| (nx.w - x.w).abs() > 1e-6
|| (nx.h - x.h).abs() > 1e-6
|| nx.flip_h != x.flip_h
|| nx.flip_v != x.flip_v;
let drop_a = start.is_some() && a.is_none();
let drop_b = end.is_some() && b.is_none();
if changed || drop_a || drop_b {
updates.push((i, nx, drop_a, drop_b));
}
}
let Some(list) = crate::shapes_mut(doc, sel) else { return };
for (i, nx, drop_a, drop_b) in updates {
if let Some(s) = list.get_mut(i) {
s.xfrm = Some(nx);
if let ShapeKind::Connector { start, end } = &mut s.kind {
if drop_a {
*start = None;
}
if drop_b {
*end = None;
}
}
}
}
}
/// Glue end `end` (0 = start, 1 = end) of line `id` to `to` (or unglue with `None`). Plain lines
/// become connectors when glued.
pub fn set_glue(list: &mut [Shape], id: ShapeId, end: u8, to: Option<(ShapeId, u32)>) {
let Some(s) = list.iter_mut().find(|s| s.id == id) else { return };
if !matches!(s.kind, ShapeKind::Connector { .. }) {
if to.is_none() || !matches!(s.kind, ShapeKind::Shape) {
return;
}
s.kind = ShapeKind::Connector { start: None, end: None };
}
if let ShapeKind::Connector { start, end: e } = &mut s.kind {
if end == 0 {
*start = to;
} else {
*e = to;
}
}
}