Meridian Notes — записные книжки
Документ приложения по соглашениям раздела 8 библии (
docs/00-foundation.md). Версия 0.1 · 2026-10-03. Фаза 5 · редакции Home и выше (Feature::Notes) · формат.mnx(urn:meridian:office:notes:1) · цвет--app-notes#7048E8· значок «закладка» · идентификаторы экрановNT-xx· макетmockups/notes.html.
Содержание: 1. Назначение и аналоги · 2. Функции · 3. Экраны · 4. Модель данных · 5. Алгоритмы и скрипты · 6. Форматы · 7. Горячие клавиши · 8. Критерии готовности · 9. Риски · 10. Вопросы к библии.
1. Назначение и аналоги
Meridian Notes — записные книжки со свободным холстом: книжки →
группы разделов → разделы → страницы → подстраницы. На странице в любом
месте размещаются контейнеры (текст, таблицы, изображения, вложения,
записи, формулы, вырезки), поверх — рукописные штрихи. Всё хранится
локально в открытом пакете .mnx, индексируется и ищется
офлайн (включая OCR изображений и распознанную рукопись),
синхронизируется через Meridian Cloud (фаза 6) или любое файловое
облако.
| Аналог | Сильные стороны | Слабости, которые закрываем |
|---|---|---|
| Microsoft OneNote | свободный холст, рукопись, разделы, печать в книжку | закрытый формат .one, синхронизация только через
OneDrive, нет Linux, поиск зависит от облака |
| Evernote | теги, веб-клиппер, OCR, напоминания | подписка, облачное хранение обязательно, нет холста и рукописи |
| Notion | блоки, базы данных, совместная работа | только онлайн, нет офлайн-формата, нет рукописи |
| Apple Notes | скорость, PencilKit, скан документов | только Apple, экспорт ограничен |
| Joplin / Obsidian | Markdown, открытость, плагины | нет свободного холста, рукопись через плагины, слабая работа с изображениями/OCR |
Отличия Meridian Notes: открытый пакет .mnx (страницы —
XML, медиа — файлы), работа без аккаунта, текстовые контейнеры на полном
движке Meridian Write (стили, таблицы, формулы Math, диаграммы Chart),
OCR и распознавание рукописи локально, импорт .enex и
.one/.onepkg, экспорт в PDF/Markdown/HTML, скрипты
MeridianScript.
Границы: не wiki-движок, не менеджер задач (задачи и напоминания живут в Meridian Mail, Notes лишь ссылается на них), не Markdown-редактор (Markdown — формат импорта/экспорта, а не внутренняя модель).
Используемые модули ядра:
text-model/text-layout (Write),
grid-model (таблицы), math,
chart, raster (вырезки, скриншоты,
OCR-предобработка), search (tantivy),
undo/crdt (Yrs), Meridian Print (виртуальный принтер
«Meridian Notes»), Meridian Sync (фаза 6), Meridian Assist (фаза 7,
опционально).
Код: kernel/crates/meridian-notes/ (модель книжки,
холст, штрихи, индекс), codecs/notes/ (.mnx,
.enex, .one, md,
html), shell/src/notes/ (QML/C++: окна,
панели, перо).
2. Функции
Приоритеты: P0 — без этого нельзя выпустить Notes 1.0; P1 — в 1.x; P2 — позже.
2.1 Структура книжек
| Функция | Описание | Приоритет |
|---|---|---|
| Книжки | одна книжка = один пакет .mnx; открыто несколько книжек
одновременно; цвет и значок книжки |
P0 |
| Разделы | плоский список в книжке, цвет вкладки, порядок перетаскиванием | P0 |
| Группы разделов | вложенные группы (глубина ≤ 4), сворачивание | P0 |
| Страницы и подстраницы | до 3 уровней вложенности (как OneNote), перетаскивание, повышение/понижение уровня | P0 |
| Перемещение/копирование | страниц и разделов между книжками с сохранением медиа и ссылок | P0 |
| Сортировка страниц | вручную, по имени, по дате создания/изменения | P1 |
| Избранное | закреплённые страницы/разделы в навигаторе | P1 |
| Недавние страницы | последние 50 (Ctrl+Shift+H) |
P1 |
| Пространства (несколько окон) | открыть страницу в отдельном окне, окно «поверх всех» | P1 |
2.2 Холст и контейнеры
| Контейнер | Описание | Приоритет |
|---|---|---|
| Текст | текстовый контейнер на движке Write: абзацы, стили (Заголовок 1–6, Цитата, Код), списки, выделение цветом, гиперссылки, встроенные изображения, таблицы внутри текста | P0 |
| Таблица | отдельный контейнер-таблица (grid-model): сортировка, ввод формул
=SUM(...) в ячейках (подмножество движка Sheets),
преобразование в лист Sheets |
P0 (формулы — P1) |
| Изображение | PNG/JPEG/WebP/GIF/SVG/HEIC; масштаб, обрезка, поворот; текст с изображения (OCR) копируется; «сделать фоном страницы» | P0 |
| Вложение | любой файл: значок + имя + размер; двойной щелчок открывает; «Печатная копия» — вложение рендерится как изображения страниц (PDF/документы Office через кодеки) | P0 |
| Аудиозапись | запись с микрофона (Opus в .ogg), привязка
штрихов/абзацев ко времени записи («что я писал в этот момент») |
P1 |
| Видеозапись | запись с камеры/экрана (VP9/.webm), те же метки
времени |
P2 |
| Рукописные штрихи | слой чернил страницы; штрихи с давлением, наклоном и временем; могут быть привязаны к контейнеру | P0 |
| Формула | объект Meridian Math (визуальный редактор, LaTeX/UnicodeMath) | P0 |
| Диаграмма | объект Meridian Chart с таблицей данных | P2 |
| Чек-лист | абзац с тегом-флажком (todo), групповые операции
«отметить все», «показать невыполненные» |
P0 |
| Теги на абзацах | именованные теги с значками (Важно, Вопрос, Идея, Контакт, Адрес, Телефон, Критично, свои) | P0 |
| Ссылки | на страницу/раздел/книжку (meridian://notes/...), на
абзац, на документ пакета (.mwx и др. с позицией), внешние
URL; обратные ссылки (панель «Ссылки на эту страницу») |
P0 (обратные — P1) |
| Веб-вырезка | HTML-фрагмент/статья/целая страница/закладка из расширения браузера; очистка разметки, сохранение изображений локально | P1 |
| Скриншот | захват области экрана (Win+Shift+N / ⌘⇧N
глобально), вставка с OCR |
P0 |
| Печать в книжку | виртуальный принтер «Meridian Notes»: любое приложение печатает → PDF → страницы-изображения + текстовый слой в выбранный раздел | P1 |
| Файл документа пакета | вставка документа Write/Sheets/Slides как встроенного объекта (редактирование на месте, как OLE) | P2 |
| Дата/время, штамп | вставка текущей даты/времени, штампы «сделано», «обсудить» | P0 |
| Фон страницы | цвет, линейка/клетка/точки, размер (бесконечный, A4, Letter), бумага «как в блокноте» | P0 |
Общие операции с контейнерами: перемещение, изменение размера, объединение текстовых контейнеров, разделение, выравнивание по сетке (8 pt), группировка, порядок (вперёд/назад), копирование между страницами с сохранением стилей, отмена/повтор на уровне книжки.
2.3 Рукописный ввод
| Функция | Описание | Приоритет |
|---|---|---|
| Устройства | перо (Wacom, Windows Ink, Apple Pencil через Sidecar/iPad-экран), мышь, тачпад, сенсорный экран; жесты: панорамирование двумя пальцами, масштаб щипком, ластик обратной стороной пера | P0 |
| Перья | ручка, карандаш, маркер (полупрозрачный, за текстом), каллиграфическое перо; толщина 0,25–8 pt, 16 цветов + своя палитра, наборы «моё перо» ×6 | P0 |
| Давление и наклон | толщина = f(давление), для карандаша прозрачность = f(наклон); сглаживание и предсказание (prediction) | P0 |
| Ластик | штрих целиком / часть штриха (разрезание полилинии) / всё на странице | P0 |
| Лассо | выделение штрихов (и контейнеров) произвольной областью; перемещение, масштабирование, поворот, копирование, изменение цвета/толщины выделенного | P0 |
| Фигуры | распознавание нарисованных фигур (линия, стрелка, прямоугольник, эллипс, треугольник) → векторные фигуры Draw; линейка и транспортир на холсте | P1 |
| Рукопись → текст | преобразование выделенных штрихов или всей страницы в текстовый
контейнер; движки: Windows Ink (InkAnalyzer), Apple
(PencilKit + Vision VNRecognizeTextRequest), локальная
модель ONNX (все ОС, офлайн; обязательно для Linux) |
P0 (локальная модель — P1) |
| Рукопись → формула | распознавание рукописных формул → объект Math (LaTeX) | P2 |
| Поиск по рукописи | фоновое распознавание штрихов (без замены) → индекс | P1 |
| Выпрямление строк | «выровнять рукопись»: сдвиг штрихов к базовой линии | P2 |
| Режим «только перо» | игнорировать касание ладонью (palm rejection), блокировка панорамирования пальцем | P0 |
2.4 Текст и форматирование
Весь текст — модель Write: стили абзацев и символов, списки
(маркированные, нумерованные, многоуровневые), таблицы, гиперссылки,
встроенные формулы, проверка орфографии (Meridian Lingo), автозамена,
автоформат (- → список, [] → флажок,
# → Заголовок 1, 1. → нумерация, ``` → код),
вставка из буфера с сохранением/очисткой форматирования, «вставить как
текст», «вставить как ссылку на документ». Подмножество ribbon Write:
вкладка «Главная» Notes содержит стили, шрифт, абзац, теги, рукопись →
текст.
2.5 Поиск
| Функция | Описание | Приоритет |
|---|---|---|
| Полнотекстовый | по тексту контейнеров, именам страниц/разделов, тегам, именам вложений; области: страница / раздел / книжка / все открытые книжки; морфология (стемминг ru/en + 20 языков tantivy), транслит, опечатки (fuzzy 1–2) | P0 |
| По содержимому изображений | OCR фоновой очередью; результат попадает в индекс и копируется как текст | P0 |
| По рукописи | текст распознанных штрихов (без преобразования) в индексе | P1 |
| По вложениям | извлечение текста из PDF/docx/xlsx/pptx через Meridian Codecs | P1 |
| Фильтры | тип контейнера, тег, автор, дата изменения/создания, «есть вложение», «есть рукопись» | P0 |
| Результаты | список с подсветкой фрагментов, переход на страницу с подсветкой на
холсте, «найти далее» в странице (Ctrl+F) |
P0 |
| Поиск по тегам | сводка всех тегов книжки/всех книжек, группировка по тегу/разделу/дате; «создать страницу-сводку» | P0 |
| Поиск из Hub | индекс Notes доступен Hub через брокер (экран HUB-08) | P1 |
2.6 Теги
Библиотека тегов книжки (имя, значок из набора Meridian Glyphs, цвет,
горячая клавиша Ctrl+1…9), глобальные теги пользователя
(копируются в книжку при первом применении), тег-флажок
(todo) с состоянием, теги на абзацах и на страницах, панель
«Сводка тегов» (NT-07), фильтр страниц по тегу, экспорт сводки в
Write.
2.7 Версии и корзина
| Функция | Описание | Приоритет |
|---|---|---|
| Версии страниц | снимок при каждом «тихом» сохранении, если прошло ≥ 5 мин с предыдущего снимка или изменил другой автор; ручная версия («Сохранить версию»); сравнение версий (подсветка добавленного/удалённого текста, новых штрихов); восстановление/копия | P0 |
| Хранение | 50 версий или 90 дней на страницу (настраивается), старые сворачиваются по правилу «1 в день → 1 в неделю» | P0 |
| Корзина | удалённые страницы и разделы (с подстраницами и медиа) — в
trash/ пакета, 60 дней, восстановление на место,
очистка |
P0 |
| История книжки | список операций (кто/когда/что) по журналу CRDT — при включённой совместной работе | P1 |
2.8 Защита
| Функция | Описание | Приоритет |
|---|---|---|
| Пароль на раздел | шифрование файлов раздела (AES-256-GCM, ключ — Argon2id от пароля); разблокировка на сессию с автоблокировкой через N минут; защищённые разделы не индексируются, пока заблокированы | P0 |
| Смена/снятие пароля | перешифрование, запрет восстановления без пароля (предупреждение) | P0 |
| Биометрия | Windows Hello / Touch ID для разблокировки (ключ в хранилище ОС) | P1 |
| Подпись страницы | подписанная версия (Ed25519 ключом аккаунта) — «заверенная запись» | P2 |
2.9 Шаблоны страниц
Галерея: встроенные (Пустая, Конспект лекции, Протокол совещания,
Задачи на день, Корнелл-заметки, Планировщик недели, Список покупок,
Дневник, Исследование, Карточка проекта), свои (сохранить страницу как
шаблон), корпоративные (из общей папки/Cloud), шаблон раздела по
умолчанию («новая страница в этом разделе = Протокол»). Шаблон — XML
страницы без идентификаторов + медиа; отдельного расширения файла нет
(библия: Notes без формата шаблона) — шаблоны хранятся в
templates/ пакета и в профиле пользователя.
2.10 Импорт и экспорт
| Направление | Формат | Объём | Приоритет |
|---|---|---|---|
| Импорт | Evernote .enex |
заметки → страницы, notebook → раздел, теги, ресурсы (изображения/вложения), чек-боксы, таблицы, даты создания/изменения, геометки в свойства | P0 |
| Импорт | OneNote .one / .onepkg (MS-ONE,
MS-ONESTORE) |
разделы, страницы, подстраницы, outline-контейнеры с текстом и
форматированием, изображения, вложения, штрихи (MS-ONE
InkStroke), теги; ограничения — см. раздел 6 |
P1 (R*) |
| Импорт | папка Markdown (Obsidian/Joplin-стиль) | файлы → страницы, подпапки → разделы/подстраницы,
[[wikilinks]] → ссылки на страницы, вложения из
attachments/, фронтматтер → свойства |
P0 |
| Импорт | HTML (файл/папка, Evernote HTML-экспорт, Notion-экспорт) | страница → текстовый контейнер + изображения | P0 |
| Импорт | Apple Notes (экспорт в HTML/папку) | как HTML | P2 |
| Экспорт | PDF (страница/раздел/книжка; «как на экране» с бесконечной страницей разбивается на листы) | через Meridian Print, PDF/A | P0 |
| Экспорт | Markdown (папка: страница → .md, медиа →
assets/, теги → фронтматтер, штрихи → SVG) |
P0 | |
| Экспорт | HTML (одиночный файл с встроенными медиа или папка) | P0 | |
| Экспорт | .mnx раздела/страницы (частичный пакет) |
P0 | |
| Экспорт | Write .mwx (страница → документ) |
P1 |
2.11 Синхронизация и совместная работа (фаза 6)
| Функция | Описание | Приоритет |
|---|---|---|
| Файловые облака | книжка в папке OneDrive/Dropbox/Яндекс Диск и т.п.: пакет разбит на файлы по страницам, конфликты на уровне страницы разрешаются слиянием (раздел 5.8) | P1 |
| Meridian Cloud | потоковая синхронизация по журналу операций (Yrs), выборочная синхронизация разделов, медиа по требованию | P1 (фаза 6) |
| Общие книжки | доступ по ссылке/приглашению, роли (просмотр/комментирование/редактирование), курсоры и присутствие, «кто менял» по авторам, уведомления об изменениях в Hub | P1 (фаза 6) |
| Комментарии | ветки комментариев к абзацам и контейнерам (общая модель рецензирования Write) | P2 |
2.12 Захват
| Функция | Описание | Приоритет |
|---|---|---|
| Быстрая заметка | глобальное сочетание (Win+Alt+N / ⌃⌥N)
открывает мини-окно (NT-14); сохраняется в раздел «Быстрые заметки»
книжки по умолчанию |
P0 |
| Отправить в Notes | из Write/Sheets/Slides/PDF/Mail: «Отправить в Notes» — выделение или документ целиком как контейнер/печатную копию со ссылкой на источник | P0 |
| Веб-вырезка | расширение для Chrome/Firefox/Edge/Safari (WebExtension) и
meridian://notes/clip протокол; режимы: статья, страница
целиком, закладка, выделение, скриншот |
P1 |
| Скан документа | с камеры/сканера (SANE/WIA/ImageCaptureCore): выравнивание, контраст, OCR | P2 |
2.13 Прочее
Печать страниц (Meridian Print, масштаб к листу/по размеру), доступность (чтение структуры контейнеров экранным диктором, навигация клавиатурой между контейнерами), ассистент Assist (P2: «подытожить страницу», «сделать список задач из конспекта», «переписать аккуратно»), ночная тема холста, параметры (NT-18).
3. Экраны
Каркас окна — по разделу 5.3 библии. Ribbon Notes: Файл · Главная · Вставка · Рисование · Вид · Рецензирование · Скрипты + контекстные вкладки «Таблица», «Изображение», «Запись», «Формула».
| ID | Экран | Описание |
|---|---|---|
| NT-01 | Главное окно | Три панели: слева навигатор (книжки → группы → разделы), в
центре-слева список страниц, холст; справа боковая панель (Поиск / Теги
/ Версии / Ссылки / Свойства). Режимы: «Полный», «Только холст»
(F11), «Список страниц справа» (как OneNote 2016). Строка
состояния: книжка · раздел · число контейнеров · режим пера ·
масштаб |
| NT-02 | Навигатор книжек | Дерево: книжка (цвет, значок облака/замка), группы разделов (сворачиваемые), разделы (цветная полоса). Контекстное меню: Новый раздел/группа, Переименовать, Цвет, Защитить паролем, Переместить/Копировать, Экспорт, Удалить. Перетаскивание с автораскрытием. Внизу: «+ Книжка», «Открыть книжку», «Корзина» |
| NT-03 | Список страниц | Страницы раздела с отступами по уровню, дата изменения, значки
(теги, вложения, рукопись, замок); фильтр «только с тегом», сортировка;
«+ Страница» внизу, перетаскивание для переупорядочивания/вложенности
(Tab/Shift+Tab) |
| NT-04 | Холст страницы | Заголовок страницы (крупный, редактируемый) + дата/время создания под ним; бесконечный холст с линейкой фона; контейнеры с рамками при наведении/выделении; маркеры изменения размера; выделение рамкой; масштаб 25–400 %; мини-карта (при холсте > 3 экранов). Пустое место при щелчке создаёт текстовый контейнер |
| NT-05 | Вкладка «Рисование» / режим пера | Галерея перьев (6 слотов + «Добавить перо»), цвет, толщина, ластик (3 режима), лассо, фигуры, линейка, «Рукопись в текст», «Рукопись в формулу» (P2), «Режим только перо». Плавающая мини-панель пера при касании пером. Автопереключение на перо при касании стилуса |
| NT-06 | Панель «Поиск» | Поле запроса, область (страница/раздел/книжка/все), фильтры (тип,
тег, дата, автор), список результатов (книжка › раздел › страница,
фрагмент с подсветкой, дата), «найти далее» на холсте (F3),
индикатор «индексируется… N» |
| NT-07 | Панель «Теги» | Сводка: группировка по тегу/разделу/дате/имени, флажок «показать только невыполненные», «обновить», «создать страницу-сводку», «настроить теги» (редактор библиотеки тегов: имя, значок, цвет, клавиша) |
| NT-08 | Панель «Версии страницы» | Список версий (дата, автор, размер, «ручная/авто»), предпросмотр выбранной, сравнение с текущей (подсветка), «Восстановить», «Открыть как копию», «Удалить версии» |
| NT-09 | Корзина | Список удалённых страниц/разделов с исходным местом и датой; «Восстановить», «Удалить окончательно», «Очистить корзину» (с подтверждением) |
| NT-10 | Диалог защиты раздела | Задать/сменить/снять пароль, подсказка (необязательно), биометрия
(P1), «заблокировать все разделы» (Ctrl+Alt+L), таймер
автоблокировки |
| NT-11 | Галерея шаблонов страниц | Сетка превью: Встроенные / Мои / Корпоративные; предпросмотр справа; «Сделать шаблоном по умолчанию для раздела»; «Сохранить текущую страницу как шаблон» |
| NT-12 | Мастер импорта | Шаг 1 — источник (файл/папка, формат определяется автоматически), Шаг 2 — куда (новая книжка / существующий раздел), сопоставление (notebook → раздел, стек → группа), Шаг 3 — ход (прогресс, журнал предупреждений: «не перенесён объект X»), итог |
| NT-13 | Диалог экспорта | Область (страница/с подстраницами/раздел/книжка), формат (PDF/MD/HTML/.mnx/.mwx), параметры (разбиение на листы, встроить медиа, штрихи как SVG/PNG), целевая папка |
| NT-14 | Быстрая заметка | Мини-окно 360×280 без ribbon: текст, перо, вставка изображения; «поверх всех»; при закрытии сохраняется в «Быстрые заметки»; кнопка «Открыть в Notes» |
| NT-15 | Приём веб-вырезки | Диалог при получении meridian://notes/clip:
предпросмотр, выбор книжки/раздела (запоминается), теги, «добавить к
последней странице» |
| NT-16 | Печать в книжку | После печати на виртуальный принтер: выбор книжки/раздела, «одна страница на лист / все на одной», «распознать текст», «запомнить выбор» |
| NT-17 | Общий доступ (фаза 6) | Диалог: ссылка, приглашения по e-mail, роли, срок, «кто имеет доступ», журнал изменений; значки присутствия на холсте |
| NT-18 | Параметры Notes | Вкладки: Общие (книжка по умолчанию, раздел быстрых заметок, автосохранение), Холст (фон, сетка, единицы), Перо (слоты, palm rejection, предсказание), Распознавание (движок: ОС/локальная модель, языки, OCR в фоне, лимит CPU), Поиск (индексировать вложения), Версии (политика хранения), Синхронизация |
| NT-19 | Рукопись → текст | Диалог: распознанный текст с подсветкой неуверенных слов (альтернативы по клику), язык, «заменить штрихи / добавить рядом / только в буфер» |
| NT-20 | Панель записи | Запись аудио/видео: уровень, таймер, пауза/стоп; после записи — контейнер-плеер с метками: щелчок по абзацу/штриху перематывает к моменту его создания |
| NT-21 | Свойства страницы | Имя, создана/изменена, автор, размер, фон, шаблон, теги страницы,
ссылка meridian://, число версий, «Ссылки на эту
страницу» |
| NT-22 | Диалог ссылки | Вставка ссылки: вкладка «Страница» (поиск по книжкам), «Документ» (файл + позиция), «URL»; текст ссылки |
| NT-23 | Распознавание фигур | Всплывающая подсказка после рисования фигуры: «Преобразовать в фигуру? [Да] [Нет] [Всегда]» |
4. Модель данных
4.1 Пакет .mnx
Одна книжка = один ZIP-пакет MOP (раздел 3.1 библии). Страницы хранятся отдельными XML-файлами, чтобы файловые облака синхронизировали и сливали книжку погранично (одна страница — один файл), а медиа — по хэшу содержимого (дедупликация).
notebook.mnx (ZIP, mimetype = application/vnd.meridian.notes)
├── mimetype
├── manifest.xml ← список частей, версии, типы, хэши
├── meta.xml ← Dublin Core + notes:* (цвет, значок, язык, устройство-создатель)
├── notebook.xml ← дерево: группы разделов → разделы → порядок страниц
├── styles.xml ← стили текста (как в Write) и библиотека тегов
├── settings.xml ← вид, шаблон по умолчанию, политика версий
├── sections/
│ └── <section-ulid>/
│ ├── section.xml ← имя, цвет, порядок страниц, защита (если есть)
│ ├── pages/<page-ulid>.xml
│ └── locked/<page-ulid>.xml.enc ← зашифрованные страницы защищённого раздела
├── media/<sha256>.<ext> ← изображения, вложения, записи (stored, без сжатия)
├── objects/<ulid>.<meq|msx|mvx> ← встроенные объекты Math/Sheets/Draw
├── ocr/<sha256>.json ← результаты OCR (слова, boxes, уверенность) по медиа
├── ink-text/<page-ulid>.json ← распознанная рукопись (для индекса), не для показа
├── templates/<ulid>.xml ← шаблоны страниц книжки
├── history/
│ ├── pages/<page-ulid>/<ts>.xml.zst ← версии страниц
│ └── ops.yrs ← журнал CRDT (опц., при совместной работе)
├── trash/
│ ├── trash.xml ← что, откуда, когда удалено
│ └── pages/<page-ulid>.xml
├── scripts/ ← MeridianScript книжки (не запускаются без разрешения)
├── signatures/
└── thumbnail.png
Идентификаторы — ULID (сортируемые по времени, 26 символов), что даёт
стабильный порядок и бесконфликтное создание на разных устройствах.
Единица длины на холсте — pt (1/72"), десятичные значения с
2 знаками (см. вопрос 10.1).
4.2 notebook.xml
<notebook xmlns="urn:meridian:office:notes:1" id="01J9W5..." version="1" color="#7048E8" icon="bookmark">
<section-group id="01J9W6.." name="Проекты" collapsed="false">
<section id="01J9W7.." name="Меридиан" color="#1C7ED6">
<page ref="01J9W8.." level="1"/>
<page ref="01J9W9.." level="2"/> <!-- подстраница предыдущей / subpage of previous -->
</section>
<section id="01J9WA.." name="Личное" color="#F2A541" locked="argon2id" hint="кличка кота"/>
</section-group>
<section id="01J9WB.." name="Быстрые заметки" color="#5C636E" quick-notes="true"/>
<recent-pages><page ref="01J9W8.."/></recent-pages>
</notebook>4.3 Страница
pages/<ulid>.xml
<page xmlns="urn:meridian:office:notes:1" xmlns:w="urn:meridian:office:write:1"
id="01J9W8.." title="Встреча с заказчиком" created="2026-10-03T14:02:11Z" modified="2026-10-03T15:40:02Z"
author="am@vektor.ru" template="01J9T1.." size="infinite" background="grid-8" paper="#FFFFFF" lang="ru">
<tags><tag ref="important"/></tags>
<properties><prop name="geo">55.75,37.62</prop></properties>
<!-- Контейнеры в абсолютных координатах (pt), z — порядок / Containers in absolute coordinates (pt) -->
<container id="c1" type="text" x="72" y="96" w="420" h="auto" z="1">
<w:body>
<w:p style="Heading2"><w:r>Повестка</w:r></w:p>
<w:p style="List" tag="todo" done="false"><w:r>Согласовать сроки</w:r></w:p>
<w:p style="List" tag="todo" done="true"><w:r>Выслать КП</w:r></w:p>
<w:p><w:r>См. </w:r><w:a href="meridian://notes/01J9W5../01J9WC..">страницу «Договор»</w:a></w:p>
</w:body>
</container>
<container id="c2" type="image" x="520" y="96" w="300" h="200" z="2" media="media/3f9a…c1.png"
rotation="0" crop="0 0 1 1" alt="Схема стенда" ocr="ocr/3f9a…c1.json"/>
<container id="c3" type="file" x="72" y="420" w="220" h="40" z="3" media="media/8b1e…77.pdf"
name="КП-143.pdf" size="410233" printout="true"/>
<container id="c4" type="audio" x="72" y="480" w="300" h="48" z="4" media="media/aa10…02.ogg"
duration="1832.4" started="2026-10-03T14:05:00Z"/>
<container id="c5" type="table" x="72" y="540" w="400" h="auto" z="5">
<grid cols="3" rows="4"> … urn:meridian:office:sheets:1 (подмножество) … </grid>
</container>
<container id="c6" type="math" x="520" y="320" w="200" h="60" z="6" object="objects/01J9WD...meq"/>
<!-- Слой чернил / Ink layer: strokes bound to the page; optional anchor to a container -->
<ink>
<stroke id="s1" pen="ballpoint" color="#1A1D21" width="1.5" anchor="c1" t0="2026-10-03T14:06:10.120Z"
bbox="80 300 210 340" points="80.00 300.00 512 0;1.20 0.30 530 8;1.10 0.42 540 8;…"/>
<!-- points: первая точка абсолютно, далее дельты: dx dy pressure dt_ms / first abs, then deltas -->
<stroke id="s2" pen="highlighter" color="#F2A541" width="8" opacity="0.4" …/>
</ink>
<ink-text ref="ink-text/01J9W8...json" engine="winink" lang="ru" at="2026-10-03T15:00:00Z"/>
</page>4.4 Структуры ядра (Rust)
// kernel/crates/meridian-notes/src/model.rs
use ulid::Ulid;
pub struct Notebook {
pub id: Ulid,
pub tree: Vec<SectionNode>, // группы и разделы / groups and sections
pub tags: TagLibrary,
pub settings: NotebookSettings,
}
pub enum SectionNode {
Group { id: Ulid, name: String, collapsed: bool, children: Vec<SectionNode> },
Section(SectionMeta),
}
pub struct SectionMeta {
pub id: Ulid,
pub name: String,
pub color: Color,
pub pages: Vec<PageEntry>, // порядок + уровень / order + level
pub lock: Option<SectionLock>, // защита паролем / password protection
}
pub struct PageEntry { pub id: Ulid, pub level: u8 } // 1..=3
pub struct Page {
pub id: Ulid,
pub title: String,
pub created: Timestamp,
pub modified: Timestamp,
pub author: AuthorId,
pub size: PageSize, // Infinite | A4 | Letter | Custom(w,h)
pub background: Background,
pub tags: Vec<TagId>,
pub containers: Vec<Container>, // отсортированы по z / sorted by z
pub ink: InkLayer,
pub props: BTreeMap<String, String>,
}
pub struct Container {
pub id: ContainerId,
pub rect: Rect, // x, y, w; h = Auto для текста / h = Auto for text
pub z: i32,
pub body: ContainerBody,
pub locked: bool,
}
pub enum ContainerBody {
Text(TextDocument), // модель Write / Write model
Table(Grid), // grid-model
Image { media: MediaRef, rotation: f32, crop: RectF, alt: String, ocr: Option<OcrRef> },
File { media: MediaRef, name: String, size: u64, printout: Option<Vec<MediaRef>> },
Audio { media: MediaRef, duration: f64, started: Timestamp },
Video { media: MediaRef, duration: f64, started: Timestamp },
Math(ObjectRef),
Chart(ObjectRef),
Embedded { object: ObjectRef, app: AppId },
WebClip { url: Url, captured: Timestamp, text: TextDocument },
}
pub struct InkLayer { pub strokes: Vec<Stroke> }
pub struct Stroke {
pub id: StrokeId,
pub pen: PenKind, // Ballpoint | Pencil | Highlighter | Calligraphy
pub color: Color,
pub width: f32, // базовая толщина, pt / base width, pt
pub opacity: f32,
pub anchor: Option<ContainerId>, // движется вместе с контейнером / moves with the container
pub t0: Timestamp,
pub bbox: Rect,
pub points: Vec<InkPoint>, // полилиния с давлением и временем / polyline with pressure and time
}
#[derive(Clone, Copy)]
pub struct InkPoint { pub x: f32, pub y: f32, pub pressure: u16 /* 0..=1023 */, pub tilt: (i8, i8), pub dt_ms: u16 }4.5 Индекс SQLite
(<profile>/notes/index.db)
Индекс — производные данные: его можно удалить и перестроить из
пакетов. Полнотекстовый поиск живёт в отдельной директории tantivy
(<profile>/notes/fts/), SQLite хранит метаданные и
очереди.
PRAGMA journal_mode = WAL; PRAGMA application_id = 0x4D4E5831; -- 'MNX1'
CREATE TABLE notebooks (
id TEXT PRIMARY KEY, path TEXT NOT NULL UNIQUE, name TEXT, color TEXT,
opened_at INTEGER, mtime INTEGER, size INTEGER, cloud_id TEXT, state TEXT CHECK(state IN ('ok','missing','locked'))
);
CREATE TABLE sections (
id TEXT PRIMARY KEY, notebook_id TEXT REFERENCES notebooks(id) ON DELETE CASCADE,
parent_group TEXT, name TEXT, color TEXT, ord INTEGER, locked INTEGER DEFAULT 0
);
CREATE TABLE pages (
id TEXT PRIMARY KEY, section_id TEXT REFERENCES sections(id) ON DELETE CASCADE,
title TEXT, level INTEGER, ord INTEGER, created INTEGER, modified INTEGER, author TEXT,
content_hash BLOB, indexed_hash BLOB, -- что в tantivy / what tantivy has
has_ink INTEGER, has_media INTEGER, has_files INTEGER, thumb BLOB
);
CREATE INDEX pages_section ON pages(section_id, ord);
CREATE TABLE containers (
id TEXT, page_id TEXT REFERENCES pages(id) ON DELETE CASCADE, type TEXT,
x REAL, y REAL, w REAL, h REAL, media_hash TEXT, PRIMARY KEY(page_id, id)
);
CREATE TABLE tags (id TEXT PRIMARY KEY, notebook_id TEXT, name TEXT, icon TEXT, color TEXT, hotkey INTEGER);
CREATE TABLE tag_uses (
tag_id TEXT REFERENCES tags(id), page_id TEXT REFERENCES pages(id) ON DELETE CASCADE,
container_id TEXT, para_index INTEGER, text TEXT, done INTEGER, modified INTEGER
);
CREATE INDEX tag_uses_tag ON tag_uses(tag_id, done);
CREATE TABLE links (src_page TEXT, dst TEXT, kind TEXT CHECK(kind IN ('page','doc','url')), text TEXT);
CREATE INDEX links_dst ON links(dst); -- обратные ссылки / backlinks
CREATE TABLE versions (page_id TEXT, ts INTEGER, author TEXT, size INTEGER, manual INTEGER, PRIMARY KEY(page_id, ts));
CREATE TABLE trash (id TEXT PRIMARY KEY, notebook_id TEXT, kind TEXT, name TEXT, origin TEXT, deleted_at INTEGER);
CREATE TABLE ocr_queue (
media_hash TEXT PRIMARY KEY, notebook_id TEXT, page_id TEXT, kind TEXT CHECK(kind IN ('image','ink','file')),
priority INTEGER DEFAULT 5, attempts INTEGER DEFAULT 0, enqueued INTEGER, last_error TEXT
);
CREATE TABLE recent_pages (page_id TEXT PRIMARY KEY, opened_at INTEGER);
CREATE TABLE sync_state (notebook_id TEXT PRIMARY KEY, remote_id TEXT, cursor BLOB, last_sync INTEGER, pending INTEGER);4.6 Схема индекса tantivy
| Поле | Тип | Назначение |
|---|---|---|
page_id |
STRING, stored, fast | ключ документа (один документ tantivy = одна страница) |
notebook_id, section_id |
STRING, fast | фильтры области |
title |
TEXT (tokenizer meridian_ru_en), boost 3 |
имя страницы |
body |
TEXT, positions | текст всех контейнеров, разделённый \n |
ink |
TEXT | распознанная рукопись |
ocr |
TEXT | текст изображений |
attach |
TEXT | текст вложений |
tags |
STRING multi, fast | идентификаторы тегов |
kind |
STRING multi | text, image, ink,
file, audio, … |
modified, created |
DATE, fast | сортировка/фильтр |
author |
STRING, fast | фильтр |
Токенайзер meridian_ru_en: Unicode-сегментация →
lowercase → удаление диакритики → стемминг Snowball (язык по полю
lang страницы, по умолчанию ru+en) → транслит-синоним для
латинских запросов (proekt → проект).
4.7 Версии, корзина, защита
- Версия — полный XML страницы, сжатый zstd
(
history/pages/<id>/<ts>.xml.zst); медиа не копируются (ссылки по хэшу, сборка мусора медиа учитывает версии и корзину). - Корзина — страница переносится в
trash/pages/, запись вtrash.xmlс исходным разделом и позицией. - Защищённый раздел:
section.xmlхранитkdf="argon2id" salt m=64MiB t=3 p=1, проверочное значениеverifier = HMAC(key, "meridian-notes-verify"); каждая страница шифруется отдельно AES-256-GCM с nonce из ULID страницы + счётчик версий; медиа раздела шифруются тем же ключом (media/<hash>.enc), хэш считается от открытого содержимого. Ключ в памяти обнуляется при блокировке (zeroize).
5. Алгоритмы и скрипты
5.1 Раскладка контейнеров и автосдвиг при росте
Контейнеры имеют абсолютные позиции. Текстовые контейнеры растут по
высоте при вводе (h = Auto); при росте нижележащие
контейнеры в той же «колонке» сдвигаются вниз (как в OneNote), а не
перекрываются.
/// Пересчёт после изменения высоты контейнера / Re-layout after a container's height changed.
/// Правило: сдвигаем вниз контейнеры, чьи x-интервалы пересекаются с изменённым
/// и чей верх лежал ниже старого низа (в пределах зазора GAP).
pub fn relayout_after_growth(page: &mut Page, changed: ContainerId, old_h: f32, new_h: f32) {
const GAP: f32 = 12.0; // минимальный зазор, pt / min gap, pt
let delta = new_h - old_h;
if delta.abs() < 0.5 { return; }
let c = page.container(changed).rect;
let old_bottom = c.y + old_h;
// Кандидаты: пересекаются по X, расположены ниже / Candidates: overlap in X, located below
let mut moved: Vec<(ContainerId, f32)> = Vec::new();
for other in page.containers.iter().filter(|o| o.id != changed && !o.locked) {
let r = other.rect;
let x_overlap = r.x < c.x + c.w && r.x + r.w > c.x;
if x_overlap && r.y >= old_bottom - 0.5 {
moved.push((other.id, delta));
}
}
// Сдвиг транзитивно: сдвинутый контейнер может толкнуть следующие / Push transitively
let mut queue: VecDeque<_> = moved.into_iter().collect();
let mut done = HashSet::new();
while let Some((id, d)) = queue.pop_front() {
if !done.insert(id) { continue; }
let r = page.container_mut(id).rect;
page.container_mut(id).rect.y += d;
let new_bottom = r.y + d + r.h.max(0.0) + GAP;
for other in page.containers.iter().filter(|o| o.id != id && !done.contains(&o.id)) {
let o = other.rect;
if o.x < r.x + r.w && o.x + o.w > r.x && o.y < new_bottom && o.y >= r.y {
queue.push_back((other.id, new_bottom - o.y)); // ровно столько, чтобы не наложиться
}
}
// Привязанные штрихи едут вместе / Anchored strokes move along
for s in page.ink.strokes.iter_mut().filter(|s| s.anchor == Some(id)) { s.translate(0.0, d); }
}
if delta < 0.0 { /* при уменьшении ничего не подтягиваем — пользователь двигает сам / shrink: no pull-up */ }
}Правила: автосдвиг только вниз (уменьшение не подтягивает), заблокированные контейнеры и фоновые изображения не двигаются, контейнеры в разных колонках (нет пересечения по X) не влияют друг на друга, операция одна в undo-стеке вместе с вводом текста.
5.2 Предотвращение наложений при создании и перетаскивании
- Создание щелчком: контейнер размещается в точке щелчка шириной 300
pt; если прямоугольник пересекает существующий контейнер, новый
сдвигается вниз под него (
y = bottom + GAP) — поиск первой свободной позиции по вертикали в пределах 2000 pt, иначе оставляем как есть. - Перетаскивание: перекрытие разрешено (намеренная раскладка), но при
отпускании с нажатым
Altсрабатывает «притяжение» к сетке 8 pt и к краям соседей (snap lines как в Draw, порог 6 px экрана). - Вставка из буфера: под последним контейнером в текущей колонке.
- Проверка пересечений — по R-дереву страниц (
rstar) при > 200 контейнеров, иначе линейно.
5.3 Сглаживание штрихов и рендер переменной толщины
Ввод: события пера (x, y, pressure, tilt, t). Конвейер:
- Фильтрация дрожания — One-Euro filter
(
fc_min = 1.0 Гц, β = 0.007) по x и y; для мыши — отключён. - Прореживание — Ramer–Douglas–Peucker с ε = 0.15 pt (сохраняя давление: точка не удаляется, если |Δp| > 48 из 1023).
- Сглаживание геометрии при рендере — Catmull-Rom сплайн по точкам (α = 0.5, центрипетальный), 16 сегментов между точками при масштабе ≥ 100 %, меньше при отдалении.
- Переменная толщина — для каждой точки
w_i = width · (0.35 + 0.65 · (p_i/1023)^γ), γ = 1.4 для ручки, для карандаша толщина постоянна, аalpha_i = 0.5 + 0.5·cos(tilt); «калиграфическое» перо — толщина зависит от угла направления относительно 45°. - Геометрия контура — по сплайну строятся две
offset-кривые (нормали ± w_i/2), соединяются круглыми торцами; результат
— замкнутый путь Skia (
Path), заливается цветом (маркер —BlendMode::Multiply, рисуется под текстовыми контейнерами). - Предсказание — при вводе показываем «хвост» на 1 кадр вперёд (линейная экстраполяция по последним двум точкам, 8–12 мс), стирается при приходе реальной точки — снижает воспринимаемую задержку.
- Кэш — завершённые штрихи растрируются в тайлы 512×512 на масштабе; активный штрих рисуется поверх в отдельном слое; при изменении масштаба тайлы перестраиваются лениво.
pub fn stroke_outline(stroke: &Stroke, zoom: f32) -> skia::Path {
let pts = catmull_rom(&stroke.points, segments_for_zoom(zoom)); // Vec<(x,y,w)>
let mut left = Vec::with_capacity(pts.len());
let mut right = Vec::with_capacity(pts.len());
for i in 0..pts.len() {
let (p, w) = (pts[i].pos, pts[i].width);
let dir = tangent(&pts, i); // единичная касательная / unit tangent
let n = Vec2::new(-dir.y, dir.x) * (w * 0.5);
left.push(p + n); right.push(p - n);
}
let mut path = skia::Path::new();
path.move_to(left[0]);
for p in &left[1..] { path.line_to(*p); }
add_round_cap(&mut path, pts.last().unwrap());
for p in right.iter().rev() { path.line_to(*p); }
add_round_cap(&mut path, pts.first().unwrap());
path.close();
path
}Целевая задержка «перо → пиксель» ≤ 20 мс на Windows Ink/Apple Pencil (рендер активного штриха вне очереди сцены QML, прямой блит слоя).
5.4 Лассо-выделение штрихов
- Пользователь рисует замкнутую (замыкаем автоматически) полилинию
Lв координатах холста. - Грубый отбор — штрихи, чей
bboxпересекает bbox(L). - Точная проверка — штрих выбран, если ≥ 50 % его точек (по длине, не
по количеству) лежат внутри
L(чётно-нечётное правило,point_in_polygon); для коротких штрихов (< 10 точек) — если внутри любая точка. - Контейнеры — выбраны, если их центр внутри
L(или ≥ 60 % площади приShift). - Выделение показывается пунктирным контуром по выпуклой оболочке +
маркеры; операции: перемещение (сдвиг всех точек), масштаб/поворот
(аффинное преобразование точек вокруг центра bbox), изменение цвета/
толщины/пера, копирование, удаление, «Рукопись в текст», «Сгруппировать
с контейнером» (установить
anchor). - Сложность O(N·M) при N точках штрихов и M вершинах лассо; при N > 50 000 используем R-дерево по точкам.
5.5 Полнотекстовый индекс tantivy с инкрементальным обновлением
- Один индекс на профиль пользователя; документ = страница;
page_id— уникальный терм для удаления. - Источник событий: сохранение страницы (журнал автосохранения, 2 с),
импорт, восстановление версии, изменение файлов книжки извне
(
notifywatcher по пути пакета), результат OCR/распознавания. - Инкрементальность: сравниваем
pages.content_hash(BLAKE3 от XML страницы + списка хэшей медиа) сindexed_hash; при различии —delete_term(page_id)+add_document; коммит батчами по 50 страниц или каждые 2 с;merge policy— LogMergePolicy с ограничением 8 сегментов; индексирование — в фоновом потоке брокера Hub с приоритетом ниже UI. - Защищённые разделы: страницы удаляются из индекса при блокировке и индексируются во временный индекс в памяти при разблокировке (объединённый поиск по двум индексам).
- Полное перестроение: при несовпадении версии схемы или повреждении
(проверка при старте,
meta.json).
pub fn reindex_page(idx: &IndexWriter, page: &Page, ocr: &OcrStore, ink_text: Option<&str>) -> Result<()> {
idx.delete_term(Term::from_field_text(F.page_id, &page.id.to_string()));
let mut doc = Document::new();
doc.add_text(F.page_id, &page.id.to_string());
doc.add_text(F.title, &page.title);
let mut body = String::new();
for c in &page.containers {
match &c.body {
ContainerBody::Text(t) | ContainerBody::WebClip { text: t, .. } => { body.push_str(&t.plain_text()); body.push('\n'); }
ContainerBody::Table(g) => body.push_str(&g.plain_text()),
ContainerBody::Image { ocr: Some(r), .. } => doc.add_text(F.ocr, &ocr.text(r)),
ContainerBody::File { media, .. } => if let Some(t) = ocr.file_text(media) { doc.add_text(F.attach, &t) },
_ => {}
}
}
doc.add_text(F.body, &body);
if let Some(t) = ink_text { doc.add_text(F.ink, t); }
for tag in &page.tags { doc.add_text(F.tags, tag.as_str()); }
doc.add_date(F.modified, page.modified.into());
idx.add_document(doc)?;
Ok(())
}5.6 OCR-очередь
- Таблица
ocr_queue: при вставке изображения/печатной копии/вложения ставится задача (priority: 1 — видимая страница, 5 — фон, 9 — импорт). - Исполнитель — фоновая задача брокера Hub (планировщик с ограничением: 1 поток, пауза на батарее < 20 %, пауза при активном вводе пера).
- Движки по порядку доступности: Windows —
Windows.Media.Ocr; macOS — VisionVNRecognizeTextRequest(уровень accurate); все ОС — встроенный движок (Tesseract черезleptess, языковые данныеrus,eng+ по выбору; P1 — собственная ONNX-модель PP-OCR для скорости). Предобработка вraster: масштаб до 300 dpi эквивалента, бинаризация Отсу, deskew по проекции. - Результат —
ocr/<hash>.json(слова, прямоугольники, уверенность, язык); слова с уверенностью < 0.6 в индекс попадают, но при копировании текста помечаются. - Повтор: до 3 попыток с задержкой 1/5/30 мин; ошибки видны в NT-18 («Распознавание»).
- Дедупликация: одно и то же медиа (хэш) распознаётся один раз на все книжки.
5.7 Распознавание рукописи
| Среда | Движок | Вход | Выход |
|---|---|---|---|
| Windows 10/11 | Windows Ink InkAnalyzer
(Windows.UI.Input.Inking.Analysis) |
штрихи как InkStroke (координаты в DIP, давление) |
строки/слова с альтернативами, границы, тип узла (текст/рисунок/список) |
| macOS 13+ | PencilKit PKDrawing + Vision
VNRecognizeTextRequest на растре 300 dpi; P1 —
PKRecognizer-подобный онлайн-путь через Scribble недоступен
на macOS, поэтому растровый путь |
растр | строки с уверенностью |
| Все ОС (офлайн, Linux) | локальная модель: CRNN/Transformer-OCR (ONNX Runtime, ~25 МБ, рукописные датасеты ru/en; P1) | растр строки | текст + уверенность |
Алгоритм «Рукопись в текст» для выделения:
- Группировка штрихов в строки: кластеризация по вертикали (DBSCAN по центрам bbox, ε = 0.6·медианной высоты штриха), внутри строки — сортировка по x; разрыв между словами — зазор > 0.35·высоты строки.
- Для движка ОС — передаём штрихи как есть; для растрового — рендерим строку в изображение высотой 64 px.
- Получаем текст + альтернативы; собираем абзацы: строки с общим левым краем (±1.5·высота) и шагом ≤ 1.8·высота — один абзац; маркер «•»/«-» в начале — список.
- Создаём текстовый контейнер в bbox выделения со стилем «Обычный», размер шрифта ≈ 0.7·медианной высоты строки (округление к 9–14 pt), штрихи удаляются (или остаются — по выбору в NT-19).
- Фоновое распознавание для поиска — тот же конвейер по всей странице,
результат в
ink-text/.
5.8 Слияние изменений страниц при синхронизации
Два пути. Meridian Cloud / общие книжки — Yrs:
страница = Y.Map (свойства) +
Y.Map<ContainerId, Y.Map> (контейнеры; текст —
Y.XmlFragment модели Write; позиция/размер — поля LWW) +
Y.Array<Stroke> (штрихи — add-only, удаление через
tombstone-поле); порядок страниц в разделе — Y.Array с
переупорядочиванием через move. Конфликтов нет по построению.
Файловые облака (две копии пакета разошлись): слияние на уровне файлов пакета.
merge_notebook(local, remote, base): // base — последняя общая версия из history/
для каждой страницы P:
если изменена только с одной стороны → взять её
если удалена с одной стороны и изменена с другой → оставить изменённую (в корзину — пометка «восстановлена»)
если изменена с обеих сторон:
merge_page(P.local, P.remote, P.base)
notebook.xml: объединить деревья по id (добавления с обеих сторон; порядок — по local, новые remote — после)
media/: объединение (контент-адресуемые — конфликтов нет)
history/: объединение по именам файлов
trash/: объединение
merge_page(L, R, B):
свойства: поле за полем, LWW по modified (ничья → local)
контейнеры: по id:
только в L или R → добавить
в обоих, равны → один
в обоих, различны:
text → трёхстороннее слияние текста Write (diff3 по абзацам, внутри абзаца — по словам);
при неразрешимом конфликте — оба варианта, второй помечен «Версия с другого устройства»
rect → LWW; если оба сдвинули — local, и remote-копия не создаётся
прочее → LWW по modified контейнера
штрихи: объединение по id; удалённые в одной стороне (есть в B, нет в L) не воскрешаются
результат получает modified = max(L, R), в history/ сохраняются обе входные версии
Обнаружение расхождения: по manifest.xml/@revision (ULID
последней записи) и base из history/; если
base отсутствует — двустороннее слияние (без удалений).
Файловые облака обычно создают «conflicted copy» — Notes ищет такие
копии рядом с пакетом (*(conflicted copy)*.mnx,
*.sync-conflict-*) и предлагает слить.
5.9 Версии страниц
Снимок делается после «тихого» сохранения (2 с без ввода), если: прошло ≥ 5 мин с предыдущего снимка, или автор изменился, или нажато «Сохранить версию». Сворачивание: версии старше 7 дней — одна в день, старше 30 дней — одна в неделю; ручные не удаляются. Сравнение версий: текст — diff по абзацам (Myers) с подсветкой, штрихи — по id (новые зелёным, удалённые красным полупрозрачно), контейнеры — по bbox.
5.10 Поиск по тегам
tag_uses обновляется при индексации страницы (разбор
абзацев с атрибутом tag). Запрос сводки —
SELECT … FROM tag_uses JOIN pages … WHERE tag_id IN (?) AND (done = 0 OR ?) ORDER BY <группировка>;
страница- сводка генерируется как текстовый контейнер со ссылками
meridian://notes/<nb>/<page>#<container>/<para>;
обновление сводки — пересоздание контейнера с сохранением
пользовательских правок ниже маркера.
5.11 MeridianScript API
Глобальный объект App (приложение Notes). Все методы
асинхронные, возвращают Promise. Песочница: доступ к файлам
только через диалоги или разрешение
fs.read/fs.write в манифесте скрипта.
// Типы / Types (d.ts поставляется с пакетом)
interface NotesApp {
notebooks: Notebook[];
activeNotebook: Notebook | null;
activePage: Page | null;
openNotebook(path: string): Promise<Notebook>;
createNotebook(path: string, name: string): Promise<Notebook>;
search(query: string, opts?: { scope?: 'page'|'section'|'notebook'|'all'; tags?: string[] }): Promise<SearchHit[]>;
on(event: 'pageOpened'|'pageSaved'|'pageCreated'|'tagChanged'|'inkStrokeAdded', handler: (e: NotesEvent) => void): Disposable;
ui: { message(text: string): Promise<void>; prompt(label: string, def?: string): Promise<string|null>; pickPage(): Promise<Page|null> };
}
interface Notebook { id: string; name: string; path: string; sections: Section[]; groups: SectionGroup[];
createSection(name: string, opts?: { color?: string; group?: SectionGroup }): Promise<Section>; tags: TagLibrary; trash: Trash; }
interface Section { id: string; name: string; color: string; pages: Page[]; locked: boolean;
createPage(title: string, opts?: { template?: string; after?: Page; level?: 1|2|3 }): Promise<Page>; unlock(password: string): Promise<boolean>; }
interface Page { id: string; title: string; created: Date; modified: Date; tags: string[]; containers: Container[]; ink: InkLayer;
addText(text: string | RichText, at?: { x: number; y: number; w?: number }): Promise<TextContainer>;
addImage(src: Blob | string, at?: Point): Promise<ImageContainer>;
addFile(path: string, at?: Point, opts?: { printout?: boolean }): Promise<FileContainer>;
addTable(rows: number, cols: number, at?: Point): Promise<TableContainer>;
versions(): Promise<PageVersion[]>; restore(v: PageVersion): Promise<void>;
export(format: 'pdf'|'md'|'html', path: string): Promise<void>; url: string; }
interface TextContainer extends Container { document: RichTextDocument /* API Write */; paragraphs: Paragraph[]; }
interface Paragraph { text: string; style: string; tag?: string; done?: boolean; setTag(tag: string|null, done?: boolean): void; }
interface InkLayer { strokes: Stroke[]; recognizeText(strokes?: Stroke[]): Promise<RecognizedText>; select(polygon: Point[]): Stroke[]; }Примеры:
// 1. Собрать все невыполненные задачи книжки на страницу «Сводка задач» / Collect all open to-dos
const nb = App.activeNotebook!;
const hits = await App.search('', { scope: 'notebook', tags: ['todo'] });
const summary = await nb.sections[0].createPage('Сводка задач');
const lines = hits.filter(h => !h.done).map(h => `- [ ] ${h.text} — [${h.page.title}](${h.page.url})`);
await summary.addText(lines.join('\n'), { x: 72, y: 96, w: 480 });
// 2. Ежедневная страница из шаблона при открытии книжки / Daily page from a template on notebook open
App.on('pageOpened', async e => {
const today = new Date().toISOString().slice(0, 10);
const journal = e.notebook.sections.find(s => s.name === 'Дневник');
if (journal && !journal.pages.some(p => p.title === today)) await journal.createPage(today, { template: 'daily' });
});
// 3. Распознать выделенные штрихи и добавить текст рядом / Recognize selected strokes
const page = App.activePage!;
const sel = page.ink.strokes.filter(s => s.selected);
const r = await page.ink.recognizeText(sel);
await page.addText(r.text, { x: r.bbox.x, y: r.bbox.y + r.bbox.h + 12 });6. Форматы
6.1 Свой формат .mnx
Описан в разделе 4. Особенности: mimetype первым
элементом без сжатия; медиа — stored; версии — zstd;
плоского варианта нет (книжка — всегда пакет); частичный экспорт
(раздел/страница) — тот же пакет с подмножеством частей и
manifest.xml/@partial="true". Открытие пакета с
неизвестными элементами — сохраняются как есть (round-trip). Сборка
мусора медиа — при «Сжать книжку» (Файл → Сведения).
6.2 Чтение чужих форматов
| Формат | Что переносится | Ограничения |
|---|---|---|
Evernote .enex (XML, ENML) |
заметки, теги, created/updated, <en-media> →
медиа по hash MD5 из <resource>,
<en-todo> → тег todo, таблицы, ссылки,
source-url → свойство, геометки, напоминания →
свойства |
ENML → модель Write через HTML-кодек; шифрованные блоки
<en-crypt> (RC2-64/AES-128) расшифровываются по
паролю, иначе остаются как вложение |
OneNote .one, .onepkg (MS-ONESTORE +
MS-ONE; .onepkg — CAB-архив с .one разделов и
.onetoc2) |
структура разделов из .onetoc2, страницы и подстраницы
(jcidPageNode, уровень из PageLevel),
заголовки, outline-контейнеры (jcidOutlineNode → текстовый
контейнер; jcidRichTextOENode → абзацы с
run-форматированием), изображения (jcidImageNode + данные
из file node), вложения (jcidEmbeddedFileNode), штрихи
(jcidInkStrokeNode, координаты в half-inch/2540 units → pt,
давление), теги (NoteTags), таблицы
(jcidTableNode), даты, авторы |
формат-проект: проходим Object Space / Revision Manifest / File Node
List, берём последнюю ревизию; поддерживаются файлы OneNote
2010–2016/365 (версия файла 2010+); Microsoft-специфичные объекты
(встроенные Visio/Excel) — как вложения; формулы — как изображения;
пароли разделов (RC4/AES со схемой OneNote) — раздел пропускается с
сообщением; .one 2007 — только текст (R*) |
| Markdown-папка | .md → страница (CommonMark + GFM: таблицы, чек-боксы,
зачёркивание, сноски; математика $…$ → Math), фронтматтер
YAML (tags, created) → свойства/теги,
[[wikilink]] и относительные ссылки → ссылки на страницы,
изображения из относительных путей, подпапки → подстраницы или разделы
(на выбор) |
HTML внутри Markdown — через HTML-кодек; неизвестные плагины-блоки Obsidian — как код |
| HTML (файл, папка, экспорт Evernote/Notion/Apple Notes) | <article>/<body> → контейнер,
<img> (включая data:) → медиа,
<table>, чек-боксы, заголовки → стили;
Notion-экспорт: папка с .md/.html +
подпапки |
CSS ограниченно (общий HTML-кодек Write) |
| Буфер обмена/перетаскивание | текст, RTF, HTML, изображения, файлы, URL (→ закладка-вырезка),
.eml (→ печатная копия со ссылкой на письмо Mail) |
— |
6.3 Запись
| Формат | Содержание | Особенности |
|---|---|---|
| страницы раздела/книжки; «как на экране» или «разбить на листы A4/Letter»; закладки PDF по структуре (раздел → страница); штрихи — вектор; текст выделяемый; изображения с OCR-слоем (невидимый текст поверх) | Meridian Print, PDF/A-2b по выбору | |
| Markdown | папка: <Раздел>/<Страница>.md, подстраницы
— вложенные папки; контейнеры — по порядку (y, затем x); штрихи →
assets/<page>-ink.svg; таблицы → GFM; теги →
фронтматтер; формулы → $…$ |
потеря абсолютной раскладки (объявляется в диалоге) |
| HTML | один файл (медиа как data:) или папка; абсолютные
позиции через CSS (position:absolute), штрихи как
inline-SVG |
тема по умолчанию светлая, печать через браузер |
.mwx |
страница → документ Write: контейнеры по порядку, изображения встроены, штрихи — как рисунок | P1 |
.enex |
обратный экспорт для совместимости | P2 |
7. Горячие клавиши
Схема по умолчанию — Microsoft Office/OneNote; ⌘ вместо
Ctrl на macOS.
| Действие | Клавиши |
|---|---|
| Новая страница / подстраница | Ctrl+N / Ctrl+Shift+Alt+N |
| Новый раздел / книжка | Ctrl+T / Ctrl+Shift+N |
| Открыть книжку / Сохранить версию | Ctrl+O / Ctrl+Shift+S |
| Печать / Экспорт | Ctrl+P / Ctrl+Shift+E |
| Поиск на странице / по всем книжкам / далее | Ctrl+F / Ctrl+E / F3 |
| Командная палитра | Ctrl+K |
| Переход: следующая/предыдущая страница | Ctrl+PgDn / Ctrl+PgUp |
| Следующий/предыдущий раздел | Ctrl+Tab / Ctrl+Shift+Tab |
| Понизить / повысить уровень страницы | Ctrl+Alt+] / Ctrl+Alt+[ |
| Недавние страницы | Ctrl+Shift+H |
| Теги 1–9 / флажок задачи / снять теги | Ctrl+1…9 / Ctrl+1 /
Ctrl+0 |
| Выделить контейнер / все контейнеры / страницу | Ctrl+A (повторно расширяет: абзац → контейнер →
страница) |
| Дата / время / дата и время | Alt+Shift+D / Alt+Shift+T /
Alt+Shift+F |
| Ссылка | Ctrl+K в выделении (без выделения — палитра) |
| Вставить таблицу / изображение / файл / формулу | Ctrl+Shift+T / Alt+N, P /
Alt+N, F / Alt+= |
| Стили: Заголовок 1–6 / Обычный / Код | Ctrl+Alt+1…6 / Ctrl+Shift+N (в тексте) /
Ctrl+Alt+K |
| Список / нумерация / отступ | Ctrl+. / Ctrl+/ /
Tab–Shift+Tab |
| Выделение цветом / жирный / курсив / подчёркнутый / зачёркнутый | Ctrl+Shift+H (в тексте) / Ctrl+B /
Ctrl+I / Ctrl+U / Ctrl+- |
| Режим пера / ластик / лассо / выход в указатель | Ctrl+Shift+P / Ctrl+Shift+E (в пере) /
Ctrl+Shift+L / Esc |
| Слоты перьев 1–6 | Alt+1…6 |
| Рукопись в текст | Ctrl+Shift+X |
| Записать аудио / видео / стоп | Ctrl+Alt+R / Ctrl+Alt+V /
Ctrl+Alt+S |
| Скриншот в заметку (глобально) | Win+Shift+N / ⌘⇧N |
| Быстрая заметка (глобально) | Win+Alt+N / ⌃⌥N |
| Заблокировать все разделы | Ctrl+Alt+L |
| Масштаб +/−/100 % / по ширине | Ctrl+= / Ctrl+- / Ctrl+0 /
Ctrl+Shift+0 |
| Полноэкранный холст / панель страниц / навигатор | F11 / Ctrl+Alt+P /
Ctrl+Alt+N |
| Версии страницы / Корзина | Ctrl+Alt+H / Ctrl+Alt+T |
| Отмена / повтор | Ctrl+Z / Ctrl+Y |
| Переместить контейнер на 1 pt / 8 pt | Стрелки / Shift+Стрелки |
8. Критерии готовности
8.1 Notes 1.0 (P0 закрыты)
- Книжка
.mnxс 50 разделами и 5 000 страниц открывается ≤ 1,5 с (SSD); переход между страницами ≤ 100 мс; страница с 20 000 штрихов и 50 контейнерами рендерится ≥ 60 к/с при панорамировании на интегрированной GPU. - Задержка пера на Windows Ink и Apple Pencil (через Sidecar) ≤ 20 мс на медиане; palm rejection без ложных штрихов в тесте 100 касаний.
- Автосдвиг: тестовый набор из 60 сценариев раскладки (рост/вставка/перетаскивание) — ни одного наложения текстовых контейнеров в одной колонке.
- Поиск: полнотекстовый по 5 000 страниц ≤ 150 мс; инкрементальная индексация сохранённой страницы ≤ 2 с после сохранения; OCR 100 скриншотов (1080p) ≤ 5 мин в фоне без просадки UI.
- Рукопись → текст: точность по словам ≥ 92 % (Windows Ink / Vision) на внутреннем корпусе 500 строк ru/en.
- Импорт
.enex(корпус 200 заметок с ресурсами, таблицами, чек-боксами) — 100 % заметок, 100 % ресурсов; Markdown-папка Obsidian с 1 000 файлов — все ссылки разрешены. - Экспорт PDF/MD/HTML — визуальный тест на 50 страницах (снимочное сравнение).
- Версии: 50 версий страницы, восстановление, сравнение; корзина: восстановление на место.
- Защита раздела: пароль, автоблокировка, страницы не читаются из пакета без пароля (проверка тестом).
- Слияние двух разошедшихся копий пакета (файловое облако) — 40 сценариев без потери контейнеров.
- Автосохранение и восстановление после
kill -9— потеря ≤ 2 с ввода. - Снимочные тесты: 300 страниц-эталонов (включая импорт OneNote/Evernote) без регрессий.
8.2 Notes 1.x (P1)
Импорт .one/.onepkg на корпусе 100 разделов (структура и
текст — 100 %, штрихи — 95 %, изображения — 100 %); веб-вырезка в трёх
браузерах; печать в книжку; аудиозаписи с метками; поиск по рукописи;
локальная ONNX-модель распознавания на Linux (≥ 85 % по словам);
синхронизация через Meridian Cloud (фаза 6).
9. Риски
| Риск | Вероятность | Влияние | Снижение |
|---|---|---|---|
| Формат MS-ONE/ONESTORE сложен; спецификация неполна по части форматирования и штрихов | высокая | импорт OneNote получится частичным | заявить R* в библии (уже), корпус реальных файлов, приоритет — структура + текст + изображения; штрихи и таблицы — итерациями |
| Задержка пера и palm rejection на Linux (Wayland/X11, libinput) хуже, чем на Windows/macOS | средняя | рукопись на Linux ощущается хуже | прямой доступ к событиям планшета через Qt
QTabletEvent, тестирование на Wacom/Huion; собственное
предсказание штриха |
| Распознавание рукописи на Linux только через локальную модель — точность ниже | высокая | «Рукопись в текст» хуже на Linux | обучение модели на собственном корпусе (ru/en), поставка как загружаемый пакет ~25 МБ, честное указание точности |
| OCR-движок (Tesseract) медленный и неточный на скриншотах с мелким текстом | средняя | поиск по изображениям неполный | предобработка, системные движки где есть, P1 — ONNX PP-OCR |
| Сложность трёхстороннего слияния раскладки при файловых облаках | средняя | дубли/потери при конфликтах | консервативная стратегия (ничего не удалять, помечать), обязательные
версии в history/, набор сценариев |
| Производительность бесконечного холста с большими медиа (сотни фото) | средняя | тормоза | ленивая загрузка медиа по видимости, мип-уровни, тайловый кэш |
| Пересечение с Mail (задачи/напоминания) и Write (текст) — размытие границ продукта | низкая | дублирование функций | задачи только по ссылке в Mail; Notes не делает напоминания сам |
Виртуальный принтер «печать в книжку» требует драйвера (Windows —
порт-монитор/Print to PDF-перехват, macOS — PDF Services,
Linux — CUPS backend) |
средняя | установка требует прав, сложная поддержка | использовать штатные механизмы: Windows —
Microsoft Print to PDF + «Отправить в Notes» из Hub; macOS
— PDF Services в меню печати; Linux — CUPS-PDF backend с хуком |
| Зашифрованные разделы: потеря пароля = потеря данных | низкая | жалобы | подсказка пароля, предупреждение при установке, опциональный ключ восстановления через Meridian ID (P1) |
10. Вопросы к библии
- Единица длины в MOP. Для холста Notes выбран
pt(1/72") с 2 десятичными знаками. Если для Write/Draw вspec/formats/будет зафиксирована другая единица (мм или EMU-подобные целые), Notes перейдёт на неё — нужно общее решение в библии/спецификации MOP. - Шаблоны Notes. В таблице расширений у Notes нет
формата шаблона; принято хранить шаблоны страниц внутри пакета
(
templates/) и в профиле. Если потребуется обмен шаблонами через галерею Hub, предлагаем.mnt(пакет с одной страницей) — требует правки таблицы 3.2. - Глобальные сочетания клавиш (быстрая заметка, скриншот в заметку) — регистрируются брокером Hub; нужно зафиксировать в документе Hub и в разделе 5.3 библии, какие глобальные сочетания пакет резервирует.
- Виртуальный принтер «Meridian Notes» — драйверная часть требует решения на уровне установщика (раздел 4 библии, «Установка»): включать ли компонент в MSIX/DMG по умолчанию.