Connectors: glue ends to connection sites, reroute on move/resize, shape.connect / shape.sites commands, site feedback on canvas
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
639957c68a
commit
74ea37b36a
|
|
@ -11,6 +11,16 @@ use crate::{Result, Session};
|
|||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(
|
||||
"shape.connect",
|
||||
"Connect Shapes",
|
||||
[],
|
||||
None,
|
||||
"{from: shape id, to: shape id, fromSite?, toSite?: site index (default: the closest pair), id?: existing line to glue (else a new connector), preset?: straightConnector1|bentConnector3|curvedConnector3} → {id}",
|
||||
has_slide,
|
||||
connect
|
||||
),
|
||||
cmd!(query "shape.sites", "Connection Sites", [], None, "{id} → [[x, y]] connection sites in slide points", has_slide, sites),
|
||||
cmd!("shape.move", "Move", [], None, "{dx?, dy?: pt (relative) | x?, y?: pt (absolute), ids?}", has_selection, move_by),
|
||||
cmd!("shape.resize", "Size", ["Shape Format", "Size"], None, "{w?, h?: pt, lockAspect?: bool, ids?}", has_selection, resize),
|
||||
cmd!("shape.setBounds", "Position and Size", [], None, "{x, y, w, h, ids?}", has_selection, set_bounds),
|
||||
|
|
@ -664,3 +674,60 @@ fn media_options(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
})?;
|
||||
Ok(json!({}))
|
||||
}
|
||||
|
||||
fn shape_id(p: &Value, key: &str) -> Option<slidecraft_model::ShapeId> {
|
||||
p.get(key).and_then(Value::as_u64).map(|v| slidecraft_model::ShapeId(v as u32))
|
||||
}
|
||||
|
||||
fn sites(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = shape_id(p, "id").ok_or_else(|| bad("shape.sites", "missing `id`"))?;
|
||||
let st = s.doc()?;
|
||||
let sh = st.shape(id).ok_or_else(|| bad("shape.sites", "no such shape"))?;
|
||||
Ok(json!(crate::connect::sites(&st.doc, &st.selection, sh).iter().map(|q| [q.x, q.y]).collect::<Vec<_>>()))
|
||||
}
|
||||
|
||||
fn connect(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (Some(from), Some(to)) = (shape_id(p, "from"), shape_id(p, "to")) else { return Err(bad("shape.connect", "missing `from` / `to`")) };
|
||||
let st = s.doc()?;
|
||||
let site_list = |id| st.shape(id).map(|sh| crate::connect::sites(&st.doc, &st.selection, sh)).unwrap_or_default();
|
||||
let (a, b) = (site_list(from), site_list(to));
|
||||
if a.is_empty() || b.is_empty() {
|
||||
return Err(bad("shape.connect", "both shapes need connection sites (lines and groups have none)"));
|
||||
}
|
||||
let pick = |v: &[slidecraft_geom::Point], key: &str| usize_param(p, key).filter(|i| *i < v.len());
|
||||
let (ia, ib) = match (pick(&a, "fromSite"), pick(&b, "toSite")) {
|
||||
(Some(i), Some(j)) => (i, j),
|
||||
(fi, fj) => {
|
||||
let mut best = (f64::MAX, 0, 0);
|
||||
for (i, pa) in a.iter().enumerate().filter(|(i, _)| fi.is_none_or(|f| f == *i)) {
|
||||
for (j, pb) in b.iter().enumerate().filter(|(j, _)| fj.is_none_or(|f| f == *j)) {
|
||||
let d = (*pa - *pb).hypot();
|
||||
if d < best.0 {
|
||||
best = (d, i, j);
|
||||
}
|
||||
}
|
||||
}
|
||||
(best.1, best.2)
|
||||
}
|
||||
};
|
||||
let (p0, p1) = (a[ia], b[ib]);
|
||||
let id = match shape_id(p, "id") {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
let preset = str_param(p, "preset").unwrap_or("straightConnector1");
|
||||
let r = s.execute("shape.insert", &json!({"preset": preset, "rect": [p0.x, p0.y, 1.0, 1.0]}))?;
|
||||
slidecraft_model::ShapeId(r.get("id").and_then(Value::as_u64).ok_or_else(|| bad("shape.connect", "insert failed"))? as u32)
|
||||
}
|
||||
};
|
||||
s.edit(|doc, sel| {
|
||||
let list = crate::shapes_mut(doc, sel).ok_or_else(|| bad("shape.connect", "no slide"))?;
|
||||
let sh = slidecraft_model::find_shape_mut(list, id).ok_or_else(|| bad("shape.connect", "no such line"))?;
|
||||
if !sh.is_line() {
|
||||
return Err(bad("shape.connect", "`id` is not a line or connector"));
|
||||
}
|
||||
sh.xfrm = Some(crate::tools::line_xfrm(p0, p1, 0.0));
|
||||
sh.kind = slidecraft_model::ShapeKind::Connector { start: Some((from, ia as u32)), end: Some((to, ib as u32)) };
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({"id": id.0, "fromSite": ia, "toSite": ib}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,137 @@
|
|||
//! 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 slidecraft_geom::{Point, Xfrm};
|
||||
use slidecraft_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 = slidecraft_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod cmd;
|
||||
pub mod connect;
|
||||
pub mod guard;
|
||||
pub mod keys;
|
||||
pub mod links;
|
||||
|
|
@ -381,6 +382,7 @@ impl Session {
|
|||
let mut doc = (*st.doc).clone();
|
||||
let mut sel = st.selection.clone();
|
||||
let r = f(&mut doc, &mut sel)?;
|
||||
connect::reroute(&mut doc, &sel);
|
||||
st.doc = Arc::new(doc);
|
||||
st.selection = sel;
|
||||
st.revision += 1;
|
||||
|
|
|
|||
|
|
@ -248,3 +248,65 @@ fn themes_and_slide_size() {
|
|||
let v = s.execute("slide.inspect", &json!({})).unwrap();
|
||||
assert!(v["shapes"].as_array().unwrap().iter().any(|x| x["placeholder"] == "sldNum"));
|
||||
}
|
||||
|
||||
fn line_ends(s: &Session, id: u64) -> (slidecraft_geom::Point, slidecraft_geom::Point) {
|
||||
let st = s.doc().unwrap();
|
||||
let sh = st.shape(slidecraft_model::ShapeId(id as u32)).unwrap();
|
||||
connect::endpoints(&cmd::xfrm_of(&st.doc, &st.selection, sh))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connectors_follow_glued_shapes() {
|
||||
let mut s = session();
|
||||
let a = s.execute("shape.insert", &json!({"preset": "rect", "rect": [100, 100, 100, 50]})).unwrap()["id"].as_u64().unwrap();
|
||||
let b = s.execute("shape.insert", &json!({"preset": "rect", "rect": [400, 300, 100, 50]})).unwrap()["id"].as_u64().unwrap();
|
||||
let r = s.execute("shape.connect", &json!({"from": a, "to": b, "preset": "bentConnector3"})).unwrap();
|
||||
let c = r["id"].as_u64().unwrap();
|
||||
let (p0, p1) = line_ends(&s, c);
|
||||
// Closest pair: right side of A → left side of B... or bottom/top; either way on the boxes.
|
||||
let sites_a: Vec<Vec<f64>> = serde_json::from_value(s.execute("shape.sites", &json!({"id": a})).unwrap()).unwrap();
|
||||
assert!(sites_a.iter().any(|q| (q[0] - p0.x).abs() < 1e-6 && (q[1] - p0.y).abs() < 1e-6));
|
||||
// Move B: the connector's end follows, its start stays.
|
||||
s.execute("edit.select", &json!({"ids": [b]})).unwrap();
|
||||
s.execute("shape.move", &json!({"dx": 50, "dy": 20})).unwrap();
|
||||
let (q0, q1) = line_ends(&s, c);
|
||||
assert!((q0 - p0).hypot() < 1e-6);
|
||||
assert!(((q1 - p1) - slidecraft_geom::Vec2::new(50.0, 20.0)).hypot() < 1e-6, "{p1:?} → {q1:?}");
|
||||
// Undo puts both back in one step.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!((line_ends(&s, c).1 - p1).hypot() < 1e-6);
|
||||
// Deleting a glued shape unglues that end and leaves the line where it was.
|
||||
s.execute("edit.select", &json!({"ids": [a]})).unwrap();
|
||||
s.execute("edit.delete", &json!({})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let sh = st.shape(slidecraft_model::ShapeId(c as u32)).unwrap();
|
||||
assert!(matches!(sh.kind, slidecraft_model::ShapeKind::Connector { start: None, end: Some(_) }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drawing_a_line_between_shapes_glues_it() {
|
||||
let mut s = session();
|
||||
let a = s.execute("shape.insert", &json!({"preset": "ellipse", "rect": [100, 100, 100, 100]})).unwrap()["id"].as_u64().unwrap();
|
||||
let b = s.execute("shape.insert", &json!({"preset": "rect", "rect": [400, 100, 100, 100]})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("edit.deselect", &json!({})).unwrap();
|
||||
s.tool.kind = tools::ToolKind::Shape { preset: "straightConnector1".into() };
|
||||
let ev = |kind, x: f64, y: f64| tools::PointerEvent { kind, x, y, mods: Default::default(), tol: 4.0 };
|
||||
// Right site of the ellipse (200,150) → left site of the rect (400,150), a little off.
|
||||
s.pointer(ev(tools::PointerKind::Down, 202.0, 151.0)).unwrap();
|
||||
s.pointer(ev(tools::PointerKind::Drag, 300.0, 150.0)).unwrap();
|
||||
assert!(!s.tool.sites.is_empty() || s.tool.glue.is_none());
|
||||
s.pointer(ev(tools::PointerKind::Drag, 398.0, 149.0)).unwrap();
|
||||
assert!(s.tool.glue.is_some());
|
||||
s.pointer(ev(tools::PointerKind::Up, 398.0, 149.0)).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let line = st.shapes().iter().find(|x| x.is_line()).unwrap();
|
||||
match line.kind {
|
||||
slidecraft_model::ShapeKind::Connector { start: Some((x, _)), end: Some((y, _)) } => {
|
||||
assert_eq!((x.0 as u64, y.0 as u64), (a, b));
|
||||
}
|
||||
ref k => panic!("not glued: {k:?}"),
|
||||
}
|
||||
let id = line.id.0 as u64;
|
||||
let (p0, p1) = line_ends(&s, id);
|
||||
assert!((p0.x - 200.0).abs() < 1e-6 && (p1.x - 400.0).abs() < 1e-6, "{p0:?} {p1:?}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,6 +121,10 @@ pub struct ToolState {
|
|||
pub guides: Vec<Guide>,
|
||||
pub hover: Option<Hit>,
|
||||
pub ink_preview: Vec<(f64, f64, f32)>,
|
||||
/// Connection sites to show (slide coordinates) while drawing or dragging a line end, and the
|
||||
/// site the end would glue to.
|
||||
pub sites: Vec<Point>,
|
||||
pub glue: Option<Point>,
|
||||
}
|
||||
|
||||
impl ToolState {
|
||||
|
|
@ -206,6 +210,13 @@ impl Session {
|
|||
match ev.kind {
|
||||
PointerKind::Move => {
|
||||
self.tool.hover = self.hit_test(p, tol);
|
||||
let line_tool = matches!(&self.tool.kind, ToolKind::Shape { preset } if slidecraft_geom::preset::is_line_like(preset));
|
||||
if line_tool {
|
||||
self.update_sites(p, tol, None);
|
||||
} else {
|
||||
self.tool.sites.clear();
|
||||
self.tool.glue = None;
|
||||
}
|
||||
Ok(Value::Null)
|
||||
}
|
||||
PointerKind::Down => self.down(p, ev.mods, tol),
|
||||
|
|
@ -393,6 +404,17 @@ impl Session {
|
|||
sh.xfrm = Some(Xfrm { x: o.x + d.x, y: o.y + d.y, ..*o });
|
||||
}
|
||||
}
|
||||
// A connector dragged away from shapes that stay put comes unglued at those ends.
|
||||
let moved: Vec<ShapeId> = origs2.iter().map(|o| o.0).collect();
|
||||
for sh in list.iter_mut().filter(|s| moved.contains(&s.id)) {
|
||||
if let ShapeKind::Connector { start, end } = &mut sh.kind {
|
||||
for c in [start, end] {
|
||||
if c.is_some_and(|(to, _)| !moved.contains(&to)) {
|
||||
*c = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
self.tool.drag =
|
||||
|
|
@ -411,8 +433,18 @@ impl Session {
|
|||
if mods.shift {
|
||||
q = constrain_45(other, q);
|
||||
}
|
||||
let glue = self.update_sites(q, tol, Some(id));
|
||||
if let Some((_, _, at)) = glue {
|
||||
q = at;
|
||||
}
|
||||
let (s0, s1) = if end == 0 { (q, p1) } else { (p0, q) };
|
||||
self.set_xfrm(id, line_xfrm(s0, s1, orig.rot))?;
|
||||
self.edit(|doc, sel| {
|
||||
if let Some(list) = crate::shapes_mut(doc, sel) {
|
||||
crate::connect::set_glue(list, id, end, glue.map(|g| (g.0, g.1)));
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
Drag::Rotate { ids, center, start, origs } => {
|
||||
let ang = (p.y - center.y).atan2(p.x - center.x);
|
||||
|
|
@ -474,8 +506,12 @@ impl Session {
|
|||
Drag::Create { start } => {
|
||||
let line = matches!(&self.tool.kind, ToolKind::Shape { preset } if slidecraft_geom::preset::is_line_like(preset));
|
||||
let x = if line {
|
||||
let q = if mods.shift { constrain_45(start, p) } else { p };
|
||||
line_xfrm(start, q, 0.0)
|
||||
let mut q = if mods.shift { constrain_45(start, p) } else { p };
|
||||
if let Some((_, _, at)) = self.update_sites(q, tol, None) {
|
||||
q = at;
|
||||
}
|
||||
let s0 = self.glue_at(start, tol).map(|g| g.2).unwrap_or(start);
|
||||
line_xfrm(s0, q, 0.0)
|
||||
} else {
|
||||
let mut r = Rect::from_points(start, p);
|
||||
if mods.shift {
|
||||
|
|
@ -515,6 +551,8 @@ impl Session {
|
|||
fn up(&mut self, p: Point, mods: Mods) -> Result<Value> {
|
||||
let drag = self.tool.drag.take();
|
||||
self.tool.guides.clear();
|
||||
self.tool.sites.clear();
|
||||
self.tool.glue = None;
|
||||
let mut out = Value::Null;
|
||||
match drag {
|
||||
Some(Drag::Move { moved: false, enter_text: Some((id, at)), .. }) => {
|
||||
|
|
@ -566,7 +604,20 @@ impl Session {
|
|||
};
|
||||
let r = self.execute("shape.insert", &json!({"preset": preset, "rect": [rect.x, rect.y, rect.w, rect.h], "flip": format!("{}{}", if rect.flip_h { "h" } else { "" }, if rect.flip_v { "v" } else { "" })}))?;
|
||||
if let (Some(id), Some(x)) = (r.get("id").and_then(Value::as_u64), preview.filter(|_| dragged && line)) {
|
||||
let _ = self.set_xfrm_cmd(ShapeId(id as u32), x);
|
||||
let id = ShapeId(id as u32);
|
||||
let _ = self.set_xfrm_cmd(id, x);
|
||||
// Glue the ends that landed on connection sites.
|
||||
let (p0, p1) = crate::connect::endpoints(&x);
|
||||
let (g0, g1) = (self.glue_at(p0, 4.0), self.glue_at(p1, 4.0));
|
||||
if g0.is_some() || g1.is_some() {
|
||||
let _ = self.edit(|doc, sel| {
|
||||
if let Some(list) = crate::shapes_mut(doc, sel) {
|
||||
crate::connect::set_glue(list, id, 0, g0.map(|g| (g.0, g.1)));
|
||||
crate::connect::set_glue(list, id, 1, g1.map(|g| (g.0, g.1)));
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
r
|
||||
}
|
||||
|
|
@ -647,6 +698,31 @@ impl Session {
|
|||
self.active().and_then(|d| d.shape(id)).is_some_and(|s| matches!(s.kind, ShapeKind::Picture { .. } | ShapeKind::Media(_)))
|
||||
}
|
||||
|
||||
/// The connection site within glue range of `p` (excluding shape `exclude`).
|
||||
fn glue_at(&self, p: Point, tol: f64) -> Option<(ShapeId, u32, Point)> {
|
||||
let st = self.active()?;
|
||||
crate::connect::nearest_site(&st.doc, &st.selection, st.shapes(), p, (tol * 2.0).max(6.0), None)
|
||||
}
|
||||
|
||||
/// Show the sites of the shape near `p` and return the site an end at `p` would glue to.
|
||||
fn update_sites(&mut self, p: Point, tol: f64, exclude: Option<ShapeId>) -> Option<(ShapeId, u32, Point)> {
|
||||
let (sites, glue) = match self.active() {
|
||||
Some(st) => {
|
||||
let shapes = st.shapes();
|
||||
let near = crate::connect::site_shape_at(&st.doc, &st.selection, shapes, p, tol * 3.0, exclude);
|
||||
let sites = near
|
||||
.and_then(|id| shapes.iter().find(|s| s.id == id))
|
||||
.map(|s| crate::connect::sites(&st.doc, &st.selection, s))
|
||||
.unwrap_or_default();
|
||||
(sites, crate::connect::nearest_site(&st.doc, &st.selection, shapes, p, (tol * 2.0).max(6.0), exclude))
|
||||
}
|
||||
None => (vec![], None),
|
||||
};
|
||||
self.tool.sites = sites;
|
||||
self.tool.glue = glue.map(|g| g.2);
|
||||
glue
|
||||
}
|
||||
|
||||
fn set_xfrm(&mut self, id: ShapeId, x: Xfrm) -> Result<()> {
|
||||
self.edit(|doc, sel| {
|
||||
let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("tool", "no slide"))?;
|
||||
|
|
|
|||
|
|
@ -280,8 +280,14 @@ fn overlays(app: &mut SlideApp, ui: &Ui, painter: &egui::Painter, xf: Xf, t: &To
|
|||
let corners = [Point::new(0.0, 0.0), Point::new(x.w, 0.0), Point::new(x.w, x.h), Point::new(0.0, x.h)].map(|q| xf.to_screen(a * q));
|
||||
if sh.is_line() {
|
||||
let (p0, p1) = (xf.to_screen(a * Point::new(0.0, 0.0)), xf.to_screen(a * Point::new(x.w, x.h)));
|
||||
for q in [p0, p1] {
|
||||
painter.circle(q, 4.5, t.handle_fill, Stroke::new(1.0, t.handle_stroke));
|
||||
let glued = match sh.kind {
|
||||
slidecraft_model::ShapeKind::Connector { start, end } => [start.is_some(), end.is_some()],
|
||||
_ => [false, false],
|
||||
};
|
||||
for (q, g) in [p0, p1].into_iter().zip(glued) {
|
||||
// Glued ends are filled with the accent, like attached connector ends.
|
||||
let fill = if g { t.accent } else { t.handle_fill };
|
||||
painter.circle(q, 4.5, fill, Stroke::new(1.0, if g { t.handle_fill } else { t.handle_stroke }));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
|
@ -375,6 +381,14 @@ fn overlays(app: &mut SlideApp, ui: &Ui, painter: &egui::Painter, xf: Xf, t: &To
|
|||
if let Some(pv) = app.session.tool.preview {
|
||||
draw_preview(app, painter, xf, pv, t);
|
||||
}
|
||||
// Connection sites of the shape under a connector end, and the site it would glue to.
|
||||
for q in &app.session.tool.sites {
|
||||
let c = xf.to_screen(*q);
|
||||
painter.circle(c, 3.5, t.handle_fill, Stroke::new(1.0, t.handle_stroke));
|
||||
}
|
||||
if let Some(g) = app.session.tool.glue {
|
||||
painter.circle(xf.to_screen(g), 5.0, t.accent, Stroke::new(1.5, t.handle_fill));
|
||||
}
|
||||
if app.session.tool.ink_preview.len() > 1
|
||||
&& let ToolKind::Ink { color, width, mode } = &app.session.tool.kind
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue