@elicdavis/node-flow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/deploy.yml +57 -0
- package/README.md +159 -0
- package/dev.tsconfig.json +18 -0
- package/docs/promotional-image.png +0 -0
- package/esbuild.ts +32 -0
- package/index.html +311 -0
- package/package.json +50 -0
- package/src/camera.ts +30 -0
- package/src/connection.ts +249 -0
- package/src/contextMenu.ts +367 -0
- package/src/draggable.ts +13 -0
- package/src/elements/base.ts +192 -0
- package/src/elements/container.ts +119 -0
- package/src/elements/interfaces.ts +50 -0
- package/src/elements/text.ts +86 -0
- package/src/graph.ts +535 -0
- package/src/graphSubsystem.ts +31 -0
- package/src/index.ts +28 -0
- package/src/input.ts +153 -0
- package/src/markdown/entry.ts +323 -0
- package/src/markdown/lexicalParser.ts +148 -0
- package/src/markdown/markdown.ts +14 -0
- package/src/markdown/syntaxParser.ts +344 -0
- package/src/markdown/token.ts +43 -0
- package/src/metadata.ts +3 -0
- package/src/node.ts +1130 -0
- package/src/nodes/factory.ts +87 -0
- package/src/nodes/publisher.ts +98 -0
- package/src/nodes/subsystem.ts +760 -0
- package/src/notes/note.ts +295 -0
- package/src/notes/subsystem.ts +262 -0
- package/src/organize.ts +181 -0
- package/src/pass/subsystem.ts +64 -0
- package/src/performance.ts +9 -0
- package/src/popup.ts +81 -0
- package/src/popups/form.ts +63 -0
- package/src/popups/string.ts +33 -0
- package/src/port.ts +223 -0
- package/src/quickMenu.ts +330 -0
- package/src/styles/box.ts +104 -0
- package/src/styles/canvasTextAlign.ts +8 -0
- package/src/styles/canvasTextBaseline.ts +9 -0
- package/src/styles/cursor.ts +18 -0
- package/src/styles/stroke.ts +39 -0
- package/src/styles/text.ts +90 -0
- package/src/styles/textBox.ts +82 -0
- package/src/theme.ts +78 -0
- package/src/types/box.ts +126 -0
- package/src/types/list.ts +41 -0
- package/src/types/pool.ts +66 -0
- package/src/types/text.ts +136 -0
- package/src/types/vector2.ts +34 -0
- package/src/utils/color.ts +136 -0
- package/src/utils/html.ts +21 -0
- package/src/utils/math.ts +8 -0
- package/src/utils/string.ts +98 -0
- package/src/widgets/button.ts +107 -0
- package/src/widgets/color.ts +151 -0
- package/src/widgets/factory.ts +46 -0
- package/src/widgets/image.ts +163 -0
- package/src/widgets/number.ts +153 -0
- package/src/widgets/slider.ts +216 -0
- package/src/widgets/string.ts +128 -0
- package/src/widgets/text.ts +92 -0
- package/src/widgets/toggle.ts +205 -0
- package/src/widgets/widget.ts +12 -0
- package/tsconfig.json +15 -0
- package/tsup.config.ts +10 -0
package/src/organize.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { Camera } from "./camera";
|
|
2
|
+
import { NodeFlowGraph } from "./graph";
|
|
3
|
+
import { FlowNode } from "./node";
|
|
4
|
+
import { NodeSubsystem } from "./nodes/subsystem";
|
|
5
|
+
import { Box } from "./types/box";
|
|
6
|
+
|
|
7
|
+
// TODO: Cyclical dependencies will end our life
|
|
8
|
+
|
|
9
|
+
function MarkInputs(graph: NodeSubsystem, positions: Array<number>, nodeLUT: Map<FlowNode, number>, node: number, depth: number, shouldSort: Map<FlowNode, boolean>) {
|
|
10
|
+
const inputs = graph.connectedInputsNodeReferencesByIndex(node);
|
|
11
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
12
|
+
const nodeIndex = nodeLUT.get(inputs[i]) as number;
|
|
13
|
+
|
|
14
|
+
if (!shouldSort.has(inputs[i])) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
positions[nodeIndex] = depth;
|
|
19
|
+
MarkInputs(graph, positions, nodeLUT, nodeIndex, depth - 1, shouldSort);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function MarkOutputs(graph: NodeSubsystem, positions: Array<number>, nodeLUT: Map<FlowNode, number>, node: number, depth: number, shouldSort: Map<FlowNode, boolean>) {
|
|
24
|
+
const outputs = graph.connectedInputsNodeReferencesByIndex(node);
|
|
25
|
+
for (let i = 0; i < outputs.length; i++) {
|
|
26
|
+
const nodeIndex = nodeLUT.get(outputs[i]) as number;
|
|
27
|
+
|
|
28
|
+
if (!shouldSort.has(outputs[i])) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
positions[nodeIndex] = depth;
|
|
33
|
+
MarkOutputs(graph, positions, nodeLUT, nodeIndex, depth + 1, shouldSort);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function Organize(ctx: CanvasRenderingContext2D, graph: NodeSubsystem, nodesToSort?: Array<number>): void {
|
|
38
|
+
const nodes = graph.getNodes();
|
|
39
|
+
const nodeLUT = new Map<FlowNode, number>();
|
|
40
|
+
const bounds = new Array<Box>(nodes.length);
|
|
41
|
+
const relativePosition = new Array<Array<number>>(nodes.length);
|
|
42
|
+
const claimed = new Array<boolean>(nodes.length);
|
|
43
|
+
const shouldSort = new Map<FlowNode, boolean>(); // TODO: we don't really need a Map, is their a Set data structure?
|
|
44
|
+
|
|
45
|
+
if (nodesToSort) {
|
|
46
|
+
if (nodesToSort.length < 2) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
for (let i = 0; i < nodesToSort.length; i++) {
|
|
50
|
+
shouldSort.set(nodes[nodesToSort[i]], true);
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
54
|
+
shouldSort.set(nodes[i], true);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const camera = new Camera();
|
|
59
|
+
|
|
60
|
+
// Initialize everything
|
|
61
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
62
|
+
bounds[i] = nodes[i].calculateBounds(ctx, camera);
|
|
63
|
+
relativePosition[i] = new Array<number>(nodes.length);
|
|
64
|
+
nodeLUT.set(nodes[i], i);
|
|
65
|
+
claimed[i] = false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
69
|
+
relativePosition[i][i] = 0;
|
|
70
|
+
MarkInputs(graph, relativePosition[i], nodeLUT, i, -1, shouldSort);
|
|
71
|
+
MarkOutputs(graph, relativePosition[i], nodeLUT, i, 1, shouldSort);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface entry {
|
|
75
|
+
node: number;
|
|
76
|
+
length: number;
|
|
77
|
+
min: number;
|
|
78
|
+
max: number
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let entries = new Array<entry>(shouldSort.size);
|
|
82
|
+
|
|
83
|
+
let nodeIndex = 0;
|
|
84
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
85
|
+
if (!shouldSort.has(nodes[i])) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let min = 0;
|
|
90
|
+
let max = 0;
|
|
91
|
+
for (let x = 0; x < nodes.length; x++) {
|
|
92
|
+
const val = relativePosition[i][x];
|
|
93
|
+
if (val === undefined) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
min = Math.min(min, val);
|
|
98
|
+
max = Math.max(max, val);
|
|
99
|
+
}
|
|
100
|
+
entries[nodeIndex] = {
|
|
101
|
+
length: max - min,
|
|
102
|
+
node: i,
|
|
103
|
+
min: min,
|
|
104
|
+
max: max
|
|
105
|
+
}
|
|
106
|
+
nodeIndex++
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
entries.sort((a, b) => b.length - a.length);
|
|
111
|
+
|
|
112
|
+
interface Column {
|
|
113
|
+
Nodes: Array<FlowNode>;
|
|
114
|
+
Width: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const columns = Array<Column>(entries[0].length + 1);
|
|
118
|
+
for (let i = 0; i < columns.length; i++) {
|
|
119
|
+
columns[i] = {
|
|
120
|
+
Nodes: new Array<FlowNode>(),
|
|
121
|
+
Width: 0
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (let i = 0; i < entries.length; i++) {
|
|
126
|
+
const entry = entries[i];
|
|
127
|
+
if (claimed[entry.node] === true) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const positions = relativePosition[entry.node];
|
|
132
|
+
|
|
133
|
+
for (let p = 0; p < positions.length; p++) {
|
|
134
|
+
const position = positions[p];
|
|
135
|
+
if (position === undefined) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (claimed[p] === true) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const nodeBounds = bounds[p];
|
|
144
|
+
const column = columns[position - entry.min];
|
|
145
|
+
column.Nodes.push(nodes[p])
|
|
146
|
+
column.Width = Math.max(column.Width, nodeBounds.Size.x)
|
|
147
|
+
|
|
148
|
+
claimed[p] = true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let allColumnsWidths = 0;
|
|
153
|
+
for (let c = 0; c < columns.length; c++) {
|
|
154
|
+
allColumnsWidths += columns[c].Width;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const widthSpacing = 100;
|
|
158
|
+
const heightSpacing = 50;
|
|
159
|
+
|
|
160
|
+
let widthOffset = 0;
|
|
161
|
+
for (let c = 0; c < columns.length; c++) {
|
|
162
|
+
var column = columns[c];
|
|
163
|
+
let heightOffset = 0;
|
|
164
|
+
|
|
165
|
+
widthOffset -= widthSpacing + column.Width;
|
|
166
|
+
|
|
167
|
+
for (let n = 0; n < column.Nodes.length; n++) {
|
|
168
|
+
const node = column.Nodes[n];
|
|
169
|
+
const nodeBounds = bounds[nodeLUT.get(node) as number];
|
|
170
|
+
|
|
171
|
+
let pos = {
|
|
172
|
+
x: widthOffset + allColumnsWidths + (columns.length * widthSpacing),
|
|
173
|
+
y: heightOffset
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
heightOffset += nodeBounds.Size.y + heightSpacing
|
|
177
|
+
node.setPosition(pos);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Camera } from "../camera";
|
|
2
|
+
import { ContextMenuConfig } from "../contextMenu";
|
|
3
|
+
import { RenderResults } from "../graphSubsystem";
|
|
4
|
+
import { List } from "../types/list";
|
|
5
|
+
import { Vector2 } from "../types/vector2";
|
|
6
|
+
|
|
7
|
+
type RenderLambda = () => void
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generic subsystem meant for other sybsystems to pass whatever to to render
|
|
12
|
+
* at a certain time.
|
|
13
|
+
*
|
|
14
|
+
* Initial inspriation is for a subsytem to submit something to render on top
|
|
15
|
+
* of all other subsystems.
|
|
16
|
+
*/
|
|
17
|
+
export class PassSubsystem {
|
|
18
|
+
|
|
19
|
+
#queue: List<RenderLambda>
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
this.#queue = new List<RenderLambda>();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
queue(lambda: RenderLambda): void {
|
|
26
|
+
if(!lambda) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
this.#queue.Push(lambda);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
render(ctx: CanvasRenderingContext2D, camera: Camera, mousePosition: Vector2 | undefined): RenderResults | undefined {
|
|
33
|
+
for (let i = 0; i < this.#queue.Count(); i++) {
|
|
34
|
+
this.#queue.At(i)();
|
|
35
|
+
}
|
|
36
|
+
this.#queue.Clear();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
openContextMenu(ctx: CanvasRenderingContext2D, position: Vector2): ContextMenuConfig | null {
|
|
41
|
+
// Left intentionally blank
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
clickStart(mousePosition: Vector2, camera: Camera, ctrlKey: boolean): boolean {
|
|
46
|
+
// Left intentionally blank
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
mouseDragEvent(delta: Vector2, scale: number): boolean {
|
|
51
|
+
// Left intentionally blank
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fileDrop(file: File): boolean {
|
|
56
|
+
// Left intentionally blank
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
clickEnd(): void {
|
|
61
|
+
// Left intentionally blank
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
}
|
package/src/popup.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export interface PopupConfig {
|
|
2
|
+
title: string;
|
|
3
|
+
options: Array<string>;
|
|
4
|
+
|
|
5
|
+
onClose?: (button: string | null) => void;
|
|
6
|
+
content?: () => Element;
|
|
7
|
+
buttonCSS?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class Popup {
|
|
11
|
+
|
|
12
|
+
#title: string;
|
|
13
|
+
|
|
14
|
+
#options: Array<string>;
|
|
15
|
+
|
|
16
|
+
#content?: () => Element;
|
|
17
|
+
|
|
18
|
+
#onClose?: (button: string | null) => void;
|
|
19
|
+
|
|
20
|
+
// Styling ================================================================
|
|
21
|
+
|
|
22
|
+
#buttonCSS: string;
|
|
23
|
+
|
|
24
|
+
// Runtime ================================================================
|
|
25
|
+
|
|
26
|
+
#popup: HTMLDivElement | null;
|
|
27
|
+
|
|
28
|
+
constructor(config: PopupConfig) {
|
|
29
|
+
this.#title = config.title;
|
|
30
|
+
this.#options = config.options;
|
|
31
|
+
this.#content = config.content;
|
|
32
|
+
this.#onClose = config.onClose;
|
|
33
|
+
this.#buttonCSS = config.buttonCSS === undefined ? "flex-grow: 1; margin-right: 8px;" : config.buttonCSS;
|
|
34
|
+
this.#popup = null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
Hide(): void {
|
|
38
|
+
if (this.#popup === null) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
document.body.removeChild(this.#popup);
|
|
42
|
+
this.#popup = null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Show(): void {
|
|
46
|
+
this.#popup = document.createElement('div');
|
|
47
|
+
this.#popup.style.cssText = 'position:absolute;top:0;width:100%;display:flex;height:100%;justify-content:center;align-items:center;background-color:#00000070;';
|
|
48
|
+
document.body.appendChild(this.#popup);
|
|
49
|
+
|
|
50
|
+
const container = document.createElement('div');
|
|
51
|
+
container.style.cssText = 'background-color:#000;color:white;font-family:Verdana;border-radius:8px;padding:16px;';
|
|
52
|
+
this.#popup.appendChild(container);
|
|
53
|
+
|
|
54
|
+
const title = document.createElement("h2");
|
|
55
|
+
title.textContent = this.#title;
|
|
56
|
+
title.style.cssText = "margin-top:0;margin-bottom:16px;";
|
|
57
|
+
container.appendChild(title)
|
|
58
|
+
|
|
59
|
+
if (this.#content !== undefined) {
|
|
60
|
+
container.appendChild(this.#content());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const buttonContainer = document.createElement("div");
|
|
64
|
+
buttonContainer.style.cssText = 'margin-top:16px;justify-content:space-around;align-items:center;flex-direction:row;display:flex;';
|
|
65
|
+
container.appendChild(buttonContainer);
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < this.#options.length; i++) {
|
|
68
|
+
const button = document.createElement("button");
|
|
69
|
+
button.style.cssText = this.#buttonCSS;
|
|
70
|
+
button.textContent = this.#options[i];
|
|
71
|
+
button.onclick = () => {
|
|
72
|
+
this.Hide();
|
|
73
|
+
if (this.#onClose !== undefined) {
|
|
74
|
+
this.#onClose(this.#options[i]);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
buttonContainer.appendChild(button);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Popup } from "../popup";
|
|
2
|
+
|
|
3
|
+
interface FormValue {
|
|
4
|
+
name: string;
|
|
5
|
+
|
|
6
|
+
// https://html.spec.whatwg.org/multipage/input.html#dom-input-type
|
|
7
|
+
type: string;
|
|
8
|
+
|
|
9
|
+
startingValue: any;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Config {
|
|
13
|
+
title: string;
|
|
14
|
+
form: Array<FormValue>;
|
|
15
|
+
onUpdate: (newValue: Array<any>) => void;
|
|
16
|
+
onCancel?: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function FormPopup(config: Config): Popup {
|
|
20
|
+
let input = new Array<HTMLInputElement | null>();
|
|
21
|
+
|
|
22
|
+
const setOption = "Set";
|
|
23
|
+
const cancelOption = "Cancel";
|
|
24
|
+
|
|
25
|
+
return new Popup({
|
|
26
|
+
title: config.title,
|
|
27
|
+
options: [setOption, cancelOption],
|
|
28
|
+
content: () => {
|
|
29
|
+
const container = document.createElement('div');
|
|
30
|
+
container.style.flexDirection = "column";
|
|
31
|
+
container.style.display = "flex";
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < config.form.length; i++) {
|
|
34
|
+
const title = document.createElement("h4");
|
|
35
|
+
title.innerText = config.form[i].name;
|
|
36
|
+
container.append(title);
|
|
37
|
+
|
|
38
|
+
const ele = document.createElement('input');
|
|
39
|
+
ele.value = config.form[i].startingValue;
|
|
40
|
+
ele.type = config.form[i].type;
|
|
41
|
+
container.append(ele);
|
|
42
|
+
input.push(ele);
|
|
43
|
+
container.append(document.createElement("br"));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return container;
|
|
47
|
+
},
|
|
48
|
+
onClose: (button: string | null): void => {
|
|
49
|
+
if (button !== setOption || input === null) {
|
|
50
|
+
if (config.onCancel) {
|
|
51
|
+
config.onCancel();
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const values = new Array<any>();
|
|
57
|
+
for (let i = 0; i < config.form.length; i++) {
|
|
58
|
+
values[i] = input[i]?.value;
|
|
59
|
+
}
|
|
60
|
+
config.onUpdate(values);
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Popup } from "../popup";
|
|
2
|
+
|
|
3
|
+
interface Config {
|
|
4
|
+
title: string;
|
|
5
|
+
startingValue: string;
|
|
6
|
+
onUpdate: (newValue: string) => void;
|
|
7
|
+
onCancel?: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function SetStringPopup(config: Config): Popup {
|
|
11
|
+
let input: HTMLInputElement | null = null;
|
|
12
|
+
|
|
13
|
+
return new Popup({
|
|
14
|
+
title: config.title,
|
|
15
|
+
options: ["Set", "Cancel"],
|
|
16
|
+
content: () => {
|
|
17
|
+
const container = document.createElement('div');
|
|
18
|
+
input = document.createElement('input')
|
|
19
|
+
input.value = config.startingValue;
|
|
20
|
+
container.append(input);
|
|
21
|
+
return container;
|
|
22
|
+
},
|
|
23
|
+
onClose: (button: string | null): void => {
|
|
24
|
+
if (button !== "Set" || input === null) {
|
|
25
|
+
if(config.onCancel) {
|
|
26
|
+
config.onCancel();
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
config.onUpdate(input.value);
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
package/src/port.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { Camera } from "./camera";
|
|
2
|
+
import { Connection } from './connection';
|
|
3
|
+
import { FlowNode } from "./node";
|
|
4
|
+
import { PassSubsystem } from "./pass/subsystem";
|
|
5
|
+
import { TextAlign } from "./styles/canvasTextAlign";
|
|
6
|
+
import { Box, BoxIntersection, InBox } from "./types/box";
|
|
7
|
+
import { Vector2 } from "./types/vector2";
|
|
8
|
+
import { Color, HSV, HSV2RGB, RgbToHex } from "./utils/color";
|
|
9
|
+
|
|
10
|
+
export enum PortType {
|
|
11
|
+
Input = "INPUT",
|
|
12
|
+
Output = "OUTPUT",
|
|
13
|
+
InputArray = "INPUTARRAY",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PortStyle {
|
|
17
|
+
size?: number;
|
|
18
|
+
fillColor?: string;
|
|
19
|
+
borderColor?: string;
|
|
20
|
+
borderSize?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type ConnectionChangeCallback = (connection: Connection, connectionIndex: number, port: Port, portType: PortType, node: FlowNode) => void
|
|
24
|
+
|
|
25
|
+
export interface PortConfig {
|
|
26
|
+
name?: string;
|
|
27
|
+
type?: string;
|
|
28
|
+
array?: boolean;
|
|
29
|
+
emptyStyle?: PortStyle;
|
|
30
|
+
filledStyle?: PortStyle;
|
|
31
|
+
onConnectionAdded?: ConnectionChangeCallback;
|
|
32
|
+
onConnectionRemoved?: ConnectionChangeCallback;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Calculate a color hash for an arbirary type
|
|
36
|
+
function fallbackColor(type: string, s: number): string {
|
|
37
|
+
let value = 0;
|
|
38
|
+
for (var i = 0; i < type.length; i++) {
|
|
39
|
+
value += type.charCodeAt(i) * (i + 1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const mod = 24;
|
|
43
|
+
value = Math.round(value) % mod;
|
|
44
|
+
|
|
45
|
+
const hsv: HSV = { h: (value / (mod - 1)) * 360, s: s, v: 1 };
|
|
46
|
+
const color: Color = { r: 0, g: 0, b: 0 };
|
|
47
|
+
HSV2RGB(hsv, color);
|
|
48
|
+
return RgbToHex(color);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class Port {
|
|
52
|
+
|
|
53
|
+
#node: FlowNode;
|
|
54
|
+
|
|
55
|
+
#displayName: string;
|
|
56
|
+
|
|
57
|
+
#emptyStyle: PortStyle;
|
|
58
|
+
|
|
59
|
+
#filledStyle: PortStyle;
|
|
60
|
+
|
|
61
|
+
#connections: Array<Connection>;
|
|
62
|
+
|
|
63
|
+
#portType: PortType
|
|
64
|
+
|
|
65
|
+
#dataType: string;
|
|
66
|
+
|
|
67
|
+
#onConnectionAdded: Array<ConnectionChangeCallback>;
|
|
68
|
+
|
|
69
|
+
#onConnectionRemoved: Array<ConnectionChangeCallback>;;
|
|
70
|
+
|
|
71
|
+
constructor(node: FlowNode, portType: PortType, config?: PortConfig) {
|
|
72
|
+
this.#node = node;
|
|
73
|
+
this.#connections = new Array<Connection>();
|
|
74
|
+
this.#portType = portType;
|
|
75
|
+
this.#displayName = config?.name === undefined ? "Port" : config?.name;
|
|
76
|
+
this.#dataType = config?.type === undefined ? "" : config?.type;
|
|
77
|
+
|
|
78
|
+
this.#emptyStyle = {
|
|
79
|
+
borderColor: config?.emptyStyle?.borderColor === undefined ? "#1c1c1c" : config.emptyStyle?.borderColor,
|
|
80
|
+
fillColor: config?.emptyStyle?.fillColor === undefined ? fallbackColor(this.#dataType, 0.3) : config.emptyStyle?.fillColor,
|
|
81
|
+
borderSize: config?.emptyStyle?.borderSize === undefined ? 1 : config.emptyStyle?.borderSize,
|
|
82
|
+
size: config?.emptyStyle?.size === undefined ? 4 : config.emptyStyle?.size
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.#filledStyle = {
|
|
86
|
+
borderColor: config?.filledStyle?.borderColor === undefined ? "#1c1c1c" : config.filledStyle?.borderColor,
|
|
87
|
+
fillColor: config?.filledStyle?.fillColor === undefined ? fallbackColor(this.#dataType, 1) : config.filledStyle?.fillColor,
|
|
88
|
+
borderSize: config?.filledStyle?.borderSize === undefined ? 1 : config.filledStyle?.borderSize,
|
|
89
|
+
size: config?.filledStyle?.size === undefined ? 5 : config.filledStyle?.size
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.#onConnectionAdded = new Array<ConnectionChangeCallback>();
|
|
93
|
+
if (config?.onConnectionAdded) {
|
|
94
|
+
this.#onConnectionAdded.push(config?.onConnectionAdded);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.#onConnectionRemoved = new Array<ConnectionChangeCallback>();
|
|
98
|
+
if (config?.onConnectionRemoved) {
|
|
99
|
+
this.#onConnectionRemoved.push(config?.onConnectionRemoved);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
addConnection(connection: Connection): void {
|
|
104
|
+
const c = this.#connections.length;
|
|
105
|
+
this.#connections.push(connection);
|
|
106
|
+
for (let i = 0; i < this.#onConnectionAdded.length; i++) {
|
|
107
|
+
this.#onConnectionAdded[i](connection, c, this, this.#portType, this.#node);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
replaceConnection(connection: Connection, index: number): void {
|
|
112
|
+
const c = this.#connections.length;
|
|
113
|
+
this.#connections[index] = connection;
|
|
114
|
+
for (let i = 0; i < this.#onConnectionAdded.length; i++) {
|
|
115
|
+
this.#onConnectionAdded[i](connection, c, this, this.#portType, this.#node);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
addConnectionAddedListener(callback: ConnectionChangeCallback) {
|
|
120
|
+
if (callback === undefined) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
this.#onConnectionAdded.push(callback);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
connections(): Array<Connection> {
|
|
127
|
+
return this.#connections;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
addConnectionRemovedListener(callback: ConnectionChangeCallback) {
|
|
131
|
+
if (callback === undefined) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
this.#onConnectionRemoved.push(callback);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
clearConnection(connection: Connection): void {
|
|
138
|
+
const index = this.#connections.indexOf(connection);
|
|
139
|
+
if (index > -1) {
|
|
140
|
+
this.#connections.splice(index, 1);
|
|
141
|
+
for (let i = 0; i < this.#onConnectionRemoved.length; i++) {
|
|
142
|
+
this.#onConnectionRemoved[i](connection, index, this, this.#portType, this.#node);
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
console.error("no connection found to remove");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
getDataType(): string {
|
|
150
|
+
return this.#dataType;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
getPortType(): PortType {
|
|
154
|
+
return this.#portType;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
getDisplayName(): string {
|
|
158
|
+
return this.#displayName;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
filledStyleColor(): string {
|
|
162
|
+
if (this.#filledStyle.fillColor === undefined) {
|
|
163
|
+
console.error("There's no fill color!!!!!!!!!")
|
|
164
|
+
return "black";
|
|
165
|
+
}
|
|
166
|
+
return this.#filledStyle.fillColor;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#box: Box = { Position: { x: 0, y: 0 }, Size: { x: 0, y: 0 } };
|
|
170
|
+
|
|
171
|
+
render(ctx: CanvasRenderingContext2D, position: Vector2, camera: Camera, mousePosition: Vector2 | undefined, postProcess: PassSubsystem): Box {
|
|
172
|
+
let style = this.#emptyStyle;
|
|
173
|
+
if (this.#connections.length > 0) {
|
|
174
|
+
style = this.#filledStyle;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let scaledRadius = style.size as number * camera.zoom
|
|
178
|
+
|
|
179
|
+
if (mousePosition && InBox(this.#box, mousePosition)) {
|
|
180
|
+
scaledRadius *= 1.25;
|
|
181
|
+
|
|
182
|
+
// Redeclare so lambda is ensured to use these values
|
|
183
|
+
const xPos = position.x;
|
|
184
|
+
const yPos = position.y;
|
|
185
|
+
|
|
186
|
+
postProcess.queue(() => {
|
|
187
|
+
ctx.textAlign = TextAlign.Center;
|
|
188
|
+
|
|
189
|
+
const padding = 13;
|
|
190
|
+
const measurement = ctx.measureText(this.#dataType)
|
|
191
|
+
const w = measurement.width + (padding * camera.zoom);
|
|
192
|
+
|
|
193
|
+
ctx.beginPath();
|
|
194
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0.75)";
|
|
195
|
+
ctx.roundRect(xPos - w / 2, yPos + (scaledRadius * 3) - (padding * camera.zoom), w, (padding * 2) * camera.zoom, 4 * camera.zoom);
|
|
196
|
+
ctx.fill();
|
|
197
|
+
|
|
198
|
+
ctx.fillStyle = "#FFFFFF";
|
|
199
|
+
ctx.fillText(this.#dataType, xPos, yPos + (scaledRadius * 3));
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.#box.Position.x = position.x - scaledRadius;
|
|
204
|
+
this.#box.Position.y = position.y - scaledRadius;
|
|
205
|
+
this.#box.Size.x = scaledRadius * 2;
|
|
206
|
+
this.#box.Size.y = scaledRadius * 2;
|
|
207
|
+
|
|
208
|
+
ctx.strokeStyle = style.borderColor as string;
|
|
209
|
+
ctx.fillStyle = style.fillColor as string;
|
|
210
|
+
|
|
211
|
+
ctx.beginPath();
|
|
212
|
+
if (this.#portType === PortType.InputArray) {
|
|
213
|
+
ctx.rect(position.x - scaledRadius, position.y - scaledRadius, scaledRadius * 2, scaledRadius * 2)
|
|
214
|
+
} else {
|
|
215
|
+
ctx.arc(position.x, position.y, scaledRadius, 0, 2 * Math.PI);
|
|
216
|
+
}
|
|
217
|
+
ctx.fill();
|
|
218
|
+
ctx.stroke();
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
return this.#box;
|
|
222
|
+
}
|
|
223
|
+
}
|