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 font_scale: 1.25,
620 show_axes: true,
621 show_title: self.renderer.geometry_overlay().is_none(),
622 title: self
623 .renderer
624 .overlay_title()
625 .cloned()
626 .or(Some("Plot".to_string())),
627 x_label: self
628 .renderer
629 .overlay_x_label()
630 .cloned()
631 .or(Some("X".to_string())),
632 y_label: self
633 .renderer
634 .overlay_y_label()
635 .cloned()
636 .or(Some("Y".to_string())),
637 show_sidebar: false,
638 ..Default::default()
639 };
640 let overlay_metrics = OverlayMetrics {
641 vertex_count: scene_stats.total_vertices,
642 triangle_count: scene_stats.total_triangles,
643 render_time_ms: 0.0,
644 fps: 60.0,
645 };
646 let frame_info = overlay.plot_overlay.render(
647 ctx,
648 &self.renderer,
649 &overlay_config,
650 overlay_metrics,
651 );
652 if let Some(textmark) = self.textmark.as_deref() {
653 let screen = ctx.screen_rect();
654 let anchor = egui::pos2(screen.max.x - 8.0, screen.max.y - 6.0);
655 let font =
656 egui::FontId::proportional(11.0 * overlay_config.font_scale.max(0.8));
657 let layer = egui::LayerId::new(
658 egui::Order::Foreground,
659 egui::Id::new("runmat_export_textmark"),
660 );
661 let painter = ctx.layer_painter(layer);
662 painter.text(
663 anchor + egui::vec2(1.0, 1.0),
664 egui::Align2::RIGHT_BOTTOM,
665 textmark,
666 font.clone(),
667 egui::Color32::from_rgba_premultiplied(0, 0, 0, 72),
668 );
669 painter.text(
670 anchor,
671 egui::Align2::RIGHT_BOTTOM,
672 textmark,
673 font,
674 egui::Color32::from_rgba_premultiplied(226, 234, 245, 96),
675 );
676 }
677 plot_area_points = frame_info.plot_area;
678 });
679
680 let cad_actions = overlay.plot_overlay.take_cad_actions();
681 self.host_actions
682 .extend(apply_cad_overlay_actions(&mut self.renderer, cad_actions));
683 overlay.wants_pointer_input = overlay.plot_overlay.overlay_pointer_captured();
684 overlay.capture_regions_px = overlay
685 .plot_overlay
686 .overlay_capture_regions()
687 .into_iter()
688 .map(|[x0, y0, x1, y1]| [x0 * ppp, y0 * ppp, x1 * ppp, y1 * ppp])
689 .collect();
690
691 let paint_jobs = overlay
692 .egui_ctx
693 .tessellate(full_output.shapes, full_output.pixels_per_point);
694 for (id, image_delta) in &full_output.textures_delta.set {
695 overlay.egui_renderer.update_texture(
696 &self.renderer.wgpu_renderer.device,
697 &self.renderer.wgpu_renderer.queue,
698 *id,
699 image_delta,
700 );
701 }
702
703 let screen_descriptor = ScreenDescriptor {
704 size_in_pixels: [self.config.width.max(1), self.config.height.max(1)],
705 pixels_per_point: full_output.pixels_per_point,
706 };
707 overlay.egui_renderer.update_buffers(
708 &self.renderer.wgpu_renderer.device,
709 &self.renderer.wgpu_renderer.queue,
710 encoder,
711 &paint_jobs,
712 &screen_descriptor,
713 );
714
715 let (vx, vy, vw, vh) = if let Some(rect) = plot_area_points {
716 let vx = (rect.min.x * ppp).round().max(0.0) as u32;
717 let vy = (rect.min.y * ppp).round().max(0.0) as u32;
718 let vw = (rect.width() * ppp).round().max(1.0) as u32;
719 let vh = (rect.height() * ppp).round().max(1.0) as u32;
720 (vx, vy, vw, vh)
721 } else {
722 (0, 0, self.config.width.max(1), self.config.height.max(1))
723 };
724 let max_w = self.config.width.max(1);
725 let max_h = self.config.height.max(1);
726 let vx = vx.min(max_w.saturating_sub(1));
727 let vy = vy.min(max_h.saturating_sub(1));
728 let vw = vw.max(1).min(max_w.saturating_sub(vx).max(1));
729 let vh = vh.max(1).min(max_h.saturating_sub(vy).max(1));
730 let mut axes_viewports_px = vec![(vx, vy, vw, vh)];
731
732 if vw > 0 && vh > 0 {
733 self.renderer
734 .camera_mut()
735 .update_aspect_ratio((vw as f32) / (vh as f32));
736 }
737
738 let (rows, cols) = self.renderer.figure_axes_grid();
739 log::debug!(
740 "runmat-plot: native_surface.render_scene_with_overlay.axes_grid rows={} cols={} plot_area_present={}",
741 rows,
742 cols,
743 plot_area_points.is_some()
744 );
745 if rows * cols > 1 {
746 log::debug!(
747 "runmat-plot: native_surface.render_scene_with_overlay.branch_subplot_axes"
748 );
749 let rect_points = plot_area_points.unwrap_or_else(|| {
750 egui::Rect::from_min_size(
751 egui::Pos2::new(0.0, 0.0),
752 egui::Vec2::new(
753 (self.config.width.max(1) as f32) / ppp,
754 (self.config.height.max(1) as f32) / ppp,
755 ),
756 )
757 });
758 let existing_rect_count = overlay.plot_overlay.axes_plot_rects().len();
759 log::debug!(
760 "runmat-plot: native_surface.render_scene_with_overlay.subplot_rect_source rows={} cols={} existing_rects={} expected_rects={}",
761 rows,
762 cols,
763 existing_rect_count,
764 rows * cols
765 );
766 let rects = if overlay.plot_overlay.axes_plot_rects().len() == rows * cols {
767 log::debug!(
768 "runmat-plot: native_surface.render_scene_with_overlay.subplot_rect_source_existing rects={}",
769 existing_rect_count
770 );
771 overlay.plot_overlay.axes_plot_rects().to_vec()
772 } else {
773 log::debug!(
774 "runmat-plot: native_surface.render_scene_with_overlay.subplot_rect_source_compute rect_points=({}, {})..({}, {})",
775 rect_points.min.x,
776 rect_points.min.y,
777 rect_points.max.x,
778 rect_points.max.y
779 );
780 overlay.plot_overlay.compute_subplot_plot_rects_snapped(
781 rect_points,
782 &self.renderer,
783 1.0,
784 ppp,
785 )
786 };
787 log::debug!(
788 "runmat-plot: native_surface.render_scene_with_overlay.subplot_rects_ready rects={}",
789 rects.len()
790 );
791 let sw = self.config.width as f32;
792 let sh = self.config.height as f32;
793 let mut viewports: Vec<(u32, u32, u32, u32)> = Vec::with_capacity(rects.len());
794 for (rect_index, r) in rects.into_iter().enumerate() {
795 log::debug!(
796 "runmat-plot: native_surface.render_scene_with_overlay.subplot_rect viewport_index={} rect=({}, {})..({}, {})",
797 rect_index,
798 r.min.x,
799 r.min.y,
800 r.max.x,
801 r.max.y
802 );
803 let mut rx = (r.min.x * ppp).round().max(0.0);
804 let mut ry = (r.min.y * ppp).round().max(0.0);
805 let mut rw = (r.width() * ppp).round().max(1.0);
806 let mut rh = (r.height() * ppp).round().max(1.0);
807 if rx >= sw {
808 rx = (sw - 1.0).max(0.0);
809 }
810 if ry >= sh {
811 ry = (sh - 1.0).max(0.0);
812 }
813 if rx + rw > sw {
814 rw = (sw - rx).max(1.0);
815 }
816 if ry + rh > sh {
817 rh = (sh - ry).max(1.0);
818 }
819 viewports.push((rx as u32, ry as u32, rw as u32, rh as u32));
820 log::debug!(
821 "runmat-plot: native_surface.render_scene_with_overlay.subplot_viewport viewport_index={} viewport=({}, {}, {}, {})",
822 rect_index,
823 rx as u32,
824 ry as u32,
825 rw as u32,
826 rh as u32
827 );
828 }
829 log::debug!(
830 "runmat-plot: native_surface.render_scene_with_overlay.subplot_viewports_ready count={}",
831 viewports.len()
832 );
833 axes_viewports_px = viewports.clone();
834 let axes_plot_sizes_px: Vec<(u32, u32)> = viewports
835 .iter()
836 .map(|&(_, _, w, h)| (w.max(1), h.max(1)))
837 .collect();
838 self.renderer
839 .ensure_scene_viewport_dependent_geometry_for_axes(&axes_plot_sizes_px);
840 self.renderer
841 .render_axes_to_viewports(
842 encoder,
843 target_view,
844 &viewports,
845 self.config.msaa_samples.max(1),
846 &self.config,
847 )
848 .map_err(|err| format!("native surface subplot render failed: {err}"))?;
849 log::debug!(
850 "runmat-plot: native_surface.render_scene_with_overlay.subplot_render_ok viewports={}",
851 viewports.len()
852 );
853 } else {
854 log::debug!(
855 "runmat-plot: native_surface.render_scene_with_overlay.branch_single_axes"
856 );
857 {
858 let clear_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
859 label: Some("runmat-native-single-axes-clear"),
860 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
861 view: target_view,
862 resolve_target: None,
863 ops: wgpu::Operations {
864 load: wgpu::LoadOp::Clear(wgpu::Color {
865 r: self.config.background_color.x as f64,
866 g: self.config.background_color.y as f64,
867 b: self.config.background_color.z as f64,
868 a: self.config.background_color.w as f64,
869 }),
870 store: wgpu::StoreOp::Store,
871 },
872 })],
873 depth_stencil_attachment: None,
874 timestamp_writes: None,
875 occlusion_query_set: None,
876 });
877 drop(clear_pass);
878 }
879
880 let mut cfg = self.config.clone();
881 cfg.width = vw.max(1);
882 cfg.height = vh.max(1);
883 let cam = self
884 .renderer
885 .axes_camera(0)
886 .cloned()
887 .unwrap_or_else(|| self.renderer.camera().clone());
888 let axes_plot_sizes_px = vec![(vw.max(1), vh.max(1))];
889 self.renderer
890 .ensure_scene_viewport_dependent_geometry_for_axes(&axes_plot_sizes_px);
891 self.renderer
892 .render_camera_to_viewport(
893 encoder,
894 target_view,
895 (vx, vy, vw, vh),
896 &cfg,
897 &cam,
898 0,
899 true,
900 )
901 .map_err(|err| format!("native surface viewport render failed: {err}"))?;
902 log::debug!(
903 "runmat-plot: native_surface.render_scene_with_overlay.single_axes_render_ok"
904 );
905 }
906
907 {
908 let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
909 label: Some("runmat-native-egui-overlay"),
910 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
911 view: target_view,
912 resolve_target: None,
913 ops: wgpu::Operations {
914 load: wgpu::LoadOp::Load,
915 store: wgpu::StoreOp::Store,
916 },
917 })],
918 depth_stencil_attachment: None,
919 timestamp_writes: None,
920 occlusion_query_set: None,
921 });
922 #[cfg(target_arch = "wasm32")]
923 let mut render_pass = render_pass.forget_lifetime();
924 #[cfg(not(target_arch = "wasm32"))]
925 let mut render_pass = render_pass;
926 overlay
927 .egui_renderer
928 .render(&mut render_pass, &paint_jobs, &screen_descriptor);
929 log::debug!(
930 "runmat-plot: native_surface.render_scene_with_overlay.overlay_ok paint_jobs={} textures_set={} textures_free={}",
931 paint_jobs.len(),
932 full_output.textures_delta.set.len(),
933 full_output.textures_delta.free.len()
934 );
935 }
936
937 for id in &full_output.textures_delta.free {
938 overlay.egui_renderer.free_texture(id);
939 }
940
941 Ok(RenderResult {
942 success: true,
943 data_bounds: self.renderer.data_bounds(),
944 axes_viewports_px,
945 vertex_count: scene_stats.total_vertices,
946 triangle_count: scene_stats.total_triangles,
947 render_time_ms: start_time.elapsed().as_secs_f64() * 1000.0,
948 })
949 }
950
951 #[cfg(not(feature = "egui-overlay"))]
952 {
953 self.renderer
954 .render_scene_to_target(encoder, target_view, &self.config)
955 .map_err(|err| format!("native surface render failed: {err}"))
956 }
957 }
958
959 fn apply_background_policy(&mut self) {
960 self.config.background_color = match self.background_policy {
961 BackgroundPolicy::ThemeDriven => {
962 self.renderer.theme.build_theme().get_background_color()
963 }
964 BackgroundPolicy::Explicit(color) => color,
965 };
966 }
967}
968
969fn is_default_figure_bg(bg: glam::Vec4) -> bool {
970 const EPS: f32 = 1e-3;
971 (bg.x - 1.0).abs() <= EPS
972 && (bg.y - 1.0).abs() <= EPS
973 && (bg.z - 1.0).abs() <= EPS
974 && (bg.w - 1.0).abs() <= EPS
975}
976
977async fn create_headless_context(
978 width: u32,
979 height: u32,
980) -> Result<NativeSurfaceRenderContext, String> {
981 let format = wgpu::TextureFormat::Rgba8Unorm;
985 if let Some(ctx) = crate::context::shared_wgpu_context() {
986 log::debug!(
987 "runmat-plot: native_surface.headless_context.branch_shared_context width={} height={}",
988 width,
989 height
990 );
991 return NativeSurfaceRenderContext::new(ctx.device, ctx.queue, width, height, format).await;
992 }
993
994 log::debug!(
995 "runmat-plot: native_surface.headless_context.branch_dedicated_context width={} height={}",
996 width,
997 height
998 );
999
1000 let instance = panic::catch_unwind(|| {
1001 crate::wgpu_compat::instance_new(wgpu::InstanceDescriptor::default())
1002 })
1003 .map_err(|payload| {
1004 format!(
1005 "{HEADLESS_GPU_CONTEXT_PANICKED_PREFIX} {}",
1006 panic_payload_to_string(payload)
1007 )
1008 })?;
1009 let adapter = instance
1010 .request_adapter(&wgpu::RequestAdapterOptions {
1011 power_preference: wgpu::PowerPreference::HighPerformance,
1012 compatible_surface: None,
1013 force_fallback_adapter: false,
1014 })
1015 .await
1016 .ok_or(HEADLESS_GPU_ADAPTER_UNAVAILABLE)?;
1017 let (device, queue) = adapter
1018 .request_device(&crate::wgpu_compat::default_device_descriptor(), None)
1019 .await
1020 .map_err(|err| format!("{HEADLESS_GPU_DEVICE_CREATION_FAILED_PREFIX} {err}"))?;
1021 let context =
1022 NativeSurfaceRenderContext::new(Arc::new(device), Arc::new(queue), width, height, format)
1023 .await?;
1024 log::debug!(
1025 "runmat-plot: native_surface.headless_context.ready width={} height={}",
1026 width,
1027 height
1028 );
1029 Ok(context)
1030}
1031
1032pub fn is_headless_gpu_unavailable_error(err: &str) -> bool {
1033 err.contains(HEADLESS_GPU_ADAPTER_UNAVAILABLE)
1034 || err.contains(HEADLESS_GPU_DEVICE_CREATION_FAILED_PREFIX)
1035 || err.contains(HEADLESS_GPU_CONTEXT_PANICKED_PREFIX)
1036}
1037
1038fn panic_payload_to_string(payload: Box<dyn Any + Send>) -> String {
1039 if let Some(message) = payload.downcast_ref::<&str>() {
1040 (*message).to_string()
1041 } else if let Some(message) = payload.downcast_ref::<String>() {
1042 message.clone()
1043 } else {
1044 "unknown panic payload".to_string()
1045 }
1046}
1047
1048pub async fn render_figure_rgba_bytes_interactive_with_camera(
1049 figure: Figure,
1050 width: u32,
1051 height: u32,
1052 camera: &Camera,
1053) -> Result<Vec<u8>, String> {
1054 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1055 context.render_to_rgba(&figure, Some(camera), None).await
1056}
1057
1058pub async fn render_figure_rgba_bytes_interactive_with_camera_and_theme(
1059 figure: Figure,
1060 width: u32,
1061 height: u32,
1062 camera: &Camera,
1063 theme: PlotThemeConfig,
1064) -> Result<Vec<u8>, String> {
1065 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1066 context.set_theme_config(theme);
1067 context.render_to_rgba(&figure, Some(camera), None).await
1068}
1069
1070pub async fn render_figure_rgba_bytes_interactive_with_axes_cameras(
1071 figure: Figure,
1072 width: u32,
1073 height: u32,
1074 axes_cameras: &[Camera],
1075) -> Result<Vec<u8>, String> {
1076 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1077 context
1078 .render_to_rgba(&figure, None, Some(axes_cameras))
1079 .await
1080}
1081
1082pub async fn render_figure_rgba_bytes_interactive_with_axes_cameras_and_theme(
1083 figure: Figure,
1084 width: u32,
1085 height: u32,
1086 axes_cameras: &[Camera],
1087 theme: PlotThemeConfig,
1088) -> Result<Vec<u8>, String> {
1089 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1090 context.set_theme_config(theme);
1091 context
1092 .render_to_rgba(&figure, None, Some(axes_cameras))
1093 .await
1094}
1095
1096pub async fn render_figure_rgba_bytes_interactive(
1097 figure: Figure,
1098 width: u32,
1099 height: u32,
1100) -> Result<Vec<u8>, String> {
1101 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1102 context.render_to_rgba(&figure, None, None).await
1103}
1104
1105pub async fn render_figure_rgba_bytes_interactive_and_theme(
1106 figure: Figure,
1107 width: u32,
1108 height: u32,
1109 theme: PlotThemeConfig,
1110) -> Result<Vec<u8>, String> {
1111 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1112 context.set_theme_config(theme);
1113 context.render_to_rgba(&figure, None, None).await
1114}
1115
1116pub async fn render_geometry_scene_rgba_bytes_interactive_with_camera_and_theme(
1117 scene: GeometryScene,
1118 width: u32,
1119 height: u32,
1120 camera: &Camera,
1121 theme: PlotThemeConfig,
1122) -> Result<Vec<u8>, String> {
1123 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1124 context.set_theme_config(theme);
1125 context
1126 .render_geometry_scene_to_rgba(&scene, Some(camera))
1127 .await
1128}
1129
1130pub async fn render_geometry_scene_rgba_bytes_interactive_and_theme(
1131 scene: GeometryScene,
1132 width: u32,
1133 height: u32,
1134 theme: PlotThemeConfig,
1135) -> Result<Vec<u8>, String> {
1136 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1137 context.set_theme_config(theme);
1138 context.render_geometry_scene_to_rgba(&scene, None).await
1139}
1140
1141pub async fn render_geometry_scene_png_bytes_interactive_with_camera_and_theme(
1142 scene: GeometryScene,
1143 width: u32,
1144 height: u32,
1145 camera: &Camera,
1146 theme: PlotThemeConfig,
1147) -> Result<Vec<u8>, String> {
1148 let rgba = render_geometry_scene_rgba_bytes_interactive_with_camera_and_theme(
1149 scene, width, height, camera, theme,
1150 )
1151 .await?;
1152 encode_png_bytes(width.max(1), height.max(1), &rgba)
1153}
1154
1155pub async fn render_geometry_scene_png_bytes_interactive_and_theme(
1156 scene: GeometryScene,
1157 width: u32,
1158 height: u32,
1159 theme: PlotThemeConfig,
1160) -> Result<Vec<u8>, String> {
1161 let rgba =
1162 render_geometry_scene_rgba_bytes_interactive_and_theme(scene, width, height, theme).await?;
1163 encode_png_bytes(width.max(1), height.max(1), &rgba)
1164}
1165
1166fn encode_png_bytes(width: u32, height: u32, rgba: &[u8]) -> Result<Vec<u8>, String> {
1167 use image::{ImageBuffer, ImageFormat, Rgba};
1168
1169 let mut opaque = rgba.to_vec();
1170 for pixel in opaque.chunks_exact_mut(4) {
1171 pixel[3] = 255;
1172 }
1173
1174 let image = ImageBuffer::<Rgba<u8>, _>::from_raw(width, height, opaque)
1175 .ok_or_else(|| "Failed to create image buffer for PNG encoding".to_string())?;
1176 let mut out = std::io::Cursor::new(Vec::new());
1177 image
1178 .write_to(&mut out, ImageFormat::Png)
1179 .map_err(|err| format!("Failed to encode PNG bytes: {err}"))?;
1180 Ok(out.into_inner())
1181}
1182
1183pub async fn render_figure_png_bytes_interactive(
1184 figure: Figure,
1185 width: u32,
1186 height: u32,
1187) -> Result<Vec<u8>, String> {
1188 let rgba = render_figure_rgba_bytes_interactive(figure, width, height).await?;
1189 encode_png_bytes(width.max(1), height.max(1), &rgba)
1190}
1191
1192pub async fn render_figure_png_bytes_interactive_and_theme(
1193 figure: Figure,
1194 width: u32,
1195 height: u32,
1196 theme: PlotThemeConfig,
1197) -> Result<Vec<u8>, String> {
1198 let rgba = render_figure_rgba_bytes_interactive_and_theme(figure, width, height, theme).await?;
1199 encode_png_bytes(width.max(1), height.max(1), &rgba)
1200}
1201
1202pub async fn render_figure_png_bytes_interactive_and_theme_and_textmark(
1203 figure: Figure,
1204 width: u32,
1205 height: u32,
1206 theme: PlotThemeConfig,
1207 textmark: Option<&str>,
1208) -> Result<Vec<u8>, String> {
1209 log::debug!(
1210 "runmat-plot: render_figure_png_bytes_interactive_and_theme_and_textmark.start width={} height={} axes={} textmark={}",
1211 width,
1212 height,
1213 figure.axes_metadata.len(),
1214 textmark.unwrap_or("")
1215 );
1216 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1217 log::debug!(
1218 "runmat-plot: render_figure_png_bytes_interactive_and_theme_and_textmark.context_ready width={} height={}",
1219 width.max(1),
1220 height.max(1)
1221 );
1222 context.set_theme_config(theme);
1223 context.set_textmark(textmark);
1224 let rgba = context.render_to_rgba(&figure, None, None).await?;
1225 log::debug!(
1226 "runmat-plot: render_figure_png_bytes_interactive_and_theme_and_textmark.rgba_ready bytes={}",
1227 rgba.len()
1228 );
1229 encode_png_bytes(width.max(1), height.max(1), &rgba)
1230}
1231
1232pub async fn render_figure_png_bytes_interactive_with_camera(
1233 figure: Figure,
1234 width: u32,
1235 height: u32,
1236 camera: &Camera,
1237) -> Result<Vec<u8>, String> {
1238 let rgba =
1239 render_figure_rgba_bytes_interactive_with_camera(figure, width, height, camera).await?;
1240 encode_png_bytes(width.max(1), height.max(1), &rgba)
1241}
1242
1243pub async fn render_figure_png_bytes_interactive_with_camera_and_theme(
1244 figure: Figure,
1245 width: u32,
1246 height: u32,
1247 camera: &Camera,
1248 theme: PlotThemeConfig,
1249) -> Result<Vec<u8>, String> {
1250 let rgba = render_figure_rgba_bytes_interactive_with_camera_and_theme(
1251 figure, width, height, camera, theme,
1252 )
1253 .await?;
1254 encode_png_bytes(width.max(1), height.max(1), &rgba)
1255}
1256
1257pub async fn render_figure_png_bytes_interactive_with_camera_and_theme_and_textmark(
1258 figure: Figure,
1259 width: u32,
1260 height: u32,
1261 camera: &Camera,
1262 theme: PlotThemeConfig,
1263 textmark: Option<&str>,
1264) -> Result<Vec<u8>, String> {
1265 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1266 context.set_theme_config(theme);
1267 context.set_textmark(textmark);
1268 let rgba = context.render_to_rgba(&figure, Some(camera), None).await?;
1269 encode_png_bytes(width.max(1), height.max(1), &rgba)
1270}
1271
1272pub async fn render_figure_png_bytes_interactive_with_axes_cameras(
1273 figure: Figure,
1274 width: u32,
1275 height: u32,
1276 axes_cameras: &[Camera],
1277) -> Result<Vec<u8>, String> {
1278 let rgba =
1279 render_figure_rgba_bytes_interactive_with_axes_cameras(figure, width, height, axes_cameras)
1280 .await?;
1281 encode_png_bytes(width.max(1), height.max(1), &rgba)
1282}
1283
1284pub async fn render_figure_png_bytes_interactive_with_axes_cameras_and_theme(
1285 figure: Figure,
1286 width: u32,
1287 height: u32,
1288 axes_cameras: &[Camera],
1289 theme: PlotThemeConfig,
1290) -> Result<Vec<u8>, String> {
1291 let rgba = render_figure_rgba_bytes_interactive_with_axes_cameras_and_theme(
1292 figure,
1293 width,
1294 height,
1295 axes_cameras,
1296 theme,
1297 )
1298 .await?;
1299 encode_png_bytes(width.max(1), height.max(1), &rgba)
1300}
1301
1302pub async fn render_figure_png_bytes_interactive_with_axes_cameras_and_theme_and_textmark(
1303 figure: Figure,
1304 width: u32,
1305 height: u32,
1306 axes_cameras: &[Camera],
1307 theme: PlotThemeConfig,
1308 textmark: Option<&str>,
1309) -> Result<Vec<u8>, String> {
1310 log::debug!(
1311 "runmat-plot: render_figure_png_bytes_interactive_with_axes_cameras_and_theme_and_textmark.start width={} height={} axes={} camera_overrides={} textmark={}",
1312 width,
1313 height,
1314 figure.axes_metadata.len(),
1315 axes_cameras.len(),
1316 textmark.unwrap_or("")
1317 );
1318 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1319 log::debug!(
1320 "runmat-plot: render_figure_png_bytes_interactive_with_axes_cameras_and_theme_and_textmark.context_ready width={} height={}",
1321 width.max(1),
1322 height.max(1)
1323 );
1324 context.set_theme_config(theme);
1325 context.set_textmark(textmark);
1326 let rgba = context
1327 .render_to_rgba(&figure, None, Some(axes_cameras))
1328 .await?;
1329 log::debug!(
1330 "runmat-plot: render_figure_png_bytes_interactive_with_axes_cameras_and_theme_and_textmark.rgba_ready bytes={}",
1331 rgba.len()
1332 );
1333 encode_png_bytes(width.max(1), height.max(1), &rgba)
1334}
1335
1336#[cfg(feature = "egui-overlay")]
1337fn apply_cad_overlay_actions(
1338 renderer: &mut PlotRenderer,
1339 actions: crate::overlay::cad_overlay::CadOverlayActions,
1340) -> Vec<NativeSurfaceHostAction> {
1341 let mut host_actions = Vec::new();
1342 if actions.reset_view {
1343 renderer.reset_geometry_view();
1344 }
1345 if actions.create_fea_study {
1346 host_actions.push(NativeSurfaceHostAction::CreateFeaStudy);
1347 }
1348 if let Some(preset) = actions.view_preset {
1349 renderer.set_camera_view_preset(preset);
1350 }
1351 if let Some(enabled) = actions.grid_enabled {
1352 renderer.set_overlay_grid_enabled(enabled);
1353 }
1354 if let Some(enabled) = actions.xray_enabled {
1355 renderer.set_geometry_xray_enabled(enabled);
1356 }
1357 for (owner_id, visible) in actions.owner_visibility {
1358 renderer.set_geometry_owner_visible(owner_id, visible);
1359 }
1360 host_actions
1361}
1362
1363#[cfg(test)]
1364mod tests {
1365 use super::*;
1366
1367 #[test]
1368 fn headless_gpu_panic_errors_are_fallback_eligible() {
1369 assert!(is_headless_gpu_unavailable_error(
1370 "Headless GPU context creation panicked: called `Option::unwrap()` on a `None` value"
1371 ));
1372 }
1373}