From 64d4a018d35b1293c2019997290f0627c8ee08d8 Mon Sep 17 00:00:00 2001 From: Tom Date: Sat, 24 Feb 2024 23:59:48 -0800 Subject: [PATCH] Add basic regular polygon feature --- detailer/src/lib.rs | 67 +++++++++++++++++ drawing/src/feature.rs | 167 ++++++++++++++++++++++++++++++++++++++++- drawing/src/handler.rs | 11 +++ drawing/src/tools.rs | 48 ++++++++++++ 4 files changed, 292 insertions(+), 1 deletion(-) diff --git a/detailer/src/lib.rs b/detailer/src/lib.rs index aa96a33..175dab2 100644 --- a/detailer/src/lib.rs +++ b/detailer/src/lib.rs @@ -210,6 +210,17 @@ impl<'a> Widget<'a> { pressure_angle, meta, ), + Some(Feature::RegularPoly(meta, _p, n, a)) => { + Widget::show_selection_entry_regular_poly( + ui, + &mut commands, + &mut changed, + &k, + a, + n, + meta, + ) + } None => {} } @@ -959,6 +970,62 @@ impl<'a> Widget<'a> { }); }); } + + fn show_selection_entry_regular_poly( + ui: &mut egui::Ui, + commands: &mut Vec, + changed: &mut bool, + k: &FeatureKey, + apothem: &mut f32, + n: &mut usize, + meta: &mut FeatureMeta, + ) { + ui.horizontal(|ui| { + let r = ui.available_size(); + let text_height = egui::TextStyle::Body.resolve(ui.style()).size; + + use slotmap::Key; + ui.add( + egui::Label::new(format!("n-poly {:?}", k.data())) + .wrap(false) + .truncate(true), + ); + if r.x - ui.available_width() < FEATURE_NAME_WIDTH { + ui.add_space(FEATURE_NAME_WIDTH - (r.x - ui.available_width())); + } + + *changed |= ui + .add(egui::Checkbox::without_text(&mut meta.construction)) + .changed(); + ui.add(egui::Image::new(CONSTRUCTION_IMG).rounding(5.0)); + + if ui.available_width() > r.x / 2. - ui.spacing().item_spacing.x { + ui.add_space(ui.available_width() - r.x / 2. - ui.spacing().item_spacing.x); + } + + *changed |= ui + .add_sized( + [50., text_height * 1.4], + egui::DragValue::new(apothem) + .clamp_range(0.1..=200.0) + .suffix("mm") + .speed(0.2), + ) + .changed(); + *changed |= ui + .add_sized( + [50., text_height * 1.4], + egui::DragValue::new(n).clamp_range(3..=25).speed(1.0), + ) + .changed(); + ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { + if ui.button("⊗").clicked() { + commands.push(ToolResponse::Delete(*k)); + } + }); + }); + } + fn show_groups_tab(&mut self, ui: &mut egui::Ui, export_save: F) where F: FnOnce(&'static str, &'static str, Vec), diff --git a/drawing/src/feature.rs b/drawing/src/feature.rs index 320c0f3..009658c 100644 --- a/drawing/src/feature.rs +++ b/drawing/src/feature.rs @@ -28,6 +28,7 @@ pub struct SerializedFeature { pub x: f32, pub y: f32, pub r: f32, + pub n: Option, pub gear_info: Option, } @@ -57,6 +58,7 @@ pub enum Feature { Arc(FeatureMeta, FeatureKey, FeatureKey, FeatureKey), // start, center, end Circle(FeatureMeta, FeatureKey, f32), // center, radius SpurGear(FeatureMeta, FeatureKey, GearInfo), // center, gear details + RegularPoly(FeatureMeta, FeatureKey, usize, f32), // center, num_sides, apothem } impl Default for Feature { @@ -67,7 +69,7 @@ impl Default for Feature { impl PartialEq for Feature { fn eq(&self, other: &Feature) -> bool { - use Feature::{Arc, Circle, LineSegment, Point, SpurGear}; + use Feature::{Arc, Circle, LineSegment, Point, RegularPoly, SpurGear}; match (self, other) { (Point(_, x1, y1), Point(_, x2, y2)) => x1 == x2 && y1 == y2, (LineSegment(_, p00, p01), LineSegment(_, p10, p11)) => { @@ -99,6 +101,9 @@ impl PartialEq for Feature { }, ), ) => p0 == p1 && (m0 - m1).abs() < 0.005 && (pa0 - pa1).abs() < 0.005 && t0 == t1, + (RegularPoly(_, p0, n0, a0, ..), RegularPoly(_, p1, n1, a1, ..)) => { + p0 == p1 && n0 == n1 && (a1 - a0).abs() < 0.005 + } _ => false, } } @@ -115,6 +120,7 @@ impl Feature { Feature::Arc(meta, ..) => meta.construction, Feature::Circle(meta, ..) => meta.construction, Feature::SpurGear(meta, ..) => meta.construction, + Feature::RegularPoly(meta, ..) => meta.construction, } } @@ -125,6 +131,7 @@ impl Feature { Feature::Arc(_, p1, p2, p3) => [Some(*p1), Some(*p2), Some(*p3)], Feature::Circle(_, p, ..) => [Some(*p), None, None], Feature::SpurGear(_, p, ..) => [Some(*p), None, None], + Feature::RegularPoly(_, p, ..) => [Some(*p), None, None], } } @@ -180,6 +187,11 @@ impl Feature { .r_tip(), ) } + Feature::RegularPoly(_, p, n, a, ..) => { + let p = drawing.features.get(*p).unwrap(); + let r = a / (std::f32::consts::PI / *n as f32).cos(); + p.bb(drawing).expand(r) + } } } @@ -265,6 +277,20 @@ impl Feature { .powi(2) .min(((x_diff.powi(2) + y_diff.powi(2)).sqrt() - r_tip / vp.zoom).powi(2)) } + + Feature::RegularPoly(_, p, n, a, ..) => { + let r = a / (std::f32::consts::PI / *n as f32).cos(); + + let p = vp.translate_point(match drawing.features.get(*p).unwrap() { + Feature::Point(_, x1, y1) => egui::Pos2 { x: *x1, y: *y1 }, + _ => unreachable!(), + }); + let (x_diff, y_diff) = (hp.x - p.x, hp.y - p.y); + + ((x_diff.powi(2) + y_diff.powi(2)).sqrt() - r / vp.zoom) + .powi(2) + .min(((x_diff.powi(2) + y_diff.powi(2)).sqrt() - a / vp.zoom).powi(2)) + } } } @@ -494,6 +520,42 @@ impl Feature { } } } + + Feature::RegularPoly(meta, p, n, a, ..) => { + let f = drawing.features.get(*p).unwrap(); + let p = match f { + Feature::Point(_, x1, y1) => { + params.vp.translate_point(egui::Pos2 { x: *x1, y: *y1 }) + } + _ => panic!("unexpected subkey type: {:?}", f), + }; + let a = a / params.vp.zoom; + + let stroke = egui::Stroke { + width: 1., + color: if params.selected { + params.colors.selected + } else if params.hovered { + params.colors.hover + } else if meta.construction { + params.colors.line.gamma_multiply(0.35) + } else { + params.colors.line + }, + }; + use std::f32::consts::PI; + let r = a / (PI / *n as f32).cos(); + let a = 2.0 * PI / *n as f32; + + for i in 0..*n { + let x0 = r * (i as f32 * a).cos() + p.x; + let y0 = r * (i as f32 * a).sin() + p.y; + let x1 = r * ((i + 1) as f32 * a).cos() + p.x; + let y1 = r * ((i + 1) as f32 * a).sin() + p.y; + + painter.line_segment([(x0, y0).into(), (x1, y1).into()], stroke); + } + } } } @@ -562,6 +624,18 @@ impl Feature { ..SerializedFeature::default() }) } + Feature::RegularPoly(meta, p, n, a) => { + let p_idx = fk_to_idx.get(p).ok_or(())?; + + Ok(SerializedFeature { + kind: "regular_poly".to_string(), + meta: meta.clone(), + using_idx: vec![*p_idx], + r: *a, + n: Some(*n), + ..SerializedFeature::default() + }) + } } } @@ -615,6 +689,20 @@ impl Feature { sf.gear_info.unwrap(), )) } + "regular_poly" => { + if sf.using_idx.len() < 1 { + return Err(()); + } + if sf.n.is_none() { + return Err(()); + } + Ok(Self::RegularPoly( + sf.meta, + *idx_to_fk.get(&sf.using_idx[0]).ok_or(())?, + sf.n.unwrap(), + sf.r, + )) + } _ => Err(()), } } @@ -738,6 +826,35 @@ impl Feature { p.x as f64, p.y as f64, ))); } + + Feature::RegularPoly(_meta, p, n, a, ..) => { + let f = drawing.features.get(*p).unwrap(); + let p = match f { + Feature::Point(_, x1, y1) => egui::Pos2 { x: *x1, y: *y1 }, + _ => panic!("unexpected subkey type: {:?}", f), + }; + + use std::f32::consts::PI; + let r = a / (PI / *n as f32).cos(); + let a = 2.0 * PI / *n as f32; + + for i in 0..(*n + 1) { + let x = r * (i as f32 * a).cos() + p.x; + let y = r * (i as f32 * a).sin() + p.y; + + if i == 0 { + out.move_to(kurbo::Point { + x: x as f64, + y: y as f64, + }); + } else { + out.line_to(kurbo::Point { + x: x as f64, + y: y as f64, + }); + } + } + } }; out } @@ -786,6 +903,15 @@ impl Feature { y: 0.0, } } + + Feature::RegularPoly(_, p, n, a) => { + // TODO: fixme + drawing.features.get(*p).unwrap().start_point(drawing) + + egui::Vec2 { + x: a / (std::f32::consts::PI / *n as f32).cos(), + y: 0.0, + } + } } } @@ -832,6 +958,15 @@ impl Feature { y: 0.0, } } + + Feature::RegularPoly(_, p, n, a) => { + // TODO: fixme + drawing.features.get(*p).unwrap().start_point(drawing) + + egui::Vec2 { + x: a / (std::f32::consts::PI / *n as f32).cos(), + y: 0.0, + } + } } } } @@ -897,6 +1032,18 @@ mod tests { ..SerializedFeature::default() }), ); + assert_eq!( + Feature::RegularPoly(FeatureMeta::default(), point_key, 6, 6.9) + .serialize(&HashMap::from([(point_key, 42)])), + Ok(SerializedFeature { + kind: "regular_poly".to_string(), + meta: FeatureMeta::default(), + using_idx: vec![42], + r: 6.9, + n: Some(6), + ..SerializedFeature::default() + }), + ); } #[test] @@ -960,5 +1107,23 @@ mod tests { 6.9, )), ); + assert_eq!( + Feature::deserialize( + SerializedFeature { + kind: "regular_poly".to_string(), + using_idx: vec![1], + r: 6.9, + n: Some(6), + ..SerializedFeature::default() + }, + &HashMap::from([(1, FeatureKey::null())]), + ), + Ok(Feature::RegularPoly( + FeatureMeta::default(), + FeatureKey::null(), + 6, + 6.9, + )), + ); } } diff --git a/drawing/src/handler.rs b/drawing/src/handler.rs index becd6fb..cadd92c 100644 --- a/drawing/src/handler.rs +++ b/drawing/src/handler.rs @@ -11,6 +11,7 @@ pub enum ToolResponse { NewArc(FeatureKey, FeatureKey), NewCircle(FeatureKey, egui::Pos2), NewSpurGear(FeatureKey), + NewRegularPoly(FeatureKey), Delete(FeatureKey), NewFixedConstraint(FeatureKey), @@ -135,6 +136,16 @@ impl Handler { drawing.features.insert(g); tools.clear(); } + ToolResponse::NewRegularPoly(p_center) => { + let g = Feature::RegularPoly(FeatureMeta::default(), p_center, 6, 4.0); + + if drawing.feature_exists(&g) { + return; + } + + drawing.features.insert(g); + tools.clear(); + } ToolResponse::Delete(k) => { drawing.delete_feature(k); diff --git a/drawing/src/tools.rs b/drawing/src/tools.rs index 991a1d3..052a63b 100644 --- a/drawing/src/tools.rs +++ b/drawing/src/tools.rs @@ -320,6 +320,23 @@ fn gear_tool_icon(b: egui::Rect, painter: &egui::Painter) { ); } +fn regular_poly_tool_icon(b: egui::Rect, painter: &egui::Painter) { + let c = b.center(); + let layout = painter.layout_no_wrap( + "n-poly".into(), + egui::FontId::monospace(7.7), + 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] @@ -328,6 +345,7 @@ enum Tool { Arc(Option), Circle(Option), Gear, + RegularPoly, Fixed, Dimension, Horizontal, @@ -346,6 +364,7 @@ impl Tool { Tool::Arc(_) => "Create Arc", Tool::Circle(_) => "Create Circle", Tool::Gear => "Create spur gear", + Tool::RegularPoly => "Create regular polygon", Tool::Fixed => "Constrain to co-ords", Tool::Dimension => "Constrain length/radius", Tool::Horizontal => "Constrain horizontal", @@ -363,6 +382,7 @@ impl Tool { Tool::Arc(_) => Some("R"), Tool::Circle(_) => Some("C"), Tool::Gear => None, + Tool::RegularPoly => None, Tool::Fixed => Some("S"), Tool::Dimension => Some("D"), Tool::Horizontal => Some("H"), @@ -380,6 +400,7 @@ impl Tool { Tool::Arc(_) => Some("Creates a circular arc between points.\n\nClick on the first point and then the second to create an arc. A center point will be automatically created."), Tool::Circle(_) => Some("Creates a circle around some center point.\n\nClick on the center point, and then again in empty space to create the circle."), Tool::Gear => Some("Creates an external spur gear around some center point.\n\nClick on the center point to create the gear."), + Tool::RegularPoly => Some("Creates a regular polygon around some center point.\n\nClick on the center point to create the polygon."), Tool::Fixed => Some("Constraints a point to be at specific co-ordinates.\n\nClick a point to constrain it to (0,0). Co-ordinates can be changed later in the selection UI."), Tool::Dimension => Some("Sets the dimensions of a line or circle.\n\nClick a line/circle to constrain it to its current length/radius respectively. The constrained value can be changed later in the selection UI."), Tool::Horizontal => Some("Constrains a line to be horizontal."), @@ -398,6 +419,7 @@ impl Tool { (Tool::Arc(_), Tool::Arc(_)) => true, (Tool::Circle(_), Tool::Circle(_)) => true, (Tool::Gear, Tool::Gear) => true, + (Tool::RegularPoly, Tool::RegularPoly) => true, (Tool::Fixed, Tool::Fixed) => true, (Tool::Dimension, Tool::Dimension) => true, (Tool::Horizontal, Tool::Horizontal) => true, @@ -417,6 +439,7 @@ impl Tool { Tool::Circle(None), Tool::Arc(None), Tool::Gear, + Tool::RegularPoly, Tool::Fixed, Tool::Dimension, Tool::Horizontal, @@ -685,6 +708,25 @@ impl Tool { } None } + Tool::RegularPoly => { + if response.clicked() { + return match hover { + Hover::Feature { + k, + feature: crate::Feature::Point(..), + } => Some(ToolResponse::NewRegularPoly(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 + } Tool::Fixed => { if response.clicked() { @@ -1123,6 +1165,11 @@ impl Tool { .clone() .on_hover_text_at_pointer("new gear: click center point"); } + Tool::RegularPoly => { + response + .clone() + .on_hover_text_at_pointer("new n-poly: click center point"); + } Tool::Fixed => { response.clone().on_hover_text_at_pointer("constrain (x,y)"); @@ -1192,6 +1239,7 @@ impl Tool { Tool::Arc(_) => arc_tool_icon, Tool::Circle(_) => circle_tool_icon, Tool::Gear => gear_tool_icon, + Tool::RegularPoly => regular_poly_tool_icon, Tool::Fixed => fixed_tool_icon, Tool::Dimension => dim_tool_icon, Tool::Horizontal => horizontal_tool_icon,