-29
src-tauri/src/frontend_calls/get_actions.rs
-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
+3
-1
src-tauri/src/frontend_calls/get_addresses.rs
+1
-4
src-tauri/src/frontend_calls/mod.rs
+1
-4
src-tauri/src/frontend_calls/mod.rs
-88
src-tauri/src/frontend_calls/triggers.rs
-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
-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
+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
+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
-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
-
}
···
+13
src-tauri/src/structs/parameter_types.rs
+13
src-tauri/src/structs/parameter_types.rs
+2
-2
src-tauri/tauri.conf.json
+2
-2
src-tauri/tauri.conf.json
+8
-95
src/App.css
+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
+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
-13
src/Components/Actions.css
-58
src/Components/Actions.tsx
-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
-8
src/Components/Debug.css
-89
src/Components/Debug.tsx
-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
src/Components/Relays.css
This is a binary file and will not be displayed.
-9
src/Components/Relays.tsx
-9
src/Components/Relays.tsx
src/Components/Settings.css
src/Components/Settings.css
This is a binary file and will not be displayed.
-9
src/Components/Settings.tsx
-9
src/Components/Settings.tsx
-32
src/Components/Sidebar.css
-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
-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
src/Components/TextInput.css
src/components/TextInput.css
+7
-1
src/Components/TextInput.tsx
src/components/TextInput.tsx
+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
-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
+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
+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
+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
+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
+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
-4
src/Structs/ActionType.ts
-11
src/Structs/OSCMessage.ts
-11
src/Structs/OSCMessage.ts
+62
src/components/ParameterList.css
+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
+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
+4
src/index.tsx
+240
src/renderer.ts
+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
+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
+9
src/structs/OscMessage.ts
+88
src/structs/node.ts
+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
+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
src/utils/lerp.ts
···
···
1
+
export let lerp = ( a: number, b: number, t: number ): number => a + ( b - a ) * t;