nanobrush / crates /nanobrush-cli /src /preflight.rs
nmarshall's picture
cli: guarded obj export
49ec522
Raw History Blame Contribute Delete
5.78 kB
//! Compatibility reporting only: parsing/encoding remain authoritative validators.
use mb3_dat::{Version, World};
use std::collections::HashSet;
#[derive(Clone, Copy, Debug)]
pub enum Target {
Txt,
Obj,
Dat(Version),
}
impl Target {
pub fn name(self) -> &'static str {
match self {
Self::Txt => "txt",
Self::Obj => "obj",
Self::Dat(Version::V201501280) => "dat-201501280",
Self::Dat(Version::V201512200) => "dat-201512200",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Issue {
pub code: &'static str,
pub count: usize,
pub lossy: bool,
}
#[derive(Clone, Debug)]
pub struct Report {
pub target: Target,
pub issues: Vec<Issue>,
}
impl Report {
pub fn blocked(&self) -> bool {
self.issues.iter().any(|i| !i.lossy)
}
pub fn compatible(&self) -> bool {
self.issues.is_empty()
}
pub fn json(&self) -> String {
let issues = self
.issues
.iter()
.map(|i| {
format!(
"{{\"code\":\"{}\",\"count\":{},\"lossy\":{}}}",
i.code, i.count, i.lossy
)
})
.collect::<Vec<_>>()
.join(",");
format!(
"{{\"schema\":1,\"target\":\"{}\",\"compatible\":{},\"issues\":[{}]}}",
self.target.name(),
self.compatible(),
issues
)
}
}
pub fn analyze(world: &World, target: Target) -> Report {
let mut issues = Vec::new();
let planes = || world.brushes.iter().flat_map(|b| &b.planes);
match target {
Target::Obj => {
if !world.computed {
issues.push(Issue {
code: "computed_required",
count: 1,
lossy: false,
});
}
let n = planes().count().saturating_add(world.patches.len());
if n != 0 {
issues.push(Issue {
code: "editable_geometry",
count: n,
lossy: true,
});
}
let n = world
.string_properties
.len()
.saturating_add(world.uv_properties.len())
.saturating_add(
planes()
.map(|p| p.strings.len().saturating_add(p.uv.len()))
.fold(0usize, usize::saturating_add),
);
if n != 0 {
issues.push(Issue {
code: "properties",
count: n,
lossy: true,
});
}
let n = world
.patches
.iter()
.filter_map(|p| p.computed.as_ref())
.map(|p| p.normals.len())
.fold(0usize, usize::saturating_add);
if n != 0 {
issues.push(Issue {
code: "stored_normals",
count: n,
lossy: true,
});
}
}
Target::Dat(version) => {
let n = world
.string_properties
.iter()
.chain(&world.uv_properties)
.filter(|s| !s.is_ascii())
.count()
.saturating_add(
planes()
.flat_map(|p| &p.strings)
.filter(|p| !p.value.is_ascii())
.count(),
);
if n != 0 {
issues.push(Issue {
lossy: false,
code: "non_ascii",
count: n,
});
}
if version == Version::V201501280 {
let n = world
.string_properties
.len()
.saturating_add(world.uv_properties.len())
.saturating_add(
planes()
.map(|p| p.strings.len().saturating_add(p.uv.len()))
.fold(0usize, usize::saturating_add),
);
if n != 0 {
issues.push(Issue {
lossy: false,
code: "legacy_properties",
count: n,
});
}
}
}
Target::Txt => {
let mut n = 0usize;
for names in [&world.string_properties, &world.uv_properties] {
let mut seen = HashSet::new();
for name in names {
if !seen.insert(name) {
n = n.saturating_add(1);
}
}
}
if n != 0 {
issues.push(Issue {
lossy: false,
code: "duplicate_names",
count: n,
});
}
}
}
Report { target, issues }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grouped_reports_and_no_mutation() {
let mut w = World {
string_properties: vec!["é".into(), "é".into()],
..World::default()
};
let original = w.clone();
assert_eq!(analyze(&w, Target::Txt).issues[0].count, 1);
let r = analyze(&w, Target::Dat(Version::V201501280));
assert_eq!(r.issues.len(), 2);
assert!(!r.compatible());
assert!(r.json().contains("\"schema\":1"));
assert_eq!(w, original);
w.string_properties.clear();
assert!(analyze(&w, Target::Txt).compatible());
}
}