runmat-geometry-core 0.5.6

Canonical geometry domain model for RunMat
Documentation
use serde::{Deserialize, Serialize};

use crate::selection::EntityKind;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Region {
    pub region_id: String,
    pub name: String,
    pub tag: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cad_ownership: Option<CadRegionOwnership>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CadSemanticKind {
    Assembly,
    Component,
    Reference,
    Body,
    Compound,
    Face,
    Subshape,
    Layer,
    Color,
    Material,
    Shape,
    Unknown,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CadLabelRef {
    pub label_entry: String,
    pub name: String,
    pub kind: CadSemanticKind,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CadColorEvidence {
    pub source: String,
    pub color_type: String,
    pub hex_rgba: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CadPhysicalMaterialEvidence {
    pub label_entry: String,
    pub name: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub density: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub density_name: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub density_value_type: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CadRegionOwnership {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub face_id: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub label: Option<CadLabelRef>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub owner_path: Vec<CadLabelRef>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub layers: Vec<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub color: Option<CadColorEvidence>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub material: Option<CadPhysicalMaterialEvidence>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EntityIdRange {
    pub start: u64,
    pub count: u64,
}

impl EntityIdRange {
    pub fn new(start: u64, count: u64) -> Self {
        Self { start, count }
    }

    pub fn end_exclusive(&self) -> Option<u64> {
        self.start.checked_add(self.count)
    }

    pub fn contains(&self, entity_id: u64) -> bool {
        self.end_exclusive()
            .is_some_and(|end| entity_id >= self.start && entity_id < end)
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RegionEntityMapping {
    pub region_id: String,
    pub mesh_id: String,
    pub entity_kind: EntityKind,
    pub ranges: Vec<EntityIdRange>,
}

impl RegionEntityMapping {
    pub fn new(
        region_id: impl Into<String>,
        mesh_id: impl Into<String>,
        entity_kind: EntityKind,
        ranges: Vec<EntityIdRange>,
    ) -> Self {
        Self {
            region_id: region_id.into(),
            mesh_id: mesh_id.into(),
            entity_kind,
            ranges,
        }
    }

    pub fn all_faces(
        region_id: impl Into<String>,
        mesh_id: impl Into<String>,
        face_count: u64,
    ) -> Self {
        Self::new(
            region_id,
            mesh_id,
            EntityKind::Face,
            if face_count == 0 {
                Vec::new()
            } else {
                vec![EntityIdRange::new(0, face_count)]
            },
        )
    }

    pub fn entity_count(&self) -> u64 {
        self.ranges.iter().map(|range| range.count).sum()
    }

    pub fn contains_entity(&self, entity_id: u64) -> bool {
        self.ranges.iter().any(|range| range.contains(entity_id))
    }
}