mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-02 06:57:04 -06:00
370 lines
17 KiB
Rust
370 lines
17 KiB
Rust
use super::hit::{constrain_45, line_xfrm, resize_box};
|
|
use super::{
|
|
Affine, DRAW_TABLE, Drag, Mods, Point, Rect, Result, Session, ShapeId, ShapeKind, TextSel, ToolKind, Value, Vec2, Xfrm, cmd, json, set_warp_adj,
|
|
};
|
|
|
|
struct MoveDrag {
|
|
ids: Vec<ShapeId>,
|
|
start: Point,
|
|
origs: Vec<(ShapeId, Xfrm)>,
|
|
moved: bool,
|
|
duplicate: bool,
|
|
enter_text: Option<(ShapeId, Point)>,
|
|
}
|
|
|
|
impl Session {
|
|
pub(super) fn drag(&mut self, p: Point, mods: Mods, tol: f64) -> Result<Value> {
|
|
let Some(drag) = self.tool.drag.clone() else { return Ok(Value::Null) };
|
|
match drag {
|
|
Drag::Guide { index } => {
|
|
let vertical = self.active().and_then(|d| d.doc.guides.get(index)).is_some_and(|g| g.vertical);
|
|
let pos = if vertical { p.x } else { p.y };
|
|
// Guides move in whole points unless snapping is off, as in PowerPoint.
|
|
let pos = if self.prefs.snap_to_grid && !mods.alt { pos.round() } else { pos };
|
|
self.tool.guide_drag = Some((index, pos));
|
|
}
|
|
Drag::Move { ids, start, origs, moved, duplicate, enter_text } => {
|
|
return self.drag_move(MoveDrag { ids, start, origs, moved, duplicate, enter_text }, p, mods, tol);
|
|
}
|
|
Drag::Resize { id, orig, ids, handle, origs, start } => {
|
|
let keep_aspect = mods.shift || self.is_picture(id);
|
|
let nx = resize_box(&orig, handle, p - start, keep_aspect, mods.alt);
|
|
let nx = self.snap_resize(nx, handle, &ids, tol);
|
|
let sx = if orig.w > 1e-9 { (nx.w / orig.w) * if nx.flip_h != orig.flip_h { -1.0 } else { 1.0 } - 1.0 } else { 0.0 };
|
|
let sy = if orig.h > 1e-9 { (nx.h / orig.h) * if nx.flip_v != orig.flip_v { -1.0 } else { 1.0 } - 1.0 } else { 0.0 };
|
|
let div = if mods.alt { 2.0 } else { 1.0 };
|
|
let hx = match handle {
|
|
0 | 6 | 7 => -1.0,
|
|
2..=4 => 1.0,
|
|
_ => 0.0,
|
|
};
|
|
let hy = match handle {
|
|
0..=2 => -1.0,
|
|
4..=6 => 1.0,
|
|
_ => 0.0,
|
|
};
|
|
for (tid, o) in &origs {
|
|
let target_nx = if *tid == id {
|
|
nx
|
|
} else {
|
|
let lp = Affine::rotate(o.rot.to_radians())
|
|
* Point::new(
|
|
hx * sx * o.w / div * if o.flip_h { -1.0 } else { 1.0 },
|
|
hy * sy * o.h / div * if o.flip_v { -1.0 } else { 1.0 },
|
|
);
|
|
resize_box(o, handle, Vec2::new(lp.x, lp.y), keep_aspect, mods.alt)
|
|
};
|
|
self.set_xfrm(*tid, target_nx)?;
|
|
}
|
|
}
|
|
Drag::LineEnd { id, end, orig } => {
|
|
let a = orig.affine();
|
|
let (p0, p1) = (a * Point::new(0.0, 0.0), a * Point::new(orig.w, orig.h));
|
|
let mut q = p;
|
|
let other = if end == 0 { p1 } else { p0 };
|
|
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);
|
|
let mut delta = (ang - start).to_degrees();
|
|
for (id, o) in &origs {
|
|
let mut r = o.rot + delta;
|
|
if mods.shift {
|
|
r = (r / 15.0).round() * 15.0;
|
|
delta = r - o.rot;
|
|
}
|
|
let mut nx = *o;
|
|
nx.rot = ((r % 360.0) + 360.0) % 360.0;
|
|
if ids.len() > 1 {
|
|
// Rotate positions around the common centre too.
|
|
let c = o.center();
|
|
let rc = Affine::translate(center.to_vec2()) * Affine::rotate(delta.to_radians()) * Affine::translate(-center.to_vec2()) * c;
|
|
nx.x = rc.x - o.w / 2.0;
|
|
nx.y = rc.y - o.h / 2.0;
|
|
}
|
|
self.set_xfrm(*id, nx)?;
|
|
}
|
|
}
|
|
Drag::Adjust { id, index, orig } => return self.drag_adjust(id, index, orig, p),
|
|
Drag::Vertex { id, path, index } => {
|
|
self.tool.drag = Some(Drag::Vertex { id, path, index });
|
|
self.edit_vertex(id, path, index, p)?;
|
|
return Ok(Value::Null);
|
|
}
|
|
Drag::Handle { id, path, index, out } => {
|
|
self.tool.drag = Some(Drag::Handle { id, path, index, out });
|
|
let side = if out { "out" } else { "in" };
|
|
// Shift, Ctrl or Alt held while dragging a handle makes the point smooth, straight or
|
|
// corner, as in PowerPoint; Cmd and Option count as Ctrl and Alt.
|
|
let held = if mods.shift {
|
|
Some("smooth")
|
|
} else if mods.cmd {
|
|
Some("straight")
|
|
} else if mods.alt {
|
|
Some("corner")
|
|
} else {
|
|
None
|
|
};
|
|
if let Err(e) = self.execute_inner_move_handle(id, path, index, side, p, held) {
|
|
log::debug!("edit handle: {e}");
|
|
}
|
|
return Ok(Value::Null);
|
|
}
|
|
Drag::Warp { id, index, name, base, orig } => {
|
|
let v = {
|
|
let st = self.doc()?;
|
|
let Some(sh) = st.shape(id) else { return Ok(Value::Null) };
|
|
let tr = deckcraft_render::shape_geometry(sh, orig.w, orig.h).text_rect;
|
|
let hs = deckcraft_geom::warp::handles(&name, &base, tr);
|
|
let Some(h) = hs.iter().find(|h| h.index == index) else { return Ok(Value::Null) };
|
|
h.value_at(orig.affine().inverse() * p)
|
|
};
|
|
self.edit(|doc, sel| {
|
|
let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("adjust", "no slide"))?;
|
|
if let Some(sh) = deckcraft_model::find_shape_mut(list, id) {
|
|
set_warp_adj(sh, &name, &base, index, v);
|
|
}
|
|
Ok(())
|
|
})?;
|
|
}
|
|
Drag::Marquee { start, .. } => {
|
|
self.tool.marquee = Some(Rect::from_points(start, p));
|
|
}
|
|
Drag::Create { start } => self.drag_create(start, p, mods, tol),
|
|
Drag::TextSelect { shape, press, .. } => return self.drag_text_select(shape, press, p),
|
|
Drag::TableBorder { id, column, index, start, size, frame, moved } => {
|
|
let local = frame.inverse() * p;
|
|
let d = if column { local.x - start.x } else { local.y - start.y };
|
|
if !moved && d.abs() < tol.max(1.0) {
|
|
return Ok(Value::Null);
|
|
}
|
|
if !moved {
|
|
self.begin(if column { "Column Width" } else { "Row Height" });
|
|
}
|
|
// An inner column border moves between its two columns (the table keeps its
|
|
// width); the right edge widens the last column and the table. A row's height is
|
|
// a minimum: rows still grow to fit their text.
|
|
if column {
|
|
let width = (size + d).max(cmd::table::MIN_COLUMN_WIDTH);
|
|
self.execute("table.columnWidth", &json!({"id": id, "col": index, "width": width, "keepTotal": true}))?;
|
|
} else {
|
|
self.execute("table.rowHeight", &json!({"id": id, "row": index, "height": (size + d).max(1.0)}))?;
|
|
}
|
|
self.tool.drag = Some(Drag::TableBorder { id, column, index, start, size, frame, moved: true });
|
|
}
|
|
Drag::Freeform { mode, mut points, pressed } => {
|
|
// Dragging scribbles (except for curves, which only take clicked points).
|
|
if pressed && mode != "curve" && points.last().is_none_or(|l| (*l - p).hypot() >= 1.5) {
|
|
points.push(p);
|
|
}
|
|
let mut v = points.clone();
|
|
if mode == "curve" {
|
|
v.push(p);
|
|
}
|
|
self.tool.path_preview = v;
|
|
self.tool.drag = Some(Drag::Freeform { mode, points, pressed });
|
|
}
|
|
Drag::Ink { mut points } => {
|
|
points.push(self.ink_sample(p));
|
|
self.tool.ink_preview = points.clone();
|
|
self.tool.drag = Some(Drag::Ink { points });
|
|
}
|
|
}
|
|
Ok(Value::Null)
|
|
}
|
|
|
|
fn drag_move(&mut self, drag: MoveDrag, p: Point, mods: Mods, tol: f64) -> Result<Value> {
|
|
let MoveDrag { ids, start, origs, moved, duplicate, enter_text } = drag;
|
|
let mut d = p - start;
|
|
if !moved && (d.hypot() < tol.max(1.0) || origs.is_empty()) {
|
|
return Ok(Value::Null);
|
|
}
|
|
if !moved {
|
|
self.begin(if duplicate { "Duplicate" } else { "Move" });
|
|
if duplicate {
|
|
// Alt-drag: drag a copy, leave the original.
|
|
self.execute_dup_in_place(&ids)?;
|
|
}
|
|
}
|
|
let ids_now = if duplicate { self.doc()?.selection.shapes.clone() } else { ids.clone() };
|
|
if mods.shift {
|
|
if d.x.abs() > d.y.abs() {
|
|
d.y = 0.0;
|
|
} else {
|
|
d.x = 0.0;
|
|
}
|
|
}
|
|
// Smart guides on the moved group's bounds.
|
|
let bounds = origs.iter().map(|(_, x)| x.bounds()).reduce(|a, b| a.union(b)).unwrap_or_default();
|
|
let moved_b = bounds + d;
|
|
let (snap, guides) = self.snap_rect(moved_b, &ids_now, tol * 1.5, !mods.cmd);
|
|
d += snap;
|
|
self.tool.guides = guides;
|
|
let origs2: Vec<(ShapeId, Xfrm)> = if duplicate { ids_now.iter().copied().zip(origs.iter().map(|o| o.1)).collect() } else { origs.clone() };
|
|
self.edit(|doc, sel| {
|
|
let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("move", "no slide"))?;
|
|
for (id, o) in &origs2 {
|
|
if let Some(sh) = deckcraft_model::find_shape_mut(list, *id) {
|
|
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 =
|
|
Some(Drag::Move { ids: ids_now, start, origs: origs2, moved: true, duplicate: false, enter_text: enter_text.filter(|_| false) });
|
|
Ok(Value::Null)
|
|
}
|
|
|
|
fn drag_adjust(&mut self, id: ShapeId, index: usize, orig: Xfrm, p: Point) -> Result<Value> {
|
|
let st = self.doc()?;
|
|
let Some(sh) = st.shape(id).cloned() else { return Ok(Value::Null) };
|
|
let local = orig.affine().inverse() * p;
|
|
let geo = deckcraft_render::shape_geometry(&sh, orig.w, orig.h);
|
|
let Some(h) = geo.handles.iter().find(|h| h.index == index) else { return Ok(Value::Null) };
|
|
let free2d = matches!(sh.geom.preset_name(), Some(n) if n.contains("Callout") && !n.contains("Arrow"));
|
|
let vals: Vec<(usize, f64)> = if free2d {
|
|
let dx = (local.x - orig.w / 2.0) / orig.w.max(1e-6) * 1e5;
|
|
let dy = (local.y - orig.h / 2.0) / orig.h.max(1e-6) * 1e5;
|
|
vec![(0, dx.round()), (1, dy.round())]
|
|
} else {
|
|
vec![(index, h.value_at(local))]
|
|
};
|
|
self.edit(|doc, sel| {
|
|
let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("adjust", "no slide"))?;
|
|
if let Some(sh) = deckcraft_model::find_shape_mut(list, id)
|
|
&& let deckcraft_model::Geom::Preset { name, adj } = &mut sh.geom
|
|
{
|
|
let defaults = deckcraft_geom::preset::info(name).map(|x| x.defaults).unwrap_or(&[]);
|
|
for (i, v) in &vals {
|
|
while adj.len() <= *i {
|
|
let k = adj.len();
|
|
adj.push(defaults.get(k).copied().unwrap_or(0.0));
|
|
}
|
|
if let Some(slot) = adj.get_mut(*i) {
|
|
*slot = *v;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
})?;
|
|
Ok(Value::Null)
|
|
}
|
|
|
|
fn drag_create(&mut self, start: Point, p: Point, mods: Mods, tol: f64) {
|
|
let line = matches!(&self.tool.kind, ToolKind::Shape { preset } if deckcraft_geom::preset::is_line_like(preset));
|
|
let x = if line {
|
|
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 if matches!(&self.tool.kind, ToolKind::Shape { preset } if preset == DRAW_TABLE)
|
|
&& crate::cmd::table::table_at(self, &Value::Null, (start.x, start.y)).is_some()
|
|
{
|
|
// The pencil inside a table draws a straight border, across or down.
|
|
let q = if (p.x - start.x).abs() >= (p.y - start.y).abs() { Point::new(p.x, start.y) } else { Point::new(start.x, p.y) };
|
|
let r = Rect::from_points(start, q);
|
|
Xfrm::new(r.x0, r.y0, r.width(), r.height())
|
|
} else {
|
|
let mut r = Rect::from_points(start, p);
|
|
if mods.shift {
|
|
let s = r.width().max(r.height());
|
|
let sx = if p.x < start.x { -s } else { s };
|
|
let sy = if p.y < start.y { -s } else { s };
|
|
r = Rect::from_points(start, start + Vec2::new(sx, sy));
|
|
}
|
|
if mods.alt {
|
|
let d = p - start;
|
|
r = Rect::new(start.x - d.x.abs(), start.y - d.y.abs(), start.x + d.x.abs(), start.y + d.y.abs());
|
|
}
|
|
Xfrm::new(r.x0, r.y0, r.width(), r.height())
|
|
};
|
|
self.tool.preview = Some(x);
|
|
}
|
|
|
|
fn drag_text_select(&mut self, shape: ShapeId, press: Option<TextSel>, p: Point) -> Result<Value> {
|
|
// In a table, leaving the cell the drag started in selects whole cells, as in
|
|
// PowerPoint; coming back to that cell selects text in it again.
|
|
let st = self.doc()?;
|
|
if let Some(start) = press.as_ref().and_then(|t| t.cell)
|
|
&& let Some(f) = st.shape(shape).and_then(|sh| cmd::table::table_frame(st, sh))
|
|
&& let Some(cell) = f.cell_at(f.local(p))
|
|
{
|
|
if cell != start {
|
|
self.select(|_, sel| {
|
|
sel.text = None;
|
|
sel.shapes = vec![shape];
|
|
sel.cells = Some((shape, start, cell));
|
|
})?;
|
|
return Ok(Value::Null);
|
|
}
|
|
if st.selection.text.is_none() {
|
|
self.select(|_, sel| {
|
|
sel.cells = None;
|
|
sel.text = press;
|
|
})?;
|
|
}
|
|
}
|
|
let st = self.doc()?;
|
|
if let Some(t) = st.selection.text.as_ref().filter(|t| t.shape == shape) {
|
|
let pos = cmd::text::text_pos_at(st, t, p);
|
|
self.select(|_, sel| {
|
|
if let Some(t) = sel.text.as_mut() {
|
|
t.caret = pos;
|
|
}
|
|
})?;
|
|
}
|
|
Ok(Value::Null)
|
|
}
|
|
|
|
/// Moves a vertex during a drag (one undo step for the whole drag).
|
|
fn edit_vertex(&mut self, id: ShapeId, path: usize, index: usize, p: Point) -> Result<()> {
|
|
let r = cmd::points::move_point_raw(self, id, path, index, p);
|
|
if let Err(e) = r {
|
|
log::debug!("edit point: {e}");
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Moves a control handle during a drag. `held` is the point type set by the modifier held
|
|
/// during the drag; the point keeps that type for later drags, as after `shape.pointType`.
|
|
fn execute_inner_move_handle(&mut self, id: ShapeId, path: usize, index: usize, side: &str, p: Point, held: Option<&str>) -> Result<Value> {
|
|
let kind = match held {
|
|
Some(k) => Some(k.to_string()),
|
|
None => self.tool.vertex_kind.clone().filter(|(s, pp, i, _)| (*s, *pp, *i) == (id, path, index)).map(|k| k.3),
|
|
};
|
|
let r =
|
|
cmd::points::move_handle_raw(self, &json!({"id": id.0, "path": path, "index": index, "side": side, "x": p.x, "y": p.y, "type": kind}))?;
|
|
if let Some(k) = held {
|
|
self.tool.vertex_kind = Some((id, path, index, k.to_string()));
|
|
}
|
|
Ok(r)
|
|
}
|
|
}
|