From ccbe4c1c49ff8d1939450481e5fc5b49403c9262 Mon Sep 17 00:00:00 2001 From: Paddle Date: Fri, 3 Nov 2023 18:49:11 +0100 Subject: [PATCH] Ellipse DSH : Implement tool parameters. --- src/Mod/Sketcher/Gui/CommandCreateGeo.cpp | 12 +- .../Sketcher/Gui/DrawSketchHandlerEllipse.h | 1685 +++++++++-------- 2 files changed, 888 insertions(+), 809 deletions(-) diff --git a/src/Mod/Sketcher/Gui/CommandCreateGeo.cpp b/src/Mod/Sketcher/Gui/CommandCreateGeo.cpp index 16fe229e0d..9b7efbd692 100644 --- a/src/Mod/Sketcher/Gui/CommandCreateGeo.cpp +++ b/src/Mod/Sketcher/Gui/CommandCreateGeo.cpp @@ -630,7 +630,7 @@ CONSTRUCTION_UPDATE_ACTION(CmdSketcherCreateEllipseByCenter, "Sketcher_CreateEll void CmdSketcherCreateEllipseByCenter::activated(int iMsg) { Q_UNUSED(iMsg); - ActivateHandler(getActiveGuiDocument(), new DrawSketchHandlerEllipse(0)); + ActivateHandler(getActiveGuiDocument(), new DrawSketchHandlerEllipse()); } bool CmdSketcherCreateEllipseByCenter::isActive() @@ -663,7 +663,9 @@ CONSTRUCTION_UPDATE_ACTION(CmdSketcherCreateEllipseBy3Points, "Sketcher_CreateEl void CmdSketcherCreateEllipseBy3Points::activated(int iMsg) { Q_UNUSED(iMsg); - ActivateHandler(getActiveGuiDocument(), new DrawSketchHandlerEllipse(1)); + ActivateHandler(getActiveGuiDocument(), + new DrawSketchHandlerEllipse( + ConstructionMethods::CircleEllipseConstructionMethod::ThreeRim)); } bool CmdSketcherCreateEllipseBy3Points::isActive() @@ -782,10 +784,12 @@ CmdSketcherCompCreateConic::CmdSketcherCompCreateConic() void CmdSketcherCompCreateConic::activated(int iMsg) { if (iMsg == 0) { - ActivateHandler(getActiveGuiDocument(), new DrawSketchHandlerEllipse(iMsg)); + ActivateHandler(getActiveGuiDocument(), new DrawSketchHandlerEllipse()); } else if (iMsg == 1) { - ActivateHandler(getActiveGuiDocument(), new DrawSketchHandlerEllipse(iMsg)); + ActivateHandler(getActiveGuiDocument(), + new DrawSketchHandlerEllipse( + ConstructionMethods::CircleEllipseConstructionMethod::ThreeRim)); } else if (iMsg == 2) { ActivateHandler(getActiveGuiDocument(), new DrawSketchHandlerArcOfEllipse()); diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerEllipse.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerEllipse.h index 21cc1cff8e..d68474f936 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerEllipse.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerEllipse.h @@ -20,24 +20,29 @@ * * ***************************************************************************/ + #ifndef SKETCHERGUI_DrawSketchHandlerEllipse_H #define SKETCHERGUI_DrawSketchHandlerEllipse_H -#include -#include #include + +#include + +#include #include #include #include #include -#include "DrawSketchHandler.h" +#include "DrawSketchDefaultWidgetController.h" +#include "DrawSketchControllableHandler.h" #include "GeometryCreationMode.h" #include "Utils.h" #include "ViewProviderSketch.h" +#include "CircleEllipseConstructionMethod.h" namespace SketcherGui { @@ -45,855 +50,925 @@ namespace SketcherGui extern GeometryCreationMode geometryCreationMode; // defined in CommandCreateGeo.cpp /* Ellipse ==============================================================================*/ -/** - * @brief This class handles user interaction to draw and save the ellipse - * - * Two construction methods are implemented: - * -Periapsis, apoapsis, and b; and - * -Center, periapsis, and b. - * - * The first method limits the ellipse to a circle, while the second method allows for - * swapping of the semi-major and semi-minor axes. - * - * We use three reference frames in this class. The first (and primary), is the cartesian - * frame of the sketcher; all our work begins and ends in this frame. The second is the - * perifocal frame of the ellipse using polar coordinates. We use this frame for naming - * conventions and working with the ellipse. The last is a rotated right-handed cartesian - * frame centered at the ellipse center with the +X direction towards periapsis, +Z out of - * screen. - * - * When working with an ellipse in the perifocal frame, the following equations are useful: - * - * \f{eqnarray*}{ - * r &\equiv& \textrm{ radial distance from the focus to a point on the ellipse}\\ - * r_a &\equiv& \textrm{ radial distance from the focus to apopasis}\\ - * r_p &\equiv& \textrm{ radial distance from the focus to periapsis}\\ - * a &\equiv& \textrm{ length of the semi-major axis, colloquially 'radius'}\\ - * b &\equiv& \textrm{ length of the semi-minor axis, colloquially 'radius'}\\ - * e &\equiv& \textrm{ eccentricity of the ellipse}\\ - * \theta_b &\equiv& \textrm{ angle to the intersection of the semi-minor axis and the - * ellipse, relative to the focus}\\ - * ae &\equiv& \textrm{ distance from the focus to the centroid}\\ - * r &=& \frac{a(1-e^2)}{1+e\cos(\theta)} = \frac{r_a(1-e)}{1+e\cos(\theta)} = - * \frac{r_p(1+e)}{1+e\cos(\theta)}\\ - * r_a &=& a(1-e)\\ - * r_p &=& a(1+e)\\ - * a &=& \frac{r_p+r_a}{2}\\ - * b &=& a\sqrt{1-e^2}\\ - * e &=& \frac{r_a-r_p}{r_a+r_p} = \sqrt{1-\frac{b^2}{a^2}}\\ - * \theta_b &=& \left[\pi - \arctan\left(\frac{b}{ae}\right)\right] \pm N\pi - * \f} - * - */ -class DrawSketchHandlerEllipse: public DrawSketchHandler +class DrawSketchHandlerEllipse; + +using DSHEllipseController = + DrawSketchDefaultWidgetController, + /*WidgetParametersT =*/WidgetParameters<0, 0>, + /*WidgetCheckboxesT =*/WidgetCheckboxes<1, 1>, + /*WidgetComboboxesT =*/WidgetComboboxes<1, 1>, + ConstructionMethods::CircleEllipseConstructionMethod, + /*bool PFirstComboboxIsConstructionMethod =*/true>; + +using DSHEllipseControllerBase = DSHEllipseController::ControllerBase; + +using DrawSketchHandlerEllipseBase = DrawSketchControllableHandler; + +class DrawSketchHandlerEllipse: public DrawSketchHandlerEllipseBase { + friend DSHEllipseController; + friend DSHEllipseControllerBase; + public: - explicit DrawSketchHandlerEllipse(int constructionMethod) - : mode(STATUS_Close) - , method(CENTER_PERIAPSIS_B) - , constrMethod(constructionMethod) - , a(0) - , b(0) - , e(0) - , ratio(0) - , ae(0) - , num(0) - , r(0) - , theta(0) - , phi(0) - , editCurve(33) - , fixedAxisLength(0) + explicit DrawSketchHandlerEllipse(ConstructionMethod constrMethod = ConstructionMethod::Center) + : DrawSketchHandlerEllipseBase(constrMethod) + , showInternal(true) + , firstRadius(0.0) + , secondRadius(0.0) + , majorRadius(0.0) + , minorRadius(0.0) {} - ~DrawSketchHandlerEllipse() override - {} - /// Mode table, describes what step of the process we are in - enum SelectMode - { - STATUS_SEEK_PERIAPSIS, /**< enum value, looking for click to set periapsis. */ - STATUS_SEEK_APOAPSIS, /**< enum value, looking for click to set apoapsis. */ - STATUS_SEEK_CENTROID, /**< enum value, looking for click to set centroid. */ - STATUS_SEEK_A, /**< enum value, looking for click to set a. */ - STATUS_SEEK_B, /**< enum value, looking for click to set b. */ - STATUS_Close /**< enum value, finalizing and saving ellipse. */ - }; - /// Construction methods, describes the method used to construct the ellipse - enum ConstructionMethod - { - CENTER_PERIAPSIS_B, /**< enum value, click on center, then periapsis, then b point. */ - PERIAPSIS_APOAPSIS_B /**< enum value, click on periapsis, then apoapsis, then b point. */ - }; - - /** - * @brief Updates the ellipse when the cursor moves - * @param onSketchPos the position of the cursor on the sketch - */ - void mouseMove(Base::Vector2d onSketchPos) override - { - if (method == PERIAPSIS_APOAPSIS_B) { - if (mode == STATUS_SEEK_PERIAPSIS) { - setPositionText(onSketchPos); - if (seekAutoConstraint(sugConstr1, - onSketchPos, - Base::Vector2d(0.f, 0.f), - AutoConstraint::CURVE)) { - renderSuggestConstraintsCursor(sugConstr1); - return; - } - } - else if (mode == STATUS_SEEK_APOAPSIS) { - solveEllipse(onSketchPos); - approximateEllipse(); - - // Display radius for user - float semiMajorRadius = a * 2; - if (showCursorCoords()) { - SbString text; - std::string majorString = lengthToDisplayFormat(semiMajorRadius, 1); - text.sprintf(" (R%s, R%s)", majorString.c_str(), majorString.c_str()); - setPositionText(onSketchPos, text); - } - - drawEdit(editCurve); - // Suggestions for ellipse and curves are disabled because many tangent constraints - // need an intermediate point or line. - if (seekAutoConstraint(sugConstr2, - onSketchPos, - Base::Vector2d(0.f, 0.f), - AutoConstraint::CURVE)) { - renderSuggestConstraintsCursor(sugConstr2); - return; - } - } - else if (mode == STATUS_SEEK_B) { - solveEllipse(onSketchPos); - approximateEllipse(); - - // Display radius for user - if (showCursorCoords()) { - SbString text; - std::string aString = lengthToDisplayFormat(a, 1); - std::string bString = lengthToDisplayFormat(b, 1); - text.sprintf(" (R%s, R%s)", aString.c_str(), bString.c_str()); - setPositionText(onSketchPos, text); - } - - drawEdit(editCurve); - if (seekAutoConstraint(sugConstr3, - onSketchPos, - Base::Vector2d(0.f, 0.f), - AutoConstraint::CURVE)) { - renderSuggestConstraintsCursor(sugConstr3); - return; - } - } - } - else { // method is CENTER_PERIAPSIS_B - if (mode == STATUS_SEEK_CENTROID) { - setPositionText(onSketchPos); - if (seekAutoConstraint(sugConstr1, - onSketchPos, - Base::Vector2d(0.f, 0.f))) { // TODO: ellipse prio 1 - renderSuggestConstraintsCursor(sugConstr1); - return; - } - } - else if (mode == STATUS_SEEK_PERIAPSIS) { - solveEllipse(onSketchPos); - approximateEllipse(); - - // Display radius for user - float semiMajorRadius = a * 2; - if (showCursorCoords()) { - SbString text; - std::string majorString = lengthToDisplayFormat(semiMajorRadius, 1); - text.sprintf(" (R%s, R%s)", majorString.c_str(), majorString.c_str()); - setPositionText(onSketchPos, text); - } - - drawEdit(editCurve); - if (seekAutoConstraint(sugConstr2, - onSketchPos, - onSketchPos - centroid, - AutoConstraint::CURVE)) { - renderSuggestConstraintsCursor(sugConstr2); - return; - } - } - else if ((mode == STATUS_SEEK_A) || (mode == STATUS_SEEK_B)) { - solveEllipse(onSketchPos); - approximateEllipse(); - - // Display radius for user - if (showCursorCoords()) { - SbString text; - std::string aString = lengthToDisplayFormat(a, 1); - std::string bString = lengthToDisplayFormat(b, 1); - text.sprintf(" (R%s, R%s)", aString.c_str(), bString.c_str()); - setPositionText(onSketchPos, text); - } - - drawEdit(editCurve); - if (seekAutoConstraint(sugConstr3, - onSketchPos, - onSketchPos - centroid, - AutoConstraint::CURVE)) { - renderSuggestConstraintsCursor(sugConstr3); - return; - } - } - } - applyCursor(); - } - - /** - * @brief Changes drawing mode on user-click - * @param onSketchPos the position of the cursor on the sketch - * @return - */ - bool pressButton(Base::Vector2d onSketchPos) override - { - if (method == PERIAPSIS_APOAPSIS_B) { - if (mode == STATUS_SEEK_PERIAPSIS) { - periapsis = onSketchPos; - setAngleSnapping(true, periapsis); - mode = STATUS_SEEK_APOAPSIS; - } - else if (mode == STATUS_SEEK_APOAPSIS) { - apoapsis = onSketchPos; - setAngleSnapping(false); - mode = STATUS_SEEK_B; - } - else { - mode = STATUS_Close; - } - } - else { // method is CENTER_PERIAPSIS_B - if (mode == STATUS_SEEK_CENTROID) { - centroid = onSketchPos; - setAngleSnapping(true, centroid); - mode = STATUS_SEEK_PERIAPSIS; - } - else if (mode == STATUS_SEEK_PERIAPSIS) { - periapsis = onSketchPos; - setAngleSnapping(false); - mode = STATUS_SEEK_B; - } - else { - mode = STATUS_Close; - } - } - return true; - } - - /** - * @brief Calls \c saveEllipse() after last user input - * @param onSketchPos the position of the cursor on the sketch - * @return - */ - bool releaseButton(Base::Vector2d onSketchPos) override - { - Q_UNUSED(onSketchPos); - if (mode == STATUS_Close) { - saveEllipse(); - ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath( - "User parameter:BaseApp/Preferences/Mod/Sketcher"); - bool continuousMode = hGrp->GetBool("ContinuousCreationMode", true); - - if (continuousMode) { - if (constrMethod == 0) { - method = CENTER_PERIAPSIS_B; - mode = STATUS_SEEK_CENTROID; - } - else { - method = PERIAPSIS_APOAPSIS_B; - mode = STATUS_SEEK_PERIAPSIS; - } - } - } - return true; - } + virtual ~DrawSketchHandlerEllipse() = default; private: - /** - * @brief Slot called when the create ellipse command is activated - * @param sketchgui A pointer to the active sketch - */ - void activated() override + void updateDataAndDrawToPosition(Base::Vector2d onSketchPos) override { - if (constrMethod == 0) { - method = CENTER_PERIAPSIS_B; - mode = STATUS_SEEK_CENTROID; - } - else { - method = PERIAPSIS_APOAPSIS_B; - mode = STATUS_SEEK_PERIAPSIS; + switch (state()) { + case SelectMode::SeekFirst: { + if (constructionMethod() == ConstructionMethod::Center) { + centerPoint = onSketchPos; + + if (seekAutoConstraint(sugConstraints[0], + onSketchPos, + Base::Vector2d(0.f, 0.f))) { + renderSuggestConstraintsCursor(sugConstraints[0]); + return; + } + } + else { + apoapsis = onSketchPos; + + if (seekAutoConstraint(sugConstraints[0], + onSketchPos, + Base::Vector2d(0.f, 0.f), + AutoConstraint::CURVE)) { + renderSuggestConstraintsCursor(sugConstraints[0]); + return; + } + } + } break; + case SelectMode::SeekSecond: { + periapsis = onSketchPos; + + calculateMajorAxisParameters(); + + CreateAndDrawShapeGeometry(); + + if (seekAutoConstraint(sugConstraints[1], + onSketchPos, + Base::Vector2d(0.f, 0.f), + AutoConstraint::CURVE)) { + renderSuggestConstraintsCursor(sugConstraints[1]); + return; + } + } break; + case SelectMode::SeekThird: { + calculateThroughPointMinorAxisParameters(onSketchPos); + + CreateAndDrawShapeGeometry(); + + if (seekAutoConstraint(sugConstraints[2], + onSketchPos, + Base::Vector2d(0.f, 0.f), + AutoConstraint::CURVE)) { + renderSuggestConstraintsCursor(sugConstraints[2]); + return; + } + } break; + default: + break; } } + + void executeCommands() override + { + try { + Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Add sketch ellipse")); + + ellipseGeoId = getHighestCurveIndex() + 1; + + createShape(false); + + commandAddShapeGeometryAndConstraints(); + + // in the exceptional event that this may lead to a circle, do not + // exposeInternalGeometry + if (!ShapeGeometry.empty() + && ShapeGeometry[0]->getTypeId() == Part::GeomEllipse::getClassTypeId() + && showInternal) { + Gui::cmdAppObjectArgs(sketchgui->getObject(), + "exposeInternalGeometry(%d)", + ellipseGeoId); + } + + Gui::Command::commitCommand(); + } + catch (const Base::Exception& e) { + Gui::NotifyError(sketchgui, + QT_TRANSLATE_NOOP("Notifications", "Error"), + QT_TRANSLATE_NOOP("Notifications", "Failed to add ellipse")); + + Gui::Command::abortCommand(); + THROWM(Base::RuntimeError, + QT_TRANSLATE_NOOP( + "Notifications", + "Tool execution aborted") "\n") // This prevents constraints from being + // applied on non existing geometry + } + } + + void generateAutoConstraints() override + { + + if (constructionMethod() == ConstructionMethod::Center) { + + auto& ac1 = sugConstraints[0]; + auto& ac2 = sugConstraints[1]; + auto& ac3 = sugConstraints[2]; + + generateAutoConstraintsOnElement( + ac1, + ellipseGeoId, + Sketcher::PointPos::mid); // add auto constraints for the center point + generateAutoConstraintsOnElement( + ac2, + ellipseGeoId, + Sketcher::PointPos::none); // add auto constraints for the edge + generateAutoConstraintsOnElement( + ac3, + ellipseGeoId, + Sketcher::PointPos::none); // add auto constraints for the edge + } + else { + + auto& ac1 = sugConstraints[0]; + auto& ac2 = sugConstraints[1]; + auto& ac3 = sugConstraints[2]; + + generateAutoConstraintsOnElement( + ac1, + ellipseGeoId, + Sketcher::PointPos::none); // add auto constraints for the first point + generateAutoConstraintsOnElement( + ac2, + ellipseGeoId, + Sketcher::PointPos::none); // add auto constraints for the second point + generateAutoConstraintsOnElement( + ac3, + ellipseGeoId, + Sketcher::PointPos::none); // add auto constraints for the edge + } + // Ensure temporary autoconstraints do not generate a redundancy and that the geometry + // parameters are accurate This is particularly important for adding widget mandated + // constraints. + removeRedundantAutoConstraints(); + } + + void createAutoConstraints() override + { + // execute python command to create autoconstraints + createGeneratedAutoConstraints(true); + + sugConstraints[0].clear(); + sugConstraints[1].clear(); + sugConstraints[2].clear(); + } + + std::string getToolName() const override + { + return "DSH_Ellipse"; + } + QString getCrosshairCursorSVGName() const override { return QString::fromLatin1("Sketcher_Pointer_Create_Ellipse"); } -protected: - std::vector sugConstr1, sugConstr2, sugConstr3; - -private: - SelectMode mode; - /// the method of constructing the ellipse - ConstructionMethod method; - int constrMethod; - /// periapsis position vector, in standard position in sketch coordinate system - Base::Vector2d periapsis; - /// apoapsis position vector, in standard position in sketch coordinate system - Base::Vector2d apoapsis; - /// centroid position vector, in standard position in sketch coordinate system - Base::Vector2d centroid; - /** - * @brief position vector of positive b point, in standard position in sketch coordinate system - * I.E. in polar perifocal system, the first intersection of the semiminor axis with the ellipse - * as theta increases from 0. This always happens when: - * \f{eqnarray*}{ - * \theta_b &=& \left[\pi - \arctan\left(\frac{b}{ae}\right)\right] \pm N 2\pi - * \f} - * - * In a rotated R^3 cartesian system, centered at the centroid, +X towards periapsis, and - * +Z coming out of the sketch, this b position is in the +Y direction from the centroid. - */ - Base::Vector2d positiveB; - /// the other b position - Base::Vector2d negativeB; - /// cart. position vector for primary focus - Base::Vector2d f; - /// cart. position vector for other focus - Base::Vector2d fPrime; - /// Unit vector for apse line - Base::Vector2d apseHat; - /// length of semimajor axis, i.e. 'radius' colloquially - double a; - /// length of semiminor axis, i.e. 'radius' colloquially - double b; - /// eccentricity [unitless] - double e; - /// optimization, holds a term that helps calculate b in terms of a and e - double ratio; - /// holds product of a * e - double ae; - /// holds numerator of orbit equation of form a(1-e^2) - double num; - /// holds a radial distance from f to the ellipse for a given theta - double r; - /// angle of a point in a perifocal frame centered at f - double theta; - /// angle of apse line relative to sketch coordinate system - double phi; - /// holds a position vector for a point on the ellipse from f - Base::Vector2d pos; - /// holds a position vector for a point on the ellipse from fPrime - Base::Vector2d posPrime; - /// holds position vectors for a points on the ellipse - std::vector editCurve; - /// local i_hat vector for ellipse, from centroid to periapsis - Base::Vector3d iPrime; - /// local j_hat vector for ellipse, from centroid to b point - Base::Vector3d jPrime; - /// length (radius) of the fixed axis - double fixedAxisLength; - /// position vector of fixed axis point in sketch coordinates - Base::Vector2d fixedAxis; - - /** - * @brief Computes a vector of 2D points representing an ellipse - * @param onSketchPos Current position of the cursor on the sketch - */ - void solveEllipse(Base::Vector2d onSketchPos) + std::unique_ptr createWidget() const override { - const double GOLDEN_RATIO = 1.6180339887; - Base::Vector3d k(0, 0, 1); + return std::make_unique(); + } - if (method == PERIAPSIS_APOAPSIS_B) { - if (mode == STATUS_SEEK_APOAPSIS) { - apoapsis = onSketchPos; - } - a = (apoapsis - periapsis).Length() / 2; - apseHat = (periapsis - apoapsis); - apseHat.Normalize(); - centroid = apseHat; - centroid.Scale(-1 * a); - centroid = periapsis + centroid; - if (mode == STATUS_SEEK_APOAPSIS) { - // for first step, we draw an ellipse inscribed in a golden rectangle - ratio = 1 / GOLDEN_RATIO; // ~= 0.6180339887 - e = sqrt(ratio); // ~= 0.7861513777 - b = a * ratio; - } - else if (mode == STATUS_SEEK_B) { - // Get the closest distance from onSketchPos to apse line, as a 'requested' value - // for b - Base::Vector2d cursor = - Base::Vector2d(onSketchPos - f); // vector from f to cursor pos - // decompose cursor with a projection, then length of w_2 will give us b - Base::Vector2d w_1 = cursor; - w_1.ProjectToLine( - cursor, - (periapsis - apoapsis)); // projection of cursor line onto apse line - Base::Vector2d w_2 = (cursor - w_1); - b = w_2.Length(); + bool isWidgetVisible() const override + { + return true; + }; - // limit us to ellipse or circles - if (b > a) { - b = a; - } + QPixmap getToolIcon() const override + { + return Gui::BitmapFactory().pixmap("Sketcher_CreateEllipseByCenter"); + } - e = sqrt(1 - ((b * b) / (a * a))); - ratio = sqrt(1 - (e * e)); - } - ae = a * e; - f = apseHat; - f.Scale(ae); - f = centroid + f; - fPrime = apseHat; - fPrime.Scale(-1 * ae); - fPrime = centroid + fPrime; - phi = atan2(apseHat.y, apseHat.x); - num = a * (1 - (e * e)); - // The ellipse is now solved + QString getToolWidgetText() const override + { + return QString(QObject::tr("Ellipse parameters")); + } + + bool canGoToNextMode() override + { + if (state() == SelectMode::SeekSecond && firstRadius < Precision::Confusion()) { + // Prevent validation of null ellipse. + return false; } - else { // method == CENTER_PERIAPSIS_B - if (mode == STATUS_SEEK_PERIAPSIS) { - // solve the ellipse inscribed in a golden rectangle - periapsis = onSketchPos; - a = (centroid - periapsis).Length(); - iPrime.x = periapsis.x - centroid.x; - iPrime.y = periapsis.y - centroid.y; - iPrime.z = 0; - jPrime = k % iPrime; // j = k cross i - // these are constant for any ellipse inscribed in a golden rectangle - ratio = 1 / GOLDEN_RATIO; // ~= 0.6180339887 - e = sqrt(ratio); // ~= 0.7861513777 + if (state() == SelectMode::SeekThird + && (firstRadius < Precision::Confusion() || secondRadius < Precision::Confusion())) { + // Prevent validation of null ellipse. + return false; + } - b = a * ratio; - ae = a * e; - apseHat = (periapsis - centroid); - apseHat.Normalize(); - f = apseHat; - f.Scale(ae); - f = centroid + f; - fPrime = apseHat; - fPrime.Scale(-1 * ae); - fPrime = centroid + fPrime; - apoapsis = apseHat; - apoapsis.Scale(-1 * a); - apoapsis = centroid + apoapsis; - phi = atan2(apseHat.y, apseHat.x); - num = a * (1 - (e * e)); - fixedAxisLength = a; - fixedAxis = periapsis; - } - else if ((mode == STATUS_SEEK_B) || (mode == STATUS_SEEK_A)) { - // while looking for the last click, we may switch back and forth - // between looking for a b point and looking for periapsis, so ensure - // we are in the right mode - Base::Vector2d cursor = - Base::Vector2d(onSketchPos - centroid); // vector from centroid to cursor pos - // decompose cursor with a projection, then length of w_2 will give us b - Base::Vector2d w_1 = cursor; - w_1.ProjectToLine( - cursor, - (fixedAxis - centroid)); // projection of cursor line onto fixed axis line - Base::Vector2d w_2 = (cursor - w_1); - if (w_2.Length() > fixedAxisLength) { - // b is fixed, we are seeking a - mode = STATUS_SEEK_A; - jPrime.x = (fixedAxis - centroid).x; - jPrime.y = (fixedAxis - centroid).y; - jPrime.Normalize(); - iPrime = jPrime % k; // cross - b = fixedAxisLength; - a = w_2.Length(); - } - else { - // a is fixed, we are seeking b - mode = STATUS_SEEK_B; - iPrime.x = (fixedAxis - centroid).x; - iPrime.y = (fixedAxis - centroid).y; - iPrime.Normalize(); - jPrime = k % iPrime; // cross - a = fixedAxisLength; - b = w_2.Length(); - } - // now finish solving the ellipse - periapsis.x = centroid.x + (iPrime * a).x; - periapsis.y = centroid.y + (iPrime * a).y; - e = sqrt(1 - ((b * b) / (a * a))); - ratio = sqrt(1 - (e * e)); - ae = a * e; - apseHat = (periapsis - centroid); - apseHat.Normalize(); - f = apseHat; - f.Scale(ae); - f = centroid + f; - fPrime = apseHat; - fPrime.Scale(-1 * ae); - fPrime = centroid + fPrime; - apoapsis = apseHat; - apoapsis.Scale(-1 * a); - apoapsis = centroid + apoapsis; - phi = atan2(apseHat.y, apseHat.x); - num = a * (1 - (e * e)); - } + return true; + } + + void angleSnappingControl() override + { + if (state() == SelectMode::SeekSecond || state() == SelectMode::SeekThird) { + setAngleSnapping(true, centerPoint); + } + else { + setAngleSnapping(false); } } - - /** - * @brief Computes a sequence of 2D vectors to approximate the ellipse - */ - void approximateEllipse() + void calculateMajorAxisParameters() { - // We will approximate the ellipse as a sequence of connected chords - // Number of points per quadrant of the ellipse - int n = static_cast((editCurve.size() - 1) / 4); + if (constructionMethod() == ConstructionMethod::ThreeRim) { + centerPoint = (apoapsis - periapsis) / 2 + periapsis; + } - // We choose points in the perifocal frame then translate them to sketch cartesian. - // This gives us a better approximation of an ellipse, i.e. more points where the - // curvature is higher. If the eccentricity is high, we shift the points a bit towards - // the semi-minor axis. - double partitionAngle = (M_PI - atan2(b, ae)) / n; - double radianShift = 0; - if (e > 0.8) { - radianShift = (partitionAngle / 5) * 4; - } - for (int i = 0; i < n; i++) { - theta = i * partitionAngle; - if (i > 0) { - theta = theta + radianShift; - } - r = num / (1 + (e * cos(theta))); - // r(pi/2) is semi-latus rectum, if we need it - pos.x = r * cos(theta + phi); // phi rotates, sin/cos translate - pos.y = r * sin(theta + phi); - pos = pos + f; - posPrime.x = r * cos(theta + phi + M_PI); - posPrime.y = r * sin(theta + phi + M_PI); - posPrime = posPrime + fPrime; - // over the loop, loads Quadrant I points, by using f as origin - editCurve[i] = pos; - // over the loop, loads Quadrant III points, by using fPrime as origin - editCurve[(2 * n) + i] = posPrime; - // load points with negative theta angles (i.e. cw) - if (i > 0) { - pos.x = r * cos(-1 * theta + phi); - pos.y = r * sin(-1 * theta + phi); - pos = pos + f; - // loads Quadrant IV points - editCurve[(4 * n) - i] = pos; - posPrime.x = r * cos(-1 * theta + phi + M_PI); - posPrime.y = r * sin(-1 * theta + phi + M_PI); - posPrime = posPrime + fPrime; - // loads Quadrant II points - editCurve[(2 * n) - i] = posPrime; - } - } - // load pos & neg b points - theta = M_PI - atan2(b, ae); // the angle from f to the positive b point - r = num / (1 + (e * cos(theta))); - pos.x = r * cos(theta + phi); - pos.y = r * sin(theta + phi); - pos = pos + f; - editCurve[n] = pos; // positive - pos.x = r * cos(-1 * theta + phi); - pos.y = r * sin(-1 * theta + phi); - pos = pos + f; - editCurve[(3 * n)] = pos; // negative - // force the curve to be a closed shape - editCurve[(4 * n)] = editCurve[0]; + firstAxis = periapsis - centerPoint; + firstRadius = firstAxis.Length(); } - /** - * @brief Prints the ellipse data to STDOUT as an GNU Octave script - * @param onSketchPos position of the cursor on the sketch - */ - void ellipseToOctave(Base::Vector2d /*onSketchPos*/) + void calculateThroughPointMinorAxisParameters(const Base::Vector2d& onSketchPos) { - int n = static_cast((editCurve.size() - 1) / 4); + // we calculate the ellipse that will pass via the cursor as per de la Hire - // send a GNU Octave script to stdout to plot points for debugging - std::ostringstream octave; - octave << std::fixed << std::setprecision(12); - octave << "\nclear all;\nclose all;\nclc;\n\n"; - octave << "periapsis = [" << periapsis.x << ", " << periapsis.y << "];\n"; - octave << "apoapsis = [" << apoapsis.x << ", " << apoapsis.y << "];\n"; - octave << "positiveB = [" << editCurve[n].x << ", " << editCurve[n].y << "];\n"; - octave << "apseHat = [" << apseHat.x << ", " << apseHat.y << "];\n"; - octave << "a = " << a << ";\n"; - octave << "b = " << b << ";\n"; - octave << "eccentricity = " << e << ";\n"; - octave << "centroid = [" << centroid.x << ", " << centroid.y << "];\n"; - octave << "f = [" << f.x << ", " << f.y << "];\n"; - octave << "fPrime = [" << fPrime.x << ", " << fPrime.y << "];\n"; - octave << "phi = " << phi << ";\n\n"; - octave << "x = ["; - for (int i = 0; i < 4 * n + 1; i++) { - octave << editCurve[i].x; - if (i < 4 * n) { - octave << ", "; - } - } - octave << "];\n"; - octave << "y = ["; - for (int i = 0; i < 4 * n + 1; i++) { - octave << editCurve[i].y; - if (i < 4 * n) { - octave << ", "; - } - } - octave << "];\n\n"; - octave << "% Draw ellipse points in red;\n"; - octave << "plot (x, y, \"r.\", \"markersize\", 5);\n"; - octave << "axis ([-300, 300, -300, 300], \"square\");grid on;\n"; - octave << "hold on;\n\n"; - octave << "% Draw centroid in blue, f in cyan, and fPrime in magenta;\n"; - octave << "plot(centroid(1), centroid(2), \"b.\", \"markersize\", 5);\n"; - octave << "plot(f(1), f(2), \"c.\", \"markersize\", 5);\n"; - octave << "plot(fPrime(1), fPrime(2), \"m.\", \"markersize\", 5);\n"; - octave << "n = [periapsis(1) - f(1), periapsis(2) - f(2)];\n"; - octave << "h = quiver(f(1),f(2),n(1),n(2), 0);\n"; - octave << "set (h, \"maxheadsize\", 0.1);\n\n"; - octave << "% Draw the three position vectors used for Gui::Command::doCommand(...)\n"; - octave << "periapsisVec = quiver(0,0,periapsis(1),periapsis(2), 0);\n"; - octave << "set (periapsisVec, \"maxheadsize\", 0.01, \"color\", \"black\");\n"; - octave << "centroidVec = quiver(0,0,centroid(1),centroid(2), 0);\n"; - octave << "set (centroidVec, \"maxheadsize\", 0.01, \"color\", \"black\");\n"; - octave << "bVec = quiver(0,0,positiveB(1),positiveB(2), 0);\n"; - octave << "set (bVec, \"maxheadsize\", 0.01, \"color\", \"black\");\n\n"; - octave << "% Draw the local x & y basis vectors, scaled to a and b, in red and blue, " - "respectively\n"; - octave << "xLocalVec = " - "quiver(centroid(1),centroid(2),periapsis(1)-centroid(1),periapsis(2)-centroid(2)" - ", 0);\n"; - octave << "set (xLocalVec, \"maxheadsize\", 0.01, \"color\", \"red\");\n"; - octave << "yLocalVec = quiver(centroid(1),centroid(2), positiveB(1)-centroid(1), " - "positiveB(2)-centroid(2), 0);\n"; - octave << "set (yLocalVec, \"maxheadsize\", 0.01, \"color\", \"blue\");\nhold off;\n"; - qDebug() << QString::fromStdString(octave.str()); + Base::Vector2d projx; + projx.ProjectToLine(onSketchPos - centerPoint, + firstAxis); // projection onto the major axis + + auto projy = onSketchPos - centerPoint - projx; + + auto lprojx = projx.Length(); // Px = a cos t + auto lprojy = projy.Length(); // Py = b sin t + + double t = std::acos(lprojx / firstRadius); + + secondRadius = lprojy / std::sin(t); // b = Py / sin t + + secondAxis = projy.Normalize() * secondRadius; } - /** - * @brief Finalizes and saves the drawn ellipse - * @return nothing - */ - void saveEllipse() + void calculateMinorAxis(double minorradius) { - unsetCursor(); - resetPositionText(); + // Find bPoint For that first we need the distance of onSketchPos to major axis. + secondAxis = firstAxis.Perpendicular(false).Normalize() * minorRadius; + secondRadius = minorradius; + } - /* There are a couple of issues with Gui::Command::doCommand(...) and - * GC_MakeEllipse(...) that cause bugs if not handled properly, even - * when we give them a mathematically-correct ellipse. - * - * GC_MakeEllipse may fail with a gce_InvertAxis error for a small - * circular ellipse when floating point roundoff or representation - * errors make the b axis slightly larger than the a axis. - * - * A similar, larger, issue arises in Gui::Command::doCommand(...) because - * we cast our double vector components into strings with a fixed - * precision of six, and then create new doubles from the strings - * in EllipsePy::PyInit(...). Thus, by the time we call GC_MakeEllipse(...) - * in EllipsePy::PyInit(...), our ellipse may not be valid anymore - * because b is now greater than a. - * - * To handle these issues, we simulate the effects Gui::Command::doCommand(...) - * has on our ellipse, and we adjust our ellipse parameters until - * GC_MakeEllipse successfully creates an ellipse with our mangled - * parameters. - * - * In almost all cases, we only have to make our test ellipse one time; - * it is only in the rare edge cases that require repeated test ellipses - * until we get a valid one, or fail due to excessive attempts. With a - * limit of 25 attempts, I have been unable to make it fail. - */ + virtual void createShape(bool onlyeditoutline) override + { + Q_UNUSED(onlyeditoutline); - // simulate loss of precision in centroid, periapsis, and apoapsis - char cx[64]; - char cy[64]; - char px[64]; - char py[64]; - char ax[64]; - char ay[64]; - sprintf(cx, "%.6lf\n", centroid.x); - sprintf(cy, "%.6lf\n", centroid.y); - sprintf(px, "%.6lf\n", periapsis.x); - sprintf(py, "%.6lf\n", periapsis.y); - sprintf(ax, "%.6lf\n", apoapsis.x); - sprintf(ay, "%.6lf\n", apoapsis.y); - centroid.x = atof(cx); - centroid.y = atof(cy); - periapsis.x = atof(px); - periapsis.y = atof(py); - apoapsis.x = atof(ax); - apoapsis.y = atof(ay); - double majorLength = (periapsis - apoapsis).Length(); - double minorLength = 0; + ShapeGeometry.clear(); - /* GC_MakeEllipse requires a right-handed coordinate system, with +X - * from centroid to periapsis, +Z out of the page. - */ - Base::Vector3d k(0, 0, 1); - Base::Vector3d i(periapsis.x - centroid.x, periapsis.y - centroid.y, 0); - Base::Vector3d j = k % i; // j = k cross i - double beta = 1e-7; - int count = 0; - int limit = 25; // no infinite loops! - bool success = false; - double tempB; - - // adjust b until our mangled vectors produce a good ellipse in GC_MakeEllipse - // and the mangled major and minor lines in LinePy::PyInit(...) are such that - // major is at least slightly larger than minor - do { - tempB = b - double(count * beta); - j = j.Normalize() * tempB; - positiveB.x = centroid.x + j.x; - positiveB.y = centroid.y + j.y; - negativeB.x = centroid.x + (j.x * -1); - negativeB.y = centroid.y + (j.y * -1); - char bpx[64]; - char bpy[64]; - char bnx[64]; - char bny[64]; - sprintf(bpx, "%.6lf\n", positiveB.x); - sprintf(bpy, "%.6lf\n", positiveB.y); - sprintf(bnx, "%.6lf\n", negativeB.x); - sprintf(bny, "%.6lf\n", negativeB.y); - positiveB.x = atof(bpx); - positiveB.y = atof(bpy); - negativeB.x = atof(bnx); - negativeB.y = atof(bny); - GC_MakeEllipse me(gp_Pnt(periapsis.x, periapsis.y, 0), - gp_Pnt(positiveB.x, positiveB.y, 0), - gp_Pnt(centroid.x, centroid.y, 0)); - minorLength = (negativeB - positiveB).Length(); - count++; - success = me.IsDone() && (minorLength + beta < majorLength); - } while (!success && (count <= limit)); - if (!success) { - qDebug() << "Failed to create a valid mangled ellipse after" << count << "attempts"; + Base::Vector2d majorAxis = firstAxis; + majorRadius = firstRadius; + if (state() == SelectMode::SeekSecond) { + minorRadius = majorRadius * 0.5; } + else { // SelectMode::SeekThird or SelectMode::End + minorRadius = secondRadius; - // save any changes to b, then recalculate ellipse as required due to change in b - b = tempB; - e = sqrt(1 - ((b * b) / (a * a))); - ae = a * e; - f = apseHat; - f.Scale(ae); - f = centroid + f; - fPrime = apseHat; - fPrime.Scale(-1 * ae); - fPrime = centroid + fPrime; - - int currentgeoid = getHighestCurveIndex(); // index of the ellipse we just created - - try { - Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Add sketch ellipse")); - Gui::cmdAppObjectArgs( - sketchgui->getObject(), - "addGeometry(Part.Ellipse" - "(App.Vector(%f,%f,0),App.Vector(%f,%f,0),App.Vector(%f,%f,0)),%s)", - periapsis.x, - periapsis.y, - positiveB.x, - positiveB.y, - centroid.x, - centroid.y, - constructionModeAsBooleanText()); - - currentgeoid++; - - Gui::cmdAppObjectArgs(sketchgui->getObject(), - "exposeInternalGeometry(%d)", - currentgeoid); + if (secondRadius > firstRadius) { + majorAxis = secondAxis; + majorRadius = secondRadius; + minorRadius = firstRadius; + } } - catch (const Base::Exception&) { - Gui::NotifyError(sketchgui, - QT_TRANSLATE_NOOP("Notifications", "Error"), - QT_TRANSLATE_NOOP("Notifications", "Failed to add an ellipse")); - Gui::Command::abortCommand(); - - tryAutoRecomputeIfNotSolve( - static_cast(sketchgui->getObject())); - + if (majorRadius < Precision::Confusion() || minorRadius < Precision::Confusion()) { return; } - Gui::Command::commitCommand(); - - if (method == CENTER_PERIAPSIS_B) { - // add auto constraints for the center point - if (!sugConstr1.empty()) { - createAutoConstraints(sugConstr1, currentgeoid, Sketcher::PointPos::mid); - sugConstr1.clear(); - } - if (!sugConstr2.empty()) { - createAutoConstraints(sugConstr2, currentgeoid, Sketcher::PointPos::none); - sugConstr2.clear(); - } - if (!sugConstr3.empty()) { - createAutoConstraints(sugConstr3, currentgeoid, Sketcher::PointPos::none); - sugConstr3.clear(); - } - } - - if (method == PERIAPSIS_APOAPSIS_B) { - if (!sugConstr1.empty()) { - createAutoConstraints(sugConstr1, currentgeoid, Sketcher::PointPos::none); - sugConstr1.clear(); - } - if (!sugConstr2.empty()) { - createAutoConstraints(sugConstr2, currentgeoid, Sketcher::PointPos::none); - sugConstr2.clear(); - } - if (!sugConstr3.empty()) { - createAutoConstraints(sugConstr3, currentgeoid, Sketcher::PointPos::none); - sugConstr3.clear(); - } - } - - tryAutoRecomputeIfNotSolve(static_cast(sketchgui->getObject())); - - // This code enables the continuous creation mode. - if (constrMethod == 0) { - method = CENTER_PERIAPSIS_B; - mode = STATUS_SEEK_CENTROID; + if (fabs(firstRadius - secondRadius) < Precision::Confusion()) { + addCircleToShapeGeometry(toVector3d(centerPoint), firstRadius, isConstructionMode()); } else { - method = PERIAPSIS_APOAPSIS_B; - mode = STATUS_SEEK_PERIAPSIS; - } - editCurve.clear(); - drawEdit(editCurve); - - ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath( - "User parameter:BaseApp/Preferences/Mod/Sketcher"); - bool continuousMode = hGrp->GetBool("ContinuousCreationMode", true); - - - if (continuousMode) { - // This code enables the continuous creation mode. - editCurve.resize(33); - applyCursor(); - /* It is ok not to call to purgeHandler - * in continuous creation mode because the - * handler is destroyed by the quit() method on pressing the - * right button of the mouse */ - } - else { - sketchgui - ->purgeHandler(); // no code after this line, Handler get deleted in ViewProvider + addEllipseToShapeGeometry(toVector3d(centerPoint), + toVector3d(majorAxis), + majorRadius, + minorRadius, + isConstructionMode()); } } + +private: + Base::Vector2d centerPoint, + periapsis; // Center Mode SeekFirst and SeekSecond, 3PointMode SeekFirst + Base::Vector2d apoapsis; // 3Point SeekSecond + Base::Vector2d firstAxis, secondAxis; + bool showInternal; + double firstRadius, secondRadius, majorRadius, minorRadius; + int ellipseGeoId; }; +template<> +auto DSHEllipseControllerBase::getState(int parameterindex) const +{ + + switch (parameterindex) { + case OnViewParameter::First: + case OnViewParameter::Second: + return SelectMode::SeekFirst; + break; + case OnViewParameter::Third: + case OnViewParameter::Fourth: + return SelectMode::SeekSecond; + break; + case OnViewParameter::Fifth: + return SelectMode::SeekThird; + break; + case OnViewParameter::Sixth: + if (handler->constructionMethod() == ConstructionMethod::ThreeRim) { + return SelectMode::SeekThird; + } + THROWM(Base::ValueError, "OnViewParameter index without an associated machine state") + break; + default: + THROWM(Base::ValueError, "OnViewParameter index without an associated machine state") + } +} + +template<> +void DSHEllipseController::configureToolWidget() +{ + + if (!init) { // Code to be executed only upon initialisation + QStringList names = {QStringLiteral("Center"), QStringLiteral("Axis endpoints and radius")}; + toolWidget->setComboboxElements(WCombobox::FirstCombo, names); + + if (isConstructionMode()) { + toolWidget->setComboboxItemIcon( + WCombobox::FirstCombo, + 0, + Gui::BitmapFactory().iconFromTheme("Sketcher_CreateEllipseByCenter_Constr")); + toolWidget->setComboboxItemIcon( + WCombobox::FirstCombo, + 1, + Gui::BitmapFactory().iconFromTheme("Sketcher_CreateEllipse_3points_Constr")); + } + else { + toolWidget->setComboboxItemIcon( + WCombobox::FirstCombo, + 0, + Gui::BitmapFactory().iconFromTheme("Sketcher_CreateEllipseByCenter")); + toolWidget->setComboboxItemIcon( + WCombobox::FirstCombo, + 1, + Gui::BitmapFactory().iconFromTheme("Sketcher_CreateEllipse_3points")); + } + + toolWidget->setCheckboxLabel( + WCheckbox::FirstBox, + QApplication::translate("TaskSketcherTool_c1_ellipse", "Show Internal geometries (U)")); + toolWidget->setCheckboxToolTip( + WCheckbox::FirstBox, + QApplication::translate("TaskSketcherTool_c1_ellipse", + "Show internal geometries (press U to toggle).")); + syncCheckboxToHandler(WCheckbox::FirstBox, handler->showInternal); + + toolWidget->setCheckboxIcon( + WCheckbox::FirstBox, + Gui::BitmapFactory().iconFromTheme("Sketcher_Element_Ellipse_All")); + } + + onViewParameters[OnViewParameter::First]->setLabelType(Gui::SoDatumLabel::DISTANCEX); + onViewParameters[OnViewParameter::Second]->setLabelType(Gui::SoDatumLabel::DISTANCEY); + + if (handler->constructionMethod() == DrawSketchHandlerEllipse::ConstructionMethod::ThreeRim) { + onViewParameters[OnViewParameter::Third]->setLabelType(Gui::SoDatumLabel::DISTANCEX); + onViewParameters[OnViewParameter::Fourth]->setLabelType(Gui::SoDatumLabel::DISTANCEY); + + onViewParameters[OnViewParameter::Fifth]->setLabelType(Gui::SoDatumLabel::DISTANCEX); + onViewParameters[OnViewParameter::Sixth]->setLabelType(Gui::SoDatumLabel::DISTANCEY); + } + else { + onViewParameters[OnViewParameter::Third]->setLabelType( + Gui::SoDatumLabel::RADIUS, + Gui::EditableDatumLabel::Function::Dimensioning); + onViewParameters[OnViewParameter::Fourth]->setLabelType( + Gui::SoDatumLabel::ANGLE, + Gui::EditableDatumLabel::Function::Dimensioning); + onViewParameters[OnViewParameter::Fifth]->setLabelType( + Gui::SoDatumLabel::RADIUS, + Gui::EditableDatumLabel::Function::Dimensioning); + } +} + +template<> +void DSHEllipseController::adaptDrawingToCheckboxChange(int checkboxindex, bool value) +{ + Q_UNUSED(checkboxindex); + + switch (checkboxindex) { + case WCheckbox::FirstBox: + handler->showInternal = value; + break; + } +} + +template<> +void DSHEllipseControllerBase::doEnforceControlParameters(Base::Vector2d& onSketchPos) +{ + + switch (handler->state()) { + case SelectMode::SeekFirst: { + if (onViewParameters[OnViewParameter::First]->isSet) { + onSketchPos.x = onViewParameters[OnViewParameter::First]->getValue(); + } + + if (onViewParameters[OnViewParameter::Second]->isSet) { + onSketchPos.y = onViewParameters[OnViewParameter::Second]->getValue(); + } + } break; + case SelectMode::SeekSecond: { + if (handler->constructionMethod() + == DrawSketchHandlerEllipse::ConstructionMethod::Center) { + Base::Vector2d dir = onSketchPos - handler->centerPoint; + if (dir.Length() < Precision::Confusion()) { + dir.x = 1.0; // if direction null, default to (1,0) + } + double length = dir.Length(); + + if (onViewParameters[OnViewParameter::Third]->isSet) { + length = onViewParameters[OnViewParameter::Third]->getValue(); + if (length < Precision::Confusion()) { + unsetOnViewParameter(onViewParameters[OnViewParameter::Third].get()); + return; + } + + onSketchPos = handler->centerPoint + length * dir.Normalize(); + } + + if (onViewParameters[OnViewParameter::Fourth]->isSet) { + double angle = + onViewParameters[OnViewParameter::Fourth]->getValue() * M_PI / 180; + onSketchPos.x = handler->centerPoint.x + cos(angle) * length; + onSketchPos.y = handler->centerPoint.y + sin(angle) * length; + } + } + else { + if (onViewParameters[OnViewParameter::Third]->isSet) { + onSketchPos.x = onViewParameters[OnViewParameter::Third]->getValue(); + } + + if (onViewParameters[OnViewParameter::Fourth]->isSet) { + onSketchPos.y = onViewParameters[OnViewParameter::Fourth]->getValue(); + } + + if (onViewParameters[OnViewParameter::Third]->isSet + && onViewParameters[OnViewParameter::Fourth]->isSet + && (onSketchPos - handler->apoapsis).Length() < Precision::Confusion()) { + unsetOnViewParameter(onViewParameters[OnViewParameter::Third].get()); + unsetOnViewParameter(onViewParameters[OnViewParameter::Fourth].get()); + } + } + } break; + case SelectMode::SeekThird: { + if (handler->constructionMethod() + == DrawSketchHandlerEllipse::ConstructionMethod::Center) { + if (onViewParameters[OnViewParameter::Fifth]->isSet) { + auto minorradius = onViewParameters[OnViewParameter::Fifth]->getValue(); + onSketchPos = handler->centerPoint + + (handler->periapsis - handler->centerPoint) + .Perpendicular(true) + .Normalize() + * minorradius; + } + } + else { + if (onViewParameters[OnViewParameter::Fifth]->isSet) { + onSketchPos.x = onViewParameters[OnViewParameter::Fifth]->getValue(); + } + + if (onViewParameters[OnViewParameter::Sixth]->isSet) { + onSketchPos.y = onViewParameters[OnViewParameter::Sixth]->getValue(); + } + + if (onViewParameters[OnViewParameter::Fifth]->isSet + && onViewParameters[OnViewParameter::Sixth]->isSet + && areColinear(handler->apoapsis, handler->periapsis, onSketchPos)) { + unsetOnViewParameter(onViewParameters[OnViewParameter::Fifth].get()); + unsetOnViewParameter(onViewParameters[OnViewParameter::Sixth].get()); + } + } + } break; + default: + break; + } +} + +template<> +void DSHEllipseController::adaptParameters(Base::Vector2d onSketchPos) +{ + + // If checkboxes need synchronisation (they were changed by the DSH, e.g. by using 'M' to switch + // construction method), synchronise them and return. + if (syncCheckboxToHandler(WCheckbox::FirstBox, handler->showInternal)) { + return; + } + + switch (handler->state()) { + case SelectMode::SeekFirst: { + if (!onViewParameters[OnViewParameter::First]->isSet) { + setOnViewParameterValue(OnViewParameter::First, onSketchPos.x); + } + + if (!onViewParameters[OnViewParameter::Second]->isSet) { + setOnViewParameterValue(OnViewParameter::Second, onSketchPos.y); + } + + bool sameSign = onSketchPos.x * onSketchPos.y > 0.; + onViewParameters[OnViewParameter::First]->setLabelAutoDistanceReverse(!sameSign); + onViewParameters[OnViewParameter::Second]->setLabelAutoDistanceReverse(sameSign); + onViewParameters[OnViewParameter::First]->setPoints(Base::Vector3d(), + toVector3d(onSketchPos)); + onViewParameters[OnViewParameter::Second]->setPoints(Base::Vector3d(), + toVector3d(onSketchPos)); + } break; + case SelectMode::SeekSecond: { + if (handler->constructionMethod() + == DrawSketchHandlerEllipse::ConstructionMethod::Center) { + + auto vec = onSketchPos - handler->centerPoint; + if (!onViewParameters[OnViewParameter::Third]->isSet) { + onViewParameters[OnViewParameter::Third]->setSpinboxValue(vec.Length()); + } + + if (!onViewParameters[OnViewParameter::Fourth]->isSet) { + double angle = vec.Length() > 0 ? vec.Angle() * 180 / M_PI : 0; + onViewParameters[OnViewParameter::Fourth]->setSpinboxValue(angle, + Base::Unit::Angle); + } + + Base::Vector3d start = toVector3d(handler->centerPoint); + Base::Vector3d end = toVector3d(onSketchPos); + + onViewParameters[OnViewParameter::Third]->setPoints(start, end); + + + onViewParameters[OnViewParameter::Fourth]->setPoints(start, Base::Vector3d()); + onViewParameters[OnViewParameter::Fourth]->setLabelRange( + (onSketchPos - handler->centerPoint).Angle()); + } + else { + if (!onViewParameters[OnViewParameter::Third]->isSet) { + setOnViewParameterValue(OnViewParameter::Third, onSketchPos.x); + } + + if (!onViewParameters[OnViewParameter::Fourth]->isSet) { + setOnViewParameterValue(OnViewParameter::Fourth, onSketchPos.y); + } + + bool sameSign = onSketchPos.x * onSketchPos.y > 0.; + onViewParameters[OnViewParameter::Third]->setLabelAutoDistanceReverse(!sameSign); + onViewParameters[OnViewParameter::Fourth]->setLabelAutoDistanceReverse(sameSign); + onViewParameters[OnViewParameter::Third]->setPoints(Base::Vector3d(), + toVector3d(onSketchPos)); + onViewParameters[OnViewParameter::Fourth]->setPoints(Base::Vector3d(), + toVector3d(onSketchPos)); + } + } break; + case SelectMode::SeekThird: { + if (handler->constructionMethod() + == DrawSketchHandlerEllipse::ConstructionMethod::Center) { + if (!onViewParameters[OnViewParameter::Fifth]->isSet) { + onViewParameters[OnViewParameter::Fifth]->setSpinboxValue( + handler->secondAxis.Length()); + } + + Base::Vector3d start = toVector3d(handler->centerPoint); + Base::Vector3d end = toVector3d(handler->centerPoint + handler->secondAxis); + + onViewParameters[OnViewParameter::Fifth]->setPoints(start, end); + } + else { + if (!onViewParameters[OnViewParameter::Fifth]->isSet) { + setOnViewParameterValue(OnViewParameter::Fifth, onSketchPos.x); + } + + if (!onViewParameters[OnViewParameter::Sixth]->isSet) { + setOnViewParameterValue(OnViewParameter::Sixth, onSketchPos.y); + } + + bool sameSign = onSketchPos.x * onSketchPos.y > 0.; + onViewParameters[OnViewParameter::Fifth]->setLabelAutoDistanceReverse(!sameSign); + onViewParameters[OnViewParameter::Sixth]->setLabelAutoDistanceReverse(sameSign); + onViewParameters[OnViewParameter::Fifth]->setPoints(Base::Vector3d(), + toVector3d(onSketchPos)); + onViewParameters[OnViewParameter::Sixth]->setPoints(Base::Vector3d(), + toVector3d(onSketchPos)); + } + } break; + default: + break; + } +} + +template<> +void DSHEllipseController::doChangeDrawSketchHandlerMode() +{ + switch (handler->state()) { + case SelectMode::SeekFirst: { + if (onViewParameters[OnViewParameter::First]->isSet + && onViewParameters[OnViewParameter::Second]->isSet) { + + handler->setState(SelectMode::SeekSecond); + } + } break; + case SelectMode::SeekSecond: { + if (onViewParameters[OnViewParameter::Third]->isSet + && onViewParameters[OnViewParameter::Fourth]->isSet) { + + handler->setState(SelectMode::SeekThird); + } + } break; + case SelectMode::SeekThird: { + if (handler->constructionMethod() + == DrawSketchHandlerEllipse::ConstructionMethod::Center) { + if (onViewParameters[OnViewParameter::Fifth]->isSet) { + + handler->setState(SelectMode::End); + } + } + else { + if (onViewParameters[OnViewParameter::Fifth]->isSet + && onViewParameters[OnViewParameter::Sixth]->isSet) { + + handler->setState(SelectMode::End); + } + } + } break; + default: + break; + } +} + +template<> +void DSHEllipseController::addConstraints() +{ + if (handler->constructionMethod() == DrawSketchHandlerEllipse::ConstructionMethod::Center) { + int firstCurve = handler->ellipseGeoId; + + auto x0 = onViewParameters[OnViewParameter::First]->getValue(); + auto y0 = onViewParameters[OnViewParameter::Second]->getValue(); + auto angle = onViewParameters[OnViewParameter::Fourth]->getValue() * M_PI / 180; + + auto x0set = onViewParameters[OnViewParameter::First]->isSet; + auto y0set = onViewParameters[OnViewParameter::Second]->isSet; + auto firstRadiusSet = onViewParameters[OnViewParameter::Third]->isSet; + auto angleSet = onViewParameters[OnViewParameter::Fourth]->isSet; + auto secondRadiusSet = onViewParameters[OnViewParameter::Fifth]->isSet; + + using namespace Sketcher; + + if (!handler->ShapeGeometry.empty() + && handler->ShapeGeometry[0]->getTypeId() == Part::GeomEllipse::getClassTypeId()) { + + int firstLine = firstCurve + 1; // this is always the major axis + int secondLine = firstCurve + 2; // this is always the minor axis + + if (handler->firstRadius < handler->secondRadius) { + std::swap(firstLine, secondLine); + } + + // NOTE: Because mouse positions are enforced by the widget, it is not possible to use + // the last radius > first radius when widget parameters are enforced. Then firstLine + // always goes with firstRadiusSet. + + auto constraintx0 = [&]() { + ConstraintToAttachment(GeoElementId(firstCurve, PointPos::mid), + GeoElementId::VAxis, + x0, + handler->sketchgui->getObject()); + }; + + auto constrainty0 = [&]() { + ConstraintToAttachment(GeoElementId(firstCurve, PointPos::mid), + GeoElementId::HAxis, + y0, + handler->sketchgui->getObject()); + }; + + auto constraintFirstRadius = [&]() { + Gui::cmdAppObjectArgs( + handler->sketchgui->getObject(), + "addConstraint(Sketcher.Constraint('Distance',%d,%d,%d,%d,%f)) ", + firstCurve, + 3, + firstLine, + 1, + handler->firstRadius); + }; + + auto constraintSecondRadius = [&]() { + Gui::cmdAppObjectArgs( + handler->sketchgui->getObject(), + "addConstraint(Sketcher.Constraint('Distance',%d,%d,%d,%d,%f)) ", + firstCurve, + 3, + secondLine, + 1, + handler->secondRadius); + }; + + auto constraintAngle = [&]() { + Gui::cmdAppObjectArgs(handler->sketchgui->getObject(), + "addConstraint(Sketcher.Constraint('Angle',%d,%f)) ", + firstLine, + angle); + }; + + // NOTE: if AutoConstraints is empty, we can add constraints directly without any + // diagnose. No diagnose was run. + if (handler->AutoConstraints.empty()) { + if (x0set) { + constraintx0(); + } + + if (y0set) { + constrainty0(); + } + + + // this require to show internal geometry. + if (firstRadiusSet) { + constraintFirstRadius(); + } + + // Todo: this makes the ellipse 'jump' because it's doing a 180 degree turn before + // applying asked angle. Probably because start and end points of line are not in + // the correct direction. + if (angleSet) { + constraintAngle(); + } + + if (secondRadiusSet) { + constraintSecondRadius(); + } + } + else { // There is a valid diagnose. + auto centerpointinfo = + handler->getPointInfo(GeoElementId(firstCurve, PointPos::mid)); + + // if Autoconstraints is empty we do not have a diagnosed system and the parameter + // will always be set + if (x0set && centerpointinfo.isXDoF()) { + constraintx0(); + + handler + ->diagnoseWithAutoConstraints(); // ensure we have recalculated parameters + // after each constraint addition + + centerpointinfo = handler->getPointInfo( + GeoElementId(firstCurve, PointPos::mid)); // get updated point position + } + + // if Autoconstraints is empty we do not have a diagnosed system and the parameter + // will always be set + if (y0set && centerpointinfo.isYDoF()) { + constrainty0(); + + handler + ->diagnoseWithAutoConstraints(); // ensure we have recalculated parameters + // after each constraint addition + } + + // Major axis (it is not a solver parameter in the solver implementation) + + int leftDoFs = handler->getLineDoFs(firstLine); + + if (firstRadiusSet && leftDoFs > 0) { + constraintFirstRadius(); + handler + ->diagnoseWithAutoConstraints(); // It is not a normal line as it is + // constrained by the Ellipse, so we need + // to recalculate after radius addition + leftDoFs = handler->getLineDoFs(firstLine); + } + + if (angleSet && leftDoFs > 0) { + constraintAngle(); + handler + ->diagnoseWithAutoConstraints(); // It is not a normal line as it is + // constrained by the Ellipse, so we need + // to recalculate after radius addition + } + + // Minor axis (it is a solver parameter in the solver implementation) + + auto edgeinfo = handler->getEdgeInfo(firstCurve); + auto ellipse = static_cast(edgeinfo); + + if (secondRadiusSet && ellipse.isMinorRadiusDoF()) { + constraintSecondRadius(); + } + } + } + else { // it is a circle + int firstCurve = handler->getHighestCurveIndex(); + + auto x0 = toolWidget->getParameter(OnViewParameter::First); + auto y0 = toolWidget->getParameter(OnViewParameter::Second); + + auto x0set = onViewParameters[OnViewParameter::First]->isSet; + auto y0set = onViewParameters[OnViewParameter::Second]->isSet; + auto radiusSet = onViewParameters[OnViewParameter::Third]->isSet; + + using namespace Sketcher; + + auto constraintx0 = [&]() { + ConstraintToAttachment(GeoElementId(firstCurve, PointPos::mid), + GeoElementId::VAxis, + x0, + handler->sketchgui->getObject()); + }; + + auto constrainty0 = [&]() { + ConstraintToAttachment(GeoElementId(firstCurve, PointPos::mid), + GeoElementId::HAxis, + y0, + handler->sketchgui->getObject()); + }; + + auto constraintradius = [&]() { + Gui::cmdAppObjectArgs(handler->sketchgui->getObject(), + "addConstraint(Sketcher.Constraint('Radius',%d,%f)) ", + firstCurve, + handler->firstRadius); + }; + + // NOTE: if AutoConstraints is empty, we can add constraints directly without any + // diagnose. No diagnose was run. + if (handler->AutoConstraints.empty()) { + if (x0set) { + constraintx0(); + } + + if (y0set) { + constrainty0(); + } + + if (radiusSet) { + constraintradius(); + } + } + else { // There is a valid diagnose. + auto startpointinfo = + handler->getPointInfo(GeoElementId(firstCurve, PointPos::mid)); + + // if Autoconstraints is empty we do not have a diagnosed system and the parameter + // will always be set + if (x0set && startpointinfo.isXDoF()) { + constraintx0(); + + handler + ->diagnoseWithAutoConstraints(); // ensure we have recalculated parameters + // after each constraint addition + + startpointinfo = handler->getPointInfo( + GeoElementId(firstCurve, PointPos::mid)); // get updated point position + } + + // if Autoconstraints is empty we do not have a diagnosed system and the parameter + // will always be set + if (y0set && startpointinfo.isYDoF()) { + constrainty0(); + + handler + ->diagnoseWithAutoConstraints(); // ensure we have recalculated parameters + // after each constraint addition + } + + auto edgeinfo = handler->getEdgeInfo(firstCurve); + auto circle = static_cast(edgeinfo); + + // if Autoconstraints is empty we do not have a diagnosed system and the parameter + // will always be set + if (radiusSet && circle.isRadiusDoF()) { + constraintradius(); + } + } + } + } + // No constraint possible for 3 rim ellipse. +} + + } // namespace SketcherGui