├── gear.psd ├── types.psd ├── types_3.psd ├── fn_mockup.psd ├── loop_mockup.psd ├── branch_mockup.psd ├── public ├── favicon.ico ├── manifest.json └── index.html ├── src ├── resource │ ├── f32.png │ ├── f64.png │ ├── i32.png │ ├── i64.png │ ├── int.png │ ├── s.png │ ├── str.png │ ├── c_int.png │ ├── c_obj.png │ ├── c_str.png │ ├── float.png │ ├── list.png │ ├── proc.png │ ├── type.png │ ├── add_port.png │ ├── c_bool.png │ ├── c_float.png │ ├── c_list.png │ ├── c_proc.png │ ├── c_type.png │ ├── c_unsolved.png │ └── App.css ├── index.css ├── App.test.js ├── index.js ├── state │ ├── GraphElement.js │ ├── Def.js │ ├── TypeSignature.js │ ├── EditProxy.js │ ├── NodeData.js │ ├── NodeGraph.js │ └── SelectionModel.js ├── render │ ├── mNewNodeDialog.js │ ├── mPortConfig.js │ ├── mSelectionList.js │ ├── mPortRack.js │ ├── mNode.js │ ├── mNarrowingList.js │ ├── mNodeView.js │ ├── mPort.js │ ├── mLink.js │ └── NodeBody.js ├── registerServiceWorker.js └── App.js ├── README.md ├── .gitignore └── package.json /gear.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/gear.psd -------------------------------------------------------------------------------- /types.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/types.psd -------------------------------------------------------------------------------- /types_3.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/types_3.psd -------------------------------------------------------------------------------- /fn_mockup.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/fn_mockup.psd -------------------------------------------------------------------------------- /loop_mockup.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/loop_mockup.psd -------------------------------------------------------------------------------- /branch_mockup.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/branch_mockup.psd -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/resource/f32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/src/resource/f32.png -------------------------------------------------------------------------------- /src/resource/f64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/src/resource/f64.png -------------------------------------------------------------------------------- /src/resource/i32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/src/resource/i32.png -------------------------------------------------------------------------------- /src/resource/i64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/src/resource/i64.png -------------------------------------------------------------------------------- /src/resource/int.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/src/resource/int.png -------------------------------------------------------------------------------- /src/resource/s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/src/resource/s.png -------------------------------------------------------------------------------- /src/resource/str.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/src/resource/str.png -------------------------------------------------------------------------------- /src/resource/c_int.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/src/resource/c_int.png -------------------------------------------------------------------------------- /src/resource/c_obj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/src/resource/c_obj.png -------------------------------------------------------------------------------- /src/resource/c_str.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/src/resource/c_str.png -------------------------------------------------------------------------------- /src/resource/float.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/src/resource/float.png -------------------------------------------------------------------------------- /src/resource/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/src/resource/list.png -------------------------------------------------------------------------------- /src/resource/proc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/src/resource/proc.png -------------------------------------------------------------------------------- /src/resource/type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/src/resource/type.png -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/resource/add_port.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/src/resource/add_port.png -------------------------------------------------------------------------------- /src/resource/c_bool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/src/resource/c_bool.png -------------------------------------------------------------------------------- /src/resource/c_float.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/src/resource/c_float.png -------------------------------------------------------------------------------- /src/resource/c_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/src/resource/c_list.png -------------------------------------------------------------------------------- /src/resource/c_proc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/src/resource/c_proc.png -------------------------------------------------------------------------------- /src/resource/c_type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/src/resource/c_type.png -------------------------------------------------------------------------------- /src/resource/c_unsolved.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trbabb/ramen/HEAD/src/resource/c_unsolved.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ramen is a node-based programming language editor, implemented as a React web app. 2 | 3 | To get started, clone the repository and run 4 | 5 | npm install 6 | npm start 7 | 8 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import registerServiceWorker from './registerServiceWorker'; 5 | import './index.css'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | //registerServiceWorker(); 9 | -------------------------------------------------------------------------------- /src/state/GraphElement.js: -------------------------------------------------------------------------------- 1 | export class GraphElement { 2 | 3 | constructor(type, id) { 4 | this.type = type 5 | this.id = id 6 | } 7 | 8 | key() { 9 | // i hate javascript. this is the dumbest goddamn thing. 10 | return JSON.stringify(this) 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | yarn.lock 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ramen", 3 | "version": "0.1.0", 4 | "private": true, 5 | "proxy": { 6 | "/socket": { 7 | "target" : "ws://localhost:5000", 8 | "ws" : true 9 | } 10 | }, 11 | "homepage": ".", 12 | "dependencies": { 13 | "immutability-helper": "^2.2.2", 14 | "immutable": "^3.8.1", 15 | "lodash": "^4.17.4", 16 | "react": "^15.5.4", 17 | "react-dom": "^15.5.4", 18 | "react-draggable": "^2.2.6", 19 | "socket.io": "^2.0.3" 20 | }, 21 | "devDependencies": { 22 | "react-scripts": "1.0.7" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "react-scripts build", 27 | "test": "react-scripts test --env=jsdom", 28 | "eject": "react-scripts eject" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/render/mNewNodeDialog.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {NarrowingList} from './mNarrowingList' 3 | 4 | 5 | export class NewNodeDialog extends React.Component { 6 | 7 | constructor(props) { 8 | super(props) 9 | this.state = { 10 | selected_def : null, 11 | } 12 | this.items = this.props.defs.map(def => def.name) 13 | } 14 | 15 | 16 | onListSelectionChanged = (selection_key) => { 17 | this.setState({ 18 | selected_def : selection_key 19 | }) 20 | } 21 | 22 | 23 | render() { 24 | 25 | return ( 26 |
27 | 31 |
32 | ) 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /src/render/mPortConfig.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import * as _ from 'lodash' 4 | 5 | import gearImg from '../resource/gear.png' 6 | 7 | export class PortConfig extends React.PureComponent { 8 | render() { 9 | var classes = ["PortConfig", this.props.is_sink ? "Sink" : "Source"] 10 | return ( 11 | { 17 | var e = {def_id : this.props.def_id, 18 | is_sink : this.props.is_sink, 19 | elem : this.elem, 20 | mouse_evt : evt} 21 | this.props.handlePortConfigClick(e); 22 | }} 23 | ref={(e) => { 24 | this.elem = e; 25 | }} /> 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/state/Def.js: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | 3 | // implements a function definition. 4 | 5 | export var NODE_TYPE = { 6 | NODE_LITERAL : "literal", 7 | NODE_BUILTIN : "builtin", 8 | NODE_CALL : "fncall", 9 | NODE_FUNCTION : "function", 10 | NODE_LOOP : "loop", 11 | NODE_BRANCH : "branch", 12 | NODE_ENTRY : "entry", 13 | NODE_EXIT : "exit", 14 | NODE_EXPORT : "export", 15 | } 16 | 17 | export class Def { 18 | 19 | constructor(name, node_type, type_sig) { 20 | this.name = name 21 | this.node_type = node_type 22 | this.type_sig = type_sig 23 | } 24 | 25 | addPort(port_id, type_id, is_sink) { 26 | var d = _.clone(this) 27 | d.type_sig = d.type_sig.addPort(port_id, type_id, is_sink) 28 | return d 29 | } 30 | 31 | 32 | removePort(port_id) { 33 | var d = _.clone(this) 34 | d.type_sig = d.type_sig.removePort(port_id) 35 | return d 36 | } 37 | 38 | 39 | hasBody() { 40 | return ( 41 | this.node_type === NODE_TYPE.NODE_LOOP || 42 | this.node_type === NODE_TYPE.NODE_FUNCTION || 43 | this.node_type === NODE_TYPE.NODE_BRANCH // unsure about this one :\ 44 | ) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/state/TypeSignature.js: -------------------------------------------------------------------------------- 1 | import {Map, List} from 'immutable' 2 | import * as _ from 'lodash' 3 | 4 | 5 | export class TypeSignature { 6 | 7 | 8 | constructor(sink_types={},source_types={}) { 9 | this.sink_types = new Map(sink_types) 10 | this.source_types = new Map(source_types) 11 | } 12 | 13 | 14 | // "mutators" 15 | 16 | 17 | addPort(port_id, type_obj, is_sink=false) { 18 | var ts = _.clone(this) 19 | if (is_sink) { 20 | ts.sink_types = ts.sink_types.add(port_id) 21 | } else { 22 | ts.source_types = ts.source_types.add(port_id) 23 | } 24 | return ts 25 | } 26 | 27 | 28 | removePort(port_id, is_sink) { 29 | var ts = _.clone(this) 30 | if (is_sink) { 31 | ts.sink_types = ts.sink_types.remove(port_id) 32 | } else { 33 | ts.source_types = ts.source_types.remove(port_id) 34 | } 35 | return ts 36 | } 37 | 38 | 39 | // getters 40 | 41 | 42 | getSourceIDs() { return this.source_types.keySeq() } 43 | 44 | getSinkIDs() { return this.sink_types.keySeq() } 45 | 46 | numSinks() { return this.sink_types.size } 47 | 48 | numSources() { return this.source_types.size } 49 | 50 | getAllPorts() { 51 | var m = new List().asMutable() 52 | for (let [port_id, t] of this.sink_types) { 53 | m.push({ 54 | port_id : port_id, 55 | is_sink : true, 56 | type : t, 57 | }) 58 | } 59 | for (let [port_id, t] of this.source_types) { 60 | m.push({ 61 | port_id : port_id, 62 | is_sink : false, 63 | type : t, 64 | }) 65 | } 66 | return m.asImmutable() 67 | } 68 | 69 | 70 | } -------------------------------------------------------------------------------- /src/render/mSelectionList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | 4 | export class SelectionList extends React.Component { 5 | 6 | 7 | componentDidMount() { 8 | document.addEventListener('keydown', this.onKeyDown); 9 | this.setFirstItemSelected() 10 | } 11 | 12 | 13 | setFirstItemSelected() { 14 | var selectionKey = this.props.itemList.length > 0 ? this.props.itemList[0].key : null 15 | this.props.onListSelectionChanged(selectionKey) 16 | } 17 | 18 | 19 | componentWillUnmount() { 20 | document.removeEventListener('keydown', this.onKeyDown) 21 | } 22 | 23 | 24 | onKeyDown = (evt) => { 25 | if (evt.key === "ArrowDown" || evt.key === "ArrowUp") { 26 | var curIdx = this.props.itemList.findIndex(x => x.key === this.props.selectionKey) 27 | var newIdx = -1 28 | if (evt.key === "ArrowDown") { 29 | newIdx = curIdx + 1 30 | } else if (evt.key === "ArrowUp") { 31 | // pick previous key in list 32 | newIdx = curIdx - 1 33 | } 34 | var b = this.props.itemList.length 35 | newIdx = (newIdx % b + b) % b // newIdx in range [0, list.length) 36 | this.props.onListSelectionChanged(this.props.itemList[newIdx].key) 37 | } 38 | } 39 | 40 | 41 | onMouseOverElement = (evt,key) => { 42 | this.props.onListSelectionChanged(key) 43 | } 44 | 45 | 46 | render() { 47 | var elems = this.props.itemList.map((x,i) => { 48 | var className = "SelectionListElement" 49 | if (x.key === this.props.selectionKey) { 50 | className += " Selected" 51 | } 52 | return {this.onMouseOverElement(evt, x.key)}} 55 | onKeyDown={this.onKeyDown} 56 | onClick={this.props.onItemClicked}> 57 | {x.val} 58 | 59 | }) 60 | return ( 61 | 62 | {elems} 63 |
64 | ) 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | export default function register() { 12 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 13 | window.addEventListener('load', () => { 14 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 15 | navigator.serviceWorker 16 | .register(swUrl) 17 | .then(registration => { 18 | registration.onupdatefound = () => { 19 | const installingWorker = registration.installing; 20 | installingWorker.onstatechange = () => { 21 | if (installingWorker.state === 'installed') { 22 | if (navigator.serviceWorker.controller) { 23 | // At this point, the old content will have been purged and 24 | // the fresh content will have been added to the cache. 25 | // It's the perfect time to display a "New content is 26 | // available; please refresh." message in your web app. 27 | console.log('New content is available; please refresh.'); 28 | } else { 29 | // At this point, everything has been precached. 30 | // It's the perfect time to display a 31 | // "Content is cached for offline use." message. 32 | console.log('Content is cached for offline use.'); 33 | } 34 | } 35 | }; 36 | }; 37 | }) 38 | .catch(error => { 39 | console.error('Error during service worker registration:', error); 40 | }); 41 | }); 42 | } 43 | } 44 | 45 | export function unregister() { 46 | if ('serviceWorker' in navigator) { 47 | navigator.serviceWorker.ready.then(registration => { 48 | registration.unregister(); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/render/mPortRack.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Port} from './mPort' 3 | import portAddImage from '../resource/add_port.png' 4 | 5 | export class PortRack extends React.PureComponent { 6 | 7 | render() { 8 | var port_objs = this.props.ports 9 | var ports = [] 10 | var self = this 11 | var prtadd = null 12 | 13 | // emit all the ports 14 | port_objs.forEach((type_obj, port_id) => { 15 | var edit_target = null 16 | if (this.props.target) { 17 | edit_target = { 18 | def_id : this.props.target, 19 | port_id : port_id, 20 | is_arg : !self.props.is_sink 21 | } 22 | } 23 | ports.push( 24 | 33 | ) 34 | }) 35 | 36 | // add the "add port" button 37 | if (this.props.target) { 38 | prtadd = {"add { 45 | this.props.cbacks.onPortConfigClick({ 46 | def_id : this.props.target, 47 | is_sink : this.props.is_sink, 48 | mouse_evt : e 49 | }) 50 | }}/> 51 | } 52 | 53 | // hack: if there are no ports, the container div will collapse 54 | // the port-add button is position: absolute, so it isn't 55 | // included in the flow calculation. so we make a dummy element. 56 | return ( 57 |
58 | {ports} 59 | {prtadd} 60 |
61 |
62 | ) 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /src/resource/App.css: -------------------------------------------------------------------------------- 1 | html, body, #root, .App { 2 | height:100%; 3 | width: 100%; 4 | } 5 | 6 | .NodeView { 7 | text-align: left; 8 | background-color: #d0d0d0; 9 | width: 100%; 10 | height: 100%; 11 | user-select:none; 12 | border: 4px solid white; 13 | box-sizing:border-box; 14 | } 15 | 16 | .MNode .NodeView { 17 | height: 200px; 18 | width: 500px; 19 | cursor: default; 20 | } 21 | 22 | .MNode { 23 | display:inline-block; 24 | background-color:white; 25 | border-radius: 4px; 26 | position:absolute; 27 | cursor:grab; 28 | } 29 | 30 | .MNode:active { 31 | cursor:grabbing; 32 | z-index:1000; 33 | } 34 | 35 | img { 36 | vertical-align: bottom; 37 | } 38 | 39 | .CallName { 40 | background-color: white; 41 | color: black; 42 | padding: 4px; 43 | font-family: helvetica; 44 | font-weight: bold; 45 | font-size: 13px; 46 | text-align: center; 47 | } 48 | 49 | .FunctionNode > .CallName { 50 | border-radius: 4px 4px 0px 0px; 51 | } 52 | 53 | .Link * { 54 | stroke: #888888; 55 | fill: #888888; 56 | } 57 | 58 | .LinkLine { 59 | stroke-width: 3px; 60 | } 61 | 62 | .Link *:hover { 63 | stroke: #f75100; 64 | fill: #f75100; 65 | } 66 | 67 | .LinkLine.Partial { 68 | stroke-dasharray: 5,5; 69 | } 70 | 71 | .PortRack { 72 | padding: 0px; 73 | margin: 0px; 74 | width: 100%; 75 | text-align: center; 76 | } 77 | 78 | .Port { 79 | border-radius: 2px; 80 | padding: 2px; 81 | margin: 2px; 82 | cursor: default; 83 | } 84 | 85 | .PortConfig { 86 | padding: 2px; 87 | margin: 2px; 88 | cursor: default; 89 | position: absolute; 90 | right: 0%; 91 | } 92 | 93 | .Port:hover { 94 | background-color:#f75100; 95 | } 96 | 97 | .Port.Connected { 98 | background-color: #888888; 99 | border: 2px solid #888888; 100 | margin: 0px; 101 | } 102 | 103 | .OverlayDialog { 104 | position: absolute; 105 | top: 30%; 106 | left: 50%; 107 | transform: translate(-50%, 0); 108 | background-color: white; 109 | border: 3px solid #f75100; 110 | border-radius:3px; 111 | } 112 | 113 | .SelectionListElement.Selected > * { 114 | background-color: #f75100; 115 | color: white; 116 | width: 100%; 117 | } 118 | 119 | .SelectedGraphElement { 120 | outline: 3px dashed black; 121 | } 122 | 123 | .SelectedGraphElement:focus { 124 | outline: 3px dashed #f75100; 125 | } 126 | 127 | input { 128 | margin: 4px; 129 | padding: 3px; 130 | background-color: white; 131 | border: none; 132 | border-radius:3px; 133 | color:black; 134 | font-family: helvetica; 135 | font-size: 13px; 136 | } 137 | 138 | .LiteralNode > input { 139 | width: 6em; 140 | } 141 | 142 | .LiteralNode > code { 143 | color: white; 144 | } 145 | 146 | .Handle { 147 | background-color: #D51; 148 | border-radius: 3px 0px 0px 3px; 149 | width: 20px; 150 | height: 100%; 151 | display: inline-block; 152 | } 153 | 154 | .Console { 155 | overflow: scroll; 156 | max-height: 15%; 157 | } 158 | 159 | .ConsoleMessage { 160 | margin:0; 161 | } 162 | 163 | .ConsoleMessage.neutral { 164 | color: black; 165 | } 166 | 167 | .ConsoleMessage.error { 168 | color: red; 169 | } 170 | 171 | .ConsoleMessage.warn { 172 | color: orange; 173 | } 174 | 175 | .ConsoleMessage.ok { 176 | color: green; 177 | } 178 | -------------------------------------------------------------------------------- /src/render/mNode.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Draggable from 'react-draggable' 3 | import ReactDOM from 'react-dom' 4 | import {GraphElement} from '../state/GraphElement' 5 | import {NODE_TYPE} from '../state/Def' 6 | import {CallNodeBody, 7 | FunctionNodeBody, 8 | LiteralNodeBody, 9 | LoopNodeBody} from './NodeBody.js' 10 | 11 | // MNode is the React element for a language node. 12 | 13 | 14 | export class MNode extends React.PureComponent { 15 | 16 | 17 | constructor(props) { 18 | super(props); 19 | this.state = { 20 | selected : false, 21 | xy : [0,0] 22 | } 23 | this.elem = null 24 | } 25 | 26 | 27 | getGraphElement() { 28 | return new GraphElement("node", this.props.node_id) 29 | } 30 | 31 | 32 | onRef = (e) => { 33 | this.elem = e 34 | if (e) { 35 | this.props.cbacks.onElementMounted(this.getGraphElement(), this) 36 | } else { 37 | this.props.cbacks.onElementUnmounted(this.getGraphElement()) 38 | } 39 | } 40 | 41 | 42 | setSelected(selected) { 43 | this.setState({selected : selected}) 44 | } 45 | 46 | 47 | grabFocus() { 48 | if (this.elem) { 49 | var dom_e = ReactDOM.findDOMNode(this.elem) 50 | if (dom_e) dom_e.focus() 51 | } 52 | } 53 | 54 | 55 | onDrag = (e, position) => { 56 | this.setState({xy: [position.x, position.y]}) 57 | this.props.cbacks.onNodeMove(this.props.node_id, [position.x, position.y]) 58 | } 59 | 60 | 61 | onFocus = (e) => { 62 | // we have to check if the focus event is actually targeting us and not one of our children. 63 | // if the latter, we don't want to steal focus/selection from them. 64 | var self_dom = ReactDOM.findDOMNode(this.elem) 65 | if (e.target === self_dom) { 66 | this.props.cbacks.onElementFocused(this.getGraphElement()) 67 | } 68 | } 69 | 70 | 71 | render() { 72 | var body = null 73 | var t = this.props.def.node_type 74 | var subprops = { 75 | node : this.props.node, 76 | node_id : this.props.node_id, 77 | def : this.props.def, 78 | cbacks : this.props.cbacks, 79 | } 80 | if (t === NODE_TYPE.NODE_CALL || 81 | t === NODE_TYPE.NODE_BUILTIN) { 82 | body = 83 | } else if (t === NODE_TYPE.NODE_FUNCTION) { 84 | body = 85 | } else if (t === NODE_TYPE.NODE_LITERAL) { 86 | body = 87 | } else if (t === NODE_TYPE.NODE_EXPORT) { 88 | body = 89 | } else if (t === NODE_TYPE.NODE_LOOP) { 90 | body = 91 | } 92 | return ( 93 | 97 |
101 | {body} 102 |
103 |
104 | ); 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/render/mNarrowingList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {SelectionList} from './mSelectionList' 3 | 4 | 5 | 6 | export class NarrowingList extends React.Component { 7 | 8 | constructor(props) { 9 | super(props) 10 | this.state = { 11 | filterString : "", 12 | selectionKey : null, 13 | filteredList : this.makeFilteredList("", this.props.items), 14 | } 15 | this.inputElem = null 16 | } 17 | 18 | 19 | makeFilteredList(filterString, itemList) { 20 | var stringy = this.props.stringifier 21 | if (!stringy) { 22 | stringy = (v) => v 23 | } 24 | var filteredList = itemList.map((x,i) => ({key:i, val:stringy(x)})).toArray() 25 | if (filterString !== "") { 26 | filteredList = filteredList.filter(x => { 27 | return x.val.toLowerCase().includes(filterString.toLowerCase()) 28 | }) 29 | } 30 | return filteredList 31 | } 32 | 33 | 34 | componentDidMount() { 35 | document.addEventListener('keydown', this.onKeyDown); 36 | this.inputElem.focus() 37 | } 38 | 39 | 40 | componentWillUnmount() { 41 | document.removeEventListener('keydown', this.onKeyDown) 42 | } 43 | 44 | 45 | onInputChanged = (evt) => { 46 | var val = evt.target.value 47 | this.setState(prevState => { 48 | var s = { 49 | filterString : val, 50 | filteredList : this.makeFilteredList(val, this.props.items) 51 | } 52 | 53 | // has the current selction disappeared from underneath us? 54 | if (s.filteredList.findIndex(x => x.key === prevState.selectionKey) < 0) { 55 | if (s.filteredList.length > 0) { 56 | // the previous selection was just filtered out. 57 | // select whatever the first thing is. 58 | s.selectionKey = s.filteredList[0].key 59 | } else { 60 | // nothing in the list; select nothing 61 | s.selectionKey = null 62 | } 63 | } 64 | 65 | return s 66 | }) 67 | } 68 | 69 | 70 | onListSelectionChanged = (selectionKey) => { 71 | this.setState({selectionKey : selectionKey}) 72 | if (this.props.onListSelectionChanged) { 73 | this.props.onListSelectionChanged(selectionKey) 74 | } 75 | } 76 | 77 | 78 | accept = () => { 79 | if (this.state.selectionKey !== null) { 80 | this.props.onAccept(this.state.selectionKey) 81 | } else { 82 | // nothing selected 83 | this.props.onAccept(null) 84 | } 85 | } 86 | 87 | 88 | cancel = () => { 89 | this.props.onAccept(null) 90 | } 91 | 92 | 93 | onKeyDown = (evt) => { 94 | if (evt.key === 'Enter') { 95 | this.accept() 96 | } else if (evt.key === 'Escape') { 97 | // canceled by user 98 | this.cancel() 99 | } 100 | } 101 | 102 | 103 | render() { 104 | 105 | return ( 106 |
107 | {this.inputElem = elem}}/> 110 | 116 |
117 | ) 118 | } 119 | 120 | } -------------------------------------------------------------------------------- /src/render/mNodeView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {MNode} from './mNode'; 4 | import {Link} from './mLink'; 5 | import {NODE_TYPE} from '../state/Def' 6 | import {GraphElement} from '../state/GraphElement' 7 | 8 | 9 | // NodeView holds nodes and links, and renders them both. 10 | 11 | export class NodeView extends React.PureComponent { 12 | 13 | 14 | constructor(props) { 15 | super(props) 16 | this.state = this.getInitialState() 17 | this.elem = null 18 | this.dom = null 19 | } 20 | 21 | 22 | getInitialState() { 23 | return { 24 | corner_offs : this.props.position 25 | }; 26 | } 27 | 28 | 29 | componentDidMount() { 30 | if (this.elem) { 31 | this.dom = ReactDOM.findDOMNode(this.elem) 32 | } else { 33 | this.dom = null 34 | } 35 | } 36 | 37 | 38 | grabFocus() { 39 | if (this.dom) { 40 | this.dom.focus() 41 | } 42 | } 43 | 44 | 45 | onRef = (e) => { 46 | this.elem = e 47 | if (e) { 48 | this.props.cbacks.onElementMounted(this.getGraphElement(), this) 49 | } else { 50 | this.props.cbacks.onElementUnmounted(this.getGraphElement()) 51 | } 52 | } 53 | 54 | 55 | getGraphElement() { 56 | return new GraphElement("view", this.props.parent_id) 57 | } 58 | 59 | 60 | getCorner() { 61 | var thisDom = ReactDOM.findDOMNode(this.elem) 62 | if (!thisDom) { 63 | return [0,0] 64 | } else { 65 | var box = thisDom.getBoundingClientRect() 66 | var offs = [box.left, box.top] 67 | return offs 68 | } 69 | } 70 | 71 | 72 | render() { 73 | var links = []; 74 | var nodes = []; 75 | 76 | // emit the links which are our direct children 77 | for (var link_id of this.props.ng.child_links) { 78 | // make the link 79 | links.push(); 86 | } 87 | 88 | // emit the nodes which are our direct children 89 | for (var node_id of this.props.ng.child_nodes) { 90 | var n = this.props.ng.nodes.get(node_id) 91 | var def = this.props.ng.defs.get(n.def_id) 92 | if (def.node_type === NODE_TYPE.NODE_ENTRY || def.node_type === NODE_TYPE.NODE_EXIT) { 93 | // magic header / footer node. don't render. 94 | continue 95 | } 96 | var x = {} 97 | if (def.hasBody()) { 98 | // todo: this is regenerated on every render, which ain't great 99 | x.ng = this.props.ng.ofNode(node_id) 100 | } 101 | nodes.push() 107 | } 108 | 109 | return ( 110 |
{ 116 | if (e.target === this.dom) { 117 | this.props.cbacks.clearSelection() 118 | } 119 | }} 120 | ref={this.onRef}> 121 | {links} 122 | {nodes} 123 |
124 | ); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/state/EditProxy.js: -------------------------------------------------------------------------------- 1 | // EditProxy tattles on everything that's done to the node graph. 2 | // This is sent back to the server, so it can keep the AST in sync. 3 | 4 | export class EditProxy { 5 | 6 | constructor(app) { 7 | this.app = app 8 | this.connected = false 9 | this.queued = [] 10 | // could this be simplified with introspection? 11 | this.callback_args = { 12 | addNode : ["node_id", "def_id", "parent_id", "value"], 13 | removeNode : ["node_id"], 14 | addDef : ["def_id", "name", "node_type", "type_sig", "placeable"], 15 | removeDef : ["def_id"], 16 | addLink : ["link_id", "link"], 17 | removeLink : ["link_id"], 18 | addPort : ["def_id", "port_id", "type_id", "is_arg"], 19 | removePort : ["def_id", "port_id", "is_arg"], 20 | addType : ["type_id", "type_info"], 21 | removeType : ["type_id"], 22 | setLiteral : ["node_id", "value"], 23 | } 24 | 25 | new Promise((resolve, reject) => { 26 | var ws_prcol = (window.location.protocol === "https:") ? ('wss://') : ('ws://') 27 | var ws_address = ws_prcol + window.location.host + "/socket" 28 | this.socket = new WebSocket(ws_address) 29 | this.socket.onopen = (open) => { resolve() } 30 | this.socket.onmessage = this.routeMessage 31 | this.socket.onclose = (close) => { reject(close) } 32 | this.socket.onerror = (error) => { reject(error) } 33 | }).then(() => { 34 | this.connected = true 35 | this.app.setConnected(true) 36 | this.flush_queue() 37 | }).catch((reason) => { 38 | this.connected = false 39 | this.app.setConnected(false) 40 | console.log(reason) 41 | }) 42 | } 43 | 44 | 45 | routeMessage = (message) => { 46 | var json = JSON.parse(message.data) 47 | var data = json.data 48 | if (json.route === "graph_edit") { 49 | this.on_network_graph_edit(data) 50 | } else if (json.route === "message") { 51 | this.on_network_message(data) 52 | } 53 | } 54 | 55 | 56 | // take an action + object holding a keyword mapping of properties, 57 | // and use the action template to figure out the order of the args. 58 | unpackArgs(act, data) { 59 | var tp = this.callback_args[act] 60 | var args = [] 61 | for (var a of tp) { 62 | args.push(data[a]) 63 | } 64 | return args 65 | } 66 | 67 | 68 | // apply an add/remove event to the given nodegraph 69 | applyEvent(ng, evt) { 70 | // the name of the function to call 71 | // (e.g. addNode or removeLink): 72 | var act = evt.action + evt.type[0].toUpperCase() + evt.type.substr(1) 73 | var fn = ng[act] // get the thing with that name 74 | var args = this.unpackArgs(act, evt.details) 75 | var new_ng = fn.apply(ng, args) // call it 76 | 77 | return new_ng 78 | } 79 | 80 | 81 | on_network_message = (data) => { 82 | this.app.appendMessage(data.text + "\n", data.style) 83 | } 84 | 85 | 86 | // handle and execute an edit action arriving from the server. 87 | on_network_graph_edit = (data) => { 88 | this.app.setState(prevState => { 89 | var ng = this.applyEvent(prevState.ng, data) 90 | if (ng === prevState.ng) { 91 | return {} 92 | } else { 93 | return { ng : ng } 94 | } 95 | }) 96 | } 97 | 98 | 99 | // Request that an event be sent back to the server. 100 | // If the server is not connected, remember it for later. 101 | action(act, type, args) { 102 | let a = {action : act, type : type, details : args} 103 | console.log("send", a) 104 | if (this.connected) { 105 | this.socket.send(JSON.stringify(a)) 106 | } else { 107 | this.queued.push(a) 108 | } 109 | } 110 | 111 | 112 | // Empty out all the requested events that haven't been sent yet 113 | // and send them all to the server. 114 | flush_queue() { 115 | if (this.connected) { 116 | while (this.queued.length > 0) { 117 | var act = this.queued.pop() 118 | this.socket.send(JSON.stringify(act)) 119 | } 120 | } 121 | } 122 | 123 | } -------------------------------------------------------------------------------- /src/render/mPort.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import {GraphElement} from '../state/GraphElement' 4 | 5 | import float_img from '../resource/c_float.png' 6 | import int_img from '../resource/c_int.png' 7 | import bool_img from '../resource/c_bool.png' 8 | import type_img from '../resource/c_type.png' 9 | import proc_img from '../resource/c_proc.png' 10 | import list_img from '../resource/c_list.png' 11 | import str_img from '../resource/c_str.png' 12 | import obj_img from '../resource/c_obj.png' 13 | import unsolved_img from '../resource/c_unsolved.png' 14 | 15 | const typeIcons = { 16 | 'int32_t' : int_img, 17 | 'int64_t' : int_img, 18 | 'float32_t' : float_img, 19 | 'float64_t' : float_img, 20 | 'string_t' : str_img, 21 | 'bool_t' : bool_img, 22 | 'proc_t' : proc_img, 23 | 'array_t' : list_img, 24 | 'type_t' : type_img, 25 | 'struct_t' : obj_img, 26 | 'unsolved_t' : unsolved_img 27 | }; 28 | 29 | 30 | // Port is a connection point on a language node. 31 | // It renders a badge representing the type of the accepted connection. 32 | 33 | // The connection point may be anywhere within or on the edge of the DOM element, 34 | // as specified by the "direction" prop, which carries two coordinates on [0,1], 35 | // representing the fraction position of the connection point within the element. 36 | 37 | 38 | export class Port extends React.PureComponent { 39 | 40 | constructor(props) { 41 | super(props); 42 | this.state = { 43 | selected : false 44 | } 45 | this.elem = null; 46 | this.cxnpt = [0,0] 47 | } 48 | 49 | 50 | // get the connection point in "window" coordinates. 51 | getConnectionPoint() { 52 | if (this.elem === null) { 53 | return [0,0] 54 | } 55 | 56 | var eDom = ReactDOM.findDOMNode(this.elem); 57 | var box = eDom.getBoundingClientRect(); 58 | var xy = [box.width / 2., box.height / 2.] 59 | xy[0] += this.props.direction[0] * xy[0] + box.left; 60 | xy[1] += this.props.direction[1] * xy[1] + box.top; 61 | 62 | return xy; 63 | } 64 | 65 | 66 | getGraphElement() { 67 | return new GraphElement("port", { 68 | node_id : this.props.node_id, 69 | port_id : this.props.port_id, 70 | is_sink : this.props.is_sink 71 | }) 72 | } 73 | 74 | 75 | onRef = (e) => { 76 | this.elem = e 77 | if (e) { 78 | this.props.cbacks.onElementMounted(this.getGraphElement(), this) 79 | } else { 80 | this.props.cbacks.onElementUnmounted(this.getGraphElement()) 81 | } 82 | } 83 | 84 | 85 | setSelected(selected) { 86 | this.setState({selected : selected}) 87 | } 88 | 89 | 90 | grabFocus() { 91 | var eDom = ReactDOM.findDOMNode(this.elem); 92 | if (eDom) eDom.focus() 93 | } 94 | 95 | 96 | onMouseEnter = () => { 97 | if (this.props.cbacks.onPortHovered && this.props.edit_target) { 98 | this.props.cbacks.onPortHovered(this.props.edit_target); 99 | } 100 | } 101 | 102 | 103 | onMouseLeave = () => { 104 | if (this.props.cbacks.onPortHovered) { 105 | this.props.cbacks.onPortHovered(null); 106 | } 107 | } 108 | 109 | 110 | render() { 111 | var classes = [ 112 | "Port", 113 | this.props.is_sink ? "Sink" : "Source" 114 | ] 115 | if (this.props.connnected) { 116 | classes.push("Connected"); 117 | } 118 | if (this.state.selected) { 119 | classes.push("SelectedGraphElement") 120 | } 121 | return ( 122 | {this.props.port_id {this.props.cbacks.onElementFocused(this.getGraphElement())}} 131 | onMouseEnter={this.onMouseEnter} 132 | onMouseLeave={this.onMouseLeave} 133 | onClick={(evt) => { 134 | var e = {node_id : this.props.node_id, 135 | port_id : this.props.port_id, 136 | is_sink : this.props.is_sink, 137 | mouse_evt : evt} 138 | this.props.cbacks.onPortClicked(e); 139 | }} 140 | ref={this.onRef} /> 141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/state/NodeData.js: -------------------------------------------------------------------------------- 1 | import {Map, Set} from 'immutable' 2 | import * as _ from 'lodash' 3 | 4 | // NodeData is the state representation of a language node. 5 | // It is rendered by an MNode react element. 6 | 7 | // It carries the type signature and name, a mapping of port ID 8 | // to connected link ID, and a list of IDs for any inner nodes and links. 9 | 10 | // A NodeData is "immutable", in that mutation functions return a 11 | // new, altered copy, leaving the original unchanged. 12 | 13 | 14 | export class NodeData { 15 | 16 | 17 | constructor(def_id, parent=null, links_by_id=null) { 18 | this.def_id = def_id 19 | this.parent = parent 20 | this.source_links = new Map() 21 | this.sink_links = new Map() 22 | this.child_nodes = new Set() 23 | this.child_links = new Set() 24 | this.entry_id = null 25 | this.exit_id = null 26 | this.position = [0,0] 27 | this.value = null 28 | } 29 | 30 | 31 | // connect a link to one of the sources/sinks of this node. 32 | addLink(port_id, is_sink, link_id) { 33 | var n = _.clone(this) 34 | var update_thing = n.source_links 35 | if (is_sink) update_thing = n.sink_links 36 | update_thing = update_thing.update( 37 | port_id, 38 | new Set(), 39 | (s) => {return s.add(link_id)}) 40 | 41 | if (is_sink) n.sink_links = update_thing 42 | else n.source_links = update_thing 43 | 44 | return n 45 | } 46 | 47 | 48 | // add a node which is a direct child of this node; 49 | // i.e. add it to this node's "body". 50 | addChildNode(node_id) { 51 | var n = _.clone(this) 52 | n.child_nodes = this.child_nodes.add(node_id) 53 | return n 54 | } 55 | 56 | 57 | // add a new link between nodes which are direct children. 58 | addChildLink(link_id) { 59 | var n = _.clone(this) 60 | n.child_links = this.child_links.add(link_id) 61 | return n 62 | } 63 | 64 | 65 | // disconnect a link from a source/sink of this node. 66 | removeLink(port_id, is_sink, link_id) { 67 | var update_thing = this.source_links 68 | if (is_sink) update_thing = this.sink_links 69 | 70 | if (!update_thing.has(port_id) || !update_thing.get(port_id).includes(link_id)) { 71 | // object not here; nothing to do. 72 | return this 73 | } 74 | 75 | var n = _.clone(this); 76 | update_thing = update_thing.update(port_id, s => { return s.remove(link_id) }) 77 | if (is_sink) n.sink_links = update_thing 78 | else n.source_links = update_thing 79 | return n 80 | } 81 | 82 | 83 | // remove a node which is a direct child of this node. 84 | removeChildNode(node_id) { 85 | var n = _.clone(this) 86 | n.child_nodes = this.child_nodes.remove(node_id) 87 | return n 88 | } 89 | 90 | 91 | // remove some link which connects two child nodes. 92 | removeChildLink(link_id) { 93 | var n = _.clone(this) 94 | n.child_links = this.child_links.remove(link_id) 95 | return n 96 | } 97 | 98 | 99 | // move this node to the specified position (relative to its parent). 100 | setPosition(position) { 101 | var n = _.clone(this) 102 | n.position = position 103 | return n 104 | } 105 | 106 | 107 | setValue(v) { 108 | var n = _.clone(this) 109 | n.value = v 110 | return n 111 | } 112 | 113 | 114 | // return the set of links which connect the direct children of this node. 115 | getLinks(port_id, is_sink) { 116 | var getty_thing = this.source_links 117 | if (is_sink) getty_thing = this.sink_links 118 | if (getty_thing.has(port_id)) { 119 | return getty_thing.get(port_id) 120 | } else { 121 | return new Set() 122 | } 123 | } 124 | 125 | 126 | getAllLinks() { 127 | return ( 128 | this.source_links.entrySeq().map( 129 | ([port_id, linkset]) => { 130 | return { 131 | port_id : port_id, 132 | is_sink : false, 133 | cxns : linkset, 134 | } 135 | } 136 | ).concat( 137 | this.sink_links.entrySeq().map( 138 | ([port_id, linkset]) => { 139 | return { 140 | port_id : port_id, 141 | is_sink : true, 142 | cxns : linkset 143 | } 144 | }) 145 | ) 146 | ) 147 | } 148 | 149 | 150 | } 151 | -------------------------------------------------------------------------------- /src/render/mLink.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {GraphElement} from '../state/GraphElement' 4 | import * as _ from 'lodash' 5 | 6 | 7 | // Link is the React element for rendering a link between two node ports. 8 | // It knows the port ID and the (local) coordinates of its endpoints. 9 | 10 | 11 | export class Link extends React.PureComponent { 12 | 13 | constructor(props) { 14 | super(props) 15 | this.state = { 16 | src_endpt_selected : false, 17 | sink_endpt_selected : false, 18 | points : [[0,0],[0,0]] 19 | } 20 | this.sink_elem = null 21 | this.src_elem = null 22 | this.whole_elem = null 23 | } 24 | 25 | 26 | bounds() { 27 | let mins = [ Infinity, Infinity]; 28 | let maxs = [-Infinity, -Infinity]; 29 | for (var i = 0; i < this.state.points.length; ++i) { 30 | let p = this.state.points[i]; 31 | mins = [Math.min(p[0], mins[0]), Math.min(p[1], mins[1])]; 32 | maxs = [Math.max(p[0], maxs[0]), Math.max(p[1], maxs[1])]; 33 | } 34 | return {lo:mins, hi:maxs}; 35 | } 36 | 37 | 38 | makeLineElement(p0, p1, extraProps) { 39 | return 45 | } 46 | 47 | 48 | getGraphElement() { 49 | if (this.props.partial) { 50 | return new GraphElement("partial_link", this.props.anchor) 51 | } else { 52 | return new GraphElement("link", this.props.link_id) 53 | } 54 | } 55 | 56 | 57 | onRef = (e) => { 58 | this.whole_elem = e 59 | if (e) { 60 | this.props.onElementMounted(this.getGraphElement(), this) 61 | } else { 62 | this.props.onElementUnmounted(this.getGraphElement()) 63 | } 64 | } 65 | 66 | 67 | setSelected(selected, src_side=null) { 68 | if (src_side || src_side === null) { 69 | this.setState({src_endpt_selected : selected}) 70 | } 71 | if (!src_side) { 72 | this.setState({sink_endpt_selected : selected}) 73 | } 74 | } 75 | 76 | 77 | setEndpoint(xy, is_sink) { 78 | this.setState(prevState => { 79 | var s = _.clone(prevState.points) 80 | s[is_sink ? 1 : 0] = xy 81 | return {points : s} 82 | }) 83 | } 84 | 85 | 86 | grabFocus(src_side) { 87 | // var e = this.sink_elem 88 | // if (src_side) { 89 | // e = this.src_elem 90 | // } 91 | var e = this.whole_elem 92 | var eDom = ReactDOM.findDOMNode(e) 93 | if (eDom) eDom.focus() 94 | } 95 | 96 | 97 | onSourceEndpointClicked = (e) => { 98 | var evt = {mouseEvt : e, link_id: this.props.link_id, isSource:true} 99 | this.props.onLinkEndpointClicked(evt) 100 | } 101 | 102 | 103 | onSinkEndpointClicked = (e) => { 104 | var evt = {mouseEvt : e, link_id: this.props.link_id, isSource:false} 105 | this.props.onLinkEndpointClicked(evt) 106 | } 107 | 108 | 109 | // todo: make the dots generate endpoint events 110 | // todo: add invisible elements to expand the click area. 111 | 112 | 113 | makeLine(pts, partial=false) { 114 | let p0 = pts[0] 115 | let p2 = pts[1] 116 | let p1 = [(p0[0] + p2[0]) / 2, (p0[1] + p2[1]) / 2] 117 | 118 | // partial links must not be clickable. because they are drawn on top of nodes, 119 | // they would mask the port-connecting clicks. all other strokes must be clickable. 120 | var style = partial ? {} : {pointerEvents : 'visiblePainted'} 121 | if (partial) { 122 | return [this.makeLineElement(p0, p2, {key:"_seg_0", className:"LinkLine Partial"})]; 123 | } else { 124 | return [this.makeLineElement(p0, p1, 125 | {key:"_seg_0", 126 | className:"LinkLine", 127 | style:style, 128 | tabIndex:1, 129 | onFocus:(e => {this.props.onElementFocused(this.getGraphElement())}), 130 | ref:(e => {this.src_elem = e}), 131 | onClick:this.onSourceEndpointClicked}), 132 | 133 | this.makeLineElement(p1, p2, 134 | {key:"_seg_1", 135 | className:"LinkLine", 136 | style:style, 137 | tabIndex:1, 138 | onFocus:(e => {this.props.onElementFocused(this.getGraphElement())}), 139 | ref:(e => {this.sink_elem = e}), 140 | onClick:this.onSinkEndpointClicked})] 141 | } 142 | } 143 | 144 | 145 | render() { 146 | var rad = 5; 147 | var pad = Math.max(rad,3); 148 | var bnds = this.bounds(); 149 | var cbaks = {0 : this.onSourceEndpointClicked, 1 : this.onSinkEndpointClicked}; 150 | 151 | // put the points into the coordsys of this svg element. 152 | var xf_pts = this.state.points.map(p => { 153 | return [p[0] - bnds.lo[0] + pad, p[1] - bnds.lo[1] + pad]; 154 | }); 155 | 156 | // make the dots. 157 | var dots = [] 158 | for (var i = 0; i < xf_pts.length; ++i) { 159 | let p = xf_pts[i]; 160 | let dot = ; 166 | dots.push(dot); 167 | } 168 | 169 | var style = { 170 | position:"absolute", 171 | left: bnds.lo[0] - pad, 172 | top: bnds.lo[1] - pad, 173 | pointerEvents: 'none' // we don't want the svg's bounding rect to capture events. 174 | } 175 | 176 | var seld = this.state.src_endpt_selected || this.state.sink_endpt_selected 177 | 178 | // NOTE: TODO: when endpoint picking is ready, rm tabindex from below: 179 | return ( 180 | 186 | {this.makeLine(xf_pts, this.props.partial)} 187 | {dots} 188 | ); 189 | } 190 | 191 | } -------------------------------------------------------------------------------- /src/render/NodeBody.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Port} from './mPort' 3 | import {NodeView} from './mNodeView' 4 | import {PortRack} from './mPortRack' 5 | 6 | 7 | // todo: passing the type index around is stupid/heavy. 8 | // it should just be flat and live with the node sig, 9 | // which should also be flat and live with the node. 10 | // see notes/types.txt in miso. 11 | 12 | 13 | export class CallNodeBody extends React.PureComponent { 14 | 15 | render() { 16 | return ( 17 |
18 | {/* call inputs */} 19 | 25 | 26 | {/* name */} 27 |
{this.props.def.name}
28 | 29 | {/* call outputs */} 30 | 36 |
37 | ) 38 | } 39 | } 40 | 41 | 42 | export class FunctionNodeBody extends React.PureComponent { 43 | 44 | render() { 45 | if (!this.props.node.entry_id || !this.props.node.exit_id) { 46 | return (
) 47 | } 48 | var entry = this.props.ng.nodes.get(this.props.node.entry_id) 49 | var exit = this.props.ng.nodes.get(this.props.node.exit_id) 50 | return ( 51 |
52 | {/* function name */} 53 |
{this.props.def.name}
54 | {/* body entry */} 55 | 61 | 62 | {/* function body */} 63 | 67 | 68 | {/* body exit */} 69 | 75 | 76 | {/* definition output */} 77 | 82 |
83 | ) 84 | } 85 | 86 | } 87 | 88 | 89 | export class LoopNodeBody extends React.PureComponent { 90 | 91 | constructor(props) { 92 | super(props) 93 | this.exit_ports = null 94 | } 95 | 96 | 97 | render() { 98 | if (!this.props.node.entry_id || !this.props.node.exit_id) { 99 | return (
) 100 | } 101 | var entry = this.props.ng.nodes.get(this.props.node.entry_id) 102 | var exit = this.props.ng.nodes.get(this.props.node.exit_id) 103 | return ( 104 |
105 | {/* parent node entry */} 106 | 112 | {/* body entry */} 113 | 118 | {/* function body */} 119 | 123 | {/* body exit */} 124 | 129 | {/* parent node exit */} 130 | 136 |
137 | ) 138 | } 139 | 140 | } 141 | 142 | 143 | export class LiteralNodeBody extends React.PureComponent { 144 | 145 | constructor(props) { 146 | super(props) 147 | this.state = { 148 | value : this.props.node.value 149 | } 150 | } 151 | 152 | 153 | componentWillReceiveProps(nextProps) { 154 | this.setState({value : nextProps.node.value}) 155 | } 156 | 157 | 158 | commitValue = (v) => { 159 | this.props.cbacks.dispatchCommand( 160 | "set", 161 | "literal", 162 | { 163 | value : v, 164 | node_id : this.props.node_id, 165 | }) 166 | } 167 | 168 | 169 | render() { 170 | 171 | const TYPE_COLORS = { 172 | 'int32_t' : '#ffb005', 173 | 'int64_t' : '#ffb005', 174 | 'float32_t' : '#ff7a7a', 175 | 'float64_t' : '#ff7a7a', 176 | 'string_t' : '#86d61d', 177 | 'bool_t' : 'black', 178 | 'proc_t' : '#00bff3', 179 | 'array_t' : '#a342f5', 180 | 'type_t' : '#ff00ff', 181 | 'struct_t' : '#3a43f9', 182 | 'unsolved_t' : '#808080', 183 | }; 184 | 185 | var is_output = this.props.is_output 186 | // not sure if "clear selection" is the right behavior, but 187 | // we need /something/ for now so that backspace doesn't baleet everthang 188 | var sig = this.props.def.type_sig 189 | var ids = (is_output) ? (sig.getSinkIDs()) : (sig.getSourceIDs()) 190 | var port_id = ids.toSeq().first() 191 | var type_obj = (is_output ? sig.sink_types : sig.source_types).get(port_id) 192 | var val = this.state.value 193 | 194 | // todo: will need a general purpose way to render data 195 | if (val === true) { 196 | val = "true" 197 | } else if (val === false) { 198 | val = "false" 199 | } 200 | 201 | // xxx hack: does not override the right CSS element. 202 | // the background of the node is the parent div, 203 | // which we don't have control over. We should 204 | // probably refactor this; maybe use class inheritance 205 | // instead of composition for the different varieties of node. 206 | var s = { 207 | backgroundColor : TYPE_COLORS[type_obj.code], 208 | borderRadius : "4px", // <-- mirroring `App.css`. :( 209 | } 210 | 211 | var field = null 212 | if (is_output) { 213 | // "view" node: code outputs. 214 | field = {val} 215 | } else { 216 | // "literal" node: user inputs. 217 | field = {this.setState({value : e.target.value})}} 223 | onFocus = {e => {this.props.cbacks.clearSelection()}} 224 | onBlur = {e => {this.commitValue(e.target.value)}} 225 | onKeyPress = {e => { 226 | if (e.key === "Enter" || e.key === "Return") { 227 | this.commitValue(e.target.value) 228 | } 229 | }}/> 230 | } 231 | 232 | var port = 240 | 241 | var handle =
242 | var body = [] 243 | 244 | if (is_output) { 245 | body = [port, field, handle] 246 | } else { 247 | body = [handle, field, port] 248 | } 249 | 250 | return ( 251 |
252 | {body} 253 |
254 | ) 255 | } 256 | 257 | } 258 | -------------------------------------------------------------------------------- /src/state/NodeGraph.js: -------------------------------------------------------------------------------- 1 | import {Map, Set} from 'immutable' 2 | import * as _ from 'lodash' 3 | 4 | import {NodeData} from './NodeData' 5 | import {Def, NODE_TYPE} from './Def' 6 | import {TypeSignature} from './TypeSignature' 7 | 8 | 9 | // an "immutable" NodeGraph. 10 | // mutation functions return a new altered copy; leaving original unchanged. 11 | 12 | // todo: it would be good to make alterations using immutable.AsMutable for performance. 13 | // the mutable intermediates could be shared by mutators which call each other. 14 | 15 | export class NodeGraph { 16 | 17 | 18 | constructor() { 19 | this.nodes = new Map() 20 | this.links = new Map() 21 | this.defs = new Map() 22 | this.types = new Map() 23 | this.child_nodes = new Set() 24 | this.child_links = new Set() 25 | this.placeable_defs = new Set() 26 | } 27 | 28 | 29 | addNode(node_id, def_id, parent_id=null, value=null) { 30 | var ng = _.clone(this) 31 | var node = new NodeData(def_id, parent_id) 32 | if (value !== null && value !== undefined) { 33 | node.value = value 34 | } 35 | ng.nodes = this.nodes.set(node_id, node) 36 | 37 | if (parent_id === null || parent_id === undefined) { 38 | ng.child_nodes = this.child_nodes.add(node_id) 39 | } else { 40 | var parent_node = this.nodes.get(parent_id) 41 | parent_node = parent_node.addChildNode(node_id) 42 | 43 | // should the parent be updated to refer to a new entry/exit? 44 | // see footnote [1] if it's unclear why this happens here. 45 | var def = this.defs.get(def_id) 46 | if (def.node_type === NODE_TYPE.NODE_ENTRY) { 47 | parent_node.entry_id = node_id 48 | } else if (def.node_type === NODE_TYPE.NODE_EXIT) { 49 | parent_node.exit_id = node_id 50 | } 51 | 52 | ng.nodes = ng.nodes.set(parent_id, parent_node) 53 | } 54 | 55 | return ng 56 | } 57 | 58 | 59 | removeNode(node_id) { 60 | if (!this.nodes.has(node_id)) { return this } 61 | var ng = _.clone(this) 62 | var n = this.nodes.get(node_id) 63 | 64 | // remove all existing links 65 | for (var {cxns} of n.getAllLinks()) { 66 | for (var link_id of cxns.valueSeq()) { 67 | ng = ng.removeLink(link_id) 68 | } 69 | } 70 | 71 | // remove all child nodes 72 | for (var child_node_id of n.child_nodes.values()) { 73 | ng = ng.removeNode(child_node_id) 74 | } 75 | 76 | // remove all child_links, if any remaining 77 | n = ng.nodes.get(node_id) 78 | for (var child_link_id of n.child_links.values()) { 79 | ng = ng.removeLink(child_link_id) 80 | } 81 | 82 | // remove from parent node 83 | if (n.parent !== null) { 84 | var parent = ng.nodes.get(n.parent) 85 | parent = parent.removeChildNode(node_id) 86 | 87 | // if we're an entry/exit node, unlink us from our parent. 88 | // in principle the parent will be removed shortly, but 89 | // this is good hygiene, ensures perfect idempotency. 90 | var def = ng.defs.get(n.def_id) 91 | if (def.node_type === NODE_TYPE.NODE_ENTRY) { 92 | parent.entry_id = null 93 | } else if (def.node_type === NODE_TYPE.NODE_EXIT) { 94 | parent.exit_id = null 95 | } 96 | 97 | ng.nodes = ng.nodes.set(n.parent, parent) 98 | } else { 99 | ng.child_nodes = ng.child_nodes.remove(node_id) 100 | } 101 | 102 | ng.nodes = ng.nodes.remove(node_id) 103 | return ng 104 | } 105 | 106 | 107 | constructLink(port_0, port_1) { 108 | // return a Link object for two ports, 109 | // such that the source/sink are correctly identified. 110 | 111 | if (port_0.is_sink === port_1.is_sink) { 112 | // can't connect a src to a src or a sink to a sink. 113 | return null 114 | } 115 | 116 | // order the link source to sink. 117 | var new_link 118 | if (port_0.is_sink) { 119 | new_link = { 120 | sink : port_0, 121 | src : port_1 122 | } 123 | } else { 124 | new_link = { 125 | sink : port_1, 126 | src : port_0 127 | } 128 | } 129 | return new_link 130 | } 131 | 132 | 133 | addLink(link_id, link) { 134 | 135 | // invalid connection 136 | if (link === null) return this 137 | 138 | // get endpoint nodes 139 | var sink_id = link.sink.node_id 140 | var src_id = link.src.node_id 141 | var sink_node = this.nodes.get(sink_id) 142 | var src_node = this.nodes.get(src_id) 143 | var src_parent = src_node.parent 144 | var sink_parent = sink_node.parent 145 | 146 | // 'mutate' the nodes 147 | src_node = src_node.addLink(link.src.port_id, false, link_id) 148 | sink_node = sink_node.addLink(link.sink.port_id, true, link_id) 149 | 150 | // clobber the old node entries 151 | var ng = _.clone(this) 152 | ng.nodes = this.nodes.set(src_id, src_node) 153 | ng.nodes = ng.nodes.set(sink_id, sink_node) 154 | 155 | // add the link 156 | ng.links = this.links.set(link_id, link) 157 | 158 | // add the link to the parent 159 | var parent_id = src_parent 160 | if (sink_parent !== src_parent && sink_parent === src_id) { 161 | // in this case, the parent of the sink is the source. 162 | // the link should belong to the source (outer) node, 163 | // not the *parent* of the outer node. 164 | parent_id = sink_parent 165 | } 166 | if (parent_id === null) { 167 | ng.child_links = this.child_links.add(link_id) 168 | } else { 169 | var parent_node = this.nodes.get(parent_id) 170 | parent_node = parent_node.addChildLink(link_id) 171 | ng.nodes = ng.nodes.set(parent_id, parent_node) 172 | } 173 | 174 | return ng 175 | } 176 | 177 | 178 | removeLink(link_id) { 179 | var link = this.links.get(link_id) 180 | var src_node = this.nodes.get(link.src.node_id) 181 | var sink_node = this.nodes.get(link.sink.node_id) 182 | src_node = src_node.removeLink(link.src.port_id, false, link_id) 183 | sink_node = sink_node.removeLink(link.sink.port_id, true, link_id) 184 | 185 | var ng = _.clone(this) 186 | ng.links = this.links.remove(link_id) 187 | ng.nodes = this.nodes.set(link.src.node_id, src_node).set(link.sink.node_id, sink_node) 188 | 189 | // remove link from its parent 190 | var parent_id = src_node.parent 191 | if (parent_id === null || parent_id === undefined) { 192 | ng.child_links = this.child_links.remove(link_id) 193 | } else { 194 | var parent_node = this.nodes.get(parent_id) 195 | parent_node = parent_node.removeChildLink(link_id) 196 | ng.nodes = ng.nodes.set(parent_id, parent_node) 197 | } 198 | 199 | return ng 200 | } 201 | 202 | 203 | addPort(def_id, port_id, type_id, is_sink) { 204 | var def = this.defs.get(def_id) 205 | def = def.addPort(port_id, type_id, is_sink) 206 | 207 | var ng = _.clone(this) 208 | ng.defs = ng.defs.set(def_id, def) 209 | 210 | return ng 211 | } 212 | 213 | 214 | removePort(def_id, port_id) { 215 | var ng = _.clone(this) 216 | var def = ng.defs.get(def_id) 217 | 218 | def = def.removePort(port_id) 219 | ng.defs = ng.defs.set(def_id, def) 220 | 221 | return ng 222 | } 223 | 224 | 225 | addDef(def_id, name, node_type, sig, placeable) { 226 | var ng = _.clone(this) 227 | if (!(sig instanceof TypeSignature)) { 228 | sig = new TypeSignature(sig.sink_types, sig.source_types) 229 | } 230 | ng.defs = this.defs.set(def_id, new Def(name, node_type, sig)) 231 | 232 | if (placeable) { 233 | ng.placeable_defs = ng.placeable_defs.add(def_id) 234 | } 235 | 236 | return ng 237 | } 238 | 239 | 240 | removeDef(name, def_id) { 241 | // this is a bit sketchy. what happens when we remove a def 242 | // referred to by many functions? we'll provide a fn for this, 243 | // but should maybe not use it for now. 244 | var ng = _.clone(this) 245 | if (!this.defs.has(def_id)) return this 246 | ng.defs = this.defs.remove(def_id) 247 | // todo: redirect any functions pointing here? 248 | return ng 249 | } 250 | 251 | 252 | addType(type_id, type_info) { 253 | var ng = _.clone(this) 254 | ng.types = ng.types.set(type_id, type_info) 255 | return ng 256 | } 257 | 258 | 259 | removeType(type_id) { 260 | var ng = _.clone(this) 261 | ng.types = ng.types.remove(type_id) 262 | return ng 263 | } 264 | 265 | 266 | setLiteral(node_id, value) { 267 | var ng = _.clone(this) 268 | var node = ng.nodes.get(node_id) 269 | ng.nodes = ng.nodes.set(node_id, node.setValue(value)) 270 | return ng 271 | } 272 | 273 | 274 | setPosition(node_id, coords) { 275 | var n = this.nodes.get(node_id) 276 | n = n.setPosition(coords) 277 | var ng = _.clone(this) 278 | ng.nodes = ng.nodes.set(node_id, n) 279 | return ng 280 | } 281 | 282 | 283 | ofNode(node_id) { 284 | // xxx todo: this introduces a performance penalty, since 285 | // this is recreated on each "frame" D: 286 | if (!this.nodes.has(node_id)) { 287 | return this 288 | } 289 | var ng = _.clone(this) 290 | var n = this.nodes.get(node_id) 291 | ng.child_nodes = n.child_nodes 292 | ng.child_links = n.child_links 293 | return ng 294 | } 295 | 296 | 297 | } 298 | 299 | 300 | // footnote [1]: 301 | 302 | // it might seem weird that we individually add the entry and exit nodes for 303 | // all nodes with bodies here. why not automatically add them the moment they are 304 | // created, you ask? we don't ever want those nodes to be without entry/exits. 305 | 306 | // we do it this way because the server/client have to sync state, and so have 307 | // to agree on the identifiers for things. this means the EditProxy has to be 308 | // aware of every "node creation" act that happens, so it can tattle to the server 309 | // about it (or so that we can hear from the server what the names of the header/footer 310 | // nodes are). 311 | 312 | // the alternative would be to name the header/footers in predictable ways, so that 313 | // we don't have to exchange information about them. this could be asking for trouble, 314 | // though, as we don't want name collisions to ever be possible. ensuring that could 315 | // become subtle/difficult given that nodes can be added/deleted in any order, and this 316 | // would also impose rules on every component that might generate an identifier (or 317 | // we are back to the same problem of centralizing node creation). 318 | 319 | -------------------------------------------------------------------------------- /src/state/SelectionModel.js: -------------------------------------------------------------------------------- 1 | import {OrderedMap} from 'immutable' 2 | import * as _ from 'lodash' 3 | import {GraphElement} from './GraphElement' 4 | import {NODE_TYPE} from './Def' 5 | 6 | 7 | // xxx: todo: have to remove elements from selection when they're removed from the NG. 8 | // 9 | // dis gon b bad when we remove a port from a common def, i.e. for a call. would actually be better if 10 | // we got an event for every node that changed instead of for shared defs. that way we 11 | // could check if there's such a thing in the selection. it's also annoying that I have to go look up the 12 | // def elsewhere whenever I need to know the signature. overall this turned out to be an annoying design change. 13 | 14 | // todo: need a way to navigate to stuff inside a function node. 15 | 16 | // todo: a notion of "current parent"; when you descend inside, everything of the outer parent 17 | // gets de-selected. sweep-selecting should not pick things across parents. 18 | 19 | // todo: is this a dumb class? should we just tell each element how to transfer its own focus? 20 | // this class is has its tendrils in a lot of code. 21 | // > yes, to a certain extent. this would be better: 22 | // each element should have a getElement(direction) funciton. 23 | // > however, each element may then have to have "global" information about its 24 | // neighbors. :| 25 | 26 | // todo: when nodes have "vertical" form, will have to switch neighbor logic. 27 | 28 | function positive_mod(a,b) { 29 | return (a % b + b) % b 30 | } 31 | 32 | 33 | export class SelectionModel { 34 | 35 | constructor(app) { 36 | this.selected_elements = new OrderedMap() 37 | this.app = app 38 | this.edge = null 39 | this.listeners = [] 40 | this.shift = false 41 | } 42 | 43 | 44 | _notifyEdgeChange(new_edge) { 45 | for (let l of this.listeners) { 46 | l.onSelectionEdgeChange(new_edge) 47 | } 48 | } 49 | 50 | 51 | clear_selection = () => { 52 | if (this.selected_elements.size > 0) { 53 | for (let [k,e] of this.selected_elements) { 54 | var obj = this.app.getElement(e) 55 | obj.setSelected(false) 56 | } 57 | this.selected_elements = new OrderedMap() 58 | this.edge = null 59 | this._notifyEdgeChange(null) 60 | } 61 | } 62 | 63 | 64 | unselect = (elem) => { 65 | var key = elem.key() 66 | if (this.selected_elements.has(key)) { 67 | if (this.edge.key() === key) { 68 | this.retract_selection() 69 | } else { 70 | var obj = this.app.getElement(elem) 71 | obj.setSelected(false) 72 | this.selected_elements = this.selected_elements.remove(key) 73 | } 74 | } 75 | } 76 | 77 | 78 | set_selection = (elem) => { 79 | var found = false 80 | for (let [k,e] of this.selected_elements) { 81 | if (!_.isEqual(e, elem)) { 82 | var o = this.app.getElement(e) 83 | if (o === undefined) { 84 | // xxx fixme. as elememts are deleted, they are not removed from selection 85 | console.log("DEBUG: Object not found:", e) 86 | } else { 87 | o.setSelected(false) 88 | } 89 | } else { 90 | // don't de-select the already-selected elem 91 | found = true 92 | } 93 | } 94 | 95 | // make this the only elem in the set of selected objs 96 | var j = {} 97 | j[elem.key()] = elem 98 | this.selected_elements = new OrderedMap(j) 99 | 100 | // select the object 101 | var obj = this.app.getElement(elem) 102 | obj.setSelected(true) 103 | 104 | // make it the edge if it wasn't already 105 | if (!_.isEqual(this.edge, elem)) { 106 | obj.grabFocus() 107 | this.edge = elem 108 | this._notifyEdgeChange(this.edge) 109 | } 110 | } 111 | 112 | 113 | extend_selection = (elem) => { 114 | if (!_.isEqual(elem, this.edge)) { 115 | var obj = this.app.getElement(elem) 116 | this.selected_elements = this.selected_elements.set(elem.key(), elem) 117 | obj.setSelected(true) 118 | obj.grabFocus() 119 | this.edge = elem 120 | this._notifyEdgeChange(this.edge) 121 | } 122 | } 123 | 124 | 125 | retract_selection = () => { 126 | if (this.selected_elements.size > 0) { 127 | var obj = this.app.getElement(this.edge) 128 | // remove last element: 129 | this.selected_elements = this.selected_elements.remove(this.selected_elements.keySeq().last()) 130 | obj.setSelected(false) 131 | if (this.selected_elements.size > 0) { 132 | this.edge = this.selected_elements.last() 133 | this.app.getElement(this.edge).grabFocus() 134 | } else { 135 | this.edge = null 136 | } 137 | this._notifyEdgeChange(this.edge) 138 | } 139 | } 140 | 141 | 142 | vertical_neighbor = (elem, up) => { 143 | if (elem.type === "port") { 144 | var inward = elem.id.is_sink ^ up 145 | if (inward) { 146 | // the element "inward" of a port is the node it belongs to 147 | let node = this.app.state.ng.nodes.get(elem.id.node_id) 148 | let def = this.app.state.ng.defs.get(node.def_id) 149 | if (def.node_type === NODE_TYPE.NODE_ENTRY || def.node_type === NODE_TYPE.NODE_EXIT) { 150 | // node is not an individual thing that can be selected :\ 151 | return null 152 | } else { 153 | return new GraphElement("node", elem.id.node_id) 154 | } 155 | } else { 156 | // the element "outward" of a port is the first link connected to it 157 | let node = this.app.state.ng.nodes.get(elem.id.node_id) 158 | var connected_links = node.getLinks(elem.id.port_id, elem.id.is_sink) 159 | if (connected_links.size > 0) 160 | // todo: order the links by their visual left-to-right ordering. 161 | return new GraphElement("link", connected_links.first()) 162 | else { 163 | // no links connected to this port 164 | return null 165 | } 166 | } 167 | } else if (elem.type === "link") { 168 | // the element above/below a link is the port it's connected to 169 | var link = this.app.state.ng.links.get(elem.id) 170 | var {node_id, port_id} = up ? link.src : link.sink 171 | return new GraphElement("port", { 172 | node_id : node_id, 173 | port_id : port_id, 174 | is_sink : !up, 175 | }) 176 | } else if (elem.type === "node") { 177 | // the element above/below a node is its first input/output port 178 | let node = this.app.state.ng.nodes.get(elem.id) 179 | var sig = this.app.state.ng.defs.get(node.def_id).type_sig 180 | var ports = up ? sig.getSinkIDs() : sig.getSourceIDs() 181 | if (ports.size > 0) { 182 | return new GraphElement("port", { 183 | node_id : elem.id, 184 | port_id : ports.first(), 185 | is_sink : up, 186 | }) 187 | } else { 188 | // no ports on this edge of the node 189 | return null 190 | } 191 | } 192 | } 193 | 194 | 195 | horizontal_neighbor = (elem, left) => { 196 | if (elem.type === "port") { 197 | // the adjacent port is one on the same edge of the node, to the left or right. 198 | let node = this.app.state.ng.nodes.get(elem.id.node_id) 199 | var sig = this.app.state.ng.defs.get(node.def_id).type_sig 200 | var ports = (elem.id.is_sink ? sig.getSinkIDs() : sig.getSourceIDs()).toIndexedSeq() 201 | var p_idx = ports.indexOf(elem.id.port_id) + (left ? -1 : 1) 202 | if (p_idx < 0 || p_idx >= ports.size) { 203 | return null 204 | } else { 205 | return new GraphElement("port", { 206 | node_id : elem.id.node_id, 207 | port_id : ports.get(p_idx), 208 | is_sink : elem.id.is_sink 209 | }) 210 | } 211 | } else if (elem.type === "link") { 212 | // todo: when half-edges are selectable, find the neighbords on this port 213 | // for sources, and find the adjacent ports' links on sinks. 214 | // todo: links should be ordered by their visual ordering on screen 215 | var link = this.app.state.ng.links.get(elem.id) 216 | let node = this.app.state.ng.nodes.get(link.src.node_id) 217 | var links = node.getLinks(link.src.port_id, false) 218 | var lseq = links.toList() 219 | var idx = lseq.indexOf(elem.id) + (left ? -1 : 1) 220 | idx = positive_mod(idx, lseq.size) 221 | return new GraphElement("link", lseq.get(idx)) 222 | } else if (elem.type === "node") { 223 | return null 224 | } 225 | } 226 | 227 | 228 | neighbor = (elem, direction) => { 229 | if (direction === "up") { 230 | return this.vertical_neighbor(elem, true) 231 | } else if (direction === "down") { 232 | return this.vertical_neighbor(elem, false) 233 | } else if (direction === "left") { 234 | return this.horizontal_neighbor(elem, true) 235 | } else if (direction === "right") { 236 | return this.horizontal_neighbor(elem, false) 237 | } else { 238 | return null 239 | } 240 | } 241 | 242 | 243 | walk = (direction) => { 244 | if (this.edge !== null) { 245 | var new_guy = this.neighbor(this.edge, direction) 246 | if (new_guy !== null) { 247 | this.set_selection(new_guy) 248 | } 249 | } 250 | } 251 | 252 | 253 | gather = (direction) => { 254 | if (this.edge !== null) { 255 | var new_guy = this.neighbor(this.edge, direction) 256 | if (new_guy !== null) { 257 | this.extend_selection(new_guy) 258 | } 259 | } 260 | } 261 | 262 | 263 | onElementFocused = (elem) => { 264 | if (this.shift) { 265 | this.extend_selection(elem) 266 | } else { 267 | this.set_selection(elem) 268 | } 269 | } 270 | 271 | 272 | onKeyDown = (evt) => { 273 | if (evt.key === "Shift") { 274 | this.shift = true 275 | } 276 | if (evt.key === "ArrowUp" || evt.key === "ArrowDown" || evt.key === "ArrowLeft" || evt.key === "ArrowRight") { 277 | // graph walking shortcuts 278 | if (this.edge !== null) { 279 | var direction = evt.key.replace("Arrow", "").toLowerCase() 280 | if (evt.shiftKey) { 281 | this.gather(direction) 282 | } else { 283 | this.walk(direction) 284 | } 285 | evt.preventDefault() 286 | } 287 | } 288 | } 289 | 290 | 291 | onKeyUp = (evt) => { 292 | if (evt.key === "Shift") { 293 | this.shift = false 294 | } 295 | } 296 | 297 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import * as _ from 'lodash' 3 | import {Map, List} from 'immutable' 4 | import ReactDOM from 'react-dom' 5 | import crypto from 'crypto' 6 | 7 | import {NodeGraph} from './state/NodeGraph' 8 | import {EditProxy} from './state/EditProxy' 9 | import {GraphElement} from './state/GraphElement' 10 | import {SelectionModel} from './state/SelectionModel' 11 | 12 | import {NodeView} from './render/mNodeView' 13 | import {Link} from './render/mLink' 14 | import {NarrowingList} from './render/mNarrowingList' 15 | import {Console} from './render/mConsole' 16 | 17 | import './resource/App.css' 18 | 19 | 20 | // todo: unify the two kinds of elements-- those used by the network and 21 | // those used by the focus model. it is dumb that we repeat ourselves in 22 | // this way. 23 | // todo: it is also better to store the signature with every node. 24 | // internal handling will be simpler because everything is right 25 | // there. it will also be easier to remove things from the 26 | // selection when they are deleted, and delete things that are 27 | // selected by just directly passing the element to the EditProxy. 28 | // while refactoring this, it would be great if things were more directly 29 | // translateable with the backed AST/module elements. (if we do away with 30 | // the def thing, that might help with this too). 31 | 32 | // todo: use asMutable to speed up some of the edits. 33 | // todo: make nodeviews resize themselves to contain their nodes. 34 | // todo: both nodeviews and ports must not drag the parent node 35 | // todo: function def ports and names should show up on the same line. 36 | // todo: link does not follow beyond the edge of the nodeview 37 | // and this is bad for interaction 38 | // todo: nodes must accept initial position 39 | // todo: the above also makes it impossible to connect function args to 40 | // function body. we should in general allow nodes to connect across 41 | // nesting levels. 42 | // todo: should we override and re-implement or take advantage of browser 43 | // native focus traversal? 44 | // todo: performance fix: only update port positions & node positions on drag 45 | // stop. find some other way (pass a callback, edit DOM directly?) to get 46 | // the links to track. 47 | 48 | 49 | // someday: draw the type at the free end of the temporary link. 50 | 51 | 52 | class App extends React.Component { 53 | 54 | constructor(props) { 55 | super(props); 56 | this.state = this.getDefaultState(); 57 | this.editProxy = new EditProxy(this) 58 | this.elements = new Map() 59 | this.selection = new SelectionModel(this) 60 | this.console = null 61 | 62 | // neccesary to store across frames, or else the entire app will 63 | // redraw on every frame D: 64 | this.mutation_callbacks = { 65 | onLinkDisconnected : this.onLinkDisconnected, 66 | onPortClicked : this.onPortClicked, 67 | onPortHovered : this.onPortHovered, 68 | onLinkEndpointClicked : this.onLinkEndpointClicked, 69 | onNodeMove : this.onNodeMove, 70 | onPortConfigClick : this.onPortConfigClick, 71 | onElementMounted : this.onElementMounted, 72 | onElementUnmounted : this.onElementUnmounted, 73 | onElementFocused : this.selection.onElementFocused, 74 | clearSelection : this.selection.clear_selection, 75 | dispatchCommand : this.dispatchCommand, 76 | appendMessage : this.appendMessage, 77 | } 78 | } 79 | 80 | 81 | /****** Setup / init ******/ 82 | 83 | 84 | getDefaultState() { 85 | return { 86 | ng : new NodeGraph(), 87 | 88 | partial_link_anchor : null, 89 | partial_link_endpt : [0,0], 90 | 91 | active_node_dialog : null, 92 | active_port_dialog : null, 93 | 94 | connected : false, 95 | 96 | port_hovered : null, 97 | } 98 | } 99 | 100 | 101 | setPosition = (node_id, pos) => { 102 | this.setState(prevState => { 103 | return { ng : prevState.ng.setPosition(node_id, pos) } 104 | }) 105 | } 106 | 107 | 108 | getElement = (elem) => { 109 | var elkey = elem.key() 110 | return this.elements.get(elkey) 111 | } 112 | 113 | 114 | componentDidMount() { 115 | if (this.elem !== null) { 116 | this.elem.focus() 117 | } 118 | } 119 | 120 | 121 | /****** Update functions ******/ 122 | 123 | 124 | setConnected = (connected) => { 125 | this.setState({connected : connected}) 126 | } 127 | 128 | 129 | setPartialLinkEndpoint = (xy) => { 130 | // move the endpoint of the partial link to the specified position, 131 | // specified in the coordinates of the top-level nodeview. 132 | if (!_.isEqual(this.state.partial_link_endpt)) { 133 | this.setState({partial_link_endpt : xy}) 134 | if (this.state.partial_link_anchor) { 135 | var ge = new GraphElement("partial_link", this.state.partial_link_anchor) 136 | var link_el = this.getElement(ge) 137 | link_el.setEndpoint(xy, true) 138 | } 139 | } 140 | } 141 | 142 | 143 | updateMouse = (x, y) => { 144 | // the partial link tracks the mouse. update its position. 145 | var eDom = ReactDOM.findDOMNode(this.elem); 146 | var box = eDom.getBoundingClientRect() 147 | var new_pos = [x - box.left, y - box.top] 148 | this.setPartialLinkEndpoint(new_pos) 149 | } 150 | 151 | 152 | // parent_corner in local (top-level nodeview) coordinates 153 | updateConnectedPortLinks(node_id, port_id, is_sink, parent_corner=null) { 154 | // update the endpoint positions of all the links connected to the given port. 155 | var node = this.state.ng.nodes.get(node_id) 156 | 157 | if (!parent_corner) { 158 | var parent_view = this.getElement(new GraphElement("view", node.parent)) 159 | parent_corner = parent_view.getCorner() 160 | } 161 | 162 | var partial_link = this.state.partial_link_anchor 163 | var pe = new GraphElement("port", {node_id, port_id, is_sink}) 164 | var port = this.getElement(pe) 165 | if (!port) return 166 | var links = node.getLinks(port_id, is_sink) 167 | var xy = port.getConnectionPoint() 168 | var uv = parent_corner 169 | 170 | for (let link_id of links) { 171 | let link_elem = this.getElement(new GraphElement("link", link_id)) 172 | // set the endpoint in the coordinate space of the object it belongs to 173 | link_elem.setEndpoint([xy[0] - uv[0], xy[1] - uv[1]], is_sink) 174 | } 175 | 176 | // is the partial link connected to this port? 177 | if (partial_link && 178 | partial_link.node_id === node_id && 179 | partial_link.port_id === port_id) { 180 | let ge = new GraphElement("partial_link", this.state.partial_link_anchor) 181 | let partial_elem = this.getElement(ge) 182 | // coordinate space is OUR coordinate space. 183 | var master_view = this.getElement(new GraphElement("view", null)) 184 | var st = master_view.getCorner() 185 | partial_elem.setEndpoint([xy[0] - st[0], xy[1] - st[1]], false) 186 | } 187 | } 188 | 189 | 190 | updateConnectedNodeLinks(node_id) { 191 | // update the endpoint positions of all the links connected to the given node. 192 | var node = this.state.ng.nodes.get(node_id) 193 | var sig = this.state.ng.defs.get(node.def_id).type_sig 194 | var parent_view = this.getElement(new GraphElement("view", node.parent)) 195 | var c = parent_view.getCorner() 196 | for (let {port_id, is_sink} of sig.getAllPorts()) { 197 | this.updateConnectedPortLinks(node_id, port_id, is_sink, c) 198 | } 199 | } 200 | 201 | 202 | /****** Callback functions ******/ 203 | 204 | 205 | onElementMounted = (elem, component) => { 206 | // some child graph element has just created a DOM node for itself. 207 | // keep track of it (the element, not the DOM node) because 208 | // we frequently need to go find them to tell them to do stuff. 209 | var elkey = elem.key() 210 | this.elements = this.elements.set(elkey, component) 211 | 212 | // we do a lot of updating to ensure 213 | // the links stay connected to the ports :| 214 | if (elem.type === "link") { 215 | var link = this.state.ng.links.get(elem.id) 216 | this.updateConnectedPortLinks(link.src.node_id, link.src.port_id, false) 217 | this.updateConnectedPortLinks(link.sink.node_id, link.sink.port_id, true) 218 | } else if (elem.type === "port") { 219 | this.updateConnectedPortLinks(elem.id.node_id, elem.id.port_id, elem.id.is_sink) 220 | } else if (elem.type === "partial_link") { 221 | this.setPartialLinkEndpoint(this.state.partial_link_endpt) 222 | this.updateConnectedPortLinks(elem.id.node_id, elem.id.port_id, elem.id.is_sink) 223 | } 224 | } 225 | 226 | 227 | onElementUnmounted = (elem) => { 228 | var elkey = elem.key() 229 | this.selection.unselect(elem) 230 | this.elements = this.elements.remove(elkey) 231 | } 232 | 233 | 234 | onPortClicked = ({node_id, port_id, is_sink, mouse_evt}) => { 235 | // either create or complete the "partial" link 236 | var p = {node_id, port_id, is_sink} 237 | if (this.state.partial_link_anchor === null) { 238 | this.setState({partial_link_anchor : {node_id, port_id, is_sink}}) 239 | } else { 240 | // complete a partial link 241 | var anchor_port = this.state.partial_link_anchor 242 | this.setState(prevState => ({ 243 | partial_link_anchor : null 244 | })) 245 | var link = this.state.ng.constructLink(anchor_port, p) 246 | if (link !== null) { // null implies src -> src or sink -> sink 247 | this.editProxy.action("add", "link", {link}) 248 | } 249 | } 250 | } 251 | 252 | 253 | dispatchCommand = (action, type, details) => { 254 | this.editProxy.action(action, type, details) 255 | } 256 | 257 | 258 | onKeyDown = (evt) => { 259 | // todo: make a user-configurable map of these 260 | if (evt.key === " " && this.state.active_node_dialog === null) { 261 | this.setState({active_node_dialog : { 262 | selected_elem : this.selection.edge 263 | }}) 264 | evt.preventDefault() 265 | } else if ((evt.key === 'Delete' || evt.key === 'Backspace')) { 266 | // port deletion 267 | if (this.state.port_hovered !== null) { 268 | this.editProxy.action("remove", "port", this.state.port_hovered) 269 | } else if (this.selection.selected_elements.size > 0) { 270 | // todo: push these to a composite action 271 | // todo: make editProxy accept the element type 272 | var sel = this.selection.selected_elements.valueSeq() 273 | var link_elems = sel.filter(e => (e.type === "link")) 274 | var port_elems = sel.filter(e => (e.type === "port")) 275 | var node_elems = sel.filter(e => (e.type === "node")) 276 | 277 | // delete links 278 | for (let e of link_elems) { 279 | this.editProxy.action("remove", "link", {link_id:e.id}) 280 | } 281 | 282 | // TODO: handle ports after we do the def/signature refactoring. 283 | 284 | // delete nodes 285 | for (let e of node_elems) { 286 | this.editProxy.action("remove", "node", {node_id:e.id}) 287 | } 288 | } 289 | } else { 290 | this.selection.onKeyDown(evt) 291 | } 292 | } 293 | 294 | 295 | onKeyUp = (evt) => { 296 | this.selection.onKeyUp(evt) 297 | } 298 | 299 | 300 | onLinkDisconnected = (link_id) => { 301 | this.editProxy.action("remove", "link", {link_id}) 302 | } 303 | 304 | 305 | onPortHovered = (target) => { 306 | this.setState({ port_hovered: target}); 307 | } 308 | 309 | 310 | onNodeMove = (node_id, new_pos) => { 311 | // all the endpoints of the connected links need to be updated 312 | // to match the node's new position. 313 | this.updateConnectedNodeLinks(node_id) 314 | } 315 | 316 | 317 | onMouseMove = (evt) => { 318 | // currently used to make the "partial link" track the cursor. 319 | if (this.state.partial_link_anchor !== null) { 320 | this.updateMouse(evt.clientX, evt.clientY) 321 | } 322 | } 323 | 324 | 325 | onClick = (evt) => { 326 | // if the user clicks on the empty space outside of a node, 327 | // let go of the partial link. 328 | this.updateMouse(evt.clientX, evt.clientY) 329 | if (this.state.partial_link_anchor !== null) { 330 | this.setState({partial_link_anchor : null}); 331 | } 332 | } 333 | 334 | 335 | onPortConfigClick = ({def_id, is_sink}) => { 336 | // bring up the type selection dialog for adding a port 337 | this.setState({active_port_dialog : { 338 | def_id : def_id, 339 | is_arg : !is_sink}}) 340 | } 341 | 342 | 343 | onLinkEndpointClicked = ({mouseEvt, link_id, isSource}) => { 344 | // if the user grabs the endpoint of a link, "pick it up" 345 | // by disconnecting the grabbed link and creating a partial link. 346 | var link = this.state.ng.links.get(link_id) 347 | // we want to keep the endpoint that *wasn't* grabbed 348 | var port = isSource ? link.sink : link.src 349 | 350 | console.assert(this.state.partial_link_anchor === null); 351 | 352 | this.setState({ 353 | partial_link_anchor : { 354 | node_id : port.node_id, 355 | port_id : port.port_id, 356 | is_sink : isSource, 357 | } 358 | }) 359 | 360 | this.editProxy.action("remove", "link", {link_id}) 361 | } 362 | 363 | 364 | onNodeCreate = (def_id) => { 365 | // drop a new node into the nodegraph with the prototype given by `def_id`. 366 | // where the node should be connected, and what its parent should be 367 | // both depend on what's selected. try to do a good job of being convenient. 368 | // called by the node placement dialog when it is closed. 369 | var elem = this.state.active_node_dialog.selected_elem 370 | 371 | this.setState(prevState => { 372 | return { active_node_dialog : null } 373 | }) 374 | 375 | // the focused thing (new node dialog) just lost focus 376 | // prevent focus from falling back to the main window 377 | this.getElement(new GraphElement("view", null)).grabFocus() 378 | 379 | if (def_id !== null) { 380 | let parent_id = null 381 | let connect_port = null 382 | 383 | // todo: maybe we should pass the selected element in the creation command, 384 | // and maybe the backend should handle what compound actions to do? 385 | // (note that tit might remove some flexibility for alternate UI authors) 386 | 387 | // the selected elem at the time of placement determines any 388 | // auto-parenting/auto-connect: 389 | if (elem !== null) { 390 | if (elem.type === "node") { 391 | let node = this.state.ng.nodes.get(elem.id) 392 | let def = this.state.ng.defs.get(node.def_id) 393 | if (def.hasBody()) { 394 | // make new node a child of the selected node 395 | parent_id = elem.id 396 | } else { 397 | // make the new node a sibling of the selected node 398 | parent_id = node.parent 399 | // connect the new node to the first output port of the selected node 400 | let sig = def.type_sig 401 | if (sig.getSourceIDs().size > 0) { 402 | connect_port = { 403 | node_id : elem.id, 404 | port_id : sig.getSourceIDs().first(), 405 | is_sink : false, 406 | } 407 | } else { 408 | connect_port = { 409 | node_id : elem.id, 410 | port_id : sig.getSinkIDs().first(), 411 | is_sink : true, 412 | } 413 | } 414 | } 415 | } else if (elem.type === "port") { 416 | let node = this.state.ng.nodes.get(elem.id.node_id) 417 | parent_id = node.parent 418 | connect_port = elem.id 419 | } else if (elem.type === "link") { 420 | // xxx todo: insert the node along the selected link. 421 | // implement composite actions; 422 | // do [rm link, add node, add link, add link]. 423 | } 424 | } 425 | 426 | // will be unique with astronomical probability: 427 | let node_id = crypto.randomBytes(8).toString('hex') 428 | 429 | // add the node 430 | this.editProxy.action("add", "node", { 431 | node_id : node_id, 432 | def_id : def_id, 433 | parent_id : parent_id, 434 | }) 435 | 436 | // complete the auto-connect, if necessary: 437 | if (connect_port !== null) { 438 | let new_sig = this.state.ng.defs.get(def_id).type_sig 439 | let new_port = { 440 | node_id : node_id, 441 | port_id : (connect_port.is_sink ? 442 | new_sig.getSourceIDs() : 443 | new_sig.getSinkIDs()).first(), 444 | is_sink : !connect_port.is_sink 445 | } 446 | let link = { 447 | src : connect_port.is_sink ? new_port : connect_port, 448 | sink : connect_port.is_sink ? connect_port : new_port, 449 | } 450 | this.editProxy.action("add", "link", {link}) 451 | } 452 | } 453 | } 454 | 455 | 456 | onPortCreated = (def_id, type_id, is_sink) => { 457 | this.editProxy.action("add", "port", { 458 | def_id, 459 | type_id, 460 | is_sink 461 | }) 462 | } 463 | 464 | 465 | appendMessage = (text, style='neutral') => { 466 | if (this.console) { 467 | this.console.push_message(text, style) 468 | } else { 469 | console.log("I am a sad panda.") 470 | } 471 | } 472 | 473 | 474 | /****** Rendering functions ******/ 475 | 476 | 477 | renderPartialLink() { 478 | if (this.state.partial_link_anchor !== null) { 479 | return (); 484 | } 485 | } 486 | 487 | 488 | render() { 489 | var new_node_dlg = this.state.active_node_dialog !== null ? 490 | ( this.state.ng.placeable_defs.has(def_id)) 495 | } 496 | onAccept={this.onNodeCreate} 497 | stringifier={def => def.name}/>) 498 | : []; 499 | 500 | var new_port_dlg = this.state.active_port_dialog != null ? 501 | ( type_info.code} 505 | onAccept={(key) => { 506 | if (key !== null) { 507 | this.editProxy.action("add", "port", { 508 | def_id : this.state.active_port_dialog.def_id, 509 | type_id : key, 510 | is_arg : this.state.active_port_dialog.is_arg 511 | }) 512 | } 513 | this.setState(prevState => { 514 | return {active_port_dialog : null} 515 | }) 516 | }}/>) 517 | : []; 518 | 519 | return ( 520 |
524 |
{this.elem = e}}> 529 | 533 | {this.renderPartialLink()} 534 |
535 | {new_node_dlg} 536 | {new_port_dlg} 537 | {this.console = e}}/> 540 |
541 | ); 542 | } 543 | 544 | } 545 | 546 | 547 | export default App; 548 | --------------------------------------------------------------------------------