lexgui 0.1.29 → 0.1.30

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.
@@ -4,43 +4,7 @@ if(!LX) {
4
4
  throw("lexgui.js missing!");
5
5
  }
6
6
 
7
- LX.components.push( 'Graph' );
8
-
9
- function flushCss(element) {
10
- // By reading the offsetHeight property, we are forcing
11
- // the browser to flush the pending CSS changes (which it
12
- // does to ensure the value obtained is accurate).
13
- element.offsetHeight;
14
- }
15
-
16
- function swapElements (obj, a, b) {
17
- [obj[a], obj[b]] = [obj[b], obj[a]];
18
- }
19
-
20
- function swapArrayElements (array, id0, id1) {
21
- [array[id0], array[id1]] = [array[id1], array[id0]];
22
- };
23
-
24
- function sliceChar(str, idx) {
25
- return str.substr(0, idx) + str.substr(idx + 1);
26
- }
27
-
28
- function firstNonspaceIndex(str) {
29
- return str.search(/\S|$/);
30
- }
31
-
32
- function deleteElement( el ) {
33
- if( el ) el.remove();
34
- }
35
-
36
- let ASYNC_ENABLED = true;
37
-
38
- function doAsync( fn, ms ) {
39
- if( ASYNC_ENABLED )
40
- setTimeout( fn, ms ?? 0 );
41
- else
42
- fn();
43
- }
7
+ LX.components.push( 'GraphEditor' );
44
8
 
