From d630bb58270136f58b081ae273d27cd5435f7ee7 Mon Sep 17 00:00:00 2001 From: NGnius Date: Sun, 2 May 2021 13:17:52 -0400 Subject: [PATCH] Implement instruction parser (untested) --- Cargo.toml | 4 +- README.md | 55 +++++++++++++++ src/instruction.rs | 131 ++++++++++++++++++++++++++++++++++++ src/instruction_parser.rs | 121 +++++++++++++++++++++++++++++++++ src/lib.rs | 30 ++++++++- src/text_parser.rs | 126 ++++++++++++++++++++++++++++++++++ src/text_parser_bindings.rs | 84 +++++++++++++++++++++++ 7 files changed, 549 insertions(+), 2 deletions(-) create mode 100644 src/instruction.rs create mode 100644 src/instruction_parser.rs create mode 100644 src/text_parser.rs create mode 100644 src/text_parser_bindings.rs diff --git a/Cargo.toml b/Cargo.toml index b9ea7c7..b08ed20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,9 @@ edition = "2018" [dependencies] libc = "0.2" clap = "2" +lazy_static = "1.4" +regex = "1" [lib] name = "filmscript" -crate-type = ["rlib", "cdylib"] \ No newline at end of file +crate-type = ["rlib", "cdylib"] diff --git a/README.md b/README.md index 526783e..bf94eb9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,58 @@ # FilmScript The best Canadian camera technology since IMAX. + +## Syntax + +FilmScript uses a simple function syntax which you will be familiar with from most modern programming languages like C and Python. +The end of an operation is denoted by a new line `\n`. +Multiple operations can be performed simultaneously by placing them on the same line. +Lines are executed sequentially starting from the top of the file until the end. + +Here's a generic example of the syntax: + +``` +operation1(operand1, operand2, operand3) +operation2(operand1, operand2) +operation3a(operand1) operation3b(operand1, operand2) +``` + +## Operations + +FilmScript is a set of commands that can be used to describe a camera and it's movements in time. +Supported operations and their operands are outlined below. + +Here's an example of a functioning FilmScript file using some of the supported operations: + +``` +#version 0.1.0 +look(0, 0, 0), move(0, 0, 0) +track(20, 0, 20, 5), rotate(0, -5.5, 0, 5) +rotate(0, 5.5, 0, 2) +track(-20, 5, 0, 5) +``` + +### TRACK(DISTANCE_X, DISTANCE_Y, DISTANCE_Z, TIME=0) + +Move the camera relative to the current position and rotation. +Use a negative value to move backwards. +This operation supports linear interpolation through the optional TIME operand (seconds). + +### MOVE(X, Y, Z) + +Move the camera to an absolute position. + +### ROTATE(DEGREES_X, DEGREES_Y, DEGREES_Z, TIME=0) + +Rotate the camera clockwise around the axes, relative to the current rotation. +Use a negative value to rotate counter-clockwise. +This operation supports linear interpolation through the optional TIME operand (seconds). + +### LOOK(DEGREES_X, DEGREES_Y, DEGREES_Z) + +Rotate the camera to an absolute rotation. + +### MULTI(OPERATION1, OPERATION2, ...) + +Perform multiple operations simultaneously. +This is implicitly used whenever more than one operation appears on a single line. diff --git a/src/instruction.rs b/src/instruction.rs new file mode 100644 index 0000000..c80ccaf --- /dev/null +++ b/src/instruction.rs @@ -0,0 +1,131 @@ + +#[derive(Clone)] +pub(crate) struct Instruction { + pub instr: InstructionType, + pub start: f64, + pub progress: f64, + pub base: CameraData, + // impl in instruction_parser.rs +} + +#[derive(Clone)] +pub(crate) enum InstructionType { + Track { + vector: Vector3, + time: f64, + }, + Move { + vector: Vector3, + }, + Rotate { + vector: Vector3, + time: f64, + }, + Look { + vector: Vector3, + }, + Multi { + instructions: Vec, + }, +} + +impl InstructionType { + pub fn time(&self) -> f64 { + match self { + Self::Track {time, ..} => *time, + Self::Move {..} => 0.0, + Self::Rotate {time, ..} => *time, + Self::Look {..} => 0.0, + Self::Multi {instructions, ..} => { + let mut max: f64 = 0.0; + for i in instructions { + if i.time() > max { + max = i.time(); + } + } + max + }, + } + } + + pub fn lerp(&self, start: f64, now: f64) -> CameraData { + let delta = now - start; + let mut portion = 1.0; + let time = self.time(); + if time > 0.0 && time < delta { + portion = delta/self.time(); + } + match self { + Self::Track {vector, ..} => CameraData {rotation: Vector3::default(), position: *vector * portion}, + Self::Move {vector, ..} => CameraData {rotation: Vector3::default(), position: *vector}, + Self::Rotate {vector, ..} => CameraData {rotation: *vector * portion, position: Vector3::default()}, + Self::Look {vector, ..} => CameraData {rotation: *vector, position: Vector3::default()}, + Self::Multi {instructions, ..} => { + let mut data = CameraData::default(); + for i in instructions { + data += i.lerp(start, now); + } + data + }, + } + } +} + +#[derive(Clone, Copy)] +pub(crate) struct CameraData { + pub rotation: Vector3, + pub position: Vector3, +} + +impl std::default::Default for CameraData { + fn default() -> Self { + Self {rotation: Vector3::default(), position: Vector3::default()} + } +} + +impl std::ops::Add for CameraData { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Self {rotation: self.rotation + rhs.rotation, position: self.position + rhs.position} + } +} + +impl std::ops::AddAssign for CameraData { + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +#[derive(Copy, Clone)] +pub(crate) struct Vector3 { + pub x: f64, + pub y: f64, + pub z: f64, +} + +impl std::default::Default for Vector3 { + fn default() -> Self { + Self {x:0.0, y:0.0, z:0.0} + } +} + +impl std::ops::Mul for Vector3 { + type Output = Self; + + fn mul(self, rhs: f64) -> Self { + Self { + x: self.x * rhs, + y: self.y * rhs, + z: self.z * rhs, + } + } +} + +impl std::ops::Add for Vector3 { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Self {x: self.x + rhs.x, y: self.y + rhs.y, z: self.z + rhs.z} + } +} diff --git a/src/instruction_parser.rs b/src/instruction_parser.rs new file mode 100644 index 0000000..dcbb889 --- /dev/null +++ b/src/instruction_parser.rs @@ -0,0 +1,121 @@ +use regex::{Regex, RegexBuilder, Captures}; +use std::sync::RwLock; + +use crate::instruction::{Instruction, InstructionType, Vector3, CameraData}; + +lazy_static!{ + static ref REGEX_CACHE: RwLock = RwLock::new(RegexPatterns{ + single_instruction: RegexBuilder::new(r"(\w+)\s*\((\d+(?:\.\d+)?),?\s*(\d+(?:\.\d+)?),?\s*(\d+(?:\.\d+)?)(?:,?\s*(\d+(?:\.\d+)?))?\s*\)").case_insensitive(true).build().unwrap(), + //multi_instruction: RegexBuilder::new("").case_insensitive(true).build().unwrap(), + }); +} + +struct RegexPatterns { + single_instruction: Regex, +} + + +impl Instruction { + pub fn parse_line(line: &str) -> Result { + let cache_lock = REGEX_CACHE.read().unwrap(); + if count(line, '(') > 1 { + // do multi parsing + let mut instructions = Vec::::new(); + for capture in cache_lock.single_instruction.captures_iter(line) { + let ins = Self::parse_single(&capture)?; + instructions.push(ins); + } + return Ok(Instruction { + instr: InstructionType::Multi{instructions}, + start: 0.0, + progress: 0.0, + base: CameraData::default(), + }) + } else { + // assume single instruction on line + if let Some(captures) = cache_lock.single_instruction.captures(line) { + let instr = Self::parse_single(&captures)?; + return Ok(Instruction { + instr, + start: 0.0, + progress: 0.0, + base: CameraData::default(), + }) + } else { + return Err("Invalid line".to_string()); + } + } + } + + fn parse_single(c: &Captures) -> Result { + let name = c.get(1).unwrap().as_str(); + let param1 = c.get(2).unwrap().as_str().parse::().unwrap(); + let param2 = c.get(3).unwrap().as_str().parse::().unwrap(); + let param3 = c.get(4).unwrap().as_str().parse::().unwrap(); + // optional 4th param + // let param4 = c.get(5).unwrap().as_str().parse::(); + match name.to_uppercase().as_str() { + "TRACK" => { + if let Some(param4) = c.get(5) { + Ok(InstructionType::Track { + vector: Vector3{x: param1, y: param2, z: param3}, + time: param4.as_str().parse::().unwrap(), + }) + } else { + Ok(InstructionType::Track { + vector: Vector3{x: param1, y: param2, z: param3}, + time: 0.0, + }) + } + }, + "MOVE" => Ok(InstructionType::Move { + vector: Vector3{x: param1, y: param2, z: param3}, + }), + "ROTATE" => { + if let Some(param4) = c.get(5) { + Ok(InstructionType::Rotate { + vector: Vector3{x: param1, y: param2, z: param3}, + time: param4.as_str().parse::().unwrap(), + }) + } else { + Ok(InstructionType::Track { + vector: Vector3{x: param1, y: param2, z: param3}, + time: 0.0, + }) + } + }, + "LOOK" => Ok(InstructionType::Look { + vector: Vector3{x: param1, y: param2, z: param3}, + }), + _ => Err(format!("Invalid instruction {}", name)) + } + } + + pub fn start(&mut self, now: f64, base: CameraData) { + self.start = now; + self.base = base; + } + + pub fn lerp(&mut self, now: f64) -> CameraData { + self.progress += now; + match self.instr { + InstructionType::Look {..} | InstructionType::Move {..} => self.instr.lerp(self.start, now), + InstructionType::Track {..} | InstructionType::Rotate {..} | InstructionType::Multi{..} => self.base + self.instr.lerp(self.start, now), + } + } + + pub fn done(&self) -> bool { + !(self.progress < (self.start + self.instr.time())) + } +} + +// string helper functions +fn count(target: &str, chr: char) -> usize { + let mut times: usize = 0; + for c in target.chars() { + if chr == c { + times+=1; + } + } + times +} diff --git a/src/lib.rs b/src/lib.rs index ac9e4f9..dadd13b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,16 @@ +#[macro_use] +extern crate lazy_static; + use std::ffi::CString; use std::os::raw::c_char; mod version; +mod text_parser; +mod text_parser_bindings; +mod instruction; +mod instruction_parser; -pub unsafe fn allocate_cstring(input: &str) -> *mut c_char { +pub(crate) unsafe fn allocate_cstring(input: &str) -> *mut c_char { let input_c = CString::new(input).expect("Rust &str -> CString conversion failed"); let space = libc::malloc(libc::strlen(input_c.as_ptr()) + 1) as *mut c_char; libc::strcpy(space, input_c.as_ptr()); @@ -12,8 +19,29 @@ pub unsafe fn allocate_cstring(input: &str) -> *mut c_char { #[cfg(test)] mod tests { + use std::io::Read; + #[test] fn it_works() { assert_eq!(2 + 2, 4); } + + #[test] + fn vec_read_test() { + let vec: Vec = vec![1, 2, 3, 4, 5]; + let file_res = crate::text_parser::FilmscriptFile::from_vec(&vec); + assert!(file_res.is_ok()); + let mut f = file_res.unwrap(); + let buf: &mut [u8] = &mut [0; 1]; + assert!(f.read(buf).is_ok()); + println!("Buffer contains value: {}", buf[0]); + assert!(f.read(buf).is_ok()); + println!("Buffer contains value: {}", buf[0]); + assert!(f.read(buf).is_ok()); + println!("Buffer contains value: {}", buf[0]); + assert!(f.read(buf).is_ok()); + println!("Buffer contains value: {}", buf[0]); + assert!(f.read(buf).is_ok()); + println!("Buffer contains value: {}", buf[0]); + } } diff --git a/src/text_parser.rs b/src/text_parser.rs new file mode 100644 index 0000000..356d4b9 --- /dev/null +++ b/src/text_parser.rs @@ -0,0 +1,126 @@ +use std::fs::File; +use std::io::{BufReader, BufRead, Read}; +use std::path::{Path, PathBuf}; +use std::sync::{RwLock}; + +use crate::instruction::{Instruction, CameraData}; + +lazy_static! { + pub(crate) static ref FILE_HANDLES: RwLock> = RwLock::new(Vec::new()); +} + +pub struct FilmscriptFile { + path: Option, + buffer: Box, + index: usize, + instructions: Vec, + done: bool, + done_msg: String, +} + +impl FilmscriptFile { + pub fn from_path<'a>(path: &'a Path) -> std::io::Result { + let new_reader = File::open(path)?; + std::io::Result::Ok(FilmscriptFile { + path: Some(PathBuf::from(path)), + //reader: new_reader, + buffer: Box::new(BufReader::new(new_reader)), + index: 0, + instructions: Vec::new(), + done: false, + done_msg: String::new(), + }) + } + + pub fn from_static_slice(buf: &'static[u8]) -> std::io::Result { + std::io::Result::Ok(FilmscriptFile { + path: None, + buffer: Box::new(buf), + index: 0, + instructions: Vec::new(), + done: false, + done_msg: String::new(), + }) + } + + pub fn from_slice(buf: &[u8]) -> std::io::Result { + Self::from_vec(&Vec::from(buf)) + } + + pub fn from_vec(buf: &Vec) -> std::io::Result { + let new_reader = VectorReader{vec: buf.clone()}; + std::io::Result::Ok(FilmscriptFile { + path: None, + buffer: Box::new(BufReader::new(new_reader)), + index: 0, + instructions: Vec::new(), + done: false, + done_msg: String::new(), + }) + } + + pub fn filepath(&self) -> Option { + self.path.clone() + } + + pub fn is_done(&self) -> bool { + self.done + } + + pub fn done_msg(&self) -> String { + self.done_msg.clone() + } + + fn next_instruction(&mut self) -> Result { + let mut buf = String::new(); + if let Ok(len) = self.buffer.read_line(&mut buf) { + let instr = Instruction::parse_line(&buf[..len])?; + self.instructions.push(instr.clone()); + return Ok(instr); + } + Err("End of file".to_string()) + } + + pub(crate) fn lerp(&mut self, now: f64) -> CameraData { + if self.index >= self.instructions.len() { + match self.next_instruction() { + Ok(_) => { + if self.index == 0 { + // do initial init + self.instructions[0].start(now, CameraData::default()); + } else { + let end = self.instructions[self.index-1].start + self.instructions[self.index-1].instr.time(); + let base = self.instructions[self.index-1].instr.lerp(0.0, self.instructions[self.index-1].instr.time()); + self.instructions[self.index].start(end, base); + } + }, + Err(msg) => { + self.done = true; + self.done_msg = msg; + }, + } + } + let curr_instr = &mut self.instructions[self.index]; + let cam_data = curr_instr.lerp(now); + if curr_instr.done() { + self.index+=1; + } + cam_data + } +} + +impl Read for FilmscriptFile { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.buffer.read(buf) + } +} + +struct VectorReader { + vec: Vec +} + +impl Read for VectorReader { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.vec.as_slice().read(buf) + } +} diff --git a/src/text_parser_bindings.rs b/src/text_parser_bindings.rs new file mode 100644 index 0000000..ee87d9d --- /dev/null +++ b/src/text_parser_bindings.rs @@ -0,0 +1,84 @@ +//use libc; +use std::os::raw::c_char; +use std::ffi::CStr; +use std::convert::From; +use std::time::Instant; + +use crate::text_parser::{FilmscriptFile, FILE_HANDLES}; +use crate::allocate_cstring; +use crate::instruction::{CameraData, Vector3}; + +#[repr(C)] +pub struct CameraDataC { + pub rotation: Vector3C, + pub position: Vector3C, +} + +impl From for CameraDataC { + fn from(c: CameraData) -> Self { + Self { + rotation: Vector3C::from(c.rotation), + position: Vector3C::from(c.position), + } + } +} + +#[repr(C)] +pub struct Vector3C { + pub x: f64, + pub y: f64, + pub z: f64, +} + +impl From for Vector3C { + fn from(v: Vector3) -> Self { + Self { + x: v.x, + y: v.y, + z: v.z, + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn filmscript_open(filepath: *const c_char) -> i64 { + let c_str = CStr::from_ptr(filepath); + if let Ok(path) = c_str.to_str() { + if let Ok(fsf) = FilmscriptFile::from_path(std::path::Path::new(path)) { + FILE_HANDLES.write().unwrap().push(fsf); + return FILE_HANDLES.read().unwrap().len() as i64; + } else { + println!("Failed to open '{}' in filmscript_open(*char)", path); + } + } else { + println!("Invalid *char parameter in filmscript_open(*char)"); + } + return -1; +} + +#[no_mangle] +pub unsafe extern "C" fn filmscript_path(handle: i64) -> *const c_char { + if let Some(path) = FILE_HANDLES.read().unwrap()[(handle-1) as usize].filepath().clone() { + if let Some(s_path) = path.to_str() { + return allocate_cstring(s_path); + } + } + allocate_cstring("") +} + +#[no_mangle] +pub unsafe extern "C" fn filmscript_is_done(handle: i64) -> bool { + FILE_HANDLES.read().unwrap()[(handle-1) as usize].is_done() +} + +#[no_mangle] +pub unsafe extern "C" fn filmscript_done_message(handle: i64) -> *const c_char { + allocate_cstring(&FILE_HANDLES.read().unwrap()[(handle-1) as usize].done_msg()) +} + +#[no_mangle] +pub unsafe extern "C" fn filmscript_poll(handle: i64) -> CameraDataC { + let now = Instant::now().elapsed().as_secs_f64(); + let cdata = FILE_HANDLES.write().unwrap()[(handle-1) as usize].lerp(now); + CameraDataC::from(cdata) +}