diff --git a/Cargo.lock b/Cargo.lock index 72d72dc..024ee27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -663,6 +663,7 @@ version = "0.1.0" dependencies = [ "drawing", "egui", + "egui_extras", "serde", "slotmap", ] @@ -804,6 +805,19 @@ dependencies = [ "winit", ] +[[package]] +name = "egui_extras" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ffe3fe5c00295f91c2a61a74ee271c32f74049c94ba0b1cea8f26eb478bc07" +dependencies = [ + "egui", + "enum-map", + "log", + "mime_guess", + "serde", +] + [[package]] name = "egui_glow" version = "0.23.0" @@ -829,6 +843,27 @@ dependencies = [ "serde", ] +[[package]] +name = "enum-map" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53337c2dbf26a3c31eccc73a37b10c1614e8d4ae99b6a50d553e8936423c1f16" +dependencies = [ + "enum-map-derive", + "serde", +] + +[[package]] +name = "enum-map-derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04d0b288e3bb1d861c4403c1774a6f7a798781dfc519b3647df2a3dd4ae95f25" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + [[package]] name = "enumflags2" version = "0.7.8" @@ -1428,6 +1463,22 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2268,6 +2319,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" diff --git a/detailer/Cargo.toml b/detailer/Cargo.toml index 7df0426..362b04b 100644 --- a/detailer/Cargo.toml +++ b/detailer/Cargo.toml @@ -8,6 +8,7 @@ rust-version = "1.71" [dependencies] egui = "0.23.0" +egui_extras = "0.23.0" slotmap = {version = "^1.0", features = ["serde"]} serde = { version = "1", features = ["derive"] } drawing = {version = "0.1.0", path = "../drawing"} \ No newline at end of file diff --git a/detailer/src/lib.rs b/detailer/src/lib.rs index 46b25e3..3f170b3 100644 --- a/detailer/src/lib.rs +++ b/detailer/src/lib.rs @@ -1,4 +1,5 @@ use drawing::{handler::ToolResponse, tools, Data, Feature, FeatureKey, Handler}; +use drawing::{Constraint, ConstraintKey}; #[derive(Debug, Default, Clone, PartialEq)] pub enum Tab { @@ -59,7 +60,7 @@ impl<'a> Widget<'a> { }; ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { - ui.add_space(4.); + ui.add_space(2.); egui::warn_if_debug_build(ui); }); }); @@ -73,19 +74,36 @@ impl<'a> Widget<'a> { } fn show_selection_tab(&mut self, ui: &mut egui::Ui) { - let mut commands: Vec = Vec::with_capacity(12); + let mut commands: Vec = Vec::with_capacity(4); let selected: Vec = self.drawing.selected_map.keys().map(|k| *k).collect(); for k in selected { - if let Some(v) = self.drawing.features.get_mut(k) { - match v { - Feature::Point(_, x, y) => { + ui.push_id(k, |ui| { + match self.drawing.feature_mut(k) { + Some(Feature::Point(_, x, y)) => { Widget::show_selection_entry_point(ui, &mut commands, &k, x, y) } - Feature::LineSegment(_, p1, p2) => { + Some(Feature::LineSegment(_, _p1, _p2)) => { Widget::show_selection_entry_line(ui, &mut commands, &k) } + None => {} } - } + + let constraints = self.drawing.constraints_by_feature(&k); + if constraints.len() > 0 { + egui::CollapsingHeader::new("Constraints") + .default_open(true) + .show(ui, |ui| { + for ck in constraints { + match self.drawing.constraint_mut(ck) { + Some(Constraint::Fixed(_, _, x, y)) => { + Widget::show_constraint_fixed(ui, &mut commands, &ck, x, y) + } + None => {} + } + } + }); + } + }); } for c in commands.drain(..) { @@ -93,6 +111,35 @@ impl<'a> Widget<'a> { } } + fn show_constraint_fixed( + ui: &mut egui::Ui, + commands: &mut Vec, + k: &ConstraintKey, + px: &mut f32, + py: &mut f32, + ) { + ui.horizontal(|ui| { + let r = ui.available_size(); + let text_height = egui::TextStyle::Body.resolve(ui.style()).size; + + let text_rect = ui + .add_sized( + [r.x / 2., text_height], + egui::Label::new("Fixed").wrap(false), + ) + .rect; + ui.add_space(r.x / 2. - text_rect.width() - ui.spacing().item_spacing.x); + + ui.add_sized([50., text_height * 1.4], egui::DragValue::new(px)); + ui.add_sized([50., text_height * 1.4], egui::DragValue::new(py)); + ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { + if ui.button("⊗").clicked() { + commands.push(ToolResponse::ConstraintDelete(*k)); + } + }); + }); + } + fn show_selection_entry_point( ui: &mut egui::Ui, commands: &mut Vec, @@ -100,21 +147,53 @@ impl<'a> Widget<'a> { px: &mut f32, py: &mut f32, ) { + // use egui_extras::{Size, StripBuilder}; + + // StripBuilder::new(ui) + // .size(Size::relative(0.42)) // name cell + // .size(Size::relative(0.23)) // x cell + // .size(Size::relative(0.23)) // y cell + // .size(Size::remainder().at_least(25.0)) + // .horizontal(|mut strip| { + // use slotmap::Key; + // strip.cell(|ui| { + // ui.label(format!("Point {:?}", k.data().as_ffi())); + // }); + // strip.cell(|ui| { + // ui.add(egui::DragValue::new(px)); + // }); + // strip.cell(|ui| { + // ui.add(egui::DragValue::new(py)); + // }); + // strip.cell(|ui| { + // if ui.button("⊗").clicked() { + // commands.push(ToolResponse::Delete(*k)); + // } + // }); + // }); + ui.horizontal(|ui| { let r = ui.available_size(); let text_height = egui::TextStyle::Body.resolve(ui.style()).size; use slotmap::Key; - ui.add_sized( - [r.x / 2., text_height], - egui::Label::new(format!("Point {:?}", k.data().as_ffi())).wrap(false), - ); - - ui.add_sized([r.x / 6., text_height * 1.4], egui::DragValue::new(px)); - ui.add_sized([r.x / 6., text_height * 1.4], egui::DragValue::new(py)); - if ui.button("⊗").clicked() { - commands.push(ToolResponse::Delete(*k)); + let text_rect = ui + .add_sized( + [r.x / 2., text_height], + egui::Label::new(format!("Point {:?}", k.data())).wrap(false), + ) + .rect; + if text_rect.width() < r.x / 2. { + ui.add_space(r.x / 2. - text_rect.width()); } + + ui.add_sized([50., text_height * 1.4], egui::DragValue::new(px)); + ui.add_sized([50., text_height * 1.4], egui::DragValue::new(py)); + ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { + if ui.button("⊗").clicked() { + commands.push(ToolResponse::Delete(*k)); + } + }); }); } @@ -130,17 +209,14 @@ impl<'a> Widget<'a> { use slotmap::Key; ui.add_sized( [r.x / 2., text_height], - egui::Label::new(format!("Line {:?}", k.data().as_ffi())).wrap(false), - ); - - ui.allocate_exact_size( - [r.x / 3. + ui.spacing().item_spacing.x, text_height * 1.4].into(), - egui::Sense::click(), + egui::Label::new(format!("Line {:?}", k.data())).wrap(false), ); - if ui.button("⊗").clicked() { - commands.push(ToolResponse::Delete(*k)); - } + ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { + if ui.button("⊗").clicked() { + commands.push(ToolResponse::Delete(*k)); + } + }); }); } diff --git a/drawing/src/constraints.rs b/drawing/src/constraints.rs index 21358d8..b184b15 100644 --- a/drawing/src/constraints.rs +++ b/drawing/src/constraints.rs @@ -26,4 +26,12 @@ impl Constraint { Fixed(..) => matches!(ft, &Feature::Point(..)), } } + + pub fn conflicts(&self, other: &Constraint) -> bool { + use Constraint::Fixed; + match (self, other) { + (Fixed(_, f1, _, _), Fixed(_, f2, _, _)) => f1 == f2, + _ => false, + } + } } diff --git a/drawing/src/data.rs b/drawing/src/data.rs index 4a5fb07..6cb4fc7 100644 --- a/drawing/src/data.rs +++ b/drawing/src/data.rs @@ -56,7 +56,7 @@ impl ConstraintData { let mut by_feature = HashMap::with_capacity(2 * self.constraints.len()); for (ck, c) in self.constraints.iter() { for fk in c.affecting_features() { - if by_feature.contains_key(&fk) { + if !by_feature.contains_key(&fk) { by_feature.insert(fk, HashSet::from([ck])); } else { by_feature.get_mut(&fk).unwrap().insert(ck); @@ -66,6 +66,54 @@ impl ConstraintData { self.by_feature = by_feature; } + + pub fn add(&mut self, c: Constraint) { + for c2 in self.constraints.values() { + if c.conflicts(c2) { + return; + } + } + + let k = self.constraints.insert(c.clone()); + for fk in c.affecting_features() { + if !self.by_feature.contains_key(&fk) { + self.by_feature.insert(fk, HashSet::from([k])); + } else { + self.by_feature.get_mut(&fk).unwrap().insert(k); + } + } + } + + pub fn delete(&mut self, ck: ConstraintKey) { + match self.constraints.remove(ck) { + Some(c) => { + for fk in c.affecting_features() { + let remaining_entries = if let Some(set) = self.by_feature.get_mut(&fk) { + set.remove(&ck); + set.len() + } else { + 99999 + }; + + if remaining_entries == 0 { + self.by_feature.remove(&fk); + } + } + } + None => {} + } + } + + pub fn by_feature(&self, k: &FeatureKey) -> Vec { + match self.by_feature.get(k) { + Some(set) => set.iter().map(|ck| ck.clone()).collect(), + None => vec![], + } + } + + pub fn get_mut<'a>(&'a mut self, ck: ConstraintKey) -> Option<&'a mut Constraint> { + self.constraints.get_mut(ck) + } } /// Data stores state about the drawing and what it is composed of. @@ -90,6 +138,28 @@ impl Default for Data { } impl Data { + pub fn feature_mut<'a>(&'a mut self, k: FeatureKey) -> Option<&'a mut Feature> { + let Data { features, .. } = self; + + features.get_mut(k) + } + + pub fn constraint_mut<'a>(&'a mut self, ck: ConstraintKey) -> Option<&'a mut Constraint> { + self.constraints.get_mut(ck) + } + + pub fn constraints_by_feature(&self, k: &FeatureKey) -> Vec { + self.constraints.by_feature(k) + } + + pub fn add_constraint(&mut self, c: Constraint) { + self.constraints.add(c); + } + + pub fn delete_constraint(&mut self, k: ConstraintKey) { + self.constraints.delete(k); + } + 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 { diff --git a/drawing/src/handler.rs b/drawing/src/handler.rs index c527db0..9fa0f0f 100644 --- a/drawing/src/handler.rs +++ b/drawing/src/handler.rs @@ -1,5 +1,6 @@ use super::{Data, Feature, FeatureKey, FeatureMeta}; use crate::tools::Toolbar; +use crate::{Constraint, ConstraintKey, ConstraintMeta}; #[derive(Debug)] pub enum ToolResponse { @@ -8,6 +9,9 @@ pub enum ToolResponse { NewPoint(egui::Pos2), NewLineSegment(egui::Pos2, egui::Pos2), Delete(FeatureKey), + + NewFixedConstraint(FeatureKey), + ConstraintDelete(ConstraintKey), } #[derive(Debug, Default)] @@ -55,6 +59,18 @@ impl Handler { ToolResponse::Delete(k) => { drawing.delete_feature(k); } + ToolResponse::ConstraintDelete(k) => { + drawing.delete_constraint(k); + } + + ToolResponse::NewFixedConstraint(k) => match drawing.features.get(k) { + Some(Feature::Point(..)) => { + drawing.add_constraint(Constraint::Fixed(ConstraintMeta::default(), k, 0., 0.)); + + tools.clear(); + } + _ => {} + }, } } } diff --git a/drawing/src/lib.rs b/drawing/src/lib.rs index 5cb7ac6..1cab98e 100644 --- a/drawing/src/lib.rs +++ b/drawing/src/lib.rs @@ -183,6 +183,7 @@ impl<'a> Widget<'a> { painter: &egui::Painter, hp: Option, hf: Option<(FeatureKey, Feature)>, + response: &egui::Response, current_drag: Option, base_params: &PaintParams, ) { @@ -225,7 +226,7 @@ impl<'a> Widget<'a> { ); } - self.tools.paint(ui, painter, hp, &base_params); + self.tools.paint(ui, painter, response, hp, &base_params); self.draw_debug(ui, painter, hp, &base_params); } @@ -307,7 +308,11 @@ impl<'a> Widget<'a> { vp: self.drawing.vp.clone(), colors: Colors { point: egui::Color32::GREEN, - line: egui::Color32::LIGHT_GRAY, + line: if ui.visuals().dark_mode { + egui::Color32::LIGHT_GRAY + } else { + egui::Color32::DARK_GRAY + }, selected: egui::Color32::RED, hover: egui::Color32::YELLOW, text: ui.visuals().text_color(), @@ -319,7 +324,7 @@ impl<'a> Widget<'a> { }; let painter = ui.painter(); - self.draw(ui, painter, hp, hf, current_drag, &base_params); + self.draw(ui, painter, hp, hf, &response, current_drag, &base_params); DrawResponse {} } } diff --git a/drawing/src/tools.rs b/drawing/src/tools.rs index 73cfd8b..b32b88b 100644 --- a/drawing/src/tools.rs +++ b/drawing/src/tools.rs @@ -69,11 +69,29 @@ fn line_tool_icon(b: egui::Rect, painter: &egui::Painter) { ); } +fn fixed_tool_icon(b: egui::Rect, painter: &egui::Painter) { + let c = b.center(); + let layout = painter.layout_no_wrap( + "(x,y)".into(), + egui::FontId::monospace(8.), + egui::Color32::WHITE, + ); + + painter.galley( + c + egui::Vec2 { + x: -layout.rect.width() / 2., + y: -layout.rect.height() / 2., + }, + layout, + ); +} + #[derive(Debug, Default, Clone)] enum Tool { #[default] Point, Line(Option), + Fixed, } impl Tool { @@ -81,12 +99,13 @@ impl Tool { match (self, other) { (Tool::Point, Tool::Point) => true, (Tool::Line(_), Tool::Line(_)) => true, + (Tool::Fixed, Tool::Fixed) => true, _ => false, } } pub fn all<'a>() -> &'a [Tool] { - &[Tool::Point, Tool::Line(None)] + &[Tool::Point, Tool::Line(None), Tool::Fixed] } pub fn toolbar_size() -> egui::Pos2 { @@ -166,20 +185,62 @@ impl Tool { None } + + Tool::Fixed => { + if response.clicked() { + return match hf { + Some((k, crate::Feature::Point(_, _, _))) => { + Some(ToolResponse::NewFixedConstraint(k.clone())) + } + _ => Some(ToolResponse::SwitchToPointer), + }; + } + + // Intercept drag events. + if response.drag_started_by(egui::PointerButton::Primary) + || response.drag_released_by(egui::PointerButton::Primary) + { + return Some(ToolResponse::Handled); + } + None + } } } - pub fn draw_active(&self, painter: &egui::Painter, hp: egui::Pos2, params: &PaintParams) { + pub fn draw_active( + &self, + painter: &egui::Painter, + response: &egui::Response, + hp: egui::Pos2, + params: &PaintParams, + ) { match self { - Tool::Line(Some(start)) => painter.line_segment( - [params.vp.translate_point(*start), hp], - egui::Stroke { - width: TOOL_ICON_STROKE, - color: egui::Color32::WHITE, - }, - ), + Tool::Line(None) => { + response + .clone() + .on_hover_text_at_pointer("new line: click 1st point"); + } + Tool::Line(Some(start)) => { + painter.line_segment( + [params.vp.translate_point(*start), hp], + egui::Stroke { + width: TOOL_ICON_STROKE, + color: egui::Color32::WHITE, + }, + ); + + response + .clone() + .on_hover_text_at_pointer("new line: click 2nd point"); + } - _ => {} + Tool::Point => { + response.clone().on_hover_text_at_pointer("new point"); + } + + Tool::Fixed => { + response.clone().on_hover_text_at_pointer("constrain (x,y)"); + } } } @@ -187,6 +248,7 @@ impl Tool { match self { Tool::Point => point_tool_icon, Tool::Line(_) => line_tool_icon, + Tool::Fixed => fixed_tool_icon, } } @@ -222,15 +284,6 @@ impl Tool { self.icon_painter()(bounds, painter); - // painter.rect_stroke( - // bounds, - // egui::Rounding::ZERO, - // egui::Stroke { - // width: TOOL_ICON_STROKE, - // color: params.colors.text, - // }, - // ); - bounds } } @@ -260,16 +313,26 @@ impl Toolbar { // Hotkeys for switching tools if hp.is_some() && !response.dragged() { - let (l, p) = ui.input(|i| (i.key_pressed(egui::Key::L), i.key_pressed(egui::Key::P))); - match (l, p) { - (true, _) => { + let (l, p, s) = ui.input(|i| { + ( + i.key_pressed(egui::Key::L), + i.key_pressed(egui::Key::P), + i.key_pressed(egui::Key::S), + ) + }); + match (l, p, s) { + (true, _, _) => { self.current = Some(Tool::Line(None)); return Some(ToolResponse::Handled); } - (_, true) => { + (_, true, _) => { self.current = Some(Tool::Point); return Some(ToolResponse::Handled); } + (_, _, true) => { + self.current = Some(Tool::Fixed); + return Some(ToolResponse::Handled); + } _ => {} } } @@ -302,6 +365,7 @@ impl Toolbar { &self, ui: &egui::Ui, painter: &egui::Painter, + response: &egui::Response, hp: Option, params: &PaintParams, ) { @@ -330,7 +394,7 @@ impl Toolbar { } if let (Some(hp), Some(tool)) = (hp, self.current.as_ref()) { - tool.draw_active(painter, hp, params); + tool.draw_active(painter, response, hp, params); } } } diff --git a/liquid-cad/src/app.rs b/liquid-cad/src/app.rs index 76215e5..a586afe 100644 --- a/liquid-cad/src/app.rs +++ b/liquid-cad/src/app.rs @@ -79,6 +79,9 @@ impl eframe::App for App { if ui.button("Quit").clicked() { _frame.close(); } + if ui.button("Reset egui state").clicked() { + ctx.memory_mut(|mem| *mem = Default::default()); + } }); ui.add_space(16.0); }