From 0cddc9d9a18eb0005adfc7ffcebe5288d35bd042 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sat, 22 Mar 2025 12:02:45 -0400 Subject: [PATCH] First attempt at re-writing the transform editor Still has issues with drag support, hover detection, but making progress --- Cargo.lock | 10 + crates/flare-shader/src/lib.rs | 4 + crates/flare/Cargo.toml | 1 + crates/flare/examples/transform_editor.rs | 23 +- crates/flare/src/gui/mod.rs | 1 + crates/flare/src/gui/transform_editor.rs | 355 ++++++++++++++++++++++ crates/flare/src/lib.rs | 3 +- 7 files changed, 386 insertions(+), 11 deletions(-) create mode 100644 crates/flare/src/gui/mod.rs create mode 100644 crates/flare/src/gui/transform_editor.rs diff --git a/Cargo.lock b/Cargo.lock index 6ae019a..806986a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" @@ -1312,6 +1321,7 @@ name = "flare" version = "0.1.0" dependencies = [ "anyhow", + "approx", "bytemuck", "eframe", "egui", diff --git a/crates/flare-shader/src/lib.rs b/crates/flare-shader/src/lib.rs index 5cd80a6..70cb535 100644 --- a/crates/flare-shader/src/lib.rs +++ b/crates/flare-shader/src/lib.rs @@ -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, diff --git a/crates/flare/Cargo.toml b/crates/flare/Cargo.toml index ece1372..ed26251 100644 --- a/crates/flare/Cargo.toml +++ b/crates/flare/Cargo.toml @@ -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" diff --git a/crates/flare/examples/transform_editor.rs b/crates/flare/examples/transform_editor.rs index 9785fb8..382f7b2 100644 --- a/crates/flare/examples/transform_editor.rs +++ b/crates/flare/examples/transform_editor.rs @@ -1,17 +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, } impl TransformEditorApp { - fn new() -> Self { - Self { - transform_editor: TransformEditor::new() - } - } } impl eframe::App for TransformEditorApp { @@ -24,13 +22,19 @@ impl eframe::App for TransformEditorApp { } }); if ui.button("Add Transform").clicked() { - self.transform_editor.add_transform() + 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| { + self.transform_editor.interact_debug(ui); + }); + egui::CentralPanel::default().show(ctx, |ui| { - self.transform_editor.ui(ctx, ui) + Frame::canvas(ui.style()).show(ui, |ui| { + self.transform_editor.interact(ui, &mut self.transforms) + }) }); } } @@ -38,7 +42,6 @@ impl eframe::App for TransformEditorApp { fn main() -> eframe::Result { std::env::set_var("RUST_LOG", "info"); env_logger::init(); - info!("WTF"); let initial_dimensions = egui::vec2(800., 600.); let mut wgpu_options = egui_wgpu::WgpuConfiguration::default(); @@ -66,7 +69,7 @@ fn main() -> eframe::Result { "transform_editor", native_options, Box::new(|cc| { - Ok(Box::new(TransformEditorApp::new())) + Ok(Box::new(TransformEditorApp::default())) }), ) } \ No newline at end of file diff --git a/crates/flare/src/gui/mod.rs b/crates/flare/src/gui/mod.rs new file mode 100644 index 0000000..23cadfe --- /dev/null +++ b/crates/flare/src/gui/mod.rs @@ -0,0 +1 @@ +pub mod transform_editor; diff --git a/crates/flare/src/gui/transform_editor.rs b/crates/flare/src/gui/transform_editor.rs new file mode 100644 index 0000000..d9debcb --- /dev/null +++ b/crates/flare/src/gui/transform_editor.rs @@ -0,0 +1,355 @@ +use egui::emath::RectTransform; +use egui::*; +use flare_shader::Coefs; + +const HANDLE_RADIUS_IFS: f32 = 0.04; +const HANDLE_RADIUS_DRAW_IFS: f32 = HANDLE_RADIUS_IFS / 2.0; + +#[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 CoefsTriangle { + origin: Pos2, + x: Pos2, + y: Pos2, +} + +impl CoefsTriangle { + pub fn new(origin: Pos2, x: Pos2, y: Pos2) -> Self { + Self { origin, x, y } + } +} + +impl From for CoefsTriangle { + 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 for CoefsTriangle { + 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, + } + } +} + +/// 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, + + /// Hover position (in IFS coordinates) of the cursor. + /// + /// Because of input latency during large movements, `egui`'s drag motion + /// doesn't precisely match the prior cursor position. Track the position + /// here to calculate an exact update from the last frame to now. + hover_pos_ifs: Option, + + /// Index of the transform the cursor is hovering over + hover_index: Option, + + /// For the hovered transform, which specific element the cursor is hovering over. + /// Assumed to always have a value when `hover_index` has a value + hover_element: Option, + + /// Index of the transform being dragged + drag_index: Option, +} + +impl Default for TransformEditor { + fn default() -> Self { + Self { + center_ifs: Pos2::ZERO, + range_ifs: 4.0, + hover_pos_ifs: None, + hover_index: None, + hover_element: None, + drag_index: None, + } + } +} + +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) +} + +/// Test whether the provided position is hovering on the transform +fn test_hovered(hover_pos: Option, coefs: Coefs) -> Option { + let coefs_triangle: CoefsTriangle = coefs.into(); + + if hover_pos.is_none() { + return None; + } + + let hover_pos = hover_pos.unwrap(); + if test_in_circle(hover_pos, coefs_triangle.x, HANDLE_RADIUS_IFS) { + Some(TransformElement::X) + } else if test_in_circle(hover_pos, coefs_triangle.y, HANDLE_RADIUS_IFS) { + Some(TransformElement::Y) + } else if test_in_circle(hover_pos, coefs_triangle.origin, HANDLE_RADIUS_IFS) + || test_in_triangle( + hover_pos, + coefs_triangle.origin, + coefs_triangle.x, + coefs_triangle.y, + ) + { + Some(TransformElement::Origin) + } else { + None + } +} + +fn build_viewport_ifs(interact_rect: Rect, center_ifs: Pos2, range_ifs: f32) -> Rect { + let aspect_ratio = interact_rect.width() / interact_rect.height(); + + 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 screen coordinate "value increases from top left to bottom right" + // convention, so the Y-axis is flipped here to behave like a Cartesian plot + 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 { + 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, self.center_ifs, self.range_ifs); + let pixels_per_unit_ifs = interact_rect.width() / ifs_rect.width(); + + // Update internal state based on screen interactions, then paint to screen + let to_ifs = RectTransform::from_to(interact_rect, ifs_rect); + let hover_pos_ifs = response.hover_pos().map(|p| to_ifs.transform_pos(p)); + let focus_index = self.interact_update( + hover_pos_ifs, + response.clicked(), + response.drag_started(), + response.drag_stopped(), + transforms, + ); + + let to_screen = RectTransform::from_to(ifs_rect, interact_rect); + self.interact_draw(painter, to_screen, pixels_per_unit_ifs, transforms); + + focus_index + } + + /// Update state of the provided transform coefficients based on current interactions. + /// + /// Assumes that positions/vectors are in IFS coordinates + fn interact_update( + &mut self, + hover_pos: Option, + clicked: bool, + drag_started: bool, + drag_stopped: bool, + transforms: &mut [Coefs], + ) -> Option { + // Check each transform to see if it is hovered (giving priority to the currently hovered transform), + let mut hover_found = false; + if self.hover_index.is_some_and(|i| i < transforms.len()) { + let hover_element = + test_hovered(self.hover_pos_ifs, transforms[self.hover_index.unwrap()]); + if hover_element.is_some() { + hover_found = true; + self.hover_element = hover_element; + } + } + + if !hover_found { + for (i, transform) in transforms.iter().enumerate() { + if let Some(hover_element) = test_hovered(self.hover_pos_ifs, *transform) { + hover_found = true; + self.hover_index = Some(i); + self.hover_element = Some(hover_element); + break; + } + } + } + + if !hover_found { + // No transforms are hovered, clear interaction state and return + self.hover_pos_ifs = hover_pos; + self.hover_index = None; + self.hover_element = None; + self.drag_index = None; + return None; + } + + let hover_delta = + if let (Some(current_hover_pos), Some(hover_pos)) = (hover_pos, self.hover_pos_ifs) { + current_hover_pos - hover_pos + } else { + Vec2::ZERO + }; + self.hover_pos_ifs = hover_pos; + + // If the hovered transform is clicked, it receives focus and we end updates + if clicked { + return self.hover_index; + } + + // If the hovered transform is dragged, it receives focus and the drag index is updated + if drag_started { + self.drag_index = self.hover_index; + } + + // If there is a transform being dragged, update its position + if self.drag_index.is_some() && hover_delta.abs().max_elem() > 0.0 { + let mut coefs = &mut transforms[self.drag_index.unwrap()]; + let mut coefs_triangle: CoefsTriangle = (*coefs).into(); + + match self.hover_element.unwrap() { + TransformElement::X => coefs_triangle.x += hover_delta, + TransformElement::Y => coefs_triangle.y += hover_delta, + TransformElement::Origin => { + coefs_triangle.x += hover_delta; + coefs_triangle.y += hover_delta; + coefs_triangle.origin += hover_delta; + } + } + + *coefs = coefs_triangle.into(); + } + + // If the transform is no longer being dragged, clear the drag index + if drag_stopped { + self.drag_index = None; + } + + self.drag_index + } + + fn interact_draw_transform( + painter: &Painter, + to_screen: RectTransform, + pixels_per_unit_ifs: f32, + transform: Coefs, + hovered_element: Option, + ) { + let coefs_triangle: CoefsTriangle = transform.into(); + let origin_screen = to_screen.transform_pos(coefs_triangle.origin); + let x_screen = to_screen.transform_pos(coefs_triangle.x); + let y_screen = to_screen.transform_pos(coefs_triangle.y); + + let stroke = Stroke::new(2.0, Color32::BLUE); + painter.circle_stroke( + origin_screen, + HANDLE_RADIUS_DRAW_IFS * pixels_per_unit_ifs, + stroke, + ); + painter.circle_stroke( + x_screen, + HANDLE_RADIUS_DRAW_IFS * pixels_per_unit_ifs, + stroke, + ); + painter.circle_stroke( + y_screen, + HANDLE_RADIUS_DRAW_IFS * pixels_per_unit_ifs, + stroke, + ); + + let body_alpha: u8 = if hovered_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![origin_screen, x_screen, y_screen], + body_fill, + stroke, + ); + painter.add(body); + } + + /// Draw the provided transform coefficients to the screen. + fn interact_draw( + &self, + painter: Painter, + to_screen: RectTransform, + pixels_per_unit_ifs: f32, + transforms: &[Coefs], + ) { + // Hovered transform is painted at the end so it has priority + transforms + .iter() + .enumerate() + .filter(|(i, _)| Some(*i) != self.hover_index) + .for_each(|(_, transform)| { + TransformEditor::interact_draw_transform( + &painter, + to_screen, + pixels_per_unit_ifs, + *transform, + None + ) + }); + + self.hover_index.map(|i| { + TransformEditor::interact_draw_transform( + &painter, + to_screen, + pixels_per_unit_ifs, + transforms[i], + self.hover_element, + ) + }); + } + + 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)); + }); + } +} diff --git a/crates/flare/src/lib.rs b/crates/flare/src/lib.rs index dfb0b30..5700a68 100644 --- a/crates/flare/src/lib.rs +++ b/crates/flare/src/lib.rs @@ -1 +1,2 @@ -pub mod transform_editor; \ No newline at end of file +pub mod transform_editor; +pub mod gui; \ No newline at end of file