Миграция с React Flow
У вас приложение на React Flow, а нужны WebGL-канвас, типизированные пины или MCP-поверхность. Две части: механически импортировать граф (один вызов функции) и перевести ментальную модель (здесь библиотеки действительно различаются). Страница покрывает обе части — включая честный список того, что импортёр унести не может.
Импорт графа
Вход — вывод React Flow toObject(); зависимости reactflow не нужно (импортёр без зависимостей,
принимает простой JSON):
import { importFromReactFlow } from '@xenolithengine/graph-editor'
const rfJson = rfInstance.toObject() // { nodes, edges, viewport }const { doc, report } = importFromReactFlow(rfJson, { // опционально: дать синтезированным пинам реальные типы inferType: ({ nodeType, handle }) => nodeType === 'number' ? 'float' : undefined, // undefined → wildcard 'any' // опционально: ноды с типом из schemas используют ЕЁ пины schemas: [myNodeSchemas],})
editor.loadJSON(doc) // или одной строкой: const report = editor.importReactFlow(rfJson, opts)Маппинг почти 1:1 там, где модели совпадают:
| React Flow | xenolith.v1 |
|---|---|
node.id / node.type / node.position | как есть (нет типа → 'default') |
node.data | node.state как есть; data.label дополнительно становится заголовком ноды |
edge.id / source / target / handles | как есть (недостающие id рёбер синтезируются) |
edge.type: default/simplebezier → bezier, straight → linear, step, smoothstep | edge.opts.pathStyle |
edge.animated, edge.label, стрелка markerEnd | edge.opts.animated / .label / .markerEnd |
viewport | viewport |
| Handles, которых касаются рёбра | пины — по одному на уникальный (node, handle, direction); label = handle id, multiple: true, тип из inferType или 'any' |
Handle у ноды с совпадающим типом в schemas[] | типизированные пины схемы; handle резолвится по label (без регистра), числовому индексу или id пина |
Отчёт о потерях
Ничто не выбрасывается молча — импортёр возвращает полный отчёт:
interface ImportReport { counts: { nodes: number; edges: number; pins: number } unknownNodes: string[] // endpoints рёбер на несуществующие ноды (ребро отброшено) unknownHandles: { edgeId, side, nodeId, handle }[] // handle не нашёл пин схемы (ребро отброшено) droppedFields: Record<string, number> // 'node.width': 14, 'edge.style': 3, … warnings: string[] // напр. subflow-parenting, синтезированные 'any'-пины}Полный список того, что не переносится:
- Subflow-иерархия (
parentId/ group-ноды). Дети импортируются как ноды верхнего уровня — сгруппируйте их макросами после импорта (editor.createMacroFromSelection). - Измеренная RF-геометрия (
node.width/node.height,sourcePosition/targetPosition). Xenolith считает размер ноды из схемы; объявленная геометрия пинов — одна из причин, почему он быстр на 1000+ нод. - Состояние представления (
selected,dragging,hidden,style,className,zIndex, …) и стилизация рёбер сверх path style/label/стрелки (edge.style,markerStart, цвета маркеров). - Handles, которых не касается ни одно ребро — JSON React Flow вообще не объявляет handles
(они живут в ваших компонентах), поэтому пины синтезируются только из того, что используют
рёбра. Передайте
schemas[]для полной картины. - Кастомные типы рёбер — всё вне
default/simplebezier/straight/step/smoothstepсчитается вedge.typeи падает обратно в bezier.
Перевод ментальной модели
Рендерер и модель состояния — вот где библиотеки действительно различаются.
| Концепция React Flow | Эквивалент XenolithGraph |
|---|---|
<ReactFlow nodes={} edges={}> (controlled-пропсы) | Документ живёт в редакторе. Чтение — editor.toJSON() / useGraphJSON(). Полная замена — editor.loadJSON(doc), точечная запись — editor.applyChanges(changes) (один шаг undo). graph:changed отдаёт склеенный массив изменений на коммите транзакции, undo-группы или undo/redo — никогда на кадр перетаскивания. |
useNodesState / applyNodeChanges | useNodesState() в @xenolithengine/graph-react и @xenolithengine/graph-vue возвращает { nodes, edges, applyChanges, setNodes, setEdges }. setNodes(next) и setEdges(next) диффят граф одним шагом undo каждый. В Svelte и Solid та же поверхность называется nodesState(), в Angular она на XenolithGraphService. Выделение — не поле узла: читай useSelection() / selection:changed. useNodes / useEdges / useViewport остаются подписками только на чтение. |
<Handle type="source"> в JSX | Типизированные пины, объявленные один раз в NodeSchema ({ direction: 'out', type: 'float', label: 'Out' }). Соединения проверяют типы; any — явный wildcard. |
nodeTypes + кастомный компонент ноды | Схема + DOM-виджеты (обёртки reactWidget, freeFloating-контролы в теле, сайдбар). Тело ноды рисует рендерер — это и есть WebGL-ров; ваш HTML живёт в виджетах. |
node.data | node.state (значения виджетов); импортёр переносит как есть. |
onConnect → addEdge | editor.connect({ source, sourceHandle: 'Out', target, targetHandle: 'In' }) — узел или id, те же селекторы пина, что у позиционной формы (id, label, индекс). null в handle значит единственный пин этого направления. Undoable, проверка типов. setEdges(eds => [...eds, edge]) — форма через массив. |
isValidConnection | editor.setIsValidConnection(predicate) — тот же хук плюс встроенная совместимость типов. |
onNodesChange (drag-обновления) | Ничего на кадр перетаскивания — позиции принадлежат рендереру, пока жест не закоммичен. Коммит приходит как node:moved и как одна запись в массиве graph:changed. |
<Background>, <Controls>, <MiniMap> | Встроено: editor.chrome.setControls(), миникарта, сетка — монтировать компоненты не нужно. |
| Темизация через CSS | Токены дизайна (Xen, Daylight, Liquid Glass) — цвет, геометрия и типографика как данные. |
Тестирование с @testing-library + jsdom | @xenolithengine/graph-test-utils — реальный редактор поднимается в jsdom. |
Самое глубокое различие: React Flow перерисовывает дерево компонентов на каждом кадре перетаскивания; у XenolithGraph рендерер владеет канвасом, DOM неподвижен. React остаётся для панелей, тулбаров и состояния приложения — поверхность редактора императивная и событийная, как map или editor instance, а не дерево пропсов.
После импорта
const report = editor.importReactFlow(rfJson, { schemas: mySchemas })if (report.unknownNodes.length) console.warn('отброшенные рёбра →', report.unknownNodes)editor.view.fitView({ padding: 80 }) // вписать импортированный лейаутeditor.history.clear() // чтобы первый Ctrl+Z не разматывал импортЗарегистрируйте схемы своих типов нод (палитра, виджеты, реальные типы пинов) и переимпортируйте
с schemas — или постобработайте doc перед loadJSON. Приложение продолжает работать с тем же
JSON, который вы версионировали для React Flow.