mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-09 11:59:36 -06:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
913f5046ea | ||
|
|
84e2d97587 | ||
|
|
30c9cf4c49 | ||
|
|
ebc171be57 |
@@ -5023,6 +5023,38 @@ fn pen_pressure_is_kept_and_a_mouse_draws_at_full_width() {
|
|||||||
assert_eq!(got, vec![vec![0.2, 1.0, 0.4], vec![1.0, 1.0]]);
|
assert_eq!(got, vec![vec![0.2, 1.0, 0.4], vec![1.0, 1.0]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_long_ink_stroke_is_extended_in_place_and_capped() {
|
||||||
|
let mut s = session();
|
||||||
|
s.set_tool(tools::ToolKind::Ink { mode: "pen".into(), color: deckcraft_model::Rgba::rgb(20, 20, 20), width: 4.0 });
|
||||||
|
let ev = |kind, x: f64| tools::PointerEvent { kind, x, y: 80.0, mods: Mods::default(), tol: 3.0, pressure: Some(0.5) };
|
||||||
|
s.pointer(ev(PointerKind::Down, 0.0)).unwrap();
|
||||||
|
let n = tools::MAX_GESTURE_POINTS + 5_000;
|
||||||
|
for i in 1..n {
|
||||||
|
s.pointer(ev(PointerKind::Drag, (i % 500) as f64)).unwrap();
|
||||||
|
}
|
||||||
|
assert_eq!(s.tool.ink_preview.len(), tools::MAX_GESTURE_POINTS);
|
||||||
|
s.pointer(ev(PointerKind::Up, 0.0)).unwrap();
|
||||||
|
let kept: usize = ink_pressures(&s).iter().map(Vec::len).sum();
|
||||||
|
assert!((2..=tools::MAX_GESTURE_POINTS).contains(&kept), "{kept}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn double_clicking_a_capped_freeform_keeps_its_vertices() {
|
||||||
|
let mut s = session();
|
||||||
|
let ev = |kind, x: f64, y: f64| tools::PointerEvent { kind, x, y, mods: Default::default(), tol: 3.0, pressure: None };
|
||||||
|
s.set_tool(tools::ToolKind::Shape { preset: "freeform".into() });
|
||||||
|
s.pointer(ev(tools::PointerKind::Down, 100.0, 100.0)).unwrap();
|
||||||
|
// Densely clicked vertices, each within the double-click tolerance of the last.
|
||||||
|
for i in 0..tools::MAX_GESTURE_POINTS + 10 {
|
||||||
|
s.pointer(ev(tools::PointerKind::Down, 300.0 + i as f64 * 0.001, 100.0)).unwrap();
|
||||||
|
}
|
||||||
|
s.pointer(ev(tools::PointerKind::DoubleClick, 300.0, 100.0)).unwrap();
|
||||||
|
let st = s.doc().unwrap();
|
||||||
|
let deckcraft_model::Geom::Custom { paths } = &st.shapes().last().unwrap().geom else { panic!("no freeform") };
|
||||||
|
assert_eq!(paths[0].d.matches(" L ").count(), tools::MAX_GESTURE_POINTS - 1);
|
||||||
|
}
|
||||||
|
|
||||||
fn ink_pressures(s: &Session) -> Vec<Vec<f32>> {
|
fn ink_pressures(s: &Session) -> Vec<Vec<f32>> {
|
||||||
let st = s.doc().unwrap();
|
let st = s.doc().unwrap();
|
||||||
let Some(sh) = st.shapes().iter().rev().find(|sh| matches!(sh.kind, deckcraft_model::ShapeKind::Ink { .. })) else {
|
let Some(sh) = st.shapes().iter().rev().find(|sh| matches!(sh.kind, deckcraft_model::ShapeKind::Ink { .. })) else {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ mod geometry;
|
|||||||
mod hit;
|
mod hit;
|
||||||
mod pointer;
|
mod pointer;
|
||||||
|
|
||||||
|
pub(crate) use drag::MAX_GESTURE_POINTS;
|
||||||
pub use hit::{line_xfrm, resize_box, table_rows, text_pos};
|
pub use hit::{line_xfrm, resize_box, table_rows, text_pos};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@@ -275,6 +276,8 @@ pub struct ToolState {
|
|||||||
pub path_preview: Vec<Point>,
|
pub path_preview: Vec<Point>,
|
||||||
pub path_smooth: bool,
|
pub path_smooth: bool,
|
||||||
pub path_closing: bool,
|
pub path_closing: bool,
|
||||||
|
/// Whether the last freeform press added a vertex (it adds none at the point cap).
|
||||||
|
pub press_added_vertex: bool,
|
||||||
/// The shape in Edit Points: its vertices are handles instead of the box's.
|
/// The shape in Edit Points: its vertices are handles instead of the box's.
|
||||||
pub edit_points: Option<ShapeId>,
|
pub edit_points: Option<ShapeId>,
|
||||||
/// The vertex (path, index) whose control handles show in Edit Points.
|
/// The vertex (path, index) whose control handles show in Edit Points.
|
||||||
|
|||||||
@@ -4,8 +4,12 @@ use super::{Drag, Hit, Point, Result, Session, ShapeId, ShapeKind, TextSel, Tool
|
|||||||
impl Session {
|
impl Session {
|
||||||
pub(super) fn double_click(&mut self, p: Point, tol: f64) -> Result<Value> {
|
pub(super) fn double_click(&mut self, p: Point, tol: f64) -> Result<Value> {
|
||||||
if let Some(Drag::Freeform { mode, mut points, .. }) = self.tool.drag.clone() {
|
if let Some(Drag::Freeform { mode, mut points, .. }) = self.tool.drag.clone() {
|
||||||
// The double-click's own click added a duplicate vertex.
|
// The double-click's own click added a duplicate vertex (none at the point cap, where
|
||||||
while points.len() > 2 && points.last().zip(points.get(points.len() - 2)).is_some_and(|(a, b)| (*a - *b).hypot() <= tol) {
|
// the existing vertices are all kept).
|
||||||
|
while self.tool.press_added_vertex
|
||||||
|
&& points.len() > 2
|
||||||
|
&& points.last().zip(points.get(points.len() - 2)).is_some_and(|(a, b)| (*a - *b).hypot() <= tol)
|
||||||
|
{
|
||||||
points.pop();
|
points.pop();
|
||||||
}
|
}
|
||||||
self.tool.drag = Some(Drag::Freeform { mode, points, pressed: false });
|
self.tool.drag = Some(Drag::Freeform { mode, points, pressed: false });
|
||||||
|
|||||||
@@ -12,9 +12,16 @@ struct MoveDrag {
|
|||||||
enter_text: Option<(ShapeId, Point)>,
|
enter_text: Option<(ShapeId, Point)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Samples one ink or freeform gesture keeps; later pointer moves extend it no further.
|
||||||
|
pub(crate) const MAX_GESTURE_POINTS: usize = 20_000;
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
/// Update the active drag interaction using the pointer position and modifiers.
|
/// Update the active drag interaction using the pointer position and modifiers.
|
||||||
pub(super) fn drag(&mut self, p: Point, mods: Mods, tol: f64) -> Result<Value> {
|
pub(super) fn drag(&mut self, p: Point, mods: Mods, tol: f64) -> Result<Value> {
|
||||||
|
if matches!(self.tool.drag, Some(Drag::Ink { .. })) {
|
||||||
|
self.extend_ink(p);
|
||||||
|
return Ok(Value::Null);
|
||||||
|
}
|
||||||
let Some(drag) = self.tool.drag.clone() else { return Ok(Value::Null) };
|
let Some(drag) = self.tool.drag.clone() else { return Ok(Value::Null) };
|
||||||
match drag {
|
match drag {
|
||||||
Drag::Guide { index } => {
|
Drag::Guide { index } => {
|
||||||
@@ -181,7 +188,7 @@ impl Session {
|
|||||||
}
|
}
|
||||||
Drag::Freeform { mode, mut points, pressed } => {
|
Drag::Freeform { mode, mut points, pressed } => {
|
||||||
// Dragging scribbles (except for curves, which only take clicked points).
|
// 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) {
|
if pressed && mode != "curve" && points.len() < MAX_GESTURE_POINTS && points.last().is_none_or(|l| (*l - p).hypot() >= 1.5) {
|
||||||
points.push(p);
|
points.push(p);
|
||||||
}
|
}
|
||||||
let mut v = points.clone();
|
let mut v = points.clone();
|
||||||
@@ -191,15 +198,24 @@ impl Session {
|
|||||||
self.tool.path_preview = v;
|
self.tool.path_preview = v;
|
||||||
self.tool.drag = Some(Drag::Freeform { mode, points, pressed });
|
self.tool.drag = Some(Drag::Freeform { mode, points, pressed });
|
||||||
}
|
}
|
||||||
Drag::Ink { mut points } => {
|
// Handled above.
|
||||||
points.push(self.ink_sample(p));
|
Drag::Ink { .. } => {}
|
||||||
self.tool.ink_preview = points.clone();
|
|
||||||
self.tool.drag = Some(Drag::Ink { points });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(Value::Null)
|
Ok(Value::Null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ink grows by one sample per move: extend it (and its preview) in place rather than
|
||||||
|
/// copying the whole stroke twice per event, up to [`MAX_GESTURE_POINTS`].
|
||||||
|
fn extend_ink(&mut self, p: Point) {
|
||||||
|
let sample = self.ink_sample(p);
|
||||||
|
if let Some(Drag::Ink { points }) = self.tool.drag.as_mut()
|
||||||
|
&& points.len() < MAX_GESTURE_POINTS
|
||||||
|
{
|
||||||
|
points.push(sample);
|
||||||
|
self.tool.ink_preview.push(sample);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn drag_move(&mut self, drag: MoveDrag, p: Point, mods: Mods, tol: f64) -> Result<Value> {
|
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 MoveDrag { ids, start, origs, moved, duplicate, enter_text } = drag;
|
||||||
let mut d = p - start;
|
let mut d = p - start;
|
||||||
|
|||||||
@@ -228,7 +228,10 @@ impl Session {
|
|||||||
self.tool.drag = Some(Drag::Freeform { mode, points, pressed: false });
|
self.tool.drag = Some(Drag::Freeform { mode, points, pressed: false });
|
||||||
return self.finish_freeform(true);
|
return self.finish_freeform(true);
|
||||||
}
|
}
|
||||||
points.push(p);
|
self.tool.press_added_vertex = points.len() < super::MAX_GESTURE_POINTS;
|
||||||
|
if self.tool.press_added_vertex {
|
||||||
|
points.push(p);
|
||||||
|
}
|
||||||
self.tool.path_preview = points.clone();
|
self.tool.path_preview = points.clone();
|
||||||
self.tool.drag = Some(Drag::Freeform { mode, points, pressed: true });
|
self.tool.drag = Some(Drag::Freeform { mode, points, pressed: true });
|
||||||
return Ok(Value::Null);
|
return Ok(Value::Null);
|
||||||
|
|||||||
Reference in New Issue
Block a user