@@ -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"] | |||
crate-type = ["rlib", "cdylib"] |
@@ -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. |
@@ -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<InstructionType>, | |||
}, | |||
} | |||
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<f64> 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} | |||
} | |||
} |
@@ -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<RegexPatterns> = 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<Instruction, String> { | |||
let cache_lock = REGEX_CACHE.read().unwrap(); | |||
if count(line, '(') > 1 { | |||
// do multi parsing | |||
let mut instructions = Vec::<InstructionType>::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<InstructionType, String> { | |||
let name = c.get(1).unwrap().as_str(); | |||
let param1 = c.get(2).unwrap().as_str().parse::<f64>().unwrap(); | |||
let param2 = c.get(3).unwrap().as_str().parse::<f64>().unwrap(); | |||
let param3 = c.get(4).unwrap().as_str().parse::<f64>().unwrap(); | |||
// optional 4th param | |||
// let param4 = c.get(5).unwrap().as_str().parse::<f64>(); | |||
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::<f64>().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::<f64>().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 | |||
} |
@@ -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<u8> = 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]); | |||
} | |||
} |
@@ -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<Vec<FilmscriptFile>> = RwLock::new(Vec::new()); | |||
} | |||
pub struct FilmscriptFile { | |||
path: Option<PathBuf>, | |||
buffer: Box<dyn BufRead + Sync + Send>, | |||
index: usize, | |||
instructions: Vec<Instruction>, | |||
done: bool, | |||
done_msg: String, | |||
} | |||
impl FilmscriptFile { | |||
pub fn from_path<'a>(path: &'a Path) -> std::io::Result<FilmscriptFile> { | |||
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<FilmscriptFile> { | |||
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<FilmscriptFile> { | |||
Self::from_vec(&Vec::from(buf)) | |||
} | |||
pub fn from_vec(buf: &Vec<u8>) -> std::io::Result<FilmscriptFile> { | |||
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<PathBuf> { | |||
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<Instruction, String> { | |||
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<usize> { | |||
self.buffer.read(buf) | |||
} | |||
} | |||
struct VectorReader { | |||
vec: Vec<u8> | |||
} | |||
impl Read for VectorReader { | |||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { | |||
self.vec.as_slice().read(buf) | |||
} | |||
} |
@@ -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<CameraData> 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<Vector3> 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) | |||
} |