semantic-scene 0.1.1

Rust parser for semantic scene descriptors, currently focused on Habitat-Sim Matterport3D .house files.
Documentation
use winnow::{Parser, ascii};

use crate::{Aabb, Obb, ParseError, Vec3};

use super::raw::{
    CategoryRecord, HouseRecord, LevelRecord, Mp3dRecord, ObjectRecord, RegionRecord, SegmentRecord,
};

/// Parses one MP3D `.house` record line.
///
/// # Errors
///
/// Returns an error when a required field is missing or has the wrong type.
pub fn parse_record(line: &str) -> Result<Mp3dRecord, ParseError> {
    let tokens: Vec<&str> = line.split_whitespace().collect();
    let tag = token(&tokens, 0)?.chars().next().ok_or_else(|| {
        ParseError::BadLine("expected a record tag in the first field".to_string())
    })?;

    match tag {
        'H' => parse_house(&tokens),
        'L' => parse_level(&tokens),
        'R' => parse_region(&tokens),
        'C' => parse_category(&tokens),
        'O' => parse_object(&tokens),
        'E' => parse_segment(&tokens),
        _ => Ok(Mp3dRecord::Ignored),
    }
}

fn parse_house(tokens: &[&str]) -> Result<Mp3dRecord, ParseError> {
    Ok(Mp3dRecord::House(HouseRecord {
        name: token(tokens, 1)?.to_string(),
        label: token(tokens, 2)?.to_string(),
        images: parse_usize(tokens, 3)?,
        panoramas: parse_usize(tokens, 4)?,
        vertices: parse_usize(tokens, 5)?,
        surfaces: parse_usize(tokens, 6)?,
        segments: parse_usize(tokens, 7)?,
        objects: parse_usize(tokens, 8)?,
        categories: parse_usize(tokens, 9)?,
        regions: parse_usize(tokens, 10)?,
        portals: parse_usize(tokens, 11)?,
        levels: parse_usize(tokens, 12)?,
        aabb: parse_aabb(tokens, 18)?,
    }))
}

fn parse_level(tokens: &[&str]) -> Result<Mp3dRecord, ParseError> {
    Ok(Mp3dRecord::Level(LevelRecord {
        index: parse_i32(tokens, 1)?,
        label: token(tokens, 3)?.to_string(),
        position: parse_vec3(tokens, 4)?,
        aabb: parse_aabb(tokens, 7)?,
    }))
}

fn parse_region(tokens: &[&str]) -> Result<Mp3dRecord, ParseError> {
    let category_code = token(tokens, 5)?
        .chars()
        .next()
        .ok_or_else(|| ParseError::BadField {
            field: 5,
            value: String::new(),
        })?;

    Ok(Mp3dRecord::Region(RegionRecord {
        index: parse_i32(tokens, 1)?,
        level_index: parse_i32(tokens, 2)?,
        category_code,
        position: parse_vec3(tokens, 6)?,
        aabb: parse_aabb(tokens, 9)?,
    }))
}

fn parse_category(tokens: &[&str]) -> Result<Mp3dRecord, ParseError> {
    Ok(Mp3dRecord::Category(CategoryRecord {
        index: parse_i32(tokens, 1)?,
        raw_index: parse_i32(tokens, 2)?,
        raw_name: token(tokens, 3)?.replace('#', " "),
        mpcat40_index: parse_i32(tokens, 4)?,
        mpcat40_name: token(tokens, 5)?.to_string(),
    }))
}

fn parse_object(tokens: &[&str]) -> Result<Mp3dRecord, ParseError> {
    Ok(Mp3dRecord::Object(ObjectRecord {
        index: parse_i32(tokens, 1)?,
        region_index: parse_i32(tokens, 2)?,
        category_index: parse_i32(tokens, 3)?,
        obb: parse_obb(tokens, 4)?,
    }))
}

fn parse_segment(tokens: &[&str]) -> Result<Mp3dRecord, ParseError> {
    Ok(Mp3dRecord::Segment(SegmentRecord {
        object_index: parse_i32(tokens, 2)?,
        segment_id: parse_i32(tokens, 3)?,
    }))
}

fn token<'a>(tokens: &'a [&str], field: usize) -> Result<&'a str, ParseError> {
    tokens
        .get(field)
        .copied()
        .ok_or(ParseError::MissingField { field })
}

fn parse_i32(tokens: &[&str], field: usize) -> Result<i32, ParseError> {
    let value = token(tokens, field)?;
    let mut input = value;
    let parsed = ascii::dec_int::<_, i32, winnow::error::ContextError>.parse_next(&mut input);
    match parsed {
        Ok(parsed) if input.is_empty() => Ok(parsed),
        _ => Err(ParseError::BadField {
            field,
            value: value.to_string(),
        }),
    }
}

