Meridian Office

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 Версии, корзина, защита


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 Предотвращение наложений при создании и перетаскивании

5.3 Сглаживание штрихов и рендер переменной толщины

Ввод: события пера (x, y, pressure, tilt, t). Конвейер:

  1. Фильтрация дрожания — One-Euro filter (fc_min = 1.0 Гц, β = 0.007) по x и y; для мыши — отключён.
  2. Прореживание — Ramer–Douglas–Peucker с ε = 0.15 pt (сохраняя давление: точка не удаляется, если |Δp| > 48 из 1023).
  3. Сглаживание геометрии при рендере — Catmull-Rom сплайн по точкам (α = 0.5, центрипетальный), 16 сегментов между точками при масштабе ≥ 100 %, меньше при отдалении.
  4. Переменная толщина — для каждой точки w_i = width · (0.35 + 0.65 · (p_i/1023)^γ), γ = 1.4 для ручки, для карандаша толщина постоянна, а alpha_i = 0.5 + 0.5·cos(tilt); «калиграфическое» перо — толщина зависит от угла направления относительно 45°.
  5. Геометрия контура — по сплайну строятся две offset-кривые (нормали ± w_i/2), соединяются круглыми торцами; результат — замкнутый путь Skia (Path), заливается цветом (маркер — BlendMode::Multiply, рисуется под текстовыми контейнерами).
  6. Предсказание — при вводе показываем «хвост» на 1 кадр вперёд (линейная экстраполяция по последним двум точкам, 8–12 мс), стирается при приходе реальной точки — снижает воспринимаемую задержку.
  7. Кэш — завершённые штрихи растрируются в тайлы 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 Лассо-выделение штрихов

  1. Пользователь рисует замкнутую (замыкаем автоматически) полилинию L в координатах холста.
  2. Грубый отбор — штрихи, чей bbox пересекает bbox(L).
  3. Точная проверка — штрих выбран, если ≥ 50 % его точек (по длине, не по количеству) лежат внутри L (чётно-нечётное правило, point_in_polygon); для коротких штрихов (< 10 точек) — если внутри любая точка.
  4. Контейнеры — выбраны, если их центр внутри L (или ≥ 60 % площади при Shift).
  5. Выделение показывается пунктирным контуром по выпуклой оболочке + маркеры; операции: перемещение (сдвиг всех точек), масштаб/поворот (аффинное преобразование точек вокруг центра bbox), изменение цвета/ толщины/пера, копирование, удаление, «Рукопись в текст», «Сгруппировать с контейнером» (установить anchor).
  6. Сложность O(N·M) при N точках штрихов и M вершинах лассо; при N > 50 000 используем R-дерево по точкам.

5.5 Полнотекстовый индекс tantivy с инкрементальным обновлением

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-очередь

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) растр строки текст + уверенность

Алгоритм «Рукопись в текст» для выделения:

  1. Группировка штрихов в строки: кластеризация по вертикали (DBSCAN по центрам bbox, ε = 0.6·медианной высоты штриха), внутри строки — сортировка по x; разрыв между словами — зазор > 0.35·высоты строки.
  2. Для движка ОС — передаём штрихи как есть; для растрового — рендерим строку в изображение высотой 64 px.
  3. Получаем текст + альтернативы; собираем абзацы: строки с общим левым краем (±1.5·высота) и шагом ≤ 1.8·высота — один абзац; маркер «•»/«-» в начале — список.
  4. Создаём текстовый контейнер в bbox выделения со стилем «Обычный», размер шрифта ≈ 0.7·медианной высоты строки (округление к 9–14 pt), штрихи удаляются (или остаются — по выбору в NT-19).
  5. Фоновое распознавание для поиска — тот же конвейер по всей странице, результат в 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 Запись

Формат Содержание Особенности
PDF страницы раздела/книжки; «как на экране» или «разбить на листы 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 закрыты)

  1. Книжка .mnx с 50 разделами и 5 000 страниц открывается ≤ 1,5 с (SSD); переход между страницами ≤ 100 мс; страница с 20 000 штрихов и 50 контейнерами рендерится ≥ 60 к/с при панорамировании на интегрированной GPU.
  2. Задержка пера на Windows Ink и Apple Pencil (через Sidecar) ≤ 20 мс на медиане; palm rejection без ложных штрихов в тесте 100 касаний.
  3. Автосдвиг: тестовый набор из 60 сценариев раскладки (рост/вставка/перетаскивание) — ни одного наложения текстовых контейнеров в одной колонке.
  4. Поиск: полнотекстовый по 5 000 страниц ≤ 150 мс; инкрементальная индексация сохранённой страницы ≤ 2 с после сохранения; OCR 100 скриншотов (1080p) ≤ 5 мин в фоне без просадки UI.
  5. Рукопись → текст: точность по словам ≥ 92 % (Windows Ink / Vision) на внутреннем корпусе 500 строк ru/en.
  6. Импорт .enex (корпус 200 заметок с ресурсами, таблицами, чек-боксами) — 100 % заметок, 100 % ресурсов; Markdown-папка Obsidian с 1 000 файлов — все ссылки разрешены.
  7. Экспорт PDF/MD/HTML — визуальный тест на 50 страницах (снимочное сравнение).
  8. Версии: 50 версий страницы, восстановление, сравнение; корзина: восстановление на место.
  9. Защита раздела: пароль, автоблокировка, страницы не читаются из пакета без пароля (проверка тестом).
  10. Слияние двух разошедшихся копий пакета (файловое облако) — 40 сценариев без потери контейнеров.
  11. Автосохранение и восстановление после kill -9 — потеря ≤ 2 с ввода.
  12. Снимочные тесты: 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. Вопросы к библии

  1. Единица длины в MOP. Для холста Notes выбран pt (1/72") с 2 десятичными знаками. Если для Write/Draw в spec/formats/ будет зафиксирована другая единица (мм или EMU-подобные целые), Notes перейдёт на неё — нужно общее решение в библии/спецификации MOP.
  2. Шаблоны Notes. В таблице расширений у Notes нет формата шаблона; принято хранить шаблоны страниц внутри пакета (templates/) и в профиле. Если потребуется обмен шаблонами через галерею Hub, предлагаем .mnt (пакет с одной страницей) — требует правки таблицы 3.2.
  3. Глобальные сочетания клавиш (быстрая заметка, скриншот в заметку) — регистрируются брокером Hub; нужно зафиксировать в документе Hub и в разделе 5.3 библии, какие глобальные сочетания пакет резервирует.
  4. Виртуальный принтер «Meridian Notes» — драйверная часть требует решения на уровне установщика (раздел 4 библии, «Установка»): включать ли компонент в MSIX/DMG по умолчанию.