1use super::types::{ClipShape, SceneEffects, ViewportEffects};
2use super::*;
3use wgpu::util::DeviceExt;
4
5impl ViewportRenderer {
6 pub(super) fn prepare_scene_internal(
14 &mut self,
15 device: &wgpu::Device,
16 queue: &wgpu::Queue,
17 frame: &FrameData,
18 scene_fx: &SceneEffects<'_>,
19 ) {
20 if !scene_fx.compute_filter_items.is_empty() {
23 self.compute_filter_results =
24 self.resources
25 .run_compute_filters(device, queue, scene_fx.compute_filter_items);
26 } else {
27 self.compute_filter_results.clear();
28 }
29
30 self.resources.ensure_colormaps_initialized(device, queue);
32 self.resources.ensure_matcaps_initialized(device, queue);
33
34 let resources = &mut self.resources;
35 let lighting = scene_fx.lighting;
36
37 let scene_items: &[SceneRenderItem] = match &frame.scene.surfaces {
39 SurfaceSubmission::Flat(items) => items,
40 };
41
42 let (shadow_center, shadow_extent) = if let Some(extent) = lighting.shadow_extent_override {
44 (glam::Vec3::ZERO, extent)
45 } else {
46 (glam::Vec3::ZERO, 20.0)
47 };
48
49 fn compute_shadow_matrix(
51 kind: &LightKind,
52 shadow_center: glam::Vec3,
53 shadow_extent: f32,
54 ) -> glam::Mat4 {
55 match kind {
56 LightKind::Directional { direction } => {
57 let dir = glam::Vec3::from(*direction).normalize();
58 let light_up = if dir.z.abs() > 0.99 {
59 glam::Vec3::Y
60 } else {
61 glam::Vec3::Z
62 };
63 let light_pos = shadow_center + dir * shadow_extent * 2.0;
64 let light_view = glam::Mat4::look_at_rh(light_pos, shadow_center, light_up);
65 let light_proj = glam::Mat4::orthographic_rh(
66 -shadow_extent,
67 shadow_extent,
68 -shadow_extent,
69 shadow_extent,
70 0.01,
71 shadow_extent * 5.0,
72 );
73 light_proj * light_view
74 }
75 LightKind::Point { position, range } => {
76 let pos = glam::Vec3::from(*position);
77 let to_center = (shadow_center - pos).normalize();
78 let light_up = if to_center.z.abs() > 0.99 {
79 glam::Vec3::Y
80 } else {
81 glam::Vec3::Z
82 };
83 let light_view = glam::Mat4::look_at_rh(pos, shadow_center, light_up);
84 let light_proj =
85 glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, *range);
86 light_proj * light_view
87 }
88 LightKind::Spot {
89 position,
90 direction,
91 range,
92 ..
93 } => {
94 let pos = glam::Vec3::from(*position);
95 let dir = glam::Vec3::from(*direction).normalize();
96 let look_target = pos + dir;
97 let up = if dir.z.abs() > 0.99 {
98 glam::Vec3::Y
99 } else {
100 glam::Vec3::Z
101 };
102 let light_view = glam::Mat4::look_at_rh(pos, look_target, up);
103 let light_proj =
104 glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1, *range);
105 light_proj * light_view
106 }
107 }
108 }
109
110 fn build_single_light_uniform(
112 src: &LightSource,
113 shadow_center: glam::Vec3,
114 shadow_extent: f32,
115 compute_shadow: bool,
116 ) -> SingleLightUniform {
117 let shadow_mat = if compute_shadow {
118 compute_shadow_matrix(&src.kind, shadow_center, shadow_extent)
119 } else {
120 glam::Mat4::IDENTITY
121 };
122
123 match &src.kind {
124 LightKind::Directional { direction } => SingleLightUniform {
125 light_view_proj: shadow_mat.to_cols_array_2d(),
126 pos_or_dir: *direction,
127 light_type: 0,
128 color: src.color,
129 intensity: src.intensity,
130 range: 0.0,
131 inner_angle: 0.0,
132 outer_angle: 0.0,
133 _pad_align: 0,
134 spot_direction: [0.0, -1.0, 0.0],
135 _pad: [0.0; 5],
136 },
137 LightKind::Point { position, range } => SingleLightUniform {
138 light_view_proj: shadow_mat.to_cols_array_2d(),
139 pos_or_dir: *position,
140 light_type: 1,
141 color: src.color,
142 intensity: src.intensity,
143 range: *range,
144 inner_angle: 0.0,
145 outer_angle: 0.0,
146 _pad_align: 0,
147 spot_direction: [0.0, -1.0, 0.0],
148 _pad: [0.0; 5],
149 },
150 LightKind::Spot {
151 position,
152 direction,
153 range,
154 inner_angle,
155 outer_angle,
156 } => SingleLightUniform {
157 light_view_proj: shadow_mat.to_cols_array_2d(),
158 pos_or_dir: *position,
159 light_type: 2,
160 color: src.color,
161 intensity: src.intensity,
162 range: *range,
163 inner_angle: *inner_angle,
164 outer_angle: *outer_angle,
165 _pad_align: 0,
166 spot_direction: *direction,
167 _pad: [0.0; 5],
168 },
169 }
170 }
171
172 let light_count = lighting.lights.len().min(8) as u32;
174 let mut lights_arr = [SingleLightUniform {
175 light_view_proj: glam::Mat4::IDENTITY.to_cols_array_2d(),
176 pos_or_dir: [0.0; 3],
177 light_type: 0,
178 color: [1.0; 3],
179 intensity: 1.0,
180 range: 0.0,
181 inner_angle: 0.0,
182 outer_angle: 0.0,
183 _pad_align: 0,
184 spot_direction: [0.0, -1.0, 0.0],
185 _pad: [0.0; 5],
186 }; 8];
187
188 for (i, src) in lighting.lights.iter().take(8).enumerate() {
189 lights_arr[i] = build_single_light_uniform(src, shadow_center, shadow_extent, i == 0);
190 }
191
192 let cascade_count = lighting.shadow_cascade_count.clamp(1, 4) as usize;
197 let atlas_res = lighting.shadow_atlas_resolution.max(64);
198 let tile_size = atlas_res / 2;
199
200 let cascade_splits = compute_cascade_splits(
201 frame.camera.render_camera.near.max(0.01),
202 frame.camera.render_camera.far.max(1.0),
203 cascade_count as u32,
204 lighting.cascade_split_lambda,
205 );
206
207 let light_dir_for_csm = if light_count > 0 {
208 match &lighting.lights[0].kind {
209 LightKind::Directional { direction } => glam::Vec3::from(*direction).normalize(),
210 LightKind::Point { position, .. } => {
211 (glam::Vec3::from(*position) - shadow_center).normalize()
212 }
213 LightKind::Spot {
214 position,
215 direction,
216 ..
217 } => {
218 let _ = position;
219 glam::Vec3::from(*direction).normalize()
220 }
221 }
222 } else {
223 glam::Vec3::new(0.3, 1.0, 0.5).normalize()
224 };
225
226 let mut cascade_view_projs = [glam::Mat4::IDENTITY; 4];
227 let mut cascade_split_distances = [0.0f32; 4];
229
230 let use_csm = light_count > 0
232 && matches!(lighting.lights[0].kind, LightKind::Directional { .. })
233 && frame.camera.render_camera.view != glam::Mat4::IDENTITY;
234
235 if use_csm {
236 for i in 0..cascade_count {
237 let split_near = if i == 0 {
238 frame.camera.render_camera.near.max(0.01)
239 } else {
240 cascade_splits[i - 1]
241 };
242 let split_far = cascade_splits[i];
243 cascade_view_projs[i] = compute_cascade_matrix(
244 light_dir_for_csm,
245 frame.camera.render_camera.view,
246 frame.camera.render_camera.fov,
247 frame.camera.render_camera.aspect,
248 split_near,
249 split_far,
250 tile_size as f32,
251 );
252 cascade_split_distances[i] = split_far;
253 }
254 } else {
255 let primary_shadow_mat = if light_count > 0 {
257 compute_shadow_matrix(&lighting.lights[0].kind, shadow_center, shadow_extent)
258 } else {
259 glam::Mat4::IDENTITY
260 };
261 cascade_view_projs[0] = primary_shadow_mat;
262 cascade_split_distances[0] = frame.camera.render_camera.far;
263 }
264 let effective_cascade_count = if use_csm { cascade_count } else { 1 };
265
266 let atlas_rects: [[f32; 4]; 8] = [
269 [0.0, 0.0, 0.5, 0.5], [0.5, 0.0, 1.0, 0.5], [0.0, 0.5, 0.5, 1.0], [0.5, 0.5, 1.0, 1.0], [0.0; 4],
274 [0.0; 4],
275 [0.0; 4],
276 [0.0; 4], ];
278
279 {
281 let mut vp_data = [[0.0f32; 4]; 16]; for c in 0..4 {
283 let cols = cascade_view_projs[c].to_cols_array_2d();
284 for row in 0..4 {
285 vp_data[c * 4 + row] = cols[row];
286 }
287 }
288 let shadow_atlas_uniform = ShadowAtlasUniform {
289 cascade_view_proj: vp_data,
290 cascade_splits: cascade_split_distances,
291 cascade_count: effective_cascade_count as u32,
292 atlas_size: atlas_res as f32,
293 shadow_filter: match lighting.shadow_filter {
294 ShadowFilter::Pcf => 0,
295 ShadowFilter::Pcss => 1,
296 },
297 pcss_light_radius: lighting.pcss_light_radius,
298 atlas_rects,
299 };
300 queue.write_buffer(
301 &resources.shadow_info_buf,
302 0,
303 bytemuck::cast_slice(&[shadow_atlas_uniform]),
304 );
305 for slot in &self.viewport_slots {
308 queue.write_buffer(
309 &slot.shadow_info_buf,
310 0,
311 bytemuck::cast_slice(&[shadow_atlas_uniform]),
312 );
313 }
314 }
315
316 let _primary_shadow_mat = cascade_view_projs[0];
319 self.last_cascade0_shadow_mat = cascade_view_projs[0];
321
322 let (ibl_enabled, ibl_intensity, ibl_rotation, show_skybox) =
325 if let Some(env) = scene_fx.environment {
326 if resources.ibl_irradiance_view.is_some() {
327 (
328 1u32,
329 env.intensity,
330 env.rotation,
331 if env.show_skybox { 1u32 } else { 0 },
332 )
333 } else {
334 (0, 0.0, 0.0, 0)
335 }
336 } else {
337 (0, 0.0, 0.0, 0)
338 };
339
340 let lights_uniform = LightsUniform {
341 count: light_count,
342 shadow_bias: lighting.shadow_bias,
343 shadows_enabled: if lighting.shadows_enabled { 1 } else { 0 },
344 _pad: 0,
345 sky_color: lighting.sky_color,
346 hemisphere_intensity: lighting.hemisphere_intensity,
347 ground_color: lighting.ground_color,
348 _pad2: 0.0,
349 lights: lights_arr,
350 ibl_enabled,
351 ibl_intensity,
352 ibl_rotation,
353 show_skybox,
354 };
355 queue.write_buffer(
356 &resources.light_uniform_buf,
357 0,
358 bytemuck::cast_slice(&[lights_uniform]),
359 );
360
361 const SHADOW_SLOT_STRIDE: u64 = 256;
365 for c in 0..4usize {
366 queue.write_buffer(
367 &resources.shadow_uniform_buf,
368 c as u64 * SHADOW_SLOT_STRIDE,
369 bytemuck::cast_slice(&cascade_view_projs[c].to_cols_array_2d()),
370 );
371 }
372
373 let visible_count = scene_items.iter().filter(|i| i.visible).count();
376 let prev_use_instancing = self.use_instancing;
377 self.use_instancing = visible_count > INSTANCING_THRESHOLD;
378
379 if self.use_instancing != prev_use_instancing {
382 self.instanced_batches.clear();
383 self.last_scene_generation = u64::MAX;
384 self.last_scene_items_count = usize::MAX;
385 }
386
387 let has_scalar_items = scene_items.iter().any(|i| i.active_attribute.is_some());
391 let has_two_sided_items = scene_items
392 .iter()
393 .any(|i| i.material.is_two_sided());
394 let has_matcap_items = scene_items.iter().any(|i| i.material.matcap_id.is_some());
395 let has_param_vis_items = scene_items.iter().any(|i| i.material.param_vis.is_some());
396 let has_wireframe_items = scene_items.iter().any(|i| i.render_as_wireframe);
397 if !self.use_instancing
398 || frame.viewport.wireframe_mode
399 || has_scalar_items
400 || has_two_sided_items
401 || has_matcap_items
402 || has_param_vis_items
403 || has_wireframe_items
404 {
405 for item in scene_items {
406 if resources
407 .mesh_store
408 .get(item.mesh_id)
409 .is_none()
410 {
411 tracing::warn!(
412 mesh_index = item.mesh_id.index(),
413 "scene item mesh_index invalid, skipping"
414 );
415 continue;
416 };
417 let m = &item.material;
418 let (has_attr, s_min, s_max) = if let Some(attr_ref) = &item.active_attribute {
420 let range = item
421 .scalar_range
422 .or_else(|| {
423 resources
424 .mesh_store
425 .get(item.mesh_id)
426 .and_then(|mesh| mesh.attribute_ranges.get(&attr_ref.name).copied())
427 })
428 .unwrap_or((0.0, 1.0));
429 (1u32, range.0, range.1)
430 } else {
431 (0u32, 0.0, 1.0)
432 };
433 let obj_uniform = ObjectUniform {
434 model: item.model,
435 color: [m.base_color[0], m.base_color[1], m.base_color[2], m.opacity],
436 selected: if item.selected { 1 } else { 0 },
437 wireframe: if frame.viewport.wireframe_mode || item.render_as_wireframe { 1 } else { 0 },
438 ambient: m.ambient,
439 diffuse: m.diffuse,
440 specular: m.specular,
441 shininess: m.shininess,
442 has_texture: if m.texture_id.is_some() { 1 } else { 0 },
443 use_pbr: if m.use_pbr { 1 } else { 0 },
444 metallic: m.metallic,
445 roughness: m.roughness,
446 has_normal_map: if m.normal_map_id.is_some() { 1 } else { 0 },
447 has_ao_map: if m.ao_map_id.is_some() { 1 } else { 0 },
448 has_attribute: has_attr,
449 scalar_min: s_min,
450 scalar_max: s_max,
451 _pad_scalar: 0,
452 nan_color: item.nan_color.unwrap_or([0.0; 4]),
453 use_nan_color: if item.nan_color.is_some() { 1 } else { 0 },
454 use_matcap: if m.matcap_id.is_some() { 1 } else { 0 },
455 matcap_blendable: m.matcap_id.map_or(0, |id| if id.blendable { 1 } else { 0 }),
456 _pad2: 0,
457 use_face_color: u32::from(item.active_attribute.as_ref().map_or(false, |a| {
458 a.kind == crate::resources::AttributeKind::FaceColor
459 })),
460 uv_vis_mode: m.param_vis.map_or(0, |pv| pv.mode as u32),
461 uv_vis_scale: m.param_vis.map_or(8.0, |pv| pv.scale),
462 backface_policy: match m.backface_policy {
463 crate::scene::material::BackfacePolicy::Cull => 0,
464 crate::scene::material::BackfacePolicy::Identical => 1,
465 crate::scene::material::BackfacePolicy::DifferentColor(_) => 2,
466 crate::scene::material::BackfacePolicy::Tint(_) => 3,
467 crate::scene::material::BackfacePolicy::Pattern { pattern, .. } => {
468 4 + pattern as u32
469 }
470 },
471 backface_color: match m.backface_policy {
472 crate::scene::material::BackfacePolicy::DifferentColor(c) => {
473 [c[0], c[1], c[2], 1.0]
474 }
475 crate::scene::material::BackfacePolicy::Tint(factor) => {
476 [factor, 0.0, 0.0, 1.0]
477 }
478 crate::scene::material::BackfacePolicy::Pattern { color, .. } => {
479 [color[0], color[1], color[2], 1.0]
480 }
481 _ => [0.0; 4],
482 },
483 };
484
485 let normal_obj_uniform = ObjectUniform {
486 model: item.model,
487 color: [1.0, 1.0, 1.0, 1.0],
488 selected: 0,
489 wireframe: 0,
490 ambient: 0.15,
491 diffuse: 0.75,
492 specular: 0.4,
493 shininess: 32.0,
494 has_texture: 0,
495 use_pbr: 0,
496 metallic: 0.0,
497 roughness: 0.5,
498 has_normal_map: 0,
499 has_ao_map: 0,
500 has_attribute: 0,
501 scalar_min: 0.0,
502 scalar_max: 1.0,
503 _pad_scalar: 0,
504 nan_color: [0.0; 4],
505 use_nan_color: 0,
506 use_matcap: 0,
507 matcap_blendable: 0,
508 _pad2: 0,
509 use_face_color: 0,
510 uv_vis_mode: 0,
511 uv_vis_scale: 8.0,
512 backface_policy: 0,
513 backface_color: [0.0; 4],
514 };
515
516 {
518 let mesh = resources
519 .mesh_store
520 .get(item.mesh_id)
521 .unwrap();
522 queue.write_buffer(
523 &mesh.object_uniform_buf,
524 0,
525 bytemuck::cast_slice(&[obj_uniform]),
526 );
527 queue.write_buffer(
528 &mesh.normal_uniform_buf,
529 0,
530 bytemuck::cast_slice(&[normal_obj_uniform]),
531 );
532 } resources.update_mesh_texture_bind_group(
536 device,
537 item.mesh_id,
538 item.material.texture_id,
539 item.material.normal_map_id,
540 item.material.ao_map_id,
541 item.colormap_id,
542 item.active_attribute.as_ref().map(|a| a.name.as_str()),
543 item.material.matcap_id,
544 );
545 }
546 }
547
548 if self.use_instancing {
549 resources.ensure_instanced_pipelines(device);
550
551 let cache_valid = frame.scene.generation == self.last_scene_generation
556 && frame.interaction.selection_generation == self.last_selection_generation
557 && scene_items.len() == self.last_scene_items_count;
558
559 if !cache_valid {
560 let mut sorted_items: Vec<&SceneRenderItem> = scene_items
562 .iter()
563 .filter(|item| {
564 item.visible
565 && item.active_attribute.is_none()
566 && !item.material.is_two_sided()
567 && item.material.matcap_id.is_none()
568 && item.material.param_vis.is_none()
569 && resources
570 .mesh_store
571 .get(item.mesh_id)
572 .is_some()
573 })
574 .collect();
575
576 sorted_items.sort_unstable_by_key(|item| {
577 (
578 item.mesh_id.index(),
579 item.material.texture_id,
580 item.material.normal_map_id,
581 item.material.ao_map_id,
582 )
583 });
584
585 let mut all_instances: Vec<InstanceData> = Vec::with_capacity(sorted_items.len());
586 let mut instanced_batches: Vec<InstancedBatch> = Vec::new();
587
588 if !sorted_items.is_empty() {
589 let mut batch_start = 0usize;
590 for i in 1..=sorted_items.len() {
591 let at_end = i == sorted_items.len();
592 let key_changed = !at_end && {
593 let a = sorted_items[batch_start];
594 let b = sorted_items[i];
595 a.mesh_id != b.mesh_id
596 || a.material.texture_id != b.material.texture_id
597 || a.material.normal_map_id != b.material.normal_map_id
598 || a.material.ao_map_id != b.material.ao_map_id
599 };
600
601 if at_end || key_changed {
602 let batch_items = &sorted_items[batch_start..i];
603 let rep = batch_items[0];
604 let instance_offset = all_instances.len() as u32;
605 let is_transparent = rep.material.opacity < 1.0;
606
607 for item in batch_items {
608 let m = &item.material;
609 all_instances.push(InstanceData {
610 model: item.model,
611 color: [
612 m.base_color[0],
613 m.base_color[1],
614 m.base_color[2],
615 m.opacity,
616 ],
617 selected: if item.selected { 1 } else { 0 },
618 wireframe: 0, ambient: m.ambient,
620 diffuse: m.diffuse,
621 specular: m.specular,
622 shininess: m.shininess,
623 has_texture: if m.texture_id.is_some() { 1 } else { 0 },
624 use_pbr: if m.use_pbr { 1 } else { 0 },
625 metallic: m.metallic,
626 roughness: m.roughness,
627 has_normal_map: if m.normal_map_id.is_some() { 1 } else { 0 },
628 has_ao_map: if m.ao_map_id.is_some() { 1 } else { 0 },
629 });
630 }
631
632 instanced_batches.push(InstancedBatch {
633 mesh_id: rep.mesh_id,
634 texture_id: rep.material.texture_id,
635 normal_map_id: rep.material.normal_map_id,
636 ao_map_id: rep.material.ao_map_id,
637 instance_offset,
638 instance_count: batch_items.len() as u32,
639 is_transparent,
640 });
641
642 batch_start = i;
643 }
644 }
645 }
646
647 self.cached_instance_data = all_instances;
648 self.cached_instanced_batches = instanced_batches;
649
650 resources.upload_instance_data(device, queue, &self.cached_instance_data);
651
652 self.instanced_batches = self.cached_instanced_batches.clone();
653
654 self.last_scene_generation = frame.scene.generation;
655 self.last_selection_generation = frame.interaction.selection_generation;
656 self.last_scene_items_count = scene_items.len();
657
658 for batch in &self.instanced_batches {
659 resources.get_instance_bind_group(
660 device,
661 batch.texture_id,
662 batch.normal_map_id,
663 batch.ao_map_id,
664 );
665 }
666 } else {
667 for batch in &self.instanced_batches {
668 resources.get_instance_bind_group(
669 device,
670 batch.texture_id,
671 batch.normal_map_id,
672 batch.ao_map_id,
673 );
674 }
675 }
676 }
677
678 self.point_cloud_gpu_data.clear();
682 if !frame.scene.point_clouds.is_empty() {
683 resources.ensure_point_cloud_pipeline(device);
684 for item in &frame.scene.point_clouds {
685 if item.positions.is_empty() {
686 continue;
687 }
688 let gpu_data = resources.upload_point_cloud(device, queue, item);
689 self.point_cloud_gpu_data.push(gpu_data);
690 }
691 }
692
693 self.glyph_gpu_data.clear();
694 if !frame.scene.glyphs.is_empty() {
695 resources.ensure_glyph_pipeline(device);
696 for item in &frame.scene.glyphs {
697 if item.positions.is_empty() || item.vectors.is_empty() {
698 continue;
699 }
700 let gpu_data = resources.upload_glyph_set(device, queue, item);
701 self.glyph_gpu_data.push(gpu_data);
702 }
703 }
704
705 self.polyline_gpu_data.clear();
709 let vp_size = frame.camera.viewport_size;
710 if !frame.scene.polylines.is_empty() {
711 resources.ensure_polyline_pipeline(device);
712 for item in &frame.scene.polylines {
713 if item.positions.is_empty() {
714 continue;
715 }
716 let gpu_data = resources.upload_polyline(device, queue, item, vp_size);
717 self.polyline_gpu_data.push(gpu_data);
718
719 if !item.node_vectors.is_empty() {
721 resources.ensure_glyph_pipeline(device);
722 let g = crate::quantities::polyline_node_vectors_to_glyphs(item);
723 if !g.positions.is_empty() {
724 let gd = resources.upload_glyph_set(device, queue, &g);
725 self.glyph_gpu_data.push(gd);
726 }
727 }
728 if !item.edge_vectors.is_empty() {
729 resources.ensure_glyph_pipeline(device);
730 let g = crate::quantities::polyline_edge_vectors_to_glyphs(item);
731 if !g.positions.is_empty() {
732 let gd = resources.upload_glyph_set(device, queue, &g);
733 self.glyph_gpu_data.push(gd);
734 }
735 }
736 }
737 }
738
739 if !frame.scene.isolines.is_empty() {
743 resources.ensure_polyline_pipeline(device);
744 for item in &frame.scene.isolines {
745 if item.positions.is_empty() || item.indices.is_empty() || item.scalars.is_empty() {
746 continue;
747 }
748 let (positions, strip_lengths) = crate::geometry::isoline::extract_isolines(item);
749 if positions.is_empty() {
750 continue;
751 }
752 let polyline = PolylineItem {
753 positions,
754 scalars: Vec::new(),
755 strip_lengths,
756 scalar_range: None,
757 colormap_id: None,
758 default_color: item.color,
759 line_width: item.line_width,
760 id: 0,
761 ..Default::default()
762 };
763 let gpu_data = resources.upload_polyline(device, queue, &polyline, vp_size);
764 self.polyline_gpu_data.push(gpu_data);
765 }
766 }
767
768 if !frame.scene.camera_frustums.is_empty() {
772 resources.ensure_polyline_pipeline(device);
773 for item in &frame.scene.camera_frustums {
774 let polyline = item.to_polyline();
775 if !polyline.positions.is_empty() {
776 let gpu_data = resources.upload_polyline(device, queue, &polyline, vp_size);
777 self.polyline_gpu_data.push(gpu_data);
778 }
779 }
780 }
781
782 self.implicit_gpu_data.clear();
786 if !frame.scene.gpu_implicit.is_empty() {
787 resources.ensure_implicit_pipeline(device);
788 for item in &frame.scene.gpu_implicit {
789 if item.primitives.is_empty() {
790 continue;
791 }
792 let gpu = resources.upload_implicit_item(device, item);
793 self.implicit_gpu_data.push(gpu);
794 }
795 }
796
797 self.mc_gpu_data.clear();
801 if !frame.scene.gpu_mc_jobs.is_empty() {
802 resources.ensure_mc_pipelines(device);
803 self.mc_gpu_data =
804 resources.run_mc_jobs(device, queue, &frame.scene.gpu_mc_jobs);
805 }
806
807 self.screen_image_gpu_data.clear();
811 if !frame.scene.screen_images.is_empty() {
812 resources.ensure_screen_image_pipeline(device);
813 if frame.scene.screen_images.iter().any(|i| i.depth.is_some()) {
815 resources.ensure_screen_image_dc_pipeline(device);
816 }
817 let vp_w = vp_size[0];
818 let vp_h = vp_size[1];
819 for item in &frame.scene.screen_images {
820 if item.width == 0 || item.height == 0 || item.pixels.is_empty() {
821 continue;
822 }
823 let gpu = resources.upload_screen_image(device, queue, item, vp_w, vp_h);
824 self.screen_image_gpu_data.push(gpu);
825 }
826 }
827
828 self.overlay_image_gpu_data.clear();
832 if !frame.overlays.images.is_empty() {
833 resources.ensure_screen_image_pipeline(device);
834 let vp_w = vp_size[0];
835 let vp_h = vp_size[1];
836 for item in &frame.overlays.images {
837 if item.width == 0 || item.height == 0 || item.pixels.is_empty() {
838 continue;
839 }
840 let gpu = resources.upload_overlay_image(device, queue, item, vp_w, vp_h);
841 self.overlay_image_gpu_data.push(gpu);
842 }
843 }
844
845 self.streamtube_gpu_data.clear();
849 if !frame.scene.streamtube_items.is_empty() {
850 resources.ensure_streamtube_pipeline(device);
851 for item in &frame.scene.streamtube_items {
852 if item.positions.is_empty() || item.strip_lengths.is_empty() {
853 continue;
854 }
855 let gpu_data = resources.upload_streamtube(device, queue, item);
856 if gpu_data.index_count > 0 {
857 self.streamtube_gpu_data.push(gpu_data);
858 }
859 }
860 }
861
862 self.volume_gpu_data.clear();
868 if !frame.scene.volumes.is_empty() {
869 resources.ensure_volume_pipeline(device);
870 let clip_planes_for_vol: Vec<crate::renderer::types::ClipPlane> = frame
872 .effects
873 .clip_objects
874 .iter()
875 .filter(|o| o.enabled)
876 .filter_map(|o| {
877 if let ClipShape::Plane {
878 normal,
879 distance,
880 cap_color,
881 } = o.shape
882 {
883 Some(crate::renderer::types::ClipPlane {
884 normal,
885 distance,
886 enabled: true,
887 cap_color,
888 })
889 } else {
890 None
891 }
892 })
893 .collect();
894 let vol_step_multiplier =
897 if self.last_stats.missed_budget
898 && self.performance_policy.allow_volume_quality_reduction
899 {
900 2.0_f32
901 } else {
902 1.0_f32
903 };
904 for item in &frame.scene.volumes {
905 let gpu = resources.upload_volume_frame(
906 device,
907 queue,
908 item,
909 &clip_planes_for_vol,
910 vol_step_multiplier,
911 );
912 self.volume_gpu_data.push(gpu);
913 }
914 }
915
916 {
918 let total = scene_items.len() as u32;
919 let visible = scene_items.iter().filter(|i| i.visible).count() as u32;
920 let mut draw_calls = 0u32;
921 let mut triangles = 0u64;
922 let instanced_batch_count = if self.use_instancing {
923 self.instanced_batches.len() as u32
924 } else {
925 0
926 };
927
928 if self.use_instancing {
929 for batch in &self.instanced_batches {
930 if let Some(mesh) = resources
931 .mesh_store
932 .get(batch.mesh_id)
933 {
934 draw_calls += 1;
935 triangles += (mesh.index_count / 3) as u64 * batch.instance_count as u64;
936 }
937 }
938 } else {
939 for item in scene_items {
940 if !item.visible {
941 continue;
942 }
943 if let Some(mesh) = resources
944 .mesh_store
945 .get(item.mesh_id)
946 {
947 draw_calls += 1;
948 triangles += (mesh.index_count / 3) as u64;
949 }
950 }
951 }
952
953 self.last_stats = crate::renderer::stats::FrameStats {
954 total_objects: total,
955 visible_objects: visible,
956 culled_objects: total.saturating_sub(visible),
957 draw_calls,
958 instanced_batches: instanced_batch_count,
959 triangles_submitted: triangles,
960 shadow_draw_calls: 0, ..self.last_stats
962 };
963 }
964
965 let skip_shadows = self.last_stats.missed_budget
970 && self.performance_policy.allow_shadow_reduction;
971 if lighting.shadows_enabled && !scene_items.is_empty() && !skip_shadows {
972 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
973 label: Some("shadow_pass_encoder"),
974 });
975 {
976 let mut shadow_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
977 label: Some("shadow_pass"),
978 color_attachments: &[],
979 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
980 view: &resources.shadow_map_view,
981 depth_ops: Some(wgpu::Operations {
982 load: wgpu::LoadOp::Clear(1.0),
983 store: wgpu::StoreOp::Store,
984 }),
985 stencil_ops: None,
986 }),
987 timestamp_writes: None,
988 occlusion_query_set: None,
989 });
990
991 let mut shadow_draws = 0u32;
992 let tile_px = tile_size as f32;
993
994 if self.use_instancing {
995 if let (Some(pipeline), Some(instance_bg)) = (
996 &resources.shadow_instanced_pipeline,
997 self.instanced_batches.first().and_then(|b| {
998 resources.instance_bind_groups.get(&(
999 b.texture_id.unwrap_or(u64::MAX),
1000 b.normal_map_id.unwrap_or(u64::MAX),
1001 b.ao_map_id.unwrap_or(u64::MAX),
1002 ))
1003 }),
1004 ) {
1005 for cascade in 0..effective_cascade_count {
1006 let tile_col = (cascade % 2) as f32;
1007 let tile_row = (cascade / 2) as f32;
1008 shadow_pass.set_viewport(
1009 tile_col * tile_px,
1010 tile_row * tile_px,
1011 tile_px,
1012 tile_px,
1013 0.0,
1014 1.0,
1015 );
1016 shadow_pass.set_scissor_rect(
1017 (tile_col * tile_px) as u32,
1018 (tile_row * tile_px) as u32,
1019 tile_size,
1020 tile_size,
1021 );
1022
1023 shadow_pass.set_pipeline(pipeline);
1024
1025 queue.write_buffer(
1026 resources.shadow_instanced_cascade_bufs[cascade]
1027 .as_ref()
1028 .expect("shadow_instanced_cascade_bufs not allocated"),
1029 0,
1030 bytemuck::cast_slice(
1031 &cascade_view_projs[cascade].to_cols_array_2d(),
1032 ),
1033 );
1034
1035 let cascade_bg = resources.shadow_instanced_cascade_bgs[cascade]
1036 .as_ref()
1037 .expect("shadow_instanced_cascade_bgs not allocated");
1038 shadow_pass.set_bind_group(0, cascade_bg, &[]);
1039 shadow_pass.set_bind_group(1, instance_bg, &[]);
1040
1041 for batch in &self.instanced_batches {
1042 if batch.is_transparent {
1043 continue;
1044 }
1045 let Some(mesh) = resources
1046 .mesh_store
1047 .get(batch.mesh_id)
1048 else {
1049 continue;
1050 };
1051 shadow_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1052 shadow_pass.set_index_buffer(
1053 mesh.index_buffer.slice(..),
1054 wgpu::IndexFormat::Uint32,
1055 );
1056 shadow_pass.draw_indexed(
1057 0..mesh.index_count,
1058 0,
1059 batch.instance_offset
1060 ..batch.instance_offset + batch.instance_count,
1061 );
1062 shadow_draws += 1;
1063 }
1064 }
1065 }
1066 } else {
1067 for cascade in 0..effective_cascade_count {
1068 let tile_col = (cascade % 2) as f32;
1069 let tile_row = (cascade / 2) as f32;
1070 shadow_pass.set_viewport(
1071 tile_col * tile_px,
1072 tile_row * tile_px,
1073 tile_px,
1074 tile_px,
1075 0.0,
1076 1.0,
1077 );
1078 shadow_pass.set_scissor_rect(
1079 (tile_col * tile_px) as u32,
1080 (tile_row * tile_px) as u32,
1081 tile_size,
1082 tile_size,
1083 );
1084
1085 shadow_pass.set_pipeline(&resources.shadow_pipeline);
1086 shadow_pass.set_bind_group(
1087 0,
1088 &resources.shadow_bind_group,
1089 &[cascade as u32 * 256],
1090 );
1091
1092 let cascade_frustum = crate::camera::frustum::Frustum::from_view_proj(
1093 &cascade_view_projs[cascade],
1094 );
1095
1096 for item in scene_items.iter() {
1097 if !item.visible {
1098 continue;
1099 }
1100 if item.material.opacity < 1.0 {
1101 continue;
1102 }
1103 let Some(mesh) = resources
1104 .mesh_store
1105 .get(item.mesh_id)
1106 else {
1107 continue;
1108 };
1109
1110 let world_aabb = mesh
1111 .aabb
1112 .transformed(&glam::Mat4::from_cols_array_2d(&item.model));
1113 if cascade_frustum.cull_aabb(&world_aabb) {
1114 continue;
1115 }
1116
1117 shadow_pass.set_bind_group(1, &mesh.object_bind_group, &[]);
1118 shadow_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1119 shadow_pass.set_index_buffer(
1120 mesh.index_buffer.slice(..),
1121 wgpu::IndexFormat::Uint32,
1122 );
1123 shadow_pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1124 shadow_draws += 1;
1125 }
1126 }
1127 }
1128 drop(shadow_pass);
1129 self.last_stats.shadow_draw_calls = shadow_draws;
1130 }
1131 queue.submit(std::iter::once(encoder.finish()));
1132 }
1133 }
1134
1135 pub(super) fn prepare_viewport_internal(
1140 &mut self,
1141 device: &wgpu::Device,
1142 queue: &wgpu::Queue,
1143 frame: &FrameData,
1144 viewport_fx: &ViewportEffects<'_>,
1145 ) {
1146 self.ensure_viewport_slot(device, frame.camera.viewport_index);
1149
1150 let scene_items: &[SceneRenderItem] = match &frame.scene.surfaces {
1151 SurfaceSubmission::Flat(items) => items,
1152 };
1153
1154 let gp_cascade0_mat = self.last_cascade0_shadow_mat.to_cols_array_2d();
1156
1157 {
1158 let resources = &mut self.resources;
1159
1160 {
1162 let mut planes = [[0.0f32; 4]; 6];
1163 let mut count = 0u32;
1164 let mut clip_vol_uniform: ClipVolumeUniform = bytemuck::Zeroable::zeroed(); for obj in viewport_fx.clip_objects.iter().filter(|o| o.enabled) {
1167 match obj.shape {
1168 ClipShape::Plane {
1169 normal, distance, ..
1170 } if count < 6 => {
1171 planes[count as usize] = [normal[0], normal[1], normal[2], distance];
1172 count += 1;
1173 }
1174 ClipShape::Box {
1175 center,
1176 half_extents,
1177 orientation,
1178 } if clip_vol_uniform.volume_type == 0 => {
1179 clip_vol_uniform.volume_type = 2;
1180 clip_vol_uniform.box_center = center;
1181 clip_vol_uniform.box_half_extents = half_extents;
1182 clip_vol_uniform.box_col0 = orientation[0];
1183 clip_vol_uniform.box_col1 = orientation[1];
1184 clip_vol_uniform.box_col2 = orientation[2];
1185 }
1186 ClipShape::Sphere { center, radius }
1187 if clip_vol_uniform.volume_type == 0 =>
1188 {
1189 clip_vol_uniform.volume_type = 3;
1190 clip_vol_uniform.sphere_center = center;
1191 clip_vol_uniform.sphere_radius = radius;
1192 }
1193 _ => {}
1194 }
1195 }
1196
1197 let clip_uniform = ClipPlanesUniform {
1198 planes,
1199 count,
1200 _pad0: 0,
1201 viewport_width: frame.camera.viewport_size[0].max(1.0),
1202 viewport_height: frame.camera.viewport_size[1].max(1.0),
1203 };
1204 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1206 queue.write_buffer(
1207 &slot.clip_planes_buf,
1208 0,
1209 bytemuck::cast_slice(&[clip_uniform]),
1210 );
1211 queue.write_buffer(
1212 &slot.clip_volume_buf,
1213 0,
1214 bytemuck::cast_slice(&[clip_vol_uniform]),
1215 );
1216 }
1217 queue.write_buffer(
1219 &resources.clip_planes_uniform_buf,
1220 0,
1221 bytemuck::cast_slice(&[clip_uniform]),
1222 );
1223 queue.write_buffer(
1224 &resources.clip_volume_uniform_buf,
1225 0,
1226 bytemuck::cast_slice(&[clip_vol_uniform]),
1227 );
1228 }
1229
1230 let camera_uniform = frame.camera.render_camera.camera_uniform();
1232 queue.write_buffer(
1234 &resources.camera_uniform_buf,
1235 0,
1236 bytemuck::cast_slice(&[camera_uniform]),
1237 );
1238 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1240 queue.write_buffer(&slot.camera_buf, 0, bytemuck::cast_slice(&[camera_uniform]));
1241 }
1242
1243 if frame.viewport.show_grid {
1245 let eye = glam::Vec3::from(frame.camera.render_camera.eye_position);
1246 if !eye.is_finite() {
1247 tracing::warn!(
1248 eye_x = eye.x,
1249 eye_y = eye.y,
1250 eye_z = eye.z,
1251 "grid skipped: eye_position is non-finite (camera distance overflow?)"
1252 );
1253 } else {
1254 let view_proj_mat = frame.camera.render_camera.view_proj().to_cols_array_2d();
1255
1256 let (spacing, minor_fade) = if frame.viewport.grid_cell_size > 0.0 {
1257 (frame.viewport.grid_cell_size, 1.0_f32)
1258 } else {
1259 let vertical_depth = (eye.z - frame.viewport.grid_z).abs().max(1.0);
1260 let world_per_pixel =
1261 2.0 * (frame.camera.render_camera.fov / 2.0).tan() * vertical_depth
1262 / frame.camera.viewport_size[1].max(1.0);
1263 let target = (world_per_pixel * 60.0).max(1e-9_f32);
1264 let mut s = 1.0_f32;
1265 let mut iters = 0u32;
1266 while s < target {
1267 s *= 10.0;
1268 iters += 1;
1269 }
1270 let ratio = (target / s).clamp(0.0, 1.0);
1271 let fade = if ratio < 0.5 {
1272 1.0_f32
1273 } else {
1274 let t = (ratio - 0.5) * 2.0;
1275 1.0 - t * t * (3.0 - 2.0 * t)
1276 };
1277 tracing::debug!(
1278 eye_z = eye.z,
1279 vertical_depth,
1280 world_per_pixel,
1281 target,
1282 spacing = s,
1283 lod_iters = iters,
1284 ratio,
1285 minor_fade = fade,
1286 "grid LOD"
1287 );
1288 (s, fade)
1289 };
1290
1291 let spacing_major = spacing * 10.0;
1292 let snap_x = (eye.x / spacing_major).floor() * spacing_major;
1293 let snap_y = (eye.y / spacing_major).floor() * spacing_major;
1294 tracing::debug!(
1295 spacing_minor = spacing,
1296 spacing_major,
1297 snap_x,
1298 snap_y,
1299 eye_x = eye.x,
1300 eye_y = eye.y,
1301 eye_z = eye.z,
1302 "grid snap"
1303 );
1304
1305 let orient = frame.camera.render_camera.orientation;
1306 let right = orient * glam::Vec3::X;
1307 let up = orient * glam::Vec3::Y;
1308 let back = orient * glam::Vec3::Z;
1309 let cam_to_world = [
1310 [right.x, right.y, right.z, 0.0_f32],
1311 [up.x, up.y, up.z, 0.0_f32],
1312 [back.x, back.y, back.z, 0.0_f32],
1313 ];
1314 let aspect =
1315 frame.camera.viewport_size[0] / frame.camera.viewport_size[1].max(1.0);
1316 let tan_half_fov = (frame.camera.render_camera.fov / 2.0).tan();
1317
1318 let uniform = GridUniform {
1319 view_proj: view_proj_mat,
1320 cam_to_world,
1321 tan_half_fov,
1322 aspect,
1323 _pad_ivp: [0.0; 2],
1324 eye_pos: frame.camera.render_camera.eye_position,
1325 grid_z: frame.viewport.grid_z,
1326 spacing_minor: spacing,
1327 spacing_major,
1328 snap_origin: [snap_x, snap_y],
1329 color_minor: [0.35, 0.35, 0.35, 0.4 * minor_fade],
1330 color_major: [0.40, 0.40, 0.40, 0.4 + 0.2 * minor_fade],
1331 };
1332 if let Some(slot) = self.viewport_slots.get(frame.camera.viewport_index) {
1334 queue.write_buffer(&slot.grid_buf, 0, bytemuck::cast_slice(&[uniform]));
1335 }
1336 queue.write_buffer(
1338 &resources.grid_uniform_buf,
1339 0,
1340 bytemuck::cast_slice(&[uniform]),
1341 );
1342 }
1343 }
1344 {
1348 let gp = &viewport_fx.ground_plane;
1349 let mode_u32: u32 = match gp.mode {
1350 crate::renderer::types::GroundPlaneMode::None => 0,
1351 crate::renderer::types::GroundPlaneMode::ShadowOnly => 1,
1352 crate::renderer::types::GroundPlaneMode::Tile => 2,
1353 crate::renderer::types::GroundPlaneMode::SolidColor => 3,
1354 };
1355 let orient = frame.camera.render_camera.orientation;
1356 let right = orient * glam::Vec3::X;
1357 let up = orient * glam::Vec3::Y;
1358 let back = orient * glam::Vec3::Z;
1359 let aspect = frame.camera.viewport_size[0] / frame.camera.viewport_size[1].max(1.0);
1360 let tan_half_fov = (frame.camera.render_camera.fov / 2.0).tan();
1361 let vp = frame.camera.render_camera.view_proj().to_cols_array_2d();
1362 let gp_uniform = crate::resources::GroundPlaneUniform {
1363 view_proj: vp,
1364 cam_right: [right.x, right.y, right.z, 0.0],
1365 cam_up: [up.x, up.y, up.z, 0.0],
1366 cam_back: [back.x, back.y, back.z, 0.0],
1367 eye_pos: frame.camera.render_camera.eye_position,
1368 height: gp.height,
1369 color: gp.color,
1370 shadow_color: gp.shadow_color,
1371 light_vp: gp_cascade0_mat,
1372 tan_half_fov,
1373 aspect,
1374 tile_size: gp.tile_size,
1375 shadow_bias: 0.002,
1376 mode: mode_u32,
1377 shadow_opacity: gp.shadow_opacity,
1378 _pad: [0.0; 2],
1379 };
1380 queue.write_buffer(
1381 &resources.ground_plane_uniform_buf,
1382 0,
1383 bytemuck::cast_slice(&[gp_uniform]),
1384 );
1385 }
1386 } let vp_idx = frame.camera.viewport_index;
1395
1396 let mut outline_object_buffers: Vec<OutlineObjectBuffers> = Vec::new();
1398 if frame.interaction.outline_selected {
1399 let resources = &self.resources;
1400 for item in scene_items {
1401 if !item.visible || !item.selected {
1402 continue;
1403 }
1404 let uniform = OutlineUniform {
1405 model: item.model,
1406 color: [0.0; 4], pixel_offset: 0.0,
1408 _pad: [0.0; 3],
1409 };
1410 let buf = device.create_buffer(&wgpu::BufferDescriptor {
1411 label: Some("outline_mask_uniform_buf"),
1412 size: std::mem::size_of::<OutlineUniform>() as u64,
1413 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1414 mapped_at_creation: false,
1415 });
1416 queue.write_buffer(&buf, 0, bytemuck::cast_slice(&[uniform]));
1417 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1418 label: Some("outline_mask_object_bg"),
1419 layout: &resources.outline_bind_group_layout,
1420 entries: &[wgpu::BindGroupEntry {
1421 binding: 0,
1422 resource: buf.as_entire_binding(),
1423 }],
1424 });
1425 outline_object_buffers.push(OutlineObjectBuffers {
1426 mesh_id: item.mesh_id,
1427 two_sided: item.material.is_two_sided(),
1428 _mask_uniform_buf: buf,
1429 mask_bind_group: bg,
1430 });
1431 }
1432 }
1433
1434 let mut xray_object_buffers: Vec<(crate::resources::mesh_store::MeshId, wgpu::Buffer, wgpu::BindGroup)> = Vec::new();
1436 if frame.interaction.xray_selected {
1437 let resources = &self.resources;
1438 for item in scene_items {
1439 if !item.visible || !item.selected {
1440 continue;
1441 }
1442 let uniform = OutlineUniform {
1443 model: item.model,
1444 color: frame.interaction.xray_color,
1445 pixel_offset: 0.0,
1446 _pad: [0.0; 3],
1447 };
1448 let buf = device.create_buffer(&wgpu::BufferDescriptor {
1449 label: Some("xray_uniform_buf"),
1450 size: std::mem::size_of::<OutlineUniform>() as u64,
1451 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1452 mapped_at_creation: false,
1453 });
1454 queue.write_buffer(&buf, 0, bytemuck::cast_slice(&[uniform]));
1455 let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1456 label: Some("xray_object_bg"),
1457 layout: &resources.outline_bind_group_layout,
1458 entries: &[wgpu::BindGroupEntry {
1459 binding: 0,
1460 resource: buf.as_entire_binding(),
1461 }],
1462 });
1463 xray_object_buffers.push((item.mesh_id, buf, bg));
1464 }
1465 }
1466
1467 let mut constraint_line_buffers = Vec::new();
1469 for overlay in &frame.interaction.constraint_overlays {
1470 constraint_line_buffers.push(self.resources.create_constraint_overlay(device, overlay));
1471 }
1472
1473 let mut clip_plane_fill_buffers = Vec::new();
1475 let mut clip_plane_line_buffers = Vec::new();
1476 for obj in viewport_fx.clip_objects.iter().filter(|o| o.enabled) {
1477 let Some(base_color) = obj.color else {
1478 continue;
1479 };
1480 if let ClipShape::Plane {
1481 normal, distance, ..
1482 } = obj.shape
1483 {
1484 let n = glam::Vec3::from(normal);
1485 let center = n * (-distance);
1488 let active = obj.active;
1489 let hovered = obj.hovered || active;
1490
1491 let fill_color = if active {
1492 [
1493 base_color[0] * 0.5,
1494 base_color[1] * 0.5,
1495 base_color[2] * 0.5,
1496 base_color[3] * 0.5,
1497 ]
1498 } else if hovered {
1499 [
1500 base_color[0] * 0.8,
1501 base_color[1] * 0.8,
1502 base_color[2] * 0.8,
1503 base_color[3] * 0.6,
1504 ]
1505 } else {
1506 [
1507 base_color[0] * 0.5,
1508 base_color[1] * 0.5,
1509 base_color[2] * 0.5,
1510 base_color[3] * 0.3,
1511 ]
1512 };
1513 let border_color = if active {
1514 [base_color[0], base_color[1], base_color[2], 0.9]
1515 } else if hovered {
1516 [base_color[0], base_color[1], base_color[2], 0.8]
1517 } else {
1518 [
1519 base_color[0] * 0.9,
1520 base_color[1] * 0.9,
1521 base_color[2] * 0.9,
1522 0.6,
1523 ]
1524 };
1525
1526 let overlay = crate::interaction::clip_plane::ClipPlaneOverlay {
1527 center,
1528 normal: n,
1529 extent: obj.extent,
1530 fill_color,
1531 border_color,
1532 hovered,
1533 active,
1534 };
1535 clip_plane_fill_buffers.push(
1536 self.resources
1537 .create_clip_plane_fill_overlay(device, &overlay),
1538 );
1539 clip_plane_line_buffers.push(
1540 self.resources
1541 .create_clip_plane_line_overlay(device, &overlay),
1542 );
1543 } else {
1544 self.resources.ensure_polyline_pipeline(device);
1548 match obj.shape {
1549 ClipShape::Box {
1550 center,
1551 half_extents,
1552 orientation,
1553 } => {
1554 let polyline =
1555 clip_box_outline(center, half_extents, orientation, base_color);
1556 let vp_size = frame.camera.viewport_size;
1557 let gpu = self
1558 .resources
1559 .upload_polyline(device, queue, &polyline, vp_size);
1560 self.polyline_gpu_data.push(gpu);
1561 }
1562 ClipShape::Sphere { center, radius } => {
1563 let polyline = clip_sphere_outline(center, radius, base_color);
1564 let vp_size = frame.camera.viewport_size;
1565 let gpu = self
1566 .resources
1567 .upload_polyline(device, queue, &polyline, vp_size);
1568 self.polyline_gpu_data.push(gpu);
1569 }
1570 _ => {}
1571 }
1572 }
1573 }
1574
1575 let mut cap_buffers = Vec::new();
1577 if viewport_fx.cap_fill_enabled {
1578 for obj in viewport_fx.clip_objects.iter().filter(|o| o.enabled) {
1579 if let ClipShape::Plane {
1580 normal,
1581 distance,
1582 cap_color,
1583 } = obj.shape
1584 {
1585 let plane_n = glam::Vec3::from(normal);
1586 for item in scene_items.iter().filter(|i| i.visible) {
1587 let Some(mesh) = self
1588 .resources
1589 .mesh_store
1590 .get(item.mesh_id)
1591 else {
1592 continue;
1593 };
1594 let model = glam::Mat4::from_cols_array_2d(&item.model);
1595 let world_aabb = mesh.aabb.transformed(&model);
1596 if !world_aabb.intersects_plane(plane_n, distance) {
1597 continue;
1598 }
1599 let (Some(pos), Some(idx)) = (&mesh.cpu_positions, &mesh.cpu_indices)
1600 else {
1601 continue;
1602 };
1603 if let Some(cap) = crate::geometry::cap_geometry::generate_cap_mesh(
1604 pos, idx, &model, plane_n, distance,
1605 ) {
1606 let bc = item.material.base_color;
1607 let color = cap_color.unwrap_or([bc[0], bc[1], bc[2], 1.0]);
1608 let buf = self.resources.upload_cap_geometry(device, &cap, color);
1609 cap_buffers.push(buf);
1610 }
1611 }
1612 }
1613 }
1614 }
1615
1616 let axes_verts = if frame.viewport.show_axes_indicator
1618 && frame.camera.viewport_size[0] > 0.0
1619 && frame.camera.viewport_size[1] > 0.0
1620 {
1621 let verts = crate::widgets::axes_indicator::build_axes_geometry(
1622 frame.camera.viewport_size[0],
1623 frame.camera.viewport_size[1],
1624 frame.camera.render_camera.orientation,
1625 );
1626 if verts.is_empty() { None } else { Some(verts) }
1627 } else {
1628 None
1629 };
1630
1631 let gizmo_update = frame.interaction.gizmo_model.map(|model| {
1633 let (verts, indices) = crate::interaction::gizmo::build_gizmo_mesh(
1634 frame.interaction.gizmo_mode,
1635 frame.interaction.gizmo_hovered,
1636 frame.interaction.gizmo_space_orientation,
1637 );
1638 (verts, indices, model)
1639 });
1640
1641 {
1645 let slot = &mut self.viewport_slots[vp_idx];
1646 slot.outline_object_buffers = outline_object_buffers;
1647 slot.xray_object_buffers = xray_object_buffers;
1648 slot.constraint_line_buffers = constraint_line_buffers;
1649 slot.clip_plane_fill_buffers = clip_plane_fill_buffers;
1650 slot.clip_plane_line_buffers = clip_plane_line_buffers;
1651 slot.cap_buffers = cap_buffers;
1652
1653 if let Some(verts) = axes_verts {
1655 let byte_size = std::mem::size_of_val(verts.as_slice()) as u64;
1656 if byte_size > slot.axes_vertex_buffer.size() {
1657 slot.axes_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1658 label: Some("vp_axes_vertex_buf"),
1659 size: byte_size,
1660 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1661 mapped_at_creation: false,
1662 });
1663 }
1664 queue.write_buffer(&slot.axes_vertex_buffer, 0, bytemuck::cast_slice(&verts));
1665 slot.axes_vertex_count = verts.len() as u32;
1666 } else {
1667 slot.axes_vertex_count = 0;
1668 }
1669
1670 if let Some((verts, indices, model)) = gizmo_update {
1672 let vert_bytes: &[u8] = bytemuck::cast_slice(&verts);
1673 let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
1674 if vert_bytes.len() as u64 > slot.gizmo_vertex_buffer.size() {
1675 slot.gizmo_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1676 label: Some("vp_gizmo_vertex_buf"),
1677 size: vert_bytes.len() as u64,
1678 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1679 mapped_at_creation: false,
1680 });
1681 }
1682 if idx_bytes.len() as u64 > slot.gizmo_index_buffer.size() {
1683 slot.gizmo_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
1684 label: Some("vp_gizmo_index_buf"),
1685 size: idx_bytes.len() as u64,
1686 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1687 mapped_at_creation: false,
1688 });
1689 }
1690 queue.write_buffer(&slot.gizmo_vertex_buffer, 0, vert_bytes);
1691 queue.write_buffer(&slot.gizmo_index_buffer, 0, idx_bytes);
1692 slot.gizmo_index_count = indices.len() as u32;
1693 let uniform = crate::interaction::gizmo::GizmoUniform {
1694 model: model.to_cols_array_2d(),
1695 };
1696 queue.write_buffer(&slot.gizmo_uniform_buf, 0, bytemuck::cast_slice(&[uniform]));
1697 }
1698 }
1699
1700 if frame.interaction.outline_selected
1711 && !self.viewport_slots[vp_idx]
1712 .outline_object_buffers
1713 .is_empty()
1714 {
1715 let w = frame.camera.viewport_size[0] as u32;
1716 let h = frame.camera.viewport_size[1] as u32;
1717
1718 self.ensure_viewport_hdr(
1720 device,
1721 queue,
1722 vp_idx,
1723 w.max(1),
1724 h.max(1),
1725 frame.effects.post_process.ssaa_factor.max(1),
1726 );
1727
1728 {
1730 let slot_hdr = self.viewport_slots[vp_idx].hdr.as_ref().unwrap();
1731 let edge_uniform = OutlineEdgeUniform {
1732 color: frame.interaction.outline_color,
1733 radius: frame.interaction.outline_width_px,
1734 viewport_w: w as f32,
1735 viewport_h: h as f32,
1736 _pad: 0.0,
1737 };
1738 queue.write_buffer(
1739 &slot_hdr.outline_edge_uniform_buf,
1740 0,
1741 bytemuck::cast_slice(&[edge_uniform]),
1742 );
1743 }
1744
1745 let slot_ref = &self.viewport_slots[vp_idx];
1748 let outlines_ptr =
1749 &slot_ref.outline_object_buffers as *const Vec<OutlineObjectBuffers>;
1750 let camera_bg_ptr = &slot_ref.camera_bind_group as *const wgpu::BindGroup;
1751 let slot_hdr = slot_ref.hdr.as_ref().unwrap();
1752 let mask_view_ptr = &slot_hdr.outline_mask_view as *const wgpu::TextureView;
1753 let color_view_ptr = &slot_hdr.outline_color_view as *const wgpu::TextureView;
1754 let depth_view_ptr = &slot_hdr.outline_depth_view as *const wgpu::TextureView;
1755 let edge_bg_ptr = &slot_hdr.outline_edge_bind_group as *const wgpu::BindGroup;
1756 let (outlines, camera_bg, mask_view, color_view, depth_view, edge_bg) = unsafe {
1759 (
1760 &*outlines_ptr,
1761 &*camera_bg_ptr,
1762 &*mask_view_ptr,
1763 &*color_view_ptr,
1764 &*depth_view_ptr,
1765 &*edge_bg_ptr,
1766 )
1767 };
1768
1769 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1770 label: Some("outline_offscreen_encoder"),
1771 });
1772
1773 {
1775 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1776 label: Some("outline_mask_pass"),
1777 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1778 view: mask_view,
1779 resolve_target: None,
1780 ops: wgpu::Operations {
1781 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1782 store: wgpu::StoreOp::Store,
1783 },
1784 depth_slice: None,
1785 })],
1786 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1787 view: depth_view,
1788 depth_ops: Some(wgpu::Operations {
1789 load: wgpu::LoadOp::Clear(1.0),
1790 store: wgpu::StoreOp::Discard,
1791 }),
1792 stencil_ops: None,
1793 }),
1794 timestamp_writes: None,
1795 occlusion_query_set: None,
1796 });
1797
1798 pass.set_bind_group(0, camera_bg, &[]);
1799 for outlined in outlines {
1800 let Some(mesh) = self
1801 .resources
1802 .mesh_store
1803 .get(outlined.mesh_id)
1804 else {
1805 continue;
1806 };
1807 let pipeline = if outlined.two_sided {
1808 &self.resources.outline_mask_two_sided_pipeline
1809 } else {
1810 &self.resources.outline_mask_pipeline
1811 };
1812 pass.set_pipeline(pipeline);
1813 pass.set_bind_group(1, &outlined.mask_bind_group, &[]);
1814 pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
1815 pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
1816 pass.draw_indexed(0..mesh.index_count, 0, 0..1);
1817 }
1818 }
1819
1820 {
1822 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1823 label: Some("outline_edge_pass"),
1824 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1825 view: color_view,
1826 resolve_target: None,
1827 ops: wgpu::Operations {
1828 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1829 store: wgpu::StoreOp::Store,
1830 },
1831 depth_slice: None,
1832 })],
1833 depth_stencil_attachment: None,
1834 timestamp_writes: None,
1835 occlusion_query_set: None,
1836 });
1837 pass.set_pipeline(&self.resources.outline_edge_pipeline);
1838 pass.set_bind_group(0, edge_bg, &[]);
1839 pass.draw(0..3, 0..1);
1840 }
1841
1842 queue.submit(std::iter::once(encoder.finish()));
1843 }
1844
1845 {
1850 let w = frame.camera.viewport_size[0];
1851 let h = frame.camera.viewport_size[1];
1852 if let Some(sel_ref) = &frame.interaction.sub_selection {
1853 let needs_rebuild = {
1854 let slot = &self.viewport_slots[vp_idx];
1855 slot.sub_highlight_generation != sel_ref.version
1856 || slot.sub_highlight.is_none()
1857 };
1858 if needs_rebuild {
1859 self.resources.ensure_sub_highlight_pipelines(device);
1860 let data = self.resources.build_sub_highlight(
1861 device,
1862 queue,
1863 sel_ref,
1864 frame.interaction.sub_highlight_face_fill_color,
1865 frame.interaction.sub_highlight_edge_color,
1866 frame.interaction.sub_highlight_edge_width_px,
1867 frame.interaction.sub_highlight_vertex_size_px,
1868 w,
1869 h,
1870 );
1871 let slot = &mut self.viewport_slots[vp_idx];
1872 slot.sub_highlight = Some(data);
1873 slot.sub_highlight_generation = sel_ref.version;
1874 }
1875 } else {
1876 let slot = &mut self.viewport_slots[vp_idx];
1877 slot.sub_highlight = None;
1878 slot.sub_highlight_generation = u64::MAX;
1879 }
1880 }
1881
1882 self.label_gpu_data = None;
1886 if !frame.overlays.labels.is_empty() {
1887 self.resources.ensure_overlay_text_pipeline(device);
1888 let vp_w = frame.camera.viewport_size[0];
1889 let vp_h = frame.camera.viewport_size[1];
1890 if vp_w > 0.0 && vp_h > 0.0 {
1891 let view = &frame.camera.render_camera.view;
1892 let proj = &frame.camera.render_camera.projection;
1893
1894 let mut sorted_labels: Vec<&crate::renderer::types::LabelItem> =
1896 frame.overlays.labels.iter().collect();
1897 sorted_labels.sort_by_key(|l| l.z_order);
1898
1899 let mut verts: Vec<crate::resources::OverlayTextVertex> = Vec::new();
1900
1901 for label in &sorted_labels {
1902 if label.text.is_empty() || label.opacity <= 0.0 {
1903 continue;
1904 }
1905
1906 let screen_pos = if let Some(sa) = label.screen_anchor {
1908 Some(sa)
1909 } else if let Some(wa) = label.world_anchor {
1910 project_to_screen(wa, view, proj, vp_w, vp_h)
1911 } else {
1912 continue;
1913 };
1914 let Some(anchor_px) = screen_pos else {
1915 continue;
1916 };
1917
1918 let opacity = label.opacity.clamp(0.0, 1.0);
1919
1920 let layout = if let Some(max_w) = label.max_width {
1922 self.resources.glyph_atlas.layout_text_wrapped(
1923 &label.text,
1924 label.font_size,
1925 label.font,
1926 max_w,
1927 device,
1928 )
1929 } else {
1930 self.resources.glyph_atlas.layout_text(
1931 &label.text,
1932 label.font_size,
1933 label.font,
1934 device,
1935 )
1936 };
1937
1938 let font_index = label.font.map_or(0, |h| h.0);
1940 let ascent = self.resources.glyph_atlas.font_ascent(font_index, label.font_size);
1941
1942 let align_offset = match label.anchor_align {
1944 crate::renderer::types::LabelAnchor::Leading => 6.0,
1945 crate::renderer::types::LabelAnchor::Center => -layout.total_width * 0.5,
1946 crate::renderer::types::LabelAnchor::Trailing => -layout.total_width - 6.0,
1947 };
1948
1949 let text_x = anchor_px[0] + align_offset + label.offset[0];
1951 let text_y = anchor_px[1] - layout.height * 0.5 + label.offset[1];
1952
1953 if label.background {
1955 let pad = label.padding;
1956 let bx0 = text_x - pad;
1957 let by0 = text_y - pad;
1958 let bx1 = text_x + layout.total_width + pad;
1959 let by1 = text_y + layout.height + pad;
1960 let bg_color = apply_opacity(label.background_color, opacity);
1961 if label.border_radius > 0.0 {
1962 emit_rounded_quad(
1963 &mut verts,
1964 bx0, by0, bx1, by1,
1965 label.border_radius,
1966 bg_color,
1967 vp_w, vp_h,
1968 );
1969 } else {
1970 emit_solid_quad(
1971 &mut verts,
1972 bx0, by0, bx1, by1,
1973 bg_color,
1974 vp_w, vp_h,
1975 );
1976 }
1977 }
1978
1979 if label.leader_line {
1981 if let Some(wa) = label.world_anchor {
1982 let world_px = project_to_screen(wa, view, proj, vp_w, vp_h);
1983 if let Some(wp) = world_px {
1984 emit_line_quad(
1985 &mut verts,
1986 wp[0], wp[1],
1987 text_x, text_y + layout.height * 0.5,
1988 1.5,
1989 apply_opacity(label.leader_color, opacity),
1990 vp_w, vp_h,
1991 );
1992 }
1993 }
1994 }
1995
1996 let text_color = apply_opacity(label.color, opacity);
1998 for gq in &layout.quads {
1999 let gx = text_x + gq.pos[0];
2000 let gy = text_y + ascent + gq.pos[1];
2001 emit_textured_quad(
2002 &mut verts,
2003 gx, gy,
2004 gx + gq.size[0], gy + gq.size[1],
2005 gq.uv_min, gq.uv_max,
2006 text_color,
2007 vp_w, vp_h,
2008 );
2009 }
2010 }
2011
2012 self.resources.glyph_atlas.upload_if_dirty(queue);
2014
2015 if !verts.is_empty() {
2016 let vertex_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2017 label: Some("overlay_label_vbuf"),
2018 contents: bytemuck::cast_slice(&verts),
2019 usage: wgpu::BufferUsages::VERTEX,
2020 });
2021 let bgl = self.resources.overlay_text_bgl.as_ref().unwrap();
2022 let sampler = self.resources.overlay_text_sampler.as_ref().unwrap();
2023 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2024 label: Some("overlay_label_bg"),
2025 layout: bgl,
2026 entries: &[
2027 wgpu::BindGroupEntry {
2028 binding: 0,
2029 resource: wgpu::BindingResource::TextureView(
2030 &self.resources.glyph_atlas.view,
2031 ),
2032 },
2033 wgpu::BindGroupEntry {
2034 binding: 1,
2035 resource: wgpu::BindingResource::Sampler(sampler),
2036 },
2037 ],
2038 });
2039 self.label_gpu_data = Some(crate::resources::LabelGpuData {
2040 vertex_buf,
2041 vertex_count: verts.len() as u32,
2042 bind_group,
2043 });
2044 }
2045 }
2046 }
2047
2048 self.scalar_bar_gpu_data = None;
2052 if !frame.overlays.scalar_bars.is_empty() {
2053 self.resources.ensure_overlay_text_pipeline(device);
2054 let vp_w = frame.camera.viewport_size[0];
2055 let vp_h = frame.camera.viewport_size[1];
2056 if vp_w > 0.0 && vp_h > 0.0 {
2057 let mut verts: Vec<crate::resources::OverlayTextVertex> = Vec::new();
2058
2059 for bar in &frame.overlays.scalar_bars {
2060 let Some(lut) = self.resources.get_colormap_rgba(bar.colormap_id).map(|l| l.to_vec()) else {
2063 continue;
2064 };
2065
2066 let is_vertical = matches!(
2067 bar.orientation,
2068 crate::renderer::types::ScalarBarOrientation::Vertical
2069 );
2070 let reversed = bar.ticks_reversed;
2071
2072 let tick_fs = bar.font_size;
2074 let title_fs = bar.title_font_size.unwrap_or(bar.font_size);
2075 let font_index = bar.font.map_or(0, |h| h.0);
2076
2077 let (strip_w, strip_h) = if is_vertical {
2079 (bar.bar_width_px, bar.bar_length_px)
2080 } else {
2081 (bar.bar_length_px, bar.bar_width_px)
2082 };
2083
2084 let tick_count = bar.tick_count.max(2);
2087 let mut tick_data: Vec<(String, f32, f32)> = Vec::new(); let mut max_tick_w = 0.0f32;
2089 let mut tick_h = 0.0f32;
2090 for i in 0..tick_count {
2091 let t = i as f32 / (tick_count - 1) as f32;
2092 let value = bar.scalar_min + t * (bar.scalar_max - bar.scalar_min);
2093 let text = format!("{value:.2}");
2094 let layout = self.resources.glyph_atlas.layout_text(
2095 &text, tick_fs, bar.font, device,
2096 );
2097 max_tick_w = max_tick_w.max(layout.total_width);
2098 tick_h = layout.height;
2099 tick_data.push((text, layout.total_width, layout.height));
2100 }
2101
2102 let half_tick = tick_h / 2.0;
2107 let title_h = if bar.title.is_some() {
2108 title_fs + 4.0 + half_tick
2110 } else {
2111 half_tick
2113 };
2114
2115 let title_w = if let Some(ref t) = bar.title {
2118 self.resources.glyph_atlas.layout_text(t, title_fs, bar.font, device).total_width
2119 } else {
2120 0.0
2121 };
2122
2123 let bg_pad = 4.0;
2129 let (inset_left, inset_right) = if is_vertical {
2130 let title_oh = ((title_w - strip_w) / 2.0).max(0.0);
2131 let right_extent = 4.0 + max_tick_w + bg_pad; (title_oh + bg_pad, right_extent)
2133 } else {
2134 let title_oh = ((title_w - strip_w) / 2.0).max(0.0);
2135 let tick_oh = max_tick_w / 2.0;
2136 let side = title_oh.max(tick_oh) + bg_pad;
2137 (side, side)
2138 };
2139
2140 let bottom_overhang = if is_vertical { half_tick } else { 3.0 + tick_h };
2145
2146 let (bar_x, bar_y) = match bar.anchor {
2152 crate::renderer::types::ScalarBarAnchor::TopLeft => (
2153 bar.margin_px + inset_left,
2154 bar.margin_px + title_h + bg_pad,
2155 ),
2156 crate::renderer::types::ScalarBarAnchor::TopRight => (
2157 vp_w - bar.margin_px - strip_w - inset_right,
2158 bar.margin_px + title_h + bg_pad,
2159 ),
2160 crate::renderer::types::ScalarBarAnchor::BottomLeft => (
2161 bar.margin_px + inset_left,
2162 vp_h - bar.margin_px - strip_h - bottom_overhang - bg_pad,
2163 ),
2164 crate::renderer::types::ScalarBarAnchor::BottomRight => (
2165 vp_w - bar.margin_px - strip_w - inset_right,
2166 vp_h - bar.margin_px - strip_h - bottom_overhang - bg_pad,
2167 ),
2168 };
2169
2170 let (bg_x0, bg_y0, bg_x1, bg_y1) = if is_vertical {
2172 let title_right = bar_x + (strip_w + title_w) / 2.0;
2173 let ticks_right = bar_x + strip_w + 4.0 + max_tick_w;
2174 (
2175 bar_x - bg_pad - ((title_w - strip_w) / 2.0).max(0.0),
2176 bar_y - title_h - bg_pad,
2177 ticks_right.max(title_right) + bg_pad,
2178 bar_y + strip_h + half_tick + bg_pad,
2179 )
2180 } else {
2181 let title_overhang = ((title_w - strip_w) / 2.0).max(0.0);
2182 let tick_overhang = max_tick_w / 2.0;
2183 let side_pad = title_overhang.max(tick_overhang);
2184 let bottom = bar_y + strip_h + 3.0 + tick_h + bg_pad;
2185 (
2186 bar_x - bg_pad - side_pad,
2187 bar_y - title_h - bg_pad,
2188 bar_x + strip_w + bg_pad + side_pad,
2189 bottom,
2190 )
2191 };
2192 emit_rounded_quad(
2193 &mut verts,
2194 bg_x0, bg_y0, bg_x1, bg_y1,
2195 3.0,
2196 bar.background_color,
2197 vp_w, vp_h,
2198 );
2199
2200 let steps: usize = 64;
2202 for s in 0..steps {
2203 let (qx0, qy0, qx1, qy1, t) = if is_vertical {
2204 let t = if reversed {
2206 s as f32 / (steps - 1) as f32
2207 } else {
2208 1.0 - s as f32 / (steps - 1) as f32
2209 };
2210 let step_h = strip_h / steps as f32;
2211 let sy = bar_y + s as f32 * step_h;
2212 (bar_x, sy, bar_x + strip_w, sy + step_h + 0.5, t)
2213 } else {
2214 let t = if reversed {
2216 1.0 - s as f32 / (steps - 1) as f32
2217 } else {
2218 s as f32 / (steps - 1) as f32
2219 };
2220 let step_w = strip_w / steps as f32;
2221 let sx = bar_x + s as f32 * step_w;
2222 (sx, bar_y, sx + step_w + 0.5, bar_y + strip_h, t)
2223 };
2224 let lut_idx = (t * 255.0).clamp(0.0, 255.0) as usize;
2225 let [r, g, b, a] = lut[lut_idx];
2226 let color = [
2227 r as f32 / 255.0,
2228 g as f32 / 255.0,
2229 b as f32 / 255.0,
2230 a as f32 / 255.0,
2231 ];
2232 emit_solid_quad(&mut verts, qx0, qy0, qx1, qy1, color, vp_w, vp_h);
2233 }
2234
2235 let ascent = self.resources.glyph_atlas.font_ascent(font_index, tick_fs);
2237 for (i, (text, tw, th)) in tick_data.iter().enumerate() {
2238 let t = i as f32 / (tick_count - 1) as f32;
2239 let layout = self.resources.glyph_atlas.layout_text(
2240 text, tick_fs, bar.font, device,
2241 );
2242
2243 let (lx, ly) = if is_vertical {
2244 let progress = if reversed { t } else { 1.0 - t };
2249 let tick_y = bar_y + progress * strip_h;
2250 (bar_x + strip_w + 4.0, tick_y - th * 0.5)
2251 } else {
2252 let frac = if reversed { 1.0 - t } else { t };
2256 let tick_x = bar_x + frac * strip_w;
2257 (tick_x - tw * 0.5, bar_y + strip_h + 3.0)
2258 };
2259 let _ = (tw, th); for gq in &layout.quads {
2262 let gx = lx + gq.pos[0];
2263 let gy = ly + ascent + gq.pos[1];
2264 emit_textured_quad(
2265 &mut verts,
2266 gx, gy,
2267 gx + gq.size[0], gy + gq.size[1],
2268 gq.uv_min, gq.uv_max,
2269 bar.label_color,
2270 vp_w, vp_h,
2271 );
2272 }
2273 }
2274
2275 if let Some(ref title_text) = bar.title {
2277 let layout = self.resources.glyph_atlas.layout_text(
2278 title_text, title_fs, bar.font, device,
2279 );
2280 let title_ascent = self.resources.glyph_atlas.font_ascent(font_index, title_fs);
2281 let tx = bar_x + (strip_w - layout.total_width) * 0.5;
2283 let ty = bar_y - title_h;
2284 for gq in &layout.quads {
2285 let gx = tx + gq.pos[0];
2286 let gy = ty + title_ascent + gq.pos[1];
2287 emit_textured_quad(
2288 &mut verts,
2289 gx, gy,
2290 gx + gq.size[0], gy + gq.size[1],
2291 gq.uv_min, gq.uv_max,
2292 bar.label_color,
2293 vp_w, vp_h,
2294 );
2295 }
2296 }
2297 }
2298
2299 self.resources.glyph_atlas.upload_if_dirty(queue);
2301
2302 if !verts.is_empty() {
2303 let vertex_buf =
2304 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2305 label: Some("overlay_scalar_bar_vbuf"),
2306 contents: bytemuck::cast_slice(&verts),
2307 usage: wgpu::BufferUsages::VERTEX,
2308 });
2309 let bgl = self.resources.overlay_text_bgl.as_ref().unwrap();
2310 let sampler = self.resources.overlay_text_sampler.as_ref().unwrap();
2311 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2312 label: Some("overlay_scalar_bar_bg"),
2313 layout: bgl,
2314 entries: &[
2315 wgpu::BindGroupEntry {
2316 binding: 0,
2317 resource: wgpu::BindingResource::TextureView(
2318 &self.resources.glyph_atlas.view,
2319 ),
2320 },
2321 wgpu::BindGroupEntry {
2322 binding: 1,
2323 resource: wgpu::BindingResource::Sampler(sampler),
2324 },
2325 ],
2326 });
2327 self.scalar_bar_gpu_data = Some(crate::resources::LabelGpuData {
2328 vertex_buf,
2329 vertex_count: verts.len() as u32,
2330 bind_group,
2331 });
2332 }
2333 }
2334 }
2335
2336 self.ruler_gpu_data = None;
2340 if !frame.overlays.rulers.is_empty() {
2341 self.resources.ensure_overlay_text_pipeline(device);
2342 let vp_w = frame.camera.viewport_size[0];
2343 let vp_h = frame.camera.viewport_size[1];
2344 if vp_w > 0.0 && vp_h > 0.0 {
2345 let view = &frame.camera.render_camera.view;
2346 let proj = &frame.camera.render_camera.projection;
2347
2348 let mut verts: Vec<crate::resources::OverlayTextVertex> = Vec::new();
2349
2350 for ruler in &frame.overlays.rulers {
2351 let start_ndc = project_to_ndc(ruler.start, view, proj);
2353 let end_ndc = project_to_ndc(ruler.end, view, proj);
2354
2355 let (Some(sndc), Some(endc)) = (start_ndc, end_ndc) else { continue };
2357
2358 let Some((csndc, cendc)) = clip_line_ndc(sndc, endc) else { continue };
2361
2362 let [sx, sy] = ndc_to_screen_px(csndc, vp_w, vp_h);
2363 let [ex, ey] = ndc_to_screen_px(cendc, vp_w, vp_h);
2364
2365 let start_on_screen = ndc_in_viewport(sndc);
2367 let end_on_screen = ndc_in_viewport(endc);
2368
2369 emit_line_quad(
2371 &mut verts,
2372 sx, sy, ex, ey,
2373 ruler.line_width_px,
2374 ruler.color,
2375 vp_w, vp_h,
2376 );
2377
2378 if ruler.end_caps {
2380 let dx = ex - sx;
2381 let dy = ey - sy;
2382 let len = (dx * dx + dy * dy).sqrt().max(0.001);
2383 let cap_half = 5.0;
2384 let px = -dy / len * cap_half;
2385 let py = dx / len * cap_half;
2386
2387 if start_on_screen {
2388 emit_line_quad(
2389 &mut verts,
2390 sx - px, sy - py,
2391 sx + px, sy + py,
2392 ruler.line_width_px,
2393 ruler.color,
2394 vp_w, vp_h,
2395 );
2396 }
2397 if end_on_screen {
2398 emit_line_quad(
2399 &mut verts,
2400 ex - px, ey - py,
2401 ex + px, ey + py,
2402 ruler.line_width_px,
2403 ruler.color,
2404 vp_w, vp_h,
2405 );
2406 }
2407 }
2408
2409 let start_world = glam::Vec3::from(ruler.start);
2412 let end_world = glam::Vec3::from(ruler.end);
2413 let distance = (end_world - start_world).length();
2414 let text = format_ruler_distance(distance, ruler.label_format.as_deref());
2415
2416 let mid_x = (sx + ex) * 0.5;
2417 let mid_y = (sy + ey) * 0.5;
2418
2419 let layout = self.resources.glyph_atlas.layout_text(
2420 &text,
2421 ruler.font_size,
2422 ruler.font,
2423 device,
2424 );
2425 let font_index = ruler.font.map_or(0, |h| h.0);
2426 let ascent = self.resources.glyph_atlas.font_ascent(font_index, ruler.font_size);
2427
2428 let lx = mid_x - layout.total_width * 0.5;
2430 let ly = mid_y - layout.height - 6.0;
2431
2432 let pad = 3.0;
2434 emit_solid_quad(
2435 &mut verts,
2436 lx - pad, ly - pad,
2437 lx + layout.total_width + pad, ly + layout.height + pad,
2438 [0.0, 0.0, 0.0, 0.55],
2439 vp_w, vp_h,
2440 );
2441
2442 for gq in &layout.quads {
2444 let gx = lx + gq.pos[0];
2445 let gy = ly + ascent + gq.pos[1];
2446 emit_textured_quad(
2447 &mut verts,
2448 gx, gy,
2449 gx + gq.size[0], gy + gq.size[1],
2450 gq.uv_min, gq.uv_max,
2451 ruler.label_color,
2452 vp_w, vp_h,
2453 );
2454 }
2455 }
2456
2457 self.resources.glyph_atlas.upload_if_dirty(queue);
2459
2460 if !verts.is_empty() {
2461 let vertex_buf =
2462 device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2463 label: Some("overlay_ruler_vbuf"),
2464 contents: bytemuck::cast_slice(&verts),
2465 usage: wgpu::BufferUsages::VERTEX,
2466 });
2467 let bgl = self.resources.overlay_text_bgl.as_ref().unwrap();
2468 let sampler = self.resources.overlay_text_sampler.as_ref().unwrap();
2469 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2470 label: Some("overlay_ruler_bg"),
2471 layout: bgl,
2472 entries: &[
2473 wgpu::BindGroupEntry {
2474 binding: 0,
2475 resource: wgpu::BindingResource::TextureView(
2476 &self.resources.glyph_atlas.view,
2477 ),
2478 },
2479 wgpu::BindGroupEntry {
2480 binding: 1,
2481 resource: wgpu::BindingResource::Sampler(sampler),
2482 },
2483 ],
2484 });
2485 self.ruler_gpu_data = Some(crate::resources::LabelGpuData {
2486 vertex_buf,
2487 vertex_count: verts.len() as u32,
2488 bind_group,
2489 });
2490 }
2491 }
2492 }
2493 }
2494
2495 pub fn prepare(
2500 &mut self,
2501 device: &wgpu::Device,
2502 queue: &wgpu::Queue,
2503 frame: &FrameData,
2504 ) -> crate::renderer::stats::FrameStats {
2505 let prepare_start = std::time::Instant::now();
2506
2507 if self.ts_needs_readback {
2511 if let Some(ref stg_buf) = self.ts_staging_buf {
2512 let (tx, rx) = std::sync::mpsc::channel::<Result<(), wgpu::BufferAsyncError>>();
2513 stg_buf.slice(..).map_async(wgpu::MapMode::Read, move |r| {
2514 let _ = tx.send(r);
2515 });
2516 device
2519 .poll(wgpu::PollType::Wait {
2520 submission_index: None,
2521 timeout: Some(std::time::Duration::from_millis(100)),
2522 })
2523 .ok();
2524 if rx.try_recv().unwrap_or(Err(wgpu::BufferAsyncError)).is_ok() {
2525 let data = stg_buf.slice(..).get_mapped_range();
2526 let t0 = u64::from_le_bytes(data[0..8].try_into().unwrap());
2527 let t1 = u64::from_le_bytes(data[8..16].try_into().unwrap());
2528 drop(data);
2529 let gpu_ms = t1.saturating_sub(t0) as f32 * self.ts_period / 1_000_000.0;
2531 self.last_stats.gpu_frame_ms = Some(gpu_ms);
2532 }
2533 stg_buf.unmap();
2534 }
2535 self.ts_needs_readback = false;
2536 }
2537
2538 let total_frame_ms = self
2540 .last_prepare_instant
2541 .map(|t| t.elapsed().as_secs_f32() * 1000.0)
2542 .unwrap_or(0.0);
2543
2544 let upload_bytes = self.resources.frame_upload_bytes;
2546 self.resources.frame_upload_bytes = 0;
2547
2548 let (scene_fx, viewport_fx) = frame.effects.split();
2549 self.prepare_scene_internal(device, queue, frame, &scene_fx);
2550 self.prepare_viewport_internal(device, queue, frame, &viewport_fx);
2551
2552 let cpu_prepare_ms = prepare_start.elapsed().as_secs_f32() * 1000.0;
2553
2554 let policy = self.performance_policy;
2555 let budget_ms = policy.target_fps.map(|fps| 1000.0 / fps);
2556 let missed_budget = budget_ms
2557 .map(|b| total_frame_ms > b)
2558 .unwrap_or(false);
2559
2560 if policy.allow_dynamic_resolution {
2564 if let Some(budget) = budget_ms {
2565 if total_frame_ms > budget {
2566 self.current_render_scale =
2568 (self.current_render_scale - 0.1).max(policy.min_render_scale);
2569 } else if total_frame_ms < budget * 0.8 {
2570 self.current_render_scale =
2572 (self.current_render_scale + 0.05).min(policy.max_render_scale);
2573 }
2574 }
2575 }
2576
2577 self.last_prepare_instant = Some(prepare_start);
2578 self.frame_counter = self.frame_counter.wrapping_add(1);
2579
2580 let stats = crate::renderer::stats::FrameStats {
2581 cpu_prepare_ms,
2582 gpu_frame_ms: self.last_stats.gpu_frame_ms,
2585 total_frame_ms,
2586 render_scale: self.current_render_scale,
2587 missed_budget,
2588 upload_bytes,
2589 ..self.last_stats
2590 };
2591 self.last_stats = stats;
2592 stats
2593 }
2594}
2595
2596fn clip_box_outline(
2602 center: [f32; 3],
2603 half: [f32; 3],
2604 orientation: [[f32; 3]; 3],
2605 color: [f32; 4],
2606) -> PolylineItem {
2607 let ax = glam::Vec3::from(orientation[0]) * half[0];
2608 let ay = glam::Vec3::from(orientation[1]) * half[1];
2609 let az = glam::Vec3::from(orientation[2]) * half[2];
2610 let c = glam::Vec3::from(center);
2611
2612 let corners = [
2613 c - ax - ay - az,
2614 c + ax - ay - az,
2615 c + ax + ay - az,
2616 c - ax + ay - az,
2617 c - ax - ay + az,
2618 c + ax - ay + az,
2619 c + ax + ay + az,
2620 c - ax + ay + az,
2621 ];
2622 let edges: [(usize, usize); 12] = [
2623 (0, 1),
2624 (1, 2),
2625 (2, 3),
2626 (3, 0), (4, 5),
2628 (5, 6),
2629 (6, 7),
2630 (7, 4), (0, 4),
2632 (1, 5),
2633 (2, 6),
2634 (3, 7), ];
2636
2637 let mut positions = Vec::with_capacity(24);
2638 let mut strip_lengths = Vec::with_capacity(12);
2639 for (a, b) in edges {
2640 positions.push(corners[a].to_array());
2641 positions.push(corners[b].to_array());
2642 strip_lengths.push(2u32);
2643 }
2644
2645 let mut item = PolylineItem::default();
2646 item.positions = positions;
2647 item.strip_lengths = strip_lengths;
2648 item.default_color = color;
2649 item.line_width = 2.0;
2650 item
2651}
2652
2653fn clip_sphere_outline(center: [f32; 3], radius: f32, color: [f32; 4]) -> PolylineItem {
2655 let c = glam::Vec3::from(center);
2656 let segs = 64usize;
2657 let mut positions = Vec::with_capacity((segs + 1) * 3);
2658 let mut strip_lengths = Vec::with_capacity(3);
2659
2660 for axis in 0..3usize {
2661 let start = positions.len();
2662 for i in 0..=segs {
2663 let t = i as f32 / segs as f32 * std::f32::consts::TAU;
2664 let (s, cs) = t.sin_cos();
2665 let p = c + match axis {
2666 0 => glam::Vec3::new(cs * radius, s * radius, 0.0),
2667 1 => glam::Vec3::new(cs * radius, 0.0, s * radius),
2668 _ => glam::Vec3::new(0.0, cs * radius, s * radius),
2669 };
2670 positions.push(p.to_array());
2671 }
2672 strip_lengths.push((positions.len() - start) as u32);
2673 }
2674
2675 let mut item = PolylineItem::default();
2676 item.positions = positions;
2677 item.strip_lengths = strip_lengths;
2678 item.default_color = color;
2679 item.line_width = 2.0;
2680 item
2681}
2682
2683fn project_to_ndc(
2691 pos: [f32; 3],
2692 view: &glam::Mat4,
2693 proj: &glam::Mat4,
2694) -> Option<[f32; 2]> {
2695 let clip = *proj * *view * glam::Vec3::from(pos).extend(1.0);
2696 if clip.w <= 0.0 { return None; }
2697 Some([clip.x / clip.w, clip.y / clip.w])
2698}
2699
2700fn ndc_to_screen_px(ndc: [f32; 2], vp_w: f32, vp_h: f32) -> [f32; 2] {
2702 [
2703 (ndc[0] * 0.5 + 0.5) * vp_w,
2704 (1.0 - (ndc[1] * 0.5 + 0.5)) * vp_h,
2705 ]
2706}
2707
2708fn ndc_in_viewport(ndc: [f32; 2]) -> bool {
2710 ndc[0] >= -1.0 && ndc[0] <= 1.0 && ndc[1] >= -1.0 && ndc[1] <= 1.0
2711}
2712
2713fn clip_line_ndc(a: [f32; 2], b: [f32; 2]) -> Option<([f32; 2], [f32; 2])> {
2717 let dx = b[0] - a[0];
2718 let dy = b[1] - a[1];
2719 let mut t0 = 0.0f32;
2720 let mut t1 = 1.0f32;
2721
2722 for (p, q) in [
2724 (-dx, a[0] + 1.0),
2725 ( dx, 1.0 - a[0]),
2726 (-dy, a[1] + 1.0),
2727 ( dy, 1.0 - a[1]),
2728 ] {
2729 if p == 0.0 {
2730 if q < 0.0 { return None; }
2731 } else {
2732 let r = q / p;
2733 if p < 0.0 { t0 = t0.max(r); } else { t1 = t1.min(r); }
2734 }
2735 }
2736
2737 if t0 > t1 { return None; }
2738 Some((
2739 [a[0] + t0 * dx, a[1] + t0 * dy],
2740 [a[0] + t1 * dx, a[1] + t1 * dy],
2741 ))
2742}
2743
2744fn project_to_screen(
2747 pos: [f32; 3],
2748 view: &glam::Mat4,
2749 proj: &glam::Mat4,
2750 vp_w: f32,
2751 vp_h: f32,
2752) -> Option<[f32; 2]> {
2753 let p = glam::Vec3::from(pos);
2754 let clip = *proj * *view * p.extend(1.0);
2755 if clip.w <= 0.0 {
2756 return None;
2757 }
2758 let ndc_x = clip.x / clip.w;
2759 let ndc_y = clip.y / clip.w;
2760 if ndc_x < -1.0 || ndc_x > 1.0 || ndc_y < -1.0 || ndc_y > 1.0 {
2761 return None;
2762 }
2763 let x = (ndc_x * 0.5 + 0.5) * vp_w;
2764 let y = (1.0 - (ndc_y * 0.5 + 0.5)) * vp_h;
2765 Some([x, y])
2766}
2767
2768#[inline]
2770fn px_to_ndc(px_x: f32, px_y: f32, vp_w: f32, vp_h: f32) -> [f32; 2] {
2771 [
2772 px_x / vp_w * 2.0 - 1.0,
2773 1.0 - px_y / vp_h * 2.0,
2774 ]
2775}
2776
2777fn emit_solid_quad(
2779 verts: &mut Vec<crate::resources::OverlayTextVertex>,
2780 x0: f32, y0: f32,
2781 x1: f32, y1: f32,
2782 color: [f32; 4],
2783 vp_w: f32, vp_h: f32,
2784) {
2785 let tl = px_to_ndc(x0, y0, vp_w, vp_h);
2786 let tr = px_to_ndc(x1, y0, vp_w, vp_h);
2787 let bl = px_to_ndc(x0, y1, vp_w, vp_h);
2788 let br = px_to_ndc(x1, y1, vp_w, vp_h);
2789 let uv = [0.0, 0.0];
2790 let tex = 0.0;
2791 let v = |pos: [f32; 2]| crate::resources::OverlayTextVertex {
2792 position: pos, uv, color, use_texture: tex, _pad: 0.0,
2793 };
2794 verts.extend_from_slice(&[v(tl), v(bl), v(tr), v(tr), v(bl), v(br)]);
2795}
2796
2797fn emit_textured_quad(
2799 verts: &mut Vec<crate::resources::OverlayTextVertex>,
2800 x0: f32, y0: f32,
2801 x1: f32, y1: f32,
2802 uv_min: [f32; 2],
2803 uv_max: [f32; 2],
2804 color: [f32; 4],
2805 vp_w: f32, vp_h: f32,
2806) {
2807 let tl = px_to_ndc(x0, y0, vp_w, vp_h);
2808 let tr = px_to_ndc(x1, y0, vp_w, vp_h);
2809 let bl = px_to_ndc(x0, y1, vp_w, vp_h);
2810 let br = px_to_ndc(x1, y1, vp_w, vp_h);
2811 let tex = 1.0;
2812 let v = |pos: [f32; 2], uv: [f32; 2]| crate::resources::OverlayTextVertex {
2813 position: pos, uv, color, use_texture: tex, _pad: 0.0,
2814 };
2815 verts.extend_from_slice(&[
2817 v(tl, uv_min),
2818 v(bl, [uv_min[0], uv_max[1]]),
2819 v(tr, [uv_max[0], uv_min[1]]),
2820 v(tr, [uv_max[0], uv_min[1]]),
2821 v(bl, [uv_min[0], uv_max[1]]),
2822 v(br, uv_max),
2823 ]);
2824}
2825
2826fn emit_line_quad(
2828 verts: &mut Vec<crate::resources::OverlayTextVertex>,
2829 x0: f32, y0: f32,
2830 x1: f32, y1: f32,
2831 thickness: f32,
2832 color: [f32; 4],
2833 vp_w: f32, vp_h: f32,
2834) {
2835 let dx = x1 - x0;
2836 let dy = y1 - y0;
2837 let len = (dx * dx + dy * dy).sqrt();
2838 if len < 0.001 {
2839 return;
2840 }
2841 let half = thickness * 0.5;
2842 let nx = -dy / len * half;
2843 let ny = dx / len * half;
2844
2845 let p0 = px_to_ndc(x0 + nx, y0 + ny, vp_w, vp_h);
2846 let p1 = px_to_ndc(x0 - nx, y0 - ny, vp_w, vp_h);
2847 let p2 = px_to_ndc(x1 + nx, y1 + ny, vp_w, vp_h);
2848 let p3 = px_to_ndc(x1 - nx, y1 - ny, vp_w, vp_h);
2849 let uv = [0.0, 0.0];
2850 let tex = 0.0;
2851 let v = |pos: [f32; 2]| crate::resources::OverlayTextVertex {
2852 position: pos, uv, color, use_texture: tex, _pad: 0.0,
2853 };
2854 verts.extend_from_slice(&[v(p0), v(p1), v(p2), v(p2), v(p1), v(p3)]);
2855}
2856
2857#[inline]
2859fn apply_opacity(color: [f32; 4], opacity: f32) -> [f32; 4] {
2860 [color[0], color[1], color[2], color[3] * opacity]
2861}
2862
2863fn emit_rounded_quad(
2867 verts: &mut Vec<crate::resources::OverlayTextVertex>,
2868 x0: f32, y0: f32,
2869 x1: f32, y1: f32,
2870 radius: f32,
2871 color: [f32; 4],
2872 vp_w: f32, vp_h: f32,
2873) {
2874 let w = x1 - x0;
2875 let h = y1 - y0;
2876 let r = radius.min(w * 0.5).min(h * 0.5).max(0.0);
2877
2878 if r < 0.5 {
2879 emit_solid_quad(verts, x0, y0, x1, y1, color, vp_w, vp_h);
2880 return;
2881 }
2882
2883 emit_solid_quad(verts, x0, y0 + r, x1, y1 - r, color, vp_w, vp_h);
2886 emit_solid_quad(verts, x0 + r, y0, x1 - r, y0 + r, color, vp_w, vp_h);
2888 emit_solid_quad(verts, x0 + r, y1 - r, x1 - r, y1, color, vp_w, vp_h);
2890
2891 let corners = [
2893 (x0 + r, y0 + r, std::f32::consts::PI, std::f32::consts::FRAC_PI_2 * 3.0), (x1 - r, y0 + r, std::f32::consts::FRAC_PI_2 * 3.0, std::f32::consts::TAU), (x1 - r, y1 - r, 0.0, std::f32::consts::FRAC_PI_2), (x0 + r, y1 - r, std::f32::consts::FRAC_PI_2, std::f32::consts::PI), ];
2898 let segments = 6;
2899 let uv = [0.0, 0.0];
2900 let tex = 0.0;
2901 let v = |pos: [f32; 2]| crate::resources::OverlayTextVertex {
2902 position: pos, uv, color, use_texture: tex, _pad: 0.0,
2903 };
2904 for (cx, cy, start, end) in corners {
2905 let center = px_to_ndc(cx, cy, vp_w, vp_h);
2906 for i in 0..segments {
2907 let a0 = start + (end - start) * i as f32 / segments as f32;
2908 let a1 = start + (end - start) * (i + 1) as f32 / segments as f32;
2909 let p0 = px_to_ndc(cx + a0.cos() * r, cy + a0.sin() * r, vp_w, vp_h);
2910 let p1 = px_to_ndc(cx + a1.cos() * r, cy + a1.sin() * r, vp_w, vp_h);
2911 verts.extend_from_slice(&[v(center), v(p0), v(p1)]);
2912 }
2913 }
2914}
2915
2916fn format_ruler_distance(distance: f32, fmt: Option<&str>) -> String {
2927 let pattern = fmt.unwrap_or("{:.3}");
2928 if let Some(open) = pattern.find('{') {
2930 if let Some(close_rel) = pattern[open..].find('}') {
2931 let close = open + close_rel;
2932 let spec = &pattern[open + 1..close]; let prefix = &pattern[..open];
2934 let suffix = &pattern[close + 1..];
2935 let formatted = if let Some(prec_str) = spec.strip_prefix(":.") {
2936 let prec_str = prec_str.trim_end_matches('f');
2938 if let Ok(prec) = prec_str.parse::<usize>() {
2939 format!("{distance:.prec$}")
2940 } else {
2941 format!("{distance:.3}")
2942 }
2943 } else if spec.is_empty() || spec == ":" {
2944 format!("{distance}")
2945 } else {
2946 format!("{distance:.3}")
2947 };
2948 return format!("{prefix}{formatted}{suffix}");
2949 }
2950 }
2951 format!("{distance:.3}")
2952}