lexgui 0.1.28 → 0.1.29

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.
@@ -29,6 +29,10 @@ function firstNonspaceIndex(str) {
29
29
  return str.search(/\S|$/);
30
30
  }
31
31
 
32
+ function deleteElement( el ) {
33
+ if( el ) el.remove();
34
+ }
35
+
32
36
  let ASYNC_ENABLED = true;
33
37
 
34
38
  function doAsync( fn, ms ) {
@@ -38,6 +42,44 @@ function doAsync( fn, ms ) {
38
42
  fn();
39
43
  }
40
44
 
45
+ class BoundingBox {
46
+
47
+ constructor( o, s )
48
+ {
49
+ this.origin = o ?? new LX.vec2( 0, 0 );
50
+ this.size = s ?? new LX.vec2( 0, 0 );
51
+ }
52
+
53
+ merge ( bb ) {
54
+
55
+ console.assert( bb.constructor == BoundingBox );
56
+
57
+ const min_0 = this.origin;
58
+ const max_0 = this.origin.add( this.size );
59
+
60
+ const min_1 = bb.origin;
61
+ const max_1 = bb.origin.add( bb.size );
62
+
63
+ const merge_min = new LX.vec2( Math.min( min_0.x, min_1.x ), Math.min( min_0.y, min_1.y ) );
64
+ const merge_max = new LX.vec2( Math.max( max_0.x, max_1.x ), Math.max( max_0.y, max_1.y ) );
65
+
66
+ this.origin = merge_min;
67
+ this.size = merge_max.sub( merge_min );
68
+ }
69
+
70
+ inside ( bb ) {
71
+
72
+ const min_0 = this.origin;
73
+ const max_0 = this.origin.add( this.size );
74
+
75
+ const min_1 = bb.origin;
76
+ const max_1 = bb.origin.add( bb.size );
77
+
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;
80
+ }
81
+ };
82
+
41
83
  /**
42
84
  * @class GraphEditor
43
85
  */
@@ -46,25 +88,22 @@ class GraphEditor {
46
88
 
47
89
  static __instances = [];
48
90
 
49
- // Canvas
91
+ // Editor
50
92
 
51
- static MIN_SCALE = 0.25;
52
- static MAX_SCALE = 4.0;
93
+ static MIN_SCALE = 0.25;
94
+ static MAX_SCALE = 4.0;
53
95
 
54
96
  static EVENT_MOUSEMOVE = 0;
55
97
  static EVENT_MOUSEWHEEL = 1;
56
98
 
57
- // Node Drawing
99
+ static LAST_GROUP_ID = 0;
58
100
 
59
- static NODE_TITLE_HEIGHT = 24;
60
- static NODE_ROW_HEIGHT = 16;
101
+ // Node Drawing
61
102
 
62
- static NODE_SHAPE_RADIUS = 12;
63
- static NODE_TITLE_RADIUS = [ GraphEditor.NODE_SHAPE_RADIUS, GraphEditor.NODE_SHAPE_RADIUS, 0, 0 ];
64
- static NODE_BODY_RADIUS = [ GraphEditor.NODE_SHAPE_RADIUS, GraphEditor.NODE_SHAPE_RADIUS, GraphEditor.NODE_SHAPE_RADIUS, GraphEditor.NODE_SHAPE_RADIUS ];
103
+ static NODE_IO_INPUT = 0;
104
+ static NODE_IO_OUTPUT = 1;
65
105
 
66
- static DEFAULT_NODE_TITLE_COLOR = "#4a59b0";
67
- static DEFAULT_NODE_BODY_COLOR = "#111";
106
+ static NODE_TYPES = { };
68
107
 
69
108
  /**
70
109
  * @param {*} options
@@ -75,6 +114,12 @@ class GraphEditor {
75
114
 
76
115
  GraphEditor.__instances.push( this );
77
116
 
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: () => { } } );
121
+ });
122
+
78
123
  this.base_area = area;
79
124
  this.area = new LX.Area( { className: "lexgraph" } );
80
125
 
@@ -87,20 +132,143 @@ class GraphEditor {
87
132
  // Bind resize
88
133
 
89
134
  area.onresize = ( bb ) => {
90
-
135
+ console.log(bb);
91
136
  };
92
137
 
93
- this.root.addEventListener( 'keydown', this._processKey.bind( this ), true );
138
+ area.addOverlayButtons( [
139
+ {
140
+ name: "Toggle Sidebar",
141
+ icon: "fa fa-table-columns",
142
+ callback: () => this._toggleSideBar(),
143
+ },
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
+ // ],
165
+ [
166
+ {
167
+ name: "Enable Snapping",
168
+ icon: "fa fa-table-cells",
169
+ callback: () => this._toggleSnapping(),
170
+ selectable: true
171
+ },
172
+ {
173
+ name: 1,
174
+ options: [1, 2, 3],
175
+ callback: value => this._setSnappingValue( value ),
176
+ }
177
+ ],
178
+ [
179
+ {
180
+ name: "Import",
181
+ icon: "fa fa-upload",
182
+ callback: (value, event) => { this.loadGraph( "../../data/graph_sample.json" ); }
183
+ },
184
+ {
185
+ name: "Export",
186
+ icon: "fa fa-diagram-project",
187
+ callback: (value, event) => this.graph.export()
188
+ }
189
+ ]
190
+ ], { float: "htc" } );
191
+
192
+ this.root.addEventListener( 'keydown', this._processKeyDown.bind( this ), true );
193
+ this.root.addEventListener( 'keyup', this._processKeyUp.bind( this ), true );
94
194
  this.root.addEventListener( 'mousedown', this._processMouse.bind( this ) );
95
195
  this.root.addEventListener( 'mouseup', this._processMouse.bind( this ) );
96
196
  this.root.addEventListener( 'mousemove', this._processMouse.bind( this ) );
97
197
  this.root.addEventListener( 'mousewheel', this._processMouse.bind(this) );
198
+ this.root.addEventListener( 'mouseleave', this._processMouse.bind(this) );
98
199
  this.root.addEventListener( 'click', this._processMouse.bind( this ) );
99
200
  this.root.addEventListener( 'contextmenu', this._processMouse.bind( this ) );
100
201
  this.root.addEventListener( 'focus', this._processFocus.bind( this, true) );
101
202
  this.root.addEventListener( 'focusout', this._processFocus.bind( this, false ) );
102
203
 
204
+ this.propertiesDialog = new LX.PocketDialog( "Properties", null, {
205
+ size: [ "300px", null ],
206
+ position: [ "8px", "8px" ],
207
+ float: "left",
208
+ class: 'lexgraphpropdialog'
209
+ } );
210
+
211
+ // Avoid closing the dialog on click..
212
+
213
+ this.propertiesDialog.root.addEventListener( "mousedown", function( e ) {
214
+ e.stopImmediatePropagation();
215
+ e.stopPropagation();
216
+ });
217
+
218
+ this.propertiesDialog.root.addEventListener( "mouseup", function( e ) {
219
+ e.stopImmediatePropagation();
220
+ e.stopPropagation();
221
+ });
222
+
223
+ // Move to root..
224
+ this.root.appendChild( this.propertiesDialog.root );
225
+
226
+ // Editor
227
+
228
+ this._mousePosition = new LX.vec2( 0, 0 );
229
+ this._deltaMousePosition = new LX.vec2( 0, 0 );
230
+ this._snappedDeltaMousePosition = new LX.vec2( 0, 0 );
103
231
  this._lastMousePosition = new LX.vec2( 0, 0 );
232
+ this._lastSnappedMousePosition = new LX.vec2( 0, 0 );
233
+
234
+ this._undoSteps = [ ];
235
+ this._redoSteps = [ ];
236
+
237
+ this.keys = { };
238
+
239
+ this._sidebar = area.sections[ 0 ].root;
240
+ this._sidebarActive = false;
241
+
242
+ this._snapToGrid = false;
243
+ this._snapValue = 1.0;
244
+
245
+ this._scale = 1.0;
246
+
247
+ // Nodes and connections
248
+
249
+ this.nodes = { };
250
+ this.variables = { };
251
+
252
+ this.selectedNodes = [ ];
253
+
254
+ this.supportedCastTypes = { };
255
+
256
+ this.addCastType( 'float', 'vec2', ( v ) => { return [ v, v ]; } );
257
+ this.addCastType( 'float', 'vec3', ( v ) => { return [ v, v, v ]; } );
258
+ this.addCastType( 'float', 'vec4', ( v ) => { return [ v, v, v, v ]; } );
259
+ this.addCastType( 'float', 'bool', ( v ) => { return !!v; } );
260
+
261
+ this.addCastType( 'vec4', 'vec3', ( v ) => { v.slice( 0, 3 ); return v; } );
262
+ this.addCastType( 'vec4', 'vec2', ( v ) => { v.slice( 0, 2 ); return v; } );
263
+ this.addCastType( 'vec3', 'vec2', ( v ) => { v.slice( 0, 2 ); return v; } );
264
+
265
+ this.addCastType( 'vec3', 'vec4', ( v ) => { v.push( 1 ); return v; } );
266
+ this.addCastType( 'vec2', 'vec3', ( v ) => { v.push( 1 ); return v; } );
267
+ this.addCastType( 'vec2', 'vec4', ( v ) => { v.push( 0, 1 ); return v; } );
268
+
269
+ this._nodeBackgroundOpacity = options.disableNodeOpacity ? 1.0 : 0.8;
270
+
271
+ this.main = null;
104
272
 
105
273
  // Back pattern
106
274
 
@@ -112,43 +280,170 @@ class GraphEditor {
112
280
 
113
281
  this._generatePattern();
114
282
 
115
- // Renderer state
283
+ // Links
116
284
 
117
- this._scale = 1.0;
285
+ this._domLinks = document.createElement( 'div' );
286
+ this._domLinks.classList.add( 'lexgraphlinks' );
287
+ this.root.appendChild( this._domLinks );
118
288
 
119
- // Node container
289
+ // Nodes
120
290
 
121
291
  this._domNodes = document.createElement( 'div' );
122
292
  this._domNodes.classList.add( 'lexgraphnodes' );
123
293
  this.root.appendChild( this._domNodes );
124
294
 
125
- // requestAnimationFrame( this._frame.bind(this) );
295
+ window.ge = this;
126
296
  }
127
297
 
128
- static getInstances()
129
- {
298
+ static getInstances() {
299
+
130
300
  return GraphEditor.__instances;
131
301
  }
132
302
 
303
+ /**
304
+ * Register a node class so it can be listed when the user wants to create a new one
305
+ * @method registerCustomNode
306
+ * @param {String} type: name of the node and path
307
+ * @param {Class} baseClass class containing the structure of the custom node
308
+ */
309
+
310
+ static registerCustomNode( type, baseClass ) {
311
+
312
+ if ( !baseClass.prototype ) {
313
+ throw "Cannot register a simple object, it must be a class with a prototype!";
314
+ }
315
+
316
+ // Get info from path
317
+ const pos = type.lastIndexOf( "/" );
318
+
319
+ baseClass.category = type.substring( 0, pos );
320
+ baseClass.title = baseClass.title ?? type.substring( pos + 1 );
321
+ baseClass.type = type;
322
+
323
+ if( !baseClass.prototype.onExecute )
324
+ {
325
+ console.warn( `GraphNode [${ this.title }] does not have a callback attached.` );
326
+ }
327
+
328
+ const prev = GraphEditor.NODE_TYPES[ type ];
329
+ if(prev) {
330
+ console.warn( `Replacing node type [${ type }]` );
331
+ }
332
+
333
+ GraphEditor.NODE_TYPES[ type ] = baseClass;
334
+
335
+ // Some callbacks..
336
+
337
+ if ( this.onNodeTypeRegistered ) {
338
+ this.onCustomNodeRegistered( type, baseClass);
339
+ }
340
+
341
+ if ( prev && this.onNodeTypeReplaced ) {
342
+ this.onNodeTypeReplaced( type, baseClass, prev );
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Create a node of a given type with a name. The node is not attached to any graph yet.
348
+ * @method createNode
349
+ * @param {String} type full name of the node class. p.e. "math/sin"
350
+ * @param {String} title a name to distinguish from other nodes
351
+ * @param {Object} options Store node options
352
+ */
353
+
354
+ static addNode( type, title, options ) {
355
+
356
+ var baseClass = GraphEditor.NODE_TYPES[ type ];
357
+
358
+ if (!baseClass) {
359
+ console.warn( `GraphNode type [${ type }] not registered.` );
360
+ return null;
361
+ }
362
+
363
+ title = title ?? baseClass.title;
364
+
365
+ const node = new baseClass( title );
366
+
367
+ if( node.onCreate )
368
+ node.onCreate();
369
+
370
+ node.type = type;
371
+ node.title = title;
372
+ node.position = new LX.vec2( 0, 0 );
373
+ node.color = null;
374
+
375
+ // Extra options
376
+ if ( options ) {
377
+ for (var i in options) {
378
+ node[ i ] = options[ i ];
379
+ }
380
+ }
381
+
382
+ if ( node.onNodeCreated ) {
383
+ node.onNodeCreated();
384
+ }
385
+
386
+ return node;
387
+ }
388
+
133
389
  /**
134
390
  * @method setGraph
135
- * @param {Graph} graph:
391
+ * @param {Graph} graph
136
392
  */
137
393
 
138
394
  setGraph( graph ) {
139
395
 
396
+ this.clear();
397
+
140
398
  this.graph = graph;
141
399
 
142
400
  if( !this.graph.nodes )
143
401
  {
144
402
  console.warn( 'Graph does not contain any node!' );
145
- return ;
403
+ return;
146
404
  }
147
405
 
148
406
  for( let node of this.graph.nodes )
149
407
  {
150
- this._createNode( node );
408
+ this._createNodeDOM( node );
409
+ }
410
+
411
+ for( let linkId in this.graph.links )
412
+ {
413
+ const links = this.graph.links[ linkId ];
414
+
415
+ for( let link of links )
416
+ {
417
+ this._createLink( link );
418
+ }
419
+ }
420
+
421
+ // TODO: REMOVE THIS (DEBUG)
422
+ this.start();
423
+ //
424
+ }
425
+
426
+ /**
427
+ * @method loadGraph
428
+ * @param {Graph} graph
429
+ */
430
+
431
+ loadGraph( url, callback ) {
432
+
433
+ const onComplete = ( json ) => {
434
+
435
+ var graph = new Graph();
436
+ graph.configure( json );
437
+
438
+ this.setGraph( graph );
439
+
440
+ if( callback )
441
+ callback( graph );
151
442
  }
443
+
444
+ const onError = (v) => console.error(v);
445
+
446
+ LX.requestJSON( url, onComplete, onError );
152
447
  }
153
448
 
154
449
  /**
@@ -158,52 +453,197 @@ class GraphEditor {
158
453
  clear() {
159
454
 
160
455
  this._domNodes.innerHTML = "";
456
+ this._domLinks.innerHTML = "";
457
+
458
+ this.nodes = { };
459
+ }
460
+
461
+ setVariable( name, value ) {
462
+
463
+ this.variables[ name ] = value;
464
+ }
465
+
466
+ getVariable( name ) {
467
+
468
+ return this.variables[ name ];
469
+ }
470
+
471
+ propagateEventToAllNodes( eventName, params ) {
472
+
473
+ if( !this.graph )
474
+ return;
475
+
476
+ for ( let node of this.graph.nodes )
477
+ {
478
+ if( !node[ eventName ] )
479
+ continue;
480
+
481
+ node[ eventName ].apply( this, params );
482
+ }
483
+ };
484
+
485
+ /**
486
+ * @method addCastType
487
+ * @param {String} type: Type to cast
488
+ * @param {String} targetType: Types to be casted from original type
489
+ * @param {Function} fn: Function to know how to cast
490
+ */
491
+
492
+ addCastType( type, targetType, fn ) {
493
+
494
+ this.supportedCastTypes[ type + '@' + targetType ] = fn;
161
495
  }
162
496
 
163
497
  /**
164
498
  * @method unSelectAll
165
499
  */
166
500
 
167
- unSelectAll( forceOrder = true ) {
501
+ unSelectAll( keepPropDialog ) {
168
502
 
169
- this._domNodes.querySelectorAll( '.lexgraphnode' ).forEach( v => {
170
- v.classList.remove( 'selected' );
171
- // if( forceOrder )
172
- // v.style.zIndex = "0";
173
- } );
503
+ this._domNodes.querySelectorAll( '.lexgraphnode' ).forEach( v => v.classList.remove( 'selected' ) );
504
+
505
+ this.selectedNodes.length = 0;
506
+
507
+ if( !keepPropDialog )
508
+ this._togglePropertiesDialog( false );
174
509
  }
175
510
 
176
- _createNode( node ) {
511
+ _createNodeDOM( node ) {
512
+
513
+ node.editor = this;
177
514
 
178
515
  var nodeContainer = document.createElement( 'div' );
179
516
  nodeContainer.classList.add( 'lexgraphnode' );
517
+ nodeContainer.style.left = "0";
518
+ nodeContainer.style.top = "0";
180
519
 
181
- nodeContainer.style.left = node.position.x + "px";
182
- nodeContainer.style.top = node.position.y + "px";
520
+ this._translateNode( nodeContainer, node.position );
183
521
 
184
- if( node.color )
522
+ var color;
523
+
524
+ // Get color from type if color if not manually specified
525
+ if( node.type && GraphEditor.NODE_TYPES[ node.type ] )
526
+ {
527
+ const category = node.constructor.category;
528
+ nodeContainer.classList.add( category );
529
+ }
530
+
531
+ // Update with manual color
532
+
533
+ color = node.color ?? color;
534
+
535
+ if( color )
185
536
  {
186
- nodeContainer.style.backgroundColor = node.color;
537
+ // RGB
538
+ if( color.constructor == Array )
539
+ {
540
+ color = color.join( ',' );
541
+ }
542
+ // Hex color..
543
+ else
544
+ {
545
+ color = LX.hexToRgb( color );
546
+ color.forEach( ( v, i ) => color[ i ] = v * 255 );
547
+ }
548
+
549
+ nodeContainer.style.backgroundColor = "rgba(" + color + ", " + this._nodeBackgroundOpacity + ")";
187
550
  }
188
551
 
189
- // nodeContainer.addEventListener( 'click', e => {
552
+ nodeContainer.addEventListener( 'mousedown', e => {
553
+
554
+ // Only for left click..
555
+ if( e.button != LX.MOUSE_LEFT_CLICK )
556
+ return;
557
+
558
+ if( e.altKey )
559
+ {
560
+ this._unSelectNode( nodeContainer );
561
+ }
562
+ else
563
+ {
564
+ if( this.selectedNodes.length > 1 && ( !e.ctrlKey && !e.shiftKey ) )
565
+ {
566
+ this.unSelectAll( true );
567
+ }
568
+
569
+ if( !nodeContainer.classList.contains( 'selected' ) )
570
+ {
571
+ this._selectNode( nodeContainer, ( e.ctrlKey || e.shiftKey ) );
572
+ }
573
+ }
574
+ } );
575
+
576
+ nodeContainer.addEventListener( 'contextmenu', e => {
190
577
 
191
- // // TODO: check multiple selection
192
- // this.unSelectAll();
578
+ e.preventDefault();
579
+ e.stopPropagation();
580
+ e.stopImmediatePropagation();
581
+
582
+ LX.addContextMenu(null, e, m => {
583
+
584
+ m.add( "Copy", () => {
585
+ // TODO
586
+ // ...
587
+ } );
588
+
589
+ m.add( "Paste", () => {
590
+ // TODO
591
+ // ...
592
+ } );
193
593
 
194
- // nodeContainer.classList.toggle( 'selected' );
195
- // nodeContainer.style.zIndex = "1";
594
+ m.add( "" );
196
595
 
197
- // // Refresh node render order
198
- // this._domNodes.appendChild( nodeContainer );
199
- // } );
596
+ m.add( "Delete", () => {
597
+ this._deleteNode( nodeContainer.dataset[ 'id' ] );
598
+ } );
599
+ });
600
+ } );
601
+
602
+ nodeContainer.addEventListener( 'dblclick', e => {
603
+
604
+ // Only for left click..
605
+ if( e.button != LX.MOUSE_LEFT_CLICK )
606
+ return;
607
+ } );
200
608
 
201
609
  // Title header
202
610
  var nodeHeader = document.createElement( 'div' );
203
611
  nodeHeader.classList.add( 'lexgraphnodeheader' );
204
- nodeHeader.innerText = node.name;
612
+ nodeHeader.innerText = node.title;
205
613
  nodeContainer.appendChild( nodeHeader );
206
614
 
615
+ // Properties
616
+ // if( node.properties.length && node.constructor.category == 'inputs' )
617
+ // {
618
+ // var nodeProperties = document.createElement( 'div' );
619
+ // nodeProperties.classList.add( 'lexgraphnodeproperties' );
620
+
621
+ // for( let p of node.properties )
622
+ // {
623
+ // var panel = new LX.Panel();
624
+
625
+ // p.signal = "@" + LX.UTILS.uidGenerator() + node.title;
626
+
627
+ // switch( p.type )
628
+ // {
629
+ // case 'float':
630
+ // case 'int':
631
+ // panel.addNumber( p.name, p.value, (v) => p.value = v, { signal: p.signal } );
632
+ // break;
633
+ // case 'string':
634
+ // panel.addText( p.name, p.value, (v) => p.value = v, { signal: p.signal } );
635
+ // break;
636
+ // }
637
+
638
+ // // var prop = document.createElement( 'div' );
639
+ // // prop.innerText = p.type;
640
+ // // prop.classList.add( 'lexgraphnodeproperty' );
641
+ // nodeProperties.appendChild( panel.root );
642
+ // }
643
+
644
+ // nodeContainer.appendChild( nodeProperties );
645
+ // }
646
+
207
647
  // Inputs and outputs
208
648
  var nodeIO = document.createElement( 'div' );
209
649
  nodeIO.classList.add( 'lexgraphnodeios' );
@@ -228,18 +668,24 @@ class GraphEditor {
228
668
  {
229
669
  if( !i.type )
230
670
  {
231
- console.warn( `Missing type for node [${ node.name }], skipping...` );
671
+ console.warn( `Missing type for node [${ node.title }], skipping...` );
232
672
  continue;
233
673
  }
234
674
 
235
675
  var input = document.createElement( 'div' );
236
- input.classList.add( 'lexgraphnodeio' );
676
+ input.className = 'lexgraphnodeio ioinput';
677
+ input.dataset[ 'index' ] = nodeInputs.childElementCount;
237
678
 
238
679
  var type = document.createElement( 'span' );
239
680
  type.className = 'io__type input ' + i.type;
240
- type.innerHTML = "<span>" + i.type[ 0 ].toUpperCase() + "</span>";
681
+ type.innerHTML = '<span>' + i.type[ 0 ].toUpperCase() + '</span>';
241
682
  input.appendChild( type );
242
683
 
684
+ var typeDesc = document.createElement( 'span' );
685
+ typeDesc.className = 'io__typedesc input ' + i.type;
686
+ typeDesc.innerHTML = i.type;
687
+ input.appendChild( typeDesc );
688
+
243
689
  if( i.name )
244
690
  {
245
691
  var name = document.createElement( 'span' );
@@ -268,11 +714,12 @@ class GraphEditor {
268
714
  {
269
715
  if( !o.type )
270
716
  {
271
- console.warn( `Missing type for node [${ node.name }], skipping...` );
717
+ console.warn( `Missing type for node [${ node.title }], skipping...` );
272
718
  }
273
719
 
274
720
  var output = document.createElement( 'div' );
275
- output.className = 'lexgraphnodeio output';
721
+ output.className = 'lexgraphnodeio iooutput';
722
+ output.dataset[ 'index' ] = nodeOutputs.childElementCount;
276
723
 
277
724
  if( o.name )
278
725
  {
@@ -284,449 +731,2387 @@ class GraphEditor {
284
731
 
285
732
  var type = document.createElement( 'span' );
286
733
  type.className = 'io__type output ' + o.type;
287
- type.innerHTML = "<span>" + o.type[ 0 ].toUpperCase() + "</span>";
734
+ type.innerHTML = '<span>' + o.type[ 0 ].toUpperCase() + '</span>';
288
735
  output.appendChild( type );
289
736
 
737
+ var typeDesc = document.createElement( 'span' );
738
+ typeDesc.className = 'io__typedesc output ' + o.type;
739
+ typeDesc.innerHTML = o.type;
740
+ output.appendChild( typeDesc );
741
+
290
742
  nodeOutputs.appendChild( output );
291
743
  }
292
744
  }
293
745
 
294
- // Drag listener
746
+ // Move nodes
295
747
 
296
- LX.makeDraggable( nodeContainer, { onMove: e => {
297
- const dP = this._deltaMousePosition.div( this._scale );
298
- nodeContainer.style.left = (parseFloat(nodeContainer.style.left) + dP.x) + 'px';
299
- nodeContainer.style.top = (parseFloat(nodeContainer.style.top) + dP.y) + 'px';
300
- } } );
748
+ LX.makeDraggable( nodeContainer, {
749
+ onMove: this._onMoveNodes.bind( this ),
750
+ onDragStart: this._onDragNode.bind( this )
751
+ } );
301
752
 
302
- this._domNodes.appendChild( nodeContainer );
303
- }
753
+ // Manage links
304
754
 
305
- _processFocus( active ) {
755
+ nodeIO.querySelectorAll( '.lexgraphnodeio' ).forEach( el => {
306
756
 
307
- this.isFocused = active;
308
- }
757
+ el.addEventListener( 'mousedown', e => {
309
758
 
310
- _processKey( e ) {
759
+ // Only for left click..
760
+ if( e.button != LX.MOUSE_LEFT_CLICK )
761
+ return;
311
762
 
312
- var key = e.key ?? e.detail.key;
313
- console.log( key );
314
- }
763
+ this.lastMouseDown = LX.getTime();
764
+
765
+ this._generatingLink = {
766
+ index: parseInt( el.dataset[ 'index' ] ),
767
+ io: el,
768
+ ioType: el.classList.contains( 'ioinput' ) ? GraphEditor.NODE_IO_INPUT : GraphEditor.NODE_IO_OUTPUT,
769
+ domEl: nodeContainer
770
+ };
315
771
 
316
- _processMouse( e ) {
772
+ e.stopPropagation();
773
+ e.stopImmediatePropagation();
774
+ } );
317
775
 
318
- const rect = this.root.getBoundingClientRect();
319
-
320
- this._mousePosition = new LX.vec2( e.clientX - rect.x , e.clientY - rect.y );
321
- this._deltaMousePosition = this._mousePosition.sub( this._lastMousePosition );
776
+ el.addEventListener( 'mouseup', e => {
322
777
 
323
- if( e.type == 'mousedown' )
324
- {
325
- this.lastMouseDown = LX.getTime();
326
- }
327
-
328
- else if( e.type == 'mouseup' )
329
- {
330
- if( (LX.getTime() - this.lastMouseDown) < 120 ) {
331
- this._processClick( e );
332
- }
333
- }
778
+ // Single click..
779
+ if( ( LX.getTime() - this.lastMouseDown ) < 120 ) {
780
+ delete this._generatingLink;
781
+ return;
782
+ }
334
783
 
335
- else if( e.type == 'mousemove' )
336
- {
337
- this._processMouseMove( e );
338
- }
784
+ if( this._generatingLink )
785
+ {
786
+ // Check for IO
787
+ if( !this._onLink( e ) )
788
+ {
789
+ // Delete entire SVG if not a successful connection..
790
+ deleteElement( this._generatingLink.path ? this._generatingLink.path.parentElement : null );
791
+ }
339
792
 
340
- else if ( e.type == 'click' ) // trip
341
- {
342
- switch( e.detail )
343
- {
344
- case LX.MOUSE_DOUBLE_CLICK:
345
- break;
346
- case LX.MOUSE_TRIPLE_CLICK:
347
- break;
348
- }
349
- }
793
+ delete this._generatingLink;
794
+ }
350
795
 
351
- else if ( e.type == 'mousewheel' ) {
352
- e.preventDefault();
353
- this._processWheel( e );
354
- }
796
+ e.stopPropagation();
797
+ e.stopImmediatePropagation();
798
+ } );
355
799
 
356
- else if ( e.type == 'contextmenu' ) {
800
+ el.addEventListener( 'click', e => {
357
801
 
358
- e.preventDefault();
359
-
360
- if( (LX.getTime() - this.lastMouseDown) < 120 ) {
361
- this._processContextMenu( e );
362
- }
363
- }
802
+ if( !el.links )
803
+ return;
364
804
 
365
- this._lastMousePosition = this._mousePosition;
366
- }
805
+ const nodeId = nodeContainer.dataset[ 'id' ];
367
806
 
368
- _processClick( e ) {
807
+ this._deleteLinks( nodeId, el );
808
+ } );
809
+
810
+ } );
811
+
812
+ const id = node.id ?? node.title.toLowerCase().replaceAll( /\s/g, '-' ) + '-' + LX.UTILS.uidGenerator();
813
+ this.nodes[ id ] = { data: node, dom: nodeContainer };
814
+
815
+ node.id = id;
816
+ nodeContainer.dataset[ 'id' ] = id;
817
+
818
+ this._domNodes.appendChild( nodeContainer );
369
819
 
370
- if( e.target.classList.contains( 'lexgraphnodes' ) )
820
+ // Only 1 main per graph!
821
+ if( node.title == 'Main' )
371
822
  {
372
- this._processBackgroundClick( e );
373
- return;
823
+ this.main = nodeContainer;
374
824
  }
375
825
 
376
- console.log(e.target);
826
+ return nodeContainer;
377
827
  }
378
828
 
379
- _processBackgroundClick( e ) {
829
+ _getAllDOMNodes( includeGroups ) {
830
+
831
+ if( includeGroups )
832
+ return this._domNodes.childNodes;
380
833
 
381
- this.unSelectAll( false );
834
+ return Array.from( this._domNodes.childNodes ).filter( v => v.classList.contains( 'lexgraphnode' ) );
382
835
  }
383
836
 
384
- _processMouseMove( e ) {
837
+ _onMoveNodes( target ) {
385
838
 
386
- const rightPressed = ( e.which == 3 );
387
-
388
- if( rightPressed )
839
+ let dT = this._snapToGrid ? this._snappedDeltaMousePosition : this._deltaMousePosition;
840
+ dT.div( this._scale, dT);
841
+
842
+ for( let nodeId of this.selectedNodes )
389
843
  {
390
- this._patternPosition.add( this._deltaMousePosition.div( this._scale ), this._patternPosition );
844
+ const el = this._getNodeDOMElement( nodeId );
391
845
 
392
- this._updatePattern();
846
+ this._translateNode( el, dT );
847
+
848
+ this._updateNodeLinks( nodeId );
393
849
  }
394
850
  }
395
851
 
396
- _processWheel( e ) {
852
+ _onDragNode( target, e ) {
853
+
854
+ if( !e.shiftKey )
855
+ return;
856
+
857
+ this._cloneNodes();
858
+ }
859
+
860
+ _onMoveGroup( target ) {
861
+
862
+ // Move nodes inside the group
863
+
864
+ const groupNodeIds = target.nodes;
865
+
866
+ if( !groupNodeIds )
867
+ return;
868
+
869
+ let dT = this._snapToGrid ? this._snappedDeltaMousePosition : this._deltaMousePosition;
870
+ dT.div( this._scale, dT);
871
+
872
+ this._translateNode( target, dT );
873
+
874
+ for( let nodeId of groupNodeIds )
875
+ {
876
+ const el = this._getNodeDOMElement( nodeId );
877
+
878
+ this._translateNode( el, dT );
879
+
880
+ this._updateNodeLinks( nodeId );
881
+ }
882
+ }
883
+
884
+ _onDragGroup( target ) {
885
+
886
+ // Get nodes inside the group to be moved
887
+
888
+ const group_bb = this._getBoundingFromGroup( target );
889
+
890
+ const groupNodeIds = [ ];
891
+
892
+ for( let dom of this._getAllDOMNodes() )
893
+ {
894
+ const x = parseFloat( dom.style.left );
895
+ const y = parseFloat( dom.style.top );
896
+ const node_bb = new BoundingBox( new LX.vec2( x, y ), new LX.vec2( dom.offsetWidth - 6, dom.offsetHeight - 6 ) );
897
+
898
+ if( !group_bb.inside( node_bb ) )
899
+ continue;
900
+
901
+ groupNodeIds.push( dom.dataset[ 'id' ] );
902
+ }
903
+
904
+ target.nodes = groupNodeIds;
905
+ }
906
+
907
+ _selectNode( dom, multiSelection, forceOrder = true ) {
908
+
909
+ if( !multiSelection )
910
+ this.unSelectAll( true );
911
+
912
+ dom.classList.add( 'selected' );
913
+
914
+ const id = dom.dataset[ 'id' ];
915
+
916
+ this.selectedNodes.push( id );
917
+
918
+ if( forceOrder )
919
+ {
920
+ // Reorder nodes to draw on top..
921
+ this._domNodes.appendChild( dom );
922
+ }
923
+
924
+ const node = this.nodes[ id ].data;
925
+
926
+ this.propertiesDialog.setTitle( node.title );
927
+
928
+ var panel = this.propertiesDialog.panel;
929
+ panel.clear();
930
+
931
+ // Add description
932
+ if( node.constructor.description )
933
+ {
934
+ panel.addText( null, node.constructor.description, null, { disabled: true } );
935
+ }
936
+
937
+ // Allow change name if input
938
+ if( node.constructor.category == 'inputs' )
939
+ {
940
+ panel.addText( 'Name', node.title, (v) => {
941
+ node.title = v;
942
+ dom.querySelector( '.lexgraphnodeheader' ).innerText = v;
943
+ } );
944
+ }
945
+
946
+ for( let p of node.properties )
947
+ {
948
+ switch( p.type )
949
+ {
950
+ case 'float':
951
+ case 'int':
952
+ panel.addNumber( p.name, p.value, (v) => { p.value = v } );
953
+ break;
954
+ case 'string':
955
+ panel.addText( p.name, p.value, (v) => { p.value = v } );
956
+ break;
957
+ case 'vec2':
958
+ panel.addVector2( p.name, p.value, (v) => { p.value = v } );
959
+ break;
960
+ case 'vec3':
961
+ panel.addVector3( p.name, p.value, (v) => { p.value = v } );
962
+ break;
963
+ case 'vec4':
964
+ panel.addVector4( p.name, p.value, (v) => { p.value = v } );
965
+ break;
966
+ case 'select':
967
+ panel.addDropdown( p.name, p.options, p.value, (v) => { p.value = v } );
968
+ break;
969
+ }
970
+ }
971
+
972
+ this._togglePropertiesDialog( true );
973
+ }
974
+
975
+ _unSelectNode( dom ) {
976
+
977
+ dom.classList.remove( 'selected' );
978
+
979
+ // Delete from selected..
980
+ const idx = this.selectedNodes.indexOf( dom.dataset[ 'id' ] );
981
+ this.selectedNodes.splice( idx, 1 );
982
+
983
+ if( !this.selectedNodes.length )
984
+ this._togglePropertiesDialog( false );
985
+ }
986
+
987
+ _translateNode( dom, deltaTranslation ) {
988
+
989
+ const translation = deltaTranslation.add( new LX.vec2( parseFloat( dom.style.left ), parseFloat( dom.style.top ) ) );
990
+
991
+ if( this._snapToGrid && dom.mustSnap )
992
+ {
993
+ const snapSize = this._patternSize.x * this._snapValue * this._snapValue;
994
+ translation.x = Math.floor( translation.x / snapSize ) * snapSize;
995
+ translation.y = Math.floor( translation.y / snapSize ) * snapSize;
996
+ dom.mustSnap = false;
997
+ }
998
+
999
+ dom.style.left = ( translation.x ) + "px";
1000
+ dom.style.top = ( translation.y ) + "px";
1001
+ }
1002
+
1003
+ _deleteNode( nodeId ) {
1004
+
1005
+ const el = this._getNodeDOMElement( nodeId );
1006
+
1007
+ console.assert( el );
1008
+
1009
+ if( el == this.main )
1010
+ {
1011
+ console.warn( `Can't delete MAIN node!` );
1012
+ return;
1013
+ }
1014
+
1015
+ deleteElement( el );
1016
+
1017
+ delete this.nodes[ nodeId ];
1018
+
1019
+ // Delete connected links..
1020
+
1021
+ for( let key in this.graph.links )
1022
+ {
1023
+ if( !key.includes( nodeId ) )
1024
+ continue;
1025
+
1026
+ const aIdx = key.indexOf( '@' );
1027
+ const targetIsInput = key.substring( aIdx + 1 ) != nodeId;
1028
+
1029
+ // Remove the connection from the other before deleting..
1030
+
1031
+ const numLinks = this.graph.links[ key ].length;
1032
+
1033
+ for( var i = 0; i < numLinks; ++i )
1034
+ {
1035
+ var link = this.graph.links[ key ][ i ];
1036
+
1037
+ deleteElement( link.path.parentElement );
1038
+
1039
+ const targetNodeId = targetIsInput ? link.inputNode : link.outputNode;
1040
+
1041
+ const targetNodeDOM = this._getNodeDOMElement( targetNodeId );
1042
+ const ios = targetNodeDOM.querySelector( targetIsInput ? '.lexgraphnodeinputs' : '.lexgraphnodeoutputs' );
1043
+ const io = ios.childNodes[ targetIsInput ? link.inputIdx : link.outputIdx ];
1044
+
1045
+ const ioIndex = targetIsInput ? link.outputIdx : link.inputIdx;
1046
+ const nodelinkidx = io.links[ ioIndex ].indexOf( nodeId );
1047
+ io.links[ ioIndex ].splice( nodelinkidx, 1 );
1048
+
1049
+ // Unique link, so it's done..
1050
+ if( targetIsInput )
1051
+ {
1052
+ delete io.dataset[ 'active' ];
1053
+ }
1054
+
1055
+ // Check if any link left in case of output
1056
+ else
1057
+ {
1058
+ var active = false;
1059
+ for( var links of io.links )
1060
+ for( var j of links ){
1061
+ console.log(j)
1062
+ active |= ( !!j );
1063
+ }
1064
+ if( !active )
1065
+ delete io.dataset[ 'active' ];
1066
+ }
1067
+ }
1068
+
1069
+ delete this.graph.links[ key ];
1070
+ }
1071
+ }
1072
+
1073
+ _cloneNodes() {
1074
+
1075
+ // Clone all selected nodes
1076
+ const selectedIds = LX.deepCopy( this.selectedNodes );
1077
+
1078
+ this.unSelectAll();
1079
+
1080
+ for( let nodeId of selectedIds )
1081
+ {
1082
+ const nodeInfo = this.nodes[ nodeId ];
1083
+ if( !nodeInfo )
1084
+ return;
1085
+
1086
+ const el = this._getNodeDOMElement( nodeId );
1087
+ const data = nodeInfo.data;
1088
+ const newNode = GraphEditor.addNode( data.type );
1089
+ const newDom = this._createNodeDOM( newNode );
1090
+
1091
+ this._translateNode( newDom, this._getNodePosition( el ) );
1092
+
1093
+ this._selectNode( newDom, true );
1094
+
1095
+ this.graph.nodes.push( newNode );
1096
+ }
1097
+ }
1098
+
1099
+ // This is in pattern space!
1100
+ _getNodePosition( dom ) {
1101
+
1102
+ return new LX.vec2( parseFloat( dom.style.left ), parseFloat( dom.style.top ) );
1103
+ }
1104
+
1105
+ _getNodeDOMElement( nodeId ) {
1106
+
1107
+ return this.nodes[ nodeId ] ? this.nodes[ nodeId ].dom : null;
1108
+ }
1109
+
1110
+ _getLinks( nodeSrcId, nodeDstId ) {
1111
+
1112
+ const str = nodeSrcId + '@' + nodeDstId;
1113
+ return this.graph.links[ str ];
1114
+ }
1115
+
1116
+ _deleteLinks( nodeId, io ) {
1117
+
1118
+ const isInput = io.classList.contains( 'ioinput' );
1119
+ const srcIndex = parseInt( io.dataset[ 'index' ] );
1120
+
1121
+ if( isInput ) // Only one "link to output" to delete
1122
+ {
1123
+ let targetIndex;
1124
+
1125
+ const targets = io.links.filter( (v, i) => { targetIndex = i; return v !== undefined; } )[ 0 ];
1126
+ const targetId = targets[ 0 ];
1127
+
1128
+ // It has been deleted..
1129
+ if( !targetId )
1130
+ return;
1131
+
1132
+ var links = this._getLinks( targetId, nodeId );
1133
+
1134
+ var linkIdx = links.findIndex( i => ( i.inputIdx == srcIndex && i.outputIdx == targetIndex ) );
1135
+ deleteElement( links[ linkIdx ].path.parentElement );
1136
+ links.splice( linkIdx, 1 );
1137
+
1138
+ // Input has no longer any connected link
1139
+
1140
+ delete io.links;
1141
+ delete io.dataset[ 'active' ];
1142
+
1143
+ // Remove a connection from the target connections
1144
+
1145
+ const targetDOM = this._getNodeDOMElement( targetId );
1146
+ const ios = targetDOM.querySelector( '.lexgraphnodeoutputs' );
1147
+ const targetIO = ios.childNodes[ targetIndex ];
1148
+
1149
+ const idx = targetIO.links[ srcIndex ].findIndex( v => v == nodeId );
1150
+ targetIO.links[ srcIndex ].splice( idx, 1 );
1151
+
1152
+ let active = false;
1153
+
1154
+ for( var ls of targetIO.links )
1155
+ {
1156
+ if( !ls ) continue;
1157
+ // Check links left per io
1158
+ active |= ls.reduce( c => c !== undefined, 0 );
1159
+ }
1160
+
1161
+ if( !active )
1162
+ {
1163
+ delete targetIO.links;
1164
+ delete targetIO.dataset[ 'active' ];
1165
+ }
1166
+ }
1167
+ else // Delete ALL "to input links"
1168
+ {
1169
+
1170
+ const numLinks = io.links.length;
1171
+
1172
+ for( let targetIndex = 0; targetIndex < numLinks; ++targetIndex )
1173
+ {
1174
+ const targets = io.links[ targetIndex ];
1175
+
1176
+ if( !targets )
1177
+ continue;
1178
+
1179
+ for( let it = ( targets.length - 1 ); it >= 0 ; --it )
1180
+ {
1181
+ const targetId = targets[ it ];
1182
+ var links = this._getLinks( nodeId, targetId );
1183
+
1184
+ var linkIdx = links.findIndex( i => ( i.inputIdx == targetIndex && i.outputIdx == srcIndex ) );
1185
+ deleteElement( links[ linkIdx ].path.parentElement );
1186
+ links.splice( linkIdx, 1 );
1187
+
1188
+ // Remove a connection from the output connections
1189
+
1190
+ io.links[ targetIndex ].splice( it, 1 );
1191
+
1192
+ // Input has no longer any connected link
1193
+
1194
+ const targetDOM = this._getNodeDOMElement( targetId );
1195
+ const ios = targetDOM.querySelector( '.lexgraphnodeinputs' );
1196
+ const targetIO = ios.childNodes[ targetIndex ];
1197
+
1198
+ delete targetIO.links;
1199
+ delete targetIO.dataset[ 'active' ];
1200
+ }
1201
+ }
1202
+
1203
+ delete io.links;
1204
+ delete io.dataset[ 'active' ];
1205
+ }
1206
+ }
1207
+
1208
+ _processFocus( active ) {
1209
+
1210
+ this.isFocused = active;
1211
+ }
1212
+
1213
+ _processKeyDown( e ) {
1214
+
1215
+ // Prevent processing keys on inputs and others
1216
+ if( document.activeElement != this.root )
1217
+ return;
1218
+
1219
+ var key = e.key ?? e.detail.key;
1220
+
1221
+ switch( key ) {
1222
+ case 'Escape':
1223
+ this.unSelectAll();
1224
+ break;
1225
+ case 'Delete':
1226
+ case 'Backspace':
1227
+ e.preventDefault();
1228
+ this._deleteSelection( e );
1229
+ break;
1230
+ case 'g':
1231
+ if( e.ctrlKey )
1232
+ {
1233
+ e.preventDefault();
1234
+ this._createGroup();
1235
+ }
1236
+ break;
1237
+ case 'y':
1238
+ if( e.ctrlKey )
1239
+ {
1240
+ e.preventDefault();
1241
+ this._doRedo();
1242
+ }
1243
+ break;
1244
+ case 'z':
1245
+ if( e.ctrlKey )
1246
+ {
1247
+ e.preventDefault();
1248
+ this._doUndo();
1249
+ }
1250
+ break;
1251
+ }
1252
+
1253
+ this.keys[ key ] = true;
1254
+ }
1255
+
1256
+ _processKeyUp( e ) {
1257
+
1258
+ // Prevent processing keys on inputs and others
1259
+ if( document.activeElement != this.root )
1260
+ return;
1261
+
1262
+ var key = e.key ?? e.detail.key;
1263
+
1264
+ delete this.keys[ key ];
1265
+ }
1266
+
1267
+ _processMouse( e ) {
1268
+
1269
+ const rect = this.root.getBoundingClientRect();
1270
+
1271
+ this._mousePosition = new LX.vec2( e.clientX - rect.x , e.clientY - rect.y );
1272
+
1273
+ const snapPosition = new LX.vec2( this._mousePosition.x, this._mousePosition.y );
1274
+
1275
+ if( this._snapToGrid )
1276
+ {
1277
+ const snapSize = this._patternSize.x * this._snapValue * this._scale;
1278
+ snapPosition.x = Math.floor( snapPosition.x / snapSize ) * snapSize;
1279
+ snapPosition.y = Math.floor( snapPosition.y / snapSize ) * snapSize;
1280
+ this._snappedDeltaMousePosition = snapPosition.sub( this._lastSnappedMousePosition );
1281
+ }
1282
+
1283
+ this._deltaMousePosition = this._mousePosition.sub( this._lastMousePosition );
1284
+
1285
+ if( e.type == 'mousedown' )
1286
+ {
1287
+ this.lastMouseDown = LX.getTime();
1288
+
1289
+ this._processMouseDown( e );
1290
+ }
1291
+
1292
+ else if( e.type == 'mouseup' )
1293
+ {
1294
+ if( ( LX.getTime() - this.lastMouseDown ) < 120 ) {
1295
+ this._processClick( e );
1296
+ }
1297
+
1298
+ this._processMouseUp( e );
1299
+ }
1300
+
1301
+ else if( e.type == 'mousemove' )
1302
+ {
1303
+ this._processMouseMove( e );
1304
+ }
1305
+
1306
+ else if ( e.type == 'click' ) // trick
1307
+ {
1308
+ switch( e.detail )
1309
+ {
1310
+ case LX.MOUSE_DOUBLE_CLICK:
1311
+ break;
1312
+ case LX.MOUSE_TRIPLE_CLICK:
1313
+ break;
1314
+ }
1315
+ }
1316
+
1317
+ else if ( e.type == 'mousewheel' ) {
1318
+ e.preventDefault();
1319
+ this._processWheel( e );
1320
+ }
1321
+
1322
+ else if ( e.type == 'contextmenu' ) {
1323
+
1324
+ e.preventDefault();
1325
+
1326
+ if( ( LX.getTime() - this.lastMouseDown ) < 120 ) {
1327
+ this._processContextMenu( e );
1328
+ }
1329
+ }
1330
+
1331
+ else if ( e.type == 'mouseleave' ) {
1332
+
1333
+ if( this._generatingLink )
1334
+ {
1335
+ this._processMouseUp( e );
1336
+ }
1337
+ }
1338
+
1339
+ if( this._snapToGrid )
1340
+ {
1341
+ this._lastSnappedMousePosition = snapPosition;
1342
+ }
1343
+
1344
+ this._lastMousePosition = this._mousePosition;
1345
+ }
1346
+
1347
+ _processClick( e ) {
1348
+
1349
+ if( e.target.classList.contains( 'lexgraphnodes' ) || e.target.classList.contains( 'lexgraphgroup' ) )
1350
+ {
1351
+ this._processBackgroundClick( e );
1352
+ return;
1353
+ }
1354
+ }
1355
+
1356
+ _processBackgroundClick( e ) {
1357
+
1358
+ this.unSelectAll();
1359
+ }
1360
+
1361
+ _processMouseDown( e ) {
1362
+
1363
+ // Don't box select over a node..
1364
+ if( !e.target.classList.contains( 'lexgraphnode' ) && !e.target.classList.contains( 'lexgraphgroup' )
1365
+ && e.button == LX.MOUSE_LEFT_CLICK )
1366
+ {
1367
+ this._boxSelecting = this._mousePosition;
1368
+ this._boxSelectRemoving = e.altKey;
1369
+ }
1370
+ }
1371
+
1372
+ _processMouseUp( e ) {
1373
+
1374
+ // It the event reaches this, the link isn't valid..
1375
+ if( this._generatingLink )
1376
+ {
1377
+ deleteElement( this._generatingLink.path ? this._generatingLink.path.parentElement : null );
1378
+ delete this._generatingLink;
1379
+ }
1380
+
1381
+ else if( this._boxSelecting )
1382
+ {
1383
+ if( !e.ctrlKey && !e.altKey )
1384
+ this.unSelectAll();
1385
+
1386
+ this._selectNodesInBox( this._boxSelecting, this._mousePosition, e.altKey );
1387
+
1388
+ deleteElement( this._currentBoxSelectionSVG );
1389
+
1390
+ delete this._currentBoxSelectionSVG;
1391
+ delete this._boxSelecting;
1392
+ delete this._boxSelectRemoving;
1393
+ }
1394
+ }
1395
+
1396
+ _processMouseMove( e ) {
1397
+
1398
+ const rightPressed = ( e.which == 3 );
1399
+
1400
+ if( rightPressed )
1401
+ {
1402
+ this._patternPosition.add( this._deltaMousePosition.div( this._scale ), this._patternPosition );
1403
+
1404
+ this._updatePattern();
1405
+
1406
+ return;
1407
+ }
1408
+
1409
+ else if( this._generatingLink )
1410
+ {
1411
+ this._updatePreviewLink( e );
1412
+
1413
+ return;
1414
+ }
1415
+
1416
+ else if( this._boxSelecting )
1417
+ {
1418
+ this._drawBoxSelection( e );
1419
+
1420
+ return;
1421
+ }
1422
+ }
1423
+
1424
+ _processWheel( e ) {
1425
+
1426
+ if( this._boxSelecting )
1427
+ return;
397
1428
 
398
1429
  // Compute zoom center in pattern space using current scale
399
1430
 
400
- const rect = this.root.getBoundingClientRect();
401
- const zoomCenter = this._mousePosition ?? new LX.vec2( rect.width * 0.5, rect.height * 0.5 );
1431
+ const rect = this.root.getBoundingClientRect();
1432
+ const zoomCenter = this._mousePosition ?? new LX.vec2( rect.width * 0.5, rect.height * 0.5 );
1433
+
1434
+ const center = this._getPatternPosition( zoomCenter );
1435
+
1436
+ const delta = e.deltaY;
1437
+
1438
+ if( delta > 0.0 ) this._scale *= 0.9;
1439
+ else this._scale *= ( 1.0 / 0.9 );
1440
+
1441
+ this._scale = LX.UTILS.clamp( this._scale, GraphEditor.MIN_SCALE, GraphEditor.MAX_SCALE );
1442
+
1443
+ // Compute zoom center in pattern space using new scale
1444
+ // and get delta..
1445
+
1446
+ const newCenter = this._getPatternPosition( zoomCenter );
1447
+
1448
+ const deltaCenter = newCenter.sub( center );
1449
+
1450
+ this._patternPosition = this._patternPosition.add( deltaCenter );
1451
+
1452
+ this._updatePattern( GraphEditor.EVENT_MOUSEWHEEL );
1453
+ }
1454
+
1455
+ _processContextMenu( e ) {
1456
+
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
+ }
1471
+
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
+ }
1482
+
1483
+ this.graph.nodes.push( newNode );
1484
+
1485
+ } );
1486
+ }
1487
+ });
1488
+ }
1489
+
1490
+ /**
1491
+ * @method start
1492
+ */
1493
+
1494
+ start() {
1495
+
1496
+ this.mustStop = false;
1497
+
1498
+ this.propagateEventToAllNodes( 'onStart' );
1499
+
1500
+ requestAnimationFrame( this._frame.bind(this) );
1501
+ }
1502
+
1503
+ /**
1504
+ * @method stop
1505
+ */
1506
+
1507
+ stop() {
1508
+
1509
+ this.mustStop = true;
1510
+ this.state = GraphEditor.STOPPED;
1511
+
1512
+ this.propagateEventToAllNodes( 'onStop' );
1513
+ }
1514
+
1515
+ /**
1516
+ * @method _frame
1517
+ */
1518
+
1519
+ _frame() {
1520
+
1521
+ if( this.mustStop )
1522
+ {
1523
+ return;
1524
+ }
1525
+
1526
+ requestAnimationFrame( this._frame.bind(this) );
1527
+
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
+ }
1592
+ }
1593
+
1594
+ _generatePattern() {
1595
+
1596
+ // Generate pattern
1597
+ {
1598
+ var pattern = document.createElementNS( 'http://www.w3.org/2000/svg', 'pattern' );
1599
+ pattern.setAttribute( 'id', 'pattern-0' );
1600
+ pattern.setAttribute( 'x', this._patternPosition.x );
1601
+ pattern.setAttribute( 'y', this._patternPosition.y );
1602
+ pattern.setAttribute( 'width', this._patternSize.x )
1603
+ pattern.setAttribute( 'height', this._patternSize.y );
1604
+ pattern.setAttribute( 'patternUnits', 'userSpaceOnUse' );
1605
+
1606
+ var circle = document.createElementNS( 'http://www.w3.org/2000/svg', 'circle' );
1607
+ circle.setAttribute( 'cx', this._circlePatternSize );
1608
+ circle.setAttribute( 'cy', this._circlePatternSize );
1609
+ circle.setAttribute( 'r', this._circlePatternSize );
1610
+ circle.setAttribute( 'fill', this._circlePatternColor );
1611
+
1612
+ pattern.appendChild( circle );
1613
+ }
1614
+
1615
+ var svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
1616
+ svg.classList.add( "background-svg" );
1617
+ svg.style.width = "100%";
1618
+ svg.style.height = "100%";
1619
+
1620
+ svg.appendChild( pattern );
1621
+
1622
+ var rect = document.createElementNS( 'http://www.w3.org/2000/svg', 'rect' );
1623
+ rect.setAttribute( 'x', '0' );
1624
+ rect.setAttribute( 'y', '0' );
1625
+ rect.setAttribute( 'width', '100%' );
1626
+ rect.setAttribute( 'height', '100%' );
1627
+ rect.setAttribute( 'fill', 'url(#pattern-0)' );
1628
+
1629
+ svg.appendChild( rect );
1630
+
1631
+ this._background = svg;
1632
+
1633
+ this.root.appendChild( this._background );
1634
+ }
1635
+
1636
+ _updatePattern() {
1637
+
1638
+ if( !this._background )
1639
+ return;
1640
+
1641
+ const patternSize = this._patternSize.mul( this._scale );
1642
+ const circlePatternSize = this._circlePatternSize * this._scale;
1643
+ const patternPosition = this._patternPosition.mul( this._scale );
1644
+
1645
+ let pattern = this._background.querySelector( 'pattern' );
1646
+ pattern.setAttribute( 'x', patternPosition.x );
1647
+ pattern.setAttribute( 'y', patternPosition.y );
1648
+ pattern.setAttribute( 'width', patternSize.x )
1649
+ pattern.setAttribute( 'height', patternSize.y );
1650
+
1651
+ var circle = this._background.querySelector( 'circle' );
1652
+ circle.setAttribute( 'cx', circlePatternSize );
1653
+ circle.setAttribute( 'cy', circlePatternSize );
1654
+ circle.setAttribute( 'r', circlePatternSize );
1655
+
1656
+ // Nodes
1657
+
1658
+ const w = this._domNodes.offsetWidth * 0.5;
1659
+ const h = this._domNodes.offsetHeight * 0.5;
1660
+
1661
+ const dw = w - w * this._scale;
1662
+ const dh = h - h * this._scale;
1663
+
1664
+ this._domNodes.style.transform = `
1665
+ translate(` + ( patternPosition.x - dw ) + `px, ` + ( patternPosition.y - dh ) + `px)
1666
+ scale(` + this._scale + `)
1667
+ `;
1668
+ this._domLinks.style.transform = this._domNodes.style.transform;
1669
+ }
1670
+
1671
+ _getPatternPosition( renderPosition ) {
1672
+
1673
+ return renderPosition.div( this._scale ).sub( this._patternPosition );
1674
+ }
1675
+
1676
+ _getRenderPosition( patternPosition ) {
1677
+
1678
+ return patternPosition.add( this._patternPosition ).mul( this._scale );
1679
+ }
1680
+
1681
+ _onLink( e ) {
1682
+
1683
+ const linkData = this._generatingLink;
1684
+ const ioType = e.target.classList.contains( 'input' ) ? GraphEditor.NODE_IO_INPUT : GraphEditor.NODE_IO_OUTPUT;
1685
+
1686
+ // Discard same IO type
1687
+ if( linkData.ioType == ioType )
1688
+ {
1689
+ console.warn( `Can't link same type of data` );
1690
+ return;
1691
+ }
1692
+
1693
+ // Info about src node
1694
+ const src_nodeContainer = linkData.domEl;
1695
+ const src_nodeId = src_nodeContainer.dataset[ 'id' ];
1696
+ const src_node = this.nodes[ src_nodeId ].data;
1697
+ const src_ioIndex = this._generatingLink.index
1698
+
1699
+ // Info about dst node
1700
+ const dst_nodeContainer = e.target.offsetParent;
1701
+ const dst_nodeId = dst_nodeContainer.dataset[ 'id' ];
1702
+ const dst_node = this.nodes[ dst_nodeId ].data;
1703
+ const dst_ioIndex = parseInt( e.target.parentElement.dataset[ 'index' ] );
1704
+
1705
+ // Discard different types
1706
+ const srcIsInput = ( linkData.ioType == GraphEditor.NODE_IO_INPUT );
1707
+ const src_ios = src_node[ srcIsInput ? 'inputs' : 'outputs' ];
1708
+ const src_ioType = src_ios[ src_ioIndex ].type;
1709
+
1710
+ const dst_ios = dst_node[ ioType == GraphEditor.NODE_IO_INPUT ? 'inputs' : 'outputs' ];
1711
+ const dst_ioType = dst_ios[ dst_ioIndex ].type;
1712
+
1713
+ if( src_ioType != dst_ioType && src_ioType != "any" && dst_ioType != "any" )
1714
+ {
1715
+ // Different types, but it might be possible to cast types
1716
+
1717
+ const inputType = srcIsInput ? src_ioType : dst_ioType;
1718
+ const outputType = srcIsInput ? dst_ioType : src_ioType;
1719
+
1720
+ if( !this.supportedCastTypes[ outputType + '@' + inputType ] )
1721
+ {
1722
+ console.warn( `Can't link ${ src_ioType } to ${ dst_ioType }.` );
1723
+ return;
1724
+ }
1725
+ }
1726
+
1727
+ // Check if target it's an active input and remove the old link
1728
+
1729
+ if( ioType == GraphEditor.NODE_IO_INPUT && e.target.parentElement.dataset[ 'active' ] )
1730
+ {
1731
+ this._deleteLinks( dst_nodeId, e.target.parentElement );
1732
+ }
1733
+
1734
+ // Check if source it's an active input and remove the old link
1735
+
1736
+ else if( linkData.ioType == GraphEditor.NODE_IO_INPUT && linkData.io.dataset[ 'active' ] )
1737
+ {
1738
+ this._deleteLinks( src_nodeId, linkData.io );
1739
+ }
1740
+
1741
+ // Mark as active
1742
+
1743
+ linkData.io.dataset[ 'active' ] = true;
1744
+ e.target.parentElement.dataset[ 'active' ] = true;
1745
+
1746
+ // Store the end io..
1747
+
1748
+ var srcDom = linkData.io;
1749
+ srcDom.links = srcDom.links ?? [ ];
1750
+ srcDom.links[ dst_ioIndex ] = srcDom.links[ dst_ioIndex ] ?? [ ];
1751
+ srcDom.links[ dst_ioIndex ].push( dst_nodeId );
1752
+
1753
+ var dstDom = e.target.parentElement;
1754
+ dstDom.links = dstDom.links ?? [ ];
1755
+ dstDom.links[ src_ioIndex ] = dstDom.links[ src_ioIndex ] ?? [ ];
1756
+ dstDom.links[ src_ioIndex ].push( src_nodeId );
1757
+
1758
+ // Call this using the io target to set the connection to the center of the input DOM element..
1759
+
1760
+ let path = this._updatePreviewLink( null, e.target.parentElement );
1761
+
1762
+ // Store link
1763
+
1764
+ const pathId = ( srcIsInput ? dst_nodeId : src_nodeId ) + '@' + ( srcIsInput ? src_nodeId : dst_nodeId );
1765
+
1766
+ if( !this.graph.links[ pathId ] ) this.graph.links[ pathId ] = [];
1767
+
1768
+ this.graph.links[ pathId ].push( {
1769
+ path: path,
1770
+ inputNode: srcIsInput ? src_nodeId : dst_nodeId,
1771
+ inputIdx: srcIsInput ? src_ioIndex : dst_ioIndex,
1772
+ inputType: srcIsInput ? src_ioType : dst_ioType,
1773
+ outputNode: srcIsInput ? dst_nodeId : src_nodeId,
1774
+ outputIdx: srcIsInput ? dst_ioIndex : src_ioIndex,
1775
+ outputType: srcIsInput ? dst_ioType : src_ioType,
1776
+ } );
1777
+
1778
+ path.dataset[ 'id' ] = pathId;
1779
+
1780
+ // Successful link..
1781
+ return true;
1782
+ }
1783
+
1784
+ _updatePreviewLink( e, endIO ) {
1785
+
1786
+ var path = this._generatingLink.path;
1787
+
1788
+ if( !path )
1789
+ {
1790
+ var svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
1791
+ svg.classList.add( "link-svg" );
1792
+ svg.style.width = "100%";
1793
+ svg.style.height = "100%";
1794
+ this._domLinks.appendChild( svg );
1795
+
1796
+ path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );
1797
+ path.setAttribute( 'fill', 'none' );
1798
+ svg.appendChild( path );
1799
+ this._generatingLink.path = path;
1800
+ }
1801
+
1802
+ // Generate bezier curve
1803
+
1804
+ const index = this._generatingLink.index;
1805
+ const type = this._generatingLink.ioType;
1806
+ const domEl = this._generatingLink.domEl;
1807
+
1808
+ const offsetX = this.root.getBoundingClientRect().x;
1809
+ const offsetY = this.root.getBoundingClientRect().y;
1810
+
1811
+ const ios = domEl.querySelector( type == GraphEditor.NODE_IO_INPUT ? '.lexgraphnodeinputs' : '.lexgraphnodeoutputs' );
1812
+ const ioEl = ios.childNodes[ index ].querySelector( '.io__type' );
1813
+ const startRect = ioEl.getBoundingClientRect();
1814
+
1815
+ let startPos = new LX.vec2( startRect.x - offsetX, startRect.y - offsetY );
1816
+ let endPos = null;
1817
+
1818
+ if( e )
1819
+ {
1820
+ endPos = new LX.vec2( e.offsetX, e.offsetY );
1821
+
1822
+ // Add node position, since I can't get the correct position directly from the event..
1823
+ if( e.target.hasClass( [ 'lexgraphnode', 'lexgraphgroup' ] ) )
1824
+ {
1825
+ console.log( this._getNodePosition( e.target ) );
1826
+ endPos.add( this._getNodePosition( e.target ), endPos );
1827
+ endPos.add( new LX.vec2( 3, 3 ), endPos );
1828
+ }
1829
+ else if( e.target.hasClass( [ 'io__type', 'lexgraphgroupresizer' ] ) )
1830
+ {
1831
+ var parent = e.target.offsetParent;
1832
+ // Add parent offset
1833
+ endPos.add( this._getNodePosition( parent ), endPos );
1834
+ // Add own offset
1835
+ endPos.add( new LX.vec2( e.target.offsetLeft, e.target.offsetTop ), endPos );
1836
+ endPos.add( new LX.vec2( 3, 3 ), endPos );
1837
+ }
1838
+
1839
+ endPos = this._getRenderPosition( endPos );
1840
+ }
1841
+ else
1842
+ {
1843
+ const ioRect = endIO.querySelector( '.io__type' ).getBoundingClientRect();
1844
+ endPos = new LX.vec2( ioRect.x - offsetX, ioRect.y - offsetY );
1845
+ }
1846
+
1847
+ if( type == GraphEditor.NODE_IO_INPUT )
1848
+ {
1849
+ var tmp = endPos;
1850
+ endPos = startPos;
1851
+ startPos = tmp;
1852
+ }
1853
+
1854
+ const color = getComputedStyle( ioEl ).backgroundColor;
1855
+ this._createLinkPath( path, startPos, endPos, color, !!e );
1856
+
1857
+ return path;
1858
+ }
1859
+
1860
+ _createLink( link ) {
1861
+
1862
+ var svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
1863
+ svg.classList.add( "link-svg" );
1864
+ svg.style.width = "100%";
1865
+ svg.style.height = "100%";
1866
+ this._domLinks.appendChild( svg );
1867
+
1868
+ var path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' );
1869
+ path.setAttribute( 'fill', 'none' );
1870
+ svg.appendChild( path );
1871
+
1872
+ const inputNodeDom = this._getNodeDOMElement( link.inputNode );
1873
+ const outputNodeDom = this._getNodeDOMElement( link.outputNode );
1874
+
1875
+ // Start pos
1876
+
1877
+ const offsetX = this.root.getBoundingClientRect().x;
1878
+ const offsetY = this.root.getBoundingClientRect().y;
1879
+
1880
+ const outputs = outputNodeDom.querySelector( '.lexgraphnodeoutputs' );
1881
+ const io0 = outputs.childNodes[ link.outputIdx ];
1882
+ const startRect = io0.querySelector( '.io__type' ).getBoundingClientRect();
1883
+
1884
+ let startPos = new LX.vec2( startRect.x - offsetX, startRect.y - offsetY + 6 );
1885
+
1886
+ // End pos
1887
+
1888
+ const inputs = inputNodeDom.querySelector( '.lexgraphnodeinputs' );
1889
+ const io1 = inputs.childNodes[ link.inputIdx ];
1890
+ const endRect = io1.querySelector( '.io__type' ).getBoundingClientRect();
1891
+
1892
+ let endPos = new LX.vec2( endRect.x - offsetX, endRect.y - offsetY + 6 );
1893
+
1894
+ // Generate bezier curve
1895
+
1896
+ const color = getComputedStyle( io1.querySelector( '.io__type' ) ).backgroundColor;
1897
+ this._createLinkPath( path, startPos, endPos, color );
1898
+
1899
+ link.path = path;
1900
+
1901
+ // Store data in each IO
1902
+
1903
+ io0.links = [ ];
1904
+ io0.links[ link.inputIdx ] = io0.links[ link.inputIdx ] ?? [ ];
1905
+ io0.links[ link.inputIdx ].push( link.inputNode );
1906
+
1907
+ io1.links = [ ];
1908
+ io1.links[ link.outputIdx ] = io1.links[ link.outputIdx ] ?? [ ];
1909
+ io1.links[ link.outputIdx ].push( link.outputNode );
1910
+ }
1911
+
1912
+ _createLinkPath( path, startPos, endPos, color, exactEnd ) {
1913
+
1914
+ const dist = 6 * this._scale;
1915
+ startPos.add( new LX.vec2( dist, dist ), startPos );
1916
+
1917
+ if( !exactEnd )
1918
+ {
1919
+ endPos.add( new LX.vec2( dist, dist ), endPos );
1920
+ }
1921
+
1922
+ startPos = this._getPatternPosition( startPos );
1923
+ endPos = this._getPatternPosition( endPos );
1924
+
1925
+ const distanceX = LX.UTILS.clamp( Math.abs( startPos.x - endPos.x ), 0.0, 256.0 );
1926
+ const cPDistance = 128.0 * Math.pow( distanceX / 256.0, 0.5 );
1927
+
1928
+ let cPoint1 = startPos.add( new LX.vec2( cPDistance, 0 ) );
1929
+ let cPoint2 = endPos.sub( new LX.vec2( cPDistance, 0 ) );
1930
+
1931
+ path.setAttribute( 'd', `
1932
+ M ${ startPos.x },${ startPos.y }
1933
+ C ${ cPoint1.x },${ cPoint1.y } ${ cPoint2.x },${ cPoint2.y } ${ endPos.x },${ endPos.y }
1934
+ ` );
1935
+
1936
+ path.setAttribute( 'stroke', color );
1937
+ }
1938
+
1939
+ _updateNodeLinks( nodeId ) {
1940
+
1941
+ var node = this.nodes[ nodeId ] ? this.nodes[ nodeId ].data : null;
1942
+
1943
+ if( !node ) {
1944
+ console.warn( `Can't finde node [${ nodeId }]` );
1945
+ return;
1946
+ }
1947
+
1948
+ const nodeDOM = this._getNodeDOMElement( nodeId );
1949
+
1950
+ // Update input links
1951
+
1952
+ for( let input of nodeDOM.querySelectorAll( '.ioinput' ) )
1953
+ {
1954
+ if( !input.links )
1955
+ continue;
1956
+
1957
+ // Get first and only target output..
1958
+ const targets = input.links.filter( v => v !== undefined )[ 0 ];
1959
+ const targetNodeId = targets[ 0 ];
1960
+
1961
+ // It has been deleted..
1962
+ if( !targetNodeId )
1963
+ continue;
1964
+
1965
+ const ioIndex = parseInt( input.dataset[ 'index' ] );
1966
+
1967
+ var links = this._getLinks( targetNodeId, nodeId );
1968
+
1969
+ // Inputs only have 1 possible output connected
1970
+ var link = links.find( i => ( i.inputIdx == ioIndex ) );
1971
+
1972
+ this._generatingLink = {
1973
+ index: ioIndex,
1974
+ io: input,
1975
+ ioType: GraphEditor.NODE_IO_INPUT,
1976
+ domEl: nodeDOM,
1977
+ path: link.path
1978
+ };
1979
+
1980
+ // Get end io
1981
+
1982
+ const outputNode = this._getNodeDOMElement( targetNodeId );
1983
+ const io = outputNode.querySelector( '.lexgraphnodeoutputs' ).childNodes[ link.outputIdx ]
1984
+
1985
+ this._updatePreviewLink( null, io );
1986
+ }
1987
+
1988
+ // Update output links
1989
+
1990
+
1991
+ for( let output of nodeDOM.querySelectorAll( '.iooutput' ) )
1992
+ {
1993
+ if( !output.links )
1994
+ continue;
1995
+
1996
+ const srcIndex = parseInt( output.dataset[ 'index' ] );
1997
+
1998
+ for( let targetIndex = 0; targetIndex < output.links.length; ++targetIndex )
1999
+ {
2000
+ const targets = output.links[ targetIndex ];
2001
+
2002
+ if( !targets )
2003
+ continue;
2004
+
2005
+ for( let targetId of targets )
2006
+ {
2007
+ var links = this._getLinks( nodeId, targetId );
2008
+ var link = links.find( i => ( i.inputIdx == targetIndex && i.outputIdx == srcIndex ) );
2009
+
2010
+ // Outputs can have different inputs connected
2011
+ this._generatingLink = {
2012
+ index: link.outputIdx,
2013
+ io: output,
2014
+ ioType: GraphEditor.NODE_IO_OUTPUT,
2015
+ domEl: nodeDOM,
2016
+ path: link.path
2017
+ };
2018
+
2019
+ // Get end io
2020
+
2021
+ const inputNode = this._getNodeDOMElement( targetId );
2022
+ const io = inputNode.querySelector( '.lexgraphnodeinputs' ).childNodes[ link.inputIdx ]
2023
+
2024
+ this._updatePreviewLink( null, io );
2025
+ }
2026
+ }
2027
+ }
2028
+
2029
+ delete this._generatingLink;
2030
+ }
2031
+
2032
+ _drawBoxSelection( e ) {
2033
+
2034
+ var svg = this._currentBoxSelectionSVG;
2035
+
2036
+ if( !svg )
2037
+ {
2038
+ var svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
2039
+ svg.classList.add( "box-selection-svg" );
2040
+ if( this._boxSelectRemoving )
2041
+ svg.classList.add( "removing" );
2042
+ svg.style.width = "100%";
2043
+ svg.style.height = "100%";
2044
+ this._domLinks.appendChild( svg );
2045
+ this._currentBoxSelectionSVG = svg;
2046
+ }
2047
+
2048
+ // Generate box
2049
+
2050
+ let startPos = this._getPatternPosition( this._boxSelecting );
2051
+ let size = this._getPatternPosition( this._mousePosition ).sub( startPos );
2052
+
2053
+ if( size.x < 0 ) startPos.x += size.x;
2054
+ if( size.y < 0 ) startPos.y += size.y;
2055
+
2056
+ size = size.abs();
2057
+
2058
+ svg.innerHTML = `<rect
2059
+ x="${ startPos.x }" y="${ startPos.y }"
2060
+ rx="${ 6 }" ry="${ 6 }"
2061
+ width="${ size.x }" height="${ size.y }"
2062
+ "/>`;
2063
+ }
2064
+
2065
+ // TODO: Return the ones in the viewport
2066
+ _getVisibleNodes() {
2067
+
2068
+ if( !this.graph )
2069
+ {
2070
+ console.warn( "No graph set" );
2071
+ return [];
2072
+ }
2073
+
2074
+ return this.graph.nodes;
2075
+ }
2076
+
2077
+ _selectNodesInBox( lt, rb, remove ) {
2078
+
2079
+ lt = this._getPatternPosition( lt );
2080
+ rb = this._getPatternPosition( rb );
2081
+
2082
+ let size = rb.sub( lt );
2083
+
2084
+ if( size.x < 0 )
2085
+ {
2086
+ var tmp = lt.x;
2087
+ lt.x = rb.x;
2088
+ rb.x = tmp;
2089
+ }
2090
+
2091
+ if( size.y < 0 )
2092
+ {
2093
+ var tmp = lt.y;
2094
+ lt.y = rb.y;
2095
+ rb.y = tmp;
2096
+ }
2097
+
2098
+ for( let nodeEl of this._getAllDOMNodes() )
2099
+ {
2100
+ let pos = this._getNodePosition( nodeEl );
2101
+ let size = new LX.vec2( nodeEl.offsetWidth, nodeEl.offsetHeight );
2102
+
2103
+ if( ( !( pos.x < lt.x && ( pos.x + size.x ) < lt.x ) && !( pos.x > rb.x && ( pos.x + size.x ) > rb.x ) ) &&
2104
+ ( !( pos.y < lt.y && ( pos.y + size.y ) < lt.y ) && !( pos.y > rb.y && ( pos.y + size.y ) > rb.y ) ) )
2105
+ {
2106
+ if( remove )
2107
+ this._unSelectNode( nodeEl );
2108
+ else
2109
+ this._selectNode( nodeEl, true, false );
2110
+ }
2111
+ }
2112
+ }
2113
+
2114
+ _deleteSelection( e ) {
2115
+
2116
+ const lastNodeCount = this._domNodes.childElementCount;
2117
+
2118
+ for( let nodeId of this.selectedNodes )
2119
+ {
2120
+ this._deleteNode( nodeId );
2121
+ }
2122
+
2123
+ this.selectedNodes.length = 0;
2124
+
2125
+ // We delete something, so add undo step..
2126
+
2127
+ if( this._domNodes.childElementCount != lastNodeCount )
2128
+ {
2129
+ this._addUndoStep();
2130
+ }
2131
+
2132
+ }
2133
+
2134
+ _getBoundingFromGroup( groupDOM ) {
2135
+
2136
+ const x = parseFloat( groupDOM.style.left );
2137
+ const y = parseFloat( groupDOM.style.top );
2138
+
2139
+ return new BoundingBox( new LX.vec2( x, y ), new LX.vec2( groupDOM.offsetWidth - 2, groupDOM.offsetHeight - 2 ) );
2140
+ }
2141
+
2142
+ _getBoundingFromNodes( nodeIds ) {
2143
+
2144
+ let group_bb = null;
2145
+
2146
+ for( let nodeId of nodeIds )
2147
+ {
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 ) );
2153
+
2154
+ if( group_bb )
2155
+ {
2156
+ group_bb.merge( node_bb );
2157
+ }
2158
+ else
2159
+ {
2160
+ group_bb = node_bb;
2161
+ }
2162
+ }
2163
+
2164
+ // Add padding
2165
+
2166
+ const groupContentPadding = 8;
2167
+
2168
+ group_bb.origin.sub( new LX.vec2( groupContentPadding ), group_bb.origin );
2169
+ group_bb.origin.sub( new LX.vec2( groupContentPadding ), group_bb.origin );
2170
+
2171
+ group_bb.size.add( new LX.vec2( groupContentPadding * 2.0 ), group_bb.size );
2172
+ group_bb.size.add( new LX.vec2( groupContentPadding * 2.0 ), group_bb.size );
2173
+
2174
+ return group_bb;
2175
+ }
2176
+
2177
+ /**
2178
+ * @method _createGroup
2179
+ * @description Creates a node group from the bounding box of the selected nodes
2180
+ * @returns JSON data from the serialized graph
2181
+ */
2182
+
2183
+ _createGroup() {
2184
+
2185
+ const group_bb = this._getBoundingFromNodes( this.selectedNodes );
2186
+
2187
+ let groupDOM = document.createElement( 'div' );
2188
+ groupDOM.classList.add( 'lexgraphgroup' );
2189
+ groupDOM.style.left = group_bb.origin.x + "px";
2190
+ groupDOM.style.top = group_bb.origin.y + "px";
2191
+ groupDOM.style.width = group_bb.size.x + "px";
2192
+ groupDOM.style.height = group_bb.size.y + "px";
2193
+
2194
+ let groupResizer = document.createElement( 'div' );
2195
+ groupResizer.classList.add( 'lexgraphgroupresizer' );
2196
+
2197
+ groupResizer.addEventListener( 'mousedown', inner_mousedown );
2198
+
2199
+ var that = this;
2200
+ var lastPos = [0,0];
2201
+
2202
+ function inner_mousedown( e )
2203
+ {
2204
+ var doc = that.root.ownerDocument;
2205
+ doc.addEventListener( 'mousemove', inner_mousemove );
2206
+ doc.addEventListener( 'mouseup', inner_mouseup );
2207
+ lastPos[0] = e.x;
2208
+ lastPos[1] = e.y;
2209
+ e.stopPropagation();
2210
+ e.preventDefault();
2211
+ document.body.classList.add( 'nocursor' );
2212
+ groupResizer.classList.add( 'nocursor' );
2213
+ }
2214
+
2215
+ function inner_mousemove( e )
2216
+ {
2217
+ let dt = new LX.vec2( lastPos[0] - e.x, lastPos[1] - e.y );
2218
+ dt.div( that._scale, dt);
2219
+
2220
+ groupDOM.style.width = ( parseFloat( groupDOM.style.width ) - dt.x ) + "px";
2221
+ groupDOM.style.height = ( parseFloat( groupDOM.style.height ) - dt.y ) + "px";
2222
+
2223
+ lastPos[0] = e.x;
2224
+ lastPos[1] = e.y;
2225
+
2226
+ e.stopPropagation();
2227
+ e.preventDefault();
2228
+ }
2229
+
2230
+ function inner_mouseup( e )
2231
+ {
2232
+ var doc = that.root.ownerDocument;
2233
+ doc.removeEventListener( 'mousemove', inner_mousemove );
2234
+ doc.removeEventListener( 'mouseup', inner_mouseup );
2235
+ document.body.classList.remove( 'nocursor' );
2236
+ groupResizer.classList.remove( 'nocursor' );
2237
+ }
2238
+
2239
+ let groupTitle = document.createElement( 'input' );
2240
+ let defaultName = `Group ${ GraphEditor.LAST_GROUP_ID }`;
2241
+ groupTitle.value = defaultName;
2242
+ groupTitle.classList.add( 'lexgraphgrouptitle' );
2243
+ groupTitle.disabled = true;
2244
+
2245
+ // Dbl click to rename
2246
+
2247
+ groupTitle.addEventListener( 'mousedown', e => {
2248
+ e.stopPropagation();
2249
+ e.stopImmediatePropagation();
2250
+ } );
2251
+
2252
+ groupTitle.addEventListener( 'focusout', e => {
2253
+ groupTitle.disabled = true;
2254
+ if( !groupTitle.value.length )
2255
+ groupTitle.value = defaultName;
2256
+ } );
2257
+
2258
+ groupTitle.addEventListener( 'keyup', e => {
2259
+ if( e.key == 'Enter' ) {
2260
+ groupTitle.blur();
2261
+ }
2262
+ else if( e.key == 'Escape' ) {
2263
+ groupTitle.value = "";
2264
+ groupTitle.blur();
2265
+ }
2266
+ });
2267
+
2268
+ groupDOM.addEventListener( 'dblclick', e => {
2269
+ // Only for left click..
2270
+ if( e.button != LX.MOUSE_LEFT_CLICK )
2271
+ return;
2272
+ groupTitle.disabled = false;
2273
+ groupTitle.focus();
2274
+ } );
2275
+
2276
+ groupDOM.appendChild( groupResizer );
2277
+ groupDOM.appendChild( groupTitle );
2278
+
2279
+ this._domNodes.prepend( groupDOM );
2280
+
2281
+ // Move group!!
2282
+
2283
+ LX.makeDraggable( groupDOM, {
2284
+ onMove: this._onMoveGroup.bind( this ),
2285
+ onDragStart: this._onDragGroup.bind( this )
2286
+ } );
2287
+
2288
+ GraphEditor.LAST_GROUP_ID++;
2289
+ }
2290
+
2291
+ _addUndoStep( deleteRedo = true ) {
2292
+
2293
+ if( deleteRedo )
2294
+ {
2295
+ // Remove all redo steps
2296
+ this._redoSteps.length = 0;
2297
+ }
2298
+
2299
+ this._undoSteps.push( {
2300
+ // TODO: Add graph state
2301
+ } );
2302
+ }
2303
+
2304
+ _doUndo() {
2305
+
2306
+ if( !this._undoSteps.length )
2307
+ return;
2308
+
2309
+ this._addRedoStep();
2310
+
2311
+ // Extract info from the last state
2312
+ const step = this._undoSteps.pop();
2313
+
2314
+ // Set old state
2315
+ // TODO
2316
+
2317
+ console.log( "Undo!!" );
2318
+ }
2319
+
2320
+ _addRedoStep() {
2321
+
2322
+ this._redoSteps.push( {
2323
+ // TODO: Add graph state
2324
+ } );
2325
+ }
2326
+
2327
+ _doRedo() {
2328
+
2329
+ if( !this._redoSteps.length )
2330
+ return;
2331
+
2332
+ this._addUndoStep( false );
2333
+
2334
+ // Extract info from the next saved code state
2335
+ const step = this._redoSteps.pop();
2336
+
2337
+ // Set old state
2338
+ // TODO
2339
+
2340
+ console.log( "Redo!!" );
2341
+ }
2342
+
2343
+ _togglePropertiesDialog( force ) {
2344
+
2345
+ this.propertiesDialog.root.classList.toggle( 'opened', force );
2346
+
2347
+ if( !force )
2348
+ {
2349
+ this.propertiesDialog.panel.clear();
2350
+ }
2351
+ }
2352
+
2353
+ _setSnappingValue( value ) {
2354
+
2355
+ this._snapValue = value;
2356
+ }
2357
+
2358
+ _toggleSnapping() {
2359
+
2360
+ this._snapToGrid = !this._snapToGrid;
2361
+
2362
+ // Trigger position snapping for each node if needed
2363
+
2364
+ if( this._snapToGrid )
2365
+ {
2366
+ for( let nodeDom of this._getAllDOMNodes( true ) )
2367
+ {
2368
+ nodeDom.mustSnap = true;
2369
+ }
2370
+ }
2371
+ }
2372
+
2373
+ _toggleSideBar() {
2374
+
2375
+ this._sidebar.classList.toggle( 'hidden' );
2376
+ }
2377
+ }
2378
+
2379
+ LX.GraphEditor = GraphEditor;
2380
+
2381
+
2382
+ /**
2383
+ * @class Graph
2384
+ */
2385
+
2386
+ class Graph {
2387
+
2388
+ /**
2389
+ * @param {*} options
2390
+ *
2391
+ */
2392
+
2393
+ constructor( name, options = {} ) {
2394
+
2395
+ this.name = name ?? "Unnamed Graph";
2396
+
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
+ // ];
2441
+ }
2442
+
2443
+ configure( o ) {
2444
+
2445
+ this.nodes.length = 0;
2446
+
2447
+ for( let node of o.nodes )
2448
+ {
2449
+ const newNode = GraphEditor.addNode( node.type );
2450
+
2451
+ newNode.id = node.id;
2452
+ newNode.title = node.title;
2453
+ newNode.color = node.color;
2454
+ newNode.position = new LX.vec2( node.position.x, node.position.y );
2455
+ newNode.type = node.type;
2456
+ // newNode.inputs = node.inputs;
2457
+ // newNode.outputs = node.outputs;
2458
+ // newNode.properties = node.properties;
2459
+
2460
+ this.nodes.push( newNode );
2461
+ }
2462
+
2463
+ this.links = o.links;
2464
+
2465
+ // editor options?
2466
+
2467
+ // zoom/translation ??
2468
+ }
2469
+
2470
+ /**
2471
+ * @method export
2472
+ * @param {Boolean} prettify
2473
+ * @returns JSON data from the serialized graph
2474
+ */
2475
+
2476
+ export( prettify = true ) {
2477
+
2478
+ var o = { };
2479
+ o.nodes = [ ];
2480
+ o.links = { };
2481
+
2482
+ for( let node of this.nodes )
2483
+ {
2484
+ o.nodes.push( node.serialize() );
2485
+ }
2486
+
2487
+ for( let linkId in this.links )
2488
+ {
2489
+ const ioLinks = LX.deepCopy( this.links[ linkId ] );
2490
+ ioLinks.forEach( v => delete v.path );
2491
+ o.links[ linkId ] = ioLinks;
2492
+ }
2493
+
2494
+ // editor options?
2495
+
2496
+ // zoom/translation ??
2497
+
2498
+ try
2499
+ {
2500
+ o = JSON.stringify( o, null, prettify ? 2 : null );
2501
+ }
2502
+ catch( e )
2503
+ {
2504
+ o = null;
2505
+ console.error( `Can't export GraphNode [${ this.title }] of type [${ this.type }].` );
2506
+ }
2507
+
2508
+ LX.downloadFile( this.name + ".json", o );
2509
+
2510
+ return o;
2511
+ }
2512
+ }
402
2513
 
403
- const center = this._getPatternPosition( zoomCenter );
2514
+ LX.Graph = Graph;
404
2515
 
405
- const delta = e.deltaY;
2516
+ /**
2517
+ * @class GraphNode
2518
+ */
406
2519
 
407
- if( delta > 0.0 ) this._scale *= 0.9;
408
- else this._scale *= ( 1.0 / 0.9 );
2520
+ class GraphNode {
409
2521
 
410
- this._scale = LX.UTILS.clamp( this._scale, GraphEditor.MIN_SCALE, GraphEditor.MAX_SCALE );
2522
+ constructor() {
411
2523
 
412
- // Compute zoom center in pattern space using new scale
413
- // and get delta..
2524
+ this.inputs = [ ];
2525
+ this.outputs = [ ];
2526
+ this.properties = [ ];
2527
+ }
414
2528
 
415
- const newCenter = this._getPatternPosition( zoomCenter );
2529
+ _hasOutputsConnected() {
416
2530
 
417
- const deltaCenter = newCenter.sub( center );
2531
+ return true;
2532
+ }
418
2533
 
419
- this._patternPosition = this._patternPosition.add( deltaCenter );
2534
+ execute() {
420
2535
 
421
- this._updatePattern( GraphEditor.EVENT_MOUSEWHEEL );
2536
+ if( !this._hasOutputsConnected() )
2537
+ return;
2538
+
2539
+ if( this.onExecute )
2540
+ {
2541
+ this.onExecute();
2542
+ }
422
2543
  }
423
2544
 
424
- _processContextMenu( e ) {
425
-
426
- LX.addContextMenu( "Test", e, m => {
427
- m.add( "option 1", () => { } );
428
- m.add( "option 2", () => { } );
429
- });
2545
+ addInput( name, type ) {
2546
+
2547
+ this.inputs.push( { name: name, type: type } );
430
2548
  }
431
2549
 
432
- /**
433
- * @method frame
434
- */
2550
+ addOutput( name, type ) {
435
2551
 
436
- _frame() {
2552
+ this.outputs.push( { name: name, type: type } );
2553
+ }
437
2554
 
438
- this._update();
2555
+ addProperty( name, type, value, selectOptions ) {
439
2556
 
440
- requestAnimationFrame( this.frame.bind(this) );
2557
+ this.properties.push( { name: name, type: type, value: value, options: selectOptions } );
441
2558
  }
442
2559
 
443
- /**
444
- * @method update
445
- */
2560
+ getInput( index ) {
446
2561
 
447
- _update() {
448
-
449
- console.log("Update");
2562
+ if( !this.inputs || !this.inputs.length || !this.inputs[ index ] )
2563
+ return;
2564
+
2565
+ // Get data from link
2566
+
2567
+ for( let linkId in this.editor.graph.links )
2568
+ {
2569
+ const idx = linkId.indexOf( '@' + this.id );
2570
+
2571
+ if( idx < 0 )
2572
+ continue;
2573
+
2574
+ const nodeLinks = this.editor.graph.links[ linkId ];
2575
+
2576
+ for ( var link of nodeLinks )
2577
+ {
2578
+ if( link.inputIdx != index )
2579
+ continue;
2580
+
2581
+ // This is the value!!
2582
+ return link.data;
2583
+ }
2584
+ }
450
2585
  }
451
2586
 
452
- _generatePattern() {
2587
+ setOutput( index, data ) {
453
2588
 
454
- // Generate pattern
2589
+ if( !this.outputs || !this.outputs.length || !this.outputs[ index ] )
2590
+ return;
2591
+
2592
+ // Set data in link
2593
+
2594
+ for( let linkId in this.editor.graph.links )
455
2595
  {
456
- var pattern = document.createElementNS( 'http://www.w3.org/2000/svg', 'pattern' );
457
- pattern.setAttribute( 'id', 'pattern-0' );
458
- pattern.setAttribute( 'x', this._patternPosition.x );
459
- pattern.setAttribute( 'y', this._patternPosition.y );
460
- pattern.setAttribute( 'width', this._patternSize.x )
461
- pattern.setAttribute( 'height', this._patternSize.y );
462
- pattern.setAttribute( 'patternUnits', 'userSpaceOnUse' );
2596
+ const idx = linkId.indexOf( this.id + '@' );
463
2597
 
464
- var circle = document.createElementNS( 'http://www.w3.org/2000/svg', 'circle' );
465
- circle.setAttribute( 'cx', this._circlePatternSize );
466
- circle.setAttribute( 'cy', this._circlePatternSize );
467
- circle.setAttribute( 'r', this._circlePatternSize );
468
- circle.setAttribute( 'fill', this._circlePatternColor );
2598
+ if( idx < 0 )
2599
+ continue;
469
2600
 
470
- pattern.appendChild( circle );
2601
+ const nodeLinks = this.editor.graph.links[ linkId ];
2602
+
2603
+ for ( var link of nodeLinks )
2604
+ {
2605
+ if( link.outputIdx != index )
2606
+ continue;
2607
+
2608
+ if( data != undefined && link.inputType != link.outputType && link.inputType != "any" && link.outputType != "any" )
2609
+ {
2610
+ // In case of supported casting, use function to cast..
2611
+
2612
+ var fn = this.editor.supportedCastTypes[ link.outputType + '@' + link.inputType ];
2613
+
2614
+ // Use function if it's possible to cast!
2615
+
2616
+ data = fn ? fn( LX.deepCopy( data ) ) : null;
2617
+ }
2618
+
2619
+ link.data = data;
2620
+ }
471
2621
  }
472
-
473
- var svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' );
474
- svg.classList.add( "background-svg" );
475
- svg.style.width = "100%";
476
- svg.style.height = "100%";
2622
+ }
477
2623
 
478
- svg.appendChild( pattern );
2624
+ getOutput( index ) {
479
2625
 
480
- var rect = document.createElementNS( 'http://www.w3.org/2000/svg', 'rect' );
481
- rect.setAttribute( 'x', '0' );
482
- rect.setAttribute( 'y', '0' );
483
- rect.setAttribute( 'width', '100%' );
484
- rect.setAttribute( 'height', '100%' );
485
- rect.setAttribute( 'fill', 'url(#pattern-0)' );
2626
+ return this.outputs[ index ].value;
2627
+ }
486
2628
 
487
- svg.appendChild( rect );
2629
+ serialize() {
488
2630
 
489
- this._background = svg;
2631
+ var o = { };
490
2632
 
491
- this.root.appendChild( this._background );
2633
+ o.id = this.id;
2634
+ o.title = this.title;
2635
+ o.color = this.color;
2636
+ o.position = this.position;
2637
+ o.type = this.type;
2638
+ o.inputs = this.inputs;
2639
+ o.outputs = this.outputs;
2640
+ o.properties = this.properties;
2641
+
2642
+ return o;
492
2643
  }
2644
+ }
493
2645
 
494
- _updatePattern() {
2646
+ LX.GraphNode = GraphNode;
495
2647
 
496
- if( !this._background )
497
- return;
2648
+ /*
2649
+ ************ PREDEFINED NODES ************
498
2650
 
499
- const patternSize = this._patternSize.mul( this._scale );
500
- const circlePatternSize = this._circlePatternSize * this._scale;
501
- const patternPosition = this._patternPosition.mul( this._scale );
502
-
503
- let pattern = this._background.querySelector( 'pattern' );
504
- pattern.setAttribute( 'x', patternPosition.x );
505
- pattern.setAttribute( 'y', patternPosition.y );
506
- pattern.setAttribute( 'width', patternSize.x )
507
- pattern.setAttribute( 'height', patternSize.y );
2651
+ Nodes can override the following methods:
508
2652
 
509
- var circle = this._background.querySelector( 'circle' );
510
- circle.setAttribute( 'cx', circlePatternSize );
511
- circle.setAttribute( 'cy', circlePatternSize );
512
- circle.setAttribute( 'r', circlePatternSize );
2653
+ - onCreate: Add inputs, outputs and properties
2654
+ - onStart: Callback on graph starts
2655
+ - onStop: Callback on graph stops
2656
+ - onExecute: Callback for node execution
2657
+ */
513
2658
 
514
- // Nodes
515
2659
 
516
- const w = this._domNodes.offsetWidth * 0.5;
517
- const h = this._domNodes.offsetHeight * 0.5;
2660
+ /*
2661
+ Math nodes
2662
+ */
518
2663
 
519
- const dw = w - w * this._scale;
520
- const dh = h - h * this._scale;
2664
+ class NodeAdd extends GraphNode
2665
+ {
2666
+ onCreate() {
2667
+ this.addInput( null, "float" );
2668
+ this.addInput( null, "float" );
2669
+ this.addOutput( null, "float" );
2670
+ this.addProperty( "A", "float", 0 );
2671
+ this.addProperty( "B", "float", 0 );
2672
+ }
2673
+
2674
+ onExecute() {
2675
+ var a = this.getInput( 0 ) ?? this.properties[ 0 ].value;
2676
+ var b = this.getInput( 1 ) ?? this.properties[ 1 ].value;
2677
+ this.setOutput( 0, a + b );
2678
+ }
2679
+ }
521
2680
 
522
- this._domNodes.style.transform = `
523
- translate(` + ( patternPosition.x - dw ) + `px, ` + ( patternPosition.y - dh ) + `px)
524
- scale(` + this._scale + `)
525
- `;
2681
+ NodeAdd.description = "Addition of 2 values (A+B)."
2682
+ GraphEditor.registerCustomNode( "math/Add", NodeAdd );
2683
+
2684
+ class NodeSubstract extends GraphNode
2685
+ {
2686
+ onCreate() {
2687
+ this.addInput( null, "float" );
2688
+ this.addInput( null, "float" );
2689
+ this.addOutput( null, "float" );
2690
+ this.addProperty( "A", "float", 0 );
2691
+ this.addProperty( "B", "float", 0 );
2692
+ }
2693
+
2694
+ onExecute() {
2695
+ var a = this.getInput( 0 ) ?? this.properties[ 0 ].value;
2696
+ var b = this.getInput( 1 ) ?? this.properties[ 1 ].value;
2697
+ this.setOutput( 0, a - b );
2698
+ }
2699
+ }
2700
+
2701
+ NodeSubstract.description = "Substraction of 2 values (A-B)."
2702
+ GraphEditor.registerCustomNode( "math/Substract", NodeSubstract );
2703
+
2704
+ class NodeMultiply extends GraphNode
2705
+ {
2706
+ onCreate() {
2707
+ this.addInput( null, "float" );
2708
+ this.addInput( null, "float" );
2709
+ this.addOutput( null, "float" );
2710
+ this.addProperty( "A", "float", 0 );
2711
+ this.addProperty( "B", "float", 0 );
526
2712
  }
2713
+
2714
+ onExecute() {
2715
+ var a = this.getInput( 0 ) ?? this.properties[ 0 ].value;
2716
+ var b = this.getInput( 1 ) ?? this.properties[ 1 ].value;
2717
+ this.setOutput( 0, a * b );
2718
+ }
2719
+ }
527
2720
 
528
- _getPatternPosition( renderPosition ) {
2721
+ NodeMultiply.description = "Multiplication of 2 values (A*B)."
2722
+ GraphEditor.registerCustomNode( "math/Multiply", NodeMultiply );
2723
+
2724
+ class NodeDivide extends GraphNode
2725
+ {
2726
+ onCreate() {
2727
+ this.addInput( null, "float" );
2728
+ this.addInput( null, "float" );
2729
+ this.addOutput( null, "float" );
2730
+ this.addProperty( "A", "float", 0 );
2731
+ this.addProperty( "B", "float", 0 );
2732
+ }
2733
+
2734
+ onExecute() {
2735
+ var a = this.getInput( 0 ) ?? this.properties[ 0 ].value;
2736
+ var b = this.getInput( 1 ) ?? this.properties[ 1 ].value;
2737
+ this.setOutput( 0, a / b );
2738
+ }
2739
+ }
529
2740
 
530
- return renderPosition.div( this._scale ).sub( this._patternPosition );
2741
+ NodeDivide.description = "Division of 2 values (A/B)."
2742
+ GraphEditor.registerCustomNode( "math/Divide", NodeDivide );
2743
+
2744
+ class NodeSqrt extends GraphNode
2745
+ {
2746
+ onCreate() {
2747
+ this.addInput( null, "float" );
2748
+ this.addOutput( null, "float" );
2749
+ this.addProperty( "Value", "float", 0 );
531
2750
  }
2751
+
2752
+ onExecute() {
2753
+ var a = this.getInput( 0 ) ?? this.properties[ 0 ].value;
2754
+ this.setOutput( 0, Math.sqrt( a ) );
2755
+ }
2756
+ }
532
2757
 
533
- _computeNodeSize( node ) {
2758
+ NodeSqrt.description = "Square root of a scalar."
2759
+ GraphEditor.registerCustomNode( "math/SQRT", NodeSqrt );
2760
+
2761
+ /*
2762
+ Math Missing:
2763
+ - abs
2764
+ - ceil
2765
+ - clamp
2766
+ - floor
2767
+ - fract
2768
+ - lerp
2769
+ - log
2770
+ - max
2771
+ - min
2772
+ - negate
2773
+ - pow
2774
+ - remainder
2775
+ - round
2776
+ - remap range
2777
+ - saturate
2778
+ - step
2779
+ */
2780
+
2781
+
2782
+ /*
2783
+ Logical operator nodes
2784
+ */
2785
+
2786
+ class NodeAnd extends GraphNode
2787
+ {
2788
+ onCreate() {
2789
+ this.addInput( null, "bool" );
2790
+ this.addInput( null, "bool" );
2791
+ this.addOutput( null, "bool" );
2792
+ }
2793
+
2794
+ onExecute() {
2795
+ var a = this.getInput( 0 ), b = this.getInput( 1 );
2796
+ if( a == undefined || b == undefined )
2797
+ return;
2798
+ this.setOutput( 0, !!( a ) && !!( b ) );
2799
+ }
2800
+ }
534
2801
 
535
- const ctx = this.dom.getContext("2d");
536
- var textMetrics = ctx.measureText( node.name );
2802
+ GraphEditor.registerCustomNode( "logic/And", NodeAnd );
537
2803
 
538
- let sX = 32 + textMetrics.width * 1.475;
2804
+ class NodeOr extends GraphNode
2805
+ {
2806
+ onCreate() {
2807
+ this.addInput( null, "bool" );
2808
+ this.addInput( null, "bool" );
2809
+ this.addOutput( null, "bool" );
2810
+ }
2811
+
2812
+ onExecute() {
2813
+ var a = this.getInput( 0 ), b = this.getInput( 1 );
2814
+ if( a == undefined || b == undefined )
2815
+ return;
2816
+ this.setOutput( 0, !!( a ) || !!( b ) );
2817
+ }
2818
+ }
539
2819
 
540
- const rows = Math.max(1, Math.max(node.inputs.length, node.outputs.length));
541
- let sY = rows * GraphEditor.NODE_ROW_HEIGHT + GraphEditor.NODE_TITLE_HEIGHT;
2820
+ GraphEditor.registerCustomNode( "logic/Or", NodeOr );
542
2821
 
543
- return [sX, sY];
2822
+ class NodeEqual extends GraphNode
2823
+ {
2824
+ onCreate() {
2825
+ this.addInput( null, "float" );
2826
+ this.addInput( null, "float" );
2827
+ this.addOutput( null, "bool" );
2828
+ }
2829
+
2830
+ logicOp( a, b ) {
2831
+ return a == b;
544
2832
  }
545
2833
 
546
- _drawConnections() {
2834
+ onExecute() {
2835
+ var a = this.getInput( 0 ), b = this.getInput( 1 );
2836
+ if( a == undefined || b == undefined )
2837
+ return;
2838
+ this.setOutput( 0, this.logicOp( a, b ) );
2839
+ }
2840
+ }
547
2841
 
548
- console.log( "_drawConnections" );
2842
+ GraphEditor.registerCustomNode( "logic/Equal", NodeEqual );
549
2843
 
550
- // const ctx = this.dom.getContext("2d");
2844
+ class NodeNotEqual extends NodeEqual
2845
+ {
2846
+ logicOp( a, b ) {
2847
+ return a != b;
2848
+ }
2849
+ }
551
2850
 
552
- // let nodes = this._getVisibleNodes();
2851
+ GraphEditor.registerCustomNode( "logic/NotEqual", NodeNotEqual );
553
2852
 
554
- // let start = { x: 50, y: 20 };
555
- // let cp1 = { x: 230, y: 30 };
556
- // let cp2 = { x: 150, y: 80 };
557
- // let end = { x: 250, y: 100 };
2853
+ class NodeLess extends NodeEqual
2854
+ {
2855
+ logicOp( a, b ) {
2856
+ return a < b;
2857
+ }
2858
+ }
558
2859
 
559
- // // Cubic Bézier curve
560
- // ctx.beginPath();
561
- // ctx.moveTo(start.x, start.y);
562
- // ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, end.x, end.y);
563
- // ctx.stroke();
2860
+ GraphEditor.registerCustomNode( "logic/Less", NodeLess );
564
2861
 
565
- // for( let node of nodes )
566
- // {
567
- // // Discard nodes without inputs...
568
- // if (!node.inputs || !node.inputs.length) {
569
- // continue;
570
- // }
2862
+ class NodeLessEqual extends NodeEqual
2863
+ {
2864
+ logicOp( a, b ) {
2865
+ return a <= b;
2866
+ }
2867
+ }
571
2868
 
572
- // for (let input of node.inputs) {
2869
+ GraphEditor.registerCustomNode( "logic/LessEqual", NodeLessEqual );
573
2870
 
2871
+ class NodeGreater extends NodeEqual
2872
+ {
2873
+ logicOp( a, b ) {
2874
+ return a > b;
2875
+ }
2876
+ }
574
2877
 
575
- // }
576
- // }
2878
+ GraphEditor.registerCustomNode( "logic/Greater", NodeGreater );
2879
+
2880
+ class NodeGreaterEqual extends NodeEqual
2881
+ {
2882
+ logicOp( a, b ) {
2883
+ return a >= b;
577
2884
  }
2885
+ }
578
2886
 
579
- // TODO: Return the ones in the viewport
580
- _getVisibleNodes() {
2887
+ GraphEditor.registerCustomNode( "logic/GreaterEqual", NodeGreaterEqual );
581
2888
 
582
- if( !this.graph )
583
- {
584
- console.warn( "No graph set" );
585
- return [];
586
- }
2889
+ class NodeSelect extends GraphNode
2890
+ {
2891
+ onCreate() {
2892
+ this.addInput( "True", "any" );
2893
+ this.addInput( "False", "any" );
2894
+ this.addInput( "Condition", "bool" );
2895
+ this.addOutput( null, "any" );
2896
+ }
2897
+
2898
+ onExecute() {
2899
+ var a = this.getInput( 0 ), b = this.getInput( 1 ), cond = this.getInput( 2 );
2900
+ if( a == undefined || b == undefined || cond == undefined )
2901
+ return;
2902
+ this.setOutput( 0, cond ? a : b );
2903
+ }
2904
+ }
587
2905
 
588
- return this.graph.nodes;
2906
+ GraphEditor.registerCustomNode( "logic/Select", NodeSelect );
2907
+
2908
+ class NodeCompare extends GraphNode
2909
+ {
2910
+ onCreate() {
2911
+ this.addInput( "A", "any" );
2912
+ this.addInput( "B", "any" );
2913
+ this.addInput( "True", "any" );
2914
+ this.addInput( "False", "any" );
2915
+ this.addProperty( "Condition", "select", 'Equal', [ 'Equal', 'Not Equal', 'Less', 'Less Equal', 'Greater', 'Greater Equal' ] );
2916
+ this.addOutput( null, "any" );
2917
+ }
2918
+
2919
+ onExecute() {
2920
+ var a = this.getInput( 0 ), b = this.getInput( 1 ), TrueVal = this.getInput( 2 ), FalseVal = this.getInput( 3 );;
2921
+ var cond = this.properties[ 0 ].value;
2922
+ if( a == undefined || b == undefined || TrueVal == undefined || FalseVal == undefined )
2923
+ return;
2924
+ var output;
2925
+ switch( cond ) {
2926
+ case 'Equal': output = ( a == b ? TrueVal : FalseVal ); break;
2927
+ case 'Not Equal': output = ( a != b ? TrueVal : FalseVal ); break;
2928
+ case 'Less': output = ( a < b ? TrueVal : FalseVal ); break;
2929
+ case 'Less Equal': output = ( a <= b ? TrueVal : FalseVal ); break;
2930
+ case 'Greater': output = ( a > b ? TrueVal : FalseVal ); break;
2931
+ case 'Greater Equal': output = ( a >= b ? TrueVal : FalseVal ); break;
2932
+ }
2933
+ this.setOutput( 0, output );
2934
+ }
2935
+ }
2936
+ NodeCompare.description = "Compare A to B given the selected operator. If true, return value of True else return value of False."
2937
+ GraphEditor.registerCustomNode( "logic/Compare", NodeCompare );
2938
+
2939
+ /*
2940
+ Event nodes
2941
+ */
2942
+
2943
+ class NodeKeyDown extends GraphNode
2944
+ {
2945
+ onCreate() {
2946
+ this.addOutput( null, "bool" );
2947
+ this.addProperty( "Key", "string", " " );
2948
+ }
2949
+
2950
+ onExecute() {
2951
+ this.setOutput( 0, !!this.editor.keys[ this.properties[ 0 ].value ] );
589
2952
  }
2953
+ }
2954
+
2955
+ GraphEditor.registerCustomNode( "events/KeyDown", NodeKeyDown );
590
2956
 
2957
+ /*
2958
+ Input nodes
2959
+ */
2960
+
2961
+ class NodeString extends GraphNode
2962
+ {
2963
+ onCreate() {
2964
+ this.addOutput( null, "string" );
2965
+ this.addProperty( null, "string", "text" );
2966
+ }
2967
+
2968
+ onExecute() {
2969
+ this.setOutput( 0, this.properties[ 0 ].value );
2970
+ }
591
2971
  }
592
2972
 
593
- LX.GraphEditor = GraphEditor;
2973
+ GraphEditor.registerCustomNode( "inputs/String", NodeString );
594
2974
 
595
- /**
596
- * @class Graph
597
- */
2975
+ class NodeFloat extends GraphNode
2976
+ {
2977
+ onCreate() {
2978
+ this.addOutput( null, "float" );
2979
+ this.addProperty( null, "float", 0.0 );
2980
+ }
2981
+
2982
+ onExecute() {
2983
+ this.setOutput( 0, this.properties[ 0 ].value );
2984
+ }
2985
+ }
598
2986
 
599
- class Graph {
2987
+ GraphEditor.registerCustomNode( "inputs/Float", NodeFloat );
600
2988
 
601
- /**
602
- * @param {*} options
603
- *
604
- */
2989
+ class NodeVector2 extends GraphNode
2990
+ {
2991
+ onCreate() {
2992
+ this.addOutput( "Value", "vec2" );
2993
+ this.addProperty( "Value", "vec2", [ 0, 0 ] );
2994
+ }
2995
+
2996
+ onExecute() {
2997
+ this.setOutput( 0, this.properties[ 0 ].value );
2998
+ }
2999
+ }
605
3000
 
606
- constructor( options = {} ) {
3001
+ GraphEditor.registerCustomNode( "inputs/Vector2", NodeVector2 );
607
3002
 
608
- // Nodes
3003
+ class NodeVector3 extends GraphNode
3004
+ {
3005
+ onCreate() {
3006
+ this.addOutput( "Value", "vec3" );
3007
+ this.addProperty( "Value", "vec3", [ 0, 0, 0 ] );
3008
+ }
3009
+
3010
+ onExecute() {
3011
+ this.setOutput( 0, this.properties[ 0 ].value );
3012
+ }
3013
+ }
609
3014
 
610
- this.nodes = [
611
- new GraphNode({
612
- name: "Node 1",
613
- position: new LX.vec2( 200, 200 ),
614
- inputs: [
615
- {
616
- name: "Speed",
617
- type: "float"
618
- },
619
- {
620
- name: "Offset",
621
- type: "vec2"
622
- }
623
- ]
624
- }),
625
- new GraphNode({
626
- name: "Node 2",
627
- size: new LX.vec2( 120, 100 ),
628
- position: new LX.vec2( 500, 350 ),
629
- color: "#f7884c",
630
- inputs: [],
631
- outputs: [
632
- {
633
- name: "Speed",
634
- type: "float"
635
- },
636
- {
637
- name: "Offset",
638
- type: "vec2"
639
- },
640
- {
641
- name: "Loop",
642
- type: "bool"
643
- }
644
- ]
645
- }),
646
- new GraphNode({
647
- name: "Node 3",
648
- position: new LX.vec2( 200, 400 ),
649
- inputs: [
650
- {
651
- name: "Speed",
652
- type: "float"
653
- },
654
- {
655
- name: "Offset",
656
- type: "vec2"
657
- }
658
- ],
659
- outputs: [
660
- {
661
- name: "Speed",
662
- type: "float"
663
- },
664
- {
665
- name: "Offset",
666
- type: "vec3"
667
- },
668
- {
669
- name: "Loop",
670
- type: "bool"
671
- }
672
- ]
673
- }),
674
- new GraphNode({
675
- name: "Add",
676
- position: new LX.vec2( 300, 300 ),
677
- inputs: [
678
- {
679
- type: "float"
680
- },
681
- {
682
- type: "float"
683
- }
684
- ],
685
- outputs: [
686
- {
687
- type: "float"
688
- }
689
- ]
690
- })
691
- ];
3015
+ GraphEditor.registerCustomNode( "inputs/Vector3", NodeVector3 );
3016
+
3017
+ class NodeVector4 extends GraphNode
3018
+ {
3019
+ onCreate() {
3020
+ this.addOutput( "Value", "vec4" );
3021
+ this.addProperty( "Value", "vec4", [ 0, 0, 0, 0 ] );
3022
+ }
3023
+
3024
+ onExecute() {
3025
+ this.setOutput( 0, this.properties[ 0 ].value );
692
3026
  }
693
3027
  }
694
3028
 
695
- LX.Graph = Graph;
3029
+ GraphEditor.registerCustomNode( "inputs/Vector4", NodeVector4 );
696
3030
 
697
- /**
698
- * @class GraphNode
699
- */
3031
+ /*
3032
+ Variable nodes
3033
+ */
700
3034
 
701
- class GraphNode {
3035
+ class NodeSetVariable extends GraphNode
3036
+ {
3037
+ onCreate() {
3038
+ this.addInput( "Value", "any" );
3039
+ this.addOutput( null, "any" );
3040
+ this.addProperty( "Name", "string", "" );
3041
+ }
3042
+
3043
+ onExecute() {
3044
+ var varName = this.getInput( 0 );
3045
+ if( varName == undefined )
3046
+ return;
3047
+ var varValue = this.getInput( 1 );
3048
+ if( varValue == undefined )
3049
+ return;
3050
+ this.graph.setVariable( varName, varValue );
3051
+ this.setOutput( 0, varValue );
3052
+ }
3053
+ }
702
3054
 
703
- /**
704
- * @param {*} options
705
- *
706
- */
3055
+ NodeSetVariable.title = "Set Variable";
3056
+ GraphEditor.registerCustomNode( "variables/SetVariable", NodeSetVariable );
707
3057
 
708
- constructor( options = {} ) {
3058
+ class NodeGetVariable extends GraphNode
3059
+ {
3060
+ onCreate() {
3061
+ this.addOutput( null, "any" );
3062
+ this.addProperty( "Name", "string", "" );
3063
+ }
709
3064
 
710
- this.name = options.name ?? "Unnamed";
711
- this.size = options.size;
712
- this.position = options.position ?? new LX.vec2( 0, 0 );
713
- this.color = options.color;
714
-
715
- this.inputs = options.inputs ?? [];
716
- this.outputs = options.outputs ?? [];
3065
+ onExecute() {
3066
+ var varName = this.getInput( 0 );
3067
+ if( varName == undefined )
3068
+ return;
3069
+ var data = this.editor.getVariable( varName );
3070
+ if( data != undefined )
3071
+ this.setOutput( 0, data );
717
3072
  }
3073
+ }
3074
+
3075
+ NodeGetVariable.title = "Get Variable";
3076
+ GraphEditor.registerCustomNode( "variables/GetVariable", NodeGetVariable );
718
3077
 
719
- computeSize() {
3078
+ /*
3079
+ System nodes
3080
+ */
720
3081
 
721
- let sX = 16 + this.name.length * 10;
3082
+ class NodeConsoleLog extends GraphNode
3083
+ {
3084
+ onCreate() {
3085
+ this.addInput( null, "any" );
3086
+ }
3087
+
3088
+ onExecute() {
3089
+ var data = this.getInput( 0 );
3090
+ if( data == undefined )
3091
+ return;
3092
+ console.log( data );
3093
+ }
3094
+ }
722
3095
 
723
- const rows = Math.max(1, Math.max(this.inputs.length, this.outputs.length));
724
- let sY = rows * GraphEditor.NODE_ROW_HEIGHT + GraphEditor.NODE_TITLE_HEIGHT;
3096
+ NodeConsoleLog.title = "Console Log";
3097
+ GraphEditor.registerCustomNode( "system/ConsoleLog", NodeConsoleLog );
725
3098
 
726
- return [sX, sY];
3099
+ class NodeMain extends GraphNode
3100
+ {
3101
+ onCreate() {
3102
+ this.addInput( "a", "float" );
3103
+ this.addInput( "b", "bool" );
3104
+ this.addInput( "Color", "vec4" );
727
3105
  }
3106
+
3107
+ onExecute() {
3108
+ var data = this.getInput( 2 );
3109
+ if( data == undefined )
3110
+ return;
3111
+ console.log( data );
3112
+ };
728
3113
  }
729
3114
 
730
- LX.GraphNode = GraphNode;
3115
+ GraphEditor.registerCustomNode( "system/Main", NodeMain );
731
3116
 
732
3117
  export { GraphEditor, Graph, GraphNode };