├── .github ├── FUNDING.yml └── workflows │ ├── commit-linter.yml │ ├── ci.yml │ ├── build-push.yml │ ├── release.yml │ ├── stale.yml │ ├── codeql.yml │ └── update-docs.yml ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── src ├── presets │ ├── minimap │ │ ├── utils.ts │ │ ├── types.ts │ │ ├── MiniNode.vue │ │ ├── MiniViewport.vue │ │ ├── index.ts │ │ └── Minimap.vue │ ├── reroute │ │ ├── utils.ts │ │ ├── types.ts │ │ ├── Pins.vue │ │ ├── Pin.vue │ │ └── index.ts │ ├── classic │ │ ├── vars.sass │ │ ├── components │ │ │ ├── Connection.vue │ │ │ ├── Control.vue │ │ │ ├── Socket.vue │ │ │ ├── ConnectionWrapper.vue │ │ │ └── Node.vue │ │ ├── types.ts │ │ └── index.ts │ ├── index.ts │ ├── context-menu │ │ ├── types.ts │ │ ├── context-vars.sass │ │ ├── utils │ │ │ └── debounce.ts │ │ ├── components │ │ │ ├── Search.vue │ │ │ ├── Block.vue │ │ │ ├── Menu.vue │ │ │ └── Item.vue │ │ └── index.ts │ └── types.ts ├── shims.d.ts ├── types.ts ├── vuecompat │ ├── types.ts │ ├── vue3.ts │ └── vue2.ts ├── Ref.vue ├── shared │ └── drag.ts ├── renderer.ts └── index.ts ├── .gitignore ├── tsconfig.json ├── eslint.config.mjs ├── LICENSE ├── rete.config.ts ├── README.md ├── package.json └── CHANGELOG.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: ni55an 2 | open_collective: rete 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Check out the [Contribution guide](https://retejs.org/docs/contribution) 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Check out the [Code of Conduct](https://retejs.org/docs/code-of-conduct) 2 | -------------------------------------------------------------------------------- /src/presets/minimap/utils.ts: -------------------------------------------------------------------------------- 1 | export function px(value: number) { 2 | return `${value}px` 3 | } 4 | -------------------------------------------------------------------------------- /src/presets/reroute/utils.ts: -------------------------------------------------------------------------------- 1 | export function px(value: number) { 2 | return `${value}px` 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea/ 3 | .vscode/ 4 | npm-debug.log 5 | dist 6 | docs 7 | /coverage 8 | .rete-cli 9 | .sonar 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "rete-cli/configs/tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["DOM"] 5 | }, 6 | "include": ["src"] 7 | } 8 | -------------------------------------------------------------------------------- /src/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { Component } from 'vue' 3 | // eslint-disable-next-line init-declarations 4 | const component: Component 5 | 6 | export default component 7 | } 8 | -------------------------------------------------------------------------------- /.github/workflows/commit-linter.yml: -------------------------------------------------------------------------------- 1 | name: Commit linter 2 | 3 | on: 4 | pull_request: 5 | branches: [ "main", "beta" ] 6 | 7 | jobs: 8 | lint: 9 | uses: retejs/.github/.github/workflows/commit-linter.yml@main 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: [ "main", "beta" ] 7 | 8 | jobs: 9 | ci: 10 | uses: retejs/.github/.github/workflows/ci.yml@main 11 | secrets: inherit 12 | -------------------------------------------------------------------------------- /.github/workflows/build-push.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push 2 | run-name: Build and Push to dist/${{ github.ref_name }} 3 | 4 | on: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | push: 9 | uses: retejs/.github/.github/workflows/build-push.yml@main 10 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ "main", "beta" ] 7 | 8 | jobs: 9 | release: 10 | uses: retejs/.github/.github/workflows/release.yml@main 11 | secrets: inherit 12 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close stale issues and PRs 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '30 1 * * 1' 7 | 8 | jobs: 9 | stale: 10 | uses: retejs/.github/.github/workflows/stale.yml@main 11 | secrets: inherit 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ "main", "beta" ] 7 | pull_request: 8 | branches: [ "main", "beta" ] 9 | 10 | jobs: 11 | codeql: 12 | uses: retejs/.github/.github/workflows/codeql.yml@main 13 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type Position = { x: number, y: number } 2 | 3 | export type RenderSignal = 4 | | { type: 'render', data: { element: HTMLElement, filled?: boolean, type: Type } & Data } 5 | | { type: 'rendered', data: { element: HTMLElement, type: Type } & Data } 6 | -------------------------------------------------------------------------------- /src/presets/classic/vars.sass: -------------------------------------------------------------------------------- 1 | $node-color: rgba(110,136,255,0.8) 2 | $node-color-selected: #ffd92c 3 | $group-color: rgba(15,80,255,0.2) 4 | $group-handler-size: 40px 5 | $group-handler-offset: -10px 6 | $socket-size: 24px 7 | $socket-margin: 6px 8 | $socket-color: #96b38a 9 | $node-width: 180px 10 | -------------------------------------------------------------------------------- /src/presets/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Built-in presets, responsible for rendering different parts of the editor. 3 | * @module 4 | */ 5 | export * as classic from './classic' 6 | export * as contextMenu from './context-menu' 7 | export * as minimap from './minimap' 8 | export * as reroute from './reroute' 9 | 10 | -------------------------------------------------------------------------------- /src/presets/context-menu/types.ts: -------------------------------------------------------------------------------- 1 | import { RenderSignal } from '../../types' 2 | 3 | export type Item = { 4 | label: string 5 | key: string 6 | handler(): void 7 | subitems?: Item[] 8 | } 9 | 10 | export type ContextMenuRender = 11 | | RenderSignal<'contextmenu', { items: Item[], onHide(): void, searchBar?: boolean }> 12 | -------------------------------------------------------------------------------- /.github/workflows/update-docs.yml: -------------------------------------------------------------------------------- 1 | name: Update docs 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: ["main"] 7 | 8 | jobs: 9 | pull: 10 | uses: retejs/.github/.github/workflows/update-docs.yml@main 11 | secrets: inherit 12 | with: 13 | filename: "7.rete-vue-plugin" 14 | package: rete-vue-plugin 15 | -------------------------------------------------------------------------------- /src/presets/context-menu/context-vars.sass: -------------------------------------------------------------------------------- 1 | // this file cannot be called vars.sass, because then it cannot be imported, probably due to a collision with a file from a classic preset 2 | 3 | $context-color: rgba(110,136,255,0.8) 4 | $context-color-light: rgba(130, 153, 255, 0.8) 5 | $context-color-dark: rgba(69, 103, 255, 0.8) 6 | $context-menu-round: 5px 7 | $width: 120px 8 | 9 | $test: 23px 10 | -------------------------------------------------------------------------------- /src/vuecompat/types.ts: -------------------------------------------------------------------------------- 1 | import type VueNamespace from 'vue' 2 | 3 | type Vue = typeof VueNamespace 4 | 5 | export type Context = Vue extends { createApp: (arg: infer U) => any } 6 | ? U 7 | : (Vue extends new(options: infer U) => any ? U : any) 8 | 9 | export type Instance = Vue extends { createApp: (arg: any) => infer U } 10 | ? U 11 | : (Vue extends new(options: any) => infer U ? U : any) 12 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import tseslint from 'typescript-eslint' 2 | import configs from 'rete-cli/configs/eslint.mjs' 3 | import gloals from 'globals' 4 | 5 | export default tseslint.config( 6 | ...configs, 7 | { 8 | languageOptions: { 9 | globals: { 10 | ...gloals.browser 11 | } 12 | }, 13 | rules: { 14 | '@typescript-eslint/unbound-method': 'off' 15 | } 16 | } 17 | ) -------------------------------------------------------------------------------- /src/presets/context-menu/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | export function debounce(delay: number, cb: () => void) { 2 | return { 3 | timeout: null as null | number, 4 | cancel() { 5 | if (this.timeout) { 6 | window.clearTimeout(this.timeout) 7 | this.timeout = null 8 | } 9 | }, 10 | call() { 11 | this.timeout = window.setTimeout(() => { 12 | cb() 13 | }, delay) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/presets/reroute/types.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionId } from 'rete' 2 | 3 | import { RenderSignal } from '../../types' 4 | 5 | type Position = { 6 | x: number 7 | y: number 8 | } 9 | export type Pin = { 10 | id: string 11 | position: Position 12 | selected?: boolean 13 | } 14 | export type PinData = { 15 | id: ConnectionId 16 | pins: Pin[] 17 | } 18 | 19 | export type PinsRender = 20 | | RenderSignal<'reroute-pins', { data: PinData }> 21 | -------------------------------------------------------------------------------- /src/Ref.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 19 | -------------------------------------------------------------------------------- /src/presets/reroute/Pins.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 25 | -------------------------------------------------------------------------------- /src/presets/minimap/types.ts: -------------------------------------------------------------------------------- 1 | import { RenderSignal } from '../../types' 2 | 3 | export type Rect = { 4 | width: number 5 | height: number 6 | left: number 7 | top: number 8 | } 9 | export type Transform = { 10 | x: number 11 | y: number 12 | k: number 13 | } 14 | export type Translate = (dx: number, dy: number) => void 15 | 16 | export type MinimapRender = 17 | | RenderSignal<'minimap', { 18 | ratio: number 19 | nodes: Rect[] 20 | viewport: Rect 21 | start(): Transform 22 | translate: Translate 23 | point(x: number, y: number): void 24 | }> 25 | -------------------------------------------------------------------------------- /src/presets/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-invalid-void-type */ 2 | import { BaseSchemes } from 'rete' 3 | 4 | import { VuePlugin } from '..' 5 | 6 | type ComponentProps = Record | undefined | void | null 7 | type RenderResult = { component: any, props: ComponentProps } | undefined | void | null 8 | 9 | export type RenderPreset = { 10 | attach?: (plugin: VuePlugin) => void 11 | update: (context: Extract, plugin: VuePlugin) => ComponentProps 12 | render: (context: Extract, plugin: VuePlugin) => RenderResult 13 | } 14 | -------------------------------------------------------------------------------- /src/presets/classic/components/Connection.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 12 | 28 | -------------------------------------------------------------------------------- /src/presets/context-menu/components/Search.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | 17 | 30 | -------------------------------------------------------------------------------- /src/presets/minimap/MiniNode.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | 23 | 30 | -------------------------------------------------------------------------------- /src/presets/context-menu/components/Block.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 34 | -------------------------------------------------------------------------------- /src/presets/classic/components/Control.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 25 | 26 | 40 | -------------------------------------------------------------------------------- /src/presets/classic/components/Socket.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 43 | -------------------------------------------------------------------------------- /src/shared/drag.ts: -------------------------------------------------------------------------------- 1 | import { Position } from '../types' 2 | 3 | type Translate = (dx: number, dy: number) => void 4 | type StartEvent = { pageX: number, pageY: number } 5 | 6 | export function useDrag(translate: Translate, getPointer: (e: StartEvent) => Position) { 7 | return { 8 | start(e: StartEvent) { 9 | let previous = { ...getPointer(e) } 10 | 11 | function move(moveEvent: MouseEvent) { 12 | const current = { ...getPointer(moveEvent) } 13 | const dx = current.x - previous.x 14 | const dy = current.y - previous.y 15 | 16 | previous = current 17 | 18 | translate(dx, dy) 19 | } 20 | function up() { 21 | window.removeEventListener('pointermove', move) 22 | window.removeEventListener('pointerup', up) 23 | window.removeEventListener('pointercancel', up) 24 | } 25 | 26 | window.addEventListener('pointermove', move) 27 | window.addEventListener('pointerup', up) 28 | window.addEventListener('pointercancel', up) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/renderer.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 2 | // @ts-ignore 3 | import { create, destroy, update } from 'process.env.VUECOMPAT' 4 | 5 | import { type Props } from './index' 6 | 7 | export type Renderer = { 8 | get(element: Element): I | undefined 9 | mount

(element: Element, vueComponent: any, payload: P, onRendered: any): I 10 | update

(app: I, payload: P): void 11 | unmount(element: Element): void 12 | } 13 | 14 | export function getRenderer(props?: Props): Renderer { 15 | const instances = new Map() 16 | 17 | return { 18 | get(element) { 19 | return instances.get(element) 20 | }, 21 | mount(element, vueComponent, payload, onRendered) { 22 | const app = create(element, vueComponent, payload, onRendered, props) 23 | 24 | instances.set(element, app) 25 | 26 | return app 27 | }, 28 | update(app, payload) { 29 | update(app, payload) 30 | }, 31 | unmount(element) { 32 | const app = instances.get(element) 33 | 34 | if (app) { 35 | destroy(app) 36 | instances.delete(element) 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 "Ni55aN" Vitaliy Stoliarov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/presets/context-menu/index.ts: -------------------------------------------------------------------------------- 1 | import { BaseSchemes } from 'rete' 2 | 3 | import { RenderPreset } from '../types' 4 | import Menu from './components/Menu.vue' 5 | import { ContextMenuRender } from './types' 6 | 7 | /** 8 | * Preset for rendering context menu. 9 | */ 10 | export function setup(props?: { delay?: number }): RenderPreset { 11 | const delay = typeof props?.delay === 'undefined' 12 | ? 1000 13 | : props.delay 14 | 15 | return { 16 | update(context) { 17 | if (context.data.type === 'contextmenu') { 18 | return { 19 | items: context.data.items, 20 | delay, 21 | searchBar: context.data.searchBar, 22 | onHide: context.data.onHide 23 | } 24 | } 25 | }, 26 | render(context) { 27 | if (context.data.type === 'contextmenu') { 28 | return { 29 | component: Menu, 30 | props: { 31 | items: context.data.items, 32 | delay, 33 | searchBar: context.data.searchBar, 34 | onHide: context.data.onHide 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/vuecompat/vue3.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | // @ts-ignore 3 | import { App, createApp, h, markRaw, Ref, ref } from 'vue' 4 | 5 | import { type Props } from './../index' 6 | 7 | type Instance

= { app: App, payload: Ref

} 8 | 9 | export function create

(element: HTMLElement, component: any, payload: P, onRendered: any, props?: Props): Instance

{ 10 | const state = ref(markRaw(payload)) as Ref

11 | 12 | const context = { 13 | render() { 14 | // @ts-ignore 15 | return h(component, { ...state.value, seed: Math.random() }) 16 | }, 17 | mounted() { 18 | onRendered() 19 | }, 20 | updated() { 21 | onRendered() 22 | } 23 | } 24 | 25 | const app = props?.setup 26 | ? props.setup(context) 27 | : createApp(context) 28 | 29 | app.mount(element) 30 | 31 | return { 32 | app, 33 | payload: state 34 | } 35 | } 36 | 37 | export function update

(instance: Instance

, payload: P) { 38 | instance.payload.value = markRaw({ ...instance.payload.value, ...payload }) 39 | } 40 | 41 | export function destroy(instance: Instance) { 42 | instance.app.unmount() 43 | } 44 | -------------------------------------------------------------------------------- /src/vuecompat/vue2.ts: -------------------------------------------------------------------------------- 1 | import Vue, { ComponentOptions } from 'vue' 2 | 3 | import { type Props } from './../index' 4 | 5 | export function create(element: any, component: any, payload: any, onRendered: any, props?: Props) { 6 | const context: ComponentOptions & { payload?: Record } = { 7 | props: ['payload'], 8 | render(h) { 9 | return h(component, { props: { ...this.payload, seed: Math.random() }, ref: 'child' }) 10 | }, 11 | mounted() { 12 | onRendered() 13 | }, 14 | updated() { 15 | onRendered() 16 | } 17 | } 18 | 19 | const app: Vue & { payload?: Record } = props?.setup 20 | ? props.setup(context) as Vue 21 | : new Vue(context) 22 | 23 | app.payload = payload 24 | 25 | const container = document.createElement('div') 26 | 27 | if (container) { 28 | element.appendChild(container) 29 | } 30 | 31 | app.$mount(container || element) 32 | 33 | return app 34 | } 35 | 36 | export function update(app: any, payload: any) { 37 | app.payload = { ...app.payload, ...payload } 38 | 39 | app.$refs.child.$forceUpdate() 40 | } 41 | 42 | export function destroy(app: any) { 43 | app.$destroy() 44 | app.$el.remove() 45 | } 46 | -------------------------------------------------------------------------------- /src/presets/reroute/Pin.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 40 | 41 | 57 | -------------------------------------------------------------------------------- /src/presets/minimap/MiniViewport.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 44 | 45 | 52 | -------------------------------------------------------------------------------- /src/presets/minimap/index.ts: -------------------------------------------------------------------------------- 1 | import { BaseSchemes } from 'rete' 2 | 3 | import { RenderPreset } from '../types' 4 | import Minimap from './Minimap.vue' 5 | import { MinimapRender } from './types' 6 | 7 | /** 8 | * Preset for rendering minimap. 9 | */ 10 | export function setup(props?: { size?: number }): RenderPreset { 11 | return { 12 | update(context) { 13 | if (context.data.type === 'minimap') { 14 | return { 15 | nodes: context.data.nodes, 16 | size: props?.size ?? 200, 17 | ratio: context.data.ratio, 18 | viewport: context.data.viewport, 19 | translate: context.data.translate, 20 | point: context.data.point 21 | } 22 | } 23 | }, 24 | render(context) { 25 | if (context.data.type === 'minimap') { 26 | return { 27 | component: Minimap, 28 | props: { 29 | nodes: context.data.nodes, 30 | size: props?.size ?? 200, 31 | ratio: context.data.ratio, 32 | viewport: context.data.viewport, 33 | translate: context.data.translate, 34 | point: context.data.point 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /rete.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import replace from '@rollup/plugin-replace' 3 | import vue3 from '@vitejs/plugin-vue' 4 | import vue2 from '@vitejs/plugin-vue2' 5 | import { ReteOptions } from 'rete-cli' 6 | import pug from 'rollup-plugin-pug' 7 | import scss from 'rollup-plugin-scss' 8 | 9 | function getPlugins(vueVersion: 2 | 3): NonNullable { 10 | return [ 11 | replace({ 12 | values: { 13 | 'process.env.VUECOMPAT': `./vuecompat/${vueVersion === 2 ? 'vue2' : 'vue3'}` 14 | }, 15 | preventAssignment: true 16 | }), 17 | pug({ 18 | pugRuntime: false 19 | }), 20 | // eslint-disable-next-line no-undef 21 | vueVersion === 2 ? vue2() : vue3({ compiler: require('@vue/compiler-sfc') }), 22 | scss({ 23 | insert: true 24 | }) 25 | ] 26 | } 27 | 28 | const vue3Config = { 29 | output: '.', 30 | plugins: getPlugins(3) 31 | } 32 | const vue2Config = { 33 | output: 'vue2', 34 | plugins: getPlugins(2) 35 | } 36 | 37 | export default [vue3Config, vue2Config].map(({ output, plugins }) => ({ 38 | input: 'src/index.ts', 39 | output, 40 | name: 'ReteVuePlugin', 41 | globals: { 42 | 'rete': 'Rete', 43 | 'rete-area-plugin': 'ReteAreaPlugin', 44 | 'rete-render-utils': 'ReteRenderUtils', 45 | 'vue': 'Vue' 46 | }, 47 | plugins 48 | })) 49 | -------------------------------------------------------------------------------- /src/presets/reroute/index.ts: -------------------------------------------------------------------------------- 1 | import { BaseSchemes } from 'rete' 2 | import { BaseAreaPlugin } from 'rete-area-plugin' 3 | 4 | import { RenderPreset } from '../types' 5 | import Pins from './Pins.vue' 6 | import { PinsRender } from './types' 7 | 8 | type Props = { 9 | translate?: (id: string, dx: number, dy: number) => void 10 | contextMenu?: (id: string) => void 11 | pointerdown?: (id: string) => void 12 | } 13 | 14 | /** 15 | * Preset for rendering pins. 16 | */ 17 | export function setup(props?: Props): RenderPreset { 18 | const getProps = () => ({ 19 | menu: props?.contextMenu ?? (() => null), 20 | translate: props?.translate ?? (() => null), 21 | down: props?.pointerdown ?? (() => null) 22 | }) 23 | 24 | return { 25 | update(context) { 26 | if (context.data.type === 'reroute-pins') { 27 | return { 28 | ...getProps(), 29 | pins: context.data.data.pins 30 | } 31 | } 32 | }, 33 | render(context, plugin) { 34 | if (context.data.type === 'reroute-pins') { 35 | const area = plugin.parentScope>(BaseAreaPlugin) 36 | 37 | return { 38 | component: Pins, 39 | props: { 40 | ...getProps(), 41 | getPointer: () => area.area.pointer, 42 | pins: context.data.data.pins 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Rete.js Vue plugin 2 | ==== 3 | [![Made in Ukraine](https://img.shields.io/badge/made_in-ukraine-ffd700.svg?labelColor=0057b7)](https://stand-with-ukraine.pp.ua) 4 | [![Discord](https://img.shields.io/discord/1081223198055604244?color=%237289da&label=Discord)](https://discord.gg/cxSFkPZdsV) 5 | 6 | **Rete.js plugin** 7 | 8 | ## Key features 9 | 10 | - **Render elements**: visualize an elements such as nodes and connections using Vue.js components 11 | - **Customization**: modify appearance and behavior for a personalized workflow 12 | - **Presets**: predefined Vue.js components for different types of features 13 | - **[Classic](https://retejs.org/docs/guides/renderers/vue#connect-plugin)**: provides a classic visualization of nodes, connections, and controls 14 | - **[Context menu](https://retejs.org/docs/guides/context-menu#render-context-menu)**: provides a classic appearance for `rete-context-menu-plugin` 15 | - **[Minimap](https://retejs.org/docs/guides/minimap#render)**: provides a classic appearance for `rete-minimap-plugin` 16 | - **[Reroute](https://retejs.org/docs/guides/reroute#rendering)**: provides a classic appearance for `rete-connection-reroute-plugin` 17 | 18 | ## Getting Started 19 | 20 | Please refer to the [guide](https://retejs.org/docs/guides/renderers/vue) and [example](https://retejs.org/examples/vue) using this plugin 21 | 22 | ## Contribution 23 | 24 | Please refer to the [Contribution](https://retejs.org/docs/contribution) guide 25 | 26 | ## License 27 | 28 | [MIT](https://github.com/retejs/vue-plugin/blob/main/LICENSE) 29 | -------------------------------------------------------------------------------- /src/presets/classic/components/ConnectionWrapper.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 55 | -------------------------------------------------------------------------------- /src/presets/context-menu/components/Menu.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 55 | 56 | 67 | -------------------------------------------------------------------------------- /src/presets/classic/types.ts: -------------------------------------------------------------------------------- 1 | import { ClassicPreset as Classic, GetSchemes, NodeId } from 'rete' 2 | 3 | import { Position, RenderSignal } from '../../types' 4 | 5 | type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) 6 | ? I 7 | : never 8 | type GetControls< 9 | T extends ClassicScheme['Node'], 10 | Intersection = UnionToIntersection 11 | > = Intersection[keyof Intersection] extends Classic.Control ? Intersection[keyof Intersection] : Classic.Control 12 | type GetSockets< 13 | T extends ClassicScheme['Node'], 14 | Intersection = UnionToIntersection, 15 | Union = Exclude 16 | > = Union extends { socket: any } ? Union['socket'] : Classic.Socket 17 | 18 | export type ClassicScheme = GetSchemes & { isLoop?: boolean }> 19 | 20 | export type Side = 'input' | 'output' 21 | 22 | export type VueArea2D = 23 | | RenderSignal<'node', { payload: T['Node'] }> 24 | | RenderSignal<'connection', { payload: T['Connection'], start?: Position, end?: Position }> 25 | | RenderSignal<'socket', { 26 | payload: GetSockets 27 | nodeId: NodeId 28 | side: Side 29 | key: string 30 | }> 31 | | RenderSignal<'control', { 32 | payload: GetControls 33 | }> 34 | | { type: 'unmount', data: { element: HTMLElement } } 35 | 36 | export type ExtractPayload = Extract, { type: 'render', data: { type: K } }>['data'] 37 | -------------------------------------------------------------------------------- /src/presets/context-menu/components/Item.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 48 | 49 | 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rete-vue-plugin", 3 | "version": "2.1.2", 4 | "description": "", 5 | "scripts": { 6 | "build": "rete build -c rete.config.ts", 7 | "lint": "rete lint", 8 | "doc": "rete doc" 9 | }, 10 | "author": "Vitaliy Stoliarov", 11 | "license": "MIT", 12 | "keywords": [ 13 | "vue", 14 | "vuejs", 15 | "rete", 16 | "Rete.js" 17 | ], 18 | "homepage": "https://retejs.org", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/retejs/vue-plugin.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/retejs/vue-plugin/issues" 25 | }, 26 | "peerDependencies": { 27 | "rete": "^2.0.1", 28 | "rete-area-plugin": "^2.0.0", 29 | "rete-render-utils": "^2.0.0", 30 | "vue": "^2.6 || ^3.2" 31 | }, 32 | "devDependencies": { 33 | "@rollup/plugin-replace": "^5.0.2", 34 | "@vitejs/plugin-vue": "^4.1.0", 35 | "@vitejs/plugin-vue2": "^2.2.0", 36 | "@vue/compiler-sfc": "^3.2.22", 37 | "globals": "^15.9.0", 38 | "rete": "^2.0.1", 39 | "rete-area-plugin": "^2.0.0", 40 | "rete-cli": "~2.0.1", 41 | "rete-render-utils": "^2.0.0", 42 | "rollup-plugin-commonjs": "^9.2.2", 43 | "rollup-plugin-pug": "1.1.1", 44 | "rollup-plugin-sass": "1.12.16", 45 | "rollup-plugin-scss": "^3.0.0", 46 | "rollup-plugin-styles": "^4.0.0", 47 | "sass": "^1.56.0", 48 | "vite": "^4.1.1", 49 | "vue": "^2.7.14", 50 | "vue-template-compiler": "^2.7.14", 51 | "vue3": "npm:vue@^3.2.47" 52 | }, 53 | "overrides": { 54 | "@vitejs/plugin-vue": { 55 | "vue": "^2" 56 | } 57 | }, 58 | "dependencies": { 59 | "@babel/runtime": "^7.21.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/presets/minimap/Minimap.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 64 | 65 | 78 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.1.2](https://github.com/retejs/vue-plugin/compare/v2.1.1...v2.1.2) (2025-04-27) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * remove unnecessary definecomponent imports in vue components for vue 2.6 ([dfb4368](https://github.com/retejs/vue-plugin/commit/dfb436891d8b102f50da055baf3ea10431e3e711)) 7 | 8 | ## [2.1.1](https://github.com/retejs/vue-plugin/compare/v2.1.0...v2.1.1) (2024-08-30) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * update cli and fix linting errors ([ec9bbac](https://github.com/retejs/vue-plugin/commit/ec9bbac07647f66e10736da1664d11ec7429ce98)) 14 | 15 | # [2.1.0](https://github.com/retejs/vue-plugin/compare/v2.0.5...v2.1.0) (2024-01-31) 16 | 17 | 18 | ### Features 19 | 20 | * add setup function to props that allows to create the vue/app instance yourself (in case you want to setup plugins, register components...) ([f752ebf](https://github.com/retejs/vue-plugin/commit/f752ebfaae176e51dc06e7d378f3f635337fb6de)) 21 | 22 | ## [2.0.5](https://github.com/retejs/vue-plugin/compare/v2.0.4...v2.0.5) (2024-01-27) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * **build:** source maps ([d3a77c0](https://github.com/retejs/vue-plugin/commit/d3a77c02968061151e8bc18aa298f8b4048f9005)) 28 | 29 | ## [2.0.4](https://github.com/retejs/vue-plugin/compare/v2.0.3...v2.0.4) (2023-08-10) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * **context-menu:** events on vue 2 ([04751db](https://github.com/retejs/vue-plugin/commit/04751db2e0fa00ca2259617822a1d0ac0ed1d66d)) 35 | 36 | ## [2.0.3](https://github.com/retejs/vue-plugin/compare/v2.0.2...v2.0.3) (2023-08-07) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * **classic:** allow custom controls with default props ([34de981](https://github.com/retejs/vue-plugin/commit/34de981a47b0561b2c4325a59c39353639d350b7)) 42 | 43 | ## [2.0.2](https://github.com/retejs/vue-plugin/compare/v2.0.1...v2.0.2) (2023-07-17) 44 | 45 | 46 | ### Bug Fixes 47 | 48 | * **vue2:** duplicate keys ([3e92baa](https://github.com/retejs/vue-plugin/commit/3e92baaa5dd4e46082c5e4038d592cca42c36619)) 49 | * **vue2:** unmount ([0db40b9](https://github.com/retejs/vue-plugin/commit/0db40b9cbc0ae8a018e33369bb3cd75e82250d17)) 50 | 51 | ## [2.0.1](https://github.com/retejs/vue-plugin/compare/v2.0.0...v2.0.1) (2023-07-17) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * sockets/controls rerender on update ([dabf1e4](https://github.com/retejs/vue-plugin/commit/dabf1e4a37889026c0d7840ddefb7a7991204795)) 57 | * unmount ref components ([437565b](https://github.com/retejs/vue-plugin/commit/437565be46cf906d74783d2508815b377ef0c759)) 58 | 59 | ## v2.0.0-beta.21 60 | 61 | Replace `change` event with `input` of classic Input control 62 | 63 | ## v2.0.0-beta.19 64 | 65 | Breaking changes: 66 | 67 | ```ts 68 | render.addPreset(Presets.reroute.setup({ 69 | translate(id, dx, dy) { 70 | // const { k } = rea.area.transform 71 | // dividing by k isn't needed 72 | reroutePlugin.translate(id, dx, dy); 73 | } 74 | })) 75 | ``` 76 | 77 | ## 2.0.0-beta.18 78 | 79 | Breaking changes: `area` property omitted from `Presets.classic.setup({ area })` 80 | 81 | ## 2.0.0-beta.14 82 | 83 | Implemented presets for minimap and reroute 84 | 85 | 86 | ## 2.0.0-beta.13 87 | 88 | Implemented preset for context menu plugin 89 | -------------------------------------------------------------------------------- /src/presets/classic/components/Node.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 39 | 76 | 77 | 154 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { BaseSchemes, CanAssignSignal, Scope } from 'rete' 2 | 3 | import { RenderPreset } from './presets/types' 4 | import { getRenderer, Renderer } from './renderer' 5 | import { Position, RenderSignal } from './types' 6 | import { Context, Instance } from './vuecompat/types' 7 | 8 | export * as Presets from './presets' 9 | export type { ClassicScheme, VueArea2D } from './presets/classic/types' 10 | export type { RenderPreset } from './presets/types' 11 | export { default as Ref } from './Ref.vue' 12 | 13 | /** 14 | * Signals that can be emitted by the plugin 15 | * @priority 10 16 | */ 17 | export type Produces = 18 | | { type: 'connectionpath', data: { payload: Schemes['Connection'], path?: string, points: Position[] } } 19 | 20 | type Requires = 21 | | RenderSignal<'node', { payload: Schemes['Node'] }> 22 | | RenderSignal<'connection', { payload: Schemes['Connection'], start?: Position, end?: Position }> 23 | | { type: 'unmount', data: { element: HTMLElement } } 24 | 25 | /** 26 | * Vue plugin options used to setup vue instance(s) used by retejs. 27 | */ 28 | export type Props = { 29 | /** 30 | * Use this to setup vue. 31 | * @param [context] to be used for createApp({ ...context }) or new Vue({ ...context }) 32 | * @returns app / vue instance. 33 | */ 34 | setup?: (context: Context) => Instance 35 | } 36 | 37 | /** 38 | * Vue plugin. Renders nodes, connections and other elements using React. 39 | * @priority 9 40 | * @emits connectionpath 41 | * @listens render 42 | * @listens unmount 43 | */ 44 | export class VuePlugin> extends Scope, [Requires | T]> { 45 | renderer: Renderer 46 | presets: RenderPreset[] = [] 47 | owners = new WeakMap>() 48 | 49 | constructor(props?: Props) { 50 | super('vue-render') 51 | this.renderer = getRenderer(props) 52 | 53 | this.addPipe(context => { 54 | if (!context || typeof context !== 'object' || !('type' in context)) return context 55 | 56 | if (context.type === 'unmount') { 57 | this.unmount(context.data.element) 58 | } else if (context.type === 'render') { 59 | if ('filled' in context.data && context.data.filled) { 60 | return context 61 | } 62 | if (this.mount(context.data.element, context as T)) { 63 | return { 64 | ...context, 65 | data: { 66 | ...context.data, 67 | filled: true 68 | } 69 | } as typeof context 70 | } 71 | } 72 | 73 | return context 74 | }) 75 | } 76 | 77 | setParent(scope: Scope | T>): void { 78 | super.setParent(scope) 79 | 80 | this.presets.forEach(preset => { 81 | if (preset.attach) preset.attach(this) 82 | }) 83 | } 84 | 85 | private unmount(element: HTMLElement) { 86 | this.owners.delete(element) 87 | this.renderer.unmount(element) 88 | } 89 | 90 | private mount(element: HTMLElement, context: T) { 91 | const existing = this.renderer.get(element) 92 | const parent = this.parentScope() 93 | 94 | if (existing) { 95 | this.presets.forEach(preset => { 96 | if (this.owners.get(element) !== preset) return 97 | const result = preset.update(context as Extract, this) 98 | 99 | if (result) { 100 | this.renderer.update(existing, result) 101 | } 102 | }) 103 | return true 104 | } 105 | 106 | for (const preset of this.presets) { 107 | const result = preset.render(context as Extract, this) 108 | 109 | if (!result) continue 110 | 111 | this.renderer.mount( 112 | element, 113 | result.component, 114 | result.props, 115 | () => parent.emit({ type: 'rendered', data: (context as Requires).data } as T) 116 | ) 117 | 118 | this.owners.set(element, preset) 119 | return true 120 | } 121 | } 122 | 123 | /** 124 | * Adds a preset to the plugin. 125 | * @param preset Preset that can render nodes, connections and other elements. 126 | */ 127 | public addPreset(preset: RenderPreset extends true ? K : 'Cannot apply preset. Provided signals are not compatible'>) { 128 | const local = preset as RenderPreset 129 | 130 | if (local.attach) local.attach(this) 131 | this.presets.push(local) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/presets/classic/index.ts: -------------------------------------------------------------------------------- 1 | import { ClassicPreset, Scope } from 'rete' 2 | import { classicConnectionPath, getDOMSocketPosition, loopConnectionPath, SocketPositionWatcher } from 'rete-render-utils' 3 | import Vue, { DefineComponent, VueConstructor } from 'vue' 4 | 5 | import { Position } from '../../types' 6 | import { RenderPreset } from '../types' 7 | import Connection from './components/Connection.vue' 8 | import ConnectionWrapper from './components/ConnectionWrapper.vue' 9 | import Control from './components/Control.vue' 10 | import Node from './components/Node.vue' 11 | import Socket from './components/Socket.vue' 12 | import { ClassicScheme, ExtractPayload, VueArea2D } from './types' 13 | 14 | export { default as Connection } from './components/Connection.vue' 15 | export { default as Control } from './components/Control.vue' 16 | export { default as Node } from './components/Node.vue' 17 | export { default as Socket } from './components/Socket.vue' 18 | 19 | type Component> = 20 | | VueConstructor, Props>> 21 | | DefineComponent 22 | 23 | type CustomizationProps = { 24 | node?: (data: ExtractPayload) => Component | null 25 | connection?: (data: ExtractPayload) => Component | null 26 | socket?: (data: ExtractPayload) => Component | null 27 | control?: (data: ExtractPayload) => Component | null 28 | } 29 | type ClassicProps = { 30 | socketPositionWatcher?: SocketPositionWatcher> 31 | customize?: CustomizationProps 32 | } 33 | 34 | /** 35 | * Classic preset for rendering nodes, connections, controls and sockets. 36 | */ 37 | export function setup>(props?: ClassicProps): RenderPreset { 38 | const positionWatcher = typeof props?.socketPositionWatcher === 'undefined' 39 | ? getDOMSocketPosition() 40 | : props.socketPositionWatcher 41 | const { node, connection, socket, control } = props?.customize ?? {} 42 | 43 | return { 44 | attach(plugin) { 45 | positionWatcher.attach(plugin as unknown as Scope) 46 | }, 47 | update(context, plugin) { 48 | const { payload } = context.data 49 | const parent = plugin.parentScope() 50 | 51 | if (!parent) throw new Error('parent') 52 | const emit = parent.emit.bind(parent) 53 | 54 | if (context.data.type === 'node') { 55 | return { data: payload, emit } 56 | } else if (context.data.type === 'connection') { 57 | const { start, end } = context.data 58 | 59 | return { 60 | data: payload, 61 | ...start 62 | ? { start } 63 | : {}, 64 | ...end 65 | ? { end } 66 | : {} 67 | } 68 | } 69 | return { data: payload } 70 | }, 71 | // eslint-disable-next-line max-statements, complexity 72 | render(context, plugin) { 73 | const parent = plugin.parentScope() 74 | const emit = parent.emit.bind(parent) 75 | 76 | if (context.data.type === 'node') { 77 | const component = node 78 | ? node(context.data) 79 | : Node 80 | 81 | return component && { 82 | component, 83 | props: { 84 | data: context.data.payload, 85 | emit 86 | } 87 | } 88 | } else if (context.data.type === 'connection') { 89 | const component = connection 90 | ? connection(context.data) 91 | : Connection 92 | const { payload } = context.data 93 | const { source, target, sourceOutput, targetInput } = payload 94 | 95 | return component && { 96 | component: ConnectionWrapper, 97 | props: { 98 | data: context.data.payload, 99 | component, 100 | start: context.data.start ?? ((change: any) => positionWatcher.listen(source, 'output', sourceOutput, change)), 101 | end: context.data.end ?? ((change: any) => positionWatcher.listen(target, 'input', targetInput, change)), 102 | path: async (start: Position, end: Position) => { 103 | const response = await plugin.emit({ type: 'connectionpath', data: { payload, points: [start, end] } }) 104 | 105 | if (!response) return '' 106 | 107 | const { path, points } = response.data 108 | const curvature = 0.3 109 | 110 | if (!path && points.length !== 2) throw new Error('cannot render connection with a custom number of points') 111 | if (!path) return payload.isLoop 112 | ? loopConnectionPath(points as [Position, Position], curvature, 120) 113 | : classicConnectionPath(points as [Position, Position], curvature) 114 | 115 | return path 116 | } 117 | } 118 | } 119 | } else if (context.data.type === 'socket') { 120 | const { payload } = context.data 121 | const component = socket 122 | ? socket(context.data) 123 | : Socket 124 | 125 | return { 126 | component, 127 | props: { 128 | data: payload 129 | } 130 | } 131 | } else if (context.data.type === 'control') { 132 | const { payload } = context.data 133 | 134 | if (control) { 135 | const component = control(context.data) 136 | 137 | return component && { 138 | component, 139 | props: { 140 | data: payload 141 | } 142 | } 143 | } 144 | 145 | return context.data.payload instanceof ClassicPreset.InputControl 146 | ? { 147 | component: Control, 148 | props: { 149 | data: payload 150 | } 151 | } 152 | : null 153 | } 154 | } 155 | } 156 | } 157 | --------------------------------------------------------------------------------