diff --git a/detailer/src/lib.rs b/detailer/src/lib.rs index bda96e8..a6388ba 100644 --- a/detailer/src/lib.rs +++ b/detailer/src/lib.rs @@ -15,16 +15,12 @@ pub struct State { pub struct Widget<'a> { state: &'a mut State, - drawing: &'a mut Data, + drawing: &'a mut Data, handler: &'a mut Handler, } impl<'a> Widget<'a> { - pub fn new( - state: &'a mut State, - drawing: &'a mut Data, - handler: &'a mut Handler, - ) -> Self { + pub fn new(state: &'a mut State, drawing: &'a mut Data, handler: &'a mut Handler) -> Self { Widget { state, drawing, @@ -33,7 +29,7 @@ impl<'a> Widget<'a> { } pub fn show(mut self, ctx: &egui::Context) { - let mut window = egui::Window::new("Liquid CAD") + let window = egui::Window::new("Liquid CAD") .id(egui::Id::new("detailer_window")) .resizable(false) .constrain(true) @@ -86,7 +82,6 @@ impl<'a> Widget<'a> { } } - use drawing::CommandHandler; for c in commands.drain(..) { self.handler.handle(self.drawing, c); } diff --git a/drawing/src/data.rs b/drawing/src/data.rs new file mode 100644 index 0000000..f81ba1f --- /dev/null +++ b/drawing/src/data.rs @@ -0,0 +1,174 @@ +use crate::Feature; +use slotmap::HopSlotMap; +use std::collections::HashMap; + +const MAX_HOVER_DISTANCE: f32 = 160.0; + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct Viewport { + pub x: f32, + pub y: f32, + pub zoom: f32, +} + +impl Viewport { + pub fn screen_to_point(&self, p: egui::Pos2) -> egui::Pos2 { + egui::Pos2 { + x: self.zoom * p.x + self.x, + y: self.zoom * p.y + self.y, + } + } + pub fn translate_point(&self, p: egui::Pos2) -> egui::Pos2 { + egui::Pos2 { + x: (p.x - self.x) / self.zoom, + y: (p.y - self.y) / self.zoom, + } + } + pub fn translate_rect(&self, r: egui::Rect) -> egui::Rect { + egui::Rect { + min: self.translate_point(r.min), + max: self.translate_point(r.max), + } + } +} + +impl Default for Viewport { + fn default() -> Self { + Self { + x: 0., + y: 0., + zoom: 1., + } + } +} + +/// Data stores state about the drawing and what it is composed of. +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +pub struct Data { + pub features: HopSlotMap, + pub vp: Viewport, + + pub selected_map: HashMap, +} + +impl Default for Data { + fn default() -> Self { + Self { + features: HopSlotMap::default(), + vp: Viewport::default(), + selected_map: HashMap::default(), + } + } +} + +impl Data { + pub fn find_point_at(&self, p: egui::Pos2) -> Option { + for (k, v) in self.features.iter() { + if v.bb(self).center().distance_sq(p) < 0.0001 { + return Some(k); + } + } + None + } + + pub fn find_screen_feature(&self, hp: egui::Pos2) -> Option<(slotmap::DefaultKey, Feature)> { + let mut closest: Option<(slotmap::DefaultKey, f32, bool)> = None; + for (k, v) in self.features.iter() { + let is_point = v.is_point(); + + // Points get a head-start in terms of being considered closer, so + // they are chosen over a line segment when hovering near the end of + // a line segment. + let dist = if is_point { + v.screen_dist(self, hp, &self.vp) - (MAX_HOVER_DISTANCE / 2.) + } else { + v.screen_dist(self, hp, &self.vp) + }; + + if dist < MAX_HOVER_DISTANCE { + closest = Some( + closest + .map(|c| if dist < c.1 { (k, dist, is_point) } else { c }) + .unwrap_or((k, dist, is_point)), + ); + } + } + + match closest { + Some((k, _dist, _is_point)) => Some((k, self.features.get(k).unwrap().clone())), + None => None, + } + } + + pub fn delete_feature(&mut self, k: slotmap::DefaultKey) -> bool { + self.selected_map.remove(&k); + + match self.features.remove(k) { + Some(_v) => { + // Find and also remove any features dependent on what we just removed. + let to_delete: std::collections::HashSet = self + .features + .iter() + .map(|(k2, v2)| { + let dependent_deleted = v2 + .depends_on() + .into_iter() + .filter_map(|d| d.map(|d| d == k)) + .reduce(|p, f| p || f); + + match dependent_deleted { + Some(true) => Some(k2), + _ => None, + } + }) + .filter_map(|d| d) + .collect(); + + for k in to_delete { + self.delete_feature(k); + } + + true + } + None => false, + } + } + + pub fn selection_delete(&mut self) { + let elements: Vec<_> = self.selected_map.drain().map(|(k, _)| k).collect(); + for k in elements { + self.delete_feature(k); + } + } + + pub fn select_feature(&mut self, feature: &slotmap::DefaultKey, select: bool) { + let currently_selected = self.selected_map.contains_key(feature); + if currently_selected && !select { + self.selected_map.remove(feature); + } else if !currently_selected && select { + let next_idx = self.selected_map.values().fold(0, |acc, x| acc.max(*x)) + 1; + self.selected_map.insert(feature.clone(), next_idx); + } + } + + pub fn select_features_in_rect(&mut self, rect: egui::Rect, select: bool) { + let keys: Vec<_> = self + .features + .iter() + .filter(|(_, v)| rect.contains_rect(v.bb(self))) + .map(|(k, _)| k) + .collect(); + + for k in keys.into_iter() { + self.select_feature(&k, select); + } + } + + pub fn selection_clear(&mut self) { + self.selected_map.clear(); + } + + pub fn feature_selected(&self, feature: &slotmap::DefaultKey) -> bool { + self.selected_map.get(feature).is_some() + } +} diff --git a/drawing/src/feature.rs b/drawing/src/feature.rs index 5e1851a..a5325e4 100644 --- a/drawing/src/feature.rs +++ b/drawing/src/feature.rs @@ -51,19 +51,19 @@ impl Default for Feature { } } -impl super::DrawingFeature for Feature { - fn is_point(&self) -> bool { +impl Feature { + pub fn is_point(&self) -> bool { matches!(self, Feature::Point(_, _)) } - fn depends_on(&self) -> [Option; 2] { + pub fn depends_on(&self) -> [Option; 2] { match self { Feature::Point(_, _) => [None, None], Feature::LineSegment(p1, p2) => [Some(*p1), Some(*p2)], } } - fn bb(&self, drawing: &Data) -> egui::Rect { + pub fn bb(&self, drawing: &Data) -> egui::Rect { match self { Feature::Point(x, y) => egui::Rect { min: egui::Pos2 { x: *x, y: *y }, @@ -80,7 +80,7 @@ impl super::DrawingFeature for Feature { } } - fn screen_dist(&self, drawing: &Data, hp: egui::Pos2, vp: &Viewport) -> f32 { + pub fn screen_dist(&self, drawing: &Data, hp: egui::Pos2, vp: &Viewport) -> f32 { match self { Feature::Point(x, y) => vp .translate_point(egui::Pos2 { x: *x, y: *y }) @@ -104,9 +104,9 @@ impl super::DrawingFeature for Feature { } } - fn paint( + pub fn paint( &self, - drawing: &Data, + drawing: &Data, _k: slotmap::DefaultKey, params: &PaintParams, painter: &egui::Painter, diff --git a/drawing/src/handler.rs b/drawing/src/handler.rs index 97a6155..12f19fe 100644 --- a/drawing/src/handler.rs +++ b/drawing/src/handler.rs @@ -4,8 +4,8 @@ use crate::tools::ToolResponse; #[derive(Debug, Default)] pub struct Handler {} -impl super::CommandHandler for Handler { - fn handle(&mut self, drawing: &mut Data, c: ToolResponse) { +impl Handler { + pub fn handle(&mut self, drawing: &mut Data, c: ToolResponse) { match c { ToolResponse::Handled => {} ToolResponse::NewPoint(pos) => { diff --git a/drawing/src/lib.rs b/drawing/src/lib.rs index 90442e6..0a15d07 100644 --- a/drawing/src/lib.rs +++ b/drawing/src/lib.rs @@ -1,197 +1,13 @@ #![warn(clippy::all, rust_2018_idioms)] -use slotmap::HopSlotMap; -use std::collections::HashMap; +mod data; +pub use data::{Data, Viewport}; mod feature; pub use feature::Feature; mod handler; pub use handler::Handler; pub mod tools; -const MAX_HOVER_DISTANCE: f32 = 160.0; - -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -pub struct Viewport { - pub x: f32, - pub y: f32, - pub zoom: f32, -} - -impl Viewport { - pub fn screen_to_point(&self, p: egui::Pos2) -> egui::Pos2 { - egui::Pos2 { - x: self.zoom * p.x + self.x, - y: self.zoom * p.y + self.y, - } - } - pub fn translate_point(&self, p: egui::Pos2) -> egui::Pos2 { - egui::Pos2 { - x: (p.x - self.x) / self.zoom, - y: (p.y - self.y) / self.zoom, - } - } - pub fn translate_rect(&self, r: egui::Rect) -> egui::Rect { - egui::Rect { - min: self.translate_point(r.min), - max: self.translate_point(r.max), - } - } -} - -impl Default for Viewport { - fn default() -> Self { - Self { - x: 0., - y: 0., - zoom: 1., - } - } -} - -/// CommandHandler maps commands issued by tools into actions performed on -/// the drawing. -pub trait CommandHandler: std::fmt::Debug + Sized -where - F: DrawingFeature, - C: std::fmt::Debug + Sized, -{ - fn handle(&mut self, drawing: &mut Data, c: C); -} - -/// Data stores state about the drawing and what it is composed of. -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -pub struct Data -where - F: DrawingFeature, -{ - pub features: HopSlotMap, - pub vp: Viewport, - - pub selected_map: HashMap, -} - -impl Default for Data { - fn default() -> Self { - Self { - features: HopSlotMap::default(), - vp: Viewport::default(), - selected_map: HashMap::default(), - } - } -} - -impl Data { - pub fn find_point_at(&self, p: egui::Pos2) -> Option { - for (k, v) in self.features.iter() { - if v.bb(self).center().distance_sq(p) < 0.0001 { - return Some(k); - } - } - None - } - - pub fn find_screen_feature(&self, hp: egui::Pos2) -> Option<(slotmap::DefaultKey, F)> { - let mut closest: Option<(slotmap::DefaultKey, f32, bool)> = None; - for (k, v) in self.features.iter() { - let is_point = v.is_point(); - - // Points get a head-start in terms of being considered closer, so - // they are chosen over a line segment when hovering near the end of - // a line segment. - let dist = if is_point { - v.screen_dist(self, hp, &self.vp) - (MAX_HOVER_DISTANCE / 2.) - } else { - v.screen_dist(self, hp, &self.vp) - }; - - if dist < MAX_HOVER_DISTANCE { - closest = Some( - closest - .map(|c| if dist < c.1 { (k, dist, is_point) } else { c }) - .unwrap_or((k, dist, is_point)), - ); - } - } - - match closest { - Some((k, _dist, _is_point)) => Some((k, self.features.get(k).unwrap().clone())), - None => None, - } - } - - pub fn delete_feature(&mut self, k: slotmap::DefaultKey) -> bool { - self.selected_map.remove(&k); - - match self.features.remove(k) { - Some(_v) => { - // Find and also remove any features dependent on what we just removed. - let to_delete: std::collections::HashSet = self - .features - .iter() - .map(|(k2, v2)| { - let dependent_deleted = v2 - .depends_on() - .into_iter() - .filter_map(|d| d.map(|d| d == k)) - .reduce(|p, f| p || f); - - match dependent_deleted { - Some(true) => Some(k2), - _ => None, - } - }) - .filter_map(|d| d) - .collect(); - - for k in to_delete { - self.delete_feature(k); - } - - true - } - None => false, - } - } - - pub fn selection_delete(&mut self) { - let elements: Vec<_> = self.selected_map.drain().map(|(k, _)| k).collect(); - for k in elements { - self.delete_feature(k); - } - } - - pub fn select_feature(&mut self, feature: &slotmap::DefaultKey, select: bool) { - let currently_selected = self.selected_map.contains_key(feature); - if currently_selected && !select { - self.selected_map.remove(feature); - } else if !currently_selected && select { - let next_idx = self.selected_map.values().fold(0, |acc, x| acc.max(*x)) + 1; - self.selected_map.insert(feature.clone(), next_idx); - } - } - - pub fn select_features_in_rect(&mut self, rect: egui::Rect, select: bool) { - let keys: Vec<_> = self - .features - .iter() - .filter(|(_, v)| rect.contains_rect(v.bb(self))) - .map(|(k, _)| k) - .collect(); - - for k in keys.into_iter() { - self.select_feature(&k, select); - } - } - - pub fn selection_clear(&mut self) { - self.selected_map.clear(); - } - - pub fn feature_selected(&self, feature: &slotmap::DefaultKey) -> bool { - self.selected_map.get(feature).is_some() - } -} - /// Colors describes the colors with which different elements should be styled. #[derive(Clone, Debug, Default)] pub struct Colors { @@ -213,62 +29,20 @@ pub struct PaintParams { font_id: egui::FontId, } -/// DrawingFeature describes elements which can make up a drawing. -pub trait DrawingFeature: std::fmt::Debug + Clone + Sized { - fn is_point(&self) -> bool; - fn depends_on(&self) -> [Option; 2]; - fn bb(&self, drawing: &Data) -> egui::Rect; - fn screen_dist(&self, drawing: &Data, hp: egui::Pos2, vp: &Viewport) -> f32; - fn paint( - &self, - drawing: &Data, - k: slotmap::DefaultKey, - params: &PaintParams, - painter: &egui::Painter, - ); -} - -/// ToolController implements tools which can be used to manipulate the drawing. -pub trait ToolController: std::fmt::Debug + Sized { - type Command: std::fmt::Debug + Sized; - type Features: DrawingFeature; - - fn handle_input( - &mut self, - ui: &mut egui::Ui, - hp: Option, - hf: &Option<(slotmap::DefaultKey, Self::Features)>, - response: &egui::Response, - ) -> Option; - fn paint( - &self, - ui: &egui::Ui, - painter: &egui::Painter, - hp: Option, - params: &PaintParams, - ); -} - /// Widget implements the egui drawing widget. #[derive(Debug)] -pub struct Widget<'a, F, TC, CH> -where - F: DrawingFeature, - TC: ToolController, - CH: CommandHandler, -{ - pub drawing: &'a mut Data, - pub tools: &'a mut TC, - pub handler: &'a mut CH, +pub struct Widget<'a> { + pub drawing: &'a mut Data, + pub tools: &'a mut tools::Toolbar, + pub handler: &'a mut Handler, } -impl<'a, F, TC, CH> Widget<'a, F, TC, CH> -where - F: DrawingFeature, - TC: ToolController, - CH: CommandHandler, -{ - pub fn new(drawing: &'a mut Data, handler: &'a mut CH, tools: &'a mut TC) -> Self { +impl<'a> Widget<'a> { + pub fn new( + drawing: &'a mut Data, + handler: &'a mut Handler, + tools: &'a mut tools::Toolbar, + ) -> Self { Self { drawing, tools, @@ -281,7 +55,7 @@ where &mut self, ui: &mut egui::Ui, hp: Option, - hf: &Option<(slotmap::DefaultKey, F)>, + hf: &Option<(slotmap::DefaultKey, Feature)>, response: &egui::Response, ) -> Option { // Handle: zooming @@ -385,17 +159,8 @@ where // Handle: Ctrl-A selects all if hp.is_some() && ui.input(|i| i.key_pressed(egui::Key::A) && i.modifiers.ctrl) { - for k in self.drawing.features.keys() { - if !self.drawing.selected_map.contains_key(&k) { - let next_idx = self - .drawing - .selected_map - .values() - .fold(0, |acc, x| acc.max(*x)) - + 1; - - self.drawing.selected_map.insert(k, next_idx); - } + for k in self.drawing.features.keys().collect::>() { + self.drawing.select_feature(&k, true); } } @@ -415,7 +180,7 @@ where ui: &egui::Ui, painter: &egui::Painter, hp: Option, - hf: Option<(slotmap::DefaultKey, F)>, + hf: Option<(slotmap::DefaultKey, Feature)>, current_drag: Option, base_params: &PaintParams, ) { diff --git a/drawing/src/tools.rs b/drawing/src/tools.rs index 1f76829..1c25e78 100644 --- a/drawing/src/tools.rs +++ b/drawing/src/tools.rs @@ -242,15 +242,12 @@ pub struct Toolbar { current: Option, } -impl super::ToolController for Toolbar { - type Command = ToolResponse; - type Features = crate::Feature; - - fn handle_input( +impl Toolbar { + pub fn handle_input( &mut self, ui: &mut egui::Ui, hp: Option, - hf: &Option<(slotmap::DefaultKey, Self::Features)>, + hf: &Option<(slotmap::DefaultKey, crate::Feature)>, response: &egui::Response, ) -> Option { // Escape to exit use of a tool @@ -299,7 +296,7 @@ impl super::ToolController for Toolbar { None } - fn paint( + pub fn paint( &self, ui: &egui::Ui, painter: &egui::Painter, diff --git a/liquid-cad/src/app.rs b/liquid-cad/src/app.rs index b3060a3..ff81fc3 100644 --- a/liquid-cad/src/app.rs +++ b/liquid-cad/src/app.rs @@ -4,7 +4,7 @@ use drawing; #[derive(serde::Deserialize, serde::Serialize)] #[serde(default)] // if we add new fields, give them default values when deserializing old state pub struct App { - drawing: drawing::Data, + drawing: drawing::Data, #[serde(skip)] handler: drawing::Handler,