runmat_geometry_core/model/
regions.rs1use 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}