@elicdavis/node-flow 0.1.14 → 0.1.16
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 +62 -1
- package/dist/index.d.ts +62 -1
- package/dist/index.js +3197 -3337
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3189 -3337
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -2
- package/src/graph.ts +7 -0
- package/src/index.ts +66 -40
- package/src/markdown/entry.ts +21 -2
- package/src/markdown/lexicalParser.ts +22 -32
- package/src/markdown/syntaxParser.ts +93 -68
- package/src/node.ts +61 -25
- package/src/nodes/subsystem.ts +7 -7
- package/src/organize.ts +276 -181
- package/src/port.ts +130 -7
- package/src/styles/text.ts +8 -0
- package/src/types/text.ts +6 -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/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/organize.test.ts +156 -0
- package/test/port.test.ts +125 -0
- package/tsconfig.json +6 -2
- package/dist/web/NodeFlow.js +0 -8
- package/dist/web/NodeFlow.js.map +0 -7
package/src/organize.ts
CHANGED
|
@@ -1,181 +1,276 @@
|
|
|
1
|
-
import { Camera } from "./camera";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
1
|
+
import { Camera } from "./camera";
|
|
2
|
+
import { FlowNode } from "./node";
|
|
3
|
+
import { NodeSubsystem } from "./nodes/subsystem";
|
|
4
|
+
import { Box } from "./types/box";
|
|
5
|
+
import { Vector2 } from "./types/vector2";
|
|
6
|
+
|
|
7
|
+
const widthSpacing = 100;
|
|
8
|
+
const heightSpacing = 50;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* How many times to sweep the graph re-ordering each layer by where its
|
|
12
|
+
* neighbours sit. Crossings drop quickly and then stop improving, so there
|
|
13
|
+
* is no reason to keep going.
|
|
14
|
+
*/
|
|
15
|
+
const orderingSweeps = 8;
|
|
16
|
+
|
|
17
|
+
export interface Layout {
|
|
18
|
+
/** Indices into the node array, one list per layer, left to right. */
|
|
19
|
+
layers: Array<Array<number>>;
|
|
20
|
+
/** Which layer each node landed in, by node index. */
|
|
21
|
+
layerOf: Map<number, number>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Orders the nodes so every edge points from an earlier node to a later one,
|
|
26
|
+
* as far as that is possible. Edges that would point backwards - the ones
|
|
27
|
+
* closing a cycle - are the ones left out, which is what keeps a cyclic
|
|
28
|
+
* graph from hanging or stacking into one column.
|
|
29
|
+
*/
|
|
30
|
+
export function topologicalOrder(sorting: Array<number>, upstream: Map<number, Array<number>>): Array<number> {
|
|
31
|
+
const visited = new Set<number>();
|
|
32
|
+
const order = new Array<number>();
|
|
33
|
+
|
|
34
|
+
// Iterative: a deep graph would blow the stack recursing.
|
|
35
|
+
for (const root of sorting) {
|
|
36
|
+
if (visited.has(root)) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const stack: Array<{ node: number, expanded: boolean }> = [{ node: root, expanded: false }];
|
|
41
|
+
while (stack.length > 0) {
|
|
42
|
+
const top = stack[stack.length - 1];
|
|
43
|
+
|
|
44
|
+
if (top.expanded) {
|
|
45
|
+
stack.pop();
|
|
46
|
+
order.push(top.node);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
top.expanded = true;
|
|
51
|
+
if (visited.has(top.node)) {
|
|
52
|
+
stack.pop();
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
visited.add(top.node);
|
|
56
|
+
|
|
57
|
+
const feeders = upstream.get(top.node);
|
|
58
|
+
if (feeders === undefined) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
for (const feeder of feeders) {
|
|
62
|
+
if (!visited.has(feeder)) {
|
|
63
|
+
stack.push({ node: feeder, expanded: false });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return order;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Puts every node one layer to the right of the furthest node feeding it, so
|
|
74
|
+
* a node never sits left of something it consumes. Taking the furthest and
|
|
75
|
+
* not the first is what keeps a long chain and a short chain that rejoin
|
|
76
|
+
* from landing on top of each other.
|
|
77
|
+
*/
|
|
78
|
+
export function assignLayers(sorting: Array<number>, upstream: Map<number, Array<number>>): Layout {
|
|
79
|
+
const order = topologicalOrder(sorting, upstream);
|
|
80
|
+
const placed = new Set<number>();
|
|
81
|
+
const layerOf = new Map<number, number>();
|
|
82
|
+
|
|
83
|
+
for (const node of order) {
|
|
84
|
+
let layer = 0;
|
|
85
|
+
for (const feeder of upstream.get(node) ?? []) {
|
|
86
|
+
// Only feeders already placed: anything else is a back edge, and
|
|
87
|
+
// following it would mean waiting on a node that waits on this one.
|
|
88
|
+
if (placed.has(feeder)) {
|
|
89
|
+
layer = Math.max(layer, (layerOf.get(feeder) as number) + 1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
layerOf.set(node, layer);
|
|
93
|
+
placed.add(node);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let deepest = 0;
|
|
97
|
+
for (const layer of layerOf.values()) {
|
|
98
|
+
deepest = Math.max(deepest, layer);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const layers = new Array<Array<number>>(deepest + 1);
|
|
102
|
+
for (let i = 0; i < layers.length; i++) {
|
|
103
|
+
layers[i] = new Array<number>();
|
|
104
|
+
}
|
|
105
|
+
for (const node of order) {
|
|
106
|
+
layers[layerOf.get(node) as number].push(node);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { layers, layerOf };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Reorders each layer so nodes sit across from the ones they connect to,
|
|
114
|
+
* by repeatedly moving every node to the average position of its neighbours
|
|
115
|
+
* in the layer before it, then the layer after it. This is what stops the
|
|
116
|
+
* edges crossing over each other.
|
|
117
|
+
*/
|
|
118
|
+
export function orderWithinLayers(
|
|
119
|
+
layout: Layout,
|
|
120
|
+
upstream: Map<number, Array<number>>,
|
|
121
|
+
downstream: Map<number, Array<number>>,
|
|
122
|
+
): void {
|
|
123
|
+
const slotOf = new Map<number, number>();
|
|
124
|
+
const recordSlots = (): void => {
|
|
125
|
+
for (const layer of layout.layers) {
|
|
126
|
+
for (let i = 0; i < layer.length; i++) {
|
|
127
|
+
slotOf.set(layer[i], i);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
recordSlots();
|
|
132
|
+
|
|
133
|
+
const meanSlot = (node: number, neighbours: Map<number, Array<number>>, layer: number): number => {
|
|
134
|
+
let total = 0;
|
|
135
|
+
let count = 0;
|
|
136
|
+
for (const neighbour of neighbours.get(node) ?? []) {
|
|
137
|
+
// Only neighbours in the adjacent layer say anything about where
|
|
138
|
+
// this node should sit; one several layers away does not.
|
|
139
|
+
if (layout.layerOf.get(neighbour) !== layer) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
total += slotOf.get(neighbour) as number;
|
|
143
|
+
count++;
|
|
144
|
+
}
|
|
145
|
+
return count === 0 ? -1 : total / count;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const sweep = (neighbours: Map<number, Array<number>>, towards: number): void => {
|
|
149
|
+
for (let i = 0; i < layout.layers.length; i++) {
|
|
150
|
+
const index = towards > 0 ? i : layout.layers.length - 1 - i;
|
|
151
|
+
const adjacent = index + towards;
|
|
152
|
+
if (adjacent < 0 || adjacent >= layout.layers.length) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const layer = layout.layers[index];
|
|
157
|
+
const keys = new Map<number, number>();
|
|
158
|
+
for (const node of layer) {
|
|
159
|
+
const mean = meanSlot(node, neighbours, adjacent);
|
|
160
|
+
// A node with nothing in the adjacent layer keeps its place
|
|
161
|
+
// rather than being dragged to the top.
|
|
162
|
+
keys.set(node, mean === -1 ? (slotOf.get(node) as number) : mean);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
layer.sort((a, b) => (keys.get(a) as number) - (keys.get(b) as number));
|
|
166
|
+
for (let s = 0; s < layer.length; s++) {
|
|
167
|
+
slotOf.set(layer[s], s);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
for (let i = 0; i < orderingSweeps; i++) {
|
|
173
|
+
sweep(upstream, -1);
|
|
174
|
+
sweep(downstream, 1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function Organize(ctx: CanvasRenderingContext2D, graph: NodeSubsystem, nodesToSort?: Array<number>): void {
|
|
179
|
+
const nodes = graph.getNodes();
|
|
180
|
+
if (nodes.length === 0) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const sorting = new Array<number>();
|
|
185
|
+
if (nodesToSort) {
|
|
186
|
+
if (nodesToSort.length < 2) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
for (const index of nodesToSort) {
|
|
190
|
+
if (index >= 0 && index < nodes.length) {
|
|
191
|
+
sorting.push(index);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
196
|
+
sorting.push(i);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (sorting.length < 2) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const inLayout = new Set<number>(sorting);
|
|
204
|
+
const nodeLUT = new Map<FlowNode, number>();
|
|
205
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
206
|
+
nodeLUT.set(nodes[i], i);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const camera = new Camera();
|
|
210
|
+
const bounds = new Map<number, Box>();
|
|
211
|
+
for (const index of sorting) {
|
|
212
|
+
bounds.set(index, nodes[index].calculateBounds(ctx, camera));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Edges, restricted to what is being laid out. Organizing a selection
|
|
216
|
+
// must not be steered by nodes that are staying where they are.
|
|
217
|
+
const upstream = new Map<number, Array<number>>();
|
|
218
|
+
const downstream = new Map<number, Array<number>>();
|
|
219
|
+
for (const index of sorting) {
|
|
220
|
+
const feeders = new Array<number>();
|
|
221
|
+
for (const feeder of graph.connectedInputsNodeReferencesByIndex(index)) {
|
|
222
|
+
const feederIndex = nodeLUT.get(feeder);
|
|
223
|
+
if (feederIndex !== undefined && inLayout.has(feederIndex) && feederIndex !== index) {
|
|
224
|
+
feeders.push(feederIndex);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
upstream.set(index, feeders);
|
|
228
|
+
|
|
229
|
+
const consumers = new Array<number>();
|
|
230
|
+
for (const consumer of graph.connectedOutputsNodeReferences(index)) {
|
|
231
|
+
const consumerIndex = nodeLUT.get(consumer);
|
|
232
|
+
if (consumerIndex !== undefined && inLayout.has(consumerIndex) && consumerIndex !== index) {
|
|
233
|
+
consumers.push(consumerIndex);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
downstream.set(index, consumers);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const layout = assignLayers(sorting, upstream);
|
|
240
|
+
orderWithinLayers(layout, upstream, downstream);
|
|
241
|
+
|
|
242
|
+
// Keep the result where the nodes already were, so organizing a
|
|
243
|
+
// selection doesn't fling it off to wherever the origin happens to be.
|
|
244
|
+
let anchor: Vector2 = { x: 0, y: 0 };
|
|
245
|
+
for (const index of sorting) {
|
|
246
|
+
const position = nodes[index].getPosition();
|
|
247
|
+
anchor.x += position.x;
|
|
248
|
+
anchor.y += position.y;
|
|
249
|
+
}
|
|
250
|
+
anchor.x /= sorting.length;
|
|
251
|
+
anchor.y /= sorting.length;
|
|
252
|
+
|
|
253
|
+
const layerWidths = layout.layers.map(layer =>
|
|
254
|
+
layer.reduce((widest, node) => Math.max(widest, (bounds.get(node) as Box).Size.x), 0));
|
|
255
|
+
const layerHeights = layout.layers.map(layer =>
|
|
256
|
+
layer.reduce((total, node) => total + (bounds.get(node) as Box).Size.y + heightSpacing, -heightSpacing));
|
|
257
|
+
|
|
258
|
+
const totalWidth = layerWidths.reduce((total, width) => total + width + widthSpacing, -widthSpacing);
|
|
259
|
+
const tallest = layerHeights.reduce((tallest, height) => Math.max(tallest, height), 0);
|
|
260
|
+
|
|
261
|
+
// Inputs on the left, consumers to their right, every layer centered
|
|
262
|
+
// against the tallest one so the graph reads as a band rather than
|
|
263
|
+
// hanging off a single corner.
|
|
264
|
+
let x = anchor.x - (totalWidth / 2);
|
|
265
|
+
for (let i = 0; i < layout.layers.length; i++) {
|
|
266
|
+
let y = anchor.y - (tallest / 2) + ((tallest - layerHeights[i]) / 2);
|
|
267
|
+
|
|
268
|
+
for (const node of layout.layers[i]) {
|
|
269
|
+
const size = (bounds.get(node) as Box).Size;
|
|
270
|
+
nodes[node].setPosition({ x: x + ((layerWidths[i] - size.x) / 2), y: y });
|
|
271
|
+
y += size.y + heightSpacing;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
x += layerWidths[i] + widthSpacing;
|
|
275
|
+
}
|
|
276
|
+
}
|
package/src/port.ts
CHANGED
|
@@ -31,12 +31,51 @@ export interface PortConfig {
|
|
|
31
31
|
type?: string;
|
|
32
32
|
description?: string;
|
|
33
33
|
array?: boolean;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Lets this port connect to a port of any type, instead of only to one
|
|
37
|
+
* declaring the same `type` string. For a port whose type is decided by
|
|
38
|
+
* whatever gets wired into it, where `type` is a placeholder shown to
|
|
39
|
+
* the user rather than a promise about the values passing through.
|
|
40
|
+
*/
|
|
41
|
+
anyType?: boolean;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Every type this port takes, when it takes more than one. For a port
|
|
45
|
+
* that works on a single value or an array of them, where `type` is
|
|
46
|
+
* only the one it happens to be showing.
|
|
47
|
+
*
|
|
48
|
+
* An empty or absent list means the port takes exactly its `type`,
|
|
49
|
+
* unless `anyType` says otherwise.
|
|
50
|
+
*/
|
|
51
|
+
acceptedTypes?: Array<string>;
|
|
52
|
+
|
|
34
53
|
emptyStyle?: PortStyle;
|
|
35
54
|
filledStyle?: PortStyle;
|
|
36
55
|
onConnectionAdded?: ConnectionChangeCallback;
|
|
37
56
|
onConnectionRemoved?: ConnectionChangeCallback;
|
|
38
57
|
};
|
|
39
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Whether two ports may be connected, going by their types alone. Each port
|
|
61
|
+
* offers the set of types it takes - one entry for an ordinary port, several
|
|
62
|
+
* for a port that takes more than one, and none at all for a port that takes
|
|
63
|
+
* anything. They connect when those sets overlap.
|
|
64
|
+
*/
|
|
65
|
+
export function portsCompatible(a: Port | undefined | null, b: Port | undefined | null): boolean {
|
|
66
|
+
if (!a || !b) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const left = a.acceptedTypes();
|
|
71
|
+
const right = b.acceptedTypes();
|
|
72
|
+
if (left.length === 0 || right.length === 0) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return left.some((type) => right.includes(type));
|
|
77
|
+
}
|
|
78
|
+
|
|
40
79
|
// Calculate a color hash for an arbirary type
|
|
41
80
|
function fallbackColor(type: string, s: number): string {
|
|
42
81
|
let value = 0;
|
|
@@ -69,6 +108,18 @@ export class Port {
|
|
|
69
108
|
|
|
70
109
|
#dataType: string;
|
|
71
110
|
|
|
111
|
+
#anyType: boolean;
|
|
112
|
+
|
|
113
|
+
#acceptedTypes: Array<string>;
|
|
114
|
+
|
|
115
|
+
#description: string;
|
|
116
|
+
|
|
117
|
+
#array: boolean;
|
|
118
|
+
|
|
119
|
+
#configuredEmptyFill: string | undefined;
|
|
120
|
+
|
|
121
|
+
#configuredFilledFill: string | undefined;
|
|
122
|
+
|
|
72
123
|
#dataTypePopupElement: RenderElementBase;
|
|
73
124
|
|
|
74
125
|
#onConnectionAdded: Array<ConnectionChangeCallback>;
|
|
@@ -81,17 +132,23 @@ export class Port {
|
|
|
81
132
|
this.#portType = portType;
|
|
82
133
|
this.#displayName = config?.name === undefined ? "Port" : config?.name;
|
|
83
134
|
this.#dataType = config?.type === undefined ? "" : config?.type;
|
|
135
|
+
this.#anyType = config?.anyType === true;
|
|
136
|
+
this.#acceptedTypes = config?.acceptedTypes === undefined ? [] : config.acceptedTypes;
|
|
137
|
+
this.#description = config?.description === undefined ? "" : config.description;
|
|
138
|
+
this.#array = config?.array === true;
|
|
139
|
+
this.#configuredEmptyFill = config?.emptyStyle?.fillColor;
|
|
140
|
+
this.#configuredFilledFill = config?.filledStyle?.fillColor;
|
|
84
141
|
|
|
85
142
|
this.#emptyStyle = {
|
|
86
143
|
borderColor: config?.emptyStyle?.borderColor === undefined ? "#1c1c1c" : config.emptyStyle?.borderColor,
|
|
87
|
-
fillColor:
|
|
144
|
+
fillColor: this.#configuredEmptyFill === undefined ? fallbackColor(this.#dataType, 0.3) : this.#configuredEmptyFill,
|
|
88
145
|
borderSize: config?.emptyStyle?.borderSize === undefined ? 1 : config.emptyStyle?.borderSize,
|
|
89
146
|
size: config?.emptyStyle?.size === undefined ? 4 : config.emptyStyle?.size
|
|
90
147
|
}
|
|
91
148
|
|
|
92
149
|
this.#filledStyle = {
|
|
93
150
|
borderColor: config?.filledStyle?.borderColor === undefined ? "#1c1c1c" : config.filledStyle?.borderColor,
|
|
94
|
-
fillColor:
|
|
151
|
+
fillColor: this.#configuredFilledFill === undefined ? fallbackColor(this.#dataType, 1) : this.#configuredFilledFill,
|
|
95
152
|
borderSize: config?.filledStyle?.borderSize === undefined ? 1 : config.filledStyle?.borderSize,
|
|
96
153
|
size: config?.filledStyle?.size === undefined ? 5 : config.filledStyle?.size
|
|
97
154
|
}
|
|
@@ -106,9 +163,20 @@ export class Port {
|
|
|
106
163
|
this.#onConnectionRemoved.push(config?.onConnectionRemoved);
|
|
107
164
|
}
|
|
108
165
|
|
|
166
|
+
this.#dataTypePopupElement = this.#buildTooltip();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#buildTooltip(): RenderElementBase {
|
|
109
170
|
const containerElements = new Array<RenderElementBase>();
|
|
171
|
+
|
|
110
172
|
let dataTypeDisplay = this.#dataType;
|
|
111
|
-
if (
|
|
173
|
+
if (this.#acceptedTypes.length > 1) {
|
|
174
|
+
dataTypeDisplay = this.#acceptedTypes.join(" or ");
|
|
175
|
+
}
|
|
176
|
+
if (dataTypeDisplay === "" && this.#anyType) {
|
|
177
|
+
dataTypeDisplay = "any";
|
|
178
|
+
}
|
|
179
|
+
if (this.#array) {
|
|
112
180
|
dataTypeDisplay = "Array<" + dataTypeDisplay + ">"
|
|
113
181
|
}
|
|
114
182
|
containerElements.push(new TextElement(
|
|
@@ -120,10 +188,9 @@ export class Port {
|
|
|
120
188
|
}
|
|
121
189
|
));
|
|
122
190
|
|
|
123
|
-
|
|
124
|
-
if (description && description !== "") {
|
|
191
|
+
if (this.#description !== "") {
|
|
125
192
|
containerElements.push(new TextElement(
|
|
126
|
-
new Text(description, { color: "white", style: FontStyle.Italic }),
|
|
193
|
+
new Text(this.#description, { color: "white", style: FontStyle.Italic }),
|
|
127
194
|
{
|
|
128
195
|
Align: TextAlign.Center,
|
|
129
196
|
Padding: { Top: 16 },
|
|
@@ -133,7 +200,7 @@ export class Port {
|
|
|
133
200
|
));
|
|
134
201
|
}
|
|
135
202
|
|
|
136
|
-
|
|
203
|
+
return new ContainerRenderElement(
|
|
137
204
|
containerElements,
|
|
138
205
|
{
|
|
139
206
|
BackgroundColor: "rgba(0, 0, 0, 0.85)",
|
|
@@ -145,6 +212,41 @@ export class Port {
|
|
|
145
212
|
);
|
|
146
213
|
}
|
|
147
214
|
|
|
215
|
+
#refreshDataType(): void {
|
|
216
|
+
if (this.#configuredEmptyFill === undefined) {
|
|
217
|
+
this.#emptyStyle.fillColor = fallbackColor(this.#dataType, 0.3);
|
|
218
|
+
}
|
|
219
|
+
if (this.#configuredFilledFill === undefined) {
|
|
220
|
+
this.#filledStyle.fillColor = fallbackColor(this.#dataType, 1);
|
|
221
|
+
}
|
|
222
|
+
this.#dataTypePopupElement = this.#buildTooltip();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
setDataType(type: string): void {
|
|
226
|
+
if (this.#dataType === type) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
this.#dataType = type;
|
|
230
|
+
this.#refreshDataType();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
setAnyType(anyType: boolean): void {
|
|
234
|
+
if (this.#anyType === anyType) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
this.#anyType = anyType;
|
|
238
|
+
this.#refreshDataType();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Scope accepted types to singular type
|
|
243
|
+
*/
|
|
244
|
+
settleOn(type: string): void {
|
|
245
|
+
this.#acceptedTypes = [type];
|
|
246
|
+
this.setDataType(type);
|
|
247
|
+
this.#refreshDataType();
|
|
248
|
+
}
|
|
249
|
+
|
|
148
250
|
addConnection(connection: Connection): void {
|
|
149
251
|
const c = this.#connections.length;
|
|
150
252
|
this.#connections.push(connection);
|
|
@@ -195,6 +297,27 @@ export class Port {
|
|
|
195
297
|
return this.#dataType;
|
|
196
298
|
}
|
|
197
299
|
|
|
300
|
+
acceptsAnyType(): boolean {
|
|
301
|
+
return this.#anyType;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Every type this port takes. Empty means it takes anything.
|
|
306
|
+
*/
|
|
307
|
+
acceptedTypes(): Array<string> {
|
|
308
|
+
if (this.#anyType) {
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
if (this.#acceptedTypes.length > 0) {
|
|
312
|
+
return this.#acceptedTypes;
|
|
313
|
+
}
|
|
314
|
+
return [this.#dataType];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
setAcceptedTypes(types: Array<string>): void {
|
|
318
|
+
this.#acceptedTypes = types;
|
|
319
|
+
}
|
|
320
|
+
|
|
198
321
|
getPortType(): PortType {
|
|
199
322
|
return this.#portType;
|
|
200
323
|
}
|