+1
public/assets/icons/pen-to-square-regular-full.svg
+1
public/assets/icons/pen-to-square-regular-full.svg
···
···
1
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path fill="#cfcfcf" d="M505 122.9L517.1 135C526.5 144.4 526.5 159.6 517.1 168.9L488 198.1L441.9 152L471 122.9C480.4 113.5 495.6 113.5 504.9 122.9zM273.8 320.2L408 185.9L454.1 232L319.8 366.2C316.9 369.1 313.3 371.2 309.4 372.3L250.9 389L267.6 330.5C268.7 326.6 270.8 323 273.7 320.1zM437.1 89L239.8 286.2C231.1 294.9 224.8 305.6 221.5 317.3L192.9 417.3C190.5 425.7 192.8 434.7 199 440.9C205.2 447.1 214.2 449.4 222.6 447L322.6 418.4C334.4 415 345.1 408.7 353.7 400.1L551 202.9C579.1 174.8 579.1 129.2 551 101.1L538.9 89C510.8 60.9 465.2 60.9 437.1 89zM152 128C103.4 128 64 167.4 64 216L64 488C64 536.6 103.4 576 152 576L424 576C472.6 576 512 536.6 512 488L512 376C512 362.7 501.3 352 488 352C474.7 352 464 362.7 464 376L464 488C464 510.1 446.1 528 424 528L152 528C129.9 528 112 510.1 112 488L112 216C112 193.9 129.9 176 152 176L264 176C277.3 176 288 165.3 288 152C288 138.7 277.3 128 264 128L152 128z"/></svg>
+1
public/assets/icons/plus-solid-full.svg
+1
public/assets/icons/plus-solid-full.svg
···
···
1
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path fill="#cfcfcf" d="M352 128C352 110.3 337.7 96 320 96C302.3 96 288 110.3 288 128L288 288L128 288C110.3 288 96 302.3 96 320C96 337.7 110.3 352 128 352L288 352L288 512C288 529.7 302.3 544 320 544C337.7 544 352 529.7 352 512L352 352L512 352C529.7 352 544 337.7 544 320C544 302.3 529.7 288 512 288L352 288L352 128z"/></svg>
+1
public/assets/icons/xmark-solid-full.svg
+1
public/assets/icons/xmark-solid-full.svg
···
···
1
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path fill="#fff" d="M183.1 137.4C170.6 124.9 150.3 124.9 137.8 137.4C125.3 149.9 125.3 170.2 137.8 182.7L275.2 320L137.9 457.4C125.4 469.9 125.4 490.2 137.9 502.7C150.4 515.2 170.7 515.2 183.2 502.7L320.5 365.3L457.9 502.6C470.4 515.1 490.7 515.1 503.2 502.6C515.7 490.1 515.7 469.8 503.2 457.3L365.8 320L503.1 182.6C515.6 170.1 515.6 149.8 503.1 137.3C490.6 124.8 470.3 124.8 457.8 137.3L320.5 274.7L183.1 137.4z"/></svg>
+2
-2
src-tauri/src/frontend_calls/save_graph.rs
+2
-2
src-tauri/src/frontend_calls/save_graph.rs
···
3
use flate2::{ write::GzEncoder, Compression };
4
5
#[tauri::command]
6
-
pub fn save_graph( graph: String ) {
7
dbg!(&graph);
8
9
-
let path = dirs::config_dir().unwrap().join("VRCMacros").join("graph");
10
11
let file = File::create(&path).unwrap();
12
let mut encoder = GzEncoder::new(file, Compression::default());
···
3
use flate2::{ write::GzEncoder, Compression };
4
5
#[tauri::command]
6
+
pub fn save_graph( tab_name: String, graph: String ) {
7
dbg!(&graph);
8
9
+
let path = dirs::config_dir().unwrap().join("VRCMacros").join(format!("{}.macro", tab_name));
10
11
let file = File::create(&path).unwrap();
12
let mut encoder = GzEncoder::new(file, Compression::default());
+18
-2
src-tauri/src/setup.rs
+18
-2
src-tauri/src/setup.rs
···
1
-
use std::sync::{self, Mutex};
2
3
-
use tauri::{ App, Emitter, Manager };
4
5
use crate::osc::{ self, OSCMessage };
6
7
pub fn setup( app: &mut App, addresses: &'static Mutex<Vec<OSCMessage>> ){
8
let window = app.get_webview_window("main").unwrap();
9
10
let ( sender, receiver ) = sync::mpsc::channel();
11
···
1
+
use std::{ fs::File, io::Read, sync::{ self, Mutex } };
2
3
+
use flate2::read::GzDecoder;
4
+
use serde_json::Value;
5
+
use tauri::{ App, Emitter, Listener, Manager };
6
7
use crate::osc::{ self, OSCMessage };
8
9
pub fn setup( app: &mut App, addresses: &'static Mutex<Vec<OSCMessage>> ){
10
let window = app.get_webview_window("main").unwrap();
11
+
12
+
let handle = window.clone();
13
+
window.listen("tauri://drag-drop", move | ev | {
14
+
let path: Value = serde_json::from_str(ev.payload()).unwrap();
15
+
let path = path["paths"][0].as_str().unwrap();
16
+
17
+
let file = File::open(path).unwrap();
18
+
let mut decoder = GzDecoder::new(file);
19
+
let mut string = String::new();
20
+
21
+
decoder.read_to_string(&mut string).unwrap();
22
+
23
+
handle.emit("load_new_tab", Value::String(string)).unwrap();
24
+
});
25
26
let ( sender, receiver ) = sync::mpsc::channel();
27
+210
-184
src/App.tsx
+210
-184
src/App.tsx
···
1
import { createSignal, onCleanup, onMount } from "solid-js";
2
import "./App.css";
3
-
import { renderBackgroundGrid, renderContextMenu, renderNodes, renderTempDrawing } from "./renderer";
4
import { lerp } from "./utils/lerp";
5
import { Node, NodeIO, NodeIOResolveAnyTypes } from "./structs/node";
6
import { isPointInRect, isPointInRectApplyOffset, screenToWorldSpace } from "./utils/interections";
7
-
import { ControlBar } from "./ControlBar";
8
import { CanvasContextMenu } from "./ContextMenu/Canvas";
9
import { NodeContextMenu } from "./ContextMenu/Node";
10
import { ContextMenu } from "./structs/ContextMenu";
11
import { NodeManager } from "./Mangers/NodeManager";
12
13
let App = () => {
14
// TODO: Delete selected node when delete key is pressed
···
55
}
56
57
onMount(() => {
58
ctx = canvas.getContext('2d')!;
59
60
canvas.width = window.innerWidth;
···
77
screenMoved = true;
78
}
79
80
-
requestAnimationFrame(update);
81
-
});
82
83
-
let update = () => {
84
-
if(stopRender)return;
85
86
-
scale = lerp(scale, targetScale, 0.25);
87
88
-
offset[0] = lerp(offset[0], offsetTarget[0], 0.5);
89
-
offset[1] = lerp(offset[1], offsetTarget[1], 0.5);
90
91
-
ctx.clearRect(canvas.width / -2, canvas.height / -2, canvas.width, canvas.height);
92
93
-
renderBackgroundGrid(canvas, ctx, { x: offset[0], y: offset[1], scale });
94
-
renderNodes(canvas, ctx, NodeManager.Instance.GetNodes(), { x: offset[0], y: offset[1], scale });
95
-
if(isDrawing)renderTempDrawing(canvas, ctx, drawingTo, drawingFrom!, { x: offset[0], y: offset[1], scale });
96
-
renderContextMenu(ctx, contextMenu);
97
98
-
requestAnimationFrame(update);
99
-
}
100
-
101
-
let isMouseDown = false;
102
-
let mouseStartPos = [ 0, 0 ];
103
-
104
-
window.oncontextmenu = ( e ) => {
105
-
e.preventDefault();
106
-
107
-
let clickedNode: Node | null = null
108
-
NodeManager.Instance.GetNodes().map(node => {
109
-
if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale },
110
-
e.clientX, e.clientY,
111
-
node.x, node.y, node.w, node.h
112
-
)){
113
-
clickedNode = node;
114
return;
115
}
116
-
})
117
118
-
if(clickedNode){
119
-
contextMenu.items = NodeContextMenu(clickedNode);
120
-
} else{
121
-
contextMenu.items = CanvasContextMenu;
122
-
}
123
124
-
contextMenu.position = [ e.clientX - 10 - canvas.width / 2, e.clientY - 10 - canvas.height / 2 ];
125
-
contextMenu.visible = true;
126
-
}
127
-
128
-
window.onmousedown = ( e ) => {
129
-
if(e.clientY < 50 || lockMovement)return;
130
-
131
-
if(e.button !== 0){
132
-
contextMenu.visible = false;
133
-
return;
134
-
}
135
136
-
if(contextMenu.visible){
137
-
let submenus: ContextMenu[] = [];
138
-
contextMenu.items.map(x => x.menu ? submenus.push(x.menu): null);
139
-
140
-
submenus.map(x => {
141
-
if(!x.visible)return;
142
if(isPointInRect(canvas, e.clientX, e.clientY,
143
-
x.position[0], x.position[1],
144
-
x.size[0], x.size[1]
145
)){
146
-
let item = x.items.filter(x => x.hovered)[0];
147
if(item && item.clicked)item.clicked(e, canvas, { x: offset[0], y: offset[1], scale });
148
}
149
-
});
150
-
151
-
if(isPointInRect(canvas, e.clientX, e.clientY,
152
-
contextMenu.position[0], contextMenu.position[1],
153
-
contextMenu.size[0], contextMenu.size[1]
154
-
)){
155
-
let item = contextMenu.items.filter(x => x.hovered)[0];
156
-
if(item && item.clicked)item.clicked(e, canvas, { x: offset[0], y: offset[1], scale });
157
}
158
-
}
159
160
-
contextMenu.visible = false;
161
162
-
let clickedNode: any = null;
163
-
isDrawing = false;
164
165
-
let clickedInput: any = null;
166
167
-
NodeManager.Instance.GetNodes().map(node => {
168
-
node.selected = false;
169
170
-
if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale },
171
-
e.clientX, e.clientY,
172
-
node.x, node.y, node.w, node.h
173
-
)){
174
-
node.outputs.map(( output, i ) => {
175
if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale },
176
e.clientX, e.clientY,
177
-
node.x + (node.w - 30),
178
-
node.y + 50 + (30 * i),
179
-
20, 20
180
)){
181
-
output.index = i;
182
183
-
drawingTo = [ node.x + (node.w - 30), node.y + 50 + (30 * i) ];
184
-
drawingFrom = output;
185
186
-
isDrawing = true;
187
return;
188
}
189
})
190
-
191
-
node.inputs.map(( input, i ) => {
192
-
if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale },
193
-
e.clientX, e.clientY,
194
-
node.x + 10,
195
-
node.y + 50 + (30 * i),
196
-
20, 20
197
-
)){
198
-
clickedInput = input;
199
-
}
200
-
})
201
-
202
-
clickedNode = node;
203
-
return;
204
}
205
-
})
206
207
-
if(clickedInput){
208
-
let partner = clickedInput.connections.pop();
209
-
if(!partner)return;
210
211
-
partner.connections = partner.connections.filter(( x: any ) => x !== clickedInput);
212
213
-
isDrawing = true;
214
-
isMouseDown = true;
215
216
-
drawingFrom = partner;
217
-
drawingTo = screenToWorldSpace(canvas, { x: offset[0], y: offset[1], scale }, e.clientX - 10 * scale, e.clientY - 10 * scale) as [ number, number ];;
218
219
-
return;
220
-
}
221
222
-
movingNode = clickedNode;
223
224
-
if(clickedNode){
225
-
clickedNode.selected = true;
226
-
setSelectedNode(clickedNode);
227
}
228
229
-
isMouseDown = true;
230
-
mouseStartPos = [ e.clientX, e.clientY ];
231
-
}
232
233
-
window.onmousemove = ( e ) => {
234
-
if(isMouseDown){
235
-
if(isDrawing){
236
-
drawingTo = screenToWorldSpace(canvas, { x: offset[0], y: offset[1], scale }, e.clientX - 10 * scale, e.clientY - 10 * scale) as [ number, number ];
237
-
} else if(movingNode){
238
-
movingNode.x = movingNode.x - (mouseStartPos[0] - e.clientX) / scale;
239
-
movingNode.y = movingNode.y - (mouseStartPos[1] - e.clientY) / scale;
240
241
-
mouseStartPos = [ e.clientX, e.clientY ];
242
-
NodeManager.Instance.UpdateConfig();
243
-
} else{
244
-
offsetTarget = [ offsetTarget[0] - (mouseStartPos[0] - e.clientX) / scale, offsetTarget[1] - (mouseStartPos[1] - e.clientY) / scale ];
245
-
mouseStartPos = [ e.clientX, e.clientY ];
246
247
-
screenMoved = true;
248
-
}
249
-
}
250
251
-
// TODO: Fix this shit lmao please
252
-
if(contextMenu.visible){
253
-
let submenus: ContextMenu[] = [];
254
-
contextMenu.items.map(x => x.menu ? submenus.push(x.menu): null);
255
256
-
submenus.map(x => {
257
-
if(!x.visible)return;
258
if(isPointInRect(canvas, e.clientX, e.clientY,
259
-
x.position[0], x.position[1],
260
-
x.size[0], x.size[1]
261
)){
262
-
x.items.map((y, i) => {
263
-
y.hovered = isPointInRect(canvas, e.clientX, e.clientY,
264
-
x.position[0], x.position[1] + 10 + 25 * i,
265
-
x.size[0], 25
266
)
267
});
268
}
269
-
});
270
271
-
if(isPointInRect(canvas, e.clientX, e.clientY,
272
-
contextMenu.position[0], contextMenu.position[1],
273
-
contextMenu.size[0], contextMenu.size[1]
274
-
)){
275
-
contextMenu.items.map((x, i) => {
276
-
x.hovered = isPointInRect(canvas, e.clientX, e.clientY,
277
-
contextMenu.position[0], contextMenu.position[1] + 10 + 25 * i,
278
-
contextMenu.size[0], 25
279
-
)
280
281
-
if(x.menu)x.menu.visible = x.hovered;
282
-
});
283
}
284
}
285
-
}
286
287
-
window.onmouseup = ( e ) => {
288
-
NodeManager.Instance.GetNodes().map(node => {
289
-
node.inputs.map(( input, i ) => {
290
-
if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale },
291
-
e.clientX, e.clientY,
292
-
node.x + 10,
293
-
node.y + 50 + (30 * i),
294
-
20, 20
295
-
)){
296
-
if(isDrawing){
297
-
let fromType = NodeIOResolveAnyTypes(drawingFrom!);
298
-
let toType = NodeIOResolveAnyTypes(input);
299
300
-
if(
301
-
drawingFrom!.connections.indexOf(input) === -1 &&
302
-
(
303
-
toType === null ||
304
-
fromType === toType
305
-
)
306
-
){
307
-
drawingFrom!.connections.push(input);
308
-
input.connections.push(drawingFrom!);
309
310
-
NodeManager.Instance.UpdateConfig();
311
-
}
312
-
}
313
-
}
314
-
})
315
-
})
316
317
-
isDrawing = false;
318
-
isMouseDown = false;
319
}
320
321
let interval = setInterval(() => {
322
if(screenMoved){
···
333
334
return (
335
<>
336
<ControlBar node={selectedNode} lockMovement={( lock ) => lockMovement = lock} />
337
<canvas ref={canvas}/>
338
</>
···
1
import { createSignal, onCleanup, onMount } from "solid-js";
2
import "./App.css";
3
+
import { renderBackgroundGrid, renderContextMenu, renderNodes, renderNullTab, renderTempDrawing } from "./renderer";
4
import { lerp } from "./utils/lerp";
5
import { Node, NodeIO, NodeIOResolveAnyTypes } from "./structs/node";
6
import { isPointInRect, isPointInRectApplyOffset, screenToWorldSpace } from "./utils/interections";
7
+
import { ControlBar } from "./components/ControlBar";
8
import { CanvasContextMenu } from "./ContextMenu/Canvas";
9
import { NodeContextMenu } from "./ContextMenu/Node";
10
import { ContextMenu } from "./structs/ContextMenu";
11
import { NodeManager } from "./Mangers/NodeManager";
12
+
import { TabMenu } from "./components/TabMenu";
13
14
let App = () => {
15
// TODO: Delete selected node when delete key is pressed
···
56
}
57
58
onMount(() => {
59
+
NodeManager.Instance.HookTabChange(() => setSelectedNode(null));
60
+
61
ctx = canvas.getContext('2d')!;
62
63
canvas.width = window.innerWidth;
···
80
screenMoved = true;
81
}
82
83
+
canvas.oncontextmenu = ( e ) => {
84
+
e.preventDefault();
85
86
+
let clickedNode: Node | null = null
87
+
let nodes = NodeManager.Instance.GetNodes();
88
89
+
if(nodes){
90
+
nodes.map(node => {
91
+
if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale },
92
+
e.clientX, e.clientY,
93
+
node.x, node.y, node.w, node.h
94
+
)){
95
+
clickedNode = node;
96
+
return;
97
+
}
98
+
})
99
+
}
100
101
+
if(clickedNode){
102
+
contextMenu.items = NodeContextMenu(clickedNode);
103
+
} else{
104
+
contextMenu.items = CanvasContextMenu;
105
+
}
106
107
+
contextMenu.position = [ e.clientX - 10 - canvas.width / 2, e.clientY - 10 - canvas.height / 2 ];
108
+
contextMenu.visible = true;
109
+
}
110
111
+
canvas.onmousedown = ( e ) => {
112
+
if(
113
+
e.clientY < 60 ||
114
+
e.clientX < 220 ||
115
+
lockMovement
116
+
)return;
117
118
+
if(e.button !== 0){
119
+
contextMenu.visible = false;
120
return;
121
}
122
123
+
if(contextMenu.visible){
124
+
let submenus: ContextMenu[] = [];
125
+
contextMenu.items.map(x => x.menu ? submenus.push(x.menu): null);
126
127
+
submenus.map(x => {
128
+
if(!x.visible)return;
129
+
if(isPointInRect(canvas, e.clientX, e.clientY,
130
+
x.position[0], x.position[1],
131
+
x.size[0], x.size[1]
132
+
)){
133
+
let item = x.items.filter(x => x.hovered)[0];
134
+
if(item && item.clicked)item.clicked(e, canvas, { x: offset[0], y: offset[1], scale });
135
+
}
136
+
});
137
138
if(isPointInRect(canvas, e.clientX, e.clientY,
139
+
contextMenu.position[0], contextMenu.position[1],
140
+
contextMenu.size[0], contextMenu.size[1]
141
)){
142
+
let item = contextMenu.items.filter(x => x.hovered)[0];
143
if(item && item.clicked)item.clicked(e, canvas, { x: offset[0], y: offset[1], scale });
144
}
145
}
146
147
+
contextMenu.visible = false;
148
149
+
let clickedNode: any = null;
150
+
isDrawing = false;
151
152
+
let clickedInput: any = null;
153
+
let nodes = NodeManager.Instance.GetNodes();
154
155
+
if(nodes){
156
+
nodes.map(node => {
157
+
node.selected = false;
158
159
if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale },
160
e.clientX, e.clientY,
161
+
node.x, node.y, node.w, node.h
162
)){
163
+
node.outputs.map(( output, i ) => {
164
+
if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale },
165
+
e.clientX, e.clientY,
166
+
node.x + (node.w - 30),
167
+
node.y + 50 + (30 * i),
168
+
20, 20
169
+
)){
170
+
output.index = i;
171
172
+
drawingTo = [ node.x + (node.w - 30), node.y + 50 + (30 * i) ];
173
+
drawingFrom = output;
174
+
175
+
isDrawing = true;
176
+
return;
177
+
}
178
+
})
179
+
180
+
node.inputs.map(( input, i ) => {
181
+
if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale },
182
+
e.clientX, e.clientY,
183
+
node.x + 10,
184
+
node.y + 50 + (30 * i),
185
+
20, 20
186
+
)){
187
+
clickedInput = input;
188
+
}
189
+
})
190
191
+
clickedNode = node;
192
return;
193
}
194
})
195
}
196
197
+
if(clickedInput){
198
+
let partner = clickedInput.connections.pop();
199
+
if(!partner)return;
200
201
+
partner.connections = partner.connections.filter(( x: any ) => x !== clickedInput);
202
203
+
isDrawing = true;
204
+
isMouseDown = true;
205
+
206
+
drawingFrom = partner;
207
+
drawingTo = screenToWorldSpace(canvas, { x: offset[0], y: offset[1], scale }, e.clientX - 10 * scale, e.clientY - 10 * scale) as [ number, number ];;
208
209
+
return;
210
+
}
211
212
+
movingNode = clickedNode;
213
214
+
if(clickedNode){
215
+
clickedNode.selected = true;
216
+
setSelectedNode(clickedNode);
217
+
}
218
219
+
isMouseDown = true;
220
+
mouseStartPos = [ e.clientX, e.clientY ];
221
}
222
223
+
canvas.onmousemove = ( e ) => {
224
+
if(isMouseDown){
225
+
if(isDrawing){
226
+
drawingTo = screenToWorldSpace(canvas, { x: offset[0], y: offset[1], scale }, e.clientX - 10 * scale, e.clientY - 10 * scale) as [ number, number ];
227
+
} else if(movingNode){
228
+
movingNode.x = movingNode.x - (mouseStartPos[0] - e.clientX) / scale;
229
+
movingNode.y = movingNode.y - (mouseStartPos[1] - e.clientY) / scale;
230
231
+
mouseStartPos = [ e.clientX, e.clientY ];
232
+
NodeManager.Instance.UpdateConfig();
233
+
} else{
234
+
offsetTarget = [ offsetTarget[0] - (mouseStartPos[0] - e.clientX) / scale, offsetTarget[1] - (mouseStartPos[1] - e.clientY) / scale ];
235
+
mouseStartPos = [ e.clientX, e.clientY ];
236
237
+
screenMoved = true;
238
+
}
239
+
}
240
241
+
// TODO: Fix this shit lmao please
242
+
if(contextMenu.visible){
243
+
let submenus: ContextMenu[] = [];
244
+
contextMenu.items.map(x => x.menu ? submenus.push(x.menu): null);
245
246
+
submenus.map(x => {
247
+
if(!x.visible)return;
248
+
if(isPointInRect(canvas, e.clientX, e.clientY,
249
+
x.position[0], x.position[1],
250
+
x.size[0], x.size[1]
251
+
)){
252
+
x.items.map((y, i) => {
253
+
y.hovered = isPointInRect(canvas, e.clientX, e.clientY,
254
+
x.position[0], x.position[1] + 10 + 25 * i,
255
+
x.size[0], 25
256
+
)
257
+
});
258
+
}
259
+
});
260
261
if(isPointInRect(canvas, e.clientX, e.clientY,
262
+
contextMenu.position[0], contextMenu.position[1],
263
+
contextMenu.size[0], contextMenu.size[1]
264
)){
265
+
contextMenu.items.map((x, i) => {
266
+
x.hovered = isPointInRect(canvas, e.clientX, e.clientY,
267
+
contextMenu.position[0], contextMenu.position[1] + 10 + 25 * i,
268
+
contextMenu.size[0], 25
269
)
270
+
271
+
if(x.menu)x.menu.visible = x.hovered;
272
});
273
}
274
+
}
275
+
}
276
+
277
+
canvas.onmouseup = ( e ) => {
278
+
let nodes = NodeManager.Instance.GetNodes();
279
+
280
+
if(nodes){
281
+
nodes.map(node => {
282
+
node.inputs.map(( input, i ) => {
283
+
if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale },
284
+
e.clientX, e.clientY,
285
+
node.x + 10,
286
+
node.y + 50 + (30 * i),
287
+
20, 20
288
+
)){
289
+
if(isDrawing){
290
+
let fromType = NodeIOResolveAnyTypes(drawingFrom!);
291
+
let toType = NodeIOResolveAnyTypes(input);
292
293
+
if(
294
+
drawingFrom!.connections.indexOf(input) === -1 &&
295
+
(
296
+
toType === null ||
297
+
fromType === toType
298
+
)
299
+
){
300
+
drawingFrom!.connections.push(input);
301
+
input.connections.push(drawingFrom!);
302
303
+
NodeManager.Instance.UpdateConfig();
304
+
}
305
+
}
306
+
}
307
+
})
308
+
})
309
}
310
+
311
+
isDrawing = false;
312
+
isMouseDown = false;
313
}
314
315
+
requestAnimationFrame(update);
316
+
});
317
+
318
+
let update = () => {
319
+
if(stopRender)return;
320
+
321
+
scale = lerp(scale, targetScale, 0.25);
322
+
323
+
offset[0] = lerp(offset[0], offsetTarget[0], 0.5);
324
+
offset[1] = lerp(offset[1], offsetTarget[1], 0.5);
325
+
326
+
ctx.clearRect(canvas.width / -2, canvas.height / -2, canvas.width, canvas.height);
327
+
328
+
let nodes = NodeManager.Instance.GetNodes();
329
330
+
renderBackgroundGrid(canvas, ctx, { x: offset[0], y: offset[1], scale });
331
+
332
+
if(nodes)
333
+
renderNodes(canvas, ctx, nodes, { x: offset[0], y: offset[1], scale });
334
+
else
335
+
renderNullTab(canvas, ctx);
336
337
+
if(isDrawing)renderTempDrawing(canvas, ctx, drawingTo, drawingFrom!, { x: offset[0], y: offset[1], scale });
338
+
renderContextMenu(ctx, contextMenu);
339
340
+
requestAnimationFrame(update);
341
}
342
+
343
+
let isMouseDown = false;
344
+
let mouseStartPos = [ 0, 0 ];
345
346
let interval = setInterval(() => {
347
if(screenMoved){
···
358
359
return (
360
<>
361
+
<TabMenu />
362
<ControlBar node={selectedNode} lockMovement={( lock ) => lockMovement = lock} />
363
<canvas ref={canvas}/>
364
</>
+35
-300
src/ContextMenu/Canvas.tsx
+35
-300
src/ContextMenu/Canvas.tsx
···
1
-
import { invoke } from "@tauri-apps/api/core";
2
import { PositionInfo } from "../renderer";
3
-
import { Node, NodeType } from "../structs/node";
4
-
import { OSCMessage } from "../structs/OscMessage";
5
import { screenToWorldSpace } from "../utils/interections";
6
import { NodeManager } from "../Mangers/NodeManager";
7
import { ContextMenuItem } from "../structs/ContextMenu";
8
-
9
-
export let CanvasContextMenu: ContextMenuItem[] = [
10
-
{
11
-
text: "Add OSC Trigger Node",
12
-
clicked: ( e: MouseEvent, canvas: HTMLCanvasElement, position: PositionInfo ) => {
13
-
let pos = screenToWorldSpace(canvas, position, e.clientX, e.clientY);
14
-
15
-
let node: Node = {
16
-
name: 'OSC Trigger',
17
-
id: NodeManager.Instance.GetNewNodeId(),
18
-
x: pos[0],
19
-
y: pos[1],
20
-
w: 200,
21
-
h: 50,
22
-
inputs: [],
23
-
outputs: [],
24
-
selected: false,
25
-
statics: [
26
-
{
27
-
name: "OSC Trigger",
28
-
type: NodeType.OSCAddress,
29
-
value: null
30
-
},
31
-
{
32
-
name: "Parameter List",
33
-
type: NodeType.ParameterList,
34
-
value: []
35
-
}
36
-
],
37
-
onStaticsUpdate: ( node ) => {
38
-
let address = node.statics[0].value;
39
-
let parameters = node.statics[1].value;
40
-
41
-
(async () => {
42
-
if(address){
43
-
let addresses = await invoke<OSCMessage[]>('get_addresses');
44
-
let msgDat = addresses.find(x => x.address == address);
45
-
46
-
if(!msgDat)return;
47
-
48
-
parameters = msgDat.values.map(x => { return { type: x.key, desc: '' }});
49
-
node.statics[1].value = parameters;
50
-
}
51
-
52
-
node.outputs.map(output => {
53
-
output.connections.map(partner => {
54
-
partner.connections = partner.connections.filter(x => x != output);
55
-
})
56
-
})
57
-
node.outputs = [];
58
-
59
-
node.outputs.push({
60
-
name: 'Flow',
61
-
type: NodeType.Flow,
62
-
connections: [],
63
-
parent: node,
64
-
index: 0
65
-
})
66
-
67
-
parameters.forEach(( dat: any, indx: number ) => {
68
-
let type: NodeType | null = null;
69
-
70
-
switch(dat.type){
71
-
case 'Int':
72
-
type = NodeType.Int;
73
-
break;
74
-
case 'Float':
75
-
type = NodeType.Float;
76
-
break;
77
-
case 'String':
78
-
type = NodeType.String;
79
-
break;
80
-
case 'Boolean':
81
-
type = NodeType.Boolean;
82
-
break;
83
-
}
84
-
85
-
if(type){
86
-
node.outputs.push({
87
-
name: dat.desc === '' ? dat.type : dat.desc,
88
-
type: type,
89
-
connections: [],
90
-
parent: node,
91
-
index: indx + 1
92
-
})
93
-
}
94
-
});
95
-
96
-
node.h = 60 + (parameters.length + 1) * 30;
97
-
NodeManager.Instance.UpdateConfig();
98
-
})();
99
-
}
100
-
};
101
-
102
-
NodeManager.Instance.AddNode(node);
103
-
},
104
-
hovered: false
105
-
},
106
-
107
-
{
108
-
text: "Conditional",
109
-
menu: {
110
-
items: [
111
-
{
112
-
text: "If Equals",
113
-
hovered: false,
114
-
clicked: ( e: MouseEvent, canvas: HTMLCanvasElement, position: PositionInfo ) => {
115
-
let pos = screenToWorldSpace(canvas, position, e.clientX, e.clientY);
116
-
117
-
let node: Node = {
118
-
name: 'If Equals',
119
-
id: NodeManager.Instance.GetNewNodeId(),
120
-
x: pos[0],
121
-
y: pos[1],
122
-
w: 220,
123
-
h: 150,
124
-
inputs: [],
125
-
outputs: [],
126
-
selected: false,
127
-
statics: [],
128
-
onStaticsUpdate: ( _node ) => {}
129
-
};
130
131
-
node.inputs.push({
132
-
name: "Flow",
133
-
type: NodeType.Flow,
134
-
connections: [],
135
-
parent: node,
136
-
index: 0
137
-
});
138
139
-
node.inputs.push({
140
-
name: "Input 1",
141
-
type: NodeType.AnyTypeA,
142
-
connections: [],
143
-
parent: node,
144
-
index: 1
145
-
});
146
147
-
node.inputs.push({
148
-
name: "Input 2",
149
-
type: NodeType.AnyTypeA,
150
-
connections: [],
151
-
parent: node,
152
-
index: 2
153
-
});
154
-
155
-
156
-
node.outputs.push({
157
-
name: "Equal",
158
-
type: NodeType.Flow,
159
-
connections: [],
160
-
parent: node,
161
-
index: 0
162
-
});
163
-
164
-
node.outputs.push({
165
-
name: "Not Equal",
166
-
type: NodeType.Flow,
167
-
connections: [],
168
-
parent: node,
169
-
index: 1
170
-
});
171
-
172
-
NodeManager.Instance.AddNode(node);
173
}
174
-
},
175
-
],
176
-
position: [ 0, 0 ],
177
-
size: [ 0, 0 ],
178
-
visible: true
179
-
},
180
-
hovered: false
181
-
},
182
-
183
-
{
184
-
text: "Statics",
185
-
menu: {
186
-
items: [
187
-
{
188
-
text: "String",
189
-
hovered: false,
190
-
clicked: ( e: MouseEvent, canvas: HTMLCanvasElement, position: PositionInfo ) => {
191
-
let pos = screenToWorldSpace(canvas, position, e.clientX, e.clientY);
192
-
193
-
let node: Node = {
194
-
name: 'String',
195
-
id: NodeManager.Instance.GetNewNodeId(),
196
-
x: pos[0],
197
-
y: pos[1],
198
-
w: 200,
199
-
h: 85,
200
-
inputs: [],
201
-
outputs: [],
202
-
selected: false,
203
-
statics: [],
204
-
onStaticsUpdate: ( _node ) => {}
205
-
};
206
-
207
-
node.outputs.push({
208
-
name: "String",
209
-
type: NodeType.String,
210
-
connections: [],
211
-
parent: node,
212
-
index: 0
213
-
});
214
-
215
-
NodeManager.Instance.AddNode(node);
216
-
}
217
-
},
218
-
219
-
{
220
-
text: "Int",
221
-
hovered: false,
222
-
clicked: ( e: MouseEvent, canvas: HTMLCanvasElement, position: PositionInfo ) => {
223
-
let pos = screenToWorldSpace(canvas, position, e.clientX, e.clientY);
224
-
225
-
let node: Node = {
226
-
name: 'Int',
227
-
id: NodeManager.Instance.GetNewNodeId(),
228
-
x: pos[0],
229
-
y: pos[1],
230
-
w: 200,
231
-
h: 85,
232
-
inputs: [],
233
-
outputs: [],
234
-
selected: false,
235
-
statics: [],
236
-
onStaticsUpdate: ( _node ) => {}
237
-
};
238
-
239
-
node.outputs.push({
240
-
name: "Int",
241
-
type: NodeType.Int,
242
-
connections: [],
243
-
parent: node,
244
-
index: 0
245
-
});
246
-
247
-
NodeManager.Instance.AddNode(node);
248
-
}
249
-
},
250
-
],
251
-
position: [ 0, 0 ],
252
-
size: [ 0, 0 ],
253
-
visible: true
254
-
},
255
-
hovered: false
256
-
},
257
-
258
-
{
259
-
text: "OSC Actions",
260
-
menu: {
261
-
items: [
262
-
{
263
-
text: "Send Chatbox",
264
-
hovered: false,
265
-
clicked: ( e: MouseEvent, canvas: HTMLCanvasElement, position: PositionInfo ) => {
266
-
let pos = screenToWorldSpace(canvas, position, e.clientX, e.clientY);
267
-
268
-
let node: Node = {
269
-
name: 'Send Chatbox',
270
-
id: NodeManager.Instance.GetNewNodeId(),
271
-
x: pos[0],
272
-
y: pos[1],
273
-
w: 200,
274
-
h: 120,
275
-
inputs: [],
276
-
outputs: [],
277
-
selected: false,
278
-
statics: [],
279
-
onStaticsUpdate: ( _node ) => {}
280
-
};
281
-
282
-
node.inputs.push({
283
-
name: "Flow",
284
-
type: NodeType.Flow,
285
-
connections: [],
286
-
parent: node,
287
-
index: 0
288
-
});
289
-
290
-
node.inputs.push({
291
-
name: "Value",
292
-
type: NodeType.String,
293
-
connections: [],
294
-
parent: node,
295
-
index: 1
296
-
});
297
-
298
-
NodeManager.Instance.AddNode(node);
299
-
}
300
-
},
301
-
],
302
-
position: [ 0, 0 ],
303
-
size: [ 0, 0 ],
304
-
visible: true
305
-
},
306
-
hovered: false
307
-
},
308
-
]
···
1
import { PositionInfo } from "../renderer";
2
+
import { Node } from "../structs/node";
3
import { screenToWorldSpace } from "../utils/interections";
4
import { NodeManager } from "../Mangers/NodeManager";
5
import { ContextMenuItem } from "../structs/ContextMenu";
6
+
import { Nodes } from "../Nodes/Nodes";
7
8
+
export let CanvasContextMenu: ContextMenuItem[] = Nodes.map(( node ) => {
9
+
if(node.isSingle){
10
+
return {
11
+
text: node.name,
12
+
clicked: async ( e: MouseEvent, canvas: HTMLCanvasElement, position: PositionInfo ) => {
13
+
let pos = screenToWorldSpace(canvas, position, e.clientX, e.clientY);
14
+
let id = await NodeManager.Instance.GetNewNodeId();
15
16
+
NodeManager.Instance.AddNode(new Node(pos, node, id));
17
+
},
18
+
hovered: false
19
+
}
20
+
} else{
21
+
return {
22
+
text: node.name,
23
+
menu: {
24
+
items: node.items!.map(x => {
25
+
return {
26
+
text: x.name,
27
+
clicked: async ( e: MouseEvent, canvas: HTMLCanvasElement, position: PositionInfo ) => {
28
+
let pos = screenToWorldSpace(canvas, position, e.clientX, e.clientY);
29
+
let id = await NodeManager.Instance.GetNewNodeId();
30
31
+
NodeManager.Instance.AddNode(new Node(pos, x, id));
32
+
},
33
+
hovered: false
34
}
35
+
}),
36
+
position: [ 0, 0 ],
37
+
size: [ 0, 0 ],
38
+
visible: true
39
+
},
40
+
hovered: false
41
+
}
42
+
}
43
+
});
+2
-2
src/ControlBar.css
src/components/ControlBar.css
+2
-2
src/ControlBar.css
src/components/ControlBar.css
+6
-6
src/ControlBar.tsx
src/components/ControlBar.tsx
+6
-6
src/ControlBar.tsx
src/components/ControlBar.tsx
···
1
import './ControlBar.css';
2
3
import { Accessor, createSignal, For, Match, Show, Switch } from 'solid-js';
4
-
import { Node, NodeType } from './structs/node';
5
-
import { TextInput } from './components/TextInput';
6
import { invoke } from '@tauri-apps/api/core';
7
-
import { OSCMessage } from './structs/OscMessage';
8
-
import { ParameterList } from './components/ParameterList';
9
-
import { NodeManager } from './Mangers/NodeManager';
10
11
export interface ControlBarProps{
12
node: Accessor<Node | null>,
···
56
57
item.value = text;
58
node.onStaticsUpdate(node);
59
-
60
NodeManager.Instance.UpdateConfig();
61
}} />
62
</div>
···
1
import './ControlBar.css';
2
3
import { Accessor, createSignal, For, Match, Show, Switch } from 'solid-js';
4
+
import { Node, NodeType } from '../structs/node';
5
+
import { TextInput } from './TextInput';
6
import { invoke } from '@tauri-apps/api/core';
7
+
import { OSCMessage } from '../structs/OscMessage';
8
+
import { ParameterList } from './ParameterList';
9
+
import { NodeManager } from '../Mangers/NodeManager';
10
11
export interface ControlBarProps{
12
node: Accessor<Node | null>,
···
56
57
item.value = text;
58
node.onStaticsUpdate(node);
59
+
60
NodeManager.Instance.UpdateConfig();
61
}} />
62
</div>
+158
-47
src/Mangers/NodeManager.tsx
+158
-47
src/Mangers/NodeManager.tsx
···
1
import { invoke } from "@tauri-apps/api/core";
2
import { Node } from "../structs/node";
3
4
export class NodeManager{
5
public static Instance: NodeManager;
6
7
private _nodes: Node[] = [];
8
-
private _newestNodeId = 0;
9
private _needsSave = false;
10
11
constructor(){
···
15
// Save config every 1 second
16
if(this._needsSave)this._saveConfigToDisk();
17
}, 1_000);
18
}
19
20
public AddNode( node: Node ){
21
this._nodes.push(node);
22
this.UpdateConfig();
23
}
24
25
public RemoveNode( node: Node ){
26
this._nodes = this._nodes.filter(x => x !== node);
27
this.UpdateConfig();
28
}
29
30
-
public GetNodes(): Node[]{
31
-
return this._nodes;
32
}
33
34
-
public GetNewNodeId(){
35
-
return this._newestNodeId++; // TODO: really need a better solution than this, but it'll work for now
36
}
37
38
public UpdateConfig(){
39
this._needsSave = true;
40
}
41
42
-
private _loadFromConfig( config: string ){
43
let json = JSON.parse(config);
44
45
this._nodes = [];
46
47
// Populate nodes
48
-
for (let i = 0; i < json.length; i++) {
49
-
let node = json[i];
50
51
-
this._nodes.push({
52
-
name: node.name,
53
-
id: node.id,
54
-
x: node.x, y: node.y,
55
-
w: node.w, h: node.h,
56
-
inputs: [],
57
-
outputs: [],
58
-
selected: false,
59
-
statics: node.statics,
60
-
onStaticsUpdate: ( _node ) => {} // TODO: Make a seperate setup for node logic so we can load from that
61
-
})
62
-
63
-
this._newestNodeId = node.id
64
-
}
65
66
-
// Populate node outputs
67
-
for (let i = 0; i < json.length; i++) {
68
-
let configNode = json[i];
69
-
let node = this._nodes[i];
70
71
-
for (let j = 0; j < configNode.outputs.length; j++) {
72
-
let output = configNode.outputs[j];
73
-
node.outputs.push({
74
-
name: output.name,
75
-
type: output.type,
76
-
connections: [],
77
-
parent: node,
78
-
index: j
79
-
})
80
-
}
81
}
82
83
// Populate node inputs
84
-
for (let i = 0; i < json.length; i++) {
85
-
let configNode = json[i];
86
let outputParentNode = this._nodes[i];
87
88
for (let j = 0; j < configNode.outputs.length; j++) {
···
90
91
for (let k = 0; k < output.connections.length; k++) {
92
let input = output.connections[k];
93
-
let node = this._nodes[input.node];
94
95
let realInput = node.inputs.find(x => x.index === input.index);
96
let realOutput = outputParentNode.outputs[j];
97
98
if(realInput){
99
realInput.connections.push(realOutput);
100
} else{
101
-
node.inputs.push({
102
name: input.name,
103
type: input.type,
104
parent: node,
105
connections: [ realOutput ],
106
index: input.index
107
-
})
108
}
109
}
110
}
111
}
112
}
113
114
-
private _saveConfigToDisk(){
115
this._needsSave = false;
116
// Convert it into a structure we can actually save...
117
118
let nodesToSave = [];
119
···
140
nodesToSave.push({
141
name: node.name,
142
id: node.id,
143
-
x: node.x, y: node.y,
144
-
w: node.w, h: node.h,
145
-
statics: node.statics,
146
-
outputs: nodeOutputs
147
})
148
}
149
150
-
invoke('save_graph', { graph: JSON.stringify(nodesToSave) });
151
}
152
}
···
1
import { invoke } from "@tauri-apps/api/core";
2
import { Node } from "../structs/node";
3
+
import { Tab } from "../structs/Tab";
4
+
import { createSignal } from "solid-js";
5
+
import { listen } from "@tauri-apps/api/event";
6
+
import { getVersion } from "@tauri-apps/api/app";
7
+
import { NodesByID } from "../Nodes/Nodes";
8
+
9
+
export interface TabHashMap {
10
+
[details: string] : Tab;
11
+
}
12
13
export class NodeManager{
14
public static Instance: NodeManager;
15
16
+
private _selectedTab: string | null = null;
17
+
private _tabs: TabHashMap = {};
18
+
19
private _nodes: Node[] = [];
20
private _needsSave = false;
21
22
constructor(){
···
26
// Save config every 1 second
27
if(this._needsSave)this._saveConfigToDisk();
28
}, 1_000);
29
+
30
+
listen('load_new_tab', ( ev: any ) => {
31
+
this._loadFromConfig(ev.payload);
32
+
})
33
+
}
34
+
35
+
36
+
private _tabUpdateHook: ( tabs: TabHashMap ) => void = () => {};
37
+
private _tabChangeHook: () => void = () => {};
38
+
39
+
public async AddTab( name: string ){
40
+
let [ selected, setSelected ] = createSignal(false);
41
+
42
+
let tab: Tab = {
43
+
name: name,
44
+
id: await NodeManager.Instance.GetNewNodeId(),
45
+
nodes: [],
46
+
selected: selected,
47
+
setSelected: setSelected
48
+
};
49
+
50
+
this._tabs[tab.id] = tab;
51
+
52
+
this.SelectTab(tab.id);
53
+
this._tabUpdateHook(this._tabs);
54
+
}
55
+
56
+
public CloseTab( id: string ){ // TODO: Add confirmation to close tab
57
+
console.log(id === this._selectedTab);
58
+
if(this._selectedTab === id){
59
+
let tabs = Object.values(this._tabs);
60
+
61
+
if(tabs.length === 1){
62
+
this.SelectTab(null);
63
+
} else{
64
+
let tabToDelete = this._tabs[id];
65
+
66
+
let index = tabs.indexOf(tabToDelete);
67
+
let nextTab = tabs[index + 1];
68
+
69
+
if(nextTab)
70
+
this.SelectTab(nextTab.id);
71
+
else
72
+
this.SelectTab(tabs[0].id);
73
+
}
74
+
}
75
+
76
+
delete this._tabs[id];
77
+
this._tabUpdateHook(this._tabs);
78
+
}
79
+
80
+
public RenameTab( id: string, name: string ){
81
+
let tab = this._tabs[id];
82
+
if(!tab)return;
83
+
84
+
tab.name = name;
85
+
this._tabUpdateHook(this._tabs);
86
+
}
87
+
88
+
public SelectTab( id: string | null ){
89
+
if(this._selectedTab && this._tabs[this._selectedTab]){
90
+
let tab = this._tabs[this._selectedTab];
91
+
92
+
tab.setSelected(false);
93
+
tab.nodes = this._nodes;
94
+
}
95
+
96
+
this._selectedTab = id;
97
+
this._tabChangeHook();
98
+
99
+
if(this._selectedTab){
100
+
let tab = this._tabs[this._selectedTab];
101
+
if(!tab){
102
+
this._selectedTab = null;
103
+
return this._nodes = [];
104
+
}
105
+
106
+
tab.setSelected(true);
107
+
this._nodes = tab.nodes;
108
+
} else{
109
+
this._nodes = [];
110
+
}
111
+
}
112
+
113
+
public HookTabUpdate( cb: ( tabs: TabHashMap ) => void ){
114
+
this._tabUpdateHook = cb;
115
+
}
116
+
117
+
public HookTabChange( cb: () => void ){
118
+
this._tabChangeHook = cb;
119
}
120
121
+
122
public AddNode( node: Node ){
123
+
if(!this._selectedTab)return;
124
+
125
this._nodes.push(node);
126
this.UpdateConfig();
127
}
128
129
public RemoveNode( node: Node ){
130
+
if(!this._selectedTab)return;
131
+
132
this._nodes = this._nodes.filter(x => x !== node);
133
this.UpdateConfig();
134
}
135
136
+
public GetNodes(): Node[] | null{
137
+
if(this._selectedTab)
138
+
return this._nodes;
139
+
else
140
+
return null;
141
}
142
143
+
public async GetNewNodeId(){
144
+
let encoder = new TextEncoder();
145
+
let data = encoder.encode(Date.now().toString() + Math.random().toString());
146
+
let hash = await window.crypto.subtle.digest("SHA-256", data); // Probably should get a better ID implementation
147
+
148
+
return btoa(String.fromCharCode(...new Uint8Array(hash)));
149
}
150
151
+
152
public UpdateConfig(){
153
this._needsSave = true;
154
}
155
156
+
private async _loadFromConfig( config: string ){
157
let json = JSON.parse(config);
158
159
+
if(
160
+
!json.tab_name ||
161
+
!json.version ||
162
+
!json.graph
163
+
)return;
164
+
165
+
await this.AddTab(json.tab_name);
166
this._nodes = [];
167
168
+
let graph = json.graph;
169
+
170
// Populate nodes
171
+
for (let i = 0; i < graph.length; i++) {
172
+
let node = graph[i];
173
174
+
let nod = new Node(node.pos, NodesByID[node.typeId], node.id);
175
176
+
nod.statics = node.statics;
177
+
nod.onStaticsUpdate(nod);
178
179
+
this._nodes.push(nod);
180
}
181
182
// Populate node inputs
183
+
for (let i = 0; i < graph.length; i++) {
184
+
let configNode = graph[i];
185
let outputParentNode = this._nodes[i];
186
187
for (let j = 0; j < configNode.outputs.length; j++) {
···
189
190
for (let k = 0; k < output.connections.length; k++) {
191
let input = output.connections[k];
192
+
let node = this._nodes.find(x => x.id === input.node)!;
193
194
let realInput = node.inputs.find(x => x.index === input.index);
195
let realOutput = outputParentNode.outputs[j];
196
197
if(realInput){
198
realInput.connections.push(realOutput);
199
+
realOutput.connections.push(realInput);
200
} else{
201
+
let realInput = {
202
name: input.name,
203
type: input.type,
204
parent: node,
205
connections: [ realOutput ],
206
index: input.index
207
+
};
208
+
209
+
node.inputs.push(realInput);
210
+
realOutput.connections.push(realInput);
211
}
212
}
213
}
214
}
215
}
216
217
+
private async _saveConfigToDisk(){
218
this._needsSave = false;
219
// Convert it into a structure we can actually save...
220
+
221
+
if(!this._selectedTab)return;
222
+
let tab = this._tabs[this._selectedTab];
223
+
if(!tab)return;
224
225
let nodesToSave = [];
226
···
247
nodesToSave.push({
248
name: node.name,
249
id: node.id,
250
+
typeId: node.typeId,
251
+
pos: [ node.x, node.y ],
252
+
outputs: nodeOutputs,
253
+
statics: node.statics
254
})
255
}
256
257
+
invoke('save_graph', { tabName: tab.name, graph: JSON.stringify({
258
+
tab_name: tab.name,
259
+
version: await getVersion(),
260
+
graph: nodesToSave
261
+
}) });
262
}
263
}
+15
src/Nodes/Conditional.tsx
+15
src/Nodes/Conditional.tsx
···
···
1
+
import { NodeDefinition } from "./Nodes";
2
+
3
+
import { NodeConditionalIfEqual } from "./Conditional/IfEqual";
4
+
import { NodeConditionalIfTrue } from "./Conditional/IfTrue";
5
+
import { NodeConditionalIfFalse } from "./Conditional/IfFalse";
6
+
7
+
export let NodeConditional: NodeDefinition = {
8
+
isSingle: false,
9
+
name: 'Conditional',
10
+
items: [
11
+
NodeConditionalIfEqual,
12
+
NodeConditionalIfTrue,
13
+
NodeConditionalIfFalse
14
+
]
15
+
}
+30
src/Nodes/Conditional/IfEqual.tsx
+30
src/Nodes/Conditional/IfEqual.tsx
···
···
1
+
import { Node, NodeType } from "../../structs/node";
2
+
import { NodeDefinition } from "../Nodes";
3
+
4
+
export let NodeConditionalIfEqual: NodeDefinition = {
5
+
isSingle: true,
6
+
name: 'If Equal',
7
+
typeId: 'ifequal',
8
+
9
+
w: 220,
10
+
h: 150,
11
+
12
+
statics: [{
13
+
type: NodeType.Label,
14
+
name: 'If Equal',
15
+
value: null
16
+
}],
17
+
18
+
inputs: [
19
+
{ name: "Flow", type: NodeType.Flow },
20
+
{ name: "Input 1", type: NodeType.AnyTypeA },
21
+
{ name: "Input 2", type: NodeType.AnyTypeA },
22
+
],
23
+
24
+
outputs: [
25
+
{ name: "Equal", type: NodeType.Flow },
26
+
{ name: "Not Equal", type: NodeType.Flow },
27
+
],
28
+
29
+
onStaticsUpdate: ( _node: Node ) => {}
30
+
}
+29
src/Nodes/Conditional/IfFalse.tsx
+29
src/Nodes/Conditional/IfFalse.tsx
···
···
1
+
import { Node, NodeType } from "../../structs/node";
2
+
import { NodeDefinition } from "../Nodes";
3
+
4
+
export let NodeConditionalIfFalse: NodeDefinition = {
5
+
isSingle: true,
6
+
name: 'If False',
7
+
typeId: 'iffalse',
8
+
9
+
w: 220,
10
+
h: 150,
11
+
12
+
statics: [{
13
+
type: NodeType.Label,
14
+
name: 'If False',
15
+
value: null
16
+
}],
17
+
18
+
inputs: [
19
+
{ name: "Flow", type: NodeType.Flow },
20
+
{ name: "Input", type: NodeType.Boolean },
21
+
],
22
+
23
+
outputs: [
24
+
{ name: "Is False", type: NodeType.Flow },
25
+
{ name: "Not False", type: NodeType.Flow },
26
+
],
27
+
28
+
onStaticsUpdate: ( _node: Node ) => {}
29
+
}
+29
src/Nodes/Conditional/IfTrue.tsx
+29
src/Nodes/Conditional/IfTrue.tsx
···
···
1
+
import { Node, NodeType } from "../../structs/node";
2
+
import { NodeDefinition } from "../Nodes";
3
+
4
+
export let NodeConditionalIfTrue: NodeDefinition = {
5
+
isSingle: true,
6
+
name: 'If True',
7
+
typeId: 'iftrue',
8
+
9
+
w: 220,
10
+
h: 150,
11
+
12
+
statics: [{
13
+
type: NodeType.Label,
14
+
name: 'If True',
15
+
value: null
16
+
}],
17
+
18
+
inputs: [
19
+
{ name: "Flow", type: NodeType.Flow },
20
+
{ name: "Input", type: NodeType.Boolean },
21
+
],
22
+
23
+
outputs: [
24
+
{ name: "Is True", type: NodeType.Flow },
25
+
{ name: "Not True", type: NodeType.Flow },
26
+
],
27
+
28
+
onStaticsUpdate: (_node: Node) => { }
29
+
}
+46
src/Nodes/Nodes.tsx
+46
src/Nodes/Nodes.tsx
···
···
1
+
import { Node, NodeStatic, NodeType } from "../structs/node";
2
+
3
+
import { NodeConditional } from "./Conditional";
4
+
import { NodeOSCActions } from "./OSCActions";
5
+
import { NodeOSCTrigger } from "./OSCTrigger";
6
+
import { NodeStatics } from "./Statics";
7
+
8
+
export interface NodeDefinition{
9
+
isSingle: boolean,
10
+
name: string,
11
+
typeId?: string,
12
+
onStaticsUpdate?: ( node: Node ) => void,
13
+
// build?: ( pos: [ number, number ], onStaticsUpdate: ( node: Node ) => void ) => Promise<Node>,
14
+
w?: number,
15
+
h?: number,
16
+
statics?: NodeStatic[],
17
+
inputs?: { name: string, type: NodeType }[],
18
+
outputs?: { name: string, type: NodeType }[],
19
+
20
+
items?: NodeDefinition[]
21
+
}
22
+
23
+
export interface NodeDefinitionHashMap {
24
+
[details: string] : NodeDefinition;
25
+
}
26
+
27
+
export let Nodes: NodeDefinition[] = [
28
+
NodeOSCTrigger,
29
+
NodeConditional,
30
+
NodeStatics,
31
+
NodeOSCActions
32
+
]
33
+
34
+
export let NodesByID: NodeDefinitionHashMap = {}
35
+
36
+
Nodes.forEach(node => {
37
+
if(node.isSingle){
38
+
NodesByID[node.typeId!] = node;
39
+
} else{
40
+
node.items!.forEach(node => {
41
+
NodesByID[node.typeId!] = node;
42
+
})
43
+
}
44
+
})
45
+
46
+
console.log(NodesByID);
+10
src/Nodes/OSCActions.tsx
+10
src/Nodes/OSCActions.tsx
+24
src/Nodes/OSCActions/Send Chatbox.tsx
+24
src/Nodes/OSCActions/Send Chatbox.tsx
···
···
1
+
import { Node, NodeType } from "../../structs/node";
2
+
import { NodeDefinition } from "../Nodes";
3
+
4
+
export let NodeOSCActionsSendChatbox: NodeDefinition = {
5
+
isSingle: true,
6
+
name: 'Send Chatbox',
7
+
typeId: 'oscsendchatbox',
8
+
9
+
w: 200,
10
+
h: 120,
11
+
12
+
statics: [{
13
+
type: NodeType.Label,
14
+
name: 'Send Chatbox',
15
+
value: null
16
+
}],
17
+
18
+
inputs: [
19
+
{ name: "Flow", type: NodeType.Flow },
20
+
{ name: "Value", type: NodeType.String }
21
+
],
22
+
23
+
onStaticsUpdate: ( _node: Node ) => {}
24
+
}
+91
src/Nodes/OSCTrigger.tsx
+91
src/Nodes/OSCTrigger.tsx
···
···
1
+
import { invoke } from "@tauri-apps/api/core";
2
+
import { Node, NodeType } from "../structs/node";
3
+
import { OSCMessage } from "../structs/OscMessage";
4
+
import { NodeManager } from "../Mangers/NodeManager";
5
+
import { NodeDefinition } from "./Nodes";
6
+
7
+
export let NodeOSCTrigger: NodeDefinition = {
8
+
isSingle: true,
9
+
name: 'OSC Trigger',
10
+
typeId: 'osctrigger',
11
+
12
+
w: 200,
13
+
h: 50,
14
+
15
+
statics: [
16
+
{
17
+
name: "OSC Trigger",
18
+
type: NodeType.OSCAddress,
19
+
value: null
20
+
},
21
+
{
22
+
name: "Parameter List",
23
+
type: NodeType.ParameterList,
24
+
value: []
25
+
}
26
+
],
27
+
28
+
onStaticsUpdate: ( node: Node ) => {
29
+
let address = node.statics[0].value;
30
+
let parameters = node.statics[1].value;
31
+
32
+
(async () => {
33
+
if(address){
34
+
let addresses = await invoke<OSCMessage[]>('get_addresses');
35
+
let msgDat = addresses.find(x => x.address == address);
36
+
37
+
if(!msgDat)return;
38
+
39
+
parameters = msgDat.values.map(x => { return { type: x.key, desc: '' }});
40
+
node.statics[1].value = parameters;
41
+
}
42
+
43
+
node.outputs.map(output => {
44
+
output.connections.map(partner => {
45
+
partner.connections = partner.connections.filter(x => x != output);
46
+
})
47
+
})
48
+
node.outputs = [];
49
+
50
+
node.outputs.push({
51
+
name: 'Flow',
52
+
type: NodeType.Flow,
53
+
connections: [],
54
+
parent: node,
55
+
index: 0
56
+
})
57
+
58
+
parameters.forEach(( dat: any, indx: number ) => {
59
+
let type: NodeType | null = null;
60
+
61
+
switch(dat.type){
62
+
case 'Int':
63
+
type = NodeType.Int;
64
+
break;
65
+
case 'Float':
66
+
type = NodeType.Float;
67
+
break;
68
+
case 'String':
69
+
type = NodeType.String;
70
+
break;
71
+
case 'Boolean':
72
+
type = NodeType.Boolean;
73
+
break;
74
+
}
75
+
76
+
if(type){
77
+
node.outputs.push({
78
+
name: dat.desc === '' ? dat.type : dat.desc,
79
+
type: type,
80
+
connections: [],
81
+
parent: node,
82
+
index: indx + 1
83
+
})
84
+
}
85
+
});
86
+
87
+
node.h = 60 + (parameters.length + 1) * 30;
88
+
NodeManager.Instance.UpdateConfig();
89
+
})();
90
+
}
91
+
};
+13
src/Nodes/Statics.tsx
+13
src/Nodes/Statics.tsx
···
···
1
+
import { NodeDefinition } from "./Nodes";
2
+
3
+
import { NodeStaticsInt } from "./Statics/Int";
4
+
import { NodeStaticsString } from "./Statics/String";
5
+
6
+
export let NodeStatics: NodeDefinition = {
7
+
isSingle: false,
8
+
name: 'Statics',
9
+
items: [
10
+
NodeStaticsInt,
11
+
NodeStaticsString
12
+
]
13
+
}
+21
src/Nodes/Statics/Int.tsx
+21
src/Nodes/Statics/Int.tsx
···
···
1
+
import { Node, NodeType } from "../../structs/node";
2
+
import { NodeDefinition } from "../Nodes";
3
+
4
+
export let NodeStaticsInt: NodeDefinition = {
5
+
isSingle: true,
6
+
name: 'Int',
7
+
typeId: 'ifelse',
8
+
9
+
w: 200,
10
+
h: 85,
11
+
12
+
statics: [{
13
+
type: NodeType.Int,
14
+
name: 'Value',
15
+
value: null
16
+
}],
17
+
18
+
outputs: [{ name: "Int", type: NodeType.Int }],
19
+
20
+
onStaticsUpdate: ( _node: Node ) => {}
21
+
}
+21
src/Nodes/Statics/String.tsx
+21
src/Nodes/Statics/String.tsx
···
···
1
+
import { Node, NodeType } from "../../structs/node";
2
+
import { NodeDefinition } from "../Nodes";
3
+
4
+
export let NodeStaticsString: NodeDefinition = {
5
+
isSingle: true,
6
+
name: 'String',
7
+
typeId: 'staticstring',
8
+
9
+
w: 200,
10
+
h: 85,
11
+
12
+
statics: [{
13
+
type: NodeType.String,
14
+
name: 'Value',
15
+
value: null
16
+
}],
17
+
18
+
outputs: [{ name: "String", type: NodeType.String }],
19
+
20
+
onStaticsUpdate: ( _node: Node ) => {}
21
+
}
+98
src/components/TabMenu.css
+98
src/components/TabMenu.css
···
···
1
+
.tab-menu{
2
+
position: fixed;
3
+
top: 70px;
4
+
left: 20px;
5
+
width: 200px;
6
+
z-index: 100;
7
+
height: calc(100vh - 100px);
8
+
background: #272e44;
9
+
border-radius: 20px;
10
+
padding: 5px;
11
+
overflow-x: hidden;
12
+
overflow-y: auto;
13
+
}
14
+
15
+
.tab{
16
+
border-radius: 10px;
17
+
padding: 8px 10px;
18
+
background: #fff0;
19
+
margin: 5px;
20
+
font-size: 14px;
21
+
transition: 0.5s;
22
+
user-select: none;
23
+
-webkit-user-select: none;
24
+
display: flex;
25
+
color: #cfcfcf;
26
+
}
27
+
28
+
.tab:hover{
29
+
background: #fff1;
30
+
}
31
+
32
+
.tab-selected{
33
+
border-radius: 10px;
34
+
padding: 8px 10px;
35
+
margin: 5px;
36
+
font-size: 14px;
37
+
transition: 0.5s;
38
+
user-select: none;
39
+
-webkit-user-select: none;
40
+
display: flex;
41
+
color: #cfcfcf;
42
+
background: #fff4;
43
+
}
44
+
45
+
.tab-selected > .tab-close{
46
+
opacity: 1;
47
+
}
48
+
49
+
.tab:hover > .tab-close{
50
+
opacity: 1;
51
+
}
52
+
53
+
.tab-meta{
54
+
width: calc(100% - 40px);
55
+
transform: translateY(1px);
56
+
}
57
+
58
+
.tab-meta-input{
59
+
color: #cfcfcf;
60
+
font-size: 14px;
61
+
width: 120px;
62
+
background: none;
63
+
outline: none;
64
+
border: none;
65
+
transform: translateY(-1px);
66
+
}
67
+
68
+
.tab-close{
69
+
opacity: 0;
70
+
width: 20px;
71
+
height: 20px;
72
+
display: flex;
73
+
align-items: center;
74
+
justify-content: center;
75
+
border-radius: 5px;
76
+
}
77
+
78
+
.tab-close:hover{
79
+
background: #fff3;
80
+
}
81
+
82
+
.tab-icon{
83
+
width: 20px;
84
+
height: 20px;
85
+
display: flex;
86
+
align-items: center;
87
+
justify-content: left;
88
+
}
89
+
90
+
.tab-new-dropdown{
91
+
opacity: 0;
92
+
position: absolute;
93
+
width: 180px;
94
+
transform: translate(-10px, 35px);
95
+
border-radius: 10px;
96
+
background: #fff1;
97
+
transition: 0.1s;
98
+
}
+66
src/components/TabMenu.tsx
+66
src/components/TabMenu.tsx
···
···
1
+
import { createSignal, For, onMount } from 'solid-js';
2
+
import './TabMenu.css';
3
+
import { NodeManager } from '../Mangers/NodeManager';
4
+
import { Tab } from '../structs/Tab';
5
+
6
+
export let TabMenu = () => {
7
+
let [ tabImportOpen, setTabImportOpen ] = createSignal(false);
8
+
let [ tabs, setTabs ] = createSignal<Tab[]>([], { equals: false });
9
+
10
+
let closeTabImportMenu = () => {
11
+
window.removeEventListener('click', closeTabImportMenu);
12
+
setTabImportOpen(false);
13
+
}
14
+
15
+
onMount(() => {
16
+
NodeManager.Instance.HookTabUpdate(setTabs);
17
+
});
18
+
19
+
return (
20
+
<div class="tab-menu">
21
+
<For each={Object.values(tabs())}>
22
+
{
23
+
tab =>
24
+
<div class={ tab.selected() ? 'tab-selected ' : 'tab' }>
25
+
<div class="tab-icon" onClick={() => {
26
+
NodeManager.Instance.SelectTab(tab.id);
27
+
}}><img src="/assets/icons/pen-to-square-regular-full.svg" width="15" /></div>
28
+
<div class="tab-meta" onClick={() => {
29
+
NodeManager.Instance.SelectTab(tab.id);
30
+
}} onDblClick={( e ) => {
31
+
let input = <input class="tab-meta-input" value={ e.target.innerHTML } /> as HTMLInputElement;
32
+
33
+
e.target.innerHTML = '';
34
+
e.target.appendChild(input);
35
+
36
+
input.select();
37
+
input.onchange = () => {
38
+
NodeManager.Instance.RenameTab(tab.id, input.value);
39
+
e.target.innerHTML = input.value;
40
+
}
41
+
}}>{ tab.name }</div>
42
+
<div class="tab-close" onClick={() => {
43
+
NodeManager.Instance.CloseTab(tab.id);
44
+
}}><img src="/assets/icons/xmark-solid-full.svg" width="12" /></div>
45
+
</div>
46
+
}
47
+
</For>
48
+
49
+
<div class="tab" onClick={() => {
50
+
NodeManager.Instance.AddTab("Untitled");
51
+
}} onContextMenu={( e ) => {
52
+
e.preventDefault();
53
+
setTabImportOpen(true);
54
+
55
+
window.addEventListener('click', closeTabImportMenu);
56
+
}}>
57
+
<div class="tab-new-dropdown" style={{ opacity: tabImportOpen() ? 1 : 0 }}>
58
+
<div class="tab">Import from file</div>
59
+
<div class="tab">Import from URL</div>
60
+
</div>
61
+
<div class="tab-icon"><img src="/assets/icons/plus-solid-full.svg" width="15" /></div>
62
+
<div class="tab-meta">New Tab</div>
63
+
</div>
64
+
</div>
65
+
)
66
+
}
+1
-2
src/index.tsx
+1
-2
src/index.tsx
+20
src/renderer.ts
+20
src/renderer.ts
···
233
ctx.bezierCurveTo(midpoint[0], midpoint[1], end[0], end[1], toX, toY);
234
}
235
236
let drawRoundedRect = ( ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, radius: number ) => {
237
ctx.beginPath();
238
ctx.arc(x + radius, y + radius, radius, Math.PI, Math.PI * 1.5);
···
233
ctx.bezierCurveTo(midpoint[0], midpoint[1], end[0], end[1], toX, toY);
234
}
235
236
+
export let renderNullTab = (
237
+
canvas: HTMLCanvasElement,
238
+
ctx: CanvasRenderingContext2D,
239
+
) => {
240
+
ctx.fillStyle = '#fff';
241
+
242
+
ctx.font = '20px Arial';
243
+
ctx.textBaseline = 'middle';
244
+
ctx.textAlign = 'center';
245
+
246
+
let textX = lerp((canvas.width / -2) + 200, canvas.width / 2, 0.5);
247
+
let textY = lerp((canvas.height / -2) + 40, canvas.height / 2, 0.5);
248
+
249
+
ctx.font = '40px Arial';
250
+
ctx.fillText('Welcome to VRCMacros', textX, textY);
251
+
252
+
ctx.font = '20px Arial';
253
+
ctx.fillText('Create a new tab to get started!', textX, textY + 40);
254
+
}
255
+
256
let drawRoundedRect = ( ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, radius: number ) => {
257
ctx.beginPath();
258
ctx.arc(x + radius, y + radius, radius, Math.PI, Math.PI * 1.5);
+10
src/structs/Tab.ts
+10
src/structs/Tab.ts
+49
-12
src/structs/node.ts
+49
-12
src/structs/node.ts
···
1
-
export interface Node{
2
-
name: string,
3
-
id: number,
4
-
x: number,
5
-
y: number,
6
-
w: number,
7
-
h: number,
8
-
inputs: NodeIO[],
9
-
outputs: NodeIO[],
10
-
selected: boolean,
11
-
statics: NodeStatic[],
12
-
onStaticsUpdate: ( node: Node ) => void
13
}
14
15
export interface NodeIO{
···
1
+
import { NodeDefinition } from "../Nodes/Nodes";
2
+
3
+
export class Node{
4
+
name: string;
5
+
id: string;
6
+
typeId: string;
7
+
x: number;
8
+
y: number;
9
+
w: number;
10
+
h: number;
11
+
inputs: NodeIO[];
12
+
outputs: NodeIO[];
13
+
selected: boolean;
14
+
statics: NodeStatic[];
15
+
onStaticsUpdate: ( node: Node ) => void;
16
+
17
+
constructor( pos: [ number, number ], node: NodeDefinition, id: string ){
18
+
this.name = node.name;
19
+
this.id = id;
20
+
this.typeId = node.typeId!;
21
+
this.x = pos[0];
22
+
this.y = pos[1];
23
+
this.w = node.w!;
24
+
this.h = node.h!;
25
+
26
+
this.inputs = node.inputs ? node.inputs.map(( x, indx ) => {
27
+
return {
28
+
name: x.name,
29
+
type: x.type,
30
+
connections: [],
31
+
parent: this,
32
+
index: indx
33
+
}
34
+
}) : [];
35
+
36
+
this.outputs = node.outputs ? node.outputs.map(( x, indx ) => {
37
+
return {
38
+
name: x.name,
39
+
type: x.type,
40
+
connections: [],
41
+
parent: this,
42
+
index: indx
43
+
}
44
+
}) : [];
45
+
46
+
this.selected = false;
47
+
this.statics = node.statics!,
48
+
this.onStaticsUpdate = node.onStaticsUpdate!;
49
+
}
50
}
51
52
export interface NodeIO{
+1
-1
src/utils/interections.ts
+1
-1
src/utils/interections.ts