diff --git a/DFTFringe.pro b/DFTFringe.pro index f722d1bd..4adcc4e2 100644 --- a/DFTFringe.pro +++ b/DFTFringe.pro @@ -210,6 +210,7 @@ SOURCES += SingleApplication/singleapplication.cpp \ percentcorrectiondlg.cpp \ pixelstats.cpp \ plotcolor.cpp \ + profilecurve.cpp \ profileplot.cpp \ profileplotpicker.cpp \ ronchicomparedialog.cpp \ @@ -330,6 +331,7 @@ HEADERS += bezier/bezier.h \ percentCorrectionSurface.h \ pixelstats.h \ plotcolor.h \ + profilecurve.h \ profileplot.h \ profileplotpicker.h \ ronchicomparedialog.h \ diff --git a/DFTFringe_Dale.pro b/DFTFringe_Dale.pro index e0c6cb34..24b5d0e7 100644 --- a/DFTFringe_Dale.pro +++ b/DFTFringe_Dale.pro @@ -45,6 +45,7 @@ SOURCES += main.cpp \ oglrendered.cpp \ pdfcalibrationdlg.cpp \ percentcorrectiondlg.cpp \ + profilecurve.cpp \ profileplot.cpp \ profileplotpicker.cpp \ ronchicomparedialog.cpp \ @@ -163,6 +164,7 @@ HEADERS += mainwindow.h \ pdfcalibrationdlg.h \ percentCorrectionSurface.h \ percentcorrectiondlg.h \ + profilecurve.h \ profileplot.h \ profileplotpicker.h \ ronchicomparedialog.h \ diff --git a/DFTFringe_QT5.pro b/DFTFringe_QT5.pro index e7c3fbdf..79b3336e 100644 --- a/DFTFringe_QT5.pro +++ b/DFTFringe_QT5.pro @@ -209,6 +209,7 @@ SOURCES += SingleApplication/singleapplication.cpp \ percentcorrectiondlg.cpp \ pixelstats.cpp \ plotcolor.cpp \ + profilecurve.cpp \ profileplot.cpp \ profileplotpicker.cpp \ ronchicomparedialog.cpp \ @@ -329,6 +330,7 @@ HEADERS += bezier/bezier.h \ percentCorrectionSurface.h \ pixelstats.h \ plotcolor.h \ + profilecurve.h \ profileplot.h \ profileplotpicker.h \ ronchicomparedialog.h \ diff --git a/profilecurve.cpp b/profilecurve.cpp new file mode 100644 index 00000000..46bd726d --- /dev/null +++ b/profilecurve.cpp @@ -0,0 +1,86 @@ +#include "profilecurve.h" +#include +#include "profilecurve.h" +#include +#include +#include + +// creates a curve of the profile and will highlight segments where the slope is violated if the user has that feature enabled. +// uses the base class for most of the work. Overrides the actual painting of the line. +ProfileCurve::ProfileCurve(const QString &title) + : QwtPlotCurve(title) + , m_showSlopeError(false) + , m_slopeLimit(0.0) + , m_highlightWidth(2) +{ + // Ensure we are using basic line style for the base profile + setStyle(QwtPlotCurve::Lines); +} + +void ProfileCurve::setSlopeSettings(bool show, double limit, int highlightWidth) +{ + m_showSlopeError = show; + m_slopeLimit = limit; + m_highlightWidth = highlightWidth; +} + +#include // Ensure this is at the top of profilecurve.cpp + +#include +#include + +void ProfileCurve::drawLines(QPainter *painter, + const QwtScaleMap &xMap, + const QwtScaleMap &yMap, + const QRectF &canvasRect, + int from, int to) const +{ + Q_UNUSED(canvasRect); // drawLines is an override function, we shall not change args + if (to <= from) return; + + painter->save(); + + // 1. MANUALLY DRAW THE BASE BLACK LINE (Replacing Qwt's default) + // This prevents Qwt from drawing its own spider lines. + QPen basePen = pen(); + painter->setPen(basePen); + + for (int i = from; i < to; ++i) { + QPointF p1 = sample(i); + QPointF p2 = sample(i + 1); + + // If either point is NaN, skip this segment entirely (lifts the pen) + if (std::isnan(p1.y()) || std::isnan(p2.y())) continue; + + // Draw the segment only between two valid points + double x1 = xMap.transform(p1.x()); + double y1 = yMap.transform(p1.y()); + double x2 = xMap.transform(p2.x()); + double y2 = yMap.transform(p2.y()); + + painter->drawLine(QPointF(x1, y1), QPointF(x2, y2)); + } + + // 2. DRAW THE ORANGE HIGHLIGHTS + if (m_showSlopeError && m_slopeLimit > 0.0) { + QPen orangePen(QColor("orange"), m_highlightWidth); + orangePen.setCapStyle(Qt::RoundCap); + painter->setPen(orangePen); + + for (int i = from; i < to; ++i) { + QPointF p1 = sample(i); + QPointF p2 = sample(i + 1); + + if (std::isnan(p1.y()) || std::isnan(p2.y())) continue; + + if (std::abs(p1.y() - p2.y()) > m_slopeLimit) { + painter->drawLine( + QPointF(xMap.transform(p1.x()), yMap.transform(p1.y())), + QPointF(xMap.transform(p2.x()), yMap.transform(p2.y())) + ); + } + } + } + + painter->restore(); +} diff --git a/profilecurve.h b/profilecurve.h new file mode 100644 index 00000000..50848682 --- /dev/null +++ b/profilecurve.h @@ -0,0 +1,42 @@ +#ifndef PROFILECURVE_H +#define PROFILECURVE_H + +#include +#include +#include + +/** + * @brief The ProfileCurve class + * A custom QwtPlotCurve that automatically highlights segments where the + * vertical delta between adjacent points exceeds a specified threshold. + */ +class ProfileCurve : public QwtPlotCurve +{ +public: + explicit ProfileCurve(const QString &title = QString()); + + /** + * @brief Configure the slope highlighting behavior + * @param show - Toggle the orange highlight on/off + * @param limit - The vertical delta threshold (hDelLimit) + * @param highlightWidth - Width of the orange pen + */ + void setSlopeSettings(bool show, double limit, int highlightWidth = 2); + +protected: + /** + * @brief Overrides the standard line drawing to add the highlight pass + */ + virtual void drawLines(QPainter *painter, + const QwtScaleMap &xMap, + const QwtScaleMap &yMap, + const QRectF &canvasRect, + int from, int to) const override; + +private: + bool m_showSlopeError; + double m_slopeLimit; + int m_highlightWidth; +}; + +#endif // PROFILECURVE_H diff --git a/profileplot.cpp b/profileplot.cpp index ad6a5acd..ce1a7e57 100644 --- a/profileplot.cpp +++ b/profileplot.cpp @@ -50,6 +50,7 @@ #include "zernikeprocess.h" #include #include "plotcolor.h" +#include extern double outputLambda; @@ -62,6 +63,8 @@ extern double outputLambda; #include #include #include "percentcorrectiondlg.h" +#include "profilecurve.h" + #define DEGTORAD M_PI/180.; double g_angle = 270. * DEGTORAD; //start at 90 deg (pointing east) double y_offset = 0.; @@ -406,95 +409,82 @@ QPolygonF ProfilePlot::createAverageProfile(double /*umnits*/, wavefront * /*wf* return avg; } -QPolygonF ProfilePlot::createProfile(double units, wavefront *wf, bool allowOffset){ + + +// creates set of points for a profile at a given angle. Adds NaN values to points in the obstruction or outside the mirror +// Tee angle for the diameter is given in a global. +QPolygonF ProfilePlot::createProfile(double units, const wavefront *wf, bool allowOffset) { QPolygonF points; mirrorDlg &md = *mirrorDlg::get_Instance(); - double steps = 1./wf->m_outside.m_radius; - double offset = y_offset; - if (!allowOffset) offset = 0.; - double radius = md.m_clearAperature/2.; - double obs_radius = md.obs/2.; - if (m_displayInches){ + // 1. Setup constants + double steps = 1.0 / wf->m_outside.m_radius; + double offset = allowOffset ? y_offset : 0.0; + double radius = md.m_clearAperature / 2.0; + double obs_radius = md.obs / 2.0; + + if (m_displayInches) { obs_radius /= 25.4; } - for (double rad = -1.; rad < 1.; rad += steps){ - int dx, dy; + // Main Sampling Loop + for (double rad = -1.0; rad < 1.0; rad += steps) { double radn = rad * wf->m_outside.m_radius; double radx = rad * radius; - if (m_displayInches){ - radx /= 25.4; - } - double e = 1.; - if (m_displayPercent){ - radx = 100. * radx/radius; - obs_radius = 100 *( md.obs/2)/radius; - } + if (m_displayInches) radx /= 25.4; - if (md.isEllipse()){ - e = md.m_verticalAxis/md.diameter; + if (m_displayPercent) { + radx = 100.0 * radx / radius; + obs_radius = 100.0 * (md.obs / 2.0) / radius; } - dx = radn * cos(g_angle + M_PI_2) + wf->m_outside.m_center.x(); - dy = -radn * e * sin(g_angle + M_PI_2) + wf->m_outside.m_center.y(); - if (dy >= wf->data.rows || dx >= wf->data.cols || dy < 0 || dx < 0){ - continue; - + double e = 1.0; + if (md.isEllipse()) { + e = md.m_verticalAxis / md.diameter; } + // Calculate matrix coordinates + int dx = radn * cos(g_angle + M_PI_2) + wf->m_outside.m_center.x(); + int dy = -radn * e * sin(g_angle + M_PI_2) + wf->m_outside.m_center.y(); - if (abs(radx) < obs_radius){ - points << QPointF(radx,0.0); + // Boundary Check: Ignore points outside the matrix + if (dy >= wf->data.rows || dx >= wf->data.cols || dy < 0 || dx < 0) { continue; } - if (wf->workMask.at(dy,dx)){ - double defocus = 0.; - - if (m_defocus_mode){ - defocus = (m_defocusValue)* (-1. + 2. * rad * rad); - points << QPointF(radx,(units * (wf->workData((int)dy,(int)dx) + defocus ) * - wf->lambda/outputLambda) +offset * units); - } - else { - - points << QPointF(radx,(units * (wf->workData((int)dy,(int)dx) ) * - wf->lambda/outputLambda) +offset * units); - - } + // Handle the obstruction (The Hole) + if (std::abs(radx) < obs_radius) { + // Only add a NaN if the very last point was a real number. + // This "lifts the pen" once and then skips the rest of the hole. + if (!points.isEmpty() && !std::isnan(points.last().y())) { + points << QPointF(radx, qQNaN()); } - //else points << QPointF(radx,0.0); - } - - if (m_showSlopeError){ - - double arcsecLimit = (slopeLimitArcSec/3600) * M_PI/180; - double xDel = points[0].x() - points[1].x(); - double hDelLimit =m_showNm * m_showSurface * ((outputLambda/m_wf->lambda)*fabs(xDel * tan(arcsecLimit)) /(outputLambda * 1.e-6)); + continue; + } - for (int i = 0; i < points.size() - 1; ++i){ - double hdel = (points[i].y()- points[i+1].y()); - if (fabs(points[i].x()) < obs_radius || fabs(points[i+1].x()) < obs_radius) - continue; - if (fabs(hdel) > hDelLimit){ + // Mask and Data Processing + if (wf->workMask.at(dy, dx)) { + double val = (units * (wf->workData(dy, dx)) * wf->lambda / outputLambda) + (offset * units); - QVector pts; - QwtPlotCurve *limitCurve = new QwtPlotCurve; - pts<< points[i] << points[i+1]; - limitCurve->setSamples(pts); + if (m_defocus_mode) { + double defocus = (m_defocusValue) * (-1.0 + 2.0 * rad * rad); + val += (units * defocus * wf->lambda / outputLambda); + } - limitCurve->setPen(QPen(QColor("orange"),Settings2::m_profile->slopeErrorWidth())); - limitCurve->setLegendAttribute(QwtPlotCurve::LegendShowSymbol,false ); - limitCurve->setLegendAttribute(QwtPlotCurve::LegendShowLine,false ); - limitCurve->setItemAttribute(QwtPlotCurve::Legend,false); - limitCurve->attach( m_plot); + points << QPointF(radx, val); + } else { + // Lift the pen if we hit a mask gap outside the center hole + if (!points.isEmpty() && !std::isnan(points.last().y())) { + points << QPointF(radx, qQNaN()); } } } + return points; } + + // create a smoothed wave front with only spherical terms. // Use that to get zernike values to send to percent completion feature // display the profile and then send the zerns to percent completion @@ -660,6 +650,8 @@ qDebug() << "Populate"; double smoothing = settings.value("GBValue", 20).toInt(); m_plot->detachItems(QwtPlotItem::Rtti_PlotTextLabel); + + if (m_wf->m_outside.m_radius > 0 && settings.value("GBlur", false).toBool()){ double val = .01 * (m_wf->diameter) * smoothing; QString t = QString("Surface Smoothing diameter %1% of surface diameter %2 mm") @@ -694,6 +686,7 @@ qDebug() << "Populate"; m_plot->detachItems( QwtPlotItem::Rtti_PlotCurve); m_plot->detachItems( QwtPlotItem::Rtti_PlotMarker); + double arcsecLimit = (slopeLimitArcSec/3600) * M_PI/180; m_plot->insertLegend( new QwtLegend() , QwtPlot::BottomLegend); surfaceAnalysisTools *saTools = surfaceAnalysisTools::get_Instance(); @@ -704,7 +697,7 @@ qDebug() << "Populate"; wavefront* wf = wfs->at(list[i]); QStringList path = wf->name.split("/"); QString name = path.last().replace(".wft",""); - QwtPlotCurve *cprofile = new QwtPlotCurve(name ); + ProfileCurve *cprofile = new ProfileCurve(name ); int width = Settings2::m_profile->lineWidth(); if (name == m_wf->name.split("/").last().replace(".wft","")) width = Settings2::m_profile->selectedWidth(); @@ -719,11 +712,23 @@ qDebug() << "Populate"; y_offset = m_waveFrontyOffsets[name + " avg"]; qDebug() << "using avg"; } -qDebug() << "offsets" << m_waveFrontyOffsets<< y_offset; + // if show one angle if (m_show_oneAngle or (!m_showAvg and !m_show_16_diameters and !m_show_oneAngle)){ - cprofile->setSamples( createProfile( units,wf, true)); + QPolygonF points = createProfile(units, wf, true); + if (points.size() >= 2) { + // Distance between two samples + double xDel = fabs(points[0].x() - points[1].x()); + + // Recalculate hDelLimit using this specific xDel + double hDelLimit = m_showNm * m_showSurface * ((outputLambda/m_wf->lambda) * fabs(xDel * tan(arcsecLimit)) / (outputLambda * 1.e-6)); + + cprofile->setSlopeSettings(m_showSlopeError, hDelLimit, Settings2::m_profile->slopeErrorWidth()); + } + cprofile->setSamples(points); + + cprofile->attach( m_plot ); } if (m_show_16_diameters){ diff --git a/profileplot.h b/profileplot.h index e797b68a..26da92d4 100644 --- a/profileplot.h +++ b/profileplot.h @@ -49,7 +49,7 @@ class ProfilePlot : public QWidget QVector *wfs; void setSurface(wavefront * wf); virtual void resizeEvent( QResizeEvent * ); - QPolygonF createProfile(double units, wavefront *wf, bool allowOffset = true); + QPolygonF createProfile(double units, const wavefront *wf, bool allowOffset = true); QPolygonF createAverageProfile(double umnits, wavefront *wf, bool removeNull); ContourTools *m_tools; double m_waveRange; diff --git a/profileplotpicker.cpp b/profileplotpicker.cpp index e09ebccb..f37ef83d 100644 --- a/profileplotpicker.cpp +++ b/profileplotpicker.cpp @@ -111,7 +111,7 @@ void profilePlotPicker::select( QPoint pos ) if ( ( *it )->rtti() == QwtPlotItem::Rtti_PlotCurve ) { QwtPlotCurve *c = static_cast( *it ); - + if (c->title().text() == "slope") continue; double d; int idx = c->closestPoint( pos, &d ); if ( d < dist )