+1
public/assets/icons/circle-info-solid-full.svg
+1
public/assets/icons/circle-info-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="M320 576C461.4 576 576 461.4 576 320C576 178.6 461.4 64 320 64C178.6 64 64 178.6 64 320C64 461.4 178.6 576 320 576zM288 224C288 206.3 302.3 192 320 192C337.7 192 352 206.3 352 224C352 241.7 337.7 256 320 256C302.3 256 288 241.7 288 224zM280 288L328 288C341.3 288 352 298.7 352 312L352 400L360 400C373.3 400 384 410.7 384 424C384 437.3 373.3 448 360 448L280 448C266.7 448 256 437.3 256 424C256 410.7 266.7 400 280 400L304 400L304 336L280 336C266.7 336 256 325.3 256 312C256 298.7 266.7 288 280 288z"/></svg>
+18
-8
src/App.tsx
+18
-8
src/App.tsx
···
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";
···
13
import { ConfirmationPopup } from "./components/ConfirmationPopup";
14
15
let App = () => {
16
-
// TODO: Delete selected node when delete key is pressed
17
// TODO: Keybind system
18
// TODO: Add undo / redo -ing
19
20
let [ selectedNode, setSelectedNode ] = createSignal<Node | null>(null);
21
···
123
return;
124
}
125
126
if(contextMenu.visible){
127
let submenus: ContextMenu[] = [];
128
contextMenu.items.map(x => x.menu ? submenus.push(x.menu): null);
···
161
162
if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale },
163
e.clientX, e.clientY,
164
-
node.x, node.y, node.w, node.h
165
)){
166
node.outputs.map(( output, i ) => {
167
if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale },
168
e.clientX, e.clientY,
169
-
node.x + (node.w - 30),
170
node.y + 50 + (30 * i),
171
20, 20
172
)){
173
output.index = i;
174
175
-
drawingTo = [ node.x + (node.w - 30), node.y + 50 + (30 * i) ];
176
drawingFrom = output;
177
178
isDrawing = true;
···
183
node.inputs.map(( input, i ) => {
184
if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale },
185
e.clientX, e.clientY,
186
-
node.x + 10,
187
node.y + 50 + (30 * i),
188
20, 20
189
)){
···
285
node.inputs.map(( input, i ) => {
286
if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale },
287
e.clientX, e.clientY,
288
-
node.x + 10,
289
node.y + 50 + (30 * i),
290
20, 20
291
)){
···
297
drawingFrom!.connections.indexOf(input) === -1 &&
298
(
299
toType === null ||
300
-
fromType === toType
301
)
302
){
303
drawingFrom!.connections.push(input);
···
2
import "./App.css";
3
import { renderBackgroundGrid, renderContextMenu, renderNodes, renderNullTab, renderTempDrawing } from "./renderer";
4
import { lerp } from "./utils/lerp";
5
+
import { Node, NodeIO, NodeIOCanCast, NodeIOResolveAnyTypes } from "./structs/node";
6
import { isPointInRect, isPointInRectApplyOffset, screenToWorldSpace } from "./utils/interections";
7
import { ControlBar } from "./components/ControlBar";
8
import { CanvasContextMenu } from "./ContextMenu/Canvas";
···
13
import { ConfirmationPopup } from "./components/ConfirmationPopup";
14
15
let App = () => {
16
// TODO: Keybind system
17
+
// TODO: Delete selected node when delete key is pressed
18
+
// TODO: Copy / paste
19
// TODO: Add undo / redo -ing
20
+
// TODO: Multi-select
21
22
let [ selectedNode, setSelectedNode ] = createSignal<Node | null>(null);
23
···
125
return;
126
}
127
128
+
if(e.shiftKey){
129
+
// TODO: Multi-select
130
+
return;
131
+
}
132
+
133
if(contextMenu.visible){
134
let submenus: ContextMenu[] = [];
135
contextMenu.items.map(x => x.menu ? submenus.push(x.menu): null);
···
168
169
if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale },
170
e.clientX, e.clientY,
171
+
node.x - 20, node.y, node.w + 40, node.h
172
)){
173
node.outputs.map(( output, i ) => {
174
if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale },
175
e.clientX, e.clientY,
176
+
node.x + (node.w - 10),
177
node.y + 50 + (30 * i),
178
20, 20
179
)){
180
output.index = i;
181
182
+
drawingTo = [
183
+
node.x + (node.w - 10),
184
+
node.y + 50 + (30 * i)
185
+
];
186
drawingFrom = output;
187
188
isDrawing = true;
···
193
node.inputs.map(( input, i ) => {
194
if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale },
195
e.clientX, e.clientY,
196
+
node.x - 10,
197
node.y + 50 + (30 * i),
198
20, 20
199
)){
···
295
node.inputs.map(( input, i ) => {
296
if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale },
297
e.clientX, e.clientY,
298
+
node.x - 10,
299
node.y + 50 + (30 * i),
300
20, 20
301
)){
···
307
drawingFrom!.connections.indexOf(input) === -1 &&
308
(
309
toType === null ||
310
+
NodeIOCanCast(fromType, toType)
311
)
312
){
313
drawingFrom!.connections.push(input);
+2
-2
src/Nodes/OSCTrigger.tsx
+2
-2
src/Nodes/OSCTrigger.tsx
+3
src/components/TabMenu.css
+3
src/components/TabMenu.css
+4
-1
src/components/TabMenu.tsx
+4
-1
src/components/TabMenu.tsx
···
4
import { NodeManager } from '../Mangers/NodeManager';
5
import { Tab } from '../structs/Tab';
6
import { SettingsMenu } from './SettingsMenu';
7
8
export let TabMenu = () => {
9
let [ tabImportOpen, setTabImportOpen ] = createSignal(false);
···
25
<Show when={settingsOpen()}>
26
<SettingsMenu close={() => setSettingsOpen(false)} />
27
</Show>
28
-
29
<div class="tab-menu">
30
<div class="tab-container">
31
<For each={Object.values(tabs())}>
···
86
87
<div class="tab-icon-bar">
88
<img src="/assets/icons/gear-solid-full.svg" width="25" onClick={() => setSettingsOpen(true)} />
89
</div>
90
</div>
91
</>
···
4
import { NodeManager } from '../Mangers/NodeManager';
5
import { Tab } from '../structs/Tab';
6
import { SettingsMenu } from './SettingsMenu';
7
+
import { openUrl } from '@tauri-apps/plugin-opener';
8
9
export let TabMenu = () => {
10
let [ tabImportOpen, setTabImportOpen ] = createSignal(false);
···
26
<Show when={settingsOpen()}>
27
<SettingsMenu close={() => setSettingsOpen(false)} />
28
</Show>
29
+
30
<div class="tab-menu">
31
<div class="tab-container">
32
<For each={Object.values(tabs())}>
···
87
88
<div class="tab-icon-bar">
89
<img src="/assets/icons/gear-solid-full.svg" width="25" onClick={() => setSettingsOpen(true)} />
90
+
<div style={{ width: 'calc(100% - 50px)' }}></div>
91
+
<img src="/assets/icons/circle-info-solid-full.svg" width="25" onClick={() => openUrl('https://github.com/phaze-the-dumb/VRCMacros/wiki')} />
92
</div>
93
</div>
94
</>
+58
-35
src/renderer.ts
+58
-35
src/renderer.ts
···
63
let nodeX = Math.round(node.x / 10) * 10;
64
let nodeY = Math.round(node.y / 10) * 10;
65
66
-
ctx.fillStyle = '#1f2129';
67
-
ctx.strokeStyle = node.selected ? '#004696ff' : '#fff5';
68
ctx.lineWidth = 5 * position.scale;
69
70
// Draw Node Box
···
75
node.h * position.scale,
76
10 * position.scale);
77
78
ctx.stroke();
79
ctx.fill();
80
81
// Draw Node Name
82
ctx.fillStyle = '#fff';
83
-
ctx.font = (25 * position.scale) + 'px Comic Mono';
84
ctx.textAlign = 'center';
85
86
ctx.fillText(node.name,
···
89
);
90
91
// Draw Inputs
92
-
ctx.font = (15 * position.scale) + 'px Comic Mono';
93
ctx.textAlign = 'left';
94
95
node.inputs.map(( input, i ) => {
96
ctx.fillStyle = NodeIOLinkColours(input);
97
-
ctx.fillRect(
98
-
(nodeX + 10 + startX + position.x) * position.scale,
99
-
(nodeY + 50 + (30 * i) + startY + position.y) * position.scale,
100
-
20 * position.scale, 20 * position.scale
101
-
)
102
103
ctx.fillText(input.name,
104
-
(nodeX + 35 + startX + position.x) * position.scale,
105
(nodeY + 53 + (30 * i) + startY + position.y) * position.scale,
106
)
107
})
···
111
112
node.outputs.map(( output, i ) => {
113
ctx.fillStyle = NodeIOLinkColours(output);
114
-
ctx.fillRect(
115
-
(nodeX + (node.w - 30) + startX + position.x) * position.scale,
116
-
(nodeY + 50 + (30 * i) + startY + position.y) * position.scale,
117
-
20 * position.scale, 20 * position.scale
118
-
)
119
120
ctx.fillText(output.name,
121
-
(nodeX + (node.w - 35) + startX + position.x) * position.scale,
122
(nodeY + 53 + (30 * i) + startY + position.y) * position.scale,
123
)
124
})
···
131
node.outputs.map(( output, i ) => {
132
output.connections.map(partner => {
133
ctx.strokeStyle = NodeIOLinkColours(output);
134
drawCurve(ctx,
135
-
(nodeX + (node.w - 30) + 10 + startX + position.x) * position.scale,
136
(nodeY + 50 + (30 * i) + 10 + startY + position.y) * position.scale,
137
-
((Math.round(partner.parent.x / 10) * 10) + 20 + startX + position.x) * position.scale,
138
((Math.round(partner.parent.y / 10) * 10) + 60 + (30 * partner.index) + startY + position.y) * position.scale,
139
);
140
ctx.stroke();
···
148
contextMenu: ContextMenu
149
) => {
150
if(contextMenu.visible){
151
-
ctx.font = '20px Arial';
152
ctx.textBaseline = 'top';
153
ctx.textAlign = 'left';
154
···
192
let startX = canvas.width / -2;
193
let startY = canvas.height / -2;
194
195
-
ctx.fillStyle = '#f00';
196
197
-
ctx.fillRect(
198
-
(drawingTo[0] + 10 + startX + position.x) * position.scale,
199
-
(drawingTo[1] + 10 + startY + position.y) * position.scale,
200
-
10, 10
201
-
);
202
203
-
ctx.fillRect(
204
-
(drawingFrom.parent.x + (drawingFrom.parent.w - 30) + 10 + startX + position.x) * position.scale,
205
-
(drawingFrom.parent.y + 50 + (30 * drawingFrom.index) + 10 + startY + position.y) * position.scale,
206
-
10, 10
207
-
);
208
209
ctx.strokeStyle = NodeIOLinkColours(drawingFrom);
210
drawCurve(ctx,
211
-
(drawingFrom.parent.x + (drawingFrom.parent.w - 30) + 10 + startX + position.x) * position.scale,
212
-
(drawingFrom.parent.y + 50 + (30 * drawingFrom.index) + 10 + startY + position.y) * position.scale,
213
(drawingTo[0] + 10 + startX + position.x) * position.scale,
214
(drawingTo[1] + 10 + startY + position.y) * position.scale,
215
);
···
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
···
63
let nodeX = Math.round(node.x / 10) * 10;
64
let nodeY = Math.round(node.y / 10) * 10;
65
66
+
ctx.fillStyle = '#343742ff';
67
+
ctx.strokeStyle = node.selected ? '#004696ff' : '#fff0';
68
ctx.lineWidth = 5 * position.scale;
69
70
// Draw Node Box
···
75
node.h * position.scale,
76
10 * position.scale);
77
78
+
ctx.shadowColor = '#0005';
79
+
ctx.shadowBlur = 10;
80
+
81
ctx.stroke();
82
ctx.fill();
83
+
84
+
ctx.shadowBlur = 0;
85
86
// Draw Node Name
87
ctx.fillStyle = '#fff';
88
+
ctx.font = (25 * position.scale) + 'px Rubik';
89
ctx.textAlign = 'center';
90
91
ctx.fillText(node.name,
···
94
);
95
96
// Draw Inputs
97
+
ctx.font = (15 * position.scale) + 'px Rubik';
98
ctx.textAlign = 'left';
99
100
node.inputs.map(( input, i ) => {
101
ctx.fillStyle = NodeIOLinkColours(input);
102
+
103
+
ctx.beginPath();
104
+
ctx.arc(
105
+
(nodeX - 10 + startX + 10 + position.x) * position.scale,
106
+
(nodeY + 50 + (30 * i) + startY + 10 + position.y) * position.scale,
107
+
7 * position.scale,
108
+
0,
109
+
Math.PI * 2,
110
+
);
111
+
ctx.fill();
112
113
ctx.fillText(input.name,
114
+
(nodeX + 15 + startX + position.x) * position.scale,
115
(nodeY + 53 + (30 * i) + startY + position.y) * position.scale,
116
)
117
})
···
121
122
node.outputs.map(( output, i ) => {
123
ctx.fillStyle = NodeIOLinkColours(output);
124
+
125
+
ctx.beginPath();
126
+
ctx.arc(
127
+
(nodeX + (node.w - 10) + startX + 10 + position.x) * position.scale,
128
+
(nodeY + 50 + (30 * i) + startY + 10 + position.y) * position.scale,
129
+
7 * position.scale,
130
+
0,
131
+
Math.PI * 2,
132
+
);
133
+
ctx.fill();
134
135
ctx.fillText(output.name,
136
+
(nodeX + (node.w - 15) + startX + position.x) * position.scale,
137
(nodeY + 53 + (30 * i) + startY + position.y) * position.scale,
138
)
139
})
···
146
node.outputs.map(( output, i ) => {
147
output.connections.map(partner => {
148
ctx.strokeStyle = NodeIOLinkColours(output);
149
+
ctx.lineWidth = 3 * position.scale;
150
+
151
drawCurve(ctx,
152
+
(nodeX + (node.w - 10) + 10 + startX + position.x) * position.scale,
153
(nodeY + 50 + (30 * i) + 10 + startY + position.y) * position.scale,
154
+
((Math.round(partner.parent.x / 10) * 10) + startX + position.x) * position.scale,
155
((Math.round(partner.parent.y / 10) * 10) + 60 + (30 * partner.index) + startY + position.y) * position.scale,
156
);
157
ctx.stroke();
···
165
contextMenu: ContextMenu
166
) => {
167
if(contextMenu.visible){
168
+
ctx.font = '20px Rubik';
169
ctx.textBaseline = 'top';
170
ctx.textAlign = 'left';
171
···
209
let startX = canvas.width / -2;
210
let startY = canvas.height / -2;
211
212
+
// DEBUG STUFF
213
+
// ctx.fillStyle = '#f00';
214
215
+
// ctx.fillRect(
216
+
// (drawingTo[0] + 10 + startX + position.x) * position.scale,
217
+
// (drawingTo[1] + 10 + startY + position.y) * position.scale,
218
+
// 10, 10
219
+
// );
220
221
+
// ctx.fillRect(
222
+
// (drawingFrom.parent.x + (drawingFrom.parent.w - 10) + 10 + startX + position.x) * position.scale,
223
+
// (drawingFrom.parent.y + 50 + (30 * drawingFrom.index) + 10 + startY + position.y) * position.scale,
224
+
// 10, 10
225
+
// );
226
227
ctx.strokeStyle = NodeIOLinkColours(drawingFrom);
228
+
ctx.lineWidth = 3 * position.scale;
229
+
230
+
let nodeX = Math.round(drawingFrom.parent.x / 10) * 10;
231
+
let nodeY = Math.round(drawingFrom.parent.y / 10) * 10;
232
+
233
drawCurve(ctx,
234
+
(nodeX + (drawingFrom.parent.w - 10) + 10 + startX + position.x) * position.scale,
235
+
(nodeY + 50 + (30 * drawingFrom.index) + 10 + startY + position.y) * position.scale,
236
(drawingTo[0] + 10 + startX + position.x) * position.scale,
237
(drawingTo[1] + 10 + startY + position.y) * position.scale,
238
);
···
262
) => {
263
ctx.fillStyle = '#fff';
264
265
+
ctx.font = '20px Rubik';
266
ctx.textBaseline = 'middle';
267
ctx.textAlign = 'center';
268
269
let textX = lerp((canvas.width / -2) + 200, canvas.width / 2, 0.5);
270
let textY = lerp((canvas.height / -2) + 40, canvas.height / 2, 0.5);
271
272
+
ctx.font = '40px Rubik';
273
ctx.fillText('Welcome to VRCMacros', textX, textY);
274
275
+
ctx.font = '20px Rubik';
276
ctx.fillText('Create a new tab to get started!', textX, textY + 40);
277
}
278
+7
src/structs/node.ts
+7
src/structs/node.ts
···
74
ParameterList = 10
75
}
76
77
+
export let NodeIOCanCast = ( input: NodeType | null, output: NodeType | null ): boolean => {
78
+
if(input === output)return true;
79
+
if(!input || !output)return false;
80
+
81
+
return false;
82
+
}
83
+
84
export let NodeIOResolveAnyTypes = ( nodeio: NodeIO ): NodeType | null => {
85
if(nodeio.type > 0 && nodeio.type < 6){
86
// It's a base type