Skip to main content

runmat_geometry_core/model/
regions.rs

1use serde::{Deserialize, Serialize};
2
3use crate::selection::EntityKind;
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6pub struct Region {
7    pub region_id: String,
8    pub name: String,
9    pub tag: Option<String>,
10    #[serde(default, skip_serializing_if = "Option::is_none")]
11    pub cad_ownership: Option<CadRegionOwnership>,
12}
13
14impl Region {
15    pub fn has_material_role(&self) -> bool {
16        self.tag.as_deref().is_some_and(is_material_role_text)
17            || self
18                .cad_ownership
19                .as_ref()
20                .is_some_and(CadRegionOwnership::has_material_role)
21    }
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum CadSemanticKind {
27    Assembly,
28    Component,
29    Reference,
30    Body,
31    Compound,
32    Face,
33    Subshape,
34    Layer,
35    Color,
36    Material,
37    Shape,
38    Unknown,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "camelCase")]
43pub struct CadLabelRef {
44    pub label_entry: String,
45    pub name: String,
46    pub kind: CadSemanticKind,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase")]
51pub struct CadColorEvidence {
52    pub source: String,
53    pub color_type: String,
54    pub hex_rgba: String,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "camelCase")]
59pub struct CadPhysicalMaterialEvidence {
60    pub label_entry: String,
61    pub name: String,
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub description: Option<String>,
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub density: Option<String>,
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub density_name: Option<String>,
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub density_value_type: Option<String>,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(rename_all = "camelCase")]
74pub struct CadRegionOwnership {
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub face_id: Option<u64>,
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub curve_id: Option<u64>,
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub label: Option<CadLabelRef>,
81    #[serde(default, skip_serializing_if = "Vec::is_empty")]
82    pub owner_path: Vec<CadLabelRef>,
83    #[serde(default, skip_serializing_if = "Vec::is_empty")]
84    pub layers: Vec<String>,
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub color: Option<CadColorEvidence>,
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub material: Option<CadPhysicalMaterialEvidence>,
89}
90
91impl CadRegionOwnership {
92    pub fn has_material_role(&self) -> bool {
93        self.material.is_some()
94            || self
95                .label
96                .as_ref()
97                .is_some_and(|label| label.kind == CadSemanticKind::Material)
98            || self
99                .owner_path
100                .iter()
101                .any(|label| label.kind == CadSemanticKind::Material)
102    }
103}
104
105fn is_material_role_text(value: &str) -> bool {
106    value
107        .split(|character: char| !character.is_ascii_alphanumeric())
108        .any(|token| token.eq_ignore_ascii_case("material"))
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(rename_all = "camelCase")]
113pub struct EntityIdRange {
114    pub start: u64,
115    pub count: u64,
116}
117
118impl EntityIdRange {
119    pub fn new(start: u64, count: u64) -> Self {
120        Self { start, count }
121    }
122
123    pub fn end_exclusive(&self) -> Option<u64> {
124        self.start.checked_add(self.count)
125    }
126
127    pub fn contains(&self, entity_id: u64) -> bool {
128        self.end_exclusive()
129            .is_some_and(|end| entity_id >= self.start && entity_id < end)
130    }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct RegionEntityMapping {
136    pub region_id: String,
137    pub mesh_id: String,
138    pub entity_kind: EntityKind,
139    pub ranges: Vec<EntityIdRange>,
140}
141
142impl RegionEntityMapping {
143    pub fn new(
144        region_id: impl Into<String>,
145        mesh_id: impl Into<String>,
146        entity_kind: EntityKind,
147        ranges: Vec<EntityIdRange>,
148    ) -> Self {
149        Self {
150            region_id: region_id.into(),
151            mesh_id: mesh_id.into(),
152            entity_kind,
153            ranges,
154        }
155    }
156
157    pub fn all_faces(
158        region_id: impl Into<String>,
159        mesh_id: impl Into<String>,
160        face_count: u64,
161    ) -> Self {
162        Self::new(
163            region_id,
164            mesh_id,
165            EntityKind::Face,
166            if face_count == 0 {
167                Vec::new()
168            } else {
169                vec![EntityIdRange::new(0, face_count)]
170            },
171        )
172    }
173
174    pub fn entity_count(&self) -> u64 {
175        self.ranges.iter().map(|range| range.count).sum()
176    }
177
178    pub fn contains_entity(&self, entity_id: u64) -> bool {
179        self.ranges.iter().any(|range| range.contains(entity_id))
180    }
181}