this repo has no description

tabs / loading, saving works fully

phaz.uk 8303945b 88be0c10

verified
+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 + <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 + <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
··· 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
··· 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
··· 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
··· 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
··· 1 1 .control-bar{ 2 2 position: fixed; 3 3 top: 20px; 4 - left: 50px; 4 + left: 20px; 5 5 height: 40px; 6 6 z-index: 100; 7 - width: calc(100vw - 100px); 7 + width: calc(100vw - 40px); 8 8 background: #272e44; 9 9 border-radius: 20px; 10 10 display: flex;
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 1 + import { NodeDefinition } from "./Nodes"; 2 + import { NodeOSCActionsSendChatbox } from "./OSCActions/Send Chatbox"; 3 + 4 + export let NodeOSCActions: NodeDefinition = { 5 + isSingle: false, 6 + name: 'OSC Actions', 7 + items: [ 8 + NodeOSCActionsSendChatbox 9 + ] 10 + }
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 3 3 import App from "./App"; 4 4 5 5 import { NodeManager } from "./Mangers/NodeManager"; 6 + new NodeManager(); 6 7 7 8 render(() => <App />, document.getElementById("root") as HTMLElement); 8 - 9 - new NodeManager();
+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
··· 1 + import { Accessor, Setter } from "solid-js"; 2 + import { Node } from "./node"; 3 + 4 + export interface Tab{ 5 + name: string, 6 + id: string, 7 + nodes: Node[], 8 + selected: Accessor<boolean>, 9 + setSelected: Setter<boolean> 10 + }
+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 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