@elicdavis/node-flow 0.1.15 → 0.1.17
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.
- package/.mocharc.json +5 -0
- package/dist/index.d.mts +139 -4
- package/dist/index.d.ts +139 -4
- package/dist/index.js +3754 -3352
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3741 -3352
- package/dist/index.mjs.map +1 -1
- package/index.html +0 -30
- package/package.json +4 -2
- package/src/graph.ts +234 -41
- package/src/index.ts +82 -40
- package/src/input.ts +22 -14
- package/src/markdown/entry.ts +26 -3
- package/src/markdown/lexicalParser.ts +22 -32
- package/src/markdown/syntaxParser.ts +93 -68
- package/src/minimap.ts +178 -0
- package/src/node.ts +114 -0
- package/src/nodes/factory.ts +9 -5
- package/src/nodes/publisher.ts +39 -4
- package/src/nodes/subsystem.ts +218 -8
- package/src/notes/subsystem.ts +3 -0
- package/src/organize.ts +276 -181
- package/src/port.ts +143 -7
- package/src/render_scheduler.ts +49 -0
- package/src/styles/text.ts +8 -0
- package/src/theme.ts +8 -0
- package/src/types/text.ts +8 -4
- package/src/utils/string.ts +7 -1
- package/src/widgets/color.ts +5 -6
- package/src/widgets/factory.ts +1 -1
- package/src/widgets/image.ts +2 -0
- package/src/widgets/number.ts +2 -2
- package/src/widgets/slider.ts +2 -2
- package/src/widgets/string.ts +2 -2
- package/src/widgets/toggle.ts +2 -2
- package/src/widgets/widget.ts +22 -0
- package/test/markdown.test.ts +165 -0
- package/test/minimap.test.ts +103 -0
- package/test/node_menu_filter.test.ts +119 -0
- package/test/organize.test.ts +156 -0
- package/test/port.test.ts +125 -0
- package/test/render_scheduler.test.ts +119 -0
- package/tsconfig.json +6 -2
package/src/node.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Port, PortConfig, PortType } from "./port";
|
|
2
|
+
import { requestRender } from "./render_scheduler";
|
|
2
3
|
import { FontWeight, TextStyle, TextStyleConfig, TextStyleFallback } from "./styles/text";
|
|
3
4
|
import { Box, InBox } from "./types/box";
|
|
4
5
|
import { CopyVector2, Distance, ScaleVector, Vector2, Zero } from "./types/vector2";
|
|
@@ -402,6 +403,7 @@ export class FlowNode {
|
|
|
402
403
|
return;
|
|
403
404
|
}
|
|
404
405
|
this.#selected = true;
|
|
406
|
+
requestRender();
|
|
405
407
|
for (let i = 0; i < this.#onSelect.length; i++) {
|
|
406
408
|
this.#onSelect[i]();
|
|
407
409
|
}
|
|
@@ -457,6 +459,7 @@ export class FlowNode {
|
|
|
457
459
|
}
|
|
458
460
|
|
|
459
461
|
this.#data[name] = value;
|
|
462
|
+
requestRender();
|
|
460
463
|
|
|
461
464
|
for (let i = 0; i < this.#registeredAnyPropertyChangeCallbacks.length; i++) {
|
|
462
465
|
this.#registeredAnyPropertyChangeCallbacks[i](name, oldValue, value);
|
|
@@ -735,6 +738,7 @@ export class FlowNode {
|
|
|
735
738
|
return;
|
|
736
739
|
}
|
|
737
740
|
this.#selected = false;
|
|
741
|
+
requestRender();
|
|
738
742
|
for (let i = 0; i < this.#onUnselect.length; i++) {
|
|
739
743
|
this.#onUnselect[i]();
|
|
740
744
|
}
|
|
@@ -772,6 +776,7 @@ export class FlowNode {
|
|
|
772
776
|
|
|
773
777
|
public setPosition(position: Vector2): void {
|
|
774
778
|
CopyVector2(this.#position, position);
|
|
779
|
+
requestRender();
|
|
775
780
|
}
|
|
776
781
|
|
|
777
782
|
public getPosition(): Vector2 {
|
|
@@ -854,6 +859,13 @@ export class FlowNode {
|
|
|
854
859
|
|
|
855
860
|
addWidget(widget: Widget): void {
|
|
856
861
|
this.#widgets.push(widget);
|
|
862
|
+
requestRender();
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
insertWidget(index: number, widget: Widget): void {
|
|
866
|
+
const at = Math.max(0, Math.min(index, this.#widgets.length));
|
|
867
|
+
this.#widgets.splice(at, 0, widget);
|
|
868
|
+
requestRender();
|
|
857
869
|
}
|
|
858
870
|
|
|
859
871
|
removeWidget(widget: Widget): void {
|
|
@@ -862,12 +874,47 @@ export class FlowNode {
|
|
|
862
874
|
throw new Error("node does not contain widget");
|
|
863
875
|
}
|
|
864
876
|
this.#widgets.splice(index, 1);
|
|
877
|
+
requestRender();
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
removeWidgetAt(index: number): Widget | undefined {
|
|
881
|
+
if (index < 0 || index >= this.#widgets.length) {
|
|
882
|
+
return undefined;
|
|
883
|
+
}
|
|
884
|
+
const removed = this.#widgets.splice(index, 1)[0];
|
|
885
|
+
requestRender();
|
|
886
|
+
return removed;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
moveWidget(from: number, to: number): void {
|
|
890
|
+
if (from < 0 || from >= this.#widgets.length) {
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
if (to < 0 || to >= this.#widgets.length || from === to) {
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
const [widget] = this.#widgets.splice(from, 1);
|
|
897
|
+
this.#widgets.splice(to, 0, widget);
|
|
898
|
+
requestRender();
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
clearWidgets(): void {
|
|
902
|
+
this.#widgets.length = 0;
|
|
903
|
+
requestRender();
|
|
865
904
|
}
|
|
866
905
|
|
|
867
906
|
getWidget(index: number): Widget {
|
|
868
907
|
return this.#widgets[index];
|
|
869
908
|
}
|
|
870
909
|
|
|
910
|
+
widgets(): ReadonlyArray<Widget> {
|
|
911
|
+
return this.#widgets;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
indexOfWidget(widget: Widget): number {
|
|
915
|
+
return this.#widgets.indexOf(widget, 0);
|
|
916
|
+
}
|
|
917
|
+
|
|
871
918
|
widgetCount(): number {
|
|
872
919
|
return this.#widgets.length;
|
|
873
920
|
}
|
|
@@ -943,6 +990,7 @@ export class FlowNode {
|
|
|
943
990
|
}
|
|
944
991
|
|
|
945
992
|
this.#title.set(cleaned);
|
|
993
|
+
requestRender();
|
|
946
994
|
|
|
947
995
|
for (let i = 0; i < this.#titleChangeCallback.length; i++) {
|
|
948
996
|
const callback = this.#titleChangeCallback[i];
|
|
@@ -966,6 +1014,7 @@ export class FlowNode {
|
|
|
966
1014
|
|
|
967
1015
|
const old = this.#infoText;
|
|
968
1016
|
this.#infoText = cleaned;
|
|
1017
|
+
requestRender();
|
|
969
1018
|
|
|
970
1019
|
for (let i = 0; i < this.#infoChangeCallback.length; i++) {
|
|
971
1020
|
const callback = this.#infoChangeCallback[i];
|
|
@@ -1018,7 +1067,15 @@ export class FlowNode {
|
|
|
1018
1067
|
this.#messages = [];
|
|
1019
1068
|
}
|
|
1020
1069
|
|
|
1070
|
+
public hasBeenLaidOut(): boolean {
|
|
1071
|
+
return this.#laidOut;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
#laidOut: boolean = false;
|
|
1075
|
+
|
|
1021
1076
|
render(ctx: CanvasRenderingContext2D, camera: Camera, state: NodeState, mousePosition: Vector2 | undefined, postProcess: PassSubsystem): void {
|
|
1077
|
+
this.#laidOut = true;
|
|
1078
|
+
|
|
1022
1079
|
VectorPool.run(() => {
|
|
1023
1080
|
const tempMeasurement = VectorPool.get();
|
|
1024
1081
|
|
|
@@ -1220,6 +1277,63 @@ export class FlowNode {
|
|
|
1220
1277
|
const message = this.#messages[i];
|
|
1221
1278
|
messageStart.y += message.render(ctx, camera.zoom, messageStart, !(state === NodeState.Idle)) + (10 * camera.zoom);
|
|
1222
1279
|
}
|
|
1280
|
+
|
|
1281
|
+
this.#rememberPortOffsets(nodeBounds, camera.zoom);
|
|
1223
1282
|
})
|
|
1224
1283
|
}
|
|
1284
|
+
|
|
1285
|
+
#portOffsets: { input: Array<Box>, output: Array<Box> } = { input: [], output: [] };
|
|
1286
|
+
|
|
1287
|
+
#rememberPortOffsets(bounds: Box, zoom: number): void {
|
|
1288
|
+
if (zoom <= 0) {
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
const record = (positions: List<Box>, into: Array<Box>) => {
|
|
1293
|
+
into.length = 0;
|
|
1294
|
+
for (let i = 0; i < positions.Count(); i++) {
|
|
1295
|
+
const box = positions.At(i);
|
|
1296
|
+
into.push({
|
|
1297
|
+
Position: {
|
|
1298
|
+
x: (box.Position.x - bounds.Position.x) / zoom,
|
|
1299
|
+
y: (box.Position.y - bounds.Position.y) / zoom,
|
|
1300
|
+
},
|
|
1301
|
+
Size: {
|
|
1302
|
+
x: box.Size.x / zoom,
|
|
1303
|
+
y: box.Size.y / zoom,
|
|
1304
|
+
},
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
|
|
1309
|
+
record(this.#inputPortPositions, this.#portOffsets.input);
|
|
1310
|
+
record(this.#outputPortPositions, this.#portOffsets.output);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
public layoutPortsWithoutDrawing(bounds: Box, zoom: number): boolean {
|
|
1314
|
+
if (this.#portOffsets.input.length === 0 && this.#portOffsets.output.length === 0) {
|
|
1315
|
+
return false;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
const project = (offsets: Array<Box>, into: List<Box>) => {
|
|
1319
|
+
into.Clear();
|
|
1320
|
+
for (let i = 0; i < offsets.length; i++) {
|
|
1321
|
+
const offset = offsets[i];
|
|
1322
|
+
into.Push({
|
|
1323
|
+
Position: {
|
|
1324
|
+
x: bounds.Position.x + (offset.Position.x * zoom),
|
|
1325
|
+
y: bounds.Position.y + (offset.Position.y * zoom),
|
|
1326
|
+
},
|
|
1327
|
+
Size: {
|
|
1328
|
+
x: offset.Size.x * zoom,
|
|
1329
|
+
y: offset.Size.y * zoom,
|
|
1330
|
+
},
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
|
|
1335
|
+
project(this.#portOffsets.input, this.#inputPortPositions);
|
|
1336
|
+
project(this.#portOffsets.output, this.#outputPortPositions);
|
|
1337
|
+
return true;
|
|
1338
|
+
}
|
|
1225
1339
|
}
|
package/src/nodes/factory.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FlowNode } from "../node";
|
|
2
|
-
import { Publisher, PublisherConfig } from "./publisher";
|
|
2
|
+
import { NodeMenuFilter, Publisher, PublisherConfig } from "./publisher";
|
|
3
3
|
import { ContextMenuConfig } from "../contextMenu";
|
|
4
4
|
import { Vector2 } from "../types/vector2";
|
|
5
5
|
import { nodeFlowGroup as nodeFlowContextMenuGroup, NodeSubsystem } from "./subsystem";
|
|
@@ -69,19 +69,23 @@ export class NodeFactory {
|
|
|
69
69
|
return node;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
public newNodeSubmenus(graph: NodeSubsystem, position: Vector2): Array<ContextMenuConfig> {
|
|
72
|
+
public newNodeSubmenus(graph: NodeSubsystem, position: Vector2, filter?: NodeMenuFilter): Array<ContextMenuConfig> {
|
|
73
73
|
const menus: Array<ContextMenuConfig> = [];
|
|
74
74
|
for (let [_, publisher] of this.#registeredPublishers) {
|
|
75
|
-
|
|
75
|
+
const built = publisher.contextMenu(graph, position, filter);
|
|
76
|
+
if (filter !== undefined && built.items?.length === 0 && built.subMenus?.length === 0) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
menus.push(built);
|
|
76
80
|
}
|
|
77
81
|
return menus;
|
|
78
82
|
}
|
|
79
83
|
|
|
80
|
-
public openMenu(graph: NodeSubsystem, position: Vector2): ContextMenuConfig {
|
|
84
|
+
public openMenu(graph: NodeSubsystem, position: Vector2, filter?: NodeMenuFilter): ContextMenuConfig {
|
|
81
85
|
return {
|
|
82
86
|
name: "New Node",
|
|
83
87
|
group: nodeFlowContextMenuGroup,
|
|
84
|
-
subMenus: this.newNodeSubmenus(graph, position),
|
|
88
|
+
subMenus: this.newNodeSubmenus(graph, position, filter),
|
|
85
89
|
};
|
|
86
90
|
}
|
|
87
91
|
}
|
package/src/nodes/publisher.ts
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
import { ContextMenuConfig, ContextMenuItemConfig } from "../contextMenu";
|
|
2
2
|
import { FlowNode, FlowNodeConfig } from '../node';
|
|
3
|
+
import { PortConfig, PortType, portConfigTypes, typeSetsCompatible } from "../port";
|
|
3
4
|
import { Vector2 } from "../types/vector2";
|
|
4
5
|
import { NodeSubsystem } from "./subsystem";
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Narrows a "new node" menu to the nodes that could actually be wired to a
|
|
9
|
+
* particular port.
|
|
10
|
+
*/
|
|
11
|
+
export interface NodeMenuFilter {
|
|
12
|
+
/** Which side the new node has to offer. */
|
|
13
|
+
needs: PortType;
|
|
14
|
+
|
|
15
|
+
/** Types that side has to accept. An empty list takes anything. */
|
|
16
|
+
types: Array<string>;
|
|
17
|
+
|
|
18
|
+
onCreated?: (node: FlowNode) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function offersMatchingPort(config: FlowNodeConfig, filter: NodeMenuFilter): boolean {
|
|
22
|
+
const ports: Array<PortConfig> | undefined =
|
|
23
|
+
filter.needs === PortType.Input ? config.inputs : config.outputs;
|
|
24
|
+
|
|
25
|
+
if (ports === undefined) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return ports.some((port) => typeSetsCompatible(portConfigTypes(port), filter.types));
|
|
30
|
+
}
|
|
31
|
+
|
|
6
32
|
interface PublisherNodes {
|
|
7
33
|
[name: string]: FlowNodeConfig
|
|
8
34
|
}
|
|
@@ -50,7 +76,7 @@ export class Publisher {
|
|
|
50
76
|
return this.#registeredNodes.delete(nodeType);
|
|
51
77
|
}
|
|
52
78
|
|
|
53
|
-
#recurseBuildMenu(graph: NodeSubsystem, name: string, subMenu: Map<string, FlowNodeConfig>, position: Vector2): ContextMenuConfig {
|
|
79
|
+
#recurseBuildMenu(graph: NodeSubsystem, name: string, subMenu: Map<string, FlowNodeConfig>, position: Vector2, filter?: NodeMenuFilter): ContextMenuConfig {
|
|
54
80
|
const items: Array<ContextMenuItemConfig> = [];
|
|
55
81
|
const subMenus = new Map<string, Map<string, FlowNodeConfig>>();
|
|
56
82
|
|
|
@@ -59,12 +85,17 @@ export class Publisher {
|
|
|
59
85
|
const bracketIndex = key.indexOf("[")
|
|
60
86
|
|
|
61
87
|
if (slashIndex === -1 || (bracketIndex !== -1 && bracketIndex < slashIndex)) {
|
|
88
|
+
if (filter !== undefined && !offersMatchingPort(nodeConfig, filter)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
62
92
|
items.push({
|
|
63
93
|
name: key,
|
|
64
94
|
callback: () => {
|
|
65
95
|
const node = new FlowNode(nodeConfig);
|
|
66
96
|
node.setPosition(position);
|
|
67
97
|
graph.addNode(node);
|
|
98
|
+
filter?.onCreated?.(node);
|
|
68
99
|
},
|
|
69
100
|
});
|
|
70
101
|
} else {
|
|
@@ -81,7 +112,11 @@ export class Publisher {
|
|
|
81
112
|
|
|
82
113
|
const menus: Array<ContextMenuConfig> = [];
|
|
83
114
|
for (let [key, nodes] of subMenus) {
|
|
84
|
-
|
|
115
|
+
const built = this.#recurseBuildMenu(graph, key, nodes, position, filter);
|
|
116
|
+
if (filter !== undefined && built.items?.length === 0 && built.subMenus?.length === 0) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
menus.push(built);
|
|
85
120
|
}
|
|
86
121
|
|
|
87
122
|
return {
|
|
@@ -91,8 +126,8 @@ export class Publisher {
|
|
|
91
126
|
}
|
|
92
127
|
}
|
|
93
128
|
|
|
94
|
-
contextMenu(graph: NodeSubsystem, position: Vector2): ContextMenuConfig {
|
|
95
|
-
return this.#recurseBuildMenu(graph, this.#name, this.#registeredNodes, position);
|
|
129
|
+
contextMenu(graph: NodeSubsystem, position: Vector2, filter?: NodeMenuFilter): ContextMenuConfig {
|
|
130
|
+
return this.#recurseBuildMenu(graph, this.#name, this.#registeredNodes, position, filter);
|
|
96
131
|
}
|
|
97
132
|
|
|
98
133
|
create(nodeType: string): FlowNode {
|
package/src/nodes/subsystem.ts
CHANGED
|
@@ -6,7 +6,8 @@ import { FlowNode, NodeState } from "../node";
|
|
|
6
6
|
import { Organize } from "../organize";
|
|
7
7
|
import { PassSubsystem } from "../pass/subsystem";
|
|
8
8
|
import { TimeExecution } from "../performance";
|
|
9
|
-
import { Port, PortType } from "../port";
|
|
9
|
+
import { Port, PortType, portsCompatible } from "../port";
|
|
10
|
+
import { requestRender } from "../render_scheduler";
|
|
10
11
|
import { BoxStyle } from "../styles/box";
|
|
11
12
|
import { CursorStyle } from "../styles/cursor";
|
|
12
13
|
import { Theme } from "../theme";
|
|
@@ -23,6 +24,33 @@ export type NodeRemovedCallback = (node: FlowNode) => void;
|
|
|
23
24
|
|
|
24
25
|
export const nodeFlowGroup = "node-flow-graph-node-menu";
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* A connection between two nodes of a copied set, by their position in that
|
|
29
|
+
* set rather than by node, so it can be rebuilt against the copies.
|
|
30
|
+
*/
|
|
31
|
+
export interface InternalConnection {
|
|
32
|
+
fromNode: number;
|
|
33
|
+
fromPort: number;
|
|
34
|
+
toNode: number;
|
|
35
|
+
toPort: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A connection that was dragged out and dropped on empty canvas. Describes
|
|
40
|
+
* what a node would have to offer to finish it, and joins the two up once
|
|
41
|
+
* one has been made.
|
|
42
|
+
*/
|
|
43
|
+
export interface DanglingConnection {
|
|
44
|
+
/** The side the new node has to offer. */
|
|
45
|
+
needs: PortType;
|
|
46
|
+
|
|
47
|
+
/** The types that side has to accept. Empty takes anything. */
|
|
48
|
+
types: Array<string>;
|
|
49
|
+
|
|
50
|
+
/** Wires the new node to the port the drag started from. */
|
|
51
|
+
connectTo: (node: FlowNode) => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
26
54
|
export interface ConnectionRendererConfiguration {
|
|
27
55
|
size?: number;
|
|
28
56
|
color?: string;
|
|
@@ -216,6 +244,10 @@ export class NodeSubsystem {
|
|
|
216
244
|
this.#boxSelect = true;
|
|
217
245
|
}
|
|
218
246
|
|
|
247
|
+
if (!hoveringSomething && !ctrlKey) {
|
|
248
|
+
this.unselectAllNodes();
|
|
249
|
+
}
|
|
250
|
+
|
|
219
251
|
return hoveringSomething || ctrlKey;
|
|
220
252
|
}
|
|
221
253
|
|
|
@@ -285,6 +317,119 @@ export class NodeSubsystem {
|
|
|
285
317
|
}
|
|
286
318
|
}
|
|
287
319
|
|
|
320
|
+
public connectionsWithin(nodes: Array<FlowNode>): Array<InternalConnection> {
|
|
321
|
+
const within: Array<InternalConnection> = [];
|
|
322
|
+
|
|
323
|
+
for (const connection of this.#connections) {
|
|
324
|
+
const outNode = connection.outNode();
|
|
325
|
+
const inNode = connection.inNode();
|
|
326
|
+
if (outNode === null || inNode === null) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const from = nodes.indexOf(outNode);
|
|
331
|
+
const to = nodes.indexOf(inNode);
|
|
332
|
+
if (from === -1 || to === -1) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const outPort = connection.outPort();
|
|
337
|
+
const inPort = connection.inPort();
|
|
338
|
+
if (outPort === null || inPort === null) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
within.push({
|
|
343
|
+
fromNode: from,
|
|
344
|
+
fromPort: this.#outputPortIndex(outNode, outPort),
|
|
345
|
+
toNode: to,
|
|
346
|
+
toPort: this.#inputPortIndex(inNode, inPort),
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return within;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
#onConnectionReleased: ((dangling: DanglingConnection) => void) | null = null;
|
|
354
|
+
|
|
355
|
+
public setConnectionReleasedHandler(handler: (dangling: DanglingConnection) => void): void {
|
|
356
|
+
this.#onConnectionReleased = handler;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
#describeDanglingConnection(conn: Connection): DanglingConnection | null {
|
|
360
|
+
const outNode = conn.outNode();
|
|
361
|
+
const inNode = conn.inNode();
|
|
362
|
+
|
|
363
|
+
// Dragged from an output, so the new node supplies the input.
|
|
364
|
+
if (outNode !== null && inNode === null) {
|
|
365
|
+
const from = conn.outPort();
|
|
366
|
+
if (from === null) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
const fromIndex = this.#outputPortIndex(outNode, from);
|
|
370
|
+
return {
|
|
371
|
+
needs: PortType.Input,
|
|
372
|
+
types: from.acceptedTypes(),
|
|
373
|
+
connectTo: (node) => {
|
|
374
|
+
const to = this.#firstCompatiblePort(node, PortType.Input, from);
|
|
375
|
+
if (to !== -1 && fromIndex !== -1) {
|
|
376
|
+
this.connectNodes(outNode, fromIndex, node, to);
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Dragged from an input, so the new node supplies the output.
|
|
383
|
+
if (inNode !== null && outNode === null) {
|
|
384
|
+
const to = conn.inPort();
|
|
385
|
+
if (to === null) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
const toIndex = this.#inputPortIndex(inNode, to);
|
|
389
|
+
return {
|
|
390
|
+
needs: PortType.Output,
|
|
391
|
+
types: to.acceptedTypes(),
|
|
392
|
+
connectTo: (node) => {
|
|
393
|
+
const from = this.#firstCompatiblePort(node, PortType.Output, to);
|
|
394
|
+
if (from !== -1 && toIndex !== -1) {
|
|
395
|
+
this.connectNodes(node, from, inNode, toIndex);
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
#outputPortIndex(node: FlowNode, port: Port): number {
|
|
405
|
+
for (let i = 0; i < node.outputs(); i++) {
|
|
406
|
+
if (node.outputPort(i) === port) {
|
|
407
|
+
return i;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return -1;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
#inputPortIndex(node: FlowNode, port: Port): number {
|
|
414
|
+
for (let i = 0; i < node.inputs(); i++) {
|
|
415
|
+
if (node.inputPort(i) === port) {
|
|
416
|
+
return i;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return -1;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
#firstCompatiblePort(node: FlowNode, side: PortType, against: Port): number {
|
|
423
|
+
const count = side === PortType.Input ? node.inputs() : node.outputs();
|
|
424
|
+
for (let i = 0; i < count; i++) {
|
|
425
|
+
const port = side === PortType.Input ? node.inputPort(i) : node.outputPort(i);
|
|
426
|
+
if (portsCompatible(port, against)) {
|
|
427
|
+
return i;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return -1;
|
|
431
|
+
}
|
|
432
|
+
|
|
288
433
|
clickEnd(): void {
|
|
289
434
|
|
|
290
435
|
for (let i = 0; i < this.#nodesGrabbed.Count(); i++) {
|
|
@@ -312,7 +457,11 @@ export class NodeSubsystem {
|
|
|
312
457
|
}
|
|
313
458
|
|
|
314
459
|
if (this.#portHovering === null) {
|
|
460
|
+
const dangling = this.#describeDanglingConnection(this.#connectionSelected);
|
|
315
461
|
this.#clearCurrentlySelectedConnection();
|
|
462
|
+
if (dangling !== null) {
|
|
463
|
+
this.#onConnectionReleased?.(dangling);
|
|
464
|
+
}
|
|
316
465
|
return;
|
|
317
466
|
}
|
|
318
467
|
|
|
@@ -341,13 +490,13 @@ export class NodeSubsystem {
|
|
|
341
490
|
}
|
|
342
491
|
|
|
343
492
|
// Different types, can't connect
|
|
344
|
-
if (this.#portHovering.InputPort && this.#portHovering.Port
|
|
493
|
+
if (this.#portHovering.InputPort && !portsCompatible(this.#portHovering.Port, conn.outPort())) {
|
|
345
494
|
this.#clearCurrentlySelectedConnection();
|
|
346
495
|
return;
|
|
347
496
|
}
|
|
348
497
|
|
|
349
498
|
// Different types, can't connect
|
|
350
|
-
if (!this.#portHovering.InputPort && this.#portHovering.Port
|
|
499
|
+
if (!this.#portHovering.InputPort && !portsCompatible(this.#portHovering.Port, conn.inPort())) {
|
|
351
500
|
this.#clearCurrentlySelectedConnection();
|
|
352
501
|
return;
|
|
353
502
|
}
|
|
@@ -377,10 +526,10 @@ export class NodeSubsystem {
|
|
|
377
526
|
}
|
|
378
527
|
|
|
379
528
|
connectNodes(nodeOut: FlowNode, outPort: number, nodeIn: FlowNode, inPort: number): Connection | undefined {
|
|
380
|
-
const
|
|
381
|
-
const
|
|
382
|
-
if (
|
|
383
|
-
console.error("can't connect nodes of different types",
|
|
529
|
+
const from = nodeOut.outputPort(outPort);
|
|
530
|
+
const to = nodeIn.inputPort(inPort);
|
|
531
|
+
if (!portsCompatible(from, to)) {
|
|
532
|
+
console.error("can't connect nodes of different types", from.getDataType(), to.getDataType());
|
|
384
533
|
return;
|
|
385
534
|
}
|
|
386
535
|
|
|
@@ -408,6 +557,7 @@ export class NodeSubsystem {
|
|
|
408
557
|
this.#idleConnectionRenderer,
|
|
409
558
|
);
|
|
410
559
|
this.#connections.push(connection);
|
|
560
|
+
requestRender();
|
|
411
561
|
return connection;
|
|
412
562
|
}
|
|
413
563
|
|
|
@@ -478,6 +628,7 @@ export class NodeSubsystem {
|
|
|
478
628
|
|
|
479
629
|
addNode(node: FlowNode): void {
|
|
480
630
|
this.#nodes.push(node);
|
|
631
|
+
requestRender();
|
|
481
632
|
for (let i = 0; i < this.#registeredNodeAddedCallbacks.length; i++) {
|
|
482
633
|
const callback = this.#registeredNodeAddedCallbacks[i];
|
|
483
634
|
if (!callback) {
|
|
@@ -531,6 +682,7 @@ export class NodeSubsystem {
|
|
|
531
682
|
this.#removeNodeConnections(nodeIndex);
|
|
532
683
|
const node = this.#nodes[nodeIndex];
|
|
533
684
|
this.#nodes.splice(nodeIndex, 1);
|
|
685
|
+
requestRender();
|
|
534
686
|
for (let i = 0; i < this.#nodeRemovedCallbacks.length; i++) {
|
|
535
687
|
this.#nodeRemovedCallbacks[i](node);
|
|
536
688
|
}
|
|
@@ -541,6 +693,7 @@ export class NodeSubsystem {
|
|
|
541
693
|
this.#connections[index].clearPorts();
|
|
542
694
|
}
|
|
543
695
|
this.#connections.splice(index, 1);
|
|
696
|
+
requestRender();
|
|
544
697
|
}
|
|
545
698
|
|
|
546
699
|
#removeConnection(connection: Connection, clearPorts: boolean): void {
|
|
@@ -594,7 +747,8 @@ export class NodeSubsystem {
|
|
|
594
747
|
]
|
|
595
748
|
}
|
|
596
749
|
|
|
597
|
-
|
|
750
|
+
const selectedCount = this.#nodesSelected().length;
|
|
751
|
+
if (selectedCount > 0) {
|
|
598
752
|
organizeNodesSubMenu.items?.push({
|
|
599
753
|
name: "Selected Nodes",
|
|
600
754
|
group: nodeFlowGroup,
|
|
@@ -602,6 +756,14 @@ export class NodeSubsystem {
|
|
|
602
756
|
this.#organizeSelected(ctx)
|
|
603
757
|
}
|
|
604
758
|
})
|
|
759
|
+
|
|
760
|
+
config.items?.push({
|
|
761
|
+
name: selectedCount === 1 ? "Delete Selected Node" : `Delete ${selectedCount} Selected Nodes`,
|
|
762
|
+
group: nodeFlowGroup,
|
|
763
|
+
callback: () => {
|
|
764
|
+
this.removeSelectedNodes();
|
|
765
|
+
}
|
|
766
|
+
})
|
|
605
767
|
}
|
|
606
768
|
|
|
607
769
|
if (this.#nodeHovering > -1) {
|
|
@@ -684,6 +846,32 @@ export class NodeSubsystem {
|
|
|
684
846
|
}
|
|
685
847
|
}
|
|
686
848
|
|
|
849
|
+
/** Drops the selection entirely. */
|
|
850
|
+
public unselectAllNodes(): void {
|
|
851
|
+
let changed = false;
|
|
852
|
+
for (let i = 0; i < this.#nodes.length; i++) {
|
|
853
|
+
if (this.#nodes[i].selected()) {
|
|
854
|
+
this.#nodes[i].unselect();
|
|
855
|
+
changed = true;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
if (changed) {
|
|
860
|
+
requestRender();
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Removes every selected node, and the connections that touched them.
|
|
866
|
+
*/
|
|
867
|
+
public removeSelectedNodes(): Array<FlowNode> {
|
|
868
|
+
const removed = this.getSelectedNodes();
|
|
869
|
+
for (const node of removed) {
|
|
870
|
+
this.removeNode(node);
|
|
871
|
+
}
|
|
872
|
+
return removed;
|
|
873
|
+
}
|
|
874
|
+
|
|
687
875
|
#selectNodeByIndex(nodeIndex: number, unselectOthers: boolean): void {
|
|
688
876
|
this.#selectNode(this.#nodes[nodeIndex], unselectOthers);
|
|
689
877
|
}
|
|
@@ -729,9 +917,22 @@ export class NodeSubsystem {
|
|
|
729
917
|
|
|
730
918
|
const selectedBox_Screenspace = this.#boxSelectionScreenspaceBox(camera);
|
|
731
919
|
|
|
920
|
+
// Nodes outside the canvas are not drawn, but their ports are still
|
|
921
|
+
// placed, because wires to them stay on screen.
|
|
922
|
+
const canvas: Box = {
|
|
923
|
+
Position: { x: 0, y: 0 },
|
|
924
|
+
Size: { x: ctx.canvas.width, y: ctx.canvas.height },
|
|
925
|
+
};
|
|
926
|
+
|
|
732
927
|
for (let i = 0; i < this.#nodes.length; i++) {
|
|
733
928
|
let state = NodeState.Idle;
|
|
734
929
|
|
|
930
|
+
const nodeBounds = this.#nodes[i].calculateBounds(ctx, camera);
|
|
931
|
+
if (!BoxIntersection(nodeBounds, canvas)
|
|
932
|
+
&& this.#nodes[i].layoutPortsWithoutDrawing(nodeBounds, camera.zoom)) {
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
|
|
735
936
|
if (mousePosition !== undefined && !this.#boxSelect) {
|
|
736
937
|
const intersection = this.#nodes[i].inBounds(ctx, camera, mousePosition);
|
|
737
938
|
|
|
@@ -791,6 +992,11 @@ export class NodeSubsystem {
|
|
|
791
992
|
render(ctx: CanvasRenderingContext2D, camera: Camera, mousePosition: Vector2 | undefined): RenderResults | undefined {
|
|
792
993
|
this.#lastCtx = ctx;
|
|
793
994
|
this.#cursor = CursorStyle.Default;
|
|
995
|
+
|
|
996
|
+
// Connections draw first but need port positions, which only exist
|
|
997
|
+
// once a node has been drawn so a new node costs one extra frame.
|
|
998
|
+
const awaitingLayout = this.#nodes.some((node) => !node.hasBeenLaidOut());
|
|
999
|
+
|
|
794
1000
|
TimeExecution("Render_Connections", () => {
|
|
795
1001
|
this.#renderConnections(ctx, camera, mousePosition);
|
|
796
1002
|
})
|
|
@@ -798,6 +1004,10 @@ export class NodeSubsystem {
|
|
|
798
1004
|
this.#renderNodes(ctx, camera, mousePosition);
|
|
799
1005
|
})
|
|
800
1006
|
|
|
1007
|
+
if (awaitingLayout) {
|
|
1008
|
+
requestRender();
|
|
1009
|
+
}
|
|
1010
|
+
|
|
801
1011
|
if (this.#boxSelect) {
|
|
802
1012
|
const box = this.#boxSelectionScreenspaceBox(camera);
|
|
803
1013
|
ctx.setLineDash([Theme.BoxSelect.LineDashLength]);
|
package/src/notes/subsystem.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Camera } from "../camera";
|
|
2
2
|
import { ContextMenuConfig } from "../contextMenu";
|
|
3
|
+
import { requestRender } from "../render_scheduler";
|
|
3
4
|
import { RenderResults } from "../graphSubsystem";
|
|
4
5
|
import { InBox } from "../types/box";
|
|
5
6
|
import { Vector2 } from "../types/vector2";
|
|
@@ -81,6 +82,7 @@ export class NoteSubsystem {
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
this.#notes.push(note);
|
|
85
|
+
requestRender();
|
|
84
86
|
for (let i = 0; i < this.#onNoteAddedCallbacks.length; i++) {
|
|
85
87
|
this.#onNoteAddedCallbacks[i](note);
|
|
86
88
|
}
|
|
@@ -222,6 +224,7 @@ export class NoteSubsystem {
|
|
|
222
224
|
if (index > -1) {
|
|
223
225
|
const noteRemoved = this.#notes[index];
|
|
224
226
|
this.#notes.splice(index, 1);
|
|
227
|
+
requestRender();
|
|
225
228
|
|
|
226
229
|
for (let i = 0; i < this.#onNoteRemovedCallbacks.length; i++) {
|
|
227
230
|
this.#onNoteRemovedCallbacks[i](noteRemoved)
|