something?
This commit is contained in:
commit
4578f50eed
9 changed files with 6073 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
.vscode
|
5299
Cargo.lock
generated
Normal file
5299
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
21
Cargo.toml
Normal file
21
Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "patchwire"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
#build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.86"
|
||||
env_logger = "0.11.5"
|
||||
log = "0.4.22"
|
||||
rodio = "0.19.0"
|
||||
slint = "1.7.1"
|
||||
|
||||
#[dependencies.steel-core]
|
||||
#git="https://github.com/mattwparas/steel.git"
|
||||
#branch = "master"
|
||||
# TODO: figure out if these should be used
|
||||
#features = [ "anyhow", "jit", "modules" ]
|
||||
|
||||
[build-dependencies]
|
||||
slint-build = "1.7.1"
|
5
README.md
Normal file
5
README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# patchwire
|
||||
|
||||
DSP/Music creation stack based on node architecture using slint and custom written bytecode vm for dsp code.
|
||||
|
||||
TODO: make this make sense lmao
|
3
build.rs
Normal file
3
build.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
slint_build::compile("ui/window.slint").unwrap();
|
||||
}
|
223
src/lisp/mod.rs
Normal file
223
src/lisp/mod.rs
Normal file
|
@ -0,0 +1,223 @@
|
|||
use std::time::Instant;
|
||||
|
||||
use anyhow::{bail, Context, Error};
|
||||
|
||||
pub mod templates;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum LispVMData {
|
||||
Sample(u32),
|
||||
// TODO: MIDI Packet, lighting packet, etc
|
||||
}
|
||||
|
||||
impl LispVMData {
|
||||
pub fn try_get_sample(self) -> Result<u32, Error> {
|
||||
match self {
|
||||
LispVMData::Sample(x) => Ok(x),
|
||||
_ => bail!(
|
||||
"Lisp VM attempted to convert {:?} to Sample data, it is not",
|
||||
self
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum OpCode {
|
||||
Add,
|
||||
AddConst(u32),
|
||||
Sub,
|
||||
SubConst(u32),
|
||||
Mul,
|
||||
Div,
|
||||
Jump(usize),
|
||||
JumpIfZero(usize),
|
||||
JumpIfEq(usize, LispVMData),
|
||||
JumpIfStackSizeEq(usize, usize),
|
||||
Input(LispVMData),
|
||||
Output(LispVMData),
|
||||
Pop,
|
||||
Dup,
|
||||
Brk,
|
||||
MicrosElapsedModN(u32),
|
||||
}
|
||||
|
||||
impl OpCode {
|
||||
pub fn try_get_data(self) -> Result<LispVMData, Error> {
|
||||
match self {
|
||||
OpCode::Input(x) => Ok(x),
|
||||
OpCode::Output(x) => Ok(x),
|
||||
_ => bail!("Failed to coerce opcode {:?} to data", self),
|
||||
}
|
||||
}
|
||||
pub fn try_get_sample(self) -> Result<u32, Error> {
|
||||
self.try_get_data()
|
||||
.context(format!("Failed to coerce opcode {:?} as sample data", self))?
|
||||
.try_get_sample()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Script {
|
||||
bytecode: Vec<OpCode>,
|
||||
source: Option<String>,
|
||||
}
|
||||
|
||||
impl Script {
|
||||
pub fn from_lisp(text: String) -> Self {
|
||||
let opcodes: Vec<OpCode> = vec![];
|
||||
|
||||
// TODO: actual parse that shit
|
||||
|
||||
Self {
|
||||
bytecode: opcodes,
|
||||
source: Some(text),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_raw_bytecode(bytecode: Vec<OpCode>) -> Self {
|
||||
Self {
|
||||
bytecode,
|
||||
source: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process(self, input: Vec<OpCode>, time: Instant) -> Result<Vec<OpCode>, Error> {
|
||||
let mut stack: Vec<OpCode> = input;
|
||||
let mut pc = 0;
|
||||
let mut dont_increment_pc = false;
|
||||
while pc < self.bytecode.len() {
|
||||
match self.bytecode.get(pc).unwrap() {
|
||||
OpCode::Add => {
|
||||
let a = stack
|
||||
.pop()
|
||||
.context(format!("Stack underflow on add at {}", pc))?
|
||||
.try_get_sample()?;
|
||||
let b = stack
|
||||
.pop()
|
||||
.context(format!("Stack underflow on add at {}", pc))?
|
||||
.try_get_sample()?;
|
||||
stack.push(OpCode::Output(LispVMData::Sample(a + b)));
|
||||
}
|
||||
OpCode::AddConst(x) => {
|
||||
let a = stack
|
||||
.pop()
|
||||
.context(format!("Stack underflow on add at {}", pc))?
|
||||
.try_get_sample()?;
|
||||
stack.push(OpCode::Output(LispVMData::Sample(a + x)));
|
||||
}
|
||||
OpCode::Sub => {
|
||||
let b = stack
|
||||
.pop()
|
||||
.context(format!("Stack underflow on sub at {}", pc))?
|
||||
.try_get_sample()?;
|
||||
let a = stack
|
||||
.pop()
|
||||
.context(format!("Stack underflow on sub at {}", pc))?
|
||||
.try_get_sample()?;
|
||||
stack.push(OpCode::Output(LispVMData::Sample(a - b)));
|
||||
}
|
||||
OpCode::SubConst(x) => {
|
||||
let a = stack
|
||||
.pop()
|
||||
.context(format!("Stack underflow on sub at {}", pc))?
|
||||
.try_get_sample()?;
|
||||
stack.push(OpCode::Output(LispVMData::Sample(a - x)));
|
||||
}
|
||||
OpCode::Mul => todo!(),
|
||||
OpCode::Div => todo!(),
|
||||
OpCode::Jump(x) => {
|
||||
if *x >= self.bytecode.len() {
|
||||
bail!(
|
||||
"Jump to {} is out of bounds. Code is {} opcodes long",
|
||||
*x,
|
||||
self.bytecode.len()
|
||||
);
|
||||
} else {
|
||||
pc = *x;
|
||||
dont_increment_pc = true;
|
||||
}
|
||||
}
|
||||
OpCode::JumpIfZero(x) => {
|
||||
if stack
|
||||
.last()
|
||||
.context(format!("Stack underflow on jz at {}", pc))?
|
||||
.try_get_sample()?
|
||||
== 0
|
||||
{
|
||||
if *x >= self.bytecode.len() {
|
||||
bail!(
|
||||
"Jump to {} is out of bounds. Code is {} opcodes long",
|
||||
*x,
|
||||
self.bytecode.len()
|
||||
);
|
||||
} else {
|
||||
pc = *x;
|
||||
dont_increment_pc = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
OpCode::JumpIfEq(x, y) => {
|
||||
if stack
|
||||
.last()
|
||||
.context(format!("Stack underflow on jq at {}", pc))?
|
||||
.try_get_data()?
|
||||
== *y
|
||||
{
|
||||
if *x >= self.bytecode.len() {
|
||||
bail!(
|
||||
"Jump to {} is out of bounds. Code is {} bytes long",
|
||||
*x,
|
||||
self.bytecode.len()
|
||||
);
|
||||
} else {
|
||||
pc = *x;
|
||||
dont_increment_pc = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
OpCode::JumpIfStackSizeEq(x, y) => {
|
||||
if stack.len() == *y {
|
||||
if *x >= self.bytecode.len() {
|
||||
bail!(
|
||||
"Jump to {} is out of bounds. Code is {} bytes long",
|
||||
*x,
|
||||
self.bytecode.len()
|
||||
);
|
||||
} else {
|
||||
pc = *x;
|
||||
dont_increment_pc = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
OpCode::Input(x) => {
|
||||
stack.push(OpCode::Output(*x));
|
||||
}
|
||||
OpCode::Output(x) => {
|
||||
stack.push(OpCode::Output(*x));
|
||||
}
|
||||
OpCode::Pop => {
|
||||
stack.pop();
|
||||
}
|
||||
OpCode::Dup => {
|
||||
stack.push(
|
||||
*stack
|
||||
.last()
|
||||
.context(format!("Stack underflow on add at dup {}", pc))?,
|
||||
);
|
||||
}
|
||||
OpCode::Brk => {
|
||||
return Ok(stack);
|
||||
}
|
||||
OpCode::MicrosElapsedModN(x) => {
|
||||
stack.push(OpCode::Output(LispVMData::Sample(u32::try_from(time.elapsed().as_micros() % (*x as u128)).unwrap())));
|
||||
},
|
||||
}
|
||||
if dont_increment_pc {
|
||||
dont_increment_pc = false;
|
||||
} else {
|
||||
pc += 1;
|
||||
}
|
||||
}
|
||||
Ok(stack)
|
||||
}
|
||||
}
|
71
src/lisp/templates.rs
Normal file
71
src/lisp/templates.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
use std::{collections::HashMap};
|
||||
|
||||
use slint::{ModelRc, VecModel};
|
||||
|
||||
use super::{OpCode, Script};
|
||||
use crate::{Node, Port};
|
||||
|
||||
pub struct NodeTemplate {
|
||||
pub name: String,
|
||||
pub script: Script, // TODO: figure out what to do with this?
|
||||
pub input_connections: usize,
|
||||
pub output_connections: usize,
|
||||
}
|
||||
|
||||
impl NodeTemplate {
|
||||
pub fn to_slint_node(self) -> Node {
|
||||
let mut in_port_vec = vec![];
|
||||
let mut out_port_vec = vec![];
|
||||
|
||||
for i in 0..self.input_connections {
|
||||
in_port_vec.push(Port {
|
||||
name: format!("i{i}").into(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
for j in 0..self.output_connections {
|
||||
out_port_vec.push(Port {
|
||||
name: format!("o{j}").into(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
Node {
|
||||
text: self.name.into(),
|
||||
x: 0.0, //?
|
||||
y: 0.0, //?
|
||||
in_ports: ModelRc::new(VecModel::from(in_port_vec)),
|
||||
out_ports: ModelRc::new(VecModel::from(out_port_vec)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_mixer(input_count: usize) -> NodeTemplate {
|
||||
NodeTemplate {
|
||||
name: "Mixer".to_owned(),
|
||||
script: Script::from_raw_bytecode(vec![
|
||||
OpCode::Add,
|
||||
OpCode::JumpIfStackSizeEq(3, 1),
|
||||
OpCode::Jump(0),
|
||||
OpCode::Brk,
|
||||
]),
|
||||
input_connections: input_count,
|
||||
output_connections: 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_splitter(output_count: usize) -> NodeTemplate {
|
||||
NodeTemplate {
|
||||
name: "Splitter".to_owned(),
|
||||
script: Script::from_raw_bytecode(vec![
|
||||
OpCode::Dup,
|
||||
OpCode::JumpIfStackSizeEq(3, output_count),
|
||||
OpCode::Jump(0),
|
||||
OpCode::Brk,
|
||||
]),
|
||||
input_connections: 1,
|
||||
output_connections: output_count,
|
||||
}
|
||||
}
|
197
src/main.rs
Normal file
197
src/main.rs
Normal file
|
@ -0,0 +1,197 @@
|
|||
use core::fmt;
|
||||
use std::{process::exit, time::Instant};
|
||||
|
||||
//use steel::steel_vm::engine::Engine;
|
||||
use anyhow::{Context, Error, Result};
|
||||
use lisp::{LispVMData, OpCode, Script};
|
||||
use log::{debug, error, info};
|
||||
use rodio::{
|
||||
cpal::{self, traits::HostTrait}, Device, DeviceTrait, OutputStream, OutputStreamHandle
|
||||
};
|
||||
use slint::{Brush, Color, Model, ModelRc, VecModel};
|
||||
|
||||
pub mod lisp;
|
||||
|
||||
slint::include_modules!();
|
||||
|
||||
trait UnwrapToLog<T, E> {
|
||||
fn unwrap_to_log(self) -> T
|
||||
where
|
||||
E: fmt::Debug;
|
||||
}
|
||||
|
||||
impl<T, E> UnwrapToLog<T, E> for Result<T, E> {
|
||||
fn unwrap_to_log(self) -> T
|
||||
where
|
||||
E: fmt::Debug,
|
||||
{
|
||||
match self {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
error!("{e:?}");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait RustMainWindowFunctions {
|
||||
fn connect(&self, node1: i32, port1: i32, node2: i32, port2: i32, brush: Brush, toggle: bool);
|
||||
fn add_connection(&self, connection: Connection, toggle: bool);
|
||||
fn add_node(&self, node: Node);
|
||||
}
|
||||
|
||||
impl RustMainWindowFunctions for MainWindow {
|
||||
fn connect(&self, node1: i32, port1: i32, node2: i32, port2: i32, brush: Brush, toggle: bool) {
|
||||
self.add_connection(
|
||||
Connection {
|
||||
node1,
|
||||
port1,
|
||||
node2,
|
||||
port2,
|
||||
color: brush,
|
||||
},
|
||||
toggle,
|
||||
);
|
||||
}
|
||||
|
||||
fn add_connection(&self, connection: Connection, toggle: bool) {
|
||||
let default = &VecModel::default();
|
||||
let connections = self.get_connections();
|
||||
let connections_ref = connections.as_any().downcast_ref().unwrap_or(default);
|
||||
let mut existing_connections: Vec<Connection> = connections_ref.iter().collect();
|
||||
|
||||
let duplicate_index = existing_connections.iter().position(|r: &Connection| {
|
||||
r.node1 == connection.node1
|
||||
&& r.node2 == connection.node2
|
||||
&& r.port1 == connection.port1
|
||||
&& r.port2 == connection.port2
|
||||
});
|
||||
|
||||
match duplicate_index {
|
||||
Some(x) => {
|
||||
if toggle {
|
||||
existing_connections.remove(x);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
existing_connections.push(connection);
|
||||
}
|
||||
}
|
||||
|
||||
info!("{existing_connections:?}");
|
||||
|
||||
self.set_connections(ModelRc::new(VecModel::from(existing_connections)));
|
||||
}
|
||||
|
||||
fn add_node(&self, node: Node) {
|
||||
let default = &VecModel::default();
|
||||
let nodes = self.get_nodes();
|
||||
let nodes_ref = nodes.as_any().downcast_ref().unwrap_or(default);
|
||||
let mut existing_nodes: Vec<Node> = nodes_ref.iter().collect();
|
||||
|
||||
existing_nodes.push(node);
|
||||
|
||||
self.set_nodes(ModelRc::new(VecModel::from(existing_nodes)));
|
||||
}
|
||||
}
|
||||
|
||||
fn open_window_and_run_event_loop(device: Device, stream: OutputStreamHandle) -> Result<(), Error> {
|
||||
let main_window = MainWindow::new().with_context(|| "failed to create main window")?;
|
||||
|
||||
main_window.set_window_title(format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")).into());
|
||||
|
||||
let patchwire_start_time = Instant::now();
|
||||
|
||||
main_window.add_node(Node {
|
||||
text: "Sine Wave".into(),
|
||||
// TODO: Node placement algorithm
|
||||
x: 0.0,
|
||||
y: 50.0,
|
||||
out_ports: ModelRc::new(VecModel::from(vec![
|
||||
Port {
|
||||
name: "a".into(),
|
||||
..Default::default()
|
||||
},
|
||||
])),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
main_window.add_node(Node {
|
||||
text: format!("Audio device {}", device.name().unwrap_or("default".to_owned())).into(),
|
||||
x: 200.0,
|
||||
y: 50.0,
|
||||
in_ports: ModelRc::new(VecModel::from(vec![
|
||||
Port {
|
||||
name: "L".into(),
|
||||
..Default::default()
|
||||
},
|
||||
Port {
|
||||
name: "R".into(),
|
||||
..Default::default()
|
||||
},
|
||||
])),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
main_window.connect(0, 0, 1, 0, Brush::SolidColor(Color::from_rgb_u8(255, 255, 0)), true);
|
||||
main_window.connect(0, 0, 1, 1, Brush::SolidColor(Color::from_rgb_u8(255, 255, 0)), true);
|
||||
|
||||
let mwr = main_window.as_weak();
|
||||
|
||||
main_window.on_add_connection_helper(move |c: Connection| mwr.unwrap().add_connection(c, true));
|
||||
|
||||
main_window
|
||||
.run()
|
||||
.with_context(|| "error in main window event loop")
|
||||
}
|
||||
|
||||
// TODO: remove
|
||||
fn script_test() {
|
||||
let script = Script::from_raw_bytecode(vec![
|
||||
OpCode::Input(LispVMData::Sample(1)),
|
||||
OpCode::Sub,
|
||||
//OpCode::JumpIfZero(4),
|
||||
OpCode::JumpIfEq(4, LispVMData::Sample(0)),
|
||||
OpCode::Jump(0),
|
||||
OpCode::Brk,
|
||||
]);
|
||||
let input = vec![
|
||||
OpCode::Input(LispVMData::Sample(69)),
|
||||
];
|
||||
|
||||
let now = Instant::now();
|
||||
|
||||
//
|
||||
let result = script.process(input, now);
|
||||
//
|
||||
|
||||
let elapsed = now.elapsed();
|
||||
|
||||
info!("Result: {:?}, Time: {:?}", result.unwrap_to_log(), elapsed);
|
||||
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
//let mut steel_engine = Engine::new();
|
||||
// TODO: other config reading and setup goes here
|
||||
|
||||
// TODO: allow user to select device
|
||||
debug!("Opening audio output stream");
|
||||
let default_device = cpal::default_host()
|
||||
.default_output_device()
|
||||
.with_context(|| "failed to find default audio device")
|
||||
.unwrap_to_log();
|
||||
|
||||
info!("Found audio device with name: \"{}\"", default_device.name().unwrap_or("default".to_string()));
|
||||
|
||||
let (_, stream) = OutputStream::try_from_device(&default_device)
|
||||
.with_context(|| "failed to open default audio device")
|
||||
.unwrap_to_log();
|
||||
|
||||
script_test();
|
||||
|
||||
debug!("Opening main window");
|
||||
open_window_and_run_event_loop(default_device, stream).unwrap_to_log();
|
||||
}
|
252
ui/window.slint
Normal file
252
ui/window.slint
Normal file
|
@ -0,0 +1,252 @@
|
|||
import { ScrollView, HorizontalBox, Button } from "std-widgets.slint";
|
||||
|
||||
struct Port {
|
||||
x: length,
|
||||
y: length,
|
||||
name: string,
|
||||
}
|
||||
|
||||
struct Node {
|
||||
x: length,
|
||||
y: length,
|
||||
z: float,
|
||||
ports-width: [length],
|
||||
text: string,
|
||||
in-ports: [Port],
|
||||
out-ports: [Port],
|
||||
}
|
||||
|
||||
component Node inherits Rectangle {
|
||||
in-out property <[Node]> nodes;
|
||||
in property <int> idx;
|
||||
private property <Node> node: nodes[idx];
|
||||
callback port_clicked(int, bool);
|
||||
|
||||
x: node.x;
|
||||
y: node.y;
|
||||
|
||||
width: self.preferred-width;
|
||||
height: self.preferred-height;
|
||||
|
||||
drop-shadow-blur: 5px;
|
||||
drop-shadow-color: black;
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
VerticalLayout {
|
||||
spacing: -heading.border-width;
|
||||
|
||||
heading := Rectangle {
|
||||
border-radius: root.border-radius;
|
||||
border-bottom-right-radius: self.border-bottom-left-radius;
|
||||
border-bottom-left-radius: {
|
||||
if node.in-ports.length == 0 && node.out-ports.length == 0 {
|
||||
root.border-radius
|
||||
} else {
|
||||
0px
|
||||
}
|
||||
};
|
||||
|
||||
background: #383838;
|
||||
border-width: 1px;
|
||||
border-color: white.with-alpha(0.15);
|
||||
|
||||
HorizontalLayout {
|
||||
padding: 5px;
|
||||
spacing: 10px;
|
||||
|
||||
Text {
|
||||
color: white;
|
||||
text: node.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
background: #303030;
|
||||
border-width: heading.border-width;
|
||||
border-color: heading.border-color;
|
||||
|
||||
border-bottom-right-radius: root.border-radius;
|
||||
border-bottom-left-radius: self.border-bottom-right-radius;
|
||||
|
||||
HorizontalLayout {
|
||||
for ports[idx] in [node.in-ports, node.out-ports]: ports-layout := VerticalLayout {
|
||||
private property <bool> is-input: idx == 0;
|
||||
|
||||
alignment: start;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
spacing: 5px;
|
||||
|
||||
for idx in ports.length: port-layout := HorizontalLayout {
|
||||
alignment: is-input ? start : end;
|
||||
x: 15px / 2 * (is-input ? -1 : 1);
|
||||
Rectangle {
|
||||
area := TouchArea {
|
||||
clicked => {
|
||||
root.port_clicked(idx, is-input);
|
||||
}
|
||||
}
|
||||
|
||||
border-radius: 3px;
|
||||
|
||||
background: #f8e45c;
|
||||
|
||||
min-width: 15px;
|
||||
height: 20px;
|
||||
|
||||
drop-shadow-blur: 5px;
|
||||
drop-shadow-color: black;
|
||||
|
||||
HorizontalLayout {
|
||||
padding: 2px;
|
||||
Text {
|
||||
color: black;
|
||||
horizontal-alignment: center;
|
||||
text: ports[idx].name;
|
||||
}
|
||||
}
|
||||
|
||||
init => {
|
||||
ports[idx].y = //
|
||||
heading.preferred-height//
|
||||
+ ports-layout.padding-top//
|
||||
+ port-layout.preferred-height / 2//
|
||||
+ (ports-layout.spacing + port-layout.preferred-height) * idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for idx in node.out-ports.length: Rectangle {
|
||||
init => {
|
||||
node.out-ports[idx].x = root.preferred-width;
|
||||
}
|
||||
}
|
||||
|
||||
TouchArea {
|
||||
moved => {
|
||||
if self.pressed {
|
||||
nodes[idx].x += self.mouse-x - self.pressed-x;
|
||||
nodes[idx].y += self.mouse-y - self.pressed-y;
|
||||
}
|
||||
}
|
||||
mouse-cursor: move;
|
||||
}
|
||||
}
|
||||
|
||||
struct Link { x1: length, x2: length, y1: length, y2: length, color: brush, }
|
||||
|
||||
component Link inherits Path {
|
||||
in property <Link> link;
|
||||
|
||||
viewbox-width: self.width / 1px;
|
||||
viewbox-height: self.height / 1px;
|
||||
|
||||
stroke: link.color;
|
||||
stroke-width: 5px;
|
||||
|
||||
MoveTo {
|
||||
x: link.x1 / 1px;
|
||||
y: link.y1 / 1px;
|
||||
}
|
||||
|
||||
// LineTo {
|
||||
// x: link.x2 / 1px;
|
||||
// y: link.y2 / 1px;
|
||||
// }
|
||||
|
||||
private property <int> h: link.y2 / 1px - link.y1 / 1px;
|
||||
private property <int> w: link.x2 / 1px - link.x1 / 1px;
|
||||
|
||||
private property <{ x: int, y: int}> c1: {
|
||||
x: max(abs(h / 2), abs(w / 2)),
|
||||
y: max(h / 4, -w / 4),
|
||||
};
|
||||
private property <{ x: int, y: int}> c2: {
|
||||
x: max(abs(h / 2), abs(w / 2)),
|
||||
y: min(h / 4, w / 4 + abs(h)),
|
||||
};
|
||||
|
||||
CubicTo {
|
||||
x: link.x2 / 1px;
|
||||
y: link.y2 / 1px;
|
||||
control-1-x: link.x1 / 1px + c1.x;
|
||||
control-1-y: link.y1 / 1px + c1.y;
|
||||
control-2-x: link.x2 / 1px - c2.x;
|
||||
control-2-y: link.y2 / 1px - c2.y;
|
||||
}
|
||||
}
|
||||
|
||||
struct Connection { node1: int, port1: int, node2: int, port2: int, color: brush}
|
||||
|
||||
export component MainWindow inherits Window {
|
||||
|
||||
background: #242424;
|
||||
|
||||
min-width: 800px;
|
||||
min-height: 400px;
|
||||
|
||||
in-out property window_title <=> self.title;
|
||||
in-out property <[Node]> nodes;
|
||||
in-out property <[Connection]> connections;
|
||||
property <[Link]> links;
|
||||
property <Connection> PendingConnection: { node1: -1, port1: -1, node2: -1, port2: -1, color: #ffa500 };
|
||||
|
||||
callback add_connection_helper(Connection);
|
||||
|
||||
preferred-height: 500px;
|
||||
preferred-width: 500px;
|
||||
|
||||
public pure function create-link-object(start: { node: int, port: int}, end: { node: int, port: int}, color: brush) -> Link {
|
||||
{
|
||||
x1: nodes[start.node].x + nodes[start.node].out-ports[start.port].x,
|
||||
y1: nodes[start.node].y + nodes[start.node].out-ports[start.port].y,
|
||||
x2: nodes[end.node].x + nodes[end.node].in-ports[end.port].x,
|
||||
y2: nodes[end.node].y + nodes[end.node].in-ports[end.port].y,
|
||||
color: color,
|
||||
}
|
||||
}
|
||||
|
||||
VerticalLayout {
|
||||
ScrollView {
|
||||
for c in connections: Link {
|
||||
link: root.create-link-object({ node: c.node1, port: c.port1 }, { node: c.node2, port: c.port2 }, c.color);
|
||||
}
|
||||
|
||||
for node[idx] in nodes: Node {
|
||||
nodes <=> nodes;
|
||||
idx: idx;
|
||||
port_clicked(port, is-input) => {
|
||||
debug("Node", idx, "Port", port, "Input", is-input);
|
||||
if (root.PendingConnection.node1 == -1) {
|
||||
if (!is-input) {
|
||||
root.PendingConnection.node1 = idx;
|
||||
root.PendingConnection.port1 = port;
|
||||
root.PendingConnection.color = #ffa500;
|
||||
} else {
|
||||
root.PendingConnection.node1 = -1;
|
||||
}
|
||||
} else if (root.PendingConnection.node1 != idx) {
|
||||
if (is-input) {
|
||||
root.PendingConnection.node2 = idx;
|
||||
root.PendingConnection.port2 = port;
|
||||
// Use rust code to add the connection because we can't do it in pure slint
|
||||
// Ugh
|
||||
root.add_connection_helper(root.PendingConnection);
|
||||
}
|
||||
root.PendingConnection.node1 = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 10%;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue