1#[path = "support/ops.rs"]
2mod ops;
3use onestore::{
4 ExGuid, RevisionIndex, Store,
5 document::Document,
6 page::{Ink, InkStroke, Page, PageObject, text::new_id},
7};
8
9/// Two drawings made with the mouse in OneNote 2010 (`tools/native/ink.ahk`): a diamond and a
10/// diagonal line, whose native read reports their positions and sizes.
11const NATIVE: &[u8] =
12 include_bytes!("../../../corpus/native-ink/cold-ui-ink/notebook/synthetic.one");
13
14fn first_page(bytes: &[u8]) -> (ExGuid, Page) {
15 let store = Store::parse(bytes).unwrap();
16 assert!(store.checksum_mismatches.is_empty());
17 let index = RevisionIndex::parse(&store).unwrap();
18 let document = Document::parse(&index).unwrap();
19 let (space, _) = document.pages().unwrap()[0];
20 (space, Page::from_space(&document, space).unwrap())
21}
22
23fn drawings(page: &Page) -> Vec<&Ink> {
24 page.objects
25 .iter()
26 .filter_map(|object| match object {
27 PageObject::Ink(ink) => Some(ink),
28 _ => None,
29 })
30 .collect()
31}
32
33fn close(actual: [f32; 4], expected: [f32; 4]) -> bool {
34 actual
35 .iter()
36 .zip(expected)
37 .all(|(a, e)| (a - e).abs() < 0.002)
38}
39
40#[test]
41fn native_ink_drawings_read_as_strokes_in_page_coordinates() {
42 let (_, page) = first_page(NATIVE);
43 let found = drawings(&page);
44 let [diamond, line] = found.as_slice() else {
45 panic!("{:?}", found.len());
46 };
47 assert_eq!(diamond.strokes.len(), 1);
48 assert_eq!(diamond.strokes[0].points.len(), 253);
49 // OneNote reports a drawing's size one HIMETRIC unit larger than its point extent.
50 let unit = 72.0 / 2540.0;
51 let native = |[x, y, w, h]: [f32; 4]| [x, y, w + unit, h + unit];
52 let bounds = (
53 native(diamond.bounds().unwrap()),
54 native(line.bounds().unwrap()),
55 );
56 assert!(
57 close(bounds.0, [421.5118, 84.7559, 60.00945, 60.00944])
58 && close(bounds.1, [504.0, 92.23936, 30.0189, 45.04252]),
59 "{bounds:?}"
60 );
61 let stroke = &diamond.strokes[0];
62 assert!(close(
63 [stroke.points[0][0], stroke.points[0][1], 0.0, 0.0],
64 [451.50235, 84.755905, 0.0, 0.0]
65 ));
66 assert!((stroke.width - 35.0 * 72.0 / 2540.0).abs() < 1e-5);
67 assert_eq!(
68 (stroke.color, stroke.transparency, stroke.pen_tip),
69 (None, None, None)
70 );
71 assert!(diamond.groups.is_empty());
72 for stroke in [&diamond.strokes[0], &line.strokes[0]] {
73 assert!(stroke.points.windows(2).all(|pair| {
74 let [dx, dy] = [pair[1][0] - pair[0][0], pair[1][1] - pair[0][1]];
75 dx.abs() < 3.0 && dy.abs() < 3.0
76 }));
77 }
78}
79
80use onestore::page::ink::snap;
81
82fn stroke(points: &[[f32; 2]], color: Option<u32>) -> InkStroke {
83 InkStroke {
84 id: new_id().unwrap(),
85 points: points.iter().map(|p| [snap(p[0]), snap(p[1])]).collect(),
86 width: snap(1.0),
87 height: snap(1.0),
88 color,
89 transparency: None,
90 pen_tip: None,
91 raster_operation: None,
92 pressure: Vec::new(),
93 }
94}
95
96fn drawing() -> Ink {
97 Ink {
98 id: new_id().unwrap(),
99 layout: Default::default(),
100 strokes: vec![
101 stroke(
102 &[
103 [300.0, 120.0],
104 [360.0, 120.0],
105 [360.0, 180.0],
106 [300.0, 180.0],
107 [300.0, 120.0],
108 ],
109 None,
110 ),
111 stroke(&[[300.0, 120.0], [360.0, 180.0]], Some(0x0000ff)),
112 ],
113 groups: Vec::new(),
114 shape: None,
115 }
116}
117
118fn page_in(bytes: &[u8], space: ExGuid) -> Page {
119 let store = Store::parse(bytes).unwrap();
120 assert!(store.checksum_mismatches.is_empty());
121 let index = RevisionIndex::parse(&store).unwrap();
122 Page::from_space(&Document::parse(&index).unwrap(), space).unwrap()
123}
124
125fn export(variable: &str, name: &str, written: &[u8]) {
126 if let Some(directory) = std::env::var_os(variable) {
127 let directory = std::path::PathBuf::from(directory);
128 std::fs::create_dir(&directory).unwrap();
129 std::fs::write(directory.join(name), written).unwrap();
130 let file_id = Store::parse(written).unwrap().header.file_id;
131 std::fs::write(
132 directory.join("Open Notebook.onetoc2"),
133 onestore::create_table_of_contents("Open Notebook.onetoc2", &[(name, file_id)])
134 .unwrap(),
135 )
136 .unwrap();
137 }
138}
139
140/// `ONESTORE_INK_EXPORT` names a new directory receiving the candidate for a cold reopen.
141#[test]
142fn an_ink_drawing_is_written_erased_stroke_by_stroke_and_removed() {
143 let source = onestore::create_section("ink.one", "Beside the drawing", "Author").unwrap();
144 let (space, before) = first_page(&source);
145 let mut after = before.clone();
146 let ink = drawing();
147 after.objects.push(PageObject::Ink(ink.clone()));
148 let written = ops::saved(&source, space, &after).unwrap();
149 let stored = page_in(written.as_slice(), space);
150 let mut expected = after.clone();
151 expected.title = stored.title.clone();
152 assert_eq!(stored, expected);
153 assert!(close(
154 drawings(&stored)[0].bounds().unwrap(),
155 [
156 snap(300.0),
157 snap(120.0),
158 snap(360.0) - snap(300.0),
159 snap(180.0) - snap(120.0)
160 ]
161 ));
162 assert_eq!(
163 ops::saved(written.as_slice(), space, &stored)
164 .unwrap()
165 .as_slice(),
166 written.as_slice()
167 );
168 export("ONESTORE_INK_EXPORT", "ink.one", written.as_slice());
169 let mut erased = stored.clone();
170 for object in &mut erased.objects {
171 if let PageObject::Ink(ink) = object {
172 ink.strokes.remove(1);
173 ink.strokes
174 .push(stroke(&[[330.0, 100.0], [330.0, 200.0]], Some(0xff0000)));
175 }
176 }
177 let again = ops::saved(written.as_slice(), space, &erased).unwrap();
178 let stored = page_in(again.as_slice(), space);
179 assert_eq!(stored, erased);
180 let mut removed = stored.clone();
181 removed.objects.retain(|object| object.id() != ink.id);
182 let last = ops::saved(again.as_slice(), space, &removed).unwrap();
183 let stored = page_in(last.as_slice(), space);
184 assert!(drawings(&stored).is_empty());
185 assert_eq!(stored, removed);
186}
187
188/// `ONESTORE_INK_PARAGRAPH_EXPORT` names a new directory receiving the candidate for a cold
189/// reopen.
190#[test]
191fn handwriting_is_written_as_paragraph_content() {
192 use onestore::page::{PageParagraph, ParagraphContent};
193 let source = onestore::create_section("ink.one", "Above the handwriting", "Author").unwrap();
194 let (space, before) = first_page(&source);
195 let mut after = before.clone();
196 let outline = after
197 .objects
198 .iter_mut()
199 .find_map(|object| match object {
200 PageObject::Outline(outline) if !outline.title => Some(outline),
201 _ => None,
202 })
203 .unwrap();
204 let mut paragraph: PageParagraph = outline.paragraphs[0].clone();
205 paragraph.id = new_id().unwrap();
206 paragraph.parent = None;
207 paragraph.level = 1;
208 paragraph.lists.clear();
209 paragraph.tags.clear();
210 paragraph.style = None;
211 paragraph.collapsed = false;
212 paragraph.format = Default::default();
213 paragraph.content = ParagraphContent::Ink(drawing());
214 outline.paragraphs.push(paragraph);
215 let written = ops::saved(&source, space, &after).unwrap();
216 let stored = page_in(written.as_slice(), space);
217 let mut expected = after.clone();
218 expected.title = stored.title.clone();
219 assert_eq!(stored, expected);
220 export(
221 "ONESTORE_INK_PARAGRAPH_EXPORT",
222 "ink.one",
223 written.as_slice(),
224 );
225}
226
227#[test]
228fn stored_strokes_keep_their_paths() {
229 let (space, page) = first_page(NATIVE);
230 let mut moved = page.clone();
231 for object in &mut moved.objects {
232 if let PageObject::Ink(ink) = object {
233 ink.strokes[0].points[0][0] += 1.0;
234 }
235 }
236 assert!(ops::saved(NATIVE, space, &moved).is_err());
237}
238
239/// OneNote 2010's Draw tab used with the mouse (`corpus/ink-tools/native-ui`): every favourite
240/// pen and highlighter, shapes, and ink erased, moved and deleted.
241const TOOLS: &[u8] = include_bytes!("../../../corpus/ink-tools/native-ui/notebook/ink.one");
242
243fn titled(bytes: &[u8], title: &str) -> (ExGuid, Page) {
244 let store = Store::parse(bytes).unwrap();
245 let index = RevisionIndex::parse(&store).unwrap();
246 let document = Document::parse(&index).unwrap();
247 document
248 .pages()
249 .unwrap()
250 .into_iter()
251 .map(|(space, _)| (space, Page::from_space(&document, space).unwrap()))
252 .find(|(_, page)| page.title == title)
253 .unwrap()
254}
255
256fn himetric(value: f32) -> f32 {
257 value * 72.0 / 2540.0
258}
259
260use onestore::page::ink::{InkShape, ShapeKind};
261
262#[test]
263fn native_pens_highlighters_and_shapes_read_as_drawn() {
264 let (_, gallery) = titled(TOOLS, "Gallery");
265 let pens: Vec<_> = drawings(&gallery)
266 .iter()
267 .map(|ink| {
268 let [stroke] = ink.strokes.as_slice() else {
269 panic!("OneNote keeps each stroke a drawing of its own")
270 };
271 (
272 (stroke.width / himetric(1.0)).round(),
273 (stroke.height / himetric(1.0)).round(),
274 stroke.color,
275 stroke.raster_operation,
276 )
277 })
278 .collect();
279 let (red, blue, green, grey) = (0x241ced, 0xbb6531, 0x367d17, 0x808080);
280 let pen = |width, color| (width, width, color, None);
281 let marker = |color| (70.0, 400.0, Some(color), Some(9));
282 assert_eq!(
283 pens,
284 [
285 pen(35.0, None),
286 pen(35.0, Some(red)),
287 pen(35.0, Some(blue)),
288 pen(35.0, Some(green)),
289 pen(35.0, Some(grey)),
290 marker(0x00ffff),
291 marker(0xffff00),
292 pen(50.0, None),
293 pen(50.0, Some(red)),
294 pen(50.0, Some(blue)),
295 pen(50.0, Some(green)),
296 pen(50.0, Some(grey)),
297 marker(0x00ff00),
298 marker(0xff00ff),
299 ]
300 );
301 let highlighter = &drawings(&gallery)[5].strokes[0];
302 assert_eq!(
303 (highlighter.transparency, highlighter.pen_tip),
304 (Some(127), Some(1))
305 );
306
307 let (_, pens) = titled(TOOLS, "Pens");
308 let shapes: Vec<&Ink> = drawings(&pens)
309 .into_iter()
310 .filter(|ink| ink.shape.is_some())
311 .collect();
312 let [rectangle, line, arrow, ellipse] = shapes.as_slice() else {
313 panic!("{}", shapes.len())
314 };
315 let pen = InkStroke {
316 points: Vec::new(),
317 ..rectangle.strokes[0].clone()
318 };
319 assert!((pen.width - himetric(50.0)).abs() < 1e-5);
320 let close_points = |a: &[[f32; 2]], b: &[[f32; 2]]| {
321 a.len() == b.len()
322 && a.iter()
323 .zip(b)
324 .all(|(a, b)| (a[0] - b[0]).abs() < 0.05 && (a[1] - b[1]).abs() < 0.05)
325 };
326 for (native, kind, from, to) in [
327 (
328 rectangle,
329 ShapeKind::Rectangle,
330 [252.0, 86.4],
331 [324.0, 122.4],
332 ),
333 (line, ShapeKind::Line, [342.0, 86.4], [414.0, 122.4]),
334 (arrow, ShapeKind::Arrow, [252.0, 158.4], [324.0, 194.4]),
335 (ellipse, ShapeKind::Ellipse, [342.0, 158.4], [414.0, 194.4]),
336 ] {
337 let drawn = Ink::drawn(kind, from, to, &pen).unwrap();
338 assert_eq!(drawn.strokes.len(), native.strokes.len(), "{kind:?}");
339 for (drawn, native) in drawn.strokes.iter().zip(&native.strokes) {
340 assert!(
341 close_points(&drawn.points, &native.points),
342 "{kind:?}: {:?} against {:?}",
343 drawn.points,
344 native.points
345 );
346 }
347 match (drawn.shape.unwrap(), native.shape.clone().unwrap()) {
348 (InkShape::Line(a), InkShape::Line(b)) => assert!(close_points(&a, &b)),
349 (
350 InkShape::Closed { transform, anchors },
351 InkShape::Closed {
352 transform: native_transform,
353 anchors: native_anchors,
354 },
355 ) => {
356 assert_eq!(anchors, native_anchors);
357 assert!(
358 transform
359 .iter()
360 .zip(native_transform)
361 .all(|(a, b)| (a - b).abs() < 0.05)
362 );
363 }
364 shapes => panic!("{shapes:?}"),
365 }
366 }
367
368 // Erased and deleted drawings leave the page; a moved one keeps its strokes and takes
369 // an offset.
370 let (_, edits) = titled(TOOLS, "Edits");
371 let moved: Vec<_> = drawings(&edits)
372 .into_iter()
373 .filter(|ink| ink.shape.is_none())
374 .map(|ink| (ink.layout.x, ink.layout.y))
375 .collect();
376 assert_eq!(moved.len(), 2);
377 assert!(
378 moved
379 .iter()
380 .any(|&(x, y)| x == Some(45.0) && y.is_some_and(|y| (y - 15.0).abs() < 1e-4))
381 );
382}
383
384fn tool_page(source: &[u8]) -> (ExGuid, Page, Vec<Ink>) {
385 let (space, page) = first_page(source);
386 let accent = InkStroke {
387 id: new_id().unwrap(),
388 points: Vec::new(),
389 width: snap(himetric(35.0)),
390 height: snap(himetric(35.0)),
391 color: Some(0x7a9a1f),
392 transparency: None,
393 pen_tip: None,
394 raster_operation: None,
395 pressure: Vec::new(),
396 };
397 let marker = InkStroke {
398 width: himetric(70.0),
399 height: himetric(400.0),
400 color: Some(0x00ffff),
401 transparency: Some(127),
402 pen_tip: Some(1),
403 raster_operation: Some(9),
404 ..accent.clone()
405 };
406 let shape_pen = InkStroke {
407 width: himetric(50.0),
408 height: himetric(50.0),
409 ..accent.clone()
410 };
411 let free = |pen: &InkStroke, points: Vec<[f32; 2]>| Ink {
412 id: new_id().unwrap(),
413 layout: Default::default(),
414 strokes: vec![InkStroke {
415 id: new_id().unwrap(),
416 points: points.into_iter().map(|p| p.map(snap)).collect(),
417 ..pen.clone()
418 }],
419 groups: Vec::new(),
420 shape: None,
421 };
422 let wave: Vec<[f32; 2]> = (0..=40)
423 .map(|step| {
424 let x = 60.0 + step as f32 * 4.0;
425 [x, 150.0 + 12.0 * (x / 18.0).sin()]
426 })
427 .collect();
428 let inks = vec![
429 free(&marker, vec![[60.0, 190.0], [220.0, 190.0]]),
430 free(&accent, wave),
431 Ink::drawn(
432 ShapeKind::Rectangle,
433 [252.0, 140.4],
434 [324.0, 194.4],
435 &shape_pen,
436 )
437 .unwrap(),
438 Ink::drawn(
439 ShapeKind::Ellipse,
440 [342.0, 140.4],
441 [432.0, 194.4],
442 &shape_pen,
443 )
444 .unwrap(),
445 Ink::drawn(ShapeKind::Arrow, [252.0, 230.4], [360.0, 266.4], &shape_pen).unwrap(),
446 Ink::drawn(ShapeKind::Line, [378.0, 230.4], [450.0, 230.4], &shape_pen).unwrap(),
447 ];
448 (space, page, inks)
449}
450
451fn added(source: &[u8], space: ExGuid, inks: &[Ink]) -> Vec<u8> {
452 let mut bytes = source.to_vec();
453 for ink in inks {
454 bytes = ops::page_edited(
455 &bytes,
456 space,
457 vec![onestore::op::PageOp::Add {
458 object: PageObject::Ink(ink.clone()),
459 before: None,
460 }],
461 )
462 .unwrap();
463 }
464 bytes
465}
466
467/// `ONESTORE_INK_TOOLS_EXPORT` names a new directory receiving the candidate for a cold
468/// reopen.
469#[test]
470fn pens_highlighters_and_shapes_are_written_as_onenote_draws_them() {
471 use onestore::document::{FieldValue, Kind};
472 let source = onestore::create_section("ink.one", "Drawn in Snowbound", "Author").unwrap();
473 let (space, before, inks) = tool_page(&source);
474 // Each stroke is one edit, as the canvas stores it.
475 let written = added(&source, space, &inks);
476 let stored = page_in(&written, space);
477 let mut expected = before.clone();
478 expected
479 .objects
480 .extend(inks.iter().cloned().map(PageObject::Ink));
481 expected.title = stored.title.clone();
482 assert_eq!(stored, expected);
483 export("ONESTORE_INK_TOOLS_EXPORT", "ink.one", &written);
484
485 let store = Store::parse(&written).unwrap();
486 let index = RevisionIndex::parse(&store).unwrap();
487 let document = Document::parse(&index).unwrap();
488 let revision = document.active(space).unwrap();
489 let node = |id: ExGuid| &revision.nodes[&id];
490 let extra = |id: ExGuid, property: u32| {
491 node(id)
492 .extra
493 .iter()
494 .flatten()
495 .find(|field| field.id == property)
496 .map(|field| match field.value {
497 FieldValue::Bytes(bytes) => bytes.to_vec(),
498 _ => Vec::new(),
499 })
500 };
501 let style = |stroke: ExGuid| match node(stroke).kind {
502 Kind::InkStroke {
503 style: Some(style),
504 bias,
505 index,
506 ..
507 } => (node(style).kind.clone(), bias, index),
508 _ => panic!(),
509 };
510 let [marker, wave, rectangle, _, arrow, _] = inks.as_slice() else {
511 unreachable!()
512 };
513 match style(marker.strokes[0].id) {
514 (
515 Kind::InkStyle {
516 raster_operation: Some(9),
517 pen_tip: Some(1),
518 transparency: Some(127),
519 ignore_pressure: Some(true),
520 ..
521 },
522 Some(2),
523 Some(1),
524 ) => {}
525 other => panic!("{other:?}"),
526 }
527 match style(wave.strokes[0].id) {
528 (
529 Kind::InkStyle {
530 raster_operation: None,
531 ignore_pressure: Some(true),
532 color: Some(0x7a9a1f),
533 ..
534 },
535 Some(0),
536 Some(2),
537 ) => {}
538 other => panic!("{other:?}"),
539 }
540 for stroke in &arrow.strokes {
541 match style(stroke.id) {
542 (
543 Kind::InkStyle {
544 ignore_pressure: None,
545 ..
546 },
547 None,
548 None,
549 ) => {}
550 other => panic!("{other:?}"),
551 }
552 }
553 assert_eq!(
554 extra(wave.id, 0x14001d4e),
555 Some(1u32.to_le_bytes().to_vec())
556 );
557 assert_eq!(
558 extra(rectangle.id, 0x14001d4e),
559 Some(2u32.to_le_bytes().to_vec())
560 );
561 match &node(rectangle.id).kind {
562 Kind::Ink {
563 shape_kind: Some(12),
564 anchors: Some(anchors),
565 ..
566 } => assert_eq!(anchors.len(), 24 + 8 * 8),
567 other => panic!("{other:?}"),
568 }
569}
570
571/// `ONESTORE_INK_TOOLS_EDIT_EXPORT` names a new directory receiving the candidate for a cold
572/// reopen: OneNote's own drawings after Snowbound erases one, moves another and draws more.
573#[test]
574fn onenote_drawings_take_erasing_moving_and_more_strokes() {
575 use onestore::{OutlineEdit, op::PageOp};
576 let (space, page) = titled(TOOLS, "Pens");
577 let native = drawings(&page);
578 let zigzag = native
579 .iter()
580 .find(|ink| ink.strokes[0].color == Some(0x241ced))
581 .unwrap()
582 .id;
583 let line = native[1].id;
584 let edited = ops::page_edited(
585 TOOLS,
586 space,
587 vec![
588 PageOp::Delete { object: zigzag },
589 PageOp::Outline {
590 object: line,
591 edit: OutlineEdit::Position { x: 36.0, y: 18.0 },
592 },
593 ],
594 )
595 .unwrap();
596 let (_, _, inks) = tool_page(&onestore::create_section("x.one", "x", "x").unwrap());
597 let moved: Vec<Ink> = inks
598 .into_iter()
599 .map(|mut ink| {
600 for stroke in &mut ink.strokes {
601 for point in &mut stroke.points {
602 point[1] = snap(point[1] + 200.0);
603 }
604 }
605 if let Some(shape) = &mut ink.shape {
606 *shape = match shape {
607 InkShape::Line([a, b]) => {
608 InkShape::Line([[a[0], a[1] + 200.0], [b[0], b[1] + 200.0]])
609 }
610 InkShape::Closed { transform, anchors } => InkShape::Closed {
611 transform: {
612 let mut t = *transform;
613 t[5] += 200.0;
614 t
615 },
616 anchors: anchors.clone(),
617 },
618 }
619 .snapped();
620 }
621 ink
622 })
623 .collect();
624 let written = added(&edited, space, &moved);
625 let stored = page_in(&written, space);
626 let found = drawings(&stored);
627 assert!(!found.iter().any(|ink| ink.id == zigzag));
628 let line = found.iter().find(|ink| ink.id == line).unwrap();
629 assert_eq!((line.layout.x, line.layout.y), (Some(36.0), Some(18.0)));
630 assert_eq!(line.strokes, native[1].strokes);
631 for ink in &moved {
632 assert_eq!(found.iter().find(|found| found.id == ink.id), Some(&ink));
633 }
634 export("ONESTORE_INK_TOOLS_EDIT_EXPORT", "ink.one", &written);
635}
636
637/// OneNote 2010's pressure ink, taken in from ISF (`tools/native/ink-pressure.ps1`).
638const PRESSURE: &[u8] = include_bytes!("../../../corpus/ink-pressure/native/notebook/Pressure.one");
639
640#[test]
641fn onenote_pressure_ink_reads_each_point_s_level_over_the_pen_s_range() {
642 let (_, page) = titled(PRESSURE, "Levels");
643 let [fine, coarse] = drawings(&page)[..] else {
644 panic!("{:?}", drawings(&page).len());
645 };
646 for (ink, range) in [(fine, 1023.0), (coarse, 255.0)] {
647 assert_eq!(ink.strokes.len(), 9);
648 for (step, stroke) in ink.strokes.iter().enumerate() {
649 let level = (step as f32 / 8.0 * range).round() / range;
650 assert_eq!(stroke.pressure.len(), stroke.points.len());
651 assert!(stroke.pressure.iter().all(|p| (p - level).abs() < 1e-6));
652 assert!((stroke.thickness(0) - (0.25 + 1.5 * level)).abs() < 1e-6);
653 }
654 }
655 let (_, page) = titled(PRESSURE, "Strokes");
656 let [ramp, _, flat, tilt, block] = drawings(&page)[..] else {
657 panic!("{:?}", drawings(&page).len());
658 };
659 let ramp = &ramp.strokes[0].pressure;
660 assert_eq!((ramp.len(), ramp[0], ramp[40]), (41, 0.0, 1.0));
661 // A pen that ignores the pressure it recorded draws at its width throughout.
662 assert!(flat.strokes[0].pressure.is_empty());
663 assert_eq!(flat.strokes[0].thickness(20), 1.0);
664 // Tilt beside the pressure leaves the pressure as it is.
665 let tilted = &tilt.strokes[0].pressure;
666 assert!((tilted[0] - 205.0 / 1023.0).abs() < 1e-6, "{tilted:?}");
667 assert_eq!(block.strokes[0].pen_tip, Some(1));
668 assert_eq!(block.strokes[0].pressure.len(), 11);
669}
670
671/// `ONESTORE_INK_PRESSURE_EXPORT` names a new directory receiving the candidate for a cold
672/// reopen: OneNote's pressure ink after Snowbound erases a stroke of one drawing, moves one
673/// and deletes another, beside Snowbound's own pressure strokes.
674#[test]
675fn pressure_ink_is_written_as_onenote_keeps_it_and_survives_edits() {
676 use onestore::document::Kind;
677 use onestore::{OutlineEdit, op::PageOp};
678 let (levels_space, levels) = titled(PRESSURE, "Levels");
679 let fine = drawings(&levels)[0].clone();
680 let erased = ops::page_edited(
681 PRESSURE,
682 levels_space,
683 vec![PageOp::Strokes {
684 ink: fine.id,
685 add: Vec::new(),
686 remove: vec![fine.strokes[0].id],
687 }],
688 )
689 .unwrap();
690 let pen = |points: Vec<[f32; 2]>, pressure: Vec<f32>| Ink {
691 id: new_id().unwrap(),
692 layout: Default::default(),
693 strokes: vec![InkStroke {
694 id: new_id().unwrap(),
695 points: points.into_iter().map(|p| p.map(snap)).collect(),
696 width: snap(himetric(500.0)),
697 height: snap(himetric(500.0)),
698 color: Some(0x7a9a1f),
699 transparency: None,
700 pen_tip: None,
701 raster_operation: None,
702 pressure: pressure
703 .into_iter()
704 .map(onestore::page::ink::level)
705 .collect(),
706 }],
707 groups: Vec::new(),
708 shape: None,
709 };
710 // Constant levels of none, half and full pressure, and a ramp from none to full.
711 let mut inks: Vec<Ink> = [0.0, 0.5, 1.0]
712 .into_iter()
713 .enumerate()
714 .map(|(row, pressure)| {
715 let y = 520.0 + 36.0 * row as f32;
716 pen(vec![[36.0, y], [108.0, y], [180.0, y]], vec![pressure; 3])
717 })
718 .collect();
719 inks.push(pen(
720 (0..=40)
721 .map(|step| [36.0 + 6.0 * step as f32, 640.0])
722 .collect(),
723 (0..=40).map(|step| step as f32 / 40.0).collect(),
724 ));
725 let written = added(&erased, levels_space, &inks);
726 let (strokes_space, strokes) = titled(&written, "Strokes");
727 let [ramp, wave, ..] = drawings(&strokes)[..] else {
728 unreachable!()
729 };
730 let written = ops::page_edited(
731 &written,
732 strokes_space,
733 vec![
734 PageOp::Delete { object: wave.id },
735 PageOp::Outline {
736 object: ramp.id,
737 edit: OutlineEdit::Position { x: 72.0, y: 108.0 },
738 },
739 ],
740 )
741 .unwrap();
742
743 let stored = page_in(&written, levels_space);
744 let found = drawings(&stored);
745 let kept = found.iter().find(|ink| ink.id == fine.id).unwrap();
746 assert_eq!(kept.strokes, fine.strokes[1..]);
747 for ink in &inks {
748 assert_eq!(found.iter().find(|found| found.id == ink.id), Some(&ink));
749 }
750 let stored = page_in(&written, strokes_space);
751 let moved = drawings(&stored);
752 assert_eq!(moved.len(), 4);
753 assert_eq!(
754 (moved[0].layout.x, moved[0].layout.y),
755 (Some(72.0), Some(108.0))
756 );
757 assert_eq!(moved[0].strokes, ramp.strokes);
758
759 // New pressure strokes store NormalPressure after X and Y and leave IgnorePressure out,
760 // as OneNote does with pressure sensitivity on; OneNote's tilt stays as it stored it.
761 let store = Store::parse(&written).unwrap();
762 let index = RevisionIndex::parse(&store).unwrap();
763 let document = Document::parse(&index).unwrap();
764 let style = |space: ExGuid, stroke: ExGuid| {
765 let revision = document.active(space).unwrap();
766 let Kind::InkStroke {
767 style: Some(style), ..
768 } = revision.nodes[&stroke].kind
769 else {
770 panic!()
771 };
772 revision.nodes[&style].kind.clone()
773 };
774 match style(levels_space, inks[0].strokes[0].id) {
775 Kind::InkStyle {
776 dimensions,
777 ignore_pressure: None,
778 ..
779 } => {
780 assert_eq!(dimensions.len(), 96);
781 assert_eq!(
782 &dimensions[64..],
783 &[
784 0x2d, 0x50, 0x07, 0x73, 0xf4, 0xf9, 0x18, 0x4e, 0xb3, 0xf2, 0x2c, 0xe1, 0xb1,
785 0xa3, 0x61, 0x0c, 0, 0, 0, 0, 0xff, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0x80, 0x3f
786 ]
787 );
788 }
789 other => panic!("{other:?}"),
790 }
791 match style(strokes_space, moved[2].strokes[0].id) {
792 Kind::InkStyle { dimensions, .. } => assert_eq!(dimensions.len(), 5 * 32),
793 other => panic!("{other:?}"),
794 }
795 export("ONESTORE_INK_PRESSURE_EXPORT", "Pressure.one", &written);
796}