Compare commits

...

4 Commits

Author SHA1 Message Date
74139cc54b Get the interaction delta from egui 2025-03-22 15:08:16 -04:00
58aad8dbab Work in screen space coordinates, and offset circle drawing to improve hover detection 2025-03-22 15:03:39 -04:00
0cddc9d9a1 First attempt at re-writing the transform editor
Still has issues with drag support, hover detection, but making progress
2025-03-22 12:02:45 -04:00
13ba4368aa Basic drag support
Code is... a disaster, but I'm figuring out what works
2025-03-15 15:04:28 -04:00
9 changed files with 737 additions and 42 deletions

69
Cargo.lock generated
View File

@ -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"

View File

@ -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,

View File

@ -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

View File

@ -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()))
}),
)
}

View File

@ -0,0 +1 @@
pub mod transform_editor;

View 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));
});
}
}

View File

@ -1 +1,2 @@
pub mod transform_editor;
pub mod transform_editor;
pub mod gui;

View File

@ -1,3 +1,5 @@
extern crate core;
use eframe::Frame;
use eframe::epaint::PaintCallbackInfo;
use egui::{Context, Rect};

View File

@ -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
}
}
}