1use std::{any::Any, panic, sync::Arc};
2
3use crate::core::{Camera, PlotRenderConfig, PlotRenderer, RenderResult};
4use crate::geometry_scene::{GeometryScene, GeometryScenePresentation};
5use crate::gpu::util::map_read_async;
6use crate::plots::Figure;
7#[cfg(feature = "egui-overlay")]
8use crate::styling::ModernDarkTheme;
9use crate::styling::PlotThemeConfig;
10#[cfg(feature = "egui-overlay")]
11use runmat_time::Instant;
12
13#[cfg(feature = "egui-overlay")]
14use crate::overlay::plot_overlay::{OverlayConfig, OverlayMetrics, PlotOverlay};
15#[cfg(feature = "egui-overlay")]
16use egui_wgpu::ScreenDescriptor;
17
18pub const HEADLESS_GPU_ADAPTER_UNAVAILABLE: &str = "Failed to find suitable GPU adapter";
19pub const HEADLESS_GPU_DEVICE_CREATION_FAILED_PREFIX: &str = "Failed to create device:";
20pub const HEADLESS_GPU_CONTEXT_PANICKED_PREFIX: &str = "Headless GPU context creation panicked:";
21
22pub struct NativeSurfaceRenderContext {
24 renderer: PlotRenderer,
25 config: PlotRenderConfig,
26 pixels_per_point: f32,
27 background_policy: BackgroundPolicy,
28 textmark: Option<String>,
29 #[cfg(feature = "egui-overlay")]
30 host_actions: Vec<NativeSurfaceHostAction>,
31 #[cfg(feature = "egui-overlay")]
32 pending_overlay_events: Vec<crate::core::PlotEvent>,
33 #[cfg(feature = "egui-overlay")]
34 overlay: Option<NativeOverlayState>,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum NativeSurfaceHostAction {
39 CreateFeaStudy,
40}
41
42#[derive(Debug, Clone, Copy)]
43enum BackgroundPolicy {
44 ThemeDriven,
45 Explicit(glam::Vec4),
46}
47
48#[cfg(feature = "egui-overlay")]
49struct NativeOverlayState {
50 egui_ctx: egui::Context,
51 egui_renderer: egui_wgpu::Renderer,
52 plot_overlay: PlotOverlay,
53 wants_pointer_input: bool,
54 capture_regions_px: Vec<[f32; 4]>,
55}
56
57#[derive(Debug, Clone)]
58pub struct NativeSurfaceCameraState {
59 pub active_camera: Camera,
60 pub axes_cameras: Vec<Camera>,
61 pub axes_camera_user_controlled: Vec<bool>,
62}
63
64impl NativeSurfaceRenderContext {
65 pub async fn new(
67 device: Arc<wgpu::Device>,
68 queue: Arc<wgpu::Queue>,
69 width: u32,
70 height: u32,
71 format: wgpu::TextureFormat,
72 ) -> Result<Self, String> {
73 let surface_config = wgpu::SurfaceConfiguration {
74 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
75 format,
76 width: width.max(1),
77 height: height.max(1),
78 present_mode: wgpu::PresentMode::Fifo,
79 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
80 view_formats: vec![],
81 desired_maximum_frame_latency: 1,
82 };
83 let renderer = PlotRenderer::new(device, queue, surface_config)
84 .await
85 .map_err(|err| format!("native surface renderer init failed: {err}"))?;
86 let config = PlotRenderConfig {
87 width: width.max(1),
88 height: height.max(1),
89 ..PlotRenderConfig::default()
90 };
91 #[cfg(feature = "egui-overlay")]
92 let overlay = {
93 let egui_ctx = egui::Context::default();
94 ModernDarkTheme::default().apply_to_egui(&egui_ctx);
95 let egui_renderer = crate::wgpu_compat::egui_renderer_new(
96 &renderer.wgpu_renderer.device,
97 format,
98 None,
99 1,
100 );
101 Some(NativeOverlayState {
102 egui_ctx,
103 egui_renderer,
104 plot_overlay: PlotOverlay::new(),
105 wants_pointer_input: false,
106 capture_regions_px: Vec::new(),
107 })
108 };
109
110 Ok(Self {
111 renderer,
112 config,
113 pixels_per_point: 1.0,
114 background_policy: BackgroundPolicy::ThemeDriven,
115 textmark: None,
116 #[cfg(feature = "egui-overlay")]
117 host_actions: Vec::new(),
118 #[cfg(feature = "egui-overlay")]
119 pending_overlay_events: Vec::new(),
120 #[cfg(feature = "egui-overlay")]
121 overlay,
122 })
123 }
124
125 pub fn resize(&mut self, width: u32, height: u32) {
127 let next_width = width.max(1);
128 let next_height = height.max(1);
129 self.config.width = next_width;
130 self.config.height = next_height;
131 self.renderer.wgpu_renderer.surface_config.width = next_width;
132 self.renderer.wgpu_renderer.surface_config.height = next_height;
133 self.renderer.on_surface_config_updated();
134 }
135
136 pub fn set_pixels_per_point(&mut self, pixels_per_point: f32) {
137 self.pixels_per_point = pixels_per_point.clamp(0.5, 4.0);
138 }
139
140 pub fn set_theme_config(&mut self, theme: PlotThemeConfig) {
141 self.renderer.theme = theme.clone();
142 self.config.theme = theme;
143 self.apply_background_policy();
144 }
145
146 pub fn set_textmark(&mut self, textmark: Option<&str>) {
147 self.textmark = textmark
148 .map(str::trim)
149 .filter(|s| !s.is_empty())
150 .map(ToOwned::to_owned);
151 }
152
153 pub fn camera_state(&self) -> NativeSurfaceCameraState {
154 let mut axes_cameras = Vec::new();
155 let mut index = 0;
156 while let Some(camera) = self.renderer.axes_camera(index) {
157 axes_cameras.push(camera.clone());
158 index += 1;
159 }
160 if axes_cameras.is_empty() {
161 axes_cameras.push(self.renderer.camera().clone());
162 }
163 let active_camera = axes_cameras
164 .first()
165 .cloned()
166 .unwrap_or_else(|| self.renderer.camera().clone());
167 let mut axes_camera_user_controlled =
168 self.renderer.axes_camera_interaction_flags().to_vec();
169 axes_camera_user_controlled.resize(axes_cameras.len().max(1), false);
170 NativeSurfaceCameraState {
171 active_camera,
172 axes_cameras,
173 axes_camera_user_controlled,
174 }
175 }
176
177 pub fn overlay_wants_pointer_input(&self) -> bool {
178 #[cfg(feature = "egui-overlay")]
179 {
180 self.overlay
181 .as_ref()
182 .map(|overlay| overlay.wants_pointer_input)
183 .unwrap_or(false)
184 }
185
186 #[cfg(not(feature = "egui-overlay"))]
187 {
188 false
189 }
190 }
191
192 pub fn overlay_capture_regions_px(&self) -> Vec<[f32; 4]> {
193 #[cfg(feature = "egui-overlay")]
194 {
195 self.overlay
196 .as_ref()
197 .map(|overlay| overlay.capture_regions_px.clone())
198 .unwrap_or_default()
199 }
200
201 #[cfg(not(feature = "egui-overlay"))]
202 {
203 Vec::new()
204 }
205 }
206
207 pub fn take_host_actions(&mut self) -> Vec<NativeSurfaceHostAction> {
208 #[cfg(feature = "egui-overlay")]
209 {
210 std::mem::take(&mut self.host_actions)
211 }
212
213 #[cfg(not(feature = "egui-overlay"))]
214 {
215 Vec::new()
216 }
217 }
218
219 #[cfg(feature = "egui-overlay")]
220 pub fn push_overlay_events(
221 &mut self,
222 events: impl IntoIterator<Item = crate::core::PlotEvent>,
223 ) {
224 self.pending_overlay_events.extend(events);
225 }
226
227 pub fn render_to_view(
229 &mut self,
230 figure: &Figure,
231 view: &wgpu::TextureView,
232 camera: Option<&Camera>,
233 axes_cameras: Option<&[Camera]>,
234 ) -> Result<RenderResult, String> {
235 self.render_to_view_with_camera_state(figure, view, camera, axes_cameras, None)
236 }
237
238 pub fn render_to_view_with_camera_state(
239 &mut self,
240 figure: &Figure,
241 view: &wgpu::TextureView,
242 camera: Option<&Camera>,
243 axes_cameras: Option<&[Camera]>,
244 axes_camera_user_controlled: Option<&[bool]>,
245 ) -> Result<RenderResult, String> {
246 self.prepare_scene(figure, camera, axes_cameras, axes_camera_user_controlled);
247
248 let mut encoder = self.renderer.wgpu_renderer.device.create_command_encoder(
249 &wgpu::CommandEncoderDescriptor {
250 label: Some("Native Surface Render Encoder"),
251 },
252 );
253
254 let render_result = self.render_scene_with_overlay(&mut encoder, view)?;
255
256 self.renderer
257 .wgpu_renderer
258 .queue
259 .submit(std::iter::once(encoder.finish()));
260
261 Ok(render_result)
262 }
263
264 pub fn render_geometry_scene_to_view(
266 &mut self,
267 scene: &GeometryScene,
268 view: &wgpu::TextureView,
269 camera: Option<&Camera>,
270 ) -> Result<RenderResult, String> {
271 self.render_geometry_scene_to_view_with_presentation(scene, view, camera, None)
272 }
273
274 pub fn render_geometry_scene_to_view_with_presentation(
275 &mut self,
276 scene: &GeometryScene,
277 view: &wgpu::TextureView,
278 camera: Option<&Camera>,
279 presentation: Option<&GeometryScenePresentation>,
280 ) -> Result<RenderResult, String> {
281 self.prepare_geometry_scene(scene, camera, presentation);
282
283 let mut encoder = self.renderer.wgpu_renderer.device.create_command_encoder(
284 &wgpu::CommandEncoderDescriptor {
285 label: Some("Native Geometry Scene Render Encoder"),
286 },
287 );
288
289 let render_result = self.render_scene_with_overlay(&mut encoder, view)?;
290
291 self.renderer
292 .wgpu_renderer
293 .queue
294 .submit(std::iter::once(encoder.finish()));
295
296 Ok(render_result)
297 }
298
299 pub async fn render_to_rgba(
301 &mut self,
302 figure: &Figure,
303 camera: Option<&Camera>,
304 axes_cameras: Option<&[Camera]>,
305 ) -> Result<Vec<u8>, String> {
306 self.render_to_rgba_with_camera_state(figure, camera, axes_cameras, None)
307 .await
308 }
309
310 pub async fn render_to_rgba_with_camera_state(
311 &mut self,
312 figure: &Figure,
313 camera: Option<&Camera>,
314 axes_cameras: Option<&[Camera]>,
315 axes_camera_user_controlled: Option<&[bool]>,
316 ) -> Result<Vec<u8>, String> {
317 log::debug!(
318 "runmat-plot: native_surface.render_to_rgba.start width={} height={} axes={} overrides={}",
319 self.config.width.max(1),
320 self.config.height.max(1),
321 figure.axes_metadata.len(),
322 axes_cameras.map(|items| items.len()).unwrap_or(0)
323 );
324 self.prepare_scene(figure, camera, axes_cameras, axes_camera_user_controlled);
325
326 let width = self.config.width.max(1);
327 let height = self.config.height.max(1);
328 let format = self.renderer.wgpu_renderer.surface_config.format;
329 let device = self.renderer.wgpu_renderer.device.clone();
330 let queue = self.renderer.wgpu_renderer.queue.clone();
331
332 let color_texture = device.create_texture(&wgpu::TextureDescriptor {
333 label: Some("native_surface_offscreen_color"),
334 size: wgpu::Extent3d {
335 width,
336 height,
337 depth_or_array_layers: 1,
338 },
339 mip_level_count: 1,
340 sample_count: 1,
341 dimension: wgpu::TextureDimension::D2,
342 format,
343 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
344 view_formats: &[],
345 });
346 let color_view = color_texture.create_view(&wgpu::TextureViewDescriptor::default());
347
348 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
349 label: Some("Native Surface RGBA Render Encoder"),
350 });
351 log::debug!(
352 "runmat-plot: native_surface.render_to_rgba.render_scene width={} height={}",
353 width,
354 height
355 );
356 self.render_scene_with_overlay(&mut encoder, &color_view)?;
357 log::debug!("runmat-plot: native_surface.render_to_rgba.render_scene_ok");
358 queue.submit(std::iter::once(encoder.finish()));
359
360 let bytes_per_pixel = 4u32;
361 let padded_bytes_per_row = (width * bytes_per_pixel).div_ceil(256) * 256;
362 let output_buffer_size = (padded_bytes_per_row * height) as wgpu::BufferAddress;
363 let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
364 label: Some("native_surface_offscreen_readback"),
365 size: output_buffer_size,
366 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
367 mapped_at_creation: false,
368 });
369
370 let mut copy_encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
371 label: Some("Native Surface RGBA Copy Encoder"),
372 });
373 copy_encoder.copy_texture_to_buffer(
374 crate::wgpu_compat::TexelCopyTextureInfo {
375 texture: &color_texture,
376 mip_level: 0,
377 origin: wgpu::Origin3d::ZERO,
378 aspect: wgpu::TextureAspect::All,
379 },
380 crate::wgpu_compat::TexelCopyBufferInfo {
381 buffer: &output_buffer,
382 layout: crate::wgpu_compat::TexelCopyBufferLayout {
383 offset: 0,
384 bytes_per_row: Some(padded_bytes_per_row),
385 rows_per_image: Some(height),
386 },
387 },
388 wgpu::Extent3d {
389 width,
390 height,
391 depth_or_array_layers: 1,
392 },
393 );
394 queue.submit(std::iter::once(copy_encoder.finish()));
395 log::debug!(
396 "runmat-plot: native_surface.render_to_rgba.copy_submitted padded_bytes_per_row={} height={}",
397 padded_bytes_per_row,
398 height
399 );
400
401 let slice = output_buffer.slice(..);
402 map_read_async(device.as_ref(), &slice).await?;
403 log::debug!("runmat-plot: native_surface.render_to_rgba.readback_ready");
404 let data = slice.get_mapped_range();
405 let mut pixels = vec![0u8; (width * height * 4) as usize];
406 for row in 0..height as usize {
407 let src_start = row * padded_bytes_per_row as usize;
408 let dst_start = row * width as usize * 4;
409 pixels[dst_start..dst_start + width as usize * 4]
410 .copy_from_slice(&data[src_start..src_start + width as usize * 4]);
411 }
412 drop(data);
413 output_buffer.unmap();
414 log::debug!(
415 "runmat-plot: native_surface.render_to_rgba.ok bytes={}",
416 pixels.len()
417 );
418 Ok(pixels)
419 }
420
421 pub async fn render_geometry_scene_to_rgba(
422 &mut self,
423 scene: &GeometryScene,
424 camera: Option<&Camera>,
425 ) -> Result<Vec<u8>, String> {
426 self.prepare_geometry_scene(scene, camera, None);
427
428 let width = self.config.width.max(1);
429 let height = self.config.height.max(1);
430 let format = self.renderer.wgpu_renderer.surface_config.format;
431 let device = self.renderer.wgpu_renderer.device.clone();
432 let queue = self.renderer.wgpu_renderer.queue.clone();
433
434 let color_texture = device.create_texture(&wgpu::TextureDescriptor {
435 label: Some("native_surface_geometry_offscreen_color"),
436 size: wgpu::Extent3d {
437 width,
438 height,
439 depth_or_array_layers: 1,
440 },
441 mip_level_count: 1,
442 sample_count: 1,
443 dimension: wgpu::TextureDimension::D2,
444 format,
445 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
446 view_formats: &[],
447 });
448 let color_view = color_texture.create_view(&wgpu::TextureViewDescriptor::default());
449
450 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
451 label: Some("Native Geometry Surface RGBA Render Encoder"),
452 });
453 self.render_scene_with_overlay(&mut encoder, &color_view)?;
454 queue.submit(std::iter::once(encoder.finish()));
455
456 let bytes_per_pixel = 4u32;
457 let padded_bytes_per_row = (width * bytes_per_pixel).div_ceil(256) * 256;
458 let output_buffer_size = (padded_bytes_per_row * height) as wgpu::BufferAddress;
459 let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
460 label: Some("native_surface_geometry_offscreen_readback"),
461 size: output_buffer_size,
462 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
463 mapped_at_creation: false,
464 });
465
466 let mut copy_encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
467 label: Some("Native Geometry Surface RGBA Copy Encoder"),
468 });
469 copy_encoder.copy_texture_to_buffer(
470 crate::wgpu_compat::TexelCopyTextureInfo {
471 texture: &color_texture,
472 mip_level: 0,
473 origin: wgpu::Origin3d::ZERO,
474 aspect: wgpu::TextureAspect::All,
475 },
476 crate::wgpu_compat::TexelCopyBufferInfo {
477 buffer: &output_buffer,
478 layout: crate::wgpu_compat::TexelCopyBufferLayout {
479 offset: 0,
480 bytes_per_row: Some(padded_bytes_per_row),
481 rows_per_image: Some(height),
482 },
483 },
484 wgpu::Extent3d {
485 width,
486 height,
487 depth_or_array_layers: 1,
488 },
489 );
490 queue.submit(std::iter::once(copy_encoder.finish()));
491
492 let slice = output_buffer.slice(..);
493 map_read_async(device.as_ref(), &slice).await?;
494 let data = slice.get_mapped_range();
495 let mut pixels = vec![0u8; (width * height * 4) as usize];
496 for row in 0..height as usize {
497 let src_start = row * padded_bytes_per_row as usize;
498 let dst_start = row * width as usize * 4;
499 pixels[dst_start..dst_start + width as usize * 4]
500 .copy_from_slice(&data[src_start..src_start + width as usize * 4]);
501 }
502 drop(data);
503 output_buffer.unmap();
504 Ok(pixels)
505 }
506
507 fn prepare_scene(
508 &mut self,
509 figure: &Figure,
510 camera: Option<&Camera>,
511 axes_cameras: Option<&[Camera]>,
512 axes_camera_user_controlled: Option<&[bool]>,
513 ) {
514 let bg = figure.background_color;
517 self.background_policy = if is_default_figure_bg(bg) {
518 BackgroundPolicy::ThemeDriven
519 } else {
520 BackgroundPolicy::Explicit(bg)
521 };
522 self.apply_background_policy();
523 self.config.show_grid = figure.grid_enabled
524 || figure.minor_grid_enabled
525 || figure
526 .axes_metadata
527 .iter()
528 .any(|meta| meta.grid_enabled || meta.minor_grid_enabled);
529 self.config.show_title = figure.has_any_titles();
530
531 self.renderer.set_figure(figure.clone());
532 if let Some(camera) = camera {
533 *self.renderer.camera_mut() = camera.clone();
534 }
535 if let Some(overrides) = axes_cameras {
536 for (index, override_camera) in overrides.iter().enumerate() {
537 if let Some(target) = self.renderer.axes_camera_mut(index) {
538 *target = override_camera.clone();
539 }
540 }
541 }
542 if let Some(flags) = axes_camera_user_controlled {
543 self.renderer.set_axes_camera_interaction_flags(flags);
544 }
545 }
546
547 fn prepare_geometry_scene(
548 &mut self,
549 scene: &GeometryScene,
550 camera: Option<&Camera>,
551 presentation: Option<&GeometryScenePresentation>,
552 ) {
553 self.background_policy = BackgroundPolicy::ThemeDriven;
554 self.apply_background_policy();
555 self.config.show_grid = scene.show_grid;
556 self.config.show_title = scene.title.is_some();
557
558 if let Some(presentation) = presentation {
559 self.renderer
560 .set_geometry_scene_with_presentation(scene.clone(), presentation.clone());
561 } else {
562 self.renderer.set_geometry_scene(scene.clone());
563 }
564 if let Some(camera) = camera {
565 *self.renderer.camera_mut() = camera.clone();
566 }
567 }
568
569 fn render_scene_with_overlay(
570 &mut self,
571 encoder: &mut wgpu::CommandEncoder,
572 target_view: &wgpu::TextureView,
573 ) -> Result<RenderResult, String> {
574 #[cfg(feature = "egui-overlay")]
575 {
576 let Some(overlay) = self.overlay.as_mut() else {
577 log::debug!(
578 "runmat-plot: native_surface.render_scene_with_overlay.branch_no_overlay"
579 );
580 return self
581 .renderer
582 .render_scene_to_target(encoder, target_view, &self.config)
583 .map_err(|err| format!("native surface render failed: {err}"));
584 };
585
586 let start_time = Instant::now();
587 log::debug!(
588 "runmat-plot: native_surface.render_scene_with_overlay.start width={} height={} ppp={}",
589 self.config.width,
590 self.config.height,
591 self.pixels_per_point
592 );
593 let mut plot_area_points: Option<egui::Rect> = None;
594 let scene_stats = self.renderer.scene.statistics();
595 let _ = self.renderer.calculate_data_bounds();
596 let ppp = self.pixels_per_point.max(0.5);
597 let screen_rect = egui::Rect::from_min_size(
598 egui::Pos2::new(0.0, 0.0),
599 egui::Vec2::new(
600 (self.config.width.max(1) as f32) / ppp,
601 (self.config.height.max(1) as f32) / ppp,
602 ),
603 );
604 let raw_input = crate::core::interaction::egui_raw_input_from_plot_events(
605 screen_rect,
606 ppp,
607 std::mem::take(&mut self.pending_overlay_events),
608 );
609 let full_output = overlay.egui_ctx.run(raw_input, |ctx| {
610 overlay
611 .plot_overlay
612 .set_theme_config(self.renderer.theme.clone());
613 overlay.plot_overlay.apply_theme(ctx);
614 let overlay_config = OverlayConfig {
615 show_grid: false,
617 show_toolbar: false,
619 show_cad_overlay: false,
621 font_scale: 1.25,
622 show_axes: true,
623 show_title: self.renderer.geometry_overlay().is_none(),
624 title: self
625 .renderer
626 .overlay_title()
627 .cloned()
628 .or(Some("Plot".to_string())),
629 x_label: self
630 .renderer
631 .overlay_x_label()
632 .cloned()
633 .or(Some("X".to_string())),
634 y_label: self
635 .renderer
636 .overlay_y_label()
637 .cloned()
638 .or(Some("Y".to_string())),
639 show_sidebar: false,
640 ..Default::default()
641 };
642 let overlay_metrics = OverlayMetrics {
643 vertex_count: scene_stats.total_vertices,
644 triangle_count: scene_stats.total_triangles,
645 render_time_ms: 0.0,
646 fps: 60.0,
647 };
648 let frame_info = overlay.plot_overlay.render(
649 ctx,
650 &self.renderer,
651 &overlay_config,
652 overlay_metrics,
653 );
654 if let Some(textmark) = self.textmark.as_deref() {
655 let screen = ctx.screen_rect();
656 let anchor = egui::pos2(screen.max.x - 8.0, screen.max.y - 6.0);
657 let font =
658 egui::FontId::proportional(11.0 * overlay_config.font_scale.max(0.8));
659 let layer = egui::LayerId::new(
660 egui::Order::Foreground,
661 egui::Id::new("runmat_export_textmark"),
662 );
663 let painter = ctx.layer_painter(layer);
664 painter.text(
665 anchor + egui::vec2(1.0, 1.0),
666 egui::Align2::RIGHT_BOTTOM,
667 textmark,
668 font.clone(),
669 egui::Color32::from_rgba_premultiplied(0, 0, 0, 72),
670 );
671 painter.text(
672 anchor,
673 egui::Align2::RIGHT_BOTTOM,
674 textmark,
675 font,
676 egui::Color32::from_rgba_premultiplied(226, 234, 245, 96),
677 );
678 }
679 plot_area_points = frame_info.plot_area;
680 });
681
682 let cad_actions = overlay.plot_overlay.take_cad_actions();
683 self.host_actions
684 .extend(apply_cad_overlay_actions(&mut self.renderer, cad_actions));
685 overlay.wants_pointer_input = overlay.plot_overlay.overlay_pointer_captured();
686 overlay.capture_regions_px = overlay
687 .plot_overlay
688 .overlay_capture_regions()
689 .into_iter()
690 .map(|[x0, y0, x1, y1]| [x0 * ppp, y0 * ppp, x1 * ppp, y1 * ppp])
691 .collect();
692
693 let paint_jobs = overlay
694 .egui_ctx
695 .tessellate(full_output.shapes, full_output.pixels_per_point);
696 for (id, image_delta) in &full_output.textures_delta.set {
697 overlay.egui_renderer.update_texture(
698 &self.renderer.wgpu_renderer.device,
699 &self.renderer.wgpu_renderer.queue,
700 *id,
701 image_delta,
702 );
703 }
704
705 let screen_descriptor = ScreenDescriptor {
706 size_in_pixels: [self.config.width.max(1), self.config.height.max(1)],
707 pixels_per_point: full_output.pixels_per_point,
708 };
709 overlay.egui_renderer.update_buffers(
710 &self.renderer.wgpu_renderer.device,
711 &self.renderer.wgpu_renderer.queue,
712 encoder,
713 &paint_jobs,
714 &screen_descriptor,
715 );
716
717 let (vx, vy, vw, vh) = if let Some(rect) = plot_area_points {
718 let vx = (rect.min.x * ppp).round().max(0.0) as u32;
719 let vy = (rect.min.y * ppp).round().max(0.0) as u32;
720 let vw = (rect.width() * ppp).round().max(1.0) as u32;
721 let vh = (rect.height() * ppp).round().max(1.0) as u32;
722 (vx, vy, vw, vh)
723 } else {
724 (0, 0, self.config.width.max(1), self.config.height.max(1))
725 };
726 let max_w = self.config.width.max(1);
727 let max_h = self.config.height.max(1);
728 let vx = vx.min(max_w.saturating_sub(1));
729 let vy = vy.min(max_h.saturating_sub(1));
730 let vw = vw.max(1).min(max_w.saturating_sub(vx).max(1));
731 let vh = vh.max(1).min(max_h.saturating_sub(vy).max(1));
732 let mut axes_viewports_px = vec![(vx, vy, vw, vh)];
733
734 if vw > 0 && vh > 0 {
735 self.renderer
736 .camera_mut()
737 .update_aspect_ratio((vw as f32) / (vh as f32));
738 }
739
740 let (rows, cols) = self.renderer.figure_axes_grid();
741 log::debug!(
742 "runmat-plot: native_surface.render_scene_with_overlay.axes_grid rows={} cols={} plot_area_present={}",
743 rows,
744 cols,
745 plot_area_points.is_some()
746 );
747 if rows * cols > 1 {
748 log::debug!(
749 "runmat-plot: native_surface.render_scene_with_overlay.branch_subplot_axes"
750 );
751 let rect_points = plot_area_points.unwrap_or_else(|| {
752 egui::Rect::from_min_size(
753 egui::Pos2::new(0.0, 0.0),
754 egui::Vec2::new(
755 (self.config.width.max(1) as f32) / ppp,
756 (self.config.height.max(1) as f32) / ppp,
757 ),
758 )
759 });
760 let existing_rect_count = overlay.plot_overlay.axes_plot_rects().len();
761 log::debug!(
762 "runmat-plot: native_surface.render_scene_with_overlay.subplot_rect_source rows={} cols={} existing_rects={} expected_rects={}",
763 rows,
764 cols,
765 existing_rect_count,
766 rows * cols
767 );
768 let rects = if overlay.plot_overlay.axes_plot_rects().len() == rows * cols {
769 log::debug!(
770 "runmat-plot: native_surface.render_scene_with_overlay.subplot_rect_source_existing rects={}",
771 existing_rect_count
772 );
773 overlay.plot_overlay.axes_plot_rects().to_vec()
774 } else {
775 log::debug!(
776 "runmat-plot: native_surface.render_scene_with_overlay.subplot_rect_source_compute rect_points=({}, {})..({}, {})",
777 rect_points.min.x,
778 rect_points.min.y,
779 rect_points.max.x,
780 rect_points.max.y
781 );
782 overlay.plot_overlay.compute_subplot_plot_rects_snapped(
783 rect_points,
784 &self.renderer,
785 1.0,
786 ppp,
787 )
788 };
789 log::debug!(
790 "runmat-plot: native_surface.render_scene_with_overlay.subplot_rects_ready rects={}",
791 rects.len()
792 );
793 let sw = self.config.width as f32;
794 let sh = self.config.height as f32;
795 let mut viewports: Vec<(u32, u32, u32, u32)> = Vec::with_capacity(rects.len());
796 for (rect_index, r) in rects.into_iter().enumerate() {
797 log::debug!(
798 "runmat-plot: native_surface.render_scene_with_overlay.subplot_rect viewport_index={} rect=({}, {})..({}, {})",
799 rect_index,
800 r.min.x,
801 r.min.y,
802 r.max.x,
803 r.max.y
804 );
805 let mut rx = (r.min.x * ppp).round().max(0.0);
806 let mut ry = (r.min.y * ppp).round().max(0.0);
807 let mut rw = (r.width() * ppp).round().max(1.0);
808 let mut rh = (r.height() * ppp).round().max(1.0);
809 if rx >= sw {
810 rx = (sw - 1.0).max(0.0);
811 }
812 if ry >= sh {
813 ry = (sh - 1.0).max(0.0);
814 }
815 if rx + rw > sw {
816 rw = (sw - rx).max(1.0);
817 }
818 if ry + rh > sh {
819 rh = (sh - ry).max(1.0);
820 }
821 viewports.push((rx as u32, ry as u32, rw as u32, rh as u32));
822 log::debug!(
823 "runmat-plot: native_surface.render_scene_with_overlay.subplot_viewport viewport_index={} viewport=({}, {}, {}, {})",
824 rect_index,
825 rx as u32,
826 ry as u32,
827 rw as u32,
828 rh as u32
829 );
830 }
831 log::debug!(
832 "runmat-plot: native_surface.render_scene_with_overlay.subplot_viewports_ready count={}",
833 viewports.len()
834 );
835 axes_viewports_px = viewports.clone();
836 let axes_plot_sizes_px: Vec<(u32, u32)> = viewports
837 .iter()
838 .map(|&(_, _, w, h)| (w.max(1), h.max(1)))
839 .collect();
840 self.renderer
841 .ensure_scene_viewport_dependent_geometry_for_axes(&axes_plot_sizes_px);
842 self.renderer
843 .render_axes_to_viewports(
844 encoder,
845 target_view,
846 &viewports,
847 self.config.msaa_samples.max(1),
848 &self.config,
849 )
850 .map_err(|err| format!("native surface subplot render failed: {err}"))?;
851 log::debug!(
852 "runmat-plot: native_surface.render_scene_with_overlay.subplot_render_ok viewports={}",
853 viewports.len()
854 );
855 } else {
856 log::debug!(
857 "runmat-plot: native_surface.render_scene_with_overlay.branch_single_axes"
858 );
859 {
860 let clear_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
861 label: Some("runmat-native-single-axes-clear"),
862 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
863 view: target_view,
864 resolve_target: None,
865 ops: wgpu::Operations {
866 load: wgpu::LoadOp::Clear(wgpu::Color {
867 r: self.config.background_color.x as f64,
868 g: self.config.background_color.y as f64,
869 b: self.config.background_color.z as f64,
870 a: self.config.background_color.w as f64,
871 }),
872 store: wgpu::StoreOp::Store,
873 },
874 })],
875 depth_stencil_attachment: None,
876 timestamp_writes: None,
877 occlusion_query_set: None,
878 });
879 drop(clear_pass);
880 }
881
882 let mut cfg = self.config.clone();
883 cfg.width = vw.max(1);
884 cfg.height = vh.max(1);
885 let cam = self
886 .renderer
887 .axes_camera(0)
888 .cloned()
889 .unwrap_or_else(|| self.renderer.camera().clone());
890 let axes_plot_sizes_px = vec![(vw.max(1), vh.max(1))];
891 self.renderer
892 .ensure_scene_viewport_dependent_geometry_for_axes(&axes_plot_sizes_px);
893 self.renderer
894 .render_camera_to_viewport(
895 encoder,
896 target_view,
897 (vx, vy, vw, vh),
898 &cfg,
899 &cam,
900 0,
901 true,
902 )
903 .map_err(|err| format!("native surface viewport render failed: {err}"))?;
904 log::debug!(
905 "runmat-plot: native_surface.render_scene_with_overlay.single_axes_render_ok"
906 );
907 }
908
909 {
910 let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
911 label: Some("runmat-native-egui-overlay"),
912 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
913 view: target_view,
914 resolve_target: None,
915 ops: wgpu::Operations {
916 load: wgpu::LoadOp::Load,
917 store: wgpu::StoreOp::Store,
918 },
919 })],
920 depth_stencil_attachment: None,
921 timestamp_writes: None,
922 occlusion_query_set: None,
923 });
924 #[cfg(target_arch = "wasm32")]
925 let mut render_pass = render_pass.forget_lifetime();
926 #[cfg(not(target_arch = "wasm32"))]
927 let mut render_pass = render_pass;
928 overlay
929 .egui_renderer
930 .render(&mut render_pass, &paint_jobs, &screen_descriptor);
931 log::debug!(
932 "runmat-plot: native_surface.render_scene_with_overlay.overlay_ok paint_jobs={} textures_set={} textures_free={}",
933 paint_jobs.len(),
934 full_output.textures_delta.set.len(),
935 full_output.textures_delta.free.len()
936 );
937 }
938
939 for id in &full_output.textures_delta.free {
940 overlay.egui_renderer.free_texture(id);
941 }
942
943 Ok(RenderResult {
944 success: true,
945 data_bounds: self.renderer.data_bounds(),
946 axes_viewports_px,
947 vertex_count: scene_stats.total_vertices,
948 triangle_count: scene_stats.total_triangles,
949 render_time_ms: start_time.elapsed().as_secs_f64() * 1000.0,
950 })
951 }
952
953 #[cfg(not(feature = "egui-overlay"))]
954 {
955 self.renderer
956 .render_scene_to_target(encoder, target_view, &self.config)
957 .map_err(|err| format!("native surface render failed: {err}"))
958 }
959 }
960
961 fn apply_background_policy(&mut self) {
962 self.config.background_color = match self.background_policy {
963 BackgroundPolicy::ThemeDriven => {
964 self.renderer.theme.build_theme().get_background_color()
965 }
966 BackgroundPolicy::Explicit(color) => color,
967 };
968 }
969}
970
971fn is_default_figure_bg(bg: glam::Vec4) -> bool {
972 const EPS: f32 = 1e-3;
973 (bg.x - 1.0).abs() <= EPS
974 && (bg.y - 1.0).abs() <= EPS
975 && (bg.z - 1.0).abs() <= EPS
976 && (bg.w - 1.0).abs() <= EPS
977}
978
979async fn create_headless_context(
980 width: u32,
981 height: u32,
982) -> Result<NativeSurfaceRenderContext, String> {
983 let format = wgpu::TextureFormat::Rgba8Unorm;
987 if let Some(ctx) = crate::context::shared_wgpu_context() {
988 log::debug!(
989 "runmat-plot: native_surface.headless_context.branch_shared_context width={} height={}",
990 width,
991 height
992 );
993 return NativeSurfaceRenderContext::new(ctx.device, ctx.queue, width, height, format).await;
994 }
995
996 log::debug!(
997 "runmat-plot: native_surface.headless_context.branch_dedicated_context width={} height={}",
998 width,
999 height
1000 );
1001
1002 let instance = panic::catch_unwind(|| {
1003 crate::wgpu_compat::instance_new(wgpu::InstanceDescriptor::default())
1004 })
1005 .map_err(|payload| {
1006 format!(
1007 "{HEADLESS_GPU_CONTEXT_PANICKED_PREFIX} {}",
1008 panic_payload_to_string(payload)
1009 )
1010 })?;
1011 let adapter = instance
1012 .request_adapter(&wgpu::RequestAdapterOptions {
1013 power_preference: wgpu::PowerPreference::HighPerformance,
1014 compatible_surface: None,
1015 force_fallback_adapter: false,
1016 })
1017 .await
1018 .ok_or(HEADLESS_GPU_ADAPTER_UNAVAILABLE)?;
1019 let (device, queue) = adapter
1020 .request_device(&crate::wgpu_compat::default_device_descriptor(), None)
1021 .await
1022 .map_err(|err| format!("{HEADLESS_GPU_DEVICE_CREATION_FAILED_PREFIX} {err}"))?;
1023 let context =
1024 NativeSurfaceRenderContext::new(Arc::new(device), Arc::new(queue), width, height, format)
1025 .await?;
1026 log::debug!(
1027 "runmat-plot: native_surface.headless_context.ready width={} height={}",
1028 width,
1029 height
1030 );
1031 Ok(context)
1032}
1033
1034pub fn is_headless_gpu_unavailable_error(err: &str) -> bool {
1035 err.contains(HEADLESS_GPU_ADAPTER_UNAVAILABLE)
1036 || err.contains(HEADLESS_GPU_DEVICE_CREATION_FAILED_PREFIX)
1037 || err.contains(HEADLESS_GPU_CONTEXT_PANICKED_PREFIX)
1038}
1039
1040fn panic_payload_to_string(payload: Box<dyn Any + Send>) -> String {
1041 if let Some(message) = payload.downcast_ref::<&str>() {
1042 (*message).to_string()
1043 } else if let Some(message) = payload.downcast_ref::<String>() {
1044 message.clone()
1045 } else {
1046 "unknown panic payload".to_string()
1047 }
1048}
1049
1050pub async fn render_figure_rgba_bytes_interactive_with_camera(
1051 figure: Figure,
1052 width: u32,
1053 height: u32,
1054 camera: &Camera,
1055) -> Result<Vec<u8>, String> {
1056 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1057 context.render_to_rgba(&figure, Some(camera), None).await
1058}
1059
1060pub async fn render_figure_rgba_bytes_interactive_with_camera_and_theme(
1061 figure: Figure,
1062 width: u32,
1063 height: u32,
1064 camera: &Camera,
1065 theme: PlotThemeConfig,
1066) -> Result<Vec<u8>, String> {
1067 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1068 context.set_theme_config(theme);
1069 context.render_to_rgba(&figure, Some(camera), None).await
1070}
1071
1072pub async fn render_figure_rgba_bytes_interactive_with_axes_cameras(
1073 figure: Figure,
1074 width: u32,
1075 height: u32,
1076 axes_cameras: &[Camera],
1077) -> Result<Vec<u8>, String> {
1078 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1079 context
1080 .render_to_rgba(&figure, None, Some(axes_cameras))
1081 .await
1082}
1083
1084pub async fn render_figure_rgba_bytes_interactive_with_axes_cameras_and_theme(
1085 figure: Figure,
1086 width: u32,
1087 height: u32,
1088 axes_cameras: &[Camera],
1089 theme: PlotThemeConfig,
1090) -> Result<Vec<u8>, String> {
1091 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1092 context.set_theme_config(theme);
1093 context
1094 .render_to_rgba(&figure, None, Some(axes_cameras))
1095 .await
1096}
1097
1098pub async fn render_figure_rgba_bytes_interactive(
1099 figure: Figure,
1100 width: u32,
1101 height: u32,
1102) -> Result<Vec<u8>, String> {
1103 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1104 context.render_to_rgba(&figure, None, None).await
1105}
1106
1107pub async fn render_figure_rgba_bytes_interactive_and_theme(
1108 figure: Figure,
1109 width: u32,
1110 height: u32,
1111 theme: PlotThemeConfig,
1112) -> Result<Vec<u8>, String> {
1113 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1114 context.set_theme_config(theme);
1115 context.render_to_rgba(&figure, None, None).await
1116}
1117
1118pub async fn render_geometry_scene_rgba_bytes_interactive_with_camera_and_theme(
1119 scene: GeometryScene,
1120 width: u32,
1121 height: u32,
1122 camera: &Camera,
1123 theme: PlotThemeConfig,
1124) -> Result<Vec<u8>, String> {
1125 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1126 context.set_theme_config(theme);
1127 context
1128 .render_geometry_scene_to_rgba(&scene, Some(camera))
1129 .await
1130}
1131
1132pub async fn render_geometry_scene_rgba_bytes_interactive_and_theme(
1133 scene: GeometryScene,
1134 width: u32,
1135 height: u32,
1136 theme: PlotThemeConfig,
1137) -> Result<Vec<u8>, String> {
1138 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1139 context.set_theme_config(theme);
1140 context.render_geometry_scene_to_rgba(&scene, None).await
1141}
1142
1143pub async fn render_geometry_scene_png_bytes_interactive_with_camera_and_theme(
1144 scene: GeometryScene,
1145 width: u32,
1146 height: u32,
1147 camera: &Camera,
1148 theme: PlotThemeConfig,
1149) -> Result<Vec<u8>, String> {
1150 let rgba = render_geometry_scene_rgba_bytes_interactive_with_camera_and_theme(
1151 scene, width, height, camera, theme,
1152 )
1153 .await?;
1154 encode_png_bytes(width.max(1), height.max(1), &rgba)
1155}
1156
1157pub async fn render_geometry_scene_png_bytes_interactive_and_theme(
1158 scene: GeometryScene,
1159 width: u32,
1160 height: u32,
1161 theme: PlotThemeConfig,
1162) -> Result<Vec<u8>, String> {
1163 let rgba =
1164 render_geometry_scene_rgba_bytes_interactive_and_theme(scene, width, height, theme).await?;
1165 encode_png_bytes(width.max(1), height.max(1), &rgba)
1166}
1167
1168fn encode_png_bytes(width: u32, height: u32, rgba: &[u8]) -> Result<Vec<u8>, String> {
1169 use image::{ImageBuffer, ImageFormat, Rgba};
1170
1171 let mut opaque = rgba.to_vec();
1172 for pixel in opaque.chunks_exact_mut(4) {
1173 pixel[3] = 255;
1174 }
1175
1176 let image = ImageBuffer::<Rgba<u8>, _>::from_raw(width, height, opaque)
1177 .ok_or_else(|| "Failed to create image buffer for PNG encoding".to_string())?;
1178 let mut out = std::io::Cursor::new(Vec::new());
1179 image
1180 .write_to(&mut out, ImageFormat::Png)
1181 .map_err(|err| format!("Failed to encode PNG bytes: {err}"))?;
1182 Ok(out.into_inner())
1183}
1184
1185pub async fn render_figure_png_bytes_interactive(
1186 figure: Figure,
1187 width: u32,
1188 height: u32,
1189) -> Result<Vec<u8>, String> {
1190 let rgba = render_figure_rgba_bytes_interactive(figure, width, height).await?;
1191 encode_png_bytes(width.max(1), height.max(1), &rgba)
1192}
1193
1194pub async fn render_figure_png_bytes_interactive_and_theme(
1195 figure: Figure,
1196 width: u32,
1197 height: u32,
1198 theme: PlotThemeConfig,
1199) -> Result<Vec<u8>, String> {
1200 let rgba = render_figure_rgba_bytes_interactive_and_theme(figure, width, height, theme).await?;
1201 encode_png_bytes(width.max(1), height.max(1), &rgba)
1202}
1203
1204pub async fn render_figure_png_bytes_interactive_and_theme_and_textmark(
1205 figure: Figure,
1206 width: u32,
1207 height: u32,
1208 theme: PlotThemeConfig,
1209 textmark: Option<&str>,
1210) -> Result<Vec<u8>, String> {
1211 log::debug!(
1212 "runmat-plot: render_figure_png_bytes_interactive_and_theme_and_textmark.start width={} height={} axes={} textmark={}",
1213 width,
1214 height,
1215 figure.axes_metadata.len(),
1216 textmark.unwrap_or("")
1217 );
1218 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1219 log::debug!(
1220 "runmat-plot: render_figure_png_bytes_interactive_and_theme_and_textmark.context_ready width={} height={}",
1221 width.max(1),
1222 height.max(1)
1223 );
1224 context.set_theme_config(theme);
1225 context.set_textmark(textmark);
1226 let rgba = context.render_to_rgba(&figure, None, None).await?;
1227 log::debug!(
1228 "runmat-plot: render_figure_png_bytes_interactive_and_theme_and_textmark.rgba_ready bytes={}",
1229 rgba.len()
1230 );
1231 encode_png_bytes(width.max(1), height.max(1), &rgba)
1232}
1233
1234pub async fn render_figure_png_bytes_interactive_with_camera(
1235 figure: Figure,
1236 width: u32,
1237 height: u32,
1238 camera: &Camera,
1239) -> Result<Vec<u8>, String> {
1240 let rgba =
1241 render_figure_rgba_bytes_interactive_with_camera(figure, width, height, camera).await?;
1242 encode_png_bytes(width.max(1), height.max(1), &rgba)
1243}
1244
1245pub async fn render_figure_png_bytes_interactive_with_camera_and_theme(
1246 figure: Figure,
1247 width: u32,
1248 height: u32,
1249 camera: &Camera,
1250 theme: PlotThemeConfig,
1251) -> Result<Vec<u8>, String> {
1252 let rgba = render_figure_rgba_bytes_interactive_with_camera_and_theme(
1253 figure, width, height, camera, theme,
1254 )
1255 .await?;
1256 encode_png_bytes(width.max(1), height.max(1), &rgba)
1257}
1258
1259pub async fn render_figure_png_bytes_interactive_with_camera_and_theme_and_textmark(
1260 figure: Figure,
1261 width: u32,
1262 height: u32,
1263 camera: &Camera,
1264 theme: PlotThemeConfig,
1265 textmark: Option<&str>,
1266) -> Result<Vec<u8>, String> {
1267 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1268 context.set_theme_config(theme);
1269 context.set_textmark(textmark);
1270 let rgba = context.render_to_rgba(&figure, Some(camera), None).await?;
1271 encode_png_bytes(width.max(1), height.max(1), &rgba)
1272}
1273
1274pub async fn render_figure_png_bytes_interactive_with_axes_cameras(
1275 figure: Figure,
1276 width: u32,
1277 height: u32,
1278 axes_cameras: &[Camera],
1279) -> Result<Vec<u8>, String> {
1280 let rgba =
1281 render_figure_rgba_bytes_interactive_with_axes_cameras(figure, width, height, axes_cameras)
1282 .await?;
1283 encode_png_bytes(width.max(1), height.max(1), &rgba)
1284}
1285
1286pub async fn render_figure_png_bytes_interactive_with_axes_cameras_and_theme(
1287 figure: Figure,
1288 width: u32,
1289 height: u32,
1290 axes_cameras: &[Camera],
1291 theme: PlotThemeConfig,
1292) -> Result<Vec<u8>, String> {
1293 let rgba = render_figure_rgba_bytes_interactive_with_axes_cameras_and_theme(
1294 figure,
1295 width,
1296 height,
1297 axes_cameras,
1298 theme,
1299 )
1300 .await?;
1301 encode_png_bytes(width.max(1), height.max(1), &rgba)
1302}
1303
1304pub async fn render_figure_png_bytes_interactive_with_axes_cameras_and_theme_and_textmark(
1305 figure: Figure,
1306 width: u32,
1307 height: u32,
1308 axes_cameras: &[Camera],
1309 theme: PlotThemeConfig,
1310 textmark: Option<&str>,
1311) -> Result<Vec<u8>, String> {
1312 log::debug!(
1313 "runmat-plot: render_figure_png_bytes_interactive_with_axes_cameras_and_theme_and_textmark.start width={} height={} axes={} camera_overrides={} textmark={}",
1314 width,
1315 height,
1316 figure.axes_metadata.len(),
1317 axes_cameras.len(),
1318 textmark.unwrap_or("")
1319 );
1320 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1321 log::debug!(
1322 "runmat-plot: render_figure_png_bytes_interactive_with_axes_cameras_and_theme_and_textmark.context_ready width={} height={}",
1323 width.max(1),
1324 height.max(1)
1325 );
1326 context.set_theme_config(theme);
1327 context.set_textmark(textmark);
1328 let rgba = context
1329 .render_to_rgba(&figure, None, Some(axes_cameras))
1330 .await?;
1331 log::debug!(
1332 "runmat-plot: render_figure_png_bytes_interactive_with_axes_cameras_and_theme_and_textmark.rgba_ready bytes={}",
1333 rgba.len()
1334 );
1335 encode_png_bytes(width.max(1), height.max(1), &rgba)
1336}
1337
1338#[cfg(feature = "egui-overlay")]
1339fn apply_cad_overlay_actions(
1340 renderer: &mut PlotRenderer,
1341 actions: crate::overlay::cad_overlay::CadOverlayActions,
1342) -> Vec<NativeSurfaceHostAction> {
1343 let mut host_actions = Vec::new();
1344 if actions.reset_view {
1345 renderer.reset_geometry_view();
1346 }
1347 if actions.create_fea_study {
1348 host_actions.push(NativeSurfaceHostAction::CreateFeaStudy);
1349 }
1350 if let Some(preset) = actions.view_preset {
1351 renderer.set_camera_view_preset(preset);
1352 }
1353 if let Some(enabled) = actions.grid_enabled {
1354 renderer.set_overlay_grid_enabled(enabled);
1355 }
1356 if let Some(enabled) = actions.xray_enabled {
1357 renderer.set_geometry_xray_enabled(enabled);
1358 }
1359 for (owner_id, visible) in actions.owner_visibility {
1360 renderer.set_geometry_owner_visible(owner_id, visible);
1361 }
1362 host_actions
1363}
1364
1365#[cfg(test)]
1366mod tests {
1367 use super::*;
1368
1369 #[test]
1370 fn headless_gpu_panic_errors_are_fallback_eligible() {
1371 assert!(is_headless_gpu_unavailable_error(
1372 "Headless GPU context creation panicked: called `Option::unwrap()` on a `None` value"
1373 ));
1374 }
1375}