dragon-graph-lib 0.1.2 → 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.
@@ -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
 
@@ -205,11 +203,6 @@ class Connector extends VisualElement
205
203
  this.dom_items = {};
206
204
  }
207
205
 
208
- update_dom()
209
- {
210
- this.dom_items.label.textContent = this.label;
211
- }
212
-
213
206
  /**
214
207
  * Adds a connection to the connector
215
208
  * @param {Connection} connection
@@ -243,8 +236,9 @@ class NodeInput extends Connector
243
236
  * @param {string} label User-visible label / title
244
237
  * @param {Socket?} socket Optional socket for incoming connectors
245
238
  * @param {HTMLInputElement?} control Optional control to set the value without a connection
239
+ * @param {boolean} multiple Whether this input accepts multiple incoming connections
246
240
  */
247
- constructor(node, name, label, socket, control)
241
+ constructor(node, name, label, socket, control, multiple=false)
248
242
  {
249
243
  super(node, name, label, socket);
250
244
  /**
@@ -252,6 +246,11 @@ class NodeInput extends Connector
252
246
  * @member {HTMLInputElement?}
253
247
  */
254
248
  this.control = control;
249
+ /**
250
+ * Whether this input accepts multiple incoming connections
251
+ * @member {boolean}
252
+ */
253
+ this.multiple = multiple;
255
254
  }
256
255
 
257
256
  build_dom()
@@ -265,20 +264,17 @@ class NodeInput extends Connector
265
264
  container.classList.add("dgl-socketed");
266
265
  }
267
266
 
268
- this.dom_items.label = document.createElement("span");
269
- this.dom_items.label.textContent = this.name;
270
- this.dom_items.label.classList.add("dgl-label", "dgl-input-label");
271
- container.appendChild(this.dom_items.label);
272
-
273
267
  if ( this.control )
274
- container.appendChild(this.control);
268
+ container.appendChild(this.control.to_dom(this.label, this.name));
269
+ else
270
+ container.appendChild(NodeControl.make_label(this.label));
275
271
 
276
272
  return container;
277
273
  }
278
274
 
279
275
  connect(connection)
280
276
  {
281
- if ( !this.socket.multi_input )
277
+ if ( !this.multiple )
282
278
  {
283
279
  for ( let conn of this.connections )
284
280
  conn.disconnect();
@@ -288,7 +284,7 @@ class NodeInput extends Connector
288
284
  this.connections.push(connection);
289
285
 
290
286
  if ( this.control )
291
- this.control.style.visibility = "hidden";
287
+ this.control.hide();
292
288
  }
293
289
 
294
290
  /**
@@ -299,7 +295,7 @@ class NodeInput extends Connector
299
295
  {
300
296
  if ( this.control )
301
297
  {
302
- this.control.value = value;
298
+ this.control.set_value(value);
303
299
  }
304
300
  }
305
301
 
@@ -309,7 +305,7 @@ class NodeInput extends Connector
309
305
  * If there's input connections, the value is based on those.
310
306
  * Otherwise the value comes from the control.
311
307
  *
312
- * 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
313
309
  * is always an array.
314
310
  *
315
311
  * @return {any}
@@ -318,20 +314,20 @@ class NodeInput extends Connector
318
314
  {
319
315
  if ( this.connections.length )
320
316
  {
321
- if ( this.socket.multi_input )
317
+ if ( this.multiple )
322
318
  return this.connections.map(c => c.output.get_value());
323
319
  return this.connections[0].output.get_value();
324
320
  }
325
321
 
326
322
  if ( !this.control )
327
323
  {
328
- if ( this.socket && this.socket.multi_input )
324
+ if ( this.socket && this.multiple )
329
325
  return [];
330
326
  return null;
331
327
  }
332
328
 
333
- let value = this.control.value
334
- if ( this.socket && this.socket.multi_input )
329
+ let value = this.control.get_value();
330
+ if ( this.socket && this.multiple )
335
331
  return [value];
336
332
  return value;
337
333
  }
@@ -340,7 +336,7 @@ class NodeInput extends Connector
340
336
  {
341
337
  super.disconnect(connection);
342
338
  if ( this.control && this.connections.length == 0 )
343
- this.control.style.visibility = "visible";
339
+ this.control.show();
344
340
  }
345
341
  }
346
342
 
@@ -402,7 +398,7 @@ class NodeOutput extends Connector
402
398
  */
403
399
  get_value()
404
400
  {
405
- if ( this.value == undefined )
401
+ if ( this.value === undefined || this.node.dirty )
406
402
  this.node.evaluate();
407
403
  return this.value;
408
404
  }
@@ -415,23 +411,221 @@ function force_default(ev)
415
411
  }
