@elicdavis/node-flow 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.
Files changed (68) hide show
  1. package/.github/workflows/deploy.yml +57 -0
  2. package/README.md +159 -0
  3. package/dev.tsconfig.json +18 -0
  4. package/docs/promotional-image.png +0 -0
  5. package/esbuild.ts +32 -0
  6. package/index.html +311 -0
  7. package/package.json +50 -0
  8. package/src/camera.ts +30 -0
  9. package/src/connection.ts +249 -0
  10. package/src/contextMenu.ts +367 -0
  11. package/src/draggable.ts +13 -0
  12. package/src/elements/base.ts +192 -0
  13. package/src/elements/container.ts +119 -0
  14. package/src/elements/interfaces.ts +50 -0
  15. package/src/elements/text.ts +86 -0
  16. package/src/graph.ts +535 -0
  17. package/src/graphSubsystem.ts +31 -0
  18. package/src/index.ts +28 -0
  19. package/src/input.ts +153 -0
  20. package/src/markdown/entry.ts +323 -0
  21. package/src/markdown/lexicalParser.ts +148 -0
  22. package/src/markdown/markdown.ts +14 -0
  23. package/src/markdown/syntaxParser.ts +344 -0
  24. package/src/markdown/token.ts +43 -0
  25. package/src/metadata.ts +3 -0
  26. package/src/node.ts +1130 -0
  27. package/src/nodes/factory.ts +87 -0
  28. package/src/nodes/publisher.ts +98 -0
  29. package/src/nodes/subsystem.ts +760 -0
  30. package/src/notes/note.ts +295 -0
  31. package/src/notes/subsystem.ts +262 -0
  32. package/src/organize.ts +181 -0
  33. package/src/pass/subsystem.ts +64 -0
  34. package/src/performance.ts +9 -0
  35. package/src/popup.ts +81 -0
  36. package/src/popups/form.ts +63 -0
  37. package/src/popups/string.ts +33 -0
  38. package/src/port.ts +223 -0
  39. package/src/quickMenu.ts +330 -0
  40. package/src/styles/box.ts +104 -0
  41. package/src/styles/canvasTextAlign.ts +8 -0
  42. package/src/styles/canvasTextBaseline.ts +9 -0
  43. package/src/styles/cursor.ts +18 -0
  44. package/src/styles/stroke.ts +39 -0
  45. package/src/styles/text.ts +90 -0
  46. package/src/styles/textBox.ts +82 -0
  47. package/src/theme.ts +78 -0
  48. package/src/types/box.ts +126 -0
  49. package/src/types/list.ts +41 -0
  50. package/src/types/pool.ts +66 -0
  51. package/src/types/text.ts +136 -0
  52. package/src/types/vector2.ts +34 -0
  53. package/src/utils/color.ts +136 -0
  54. package/src/utils/html.ts +21 -0
  55. package/src/utils/math.ts +8 -0
  56. package/src/utils/string.ts +98 -0
  57. package/src/widgets/button.ts +107 -0
  58. package/src/widgets/color.ts +151 -0
  59. package/src/widgets/factory.ts +46 -0
  60. package/src/widgets/image.ts +163 -0
  61. package/src/widgets/number.ts +153 -0
  62. package/src/widgets/slider.ts +216 -0
  63. package/src/widgets/string.ts +128 -0
  64. package/src/widgets/text.ts +92 -0
  65. package/src/widgets/toggle.ts +205 -0
  66. package/src/widgets/widget.ts +12 -0
  67. package/tsconfig.json +15 -0
  68. package/tsup.config.ts +10 -0
