this repo has no description

fuck it, redo entire frontend

phaz.uk 0dd169d3 79d25cc1

verified
-29
src-tauri/src/frontend_calls/get_actions.rs
··· 1 - use crate::structs::action::{ Action, ActionParameters }; 2 - 3 - #[tauri::command] 4 - pub fn get_actions() -> Vec<Action> { 5 - vec![ 6 - Action { 7 - name: "If Equals".into(), 8 - parameters: vec![ ActionParameters::AnyType, ActionParameters::Label(" = "), ActionParameters::AnyType ] 9 - } 10 - ] 11 - } 12 - 13 - #[tauri::command] 14 - pub fn get_action( name: String ) -> Option<Action> { 15 - let actions = vec![ 16 - Action { 17 - name: "If Equals".into(), 18 - parameters: vec![ ActionParameters::AnyType, ActionParameters::Label(" = "), ActionParameters::AnyType ] 19 - } 20 - ]; 21 - 22 - let action = actions.iter().find(| x | x.name == name); 23 - 24 - if action.is_some(){ 25 - Some(action.unwrap().clone()) 26 - } else{ 27 - None 28 - } 29 - }
···
+3 -1
src-tauri/src/frontend_calls/get_addresses.rs
··· 2 3 use tauri::State; 4 5 #[tauri::command] 6 - pub fn get_addresses( addresses: State<&Mutex<Vec<String>>> ) -> Vec<String> { 7 let addresses = addresses.lock().unwrap(); 8 addresses.clone() 9 }
··· 2 3 use tauri::State; 4 5 + use crate::osc::OSCMessage; 6 + 7 #[tauri::command] 8 + pub fn get_addresses( addresses: State<&Mutex<Vec<OSCMessage>>> ) -> Vec<OSCMessage> { 9 let addresses = addresses.lock().unwrap(); 10 addresses.clone() 11 }
+1 -4
src-tauri/src/frontend_calls/mod.rs
··· 1 - pub mod get_addresses; 2 - pub mod get_actions; 3 - 4 - pub mod triggers;
··· 1 + pub mod get_addresses;
-88
src-tauri/src/frontend_calls/triggers.rs
··· 1 - use serde_json::{json, Value}; 2 - use tauri::State; 3 - 4 - use crate::utils::config::Config; 5 - 6 - #[tauri::command] 7 - pub fn new_trigger( id: String, conf: State<Config> ){ 8 - if let Some(triggers) = conf.get("triggers").unwrap_or(Value::Array(Vec::new())).as_array(){ 9 - let mut triggers = triggers.clone(); 10 - triggers.push(json!({ 11 - "id": id, 12 - "address": "", 13 - "actions": [] 14 - })); 15 - 16 - conf.set("triggers", Value::Array(triggers)); 17 - conf.save(); 18 - } 19 - } 20 - 21 - #[tauri::command] 22 - pub fn rm_trigger( indx: usize, conf: State<Config> ){ 23 - if let Some(triggers) = conf.get("triggers").unwrap_or(Value::Array(Vec::new())).as_array(){ 24 - let mut triggers = triggers.clone(); 25 - triggers.remove(indx); 26 - 27 - conf.set("triggers", Value::Array(triggers)); 28 - conf.save(); 29 - } 30 - } 31 - 32 - #[tauri::command] 33 - pub fn add_trigger_action( indx: usize, action: Value, conf: State<Config> ){ 34 - if let Some(triggers) = conf.get("triggers").unwrap_or(Value::Array(Vec::new())).as_array(){ 35 - let mut triggers = triggers.clone(); 36 - 37 - let actions = triggers[indx]["actions"].as_array_mut().unwrap(); 38 - actions.push(action); 39 - 40 - conf.set("triggers", Value::Array(triggers)); 41 - conf.save(); 42 - } 43 - } 44 - 45 - #[tauri::command] 46 - pub fn rm_trigger_action( indx: usize, action_indx: usize, conf: State<Config> ){ 47 - if let Some(triggers) = conf.get("triggers").unwrap_or(Value::Array(Vec::new())).as_array(){ 48 - let mut triggers = triggers.clone(); 49 - 50 - let actions = triggers[indx]["actions"].as_array_mut().unwrap(); 51 - actions.remove(action_indx); 52 - 53 - conf.set("triggers", Value::Array(triggers)); 54 - conf.save(); 55 - } 56 - } 57 - 58 - #[tauri::command] 59 - pub fn set_trigger_action_type( indx: usize, action_indx: usize, action_type: Option<String>, conf: State<Config> ){ 60 - if let Some(triggers) = conf.get("triggers").unwrap_or(Value::Array(Vec::new())).as_array(){ 61 - let mut triggers = triggers.clone(); 62 - 63 - triggers[indx]["actions"][action_indx]["actionType"] = if action_type.is_none(){ 64 - Value::Null 65 - } else { 66 - Value::String(action_type.unwrap()) 67 - }; 68 - 69 - conf.set("triggers", Value::Array(triggers)); 70 - conf.save(); 71 - } 72 - } 73 - 74 - #[tauri::command] 75 - pub fn set_trigger_address( indx: usize, address: String, conf: State<Config> ){ 76 - if let Some(triggers) = conf.get("triggers").unwrap_or(Value::Array(Vec::new())).as_array(){ 77 - let mut triggers = triggers.clone(); 78 - triggers[indx]["address"] = Value::String(address); 79 - 80 - conf.set("triggers", Value::Array(triggers)); 81 - conf.save(); 82 - } 83 - } 84 - 85 - #[tauri::command] 86 - pub fn list_triggers( conf: State<Config> ) -> Value{ 87 - conf.get("triggers").unwrap_or(Value::Array(Vec::new())) 88 - }
···
+2 -12
src-tauri/src/lib.rs
··· 2 3 use frontend_calls::*; 4 5 - use crate::{ setup::setup, utils::config::Config }; 6 7 mod frontend_calls; 8 mod structs; ··· 29 let conf_file = container_folder.join("conf"); 30 let conf = Config::new(conf_file); 31 32 - static ADDRESSES: Mutex<Vec<String>> = Mutex::new(Vec::new()); 33 34 tauri::Builder::default() 35 .plugin(tauri_plugin_opener::init()) 36 .invoke_handler(tauri::generate_handler![ 37 get_addresses::get_addresses, 38 - get_actions::get_actions, 39 - get_actions::get_action, 40 - 41 - triggers::new_trigger, 42 - triggers::rm_trigger, 43 - triggers::add_trigger_action, 44 - triggers::rm_trigger_action, 45 - triggers::set_trigger_action_type, 46 - triggers::set_trigger_address, 47 - triggers::list_triggers, 48 ]) 49 .manage(conf) 50 .manage(&ADDRESSES)
··· 2 3 use frontend_calls::*; 4 5 + use crate::{ osc::OSCMessage, setup::setup, utils::config::Config }; 6 7 mod frontend_calls; 8 mod structs; ··· 29 let conf_file = container_folder.join("conf"); 30 let conf = Config::new(conf_file); 31 32 + static ADDRESSES: Mutex<Vec<OSCMessage>> = Mutex::new(Vec::new()); 33 34 tauri::Builder::default() 35 .plugin(tauri_plugin_opener::init()) 36 .invoke_handler(tauri::generate_handler![ 37 get_addresses::get_addresses, 38 ]) 39 .manage(conf) 40 .manage(&ADDRESSES)
+29 -21
src-tauri/src/osc.rs
··· 4 5 use serde::Serialize; 6 7 - #[derive(Debug, Clone, Serialize)] 8 - pub enum OSCValue{ 9 - Int(i32), 10 - Float(f32), 11 - Boolean(bool), 12 - String(String), 13 - } 14 15 #[derive(Debug, Clone, Serialize)] 16 pub struct OSCMessage{ 17 pub address: String, 18 - pub values: Vec<OSCValue> 19 } 20 21 - // TODO: implement osc bundles 22 pub fn start_server( sender: Sender<OSCMessage>, addr: &str ) { 23 let socket = UdpSocket::bind(addr).unwrap(); 24 ··· 68 let bytes = <&[u8; 4]>::try_from(val_buf).unwrap().clone(); 69 let int = i32::from_be_bytes(bytes); 70 71 - values.push(OSCValue::Int(int)); 72 value_start += 4; 73 }, 74 0x66 => { ··· 77 let bytes = <&[u8; 4]>::try_from(val_buf).unwrap().clone(); 78 let float = f32::from_be_bytes(bytes); 79 80 - values.push(OSCValue::Float(float)); 81 value_start += 4; 82 }, 83 - 0x54 => values.push(OSCValue::Boolean(true)), 84 - 0x46 => values.push(OSCValue::Boolean(false)), 85 _ => {} 86 } 87 } ··· 95 } 96 } 97 98 - pub fn send_message( address: &str, values: Vec<OSCValue>, ip_addr: &str ){ 99 let socket = UdpSocket::bind("127.0.0.1:0").unwrap(); 100 let mut buf: Vec<u8> = Vec::new(); 101 ··· 113 let mut value_count = 1; 114 for value in values.clone() { 115 match value { 116 - OSCValue::Boolean( val ) => buf.push(if val { 0x54 } else { 0x46 }), 117 - OSCValue::Float(_) => buf.push(0x66), 118 - OSCValue::Int(_) => buf.push(0x69), 119 - OSCValue::String(_) => buf.push(0x73) 120 }; 121 122 value_count += 1; ··· 128 129 for value in values{ 130 match value{ 131 - OSCValue::Float( val ) => buf.append(&mut val.to_be_bytes().to_vec()), 132 - OSCValue::Int( val ) => buf.append(&mut val.to_be_bytes().to_vec()), 133 - OSCValue::String( val ) => { 134 let mut str_buf = val.as_bytes().to_vec(); 135 let buf_len = str_buf.len().clone(); 136
··· 4 5 use serde::Serialize; 6 7 + use crate::structs::parameter_types::ParameterType; 8 9 #[derive(Debug, Clone, Serialize)] 10 pub struct OSCMessage{ 11 pub address: String, 12 + pub values: Vec<ParameterType> 13 + } 14 + 15 + impl PartialEq for OSCMessage{ 16 + // Technically this isn't exactly equal, but the only time i'm checking if OSCMessage's are equal 17 + // Is when i'm checking for the "address" value 18 + 19 + fn eq(&self, other: &Self) -> bool { 20 + self.address == other.address 21 + } 22 + 23 + fn ne(&self, other: &Self) -> bool { 24 + self.address != other.address 25 + } 26 } 27 28 + // TODO: Implement osc bundles 29 pub fn start_server( sender: Sender<OSCMessage>, addr: &str ) { 30 let socket = UdpSocket::bind(addr).unwrap(); 31 ··· 75 let bytes = <&[u8; 4]>::try_from(val_buf).unwrap().clone(); 76 let int = i32::from_be_bytes(bytes); 77 78 + values.push(ParameterType::Int(int)); 79 value_start += 4; 80 }, 81 0x66 => { ··· 84 let bytes = <&[u8; 4]>::try_from(val_buf).unwrap().clone(); 85 let float = f32::from_be_bytes(bytes); 86 87 + values.push(ParameterType::Float(float)); 88 value_start += 4; 89 }, 90 + 0x54 => values.push(ParameterType::Boolean(true)), 91 + 0x46 => values.push(ParameterType::Boolean(false)), 92 _ => {} 93 } 94 } ··· 102 } 103 } 104 105 + pub fn send_message( address: &str, values: Vec<ParameterType>, ip_addr: &str ){ 106 let socket = UdpSocket::bind("127.0.0.1:0").unwrap(); 107 let mut buf: Vec<u8> = Vec::new(); 108 ··· 120 let mut value_count = 1; 121 for value in values.clone() { 122 match value { 123 + ParameterType::Boolean( val ) => buf.push(if val { 0x54 } else { 0x46 }), 124 + ParameterType::Float(_) => buf.push(0x66), 125 + ParameterType::Int(_) => buf.push(0x69), 126 + ParameterType::String(_) => buf.push(0x73), 127 + _ => {} 128 }; 129 130 value_count += 1; ··· 136 137 for value in values{ 138 match value{ 139 + ParameterType::Float( val ) => buf.append(&mut val.to_be_bytes().to_vec()), 140 + ParameterType::Int( val ) => buf.append(&mut val.to_be_bytes().to_vec()), 141 + ParameterType::String( val ) => { 142 let mut str_buf = val.as_bytes().to_vec(); 143 let buf_len = str_buf.len().clone(); 144
+4 -4
src-tauri/src/setup.rs
··· 2 3 use tauri::{ App, Emitter, Manager }; 4 5 - use crate::{ osc }; 6 7 - pub fn setup( app: &mut App, addresses: &'static Mutex<Vec<String>> ){ 8 let window = app.get_webview_window("main").unwrap(); 9 10 let ( sender, receiver ) = sync::mpsc::channel(); ··· 19 20 window.emit("osc-message", &message).unwrap(); 21 22 - let addr = message.address.clone(); 23 let mut addresses = addresses.lock().unwrap(); 24 - if !addresses.contains(&addr){ addresses.push(addr); } 25 } 26 }); 27 }
··· 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(); ··· 19 20 window.emit("osc-message", &message).unwrap(); 21 22 + let msg = message.clone(); 23 let mut addresses = addresses.lock().unwrap(); 24 + if !addresses.contains(&msg){ addresses.push(msg); } 25 } 26 }); 27 }
-19
src-tauri/src/structs/action.rs
··· 1 - use serde::Serialize; 2 - 3 - #[derive(Serialize, Clone)] 4 - #[serde(tag = "key", content = "value")] 5 - pub enum ActionParameters{ 6 - AnyType, 7 - Int, 8 - String, 9 - Float, 10 - Boolean, 11 - Actions, 12 - Label(&'static str) 13 - } 14 - 15 - #[derive(Serialize, Clone)] 16 - pub struct Action{ 17 - pub name: String, 18 - pub parameters: Vec<ActionParameters> 19 - }
···
+1 -1
src-tauri/src/structs/mod.rs
··· 1 - pub mod action;
··· 1 + pub mod parameter_types;
+13
src-tauri/src/structs/parameter_types.rs
···
··· 1 + use serde::Serialize; 2 + 3 + #[derive(Serialize, Clone, Debug)] 4 + #[serde(tag = "key", content = "value")] 5 + pub enum ParameterType{ 6 + AnyType(String), 7 + Label(&'static str), 8 + 9 + Int(i32), 10 + Float(f32), 11 + Boolean(bool), 12 + String(String), 13 + }
+2 -2
src-tauri/tauri.conf.json
··· 13 "windows": [ 14 { 15 "title": "vrcmacros", 16 - "width": 1000, 17 - "height": 500 18 } 19 ], 20 "security": {
··· 13 "windows": [ 14 { 15 "title": "vrcmacros", 16 + "width": 1400, 17 + "height": 700 18 } 19 ], 20 "security": {
+8 -95
src/App.css
··· 1 - @font-face { 2 - font-family: Rubik; 3 - src: url('/assets/fonts/Rubik-VariableFont_wght.ttf'); 4 - } 5 - 6 - * { 7 - box-sizing: border-box; 8 - -webkit-user-select: none; 9 - } 10 - 11 body{ 12 - background: linear-gradient(-45deg, 13 - #12141a 0%, #12141a 10%, 14 - #22262e 10%, #22262e 20%, 15 - #272e44 20%, #272e44 22%, 16 - #1f2129 22% 17 - ); 18 background-attachment: fixed; 19 color: #fff; 20 font-family: Rubik, 'Courier New'; 21 margin: 0; 22 } 23 24 - h1, h2, h3, h4, h5, h6, p{ 25 - margin: 0; 26 - font-weight: 500; 27 } 28 29 - span{ 30 - -webkit-user-select: auto; 31 } 32 33 - div[app-carousel]{ 34 position: fixed; 35 - top: 10px; 36 - left: 220px; 37 - width: calc(100vw - 230px); 38 - height: calc(100vh - 20px); 39 - overflow: hidden; 40 - } 41 - 42 - div[app-page]{ 43 - width: calc(100vw - 230px); 44 - height: calc(100vh - 20px); 45 - overflow-y: auto; 46 - overflow-x: hidden; 47 - } 48 - 49 - div[app-button]{ 50 - display: inline-block; 51 - background: #2a3452; 52 - padding: 10px 25px; 53 - cursor: pointer; 54 - user-select: none; 55 - border-radius: 5px; 56 - transition: 0.25s; 57 - min-width: 100px; 58 - text-align: center; 59 - } 60 - 61 - div[app-button]:hover{ 62 - background: #151a29; 63 - } 64 - 65 - div[app-button-minimal]{ 66 - display: inline-block; 67 - background: #424242; 68 - padding: 10px 25px; 69 - cursor: pointer; 70 - user-select: none; 71 - border-radius: 5px; 72 - transition: 0.25s; 73 - min-width: 100px; 74 - text-align: center; 75 - opacity: 0.5; 76 - } 77 - 78 - div[app-button-minimal]:hover{ 79 - background: #525252; 80 - } 81 - 82 - div[app-col]{ 83 - display: flex; 84 - } 85 - 86 - div[app-col-50]{ 87 - display: flex; 88 - } 89 - 90 - div[app-col-50] > div{ 91 - width: 50%; 92 - } 93 - 94 - div[app-icon]{ 95 - display: flex; 96 - justify-content: center; 97 - align-items: center; 98 - width: 30px; 99 - height: 30px; 100 - user-select: none; 101 - -webkit-user-select: none; 102 - cursor: pointer; 103 - transition: 0.1s; 104 - } 105 - 106 - div[app-icon]:hover{ 107 - opacity: 0.75; 108 }
··· 1 body{ 2 + background: #1f2129; 3 background-attachment: fixed; 4 color: #fff; 5 font-family: Rubik, 'Courier New'; 6 margin: 0; 7 } 8 9 + * { 10 + box-sizing: border-box; 11 } 12 13 + p, h1, h2, h3, h4, h5, h6{ 14 + margin: 0; 15 } 16 17 + canvas{ 18 position: fixed; 19 + top: 0; 20 + left: 0; 21 }
+326 -22
src/App.tsx
··· 1 import "./App.css"; 2 3 - import { createEffect, createSignal } from "solid-js"; 4 5 - import { Sidebar } from "./Components/Sidebar"; 6 - import { Actions } from "./Components/Actions"; 7 - import { Relays } from "./Components/Relays"; 8 - import { animate } from "animejs"; 9 - import { Settings } from "./Components/Settings"; 10 - import { Debug } from "./Components/Debug"; 11 12 - let App = () => { 13 - let [ page, setPage ] = createSignal(0); 14 - let carousel!: HTMLDivElement; 15 16 - createEffect(() => { 17 - let pagenum = page(); 18 - animate(carousel.children, { translateY: '-' + ( 100 * pagenum ) + '%', ease: 'outElastic(.1, .7)', duration: 500 }); 19 - }) 20 21 return ( 22 <> 23 - <Sidebar setPage={setPage} /> 24 - 25 - <div app-carousel ref={carousel}> 26 - <Actions /> 27 - <Relays /> 28 - <Debug page={page} /> 29 - <Settings /> 30 - </div> 31 </> 32 ); 33 }
··· 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 15 + // TODO: Add undo / redo -ing 16 + let [ selectedNode, setSelectedNode ] = createSignal<Node | null>(null); 17 + 18 + let canvas!: HTMLCanvasElement; 19 + let ctx: CanvasRenderingContext2D; 20 + 21 + let stopRender = false; 22 + 23 + let scale = 0.25; 24 + let targetScale = 1; 25 + 26 + let offset = [ 0, 0 ]; 27 + let offsetTarget = [ 0, 0 ]; 28 + 29 + let movingNode: Node | null = null; 30 + 31 + let isDrawing = false; 32 + let drawingFrom: NodeIO | null = null; 33 + let drawingTo: [ number, number ] = [ 0, 0 ]; 34 + 35 + let lockMovement = false; 36 + 37 + { 38 + let loadedScale = localStorage.getItem('scale'); 39 + if(loadedScale)targetScale = parseFloat(loadedScale); 40 + 41 + let loadedOffsetX = localStorage.getItem('offsetX'); 42 + if(loadedOffsetX)offsetTarget[0] = parseFloat(loadedOffsetX); 43 + 44 + let loadedOffsetY = localStorage.getItem('offsetY'); 45 + if(loadedOffsetY)offsetTarget[1] = parseFloat(loadedOffsetY); 46 + }; 47 + 48 + let screenMoved = false; 49 + 50 + let contextMenu: ContextMenu = { 51 + items: [], 52 + position: [ 0, 0 ], 53 + size: [ 0, 0 ], 54 + visible: false 55 + } 56 + 57 + onMount(() => { 58 + ctx = canvas.getContext('2d')!; 59 + 60 + canvas.width = window.innerWidth; 61 + canvas.height = window.innerHeight; 62 + ctx.translate(canvas.width / 2, canvas.height / 2); 63 + 64 + window.onresize = () => { 65 + canvas.width = window.innerWidth; 66 + canvas.height = window.innerHeight; 67 + 68 + ctx.translate(canvas.width / 2, canvas.height / 2); 69 + } 70 71 + canvas.onwheel = ( e ) => { 72 + targetScale += e.deltaY * -(Math.sqrt(targetScale) * 0.001); 73 74 + if(targetScale < 0.25)targetScale = 0.25 75 + else if(targetScale > 5)targetScale = 5; 76 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 + } else{ 243 + offsetTarget = [ offsetTarget[0] - (mouseStartPos[0] - e.clientX) / scale, offsetTarget[1] - (mouseStartPos[1] - e.clientY) / scale ]; 244 + mouseStartPos = [ e.clientX, e.clientY ]; 245 + 246 + screenMoved = true; 247 + } 248 + } 249 + 250 + // TODO: Fix this shit lmao please 251 + if(contextMenu.visible){ 252 + let submenus: ContextMenu[] = []; 253 + contextMenu.items.map(x => x.menu ? submenus.push(x.menu): null); 254 + 255 + submenus.map(x => { 256 + if(!x.visible)return; 257 + if(isPointInRect(canvas, e.clientX, e.clientY, 258 + x.position[0], x.position[1], 259 + x.size[0], x.size[1] 260 + )){ 261 + x.items.map((y, i) => { 262 + y.hovered = isPointInRect(canvas, e.clientX, e.clientY, 263 + x.position[0], x.position[1] + 10 + 25 * i, 264 + x.size[0], 25 265 + ) 266 + }); 267 + } 268 + }); 269 + 270 + if(isPointInRect(canvas, e.clientX, e.clientY, 271 + contextMenu.position[0], contextMenu.position[1], 272 + contextMenu.size[0], contextMenu.size[1] 273 + )){ 274 + contextMenu.items.map((x, i) => { 275 + x.hovered = isPointInRect(canvas, e.clientX, e.clientY, 276 + contextMenu.position[0], contextMenu.position[1] + 10 + 25 * i, 277 + contextMenu.size[0], 25 278 + ) 279 + 280 + if(x.menu)x.menu.visible = x.hovered; 281 + }); 282 + } 283 + } 284 + } 285 + 286 + window.onmouseup = ( e ) => { 287 + NodeManager.Instance.GetNodes().map(node => { 288 + node.inputs.map(( input, i ) => { 289 + if(isPointInRectApplyOffset(canvas, { x: offset[0], y: offset[1], scale }, 290 + e.clientX, e.clientY, 291 + node.x + 10, 292 + node.y + 50 + (30 * i), 293 + 20, 20 294 + )){ 295 + if(isDrawing){ 296 + let fromType = NodeIOResolveAnyTypes(drawingFrom!); 297 + let toType = NodeIOResolveAnyTypes(input); 298 + 299 + if( 300 + drawingFrom!.connections.indexOf(input) === -1 && 301 + ( 302 + toType === null || 303 + fromType === toType 304 + ) 305 + ){ 306 + drawingFrom!.connections.push(input); 307 + input.connections.push(drawingFrom!); 308 + } 309 + } 310 + } 311 + }) 312 + }) 313 + 314 + isDrawing = false; 315 + isMouseDown = false; 316 + } 317 + 318 + let interval = setInterval(() => { 319 + if(screenMoved){ 320 + localStorage.setItem('scale', targetScale.toFixed(4)); 321 + localStorage.setItem('offsetX', offset[0].toFixed(4)); 322 + localStorage.setItem('offsetY', offset[1].toFixed(4)); 323 + } 324 + }, 1000); 325 + 326 + onCleanup(() => { 327 + stopRender = true; 328 + window.clearInterval(interval); 329 + }); 330 331 return ( 332 <> 333 + <ControlBar node={selectedNode} lockMovement={( lock ) => lockMovement = lock} /> 334 + <canvas ref={canvas}/> 335 </> 336 ); 337 }
-13
src/Components/Actions.css
··· 1 - div[app-trigger-el]{ 2 - padding: 10px; 3 - background: #272e44; 4 - margin: 10px; 5 - border-radius: 5px; 6 - } 7 - 8 - div[app-trigger-action]{ 9 - padding: 5px; 10 - margin: 5px; 11 - border-radius: 5px; 12 - background: #1c2030; 13 - }
···
-58
src/Components/Actions.tsx
··· 1 - import { For } from 'solid-js'; 2 - import './Actions.css'; 3 - import { TriggerEl } from './TriggerEl'; 4 - import { createStore } from 'solid-js/store'; 5 - import { invoke } from '@tauri-apps/api/core'; 6 - 7 - export interface Trigger{ 8 - id: string, 9 - address: string, 10 - actions: any[] 11 - } 12 - 13 - export let Actions = () => { 14 - let [ triggers, setTriggers ] = createStore<Trigger[]>([]); 15 - 16 - invoke<Trigger[]>('list_triggers').then(triggers => { setTriggers(triggers) }) 17 - 18 - return ( 19 - <div app-page> 20 - <div app-col> 21 - <div style={{ width: '100%' }}><h1>Actions</h1></div> 22 - <div app-button style={{ width: 'fit-content', "margin-left": '50%' }} onClick={() => { 23 - let id = Math.random().toString().replace('0.', ''); 24 - 25 - invoke('new_trigger', { id }); 26 - setTriggers(( trig ) => [ 27 - ...trig, 28 - { address: '', actions: [], id } 29 - ]); 30 - }}>+</div> 31 - </div> 32 - 33 - <For each={triggers}> 34 - { ( item, index ) => <TriggerEl 35 - trigger={item} 36 - onDelete={() => { 37 - invoke('rm_trigger', { indx: index() }); 38 - setTriggers(( trig ) => trig.filter(x => x.id !== item.id)); 39 - }} 40 - onAddAction={( action ) => { 41 - invoke('add_trigger_action', { indx: index(), action }); 42 - setTriggers(index(), "actions", ( actions ) => [ ...actions, action ]); 43 - }} 44 - onDeleteAction={( id, indx ) => { 45 - invoke('rm_trigger_action', { indx: index(), actionIndx: indx }); 46 - setTriggers(index(), "actions", ( actions ) => actions.filter(x => x.id !== id)) 47 - }} 48 - onSetActionType={( i, type ) => { 49 - invoke('set_trigger_action_type', { indx: index(), actionIndx: i, actionType: type }); 50 - setTriggers(index(), "actions", i, "actionType", type) 51 - }} 52 - onSetOSCAddress={( address ) => { 53 - invoke('set_trigger_address', { indx: index(), address }); 54 - }} /> } 55 - </For> 56 - </div> 57 - ) 58 - }
···
-8
src/Components/Debug.css
··· 1 - div[app-debug-el]{ 2 - padding: 10px; 3 - background: #272e44; 4 - margin: 10px; 5 - border-radius: 5px; 6 - box-shadow: #00ccff 0 0 10px; 7 - transition: 0.5s; 8 - }
···
-89
src/Components/Debug.tsx
··· 1 - import './Debug.css'; 2 - 3 - import { createEffect, onCleanup, onMount } from 'solid-js'; 4 - import { listen, UnlistenFn } from '@tauri-apps/api/event'; 5 - import { OSCMessage, OSCValue } from '../Structs/OSCMessage'; 6 - 7 - let formatValuesForDebug = ( values: OSCValue[] ): string => { 8 - let text = ''; 9 - 10 - for(let value of values){ 11 - if(value.Boolean !== undefined) 12 - text += ' Boolean: ' + value.Boolean; 13 - else if(value.Float !== undefined) 14 - text += ' Float: ' + value.Float.toFixed(6); 15 - else if(value.Int !== undefined) 16 - text += ' Int: ' + value.Int; 17 - else if(value.String !== undefined) 18 - text += ' String: ' + value.String; 19 - } 20 - 21 - return text.trimStart(); 22 - } 23 - 24 - export interface DebugProps{ 25 - page: () => number 26 - } 27 - 28 - export let Debug = ( props: DebugProps ) => { 29 - let debugContainer!: HTMLDivElement; 30 - 31 - let debugEls: any = {}; 32 - 33 - let isListening = false; 34 - let unlisten: UnlistenFn; 35 - 36 - let stopListening = () => { 37 - if(!isListening)return; 38 - isListening = false; 39 - 40 - unlisten(); 41 - } 42 - 43 - let startListening = async () => { 44 - if(isListening)return; 45 - isListening = true; 46 - 47 - unlisten = await listen<OSCMessage>('osc-message', ( ev ) => { 48 - let el = debugEls[ev.payload.address]; 49 - if(el){ 50 - el.style.boxShadow = '#00ccff 0 0 10px'; 51 - debugContainer.insertBefore(el, debugContainer.firstChild); 52 - 53 - el.innerHTML = `<div><span>${ ev.payload.address }</span></div><div>${ formatValuesForDebug(ev.payload.values) }</div>`; 54 - setTimeout(() => { el.style.boxShadow = '#00ccff 0 0 0px'; }) 55 - } else{ 56 - el = <div app-debug-el app-col-50><div><span>{ ev.payload.address }</span></div><div>{ formatValuesForDebug(ev.payload.values) }</div></div> as Node; 57 - 58 - el.style.boxShadow = '#00ccff 0 0 10px'; 59 - debugContainer.insertBefore(el, debugContainer.firstChild); 60 - 61 - setTimeout(() => { el.style.boxShadow = '#00ccff 0 0 0px'; }) 62 - debugEls[ev.payload.address] = el; 63 - } 64 - }) 65 - } 66 - 67 - onMount(() => { 68 - createEffect(() => { 69 - if(props.page() === 2) 70 - startListening(); 71 - else 72 - stopListening(); 73 - }); 74 - }); 75 - 76 - onCleanup(() => { 77 - stopListening(); 78 - }); 79 - 80 - return ( 81 - <div app-page> 82 - <h1>Debug</h1> 83 - 84 - <div ref={debugContainer}> 85 - 86 - </div> 87 - </div> 88 - ) 89 - }
···
src/Components/Relays.css

This is a binary file and will not be displayed.

-9
src/Components/Relays.tsx
··· 1 - import './Relays.css'; 2 - 3 - export let Relays = () => { 4 - return ( 5 - <div app-page> 6 - <h1>Relays</h1> 7 - </div> 8 - ) 9 - }
···
src/Components/Settings.css

This is a binary file and will not be displayed.

-9
src/Components/Settings.tsx
··· 1 - import './Settings.css'; 2 - 3 - export let Settings = () => { 4 - return ( 5 - <div app-page> 6 - <h1>Settings</h1> 7 - </div> 8 - ) 9 - }
···
-32
src/Components/Sidebar.css
··· 1 - div[app-sidebar]{ 2 - position: fixed; 3 - top: 10px; 4 - left: 10px; 5 - 6 - height: calc(100vh - 20px); 7 - width: 200px; 8 - padding: 0px; 9 - 10 - background: #272e44; 11 - border-radius: 5px; 12 - } 13 - 14 - div[app-sidebar-tab]{ 15 - padding: 10px; 16 - cursor: pointer; 17 - user-select: none; 18 - -webkit-user-select: none; 19 - transition: 0.1s; 20 - margin: 10px; 21 - border-radius: 5px; 22 - } 23 - 24 - div[app-sidebar-tab]:hover{ 25 - background: #5b6ca5; 26 - } 27 - 28 - div[app-sidebar-tab-dropped]{ 29 - position: absolute; 30 - width: calc(100% - 20px); 31 - bottom: 0px; 32 - }
···
-19
src/Components/Sidebar.tsx
··· 1 - import './Sidebar.css' 2 - 3 - export interface SidebarProps{ 4 - setPage: ( page: number ) => number 5 - } 6 - 7 - export let Sidebar = ( props: SidebarProps ) => { 8 - return ( 9 - <> 10 - <div app-sidebar> 11 - <div app-sidebar-tab onClick={() => props.setPage(0)}>Actions</div> 12 - <div app-sidebar-tab onClick={() => props.setPage(1)}>Relays</div> 13 - <div app-sidebar-tab onClick={() => props.setPage(2)}>Debug</div> 14 - 15 - <div app-sidebar-tab app-sidebar-tab-dropped onClick={() => props.setPage(3)}>Settings</div> 16 - </div> 17 - </> 18 - ) 19 - }
···
src/Components/TextInput.css src/components/TextInput.css
+7 -1
src/Components/TextInput.tsx src/components/TextInput.tsx
··· 83 onChange={() => props.change ? props.change(input.value) : null} 84 onInput={onInput} 85 onKeyDown={onKeyDown} 86 - onFocusOut={() => setSuggestionsOpen(false)} 87 ref={input} /> 88 89 <Show when={suggestionsOpen()}> ··· 94 95 input.value = thisEl.innerHTML; 96 setSuggestionsOpen(false); 97 }}>{ item }</div> } 98 </For> 99 </div>
··· 83 onChange={() => props.change ? props.change(input.value) : null} 84 onInput={onInput} 85 onKeyDown={onKeyDown} 86 + onFocusOut={() => setTimeout(() => { 87 + setSuggestionsOpen(false); 88 + suggestionsIndex = -1; 89 + }, 100)} 90 ref={input} /> 91 92 <Show when={suggestionsOpen()}> ··· 97 98 input.value = thisEl.innerHTML; 99 setSuggestionsOpen(false); 100 + 101 + props.change ? props.change(input.value) : null 102 + suggestionsIndex = -1; 103 }}>{ item }</div> } 104 </For> 105 </div>
-81
src/Components/TriggerEl.tsx
··· 1 - import { For } from "solid-js" 2 - import { Trigger } from "./Actions" 3 - import { TextInput } from "./TextInput" 4 - import { invoke } from "@tauri-apps/api/core" 5 - import { ActionType } from "../Structs/ActionType" 6 - 7 - export interface TriggerElProps{ 8 - trigger: Trigger, 9 - 10 - onDelete: () => void, 11 - 12 - onAddAction: ( value: any ) => void, 13 - onDeleteAction: ( id: string, index: number ) => void, 14 - 15 - onSetActionType: ( index: number, type: string | null ) => void, 16 - 17 - onSetOSCAddress: ( address: string ) => void, 18 - } 19 - 20 - export let TriggerEl = ( { trigger, onDelete, onAddAction, onDeleteAction, onSetActionType, onSetOSCAddress }: TriggerElProps ) => { 21 - let suggestOSCAddresses = async ( text: string ): Promise<string[]> => { 22 - let addresses = await invoke<string[]>('get_addresses'); 23 - return addresses.filter(x => x.toLowerCase().includes(text.toLowerCase())); 24 - } 25 - 26 - let suggestActionNames = async ( text: string ): Promise<string[]> => { 27 - let actions = await invoke<ActionType[]>('get_actions'); 28 - return actions.filter(x => x.name.toLowerCase().includes(text.toLowerCase())).map(x => x.name); 29 - } 30 - 31 - return ( 32 - <div app-trigger-el> 33 - <div app-col> 34 - OSC Address: 35 - <div style={{ width: '400px', display: 'inline-block', "margin-left": '10px' }}> 36 - <TextInput 37 - placeholder="/avatar/parameters/MyValue" 38 - value={ trigger.address } 39 - requestSuggestions={suggestOSCAddresses} 40 - change={onSetOSCAddress} /> 41 - </div> 42 - <div app-icon style={{ 'margin-left': 'calc(100% - 545px)' }} onClick={onDelete}> 43 - <img src="/assets/icons/trash-can-solid-full.svg" width="18" /> 44 - </div> 45 - </div> 46 - 47 - <br /> 48 - <For each={trigger.actions}> 49 - { ( item, index ) => <div app-trigger-action> 50 - <div app-col> 51 - <div style={{ width: 'calc(100% - 40px)', height: '30px', display: 'flex', "justify-content": 'center', 'align-items': 'center' }}> 52 - <TextInput 53 - placeholder="Search Actions..." 54 - requestSuggestions={suggestActionNames} 55 - value={item.actionType} 56 - change={async ( text: string ) => { 57 - let action = await invoke<ActionType>('get_action', { name: text }); 58 - if(action)onSetActionType(index(), action.name); 59 - else onSetActionType(index(), null); 60 - }} /> 61 - </div> 62 - <div app-icon style={{ width: '40px' }} onClick={() => { 63 - onDeleteAction(item.id, index()); 64 - }}> 65 - <img src="/assets/icons/trash-can-solid-full.svg" width="18" /> 66 - </div> 67 - </div> 68 - 69 - <div> 70 - 71 - </div> 72 - </div> } 73 - </For> 74 - 75 - <br /> 76 - <div app-button onClick={() => { 77 - onAddAction({ id: Math.random().toString().replace('0.', '') }) 78 - }}>Add Action +</div> 79 - </div> 80 - ) 81 - }
···
+307
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: 'trigger', 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 + })(); 98 + } 99 + }; 100 + 101 + NodeManager.Instance.AddNode(node); 102 + }, 103 + hovered: false 104 + }, 105 + 106 + { 107 + text: "Conditional", 108 + menu: { 109 + items: [ 110 + { 111 + text: "If Equals", 112 + hovered: false, 113 + clicked: ( e: MouseEvent, canvas: HTMLCanvasElement, position: PositionInfo ) => { 114 + let pos = screenToWorldSpace(canvas, position, e.clientX, e.clientY); 115 + 116 + let node: Node = { 117 + name: 'If Equals', 118 + id: 'ifeq', 119 + x: pos[0], 120 + y: pos[1], 121 + w: 220, 122 + h: 150, 123 + inputs: [], 124 + outputs: [], 125 + selected: false, 126 + statics: [], 127 + onStaticsUpdate: ( _node ) => {} 128 + }; 129 + 130 + node.inputs.push({ 131 + name: "Flow", 132 + type: NodeType.Flow, 133 + connections: [], 134 + parent: node, 135 + index: 0 136 + }); 137 + 138 + node.inputs.push({ 139 + name: "Input 1", 140 + type: NodeType.AnyTypeA, 141 + connections: [], 142 + parent: node, 143 + index: 1 144 + }); 145 + 146 + node.inputs.push({ 147 + name: "Input 2", 148 + type: NodeType.AnyTypeA, 149 + connections: [], 150 + parent: node, 151 + index: 2 152 + }); 153 + 154 + 155 + node.outputs.push({ 156 + name: "Equal", 157 + type: NodeType.Flow, 158 + connections: [], 159 + parent: node, 160 + index: 0 161 + }); 162 + 163 + node.outputs.push({ 164 + name: "Not Equal", 165 + type: NodeType.Flow, 166 + connections: [], 167 + parent: node, 168 + index: 1 169 + }); 170 + 171 + NodeManager.Instance.AddNode(node); 172 + } 173 + }, 174 + ], 175 + position: [ 0, 0 ], 176 + size: [ 0, 0 ], 177 + visible: true 178 + }, 179 + hovered: false 180 + }, 181 + 182 + { 183 + text: "Statics", 184 + menu: { 185 + items: [ 186 + { 187 + text: "String", 188 + hovered: false, 189 + clicked: ( e: MouseEvent, canvas: HTMLCanvasElement, position: PositionInfo ) => { 190 + let pos = screenToWorldSpace(canvas, position, e.clientX, e.clientY); 191 + 192 + let node: Node = { 193 + name: 'String', 194 + id: 'static-string', 195 + x: pos[0], 196 + y: pos[1], 197 + w: 200, 198 + h: 85, 199 + inputs: [], 200 + outputs: [], 201 + selected: false, 202 + statics: [], 203 + onStaticsUpdate: ( _node ) => {} 204 + }; 205 + 206 + node.outputs.push({ 207 + name: "String", 208 + type: NodeType.String, 209 + connections: [], 210 + parent: node, 211 + index: 0 212 + }); 213 + 214 + NodeManager.Instance.AddNode(node); 215 + } 216 + }, 217 + 218 + { 219 + text: "Int", 220 + hovered: false, 221 + clicked: ( e: MouseEvent, canvas: HTMLCanvasElement, position: PositionInfo ) => { 222 + let pos = screenToWorldSpace(canvas, position, e.clientX, e.clientY); 223 + 224 + let node: Node = { 225 + name: 'Int', 226 + id: 'static-int', 227 + x: pos[0], 228 + y: pos[1], 229 + w: 200, 230 + h: 85, 231 + inputs: [], 232 + outputs: [], 233 + selected: false, 234 + statics: [], 235 + onStaticsUpdate: ( _node ) => {} 236 + }; 237 + 238 + node.outputs.push({ 239 + name: "Int", 240 + type: NodeType.Int, 241 + connections: [], 242 + parent: node, 243 + index: 0 244 + }); 245 + 246 + NodeManager.Instance.AddNode(node); 247 + } 248 + }, 249 + ], 250 + position: [ 0, 0 ], 251 + size: [ 0, 0 ], 252 + visible: true 253 + }, 254 + hovered: false 255 + }, 256 + 257 + { 258 + text: "OSC Actions", 259 + menu: { 260 + items: [ 261 + { 262 + text: "Send Chatbox", 263 + hovered: false, 264 + clicked: ( e: MouseEvent, canvas: HTMLCanvasElement, position: PositionInfo ) => { 265 + let pos = screenToWorldSpace(canvas, position, e.clientX, e.clientY); 266 + 267 + let node: Node = { 268 + name: 'Send Chatbox', 269 + id: 'send-chatbox', 270 + x: pos[0], 271 + y: pos[1], 272 + w: 200, 273 + h: 120, 274 + inputs: [], 275 + outputs: [], 276 + selected: false, 277 + statics: [], 278 + onStaticsUpdate: ( _node ) => {} 279 + }; 280 + 281 + node.inputs.push({ 282 + name: "Flow", 283 + type: NodeType.Flow, 284 + connections: [], 285 + parent: node, 286 + index: 0 287 + }); 288 + 289 + node.inputs.push({ 290 + name: "Value", 291 + type: NodeType.String, 292 + connections: [], 293 + parent: node, 294 + index: 1 295 + }); 296 + 297 + NodeManager.Instance.AddNode(node); 298 + } 299 + }, 300 + ], 301 + position: [ 0, 0 ], 302 + size: [ 0, 0 ], 303 + visible: true 304 + }, 305 + hovered: false 306 + }, 307 + ]
+26
src/ContextMenu/Node.tsx
···
··· 1 + import { NodeManager } from "../Mangers/NodeManager"; 2 + import { PositionInfo } from "../renderer"; 3 + import { Node } from "../structs/node"; 4 + 5 + export let NodeContextMenu = ( clickedNode: Node ) => [ 6 + { 7 + text: "Delete Node", 8 + clicked: ( _e: MouseEvent, _canvas: HTMLCanvasElement, _position: PositionInfo ) => { 9 + clickedNode!.inputs.map(input => { 10 + input.connections.map(partner => { 11 + partner.connections = partner.connections.filter(x => x != input); 12 + }) 13 + }) 14 + 15 + clickedNode!.outputs.map(output => { 16 + output.connections.map(partner => { 17 + partner.connections = partner.connections.filter(x => x != output); 18 + }) 19 + }) 20 + 21 + // TODO: If node is currently selected, deselect it. 22 + NodeManager.Instance.RemoveNode(clickedNode!) 23 + }, 24 + hovered: false 25 + } 26 + ]
+29
src/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; 11 + padding: 10px 20px; 12 + align-items: center; 13 + } 14 + 15 + .button{ 16 + padding: 5px 10px; 17 + margin: 0 10px; 18 + background: #445077; 19 + border-radius: 5px; 20 + transition: 0.1s; 21 + cursor: pointer; 22 + user-select: none; 23 + -webkit-user-select: none; 24 + width: fit-content; 25 + } 26 + 27 + .button:hover{ 28 + background: #363f5e; 29 + }
+92
src/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 + 10 + export interface ControlBarProps{ 11 + node: Accessor<Node | null>, 12 + lockMovement: ( lock: boolean ) => void 13 + } 14 + 15 + export let ControlBar = ( props: ControlBarProps ) => { 16 + return ( 17 + <div class="control-bar"> 18 + <For each={props.node()?.statics}> 19 + { ( item ) => { 20 + let [ popupOpen, setPopupOpen ] = createSignal(false); 21 + 22 + return ( 23 + <div> 24 + <Switch> 25 + <Match when={item.type == NodeType.String}> 26 + { item.name } 27 + <div style={{ display: 'inline-block', 'margin-left': '10px' }}> 28 + 29 + </div> 30 + </Match> 31 + <Match when={item.type == NodeType.Int}> 32 + { item.name } 33 + <div style={{ display: 'inline-block', 'margin-left': '10px' }}> 34 + 35 + </div> 36 + </Match> 37 + <Match when={item.type == NodeType.Float}> 38 + { item.name } 39 + <div style={{ display: 'inline-block', 'margin-left': '10px' }}> 40 + 41 + </div> 42 + </Match> 43 + <Match when={item.type == NodeType.OSCAddress}> 44 + { item.name } 45 + <div style={{ display: 'inline-block', 'margin-left': '10px', width: '300px' }}> 46 + <TextInput 47 + placeholder='Enter OSC Address...' 48 + value={item.value || ''} 49 + requestSuggestions={async ( text: string ): Promise<string[]> => { 50 + let addresses = await invoke<OSCMessage[]>('get_addresses'); 51 + return addresses.map(x => x.address).filter(x => x.toLowerCase().includes(text.toLowerCase())); 52 + }} 53 + change={( text ) => { 54 + let node = props.node()!; 55 + 56 + item.value = text; 57 + node.onStaticsUpdate(node); 58 + }} /> 59 + </div> 60 + </Match> 61 + <Match when={item.type == NodeType.ParameterList}> 62 + <div class="button" onClick={() => { 63 + let popup = !popupOpen(); 64 + 65 + props.lockMovement(popup); 66 + setPopupOpen(popup); 67 + }}> 68 + { item.name } 69 + </div> 70 + <Show when={popupOpen()}> 71 + <ParameterList 72 + setPopupOpen={( open: boolean ) => { 73 + setPopupOpen(open); 74 + props.lockMovement(open); 75 + }} 76 + value={item.value} 77 + changed={( value ) => { 78 + let node = props.node()!; 79 + 80 + item.value = value; 81 + node.onStaticsUpdate(node); 82 + }} /> 83 + </Show> 84 + </Match> 85 + </Switch> 86 + </div> 87 + ) 88 + }} 89 + </For> 90 + </div> 91 + ) 92 + }
+23
src/Mangers/NodeManager.tsx
···
··· 1 + import { Node } from "../structs/node"; 2 + 3 + export class NodeManager{ 4 + public static Instance: NodeManager; 5 + 6 + private _nodes: Node[] = []; 7 + 8 + constructor(){ 9 + NodeManager.Instance = this; 10 + } 11 + 12 + public AddNode( node: Node ){ 13 + this._nodes.push(node); 14 + } 15 + 16 + public RemoveNode( node: Node ){ 17 + this._nodes = this._nodes.filter(x => x !== node); 18 + } 19 + 20 + public GetNodes(): Node[]{ 21 + return this._nodes; 22 + } 23 + }
-4
src/Structs/ActionType.ts
··· 1 - export interface ActionType{ 2 - name: string, 3 - parameters: any[] 4 - }
···
-11
src/Structs/OSCMessage.ts
··· 1 - export interface OSCMessage{ 2 - address: string, 3 - values: OSCValue[] 4 - } 5 - 6 - export interface OSCValue{ 7 - Float?: number, 8 - Int?: number, 9 - Boolean?: boolean, 10 - String?: string 11 - }
···
+62
src/components/ParameterList.css
···
··· 1 + .parameter-list{ 2 + position: fixed; 3 + z-index: 100; 4 + top: 0; 5 + left: 0; 6 + width: 100vw; 7 + height: 100vh; 8 + background: rgba(0, 0, 0, 0.75); 9 + } 10 + 11 + .parameter-list-inner{ 12 + position: fixed; 13 + top: 50%; 14 + left: 50%; 15 + transform: translate(-50%, -50%); 16 + padding: 10px; 17 + background: #373738; 18 + border-radius: 10px; 19 + width: 40%; 20 + height: 80%; 21 + } 22 + 23 + .parameter-list-close{ 24 + width: 25px; 25 + height: 43px; 26 + display: flex; 27 + justify-content: center; 28 + align-items: center; 29 + } 30 + 31 + .parameter-list-header{ 32 + width: 100%; 33 + height: 50px; 34 + } 35 + 36 + .parameter-list-content{ 37 + width: 100%; 38 + height: calc(100% - 50px); 39 + overflow-x: hidden; 40 + overflow-y: auto; 41 + } 42 + 43 + .parameter-list-button-dropdown{ 44 + position: fixed; 45 + padding: 5px 10px; 46 + margin: 0 10px; 47 + background: #445077; 48 + border-radius: 5px; 49 + transition: 0.1s; 50 + cursor: pointer; 51 + user-select: none; 52 + transform: translate(0, 5px); 53 + -webkit-user-select: none; 54 + } 55 + 56 + .parameter-list-button-dropdown > div{ 57 + transition: 0.1s; 58 + } 59 + 60 + .parameter-list-button-dropdown > div:hover{ 61 + color: #aaa; 62 + }
+63
src/components/ParameterList.tsx
···
··· 1 + import { createSignal, For, Show } from 'solid-js'; 2 + import './ParameterList.css'; 3 + 4 + export interface ParameterListProps{ 5 + setPopupOpen: ( open: boolean ) => void 6 + value: { type: string, desc: string }[], 7 + changed: ( value: { type: string, desc: string }[] ) => void 8 + } 9 + 10 + export let ParameterList = ( props: ParameterListProps ) => { 11 + let [ parameters, setParameters ] = createSignal<{ type: string, desc: string }[]>(props.value, { equals: false }); 12 + let [ addParametersOpen, setAddParametersOpen ] = createSignal(false); 13 + 14 + return ( 15 + <div class="parameter-list"> 16 + <div class="parameter-list-inner"> 17 + <div class="parameter-list-header"> 18 + <h1 style={{ float: 'left' }}>Parameter List</h1> 19 + <div style={{ float: 'right' }} class="parameter-list-close"> 20 + <div style={{ background: 'red', width: '25px', height: '25px', cursor: 'pointer' }} onClick={() => props.setPopupOpen(false)}></div> 21 + </div> 22 + </div> 23 + <div class="parameter-list-content"> 24 + <For each={parameters()}> 25 + { i => <div>{ JSON.stringify(i) }</div>} 26 + </For> 27 + <div class="button" onClick={() => { setAddParametersOpen(!addParametersOpen()) }}>Add Parameter + </div> 28 + <Show when={addParametersOpen()}> 29 + <div class="parameter-list-button-dropdown"> 30 + <div onClick={() => { 31 + setAddParametersOpen(false); 32 + 33 + let params = parameters(); 34 + params.push({ type: 'Float', desc: '' }); 35 + 36 + setParameters(params); 37 + props.changed(params); 38 + }}>Float Parameter</div> 39 + <div onClick={() => { 40 + setAddParametersOpen(false); 41 + 42 + let params = parameters(); 43 + params.push({ type: 'Int', desc: '' }); 44 + 45 + setParameters(params); 46 + props.changed(params); 47 + }}>Integer Parameter</div> 48 + <div onClick={() => { 49 + setAddParametersOpen(false); 50 + 51 + let params = parameters(); 52 + params.push({ type: 'Boolean', desc: '' }); 53 + 54 + setParameters(params); 55 + props.changed(params); 56 + }}>Boolean Parameter</div> 57 + </div> 58 + </Show> 59 + </div> 60 + </div> 61 + </div> 62 + ) 63 + }
+4
src/index.tsx
··· 2 import { render } from "solid-js/web"; 3 import App from "./App"; 4 5 render(() => <App />, document.getElementById("root") as HTMLElement);
··· 2 import { render } from "solid-js/web"; 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();
+240
src/renderer.ts
···
··· 1 + import { ContextMenu } from "./structs/ContextMenu"; 2 + import { Node, NodeIO, NodeIOLinkColours } from "./structs/node"; 3 + import { lerp } from "./utils/lerp"; 4 + 5 + export interface PositionInfo{ 6 + x: number, 7 + y: number, 8 + scale: number 9 + } 10 + 11 + const GRID_SIZE = 50; 12 + 13 + export let renderBackgroundGrid = ( 14 + canvas: HTMLCanvasElement, 15 + ctx: CanvasRenderingContext2D, 16 + position: PositionInfo 17 + ) => { 18 + let offsetX = position.x % 50; 19 + let offsetY = position.y % 50; 20 + 21 + let gridAmountX = canvas.width / (GRID_SIZE * position.scale); 22 + let gridAmountY = canvas.height / (GRID_SIZE * position.scale); 23 + 24 + ctx.fillStyle = '#fff1'; 25 + 26 + for (let x = 0; x < gridAmountX / 2; x++) { 27 + for (let y = 0; y < gridAmountY / 2; y++) { 28 + ctx.fillRect( 29 + ((x * GRID_SIZE) + offsetX) * position.scale, 30 + ((y * GRID_SIZE) + offsetY) * position.scale, 31 + 5 * position.scale, 5 * position.scale); 32 + 33 + ctx.fillRect( 34 + (((x + 1) * GRID_SIZE) - offsetX) * -position.scale, 35 + ((y * GRID_SIZE) + offsetY) * position.scale, 36 + 5 * position.scale, 5 * position.scale); 37 + 38 + ctx.fillRect( 39 + ((x * GRID_SIZE) + offsetX) * position.scale, 40 + (((y + 1) * GRID_SIZE) - offsetY) * -position.scale, 41 + 5 * position.scale, 5 * position.scale); 42 + 43 + ctx.fillRect( 44 + (((x + 1) * GRID_SIZE) - offsetX) * -position.scale, 45 + (((y + 1) * GRID_SIZE) - offsetY) * -position.scale, 46 + 5 * position.scale, 5 * position.scale); 47 + } 48 + } 49 + } 50 + 51 + export let renderNodes = ( 52 + canvas: HTMLCanvasElement, 53 + ctx: CanvasRenderingContext2D, 54 + nodes: Node[], 55 + position: PositionInfo 56 + ) => { 57 + let startX = canvas.width / -2; 58 + let startY = canvas.height / -2; 59 + 60 + ctx.textBaseline = 'top'; 61 + 62 + nodes.map(node => { 63 + ctx.fillStyle = '#1f2129'; 64 + ctx.strokeStyle = node.selected ? '#004696ff' : '#fff5'; 65 + ctx.lineWidth = 5 * position.scale; 66 + 67 + // Draw Node Box 68 + drawRoundedRect(ctx, 69 + (node.x + startX + position.x) * position.scale, 70 + (node.y + startY + position.y) * position.scale, 71 + node.w * position.scale, 72 + node.h * position.scale, 73 + 10 * position.scale); 74 + 75 + ctx.stroke(); 76 + ctx.fill(); 77 + 78 + // Draw Node Name 79 + ctx.fillStyle = '#fff'; 80 + ctx.font = (25 * position.scale) + 'px Comic Mono'; 81 + ctx.textAlign = 'center'; 82 + 83 + ctx.fillText(node.name, 84 + (node.x + (node.w * 0.5) + startX + position.x) * position.scale, 85 + (node.y + 10 + startY + position.y) * position.scale 86 + ); 87 + 88 + // Draw Inputs 89 + ctx.font = (15 * position.scale) + 'px Comic Mono'; 90 + ctx.textAlign = 'left'; 91 + 92 + node.inputs.map(( input, i ) => { 93 + ctx.fillStyle = NodeIOLinkColours(input); 94 + ctx.fillRect( 95 + (node.x + 10 + startX + position.x) * position.scale, 96 + (node.y + 50 + (30 * i) + startY + position.y) * position.scale, 97 + 20 * position.scale, 20 * position.scale 98 + ) 99 + 100 + ctx.fillText(input.name, 101 + (node.x + 35 + startX + position.x) * position.scale, 102 + (node.y + 53 + (30 * i) + startY + position.y) * position.scale, 103 + ) 104 + }) 105 + 106 + // Draw Outputs 107 + ctx.textAlign = 'right'; 108 + 109 + node.outputs.map(( output, i ) => { 110 + ctx.fillStyle = NodeIOLinkColours(output); 111 + ctx.fillRect( 112 + (node.x + (node.w - 30) + startX + position.x) * position.scale, 113 + (node.y + 50 + (30 * i) + startY + position.y) * position.scale, 114 + 20 * position.scale, 20 * position.scale 115 + ) 116 + 117 + ctx.fillText(output.name, 118 + (node.x + (node.w - 35) + startX + position.x) * position.scale, 119 + (node.y + 53 + (30 * i) + startY + position.y) * position.scale, 120 + ) 121 + }) 122 + }) 123 + 124 + nodes.map(node => { 125 + node.outputs.map(( output, i ) => { 126 + output.connections.map(partner => { 127 + ctx.strokeStyle = NodeIOLinkColours(output); 128 + drawCurve(ctx, 129 + (node.x + (node.w - 30) + 10 + startX + position.x) * position.scale, 130 + (node.y + 50 + (30 * i) + 10 + startY + position.y) * position.scale, 131 + (partner.parent.x + 20 + startX + position.x) * position.scale, 132 + (partner.parent.y + 60 + (30 * partner.index) + startY + position.y) * position.scale, 133 + ); 134 + ctx.stroke(); 135 + }) 136 + }) 137 + }) 138 + } 139 + 140 + export let renderContextMenu = ( 141 + ctx: CanvasRenderingContext2D, 142 + contextMenu: ContextMenu 143 + ) => { 144 + if(contextMenu.visible){ 145 + ctx.font = '20px Arial'; 146 + ctx.textBaseline = 'top'; 147 + ctx.textAlign = 'left'; 148 + 149 + let widestItem = 0; 150 + contextMenu.items.map(x => { 151 + let width = ctx.measureText(x.text).width; 152 + if(widestItem < width)widestItem = width; 153 + }); 154 + 155 + contextMenu.size = [ widestItem + 20, 25 * contextMenu.items.length + 20 ] 156 + 157 + drawRoundedRect(ctx, contextMenu.position[0], contextMenu.position[1], contextMenu.size[0], contextMenu.size[1], 10); 158 + ctx.fillStyle = '#444'; 159 + ctx.fill(); 160 + 161 + let submenuToRender: any = null; 162 + 163 + contextMenu.items.map((x, i) => { 164 + ctx.fillStyle = x.hovered ? '#aaa' : '#fff'; 165 + ctx.fillText(x.text, contextMenu.position[0] + 10, contextMenu.position[1] + 10 + 25 * i); 166 + 167 + if(x.hovered && x.menu){ 168 + submenuToRender = x.menu; 169 + submenuToRender.position = [ contextMenu.position[0] + contextMenu.size[0] + 5, contextMenu.position[1] + 25 * i ]; 170 + } 171 + }); 172 + 173 + if(submenuToRender){ 174 + renderContextMenu(ctx, submenuToRender); 175 + } 176 + } 177 + } 178 + 179 + export let renderTempDrawing = ( 180 + canvas: HTMLCanvasElement, 181 + ctx: CanvasRenderingContext2D, 182 + drawingTo: [ number, number ], 183 + drawingFrom: NodeIO, 184 + position: PositionInfo 185 + ) => { 186 + let startX = canvas.width / -2; 187 + let startY = canvas.height / -2; 188 + 189 + ctx.fillStyle = '#f00'; 190 + 191 + ctx.fillRect( 192 + (drawingTo[0] + 10 + startX + position.x) * position.scale, 193 + (drawingTo[1] + 10 + startY + position.y) * position.scale, 194 + 10, 10 195 + ); 196 + 197 + ctx.fillRect( 198 + (drawingFrom.parent.x + (drawingFrom.parent.w - 30) + 10 + startX + position.x) * position.scale, 199 + (drawingFrom.parent.y + 50 + (30 * drawingFrom.index) + 10 + startY + position.y) * position.scale, 200 + 10, 10 201 + ); 202 + 203 + ctx.strokeStyle = NodeIOLinkColours(drawingFrom); 204 + drawCurve(ctx, 205 + (drawingFrom.parent.x + (drawingFrom.parent.w - 30) + 10 + startX + position.x) * position.scale, 206 + (drawingFrom.parent.y + 50 + (30 * drawingFrom.index) + 10 + startY + position.y) * position.scale, 207 + (drawingTo[0] + 10 + startX + position.x) * position.scale, 208 + (drawingTo[1] + 10 + startY + position.y) * position.scale, 209 + ); 210 + ctx.stroke(); 211 + } 212 + 213 + let drawCurve = ( ctx: CanvasRenderingContext2D, fromX: number, fromY: number, toX: number, toY: number ) => { 214 + ctx.beginPath(); 215 + 216 + let bias = Math.sqrt(( fromX - toX ) * ( fromX - toX ) + ( fromY - toY ) * ( fromY - toY )) / 3; 217 + 218 + let start = [ fromX + bias, fromY ]; 219 + let end = [ toX - bias, toY ]; 220 + 221 + let midpoint = [ 222 + lerp(start[0], end[0], 0.5), 223 + lerp(start[1], end[1], 0.5) 224 + ]; 225 + 226 + ctx.bezierCurveTo(fromX, fromY, start[0], start[1], midpoint[0], midpoint[1]); 227 + ctx.bezierCurveTo(midpoint[0], midpoint[1], end[0], end[1], toX, toY); 228 + } 229 + 230 + let drawRoundedRect = ( ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, radius: number ) => { 231 + ctx.beginPath(); 232 + ctx.arc(x + radius, y + radius, radius, Math.PI, Math.PI * 1.5); 233 + ctx.lineTo(x + w - radius, y); 234 + ctx.arc(x + w - radius, y + radius, radius, Math.PI * 1.5, 0); 235 + ctx.lineTo(x + w, y + h - radius); 236 + ctx.arc(x + w - radius, y + h - radius, radius, 0, Math.PI * 0.5); 237 + ctx.lineTo(x + radius, y + h); 238 + ctx.arc(x + radius, y + h - radius, radius, Math.PI * 0.5, Math.PI); 239 + ctx.closePath(); 240 + }
+16
src/structs/ContextMenu.ts
···
··· 1 + import { PositionInfo } from "../renderer"; 2 + import { Node } from "./node"; 3 + 4 + export interface ContextMenuItem{ 5 + text: string, 6 + hovered: boolean, 7 + clicked?: ( e: MouseEvent, canvas: HTMLCanvasElement, pos: PositionInfo, clickedNode?: Node ) => void, 8 + menu?: ContextMenu 9 + } 10 + 11 + export interface ContextMenu{ 12 + items: ContextMenuItem[]; 13 + position: [ number, number ]; 14 + size: [ number, number ]; 15 + visible: boolean; 16 + }
+9
src/structs/OscMessage.ts
···
··· 1 + export interface OSCMessage{ 2 + address: string, 3 + values: OSCValue[] 4 + } 5 + 6 + export interface OSCValue{ 7 + key: string, 8 + value: any 9 + }
+88
src/structs/node.ts
···
··· 1 + export interface Node{ 2 + name: string, 3 + id: string, 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{ 16 + name: string, 17 + type: NodeType, 18 + connections: NodeIO[], 19 + parent: Node, 20 + index: number 21 + } 22 + 23 + export enum NodeType{ 24 + Label, 25 + 26 + String = 1, 27 + Float = 2, 28 + Int = 3, 29 + Boolean = 4, 30 + Flow = 5, 31 + 32 + AnyTypeA = 6, 33 + AnyTypeB = 7, 34 + AnyTypeC = 8, 35 + 36 + OSCAddress, 37 + ParameterList 38 + } 39 + 40 + export let NodeIOResolveAnyTypes = ( nodeio: NodeIO ): NodeType | null => { 41 + if(nodeio.type > 0 && nodeio.type < 6){ 42 + // It's a base type 43 + return nodeio.type; 44 + } 45 + 46 + // It's an "AnyType" value and we should resolve it, 47 + // it also means it's an input as "AnyType" is not valid on outputs 48 + let type = nodeio.type; 49 + 50 + // Check if we have any connections 51 + if(nodeio.connections.length > 0){ 52 + // We do, lets copy the type of the first input 53 + return nodeio.connections[0].type; 54 + } 55 + 56 + // Check if there are any others of the same "AnyType" 57 + let other = nodeio.parent.inputs.filter(x => x !== nodeio).find(x => x.type === type); 58 + if(other){ 59 + // There are others with the same type, lets copy that type 60 + // Does other have any connections 61 + 62 + if(other.connections.length > 0){ 63 + return other.connections[0].type; 64 + } 65 + } 66 + 67 + // We can't resolve it yet 68 + return null; 69 + } 70 + 71 + export let NodeIOLinkColours = ( nodeio: NodeIO ) => { 72 + let cols: any = { 73 + 1: '#ffff9f', 74 + 2: '#cda0cb', 75 + 3: '#7ecaca', 76 + 4: '#8bc0a2', 77 + 5: '#edeae3' 78 + } 79 + 80 + let type = NodeIOResolveAnyTypes(nodeio); 81 + return type ? cols[type] : '#fff5'; 82 + } 83 + 84 + export interface NodeStatic{ 85 + name: string, 86 + type: NodeType, 87 + value: any 88 + }
+40
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 + 7 + let worldX = ((pointX + startX) / position.scale) - position.x - startX; 8 + let worldY = ((pointY + startY) / position.scale) - position.y - startY; 9 + 10 + return [ worldX, worldY ]; 11 + } 12 + 13 + export let isPointInRectApplyOffset = ( canvas: HTMLCanvasElement, position: PositionInfo, pointX: number, pointY: number, rectX: number, rectY: number, rectW: number, rectH: number ): boolean => { 14 + let startX = canvas.width / -2; 15 + let startY = canvas.height / -2; 16 + 17 + let screenPointX = (pointX + startX); 18 + let screenPointY = (pointY + startY); 19 + 20 + let rectScreenX = (rectX + startX + position.x) * position.scale; 21 + let rectScreenY = (rectY + startY + position.y) * position.scale; 22 + let rectScreenW = rectW * position.scale; 23 + let rectScreenH = rectH * position.scale; 24 + 25 + return ( 26 + screenPointX > rectScreenX && 27 + screenPointX < rectScreenX + rectScreenW && 28 + screenPointY > rectScreenY && 29 + screenPointY < rectScreenY + rectScreenH 30 + ) 31 + } 32 + 33 + export let isPointInRect = ( canvas: HTMLCanvasElement, pointX: number, pointY: number, rectX: number, rectY: number, rectW: number, rectH: number ): boolean => { 34 + return ( 35 + pointX > canvas.width / 2 + rectX && 36 + pointX < canvas.width / 2 + rectX + rectW && 37 + pointY > canvas.height / 2 + rectY && 38 + pointY < canvas.height / 2 + rectY + rectH 39 + ) 40 + }
+1
src/utils/lerp.ts
···
··· 1 + export let lerp = ( a: number, b: number, t: number ): number => a + ( b - a ) * t;