nmarshall's picture
document: reindex properties
fc56f58
Raw History Blame Contribute Delete
9.85 kB
//! Explicit transactional table reordering/removal over the existing world model.
use mb3_dat::{Limits, Property, Uv, World};
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Error {
Index,
DuplicateOrder,
InvalidSlots,
Limit,
Allocation,
Overflow,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "property edit: {self:?}")
}
}
impl std::error::Error for Error {}
type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Report {
pub dropped_string_names: usize,
pub dropped_uv_names: usize,
pub dropped_string_values: usize,
pub dropped_uv_values: usize,
}
struct Budget {
items: usize,
bytes: usize,
limits: Limits,
}
impl Budget {
fn items(&mut self, n: usize) -> Result<()> {
self.items = self.items.checked_add(n).ok_or(Error::Overflow)?;
if self.items > self.limits.max_items {
Err(Error::Limit)
} else {
Ok(())
}
}
fn vec<T>(&mut self, n: usize) -> Result<Vec<T>> {
self.bytes(
n.checked_mul(std::mem::size_of::<T>())
.ok_or(Error::Overflow)?,
)?;
let mut v = Vec::new();
v.try_reserve_exact(n).map_err(|_| Error::Allocation)?;
Ok(v)
}
fn bytes(&mut self, n: usize) -> Result<()> {
self.bytes = self.bytes.checked_add(n).ok_or(Error::Overflow)?;
if self.bytes > self.limits.max_owned_bytes {
Err(Error::Limit)
} else {
Ok(())
}
}
fn string(&mut self, s: &str) -> Result<String> {
if s.len() > self.limits.max_string_bytes {
return Err(Error::Limit);
}
self.bytes(s.len())?;
let mut out = String::new();
out.try_reserve_exact(s.len())
.map_err(|_| Error::Allocation)?;
out.push_str(s);
Ok(out)
}
fn table(&mut self, names: &[String], order: &[u32]) -> Result<(Vec<String>, Vec<u32>)> {
self.items(names.len())?;
self.items(order.len())?;
if order.len() > i32::MAX as usize {
return Err(Error::Overflow);
}
let mut map = self.vec(names.len())?;
map.resize(names.len(), u32::MAX);
let mut out = self.vec(order.len())?;
for (new, old) in order.iter().enumerate() {
let slot = map.get_mut(*old as usize).ok_or(Error::Index)?;
if *slot != u32::MAX {
return Err(Error::DuplicateOrder);
}
*slot = new as u32;
out.push(self.string(&names[*old as usize])?);
}
Ok((out, map))
}
}
fn validate<T>(slots: &[Property<T>], map: &[u32]) -> Result<usize> {
let mut previous = None;
let mut kept = 0;
for slot in slots {
if previous.is_some_and(|n| n >= slot.index) {
return Err(Error::InvalidSlots);
}
previous = Some(slot.index);
let id = map.get(slot.index as usize).ok_or(Error::InvalidSlots)?;
if *id != u32::MAX {
kept += 1;
}
}
Ok(kept)
}
struct Update {
strings: Vec<Property<String>>,
uv: Vec<Property<Uv>>,
}
/// New tables select old indices in the desired order. Omitted indices explicitly
/// remove names and associated values. All validation/allocation precedes mutation.
pub fn remap(
world: &mut World,
string_order: &[u32],
uv_order: &[u32],
limits: Limits,
) -> Result<Report> {
let mut b = Budget {
items: 0,
bytes: 0,
limits,
};
let (strings, sm) = b.table(&world.string_properties, string_order)?;
let (uv, um) = b.table(&world.uv_properties, uv_order)?;
let mut report = Report {
dropped_string_names: world.string_properties.len() - strings.len(),
dropped_uv_names: world.uv_properties.len() - uv.len(),
..Report::default()
};
let mut count = 0usize;
for brush in &world.brushes {
b.items(1)?;
count = count
.checked_add(brush.planes.len())
.ok_or(Error::Overflow)?;
}
b.items(count)?;
let mut updates = b.vec(count)?;
for p in world.brushes.iter().flat_map(|b| &b.planes) {
b.items(p.strings.len())?;
b.items(p.uv.len())?;
let ns = validate(&p.strings, &sm)?;
let nu = validate(&p.uv, &um)?;
report.dropped_string_values = report
.dropped_string_values
.checked_add(p.strings.len() - ns)
.ok_or(Error::Overflow)?;
report.dropped_uv_values = report
.dropped_uv_values
.checked_add(p.uv.len() - nu)
.ok_or(Error::Overflow)?;
let mut ss = b.vec(ns)?;
for s in &p.strings {
let index = sm[s.index as usize];
if index != u32::MAX {
ss.push(Property {
index,
value: b.string(&s.value)?,
});
}
}
ss.sort_unstable_by_key(|p| p.index);
let mut us = b.vec(nu)?;
for s in &p.uv {
let index = um[s.index as usize];
if index != u32::MAX {
us.push(Property {
index,
value: s.value.clone(),
});
}
}
us.sort_unstable_by_key(|p| p.index);
updates.push(Update {
strings: ss,
uv: us,
});
}
world.string_properties = strings;
world.uv_properties = uv;
for (p, u) in world
.brushes
.iter_mut()
.flat_map(|b| &mut b.planes)
.zip(updates)
{
p.strings = u.strings;
p.uv = u.uv;
}
Ok(report)
}
#[cfg(test)]
mod tests {
use super::*;
use mb3_dat::{Brush, Document, Plane, Version};
fn world() -> World {
World {
string_properties: vec!["unused".into(), "a".into(), "b".into()],
uv_properties: vec!["uv".into()],
brushes: vec![Brush {
planes: vec![Plane {
p0: [1., 2., 3.],
strings: vec![
Property {
index: 1,
value: String::new(),
},
Property {
index: 2,
value: "value".into(),
},
],
uv: vec![Property {
index: 0,
value: Uv {
u: [1., 0., 0.],
v: [0., 1., 0.],
u_scale: 1.,
u_shift: 0.,
v_scale: 1.,
v_shift: 0.,
},
}],
..Default::default()
}],
}],
..Default::default()
}
}
#[test]
fn reorder_remove_empty_values_and_geometry_preserved() {
let mut w = world();
let p = w.brushes[0].planes[0].p0;
let r = remap(&mut w, &[2, 1], &[], Limits::default()).unwrap();
assert_eq!(w.string_properties, ["b", "a"]);
assert_eq!(
w.brushes[0].planes[0].strings,
[
Property {
index: 0,
value: "value".into()
},
Property {
index: 1,
value: String::new()
}
]
);
assert_eq!(w.brushes[0].planes[0].p0, p);
assert_eq!(
r,
Report {
dropped_string_names: 1,
dropped_uv_names: 1,
dropped_uv_values: 1,
dropped_string_values: 0
}
);
let d = Document {
world: w,
version: Version::V201512200,
};
assert_eq!(
mb3_dat::decode(
&mb3_dat::encode(&d, &Limits::default()).unwrap(),
&Limits::default()
)
.unwrap(),
d
);
}
#[test]
fn explicit_drop_counts_and_roundtrip_permutation() {
let mut w = world();
let original = w.clone();
remap(&mut w, &[2, 0, 1], &[0], Limits::default()).unwrap();
remap(&mut w, &[1, 2, 0], &[0], Limits::default()).unwrap();
assert_eq!(w, original);
let r = remap(&mut w, &[], &[], Limits::default()).unwrap();
assert_eq!(r.dropped_string_values, 2);
assert!(w.brushes[0].planes[0].strings.is_empty());
}
#[test]
fn errors_are_transactional_including_late_slots() {
for order in [vec![1, 1], vec![9]] {
let mut w = world();
let before = w.clone();
assert!(remap(&mut w, &order, &[0], Limits::default()).is_err());
assert_eq!(w, before);
}
let mut w = world();
w.brushes[0].planes[0].strings[1].index = 99;
let before = w.clone();
assert!(remap(&mut w, &[0, 1, 2], &[0], Limits::default()).is_err());
assert_eq!(w, before);
for l in [
Limits {
max_items: 1,
..Limits::default()
},
Limits {
max_owned_bytes: 1,
..Limits::default()
},
Limits {
max_string_bytes: 1,
..Limits::default()
},
] {
let mut w = world();
let before = w.clone();
assert!(remap(&mut w, &[0, 1, 2], &[0], l).is_err());
assert_eq!(w, before);
}
}
}