lexgui 0.7.15 → 8.1.0

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