/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { // tests that some coordinates aren't NaNs #define JUCE_CHECK_COORDS_ARE_VALID(x, y) \ jassert (x == x && y == y); //============================================================================== namespace PathHelpers { const float ellipseAngularIncrement = 0.05f; static String nextToken (String::CharPointerType& t) { t.incrementToEndOfWhitespace(); auto start = t; size_t numChars = 0; while (! (t.isEmpty() || t.isWhitespace())) { ++t; ++numChars; } return { start, numChars }; } inline double lengthOf (float x1, float y1, float x2, float y2) noexcept { return juce_hypot ((double) (x1 - x2), (double) (y1 - y2)); } } //============================================================================== const float Path::lineMarker = 100001.0f; const float Path::moveMarker = 100002.0f; const float Path::quadMarker = 100003.0f; const float Path::cubicMarker = 100004.0f; const float Path::closeSubPathMarker = 100005.0f; const float Path::defaultToleranceForTesting = 1.0f; const float Path::defaultToleranceForMeasurement = 0.6f; static bool isMarker (float value, float marker) noexcept { return value == marker; } //============================================================================== Path::PathBounds::PathBounds() noexcept { } Rectangle Path::PathBounds::getRectangle() const noexcept { return { pathXMin, pathYMin, pathXMax - pathXMin, pathYMax - pathYMin }; } void Path::PathBounds::reset() noexcept { pathXMin = pathYMin = pathYMax = pathXMax = 0; } void Path::PathBounds::reset (float x, float y) noexcept { pathXMin = pathXMax = x; pathYMin = pathYMax = y; } void Path::PathBounds::extend (float x, float y) noexcept { if (x < pathXMin) pathXMin = x; else if (x > pathXMax) pathXMax = x; if (y < pathYMin) pathYMin = y; else if (y > pathYMax) pathYMax = y; } //============================================================================== Path::Path() { } Path::~Path() { } Path::Path (const Path& other) : data (other.data), bounds (other.bounds), useNonZeroWinding (other.useNonZeroWinding) { } Path& Path::operator= (const Path& other) { if (this != &other) { data = other.data; bounds = other.bounds; useNonZeroWinding = other.useNonZeroWinding; } return *this; } Path::Path (Path&& other) noexcept : data (std::move (other.data)), bounds (other.bounds), useNonZeroWinding (other.useNonZeroWinding) { } Path& Path::operator= (Path&& other) noexcept { data = std::move (other.data); bounds = other.bounds; useNonZeroWinding = other.useNonZeroWinding; return *this; } bool Path::operator== (const Path& other) const noexcept { return useNonZeroWinding == other.useNonZeroWinding && data == other.data; } bool Path::operator!= (const Path& other) const noexcept { return ! operator== (other); } void Path::clear() noexcept { data.clearQuick(); bounds.reset(); } void Path::swapWithPath (Path& other) noexcept { data.swapWith (other.data); std::swap (bounds.pathXMin, other.bounds.pathXMin); std::swap (bounds.pathXMax, other.bounds.pathXMax); std::swap (bounds.pathYMin, other.bounds.pathYMin); std::swap (bounds.pathYMax, other.bounds.pathYMax); std::swap (useNonZeroWinding, other.useNonZeroWinding); } //============================================================================== void Path::setUsingNonZeroWinding (const bool isNonZero) noexcept { useNonZeroWinding = isNonZero; } void Path::scaleToFit (float x, float y, float w, float h, bool preserveProportions) noexcept { applyTransform (getTransformToScaleToFit (x, y, w, h, preserveProportions)); } //============================================================================== bool Path::isEmpty() const noexcept { for (auto i = data.begin(), e = data.end(); i != e; ++i) { auto type = *i; if (isMarker (type, moveMarker)) { i += 2; } else if (isMarker (type, lineMarker) || isMarker (type, quadMarker) || isMarker (type, cubicMarker)) { return false; } } return true; } Rectangle Path::getBounds() const noexcept { return bounds.getRectangle(); } Rectangle Path::getBoundsTransformed (const AffineTransform& transform) const noexcept { return getBounds().transformedBy (transform); } //============================================================================== void Path::preallocateSpace (int numExtraCoordsToMakeSpaceFor) { data.ensureStorageAllocated (data.size() + numExtraCoordsToMakeSpaceFor); } void Path::startNewSubPath (const float x, const float y) { JUCE_CHECK_COORDS_ARE_VALID (x, y) if (data.isEmpty()) bounds.reset (x, y); else bounds.extend (x, y); data.add (moveMarker, x, y); } void Path::startNewSubPath (Point start) { startNewSubPath (start.x, start.y); } void Path::lineTo (const float x, const float y) { JUCE_CHECK_COORDS_ARE_VALID (x, y) if (data.isEmpty()) startNewSubPath (0, 0); data.add (lineMarker, x, y); bounds.extend (x, y); } void Path::lineTo (Point end) { lineTo (end.x, end.y); } void Path::quadraticTo (const float x1, const float y1, const float x2, const float y2) { JUCE_CHECK_COORDS_ARE_VALID (x1, y1) JUCE_CHECK_COORDS_ARE_VALID (x2, y2) if (data.isEmpty()) startNewSubPath (0, 0); data.add (quadMarker, x1, y1, x2, y2); bounds.extend (x1, y1, x2, y2); } void Path::quadraticTo (Point controlPoint, Point endPoint) { quadraticTo (controlPoint.x, controlPoint.y, endPoint.x, endPoint.y); } void Path::cubicTo (const float x1, const float y1, const float x2, const float y2, const float x3, const float y3) { JUCE_CHECK_COORDS_ARE_VALID (x1, y1) JUCE_CHECK_COORDS_ARE_VALID (x2, y2) JUCE_CHECK_COORDS_ARE_VALID (x3, y3) if (data.isEmpty()) startNewSubPath (0, 0); data.add (cubicMarker, x1, y1, x2, y2, x3, y3); bounds.extend (x1, y1, x2, y2, x3, y3); } void Path::cubicTo (Point controlPoint1, Point controlPoint2, Point endPoint) { cubicTo (controlPoint1.x, controlPoint1.y, controlPoint2.x, controlPoint2.y, endPoint.x, endPoint.y); } void Path::closeSubPath() { if (! (data.isEmpty() || isMarker (data.getLast(), closeSubPathMarker))) data.add (closeSubPathMarker); } Point Path::getCurrentPosition() const { if (data.isEmpty()) return {}; auto* i = data.end() - 1; if (isMarker (*i, closeSubPathMarker)) { while (i != data.begin()) { if (isMarker (*--i, moveMarker)) { i += 2; break; } } } if (i != data.begin()) return { *(i - 1), *i }; return {}; } void Path::addRectangle (float x, float y, float w, float h) { auto x1 = x, y1 = y, x2 = x + w, y2 = y + h; if (w < 0) std::swap (x1, x2); if (h < 0) std::swap (y1, y2); if (data.isEmpty()) { bounds.pathXMin = x1; bounds.pathXMax = x2; bounds.pathYMin = y1; bounds.pathYMax = y2; } else { bounds.pathXMin = jmin (bounds.pathXMin, x1); bounds.pathXMax = jmax (bounds.pathXMax, x2); bounds.pathYMin = jmin (bounds.pathYMin, y1); bounds.pathYMax = jmax (bounds.pathYMax, y2); } data.add (moveMarker, x1, y2, lineMarker, x1, y1, lineMarker, x2, y1, lineMarker, x2, y2, closeSubPathMarker); } void Path::addRoundedRectangle (float x, float y, float w, float h, float csx, float csy) { addRoundedRectangle (x, y, w, h, csx, csy, true, true, true, true); } void Path::addRoundedRectangle (const float x, const float y, const float w, const float h, float csx, float csy, const bool curveTopLeft, const bool curveTopRight, const bool curveBottomLeft, const bool curveBottomRight) { csx = jmin (csx, w * 0.5f); csy = jmin (csy, h * 0.5f); auto cs45x = csx * 0.45f; auto cs45y = csy * 0.45f; auto x2 = x + w; auto y2 = y + h; if (curveTopLeft) { startNewSubPath (x, y + csy); cubicTo (x, y + cs45y, x + cs45x, y, x + csx, y); } else { startNewSubPath (x, y); } if (curveTopRight) { lineTo (x2 - csx, y); cubicTo (x2 - cs45x, y, x2, y + cs45y, x2, y + csy); } else { lineTo (x2, y); } if (curveBottomRight) { lineTo (x2, y2 - csy); cubicTo (x2, y2 - cs45y, x2 - cs45x, y2, x2 - csx, y2); } else { lineTo (x2, y2); } if (curveBottomLeft) { lineTo (x + csx, y2); cubicTo (x + cs45x, y2, x, y2 - cs45y, x, y2 - csy); } else { lineTo (x, y2); } closeSubPath(); } void Path::addRoundedRectangle (float x, float y, float w, float h, float cs) { addRoundedRectangle (x, y, w, h, cs, cs); } void Path::addTriangle (float x1, float y1, float x2, float y2, float x3, float y3) { addTriangle ({ x1, y1 }, { x2, y2 }, { x3, y3 }); } void Path::addTriangle (Point p1, Point p2, Point p3) { startNewSubPath (p1); lineTo (p2); lineTo (p3); closeSubPath(); } void Path::addQuadrilateral (float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4) { startNewSubPath (x1, y1); lineTo (x2, y2); lineTo (x3, y3); lineTo (x4, y4); closeSubPath(); } void Path::addEllipse (float x, float y, float w, float h) { addEllipse ({ x, y, w, h }); } void Path::addEllipse (Rectangle area) { auto hw = area.getWidth() * 0.5f; auto hw55 = hw * 0.55f; auto hh = area.getHeight() * 0.5f; auto hh55 = hh * 0.55f; auto cx = area.getX() + hw; auto cy = area.getY() + hh; startNewSubPath (cx, cy - hh); cubicTo (cx + hw55, cy - hh, cx + hw, cy - hh55, cx + hw, cy); cubicTo (cx + hw, cy + hh55, cx + hw55, cy + hh, cx, cy + hh); cubicTo (cx - hw55, cy + hh, cx - hw, cy + hh55, cx - hw, cy); cubicTo (cx - hw, cy - hh55, cx - hw55, cy - hh, cx, cy - hh); closeSubPath(); } void Path::addArc (float x, float y, float w, float h, float fromRadians, float toRadians, bool startAsNewSubPath) { auto radiusX = w / 2.0f; auto radiusY = h / 2.0f; addCentredArc (x + radiusX, y + radiusY, radiusX, radiusY, 0.0f, fromRadians, toRadians, startAsNewSubPath); } void Path::addCentredArc (float centreX, float centreY, float radiusX, float radiusY, float rotationOfEllipse, float fromRadians, float toRadians, bool startAsNewSubPath) { if (radiusX > 0.0f && radiusY > 0.0f) { Point centre (centreX, centreY); auto rotation = AffineTransform::rotation (rotationOfEllipse, centreX, centreY); auto angle = fromRadians; if (startAsNewSubPath) startNewSubPath (centre.getPointOnCircumference (radiusX, radiusY, angle).transformedBy (rotation)); if (fromRadians < toRadians) { if (startAsNewSubPath) angle += PathHelpers::ellipseAngularIncrement; while (angle < toRadians) { lineTo (centre.getPointOnCircumference (radiusX, radiusY, angle).transformedBy (rotation)); angle += PathHelpers::ellipseAngularIncrement; } } else { if (startAsNewSubPath) angle -= PathHelpers::ellipseAngularIncrement; while (angle > toRadians) { lineTo (centre.getPointOnCircumference (radiusX, radiusY, angle).transformedBy (rotation)); angle -= PathHelpers::ellipseAngularIncrement; } } lineTo (centre.getPointOnCircumference (radiusX, radiusY, toRadians).transformedBy (rotation)); } } void Path::addPieSegment (float x, float y, float width, float height, float fromRadians, float toRadians, float innerCircleProportionalSize) { auto radiusX = width * 0.5f; auto radiusY = height * 0.5f; Point centre (x + radiusX, y + radiusY); startNewSubPath (centre.getPointOnCircumference (radiusX, radiusY, fromRadians)); addArc (x, y, width, height, fromRadians, toRadians); if (std::abs (fromRadians - toRadians) > MathConstants::pi * 1.999f) { closeSubPath(); if (innerCircleProportionalSize > 0) { radiusX *= innerCircleProportionalSize; radiusY *= innerCircleProportionalSize; startNewSubPath (centre.getPointOnCircumference (radiusX, radiusY, toRadians)); addArc (centre.x - radiusX, centre.y - radiusY, radiusX * 2.0f, radiusY * 2.0f, toRadians, fromRadians); } } else { if (innerCircleProportionalSize > 0) { radiusX *= innerCircleProportionalSize; radiusY *= innerCircleProportionalSize; addArc (centre.x - radiusX, centre.y - radiusY, radiusX * 2.0f, radiusY * 2.0f, toRadians, fromRadians); } else { lineTo (centre); } } closeSubPath(); } void Path::addPieSegment (Rectangle segmentBounds, float fromRadians, float toRadians, float innerCircleProportionalSize) { addPieSegment (segmentBounds.getX(), segmentBounds.getY(), segmentBounds.getWidth(), segmentBounds.getHeight(), fromRadians, toRadians, innerCircleProportionalSize); } //============================================================================== void Path::addLineSegment (Line line, float lineThickness) { auto reversed = line.reversed(); lineThickness *= 0.5f; startNewSubPath (line.getPointAlongLine (0, lineThickness)); lineTo (line.getPointAlongLine (0, -lineThickness)); lineTo (reversed.getPointAlongLine (0, lineThickness)); lineTo (reversed.getPointAlongLine (0, -lineThickness)); closeSubPath(); } void Path::addArrow (Line line, float lineThickness, float arrowheadWidth, float arrowheadLength) { auto reversed = line.reversed(); lineThickness *= 0.5f; arrowheadWidth *= 0.5f; arrowheadLength = jmin (arrowheadLength, 0.8f * line.getLength()); startNewSubPath (line.getPointAlongLine (0, lineThickness)); lineTo (line.getPointAlongLine (0, -lineThickness)); lineTo (reversed.getPointAlongLine (arrowheadLength, lineThickness)); lineTo (reversed.getPointAlongLine (arrowheadLength, arrowheadWidth)); lineTo (line.getEnd()); lineTo (reversed.getPointAlongLine (arrowheadLength, -arrowheadWidth)); lineTo (reversed.getPointAlongLine (arrowheadLength, -lineThickness)); closeSubPath(); } void Path::addPolygon (Point centre, int numberOfSides, float radius, float startAngle) { jassert (numberOfSides > 1); // this would be silly. if (numberOfSides > 1) { auto angleBetweenPoints = MathConstants::twoPi / (float) numberOfSides; for (int i = 0; i < numberOfSides; ++i) { auto angle = startAngle + (float) i * angleBetweenPoints; auto p = centre.getPointOnCircumference (radius, angle); if (i == 0) startNewSubPath (p); else lineTo (p); } closeSubPath(); } } void Path::addStar (Point centre, int numberOfPoints, float innerRadius, float outerRadius, float startAngle) { jassert (numberOfPoints > 1); // this would be silly. if (numberOfPoints > 1) { auto angleBetweenPoints = MathConstants::twoPi / (float) numberOfPoints; for (int i = 0; i < numberOfPoints; ++i) { auto angle = startAngle + (float) i * angleBetweenPoints; auto p = centre.getPointOnCircumference (outerRadius, angle); if (i == 0) startNewSubPath (p); else lineTo (p); lineTo (centre.getPointOnCircumference (innerRadius, angle + angleBetweenPoints * 0.5f)); } closeSubPath(); } } void Path::addBubble (Rectangle bodyArea, Rectangle maximumArea, Point arrowTip, float cornerSize, float arrowBaseWidth) { auto halfW = bodyArea.getWidth() / 2.0f; auto halfH = bodyArea.getHeight() / 2.0f; auto cornerSizeW = jmin (cornerSize, halfW); auto cornerSizeH = jmin (cornerSize, halfH); auto cornerSizeW2 = 2.0f * cornerSizeW; auto cornerSizeH2 = 2.0f * cornerSizeH; startNewSubPath (bodyArea.getX() + cornerSizeW, bodyArea.getY()); auto targetLimit = bodyArea.reduced (jmin (halfW - 1.0f, cornerSizeW + arrowBaseWidth), jmin (halfH - 1.0f, cornerSizeH + arrowBaseWidth)); if (Rectangle (targetLimit.getX(), maximumArea.getY(), targetLimit.getWidth(), bodyArea.getY() - maximumArea.getY()).contains (arrowTip)) { lineTo (arrowTip.x - arrowBaseWidth, bodyArea.getY()); lineTo (arrowTip.x, arrowTip.y); lineTo (arrowTip.x + arrowBaseWidth, bodyArea.getY()); } lineTo (bodyArea.getRight() - cornerSizeW, bodyArea.getY()); addArc (bodyArea.getRight() - cornerSizeW2, bodyArea.getY(), cornerSizeW2, cornerSizeH2, 0, MathConstants::halfPi); if (Rectangle (bodyArea.getRight(), targetLimit.getY(), maximumArea.getRight() - bodyArea.getRight(), targetLimit.getHeight()).contains (arrowTip)) { lineTo (bodyArea.getRight(), arrowTip.y - arrowBaseWidth); lineTo (arrowTip.x, arrowTip.y); lineTo (bodyArea.getRight(), arrowTip.y + arrowBaseWidth); } lineTo (bodyArea.getRight(), bodyArea.getBottom() - cornerSizeH); addArc (bodyArea.getRight() - cornerSizeW2, bodyArea.getBottom() - cornerSizeH2, cornerSizeW2, cornerSizeH2, MathConstants::halfPi, MathConstants::pi); if (Rectangle (targetLimit.getX(), bodyArea.getBottom(), targetLimit.getWidth(), maximumArea.getBottom() - bodyArea.getBottom()).contains (arrowTip)) { lineTo (arrowTip.x + arrowBaseWidth, bodyArea.getBottom()); lineTo (arrowTip.x, arrowTip.y); lineTo (arrowTip.x - arrowBaseWidth, bodyArea.getBottom()); } lineTo (bodyArea.getX() + cornerSizeW, bodyArea.getBottom()); addArc (bodyArea.getX(), bodyArea.getBottom() - cornerSizeH2, cornerSizeW2, cornerSizeH2, MathConstants::pi, MathConstants::pi * 1.5f); if (Rectangle (maximumArea.getX(), targetLimit.getY(), bodyArea.getX() - maximumArea.getX(), targetLimit.getHeight()).contains (arrowTip)) { lineTo (bodyArea.getX(), arrowTip.y + arrowBaseWidth); lineTo (arrowTip.x, arrowTip.y); lineTo (bodyArea.getX(), arrowTip.y - arrowBaseWidth); } lineTo (bodyArea.getX(), bodyArea.getY() + cornerSizeH); addArc (bodyArea.getX(), bodyArea.getY(), cornerSizeW2, cornerSizeH2, MathConstants::pi * 1.5f, MathConstants::twoPi - 0.05f); closeSubPath(); } void Path::addPath (const Path& other) { const auto* d = other.data.begin(); for (int i = 0; i < other.data.size();) { auto type = d[i++]; if (isMarker (type, moveMarker)) { startNewSubPath (d[i], d[i + 1]); i += 2; } else if (isMarker (type, lineMarker)) { lineTo (d[i], d[i + 1]); i += 2; } else if (isMarker (type, quadMarker)) { quadraticTo (d[i], d[i + 1], d[i + 2], d[i + 3]); i += 4; } else if (isMarker (type, cubicMarker)) { cubicTo (d[i], d[i + 1], d[i + 2], d[i + 3], d[i + 4], d[i + 5]); i += 6; } else if (isMarker (type, closeSubPathMarker)) { closeSubPath(); } else { // something's gone wrong with the element list! jassertfalse; } } } void Path::addPath (const Path& other, const AffineTransform& transformToApply) { const auto* d = other.data.begin(); for (int i = 0; i < other.data.size();) { auto type = d[i++]; if (isMarker (type, closeSubPathMarker)) { closeSubPath(); } else { auto x = d[i++]; auto y = d[i++]; transformToApply.transformPoint (x, y); if (isMarker (type, moveMarker)) { startNewSubPath (x, y); } else if (isMarker (type, lineMarker)) { lineTo (x, y); } else if (isMarker (type, quadMarker)) { auto x2 = d[i++]; auto y2 = d[i++]; transformToApply.transformPoint (x2, y2); quadraticTo (x, y, x2, y2); } else if (isMarker (type, cubicMarker)) { auto x2 = d[i++]; auto y2 = d[i++]; auto x3 = d[i++]; auto y3 = d[i++]; transformToApply.transformPoints (x2, y2, x3, y3); cubicTo (x, y, x2, y2, x3, y3); } else { // something's gone wrong with the element list! jassertfalse; } } } } //============================================================================== void Path::applyTransform (const AffineTransform& transform) noexcept { bounds.reset(); bool firstPoint = true; float* d = data.begin(); auto* end = data.end(); while (d < end) { auto type = *d++; if (isMarker (type, moveMarker)) { transform.transformPoint (d[0], d[1]); JUCE_CHECK_COORDS_ARE_VALID (d[0], d[1]) if (firstPoint) { firstPoint = false; bounds.reset (d[0], d[1]); } else { bounds.extend (d[0], d[1]); } d += 2; } else if (isMarker (type, lineMarker)) { transform.transformPoint (d[0], d[1]); JUCE_CHECK_COORDS_ARE_VALID (d[0], d[1]) bounds.extend (d[0], d[1]); d += 2; } else if (isMarker (type, quadMarker)) { transform.transformPoints (d[0], d[1], d[2], d[3]); JUCE_CHECK_COORDS_ARE_VALID (d[0], d[1]) JUCE_CHECK_COORDS_ARE_VALID (d[2], d[3]) bounds.extend (d[0], d[1], d[2], d[3]); d += 4; } else if (isMarker (type, cubicMarker)) { transform.transformPoints (d[0], d[1], d[2], d[3], d[4], d[5]); JUCE_CHECK_COORDS_ARE_VALID (d[0], d[1]) JUCE_CHECK_COORDS_ARE_VALID (d[2], d[3]) JUCE_CHECK_COORDS_ARE_VALID (d[4], d[5]) bounds.extend (d[0], d[1], d[2], d[3], d[4], d[5]); d += 6; } } } //============================================================================== AffineTransform Path::getTransformToScaleToFit (Rectangle area, bool preserveProportions, Justification justification) const { return getTransformToScaleToFit (area.getX(), area.getY(), area.getWidth(), area.getHeight(), preserveProportions, justification); } AffineTransform Path::getTransformToScaleToFit (float x, float y, float w, float h, bool preserveProportions, Justification justification) const { auto boundsRect = getBounds(); if (preserveProportions) { if (w <= 0 || h <= 0 || boundsRect.isEmpty()) return AffineTransform(); float newW, newH; auto srcRatio = boundsRect.getHeight() / boundsRect.getWidth(); if (srcRatio > h / w) { newW = h / srcRatio; newH = h; } else { newW = w; newH = w * srcRatio; } auto newXCentre = x; auto newYCentre = y; if (justification.testFlags (Justification::left)) newXCentre += newW * 0.5f; else if (justification.testFlags (Justification::right)) newXCentre += w - newW * 0.5f; else newXCentre += w * 0.5f; if (justification.testFlags (Justification::top)) newYCentre += newH * 0.5f; else if (justification.testFlags (Justification::bottom)) newYCentre += h - newH * 0.5f; else newYCentre += h * 0.5f; return AffineTransform::translation (boundsRect.getWidth() * -0.5f - boundsRect.getX(), boundsRect.getHeight() * -0.5f - boundsRect.getY()) .scaled (newW / boundsRect.getWidth(), newH / boundsRect.getHeight()) .translated (newXCentre, newYCentre); } else { return AffineTransform::translation (-boundsRect.getX(), -boundsRect.getY()) .scaled (w / boundsRect.getWidth(), h / boundsRect.getHeight()) .translated (x, y); } } //============================================================================== bool Path::contains (float x, float y, float tolerance) const { if (x <= bounds.pathXMin || x >= bounds.pathXMax || y <= bounds.pathYMin || y >= bounds.pathYMax) return false; PathFlatteningIterator i (*this, AffineTransform(), tolerance); int positiveCrossings = 0; int negativeCrossings = 0; while (i.next()) { if ((i.y1 <= y && i.y2 > y) || (i.y2 <= y && i.y1 > y)) { auto intersectX = i.x1 + (i.x2 - i.x1) * (y - i.y1) / (i.y2 - i.y1); if (intersectX <= x) { if (i.y1 < i.y2) ++positiveCrossings; else ++negativeCrossings; } } } return useNonZeroWinding ? (negativeCrossings != positiveCrossings) : ((negativeCrossings + positiveCrossings) & 1) != 0; } bool Path::contains (Point point, float tolerance) const { return contains (point.x, point.y, tolerance); } bool Path::intersectsLine (Line line, float tolerance) const { PathFlatteningIterator i (*this, AffineTransform(), tolerance); Point intersection; while (i.next()) if (line.intersects (Line (i.x1, i.y1, i.x2, i.y2), intersection)) return true; return false; } Line Path::getClippedLine (Line line, bool keepSectionOutsidePath) const { Line result (line); const bool startInside = contains (line.getStart()); const bool endInside = contains (line.getEnd()); if (startInside == endInside) { if (keepSectionOutsidePath == startInside) result = Line(); } else { PathFlatteningIterator i (*this, AffineTransform()); Point intersection; while (i.next()) { if (line.intersects ({ i.x1, i.y1, i.x2, i.y2 }, intersection)) { if ((startInside && keepSectionOutsidePath) || (endInside && ! keepSectionOutsidePath)) result.setStart (intersection); else result.setEnd (intersection); } } } return result; } float Path::getLength (const AffineTransform& transform, float tolerance) const { float length = 0; PathFlatteningIterator i (*this, transform, tolerance); while (i.next()) length += Line (i.x1, i.y1, i.x2, i.y2).getLength(); return length; } Point Path::getPointAlongPath (float distanceFromStart, const AffineTransform& transform, float tolerance) const { PathFlatteningIterator i (*this, transform, tolerance); while (i.next()) { const Line line (i.x1, i.y1, i.x2, i.y2); auto lineLength = line.getLength(); if (distanceFromStart <= lineLength) return line.getPointAlongLine (distanceFromStart); distanceFromStart -= lineLength; } return { i.x2, i.y2 }; } float Path::getNearestPoint (Point targetPoint, Point& pointOnPath, const AffineTransform& transform, float tolerance) const { PathFlatteningIterator i (*this, transform, tolerance); float bestPosition = 0, bestDistance = std::numeric_limits::max(); float length = 0; Point pointOnLine; while (i.next()) { const Line line (i.x1, i.y1, i.x2, i.y2); auto distance = line.getDistanceFromPoint (targetPoint, pointOnLine); if (distance < bestDistance) { bestDistance = distance; bestPosition = length + pointOnLine.getDistanceFrom (line.getStart()); pointOnPath = pointOnLine; } length += line.getLength(); } return bestPosition; } //============================================================================== Path Path::createPathWithRoundedCorners (const float cornerRadius) const { if (cornerRadius <= 0.01f) return *this; Path p; int n = 0, indexOfPathStart = 0, indexOfPathStartThis = 0; auto* elements = data.begin(); bool lastWasLine = false, firstWasLine = false; while (n < data.size()) { auto type = elements[n++]; if (isMarker (type, moveMarker)) { indexOfPathStart = p.data.size(); indexOfPathStartThis = n - 1; auto x = elements[n++]; auto y = elements[n++]; p.startNewSubPath (x, y); lastWasLine = false; firstWasLine = (isMarker (elements[n], lineMarker)); } else if (isMarker (type, lineMarker) || isMarker (type, closeSubPathMarker)) { float startX = 0, startY = 0, joinX = 0, joinY = 0, endX, endY; if (isMarker (type, lineMarker)) { endX = elements[n++]; endY = elements[n++]; if (n > 8) { startX = elements[n - 8]; startY = elements[n - 7]; joinX = elements[n - 5]; joinY = elements[n - 4]; } } else { endX = elements[indexOfPathStartThis + 1]; endY = elements[indexOfPathStartThis + 2]; if (n > 6) { startX = elements[n - 6]; startY = elements[n - 5]; joinX = elements[n - 3]; joinY = elements[n - 2]; } } if (lastWasLine) { auto len1 = PathHelpers::lengthOf (startX, startY, joinX, joinY); if (len1 > 0) { auto propNeeded = jmin (0.5, cornerRadius / len1); *(p.data.end() - 2) = (float) (joinX - (joinX - startX) * propNeeded); *(p.data.end() - 1) = (float) (joinY - (joinY - startY) * propNeeded); } auto len2 = PathHelpers::lengthOf (endX, endY, joinX, joinY); if (len2 > 0) { auto propNeeded = jmin (0.5, cornerRadius / len2); p.quadraticTo (joinX, joinY, (float) (joinX + (endX - joinX) * propNeeded), (float) (joinY + (endY - joinY) * propNeeded)); } p.lineTo (endX, endY); } else if (isMarker (type, lineMarker)) { p.lineTo (endX, endY); lastWasLine = true; } if (isMarker (type, closeSubPathMarker)) { if (firstWasLine) { startX = elements[n - 3]; startY = elements[n - 2]; joinX = endX; joinY = endY; endX = elements[indexOfPathStartThis + 4]; endY = elements[indexOfPathStartThis + 5]; auto len1 = PathHelpers::lengthOf (startX, startY, joinX, joinY); if (len1 > 0) { auto propNeeded = jmin (0.5, cornerRadius / len1); *(p.data.end() - 2) = (float) (joinX - (joinX - startX) * propNeeded); *(p.data.end() - 1) = (float) (joinY - (joinY - startY) * propNeeded); } auto len2 = PathHelpers::lengthOf (endX, endY, joinX, joinY); if (len2 > 0) { auto propNeeded = jmin (0.5, cornerRadius / len2); endX = (float) (joinX + (endX - joinX) * propNeeded); endY = (float) (joinY + (endY - joinY) * propNeeded); p.quadraticTo (joinX, joinY, endX, endY); p.data.begin()[indexOfPathStart + 1] = endX; p.data.begin()[indexOfPathStart + 2] = endY; } } p.closeSubPath(); } } else if (isMarker (type, quadMarker)) { lastWasLine = false; auto x1 = elements[n++]; auto y1 = elements[n++]; auto x2 = elements[n++]; auto y2 = elements[n++]; p.quadraticTo (x1, y1, x2, y2); } else if (isMarker (type, cubicMarker)) { lastWasLine = false; auto x1 = elements[n++]; auto y1 = elements[n++]; auto x2 = elements[n++]; auto y2 = elements[n++]; auto x3 = elements[n++]; auto y3 = elements[n++]; p.cubicTo (x1, y1, x2, y2, x3, y3); } } return p; } //============================================================================== void Path::loadPathFromStream (InputStream& source) { while (! source.isExhausted()) { switch (source.readByte()) { case 'm': { auto x = source.readFloat(); auto y = source.readFloat(); startNewSubPath (x, y); break; } case 'l': { auto x = source.readFloat(); auto y = source.readFloat(); lineTo (x, y); break; } case 'q': { auto x1 = source.readFloat(); auto y1 = source.readFloat(); auto x2 = source.readFloat(); auto y2 = source.readFloat(); quadraticTo (x1, y1, x2, y2); break; } case 'b': { auto x1 = source.readFloat(); auto y1 = source.readFloat(); auto x2 = source.readFloat(); auto y2 = source.readFloat(); auto x3 = source.readFloat(); auto y3 = source.readFloat(); cubicTo (x1, y1, x2, y2, x3, y3); break; } case 'c': closeSubPath(); break; case 'n': useNonZeroWinding = true; break; case 'z': useNonZeroWinding = false; break; case 'e': return; // end of path marker default: jassertfalse; // illegal char in the stream break; } } } void Path::loadPathFromData (const void* const pathData, const size_t numberOfBytes) { MemoryInputStream in (pathData, numberOfBytes, false); loadPathFromStream (in); } void Path::writePathToStream (OutputStream& dest) const { dest.writeByte (useNonZeroWinding ? 'n' : 'z'); for (auto* i = data.begin(); i != data.end();) { auto type = *i++; if (isMarker (type, moveMarker)) { dest.writeByte ('m'); dest.writeFloat (*i++); dest.writeFloat (*i++); } else if (isMarker (type, lineMarker)) { dest.writeByte ('l'); dest.writeFloat (*i++); dest.writeFloat (*i++); } else if (isMarker (type, quadMarker)) { dest.writeByte ('q'); dest.writeFloat (*i++); dest.writeFloat (*i++); dest.writeFloat (*i++); dest.writeFloat (*i++); } else if (isMarker (type, cubicMarker)) { dest.writeByte ('b'); dest.writeFloat (*i++); dest.writeFloat (*i++); dest.writeFloat (*i++); dest.writeFloat (*i++); dest.writeFloat (*i++); dest.writeFloat (*i++); } else if (isMarker (type, closeSubPathMarker)) { dest.writeByte ('c'); } } dest.writeByte ('e'); // marks the end-of-path } String Path::toString() const { MemoryOutputStream s (2048); if (! useNonZeroWinding) s << 'a'; float lastMarker = 0.0f; for (int i = 0; i < data.size();) { auto type = data.begin()[i++]; char markerChar = 0; int numCoords = 0; if (isMarker (type, moveMarker)) { markerChar = 'm'; numCoords = 2; } else if (isMarker (type, lineMarker)) { markerChar = 'l'; numCoords = 2; } else if (isMarker (type, quadMarker)) { markerChar = 'q'; numCoords = 4; } else if (isMarker (type, cubicMarker)) { markerChar = 'c'; numCoords = 6; } else { jassert (isMarker (type, closeSubPathMarker)); markerChar = 'z'; } if (! isMarker (type, lastMarker)) { if (s.getDataSize() != 0) s << ' '; s << markerChar; lastMarker = type; } while (--numCoords >= 0 && i < data.size()) { String coord (data.begin()[i++], 3); while (coord.endsWithChar ('0') && coord != "0") coord = coord.dropLastCharacters (1); if (coord.endsWithChar ('.')) coord = coord.dropLastCharacters (1); if (s.getDataSize() != 0) s << ' '; s << coord; } } return s.toUTF8(); } void Path::restoreFromString (StringRef stringVersion) { clear(); setUsingNonZeroWinding (true); auto t = stringVersion.text; juce_wchar marker = 'm'; int numValues = 2; float values[6]; for (;;) { auto token = PathHelpers::nextToken (t); auto firstChar = token[0]; int startNum = 0; if (firstChar == 0) break; if (firstChar == 'm' || firstChar == 'l') { marker = firstChar; numValues = 2; } else if (firstChar == 'q') { marker = firstChar; numValues = 4; } else if (firstChar == 'c') { marker = firstChar; numValues = 6; } else if (firstChar == 'z') { marker = firstChar; numValues = 0; } else if (firstChar == 'a') { setUsingNonZeroWinding (false); continue; } else { ++startNum; values [0] = token.getFloatValue(); } for (int i = startNum; i < numValues; ++i) values [i] = PathHelpers::nextToken (t).getFloatValue(); switch (marker) { case 'm': startNewSubPath (values[0], values[1]); break; case 'l': lineTo (values[0], values[1]); break; case 'q': quadraticTo (values[0], values[1], values[2], values[3]); break; case 'c': cubicTo (values[0], values[1], values[2], values[3], values[4], values[5]); break; case 'z': closeSubPath(); break; default: jassertfalse; break; // illegal string format? } } } //============================================================================== Path::Iterator::Iterator (const Path& p) noexcept : elementType (startNewSubPath), path (p), index (path.data.begin()) { } Path::Iterator::~Iterator() noexcept { } bool Path::Iterator::next() noexcept { if (index != path.data.end()) { auto type = *index++; if (isMarker (type, moveMarker)) { elementType = startNewSubPath; x1 = *index++; y1 = *index++; } else if (isMarker (type, lineMarker)) { elementType = lineTo; x1 = *index++; y1 = *index++; } else if (isMarker (type, quadMarker)) { elementType = quadraticTo; x1 = *index++; y1 = *index++; x2 = *index++; y2 = *index++; } else if (isMarker (type, cubicMarker)) { elementType = cubicTo; x1 = *index++; y1 = *index++; x2 = *index++; y2 = *index++; x3 = *index++; y3 = *index++; } else if (isMarker (type, closeSubPathMarker)) { elementType = closePath; } return true; } return false; } #undef JUCE_CHECK_COORDS_ARE_VALID } // namespace juce