@elicdavis/node-flow 0.1.16 → 0.1.18
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/dist/index.d.mts +97 -4
- package/dist/index.d.ts +97 -4
- package/dist/index.js +674 -79
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +668 -78
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/graph.ts +234 -41
- package/src/index.ts +16 -0
- package/src/input.ts +22 -14
- package/src/markdown/entry.ts +6 -2
- package/src/minimap.ts +178 -0
- package/src/node.ts +83 -1
- package/src/nodes/factory.ts +9 -5
- package/src/nodes/publisher.ts +39 -4
- package/src/nodes/subsystem.ts +214 -2
- package/src/notes/subsystem.ts +3 -0
- package/src/popup.ts +27 -6
- package/src/port.ts +17 -4
- package/src/render_scheduler.ts +49 -0
- package/src/theme.ts +8 -0
- package/src/types/text.ts +2 -0
- package/src/widgets/image.ts +2 -0
- package/src/widgets/number.ts +2 -1
- package/src/widgets/string.ts +3 -1
- package/src/widgets/toggle.ts +3 -1
- package/test/minimap.test.ts +103 -0
- package/test/node_menu_filter.test.ts +119 -0
- package/test/popup.test.ts +104 -0
- package/test/render_scheduler.test.ts +119 -0
- package/test/widget_click.test.ts +126 -0
- package/test/widget_construction.test.ts +48 -0
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,11 +859,13 @@ export class FlowNode {
|
|
|
854
859
|
|
|
855
860
|
addWidget(widget: Widget): void {
|
|
856
861
|
this.#widgets.push(widget);
|
|
862
|
+
requestRender();
|
|
857
863
|
}
|
|
858
864
|
|
|
859
865
|
insertWidget(index: number, widget: Widget): void {
|
|
860
866
|
const at = Math.max(0, Math.min(index, this.#widgets.length));
|
|
861
867
|
this.#widgets.splice(at, 0, widget);
|
|
868
|
+
requestRender();
|
|
862
869
|
}
|
|
863
870
|
|
|
864
871
|
removeWidget(widget: Widget): void {
|
|
@@ -867,13 +874,16 @@ export class FlowNode {
|
|
|
867
874
|
throw new Error("node does not contain widget");
|
|
868
875
|
}
|
|
869
876
|
this.#widgets.splice(index, 1);
|
|
877
|
+
requestRender();
|
|
870
878
|
}
|
|
871
879
|
|
|
872
880
|
removeWidgetAt(index: number): Widget | undefined {
|
|
873
881
|
if (index < 0 || index >= this.#widgets.length) {
|
|
874
882
|
return undefined;
|
|
875
883
|
}
|
|
876
|
-
|
|
884
|
+
const removed = this.#widgets.splice(index, 1)[0];
|
|
885
|
+
requestRender();
|
|
886
|
+
return removed;
|
|
877
887
|
}
|
|
878
888
|
|
|
879
889
|
moveWidget(from: number, to: number): void {
|
|
@@ -885,10 +895,12 @@ export class FlowNode {
|
|
|
885
895
|
}
|
|
886
896
|
const [widget] = this.#widgets.splice(from, 1);
|
|
887
897
|
this.#widgets.splice(to, 0, widget);
|
|
898
|
+
requestRender();
|
|
888
899
|
}
|
|
889
900
|
|
|
890
901
|
clearWidgets(): void {
|
|
891
902
|
this.#widgets.length = 0;
|
|
903
|
+
requestRender();
|
|
892
904
|
}
|
|
893
905
|
|
|
894
906
|
getWidget(index: number): Widget {
|
|
@@ -978,6 +990,7 @@ export class FlowNode {
|
|
|
978
990
|
}
|
|
979
991
|
|
|
980
992
|
this.#title.set(cleaned);
|
|
993
|
+
requestRender();
|
|
981
994
|
|
|
982
995
|
for (let i = 0; i < this.#titleChangeCallback.length; i++) {
|
|
983
996
|
const callback = this.#titleChangeCallback[i];
|
|
@@ -1001,6 +1014,7 @@ export class FlowNode {
|
|
|
1001
1014
|
|
|
1002
1015
|
const old = this.#infoText;
|
|
1003
1016
|
this.#infoText = cleaned;
|
|
1017
|
+
requestRender();
|
|
1004
1018
|
|
|
1005
1019
|
for (let i = 0; i < this.#infoChangeCallback.length; i++) {
|
|
1006
1020
|
const callback = this.#infoChangeCallback[i];
|
|
@@ -1053,7 +1067,15 @@ export class FlowNode {
|
|
|
1053
1067
|
this.#messages = [];
|
|
1054
1068
|
}
|
|
1055
1069
|
|
|
1070
|
+
public hasBeenLaidOut(): boolean {
|
|
1071
|
+
return this.#laidOut;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
#laidOut: boolean = false;
|
|
1075
|
+
|
|
1056
1076
|
render(ctx: CanvasRenderingContext2D, camera: Camera, state: NodeState, mousePosition: Vector2 | undefined, postProcess: PassSubsystem): void {
|
|
1077
|
+
this.#laidOut = true;
|
|
1078
|
+
|
|
1057
1079
|
VectorPool.run(() => {
|
|
1058
1080
|
const tempMeasurement = VectorPool.get();
|
|
1059
1081
|
|
|
@@ -1255,6 +1277,66 @@ export class FlowNode {
|
|
|
1255
1277
|
const message = this.#messages[i];
|
|
1256
1278
|
messageStart.y += message.render(ctx, camera.zoom, messageStart, !(state === NodeState.Idle)) + (10 * camera.zoom);
|
|
1257
1279
|
}
|
|
1280
|
+
|
|
1281
|
+
this.#rememberPortOffsets(nodeBounds, camera.zoom);
|
|
1258
1282
|
})
|
|
1259
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
|
+
|
|
1338
|
+
this.#widgetPositions.Clear();
|
|
1339
|
+
|
|
1340
|
+
return true;
|
|
1341
|
+
}
|
|
1260
1342
|
}
|
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
|
@@ -7,6 +7,7 @@ import { Organize } from "../organize";
|
|
|
7
7
|
import { PassSubsystem } from "../pass/subsystem";
|
|
8
8
|
import { TimeExecution } from "../performance";
|
|
9
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++) {
|
|
@@ -303,7 +448,9 @@ export class NodeSubsystem {
|
|
|
303
448
|
}
|
|
304
449
|
|
|
305
450
|
if (this.#widgetCurrentlyClicking !== null) {
|
|
306
|
-
this.#widgetCurrentlyClicking
|
|
451
|
+
if (this.#widgetHovering === this.#widgetCurrentlyClicking) {
|
|
452
|
+
this.#widgetCurrentlyClicking.ClickEnd();
|
|
453
|
+
}
|
|
307
454
|
this.#widgetCurrentlyClicking = null;
|
|
308
455
|
}
|
|
309
456
|
|
|
@@ -312,7 +459,11 @@ export class NodeSubsystem {
|
|
|
312
459
|
}
|
|
313
460
|
|
|
314
461
|
if (this.#portHovering === null) {
|
|
462
|
+
const dangling = this.#describeDanglingConnection(this.#connectionSelected);
|
|
315
463
|
this.#clearCurrentlySelectedConnection();
|
|
464
|
+
if (dangling !== null) {
|
|
465
|
+
this.#onConnectionReleased?.(dangling);
|
|
466
|
+
}
|
|
316
467
|
return;
|
|
317
468
|
}
|
|
318
469
|
|
|
@@ -408,6 +559,7 @@ export class NodeSubsystem {
|
|
|
408
559
|
this.#idleConnectionRenderer,
|
|
409
560
|
);
|
|
410
561
|
this.#connections.push(connection);
|
|
562
|
+
requestRender();
|
|
411
563
|
return connection;
|
|
412
564
|
}
|
|
413
565
|
|
|
@@ -478,6 +630,7 @@ export class NodeSubsystem {
|
|
|
478
630
|
|
|
479
631
|
addNode(node: FlowNode): void {
|
|
480
632
|
this.#nodes.push(node);
|
|
633
|
+
requestRender();
|
|
481
634
|
for (let i = 0; i < this.#registeredNodeAddedCallbacks.length; i++) {
|
|
482
635
|
const callback = this.#registeredNodeAddedCallbacks[i];
|
|
483
636
|
if (!callback) {
|
|
@@ -531,6 +684,7 @@ export class NodeSubsystem {
|
|
|
531
684
|
this.#removeNodeConnections(nodeIndex);
|
|
532
685
|
const node = this.#nodes[nodeIndex];
|
|
533
686
|
this.#nodes.splice(nodeIndex, 1);
|
|
687
|
+
requestRender();
|
|
534
688
|
for (let i = 0; i < this.#nodeRemovedCallbacks.length; i++) {
|
|
535
689
|
this.#nodeRemovedCallbacks[i](node);
|
|
536
690
|
}
|
|
@@ -541,6 +695,7 @@ export class NodeSubsystem {
|
|
|
541
695
|
this.#connections[index].clearPorts();
|
|
542
696
|
}
|
|
543
697
|
this.#connections.splice(index, 1);
|
|
698
|
+
requestRender();
|
|
544
699
|
}
|
|
545
700
|
|
|
546
701
|
#removeConnection(connection: Connection, clearPorts: boolean): void {
|
|
@@ -594,7 +749,8 @@ export class NodeSubsystem {
|
|
|
594
749
|
]
|
|
595
750
|
}
|
|
596
751
|
|
|
597
|
-
|
|
752
|
+
const selectedCount = this.#nodesSelected().length;
|
|
753
|
+
if (selectedCount > 0) {
|
|
598
754
|
organizeNodesSubMenu.items?.push({
|
|
599
755
|
name: "Selected Nodes",
|
|
600
756
|
group: nodeFlowGroup,
|
|
@@ -602,6 +758,14 @@ export class NodeSubsystem {
|
|
|
602
758
|
this.#organizeSelected(ctx)
|
|
603
759
|
}
|
|
604
760
|
})
|
|
761
|
+
|
|
762
|
+
config.items?.push({
|
|
763
|
+
name: selectedCount === 1 ? "Delete Selected Node" : `Delete ${selectedCount} Selected Nodes`,
|
|
764
|
+
group: nodeFlowGroup,
|
|
765
|
+
callback: () => {
|
|
766
|
+
this.removeSelectedNodes();
|
|
767
|
+
}
|
|
768
|
+
})
|
|
605
769
|
}
|
|
606
770
|
|
|
607
771
|
if (this.#nodeHovering > -1) {
|
|
@@ -684,6 +848,32 @@ export class NodeSubsystem {
|
|
|
684
848
|
}
|
|
685
849
|
}
|
|
686
850
|
|
|
851
|
+
/** Drops the selection entirely. */
|
|
852
|
+
public unselectAllNodes(): void {
|
|
853
|
+
let changed = false;
|
|
854
|
+
for (let i = 0; i < this.#nodes.length; i++) {
|
|
855
|
+
if (this.#nodes[i].selected()) {
|
|
856
|
+
this.#nodes[i].unselect();
|
|
857
|
+
changed = true;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (changed) {
|
|
862
|
+
requestRender();
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Removes every selected node, and the connections that touched them.
|
|
868
|
+
*/
|
|
869
|
+
public removeSelectedNodes(): Array<FlowNode> {
|
|
870
|
+
const removed = this.getSelectedNodes();
|
|
871
|
+
for (const node of removed) {
|
|
872
|
+
this.removeNode(node);
|
|
873
|
+
}
|
|
874
|
+
return removed;
|
|
875
|
+
}
|
|
876
|
+
|
|
687
877
|
#selectNodeByIndex(nodeIndex: number, unselectOthers: boolean): void {
|
|
688
878
|
this.#selectNode(this.#nodes[nodeIndex], unselectOthers);
|
|
689
879
|
}
|
|
@@ -729,9 +919,22 @@ export class NodeSubsystem {
|
|
|
729
919
|
|
|
730
920
|
const selectedBox_Screenspace = this.#boxSelectionScreenspaceBox(camera);
|
|
731
921
|
|
|
922
|
+
// Nodes outside the canvas are not drawn, but their ports are still
|
|
923
|
+
// placed, because wires to them stay on screen.
|
|
924
|
+
const canvas: Box = {
|
|
925
|
+
Position: { x: 0, y: 0 },
|
|
926
|
+
Size: { x: ctx.canvas.width, y: ctx.canvas.height },
|
|
927
|
+
};
|
|
928
|
+
|
|
732
929
|
for (let i = 0; i < this.#nodes.length; i++) {
|
|
733
930
|
let state = NodeState.Idle;
|
|
734
931
|
|
|
932
|
+
const nodeBounds = this.#nodes[i].calculateBounds(ctx, camera);
|
|
933
|
+
if (!BoxIntersection(nodeBounds, canvas)
|
|
934
|
+
&& this.#nodes[i].layoutPortsWithoutDrawing(nodeBounds, camera.zoom)) {
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
|
|
735
938
|
if (mousePosition !== undefined && !this.#boxSelect) {
|
|
736
939
|
const intersection = this.#nodes[i].inBounds(ctx, camera, mousePosition);
|
|
737
940
|
|
|
@@ -791,6 +994,11 @@ export class NodeSubsystem {
|
|
|
791
994
|
render(ctx: CanvasRenderingContext2D, camera: Camera, mousePosition: Vector2 | undefined): RenderResults | undefined {
|
|
792
995
|
this.#lastCtx = ctx;
|
|
793
996
|
this.#cursor = CursorStyle.Default;
|
|
997
|
+
|
|
998
|
+
// Connections draw first but need port positions, which only exist
|
|
999
|
+
// once a node has been drawn so a new node costs one extra frame.
|
|
1000
|
+
const awaitingLayout = this.#nodes.some((node) => !node.hasBeenLaidOut());
|
|
1001
|
+
|
|
794
1002
|
TimeExecution("Render_Connections", () => {
|
|
795
1003
|
this.#renderConnections(ctx, camera, mousePosition);
|
|
796
1004
|
})
|
|
@@ -798,6 +1006,10 @@ export class NodeSubsystem {
|
|
|
798
1006
|
this.#renderNodes(ctx, camera, mousePosition);
|
|
799
1007
|
})
|
|
800
1008
|
|
|
1009
|
+
if (awaitingLayout) {
|
|
1010
|
+
requestRender();
|
|
1011
|
+
}
|
|
1012
|
+
|
|
801
1013
|
if (this.#boxSelect) {
|
|
802
1014
|
const box = this.#boxSelectionScreenspaceBox(camera);
|
|
803
1015
|
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)
|