fn parse_usize(tokens: &[&str], field: usize) -> Result<usize, ParseError> {
    let parsed = parse_i32(tokens, field)?;
    usize::try_from(parsed).map_err(|_| ParseError::BadField {
        field,
        value: parsed.to_string(),
    })
}

fn parse_f32(tokens: &[&str], field: usize) -> Result<f32, ParseError> {
    let value = token(tokens, field)?;
    let mut input = value;
    let parsed = ascii::float::<_, f32, winnow::error::ContextError>.parse_next(&mut input);
    match parsed {
        Ok(parsed) if input.is_empty() => Ok(parsed),
        _ => Err(ParseError::BadField {
            field,
            value: value.to_string(),
        }),
    }
}

fn parse_vec3(tokens: &[&str], offset: usize) -> Result<Vec3, ParseError> {
    Ok(Vec3(
        parse_f32(tokens, offset)?,
        parse_f32(tokens, offset + 1)?,
        parse_f32(tokens, offset + 2)?,
    ))
}

fn parse_aabb(tokens: &[&str], offset: usize) -> Result<Aabb, ParseError> {
    Ok(Aabb {
        min: parse_vec3(tokens, offset)?,
        max: parse_vec3(tokens, offset + 3)?,
    })
}

fn parse_obb(tokens: &[&str], offset: usize) -> Result<Obb, ParseError> {
    let center = parse_vec3(tokens, offset)?;
    let axis0 = parse_vec3(tokens, offset + 3)?;
    let axis1 = parse_vec3(tokens, offset + 6)?;
    let axis2 = axis0.cross(axis1);
    let half_extents = parse_vec3(tokens, offset + 9)?;

    Ok(Obb {
        center,
        axis0,
        axis1,
        axis2,
        half_extents,
    })
}

#[cfg(test)]
mod tests {
    use crate::{Vec3, mp3d::raw::Mp3dRecord};

    use super::parse_record;

    #[test]
    fn parses_mp3d_records() {
        let house =
            parse_record("H test label 1 2 3 4 5 6 7 8 9 10 0 0 0 0 0 -1 -2 -3 4 5 6 0 0 0 0 0")
                .unwrap();
        let Mp3dRecord::House(house) = house else {
            panic!("expected house record");
        };
        assert_eq!(house.name, "test");
        assert_eq!(house.levels, 10);
        assert_eq!(house.aabb.min, Vec3(-1.0, -2.0, -3.0));
        assert_eq!(house.aabb.max, Vec3(4.0, 5.0, 6.0));

        let level = parse_record("L 2 1 lvl 1 2 3 0 0 0 10 20 30 0 0 0 0 0").unwrap();
        let Mp3dRecord::Level(level) = level else {
            panic!("expected level record");
        };
        assert_eq!(level.index, 2);
        assert_eq!(level.label, "lvl");
        assert_eq!(level.position, Vec3(1.0, 2.0, 3.0));

        let region = parse_record("R 3 2 0 0 k 4 5 6 1 2 3 7 8 9 0 0 0 0 0").unwrap();
        let Mp3dRecord::Region(region) = region else {
            panic!("expected region record");
        };
        assert_eq!(region.index, 3);
        assert_eq!(region.level_index, 2);
        assert_eq!(region.category_code, 'k');

        let category = parse_record("C 4 40 raw#chair 5 chair 0 0 0 0 0").unwrap();
        let Mp3dRecord::Category(category) = category else {
            panic!("expected category record");
        };
        assert_eq!(category.index, 4);
        assert_eq!(category.raw_name, "raw chair");
        assert_eq!(category.mpcat40_name, "chair");

        let object = parse_record("O 5 3 4 1 2 3 1 0 0 0 1 0 2 3 4 0 0 0 0 0 0 0 0").unwrap();
        let Mp3dRecord::Object(object) = object else {
            panic!("expected object record");
        };
        assert_eq!(object.index, 5);
        assert_eq!(object.region_index, 3);
        assert_eq!(object.category_index, 4);
        assert_eq!(object.obb.axis2, Vec3(0.0, 0.0, 1.0));
        assert_eq!(object.obb.half_extents, Vec3(2.0, 3.0, 4.0));

        let segment = parse_record("E 0 5 123 1.0 0 0 0 0 0 0 1 1 1 0 0 0 0 0").unwrap();
        let Mp3dRecord::Segment(segment) = segment else {
            panic!("expected segment record");
        };
        assert_eq!(segment.object_index, 5);
        assert_eq!(segment.segment_id, 123);

        assert!(matches!(
            parse_record("P ignored").unwrap(),
            Mp3dRecord::Ignored
        ));
    }
}