1use crate::core::{
8 AlphaMode, BoundingBox, Camera, DrawCall, Material, PipelineType, RenderData, SceneNode, Vertex,
9};
10use glam::{Mat4, Vec2, Vec3, Vec4};
11use serde::{Deserialize, Deserializer, Serialize};
12use std::collections::BTreeSet;
13
14const SECTION_DISTANCE_EPSILON: f32 = 1.0e-6;
15const GEOMETRY_SELECTED_REGION_COLOR: [f32; 4] = [0.98, 0.45, 0.12, 1.0];
16const GEOMETRY_HOVER_REGION_COLOR: [f32; 4] = [0.43, 0.78, 1.0, 1.0];
17const GEOMETRY_MATERIAL_REGION_COLOR: [f32; 4] = [0.38, 0.58, 0.96, 0.92];
18const GEOMETRY_BOUNDARY_REGION_COLOR: [f32; 4] = [0.22, 0.82, 0.52, 0.96];
19const GEOMETRY_DRIVING_REGION_COLOR: [f32; 4] = [0.98, 0.38, 0.28, 0.96];
20
21#[derive(Debug, Clone)]
22pub struct GeometryScene {
23 pub scene_id: String,
24 pub revision: u64,
25 pub title: Option<String>,
26 pub overlay: Option<GeometrySceneOverlay>,
27 pub chunks: Vec<GeometrySceneChunk>,
28 pub bounds: BoundingBox,
29 pub show_grid: bool,
30 pub axis_equal: bool,
31}
32
33impl GeometryScene {
34 pub fn new(
35 scene_id: impl Into<String>,
36 revision: u64,
37 chunks: Vec<GeometrySceneChunk>,
38 ) -> Self {
39 let bounds = combined_chunk_bounds(&chunks);
40 Self {
41 scene_id: scene_id.into(),
42 revision,
43 title: None,
44 overlay: None,
45 chunks,
46 bounds,
47 show_grid: false,
48 axis_equal: true,
49 }
50 }
51
52 pub fn with_title(mut self, title: impl Into<String>) -> Self {
53 self.title = Some(title.into());
54 self
55 }
56
57 pub fn with_overlay(mut self, overlay: GeometrySceneOverlay) -> Self {
58 self.overlay = Some(overlay);
59 self
60 }
61
62 pub fn append_chunks(&mut self, chunks: impl IntoIterator<Item = GeometrySceneChunk>) {
63 self.chunks.extend(chunks);
64 self.revision = self.revision.saturating_add(1);
65 self.bounds = combined_chunk_bounds(&self.chunks);
66 }
67
68 pub fn set_overlay(&mut self, overlay: GeometrySceneOverlay) {
69 self.overlay = Some(overlay);
70 self.revision = self.revision.saturating_add(1);
71 }
72
73 pub fn cache_key(&self) -> GeometrySceneCacheKey {
74 GeometrySceneCacheKey {
75 scene_id: self.scene_id.clone(),
76 revision: self.revision,
77 chunk_count: self.chunks.len(),
78 vertex_count: self.vertex_count(),
79 index_count: self.index_count(),
80 }
81 }
82
83 pub fn vertex_count(&self) -> usize {
84 self.chunks
85 .iter()
86 .map(|chunk| chunk.render_data.vertex_count())
87 .sum()
88 }
89
90 pub fn index_count(&self) -> usize {
91 self.chunks
92 .iter()
93 .map(|chunk| chunk.indices.as_ref().map(Vec::len).unwrap_or(0))
94 .sum()
95 }
96
97 pub fn triangle_count(&self) -> usize {
98 self.chunks
99 .iter()
100 .map(GeometrySceneChunk::triangle_count)
101 .sum()
102 }
103
104 pub fn is_empty(&self) -> bool {
105 self.chunks.is_empty()
106 }
107
108 pub fn nodes(&self) -> Vec<SceneNode> {
109 self.nodes_with_presentation(&GeometryScenePresentation::default())
110 }
111
112 pub fn nodes_with_presentation(
113 &self,
114 presentation: &GeometryScenePresentation,
115 ) -> Vec<SceneNode> {
116 let mut nodes: Vec<SceneNode> = self
117 .chunks
118 .iter()
119 .enumerate()
120 .map(|(index, chunk)| SceneNode {
121 id: self.chunk_node_id(index, &chunk.chunk_id),
122 name: chunk
123 .label
124 .clone()
125 .unwrap_or_else(|| format!("Geometry chunk {}", index + 1)),
126 transform: Mat4::IDENTITY,
127 visible: chunk.visible,
128 cast_shadows: false,
129 receive_shadows: false,
130 axes_index: 0,
131 parent: None,
132 children: Vec::new(),
133 render_data: Some(chunk.render_data_with_presentation(presentation)),
134 bounds: chunk.bounds,
135 lod_levels: Vec::new(),
136 current_lod: 0,
137 })
138 .collect();
139 nodes.extend(self.annotation_nodes(presentation));
140 nodes
141 }
142
143 pub fn chunk_node_id(&self, index: usize, chunk_id: &str) -> u64 {
144 stable_node_id(&self.scene_id, self.revision, index, chunk_id)
145 }
146
147 fn annotation_nodes(&self, presentation: &GeometryScenePresentation) -> Vec<SceneNode> {
148 if presentation.region_annotations.is_empty() {
149 return Vec::new();
150 }
151
152 let mut point_vertices = Vec::new();
153 let mut line_vertices = Vec::new();
154 let arrow_length = annotation_arrow_length(self.bounds);
155
156 for annotation in &presentation.region_annotations {
157 for chunk in &self.chunks {
158 if !chunk.visible || chunk.render_data.pipeline_type != PipelineType::Triangles {
159 continue;
160 }
161 let Some(anchor) = chunk.region_anchor(&annotation.region_id) else {
162 continue;
163 };
164 let color = annotation
165 .color
166 .unwrap_or_else(|| geometry_region_role_color(annotation.role.as_deref()));
167 let mut marker = vertex(
168 anchor.to_array(),
169 color,
170 [0.0, 0.0, annotation.size.unwrap_or(15.0)],
171 );
172 marker.tex_coords = [1.0, 1.0];
173 point_vertices.push(marker);
174
175 if let Some(direction) = annotation
176 .direction
177 .and_then(normalized_annotation_direction)
178 {
179 append_annotation_arrow(
180 &mut line_vertices,
181 anchor,
182 direction,
183 arrow_length,
184 color,
185 );
186 }
187 }
188 }
189
190 let mut nodes = Vec::new();
191 if !point_vertices.is_empty() {
192 nodes.push(self.annotation_node(
193 "FEA region markers",
194 "__fea_annotations:markers",
195 self.chunks.len(),
196 PipelineType::Points,
197 point_vertices,
198 ));
199 }
200 if !line_vertices.is_empty() {
201 nodes.push(self.annotation_node(
202 "FEA load vectors",
203 "__fea_annotations:vectors",
204 self.chunks.len() + 1,
205 PipelineType::Lines,
206 line_vertices,
207 ));
208 }
209 nodes
210 }
211
212 fn annotation_node(
213 &self,
214 name: &str,
215 chunk_id: &str,
216 index: usize,
217 pipeline_type: PipelineType,
218 vertices: Vec<Vertex>,
219 ) -> SceneNode {
220 let vertex_count = vertices.len();
221 let bounds = bounds_from_vertices(&vertices);
222 SceneNode {
223 id: stable_node_id(&self.scene_id, self.revision, index, chunk_id),
224 name: name.to_string(),
225 transform: Mat4::IDENTITY,
226 visible: true,
227 cast_shadows: false,
228 receive_shadows: false,
229 axes_index: 0,
230 parent: None,
231 children: Vec::new(),
232 render_data: Some(RenderData {
233 pipeline_type,
234 vertices,
235 indices: None,
236 gpu_vertices: None,
237 bounds: Some(bounds),
238 material: Material {
239 albedo: Vec4::ONE,
240 alpha_mode: AlphaMode::Blend,
241 double_sided: true,
242 ..Default::default()
243 },
244 draw_calls: vec![DrawCall {
245 vertex_offset: 0,
246 vertex_count,
247 index_offset: None,
248 index_count: None,
249 instance_count: 1,
250 }],
251 image: None,
252 }),
253 bounds,
254 lod_levels: Vec::new(),
255 current_lod: 0,
256 }
257 }
258}
259
260#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
261#[serde(rename_all = "camelCase")]
262pub struct GeometryScenePresentation {
263 pub selected_region_id: Option<String>,
264 #[serde(default)]
265 pub selected_region_ids: Vec<String>,
266 pub hovered_region_id: Option<String>,
267 #[serde(default)]
268 pub region_highlights: Vec<GeometrySceneRegionHighlight>,
269 #[serde(default)]
270 pub region_annotations: Vec<GeometrySceneRegionAnnotation>,
271 #[serde(default)]
272 pub display_mode: GeometrySceneDisplayMode,
273 #[serde(default = "default_edge_overlay_enabled")]
274 pub edge_overlay_enabled: bool,
275 #[serde(default, skip_serializing_if = "Option::is_none")]
276 pub hidden_owner_node_ids: Option<Vec<String>>,
277 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub isolated_owner_node_ids: Option<Vec<String>>,
279 #[serde(
280 default,
281 deserialize_with = "deserialize_explicit_optional_section",
282 skip_serializing_if = "Option::is_none"
283 )]
284 pub section: Option<Option<GeometrySceneSection>>,
285 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub view_preset: Option<GeometrySceneViewPreset>,
287}
288
289impl Default for GeometryScenePresentation {
290 fn default() -> Self {
291 Self {
292 selected_region_id: None,
293 selected_region_ids: Vec::new(),
294 hovered_region_id: None,
295 region_highlights: Vec::new(),
296 region_annotations: Vec::new(),
297 display_mode: GeometrySceneDisplayMode::Shaded,
298 edge_overlay_enabled: true,
299 hidden_owner_node_ids: None,
300 isolated_owner_node_ids: None,
301 section: None,
302 view_preset: None,
303 }
304 }
305}
306
307impl GeometryScenePresentation {
308 pub(crate) fn rewrites_geometry_vertices(&self) -> bool {
309 self.hovered_region_id.is_some()
310 || self.selected_region_id.is_some()
311 || !self.selected_region_ids.is_empty()
312 || !self.region_highlights.is_empty()
313 || self.active_section().is_some()
314 }
315
316 pub(crate) fn resolves_owner_visibility(&self) -> bool {
317 self.hidden_owner_node_ids.is_some() || self.isolated_owner_node_ids.is_some()
318 }
319
320 pub(crate) fn resolved_hidden_owner_node_ids<'a, I>(
321 &self,
322 all_owner_node_ids: I,
323 current_hidden_owner_node_ids: &BTreeSet<String>,
324 ) -> BTreeSet<String>
325 where
326 I: IntoIterator<Item = &'a str>,
327 {
328 if !self.resolves_owner_visibility() {
329 return current_hidden_owner_node_ids.clone();
330 }
331
332 let mut hidden = BTreeSet::new();
333 if let Some(isolated_owner_node_ids) = self.isolated_owner_node_ids.as_ref() {
334 let isolated =
335 normalized_owner_node_id_set(isolated_owner_node_ids.iter().map(String::as_str));
336 for owner_id in all_owner_node_ids {
337 let normalized = owner_id.trim();
338 if !normalized.is_empty() && !isolated.contains(normalized) {
339 hidden.insert(normalized.to_string());
340 }
341 }
342 }
343
344 if let Some(hidden_owner_node_ids) = self.hidden_owner_node_ids.as_ref() {
345 hidden.extend(normalized_owner_node_id_set(
346 hidden_owner_node_ids.iter().map(String::as_str),
347 ));
348 }
349 hidden
350 }
351
352 pub(crate) fn resolves_section(&self) -> bool {
353 self.section.is_some()
354 }
355
356 pub(crate) fn active_section(&self) -> Option<&GeometrySceneSection> {
357 self.section.as_ref().and_then(Option::as_ref)
358 }
359}
360
361fn normalized_owner_node_id_set<'a, I>(ids: I) -> BTreeSet<String>
362where
363 I: IntoIterator<Item = &'a str>,
364{
365 ids.into_iter()
366 .map(str::trim)
367 .filter(|id| !id.is_empty())
368 .map(ToOwned::to_owned)
369 .collect()
370}
371
372fn deserialize_explicit_optional_section<'de, D>(
373 deserializer: D,
374) -> Result<Option<Option<GeometrySceneSection>>, D::Error>
375where
376 D: Deserializer<'de>,
377{
378 Option::<GeometrySceneSection>::deserialize(deserializer).map(Some)
379}
380
381#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
382#[serde(rename_all = "camelCase")]
383pub struct GeometrySceneSection {
384 pub plane: GeometrySceneSectionPlane,
385}
386
387#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
388#[serde(rename_all = "camelCase")]
389pub struct GeometrySceneSectionPlane {
390 pub normal: [f32; 3],
391 #[serde(default)]
392 pub origin: Option<[f32; 3]>,
393 #[serde(default)]
394 pub offset: Option<f32>,
395 #[serde(default)]
396 pub label: Option<String>,
397}
398
399#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
400#[serde(rename_all = "snake_case")]
401pub enum GeometrySceneViewPreset {
402 Perspective,
403 Isometric,
404 Front,
405 Back,
406 Left,
407 Right,
408 Top,
409 Bottom,
410}
411
412#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
413#[serde(rename_all = "camelCase")]
414pub struct GeometrySceneRegionHighlight {
415 pub region_id: String,
416 #[serde(default)]
417 pub color: Option<[f32; 4]>,
418 #[serde(default)]
419 pub role: Option<String>,
420 #[serde(default)]
421 pub label: Option<String>,
422}
423
424#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
425#[serde(rename_all = "camelCase")]
426pub struct GeometrySceneRegionAnnotation {
427 pub region_id: String,
428 #[serde(default)]
429 pub color: Option<[f32; 4]>,
430 #[serde(default)]
431 pub role: Option<String>,
432 #[serde(default)]
433 pub label: Option<String>,
434 #[serde(default)]
435 pub direction: Option<[f32; 3]>,
436 #[serde(default)]
437 pub size: Option<f32>,
438}
439
440#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
441#[serde(rename_all = "camelCase")]
442pub enum GeometrySceneDisplayMode {
443 Shaded,
444 Edges,
445 Wireframe,
446}
447
448impl Default for GeometrySceneDisplayMode {
449 fn default() -> Self {
450 Self::Shaded
451 }
452}
453
454impl GeometrySceneDisplayMode {
455 fn alpha(self, edge_overlay_enabled: bool) -> f32 {
456 match self {
457 Self::Shaded => 1.0,
458 Self::Edges if edge_overlay_enabled => 0.84,
459 Self::Edges => 0.94,
460 Self::Wireframe => 0.16,
461 }
462 }
463}
464
465#[derive(Debug, Clone)]
466pub struct GeometryScenePickRequest {
467 pub camera: Camera,
468 pub surface_size: [f32; 2],
469 pub position: [f32; 2],
470}
471
472#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
473#[serde(rename_all = "camelCase")]
474pub struct GeometryScenePickResult {
475 pub mesh_id: Option<String>,
476 pub chunk_id: String,
477 pub triangle_index: usize,
478 pub region_id: Option<String>,
479 pub region_label: Option<String>,
480 pub region_tag: Option<String>,
481 pub distance: f32,
482 pub position: [f32; 3],
483}
484
485fn default_edge_overlay_enabled() -> bool {
486 true
487}
488
489#[derive(Debug, Clone)]
490pub struct GeometryScenePickIndex {
491 scene_key: GeometrySceneCacheKey,
492 triangles: Vec<IndexedTriangle>,
493 nodes: Vec<PickBvhNode>,
494 root: Option<usize>,
495}
496
497impl GeometryScenePickIndex {
498 pub fn build(scene: &GeometryScene) -> Self {
499 let mut triangles = Vec::with_capacity(scene.triangle_count());
500 for chunk in &scene.chunks {
501 if !chunk.visible || chunk.render_data.pipeline_type != PipelineType::Triangles {
502 continue;
503 }
504 let Some(indices) = chunk.indices.as_ref() else {
505 continue;
506 };
507 for (triangle_index, triangle) in indices.chunks_exact(3).enumerate() {
508 let a = chunk.vertices.get(triangle[0] as usize);
509 let b = chunk.vertices.get(triangle[1] as usize);
510 let c = chunk.vertices.get(triangle[2] as usize);
511 let (Some(a), Some(b), Some(c)) = (a, b, c) else {
512 continue;
513 };
514 let a = Vec3::from_array(a.position);
515 let b = Vec3::from_array(b.position);
516 let c = Vec3::from_array(c.position);
517 let bounds = triangle_bounds(a, b, c);
518 let region = chunk.region_for_triangle(triangle_index as u32);
519 triangles.push(IndexedTriangle {
520 a,
521 b,
522 c,
523 bounds,
524 centroid: (a + b + c) / 3.0,
525 mesh_id: chunk.mesh_id.clone(),
526 chunk_id: chunk.chunk_id.clone(),
527 triangle_index,
528 region_id: region.map(|item| item.region_id.clone()),
529 region_label: region.and_then(|item| item.label.clone()),
530 region_tag: region.and_then(|item| item.tag.clone()),
531 });
532 }
533 }
534
535 let mut nodes = Vec::new();
536 let triangle_count = triangles.len();
537 let root = build_pick_bvh_node(&mut nodes, &mut triangles, 0, triangle_count);
538 Self {
539 scene_key: scene.cache_key(),
540 triangles,
541 nodes,
542 root,
543 }
544 }
545
546 pub fn scene_key(&self) -> &GeometrySceneCacheKey {
547 &self.scene_key
548 }
549
550 pub fn triangle_count(&self) -> usize {
551 self.triangles.len()
552 }
553
554 pub fn is_empty(&self) -> bool {
555 self.triangles.is_empty()
556 }
557
558 pub fn pick(&self, request: GeometryScenePickRequest) -> Option<GeometryScenePickResult> {
559 if request.surface_size[0] <= 0.0 || request.surface_size[1] <= 0.0 {
560 return None;
561 }
562 let mut camera = request.camera;
563 let screen_size = Vec2::new(request.surface_size[0], request.surface_size[1]);
564 let screen_pos = Vec2::new(request.position[0], request.position[1]);
565 let origin = camera.screen_to_world(screen_pos, screen_size, 0.0);
566 let far = camera.screen_to_world(screen_pos, screen_size, 1.0);
567 let direction = (far - origin).normalize_or_zero();
568 if direction.length_squared() <= f32::EPSILON {
569 return None;
570 }
571 let ray = PickRay { origin, direction };
572 let mut best: Option<PickHit> = None;
573 if let Some(root) = self.root {
574 self.pick_node(root, &ray, &mut best);
575 }
576 let hit = best?;
577 let triangle = self.triangles.get(hit.triangle_index)?;
578 Some(GeometryScenePickResult {
579 mesh_id: triangle.mesh_id.clone(),
580 chunk_id: triangle.chunk_id.clone(),
581 triangle_index: triangle.triangle_index,
582 region_id: triangle.region_id.clone(),
583 region_label: triangle.region_label.clone(),
584 region_tag: triangle.region_tag.clone(),
585 distance: hit.distance,
586 position: (ray.origin + ray.direction * hit.distance).to_array(),
587 })
588 }
589
590 fn pick_node(&self, node_index: usize, ray: &PickRay, best: &mut Option<PickHit>) {
591 let Some(node) = self.nodes.get(node_index) else {
592 return;
593 };
594 let max_distance = best
595 .as_ref()
596 .map(|hit| hit.distance)
597 .unwrap_or(f32::INFINITY);
598 let Some(bounds_distance) = ray_intersects_bounds(ray, node.bounds) else {
599 return;
600 };
601 if bounds_distance > max_distance {
602 return;
603 }
604 match node.kind {
605 PickBvhNodeKind::Leaf { start, end } => {
606 for triangle_index in start..end {
607 let Some(triangle) = self.triangles.get(triangle_index) else {
608 continue;
609 };
610 if let Some(hit_distance) =
611 ray_intersects_triangle(ray, triangle.a, triangle.b, triangle.c)
612 {
613 if hit_distance > 0.0
614 && hit_distance
615 < best
616 .as_ref()
617 .map(|hit| hit.distance)
618 .unwrap_or(f32::INFINITY)
619 {
620 *best = Some(PickHit {
621 triangle_index,
622 distance: hit_distance,
623 });
624 }
625 }
626 }
627 }
628 PickBvhNodeKind::Branch { left, right } => {
629 self.pick_node(left, ray, best);
630 self.pick_node(right, ray, best);
631 }
632 }
633 }
634}
635
636#[derive(Debug, Clone, Copy, PartialEq, Eq)]
637pub enum GeometrySceneCompleteness {
638 Complete,
639 Loading,
640 BoundedPreview,
641 FailedComplete,
642}
643
644#[derive(Debug, Clone)]
645pub struct GeometrySceneOverlay {
646 pub source_name: Option<String>,
647 pub status: GeometrySceneCompleteness,
648 pub quality_label: Option<String>,
649 pub format: Option<String>,
650 pub source_label: Option<String>,
651 pub allow_create_fea_study: bool,
652 pub byte_count: Option<u64>,
653 pub mesh_count: usize,
654 pub vertex_count: usize,
655 pub triangle_count: usize,
656 pub progress_percent: Option<f64>,
657 pub region_count: usize,
658 pub mapped_region_count: usize,
659 pub assembly_nodes: Vec<GeometrySceneAssemblyNode>,
660 pub regions: Vec<GeometrySceneRegionSummary>,
661 pub warnings: Vec<String>,
662}
663
664#[derive(Debug, Clone)]
665pub struct GeometrySceneAssemblyNode {
666 pub node_id: String,
667 pub label: String,
668 pub children: Vec<GeometrySceneAssemblyNode>,
669}
670
671#[derive(Debug, Clone)]
672pub struct GeometrySceneRegionSummary {
673 pub region_id: String,
674 pub label: String,
675 pub tag: Option<String>,
676 pub kind: Option<String>,
677 pub triangle_count: usize,
678}
679
680#[derive(Debug, Clone, PartialEq, Eq)]
681pub struct GeometrySceneCacheKey {
682 pub scene_id: String,
683 pub revision: u64,
684 pub chunk_count: usize,
685 pub vertex_count: usize,
686 pub index_count: usize,
687}
688
689#[derive(Debug, Clone)]
690pub struct GeometrySceneChunk {
691 pub chunk_id: String,
692 pub mesh_id: Option<String>,
693 pub label: Option<String>,
694 pub vertices: Vec<Vertex>,
695 pub indices: Option<Vec<u32>>,
696 pub render_data: RenderData,
697 pub bounds: BoundingBox,
698 pub material: Material,
699 pub regions: Vec<GeometrySceneRegion>,
700 pub owner_node_ids: Vec<String>,
701 pub visible: bool,
702}
703
704impl GeometrySceneChunk {
705 pub fn indexed_triangles(
706 chunk_id: impl Into<String>,
707 vertices: Vec<Vertex>,
708 indices: Vec<u32>,
709 material: Material,
710 ) -> Self {
711 let bounds = bounds_from_vertices(&vertices);
712 let vertex_count = vertices.len();
713 let index_count = indices.len();
714 let render_data = RenderData {
715 pipeline_type: PipelineType::Triangles,
716 vertices: vertices.clone(),
717 indices: Some(indices.clone()),
718 gpu_vertices: None,
719 bounds: Some(bounds),
720 material: material.clone(),
721 draw_calls: vec![DrawCall {
722 vertex_offset: 0,
723 vertex_count,
724 index_offset: Some(0),
725 index_count: Some(index_count),
726 instance_count: 1,
727 }],
728 image: None,
729 };
730 Self {
731 chunk_id: chunk_id.into(),
732 mesh_id: None,
733 label: None,
734 vertices,
735 indices: Some(indices),
736 render_data,
737 bounds,
738 material,
739 regions: Vec::new(),
740 owner_node_ids: Vec::new(),
741 visible: true,
742 }
743 }
744
745 pub fn from_render_data(chunk_id: impl Into<String>, render_data: RenderData) -> Self {
746 let material = render_data.material.clone();
747 let vertices = render_data.vertices.clone();
748 let indices = render_data.indices.clone();
749 let bounds = render_data
750 .bounds
751 .unwrap_or_else(|| bounds_from_vertices(&vertices));
752 Self {
753 chunk_id: chunk_id.into(),
754 mesh_id: None,
755 label: None,
756 vertices,
757 indices,
758 render_data,
759 bounds,
760 material,
761 regions: Vec::new(),
762 owner_node_ids: Vec::new(),
763 visible: true,
764 }
765 }
766
767 pub fn with_mesh_id(mut self, mesh_id: impl Into<String>) -> Self {
768 self.mesh_id = Some(mesh_id.into());
769 self
770 }
771
772 pub fn with_label(mut self, label: impl Into<String>) -> Self {
773 self.label = Some(label.into());
774 self
775 }
776
777 pub fn with_regions(mut self, regions: Vec<GeometrySceneRegion>) -> Self {
778 self.regions = regions;
779 self
780 }
781
782 pub fn with_owner_node_ids(mut self, owner_node_ids: Vec<String>) -> Self {
783 self.owner_node_ids = owner_node_ids;
784 self
785 }
786
787 pub fn triangle_count(&self) -> usize {
788 if self.render_data.pipeline_type != PipelineType::Triangles {
789 return 0;
790 }
791 self.indices
792 .as_ref()
793 .map(|indices| indices.len() / 3)
794 .unwrap_or_else(|| self.render_data.vertex_count() / 3)
795 }
796
797 pub fn render_data(&self) -> RenderData {
798 self.render_data.clone()
799 }
800
801 pub fn render_data_with_presentation(
802 &self,
803 presentation: &GeometryScenePresentation,
804 ) -> RenderData {
805 let mut render_data = self.render_data.clone();
806 if presentation.rewrites_geometry_vertices() {
807 render_data.gpu_vertices = None;
810 }
811 let is_edge_chunk = self.is_edge_chunk();
812 match presentation.display_mode {
813 GeometrySceneDisplayMode::Wireframe if !is_edge_chunk => {
814 render_data.material.alpha_mode = AlphaMode::Blend;
815 render_data.material.albedo.w = presentation
816 .display_mode
817 .alpha(presentation.edge_overlay_enabled);
818 }
819 GeometrySceneDisplayMode::Wireframe => {}
820 GeometrySceneDisplayMode::Edges
821 if is_edge_chunk && !presentation.edge_overlay_enabled =>
822 {
823 for vertex in &mut render_data.vertices {
824 vertex.color[3] = 0.0;
825 }
826 }
827 GeometrySceneDisplayMode::Edges | GeometrySceneDisplayMode::Shaded => {
828 if !is_edge_chunk {
829 let alpha = presentation
830 .display_mode
831 .alpha(presentation.edge_overlay_enabled)
832 .min(render_data.material.albedo.w);
833 render_data.material.albedo.w = alpha;
834 render_data.material.alpha_mode = if alpha < 0.98 {
835 AlphaMode::Blend
836 } else {
837 render_data.material.alpha_mode
838 };
839 for vertex in &mut render_data.vertices {
840 vertex.color[3] = vertex.color[3].min(alpha);
841 }
842 }
843 }
844 }
845
846 for highlight in &presentation.region_highlights {
847 self.apply_region_color(
848 &mut render_data,
849 &highlight.region_id,
850 highlight
851 .color
852 .unwrap_or_else(|| geometry_region_role_color(highlight.role.as_deref())),
853 );
854 }
855 if let Some(region_id) = presentation.hovered_region_id.as_deref() {
856 self.apply_region_color(&mut render_data, region_id, GEOMETRY_HOVER_REGION_COLOR);
857 }
858 for region_id in &presentation.selected_region_ids {
859 let colored = self.apply_region_color(
860 &mut render_data,
861 region_id,
862 GEOMETRY_SELECTED_REGION_COLOR,
863 );
864 log::info!(
865 target: "runmat_plot",
866 "geometry_scene.selection_color chunk_id={} region_id={} colored_triangles={}",
867 self.chunk_id,
868 region_id,
869 colored
870 );
871 }
872 if let Some(region_id) = presentation.selected_region_id.as_deref() {
873 let colored = self.apply_region_color(
874 &mut render_data,
875 region_id,
876 GEOMETRY_SELECTED_REGION_COLOR,
877 );
878 log::info!(
879 target: "runmat_plot",
880 "geometry_scene.selection_color chunk_id={} region_id={} colored_triangles={}",
881 self.chunk_id,
882 region_id,
883 colored
884 );
885 }
886 if let Some(section) = presentation.active_section() {
887 render_data = self.render_data_with_section(render_data, section);
888 }
889 render_data
890 }
891
892 fn render_data_with_section(
893 &self,
894 render_data: RenderData,
895 section: &GeometrySceneSection,
896 ) -> RenderData {
897 let Some(plane) = section_plane(§ion.plane) else {
898 return render_data;
899 };
900 match render_data.pipeline_type {
901 PipelineType::Triangles => clip_triangle_render_data(render_data, plane),
902 PipelineType::Lines => clip_line_render_data(render_data, plane),
903 _ => render_data,
904 }
905 }
906
907 fn is_edge_chunk(&self) -> bool {
908 matches!(
909 self.render_data.pipeline_type,
910 PipelineType::Lines | PipelineType::LinesNoDepth
911 ) || self.chunk_id.contains(":edges")
912 || self
913 .label
914 .as_ref()
915 .map(|label| label.to_ascii_lowercase().contains("edge"))
916 .unwrap_or(false)
917 }
918
919 fn region_for_triangle(&self, triangle_index: u32) -> Option<&GeometrySceneRegion> {
920 self.regions.iter().find(|region| {
921 region.triangle_ranges.iter().any(|range| {
922 triangle_index >= range.start
923 && triangle_index < range.start.saturating_add(range.count)
924 })
925 })
926 }
927
928 fn region_anchor(&self, region_id: &str) -> Option<Vec3> {
929 if self.render_data.pipeline_type != PipelineType::Triangles {
930 return None;
931 }
932 let region = self
933 .regions
934 .iter()
935 .find(|item| item.region_id == region_id)?;
936 let mut weighted_centroid = Vec3::ZERO;
937 let mut total_area = 0.0_f32;
938 let mut fallback_centroid = Vec3::ZERO;
939 let mut fallback_count = 0_usize;
940
941 for range in ®ion.triangle_ranges {
942 let start = range.start as usize;
943 let end = start.saturating_add(range.count as usize);
944 for triangle_index in start..end {
945 let Some((a, b, c)) = self.triangle_vertices(triangle_index) else {
946 continue;
947 };
948 let centroid = (a + b + c) / 3.0;
949 let area = (b - a).cross(c - a).length() * 0.5;
950 if area.is_finite() && area > 1.0e-8 {
951 weighted_centroid += centroid * area;
952 total_area += area;
953 } else {
954 fallback_centroid += centroid;
955 fallback_count += 1;
956 }
957 }
958 }
959
960 if total_area > 0.0 {
961 Some(weighted_centroid / total_area)
962 } else if fallback_count > 0 {
963 Some(fallback_centroid / fallback_count as f32)
964 } else {
965 None
966 }
967 }
968
969 fn triangle_vertices(&self, triangle_index: usize) -> Option<(Vec3, Vec3, Vec3)> {
970 let vertex_at = |index: u32| {
971 self.render_data
972 .vertices
973 .get(index as usize)
974 .map(|vertex| Vec3::from_array(vertex.position))
975 };
976
977 if let Some(indices) = self.indices.as_ref() {
978 let base = triangle_index.checked_mul(3)?;
979 let triangle = indices.get(base..base + 3)?;
980 Some((
981 vertex_at(triangle[0])?,
982 vertex_at(triangle[1])?,
983 vertex_at(triangle[2])?,
984 ))
985 } else {
986 let base = triangle_index.checked_mul(3)?;
987 let a = self.render_data.vertices.get(base)?;
988 let b = self.render_data.vertices.get(base + 1)?;
989 let c = self.render_data.vertices.get(base + 2)?;
990 Some((
991 Vec3::from_array(a.position),
992 Vec3::from_array(b.position),
993 Vec3::from_array(c.position),
994 ))
995 }
996 }
997
998 fn apply_region_color(
999 &self,
1000 render_data: &mut RenderData,
1001 region_id: &str,
1002 color: [f32; 4],
1003 ) -> usize {
1004 if self.render_data.pipeline_type != PipelineType::Triangles {
1005 return 0;
1006 }
1007 let Some(region) = self.regions.iter().find(|item| item.region_id == region_id) else {
1008 log::info!(
1009 target: "runmat_plot",
1010 "geometry_scene.selection_color_missing chunk_id={} region_id={} available_regions={}",
1011 self.chunk_id,
1012 region_id,
1013 self.regions.len()
1014 );
1015 return 0;
1016 };
1017 let (Some(indices), Some(render_indices)) =
1018 (self.indices.as_ref(), render_data.indices.as_mut())
1019 else {
1020 return self.apply_direct_region_color(render_data, region, color);
1021 };
1022
1023 let mut colored = 0usize;
1024 for range in ®ion.triangle_ranges {
1025 let start = range.start as usize;
1026 let end = start.saturating_add(range.count as usize);
1027 for triangle_index in start..end {
1028 let base = triangle_index.saturating_mul(3);
1029 let Some(triangle) = indices.get(base..base + 3) else {
1030 continue;
1031 };
1032 if render_indices.get(base..base + 3).is_none() {
1033 continue;
1034 }
1035 let mut isolated_indices = [0_u32; 3];
1036 let mut isolated = true;
1037 for (slot, vertex_index) in triangle.iter().copied().enumerate() {
1038 let Some(vertex) = render_data.vertices.get(vertex_index as usize).copied()
1039 else {
1040 isolated = false;
1041 break;
1042 };
1043 let mut vertex = vertex;
1044 vertex.color = color;
1045 let next_index = render_data.vertices.len();
1046 if next_index > u32::MAX as usize {
1047 isolated = false;
1048 break;
1049 }
1050 render_data.vertices.push(vertex);
1051 isolated_indices[slot] = next_index as u32;
1052 }
1053 if isolated {
1054 render_indices[base..base + 3].copy_from_slice(&isolated_indices);
1055 colored += 1;
1056 }
1057 }
1058 }
1059 colored
1060 }
1061
1062 fn apply_direct_region_color(
1063 &self,
1064 render_data: &mut RenderData,
1065 region: &GeometrySceneRegion,
1066 color: [f32; 4],
1067 ) -> usize {
1068 if render_data.indices.is_some() {
1069 return 0;
1070 }
1071 let mut colored = 0usize;
1072 for range in ®ion.triangle_ranges {
1073 let start = range.start as usize;
1074 let end = start.saturating_add(range.count as usize);
1075 for triangle_index in start..end {
1076 let base = triangle_index.saturating_mul(3);
1077 let Some(triangle) = render_data.vertices.get_mut(base..base + 3) else {
1078 continue;
1079 };
1080 for vertex in triangle {
1081 vertex.color = color;
1082 }
1083 colored += 1;
1084 }
1085 }
1086 colored
1087 }
1088}
1089
1090fn section_plane(plane: &GeometrySceneSectionPlane) -> Option<(Vec3, Vec3)> {
1091 let normal = Vec3::from_array(plane.normal).normalize_or_zero();
1092 if normal.length_squared() <= f32::EPSILON {
1093 return None;
1094 }
1095 let origin = plane
1096 .origin
1097 .map(Vec3::from_array)
1098 .unwrap_or_else(|| normal * plane.offset.unwrap_or(0.0));
1099 if !origin.is_finite() {
1100 return None;
1101 }
1102 Some((normal, origin))
1103}
1104
1105fn geometry_region_role_color(role: Option<&str>) -> [f32; 4] {
1106 match role.unwrap_or_default() {
1107 "material" => GEOMETRY_MATERIAL_REGION_COLOR,
1108 "boundary" => GEOMETRY_BOUNDARY_REGION_COLOR,
1109 "driving" => GEOMETRY_DRIVING_REGION_COLOR,
1110 "selection" => GEOMETRY_SELECTED_REGION_COLOR,
1111 _ => GEOMETRY_HOVER_REGION_COLOR,
1112 }
1113}
1114
1115fn clip_triangle_render_data(mut render_data: RenderData, plane: (Vec3, Vec3)) -> RenderData {
1116 let mut clipped_vertices = Vec::new();
1117 if let Some(indices) = render_data.indices.as_ref() {
1118 for triangle in indices.chunks_exact(3) {
1119 let Some(vertices) = triangle_vertices_from_indices(&render_data.vertices, triangle)
1120 else {
1121 continue;
1122 };
1123 push_clipped_triangle(vertices, plane, &mut clipped_vertices);
1124 }
1125 } else {
1126 for triangle in render_data.vertices.chunks_exact(3) {
1127 push_clipped_triangle(
1128 [triangle[0], triangle[1], triangle[2]],
1129 plane,
1130 &mut clipped_vertices,
1131 );
1132 }
1133 }
1134 let bounds = bounds_from_vertices(&clipped_vertices);
1135 let vertex_count = clipped_vertices.len();
1136 render_data.vertices = clipped_vertices;
1137 render_data.indices = None;
1138 render_data.bounds = Some(bounds);
1139 render_data.draw_calls = vec![DrawCall {
1140 vertex_offset: 0,
1141 vertex_count,
1142 index_offset: None,
1143 index_count: None,
1144 instance_count: 1,
1145 }];
1146 render_data
1147}
1148
1149fn clip_line_render_data(mut render_data: RenderData, plane: (Vec3, Vec3)) -> RenderData {
1150 let mut clipped_vertices = Vec::new();
1151 if let Some(indices) = render_data.indices.as_ref() {
1152 for line in indices.chunks_exact(2) {
1153 let Some(a) = render_data.vertices.get(line[0] as usize).copied() else {
1154 continue;
1155 };
1156 let Some(b) = render_data.vertices.get(line[1] as usize).copied() else {
1157 continue;
1158 };
1159 push_clipped_line(a, b, plane, &mut clipped_vertices);
1160 }
1161 } else {
1162 for line in render_data.vertices.chunks_exact(2) {
1163 push_clipped_line(line[0], line[1], plane, &mut clipped_vertices);
1164 }
1165 }
1166 let bounds = bounds_from_vertices(&clipped_vertices);
1167 let vertex_count = clipped_vertices.len();
1168 render_data.vertices = clipped_vertices;
1169 render_data.indices = None;
1170 render_data.bounds = Some(bounds);
1171 render_data.draw_calls = vec![DrawCall {
1172 vertex_offset: 0,
1173 vertex_count,
1174 index_offset: None,
1175 index_count: None,
1176 instance_count: 1,
1177 }];
1178 render_data
1179}
1180
1181fn triangle_vertices_from_indices(vertices: &[Vertex], triangle: &[u32]) -> Option<[Vertex; 3]> {
1182 Some([
1183 *vertices.get(*triangle.first()? as usize)?,
1184 *vertices.get(*triangle.get(1)? as usize)?,
1185 *vertices.get(*triangle.get(2)? as usize)?,
1186 ])
1187}
1188
1189fn push_clipped_triangle(vertices: [Vertex; 3], plane: (Vec3, Vec3), output: &mut Vec<Vertex>) {
1190 let clipped = clip_polygon_to_plane(&vertices, plane);
1191 if clipped.len() < 3 {
1192 return;
1193 }
1194 for index in 1..clipped.len().saturating_sub(1) {
1195 output.push(clipped[0]);
1196 output.push(clipped[index]);
1197 output.push(clipped[index + 1]);
1198 }
1199}
1200
1201fn push_clipped_line(a: Vertex, b: Vertex, plane: (Vec3, Vec3), output: &mut Vec<Vertex>) {
1202 let distance_a = signed_distance_to_plane(a, plane);
1203 let distance_b = signed_distance_to_plane(b, plane);
1204 let inside_a = distance_a >= -SECTION_DISTANCE_EPSILON;
1205 let inside_b = distance_b >= -SECTION_DISTANCE_EPSILON;
1206 match (inside_a, inside_b) {
1207 (true, true) => {
1208 output.push(a);
1209 output.push(b);
1210 }
1211 (false, false) => {}
1212 (true, false) => {
1213 output.push(a);
1214 output.push(interpolate_vertex(
1215 a,
1216 b,
1217 section_intersection_t(distance_a, distance_b),
1218 ));
1219 }
1220 (false, true) => {
1221 output.push(interpolate_vertex(
1222 a,
1223 b,
1224 section_intersection_t(distance_a, distance_b),
1225 ));
1226 output.push(b);
1227 }
1228 }
1229}
1230
1231fn clip_polygon_to_plane(vertices: &[Vertex], plane: (Vec3, Vec3)) -> Vec<Vertex> {
1232 let Some(mut previous) = vertices.last().copied() else {
1233 return Vec::new();
1234 };
1235 let mut previous_distance = signed_distance_to_plane(previous, plane);
1236 let mut previous_inside = previous_distance >= -SECTION_DISTANCE_EPSILON;
1237 let mut output = Vec::with_capacity(vertices.len() + 1);
1238 for current in vertices.iter().copied() {
1239 let current_distance = signed_distance_to_plane(current, plane);
1240 let current_inside = current_distance >= -SECTION_DISTANCE_EPSILON;
1241 if current_inside != previous_inside {
1242 output.push(interpolate_vertex(
1243 previous,
1244 current,
1245 section_intersection_t(previous_distance, current_distance),
1246 ));
1247 }
1248 if current_inside {
1249 output.push(current);
1250 }
1251 previous = current;
1252 previous_distance = current_distance;
1253 previous_inside = current_inside;
1254 }
1255 output
1256}
1257
1258fn signed_distance_to_plane(vertex: Vertex, (normal, origin): (Vec3, Vec3)) -> f32 {
1259 (Vec3::from_array(vertex.position) - origin).dot(normal)
1260}
1261
1262fn section_intersection_t(distance_a: f32, distance_b: f32) -> f32 {
1263 let denominator = distance_a - distance_b;
1264 if denominator.abs() <= f32::EPSILON {
1265 0.0
1266 } else {
1267 (distance_a / denominator).clamp(0.0, 1.0)
1268 }
1269}
1270
1271fn interpolate_vertex(a: Vertex, b: Vertex, t: f32) -> Vertex {
1272 let position = Vec3::from_array(a.position).lerp(Vec3::from_array(b.position), t);
1273 let color = Vec4::from_array(a.color).lerp(Vec4::from_array(b.color), t);
1274 let normal = Vec3::from_array(a.normal)
1275 .lerp(Vec3::from_array(b.normal), t)
1276 .normalize_or_zero();
1277 let tex_coords_a = Vec2::from_array(a.tex_coords);
1278 let tex_coords_b = Vec2::from_array(b.tex_coords);
1279 Vertex {
1280 position: position.to_array(),
1281 color: color.to_array(),
1282 normal: if normal.length_squared() > f32::EPSILON {
1283 normal.to_array()
1284 } else {
1285 a.normal
1286 },
1287 tex_coords: tex_coords_a.lerp(tex_coords_b, t).to_array(),
1288 }
1289}
1290
1291#[derive(Debug, Clone, PartialEq, Eq)]
1292pub struct GeometrySceneRegion {
1293 pub region_id: String,
1294 pub label: Option<String>,
1295 pub tag: Option<String>,
1296 pub triangle_ranges: Vec<GeometrySceneTriangleRange>,
1297}
1298
1299impl GeometrySceneRegion {
1300 pub fn new(
1301 region_id: impl Into<String>,
1302 label: Option<String>,
1303 tag: Option<String>,
1304 triangle_ranges: Vec<GeometrySceneTriangleRange>,
1305 ) -> Self {
1306 Self {
1307 region_id: region_id.into(),
1308 label,
1309 tag,
1310 triangle_ranges,
1311 }
1312 }
1313}
1314
1315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1316pub struct GeometrySceneTriangleRange {
1317 pub start: u32,
1318 pub count: u32,
1319}
1320
1321impl GeometrySceneTriangleRange {
1322 pub fn new(start: u32, count: u32) -> Self {
1323 Self { start, count }
1324 }
1325}
1326
1327pub fn cad_default_material() -> Material {
1328 Material {
1329 albedo: Vec4::new(0.46, 0.49, 0.48, 1.0),
1330 roughness: 0.72,
1331 metallic: 0.0,
1332 emissive: Vec4::ZERO,
1333 alpha_mode: AlphaMode::Opaque,
1334 double_sided: true,
1335 }
1336}
1337
1338pub fn vertex(position: [f32; 3], color: [f32; 4], normal: [f32; 3]) -> Vertex {
1339 Vertex {
1340 position,
1341 color,
1342 normal,
1343 tex_coords: [0.0, 0.0],
1344 }
1345}
1346
1347fn bounds_from_vertices(vertices: &[Vertex]) -> BoundingBox {
1348 if vertices.is_empty() {
1349 return BoundingBox::default();
1350 }
1351 let mut bounds = BoundingBox::new(
1352 Vec3::from_array(vertices[0].position),
1353 Vec3::from_array(vertices[0].position),
1354 );
1355 for item in vertices.iter().skip(1) {
1356 bounds.expand(Vec3::from_array(item.position));
1357 }
1358 bounds
1359}
1360
1361fn combined_chunk_bounds(chunks: &[GeometrySceneChunk]) -> BoundingBox {
1362 let mut bounds = BoundingBox::default();
1363 for chunk in chunks {
1364 bounds.expand_by_box(&chunk.bounds);
1365 }
1366 bounds
1367}
1368
1369fn annotation_arrow_length(bounds: BoundingBox) -> f32 {
1370 let size = bounds.size();
1371 let diagonal = size.length();
1372 if diagonal.is_finite() && diagonal > 1.0e-6 {
1373 diagonal * 0.075
1374 } else {
1375 1.0
1376 }
1377}
1378
1379fn normalized_annotation_direction(direction: [f32; 3]) -> Option<Vec3> {
1380 let direction = Vec3::from_array(direction);
1381 let length = direction.length();
1382 (length.is_finite() && length > 1.0e-8).then_some(direction / length)
1383}
1384
1385fn append_annotation_arrow(
1386 vertices: &mut Vec<Vertex>,
1387 anchor: Vec3,
1388 direction: Vec3,
1389 length: f32,
1390 color: [f32; 4],
1391) {
1392 let start = anchor;
1393 let end = anchor + direction * length;
1394 append_annotation_line(vertices, start, end, color);
1395
1396 let side = perpendicular_unit(direction);
1397 let wing_base = end - direction * (length * 0.28);
1398 let wing_size = length * 0.12;
1399 append_annotation_line(vertices, end, wing_base + side * wing_size, color);
1400 append_annotation_line(vertices, end, wing_base - side * wing_size, color);
1401}
1402
1403fn append_annotation_line(vertices: &mut Vec<Vertex>, start: Vec3, end: Vec3, color: [f32; 4]) {
1404 vertices.push(vertex(start.to_array(), color, [0.0, 0.0, 1.0]));
1405 vertices.push(vertex(end.to_array(), color, [0.0, 0.0, 1.0]));
1406}
1407
1408fn perpendicular_unit(direction: Vec3) -> Vec3 {
1409 let reference = if direction.z.abs() < 0.9 {
1410 Vec3::Z
1411 } else {
1412 Vec3::Y
1413 };
1414 let side = direction.cross(reference);
1415 let length = side.length();
1416 if length > 1.0e-8 {
1417 side / length
1418 } else {
1419 Vec3::X
1420 }
1421}
1422
1423fn stable_node_id(scene_id: &str, revision: u64, index: usize, chunk_id: &str) -> u64 {
1424 const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
1425 const FNV_PRIME: u64 = 0x100000001b3;
1426 let mut hash = FNV_OFFSET_BASIS;
1427 for byte in scene_id
1428 .as_bytes()
1429 .iter()
1430 .chain(chunk_id.as_bytes())
1431 .copied()
1432 {
1433 hash ^= u64::from(byte);
1434 hash = hash.wrapping_mul(FNV_PRIME);
1435 }
1436 hash ^= revision;
1437 hash = hash.wrapping_mul(FNV_PRIME);
1438 hash ^ index as u64
1439}
1440
1441#[derive(Debug, Clone)]
1442struct IndexedTriangle {
1443 a: Vec3,
1444 b: Vec3,
1445 c: Vec3,
1446 bounds: BoundingBox,
1447 centroid: Vec3,
1448 mesh_id: Option<String>,
1449 chunk_id: String,
1450 triangle_index: usize,
1451 region_id: Option<String>,
1452 region_label: Option<String>,
1453 region_tag: Option<String>,
1454}
1455
1456#[derive(Debug, Clone)]
1457struct PickBvhNode {
1458 bounds: BoundingBox,
1459 kind: PickBvhNodeKind,
1460}
1461
1462#[derive(Debug, Clone, Copy)]
1463enum PickBvhNodeKind {
1464 Leaf { start: usize, end: usize },
1465 Branch { left: usize, right: usize },
1466}
1467
1468#[derive(Debug, Clone, Copy)]
1469struct PickRay {
1470 origin: Vec3,
1471 direction: Vec3,
1472}
1473
1474#[derive(Debug, Clone, Copy)]
1475struct PickHit {
1476 triangle_index: usize,
1477 distance: f32,
1478}
1479
1480fn build_pick_bvh_node(
1481 nodes: &mut Vec<PickBvhNode>,
1482 triangles: &mut [IndexedTriangle],
1483 start: usize,
1484 end: usize,
1485) -> Option<usize> {
1486 if start >= end {
1487 return None;
1488 }
1489 let bounds = combined_triangle_bounds(&triangles[start..end]);
1490 let node_index = nodes.len();
1491 nodes.push(PickBvhNode {
1492 bounds,
1493 kind: PickBvhNodeKind::Leaf { start, end },
1494 });
1495 const LEAF_TRIANGLES: usize = 32;
1496 if end - start <= LEAF_TRIANGLES {
1497 return Some(node_index);
1498 }
1499 let centroid_bounds = combined_centroid_bounds(&triangles[start..end]);
1500 let extent = centroid_bounds.max - centroid_bounds.min;
1501 let axis = if extent.x >= extent.y && extent.x >= extent.z {
1502 0
1503 } else if extent.y >= extent.z {
1504 1
1505 } else {
1506 2
1507 };
1508 triangles[start..end].sort_by(|a, b| {
1509 a.centroid[axis]
1510 .partial_cmp(&b.centroid[axis])
1511 .unwrap_or(std::cmp::Ordering::Equal)
1512 });
1513 let mid = start + (end - start) / 2;
1514 let left = build_pick_bvh_node(nodes, triangles, start, mid);
1515 let right = build_pick_bvh_node(nodes, triangles, mid, end);
1516 if let (Some(left), Some(right)) = (left, right) {
1517 nodes[node_index].kind = PickBvhNodeKind::Branch { left, right };
1518 }
1519 Some(node_index)
1520}
1521
1522fn triangle_bounds(a: Vec3, b: Vec3, c: Vec3) -> BoundingBox {
1523 let mut bounds = BoundingBox::new(a, a);
1524 bounds.expand(b);
1525 bounds.expand(c);
1526 bounds
1527}
1528
1529fn combined_triangle_bounds(triangles: &[IndexedTriangle]) -> BoundingBox {
1530 let mut bounds = BoundingBox::default();
1531 for triangle in triangles {
1532 bounds.expand_by_box(&triangle.bounds);
1533 }
1534 bounds
1535}
1536
1537fn combined_centroid_bounds(triangles: &[IndexedTriangle]) -> BoundingBox {
1538 let Some(first) = triangles.first() else {
1539 return BoundingBox::default();
1540 };
1541 let mut bounds = BoundingBox::new(first.centroid, first.centroid);
1542 for triangle in triangles.iter().skip(1) {
1543 bounds.expand(triangle.centroid);
1544 }
1545 bounds
1546}
1547
1548fn ray_intersects_bounds(ray: &PickRay, bounds: BoundingBox) -> Option<f32> {
1549 let mut t_min: f32 = 0.0;
1550 let mut t_max = f32::INFINITY;
1551 for axis in 0..3 {
1552 let origin = ray.origin[axis];
1553 let direction = ray.direction[axis];
1554 let min = bounds.min[axis];
1555 let max = bounds.max[axis];
1556 if direction.abs() < 1e-8 {
1557 if origin < min || origin > max {
1558 return None;
1559 }
1560 continue;
1561 }
1562 let inv_direction = 1.0 / direction;
1563 let mut t0 = (min - origin) * inv_direction;
1564 let mut t1 = (max - origin) * inv_direction;
1565 if t0 > t1 {
1566 std::mem::swap(&mut t0, &mut t1);
1567 }
1568 t_min = t_min.max(t0);
1569 t_max = t_max.min(t1);
1570 if t_max < t_min {
1571 return None;
1572 }
1573 }
1574 Some(t_min.max(0.0))
1575}
1576
1577fn ray_intersects_triangle(ray: &PickRay, a: Vec3, b: Vec3, c: Vec3) -> Option<f32> {
1578 let edge1 = b - a;
1579 let edge2 = c - a;
1580 let pvec = ray.direction.cross(edge2);
1581 let det = edge1.dot(pvec);
1582 if det.abs() < 1e-7 {
1583 return None;
1584 }
1585 let inv_det = 1.0 / det;
1586 let tvec = ray.origin - a;
1587 let u = tvec.dot(pvec) * inv_det;
1588 if !(0.0..=1.0).contains(&u) {
1589 return None;
1590 }
1591 let qvec = tvec.cross(edge1);
1592 let v = ray.direction.dot(qvec) * inv_det;
1593 if v < 0.0 || u + v > 1.0 {
1594 return None;
1595 }
1596 let t = edge2.dot(qvec) * inv_det;
1597 (t > 1e-6).then_some(t)
1598}
1599
1600#[cfg(test)]
1601mod tests {
1602 use super::*;
1603 use crate::core::ProjectionType;
1604
1605 #[test]
1606 fn pick_index_returns_region_for_triangle() {
1607 let material = cad_default_material();
1608 let chunk = GeometrySceneChunk::indexed_triangles(
1609 "face_chunk",
1610 vec![
1611 vertex([-1.0, -1.0, 0.0], [0.5, 0.5, 0.5, 1.0], [0.0, 0.0, 1.0]),
1612 vertex([1.0, -1.0, 0.0], [0.5, 0.5, 0.5, 1.0], [0.0, 0.0, 1.0]),
1613 vertex([0.0, 1.0, 0.0], [0.5, 0.5, 0.5, 1.0], [0.0, 0.0, 1.0]),
1614 ],
1615 vec![0, 1, 2],
1616 material,
1617 )
1618 .with_regions(vec![GeometrySceneRegion::new(
1619 "face_a",
1620 Some("Face A".to_string()),
1621 Some("cad-face".to_string()),
1622 vec![GeometrySceneTriangleRange::new(0, 1)],
1623 )]);
1624 let scene = GeometryScene::new("scene", 1, vec![chunk]);
1625 let index = GeometryScenePickIndex::build(&scene);
1626 let mut camera = Camera::new();
1627 camera.position = Vec3::new(0.0, 0.0, 5.0);
1628 camera.target = Vec3::ZERO;
1629 camera.up = Vec3::Y;
1630 camera.aspect_ratio = 1.0;
1631 camera.projection = ProjectionType::Perspective {
1632 fov: 45.0_f32.to_radians(),
1633 near: 0.1,
1634 far: 100.0,
1635 };
1636 camera.mark_dirty();
1637
1638 let hit = index.pick(GeometryScenePickRequest {
1639 camera,
1640 surface_size: [800.0, 800.0],
1641 position: [400.0, 400.0],
1642 });
1643 assert_eq!(
1644 hit.and_then(|hit| hit.region_id),
1645 Some("face_a".to_string())
1646 );
1647 }
1648
1649 #[test]
1650 fn presentation_selection_colors_indexed_triangle_region() {
1651 let material = cad_default_material();
1652 let chunk = GeometrySceneChunk::indexed_triangles(
1653 "face_chunk",
1654 vec![
1655 vertex([-1.0, -1.0, 0.0], [0.5, 0.5, 0.5, 1.0], [0.0, 0.0, 1.0]),
1656 vertex([1.0, -1.0, 0.0], [0.5, 0.5, 0.5, 1.0], [0.0, 0.0, 1.0]),
1657 vertex([0.0, 1.0, 0.0], [0.5, 0.5, 0.5, 1.0], [0.0, 0.0, 1.0]),
1658 vertex([2.0, -1.0, 0.0], [0.5, 0.5, 0.5, 1.0], [0.0, 0.0, 1.0]),
1659 ],
1660 vec![0, 1, 2, 1, 3, 2],
1661 material,
1662 )
1663 .with_regions(vec![GeometrySceneRegion::new(
1664 "face_b",
1665 Some("Face B".to_string()),
1666 Some("cad-face".to_string()),
1667 vec![GeometrySceneTriangleRange::new(1, 1)],
1668 )]);
1669
1670 let render_data = chunk.render_data_with_presentation(&GeometryScenePresentation {
1671 selected_region_ids: vec!["face_b".to_string()],
1672 ..Default::default()
1673 });
1674
1675 assert_eq!(render_data.vertices.len(), 7);
1676 assert_eq!(
1677 render_data.indices.as_deref(),
1678 Some(&[0, 1, 2, 4, 5, 6][..])
1679 );
1680 assert_eq!(
1681 render_data.vertices[4].color,
1682 GEOMETRY_SELECTED_REGION_COLOR
1683 );
1684 assert_eq!(
1685 render_data.vertices[5].color,
1686 GEOMETRY_SELECTED_REGION_COLOR
1687 );
1688 assert_eq!(
1689 render_data.vertices[6].color,
1690 GEOMETRY_SELECTED_REGION_COLOR
1691 );
1692 }
1693
1694 #[test]
1695 fn presentation_selection_colors_direct_triangle_region() {
1696 let material = cad_default_material();
1697 let base_color = [0.5, 0.5, 0.5, 1.0];
1698 let vertices = vec![
1699 vertex([-1.0, -1.0, 0.0], base_color, [0.0, 0.0, 1.0]),
1700 vertex([1.0, -1.0, 0.0], base_color, [0.0, 0.0, 1.0]),
1701 vertex([0.0, 1.0, 0.0], base_color, [0.0, 0.0, 1.0]),
1702 vertex([1.0, -1.0, 0.0], base_color, [0.0, 0.0, 1.0]),
1703 vertex([2.0, -1.0, 0.0], base_color, [0.0, 0.0, 1.0]),
1704 vertex([0.0, 1.0, 0.0], base_color, [0.0, 0.0, 1.0]),
1705 ];
1706 let chunk = GeometrySceneChunk::from_render_data(
1707 "face_chunk",
1708 RenderData {
1709 pipeline_type: PipelineType::Triangles,
1710 vertices,
1711 indices: None,
1712 gpu_vertices: None,
1713 bounds: None,
1714 material,
1715 draw_calls: vec![DrawCall {
1716 vertex_offset: 0,
1717 vertex_count: 6,
1718 index_offset: None,
1719 index_count: None,
1720 instance_count: 1,
1721 }],
1722 image: None,
1723 },
1724 )
1725 .with_regions(vec![GeometrySceneRegion::new(
1726 "face_b",
1727 Some("Face B".to_string()),
1728 Some("cad-face".to_string()),
1729 vec![GeometrySceneTriangleRange::new(1, 1)],
1730 )]);
1731
1732 let render_data = chunk.render_data_with_presentation(&GeometryScenePresentation {
1733 selected_region_id: Some("face_b".to_string()),
1734 ..Default::default()
1735 });
1736
1737 assert!(render_data.indices.is_none());
1738 assert_eq!(render_data.vertices.len(), 6);
1739 assert_eq!(render_data.vertices[0].color, base_color);
1740 assert_eq!(render_data.vertices[1].color, base_color);
1741 assert_eq!(render_data.vertices[2].color, base_color);
1742 assert_eq!(
1743 render_data.vertices[3].color,
1744 GEOMETRY_SELECTED_REGION_COLOR
1745 );
1746 assert_eq!(
1747 render_data.vertices[4].color,
1748 GEOMETRY_SELECTED_REGION_COLOR
1749 );
1750 assert_eq!(
1751 render_data.vertices[5].color,
1752 GEOMETRY_SELECTED_REGION_COLOR
1753 );
1754 }
1755
1756 #[test]
1757 fn presentation_resolves_hidden_and_isolated_owner_visibility() {
1758 let current_hidden = BTreeSet::from(["panel_b".to_string()]);
1759 let all_owner_ids = ["panel_a", "panel_b", "panel_c"];
1760
1761 assert_eq!(
1762 GeometryScenePresentation::default()
1763 .resolved_hidden_owner_node_ids(all_owner_ids, ¤t_hidden),
1764 current_hidden
1765 );
1766
1767 assert_eq!(
1768 GeometryScenePresentation {
1769 hidden_owner_node_ids: Some(vec![
1770 " panel_a ".to_string(),
1771 String::new(),
1772 "panel_a".to_string(),
1773 ]),
1774 ..Default::default()
1775 }
1776 .resolved_hidden_owner_node_ids(all_owner_ids, &BTreeSet::new()),
1777 BTreeSet::from(["panel_a".to_string()])
1778 );
1779
1780 assert_eq!(
1781 GeometryScenePresentation {
1782 hidden_owner_node_ids: Some(vec!["panel_c".to_string()]),
1783 isolated_owner_node_ids: Some(vec!["panel_a".to_string()]),
1784 ..Default::default()
1785 }
1786 .resolved_hidden_owner_node_ids(all_owner_ids, &BTreeSet::new()),
1787 BTreeSet::from(["panel_b".to_string(), "panel_c".to_string()])
1788 );
1789
1790 assert_eq!(
1791 GeometryScenePresentation {
1792 hidden_owner_node_ids: Some(Vec::new()),
1793 isolated_owner_node_ids: None,
1794 ..Default::default()
1795 }
1796 .resolved_hidden_owner_node_ids(all_owner_ids, ¤t_hidden),
1797 BTreeSet::new()
1798 );
1799 }
1800
1801 #[test]
1802 fn presentation_section_distinguishes_preserve_clear_and_plane() {
1803 let preserve: GeometryScenePresentation =
1804 serde_json::from_value(serde_json::json!({})).expect("preserve presentation");
1805 assert_eq!(preserve.section, None);
1806
1807 let clear: GeometryScenePresentation =
1808 serde_json::from_value(serde_json::json!({ "section": null }))
1809 .expect("clear presentation");
1810 assert_eq!(clear.section, Some(None));
1811
1812 let sectioned: GeometryScenePresentation = serde_json::from_value(serde_json::json!({
1813 "section": {
1814 "plane": {
1815 "normal": [1.0, 0.0, 0.0],
1816 "origin": [0.0, 0.0, 0.0],
1817 "label": "midplane"
1818 }
1819 }
1820 }))
1821 .expect("section presentation");
1822 let section = sectioned
1823 .section
1824 .as_ref()
1825 .and_then(Option::as_ref)
1826 .expect("section");
1827 assert_eq!(section.plane.normal, [1.0, 0.0, 0.0]);
1828 assert_eq!(section.plane.label.as_deref(), Some("midplane"));
1829 }
1830
1831 #[test]
1832 fn presentation_accepts_bounded_view_presets() {
1833 let front: GeometryScenePresentation =
1834 serde_json::from_value(serde_json::json!({ "viewPreset": "front" }))
1835 .expect("front view presentation");
1836 assert_eq!(front.view_preset, Some(GeometrySceneViewPreset::Front));
1837
1838 let isometric: GeometryScenePresentation =
1839 serde_json::from_value(serde_json::json!({ "viewPreset": "isometric" }))
1840 .expect("isometric view presentation");
1841 assert_eq!(
1842 isometric.view_preset,
1843 Some(GeometrySceneViewPreset::Isometric)
1844 );
1845 }
1846
1847 #[test]
1848 fn presentation_section_clips_triangle_render_data() {
1849 let material = cad_default_material();
1850 let chunk = GeometrySceneChunk::indexed_triangles(
1851 "face_chunk",
1852 vec![
1853 vertex([-1.0, -1.0, 0.0], [0.5, 0.5, 0.5, 1.0], [0.0, 0.0, 1.0]),
1854 vertex([1.0, -1.0, 0.0], [0.5, 0.5, 0.5, 1.0], [0.0, 0.0, 1.0]),
1855 vertex([0.0, 1.0, 0.0], [0.5, 0.5, 0.5, 1.0], [0.0, 0.0, 1.0]),
1856 ],
1857 vec![0, 1, 2],
1858 material,
1859 );
1860 let render_data = chunk.render_data_with_presentation(&GeometryScenePresentation {
1861 section: Some(Some(GeometrySceneSection {
1862 plane: GeometrySceneSectionPlane {
1863 normal: [1.0, 0.0, 0.0],
1864 origin: Some([0.0, 0.0, 0.0]),
1865 offset: None,
1866 label: Some("midplane".to_string()),
1867 },
1868 })),
1869 ..Default::default()
1870 });
1871
1872 assert_eq!(render_data.pipeline_type, PipelineType::Triangles);
1873 assert!(render_data.indices.is_none());
1874 assert_eq!(render_data.vertices.len(), 6);
1875 assert_eq!(
1876 render_data.draw_calls[0].vertex_count,
1877 render_data.vertices.len()
1878 );
1879 assert!(render_data
1880 .vertices
1881 .iter()
1882 .all(|vertex| vertex.position[0] >= -SECTION_DISTANCE_EPSILON));
1883 }
1884
1885 #[test]
1886 fn presentation_region_annotations_emit_marker_and_vector_nodes() {
1887 let material = cad_default_material();
1888 let chunk = GeometrySceneChunk::indexed_triangles(
1889 "face_chunk",
1890 vec![
1891 vertex([-1.0, -1.0, 0.0], [0.5, 0.5, 0.5, 1.0], [0.0, 0.0, 1.0]),
1892 vertex([1.0, -1.0, 0.0], [0.5, 0.5, 0.5, 1.0], [0.0, 0.0, 1.0]),
1893 vertex([0.0, 1.0, 0.0], [0.5, 0.5, 0.5, 1.0], [0.0, 0.0, 1.0]),
1894 ],
1895 vec![0, 1, 2],
1896 material,
1897 )
1898 .with_regions(vec![GeometrySceneRegion::new(
1899 "loaded_face",
1900 Some("Loaded face".to_string()),
1901 Some("cad-face".to_string()),
1902 vec![GeometrySceneTriangleRange::new(0, 1)],
1903 )]);
1904 let scene = GeometryScene::new("scene", 1, vec![chunk]);
1905 let nodes = scene.nodes_with_presentation(&GeometryScenePresentation {
1906 region_annotations: vec![GeometrySceneRegionAnnotation {
1907 region_id: "loaded_face".to_string(),
1908 color: Some([0.9, 0.1, 0.1, 1.0]),
1909 role: Some("load".to_string()),
1910 label: Some("load".to_string()),
1911 direction: Some([0.0, 0.0, 1.0]),
1912 size: Some(18.0),
1913 }],
1914 ..Default::default()
1915 });
1916
1917 let marker = nodes
1918 .iter()
1919 .find(|node| node.name == "FEA region markers")
1920 .and_then(|node| node.render_data.as_ref())
1921 .expect("marker annotation node");
1922 assert_eq!(marker.pipeline_type, PipelineType::Points);
1923 assert_eq!(marker.vertices.len(), 1);
1924 assert!((marker.vertices[0].position[1] - (-1.0 / 3.0)).abs() < 1.0e-6);
1925 assert_eq!(marker.vertices[0].normal[2], 18.0);
1926
1927 let vector = nodes
1928 .iter()
1929 .find(|node| node.name == "FEA load vectors")
1930 .and_then(|node| node.render_data.as_ref())
1931 .expect("vector annotation node");
1932 assert_eq!(vector.pipeline_type, PipelineType::Lines);
1933 assert_eq!(vector.vertices.len(), 6);
1934 assert!(vector.vertices[1].position[2] > vector.vertices[0].position[2]);
1935 }
1936}