//! Edit Points (Shape Format ▸ Edit Shape ▸ Edit Points): turn a shape into a freeform and move, //! add or delete the vertices of its outline. //! //! While points are edited every custom path of the shape is kept in shape-local points (its //! `w`/`h` equal the shape's size), so a vertex's path coordinates are its offset in the shape. //! After each change the shape's box is fitted around the outline again, keeping it in place on //! the slide (rotation and flips included). use deckcraft_geom::{PathEl, Point, Xfrm}; use deckcraft_model::{CustomPath, Geom, Shape, ShapeId, ShapeKind}; use serde_json::{Value, json}; use super::*; use crate::{Result, Session}; pub fn specs() -> Vec { vec![ cmd!( "shape.editPoints", "Edit Points", ["Shape Format", "Insert Shapes", "Edit Shape"], None, "{on?: bool (default: toggle), id?} — enter or leave Edit Points for one selected shape; entering turns a preset into a freeform", has_selection, edit_points ), cmd!(query "shape.points", "Points", [], None, "{id?} → {id, paths: [[[x, y] slide pt]], types: [[smooth|straight|corner]]} vertices of a freeform", has_selection, points), cmd!("shape.movePoint", "Move Point", [], None, "{path?: 0, index, x, y: slide pt, id?}", has_selection, move_point), cmd!( "shape.addPoint", "Add Point", [], None, "{path?: 0, segment: index of the vertex ending it, t?: 0..1 (0.5), id?}", has_selection, add_point ), cmd!( "shape.pointType", "Point Type", [], None, "{path?: 0, index, type: smooth|straight|corner, id?} — smooth: handles in line and equal; straight: in line; corner: independent", has_selection, point_type ), cmd!( "shape.moveHandle", "Move Handle", [], None, "{path?: 0, index, side: in|out, x, y: slide pt, type?: smooth|straight|corner (default: read from the handles), id?}", has_selection, move_handle ), cmd!("shape.deletePoint", "Delete Point", [], None, "{path?: 0, index, id?}", has_selection, delete_point), ] } /// One path command, in path units. #[derive(Clone, Copy, Debug, PartialEq)] pub enum Seg { M(Point), L(Point), C(Point, Point, Point), Q(Point, Point), Z, } impl Seg { pub fn end(&self) -> Option { match self { Seg::M(p) | Seg::L(p) | Seg::C(_, _, p) | Seg::Q(_, p) => Some(*p), Seg::Z => None, } } fn points_mut(&mut self) -> Vec<&mut Point> { match self { Seg::M(p) | Seg::L(p) => vec![p], Seg::C(a, b, p) => vec![a, b, p], Seg::Q(a, p) => vec![a, p], Seg::Z => vec![], } } } /// Parses path data (`M x y L x y C … Q … Z`); malformed commands are skipped. pub fn parse(d: &str) -> Vec { let toks: Vec<&str> = d.split(|c: char| c.is_whitespace() || c == ',').filter(|t| !t.is_empty()).collect(); let num = |i: usize| toks.get(i).and_then(|t| t.parse::().ok()).filter(|v| v.is_finite()); let pt = |i: usize| Some(Point::new(num(i)?, num(i + 1)?)); let mut out = Vec::new(); let mut i = 0; while i < toks.len() && out.len() < 100_000 { let (seg, n) = match toks.get(i).copied().unwrap_or("") { "M" => (pt(i + 1).map(Seg::M), 3), "L" => (pt(i + 1).map(Seg::L), 3), "C" => ((|| Some(Seg::C(pt(i + 1)?, pt(i + 3)?, pt(i + 5)?)))(), 7), "Q" => ((|| Some(Seg::Q(pt(i + 1)?, pt(i + 3)?)))(), 5), "Z" => (Some(Seg::Z), 1), _ => (None, 1), }; // Make a closing edge explicit, so it is a segment that can take points. if matches!(seg, Some(Seg::Z)) { let start = out.iter().rev().find_map(|x| if let Seg::M(p) = x { Some(*p) } else { None }); let end = out.last().and_then(Seg::end); if let (Some(a), Some(b)) = (start, end) && (a - b).hypot() > 1e-6 { out.push(Seg::L(a)); } } if let Some(s) = seg { // A path starts with a move. let s = match (out.is_empty(), s) { (true, Seg::L(p)) => Seg::M(p), (true, Seg::C(..) | Seg::Q(..) | Seg::Z) => { i += n; continue; } (_, s) => s, }; out.push(s); } i += n; } out } /// Path data for `segs`. pub fn write(segs: &[Seg]) -> String { let f = |v: f64| { let s = format!("{:.3}", v); let s = s.trim_end_matches('0').trim_end_matches('.'); if s == "-0" { "0".to_string() } else { s.to_string() } }; let p = |q: &Point| format!("{} {}", f(q.x), f(q.y)); segs.iter() .map(|s| match s { Seg::M(a) => format!("M {}", p(a)), Seg::L(a) => format!("L {}", p(a)), Seg::C(a, b, c) => format!("C {} {} {}", p(a), p(b), p(c)), Seg::Q(a, b) => format!("Q {} {}", p(a), p(b)), Seg::Z => "Z".into(), }) .collect::>() .join(" ") } /// Whether segment `i` ends back on its sub-path's start right before a `Z` (the closing edge of /// a closed outline). Its end is the same vertex as the start, not a vertex of its own. fn closes_onto_start(segs: &[Seg], i: usize) -> bool { if !matches!(segs.get(i + 1), Some(Seg::Z)) { return false; } let start = segs.get(..=i).and_then(|s| s.iter().rev().find_map(|x| if let Seg::M(p) = x { Some(*p) } else { None })); match (start, segs.get(i).and_then(Seg::end)) { (Some(a), Some(b)) => !matches!(segs.get(i), Some(Seg::M(_))) && (a - b).hypot() < 1e-6, _ => false, } } /// The segments whose end is a vertex, in order. pub fn vertices(segs: &[Seg]) -> Vec { (0..segs.len()).filter(|i| segs.get(*i).and_then(Seg::end).is_some() && !closes_onto_start(segs, *i)).collect() } fn shift(segs: &mut [Seg], i: usize, which: &[usize], d: deckcraft_geom::Vec2) { if let Some(s) = segs.get_mut(i) { let mut pts = s.points_mut(); for w in which { if let Some(p) = pts.get_mut(*w) { **p += d; } } } } /// The indices of the points of segment `i` that sit on its end vertex (the end and, for a /// cubic, its incoming control point). fn end_points(seg: Option<&Seg>) -> &'static [usize] { match seg { Some(Seg::C(..)) => &[1, 2], Some(Seg::Q(..)) => &[1], Some(Seg::M(_) | Seg::L(_)) => &[0], _ => &[], } } /// Moves the vertex ending segment `si` by `d`, with the control points attached to it. fn move_vertex(segs: &mut [Seg], si: usize, d: deckcraft_geom::Vec2) { // A closed sub-path's start is also its closing edge's end. let close = if matches!(segs.get(si), Some(Seg::M(_))) { (si + 1..segs.len()).take_while(|j| !matches!(segs.get(*j), Some(Seg::M(_)))).find(|j| closes_onto_start(segs, *j)) } else { None }; let own = end_points(segs.get(si)); shift(segs, si, own, d); // The outgoing curve's first control point follows the vertex. if matches!(segs.get(si + 1), Some(Seg::C(..))) { shift(segs, si + 1, &[0], d); } if let Some(j) = close { let w = end_points(segs.get(j)); shift(segs, j, w, d); } } /// The point where segment `si` starts. fn seg_start(segs: &[Seg], si: usize) -> Option { let prev = segs.get(..si)?; match prev.last()? { Seg::Z => prev.iter().rev().find_map(|x| if let Seg::M(p) = x { Some(*p) } else { None }), s => s.end(), } } /// Splits segment `si` at `t`, adding a vertex there. Returns false for a move or close. fn split(segs: &mut Vec, si: usize, t: f64) -> bool { let Some(p0) = seg_start(segs, si) else { return false }; let lerp = |a: Point, b: Point| a.lerp(b, t); let (a, b) = match segs.get(si).copied() { Some(Seg::L(p1)) => (Seg::L(lerp(p0, p1)), Seg::L(p1)), Some(Seg::Q(c, p1)) => { let (m0, m1) = (lerp(p0, c), lerp(c, p1)); let m = m0.lerp(m1, t); (Seg::Q(m0, m), Seg::Q(m1, p1)) } Some(Seg::C(c1, c2, p1)) => { let (a1, a2, a3) = (lerp(p0, c1), lerp(c1, c2), lerp(c2, p1)); let (b1, b2) = (a1.lerp(a2, t), a2.lerp(a3, t)); let m = b1.lerp(b2, t); (Seg::C(a1, b1, m), Seg::C(b2, a3, p1)) } _ => return false, }; if let Some(s) = segs.get_mut(si) { *s = a; } segs.insert(si + 1, b); true } /// Removes the vertex ending segment `si`. A sub-path keeps at least two vertices. fn remove_vertex(segs: &mut Vec, si: usize) -> bool { // The vertices of `si`'s sub-path. let from = segs.get(..=si).and_then(|s| s.iter().rposition(|x| matches!(x, Seg::M(_)))).unwrap_or(0); let to = (si + 1..segs.len()).find(|j| matches!(segs.get(*j), Some(Seg::M(_)))).unwrap_or(segs.len()); let count = vertices(segs).iter().filter(|v| (from..to).contains(*v)).count(); if count <= 2 { return false; } if matches!(segs.get(si), Some(Seg::M(_))) { // The next vertex becomes the start; a closed outline's closing edge now ends there. let Some(next) = segs.get(si + 1).and_then(Seg::end) else { return false }; segs.remove(si + 1); if let Some(s) = segs.get_mut(si) { *s = Seg::M(next); } let close = (si + 1..segs.len()).take_while(|j| !matches!(segs.get(*j), Some(Seg::M(_)))).find(|j| matches!(segs.get(j + 1), Some(Seg::Z))); if let Some(j) = close && let Some(Seg::L(p) | Seg::C(_, _, p) | Seg::Q(_, p)) = segs.get_mut(j) { *p = next; } return true; } // Merge with the next segment: it now starts where the removed vertex's segment started. match (segs.get(si).copied(), segs.get(si + 1).copied()) { (Some(Seg::C(c1, _, _)), Some(Seg::C(_, c2, p))) => { segs.remove(si + 1); if let Some(s) = segs.get_mut(si) { *s = Seg::C(c1, c2, p); } } _ => { segs.remove(si); } } true } /// Custom paths of a preset shape, in shape-local points. fn to_custom(sh: &Shape, w: f64, h: f64) -> Vec { let geo = deckcraft_render::shape_geometry(sh, w, h); geo.paths .iter() .map(|sp| { let segs: Vec = sp .path .elements() .iter() .map(|e| match *e { PathEl::MoveTo(p) => Seg::M(p), PathEl::LineTo(p) => Seg::L(p), PathEl::QuadTo(a, p) => Seg::Q(a, p), PathEl::CurveTo(a, b, p) => Seg::C(a, b, p), PathEl::ClosePath => Seg::Z, }) .collect(); CustomPath { w, h, d: write(&segs), fill: sp.fill, stroke: sp.stroke } }) .collect() } /// Rescales every path of a custom geometry to shape-local points. fn normalize(paths: &mut [CustomPath], w: f64, h: f64) { for p in paths { let (sx, sy) = (if p.w > 0.0 { w / p.w } else { 1.0 }, if p.h > 0.0 { h / p.h } else { 1.0 }); if (sx - 1.0).abs() > 1e-12 || (sy - 1.0).abs() > 1e-12 { let mut segs = parse(&p.d); for s in &mut segs { for q in s.points_mut() { *q = Point::new(q.x * sx, q.y * sy); } } p.d = write(&segs); } p.w = w; p.h = h; } } /// Fits the box around the outline, keeping the drawing where it is on the slide. fn refit(sh: &mut Shape) { let (Some(x), Geom::Custom { paths }) = (sh.xfrm, &mut sh.geom) else { return }; let mut all: Vec> = paths.iter().map(|p| parse(&p.d)).collect(); let mut bb: Option = None; for s in all.iter_mut().flatten() { for q in s.points_mut() { bb = Some(bb.map_or(deckcraft_geom::Rect::from_points(*q, *q), |b| b.union_pt(*q))); } } let Some(bb) = bb else { return }; let (w, h) = (bb.width().max(1.0), bb.height().max(1.0)); if bb.x0.abs() < 1e-6 && bb.y0.abs() < 1e-6 && (w - x.w).abs() < 1e-6 && (h - x.h).abs() < 1e-6 { return; } let c = x.affine() * Point::new(bb.x0 + w / 2.0, bb.y0 + h / 2.0); let off = deckcraft_geom::Vec2::new(bb.x0, bb.y0); for (p, segs) in paths.iter_mut().zip(&mut all) { for s in segs.iter_mut() { for q in s.points_mut() { *q -= off; } } p.d = write(segs); p.w = w; p.h = h; } sh.xfrm = Some(Xfrm { x: c.x - w / 2.0, y: c.y - h / 2.0, w, h, ..x }); } /// The one shape whose points are edited: `id`, or the single selected shape. fn target(s: &Session, p: &Value, cmd: &str) -> Result { let st = s.doc()?; let id = match id_param(p, "id", cmd)? { Some(i) => i, None => match st.selection.shapes.as_slice() { [one] => *one, _ => return Err(bad(cmd, "select one shape")), }, }; let sh = st.shape(id).ok_or_else(|| bad(cmd, "no such shape"))?; if !st.shapes().iter().any(|t| t.id == id) { return Err(bad(cmd, "a shape in a group can't have its points edited; ungroup it first")); } if !matches!(sh.kind, ShapeKind::Shape) || sh.is_line() || sh.xfrm.is_none() { return Err(bad(cmd, "only a shape with an outline has points to edit")); } if sh.locked { return Err(bad(cmd, "the shape is locked")); } Ok(id) } /// Makes `id` a freeform in shape-local points (the state Edit Points works in). pub(crate) fn prepare(s: &mut Session, id: ShapeId) -> Result<()> { let st = s.doc()?; let Some(sh) = st.shape(id) else { return Ok(()) }; let Some(x) = sh.xfrm else { return Ok(()) }; let ready = matches!(&sh.geom, Geom::Custom { paths } if paths.iter().all(|p| p.w == x.w && p.h == x.h)); if ready { return Ok(()); } s.edit(|doc, sel| { let list = crate::shapes_mut(doc, sel).ok_or_else(|| bad("shape.editPoints", "no slide"))?; if let Some(sh) = deckcraft_model::find_shape_mut(list, id) { match &mut sh.geom { Geom::Custom { paths } => normalize(paths, x.w, x.h), Geom::Preset { .. } => { let paths = to_custom(sh, x.w, x.h); sh.geom = Geom::Custom { paths }; } } } Ok(()) }) } fn edit_points(s: &mut Session, p: &Value) -> Result { let on = p.get("on").and_then(Value::as_bool).unwrap_or(s.tool.edit_points.is_none()); s.tool.edit_vertex = None; if !on { s.tool.edit_points = None; return Ok(json!({"editing": Value::Null})); } let id = target(s, p, "shape.editPoints")?; prepare(s, id)?; s.tool.edit_points = Some(id); Ok(json!({"editing": id.0})) } /// Vertices of every path of `sh` in slide points. pub fn slide_vertices(doc: &deckcraft_model::Presentation, sel: &crate::Selection, sh: &Shape) -> Vec> { let Geom::Custom { paths } = &sh.geom else { return vec![] }; let x = xfrm_of(doc, sel, sh); let a = x.affine(); paths .iter() .map(|p| { let (sx, sy) = (if p.w > 0.0 { x.w / p.w } else { 1.0 }, if p.h > 0.0 { x.h / p.h } else { 1.0 }); let segs = parse(&p.d); vertices(&segs).iter().filter_map(|i| segs.get(*i).and_then(Seg::end)).map(|q| a * Point::new(q.x * sx, q.y * sy)).collect() }) .collect() } fn points(s: &mut Session, p: &Value) -> Result { let st = s.doc()?; let id = match id_param(p, "id", "shape.points")? { Some(i) => i, None => *st.selection.shapes.first().ok_or_else(|| bad("shape.points", "select a shape"))?, }; let sh = st.shape(id).ok_or_else(|| bad("shape.points", "no such shape"))?; let paths: Vec> = slide_vertices(&st.doc, &st.selection, sh).into_iter().map(|v| v.into_iter().map(|q| [q.x, q.y]).collect()).collect(); let types: Vec> = match &sh.geom { Geom::Custom { paths } => paths .iter() .map(|cp| { let segs = parse(&cp.d); vertices(&segs).iter().map(|si| point_kind(&segs, *si)).collect() }) .collect(), Geom::Preset { .. } => vec![], }; Ok(json!({"id": id.0, "paths": paths, "types": types})) } /// Runs `f` on the segments of path `path` of `id` (in shape-local points), then refits the box. /// `f` gets the segment indices of the vertices and returns whether it changed anything. pub(crate) fn edit_path( s: &mut Session, id: ShapeId, path: usize, cmd: &'static str, f: impl Fn(&mut Vec, &[usize], &Xfrm) -> Result, ) -> Result { prepare(s, id)?; s.edit(|doc, sel| { let list = crate::shapes_mut(doc, sel).ok_or_else(|| bad(cmd, "no slide"))?; let sh = deckcraft_model::find_shape_mut(list, id).ok_or_else(|| bad(cmd, "no such shape"))?; let x = sh.xfrm.ok_or_else(|| bad(cmd, "the shape has no position"))?; let Geom::Custom { paths } = &mut sh.geom else { return Err(bad(cmd, "not a freeform")) }; let cp = paths.get_mut(path).ok_or_else(|| bad(cmd, "no such path"))?; let mut segs = parse(&cp.d); let vs = vertices(&segs); if f(&mut segs, &vs, &x)? { cp.d = write(&segs); refit(sh); } Ok(()) })?; ok() } fn index_param(p: &Value, key: &str, cmd: &str) -> Result { p.get(key).and_then(Value::as_u64).map(|v| v as usize).ok_or_else(|| bad(cmd, format!("give `{key}`"))) } fn move_point(s: &mut Session, p: &Value) -> Result { let id = target(s, p, "shape.movePoint")?; let path = p.get("path").and_then(Value::as_u64).unwrap_or(0) as usize; let index = index_param(p, "index", "shape.movePoint")?; let to = match (f64_param(p, "x"), f64_param(p, "y")) { (Some(x), Some(y)) if x.is_finite() && y.is_finite() => Point::new(x.clamp(-1e5, 1e5), y.clamp(-1e5, 1e5)), _ => return Err(bad("shape.movePoint", "give `x` and `y` in slide points")), }; edit_path(s, id, path, "shape.movePoint", move |segs, vs, x| { let si = *vs.get(index).ok_or_else(|| bad("shape.movePoint", "no such point"))?; let local = x.affine().inverse() * to; let Some(cur) = segs.get(si).and_then(Seg::end) else { return Ok(false) }; move_vertex(segs, si, local - cur); Ok(true) }) } fn add_point(s: &mut Session, p: &Value) -> Result { let id = target(s, p, "shape.addPoint")?; let path = p.get("path").and_then(Value::as_u64).unwrap_or(0) as usize; let seg = index_param(p, "segment", "shape.addPoint")?; let t = f64_param(p, "t").filter(|t| t.is_finite()).unwrap_or(0.5).clamp(0.01, 0.99); edit_path(s, id, path, "shape.addPoint", move |segs, vs, _| { let si = *vs.get(seg).ok_or_else(|| bad("shape.addPoint", "no such segment"))?; // The closing edge of a closed outline ends at the start: split it, not the start. let si = if matches!(segs.get(si), Some(Seg::M(_))) { (si + 1..segs.len()).find(|j| closes_onto_start(segs, *j)).ok_or_else(|| bad("shape.addPoint", "the first point ends no segment"))? } else { si }; if split(segs, si, t) { Ok(true) } else { Err(bad("shape.addPoint", "no segment ends at that point")) } }) } fn delete_point(s: &mut Session, p: &Value) -> Result { let id = target(s, p, "shape.deletePoint")?; let path = p.get("path").and_then(Value::as_u64).unwrap_or(0) as usize; let index = index_param(p, "index", "shape.deletePoint")?; edit_path(s, id, path, "shape.deletePoint", move |segs, vs, _| { let si = *vs.get(index).ok_or_else(|| bad("shape.deletePoint", "no such point"))?; if remove_vertex(segs, si) { Ok(true) } else { Err(bad("shape.deletePoint", "an outline keeps at least two points")) } }) } /// The vertex of `sh` near slide point `p`: (path, index). pub fn vertex_at(doc: &deckcraft_model::Presentation, sel: &crate::Selection, sh: &Shape, p: Point, tol: f64) -> Option<(usize, usize)> { for (pi, vs) in slide_vertices(doc, sel, sh).iter().enumerate() { for (i, q) in vs.iter().enumerate() { if (*q - p).hypot() <= tol { return Some((pi, i)); } } } None } /// The segment of `sh`'s outline near slide point `p`: (path, index of the vertex ending it, t). pub fn segment_at(doc: &deckcraft_model::Presentation, sel: &crate::Selection, sh: &Shape, p: Point, tol: f64) -> Option<(usize, usize, f64)> { let Geom::Custom { paths } = &sh.geom else { return None }; let x = xfrm_of(doc, sel, sh); let local = x.affine().inverse() * p; let mut best: Option<(f64, usize, usize, f64)> = None; for (pi, cp) in paths.iter().enumerate() { let (sx, sy) = (if cp.w > 0.0 { x.w / cp.w } else { 1.0 }, if cp.h > 0.0 { x.h / cp.h } else { 1.0 }); let segs = parse(&cp.d); let vs = vertices(&segs); for si in 0..segs.len() { let Some(p0) = seg_start(&segs, si) else { continue }; let at = |t: f64| -> Option { let q = match segs.get(si)? { Seg::L(p1) => p0.lerp(*p1, t), Seg::Q(c, p1) => p0.lerp(*c, t).lerp(c.lerp(*p1, t), t), Seg::C(c1, c2, p1) => { let (a, b, c) = (p0.lerp(*c1, t), c1.lerp(*c2, t), c2.lerp(*p1, t)); a.lerp(b, t).lerp(b.lerp(c, t), t) } _ => return None, }; Some(Point::new(q.x * sx, q.y * sy)) }; // The vertex that ends this segment (a closing edge ends on the start). let end_vertex = if closes_onto_start(&segs, si) { let start = segs.get(..si).and_then(|s| s.iter().rposition(|x| matches!(x, Seg::M(_)))); start.and_then(|m| vs.iter().position(|v| *v == m)) } else { vs.iter().position(|v| *v == si) }; let Some(ev) = end_vertex else { continue }; for k in 1..32 { let t = k as f64 / 32.0; if let Some(q) = at(t) { let d = (q - local).hypot(); if d <= tol && best.is_none_or(|b| d < b.0) { best = Some((d, pi, ev, t)); } } } } } best.map(|(_, pi, ev, t)| (pi, ev, t)) } /// Moves vertex `index` of path `path` of `id` to slide point `to` (pointer drags). pub(crate) fn move_point_raw(s: &mut Session, id: ShapeId, path: usize, index: usize, to: Point) -> Result { move_point(s, &json!({"id": id.0, "path": path, "index": index, "x": to.x, "y": to.y})) } /// Where the control handles of the vertex ending segment `si` live: (segment, point index) for /// the incoming and the outgoing one, when those segments are cubics. fn handle_slots(segs: &[Seg], si: usize) -> (Option<(usize, usize)>, Option<(usize, usize)>) { let incoming = if matches!(segs.get(si), Some(Seg::M(_))) { (si + 1..segs.len()).take_while(|j| !matches!(segs.get(*j), Some(Seg::M(_)))).find(|j| closes_onto_start(segs, *j)) } else { Some(si) }; let inc = incoming.filter(|j| matches!(segs.get(*j), Some(Seg::C(..)))).map(|j| (j, 1)); let out = Some(si + 1).filter(|j| matches!(segs.get(*j), Some(Seg::C(..)))).map(|j| (j, 0)); (inc, out) } fn get_pt(segs: &[Seg], slot: (usize, usize)) -> Option { match segs.get(slot.0)? { Seg::C(a, b, c) => [*a, *b, *c].get(slot.1).copied(), _ => None, } } fn set_pt(segs: &mut [Seg], slot: (usize, usize), p: Point) { if let Some(s) = segs.get_mut(slot.0) && let Some(q) = s.points_mut().into_iter().nth(slot.1) { *q = p; } } /// Turns segment `j` into a cubic with the same shape. fn to_cubic(segs: &mut [Seg], j: usize) { let Some(p0) = seg_start(segs, j) else { return }; let c = match segs.get(j).copied() { Some(Seg::L(p1)) => Seg::C(p0.lerp(p1, 1.0 / 3.0), p0.lerp(p1, 2.0 / 3.0), p1), Some(Seg::Q(c, p1)) => Seg::C(p0.lerp(c, 2.0 / 3.0), p1.lerp(c, 2.0 / 3.0), p1), _ => return, }; if let Some(s) = segs.get_mut(j) { *s = c; } } /// Sets the point type of the vertex ending segment `si`. Its segments become curves so it has /// handles; smooth and straight points get their handles in line through the vertex. fn set_type(segs: &mut [Seg], si: usize, kind: &str) { let incoming = if matches!(segs.get(si), Some(Seg::M(_))) { (si + 1..segs.len()).take_while(|j| !matches!(segs.get(*j), Some(Seg::M(_)))).find(|j| closes_onto_start(segs, *j)) } else { Some(si) }; if let Some(j) = incoming { to_cubic(segs, j); } to_cubic(segs, si + 1); if kind == "corner" { return; } let (Some(v), (Some(hi), Some(ho))) = (segs.get(si).and_then(Seg::end), handle_slots(segs, si)) else { return }; let (Some(a), Some(b)) = (get_pt(segs, hi), get_pt(segs, ho)) else { return }; let (li, lo) = ((a - v).hypot(), (b - v).hypot()); let mut dir = b - a; if dir.hypot() < 1e-9 { // Both handles on the vertex: point them along the neighbours. let prev = seg_start(segs, hi.0).unwrap_or(v); let next = segs.get(ho.0).and_then(Seg::end).unwrap_or(v); dir = next - prev; } let len = dir.hypot(); if len < 1e-9 { return; } let u = dir / len; let (li, lo) = if kind == "smooth" { let m = if li + lo > 1e-9 { (li + lo) / 2.0 } else { len / 6.0 }; (m, m) } else { (li, lo) }; set_pt(segs, hi, v - u * li); set_pt(segs, ho, v + u * lo); } /// The type a vertex's handles show: smooth (in line, equal), straight (in line) or corner. pub fn point_kind(segs: &[Seg], si: usize) -> &'static str { let (Some(v), (Some(hi), Some(ho))) = (segs.get(si).and_then(Seg::end), handle_slots(segs, si)) else { return "corner" }; let (Some(a), Some(b)) = (get_pt(segs, hi), get_pt(segs, ho)) else { return "corner" }; let (da, db) = (a - v, b - v); let (la, lb) = (da.hypot(), db.hypot()); if la < 1e-6 || lb < 1e-6 { return "corner"; } let cos = da.dot(db) / (la * lb); if cos > -0.9998 { "corner" } else if (la - lb).abs() <= 0.01 * la.max(lb) { "smooth" } else { "straight" } } /// Moves handle `side` of the vertex ending segment `si` to `p`; the other handle follows as /// the point's type says. fn set_handle(segs: &mut [Seg], si: usize, out: bool, p: Point, kind: Option<&str>) -> bool { let kind = kind.unwrap_or_else(|| point_kind(segs, si)); let (hi, ho) = handle_slots(segs, si); let (mine, other) = if out { (ho, hi) } else { (hi, ho) }; let Some(mine) = mine else { return false }; let Some(v) = segs.get(si).and_then(Seg::end) else { return false }; set_pt(segs, mine, p); if let Some(o) = other && kind != "corner" && let Some(q) = get_pt(segs, o) { let d = p - v; let l = d.hypot(); if l > 1e-9 { let lo = if kind == "smooth" { l } else { (q - v).hypot() }; set_pt(segs, o, v - d / l * lo); } } true } /// Handles of vertex `index` of path `path` of `sh` in slide points: (incoming, outgoing, vertex). pub fn slide_handles( doc: &deckcraft_model::Presentation, sel: &crate::Selection, sh: &Shape, path: usize, index: usize, ) -> Option<(Option, Option, Point)> { let Geom::Custom { paths } = &sh.geom else { return None }; let cp = paths.get(path)?; let x = xfrm_of(doc, sel, sh); let (sx, sy) = (if cp.w > 0.0 { x.w / cp.w } else { 1.0 }, if cp.h > 0.0 { x.h / cp.h } else { 1.0 }); let a = x.affine(); let to = |q: Point| a * Point::new(q.x * sx, q.y * sy); let segs = parse(&cp.d); let si = *vertices(&segs).get(index)?; let v = segs.get(si).and_then(Seg::end)?; let (hi, ho) = handle_slots(&segs, si); Some((hi.and_then(|s| get_pt(&segs, s)).map(to), ho.and_then(|s| get_pt(&segs, s)).map(to), to(v))) } fn point_type(s: &mut Session, p: &Value) -> Result { let id = target(s, p, "shape.pointType")?; let path = p.get("path").and_then(Value::as_u64).unwrap_or(0) as usize; let index = index_param(p, "index", "shape.pointType")?; let kind = match str_param(p, "type") { Some(k @ ("smooth" | "straight" | "corner")) => k.to_string(), _ => return Err(bad("shape.pointType", "`type` is smooth, straight or corner")), }; // A straight point's handles can be as long as each other; remember the choice for drags. s.tool.vertex_kind = Some((path, index, kind.clone())); edit_path(s, id, path, "shape.pointType", move |segs, vs, _| { let si = *vs.get(index).ok_or_else(|| bad("shape.pointType", "no such point"))?; set_type(segs, si, &kind); Ok(true) }) } fn move_handle(s: &mut Session, p: &Value) -> Result { let id = target(s, p, "shape.moveHandle")?; let path = p.get("path").and_then(Value::as_u64).unwrap_or(0) as usize; let index = index_param(p, "index", "shape.moveHandle")?; let out = match str_param(p, "side") { Some("out") => true, Some("in") => false, _ => return Err(bad("shape.moveHandle", "`side` is in or out")), }; let to = match (f64_param(p, "x"), f64_param(p, "y")) { (Some(x), Some(y)) if x.is_finite() && y.is_finite() => Point::new(x.clamp(-1e5, 1e5), y.clamp(-1e5, 1e5)), _ => return Err(bad("shape.moveHandle", "give `x` and `y` in slide points")), }; let kind = str_param(p, "type").filter(|k| matches!(*k, "smooth" | "straight" | "corner")).map(str::to_string); edit_path(s, id, path, "shape.moveHandle", move |segs, vs, x| { let si = *vs.get(index).ok_or_else(|| bad("shape.moveHandle", "no such point"))?; if set_handle(segs, si, out, x.affine().inverse() * to, kind.as_deref()) { Ok(true) } else { Err(bad("shape.moveHandle", "that side of the point has no handle")) } }) } /// The type of vertex `index` of path `path` of `sh`. pub fn kind_of(sh: &Shape, path: usize, index: usize) -> Option<&'static str> { let Geom::Custom { paths } = &sh.geom else { return None }; let segs = parse(&paths.get(path)?.d); let si = *vertices(&segs).get(index)?; Some(point_kind(&segs, si)) } /// `shape.moveHandle` for pointer drags. pub(crate) fn move_handle_raw(s: &mut Session, p: &Value) -> Result { move_handle(s, p) }