+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
3
use flate2::{ write::GzEncoder, Compression };
4
4
5
5
#[tauri::command]
6
-
pub fn save_graph( graph: String ) {
6
+
pub fn save_graph( tab_name: String, graph: String ) {
7
7
dbg!(&graph);
8
8
9
-
let path = dirs::config_dir().unwrap().join("VRCMacros").join("graph");
9
+
let path = dirs::config_dir().unwrap().join("VRCMacros").join(format!("{}.macro", tab_name));
10
10
11
11
let file = File::create(&path).unwrap();
12
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};
1
+
use std::{ fs::File, io::Read, sync::{ self, Mutex } };
2
2
3
-
use tauri::{ App, Emitter, Manager };
3
+
use flate2::read::GzDecoder;
4
+
use serde_json::Value;
5
+
use tauri::{ App, Emitter, Listener, Manager };
4
6
5
7
use crate::osc::{ self, OSCMessage };
6
8
7
9
pub fn setup( app: &mut App, addresses: &'static Mutex<Vec<OSCMessage>> ){
8
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
+
});
9
25
10
26
let ( sender, receiver ) = sync::mpsc::channel();
11
27
+210
-184
src/App.tsx
+210
-184
src/App.tsx
···
1
1
import { createSignal, onCleanup, onMount } from "solid-js";
2
2
import "./App.css";
3
-
import { renderBackgroundGrid, renderContextMenu, renderNodes, renderTempDrawing } from "./renderer";
3
+
import { renderBackgroundGrid, renderContextMenu, renderNodes, renderNullTab, renderTempDrawing } from "./renderer";
4
4
import { lerp } from "./utils/lerp";
5
5
import { Node, NodeIO, NodeIOResolveAnyTypes } from "./structs/node";
6
6
import { isPointInRect, isPointInRectApplyOffset, screenToWorldSpace } from "./utils/interections";
7
-
import { ControlBar } from "./ControlBar";
7
+
import { ControlBar } from "./components/ControlBar";
8
8
import { CanvasContextMenu } from "./ContextMenu/Canvas";
9
9
import { NodeContextMenu } from "./ContextMenu/Node";
10
10
import { ContextMenu } from "./structs/ContextMenu";
11
11
import { NodeManager } from "./Mangers/NodeManager";
12
+
import { TabMenu } from "./components/TabMenu";
12
13
13
14
let App = () => {
14
15
// TODO: Delete selected node when delete key is pressed
···
55
56
}
56
57
57
58
onMount(() => {
59
+
NodeManager.Instance.HookTabChange(() => setSelectedNode(null));
60
+
58
61
ctx = canvas.getContext('2d')!;
59
62
60
63
canvas.width = window.innerWidth;
···
77
80
screenMoved = true;
78
81
}
79
82
80
-
requestAnimationFrame(update);
81
-
});
83
+
canvas.oncontextmenu = ( e ) => {
84
+
e.preventDefault();
82
85
83
-
let update = () => {
84
-
if(stopRender)return;
86
+
let clickedNode: Node | null = null
87
+
let nodes = NodeManager.Instance.GetNodes();
85
88
86
-
scale = lerp(scale, targetScale, 0.25);
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
+
}
87
100
88
-
offset[0] = lerp(offset[0], offsetTarget[0], 0.5);
89
-
offset[1] = lerp(offset[1], offsetTarget[1], 0.5);
101
+
if(clickedNode){
102
+
contextMenu.items = NodeContextMenu(clickedNode);
103
+
} else{
104
+
contextMenu.items = CanvasContextMenu;
105
+
}
90
106
91
-
ctx.clearRect(canvas.width / -2, canvas.height / -2, canvas.width, canvas.height);
107
+
contextMenu.position = [ e.clientX - 10 - canvas.width / 2, e.clientY - 10 - canvas.height / 2 ];
108
+
contextMenu.visible = true;
109
+
}
92
110
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);
111
+
canvas.onmousedown = ( e ) => {
112
+
if(
113
+
e.clientY < 60 ||
114
+
e.clientX < 220 ||
115
+
lockMovement
116
+
)return;
97
117
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;
118
+
if(e.button !== 0){
119
+
contextMenu.visible = false;
114
120
return;
115
121
}
116
-
})
117
122
118
-
if(clickedNode){
119
-
contextMenu.items = NodeContextMenu(clickedNode);
120
-
} else{
121
-
contextMenu.items = CanvasContextMenu;
122
-
}
123
+
if(contextMenu.visible){
124
+
let submenus: ContextMenu[] = [];
125
+
contextMenu.items.map(x => x.menu ? submenus.push(x.menu): null);
123
126
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
-
}
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
+
});
135
137
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
138
if(isPointInRect(canvas, e.clientX, e.clientY,
143
-
x.position[0], x.position[1],
144
-
x.size[0], x.size[1]
139
+
contextMenu.position[0], contextMenu.position[1],
140
+
contextMenu.size[0], contextMenu.size[1]
145
141
)){
146
-
let item = x.items.filter(x => x.hovered)[0];
142
+
let item = contextMenu.items.filter(x => x.hovered)[0];
147
143
if(item && item.clicked)item.clicked(e, canvas, { x: offset[0], y: offset[1], scale });
148
144
}
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
145
}
158
-
}
159
146
160
-
contextMenu.visible = false;
147
+
contextMenu.visible = false;
161
148
162
-
let clickedNode: any = null;
163
-
isDrawing = false;
149
+
let clickedNode: any = null;
150
+
isDrawing = false;
164
151
165
-
let clickedInput: any = null;
152
+
let clickedInput: any = null;
153
+
let nodes = NodeManager.Instance.GetNodes();
166
154
167
-
NodeManager.Instance.GetNodes().map(node => {
168
-
node.selected = false;
155
+
if(nodes){
156
+
nodes.map(node => {
157
+
node.selected = false;
169
158
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
159
if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale },
176
160
e.clientX, e.clientY,
177
-
node.x + (node.w - 30),
178
-
node.y + 50 + (30 * i),
179
-
20, 20
161
+
node.x, node.y, node.w, node.h
180
162
)){
181
-
output.index = i;
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;
182
171
183
-
drawingTo = [ node.x + (node.w - 30), node.y + 50 + (30 * i) ];
184
-
drawingFrom = output;
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
+
})
185
190
186
-
isDrawing = true;
191
+
clickedNode = node;
187
192
return;
188
193
}
189
194
})
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
195
}
205
-
})
206
196
207
-
if(clickedInput){
208
-
let partner = clickedInput.connections.pop();
209
-
if(!partner)return;
197
+
if(clickedInput){
198
+
let partner = clickedInput.connections.pop();
199
+
if(!partner)return;
210
200
211
-
partner.connections = partner.connections.filter(( x: any ) => x !== clickedInput);
201
+
partner.connections = partner.connections.filter(( x: any ) => x !== clickedInput);
212
202
213
-
isDrawing = true;
214
-
isMouseDown = true;
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 ];;
215
208
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 ];;
209
+
return;
210
+
}
218
211
219
-
return;
220
-
}
212
+
movingNode = clickedNode;
221
213
222
-
movingNode = clickedNode;
214
+
if(clickedNode){
215
+
clickedNode.selected = true;
216
+
setSelectedNode(clickedNode);
217
+
}
223
218
224
-
if(clickedNode){
225
-
clickedNode.selected = true;
226
-
setSelectedNode(clickedNode);
219
+
isMouseDown = true;
220
+
mouseStartPos = [ e.clientX, e.clientY ];
227
221
}
228
222
229
-
isMouseDown = true;
230
-
mouseStartPos = [ e.clientX, e.clientY ];
231
-
}
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;
232
230
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;
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 ];
240
236
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 ];
237
+
screenMoved = true;
238
+
}
239
+
}
246
240
247
-
screenMoved = true;
248
-
}
249
-
}
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);
250
245
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);
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
+
});
255
260
256
-
submenus.map(x => {
257
-
if(!x.visible)return;
258
261
if(isPointInRect(canvas, e.clientX, e.clientY,
259
-
x.position[0], x.position[1],
260
-
x.size[0], x.size[1]
262
+
contextMenu.position[0], contextMenu.position[1],
263
+
contextMenu.size[0], contextMenu.size[1]
261
264
)){
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
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
266
269
)
270
+
271
+
if(x.menu)x.menu.visible = x.hovered;
267
272
});
268
273
}
269
-
});
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);
270
292
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
-
)
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!);
280
302
281
-
if(x.menu)x.menu.visible = x.hovered;
282
-
});
303
+
NodeManager.Instance.UpdateConfig();
304
+
}
305
+
}
306
+
}
307
+
})
308
+
})
283
309
}
310
+
311
+
isDrawing = false;
312
+
isMouseDown = false;
284
313
}
285
-
}
286
314
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);
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();
299
329
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!);
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);
309
336
310
-
NodeManager.Instance.UpdateConfig();
311
-
}
312
-
}
313
-
}
314
-
})
315
-
})
337
+
if(isDrawing)renderTempDrawing(canvas, ctx, drawingTo, drawingFrom!, { x: offset[0], y: offset[1], scale });
338
+
renderContextMenu(ctx, contextMenu);
316
339
317
-
isDrawing = false;
318
-
isMouseDown = false;
340
+
requestAnimationFrame(update);
319
341
}
342
+
343
+
let isMouseDown = false;
344
+
let mouseStartPos = [ 0, 0 ];
320
345
321
346
let interval = setInterval(() => {
322
347
if(screenMoved){
···
333
358
334
359
return (
335
360
<>
361
+
<TabMenu />
336
362
<ControlBar node={selectedNode} lockMovement={( lock ) => lockMovement = lock} />
337
363
<canvas ref={canvas}/>
338
364
</>
+35
-300
src/ContextMenu/Canvas.tsx
+35
-300
src/ContextMenu/Canvas.tsx
···
1
-
import { invoke } from "@tauri-apps/api/core";
2
1
import { PositionInfo } from "../renderer";
3
-
import { Node, NodeType } from "../structs/node";
4
-
import { OSCMessage } from "../structs/OscMessage";
2
+
import { Node } from "../structs/node";
5
3
import { screenToWorldSpace } from "../utils/interections";
6
4
import { NodeManager } from "../Mangers/NodeManager";
7
5
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
-
};
6
+
import { Nodes } from "../Nodes/Nodes";
130
7
131
-
node.inputs.push({
132
-
name: "Flow",
133
-
type: NodeType.Flow,
134
-
connections: [],
135
-
parent: node,
136
-
index: 0
137
-
});
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();
138
15
139
-
node.inputs.push({
140
-
name: "Input 1",
141
-
type: NodeType.AnyTypeA,
142
-
connections: [],
143
-
parent: node,
144
-
index: 1
145
-
});
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();
146
30
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);
31
+
NodeManager.Instance.AddNode(new Node(pos, x, id));
32
+
},
33
+
hovered: false
173
34
}
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
-
]
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
1
import './ControlBar.css';
2
2
3
3
import { Accessor, createSignal, For, Match, Show, Switch } from 'solid-js';
4
-
import { Node, NodeType } from './structs/node';
5
-
import { TextInput } from './components/TextInput';
4
+
import { Node, NodeType } from '../structs/node';
5
+
import { TextInput } from './TextInput';
6
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';
7
+
import { OSCMessage } from '../structs/OscMessage';
8
+
import { ParameterList } from './ParameterList';
9
+
import { NodeManager } from '../Mangers/NodeManager';
10
10
11
11
export interface ControlBarProps{
12
12
node: Accessor<Node | null>,
···
56
56
57
57
item.value = text;
58
58
node.onStaticsUpdate(node);
59
-
59
+
60
60
NodeManager.Instance.UpdateConfig();
61
61
}} />
62
62
</div>
+158
-47
src/Mangers/NodeManager.tsx
+158
-47
src/Mangers/NodeManager.tsx
···
1
1
import { invoke } from "@tauri-apps/api/core";
2
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
+
}
3
12
4
13
export class NodeManager{
5
14
public static Instance: NodeManager;
6
15
16
+
private _selectedTab: string | null = null;
17
+
private _tabs: TabHashMap = {};
18
+
7
19
private _nodes: Node[] = [];
8
-
private _newestNodeId = 0;
9
20
private _needsSave = false;
10
21
11
22
constructor(){
···
15
26
// Save config every 1 second
16
27
if(this._needsSave)this._saveConfigToDisk();
17
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;
18
119
}
19
120
121
+
20
122
public AddNode( node: Node ){
123
+
if(!this._selectedTab)return;
124
+
21
125
this._nodes.push(node);
22
126
this.UpdateConfig();
23
127
}
24
128
25
129
public RemoveNode( node: Node ){
130
+
if(!this._selectedTab)return;
131
+
26
132
this._nodes = this._nodes.filter(x => x !== node);
27
133
this.UpdateConfig();
28
134
}
29
135
30
-
public GetNodes(): Node[]{
31
-
return this._nodes;
136
+
public GetNodes(): Node[] | null{
137
+
if(this._selectedTab)
138
+
return this._nodes;
139
+
else
140
+
return null;
32
141
}
33
142
34
-
public GetNewNodeId(){
35
-
return this._newestNodeId++; // TODO: really need a better solution than this, but it'll work for now
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)));
36
149
}
37
150
151
+
38
152
public UpdateConfig(){
39
153
this._needsSave = true;
40
154
}
41
155
42
-
private _loadFromConfig( config: string ){
156
+
private async _loadFromConfig( config: string ){
43
157
let json = JSON.parse(config);
44
158
159
+
if(
160
+
!json.tab_name ||
161
+
!json.version ||
162
+
!json.graph
163
+
)return;
164
+
165
+
await this.AddTab(json.tab_name);
45
166
this._nodes = [];
46
167
168
+
let graph = json.graph;
169
+
47
170
// Populate nodes
48
-
for (let i = 0; i < json.length; i++) {
49
-
let node = json[i];
171
+
for (let i = 0; i < graph.length; i++) {
172
+
let node = graph[i];
50
173
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
-
}
174
+
let nod = new Node(node.pos, NodesByID[node.typeId], node.id);
65
175
66
-
// Populate node outputs
67
-
for (let i = 0; i < json.length; i++) {
68
-
let configNode = json[i];
69
-
let node = this._nodes[i];
176
+
nod.statics = node.statics;
177
+
nod.onStaticsUpdate(nod);
70
178
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
-
}
179
+
this._nodes.push(nod);
81
180
}
82
181
83
182
// Populate node inputs
84
-
for (let i = 0; i < json.length; i++) {
85
-
let configNode = json[i];
183
+
for (let i = 0; i < graph.length; i++) {
184
+
let configNode = graph[i];
86
185
let outputParentNode = this._nodes[i];
87
186
88
187
for (let j = 0; j < configNode.outputs.length; j++) {
···
90
189
91
190
for (let k = 0; k < output.connections.length; k++) {
92
191
let input = output.connections[k];
93
-
let node = this._nodes[input.node];
192
+
let node = this._nodes.find(x => x.id === input.node)!;
94
193
95
194
let realInput = node.inputs.find(x => x.index === input.index);
96
195
let realOutput = outputParentNode.outputs[j];
97
196
98
197
if(realInput){
99
198
realInput.connections.push(realOutput);
199
+
realOutput.connections.push(realInput);
100
200
} else{
101
-
node.inputs.push({
201
+
let realInput = {
102
202
name: input.name,
103
203
type: input.type,
104
204
parent: node,
105
205
connections: [ realOutput ],
106
206
index: input.index
107
-
})
207
+
};
208
+
209
+
node.inputs.push(realInput);
210
+
realOutput.connections.push(realInput);
108
211
}
109
212
}
110
213
}
111
214
}
112
215
}
113
216
114
-
private _saveConfigToDisk(){
217
+
private async _saveConfigToDisk(){
115
218
this._needsSave = false;
116
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;
117
224
118
225
let nodesToSave = [];
119
226
···
140
247
nodesToSave.push({
141
248
name: node.name,
142
249
id: node.id,
143
-
x: node.x, y: node.y,
144
-
w: node.w, h: node.h,
145
-
statics: node.statics,
146
-
outputs: nodeOutputs
250
+
typeId: node.typeId,
251
+
pos: [ node.x, node.y ],
252
+
outputs: nodeOutputs,
253
+
statics: node.statics
147
254
})
148
255
}
149
256
150
-
invoke('save_graph', { graph: JSON.stringify(nodesToSave) });
257
+
invoke('save_graph', { tabName: tab.name, graph: JSON.stringify({
258
+
tab_name: tab.name,
259
+
version: await getVersion(),
260
+
graph: nodesToSave
261
+
}) });
151
262
}
152
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
233
ctx.bezierCurveTo(midpoint[0], midpoint[1], end[0], end[1], toX, toY);
234
234
}
235
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
+
236
256
let drawRoundedRect = ( ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, radius: number ) => {
237
257
ctx.beginPath();
238
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
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
+
}
13
50
}
14
51
15
52
export interface NodeIO{
+1
-1
src/utils/interections.ts
+1
-1
src/utils/interections.ts
···
1
1
import { PositionInfo } from "../renderer";
2
2
3
-
export let screenToWorldSpace = ( canvas: HTMLCanvasElement, position: PositionInfo, pointX: number, pointY: number ) => {
3
+
export let screenToWorldSpace = ( canvas: HTMLCanvasElement, position: PositionInfo, pointX: number, pointY: number ): [ number, number ] => {
4
4
let startX = canvas.width / -2;
5
5
let startY = canvas.height / -2;
6
6