1//! Drawing on the page as OneNote 2010's Draw tab does (`corpus/ink-tools`): each stroke and
2//! shape is a drawing of its own, the stroke eraser takes whole strokes, and the lasso picks
3//! drawings to move or delete.
4
5use super::{CanvasEditor, EditError, EditorError, History, Placement, page};
6use onestore::{
7 ExGuid,
8 op::PageOp,
9 page::{
10 Ink, InkStroke, PageObject,
11 ink::{level, snap},
12 text::new_id,
13 },
14};
15
16/// A pen or highlighter, as OneNote's pen gallery and Color & Thickness choose one.
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct Pen {
19 /// Tip width in HIMETRIC (hundredths of a millimetre); a highlighter's tip is as tall as `HIGHLIGHTER_HEIGHT`.
20 pub width: f32,
21 /// COLORREF; `None` draws in the paper's ink, as OneNote's black pens store it.
22 pub color: Option<u32>,
23 pub highlighter: bool,
24}
25
26/// A highlighter tip's height in HIMETRIC.
27const HIGHLIGHTER_HEIGHT: f32 = 400.0;
28
29impl Pen {
30 pub const fn new(width: f32, color: Option<u32>) -> Self {
31 Self {
32 width,
33 color,
34 highlighter: false,
35 }
36 }
37
38 pub(crate) const fn highlighter(color: u32) -> Self {
39 Self {
40 width: 70.0,
41 color: Some(color),
42 highlighter: true,
43 }
44 }
45
46 /// The pen shapes draw with: a little thicker, as OneNote's are.
47 pub const fn shape(self) -> Self {
48 Self {
49 width: 50.0,
50 ..self
51 }
52 }
53
54 /// A stroke of this pen through `points`, snapped to the HIMETRIC grid stored, its width
55 /// following `pressure` at each point unless that is empty, as a mouse's is.
56 pub fn stroke(&self, points: &[[f32; 2]], pressure: &[f32]) -> Result<InkStroke, EditError> {
57 let points_of = |himetric: f32| snap(himetric * 72.0 / 2540.0);
58 Ok(InkStroke {
59 id: new_id()?,
60 points: points.iter().map(|point| point.map(snap)).collect(),
61 width: points_of(self.width),
62 height: points_of(if self.highlighter {
63 HIGHLIGHTER_HEIGHT
64 } else {
65 self.width
66 }),
67 color: self.color,
68 transparency: self.highlighter.then_some(127),
69 pen_tip: self.highlighter.then_some(1),
70 raster_operation: self.highlighter.then_some(9),
71 pressure: pressure.iter().copied().map(level).collect(),
72 })
73 }
74}
75
76/// OneNote 2010's favourite pens, in its gallery's order.
77pub const FAVORITES: [Pen; 14] = {
78 const RED: u32 = 0x241ced;
79 const BLUE: u32 = 0xbb6531;
80 const GREEN: u32 = 0x367d17;
81 const GREY: u32 = 0x808080;
82 [
83 Pen::new(35.0, None),
84 Pen::new(35.0, Some(RED)),
85 Pen::new(35.0, Some(BLUE)),
86 Pen::new(35.0, Some(GREEN)),
87 Pen::new(35.0, Some(GREY)),
88 Pen::highlighter(0x00ffff),
89 Pen::highlighter(0xffff00),
90 Pen::new(50.0, None),
91 Pen::new(50.0, Some(RED)),
92 Pen::new(50.0, Some(BLUE)),
93 Pen::new(50.0, Some(GREEN)),
94 Pen::new(50.0, Some(GREY)),
95 Pen::highlighter(0x00ff00),
96 Pen::highlighter(0xff00ff),
97 ]
98};
99
100impl CanvasEditor {
101 /// Puts `ink`, a stroke or shape just drawn, on top of the page as one undo step.
102 pub fn draw(&mut self, ink: Ink) -> Result<(), EditorError> {
103 if ink.strokes.is_empty() {
104 return Err(EditError::InvalidRange.into());
105 }
106 self.finish_composition();
107 let index = self.objects.len();
108 self.add_ink(index, ink);
109 self.undo.push(History::Ink { index, ink: None });
110 self.redo.clear();
111 Ok(())
112 }
113
114 /// Erases what the eraser touches moving from `from` to `to`, `reach` page points either
115 /// side of its path: a whole drawing where it is a shape, a single stroke or a group, as
116 /// OneNote erases a shape at a touch, and otherwise the strokes touched. With `join` the
117 /// erasing joins the last undo step, which the same sweep of the eraser made. Returns
118 /// whether anything was erased.
119 pub fn erase(
120 &mut self,
121 from: [f32; 2],
122 to: [f32; 2],
123 reach: f32,
124 join: bool,
125 ) -> Result<bool, EditorError> {
126 let mut entries = Vec::new();
127 let mut index = self.objects.len();
128 while index > 0 {
129 index -= 1;
130 let page::Content::Ink(ink) = &self.objects[index] else {
131 continue;
132 };
133 let offset = crate::origin(&ink.layout);
134 let touched = |stroke: &InkStroke| stroke_near(stroke, offset, [from, to], reach);
135 let (kept, erased): (Vec<InkStroke>, Vec<InkStroke>) = ink
136 .strokes
137 .iter()
138 .cloned()
139 .partition(|stroke| !touched(stroke));
140 let grouped = ink
141 .groups
142 .iter()
143 .any(|group| group_touched(group, &touched));
144 if erased.is_empty() && !grouped {
145 continue;
146 }
147 if ink.shape.is_some() || !ink.groups.is_empty() || kept.is_empty() {
148 let removed = self.remove_ink(index);
149 entries.push(History::Ink {
150 index,
151 ink: Some(Box::new(removed)),
152 });
153 } else {
154 let id = ink.id;
155 entries.push(self.set_strokes(id, kept));
156 }
157 }
158 if entries.is_empty() {
159 return Ok(false);
160 }
161 self.finish_composition();
162 if join
163 && let Some(History::Group {
164 entries: earlier, ..
165 }) = self.undo.last_mut()
166 {
167 earlier.extend(entries);
168 } else {
169 self.undo.push(History::Group {
170 entries,
171 page: false,
172 });
173 }
174 self.redo.clear();
175 Ok(true)
176 }
177
178 /// How many ops wait for `take_ops`.
179 pub(crate) fn pending_ops(&self) -> usize {
180 self.ops.as_ref().map_or(0, Vec::len)
181 }
182
183 /// Takes back an eraser's sweep as though it never happened: its undo step, and the ops
184 /// recorded since there were `mark`, none of which were taken since.
185 pub(crate) fn retract_erasing(
186 &mut self,
187 engine: &mut crate::layout::TextEngine,
188 mark: usize,
189 ) -> Result<(), EditorError> {
190 if let Some(step) = self.undo.pop() {
191 self.apply_history(engine, step)
192 .map_err(|(_, error)| error)?;
193 }
194 if let Ok(ops) = &mut self.ops {
195 ops.truncate(mark);
196 }
197 Ok(())
198 }
199
200 /// The page's drawings, on top first, with most of their points inside `lasso`, a
201 /// closed path in page points.
202 pub(crate) fn ink_within(&self, lasso: &[[f32; 2]]) -> Vec<ExGuid> {
203 self.objects
204 .iter()
205 .rev()
206 .filter_map(|object| match object {
207 page::Content::Ink(ink) => {
208 let offset = crate::origin(&ink.layout);
209 let points = points(ink);
210 let inside = points
211 .iter()
212 .filter(|p| contains(lasso, [p[0] + offset[0], p[1] + offset[1]]))
213 .count();
214 (!points.is_empty() && inside * 2 > points.len()).then_some(ink.id)
215 }
216 _ => None,
217 })
218 .collect()
219 }
220
221 /// The drawing on top whose strokes pass within `reach` of `point`.
222 pub(crate) fn ink_at(&self, point: [f32; 2], reach: f32) -> Option<ExGuid> {
223 self.objects.iter().rev().find_map(|object| match object {
224 page::Content::Ink(ink) => {
225 let offset = crate::origin(&ink.layout);
226 let near = |stroke: &InkStroke| stroke_near(stroke, offset, [point, point], reach);
227 (ink.strokes.iter().any(near) || ink.groups.iter().any(|g| group_touched(g, &near)))
228 .then_some(ink.id)
229 }
230 _ => None,
231 })
232 }
233
234 /// The painted extent of drawings `ids`, as `[x0, y0, x1, y1]` page points, while dragged
235 /// where they show.
236 pub fn ink_extent(&self, ids: &[ExGuid]) -> Option<[f32; 4]> {
237 self.objects
238 .iter()
239 .filter_map(|object| match object {
240 page::Content::Ink(ink) if ids.contains(&ink.id) => page::ink_bounds(ink)
241 .map(|bounds| crate::translated(bounds, self.ink_drag_offset(ink.id))),
242 _ => None,
243 })
244 .reduce(|a, b| {
245 [
246 a[0].min(b[0]),
247 a[1].min(b[1]),
248 a[2].max(b[2]),
249 a[3].max(b[3]),
250 ]
251 })
252 }
253
254 /// Shows drawings dragged `delta` from where they lie, without storing it, until the
255 /// drag ends with `move_ink` or `None`.
256 pub(crate) fn drag_ink(&mut self, drag: Option<(Vec<ExGuid>, [f32; 2])>) {
257 self.ink_drag = drag;
258 }
259
260 /// How far a drawing shows from where it lies, while dragged.
261 pub(crate) fn ink_drag_offset(&self, id: ExGuid) -> [f32; 2] {
262 match &self.ink_drag {
263 Some((ids, delta)) if ids.contains(&id) => *delta,
264 _ => [0.0; 2],
265 }
266 }
267
268 /// Moves drawings `ids` by `delta` as one undo step, as OneNote moves ink: by the
269 /// drawing's offset, its strokes as they were.
270 pub fn move_ink(&mut self, ids: &[ExGuid], delta: [f32; 2]) -> Result<(), EditorError> {
271 self.ink_drag = None;
272 if delta == [0.0; 2] {
273 return Ok(());
274 }
275 let mut entries = Vec::new();
276 for &id in ids {
277 let Some(layout) = self.ink_layout_mut(id) else {
278 continue;
279 };
280 let stored = [layout.x, layout.y];
281 // Undoing restores a position: a drawing never moved lies at no offset.
282 let position = stored.map(|v| Some(v.unwrap_or(0.0)));
283 [layout.x, layout.y] = [0, 1].map(|axis| Some(position[axis].unwrap() + delta[axis]));
284 self.record(self.placement_ops(&Placement {
285 id,
286 position: stored,
287 }));
288 entries.push(History::Position {
289 object: id,
290 position,
291 });
292 }
293 self.push_ink_step(entries);
294 Ok(())
295 }
296
297 /// Deletes drawings `ids` as one undo step.
298 pub fn delete_ink(&mut self, ids: &[ExGuid]) -> Result<(), EditorError> {
299 let mut entries = Vec::new();
300 let mut index = self.objects.len();
301 while index > 0 {
302 index -= 1;
303 if matches!(&self.objects[index], page::Content::Ink(ink) if ids.contains(&ink.id)) {
304 let ink = self.remove_ink(index);
305 entries.push(History::Ink {
306 index,
307 ink: Some(Box::new(ink)),
308 });
309 }
310 }
311 self.push_ink_step(entries);
312 Ok(())
313 }
314
315 fn push_ink_step(&mut self, entries: Vec<History>) {
316 if entries.is_empty() {
317 return;
318 }
319 self.finish_composition();
320 self.undo.push(History::Group {
321 entries,
322 page: false,
323 });
324 self.redo.clear();
325 }
326
327 fn ink_layout_mut(&mut self, id: ExGuid) -> Option<&mut onestore::document::Layout> {
328 self.objects.iter_mut().find_map(|object| match object {
329 page::Content::Ink(ink) if ink.id == id => Some(&mut ink.layout),
330 _ => None,
331 })
332 }
333
334 /// Puts a drawing on the page at `index` in paint order, recording its op.
335 pub(super) fn add_ink(&mut self, index: usize, ink: Ink) {
336 let object = PageObject::Ink(ink.clone());
337 self.objects.insert(index, page::Content::Ink(ink));
338 let before = self.successor(object.id());
339 self.record(Ok(vec![PageOp::Add { object, before }]));
340 }
341
342 /// Takes the drawing at `index` in paint order off the page, recording its op.
343 pub(super) fn remove_ink(&mut self, index: usize) -> Ink {
344 let page::Content::Ink(ink) = self.objects.remove(index) else {
345 unreachable!("the index holds a drawing")
346 };
347 self.record(Ok(vec![PageOp::Delete { object: ink.id }]));
348 ink
349 }
350
351 /// Gives drawing `id` `strokes`, recording the op that erases the strokes it loses and
352 /// adds those it gains; the entry restoring what it had.
353 pub(super) fn set_strokes(&mut self, id: ExGuid, strokes: Vec<InkStroke>) -> History {
354 let ink = self
355 .objects
356 .iter_mut()
357 .find_map(|object| match object {
358 page::Content::Ink(ink) if ink.id == id => Some(ink),
359 _ => None,
360 })
361 .expect("the drawing is on the page");
362 let known = |stroke: &InkStroke, list: &[InkStroke]| list.iter().any(|s| s.id == stroke.id);
363 let remove = ink
364 .strokes
365 .iter()
366 .filter(|stroke| !known(stroke, &strokes))
367 .map(|stroke| stroke.id)
368 .collect();
369 let (mut kept, add): (Vec<InkStroke>, Vec<InkStroke>) = strokes
370 .into_iter()
371 .partition(|stroke| known(stroke, &ink.strokes));
372 // Strokes a drawing gains follow those it keeps, as the stored list orders them.
373 kept.extend(add.iter().cloned());
374 let previous = std::mem::replace(&mut ink.strokes, kept);
375 self.record(Ok(vec![PageOp::Strokes {
376 ink: id,
377 add,
378 remove,
379 }]));
380 History::Strokes {
381 ink: id,
382 strokes: previous,
383 }
384 }
385
386 pub(super) fn has_ink(&self, id: ExGuid) -> bool {
387 self.objects
388 .iter()
389 .any(|object| matches!(object, page::Content::Ink(ink) if ink.id == id))
390 }
391}
392
393/// Whether `stroke`, of a drawing at `offset`, passes within `reach` of segment `path`, its pen's
394/// width included.
395fn stroke_near(stroke: &InkStroke, offset: [f32; 2], path: [[f32; 2]; 2], reach: f32) -> bool {
396 let reach = reach + stroke.width.max(stroke.height) / 2.0;
397 let mut points = stroke
398 .points
399 .iter()
400 .map(|p| [p[0] + offset[0], p[1] + offset[1]]);
401 let Some(mut previous) = points.next() else {
402 return false;
403 };
404 segments_distance([previous, previous], path) <= reach
405 || points.any(|point| {
406 let near = segments_distance([previous, point], path) <= reach;
407 previous = point;
408 near
409 })
410}
411
412fn group_touched(ink: &Ink, touched: &impl Fn(&InkStroke) -> bool) -> bool {
413 ink.strokes.iter().any(touched) || ink.groups.iter().any(|group| group_touched(group, touched))
414}
415
416fn points(ink: &Ink) -> Vec<[f32; 2]> {
417 ink.strokes
418 .iter()
419 .flat_map(|stroke| stroke.points.iter().copied())
420 .chain(ink.groups.iter().flat_map(points))
421 .collect()
422}
423
424/// Whether `point` lies inside the closed path `polygon` (even-odd).
425fn contains(polygon: &[[f32; 2]], point: [f32; 2]) -> bool {
426 let mut inside = false;
427 let mut previous = match polygon.last() {
428 Some(last) => *last,
429 None => return false,
430 };
431 for &current in polygon {
432 if (current[1] > point[1]) != (previous[1] > point[1])
433 && point[0]
434 < (previous[0] - current[0]) * (point[1] - current[1]) / (previous[1] - current[1])
435 + current[0]
436 {
437 inside = !inside;
438 }
439 previous = current;
440 }
441 inside
442}
443
444/// The least distance between segments `a` and `b`.
445fn segments_distance(a: [[f32; 2]; 2], b: [[f32; 2]; 2]) -> f32 {
446 let cross = |o: [f32; 2], p: [f32; 2], q: [f32; 2]| {
447 (p[0] - o[0]) * (q[1] - o[1]) - (p[1] - o[1]) * (q[0] - o[0])
448 };
449 let [d1, d2] = [cross(b[0], b[1], a[0]), cross(b[0], b[1], a[1])];
450 let [d3, d4] = [cross(a[0], a[1], b[0]), cross(a[0], a[1], b[1])];
451 if d1 * d2 < 0.0 && d3 * d4 < 0.0 {
452 return 0.0;
453 }
454 let to_segment = |p: [f32; 2], [s, e]: [[f32; 2]; 2]| {
455 let [dx, dy] = [e[0] - s[0], e[1] - s[1]];
456 let length = dx * dx + dy * dy;
457 let t = if length > 0.0 {
458 (((p[0] - s[0]) * dx + (p[1] - s[1]) * dy) / length).clamp(0.0, 1.0)
459 } else {
460 0.0
461 };
462 (p[0] - s[0] - t * dx).hypot(p[1] - s[1] - t * dy)
463 };
464 to_segment(a[0], b)
465 .min(to_segment(a[1], b))
466 .min(to_segment(b[0], a))
467 .min(to_segment(b[1], a))
468}