dragon-graph-lib 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dragon-graph-lib",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "JS Library to create node-based editors",
5
5
  "homepage": "https://gitlab.com/mattbas/dragon-graph-lib",
6
6
  "bugs": {
@@ -15,16 +15,14 @@ class Socket
15
15
  {
16
16
  /**
17
17
  * @param {string} name Socket type name
18
- * @param {boolean} multi_input (For input sockets) whether it should accept multiple incoming connections
19
18
  */
20
- constructor(name, multi_input=false)
19
+ constructor(name)
21
20
  {
22
21
  /**
23
22
  * Socket type name
24
23
  * @member {string}
25
24
  */
26
25
  this.name = name;
27
- this.multi_input = multi_input;
28
26
  this.accepts_incoming = new Set();
29
27
  }
30
28
 
@@ -238,8 +236,9 @@ class NodeInput extends Connector
238
236
  * @param {string} label User-visible label / title
239
237
  * @param {Socket?} socket Optional socket for incoming connectors
240
238
  * @param {HTMLInputElement?} control Optional control to set the value without a connection
239
+ * @param {boolean} multiple Whether this input accepts multiple incoming connections
241
240
  */
242
- constructor(node, name, label, socket, control)
241
+ constructor(node, name, label, socket, control, multiple=false)
243
242
  {
244
243
  super(node, name, label, socket);
245
244
  /**
@@ -247,6 +246,11 @@ class NodeInput extends Connector
247
246
  * @member {HTMLInputElement?}
248
247
  */
249
248
  this.control = control;
249
+ /**
250
+ * Whether this input accepts multiple incoming connections
251
+ * @member {boolean}
252
+ */
253
+ this.multiple = multiple;
250
254
  }
251
255
 
252
256
  build_dom()
@@ -270,7 +274,7 @@ class NodeInput extends Connector
270
274
 
271
275
  connect(connection)
272
276
  {
273
- if ( !this.socket.multi_input )
277
+ if ( !this.multiple )
274
278
  {
275
279
  for ( let conn of this.connections )
276
280
  conn.disconnect();
@@ -301,7 +305,7 @@ class NodeInput extends Connector
301
305
  * If there's input connections, the value is based on those.
302
306
  * Otherwise the value comes from the control.
303
307
  *
304
- * If there is a socket on this connector with multi_input, the returned value
308
+ * If there this connector accepts multiple inputs, the returned value
305
309
  * is always an array.
306
310
  *
307
311
  * @return {any}
@@ -310,20 +314,20 @@ class NodeInput extends Connector
310
314
  {
311
315
  if ( this.connections.length )
312
316
  {
313
- if ( this.socket.multi_input )
317
+ if ( this.multiple )
314
318
  return this.connections.map(c => c.output.get_value());
315
319
  return this.connections[0].output.get_value();
316
320
  }
317
321
 
318
322
  if ( !this.control )
319
323
  {
320
- if ( this.socket && this.socket.multi_input )
324
+ if ( this.socket && this.multiple )
321
325
  return [];
322
326
  return null;
323
327
  }
324
328
 
325
329
  let value = this.control.get_value();
326
- if ( this.socket && this.socket.multi_input )
330
+ if ( this.socket && this.multiple )
327
331
  return [value];
328
332
  return value;
329
333
  }
@@ -512,6 +516,26 @@ class NodeControl
512
516
  {
513
517
  this.set_visibility("visible");
514
518
  }
519
+
520
+ /**
521
+ * Serializes a value to JSON
522
+ * @param {any} value Value to serialize, as returned from this.get_value()
523
+ * @return {any} value Value that can be serialized to JSON
524
+ */
525
+ serialize_value(value)
526
+ {
527
+ return value;
528
+ }
529
+
530
+ /**
531
+ * Deserializes a value from JSON
532
+ * @param {any} value Value from a JSON object
533
+ * @return {any} value Value that can be passed to this.set_value()
534
+ */
535
+ deserialize_value(value)
536
+ {
537
+ return value;
538
+ }
515
539
  }
516
540
 
517
541
  /**
@@ -547,6 +571,8 @@ class InputControl extends NodeControl
547
571
  {
548
572
  if ( this.config.type == "checkbox" )
549
573
  return this.input.checked;
574
+ if ( this.config.type == "number" || this.config.type == "range" )
575
+ return Number(this.input.value);
550
576
  return this.input.value;
551
577
  }
552
578
 
@@ -648,7 +674,7 @@ class Node extends VisualElement
648
674
  * Sets control values for inputs
649
675
  * @param {object} data Object with input names as keys and associated values
650
676
  */
651
- set_input_value(data)
677
+ set_input_data(data)
652
678
  {
653
679
  for ( let [k, v] of Object.entries(this.inputs) )
654
680
  {
@@ -661,7 +687,7 @@ class Node extends VisualElement
661
687
  * Gets values from the inputs
662
688
  * @returns {object} Object with input names as keys and associated values
663
689
  */
664
- get_input_value()
690
+ get_input_data()
665
691
  {
666
692
  let data = {};
667
693
  for ( let [k, v] of Object.entries(this.inputs) )
@@ -695,10 +721,11 @@ class Node extends VisualElement
695
721
  * @param {string} label User-visible label / title
696
722
  * @param {Socket?} socket Optional socket for incoming connectors
697
723
  * @param {HTMLInputElement?} control Optional control to set the value without a connection
724
+ * @param {boolean} multiple Whether this input accepts multiple incoming connections
698
725
  */
699
- add_input(name, title, socket, control)
726
+ add_input(name, title, socket, control, multiple)
700
727
  {
701
- this.inputs[name] = new NodeInput(this, name, title, socket, control);
728
+ this.inputs[name] = new NodeInput(this, name, title, socket, control, multiple);
702
729
  }
703
730
 
704
731
  /**
@@ -765,7 +792,7 @@ class Node extends VisualElement
765
792
  * Returns a list of all output connections
766
793
  * @returns {Connection[]}
767
794
  */
768
- all_connections()
795
+ output_connections()
769
796
  {
770
797
  return Object.values(this.outputs)
771
798
  .map(c => c.connections)
@@ -851,35 +878,37 @@ class NodeType
851
878
  class ContextMenu extends VisualElement
852
879
  {
853
880
  /**
881
+ * @param {Editor} editor
854
882
  * @param {object|NodeType[]} actions If an object,
855
883
  * keys will be items in the menu while values are the performed actions.
856
884
  * Possible values are callbacks, NodeType instances (which will create new nodes),
857
885
  * Or other actions definitions for submenus
858
886
  */
859
- constructor(actions)
887
+ constructor(editor, actions)
860
888
  {
861
889
  super();
890
+ this.editor = editor;
862
891
  this.actions = actions;
863
892
  }
864
893
 
865
- to_dom(editor)
894
+ to_dom()
866
895
  {
867
- this.dom = this.build(editor);
896
+ this.dom = this.build();
868
897
  return this.dom;
869
898
  }
870
899
 
871
- build(editor)
900
+ build()
872
901
  {
873
- let dom = this.make_menu("dgl-menu", this.actions, editor);
902
+ let dom = this.make_menu("dgl-menu", this.actions);
874
903
  dom.style.display = "none";
875
904
  return dom;
876
905
  }
877
906
 
878
- make_action(title, action, editor)
907
+ make_action(title, action)
879
908
  {
880
909
  let onclick = action;
881
910
  if ( action instanceof NodeType )
882
- onclick = () => editor.create_node(action);
911
+ onclick = () => this.editor.create_node(action);
883
912
 
884
913
  let item = document.createElement("li");
885
914
  item.classList.add("dgl-menu-item");
@@ -894,7 +923,7 @@ class ContextMenu extends VisualElement
894
923
  return item;
895
924
  }
896
925
 
897
- make_menu(cls, actions, editor)
926
+ make_menu(cls, actions)
898
927
  {
899
928
  let dom = document.createElement("ul");
900
929
  dom.classList.add(cls);
@@ -903,7 +932,7 @@ class ContextMenu extends VisualElement
903
932
  {
904
933
  for ( let action of actions )
905
934
  {
906
- dom.appendChild(this.make_action(action.name, action, editor));
935
+ dom.appendChild(this.make_action(action.name, action));
907
936
  }
908
937
  }
909
938
  else
@@ -911,7 +940,7 @@ class ContextMenu extends VisualElement
911
940
  for ( let [name, val] of Object.entries(actions) )
912
941
  {
913
942
  if ( Array.isArray(val) || (typeof val == "object" && !(val instanceof NodeType)) )
914
- dom.appendChild(this.make_submenu(name, val, editor));
943
+ dom.appendChild(this.make_submenu(name, val));
915
944
  else
916
945
  dom.appendChild(this.make_action(name, val));
917
946
  }
@@ -919,14 +948,14 @@ class ContextMenu extends VisualElement
919
948
  return dom;
920
949
  }
921
950
 
922
- make_submenu(name, actions, editor)
951
+ make_submenu(name, actions)
923
952
  {
924
953
  let item = document.createElement("li");
925
954
  item.classList.add("dgl-menu-item");
926
955
  let label = item.appendChild(document.createElement("span"));
927
956
  label.textContent = name;
928
957
  label.classList.add("dgl-submenu-label");
929
- item.appendChild(this.make_menu("dgl-submenu", actions, editor));
958
+ item.appendChild(this.make_menu("dgl-submenu", actions));
930
959
  return item;
931
960
  }
932
961
 
@@ -976,6 +1005,7 @@ class Editor
976
1005
  constructor(container, context_menu={})
977
1006
  {
978
1007
  this.nodes = [];
1008
+ this.type_registry = {};
979
1009
 
980
1010
  this.container = container;
981
1011
  if ( !container.hasAttribute("tabindex") )
@@ -1014,23 +1044,141 @@ class Editor
1014
1044
  this.container.addEventListener("wheel", this._on_area_wheel.bind(this));
1015
1045
  this.container.addEventListener("contextmenu", this._on_area_menu.bind(this));
1016
1046
 
1017
- this.context_menu = new ContextMenu(context_menu);
1018
- this.node_menu = new ContextMenu({
1047
+ this.context_menu = new ContextMenu(this, context_menu);
1048
+ this.node_menu = new ContextMenu(this, {
1019
1049
  "Delete": () => {
1020
1050
  if ( this.selected_node )
1021
1051
  this.remove_node(this.selected_node);
1022
1052
  },
1023
1053
  "Duplicate": () => {
1024
1054
  if ( this.selected_node ) {
1025
- let node = this.create_node(this.selected_node.type, this.selected_node.get_input_value());
1055
+ let node = this.create_node(this.selected_node.type, this.selected_node.get_input_data());
1026
1056
  this._select_node(node);
1027
1057
 
1028
1058
  }
1029
1059
  }
1030
1060
  });
1031
1061
 
1032
- this.container.appendChild(this.node_menu.to_dom(this));
1033
- this.container.appendChild(this.context_menu.to_dom(this));
1062
+ this.container.appendChild(this.node_menu.to_dom());
1063
+ this.container.appendChild(this.context_menu.to_dom());
1064
+ }
1065
+
1066
+ /**
1067
+ * Registers node types for deserialization of graph data
1068
+ * @param {NodeType} type Type to register, you must ensure names are unique.
1069
+ * @param {string} name Name override in the registry
1070
+ */
1071
+ register_type(type, name=null)
1072
+ {
1073
+ if ( !name )
1074
+ name = type.name;
1075
+ type.registry_name = name;
1076
+ this.type_registry[name] = type;
1077
+ return type;
1078
+ }
1079
+
1080
+ /**
1081
+ * Returns a list of registered node types
1082
+ * @returns {NodeType[]}
1083
+ */
1084
+ registered_types()
1085
+ {
1086
+ return Object.values(this.type_registry);
1087
+ }
1088
+
1089
+ /**
1090
+ * Serializes the editor data to a JSON object
1091
+ * Requires all node types to be registered
1092
+ * @returns {object}
1093
+ */
1094
+ toJSON()
1095
+ {
1096
+ let data = {
1097
+ view: {
1098
+ x: this.offset_x,
1099
+ y: this.offset_y,
1100
+ scale: this.scale
1101
+ },
1102
+ nodes: [],
1103
+ connections: [],
1104
+ };
1105
+
1106
+
1107
+ for ( let i = 0; i < this.nodes.length; i++ )
1108
+ {
1109
+ this.nodes[i]._json_index = i;
1110
+ }
1111
+
1112
+ for ( let node of this.nodes )
1113
+ {
1114
+ let node_data = {
1115
+ index: node._json_index,
1116
+ type: node.type.registry_name || node.type.name,
1117
+ title: node.title,
1118
+ x: node.x,
1119
+ y: node.y,
1120
+ inputs: {},
1121
+ };
1122
+
1123
+ for ( let [name, input] of Object.entries(node.inputs) )
1124
+ {
1125
+ if ( input.control )
1126
+ node_data.inputs[name] = input.control.serialize_value(input.control.get_value());
1127
+ }
1128
+
1129
+ for ( let conn of node.output_connections() )
1130
+ {
1131
+ data.connections.push([
1132
+ conn.output.node._json_index,
1133
+ conn.output.name,
1134
+ conn.input.node._json_index,
1135
+ conn.input.name
1136
+ ]);
1137
+ }
1138
+
1139
+ data.nodes.push(node_data);
1140
+ }
1141
+
1142
+ return data;
1143
+ }
1144
+
1145
+ /**
1146
+ * Loads editor data from JSON
1147
+ * Requires all node types to be registered
1148
+ * @param {object} data Editor data as returned from toJSON
1149
+ */
1150
+ from_json(data)
1151
+ {
1152
+ this.clear();
1153
+
1154
+ this.offset_x = data.view.x;
1155
+ this.offset_y = data.view.y;
1156
+ this.scale = data.view.scale;
1157
+ this._apply_transform();
1158
+
1159
+ for ( let node_data of data.nodes )
1160
+ {
1161
+ let node = this.type_registry[node_data.type].create_node();
1162
+ node.title = node_data.title;
1163
+ node.x = node_data.x;
1164
+ node.y = node_data.y;
1165
+ this.add_node(node);
1166
+ for ( let [name, input] of Object.entries(node.inputs) )
1167
+ {
1168
+ let val = node_data.inputs[name];
1169
+ if ( val !== undefined && input.control )
1170
+ {
1171
+ input.set_value(input.control.deserialize_value(val));
1172
+ }
1173
+ }
1174
+ }
1175
+
1176
+ for ( let [oind, oname, iind, iname] of data.connections )
1177
+ {
1178
+ this._create_connection(this.nodes[oind], oname, this.nodes[iind], iname);
1179
+ }
1180
+
1181
+ this._refresh_graph();
1034
1182
  }
1035
1183
 
1036
1184
  /**
@@ -1039,8 +1187,6 @@ class Editor
1039
1187
  */
1040
1188
  add_node(node)
1041
1189
  {
1042
- node.x = this.cursor_x;
1043
- node.y = this.cursor_y;
1044
1190
  this.nodes.push(node);
1045
1191
  let node_dom = node.to_dom();
1046
1192
  this.area.appendChild(node_dom);
@@ -1060,6 +1206,22 @@ class Editor
1060
1206
  }
1061
1207
  }
1062
1208
 
1209
+ /**
1210
+ * Removes all nodes from the graph
1211
+ */
1212
+ clear()
1213
+ {
1214
+ for ( let node of this.nodes )
1215
+ {
1216
+ for ( let conn of node.all_connections() )
1217
+ conn.destroy();
1218
+ node.destroy();
1219
+ }
1220
+ this.nodes = [];
1221
+ this.reset_view();
1222
+ this._refresh_graph();
1223
+ }
1224
+
1063
1225
  /**
1064
1226
  * Removes a node from the graph
1065
1227
  * @param {Node} node
@@ -1086,8 +1248,10 @@ class Editor
1086
1248
  create_node(type, data={})
1087
1249
  {
1088
1250
  let node = type.create_node();
1251
+ node.x = this.cursor_x;
1252
+ node.y = this.cursor_y;
1089
1253
  this.add_node(node);
1090
- node.set_input_value(data);
1254
+ node.set_input_data(data);
1091
1255
  this._refresh_graph();
1092
1256
  return node;
1093
1257
  }
@@ -1192,6 +1356,17 @@ class Editor
1192
1356
  this.fit_view();
1193
1357
  }
1194
1358
 
1359
+ /**
1360
+ * Clears scale and offset
1361
+ */
1362
+ reset_view()
1363
+ {
1364
+ this.offset_x = 0;
1365
+ this.offset_y = 0;
1366
+ this.scale = 1;
1367
+ this._apply_transform();
1368
+ }
1369
+
1195
1370
  /**
1196
1371
  * Pan and zoom to fit all the nodes
1197
1372
  * @param {number} margin Margin to leave around the area
@@ -1199,28 +1374,38 @@ class Editor
1199
1374
  fit_view(margin=20)
1200
1375
  {
1201
1376
  if ( this.nodes.length == 0 )
1377
+ {
1378
+ this.reset_view();
1202
1379
  return;
1380
+ }
1203
1381
 
1204
1382
  let left = Infinity;
1205
1383
  let right = -Infinity;
1206
1384
  let top = Infinity;
1207
1385
  let bottom = -Infinity;
1208
1386
 
1387
+ let crect = this.container.getBoundingClientRect();
1388
+
1209
1389
  for ( let node of this.nodes )
1210
1390
  {
1211
- let rect = node.dom.getBoundingClientRect();
1391
+ let client_rect = node.dom.getBoundingClientRect();
1392
+
1393
+ let rect = {};
1394
+ rect.left = node.x;
1395
+ rect.right = node.x + client_rect.width / this.scale;
1396
+ rect.top = node.y;
1397
+ rect.bottom = node.y + client_rect.height / this.scale;
1398
+
1212
1399
  if ( rect.left < left ) left = rect.left;
1213
1400
  if ( rect.right > right ) right = rect.right;
1214
1401
  if ( rect.top < top ) top = rect.top;
1215
1402
  if ( rect.bottom > bottom ) bottom = rect.bottom;
1216
1403
  }
1217
1404
 
1218
- let crect = this.container.getBoundingClientRect();
1219
-
1220
- left += -margin - crect.left;
1221
- right += margin - crect.left;
1222
- top += -margin - crect.top;
1223
- bottom += margin - crect.top;
1405
+ left += -margin;
1406
+ right += margin;
1407
+ top += -margin;
1408
+ bottom += margin;
1224
1409
 
1225
1410
  let width = right - left;
1226
1411
  let height = bottom - top;
@@ -1229,12 +1414,25 @@ class Editor
1229
1414
  let scale = Math.min(x_scale, y_scale);
1230
1415
  this.scale = scale;
1231
1416
 
1232
- this.offset_x = (crect.width - width * scale) / 2 - left + margin;
1233
- this.offset_y = (crect.height - height * scale) / 2 - top + margin;
1417
+ this.offset_x = (crect.width - width * scale) / 2 - left * scale;
1418
+ this.offset_y = (crect.height - height * scale) / 2 - top * scale;
1234
1419
  this._apply_transform();
1235
1420
 
1236
1421
  }
1237
1422
 
1423
+ _create_connection(output_node, output_name, input_node, input_name)
1424
+ {
1425
+ let conn = output_node.connect(output_name, input_node, input_name);
1426
+ if ( conn )
1427
+ {
1428
+ conn.dom = document.createElementNS("http://www.w3.org/2000/svg", "path");
1429
+ conn.dom.classList.add("connection-from-" + to_slug(output_node.outputs[output_name].socket.name));
1430
+ conn.dom.classList.add("connection-to-" + to_slug(input_node.inputs[input_name].socket.name));
1431
+ this.connection_parent.appendChild(conn.dom);
1432
+ }
1433
+ return conn;
1434
+ }
1435
+
1238
1436
  /**
1239
1437
  * Adds a connection to the graph
1240
1438
  * @param {Node} output_node Node the connection is coming from
@@ -1245,13 +1443,9 @@ class Editor
1245
1443
  */
1246
1444
  connect(output_node, output_name, input_node, input_name)
1247
1445
  {
1248
- let conn = output_node.connect(output_name, input_node, input_name);
1446
+ let conn = this._create_connection(output_node, output_name, input_node, input_name)
1249
1447
  if ( conn )
1250
1448
  {
1251
- conn.dom = document.createElementNS("http://www.w3.org/2000/svg", "path");
1252
- conn.dom.classList.add("connection-from-" + to_slug(output_node.outputs[output_name].socket.name));
1253
- conn.dom.classList.add("connection-to-" + to_slug(input_node.inputs[input_name].socket.name));
1254
- this.connection_parent.appendChild(conn.dom);
1255
1449
  this._update_connection(conn);
1256
1450
  this._refresh_graph();
1257
1451
  }
@@ -1375,6 +1569,14 @@ class Editor
1375
1569
  this.area.style.transform = `translate(${this.offset_x}px, ${this.offset_y}px) scale(${this.scale})`;
1376
1570
  }
1377
1571
 
1572
+ /**
1573
+ * Re-evaluates all the nodes
1574
+ */
1575
+ update()
1576
+ {
1577
+ this._refresh_graph();
1578
+ }
1579
+
1378
1580
  _refresh_graph()
1379
1581
  {
1380
1582
  for ( let node of this.nodes )
@@ -59,7 +59,7 @@
59
59
  }
60
60
 
61
61
  .dgl-socket:hover {
62
- outline: 3px solid var(--dgl-node-text);
62
+ outline: 3px solid var(--dgl-node-text);
63
63
  border-width: 10px;
64
64
  }
65
65
 
@@ -74,6 +74,8 @@
74
74
  .dgl-connection {
75
75
  overflow: visible;
76
76
  position: absolute;
77
+ left: 0;
78
+ right: 0;
77
79
  }
78
80
  .dgl-connection path {
79
81
  stroke: var(--dgl-connection-color);
@@ -81,10 +83,6 @@
81
83
  fill: none;
82
84
  }
83
85
 
84
- .dgl-preview {
85
- max-width: 20ch;
86
- }
87
-
88
86
  .dgl-title {
89
87
  font-weight: bold;
90
88
  border-bottom: 1px solid var(--dgl-node-text);
@@ -14,16 +14,14 @@ export class Socket
14
14
  {
15
15
  /**
16
16
  * @param {string} name Socket type name
17
- * @param {boolean} multi_input (For input sockets) whether it should accept multiple incoming connections
18
17
  */
19
- constructor(name, multi_input=false)
18
+ constructor(name)
20
19
  {
21
20
  /**
22
21
  * Socket type name
23
22
  * @member {string}
24
23
  */
25
24
  this.name = name;
26
- this.multi_input = multi_input;
27
25
  this.accepts_incoming = new Set();
28
26
  }
29
27
 
@@ -237,8 +235,9 @@ export class NodeInput extends Connector
237
235
  * @param {string} label User-visible label / title
238
236
  * @param {Socket?} socket Optional socket for incoming connectors
239
237
  * @param {HTMLInputElement?} control Optional control to set the value without a connection
238
+ * @param {boolean} multiple Whether this input accepts multiple incoming connections
240
239
  */
241
- constructor(node, name, label, socket, control)
240
+ constructor(node, name, label, socket, control, multiple=false)
242
241
  {
243
242
  super(node, name, label, socket);
244
243
  /**
@@ -246,6 +245,11 @@ export class NodeInput extends Connector
246
245
  * @member {HTMLInputElement?}
247
246
  */
248
247
  this.control = control;
248
+ /**
249
+ * Whether this input accepts multiple incoming connections
250
+ * @member {boolean}
251
+ */
252
+ this.multiple = multiple;
249
253
  }
250
254
 
251
255
  build_dom()
@@ -269,7 +273,7 @@ export class NodeInput extends Connector
269
273
 
270
274
  connect(connection)
271
275
  {
272
- if ( !this.socket.multi_input )
276
+ if ( !this.multiple )
273
277
  {
274
278
  for ( let conn of this.connections )
275
279
  conn.disconnect();
@@ -300,7 +304,7 @@ export class NodeInput extends Connector
300
304
  * If there's input connections, the value is based on those.
301
305
  * Otherwise the value comes from the control.
302
306
  *
303
- * If there is a socket on this connector with multi_input, the returned value
307
+ * If there this connector accepts multiple inputs, the returned value
304
308
  * is always an array.
305
309
  *
306
310
  * @return {any}
@@ -309,20 +313,20 @@ export class NodeInput extends Connector
309
313
  {
310
314
  if ( this.connections.length )
311
315
  {
312
- if ( this.socket.multi_input )
316
+ if ( this.multiple )
313
317
  return this.connections.map(c => c.output.get_value());
314
318
  return this.connections[0].output.get_value();
315
319
  }
316
320
 
317
321
  if ( !this.control )
318
322
  {
319
- if ( this.socket && this.socket.multi_input )
323
+ if ( this.socket && this.multiple )
320
324
  return [];
321
325
  return null;
322
326
  }
323
327
 
324
328
  let value = this.control.get_value();
325
- if ( this.socket && this.socket.multi_input )
329
+ if ( this.socket && this.multiple )
326
330
  return [value];
327
331
  return value;
328
332
  }
@@ -511,6 +515,26 @@ export class NodeControl
511
515
  {
512
516
  this.set_visibility("visible");
513
517
  }
518
+
519
+ /**
520
+ * Serializes a value to JSON
521
+ * @param {any} value Value to serialize, as returned from this.get_value()
522
+ * @return {any} value Value that can be serialized to JSON
523
+ */
524
+ serialize_value(value)
525
+ {
526
+ return value;
527
+ }
528
+
529
+ /**
530
+ * Deserializes a value from JSON
531
+ * @param {any} value Value from a JSON object
532
+ * @return {any} value Value that can be passed to this.set_value()
533
+ */
534
+ deserialize_value(value)
535
+ {
536
+ return value;
537
+ }
514
538
  }
515
539
 
516
540
  /**
@@ -546,6 +570,8 @@ export class InputControl extends NodeControl
546
570
  {
547
571
  if ( this.config.type == "checkbox" )
548
572
  return this.input.checked;
573
+ if ( this.config.type == "number" || this.config.type == "range" )
574
+ return Number(this.input.value);
549
575
  return this.input.value;
550
576
  }
551
577
 
@@ -647,7 +673,7 @@ export class Node extends VisualElement
647
673
  * Sets control values for inputs
648
674
  * @param {object} data Object with input names as keys and associated values
649
675
  */
650
- set_input_value(data)
676
+ set_input_data(data)
651
677
  {
652
678
  for ( let [k, v] of Object.entries(this.inputs) )
653
679
  {
@@ -660,7 +686,7 @@ export class Node extends VisualElement
660
686
  * Gets values from the inputs
661
687
  * @returns {object} Object with input names as keys and associated values
662
688
  */
663
- get_input_value()
689
+ get_input_data()
664
690
  {
665
691
  let data = {};
666
692
  for ( let [k, v] of Object.entries(this.inputs) )
@@ -694,10 +720,11 @@ export class Node extends VisualElement
694
720
  * @param {string} label User-visible label / title
695
721
  * @param {Socket?} socket Optional socket for incoming connectors
696
722
  * @param {HTMLInputElement?} control Optional control to set the value without a connection
723
+ * @param {boolean} multiple Whether this input accepts multiple incoming connections
697
724
  */
698
- add_input(name, title, socket, control)
725
+ add_input(name, title, socket, control, multiple)
699
726
  {
700
- this.inputs[name] = new NodeInput(this, name, title, socket, control);
727
+ this.inputs[name] = new NodeInput(this, name, title, socket, control, multiple);
701
728
  }
702
729
 
703
730
  /**
@@ -764,7 +791,7 @@ export class Node extends VisualElement
764
791
  * Returns a list of all output connections
765
792
  * @returns {Connection[]}
766
793
  */
767
- all_connections()
794
+ output_connections()
768
795
  {
769
796
  return Object.values(this.outputs)
770
797
  .map(c => c.connections)
@@ -850,35 +877,37 @@ export class NodeType
850
877
  export class ContextMenu extends VisualElement
851
878
  {
852
879
  /**
880
+ * @param {Editor} editor
853
881
  * @param {object|NodeType[]} actions If an object,
854
882
  * keys will be items in the menu while values are the performed actions.
855
883
  * Possible values are callbacks, NodeType instances (which will create new nodes),
856
884
  * Or other actions definitions for submenus
857
885
  */
858
- constructor(actions)
886
+ constructor(editor, actions)
859
887
  {
860
888
  super();
889
+ this.editor = editor;
861
890
  this.actions = actions;
862
891
  }
863
892
 
864
- to_dom(editor)
893
+ to_dom()
865
894
  {
866
- this.dom = this.build(editor);
895
+ this.dom = this.build();
867
896
  return this.dom;
868
897
  }
869
898
 
870
- build(editor)
899
+ build()
871
900
  {
872
- let dom = this.make_menu("dgl-menu", this.actions, editor);
901
+ let dom = this.make_menu("dgl-menu", this.actions);
873
902
  dom.style.display = "none";
874
903
  return dom;
875
904
  }
876
905
 
877
- make_action(title, action, editor)
906
+ make_action(title, action)
878
907
  {
879
908
  let onclick = action;
880
909
  if ( action instanceof NodeType )
881
- onclick = () => editor.create_node(action);
910
+ onclick = () => this.editor.create_node(action);
882
911
 
883
912
  let item = document.createElement("li");
884
913
  item.classList.add("dgl-menu-item");
@@ -893,7 +922,7 @@ export class ContextMenu extends VisualElement
893
922
  return item;
894
923
  }
895
924
 
896
- make_menu(cls, actions, editor)
925
+ make_menu(cls, actions)
897
926
  {
898
927
  let dom = document.createElement("ul");
899
928
  dom.classList.add(cls);
@@ -902,7 +931,7 @@ export class ContextMenu extends VisualElement
902
931
  {
903
932
  for ( let action of actions )
904
933
  {
905
- dom.appendChild(this.make_action(action.name, action, editor));
934
+ dom.appendChild(this.make_action(action.name, action));
906
935
  }
907
936
  }
908
937
  else
@@ -910,7 +939,7 @@ export class ContextMenu extends VisualElement
910
939
  for ( let [name, val] of Object.entries(actions) )
911
940
  {
912
941
  if ( Array.isArray(val) || (typeof val == "object" && !(val instanceof NodeType)) )
913
- dom.appendChild(this.make_submenu(name, val, editor));
942
+ dom.appendChild(this.make_submenu(name, val));
914
943
  else
915
944
  dom.appendChild(this.make_action(name, val));
916
945
  }
@@ -918,14 +947,14 @@ export class ContextMenu extends VisualElement
918
947
  return dom;
919
948
  }
920
949
 
921
- make_submenu(name, actions, editor)
950
+ make_submenu(name, actions)
922
951
  {
923
952
  let item = document.createElement("li");
924
953
  item.classList.add("dgl-menu-item");
925
954
  let label = item.appendChild(document.createElement("span"));
926
955
  label.textContent = name;
927
956
  label.classList.add("dgl-submenu-label");
928
- item.appendChild(this.make_menu("dgl-submenu", actions, editor));
957
+ item.appendChild(this.make_menu("dgl-submenu", actions));
929
958
  return item;
930
959
  }
931
960
 
@@ -975,6 +1004,7 @@ export class Editor
975
1004
  constructor(container, context_menu={})
976
1005
  {
977
1006
  this.nodes = [];
1007
+ this.type_registry = {};
978
1008
 
979
1009
  this.container = container;
980
1010
  if ( !container.hasAttribute("tabindex") )
@@ -1013,23 +1043,141 @@ export class Editor
1013
1043
  this.container.addEventListener("wheel", this._on_area_wheel.bind(this));
1014
1044
  this.container.addEventListener("contextmenu", this._on_area_menu.bind(this));
1015
1045
 
1016
- this.context_menu = new ContextMenu(context_menu);
1017
- this.node_menu = new ContextMenu({
1046
+ this.context_menu = new ContextMenu(this, context_menu);
1047
+ this.node_menu = new ContextMenu(this, {
1018
1048
  "Delete": () => {
1019
1049
  if ( this.selected_node )
1020
1050
  this.remove_node(this.selected_node);
1021
1051
  },
1022
1052
  "Duplicate": () => {
1023
1053
  if ( this.selected_node ) {
1024
- let node = this.create_node(this.selected_node.type, this.selected_node.get_input_value());
1054
+ let node = this.create_node(this.selected_node.type, this.selected_node.get_input_data());
1025
1055
  this._select_node(node);
1026
1056
 
1027
1057
  }
1028
1058
  }
1029
1059
  });
1030
1060
 
1031
- this.container.appendChild(this.node_menu.to_dom(this));
1032
- this.container.appendChild(this.context_menu.to_dom(this));
1061
+ this.container.appendChild(this.node_menu.to_dom());
1062
+ this.container.appendChild(this.context_menu.to_dom());
1063
+ }
1064
+
1065
+ /**
1066
+ * Registers node types for deserialization of graph data
1067
+ * @param {NodeType} type Type to register, you must ensure names are unique.
1068
+ * @param {string} name Name override in the registry
1069
+ */
1070
+ register_type(type, name=null)
1071
+ {
1072
+ if ( !name )
1073
+ name = type.name;
1074
+ type.registry_name = name;
1075
+ this.type_registry[name] = type;
1076
+ return type;
1077
+ }
1078
+
1079
+ /**
1080
+ * Returns a list of registered node types
1081
+ * @returns {NodeType[]}
1082
+ */
1083
+ registered_types()
1084
+ {
1085
+ return Object.values(this.type_registry);
1086
+ }
1087
+
1088
+ /**
1089
+ * Serializes the editor data to a JSON object
1090
+ * Requires all node types to be registered
1091
+ * @returns {object}
1092
+ */
1093
+ toJSON()
1094
+ {
1095
+ let data = {
1096
+ view: {
1097
+ x: this.offset_x,
1098
+ y: this.offset_y,
1099
+ scale: this.scale
1100
+ },
1101
+ nodes: [],
1102
+ connections: [],
1103
+ };
1104
+
1105
+
1106
+ for ( let i = 0; i < this.nodes.length; i++ )
1107
+ {
1108
+ this.nodes[i]._json_index = i;
1109
+ }
1110
+
1111
+ for ( let node of this.nodes )
1112
+ {
1113
+ let node_data = {
1114
+ index: node._json_index,
1115
+ type: node.type.registry_name || node.type.name,
1116
+ title: node.title,
1117
+ x: node.x,
1118
+ y: node.y,
1119
+ inputs: {},
1120
+ };
1121
+
1122
+ for ( let [name, input] of Object.entries(node.inputs) )
1123
+ {
1124
+ if ( input.control )
1125
+ node_data.inputs[name] = input.control.serialize_value(input.control.get_value());
1126
+ }
1127
+
1128
+ for ( let conn of node.output_connections() )
1129
+ {
1130
+ data.connections.push([
1131
+ conn.output.node._json_index,
1132
+ conn.output.name,
1133
+ conn.input.node._json_index,
1134
+ conn.input.name
1135
+ ]);
1136
+ }
1137
+
1138
+ data.nodes.push(node_data);
1139
+ }
1140
+
1141
+ return data;
1142
+ }
1143
+
1144
+ /**
1145
+ * Loads editor data from JSON
1146
+ * Requires all node types to be registered
1147
+ * @param {object} data Editor data as returned from toJSON
1148
+ */
1149
+ from_json(data)
1150
+ {
1151
+ this.clear();
1152
+
1153
+ this.offset_x = data.view.x;
1154
+ this.offset_y = data.view.y;
1155
+ this.scale = data.view.scale;
1156
+ this._apply_transform();
1157
+
1158
+ for ( let node_data of data.nodes )
1159
+ {
1160
+ let node = this.type_registry[node_data.type].create_node();
1161
+ node.title = node_data.title;
1162
+ node.x = node_data.x;
1163
+ node.y = node_data.y;
1164
+ this.add_node(node);
1165
+ for ( let [name, input] of Object.entries(node.inputs) )
1166
+ {
1167
+ let val = node_data.inputs[name];
1168
+ if ( val !== undefined && input.control )
1169
+ {
1170
+ input.set_value(input.control.deserialize_value(val));
1171
+ }
1172
+ }
1173
+ }
1174
+
1175
+ for ( let [oind, oname, iind, iname] of data.connections )
1176
+ {
1177
+ this._create_connection(this.nodes[oind], oname, this.nodes[iind], iname);
1178
+ }
1179
+
1180
+ this._refresh_graph();
1033
1181
  }
1034
1182
 
1035
1183
  /**
@@ -1038,8 +1186,6 @@ export class Editor
1038
1186
  */
1039
1187
  add_node(node)
1040
1188
  {
1041
- node.x = this.cursor_x;
1042
- node.y = this.cursor_y;
1043
1189
  this.nodes.push(node);
1044
1190
  let node_dom = node.to_dom();
1045
1191
  this.area.appendChild(node_dom);
@@ -1059,6 +1205,22 @@ export class Editor
1059
1205
  }
1060
1206
  }
1061
1207
 
1208
+ /**
1209
+ * Removes all nodes from the graph
1210
+ */
1211
+ clear()
1212
+ {
1213
+ for ( let node of this.nodes )
1214
+ {
1215
+ for ( let conn of node.all_connections() )
1216
+ conn.destroy();
1217
+ node.destroy();
1218
+ }
1219
+ this.nodes = [];
1220
+ this.reset_view();
1221
+ this._refresh_graph();
1222
+ }
1223
+
1062
1224
  /**
1063
1225
  * Removes a node from the graph
1064
1226
  * @param {Node} node
@@ -1085,8 +1247,10 @@ export class Editor
1085
1247
  create_node(type, data={})
1086
1248
  {
1087
1249
  let node = type.create_node();
1250
+ node.x = this.cursor_x;
1251
+ node.y = this.cursor_y;
1088
1252
  this.add_node(node);
1089
- node.set_input_value(data);
1253
+ node.set_input_data(data);
1090
1254
  this._refresh_graph();
1091
1255
  return node;
1092
1256
  }
@@ -1191,6 +1355,17 @@ export class Editor
1191
1355
  this.fit_view();
1192
1356
  }
1193
1357
 
1358
+ /**
1359
+ * Clears scale and offset
1360
+ */
1361
+ reset_view()
1362
+ {
1363
+ this.offset_x = 0;
1364
+ this.offset_y = 0;
1365
+ this.scale = 1;
1366
+ this._apply_transform();
1367
+ }
1368
+
1194
1369
  /**
1195
1370
  * Pan and zoom to fit all the nodes
1196
1371
  * @param {number} margin Margin to leave around the area
@@ -1198,28 +1373,38 @@ export class Editor
1198
1373
  fit_view(margin=20)
1199
1374
  {
1200
1375
  if ( this.nodes.length == 0 )
1376
+ {
1377
+ this.reset_view();
1201
1378
  return;
1379
+ }
1202
1380
 
1203
1381
  let left = Infinity;
1204
1382
  let right = -Infinity;
1205
1383
  let top = Infinity;
1206
1384
  let bottom = -Infinity;
1207
1385
 
1386
+ let crect = this.container.getBoundingClientRect();
1387
+
1208
1388
  for ( let node of this.nodes )
1209
1389
  {
1210
- let rect = node.dom.getBoundingClientRect();
1390
+ let client_rect = node.dom.getBoundingClientRect();
1391
+
1392
+ let rect = {};
1393
+ rect.left = node.x;
1394
+ rect.right = node.x + client_rect.width / this.scale;
1395
+ rect.top = node.y;
1396
+ rect.bottom = node.y + client_rect.height / this.scale;
1397
+
1211
1398
  if ( rect.left < left ) left = rect.left;
1212
1399
  if ( rect.right > right ) right = rect.right;
1213
1400
  if ( rect.top < top ) top = rect.top;
1214
1401
  if ( rect.bottom > bottom ) bottom = rect.bottom;
1215
1402
  }
1216
1403
 
1217
- let crect = this.container.getBoundingClientRect();
1218
-
1219
- left += -margin - crect.left;
1220
- right += margin - crect.left;
1221
- top += -margin - crect.top;
1222
- bottom += margin - crect.top;
1404
+ left += -margin;
1405
+ right += margin;
1406
+ top += -margin;
1407
+ bottom += margin;
1223
1408
 
1224
1409
  let width = right - left;
1225
1410
  let height = bottom - top;
@@ -1228,12 +1413,25 @@ export class Editor
1228
1413
  let scale = Math.min(x_scale, y_scale);
1229
1414
  this.scale = scale;
1230
1415
 
1231
- this.offset_x = (crect.width - width * scale) / 2 - left + margin;
1232
- this.offset_y = (crect.height - height * scale) / 2 - top + margin;
1416
+ this.offset_x = (crect.width - width * scale) / 2 - left * scale;
1417
+ this.offset_y = (crect.height - height * scale) / 2 - top * scale;
1233
1418
  this._apply_transform();
1234
1419
 
1235
1420
  }
1236
1421
 
1422
+ _create_connection(output_node, output_name, input_node, input_name)
1423
+ {
1424
+ let conn = output_node.connect(output_name, input_node, input_name);
1425
+ if ( conn )
1426
+ {
1427
+ conn.dom = document.createElementNS("http://www.w3.org/2000/svg", "path");
1428
+ conn.dom.classList.add("connection-from-" + to_slug(output_node.outputs[output_name].socket.name));
1429
+ conn.dom.classList.add("connection-to-" + to_slug(input_node.inputs[input_name].socket.name));
1430
+ this.connection_parent.appendChild(conn.dom);
1431
+ }
1432
+ return conn;
1433
+ }
1434
+
1237
1435
  /**
1238
1436
  * Adds a connection to the graph
1239
1437
  * @param {Node} output_node Node the connection is coming from
@@ -1244,13 +1442,9 @@ export class Editor
1244
1442
  */
1245
1443
  connect(output_node, output_name, input_node, input_name)
1246
1444
  {
1247
- let conn = output_node.connect(output_name, input_node, input_name);
1445
+ let conn = this._create_connection(output_node, output_name, input_node, input_name)
1248
1446
  if ( conn )
1249
1447
  {
1250
- conn.dom = document.createElementNS("http://www.w3.org/2000/svg", "path");
1251
- conn.dom.classList.add("connection-from-" + to_slug(output_node.outputs[output_name].socket.name));
1252
- conn.dom.classList.add("connection-to-" + to_slug(input_node.inputs[input_name].socket.name));
1253
- this.connection_parent.appendChild(conn.dom);
1254
1448
  this._update_connection(conn);
1255
1449
  this._refresh_graph();
1256
1450
  }
@@ -1374,6 +1568,14 @@ export class Editor
1374
1568
  this.area.style.transform = `translate(${this.offset_x}px, ${this.offset_y}px) scale(${this.scale})`;
1375
1569
  }
1376
1570
 
1571
+ /**
1572
+ * Re-evaluates all the nodes
1573
+ */
1574
+ update()
1575
+ {
1576
+ this._refresh_graph();
1577
+ }
1578
+
1377
1579
  _refresh_graph()
1378
1580
  {
1379
1581
  for ( let node of this.nodes )