diff --git a/Cargo.lock b/Cargo.lock index 2db49f6..07af1f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,10 +118,30 @@ dependencies = [ "objc", "objc-foundation", "objc_id", - "parking_lot", + "parking_lot 0.12.1", "thiserror", "winapi", - "x11rb", + "x11rb 0.9.0", +] + +[[package]] +name = "arboard" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6041616acea41d67c4a984709ddab1587fd0b10efe5cc563fee954d2f011854" +dependencies = [ + "clipboard-win", + "core-graphics", + "image 0.24.5", + "log", + "objc", + "objc-foundation", + "objc_id", + "once_cell", + "parking_lot 0.12.1", + "thiserror", + "winapi", + "x11rb 0.10.1", ] [[package]] @@ -265,7 +285,7 @@ dependencies = [ "fastrand", "js-sys", "ndk-glue", - "parking_lot", + "parking_lot 0.12.1", "serde", "thiserror", "wasm-bindgen", @@ -371,7 +391,7 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cec5fb091ccae94917266fe9b1af8339ef1e47763360b38273a31457598b030" dependencies = [ - "arboard", + "arboard 2.1.1", "bevy", "egui", "thread_local", @@ -578,7 +598,7 @@ dependencies = [ "erased-serde", "glam", "once_cell", - "parking_lot", + "parking_lot 0.12.1", "serde", "smallvec", "thiserror", @@ -631,7 +651,7 @@ dependencies = [ "image 0.24.5", "naga", "once_cell", - "parking_lot", + "parking_lot 0.12.1", "regex", "serde", "smallvec", @@ -843,12 +863,14 @@ dependencies = [ [[package]] name = "biquinho" -version = "0.2.4" +version = "0.2.7" dependencies = [ + "arboard 3.2.0", "bevy", "bevy_egui", "bevy_prototype_lyon", "egui", + "futures-intrusive", "futures-lite", "geo-booleanop", "geo-types", @@ -857,12 +879,14 @@ dependencies = [ "imc-rs", "mixbox", "nalgebra", + "pollster", "rand", "rfd", "serde", "serde_json", "smartcore", "tiff 0.7.4", + "wgpu", "winit", ] @@ -1457,7 +1481,7 @@ dependencies = [ "bytemuck", "emath", "nohash-hasher", - "parking_lot", + "parking_lot 0.12.1", "serde", ] @@ -1652,6 +1676,17 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" +[[package]] +name = "futures-intrusive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot 0.11.2", +] + [[package]] name = "futures-io" version = "0.3.25" @@ -2502,7 +2537,7 @@ dependencies = [ "ndk-macro", "ndk-sys", "once_cell", - "parking_lot", + "parking_lot 0.12.1", ] [[package]] @@ -2788,6 +2823,17 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.5", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -2795,7 +2841,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.4", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", ] [[package]] @@ -2889,6 +2949,12 @@ dependencies = [ "miniz_oxide 0.6.2", ] +[[package]] +name = "pollster" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" + [[package]] name = "pp-rs" version = "0.2.1" @@ -3939,7 +4005,7 @@ dependencies = [ "js-sys", "log", "naga", - "parking_lot", + "parking_lot 0.12.1", "raw-window-handle 0.5.0", "smallvec", "static_assertions", @@ -3965,7 +4031,7 @@ dependencies = [ "fxhash", "log", "naga", - "parking_lot", + "parking_lot 0.12.1", "profiling", "raw-window-handle 0.5.0", "smallvec", @@ -4001,7 +4067,7 @@ dependencies = [ "metal", "naga", "objc", - "parking_lot", + "parking_lot 0.12.1", "profiling", "range-alloc", "raw-window-handle 0.5.0", @@ -4241,7 +4307,7 @@ dependencies = [ "ndk-glue", "objc", "once_cell", - "parking_lot", + "parking_lot 0.12.1", "percent-encoding", "raw-window-handle 0.4.3", "raw-window-handle 0.5.0", @@ -4287,6 +4353,28 @@ dependencies = [ "winapi-wsapoll", ] +[[package]] +name = "x11rb" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "592b4883219f345e712b3209c62654ebda0bb50887f330cbd018d0f654bfd507" +dependencies = [ + "gethostname", + "nix 0.24.2", + "winapi", + "winapi-wsapoll", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56b245751c0ac9db0e006dc812031482784e434630205a93c73cfefcaabeac67" +dependencies = [ + "nix 0.24.2", +] + [[package]] name = "xcursor" version = "0.3.4" diff --git a/Cargo.toml b/Cargo.toml index 796cd3b..ff37623 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "biquinho" -version = "0.2.4" +version = "0.2.7" edition = "2021" description = "" @@ -14,6 +14,13 @@ image = "0.24" tiff = "0.7.4" rfd = "0.10" +# Interacting with clipboard +arboard = "3.2.0" + +wgpu = "0.14" +pollster = "0.2.5" +futures-intrusive = "0.4.0" + # Colour mixing mixbox = "2.0.0" diff --git a/src/camera.rs b/src/camera.rs index 395d5c9..bb6defb 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -1,14 +1,29 @@ +use arboard::Clipboard; use bevy::{ core_pipeline::clear_color::ClearColorConfig, ecs::query::WorldQuery, input::mouse::{MouseScrollUnit, MouseWheel}, prelude::*, - render::camera::Viewport, + render::{ + camera::{RenderTarget, Viewport}, + render_resource::{ + Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, + }, + renderer::RenderDevice, + view::RenderLayers, + RenderStage, + }, window::{WindowId, WindowResized}, }; use bevy_egui::EguiContext; +use pollster::FutureExt; +use wgpu::{BufferDescriptor, BufferUsages}; -use crate::ui::{UiLabel, UiSpace}; +use crate::{ + image_copy::{ImageCopier, ImageCopyPlugin}, + ui::{UiLabel, UiSpace}, + Message, Severity, +}; /// CameraPlugin /// @@ -26,6 +41,7 @@ pub struct CameraPlugin { impl Plugin for CameraPlugin { fn build(&self, app: &mut App) { app.insert_resource(self.camera_setup.clone()) + .add_plugin(ImageCopyPlugin) .add_startup_system(setup) // .add_startup_system(set_camera_viewports.after("initial_setup")) .add_event::() @@ -35,6 +51,7 @@ impl Plugin for CameraPlugin { // .after(UiLabel::Display), ) .add_system(ui_changed) + .add_system(copy_to_clipboard.before("handle_camera_event")) // This should be before handling camera events, to force it to be run on the next frame - otherwise the screenshot is empty .add_system_to_stage(CoreStage::Update, update_camera) .add_system(handle_camera_event.label("handle_camera_event")) .add_system( @@ -68,6 +85,8 @@ pub enum CameraEvent { /// Set the scale of all cameras displaying data to the given value. This has the effect of zooming in or out. All cameras are kept /// in-sync. Zoom(f32), + + CopyToClipboard, } /// Handle all camera events @@ -78,6 +97,8 @@ fn handle_camera_event( mut q_text: Query<&mut Text>, mut windows: ResMut, mut camera_setup: ResMut, + images: Res>, + render_device: Res, ) { let window = windows.primary_mut(); @@ -126,6 +147,91 @@ fn handle_camera_event( transform.scale.y = *zoom; } } + CameraEvent::CopyToClipboard => { + if let Some(view_texture) = images.get(&camera_setup.cpu_target.as_ref().unwrap()) { + let size = view_texture.size().as_ivec2(); + let size = Extent3d { + width: size.x as u32, + height: size.y as u32, + depth_or_array_layers: 4, + }; + + println!("Setting up screenshot: {:?}", size); + + // TODO: update the size of cpu_target here to re + + commands.spawn(ImageCopier::new( + camera_setup.target.as_ref().unwrap().clone(), + camera_setup.cpu_target.as_ref().unwrap().clone(), + size, + &render_device, + )); + // image_copier.disable();) + + // let bytes = [ + // 255, 100, 100, 255, 100, 255, 100, 100, 100, 100, 255, 100, 0, 0, 0, 255, + // ]; + // let img_data = arboard::ImageData { + // width: 2, + // height: 2, + // bytes: bytes.as_ref().into(), + // }; + // ctx.set_image(img_data).unwrap(); + } + } + } + } +} + +fn copy_to_clipboard( + mut commands: Commands, + q_copier: Query<(Entity, &ImageCopier)>, + camera_setup: Res, + mut images: ResMut>, +) { + for (entity, copier) in q_copier.iter() { + let mut ctx = Clipboard::new().unwrap(); + + let view_texture = images + .get_mut(&camera_setup.cpu_target.as_ref().unwrap()) + .unwrap(); + + let size = view_texture.size(); + + let width_in_bytes = size.x as usize * 4; + + let pre_data_length = view_texture.data.len(); + + let expected_length = (size.x * size.y) as usize * 4; + + // Due to the padding added (power of 2), we need to filter the data + let data = view_texture + .data + .iter() + .enumerate() + .filter(|(index, _value)| index % copier.padded_bytes_per_row() < width_in_bytes) + .map(|(_, value)| *value) + .collect::>(); + // let data = &view_texture.data; + + let data_length = data.len(); + + if expected_length == data.len() { + let img_data = arboard::ImageData { + width: size.x as usize, + height: size.y as usize, + bytes: data.into(), + }; + + if let Err(error) = ctx.set_image(img_data) { + commands.spawn(Message { + severity: Severity::Error, + message: format!("Error occured when trying to copy image to clipboard. This can happen if data is still loading in the background. Please try again.\n\nExpected size: {} x {}\nData size: {}\nData size(pre-filter): {}\n\nDescription: {:?}", + size.x, size.y, data_length, pre_data_length, error), + }); + } + + commands.entity(entity).despawn_recursive(); } } } @@ -172,6 +278,9 @@ pub struct CameraSetup { pub margin: u32, + pub target: Option>, + pub cpu_target: Option>, + pub names: Vec, } @@ -182,6 +291,8 @@ impl Default for CameraSetup { y: 1, margin: 10, names: Vec::new(), + target: None, + cpu_target: None, } } } @@ -194,7 +305,64 @@ impl Default for CameraSetup { // camera_text: Entity, // } -fn setup(mut commands: Commands, camera_setup: Res, asset_server: Res) { +#[derive(Component)] +struct ViewTexture; + +fn setup( + mut commands: Commands, + mut camera_setup: ResMut, + asset_server: Res, + mut images: ResMut>, + render_device: Res, +) { + let size = Extent3d { + width: 512, + height: 512, + ..default() + }; + + // This is the texture that will be rendered to. + let mut image = Image { + texture_descriptor: TextureDescriptor { + label: None, + size, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba8UnormSrgb, //Rgba8Unorm, // + mip_level_count: 1, + sample_count: 1, + usage: TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_SRC + | TextureUsages::COPY_DST + | TextureUsages::RENDER_ATTACHMENT, + }, + ..default() + }; + + // fill image.data with zeroes + image.resize(size); + + // This is the texture that will be rendered to. + let mut cpu_image = Image { + texture_descriptor: TextureDescriptor { + label: None, + size, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba8UnormSrgb, //Rgba8Unorm, // + mip_level_count: 1, + sample_count: 1, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, + }, + ..default() + }; + + // fill image.data with zeroes + cpu_image.resize(size); + + let image_handle = images.add(image); + camera_setup.target = Some(image_handle.clone()); + let cpu_image_handle = images.add(cpu_image); + camera_setup.cpu_target = Some(cpu_image_handle.clone()); + let far = 1000.0; let mut camera_transform = Transform::from_xyz(0.0, 0.0, far - 0.1); @@ -212,6 +380,17 @@ fn setup(mut commands: Commands, camera_setup: Res, asset_server: R // margin: 10, // }); + // This specifies the layer used for the ui and showing the texture + let ui_layer = RenderLayers::layer(1); + commands.spawn(( + SpriteBundle { + texture: image_handle, + ..default() + }, + ui_layer, + ViewTexture, + )); + // Spawn the UI camera // Transform the camera well away from the view - there should be a nicer way to do this, but otherwise // this camera displays the scene as well as the UI, causing issues for the multiview cameras @@ -225,10 +404,11 @@ fn setup(mut commands: Commands, camera_setup: Res, asset_server: R // don't clear the color while rendering this camera clear_color: ClearColorConfig::None, }, - transform: Transform::from_xyz(-1000000.0, 1000000.0, 0.0), + // transform: Transform::from_xyz(-1000000.0, 1000000.0, 0.0), ..default() }, UiCameraConfig::default(), + ui_layer, )); commands.spawn(MousePosition::default()); @@ -267,6 +447,11 @@ fn create_cameras(mut commands: Commands, camera_setup: &CameraSetup, asset_serv let camera_bundle = if x == 0 && y == 0 { Camera2dBundle { transform, + camera: Camera { + target: RenderTarget::Image(camera_setup.target.as_ref().unwrap().clone()), + ..default() + }, + ..default() } } else { @@ -274,6 +459,8 @@ fn create_cameras(mut commands: Commands, camera_setup: &CameraSetup, asset_serv transform, camera: Camera { priority: ((y * camera_setup.x) + x) as isize, + + target: RenderTarget::Image(camera_setup.target.as_ref().unwrap().clone()), ..default() }, camera_2d: Camera2d { @@ -367,6 +554,8 @@ fn update_camera( windows: Res, ui_space: Res, camera_setup: Res, + mut view_texture: Query<&mut Transform, With>, + mut images: ResMut>, mut cameras: Query<(&mut Camera, &PanCamera), Changed>, mut camera_text: Query<(&mut Text, &mut Style), With>, ) { @@ -378,19 +567,45 @@ fn update_camera( let window = windows.primary(); let panel_width = ui_space.right() * window.scale_factor() as f32; - let top_panel_width = ui_space.top() * window.scale_factor() as f32; - let bottom_panel_width = ui_space.bottom() * window.scale_factor() as f32; - - let physical_width = ((window.physical_width() - panel_width as u32) - - (camera_setup.margin * (camera_setup.x - 1))) - / camera_setup.x; - let physical_height = - ((window.physical_height() - top_panel_width as u32 - bottom_panel_width as u32) - - (camera_setup.margin * (camera_setup.y - 1))) - / camera_setup.y; + let top_panel_height = ui_space.top() * window.scale_factor() as f32; + let bottom_panel_height = ui_space.bottom() * window.scale_factor() as f32; + + let physical_view_width = window.physical_width() - panel_width as u32; + let physical_view_height = + window.physical_height() - top_panel_height as u32 - bottom_panel_height as u32; + + let physical_camera_width = + (physical_view_width - (camera_setup.margin * (camera_setup.x - 1))) / camera_setup.x; + let physical_camera_height = + (physical_view_height - (camera_setup.margin * (camera_setup.y - 1))) / camera_setup.y; + + let width = physical_camera_width as f32 / window.scale_factor() as f32; + let height = physical_camera_height as f32 / window.scale_factor() as f32; + + info!("Window resized - resizing all textures"); + + // TODO: Do we really want to resize these images every time? + images + .get_mut(&camera_setup.target.as_ref().unwrap()) + .unwrap() + .resize(Extent3d { + width: physical_view_width, + height: window.physical_height() - top_panel_height as u32, + ..default() + }); + images + .get_mut(&camera_setup.cpu_target.as_ref().unwrap()) + .unwrap() + .resize(Extent3d { + width: physical_view_width, + height: window.physical_height() - top_panel_height as u32, + ..default() + }); - let width = physical_width as f32 / window.scale_factor() as f32; - let height = physical_height as f32 / window.scale_factor() as f32; + if let Ok(mut view_texture_transform) = view_texture.get_single_mut() { + view_texture_transform.translation.x = -panel_width / 2.0; + view_texture_transform.translation.y = top_panel_height / 2.0; + } // println!("{:?}", window); // println!("{:?}", ui_space); @@ -400,13 +615,16 @@ fn update_camera( for (mut camera, position) in cameras.iter_mut() { camera.viewport = Some(Viewport { physical_position: UVec2::new( - (physical_width + camera_setup.margin) * position.x, - (physical_height + camera_setup.margin) * (position.y) + top_panel_width as u32, + (physical_camera_width + camera_setup.margin) * position.x, + (physical_camera_height + camera_setup.margin) * (position.y) + + top_panel_height as u32, ), - physical_size: UVec2::new(physical_width, physical_height), + physical_size: UVec2::new(physical_camera_width, physical_camera_height), ..default() }); + // println!("{:?}", camera.viewport); + if let Ok((mut text, mut style)) = camera_text.get_mut(position.camera_text) { let index = (position.y * camera_setup.x) + position.x; diff --git a/src/image_copy.rs b/src/image_copy.rs new file mode 100644 index 0000000..560afd6 --- /dev/null +++ b/src/image_copy.rs @@ -0,0 +1,191 @@ +// Using: https://github.com/DGriffin91/bevy/tree/frame_capture_tool + +use std::sync::Arc; + +use bevy::prelude::*; +use bevy::render::render_asset::RenderAssets; +use bevy::render::render_graph::{self, NodeRunError, RenderGraph, RenderGraphContext}; +use bevy::render::renderer::{RenderContext, RenderDevice, RenderQueue}; +use bevy::render::{Extract, RenderApp, RenderStage}; + +use bevy::render::render_resource::{ + Buffer, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, ImageCopyBuffer, + ImageDataLayout, +}; +use pollster::FutureExt; + +use std::sync::atomic::{AtomicBool, Ordering}; + +pub fn receive_images( + image_copiers: Query<&ImageCopier>, + mut images: ResMut>, + render_device: Res, +) { + for image_copier in image_copiers.iter() { + if !image_copier.enabled() { + continue; + } + // Derived from: https://sotrh.github.io/learn-wgpu/showcase/windowless/#a-triangle-without-a-window + // We need to scope the mapping variables so that we can + // unmap the buffer + async { + let buffer_slice = image_copier.buffer.slice(..); + + // NOTE: We have to create the mapping THEN device.poll() before await + // the future. Otherwise the application will freeze. + let (tx, rx) = futures_intrusive::channel::shared::oneshot_channel(); + buffer_slice.map_async(wgpu::MapMode::Read, move |result| { + tx.send(result).unwrap(); + }); + render_device.poll(wgpu::Maintain::Wait); + rx.receive().await.unwrap().unwrap(); + if let Some(mut image) = images.get_mut(&image_copier.dst_image) { + image.data = buffer_slice.get_mapped_range().to_vec(); + } + + image_copier.buffer.unmap(); + } + .block_on(); + } +} + +pub const IMAGE_COPY: &str = "image_copy"; + +pub struct ImageCopyPlugin; +impl Plugin for ImageCopyPlugin { + fn build(&self, app: &mut App) { + let render_app = app.add_system(receive_images).sub_app_mut(RenderApp); + + render_app.add_system_to_stage(RenderStage::Extract, image_copy_extract); + + let mut graph = render_app.world.get_resource_mut::().unwrap(); + + graph.add_node(IMAGE_COPY, ImageCopyDriver::default()); + + graph + .add_node_edge(IMAGE_COPY, bevy::render::main_graph::node::CAMERA_DRIVER) + .unwrap(); + } +} + +#[derive(Clone, Default, Resource, Deref, DerefMut)] +pub struct ImageCopiers(pub Vec); + +#[derive(Clone, Component)] +pub struct ImageCopier { + buffer: Buffer, + enabled: Arc, + src_image: Handle, + dst_image: Handle, + + padded_bytes_per_row: usize, +} + +impl ImageCopier { + pub fn new( + src_image: Handle, + dst_image: Handle, + size: Extent3d, + render_device: &RenderDevice, + ) -> ImageCopier { + let padded_bytes_per_row = + RenderDevice::align_copy_bytes_per_row((size.width) as usize) * 4; + + let cpu_buffer = render_device.create_buffer(&BufferDescriptor { + label: None, + size: padded_bytes_per_row as u64 * size.height as u64, + usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + ImageCopier { + buffer: cpu_buffer, + src_image, + dst_image, + enabled: Arc::new(AtomicBool::new(true)), + padded_bytes_per_row, + } + } + + pub fn enable(&self) { + self.enabled.store(true, Ordering::Relaxed); + } + + pub fn disable(&self) { + self.enabled.store(false, Ordering::Relaxed); + } + + pub fn enabled(&self) -> bool { + self.enabled.load(Ordering::Relaxed) + } + + pub fn padded_bytes_per_row(&self) -> usize { + self.padded_bytes_per_row + } +} + +pub fn image_copy_extract(mut commands: Commands, image_copiers: Extract>) { + commands.insert_resource(ImageCopiers( + image_copiers.iter().cloned().collect::>(), + )); +} + +#[derive(Default)] +pub struct ImageCopyDriver; + +impl render_graph::Node for ImageCopyDriver { + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let image_copiers = world.get_resource::().unwrap(); + let gpu_images = world.get_resource::>().unwrap(); + + for image_copier in image_copiers.iter() { + if !image_copier.enabled() { + continue; + } + + let src_image = gpu_images.get(&image_copier.src_image).unwrap(); + + let mut encoder = render_context + .render_device + .create_command_encoder(&CommandEncoderDescriptor::default()); + + let format = src_image.texture_format.describe(); + + let padded_bytes_per_row = RenderDevice::align_copy_bytes_per_row( + (src_image.size.x as usize / format.block_dimensions.0 as usize) + * format.block_size as usize, + ); + + let texture_extent = Extent3d { + width: src_image.size.x as u32, + height: src_image.size.y as u32, + depth_or_array_layers: 1, + }; + + encoder.copy_texture_to_buffer( + src_image.texture.as_image_copy(), + ImageCopyBuffer { + buffer: &image_copier.buffer, + layout: ImageDataLayout { + offset: 0, + bytes_per_row: Some( + std::num::NonZeroU32::new(padded_bytes_per_row as u32).unwrap(), + ), + rows_per_image: None, + }, + }, + texture_extent, + ); + + let render_queue = world.get_resource::().unwrap(); + render_queue.submit(std::iter::once(encoder.finish())); + } + + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 1402ff0..6ee9d64 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,6 +32,7 @@ mod camera; mod colour; mod data; // mod geometry; +mod image_copy; /// ImagePlugin - handles loading and viewing image data (including channel images). mod image_plugin; /// IMCPlugin - handles specific loading and visualisation of imaging mass cytometry data. diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 3cf4687..88b2d47 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -36,7 +36,9 @@ impl Plugin for UiPlugin { app.add_event::() .add_plugin(EguiPlugin) .add_plugin(CameraPlugin { camera_setup: CameraSetup { - x: 1, y: 1, margin: 10, names: vec![] + x: 1, y: 1, margin: 10, names: vec![], + target: None, + cpu_target: None, // vec!["10^6 WT +50mpk".to_string(), "10^6 VS".to_string(), "10^6 VS+100mpk".to_string(), // "10^6 WT +20mpk".to_string(), "10^6 WT".to_string(), "Control".to_string()] }}) @@ -841,6 +843,10 @@ fn ui_camera_panel(world: &mut World, ui: &mut Ui) { .show_viewport(ui, |ui, viewport| { let camera_setup = world.resource::(); + if ui.button("Copy view to clipboard").clicked() { + camera_events.push(CameraEvent::CopyToClipboard); + } + ui.horizontal(|ui| { ui.label("Grid size"); let x = ui_state.get_mut_string_with_default(