dragon-graph-lib 0.1.1 → 0.1.3

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.
@@ -5,15 +5,33 @@ function to_slug(string)
5
5
  return string.replace(/[^a-z0-9_-]+/ig, "-").toLowerCase();
6
6
  }
7
7
 
8
+ /**
9
+ * Connection socket
10
+ *
11
+ * This is what allows nodes to be connected between each other,
12
+ * it provides simple type checking but it can be customized for more complex cases.
13
+ */
8
14
  class Socket
9
15
  {
16
+ /**
17
+ * @param {string} name Socket type name
18
+ * @param {boolean} multi_input (For input sockets) whether it should accept multiple incoming connections
19
+ */
10
20
  constructor(name, multi_input=false)
11
21
  {
22
+ /**
23
+ * Socket type name
24
+ * @member {string}
25
+ */
12
26
  this.name = name;
13
27
  this.multi_input = multi_input;
14
28
  this.accepts_incoming = new Set();
15
29
  }
16
30
 
31
+ /**
32
+ * Allows incoming connections from the given type
33
+ * @param {Socket|string} type Type name or Socket instance
34
+ */
17
35
  accept(type)
18
36
  {
19
37
  let type_name = type instanceof Socket ? type.name : type;
@@ -27,16 +45,41 @@ class Socket
27
45
  return elem;
28
46
  }
29
47
 
48
+ /**
49
+ * Checks whether an output sockets allows a connection to the given input socket
50
+ *
51
+ * The default implementation defers to the input socket
52
+ *
53
+ * @param {Socket} input_socket
54
+ * @returns {boolean}
55
+ */
30
56
  output_can_connect(input_socket)
31
57
  {
32
58
  return input_socket.input_can_connect(this);
33
59
  }
34
60
 
61
+ /**
62
+ * Checks whether an input sockets allows a connection from the given output socket
63
+ *
64
+ * The default implementation checks types based on name (same or added with accept())
65
+ *
66
+ * @param {Socket} output_socket
67
+ * @returns {boolean}
68
+ */
35
69
  input_can_connect(output_socket)
36
70
  {
37
71
  return output_socket.name == this.name || this.accepts_incoming.has(output_socket.name);
38
72
  }
39
73
 
74
+ /**
75
+ * Checks whether two sockets allow connections between them
76
+ *
77
+ * Equivalent to output_socket.output_can_connect(input_socket)
78
+ *
79
+ * @param {Socket} output_socket
80
+ * @param {Socket} input_socket
81
+ * @returns {boolean}
82
+ */
40
83
  static can_connect(output_socket, input_socket)
41
84
  {
42
85
  return output_socket.output_can_connect(input_socket);
@@ -77,14 +120,27 @@ class VisualElement
77
120
  }
78
121
  }
79
122
 
123
+ /**
124
+ * Connection between two sockets
125
+ */
80
126
  class Connection extends VisualElement