45
9
  class BoundingBox {
46
10
 
@@ -50,7 +14,7 @@ class BoundingBox {
50
14
  this.size = s ?? new LX.vec2( 0, 0 );
51
15
  }
52
16
 
53
- merge ( bb ) {
17
+ merge( bb ) {
54
18
 
55
19
  console.assert( bb.constructor == BoundingBox );
56
20
 
@@ -67,7 +31,7 @@ class BoundingBox {
67
31
  this.size = merge_max.sub( merge_min );
68
32
  }
69
33
 
70
- inside ( bb ) {
34
+ inside( bb, full = true ) {
71
35
 
72
36
  const min_0 = this.origin;
73
37
  const max_0 = this.origin.add( this.size );
@@ -75,8 +39,16 @@ class BoundingBox {
75
39
  const min_1 = bb.origin;
76
40
  const max_1 = bb.origin.add( bb.size );
77
41
 
78
- return min_1.x >= min_0.x && max_1.x <= max_0.x
79
- && min_1.y >= min_0.y && max_1.y <= max_0.y;
42
+ if( full )
43
+ {
44
+ return min_1.x >= min_0.x && max_1.x <= max_0.x
45
+ && min_1.y >= min_0.y && max_1.y <= max_0.y;
46
+ }
47
+ else
48
+ {
49
+ return max_1.x >= min_0.x && min_1.x <= max_0.x
50
+ && max_1.y >= min_0.y && min_1.y <= max_0.y;
51
+ }
80
52
  }
81
53
  };
82
54
 
@@ -97,6 +69,10 @@ class GraphEditor {
97
69
  static EVENT_MOUSEWHEEL = 1;
98
70
 
99
71
  static LAST_GROUP_ID = 0;
72
+ static LAST_FUNCTION_ID = 0;
73
+
74
+ static STOPPED = 0;
75
+ static RUNNING = 1;
100
76
 
101
77
  // Node Drawing
102
78
 
@@ -114,10 +90,10 @@ class GraphEditor {
114
90
 
115
91
  GraphEditor.__instances.push( this );
116
92
 
117
- area.addSidebar( m => {
118
- m.add( "Scene", { icon: "fa fa-cube", callback: () => { codeArea.hide(); show( canvas ); } } );
119
- m.add( "Code", { icon: "fa fa-code", callback: () => { hide( canvas ); codeArea.show(); } } );
120
- m.add( "Search", { icon: "fa fa-search", bottom: true, callback: () => { } } );
93
+ const useSidebar = options.sidebar ?? true;
94
+
95
+ this._sidebar = area.addSidebar( m => {
96
+ m.add( "Create", { icon: "fa fa-add", bottom: true, callback: (e) => this._onSidebarCreate( e ) } );
121
97
  });
122
98
 
123
99
  this.base_area = area;
@@ -127,12 +103,20 @@ class GraphEditor {
127
103
 
128
104
  this.root = this.area.root;
129
105
  this.root.tabIndex = -1;
106
+
130
107
  area.attach( this.root );
131
108
 
109
+ this._graphContainer = area.sections[ 1 ].root;
110
+ this._sidebarDom = area.sections[ 0 ].root;
111
+ this._sidebarActive = useSidebar;
112
+
113
+ // Set sidebar state depending on options..
114
+ this._toggleSideBar( useSidebar );
115
+
132
116
  // Bind resize
133
117
 
134
118
  area.onresize = ( bb ) => {
135
- console.log(bb);
119
+
136
120
  };
137
121
 
138
122
  area.addOverlayButtons( [
@@ -141,27 +125,20 @@ class GraphEditor {
141
125
  icon: "fa fa-table-columns",
142
126
  callback: () => this._toggleSideBar(),
143
127
  },
144
- // [
145
- // {
146
- // name: "Select",
147
- // icon: "fa fa-arrow-pointer",
148
- // callback: (value, event) => console.log(value),
149
- // selectable: true
150
- // },
151
- // {
152
- // name: "Move",
153
- // icon: "fa-solid fa-arrows-up-down-left-right",
154
- // img: "https://webglstudio.org/latest/imgs/mini-icon-gizmo.png",
155
- // callback: (value, event) => console.log(value),
156
- // selectable: true
157
- // },
158
- // {
159
- // name: "Rotate",
160
- // icon: "fa-solid fa-rotate-right",
161
- // callback: (value, event) => console.log(value),
162
- // selectable: true
163
- // }
164
- // ],
128
+ [
129
+ {
130
+ name: "Start Graph",
131
+ icon: "fa fa-play",
132
+ callback: (value, event) => this.start(),
133
+ selectable: true
134
+ },
135
+ {
136
+ name: "Stop Graph",
137
+ icon: "fa-solid fa-stop",
138
+ callback: (value, event) => this.stop(),
139
+ selectable: true
140
+ }
141
+ ],
165
142
  [
166
143
  {
167
144
  name: "Enable Snapping",
@@ -184,9 +161,14 @@ class GraphEditor {
184
161
  {
185
162
  name: "Export",
186
163
  icon: "fa fa-diagram-project",
187
- callback: (value, event) => this.graph.export()
164
+ callback: (value, event) => this.currentGraph.export()
188
165
  }
189
- ]
166
+ ],
167
+ {
168
+ name: "",
169
+ class: "graph-title",
170
+ callback: (value, event) => this._showRenameGraphDialog()
171
+ }
190
172
  ], { float: "htc" } );
191
173
 
192
174
  this.root.addEventListener( 'keydown', this._processKeyDown.bind( this ), true );
@@ -202,7 +184,7 @@ class GraphEditor {
202
184
  this.root.addEventListener( 'focusout', this._processFocus.bind( this, false ) );
203
185
 
204
186
  this.propertiesDialog = new LX.PocketDialog( "Properties", null, {
205
- size: [ "300px", null ],
187
+ size: [ "350px", null ],
206
188
  position: [ "8px", "8px" ],
207
189
  float: "left",
208
190
  class: 'lexgraphpropdialog'
@@ -236,18 +218,17 @@ class GraphEditor {
236
218
 
237
219
  this.keys = { };
238
220
 
239
- this._sidebar = area.sections[ 0 ].root;
240
- this._sidebarActive = false;
241
-
242
221
  this._snapToGrid = false;
243
222
  this._snapValue = 1.0;
244
223
 
245
- this._scale = 1.0;
224
+ // Graphs, Nodes and connections
246
225
 
247
- // Nodes and connections
226
+ this.currentGraph = null;
248
227
 
249
- this.nodes = { };
250
- this.variables = { };
228
+ this.graphs = { };
229
+ this.nodes = { };
230
+ this.groups = { };
231
+ this.variables = { };
251
232
 
252
233
  this.selectedNodes = [ ];
253
234
 
@@ -273,7 +254,6 @@ class GraphEditor {
273
254
  // Back pattern
274
255
 
275
256
  const f = 15.0;
276
- this._patternPosition = new LX.vec2( 0, 0 );
277
257
  this._patternSize = new LX.vec2( f );
278
258
  this._circlePatternSize = f * 0.04;
279
259
  this._circlePatternColor = '#71717a9c';
@@ -393,24 +373,41 @@ class GraphEditor {
393
373
 
394
374
  setGraph( graph ) {
395
375
 
376
+ // Nothing to do, already there...
377
+ if( this.currentGraph && graph.id == this.currentGraph.id )
378
+ return;
379
+
396
380
  this.clear();
397
381
 
398
- this.graph = graph;
382
+ graph.id = graph.id ?? graph.constructor.name + '-' + LX.UTILS.uidGenerator();
399
383
 
400
- if( !this.graph.nodes )
384
+ this.graphs[ graph.id ] = graph;
385
+
386
+ if( !graph.nodes )
401
387
  {
402
388
  console.warn( 'Graph does not contain any node!' );
403
389
  return;
404
390
  }
405
391
 
406
- for( let node of this.graph.nodes )
392
+ this.currentGraph = graph;
393
+
394
+ this._updatePattern();
395
+
396
+ for( let node of graph.nodes )
407
397
  {
408
398
  this._createNodeDOM( node );
409
399
  }
410
400
 
411
- for( let linkId in this.graph.links )
401
+ for( let group of graph.groups )
402
+ {
403
+ const groupDom = this._createGroup( group );
404
+ groupDom.querySelector( '.lexgraphgrouptitle' ).value = group.name;
405
+ this._domNodes.prepend( groupDom );
406
+ }
407
+
408
+ for( let linkId in graph.links )
412
409
  {
413
- const links = this.graph.links[ linkId ];
410
+ const links = graph.links[ linkId ];
414
411
 
415
412
  for( let link of links )
416
413
  {
@@ -418,24 +415,21 @@ class GraphEditor {
418
415
  }
419
416
  }
420
417
 
421
- // TODO: REMOVE THIS (DEBUG)
422
- this.start();
423
- //
418
+ this._updateGraphName( graph.name );
419
+ this._togglePropertiesDialog( false );
424
420
  }
425
421
 
426
422
  /**
427
423
  * @method loadGraph
428
- * @param {Graph} graph
424
+ * @param {String} url
425
+ * @param {Function} callback Function to call once the graph is loaded
429
426
  */
430
427
 
431
428
  loadGraph( url, callback ) {
432
429
 
433
430
  const onComplete = ( json ) => {
434
431
 
435
- var graph = new Graph();
436
- graph.configure( json );
437
-
438
- this.setGraph( graph );
432
+ let graph = ( json.type == 'Graph' ) ? this.addGraph( json ) : this.addGraphFunction( json );
439
433
 
440
434
  if( callback )
441
435
  callback( graph );
@@ -446,6 +440,65 @@ class GraphEditor {
446
440
  LX.requestJSON( url, onComplete, onError );
447
441
  }
448
442
 
443
+ /**
444
+ * @method addGraph
445
+ * @param {Object} o Options to configure the graph
446
+ */
447
+
448
+ addGraph( o ) {
449
+
450
+ let graph = new Graph();
451
+ graph.editor = this;
452
+
453
+ if( o ) graph.configure( o );
454
+
455
+ this.setGraph( graph );
456
+
457
+ this._sidebar.add( graph.name, { icon: "fa fa-diagram-project", className: graph.id, callback: (e) => { this.setGraph( graph ) } } );
458
+
459
+ this._sidebar.select( graph.name );
460
+
461
+ return graph;
462
+ }
463
+
464
+ /**
465
+ * @method addGraphFunction
466
+ * @param {Object} o Options to configure the graph
467
+ */
468
+
469
+ addGraphFunction( o ) {
470
+
471
+ let func = new GraphFunction();
472
+ func.editor = this;
473
+
474
+ if( o ) func.configure( o );
475
+
476
+ this.setGraph( func );
477
+
478
+ // Add a new node to use this function..
479
+
480
+ class NodeFunction extends GraphNode
481
+ {
482
+ onCreate() {
483
+ this.addInput( null, "float" );
484
+ this.addOutput( null, "any" );
485
+ }
486
+
487
+ onExecute() {
488
+ const func = NodeFunction.func;
489
+ const value = func.getOutputData( this.getInput( 0 ) );
490
+ this.setOutput( 0, value );
491
+ }
492
+ }
493
+
494
+ NodeFunction.func = func;
495
+ GraphEditor.registerCustomNode( "function/" + func.name, NodeFunction );
496
+
497
+ this._sidebar.add( func.name, { icon: "fa fa-florin-sign", className: func.id, callback: (e) => { this.setGraph( func ) } } );
498
+
499
+ this._sidebar.select( func.name );
500
+ }
501
+
449
502
  /**
450
503
  * @method clear
451
504
  */
@@ -470,10 +523,10 @@ class GraphEditor {
470
523
 
471
524
  propagateEventToAllNodes( eventName, params ) {
472
525
 
473
- if( !this.graph )
526
+ if( !this.currentGraph )
474
527
  return;
475
528
 
476
- for ( let node of this.graph.nodes )
529
+ for ( let node of this.currentGraph.nodes )
477
530
  {
478
531
  if( !node[ eventName ] )
479
532
  continue;
@@ -511,6 +564,7 @@ class GraphEditor {
511
564
  _createNodeDOM( node ) {
512
565
 
513
566
  node.editor = this;
567
+ node.graphID = this.currentGraph.id;
514
568
 
515
569
  var nodeContainer = document.createElement( 'div' );
516
570
  nodeContainer.classList.add( 'lexgraphnode' );
@@ -527,6 +581,12 @@ class GraphEditor {
527
581
  const category = node.constructor.category;
528
582
  nodeContainer.classList.add( category );
529
583
  }
584
+ else
585
+ {
586
+ const pos = node.type.lastIndexOf( "/" );
587
+ const category = node.type.substring( 0, pos );
588
+ nodeContainer.classList.add( category );
589
+ }
530
590
 
531
591
  // Update with manual color
532
592
 
@@ -582,8 +642,7 @@ class GraphEditor {
582
642
  LX.addContextMenu(null, e, m => {
583
643
 
584
644
  m.add( "Copy", () => {
585
- // TODO
586
- // ...
645
+ this._clipboardData = node.id;
587
646
  } );
588
647
 
589
648
  m.add( "Paste", () => {
@@ -604,6 +663,13 @@ class GraphEditor {
604
663
  // Only for left click..
605
664
  if( e.button != LX.MOUSE_LEFT_CLICK )
606
665
  return;
666
+
667
+ // Open graph function..
668
+ if( node.constructor.func )
669
+ {
670
+ this._sidebar.select( node.constructor.func.name )
671
+ }
672
+
607
673
  } );
608
674
 
609
675
  // Title header
@@ -613,7 +679,7 @@ class GraphEditor {
613
679
  nodeContainer.appendChild( nodeHeader );
614
680
 
615
681
  // Properties
616
- // if( node.properties.length && node.constructor.category == 'inputs' )
682
+ // if( node.properties.length )
617
683
  // {
618
684
  // var nodeProperties = document.createElement( 'div' );
619
685
  // nodeProperties.classList.add( 'lexgraphnodeproperties' );
@@ -656,7 +722,7 @@ class GraphEditor {
656
722
  {
657
723
  var nodeInputs = null;
658
724
 
659
- if( node.inputs && node.inputs.length )
725
+ if( hasInputs )
660
726
  {
661
727
  nodeInputs = document.createElement( 'div' );
662
728
  nodeInputs.classList.add( 'lexgraphnodeinputs' );
@@ -678,6 +744,7 @@ class GraphEditor {
678
744
 
679
745
  var type = document.createElement( 'span' );
680
746
  type.className = 'io__type input ' + i.type;
747
+ type.dataset[ 'type' ] = i.type;
681
748
  type.innerHTML = '<span>' + i.type[ 0 ].toUpperCase() + '</span>';
682
749
  input.appendChild( type );
683
750
 
@@ -702,7 +769,7 @@ class GraphEditor {
702
769
  {
703
770
  var nodeOutputs = null;
704
771
 
705
- if( node.outputs && node.outputs.length )
772
+ if( hasOutputs )
706
773
  {
707
774
  nodeOutputs = document.createElement( 'div' );
708
775
  nodeOutputs.classList.add( 'lexgraphnodeoutputs' );
@@ -731,6 +798,7 @@ class GraphEditor {
731
798
 
732
799
  var type = document.createElement( 'span' );
733
800
  type.className = 'io__type output ' + o.type;
801
+ type.dataset[ 'type' ] = o.type;
734
802
  type.innerHTML = '<span>' + o.type[ 0 ].toUpperCase() + '</span>';
735
803
  output.appendChild( type );
736
804
 
@@ -750,6 +818,141 @@ class GraphEditor {
750
818
  onDragStart: this._onDragNode.bind( this )
751
819
  } );
752
820
 
821
+ this._addNodeIOEvents( nodeContainer );
822
+
823
+ const id = node.id ?? node.title.toLowerCase().replaceAll( /\s/g, '-' ) + '-' + LX.UTILS.uidGenerator();
824
+ this.nodes[ id ] = { data: node, dom: nodeContainer };
825
+
826
+ node.id = id;
827
+ nodeContainer.dataset[ 'id' ] = id;
828
+
829
+ this._domNodes.appendChild( nodeContainer );
830
+
831
+ // Only 1 main per graph!
832
+ if( node.title == 'Main' )
833
+ {
834
+ this.main = id;
835
+ }
836
+
837
+ node.size = new LX.vec2( nodeContainer.offsetWidth, nodeContainer.offsetHeight );
838
+
839
+ node.resizeObserver = new ResizeObserver( entries => {
840
+
841
+ for( const entry of entries ) {
842
+ const bb = entry.contentRect;
843
+ if( !bb.width || !bb.height )
844
+ continue;
845
+ node.size = new LX.vec2( nodeContainer.offsetWidth, nodeContainer.offsetHeight );
846
+ }
847
+ });
848
+
849
+ node.resizeObserver.observe( nodeContainer );
850
+
851
+ return nodeContainer;
852
+ }
853
+
854
+ _updateNodeDOMIOs( dom, node ) {
855
+
856
+ // Inputs and outputs
857
+ var nodeIO = dom.querySelector( '.lexgraphnodeios' );
858
+
859
+ const hasInputs = node.inputs && node.inputs.length;
860
+ const hasOutputs = node.outputs && node.outputs.length;
861
+
862
+ // Inputs
863
+ {
864
+ var nodeInputs = null;
865
+
866
+ if( hasInputs )
867
+ {
868
+ nodeInputs = nodeIO.querySelector( '.lexgraphnodeinputs' );
869
+ nodeInputs.innerHTML = "";
870
+ }
871
+
872
+ for( let i of node.inputs )
873
+ {
874
+ if( !i.type )
875
+ {
876
+ console.warn( `Missing type for node [${ node.title }], skipping...` );
877
+ continue;
878
+ }
879
+
880
+ var input = document.createElement( 'div' );
881
+ input.className = 'lexgraphnodeio ioinput';
882
+ input.dataset[ 'index' ] = nodeInputs.childElementCount;
883
+
884
+ var type = document.createElement( 'span' );
885
+ type.className = 'io__type input ' + i.type;
886
+ type.innerHTML = '<span>' + i.type[ 0 ].toUpperCase() + '</span>';
887
+ input.appendChild( type );
888
+
889
+ var typeDesc = document.createElement( 'span' );
890
+ typeDesc.className = 'io__typedesc input ' + i.type;
891
+ typeDesc.innerHTML = i.type;
892
+ input.appendChild( typeDesc );
893
+
894
+ if( i.name )
895
+ {
896
+ var name = document.createElement( 'span' );
897
+ name.classList.add( 'io__name' );
898
+ name.innerText = i.name;
899
+ input.appendChild( name );
900
+ }
901
+
902
+ nodeInputs.appendChild( input );
903
+ }
904
+ }
905
+
906
+ // Outputs
907
+ {
908
+ var nodeOutputs = null;
909
+
910
+ if( hasOutputs )
911
+ {
912
+ nodeOutputs = nodeIO.querySelector( '.lexgraphnodeoutputs' );
913
+ nodeOutputs.innerHTML = "";
914
+ }
915
+
916
+ for( let o of node.outputs )
917
+ {
918
+ if( !o.type )
919
+ {
920
+ console.warn( `Missing type for node [${ node.title }], skipping...` );
921
+ }
922
+
923
+ var output = document.createElement( 'div' );
924
+ output.className = 'lexgraphnodeio iooutput';
925
+ output.dataset[ 'index' ] = nodeOutputs.childElementCount;
926
+
927
+ if( o.name )
928
+ {
929
+ var name = document.createElement( 'span' );
930
+ name.classList.add( 'io__name' );
931
+ name.innerText = o.name;
932
+ output.appendChild( name );
933
+ }
934
+
935
+ var type = document.createElement( 'span' );
936
+ type.className = 'io__type output ' + o.type;
937
+ type.innerHTML = '<span>' + o.type[ 0 ].toUpperCase() + '</span>';
938
+ output.appendChild( type );
939
+
940
+ var typeDesc = document.createElement( 'span' );
941
+ typeDesc.className = 'io__typedesc output ' + o.type;
942
+ typeDesc.innerHTML = o.type;
943
+ output.appendChild( typeDesc );
944
+
945
+ nodeOutputs.appendChild( output );
946
+ }
947
+ }
948
+
949
+ this._addNodeIOEvents( dom );
950
+ }
951
+
952
+ _addNodeIOEvents( nodeContainer ) {
953
+
954
+ const nodeIO = nodeContainer.querySelector( '.lexgraphnodeios' );
955
+
753
956
  // Manage links
754
957
 
755
958
  nodeIO.querySelectorAll( '.lexgraphnodeio' ).forEach( el => {
@@ -775,8 +978,11 @@ class GraphEditor {
775
978
 
776
979
  el.addEventListener( 'mouseup', e => {
777
980
 
981
+ e.stopPropagation();
982
+ e.stopImmediatePropagation();
983
+
778
984
  // Single click..
779
- if( ( LX.getTime() - this.lastMouseDown ) < 120 ) {
985
+ if( ( LX.getTime() - this.lastMouseDown ) < 200 ) {
780
986
  delete this._generatingLink;
781
987
  return;
782
988
  }
@@ -787,14 +993,11 @@ class GraphEditor {
787
993
  if( !this._onLink( e ) )
788
994
  {
789
995
  // Delete entire SVG if not a successful connection..
790
- deleteElement( this._generatingLink.path ? this._generatingLink.path.parentElement : null );
996
+ LX.UTILS.deleteElement( this._generatingLink.path ? this._generatingLink.path.parentElement : null );
791
997
  }
792
998
 
793
999
  delete this._generatingLink;
794
1000
  }
795
-
796
- e.stopPropagation();
797
- e.stopImmediatePropagation();
798
1001
  } );
799
1002
 
800
1003
  el.addEventListener( 'click', e => {
@@ -808,36 +1011,29 @@ class GraphEditor {
808
1011
  } );
809
1012
 
810
1013
  } );
1014
+ }
811
1015
 
812
- const id = node.id ?? node.title.toLowerCase().replaceAll( /\s/g, '-' ) + '-' + LX.UTILS.uidGenerator();
813
- this.nodes[ id ] = { data: node, dom: nodeContainer };
1016
+ _getAllDOMNodes( includeGroups, exclude ) {
814
1017
 
815
- node.id = id;
816
- nodeContainer.dataset[ 'id' ] = id;
1018
+ var elements = null;
817
1019
 
818
- this._domNodes.appendChild( nodeContainer );
1020
+ if( includeGroups )
1021
+ elements = Array.from( this._domNodes.childNodes );
1022
+ else
1023
+ elements = Array.from( this._domNodes.childNodes ).filter( v => v.classList.contains( 'lexgraphnode' ) );
819
1024
 
820
- // Only 1 main per graph!
821
- if( node.title == 'Main' )
1025
+ if( exclude )
822
1026
  {
823
- this.main = nodeContainer;
1027
+ elements = elements.filter( v => v != exclude );
824
1028
  }
825
1029
 
826
- return nodeContainer;
827
- }
828
-
829
- _getAllDOMNodes( includeGroups ) {
830
-
831
- if( includeGroups )
832
- return this._domNodes.childNodes;
833
-
834
- return Array.from( this._domNodes.childNodes ).filter( v => v.classList.contains( 'lexgraphnode' ) );
1030
+ return elements;
835
1031
  }
836
1032
 
837
1033
  _onMoveNodes( target ) {
838
1034
 
839
1035
  let dT = this._snapToGrid ? this._snappedDeltaMousePosition : this._deltaMousePosition;
840
- dT.div( this._scale, dT);
1036
+ dT.div( this.currentGraph.scale, dT);
841
1037
 
842
1038
  for( let nodeId of this.selectedNodes )
843
1039
  {
@@ -867,17 +1063,20 @@ class GraphEditor {
867
1063
  return;
868
1064
 
869
1065
  let dT = this._snapToGrid ? this._snappedDeltaMousePosition : this._deltaMousePosition;
870
- dT.div( this._scale, dT);
1066
+ dT.div( this.currentGraph.scale, dT);
871
1067
 
872
1068
  this._translateNode( target, dT );
873
1069
 
874
1070
  for( let nodeId of groupNodeIds )
875
1071
  {
876
- const el = this._getNodeDOMElement( nodeId );
1072
+ const isGroup = nodeId.constructor !== String;
877
1073
 
878
- this._translateNode( el, dT );
1074
+ const el = isGroup ? nodeId : this._getNodeDOMElement( nodeId );
879
1075
 
880
- this._updateNodeLinks( nodeId );
1076
+ this._translateNode( el, dT, !isGroup );
1077
+
1078
+ if( !isGroup )
1079
+ this._updateNodeLinks( nodeId );
881
1080
  }
882
1081
  }
883
1082
 
@@ -889,7 +1088,7 @@ class GraphEditor {
889
1088
 
890
1089
  const groupNodeIds = [ ];
891
1090
 
892
- for( let dom of this._getAllDOMNodes() )
1091
+ for( let dom of this._getAllDOMNodes( true, target ) )
893
1092
  {
894
1093
  const x = parseFloat( dom.style.left );
895
1094
  const y = parseFloat( dom.style.top );
@@ -898,7 +1097,7 @@ class GraphEditor {
898
1097
  if( !group_bb.inside( node_bb ) )
899
1098
  continue;
900
1099
 
901
- groupNodeIds.push( dom.dataset[ 'id' ] );
1100
+ groupNodeIds.push( dom.dataset[ 'id' ] ?? dom );
902
1101
  }
903
1102
 
904
1103
  target.nodes = groupNodeIds;
@@ -966,6 +1165,16 @@ class GraphEditor {
966
1165
  case 'select':
967
1166
  panel.addDropdown( p.name, p.options, p.value, (v) => { p.value = v } );
968
1167
  break;
1168
+ case 'array':
1169
+ panel.addArray( p.name, p.value, (v) => {
1170
+ p.value = v;
1171
+ if( node.type == "function/Input" )
1172
+ {
1173
+ node.setOutputs( v );
1174
+ this._updateNodeDOMIOs( dom, node );
1175
+ }
1176
+ }, { innerValues: p.options } );
1177
+ break;
969
1178
  }
970
1179
  }
971
1180
 
@@ -984,7 +1193,7 @@ class GraphEditor {
984
1193
  this._togglePropertiesDialog( false );
985
1194
  }
986
1195
 
987
- _translateNode( dom, deltaTranslation ) {
1196
+ _translateNode( dom, deltaTranslation, updateBasePosition = true ) {
988
1197
 
989
1198
  const translation = deltaTranslation.add( new LX.vec2( parseFloat( dom.style.left ), parseFloat( dom.style.top ) ) );
990
1199
 
@@ -998,27 +1207,44 @@ class GraphEditor {
998
1207
 
999
1208
  dom.style.left = ( translation.x ) + "px";
1000
1209
  dom.style.top = ( translation.y ) + "px";
1210
+
1211
+ // Update base node position..
1212
+ if( updateBasePosition && dom.dataset[ 'id' ] )
1213
+ {
1214
+ let baseNode = this.nodes[ dom.dataset[ 'id' ] ];
1215
+ baseNode.data.position = translation;
1216
+ }
1001
1217
  }
1002
1218
 
1003
1219
  _deleteNode( nodeId ) {
1004
1220
 
1005
- const el = this._getNodeDOMElement( nodeId );
1221
+ const nodeInfo = this.nodes[ nodeId ];
1222
+ const node = nodeInfo.data;
1223
+ const el = nodeInfo.dom;
1006
1224
 
1007
1225
  console.assert( el );
1008
1226
 
1009
- if( el == this.main )
1227
+ if( node.constructor.blockDelete )
1010
1228
  {
1011
- console.warn( `Can't delete MAIN node!` );
1229
+ console.warn( `Can't delete node!` );
1012
1230
  return;
1013
1231
  }
1014
1232
 
1015
- deleteElement( el );
1233
+ LX.UTILS.deleteElement( el );
1234
+
1235
+ // Delete from the editor
1016
1236
 
1017
1237
  delete this.nodes[ nodeId ];
1018
1238
 
1239
+ // Delete from the graph data
1240
+
1241
+ const idx = this.currentGraph.nodes.findIndex( v => v.id === nodeId );
1242
+ console.assert( idx >= 0 );
1243
+ this.currentGraph.nodes.splice( idx, 1 );
1244
+
1019
1245
  // Delete connected links..
1020
1246
 
1021
- for( let key in this.graph.links )
1247
+ for( let key in this.currentGraph.links )
1022
1248
  {
1023
1249
  if( !key.includes( nodeId ) )
1024
1250
  continue;
@@ -1028,13 +1254,13 @@ class GraphEditor {
1028
1254
 
1029
1255
  // Remove the connection from the other before deleting..
1030
1256
 
1031
- const numLinks = this.graph.links[ key ].length;
1257
+ const numLinks = this.currentGraph.links[ key ].length;
1032
1258
 
1033
1259
  for( var i = 0; i < numLinks; ++i )
1034
1260
  {
1035
- var link = this.graph.links[ key ][ i ];
1261
+ var link = this.currentGraph.links[ key ][ i ];
1036
1262
 
1037
- deleteElement( link.path.parentElement );
1263
+ LX.UTILS.deleteElement( link.path.parentElement );
1038
1264
 
1039
1265
  const targetNodeId = targetIsInput ? link.inputNode : link.outputNode;
1040
1266
 
@@ -1057,19 +1283,38 @@ class GraphEditor {
1057
1283
  {
1058
1284
  var active = false;
1059
1285
  for( var links of io.links )
1060
- for( var j of links ){
1061
- console.log(j)
1286
+ {
1287
+ if( !links )
1288
+ continue;
1289
+ for( var j of links ) {
1062
1290
  active |= ( !!j );
1063
1291
  }
1292
+ }
1064
1293
  if( !active )
1065
1294
  delete io.dataset[ 'active' ];
1066
1295
  }
1067
1296
  }
1068
1297
 
1069
- delete this.graph.links[ key ];
1298
+ delete this.currentGraph.links[ key ];
1070
1299
  }
1071
1300
  }
1072
1301
 
1302
+ _deleteGroup( groupId ) {
1303
+
1304
+ const dom = this.groups[ groupId ];
1305
+ LX.UTILS.deleteElement( dom );
1306
+
1307
+ // Delete from the editor
1308
+
1309
+ delete this.groups[ groupId ];
1310
+
1311
+ // Delete from the graph data
1312
+
1313
+ const idx = this.currentGraph.groups.findIndex( v => v.id === groupId );
1314
+ console.assert( idx >= 0 );
1315
+ this.currentGraph.groups.splice( idx, 1 );
1316
+ }
1317
+
1073
1318
  _cloneNodes() {
1074
1319
 
1075
1320
  // Clone all selected nodes
@@ -1092,7 +1337,7 @@ class GraphEditor {
1092
1337
 
1093
1338
  this._selectNode( newDom, true );
1094
1339
 
1095
- this.graph.nodes.push( newNode );
1340
+ this.currentGraph.nodes.push( newNode );
1096
1341
  }
1097
1342
  }
1098
1343
 
@@ -1110,7 +1355,7 @@ class GraphEditor {
1110
1355
  _getLinks( nodeSrcId, nodeDstId ) {
1111
1356
 
1112
1357
  const str = nodeSrcId + '@' + nodeDstId;
1113
- return this.graph.links[ str ];
1358
+ return this.currentGraph.links[ str ];
1114
1359
  }
1115
1360
 
1116
1361
  _deleteLinks( nodeId, io ) {
@@ -1132,7 +1377,7 @@ class GraphEditor {
1132
1377
  var links = this._getLinks( targetId, nodeId );
1133
1378
 
1134
1379
  var linkIdx = links.findIndex( i => ( i.inputIdx == srcIndex && i.outputIdx == targetIndex ) );
1135
- deleteElement( links[ linkIdx ].path.parentElement );
1380
+ LX.UTILS.deleteElement( links[ linkIdx ].path.parentElement );
1136
1381
  links.splice( linkIdx, 1 );
1137
1382
 
1138
1383
  // Input has no longer any connected link
@@ -1182,7 +1427,7 @@ class GraphEditor {
1182
1427
  var links = this._getLinks( nodeId, targetId );
1183
1428
 
1184
1429
  var linkIdx = links.findIndex( i => ( i.inputIdx == targetIndex && i.outputIdx == srcIndex ) );
1185
- deleteElement( links[ linkIdx ].path.parentElement );
1430
+ LX.UTILS.deleteElement( links[ linkIdx ].path.parentElement );
1186
1431
  links.splice( linkIdx, 1 );
1187
1432
 
1188
1433
  // Remove a connection from the output connections
@@ -1274,7 +1519,7 @@ class GraphEditor {
1274
1519
 
1275
1520
  if( this._snapToGrid )
1276
1521
  {
1277
- const snapSize = this._patternSize.x * this._snapValue * this._scale;
1522
+ const snapSize = this._patternSize.x * this._snapValue * this.currentGraph.scale;
1278
1523
  snapPosition.x = Math.floor( snapPosition.x / snapSize ) * snapSize;
1279
1524
  snapPosition.y = Math.floor( snapPosition.y / snapSize ) * snapSize;
1280
1525
  this._snappedDeltaMousePosition = snapPosition.sub( this._lastSnappedMousePosition );
@@ -1291,7 +1536,8 @@ class GraphEditor {
1291
1536
 
1292
1537
  else if( e.type == 'mouseup' )
1293
1538
  {
1294
- if( ( LX.getTime() - this.lastMouseDown ) < 120 ) {
1539
+ if( ( LX.getTime() - this.lastMouseDown ) < 200 ) {
1540
+
1295
1541
  this._processClick( e );
1296
1542
  }
1297
1543
 
@@ -1323,7 +1569,7 @@ class GraphEditor {
1323
1569
 
1324
1570
  e.preventDefault();
1325
1571
 
1326
- if( ( LX.getTime() - this.lastMouseDown ) < 120 ) {
1572
+ if( ( LX.getTime() - this.lastMouseDown ) < 300 ) {
1327
1573
  this._processContextMenu( e );
1328
1574
  }
1329
1575
  }
@@ -1374,8 +1620,14 @@ class GraphEditor {
1374
1620
  // It the event reaches this, the link isn't valid..
1375
1621
  if( this._generatingLink )
1376
1622
  {
1377
- deleteElement( this._generatingLink.path ? this._generatingLink.path.parentElement : null );
1623
+ const linkInfo = Object.assign( { }, this._generatingLink );
1624
+
1625
+ // Delete old link
1626
+ LX.UTILS.deleteElement( this._generatingLink.path ? this._generatingLink.path.parentElement : null );
1378
1627
  delete this._generatingLink;
1628
+
1629
+ // Open contextmenu to auto-connect something..
1630
+ this._processContextMenu( e, linkInfo );
1379
1631
  }
1380
1632
 
1381
1633
  else if( this._boxSelecting )
@@ -1385,7 +1637,7 @@ class GraphEditor {
1385
1637
 
1386
1638
  this._selectNodesInBox( this._boxSelecting, this._mousePosition, e.altKey );
1387
1639
 
1388
- deleteElement( this._currentBoxSelectionSVG );
1640
+ LX.UTILS.deleteElement( this._currentBoxSelectionSVG );
1389
1641
 
1390
1642
  delete this._currentBoxSelectionSVG;
1391
1643
  delete this._boxSelecting;
@@ -1399,7 +1651,7 @@ class GraphEditor {
1399
1651
 
1400
1652
  if( rightPressed )
1401
1653
  {
1402
- this._patternPosition.add( this._deltaMousePosition.div( this._scale ), this._patternPosition );
1654
+ this.currentGraph.translation.add( this._deltaMousePosition.div( this.currentGraph.scale ), this.currentGraph.translation );
1403
1655
 
1404
1656
  this._updatePattern();
1405
1657
 
@@ -1435,10 +1687,10 @@ class GraphEditor {
1435
1687
 
1436
1688
  const delta = e.deltaY;
1437
1689
 
1438
- if( delta > 0.0 ) this._scale *= 0.9;
1439
- else this._scale *= ( 1.0 / 0.9 );
1690
+ if( delta > 0.0 ) this.currentGraph.scale *= 0.9;
1691
+ else this.currentGraph.scale *= ( 1.0 / 0.9 );
1440
1692
 
1441
- this._scale = LX.UTILS.clamp( this._scale, GraphEditor.MIN_SCALE, GraphEditor.MAX_SCALE );
1693
+ this.currentGraph.scale = LX.UTILS.clamp( this.currentGraph.scale, GraphEditor.MIN_SCALE, GraphEditor.MAX_SCALE );
1442
1694
 
1443
1695
  // Compute zoom center in pattern space using new scale
1444
1696
  // and get delta..
@@ -1447,44 +1699,87 @@ class GraphEditor {
1447
1699
 
1448
1700
  const deltaCenter = newCenter.sub( center );
1449
1701
 
1450
- this._patternPosition = this._patternPosition.add( deltaCenter );
1702
+ this.currentGraph.translation.add( deltaCenter, this.currentGraph.translation );
1451
1703
 
1452
- this._updatePattern( GraphEditor.EVENT_MOUSEWHEEL );
1704
+ this._updatePattern();
1453
1705
  }
1454
1706
 
1455
- _processContextMenu( e ) {
1707
+ _processContextMenu( e, autoConnect ) {
1456
1708
 
1457
- LX.addContextMenu( "ADD NODE", e, m => {
1458
-
1459
- for( let type in GraphEditor.NODE_TYPES )
1460
- {
1461
- m.add( type, () => {
1462
-
1463
- const newNode = GraphEditor.addNode( type );
1464
-
1465
- const dom = this._createNodeDOM( newNode );
1466
-
1467
- if( this._snapToGrid )
1468
- {
1469
- dom.mustSnap = true;
1470
- }
1709
+ if( this._clipboardData )
1710
+ {
1711
+ LX.addContextMenu(null, e, m => {
1712
+ m.add( "Paste", () => {
1713
+ // TODO: paste node data
1714
+ // ...
1715
+ } );
1716
+ });
1717
+ }
1718
+ else
1719
+ {
1720
+ LX.addContextMenu( "ADD NODE", e, m => {
1471
1721
 
1472
- if( e )
1473
- {
1474
- const rect = this.root.getBoundingClientRect();
1475
-
1476
- let position = new LX.vec2( e.clientX - rect.x, e.clientY - rect.y );
1477
-
1478
- position = this._getPatternPosition( position );
1479
-
1480
- this._translateNode( dom, position );
1481
- }
1722
+ for( let type in GraphEditor.NODE_TYPES )
1723
+ {
1724
+ const baseClass = GraphEditor.NODE_TYPES[ type ];
1482
1725
 
1483
- this.graph.nodes.push( newNode );
1726
+ if( baseClass.blockAdd )
1727
+ continue;
1484
1728
 
1485
- } );
1486
- }
1487
- });
1729
+ m.add( type, () => {
1730
+
1731
+ const newNode = GraphEditor.addNode( type );
1732
+
1733
+ const dom = this._createNodeDOM( newNode );
1734
+
1735
+ if( this._snapToGrid )
1736
+ {
1737
+ dom.mustSnap = true;
1738
+ }
1739
+
1740
+ if( e )
1741
+ {
1742
+ const rect = this.root.getBoundingClientRect();
1743
+
1744
+ let position = new LX.vec2( e.clientX - rect.x, e.clientY - rect.y );
1745
+
1746
+ position = this._getPatternPosition( position );
1747
+
1748
+ this._translateNode( dom, position );
1749
+ }
1750
+
1751
+ this.currentGraph.nodes.push( newNode );
1752
+
1753
+ if( autoConnect && newNode.inputs.length )
1754
+ {
1755
+ const srcId = autoConnect.domEl.dataset[ 'id' ];
1756
+ const srcType = autoConnect.io.childNodes[ autoConnect.index ].dataset[ 'type' ];
1757
+ const srcIsInput = autoConnect.ioType == GraphEditor.NODE_IO_INPUT;
1758
+
1759
+ const newLink = {
1760
+ inputNode: srcIsInput ? srcId : newNode.id,
1761
+ inputIdx: srcIsInput ? autoConnect.index : 0,
1762
+ inputType: srcIsInput ? srcType : newNode.inputs[ 0 ].type,
1763
+ outputNode: srcIsInput ? newNode.id : srcId,
1764
+ outputIdx: srcIsInput ? 0 : autoConnect.index,
1765
+ outputType: srcIsInput ? newNode.inputs[ 0 ].type : srcType,
1766
+ }
1767
+
1768
+ // Store link
1769
+
1770
+ const pathId = newLink.outputNode + '@' + newLink.inputNode;
1771
+
1772
+ if( !this.currentGraph.links[ pathId ] ) this.currentGraph.links[ pathId ] = [];
1773
+
1774
+ this.currentGraph.links[ pathId ].push( newLink );
1775
+
1776
+ this._createLink( newLink );
1777
+ }
1778
+
1779
+ } );
1780
+ }
1781
+ });
1782
+ }
1488
1783
  }
1489
1784
 
1490
1785
  /**
@@ -1494,6 +1789,7 @@ class GraphEditor {
1494
1789
  start() {
1495
1790
 
1496
1791
  this.mustStop = false;
1792
+ this.state = GraphEditor.RUNNING;
1497
1793
 
1498
1794
  this.propagateEventToAllNodes( 'onStart' );
1499
1795
 
@@ -1525,70 +1821,8 @@ class GraphEditor {
1525
1821
 
1526
1822
  requestAnimationFrame( this._frame.bind(this) );
1527
1823
 
1528
- this._runStep();
1529
- }
1530
-
1531
- /**
1532
- * @method _runStep
1533
- */
1534
-
1535
- _runStep() {
1536
-
1537
- const main = this.main;
1538
-
1539
- if( !main )
1540
- return;
1541
-
1542
- const visitedNodes = { };
1543
-
1544
- this._executionNodes = [ ];
1545
-
1546
- // Reser variables each step?
1547
- this.variables = { };
1548
-
1549
- const mainId = this.main.dataset[ 'id' ];
1550
-
1551
- const addNode = ( id ) => {
1552
-
1553
- if( visitedNodes[ id ] )
1554
- return;
1555
-
1556
- visitedNodes[ id ] = true;
1557
-
1558
- for( let linkId in this.graph.links )
1559
- {
1560
- const idx = linkId.indexOf( '@' + id );
1561
-
1562
- if( idx < 0 )
1563
- continue;
1564
-
1565
- const preNodeId = linkId.substring( 0, idx );
1566
-
1567
- this._executionNodes.push( preNodeId );
1568
-
1569
- addNode( preNodeId );
1570
- }
1571
- };
1572
-
1573
- // TODO: Search "no output" nodes and add to the executable list (same as main)..
1574
- // ...
1575
-
1576
- this._executionNodes.push( mainId );
1577
-
1578
- addNode( mainId );
1579
-
1580
- for( var i = this._executionNodes.length - 1; i >= 0; --i )
1581
- {
1582
- const node = this.nodes[ this._executionNodes[ i ] ];
1583
-
1584
- if( node.data.onBeforeStep )
1585
- node.data.onBeforeStep();
1586
-
1587
- node.data.execute();
1588
-
1589
- if( node.data.onBeforeStep )
1590
- node.data.onAfterStep();
1591
- }
1824
+ // Only run here main graph!
1825
+ this.currentGraph._runStep( this.main );
1592
1826
  }
1593
1827
 
1594
1828
  _generatePattern() {
@@ -1597,8 +1831,8 @@ class GraphEditor {
1597
1831
  {
1598
1832
  var pattern = document.createElementNS( 'http://www.w3.org/2000/svg', 'pattern' );
1599
1833
  pattern.setAttribute( 'id', 'pattern-0' );
1600
- pattern.setAttribute( 'x', this._patternPosition.x );
1601
- pattern.setAttribute( 'y', this._patternPosition.y );
1834
+ pattern.setAttribute( 'x', 0.0 );
1835
+ pattern.setAttribute( 'y', 0.0 );
1602
1836
  pattern.setAttribute( 'width', this._patternSize.x )
1603
1837
  pattern.setAttribute( 'height', this._patternSize.y );
1604
1838
  pattern.setAttribute( 'patternUnits', 'userSpaceOnUse' );
@@ -1638,9 +1872,9 @@ class GraphEditor {
1638
1872
  if( !this._background )
1639
1873
  return;
1640
1874
 
1641
- const patternSize = this._patternSize.mul( this._scale );
1642
- const circlePatternSize = this._circlePatternSize * this._scale;
1643
- const patternPosition = this._patternPosition.mul( this._scale );
1875
+ const patternSize = this._patternSize.mul( this.currentGraph.scale );
1876
+ const circlePatternSize = this._circlePatternSize * this.currentGraph.scale;
1877
+ const patternPosition = this.currentGraph.translation.mul( this.currentGraph.scale );
1644
1878
 
1645
1879
  let pattern = this._background.querySelector( 'pattern' );
1646
1880
  pattern.setAttribute( 'x', patternPosition.x );
@@ -1658,24 +1892,34 @@ class GraphEditor {
1658
1892
  const w = this._domNodes.offsetWidth * 0.5;
1659
1893
  const h = this._domNodes.offsetHeight * 0.5;
1660
1894
 
1661
- const dw = w - w * this._scale;
1662
- const dh = h - h * this._scale;
1895
+ const dw = w - w * this.currentGraph.scale;
1896
+ const dh = h - h * this.currentGraph.scale;
1663
1897
 
1664
1898
  this._domNodes.style.transform = `
1665
1899
  translate(` + ( patternPosition.x - dw ) + `px, ` + ( patternPosition.y - dh ) + `px)
1666
- scale(` + this._scale + `)
1900
+ scale(` + this.currentGraph.scale + `)
1667
1901
  `;
1668
1902
  this._domLinks.style.transform = this._domNodes.style.transform;
1903
+
1904
+ // Hide nodes outside the viewport
1905
+
1906
+ const nodesOutsideViewport = this._getNonVisibleNodes();
1907
+
1908
+ for( let node of nodesOutsideViewport )
1909
+ {
1910
+ let dom = this._getNodeDOMElement( node.id );
1911
+ dom.classList.toggle( 'hiddenOpacity', true );
1912
+ }
1669
1913
  }
1670
1914
 
1671
1915
  _getPatternPosition( renderPosition ) {
1672
1916
 
1673
- return renderPosition.div( this._scale ).sub( this._patternPosition );
1917
+ return renderPosition.div( this.currentGraph.scale ).sub( this.currentGraph.translation );
1674
1918
  }
1675
1919
 
1676
1920
  _getRenderPosition( patternPosition ) {
1677
1921
 
1678
- return patternPosition.add( this._patternPosition ).mul( this._scale );
1922
+ return patternPosition.add( this.currentGraph.translation ).mul( this.currentGraph.scale );
1679
1923
  }
1680
1924
 
1681
1925
  _onLink( e ) {
@@ -1738,11 +1982,6 @@ class GraphEditor {
1738
1982
  this._deleteLinks( src_nodeId, linkData.io );
1739
1983
  }
1740
1984
 
1741
- // Mark as active
1742
-
1743
- linkData.io.dataset[ 'active' ] = true;
1744
- e.target.parentElement.dataset[ 'active' ] = true;
1745
-
1746
1985
  // Store the end io..
1747
1986
 
1748
1987
  var srcDom = linkData.io;
@@ -1763,9 +2002,9 @@ class GraphEditor {
1763
2002
 
1764
2003
  const pathId = ( srcIsInput ? dst_nodeId : src_nodeId ) + '@' + ( srcIsInput ? src_nodeId : dst_nodeId );
1765
2004
 
1766
- if( !this.graph.links[ pathId ] ) this.graph.links[ pathId ] = [];
2005
+ if( !this.currentGraph.links[ pathId ] ) this.currentGraph.links[ pathId ] = [];
1767
2006
 
1768
- this.graph.links[ pathId ].push( {
2007
+ this.currentGraph.links[ pathId ].push( {
1769
2008
  path: path,
1770
2009
  inputNode: srcIsInput ? src_nodeId : dst_nodeId,
1771
2010
  inputIdx: srcIsInput ? src_ioIndex : dst_ioIndex,
@@ -1777,6 +2016,11 @@ class GraphEditor {
1777
2016
 
1778
2017
  path.dataset[ 'id' ] = pathId;
1779
2018
 
2019
+ // Mark as active links...
2020
+
2021
+ linkData.io.dataset[ 'active' ] = true;
2022
+ e.target.parentElement.dataset[ 'active' ] = true;
2023
+
1780
2024
  // Successful link..
1781
2025
  return true;
1782
2026
  }
@@ -1814,6 +2058,7 @@ class GraphEditor {
1814
2058
 
1815
2059
  let startPos = new LX.vec2( startRect.x - offsetX, startRect.y - offsetY );
1816
2060
  let endPos = null;
2061
+ let endioEl = null;
1817
2062
 
1818
2063
  if( e )
1819
2064
  {
@@ -1822,7 +2067,6 @@ class GraphEditor {
1822
2067
  // Add node position, since I can't get the correct position directly from the event..
1823
2068
  if( e.target.hasClass( [ 'lexgraphnode', 'lexgraphgroup' ] ) )
1824
2069
  {
1825
- console.log( this._getNodePosition( e.target ) );
1826
2070
  endPos.add( this._getNodePosition( e.target ), endPos );
1827
2071
  endPos.add( new LX.vec2( 3, 3 ), endPos );
1828
2072
  }
@@ -1840,7 +2084,8 @@ class GraphEditor {
1840
2084
  }
1841
2085
  else
1842
2086
  {
1843
- const ioRect = endIO.querySelector( '.io__type' ).getBoundingClientRect();
2087
+ endioEl = endIO.querySelector( '.io__type' );
2088
+ const ioRect = endioEl.getBoundingClientRect();
1844
2089
  endPos = new LX.vec2( ioRect.x - offsetX, ioRect.y - offsetY );
1845
2090
  }
1846
2091
 
@@ -1851,7 +2096,11 @@ class GraphEditor {
1851
2096
  startPos = tmp;
1852
2097
  }
1853
2098
 
1854
- const color = getComputedStyle( ioEl ).backgroundColor;
2099
+ let color = getComputedStyle( ioEl ).backgroundColor;
2100
+
2101
+ if( type == GraphEditor.NODE_IO_OUTPUT && endioEl )
2102
+ color = getComputedStyle( endioEl ).backgroundColor;
2103
+
1855
2104
  this._createLinkPath( path, startPos, endPos, color, !!e );
1856
2105
 
1857
2106
  return path;
@@ -1907,11 +2156,14 @@ class GraphEditor {
1907
2156
  io1.links = [ ];
1908
2157
  io1.links[ link.outputIdx ] = io1.links[ link.outputIdx ] ?? [ ];
1909
2158
  io1.links[ link.outputIdx ].push( link.outputNode );
2159
+
2160
+ io0.dataset[ 'active' ] = true;
2161
+ io1.dataset[ 'active' ] = true;
1910
2162
  }
1911
2163
 
1912
2164
  _createLinkPath( path, startPos, endPos, color, exactEnd ) {
1913
2165
 
1914
- const dist = 6 * this._scale;
2166
+ const dist = 6 * this.currentGraph.scale;
1915
2167
  startPos.add( new LX.vec2( dist, dist ), startPos );
1916
2168
 
1917
2169
  if( !exactEnd )
@@ -2062,16 +2314,44 @@ class GraphEditor {
2062
2314
  "/>`;
2063
2315
  }
2064
2316
 
2065
- // TODO: Return the ones in the viewport
2066
- _getVisibleNodes() {
2317
+ _getNonVisibleNodes() {
2067
2318
 
2068
- if( !this.graph )
2319
+ const nonVisibleNodes = [ ];
2320
+
2321
+ if( !this.currentGraph )
2069
2322
  {
2070
2323
  console.warn( "No graph set" );
2071
2324
  return [];
2072
2325
  }
2073
2326
 
2074
- return this.graph.nodes;
2327
+ const graph_bb = new BoundingBox( new LX.vec2( 0, 0 ), new LX.vec2( this.root.offsetWidth, this.root.offsetHeight ) );
2328
+
2329
+ for( let node of this.currentGraph.nodes )
2330
+ {
2331
+ let pos = this._getRenderPosition( node.position );
2332
+
2333
+ let dom = this._getNodeDOMElement( node.id );
2334
+
2335
+ if( !dom )
2336
+ continue;
2337
+
2338
+ const node_bb = new BoundingBox( pos, node.size.mul( this.currentGraph.scale ) );
2339
+
2340
+ if( graph_bb.inside( node_bb, false ) )
2341
+ {
2342
+ // Show if node in viewport..
2343
+ dom.classList.toggle( 'hiddenOpacity', false );
2344
+
2345
+ // And hide content if scale is very small..
2346
+ dom.childNodes[ 1 ].classList.toggle( 'hiddenOpacity', this.currentGraph.scale < 0.5 );
2347
+
2348
+ continue;
2349
+ }
2350
+
2351
+ nonVisibleNodes.push( node );
2352
+ }
2353
+
2354
+ return nonVisibleNodes;
2075
2355
  }
2076
2356
 
2077
2357
  _selectNodesInBox( lt, rb, remove ) {
@@ -2145,11 +2425,8 @@ class GraphEditor {
2145
2425
 
2146
2426
  for( let nodeId of nodeIds )
2147
2427
  {
2148
- const dom = this._getNodeDOMElement( nodeId );
2149
-
2150
- const x = parseFloat( dom.style.left );
2151
- const y = parseFloat( dom.style.top );
2152
- const node_bb = new BoundingBox( new LX.vec2( x, y ), new LX.vec2( dom.offsetWidth - 6, dom.offsetHeight - 6 ) );
2428
+ const node = this.nodes[ nodeId ].data;
2429
+ const node_bb = new BoundingBox( node.position, node.size );
2153
2430
 
2154
2431
  if( group_bb )
2155
2432
  {
@@ -2180,11 +2457,13 @@ class GraphEditor {
2180
2457
  * @returns JSON data from the serialized graph
2181
2458
  */
2182
2459
 
2183
- _createGroup() {
2460
+ _createGroup( bb ) {
2184
2461
 
2185
- const group_bb = this._getBoundingFromNodes( this.selectedNodes );
2462
+ const group_bb = bb ?? this._getBoundingFromNodes( this.selectedNodes );
2463
+ const group_id = bb ? bb.id : "group-" + LX.UTILS.uidGenerator();
2186
2464
 
2187
2465
  let groupDOM = document.createElement( 'div' );
2466
+ groupDOM.id = group_id;
2188
2467
  groupDOM.classList.add( 'lexgraphgroup' );
2189
2468
  groupDOM.style.left = group_bb.origin.x + "px";
2190
2469
  groupDOM.style.top = group_bb.origin.y + "px";
@@ -2196,6 +2475,8 @@ class GraphEditor {
2196
2475
 
2197
2476
  groupResizer.addEventListener( 'mousedown', inner_mousedown );
2198
2477
 
2478
+ this.groups[ group_id ] = groupDOM;
2479
+
2199
2480
  var that = this;
2200
2481
  var lastPos = [0,0];
2201
2482
 
@@ -2215,7 +2496,7 @@ class GraphEditor {
2215
2496
  function inner_mousemove( e )
2216
2497
  {
2217
2498
  let dt = new LX.vec2( lastPos[0] - e.x, lastPos[1] - e.y );
2218
- dt.div( that._scale, dt);
2499
+ dt.div( that.currentGraph.scale, dt);
2219
2500
 
2220
2501
  groupDOM.style.width = ( parseFloat( groupDOM.style.width ) - dt.x ) + "px";
2221
2502
  groupDOM.style.height = ( parseFloat( groupDOM.style.height ) - dt.y ) + "px";
@@ -2273,6 +2554,19 @@ class GraphEditor {
2273
2554
  groupTitle.focus();
2274
2555
  } );
2275
2556
 
2557
+ groupDOM.addEventListener( 'contextmenu', e => {
2558
+
2559
+ e.preventDefault();
2560
+ e.stopPropagation();
2561
+ e.stopImmediatePropagation();
2562
+
2563
+ LX.addContextMenu(null, e, m => {
2564
+ m.add( "Delete", () => {
2565
+ this._deleteGroup( group_id );
2566
+ } );
2567
+ });
2568
+ } );
2569
+
2276
2570
  groupDOM.appendChild( groupResizer );
2277
2571
  groupDOM.appendChild( groupTitle );
2278
2572
 
@@ -2286,6 +2580,8 @@ class GraphEditor {
2286
2580
  } );
2287
2581
 
2288
2582
  GraphEditor.LAST_GROUP_ID++;
2583
+
2584
+ return groupDOM;
2289
2585
  }
2290
2586
 
2291
2587
  _addUndoStep( deleteRedo = true ) {
@@ -2370,9 +2666,43 @@ class GraphEditor {
2370
2666
  }
2371
2667
  }
2372
2668
 
2373
- _toggleSideBar() {
2669
+ _toggleSideBar( force ) {
2670
+
2671
+ this._sidebarActive = force ?? !this._sidebarActive;
2672
+ this._sidebarDom.classList.toggle( 'hidden', !this._sidebarActive );
2673
+ this._graphContainer.style.width = this._sidebarActive ? "calc( 100% - 64px )" : "100%";
2674
+ }
2675
+
2676
+ _onSidebarCreate( e ) {
2677
+
2678
+ LX.addContextMenu(null, e, m => {
2679
+ m.add( "Graph", () => this.addGraph() );
2680
+ m.add( "Function", () => this.addGraphFunction() );
2681
+ });
2682
+ }
2683
+
2684
+ _showRenameGraphDialog() {
2685
+
2686
+ new LX.Dialog( this.currentGraph.constructor.name, p => {
2687
+ p.addText( "Name", this.currentGraph.name, v => this._updateGraphName(v) );
2688
+ }, { modal: true, size: [ "350px", null ] } );
2689
+ }
2690
+
2691
+ _updateGraphName( name ) {
2692
+
2693
+ this.currentGraph.name = name;
2694
+
2695
+ const nameDom = LX.root.querySelector( '.graph-title button' );
2696
+ console.assert( nameDom );
2697
+ nameDom.innerText = name;
2698
+
2699
+ // TODO:
2700
+ // Update name in sidebar and all references in current nodes..
2701
+ }
2702
+
2703
+ _addGlobalActions() {
2374
2704
 
2375
- this._sidebar.classList.toggle( 'hidden' );
2705
+
2376
2706
  }
2377
2707
  }
2378
2708
 
@@ -2393,55 +2723,21 @@ class Graph {
2393
2723
  constructor( name, options = {} ) {
2394
2724
 
2395
2725
  this.name = name ?? "Unnamed Graph";
2726
+ this.type = 'Graph';
2396
2727
 
2397
- // Nodes
2398
-
2399
- this.nodes = [ ];
2400
-
2401
- this.links = { };
2402
-
2403
- // const mainNode = GraphEditor.addNode( 'system/Main' );
2404
- // mainNode.position = new LX.vec2( 650, 400 );
2405
-
2406
- // const addNode = GraphEditor.addNode( 'math/Add' );
2407
- // addNode.position = new LX.vec2( 425, 250 );
2408
-
2409
- // const floatNode = GraphEditor.addNode( 'inputs/Float' );
2410
- // floatNode.position = new LX.vec2( 200, 200 );
2411
-
2412
- // const stringNode = GraphEditor.addNode( 'inputs/String' );
2413
- // stringNode.position = new LX.vec2( 400, 50 );
2414
-
2415
- // const setVarNode = GraphEditor.addNode( 'variables/SetVariable' );
2416
- // setVarNode.position = new LX.vec2( 650, 50 );
2417
-
2418
- // // const multNode = new GraphNode( GraphEditor.DEFAULT_NODES[ 'Multiply' ] );
2419
- // // multNode.position = new LX.vec2( 200, 400 );
2420
-
2421
- // const keydownNode = GraphEditor.addNode( 'events/KeyDown' );
2422
- // keydownNode.position = new LX.vec2( 600, 200 );
2423
-
2424
- // const orNode = GraphEditor.addNode( 'logic/Or' );
2425
- // orNode.position = new LX.vec2( 435, 435 );
2426
-
2427
- // const equalNode = GraphEditor.addNode( 'logic/Select' );
2428
- // equalNode.position = new LX.vec2( 135, 400 );
2429
-
2430
- // this.nodes = [
2431
- // mainNode,
2432
- // addNode,
2433
- // floatNode,
2434
- // setVarNode,
2435
- // stringNode,
2436
- // keydownNode,
2437
- // orNode,
2438
- // equalNode,
2439
- // // multNode
2440
- // ];
2728
+ this.nodes = [ ];
2729
+ this.groups = [ ];
2730
+ this.links = { };
2731
+
2732
+ this.scale = 1.0;
2733
+ this.translation = new LX.vec2( 0, 0 );
2441
2734
  }
2442
2735
 
2443
2736
  configure( o ) {
2444
2737
 
2738
+ this.id = o.id;
2739
+ this.name = o.name;
2740
+
2445
2741
  this.nodes.length = 0;
2446
2742
 
2447
2743
  for( let node of o.nodes )
@@ -2453,13 +2749,12 @@ class Graph {
2453
2749
  newNode.color = node.color;
2454
2750
  newNode.position = new LX.vec2( node.position.x, node.position.y );
2455
2751
  newNode.type = node.type;
2456
- // newNode.inputs = node.inputs;
2457
- // newNode.outputs = node.outputs;
2458
- // newNode.properties = node.properties;
2752
+ newNode.properties = node.properties;
2459
2753
 
2460
2754
  this.nodes.push( newNode );
2461
2755
  }
2462
2756
 
2757
+ this.groups = o.groups;
2463
2758
  this.links = o.links;
2464
2759
 
2465
2760
  // editor options?
@@ -2467,6 +2762,71 @@ class Graph {
2467
2762
  // zoom/translation ??
2468
2763
  }
2469
2764
 
2765
+ /**
2766
+ * @method _runStep
2767
+ */
2768
+
2769
+ _runStep( mainId ) {
2770
+
2771
+ if( !mainId )
2772
+ return;
2773
+
2774
+ const nodes = this.nodes.reduce( ( ac, a ) => ( {...ac, [ a.id ] : a } ), {} );
2775
+
2776
+ // Not main graph..
2777
+ if( !nodes[ mainId ] )
2778
+ return;
2779
+
2780
+ const visitedNodes = { };
2781
+
2782
+ this._executionNodes = [ ];
2783
+
2784
+ // Reser variables each step?
2785
+ this.variables = { };
2786
+
2787
+ const addNode = ( id ) => {
2788
+
2789
+ if( visitedNodes[ id ] )
2790
+ return;
2791
+
2792
+ visitedNodes[ id ] = true;
2793
+
2794
+ for( let linkId in this.links )
2795
+ {
2796
+ const idx = linkId.indexOf( '@' + id );
2797
+
2798
+ if( idx < 0 )
2799
+ continue;
2800
+
2801
+ const preNodeId = linkId.substring( 0, idx );
2802
+
2803
+ this._executionNodes.push( preNodeId );
2804
+
2805
+ addNode( preNodeId );
2806
+ }
2807
+ };
2808
+
2809
+ // TODO: Search "no output" nodes and add to the executable list (same as main)..
2810
+ // ...
2811
+
2812
+ this._executionNodes.push( mainId );
2813
+
2814
+ addNode( mainId );
2815
+
2816
+ for( var i = this._executionNodes.length - 1; i >= 0; --i )
2817
+ {
2818
+ const node = nodes[ this._executionNodes[ i ] ];
2819
+
2820
+ if( node.onBeforeStep )
2821
+ node.onBeforeStep();
2822
+
2823
+ node.execute();
2824
+
2825
+ if( node.onBeforeStep )
2826
+ node.onAfterStep();
2827
+ }
2828
+ }
2829
+
2470
2830
  /**
2471
2831
  * @method export
2472
2832
  * @param {Boolean} prettify
@@ -2476,7 +2836,13 @@ class Graph {
2476
2836
  export( prettify = true ) {
2477
2837
 
2478
2838
  var o = { };
2839
+
2840
+ o.id = this.id;
2841
+ o.name = this.name;
2842
+ o.type = this.type;
2843
+
2479
2844
  o.nodes = [ ];
2845
+ o.groups = [ ];
2480
2846
  o.links = { };
2481
2847
 
2482
2848
  for( let node of this.nodes )
@@ -2491,6 +2857,15 @@ class Graph {
2491
2857
  o.links[ linkId ] = ioLinks;
2492
2858
  }
2493
2859
 
2860
+ for( let group of this.groups )
2861
+ {
2862
+ const groupDom = this.editor.groups[ group.id ];
2863
+ const group_bb = this.editor._getBoundingFromGroup( groupDom );
2864
+ group_bb.id = group.id;
2865
+ group_bb.name = group.name
2866
+ o.groups.push( group_bb );
2867
+ }
2868
+
2494
2869
  // editor options?
2495
2870
 
2496
2871
  // zoom/translation ??
@@ -2562,16 +2937,18 @@ class GraphNode {
2562
2937
  if( !this.inputs || !this.inputs.length || !this.inputs[ index ] )
2563
2938
  return;
2564
2939
 
2940
+ const graph = this.editor.graphs[ this.graphID ];
2941
+
2565
2942
  // Get data from link
2566
2943
 
2567
- for( let linkId in this.editor.graph.links )
2944
+ for( let linkId in graph.links )
2568
2945
  {
2569
2946
  const idx = linkId.indexOf( '@' + this.id );
2570
2947
 
2571
2948
  if( idx < 0 )
2572
2949
  continue;
2573
2950
 
2574
- const nodeLinks = this.editor.graph.links[ linkId ];
2951
+ const nodeLinks = graph.links[ linkId ];
2575
2952
 
2576
2953
  for ( var link of nodeLinks )
2577
2954
  {
@@ -2589,23 +2966,27 @@ class GraphNode {
2589
2966
  if( !this.outputs || !this.outputs.length || !this.outputs[ index ] )
2590
2967
  return;
2591
2968
 
2969
+ const graph = this.editor.graphs[ this.graphID ];
2970
+
2592
2971
  // Set data in link
2593
2972
 
2594
- for( let linkId in this.editor.graph.links )
2973
+ for( let linkId in graph.links )
2595
2974
  {
2596
2975
  const idx = linkId.indexOf( this.id + '@' );
2597
2976
 
2598
2977
  if( idx < 0 )
2599
2978
  continue;
2600
2979
 
2601
- const nodeLinks = this.editor.graph.links[ linkId ];
2980
+ const nodeLinks = graph.links[ linkId ];
2602
2981
 
2603
2982
  for ( var link of nodeLinks )
2604
2983
  {
2605
2984
  if( link.outputIdx != index )
2606
2985
  continue;
2607
2986
 
2608
- if( data != undefined && link.inputType != link.outputType && link.inputType != "any" && link.outputType != "any" )
2987
+ let innerData = data;
2988
+
2989
+ if( innerData != undefined && link.inputType != link.outputType && link.inputType != "any" && link.outputType != "any" )
2609
2990
  {
2610
2991
  // In case of supported casting, use function to cast..
2611
2992
 
@@ -2613,19 +2994,14 @@ class GraphNode {
2613
2994
 
2614
2995
  // Use function if it's possible to cast!
2615
2996
 
2616
- data = fn ? fn( LX.deepCopy( data ) ) : null;
2997
+ innerData = fn ? fn( LX.deepCopy( innerData ) ) : null;
2617
2998
  }
2618
2999
 
2619
- link.data = data;
3000
+ link.data = innerData;
2620
3001
  }
2621
3002
  }
2622
3003
  }
2623
3004
 
2624
- getOutput( index ) {
2625
-
2626
- return this.outputs[ index ].value;
2627
- }
2628
-
2629
3005
  serialize() {
2630
3006
 
2631
3007
  var o = { };
@@ -2645,6 +3021,45 @@ class GraphNode {
2645
3021
 
2646
3022
  LX.GraphNode = GraphNode;
2647
3023
 
3024
+ /**
3025
+ * @class GraphFunction
3026
+ */
3027
+
3028
+ class GraphFunction extends Graph {
3029
+
3030
+ constructor( name, options = { } ) {
3031
+
3032
+ super();
3033
+
3034
+ this.name = name ?? ( "GraphFunction" + GraphEditor.LAST_FUNCTION_ID );
3035
+ this.type = 'GraphFunction';
3036
+
3037
+ GraphEditor.LAST_FUNCTION_ID++
3038
+
3039
+ const nodeInput = GraphEditor.addNode( "function/Input" );
3040
+ nodeInput.position = new LX.vec2( 150, 250 );
3041
+
3042
+ const nodeOutput = GraphEditor.addNode( "function/Output" );
3043
+ nodeOutput.position = new LX.vec2( 650, 350 );
3044
+
3045
+ this.nodes.push( nodeInput, nodeOutput );
3046
+ }
3047
+
3048
+ getOutputData( inputValue ) {
3049
+
3050
+ const inputNode = this.nodes[ 0 ];
3051
+ inputNode.setOutput( 0, inputValue );
3052
+
3053
+ const outputNode = this.nodes[ 1 ];
3054
+
3055
+ this._runStep( outputNode.id );
3056
+
3057
+ return outputNode.getInput( 0 );
3058
+ }
3059
+ }
3060
+
3061
+ LX.GraphFunction = GraphFunction;
3062
+
2648
3063
  /*
2649
3064
  ************ PREDEFINED NODES ************
2650
3065
 
@@ -2656,6 +3071,54 @@ LX.GraphNode = GraphNode;
2656
3071
  - onExecute: Callback for node execution
2657
3072
  */
2658
3073
 
3074
+ /*
3075
+ Function nodes
3076
+ */
3077
+
3078
+ class NodeFuncInput extends GraphNode
3079
+ {
3080
+ onCreate() {
3081
+ this.addOutput( null, "float" );
3082
+ this.addProperty( "Outputs", "array", [ "float" ], [ 'float', 'int', 'bool', 'vec2', 'vec3', 'vec4', 'mat44' ] );
3083
+ }
3084
+
3085
+ onExecute() {
3086
+ // var a = this.getInput( 0 ) ?? this.properties[ 0 ].value;
3087
+ // var b = this.getInput( 1 ) ?? this.properties[ 1 ].value;
3088
+ // this.setOutput( 0, a + b );
3089
+ }
3090
+
3091
+ setOutputs( v ) {
3092
+
3093
+ this.outputs.length = 0;
3094
+
3095
+ for( var i of v )
3096
+ {
3097
+ this.outputs.push( { name: null, type: i } );
3098
+ }
3099
+ }
3100
+ }
3101
+
3102
+ NodeFuncInput.blockDelete = true;
3103
+ NodeFuncInput.blockAdd = true;
3104
+ GraphEditor.registerCustomNode( "function/Input", NodeFuncInput );
3105
+
3106
+ class NodeFuncOutput extends GraphNode
3107
+ {
3108
+ onCreate() {
3109
+ this.addInput( null, "any" );
3110
+ }
3111
+
3112
+ onExecute() {
3113
+ // var a = this.getInput( 0 ) ?? this.properties[ 0 ].value;
3114
+ // var b = this.getInput( 1 ) ?? this.properties[ 1 ].value;
3115
+ // this.setOutput( 0, a + b );
3116
+ }
3117
+ }
3118
+
3119
+ NodeFuncOutput.blockDelete = true;
3120
+ NodeFuncOutput.blockAdd = true;
3121
+ GraphEditor.registerCustomNode( "function/Output", NodeFuncOutput );
2659
3122
 
2660
3123
  /*
2661
3124
  Math nodes
@@ -3047,7 +3510,7 @@ class NodeSetVariable extends GraphNode
3047
3510
  var varValue = this.getInput( 1 );
3048
3511
  if( varValue == undefined )
3049
3512
  return;
3050
- this.graph.setVariable( varName, varValue );
3513
+ this.editor.setVariable( varName, varValue );
3051
3514
  this.setOutput( 0, varValue );
3052
3515
  }
3053
3516
  }
@@ -3112,6 +3575,7 @@ class NodeMain extends GraphNode
3112
3575
  };
3113
3576
  }
3114
3577
 
3578
+ NodeMain.blockDelete = true;
3115
3579
  GraphEditor.registerCustomNode( "system/Main", NodeMain );
3116
3580
 
3117
3581
  export { GraphEditor, Graph, GraphNode };