use egui::{Color32, Stroke, Vec2}; use rand::Rng; use crate::circle::Circle; /// We derive Deserialize/Serialize so we can persist app state on shutdown. #[derive(serde::Deserialize, serde::Serialize)] #[serde(default)] // if we add new fields, give them default values when deserializing old state pub struct Simulation { circles_count: usize, #[serde(skip)] // This how you opt-out of serialization of a field circles: Vec, #[serde(skip)] colors: Vec, } impl Default for Simulation { fn default() -> Self { Self { circles: Vec::new(), circles_count: 2, colors: Vec::new(), } } } impl Simulation { /// Called once before the first frame. pub fn new(cc: &eframe::CreationContext<'_>) -> Self { // This is also where you can customize the look and feel of egui using // `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`. // Looks better on 4k montior cc.egui_ctx.set_pixels_per_point(1.5); // Load previous app state (if any). // Note that you must enable the `persistence` feature for this to work. if let Some(storage) = cc.storage { return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default(); } Default::default() } } impl eframe::App for Simulation { /// Called by the frame work to save state before shutdown. fn save(&mut self, storage: &mut dyn eframe::Storage) { eframe::set_value(storage, eframe::APP_KEY, self); } /// Called each time the UI needs repainting, which may be many times per second. fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { // Put your widgets into a `SidePanel`, `TopBottomPanel`, `CentralPanel`, `Window` or `Area`. // For inspiration and more examples, go to https://emilk.github.io/egui egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { // The top panel is often a good place for a menu bar: egui::menu::bar(ui, |ui| { // NOTE: no File->Quit on web pages! let is_web = cfg!(target_arch = "wasm32"); if !is_web { ui.menu_button("File", |ui| { if ui.button("Quit").clicked() { ctx.send_viewport_cmd(egui::ViewportCommand::Close); } }); ui.add_space(16.0); } egui::widgets::global_dark_light_mode_buttons(ui); ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| { ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 4.0; ui.hyperlink_to( "source code", "https://gitea.djmil.dev/djmil/egui-circles", ); ui.label("rust"); }); }); }); }); egui::CentralPanel::default().show(ctx, |ui| { // The central panel the region left after adding TopPanel's and SidePanel's // Simulatiom ui.horizontal(|ui| { ui.label("Click on circles or press"); if ui.button("push").clicked() { self.circles.iter_mut().for_each(|c| (*c).v = Vec2{ x: rand::thread_rng().gen_range(-4.0..4.0), y: rand::thread_rng().gen_range(-4.0..4.0)}); }; ui.label("or"); if ui.button("halt").clicked() { self.circles.iter_mut().for_each(|c| (*c).v = Vec2{x: 0.0, y: 0.0}); }; ui.label("buttons"); }); ui.add(egui::Slider::new(&mut self.circles_count, 0..=25).text("circles count")); let diff = (self.circles.len() as i32) - (self.circles_count as i32); if diff > 0 { self.circles.truncate(self.circles_count); } else { for _ in diff..0 { self.circles.push(Circle::default()); self.colors.push( egui::Color32::from_rgba_premultiplied( rand::thread_rng().gen_range(64..255), rand::thread_rng().gen_range(64..255), rand::thread_rng().gen_range(64..255), 128) ); } } let painter = ui.painter(); let (hover_pos, any_down, any_released) = ctx.input(|input| ( input.pointer.hover_pos(), input.pointer.any_down(), input.pointer.any_released() )); if let Some(mousepos) = hover_pos { self.circles.iter_mut().for_each(|circle|{ let d = circle.c - mousepos; if d.length() < circle.r { if any_down { painter.line_segment( [circle.c, circle.c +d], Stroke{width: 1.0, color: Color32::from_rgb(128, 255, 255)}); } if any_released { circle.v += d.normalized() * (d.length() / circle.r) * 8.0; } } }); } for i in 0..self.circles_count { painter.circle( self.circles[i].c, self.circles[i].r, self.colors[i] /*Color32::TRANSPARENT*/, Stroke{width: 2.0, color: Color32::from_rgb(200, 255, 255)} ); } // End simultion ui.separator(); ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { powered_by_egui_and_eframe(ui); egui::warn_if_debug_build(ui); }); }); for circle in &mut self.circles { circle.apply_force(&ctx.used_rect()); } // Naive N^2 Colition detection // Optimization https://en.wikipedia.org/wiki/Sweep_and_prune for i in 0..self.circles_count { for j in i+1..self.circles_count { if (i + j) % 3 == 0 { continue; // skip collsions for every third ball } let dc = self.circles[i].c - self.circles[j].c; let dr = self.circles[i].r + self.circles[j].r; if dc.length() < dr { (self.circles[i].v, self.circles[j].v) = collision(&self.circles[i], &self.circles[j]); } } } // This is how to go into continuous mode - uncomment this to see example of continuous mode ctx.request_repaint(); } } fn powered_by_egui_and_eframe(ui: &mut egui::Ui) { ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 0.0; ui.label("Powered by "); ui.hyperlink_to("egui", "https://github.com/emilk/egui"); ui.label(" and "); ui.hyperlink_to( "eframe", "https://github.com/emilk/egui/tree/master/crates/eframe", ); }); } fn collision(c1: &Circle, c2: &Circle) -> (Vec2, Vec2) { let m1 = c1.r; let m2 = c2.r; // collision normal let n = Vec2{ x: c2.c.x - c1.c.x, y: c2.c.y - c1.c.y }; // normal vector unit let un = n.normalized(); // collision tangen let ut = Vec2{x: -un.y, y: un.x}; // 3 let v1n = un.dot(c1.v); let v1t = ut.dot(c1.v); let v2n = un.dot(c2.v); let v2t = ut.dot(c2.v); // 4 let v1t_new = v1t; let v2t_new = v2t; // 5 let v1n_new = (v1n * (m1 - m2) + 2.0 * m2 * v2n) / (m1 + m2); let v2n_new = (v2n * (m2 - m1) + 2.0 * m1 * v1n) / (m1 + m2); // 6 let vec1n = v1n_new * un; let vec1t = v1t_new * ut; let vec2n = v2n_new * un; let vec2t = v2t_new * ut; return ( vec1n + vec1t, vec2n + vec2t ); }