/*****************************************************************************/ /* gear.cpp - plugin gear for LibreCAD */ /* */ /* Copyright (C) 2016 Cédric Bosdonnat cedric@bosdonnat.fr */ /* Edited 2017 Luis Colorado */ /* */ /* This library is free software, licensed under the terms of the GNU */ /* General Public License as published by the Free Software Foundation, */ /* either version 2 of the License, or (at your option) any later version. */ /* You should have received a copy of the GNU General Public License */ /* along with this program. If not, see . */ /*****************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "document_interface.h" #include "gear.h" QString LC_Gear::name() const { return (tr("Gear creation plugin")); } PluginCapabilities LC_Gear::getCapabilities() const { PluginCapabilities pluginCapabilities; pluginCapabilities.menuEntryPoints << PluginMenuLocation("plugins_menu", tr("Gear plugin")); return pluginCapabilities; } void LC_Gear::execComm([[maybe_unused]] Document_Interface *doc, QWidget *parent, [[maybe_unused]] QString cmd) { QPointF center; if (!doc->getPoint(¢er, QString("select center"))) { return; } if (!parameters_dialog) { parameters_dialog = new lc_Geardlg(parent); if (!parameters_dialog) { return; } } int result = parameters_dialog->exec(); if (result == QDialog::Accepted) parameters_dialog->processAction(doc, cmd, center); } /*****************************/ lc_Geardlg::lc_Geardlg(QWidget *parent) : QDialog(parent), settings(QSettings::IniFormat, QSettings::UserScope, "LibreCAD", "gear_plugin") { const char *windowTitle = "Draw a gear"; setWindowTitle(tr(windowTitle)); QLabel *label; QGridLayout *mainLayout = new QGridLayout(this); int i = 0, j = 0; #define RST() do{ if(j) { ++i; j = 0; } } while(0) #define Q(name, type, text, min, max, stp) do { \ label = new QLabel((text), this); \ name = new type(this); \ name->setMinimum(min); \ name->setMaximum(max); \ name->setSingleStep(stp); \ mainLayout->addWidget(label, i, 0); \ mainLayout->addWidget((name), i, 1); \ } while(0) #define QDSB(name, text, min, max, stp, dec) do { \ RST(); \ Q(name, QDoubleSpinBox, (text),(min),(max),(stp)); \ name->setDecimals(dec); \ ++i; j = 0; \ } while(0) #define QSB(name, text, min, max, stp) do { \ RST(); \ Q(name, QSpinBox, (text),(min),(max), (stp)); \ ++i; j = 0; \ } while(0) #define QCB(name, text) do { \ name = new QCheckBox((text), this); \ mainLayout->addWidget(name, i, j); \ j++; if (j >= 2) { j = 0; i++; } \ } while(0) QDSB(rotateBox, tr("Rotation angle"), -360.0, 360.0, 1.0, 6); QSB (nteethBox, tr("Number of teeth"), 1, 2000, 1); QDSB(modulusBox, tr("Modulus"), 1.0E-10, 1.0E+10, 0.1, 6); QDSB(pressureBox, tr("Pressure angle (deg)"), 0.1, 89.9, 1.0, 5); QDSB(addendumBox, tr("Addendum (rel. to modulus)"), 0.0, 5.0, 0.1, 5); QDSB(dedendumBox, tr("Dedendum (rel. to modulus)"), 0.0, 5.0, 0.1, 5); QSB (n1Box, tr("Number of segments to draw (dedendum)"), 1, 1024, 8); QSB (n2Box, tr("Number of segments to draw (addendum)"), 1, 1024, 8); QCB (drawAllTeethBox, tr("Draw all teeth?")); QCB (drawBothSidesOfToothBox, tr("Draw symmetric face?")); QCB (useLayersBox, tr("Use layers?")); RST(); QCB (drawAddendumCircleBox, tr("Draw addendum circle?")); QCB (drawPitchCircleBox, tr("Draw pitch circle?")); QCB (drawBaseCircleBox, tr("Draw base circle?")); QCB (drawRootCircleBox, tr("Draw root circle?")); QCB (drawPressureLineBox, tr("Draw pressure line?")); QCB (drawPressureLimitBox, tr("Draw pressure limits?")); QCB (calcInterferenceBox, tr("Calculate interference?")); QSB (n3Box, tr("Number of segments to draw (interference)"), 1, 1024, 8); QPushButton *acceptbut = new QPushButton(tr("Accept"), this); QPushButton *cancelbut = new QPushButton(tr("Cancel"), this); QHBoxLayout *acceptLayout = new QHBoxLayout(); acceptLayout->addStretch(); acceptLayout->addWidget(acceptbut); acceptLayout->addStretch(); acceptLayout->addWidget(cancelbut); acceptLayout->addStretch(); mainLayout->addLayout(acceptLayout, i, 0, 1, 2); setLayout(mainLayout); readSettings(); connect(cancelbut, SIGNAL(clicked()), this, SLOT(reject())); connect(acceptbut, SIGNAL(clicked()), this, SLOT(checkAccept())); } /* calculate the radius of a point in canonical evoluta * whose radius is given. */ static double radius2arg(const double radius, const double alpha = 0.0) { const double aux = 1.0 - alpha; return sqrt(radius * radius - aux*aux); } /* canonical evolute is generated by a 1.0 radius circle. * We consider it the next complex function: * (1.0 - alpha - i*phi) * exp(i*phi) */ static double re_evolute(const double phi, const double alpha = 0.0) { return (1.0 - alpha) * cos(phi) + phi * sin(phi); } static double im_evolute(const double phi, const double alpha = 0.0) { return (1.0 - alpha) * sin(phi) - phi * cos(phi); } static double mod_evolute(const double phi, const double alpha = 0.0) { double aux = (1.0 - alpha); return sqrt(aux*aux + phi*phi); } static double arg_evolute(const double phi, const double alpha = 0.0) { double aux = (1.0 - alpha); return phi - atan2(phi, aux); } struct evolute { static const double default_eps; evolute(int n_t, double add, double ded, double p_ang); QPointF evo0(const double phi); /* evolute for tooth face */ QPointF evo1(const double phi); /* evolute for tooth carving (interference) */ double aux(const double phi); /* auxiliary function */ double find_common_phi_evo1(const double eps = default_eps); const int n_teeth; const double addendum, dedendum, c_modulus, p_angle, cos_p_angle, cos2_p_angle, angle_0, cos_angle_0, sin_angle_0, dedendum_radius, addendum_radius, phi_at_dedendum, phi_at_addendum, alpha, angle_1, cos_angle_1, sin_angle_1; }; const double evolute::default_eps = 8 * DBL_EPSILON; evolute::evolute(int n_t, double add, double ded, double p_ang): n_teeth(n_t), addendum(add), dedendum(ded), c_modulus(2.0/n_teeth), p_angle(p_ang), cos_p_angle(std::cos(p_ang)), cos2_p_angle(cos_p_angle * cos_p_angle), angle_0(p_angle - tan(p_angle)), cos_angle_0(std::cos(angle_0)), sin_angle_0(sin(angle_0)), dedendum_radius(1.0 - c_modulus * dedendum), addendum_radius(1.0 + c_modulus * addendum), phi_at_dedendum(dedendum_radius > cos_p_angle ? radius2arg(dedendum_radius / cos_p_angle) : 0.0), phi_at_addendum(radius2arg(addendum_radius / cos_p_angle)), alpha(1.0 - dedendum_radius), angle_1(-alpha * tan(p_angle)), cos_angle_1(std::cos(angle_1)), sin_angle_1(sin(angle_1)) { } /* this evolute calculates points for an argument phi for the * curve that defines de active face of the tooth. */ QPointF evolute::evo0(const double phi) { double x = cos_p_angle * re_evolute(phi), y = cos_p_angle * im_evolute(phi); return QPointF(cos_angle_0 * x - sin_angle_0 * y, sin_angle_0 * x + cos_angle_0 * y); } /* this evolute calculates points for an argument phi for the * curve that defines the carved neck of the tooth in case of * interference. */ QPointF evolute::evo1(const double phi) { double x = re_evolute(phi, alpha); double y = im_evolute(phi, alpha); return QPointF(cos_angle_1 * x - sin_angle_1 * y, sin_angle_1 * x + cos_angle_1 * y); } /* Auxiliary function to determine if we are in one side of the * primary evolute or in the other side. We look for a zero in this * function to derive the phi angle of the secondary evolute at which * it crosses the primary. This is the common point for both evolutes */ double evolute::aux(const double phi) { const double mod = mod_evolute(phi, alpha); const double arg = arg_evolute(phi, alpha); if (mod <= cos_p_angle) { return arg + angle_1 - angle_0; } const double phi0 = radius2arg(mod / cos_p_angle); return arg + angle_1 + atan(phi0) - phi0 - angle_0; } /* find the common point of both evolutes. this function uses two * values a and b of phi in the evolute::evo1 curve to find a root of * the function evolute::aux that gives the difference between the argument * at which the evolute of the primary curve touches the base circle minus * the argument at which an evolute that pases for the point calculated in * the second curve hits the base circle. This being positive means the * second evolute has already crossed the first. Being negative means it * has not yet crossed the primary evolute. */ double evolute::find_common_phi_evo1(const double eps) { double a = -radius2arg(cos_p_angle, alpha); double b = -radius2arg(1.0, alpha); double f_a = aux(a), f_b = aux(b); double x = a; if (f_a > 0) do { x = (a*f_b - b*f_a) / (f_b - f_a); double f_x = aux(x); if (fabs(x - a) < fabs(x - b)) { b = x; f_b = f_x; } else { a = x; f_a = f_x; } } while (fabs(a-b) >= eps); return x; } /* find_common_phi_evo1 */ void lc_Geardlg::processAction([[maybe_unused]] Document_Interface *doc, [[maybe_unused]] const QString& cmd, [[maybe_unused]] QPointF& center) { std::vector polyline; std::vector first_tooth; QTransform rotate_and_disp; /* we shall proceed by calculating the points for root radius, * base of tooth, n1 line segments to the pitch circle (this makes * possible to have a reference point on the pitch circle) * n2 line segments to the addendum circle. * The tooth face is aligned so the pitch point is aligned with the * X axis, so we can align gears over this reference point. * * Once we get one face, we mirror it using as the axis one quarter * of the pitch angular modulus (half of the tooth width) passing * along the origin. * * Finally, we get the complete set of teeth by rotating them by the * pitch angular modulus to get the whole gear */ evolute ev(nteethBox->value(), /* number of teeth */ addendumBox->value(), /* addendum */ dedendumBox->value(), /* dedendum */ M_PI / 180.0 * pressureBox->value()); /* pressure angle (converted to rad) */ const double modulus = modulusBox->value(); const double scale_factor = modulus / ev.c_modulus; const int n1 = n1Box->value(); const int n2 = n2Box->value(); const double rotation = rotateBox->value() * M_PI / 180.0; rotate_and_disp = rotate_and_disp .translate(center.x(), center.y()) .rotateRadians(rotation); double phi_0 = 0.0; /* Build one tooth face */ if (calcInterferenceBox->isChecked() && ev.cos2_p_angle > ev.dedendum_radius) { const int n3 = n3Box->value(); double angle_2 = ev.find_common_phi_evo1(); phi_0 = radius2arg(mod_evolute(angle_2, ev.alpha) / ev.cos_p_angle); double phi = 0.0, delta = angle_2 / n3; for(int i = 0; i < n3; i++) { const QPointF point(scale_factor * ev.evo1(phi)); first_tooth.push_back(point); polyline.push_back(Plug_VertexData(rotate_and_disp.map(point), 0.0)); phi += delta; } /* for */ } else if (ev.cos_p_angle > ev.dedendum_radius) { /* no interference calculation at all. just draw the point at the * intersection of the root circle with the 0 press angle point. */ QPointF root(scale_factor * ev.dedendum_radius * ev.cos_angle_0, scale_factor * ev.dedendum_radius * ev.sin_angle_0); first_tooth.push_back(root); polyline.push_back(Plug_VertexData(rotate_and_disp.map(root), 0.0)); } if (phi_0 < ev.phi_at_dedendum) phi_0 = ev.phi_at_dedendum; double phi = phi_0; /* if the carving has eaten some active part of the tooth dedendum face */ if (phi < ev.p_angle - ev.angle_0) { double delta = (ev.p_angle - ev.angle_0 - phi) / n1; for (int i = 0; i < n1; ++i) { const QPointF point(scale_factor * ev.evo0(phi)); first_tooth.push_back(point); polyline.push_back(Plug_VertexData(rotate_and_disp.map(point), 0.0)); phi += delta; } /* for */ } double phi_1 = radius2arg(ev.addendum_radius / ev.cos_p_angle); if (phi < phi_1) { double delta = (phi_1 - phi) / n2; for (int i = 0; i <= n2; ++i) { const QPointF point(scale_factor * ev.evo0(phi)); first_tooth.push_back(point); polyline.push_back(Plug_VertexData(rotate_and_disp.map(point), 0.0)); phi += delta; } } /* calculate the symmetric face from the original points */ /* one half of pitch angular modulus = double of symmetry axis. * symmetry on an axis that passes through origin is calculated as follows: * x' = cos(2.0*axis_angle) * x + sin(2.0*axis_angle) * y * y' = sin(2.0*axis_angle) * x - cos(2.0*axis_angle) * y * (note: we don't use iterators as the array is growing as long as we * navigate it) */ const double axis_angle_x_2 = M_PI / ev.n_teeth; const double axis_angle = axis_angle_x_2 / 2.0; const double cos_axis_angle_x_2 = std::cos(axis_angle_x_2); const double sin_axis_angle_x_2 = std::sin(axis_angle_x_2); /* remember size, as we don't want to duplicate next point */ const double n_to_mirror = first_tooth.size(); if (drawBothSidesOfToothBox->isChecked()) { /* symmetry axis point (at top of tooth) */ QPointF mirror_point(scale_factor * ev.addendum_radius * std::cos(axis_angle), scale_factor * ev.addendum_radius * std::sin(axis_angle)); first_tooth.push_back(mirror_point); polyline.push_back(Plug_VertexData(rotate_and_disp.map(mirror_point), 0.0)); /* for all points we have to mirror (all but the last one) */ for (int i = n_to_mirror - 1; i >= 0; --i) { const QPointF& orig(first_tooth[i]); QPointF target(cos_axis_angle_x_2 * orig.x() + sin_axis_angle_x_2 * orig.y(), sin_axis_angle_x_2 * orig.x() - cos_axis_angle_x_2 * orig.y()); first_tooth.push_back(target); polyline.push_back(Plug_VertexData(rotate_and_disp.map(target), 0.0)); } /* for */ if (drawAllTeethBox->isChecked()) { /* symmetry axis point (at interteeth) */ QPointF mirror_point2(scale_factor * ev.dedendum_radius * std::cos(axis_angle + axis_angle_x_2), scale_factor * ev.dedendum_radius * std::sin(axis_angle + axis_angle_x_2)); first_tooth.push_back(mirror_point2); polyline.push_back(Plug_VertexData(rotate_and_disp.map(mirror_point2), 0.0)); /* now, we have to rotate the tooth to get all the teeth missing. */ for (int i = 1; i < ev.n_teeth; i++) { const double angle = M_PI * ev.c_modulus * i; const double cos_angle = std::cos(angle); const double sin_angle = std::sin(angle); for (std::vector::iterator it = first_tooth.begin(); it != first_tooth.end(); ++it) { const QPointF& orig = *it; polyline.push_back(Plug_VertexData(rotate_and_disp.map(QPointF( cos_angle * orig.x() - sin_angle * orig.y(), sin_angle * orig.x() + cos_angle * orig.y())), 0.0)); } /* for */ } /* for */ } } QString lastLayer = doc->getCurrentLayer(); #define LAYER(fmt) do { \ if (useLayersBox->isChecked()) { \ char buffer[128]; \ snprintf(buffer, sizeof buffer, \ "gear_M%6.4f_" fmt, modulus); \ doc->setLayer(buffer); \ } \ } while(0) LAYER("shapes"); doc->addPolyline(polyline, drawAllTeethBox->isChecked() && drawBothSidesOfToothBox->isChecked()); if (drawPitchCircleBox->isChecked()) { LAYER("pitch_circles"); doc->addCircle(¢er, scale_factor); } if (drawAddendumCircleBox->isChecked()) { LAYER("addendums"); doc->addCircle(¢er, scale_factor * ev.addendum_radius); } if (drawRootCircleBox->isChecked()) { LAYER("dedendums"); doc->addCircle(¢er, scale_factor * ev.dedendum_radius); } if (drawBaseCircleBox->isChecked()) { LAYER("base_lines"); doc->addCircle(¢er, scale_factor * ev.cos_p_angle); } if (drawPressureLineBox->isChecked() || drawPressureLimitBox->isChecked()) { LAYER("action_lines"); QPointF p1(scale_factor * std::cos(ev.p_angle + rotation) * ev.cos_p_angle, scale_factor * std::sin(ev.p_angle + rotation) * ev.cos_p_angle), p2(scale_factor * cos(rotation), scale_factor * sin(rotation)); p1 += center; p2 += center; if (drawPressureLimitBox->isChecked()) doc->addLine(¢er, &p1); if (drawPressureLineBox->isChecked()) doc->addLine(&p1, &p2); } if (useLayersBox->isChecked()) doc->setLayer(lastLayer); writeSettings(); } void lc_Geardlg::checkAccept() { accept(); } lc_Geardlg::~lc_Geardlg() = default; void lc_Geardlg::closeEvent(QCloseEvent *event) { QWidget::closeEvent(event); } void lc_Geardlg::readSettings() { QPoint pos = settings.value("pos", QPoint(200, 200)).toPoint(); QSize size = settings.value("size", QSize(430,140)).toSize(); #define R(var,toFunc, defval) do { \ var ## Box->setValue(settings.value(#var, defval).toFunc()); \ } while(0) #define RB(var,defval) do { \ var ## Box->setChecked(settings.value(#var, defval).toBool()); \ } while (0) R(rotate, toDouble, 0.0 ); R(nteeth, toInt, 20 ); R(modulus, toDouble, 1.0 ); R(pressure, toDouble, 20.0 ); R(addendum, toDouble, 1.0 ); R(dedendum, toDouble, 1.25); R(n1, toInt, 16 ); R(n2, toInt, 16 ); RB(drawAllTeeth, true ); RB(drawBothSidesOfTooth, true ); RB(useLayers, true ); RB(drawAddendumCircle, false ); RB(drawPitchCircle, true ); RB(drawBaseCircle, true ); RB(drawRootCircle, false ); RB(drawPressureLine, true ); RB(drawPressureLimit, false ); RB(calcInterference, false ); R(n3, toInt, 16 ); resize(size); move(pos); } void lc_Geardlg::writeSettings() { #define W(var, vfunc) do { \ settings.setValue(#var, var##Box->vfunc()); \ } while (0) #define WN(var) W(var, value) #define WB(var) W(var, isChecked) settings.setValue("pos", pos()); settings.setValue("size", size()); WN(nteeth); WN(modulus); WN(pressure); WN(addendum); WN(dedendum); WN(n1); WN(n2); WB(drawAllTeeth); WB(drawBothSidesOfTooth); WB(useLayers); WB(drawAddendumCircle); WB(drawPitchCircle); WB(drawBaseCircle); WB(drawRootCircle); WB(drawPressureLine); WB(drawPressureLimit); WB(calcInterference); WN(n3); }