416
412
 
417
413
  /**
418
- * Creates a node control
419
- * @param {string} type Input type
420
- * @param {object} attrs Input attributes as an object
421
- * @returns {HTMLInputElement}
414
+ * Custom control interface
422
415
  */
423
- function NodeControl(type, attrs={})
416
+ class NodeControl
424
417
  {
425
- let elem = document.createElement("input");
426
- elem.type = type;
427
- elem.classList.add("dgl-control");
428
- for ( let [k, v] of Object.entries(attrs) )
429
- elem.setAttribute(k, v);
430
- elem.addEventListener("mousedown", force_default);
431
- elem.addEventListener("mouseup", force_default);
432
- elem.addEventListener("mousemove", force_default);
433
- elem.addEventListener("wheel", force_default);
434
- return elem;
418
+ /**
419
+ * @param {object} config Control config
420
+ * @param {any} config.default Control default value
421
+ */
422
+ constructor(config={})
423
+ {
424
+ this.on_change = null;
425
+ this.config = config;
426
+ }
427
+
428
+ to_dom(label, name)
429
+ {
430
+ this.element = document.createElement("div");
431
+ this.element.classList.add("dgl-control-parent");
432
+ if ( label.length )
433
+ this.element.appendChild(NodeControl.make_label(label));
434
+ this.setup_dom(this.element, name);
435
+ if ( "default" in this.config )
436
+ this.set_value(this.config.default);
437
+ return this.element;
438
+ }
439
+
440
+ /**
441
+ * Sets up the DOM
442
+ * @param {HTMLElement} parent Element to add additional content to
443
+ * @param {name} name Control name
444
+ */
445
+ setup_dom(_parent, _name)
446
+ {
447
+ }
448
+
449
+ /**
450
+ * Dispatches the change event to update the graph
451
+ */
452
+ notify_change()
453
+ {
454
+ this.on_change();
455
+ }
456
+
457
+ /**
458
+ * Override to return the control value
459
+ * @returns {any} value
460
+ */
461
+ get_value()
462
+ {
463
+ }
464
+
465
+ /**
466
+ * Override to return set a value to the control
467
+ * @param {any} value
468
+ */
469
+ set_value(_value)
470
+ {
471
+ }
472
+
473
+ /**
474
+ * Ensures an element uses the default mouse interactions instead of
475
+ * propagating to the node editor
476
+ */
477
+ use_default_events(element)
478
+ {
479
+ element.addEventListener("mousedown", force_default);
480
+ element.addEventListener("mouseup", force_default);
481
+ element.addEventListener("mousemove", force_default);
482
+ element.addEventListener("wheel", force_default);
483
+ }
484
+
485
+ /**
486
+ * Creates a label element
487
+ * @param {string} label Text to display
488
+ */
489
+ static make_label(label)
490
+ {
491
+ let elem = document.createElement("span");
492
+ elem.textContent = label;
493
+ elem.classList.add("dgl-label", "dgl-input-label");
494
+ return elem;
495
+ }
496
+
497
+ set_visibility(value)
498
+ {
499
+ for ( let ch of this.element.children )
500
+ if ( !ch.classList.contains("dgl-label") )
501
+ ch.style.visibility = value;
502
+ }
503
+
504
+ /**
505
+ * Hides the control when an input connection is attached
506
+ */
507
+ hide()
508
+ {
509
+ this.set_visibility("hidden");
510
+ }
511
+
512
+ /**
513
+ * Shows the control when an input connection is detached
514
+ */
515
+ show()
516
+ {
517
+ this.set_visibility("visible");
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
+ }
539
+ }
540
+
541
+ /**
542
+ * Control for simple input elements
543
+ */
544
+ class InputControl extends NodeControl
545
+ {
546
+ /**
547
+ * @param {object} config Control config
548
+ * @param {string} config.type Input type
549
+ * @param {object} config.attrs Input attributes
550
+ * @param {any} config.default Default value
551
+ */
552
+ constructor(config={type: "text"})
553
+ {
554
+ super(config);
555
+ }
556
+
557
+ setup_dom(parent, name)
558
+ {
559
+ this.input = document.createElement("input");
560
+ parent.appendChild(this.input);
561
+ this.input.type = this.config.type;
562
+ this.input.classList.add("dgl-control");
563
+ for ( let [k, v] of Object.entries(this.config.attrs ?? {}) )
564
+ this.input.setAttribute(k, v);
565
+ this.use_default_events(this.input);
566
+ this.input.name = name;
567
+ this.input.addEventListener("input", () => this.notify_change());
568
+ }
569
+
570
+ get_value()
571
+ {
572
+ if ( this.config.type == "checkbox" )
573
+ return this.input.checked;
574
+ if ( this.config.type == "number" || this.config.type == "range" )
575
+ return Number(this.input.value);
576
+ return this.input.value;
577
+ }
578
+
579
+ set_value(value)
580
+ {
581
+ if ( this.config.type == "checkbox" )
582
+ this.input.checked = value;
583
+ else
584
+ this.input.value = value;
585
+ }
586
+ }
587
+
588
+ /**
589
+ * Drop-down Control
590
+ */
591
+ class DropDownControl extends NodeControl
592
+ {
593
+
594
+ /**
595
+ * @param {object} config Control config
596
+ * @param {Array.<string, string>} config.options Drop down options ([value, display])
597
+ * @param {any} config.default Default value
598
+ */
599
+ constructor(config={options: []})
600
+ {
601
+ super(config);
602
+ }
603
+
604
+ setup_dom(parent, name)
605
+ {
606
+ this.input = document.createElement("select");
607
+ for ( let [value, display] of this.config.options )
608
+ {
609
+ let option = document.createElement("option");
610
+ this.input.appendChild(option);
611
+ option.value = value;
612
+ option.textContent = display ?? value;
613
+ }
614
+ this.input.name = name;
615
+ parent.appendChild(this.input);
616
+ this.use_default_events(this.input);
617
+ this.input.addEventListener("input", () => this.notify_change());
618
+ }
619
+
620
+ get_value()
621
+ {
622
+ return this.input.value;
623
+ }
624
+
625
+ set_value(value)
626
+ {
627
+ this.input.value = value;
628
+ }
435
629
  }
