Демо: village-elder.github.io/road_sign_builder
Веб-застосунок для побудови знаків за путівником roadguide.a3.kyiv.ua.
Повністю клієнтський застосунок. Жодного бекенду чи бази даних — генерація SVG, метрика шрифту, конструктор з drag-and-drop і персистентність усе на боці клієнта. Збірка — TypeScript + Vite (до Фази H цього репозиторію проєкт був навмисно zero-build-step; повний контекст рішення — у git-історії).
road_sign_builder/
│
├── index.html # UI-розмітка, вхідна точка для Vite
│
├── src/
│ ├── main.ts # bootstrap: DOMContentLoaded → Controller.init()
│ ├── css/main.css # Стилі
│ ├── types.ts # Спільні типи pipeline (RenderContext/LayoutResult/…)
│ ├── types/ # Ambient-типи (глобальний `opentype`, тестовий import .js)
│ ├── stores/ # state.ts — тонкий фасад над цими 5 сторами
│ └── *.ts # Увесь застосунок — див. таблиці нижче
│
├── public/ # Копіюється в dist/ як є (Vite-конвенція)
│ ├── fonts/ # Road UA (Bold/Medium) + RoadUA-LICENSE.txt (CC BY-ND 4.0)
│ ├── vendor/opentype.min.js # Парсинг TTF/контурів гліфів (MIT), глобальний <script>
│ ├── examples/ # Готові приклади для оверлея "Почніть з прикладу"
│ └── config-defaults.json # Дефолти метрики (module/spacing/border/…)
│
├── tests/ # Vitest — див. tests/README.md
│
├── .github/workflows/deploy.yml # Збірка + деплой на GitHub Pages
├── vite.config.ts / vitest.config.ts / tsconfig.json
├── package.json
└── .gitignore
opentype.min.js навмисно лишається вендорним <script>-тегом (не npm-пакетом
opentype.js) — щоб не ризикувати розбіжністю зі збіркою, вже перевіреною
побайтово проти Python-оригіналу. Типи для нього — власний ambient
src/types/opentype.d.ts, лише сигнатури, що реально використовуються.
| Шар | Модулі | Відповідальність |
|---|---|---|
| Шрифтові метрики | font-metrics.ts |
opentype.js-парсинг TTF, LSB/RSB/advance/контур гліфа |
| Вимір (measure) | text-measure.ts |
Розбиття на блоки/рядки, ink-width через реальні метрики шрифту |
| Контекст рендеру | render-context.ts |
RenderContext — розміри, кольори, шрифти за пресетом/темою |
| Компоновка (layout) | layout-engine.ts |
Абсолютні координати всіх елементів знаку |
| Елементи знаку | km-group.ts, road-number-generator.ts + road-num-group.ts, road-ua-symbols.ts + symbol-group.ts |
Вимір/рендер км-позначки, номерів доріг, символів/піктограм/стрілок (стрілки — category:"arrows" у symbol_configs, той самий рендер-шлях, що й решта символів). road_num/km — кілька незалежних груп (0..N екземплярів кожного типу), НЕ singleton; road_num має власну комо-множинність всередині (road_numbers), км — ОДНЕ значення на екземпляр, повністю незалежний від рядків тексту назви (layout.kmBaselineY — фіксований якір, не рухається при редагуванні/перетягуванні назви) |
| Текст | render-text.ts |
Рендер <text> назви знаку |
| Конструктор | bounds.ts |
Межі елементів, автосайз, drag-офсети, bounds/padding-оверлей. road_num/km — спільний layout-якір рахується лише з екземпляра [0]; кожен інстанс отримує власний drag-офсет через shifted-layout-копію (extraRoadNumOffsets/extraKmOffsets), без мутації спільного layout |
| Піктограми (SVG-контури) | pictogram-icons-svg.ts |
Незалежні від Road UA font контури (bus/train/intersection/airplane_12/9/10/2/3 тощо) для symbol_configs, поряд із font-лігатурами |
| Дорожні знаки | traffic-signs-svg.ts + traffic-sign-group.ts, traffic-sign-manager.ts/traffic-sign-picker.ts |
7 стандартних дорожніх знаків (SVG-контур з гайду) як ОКРЕМА сутність від символів/піктограм — власний список traffic_sign_configs |
| Смуга «Кінець» / Роздільна лінія / Хвиля | end-stripe-manager.ts, separator-line-manager.ts, wave-manager.ts + wave-group.ts |
Геометричні елементи без layout-якоря: діагональна смуга "Кінець населеного пункту", горизонтальна лінія між напрямками, хвиля (водойма/річка) |
| Контур знаку | sign-outline.ts |
Форма знаку: закруглені кути, скошений правий край (exit_shape для знаку з'їзду з дороги) |
| Відступи | indent.ts |
Спільний каркас IndentOffsets для 7 типів елементів — лише жовтий guide-прямокутник, не зсуває фактичний рендер |
| Анотації | annotation-generator.ts |
Вимірювальна розмітка (pad/cap/border/km) поверх знаку |
| Оркестрація | svg-generator.ts |
Збирає весь pipeline в один SVG-рядок |
| KiCad-експорт | text-outliner.ts |
Конвертація <text> → <path> (шрифт-незалежний SVG) |
| Дефолти / валідація | config-defaults.ts, norm-check.ts, legal-scale.ts |
Глобальні параметри метрики (JSON, аналог колишнього config.toml); перевірка введених значень проти легальної x-шкали |
| Утиліта | html-escape.ts |
escapeHtml() — спільне екранування перед вставкою в <text> (раніше дублювалось у 6 файлах) |
| Модуль | Відповідальність |
|---|---|
local-preview.ts |
State (snake_case) → generateSvg() конфіг (camelCase); локальний прев'ю/export/outlined/annotate |
sign-files.ts |
Збереження/завантаження знаку у .json-файл (download/upload браузером) — єдиний спосіб персистентності, без localStorage |
example-picker.ts |
Оверлей "Почніть з прикладу" — з'являється, коли знак порожній; готові приклади в public/examples/ (manifest.json + по одному .json на приклад, той самий формат, що sign-files.ts) |
preview-trigger.ts |
Debounced запуск оновлення прев'ю |
state.ts — тонкий фасад над stores/{sign,settings,grid,annotation,bounds}-store.ts
(один store = один зріз стану, той самий публічний State-об'єкт для ~20 споживачів).
constants.ts (таблиці пресетів), zoom.ts / grid.ts / drag-overlay.ts /
preview.ts (канвас), symbol-picker.ts / symbol-element-picker.ts (піктограми),
names-manager.ts / symbol-manager.ts / road-num-manager.ts / km-manager.ts /
traffic-sign-manager.ts / end-stripe-manager.ts / separator-line-manager.ts /
wave-manager.ts
(data stores елементів-списків — спільне ядро list-manager.ts, toggle-групи —
toggle-group.ts, одиночні числові поля — numeric-field.ts, drag — drag-group.ts
element-auto-position.ts;symbol-manager.tsпокриває піктограми/райони/країни/стрілки одразу — колишній окремийarrow-manager.tsзлито сюди якcategory:"arrows";traffic-sign-manager.ts— ОКРЕМА від символів сутність (7 стандартних дорожніх знаків, не category);road-num-manager.ts/km-manager.ts— колишній singleton-реєстрelement-types.ts/SINGLETON_ELEMENT_TYPES(видалено), тепер 0..N незалежних екземплярів кожного типу, той самий каркас, щоsymbol-manager.ts),tree-manager.ts/inspector.ts/element-list.ts(sidebar),main-tab.ts/settings-tab.ts/topbar-actions.ts(вкладки),preview-side-panel.ts(права панель "Чисте прев'ю" — специфікація знаку в мм, перевірка норми, експорт),controller.ts/main.ts(bootstrap).
Ці 8 менеджерів списків + tree-manager.ts/inspector.ts/element-list.ts не
імпортують одне одного напряму — кожен отримує залежності через setDeps() з
роль-специфічними інтерфейсами (ui-contracts.ts), звуженими до реально
використовуваного зрізу API сусіда. wire-ui-modules.ts — єдиний
композиційний корінь, де цих 11 модулів зв'язуються разом; викликається один
раз з controller.ts:init().
Увесь застосунок — 100% клієнтський; жодного мережевого виклику до власного
бекенду немає (транслітерація — локальна, transliteration.ts).
- Назва знаку: укр./лат., кілька рядків, кілька населених пунктів через
|, довільний відступ/центрування кожного блоку - Елементи знаку (кожен — 0..N незалежних екземплярів, вільний drag по канві): км-позначка, номери доріг (малі/великі бейджі, тип E/H/P), символи/піктограми й стрілки напрямку (лігатури Road UA + окремі SVG-контури — bus/train/ intersection/аеропорт-у-5-напрямках), дорожні знаки (7 стандартних, окрема від символів сутність), смуга «Кінець населеного пункту», роздільна лінія напрямків, хвиля (водойма/річка)
- Кольорові теми (8 офіційних), режими облямівки, скошений правий край для знаку з'їзду з дороги, довільне обертання символу
- Конструктор: drag-and-drop елементів, bounds/padding-оверлей, вимірювальні анотації, сітка з кроком/прозорістю/кольором
- Оверлей "Почніть з прикладу" — готові приклади знаків при порожньому знаку
- Експорт: кольоровий SVG і чорно-білий контурний SVG для KiCad/PCB (текст →
<path>, без залежності від зовнішніх шрифтів) - Збереження знаку/налаштувань — лише файл (
.json), без сервера й безlocalStorage; кожне перезавантаження сторінки стартує з чистих дефолтів
Потрібен Node.js 20+.
npm install
npm run dev
# відкрити URL, який виведе Vite (типово http://localhost:5173/)Vite dev-сервер дає HMR — зміни в .ts/CSS підхоплюються без ручного
перезавантаження сторінки.
npm run build # tsc --noEmit + vite build → dist/
npm run preview # локально підняти зібраний dist/ (перевірка production-білду)npm test # vitest run — усі тести одноразово
npm run test:watch
npm run typecheck # tsc --noEmit окремо, без збіркиVitest, strict: true в tsconfig.json — див. tests/README.md
для розбивки по файлах і відомих прогалин покриття.
develop— основна гілка розробки, усі поточні зміни йдуть сюди.main— лише релізні версії сайту; оновлюється (merge/fast-forward зdevelop) свідомо, окремим кроком, коли поточний стан готовий до релізу. Саме push уmainзапускає деплой (нижче).
.github/workflows/deploy.yml — при push у main: npm ci → typecheck → test →
npm run build → публікація dist/ на GitHub Pages через actions/deploy-pages.
Потрібна одноразова ручна дія: у налаштуваннях репозиторію на GitHub — Settings → Pages → Source — перемкнути на "GitHub Actions" (замість "Deploy from a branch"). Без цього workflow відпрацює, але Pages не підхопить результат.
vite.config.ts's base: './' — відносний шлях, працює з будь-якого
підкаталогу чи кастомного домену без додаткового налаштування.
- Компонування: roadguide.a3.kyiv.ua
- Транслітерація: Постанова КМУ №55 від 27.01.2010
- Шрифт: Road UA (Андрій Константинов,
minttype.com, CC BY-ND 4.0 — див.
public/fonts/RoadUA-LICENSE.txt) / Overpass (Google Fonts fallback) - opentype.js: Frederik De Bleser (MIT —
див.
public/vendor/opentype-LICENSE.txt)
Власний код проєкту — MIT. Шрифт Road UA та opentype.js поширюються на власних умовах (див. вище).