@@ -0,0 +1,760 @@
1
+ import { Camera } from "../camera";
2
+ import { Connection, ConnectionRenderer, DefaultConnectionRenderer } from "../connection";
3
+ import { CombineContextMenus, ContextMenuConfig } from "../contextMenu";
4
+ import { RenderResults } from "../graphSubsystem";
5
+ import { FlowNode, NodeState } from "../node";
6
+ import { Organize } from "../organize";
7
+ import { PassSubsystem } from "../pass/subsystem";
8
+ import { TimeExecution } from "../performance";
9
+ import { Port, PortType } from "../port";
10
+ import { BoxStyle } from "../styles/box";
11
+ import { CursorStyle } from "../styles/cursor";
12
+ import { Theme } from "../theme";
13
+ import { Box, BoxIntersection } from "../types/box";
14
+ import { List } from "../types/list";
15
+ import { Pool, VectorPool } from "../types/pool";
16
+ import { AddVector2, CopyVector2, SubVector2, Vector2, Zero } from "../types/vector2";
17
+ import { Widget } from "../widgets/widget";
18
+ import { NodeCreatedCallback, NodeFactory, NodeFactoryConfig } from "./factory";
19
+ import { Publisher } from "./publisher";
20
+
21
+ export type NodeAddedCallback = (node: FlowNode) => void;
22
+ export type NodeRemovedCallback = (node: FlowNode) => void;
23
+
24
+ export const nodeFlowGroup = "node-flow-graph-node-menu";
25
+
26
+ export interface ConnectionRendererConfiguration {
27
+ size?: number;
28
+ color?: string;
29
+
30
+ mouseOverSize?: number;
31
+ mouseOverColor?: string;
32
+
33
+ renderer?: ConnectionRenderer;
34
+ }
35
+
36
+ function BuildConnectionRenderer(config: ConnectionRendererConfiguration | undefined): ConnectionRenderer {
37
+ if (config?.renderer !== undefined) {
38
+ return config.renderer;
39
+ }
40
+
41
+ return DefaultConnectionRenderer(
42
+ config?.size === undefined ? 2 : config.size,
43
+ undefined, // config?.color === undefined ? "#00FF00" : config.color,
44
+ config?.mouseOverSize === undefined ? 4 : config.mouseOverSize,
45
+ undefined, // config?.mouseOverColor === undefined ? "#00FF22" : config.mouseOverColor
46
+ );
47
+ }
48
+
49
+ interface PortIntersection {
50
+ Node: FlowNode;
51
+ Port: Port;
52
+ Index: number;
53
+ InputPort: boolean;
54
+ }
55
+
56
+ interface NodeSubsystemConfig {
57
+ idleConnection?: ConnectionRendererConfiguration
58
+ nodes?: NodeFactoryConfig
59
+ }
60
+
61
+ export class NodeSubsystem {
62
+
63
+ #postProcessPass: PassSubsystem;
64
+
65
+ #nodes: Array<FlowNode>;
66
+
67
+ #connections: Array<Connection>;
68
+
69
+ #nodeHovering: number;
70
+
71
+ #nodesGrabbed: List<number>;
72
+
73
+ #connectionSelected: Connection | null;
74
+
75
+ #idleConnectionRenderer: ConnectionRenderer
76
+
77
+ #portHovering: PortIntersection | null;
78
+
79
+ #widgetHovering: Widget | null;
80
+
81
+ #widgetCurrentlyClicking: Widget | null;
82
+
83
+ #cursor: CursorStyle;
84
+
85
+ #nodeFactory: NodeFactory;
86
+
87
+ #boxSelect: boolean;
88
+
89
+ #boxSelectStart_graphSpace: Vector2;
90
+
91
+ #boxSelectEnd_graphSpace: Vector2;
92
+
93
+ #boxSelectionNodes: List<number>;
94
+
95
+ #boxSelectStyle: BoxStyle;
96
+
97
+ // LISTENERS ==============================================================
98
+
99
+ #nodeRemovedCallbacks: Array<NodeRemovedCallback>;
100
+
101
+ #registeredNodeAddedCallbacks: Array<NodeAddedCallback>;
102
+
103
+ // ========================================================================
104
+
105
+ constructor(postProcessPass: PassSubsystem, config?: NodeSubsystemConfig) {
106
+ this.#postProcessPass = postProcessPass;
107
+ this.#nodes = [];
108
+ this.#nodeHovering = -1;
109
+ this.#nodesGrabbed = new List<number>();
110
+ this.#connectionSelected = null;
111
+ this.#portHovering = null;
112
+ this.#widgetHovering = null;
113
+ this.#widgetCurrentlyClicking = null;
114
+ this.#connections = new Array<Connection>();
115
+ this.#nodeFactory = new NodeFactory(config?.nodes);
116
+ this.#idleConnectionRenderer = BuildConnectionRenderer(config?.idleConnection);
117
+ this.#registeredNodeAddedCallbacks = new Array<NodeAddedCallback>();
118
+ this.#nodeRemovedCallbacks = new Array<NodeRemovedCallback>();
119
+ this.#cursor = CursorStyle.Default;
120
+
121
+ this.#boxSelect = false;
122
+ this.#boxSelectStart_graphSpace = Zero();
123
+ this.#boxSelectEnd_graphSpace = Zero();
124
+ this.#boxSelectionNodes = new List<number>();
125
+ this.#boxSelectStyle = new BoxStyle({
126
+ border: {
127
+ color: Theme.BoxSelect.Color,
128
+ size: Theme.BoxSelect.Size,
129
+ },
130
+ color: "rgba(0,0,0,0)",
131
+ radius: Theme.BoxSelect.Radius
132
+ });
133
+ }
134
+
135
+ public addPublisher(identifier: string, publisher: Publisher): void {
136
+ this.#nodeFactory.addPublisher(identifier, publisher);
137
+ }
138
+
139
+ public addOnNodeCreatedListener(callback: NodeCreatedCallback): void {
140
+ this.#nodeFactory.addOnNodeCreatedListener(callback);
141
+ }
142
+
143
+ public nodeFactory(): NodeFactory {
144
+ return this.#nodeFactory;
145
+ }
146
+
147
+ public addOnNodeAddedListener(callback: NodeAddedCallback): void {
148
+ if (callback === null || callback === undefined) {
149
+ return;
150
+ }
151
+ this.#registeredNodeAddedCallbacks.push(callback);
152
+ }
153
+
154
+ public addOnNodeRemovedListener(callback: NodeRemovedCallback): void {
155
+ if (callback === null || callback === undefined) {
156
+ return;
157
+ }
158
+ this.#nodeRemovedCallbacks.push(callback);
159
+ }
160
+
161
+ clickStart(mousePosition: Vector2, camera: Camera, ctrlKey: boolean): boolean {
162
+ this.#boxSelect = false;
163
+
164
+ let hoveringSomething = false;
165
+ if (this.#nodeHovering > -1) {
166
+ this.#selectNodeByIndex(this.#nodeHovering, !ctrlKey);
167
+
168
+ for (let i = 0; i < this.#nodes.length; i++) {
169
+ if (this.#nodes[i].selected()) {
170
+ this.#nodesGrabbed.Push(i);
171
+ }
172
+ }
173
+
174
+ hoveringSomething = true;
175
+ }
176
+
177
+ if (this.#widgetHovering !== null) {
178
+ this.#widgetHovering.ClickStart();
179
+ this.#widgetCurrentlyClicking = this.#widgetHovering;
180
+ hoveringSomething = true;
181
+ }
182
+
183
+ if (this.#portHovering === null) {
184
+
185
+ if (ctrlKey && !hoveringSomething) {
186
+ camera.screenSpaceToGraphSpace(mousePosition, this.#boxSelectStart_graphSpace)
187
+ CopyVector2(this.#boxSelectEnd_graphSpace, this.#boxSelectStart_graphSpace);
188
+ this.#boxSelect = true;
189
+ }
190
+
191
+ return hoveringSomething || ctrlKey;
192
+ }
193
+
194
+ if (this.#portHovering.InputPort) {
195
+ for (let i = 0; i < this.#connections.length; i++) {
196
+ if (this.#connections[i].inPort() === this.#portHovering.Port) {
197
+ this.#connections[i].clearInput();
198
+ this.#connectionSelected = this.#connections[i];
199
+ break;
200
+ }
201
+ }
202
+ } else {
203
+ let inNode: FlowNode | null = this.#portHovering.Node
204
+ let inNodeIndex = this.#portHovering.Index
205
+ let outNode: FlowNode | null = this.#portHovering.Node
206
+ let outNodeIndex = this.#portHovering.Index
207
+
208
+ if (this.#portHovering.InputPort) {
209
+ outNode = null;
210
+ outNodeIndex = -1;
211
+ } else {
212
+ inNode = null;
213
+ inNodeIndex = -1;
214
+ }
215
+
216
+ const connection = new Connection(inNode, inNodeIndex, outNode, outNodeIndex, this.#idleConnectionRenderer);
217
+ this.#connectionSelected = connection;
218
+ this.#connections.push(connection);
219
+ }
220
+
221
+ return true;
222
+ }
223
+
224
+ mouseDragEvent(delta: Vector2, scale: number): boolean {
225
+ let nodeMoved = false;
226
+
227
+ VectorPool.run(() => {
228
+ const scaledDelta = VectorPool.get();
229
+ scaledDelta.x = delta.x / scale;
230
+ scaledDelta.y = delta.y / scale;
231
+ AddVector2(this.#boxSelectEnd_graphSpace, this.#boxSelectEnd_graphSpace, scaledDelta);
232
+
233
+ if (this.#interactingWithNode()) {
234
+ for (let i = 0; i < this.#nodesGrabbed.Count(); i++) {
235
+ const node = this.#nodes[this.#nodesGrabbed.At(i)];
236
+ if (node.locked()) {
237
+ continue;
238
+ }
239
+ node.translate(scaledDelta);
240
+ nodeMoved = true;
241
+ }
242
+ }
243
+ });
244
+
245
+ if (nodeMoved) {
246
+ return true;
247
+ }
248
+ return this.#interactingWithConnection() || this.#interactingWithWidget() || this.#boxSelect;
249
+ }
250
+
251
+ clearNodeInputConnection(node: FlowNode, index: number): void {
252
+ const port = node.inputPort(index);
253
+ for (let i = this.#connections.length - 1; i >= 0; i--) {
254
+ if (this.#connections[i].inPort() === port) {
255
+ this.#removeConnection(this.#connections[i], true);
256
+ }
257
+ }
258
+ }
259
+
260
+ clickEnd(): void {
261
+
262
+ for (let i = 0; i < this.#nodesGrabbed.Count(); i++) {
263
+ const node = this.#nodes[this.#nodesGrabbed.At(i)];
264
+ node.raiseDragStoppedEvent();
265
+ }
266
+ this.#nodesGrabbed.Clear();
267
+
268
+ if (this.#boxSelect) {
269
+ for (let i = 0; i < this.#boxSelectionNodes.Count(); i++) {
270
+ this.#selectNode(this.#nodes[this.#boxSelectionNodes.At(i)], false);
271
+ }
272
+
273
+ this.#boxSelectionNodes.Clear();
274
+ this.#boxSelect = false;
275
+ }
276
+
277
+ if (this.#widgetCurrentlyClicking !== null) {
278
+ this.#widgetCurrentlyClicking.ClickEnd();
279
+ this.#widgetCurrentlyClicking = null;
280
+ }
281
+
282
+ if (this.#connectionSelected === null) {
283
+ return;
284
+ }
285
+
286
+ if (this.#portHovering === null) {
287
+ this.#clearCurrentlySelectedConnection();
288
+ return;
289
+ }
290
+
291
+ const port = this.#portHovering.Port;
292
+ const conn = this.#connectionSelected;
293
+
294
+ // If the port we're hovering is the one we started the connection
295
+ // with...
296
+ if (port === conn.inPort() || port === conn.outPort()) {
297
+ this.#clearCurrentlySelectedConnection();
298
+ return;
299
+ }
300
+
301
+ // If the port we're hoving is input, and also we started the
302
+ // connection with an input, we can't do anything. We can't connect
303
+ // input to input
304
+ if (this.#portHovering.InputPort && conn.inPort() !== null) {
305
+ this.#clearCurrentlySelectedConnection();
306
+ return;
307
+ }
308
+
309
+ // Same with out. Can't connect output to output
310
+ if (!this.#portHovering.InputPort && conn.outPort() !== null) {
311
+ this.#clearCurrentlySelectedConnection();
312
+ return;
313
+ }
314
+
315
+ // Different types, can't connect
316
+ if (this.#portHovering.InputPort && this.#portHovering.Port.getDataType() !== conn.outPort()?.getDataType()) {
317
+ this.#clearCurrentlySelectedConnection();
318
+ return;
319
+ }
320
+
321
+ // Different types, can't connect
322
+ if (!this.#portHovering.InputPort && this.#portHovering.Port.getDataType() !== conn.inPort()?.getDataType()) {
323
+ this.#clearCurrentlySelectedConnection();
324
+ return;
325
+ }
326
+
327
+ // Ight. Let's make the connection.
328
+ if (this.#portHovering.InputPort) {
329
+ let replace = false;
330
+ const inputPort = this.#portHovering.Node.inputPort(this.#portHovering.Index);
331
+ if (this.#portHovering.Port.getPortType() !== PortType.InputArray && inputPort.connections().length > 0) {
332
+ replace = true;
333
+ }
334
+
335
+ if (replace) {
336
+ // Remove input port's connection's output
337
+ inputPort.connections()[0].clearOutput();
338
+
339
+ // Remove connection from this subsystem
340
+ this.#removeConnection(inputPort.connections()[0], false);
341
+ }
342
+ conn.setInput(this.#portHovering.Node, this.#portHovering.Index, replace);
343
+
344
+ } else {
345
+ conn.setOutput(this.#portHovering.Node, this.#portHovering.Index);
346
+ }
347
+
348
+ this.#connectionSelected = null;
349
+ }
350
+
351
+ connectNodes(nodeOut: FlowNode, outPort: number, nodeIn: FlowNode, inPort: number): Connection | undefined {
352
+ const outType = nodeOut.outputPort(outPort).getDataType();
353
+ const inType = nodeIn.inputPort(inPort).getDataType();
354
+ if (outType !== inType) {
355
+ console.error("can't connect nodes of different types", outType, inType);
356
+ return;
357
+ }
358
+
359
+ // Check and make sure the nodes aren't already connected
360
+ const existingInputConnections = nodeIn.inputPort(inPort).connections();
361
+ for (let i = 0; i < existingInputConnections.length; i++) {
362
+ const connection = existingInputConnections[i];
363
+
364
+
365
+ if (connection.outNode() !== nodeOut) {
366
+ continue;
367
+ }
368
+
369
+ if (connection.outPort() !== nodeOut.outputPort(outPort)) {
370
+ continue;
371
+ }
372
+
373
+ // This kind of connection already *exists*. Let's not add a duplicate
374
+ return;
375
+ }
376
+
377
+ const connection = new Connection(
378
+ nodeIn, inPort,
379
+ nodeOut, outPort,
380
+ this.#idleConnectionRenderer,
381
+ );
382
+ this.#connections.push(connection);
383
+ return connection;
384
+ }
385
+
386
+ getNodes(): Array<FlowNode> {
387
+ return this.#nodes;
388
+ }
389
+
390
+ /**
391
+ * Returns all nodes who are connected to the inputs of the
392
+ * node in question
393
+ *
394
+ * @param nodeIndex index of the node to examine inputs to
395
+ */
396
+ connectedInputsNodeReferencesByIndex(nodeIndex: number): Array<FlowNode> {
397
+ return this.connectedInputsNodeReferences(this.#nodes[nodeIndex]);
398
+ }
399
+
400
+ connectedInputsNodeReferences(node: FlowNode): Array<FlowNode> {
401
+ const connections = new Array<FlowNode>();
402
+ for (let i = 0; i < this.#connections.length; i++) {
403
+ const connection = this.#connections[i];
404
+ if (node !== connection.inNode()) {
405
+ continue;
406
+ }
407
+
408
+ const outNode = connection.outNode();
409
+ if (outNode === null) {
410
+ continue;
411
+ }
412
+
413
+ connections.push(outNode);
414
+ }
415
+ return connections;
416
+ }
417
+
418
+ connectedOutputsNodeReferences(nodeIndex: number): Array<FlowNode> {
419
+ const node = this.#nodes[nodeIndex]
420
+ const connections = new Array<FlowNode>();
421
+ for (let i = 0; i < this.#connections.length; i++) {
422
+ const connection = this.#connections[i];
423
+ if (node !== connection.outNode()) {
424
+ continue;
425
+ }
426
+
427
+ const inNode = connection.inNode();
428
+ if (inNode === null) {
429
+ continue;
430
+ }
431
+
432
+ connections.push(inNode);
433
+ }
434
+ return connections;
435
+ }
436
+
437
+ addNode(node: FlowNode): void {
438
+ this.#nodes.push(node);
439
+ for (let i = 0; i < this.#registeredNodeAddedCallbacks.length; i++) {
440
+ const callback = this.#registeredNodeAddedCallbacks[i];
441
+ if (!callback) {
442
+ continue;
443
+ }
444
+ callback(node);
445
+ }
446
+ }
447
+
448
+ fileDrop(file: File): boolean {
449
+ if (this.#nodeHovering === -1) {
450
+ return false;
451
+ }
452
+ this.#nodes[this.#nodeHovering].dropFile(file);
453
+ return true;
454
+ }
455
+
456
+ #removeNodeConnections(nodeIndex: number): void {
457
+ if (nodeIndex >= this.#nodes.length || nodeIndex < 0) {
458
+ console.error("invalid node connection");
459
+ return;
460
+ }
461
+ for (let i = this.#connections.length - 1; i >= 0; i--) {
462
+ if (this.#connections[i].referencesNode(this.#nodes[nodeIndex])) {
463
+ this.#removeConnectionByIndex(i, true);
464
+ }
465
+ }
466
+ }
467
+
468
+ #interactingWithNode(): boolean {
469
+ return this.#nodesGrabbed.Count() > 0;
470
+ }
471
+
472
+ #interactingWithConnection(): boolean {
473
+ return this.#connectionSelected !== null;
474
+ }
475
+
476
+ #interactingWithWidget(): boolean {
477
+ return this.#widgetCurrentlyClicking !== null;
478
+ }
479
+
480
+ #removeNodeByIndex(nodeIndex: number): void {
481
+ this.#removeNodeConnections(nodeIndex);
482
+ const node = this.#nodes[nodeIndex];
483
+ this.#nodes.splice(nodeIndex, 1);
484
+ for (let i = 0; i < this.#nodeRemovedCallbacks.length; i++) {
485
+ this.#nodeRemovedCallbacks[i](node);
486
+ }
487
+ }
488
+
489
+ #removeConnectionByIndex(index: number, clearPorts: boolean): void {
490
+ if (clearPorts) {
491
+ this.#connections[index].clearPorts();
492
+ }
493
+ this.#connections.splice(index, 1);
494
+ }
495
+
496
+ #removeConnection(connection: Connection, clearPorts: boolean): void {
497
+ const index = this.#connections.indexOf(connection);
498
+ if (index > -1) {
499
+ this.#removeConnectionByIndex(index, clearPorts);
500
+ } else {
501
+ console.error("no connection found to remove");
502
+ }
503
+ }
504
+
505
+ organize(ctx: CanvasRenderingContext2D): void {
506
+ Organize(ctx, this);
507
+ }
508
+
509
+ #nodesSelected(): Array<number> {
510
+ const selected = new Array<number>();
511
+ for (let i = 0; i < this.#nodes.length; i++) {
512
+ if (this.#nodes[i].selected()) {
513
+ selected.push(i);
514
+ }
515
+ }
516
+ return selected;
517
+ }
518
+
519
+ #organizeSelected(ctx: CanvasRenderingContext2D): void {
520
+ Organize(ctx, this, this.#nodesSelected());
521
+ }
522
+
523
+ openContextMenu(ctx: CanvasRenderingContext2D, position: Vector2): ContextMenuConfig | null {
524
+
525
+ const organizeNodesSubMenu: ContextMenuConfig = {
526
+ name: "Organize",
527
+ group: nodeFlowGroup,
528
+ items: [
529
+ {
530
+ name: "All Nodes",
531
+ group: nodeFlowGroup,
532
+ callback: () => {
533
+ this.organize(ctx);
534
+ }
535
+ }
536
+ ]
537
+ }
538
+
539
+ let config: ContextMenuConfig = {
540
+ items: [],
541
+ subMenus: [
542
+ organizeNodesSubMenu,
543
+ this.#nodeFactory.openMenu(this, position)
544
+ ]
545
+ }
546
+
547
+ if (this.#nodesSelected().length > 0) {
548
+ organizeNodesSubMenu.items?.push({
549
+ name: "Selected Nodes",
550
+ group: nodeFlowGroup,
551
+ callback: () => {
552
+ this.#organizeSelected(ctx)
553
+ }
554
+ })
555
+ }
556
+
557
+ if (this.#nodeHovering > -1) {
558
+ const nodeToReview = this.#nodeHovering;
559
+ const nodeToReviewNode = this.#nodes[nodeToReview];
560
+
561
+ config.subMenus?.push({
562
+ group: nodeFlowGroup,
563
+ name: "Select",
564
+ items: [
565
+ {
566
+ name: "Direct Connected Nodes",
567
+ group: nodeFlowGroup,
568
+ callback: () => {
569
+ this.#selectConnectedNodes(nodeToReview);
570
+ }
571
+ },
572
+ {
573
+ name: "Input Nodes + Descendents",
574
+ group: nodeFlowGroup,
575
+ callback: () => {
576
+ this.#selectInputNodesAndDescendents(nodeToReviewNode);
577
+ }
578
+ },
579
+ ]
580
+ })
581
+
582
+ config.subMenus?.push({
583
+ group: nodeFlowGroup,
584
+ name: "Delete",
585
+ items: [
586
+ {
587
+ name: "Node",
588
+ group: nodeFlowGroup,
589
+ callback: () => {
590
+ this.#removeNodeByIndex(nodeToReview);
591
+ }
592
+ },
593
+ {
594
+ name: "Connections",
595
+ group: nodeFlowGroup,
596
+ callback: () => {
597
+ this.#removeNodeConnections(nodeToReview);
598
+ }
599
+ }
600
+ ]
601
+ })
602
+ config = CombineContextMenus(config, nodeToReviewNode.contextMenu());
603
+ }
604
+ return config;
605
+ }
606
+
607
+ #selectInputNodesAndDescendents(node: FlowNode): void {
608
+ if (node === undefined) {
609
+ return;
610
+ }
611
+ this.#selectNode(node, false);
612
+
613
+ const inputs = this.connectedInputsNodeReferences(node);
614
+ for (let i = 0; i < inputs.length; i++) {
615
+ this.#selectInputNodesAndDescendents(inputs[i]);
616
+ }
617
+ }
618
+
619
+ #selectConnectedNodes(nodeIndex: number): void {
620
+ const node = this.#nodes[nodeIndex];
621
+ if (node === undefined) {
622
+ return;
623
+ }
624
+ this.#selectNode(node, false);
625
+
626
+ const outputs = this.connectedOutputsNodeReferences(nodeIndex);
627
+ for (let i = 0; i < outputs.length; i++) {
628
+ this.#selectNode(outputs[i], false);
629
+ }
630
+
631
+ const inputs = this.connectedInputsNodeReferencesByIndex(nodeIndex);
632
+ for (let i = 0; i < inputs.length; i++) {
633
+ this.#selectNode(inputs[i], false);
634
+ }
635
+ }
636
+
637
+ #selectNodeByIndex(nodeIndex: number, unselectOthers: boolean): void {
638
+ this.#selectNode(this.#nodes[nodeIndex], unselectOthers);
639
+ }
640
+
641
+ #selectNode(node: FlowNode, unselectOthers: boolean): void {
642
+ node.select();
643
+
644
+ if (!unselectOthers) {
645
+ return;
646
+ }
647
+
648
+ for (let i = 0; i < this.#nodes.length; i++) {
649
+ if (node === this.#nodes[i]) {
650
+ continue
651
+ }
652
+ this.#nodes[i].unselect();
653
+ }
654
+ }
655
+
656
+ #clearCurrentlySelectedConnection(): void {
657
+ if (this.#connectionSelected === null) {
658
+ return;
659
+ }
660
+ this.#removeConnection(this.#connectionSelected, true);
661
+ this.#connectionSelected = null;
662
+ }
663
+
664
+ #renderConnections(ctx: CanvasRenderingContext2D, camera: Camera, mousePosition: Vector2 | undefined): void {
665
+ for (let i = 0; i < this.#connections.length; i++) {
666
+ let portMousedOver = false;
667
+ if (mousePosition !== undefined) {
668
+ portMousedOver = this.#connections[i].mouseOverPort(mousePosition) !== null;
669
+ }
670
+ this.#connections[i].render(ctx, camera.zoom, portMousedOver, mousePosition);
671
+ }
672
+ }
673
+
674
+ #renderNodes(ctx: CanvasRenderingContext2D, camera: Camera, mousePosition: Vector2 | undefined) {
675
+ this.#portHovering = null;
676
+ this.#widgetHovering = null;
677
+ this.#nodeHovering = -1;
678
+ this.#boxSelectionNodes.Clear();
679
+
680
+ const selectedBox_Screenspace = this.#boxSelectionScreenspaceBox(camera);
681
+
682
+ for (let i = 0; i < this.#nodes.length; i++) {
683
+ let state = NodeState.Idle;
684
+
685
+ if (mousePosition !== undefined && !this.#boxSelect) {
686
+ const intersection = this.#nodes[i].inBounds(ctx, camera, mousePosition);
687
+
688
+ if (intersection.Node !== undefined && intersection.PortIndex === undefined && intersection.Widget === undefined) {
689
+ state = NodeState.MouseOver;
690
+ this.#nodeHovering = i;
691
+ this.#cursor = CursorStyle.Grab;
692
+ }
693
+
694
+ if (intersection.Widget !== undefined) {
695
+ // console.log(i, this.#nodes[i].title(), "widget")
696
+ this.#widgetHovering = intersection.Widget
697
+ this.#cursor = CursorStyle.Pointer;
698
+ }
699
+
700
+ if (intersection.Port !== undefined && intersection.Node !== undefined && intersection.PortIndex !== undefined && intersection.PortIsInput !== undefined) {
701
+ this.#portHovering = {
702
+ Index: intersection.PortIndex,
703
+ Node: intersection.Node,
704
+ Port: intersection.Port,
705
+ InputPort: intersection.PortIsInput
706
+ };
707
+ }
708
+ } else if (this.#boxSelect) {
709
+ const nodeBounds = this.#nodes[i].calculateBounds(ctx, camera);
710
+
711
+ if (BoxIntersection(selectedBox_Screenspace, nodeBounds)) {
712
+ state = NodeState.MouseOver;
713
+ this.#boxSelectionNodes.Push(i);
714
+ }
715
+ }
716
+
717
+ // if (i === this.#nodeSelected) {
718
+ // state = NodeState.Selected;
719
+ // }
720
+
721
+ if (this.#nodes[i].selected() && this.#nodesGrabbed.Count() > 0) {
722
+ state = NodeState.Grabbed;
723
+ this.#cursor = CursorStyle.Grabbing;
724
+ }
725
+
726
+ this.#nodes[i].render(ctx, camera, state, mousePosition, this.#postProcessPass);
727
+ }
728
+ }
729
+
730
+ #boxSelectionScreenspaceBox(camera: Camera): Box {
731
+ const box = { Position: Zero(), Size: Zero() }
732
+
733
+ camera.graphSpaceToScreenSpace(this.#boxSelectStart_graphSpace, box.Position);
734
+ camera.graphSpaceToScreenSpace(this.#boxSelectEnd_graphSpace, box.Size);
735
+
736
+ SubVector2(box.Size, box.Size, box.Position);
737
+
738
+ return box;
739
+ }
740
+
741
+ render(ctx: CanvasRenderingContext2D, camera: Camera, mousePosition: Vector2 | undefined): RenderResults | undefined {
742
+ this.#cursor = CursorStyle.Default;
743
+ TimeExecution("Render_Connections", () => {
744
+ this.#renderConnections(ctx, camera, mousePosition);
745
+ })
746
+ TimeExecution("Render_Nodes", () => {
747
+ this.#renderNodes(ctx, camera, mousePosition);
748
+ })
749
+
750
+ if (this.#boxSelect) {
751
+ const box = this.#boxSelectionScreenspaceBox(camera);
752
+ ctx.setLineDash([Theme.BoxSelect.LineDashLength]);
753
+ this.#boxSelectStyle.Draw(ctx, box, 1);
754
+ ctx.setLineDash([]);
755
+ }
756
+
757
+ return { cursorStyle: this.#cursor }
758
+ }
759
+
760
+ }