1use crate::core::{
8 AlphaMode, BoundingBox, Camera, DrawCall, Material, PipelineType, RenderData, SceneNode, Vertex,
9};
10use glam::{Mat4, Vec2, Vec3, Vec4};
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone)]
14pub struct GeometryScene {
15 pub scene_id: String,
16 pub revision: u64,
17 pub title: Option<String>,
18 pub overlay: Option<GeometrySceneOverlay>,
19 pub chunks: Vec<GeometrySceneChunk>,
20 pub bounds: BoundingBox,
21 pub show_grid: bool,
22 pub axis_equal: bool,
23}
24
25impl GeometryScene {
26 pub fn new(
27 scene_id: impl Into<String>,
28 revision: u64,
29 chunks: Vec<GeometrySceneChunk>,
30 ) -> Self {
31 let bounds = combined_chunk_bounds(&chunks);
32 Self {
33 scene_id: scene_id.into(),
34 revision,
35 title: None,
36 overlay: None,
37 chunks,
38 bounds,
39 show_grid: false,
40 axis_equal: true,
41 }
42 }
43
44 pub fn with_title(mut self, title: impl Into<String>) -> Self {
45 self.title = Some(title.into());
46 self
47 }
48
49 pub fn with_overlay(mut self, overlay: GeometrySceneOverlay) -> Self {
50 self.overlay = Some(overlay);
51 self
52 }
53
54 pub fn append_chunks(&mut self, chunks: impl IntoIterator<Item = GeometrySceneChunk>) {
55 self.chunks.extend(chunks);
56 self.revision = self.revision.saturating_add(1);
57 self.bounds = combined_chunk_bounds(&self.chunks);
58 }
59
60 pub fn set_overlay(&mut self, overlay: GeometrySceneOverlay) {
61 self.overlay = Some(overlay);
62 self.revision = self.revision.saturating_add(1);
63 }
64
65 pub fn cache_key(&self) -> GeometrySceneCacheKey {
66 GeometrySceneCacheKey {
67 scene_id: self.scene_id.clone(),
68 revision: self.revision,
69 chunk_count: self.chunks.len(),
70 vertex_count: self.vertex_count(),
71 index_count: self.index_count(),
72 }
73 }
74
75 pub fn vertex_count(&self) -> usize {
76 self.chunks
77 .iter()
78 .map(|chunk| chunk.render_data.vertex_count())
79 .sum()
80 }
81
82 pub fn index_count(&self) -> usize {
83 self.chunks
84 .iter()
85 .map(|chunk| chunk.indices.as_ref().map(Vec::len).unwrap_or(0))
86 .sum()
87 }
88
89 pub fn triangle_count(&self) -> usize {
90 self.chunks
91 .iter()
92 .map(GeometrySceneChunk::triangle_count)
93 .sum()
94 }
95
96 pub fn is_empty(&self) -> bool {
97 self.chunks.is_empty()
98 }
99
100 pub fn nodes(&self) -> Vec<SceneNode> {
101 self.nodes_with_presentation(&GeometryScenePresentation::default())
102 }
103
104 pub fn nodes_with_presentation(
105 &self,
106 presentation: &GeometryScenePresentation,
107 ) -> Vec<SceneNode> {
108 let mut nodes: Vec<SceneNode> = self
109 .chunks
110 .iter()
111 .enumerate()
112 .map(|(index, chunk)| SceneNode {
113 id: self.chunk_node_id(index, &chunk.chunk_id),
114 name: chunk
115 .label
116 .clone()
117 .unwrap_or_else(|| format!("Geometry chunk {}", index + 1)),
118 transform: Mat4::IDENTITY,
119 visible: chunk.visible,
120 cast_shadows: false,
121 receive_shadows: false,
122 axes_index: 0,
123 parent: None,
124 children: Vec::new(),
125 render_data: Some(chunk.render_data_with_presentation(presentation)),
126 bounds: chunk.bounds,
127 lod_levels: Vec::new(),
128 current_lod: 0,
129 })
130 .collect();
131 nodes.extend(self.annotation_nodes(presentation));
132 nodes
133 }
134
135 pub fn chunk_node_id(&self, index: usize, chunk_id: &str) -> u64 {
136 stable_node_id(&self.scene_id, self.revision, index, chunk_id)
137 }
138
139 fn annotation_nodes(&self, presentation: &GeometryScenePresentation) -> Vec<SceneNode> {
140 if presentation.region_annotations.is_empty() {
141 return Vec::new();
142 }
143
144 let mut point_vertices = Vec::new();
145 let mut line_vertices = Vec::new();
146 let arrow_length = annotation_arrow_length(self.bounds);
147
148 for annotation in &presentation.region_annotations {
149 for chunk in &self.chunks {
150 if !chunk.visible || chunk.render_data.pipeline_type != PipelineType::Triangles {
151 continue;
152 }
153 let Some(anchor) = chunk.region_anchor(&annotation.region_id) else {
154 continue;
155 };
156 let color = annotation.color;
157 let mut marker = vertex(
158 anchor.to_array(),
159 color,
160 [0.0, 0.0, annotation.size.unwrap_or(15.0)],
161 );
162 marker.tex_coords = [1.0, 1.0];
163 point_vertices.push(marker);
164
165 if let Some(direction) = annotation
166 .direction
167 .and_then(normalized_annotation_direction)
168 {
169 append_annotation_arrow(
170 &mut line_vertices,
171 anchor,
172 direction,
173 arrow_length,
174 color,
175 );
176 }
177 }
178 }
179
180 let mut nodes = Vec::new();
181 if !point_vertices.is_empty() {
182 nodes.push(self.annotation_node(
183 "FEA region markers",
184 "__fea_annotations:markers",
185 self.chunks.len(),
186 PipelineType::Points,
187 point_vertices,
188 ));
189 }
190 if !line_vertices.is_empty() {
191 nodes.push(self.annotation_node(
192 "FEA load vectors",
193 "__fea_annotations:vectors",
194 self.chunks.len() + 1,
195 PipelineType::Lines,
196 line_vertices,
197 ));
198 }
199 nodes
200 }
201
202 fn annotation_node(
203 &self,
204 name: &str,
205 chunk_id: &str,
206 index: usize,
207 pipeline_type: PipelineType,
208 vertices: Vec<Vertex>,
209 ) -> SceneNode {
210 let vertex_count = vertices.len();
211 let bounds = bounds_from_vertices(&vertices);
212 SceneNode {
213 id: stable_node_id(&self.scene_id, self.revision, index, chunk_id),
214 name: name.to_string(),
215 transform: Mat4::IDENTITY,
216 visible: true,
217 cast_shadows: false,
218 receive_shadows: false,
219 axes_index: 0,
220 parent: None,
221 children: Vec::new(),
222 render_data: Some(RenderData {
223 pipeline_type,
224 vertices,
225 indices: None,
226 gpu_vertices: None,
227 bounds: Some(bounds),
228 material: Material {
229 albedo: Vec4::ONE,
230 alpha_mode: AlphaMode::Blend,
231 double_sided: true,
232 ..Default::default()
233 },
234 draw_calls: vec![DrawCall {
235 vertex_offset: 0,
236 vertex_count,
237 index_offset: None,
238 index_count: None,
239 instance_count: 1,
240 }],
241 image: None,
242 }),
243 bounds,
244 lod_levels: Vec::new(),
245 current_lod: 0,
246 }
247 }
248}
249
250#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
251#[serde(rename_all = "camelCase")]
252pub struct GeometryScenePresentation {
253 pub selected_region_id: Option<String>,
254 pub hovered_region_id: Option<String>,
255 #[serde(default)]
256 pub region_highlights: Vec<GeometrySceneRegionHighlight>,
257 #[serde(default)]
258 pub region_annotations: Vec<GeometrySceneRegionAnnotation>,
259 #[serde(default)]
260 pub display_mode: GeometrySceneDisplayMode,
261 #[serde(default = "default_edge_overlay_enabled")]
262 pub edge_overlay_enabled: bool,
263}
264
265impl Default for GeometryScenePresentation {
266 fn default() -> Self {
267 Self {
268 selected_region_id: None,
269 hovered_region_id: None,
270 region_highlights: Vec::new(),
271 region_annotations: Vec::new(),
272 display_mode: GeometrySceneDisplayMode::Shaded,
273 edge_overlay_enabled: true,
274 }
275 }
276}
277
278#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
279#[serde(rename_all = "camelCase")]
280pub struct GeometrySceneRegionHighlight {
281 pub region_id: String,
282 pub color: [f32; 4],
283 #[serde(default)]
284 pub role: Option<String>,
285 #[serde(default)]
286 pub label: Option<String>,
287}
288
289#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
290#[serde(rename_all = "camelCase")]
291pub struct GeometrySceneRegionAnnotation {
292 pub region_id: String,
293 pub color: [f32; 4],
294 #[serde(default)]
295 pub role: Option<String>,
296 #[serde(default)]
297 pub label: Option<String>,
298 #[serde(default)]
299 pub direction: Option<[f32; 3]>,
300 #[serde(default)]
301 pub size: Option<f32>,
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
305#[serde(rename_all = "camelCase")]
306pub enum GeometrySceneDisplayMode {
307 Shaded,
308 Edges,
309 Wireframe,
310}
311
312impl Default for GeometrySceneDisplayMode {
313 fn default() -> Self {
314 Self::Shaded
315 }
316}
317
318impl GeometrySceneDisplayMode {
319 fn alpha(self, edge_overlay_enabled: bool) -> f32 {
320 match self {
321 Self::Shaded => 1.0,
322 Self::Edges if edge_overlay_enabled => 0.84,
323 Self::Edges => 0.94,
324 Self::Wireframe => 0.16,
325 }
326 }
327}
328
329#[derive(Debug, Clone)]
330pub struct GeometryScenePickRequest {
331 pub camera: Camera,
332 pub surface_size: [f32; 2],
333 pub position: [f32; 2],
334}
335
336#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
337#[serde(rename_all = "camelCase")]
338pub struct GeometryScenePickResult {
339 pub mesh_id: Option<String>,
340 pub chunk_id: String,
341 pub triangle_index: usize,
342 pub region_id: Option<String>,
343 pub region_label: Option<String>,
344 pub region_tag: Option<String>,
345 pub distance: f32,
346 pub position: [f32; 3],
347}
348
349fn default_edge_overlay_enabled() -> bool {
350 true
351}
352
353#[derive(Debug, Clone)]
354pub struct GeometryScenePickIndex {
355 scene_key: GeometrySceneCacheKey,
356 triangles: Vec<IndexedTriangle>,
357 nodes: Vec<PickBvhNode>,
358 root: Option<usize>,
359}
360
361impl GeometryScenePickIndex {
362 pub fn build(scene: &GeometryScene) -> Self {
363 let mut triangles = Vec::with_capacity(scene.triangle_count());
364 for chunk in &scene.chunks {
365 if !chunk.visible || chunk.render_data.pipeline_type != PipelineType::Triangles {
366 continue;
367 }
368 let Some(indices) = chunk.indices.as_ref() else {
369 continue;
370 };
371 for (triangle_index, triangle) in indices.chunks_exact(3).enumerate() {
372 let a = chunk.vertices.get(triangle[0] as usize);
373 let b = chunk.vertices.get(triangle[1] as usize);
374 let c = chunk.vertices.get(triangle[2] as usize);
375 let (Some(a), Some(b), Some(c)) = (a, b, c) else {
376 continue;
377 };
378 let a = Vec3::from_array(a.position);
379 let b = Vec3::from_array(b.position);
380 let c = Vec3::from_array(c.position);
381 let bounds = triangle_bounds(a, b, c);
382 let region = chunk.region_for_triangle(triangle_index as u32);
383 triangles.push(IndexedTriangle {
384 a,
385 b,
386 c,
387 bounds,
388 centroid: (a + b + c) / 3.0,
389 mesh_id: chunk.mesh_id.clone(),
390 chunk_id: chunk.chunk_id.clone(),
391 triangle_index,
392 region_id: region.map(|item| item.region_id.clone()),
393 region_label: region.and_then(|item| item.label.clone()),
394 region_tag: region.and_then(|item| item.tag.clone()),
395 });
396 }
397 }
398
399 let mut nodes = Vec::new();
400 let triangle_count = triangles.len();
401 let root = build_pick_bvh_node(&mut nodes, &mut triangles, 0, triangle_count);
402 Self {
403 scene_key: scene.cache_key(),
404 triangles,
405 nodes,
406 root,
407 }
408 }
409
410 pub fn scene_key(&self) -> &GeometrySceneCacheKey {
411 &self.scene_key
412 }
413
414 pub fn triangle_count(&self) -> usize {
415 self.triangles.len()
416 }
417
418 pub fn is_empty(&self) -> bool {
419 self.triangles.is_empty()
420 }
421
422 pub fn pick(&self, request: GeometryScenePickRequest) -> Option<GeometryScenePickResult> {
423 if request.surface_size[0] <= 0.0 || request.surface_size[1] <= 0.0 {
424 return None;
425 }
426 let mut camera = request.camera;
427 let screen_size = Vec2::new(request.surface_size[0], request.surface_size[1]);
428 let screen_pos = Vec2::new(request.position[0], request.position[1]);
429 let origin = camera.screen_to_world(screen_pos, screen_size, 0.0);
430 let far = camera.screen_to_world(screen_pos, screen_size, 1.0);
431 let direction = (far - origin).normalize_or_zero();
432 if direction.length_squared() <= f32::EPSILON {
433 return None;
434 }
435 let ray = PickRay { origin, direction };
436 let mut best: Option<PickHit> = None;
437 if let Some(root) = self.root {
438 self.pick_node(root, &ray, &mut best);
439 }
440 let hit = best?;
441 let triangle = self.triangles.get(hit.triangle_index)?;
442 Some(GeometryScenePickResult {
443 mesh_id: triangle.mesh_id.clone(),
444 chunk_id: triangle.chunk_id.clone(),
445 triangle_index: triangle.triangle_index,
446 region_id: triangle.region_id.clone(),
447 region_label: triangle.region_label.clone(),
448 region_tag: triangle.region_tag.clone(),
449 distance: hit.distance,
450 position: (ray.origin + ray.direction * hit.distance).to_array(),
451 })
452 }
453
454 fn pick_node(&self, node_index: usize, ray: &PickRay, best: &mut Option<PickHit>) {
455 let Some(node) = self.nodes.get(node_index) else {
456 return;
457 };
458 let max_distance = best
459 .as_ref()
460 .map(|hit| hit.distance)
461 .unwrap_or(f32::INFINITY);
462 let Some(bounds_distance) = ray_intersects_bounds(ray, node.bounds) else {
463 return;
464 };
465 if bounds_distance > max_distance {
466 return;
467 }
468 match node.kind {
469 PickBvhNodeKind::Leaf { start, end } => {
470 for triangle_index in start..end {
471 let Some(triangle) = self.triangles.get(triangle_index) else {
472 continue;
473 };
474 if let Some(hit_distance) =
475 ray_intersects_triangle(ray, triangle.a, triangle.b, triangle.c)
476 {
477 if hit_distance > 0.0
478 && hit_distance
479 < best
480 .as_ref()
481 .map(|hit| hit.distance)
482 .unwrap_or(f32::INFINITY)
483 {
484 *best = Some(PickHit {
485 triangle_index,
486 distance: hit_distance,
487 });
488 }
489 }
490 }
491 }
492 PickBvhNodeKind::Branch { left, right } => {
493 self.pick_node(left, ray, best);
494 self.pick_node(right, ray, best);
495 }
496 }
497 }
498}
499
500#[derive(Debug, Clone, Copy, PartialEq, Eq)]
501pub enum GeometrySceneCompleteness {
502 Complete,
503 Loading,
504 BoundedPreview,
505 FailedComplete,
506}
507
508#[derive(Debug, Clone)]
509pub struct GeometrySceneOverlay {
510 pub source_name: Option<String>,
511 pub status: GeometrySceneCompleteness,
512 pub quality_label: Option<String>,
513 pub format: Option<String>,
514 pub source_label: Option<String>,
515 pub allow_create_fea_study: bool,
516 pub byte_count: Option<u64>,
517 pub mesh_count: usize,
518 pub vertex_count: usize,
519 pub triangle_count: usize,
520 pub progress_percent: Option<f64>,
521 pub region_count: usize,
522 pub mapped_region_count: usize,
523 pub assembly_nodes: Vec<GeometrySceneAssemblyNode>,
524 pub regions: Vec<GeometrySceneRegionSummary>,
525 pub warnings: Vec<String>,
526}
527
528#[derive(Debug, Clone)]
529pub struct GeometrySceneAssemblyNode {
530 pub node_id: String,
531 pub label: String,
532 pub children: Vec<GeometrySceneAssemblyNode>,
533}
534
535#[derive(Debug, Clone)]
536pub struct GeometrySceneRegionSummary {
537 pub region_id: String,
538 pub label: String,
539 pub tag: Option<String>,
540 pub kind: Option<String>,
541 pub triangle_count: usize,
542}
543
544#[derive(Debug, Clone, PartialEq, Eq)]
545pub struct GeometrySceneCacheKey {
546 pub scene_id: String,
547 pub revision: u64,
548 pub chunk_count: usize,
549 pub vertex_count: usize,
550 pub index_count: usize,
551}
552
553#[derive(Debug, Clone)]
554pub struct GeometrySceneChunk {
555 pub chunk_id: String,
556 pub mesh_id: Option<String>,
557 pub label: Option<String>,
558 pub vertices: Vec<Vertex>,
559 pub indices: Option<Vec<u32>>,
560 pub render_data: RenderData,
561 pub bounds: BoundingBox,
562 pub material: Material,
563 pub regions: Vec<GeometrySceneRegion>,
564 pub owner_node_ids: Vec<String>,
565 pub visible: bool,
566}
567
568impl GeometrySceneChunk {
569 pub fn indexed_triangles(
570 chunk_id: impl Into<String>,
571 vertices: Vec<Vertex>,
572 indices: Vec<u32>,
573 material: Material,
574 ) -> Self {
575 let bounds = bounds_from_vertices(&vertices);
576 let vertex_count = vertices.len();
577 let index_count = indices.len();
578 let render_data = RenderData {
579 pipeline_type: PipelineType::Triangles,
580 vertices: vertices.clone(),
581 indices: Some(indices.clone()),
582 gpu_vertices: None,
583 bounds: Some(bounds),
584 material: material.clone(),
585 draw_calls: vec![DrawCall {
586 vertex_offset: 0,
587 vertex_count,
588 index_offset: Some(0),
589 index_count: Some(index_count),
590 instance_count: 1,
591 }],
592 image: None,
593 };
594 Self {
595 chunk_id: chunk_id.into(),
596 mesh_id: None,
597 label: None,
598 vertices,
599 indices: Some(indices),
600 render_data,
601 bounds,
602 material,
603 regions: Vec::new(),
604 owner_node_ids: Vec::new(),
605 visible: true,
606 }
607 }
608
609 pub fn from_render_data(chunk_id: impl Into<String>, render_data: RenderData) -> Self {
610 let material = render_data.material.clone();
611 let vertices = render_data.vertices.clone();
612 let indices = render_data.indices.clone();
613 let bounds = render_data
614 .bounds
615 .unwrap_or_else(|| bounds_from_vertices(&vertices));
616 Self {
617 chunk_id: chunk_id.into(),
618 mesh_id: None,
619 label: None,
620 vertices,
621 indices,
622 render_data,
623 bounds,
624 material,
625 regions: Vec::new(),
626 owner_node_ids: Vec::new(),
627 visible: true,
628 }
629 }
630
631 pub fn with_mesh_id(mut self, mesh_id: impl Into<String>) -> Self {
632 self.mesh_id = Some(mesh_id.into());
633 self
634 }
635
636 pub fn with_label(mut self, label: impl Into<String>) -> Self {
637 self.label = Some(label.into());
638 self
639 }
640
641 pub fn with_regions(mut self, regions: Vec<GeometrySceneRegion>) -> Self {
642 self.regions = regions;
643 self
644 }
645
646 pub fn with_owner_node_ids(mut self, owner_node_ids: Vec<String>) -> Self {
647 self.owner_node_ids = owner_node_ids;
648 self
649 }
650
651 pub fn triangle_count(&self) -> usize {
652 if self.render_data.pipeline_type != PipelineType::Triangles {
653 return 0;
654 }
655 self.indices
656 .as_ref()
657 .map(|indices| indices.len() / 3)
658 .unwrap_or_else(|| self.render_data.vertex_count() / 3)
659 }
660
661 pub fn render_data(&self) -> RenderData {
662 self.render_data.clone()
663 }
664
665 pub fn render_data_with_presentation(
666 &self,
667 presentation: &GeometryScenePresentation,
668 ) -> RenderData {
669 let mut render_data = self.render_data.clone();
670 let is_edge_chunk = self.is_edge_chunk();
671 match presentation.display_mode {
672 GeometrySceneDisplayMode::Wireframe if !is_edge_chunk => {
673 render_data.material.alpha_mode = AlphaMode::Blend;
674 render_data.material.albedo.w = presentation
675 .display_mode
676 .alpha(presentation.edge_overlay_enabled);
677 }
678 GeometrySceneDisplayMode::Wireframe => {}
679 GeometrySceneDisplayMode::Edges
680 if is_edge_chunk && !presentation.edge_overlay_enabled =>
681 {
682 for vertex in &mut render_data.vertices {
683 vertex.color[3] = 0.0;
684 }
685 }
686 GeometrySceneDisplayMode::Edges | GeometrySceneDisplayMode::Shaded => {
687 if !is_edge_chunk {
688 let alpha = presentation
689 .display_mode
690 .alpha(presentation.edge_overlay_enabled)
691 .min(render_data.material.albedo.w);
692 render_data.material.albedo.w = alpha;
693 render_data.material.alpha_mode = if alpha < 0.98 {
694 AlphaMode::Blend
695 } else {
696 render_data.material.alpha_mode
697 };
698 for vertex in &mut render_data.vertices {
699 vertex.color[3] = vertex.color[3].min(alpha);
700 }
701 }
702 }
703 }
704
705 for highlight in &presentation.region_highlights {
706 self.apply_region_color(&mut render_data, &highlight.region_id, highlight.color);
707 }
708 if let Some(region_id) = presentation.hovered_region_id.as_deref() {
709 self.apply_region_color(&mut render_data, region_id, [0.43, 0.78, 1.0, 1.0]);
710 }
711 if let Some(region_id) = presentation.selected_region_id.as_deref() {
712 self.apply_region_color(&mut render_data, region_id, [0.98, 0.78, 0.22, 1.0]);
713 }
714 render_data
715 }
716
717 fn is_edge_chunk(&self) -> bool {
718 self.render_data.pipeline_type == PipelineType::Lines
719 || self.chunk_id.contains(":edges")
720 || self
721 .label
722 .as_ref()
723 .map(|label| label.to_ascii_lowercase().contains("edge"))
724 .unwrap_or(false)
725 }
726
727 fn region_for_triangle(&self, triangle_index: u32) -> Option<&GeometrySceneRegion> {
728 self.regions.iter().find(|region| {
729 region.triangle_ranges.iter().any(|range| {
730 triangle_index >= range.start
731 && triangle_index < range.start.saturating_add(range.count)
732 })
733 })
734 }
735
736 fn region_anchor(&self, region_id: &str) -> Option<Vec3> {
737 if self.render_data.pipeline_type != PipelineType::Triangles {
738 return None;
739 }
740 let region = self
741 .regions
742 .iter()
743 .find(|item| item.region_id == region_id)?;
744 let mut weighted_centroid = Vec3::ZERO;
745 let mut total_area = 0.0_f32;
746 let mut fallback_centroid = Vec3::ZERO;
747 let mut fallback_count = 0_usize;
748
749 for range in ®ion.triangle_ranges {
750 let start = range.start as usize;
751 let end = start.saturating_add(range.count as usize);
752 for triangle_index in start..end {
753 let Some((a, b, c)) = self.triangle_vertices(triangle_index) else {
754 continue;
755 };
756 let centroid = (a + b + c) / 3.0;
757 let area = (b - a).cross(c - a).length() * 0.5;
758 if area.is_finite() && area > 1.0e-8 {
759 weighted_centroid += centroid * area;
760 total_area += area;
761 } else {
762 fallback_centroid += centroid;
763 fallback_count += 1;
764 }
765 }
766 }
767
768 if total_area > 0.0 {
769 Some(weighted_centroid / total_area)
770 } else if fallback_count > 0 {
771 Some(fallback_centroid / fallback_count as f32)
772 } else {
773 None
774 }
775 }
776
777 fn triangle_vertices(&self, triangle_index: usize) -> Option<(Vec3, Vec3, Vec3)> {
778 let vertex_at = |index: u32| {
779 self.render_data
780 .vertices
781 .get(index as usize)
782 .map(|vertex| Vec3::from_array(vertex.position))
783 };
784
785 if let Some(indices) = self.indices.as_ref() {
786 let base = triangle_index.checked_mul(3)?;
787 let triangle = indices.get(base..base + 3)?;
788 Some((
789 vertex_at(triangle[0])?,
790 vertex_at(triangle[1])?,
791 vertex_at(triangle[2])?,
792 ))
793 } else {
794 let base = triangle_index.checked_mul(3)?;
795 let a = self.render_data.vertices.get(base)?;
796 let b = self.render_data.vertices.get(base + 1)?;
797 let c = self.render_data.vertices.get(base + 2)?;
798 Some((
799 Vec3::from_array(a.position),
800 Vec3::from_array(b.position),
801 Vec3::from_array(c.position),
802 ))
803 }
804 }
805
806 fn apply_region_color(&self, render_data: &mut RenderData, region_id: &str, color: [f32; 4]) {
807 if self.render_data.pipeline_type != PipelineType::Triangles {
808 return;
809 }
810 let Some(region) = self.regions.iter().find(|item| item.region_id == region_id) else {
811 return;
812 };
813 let Some(indices) = self.indices.as_ref() else {
814 return;
815 };
816 let Some(render_indices) = render_data.indices.as_mut() else {
817 return;
818 };
819 for range in ®ion.triangle_ranges {
820 let start = range.start as usize;
821 let end = start.saturating_add(range.count as usize);
822 for triangle_index in start..end {
823 let base = triangle_index.saturating_mul(3);
824 let Some(triangle) = indices.get(base..base + 3) else {
825 continue;
826 };
827 if render_indices.get(base..base + 3).is_none() {
828 continue;
829 }
830 let mut isolated_indices = [0_u32; 3];
831 let mut isolated = true;
832 for (slot, vertex_index) in triangle.iter().copied().enumerate() {
833 let Some(vertex) = render_data.vertices.get(vertex_index as usize).copied()
834 else {
835 isolated = false;
836 break;
837 };
838 let mut vertex = vertex;
839 vertex.color = color;
840 let next_index = render_data.vertices.len();
841 if next_index > u32::MAX as usize {
842 isolated = false;
843 break;
844 }
845 render_data.vertices.push(vertex);
846 isolated_indices[slot] = next_index as u32;
847 }
848 if isolated {
849 render_indices[base..base + 3].copy_from_slice(&isolated_indices);
850 }
851 }
852 }
853 }
854}
855
856#[derive(Debug, Clone, PartialEq, Eq)]
857pub struct GeometrySceneRegion {
858 pub region_id: String,
859 pub label: Option<String>,
860 pub tag: Option<String>,
861 pub triangle_ranges: Vec<GeometrySceneTriangleRange>,
862}
863
864impl GeometrySceneRegion {
865 pub fn new(
866 region_id: impl Into<String>,
867 label: Option<String>,
868 tag: Option<String>,
869 triangle_ranges: Vec<GeometrySceneTriangleRange>,
870 ) -> Self {
871 Self {
872 region_id: region_id.into(),
873 label,
874 tag,
875 triangle_ranges,
876 }
877 }
878}
879
880#[derive(Debug, Clone, Copy, PartialEq, Eq)]
881pub struct GeometrySceneTriangleRange {
882 pub start: u32,
883 pub count: u32,
884}
885
886impl GeometrySceneTriangleRange {
887 pub fn new(start: u32, count: u32) -> Self {
888 Self { start, count }
889 }
890}
891
892pub fn cad_default_material() -> Material {
893 Material {
894 albedo: Vec4::new(0.46, 0.49, 0.48, 1.0),
895 roughness: 0.72,
896 metallic: 0.0,
897 emissive: Vec4::ZERO,
898 alpha_mode: AlphaMode::Opaque,
899 double_sided: true,
900 }
901}
902
903pub fn vertex(position: [f32; 3], color: [f32; 4], normal: [f32; 3]) -> Vertex {
904 Vertex {
905 position,
906 color,
907 normal,
908 tex_coords: [0.0, 0.0],
909 }
910}
911
912fn bounds_from_vertices(vertices: &[Vertex]) -> BoundingBox {
913 if vertices.is_empty() {
914 return BoundingBox::default();
915 }
916 let mut bounds = BoundingBox::new(
917 Vec3::from_array(vertices[0].position),
918 Vec3::from_array(vertices[0].position),
919 );
920 for item in vertices.iter().skip(1) {
921 bounds.expand(Vec3::from_array(item.position));
922 }
923 bounds
924}
925
926fn combined_chunk_bounds(chunks: &[GeometrySceneChunk]) -> BoundingBox {
927 let mut bounds = BoundingBox::default();
928 for chunk in chunks {
929 bounds.expand_by_box(&chunk.bounds);
930 }
931 bounds
932}
933
934fn annotation_arrow_length(bounds: BoundingBox) -> f32 {
935 let size = bounds.size();
936 let diagonal = size.length();
937 if diagonal.is_finite() && diagonal > 1.0e-6 {
938 diagonal * 0.075
939 } else {
940 1.0
941 }
942}
943
944fn normalized_annotation_direction(direction: [f32; 3]) -> Option<Vec3> {
945 let direction = Vec3::from_array(direction);
946 let length = direction.length();
947 (length.is_finite() && length > 1.0e-8).then_some(direction / length)
948}
949
950fn append_annotation_arrow(
951 vertices: &mut Vec<Vertex>,
952 anchor: Vec3,
953 direction: Vec3,
954 length: f32,
955 color: [f32; 4],
956) {
957 let start = anchor;
958 let end = anchor + direction * length;
959 append_annotation_line(vertices, start, end, color);
960
961 let side = perpendicular_unit(direction);
962 let wing_base = end - direction * (length * 0.28);
963 let wing_size = length * 0.12;
964 append_annotation_line(vertices, end, wing_base + side * wing_size, color);
965 append_annotation_line(vertices, end, wing_base - side * wing_size, color);
966}
967
968fn append_annotation_line(vertices: &mut Vec<Vertex>, start: Vec3, end: Vec3, color: [f32; 4]) {
969 vertices.push(vertex(start.to_array(), color, [0.0, 0.0, 1.0]));
970 vertices.push(vertex(end.to_array(), color, [0.0, 0.0, 1.0]));
971}
972
973fn perpendicular_unit(direction: Vec3) -> Vec3 {
974 let reference = if direction.z.abs() < 0.9 {
975 Vec3::Z
976 } else {
977 Vec3::Y
978 };
979 let side = direction.cross(reference);
980 let length = side.length();
981 if length > 1.0e-8 {
982 side / length
983 } else {
984 Vec3::X
985 }
986}
987
988fn stable_node_id(scene_id: &str, revision: u64, index: usize, chunk_id: &str) -> u64 {
989 const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
990 const FNV_PRIME: u64 = 0x100000001b3;
991 let mut hash = FNV_OFFSET_BASIS;
992 for byte in scene_id
993 .as_bytes()
994 .iter()
995 .chain(chunk_id.as_bytes())
996 .copied()
997 {
998 hash ^= u64::from(byte);
999 hash = hash.wrapping_mul(FNV_PRIME);
1000 }
1001 hash ^= revision;
1002 hash = hash.wrapping_mul(FNV_PRIME);
1003 hash ^ index as u64
1004}
1005
1006#[derive(Debug, Clone)]
1007struct IndexedTriangle {
1008 a: Vec3,
1009 b: Vec3,
1010 c: Vec3,
1011 bounds: BoundingBox,
1012 centroid: Vec3,
1013 mesh_id: Option<String>,
1014 chunk_id: String,
1015 triangle_index: usize,
1016 region_id: Option<String>,
1017 region_label: Option<String>,
1018 region_tag: Option<String>,
1019}
1020
1021#[derive(Debug, Clone)]
1022struct PickBvhNode {
1023 bounds: BoundingBox,
1024 kind: PickBvhNodeKind,
1025}
1026
1027#[derive(Debug, Clone, Copy)]
1028enum PickBvhNodeKind {
1029 Leaf { start: usize, end: usize },
1030 Branch { left: usize, right: usize },
1031}
1032
1033#[derive(Debug, Clone, Copy)]
1034struct PickRay {
1035 origin: Vec3,
1036 direction: Vec3,
1037}
1038
1039#[derive(Debug, Clone, Copy)]
1040struct PickHit {
1041 triangle_index: usize,
1042 distance: f32,
1043}
1044
1045fn build_pick_bvh_node(
1046 nodes: &mut Vec<PickBvhNode>,
1047 triangles: &mut [IndexedTriangle],
1048 start: usize,
1049 end: usize,
1050) -> Option<usize> {
1051 if start >= end {
1052 return None;
1053 }
1054 let bounds = combined_triangle_bounds(&triangles[start..end]);
1055 let node_index = nodes.len();
1056 nodes.push(PickBvhNode {
1057 bounds,
1058 kind: PickBvhNodeKind::Leaf { start, end },
1059 });
1060 const LEAF_TRIANGLES: usize = 32;
1061 if end - start <= LEAF_TRIANGLES {
1062 return Some(node_index);
1063 }
1064 let centroid_bounds = combined_centroid_bounds(&triangles[start..end]);
1065 let extent = centroid_bounds.max - centroid_bounds.min;
1066 let axis = if extent.x >= extent.y && extent.x >= extent.z {
1067 0
1068 } else if extent.y >= extent.z {
1069 1
1070 } else {
1071 2
1072 };
1073 triangles[start..end].sort_by(|a, b| {
1074 a.centroid[axis]
1075 .partial_cmp(&b.centroid[axis])
1076 .unwrap_or(std::cmp::Ordering::Equal)
1077 });
1078 let mid = start + (end - start) / 2;
1079 let left = build_pick_bvh_node(nodes, triangles, start, mid);
1080 let right = build_pick_bvh_node(nodes, triangles, mid, end);
1081 if let (Some(left), Some(right)) = (left, right) {
1082 nodes[node_index].kind = PickBvhNodeKind::Branch { left, right };
1083 }
1084 Some(node_index)
1085}
1086
1087fn triangle_bounds(a: Vec3, b: Vec3, c: Vec3) -> BoundingBox {
1088 let mut bounds = BoundingBox::new(a, a);
1089 bounds.expand(b);
1090 bounds.expand(c);
1091 bounds
1092}
1093
1094fn combined_triangle_bounds(triangles: &[IndexedTriangle]) -> BoundingBox {
1095 let mut bounds = BoundingBox::default();
1096 for triangle in triangles {
1097 bounds.expand_by_box(&triangle.bounds);
1098 }
1099 bounds
1100}
1101
1102fn combined_centroid_bounds(triangles: &[IndexedTriangle]) -> BoundingBox {
1103 let Some(first) = triangles.first() else {
1104 return BoundingBox::default();
1105 };
1106 let mut bounds = BoundingBox::new(first.centroid, first.centroid);
1107 for triangle in triangles.iter().skip(1) {
1108 bounds.expand(triangle.centroid);
1109 }
1110 bounds
1111}
1112
1113fn ray_intersects_bounds(ray: &PickRay, bounds: BoundingBox) -> Option<f32> {
1114 let mut t_min: f32 = 0.0;
1115 let mut t_max = f32::INFINITY;
1116 for axis in 0..3 {
1117 let origin = ray.origin[axis];
1118 let direction = ray.direction[axis];
1119 let min = bounds.min[axis];
1120 let max = bounds.max[axis];
1121 if direction.abs() < 1e-8 {
1122 if origin < min || origin > max {
1123 return None;
1124 }
1125 continue;
1126 }
1127 let inv_direction = 1.0 / direction;
1128 let mut t0 = (min - origin) * inv_direction;
1129 let mut t1 = (max - origin) * inv_direction;
1130 if t0 > t1 {
1131 std::mem::swap(&mut t0, &mut t1);
1132 }
1133 t_min = t_min.max(t0);
1134 t_max = t_max.min(t1);
1135 if t_max < t_min {
1136 return None;
1137 }
1138 }
1139 Some(t_min.max(0.0))
1140}
1141
1142fn ray_intersects_triangle(ray: &PickRay, a: Vec3, b: Vec3, c: Vec3) -> Option<f32> {
1143 let edge1 = b - a;
1144 let edge2 = c - a;
1145 let pvec = ray.direction.cross(edge2);
1146 let det = edge1.dot(pvec);
1147 if det.abs() < 1e-7 {
1148 return None;
1149 }
1150 let inv_det = 1.0 / det;
1151 let tvec = ray.origin - a;
1152 let u = tvec.dot(pvec) * inv_det;
1153 if !(0.0..=1.0).contains(&u) {
1154 return None;
1155 }
1156 let qvec = tvec.cross(edge1);
1157 let v = ray.direction.dot(qvec) * inv_det;
1158 if v < 0.0 || u + v > 1.0 {
1159 return None;
1160 }
1161 let t = edge2.dot(qvec) * inv_det;
1162 (t > 1e-6).then_some(t)
1163}
1164
1165#[cfg(test)]
1166mod tests {
1167 use super::*;
1168 use crate::core::ProjectionType;
1169
1170 #[test]
1171 fn pick_index_returns_region_for_triangle() {
1172 let material = cad_default_material();
1173 let chunk = GeometrySceneChunk::indexed_triangles(
1174 "face_chunk",
1175 vec![
1176 vertex([-1.0, -1.0, 0.0], [0.5, 0.5, 0.5, 1.0], [0.0, 0.0, 1.0]),
1177 vertex([1.0, -1.0, 0.0], [0.5, 0.5, 0.5, 1.0], [0.0, 0.0, 1.0]),
1178 vertex([0.0, 1.0, 0.0], [0.5, 0.5, 0.5, 1.0], [0.0, 0.0, 1.0]),
1179 ],
1180 vec![0, 1, 2],
1181 material,
1182 )
1183 .with_regions(vec![GeometrySceneRegion::new(
1184 "face_a",
1185 Some("Face A".to_string()),
1186 Some("cad-face".to_string()),
1187 vec![GeometrySceneTriangleRange::new(0, 1)],
1188 )]);
1189 let scene = GeometryScene::new("scene", 1, vec![chunk]);
1190 let index = GeometryScenePickIndex::build(&scene);
1191 let mut camera = Camera::new();
1192 camera.position = Vec3::new(0.0, 0.0, 5.0);
1193 camera.target = Vec3::ZERO;
1194 camera.up = Vec3::Y;
1195 camera.aspect_ratio = 1.0;
1196 camera.projection = ProjectionType::Perspective {
1197 fov: 45.0_f32.to_radians(),
1198 near: 0.1,
1199 far: 100.0,
1200 };
1201 camera.mark_dirty();
1202
1203 let hit = index.pick(GeometryScenePickRequest {
1204 camera,
1205 surface_size: [800.0, 800.0],
1206 position: [400.0, 400.0],
1207 });
1208 assert_eq!(
1209 hit.and_then(|hit| hit.region_id),
1210 Some("face_a".to_string())
1211 );
1212 }
1213
1214 #[test]
1215 fn presentation_region_annotations_emit_marker_and_vector_nodes() {
1216 let material = cad_default_material();
1217 let chunk = GeometrySceneChunk::indexed_triangles(
1218 "face_chunk",
1219 vec![
1220 vertex([-1.0, -1.0, 0.0], [0.5, 0.5, 0.5, 1.0], [0.0, 0.0, 1.0]),
1221 vertex([1.0, -1.0, 0.0], [0.5, 0.5, 0.5, 1.0], [0.0, 0.0, 1.0]),
1222 vertex([0.0, 1.0, 0.0], [0.5, 0.5, 0.5, 1.0], [0.0, 0.0, 1.0]),
1223 ],
1224 vec![0, 1, 2],
1225 material,
1226 )
1227 .with_regions(vec![GeometrySceneRegion::new(
1228 "loaded_face",
1229 Some("Loaded face".to_string()),
1230 Some("cad-face".to_string()),
1231 vec![GeometrySceneTriangleRange::new(0, 1)],
1232 )]);
1233 let scene = GeometryScene::new("scene", 1, vec![chunk]);
1234 let nodes = scene.nodes_with_presentation(&GeometryScenePresentation {
1235 region_annotations: vec![GeometrySceneRegionAnnotation {
1236 region_id: "loaded_face".to_string(),
1237 color: [0.9, 0.1, 0.1, 1.0],
1238 role: Some("load".to_string()),
1239 label: Some("load".to_string()),
1240 direction: Some([0.0, 0.0, 1.0]),
1241 size: Some(18.0),
1242 }],
1243 ..Default::default()
1244 });
1245
1246 let marker = nodes
1247 .iter()
1248 .find(|node| node.name == "FEA region markers")
1249 .and_then(|node| node.render_data.as_ref())
1250 .expect("marker annotation node");
1251 assert_eq!(marker.pipeline_type, PipelineType::Points);
1252 assert_eq!(marker.vertices.len(), 1);
1253 assert!((marker.vertices[0].position[1] - (-1.0 / 3.0)).abs() < 1.0e-6);
1254 assert_eq!(marker.vertices[0].normal[2], 18.0);
1255
1256 let vector = nodes
1257 .iter()
1258 .find(|node| node.name == "FEA load vectors")
1259 .and_then(|node| node.render_data.as_ref())
1260 .expect("vector annotation node");
1261 assert_eq!(vector.pipeline_type, PipelineType::Lines);
1262 assert_eq!(vector.vertices.len(), 6);
1263 assert!(vector.vertices[1].position[2] > vector.vertices[0].position[2]);
1264 }
1265}