81
127
  {
82
- margin = 10;
83
-
128
+ /**
129
+ * @param {NodeOutput} output Output connector
130
+ * @param {NodeInput} input Input connector
131
+ */
84
132
  constructor(output, input)
85
133
  {
86
134
  super();
135
+ /**
136
+ * Output connector
137
+ * @member {NodeOutput}
138
+ */
87
139
  this.output = output;
140
+ /**
141
+ * Input connector
142
+ * @member {NodeInput}
143
+ */
88
144
  this.input = input;
89
145
  }
90
146
 
@@ -104,7 +160,6 @@ class Connection extends VisualElement
104
160
  static path_d(x1, y1, x2, y2)
105
161
  {
106
162
  let w = x2 - x1;
107
- let h = y2 - y1;
108
163
 
109
164
  return `M ${x1} ${y1}
110
165
  C ${x1 + w / 3} ${y1}
@@ -114,29 +169,55 @@ class Connection extends VisualElement
114
169
  }
115
170
  }
116
171
 
117
- class NodeIO extends VisualElement
172
+ /**
173
+ * Base connector class
174
+ */
175
+ class Connector extends VisualElement
118
176
  {
119
177
  constructor(node, name, label, socket)
120
178
  {
121
179
  super();
180
+ /**
181
+ * Node this connector is part of
182
+ * @member {Node}
183
+ */
122
184
  this.node = node;
185
+ /**
186
+ * Name of the connector within the node
187
+ * @member {string}
188
+ */
123
189
  this.name = name;
190
+ /**
191
+ * User-visible label / title
192
+ * @member {NodeOutput}
193
+ */
124
194
  this.label = label;
195
+ /**
196
+ * Associated socket
197
+ * @member {Socket?}
198
+ */
125
199
  this.socket = socket;
200
+ /**
201
+ * Active connections
202
+ * @member {Connection[]}
203
+ */
126
204
  this.connections = [];
127
205
  this.dom_items = {};
128
206
  }
129
207
 
130
- update_dom()
131
- {
132
- this.dom_items.label.textContent = this.label;
133
- }
134
-
208
+ /**
209
+ * Adds a connection to the connector
210
+ * @param {Connection} connection
211
+ */
135
212
  connect(connection)
136
213
  {
137
214
  this.connections.push(connection);
138
215
  }
139
216
 
217
+ /**
218
+ * Removes a connection from the connector
219
+ * @param {Connection} connection
220
+ */
140
221
  disconnect(connection)
141
222
  {
142
223
  let index = this.connections.indexOf(connection);
@@ -145,12 +226,26 @@ class NodeIO extends VisualElement
145
226
  }
146
227
  }
147
228
 
148
-
149
- class NodeInput extends NodeIO
229
+ /**
230
+ * Input connector
231
+ * @extends Connector
232
+ */
233
+ class NodeInput extends Connector
150
234
  {
235
+ /**
236
+ * @param {Node} node Node owning this connector
237
+ * @param {string} name Name of the connector within the node
238
+ * @param {string} label User-visible label / title
239
+ * @param {Socket?} socket Optional socket for incoming connectors
240
+ * @param {HTMLInputElement?} control Optional control to set the value without a connection
241
+ */
151
242
  constructor(node, name, label, socket, control)
152
243
  {
153
244
  super(node, name, label, socket);
245
+ /**
246
+ * Optional control to set the value without a connection
247
+ * @member {HTMLInputElement?}
248
+ */
154
249
  this.control = control;
155
250
  }
156
251
 
@@ -165,13 +260,10 @@ class NodeInput extends NodeIO
165
260
  container.classList.add("dgl-socketed");
166
261
  }
167
262
 
168
- this.dom_items.label = document.createElement("span");
169
- this.dom_items.label.textContent = this.name;
170
- this.dom_items.label.classList.add("dgl-label", "dgl-input-label");
171
- container.appendChild(this.dom_items.label);
172
-
173
263
  if ( this.control )
174
- container.appendChild(this.control);
264
+ container.appendChild(this.control.to_dom(this.label, this.name));
265
+ else
266
+ container.appendChild(NodeControl.make_label(this.label));
175
267
 
176
268
  return container;
177
269
  }
@@ -188,17 +280,32 @@ class NodeInput extends NodeIO
188
280
  this.connections.push(connection);
189
281
 
190
282
  if ( this.control )
191
- this.control.style.visibility = "hidden";
283
+ this.control.hide();
192
284
  }
193
285
 
286
+ /**
287
+ * Sets the control value
288
+ * @param value
289
+ */
194
290
  set_value(value)
195
291
  {
196
292
  if ( this.control )
197
293
  {
198
- this.control.value = value;
294
+ this.control.set_value(value);
199
295
  }
200
296
  }
201
297
 
298
+ /**
299
+ * Gets the input value
300
+ *
301
+ * If there's input connections, the value is based on those.
302
+ * Otherwise the value comes from the control.
303
+ *
304
+ * If there is a socket on this connector with multi_input, the returned value
305
+ * is always an array.
306
+ *
307
+ * @return {any}
308
+ */
202
309
  get_value()
203
310
  {
204
311
  if ( this.connections.length )
@@ -215,7 +322,7 @@ class NodeInput extends NodeIO
215
322
  return null;
216
323
  }
217
324
 
218
- let value = this.control.value
325
+ let value = this.control.get_value();
219
326
  if ( this.socket && this.socket.multi_input )
220
327
  return [value];
221
328
  return value;
@@ -225,12 +332,22 @@ class NodeInput extends NodeIO
225
332
  {
226
333
  super.disconnect(connection);
227
334
  if ( this.control && this.connections.length == 0 )
228
- this.control.style.visibility = "visible";
335
+ this.control.show();
229
336
  }
230
337
  }
231
338
 
232
- class NodeOutput extends NodeIO
339
+ /**
340
+ * Output connector
341
+ * @extends Connector
342
+ */
343
+ class NodeOutput extends Connector
233
344
  {
345
+ /**
346
+ * @param {Node} node Node owning this connector
347
+ * @param {string} name Name of the connector within the node
348
+ * @param {string} label User-visible label / title
349
+ * @param {Socket} socket Socket for outgoing connectors
350
+ */
234
351
  constructor(node, name, label, socket)
235
352
  {
236
353
  super(node, name, label, socket);
@@ -251,19 +368,33 @@ class NodeOutput extends NodeIO
251
368
  return container;
252
369
  }
253
370
 
371
+ /**
372
+ * Clears the stored value.
373
+ *
374
+ * Will force re-evaluation of the node on get_value()
375
+ */
254
376
  clear_value()
255
377
  {
256
378
  this.value = undefined;
257
379
  }
258
380
 
381
+ /**
382
+ * Sets value for outgoing connections
383
+ * @param value
384
+ */
259
385
  set_value(value)
260
386
  {
261
387
  this.value = value;
262
388
  }
263
389
 
390
+ /**
391
+ * Gets the output value
392
+ *
393
+ * Used by outgoing connections. Might cause the node to be evaluated
394
+ */
264
395
  get_value()
265
396
  {
266
- if ( this.value == undefined )
397
+ if ( this.value === undefined || this.node.dirty )
267
398
  this.node.evaluate();
268
399
  return this.value;
269
400
  }
@@ -275,34 +406,248 @@ function force_default(ev)
275
406
  ev.stopPropagation();
276
407
  }
277
408
 
278
- function NodeControl(type, attrs={})
409
+ /**
410
+ * Custom control interface
411
+ */
412
+ class NodeControl
413
+ {
414
+ /**
415
+ * @param {object} config Control config
416
+ * @param {any} config.default Control default value
417
+ */
418
+ constructor(config={})
419
+ {
420
+ this.on_change = null;
421
+ this.config = config;
422
+ }
423
+
424
+ to_dom(label, name)
425
+ {
426
+ this.element = document.createElement("div");
427
+ this.element.classList.add("dgl-control-parent");
428
+ if ( label.length )
429
+ this.element.appendChild(NodeControl.make_label(label));
430
+ this.setup_dom(this.element, name);
431
+ if ( "default" in this.config )
432
+ this.set_value(this.config.default);
433
+ return this.element;
434
+ }
435
+
436
+ /**
437
+ * Sets up the DOM
438
+ * @param {HTMLElement} parent Element to add additional content to
439
+ * @param {name} name Control name
440
+ */
441
+ setup_dom(_parent, _name)
442
+ {
443
+ }
444
+
445
+ /**
446
+ * Dispatches the change event to update the graph
447
+ */
448
+ notify_change()
449
+ {
450
+ this.on_change();
451
+ }
452
+
453
+ /**
454
+ * Override to return the control value
455
+ * @returns {any} value
456
+ */
457
+ get_value()
458
+ {
459
+ }
460
+
461
+ /**
462
+ * Override to return set a value to the control
463
+ * @param {any} value
464
+ */
465
+ set_value(_value)
466
+ {
467
+ }
468
+
469
+ /**
470
+ * Ensures an element uses the default mouse interactions instead of
471
+ * propagating to the node editor
472
+ */
473
+ use_default_events(element)
474
+ {
475
+ element.addEventListener("mousedown", force_default);
476
+ element.addEventListener("mouseup", force_default);
477
+ element.addEventListener("mousemove", force_default);
478
+ element.addEventListener("wheel", force_default);
479
+ }
480
+
481
+ /**
482
+ * Creates a label element
483
+ * @param {string} label Text to display
484
+ */
485
+ static make_label(label)
486
+ {
487
+ let elem = document.createElement("span");
488
+ elem.textContent = label;
489
+ elem.classList.add("dgl-label", "dgl-input-label");
490
+ return elem;
491
+ }
492
+
493
+ set_visibility(value)
494
+ {
495
+ for ( let ch of this.element.children )
496
+ if ( !ch.classList.contains("dgl-label") )
497
+ ch.style.visibility = value;
498
+ }
499
+
500
+ /**
501
+ * Hides the control when an input connection is attached
502
+ */
503
+ hide()
504
+ {
505
+ this.set_visibility("hidden");
506
+ }
507
+
508
+ /**
509
+ * Shows the control when an input connection is detached
510
+ */
511
+ show()
512
+ {
513
+ this.set_visibility("visible");
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Control for simple input elements
519
+ */
520
+ class InputControl extends NodeControl
279
521
  {
280
- let elem = document.createElement("input");
281
- elem.type = type;
282
- elem.classList.add("dgl-control");
283
- for ( let [k, v] of Object.entries(attrs) )
284
- elem.setAttribute(k, v);
285
- elem.addEventListener("mousedown", force_default);
286
- elem.addEventListener("mouseup", force_default);
287
- elem.addEventListener("mousemove", force_default);
288
- elem.addEventListener("wheel", force_default);
289
- return elem;
522
+ /**
523
+ * @param {object} config Control config
524
+ * @param {string} config.type Input type
525
+ * @param {object} config.attrs Input attributes
526
+ * @param {any} config.default Default value
527
+ */
528
+ constructor(config={type: "text"})
529
+ {
530
+ super(config);
531
+ }
532
+
533
+ setup_dom(parent, name)
534
+ {
535
+ this.input = document.createElement("input");
536
+ parent.appendChild(this.input);
537
+ this.input.type = this.config.type;
538
+ this.input.classList.add("dgl-control");
539
+ for ( let [k, v] of Object.entries(this.config.attrs ?? {}) )
540
+ this.input.setAttribute(k, v);
541
+ this.use_default_events(this.input);
542
+ this.input.name = name;
543
+ this.input.addEventListener("input", () => this.notify_change());
544
+ }
545
+
546
+ get_value()
547
+ {
548
+ if ( this.config.type == "checkbox" )
549
+ return this.input.checked;
550
+ return this.input.value;
551
+ }
552
+
553
+ set_value(value)
554
+ {
555
+ if ( this.config.type == "checkbox" )
556
+ this.input.checked = value;
557
+ else
558
+ this.input.value = value;
559
+ }
560
+ }
561
+
562
+ /**
563
+ * Drop-down Control
564
+ */
565
+ class DropDownControl extends NodeControl
566
+ {
567
+
568
+ /**
569
+ * @param {object} config Control config
570
+ * @param {Array.<string, string>} config.options Drop down options ([value, display])
571
+ * @param {any} config.default Default value
572
+ */
573
+ constructor(config={options: []})
574
+ {
575
+ super(config);
576
+ }
577
+
578
+ setup_dom(parent, name)
579
+ {
580
+ this.input = document.createElement("select");
581
+ for ( let [value, display] of this.config.options )
582
+ {
583
+ let option = document.createElement("option");
584
+ this.input.appendChild(option);
585
+ option.value = value;
586
+ option.textContent = display ?? value;
587
+ }
588
+ this.input.name = name;
589
+ parent.appendChild(this.input);
590
+ this.use_default_events(this.input);
591
+ this.input.addEventListener("input", () => this.notify_change());
592
+ }
593
+
594
+ get_value()
595
+ {
596
+ return this.input.value;
597
+ }
598
+
599
+ set_value(value)
600
+ {
601
+ this.input.value = value;
602
+ }
290
603
  }
291
604
 
605
+ /**
606
+ * Main node class
607
+ */
292
608
  class Node extends VisualElement
293
609
  {
610
+ /**
611
+ * @param {string} title Node title, shown to the user
612
+ * @param {NodeType?} type Type controlling the node
613
+ */
294
614
  constructor(title, type)
295
615
  {
296
616
  super();
297
617
  this.title = title;
618
+ /**
619
+ * Registered outputs
620
+ * @member {Object.<string, NodeOutput>}
621
+ */
298
622
  this.outputs = {};
623
+ /**
624
+ * Registered inputs
625
+ * @member {Object.<string, NodeInput>}
626
+ */
299
627
  this.inputs = {};
628
+ /**
629
+ * Element that can be used to preview the node effect on the editor
630
+ * @member {HTMLElement}
631
+ */
300
632
  this.preview = null;
301
633
  this.type = type;
634
+ /**
635
+ * X position in the editor
636
+ * @member {number}
637
+ */
302
638
  this.x = 0;
639
+ /**
640
+ * Y position in the editor
641
+ * @member {number}
642
+ */
303
643
  this.y = 0;
644
+ this.dirty = true;
304
645
  }
305
646
 
647
+ /**
648
+ * Sets control values for inputs
649
+ * @param {object} data Object with input names as keys and associated values
650
+ */
306
651
  set_input_value(data)
307
652
  {
308
653
  for ( let [k, v] of Object.entries(this.inputs) )
@@ -312,6 +657,10 @@ class Node extends VisualElement
312
657
  }
313
658
  }
314
659
 
660
+ /**
661
+ * Gets values from the inputs
662
+ * @returns {object} Object with input names as keys and associated values
663
+ */
315
664
  get_input_value()
316
665
  {
317
666
  let data = {};
@@ -337,11 +686,30 @@ class Node extends VisualElement
337
686
  return connection;
338
687
  }
339
688
 
689
+ /**
690
+ * Adds a new input
691
+ *
692
+ * The resulting input will be accessible from this.inputs[name]
693
+ *
694
+ * @param {string} name Name of the connector within the node
695
+ * @param {string} label User-visible label / title
696
+ * @param {Socket?} socket Optional socket for incoming connectors
697
+ * @param {HTMLInputElement?} control Optional control to set the value without a connection
698
+ */
340
699
  add_input(name, title, socket, control)
341
700
  {
342
701
  this.inputs[name] = new NodeInput(this, name, title, socket, control);
343
702
  }
344
703
 
704
+ /**
705
+ * Adds a new output
706
+ *
707
+ * The resulting input will be accessible from this.outputs[name]
708
+ *
709
+ * @param {string} name Name of the connector within the node
710
+ * @param {string} label User-visible label / title
711
+ * @param {Socket} socket Socket for outgoing connectors
712
+ */
345
713
  add_output(name, title, socket)
346
714
  {
347
715
  this.outputs[name] = new NodeOutput(this, name, title, socket);
@@ -349,7 +717,8 @@ class Node extends VisualElement
349
717
 
350
718
  update_dom()
351
719
  {
352
- this.dom.style.transform = `translate(${this.x}px, ${this.y}px`;
720
+ this.dom.style.left = `${this.x}px`;
721
+ this.dom.style.top = `${this.y}px`;
353
722
  }
354
723
 
355
724
  build_dom()
@@ -375,10 +744,39 @@ class Node extends VisualElement
375
744
 
376
745
  evaluate()
377
746
  {
747
+ this.dirty = false;
378
748
  if ( this.type )
379
749
  this.type.evaluate_node(this);
380
750
  }
381
751
 
752
+ /**
753
+ * Returns a list of all input connections
754
+ * @returns {Connection[]}
755
+ */
756
+ input_connections()
757
+ {
758
+ return Object.values(this.inputs)
759
+ .map(c => c.connections)
760
+ .reduce((a, b) => a.concat(b), [])
761
+ ;
762
+ }
763
+
764
+ /**
765
+ * Returns a list of all output connections
766
+ * @returns {Connection[]}
767
+ */
768
+ all_connections()
769
+ {
770
+ return Object.values(this.outputs)
771
+ .map(c => c.connections)
772
+ .reduce((a, b) => a.concat(b), [])
773
+ ;
774
+ }
775
+
776
+ /**
777
+ * Returns a list of all connections (both input and output)
778
+ * @returns {Connection[]}
779
+ */
382
780
  all_connections()
383
781
  {
384
782
  return Object.values(this.inputs).concat(Object.values(this.outputs))
@@ -388,13 +786,32 @@ class Node extends VisualElement
388
786
  }
389
787
  }
390
788
 
789
+ /**
790
+ * Node type
791
+ *
792
+ * This class is responsible for creating nodes, and adding logic for output values
793
+ */
391
794
  class NodeType
392
795
  {
796
+ /**
797
+ * @param {string} name Type name
798
+ */
393
799
  constructor(name)
394
800
  {
801
+ /**
802
+ * Type name
803
+ * @member {string}
804
+ */
395
805
  this.name = name;
396
806
  }
397
807
 
808
+ /**
809
+ * Creates and sets up a node for this type
810
+ *
811
+ * Custom types should override populate_node() to add connectors or otherwise customize the node
812
+ *
813
+ * @returns Node
814
+ */
398
815
  create_node()
399
816
  {
400
817
  let node = new Node(this.name, this);
@@ -402,23 +819,43 @@ class NodeType
402
819
  return node;
403
820
  }
404
821
 
405
- populate_node(node)
822
+ /**
823
+ * Allows custom types to add connectors or otherwise customize the node
824
+ * @protected
825
+ * @param {Node} node
826
+ */
827
+ populate_node(_node)
406
828
  {
407
829
  }
408
830
 
409
- update_node(node)
410
- {
411
-
412
- }
413
-
414
- evaluate_node(node)
831
+ /**
832
+ * Evaluates node logic
833
+ *
834
+ * Generally you should get input values with `node.inputs[name].get_value()`
835
+ * And set output values with `node.outputs[name].set_value()`.
836
+ *
837
+ * You can also alter the `node.preview` to add custom HTML to the node
838
+ * based on these values
839
+ *
840
+ * @param {Node} node
841
+ */
842
+ evaluate_node(_node)
415
843
  {
416
844
 
417
845
  }
418
846
  }
419
847
 
848
+ /**
849
+ * Context menu for the node editor
850
+ */
420
851
  class ContextMenu extends VisualElement
421
852
  {
853
+ /**
854
+ * @param {object|NodeType[]} actions If an object,
855
+ * keys will be items in the menu while values are the performed actions.
856
+ * Possible values are callbacks, NodeType instances (which will create new nodes),
857
+ * Or other actions definitions for submenus
858
+ */
422
859
  constructor(actions)
423
860
  {
424
861
  super();
@@ -427,11 +864,17 @@ class ContextMenu extends VisualElement
427
864
 
428
865
  to_dom(editor)
429
866
  {
430
- this.dom = this.make_menu("dgl-menu", this.actions, editor);
431
- this.dom.style.display = "none";
867
+ this.dom = this.build(editor);
432
868
  return this.dom;
433
869
  }
434
870
 
871
+ build(editor)
872
+ {
873
+ let dom = this.make_menu("dgl-menu", this.actions, editor);
874
+ dom.style.display = "none";
875
+ return dom;
876
+ }
877
+
435
878
  make_action(title, action, editor)
436
879
  {
437
880
  let onclick = action;
@@ -487,6 +930,11 @@ class ContextMenu extends VisualElement
487
930
  return item;
488
931
  }
489
932
 
933
+ /**
934
+ * Opens the menu at the given point
935
+ * @param {number} x
936
+ * @param {number} y
937
+ */
490
938
  show(x, y)
491
939
  {
492
940
  this.dom.style.display = "";
@@ -494,19 +942,44 @@ class ContextMenu extends VisualElement
494
942
  this.dom.style.top = y + "px";
495
943
  }
496
944
 
945
+ /**
946
+ * Closes the menu
947
+ */
497
948
  close()
498
949
  {
499
950
  this.dom.style.display = "none";
500
951
  }
952
+
953
+ /**
954
+ * Rebuilds the DOM element
955
+ */
956
+ rebuild(editor)
957
+ {
958
+ let parent = this.dom.parentNode;
959
+ let old_e = this.dom;
960
+ let new_e = this.build(editor);
961
+ parent.replaceChild(new_e, old_e);
962
+ this.dom = new_e;
963
+ }
501
964
  }
502
965
 
966
+ /**
967
+ * Node Editor
968
+ */
503
969
  class Editor
504
970
  {
971
+
972
+ /**
973
+ * @param {HTMLElement} container Parent element in the DOM
974
+ * @param {object|NodeType[]} Actions for the context menu (See ContextMenu)
975
+ */
505
976
  constructor(container, context_menu={})
506
977
  {
507
978
  this.nodes = [];
508
979
 
509
980
  this.container = container;
981
+ if ( !container.hasAttribute("tabindex") )
982
+ container.setAttribute("tabindex", "0");
510
983
 
511
984
  this.area = container.appendChild(document.createElement("div"));
512
985
  container.classList.add("dgl-editor");
@@ -532,6 +1005,12 @@ class Editor
532
1005
  this.container.addEventListener("mousedown", this._on_area_mouse_down.bind(this));
533
1006
  this.container.addEventListener("mousemove", this._on_area_mouse_move.bind(this));
534
1007
  this.container.addEventListener("mouseup", this._on_area_mouse_up.bind(this));
1008
+
1009
+ this.container.addEventListener("touchstart", this._on_area_mouse_down.bind(this));
1010
+ this.container.addEventListener("touchmove", this._on_area_mouse_move.bind(this));
1011
+ this.container.addEventListener("touchend", this._on_area_mouse_up.bind(this));
1012
+ this.container.addEventListener("touchcancel", this._on_area_mouse_up.bind(this));
1013
+
535
1014
  this.container.addEventListener("wheel", this._on_area_wheel.bind(this));
536
1015
  this.container.addEventListener("contextmenu", this._on_area_menu.bind(this));
537
1016
 
@@ -544,8 +1023,6 @@ class Editor
544
1023
  "Duplicate": () => {
545
1024
  if ( this.selected_node ) {
546
1025
  let node = this.create_node(this.selected_node.type, this.selected_node.get_input_value());
547
- node.x = this.selected_node.x;
548
- node.y = this.selected_node.y;
549
1026
  this._select_node(node);
550
1027
 
551
1028
  }
@@ -556,10 +1033,12 @@ class Editor
556
1033
  this.container.appendChild(this.context_menu.to_dom(this));
557
1034
  }
558
1035
 
559
- create_node(type, data={})
1036
+ /**
1037
+ * Adds a new node to the graph
1038
+ * @param {Node} node
1039
+ */
1040
+ add_node(node)
560
1041
  {
561
- let node = type.create_node();
562
- node.set_input_value(data);
563
1042
  node.x = this.cursor_x;
564
1043
  node.y = this.cursor_y;
565
1044
  this.nodes.push(node);
@@ -567,11 +1046,57 @@ class Editor
567
1046
  this.area.appendChild(node_dom);
568
1047
 
569
1048
  node_dom.addEventListener("mousedown", ev => this._on_node_mouse_down(ev, node));
1049
+ node_dom.addEventListener("touchstart", ev => this._on_node_mouse_down(ev, node));
570
1050
  node_dom.addEventListener("contextmenu", ev => this._on_node_menu(ev, node));
1051
+ for ( let input of Object.values(node.inputs) )
1052
+ {
1053
+ if ( input.control )
1054
+ {
1055
+ input.control.on_change = () => {
1056
+ node.dirty = true;
1057
+ this._refresh_graph();
1058
+ };
1059
+ }
1060
+ }
1061
+ }
1062
+
1063
+ /**
1064
+ * Removes a node from the graph
1065
+ * @param {Node} node
1066
+ */
1067
+ remove_node(node)
1068
+ {
1069
+ for ( let conn of node.all_connections() )
1070
+ conn.disconnect();
571
1071
 
1072
+ let ind = this.nodes.indexOf(node);
1073
+ if ( ind != -1 )
1074
+ this.nodes.splice(ind, 1);
1075
+ node.destroy();
1076
+
1077
+ this._refresh_graph();
1078
+ }
1079
+
1080
+ /**
1081
+ * Creates a new node from the given type
1082
+ * @param {NodeType} type
1083
+ * @param {object} data Optional data to populate node inputs
1084
+ * @returns {Node} The created node
1085
+ */
1086
+ create_node(type, data={})
1087
+ {
1088
+ let node = type.create_node();
1089
+ this.add_node(node);
1090
+ node.set_input_value(data);
1091
+ this._refresh_graph();
572
1092
  return node;
573
1093
  }
574
1094
 
1095
+ /**
1096
+ * Automatically lays out the graph
1097
+ *
1098
+ * It assumes all nodes and connections are set up
1099
+ */
575
1100
  auto_layout()
576
1101
  {
577
1102
  setTimeout(() => this._on_auto_layout());
@@ -664,8 +1189,60 @@ class Editor
664
1189
  }
665
1190
 
666
1191
  this._refresh_graph();
1192
+ this.fit_view();
667
1193
  }
668
1194
 
1195
+ /**
1196
+ * Pan and zoom to fit all the nodes
1197
+ * @param {number} margin Margin to leave around the area
1198
+ */
1199
+ fit_view(margin=20)
1200
+ {
1201
+ if ( this.nodes.length == 0 )
1202
+ return;
1203
+
1204
+ let left = Infinity;
1205
+ let right = -Infinity;
1206
+ let top = Infinity;
1207
+ let bottom = -Infinity;
1208
+
1209
+ for ( let node of this.nodes )
1210
+ {
1211
+ let rect = node.dom.getBoundingClientRect();
1212
+ if ( rect.left < left ) left = rect.left;
1213
+ if ( rect.right > right ) right = rect.right;
1214
+ if ( rect.top < top ) top = rect.top;
1215
+ if ( rect.bottom > bottom ) bottom = rect.bottom;
1216
+ }
1217
+
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;
1224
+
1225
+ let width = right - left;
1226
+ let height = bottom - top;
1227
+ let x_scale = crect.width / width;
1228
+ let y_scale = crect.height / height;
1229
+ let scale = Math.min(x_scale, y_scale);
1230
+ this.scale = scale;
1231
+
1232
+ this.offset_x = (crect.width - width * scale) / 2 - left + margin;
1233
+ this.offset_y = (crect.height - height * scale) / 2 - top + margin;
1234
+ this._apply_transform();
1235
+
1236
+ }
1237
+
1238
+ /**
1239
+ * Adds a connection to the graph
1240
+ * @param {Node} output_node Node the connection is coming from
1241
+ * @param {string} output_name Name of the output connector within output_node
1242
+ * @param {Node} input_node Node the connection is going to
1243
+ * @param {string} input_name Name of the input connector within input_node
1244
+ * @return {Connection?} Connection (Or null if the connection is impossible)
1245
+ */
669
1246
  connect(output_node, output_name, input_node, input_name)
670
1247
  {
671
1248
  let conn = output_node.connect(output_name, input_node, input_name);
@@ -681,6 +1258,10 @@ class Editor
681
1258
  return conn;
682
1259
  }
683
1260
 
1261
+ /**
1262
+ * Removes a connection from the graph
1263
+ * @param {Connection} connection
1264
+ */
684
1265
  disconnect(connection)
685
1266
  {
686
1267
  connection.disconnect();
@@ -700,18 +1281,27 @@ class Editor
700
1281
  this.node_menu.close();
701
1282
  }
702
1283
 
1284
+ _touch_mouse_pos(ev)
1285
+ {
1286
+ if ( ev.changedTouches )
1287
+ return ev.changedTouches[0];
1288
+ return ev;
1289
+ }
1290
+
703
1291
  _on_area_mouse_down(ev)
704
1292
  {
1293
+ this.container.focus();
705
1294
  this._close_menus();
706
1295
  if ( ev.button == 0 )
707
1296
  {
708
1297
  this._select_node(null);
709
1298
  }
710
- else if ( ev.button == 1 )
1299
+ else if ( ev.button == 1 || ev.changedTouches )
711
1300
  {
712
1301
  let rect = this.container.getBoundingClientRect();
713
- this.drag_start_x = ev.clientX - rect.left;
714
- this.drag_start_y = ev.clientY - rect.top;
1302
+ let evp = this._touch_mouse_pos(ev);
1303
+ this.drag_start_x = evp.clientX - rect.left;
1304
+ this.drag_start_y = evp.clientY - rect.top;
715
1305
  this.drag_start_ox = this.offset_x;
716
1306
  this.drag_start_oy = this.offset_y;
717
1307
  this.drag = "pan";
@@ -737,8 +1327,9 @@ class Editor
737
1327
  _on_area_mouse_move(ev)
738
1328
  {
739
1329
  let rect = this.container.getBoundingClientRect();
740
- let drag_x = ev.clientX - rect.left;
741
- let drag_y = ev.clientY - rect.top;
1330
+ let evp = this._touch_mouse_pos(ev);
1331
+ let drag_x = evp.clientX - rect.left;
1332
+ let drag_y = evp.clientY - rect.top;
742
1333
  this.cursor_x = (drag_x - this.offset_x) / this.scale;
743
1334
  this.cursor_y = (drag_y - this.offset_y) / this.scale;
744
1335
 
@@ -758,7 +1349,8 @@ class Editor
758
1349
  let y = this.drag_start_oy + delta_y / this.scale;
759
1350
  this.selected_node.x = x;
760
1351
  this.selected_node.y = y;
761
- this.selected_node.dom.style.transform = `translate(${x}px, ${y}px`;
1352
+ this.selected_node.dom.style.left = `${x}px`;
1353
+ this.selected_node.dom.style.top = `${y}px`;
762
1354
  for ( let connection of this.selected_node.all_connections() )
763
1355
  this._update_connection(connection);
764
1356
  }
@@ -803,7 +1395,8 @@ class Editor
803
1395
 
804
1396
  _get_dest_connector(ev)
805
1397
  {
806
- let elem = document.elementFromPoint(ev.clientX, ev.clientY);
1398
+ let evp = this._touch_mouse_pos(ev);
1399
+ let elem = document.elementFromPoint(evp.clientX, evp.clientY);
807
1400
  if ( !elem.classList.contains("dgl-socket") )
808
1401
  return null;
809
1402
 
@@ -845,6 +1438,9 @@ class Editor
845
1438
  ev.preventDefault();
846
1439
  ev.stopPropagation();
847
1440
 
1441
+ if ( !this.container.contains(document.activeElement) )
1442
+ return;
1443
+
848
1444
  let rect = this.container.getBoundingClientRect();
849
1445
  let mouse_x = ev.clientX - rect.left;
850
1446
  let mouse_y = ev.clientY - rect.top;
@@ -870,15 +1466,17 @@ class Editor
870
1466
 
871
1467
  _on_node_mouse_down(ev, node)
872
1468
  {
1469
+ // this.container.focus();
873
1470
  this._close_menus();
874
- if ( ev.button == 0 )
1471
+ if ( ev.button == 0 || ev.changedTouches )
875
1472
  {
876
1473
  ev.stopPropagation();
877
1474
  this._select_node(node);
878
1475
  this.drag = "node";
879
1476
  let rect = this.container.getBoundingClientRect();
880
- this.drag_start_x = ev.clientX - rect.left;
881
- this.drag_start_y = ev.clientY - rect.top;
1477
+ let evp = this._touch_mouse_pos(ev);
1478
+ this.drag_start_x = evp.clientX - rect.left;
1479
+ this.drag_start_y = evp.clientY - rect.top;
882
1480
  this.drag_start_ox = node.x;
883
1481
  this.drag_start_oy = node.y;
884
1482
  if ( ev.target.classList.contains("dgl-socket") )
@@ -926,6 +1524,8 @@ Connection,
926
1524
  NodeInput,
927
1525
  NodeOutput,
928
1526
  NodeControl,
1527
+ InputControl,
1528
+ DropDownControl,
929
1529
  Node,
930
1530
  NodeType,
931
1531
  ContextMenu,