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 use flate2::{ write::GzEncoder, Compression }; 4 5 #[tauri::command] 6 - pub fn save_graph( graph: String ) { 7 dbg!(&graph); 8 9 - let path = dirs::config_dir().unwrap().join("VRCMacros").join("graph"); 10 11 let file = File::create(&path).unwrap(); 12 let mut encoder = GzEncoder::new(file, Compression::default());
··· 3 use flate2::{ write::GzEncoder, Compression }; 4 5 #[tauri::command] 6 + pub fn save_graph( tab_name: String, graph: String ) { 7 dbg!(&graph); 8 9 + let path = dirs::config_dir().unwrap().join("VRCMacros").join(format!("{}.macro", tab_name)); 10 11 let file = File::create(&path).unwrap(); 12 let mut encoder = GzEncoder::new(file, Compression::default());
+18 -2
src-tauri/src/setup.rs
··· 1 - use std::sync::{self, Mutex}; 2 3 - use tauri::{ App, Emitter, Manager }; 4 5 use crate::osc::{ self, OSCMessage }; 6 7 pub fn setup( app: &mut App, addresses: &'static Mutex<Vec<OSCMessage>> ){ 8 let window = app.get_webview_window("main").unwrap(); 9 10 let ( sender, receiver ) = sync::mpsc::channel(); 11
··· 1 + use std::{ fs::File, io::Read, sync::{ self, Mutex } }; 2 3 + use flate2::read::GzDecoder; 4 + use serde_json::Value; 5 + use tauri::{ App, Emitter, Listener, Manager }; 6 7 use crate::osc::{ self, OSCMessage }; 8 9 pub fn setup( app: &mut App, addresses: &'static Mutex<Vec<OSCMessage>> ){ 10 let window = app.get_webview_window("main").unwrap(); 11 + 12 + let handle = window.clone(); 13 + window.listen("tauri://drag-drop", move | ev | { 14 + let path: Value = serde_json::from_str(ev.payload()).unwrap(); 15 + let path = path["paths"][0].as_str().unwrap(); 16 + 17 + let file = File::open(path).unwrap(); 18 + let mut decoder = GzDecoder::new(file); 19 + let mut string = String::new(); 20 + 21 + decoder.read_to_string(&mut string).unwrap(); 22 + 23 + handle.emit("load_new_tab", Value::String(string)).unwrap(); 24 + }); 25 26 let ( sender, receiver ) = sync::mpsc::channel(); 27
+210 -184
src/App.tsx
··· 1 import { createSignal, onCleanup, onMount } from "solid-js"; 2 import "./App.css"; 3 - import { renderBackgroundGrid, renderContextMenu, renderNodes, renderTempDrawing } from "./renderer"; 4 import { lerp } from "./utils/lerp"; 5 import { Node, NodeIO, NodeIOResolveAnyTypes } from "./structs/node"; 6 import { isPointInRect, isPointInRectApplyOffset, screenToWorldSpace } from "./utils/interections"; 7 - import { ControlBar } from "./ControlBar"; 8 import { CanvasContextMenu } from "./ContextMenu/Canvas"; 9 import { NodeContextMenu } from "./ContextMenu/Node"; 10 import { ContextMenu } from "./structs/ContextMenu"; 11 import { NodeManager } from "./Mangers/NodeManager"; 12 13 let App = () => { 14 // TODO: Delete selected node when delete key is pressed ··· 55 } 56 57 onMount(() => { 58 ctx = canvas.getContext('2d')!; 59 60 canvas.width = window.innerWidth; ··· 77 screenMoved = true; 78 } 79 80 - requestAnimationFrame(update); 81 - }); 82 83 - let update = () => { 84 - if(stopRender)return; 85 86 - scale = lerp(scale, targetScale, 0.25); 87 88 - offset[0] = lerp(offset[0], offsetTarget[0], 0.5); 89 - offset[1] = lerp(offset[1], offsetTarget[1], 0.5); 90 91 - ctx.clearRect(canvas.width / -2, canvas.height / -2, canvas.width, canvas.height); 92 93 - renderBackgroundGrid(canvas, ctx, { x: offset[0], y: offset[1], scale }); 94 - renderNodes(canvas, ctx, NodeManager.Instance.GetNodes(), { x: offset[0], y: offset[1], scale }); 95 - if(isDrawing)renderTempDrawing(canvas, ctx, drawingTo, drawingFrom!, { x: offset[0], y: offset[1], scale }); 96 - renderContextMenu(ctx, contextMenu); 97 98 - requestAnimationFrame(update); 99 - } 100 - 101 - let isMouseDown = false; 102 - let mouseStartPos = [ 0, 0 ]; 103 - 104 - window.oncontextmenu = ( e ) => { 105 - e.preventDefault(); 106 - 107 - let clickedNode: Node | null = null 108 - NodeManager.Instance.GetNodes().map(node => { 109 - if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale }, 110 - e.clientX, e.clientY, 111 - node.x, node.y, node.w, node.h 112 - )){ 113 - clickedNode = node; 114 return; 115 } 116 - }) 117 118 - if(clickedNode){ 119 - contextMenu.items = NodeContextMenu(clickedNode); 120 - } else{ 121 - contextMenu.items = CanvasContextMenu; 122 - } 123 124 - contextMenu.position = [ e.clientX - 10 - canvas.width / 2, e.clientY - 10 - canvas.height / 2 ]; 125 - contextMenu.visible = true; 126 - } 127 - 128 - window.onmousedown = ( e ) => { 129 - if(e.clientY < 50 || lockMovement)return; 130 - 131 - if(e.button !== 0){ 132 - contextMenu.visible = false; 133 - return; 134 - } 135 136 - if(contextMenu.visible){ 137 - let submenus: ContextMenu[] = []; 138 - contextMenu.items.map(x => x.menu ? submenus.push(x.menu): null); 139 - 140 - submenus.map(x => { 141 - if(!x.visible)return; 142 if(isPointInRect(canvas, e.clientX, e.clientY, 143 - x.position[0], x.position[1], 144 - x.size[0], x.size[1] 145 )){ 146 - let item = x.items.filter(x => x.hovered)[0]; 147 if(item && item.clicked)item.clicked(e, canvas, { x: offset[0], y: offset[1], scale }); 148 } 149 - }); 150 - 151 - if(isPointInRect(canvas, e.clientX, e.clientY, 152 - contextMenu.position[0], contextMenu.position[1], 153 - contextMenu.size[0], contextMenu.size[1] 154 - )){ 155 - let item = contextMenu.items.filter(x => x.hovered)[0]; 156 - if(item && item.clicked)item.clicked(e, canvas, { x: offset[0], y: offset[1], scale }); 157 } 158 - } 159 160 - contextMenu.visible = false; 161 162 - let clickedNode: any = null; 163 - isDrawing = false; 164 165 - let clickedInput: any = null; 166 167 - NodeManager.Instance.GetNodes().map(node => { 168 - node.selected = false; 169 170 - if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale }, 171 - e.clientX, e.clientY, 172 - node.x, node.y, node.w, node.h 173 - )){ 174 - node.outputs.map(( output, i ) => { 175 if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale }, 176 e.clientX, e.clientY, 177 - node.x + (node.w - 30), 178 - node.y + 50 + (30 * i), 179 - 20, 20 180 )){ 181 - output.index = i; 182 183 - drawingTo = [ node.x + (node.w - 30), node.y + 50 + (30 * i) ]; 184 - drawingFrom = output; 185 186 - isDrawing = true; 187 return; 188 } 189 }) 190 - 191 - node.inputs.map(( input, i ) => { 192 - if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale }, 193 - e.clientX, e.clientY, 194 - node.x + 10, 195 - node.y + 50 + (30 * i), 196 - 20, 20 197 - )){ 198 - clickedInput = input; 199 - } 200 - }) 201 - 202 - clickedNode = node; 203 - return; 204 } 205 - }) 206 207 - if(clickedInput){ 208 - let partner = clickedInput.connections.pop(); 209 - if(!partner)return; 210 211 - partner.connections = partner.connections.filter(( x: any ) => x !== clickedInput); 212 213 - isDrawing = true; 214 - isMouseDown = true; 215 216 - drawingFrom = partner; 217 - drawingTo = screenToWorldSpace(canvas, { x: offset[0], y: offset[1], scale }, e.clientX - 10 * scale, e.clientY - 10 * scale) as [ number, number ];; 218 219 - return; 220 - } 221 222 - movingNode = clickedNode; 223 224 - if(clickedNode){ 225 - clickedNode.selected = true; 226 - setSelectedNode(clickedNode); 227 } 228 229 - isMouseDown = true; 230 - mouseStartPos = [ e.clientX, e.clientY ]; 231 - } 232 233 - window.onmousemove = ( e ) => { 234 - if(isMouseDown){ 235 - if(isDrawing){ 236 - drawingTo = screenToWorldSpace(canvas, { x: offset[0], y: offset[1], scale }, e.clientX - 10 * scale, e.clientY - 10 * scale) as [ number, number ]; 237 - } else if(movingNode){ 238 - movingNode.x = movingNode.x - (mouseStartPos[0] - e.clientX) / scale; 239 - movingNode.y = movingNode.y - (mouseStartPos[1] - e.clientY) / scale; 240 241 - mouseStartPos = [ e.clientX, e.clientY ]; 242 - NodeManager.Instance.UpdateConfig(); 243 - } else{ 244 - offsetTarget = [ offsetTarget[0] - (mouseStartPos[0] - e.clientX) / scale, offsetTarget[1] - (mouseStartPos[1] - e.clientY) / scale ]; 245 - mouseStartPos = [ e.clientX, e.clientY ]; 246 247 - screenMoved = true; 248 - } 249 - } 250 251 - // TODO: Fix this shit lmao please 252 - if(contextMenu.visible){ 253 - let submenus: ContextMenu[] = []; 254 - contextMenu.items.map(x => x.menu ? submenus.push(x.menu): null); 255 256 - submenus.map(x => { 257 - if(!x.visible)return; 258 if(isPointInRect(canvas, e.clientX, e.clientY, 259 - x.position[0], x.position[1], 260 - x.size[0], x.size[1] 261 )){ 262 - x.items.map((y, i) => { 263 - y.hovered = isPointInRect(canvas, e.clientX, e.clientY, 264 - x.position[0], x.position[1] + 10 + 25 * i, 265 - x.size[0], 25 266 ) 267 }); 268 } 269 - }); 270 271 - if(isPointInRect(canvas, e.clientX, e.clientY, 272 - contextMenu.position[0], contextMenu.position[1], 273 - contextMenu.size[0], contextMenu.size[1] 274 - )){ 275 - contextMenu.items.map((x, i) => { 276 - x.hovered = isPointInRect(canvas, e.clientX, e.clientY, 277 - contextMenu.position[0], contextMenu.position[1] + 10 + 25 * i, 278 - contextMenu.size[0], 25 279 - ) 280 281 - if(x.menu)x.menu.visible = x.hovered; 282 - }); 283 } 284 } 285 - } 286 287 - window.onmouseup = ( e ) => { 288 - NodeManager.Instance.GetNodes().map(node => { 289 - node.inputs.map(( input, i ) => { 290 - if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale }, 291 - e.clientX, e.clientY, 292 - node.x + 10, 293 - node.y + 50 + (30 * i), 294 - 20, 20 295 - )){ 296 - if(isDrawing){ 297 - let fromType = NodeIOResolveAnyTypes(drawingFrom!); 298 - let toType = NodeIOResolveAnyTypes(input); 299 300 - if( 301 - drawingFrom!.connections.indexOf(input) === -1 && 302 - ( 303 - toType === null || 304 - fromType === toType 305 - ) 306 - ){ 307 - drawingFrom!.connections.push(input); 308 - input.connections.push(drawingFrom!); 309 310 - NodeManager.Instance.UpdateConfig(); 311 - } 312 - } 313 - } 314 - }) 315 - }) 316 317 - isDrawing = false; 318 - isMouseDown = false; 319 } 320 321 let interval = setInterval(() => { 322 if(screenMoved){ ··· 333 334 return ( 335 <> 336 <ControlBar node={selectedNode} lockMovement={( lock ) => lockMovement = lock} /> 337 <canvas ref={canvas}/> 338 </>
··· 1 import { createSignal, onCleanup, onMount } from "solid-js"; 2 import "./App.css"; 3 + import { renderBackgroundGrid, renderContextMenu, renderNodes, renderNullTab, renderTempDrawing } from "./renderer"; 4 import { lerp } from "./utils/lerp"; 5 import { Node, NodeIO, NodeIOResolveAnyTypes } from "./structs/node"; 6 import { isPointInRect, isPointInRectApplyOffset, screenToWorldSpace } from "./utils/interections"; 7 + import { ControlBar } from "./components/ControlBar"; 8 import { CanvasContextMenu } from "./ContextMenu/Canvas"; 9 import { NodeContextMenu } from "./ContextMenu/Node"; 10 import { ContextMenu } from "./structs/ContextMenu"; 11 import { NodeManager } from "./Mangers/NodeManager"; 12 + import { TabMenu } from "./components/TabMenu"; 13 14 let App = () => { 15 // TODO: Delete selected node when delete key is pressed ··· 56 } 57 58 onMount(() => { 59 + NodeManager.Instance.HookTabChange(() => setSelectedNode(null)); 60 + 61 ctx = canvas.getContext('2d')!; 62 63 canvas.width = window.innerWidth; ··· 80 screenMoved = true; 81 } 82 83 + canvas.oncontextmenu = ( e ) => { 84 + e.preventDefault(); 85 86 + let clickedNode: Node | null = null 87 + let nodes = NodeManager.Instance.GetNodes(); 88 89 + if(nodes){ 90 + nodes.map(node => { 91 + if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale }, 92 + e.clientX, e.clientY, 93 + node.x, node.y, node.w, node.h 94 + )){ 95 + clickedNode = node; 96 + return; 97 + } 98 + }) 99 + } 100 101 + if(clickedNode){ 102 + contextMenu.items = NodeContextMenu(clickedNode); 103 + } else{ 104 + contextMenu.items = CanvasContextMenu; 105 + } 106 107 + contextMenu.position = [ e.clientX - 10 - canvas.width / 2, e.clientY - 10 - canvas.height / 2 ]; 108 + contextMenu.visible = true; 109 + } 110 111 + canvas.onmousedown = ( e ) => { 112 + if( 113 + e.clientY < 60 || 114 + e.clientX < 220 || 115 + lockMovement 116 + )return; 117 118 + if(e.button !== 0){ 119 + contextMenu.visible = false; 120 return; 121 } 122 123 + if(contextMenu.visible){ 124 + let submenus: ContextMenu[] = []; 125 + contextMenu.items.map(x => x.menu ? submenus.push(x.menu): null); 126 127 + submenus.map(x => { 128 + if(!x.visible)return; 129 + if(isPointInRect(canvas, e.clientX, e.clientY, 130 + x.position[0], x.position[1], 131 + x.size[0], x.size[1] 132 + )){ 133 + let item = x.items.filter(x => x.hovered)[0]; 134 + if(item && item.clicked)item.clicked(e, canvas, { x: offset[0], y: offset[1], scale }); 135 + } 136 + }); 137 138 if(isPointInRect(canvas, e.clientX, e.clientY, 139 + contextMenu.position[0], contextMenu.position[1], 140 + contextMenu.size[0], contextMenu.size[1] 141 )){ 142 + let item = contextMenu.items.filter(x => x.hovered)[0]; 143 if(item && item.clicked)item.clicked(e, canvas, { x: offset[0], y: offset[1], scale }); 144 } 145 } 146 147 + contextMenu.visible = false; 148 149 + let clickedNode: any = null; 150 + isDrawing = false; 151 152 + let clickedInput: any = null; 153 + let nodes = NodeManager.Instance.GetNodes(); 154 155 + if(nodes){ 156 + nodes.map(node => { 157 + node.selected = false; 158 159 if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale }, 160 e.clientX, e.clientY, 161 + node.x, node.y, node.w, node.h 162 )){ 163 + node.outputs.map(( output, i ) => { 164 + if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale }, 165 + e.clientX, e.clientY, 166 + node.x + (node.w - 30), 167 + node.y + 50 + (30 * i), 168 + 20, 20 169 + )){ 170 + output.index = i; 171 172 + drawingTo = [ node.x + (node.w - 30), node.y + 50 + (30 * i) ]; 173 + drawingFrom = output; 174 + 175 + isDrawing = true; 176 + return; 177 + } 178 + }) 179 + 180 + node.inputs.map(( input, i ) => { 181 + if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale }, 182 + e.clientX, e.clientY, 183 + node.x + 10, 184 + node.y + 50 + (30 * i), 185 + 20, 20 186 + )){ 187 + clickedInput = input; 188 + } 189 + }) 190 191 + clickedNode = node; 192 return; 193 } 194 }) 195 } 196 197 + if(clickedInput){ 198 + let partner = clickedInput.connections.pop(); 199 + if(!partner)return; 200 201 + partner.connections = partner.connections.filter(( x: any ) => x !== clickedInput); 202 203 + isDrawing = true; 204 + isMouseDown = true; 205 + 206 + drawingFrom = partner; 207 + drawingTo = screenToWorldSpace(canvas, { x: offset[0], y: offset[1], scale }, e.clientX - 10 * scale, e.clientY - 10 * scale) as [ number, number ];; 208 209 + return; 210 + } 211 212 + movingNode = clickedNode; 213 214 + if(clickedNode){ 215 + clickedNode.selected = true; 216 + setSelectedNode(clickedNode); 217 + } 218 219 + isMouseDown = true; 220 + mouseStartPos = [ e.clientX, e.clientY ]; 221 } 222 223 + canvas.onmousemove = ( e ) => { 224 + if(isMouseDown){ 225 + if(isDrawing){ 226 + drawingTo = screenToWorldSpace(canvas, { x: offset[0], y: offset[1], scale }, e.clientX - 10 * scale, e.clientY - 10 * scale) as [ number, number ]; 227 + } else if(movingNode){ 228 + movingNode.x = movingNode.x - (mouseStartPos[0] - e.clientX) / scale; 229 + movingNode.y = movingNode.y - (mouseStartPos[1] - e.clientY) / scale; 230 231 + mouseStartPos = [ e.clientX, e.clientY ]; 232 + NodeManager.Instance.UpdateConfig(); 233 + } else{ 234 + offsetTarget = [ offsetTarget[0] - (mouseStartPos[0] - e.clientX) / scale, offsetTarget[1] - (mouseStartPos[1] - e.clientY) / scale ]; 235 + mouseStartPos = [ e.clientX, e.clientY ]; 236 237 + screenMoved = true; 238 + } 239 + } 240 241 + // TODO: Fix this shit lmao please 242 + if(contextMenu.visible){ 243 + let submenus: ContextMenu[] = []; 244 + contextMenu.items.map(x => x.menu ? submenus.push(x.menu): null); 245 246 + submenus.map(x => { 247 + if(!x.visible)return; 248 + if(isPointInRect(canvas, e.clientX, e.clientY, 249 + x.position[0], x.position[1], 250 + x.size[0], x.size[1] 251 + )){ 252 + x.items.map((y, i) => { 253 + y.hovered = isPointInRect(canvas, e.clientX, e.clientY, 254 + x.position[0], x.position[1] + 10 + 25 * i, 255 + x.size[0], 25 256 + ) 257 + }); 258 + } 259 + }); 260 261 if(isPointInRect(canvas, e.clientX, e.clientY, 262 + contextMenu.position[0], contextMenu.position[1], 263 + contextMenu.size[0], contextMenu.size[1] 264 )){ 265 + contextMenu.items.map((x, i) => { 266 + x.hovered = isPointInRect(canvas, e.clientX, e.clientY, 267 + contextMenu.position[0], contextMenu.position[1] + 10 + 25 * i, 268 + contextMenu.size[0], 25 269 ) 270 + 271 + if(x.menu)x.menu.visible = x.hovered; 272 }); 273 } 274 + } 275 + } 276 + 277 + canvas.onmouseup = ( e ) => { 278 + let nodes = NodeManager.Instance.GetNodes(); 279 + 280 + if(nodes){ 281 + nodes.map(node => { 282 + node.inputs.map(( input, i ) => { 283 + if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale }, 284 + e.clientX, e.clientY, 285 + node.x + 10, 286 + node.y + 50 + (30 * i), 287 + 20, 20 288 + )){ 289 + if(isDrawing){ 290 + let fromType = NodeIOResolveAnyTypes(drawingFrom!); 291 + let toType = NodeIOResolveAnyTypes(input); 292 293 + if( 294 + drawingFrom!.connections.indexOf(input) === -1 && 295 + ( 296 + toType === null || 297 + fromType === toType 298 + ) 299 + ){ 300 + drawingFrom!.connections.push(input); 301 + input.connections.push(drawingFrom!); 302 303 + NodeManager.Instance.UpdateConfig(); 304 + } 305 + } 306 + } 307 + }) 308 + }) 309 } 310 + 311 + isDrawing = false; 312 + isMouseDown = false; 313 } 314 315 + requestAnimationFrame(update); 316 + }); 317 + 318 + let update = () => { 319 + if(stopRender)return; 320 + 321 + scale = lerp(scale, targetScale, 0.25); 322 + 323 + offset[0] = lerp(offset[0], offsetTarget[0], 0.5); 324 + offset[1] = lerp(offset[1], offsetTarget[1], 0.5); 325 + 326 + ctx.clearRect(canvas.width / -2, canvas.height / -2, canvas.width, canvas.height); 327 + 328 + let nodes = NodeManager.Instance.GetNodes(); 329 330 + renderBackgroundGrid(canvas, ctx, { x: offset[0], y: offset[1], scale }); 331 + 332 + if(nodes) 333 + renderNodes(canvas, ctx, nodes, { x: offset[0], y: offset[1], scale }); 334 + else 335 + renderNullTab(canvas, ctx); 336 337 + if(isDrawing)renderTempDrawing(canvas, ctx, drawingTo, drawingFrom!, { x: offset[0], y: offset[1], scale }); 338 + renderContextMenu(ctx, contextMenu); 339 340 + requestAnimationFrame(update); 341 } 342 + 343 + let isMouseDown = false; 344 + let mouseStartPos = [ 0, 0 ]; 345 346 let interval = setInterval(() => { 347 if(screenMoved){ ··· 358 359 return ( 360 <> 361 + <TabMenu /> 362 <ControlBar node={selectedNode} lockMovement={( lock ) => lockMovement = lock} /> 363 <canvas ref={canvas}/> 364 </>
+35 -300
src/ContextMenu/Canvas.tsx
··· 1 - import { invoke } from "@tauri-apps/api/core"; 2 import { PositionInfo } from "../renderer"; 3 - import { Node, NodeType } from "../structs/node"; 4 - import { OSCMessage } from "../structs/OscMessage"; 5 import { screenToWorldSpace } from "../utils/interections"; 6 import { NodeManager } from "../Mangers/NodeManager"; 7 import { ContextMenuItem } from "../structs/ContextMenu"; 8 - 9 - export let CanvasContextMenu: ContextMenuItem[] = [ 10 - { 11 - text: "Add OSC Trigger Node", 12 - clicked: ( e: MouseEvent, canvas: HTMLCanvasElement, position: PositionInfo ) => { 13 - let pos = screenToWorldSpace(canvas, position, e.clientX, e.clientY); 14 - 15 - let node: Node = { 16 - name: 'OSC Trigger', 17 - id: NodeManager.Instance.GetNewNodeId(), 18 - x: pos[0], 19 - y: pos[1], 20 - w: 200, 21 - h: 50, 22 - inputs: [], 23 - outputs: [], 24 - selected: false, 25 - statics: [ 26 - { 27 - name: "OSC Trigger", 28 - type: NodeType.OSCAddress, 29 - value: null 30 - }, 31 - { 32 - name: "Parameter List", 33 - type: NodeType.ParameterList, 34 - value: [] 35 - } 36 - ], 37 - onStaticsUpdate: ( node ) => { 38 - let address = node.statics[0].value; 39 - let parameters = node.statics[1].value; 40 - 41 - (async () => { 42 - if(address){ 43 - let addresses = await invoke<OSCMessage[]>('get_addresses'); 44 - let msgDat = addresses.find(x => x.address == address); 45 - 46 - if(!msgDat)return; 47 - 48 - parameters = msgDat.values.map(x => { return { type: x.key, desc: '' }}); 49 - node.statics[1].value = parameters; 50 - } 51 - 52 - node.outputs.map(output => { 53 - output.connections.map(partner => { 54 - partner.connections = partner.connections.filter(x => x != output); 55 - }) 56 - }) 57 - node.outputs = []; 58 - 59 - node.outputs.push({ 60 - name: 'Flow', 61 - type: NodeType.Flow, 62 - connections: [], 63 - parent: node, 64 - index: 0 65 - }) 66 - 67 - parameters.forEach(( dat: any, indx: number ) => { 68 - let type: NodeType | null = null; 69 - 70 - switch(dat.type){ 71 - case 'Int': 72 - type = NodeType.Int; 73 - break; 74 - case 'Float': 75 - type = NodeType.Float; 76 - break; 77 - case 'String': 78 - type = NodeType.String; 79 - break; 80 - case 'Boolean': 81 - type = NodeType.Boolean; 82 - break; 83 - } 84 - 85 - if(type){ 86 - node.outputs.push({ 87 - name: dat.desc === '' ? dat.type : dat.desc, 88 - type: type, 89 - connections: [], 90 - parent: node, 91 - index: indx + 1 92 - }) 93 - } 94 - }); 95 - 96 - node.h = 60 + (parameters.length + 1) * 30; 97 - NodeManager.Instance.UpdateConfig(); 98 - })(); 99 - } 100 - }; 101 - 102 - NodeManager.Instance.AddNode(node); 103 - }, 104 - hovered: false 105 - }, 106 - 107 - { 108 - text: "Conditional", 109 - menu: { 110 - items: [ 111 - { 112 - text: "If Equals", 113 - hovered: false, 114 - clicked: ( e: MouseEvent, canvas: HTMLCanvasElement, position: PositionInfo ) => { 115 - let pos = screenToWorldSpace(canvas, position, e.clientX, e.clientY); 116 - 117 - let node: Node = { 118 - name: 'If Equals', 119 - id: NodeManager.Instance.GetNewNodeId(), 120 - x: pos[0], 121 - y: pos[1], 122 - w: 220, 123 - h: 150, 124 - inputs: [], 125 - outputs: [], 126 - selected: false, 127 - statics: [], 128 - onStaticsUpdate: ( _node ) => {} 129 - }; 130 131 - node.inputs.push({ 132 - name: "Flow", 133 - type: NodeType.Flow, 134 - connections: [], 135 - parent: node, 136 - index: 0 137 - }); 138 139 - node.inputs.push({ 140 - name: "Input 1", 141 - type: NodeType.AnyTypeA, 142 - connections: [], 143 - parent: node, 144 - index: 1 145 - }); 146 147 - node.inputs.push({ 148 - name: "Input 2", 149 - type: NodeType.AnyTypeA, 150 - connections: [], 151 - parent: node, 152 - index: 2 153 - }); 154 - 155 - 156 - node.outputs.push({ 157 - name: "Equal", 158 - type: NodeType.Flow, 159 - connections: [], 160 - parent: node, 161 - index: 0 162 - }); 163 - 164 - node.outputs.push({ 165 - name: "Not Equal", 166 - type: NodeType.Flow, 167 - connections: [], 168 - parent: node, 169 - index: 1 170 - }); 171 - 172 - NodeManager.Instance.AddNode(node); 173 } 174 - }, 175 - ], 176 - position: [ 0, 0 ], 177 - size: [ 0, 0 ], 178 - visible: true 179 - }, 180 - hovered: false 181 - }, 182 - 183 - { 184 - text: "Statics", 185 - menu: { 186 - items: [ 187 - { 188 - text: "String", 189 - hovered: false, 190 - clicked: ( e: MouseEvent, canvas: HTMLCanvasElement, position: PositionInfo ) => { 191 - let pos = screenToWorldSpace(canvas, position, e.clientX, e.clientY); 192 - 193 - let node: Node = { 194 - name: 'String', 195 - id: NodeManager.Instance.GetNewNodeId(), 196 - x: pos[0], 197 - y: pos[1], 198 - w: 200, 199 - h: 85, 200 - inputs: [], 201 - outputs: [], 202 - selected: false, 203 - statics: [], 204 - onStaticsUpdate: ( _node ) => {} 205 - }; 206 - 207 - node.outputs.push({ 208 - name: "String", 209 - type: NodeType.String, 210 - connections: [], 211 - parent: node, 212 - index: 0 213 - }); 214 - 215 - NodeManager.Instance.AddNode(node); 216 - } 217 - }, 218 - 219 - { 220 - text: "Int", 221 - hovered: false, 222 - clicked: ( e: MouseEvent, canvas: HTMLCanvasElement, position: PositionInfo ) => { 223 - let pos = screenToWorldSpace(canvas, position, e.clientX, e.clientY); 224 - 225 - let node: Node = { 226 - name: 'Int', 227 - id: NodeManager.Instance.GetNewNodeId(), 228 - x: pos[0], 229 - y: pos[1], 230 - w: 200, 231 - h: 85, 232 - inputs: [], 233 - outputs: [], 234 - selected: false, 235 - statics: [], 236 - onStaticsUpdate: ( _node ) => {} 237 - }; 238 - 239 - node.outputs.push({ 240 - name: "Int", 241 - type: NodeType.Int, 242 - connections: [], 243 - parent: node, 244 - index: 0 245 - }); 246 - 247 - NodeManager.Instance.AddNode(node); 248 - } 249 - }, 250 - ], 251 - position: [ 0, 0 ], 252 - size: [ 0, 0 ], 253 - visible: true 254 - }, 255 - hovered: false 256 - }, 257 - 258 - { 259 - text: "OSC Actions", 260 - menu: { 261 - items: [ 262 - { 263 - text: "Send Chatbox", 264 - hovered: false, 265 - clicked: ( e: MouseEvent, canvas: HTMLCanvasElement, position: PositionInfo ) => { 266 - let pos = screenToWorldSpace(canvas, position, e.clientX, e.clientY); 267 - 268 - let node: Node = { 269 - name: 'Send Chatbox', 270 - id: NodeManager.Instance.GetNewNodeId(), 271 - x: pos[0], 272 - y: pos[1], 273 - w: 200, 274 - h: 120, 275 - inputs: [], 276 - outputs: [], 277 - selected: false, 278 - statics: [], 279 - onStaticsUpdate: ( _node ) => {} 280 - }; 281 - 282 - node.inputs.push({ 283 - name: "Flow", 284 - type: NodeType.Flow, 285 - connections: [], 286 - parent: node, 287 - index: 0 288 - }); 289 - 290 - node.inputs.push({ 291 - name: "Value", 292 - type: NodeType.String, 293 - connections: [], 294 - parent: node, 295 - index: 1 296 - }); 297 - 298 - NodeManager.Instance.AddNode(node); 299 - } 300 - }, 301 - ], 302 - position: [ 0, 0 ], 303 - size: [ 0, 0 ], 304 - visible: true 305 - }, 306 - hovered: false 307 - }, 308 - ]
··· 1 import { PositionInfo } from "../renderer"; 2 + import { Node } from "../structs/node"; 3 import { screenToWorldSpace } from "../utils/interections"; 4 import { NodeManager } from "../Mangers/NodeManager"; 5 import { ContextMenuItem } from "../structs/ContextMenu"; 6 + import { Nodes } from "../Nodes/Nodes"; 7 8 + export let CanvasContextMenu: ContextMenuItem[] = Nodes.map(( node ) => { 9 + if(node.isSingle){ 10 + return { 11 + text: node.name, 12 + clicked: async ( e: MouseEvent, canvas: HTMLCanvasElement, position: PositionInfo ) => { 13 + let pos = screenToWorldSpace(canvas, position, e.clientX, e.clientY); 14 + let id = await NodeManager.Instance.GetNewNodeId(); 15 16 + NodeManager.Instance.AddNode(new Node(pos, node, id)); 17 + }, 18 + hovered: false 19 + } 20 + } else{ 21 + return { 22 + text: node.name, 23 + menu: { 24 + items: node.items!.map(x => { 25 + return { 26 + text: x.name, 27 + clicked: async ( e: MouseEvent, canvas: HTMLCanvasElement, position: PositionInfo ) => { 28 + let pos = screenToWorldSpace(canvas, position, e.clientX, e.clientY); 29 + let id = await NodeManager.Instance.GetNewNodeId(); 30 31 + NodeManager.Instance.AddNode(new Node(pos, x, id)); 32 + }, 33 + hovered: false 34 } 35 + }), 36 + position: [ 0, 0 ], 37 + size: [ 0, 0 ], 38 + visible: true 39 + }, 40 + hovered: false 41 + } 42 + } 43 + });
+2 -2
src/ControlBar.css src/components/ControlBar.css
··· 1 .control-bar{ 2 position: fixed; 3 top: 20px; 4 - left: 50px; 5 height: 40px; 6 z-index: 100; 7 - width: calc(100vw - 100px); 8 background: #272e44; 9 border-radius: 20px; 10 display: flex;
··· 1 .control-bar{ 2 position: fixed; 3 top: 20px; 4 + left: 20px; 5 height: 40px; 6 z-index: 100; 7 + width: calc(100vw - 40px); 8 background: #272e44; 9 border-radius: 20px; 10 display: flex;
+6 -6
src/ControlBar.tsx src/components/ControlBar.tsx
··· 1 import './ControlBar.css'; 2 3 import { Accessor, createSignal, For, Match, Show, Switch } from 'solid-js'; 4 - import { Node, NodeType } from './structs/node'; 5 - import { TextInput } from './components/TextInput'; 6 import { invoke } from '@tauri-apps/api/core'; 7 - import { OSCMessage } from './structs/OscMessage'; 8 - import { ParameterList } from './components/ParameterList'; 9 - import { NodeManager } from './Mangers/NodeManager'; 10 11 export interface ControlBarProps{ 12 node: Accessor<Node | null>, ··· 56 57 item.value = text; 58 node.onStaticsUpdate(node); 59 - 60 NodeManager.Instance.UpdateConfig(); 61 }} /> 62 </div>
··· 1 import './ControlBar.css'; 2 3 import { Accessor, createSignal, For, Match, Show, Switch } from 'solid-js'; 4 + import { Node, NodeType } from '../structs/node'; 5 + import { TextInput } from './TextInput'; 6 import { invoke } from '@tauri-apps/api/core'; 7 + import { OSCMessage } from '../structs/OscMessage'; 8 + import { ParameterList } from './ParameterList'; 9 + import { NodeManager } from '../Mangers/NodeManager'; 10 11 export interface ControlBarProps{ 12 node: Accessor<Node | null>, ··· 56 57 item.value = text; 58 node.onStaticsUpdate(node); 59 + 60 NodeManager.Instance.UpdateConfig(); 61 }} /> 62 </div>
+158 -47
src/Mangers/NodeManager.tsx
··· 1 import { invoke } from "@tauri-apps/api/core"; 2 import { Node } from "../structs/node"; 3 4 export class NodeManager{ 5 public static Instance: NodeManager; 6 7 private _nodes: Node[] = []; 8 - private _newestNodeId = 0; 9 private _needsSave = false; 10 11 constructor(){ ··· 15 // Save config every 1 second 16 if(this._needsSave)this._saveConfigToDisk(); 17 }, 1_000); 18 } 19 20 public AddNode( node: Node ){ 21 this._nodes.push(node); 22 this.UpdateConfig(); 23 } 24 25 public RemoveNode( node: Node ){ 26 this._nodes = this._nodes.filter(x => x !== node); 27 this.UpdateConfig(); 28 } 29 30 - public GetNodes(): Node[]{ 31 - return this._nodes; 32 } 33 34 - public GetNewNodeId(){ 35 - return this._newestNodeId++; // TODO: really need a better solution than this, but it'll work for now 36 } 37 38 public UpdateConfig(){ 39 this._needsSave = true; 40 } 41 42 - private _loadFromConfig( config: string ){ 43 let json = JSON.parse(config); 44 45 this._nodes = []; 46 47 // Populate nodes 48 - for (let i = 0; i < json.length; i++) { 49 - let node = json[i]; 50 51 - this._nodes.push({ 52 - name: node.name, 53 - id: node.id, 54 - x: node.x, y: node.y, 55 - w: node.w, h: node.h, 56 - inputs: [], 57 - outputs: [], 58 - selected: false, 59 - statics: node.statics, 60 - onStaticsUpdate: ( _node ) => {} // TODO: Make a seperate setup for node logic so we can load from that 61 - }) 62 - 63 - this._newestNodeId = node.id 64 - } 65 66 - // Populate node outputs 67 - for (let i = 0; i < json.length; i++) { 68 - let configNode = json[i]; 69 - let node = this._nodes[i]; 70 71 - for (let j = 0; j < configNode.outputs.length; j++) { 72 - let output = configNode.outputs[j]; 73 - node.outputs.push({ 74 - name: output.name, 75 - type: output.type, 76 - connections: [], 77 - parent: node, 78 - index: j 79 - }) 80 - } 81 } 82 83 // Populate node inputs 84 - for (let i = 0; i < json.length; i++) { 85 - let configNode = json[i]; 86 let outputParentNode = this._nodes[i]; 87 88 for (let j = 0; j < configNode.outputs.length; j++) { ··· 90 91 for (let k = 0; k < output.connections.length; k++) { 92 let input = output.connections[k]; 93 - let node = this._nodes[input.node]; 94 95 let realInput = node.inputs.find(x => x.index === input.index); 96 let realOutput = outputParentNode.outputs[j]; 97 98 if(realInput){ 99 realInput.connections.push(realOutput); 100 } else{ 101 - node.inputs.push({ 102 name: input.name, 103 type: input.type, 104 parent: node, 105 connections: [ realOutput ], 106 index: input.index 107 - }) 108 } 109 } 110 } 111 } 112 } 113 114 - private _saveConfigToDisk(){ 115 this._needsSave = false; 116 // Convert it into a structure we can actually save... 117 118 let nodesToSave = []; 119 ··· 140 nodesToSave.push({ 141 name: node.name, 142 id: node.id, 143 - x: node.x, y: node.y, 144 - w: node.w, h: node.h, 145 - statics: node.statics, 146 - outputs: nodeOutputs 147 }) 148 } 149 150 - invoke('save_graph', { graph: JSON.stringify(nodesToSave) }); 151 } 152 }
··· 1 import { invoke } from "@tauri-apps/api/core"; 2 import { Node } from "../structs/node"; 3 + import { Tab } from "../structs/Tab"; 4 + import { createSignal } from "solid-js"; 5 + import { listen } from "@tauri-apps/api/event"; 6 + import { getVersion } from "@tauri-apps/api/app"; 7 + import { NodesByID } from "../Nodes/Nodes"; 8 + 9 + export interface TabHashMap { 10 + [details: string] : Tab; 11 + } 12 13 export class NodeManager{ 14 public static Instance: NodeManager; 15 16 + private _selectedTab: string | null = null; 17 + private _tabs: TabHashMap = {}; 18 + 19 private _nodes: Node[] = []; 20 private _needsSave = false; 21 22 constructor(){ ··· 26 // Save config every 1 second 27 if(this._needsSave)this._saveConfigToDisk(); 28 }, 1_000); 29 + 30 + listen('load_new_tab', ( ev: any ) => { 31 + this._loadFromConfig(ev.payload); 32 + }) 33 + } 34 + 35 + 36 + private _tabUpdateHook: ( tabs: TabHashMap ) => void = () => {}; 37 + private _tabChangeHook: () => void = () => {}; 38 + 39 + public async AddTab( name: string ){ 40 + let [ selected, setSelected ] = createSignal(false); 41 + 42 + let tab: Tab = { 43 + name: name, 44 + id: await NodeManager.Instance.GetNewNodeId(), 45 + nodes: [], 46 + selected: selected, 47 + setSelected: setSelected 48 + }; 49 + 50 + this._tabs[tab.id] = tab; 51 + 52 + this.SelectTab(tab.id); 53 + this._tabUpdateHook(this._tabs); 54 + } 55 + 56 + public CloseTab( id: string ){ // TODO: Add confirmation to close tab 57 + console.log(id === this._selectedTab); 58 + if(this._selectedTab === id){ 59 + let tabs = Object.values(this._tabs); 60 + 61 + if(tabs.length === 1){ 62 + this.SelectTab(null); 63 + } else{ 64 + let tabToDelete = this._tabs[id]; 65 + 66 + let index = tabs.indexOf(tabToDelete); 67 + let nextTab = tabs[index + 1]; 68 + 69 + if(nextTab) 70 + this.SelectTab(nextTab.id); 71 + else 72 + this.SelectTab(tabs[0].id); 73 + } 74 + } 75 + 76 + delete this._tabs[id]; 77 + this._tabUpdateHook(this._tabs); 78 + } 79 + 80 + public RenameTab( id: string, name: string ){ 81 + let tab = this._tabs[id]; 82 + if(!tab)return; 83 + 84 + tab.name = name; 85 + this._tabUpdateHook(this._tabs); 86 + } 87 + 88 + public SelectTab( id: string | null ){ 89 + if(this._selectedTab && this._tabs[this._selectedTab]){ 90 + let tab = this._tabs[this._selectedTab]; 91 + 92 + tab.setSelected(false); 93 + tab.nodes = this._nodes; 94 + } 95 + 96 + this._selectedTab = id; 97 + this._tabChangeHook(); 98 + 99 + if(this._selectedTab){ 100 + let tab = this._tabs[this._selectedTab]; 101 + if(!tab){ 102 + this._selectedTab = null; 103 + return this._nodes = []; 104 + } 105 + 106 + tab.setSelected(true); 107 + this._nodes = tab.nodes; 108 + } else{ 109 + this._nodes = []; 110 + } 111 + } 112 + 113 + public HookTabUpdate( cb: ( tabs: TabHashMap ) => void ){ 114 + this._tabUpdateHook = cb; 115 + } 116 + 117 + public HookTabChange( cb: () => void ){ 118 + this._tabChangeHook = cb; 119 } 120 121 + 122 public AddNode( node: Node ){ 123 + if(!this._selectedTab)return; 124 + 125 this._nodes.push(node); 126 this.UpdateConfig(); 127 } 128 129 public RemoveNode( node: Node ){ 130 + if(!this._selectedTab)return; 131 + 132 this._nodes = this._nodes.filter(x => x !== node); 133 this.UpdateConfig(); 134 } 135 136 + public GetNodes(): Node[] | null{ 137 + if(this._selectedTab) 138 + return this._nodes; 139 + else 140 + return null; 141 } 142 143 + public async GetNewNodeId(){ 144 + let encoder = new TextEncoder(); 145 + let data = encoder.encode(Date.now().toString() + Math.random().toString()); 146 + let hash = await window.crypto.subtle.digest("SHA-256", data); // Probably should get a better ID implementation 147 + 148 + return btoa(String.fromCharCode(...new Uint8Array(hash))); 149 } 150 151 + 152 public UpdateConfig(){ 153 this._needsSave = true; 154 } 155 156 + private async _loadFromConfig( config: string ){ 157 let json = JSON.parse(config); 158 159 + if( 160 + !json.tab_name || 161 + !json.version || 162 + !json.graph 163 + )return; 164 + 165 + await this.AddTab(json.tab_name); 166 this._nodes = []; 167 168 + let graph = json.graph; 169 + 170 // Populate nodes 171 + for (let i = 0; i < graph.length; i++) { 172 + let node = graph[i]; 173 174 + let nod = new Node(node.pos, NodesByID[node.typeId], node.id); 175 176 + nod.statics = node.statics; 177 + nod.onStaticsUpdate(nod); 178 179 + this._nodes.push(nod); 180 } 181 182 // Populate node inputs 183 + for (let i = 0; i < graph.length; i++) { 184 + let configNode = graph[i]; 185 let outputParentNode = this._nodes[i]; 186 187 for (let j = 0; j < configNode.outputs.length; j++) { ··· 189 190 for (let k = 0; k < output.connections.length; k++) { 191 let input = output.connections[k]; 192 + let node = this._nodes.find(x => x.id === input.node)!; 193 194 let realInput = node.inputs.find(x => x.index === input.index); 195 let realOutput = outputParentNode.outputs[j]; 196 197 if(realInput){ 198 realInput.connections.push(realOutput); 199 + realOutput.connections.push(realInput); 200 } else{ 201 + let realInput = { 202 name: input.name, 203 type: input.type, 204 parent: node, 205 connections: [ realOutput ], 206 index: input.index 207 + }; 208 + 209 + node.inputs.push(realInput); 210 + realOutput.connections.push(realInput); 211 } 212 } 213 } 214 } 215 } 216 217 + private async _saveConfigToDisk(){ 218 this._needsSave = false; 219 // Convert it into a structure we can actually save... 220 + 221 + if(!this._selectedTab)return; 222 + let tab = this._tabs[this._selectedTab]; 223 + if(!tab)return; 224 225 let nodesToSave = []; 226 ··· 247 nodesToSave.push({ 248 name: node.name, 249 id: node.id, 250 + typeId: node.typeId, 251 + pos: [ node.x, node.y ], 252 + outputs: nodeOutputs, 253 + statics: node.statics 254 }) 255 } 256 257 + invoke('save_graph', { tabName: tab.name, graph: JSON.stringify({ 258 + tab_name: tab.name, 259 + version: await getVersion(), 260 + graph: nodesToSave 261 + }) }); 262 } 263 }
+15
src/Nodes/Conditional.tsx
···
··· 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 import App from "./App"; 4 5 import { NodeManager } from "./Mangers/NodeManager"; 6 7 render(() => <App />, document.getElementById("root") as HTMLElement); 8 - 9 - new NodeManager();
··· 3 import App from "./App"; 4 5 import { NodeManager } from "./Mangers/NodeManager"; 6 + new NodeManager(); 7 8 render(() => <App />, document.getElementById("root") as HTMLElement);
+20
src/renderer.ts
··· 233 ctx.bezierCurveTo(midpoint[0], midpoint[1], end[0], end[1], toX, toY); 234 } 235 236 let drawRoundedRect = ( ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, radius: number ) => { 237 ctx.beginPath(); 238 ctx.arc(x + radius, y + radius, radius, Math.PI, Math.PI * 1.5);
··· 233 ctx.bezierCurveTo(midpoint[0], midpoint[1], end[0], end[1], toX, toY); 234 } 235 236 + export let renderNullTab = ( 237 + canvas: HTMLCanvasElement, 238 + ctx: CanvasRenderingContext2D, 239 + ) => { 240 + ctx.fillStyle = '#fff'; 241 + 242 + ctx.font = '20px Arial'; 243 + ctx.textBaseline = 'middle'; 244 + ctx.textAlign = 'center'; 245 + 246 + let textX = lerp((canvas.width / -2) + 200, canvas.width / 2, 0.5); 247 + let textY = lerp((canvas.height / -2) + 40, canvas.height / 2, 0.5); 248 + 249 + ctx.font = '40px Arial'; 250 + ctx.fillText('Welcome to VRCMacros', textX, textY); 251 + 252 + ctx.font = '20px Arial'; 253 + ctx.fillText('Create a new tab to get started!', textX, textY + 40); 254 + } 255 + 256 let drawRoundedRect = ( ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, radius: number ) => { 257 ctx.beginPath(); 258 ctx.arc(x + radius, y + radius, radius, Math.PI, Math.PI * 1.5);
+10
src/structs/Tab.ts
···
··· 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 13 } 14 15 export interface NodeIO{
··· 1 + import { NodeDefinition } from "../Nodes/Nodes"; 2 + 3 + export class Node{ 4 + name: string; 5 + id: string; 6 + typeId: string; 7 + x: number; 8 + y: number; 9 + w: number; 10 + h: number; 11 + inputs: NodeIO[]; 12 + outputs: NodeIO[]; 13 + selected: boolean; 14 + statics: NodeStatic[]; 15 + onStaticsUpdate: ( node: Node ) => void; 16 + 17 + constructor( pos: [ number, number ], node: NodeDefinition, id: string ){ 18 + this.name = node.name; 19 + this.id = id; 20 + this.typeId = node.typeId!; 21 + this.x = pos[0]; 22 + this.y = pos[1]; 23 + this.w = node.w!; 24 + this.h = node.h!; 25 + 26 + this.inputs = node.inputs ? node.inputs.map(( x, indx ) => { 27 + return { 28 + name: x.name, 29 + type: x.type, 30 + connections: [], 31 + parent: this, 32 + index: indx 33 + } 34 + }) : []; 35 + 36 + this.outputs = node.outputs ? node.outputs.map(( x, indx ) => { 37 + return { 38 + name: x.name, 39 + type: x.type, 40 + connections: [], 41 + parent: this, 42 + index: indx 43 + } 44 + }) : []; 45 + 46 + this.selected = false; 47 + this.statics = node.statics!, 48 + this.onStaticsUpdate = node.onStaticsUpdate!; 49 + } 50 } 51 52 export interface NodeIO{
+1 -1
src/utils/interections.ts
··· 1 import { PositionInfo } from "../renderer"; 2 3 - export let screenToWorldSpace = ( canvas: HTMLCanvasElement, position: PositionInfo, pointX: number, pointY: number ) => { 4 let startX = canvas.width / -2; 5 let startY = canvas.height / -2; 6
··· 1 import { PositionInfo } from "../renderer"; 2 3 + export let screenToWorldSpace = ( canvas: HTMLCanvasElement, position: PositionInfo, pointX: number, pointY: number ): [ number, number ] => { 4 let startX = canvas.width / -2; 5 let startY = canvas.height / -2; 6