diff --git a/Cargo.lock b/Cargo.lock index a31cf1c..c34a24e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -963,6 +963,7 @@ dependencies = [ "num", "serde", "slotmap", + "tiny-skia 0.11.4", "truck-meshalgo", "truck-modeling", "truck-polymesh", @@ -1068,7 +1069,7 @@ dependencies = [ "mime_guess2", "resvg", "serde", - "tiny-skia", + "tiny-skia 0.8.4", "usvg", ] @@ -2686,7 +2687,7 @@ dependencies = [ "pico-args", "rgb", "svgtypes", - "tiny-skia", + "tiny-skia 0.8.4", "usvg", ] @@ -3187,7 +3188,22 @@ dependencies = [ "bytemuck", "cfg-if", "png", - "tiny-skia-path", + "tiny-skia-path 0.8.4", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png", + "tiny-skia-path 0.11.4", ] [[package]] @@ -3201,6 +3217,17 @@ dependencies = [ "strict-num", ] +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + [[package]] name = "tinyvec" version = "1.6.0" diff --git a/detailer/src/lib.rs b/detailer/src/lib.rs index 5be20a5..aa96a33 100644 --- a/detailer/src/lib.rs +++ b/detailer/src/lib.rs @@ -191,6 +191,25 @@ impl<'a> Widget<'a> { meta, ) } + Some(Feature::SpurGear( + meta, + _p, + drawing::GearInfo { + module, + teeth, + pressure_angle, + offset: _, + }, + )) => Widget::show_selection_entry_spur_gear( + ui, + &mut commands, + &mut changed, + &k, + module, + teeth, + pressure_angle, + meta, + ), None => {} } @@ -882,6 +901,64 @@ impl<'a> Widget<'a> { }); } + fn show_selection_entry_spur_gear( + ui: &mut egui::Ui, + commands: &mut Vec, + changed: &mut bool, + k: &FeatureKey, + module: &mut f32, + teeth: &mut usize, + _pressure_angle: &mut f32, + 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!("Spur gear {:?}", 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(module) + .clamp_range(0.1..=25.0) + .prefix("m") + .speed(1.0), + ) + .changed(); + *changed |= ui + .add_sized( + [50., text_height * 1.4], + egui::DragValue::new(teeth) + .clamp_range(5..=150) + .suffix("t") + .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/Cargo.toml b/drawing/Cargo.toml index 3324398..2ff7af1 100644 --- a/drawing/Cargo.toml +++ b/drawing/Cargo.toml @@ -19,4 +19,7 @@ kurbo.workspace = true truck-modeling.workspace = true truck-polymesh.workspace = true truck-meshalgo.workspace = true -truck-topology.workspace = true \ No newline at end of file +truck-topology.workspace = true + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +tiny-skia = "0.11" \ No newline at end of file diff --git a/drawing/src/feature.rs b/drawing/src/feature.rs index 8636062..320c0f3 100644 --- a/drawing/src/feature.rs +++ b/drawing/src/feature.rs @@ -28,6 +28,26 @@ pub struct SerializedFeature { pub x: f32, pub y: f32, pub r: f32, + pub gear_info: Option, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, PartialEq)] +pub struct GearInfo { + pub module: f32, + pub teeth: usize, + pub pressure_angle: f32, + pub offset: f32, +} + +impl Default for GearInfo { + fn default() -> Self { + Self { + module: 3.0, + teeth: 5, + pressure_angle: 20.0, + offset: 0.0, + } + } } #[derive(Debug, Clone)] @@ -36,6 +56,7 @@ pub enum Feature { LineSegment(FeatureMeta, FeatureKey, FeatureKey), Arc(FeatureMeta, FeatureKey, FeatureKey, FeatureKey), // start, center, end Circle(FeatureMeta, FeatureKey, f32), // center, radius + SpurGear(FeatureMeta, FeatureKey, GearInfo), // center, gear details } impl Default for Feature { @@ -46,7 +67,7 @@ impl Default for Feature { impl PartialEq for Feature { fn eq(&self, other: &Feature) -> bool { - use Feature::{Arc, Circle, LineSegment, Point}; + use Feature::{Arc, Circle, LineSegment, Point, SpurGear}; match (self, other) { (Point(_, x1, y1), Point(_, x2, y2)) => x1 == x2 && y1 == y2, (LineSegment(_, p00, p01), LineSegment(_, p10, p11)) => { @@ -56,6 +77,28 @@ impl PartialEq for Feature { p01 == p11 && ((p00 == p10 && p02 == p12) || (p00 == p12 && p02 == p10)) } (Circle(_, p0, r0, ..), Circle(_, p1, r1, ..)) => p0 == p1 && (r1 - r0).abs() < 0.005, + ( + SpurGear( + _, + p0, + GearInfo { + module: m0, + teeth: t0, + pressure_angle: pa0, + offset: _, + }, + ), + SpurGear( + _, + p1, + GearInfo { + module: m1, + teeth: t1, + pressure_angle: pa1, + offset: _, + }, + ), + ) => p0 == p1 && (m0 - m1).abs() < 0.005 && (pa0 - pa1).abs() < 0.005 && t0 == t1, _ => false, } } @@ -71,6 +114,7 @@ impl Feature { Feature::LineSegment(meta, ..) => meta.construction, Feature::Arc(meta, ..) => meta.construction, Feature::Circle(meta, ..) => meta.construction, + Feature::SpurGear(meta, ..) => meta.construction, } } @@ -80,6 +124,7 @@ impl Feature { Feature::LineSegment(_, p1, p2) => [Some(*p1), Some(*p2), None], 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], } } @@ -114,6 +159,27 @@ impl Feature { let p = drawing.features.get(*p).unwrap(); p.bb(drawing).expand(*r) } + Feature::SpurGear( + _, + p, + GearInfo { + module: m, + teeth: t, + pressure_angle, + offset: _, + }, + .., + ) => { + let p = drawing.features.get(*p).unwrap(); + p.bb(drawing).expand( + crate::l::SpurGear { + module: *m, + teeth: *t, + pressure_angle: *pressure_angle, + } + .r_tip(), + ) + } } } @@ -170,6 +236,35 @@ impl Feature { ((x_diff.powi(2) + y_diff.powi(2)).sqrt() - r / vp.zoom).powi(2) } + + Feature::SpurGear( + _, + p, + GearInfo { + module: m, + teeth: t, + pressure_angle, + offset: _, + }, + .., + ) => { + let g = crate::l::SpurGear { + module: *m, + teeth: *t, + pressure_angle: *pressure_angle, + }; + let (r_pitch, r_tip) = (g.r_pitch(), g.r_tip()); + + 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_pitch / vp.zoom) + .powi(2) + .min(((x_diff.powi(2) + y_diff.powi(2)).sqrt() - r_tip / vp.zoom).powi(2)) + } } } @@ -290,6 +385,115 @@ impl Feature { }, ) } + + Feature::SpurGear( + meta, + p, + GearInfo { + module: m, + teeth: t, + pressure_angle, + offset: _, + }, + .., + ) => { + 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), + }; + + 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 + }, + }; + + let mut path = crate::l::SpurGear { + module: *m, + teeth: *t, + pressure_angle: *pressure_angle, + } + .path(); + path.apply_affine(kurbo::Affine::translate(kurbo::Vec2::new( + p.x as f64, p.y as f64, + ))); + + for s in path.segments() { + match s { + kurbo::PathSeg::Line(kurbo::Line { p0, p1 }) => { + painter.line_segment( + [ + params.vp.translate_point(egui::Pos2 { + x: p0.x as f32, + y: p0.y as f32, + }), + params.vp.translate_point(egui::Pos2 { + x: p1.x as f32, + y: p1.y as f32, + }), + ], + stroke, + ); + } + kurbo::PathSeg::Quad(kurbo::QuadBez { p0, p1, p2 }) => { + let shape = egui::epaint::QuadraticBezierShape::from_points_stroke( + [ + params.vp.translate_point(egui::Pos2 { + x: p0.x as f32, + y: p0.y as f32, + }), + params.vp.translate_point(egui::Pos2 { + x: p1.x as f32, + y: p1.y as f32, + }), + params.vp.translate_point(egui::Pos2 { + x: p2.x as f32, + y: p2.y as f32, + }), + ], + false, + egui::Color32::TRANSPARENT, + stroke, + ); + painter.add(shape); + } + kurbo::PathSeg::Cubic(kurbo::CubicBez { p0, p1, p2, p3 }) => { + let shape = egui::epaint::CubicBezierShape::from_points_stroke( + [ + params.vp.translate_point(egui::Pos2 { + x: p0.x as f32, + y: p0.y as f32, + }), + params.vp.translate_point(egui::Pos2 { + x: p1.x as f32, + y: p1.y as f32, + }), + params.vp.translate_point(egui::Pos2 { + x: p2.x as f32, + y: p2.y as f32, + }), + params.vp.translate_point(egui::Pos2 { + x: p3.x as f32, + y: p3.y as f32, + }), + ], + false, + egui::Color32::TRANSPARENT, + stroke, + ); + painter.add(shape); + } + } + } + } } } @@ -346,6 +550,18 @@ impl Feature { ..SerializedFeature::default() }) } + + Feature::SpurGear(meta, p, gear_info) => { + let p_idx = fk_to_idx.get(p).ok_or(())?; + + Ok(SerializedFeature { + kind: "spur".to_string(), + meta: meta.clone(), + using_idx: vec![*p_idx], + gear_info: Some(gear_info.clone()), + ..SerializedFeature::default() + }) + } } } @@ -386,6 +602,19 @@ impl Feature { sf.r, )) } + "spur" => { + if sf.using_idx.len() < 1 { + return Err(()); + } + if sf.gear_info.is_none() { + return Err(()); + } + Ok(Self::SpurGear( + sf.meta, + *idx_to_fk.get(&sf.using_idx[0]).ok_or(())?, + sf.gear_info.unwrap(), + )) + } _ => Err(()), } } @@ -481,6 +710,34 @@ impl Feature { ) .into_path(0.1); } + + Feature::SpurGear( + _, + p_center, + GearInfo { + module: m, + teeth: t, + pressure_angle, + offset: _, + }, + .., + ) => { + let p = drawing + .features + .get(*p_center) + .unwrap() + .start_point(drawing); + + out = crate::l::SpurGear { + module: *m, + teeth: *t, + pressure_angle: *pressure_angle, + } + .path(); + out.apply_affine(kurbo::Affine::translate(kurbo::Vec2::new( + p.x as f64, p.y as f64, + ))); + } }; out } @@ -502,6 +759,33 @@ impl Feature { .start_point(drawing) + egui::Vec2 { x: *radius, y: 0.0 } } + + Feature::SpurGear( + _, + p_center, + GearInfo { + module: m, + teeth: t, + pressure_angle, + offset: _, + }, + .., + ) => { + drawing + .features + .get(*p_center) + .unwrap() + .start_point(drawing) + + egui::Vec2 { + x: crate::l::SpurGear { + module: *m, + teeth: *t, + pressure_angle: *pressure_angle, + } + .r_tip(), + y: 0.0, + } + } } } @@ -522,6 +806,32 @@ impl Feature { .start_point(drawing) + egui::Vec2 { x: *radius, y: 0.0 } } + Feature::SpurGear( + _, + p_center, + GearInfo { + module: m, + teeth: t, + pressure_angle, + offset: _, + }, + .., + ) => { + drawing + .features + .get(*p_center) + .unwrap() + .start_point(drawing) + + egui::Vec2 { + x: crate::l::SpurGear { + module: *m, + teeth: *t, + pressure_angle: *pressure_angle, + } + .r_tip(), + y: 0.0, + } + } } } } diff --git a/drawing/src/handler.rs b/drawing/src/handler.rs index b7ad5c9..becd6fb 100644 --- a/drawing/src/handler.rs +++ b/drawing/src/handler.rs @@ -10,6 +10,7 @@ pub enum ToolResponse { NewLineSegment(FeatureKey, FeatureKey), NewArc(FeatureKey, FeatureKey), NewCircle(FeatureKey, egui::Pos2), + NewSpurGear(FeatureKey), Delete(FeatureKey), NewFixedConstraint(FeatureKey), @@ -123,6 +124,17 @@ impl Handler { drawing.features.insert(p); tools.clear(); } + ToolResponse::NewSpurGear(p_center) => { + let g = + Feature::SpurGear(FeatureMeta::default(), p_center, super::GearInfo::default()); + + if drawing.feature_exists(&g) { + return; + } + + drawing.features.insert(g); + tools.clear(); + } ToolResponse::Delete(k) => { drawing.delete_feature(k); diff --git a/drawing/src/l/gear.rs b/drawing/src/l/gear.rs new file mode 100644 index 0000000..b6715f4 --- /dev/null +++ b/drawing/src/l/gear.rs @@ -0,0 +1,454 @@ +#[derive(Debug, Clone)] +pub struct SpurGear { + pub module: f32, + pub teeth: usize, + pub pressure_angle: f32, +} + +impl Default for SpurGear { + fn default() -> Self { + Self { + module: 1., + teeth: 20, + pressure_angle: 20., + } + } +} + +#[allow(dead_code)] +impl SpurGear { + /// distance from pitch circle to tip circle + fn addendum(&self) -> f32 { + self.module + } + /// distance from pitch circle to root circle + fn deddendum(&self) -> f32 { + 1.25 * self.module + } + fn clearance(&self) -> f32 { + self.deddendum() - self.addendum() + } + + /// pitch circle radius + pub fn r_pitch(&self) -> f32 { + self.teeth as f32 * self.module / 2.0 + } + /// base circle radius + pub fn r_base(&self) -> f32 { + self.r_pitch() * (self.pressure_angle * std::f32::consts::PI / 180.).cos() + } + /// tip circle radius, aka outer-most radius + pub fn r_tip(&self) -> f32 { + self.r_pitch() + self.addendum() + } + /// root circle radius + pub fn r_root(&self) -> f32 { + self.r_pitch() - self.deddendum() + } + + // fillet radius + fn r_fillet(&self) -> f32 { + 1.5 * self.clearance() + } + // radius at top of fillet + fn r_top_fillet(&self) -> f32 { + let rf = ((self.r_root() + self.r_fillet()).powi(2) - self.r_fillet().powi(2)).sqrt(); + if self.r_base() < rf { + rf + self.clearance() + } else { + rf + } + } + + // angular spacing between teeth + fn pitch_tooth_rads(&self) -> f32 { + 2.0 * std::f32::consts::PI / self.teeth as f32 + } + // involute angle + fn base_to_pitch_rads(&self) -> f32 { + ((self.r_pitch().powi(2) - self.r_base().powi(2)).sqrt() / self.r_base()) + - (self.r_base() / self.r_pitch()).acos() + } + fn pitch_to_fillet_rads(&self) -> f32 { + let bp = self.base_to_pitch_rads(); + if self.r_top_fillet() > self.r_base() { + bp - ((self.r_top_fillet().powi(2) - self.r_base().powi(2)).sqrt() / self.r_base()) + - (self.r_base() / self.r_top_fillet()).cos() + } else { + bp + } + } + + fn bez_coeffs(&self) -> (Vec<[f64; 2]>, Vec<[f64; 2]>) { + let mut fs: f64 = 0.01; // fraction of length offset from base to avoid singularity + let rf = self.r_top_fillet(); + let rb = self.r_base(); + if rf > rb { + fs = (rf.powi(2) - rb.powi(2)) as f64 / (self.r_tip().powi(2) - rb.powi(2)) as f64; + // offset start to top of fillet + } + + let fm = fs + (1.0 - fs) / 4.; // fraction of length at junction (25% along profile) + let mut ded_bez = BezCoeffs::<3>::new( + self.module as f64, + self.teeth as f64, + self.pressure_angle as f64, + fs, + fm, + ) + .involute_bez_coeffs(); + let mut add_bez = BezCoeffs::<3>::new( + self.module as f64, + self.teeth as f64, + self.pressure_angle as f64, + fm, + 1., + ) + .involute_bez_coeffs(); + + // Normalize rotation + let rotate_rads = (-self.base_to_pitch_rads() - self.pitch_tooth_rads() / 4.) as f64; + for p in [ded_bez.iter_mut(), add_bez.iter_mut()] + .into_iter() + .flatten() + { + *p = [ + p[0] * rotate_rads.cos() - p[1] * rotate_rads.sin(), + p[0] * rotate_rads.sin() + p[1] * rotate_rads.cos(), + ]; + } + + (ded_bez, add_bez) + } + + pub fn path(&self) -> kurbo::BezPath { + let (ded_bez, add_bez) = self.bez_coeffs(); + + let pt = |px: f64, py: f64, r: f64| { + kurbo::Point::new(px * r.cos() - py * r.sin(), px * r.sin() + py * r.cos()) + }; + + let mut path = kurbo::BezPath::new(); + let r = self.pitch_tooth_rads() as f64; + let rf = self.r_top_fillet() as f64; + let rb = self.r_base() as f64; + let rr = self.r_root() as f64; + + let start_rads = (-self.base_to_pitch_rads() - self.pitch_tooth_rads() / 4.) as f64; + let fillet = pt(rf, 0., start_rads); + let fillet_back = pt(rf, 0., -start_rads); + + path.move_to(fillet); // start at top of fillet + for t in 0..self.teeth { + let rt = r * t as f64; + + // Line from fillet to base + if rf < rb { + path.line_to(pt(ded_bez[0][0], ded_bez[0][1], rt)); + } + + // Climb the tooth + path.curve_to( + pt(ded_bez[1][0], ded_bez[1][1], rt), + pt(ded_bez[2][0], ded_bez[2][1], rt), + pt(ded_bez[3][0], ded_bez[3][1], rt), + ); + path.curve_to( + pt(add_bez[1][0], add_bez[1][1], rt), + pt(add_bez[2][0], add_bez[2][1], rt), + pt(add_bez[3][0], add_bez[3][1], rt), + ); + + // TODO: arc? + path.line_to(pt(add_bez[3][0], -add_bez[3][1], rt)); + + // Descend the tooth + path.curve_to( + pt(add_bez[2][0], -add_bez[2][1], rt), + pt(add_bez[1][0], -add_bez[1][1], rt), + pt(add_bez[0][0], -add_bez[0][1], rt), + ); + path.curve_to( + pt(ded_bez[2][0], -ded_bez[2][1], rt), + pt(ded_bez[1][0], -ded_bez[1][1], rt), + pt(ded_bez[0][0], -ded_bez[0][1], rt), + ); + + // Line from base to fillet + if rf < rb { + path.line_to(pt(fillet_back.x, fillet_back.y, rt)); + } + + // End of fillet + path.line_to(pt(rr, 0., rt + r / 4.)); + + // Arc along root to next fillet + path.extend( + kurbo::Arc::new( + kurbo::Point::ZERO, + kurbo::Vec2::new(rr, rr), + rt + r / 4., + r / 2., + 0., + ) + .append_iter(0.001), + ); + } + + path.line_to(fillet); // close the path + path.close_path(); + path + } +} + +use std::f64::consts::PI; + +// Adapted to rust code from gearUtils-09.js +// By: Dr A.R.Collins +// +// Original source says: Kindly give credit to Dr A.R.Collins +// Thanks Doc!! +// +// See: https://www.arc.id.au/GearDrawing.html +struct BezCoeffs { + r_base: f64, + ts: f64, + te: f64, +} + +impl BezCoeffs

{ + // Parameters: + // module - sets the size of teeth (see gear design texts) + // numTeeth - number of teeth on the gear + // pressure angle - angle in degrees, usually 14.5 or 20 + // order - the order of the Bezier curve to be fitted [3, 4, 5, ..] + // fstart - fraction of distance along tooth profile to start + // fstop - fraction of distance along profile to stop + fn new(module: f64, num_teeth: f64, pressure_angle: f64, fstart: f64, fstop: f64) -> Self { + let r_pitch = module * num_teeth / 2.0; // pitch circle radius + let phi = pressure_angle; // pressure angle + let r_base = r_pitch * (phi * PI / 180.0).cos(); // base circle radius + let r_addendum = r_pitch + module; // addendum radius (outer radius) + let ta = (r_addendum.powi(2) - r_base.powi(2)).sqrt() / r_base; // involute angle at addendum + let stop = fstop; + + let start = if fstart < stop { fstart } else { 0.0 }; + + let te = (stop.sqrt()) * ta; // involute angle, theta, at end of approx + let ts = (start.sqrt()) * ta; // involute angle, theta, at start of approx + + BezCoeffs { r_base, ts, te } + } + + fn cheby_expn_coeffs(&self, j: f64, func: impl Fn(f64) -> f64) -> f64 { + let n = 50; // a suitably large number N>>p + let mut c = 0.0; + for k in 1..=n { + c += func((PI * (k as f64 - 0.5) / n as f64).cos()) + * (PI * j * (k as f64 - 0.5) / n as f64).cos(); + } + 2.0 * c / n as f64 + } + + fn cheby_poly_coeffs(&self, p: usize, func: impl Fn(f64) -> f64) -> [f64; 4] { + let mut coeffs = [0.0, 0.0, 0.0, 0.0]; + let mut fn_coeff = [0.0, 0.0, 0.0, 0.0]; + let mut t = [ + [1.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0], + ]; + + // generate the Chebyshev polynomial coefficient using + // formula T(k+1) = 2xT(k) - T(k-1) which yields + for k in 1..=p { + for j in 0..t[k].len() - 1 { + t[k + 1][j + 1] = 2.0 * t[k][j]; + } + for j in 0..t[k - 1].len() { + t[k + 1][j] -= t[k - 1][j]; + } + } + + for k in 0..=p { + fn_coeff[k] = self.cheby_expn_coeffs(k as f64, &func); + coeffs[k] = 0.0; + } + + for k in 0..=p { + for pwr in 0..=p { + coeffs[pwr] += fn_coeff[k] * t[k][pwr]; + } + } + + coeffs[0] -= self.cheby_expn_coeffs(0.0, &func) / 2.0; // fix the 0th coeff + + coeffs + } + + // Equation of involute using the Bezier parameter t as variable + fn involute_x_bez(&self, t: f64) -> f64 { + // map t (0 <= t <= 1) onto x (where -1 <= x <= 1) + let x = t * 2.0 - 1.0; + // map theta (where ts <= theta <= te) from x (-1 <=x <= 1) + let theta = x * (self.te - self.ts) / 2.0 + (self.ts + self.te) / 2.0; + self.r_base * (theta.cos() + theta * theta.sin()) + } + + fn involute_y_bez(&self, t: f64) -> f64 { + // map t (0 <= t <= 1) onto x (where -1 <= x <= 1) + let x = t * 2.0 - 1.0; + // map theta (where ts <= theta <= te) from x (-1 <=x <= 1) + let theta = x * (self.te - self.ts) / 2.0 + (self.ts + self.te) / 2.0; + self.r_base * (theta.sin() - theta * theta.cos()) + } + + fn binom(&self, n: usize, k: usize) -> f64 { + let mut coeff = 1.0; + for i in n - k + 1..=n { + coeff *= i as f64; + } + + for i in 1..=k { + coeff /= i as f64; + } + + coeff + } + + fn bez_coeff(&self, i: usize, func: impl Fn(f64) -> f64) -> f64 { + // generate the polynomial coeffs in one go + let poly_coeffs = self.cheby_poly_coeffs(P, &func); + + let mut bc = 0.0; + for j in 0..=i { + bc += self.binom(i, j) * poly_coeffs[j] / self.binom(P, j); + } + + bc + } + + fn involute_bez_coeffs(&self) -> Vec<[f64; 2]> { + // calc Bezier coeffs + let mut bz_coeffs = Vec::with_capacity(P + 1); + for i in 0..=P { + let bcoeff = [ + self.bez_coeff(i, |t| self.involute_x_bez(t)), + self.bez_coeff(i, |t| self.involute_y_bez(t)), + ]; + bz_coeffs.push(bcoeff); + } + + bz_coeffs + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn spur_basic_dimensions() { + let g = SpurGear { + module: 2.0, + teeth: 20, + ..SpurGear::default() + }; + assert_eq!(g.r_pitch(), 20.0); + assert_eq!(g.r_root(), 35.0 / 2.0); + assert_eq!(g.r_tip(), 44.0 / 2.0); + assert!((g.r_base() - 37.5877 / 2.0).abs() < 0.01); + + assert_eq!(g.addendum(), 2.0); + assert_eq!(g.deddendum(), 2.5); + } + + // #[test] + // fn spur_debug_paint() { + // use tiny_skia::*; + // let g = SpurGear { + // module: 15., + // teeth: 16, + // ..SpurGear::default() + // }; + + // let mut cyan = Paint::default(); + // cyan.set_color_rgba8(50, 127, 150, 255); + // cyan.anti_alias = true; + // let mut red = Paint::default(); + // red.set_color_rgba8(250, 27, 50, 255); + // red.anti_alias = true; + // let mut green = Paint::default(); + // green.set_color_rgba8(27, 250, 20, 255); + // green.anti_alias = true; + // let mut blue = Paint::default(); + // blue.set_color_rgba8(0, 0, 255, 255); + // blue.anti_alias = true; + + // let root = { + // let mut pb = PathBuilder::new(); + // // pb.move_to(100.0, 100.0); + // // pb.line_to(150.0, 100.0); + // // pb.close(); + // pb.push_circle(150., 150., g.r_root()); + // pb.finish().unwrap() + // }; + // let base = { + // let mut pb = PathBuilder::new(); + // pb.push_circle(150., 150., g.r_base()); + // pb.finish().unwrap() + // }; + // let pitch = { + // let mut pb = PathBuilder::new(); + // pb.push_circle(150., 150., g.r_pitch()); + // pb.finish().unwrap() + // }; + // let tip = { + // let mut pb = PathBuilder::new(); + // pb.push_circle(150., 150., g.r_tip()); + // pb.finish().unwrap() + // }; + + // let bez = { + // let mut pb = PathBuilder::new(); + // for e in g.path().elements().into_iter() { + // use kurbo::PathEl::*; + // match e { + // MoveTo(p) => pb.move_to(p.x as f32, p.y as f32), + // LineTo(p) => pb.line_to(p.x as f32, p.y as f32), + // CurveTo(p1, p2, p3) => pb.cubic_to( + // p1.x as f32, + // p1.y as f32, + // p2.x as f32, + // p2.y as f32, + // p3.x as f32, + // p3.y as f32, + // ), + // QuadTo(..) => todo!(), + // ClosePath => pb.close(), + // } + // } + + // pb.finish().unwrap() + // }; + + // let mut stroke = Stroke::default(); + // stroke.width = 1.0; + // let mut pixmap = Pixmap::new(300, 300).unwrap(); + // pixmap.fill(Color::WHITE); + // pixmap.stroke_path(&root, &cyan, &stroke, Transform::identity(), None); + // // pixmap.stroke_path(&base, &red, &stroke, Transform::identity(), None); + // pixmap.stroke_path(&pitch, &green, &stroke, Transform::identity(), None); + // pixmap.stroke_path(&tip, &blue, &stroke, Transform::identity(), None); + // pixmap.stroke_path( + // &bez, + // &red, + // &stroke, + // Transform::from_translate(150., 150.), + // None, + // ); + // pixmap.save_png("/tmp/image.png").unwrap(); + // } +} diff --git a/drawing/src/l/mod.rs b/drawing/src/l/mod.rs index ada89eb..7f30e6e 100644 --- a/drawing/src/l/mod.rs +++ b/drawing/src/l/mod.rs @@ -1,6 +1,8 @@ use egui::Pos2; pub mod draw; +mod gear; +pub use gear::SpurGear; pub mod three_d; #[derive(Debug)] diff --git a/drawing/src/lib.rs b/drawing/src/lib.rs index 3fb071d..9651394 100644 --- a/drawing/src/lib.rs +++ b/drawing/src/lib.rs @@ -5,7 +5,7 @@ pub mod l; mod data; pub use data::{group::*, Data, Hover, SelectedElement, SerializedDrawing, Viewport}; mod feature; -pub use feature::{Feature, FeatureKey, FeatureMeta, SerializedFeature}; +pub use feature::{Feature, FeatureKey, FeatureMeta, GearInfo, SerializedFeature}; mod constraints; pub use constraints::{ Axis, Constraint, ConstraintKey, ConstraintMeta, DimensionDisplay, SerializedConstraint, diff --git a/drawing/src/tools.rs b/drawing/src/tools.rs index 0dcfb9f..991a1d3 100644 --- a/drawing/src/tools.rs +++ b/drawing/src/tools.rs @@ -76,7 +76,7 @@ fn fixed_tool_icon(b: egui::Rect, painter: &egui::Painter) { let layout = painter.layout_no_wrap( "(x,y)".into(), egui::FontId::monospace(8.), - egui::Color32::WHITE, + egui::Color32::LIGHT_BLUE, ); painter.galley( @@ -303,6 +303,23 @@ fn angle_tool_icon(b: egui::Rect, painter: &egui::Painter) { ); } +fn gear_tool_icon(b: egui::Rect, painter: &egui::Painter) { + let c = b.center(); + let layout = painter.layout_no_wrap( + "gear".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] @@ -310,6 +327,7 @@ enum Tool { Line(Option), Arc(Option), Circle(Option), + Gear, Fixed, Dimension, Horizontal, @@ -327,6 +345,7 @@ impl Tool { Tool::Line(_) => "Create Line", Tool::Arc(_) => "Create Arc", Tool::Circle(_) => "Create Circle", + Tool::Gear => "Create spur gear", Tool::Fixed => "Constrain to co-ords", Tool::Dimension => "Constrain length/radius", Tool::Horizontal => "Constrain horizontal", @@ -343,6 +362,7 @@ impl Tool { Tool::Line(_) => Some("L"), Tool::Arc(_) => Some("R"), Tool::Circle(_) => Some("C"), + Tool::Gear => None, Tool::Fixed => Some("S"), Tool::Dimension => Some("D"), Tool::Horizontal => Some("H"), @@ -359,6 +379,7 @@ impl Tool { Tool::Line(_) => Some("Creates lines from existing points.\n\nClick on the first point and then the second to create a line."), 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::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."), @@ -376,6 +397,7 @@ impl Tool { (Tool::Line(_), Tool::Line(_)) => true, (Tool::Arc(_), Tool::Arc(_)) => true, (Tool::Circle(_), Tool::Circle(_)) => true, + (Tool::Gear, Tool::Gear) => true, (Tool::Fixed, Tool::Fixed) => true, (Tool::Dimension, Tool::Dimension) => true, (Tool::Horizontal, Tool::Horizontal) => true, @@ -394,6 +416,7 @@ impl Tool { Tool::Line(None), Tool::Circle(None), Tool::Arc(None), + Tool::Gear, Tool::Fixed, Tool::Dimension, Tool::Horizontal, @@ -643,6 +666,25 @@ impl Tool { None } + Tool::Gear => { + if response.clicked() { + return match hover { + Hover::Feature { + k, + feature: crate::Feature::Point(..), + } => Some(ToolResponse::NewSpurGear(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() { @@ -1076,6 +1118,11 @@ impl Tool { .clone() .on_hover_text_at_pointer("new circle: click to set radius"); } + Tool::Gear => { + response + .clone() + .on_hover_text_at_pointer("new gear: click center point"); + } Tool::Fixed => { response.clone().on_hover_text_at_pointer("constrain (x,y)"); @@ -1144,6 +1191,7 @@ impl Tool { Tool::Line(_) => line_tool_icon, Tool::Arc(_) => arc_tool_icon, Tool::Circle(_) => circle_tool_icon, + Tool::Gear => gear_tool_icon, Tool::Fixed => fixed_tool_icon, Tool::Dimension => dim_tool_icon, Tool::Horizontal => horizontal_tool_icon,