@elicdavis/node-flow 0.1.16 → 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/dist/index.d.mts +97 -4
- package/dist/index.d.ts +97 -4
- package/dist/index.js +643 -69
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +637 -68
- 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 +80 -1
- package/src/nodes/factory.ts +9 -5
- package/src/nodes/publisher.ts +39 -4
- package/src/nodes/subsystem.ts +211 -1
- package/src/notes/subsystem.ts +3 -0
- 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/test/minimap.test.ts +103 -0
- package/test/node_menu_filter.test.ts +119 -0
- package/test/render_scheduler.test.ts +119 -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,63 @@ 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
|
+
return true;
|
|
1338
|
+
}
|
|
1260
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
|
@@ -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++) {
|
|
@@ -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
|
|
|
@@ -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)
|
package/src/port.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Camera } from "./camera";
|
|
2
2
|
import { Connection } from './connection';
|
|
3
|
+
import { requestRender } from "./render_scheduler";
|
|
3
4
|
import { RenderElementBase } from "./elements/base";
|
|
4
5
|
import { ContainerRenderElement, AlignItems } from './elements/container';
|
|
5
6
|
import { TextAlign, TextElement } from "./elements/text";
|
|
@@ -67,13 +68,24 @@ export function portsCompatible(a: Port | undefined | null, b: Port | undefined
|
|
|
67
68
|
return false;
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
return typeSetsCompatible(a.acceptedTypes(), b.acceptedTypes());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function typeSetsCompatible(a: Array<string>, b: Array<string>): boolean {
|
|
75
|
+
if (a.length === 0 || b.length === 0) {
|
|
73
76
|
return true;
|
|
74
77
|
}
|
|
78
|
+
return a.some((type) => b.includes(type));
|
|
79
|
+
}
|
|
75
80
|
|
|
76
|
-
|
|
81
|
+
export function portConfigTypes(config: PortConfig): Array<string> {
|
|
82
|
+
if (config.anyType === true) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
if (config.acceptedTypes !== undefined && config.acceptedTypes.length > 0) {
|
|
86
|
+
return config.acceptedTypes;
|
|
87
|
+
}
|
|
88
|
+
return [config.type === undefined ? "" : config.type];
|
|
77
89
|
}
|
|
78
90
|
|
|
79
91
|
// Calculate a color hash for an arbirary type
|
|
@@ -220,6 +232,7 @@ export class Port {
|
|
|
220
232
|
this.#filledStyle.fillColor = fallbackColor(this.#dataType, 1);
|
|
221
233
|
}
|
|
222
234
|
this.#dataTypePopupElement = this.#buildTooltip();
|
|
235
|
+
requestRender();
|
|
223
236
|
}
|
|
224
237
|
|
|
225
238
|
setDataType(type: string): void {
|