436
630
 
437
631
  /**
@@ -473,13 +667,14 @@ class Node extends VisualElement
473
667
  * @member {number}
474
668
  */
475
669
  this.y = 0;
670
+ this.dirty = true;
476
671
  }
477
672
 
478
673
  /**
479
674
  * Sets control values for inputs
480
675
  * @param {object} data Object with input names as keys and associated values
481
676
  */
482
- set_input_value(data)
677
+ set_input_data(data)
483
678
  {
484
679
  for ( let [k, v] of Object.entries(this.inputs) )
485
680
  {
@@ -492,7 +687,7 @@ class Node extends VisualElement
492
687
  * Gets values from the inputs
493
688
  * @returns {object} Object with input names as keys and associated values
494
689
  */
495
- get_input_value()
690
+ get_input_data()
496
691
  {
497
692
  let data = {};
498
693
  for ( let [k, v] of Object.entries(this.inputs) )
@@ -526,10 +721,11 @@ class Node extends VisualElement
526
721
  * @param {string} label User-visible label / title
527
722
  * @param {Socket?} socket Optional socket for incoming connectors
528
723
  * @param {HTMLInputElement?} control Optional control to set the value without a connection
724
+ * @param {boolean} multiple Whether this input accepts multiple incoming connections
529
725
  */
530
- add_input(name, title, socket, control)
726
+ add_input(name, title, socket, control, multiple)
531
727
  {
532
- this.inputs[name] = new NodeInput(this, name, title, socket, control);
728
+ this.inputs[name] = new NodeInput(this, name, title, socket, control, multiple);
533
729
  }
534
730
 
535
731
  /**
@@ -575,6 +771,7 @@ class Node extends VisualElement
575
771
 
576
772
  evaluate()
577
773
  {
774
+ this.dirty = false;
578
775
  if ( this.type )
579
776
  this.type.evaluate_node(this);
580
777
  }
@@ -595,7 +792,7 @@ class Node extends VisualElement
595
792
  * Returns a list of all output connections
596
793
  * @returns {Connection[]}
597
794
  */
598
- all_connections()
795
+ output_connections()
599
796
  {
600
797
  return Object.values(this.outputs)
601
798
  .map(c => c.connections)
@@ -681,29 +878,37 @@ class NodeType
681
878
  class ContextMenu extends VisualElement
682
879
  {
683
880
  /**
881
+ * @param {Editor} editor
684
882
  * @param {object|NodeType[]} actions If an object,
685
883
  * keys will be items in the menu while values are the performed actions.
686
884
  * Possible values are callbacks, NodeType instances (which will create new nodes),
687
885
  * Or other actions definitions for submenus
688
886
  */
689
- constructor(actions)
887
+ constructor(editor, actions)
690
888
  {
691
889
  super();
890
+ this.editor = editor;
692
891
  this.actions = actions;
693
892
  }
694
893
 
695
- to_dom(editor)
894
+ to_dom()
696
895
  {
697
- this.dom = this.make_menu("dgl-menu", this.actions, editor);
698
- this.dom.style.display = "none";
896
+ this.dom = this.build();
699
897
  return this.dom;
700
898
  }
701
899
 
702
- make_action(title, action, editor)
900
+ build()
901
+ {
902
+ let dom = this.make_menu("dgl-menu", this.actions);
903
+ dom.style.display = "none";
904
+ return dom;
905
+ }
906
+
907
+ make_action(title, action)
703
908
  {
704
909
  let onclick = action;
705
910
  if ( action instanceof NodeType )
706
- onclick = () => editor.create_node(action);
911
+ onclick = () => this.editor.create_node(action);
707
912
 
708
913
  let item = document.createElement("li");
709
914
  item.classList.add("dgl-menu-item");
@@ -718,7 +923,7 @@ class ContextMenu extends VisualElement
718
923
  return item;
719
924
  }
720
925
 
721
- make_menu(cls, actions, editor)
926
+ make_menu(cls, actions)
722
927
  {
723
928
  let dom = document.createElement("ul");
724
929
  dom.classList.add(cls);
@@ -727,7 +932,7 @@ class ContextMenu extends VisualElement
727
932
  {
728
933
  for ( let action of actions )
729
934
  {
730
- dom.appendChild(this.make_action(action.name, action, editor));
935
+ dom.appendChild(this.make_action(action.name, action));
731
936
  }
732
937
  }
733
938
  else
@@ -735,7 +940,7 @@ class ContextMenu extends VisualElement
735
940
  for ( let [name, val] of Object.entries(actions) )
736
941
  {
737
942
  if ( Array.isArray(val) || (typeof val == "object" && !(val instanceof NodeType)) )
738
- dom.appendChild(this.make_submenu(name, val, editor));
943
+ dom.appendChild(this.make_submenu(name, val));
739
944
  else
740
945
  dom.appendChild(this.make_action(name, val));
741
946
  }
@@ -743,14 +948,14 @@ class ContextMenu extends VisualElement
743
948
  return dom;
744
949
  }
745
950
 
746
- make_submenu(name, actions, editor)
951
+ make_submenu(name, actions)
747
952
  {
748
953
  let item = document.createElement("li");
749
954
  item.classList.add("dgl-menu-item");
750
955
  let label = item.appendChild(document.createElement("span"));
751
956
  label.textContent = name;
752
957
  label.classList.add("dgl-submenu-label");
753
- item.appendChild(this.make_menu("dgl-submenu", actions, editor));
958
+ item.appendChild(this.make_menu("dgl-submenu", actions));
754
959
  return item;
755
960
  }
756
961
 
@@ -773,6 +978,18 @@ class ContextMenu extends VisualElement
773
978
  {
774
979
  this.dom.style.display = "none";
775
980
  }
981
+
982
+ /**
983
+ * Rebuilds the DOM element
984
+ */
985
+ rebuild(editor)
986
+ {
987
+ let parent = this.dom.parentNode;
988
+ let old_e = this.dom;
989
+ let new_e = this.build(editor);
990
+ parent.replaceChild(new_e, old_e);
991
+ this.dom = new_e;
992
+ }
776
993
  }
777
994
 
778
995
  /**
@@ -788,6 +1005,7 @@ class Editor
788
1005
  constructor(container, context_menu={})
789
1006
  {
790
1007
  this.nodes = [];
1008
+ this.type_registry = {};
791
1009
 
792
1010
  this.container = container;
793
1011
  if ( !container.hasAttribute("tabindex") )
@@ -826,25 +1044,142 @@ class Editor
826
1044
  this.container.addEventListener("wheel", this._on_area_wheel.bind(this));
827
1045
  this.container.addEventListener("contextmenu", this._on_area_menu.bind(this));
828
1046
 
829
- this.context_menu = new ContextMenu(context_menu);
830
- this.node_menu = new ContextMenu({
1047
+ this.context_menu = new ContextMenu(this, context_menu);
1048
+ this.node_menu = new ContextMenu(this, {
831
1049
  "Delete": () => {
832
1050
  if ( this.selected_node )
833
1051
  this.remove_node(this.selected_node);
834
1052
  },
835
1053
  "Duplicate": () => {
836
1054
  if ( this.selected_node ) {
837
- 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());
838
1056
  this._select_node(node);
839
1057
 
840
1058
  }
841
1059
  }
842
1060
  });
843
1061
 
844
- this.container.appendChild(this.node_menu.to_dom(this));
845
- 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);
846
1087
  }
847
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();
1182
+ }
848
1183
 
849
1184
  /**
850
1185
  * Adds a new node to the graph
@@ -852,8 +1187,6 @@ class Editor
852
1187
  */
853
1188
  add_node(node)
854
1189
  {
855
- node.x = this.cursor_x;
856
- node.y = this.cursor_y;
857
1190
  this.nodes.push(node);
858
1191
  let node_dom = node.to_dom();
859
1192
  this.area.appendChild(node_dom);
@@ -865,11 +1198,30 @@ class Editor
865
1198
  {
866
1199
  if ( input.control )
867
1200
  {
868
- input.control.addEventListener("change", () => this._refresh_graph());
1201
+ input.control.on_change = () => {
1202
+ node.dirty = true;
1203
+ this._refresh_graph();
1204
+ };
869
1205
  }
870
1206
  }
871
1207
  }
872
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
+
873
1225
  /**
874
1226
  * Removes a node from the graph
875
1227
  * @param {Node} node
@@ -896,8 +1248,11 @@ class Editor
896
1248
  create_node(type, data={})
897
1249
  {
898
1250
  let node = type.create_node();
899
- node.set_input_value(data);
1251
+ node.x = this.cursor_x;
1252
+ node.y = this.cursor_y;
900
1253
  this.add_node(node);
1254
+ node.set_input_data(data);
1255
+ this._refresh_graph();
901
1256
  return node;
902
1257
  }
903
1258
 
@@ -1001,6 +1356,17 @@ class Editor
1001
1356
  this.fit_view();
1002
1357
  }
1003
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
+
1004
1370
  /**
1005
1371
  * Pan and zoom to fit all the nodes
1006
1372
  * @param {number} margin Margin to leave around the area
@@ -1008,28 +1374,38 @@ class Editor
1008
1374
  fit_view(margin=20)
1009
1375
  {
1010
1376
  if ( this.nodes.length == 0 )
1377
+ {
1378
+ this.reset_view();
1011
1379
  return;
1380
+ }
1012
1381
 
1013
1382
  let left = Infinity;
1014
1383
  let right = -Infinity;
1015
1384
  let top = Infinity;
1016
1385
  let bottom = -Infinity;
1017
1386
 
1387
+ let crect = this.container.getBoundingClientRect();
1388
+
1018
1389
  for ( let node of this.nodes )
1019
1390
  {
1020
- 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
+
1021
1399
  if ( rect.left < left ) left = rect.left;
1022
1400
  if ( rect.right > right ) right = rect.right;
1023
1401
  if ( rect.top < top ) top = rect.top;
1024
1402
  if ( rect.bottom > bottom ) bottom = rect.bottom;
1025
1403
  }
1026
1404
 
1027
- let crect = this.container.getBoundingClientRect();
1028
-
1029
- left += -margin - crect.left;
1030
- right += margin - crect.left;
1031
- top += -margin - crect.top;
1032
- bottom += margin - crect.top;
1405
+ left += -margin;
1406
+ right += margin;
1407
+ top += -margin;
1408
+ bottom += margin;
1033
1409
 
1034
1410
  let width = right - left;
1035
1411
  let height = bottom - top;
@@ -1038,12 +1414,25 @@ class Editor
1038
1414
  let scale = Math.min(x_scale, y_scale);
1039
1415
  this.scale = scale;
1040
1416
 
1041
- this.offset_x = (crect.width - width * scale) / 2 - left + margin;
1042
- 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;
1043
1419
  this._apply_transform();
1044
1420
 
1045
1421
  }
1046
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
+
1047
1436
  /**
1048
1437
  * Adds a connection to the graph
1049
1438
  * @param {Node} output_node Node the connection is coming from
@@ -1054,13 +1443,9 @@ class Editor
1054
1443
  */
1055
1444
  connect(output_node, output_name, input_node, input_name)
1056
1445
  {
1057
- 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)
1058
1447
  if ( conn )
1059
1448
  {
1060
- conn.dom = document.createElementNS("http://www.w3.org/2000/svg", "path");
1061
- conn.dom.classList.add("connection-from-" + to_slug(output_node.outputs[output_name].socket.name));
1062
- conn.dom.classList.add("connection-to-" + to_slug(input_node.inputs[input_name].socket.name));
1063
- this.connection_parent.appendChild(conn.dom);
1064
1449
  this._update_connection(conn);
1065
1450
  this._refresh_graph();
1066
1451
  }
@@ -1184,6 +1569,14 @@ class Editor
1184
1569
  this.area.style.transform = `translate(${this.offset_x}px, ${this.offset_y}px) scale(${this.scale})`;
1185
1570
  }
1186
1571
 
1572
+ /**
1573
+ * Re-evaluates all the nodes
1574
+ */
1575
+ update()
1576
+ {
1577
+ this._refresh_graph();
1578
+ }
1579
+
1187
1580
  _refresh_graph()
1188
1581
  {
1189
1582
  for ( let node of this.nodes )
@@ -1333,6 +1726,8 @@ Connection,
1333
1726
  NodeInput,
1334
1727
  NodeOutput,
1335
1728
  NodeControl,
1729
+ InputControl,
1730
+ DropDownControl,
1336
1731
  Node,
1337
1732
  NodeType,
1338
1733
  ContextMenu,