#include "curve_view.h" #include "../constant.h" #include "dots.h" #include #include #include #include #include #include #include #include #include using namespace Poincare; namespace Shared { CurveView::CurveView(CurveViewRange * curveViewRange, CurveViewCursor * curveViewCursor, BannerView * bannerView, CursorView * cursorView, View * okView, bool displayBanner) : View(), m_bannerView(bannerView), m_curveViewCursor(curveViewCursor), m_curveViewRange(curveViewRange), m_cursorView(cursorView), m_okView(okView), m_forceOkDisplay(false), m_mainViewSelected(false), m_drawnRangeVersion(0) { } void CurveView::reload() { uint32_t rangeVersion = m_curveViewRange->rangeChecksum(); if (m_drawnRangeVersion != rangeVersion) { // FIXME: This should also be called if the *curve* changed m_drawnRangeVersion = rangeVersion; KDCoordinate bannerHeight = (m_bannerView != nullptr) ? m_bannerView->bounds().height() : 0; markRectAsDirty(KDRect(0, 0, bounds().width(), bounds().height() - bannerHeight)); if (label(Axis::Horizontal, 0) != nullptr) { computeLabels(Axis::Horizontal); } if (label(Axis::Vertical, 0) != nullptr) { computeLabels(Axis::Vertical); } } layoutSubviews(); } bool CurveView::isMainViewSelected() const { return m_mainViewSelected; } void CurveView::selectMainView(bool mainViewSelected) { if (m_mainViewSelected != mainViewSelected) { m_mainViewSelected = mainViewSelected; reload(); } } void CurveView::setCurveViewRange(CurveViewRange * curveViewRange) { m_curveViewRange = curveViewRange; } /* When setting cursor, banner or ok view we first dirty the former element * frame (in case we set the new element to be nullptr or the new element frame * does not recover the former element frame) and then we dirty the new element * frame (most of the time it is automatically done by the layout but the frame * might be identical to the previous one and in that case layoutSubviews will * do nothing). */ void CurveView::setCursorView(CursorView * cursorView) { markRectAsDirty(cursorFrame()); m_cursorView = cursorView; markRectAsDirty(cursorFrame()); layoutSubviews(); } void CurveView::setBannerView(View * bannerView) { markRectAsDirty(bannerFrame()); m_bannerView = bannerView; layoutSubviews(); } void CurveView::setOkView(View * okView) { markRectAsDirty(okFrame()); m_okView = okView; layoutSubviews(); } /* We need to locate physical points on the screen more precisely than pixels, * hence by floating-point coordinates. We agree that the coordinates of the * center of a pixel corresponding to KDPoint(x,y) are precisely (x,y). In * particular, the coordinates of a pixel's corners are not integers but half * integers. Finally, a physical point with floating-point coordinates (x,y) * is located in the pixel with coordinates (std::round(x), std::round(y)). * * Translating CurveViewRange coordinates to pixel coordinates on the screen: * Along the horizontal axis * Pixel / physical coordinate CurveViewRange coordinate * 0 xMin() * m_frame.width() - 1 xMax() * Along the vertical axis * Pixel / physical coordinate CurveViewRange coordinate * 0 yMax() * m_frame.height() - 1 yMin() */ float CurveView::pixelWidth() const { return (m_curveViewRange->xMax() - m_curveViewRange->xMin()) / (m_frame.width() - 1); } float CurveView::pixelHeight() const { return (m_curveViewRange->yMax() - m_curveViewRange->yMin()) / (m_frame.height() - 1); } float CurveView::pixelLength(Axis axis) const { return axis == Axis::Horizontal ? pixelWidth() : pixelHeight(); } float CurveView::pixelToFloat(Axis axis, KDCoordinate p) const { return (axis == Axis::Horizontal) ? m_curveViewRange->xMin() + p * pixelWidth() : m_curveViewRange->yMax() - p * pixelHeight(); } static float clippedFloat(float f) { /* Make sure that the returned value is between the maximum and minimum * possible values of KDCoordinate. */ if (f == NAN) { return NAN; } else if (f < KDCOORDINATE_MIN) { return KDCOORDINATE_MIN; } else if (f > KDCOORDINATE_MAX) { return KDCOORDINATE_MAX; } else { return f; } } float CurveView::floatToPixel(Axis axis, float f) const { float result = (axis == Axis::Horizontal) ? (f - m_curveViewRange->xMin()) / pixelWidth() : (m_curveViewRange->yMax() - f) / pixelHeight(); return clippedFloat(result); } float CurveView::floatLengthToPixelLength(Axis axis, float f) const { float dist = f / pixelLength(axis); return clippedFloat(dist); } float CurveView::floatLengthToPixelLength(float dx, float dy) const { float dxPixel = floatLengthToPixelLength(Axis::Horizontal, dx); float dyPixel = floatLengthToPixelLength(Axis::Vertical, dy); return std::sqrt(dxPixel*dxPixel+dyPixel*dyPixel); } float CurveView::pixelLengthToFloatLength(Axis axis, float f) const { return f*pixelLength(axis); } void CurveView::drawGridLines(KDContext * ctx, KDRect rect, Axis axis, float step, KDColor boldColor, KDColor lightColor) const { Axis otherAxis = (axis == Axis::Horizontal) ? Axis::Vertical : Axis::Horizontal; /* We translate the pixel coordinates into floats, adding/subtracting 1 to * account for conversion errors. */ float otherAxisMin = pixelToFloat(otherAxis, otherAxis == Axis::Horizontal ? rect.left() - 1 : rect.bottom() + 1); float otherAxisMax = pixelToFloat(otherAxis, otherAxis == Axis::Horizontal ? rect.right() + 1 : rect.top() - 1); const int start = otherAxisMin/step; const int end = otherAxisMax/step; for (int i = start; i <= end; i++) { drawLine(ctx, rect, axis, i * step, i % 2 == 0 ? boldColor : lightColor); } } float CurveView::min(Axis axis) const { assert(axis == Axis::Horizontal || axis == Axis::Vertical); return (axis == Axis::Horizontal ? m_curveViewRange->xMin(): m_curveViewRange->yMin()); } float CurveView::max(Axis axis) const { assert(axis == Axis::Horizontal || axis == Axis::Vertical); return (axis == Axis::Horizontal ? m_curveViewRange->xMax() : m_curveViewRange->yMax()); } float CurveView::gridUnit(Axis axis) const { return (axis == Axis::Horizontal ? m_curveViewRange->xGridUnit() : m_curveViewRange->yGridUnit()); } int CurveView::numberOfLabels(Axis axis) const { float labelStep = 2.0f * gridUnit(axis); if (labelStep <= 0.0f) { return 0; } float minLabel = std::ceil(min(axis)/labelStep); float maxLabel = std::floor(max(axis)/labelStep); int numberOfLabels = maxLabel - minLabel + 1; assert(numberOfLabels <= (axis == Axis::Horizontal ? k_maxNumberOfXLabels : k_maxNumberOfYLabels)); return numberOfLabels; } void CurveView::computeLabels(Axis axis) { float step = gridUnit(axis); int axisLabelsCount = numberOfLabels(axis); for (int i = 0; i < axisLabelsCount; i++) { float labelValue = labelValueAtIndex(axis, i); /* Label cannot hold more than k_labelBufferMaxGlyphLength characters to prevent * them from overprinting one another.*/ int labelMaxGlyphLength = labelMaxGlyphLengthSize(); if (axis == Axis::Horizontal) { float pixelsPerLabel = std::max(0.0f, ((float)Ion::Display::Width)/((float)axisLabelsCount) - k_labelMargin); labelMaxGlyphLength = std::min(labelMaxGlyphLengthSize(), pixelsPerLabel/k_font->glyphSize().width()); } if (labelValue < step && labelValue > -step) { // Make sure the 0 value is really written 0 labelValue = 0.0f; } /* Label cannot hold more than k_labelBufferSize characters to prevent them * from overprinting one another. */ char * labelBuffer = label(axis, i); PrintFloat::ConvertFloatToText( labelValue, labelBuffer, k_labelBufferMaxSize, labelMaxGlyphLength, k_numberSignificantDigits, Preferences::PrintFloatMode::Decimal); if (axis == Axis::Horizontal) { if (labelBuffer[0] == 0) { /* Some labels are too big and may overlap their neighbors. We write the * extrema labels only. */ computeHorizontalExtremaLabels(); break; } if (i > 0 && strcmp(labelBuffer, label(axis, i-1)) == 0) { /* We need to increase the number if significant digits, otherwise some * labels are rounded to the same value. */ computeHorizontalExtremaLabels(true); break; } } } int maxNumberOfLabels = (axis == Axis::Horizontal ? k_maxNumberOfXLabels : k_maxNumberOfYLabels); // All remaining labels are empty. They shouldn't be accessed anyway. for (int i = axisLabelsCount; i < maxNumberOfLabels; i++) { label(axis, i)[0] = 0; } } void CurveView::simpleDrawBothAxesLabels(KDContext * ctx, KDRect rect) const { drawLabelsAndGraduations(ctx, rect, Axis::Vertical, true); drawLabelsAndGraduations(ctx, rect, Axis::Horizontal, true); } KDPoint CurveView::positionLabel(KDCoordinate xPosition, KDCoordinate yPosition, KDSize labelSize, RelativePosition horizontalPosition, RelativePosition verticalPosition) const { switch (horizontalPosition) { case RelativePosition::Before: // Left xPosition -= labelSize.width() + k_labelMargin; break; case RelativePosition::After: // Right xPosition += k_labelMargin; break; default: xPosition -= labelSize.width()/2; } switch (verticalPosition) { case RelativePosition::After: // Above yPosition -= labelSize.height() + k_labelMargin; break; case RelativePosition::Before: // Below yPosition += k_labelMargin; break; default: yPosition -= labelSize.height()/2; } return KDPoint(xPosition, yPosition); } void CurveView::drawLabel(KDContext * ctx, KDRect rect, float xPosition, float yPosition, const char * label, KDColor color, RelativePosition horizontalPosition, RelativePosition verticalPosition) const { KDSize labelSize = k_font->stringSize(label); KDCoordinate xCoordinate = std::round(floatToPixel(Axis::Horizontal, xPosition)); KDCoordinate yCoordinate = std::round(floatToPixel(Axis::Vertical, yPosition)); KDPoint position = positionLabel(xCoordinate, yCoordinate, labelSize, horizontalPosition, verticalPosition); if (rect.intersects(KDRect(position, labelSize))) { // TODO: should we blend? ctx->drawString(label, position, k_font, color, *Palette::BackgroundApps); } } enum class FloatingPosition : uint8_t { None, Min, Max }; void CurveView::drawLabelsAndGraduations(KDContext * ctx, KDRect rect, Axis axis, bool shiftOrigin, bool graduationOnly, bool fixCoordinate, KDCoordinate fixedCoordinate, KDColor backgroundColor) const { int numberLabels = numberOfLabels(axis); if (numberLabels <= 1) { return; } float verticalCoordinate = fixCoordinate ? fixedCoordinate : std::round(floatToPixel(Axis::Vertical, 0.0f)); float horizontalCoordinate = fixCoordinate ? fixedCoordinate : std::round(floatToPixel(Axis::Horizontal, 0.0f)); KDCoordinate viewHeight = bounds().height() - (bannerIsVisible() ? m_bannerView->minimalSizeForOptimalDisplay().height() : 0); /* If the axis is not visible, draw floating labels on the edge of the screen. * The X axis floating status is needed when drawing both axes labels. */ FloatingPosition floatingHorizontalLabels = FloatingPosition::None; KDCoordinate maximalVerticalPosition = graduationOnly ? viewHeight : viewHeight - k_font->glyphSize().height() - k_labelMargin; if (verticalCoordinate > maximalVerticalPosition) { floatingHorizontalLabels = FloatingPosition::Max; } else if (max(Axis::Vertical) < 0.0f) { floatingHorizontalLabels = FloatingPosition::Min; } FloatingPosition floatingLabels = FloatingPosition::None; if (axis == Axis::Horizontal) { floatingLabels = floatingHorizontalLabels; } else { KDCoordinate minimalHorizontalPosition = graduationOnly ? 0 : k_labelMargin + k_font->glyphSize().width() * 3; // We want do display at least 3 characters left of the Y axis if (horizontalCoordinate < minimalHorizontalPosition) { floatingLabels = FloatingPosition::Min; } else if (max(Axis::Horizontal) < 0.0f) { floatingLabels = FloatingPosition::Max; } } /* There might be less labels than graduations, if the extrema labels are too * close to the screen edge to write them. We must thus draw the graduations * separately from the labels. */ float labelStep = 2.0f * gridUnit(axis); int minLabelPixelPosition = std::round(floatToPixel(axis, labelStep * std::ceil(min(axis)/labelStep))); int maxLabelPixelPosition = std::round(floatToPixel(axis, labelStep * std::floor(max(axis)/labelStep))); // Draw the graduations int minDrawnLabel = 0; int maxDrawnLabel = numberLabels; if (axis == Axis::Vertical) { /* Do not draw an extremal vertical label if it collides with the horizontal * labels */ int horizontalLabelsMargin = k_font->glyphSize().height() * 2; if (floatingHorizontalLabels == FloatingPosition::Min && maxLabelPixelPosition < horizontalLabelsMargin) { maxDrawnLabel--; } else if (floatingHorizontalLabels == FloatingPosition::Max && minLabelPixelPosition > viewHeight - horizontalLabelsMargin) { minDrawnLabel++; } } if (floatingLabels == FloatingPosition::None) { for (int i = minDrawnLabel; i < maxDrawnLabel; i++) { KDCoordinate labelPosition = std::round(floatToPixel(axis, labelValueAtIndex(axis, i))); KDRect graduation = axis == Axis::Horizontal ? KDRect( labelPosition, verticalCoordinate -(k_labelGraduationLength-2)/2, 1, k_labelGraduationLength) : KDRect( horizontalCoordinate-(k_labelGraduationLength-2)/2, labelPosition, k_labelGraduationLength, 1); ctx->fillRect(graduation, *Palette::PrimaryText); } } if (graduationOnly) { return; } // Labels will be pulled. They must be up to date with current curve view. assert(m_drawnRangeVersion == m_curveViewRange->rangeChecksum()); // Draw the labels for (int i = minDrawnLabel; i < maxDrawnLabel; i++) { KDCoordinate labelPosition = std::round(floatToPixel(axis, labelValueAtIndex(axis, i))); char * labelI = label(axis, i); KDSize textSize = k_font->stringSize(labelI); KDPoint position = KDPointZero; if (strcmp(labelI, "0") == 0) { if (floatingLabels != FloatingPosition::None) { // Do not draw the zero, it is symbolized by the other axis continue; } if (shiftOrigin && floatingLabels == FloatingPosition::None) { position = positionLabel(horizontalCoordinate, verticalCoordinate, textSize, RelativePosition::Before, RelativePosition::Before); goto DrawLabel; } } if (axis == Axis::Horizontal) { position = positionLabel(labelPosition, verticalCoordinate, textSize, RelativePosition::None, RelativePosition::Before); if (floatingLabels == FloatingPosition::Min) { position = KDPoint(position.x(), k_labelMargin); } else if (floatingLabels == FloatingPosition::Max) { position = KDPoint(position.x(), viewHeight - k_font->glyphSize().height() - k_labelMargin); } } else { position = positionLabel(horizontalCoordinate, labelPosition, textSize, RelativePosition::Before, RelativePosition::None); if (floatingLabels == FloatingPosition::Min) { position = KDPoint(k_labelMargin, position.y()); } else if (floatingLabels == FloatingPosition::Max) { position = KDPoint(Ion::Display::Width - textSize.width() - k_labelMargin, position.y()); } } DrawLabel: if (rect.intersects(KDRect(position, textSize))) { ctx->drawString(labelI, position, k_font, *Palette::PrimaryText, backgroundColor); } } } void CurveView::drawHorizontalOrVerticalSegment(KDContext * ctx, KDRect rect, Axis axis, float coordinate, float lowerBound, float upperBound, KDColor color, KDCoordinate thickness, KDCoordinate dashSize) const { KDCoordinate min = (axis == Axis::Horizontal) ? rect.x() : rect.y(); KDCoordinate max = (axis == Axis::Horizontal) ? rect.x() + rect.width() : rect.y() + rect.height(); KDCoordinate start = std::isinf(lowerBound) ? min : std::round(floatToPixel(axis, lowerBound)); KDCoordinate end = std::isinf(upperBound) ? max : std::round(floatToPixel(axis, upperBound)); if (start > end) { start = end; end = std::round(floatToPixel(axis, lowerBound)); } Axis otherAxis = (axis == Axis::Horizontal) ? Axis::Vertical : Axis::Horizontal; KDCoordinate pixelCoordinate = std::round(floatToPixel(otherAxis, coordinate)); if (dashSize < 0) { // Continuous segment is equivalent to one big dash dashSize = end - start; if (dashSize < 0) { // end-start overflowed dashSize = KDCOORDINATE_MAX; } } KDRect lineRect = KDRectZero; for (KDCoordinate i = start; i < end; i += 2*dashSize) { switch(axis) { case Axis::Horizontal: lineRect = KDRect(i, pixelCoordinate, dashSize, thickness); break; case Axis::Vertical: lineRect = KDRect(pixelCoordinate, i, thickness, dashSize); break; } if (rect.intersects(lineRect)) { ctx->fillRect(lineRect, color); } if (i > KDCOORDINATE_MAX - 2*dashSize) { // Avoid overflowing KDCoordinate break; } } } void CurveView::drawSegment(KDContext * ctx, KDRect rect, float x, float y, float u, float v, KDColor color, bool thick) const { float pxf = floatToPixel(Axis::Horizontal, x); float pyf = floatToPixel(Axis::Vertical, y); float puf = floatToPixel(Axis::Horizontal, u); float pvf = floatToPixel(Axis::Vertical, v); straightJoinDots(ctx, rect, pxf, pyf, puf, pvf, color, thick); } void CurveView::drawDot(KDContext * ctx, KDRect rect, float x, float y, KDColor color, Size size) const { KDCoordinate diameter = 0; const uint8_t * mask = nullptr; switch (size) { case Size::Tiny: diameter = Dots::TinyDotDiameter; mask = (const uint8_t *)Dots::TinyDotMask; break; case Size::Small: diameter = Dots::SmallDotDiameter; mask = (const uint8_t *)Dots::SmallDotMask; break; default: assert(size == Size::Large); diameter = Dots::LargeDotDiameter; mask = (const uint8_t *)Dots::LargeDotMask; } KDCoordinate px = std::round(floatToPixel(Axis::Horizontal, x)); KDCoordinate py = std::round(floatToPixel(Axis::Vertical, y)); KDRect dotRect(px - diameter/2, py - diameter/2, diameter, diameter); if (!rect.intersects(dotRect)) { return; } KDColor workingBuffer[Dots::LargeDotDiameter*Dots::LargeDotDiameter]; ctx->blendRectWithMask(dotRect, color, mask, workingBuffer); } void CurveView::drawArrow(KDContext * ctx, KDRect rect, float x, float y, float dx, float dy, KDColor color, float arrowWidth, float tanAngle) const { assert(tanAngle >= 0.0f); if (std::fabs(dx) < FLT_EPSILON && std::fabs(dy) < FLT_EPSILON) { // We can't draw an arrow without any orientation return; } // Translate arrowWidth in pixel length float pixelArrowWidth = 8.0f; // default value in pixels if (arrowWidth > 0.0f) { float dxdyFloat = std::sqrt(dx * dx + dy * dy); float dxArrowFloat = arrowWidth * std::fabs(dy) / dxdyFloat; float dyArrowFloat = arrowWidth * std::fabs(dx) / dxdyFloat; pixelArrowWidth = floatLengthToPixelLength(dxArrowFloat, dyArrowFloat); assert(pixelArrowWidth > 0.0f); } /* Let's call the following variables L and l: * * /arrow2 | * / | * / l * / | * / B | * <---------+---------------------------------------- * \ * \ * \ * \ * \arrow1 * * ----- L ----- * */ float lPixel = pixelArrowWidth / 2.0; float LPixel = lPixel / tanAngle; float xPixel = floatToPixel(Axis::Horizontal, x); float yPixel = floatToPixel(Axis::Vertical, y); // We compute the arrow segments in pixels float dxPixel = floatLengthToPixelLength(Axis::Horizontal, dx); float dyPixel = floatLengthToPixelLength(Axis::Vertical, dy); float dx2dy2Pixel = floatLengthToPixelLength(dx, dy); // Point B is the orthogonal projection of the arrow tips on the arrow body float bxPixel = xPixel - LPixel * dxPixel / dx2dy2Pixel; float byPixel = yPixel + LPixel * dyPixel / dx2dy2Pixel; float dxArrowPixel = - lPixel * dyPixel / dx2dy2Pixel; float dyArrowPixel = lPixel * dxPixel / dx2dy2Pixel; float arrow1xPixel = bxPixel + dxArrowPixel; float arrow1yPixel = byPixel - dyArrowPixel; float arrow2xPixel = bxPixel - dxArrowPixel; float arrow2yPixel = byPixel + dyArrowPixel; straightJoinDots(ctx, rect, xPixel, yPixel, arrow1xPixel, arrow1yPixel, color, true); straightJoinDots(ctx, rect, xPixel, yPixel, arrow2xPixel, arrow2yPixel, color, true); } void CurveView::drawGrid(KDContext * ctx, KDRect rect) const { KDColor boldColor = *Palette::GridPrimaryLine; KDColor lightColor = *Palette::GridSecondaryLine; drawGridLines(ctx, rect, Axis::Vertical, m_curveViewRange->xGridUnit(), boldColor, lightColor); drawGridLines(ctx, rect, Axis::Horizontal, m_curveViewRange->yGridUnit(), boldColor, lightColor); } void CurveView::drawAxes(KDContext * ctx, KDRect rect) const { drawAxis(ctx, rect, Axis::Vertical); drawAxis(ctx, rect, Axis::Horizontal); } void CurveView::drawAxis(KDContext * ctx, KDRect rect, Axis axis) const { drawLine(ctx, rect, axis, 0.0f, *Palette::PrimaryText, 1); } constexpr KDCoordinate thinCircleDiameter = 1; constexpr KDCoordinate thinStampSize = thinCircleDiameter+1; const uint8_t thinStampMask[(thinStampSize+1)*(thinStampSize+1)] = { 0xFF, 0xE1, 0xFF, 0xE1, 0x00, 0xE1, 0xFF, 0xE1, 0xFF, }; #define LINE_THICKNESS 2 #if LINE_THICKNESS == 2 constexpr KDCoordinate thickCircleDiameter = 2; constexpr KDCoordinate thickStampSize = thickCircleDiameter+1; const uint8_t thickStampMask[(thickStampSize+1)*(thickStampSize+1)] = { 0xFF, 0xE6, 0xE6, 0xFF, 0xE6, 0x33, 0x33, 0xE6, 0xE6, 0x33, 0x33, 0xE6, 0xFF, 0xE6, 0xE6, 0xFF, }; #elif LINE_THICKNESS == 3 constexpr KDCoordinate thickCircleDiameter = 3; constexpr KDCoordinate thickStampSize = thickCircleDiameter+1; const uint8_t thickStampMask[(thickStampSize+1)*(thickStampSize+1)] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7A, 0x0C, 0x7A, 0xFF, 0xFF, 0x0C, 0x00, 0x0C, 0xFF, 0xFF, 0x7A, 0x0C, 0x7A, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; #elif LINE_THICKNESS == 5 constexpr KDCoordinate thickCircleDiameter = 5; constexpr KDCoordinate thickStampSize = thickCircleDiameter+1; const uint8_t thickStampMask[(thickStampSize+1)*(thickStampSize+1)] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE1, 0x45, 0x0C, 0x45, 0xE1, 0xFF, 0xFF, 0x45, 0x00, 0x00, 0x00, 0x45, 0xFF, 0xFF, 0x0C, 0x00, 0x00, 0x00, 0x0C, 0xFF, 0xFF, 0x45, 0x00, 0x00, 0x00, 0x45, 0xFF, 0xFF, 0xE1, 0x45, 0x0C, 0x45, 0xE1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }; #endif constexpr static int k_maxNumberOfIterations = 10; void CurveView::drawCurve(KDContext * ctx, KDRect rect, float tStart, float tEnd, float tStep, EvaluateXYForFloatParameter xyFloatEvaluation, void * model, void * context, bool drawStraightLinesEarly, KDColor color, bool thick, bool colorUnderCurve, float colorLowerBound, float colorUpperBound, EvaluateXYForDoubleParameter xyDoubleEvaluation) const { float previousT = NAN; float t = NAN; float previousX = NAN; float x = NAN; float previousY = NAN; float y = NAN; int i = 0; bool isLastSegment = false; do { previousT = t; t = tStart + (i++) * tStep; if (t <= tStart) { t = tStart + FLT_EPSILON; } if (t >= tEnd) { t = tEnd - FLT_EPSILON; isLastSegment = true; } if (previousT == t) { // No need to draw segment. Happens when tStep << tStart . continue; } previousX = x; previousY = y; Coordinate2D xy = xyFloatEvaluation(t, model, context); x = xy.x1(); y = xy.x2(); if (colorUnderCurve && !std::isnan(x) && colorLowerBound < x && x < colorUpperBound && !(std::isnan(y) || std::isinf(y))) { drawHorizontalOrVerticalSegment(ctx, rect, Axis::Vertical, x, std::min(0.0f, y), std::max(0.0f, y), color, 1); } joinDots(ctx, rect, xyFloatEvaluation, model, context, drawStraightLinesEarly, previousT, previousX, previousY, t, x, y, color, thick, k_maxNumberOfIterations, xyDoubleEvaluation); } while (!isLastSegment); } void CurveView::drawCartesianCurve(KDContext * ctx, KDRect rect, float xMin, float xMax, EvaluateXYForFloatParameter xyFloatEvaluation, void * model, void * context, KDColor color, bool thick, bool colorUnderCurve, float colorLowerBound, float colorUpperBound, EvaluateXYForDoubleParameter xyDoubleEvaluation) const { float rectLeft = pixelToFloat(Axis::Horizontal, rect.left() - k_externRectMargin); float rectRight = pixelToFloat(Axis::Horizontal, rect.right() + k_externRectMargin); float tStart = std::isnan(rectLeft) ? xMin : std::max(xMin, rectLeft); float tEnd = std::isnan(rectRight) ? xMax : std::min(xMax, rectRight); assert(!std::isnan(tStart) && !std::isnan(tEnd)); if (std::isinf(tStart) || std::isinf(tEnd) || tStart > tEnd) { return; } float tStep = pixelWidth(); drawCurve(ctx, rect, tStart, tEnd, tStep, xyFloatEvaluation, model, context, true, color, thick, colorUnderCurve, colorLowerBound, colorUpperBound, xyDoubleEvaluation); } float PolarThetaFromCoordinates(float x, float y, Preferences::AngleUnit angleUnit) { // Return θ, between -π and π in given angleUnit for a (x,y) position. return Trigonometry::ConvertRadianToAngleUnit(std::arg(std::complex(x,y)), angleUnit).real(); } void CurveView::drawPolarCurve(KDContext * ctx, KDRect rect, float tStart, float tEnd, float tStep, EvaluateXYForFloatParameter xyFloatEvaluation, void * model, void * context, bool drawStraightLinesEarly, KDColor color, bool thick, bool colorUnderCurve, float colorLowerBound, float colorUpperBound, EvaluateXYForDoubleParameter xyDoubleEvaluation) const { // Compute rect limits float rectLeft = pixelToFloat(Axis::Horizontal, rect.left() - k_externRectMargin); float rectRight = pixelToFloat(Axis::Horizontal, rect.right() + k_externRectMargin); float rectUp = pixelToFloat(Axis::Vertical, rect.top() + k_externRectMargin); float rectDown = pixelToFloat(Axis::Vertical, rect.bottom() - k_externRectMargin); const Preferences::AngleUnit angleUnit = Preferences::sharedPreferences()->angleUnit(); const float piInAngleUnit = Trigonometry::PiInAngleUnit(angleUnit); /* Cancel optimization if : * - One of rect limits is nan. * - Step is too large, see cache optimization comments * ("To optimize cache..."). */ bool cancelOptimization = std::isnan(rectLeft + rectRight + rectUp + rectDown) || tStep >= piInAngleUnit; bool rectOverlapsNegativeAbscissaAxis = false; if (cancelOptimization || (rectUp > 0.0f && rectDown < 0.0f && rectLeft < 0.0f)) { if (cancelOptimization || rectRight > 0.0f) { // Origin is inside rect, tStart and tEnd cannot be optimized return drawCurve(ctx, rect, tStart, tEnd, tStep, xyFloatEvaluation, model, context, drawStraightLinesEarly, color, thick, colorUnderCurve, colorLowerBound, colorUpperBound, xyDoubleEvaluation); } // Rect view overlaps the abscissa, on the left of the origin. rectOverlapsNegativeAbscissaAxis = true; } float tMin, tMax; /* Compute angular coordinate of each corners of rect. * t3 --- t2 * | | * t4 --- t1 */ float t1 = PolarThetaFromCoordinates(rectRight, rectDown, angleUnit); float t2 = PolarThetaFromCoordinates(rectRight, rectUp, angleUnit); if (!rectOverlapsNegativeAbscissaAxis) { float t3 = PolarThetaFromCoordinates(rectLeft, rectUp, angleUnit); float t4 = PolarThetaFromCoordinates(rectLeft, rectDown, angleUnit); /* The area between tMin and tMax (modulo π) is the only area where * something needs to be plotted. */ tMin = std::min(std::min(t1,t2),std::min(t3,t4)); tMax = std::max(std::max(t1,t2),std::max(t3,t4)); } else { /* PolarThetaFromCoordinates yields coordinates between -π and π. When rect * is overlapping the negative abscissa (at this point, the origin cannot be * inside rect), t1 and t4 have a negative angle whereas t2 and t3 have a * positive angle. We ensure here that tMin is t2 (modulo 2π), tMax is t1, * and that tMax-tMin is minimal and positive. */ tMin = t2 - 2 * piInAngleUnit; tMax = t1; } // Add a thousandth of π as a margin to avoid visible approximation errors. tMax += piInAngleUnit / 1000.0f; tMin -= piInAngleUnit / 1000.0f; /* To optimize cache hits, the area actually drawn will be extended to nearest * cached θ. tStep being a multiple of cache steps (see * ComputeNonCartesianSteps), we extend segments on both ends to the closest * θ = tStart + tStep * i * If the drawn segment is extended too much, it might overlap with the next * extended segment. * For example, with * the segments that must be drawn and piInAngleUnit=7 : * tStart tEnd * kπ | (k+1)π (k+2)π (k+3)π (k+4)π (k+5)π (k+6)π |(k+7)π * |-------|-------|-------|-------|-------|-------|-------|-- * tMax-tMin=3 : |---***-|---***-|---***-|---***-|---***-|---***-|---***-|-- * A - tStep=3 : |---***-|---***-|---***-|---***-|---***-|---***-|---***-|-- * |___^^^_|__ | ^^^^^^|___ _|__^^^^^|^ |___^^^_|__ * | | ^^^^^|^ | ^^^ | | ^^^^^^| | * * B - tStep=6 : |---***-|---***-|---***-|---***-|---***-|---***-|---***-|-- * |___^^^^|^^ | ^^^^^^| ^|^^^^^^^|^^^^ | ^^^^|^^ * | | ^^^^^|^ |^^^^^^ | ^^|^^^^^^^|^^^ | * In situation A, Step are small enough, not all segments must be drawn. * In situation B, The entire range should be drawn, and two extended segments * overlap (at tStart+5*tStep). Optimization is useless. * If tStep < piInAngleUnit - (tMax - tMin), situation B cannot happen. */ if (tStep >= piInAngleUnit - tMax + tMin) { return drawCurve(ctx, rect, tStart, tEnd, tStep, xyFloatEvaluation, model, context, drawStraightLinesEarly, color, thick, colorUnderCurve, colorLowerBound, colorUpperBound, xyDoubleEvaluation); } /* The number of segments to draw can be reduced by drawing curve on intervals * where (tMin%π, tMax%π) intersects (tStart, tEnd).For instance : * if tStart=-π, tEnd=3π, tMin=π/4 and tMax=π/3, a curve is drawn between : * - [ π/4, π/3 ], [ 2π + π/4, 2π + π/3 ] * - [ -π + π/4, -π + π/3 ], [ π + π/4, π + π/3 ] in case f(θ) is negative */ // 1 - Set offset so that tStart <= tMax+thetaOffset < piInAngleUnit+tStart float thetaOffset = std::ceil((tStart - tMax)/piInAngleUnit) * piInAngleUnit; // 2 - Increase offset until tMin + thetaOffset > tEnd float tCache2 = tStart; while (tMin + thetaOffset <= tEnd) { float tS = std::max(tMin + thetaOffset, tStart); float tE = std::min(tMax + thetaOffset, tEnd); // Draw curve if there is an intersection if (tS <= tE) { /* To maximize cache hits, we floor (and ceil) tS (and tE) to the closest * cached value. Step is small enough so that the extra drawn curve cannot * overlap as tMax + tStep < piInAngleUnit + tMin) */ int i = std::floor((tS - tStart) / tStep); assert(tStart + tStep * i >= tCache2); float tCache1 = tStart + tStep * i; int j = std::ceil((tE - tStart) / tStep); tCache2 = std::min(tStart + tStep * j, tEnd); assert(tCache1 <= tCache2); drawCurve(ctx, rect, tCache1, tCache2, tStep, xyFloatEvaluation, model, context, drawStraightLinesEarly, color, thick, colorUnderCurve, colorLowerBound, colorUpperBound, xyDoubleEvaluation); } thetaOffset += piInAngleUnit; } } void CurveView::drawHistogram(KDContext * ctx, KDRect rect, EvaluateYForX yEvaluation, void * model, void * context, float firstBarAbscissa, float barWidth, bool fillBar, KDColor defaultColor, KDColor highlightColor, float highlightLowerBound, float highlightUpperBound) const { float rectMin = pixelToFloat(Axis::Horizontal, rect.left()); float rectMinBinNumber = std::floor((rectMin - firstBarAbscissa)/barWidth); float rectMinLowerBound = firstBarAbscissa + rectMinBinNumber*barWidth; float rectMax = pixelToFloat(Axis::Horizontal, rect.right()); float rectMaxBinNumber = std::floor((rectMax - firstBarAbscissa)/barWidth); float rectMaxUpperBound = firstBarAbscissa + (rectMaxBinNumber+1)*barWidth + barWidth; float pHighlightLowerBound = floatToPixel(Axis::Horizontal, highlightLowerBound); float pHighlightUpperBound = floatToPixel(Axis::Horizontal, highlightUpperBound); const float step = std::fmax(barWidth, pixelWidth()); for (float x = rectMinLowerBound; x < rectMaxUpperBound; x += step) { /* When |rectMinLowerBound| >> step, rectMinLowerBound + step = rectMinLowerBound. * In that case, quit the infinite loop. */ if (x == x-step || x == x+step) { return; } float centerX = fillBar ? x+barWidth/2.0f : x; float y = yEvaluation(centerX, model, context); if (std::isnan(y)) { continue; } KDCoordinate pxf = std::round(floatToPixel(Axis::Horizontal, x)); KDCoordinate pyf = std::round(floatToPixel(Axis::Vertical, y)); KDCoordinate pixelBarWidth = fillBar ? std::round(floatToPixel(Axis::Horizontal, x+barWidth)) - std::round(floatToPixel(Axis::Horizontal, x))-1 : 2; KDRect binRect(pxf, pyf, pixelBarWidth, std::round(floatToPixel(Axis::Vertical, 0.0f)) - pyf); if (floatToPixel(Axis::Vertical, 0.0f) < pyf) { binRect = KDRect(pxf, std::round(floatToPixel(Axis::Vertical, 0.0f)), pixelBarWidth+1, pyf - std::round(floatToPixel(Axis::Vertical, 0.0f))); } KDColor binColor = defaultColor; bool shouldColorBin = fillBar ? centerX >= highlightLowerBound && centerX <= highlightUpperBound : pxf >= floorf(pHighlightLowerBound) && pxf <= floorf(pHighlightUpperBound); if (shouldColorBin) { binColor = highlightColor; } ctx->fillRect(binRect, binColor); } } static bool pointInBoundingBox(float x1, float y1, float x2, float y2, float xC, float yC) { return ((x1 <= xC && xC <= x2) || (x2 <= xC && xC <= x1)) && ((y1 <= yC && yC <= y2) || (y2 <= yC && yC <= y1)); } void CurveView::joinDots(KDContext * ctx, KDRect rect, EvaluateXYForFloatParameter xyFloatEvaluation , void * model, void * context, bool drawStraightLinesEarly, float t, float x, float y, float s, float u, float v, KDColor color, bool thick, int maxNumberOfRecursion, EvaluateXYForDoubleParameter xyDoubleEvaluation) const { const bool isFirstDot = std::isnan(t); const bool isLeftDotValid = !( std::isnan(x) || std::isinf(x) || std::isnan(y) || std::isinf(y)); const bool isRightDotValid = !( std::isnan(u) || std::isinf(u) || std::isnan(v) || std::isinf(v)); float pxf = floatToPixel(Axis::Horizontal, x); float pyf = floatToPixel(Axis::Vertical, y); float puf = floatToPixel(Axis::Horizontal, u); float pvf = floatToPixel(Axis::Vertical, v); if (!isRightDotValid && !isLeftDotValid) { return; } KDCoordinate circleDiameter = thick ? thickCircleDiameter : thinCircleDiameter; if (isRightDotValid) { const float deltaX = pxf - puf; const float deltaY = pyf - pvf; if (isFirstDot // First dot has to be stamped || (!isLeftDotValid && maxNumberOfRecursion <= 0) // Last step of the recursion with an undefined left dot: we stamp the last right dot || (isLeftDotValid && deltaX*deltaX + deltaY*deltaY < circleDiameter * circleDiameter / 4.0f)) { // the dots are already close enough // the dots are already joined /* We need to be sure that the point is not an artifact caused by error * in float approximation. */ float pvd = xyDoubleEvaluation ? floatToPixel(Axis::Vertical, static_cast(xyDoubleEvaluation(u, model, context).x2())) : pvf; stampAtLocation(ctx, rect, puf, pvd, color, thick); return; } } // Middle point float ct = (t + s)/2.0f; Coordinate2D cxy = xyFloatEvaluation(ct, model, context); float cx = cxy.x1(); float cy = cxy.x2(); if ((drawStraightLinesEarly || maxNumberOfRecursion <= 0) && isRightDotValid && isLeftDotValid && pointInBoundingBox(x, y, u, v, cx, cy)) { /* As the middle dot is between the two dots, we assume that we * can draw a 'straight' line between the two */ constexpr float dangerousSlope = 1e6f; if (xyDoubleEvaluation && std::fabs((v-y) / (u-x)) > dangerousSlope) { /* We need to make sure we're not drawing a vertical asymptote because of * rounding errors. */ Coordinate2D xyD = xyDoubleEvaluation(static_cast(t), model, context); Coordinate2D uvD = xyDoubleEvaluation(static_cast(s), model, context); Coordinate2D cxyD = xyDoubleEvaluation(static_cast(ct), model, context); if (pointInBoundingBox(xyD.x1(), xyD.x2(), uvD.x1(), uvD.x2(), cxyD.x1(), cxyD.x2())) { straightJoinDots(ctx, rect, floatToPixel(Axis::Horizontal, xyD.x1()), floatToPixel(Axis::Vertical, xyD.x2()), floatToPixel(Axis::Horizontal, uvD.x1()), floatToPixel(Axis::Vertical, uvD.x2()), color, thick); return; } } else { straightJoinDots(ctx, rect, pxf, pyf, puf, pvf, color, thick); return; } } if (maxNumberOfRecursion > 0) { float xmin = min(Axis::Horizontal); float xmax = max(Axis::Horizontal); float ymax = max(Axis::Vertical); float ymin = min(Axis::Vertical); int nextMaxNumberOfRecursion = maxNumberOfRecursion - 1; // If both dots are out of rect bounds, and on a same side if ((xmax < x && xmax < u) || (x < xmin && u < xmin) || (ymax < y && ymax < v) || (y < ymin && v < ymin)) { /* Discard a recursion step to save computation time on dots that are * likely not to be drawn. It can alter precision with some functions when * zooming excessively (compared to plot range) on local minimums * For instance, plotting parametric function [t,|t-π|] with t in [0,360], * x in [-1,20] and y in [-1,3] will show inaccuracies that would * otherwise have been visible at higher zoom only, with x in [2,4] and y * in [-0.2,0.2] in this case. */ nextMaxNumberOfRecursion--; } joinDots(ctx, rect, xyFloatEvaluation, model, context, drawStraightLinesEarly, t, x, y, ct, cx, cy, color, thick, nextMaxNumberOfRecursion, xyDoubleEvaluation); joinDots(ctx, rect, xyFloatEvaluation, model, context, drawStraightLinesEarly, ct, cx, cy, s, u, v, color, thick, nextMaxNumberOfRecursion, xyDoubleEvaluation); } } static void clipBarycentricCoordinatesBetweenBounds(float & start, float & end, const KDCoordinate * bounds, const float p1f, const float p2f) { static constexpr int lower = 0; static constexpr int upper = 1; if (p1f == p2f) { if (p1f < bounds[lower] || bounds[upper] < p1f) { start = 1; end = 0; } } else { start = std::max(start, (bounds[(p1f > p2f) ? lower : upper] - p2f)/(p1f-p2f)); end = std::min( end , (bounds[(p1f > p2f) ? upper : lower] - p2f)/(p1f-p2f)); } } void CurveView::straightJoinDots(KDContext * ctx, KDRect rect, float pxf, float pyf, float puf, float pvf, KDColor color, bool thick) const { { /* Before drawing the line segment, clip it to rect: * start and end are the barycentric coordinates on the line segment (0 * corresponding to (u, v) and 1 to (x, y)), of the drawing start and end * points. */ float start = 0; float end = 1; KDCoordinate stampSize = thick ? thickStampSize : thinStampSize; const KDCoordinate xBounds[2] = { static_cast(rect.left() - stampSize), static_cast(rect.right() + stampSize) }; const KDCoordinate yBounds[2] = { static_cast(rect.top() - stampSize), static_cast(rect.bottom() + stampSize) }; clipBarycentricCoordinatesBetweenBounds(start, end, xBounds, pxf, puf); clipBarycentricCoordinatesBetweenBounds(start, end, yBounds, pyf, pvf); if (start > end) { return; } puf = start * pxf + (1-start) * puf; pvf = start * pyf + (1-start) * pvf; pxf = end * pxf + (1- end ) * puf; pyf = end * pyf + (1- end ) * pvf; } const float deltaX = pxf - puf; const float deltaY = pyf - pvf; KDCoordinate circleDiameter = thick ? thickCircleDiameter : thinCircleDiameter; const float normsRatio = std::sqrt(deltaX*deltaX + deltaY*deltaY) / (circleDiameter / 2.0f); const float stepX = deltaX / normsRatio ; const float stepY = deltaY / normsRatio; const int numberOfStamps = std::floor(normsRatio); for (int i = 0; i < numberOfStamps; i++) { stampAtLocation(ctx, rect, puf, pvf, color, thick); puf += stepX; pvf += stepY; } } void CurveView::stampAtLocation(KDContext * ctx, KDRect rect, float pxf, float pyf, KDColor color, bool thick) const { /* The (pxf, pyf) coordinates are not generally locating the center of a * pixel. We use stampMask, which is one pixel wider and higher than * stampSize, in order to cover stampRect without aligning the pixels. Then * shiftedMask is computed so that each pixel is the average of the values of * the four pixels of stampMask by which it is covered, proportionally to the * area of the intersection with each of those. * * In order to compute the coordinates (px, py) of the top-left pixel of * stampRect, we consider that stampMask is centered at the provided point * (pxf,pyf) which is then translated to the center of the top-left pixel of * stampMask. */ KDCoordinate stampSize = thick ? thickStampSize : thinStampSize; const uint8_t * stampMask = thick ? thickStampMask : thinStampMask; pxf -= (stampSize + 1 - 1)/2.0f; pyf -= (stampSize + 1 - 1)/2.0f; const KDCoordinate px = std::ceil(pxf); const KDCoordinate py = std::ceil(pyf); KDRect stampRect(px, py, stampSize, stampSize); if (!rect.intersects(stampRect)) { return; } uint8_t shiftedMask[stampSize][stampSize]; KDColor workingBuffer[stampSize*stampSize]; const float dx = px - pxf; const float dy = py - pyf; /* TODO: this could be optimized by precomputing 10 or 100 shifted masks. The * dx and dy would be rounded to one tenth or one hundredth to choose the * right shifted mask. */ const KDCoordinate stampMaskSize = stampSize + 1; for (int i=0; iblendRectWithMask(stampRect, color, (const uint8_t *)shiftedMask, workingBuffer); } void CurveView::layoutSubviews(bool force) { if (m_curveViewCursor != nullptr && m_cursorView != nullptr) { m_cursorView->setCursorFrame(cursorFrame(), force); } if (m_bannerView != nullptr) { m_bannerView->setFrame(bannerFrame(), force); } if (m_okView != nullptr) { m_okView->setFrame(okFrame(), force); } } KDRect CurveView::cursorFrame() { KDRect cursorFrame = KDRectZero; if (m_cursorView && m_mainViewSelected && std::isfinite(m_curveViewCursor->x()) && std::isfinite(m_curveViewCursor->y())) { KDSize cursorSize = m_cursorView->minimalSizeForOptimalDisplay(); float xCursorPixelPosition = std::round(floatToPixel(Axis::Horizontal, m_curveViewCursor->x())); float yCursorPixelPosition = std::round(floatToPixel(Axis::Vertical, m_curveViewCursor->y())); /* If the cursor is not visible, put its frame to zero, because it might be * very far out of the visible frame and thus later overflow KDCoordinate. * The "2" factor is a big safety margin. */ constexpr int maxCursorPixel = KDCOORDINATE_MAX / 2; // Assert we are not removing visible cursors static_assert((Ion::Display::Width * 2 < maxCursorPixel) && (Ion::Display::Height * 2 < maxCursorPixel), "maxCursorPixel is should be bigger"); if (std::abs(yCursorPixelPosition) > maxCursorPixel || std::abs(xCursorPixelPosition) > maxCursorPixel) { return KDRectZero; } KDCoordinate xCursor = xCursorPixelPosition; KDCoordinate yCursor = yCursorPixelPosition; KDCoordinate xCursorFrame = xCursor - (cursorSize.width()-1)/2; cursorFrame = KDRect(xCursorFrame, yCursor - (cursorSize.height()-1)/2, cursorSize); if (cursorSize.height() == 0) { KDCoordinate bannerHeight = (m_bannerView != nullptr) ? m_bannerView->minimalSizeForOptimalDisplay().height() : 0; cursorFrame = KDRect(xCursorFrame, 0, cursorSize.width(), bounds().height() - bannerHeight); } } return cursorFrame; } KDRect CurveView::bannerFrame() { KDRect bannerFrame = KDRectZero; if (bannerIsVisible()) { assert(bounds().width() == Ion::Display::Width); // Else the bannerHeight will not be properly computed KDCoordinate bannerHeight = m_bannerView->minimalSizeForOptimalDisplay().height(); bannerFrame = KDRect(0, bounds().height()- bannerHeight, bounds().width(), bannerHeight); } return bannerFrame; } KDRect CurveView::okFrame() { KDRect okFrame = KDRectZero; if (m_okView && (m_mainViewSelected || m_forceOkDisplay)) { KDCoordinate bannerHeight = 0; if (m_bannerView != nullptr) { bannerHeight = m_bannerView->minimalSizeForOptimalDisplay().height(); } KDSize okSize = m_okView->minimalSizeForOptimalDisplay(); okFrame = KDRect(bounds().width()- okSize.width()-k_okHorizontalMargin, bounds().height()- bannerHeight-okSize.height()-k_okVerticalMargin, okSize); } return okFrame; } int CurveView::numberOfSubviews() const { return (m_bannerView != nullptr) + (m_cursorView != nullptr) + (m_okView != nullptr); }; View * CurveView::subviewAtIndex(int index) { assert(index >= 0 && index < 3); /* If all subviews exist, we want Ok view to be the first child to avoid * redrawing it because it falls in the union of dirty rectangles linked to * the banner view and curve view */ if (index == 0) { if (m_okView != nullptr) { return m_okView; } else { if (m_cursorView != nullptr) { return m_cursorView; } } } if (index == 1 && m_cursorView != nullptr && m_okView != nullptr) { return m_cursorView; } return m_bannerView; } void CurveView::computeHorizontalExtremaLabels(bool increaseNumberOfSignificantDigits) { Axis axis = Axis::Horizontal; int axisLabelsCount = numberOfLabels(axis); float minA = min(axis); /* We want to draw the extrema labels (0 and numberOfLabels -1), but if they * might not be fully visible, draw the labels 1 and numberOfLabels - 2. */ bool skipExtremaLabels = (axisLabelsCount >= 4) && ((labelValueAtIndex(axis, 0) - minA)/(max(axis) - minA) < k_labelsHorizontalMarginRatio+FLT_EPSILON); int firstLabel = skipExtremaLabels ? 1 : 0; int lastLabel = axisLabelsCount - (skipExtremaLabels ? 2 : 1); assert(firstLabel != lastLabel); // All labels but the extrema are empty for (int i = 0; i < firstLabel; i++) { label(axis, i)[0] = 0; } for (int i = firstLabel + 1; i < lastLabel; i++) { label(axis, i)[0] = 0; } for (int i = lastLabel + 1; i < axisLabelsCount; i++) { label(axis, i)[0] = 0; } int minMax[] = {firstLabel, lastLabel}; for (int i : minMax) { // Compute the minimal and maximal label PrintFloat::ConvertFloatToText( labelValueAtIndex(axis, i), label(axis, i), k_labelBufferMaxSize, labelMaxGlyphLengthSize(), increaseNumberOfSignificantDigits ? k_bigNumberSignificantDigits : k_numberSignificantDigits, Preferences::PrintFloatMode::Decimal); } } float CurveView::labelValueAtIndex(Axis axis, int i) const { assert(i >= 0 && i < numberOfLabels(axis)); float labelStep = 2.0f * gridUnit(axis); return labelStep*(std::ceil(min(axis)/labelStep)+i); } bool CurveView::bannerIsVisible() const { return m_bannerView && m_mainViewSelected; } }