diff --git a/src/main.rs b/src/main.rs index 46c9914..dad1147 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,224 +1,18 @@ -use std::ops::{Deref, DerefMut}; - -use bevy_ecs::{ - prelude::*, - query::WorldQuery, - system::{Commands, IntoSystem, Query, ReadOnlySystem, Res, ResMut, System}, -}; -use eframe::{ - egui::{self, CursorIcon, Painter, Response, Sense}, - emath::{Pos2, Rect, RectTransform, Vec2}, - epaint::{Color32, Hsva, Stroke}, -}; -use geometry::{Line, LineBundle, Point, PointId, PointPos, PointPosQueryMut}; +use bevy_ecs::prelude::*; +use eframe::egui; mod geometry; pub mod optimization; mod relations; #[cfg(feature = "trace")] mod tracing; +mod ui; fn init(mut commands: Commands) { let p1 = geometry::insert_point_at(&mut commands, (10., 30.)); let p2 = geometry::insert_point_at(&mut commands, (-20., 15.)); geometry::insert_point_at(&mut commands, (0., -10.)); - commands.spawn(LineBundle::new(p1, p2)); -} - -#[derive(Resource)] -struct ContextRes(egui::Context); - -impl Deref for ContextRes { - type Target = egui::Context; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -fn prepare(ctx: Res) { - ctx.request_repaint(); - ctx.set_visuals(egui::Visuals::dark()); -} - -fn toolbar( - ctx: Res, - selected: Query>, - mut commands: Commands, - mut tool: ResMut, -) { - egui::TopBottomPanel::top("top_panel").show(&ctx, |ui| { - // ui.heading("sketchrs"); - let mut selected_tool = *tool; - ui.horizontal(|ui| { - ui.radio_value(&mut selected_tool, Tool::Select, "Select"); - ui.radio_value(&mut selected_tool, Tool::Move, "Move"); - ui.radio_value(&mut selected_tool, Tool::AddPoint, "Add Point"); - ui.radio_value(&mut selected_tool, Tool::AddLine, "Add Line"); - }); - if selected_tool != *tool { - *tool = selected_tool; - - for selected in selected.iter() { - commands.entity(selected).remove::(); - } - } - }); -} - -struct ShowEntitiesSchedule(Schedule); - -#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] -#[system_set(base)] -pub enum ShowEntitiesStage { - Update, - Input, - Tools, - PostTools, - Paint, -} - -impl Default for ShowEntitiesSchedule { - fn default() -> Self { - let mut schedule = Schedule::new(); - schedule.configure_sets( - ( - ShowEntitiesStage::Update, - ShowEntitiesStage::Input, - ShowEntitiesStage::Tools, - ShowEntitiesStage::PostTools, - ShowEntitiesStage::Paint, - ) - .chain(), - ); - schedule.add_systems( - (geometry::update_point_pos, geometry::update_line_pos) - .in_base_set(ShowEntitiesStage::Update), - ); - schedule.add_systems( - (update_hover_point, update_hover_line).in_base_set(ShowEntitiesStage::Input), - ); - schedule.add_systems( - ( - select_tool.run_if(is_tool_active(Tool::Select)), - move_tool.run_if(is_tool_active(Tool::Move)), - add_point_tool.run_if(is_tool_active(Tool::AddPoint)), - add_line_tool.run_if(is_tool_active(Tool::AddLine)), - add_relation_tool.run_if(is_tool_active(Tool::AddRelation)), - ) - .distributive_run_if(is_hovered) - .in_base_set(ShowEntitiesStage::Tools), - ); - schedule - .add_system(geometry::remove_dangling_lines.in_base_set(ShowEntitiesStage::PostTools)); - schedule.add_systems( - (paint_lines, paint_points) - .chain() - .in_base_set(ShowEntitiesStage::Paint), - ); - Self(schedule) - } -} - -fn central_panel(world: &mut World, mut schedule: Local) { - let ctx = world.get_resource::().unwrap().0.clone(); - egui::CentralPanel::default() - .frame(egui::Frame::none()) - .show(&ctx, |ui| { - let sense = match *world.resource::() { - Tool::Move => Sense::drag(), - Tool::Select | Tool::AddPoint | Tool::AddLine | Tool::AddRelation => Sense::click(), - }; - - let (response, painter) = ui.allocate_painter(ui.available_size(), sense); - let to_screen = RectTransform::from_to( - Rect::from_center_size(Pos2::ZERO, response.rect.size() / 2.0), - response.rect, - ); - world.insert_resource(ToScreen(to_screen)); - world.insert_resource(ResponseRes(response)); - world.insert_resource(PainterRes(painter)); - - schedule.0.run(world); - - world.remove_resource::(); - world.remove_resource::(); - world.remove_resource::(); - }); -} - -#[derive(WorldQuery)] -struct SelectableEntity<'a> { - id: Entity, - point: Option<&'a Point>, - line: Option<&'a Line>, -} - -fn side_panel_ui( - ui: &mut egui::Ui, - selected: Query>, - tool: Res, -) { - let tool = *tool; - ui.vertical(|ui| match tool { - Tool::Select => { - let mut count = 0; - selected.for_each(|sel| { - count += 1; - if sel.point.is_some() { - ui.label(format!("Selected point {}", sel.id.index())); - } else if sel.line.is_some() { - ui.label(format!("Selected line {}", sel.id.index())); - } - }); - if count == 0 { - ui.label("Nothing selected"); - } - } - Tool::Move => { - let mut count = 0; - selected.for_each(|sel| { - count += 1; - if sel.point.is_some() { - ui.label(format!("Selected point {}", sel.id.index())); - } else if sel.line.is_some() { - ui.label(format!("Selected line {}", sel.id.index())); - } - }); - if count == 0 { - ui.label("Nothing selected"); - } - } - Tool::AddPoint => { - ui.label("Click to add a point"); - } - Tool::AddLine => { - ui.label("Click to add a line"); - } - Tool::AddRelation => { - ui.label("Click to add a relation"); - } - }); -} - -fn side_panel( - ctx: Res, - selected: Query>, - tool: Res, -) { - egui::SidePanel::right("side_panel") - .resizable(true) - .default_width(150.0) - .width_range(80.0..=200.0) - .show(&ctx, |ui| side_panel_ui(ui, selected, tool)); -} - -fn bottom_panel(ctx: Res) { - egui::TopBottomPanel::bottom("bottom_panel").show(&ctx, |ui| { - ui.horizontal(|ui| { - ui.label("Status:"); - }); - }); + commands.spawn(geometry::LineBundle::new(p1, p2)); } struct MyApp { @@ -231,6 +25,7 @@ impl Default for MyApp { Self::new() } } + impl MyApp { fn new() -> Self { let mut world = World::default(); @@ -238,27 +33,29 @@ impl MyApp { #[cfg(feature = "trace")] tracing::setup(&mut world); + world.init_resource::(); + let mut init_sched = Schedule::new(); init_sched.add_system(init); init_sched.run(&mut world); - let mut update = Schedule::default(); - update.add_systems((prepare, toolbar, side_panel, bottom_panel, central_panel).chain()); + let mut update_schedule = Schedule::default(); + ui::add_to_schedule(&mut update_schedule); Self { world, - update_schedule: update, + update_schedule, } } } impl eframe::App for MyApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - self.world.insert_resource(ContextRes(ctx.clone())); + self.world.insert_resource(ui::ContextRes(ctx.clone())); self.update_schedule.run(&mut self.world); - self.world.remove_resource::(); + self.world.remove_resource::(); } } diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..7dc3c2c --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,570 @@ +use std::ops::Deref; + +use bevy_ecs::{prelude::*, query::WorldQuery, system::ReadOnlySystem}; +use eframe::{ + egui::{self, CursorIcon, Painter, Response, Sense}, + emath::RectTransform, + epaint::{Color32, Hsva, Pos2, Rect, Stroke, Vec2}, +}; + +use crate::geometry::{self}; + +#[derive(Clone, Copy, PartialEq, Resource)] +pub enum Tool { + Select, + Move, + AddPoint, + AddLine, + AddRelation, +} + +#[allow(clippy::derivable_impls)] +impl Default for Tool { + fn default() -> Self { + Tool::Select + } +} + +#[derive(Resource)] +struct UiRes { + response: Response, + painter: Painter, + to_screen: ToScreen, +} +struct ToScreen(RectTransform); + +impl ToScreen { + fn transform_pos(&self, pos: &geometry::PointPos) -> Pos2 { + self.0 * Pos2::new(pos.x as f32, pos.y as f32) + } + + fn inverse_transform(&self, pos: Pos2) -> Pos2 { + self.0.inverse() * pos + } + + fn inverse_transform_to_point(&self, pos: Pos2) -> geometry::PointPos { + let pos = self.inverse_transform(pos); + geometry::PointPos::new(pos.x as f64, pos.y as f64) + } +} + +#[derive(Component)] +struct Hovered; + +#[derive(Component)] +struct Selected; + +fn color_for_var_status(status: geometry::VarStatus) -> Hsva { + use geometry::VarStatus::*; + match status { + Free => Hsva::new(200. / 360., 0.90, 0.80, 1.0), + Dependent => todo!(), + Unique => todo!(), + Overconstrained => todo!(), + } +} + +const POINT_RADIUS: f32 = 3.0; + +fn update_hover_point( + ui: Res, + points: Query<(geometry::PointId, &geometry::ComputedPointPos)>, + mut commands: Commands, +) { + points.for_each(|(id, pos)| { + let hovered = if let Some(hover_pos) = ui.response.hover_pos() { + let center = ui.to_screen.transform_pos(pos); + + (hover_pos - center).length() < (POINT_RADIUS * 3.) + } else { + false + }; + + if hovered { + commands.entity(id).insert(Hovered); + } else { + commands.entity(id).remove::(); + } + }); +} + +fn update_hover_line( + ui: Res, + lines: Query<(Entity, &geometry::ComputedLinePos)>, + mut commands: Commands, +) { + lines.for_each(|(id, pos)| { + let hovered = if let Some(hover_pos) = ui.response.hover_pos() { + let points = [ + ui.to_screen.transform_pos(&pos.start), + ui.to_screen.transform_pos(&pos.end), + ]; + + let b = points[1] - points[0]; + let a = hover_pos - points[0]; + let p = a.dot(b) / b.dot(b); + let perp = a - (p * b); + ((0.)..=1.).contains(&p) && perp.length() < 5.0 + } else { + false + }; + + if hovered { + commands.entity(id).insert(Hovered); + } else { + commands.entity(id).remove::(); + } + }); +} + +fn select_tool( + ui: Res, + hovered: Query>, + selected: Query>, + mut commands: Commands, +) { + ui.response.ctx.output_mut(|output| { + output.cursor_icon = if !hovered.is_empty() { + CursorIcon::PointingHand + } else { + CursorIcon::Default + } + }); + + if ui.response.clicked() { + if !ui.response.ctx.input(|input| input.modifiers.shift) { + selected.for_each(|selected| { + commands.entity(selected).remove::(); + }); + } + // TODO: choose which to select + if let Some(hovered) = hovered.iter().next() { + commands.entity(hovered).insert(Selected); + } + } + + if ui + .response + .ctx + .input(|input| input.key_pressed(egui::Key::Escape)) + { + selected.for_each(|selected| { + commands.entity(selected).remove::(); + }); + } else if ui + .response + .ctx + .input(|input| input.key_pressed(egui::Key::Delete)) + { + selected.for_each(|selected| { + commands.entity(selected).despawn(); + }); + } +} + +#[derive(Default)] +struct DragDelta(Vec2); + +// TODO: move other entities +fn move_tool( + ui: Res, + mut drag_delta: Local, + mut point_pos: geometry::PointPosQueryMut, + hovered: Query<(Entity, &geometry::Point), With>, + selected: Query<(Entity, &geometry::Point), With>, + mut commands: Commands, +) { + let response = &ui.response; + let to_screen = &ui.to_screen; + let hover_pos = response.hover_pos().unwrap(); + + response.ctx.output_mut(|o| { + o.cursor_icon = if !selected.is_empty() { + CursorIcon::Grabbing + } else if !hovered.is_empty() { + CursorIcon::Grab + } else { + CursorIcon::Move + } + }); + + let selected = if response.drag_started() { + // TODO: choose which to select + let to_select = hovered.iter().next(); + if let Some(hovered) = to_select { + commands.entity(hovered.0).insert(Selected); + } + to_select + } else if response.drag_released() { + selected.for_each(|selected| { + commands.entity(selected.0).remove::(); + }); + drag_delta.0 = Vec2::ZERO; + None + } else { + selected.iter().next() + }; + + if let Some((_, point)) = selected { + if response.drag_started() { + let drag_point_pos = point_pos.get(point).unwrap(); + drag_delta.0 = hover_pos - to_screen.transform_pos(&drag_point_pos); + } + let move_to = to_screen.inverse_transform_to_point(hover_pos - drag_delta.0); + point_pos.set(point, move_to).unwrap(); + } +} + +fn add_point(commands: &mut Commands, pos: Pos2, to_screen: &ToScreen) -> geometry::PointId { + let point_pos = to_screen.inverse_transform(pos); + let point_pos = (point_pos.x as f64, point_pos.y as f64); + geometry::insert_point_at(commands, point_pos) +} + +fn add_point_tool(ui: Res, mut commands: Commands) { + let hover_pos = ui.response.hover_pos().unwrap(); + if ui.response.clicked() { + add_point(&mut commands, hover_pos, &ui.to_screen); + } else { + ui.painter + .circle_filled(hover_pos, POINT_RADIUS, Color32::WHITE); + } +} + +fn add_line_tool( + ui: Res, + hovered: Query, With)>, + selected: Query<(Entity, &geometry::ComputedPointPos), With>, + mut commands: Commands, +) { + let UiRes { + response, + painter, + to_screen, + } = &*ui; + let hover_pos = response.hover_pos().unwrap(); + + match (selected.iter().next(), response.clicked()) { + (None, false) => { + painter.circle_filled(hover_pos, POINT_RADIUS, Color32::DARK_GRAY); + } + (None, true) => { + let point_id = match hovered.iter().next() { + Some(hovered) => hovered, + None => add_point(&mut commands, hover_pos, &to_screen), + }; + commands.entity(point_id).insert(Selected); + } + (Some((_, start_point_pos)), false) => { + let points = [to_screen.transform_pos(start_point_pos), hover_pos]; + + let stroke = Stroke::new(2.0, Color32::DARK_GRAY); + + painter.line_segment(points, stroke); + + painter.circle_filled(hover_pos, POINT_RADIUS, Color32::DARK_GRAY); + } + (Some((start_point_id, _)), true) => { + // TODO: add point if no hover point + let end_point = hovered + .iter() + .next() + .unwrap_or_else(|| add_point(&mut commands, hover_pos, &to_screen)); + + let line = geometry::LineBundle::new(start_point_id, end_point); + commands.spawn(line); + + selected.for_each(|(selected_id, _)| { + commands.entity(selected_id).remove::(); + }); + } + } +} + +fn add_relation_tool( + ui: Res, + hovered: Query>, + selected: Query>, + mut commands: Commands, +) { + let response = &ui.response; + response.ctx.output_mut(|o| { + o.cursor_icon = if !hovered.is_empty() { + CursorIcon::PointingHand + } else { + CursorIcon::Default + } + }); + + if response.clicked() { + // TODO: choose which to select + if let Some(hovered) = hovered.iter().next() { + commands.entity(hovered).insert(Selected); + } else { + selected.for_each(|selected| { + commands.entity(selected).remove::(); + }); + } + } +} + +fn is_hovered(ui: Res) -> bool { + ui.response.hover_pos().is_some() +} + +fn is_tool_active(tool: Tool) -> impl System + ReadOnlySystem { + IntoSystem::into_system(move |active_tool: Res| *active_tool == tool) +} + +fn paint_lines( + ui: Res, + lines: Query<(Entity, &geometry::ComputedLinePos)>, + hovered: Query<(), With>, + selected: Query<(), With>, +) { + lines.for_each(|(id, pos)| { + let points = [ + ui.to_screen.transform_pos(&pos.start), + ui.to_screen.transform_pos(&pos.end), + ]; + + let mut color = color_for_var_status(pos.status); + color.v -= 0.6; + if hovered.contains(id) || selected.contains(id) { + color.s -= 0.8; + } + + let stroke = Stroke::new(2.0, color); + + ui.painter.line_segment(points, stroke); + }); +} + +fn paint_points( + ui: Res, + points: Query<(Entity, &geometry::ComputedPointPos)>, + hovered: Query<(), With>, + selected: Query<(), With>, +) { + points.for_each(|(id, pos)| { + let center = ui.to_screen.transform_pos(pos); + + let color = color_for_var_status(pos.status); + let stroke = if selected.contains(id) || hovered.contains(id) { + // color.s -= 0.8; + Stroke::new(1.0, Color32::WHITE) + } else { + Stroke::default() + }; + ui.painter.circle(center, POINT_RADIUS, color, stroke); + }); +} + +#[derive(Resource)] +pub struct ContextRes(pub egui::Context); + +impl Deref for ContextRes { + type Target = egui::Context; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +fn prepare(ctx: Res) { + // ctx.request_repaint(); + ctx.set_visuals(egui::Visuals::dark()); +} + +fn toolbar( + ctx: Res, + selected: Query>, + mut commands: Commands, + mut tool: ResMut, +) { + egui::TopBottomPanel::top("top_panel").show(&ctx, |ui| { + // ui.heading("sketchrs"); + let mut selected_tool = *tool; + ui.horizontal(|ui| { + ui.radio_value(&mut selected_tool, Tool::Select, "Select"); + ui.radio_value(&mut selected_tool, Tool::Move, "Move"); + ui.radio_value(&mut selected_tool, Tool::AddPoint, "Add Point"); + ui.radio_value(&mut selected_tool, Tool::AddLine, "Add Line"); + }); + if selected_tool != *tool { + *tool = selected_tool; + + for selected in selected.iter() { + commands.entity(selected).remove::(); + } + } + }); +} + +struct ShowEntitiesSchedule(Schedule); + +#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] +#[system_set(base)] +pub enum ShowEntitiesStage { + Update, + UpdateFlush, + Input, + InputFlush, + Tools, + PostTools, + ToolsFlush, + Paint, +} + +impl Default for ShowEntitiesSchedule { + fn default() -> Self { + let mut schedule = Schedule::new(); + schedule + .configure_sets( + ( + ShowEntitiesStage::Update, + ShowEntitiesStage::UpdateFlush, + ShowEntitiesStage::Input, + ShowEntitiesStage::InputFlush, + ShowEntitiesStage::Tools, + ShowEntitiesStage::ToolsFlush, + ShowEntitiesStage::PostTools, + ShowEntitiesStage::Paint, + ) + .chain(), + ) + .add_systems( + (geometry::update_point_pos, geometry::update_line_pos) + .in_base_set(ShowEntitiesStage::Update), + ) + .add_system(apply_system_buffers.in_base_set(ShowEntitiesStage::UpdateFlush)) + .add_systems( + (update_hover_point, update_hover_line).in_base_set(ShowEntitiesStage::Input), + ) + .add_system(apply_system_buffers.in_base_set(ShowEntitiesStage::InputFlush)) + .add_systems( + ( + select_tool.run_if(is_tool_active(Tool::Select)), + move_tool.run_if(is_tool_active(Tool::Move)), + add_point_tool.run_if(is_tool_active(Tool::AddPoint)), + add_line_tool.run_if(is_tool_active(Tool::AddLine)), + add_relation_tool.run_if(is_tool_active(Tool::AddRelation)), + ) + .distributive_run_if(is_hovered) + .in_base_set(ShowEntitiesStage::Tools), + ) + .add_system(geometry::remove_dangling_lines.in_base_set(ShowEntitiesStage::PostTools)) + .add_system(apply_system_buffers.in_base_set(ShowEntitiesStage::ToolsFlush)) + .add_systems( + (paint_lines, paint_points) + .chain() + .in_base_set(ShowEntitiesStage::Paint), + ); + Self(schedule) + } +} + +fn central_panel(world: &mut World, mut schedule: Local) { + let ctx = world.get_resource::().unwrap().0.clone(); + egui::CentralPanel::default() + .frame(egui::Frame::none()) + .show(&ctx, |ui| { + let sense = match *world.resource::() { + Tool::Move => Sense::drag(), + Tool::Select | Tool::AddPoint | Tool::AddLine | Tool::AddRelation => Sense::click(), + }; + + let (response, painter) = ui.allocate_painter(ui.available_size(), sense); + let to_screen = ToScreen(RectTransform::from_to( + Rect::from_center_size(Pos2::ZERO, response.rect.size() / 2.0), + response.rect, + )); + world.insert_resource(UiRes { + response, + painter, + to_screen, + }); + + schedule.0.run(world); + + world.remove_resource::(); + }); +} + +#[derive(WorldQuery)] +struct SelectableEntity<'a> { + id: Entity, + point: Option<&'a geometry::Point>, + line: Option<&'a geometry::Line>, +} + +fn side_panel_ui( + ui: &mut egui::Ui, + selected: Query>, + tool: Res, +) { + let tool = *tool; + ui.vertical(|ui| match tool { + Tool::Select => { + let mut count = 0; + selected.for_each(|sel| { + count += 1; + if sel.point.is_some() { + ui.label(format!("Selected point {}", sel.id.index())); + } else if sel.line.is_some() { + ui.label(format!("Selected line {}", sel.id.index())); + } + }); + if count == 0 { + ui.label("Nothing selected"); + } + } + Tool::Move => { + let mut count = 0; + selected.for_each(|sel| { + count += 1; + if sel.point.is_some() { + ui.label(format!("Selected point {}", sel.id.index())); + } else if sel.line.is_some() { + ui.label(format!("Selected line {}", sel.id.index())); + } + }); + if count == 0 { + ui.label("Nothing selected"); + } + } + Tool::AddPoint => { + ui.label("Click to add a point"); + } + Tool::AddLine => { + ui.label("Click to add a line"); + } + Tool::AddRelation => { + ui.label("Click to add a relation"); + } + }); +} + +fn side_panel( + ctx: Res, + selected: Query>, + tool: Res, +) { + egui::SidePanel::right("side_panel") + .resizable(true) + .default_width(150.0) + .width_range(80.0..=200.0) + .show(&ctx, |ui| side_panel_ui(ui, selected, tool)); +} + +fn bottom_panel(ctx: Res) { + egui::TopBottomPanel::bottom("bottom_panel").show(&ctx, |ui| { + ui.horizontal(|ui| { + ui.label("Status:"); + }); + }); +} + +pub fn add_to_schedule(schedule: &mut Schedule) { + schedule.add_systems((prepare, toolbar, side_panel, bottom_panel, central_panel).chain()); +}