Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions inox2d/examples/copy-parameters.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::PathBuf;

use clap::Parser;
use inox2d::formats::inp::{parse_inp_parts, serialize_parts};
use inox2d::formats::vendors::{SessionBinding, SESSION_BINDINGS_KEY};
use inox2d::puppet::Puppet;
use std::collections::HashSet;

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[arg(help = "The .inp to copy parameter mappings from")]
in_path: PathBuf,

#[arg(help = "The .inp to copy parameter mappings to")]
out_path: PathBuf,
}

fn main() {
let cli = Cli::parse();

let indata = {
let file = File::open(cli.in_path).unwrap();
let mut file = BufReader::new(file);
let mut data = Vec::new();
file.read_to_end(&mut data).unwrap();
data
};

let (_in_puppet, _in_textures, in_vendors) = match parse_inp_parts(indata.as_slice()) {
Ok(m) => m,
Err(e) => {
eprintln!("Error when reading input puppet: {e}");
return;
}
};

let outdata = {
let file = File::open(cli.out_path.clone()).unwrap();
let mut file = BufReader::new(file);
let mut data = Vec::new();
file.read_to_end(&mut data).unwrap();
data
};

let (out_puppet, out_textures, mut out_vendors) = match parse_inp_parts(outdata.as_slice()) {
Ok(m) => m,
Err(e) => {
eprintln!("Error when reading input puppet: {e}");
return;
}
};

// Bindings are treated as "vendor data" for some reason
let mut in_bindings = None;
for (index, item) in in_vendors.iter().enumerate() {
if item.name == SESSION_BINDINGS_KEY {
in_bindings = Some(index);
}
}

let in_bindings = in_vendors
.get(in_bindings.expect("Input puppet has bindings data"))
.expect("Bindings data didn't change from prior lookup");

let mut out_bindings = None;
for (index, item) in out_vendors.iter().enumerate() {
if item.name == SESSION_BINDINGS_KEY {
out_bindings = Some(index);
}
}

if out_bindings.is_some() {
eprintln!("Output puppet already has bindings data! Refusing to continue.");
return;
}

// Validate that all params mentioned in in_bindings exists in out_puppet.
let out_puppet_data = Puppet::new_from_json(&out_puppet).expect("valid puppet JSON");
let mut good_bindings = HashSet::new();
for (_param_name, param) in out_puppet_data.params {
good_bindings.insert(param.uuid);
}

let in_bindings_data = SessionBinding::new_from_json_list(&in_bindings.payload).expect("valid bindings data");
for binding_block in in_bindings_data {
if !good_bindings.contains(&binding_block.param) {
eprintln!(
"Binding name {} refers to nonexistent param {:?}.",
binding_block.name, binding_block.param
);
eprintln!("These puppets do not have compatible bindings and cannot have bindings copied.");
return;
}
}

out_vendors.push(in_bindings.clone());

let file = File::create(cli.out_path).unwrap();
serialize_parts(file, out_puppet, &out_textures, &out_vendors).unwrap();
}
1 change: 1 addition & 0 deletions inox2d/src/formats.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod inp;
mod json;
mod payload;
pub mod vendors;

use glam::Vec2;

Expand Down
76 changes: 71 additions & 5 deletions inox2d/src/formats/inp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use super::json::JsonError;
use super::payload::InoxParseError;
use super::{read_be_u32, read_n, read_u8, read_vec};

use json::JsonValue;

#[derive(Debug, thiserror::Error)]
#[error("Could not parse INP file\n - {0}")]
pub enum ParseInpError {
Expand All @@ -40,8 +42,8 @@ const TEX_SECT: &[u8] = b"TEX_SECT";
/// Optional EXTended Vendor Data section for app provided settings for the puppet
const EXT_SECT: &[u8] = b"EXT_SECT";

/// Parse `.inp` and `.inx` files.
pub fn parse_inp<R: Read>(mut data: R) -> Result<Model, ParseInpError> {
/// Parse `.inp` and `.inx` files into parts.
pub fn parse_inp_parts<R: Read>(mut data: R) -> Result<(JsonValue, Vec<ModelTexture>, Vec<VendorData>), ParseInpError> {
// check magic bytes
let magic = read_n::<_, 8>(&mut data)?;
if magic != MAGIC {
Expand All @@ -52,8 +54,7 @@ pub fn parse_inp<R: Read>(mut data: R) -> Result<Model, ParseInpError> {
let length = read_be_u32(&mut data)? as usize;
let payload = read_vec(&mut data, length)?;
let payload = std::str::from_utf8(&payload)?;
let payload = json::parse(payload)?;
let puppet = Puppet::new_from_json(&payload)?;
let puppet = json::parse(payload)?;

// check texture section header
let tex_sect = read_n::<_, 8>(&mut data).map_err(|_| ParseInpError::NoTexSect)?;
Expand Down Expand Up @@ -101,14 +102,23 @@ pub fn parse_inp<R: Read>(mut data: R) -> Result<Model, ParseInpError> {
_ => Vec::new(),
};

Ok((puppet, textures, vendors))
}

/// Parse `.inp` and `.inx` files into a Model.
pub fn parse_inp<R: Read>(data: R) -> Result<Model, ParseInpError> {
let (puppet_json, textures, vendors) = parse_inp_parts(data)?;

let puppet = Puppet::new_from_json(&puppet_json)?;

Ok(Model {
puppet,
textures,
vendors,
})
}

/// Parse `.inp` and `.inx` files.
/// Parse `.inp` and `.inx` files into an on-disk format.
pub fn dump_inp<R: Read>(mut data: R, directory: &Path) -> Result<(), ParseInpError> {
// check magic bytes
let magic = read_n::<_, 8>(&mut data)?;
Expand Down Expand Up @@ -188,6 +198,7 @@ pub fn dump_inp<R: Read>(mut data: R, directory: &Path) -> Result<(), ParseInpEr
Ok(())
}

/// Serialize on-disk JSON and texture files into an INP file.
pub fn dump_to_inp<W: Write>(directory: &Path, w: &mut W) -> io::Result<()> {
let mut payload_file = File::open(directory.join("payload.json"))?;

Expand Down Expand Up @@ -243,3 +254,58 @@ pub fn dump_to_inp<W: Write>(directory: &Path, w: &mut W) -> io::Result<()> {
w.flush().unwrap();
Ok(())
}

/// Serialize an INP1 file as parts.
///
/// The parts taken by this function are equivalent to those specified in
/// `parse_inp_parts`.
pub fn serialize_parts<W: Write>(
mut file: W,
puppet: JsonValue,
textures: &[ModelTexture],
vendors: &[VendorData],
) -> Result<(), ParseInpError> {
file.write_all(MAGIC)?;

let json = json::stringify(puppet).into_bytes();
file.write_all(&(json.len() as u32).to_be_bytes())?;
file.write_all(&json)?;

file.write_all(TEX_SECT)?;

file.write_all(&(textures.len() as u32).to_be_bytes())?;
for texture in textures.iter() {
file.write_all(&(texture.data.len() as u32).to_be_bytes())?;

file.write_all(
&(match texture.format {
ImageFormat::Png => 0,
ImageFormat::Tga => 1,
_ => return Err(ParseInpError::InvalidTexEncoding(0xFF)), //TODO: WriteInpError
} as u8)
.to_be_bytes(),
)?;

file.write_all(&texture.data)?;
}

if vendors.is_empty() {
//Don't write extended data if we don't have to.
return Ok(());
}

file.write_all(EXT_SECT)?;
file.write_all(&(vendors.len() as u32).to_be_bytes())?;

for vendor in vendors.iter() {
let name = vendor.name.as_bytes();
file.write_all(&(name.len() as u32).to_be_bytes())?;
file.write_all(&name)?;

let json = json::stringify(vendor.payload.clone()).into_bytes();
file.write_all(&(json.len() as u32).to_be_bytes())?;
file.write_all(&json)?;
}

Ok(())
}
6 changes: 4 additions & 2 deletions inox2d/src/formats/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ pub enum InoxParseError {
OddNumberOfFloatsInList(usize),
#[error("Expected 2 floats in list, got {0}")]
Not2FloatsInList(usize),
#[error("Unknown vendor data value \"{1}\" for key {0}")]
UnknownVendorKeyValue(&'static str, String),
}

// json structure helpers
Expand All @@ -63,14 +65,14 @@ fn vals<T>(key: &str, res: InoxParseResult<T>) -> InoxParseResult<T> {
res.map_err(|e| e.nested(key))
}

fn as_nested_list(index: usize, val: &json::JsonValue) -> InoxParseResult<&[json::JsonValue]> {
pub fn as_nested_list(index: usize, val: &json::JsonValue) -> InoxParseResult<&[json::JsonValue]> {
match val {
json::JsonValue::Array(arr) => Ok(arr),
_ => Err(InoxParseError::JsonError(JsonError::ValueIsNotList(index.to_string()))),
}
}

fn as_object<'file>(msg: &str, val: &'file JsonValue) -> InoxParseResult<JsonObject<'file>> {
pub fn as_object<'file>(msg: &str, val: &'file JsonValue) -> InoxParseResult<JsonObject<'file>> {
if let Some(obj) = val.as_object() {
Ok(JsonObject(obj))
} else {
Expand Down
52 changes: 52 additions & 0 deletions inox2d/src/formats/vendors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/// Additional payload support for known vendor blocks.
use crate::formats::json::JsonObject;
use crate::formats::payload::{as_nested_list, as_object, InoxParseError, InoxParseResult};
use crate::params::{Binding, ParamUuid};
use json::JsonValue;

pub const SESSION_BINDINGS_KEY: &str = "com.inochi2d.inochi-session.bindings";

pub enum BindingType {
RatioBinding,
ExpressionBinding,
}

pub struct SessionBinding<'file> {
pub name: &'file str,
pub source_name: &'file str,
pub source_display_name: &'file str,
pub source_type: &'file str,
pub binding_type: BindingType,
pub param: ParamUuid,
pub axis: u8,
pub dampen_level: f32,
}

impl<'file> SessionBinding<'file> {
pub fn new_from_json_object(object: JsonObject<'file>) -> InoxParseResult<Self> {
Ok(Self {
name: object.get_str("name")?,
source_name: object.get_str("sourceName")?,
source_display_name: object.get_str("sourceDisplayName")?,
source_type: object.get_str("sourceType")?,
binding_type: match object.get_str("bindingType")? {
"RatioBinding" => BindingType::RatioBinding,
"ExpressionBinding" => BindingType::ExpressionBinding,
unknown => return Err(InoxParseError::UnknownVendorKeyValue("bindingType", unknown.to_owned())),
},
param: ParamUuid(object.get_u32("param")?),
axis: object.get_u8("axis")?,
dampen_level: object.get_f32("dampenLevel")?,
})
}

pub fn new_from_json_list(value: &'file JsonValue) -> InoxParseResult<Vec<Self>> {
let mut out = vec![];

for (index, binding) in as_nested_list(0, value)?.iter().enumerate() {
out.push(Self::new_from_json_object(as_object(&format!("{}", index), binding)?)?);
}

Ok(out)
}
}
2 changes: 1 addition & 1 deletion inox2d/src/puppet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub struct Puppet {
pub(crate) transform_ctx: Option<TransformCtx>,
/// Context for rendering this puppet. See `.init_rendering()`.
pub render_ctx: Option<RenderCtx>,
pub(crate) params: HashMap<String, Param>,
pub params: HashMap<String, Param>,
/// Context for animating puppet with parameters. See `.init_params()`
pub param_ctx: Option<ParamCtx>,
}
Expand Down