| 1 | //! The Draw tab's tools through the page view, as a host drives them: every stroke and shape |
| 2 | //! is one edit adding one drawing, the eraser and the lasso's Delete remove drawings, a lasso |
| 3 | //! drag moves them, and each is one undo step. The ops seal into a section reading back as the |
| 4 | //! editor's page. `CANVAS_INK_EXPORT` names a directory receiving that section as a notebook, |
| 5 | //! for a cold reopen in OneNote 2010 (`corpus/ink-tools`). |
| 6 | #![cfg(feature = "interaction")] |
| 7 | |
| 8 | use canvas::{ |
| 9 | editor::{FAVORITES, Pen}, |
| 10 | gpu::page::PageScene, |
| 11 | interaction::{PageView, TextColors, ink::Tool}, |
| 12 | layout::TextEngine, |
| 13 | }; |
| 14 | use draw::edit::{Key, NamedKey}; |
| 15 | use onestore::{ |
| 16 | Arena, ExGuid, Section, Store, |
| 17 | op::{Edit, Op, PageOp}, |
| 18 | page::{Page, PageObject, ink::ShapeKind}, |
| 19 | }; |
| 20 | use std::time::{Duration, Instant}; |
| 21 | |
| 22 | const COLORS: TextColors = TextColors { |
| 23 | caret: [0.0, 0.0, 0.0, 1.0], |
| 24 | selection: [0.7, 0.8, 1.0, 1.0], |
| 25 | paper: canvas::gpu::Paper::WHITE, |
| 26 | }; |
| 27 | |
| 28 | struct Lab<'a> { |
| 29 | section: Section<'a>, |
| 30 | space: ExGuid, |
| 31 | view: PageView, |
| 32 | at: u64, |
| 33 | } |
| 34 | |
| 35 | impl Lab<'_> { |
| 36 | /// Stores what the last gesture recorded, returning its ops. |
| 37 | fn store(&mut self) -> Vec<PageOp> { |
| 38 | let ops = self.view.editor.take_ops().unwrap(); |
| 39 | self.at += 10_000_000; |
| 40 | let edit = Edit { |
| 41 | at: self.at, |
| 42 | ops: ops |
| 43 | .iter() |
| 44 | .cloned() |
| 45 | .map(|op| Op::Page { |
| 46 | space: self.space, |
| 47 | op, |
| 48 | }) |
| 49 | .collect(), |
| 50 | }; |
| 51 | self.section.apply("Author", &edit).unwrap(); |
| 52 | assert_eq!( |
| 53 | self.section.page(self.space).unwrap(), |
| 54 | self.view.editor.page().unwrap() |
| 55 | ); |
| 56 | ops |
| 57 | } |
| 58 | |
| 59 | /// A press at page point `from` dragged through `through`, in a view at 1 device pixel |
| 60 | /// per point. |
| 61 | fn drag(&mut self, path: &[[f32; 2]]) { |
| 62 | let view = &mut self.view; |
| 63 | let _ = view.pointer_moved(path[0]).unwrap(); |
| 64 | let _ = view.pointer_pressed(Instant::now()).unwrap(); |
| 65 | for point in &path[1..] { |
| 66 | let _ = view.pointer_moved(*point).unwrap(); |
| 67 | } |
| 68 | let _ = view.pointer_released().unwrap(); |
| 69 | } |
| 70 | |
| 71 | fn drawings(&self) -> Vec<onestore::page::Ink> { |
| 72 | self.view |
| 73 | .editor |
| 74 | .page() |
| 75 | .unwrap() |
| 76 | .objects |
| 77 | .into_iter() |
| 78 | .filter_map(|object| match object { |
| 79 | PageObject::Ink(ink) => Some(ink), |
| 80 | _ => None, |
| 81 | }) |
| 82 | .collect() |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | fn open(arena: &Arena, source: Vec<u8>) -> Lab<'_> { |
| 87 | let mut section = Section::open(arena, source).unwrap(); |
| 88 | let (space, ..) = section.pages().unwrap()[0].clone(); |
| 89 | let page: Page = section.page(space).unwrap(); |
| 90 | let mut engine = TextEngine::default(); |
| 91 | let (scene, editor) = PageScene::from_page(page, &mut engine).unwrap(); |
| 92 | let mut view = PageView::new( |
| 93 | editor, |
| 94 | engine, |
| 95 | Some((scene, [0.0; 2])), |
| 96 | [800, 600], |
| 97 | 1.0, |
| 98 | Duration::from_millis(500), |
| 99 | ); |
| 100 | view.viewport.scale = 1.0; |
| 101 | view.viewport.origin = [0.0; 2]; |
| 102 | Lab { |
| 103 | section, |
| 104 | space, |
| 105 | view, |
| 106 | at: 133_000_000_000_000_000, |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | fn line(from: [f32; 2], to: [f32; 2], steps: usize) -> Vec<[f32; 2]> { |
| 111 | (0..=steps) |
| 112 | .map(|step| { |
| 113 | let t = step as f32 / steps as f32; |
| 114 | [ |
| 115 | from[0] + (to[0] - from[0]) * t, |
| 116 | from[1] + (to[1] - from[1]) * t, |
| 117 | ] |
| 118 | }) |
| 119 | .collect() |
| 120 | } |
| 121 | |
| 122 | #[test] |
| 123 | fn drawing_tools_store_each_stroke_and_undo_it_in_one_step() { |
| 124 | let source = onestore::create_section("ink.one", "Drawn in Snowbound", "Author").unwrap(); |
| 125 | let arena = Arena::default(); |
| 126 | let mut lab = open(&arena, source.clone()); |
| 127 | let accent = Pen::new(35.0, Some(0x7a9a1f)); |
| 128 | |
| 129 | let _ = lab.view.set_tool(Tool::Pen(accent)); |
| 130 | let wave: Vec<[f32; 2]> = (0..=40) |
| 131 | .map(|step| { |
| 132 | let x = 60.0 + step as f32 * 4.0; |
| 133 | [x, 150.0 + 12.0 * (x / 18.0).sin()] |
| 134 | }) |
| 135 | .collect(); |
| 136 | lab.drag(&wave); |
| 137 | let ops = lab.store(); |
| 138 | let [ |
| 139 | PageOp::Add { |
| 140 | object: PageObject::Ink(ink), |
| 141 | .. |
| 142 | }, |
| 143 | ] = ops.as_slice() |
| 144 | else { |
| 145 | panic!("{ops:?}") |
| 146 | }; |
| 147 | assert_eq!((ink.strokes.len(), ink.strokes[0].points.len()), (1, 41)); |
| 148 | assert_eq!(ink.strokes[0].color, Some(0x7a9a1f)); |
| 149 | |
| 150 | let _ = lab.view.set_tool(Tool::Pen(FAVORITES[5])); |
| 151 | lab.drag(&line([60.0, 190.0], [220.0, 190.0], 20)); |
| 152 | lab.store(); |
| 153 | for (kind, from, to) in [ |
| 154 | (ShapeKind::Rectangle, [250.0, 140.0], [326.0, 196.0]), |
| 155 | (ShapeKind::Ellipse, [340.0, 140.0], [430.0, 196.0]), |
| 156 | (ShapeKind::Arrow, [250.0, 230.0], [358.0, 266.0]), |
| 157 | (ShapeKind::Line, [378.0, 230.0], [450.0, 232.0]), |
| 158 | ] { |
| 159 | let _ = lab |
| 160 | .view |
| 161 | .set_tool(Tool::Shape(kind, Pen::new(50.0, Some(0x7a9a1f)))); |
| 162 | lab.drag(&line(from, to, 8)); |
| 163 | lab.store(); |
| 164 | } |
| 165 | let drawn = lab.drawings(); |
| 166 | assert_eq!(drawn.len(), 6); |
| 167 | // Shapes take their corners from the grid, anchored at the margin origin. |
| 168 | let rectangle = &drawn[2]; |
| 169 | assert_eq!( |
| 170 | ( |
| 171 | rectangle.strokes[0].points[0], |
| 172 | rectangle.strokes[0].points[2] |
| 173 | ), |
| 174 | ( |
| 175 | [252.0, 140.4].map(onestore::page::ink::snap), |
| 176 | [324.0, 194.4].map(onestore::page::ink::snap) |
| 177 | ) |
| 178 | ); |
| 179 | assert_eq!( |
| 180 | drawn[4].strokes.len(), |
| 181 | 2, |
| 182 | "an arrow's head is a stroke of its own" |
| 183 | ); |
| 184 | let highlighter = drawn[1].strokes[0].clone(); |
| 185 | assert_eq!( |
| 186 | ( |
| 187 | highlighter.raster_operation, |
| 188 | highlighter.pen_tip, |
| 189 | highlighter.transparency |
| 190 | ), |
| 191 | (Some(9), Some(1), Some(127)) |
| 192 | ); |
| 193 | // A highlighter multiplies what lies beneath. |
| 194 | let primitives = lab.view.primitives(COLORS).unwrap(); |
| 195 | assert!( |
| 196 | primitives |
| 197 | .iter() |
| 198 | .any(|primitive| matches!(primitive, draw::Primitive::Highlight { .. })) |
| 199 | ); |
| 200 | if let Some(directory) = std::env::var_os("CANVAS_INK_EXPORT") { |
| 201 | let mut image = source.clone(); |
| 202 | let mut sealed = Section::open(&arena, source.clone()).unwrap(); |
| 203 | std::mem::swap(&mut sealed, &mut lab.section); |
| 204 | sealed.seal().unwrap().unwrap().apply(&mut image).unwrap(); |
| 205 | let directory = std::path::PathBuf::from(directory); |
| 206 | std::fs::create_dir_all(&directory).unwrap(); |
| 207 | std::fs::write(directory.join("ink.one"), &image).unwrap(); |
| 208 | let file_id = Store::parse(&image).unwrap().header.file_id; |
| 209 | std::fs::write( |
| 210 | directory.join("Open Notebook.onetoc2"), |
| 211 | onestore::create_table_of_contents("Open Notebook.onetoc2", &[("ink.one", file_id)]) |
| 212 | .unwrap(), |
| 213 | ) |
| 214 | .unwrap(); |
| 215 | return; |
| 216 | } |
| 217 | |
| 218 | // Undo takes the last drawing away, and only that. |
| 219 | let arrow = drawn[4].id; |
| 220 | let line_id = drawn[5].id; |
| 221 | let _ = lab.view.undo(false).unwrap(); |
| 222 | assert_eq!(lab.store(), [PageOp::Delete { object: line_id }]); |
| 223 | let _ = lab.view.undo(true).unwrap(); |
| 224 | lab.store(); |
| 225 | assert_eq!(lab.drawings().len(), 6); |
| 226 | |
| 227 | // The eraser takes the wave it crosses, and the arrow's two strokes with it at a touch. |
| 228 | let _ = lab.view.set_tool(Tool::Eraser); |
| 229 | lab.drag(&[ |
| 230 | [100.0, 120.0], |
| 231 | [100.0, 160.0], |
| 232 | [240.0, 160.0], |
| 233 | [240.0, 215.0], |
| 234 | [320.0, 250.0], |
| 235 | ]); |
| 236 | let ops = lab.store(); |
| 237 | assert_eq!( |
| 238 | ops, |
| 239 | [ |
| 240 | PageOp::Delete { |
| 241 | object: drawn[0].id |
| 242 | }, |
| 243 | PageOp::Delete { object: arrow }, |
| 244 | ] |
| 245 | ); |
| 246 | let _ = lab.view.undo(false).unwrap(); |
| 247 | lab.store(); |
| 248 | assert_eq!(lab.drawings().len(), 6); |
| 249 | |
| 250 | // The lasso picks the highlighter, a drag moves it, Delete removes it. |
| 251 | let _ = lab.view.set_tool(Tool::Lasso); |
| 252 | lab.drag(&[ |
| 253 | [50.0, 175.0], |
| 254 | [230.0, 175.0], |
| 255 | [230.0, 205.0], |
| 256 | [50.0, 205.0], |
| 257 | [50.0, 176.0], |
| 258 | ]); |
| 259 | assert_eq!(lab.view.ink_selection(), [drawn[1].id]); |
| 260 | lab.drag(&line([140.0, 190.0], [170.0, 220.0], 4)); |
| 261 | let ops = lab.store(); |
| 262 | assert!(matches!( |
| 263 | ops.as_slice(), |
| 264 | [PageOp::Outline { object, .. }] if *object == drawn[1].id |
| 265 | )); |
| 266 | let moved = &lab.drawings()[1]; |
| 267 | assert_eq!((moved.layout.x, moved.layout.y), (Some(30.0), Some(30.0))); |
| 268 | let _ = lab.view.key(&Key::Named(NamedKey::Delete), None).unwrap(); |
| 269 | assert_eq!( |
| 270 | lab.store(), |
| 271 | [PageOp::Delete { |
| 272 | object: drawn[1].id |
| 273 | }] |
| 274 | ); |
| 275 | let _ = lab.view.undo(false).unwrap(); |
| 276 | lab.store(); |
| 277 | let _ = lab.view.undo(false).unwrap(); |
| 278 | lab.store(); |
| 279 | assert_eq!(lab.drawings()[1].layout.x, Some(0.0)); |
| 280 | |
| 281 | // Escape leaves the drawing tools for Select & Type, where a click picks a drawing. |
| 282 | let _ = lab.view.key(&Key::Named(NamedKey::Escape), None).unwrap(); |
| 283 | assert_eq!(lab.view.tool(), Tool::Select); |
| 284 | lab.drag(&[[400.0, 231.0]]); |
| 285 | assert!(lab.view.editor.take_ops().unwrap().is_empty()); |
| 286 | assert_eq!(lab.view.ink_selection(), [drawn[5].id]); |
| 287 | } |
| 288 | |
| 289 | /// A pen reporting pressure stores it at each point, following the pen between reports; a |
| 290 | /// mouse's stroke after it keeps the pen's width, as OneNote stores one (`corpus/ink-pressure`). |
| 291 | #[test] |
| 292 | fn a_pressure_pen_stores_its_pressure_and_a_mouse_its_width() { |
| 293 | let source = onestore::create_section("ink.one", "Pressure", "Author").unwrap(); |
| 294 | let arena = Arena::default(); |
| 295 | let mut lab = open(&arena, source); |
| 296 | let _ = lab.view.set_tool(Tool::Pen(FAVORITES[0])); |
| 297 | let path = line([60.0, 150.0], [220.0, 150.0], 4); |
| 298 | let view = &mut lab.view; |
| 299 | view.set_pressure(Some(0.2)); |
| 300 | let _ = view.pointer_moved(path[0]).unwrap(); |
| 301 | let _ = view.pointer_pressed(Instant::now()).unwrap(); |
| 302 | for (point, pressure) in path[1..] |
| 303 | .iter() |
| 304 | .zip([Some(0.4), None, Some(1.0), Some(0.9)]) |
| 305 | { |
| 306 | if pressure.is_some() { |
| 307 | view.set_pressure(pressure); |
| 308 | } |
| 309 | let _ = view.pointer_moved(*point).unwrap(); |
| 310 | } |
| 311 | let _ = view.pointer_released().unwrap(); |
| 312 | lab.store(); |
| 313 | lab.view.set_pressure(None); |
| 314 | lab.drag(&line([60.0, 200.0], [220.0, 200.0], 4)); |
| 315 | lab.store(); |
| 316 | let drawn = lab.drawings(); |
| 317 | let level = onestore::page::ink::level; |
| 318 | assert_eq!( |
| 319 | drawn[0].strokes[0].pressure, |
| 320 | [0.2, 0.4, 0.4, 1.0, 0.9].map(level) |
| 321 | ); |
| 322 | assert!(drawn[1].strokes[0].pressure.is_empty()); |
| 323 | } |