Compare commits
	
		
			4 Commits
		
	
	
		
			2dfdea361e
			...
			74139cc54b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 74139cc54b | |||
| 58aad8dbab | |||
| 0cddc9d9a1 | |||
| 13ba4368aa | 
							
								
								
									
										69
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										69
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -242,6 +242,15 @@ version = "1.0.95" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" | ||||
|  | ||||
| [[package]] | ||||
| name = "approx" | ||||
| version = "0.5.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" | ||||
| dependencies = [ | ||||
|  "num-traits", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "ar" | ||||
| version = "0.9.0" | ||||
| @ -539,6 +548,15 @@ dependencies = [ | ||||
|  "arrayvec", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "bincode" | ||||
| version = "1.3.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" | ||||
| dependencies = [ | ||||
|  "serde", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "bit-set" | ||||
| version = "0.8.0" | ||||
| @ -866,6 +884,15 @@ dependencies = [ | ||||
|  "cfg-if", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "crossbeam-channel" | ||||
| version = "0.5.14" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" | ||||
| dependencies = [ | ||||
|  "crossbeam-utils", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "crossbeam-deque" | ||||
| version = "0.8.6" | ||||
| @ -1294,6 +1321,7 @@ name = "flare" | ||||
| version = "0.1.0" | ||||
| dependencies = [ | ||||
|  "anyhow", | ||||
|  "approx", | ||||
|  "bytemuck", | ||||
|  "eframe", | ||||
|  "egui", | ||||
| @ -1305,6 +1333,10 @@ dependencies = [ | ||||
|  "futures-executor", | ||||
|  "glam", | ||||
|  "image", | ||||
|  "log", | ||||
|  "profiling", | ||||
|  "puffin", | ||||
|  "puffin_http", | ||||
|  "rand", | ||||
|  "rand_xoshiro", | ||||
|  "spirv-builder", | ||||
| @ -2158,6 +2190,12 @@ dependencies = [ | ||||
|  "imgref", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "lz4_flex" | ||||
| version = "0.11.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" | ||||
|  | ||||
| [[package]] | ||||
| name = "malloc_buf" | ||||
| version = "0.0.6" | ||||
| @ -2851,6 +2889,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" | ||||
| dependencies = [ | ||||
|  "profiling-procmacros", | ||||
|  "puffin", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @ -2863,6 +2902,36 @@ dependencies = [ | ||||
|  "syn", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "puffin" | ||||
| version = "0.19.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "fa9dae7b05c02ec1a6bc9bcf20d8bc64a7dcbf57934107902a872014899b741f" | ||||
| dependencies = [ | ||||
|  "anyhow", | ||||
|  "bincode", | ||||
|  "byteorder", | ||||
|  "cfg-if", | ||||
|  "itertools 0.10.5", | ||||
|  "lz4_flex", | ||||
|  "once_cell", | ||||
|  "parking_lot", | ||||
|  "serde", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "puffin_http" | ||||
| version = "0.16.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "739a3c7f56604713b553d7addd7718c226e88d598979ae3450320800bd0e9810" | ||||
| dependencies = [ | ||||
|  "anyhow", | ||||
|  "crossbeam-channel", | ||||
|  "log", | ||||
|  "parking_lot", | ||||
|  "puffin", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "qoi" | ||||
| version = "0.4.1" | ||||
|  | ||||
| @ -180,6 +180,10 @@ impl Coefs { | ||||
|         Self { a, b, c, d, e, f } | ||||
|     } | ||||
|  | ||||
|     pub fn new_from_xml_order(a: f32, d: f32, b: f32, e: f32, c: f32, f: f32) -> Self { | ||||
|         Self { a, b, c, d, e, f } | ||||
|     } | ||||
|  | ||||
|     pub fn apply(&self, point: glam::Vec2) -> glam::Vec2 { | ||||
|         vec2( | ||||
|             self.a * point.x + self.b * point.y + self.c, | ||||
|  | ||||
| @ -7,6 +7,7 @@ license.workspace = true | ||||
|  | ||||
| [dependencies] | ||||
| anyhow.workspace = true | ||||
| approx = "0.5" | ||||
| bytemuck.workspace = true | ||||
| eframe = { version = "0.31", features = ["wgpu"]} | ||||
| egui = "0.31" | ||||
| @ -18,9 +19,13 @@ futures = "0.3" | ||||
| futures-executor.workspace = true | ||||
| glam.workspace = true | ||||
| image = "0.25" | ||||
| puffin = "0.19" | ||||
| puffin_http = "0.16" | ||||
| rand = { workspace = true, default-features = true } | ||||
| rand_xoshiro.workspace = true | ||||
| wgpu.workspace = true | ||||
| log = "0.4.25" | ||||
| profiling = { version = "1.0", features = ["profile-with-puffin"]} | ||||
|  | ||||
| [build-dependencies] | ||||
| spirv-builder.workspace = true | ||||
|  | ||||
| @ -1,16 +1,15 @@ | ||||
| use flare::transform_editor::TransformEditor; | ||||
| use egui::{Frame, Ui}; | ||||
| use flare::gui::transform_editor::TransformEditor; | ||||
| use flare_shader::Coefs; | ||||
| use log::info; | ||||
|  | ||||
| #[derive(Clone, Default, Debug)] | ||||
| struct TransformEditorApp { | ||||
|     transform_editor: TransformEditor, | ||||
|     transforms: Vec<Coefs>, | ||||
| } | ||||
|  | ||||
| impl TransformEditorApp { | ||||
|     fn new() -> Self { | ||||
|         Self { | ||||
|             transform_editor: TransformEditor::new() | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl eframe::App for TransformEditorApp { | ||||
| @ -23,36 +22,54 @@ impl eframe::App for TransformEditorApp { | ||||
|                     } | ||||
|                 }); | ||||
|                 if ui.button("Add Transform").clicked() { | ||||
|                     self.transform_editor.add_transform(Coefs::IDENTITY) | ||||
|                     self.transforms.push(Coefs::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0)) | ||||
|                 } | ||||
|             }) | ||||
|         }); | ||||
|  | ||||
|         egui::TopBottomPanel::bottom("bottom_panel").show(ctx, |ui| { | ||||
|             ui.label(format!("Transforms: {}", self.transform_editor.len())) | ||||
|             self.transform_editor.interact_debug(ui); | ||||
|         }); | ||||
|  | ||||
|         egui::CentralPanel::default().show(ctx, |ui| { | ||||
|             self.transform_editor.ui(ui) | ||||
|             Frame::canvas(ui.style()).show(ui, |ui| { | ||||
|                 self.transform_editor.interact(ui, &mut self.transforms) | ||||
|             }) | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn main() -> eframe::Result { | ||||
|     std::env::set_var("RUST_LOG", "info"); | ||||
|     env_logger::init(); | ||||
|  | ||||
|     let initial_dimensions = egui::vec2(800., 600.); | ||||
|     let mut wgpu_options = egui_wgpu::WgpuConfiguration::default(); | ||||
|     wgpu_options.present_mode = wgpu::PresentMode::Immediate; | ||||
|     wgpu_options.desired_maximum_frame_latency = Some(1); | ||||
|     let native_options = eframe::NativeOptions { | ||||
|         viewport: egui::ViewportBuilder::default().with_inner_size(initial_dimensions), | ||||
|         wgpu_options, | ||||
|         renderer: eframe::Renderer::Wgpu, | ||||
|         vsync: false, | ||||
|         ..Default::default() | ||||
|     }; | ||||
|  | ||||
|     puffin::set_scopes_on(true); | ||||
|     match puffin_http::Server::new("localhost:8585") { | ||||
|         Ok(server) => { | ||||
|             info!("Server open"); | ||||
|             std::mem::forget(server); | ||||
|         } | ||||
|         _ => {} | ||||
|     } | ||||
|     // std::mem::forget(puffin_http::Server::new("127.0.0.1:8585").expect("Unable to start server")); | ||||
|  | ||||
|     eframe::run_native( | ||||
|         "transform_editor", | ||||
|         native_options, | ||||
|         Box::new(|cc| { | ||||
|             Ok(Box::new(TransformEditorApp::new())) | ||||
|             Ok(Box::new(TransformEditorApp::default())) | ||||
|         }), | ||||
|     ) | ||||
| } | ||||
							
								
								
									
										1
									
								
								crates/flare/src/gui/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								crates/flare/src/gui/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| pub mod transform_editor; | ||||
							
								
								
									
										364
									
								
								crates/flare/src/gui/transform_editor.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										364
									
								
								crates/flare/src/gui/transform_editor.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,364 @@ | ||||
| use egui::emath::RectTransform; | ||||
| use egui::*; | ||||
| use flare_shader::Coefs; | ||||
| use std::ops::Add; | ||||
|  | ||||
| /// Radius (in pixels) of the transform element draw circle | ||||
| const ELEMENT_DRAW_RADIUS_PX: f32 = 7.0; | ||||
|  | ||||
| /// Stroke size (in pixels) of the transform element draw circle | ||||
| const ELEMENT_DRAW_STROKE_PX: f32 = 2.0; | ||||
|  | ||||
| /// Offset (in pixels) of the transform element draw circle | ||||
| const ELEMENT_DRAW_OFFSET_PX: Vec2 = vec2(-2.0, -2.0); | ||||
|  | ||||
| fn test_in_circle(pt: Pos2, center: Pos2, radius: f32) -> bool { | ||||
|     ((pt.x - center.x).powf(2.0) + (pt.y - center.y).powf(2.0)) <= radius.powf(2.0) | ||||
| } | ||||
|  | ||||
| fn test_in_triangle(pt: Pos2, v1: Pos2, v2: Pos2, v3: Pos2) -> bool { | ||||
|     // https://stackoverflow.com/a/2049593 | ||||
|     let sign = |p1: Pos2, p2: Pos2, p3: Pos2| -> f32 { | ||||
|         (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y) | ||||
|     }; | ||||
|  | ||||
|     let d1 = sign(pt, v1, v2); | ||||
|     let d2 = sign(pt, v2, v3); | ||||
|     let d3 = sign(pt, v3, v1); | ||||
|  | ||||
|     let has_neg = [d1, d2, d3].iter().any(|v| *v < 0.0); | ||||
|     let has_pos = [d1, d2, d3].iter().any(|v| *v > 0.0); | ||||
|  | ||||
|     !(has_neg && has_pos) | ||||
| } | ||||
|  | ||||
| #[derive(Copy, Clone, Debug, PartialEq)] | ||||
| pub enum TransformElement { | ||||
|     Origin, | ||||
|     X, | ||||
|     Y, | ||||
| } | ||||
|  | ||||
| /// Affine coefficients expressed as three points of a triangle | ||||
| #[derive(Copy, Clone, Debug, PartialEq)] | ||||
| pub struct TransformTriangle { | ||||
|     origin: Pos2, | ||||
|     x: Pos2, | ||||
|     y: Pos2, | ||||
| } | ||||
|  | ||||
| impl TransformTriangle { | ||||
|     pub fn new(origin: Pos2, x: Pos2, y: Pos2) -> Self { | ||||
|         Self { origin, x, y } | ||||
|     } | ||||
|  | ||||
|     pub fn interact_drag(self, element: TransformElement, drag_delta: Vec2) -> Self { | ||||
|         match element { | ||||
|             TransformElement::X => Self::new(self.origin, self.x + drag_delta, self.y), | ||||
|             TransformElement::Y => Self::new(self.origin, self.x, self.y + drag_delta), | ||||
|             TransformElement::Origin => Self::new( | ||||
|                 self.origin + drag_delta, | ||||
|                 self.x + drag_delta, | ||||
|                 self.y + drag_delta, | ||||
|             ), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn transform_pos(self, rect: RectTransform) -> Self { | ||||
|         Self { | ||||
|             origin: rect.transform_pos(self.origin), | ||||
|             x: rect.transform_pos(self.x), | ||||
|             y: rect.transform_pos(self.y), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn is_hovered( | ||||
|         &self, | ||||
|         hover_pos: Pos2, | ||||
|         ifs_to_screen: RectTransform, | ||||
|     ) -> Option<TransformElement> { | ||||
|         let origin_pos = ifs_to_screen.transform_pos(self.origin); | ||||
|         let x_pos = ifs_to_screen.transform_pos(self.x); | ||||
|         let y_pos = ifs_to_screen.transform_pos(self.y); | ||||
|  | ||||
|         if test_in_circle(hover_pos, x_pos, ELEMENT_DRAW_RADIUS_PX) { | ||||
|             Some(TransformElement::X) | ||||
|         } else if test_in_circle(hover_pos, y_pos, ELEMENT_DRAW_RADIUS_PX) { | ||||
|             Some(TransformElement::Y) | ||||
|         } else if test_in_circle(hover_pos, origin_pos, ELEMENT_DRAW_RADIUS_PX) | ||||
|             || test_in_triangle(hover_pos, origin_pos, x_pos, y_pos) | ||||
|         { | ||||
|             Some(TransformElement::Origin) | ||||
|         } else { | ||||
|             None | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<Coefs> for TransformTriangle { | ||||
|     fn from(value: Coefs) -> Self { | ||||
|         let origin = pos2(value.c, -value.f); | ||||
|         Self { | ||||
|             origin, | ||||
|             x: origin + vec2(value.a, -value.d), | ||||
|             y: origin + vec2(-value.b, value.e), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Into<Coefs> for TransformTriangle { | ||||
|     fn into(self) -> Coefs { | ||||
|         Coefs { | ||||
|             a: self.x.x - self.origin.x, | ||||
|             b: self.origin.x - self.y.x, | ||||
|             c: self.origin.x, | ||||
|             d: self.origin.y - self.x.y, | ||||
|             e: self.y.y - self.origin.y, | ||||
|             f: -self.origin.y, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Add<Vec2> for TransformTriangle { | ||||
|     type Output = TransformTriangle; | ||||
|  | ||||
|     fn add(self, rhs: Vec2) -> Self::Output { | ||||
|         Self { | ||||
|             origin: self.origin + rhs, | ||||
|             x: self.x + rhs, | ||||
|             y: self.y + rhs, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Widget for manipulating IFS transform affine coefficients | ||||
| #[derive(Copy, Clone, Debug)] | ||||
| pub struct TransformEditor { | ||||
|     /// Center point (in IFS coordinates) of the editor window | ||||
|     center_ifs: Pos2, | ||||
|  | ||||
|     /// Total range (in IFS coordinates) of the editor window | ||||
|     range_ifs: f32, | ||||
|  | ||||
|     /// Transform index the cursor is hovering over | ||||
|     hover_index: Option<usize>, | ||||
|  | ||||
|     /// Specific element of the transform hovered by the cursor | ||||
|     hover_element: Option<TransformElement>, | ||||
|  | ||||
|     /// Transform index the cursor is dragging | ||||
|     drag_index: Option<usize>, | ||||
| } | ||||
|  | ||||
| impl Default for TransformEditor { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             center_ifs: Pos2::ZERO, | ||||
|             range_ifs: 4.0, | ||||
|             hover_index: None, | ||||
|             hover_element: None, | ||||
|             drag_index: None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn build_viewport_ifs(aspect_ratio: f32, center_ifs: Pos2, range_ifs: f32) -> Rect { | ||||
|     let size_ifs = if aspect_ratio >= 1.0 { | ||||
|         vec2(range_ifs * aspect_ratio, range_ifs) | ||||
|     } else { | ||||
|         vec2(range_ifs, range_ifs / aspect_ratio) | ||||
|     }; | ||||
|     let min_ifs = center_ifs - size_ifs / 2.0; | ||||
|     let max_ifs = center_ifs + size_ifs / 2.0; | ||||
|  | ||||
|     // IFS coordinates follow the "value increases from top left to bottom right" convention. | ||||
|     // Because we want coordinates to behave like a Cartesian plot, the Y-axis is flipped | ||||
|     Rect::from_min_max(pos2(min_ifs.x, max_ifs.y), pos2(max_ifs.x, min_ifs.y)) | ||||
| } | ||||
|  | ||||
| impl TransformEditor { | ||||
|     /// Interact with the provided transform coefficients. Returns the index of the focused transform, if any | ||||
|     pub fn interact(&mut self, ui: &mut Ui, transforms: &mut [Coefs]) -> Option<usize> { | ||||
|         let (response, painter) = ui.allocate_painter(ui.available_size(), Sense::drag()); | ||||
|  | ||||
|         if transforms.is_empty() { | ||||
|             return None; | ||||
|         } | ||||
|  | ||||
|         let interact_rect = response.interact_rect; | ||||
|         let ifs_rect = build_viewport_ifs( | ||||
|             interact_rect.aspect_ratio(), | ||||
|             self.center_ifs, | ||||
|             self.range_ifs, | ||||
|         ); | ||||
|  | ||||
|         // Update internal state based on screen interactions, then paint to screen | ||||
|         let ifs_to_screen = RectTransform::from_to(ifs_rect, interact_rect); | ||||
|         self.interact_update( | ||||
|             ui.input(|i| i.pointer.interact_pos()), | ||||
|             ui.input(|i| i.pointer.delta()), | ||||
|             ui.input(|i| i.pointer.primary_pressed()), | ||||
|             ui.input(|i| i.pointer.primary_released()), | ||||
|             ifs_to_screen, | ||||
|             transforms, | ||||
|         ); | ||||
|  | ||||
|         self.interact_draw(painter, ifs_to_screen, transforms); | ||||
|  | ||||
|         self.drag_index | ||||
|     } | ||||
|  | ||||
|     /// Update state of the provided transform coefficients based on current interactions, | ||||
|     /// return the transform coefficients that have claimed focus (if any) | ||||
|     fn interact_update( | ||||
|         &mut self, | ||||
|         hover_pos: Option<Pos2>, | ||||
|         hover_delta: Vec2, | ||||
|         primary_pressed: bool, | ||||
|         primary_released: bool, | ||||
|         ifs_to_screen: RectTransform, | ||||
|         transforms: &mut [Coefs], | ||||
|     ) { | ||||
|         // If the cursor is not in this widget, reset state | ||||
|         if hover_pos.is_none() { | ||||
|             self.hover_index = None; | ||||
|             self.hover_element = None; | ||||
|             self.drag_index = None; | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         let hover_pos = hover_pos.unwrap(); | ||||
|  | ||||
|         // If the transform array was modified, reset state and then proceed | ||||
|         if self.hover_index.map_or(false, |i| i >= transforms.len()) | ||||
|             || self.drag_index.map_or(false, |i| i >= transforms.len()) | ||||
|         { | ||||
|             self.hover_index = None; | ||||
|             self.hover_element = None; | ||||
|             self.drag_index = None; | ||||
|         } | ||||
|  | ||||
|         // If a transform is being dragged, update its position | ||||
|         if self.drag_index.is_some() { | ||||
|             let hover_index = self.hover_index.unwrap(); | ||||
|             let hover_element = self.hover_element.unwrap(); | ||||
|  | ||||
|             let hover_delta_ifs = hover_delta / ifs_to_screen.scale(); | ||||
|             let transform_triangle: TransformTriangle = transforms[hover_index].into(); | ||||
|             transforms[hover_index] = transform_triangle | ||||
|                 .interact_drag(hover_element, hover_delta_ifs) | ||||
|                 .into(); | ||||
|         } | ||||
|  | ||||
|         // Check if the currently-hovered transform is still hovered | ||||
|         let mut hover_found = false; | ||||
|  | ||||
|         if let Some(hover_index) = self.hover_index { | ||||
|             let transform_triangle: TransformTriangle = transforms[hover_index].into(); | ||||
|             self.hover_element = transform_triangle.is_hovered(hover_pos, ifs_to_screen); | ||||
|             hover_found = self.hover_element.is_some(); | ||||
|         } | ||||
|  | ||||
|         // Check if any transform is hovered | ||||
|         if !hover_found { | ||||
|             for (i, transform) in transforms.iter().enumerate() { | ||||
|                 let transform_triangle: TransformTriangle = (*transform).into(); | ||||
|                 self.hover_element = transform_triangle.is_hovered(hover_pos, ifs_to_screen); | ||||
|  | ||||
|                 if self.hover_element.is_some() { | ||||
|                     hover_found = true; | ||||
|                     self.hover_index = Some(i); | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if !hover_found { | ||||
|             // No hovers found, reset state | ||||
|             self.hover_index = None; | ||||
|             self.hover_element = None; | ||||
|             self.drag_index = None; | ||||
|         } | ||||
|  | ||||
|         // Check drag state | ||||
|         if primary_pressed && self.hover_index.is_some() { | ||||
|             self.drag_index = self.hover_index; | ||||
|         } | ||||
|         if primary_released { | ||||
|             self.drag_index = None; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn interact_draw_transform( | ||||
|         hover_element: Option<TransformElement>, | ||||
|         painter: &Painter, | ||||
|         ifs_to_screen: RectTransform, | ||||
|         transform: Coefs, | ||||
|     ) { | ||||
|         // `epaint` doesn't provide an option for whether the stroke is drawn inside, in the middle, | ||||
|         // or outside the shape to paint. In manual testing, hover detection works best when assuming | ||||
|         // the stroke is outside the shape. | ||||
|         // Also in manual testing, hover detection seems to work best when drawing the circle at | ||||
|         // a slight offset to the actual center position. Not clear why. | ||||
|         let transform_triangle: TransformTriangle = transform.into(); | ||||
|         let draw_triangle = | ||||
|             transform_triangle.transform_pos(ifs_to_screen) + ELEMENT_DRAW_OFFSET_PX; | ||||
|  | ||||
|         let stroke = Stroke::new(ELEMENT_DRAW_STROKE_PX, Color32::BLUE); | ||||
|         painter.circle_stroke( | ||||
|             draw_triangle.origin, | ||||
|             ELEMENT_DRAW_RADIUS_PX - ELEMENT_DRAW_STROKE_PX, | ||||
|             stroke, | ||||
|         ); | ||||
|         painter.circle_stroke( | ||||
|             draw_triangle.x, | ||||
|             ELEMENT_DRAW_RADIUS_PX - ELEMENT_DRAW_STROKE_PX, | ||||
|             stroke, | ||||
|         ); | ||||
|         painter.circle_stroke( | ||||
|             draw_triangle.y, | ||||
|             ELEMENT_DRAW_RADIUS_PX - ELEMENT_DRAW_STROKE_PX, | ||||
|             stroke, | ||||
|         ); | ||||
|  | ||||
|         let body_alpha: u8 = if hover_element.is_some() { 8 } else { 0 }; | ||||
|         let body_fill = Color32::from_rgba_unmultiplied(0, 0, u8::MAX, body_alpha); | ||||
|         let body = Shape::convex_polygon( | ||||
|             vec![draw_triangle.origin, draw_triangle.x, draw_triangle.y], | ||||
|             body_fill, | ||||
|             stroke, | ||||
|         ); | ||||
|         painter.add(body); | ||||
|     } | ||||
|  | ||||
|     /// Draw the provided transform coefficients to the screen. | ||||
|     fn interact_draw(&self, painter: Painter, ifs_to_screen: RectTransform, transforms: &[Coefs]) { | ||||
|         transforms | ||||
|             .iter() | ||||
|             .enumerate() | ||||
|             .filter(|(i, _)| Some(*i) != self.hover_index) | ||||
|             .for_each(|(_, transform)| { | ||||
|                 Self::interact_draw_transform(None, &painter, ifs_to_screen, *transform) | ||||
|             }); | ||||
|  | ||||
|         self.hover_index.map(|i| { | ||||
|             Self::interact_draw_transform( | ||||
|                 self.hover_element, | ||||
|                 &painter, | ||||
|                 ifs_to_screen, | ||||
|                 transforms[i], | ||||
|             ) | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     pub fn interact_debug(&self, ui: &mut Ui) { | ||||
|         Grid::new("transform_editor_debug") | ||||
|             .num_columns(3) | ||||
|             .show(ui, |ui| { | ||||
|                 ui.set_width(ui.available_width()); | ||||
|                 ui.label(format!("Hover index: {:?}", self.hover_index)); | ||||
|                 ui.label(format!("Hover element: {:?}", self.hover_element)); | ||||
|                 ui.label(format!("Drag index: {:?}", self.drag_index)); | ||||
|             }); | ||||
|     } | ||||
| } | ||||
| @ -1 +1,2 @@ | ||||
| pub mod transform_editor; | ||||
| pub mod transform_editor; | ||||
| pub mod gui; | ||||
| @ -1,3 +1,5 @@ | ||||
| extern crate core; | ||||
|  | ||||
| use eframe::Frame; | ||||
| use eframe::epaint::PaintCallbackInfo; | ||||
| use egui::{Context, Rect}; | ||||
|  | ||||
| @ -1,51 +1,266 @@ | ||||
| use egui::{emath, Rect, Sense}; | ||||
| use bytemuck::Contiguous; | ||||
| use egui::{DragValue, Rect, Sense, Ui, emath}; | ||||
| use epaint::{Color32, Shape, Stroke}; | ||||
| use log::info; | ||||
| use flare_shader::Coefs; | ||||
|  | ||||
| pub struct TransformEditor { | ||||
|     transforms: Vec<Coefs> | ||||
| pub const TRANSFORM_POINT_RADIUS: f32 = 0.03; | ||||
|  | ||||
| #[derive(Copy, Clone, Debug, PartialEq)] | ||||
| pub enum TransformElement { | ||||
|     Origin, | ||||
|     X, | ||||
|     Y, | ||||
| } | ||||
|  | ||||
| fn coef_to_shapes(coef: &Coefs, to_screen: emath::RectTransform) -> Vec<Shape> { | ||||
|     let origin = to_screen.transform_pos(egui::pos2(coef.c, -coef.f)); | ||||
|     let x = to_screen.transform_pos(egui::pos2(coef.c + coef.a, -(coef.f + coef.b))); | ||||
|     let y = to_screen.transform_pos(egui::pos2(coef.c + coef.d, -(coef.f + coef.e))); | ||||
| #[derive(Copy, Clone)] | ||||
| pub struct Transform { | ||||
|     origin: egui::Pos2, | ||||
|     x: egui::Pos2, | ||||
|     y: egui::Pos2, | ||||
|     active_element: Option<TransformElement>, | ||||
| } | ||||
|  | ||||
|     let stroke = Stroke::new(2.0, Color32::BLUE); | ||||
|     let translucent = Color32::from_rgba_unmultiplied(0, 0, 255, 8); | ||||
| impl Transform { | ||||
|     pub fn check_active(&mut self, hover_pos: Option<egui::Pos2>, drag_delta: egui::Vec2) -> bool { | ||||
|         if hover_pos.is_none() { | ||||
|             self.active_element.take(); | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|     let shapes = vec![ | ||||
|         Shape::circle_stroke(origin, 5.0, stroke), | ||||
|         Shape::circle_stroke(x, 5.0, stroke), | ||||
|         Shape::circle_stroke(y, 5.0, stroke), | ||||
|         Shape::convex_polygon(vec![origin, x, y], translucent, stroke), | ||||
|     ]; | ||||
|         let hover_pos = hover_pos.unwrap(); | ||||
|         self.active_element = if test_point_in_circle(hover_pos, self.x + drag_delta, TRANSFORM_POINT_RADIUS * 2.0) { | ||||
|             Some(TransformElement::X) | ||||
|         } else if test_point_in_circle(hover_pos, self.y + drag_delta, TRANSFORM_POINT_RADIUS * 2.0) { | ||||
|             Some(TransformElement::Y) | ||||
|         } else if test_point_in_circle(hover_pos, self.origin + drag_delta, TRANSFORM_POINT_RADIUS * 2.0) | ||||
|             || test_point_in_triangle(hover_pos, self.origin + drag_delta, self.x + drag_delta, self.y + drag_delta) | ||||
|         { | ||||
|             Some(TransformElement::Origin) | ||||
|         } else { | ||||
|             None | ||||
|         }; | ||||
|  | ||||
|     shapes | ||||
|         self.active_element.is_some() | ||||
|     } | ||||
|  | ||||
|     pub fn drag_update(&mut self, drag_delta: egui::Vec2) { | ||||
|         if let Some(active_element) = self.active_element { | ||||
|             match active_element { | ||||
|                 TransformElement::X => self.x += drag_delta, | ||||
|                 TransformElement::Y => self.y += drag_delta, | ||||
|                 TransformElement::Origin => { | ||||
|                     self.origin += drag_delta; | ||||
|                     self.x += drag_delta; | ||||
|                     self.y += drag_delta; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn ui_draw(&self, painter: &egui::Painter, to_screen: emath::RectTransform, drag_delta: egui::Vec2) { | ||||
|         let color_active = Color32::from_rgba_unmultiplied(0, 0, u8::MAX_VALUE, 8); | ||||
|  | ||||
|         let stroke = Stroke::new(2.0, Color32::BLUE); | ||||
|  | ||||
|         let origin_ifs = match self.active_element { | ||||
|             Some(TransformElement::Origin) => self.origin + drag_delta, | ||||
|             _ => self.origin | ||||
|         }; | ||||
|         let origin_screen = to_screen.transform_pos(origin_ifs); | ||||
|  | ||||
|         let x_ifs = match self.active_element { | ||||
|             Some(TransformElement::X | TransformElement::Origin) => self.x + drag_delta, | ||||
|             _ => self.x | ||||
|         }; | ||||
|         let x_screen = to_screen.transform_pos(x_ifs); | ||||
|  | ||||
|         let y_ifs = match self.active_element { | ||||
|             Some(TransformElement::Y | TransformElement::Origin) => self.y + drag_delta, | ||||
|             _ => self.y | ||||
|         }; | ||||
|         let y_screen = to_screen.transform_pos(y_ifs); | ||||
|  | ||||
|         let body_color = if self.active_element.is_some() { | ||||
|             color_active | ||||
|         } else { | ||||
|             Color32::TRANSPARENT | ||||
|         }; | ||||
|         painter.add(Shape::convex_polygon( | ||||
|             vec![origin_screen, x_screen, y_screen], | ||||
|             body_color, | ||||
|             stroke, | ||||
|         )); | ||||
|  | ||||
|         let point_radius = to_screen.scale().min_elem() * TRANSFORM_POINT_RADIUS; | ||||
|  | ||||
|         let origin_color = match self.active_element { | ||||
|             Some(TransformElement::Origin) => color_active, | ||||
|             _ => Color32::TRANSPARENT, | ||||
|         }; | ||||
|         painter.add(Shape::circle_stroke(origin_screen, point_radius, stroke)); | ||||
|         painter.add(Shape::circle_filled( | ||||
|             origin_screen, | ||||
|             point_radius, | ||||
|             origin_color, | ||||
|         )); | ||||
|  | ||||
|         let x_color = match self.active_element { | ||||
|             Some(TransformElement::X) => color_active, | ||||
|             _ => Color32::TRANSPARENT, | ||||
|         }; | ||||
|         painter.add(Shape::circle_stroke(x_screen, point_radius, stroke)); | ||||
|         painter.add(Shape::circle_filled(x_screen, point_radius, x_color)); | ||||
|  | ||||
|         let y_color = match self.active_element { | ||||
|             Some(TransformElement::X) => color_active, | ||||
|             _ => Color32::TRANSPARENT, | ||||
|         }; | ||||
|         painter.add(Shape::circle_stroke(y_screen, point_radius, stroke)); | ||||
|         painter.add(Shape::circle_filled(y_screen, point_radius, y_color)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for Transform { | ||||
|     fn default() -> Self { | ||||
|         Transform { | ||||
|             origin: egui::pos2(0.0, 0.0), | ||||
|             x: egui::pos2(1.0, 0.0), | ||||
|             y: egui::pos2(0.0, -1.0), | ||||
|             active_element: None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Clone, Default)] | ||||
| pub struct TransformEditor { | ||||
|     transforms: Vec<Transform>, | ||||
|     hover_index: Option<usize>, | ||||
|     hover_pos: Option<egui::Pos2>, | ||||
|     drag_index: Option<usize>, | ||||
|     drag_start_pos: Option<egui::Pos2>, | ||||
| } | ||||
|  | ||||
| fn test_point_in_triangle(pt: egui::Pos2, v1: egui::Pos2, v2: egui::Pos2, v3: egui::Pos2) -> bool { | ||||
|     puffin::profile_function!(); | ||||
|  | ||||
|     // https://stackoverflow.com/a/2049593 | ||||
|     let sign = |p1: egui::Pos2, p2: egui::Pos2, p3: egui::Pos2| -> f32 { | ||||
|         (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y) | ||||
|     }; | ||||
|  | ||||
|     let d1 = sign(pt, v1, v2); | ||||
|     let d2 = sign(pt, v2, v3); | ||||
|     let d3 = sign(pt, v3, v1); | ||||
|  | ||||
|     let has_neg = [d1, d2, d3].iter().any(|v| *v < 0.0); | ||||
|     let has_pos = [d1, d2, d3].iter().any(|v| *v > 0.0); | ||||
|  | ||||
|     !(has_neg && has_pos) | ||||
| } | ||||
|  | ||||
| fn test_point_in_circle(pt: egui::Pos2, center: egui::Pos2, radius: f32) -> bool { | ||||
|     ((pt.x - center.x).powf(2.0) + (pt.y - center.y).powf(2.0)) < radius.powf(2.0) | ||||
| } | ||||
|  | ||||
| impl TransformEditor { | ||||
|     pub fn new() -> Self { | ||||
|         let mut editor = Self { transforms: vec![] }; | ||||
|         editor.add_transform(Coefs::IDENTITY); | ||||
|         let mut editor = TransformEditor::default(); | ||||
|         editor.add_transform(); | ||||
|  | ||||
|         editor | ||||
|     } | ||||
|  | ||||
|     pub fn add_transform(&mut self, coefs: Coefs) { | ||||
|         self.transforms.push(coefs); | ||||
|     pub fn add_transform(&mut self) { | ||||
|         self.transforms.push(Default::default()); | ||||
|     } | ||||
|  | ||||
|     pub fn len(&self) -> usize { | ||||
|         self.transforms.len() | ||||
|     fn check_active(&mut self, hover_pos: Option<egui::Pos2>) { | ||||
|         // Find the active transform; the previously active transform has priority | ||||
|         let drag_delta = self.drag_delta(); | ||||
|         if let Some(hover_index) = self.hover_index { | ||||
|             if self.transforms[hover_index].check_active(hover_pos, drag_delta) { | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         for (i, transform) in self.transforms.iter_mut().enumerate() { | ||||
|             if transform.check_active(hover_pos, drag_delta) { | ||||
|                 self.hover_index = Some(i); | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         self.hover_index.take(); | ||||
|     } | ||||
|  | ||||
|     pub fn ui(&mut self, ui: &mut egui::Ui) -> egui::Response { | ||||
|         let (response, painter) = ui.allocate_painter(ui.available_size(), Sense::hover()); | ||||
|     fn drag_delta(&self) -> egui::Vec2 { | ||||
|         if let (Some(hover_pos), Some(drag_start_pos)) = (self.hover_pos, self.drag_start_pos) { | ||||
|             hover_pos - drag_start_pos | ||||
|         } else { | ||||
|             egui::Vec2::ZERO | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn ui_update( | ||||
|         &mut self, | ||||
|         hover_pos: Option<egui::Pos2>, | ||||
|         drag_delta: egui::Vec2, | ||||
|         clicked: bool, | ||||
|         drag_started: bool, | ||||
|         drag_ended: bool, | ||||
|     ) { | ||||
|         if self.transforms.is_empty() { | ||||
|             self.hover_index.take(); | ||||
|             self.drag_index.take(); | ||||
|             self.drag_start_pos.take(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         self.hover_pos = hover_pos; | ||||
|         self.check_active(hover_pos); | ||||
|  | ||||
|         if clicked || drag_started { | ||||
|             self.drag_index = self.hover_index; | ||||
|             self.drag_start_pos = Some(hover_pos.unwrap() - drag_delta); | ||||
|         } | ||||
|  | ||||
|         if drag_ended { | ||||
|             if let Some(drag_index) = self.drag_index { | ||||
|                 let drag_delta = self.drag_delta(); | ||||
|                 info!("Applying drag delta {:?}", drag_delta); | ||||
|                 self.transforms[drag_index].drag_update(drag_delta); | ||||
|             } | ||||
|  | ||||
|             self.drag_index.take(); | ||||
|             self.drag_start_pos.take(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn ui_draw(&mut self, painter: &egui::Painter, to_screen: emath::RectTransform) { | ||||
|         let drag_delta = if let (Some(hover_pos), Some(drag_start_pos)) = (self.hover_pos, self.drag_start_pos) { | ||||
|             hover_pos - drag_start_pos | ||||
|         } else { | ||||
|             egui::Vec2::ZERO | ||||
|         }; | ||||
|  | ||||
|         for (index, transform) in self.transforms.iter().enumerate() { | ||||
|             // Active transform is painted at the end to maintain priority | ||||
|             if self.hover_index.map_or(false, |i| i == index) { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             transform.ui_draw(painter, to_screen, drag_delta); | ||||
|         } | ||||
|  | ||||
|         self.hover_index.map(|i| self.transforms[i].ui_draw(painter, to_screen, drag_delta)); | ||||
|     } | ||||
|  | ||||
|     pub fn ui(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) -> egui::Response { | ||||
|         let (response, painter) = ui.allocate_painter(ui.available_size(), Sense::click_and_drag()); | ||||
|  | ||||
|         // Step one: set up conversions between screen space and IFS space coordinates | ||||
|         let interact_rect = response.interact_rect; | ||||
|  | ||||
|         // Aspect-ratio scaling; minimum dimension will be [-2.0, 2.0] | ||||
|         let interact_max_dim = interact_rect.width().max(interact_rect.height()); | ||||
|         let interact_min_dim = interact_rect.width().min(interact_rect.height()); | ||||
|         let interact_max_is_width = interact_max_dim == interact_rect.width(); | ||||
| @ -59,13 +274,30 @@ impl TransformEditor { | ||||
|  | ||||
|         let transform_area = Rect::from_min_max(ifs_min, ifs_min * -1.0); | ||||
|         let to_screen = emath::RectTransform::from_to(transform_area, response.interact_rect); | ||||
|         let to_ifs = emath::RectTransform::from_to(response.interact_rect, transform_area); | ||||
|  | ||||
|         self.transforms.iter().map(|coef| { | ||||
|             coef_to_shapes(coef, to_screen) | ||||
|         }).flat_map(|x| x).for_each(|shape| { | ||||
|             let _ = painter.add(shape); | ||||
|         egui::TopBottomPanel::bottom("response_stats").show(ctx, |ui| { | ||||
|             let hover_pos_string = response.hover_pos().map_or_else(|| "None".to_owned(), |p| { | ||||
|                 let ifs_pos = to_ifs.transform_pos(p); | ||||
|                 format!("({} {})", ifs_pos.x, ifs_pos.y) | ||||
|             }); | ||||
|             ui.label(format!("Hover Pos: {}", hover_pos_string)); | ||||
|             ui.label(format!("Hover Index: {:?}", self.hover_index)); | ||||
|             ui.label(format!("Drag Index: {:?}", self.drag_index)); | ||||
|         }); | ||||
|  | ||||
|         // Step two: update internal state based on recent interactions | ||||
|         self.ui_update( | ||||
|             response.hover_pos().map(|v| to_ifs.transform_pos(v)), | ||||
|             response.drag_delta() / interact_max_dim, | ||||
|             response.clicked(), | ||||
|             response.drag_started(), | ||||
|             response.drag_stopped(), | ||||
|         ); | ||||
|  | ||||
|         // Step three: draw the transforms | ||||
|         self.ui_draw(&painter, to_screen); | ||||
|  | ||||
|         response | ||||
|     } | ||||
| } | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user