├── .editorconfig
├── .eslintrc.js
├── .gitignore
├── README.md
├── dist
└── index.html
├── license.txt
├── package-lock.json
├── package.json
├── specs.pdf
├── src
├── Canvas.ts
├── Component.ts
├── ConsoleWriter.ts
├── Draggable.ts
├── Editor.ts
├── Finder.ts
├── Graph.ts
├── GraphEventHandler.ts
├── GraphObject.ts
├── GraphObjectStore.ts
├── Link.ts
├── Linkable.ts
├── Path.ts
├── controller
│ ├── EventHandler.ts
│ ├── KeyCode.ts
│ ├── KeyEventHandler.ts
│ ├── MouseCode.ts
│ ├── MouseEventHandler.ts
│ ├── MouseWheelEventHandler.ts
│ └── state
│ │ ├── EditorState.ts
│ │ ├── FinderState.ts
│ │ ├── StateManager.ts
│ │ └── interfaces.ts
├── dock
│ ├── Dock.ts
│ ├── EmptyDock.ts
│ ├── InDataDock.ts
│ ├── InExeDock.ts
│ ├── MultipleSocket.ts
│ ├── OutDataDock.ts
│ ├── OutExeDock.ts
│ ├── Socket.ts
│ ├── UniqueSocket.ts
│ └── interfaces.ts
├── index.ts
├── interpreter
│ ├── ExecutionTree.ts
│ ├── GraphEventHandler.ts
│ ├── Scope.ts
│ ├── interfaces.ts
│ ├── process
│ │ ├── ControlFlowProcess.ts
│ │ ├── CustomProcess.ts
│ │ ├── GetterProcess.ts
│ │ ├── Process.ts
│ │ └── SetterProcess.ts
│ └── router
│ │ ├── ControlFlowRouter.ts
│ │ ├── DefaultRouter.ts
│ │ ├── NullRouter.ts
│ │ └── Router.ts
├── node
│ ├── ControlFlowNode.ts
│ ├── FunctionNode.ts
│ ├── GetterNode.ts
│ ├── Node.ts
│ ├── OperatorNode.ts
│ ├── SetterNode.ts
│ └── interfaces.ts
├── types.ts
├── utils.ts
└── view
│ ├── CanvasElement.ts
│ ├── CanvasZoom.ts
│ ├── DockElement.ts
│ ├── Element.ts
│ ├── LinkElement.ts
│ ├── NodeElement.ts
│ └── Template.ts
├── style
├── finder.less
├── input.less
├── main.less
└── node.less
├── tsconfig.json
└── webpack.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,jsx,ts,tsx,vue}]
2 | indent_style = space
3 | indent_size = 2
4 | end_of_line = lf
5 | trim_trailing_whitespace = true
6 | insert_final_newline = true
7 | max_line_length = 100
8 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true,
5 | },
6 | parser: '@typescript-eslint/parser',
7 | extends: [
8 | 'plugin:@typescript-eslint/recommended'
9 | ],
10 | parserOptions: {
11 | ecmaVersion: 2020,
12 | sourceType: 'module',
13 | },
14 | rules: {
15 | 'semi': ['error', 'never'],
16 | '@typescript-eslint/no-unused-vars': ['warn', { 'argsIgnorePattern': '^_', 'varsIgnorePattern': '^_' }],
17 | },
18 | ignorePatterns: [ "dist/*" ],
19 | }
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.ignore
3 | .vscode
4 |
5 | jsdoc.json
6 | *.pdf
7 | *.css
8 | dist/*
9 | !dist/index.html
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # CodeGraph [](https://badge.fury.io/gh/WonJunior%2FCodeGraph)[](https://reposs.herokuapp.com/?path=WonJunior/CodeGraph&color=ff69b4) [](https://opensource.org/licenses/MIT)
3 |
4 | **Node based programming editor** to create code with a visual interface.
5 |
6 | This is very much a work in progress! Objective for 2021/2022 has shifted towards building a Pytorch binding through a Python kernel.
7 |
8 | Read **[specs.pdf](./specs.pdf)** for a full description of the project.
9 |
10 | ---
11 |
12 | ##### Codegraph philosophy: code reuse through componentization
13 |
14 | Codegraph can work with an existing code base, the objective is not to translate this code into a complete graph. Instead the aim is to rather to create high level components based on this code base. This allows us to use the codebase in the graph to build new components. Codegraph is not a dependency, it should allow anyone to get in and out very easily. Therefore, it needs to be able to compile node graphs into code.
15 | 
16 |
17 | ---
18 |
19 | ##### cg2020 - release 0.1 (Mar 13) - Data propagation and tree execution with detailed logs
20 |
21 | 
22 |
23 | ---
24 |
25 | ##### Screenshot from previous build: Linking system, Node Finder and Live feedback
26 |
27 | 
28 |
29 | 
30 |
31 | 
32 |
33 |
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Codegraph cg2022
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
23 |
24 |
25 |
26 |
27 |
37 |
38 |
39 |
40 |
54 |
55 |
56 |
57 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
90 |
91 |
92 |
93 | |
94 |
95 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright CodeGraph 2017-2021 by Ivan (https://github.com/WonJunior)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "codegraph",
3 | "version": "1.0.0",
4 | "description": "Node based programming editor that allows you to create code with a visual interface.",
5 | "main": "index.js",
6 | "directories": {
7 | "doc": "docs"
8 | },
9 | "scripts": {
10 | "start": "webpack-dev-server --mode development"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/WonJunior/CodeGraph.git"
15 | },
16 | "author": "Ivan Lopes",
17 | "license": "MIT",
18 | "bugs": {
19 | "url": "https://github.com/WonJunior/CodeGraph/issues"
20 | },
21 | "homepage": "https://github.com/WonJunior/CodeGraph#readme",
22 | "devDependencies": {
23 | "cloc": "^2.7.0",
24 | "@typescript-eslint/eslint-plugin": "^4.5.0",
25 | "@typescript-eslint/parser": "^4.5.0",
26 | "eslint": "^7.11.0",
27 | "eslint-webpack-plugin": "^2.1.0",
28 | "ts-loader": "^8.0.6",
29 | "typescript": "^4.0.3",
30 | "webpack": "^4.41.3",
31 | "webpack-cli": "^3.3.10",
32 | "webpack-dev-server": "^3.9.0"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/specs.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wonjunior/CodeGraph/bc3525305879653f43c5ed969c40f791e8f9fd38/specs.pdf
--------------------------------------------------------------------------------
/src/Canvas.ts:
--------------------------------------------------------------------------------
1 | import { Pair } from '@/types'
2 | import { assert, clamp, pair, zip } from '@/utils'
3 | import CanvasElement from '@/view/CanvasElement'
4 | import CanvasZoom from '@/view/CanvasZoom'
5 | import { GraphObject, GraphObjectItem } from './GraphObject'
6 |
7 | export default class Canvas extends GraphObject {
8 | public element: CanvasElement
9 | public zoom: CanvasZoom
10 |
11 | public get binds(): Array {
12 | return [[this.element.positionWrapper, this]]
13 | }
14 |
15 | set position(position: Pair) {
16 | const pos = this.boundaryClamp(position)
17 | this.element.position = pos
18 | this.element.updateInfo(this.coordinates)
19 | }
20 |
21 | constructor(parent: HTMLElement) {
22 | super()
23 | this.element = new CanvasElement(parent)
24 | this.zoom = new CanvasZoom(this, this.element.zoomWrapper)
25 | this.position = pair(0)
26 | // const x = this.element.getProperties()
27 | // const pos = [-x[0][3]/2, -x[1][3]/2] as Pair
28 | // this.position = pos
29 | }
30 |
31 | recalculatePosition(): void { //? update position
32 | this.position = this.element.position
33 | }
34 |
35 | mousePosition(event: MouseEvent): Pair {
36 | const [ x, y ] = [ event.clientX, event.clientY ]
37 | const [ offsetX, offsetY ] = this.element.offset
38 | return [ (x - offsetX) / this.zoom.level, (y - offsetY) / this.zoom.level ]
39 | }
40 |
41 | getLimitValues(): Pair> {
42 | return this.element.getProperties().map(([pos, offset, parentOffset, size, parentSize]) => {
43 | const maxValue = (parentOffset - offset) / this.zoom.level + pos
44 | const minValue = maxValue + (parentSize - size) / this.zoom.level
45 | return [ minValue, maxValue ]
46 | }) as Pair>
47 | }
48 |
49 | boundaryClamp(position: Pair): Pair {
50 | return zip(position, this.getLimitValues())
51 | .map(([value, limits]) => clamp(value, ...limits)) as Pair
52 | }
53 |
54 | private get coordinates(): Pair {
55 | return this.element.getProperties().map(([_0, offset, parentOffset, _3, _4]) => {
56 | return (parentOffset - offset) / this.zoom.level
57 | }) as Pair
58 | }
59 | }
--------------------------------------------------------------------------------
/src/Component.ts:
--------------------------------------------------------------------------------
1 | import { DockDef } from './dock/interfaces'
2 | import Graph from './Graph'
3 | import { NodeInstance } from './node/interfaces'
4 | import SetterNode from './node/SetterNode'
5 | import GetterNode from './node/GetterNode'
6 | import OperatorNode from './node/OperatorNode'
7 |
8 | export default class Component {
9 | private graph: Graph
10 | private getter: GetterNode
11 | private setter: SetterNode
12 |
13 | constructor(element: HTMLElement, private name: string, private inputs: DockDef[]) {
14 | this.graph = new Graph(element)
15 |
16 | this.getter = this.graph.add({
17 | cstr: GetterNode,
18 | label: 'in',
19 | header: 'pink',
20 | background: '·',
21 | position: [470, 100],
22 | process: {
23 | compute: function() { return null },
24 | string: function() { return 'yes' },
25 | result: inputs,
26 | }
27 | })
28 |
29 | this.setter = this.graph.add({
30 | cstr: SetterNode,
31 | label: 'out',
32 | header: 'pink',
33 | background: '·',
34 | position: [750, 100],
35 | process: {
36 | compute: function() { return null },
37 | string: function() { return 'yes' },
38 | params: [],
39 | }
40 | })
41 | }
42 |
43 | public get instance(): NodeInstance {
44 | return {
45 | cstr: OperatorNode,
46 | label: this.name,
47 | header: 'pink',
48 | background: '',
49 | position: [0, 0],
50 | process: {
51 | compute: function() { return null },
52 | string: function() { return 'yes' },
53 | params: this.inputs,
54 | result: []
55 | }
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/src/ConsoleWriter.ts:
--------------------------------------------------------------------------------
1 | import { space } from '@/utils'
2 |
3 | export enum Consoles { EVENT, EXECUTION, STATE, LINKABLE, DRAGGABLE, GRAPHEVENT }
4 |
5 |
6 | /**
7 | * Logger generator enabled on debug mode outputs in a tree-like structure.
8 | */
9 | export class Writer {
10 | private static spacer = 11
11 | private static consoles = {
12 | [Consoles.EVENT]: { active: false, color: 'navy', label: 'Ev' },
13 | [Consoles.EXECUTION]: { active: true, color: 'green', label: 'Ex' },
14 | [Consoles.STATE]: { active: true, color: 'grey', label: 'St' },
15 | [Consoles.LINKABLE]: { active: false, color: 'magenta', label: 'Lk' },
16 | [Consoles.DRAGGABLE]: { active: false, color: 'purple', label: 'Dg' },
17 | [Consoles.GRAPHEVENT]: { active: true, color: '#4285f4', label: 'GE' },
18 | }
19 |
20 | private active: boolean
21 | private head = ''
22 | private label: string
23 | private color: string
24 |
25 | constructor(private type: Consoles) { //? of just Consoles
26 | const { active, color, label } = Writer.consoles[type]
27 | Object.assign(this, { active, color, label: this.getLabel(label) })
28 | }
29 |
30 | indent(): void {
31 | this.head += ' '
32 | }
33 |
34 | pipe(): void {
35 | this.head += ' |'
36 | }
37 |
38 | unindent(): void {
39 | this.head = this.head.slice(0,-2)
40 | }
41 |
42 | newline(): void {
43 | if (this.active) console.log('')
44 | }
45 |
46 | log(...args: T): void {
47 | if (!this.active) return;
48 | console.log(`%c ${this.label} %c`, `background: ${this.color} color: white`, '', this.head, ...args)
49 | }
50 |
51 | private getLabel(text: string): string {
52 | const gap = Writer.spacer - text.length
53 | if (gap < 2) return ` ${text.slice(0, gap-3)}.`
54 | return `${space(Math.ceil(gap/2))}${text}${space(Math.floor(gap/2))}`
55 | }
56 | }
--------------------------------------------------------------------------------
/src/Draggable.ts:
--------------------------------------------------------------------------------
1 | import { Pair } from '@/types'
2 | import { assert, normalize, pair, zip } from '@/utils'
3 | import CanvasZoom from '@/view/CanvasZoom'
4 | import Element from '@/view/Element'
5 |
6 | interface Placeable {
7 | position: Pair
8 | }
9 |
10 | interface DragOptions {
11 | type: DragType,
12 | position: MousePosition,
13 | element: HTMLElement,
14 | object: Placeable,
15 | zoom: CanvasZoom,
16 | callback?: () => void
17 | }
18 |
19 | export interface MousePosition {
20 | clientX: number,
21 | clientY: number
22 | }
23 |
24 | export enum DragType {
25 | DRAG, STICK
26 | }
27 |
28 | export class Draggable {
29 | public element: HTMLElement
30 | public object: Placeable
31 | public zoom: CanvasZoom
32 | private offset: Pair
33 | public callback: () => void
34 |
35 | constructor({ type, position, element, object, zoom, callback }: DragOptions) {
36 | // console.log('Draggable', type, element, object, zoom)
37 | // just setter position
38 |
39 | // $.Draggable.log(`┌── Starting dragging`, element)
40 | this.element = element
41 | this.object = object
42 | this.callback = callback || (() => void 0)
43 | this.zoom = zoom
44 |
45 | this[type](position)
46 | }
47 |
48 | [DragType.STICK](position: MousePosition): void {
49 | assert(this.element.parentElement)
50 | const parentProp = this.element.parentElement.getBoundingClientRect() //? use custom Element
51 | this.offset = [ parentProp.x + 50, parentProp.y + 10 ] as Pair
52 |
53 | this.dragging(position)
54 | document.addEventListener('mousemove', this.dragging)
55 | document.addEventListener('mousedown', this.endDrag, { once: true })
56 | }
57 |
58 | [DragType.DRAG]({ clientX, clientY }: MousePosition): void {
59 | const selfPos = this.element.getBoundingClientRect()
60 | assert(this.element.parentElement)
61 | const parentPos = this.element.parentElement.getBoundingClientRect()
62 | this.offset = [ clientX - selfPos.x + parentPos.x, clientY - selfPos.y + parentPos.y ] //# zip map normalize
63 |
64 | // const offset = zip([clientX, clientY], [selfPos.x, selfPos.y], [], [parentPos.x, parentPos.y]).map(normalize)
65 |
66 | document.addEventListener('mousemove', this.dragging)
67 | document.addEventListener('mouseup', this.endDrag, { once: true })
68 | }
69 |
70 | dragging = ({ clientX, clientY }: MousePosition): void => {
71 | // $.Draggable.log(`├──> client=[${e.clientX}, ${e.clientY}], offset=${this.offset}`)
72 | const pos = zip([clientX, clientY], this.offset, pair(this.zoom.level), []).map(normalize)
73 |
74 | // $.Draggable.pipe()
75 | // $.Draggable.log(`└──> new position = [${targetPosition}]`)
76 | // $.Draggable.indent()
77 | this.object.position = pos as Pair
78 | // $.Draggable.unindent()
79 | // $.Draggable.unindent()
80 |
81 | this.callback()
82 | }
83 |
84 | endDrag = (): void => {
85 | // $.Draggable.log('└──/ dragging ended')
86 | document.removeEventListener('mousemove', this.dragging)
87 | }
88 | }
--------------------------------------------------------------------------------
/src/Editor.ts:
--------------------------------------------------------------------------------
1 |
2 | export default class Editor {
3 | public static state = {
4 | default: Symbol('editor.default')
5 | }
6 | }
--------------------------------------------------------------------------------
/src/Finder.ts:
--------------------------------------------------------------------------------
1 |
2 | export default class Finder {
3 | public static state: symbol
4 |
5 | // get visible() {
6 | // return this.finderElement.style.display != 'none'
7 | // }
8 |
9 | // set visible(state) {
10 | // if (state) this.hideResultBox()
11 |
12 | // const style = !!state ? 'block' : 'none'
13 | // this.finderElement.style.display = style
14 | // }
15 |
16 | // constructor({ name, data, container, placeholder }) {
17 | // Object.assign(this, {
18 | // // name,
19 | // container,
20 | // placeholder,
21 | // // hovering: false
22 | // })
23 |
24 | // this.create()
25 |
26 | // this.data = Object.entries(data).map(([ ref, obj ]) => ({ ref, obj })) // comes from the library
27 | // this.results = {}
28 | // this.populate()
29 | // }
30 |
31 | // create() {
32 | // let template = document.querySelector('template#finder')
33 | // template = document.importNode(template.content, true)
34 |
35 | // this.finderElement = template.querySelector('.finder')
36 | // this.wrapperElement = template.querySelector('.search-wrap')
37 | // this.tableElement = template.querySelector('.search-table')
38 | // this.inputElement = template.querySelector('input')
39 |
40 | // this.inputElement.placeholder = this.placeholder
41 |
42 | // this.container.appendChild(this.finderElement)
43 | // }
44 |
45 | // populate() {
46 | // this.data.forEach(({ ref, obj }) => {
47 | // let template = document.querySelector('template#finderResult')
48 | // template = document.importNode(template.content, true)
49 |
50 | // template.querySelector('td').textContent = obj.label
51 | // template.querySelector('td').id = ref
52 |
53 | // const element = template.querySelector('tr')
54 |
55 | // this.tableElement.appendChild(element)
56 |
57 | // this.results[ ref ] = { element: element, obj }
58 | // })
59 | // }
60 |
61 | // isIn(input, data) {
62 | // return Boolean(~data.toLowerCase().indexOf(input.toLowerCase()))
63 | // }
64 |
65 | // filter(input, data) {
66 | // return input == '*' || (input && this.isIn(input, data))
67 | // }
68 |
69 | // search(input) {
70 | // let noResults = true
71 |
72 | // this.data.forEach(({ ref, obj } , i) => {
73 | // if (this.filter(input, obj.meta)) {
74 | // noResults = false
75 | // this.displayResult(ref)
76 | // } else {
77 | // this.hideResult(ref)
78 | // }
79 | // })
80 |
81 | // if (noResults) {
82 | // this.hideResultBox()
83 | // } else {
84 | // this.displayResultBox()
85 | // }
86 | // }
87 |
88 | // displayResultBox() {
89 | // this.wrapperElement.classList.remove('hidden') // could use handleDisplay('add', Element)
90 | // }
91 |
92 | // hideResultBox() {
93 | // this.wrapperElement.classList.add('hidden')
94 | // }
95 |
96 | // displayResult(ref) {
97 | // this.results[ ref ].element.classList.remove('hidden')
98 | // }
99 |
100 | // hideResult(ref) {
101 | // this.results[ ref ].element.classList.add('hidden')
102 | // }
103 |
104 | // show() {
105 | // this.visible = true
106 | // this.inputElement.focus()
107 | // State.change(Finder.state)
108 | // }
109 |
110 | // hide() {
111 | // this.visible = false
112 | // this.inputElement.value = ""
113 | // State.change(Editor.state.default)
114 | // }
115 |
116 | // select(event) {
117 | // this.hide()
118 | // return new Node(this.results[ event.target.id ].obj)
119 | // }
120 |
121 | // down() {}
122 |
123 | // up() {}
124 | }
--------------------------------------------------------------------------------
/src/Graph.ts:
--------------------------------------------------------------------------------
1 | import Canvas from '@/Canvas'
2 | import GraphObjectStore from '@/GraphObjectStore'
3 | import Node from '@/node/Node'
4 | import { GraphEventHandler, GraphInputEvent } from './GraphEventHandler'
5 | import Link from './Link'
6 | import { NodeInstance } from './node/interfaces'
7 |
8 |
9 | export default class Graph {
10 | // private static all = new WeakMap()
11 |
12 | private nodes = new Set()
13 | public canvas: Canvas
14 | private store = new GraphObjectStore()
15 | // public graphEventHandler = new GraphEventHandler()
16 |
17 | // public static get(key: HTMLElement): Graph {
18 | // const canvas = CanvasElement.closestCanvas(key)
19 | // if (canvas == null) throw new Error(`could not find canvas: ${key}`)
20 |
21 | // // return Graph.all.get(key) || Graph.all.get(canvas) //? for now cast
22 | // }
23 |
24 | private resolver = (target?: EventTarget | null | undefined): GraphInputEvent => {
25 | return {
26 | graph: this,
27 | object: this.store.get( target),
28 | eventHandler: {
29 | receiver: this.eventReceiver,
30 | resolver: this.resolver,
31 | }
32 | }
33 | }
34 |
35 | private get eventReceiver(): HTMLElement {
36 | return this.canvas.element.container
37 | }
38 |
39 | constructor(parent: HTMLElement) {
40 | this.canvas = new Canvas(parent)
41 | this.store.bind(this.canvas.binds[0])
42 | new GraphEventHandler(this, this.store)
43 | }
44 |
45 | add({ cstr, ...args }: NodeInstance): N {
46 | const node = new cstr(this, args)
47 | node.binds.forEach(this.store.bind)
48 | return this.register(node)
49 | }
50 |
51 | // add(component: Component): Node {
52 | // const node = component.instanciate(this)
53 | // node.binds.forEach(this.store.bind)
54 | // return this.register(node)
55 | // }
56 |
57 | addLink(link: Link): void {
58 | this.store.bind(link.binds[0])
59 | }
60 |
61 | removeLink(link: Link): void {
62 | link.binds.forEach(([key, _]) => this.store.unbind(key))
63 | }
64 |
65 | unregister(node: Node): boolean {
66 | return this.nodes.delete(node)
67 | }
68 |
69 | private register(node: Node): N {
70 | return this.nodes.add(node) && node
71 | }
72 | }
--------------------------------------------------------------------------------
/src/GraphEventHandler.ts:
--------------------------------------------------------------------------------
1 | import EventHandler from './controller/EventHandler'
2 | import { EditorDefaultState } from './controller/state/EditorState'
3 | import Graph from './Graph'
4 | import { GraphObject } from './GraphObject'
5 | import GraphObjectStore from './GraphObjectStore'
6 |
7 | export interface GraphInputEvent {
8 | graph: Graph
9 | object: GraphObject | null
10 | eventHandler: HandlerParams
11 | }
12 |
13 | export interface HandlerParams { //# rename this kind of EventHandler...
14 | receiver: HTMLElement
15 | resolver: (target?: Element | null | undefined) => GraphInputEvent
16 | }
17 |
18 | export class GraphEventHandler {
19 | constructor(private graph: Graph, private store: GraphObjectStore) {
20 | new EventHandler(EditorDefaultState, this.receiver, this.resolver)
21 | }
22 |
23 | resolver = (target?: EventTarget | null | undefined): GraphInputEvent => {
24 | return {
25 | graph: this.graph,
26 | object: this.store.get( target),
27 | eventHandler: this
28 | }
29 | }
30 |
31 | get receiver(): HTMLElement {
32 | return this.graph.canvas.element.container
33 | }
34 | }
--------------------------------------------------------------------------------
/src/GraphObject.ts:
--------------------------------------------------------------------------------
1 |
2 | export abstract class GraphObject {
3 | public abstract get binds(): Array
4 | }
5 |
6 | export type GraphObjectItem = [HTMLElement, GraphObject]
--------------------------------------------------------------------------------
/src/GraphObjectStore.ts:
--------------------------------------------------------------------------------
1 | import { GraphObject, GraphObjectItem } from '@/GraphObject'
2 |
3 | export default class GraphObjectStore {
4 | private data = new WeakMap()
5 |
6 | public bind = ([key, object]: GraphObjectItem): void => { //# bound?
7 | this.data.set(key, object)
8 | }
9 |
10 | public get = (key: HTMLElement): GraphObject | null => { //# bound?
11 | return this.data.get(key) || null
12 | }
13 |
14 | public unbind = (key: HTMLElement): void => {
15 | this.data.delete(key)
16 | }
17 | }
--------------------------------------------------------------------------------
/src/Link.ts:
--------------------------------------------------------------------------------
1 | import Graph from '@/Graph'
2 | import { GraphObject, GraphObjectItem } from '@/GraphObject'
3 | import { DockSide, InDock, FlowType } from '@/dock/interfaces'
4 | import UniqueSocket from '@/dock/UniqueSocket'
5 | import Dock from '@/dock/Dock'
6 | import LinkElement from '@/view/LinkElement'
7 | import { TriggerArgs } from '@/interpreter/interfaces'
8 | import { Pair } from '@/types'
9 | import { assert } from '@/utils'
10 | import Socket from './dock/Socket'
11 |
12 | /**
13 | * Represent a link object used to connect two nodes together
14 | * Edited links (those that don't yet have an end dock are not registered in `Links`)
15 | */
16 | export default class Link extends GraphObject {
17 | private element: LinkElement
18 |
19 | public get binds(): Array {
20 | return [[this.element.container, this]]
21 | }
22 |
23 | public static get(origin: Socket, graph: Graph): Link {
24 | if (origin instanceof UniqueSocket && origin.occupied) return origin.editLink()
25 | return new Link(origin, null, graph)
26 | }
27 |
28 | // public tmp?: Dock
29 | // public start?: OutDock
30 | // public end?: InDock
31 |
32 | public get type(): FlowType {
33 | return this.start.type
34 | }
35 |
36 | public get origin(): Dock {
37 | return this.start
38 | }
39 |
40 | /**
41 | * Adds a new link object to the Canvas or edit a link if already exists
42 | * @param start the dock from which the link has been pulled
43 | * @param end a dock instance if second dock is known
44 | */
45 | constructor(private start: Socket, private end: Socket | null, public graph: Graph) {
46 | super()
47 |
48 | this.element = new LinkElement(graph.canvas.element.linkArea, start.type)
49 | this.start.addLink(this)
50 | if (end) this.setEndDock(end)
51 | }
52 |
53 | public isCompatible(dock: Dock): boolean {
54 | return this.start.isCompatible(dock)
55 | }
56 |
57 | /**
58 | * Adds the link to the endDock's links, swapping docks if necessary
59 | */
60 | public pin(end: Socket): void {
61 | this.end = end
62 | this.end.popExisting().addLink(this)
63 | if (this.end.side == DockSide.RIGHT) this.swapDocks()
64 | // this.graph.eventHandler.handle(this)
65 | }
66 |
67 | /**
68 | * Remove the link from its endDock's links
69 | * @returns the link that is edited
70 | */
71 | public unpin(): Link {
72 | assert(this.end)
73 | this.end.dropLink(this)
74 | this.end = null
75 | return this
76 | }
77 |
78 | public update(position?: Pair): void {
79 | const pos = (position || this.end && this.end.element.position) as Pair
80 | const [a, b] = [this.start.element.position, pos]
81 | return this.element.update(this.start.side == DockSide.RIGHT ? [a, b] : [b, a])
82 | }
83 |
84 | /**
85 | * Unregisters the link, deletes the HTML object and unpins from start and end docks.
86 | */
87 | public destroy(): void {
88 | this.element.remove()
89 | this.start.dropLink(this)
90 | if (this.end) this.end.dropLink(this)
91 | this.graph.removeLink(this)
92 | }
93 |
94 | public trigger(payload?: TriggerArgs): void {
95 | ( this.end).trigger(payload)
96 | }
97 |
98 | /**
99 | * Check if end is compatible with link then save the link on the dock.
100 | */
101 | private setEndDock(end: Socket): void {
102 | if (!this.isCompatible(end)) this.destroy()
103 | this.pin(end)
104 | this.update()
105 | }
106 |
107 | private swapDocks(): void {
108 | [ this.start, this.end ] = [ this.end, this.start ]
109 | }
110 | }
--------------------------------------------------------------------------------
/src/Linkable.ts:
--------------------------------------------------------------------------------
1 | import Dock from '@/dock/Dock'
2 | import Graph from '@/Graph'
3 | import Link from '@/Link'
4 | import EventHandler from './controller/EventHandler'
5 | import { EventType } from './controller/state/interfaces'
6 | import Socket from './dock/Socket'
7 | import { GraphInputEvent, HandlerParams } from './GraphEventHandler'
8 |
9 | /**
10 | * Handles the dragging behavior of links in UI. When a user is interacting with a link a new
11 | * instance is created and takes care of initiating and closing mouse events.
12 | */
13 | export default class Linkable {
14 | /**
15 | * The dock the link is snapped to, i.e. not the dock the link originates from.
16 | */
17 | private snapped: Dock | null = null
18 | private link: Link
19 | private eventHandler: EventHandler
20 |
21 | /**
22 | * Creates a new event handler for the linking behavior and initiates the mouse event listeners.
23 | */
24 | constructor(start: Socket, public graph: Graph, { receiver, resolver }: HandlerParams) { //# rename eventHandler
25 | this.link = Link.get(start, graph)
26 |
27 | this.eventHandler = new EventHandler({
28 | mousemove: { callback: this.mouseMove },
29 | mouseup: { callback: this.mouseUp, once: true },
30 | }, receiver, resolver)
31 | }
32 |
33 | /**
34 | * {Event callback} Executed when mouse moves.
35 | */
36 | private mouseMove = (event: MouseEvent, { object }: GraphInputEvent) => {
37 | const dock = object
38 | this.insideSnapArea(event) ? this.mouseIn(event, dock) : this.mouseOut(event)
39 | }
40 |
41 | /**
42 | * {Event callback} Executed when mouse button gets released.
43 | */
44 | private mouseUp = (): void => {
45 | this.snapped ? this.mouseUpIn() : this.mouseUpOut()
46 | this.eventHandler.removeEventListener(EventType.MOUSEMOVE)
47 | }
48 |
49 | /**
50 | * {Event callback} Executed when mouse is moving inside a snap area.
51 | */
52 | private mouseIn(event: MouseEvent, dock?: Dock): void {
53 | if (!this.snapped && dock) this.mouseEnter(event, dock)
54 | }
55 |
56 | /**
57 | * {Event callback} Executed when mouse is moving outside a snap area.
58 | */
59 | private mouseOut(event: MouseEvent): void {
60 | if (this.snapped) this.mouseLeave(event)
61 | this.trackMouse(event)
62 | }
63 |
64 | /**
65 | * {Event callback} Executed when mouse is entering a snap area.
66 | */
67 | private mouseEnter(event: MouseEvent, dock: Dock): void {
68 | this.canSnap(dock) ? this.snap(dock) : this.trackMouse(event)
69 | }
70 |
71 | /**
72 | * {Event callback} Executed when mouse is leaving a snap area.
73 | */
74 | private mouseLeave(_: MouseEvent): void {
75 | this.snapped = null
76 | }
77 |
78 | /**
79 | * {Event callback} Executed when mouse is released and link is snapped.
80 | */
81 | private mouseUpIn(): void {
82 | this.link.pin( this.snapped)
83 | }
84 |
85 | /**
86 | * {Event callback} Executed when mouse up is released outside of a snap area.
87 | */
88 | private mouseUpOut(): void {
89 | this.link.destroy()
90 | }
91 |
92 | /**
93 | * Returns true if the mouse is located on a snap area.
94 | */
95 | private insideSnapArea({ target }: MouseEvent): boolean {
96 | return ( target).matches('.snap-dock')
97 | }
98 |
99 | /**
100 | * Updates the position of the link's end dock with the mouse's position.
101 | */
102 | private trackMouse(event: MouseEvent): void {
103 | this.link.update(this.graph.canvas.mousePosition(event))
104 | }
105 |
106 | /**
107 | * Returns whether the link can snap on the given dock or not.
108 | * @param dock against which we're testing the link's compatibility
109 | */
110 | private canSnap(dock: Dock): boolean {
111 | return this.link.isCompatible(dock)
112 | }
113 |
114 | /**
115 | * Snaps the link to the dock given as argument.
116 | */
117 | private snap(dock: Dock): void {
118 | this.snapped = dock
119 | this.link.update(dock.element.position)
120 | }
121 | }
--------------------------------------------------------------------------------
/src/Path.ts:
--------------------------------------------------------------------------------
1 | import { Pair } from '@/types'
2 |
3 | enum PathType { CURVE, STRAIGHT }
4 |
5 | /**
6 | * Utility class to generate SVG paths
7 | */
8 | export default class Path {
9 | /**
10 | * Default global path properties.
11 | */
12 | static parameters = {
13 | factor: 0.4,
14 | extend: 0.05,
15 | type: PathType.STRAIGHT
16 | }
17 |
18 | /**
19 | * Generates the svg path string expression representing a curve.
20 | * @param [x1, y1] start position
21 | * @param [x2, y2] end position
22 | */
23 | static calculate([x1, y1]: Pair, [x2, y2]: Pair): string {
24 | const diff = Math.abs(x2 - x1)
25 | const { extend, factor, type } = this.parameters
26 | const [L1, L2] = [x1 + extend*diff, x2 - extend*diff]
27 | const [C1, C2] = [x1 + factor*diff, x2 - factor*diff]
28 |
29 | return type == PathType.CURVE
30 | ? `M ${x1},${y1} L ${L1},${y1} C ${C1},${y1} ${C2},${y2} ${L2},${y2} L ${x2},${y2}`
31 | : `M ${x1},${y1} L ${L1},${y1} L ${L2},${y2} L ${x2},${y2}`
32 | }
33 | }
--------------------------------------------------------------------------------
/src/controller/EventHandler.ts:
--------------------------------------------------------------------------------
1 | import { assert } from '@/utils'
2 | import KeyEventHandler from './KeyEventHandler'
3 | import { MouseButton } from './MouseCode'
4 | import { MouseEventHandler } from './MouseEventHandler'
5 | import MouseWheelEventHandler from './MouseWheelEventHandler'
6 | import { Bindings as Bindings, SingleEvent, EventType } from './state/interfaces'
7 |
8 |
9 | export default class EventHandler {
10 | private keyEventHandler?: KeyEventHandler
11 | private mouseEventHandler?: MouseEventHandler
12 | private mouseWheelEventHandler?: MouseWheelEventHandler
13 | private mousemoveEventHandler?: SingleEvent
14 | private mouseupEventHandler?: SingleEvent
15 |
16 | public removeEventListener(type: EventType): void {
17 | this.element.removeEventListener(type, this[type])
18 | }
19 |
20 | constructor(binds: Bindings, private element: HTMLElement,
21 | private resolver: (target?: Element | null) => T) {
22 |
23 | if (binds.keybinds) {
24 | this.keyEventHandler = new KeyEventHandler(binds.keybinds)
25 | element.addEventListener(EventType.KEYUP, this.keyup)
26 | }
27 |
28 | if (binds.mousebinds) {
29 | this.mouseEventHandler = new MouseEventHandler(binds.mousebinds)
30 | element.addEventListener(EventType.MOUSEDOWN, this.mousedown)
31 |
32 | if (binds.mousebinds[MouseButton.MIDDLE]) {
33 | this.mouseWheelEventHandler = new MouseWheelEventHandler(binds.mousebinds)
34 | element.addEventListener(EventType.MOUSEWHEEL, this.wheel)
35 | }
36 | }
37 |
38 | if (binds.mousemove) {
39 | this.mousemoveEventHandler = binds.mousemove
40 | element.addEventListener(EventType.MOUSEMOVE, this.mousemove)
41 | }
42 |
43 | if (binds.mouseup) {
44 | this.mouseupEventHandler = binds.mouseup
45 | element.addEventListener(EventType.MOUSEUP, this.mouseup)
46 | }
47 | }
48 |
49 |
50 | // All non-null assertions have been replaced with explicit assert() calls. In any case if a
51 | // [EventType] method has been called, then the associated handler *has* been initialized in the
52 | // constructor and is therefore accessible. Might disable the no-non-null-assertion rule though.
53 |
54 | private [EventType.KEYUP] = (event: Event) => {
55 | assert(this.keyEventHandler)
56 | this.keyEventHandler.call( event)
57 | }
58 |
59 | private [EventType.MOUSEDOWN] = (event: Event) => {
60 | assert(this.mouseEventHandler)
61 | // Resolver should not be applied on event.target but on the match returned by the
62 | // eventHandler. This is because events can bubble up, which ultimately, will end
63 | // up getting caught on a different event target.
64 | const match = this.mouseEventHandler.call( event)
65 | if (match) match.callback(event, this.resolver(match.target))
66 | }
67 |
68 | private [EventType.MOUSEWHEEL] = (event: Event) => {
69 | assert(this.mouseWheelEventHandler)
70 | const match = this.mouseWheelEventHandler.call( event)
71 | const direction = this.mouseWheelEventHandler.direction( event)
72 | if (match) match.callback(event, direction, this.resolver( match.target))
73 | }
74 |
75 | private [EventType.MOUSEMOVE] = (event: Event) => {
76 | //# for unique events use event.target since no bubbling mechanism... target must be visible blah blah blah
77 | assert(this.mousemoveEventHandler)
78 | this.mousemoveEventHandler.callback(event, this.resolver( event.target))
79 | }
80 |
81 | private [EventType.MOUSEUP] = (event: Event) => {
82 | assert(this.mouseupEventHandler)
83 | const { callback, once } = this.mouseupEventHandler
84 | callback(event, this.resolver())
85 | if (once) this.element.removeEventListener('mouseup', this.mouseup)
86 | }
87 | }
--------------------------------------------------------------------------------
/src/controller/KeyCode.ts:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Helper class that associates key codes with their names
4 | */
5 | export class KeyCode {
6 | /**
7 | * Gives the key combination name corresponding to provided Event.
8 | */
9 | static get({ code, ctrlKey, shiftKey, altKey }: KeyboardEvent) {
10 | return `${ctrlKey ? 'Ctrl_' : ''}${shiftKey ? 'Shift_' : ''}${altKey ? 'Alt_' : ''}${code}`
11 | }
12 | }
--------------------------------------------------------------------------------
/src/controller/KeyEventHandler.ts:
--------------------------------------------------------------------------------
1 | import { KeyCode } from './KeyCode'
2 | import { Keybinds } from './state/interfaces'
3 |
4 | export default class KeyEventHandler {
5 | constructor(private binds: Keybinds) {}
6 |
7 | public call(event: KeyboardEvent) {
8 | const keyName = KeyCode.get(event)
9 | if (!keyName) return
10 |
11 | const eventCallback = this.binds[keyName]
12 | if (eventCallback) eventCallback(event) //? formerly there was a binds on this.state.data
13 | }
14 | }
--------------------------------------------------------------------------------
/src/controller/MouseCode.ts:
--------------------------------------------------------------------------------
1 |
2 | export enum MouseButton { LEFT, MIDDLE, RIGHT }
3 |
4 | /**
5 | * Helper class that associates mouse button codes codes with their names
6 | */
7 | export class MouseCode {
8 | /**
9 | * Gives the button name associated with the mouse code
10 | */
11 | static get(event: MouseEvent): MouseButton {
12 | return event.button
13 | }
14 | }
--------------------------------------------------------------------------------
/src/controller/MouseEventHandler.ts:
--------------------------------------------------------------------------------
1 | import { MouseCode } from './MouseCode'
2 | import { Mousebinds } from './state/interfaces'
3 |
4 | /**
5 | * Contains information regarding the event: the `event` object itself, the selector that caught
6 | * the event and the target element as well as the actual function that needs to be called: `callback`.
7 | */
8 | export interface EventPayload { //# replace Function
9 | callback: Function,
10 | event: Event,
11 | selector: string
12 | distance: number,
13 | target: Element | null,
14 | }
15 |
16 | /**
17 | * Any events of type `MouseEvent`, triggered on the document, are handled here.
18 | */
19 | export class MouseEventHandler {
20 | constructor(protected binds: Mousebinds) {}
21 |
22 | // public call(event: MouseEvent) {
23 | // // $.Event.log(`┌── new state = ${State.current.id.toString()}`)
24 | // // $.Event.pipe()
25 | // // $.Event.log('(1) bubble path:', [event.path])
26 |
27 | // return this.checkSelectors(event)
28 |
29 | // // $.Event.unindent()
30 | // // $.Event.log(`└──/ ended`)
31 | // }
32 |
33 | /**
34 | * Depending on the key that was pressed, check for "on-element" and "off-element" triggers.
35 | */
36 | public call(event: MouseEvent): EventPayload | undefined {
37 | const code = MouseCode.get(event)
38 |
39 | const { off, on } = { ...this.binds.all, ...this.binds[code] }
40 |
41 | // $.Event.log(`(2) candidates for "${buttonName} mouse button":`)
42 |
43 | // $.Event.indent()
44 | // $.Event.log('└──> (on-candidates)', Object.keys(on || {}))
45 | const matchedOn = this.checkSelectorsOn(event, Object.entries(on || {}))
46 | if (matchedOn) return matchedOn
47 |
48 | // $.Event.log('└──> (not on el.)', Object.keys(off || {}))
49 | const matchedOff = this.checkSelectorsOff(event, Object.entries(off || {}))
50 | if (matchedOff) return matchedOff
51 | // $.Event.unindent()
52 | }
53 |
54 | /**
55 | * Finds the event handler that is the closest one to the element that fired the event, then calls its callback.
56 | * @param event
57 | * @param candidates contains the selectors as keys and callbacks as values
58 | */
59 | protected checkSelectorsOn(event: Event, candidates: [string, Function][]): EventPayload | undefined {
60 | const matches = candidates.map(([selector, callback]) => {
61 | const targets = event.composedPath() as Element[]
62 | const i = targets.findIndex(t => t.matches && t.matches(selector))
63 |
64 | if (~i) return { distance: i, target: targets[i], callback, event, selector }
65 |
66 | }).filter(Boolean) as EventPayload[]
67 |
68 | // $.Event.indent()
69 | // $.Event.log(`(1) cross match`, matches.map(({target, distance}) => `.${target.classList}#${target.id} (d=${distance})`))
70 |
71 | if (matches.length) {
72 | const match = matches.reduce((min, curr) => curr.distance < min.distance ? curr : min)
73 | // $.Event.log(`(2) closest match`, match.target)
74 | // $.Event.log(`(3) executing callback`)
75 | // $.Event.unindent()
76 | return match
77 | }
78 |
79 | // $.Event.log('(2) no match, exiting.')
80 | // $.Event.unindent()
81 | }
82 |
83 | /**
84 | * Finds the event handlers that are not in the event element tree then calls their callbacks
85 | * @param selectors contains the selectors as keys and callbacks as values
86 | */
87 | private checkSelectorsOff(event: Event, selectors: [string, Function][]): EventPayload | undefined {
88 | // $.Event.indent()
89 | // $.Event.log(`(1) checking for selectors with zero match to elements of event path`)
90 |
91 | const match = selectors.find(([selector]) => {
92 | const targets = event.composedPath() as Element[]
93 | const noMatch = targets.every(el => !(el.matches && el.matches(selector)))
94 |
95 | if (noMatch) return true
96 | // $.Event.indent()
97 | // $.Event.log('- not a single match for', selector, 'executing callback')
98 | // $.Event.unindent()
99 |
100 | // this.trigger({ callback, event, selector, distance: 0 })
101 | })
102 |
103 | if (!match) return match
104 |
105 | const [selector, callback] = match
106 | return { event, selector, callback, target: null, distance: 0 }
107 |
108 | // $.Event.unindent()
109 | }
110 | }
--------------------------------------------------------------------------------
/src/controller/MouseWheelEventHandler.ts:
--------------------------------------------------------------------------------
1 | import { MouseButton } from './MouseCode'
2 | import { MouseEventHandler, EventPayload } from './MouseEventHandler'
3 |
4 | interface WheelEventPayload {
5 | match?: EventPayload,
6 | direction: number
7 | }
8 |
9 | export default class MouseWheelEventHandler extends MouseEventHandler {
10 | public call(event: MouseEvent): EventPayload | undefined {
11 | const on = this.binds[MouseButton.MIDDLE]
12 | const matchedOn = this.checkSelectorsOn(event, Object.entries(on || {}))
13 | if (matchedOn) return matchedOn
14 | }
15 |
16 | public direction(event: Event): number {
17 | return Math.sign(( event).deltaY)
18 | }
19 | }
--------------------------------------------------------------------------------
/src/controller/state/EditorState.ts:
--------------------------------------------------------------------------------
1 |
2 | import Socket from '@/dock/Socket'
3 | import { Draggable, DragType } from '@/Draggable'
4 | import { GraphInputEvent } from '@/GraphEventHandler'
5 | import Linkable from '@/Linkable'
6 | import Node from '@/node/Node'
7 | import { MouseButton } from '../MouseCode'
8 | import { Bindings as Bindings } from './interfaces'
9 |
10 |
11 | //# ultimately I don't want to see a single css selector here, yikers. enum mapping would be a quick fix...
12 | //# { on: { [GraphObjectType.NODE]: (event: MouseEvent, { object, graph }: {Node, graph})?
13 | export const EditorDefaultState: Bindings = {
14 | keybinds: {
15 | KeyQ: (): void => { console.log('yeah') },
16 | Shift_KeyQ: (): void => { console.log('no!!') },
17 | // spacebar: () => nodeFinder.show(),
18 |
19 | Ctrl_Shift_Space: (): void => console.log('nope'),
20 | },
21 |
22 | mousebinds: {
23 | [MouseButton.MIDDLE]: {
24 | '.objects': (_: MouseEvent, direction: number, { graph }: GraphInputEvent): void => {
25 | graph.canvas.zoom.update(direction)
26 | }
27 | },
28 |
29 | [MouseButton.RIGHT]: {
30 | on: {
31 | '.objects': (event: MouseEvent, { graph }: GraphInputEvent): void => {
32 | new Draggable({
33 | position: event,
34 | type: DragType.DRAG,
35 | element: graph.canvas.element.positionWrapper,
36 | object: graph.canvas,
37 | zoom: graph.canvas.zoom,
38 | })
39 | },
40 | // --debug
41 | // '.snap-dock': ({ target },
42 | // graph = Graph.get(target),
43 | // dock = graph.store.get(target)) => {
44 | // window.$DOCK = dock
45 | // },
46 | },
47 | },
48 |
49 | [MouseButton.LEFT]: {
50 | on: {
51 | '.header': (event: MouseEvent, { graph, object }: GraphInputEvent): void => {
52 | const node = object
53 | new Draggable({
54 | position: event,
55 | type: DragType.DRAG,
56 | element: node.element.container,
57 | object: node.element,
58 | zoom: graph.canvas.zoom,
59 | callback: node.update.bind(object),
60 | })
61 | },
62 |
63 | '.node-container': (event: MouseEvent, { object }: GraphInputEvent): void => {
64 | // ( object).select()
65 | },
66 |
67 | '.snap-dock': (_: MouseEvent, { graph, object, eventHandler }: GraphInputEvent): void => {
68 | new Linkable( object, graph, eventHandler)
69 | },
70 |
71 | // // --debug = links need an exact mouse click on the element, we will need a ghost element
72 | // 'path': ({ target }) => {
73 | // print(target.id)
74 | // }
75 | },
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/src/controller/state/FinderState.ts:
--------------------------------------------------------------------------------
1 | // import Finder from '@/Finder'
2 | // import { StateManager } from './StateManager'
3 |
4 | // Finder.state = Symbol('finder')
5 |
6 | // export default new StateManager({
7 | // id: Finder.state,
8 |
9 | // keybinds: {
10 | // // 'escape': () => nodeFinder.hide(),
11 | // // 'alphabet': event => nodeFinder.search(event.target.value),
12 | // },
13 |
14 | // mousebinds: {
15 | // all: {
16 | // off: {
17 | // // '.search-container': () => nodeFinder.hide()
18 | // }
19 | // }
20 | // }
21 | // })
22 |
--------------------------------------------------------------------------------
/src/controller/state/StateManager.ts:
--------------------------------------------------------------------------------
1 | // import $ from '@/ConsoleWriter'
2 | import { assert } from '@/utils'
3 | import { MouseButton } from '../MouseCode'
4 | import { Bindings } from './interfaces'
5 |
6 |
7 | /**
8 | * Data structure for managing event handler states. Each state bundles a set of keybinds and mousebinds.
9 | */
10 | // export class StateManager {
11 | // /**
12 | // * Contains all the states that have been instanciated
13 | // */
14 | // static all = new Map()
15 | // static current: State
16 |
17 | // /**
18 | // * Adds a new state that can later be used with `State.change`
19 | // */
20 | // constructor(id, { keybinds, mousebinds }: State) {
21 | // StateManager.all.set(id, Object.assign(this, { keybinds, mousebinds }))
22 | // }
23 |
24 | // /**
25 | // * Changes the state to the one associated with the provided symbol
26 | // * @param symbol the identifier of the state
27 | // * @param data the object that can be passed to the new state
28 | // */
29 | // static change(symbol: symbol, data: StateData = {}) {
30 | // assert(StateManager.all.has(symbol), `The provided id (${String(symbol)}) doesn't exist`)
31 |
32 | // // $.State.log(`state changed to ${State.all.get(symbol).id.toString()}`)
33 | // StateManager.current = { ...(StateManager.all.get(symbol) as State), data }
34 | // }
35 |
36 | // }
--------------------------------------------------------------------------------
/src/controller/state/interfaces.ts:
--------------------------------------------------------------------------------
1 | import { MouseButton } from '../MouseCode'
2 |
3 |
4 | export type MouseClickEventCallbacks = { [k: string]: (event: MouseEvent, payload: T) => void }
5 | export type MouseWheelEventCallback = { [k: string]: (event: WheelEvent, direction: number, payload: T) => void }
6 | export type KeyEventCallbacks = { [k: string]: (event: KeyboardEvent) => void }
7 |
8 | export interface SingleEvent {
9 | callback: Function,
10 | once?: boolean,
11 | }
12 |
13 | export interface Mousebinds {
14 | all?: { on?: MouseClickEventCallbacks, off?: MouseClickEventCallbacks },
15 | [MouseButton.MIDDLE]?: MouseWheelEventCallback, // might separate from Mousebinds
16 | [MouseButton.RIGHT]?: { on?: MouseClickEventCallbacks, off?: MouseClickEventCallbacks },
17 | [MouseButton.LEFT]?: { on?: MouseClickEventCallbacks, off?: MouseClickEventCallbacks },
18 | }
19 |
20 | export type Keybinds = KeyEventCallbacks
21 |
22 | export interface Bindings {
23 | keybinds?: Keybinds,
24 | mousebinds?: Mousebinds,
25 | mousemove?: SingleEvent,
26 | mouseup?: SingleEvent,
27 | }
28 |
29 | export enum EventType {
30 | MOUSEMOVE = 'mousemove',
31 | MOUSEDOWN = 'mousedown',
32 | MOUSEWHEEL = 'wheel',
33 | MOUSEUP = 'mouseup',
34 | KEYUP = 'keyup',
35 | }
--------------------------------------------------------------------------------
/src/dock/Dock.ts:
--------------------------------------------------------------------------------
1 | import { GraphObject, GraphObjectItem } from '@/GraphObject'
2 | import Router from '@/interpreter/router/Router'
3 | import DockElement from '@/view/DockElement'
4 | import { DockParams, DockSide, FlowType } from './interfaces'
5 |
6 | /**
7 | * This class is part of the connection-layer for docks
8 | */
9 | export default abstract class Dock extends GraphObject {
10 | // public abstract getDependencies(): Object //?
11 | // public abstract getValue(): Object //?
12 |
13 | public node: string
14 | public element: DockElement
15 | public router: Router
16 | public label: string
17 |
18 | // isFull() {
19 | // return !(this instanceof OutDataDock) && this.links.size > 0
20 | // }
21 |
22 | public abstract update(): void
23 |
24 | public get binds(): Array {
25 | return [[ this.element.snap, this]] //# make snap HTMLElement rather?
26 | }
27 |
28 | constructor(public type: FlowType, public side: DockSide, params: DockParams) {
29 | super()
30 | this.label = params.label
31 | this.element = new DockElement(type, side, params.label, params.location, params.type)
32 | }
33 |
34 | isCompatible(other: Dock): boolean {
35 | return this.router !== other.router && this.side != other.side && this.type == other.type
36 | }
37 | }
--------------------------------------------------------------------------------
/src/dock/EmptyDock.ts:
--------------------------------------------------------------------------------
1 | import { Deps } from '@/interpreter/interfaces'
2 | import { assert } from '@/utils'
3 | import Dock from './Dock'
4 |
5 | /**
6 | * Singleton class for InDataDock which do not have an ancestor.
7 | */
8 | export default class EmptyDock extends Dock {
9 | private static instance: EmptyDock
10 |
11 | private dependencies = { parents: new Set(), getters: new Set() } as Deps
12 |
13 | static get singleton() {
14 | return this.instance
15 | }
16 |
17 | getDependencies(): Deps {
18 | return this.dependencies
19 | }
20 |
21 | getValue(): never {
22 | assert(false)
23 | }
24 | popExisting(): never {
25 | assert(false)
26 | }
27 |
28 | addLink() {}
29 | dropLink() {}
30 | destroy() {}
31 | trigger() {}
32 | update() {}
33 | }
--------------------------------------------------------------------------------
/src/dock/InDataDock.ts:
--------------------------------------------------------------------------------
1 | import { Data, Deps, TriggerArgs } from '@/interpreter/interfaces'
2 | import Process from '@/interpreter/process/Process'
3 | // import EmptyDock from './EmptyDock'
4 | import { DockParams, DockSide, FlowType } from './interfaces'
5 | import OutDataDock from './OutDataDock'
6 | import UniqueSocket from './UniqueSocket'
7 |
8 | export default class InDataDock extends UniqueSocket {
9 | public get ancestor(): OutDataDock {
10 | return {}
11 | // return this.link ? this.link.origin : EmptyDock.singleton
12 | }
13 |
14 | constructor(params: DockParams) {
15 | super(FlowType.DATA, DockSide.LEFT, params)
16 | }
17 |
18 |
19 | getDependencies(): Deps {
20 | return this.ancestor.getDependencies()
21 | }
22 |
23 | getValue(): [number, string] | null { //?
24 | return this.ancestor.getValue()
25 | }
26 |
27 | trigger(payload: TriggerArgs | void) { //? type of payload (on all #trigger)
28 | // $.Execution.log(`├──> data propagation to ${this.node}`)
29 | // $.Execution.pipe()
30 | this.router.trigger({ accessor: this, ...payload }) // do you want "accessor" to potentially be overwriten by "payload"'s content.accesor?
31 | // $.Execution.unindent()
32 | // $.Execution.log('└──/ data propagation ended')
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/dock/InExeDock.ts:
--------------------------------------------------------------------------------
1 | import { TriggerArgs } from '@/interpreter/interfaces'
2 | // import Router from '@/interpreter/router/Router'
3 | import EmptyDock from './EmptyDock'
4 | import { DockParams, DockSide, FlowType } from './interfaces'
5 | import OutExeDock from './OutExeDock'
6 | import UniqueSocket from './UniqueSocket'
7 |
8 | export default class InExeDock extends UniqueSocket {
9 | // private router: Router
10 |
11 | get ancestor(): OutExeDock {
12 | return {}
13 | // return (this.link ? this.link.origin : EmptyDock) as OutExeDock //? Link would make is easier
14 | }
15 |
16 | constructor(params: DockParams) {
17 | super(FlowType.EXE, DockSide.LEFT, params)
18 | }
19 |
20 | trigger(payload: TriggerArgs | void) {
21 | // this.node.router.trigger(payload) //? what about this.router.?
22 |
23 | // $.Execution.log(`├──> exe propagation to ${this.node}`)
24 |
25 | // $.Execution.pipe()
26 | // this.node.router.trigger({accessor: this, ...payload })
27 | // $.Execution.unindent()
28 | // $.Execution.log('└──/ data propagation ended')
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/dock/MultipleSocket.ts:
--------------------------------------------------------------------------------
1 | import Link from '@/Link'
2 | import Socket from './Socket'
3 |
4 | export default abstract class MultipleSocket extends Socket {
5 | get occupied(): boolean {
6 | return false
7 | }
8 |
9 | public popExisting(): Socket {
10 | return this
11 | }
12 | }
--------------------------------------------------------------------------------
/src/dock/OutDataDock.ts:
--------------------------------------------------------------------------------
1 | import { Deps, TriggerArgs, Data } from '@/interpreter/interfaces'
2 | import Process from '@/interpreter/process/Process'
3 | // import DockElement from '@/view/DockElement'
4 | import { DockParams, DockSide, FlowType } from './interfaces'
5 | import MultipleSocket from './MultipleSocket'
6 |
7 | export default class OutDataDock extends MultipleSocket {
8 | private process: Process
9 |
10 | /**
11 | * OutDataDock#value is null if the value is not available on that dock
12 | */
13 | public value = {} as Data | null
14 |
15 | constructor(params: DockParams) {
16 | super(FlowType.DATA, DockSide.RIGHT, params)
17 | }
18 |
19 | getDependencies(): Deps { //? is this a good idea?
20 | this.process.dependencies.parents.add(this.router)
21 | return this.process.dependencies
22 | }
23 |
24 | getValue(): [number, string] | null {
25 | return this.value ? [ this.value.computed, this.value.string ] as [number, string] : null
26 | }
27 |
28 | setValue(value?: Data) {
29 | this.value = value ? value : null
30 | }
31 |
32 | propagate(payload: TriggerArgs) {
33 | this.links.forEach(link => link.trigger(payload))
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/dock/OutExeDock.ts:
--------------------------------------------------------------------------------
1 | import { DockParams, DockSide, FlowType } from './interfaces'
2 | import UniqueSocket from './UniqueSocket'
3 |
4 | export default class OutExeDock extends UniqueSocket {
5 | // private router: Router
6 |
7 | constructor(params: DockParams) {
8 | super(FlowType.EXE, DockSide.RIGHT, params)
9 | }
10 |
11 | propagate(updateET: boolean) {
12 | // this.links.forEach(({ endDock }) => endDock.trigger(updateET)) // this is obvious
13 | // if (this.link) this.link.end.trigger(updateET)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/dock/Socket.ts:
--------------------------------------------------------------------------------
1 | import Link from '@/Link'
2 | import Dock from './Dock'
3 |
4 | export default abstract class Socket extends Dock {
5 | public abstract popExisting(): Socket
6 | public abstract get occupied(): boolean
7 | protected links = new Set()
8 |
9 | public addLink(link: Link): void {
10 | this.links.add(link)
11 | }
12 |
13 | public dropLink(link: Link): void {
14 | this.links.delete(link)
15 | }
16 |
17 | public update(): void {
18 | this.links.forEach(link => link.update())
19 | }
20 |
21 | public destroy(): void {
22 | this.links.forEach(link => link.destroy())
23 | }
24 | }
--------------------------------------------------------------------------------
/src/dock/UniqueSocket.ts:
--------------------------------------------------------------------------------
1 | import Link from '@/Link'
2 | import { assert } from '@/utils'
3 | import Socket from './Socket'
4 |
5 | export default abstract class UniqueSocket extends Socket {
6 | private get link(): Link {
7 | return this.links.values().next().value
8 | }
9 |
10 | public get occupied(): boolean {
11 | return this.links.size > 0
12 | }
13 |
14 | public popExisting(): UniqueSocket {
15 | if (this.occupied) this.destroy()
16 | return this
17 | }
18 |
19 | public editLink(): Link {
20 | assert(this.occupied, `link does not exist on socket ${this}`)
21 | return this.link.unpin()
22 | }
23 | }
--------------------------------------------------------------------------------
/src/dock/interfaces.ts:
--------------------------------------------------------------------------------
1 | import Dock from './Dock'
2 | import InDataDock from './InDataDock'
3 | import InExeDock from './InExeDock'
4 | import OutDataDock from './OutDataDock'
5 | import OutExeDock from './OutExeDock'
6 |
7 | export type Docks = Array
8 |
9 | export enum FlowType { DATA = 'data', EXE = 'exe' }
10 |
11 | export enum DataType { INT, STR, LIST, DICT, TENSOR, MODULE }
12 |
13 | export enum DockSide { LEFT = 'left', RIGHT = 'right' }
14 |
15 | export interface DockDef {
16 | label: string
17 | optional?: boolean
18 | type?: InputType
19 |
20 | }
21 |
22 | export interface DockParams {
23 | label: string
24 | location: string
25 | type?: InputType
26 | }
27 |
28 | // export type DataDock = InDataDock | OutDataDock
29 | // export type ExeDock = InExeDock | OutExeDock
30 |
31 | export type InDock = InDataDock | InExeDock
32 | export type OutDock = OutDataDock | OutExeDock
33 |
34 | export function create(cstr: DockCstr, defs: DockParams[]): Array {
35 | return defs.map(d => new cstr(d))
36 | }
37 |
38 | export type DockCstr = new (d: DockParams) => D
39 |
40 | export enum InputType {
41 | INT = 'int',
42 | STR = 'str',
43 | LIST = 'list',
44 | DICT = 'dict',
45 | TENSOR = 'tensor',
46 | MODULE = 'module',
47 | }
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import Graph from '@/Graph'
2 | import { assert } from '@/utils'
3 | import Component from './Component'
4 | import { InputType } from './dock/interfaces'
5 | import { NodeInstance } from './node/interfaces'
6 | import OperatorNode from './node/OperatorNode'
7 | import SetterNode from './node/SetterNode'
8 |
9 | document.addEventListener('keydown', event => {
10 | if (event.code == 'Tab') event.preventDefault()
11 | })
12 |
13 |
14 | const element = document.querySelector('.window') as HTMLElement
15 | assert(element)
16 |
17 | const c = new Component(element, 'myComponent', [
18 | // { label: 'name' },
19 | { label: 'in_channels' },
20 | { label: 'out_channels' },
21 | { label: 'kernel_size' },
22 | { label: 'stride', optional: true },
23 | { label: 'dilation', optional: true },
24 | { label: 'group', optional: true },
25 | { label: 'bias', optional: true },
26 | { label: 'padding_mode', optional: true },
27 | ])
28 |
29 | // const graph = new Graph(element)
30 |
31 |
32 | // in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros'
33 |
34 | const c1 = {
35 | cstr: SetterNode,
36 | label: 'Conv2d',
37 | header: 'pink',
38 | background: 'conv',
39 | position: [470,100],
40 | process: {
41 | // requires: ['import torch.nn as nn']
42 | compute: function() { return null },
43 | string: function(name: string, in_channels: string, out_channels: string, kernel_size: string,
44 | stride: number, padding: number, dilation: number, groups: number, bias: boolean, padding_mode: string) { // convert optional parameters to string stride=${stride} if set
45 | return `${name} = nn.Conv2d(${in_channels}, ${out_channels}, ${kernel_size})`
46 | },
47 | params: [
48 | { label: 'name', type: InputType.STR },
49 | { label: 'in_channels' },
50 | { label: 'out_channels' },
51 | { label: 'kernel_size' },
52 | { label: 'stride', optional: true },
53 | { label: 'dilation', optional: true },
54 | { label: 'group', optional: true },
55 | { label: 'bias', optional: true },
56 | { label: 'padding_mode', optional: true },
57 | ],
58 | // result: {}
59 | }
60 | }
61 |
62 | const c2 = {
63 | cstr: OperatorNode,
64 | label: 'conv s=0',
65 | header: 'pink',
66 | background: '*',
67 | position: [150,220],
68 | process: {
69 | compute: function() { return null },
70 | string: function() {
71 | return 'yeeet'
72 | },
73 | params: [],
74 | result: [{ label: 'name' }],
75 | }
76 | }
77 |
78 | // graph.add(c1)
79 | // graph.add(c2)
80 |
81 | // params : [{
82 | // label:
83 | // type: { int, str, list, dict, Tensor, Module }
84 | // optional:
85 | // }]
86 |
--------------------------------------------------------------------------------
/src/interpreter/ExecutionTree.ts:
--------------------------------------------------------------------------------
1 | import InDataDock from '@/dock/InDataDock'
2 | import { assert } from '@/utils'
3 | import Router from './router/Router'
4 |
5 | export default class ExecutionTree {
6 | public root: Router
7 | public current: Router
8 | public scope = new Map()
9 | public accessBuffer = new Map>()
10 |
11 | //? please unpack this {}
12 | public active = { origin: null, accessors: new Set() } as { origin: Router | null, accessors: Set }
13 |
14 | static get(root: Router): ExecutionTree {
15 | return root.executionTree || new ExecutionTree(root)
16 | }
17 |
18 | constructor(root: Router) {
19 | this.root = root
20 | }
21 |
22 | update(accessor: InDataDock, origin: Router): void {
23 | // $.Execution.log(`└──> [ET] ${this.constructor.name}#update()`)
24 | // $.Execution.indent()
25 | // $.Execution.log(`├── (1) validating access`)
26 |
27 | const dependencies = accessor.getDependencies()
28 | this.fillAccessBuffer(dependencies.parents, accessor)
29 |
30 | const validated = accessor.router == origin || this.validateAccess(accessor, origin)
31 |
32 | if (!validated) {
33 | // $.Execution.log('└──/ access not validated, exiting.')
34 | // $.Execution.unindent()
35 | return //? nothing?
36 | }
37 |
38 | // $.Execution.log('├── (2) updated the access buffer', this.accessBuffer)
39 | // $.Execution.log(`├── (3) executing tree nodes`)
40 | // $.Execution.pipe()
41 | // $.Execution.indent()
42 | this.current = this.root
43 |
44 | this.execute(origin)
45 |
46 |
47 | // $.Execution.unindent()
48 | // $.Execution.unindent()
49 | // $.Execution.log('└──/ tree execution ended')
50 | // $.Execution.unindent()
51 |
52 | // while(this.next()) {
53 | // this.current = this.current.execute()
54 | // break // safety first
55 | // }
56 | }
57 |
58 | fillAccessBuffer(dependencies: Set, accessor: InDataDock) {
59 | dependencies.forEach(dep => {
60 | this.accessBuffer.set(dep, (this.accessBuffer.get(dep) || new Set()).add(accessor))
61 | })
62 | }
63 |
64 | validateAccess(accessor: InDataDock, origin: Router): boolean {
65 | // print('this.accessBuffer', this.accessBuffer, 'origin', origin)
66 | // print('1st cond.:', !this.active.origin)
67 | if (!this.active.origin) {
68 | this.active.origin = origin
69 | assert(this.accessBuffer.has(origin))
70 | this.active.accessors = >this.accessBuffer.get(origin)
71 | }
72 |
73 | // print('2nd cond.:', this.active.origin == origin)
74 | if (this.active.origin == origin) {
75 | this.active.accessors.delete(accessor)
76 | this.active.origin = this.active.accessors.size ? this.active.origin : null
77 | return !this.active.origin
78 | }
79 |
80 | return false
81 | }
82 |
83 | merge(other: ExecutionTree): void {
84 | if (other == null) return void 0
85 | this.accessBuffer = [...this.accessBuffer, ...other.accessBuffer].reduce((buffer, [ origin, accesses ]) => {
86 | return buffer.set(origin, new Set([...buffer.get(origin) || [], ...accesses]))
87 | }, new Map())
88 | }
89 |
90 | execute(origin: Router): void {
91 | // this.current.execute({ origin, updateET: false })
92 | }
93 |
94 | next() {
95 | return this.current
96 | }
97 |
98 | toString(): string {
99 | return `ExecutionTree (scope=Map[${[...this.scope.keys()]}])`
100 | }
101 | }
--------------------------------------------------------------------------------
/src/interpreter/GraphEventHandler.ts:
--------------------------------------------------------------------------------
1 | import { FlowType } from '@/dock/interfaces'
2 | import Link from '@/Link'
3 | import { assert } from '@/utils'
4 |
5 | export default class GraphEventHandler { //# rename
6 | handle(object: unknown): void {
7 | if (object instanceof Link) return this.handleLink(object)
8 | assert(false)
9 | }
10 |
11 | handleLink(link: Link): void {
12 | switch (link.type) {
13 | case FlowType.DATA:
14 | // $.GraphEvent.log(`[data-L] triggers update on "${link.end.node}"`)
15 | link.trigger()
16 | break
17 | case FlowType.EXE:
18 | // $.GraphEvent.log(`[exe-L] triggers execution tree merge`)
19 | // ExecutionTree.get(link.start.router).merge(link.end.router.executionTree)
20 | break
21 | default: break
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/src/interpreter/Scope.ts:
--------------------------------------------------------------------------------
1 |
2 | export default class Scope {
3 | public map = new Map()
4 |
5 | add(key: string, value: string) { //? generic type for value?
6 | this.map.set(key, value)
7 | }
8 | }
--------------------------------------------------------------------------------
/src/interpreter/interfaces.ts:
--------------------------------------------------------------------------------
1 | import InDataDock from '@/dock/InDataDock'
2 | import { DockDef } from '@/dock/interfaces'
3 | import Router from './router/Router'
4 |
5 | export interface TriggerArgs {
6 | accessor: InDataDock,
7 | origin?: Router,
8 | updateET?: boolean
9 | }
10 |
11 | export interface Deps {
12 | parents: Set,
13 | getters: Set //? not sure about that
14 | }
15 |
16 | export interface Data {
17 | computed?: T,
18 | string?: string
19 | }
20 |
21 | export interface ProcessParams {
22 | compute(...args: unknown[]): void,
23 | string(...args: unknown[]): string,
24 | params?: DockDef[],
25 | result?: DockDef[]
26 | }
27 |
28 | export interface FunctionParams extends ProcessParams {
29 | params: DockDef[],
30 | result: DockDef[]
31 | }
32 |
33 | export interface GetterParams extends ProcessParams {
34 | result: DockDef[]
35 | }
36 |
37 | export interface SetterParams extends ProcessParams {
38 | params: DockDef[],
39 | }
--------------------------------------------------------------------------------
/src/interpreter/process/ControlFlowProcess.ts:
--------------------------------------------------------------------------------
1 | import CustomProcess from './CustomProcess'
2 |
3 | class Conditional extends CustomProcess {
4 | constructor() {
5 | super(null, null, [{ label: 'cond1' }, { label: 'cond2' }], [])
6 | }
7 |
8 | // string(condition: boolean) { return condition }
9 | // compute(condition: boolean) { return Boolean(condition) }
10 | }
11 |
12 | class ForLoop extends CustomProcess {
13 | constructor() {
14 | super(null, null, [{ label: 'first'}, { label: 'last'}], [{ label: 'index'}, { label: 'array'}])
15 | }
16 |
17 | // string(a: number, b: number) { return a + b }
18 | // compute(a: number, b: number) { return a + b }
19 | }
20 |
21 | export default { Conditional, ForLoop }
--------------------------------------------------------------------------------
/src/interpreter/process/CustomProcess.ts:
--------------------------------------------------------------------------------
1 | import Dock from '@/dock/Dock'
2 | import { DockDef, DockCstr, create } from '@/dock/interfaces'
3 | import InDataDock from '@/dock/InDataDock'
4 | import OutDataDock from '@/dock/OutDataDock'
5 | import Process from './Process'
6 |
7 | type ComputeFn = ((...args: unknown[]) => void) | null
8 | type StringFn = ((...args: unknown[]) => string) | null
9 |
10 | export default class CustomProcess extends Process {
11 | constructor(compute: ComputeFn, string: StringFn, inputs: DockDef[], outputs: DockDef[]) {
12 | super()
13 |
14 | this.inputs = this.createDocks(InDataDock, inputs)
15 | this.outputs = this.createDocks(OutDataDock, outputs)
16 | if (this.constructor === CustomProcess) Object.assign(this, { compute, string })
17 | }
18 |
19 | private createDocks(cstr: DockCstr, params: DockDef[]): D[] {
20 | const defs = params.map(d => Object.assign(d, { location: 'body' }))
21 | return create(cstr, defs)
22 | }
23 | }
--------------------------------------------------------------------------------
/src/interpreter/process/GetterProcess.ts:
--------------------------------------------------------------------------------
1 | import CustomProcess from './CustomProcess'
2 |
3 | export default class GetterProcess extends CustomProcess {
4 | constructor(private variable: string) {
5 | super(null, null, [], [{ label: variable }])
6 | }
7 |
8 | compute() {
9 | return true // this.process.scope.get(this.getter)
10 | }
11 |
12 | string() {
13 | return this.variable
14 | }
15 | }
--------------------------------------------------------------------------------
/src/interpreter/process/Process.ts:
--------------------------------------------------------------------------------
1 | import InDataDock from '@/dock/InDataDock'
2 | import OutDataDock from '@/dock/OutDataDock'
3 | import { zip } from '@/utils'
4 | import { Deps } from '../interfaces'
5 | import Router from '../router/Router'
6 |
7 | export default class Process { //? abstract?
8 | public inputs: Array
9 | public outputs: Array
10 | public dependencies: Deps = { parents: new Set(), getters: new Set() }
11 | public arguments = [] as string[]
12 | public result = null
13 |
14 | get docks() {
15 | return new Set([...this.inputs, ...this.outputs])
16 | }
17 |
18 | execute(origin: Router, allowPropagation = false) {
19 | // $.Execution.log(`└──> [P-${this.constructor.name}] #execute`)
20 | // $.Execution.indent()
21 |
22 | // this.mergeDependencies()
23 |
24 | this.mergeArguments()
25 | // $.Execution.log(`├── (1) merged arguments`, this.arguments)
26 |
27 | this.updateResult()
28 | // $.Execution.log(`├── (2) updating the result`, this.result)
29 |
30 | this.routeData()
31 | // $.Execution.log(`├── (3) routing data inside node`)
32 |
33 | // $.Execution.log(`└──> (4) propagating data`)
34 | // $.Execution.indent()
35 | // this.propagate(origin, allowPropagation)
36 | // $.Execution.unindent()
37 |
38 | // $.Execution.unindent()
39 | }
40 |
41 | mergeDependencies(self?: Router) {
42 | this.dependencies = { parents: new Set(), getters: new Set() }
43 |
44 | this.inputs.forEach(input => {
45 | const { parents, getters } = input.getDependencies()
46 |
47 | parents.forEach(p => this.dependencies.parents.add(p))
48 | getters.forEach(g => this.dependencies.getters.add(g))
49 | })
50 |
51 | if (self) this.dependencies.parents.add(self)
52 | }
53 |
54 | mergeArguments() {
55 | // this.arguments = this.inputs.map(i => i.getValue())
56 | }
57 |
58 | calculate() {
59 | const params = zip(...this.arguments)
60 | const [ computedParams, stringParams ] = params.length ? params : [[], []]
61 |
62 | // return [ this.compute(...computedParams), this.string(...stringParams) ]
63 | }
64 |
65 | readyToCalculate() {
66 | return !this.arguments.some(arg => arg == null)
67 | }
68 |
69 | updateResult() {
70 | // this.result = this.readyToCalculate() ? this.calculate() : null
71 | }
72 |
73 | routeData() {
74 | // this.outputs.forEach(output => output.setValue(this.result))
75 | }
76 |
77 | // propagate(payload: Object, allowPropagation: boolean) { //? not used!
78 | // this.outputs.forEach(output => output.propagate(payload))
79 | // }
80 |
81 | compute() {
82 | throw `compute() not set on Process ${this}`
83 | }
84 |
85 | string(_?: string): string {
86 | throw `string() not set on Process ${this}`
87 | }
88 | }
--------------------------------------------------------------------------------
/src/interpreter/process/SetterProcess.ts:
--------------------------------------------------------------------------------
1 | import CustomProcess from './CustomProcess'
2 |
3 | export default class SetterProcess extends CustomProcess {
4 | constructor(private variable: string) {
5 | super(null, null, [{ label: variable }], [])
6 | }
7 |
8 | compute() {
9 | return null
10 | }
11 |
12 | string(value: string) {
13 | return `${this.variable} = ${value}`
14 | }
15 | }
--------------------------------------------------------------------------------
/src/interpreter/router/ControlFlowRouter.ts:
--------------------------------------------------------------------------------
1 | import { create } from '@/dock/interfaces'
2 | import InExeDock from '@/dock/InExeDock'
3 | import OutExeDock from '@/dock/OutExeDock'
4 | import Router from './Router'
5 |
6 | export default (function ControlFlowRouter() { //? don't need wrapper IIFE
7 | class Conditional extends Router {
8 | constructor() {
9 | super()
10 | this.in = create(InExeDock, [{label: 'in', location: 'head'}])
11 | this.out = create(OutExeDock, [{label: 'if', location: 'body'}, {label: 'else', location: 'body'}])
12 | }
13 |
14 | func() {
15 | // this.process.execute() //? no param?
16 | // this.out[this.process.arguments[0][0] ? 0 : 1].trigger()
17 | }
18 |
19 | header() {
20 | return `if (${this.process.arguments[1][0]}) { \n blah blah \n }`
21 | }
22 | }
23 |
24 | class ForLoop extends Router {
25 | constructor() {
26 | super()
27 | this.in = create(InExeDock, [{label: 'in', location: 'head'}])
28 | this.out = create(OutExeDock, [{label: 'end', location: 'body'}, {label: 'body', location: 'body'}])
29 | }
30 | }
31 |
32 | return { Conditional, ForLoop }
33 | })()
--------------------------------------------------------------------------------
/src/interpreter/router/DefaultRouter.ts:
--------------------------------------------------------------------------------
1 | import { create } from '@/dock/interfaces'
2 | import InExeDock from '@/dock/InExeDock'
3 | import OutExeDock from '@/dock/OutExeDock'
4 | import Router from './Router'
5 |
6 | export default class DefaultRouter extends Router {
7 | public in: Array
8 | public out: Array
9 |
10 | constructor(in_ = true, out_ = true) { //? don't think this is necessary
11 | super()
12 | this.in = create(InExeDock, in_ ? [{ label: '', location: 'head' }] : [])
13 | this.out = create(OutExeDock, out_ ? [{ label: '', location: 'head' }] : [])
14 | }
15 |
16 | func() {
17 | this.out[0].propagate(true) //? what should updateET be?
18 | }
19 | }
--------------------------------------------------------------------------------
/src/interpreter/router/NullRouter.ts:
--------------------------------------------------------------------------------
1 | // import { TriggerArgs } from '../interfaces'
2 | import Router from './Router'
3 |
4 | export default class NullRouter extends Router {
5 | constructor() {
6 | super()
7 | this.in = []
8 | this.out = []
9 | }
10 |
11 | // /**
12 | // * @Overrides Router#trigger
13 | // */
14 | // trigger({ origin = this, updateET = true }: TriggerArgs) {
15 | // // $.Execution.log(`└──> [R-${this.constructor.name}] #trigger`)
16 | // // $.Execution.indent()
17 | // // this.execute({ origin, updateET })
18 | // // $.Execution.unindent()
19 | // }
20 | }
--------------------------------------------------------------------------------
/src/interpreter/router/Router.ts:
--------------------------------------------------------------------------------
1 | import InExeDock from '@/dock/InExeDock'
2 | import OutExeDock from '@/dock/OutExeDock'
3 | import ExecutionTree from '../ExecutionTree'
4 | import { TriggerArgs } from '../interfaces'
5 | import Process from '../process/Process'
6 |
7 | export default class Router {
8 | public in: Array
9 | public out: Array
10 | public executionTree?: ExecutionTree
11 | public process: Process
12 |
13 | get docks() {
14 | return new Set([...this.in, ...this.out])
15 | }
16 |
17 | public getRoot(): Router {
18 | return this.in[0].ancestor ? this.in[0].ancestor.router.getRoot() : this
19 | }
20 |
21 | trigger({ accessor, origin = this, updateET = true }: TriggerArgs) { //? type
22 | // $.Execution.log(`└──> [R-${this.constructor.name}] #trigger`)
23 | // $.Execution.indent()
24 |
25 | if (!updateET) {
26 | // $.Execution.log('└── update blocked, exiting')
27 | // $.Execution.unindent()
28 | return
29 | }
30 |
31 | // $.Execution.log(`├── (1) root ${this.root === this ? 'is' : 'is not'} self`)
32 | // $.Execution.log(`├── (2) get the execution tree ${this.root === this ? 'from self' : 'from root'}`)
33 |
34 | const root = this.getRoot()
35 | root.executionTree = ExecutionTree.get(root)
36 |
37 | // $.Execution.log(`└── (3) executing the execution tree ${this.root.executionTree}`)
38 | // $.Execution.indent()
39 | root.executionTree.update(accessor, origin)
40 | // $.Execution.unindent()
41 | // $.Execution.unindent()
42 | }
43 |
44 | execute(payload: TriggerArgs) { //?
45 | // $.Execution.log(`└──> [R-${this.constructor.name}] #execute`)
46 | // $.Execution.indent()
47 | // this.process.execute(payload)
48 | // $.Execution.unindent()
49 | }
50 |
51 | // header() {}
52 | }
--------------------------------------------------------------------------------
/src/node/ControlFlowNode.ts:
--------------------------------------------------------------------------------
1 | // import Graph from '@/Graph'
2 | // import ControlFlowProcess from '@/interpreter/process/ControlFlowProcess'
3 | // import ControlFlowRouter from '@/interpreter/router/ControlFlowRouter'
4 | // import Node from '@/Node'
5 |
6 | export default class ControlFlowNode extends Node {
7 | // constructor(graph: Graph, args) { //? ???
8 | // const { type, ...nodeAttributes } = args
9 | // super(new ControlFlowProcess[type](), new ControlFlowRouter[type](), graph, nodeAttributes)
10 | // }
11 | }
--------------------------------------------------------------------------------
/src/node/FunctionNode.ts:
--------------------------------------------------------------------------------
1 | import Graph from '@/Graph'
2 | import { FunctionParams } from '@/interpreter/interfaces'
3 | import CustomProcess from '@/interpreter/process/CustomProcess'
4 | import DefaultRouter from '@/interpreter/router/DefaultRouter'
5 | import { ComponentParams } from './interfaces'
6 | import Node from './Node'
7 |
8 | export default class FunctionNode extends Node {
9 | constructor(graph: Graph, { process, ...nargs }: ComponentParams) {
10 | const { compute, string, params, result } = process as FunctionParams
11 | super(new CustomProcess(compute, string, params, result), new DefaultRouter(), graph, nargs)
12 | }
13 | }
--------------------------------------------------------------------------------
/src/node/GetterNode.ts:
--------------------------------------------------------------------------------
1 | import Graph from '@/Graph'
2 | import { GetterParams } from '@/interpreter/interfaces'
3 | import CustomProcess from '@/interpreter/process/CustomProcess'
4 | import DefaultRouter from '@/interpreter/router/DefaultRouter'
5 | import Node from '@/node/Node'
6 | import { ComponentParams } from './interfaces'
7 |
8 | export default class GetterNode extends Node {
9 | constructor(graph: Graph, { process, ...nargs }: ComponentParams) {
10 | const { compute, string, result } = process as GetterParams
11 | super(new CustomProcess(compute, string, [], result), new DefaultRouter(false, true), graph, nargs)
12 | }
13 | }
--------------------------------------------------------------------------------
/src/node/Node.ts:
--------------------------------------------------------------------------------
1 | import Dock from '@/dock/Dock'
2 | import Graph from '@/Graph'
3 | import { GraphObject, GraphObjectItem } from '@/GraphObject'
4 | import Process from '@/interpreter/process/Process'
5 | import NullRouter from '@/interpreter/router/NullRouter'
6 | import Router from '@/interpreter/router/Router'
7 | import { id } from '@/utils'
8 | import DockElement from '@/view/DockElement'
9 | import NodeElement from '@/view/NodeElement'
10 | import { NodeParams } from './interfaces'
11 |
12 | class Docks extends Array {
13 | constructor(...docks: Set[]) {
14 | super(...docks.map(d => Array.from(d)).flat())
15 | }
16 |
17 | public get elements(): Array {
18 | return this.map(({ element }) => element)
19 | }
20 |
21 | public get binds(): Array {
22 | return this.map(d => d.binds[0])
23 | }
24 |
25 | public attachTo(router: Router): Docks {
26 | this.forEach(d => d.router = router)
27 | return this
28 | }
29 |
30 | public update(): void {
31 | this.forEach(d => d.update())
32 | }
33 | }
34 |
35 | /**
36 | * Node is the model of a graph node. It uses by default to represent itself visually.
37 | * Node's view class (by default NodeElement) should meet implement the #update and #remove methods.
38 | */
39 | export default class Node extends GraphObject {
40 | private id: symbol
41 | public graph: Graph
42 | public router: Router
43 | public element: NodeElement
44 | public docks: Docks
45 |
46 | public get binds(): Array {
47 | const binds = this.docks.binds
48 | binds.push([this.element.header, this], [this.element.container, this])
49 | return binds
50 | }
51 |
52 | constructor(process: Process, router: Router | null, graph: Graph, params: NodeParams) {
53 | //? can you pass the process to the router's constructor?
54 | super()
55 |
56 | this.id = id()
57 | this.graph = graph
58 | this.router = router || new NullRouter()
59 | this.router.process = process
60 |
61 | this.docks = new Docks(process.docks, this.router.docks).attachTo(this.router)
62 | this.element = new NodeElement(this.docks.elements, graph.canvas, params)
63 | }
64 |
65 | /**
66 | * This method updates all links connected to the node.
67 | */
68 | update(): void {
69 | this.docks.update()
70 | }
71 |
72 | destroy() {
73 | // this.docks.forEach(dock => dock.destroy())
74 | this.graph.unregister(this)
75 | this.element.remove()
76 | }
77 |
78 | toString() {
79 | return this.element.labelText
80 | }
81 |
82 | select() {
83 | return this.element.container.classList.toggle('selected')
84 | }
85 | }
--------------------------------------------------------------------------------
/src/node/OperatorNode.ts:
--------------------------------------------------------------------------------
1 | import Graph from '@/Graph'
2 | import { FunctionParams } from '@/interpreter/interfaces'
3 | import CustomProcess from '@/interpreter/process/CustomProcess'
4 | import { ComponentParams } from '@/node/interfaces'
5 | import Node from '@/node/Node'
6 |
7 | export default class OperatorNode extends Node {
8 | constructor(graph: Graph, { process, ...nargs }: ComponentParams) {
9 | const { compute, string, params, result } = process as FunctionParams
10 | super(new CustomProcess(compute, string, params, result), null, graph, nargs)
11 | }
12 | }
--------------------------------------------------------------------------------
/src/node/SetterNode.ts:
--------------------------------------------------------------------------------
1 | import Graph from '@/Graph'
2 | import { SetterParams } from '@/interpreter/interfaces'
3 | import CustomProcess from '@/interpreter/process/CustomProcess'
4 | import DefaultRouter from '@/interpreter/router/DefaultRouter'
5 | import { ComponentParams } from '@/node/interfaces'
6 | import Node from './Node'
7 |
8 | export default class SetterNode extends Node {
9 | constructor(graph: Graph, { process, ...nargs }: ComponentParams) {
10 | const { compute, string, params } = process as SetterParams
11 | super(new CustomProcess(compute, string, params, []), new DefaultRouter(true, false), graph, nargs)
12 | }
13 | }
--------------------------------------------------------------------------------
/src/node/interfaces.ts:
--------------------------------------------------------------------------------
1 | import Graph from '@/Graph'
2 | import { ProcessParams } from '@/interpreter/interfaces'
3 | import { Pair } from '@/types'
4 |
5 | export interface NodeParams {
6 | label: string
7 | background: string
8 | header: string
9 | position: number[]
10 | }
11 |
12 | export interface ComponentParams {
13 | process: ProcessParams
14 | label: string
15 | header: string
16 | background: string
17 | position: number[]
18 | }
19 |
20 | export type NodeCstr = new (graph: Graph, args: ComponentParams) => N
21 |
22 | export interface NodeInstance extends ComponentParams {
23 | cstr: NodeCstr
24 | }
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import Dock from '@/dock/Dock'
2 |
3 | /**
4 | * Just a string -> number mapping, merely a type alias
5 | */
6 | export type map = {[key: string]: number}
7 |
8 | /**
9 | * Tuple shorthand: it's better to spread a two element number [T, T] than a two element array T[]
10 | */
11 | export type Pair = [T, T]
12 |
13 | /**
14 | * Unpack the type of an array: for instance Unpack will return type number.
15 | */
16 | export type Unpack = T extends (infer U)[] ? U : T
17 |
18 | /**
19 | * Extremely useful when mapping a zipped array since types are infered from the input.
20 | * Vary cool zip type: Zip<[number[], string[], boolean[]]> will be [number, string, boolean]
21 | */
22 | export type Zip = { [K in keyof T]: Unpack }
23 | export type ZipIterable = IterableIterator>
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { Pair, Zip } from '@/types'
2 |
3 | /**
4 | * One liner utility functions. It's basically a handy little TS toolbox. Do enjoy of course.
5 | */
6 |
7 |
8 | /**
9 | * The most useful equipment out there. This is a one-line firewall right there.
10 | */
11 | export function assert(condition: T, msg?: string): asserts condition {
12 | if (!condition) throw new Error(msg)
13 | }
14 |
15 | /**
16 | * This is probably never going to be used because it's useless.
17 | */
18 | export function isEmpty(str: string): boolean {
19 | return !str.replace(/\s/g, '').length
20 | }
21 |
22 | /**
23 | * You think it's useless until you try it. Think of "async execution queue".
24 | */
25 | export function wait(fn: Function): number {
26 | return setTimeout(fn, 0)
27 | }
28 |
29 | /**
30 | * Space string generator, always useful when you want to stretch it a bit.
31 | */
32 | export function space(n: number): string {
33 | return new Array(n).fill(' ').join('')
34 | }
35 |
36 | /**
37 | * Returns unique identifier, not thanks to Math.random.
38 | */
39 | export function id(): symbol {
40 | return Symbol(Math.random().toString(36).substr(2))
41 | }
42 |
43 | /**
44 | * Probably better than Python because you get an actual tuple type which is very neat.
45 | */
46 | export function zip(...arrays: T): Zip[] {
47 | return (arrays[0] || []).map((_ = null, i: number) => arrays.map(arr => arr[i]))
48 | }
49 |
50 | /**
51 | * Very nice for duping single values into tuple pairs.
52 | * Called with either a single number `pair(0)` -> `[0, 0]: Pair`
53 | * or with multiple number pair(posX, posY)` -> `[posX, posY]: Pair`.
54 | */
55 | export function pair(...x: T[]): Pair {
56 | return (x.length > 1 ? x : [x[0], x[0]]) as Pair
57 | }
58 | // export function pair(x: T, y = x): Pair {
59 | // return [x, y] as Pair
60 | // }
61 |
62 | // /**
63 | // * This function was nearly born.
64 | // */
65 | // export function pair(a: Pair, b: Pair, f: (x: T, y: T) => U): Pair {
66 | // return [a, b].map(([x, y]) => f(x, y)) as Pair
67 | // }
68 |
69 | /**
70 | * Stops runaways from going too far off.
71 | */
72 | export function clamp(x: number, min: number, max: number): number {
73 | return x > max ? max : (x < min ? min : x)
74 | }
75 |
76 | /**
77 | * Totally worth it if you want to make some string look cooler than it actually is.
78 | */
79 | export function capitalize(str: string) {
80 | return str.charAt(0).toUpperCase() + str.slice(1)
81 | }
82 |
83 | /**
84 | * I don't know what this is for.
85 | */
86 | export function toAlphabet(i: number): string {
87 | return String.fromCharCode(97 + i)
88 | }
89 |
90 | /**
91 | * Advanced normalizer utility function, does extremely complex computation behind the scenes.
92 | */
93 | export function normalize([x, shift = 0, scale = 1, adjust = 0]: [number, number, number, number]): number {
94 | return (x - shift) / scale + adjust
95 | }
96 |
97 | /**
98 | * As the name implies. It won't do much... well I hope so.
99 | */
100 | export function identity(...any: T[]): T[] {
101 | return any
102 | }
--------------------------------------------------------------------------------
/src/view/CanvasElement.ts:
--------------------------------------------------------------------------------
1 | import { Pair } from '@/types'
2 | import { zip } from '@/utils'
3 | import ElementWrapper from './Element'
4 | import Template from './Template'
5 |
6 | export default class CanvasElement extends ElementWrapper {
7 | public zoomWrapper: HTMLElement
8 | public positionWrapper: HTMLElement
9 | public nodeArea: HTMLElement
10 | public linkArea: HTMLElement
11 | public infobox: HTMLElement
12 |
13 | static closestCanvas(element: Element): HTMLElement | null {
14 | return element.closest('.objects')
15 | }
16 |
17 | get parentSize(): Pair {
18 | return this.getBoundingClientRect(this.container, 'size')
19 | }
20 |
21 | get size(): Pair {
22 | return this.getBoundingClientRect(this.positionWrapper, 'size')
23 | }
24 |
25 | get parentOffset(): Pair {
26 | return this.getBoundingClientRect(this.container, 'position')
27 | }
28 |
29 | get offset(): Pair {
30 | return this.getBoundingClientRect(this.positionWrapper, 'position')
31 | }
32 |
33 | get position(): Pair {
34 | return [ this.positionWrapper.style.left, this.positionWrapper.style.top ]
35 | .map(parseFloat) as Pair
36 | }
37 |
38 | set position([ x, y ]: Pair) {
39 | Object.assign(this.positionWrapper.style, { left: `${x}px`, top: `${y}px` })
40 | }
41 |
42 | constructor(parent: Element) {
43 | super()
44 | this.create()
45 | parent.appendChild(this.container)
46 | // this.render(parent) //? can this be called from super?
47 | }
48 |
49 | getProperties(): [number, number, number, number, number][] {
50 | return zip(this.position, this.offset, this.parentOffset, this.size, this.parentSize)
51 | }
52 |
53 | updateInfo(pos: Pair): void {
54 | this.infobox.textContent = pos.map(x => `${Math.round(x)}px`).join(', ')
55 | }
56 |
57 | /**
58 | * @overrides Element#create
59 | */
60 | create(): void { //? rename to setup
61 | const $ = Template.import('canvas')
62 |
63 | Object.assign(this, {
64 | container: $('.canvas-wrapper'),
65 | zoomWrapper: $('.canvas'),
66 | positionWrapper: $('.objects'),
67 | nodeArea: $('.nodes'),
68 | linkArea: $('.links > svg'),
69 | infobox: $('.canvas-infobox'),
70 | })
71 | }
72 | }
--------------------------------------------------------------------------------
/src/view/CanvasZoom.ts:
--------------------------------------------------------------------------------
1 | import Canvas from '@/Canvas'
2 |
3 | export default class CanvasZoom {
4 | public FACTOR = -0.05
5 |
6 | get level(): number {
7 | const scaleFromStyle = this.element.style.transform.replace(/[^\d.]/g, '')
8 | return parseFloat(scaleFromStyle) || 1
9 | }
10 |
11 | set level(scale: number) {
12 | this.element.style.transform = `scale(${scale})`
13 | }
14 |
15 | constructor(private canvas: Canvas, private element: HTMLElement) {}
16 |
17 | update(direction: number): void {
18 | const scale = this.level + this.FACTOR * direction
19 | if (0.5 <= scale && scale <= 2) this.zoom(scale)
20 | }
21 |
22 | zoom(scale: number): void {
23 | this.updateOrigin()
24 | this.level = scale
25 | this.canvas.recalculatePosition()
26 | }
27 |
28 | updateOrigin(): void {
29 | this.element.style.transformOrigin = this.canvas.element.parentSize
30 | .map(value => value / 2 + 'px').join(' ')
31 | }
32 | }
--------------------------------------------------------------------------------
/src/view/DockElement.ts:
--------------------------------------------------------------------------------
1 | import { DockSide, FlowType, InputType } from '@/dock/interfaces'
2 | import { Pair } from '@/types'
3 | import { normalize, pair, wait, zip } from '@/utils'
4 | import CanvasZoom from './CanvasZoom'
5 | import ElementWrapper from './Element'
6 | import NodeElement from './NodeElement'
7 | import Template from './Template'
8 |
9 | export default class DockElement extends ElementWrapper {
10 | static params = {
11 | [FlowType.EXE]: { offset: 7 },
12 | [FlowType.DATA]: { offset: 7 },
13 | }
14 |
15 | public pin: Element
16 | public snap: Element
17 | public param: Element
18 | public label: Element
19 | private node: NodeElement
20 | public offset: Pair
21 |
22 | get position(): Pair {
23 | return zip(this.node.position, [], [], this.offset).map(normalize) as Pair
24 | }
25 |
26 | get labelText() {
27 | return this.label.textContent
28 | }
29 |
30 | set labelText(label) {
31 | this.label.textContent = label
32 | }
33 |
34 | constructor(public type: FlowType, public side: DockSide, label: string, public location: string, private datatype?: InputType) {
35 | super()
36 | this.create()
37 |
38 | if (this.label) this.labelText = label
39 | }
40 |
41 | render(node: NodeElement, zoom: CanvasZoom) {
42 | this.node = node
43 | node.getBodyPart(this.location, this.side).appendChild(this.container)
44 | wait(() => this.offset = this.computeOffset(zoom))
45 | }
46 |
47 | /**
48 | * @overrides Element#create
49 | */
50 | create() { //# if that's the constructor then put it in the constructor
51 | const $ = Template.import('dock', this.datatype)
52 |
53 | Object.assign(this, {
54 | container: $('.dock-container'),
55 | pin: $('.dock'),
56 | snap: $('.snap-dock'),
57 | param: $('.param-container'),
58 | label: $('.param-label'),
59 | })
60 |
61 | this.container.classList.add(this.side, this.type)
62 | }
63 |
64 | private computeOffset(zoom: CanvasZoom): Pair { //? can use Element#getBoundingClientRect directly
65 | //# use this.node.getBoundingClientRect('container') and use minimized interface to NodeElement
66 | const nodePos = this.node.container.getBoundingClientRect()
67 | const dockPos = this.pin.getBoundingClientRect()
68 | const offset = DockElement.params[this.type].offset
69 |
70 | return zip([dockPos.x, dockPos.y], [nodePos.x, nodePos.y], pair(zoom.level), pair(offset))
71 | .map(normalize) as Pair
72 | }
73 | }
--------------------------------------------------------------------------------
/src/view/Element.ts:
--------------------------------------------------------------------------------
1 | import { Pair } from '@/types'
2 | import { assert } from '@/utils'
3 |
4 | export default abstract class ElementWrapper {
5 | public container: HTMLElement
6 |
7 | abstract create(args?: {[k: string]: string}): void
8 | abstract get position(): Pair
9 | abstract set position(position: Pair)
10 |
11 | // render(parent: Element) { //? do we really need this?
12 | // parent.appendChild(this.container)
13 | // }
14 |
15 | remove() {
16 | this.container.remove()
17 | }
18 |
19 | getBoundingClientRect(element: Element, property: string): Pair { //# rename shorter
20 | const { width, height, x, y } = element.getBoundingClientRect()
21 |
22 | switch (property) { //# use enum please
23 | case 'size': return [ width, height ]
24 | case 'position': return [ x, y ]
25 | default: assert(false, `property "${property}" not supported`)
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/src/view/LinkElement.ts:
--------------------------------------------------------------------------------
1 | import Path from '@/Path'
2 | import { FlowType } from '@/dock/interfaces'
3 | import { Pair } from '@/types'
4 | import { pair } from '@/utils'
5 | import ElementWrapper from './Element'
6 | import Template from './Template'
7 |
8 | export default class LinkElement extends ElementWrapper {
9 | static props = {
10 | [FlowType.DATA]: { width: 3, stroke: '#4CAF50' },
11 | [FlowType.EXE]: { width: 4, stroke: '#3F51B5' },
12 | }
13 |
14 | get position(): Pair {
15 | return pair(0) //# should be ElementWrapper default implementation
16 | }
17 |
18 | /**
19 | * Getter/Setter for `LinkElement#stroke` the color of the link.
20 | */
21 | get stroke(): string {
22 | return this.container.style.stroke
23 | }
24 |
25 | set stroke(newStroke: string) {
26 | this.container.style.stroke = newStroke
27 | }
28 |
29 | /**
30 | * Getter/Setter for `LinkElement#width`, the stroke size.
31 | */
32 | get width(): string {
33 | return this.container.style.strokeWidth
34 | }
35 |
36 | set width(newWidth: string) {
37 | this.container.style.strokeWidth = newWidth
38 | }
39 |
40 | /**
41 | * Getter/Setter for `LinkElement#path`, the path definition of the SVG element.
42 | */
43 | get path(): string {
44 | return this.container.getAttribute('d') || ''
45 | }
46 |
47 | set path(newPath: string) {
48 | this.container.setAttribute('d', newPath)
49 | }
50 |
51 | constructor(parent: Element, type: FlowType) {
52 | super()
53 | this.create()
54 | parent.appendChild(this.container)
55 | Object.assign(this, LinkElement.props[type])
56 | }
57 |
58 | /**
59 | * @overrides Element#create
60 | */
61 | create(): void {
62 | const $ = Template.import('link')
63 | Object.assign(this, { container: $('path') })
64 | }
65 |
66 | /**
67 | * Updates the link's svg representation.
68 | */
69 | update([a, b]: [Pair, Pair]): void {
70 | this.path = Path.calculate(a, b)
71 | }
72 | }
--------------------------------------------------------------------------------
/src/view/NodeElement.ts:
--------------------------------------------------------------------------------
1 | import { assert, capitalize, pair } from '@/utils'
2 | import Canvas from '@/Canvas'
3 | import { Pair } from '@/types'
4 | import { NodeParams } from '@/node/interfaces'
5 | import ElementWrapper from './Element'
6 | import DockElement from './DockElement'
7 | import Template from './Template'
8 |
9 | export default class NodeElement extends ElementWrapper {
10 | public canvas: Canvas
11 | public header: HTMLElement
12 | public label: HTMLElement
13 | public headLeft: HTMLElement
14 | public headRight: HTMLElement
15 | public bodyLeft: HTMLElement
16 | public bodyRight: HTMLElement
17 | public background: HTMLElement
18 |
19 | /**
20 | * Getter/Setter for the node's header background color.
21 | */
22 | get headerColor(): string {
23 | return this.header.style.background
24 | }
25 |
26 | set headerColor(color: string) {
27 | this.header.style.background = color
28 | }
29 |
30 | /**
31 | * Getter for the width and length of the node's container returned as an array.
32 | */
33 | get size(): Pair {
34 | const properties = this.container.getBoundingClientRect()
35 | return [ properties.width, properties.height ]
36 | }
37 |
38 | /**
39 | * Setter which adds/removes the `selected` class name from the node's container.
40 | */
41 | set highlight(bool: boolean) {
42 | this.container.classList[ bool ? 'add' : 'remove' ]('selected')
43 | }
44 |
45 | /**
46 | * Getter/Setter which returns the x and y coordinates of the node's position on the canvas.
47 | */
48 | get position(): Pair {
49 | return [ this.container.style.left, this.container.style.top ].map(parseFloat) as Pair
50 | }
51 |
52 | set position(position: Pair) {
53 | const [ x, y ] = this.boundaryClamp(position) //# this should be in Node and pass Node object as Placeable instead of Node#element...
54 | Object.assign(this.container.style, { left: `${x}px`, top: `${y}px` })
55 | }
56 |
57 | /**
58 | * Getter/Setter for the text label on the node's header section.
59 | */
60 | get labelText(): string {
61 | return this.label.textContent || ''
62 | }
63 |
64 | set labelText(label) {
65 | this.label.textContent = label
66 | }
67 |
68 | /**
69 | * Getter/Setter for node's background text label.
70 | */
71 | get backgroundText(): string {
72 | return this.label.textContent || ''
73 | }
74 |
75 | set backgroundText(background) {
76 | this.background.textContent = background
77 | }
78 |
79 | constructor(dockElements: Array, canvas: Canvas, params: NodeParams) {
80 | super()
81 |
82 | this.create()
83 | this.canvas = canvas
84 | this.render(dockElements)
85 |
86 | this.labelText = params.label
87 | this.backgroundText = params.background
88 | this.headerColor = params.header
89 | this.position = pair(...params.position || 0)
90 |
91 | // if (this.hideBody) this.hide('body') //? what is that
92 | // if (this.hideHeader) this.hide('header')
93 | }
94 |
95 | render(dockElements: DockElement[]) {
96 | this.canvas.element.nodeArea.appendChild(this.container)
97 | dockElements.forEach(dock => dock.render(this, this.canvas.zoom))
98 | }
99 |
100 | getBodyPart(location: string, side: string): Element {
101 | const part = location + capitalize(side)
102 | switch(part) {
103 | case 'headLeft': return this.headLeft
104 | case 'headRight': return this.headRight
105 | case 'bodyLeft': return this.bodyLeft
106 | case 'bodyRight': return this.bodyRight
107 | default: assert(false, `Node part "${part}" is not valid`)
108 | }
109 |
110 | }
111 |
112 | /**
113 | * @overrides Element#create
114 | */
115 | create(): void {
116 | const $ = Template.import('node')
117 |
118 | Object.assign(this, {
119 | container: $('.node-container'),
120 | header: $('.header'),
121 | label: $('.header-title'),
122 | headLeft: $('.header-block > .left-block'),
123 | headRight: $('.header-block > .right-block'),
124 | bodyLeft: $('.body > .left-block'),
125 | bodyRight: $('.body > .right-block'),
126 | background: $('.body > .background')
127 | })
128 | }
129 |
130 | /**
131 | * Clamps the provided [x, y] position so the node container remains inside its parent canvas area.
132 | */
133 | boundaryClamp(position: Pair): Pair {
134 | return position.map((value, i) => {
135 | const max = (this.canvas.element.size[i] - this.size[i]) / this.canvas.zoom.level
136 | return value <= 0 ? 0 : (value >= max ? max : value)
137 | }) as Pair //# refactor this boi
138 | }
139 |
140 | /**
141 | * Hides the corresponding node portion
142 | * @param part is either `header` or `body`
143 | */
144 | hide(part: string): void {
145 | this.container.classList.add(`hide-${part}`)
146 | }
147 |
148 | /**
149 | * Toggles the `selected` class name on the node's container element.
150 | * @returns the state of the node, i.e. if it is selected or not
151 | */
152 | toggleHighlight(): boolean {
153 | return this.container.classList.toggle('selected') //? is it this.container?
154 | }
155 | }
--------------------------------------------------------------------------------
/src/view/Template.ts:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Factory class for all HTML elements defined with a HTML template.
4 | */
5 | export default class Template {
6 | /**
7 | * Retrieves the template from the DOM then clones its content.
8 | * @param name template's name
9 | * @return a `querySelector` bound to the cloned template element
10 | */
11 | public static import(name: string, variant = ''): (selectors: string) => HTMLElement | null {
12 | const id = `template#${name}${variant ? '-' : ''}${variant}`
13 | const template = document.querySelector(id) as HTMLTemplateElement
14 | if (template == null) throw new Error(`could not find template with id ${id}`)
15 |
16 | const element = document.importNode(template.content, true)
17 | return element.querySelector.bind(element)
18 | }
19 | }
--------------------------------------------------------------------------------
/style/finder.less:
--------------------------------------------------------------------------------
1 | .finder {
2 | z-index: 3;
3 | padding-top: 100px;
4 | position: fixed;
5 | left: 0;
6 | top: 0;
7 | width: 100%;
8 | height: 100%;
9 | overflow: auto;
10 | background-color: rgb(0,0,0);
11 | background-color: rgba(0,0,0,0.4);
12 | }
13 |
14 | .search-container {
15 | box-sizing: border-box;
16 | padding: 10px;
17 | width: 600px;
18 | background-color: #282c34;
19 | border-radius: 6px;
20 | margin: 20px auto;
21 | }
22 |
23 | .search-input {
24 | background-position: 10px 10px;
25 | background-repeat: no-repeat;
26 | width: 100%;
27 | padding: 1px 20px 1px 10px;
28 | box-sizing: border-box;
29 | font-size: 1.1em;
30 | line-height: 33px;
31 | border-radius: 6px;
32 | color: #d7dae0;
33 | background-color: #1b1d23;
34 | border: 2px solid #282c34;
35 |
36 | &:focus {
37 | border: 2px solid rgba(8, 157, 10, 0.86);
38 | outline: 0 none;
39 | }
40 | }
41 |
42 | .search-wrap {
43 | max-height: 229px;
44 | overflow-y: auto;
45 | padding: 0px;
46 | margin-top: 12px;
47 | border: 1px solid rgba(0, 0, 0, 0.56);
48 | overflow-x: hidden;
49 | margin-bottom: 4px;
50 | box-sizing: border-box;
51 |
52 | &::-webkit-scrollbar {
53 | width: 9px;
54 | }
55 | &::-webkit-scrollbar-track {
56 | box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
57 | }
58 | &::-webkit-scrollbar-thumb {
59 | border-radius: 10px;
60 | box-shadow: inset 0 0 6px rgba(0,0,0,0.5);
61 | margin: 1px;
62 | }
63 | &::-webkit-scrollbar-thumb:hover {
64 | box-shadow: inset 0px 0px 13px 2px rgba(0,0,0,0.5);
65 | border-radius: 0px;
66 | }
67 | }
68 |
69 | ::selection {
70 | background-color: rgba(15, 140, 15, 0.75);
71 | }
72 |
73 | .search-table {
74 | width: calc(100% + 0px);
75 | font-size: 18px;
76 | padding: 0px;
77 | border-collapse: collapse;
78 | margin-bottom: 0px;
79 | box-sizing: border-box;
80 | margin-top: -1px;
81 | overflow-x: hidden;
82 |
83 | tr {
84 | border-top: 1px solid rgba(0, 0, 0, 0.49);
85 | border-right: rgb(44, 49, 58);
86 | border-left: rgb(44, 49, 58);
87 | padding: 77px;
88 | margin: 0px;
89 | color: #aaabac;
90 | font-size: 14px;
91 | font-family: 'Lato', sans-serif;
92 | font-family: 'Saira Semi Condensed', sans-serif;
93 | cursor: pointer;
94 | }
95 |
96 | tr, td {
97 | text-align: left;
98 | padding: 12px;
99 | }
100 | }
101 |
102 | tr {
103 | &.hovered {
104 | background-color: rgb(58, 63, 75);
105 | }
106 | &.not-hovered, &:hover {
107 | background-color: rgb(44, 49, 58);
108 | }
109 | }
--------------------------------------------------------------------------------
/style/input.less:
--------------------------------------------------------------------------------
1 | // .header > .header-block {
2 | // position: relative;
3 | // width: 100%;
4 | // bottom: calc(50% - 14px);
5 | // pointer-events: none;
6 | // }
7 |
8 | input:focus, textarea:focus, select:focus {
9 | outline-offset: 0px;
10 | }
11 |
12 | .constant > .dock {
13 | border-radius: 0px;
14 | transform: rotate(90deg);
15 | }
16 |
17 | /*input dock parameter*/
18 | input.occupied {
19 | // background-color: rgb(81, 81, 81) !important;
20 | // background: linear-gradient(to top left,
21 | // rgba(0,0,0,0) 0%,
22 | // rgba(0,0,0,0) calc(50% - 0.8px),
23 | // rgba(0,0,0,1) 50%,
24 | // rgba(0,0,0,0) calc(50% + 0.8px),
25 | // rgba(0,0,0,0) 100%);
26 | color: transparent;
27 | }
28 | /*
29 | .exe > .dock {
30 | transition: 0.08s linear all;
31 | }
32 | .exe > .occupied {
33 | transform: rotate(0deg) !important;
34 | }*/
35 |
36 | input[type=number]::-webkit-inner-spin-button,
37 | input[type=number]::-webkit-outer-spin-button {
38 | -webkit-appearance: none;
39 | margin: 0;
40 | }
41 |
42 | input.var-input::placeholder {
43 | color: #666161;
44 | font-style: italic;
45 | }
46 |
47 | .param-container > input {
48 | text-align: left;
49 | line-height: 25px;
50 | font-size: 13px;
51 | font-family: Consolas, 'Courier New', monospace;
52 | font-weight: 500;
53 | background-color: rgba(111, 111, 111, 0.02);
54 | outline: none;
55 | border: 1px solid #403e3e;
56 | border-radius: 5px;
57 |
58 | &:focus {
59 | border: 1px solid rgb(34, 33, 33);
60 | }
61 | }
62 |
63 | .left > .param-container > input {
64 | pointer-events: visible;
65 | margin-left: 12px;
66 | width: calc(100% - 12px);
67 | color: #1b1b1b;
68 | box-sizing: border-box;
69 | background-color: rgb(73, 70, 70);
70 | padding: 0 5px;
71 | }
72 |
73 | .right > .param-container > input {
74 | margin-right: 8px;
75 | width: calc(100% - 8px);
76 | box-sizing: border-box;
77 | text-align: left;
78 | }
79 | // .param-container > input {
80 | // outline: 2px auto rgba(30, 29, 29, 0.52);
81 | // }
82 | // .param-container > input:hover {
83 | // outline: #4f70a7 auto 5px;
84 | // }
85 | // .param-container > input::selection {
86 | // background-color: darkgrey;
87 | // }
--------------------------------------------------------------------------------
/style/main.less:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | html,
6 | body,
7 | section,
8 | svg {
9 | height: 100%;
10 | margin: 0;
11 | padding: 0;
12 | }
13 |
14 | body,
15 | svg {
16 | width: 100%;
17 | border: 1px transparent;
18 | // z-index: 1;
19 | }
20 |
21 | .links {
22 | position: absolute;
23 | }
24 |
25 | body {
26 | user-select: none;
27 | cursor: default;
28 | overflow: hidden;
29 | background: #393939;
30 | }
31 |
32 | .canvas-wrapper {
33 | height: 100%;
34 | }
35 |
36 | .canvas-infobox {
37 | position: absolute;
38 | top: 0;
39 | right: 0;
40 | padding: 10px;
41 | font-size: 18px;
42 | font-family: 'Montserrat', sans-serif;
43 | font-weight: 900;
44 | z-index: 10;
45 | color: rgba(0, 0, 0, 0.37);
46 | }
47 |
48 | .window {
49 | overflow: hidden;
50 | width: 100%;
51 | height: 100%;
52 | // width: 1000px;
53 | // height: 750px;
54 | // position: absolute;
55 | // left: 10px;
56 | }
57 |
58 | .canvas {
59 | position: relative;
60 | }
61 |
62 | .objects {
63 | --objects-size: 5000px;
64 | // --objects-size: 1500px;
65 | width: var(--objects-size);
66 | height: var(--objects-size);
67 |
68 | // ------------------------- background
69 | --small-line-color: #1a1a1a45;
70 | --main-line-color: #24242487;
71 | --background-color: #393939;
72 | --small-line-size: 1px;
73 | --main-line-size: 1.5px;
74 |
75 | background:
76 | linear-gradient(-90deg, var(--small-line-color) var(--small-line-size), transparent 1px),
77 | linear-gradient(var(--small-line-color) var(--small-line-size), transparent 1px),
78 | linear-gradient(-90deg, var(--main-line-color) var(--main-line-size), transparent 1px),
79 | linear-gradient(var(--main-line-color) var(--main-line-size), transparent 1px),
80 | linear-gradient(transparent 3px, var(--background-color) 3px, var(--background-color) 88px, transparent 88px),
81 | linear-gradient(-90deg, black 1px, transparent 1px),
82 | linear-gradient(-90deg, transparent 3px, var(--background-color) 3px, var(--background-color) 88px, transparent 88px),
83 | linear-gradient(black 1px, transparent 1px),
84 | var(--background-color);
85 |
86 | background-size:
87 | 18px 18px,
88 | 18px 18px,
89 | 90px 90px,
90 | 90px 90px,
91 | 90px 90px,
92 | 90px 90px,
93 | 90px 90px,
94 | 90px 90px;
95 |
96 | font-weight: 500;
97 | font-family: Calibri;
98 | position: absolute;
99 | z-index: 2;
100 | border: 3px dashed #c31a1acf;
101 | }
102 |
103 | @font-face {
104 | font-family: CONSOLA;
105 | src: url('fonts/CONSOLA.ttf') format('ttf');
106 | }
107 |
108 | .hidden {
109 | display: none;
110 | }
111 |
112 | path {
113 | fill: #ffffff0f;
114 | // pointer-events: none; // w/out it, is this an issue?
115 | stroke-dasharray: 10;
116 | animation: dash 30s linear;
117 | animation-iteration-count: infinite;
118 | animation-direction: reverse;
119 | fill: none;
120 | }
121 |
122 | @keyframes dash {
123 | to {
124 | stroke-dashoffset: 1000;
125 | }
126 | }
127 |
128 | .background-text {
129 | // color: rgba(19, 19, 19, 0.075);
130 | color: red;
131 | font-size: 120px;
132 | font-family: 'Montserrat', sans-serif;
133 | font-weight: 900;
134 | position: absolute;
135 | transform: translate(-50%,-50%);
136 | left: 50%;
137 | top: 50%;
138 | pointer-events: none;
139 | z-index: 0;
140 | }
141 |
142 | .footer {
143 | pointer-events: none;
144 | z-index: 100000;
145 | position: absolute;
146 | bottom: 0;
147 | font-family: 'Montserrat', sans-serif;
148 | font-weight: 900;
149 | width: 100%;
150 |
151 | .left {
152 | font-size: 44px;
153 | color: rgba(0, 0, 0, 0.37);
154 | padding: 10px 30px;
155 | text-align: left;
156 | float: left;
157 | }
158 |
159 | .right {
160 | right: 0;
161 | font-size: 52px;
162 | color: rgba(0, 0, 0, 0.37);
163 | padding: 2px 15px;
164 | text-align: right;
165 | float: right;
166 | }
167 | }
--------------------------------------------------------------------------------
/style/node.less:
--------------------------------------------------------------------------------
1 | // ::parameter container
2 | .param-container {
3 | width: 100%;
4 | color: #030303;
5 | top: 50%;
6 | position: absolute;
7 | transform: translateY(-50%);
8 | font-size: 13px;
9 | font-weight: 500;
10 | box-sizing: border-box;
11 | }
12 | .param-label {
13 | box-sizing: border-box;
14 | width: 100%;
15 | display: inline-block;
16 | color: #1b1b1b;
17 | }
18 | .left > .param-container > .param-label {
19 | text-align: left;
20 | width: calc(100% - 12px);
21 | margin-left: 12px;
22 | }
23 | .right > .param-container > .param-label {
24 | text-align: right;
25 | padding-right: 11px;
26 | }
27 |
28 | // ::Canvas
29 | .dock-container {
30 | height: 25px;
31 | line-height: 25px;
32 | margin: auto;
33 | position: relative;
34 | box-sizing: border-box;
35 | vertical-align: middle;
36 | z-index: 10;
37 | }
38 |
39 | .nodes {
40 | pointer-events: visible;
41 | }
42 |
43 | // ::Dock::
44 | .data {
45 | &.left > .dock {
46 | background-color: #14a914;
47 | }
48 | &.right > .dock {
49 | background-color: #d93f8c;
50 | }
51 | }
52 |
53 | // optional
54 | // background-color: #515151;
55 | // border: 2px solid #14a914;
56 | // border-radius: 0;
57 |
58 | .exe {
59 | > .dock {
60 | // transform: rotate(45deg);
61 | background-color: #2439a9;
62 | border-radius: 0px;
63 | }
64 | }
65 |
66 | .dock {
67 | cursor: pointer;
68 | top: ~"calc(50% - 4px)";
69 | }
70 |
71 | .dock,
72 | .snap-dock {
73 | pointer-events: visible;
74 | height: 12px;
75 | width: 12px;
76 | border-radius: 8px;
77 | vertical-align: middle;
78 | position: absolute;
79 | border: 0.05em solid rgba(0, 0, 0, 0.5);
80 | box-sizing: border-box;
81 | // z-index: 1;
82 | }
83 |
84 | .snap-dock {
85 | // z-index: 3;
86 | width: 30px; //# make smaller
87 | height: 30px;
88 | background-color: rgba(111, 111, 111, 0);
89 | cursor: pointer;
90 | border-color: rgba(111, 111, 111, 0);
91 | top: ~"calc(50% - 14px)";
92 | }
93 |
94 | .left {
95 | > .dock {
96 | left: -4px;
97 | }
98 | > .snap-dock {
99 | left: -13px;
100 | }
101 | }
102 |
103 | .right {
104 | > .dock {
105 | right: -5px;
106 | }
107 | > .snap-dock {
108 | right: -14px;
109 | }
110 | }
111 |
112 | .dock, .snap-dock {
113 | border: none;
114 | }
115 |
116 | // .right > .dock {
117 | // right: -7px;
118 | // }
119 |
120 | // .left > .param-container > .param-label {
121 | // }
122 |
123 | // ::Node
124 | .node-container {
125 | font-family: Consolas, 'Courier New', monospace;
126 | // z-index: 1;
127 | position: absolute;
128 | width: 200px;
129 | box-sizing: border-box;
130 | transition: border 0.2s linear;
131 | border: 3px solid transparent;
132 | border-radius: 12px;
133 | border-color: transparent;
134 |
135 | &.selected {
136 | box-shadow: 0 0 2px #f44336;
137 | }
138 |
139 | &:hover {
140 | border: 3px solid #1b1b1b85;
141 | }
142 |
143 | &.hide-body {
144 | > .header {
145 | border-radius: 10px;
146 | }
147 | > .body {
148 | display: none;
149 | }
150 | }
151 |
152 | &.hide-header {
153 | > .header {
154 | display: none;
155 | }
156 | > .body {
157 | border-radius: 10px;
158 | }
159 | }
160 |
161 | > .body > .background {
162 | position: absolute;
163 | font-size: 5em;
164 | left: 50%;
165 | transform: translate(-50%, -50%);
166 | top: 50%;
167 | color: #00000042;
168 | padding: 0;
169 | // text-align: center;
170 | vertical-align: middle;
171 | float: left;
172 | }
173 | }
174 |
175 | .body {
176 | background-color: rgb(81, 81, 81);
177 | position: relative;
178 | pointer-events: none;
179 | min-height: 50px;
180 | padding: 8px 0px;
181 | border-radius: 0px 0px 12px 12px;
182 | }
183 |
184 | .right-block, .left-block {
185 | display: inline-block;
186 | width: 50%;
187 | vertical-align: middle;
188 | }
189 |
190 | .header {
191 | // border-radius: 8px 8px 0px 0px;
192 | border-radius: 12px 12px 0px 0px;
193 | background-color: rgb(56, 55, 56);
194 | position: relative;
195 | cursor: move;
196 | // z-index: 1;
197 | height: 100%;
198 | font-weight: bold;
199 | font-family: 'Saira Semi Condensed';
200 | // min-height: 40px;
201 | line-height: 30px;
202 | }
203 |
204 | .header-title {
205 | text-align: center;
206 | width: 100%;
207 | position: absolute;
208 | // margin: auto;
209 | // vertical-align: middle;
210 | top: 50%;
211 | left: 50%;
212 | pointer-events: none;
213 | transform: translate(-50%, -50%);
214 | font-size: 15px;
215 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "strict": true,
6 | "jsx": "preserve",
7 | "importHelpers": true,
8 | "moduleResolution": "node",
9 | "experimentalDecorators": true,
10 | "esModuleInterop": true,
11 | "allowSyntheticDefaultImports": true,
12 | "strictPropertyInitialization": false,
13 | "sourceMap": true,
14 | "baseUrl": ".",
15 | "paths": {
16 | "@*": [
17 | "src*"
18 | ]
19 | },
20 | "lib": [
21 | "esnext",
22 | "dom",
23 | "dom.iterable",
24 | "scripthost"
25 | ]
26 | },
27 | "include": [
28 | "src/**/*.ts",
29 | ],
30 | "exclude": [
31 | "node_modules"
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const ESLintPlugin = require('eslint-webpack-plugin')
3 |
4 | module.exports = {
5 | mode: 'development',
6 | entry: './src/index.ts',
7 | devtool: 'inline-source-map',
8 | devServer: {
9 | contentBase: path.resolve(__dirname, 'dist')
10 | // publicPath: "/assets", // path of the served resources (js, img, fonts...)
11 | },
12 | module: {
13 | rules: [
14 | {
15 | test: /\.tsx?$/,
16 | use: 'ts-loader',
17 | exclude: /node_modules/,
18 | },
19 | ],
20 | },
21 | plugins: [
22 | new ESLintPlugin({
23 | extensions: ['ts']
24 | })
25 | ],
26 | resolve: {
27 | extensions: [ '.tsx', '.ts', '.js' ],
28 | alias: {
29 | '@': path.resolve(__dirname, 'src/'),
30 | }
31 | },
32 | output: {
33 | filename: 'bundle.js',
34 | path: path.resolve(__dirname, 'dist'),
35 | },
36 | }
37 |
--------------------------------------------------------------------------------