1use crate::model::{Geometry, Model, ResourceId};
2use crate::validation::report::ValidationReport;
3use std::collections::{HashMap, HashSet};
4
5pub fn validate_semantic(model: &Model, report: &mut ValidationReport) {
6 validate_build_references(model, report);
8
9 validate_boolean_cycles(model, report);
11
12 for object in model.resources.iter_objects() {
14 if let Some(pid) = object.pid {
16 if !model.resources.exists(pid) {
18 report.add_error(
19 2001,
20 format!(
21 "Object {} references non-existent property group {}",
22 object.id.0, pid.0
23 ),
24 );
25 }
26 }
27
28 match &object.geometry {
29 Geometry::Mesh(mesh) => {
30 for (i, tri) in mesh.triangles.iter().enumerate() {
31 if tri.v1 as usize >= mesh.vertices.len()
33 || tri.v2 as usize >= mesh.vertices.len()
34 || tri.v3 as usize >= mesh.vertices.len()
35 {
36 report.add_error(
37 3001,
38 format!(
39 "Triangle {} in Object {} references out-of-bounds vertex",
40 i, object.id.0
41 ),
42 );
43 }
44
45 if let Some(pid) = tri.pid.map(crate::model::ResourceId)
47 && !model.resources.exists(pid)
48 {
49 report.add_error(2002, format!("Triangle {} in Object {} references non-existent property group {}", i, object.id.0, pid.0));
50 }
51 }
52 }
53 Geometry::Components(comps) => {
54 for comp in &comps.components {
55 if comp.path.is_none() && model.resources.get_object(comp.object_id).is_none() {
57 report.add_error(
58 2003,
59 format!(
60 "Component in Object {} references non-existent object {}",
61 object.id.0, comp.object_id.0
62 ),
63 );
64 }
65 }
66 }
67 Geometry::SliceStack(stack_id) => {
68 if model.resources.get_slice_stack(*stack_id).is_none() {
69 report.add_error(
70 2004,
71 format!(
72 "Object {} references non-existent slicestack {}",
73 object.id.0, stack_id.0
74 ),
75 );
76 }
77 }
78 Geometry::VolumetricStack(stack_id) => {
79 if model.resources.get_volumetric_stack(*stack_id).is_none() {
80 report.add_error(
81 2005,
82 format!(
83 "Object {} references non-existent volumetricstack {}",
84 object.id.0, stack_id.0
85 ),
86 );
87 }
88 }
89 Geometry::BooleanShape(bs) => {
90 if let Some(base_obj) = model.resources.get_object(bs.base_object_id) {
92 match &base_obj.geometry {
94 Geometry::Mesh(_) | Geometry::BooleanShape(_) => {
95 }
97 Geometry::Components(_) => {
98 report.add_error(
99 2101,
100 format!(
101 "BooleanShape {} base object {} cannot be Components type",
102 object.id.0, bs.base_object_id.0
103 ),
104 );
105 }
106 _ => {
107 }
109 }
110 } else {
111 report.add_error(
112 2102,
113 format!(
114 "BooleanShape {} references non-existent base object {}",
115 object.id.0, bs.base_object_id.0
116 ),
117 );
118 }
119
120 for (idx, op) in bs.operations.iter().enumerate() {
122 if let Some(op_obj) = model.resources.get_object(op.object_id) {
123 match &op_obj.geometry {
125 Geometry::Mesh(_) => {
126 }
128 _ => {
129 report.add_error(
130 2103,
131 format!(
132 "BooleanShape {} operation {} references non-mesh object {} (type must be mesh)",
133 object.id.0, idx, op.object_id.0
134 ),
135 );
136 }
137 }
138 } else {
139 report.add_error(
140 2104,
141 format!(
142 "BooleanShape {} operation {} references non-existent object {}",
143 object.id.0, idx, op.object_id.0
144 ),
145 );
146 }
147 }
148
149 if !is_transform_valid(&bs.base_transform) {
151 report.add_error(
152 2106,
153 format!(
154 "BooleanShape {} has invalid base transformation matrix (contains NaN or Infinity)",
155 object.id.0
156 ),
157 );
158 }
159
160 for (idx, op) in bs.operations.iter().enumerate() {
162 if !is_transform_valid(&op.transform) {
163 report.add_error(
164 2105,
165 format!(
166 "BooleanShape {} operation {} has invalid transformation matrix (contains NaN or Infinity)",
167 object.id.0, idx
168 ),
169 );
170 }
171 }
172 }
173 Geometry::DisplacementMesh(_mesh) => {
174 }
177 }
178 }
179}
180
181fn validate_build_references(model: &Model, report: &mut ValidationReport) {
182 for (idx, item) in model.build.items.iter().enumerate() {
183 if let Some(obj) = model.resources.get_object(item.object_id) {
185 if !obj.object_type.can_be_in_build() {
187 report.add_error(
188 3010,
189 format!(
190 "Build item {} references object {} with type '{}' which cannot be in build",
191 idx, item.object_id.0, obj.object_type
192 ),
193 );
194 }
195 } else {
196 report.add_error(
198 3002,
199 format!(
200 "Build item {} references non-existent object {}",
201 idx, item.object_id.0
202 ),
203 );
204 }
205 }
206}
207
208fn validate_boolean_cycles(model: &Model, report: &mut ValidationReport) {
210 let mut graph: HashMap<ResourceId, Vec<ResourceId>> = HashMap::new();
212
213 for obj in model.resources.iter_objects() {
214 if let Geometry::BooleanShape(bs) = &obj.geometry {
215 let mut refs = vec![bs.base_object_id];
216 refs.extend(bs.operations.iter().map(|op| op.object_id));
217 graph.insert(obj.id, refs);
218 }
219 }
220
221 let mut visited = HashSet::new();
223 let mut rec_stack = HashSet::new();
224
225 for &start_id in graph.keys() {
226 if !visited.contains(&start_id)
227 && has_cycle_dfs(start_id, &graph, &mut visited, &mut rec_stack)
228 {
229 report.add_error(
230 2100,
231 format!(
232 "Cycle detected in boolean operation graph involving object {}",
233 start_id.0
234 ),
235 );
236 }
237 }
238}
239
240fn has_cycle_dfs(
241 node: ResourceId,
242 graph: &HashMap<ResourceId, Vec<ResourceId>>,
243 visited: &mut HashSet<ResourceId>,
244 rec_stack: &mut HashSet<ResourceId>,
245) -> bool {
246 visited.insert(node);
247 rec_stack.insert(node);
248
249 if let Some(neighbors) = graph.get(&node) {
250 for &neighbor in neighbors {
251 if graph.contains_key(&neighbor) {
253 if !visited.contains(&neighbor) {
254 if has_cycle_dfs(neighbor, graph, visited, rec_stack) {
255 return true;
256 }
257 } else if rec_stack.contains(&neighbor) {
258 return true;
260 }
261 }
262 }
263 }
264
265 rec_stack.remove(&node);
266 false
267}
268
269fn is_transform_valid(mat: &glam::Mat4) -> bool {
271 mat.x_axis.is_finite()
272 && mat.y_axis.is_finite()
273 && mat.z_axis.is_finite()
274 && mat.w_axis.is_finite()
275}