extract ui to module

This commit is contained in:
Alex Mikhalev 2023-05-08 11:11:34 -07:00
parent 4eccbf78e0
commit 6856a5830f
2 changed files with 582 additions and 215 deletions

View File

@ -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<ContextRes>) {
ctx.request_repaint();
ctx.set_visuals(egui::Visuals::dark());
}
fn toolbar(
ctx: Res<ContextRes>,
selected: Query<Entity, With<Selected>>,
mut commands: Commands,
mut tool: ResMut<Tool>,
) {
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::<Selected>();
}
}
});
}
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<ShowEntitiesSchedule>) {
let ctx = world.get_resource::<ContextRes>().unwrap().0.clone();
egui::CentralPanel::default()
.frame(egui::Frame::none())
.show(&ctx, |ui| {
let sense = match *world.resource::<Tool>() {
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::<PainterRes>();
world.remove_resource::<ResponseRes>();
world.remove_resource::<ToScreen>();
});
}
#[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<SelectableEntity, With<Selected>>,
tool: Res<Tool>,
) {
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<ContextRes>,
selected: Query<SelectableEntity, With<Selected>>,
tool: Res<Tool>,
) {
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<ContextRes>) {
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::<ui::Tool>();
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::<ContextRes>();
self.world.remove_resource::<ui::ContextRes>();
}
}

570
src/ui.rs Normal file
View File

@ -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<UiRes>,
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::<Hovered>();
}
});
}
fn update_hover_line(
ui: Res<UiRes>,
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::<Hovered>();
}
});
}
fn select_tool(
ui: Res<UiRes>,
hovered: Query<Entity, With<Hovered>>,
selected: Query<Entity, With<Selected>>,
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::<Selected>();
});
}
// 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::<Selected>();
});
} 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<UiRes>,
mut drag_delta: Local<DragDelta>,
mut point_pos: geometry::PointPosQueryMut,
hovered: Query<(Entity, &geometry::Point), With<Hovered>>,
selected: Query<(Entity, &geometry::Point), With<Selected>>,
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::<Selected>();
});
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<UiRes>, 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<UiRes>,
hovered: Query<Entity, (With<Hovered>, With<geometry::Point>)>,
selected: Query<(Entity, &geometry::ComputedPointPos), With<Selected>>,
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::<Selected>();
});
}
}
}
fn add_relation_tool(
ui: Res<UiRes>,
hovered: Query<Entity, With<Hovered>>,
selected: Query<Entity, With<Selected>>,
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::<Selected>();
});
}
}
}
fn is_hovered(ui: Res<UiRes>) -> bool {
ui.response.hover_pos().is_some()
}
fn is_tool_active(tool: Tool) -> impl System<In = (), Out = bool> + ReadOnlySystem {
IntoSystem::into_system(move |active_tool: Res<Tool>| *active_tool == tool)
}
fn paint_lines(
ui: Res<UiRes>,
lines: Query<(Entity, &geometry::ComputedLinePos)>,
hovered: Query<(), With<Hovered>>,
selected: Query<(), With<Selected>>,
) {
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<UiRes>,
points: Query<(Entity, &geometry::ComputedPointPos)>,
hovered: Query<(), With<Hovered>>,
selected: Query<(), With<Selected>>,
) {
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<ContextRes>) {
// ctx.request_repaint();
ctx.set_visuals(egui::Visuals::dark());
}
fn toolbar(
ctx: Res<ContextRes>,
selected: Query<Entity, With<Selected>>,
mut commands: Commands,
mut tool: ResMut<Tool>,
) {
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::<Selected>();
}
}
});
}
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<ShowEntitiesSchedule>) {
let ctx = world.get_resource::<ContextRes>().unwrap().0.clone();
egui::CentralPanel::default()
.frame(egui::Frame::none())
.show(&ctx, |ui| {
let sense = match *world.resource::<Tool>() {
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::<UiRes>();
});
}
#[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<SelectableEntity, With<Selected>>,
tool: Res<Tool>,
) {
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<ContextRes>,
selected: Query<SelectableEntity, With<Selected>>,
tool: Res<Tool>,
) {
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<ContextRes>) {
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());
}