dragon-graph-lib 0.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.
@@ -0,0 +1,920 @@
1
+
2
+ function to_slug(string)
3
+ {
4
+ return string.replace(/[^a-z0-9_-]+/ig, "-").toLowerCase();
5
+ }
6
+
7
+ export class Socket
8
+ {
9
+ constructor(name, multi_input=false)
10
+ {
11
+ this.name = name;
12
+ this.multi_input = multi_input;
13
+ this.accepts_incoming = new Set();
14
+ }
15
+
16
+ accept(type)
17
+ {
18
+ let type_name = type instanceof Socket ? type.name : type;
19
+ this.accepts_incoming.add(type_name);
20
+ }
21
+
22
+ to_dom()
23
+ {
24
+ let elem = document.createElement("div");
25
+ elem.classList.add("dgl-socket", "socket-" + to_slug(this.name));
26
+ return elem;
27
+ }
28
+
29
+ output_can_connect(input_socket)
30
+ {
31
+ return input_socket.input_can_connect(this);
32
+ }
33
+
34
+ input_can_connect(output_socket)
35
+ {
36
+ return output_socket.name == this.name || this.accepts_incoming.has(output_socket.name);
37
+ }
38
+
39
+ static can_connect(output_socket, input_socket)
40
+ {
41
+ return output_socket.output_can_connect(input_socket);
42
+ }
43
+ }
44
+
45
+
46
+ class VisualElement
47
+ {
48
+ constructor()
49
+ {
50
+ this.dom = null;
51
+ }
52
+
53
+ to_dom()
54
+ {
55
+ this.dom = this.build_dom();
56
+ this.update_dom();
57
+ return this.dom;
58
+ }
59
+
60
+ build_dom()
61
+ {
62
+ return document.createElement("div");
63
+ }
64
+
65
+ update_dom()
66
+ {
67
+ }
68
+
69
+ destroy()
70
+ {
71
+ if ( this.dom && this.dom.parentNode )
72
+ {
73
+ this.dom.parentNode.removeChild(this.dom);
74
+ this.dom = null;
75
+ }
76
+ }
77
+ }
78
+
79
+ export class Connection extends VisualElement
80
+ {
81
+ margin = 10;
82
+
83
+ constructor(output, input)
84
+ {
85
+ super();
86
+ this.output = output;
87
+ this.input = input;
88
+ }
89
+
90
+ connect()
91
+ {
92
+ this.output.connect(this);
93
+ this.input.connect(this);
94
+ }
95
+
96
+ disconnect()
97
+ {
98
+ this.destroy();
99
+ this.output.disconnect(this);
100
+ this.input.disconnect(this);
101
+ }
102
+
103
+ static path_d(x1, y1, x2, y2)
104
+ {
105
+ let w = x2 - x1;
106
+ let h = y2 - y1;
107
+
108
+ return `M ${x1} ${y1}
109
+ C ${x1 + w / 3} ${y1}
110
+ ${x2 - w / 3} ${y2}
111
+ ${x2} ${y2}
112
+ `;
113
+ }
114
+ }
115
+
116
+ class NodeIO extends VisualElement
117
+ {
118
+ constructor(node, name, label, socket)
119
+ {
120
+ super();
121
+ this.node = node;
122
+ this.name = name;
123
+ this.label = label;
124
+ this.socket = socket;
125
+ this.connections = [];
126
+ this.dom_items = {};
127
+ }
128
+
129
+ update_dom()
130
+ {
131
+ this.dom_items.label.textContent = this.label;
132
+ }
133
+
134
+ connect(connection)
135
+ {
136
+ this.connections.push(connection);
137
+ }
138
+
139
+ disconnect(connection)
140
+ {
141
+ let index = this.connections.indexOf(connection);
142
+ if ( index != -1 )
143
+ this.connections.splice(index, 1);
144
+ }
145
+ }
146
+
147
+
148
+ export class NodeInput extends NodeIO
149
+ {
150
+ constructor(node, name, label, socket, control)
151
+ {
152
+ super(node, name, label, socket);
153
+ this.control = control;
154
+ }
155
+
156
+ build_dom()
157
+ {
158
+ let container = document.createElement("div");
159
+ container.classList.add("dgl-input");
160
+ if ( this.socket )
161
+ {
162
+ this.dom_items.socket = container.appendChild(this.socket.to_dom());
163
+ this.dom_items.socket.classList.add("dgl-input-socket");
164
+ container.classList.add("dgl-socketed");
165
+ }
166
+
167
+ this.dom_items.label = document.createElement("span");
168
+ this.dom_items.label.textContent = this.name;
169
+ this.dom_items.label.classList.add("dgl-label", "dgl-input-label");
170
+ container.appendChild(this.dom_items.label);
171
+
172
+ if ( this.control )
173
+ container.appendChild(this.control);
174
+
175
+ return container;
176
+ }
177
+
178
+ connect(connection)
179
+ {
180
+ if ( !this.socket.multi_input )
181
+ {
182
+ for ( let conn of this.connections )
183
+ conn.disconnect();
184
+ this.connections = [];
185
+ }
186
+
187
+ this.connections.push(connection);
188
+
189
+ if ( this.control )
190
+ this.control.style.visibility = "hidden";
191
+ }
192
+
193
+ set_value(value)
194
+ {
195
+ if ( this.control )
196
+ {
197
+ this.control.value = value;
198
+ }
199
+ }
200
+
201
+ get_value()
202
+ {
203
+ if ( this.connections.length )
204
+ {
205
+ if ( this.socket.multi_input )
206
+ return this.connections.map(c => c.output.get_value());
207
+ return this.connections[0].output.get_value();
208
+ }
209
+
210
+ if ( !this.control )
211
+ {
212
+ if ( this.socket && this.socket.multi_input )
213
+ return [];
214
+ return null;
215
+ }
216
+
217
+ let value = this.control.value
218
+ if ( this.socket && this.socket.multi_input )
219
+ return [value];
220
+ return value;
221
+ }
222
+
223
+ disconnect(connection)
224
+ {
225
+ super.disconnect(connection);
226
+ if ( this.control && this.connections.length == 0 )
227
+ this.control.style.visibility = "visible";
228
+ }
229
+ }
230
+
231
+ export class NodeOutput extends NodeIO
232
+ {
233
+ constructor(node, name, label, socket)
234
+ {
235
+ super(node, name, label, socket);
236
+ this.value = undefined;
237
+ }
238
+
239
+ build_dom()
240
+ {
241
+ let container = document.createElement("div");
242
+ container.classList.add("dgl-output");
243
+ this.dom_items.label = document.createElement("span");
244
+ this.dom_items.label.textContent = this.name;
245
+ container.appendChild(this.dom_items.label);
246
+ this.dom_items.label.classList.add("dgl-label", "dgl-output-label");
247
+ this.dom_items.socket = container.appendChild(this.socket.to_dom());
248
+ this.dom_items.socket.classList.add("dgl-output-socket");
249
+
250
+ return container;
251
+ }
252
+
253
+ clear_value()
254
+ {
255
+ this.value = undefined;
256
+ }
257
+
258
+ set_value(value)
259
+ {
260
+ this.value = value;
261
+ }
262
+
263
+ get_value()
264
+ {
265
+ if ( this.value == undefined )
266
+ this.node.evaluate();
267
+ return this.value;
268
+ }
269
+ }
270
+
271
+
272
+ function force_default(ev)
273
+ {
274
+ ev.stopPropagation();
275
+ }
276
+
277
+ export function NodeControl(type, attrs={})
278
+ {
279
+ let elem = document.createElement("input");
280
+ elem.type = type;
281
+ elem.classList.add("dgl-control");
282
+ for ( let [k, v] of Object.entries(attrs) )
283
+ elem.setAttribute(k, v);
284
+ elem.addEventListener("mousedown", force_default);
285
+ elem.addEventListener("mouseup", force_default);
286
+ elem.addEventListener("mousemove", force_default);
287
+ elem.addEventListener("wheel", force_default);
288
+ return elem;
289
+ }
290
+
291
+ export class Node extends VisualElement
292
+ {
293
+ constructor(title, type)
294
+ {
295
+ super();
296
+ this.title = title;
297
+ this.outputs = {};
298
+ this.inputs = {};
299
+ this.preview = null;
300
+ this.type = type;
301
+ this.x = 0;
302
+ this.y = 0;
303
+ }
304
+
305
+ set_input_value(data)
306
+ {
307
+ for ( let [k, v] of Object.entries(this.inputs) )
308
+ {
309
+ if ( k in data )
310
+ v.set_value(data[k]);
311
+ }
312
+ }
313
+
314
+ get_input_value()
315
+ {
316
+ let data = {};
317
+ for ( let [k, v] of Object.entries(this.inputs) )
318
+ {
319
+ data[k] = v.get_value();
320
+ }
321
+ return data;
322
+ }
323
+
324
+ connect(output_name, node, input_name)
325
+ {
326
+ let output = this.outputs[output_name];
327
+ let input = node.inputs[input_name];
328
+ if ( !output || !input )
329
+ return null;
330
+
331
+ if ( !Socket.can_connect(output.socket, input.socket) )
332
+ return null;
333
+
334
+ let connection = new Connection(output, input);
335
+ connection.connect();
336
+ return connection;
337
+ }
338
+
339
+ add_input(name, title, socket, control)
340
+ {
341
+ this.inputs[name] = new NodeInput(this, name, title, socket, control);
342
+ }
343
+
344
+ add_output(name, title, socket)
345
+ {
346
+ this.outputs[name] = new NodeOutput(this, name, title, socket);
347
+ }
348
+
349
+ update_dom()
350
+ {
351
+ this.dom.style.transform = `translate(${this.x}px, ${this.y}px`;
352
+ }
353
+
354
+ build_dom()
355
+ {
356
+ let dom = document.createElement("div");
357
+ dom.classList.add("dgl-node", "node-" + to_slug(this.title));
358
+
359
+ let title = dom.appendChild(document.createElement("div"));
360
+ title.textContent = this.title;
361
+ title.classList.add("dgl-title");
362
+
363
+ for ( let output of Object.values(this.outputs) )
364
+ dom.appendChild(output.to_dom());
365
+
366
+ this.preview = dom.appendChild(document.createElement("div"));
367
+ this.preview.classList.add("dgl-preview");
368
+
369
+ for ( let input of Object.values(this.inputs) )
370
+ dom.appendChild(input.to_dom());
371
+
372
+ return dom;
373
+ }
374
+
375
+ evaluate()
376
+ {
377
+ if ( this.type )
378
+ this.type.evaluate_node(this);
379
+ }
380
+
381
+ all_connections()
382
+ {
383
+ return Object.values(this.inputs).concat(Object.values(this.outputs))
384
+ .map(c => c.connections)
385
+ .reduce((a, b) => a.concat(b), [])
386
+ ;
387
+ }
388
+ }
389
+
390
+ export class NodeType
391
+ {
392
+ constructor(name)
393
+ {
394
+ this.name = name;
395
+ }
396
+
397
+ create_node()
398
+ {
399
+ let node = new Node(this.name, this);
400
+ this.populate_node(node);
401
+ return node;
402
+ }
403
+
404
+ populate_node(node)
405
+ {
406
+ }
407
+
408
+ update_node(node)
409
+ {
410
+
411
+ }
412
+
413
+ evaluate_node(node)
414
+ {
415
+
416
+ }
417
+ }
418
+
419
+ export class ContextMenu extends VisualElement
420
+ {
421
+ constructor(actions)
422
+ {
423
+ super();
424
+ this.actions = actions;
425
+ }
426
+
427
+ to_dom(editor)
428
+ {
429
+ this.dom = this.make_menu("dgl-menu", this.actions, editor);
430
+ this.dom.style.display = "none";
431
+ return this.dom;
432
+ }
433
+
434
+ make_action(title, action, editor)
435
+ {
436
+ let onclick = action;
437
+ if ( action instanceof NodeType )
438
+ onclick = () => editor.create_node(action);
439
+
440
+ let item = document.createElement("li");
441
+ item.classList.add("dgl-menu-item");
442
+ item.addEventListener("mousedown", (ev) => {
443
+ onclick();
444
+ ev.stopPropagation();
445
+ ev.preventDefault();
446
+ this.dom.style.display = "none";
447
+ });
448
+ let label = item.appendChild(document.createElement("span"));
449
+ label.textContent = title;
450
+ return item;
451
+ }
452
+
453
+ make_menu(cls, actions, editor)
454
+ {
455
+ let dom = document.createElement("ul");
456
+ dom.classList.add(cls);
457
+
458
+ if ( Array.isArray(actions) )
459
+ {
460
+ for ( let action of actions )
461
+ {
462
+ dom.appendChild(this.make_action(action.name, action, editor));
463
+ }
464
+ }
465
+ else
466
+ {
467
+ for ( let [name, val] of Object.entries(actions) )
468
+ {
469
+ if ( Array.isArray(val) || (typeof val == "object" && !(val instanceof NodeType)) )
470
+ dom.appendChild(this.make_submenu(name, val, editor));
471
+ else
472
+ dom.appendChild(this.make_action(name, val));
473
+ }
474
+ }
475
+ return dom;
476
+ }
477
+
478
+ make_submenu(name, actions, editor)
479
+ {
480
+ let item = document.createElement("li");
481
+ item.classList.add("dgl-menu-item");
482
+ let label = item.appendChild(document.createElement("span"));
483
+ label.textContent = name;
484
+ label.classList.add("dgl-submenu-label");
485
+ item.appendChild(this.make_menu("dgl-submenu", actions, editor));
486
+ return item;
487
+ }
488
+
489
+ show(x, y)
490
+ {
491
+ this.dom.style.display = "";
492
+ this.dom.style.left = x + "px";
493
+ this.dom.style.top = y + "px";
494
+ }
495
+
496
+ close()
497
+ {
498
+ this.dom.style.display = "none";
499
+ }
500
+ }
501
+
502
+ export class Editor
503
+ {
504
+ constructor(container, context_menu={})
505
+ {
506
+ this.nodes = [];
507
+
508
+ this.container = container;
509
+
510
+ this.area = container.appendChild(document.createElement("div"));
511
+ container.classList.add("dgl-editor");
512
+ this.area.classList.add("dgl-area");
513
+
514
+ this.connection_parent = document.createElementNS("http://www.w3.org/2000/svg", "svg");
515
+ this.area.appendChild(this.connection_parent);
516
+ this.connection_parent.classList.add("dgl-connection");
517
+ this.dragging_connection = document.createElementNS("http://www.w3.org/2000/svg", "path");
518
+ this.connection_parent.appendChild(this.dragging_connection);
519
+
520
+ this.selected_node = null;
521
+ this.scale = 1;
522
+ this.offset_x = 0;
523
+ this.offset_y = 0;
524
+ this.drag_start_x = 0;
525
+ this.drag_start_y = 0;
526
+ this.drag = null;
527
+ this.drag_connector = null;
528
+ this.cursor_x = 0;
529
+ this.cursor_y = 0;
530
+
531
+ this.container.addEventListener("mousedown", this._on_area_mouse_down.bind(this));
532
+ this.container.addEventListener("mousemove", this._on_area_mouse_move.bind(this));
533
+ this.container.addEventListener("mouseup", this._on_area_mouse_up.bind(this));
534
+ this.container.addEventListener("wheel", this._on_area_wheel.bind(this));
535
+ this.container.addEventListener("contextmenu", this._on_area_menu.bind(this));
536
+
537
+ this.context_menu = new ContextMenu(context_menu);
538
+ this.node_menu = new ContextMenu({
539
+ "Delete": () => {
540
+ if ( this.selected_node )
541
+ this.remove_node(this.selected_node);
542
+ },
543
+ "Duplicate": () => {
544
+ if ( this.selected_node ) {
545
+ let node = this.create_node(this.selected_node.type, this.selected_node.get_input_value());
546
+ node.x = this.selected_node.x;
547
+ node.y = this.selected_node.y;
548
+ this._select_node(node);
549
+
550
+ }
551
+ }
552
+ });
553
+
554
+ this.container.appendChild(this.node_menu.to_dom(this));
555
+ this.container.appendChild(this.context_menu.to_dom(this));
556
+ }
557
+
558
+ create_node(type, data={})
559
+ {
560
+ let node = type.create_node();
561
+ node.set_input_value(data);
562
+ node.x = this.cursor_x;
563
+ node.y = this.cursor_y;
564
+ this.nodes.push(node);
565
+ let node_dom = node.to_dom();
566
+ this.area.appendChild(node_dom);
567
+
568
+ node_dom.addEventListener("mousedown", ev => this._on_node_mouse_down(ev, node));
569
+ node_dom.addEventListener("contextmenu", ev => this._on_node_menu(ev, node));
570
+
571
+ return node;
572
+ }
573
+
574
+ auto_layout()
575
+ {
576
+ setTimeout(() => this._on_auto_layout());
577
+ }
578
+
579
+ _auto_layout_traverse(node, depth)
580
+ {
581
+ if ( node._depth >= depth )
582
+ return depth;
583
+
584
+ if ( node._depth < depth )
585
+ node._depth = depth;
586
+
587
+ let max = depth;
588
+
589
+ for ( let inp of Object.values(node.inputs) )
590
+ {
591
+ for ( let conn of inp.connections )
592
+ {
593
+ let cd = this._auto_layout_traverse(conn.output.node, depth + 1);
594
+ if ( cd > max )
595
+ max = cd;
596
+ }
597
+ }
598
+
599
+ return max;
600
+ }
601
+
602
+ _on_auto_layout()
603
+ {
604
+ for ( let node of this.nodes )
605
+ {
606
+ node._depth = -1;
607
+ }
608
+ let max_depth = 0;
609
+
610
+ for ( let node of this.nodes )
611
+ {
612
+ let out_conns = 0;
613
+ for ( let outp of Object.values(node.outputs) )
614
+ {
615
+ out_conns += outp.connections.length;
616
+ }
617
+ if ( out_conns == 0 )
618
+ {
619
+ let depth = this._auto_layout_traverse(node, 0);
620
+ if ( depth > max_depth )
621
+ max_depth = depth;
622
+ }
623
+ }
624
+
625
+ let layers = new Array(max_depth + 1).fill(0).map(() => []);
626
+ for ( let node of this.nodes )
627
+ {
628
+ layers[max_depth - node._depth].push(node);
629
+ }
630
+
631
+ let xgap = 48;
632
+ let ygap = 12;
633
+ let x = 0;
634
+ let layer_heights = [];
635
+ let max_height = 0;
636
+ for ( let layer of layers )
637
+ {
638
+ let width = 0;
639
+ let y = 0;
640
+ for ( let node of layer )
641
+ {
642
+ node.x = x;
643
+ node.y = y;
644
+ let rect = node.dom.getBoundingClientRect();
645
+ if ( rect.width > width )
646
+ width = rect.width;
647
+ y += rect.height + ygap;
648
+ }
649
+
650
+ x += width + xgap;
651
+ let height = y - ygap;
652
+ layer_heights.push(height);
653
+ if ( height > max_height )
654
+ max_height = height;
655
+ }
656
+
657
+ // Center vertically
658
+ for ( let i = 0; i < layers.length; i++ )
659
+ {
660
+ let offset = (max_height - layer_heights[i]) / 2;
661
+ for ( let node of layers[i] )
662
+ node.y += offset;
663
+ }
664
+
665
+ this._refresh_graph();
666
+ }
667
+
668
+ connect(output_node, output_name, input_node, input_name)
669
+ {
670
+ let conn = output_node.connect(output_name, input_node, input_name);
671
+ if ( conn )
672
+ {
673
+ conn.dom = document.createElementNS("http://www.w3.org/2000/svg", "path");
674
+ conn.dom.classList.add("connection-from-" + to_slug(output_node.outputs[output_name].socket.name));
675
+ conn.dom.classList.add("connection-to-" + to_slug(input_node.inputs[input_name].socket.name));
676
+ this.connection_parent.appendChild(conn.dom);
677
+ this._update_connection(conn);
678
+ this._refresh_graph();
679
+ }
680
+ return conn;
681
+ }
682
+
683
+ disconnect(connection)
684
+ {
685
+ connection.disconnect();
686
+ this._refresh_graph();
687
+ }
688
+
689
+ _update_connection(conn)
690
+ {
691
+ let [x1, y1] = this._connection_point(conn.output);
692
+ let [x2, y2] = this._connection_point(conn.input);
693
+ conn.dom.setAttribute("d", Connection.path_d(x1, y1, x2, y2));
694
+ }
695
+
696
+ _close_menus()
697
+ {
698
+ this.context_menu.close();
699
+ this.node_menu.close();
700
+ }
701
+
702
+ _on_area_mouse_down(ev)
703
+ {
704
+ this._close_menus();
705
+ if ( ev.button == 0 )
706
+ {
707
+ this._select_node(null);
708
+ }
709
+ else if ( ev.button == 1 )
710
+ {
711
+ let rect = this.container.getBoundingClientRect();
712
+ this.drag_start_x = ev.clientX - rect.left;
713
+ this.drag_start_y = ev.clientY - rect.top;
714
+ this.drag_start_ox = this.offset_x;
715
+ this.drag_start_oy = this.offset_y;
716
+ this.drag = "pan";
717
+ }
718
+
719
+ ev.stopPropagation();
720
+ ev.preventDefault();
721
+ }
722
+
723
+ _connection_point(connector)
724
+ {
725
+
726
+ let rect = connector.dom_items.socket.getBoundingClientRect();
727
+ let px = rect.x + rect.width / 2;
728
+ let py = rect.y + rect.height / 2;
729
+ let crect = this.container.getBoundingClientRect();
730
+ return [
731
+ (px - crect.left - this.offset_x) / this.scale,
732
+ (py - crect.top - this.offset_y) / this.scale
733
+ ];
734
+ }
735
+
736
+ _on_area_mouse_move(ev)
737
+ {
738
+ let rect = this.container.getBoundingClientRect();
739
+ let drag_x = ev.clientX - rect.left;
740
+ let drag_y = ev.clientY - rect.top;
741
+ this.cursor_x = (drag_x - this.offset_x) / this.scale;
742
+ this.cursor_y = (drag_y - this.offset_y) / this.scale;
743
+
744
+ if ( this.drag )
745
+ {
746
+ let delta_x = drag_x - this.drag_start_x;
747
+ let delta_y = drag_y - this.drag_start_y;
748
+
749
+ if ( this.drag == "pan" )
750
+ {
751
+ this.offset_x = this.drag_start_ox + delta_x;
752
+ this.offset_y = this.drag_start_oy + delta_y;
753
+ }
754
+ else if ( this.drag == "node" )
755
+ {
756
+ let x = this.drag_start_ox + delta_x / this.scale;
757
+ let y = this.drag_start_oy + delta_y / this.scale;
758
+ this.selected_node.x = x;
759
+ this.selected_node.y = y;
760
+ this.selected_node.dom.style.transform = `translate(${x}px, ${y}px`;
761
+ for ( let connection of this.selected_node.all_connections() )
762
+ this._update_connection(connection);
763
+ }
764
+ else if ( this.drag == "socket" )
765
+ {
766
+ let x1 = (drag_x - this.offset_x) / this.scale;
767
+ let y1 = (drag_y - this.offset_y) / this.scale;
768
+ let [x2, y2] = this._connection_point(this.drag_connector);
769
+
770
+ this.dragging_connection.setAttribute("d", Connection.path_d(x1, y1, x2, y2));
771
+ }
772
+
773
+ this._apply_transform();
774
+ }
775
+
776
+ ev.stopPropagation();
777
+ ev.preventDefault();
778
+ }
779
+
780
+ _apply_transform()
781
+ {
782
+ this.area.style.transform = `translate(${this.offset_x}px, ${this.offset_y}px) scale(${this.scale})`;
783
+ }
784
+
785
+ _refresh_graph()
786
+ {
787
+ for ( let node of this.nodes )
788
+ {
789
+ node.evaluate();
790
+ node.update_dom();
791
+ }
792
+
793
+ window.setTimeout(() => {
794
+ // TODO could optimize this, currently each connection is updated twice
795
+ for ( let node of this.nodes )
796
+ {
797
+ for ( let connection of node.all_connections() )
798
+ this._update_connection(connection);
799
+ }
800
+ });
801
+ }
802
+
803
+ _get_dest_connector(ev)
804
+ {
805
+ let elem = document.elementFromPoint(ev.clientX, ev.clientY);
806
+ if ( !elem.classList.contains("dgl-socket") )
807
+ return null;
808
+
809
+ let conns = "inputs";
810
+ if ( this.drag_connector instanceof NodeInput )
811
+ conns = "outputs";
812
+
813
+ for ( let node of this.nodes )
814
+ {
815
+ for ( let conn of Object.values(node[conns]) )
816
+ if ( conn.dom_items.socket === elem )
817
+ return conn;
818
+ }
819
+ return null;
820
+ }
821
+
822
+ _on_area_mouse_up(ev)
823
+ {
824
+ if ( this.drag == "socket" )
825
+ {
826
+ let dest = this._get_dest_connector(ev);
827
+ if ( dest && dest.node !== this.selected_node )
828
+ {
829
+ if ( this.drag_connector instanceof NodeInput )
830
+ this.connect(dest.node, dest.name, this.selected_node, this.drag_connector.name);
831
+ else
832
+ this.connect(this.selected_node, this.drag_connector.name, dest.node, dest.name);
833
+ }
834
+ this.drag_connector = null;
835
+ this.dragging_connection.setAttribute("d", "");
836
+ }
837
+ this.drag = null;
838
+ ev.stopPropagation();
839
+ ev.preventDefault();
840
+ }
841
+
842
+ _on_area_wheel(ev)
843
+ {
844
+ ev.preventDefault();
845
+ ev.stopPropagation();
846
+
847
+ let rect = this.container.getBoundingClientRect();
848
+ let mouse_x = ev.clientX - rect.left;
849
+ let mouse_y = ev.clientY - rect.top;
850
+
851
+ let unscaled_x = (mouse_x - this.offset_x) / this.scale;
852
+ let unscaled_y = (mouse_y - this.offset_y) / this.scale;
853
+
854
+ let delta = ev.deltaY > 0 ? 0.9 : 1.1;
855
+ this.scale *= delta;
856
+
857
+ this.offset_x = mouse_x - unscaled_x * this.scale;
858
+ this.offset_y = mouse_y - unscaled_y * this.scale;
859
+ this._apply_transform();
860
+ }
861
+
862
+ _select_node(node)
863
+ {
864
+ this.area.querySelectorAll(".dgl-node").forEach(e => e.classList.remove("dgl-selected"));
865
+ if ( node )
866
+ node.dom.classList.add("dgl-selected");
867
+ this.selected_node = node;
868
+ }
869
+
870
+ _on_node_mouse_down(ev, node)
871
+ {
872
+ this._close_menus();
873
+ if ( ev.button == 0 )
874
+ {
875
+ ev.stopPropagation();
876
+ this._select_node(node);
877
+ this.drag = "node";
878
+ let rect = this.container.getBoundingClientRect();
879
+ this.drag_start_x = ev.clientX - rect.left;
880
+ this.drag_start_y = ev.clientY - rect.top;
881
+ this.drag_start_ox = node.x;
882
+ this.drag_start_oy = node.y;
883
+ if ( ev.target.classList.contains("dgl-socket") )
884
+ {
885
+ this.drag = "socket";
886
+ let is_input = ev.target.classList.contains("dgl-input-socket");
887
+ let connectors = is_input ? node.inputs : node.outputs;
888
+ for ( let connector of Object.values(connectors) )
889
+ {
890
+ if ( connector.dom_items.socket === ev.target)
891
+ {
892
+ this.drag_connector = connector;
893
+ if ( is_input )
894
+ {
895
+ for ( let conn of connector.connections.slice() )
896
+ conn.disconnect(conn);
897
+ this._refresh_graph();
898
+ }
899
+ break;
900
+ }
901
+ }
902
+ }
903
+ }
904
+ }
905
+
906
+ _on_node_menu(ev, node)
907
+ {
908
+ this._select_node(node);
909
+ this.node_menu.show(ev.clientX, ev.clientY);
910
+ ev.stopPropagation();
911
+ ev.preventDefault();
912
+ }
913
+
914
+ _on_area_menu(ev)
915
+ {
916
+ this.context_menu.show(ev.clientX, ev.clientY);
917
+ ev.stopPropagation();
918
+ ev.preventDefault();
919
+ }
920
+ }