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