semantic-scene 0.1.0

Rust parser for Habitat-Sim `SemanticScene` descriptors.
Documentation
//! Semantic region model.

use std::fmt;

use crate::{Aabb, SemanticScene, Vec2, Vec3};

use super::RegionCategory;

/// Represents a semantic region, typically a room in a level of a house.
///
/// MP3D descriptors provide an AABB but not polyloop geometry. Polyloop-related
/// accessors are present to preserve Habitat-Sim's API shape and return empty
/// slices for MP3D-loaded scenes.
#[derive(Debug, Clone, PartialEq)]
pub struct SemanticRegion {
    index: i32,
    level_index: Option<usize>,
    category: RegionCategory,
    position: Vec3,
    aabb: Aabb,
    object_indices: Vec<usize>,
    poly_loop_points: Vec<Vec2>,
    volume_edges: Vec<Vec<Vec3>>,
    floor_height: f64,
    extrusion_height: f64,
    area: f64,
}

impl SemanticRegion {
    #[must_use]
    pub(crate) fn new(
        index: i32,
        level_index: Option<usize>,
        category: RegionCategory,
        position: Vec3,
        aabb: Aabb,
    ) -> Self {
        Self {
            index,
            level_index,
            category,
            position,
            aabb,
            object_indices: Vec::new(),
            poly_loop_points: Vec::new(),
            volume_edges: Vec::new(),
            floor_height: f64::from(aabb.min.1),
            extrusion_height: f64::from(aabb.max.1 - aabb.min.1),
            area: f64::from((aabb.max.0 - aabb.min.0).abs() * (aabb.max.2 - aabb.min.2).abs()),
        }
    }

    /// Returns the Habitat-Sim-style region id.
    ///
    /// If the region has a parent level, the form is `<level_id>_<region_id>`.
    /// Otherwise, the form is `_<region_id>`.
    #[must_use]
    pub fn id(&self, scene: &SemanticScene) -> String {
        self.level_index.map_or_else(
            || format!("_{}", self.index),
            |level_index| format!("{}_{}", scene.levels()[level_index].id(), self.index),
        )
    }

    /// Returns the source descriptor index.
    #[must_use]
    pub const fn index(&self) -> i32 {
        self.index
    }

    /// Returns the index of this region's parent level in `SemanticScene::levels`.
    #[must_use]
    pub const fn level_index(&self) -> Option<usize> {
        self.level_index
    }

    /// Returns the semantic category of the region.
    #[must_use]
    pub const fn category(&self) -> &RegionCategory {
        &self.category
    }

    /// Returns the region position from the source descriptor.
    #[must_use]
    pub const fn position(&self) -> Vec3 {
        self.position
    }

    /// Returns the region axis-aligned bounding box.
    #[must_use]
    pub const fn aabb(&self) -> &Aabb {
        &self.aabb
    }

    /// Returns indices of objects contained by this region.
    #[must_use]
    pub fn object_indices(&self) -> &[usize] {
        &self.object_indices
    }

    /// Tests whether this region contains the point.
    ///
    /// For MP3D-loaded scenes this uses the region AABB. Future descriptor
    /// loaders with polyloop geometry can provide tighter containment.
    #[must_use]
    pub fn contains(&self, point: Vec3) -> bool {
        self.aabb.contains(point)
    }

    /// Returns points making up the region's floor-parallel polyloop.
    #[must_use]
    pub fn poly_loop_points(&self) -> &[Vec2] {
        &self.poly_loop_points
    }

    /// Returns bounding volume edges for visualization.
    #[must_use]
    pub fn volume_edges(&self) -> &[Vec<Vec3>] {
        &self.volume_edges
    }

    /// Returns the floor height above the x-z plane.
    #[must_use]
    pub const fn floor_height(&self) -> f64 {
        self.floor_height
    }

    /// Returns the extrusion height above the floor.
    #[must_use]
    pub const fn extrusion_height(&self) -> f64 {
        self.extrusion_height
    }

    /// Returns the area of the region base.
    #[must_use]
    pub const fn area(&self) -> f64 {
        self.area
    }

    /// Returns the volume of the region bounds.
    #[must_use]
    pub const fn volume(&self) -> f64 {
        self.area * self.extrusion_height
    }

    pub(crate) fn add_object(&mut self, object_index: usize) {
        self.object_indices.push(object_index);
    }
}

impl fmt::Display for SemanticRegion {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        let level = self
            .level_index
            .map_or_else(|| "none".to_string(), |index| index.to_string());
        write!(
            formatter,
            "region {}: level={}, category={}, objects={}, bounds=[{}]",
            self.index,
            level,
            self.category,
            self.object_indices.len(),
            self.aabb
        )
    }
}