├── 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 |
26 | You need to enable JavaScript to run this app.
27 |
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 |
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 = {
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.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 |
--------------------------------------------------------------------------------