From a861b59985b1920526baa1ea2be635a760965f52 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Tue, 29 Sep 2026 17:45:49 -0700 Subject: [PATCH] feat: drawing tools and audio recording - Drawing tools (#21): pens and highlighters from OneNote 2010's gallery, the stroke eraser, lasso select, move and delete, and line, arrow, rectangle and oval shapes. Each stroke is its own ink drawing, stored as OneNote stores it, with highlighters drawn by a multiply blend. - Audio recording (#36): Record Audio inserts a recording and OneNote's started line, links notes typed while recording to their moment, and plays them back from a transport over the page. Recordings are stored as the spec requires, as IMA ADPCM WAV that OneNote 2010 plays, and undo of a page delete restores them. Assisted-by: claude-opus-5.5 --- apps/ios/Snowbound/Notebooks.swift | 85 +- arc/canvas.md | 27 + arc/file-format.md | 13 +- arc/sync.md | 54 +- corpus/ink-tools/README.md | 55 ++ .../drawn/candidate/Open Notebook.onetoc2 | Bin 0 -> 1596 bytes corpus/ink-tools/drawn/candidate/ink.one | Bin 0 -> 11656 bytes corpus/ink-tools/drawn/cold/commands.jsonl | 3 + corpus/ink-tools/drawn/cold/machine.json | 1 + .../drawn/cold/notebook/Open Notebook.onetoc2 | Bin 0 -> 3748 bytes corpus/ink-tools/drawn/cold/notebook/ink.one | Bin 0 -> 11656 bytes .../drawn/cold/read/environment.json | 7 + .../ink-tools/drawn/cold/read/hierarchy.xml | 2 + .../drawn/cold/read/page-000.navigation.json | 13 + corpus/ink-tools/drawn/cold/read/page-000.png | Bin 0 -> 44020 bytes corpus/ink-tools/drawn/cold/read/page-000.xml | 19 + .../ink-tools/drawn/cold/read/payloads.json | 3 + corpus/ink-tools/drawn/cold/run.json | 18 + corpus/ink-tools/drawn/cold/scripts/cold.ps1 | 27 + corpus/ink-tools/drawn/cold/scripts/read.ps1 | 144 +++ corpus/ink-tools/drawn/cold/source.json | 14 + corpus/ink-tools/drawn/cold/teardown.json | 1 + .../edited/candidate/Open Notebook.onetoc2 | Bin 0 -> 1596 bytes corpus/ink-tools/edited/candidate/ink.one | Bin 0 -> 86952 bytes corpus/ink-tools/edited/cold/commands.jsonl | 3 + corpus/ink-tools/edited/cold/machine.json | 1 + .../cold/notebook/Open Notebook.onetoc2 | Bin 0 -> 3748 bytes corpus/ink-tools/edited/cold/notebook/ink.one | Bin 0 -> 86952 bytes .../edited/cold/read/environment.json | 7 + .../ink-tools/edited/cold/read/hierarchy.xml | 2 + .../edited/cold/read/page-000.navigation.json | 13 + .../ink-tools/edited/cold/read/page-000.png | Bin 0 -> 46557 bytes .../ink-tools/edited/cold/read/page-000.xml | 24 + .../edited/cold/read/page-001.navigation.json | 13 + .../ink-tools/edited/cold/read/page-001.png | Bin 0 -> 42240 bytes .../ink-tools/edited/cold/read/page-001.xml | 7 + .../edited/cold/read/page-002.navigation.json | 13 + .../ink-tools/edited/cold/read/page-002.png | Bin 0 -> 42399 bytes .../ink-tools/edited/cold/read/page-002.xml | 15 + .../edited/cold/read/page-003.navigation.json | 13 + .../ink-tools/edited/cold/read/page-003.png | Bin 0 -> 42584 bytes .../ink-tools/edited/cold/read/page-003.xml | 75 ++ .../edited/cold/read/page-004.navigation.json | 13 + .../ink-tools/edited/cold/read/page-004.png | Bin 0 -> 45358 bytes .../ink-tools/edited/cold/read/page-004.xml | 46 + .../ink-tools/edited/cold/read/payloads.json | 3 + corpus/ink-tools/edited/cold/run.json | 18 + corpus/ink-tools/edited/cold/scripts/cold.ps1 | 27 + corpus/ink-tools/edited/cold/scripts/read.ps1 | 144 +++ corpus/ink-tools/edited/cold/source.json | 14 + corpus/ink-tools/edited/cold/teardown.json | 1 + corpus/ink-tools/native-ui/commands.jsonl | 3 + corpus/ink-tools/native-ui/machine.json | 1 + .../ink-tools/native-ui/read/environment.json | 7 + corpus/ink-tools/native-ui/read/hierarchy.xml | 2 + .../native-ui/read/page-000.navigation.json | 13 + corpus/ink-tools/native-ui/read/page-000.png | Bin 0 -> 46536 bytes corpus/ink-tools/native-ui/read/page-000.xml | 24 + .../native-ui/read/page-001.navigation.json | 13 + corpus/ink-tools/native-ui/read/page-001.png | Bin 0 -> 42379 bytes corpus/ink-tools/native-ui/read/page-001.xml | 15 + .../native-ui/read/page-002.navigation.json | 13 + corpus/ink-tools/native-ui/read/page-002.png | Bin 0 -> 42221 bytes corpus/ink-tools/native-ui/read/page-002.xml | 7 + .../native-ui/read/page-003.navigation.json | 13 + corpus/ink-tools/native-ui/read/page-003.png | Bin 0 -> 42564 bytes corpus/ink-tools/native-ui/read/page-003.xml | 75 ++ .../native-ui/read/page-004.navigation.json | 13 + corpus/ink-tools/native-ui/read/page-004.png | Bin 0 -> 45175 bytes corpus/ink-tools/native-ui/read/page-004.xml | 39 + corpus/ink-tools/native-ui/read/payloads.json | 3 + corpus/ink-tools/native-ui/run.json | 18 + corpus/ink-tools/native-ui/scripts/cold.ps1 | 27 + corpus/ink-tools/native-ui/scripts/read.ps1 | 144 +++ corpus/ink-tools/native-ui/source.json | 14 + corpus/ink-tools/native-ui/teardown.json | 1 + corpus/media-edit/README.md | 3 +- corpus/recording/README.md | 65 ++ .../recording/candidate/Open Notebook.onetoc2 | Bin 0 -> 1604 bytes corpus/recording/candidate/Recorded.one | Bin 0 -> 26848 bytes corpus/recording/cold/commands.jsonl | 3 + corpus/recording/cold/machine.json | 1 + .../cold/notebook/Open Notebook.onetoc2 | Bin 0 -> 3764 bytes corpus/recording/cold/notebook/Recorded.one | Bin 0 -> 28344 bytes ...261c422c4a083ab7f721cb94b186087.attachment | Bin 0 -> 16444 bytes corpus/recording/cold/read/environment.json | 7 + corpus/recording/cold/read/hierarchy.xml | 2 + .../cold/read/page-000.navigation.json | 13 + corpus/recording/cold/read/page-000.png | Bin 0 -> 45564 bytes corpus/recording/cold/read/page-000.xml | 2 + corpus/recording/cold/read/payloads.json | 10 + corpus/recording/cold/run.json | 18 + corpus/recording/cold/scripts/cold.ps1 | 27 + corpus/recording/cold/scripts/read.ps1 | 144 +++ corpus/recording/cold/source.json | 14 + corpus/recording/cold/teardown.json | 1 + .../edit/candidate/Open Notebook.onetoc2 | Bin 0 -> 3764 bytes corpus/recording/edit/candidate/files.one | Bin 0 -> 82304 bytes corpus/recording/edit/cold/commands.jsonl | 3 + corpus/recording/edit/cold/machine.json | 1 + .../edit/cold/notebook/Open Notebook.onetoc2 | Bin 0 -> 3764 bytes corpus/recording/edit/cold/notebook/files.one | Bin 0 -> 82304 bytes ...a3672dbbd27ed9d2e4ac866c13ec4b6.attachment | 1 + ...8451ba5b4665aba662ac1280d907ca3.attachment | Bin 0 -> 24827 bytes .../recording/edit/cold/read/environment.json | 7 + corpus/recording/edit/cold/read/hierarchy.xml | 2 + .../edit/cold/read/page-000.navigation.json | 13 + corpus/recording/edit/cold/read/page-000.png | Bin 0 -> 47800 bytes corpus/recording/edit/cold/read/page-000.xml | 2 + .../edit/cold/read/page-001.navigation.json | 13 + corpus/recording/edit/cold/read/page-001.png | Bin 0 -> 41400 bytes corpus/recording/edit/cold/read/page-001.xml | 2 + corpus/recording/edit/cold/read/payloads.json | 18 + corpus/recording/edit/cold/run.json | 18 + corpus/recording/edit/cold/scripts/cold.ps1 | 27 + corpus/recording/edit/cold/scripts/read.ps1 | 144 +++ corpus/recording/edit/cold/source.json | 14 + corpus/recording/edit/cold/teardown.json | 1 + corpus/recording/native/attached/files.one | Bin 0 -> 78616 bytes corpus/recording/native/deleted/files.one | Bin 0 -> 57960 bytes .../native/notebook/Open Notebook.onetoc2 | Bin 0 -> 3764 bytes corpus/recording/native/notebook/files.one | Bin 0 -> 51208 bytes .../native/read/onenote-linked-notes.png | Bin 0 -> 49251 bytes .../native/read/onenote-plays-ima-adpcm.png | Bin 0 -> 58361 bytes .../native/read/onenote-recording-tab.png | Bin 0 -> 55895 bytes .../recording/native/read/page-recorded.xml | 3 + corpus/recording/played-from-note.png | Bin 0 -> 56750 bytes crates/canvas/src/editor.rs | 50 +- crates/canvas/src/editor/ink.rs | 450 ++++++++++ crates/canvas/src/editor/recording.rs | 284 ++++++ crates/canvas/src/gpu/page.rs | 97 +- crates/canvas/src/interaction/ink.rs | 326 +++++++ crates/canvas/src/interaction/mod.rs | 107 ++- crates/canvas/src/outline.rs | 2 + crates/canvas/tests/ink_tools.rs | 287 ++++++ crates/canvas/tests/ops_differential.rs | 153 +++- crates/canvas/tests/recording.rs | 187 ++++ crates/draw/src/render.rs | 24 + crates/draw/src/render/gl.rs | 4 + crates/draw/src/render/webgpu.rs | 17 + crates/mobile/include/snowbound.h | 6 +- crates/mobile/src/lib.rs | 88 +- crates/mobile/src/library.rs | 40 +- crates/mobile/src/tests.rs | 129 ++- crates/notebook/README.md | 46 +- crates/notebook/examples/launch_probe.rs | 228 ----- crates/notebook/src/background.rs | 827 +++++++++++++++--- crates/notebook/src/discover.rs | 377 +++++--- crates/notebook/src/discover/source.rs | 40 +- crates/notebook/src/lib.rs | 34 +- crates/notebook/src/session.rs | 131 ++- crates/notebook/src/smb/directory.rs | 10 +- crates/notebook/src/smb/mod.rs | 5 + crates/notebook/src/worker.rs | 23 +- crates/notebook/tests/discovery.rs | 81 +- crates/notebook/tests/session.rs | 124 ++- crates/onestore/README.md | 4 +- crates/onestore/examples/ink_lab.rs | 25 - crates/onestore/examples/scratch_dump.rs | 29 - crates/onestore/src/op/apply.rs | 11 +- crates/onestore/src/op/content.rs | 20 +- crates/onestore/src/page/ink.rs | 12 +- crates/onestore/src/page/text.rs | 5 + crates/onestore/tests/page_ink.rs | 44 +- crates/onestore/tests/page_media.rs | 114 ++- crates/snowbound/assets/icons/eraser.svg | 11 + crates/snowbound/assets/icons/lasso.svg | 4 + crates/snowbound/assets/icons/pen.svg | 11 + crates/snowbound/assets/icons/select.svg | 5 + crates/snowbound/assets/icons/shape-arrow.svg | 3 + crates/snowbound/assets/icons/shape-line.svg | 3 + crates/snowbound/assets/icons/shape-oval.svg | 3 + .../assets/icons/shape-rectangle.svg | 3 + crates/snowbound/assets/icons/shapes.svg | 8 + crates/snowbound/src/art.rs | 10 + crates/snowbound/src/attachment.rs | 34 +- crates/snowbound/src/commands.rs | 93 +- crates/snowbound/src/library.rs | 54 +- crates/snowbound/src/main.rs | 185 +++- crates/snowbound/src/media_linux.rs | 270 ++++++ crates/snowbound/src/media_macos.rs | 182 ++++ crates/snowbound/src/pane.rs | 4 +- crates/snowbound/src/recording.rs | 687 +++++++++++++++ crates/snowbound/src/search.rs | 54 +- crates/snowbound/src/settings.rs | 4 + crates/snowbound/src/watch.rs | 121 ++- tools/canvas/build_macos.py | 1 + tools/test_ink_tools.py | 82 ++ tools/test_recording.py | 88 ++ tools/verify-document.py | 3 + 190 files changed, 7478 insertions(+), 848 deletions(-) create mode 100644 corpus/ink-tools/README.md create mode 100644 corpus/ink-tools/drawn/candidate/Open Notebook.onetoc2 create mode 100644 corpus/ink-tools/drawn/candidate/ink.one create mode 100644 corpus/ink-tools/drawn/cold/commands.jsonl create mode 100644 corpus/ink-tools/drawn/cold/machine.json create mode 100644 corpus/ink-tools/drawn/cold/notebook/Open Notebook.onetoc2 create mode 100644 corpus/ink-tools/drawn/cold/notebook/ink.one create mode 100644 corpus/ink-tools/drawn/cold/read/environment.json create mode 100644 corpus/ink-tools/drawn/cold/read/hierarchy.xml create mode 100644 corpus/ink-tools/drawn/cold/read/page-000.navigation.json create mode 100644 corpus/ink-tools/drawn/cold/read/page-000.png create mode 100644 corpus/ink-tools/drawn/cold/read/page-000.xml create mode 100644 corpus/ink-tools/drawn/cold/read/payloads.json create mode 100644 corpus/ink-tools/drawn/cold/run.json create mode 100644 corpus/ink-tools/drawn/cold/scripts/cold.ps1 create mode 100644 corpus/ink-tools/drawn/cold/scripts/read.ps1 create mode 100644 corpus/ink-tools/drawn/cold/source.json create mode 100644 corpus/ink-tools/drawn/cold/teardown.json create mode 100644 corpus/ink-tools/edited/candidate/Open Notebook.onetoc2 create mode 100644 corpus/ink-tools/edited/candidate/ink.one create mode 100644 corpus/ink-tools/edited/cold/commands.jsonl create mode 100644 corpus/ink-tools/edited/cold/machine.json create mode 100644 corpus/ink-tools/edited/cold/notebook/Open Notebook.onetoc2 create mode 100644 corpus/ink-tools/edited/cold/notebook/ink.one create mode 100644 corpus/ink-tools/edited/cold/read/environment.json create mode 100644 corpus/ink-tools/edited/cold/read/hierarchy.xml create mode 100644 corpus/ink-tools/edited/cold/read/page-000.navigation.json create mode 100644 corpus/ink-tools/edited/cold/read/page-000.png create mode 100644 corpus/ink-tools/edited/cold/read/page-000.xml create mode 100644 corpus/ink-tools/edited/cold/read/page-001.navigation.json create mode 100644 corpus/ink-tools/edited/cold/read/page-001.png create mode 100644 corpus/ink-tools/edited/cold/read/page-001.xml create mode 100644 corpus/ink-tools/edited/cold/read/page-002.navigation.json create mode 100644 corpus/ink-tools/edited/cold/read/page-002.png create mode 100644 corpus/ink-tools/edited/cold/read/page-002.xml create mode 100644 corpus/ink-tools/edited/cold/read/page-003.navigation.json create mode 100644 corpus/ink-tools/edited/cold/read/page-003.png create mode 100644 corpus/ink-tools/edited/cold/read/page-003.xml create mode 100644 corpus/ink-tools/edited/cold/read/page-004.navigation.json create mode 100644 corpus/ink-tools/edited/cold/read/page-004.png create mode 100644 corpus/ink-tools/edited/cold/read/page-004.xml create mode 100644 corpus/ink-tools/edited/cold/read/payloads.json create mode 100644 corpus/ink-tools/edited/cold/run.json create mode 100644 corpus/ink-tools/edited/cold/scripts/cold.ps1 create mode 100644 corpus/ink-tools/edited/cold/scripts/read.ps1 create mode 100644 corpus/ink-tools/edited/cold/source.json create mode 100644 corpus/ink-tools/edited/cold/teardown.json create mode 100644 corpus/ink-tools/native-ui/commands.jsonl create mode 100644 corpus/ink-tools/native-ui/machine.json create mode 100644 corpus/ink-tools/native-ui/read/environment.json create mode 100644 corpus/ink-tools/native-ui/read/hierarchy.xml create mode 100644 corpus/ink-tools/native-ui/read/page-000.navigation.json create mode 100644 corpus/ink-tools/native-ui/read/page-000.png create mode 100644 corpus/ink-tools/native-ui/read/page-000.xml create mode 100644 corpus/ink-tools/native-ui/read/page-001.navigation.json create mode 100644 corpus/ink-tools/native-ui/read/page-001.png create mode 100644 corpus/ink-tools/native-ui/read/page-001.xml create mode 100644 corpus/ink-tools/native-ui/read/page-002.navigation.json create mode 100644 corpus/ink-tools/native-ui/read/page-002.png create mode 100644 corpus/ink-tools/native-ui/read/page-002.xml create mode 100644 corpus/ink-tools/native-ui/read/page-003.navigation.json create mode 100644 corpus/ink-tools/native-ui/read/page-003.png create mode 100644 corpus/ink-tools/native-ui/read/page-003.xml create mode 100644 corpus/ink-tools/native-ui/read/page-004.navigation.json create mode 100644 corpus/ink-tools/native-ui/read/page-004.png create mode 100644 corpus/ink-tools/native-ui/read/page-004.xml create mode 100644 corpus/ink-tools/native-ui/read/payloads.json create mode 100644 corpus/ink-tools/native-ui/run.json create mode 100644 corpus/ink-tools/native-ui/scripts/cold.ps1 create mode 100644 corpus/ink-tools/native-ui/scripts/read.ps1 create mode 100644 corpus/ink-tools/native-ui/source.json create mode 100644 corpus/ink-tools/native-ui/teardown.json create mode 100644 corpus/recording/README.md create mode 100644 corpus/recording/candidate/Open Notebook.onetoc2 create mode 100644 corpus/recording/candidate/Recorded.one create mode 100644 corpus/recording/cold/commands.jsonl create mode 100644 corpus/recording/cold/machine.json create mode 100644 corpus/recording/cold/notebook/Open Notebook.onetoc2 create mode 100644 corpus/recording/cold/notebook/Recorded.one create mode 100644 corpus/recording/cold/read/36b0ffb210410ba1e1afffb66c6817f76261c422c4a083ab7f721cb94b186087.attachment create mode 100644 corpus/recording/cold/read/environment.json create mode 100644 corpus/recording/cold/read/hierarchy.xml create mode 100644 corpus/recording/cold/read/page-000.navigation.json create mode 100644 corpus/recording/cold/read/page-000.png create mode 100644 corpus/recording/cold/read/page-000.xml create mode 100644 corpus/recording/cold/read/payloads.json create mode 100644 corpus/recording/cold/run.json create mode 100644 corpus/recording/cold/scripts/cold.ps1 create mode 100644 corpus/recording/cold/scripts/read.ps1 create mode 100644 corpus/recording/cold/source.json create mode 100644 corpus/recording/cold/teardown.json create mode 100644 corpus/recording/edit/candidate/Open Notebook.onetoc2 create mode 100644 corpus/recording/edit/candidate/files.one create mode 100644 corpus/recording/edit/cold/commands.jsonl create mode 100644 corpus/recording/edit/cold/machine.json create mode 100644 corpus/recording/edit/cold/notebook/Open Notebook.onetoc2 create mode 100644 corpus/recording/edit/cold/notebook/files.one create mode 100644 corpus/recording/edit/cold/read/b20a14e044e33eac39b4665e7d79813b8a3672dbbd27ed9d2e4ac866c13ec4b6.attachment create mode 100644 corpus/recording/edit/cold/read/c41bd6bd61277bdc4bf3557b5572be5c08451ba5b4665aba662ac1280d907ca3.attachment create mode 100644 corpus/recording/edit/cold/read/environment.json create mode 100644 corpus/recording/edit/cold/read/hierarchy.xml create mode 100644 corpus/recording/edit/cold/read/page-000.navigation.json create mode 100644 corpus/recording/edit/cold/read/page-000.png create mode 100644 corpus/recording/edit/cold/read/page-000.xml create mode 100644 corpus/recording/edit/cold/read/page-001.navigation.json create mode 100644 corpus/recording/edit/cold/read/page-001.png create mode 100644 corpus/recording/edit/cold/read/page-001.xml create mode 100644 corpus/recording/edit/cold/read/payloads.json create mode 100644 corpus/recording/edit/cold/run.json create mode 100644 corpus/recording/edit/cold/scripts/cold.ps1 create mode 100644 corpus/recording/edit/cold/scripts/read.ps1 create mode 100644 corpus/recording/edit/cold/source.json create mode 100644 corpus/recording/edit/cold/teardown.json create mode 100644 corpus/recording/native/attached/files.one create mode 100644 corpus/recording/native/deleted/files.one create mode 100644 corpus/recording/native/notebook/Open Notebook.onetoc2 create mode 100644 corpus/recording/native/notebook/files.one create mode 100644 corpus/recording/native/read/onenote-linked-notes.png create mode 100644 corpus/recording/native/read/onenote-plays-ima-adpcm.png create mode 100644 corpus/recording/native/read/onenote-recording-tab.png create mode 100644 corpus/recording/native/read/page-recorded.xml create mode 100644 corpus/recording/played-from-note.png create mode 100644 crates/canvas/src/editor/ink.rs create mode 100644 crates/canvas/src/editor/recording.rs create mode 100644 crates/canvas/src/interaction/ink.rs create mode 100644 crates/canvas/tests/ink_tools.rs create mode 100644 crates/canvas/tests/recording.rs delete mode 100644 crates/notebook/examples/launch_probe.rs delete mode 100644 crates/onestore/examples/ink_lab.rs delete mode 100644 crates/onestore/examples/scratch_dump.rs create mode 100644 crates/snowbound/assets/icons/eraser.svg create mode 100644 crates/snowbound/assets/icons/lasso.svg create mode 100644 crates/snowbound/assets/icons/pen.svg create mode 100644 crates/snowbound/assets/icons/select.svg create mode 100644 crates/snowbound/assets/icons/shape-arrow.svg create mode 100644 crates/snowbound/assets/icons/shape-line.svg create mode 100644 crates/snowbound/assets/icons/shape-oval.svg create mode 100644 crates/snowbound/assets/icons/shape-rectangle.svg create mode 100644 crates/snowbound/assets/icons/shapes.svg create mode 100644 crates/snowbound/src/media_linux.rs create mode 100644 crates/snowbound/src/media_macos.rs create mode 100644 crates/snowbound/src/recording.rs create mode 100644 tools/test_ink_tools.py create mode 100644 tools/test_recording.py diff --git a/apps/ios/Snowbound/Notebooks.swift b/apps/ios/Snowbound/Notebooks.swift index 89e75a8eabd3b6ded4ebe0a3401653c9859833ff..b5d364c00d259efd0e411d0f5f82a5fce993db43 100644 --- a/apps/ios/Snowbound/Notebooks.swift +++ b/apps/ios/Snowbound/Notebooks.swift @@ -159,6 +159,8 @@ final class Notebook { private(set) var problem: String? /// The Files location being read, held open while the notebook is. private var accessed: URL? + /// Reports other apps' changes to a folder on this device to the library. + private var presenter: FolderPresenter? init(id: UUID = UUID(), name: String, source: Source) { self.id = id @@ -167,6 +169,7 @@ final class Notebook { } deinit { + presenter?.stop() if let handle { sb_library_free(handle) } accessed?.stopAccessingSecurityScopedResource() } @@ -175,39 +178,46 @@ final class Notebook { /// thread either way. func open(done: @escaping () -> Void) { let source = source - background({ () -> (OpaquePointer?, URL?, String?) in + // The library, the Files location held open, why it failed, and a local folder to watch. + background({ () -> (OpaquePointer?, URL?, String?, URL?) in var error: UnsafeMutablePointer? switch source { case .server(let server): let library = sb_library_server( server.host, server.share, server.user, Keychain.password(server) ?? "", "", server.root, cacheDirectory.path, &error) - return (library, nil, take(error)) + return (library, nil, take(error), nil) case .files(let bookmark): var stale = false guard let url = try? URL(resolvingBookmarkData: bookmark, bookmarkDataIsStale: &stale) else { - return (nil, nil, "The notebook was moved or deleted.") + return (nil, nil, "The notebook was moved or deleted.", nil) } let accessed = url.startAccessingSecurityScopedResource() ? url : nil + let local = onThisDevice(url) var library: OpaquePointer? // A coordinated read brings a cloud folder's files down before they are read. var coordination: NSError? NSFileCoordinator().coordinate(readingItemAt: url, options: [], error: &coordination) { url in - library = sb_library_open(url.path, cacheDirectory.path, &error) + library = sb_library_open(url.path, cacheDirectory.path, local, &error) } - return (library, accessed, take(error) ?? coordination?.localizedDescription) + return (library, accessed, take(error) ?? coordination?.localizedDescription, local ? url : nil) case .documents(let path): - let library = sb_library_open( - documentsDirectory.appendingPathComponent(path).path, cacheDirectory.path, &error) - return (library, nil, take(error)) + let url = documentsDirectory.appendingPathComponent(path) + let library = sb_library_open(url.path, cacheDirectory.path, true, &error) + return (library, nil, take(error), url) case .path(let path): - return (sb_library_open(path, cacheDirectory.path, &error), nil, take(error)) + let url = URL(fileURLWithPath: path) + let local = onThisDevice(url) + return (sb_library_open(path, cacheDirectory.path, local, &error), nil, take(error), local ? url : nil) } - }) { [self] (library, accessed, problem) in + }) { [self] (library, accessed, problem, watched) in self.accessed?.stopAccessingSecurityScopedResource() self.accessed = accessed + presenter?.stop() + presenter = nil if let handle { sb_library_free(handle) } handle = library + if let library, let watched { presenter = FolderPresenter(watching: watched, for: library) } self.problem = library == nil ? (problem ?? "The notebook can’t be read.") : nil follow() reload(done: done) @@ -244,6 +254,61 @@ final class Notebook { } } +/// Whether `url` is kept on this device, rather than by a file provider that keeps it elsewhere +/// too, as iCloud Drive does. +private func onThisDevice(_ url: URL) -> Bool { + let values = try? url.resourceValues(forKeys: [.volumeIsLocalKey, .isUbiquitousItemKey]) + return values?.volumeIsLocal == true && values?.isUbiquitousItem != true +} + +/// Tells a library which files of its folder on this device changed, as file coordination +/// reports other apps' writes; the library checks just those sections. +final class FolderPresenter: NSObject, NSFilePresenter { + let presentedItemURL: URL? + let presentedItemOperationQueue: OperationQueue = { + let queue = OperationQueue() + queue.maxConcurrentOperationCount = 1 + return queue + }() + private let root: String + private let library: OpaquePointer + + init(watching url: URL, for library: OpaquePointer) { + presentedItemURL = url + root = url.standardizedFileURL.resolvingSymlinksInPath().path + self.library = library + super.init() + NSFileCoordinator.addFilePresenter(self) + } + + /// Stops reporting, before the library goes. + func stop() { + NSFileCoordinator.removeFilePresenter(self) + presentedItemOperationQueue.waitUntilAllOperationsAreFinished() + } + + func presentedItemDidChange() { touched("") } + func presentedSubitemDidChange(at url: URL) { touched(url) } + func presentedSubitemDidAppear(at url: URL) { touched(url) } + func presentedSubitem(at oldURL: URL, didMoveTo newURL: URL) { + touched(oldURL) + touched(newURL) + } + func accommodatePresentedSubitemDeletion(at url: URL, completionHandler: @escaping (Error?) -> Void) { + touched(url) + completionHandler(nil) + } + + private func touched(_ url: URL) { + let path = url.standardizedFileURL.resolvingSymlinksInPath().path + touched(path.hasPrefix(root + "/") ? String(path.dropFirst(root.count + 1)) : "") + } + + private func touched(_ path: String) { + sb_library_touched(library, path) + } +} + /// The notebooks the list shows, kept across launches. enum Notebooks { private struct Entry: Codable { diff --git a/arc/canvas.md b/arc/canvas.md index 22f8e65311ad6a893d32cd6e473959a137c4b7f2..d9649cde3a02bdc6d6abf5e91cd1f7d695164361 100644 --- a/arc/canvas.md +++ b/arc/canvas.md @@ -106,6 +106,33 @@ on the page itself, as OneNote 2010 places it: the same column at the caret's grid point, outside any outline. It selects, drags on the grid, deletes and moves with Insert Space as a picture does, and opens and saves as above. +## Recordings + +Record Audio works as OneNote 2010's does. The caret's paragraph splits around +a line saying when recording started, in OneNote's grey `cite` style, and the +caret goes on below it. Text written while recording links to the moment it +was written. On Stop, the file goes in above that line, named after the page. +Hovering a linked note or a recording shows OneNote's blue play button in the +margin, and a click asks the host to play from that moment. The host records a +16 kHz WAV and stores it as IMA ADPCM, which OneNote plays. + +## Drawing + +The Draw tab's tools work as OneNote 2010's do with a mouse (`corpus/ink-tools`). +Every stroke and every shape is a drawing of its own, added on top of the page +when the pen lifts: one edit and one undo step, published as one appended +revision, or in one with the strokes queued behind it during a sync round trip, +as keystrokes are. Shapes are ink too. Their corners snap to the placement grid, and the +drawing keeps the shape's kind and anchors beside its strokes, which OneNote +edits it by. A highlighter's rectangular tip multiplies what lies beneath, so +text under it stays dark. The stroke eraser takes whole drawings, or only the +strokes it touches in an older drawing of several. The lasso picks the drawings +with most of their points inside it. They move by their offset, as OneNote +moves ink, and Delete removes them. Escape returns to Select & Type, where a +click on ink picks it. The default pen draws in the open section's accent at +the light theme's shade, so a stroke stores one real colour and shows it in both +themes. + ## Content the editor doesn't understand Nothing is lost for being unfamiliar. A paragraph the canvas can't draw diff --git a/arc/file-format.md b/arc/file-format.md index 76fbfd1bd3251b488fe3633c138eea344ba26eaa..93c4b0a6ad1a24a6a439e9cd114793bf46c97ca3 100644 --- a/arc/file-format.md +++ b/arc/file-format.md @@ -165,11 +165,14 @@ MS-ONESTORE and MS-ONE are good, but they are not the whole truth: stores beside every author name. Snowbound writes them too. - Some things the spec requires aren't needed by OneNote to open a file. Snowbound writes them anyway, because other readers exist. -- Ink, equations and media recordings are absent from MS-ONE entirely. Ink and - math were reverse-engineered, each against an independent oracle. For ink, - the stroke extents from OneNote's own export. For math, the MathML it exports, - matched byte for byte. Recordings and embedded objects are read and kept, but - never authored. +- Ink and equations are absent from MS-ONE entirely, and so is the link from a + note to a moment in a recording. Ink and math were reverse-engineered, each + against an independent oracle. For ink, the stroke extents from OneNote's own + export, and for the Draw tab's pens, highlighters and shapes, the objects + OneNote stored while drawing them in the lab (`corpus/ink-tools`). For math, the MathML it exports, matched byte for byte. Recordings + and their links follow what OneNote stored while recording in the lab, read + back through its XML export (`corpus/recording`). Embedded objects are read + and kept, but never authored. Readers stay tolerant (files in the wild are older, odder, or written by other tools), while the writer stays strict. diff --git a/arc/sync.md b/arc/sync.md index b3676e680cbc3b2a1db55e0cd82f7918be8b9835..2598bbeaca1010a76926c3e0d018fcf176ad68af 100644 --- a/arc/sync.md +++ b/arc/sync.md @@ -16,7 +16,7 @@ happens when the share is busy, changed or gone. ```text UI thread (winit, UIKit) section thread sync thread ──────────────────────── ────────────────────────────── ────────────────────────────── -editor emits Edit (ops) ───► apply to the parsed Section poll the stamp (header + length) +editor emits Edit (ops) ───► apply to the parsed Section read the stamp (header + length) and returns at once write each burst to SQLite unchanged: ask for a seal, page reads ◄──────────────── answered from the Section publish it events ◄──────────────────── Changed, Rejected, published changed: read once, ask @@ -33,8 +33,9 @@ events ◄──────────────────── Changed, fresh thread that builds its own section while the old one keeps answering reads, so opening a page never waits on the network. - **The sync thread** (`notebook::worker`, stepping `notebook::sync`) does all - network I/O. It polls, publishes and rereads. When the queue is idle it - reads nothing but the header. + network I/O. It reads the stamp when a local edit waits or the notebook's watch + reports the file (every two seconds where nothing watches), then publishes or + rereads. Idle, it reads nothing. `notebook::session` wraps this up for apps. `Notebook` covers discovery and structure, `Section` covers one section's pages, edits, events and conflict @@ -88,7 +89,7 @@ else: ``` A batch stays open while the sync thread is busy, so keystrokes that arrive -during one round trip publish together in the next. Polling costs a +during one round trip publish together in the next. A check costs a kilobyte-sized read. A publication costs one appended revision, not the section. @@ -159,7 +160,7 @@ client with the account the system keeps for that mount. It falls back to the mount only when it can't sign in that way. A file's identity is its root object space, not its server file ID, which -changes when maintenance replaces the file. Polling opens by path each time +changes when maintenance replaces the file. A check opens by path each time for the same reason. ## Offline @@ -180,31 +181,48 @@ OneNote keeps every section of an open notebook in sync and on this computer, no the one on screen. Watched in the lab with a notebook of 200 sections on Samba, OneNote 2010: -- reads every section file once when the notebook opens (all 200 within 15 seconds), so a - section never shown before opens with the share gone; +- reads every section file once when it first opens the notebook (all 200 within 15 + seconds), so a section never shown before opens with the share gone; opening it again + with its cache warm, it only lists the folders and reads the files listed otherwise; - arms one CHANGE_NOTIFY on the notebook's folder, with WATCH_TREE and a filter of names, attributes, size and last write, and otherwise sends nothing: 22 idle minutes put no SMB - request on the wire, not even an echo; + request on the wire, not even an echo, and the section on screen waits on the same watch; - lists the folder once when the server reports a change, and reads only the section that changed, some seconds later; - tries a section it could not reach again about every 31 seconds. -Snowbound's `session::Background` does the same, with one thread per notebook: +Snowbound does the same. Opening a notebook lists its folders and reads only the files whose +listed size or last write time changed since it last read them, keeping what it took from +each, with the file's stamp, in the cache (`discover::Cache`). On a share, a changed section +whose replica already holds it as it stands, as after its own edits published, costs just a +stamp read. `session::Background` then keeps the sections in sync, one thread per notebook: ```text -share ─ CHANGE_NOTIFY (tree) ─► the sections it names are due in 1 s ─► stamp ─► moved: rebase -first pass: one section every 100 ms; a section without a replica gets its offline copy -failing: again in 31 s otherwise: again in an hour, against a lost notification +connect: arm the watch, list every folder + listed as before: compare the known stamp with the replica, locally + listed otherwise: check, one section every 100 ms +share ─ CHANGE_NOTIFY (tree) ─► the file it names is due in 1 s ─► stamp ─► moved: rebase + └► a folder it names is listed in 1 s ─► the files listed otherwise +failing: again in 31 s otherwise: again in an hour, against a lost notification ``` A check costs one stamp read. Only a section whose replica has edits waiting, or whose file moved past the replica's base, has its replica opened for the usual sync steps, and it is -closed again afterwards; the open section is left to its own session. A connection that -drops takes its watch with it, so the next one checks every section again. A notebook in a -folder on this computer has no copies and learns of changes from FSEvents or inotify; one on -a network volume the system mounted, whose server's changes those never report, keeps copies -and is checked every 15 seconds. Closing a notebook deletes its copies, except any with -edits still waiting. +closed again afterwards; a section without one gets its offline copy. The section open on +screen is left to its session, whose worker no longer polls: the watch wakes it, and once the +session lets go of the replica the background checks the file at once. A connection that drops +takes its watch with it, so the next one lists every folder again, and so does working online +again. + +A notebook in a folder on this computer has no copies and learns of changes from FSEvents or +inotify. One on a network volume the system mounted keeps copies. A Mac's SMB mount reports +another client's change to FSEvents only for a folder watched in its own right, and names just +that folder, so every folder of the notebook is watched and a report lists it. A Linux SMB +mount reports none of another client's changes to inotify, so there each section's stamp is +read every 15 seconds. On iOS, a folder on the device (Snowbound's own, or On My iPhone) has no +copies and learns of other apps' writes through file coordination (`NSFilePresenter`); one a +file provider keeps elsewhere, as iCloud Drive does, keeps copies and is checked every 15 +seconds. Closing a notebook deletes its copies, except any with edits still waiting. ## Notebook structure diff --git a/corpus/ink-tools/README.md b/corpus/ink-tools/README.md new file mode 100644 index 0000000000000000000000000000000000000000..1d2dae926c8d8c5c078f5bb98b974d1a0707b2f1 --- /dev/null +++ b/corpus/ink-tools/README.md @@ -0,0 +1,55 @@ +# The Draw tab's tools + +`native-ui/notebook/ink.one` is OneNote 2010's own work in a lab clone, drawn with the +mouse from the Draw tab (screenshots of the tab, the pen gallery and Color & Thickness +were taken alongside): + +- "Pens": the first favourite pen (black, 35 HIMETRIC wide), a second stroke drawn at once + beside it, the red thick pen, the yellow highlighter, and shapes: rectangle, line, + arrow and oval with the default shape pen (black, 50 wide). +- "Gallery": one stroke with each of the 14 favourite pens in the gallery's order. +- "Edits": four strokes, the second removed by the stroke eraser, the third lassoed and + dragged 60 by 20 pixels, the fourth lassoed and deleted, then a rectangle and a line. +- "Shapes2": arrows at other angles and pen widths, a rectangle moved after drawing, and + lines in each of Color & Thickness's nine widths. + +What it shows, and `crates/onestore/tests/page_ink.rs` pins: + +- Every stroke and every shape is an ink container of its own, on top of the page; OneNote + never adds a stroke to an earlier drawing. Erasing or deleting removes the container; + moving sets its offset and leaves the strokes as they were. +- A pen's drawing attributes ignore pressure. A highlighter's tip is a 70 by 400 + rectangle (pen tip 1) with transparency 127 and raster operation 9 (MaskPen), and its + stroke's bias is 2. Strokes are numbered through the page. +- Pen widths are 25, 35, 50, 70, 100, 150, 200, 350 and 500 HIMETRIC. The favourites are + black, red `0x241ced`, blue `0xbb6531`, green `0x367d17` and grey `0x808080` at 35 and + 50, and yellow, cyan, green and magenta highlighters; black is stored as no colour. +- A shape's container stores 2 in `0x14001d4e` (1 for a drawn stroke), its kind in + `0x0c001d4f` (11 a line or arrow, 12 a closed shape) and its geometry: a line's ends in + half inches (`0x1c001dac`), or a closed shape's transform from the unit square in half + inches and its anchors in the unit square (`0x1c001daa`: 8 for a rectangle, 4 for an + oval). Its strokes carry only their path and pen, whose dimension table spans the whole + 32-bit range at resolution 1000. A rectangle is drawn clockwise from its top left, an + oval in 50 steps clockwise from its right end, and an arrow's head is a second stroke of + two barbs at atan(1/2) to the line, 8 points plus twice the pen width long. Shape + corners snap to the 18 pt grid. + +`native-ui/read` is a fresh OneNote 2010 read of that file. + +`drawn/candidate` is the page view's output for +`drawing_tools_store_each_stroke_and_undo_it_in_one_step` in +`crates/canvas/tests/ink_tools.rs` (`CANVAS_INK_EXPORT`): a pen stroke in an accent +colour, a yellow highlighter, and a rectangle, oval, arrow and line dragged out with the +shape tools, each its own edit. `drawn/cold` is a fresh OneNote 2010 read: six +`InkDrawing`s, the four shapes with their `ShapeInfo` anchors, drawn as Snowbound drew +them (`page-000.png`). + +`edited/candidate` is `onenote_drawings_take_erasing_moving_and_more_strokes` in +`page_ink.rs` (`ONESTORE_INK_TOOLS_EDIT_EXPORT`): the native file with the red zigzag on +"Pens" deleted, the vertical line moved by (36, 18), and the six drawings above added +below. `edited/cold` shows OneNote reading OneNote's own drawings where they were, the +moved one offset, and Snowbound's beside them. + +`tools/test_ink_tools.py` checks all three without a VM. Regenerate the candidates with +the export variables set to new absolute directories, then cold-open each with +`tools/native_runner.py OUTPUT COLD --expected-pages N --collect-notebook --screenshots`. diff --git a/corpus/ink-tools/drawn/candidate/Open Notebook.onetoc2 b/corpus/ink-tools/drawn/candidate/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..7da8953bfac2ead471711eb44ff5cc52017cf500 GIT binary patch literal 1596 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 F{{(#I$kPA- literal 0 HcmV?d00001 diff --git a/corpus/ink-tools/drawn/cold/commands.jsonl b/corpus/ink-tools/drawn/cold/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..4b84ef09c35aae7e45b709dc8ddb7216c9110285 --- /dev/null +++ b/corpus/ink-tools/drawn/cold/commands.jsonl @@ -0,0 +1,3 @@ +{"command": "powershell -NoProfile -Command \"Expand-Archive -LiteralPath C:\\one-tests\\transfer.zip -DestinationPath C:\\one-tests\\runs\\capture\\notebook\"", "exit": 0, "stdout": "", "stderr": "", "error": null} +{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-68F30CFB -ExpectedPages 1 -KeepOpen", "exit": 0, "stdout": "Read 1 sections and 1 pages.\r\n", "stderr": "", "error": null} +{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null} diff --git a/corpus/ink-tools/drawn/cold/machine.json b/corpus/ink-tools/drawn/cold/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..e31a59add1b8aeb9f7c840630c8594ec15f09b9d --- /dev/null +++ b/corpus/ink-tools/drawn/cold/machine.json @@ -0,0 +1 @@ +{"name": "m6-68f30cfb", "hostname": "ONE-M6-68F30CFB"} diff --git a/corpus/ink-tools/drawn/cold/notebook/Open Notebook.onetoc2 b/corpus/ink-tools/drawn/cold/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..23a94199a2da742810c7ccbc0041e4bab5ebe3e8 GIT binary patch literal 3748 zcmZ3e|KIuM`!b(-j|5(YE0x77H#D=ZIq}{R z+Y{? zNQa3vwv#%8MWSJLp@V@1q>F*U1BfAB0P#RNK(ZcCSqYFV zSR5n+!jT3*FGJZfK(iQ@K+7tSf!@1}n+}T}>W2AW2&fPQcz}*UcOb|x0SAT&2~exR zrUS*m1st@DLpM(tWEc=|OaSVGmWu&EJ~p#iaM+2>d{8`rJOy$usO$#W1EN80T(+%# z$`X(y2teH0fIaOr9Agi@TDZV%ao&Tc7h`VlfPDzmggxy5%?GC)kkL%w^aDzRAX~T0 zy9Xp;VF+>?$Rz0(4GkNYPCvySp6j}%-Ru1EDn}WxIF_{2Ft^*RBH%6~NS=W~QKSKs zPJjdul$QO{njv=6sWGr-<%_Ror&J9f;vkDbY>*ZZ)(~iD=mF|s&WI}yie2WiPQGwk zwfp;NAUQ{ihMpE+w6kylb+PIOH1ya2d7Q9h0W_c`g`o#Ipn;kg7-}>be#roh1#?D; o(GVC70cZ#yXBAkP2g>)NKn%)onsYy7K$VVCqaiRF0wW*<0LbDsw*UYD literal 0 HcmV?d00001 diff --git a/corpus/ink-tools/drawn/cold/notebook/ink.one b/corpus/ink-tools/drawn/cold/notebook/ink.one new file mode 100644 index 0000000000000000000000000000000000000000..4d05e7d4e2113cb6c388c41307320166f37e0905 GIT binary patch literal 11656 zcmeHNe_YSk8b9aTZ$b#sH@+pyk6k~mSQG18LiJVhV-uSVGsAUBV#M?rV@;_LDz?#& z$?xjckQgyD*}d($$VPtLY_63t*|;w5^Zxv(Z{I%o*8TCvUay{B=W~8M=Q-y&&-0w; zJm-8XAM@IhxYF&xdvnGvD(SKDtZ>q1?74Ycho{fEdgGhdO6{2m_*_nPa9(>qFten? z+R9UVOM4RDI4Rwq5}>S68suDQSDL#DE39^(dKA3p+l$xF|M7!Bcz`_X4C<6E zm&Z~0Xmg1wMj@8-nBUcIC=R@q5?C5GZ1ZI|?*juI&Rfdp!h~V|F6-v`r)|)tyTv`tvntFy}G4(N&QHDkZ&!k)+2@wctk zx;dx%r6$eWmOW(Ev^rz66IH+7&xv{^1HA!xlbP^f_KTNZ-Y`2bwOj7V9Vb@I!}x9v zOmHV0`W(_)zq~8bIcRn1oFP36AGx9(eZF2Zp#6z&dl{0xU-zlqn&gu1M3WiGVwmh0 zyr@qzN;b?|&o>T^fR zRL3Jug!Y8ztP9y^EolhWqV3)z`&I1mDcL1#8P@aoQE8ZeLBEjL!q*EbUdej#IcoD~ z5o`*3hs|L+HdFkLrSDv7nM&UURGvn?6pI#TtFeBe1L5pg6wQIXvcQFDA5a=hX(a7~ zU`ipBMp5!As2mUKmXv&H+(+Ofl(a^?a7XGc+X@W`&{K}2Oc3gun$l6iSx{?7>e5M0*5W+S0p?lc!gPx% z*^w-iwQ>|hzMP51QtaP~*io7UTm z=4?*$ff{x&lr%n@v^|yNKa=XPCRXid+KM&F&pf~TK=Y1cg8Ww9^ZaLVTwxpV@Zb2E zENf|S=n2Aq!Pw6nm673uXh*3pGx9TiZK#DHkrMcAUW-yjP9_9Q(%u-Uv!xaS_%_Vj z!WbDQBSQ%R<+fCiU*xfvEEx&wht@fxJ&euHr<_e5Qu);u_kHiD_VMuYK<5&(k z-NEP#d42bb;mCyhd6lo`983H7E#GBujKNlX}#ArFQs5+NvT5)TE}_`uyTYA@hsWd>=T)=xR795TcU&Vt02c5=