Конструктор схем на символьной сетке. Схема — не картинка, а список узлов:
рамки, врезки [ ЗАГОЛОВОК ], пары «имя — значение», строки с суммой, таблицы,
мерные стрелки, линейки, блоки текста. Всё живёт в моноширинной сетке, поэтому
вывод одинаков на экране, в буфере обмена и в PNG.
Интерфейс английский, комментарии и документация — русские.
npm install
npm run devsrc/engine/ ядро без React
canvas.js сетка символов: put/text/hline/vline/frame/label → HTML, текст, ячейки
schema.js словарь узлов: дефолты, поля инспектора, отрисовка, габарит, ручки
render.js документ → полотно
expr.js переменные, подстановка {{ … }} и денежный формат
presets.js шаблоны: схема радиусов, три вида счёта, пустой лист
exporters.js txt, png (тем же обходом ячеек, что и экран), json
typefaces.js гарнитуры полотна: метрики, начертания, подстановки символов
theme.js тёмная и светлая темы — один источник для CSS и растра
src/ui/ панели: Grid (полотно и мышь), Layers, Inspector, Vars, Toolbar
src/store.js документ, история снимками, автосохранение в localStorage
Новый тип узла добавляется одним объектом в NODE_TYPES — инспектор, стопка
слоёв и выделение подхватят его сами.
| шаблон | что это |
|---|---|
| Invoice · Sheet | счёт листом: шапка, стороны сделки, таблица работ, итог, реквизиты |
| Invoice · Receipt | узкая лента: всё по центру, суммы на точечных лидерах |
| Invoice · Statement | выписка: движение по счёту, итоги справа, разбивка по срокам |
| Invoice · Quote | предложение: объём фазами, опции чекбоксами, поле подписи |
| Invoice · Timesheet | счёт по часам: списания по неделям и полоска нагрузки |
| Invoice · Credit note | кредит-нота: ссылка на исходный счёт, суммы со знаком минус |
| Invoice · Payment slip | платёжка: корешок, реквизиты, машинная полоса; открывается на OCR B |
| Scheme · Nested radii | схема вложенных радиусов, с неё начался проект |
| Scheme · Empty sheet | пустой лист с рамкой и врезкой |
Суммы в счётах не вписаны руками: ставки лежат в переменных, а строки и итог считаются из них. Данные вымышленные — это образец вёрстки, а не платёжный документ.
Документ помнит, из какого шаблона собран (preset в json), поэтому селектор
показывает активный выбор и после перезагрузки, и после открытия проекта.
Выбирается в верхней строке и хранится в документе, поэтому уезжает вместе с
.json и совпадает в PNG. Интерфейс редактора это не трогает — он всегда набран
Departure Mono.
| гарнитура | что даёт |
|---|---|
| OCR B | машиночитаемый, 140 кодовых точек — умолчание |
| Departure Mono | пиксельный растр 11px, полный box-drawing |
| JetBrains Mono | контурный, два начертания: 300 — текст, 500 — акценты |
У OCR B нет ни box-drawing, ни стрелок, поэтому профиль подставляет ASCII
(- | +) прямо на записи в полотно: если отдать браузеру символ, которого в
шрифте нет, он молча возьмёт системный — с другой шириной знакоместа, и сетка
поедет.
Тёмная и светлая, переключаются там же. Тема — настройка редактора, а не
свойство схемы: один документ смотрят и так, и так. Цвета лежат в
src/engine/theme.js и разъезжаются оттуда в CSS-переменные и в растровый
экспорт, поэтому выгруженная картинка совпадает с экраном.
Значение переменной — число или выражение над объявленными выше: sub считается
из ставок, vat из sub, total из обоих. В любом текстовом поле узла работает
подстановка:
{{ outer - inset }} 12
{{ sub + vat | 2 }} 4770.22 ← знаков после точки
{{ sub + vat | 2g }} 4,770.22 ← и разбивка по тысячам
Колонки задаются строкой 29l 5r 10r 11r — ширина и выравнивание (l, r,
c). Ячейки строк разделяются |, в каждой работает подстановка. Что не влезло
в колонку, обрезается, а не наезжает на соседнюю.
Пунктир через клетку упирается штрихами в оба угла только если длина стороны
кратна шагу: x1 - x0 делится на step ─, y1 - y0 — на step │. Иначе один
угол остаётся с дыркой рядом, а в середине стороны появляется двойной зазор.
Рамка с такой стороной сама говорит об этом в инспекторе и правится кнопкой
fit (дальний край сдвигается на клетку).
Поле dash у рамки переключает раскладку штрихов:
corners— штрихи идут от каждого угла к середине, углы всегда закрыты;grid— фаза считается от начала полотна: штрихи попадают в те же колонки, что у отдельных линеек-узлов, но у угла может остаться пустая клетка.
| действие | как |
|---|---|
| выбрать узел | клик по нему (у рамки — по контуру) |
| двигать | тяга мышью, привязка к клетке |
| растянуть | ручки на выделении |
| сдвиг на клетку | стрелки, с Shift — на пять |
| копия / удалить | ⌘D / Backspace |
| отмена | ⌘Z, ⇧⌘Z |
copy и .txt отдают plain text, png ×2 / ×4 — растр (кегль кратен 11px,
поэтому пиксельный шрифт не мылится), .pdf — одностраничный документ,
.json — проект целиком, open — обратно.
Растр, PDF и экран рисуются одним обходом ячеек, одним шрифтом и в цветах текущей темы, поэтому выгруженное совпадает с редактором. В PDF лист лежит картинкой: страница меряется в экранных пикселях листа (96 dpi), а растр внутри плотнее в четыре раза — около 380 dpi. Текст в таком файле не выделяется: встроить woff2 без распаковки нельзя, а подменить гарнитуру на стандартную значит потерять всю разметку.
Кнопка backend выгружает <имя>.backend.json: документ с невычисленными
{{ }} плюс контракт — какие переменные бекенд обязан прислать (inputs),
какие считаются сами (derived) и какие подстановки встречаются в узлах
(slots).
Рисует бекенд тем же движком: src/engine — чистый ESM без зависимостей,
работает в Node как есть.
import { renderDoc } from './engine/render.js';
import template from './invoice-sheet.backend.json' with { type: 'json' };
const { text } = renderDoc(template.doc, { seats: 40, seatRate: 19.5 });data перебивает переменные по имени, производные пересчитываются сами.
Проверить, не поднимая сервер:
npm run render -- invoice-sheet.backend.json '{"seats":40,"vatRate":21}'legacy/ — первая версия: счёт на той же сетке, но без сборщика, плюс схема
радиусов, с которой начался конструктор. Шрифты общие, лежат в fonts/.