lexgui 0.1.28 → 0.1.30

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