Add UI to script node
This commit is contained in:
parent
2804c2bd5d
commit
92dffbaa4e
|
|
@ -12,6 +12,7 @@ pub struct Script {
|
||||||
pub state: Vec<StateDecl>,
|
pub state: Vec<StateDecl>,
|
||||||
pub ui: Option<Vec<UiElement>>,
|
pub ui: Option<Vec<UiElement>>,
|
||||||
pub process: Block,
|
pub process: Block,
|
||||||
|
pub draw: Option<Block>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ struct Compiler {
|
||||||
vars: Vec<(String, VarLoc)>,
|
vars: Vec<(String, VarLoc)>,
|
||||||
next_local: u16,
|
next_local: u16,
|
||||||
scope_stack: Vec<u16>, // local count at scope entry
|
scope_stack: Vec<u16>, // local count at scope entry
|
||||||
|
draw_context: bool, // true when compiling a draw {} block
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Compiler {
|
impl Compiler {
|
||||||
|
|
@ -48,6 +49,7 @@ impl Compiler {
|
||||||
vars: Vec::new(),
|
vars: Vec::new(),
|
||||||
next_local: 0,
|
next_local: 0,
|
||||||
scope_stack: Vec::new(),
|
scope_stack: Vec::new(),
|
||||||
|
draw_context: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,6 +212,55 @@ impl Compiler {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compile the draw block into separate bytecode (for the DrawVM)
|
||||||
|
fn compile_draw(&mut self, script: &Script) -> Result<(), CompileError> {
|
||||||
|
self.draw_context = true;
|
||||||
|
|
||||||
|
// Register params (same as process)
|
||||||
|
for (i, param) in script.params.iter().enumerate() {
|
||||||
|
self.vars.push((param.name.clone(), VarLoc::Param(i as u16)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register state variables (draw gets its own copy)
|
||||||
|
let mut scalar_idx: u16 = 0;
|
||||||
|
let mut array_idx: u16 = 0;
|
||||||
|
for state in &script.state {
|
||||||
|
match &state.ty {
|
||||||
|
StateType::F32 => {
|
||||||
|
self.vars.push((state.name.clone(), VarLoc::StateScalar(scalar_idx, VType::F32)));
|
||||||
|
scalar_idx += 1;
|
||||||
|
}
|
||||||
|
StateType::Int => {
|
||||||
|
self.vars.push((state.name.clone(), VarLoc::StateScalar(scalar_idx, VType::Int)));
|
||||||
|
scalar_idx += 1;
|
||||||
|
}
|
||||||
|
StateType::Bool => {
|
||||||
|
self.vars.push((state.name.clone(), VarLoc::StateScalar(scalar_idx, VType::Bool)));
|
||||||
|
scalar_idx += 1;
|
||||||
|
}
|
||||||
|
StateType::ArrayF32(_) => {
|
||||||
|
self.vars.push((state.name.clone(), VarLoc::StateArray(array_idx, VType::F32)));
|
||||||
|
array_idx += 1;
|
||||||
|
}
|
||||||
|
StateType::ArrayInt(_) => {
|
||||||
|
self.vars.push((state.name.clone(), VarLoc::StateArray(array_idx, VType::Int)));
|
||||||
|
array_idx += 1;
|
||||||
|
}
|
||||||
|
StateType::Sample => {} // no samples in draw context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile draw block
|
||||||
|
if let Some(draw) = &script.draw {
|
||||||
|
for stmt in draw {
|
||||||
|
self.compile_stmt(stmt)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.emit(OpCode::Halt);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn compile_stmt(&mut self, stmt: &Stmt) -> Result<(), CompileError> {
|
fn compile_stmt(&mut self, stmt: &Stmt) -> Result<(), CompileError> {
|
||||||
match stmt {
|
match stmt {
|
||||||
Stmt::Let { name, init, .. } => {
|
Stmt::Let { name, init, .. } => {
|
||||||
|
|
@ -235,6 +286,10 @@ impl Compiler {
|
||||||
self.emit(OpCode::StoreState);
|
self.emit(OpCode::StoreState);
|
||||||
self.emit_u16(idx);
|
self.emit_u16(idx);
|
||||||
}
|
}
|
||||||
|
VarLoc::Param(idx) if self.draw_context => {
|
||||||
|
self.emit(OpCode::StoreParam);
|
||||||
|
self.emit_u16(idx);
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(CompileError::new(
|
return Err(CompileError::new(
|
||||||
format!("Cannot assign to {}", name), *span,
|
format!("Cannot assign to {}", name), *span,
|
||||||
|
|
@ -336,10 +391,13 @@ impl Compiler {
|
||||||
self.pop_scope();
|
self.pop_scope();
|
||||||
}
|
}
|
||||||
Stmt::ExprStmt(expr) => {
|
Stmt::ExprStmt(expr) => {
|
||||||
|
let is_void = self.is_void_call(expr);
|
||||||
self.compile_expr(expr)?;
|
self.compile_expr(expr)?;
|
||||||
|
if !is_void {
|
||||||
self.emit(OpCode::Pop);
|
self.emit(OpCode::Pop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -548,6 +606,18 @@ impl Compiler {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if the expression is a call to a void function (no return value).
|
||||||
|
fn is_void_call(&self, expr: &Expr) -> bool {
|
||||||
|
if let Expr::Call(name, _, _) = expr {
|
||||||
|
matches!(name.as_str(),
|
||||||
|
"fill_circle" | "stroke_circle" | "stroke_arc" |
|
||||||
|
"line" | "fill_rect" | "stroke_rect"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn compile_call(&mut self, name: &str, args: &[Expr], span: Span) -> Result<(), CompileError> {
|
fn compile_call(&mut self, name: &str, args: &[Expr], span: Span) -> Result<(), CompileError> {
|
||||||
match name {
|
match name {
|
||||||
// 1-arg math → push arg, emit opcode
|
// 1-arg math → push arg, emit opcode
|
||||||
|
|
@ -717,6 +787,48 @@ impl Compiler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw builtins (only valid in draw context)
|
||||||
|
"fill_circle" | "stroke_circle" | "stroke_arc" | "line" |
|
||||||
|
"fill_rect" | "stroke_rect" |
|
||||||
|
"mouse_x" | "mouse_y" | "mouse_down" if self.draw_context => {
|
||||||
|
match name {
|
||||||
|
"fill_circle" => {
|
||||||
|
// fill_circle(cx, cy, r, color)
|
||||||
|
for arg in args { self.compile_expr(arg)?; }
|
||||||
|
self.emit(OpCode::DrawFillCircle);
|
||||||
|
}
|
||||||
|
"stroke_circle" => {
|
||||||
|
// stroke_circle(cx, cy, r, color, width)
|
||||||
|
for arg in args { self.compile_expr(arg)?; }
|
||||||
|
self.emit(OpCode::DrawStrokeCircle);
|
||||||
|
}
|
||||||
|
"stroke_arc" => {
|
||||||
|
// stroke_arc(cx, cy, r, start_deg, end_deg, color, width)
|
||||||
|
for arg in args { self.compile_expr(arg)?; }
|
||||||
|
self.emit(OpCode::DrawStrokeArc);
|
||||||
|
}
|
||||||
|
"line" => {
|
||||||
|
// line(x1, y1, x2, y2, color, width)
|
||||||
|
for arg in args { self.compile_expr(arg)?; }
|
||||||
|
self.emit(OpCode::DrawLine);
|
||||||
|
}
|
||||||
|
"fill_rect" => {
|
||||||
|
// fill_rect(x, y, w, h, color)
|
||||||
|
for arg in args { self.compile_expr(arg)?; }
|
||||||
|
self.emit(OpCode::DrawFillRect);
|
||||||
|
}
|
||||||
|
"stroke_rect" => {
|
||||||
|
// stroke_rect(x, y, w, h, color, width)
|
||||||
|
for arg in args { self.compile_expr(arg)?; }
|
||||||
|
self.emit(OpCode::DrawStrokeRect);
|
||||||
|
}
|
||||||
|
"mouse_x" => { self.emit(OpCode::MouseX); }
|
||||||
|
"mouse_y" => { self.emit(OpCode::MouseY); }
|
||||||
|
"mouse_down" => { self.emit(OpCode::MouseDown); }
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
return Err(CompileError::new(format!("Unknown function: {}", name), span));
|
return Err(CompileError::new(format!("Unknown function: {}", name), span));
|
||||||
}
|
}
|
||||||
|
|
@ -793,7 +905,7 @@ impl Compiler {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compile a validated AST into bytecode VM and UI declaration
|
/// Compile a validated AST into bytecode VM and UI declaration
|
||||||
pub fn compile(script: &Script) -> Result<(ScriptVM, UiDeclaration), CompileError> {
|
pub fn compile(script: &Script) -> Result<(ScriptVM, UiDeclaration, Option<crate::vm::DrawVM>), CompileError> {
|
||||||
let mut compiler = Compiler::new();
|
let mut compiler = Compiler::new();
|
||||||
compiler.compile_script(script)?;
|
compiler.compile_script(script)?;
|
||||||
|
|
||||||
|
|
@ -843,7 +955,24 @@ pub fn compile(script: &Script) -> Result<(ScriptVM, UiDeclaration), CompileErro
|
||||||
UiDeclaration { elements }
|
UiDeclaration { elements }
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((vm, ui_decl))
|
// Compile draw block if present
|
||||||
|
let draw_vm = if script.draw.is_some() {
|
||||||
|
let mut draw_compiler = Compiler::new();
|
||||||
|
draw_compiler.compile_draw(script)?;
|
||||||
|
Some(crate::vm::DrawVM::new(
|
||||||
|
draw_compiler.code,
|
||||||
|
draw_compiler.constants_f32,
|
||||||
|
draw_compiler.constants_i32,
|
||||||
|
script.params.len(),
|
||||||
|
¶m_defaults,
|
||||||
|
num_state_scalars,
|
||||||
|
&state_array_sizes,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((vm, ui_decl, draw_vm))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -859,7 +988,8 @@ mod tests {
|
||||||
let mut parser = Parser::new(&tokens);
|
let mut parser = Parser::new(&tokens);
|
||||||
let script = parser.parse()?;
|
let script = parser.parse()?;
|
||||||
let validated = validator::validate(&script)?;
|
let validated = validator::validate(&script)?;
|
||||||
compile(validated)
|
let (vm, ui, _draw_vm) = compile(validated)?;
|
||||||
|
Ok((vm, ui))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -988,4 +1118,43 @@ mod tests {
|
||||||
assert!(matches!(&ui.elements[0], UiElement::Sample(n) if n == "clip"));
|
assert!(matches!(&ui.elements[0], UiElement::Sample(n) if n == "clip"));
|
||||||
assert!(matches!(&ui.elements[1], UiElement::Param(n) if n == "gain"));
|
assert!(matches!(&ui.elements[1], UiElement::Param(n) if n == "gain"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_draw_block() {
|
||||||
|
let src = r#"
|
||||||
|
name "Knob"
|
||||||
|
category utility
|
||||||
|
params { volume: 0.75 [0.0, 1.0] "" }
|
||||||
|
outputs { out: audio }
|
||||||
|
ui { canvas [80, 80] }
|
||||||
|
draw {
|
||||||
|
let cx = 40.0;
|
||||||
|
let cy = 40.0;
|
||||||
|
fill_circle(cx, cy, 35.0, 0x333333FF);
|
||||||
|
let angle = volume * 270.0 - 135.0;
|
||||||
|
stroke_arc(cx, cy, 30.0, -135.0, angle, 0x4488FFFF, 3.0);
|
||||||
|
}
|
||||||
|
process {
|
||||||
|
for i in 0..buffer_size {
|
||||||
|
out[i] = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let mut lexer = Lexer::new(src);
|
||||||
|
let tokens = lexer.tokenize().unwrap();
|
||||||
|
let mut parser = Parser::new(&tokens);
|
||||||
|
let script = parser.parse().unwrap();
|
||||||
|
let validated = validator::validate(&script).unwrap();
|
||||||
|
let (_vm, _ui, draw_vm) = compile(validated).unwrap();
|
||||||
|
|
||||||
|
// Draw VM should exist
|
||||||
|
let mut dvm = draw_vm.expect("draw_vm should be Some");
|
||||||
|
assert!(!dvm.bytecode.is_empty());
|
||||||
|
|
||||||
|
// Execute should succeed without stack errors
|
||||||
|
dvm.execute().unwrap();
|
||||||
|
|
||||||
|
// Should have produced draw commands
|
||||||
|
assert_eq!(dvm.draw_commands.len(), 2); // fill_circle + stroke_arc
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,28 @@ impl<'a> Lexer<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_number(&mut self, first: u8, span: Span) -> Result<Token, CompileError> {
|
fn read_number(&mut self, first: u8, span: Span) -> Result<Token, CompileError> {
|
||||||
|
// Check for hex literal: 0x...
|
||||||
|
if first == b'0' && self.peek() == Some(b'x') {
|
||||||
|
self.advance(); // skip 'x'
|
||||||
|
let mut hex = String::new();
|
||||||
|
while let Some(ch) = self.peek() {
|
||||||
|
if ch.is_ascii_hexdigit() {
|
||||||
|
hex.push(self.advance() as char);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hex.is_empty() {
|
||||||
|
return Err(CompileError::new("Expected hex digits after 0x", span));
|
||||||
|
}
|
||||||
|
let val = u32::from_str_radix(&hex, 16)
|
||||||
|
.map_err(|_| CompileError::new(format!("Invalid hex literal: 0x{}", hex), span))?;
|
||||||
|
return Ok(Token {
|
||||||
|
kind: TokenKind::IntLit(val as i32),
|
||||||
|
span,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
s.push(first as char);
|
s.push(first as char);
|
||||||
let mut is_float = false;
|
let mut is_float = false;
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ use parser::Parser;
|
||||||
|
|
||||||
pub use error::ScriptError;
|
pub use error::ScriptError;
|
||||||
pub use ui_decl::{UiDeclaration, UiElement};
|
pub use ui_decl::{UiDeclaration, UiElement};
|
||||||
pub use vm::{ScriptVM, SampleSlot};
|
pub use vm::{ScriptVM, SampleSlot, DrawVM, DrawCommand, MouseState};
|
||||||
|
|
||||||
/// Compiled script metadata — everything needed to create a ScriptNode
|
/// Compiled script metadata — everything needed to create a ScriptNode
|
||||||
pub struct CompiledScript {
|
pub struct CompiledScript {
|
||||||
|
|
@ -28,6 +28,7 @@ pub struct CompiledScript {
|
||||||
pub sample_slots: Vec<String>,
|
pub sample_slots: Vec<String>,
|
||||||
pub ui_declaration: UiDeclaration,
|
pub ui_declaration: UiDeclaration,
|
||||||
pub source: String,
|
pub source: String,
|
||||||
|
pub draw_vm: Option<DrawVM>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -55,7 +56,7 @@ pub fn compile(source: &str) -> Result<CompiledScript, CompileError> {
|
||||||
|
|
||||||
let validated = validator::validate(&script)?;
|
let validated = validator::validate(&script)?;
|
||||||
|
|
||||||
let (vm, ui_decl) = codegen::compile(&validated)?;
|
let (vm, ui_decl, draw_vm) = codegen::compile(&validated)?;
|
||||||
|
|
||||||
let input_ports = script
|
let input_ports = script
|
||||||
.inputs
|
.inputs
|
||||||
|
|
@ -104,5 +105,6 @@ pub fn compile(source: &str) -> Result<CompiledScript, CompileError> {
|
||||||
sample_slots,
|
sample_slots,
|
||||||
ui_declaration: ui_decl,
|
ui_declaration: ui_decl,
|
||||||
source: source.to_string(),
|
source: source.to_string(),
|
||||||
|
draw_vm,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,22 @@ pub enum OpCode {
|
||||||
LoadSampleRate = 140,
|
LoadSampleRate = 140,
|
||||||
LoadBufferSize = 141,
|
LoadBufferSize = 141,
|
||||||
|
|
||||||
|
// Draw commands (pop args from stack, push to draw command buffer)
|
||||||
|
DrawFillCircle = 150, // pops: color(i32), r, cy, cx
|
||||||
|
DrawStrokeCircle = 151, // pops: width, color(i32), r, cy, cx
|
||||||
|
DrawStrokeArc = 152, // pops: width, color(i32), end_deg, start_deg, r, cy, cx
|
||||||
|
DrawLine = 153, // pops: width, color(i32), y2, x2, y1, x1
|
||||||
|
DrawFillRect = 154, // pops: color(i32), h, w, y, x
|
||||||
|
DrawStrokeRect = 155, // pops: width, color(i32), h, w, y, x
|
||||||
|
|
||||||
|
// Mouse input (push onto stack)
|
||||||
|
MouseX = 160, // pushes canvas-relative X as f32
|
||||||
|
MouseY = 161, // pushes canvas-relative Y as f32
|
||||||
|
MouseDown = 162, // pushes 1.0 if pressed, 0.0 if not
|
||||||
|
|
||||||
|
// Param write (draw context only)
|
||||||
|
StoreParam = 170, // u16 param index, pops value from stack
|
||||||
|
|
||||||
Halt = 255,
|
Halt = 255,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -190,6 +206,16 @@ impl OpCode {
|
||||||
130 => Some(OpCode::ArrayLen),
|
130 => Some(OpCode::ArrayLen),
|
||||||
140 => Some(OpCode::LoadSampleRate),
|
140 => Some(OpCode::LoadSampleRate),
|
||||||
141 => Some(OpCode::LoadBufferSize),
|
141 => Some(OpCode::LoadBufferSize),
|
||||||
|
150 => Some(OpCode::DrawFillCircle),
|
||||||
|
151 => Some(OpCode::DrawStrokeCircle),
|
||||||
|
152 => Some(OpCode::DrawStrokeArc),
|
||||||
|
153 => Some(OpCode::DrawLine),
|
||||||
|
154 => Some(OpCode::DrawFillRect),
|
||||||
|
155 => Some(OpCode::DrawStrokeRect),
|
||||||
|
160 => Some(OpCode::MouseX),
|
||||||
|
161 => Some(OpCode::MouseY),
|
||||||
|
162 => Some(OpCode::MouseDown),
|
||||||
|
170 => Some(OpCode::StoreParam),
|
||||||
255 => Some(OpCode::Halt),
|
255 => Some(OpCode::Halt),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ impl<'a> Parser<'a> {
|
||||||
let mut state = Vec::new();
|
let mut state = Vec::new();
|
||||||
let mut ui = None;
|
let mut ui = None;
|
||||||
let mut process = Vec::new();
|
let mut process = Vec::new();
|
||||||
|
let mut draw = None;
|
||||||
|
|
||||||
while *self.peek() != TokenKind::Eof {
|
while *self.peek() != TokenKind::Eof {
|
||||||
match self.peek() {
|
match self.peek() {
|
||||||
|
|
@ -131,6 +132,10 @@ impl<'a> Parser<'a> {
|
||||||
self.advance();
|
self.advance();
|
||||||
process = self.parse_block()?;
|
process = self.parse_block()?;
|
||||||
}
|
}
|
||||||
|
TokenKind::Draw => {
|
||||||
|
self.advance();
|
||||||
|
draw = Some(self.parse_block()?);
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(CompileError::new(
|
return Err(CompileError::new(
|
||||||
format!("Unexpected token {:?} at top level", self.peek()),
|
format!("Unexpected token {:?} at top level", self.peek()),
|
||||||
|
|
@ -156,6 +161,7 @@ impl<'a> Parser<'a> {
|
||||||
state,
|
state,
|
||||||
ui,
|
ui,
|
||||||
process,
|
process,
|
||||||
|
draw,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,9 @@ pub enum TokenKind {
|
||||||
Canvas,
|
Canvas,
|
||||||
Spacer,
|
Spacer,
|
||||||
|
|
||||||
|
// Draw block
|
||||||
|
Draw,
|
||||||
|
|
||||||
// Literals
|
// Literals
|
||||||
FloatLit(f32),
|
FloatLit(f32),
|
||||||
IntLit(i32),
|
IntLit(i32),
|
||||||
|
|
@ -133,6 +136,7 @@ impl TokenKind {
|
||||||
"param" => TokenKind::Param,
|
"param" => TokenKind::Param,
|
||||||
"canvas" => TokenKind::Canvas,
|
"canvas" => TokenKind::Canvas,
|
||||||
"spacer" => TokenKind::Spacer,
|
"spacer" => TokenKind::Spacer,
|
||||||
|
"draw" => TokenKind::Draw,
|
||||||
"true" => TokenKind::True,
|
"true" => TokenKind::True,
|
||||||
"false" => TokenKind::False,
|
"false" => TokenKind::False,
|
||||||
_ => TokenKind::Ident(s.to_string()),
|
_ => TokenKind::Ident(s.to_string()),
|
||||||
|
|
|
||||||
|
|
@ -375,6 +375,15 @@ impl ScriptVM {
|
||||||
self.push_i(buffer_size as i32)?;
|
self.push_i(buffer_size as i32)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw/mouse opcodes are not valid in the audio ScriptVM
|
||||||
|
Some(OpCode::DrawFillCircle) | Some(OpCode::DrawStrokeCircle) |
|
||||||
|
Some(OpCode::DrawStrokeArc) | Some(OpCode::DrawLine) |
|
||||||
|
Some(OpCode::DrawFillRect) | Some(OpCode::DrawStrokeRect) |
|
||||||
|
Some(OpCode::MouseX) | Some(OpCode::MouseY) | Some(OpCode::MouseDown) |
|
||||||
|
Some(OpCode::StoreParam) => {
|
||||||
|
return Err(ScriptError::InvalidOpcode(op));
|
||||||
|
}
|
||||||
|
|
||||||
None => return Err(ScriptError::InvalidOpcode(op)),
|
None => return Err(ScriptError::InvalidOpcode(op)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -449,3 +458,391 @@ impl ScriptVM {
|
||||||
v
|
v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Draw VM (runs on UI thread, produces draw commands) ----
|
||||||
|
|
||||||
|
/// A draw command produced by the draw block
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum DrawCommand {
|
||||||
|
FillCircle { cx: f32, cy: f32, r: f32, color: u32 },
|
||||||
|
StrokeCircle { cx: f32, cy: f32, r: f32, color: u32, width: f32 },
|
||||||
|
StrokeArc { cx: f32, cy: f32, r: f32, start_deg: f32, end_deg: f32, color: u32, width: f32 },
|
||||||
|
Line { x1: f32, y1: f32, x2: f32, y2: f32, color: u32, width: f32 },
|
||||||
|
FillRect { x: f32, y: f32, w: f32, h: f32, color: u32 },
|
||||||
|
StrokeRect { x: f32, y: f32, w: f32, h: f32, color: u32, width: f32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mouse state passed to the draw VM each frame
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct MouseState {
|
||||||
|
pub x: f32,
|
||||||
|
pub y: f32,
|
||||||
|
pub down: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lightweight VM for executing draw bytecode on the UI thread
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DrawVM {
|
||||||
|
pub bytecode: Vec<u8>,
|
||||||
|
pub constants_f32: Vec<f32>,
|
||||||
|
pub constants_i32: Vec<i32>,
|
||||||
|
stack: Vec<Value>,
|
||||||
|
sp: usize,
|
||||||
|
locals: Vec<Value>,
|
||||||
|
pub params: Vec<f32>,
|
||||||
|
pub state_scalars: Vec<Value>,
|
||||||
|
pub state_arrays: Vec<Vec<f32>>,
|
||||||
|
pub draw_commands: Vec<DrawCommand>,
|
||||||
|
pub mouse: MouseState,
|
||||||
|
instruction_limit: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DrawVM {
|
||||||
|
pub fn new(
|
||||||
|
bytecode: Vec<u8>,
|
||||||
|
constants_f32: Vec<f32>,
|
||||||
|
constants_i32: Vec<i32>,
|
||||||
|
num_params: usize,
|
||||||
|
param_defaults: &[f32],
|
||||||
|
num_state_scalars: usize,
|
||||||
|
state_array_sizes: &[usize],
|
||||||
|
) -> Self {
|
||||||
|
let mut params = vec![0.0f32; num_params];
|
||||||
|
for (i, &d) in param_defaults.iter().enumerate() {
|
||||||
|
if i < params.len() {
|
||||||
|
params[i] = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
bytecode,
|
||||||
|
constants_f32,
|
||||||
|
constants_i32,
|
||||||
|
stack: vec![Value::default(); STACK_SIZE],
|
||||||
|
sp: 0,
|
||||||
|
locals: vec![Value::default(); MAX_LOCALS],
|
||||||
|
params,
|
||||||
|
state_scalars: vec![Value::default(); num_state_scalars],
|
||||||
|
state_arrays: state_array_sizes.iter().map(|&sz| vec![0.0f32; sz]).collect(),
|
||||||
|
draw_commands: Vec::new(),
|
||||||
|
mouse: MouseState::default(),
|
||||||
|
instruction_limit: 1_000_000, // lower limit for draw (runs per frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute the draw bytecode. Call once per frame.
|
||||||
|
/// Draw commands accumulate in `self.draw_commands` (cleared at start).
|
||||||
|
pub fn execute(&mut self) -> Result<(), ScriptError> {
|
||||||
|
self.sp = 0;
|
||||||
|
self.draw_commands.clear();
|
||||||
|
for l in &mut self.locals {
|
||||||
|
*l = Value::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pc: usize = 0;
|
||||||
|
let mut ic: u64 = 0;
|
||||||
|
let limit = self.instruction_limit;
|
||||||
|
|
||||||
|
while pc < self.bytecode.len() {
|
||||||
|
ic += 1;
|
||||||
|
if ic > limit {
|
||||||
|
return Err(ScriptError::ExecutionLimitExceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
let op = self.bytecode[pc];
|
||||||
|
pc += 1;
|
||||||
|
|
||||||
|
match OpCode::from_u8(op) {
|
||||||
|
Some(OpCode::Halt) => return Ok(()),
|
||||||
|
|
||||||
|
Some(OpCode::PushF32) => {
|
||||||
|
let idx = self.read_u16(&mut pc) as usize;
|
||||||
|
self.push_f(self.constants_f32[idx])?;
|
||||||
|
}
|
||||||
|
Some(OpCode::PushI32) => {
|
||||||
|
let idx = self.read_u16(&mut pc) as usize;
|
||||||
|
self.push_i(self.constants_i32[idx])?;
|
||||||
|
}
|
||||||
|
Some(OpCode::PushBool) => {
|
||||||
|
let v = self.bytecode[pc];
|
||||||
|
pc += 1;
|
||||||
|
self.push_b(v != 0)?;
|
||||||
|
}
|
||||||
|
Some(OpCode::Pop) => { self.pop()?; }
|
||||||
|
|
||||||
|
Some(OpCode::LoadLocal) => {
|
||||||
|
let idx = self.read_u16(&mut pc) as usize;
|
||||||
|
self.push(self.locals[idx])?;
|
||||||
|
}
|
||||||
|
Some(OpCode::StoreLocal) => {
|
||||||
|
let idx = self.read_u16(&mut pc) as usize;
|
||||||
|
self.locals[idx] = self.pop()?;
|
||||||
|
}
|
||||||
|
Some(OpCode::LoadParam) => {
|
||||||
|
let idx = self.read_u16(&mut pc) as usize;
|
||||||
|
self.push_f(self.params[idx])?;
|
||||||
|
}
|
||||||
|
Some(OpCode::LoadState) => {
|
||||||
|
let idx = self.read_u16(&mut pc) as usize;
|
||||||
|
self.push(self.state_scalars[idx])?;
|
||||||
|
}
|
||||||
|
Some(OpCode::StoreState) => {
|
||||||
|
let idx = self.read_u16(&mut pc) as usize;
|
||||||
|
self.state_scalars[idx] = self.pop()?;
|
||||||
|
}
|
||||||
|
Some(OpCode::LoadStateArray) => {
|
||||||
|
let arr_id = self.read_u16(&mut pc) as usize;
|
||||||
|
let idx = unsafe { self.pop()?.i };
|
||||||
|
let val = if arr_id < self.state_arrays.len() {
|
||||||
|
let arr_len = self.state_arrays[arr_id].len();
|
||||||
|
let idx = ((idx % arr_len as i32) + arr_len as i32) as usize % arr_len;
|
||||||
|
self.state_arrays[arr_id][idx]
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
self.push_f(val)?;
|
||||||
|
}
|
||||||
|
Some(OpCode::StoreStateArray) => {
|
||||||
|
let arr_id = self.read_u16(&mut pc) as usize;
|
||||||
|
let val = unsafe { self.pop()?.f };
|
||||||
|
let idx = unsafe { self.pop()?.i };
|
||||||
|
if arr_id < self.state_arrays.len() {
|
||||||
|
let arr_len = self.state_arrays[arr_id].len();
|
||||||
|
let idx = ((idx % arr_len as i32) + arr_len as i32) as usize % arr_len;
|
||||||
|
self.state_arrays[arr_id][idx] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(OpCode::ArrayLen) => {
|
||||||
|
let arr_id = self.read_u16(&mut pc) as usize;
|
||||||
|
let len = if arr_id < self.state_arrays.len() {
|
||||||
|
self.state_arrays[arr_id].len() as i32
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
self.push_i(len)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio I/O not available in draw context
|
||||||
|
Some(OpCode::LoadInput) | Some(OpCode::StoreOutput) => {
|
||||||
|
return Err(ScriptError::InvalidOpcode(op));
|
||||||
|
}
|
||||||
|
Some(OpCode::LoadSampleRate) | Some(OpCode::LoadBufferSize) => {
|
||||||
|
return Err(ScriptError::InvalidOpcode(op));
|
||||||
|
}
|
||||||
|
// Sample access not available in draw context
|
||||||
|
Some(OpCode::SampleLen) | Some(OpCode::SampleRead) | Some(OpCode::SampleRateOf) => {
|
||||||
|
pc += 1; // skip slot byte
|
||||||
|
self.push_i(0)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float arithmetic
|
||||||
|
Some(OpCode::AddF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(a + b)?; }
|
||||||
|
Some(OpCode::SubF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(a - b)?; }
|
||||||
|
Some(OpCode::MulF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(a * b)?; }
|
||||||
|
Some(OpCode::DivF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(if b.abs() > 1e-30 { a / b } else { 0.0 })?; }
|
||||||
|
Some(OpCode::ModF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(if b.abs() > 1e-30 { a % b } else { 0.0 })?; }
|
||||||
|
Some(OpCode::NegF) => { let v = self.pop_f()?; self.push_f(-v)?; }
|
||||||
|
|
||||||
|
// Int arithmetic
|
||||||
|
Some(OpCode::AddI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_i(a.wrapping_add(b))?; }
|
||||||
|
Some(OpCode::SubI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_i(a.wrapping_sub(b))?; }
|
||||||
|
Some(OpCode::MulI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_i(a.wrapping_mul(b))?; }
|
||||||
|
Some(OpCode::DivI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_i(if b != 0 { a / b } else { 0 })?; }
|
||||||
|
Some(OpCode::ModI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_i(if b != 0 { a % b } else { 0 })?; }
|
||||||
|
Some(OpCode::NegI) => { let v = self.pop_i()?; self.push_i(-v)?; }
|
||||||
|
|
||||||
|
// Float comparison
|
||||||
|
Some(OpCode::EqF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a == b)?; }
|
||||||
|
Some(OpCode::NeF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a != b)?; }
|
||||||
|
Some(OpCode::LtF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a < b)?; }
|
||||||
|
Some(OpCode::GtF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a > b)?; }
|
||||||
|
Some(OpCode::LeF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a <= b)?; }
|
||||||
|
Some(OpCode::GeF) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_b(a >= b)?; }
|
||||||
|
|
||||||
|
// Int comparison
|
||||||
|
Some(OpCode::EqI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a == b)?; }
|
||||||
|
Some(OpCode::NeI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a != b)?; }
|
||||||
|
Some(OpCode::LtI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a < b)?; }
|
||||||
|
Some(OpCode::GtI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a > b)?; }
|
||||||
|
Some(OpCode::LeI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a <= b)?; }
|
||||||
|
Some(OpCode::GeI) => { let b = self.pop_i()?; let a = self.pop_i()?; self.push_b(a >= b)?; }
|
||||||
|
|
||||||
|
// Logical
|
||||||
|
Some(OpCode::And) => { let b = self.pop_b()?; let a = self.pop_b()?; self.push_b(a && b)?; }
|
||||||
|
Some(OpCode::Or) => { let b = self.pop_b()?; let a = self.pop_b()?; self.push_b(a || b)?; }
|
||||||
|
Some(OpCode::Not) => { let v = self.pop_b()?; self.push_b(!v)?; }
|
||||||
|
|
||||||
|
// Casts
|
||||||
|
Some(OpCode::F32ToI32) => { let v = self.pop_f()?; self.push_i(v as i32)?; }
|
||||||
|
Some(OpCode::I32ToF32) => { let v = self.pop_i()?; self.push_f(v as f32)?; }
|
||||||
|
|
||||||
|
// Control flow
|
||||||
|
Some(OpCode::Jump) => {
|
||||||
|
pc = self.read_u32(&mut pc) as usize;
|
||||||
|
}
|
||||||
|
Some(OpCode::JumpIfFalse) => {
|
||||||
|
let target = self.read_u32(&mut pc) as usize;
|
||||||
|
let cond = self.pop_b()?;
|
||||||
|
if !cond {
|
||||||
|
pc = target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Math builtins
|
||||||
|
Some(OpCode::Sin) => { let v = self.pop_f()?; self.push_f(v.sin())?; }
|
||||||
|
Some(OpCode::Cos) => { let v = self.pop_f()?; self.push_f(v.cos())?; }
|
||||||
|
Some(OpCode::Tan) => { let v = self.pop_f()?; self.push_f(v.tan())?; }
|
||||||
|
Some(OpCode::Asin) => { let v = self.pop_f()?; self.push_f(v.asin())?; }
|
||||||
|
Some(OpCode::Acos) => { let v = self.pop_f()?; self.push_f(v.acos())?; }
|
||||||
|
Some(OpCode::Atan) => { let v = self.pop_f()?; self.push_f(v.atan())?; }
|
||||||
|
Some(OpCode::Atan2) => { let x = self.pop_f()?; let y = self.pop_f()?; self.push_f(y.atan2(x))?; }
|
||||||
|
Some(OpCode::Exp) => { let v = self.pop_f()?; self.push_f(v.exp())?; }
|
||||||
|
Some(OpCode::Log) => { let v = self.pop_f()?; self.push_f(v.ln())?; }
|
||||||
|
Some(OpCode::Log2) => { let v = self.pop_f()?; self.push_f(v.log2())?; }
|
||||||
|
Some(OpCode::Pow) => { let e = self.pop_f()?; let b = self.pop_f()?; self.push_f(b.powf(e))?; }
|
||||||
|
Some(OpCode::Sqrt) => { let v = self.pop_f()?; self.push_f(v.sqrt())?; }
|
||||||
|
Some(OpCode::Floor) => { let v = self.pop_f()?; self.push_f(v.floor())?; }
|
||||||
|
Some(OpCode::Ceil) => { let v = self.pop_f()?; self.push_f(v.ceil())?; }
|
||||||
|
Some(OpCode::Round) => { let v = self.pop_f()?; self.push_f(v.round())?; }
|
||||||
|
Some(OpCode::Trunc) => { let v = self.pop_f()?; self.push_f(v.trunc())?; }
|
||||||
|
Some(OpCode::Fract) => { let v = self.pop_f()?; self.push_f(v.fract())?; }
|
||||||
|
Some(OpCode::Abs) => { let v = self.pop_f()?; self.push_f(v.abs())?; }
|
||||||
|
Some(OpCode::Sign) => { let v = self.pop_f()?; self.push_f(v.signum())?; }
|
||||||
|
Some(OpCode::Clamp) => {
|
||||||
|
let hi = self.pop_f()?;
|
||||||
|
let lo = self.pop_f()?;
|
||||||
|
let v = self.pop_f()?;
|
||||||
|
self.push_f(v.clamp(lo, hi))?;
|
||||||
|
}
|
||||||
|
Some(OpCode::Min) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(a.min(b))?; }
|
||||||
|
Some(OpCode::Max) => { let b = self.pop_f()?; let a = self.pop_f()?; self.push_f(a.max(b))?; }
|
||||||
|
Some(OpCode::Mix) => {
|
||||||
|
let t = self.pop_f()?;
|
||||||
|
let b = self.pop_f()?;
|
||||||
|
let a = self.pop_f()?;
|
||||||
|
self.push_f(a + (b - a) * t)?;
|
||||||
|
}
|
||||||
|
Some(OpCode::Smoothstep) => {
|
||||||
|
let x = self.pop_f()?;
|
||||||
|
let e1 = self.pop_f()?;
|
||||||
|
let e0 = self.pop_f()?;
|
||||||
|
let t = ((x - e0) / (e1 - e0)).clamp(0.0, 1.0);
|
||||||
|
self.push_f(t * t * (3.0 - 2.0 * t))?;
|
||||||
|
}
|
||||||
|
Some(OpCode::IsNan) => { let v = self.pop_f()?; self.push_b(v.is_nan())?; }
|
||||||
|
|
||||||
|
// Draw commands
|
||||||
|
Some(OpCode::DrawFillCircle) => {
|
||||||
|
let color = self.pop_i()? as u32;
|
||||||
|
let r = self.pop_f()?;
|
||||||
|
let cy = self.pop_f()?;
|
||||||
|
let cx = self.pop_f()?;
|
||||||
|
self.draw_commands.push(DrawCommand::FillCircle { cx, cy, r, color });
|
||||||
|
}
|
||||||
|
Some(OpCode::DrawStrokeCircle) => {
|
||||||
|
let width = self.pop_f()?;
|
||||||
|
let color = self.pop_i()? as u32;
|
||||||
|
let r = self.pop_f()?;
|
||||||
|
let cy = self.pop_f()?;
|
||||||
|
let cx = self.pop_f()?;
|
||||||
|
self.draw_commands.push(DrawCommand::StrokeCircle { cx, cy, r, color, width });
|
||||||
|
}
|
||||||
|
Some(OpCode::DrawStrokeArc) => {
|
||||||
|
let width = self.pop_f()?;
|
||||||
|
let color = self.pop_i()? as u32;
|
||||||
|
let end_deg = self.pop_f()?;
|
||||||
|
let start_deg = self.pop_f()?;
|
||||||
|
let r = self.pop_f()?;
|
||||||
|
let cy = self.pop_f()?;
|
||||||
|
let cx = self.pop_f()?;
|
||||||
|
self.draw_commands.push(DrawCommand::StrokeArc { cx, cy, r, start_deg, end_deg, color, width });
|
||||||
|
}
|
||||||
|
Some(OpCode::DrawLine) => {
|
||||||
|
let width = self.pop_f()?;
|
||||||
|
let color = self.pop_i()? as u32;
|
||||||
|
let y2 = self.pop_f()?;
|
||||||
|
let x2 = self.pop_f()?;
|
||||||
|
let y1 = self.pop_f()?;
|
||||||
|
let x1 = self.pop_f()?;
|
||||||
|
self.draw_commands.push(DrawCommand::Line { x1, y1, x2, y2, color, width });
|
||||||
|
}
|
||||||
|
Some(OpCode::DrawFillRect) => {
|
||||||
|
let color = self.pop_i()? as u32;
|
||||||
|
let h = self.pop_f()?;
|
||||||
|
let w = self.pop_f()?;
|
||||||
|
let y = self.pop_f()?;
|
||||||
|
let x = self.pop_f()?;
|
||||||
|
self.draw_commands.push(DrawCommand::FillRect { x, y, w, h, color });
|
||||||
|
}
|
||||||
|
Some(OpCode::DrawStrokeRect) => {
|
||||||
|
let width = self.pop_f()?;
|
||||||
|
let color = self.pop_i()? as u32;
|
||||||
|
let h = self.pop_f()?;
|
||||||
|
let w = self.pop_f()?;
|
||||||
|
let y = self.pop_f()?;
|
||||||
|
let x = self.pop_f()?;
|
||||||
|
self.draw_commands.push(DrawCommand::StrokeRect { x, y, w, h, color, width });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mouse input
|
||||||
|
Some(OpCode::MouseX) => { self.push_f(self.mouse.x)?; }
|
||||||
|
Some(OpCode::MouseY) => { self.push_f(self.mouse.y)?; }
|
||||||
|
Some(OpCode::MouseDown) => { self.push_f(if self.mouse.down { 1.0 } else { 0.0 })?; }
|
||||||
|
|
||||||
|
// Param write
|
||||||
|
Some(OpCode::StoreParam) => {
|
||||||
|
let idx = self.read_u16(&mut pc) as usize;
|
||||||
|
let val = self.pop_f()?;
|
||||||
|
if idx < self.params.len() {
|
||||||
|
self.params[idx] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None => return Err(ScriptError::InvalidOpcode(op)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stack helpers (identical to ScriptVM)
|
||||||
|
#[inline]
|
||||||
|
fn push(&mut self, v: Value) -> Result<(), ScriptError> {
|
||||||
|
if self.sp >= STACK_SIZE { return Err(ScriptError::StackOverflow); }
|
||||||
|
self.stack[self.sp] = v;
|
||||||
|
self.sp += 1;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
fn push_f(&mut self, v: f32) -> Result<(), ScriptError> { self.push(Value { f: v }) }
|
||||||
|
#[inline]
|
||||||
|
fn push_i(&mut self, v: i32) -> Result<(), ScriptError> { self.push(Value { i: v }) }
|
||||||
|
#[inline]
|
||||||
|
fn push_b(&mut self, v: bool) -> Result<(), ScriptError> { self.push(Value { b: v }) }
|
||||||
|
#[inline]
|
||||||
|
fn pop(&mut self) -> Result<Value, ScriptError> {
|
||||||
|
if self.sp == 0 { return Err(ScriptError::StackUnderflow); }
|
||||||
|
self.sp -= 1;
|
||||||
|
Ok(self.stack[self.sp])
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
fn pop_f(&mut self) -> Result<f32, ScriptError> { Ok(unsafe { self.pop()?.f }) }
|
||||||
|
#[inline]
|
||||||
|
fn pop_i(&mut self) -> Result<i32, ScriptError> { Ok(unsafe { self.pop()?.i }) }
|
||||||
|
#[inline]
|
||||||
|
fn pop_b(&mut self) -> Result<bool, ScriptError> { Ok(unsafe { self.pop()?.b }) }
|
||||||
|
#[inline]
|
||||||
|
fn read_u16(&self, pc: &mut usize) -> u16 {
|
||||||
|
let v = u16::from_le_bytes([self.bytecode[*pc], self.bytecode[*pc + 1]]);
|
||||||
|
*pc += 2;
|
||||||
|
v
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
fn read_u32(&self, pc: &mut usize) -> u32 {
|
||||||
|
let v = u32::from_le_bytes([
|
||||||
|
self.bytecode[*pc], self.bytecode[*pc + 1],
|
||||||
|
self.bytecode[*pc + 2], self.bytecode[*pc + 3],
|
||||||
|
]);
|
||||||
|
*pc += 4;
|
||||||
|
v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,10 @@ pub struct GraphState {
|
||||||
pub pending_load_script_file: Option<NodeId>,
|
pub pending_load_script_file: Option<NodeId>,
|
||||||
/// Pending script sample load request from bottom_ui sample picker
|
/// Pending script sample load request from bottom_ui sample picker
|
||||||
pub pending_script_sample_load: Option<PendingScriptSampleLoad>,
|
pub pending_script_sample_load: Option<PendingScriptSampleLoad>,
|
||||||
|
/// Draw VMs for canvas rendering, keyed by node ID
|
||||||
|
pub draw_vms: HashMap<NodeId, beamdsp::DrawVM>,
|
||||||
|
/// Pending param changes from draw block (node_id, param_index, new_value)
|
||||||
|
pub pending_draw_param_changes: Vec<(NodeId, u32, f32)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for GraphState {
|
impl Default for GraphState {
|
||||||
|
|
@ -261,6 +265,8 @@ impl Default for GraphState {
|
||||||
pending_new_script: None,
|
pending_new_script: None,
|
||||||
pending_load_script_file: None,
|
pending_load_script_file: None,
|
||||||
pending_script_sample_load: None,
|
pending_script_sample_load: None,
|
||||||
|
draw_vms: HashMap::new(),
|
||||||
|
pending_draw_param_changes: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1362,7 +1368,21 @@ impl NodeDataTrait for NodeData {
|
||||||
egui::Popup::close_id(ui.ctx(), popup_id);
|
egui::Popup::close_id(ui.ctx(), popup_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render declarative UI elements (sample pickers, groups)
|
// Sync param values from node input ports to draw VM
|
||||||
|
if let Some(draw_vm) = user_state.draw_vms.get_mut(&node_id) {
|
||||||
|
if let Some(node) = _graph.nodes.get(node_id) {
|
||||||
|
for (_name, input_id) in &node.inputs {
|
||||||
|
if let ValueType::Float { value, backend_param_id: Some(pid), .. } = &_graph.get_input(*input_id).value {
|
||||||
|
let idx = *pid as usize;
|
||||||
|
if idx < draw_vm.params.len() {
|
||||||
|
draw_vm.params[idx] = *value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render declarative UI elements (sample pickers, groups, canvas)
|
||||||
if let Some(ref ui_decl) = self.ui_declaration {
|
if let Some(ref ui_decl) = self.ui_declaration {
|
||||||
let backend_node_id = user_state.node_backend_ids.get(&node_id).copied().unwrap_or(0);
|
let backend_node_id = user_state.node_backend_ids.get(&node_id).copied().unwrap_or(0);
|
||||||
render_script_ui_elements(
|
render_script_ui_elements(
|
||||||
|
|
@ -1373,6 +1393,8 @@ impl NodeDataTrait for NodeData {
|
||||||
&user_state.available_clips,
|
&user_state.available_clips,
|
||||||
&mut user_state.sampler_search_text,
|
&mut user_state.sampler_search_text,
|
||||||
&mut user_state.pending_script_sample_load,
|
&mut user_state.pending_script_sample_load,
|
||||||
|
&mut user_state.draw_vms,
|
||||||
|
&mut user_state.pending_draw_param_changes,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1382,7 +1404,17 @@ impl NodeDataTrait for NodeData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render UiDeclaration elements for Script nodes (sample pickers, groups, spacers)
|
/// Convert a u32 RGBA color to egui Color32
|
||||||
|
fn color_from_u32(c: u32) -> egui::Color32 {
|
||||||
|
egui::Color32::from_rgba_unmultiplied(
|
||||||
|
((c >> 24) & 0xFF) as u8,
|
||||||
|
((c >> 16) & 0xFF) as u8,
|
||||||
|
((c >> 8) & 0xFF) as u8,
|
||||||
|
(c & 0xFF) as u8,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render UiDeclaration elements for Script nodes (sample pickers, groups, canvas, spacers)
|
||||||
fn render_script_ui_elements(
|
fn render_script_ui_elements(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
node_id: NodeId,
|
node_id: NodeId,
|
||||||
|
|
@ -1393,9 +1425,121 @@ fn render_script_ui_elements(
|
||||||
available_clips: &[SamplerClipInfo],
|
available_clips: &[SamplerClipInfo],
|
||||||
search_text: &mut String,
|
search_text: &mut String,
|
||||||
pending_load: &mut Option<PendingScriptSampleLoad>,
|
pending_load: &mut Option<PendingScriptSampleLoad>,
|
||||||
|
draw_vms: &mut HashMap<NodeId, beamdsp::DrawVM>,
|
||||||
|
pending_param_changes: &mut Vec<(NodeId, u32, f32)>,
|
||||||
) {
|
) {
|
||||||
for element in elements {
|
for element in elements {
|
||||||
match element {
|
match element {
|
||||||
|
beamdsp::UiElement::Canvas { width, height } => {
|
||||||
|
let size = egui::vec2(*width, *height);
|
||||||
|
let (rect, response) = ui.allocate_exact_size(size, egui::Sense::click_and_drag());
|
||||||
|
let painter = ui.painter_at(rect);
|
||||||
|
|
||||||
|
// Dark background
|
||||||
|
painter.rect_filled(rect, 2.0, egui::Color32::from_rgb(0x1a, 0x1a, 0x1a));
|
||||||
|
|
||||||
|
if let Some(draw_vm) = draw_vms.get_mut(&node_id) {
|
||||||
|
// Set mouse state
|
||||||
|
if let Some(pos) = response.hover_pos() {
|
||||||
|
draw_vm.mouse.x = pos.x - rect.left();
|
||||||
|
draw_vm.mouse.y = pos.y - rect.top();
|
||||||
|
}
|
||||||
|
draw_vm.mouse.down = response.dragged() || response.drag_started();
|
||||||
|
|
||||||
|
// Save params before execution to detect changes
|
||||||
|
let params_before: Vec<f32> = draw_vm.params.clone();
|
||||||
|
|
||||||
|
// Execute draw block
|
||||||
|
if let Err(e) = draw_vm.execute() {
|
||||||
|
painter.text(
|
||||||
|
rect.center(), egui::Align2::CENTER_CENTER,
|
||||||
|
&format!("draw error: {}", e),
|
||||||
|
egui::FontId::monospace(9.0), egui::Color32::RED,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Render draw commands
|
||||||
|
for cmd in &draw_vm.draw_commands {
|
||||||
|
match cmd {
|
||||||
|
beamdsp::DrawCommand::FillCircle { cx, cy, r, color } => {
|
||||||
|
painter.circle_filled(
|
||||||
|
egui::pos2(rect.left() + cx, rect.top() + cy),
|
||||||
|
*r, color_from_u32(*color),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
beamdsp::DrawCommand::StrokeCircle { cx, cy, r, color, width } => {
|
||||||
|
painter.circle_stroke(
|
||||||
|
egui::pos2(rect.left() + cx, rect.top() + cy),
|
||||||
|
*r, egui::Stroke::new(*width, color_from_u32(*color)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
beamdsp::DrawCommand::StrokeArc { cx, cy, r, start_deg, end_deg, color, width } => {
|
||||||
|
// Generate arc as polyline
|
||||||
|
let center = egui::pos2(rect.left() + cx, rect.top() + cy);
|
||||||
|
let start_rad = start_deg.to_radians();
|
||||||
|
let end_rad = end_deg.to_radians();
|
||||||
|
let arc_len = (end_rad - start_rad).abs();
|
||||||
|
let segments = ((arc_len * *r / 2.0).ceil() as usize).max(8).min(128);
|
||||||
|
let points: Vec<egui::Pos2> = (0..=segments)
|
||||||
|
.map(|i| {
|
||||||
|
let t = i as f32 / segments as f32;
|
||||||
|
let angle = start_rad + (end_rad - start_rad) * t;
|
||||||
|
egui::pos2(
|
||||||
|
center.x + angle.cos() * r,
|
||||||
|
center.y + angle.sin() * r,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
painter.add(egui::Shape::line(
|
||||||
|
points,
|
||||||
|
egui::Stroke::new(*width, color_from_u32(*color)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
beamdsp::DrawCommand::Line { x1, y1, x2, y2, color, width } => {
|
||||||
|
painter.line_segment(
|
||||||
|
[
|
||||||
|
egui::pos2(rect.left() + x1, rect.top() + y1),
|
||||||
|
egui::pos2(rect.left() + x2, rect.top() + y2),
|
||||||
|
],
|
||||||
|
egui::Stroke::new(*width, color_from_u32(*color)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
beamdsp::DrawCommand::FillRect { x, y, w, h, color } => {
|
||||||
|
painter.rect_filled(
|
||||||
|
egui::Rect::from_min_size(
|
||||||
|
egui::pos2(rect.left() + x, rect.top() + y),
|
||||||
|
egui::vec2(*w, *h),
|
||||||
|
),
|
||||||
|
0.0, color_from_u32(*color),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
beamdsp::DrawCommand::StrokeRect { x, y, w, h, color, width } => {
|
||||||
|
painter.rect_stroke(
|
||||||
|
egui::Rect::from_min_size(
|
||||||
|
egui::pos2(rect.left() + x, rect.top() + y),
|
||||||
|
egui::vec2(*w, *h),
|
||||||
|
),
|
||||||
|
0.0,
|
||||||
|
egui::Stroke::new(*width, color_from_u32(*color)),
|
||||||
|
egui::StrokeKind::Outside,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect param changes from draw block (e.g. knob drag)
|
||||||
|
for (i, (&before, &after)) in params_before.iter().zip(draw_vm.params.iter()).enumerate() {
|
||||||
|
if (after - before).abs() > 1e-10 {
|
||||||
|
pending_param_changes.push((node_id, i as u32, after));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request repaint while interacting
|
||||||
|
if draw_vm.mouse.down || response.hovered() {
|
||||||
|
ui.ctx().request_repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
beamdsp::UiElement::Sample(slot_name) => {
|
beamdsp::UiElement::Sample(slot_name) => {
|
||||||
// Find the slot index by name
|
// Find the slot index by name
|
||||||
let slot_index = sample_slot_names.iter().position(|n| n == slot_name);
|
let slot_index = sample_slot_names.iter().position(|n| n == slot_name);
|
||||||
|
|
@ -1454,14 +1598,15 @@ fn render_script_ui_elements(
|
||||||
ui, node_id, backend_node_id,
|
ui, node_id, backend_node_id,
|
||||||
children, sample_slot_names, script_sample_names,
|
children, sample_slot_names, script_sample_names,
|
||||||
available_clips, search_text, pending_load,
|
available_clips, search_text, pending_load,
|
||||||
|
draw_vms, pending_param_changes,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
beamdsp::UiElement::Spacer(height) => {
|
beamdsp::UiElement::Spacer(height) => {
|
||||||
ui.add_space(*height);
|
ui.add_space(*height);
|
||||||
}
|
}
|
||||||
beamdsp::UiElement::Param(_) | beamdsp::UiElement::Canvas { .. } => {
|
beamdsp::UiElement::Param(_) => {
|
||||||
// Params are handled as inline input ports; Canvas is phase 6
|
// Params are handled as inline input ports
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -391,6 +391,12 @@ impl NodeGraphPane {
|
||||||
node.user_data.ui_declaration = Some(compiled.ui_declaration.clone());
|
node.user_data.ui_declaration = Some(compiled.ui_declaration.clone());
|
||||||
node.user_data.sample_slot_names = compiled.sample_slots.clone();
|
node.user_data.sample_slot_names = compiled.sample_slots.clone();
|
||||||
}
|
}
|
||||||
|
// Store draw VM in GraphState (needs &mut, can't live on immutable NodeData)
|
||||||
|
if let Some(draw_vm) = &compiled.draw_vm {
|
||||||
|
self.user_state.draw_vms.insert(node_id, draw_vm.clone());
|
||||||
|
} else {
|
||||||
|
self.user_state.draw_vms.remove(&node_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_graph_response(
|
fn handle_graph_response(
|
||||||
|
|
@ -2695,6 +2701,33 @@ impl crate::panes::PaneRenderer for NodeGraphPane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle param changes from draw block (canvas knob drag etc.)
|
||||||
|
if !self.user_state.pending_draw_param_changes.is_empty() {
|
||||||
|
let changes: Vec<_> = self.user_state.pending_draw_param_changes.drain(..).collect();
|
||||||
|
if let Some(backend_track_id) = self.track_id.and_then(|tid| shared.layer_to_track_map.get(&tid).copied()) {
|
||||||
|
if let Some(controller_arc) = &shared.audio_controller {
|
||||||
|
let mut controller = controller_arc.lock().unwrap();
|
||||||
|
for (node_id, param_id, value) in changes {
|
||||||
|
// Send to backend
|
||||||
|
if let Some(backend_id) = self.node_id_map.get(&node_id) {
|
||||||
|
let BackendNodeId::Audio(node_idx) = backend_id;
|
||||||
|
controller.graph_set_parameter(backend_track_id, node_idx.index() as u32, param_id, value);
|
||||||
|
}
|
||||||
|
// Update frontend graph input port value
|
||||||
|
if let Some(node) = self.state.graph.nodes.get(node_id) {
|
||||||
|
for (_name, input_id) in &node.inputs {
|
||||||
|
if let ValueType::Float { value: ref mut v, backend_param_id: Some(pid), .. } = self.state.graph.inputs[*input_id].value {
|
||||||
|
if pid == param_id {
|
||||||
|
*v = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve Script nodes loaded from preset: find or create ScriptDefinitions
|
// Resolve Script nodes loaded from preset: find or create ScriptDefinitions
|
||||||
// (ports were already rebuilt during load_graph_from_json, this just sets script_id)
|
// (ports were already rebuilt during load_graph_from_json, this just sets script_id)
|
||||||
if !self.pending_script_resolutions.is_empty() {
|
if !self.pending_script_resolutions.is_empty() {
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ fn beamdsp_syntax() -> Syntax {
|
||||||
"if", "else", "for", "in", "let", "mut",
|
"if", "else", "for", "in", "let", "mut",
|
||||||
"generator", "effect", "utility",
|
"generator", "effect", "utility",
|
||||||
"audio", "cv", "midi",
|
"audio", "cv", "midi",
|
||||||
"param", "sample", "group", "canvas", "spacer",
|
"param", "sample", "group", "canvas", "spacer", "draw",
|
||||||
]),
|
]),
|
||||||
types: std::collections::BTreeSet::from([
|
types: std::collections::BTreeSet::from([
|
||||||
"f32", "int", "bool",
|
"f32", "int", "bool",
|
||||||
|
|
@ -119,6 +119,9 @@ fn beamdsp_syntax() -> Syntax {
|
||||||
"len", "cv_or", "float",
|
"len", "cv_or", "float",
|
||||||
"sample_len", "sample_read", "sample_rate_of",
|
"sample_len", "sample_read", "sample_rate_of",
|
||||||
"sample_rate", "buffer_size",
|
"sample_rate", "buffer_size",
|
||||||
|
"fill_circle", "stroke_circle", "stroke_arc",
|
||||||
|
"fill_rect", "stroke_rect", "line",
|
||||||
|
"mouse_x", "mouse_y", "mouse_down",
|
||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue