#[cfg(feature = "fbx-reader")]
use crate::fbx_node::FbxProperty;
pub(crate) const NAME_CLASS_SEPARATOR: &str = "\u{0}\u{1}";
#[cfg(feature = "fbx-writer")]
pub(crate) fn name_class(name: &str, class: &str) -> String {
format!("{name}{NAME_CLASS_SEPARATOR}{class}")
}
#[cfg(feature = "fbx-writer")]
pub(crate) const FBX_VERSION: u32 = 7500;
#[cfg(any(feature = "fbx-reader", feature = "fbx-writer"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ArrayElement {
I32,
I64,
F64,
F32Bits,
}
#[cfg(any(feature = "fbx-reader", feature = "fbx-writer"))]
pub(crate) fn array_element_type(owner: &str) -> ArrayElement {
match owner {
"PolygonVertexIndex" | "Edges" | "Indexes" | "Materials" | "Smoothing" | "UVIndex"
| "NormalsIndex" | "NormalIndex" | "ColorIndex" | "TangentIndex" | "BinormalIndex"
| "KeyAttrFlags" | "KeyAttrRefCount" => ArrayElement::I32,
"KeyTime" => ArrayElement::I64,
"KeyAttrDataFloat" => ArrayElement::F32Bits,
_ => ArrayElement::F64,
}
}
#[cfg(any(feature = "fbx-reader", feature = "fbx-writer"))]
pub(crate) fn is_base64_node(node_name: &str) -> bool {
node_name == "Content"
}
#[cfg(feature = "fbx-reader")]
pub(crate) fn properties70_type_is_integral(type_name: &str) -> bool {
matches!(
type_name,
"int" | "Integer" | "enum" | "bool" | "Bool" | "Visibility" | "KTime"
)
}
#[cfg(feature = "fbx-reader")]
pub(crate) fn parse_ascii_bool(word: &str) -> Option<bool> {
match word {
"T" | "Y" => Some(true),
"F" | "N" => Some(false),
_ => None,
}
}
#[cfg(feature = "fbx-reader")]
pub(crate) fn normalize_object_name(raw: &[u8]) -> String {
let unescaped;
let raw = if raw.windows(6).any(|window| window == b""") {
unescaped = String::from_utf8_lossy(raw)
.replace(""", "\"")
.into_bytes();
unescaped.as_slice()
} else {
raw
};
let Some(split) = raw.windows(2).position(|pair| pair == b"::") else {
return String::from_utf8_lossy(raw).into_owned();
};
let class = &raw[..split];
let name = &raw[split + 2..];
let mut out = Vec::with_capacity(raw.len());
out.extend_from_slice(name);
out.extend_from_slice(NAME_CLASS_SEPARATOR.as_bytes());
out.extend_from_slice(class);
String::from_utf8_lossy(&out).into_owned()
}
#[cfg(feature = "fbx-writer")]
pub(crate) fn ascii_object_name(raw: &str) -> String {
let joined = match raw.split_once(NAME_CLASS_SEPARATOR) {
Some((name, class)) => format!("{class}::{name}"),
None => raw.to_string(),
};
if joined.contains('"') {
return joined.replace('"', """);
}
joined
}
#[cfg(feature = "fbx-writer")]
pub(crate) fn format_f64(value: f64) -> Option<String> {
if !value.is_finite() {
return None;
}
let mut text = format!("{value}");
if !text.contains(['.', 'e', 'E']) {
text.push_str(".0");
}
Some(text)
}
#[cfg(feature = "fbx-reader")]
pub(crate) fn number_property(text: &str) -> FbxProperty {
let integral = !text.contains(['.', 'e', 'E']);
if integral && text.starts_with('-') && text.trim_start_matches(['-', '0']).is_empty() {
return FbxProperty::F64(-0.0);
}
if integral {
if let Ok(value) = text.parse::<i32>() {
return FbxProperty::I32(value);
}
if let Ok(value) = text.parse::<i64>() {
return FbxProperty::I64(value);
}
}
FbxProperty::F64(text.parse::<f64>().unwrap_or(0.0))
}
#[cfg(feature = "fbx-writer")]
pub(crate) fn encode_base64(bytes: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.chunks(3) {
let mut packed = 0u32;
for index in 0..3 {
packed = (packed << 8) | u32::from(chunk.get(index).copied().unwrap_or(0));
}
for index in 0..4 {
if index <= chunk.len() {
let sextet = (packed >> (18 - index * 6)) & 0x3f;
out.push(char::from(ALPHABET[sextet as usize]));
} else {
out.push('=');
}
}
}
out
}
#[cfg(feature = "fbx-reader")]
pub(crate) fn decode_base64(text: &str) -> Option<Vec<u8>> {
fn sextet(byte: u8) -> Option<u32> {
match byte {
b'A'..=b'Z' => Some(u32::from(byte - b'A')),
b'a'..=b'z' => Some(u32::from(byte - b'a') + 26),
b'0'..=b'9' => Some(u32::from(byte - b'0') + 52),
b'+' => Some(62),
b'/' => Some(63),
_ => None,
}
}
let body: Vec<u8> = text
.bytes()
.filter(|byte| !byte.is_ascii_whitespace())
.collect();
if body.is_empty() || !body.len().is_multiple_of(4) {
return None;
}
let mut out = Vec::with_capacity(body.len() / 4 * 3);
for chunk in body.chunks_exact(4) {
let padding = chunk.iter().filter(|byte| **byte == b'=').count();
if padding > 2 {
return None;
}
let mut packed = 0u32;
for &byte in chunk {
let value = if byte == b'=' { 0 } else { sextet(byte)? };
packed = (packed << 6) | value;
}
let bytes = packed.to_be_bytes();
out.push(bytes[1]);
if padding < 2 {
out.push(bytes[2]);
}
if padding < 1 {
out.push(bytes[3]);
}
}
Some(out)
}