├── .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 [![GitHub version](https://img.shields.io/static/v1?label=version&message=cg2020&color=green)](https://badge.fury.io/gh/WonJunior%2FCodeGraph)[](https://reposs.herokuapp.com/?path=WonJunior/CodeGraph&color=ff69b4) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 | ![](https://i.ibb.co/DrnQ3Q4/process.png) 16 | 17 | --- 18 | 19 | ##### cg2020 - release 0.1 (Mar 13) - Data propagation and tree execution with detailed logs 20 | 21 | ![](https://i.ibb.co/pZND4x9/releaseA.png) 22 | 23 | --- 24 | 25 | ##### Screenshot from previous build: Linking system, Node Finder and Live feedback 26 | 27 | ![alt text](https://image.ibb.co/iswbep/first.png) 28 | 29 | ![alt text](https://image.ibb.co/bZtyc9/finder.png) 30 | 31 | ![alt text](https://image.ibb.co/ezvpjp/action.png) 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 | 38 | 39 | 55 | 56 | 65 | 66 | 76 | 77 | 80 | 81 | 91 | 92 | 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 | // 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') // () 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 | --------------------------------------------------------------------------------