From 17bb6d8eb7e860ba09dbee7472f3b3120caac92d Mon Sep 17 00:00:00 2001 From: kunitoki Date: Thu, 12 Jun 2025 15:54:00 +0200 Subject: [PATCH 01/11] More tweaks --- examples/graphics/source/examples/Audio.h | 29 +- .../yup_graphics/graphics/yup_Graphics.cpp | 3 +- modules/yup_graphics/primitives/yup_Path.cpp | 578 +++++++++++++++++- modules/yup_graphics/primitives/yup_Path.h | 45 ++ 4 files changed, 630 insertions(+), 25 deletions(-) diff --git a/examples/graphics/source/examples/Audio.h b/examples/graphics/source/examples/Audio.h index afca4e9bc..fd776e656 100644 --- a/examples/graphics/source/examples/Audio.h +++ b/examples/graphics/source/examples/Audio.h @@ -123,42 +123,33 @@ class Oscilloscope : public yup::Component for (std::size_t i = 1; i < renderData.size(); ++i) path.lineTo (i * xSize, (renderData[i] + 1.0f) * 0.5f * getHeight()); - // Outermost glow layer - g.setStrokeColor (lineColor.withAlpha (0.1f)); - g.setStrokeWidth (12.0f); - g.setStrokeCap (yup::StrokeCap::Round); - g.setStrokeJoin (yup::StrokeJoin::Round); - g.strokePath (path); + filledPath = path.createStrokePolygon(4.0f); - // Second glow layer - g.setStrokeColor (lineColor.withAlpha (0.2f)); - g.setStrokeWidth (8.0f); - g.strokePath (path); + g.setFillColor (lineColor); + g.setFeather (8.0f); + g.fillPath (filledPath); - // Third glow layer - g.setStrokeColor (lineColor.withAlpha (0.4f)); - g.setStrokeWidth (5.0f); - g.strokePath (path); + g.setFillColor (lineColor.brighter (0.2f)); + g.setFeather (4.0f); + g.fillPath (filledPath); - // Main stroke g.setStrokeColor (lineColor.withAlpha (0.8f)); - g.setStrokeWidth (2.5f); + g.setStrokeWidth (2.0f); g.strokePath (path); - // Bright center line g.setStrokeColor (lineColor.brighter (0.3f)); g.setStrokeWidth (1.0f); g.strokePath (path); - // Ultra-bright core g.setStrokeColor (yup::Colors::white.withAlpha (0.9f)); - g.setStrokeWidth (0.3f); + g.setStrokeWidth (0.5f); g.strokePath (path); } private: std::vector renderData; yup::Path path; + yup::Path filledPath; }; //============================================================================== diff --git a/modules/yup_graphics/graphics/yup_Graphics.cpp b/modules/yup_graphics/graphics/yup_Graphics.cpp index 047accfac..00982dcdd 100644 --- a/modules/yup_graphics/graphics/yup_Graphics.cpp +++ b/modules/yup_graphics/graphics/yup_Graphics.cpp @@ -656,8 +656,6 @@ void Graphics::renderFittedText (const StyledText& text, const Rectangle& const auto& options = currentRenderOptions(); - auto offset = text.getOffset (rect); // We will just use vertical offset - renderer.save(); rive::RawPath path; @@ -666,6 +664,7 @@ void Graphics::renderFittedText (const StyledText& text, const Rectangle& auto renderPath = rive::make_rcp (rive::FillRule::clockwise, path); renderer.clipPath (renderPath.get()); + auto offset = text.getOffset (rect); // We will just use vertical offset auto transform = options.getTransform (rect.getX(), rect.getY() + offset.getY()); renderer.transform (transform.toMat2D()); diff --git a/modules/yup_graphics/primitives/yup_Path.cpp b/modules/yup_graphics/primitives/yup_Path.cpp index bc47cc588..b2c59f185 100644 --- a/modules/yup_graphics/primitives/yup_Path.cpp +++ b/modules/yup_graphics/primitives/yup_Path.cpp @@ -249,8 +249,8 @@ Path& Path::addEllipse (float x, float y, float width, float height) const float ry = height * 0.5f; const float cx = x + rx; const float cy = y + ry; - const float dx = rx * 0.5522847498; - const float dy = ry * 0.5522847498; + const float dx = rx * 0.5522847498f; + const float dy = ry * 0.5522847498f; moveTo (cx + rx, cy); cubicTo (cx + rx, cy - dy, cx + dx, cy - ry, cx, cy - ry); @@ -277,8 +277,8 @@ Path& Path::addCenteredEllipse (float centerX, float centerY, float radiusX, flo const float ry = radiusY; const float cx = centerX; const float cy = centerY; - const float dx = rx * 0.5522847498; - const float dy = ry * 0.5522847498; + const float dx = rx * 0.5522847498f; + const float dy = ry * 0.5522847498f; moveTo (cx + rx, cy); cubicTo (cx + rx, cy - dy, cx + dx, cy - ry, cx, cy - ry); @@ -867,6 +867,103 @@ void handleEllipticalArc (String::CharPointerType& data, Path& path, float& curr } } +void addRoundedSubpath (Path& targetPath, const std::vector>& points, float cornerRadius, bool closed) +{ + if (points.size() < 3) + return; + + bool first = true; + + for (size_t i = 0; i < points.size(); ++i) + { + size_t prevIndex = (i == 0) ? (closed ? points.size() - 1 : 0) : i - 1; + size_t nextIndex = (i == points.size() - 1) ? (closed ? 0 : i) : i + 1; + + if (!closed && (i == 0 || i == points.size() - 1)) + { + // Don't round first/last points in open paths + if (first) + { + targetPath.moveTo (points[i]); + first = false; + } + else + { + targetPath.lineTo (points[i]); + } + continue; + } + + Point current = points[i]; + Point prev = points[prevIndex]; + Point next = points[nextIndex]; + + // Calculate vectors + Point toPrev = (prev - current).normalized(); + Point toNext = (next - current).normalized(); + + // Calculate the angle between vectors + float dot = toPrev.dotProduct (toNext); + dot = jlimit (-1.0f, 1.0f, dot); // Clamp to avoid numerical issues + + if (std::abs (dot + 1.0f) < 0.001f) // Vectors are opposite (180 degrees) + { + // Straight line, no rounding needed + if (first) + { + targetPath.moveTo (current); + first = false; + } + else + { + targetPath.lineTo (current); + } + continue; + } + + // Calculate distances to round corner + float prevDist = current.distanceTo (prev); + float nextDist = current.distanceTo (next); + float maxRadius = jmin (cornerRadius, prevDist * 0.5f, nextDist * 0.5f); + + if (maxRadius <= 0.0f) + { + if (first) + { + targetPath.moveTo (current); + first = false; + } + else + { + targetPath.lineTo (current); + } + continue; + } + + // Calculate corner points + Point cornerStart = current + toPrev * maxRadius; + Point cornerEnd = current + toNext * maxRadius; + + if (first) + { + targetPath.moveTo (cornerStart); + first = false; + } + else + { + targetPath.lineTo (cornerStart); + } + + // Add rounded corner using quadratic curve + targetPath.quadTo (cornerEnd.getX(), cornerEnd.getY(), current.getX(), current.getY()); + } + + if (closed) + { + targetPath.close(); + } +} + } // namespace bool Path::parsePathData (const String& pathData) @@ -960,4 +1057,477 @@ bool Path::parsePathData (const String& pathData) return true; } +//============================================================================== +Rectangle Path::getBounds() const +{ + return getBoundingBox(); +} + +Rectangle Path::getBoundsTransformed (const AffineTransform& transform) const +{ + Path transformedPath = transformed (transform); + return transformedPath.getBoundingBox(); +} + +//============================================================================== +Point Path::getPointAlongPath (float distance) const +{ + // Clamp distance to valid range + distance = jlimit (0.0f, 1.0f, distance); + + const auto& rawPath = path->getRawPath(); + const auto& points = rawPath.points(); + const auto& verbs = rawPath.verbs(); + + if (points.empty() || verbs.empty()) + return Point (0.0f, 0.0f); + + // Calculate total path length by walking through all segments + float totalLength = 0.0f; + std::vector segmentLengths; + segmentLengths.resize (verbs.size()); + Point currentPoint (0.0f, 0.0f); + Point lastMovePoint (0.0f, 0.0f); + + for (size_t i = 0, pointIndex = 0; i < verbs.size(); ++i) + { + auto verb = verbs[i]; + + switch (verb) + { + case rive::PathVerb::move: + if (pointIndex < points.size()) + { + currentPoint = Point (points[pointIndex].x, points[pointIndex].y); + lastMovePoint = currentPoint; + pointIndex++; + } + segmentLengths.push_back (0.0f); + break; + + case rive::PathVerb::line: + if (pointIndex < points.size()) + { + Point nextPoint (points[pointIndex].x, points[pointIndex].y); + float segmentLength = currentPoint.distanceTo (nextPoint); + segmentLengths.push_back (segmentLength); + totalLength += segmentLength; + currentPoint = nextPoint; + pointIndex++; + } + break; + + case rive::PathVerb::quad: + if (pointIndex + 1 < points.size()) + { + Point control (points[pointIndex].x, points[pointIndex].y); + Point end (points[pointIndex + 1].x, points[pointIndex + 1].y); + + // Approximate quadratic curve length using control polygon + float segmentLength = currentPoint.distanceTo (control) + control.distanceTo (end); + segmentLengths.push_back (segmentLength * 0.8f); // Approximation factor + totalLength += segmentLength * 0.8f; + currentPoint = end; + pointIndex += 2; + } + break; + + case rive::PathVerb::cubic: + if (pointIndex + 2 < points.size()) + { + Point control1 (points[pointIndex].x, points[pointIndex].y); + Point control2 (points[pointIndex + 1].x, points[pointIndex + 1].y); + Point end (points[pointIndex + 2].x, points[pointIndex + 2].y); + + // Approximate cubic curve length using control polygon + float segmentLength = currentPoint.distanceTo (control1) + + control1.distanceTo (control2) + + control2.distanceTo (end); + segmentLengths.push_back (segmentLength * 0.75f); // Approximation factor + totalLength += segmentLength * 0.75f; + currentPoint = end; + pointIndex += 3; + } + break; + + case rive::PathVerb::close: + { + float segmentLength = currentPoint.distanceTo (lastMovePoint); + segmentLengths.push_back (segmentLength); + totalLength += segmentLength; + currentPoint = lastMovePoint; + } + break; + } + } + + if (totalLength == 0.0f) + return Point (0.0f, 0.0f); + + // Find the segment containing the target distance + float targetDistance = distance * totalLength; + float accumulatedLength = 0.0f; + + currentPoint = Point (0.0f, 0.0f); + lastMovePoint = Point (0.0f, 0.0f); + + for (size_t i = 0, pointIndex = 0; i < verbs.size() && i < segmentLengths.size(); ++i) + { + auto verb = verbs[i]; + float segmentLength = segmentLengths[i]; + + if (accumulatedLength + segmentLength >= targetDistance) + { + // Found the segment, interpolate within it + float segmentProgress = segmentLength > 0.0f ? + (targetDistance - accumulatedLength) / segmentLength : 0.0f; + + switch (verb) + { + case rive::PathVerb::move: + if (pointIndex < points.size()) + return Point (points[pointIndex].x, points[pointIndex].y); + break; + + case rive::PathVerb::line: + if (pointIndex < points.size()) + { + Point nextPoint (points[pointIndex].x, points[pointIndex].y); + return currentPoint.pointBetween (nextPoint, segmentProgress); + } + break; + + case rive::PathVerb::quad: + case rive::PathVerb::cubic: + // For curves, approximate with linear interpolation to end point + if (pointIndex < points.size()) + { + size_t endIndex = verb == rive::PathVerb::quad ? pointIndex + 1 : pointIndex + 2; + if (endIndex < points.size()) + { + Point endPoint (points[endIndex].x, points[endIndex].y); + return currentPoint.pointBetween (endPoint, segmentProgress); + } + } + break; + + case rive::PathVerb::close: + return currentPoint.pointBetween (lastMovePoint, segmentProgress); + } + } + + accumulatedLength += segmentLength; + + // Update current point based on verb + switch (verb) + { + case rive::PathVerb::move: + if (pointIndex < points.size()) + { + currentPoint = Point (points[pointIndex].x, points[pointIndex].y); + lastMovePoint = currentPoint; + pointIndex++; + } + break; + + case rive::PathVerb::line: + if (pointIndex < points.size()) + { + currentPoint = Point (points[pointIndex].x, points[pointIndex].y); + pointIndex++; + } + break; + + case rive::PathVerb::quad: + if (pointIndex + 1 < points.size()) + { + currentPoint = Point (points[pointIndex + 1].x, points[pointIndex + 1].y); + pointIndex += 2; + } + break; + + case rive::PathVerb::cubic: + if (pointIndex + 2 < points.size()) + { + currentPoint = Point (points[pointIndex + 2].x, points[pointIndex + 2].y); + pointIndex += 3; + } + break; + + case rive::PathVerb::close: + currentPoint = lastMovePoint; + break; + } + } + + // If we reach here, return the last point + return currentPoint; +} + +//============================================================================== +Path Path::createStrokePolygon (float strokeWidth) const +{ + // For now, create a simple approximation by offsetting the path + // This is a basic implementation - a more sophisticated version would + // properly handle joins, caps, and curves + + const auto& rawPath = path->getRawPath(); + const auto& points = rawPath.points(); + const auto& verbs = rawPath.verbs(); + + if (points.empty() || verbs.empty()) + return Path(); + + Path strokePath; + float halfWidth = strokeWidth * 0.5f; + + // Simple approach: for each line segment, create perpendicular offsets + Point currentPoint (0.0f, 0.0f); + Point lastMovePoint (0.0f, 0.0f); + + std::vector> leftSide; + leftSide.reserve(verbs.size()); + std::vector> rightSide; + rightSide.reserve(verbs.size()); + + for (size_t i = 0, pointIndex = 0; i < verbs.size(); ++i) + { + auto verb = verbs[i]; + + switch (verb) + { + case rive::PathVerb::move: + if (pointIndex < points.size()) + { + currentPoint = Point (points[pointIndex].x, points[pointIndex].y); + lastMovePoint = currentPoint; + leftSide.clear(); + rightSide.clear(); + pointIndex++; + } + break; + + case rive::PathVerb::line: + if (pointIndex < points.size()) + { + Point nextPoint (points[pointIndex].x, points[pointIndex].y); + + // Calculate perpendicular direction + Point direction = nextPoint - currentPoint; + float length = direction.magnitude(); + if (length > 0.0f) + { + direction.normalize(); + Point perpendicular (-direction.getY(), direction.getX()); + + Point leftOffset = perpendicular * halfWidth; + Point rightOffset = perpendicular * (-halfWidth); + + if (leftSide.empty()) + { + leftSide.push_back (currentPoint + leftOffset); + rightSide.push_back (currentPoint + rightOffset); + } + + leftSide.push_back (nextPoint + leftOffset); + rightSide.push_back (nextPoint + rightOffset); + } + + currentPoint = nextPoint; + pointIndex++; + } + break; + + case rive::PathVerb::quad: + case rive::PathVerb::cubic: + // For curves, approximate with line segments + if (verb == rive::PathVerb::quad && pointIndex + 1 < points.size()) + { + Point endPoint (points[pointIndex + 1].x, points[pointIndex + 1].y); + + Point direction = endPoint - currentPoint; + float length = direction.magnitude(); + if (length > 0.0f) + { + direction.normalize(); + Point perpendicular (-direction.getY(), direction.getX()); + + Point leftOffset = perpendicular * halfWidth; + Point rightOffset = perpendicular * (-halfWidth); + + if (leftSide.empty()) + { + leftSide.push_back (currentPoint + leftOffset); + rightSide.push_back (currentPoint + rightOffset); + } + + leftSide.push_back (endPoint + leftOffset); + rightSide.push_back (endPoint + rightOffset); + } + + currentPoint = endPoint; + pointIndex += 2; + } + else if (verb == rive::PathVerb::cubic && pointIndex + 2 < points.size()) + { + Point endPoint (points[pointIndex + 2].x, points[pointIndex + 2].y); + + Point direction = endPoint - currentPoint; + float length = direction.magnitude(); + if (length > 0.0f) + { + direction.normalize(); + Point perpendicular (-direction.getY(), direction.getX()); + + Point leftOffset = perpendicular * halfWidth; + Point rightOffset = perpendicular * (-halfWidth); + + if (leftSide.empty()) + { + leftSide.push_back (currentPoint + leftOffset); + rightSide.push_back (currentPoint + rightOffset); + } + + leftSide.push_back (endPoint + leftOffset); + rightSide.push_back (endPoint + rightOffset); + } + + currentPoint = endPoint; + pointIndex += 3; + } + break; + + case rive::PathVerb::close: + // Connect back to start and create the stroke polygon + if (!leftSide.empty() && !rightSide.empty()) + { + // Create the stroke polygon by combining left and right sides + strokePath.moveTo (leftSide[0]); + + // Add all left side points + for (size_t j = 1; j < leftSide.size(); ++j) + strokePath.lineTo (leftSide[j]); + + // Add all right side points in reverse order + for (int j = static_cast (rightSide.size()) - 1; j >= 0; --j) + strokePath.lineTo (rightSide[j]); + + strokePath.close(); + } + + currentPoint = lastMovePoint; + leftSide.clear(); + rightSide.clear(); + break; + } + } + + // If path wasn't closed, still create stroke polygon + if (!leftSide.empty() && !rightSide.empty()) + { + strokePath.moveTo (leftSide[0]); + + for (size_t j = 1; j < leftSide.size(); ++j) + strokePath.lineTo (leftSide[j]); + + for (int j = static_cast (rightSide.size()) - 1; j >= 0; --j) + strokePath.lineTo (rightSide[j]); + + strokePath.close(); + } + + return strokePath; +} + +//============================================================================== +Path Path::createPathWithRoundedCorners (const Path& originalPath, float cornerRadius) +{ + if (cornerRadius <= 0.0f) + return originalPath; + + const auto& rawPath = originalPath.path->getRawPath(); + const auto& points = rawPath.points(); + const auto& verbs = rawPath.verbs(); + + if (points.empty() || verbs.empty()) + return Path(); + + Path roundedPath; + Point currentPoint (0.0f, 0.0f); + Point lastMovePoint (0.0f, 0.0f); + Point previousPoint (0.0f, 0.0f); + bool hasPreviousPoint = false; + + std::vector> pathPoints; + + // First pass: collect all points + for (size_t i = 0, pointIndex = 0; i < verbs.size(); ++i) + { + auto verb = verbs[i]; + + switch (verb) + { + case rive::PathVerb::move: + if (pointIndex < points.size()) + { + if (!pathPoints.empty()) + { + // Process previous subpath + if (pathPoints.size() >= 3) + addRoundedSubpath (roundedPath, pathPoints, cornerRadius, false); + + pathPoints.clear(); + } + + currentPoint = Point (points[pointIndex].x, points[pointIndex].y); + lastMovePoint = currentPoint; + pathPoints.push_back (currentPoint); + pointIndex++; + } + break; + + case rive::PathVerb::line: + if (pointIndex < points.size()) + { + currentPoint = Point (points[pointIndex].x, points[pointIndex].y); + pathPoints.push_back (currentPoint); + pointIndex++; + } + break; + + case rive::PathVerb::quad: + if (pointIndex + 1 < points.size()) + { + currentPoint = Point (points[pointIndex + 1].x, points[pointIndex + 1].y); + pathPoints.push_back (currentPoint); + pointIndex += 2; + } + break; + + case rive::PathVerb::cubic: + if (pointIndex + 2 < points.size()) + { + currentPoint = Point (points[pointIndex + 2].x, points[pointIndex + 2].y); + pathPoints.push_back (currentPoint); + pointIndex += 3; + } + break; + + case rive::PathVerb::close: + if (pathPoints.size() >= 3) + addRoundedSubpath (roundedPath, pathPoints, cornerRadius, true); + + pathPoints.clear(); + currentPoint = lastMovePoint; + break; + } + } + + // Handle remaining subpath + if (pathPoints.size() >= 3) + addRoundedSubpath (roundedPath, pathPoints, cornerRadius, false); + + return roundedPath; +} + } // namespace yup diff --git a/modules/yup_graphics/primitives/yup_Path.h b/modules/yup_graphics/primitives/yup_Path.h index 01e4a0f66..3c8f9040b 100644 --- a/modules/yup_graphics/primitives/yup_Path.h +++ b/modules/yup_graphics/primitives/yup_Path.h @@ -408,6 +408,51 @@ class YUP_API Path /** Returns the bounding box of this path. */ Rectangle getBoundingBox() const; + /** Returns the bounding box of this path. + + @return The bounding rectangle that contains all points in this path. + */ + Rectangle getBounds() const; + + /** Returns the bounding box of this path after applying a transformation. + + @param transform The transformation to apply before calculating the bounds. + @return The bounding rectangle that contains all transformed points in this path. + */ + Rectangle getBoundsTransformed (const AffineTransform& transform) const; + + //============================================================================== + /** Gets a point at a specific position along the path. + + This method returns a point located at the specified normalized distance along the path. + The distance parameter should be between 0.0 (start of path) and 1.0 (end of path). + + @param distance The normalized distance along the path (0.0 to 1.0). + @return The point at the specified distance along the path. + */ + Point getPointAlongPath (float distance) const; + + /** Converts the path to a stroke polygon with specified width. + + This method generates a closed polygon that represents the stroke of this path + with the given stroke width. The resulting path can be filled to achieve the + appearance of a stroked path. + + @param strokeWidth The width of the stroke. + @return A new Path representing the stroke as a closed polygon. + */ + Path createStrokePolygon (float strokeWidth) const; + + /** Creates a new path with rounded corners applied to this path. + + This method generates a new path where sharp corners are replaced with + rounded corners of the specified radius. + + @param cornerRadius The radius of the rounded corners. + @return A new Path with rounded corners applied. + */ + static Path createPathWithRoundedCorners (const Path& originalPath, float cornerRadius); + //============================================================================== // TODO - doxygen bool parsePathData (const String& pathData); From 2715fa444cd6b72188a49e3d1b6a847ad7f36808 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Thu, 12 Jun 2025 16:07:40 +0200 Subject: [PATCH 02/11] More work --- modules/yup_graphics/primitives/yup_Path.cpp | 198 ++++++++++--------- 1 file changed, 101 insertions(+), 97 deletions(-) diff --git a/modules/yup_graphics/primitives/yup_Path.cpp b/modules/yup_graphics/primitives/yup_Path.cpp index b2c59f185..b9e4ecd62 100644 --- a/modules/yup_graphics/primitives/yup_Path.cpp +++ b/modules/yup_graphics/primitives/yup_Path.cpp @@ -867,103 +867,6 @@ void handleEllipticalArc (String::CharPointerType& data, Path& path, float& curr } } -void addRoundedSubpath (Path& targetPath, const std::vector>& points, float cornerRadius, bool closed) -{ - if (points.size() < 3) - return; - - bool first = true; - - for (size_t i = 0; i < points.size(); ++i) - { - size_t prevIndex = (i == 0) ? (closed ? points.size() - 1 : 0) : i - 1; - size_t nextIndex = (i == points.size() - 1) ? (closed ? 0 : i) : i + 1; - - if (!closed && (i == 0 || i == points.size() - 1)) - { - // Don't round first/last points in open paths - if (first) - { - targetPath.moveTo (points[i]); - first = false; - } - else - { - targetPath.lineTo (points[i]); - } - continue; - } - - Point current = points[i]; - Point prev = points[prevIndex]; - Point next = points[nextIndex]; - - // Calculate vectors - Point toPrev = (prev - current).normalized(); - Point toNext = (next - current).normalized(); - - // Calculate the angle between vectors - float dot = toPrev.dotProduct (toNext); - dot = jlimit (-1.0f, 1.0f, dot); // Clamp to avoid numerical issues - - if (std::abs (dot + 1.0f) < 0.001f) // Vectors are opposite (180 degrees) - { - // Straight line, no rounding needed - if (first) - { - targetPath.moveTo (current); - first = false; - } - else - { - targetPath.lineTo (current); - } - continue; - } - - // Calculate distances to round corner - float prevDist = current.distanceTo (prev); - float nextDist = current.distanceTo (next); - float maxRadius = jmin (cornerRadius, prevDist * 0.5f, nextDist * 0.5f); - - if (maxRadius <= 0.0f) - { - if (first) - { - targetPath.moveTo (current); - first = false; - } - else - { - targetPath.lineTo (current); - } - continue; - } - - // Calculate corner points - Point cornerStart = current + toPrev * maxRadius; - Point cornerEnd = current + toNext * maxRadius; - - if (first) - { - targetPath.moveTo (cornerStart); - first = false; - } - else - { - targetPath.lineTo (cornerStart); - } - - // Add rounded corner using quadratic curve - targetPath.quadTo (cornerEnd.getX(), cornerEnd.getY(), current.getX(), current.getY()); - } - - if (closed) - { - targetPath.close(); - } -} - } // namespace bool Path::parsePathData (const String& pathData) @@ -1440,6 +1343,107 @@ Path Path::createStrokePolygon (float strokeWidth) const } //============================================================================== +namespace { + +void addRoundedSubpath (Path& targetPath, const std::vector>& points, float cornerRadius, bool closed) +{ + if (points.size() < 3) + return; + + bool first = true; + + for (size_t i = 0; i < points.size(); ++i) + { + size_t prevIndex = (i == 0) ? (closed ? points.size() - 1 : 0) : i - 1; + size_t nextIndex = (i == points.size() - 1) ? (closed ? 0 : i) : i + 1; + + if (!closed && (i == 0 || i == points.size() - 1)) + { + // Don't round first/last points in open paths + if (first) + { + targetPath.moveTo (points[i]); + first = false; + } + else + { + targetPath.lineTo (points[i]); + } + continue; + } + + Point current = points[i]; + Point prev = points[prevIndex]; + Point next = points[nextIndex]; + + // Calculate vectors + Point toPrev = (prev - current).normalized(); + Point toNext = (next - current).normalized(); + + // Calculate the angle between vectors + float dot = toPrev.dotProduct (toNext); + dot = jlimit (-1.0f, 1.0f, dot); // Clamp to avoid numerical issues + + if (std::abs (dot + 1.0f) < 0.001f) // Vectors are opposite (180 degrees) + { + // Straight line, no rounding needed + if (first) + { + targetPath.moveTo (current); + first = false; + } + else + { + targetPath.lineTo (current); + } + continue; + } + + // Calculate distances to round corner + float prevDist = current.distanceTo (prev); + float nextDist = current.distanceTo (next); + float maxRadius = jmin (cornerRadius, prevDist * 0.5f, nextDist * 0.5f); + + if (maxRadius <= 0.0f) + { + if (first) + { + targetPath.moveTo (current); + first = false; + } + else + { + targetPath.lineTo (current); + } + continue; + } + + // Calculate corner points + Point cornerStart = current + toPrev * maxRadius; + Point cornerEnd = current + toNext * maxRadius; + + if (first) + { + targetPath.moveTo (cornerStart); + first = false; + } + else + { + targetPath.lineTo (cornerStart); + } + + // Add rounded corner using quadratic curve + targetPath.quadTo (cornerEnd.getX(), cornerEnd.getY(), current.getX(), current.getY()); + } + + if (closed) + { + targetPath.close(); + } +} + +} // namespace + Path Path::createPathWithRoundedCorners (const Path& originalPath, float cornerRadius) { if (cornerRadius <= 0.0f) From 8aaee820c095b48fca6616cea093d7b171594fff Mon Sep 17 00:00:00 2001 From: Yup Bot Date: Thu, 12 Jun 2025 14:08:17 +0000 Subject: [PATCH 03/11] Code formatting --- modules/yup_graphics/primitives/yup_Path.cpp | 320 +++++++++---------- 1 file changed, 159 insertions(+), 161 deletions(-) diff --git a/modules/yup_graphics/primitives/yup_Path.cpp b/modules/yup_graphics/primitives/yup_Path.cpp index b9e4ecd62..2e0a51ed9 100644 --- a/modules/yup_graphics/primitives/yup_Path.cpp +++ b/modules/yup_graphics/primitives/yup_Path.cpp @@ -998,62 +998,60 @@ Point Path::getPointAlongPath (float distance) const switch (verb) { - case rive::PathVerb::move: - if (pointIndex < points.size()) - { - currentPoint = Point (points[pointIndex].x, points[pointIndex].y); - lastMovePoint = currentPoint; - pointIndex++; - } - segmentLengths.push_back (0.0f); - break; + case rive::PathVerb::move: + if (pointIndex < points.size()) + { + currentPoint = Point (points[pointIndex].x, points[pointIndex].y); + lastMovePoint = currentPoint; + pointIndex++; + } + segmentLengths.push_back (0.0f); + break; - case rive::PathVerb::line: - if (pointIndex < points.size()) - { - Point nextPoint (points[pointIndex].x, points[pointIndex].y); - float segmentLength = currentPoint.distanceTo (nextPoint); - segmentLengths.push_back (segmentLength); - totalLength += segmentLength; - currentPoint = nextPoint; - pointIndex++; - } - break; + case rive::PathVerb::line: + if (pointIndex < points.size()) + { + Point nextPoint (points[pointIndex].x, points[pointIndex].y); + float segmentLength = currentPoint.distanceTo (nextPoint); + segmentLengths.push_back (segmentLength); + totalLength += segmentLength; + currentPoint = nextPoint; + pointIndex++; + } + break; - case rive::PathVerb::quad: - if (pointIndex + 1 < points.size()) - { - Point control (points[pointIndex].x, points[pointIndex].y); - Point end (points[pointIndex + 1].x, points[pointIndex + 1].y); - - // Approximate quadratic curve length using control polygon - float segmentLength = currentPoint.distanceTo (control) + control.distanceTo (end); - segmentLengths.push_back (segmentLength * 0.8f); // Approximation factor - totalLength += segmentLength * 0.8f; - currentPoint = end; - pointIndex += 2; - } - break; + case rive::PathVerb::quad: + if (pointIndex + 1 < points.size()) + { + Point control (points[pointIndex].x, points[pointIndex].y); + Point end (points[pointIndex + 1].x, points[pointIndex + 1].y); + + // Approximate quadratic curve length using control polygon + float segmentLength = currentPoint.distanceTo (control) + control.distanceTo (end); + segmentLengths.push_back (segmentLength * 0.8f); // Approximation factor + totalLength += segmentLength * 0.8f; + currentPoint = end; + pointIndex += 2; + } + break; - case rive::PathVerb::cubic: - if (pointIndex + 2 < points.size()) - { - Point control1 (points[pointIndex].x, points[pointIndex].y); - Point control2 (points[pointIndex + 1].x, points[pointIndex + 1].y); - Point end (points[pointIndex + 2].x, points[pointIndex + 2].y); - - // Approximate cubic curve length using control polygon - float segmentLength = currentPoint.distanceTo (control1) + - control1.distanceTo (control2) + - control2.distanceTo (end); - segmentLengths.push_back (segmentLength * 0.75f); // Approximation factor - totalLength += segmentLength * 0.75f; - currentPoint = end; - pointIndex += 3; - } - break; + case rive::PathVerb::cubic: + if (pointIndex + 2 < points.size()) + { + Point control1 (points[pointIndex].x, points[pointIndex].y); + Point control2 (points[pointIndex + 1].x, points[pointIndex + 1].y); + Point end (points[pointIndex + 2].x, points[pointIndex + 2].y); + + // Approximate cubic curve length using control polygon + float segmentLength = currentPoint.distanceTo (control1) + control1.distanceTo (control2) + control2.distanceTo (end); + segmentLengths.push_back (segmentLength * 0.75f); // Approximation factor + totalLength += segmentLength * 0.75f; + currentPoint = end; + pointIndex += 3; + } + break; - case rive::PathVerb::close: + case rive::PathVerb::close: { float segmentLength = currentPoint.distanceTo (lastMovePoint); segmentLengths.push_back (segmentLength); @@ -1082,40 +1080,39 @@ Point Path::getPointAlongPath (float distance) const if (accumulatedLength + segmentLength >= targetDistance) { // Found the segment, interpolate within it - float segmentProgress = segmentLength > 0.0f ? - (targetDistance - accumulatedLength) / segmentLength : 0.0f; + float segmentProgress = segmentLength > 0.0f ? (targetDistance - accumulatedLength) / segmentLength : 0.0f; switch (verb) { - case rive::PathVerb::move: - if (pointIndex < points.size()) - return Point (points[pointIndex].x, points[pointIndex].y); - break; + case rive::PathVerb::move: + if (pointIndex < points.size()) + return Point (points[pointIndex].x, points[pointIndex].y); + break; - case rive::PathVerb::line: - if (pointIndex < points.size()) - { - Point nextPoint (points[pointIndex].x, points[pointIndex].y); - return currentPoint.pointBetween (nextPoint, segmentProgress); - } - break; + case rive::PathVerb::line: + if (pointIndex < points.size()) + { + Point nextPoint (points[pointIndex].x, points[pointIndex].y); + return currentPoint.pointBetween (nextPoint, segmentProgress); + } + break; - case rive::PathVerb::quad: - case rive::PathVerb::cubic: - // For curves, approximate with linear interpolation to end point - if (pointIndex < points.size()) - { - size_t endIndex = verb == rive::PathVerb::quad ? pointIndex + 1 : pointIndex + 2; - if (endIndex < points.size()) + case rive::PathVerb::quad: + case rive::PathVerb::cubic: + // For curves, approximate with linear interpolation to end point + if (pointIndex < points.size()) { - Point endPoint (points[endIndex].x, points[endIndex].y); - return currentPoint.pointBetween (endPoint, segmentProgress); + size_t endIndex = verb == rive::PathVerb::quad ? pointIndex + 1 : pointIndex + 2; + if (endIndex < points.size()) + { + Point endPoint (points[endIndex].x, points[endIndex].y); + return currentPoint.pointBetween (endPoint, segmentProgress); + } } - } - break; + break; - case rive::PathVerb::close: - return currentPoint.pointBetween (lastMovePoint, segmentProgress); + case rive::PathVerb::close: + return currentPoint.pointBetween (lastMovePoint, segmentProgress); } } @@ -1124,42 +1121,42 @@ Point Path::getPointAlongPath (float distance) const // Update current point based on verb switch (verb) { - case rive::PathVerb::move: - if (pointIndex < points.size()) - { - currentPoint = Point (points[pointIndex].x, points[pointIndex].y); - lastMovePoint = currentPoint; - pointIndex++; - } - break; + case rive::PathVerb::move: + if (pointIndex < points.size()) + { + currentPoint = Point (points[pointIndex].x, points[pointIndex].y); + lastMovePoint = currentPoint; + pointIndex++; + } + break; - case rive::PathVerb::line: - if (pointIndex < points.size()) - { - currentPoint = Point (points[pointIndex].x, points[pointIndex].y); - pointIndex++; - } - break; + case rive::PathVerb::line: + if (pointIndex < points.size()) + { + currentPoint = Point (points[pointIndex].x, points[pointIndex].y); + pointIndex++; + } + break; - case rive::PathVerb::quad: - if (pointIndex + 1 < points.size()) - { - currentPoint = Point (points[pointIndex + 1].x, points[pointIndex + 1].y); - pointIndex += 2; - } - break; + case rive::PathVerb::quad: + if (pointIndex + 1 < points.size()) + { + currentPoint = Point (points[pointIndex + 1].x, points[pointIndex + 1].y); + pointIndex += 2; + } + break; - case rive::PathVerb::cubic: - if (pointIndex + 2 < points.size()) - { - currentPoint = Point (points[pointIndex + 2].x, points[pointIndex + 2].y); - pointIndex += 3; - } - break; + case rive::PathVerb::cubic: + if (pointIndex + 2 < points.size()) + { + currentPoint = Point (points[pointIndex + 2].x, points[pointIndex + 2].y); + pointIndex += 3; + } + break; - case rive::PathVerb::close: - currentPoint = lastMovePoint; - break; + case rive::PathVerb::close: + currentPoint = lastMovePoint; + break; } } @@ -1189,9 +1186,9 @@ Path Path::createStrokePolygon (float strokeWidth) const Point lastMovePoint (0.0f, 0.0f); std::vector> leftSide; - leftSide.reserve(verbs.size()); + leftSide.reserve (verbs.size()); std::vector> rightSide; - rightSide.reserve(verbs.size()); + rightSide.reserve (verbs.size()); for (size_t i = 0, pointIndex = 0; i < verbs.size(); ++i) { @@ -1302,7 +1299,7 @@ Path Path::createStrokePolygon (float strokeWidth) const case rive::PathVerb::close: // Connect back to start and create the stroke polygon - if (!leftSide.empty() && !rightSide.empty()) + if (! leftSide.empty() && ! rightSide.empty()) { // Create the stroke polygon by combining left and right sides strokePath.moveTo (leftSide[0]); @@ -1326,7 +1323,7 @@ Path Path::createStrokePolygon (float strokeWidth) const } // If path wasn't closed, still create stroke polygon - if (!leftSide.empty() && !rightSide.empty()) + if (! leftSide.empty() && ! rightSide.empty()) { strokePath.moveTo (leftSide[0]); @@ -1343,7 +1340,8 @@ Path Path::createStrokePolygon (float strokeWidth) const } //============================================================================== -namespace { +namespace +{ void addRoundedSubpath (Path& targetPath, const std::vector>& points, float cornerRadius, bool closed) { @@ -1357,7 +1355,7 @@ void addRoundedSubpath (Path& targetPath, const std::vector>& point size_t prevIndex = (i == 0) ? (closed ? points.size() - 1 : 0) : i - 1; size_t nextIndex = (i == points.size() - 1) ? (closed ? 0 : i) : i + 1; - if (!closed && (i == 0 || i == points.size() - 1)) + if (! closed && (i == 0 || i == points.size() - 1)) { // Don't round first/last points in open paths if (first) @@ -1471,59 +1469,59 @@ Path Path::createPathWithRoundedCorners (const Path& originalPath, float cornerR switch (verb) { - case rive::PathVerb::move: - if (pointIndex < points.size()) - { - if (!pathPoints.empty()) + case rive::PathVerb::move: + if (pointIndex < points.size()) { - // Process previous subpath - if (pathPoints.size() >= 3) - addRoundedSubpath (roundedPath, pathPoints, cornerRadius, false); + if (! pathPoints.empty()) + { + // Process previous subpath + if (pathPoints.size() >= 3) + addRoundedSubpath (roundedPath, pathPoints, cornerRadius, false); - pathPoints.clear(); - } + pathPoints.clear(); + } - currentPoint = Point (points[pointIndex].x, points[pointIndex].y); - lastMovePoint = currentPoint; - pathPoints.push_back (currentPoint); - pointIndex++; - } - break; + currentPoint = Point (points[pointIndex].x, points[pointIndex].y); + lastMovePoint = currentPoint; + pathPoints.push_back (currentPoint); + pointIndex++; + } + break; - case rive::PathVerb::line: - if (pointIndex < points.size()) - { - currentPoint = Point (points[pointIndex].x, points[pointIndex].y); - pathPoints.push_back (currentPoint); - pointIndex++; - } - break; + case rive::PathVerb::line: + if (pointIndex < points.size()) + { + currentPoint = Point (points[pointIndex].x, points[pointIndex].y); + pathPoints.push_back (currentPoint); + pointIndex++; + } + break; - case rive::PathVerb::quad: - if (pointIndex + 1 < points.size()) - { - currentPoint = Point (points[pointIndex + 1].x, points[pointIndex + 1].y); - pathPoints.push_back (currentPoint); - pointIndex += 2; - } - break; + case rive::PathVerb::quad: + if (pointIndex + 1 < points.size()) + { + currentPoint = Point (points[pointIndex + 1].x, points[pointIndex + 1].y); + pathPoints.push_back (currentPoint); + pointIndex += 2; + } + break; - case rive::PathVerb::cubic: - if (pointIndex + 2 < points.size()) - { - currentPoint = Point (points[pointIndex + 2].x, points[pointIndex + 2].y); - pathPoints.push_back (currentPoint); - pointIndex += 3; - } - break; + case rive::PathVerb::cubic: + if (pointIndex + 2 < points.size()) + { + currentPoint = Point (points[pointIndex + 2].x, points[pointIndex + 2].y); + pathPoints.push_back (currentPoint); + pointIndex += 3; + } + break; - case rive::PathVerb::close: - if (pathPoints.size() >= 3) - addRoundedSubpath (roundedPath, pathPoints, cornerRadius, true); + case rive::PathVerb::close: + if (pathPoints.size() >= 3) + addRoundedSubpath (roundedPath, pathPoints, cornerRadius, true); - pathPoints.clear(); - currentPoint = lastMovePoint; - break; + pathPoints.clear(); + currentPoint = lastMovePoint; + break; } } From 98567c281700980d09cf331b9e1a967b52a0fbec Mon Sep 17 00:00:00 2001 From: kunitoki Date: Thu, 12 Jun 2025 17:54:23 +0200 Subject: [PATCH 04/11] More path goodies --- examples/graphics/source/examples/Paths.h | 613 +++++++++++++++++++ modules/yup_graphics/graphics/yup_Color.h | 89 ++- modules/yup_graphics/primitives/yup_Path.cpp | 187 +++++- modules/yup_graphics/primitives/yup_Path.h | 200 +++++- 4 files changed, 1035 insertions(+), 54 deletions(-) diff --git a/examples/graphics/source/examples/Paths.h b/examples/graphics/source/examples/Paths.h index 04138bf24..460140e3c 100644 --- a/examples/graphics/source/examples/Paths.h +++ b/examples/graphics/source/examples/Paths.h @@ -33,7 +33,620 @@ class PathsExample : public yup::Component void paint (yup::Graphics& g) override { + // Draw title + /* + g.setColour (yup::Color (50, 50, 80)); + g.setFont (yup::Font (24.0f, yup::Font::bold)); + g.drawText ("YUP Path Class - Complete Feature Demonstration", + yup::Rectangle (20, 10, getWidth() - 40, 40), + yup::Justification::centred); + */ + + auto bounds = getLocalBounds().to().reduced (20, 60); + auto sectionHeight = bounds.getHeight() / 4.0f; + auto sectionWidth = bounds.getWidth() / 3.0f; + + // Section 1: Basic Path Operations + drawBasicPathOperations (g, yup::Rectangle (bounds.getX(), bounds.getY(), + sectionWidth, sectionHeight)); + + // Section 2: Basic Shapes + drawBasicShapes (g, yup::Rectangle (bounds.getX() + sectionWidth, bounds.getY(), + sectionWidth, sectionHeight)); + + // Section 3: Complex Shapes (Polygons, Stars, Bubbles) + drawComplexShapes (g, yup::Rectangle (bounds.getX() + sectionWidth * 2, bounds.getY(), + sectionWidth, sectionHeight)); + + // Section 4: Arcs and Curves + drawArcsAndCurves (g, yup::Rectangle (bounds.getX(), bounds.getY() + sectionHeight, + sectionWidth, sectionHeight)); + + // Section 5: Path Transformations + drawPathTransformations (g, yup::Rectangle (bounds.getX() + sectionWidth, bounds.getY() + sectionHeight, + sectionWidth, sectionHeight)); + + // Section 6: Advanced Features + drawAdvancedFeatures (g, yup::Rectangle (bounds.getX() + sectionWidth * 2, bounds.getY() + sectionHeight, + sectionWidth, sectionHeight)); + + // Section 7: Path Utilities + drawPathUtilities (g, yup::Rectangle (bounds.getX(), bounds.getY() + sectionHeight * 2, + sectionWidth, sectionHeight)); + + // Section 8: SVG Path Data + drawSVGPathData (g, yup::Rectangle (bounds.getX() + sectionWidth, bounds.getY() + sectionHeight * 2, + sectionWidth, sectionHeight)); + + // Section 9: Creative Examples + drawCreativeExamples (g, yup::Rectangle (bounds.getX() + sectionWidth * 2, bounds.getY() + sectionHeight * 2, + sectionWidth, sectionHeight)); + + // Section 10: Interactive Features Demo + drawInteractiveDemo (g, yup::Rectangle (bounds.getX(), bounds.getY() + sectionHeight * 3, + bounds.getWidth(), sectionHeight)); } private: + void drawSectionTitle (yup::Graphics& g, const yup::String& title, yup::Rectangle area) + { + yup::StyledText text; + + { + auto modifier = text.startUpdate(); + modifier.setMaxSize (area.getSize()); + modifier.setHorizontalAlign (yup::StyledText::center); + modifier.appendText (title, yup::ApplicationTheme::getGlobalTheme()->getDefaultFont()); + } + + g.setFillColor (yup::Colors::white); + g.fillFittedText(text, area.removeFromTop (20)); + } + + void drawBasicPathOperations (yup::Graphics& g, yup::Rectangle area) + { + drawSectionTitle (g, "Basic Path Operations", area); + area = area.reduced (10); // .withTrimmedTop (25); + + yup::Path path; + float x = area.getX() + 20; + float y = area.getY() + 30; + + // MoveTo and LineTo demo + path.moveTo (x, y); + path.lineTo (x + 60, y); + path.lineTo (x + 60, y + 40); + path.lineTo (x, y + 40); + path.close(); + + g.setFillColor (yup::Color (100, 150, 255)); + g.fillPath (path); + g.setStrokeColor (yup::Color (50, 100, 200)); + g.setStrokeWidth (2.0f); + g.strokePath (path); + + // QuadTo demo + yup::Path quadPath; + x += 80; + quadPath.moveTo (x, y + 40); + quadPath.quadTo (x + 30, y, x + 60, y + 40); + + g.setStrokeColor (yup::Color (255, 150, 100)); + g.setStrokeWidth (3.0f); + g.strokePath (quadPath); + + // CubicTo demo + yup::Path cubicPath; + x += 80; + cubicPath.moveTo (x, y + 40); + cubicPath.cubicTo (x + 60, y + 40, x + 10, y, x + 50, y); + + g.setStrokeColor (yup::Color (150, 255, 150)); + g.setStrokeWidth (3.0f); + g.strokePath (cubicPath); + + // Draw labels + //g.setColour (yup::Color (80, 80, 80)); + //g.setFont (yup::Font (10.0f)); + //g.drawText ("Rectangle", yup::Rectangle (area.getX(), y + 50, 80, 15), yup::Justification::centred); + //g.drawText ("Quadratic", yup::Rectangle (area.getX() + 80, y + 50, 80, 15), yup::Justification::centred); + //g.drawText ("Cubic Curve", yup::Rectangle (area.getX() + 160, y + 50, 80, 15), yup::Justification::centred); + } + + void drawBasicShapes (yup::Graphics& g, yup::Rectangle area) + { + drawSectionTitle (g, "Basic Shapes", area); + area = area.reduced (10); // .withTrimmedTop (25); + + float x = area.getX() + 10; + float y = area.getY() + 10; + float spacing = 80; + + // Rectangle + yup::Path rectPath; + rectPath.addRectangle (x, y, 60, 40); + g.setFillColor (yup::Color (255, 200, 200)); + g.fillPath (rectPath); + g.setStrokeColor (yup::Color (200, 100, 100)); + g.setStrokeWidth (1.0f); + g.strokePath (rectPath); + + // Rounded Rectangle + yup::Path roundedRectPath; + roundedRectPath.addRoundedRectangle (x, y + 60, 60, 40, 10); + g.setFillColor (yup::Color (200, 255, 200)); + g.fillPath (roundedRectPath); + g.setStrokeColor (yup::Color (100, 200, 100)); + g.strokePath (roundedRectPath); + + // Ellipse + yup::Path ellipsePath; + ellipsePath.addEllipse (x + spacing, y, 60, 40); + g.setFillColor (yup::Color (200, 200, 255)); + g.fillPath (ellipsePath); + g.setStrokeColor (yup::Color (100, 100, 200)); + g.strokePath (ellipsePath); + + // Centered Ellipse + yup::Path centeredEllipsePath; + centeredEllipsePath.addCenteredEllipse (yup::Point (x + spacing + 30, y + 80), 30, 20); + g.setFillColor (yup::Color (255, 255, 200)); + g.fillPath (centeredEllipsePath); + g.setStrokeColor (yup::Color (200, 200, 100)); + g.strokePath (centeredEllipsePath); + + // Labels + /* + g.setColour (yup::Color (80, 80, 80)); + g.setFont (yup::Font (9.0f)); + g.drawText ("Rectangle", yup::Rectangle (x - 10, y + 45, 80, 12), yup::Justification::centred); + g.drawText ("Rounded", yup::Rectangle (x - 10, y + 105, 80, 12), yup::Justification::centred); + g.drawText ("Ellipse", yup::Rectangle (x + spacing - 10, y + 45, 80, 12), yup::Justification::centred); + g.drawText ("Centered", yup::Rectangle (x + spacing - 10, y + 105, 80, 12), yup::Justification::centred); + */ + } + + void drawComplexShapes (yup::Graphics& g, yup::Rectangle area) + { + drawSectionTitle (g, "Complex Shapes", area); + area = area.reduced (10); // .withTrimmedTop (25); + + float x = area.getX() + 40; + float y = area.getY() + 40; + + // Pentagon + yup::Path pentagonPath; + pentagonPath.addPolygon (yup::Point (x, y), 5, 25, -yup::MathConstants::halfPi); + g.setFillColor (yup::Color (255, 180, 120)); + g.fillPath (pentagonPath); + g.setStrokeColor (yup::Color (200, 120, 60)); + g.setStrokeWidth (1.5f); + g.strokePath (pentagonPath); + + // Hexagon + yup::Path hexagonPath; + hexagonPath.addPolygon (yup::Point (x + 80, y), 6, 25, 0); + g.setFillColor (yup::Color (180, 255, 180)); + g.fillPath (hexagonPath); + g.setStrokeColor (yup::Color (120, 200, 120)); + g.strokePath (hexagonPath); + + // Star + yup::Path starPath; + starPath.addStar (yup::Point (x + 160, y), 5, 15, 25, -yup::MathConstants::halfPi); + g.setFillColor (yup::Color (255, 255, 120)); + g.fillPath (starPath); + g.setStrokeColor (yup::Color (200, 200, 60)); + g.strokePath (starPath); + + // Speech Bubble + yup::Path bubblePath; + yup::Rectangle bodyArea (x - 20, y + 70, 80, 40); + yup::Rectangle maxArea = bodyArea.enlarged(20); + yup::Point tipPosition (x + 70, y + 120); + bubblePath.addBubble (bodyArea, maxArea, tipPosition, 8, 12); + g.setFillColor (yup::Color (220, 240, 255)); + g.fillPath (bubblePath); + g.setStrokeColor (yup::Color (100, 150, 200)); + g.strokePath (bubblePath); + + // Triangle (3-sided polygon) + yup::Path trianglePath; + trianglePath.addPolygon (yup::Point (x + 120, y + 85), 3, 20, -yup::MathConstants::halfPi); + g.setFillColor (yup::Color (255, 200, 255)); + g.fillPath (trianglePath); + g.setStrokeColor (yup::Color (200, 100, 200)); + g.strokePath (trianglePath); + + // Labels + /* + g.setColour (yup::Color (80, 80, 80)); + g.setFont (yup::Font (8.0f)); + g.drawText ("Pentagon", yup::Rectangle (x - 25, y + 30, 50, 12), yup::Justification::centred); + g.drawText ("Hexagon", yup::Rectangle (x + 55, y + 30, 50, 12), yup::Justification::centred); + g.drawText ("Star", yup::Rectangle (x + 135, y + 30, 50, 12), yup::Justification::centred); + g.drawText ("Bubble", yup::Rectangle (x - 25, y + 120, 50, 12), yup::Justification::centred); + g.drawText ("Triangle", yup::Rectangle (x + 95, y + 110, 50, 12), yup::Justification::centred); + */ + } + + void drawArcsAndCurves (yup::Graphics& g, yup::Rectangle area) + { + drawSectionTitle (g, "Arcs & Curves", area); + area = area.reduced (10); // .withTrimmedTop (25); + + float x = area.getX() + 30; + float y = area.getY() + 30; + + // Simple Arc + yup::Path arcPath; + arcPath.addArc (x, y, 60, 60, 0, yup::MathConstants::halfPi, true); + g.setStrokeColor (yup::Color (255, 150, 150)); + g.setStrokeWidth (3.0f); + g.strokePath (arcPath); + + // Centered Arc with rotation + yup::Path centeredArcPath; + centeredArcPath.addCenteredArc (yup::Point (x + 100, y + 30), 25, 15, + yup::MathConstants::pi / 4, 0, yup::MathConstants::pi, true); + g.setStrokeColor (yup::Color (150, 255, 150)); + g.setStrokeWidth (3.0f); + g.strokePath (centeredArcPath); + + // Complete circle using arc + yup::Path circlePath; + circlePath.addArc (x + 160, y, 50, 50, 0, yup::MathConstants::twoPi, true); + g.setFillColor (yup::Color (150, 150, 255)); + g.fillPath (circlePath); + + // Complex curve combination + yup::Path complexPath; + complexPath.moveTo (x, y + 80); + complexPath.quadTo (x + 50, y + 60, x + 100, y + 80); + complexPath.cubicTo (x + 150, y + 80, x + 180, y + 120, x + 200, y + 100); + g.setStrokeColor (yup::Color (255, 200, 100)); + g.setStrokeWidth (2.0f); + g.strokePath (complexPath); + + // Labels + /* + g.setColour (yup::Color (80, 80, 80)); + g.setFont (yup::Font (9.0f)); + g.drawText ("Arc", yup::Rectangle (x - 10, y + 70, 60, 12), yup::Justification::centred); + g.drawText ("Rotated Arc", yup::Rectangle (x + 70, y + 70, 60, 12), yup::Justification::centred); + g.drawText ("Circle", yup::Rectangle (x + 140, y + 60, 60, 12), yup::Justification::centred); + g.drawText ("Complex Curves", yup::Rectangle (x + 50, y + 110, 100, 12), yup::Justification::centred); + */ + } + + void drawPathTransformations (yup::Graphics& g, yup::Rectangle area) + { + drawSectionTitle (g, "Transformations", area); + area = area.reduced (10); // .withTrimmedTop (25); + + float x = area.getX() + 20; + float y = area.getY() + 20; + + // Original shape + yup::Path originalPath; + originalPath.addStar (yup::Point (x + 30, y + 30), 5, 15, 25, 0); + g.setFillColor (yup::Color (200, 200, 200)); + g.fillPath (originalPath); + g.setStrokeColor (yup::Color (100, 100, 100)); + g.setStrokeWidth (1.0f); + g.strokePath (originalPath); + + // Scaled version + yup::Path scaledPath = originalPath.transformed (yup::AffineTransform::scaling (0.7f, 1.3f)); + scaledPath.transform (yup::AffineTransform::translation (80, 0)); + g.setFillColor (yup::Color (255, 200, 200)); + g.fillPath (scaledPath); + g.setStrokeColor (yup::Color (200, 100, 100)); + g.strokePath (scaledPath); + + // Rotated version + yup::Path rotatedPath = originalPath.transformed ( + yup::AffineTransform::rotation (yup::MathConstants::pi / 4, x + 30, y + 30)); + rotatedPath.transform (yup::AffineTransform::translation (160, 0)); + g.setFillColor (yup::Color (200, 255, 200)); + g.fillPath (rotatedPath); + g.setStrokeColor (yup::Color (100, 200, 100)); + g.strokePath (rotatedPath); + + // ScaleToFit demo + yup::Path scaleToFitPath; + scaleToFitPath.addPolygon (yup::Point (0, 0), 6, 20, 0); + scaleToFitPath.scaleToFit (x, y + 80, 180, 30, true); + g.setFillColor (yup::Color (200, 200, 255)); + g.fillPath (scaleToFitPath); + g.setStrokeColor (yup::Color (100, 100, 200)); + g.strokePath (scaleToFitPath); + + // Labels + /* + g.setColour (yup::Color (80, 80, 80)); + g.setFont (yup::Font (8.0f)); + g.drawText ("Original", yup::Rectangle (x - 5, y + 60, 60, 12), yup::Justification::centred); + g.drawText ("Scaled", yup::Rectangle (x + 55, y + 60, 60, 12), yup::Justification::centred); + g.drawText ("Rotated", yup::Rectangle (x + 135, y + 60, 60, 12), yup::Justification::centred); + g.drawText ("Scale to Fit", yup::Rectangle (x + 40, y + 115, 100, 12), yup::Justification::centred); + */ + } + + void drawAdvancedFeatures (yup::Graphics& g, yup::Rectangle area) + { + drawSectionTitle (g, "Advanced Features", area); + area = area.reduced (10); // .withTrimmedTop (25); + + float x = area.getX() + 20; + float y = area.getY() + 20; + + // Stroke polygon demo + yup::Path originalCurve; + originalCurve.moveTo (x, y + 40); + originalCurve.quadTo (x + 40, y, x + 80, y + 40); + + yup::Path strokePolygon = originalCurve.createStrokePolygon (8.0f); + g.setFillColor (yup::Color (255, 220, 180)); + g.fillPath (strokePolygon); + g.setStrokeColor (yup::Color (200, 150, 100)); + g.setStrokeWidth (1.0f); + g.strokePath (originalCurve); + + // Rounded corners demo + yup::Path sharpPath; + sharpPath.addRectangle (x + 100, y, 60, 60); + yup::Path roundedPath = sharpPath.withRoundedCorners (10.0f); + + g.setFillColor (yup::Color (200, 255, 220)); + g.fillPath (roundedPath); + g.setStrokeColor (yup::Color (100, 200, 120)); + g.strokePath (sharpPath); + + // Point along path demo + yup::Path curvePath; + curvePath.moveTo (x, y + 80); + curvePath.cubicTo (x + 60, y + 80, x + 120, y + 120, x + 180, y + 100); + + g.setStrokeColor (yup::Color (100, 150, 255)); + g.setStrokeWidth (2.0f); + g.strokePath (curvePath); + + // Draw points along the path + for (float t = 0.0f; t <= 1.0f; t += 0.2f) + { + yup::Point point = curvePath.getPointAlongPath (t); + yup::Path pointPath; + pointPath.addCenteredEllipse (point, 4, 4); + g.setFillColor (yup::Color (255, 100, 100)); + g.fillPath (pointPath); + } + + // Labels + /* + g.setColour (yup::Color (80, 80, 80)); + g.setFont (yup::Font (8.0f)); + g.drawText ("Stroke Polygon", yup::Rectangle (x - 10, y + 60, 100, 12), yup::Justification::centred); + g.drawText ("Rounded Corners", yup::Rectangle (x + 90, y + 70, 80, 12), yup::Justification::centred); + g.drawText ("Points Along Path", yup::Rectangle (x + 40, y + 130, 100, 12), yup::Justification::centred); + */ + } + + void drawPathUtilities (yup::Graphics& g, yup::Rectangle area) + { + drawSectionTitle (g, "Path Utilities", area); + area = area.reduced (10); // .withTrimmedTop (25); + + float x = area.getX() + 20; + float y = area.getY() + 20; + + // AppendPath demo + yup::Path path1; + path1.addEllipse (x, y, 40, 40); + + yup::Path path2; + path2.addRectangle (x + 20, y + 20, 40, 40); + + path1.appendPath (path2); + g.setFillColor (yup::Color (255, 200, 255)); + g.fillPath (path1); + g.setStrokeColor (yup::Color (200, 100, 200)); + g.setStrokeWidth (1.0f); + g.strokePath (path1); + + // Bounds demonstration + yup::Path boundsPath; + boundsPath.addStar (yup::Point (x + 120, y + 30), 5, 15, 25, 0); + yup::Rectangle bounds = boundsPath.getBounds(); + + g.setFillColor (yup::Color (180, 255, 180)); + g.fillPath (boundsPath); + g.setStrokeColor (yup::Color (255, 100, 100)); + g.setStrokeWidth (1.0f); + g.strokeRect (bounds); + + // Size and clear demo + yup::Path infoPath; + infoPath.addPolygon (yup::Point (x + 60, y + 80), 8, 20, 0); + + g.setFillColor (yup::Color (200, 220, 255)); + g.fillPath (infoPath); + + // Display path info + /* + g.setColour (yup::Color (80, 80, 80)); + g.setFont (yup::Font (8.0f)); + yup::String sizeText = "Size: " + yup::String (infoPath.size()); + g.drawText (sizeText, yup::Rectangle (x + 20, y + 110, 100, 12), yup::Justification::left); + + // Labels + g.drawText ("Append Paths", yup::Rectangle (x - 10, y + 70, 80, 12), yup::Justification::centred); + g.drawText ("Bounds", yup::Rectangle (x + 90, y + 70, 80, 12), yup::Justification::centred); + g.drawText ("Path Info", yup::Rectangle (x + 20, y + 130, 80, 12), yup::Justification::centred); + */ + } + + void drawSVGPathData (yup::Graphics& g, yup::Rectangle area) + { + drawSectionTitle (g, "SVG Path Data", area); + area = area.reduced (10); // .withTrimmedTop (25); + + float x = area.getX() + 20; + float y = area.getY() + 20; + + // Parse SVG path data examples + yup::Path svgHeart; + svgHeart.parsePathData ("M12,21.35l-1.45-1.32C5.4,15.36,2,12.28,2,8.5 C2,5.42,4.42,3,7.5,3c1.74,0,3.41,0.81,4.5,2.09C13.09,3.81,14.76,3,16.5,3 C19.58,3,22,5.42,22,8.5c0,3.78-3.4,6.86-8.55,11.54L12,21.35z"); + + // Scale and position the heart + yup::Rectangle heartBounds = svgHeart.getBounds(); + float scale = 3.0f; + svgHeart.transform (yup::AffineTransform::scaling (scale)); + svgHeart.transform (yup::AffineTransform::translation (x - heartBounds.getX() * scale, y - heartBounds.getY() * scale)); + + g.setFillColor (yup::Color (255, 150, 150)); + g.fillPath (svgHeart); + g.setStrokeColor (yup::Color (200, 100, 100)); + g.setStrokeWidth (1.0f); + g.strokePath (svgHeart); + + // Simple SVG path + yup::Path svgTriangle; + svgTriangle.parsePathData ("M100,20 L180,160 L20,160 Z"); + svgTriangle.scaleToFit (x + 100, y, 80, 80, true); + + g.setFillColor (yup::Color (150, 255, 150)); + g.fillPath (svgTriangle); + g.setStrokeColor (yup::Color (100, 200, 100)); + g.setStrokeWidth (1.0f); + g.strokePath (svgTriangle); + + // Labels + /* + g.setColour (yup::Color (80, 80, 80)); + g.setFont (yup::Font (8.0f)); + g.drawText ("SVG Heart", yup::Rectangle (x - 10, y + 90, 80, 12), yup::Justification::centred); + g.drawText ("SVG Triangle", yup::Rectangle (x + 90, y + 90, 80, 12), yup::Justification::centred); + */ + } + + void drawCreativeExamples (yup::Graphics& g, yup::Rectangle area) + { + drawSectionTitle (g, "Creative Examples", area); + area = area.reduced (10); // .withTrimmedTop (25); + + float x = area.getX() + 20; + float y = area.getY() + 20; + + // Flower pattern using multiple shapes + yup::Point center (x + 60, y + 60); + + // Petals + for (int i = 0; i < 8; ++i) + { + float angle = i * yup::MathConstants::twoPi / 8.0f; + yup::Path petal; + petal.addCenteredEllipse (yup::Point (0, 0), 15, 30); + petal.transform (yup::AffineTransform::rotation (angle)); + petal.transform (yup::AffineTransform::translation (center.getX(), center.getY())); + + float hue = i / 8.0f; + g.setFillColor (yup::Color::fromHSV (hue, 0.7f, 1.0f, 0.8f)); + g.fillPath (petal); + } + + // Center + yup::Path flowerCenter; + flowerCenter.addCenteredEllipse (center, 12, 12); + g.setFillColor (yup::Color (255, 255, 100)); + g.fillPath (flowerCenter); + g.setStrokeColor (yup::Color (200, 200, 50)); + g.setStrokeWidth (1.0f); + g.strokePath (flowerCenter); + + // Gear shape using polygons + yup::Path gear; + gear.addPolygon (yup::Point (x + 180, y + 60), 12, 35, 0); + yup::Path innerGear; + innerGear.addPolygon (yup::Point (x + 180, y + 60), 12, 25, yup::MathConstants::pi / 12); + + g.setFillColor (yup::Color (180, 180, 180)); + g.fillPath (gear); + g.setFillColor (yup::Color (220, 220, 220)); + g.fillPath (innerGear); + g.setStrokeColor (yup::Color (100, 100, 100)); + g.strokePath (gear); + + // Center hole + yup::Path centerHole; + centerHole.addCenteredEllipse (yup::Point (x + 180, y + 60), 10, 10); + g.setFillColor (yup::Color (245, 245, 250)); + g.fillPath (centerHole); + + // Labels + /* + g.setColour (yup::Color (80, 80, 80)); + g.setFont (yup::Font (8.0f)); + g.drawText ("Flower", yup::Rectangle (x + 20, y + 130, 80, 12), yup::Justification::centred); + g.drawText ("Gear", yup::Rectangle (x + 140, y + 130, 80, 12), yup::Justification::centred); + */ + } + + void drawInteractiveDemo (yup::Graphics& g, yup::Rectangle area) + { + drawSectionTitle (g, "Interactive Path Builder Demo", area); + area = area.reduced (10); // .withTrimmedTop (25); + + // Create a complex path combining multiple features + yup::Path masterPath; + + float centerX = area.getCenterX(); + float centerY = area.getCenterY(); + + // Base shape - rounded rectangle + masterPath.addRoundedRectangle (centerX - 150, centerY - 40, 300, 80, 20); + + // Add decorative elements + for (int i = 0; i < 5; ++i) + { + yup::Path star; + float x = centerX - 120 + i * 60; + star.addStar (yup::Point (x, centerY - 60), 5, 8, 15, 0); + masterPath.appendPath (star); + } + + // Add speech bubble + yup::Path bubble; + yup::Rectangle bubbleBody (centerX + 160, centerY - 30, 80, 40); + bubble.addBubble (bubbleBody, bubbleBody.enlarged (10), yup::Point (centerX + 140, centerY), 8, 15); + masterPath.appendPath (bubble); + + // Add connecting arc + yup::Path arc; + arc.addCenteredArc (yup::Point (centerX, centerY + 60), 200, 20, 0, -yup::MathConstants::pi, 0, true); + masterPath.appendPath (arc); + + // Render with gradient-like effect + g.setFillColor (yup::Color (100, 150, 255).withAlpha (0.3f)); + g.fillPath (masterPath); + g.setStrokeColor (yup::Color (50, 100, 200)); + g.setStrokeWidth (2.0f); + g.strokePath (masterPath); + + // Add text + /* + g.setColour (yup::Color (255, 255, 255)); + g.setFont (yup::Font (14.0f, yup::Font::bold)); + g.drawText ("YUP Path System", + yup::Rectangle (centerX - 150, centerY - 10, 300, 20), + yup::Justification::centred); + + // Info text in bubble + g.setColour (yup::Color (50, 50, 50)); + g.setFont (yup::Font (10.0f)); + g.drawText ("Powerful &\nFlexible", + yup::Rectangle (centerX + 165, centerY - 20, 70, 30), + yup::Justification::centred); + */ + } }; diff --git a/modules/yup_graphics/graphics/yup_Color.h b/modules/yup_graphics/graphics/yup_Color.h index 1666d59ac..51b0b8c4c 100644 --- a/modules/yup_graphics/graphics/yup_Color.h +++ b/modules/yup_graphics/graphics/yup_Color.h @@ -516,10 +516,11 @@ class YUP_API Color @param h The hue component, normalized to [0, 1]. @param s The saturation component, normalized to [0, 1]. @param l The luminance component, normalized to [0, 1]. + @param a The alpha component, normalized to [0, 1]. @return A Color object corresponding to the given HSL values. */ - constexpr static Color fromHSL (float h, float s, float l) noexcept + constexpr static Color fromHSL (float h, float s, float l, float a = 1.0f) noexcept { auto hue2rgb = [] (float p, float q, float t) { @@ -548,7 +549,91 @@ class YUP_API Color b = hue2rgb (p, q, h - 1.0f / 3.0f); } - return { static_cast (r * 255), static_cast (g * 255), static_cast (b * 255) }; + return { + static_cast (r * 255), + static_cast (g * 255), + static_cast (b * 255), + static_cast (a * 255) + }; + } + + //============================================================================== + /** Converts the color to its HSV (Hue, Saturation, Value) components. + + This method provides a way to obtain the HSV representation of the color, which can be useful for color manipulation + and effects. The returned tuple contains the hue, saturation, and value components, respectively. + + @return A tuple consisting of hue, saturation, and value. + */ + constexpr std::tuple toHSV() const noexcept + { + const float rf = getRedFloat(); + const float gf = getGreenFloat(); + const float bf = getBlueFloat(); + + const float max = jmax(rf, gf, bf); + const float min = jmin(rf, gf, bf); + const float delta = max - min; + + float h = 0.0f; + float s = (max == 0.0f) ? 0.0f : delta / max; + float v = max; + + if (delta != 0.0f) + { + if (max == rf) + h = fmodf((gf - bf) / delta + (gf < bf ? 6.0f : 0.0f), 6.0f); + else if (max == gf) + h = (bf - rf) / delta + 2.0f; + else if (max == bf) + h = (rf - gf) / delta + 4.0f; + + h /= 6.0f; + } + + return std::make_tuple(h, s, v); + } + + /** Constructs a color from HSV values. + + This static method allows for the creation of a color from its HSV representation. + It is useful for generating colors based on more perceptual components rather than direct color component manipulation. + + @param h The hue component, normalized to [0, 1]. + @param s The saturation component, normalized to [0, 1]. + @param v The value component, normalized to [0, 1]. + @param a The alpha component, normalized to [0, 1]. + + @return A Color object corresponding to the given HSV values. + */ + constexpr static Color fromHSV(float h, float s, float v, float a = 1.0f) noexcept + { + float r = 0.0f, g = 0.0f, b = 0.0f; + + h = modulo(h, 1.0f); // ensure h is in [0,1] + const float hh = h * 6.0f; + const int i = static_cast(hh); + const float f = hh - static_cast(i); + const float p = v * (1.0f - s); + const float q = v * (1.0f - f * s); + const float t = v * (1.0f - (1.0f - f) * s); + + switch (i % 6) + { + case 0: r = v; g = t; b = p; break; + case 1: r = q; g = v; b = p; break; + case 2: r = p; g = v; b = t; break; + case 3: r = p; g = q; b = v; break; + case 4: r = t; g = p; b = v; break; + case 5: r = v; g = p; b = q; break; + } + + return { + static_cast(r * 255), + static_cast(g * 255), + static_cast(b * 255), + static_cast(a * 255) + }; } //============================================================================== diff --git a/modules/yup_graphics/primitives/yup_Path.cpp b/modules/yup_graphics/primitives/yup_Path.cpp index 2e0a51ed9..2eebeab40 100644 --- a/modules/yup_graphics/primitives/yup_Path.cpp +++ b/modules/yup_graphics/primitives/yup_Path.cpp @@ -368,6 +368,123 @@ Path& Path::addCenteredArc (const Point& center, const Size& diame return addCenteredArc (center.getX(), center.getY(), diameter.getWidth() / 2.0f, diameter.getHeight() / 2.0f, rotationOfEllipse, fromRadians, toRadians, startAsNewSubPath); } +//============================================================================== +void Path::addPolygon (Point centre, int numberOfSides, float radius, float startAngle) +{ + if (numberOfSides < 3 || radius <= 0.0f) + return; + + reserveSpace (size() + numberOfSides + 1); + + const float angleIncrement = MathConstants::twoPi / numberOfSides; + + // Start with the first vertex + float angle = startAngle; + float x = centre.getX() + radius * std::cos (angle); + float y = centre.getY() + radius * std::sin (angle); + + moveTo (x, y); + + // Add remaining vertices + for (int i = 1; i < numberOfSides; ++i) + { + angle += angleIncrement; + x = centre.getX() + radius * std::cos (angle); + y = centre.getY() + radius * std::sin (angle); + lineTo (x, y); + } + + close(); +} + +//============================================================================== +void Path::addStar (Point centre, int numberOfPoints, float innerRadius, float outerRadius, float startAngle) +{ + if (numberOfPoints < 3 || innerRadius <= 0.0f || outerRadius <= 0.0f) + return; + + reserveSpace (size() + numberOfPoints * 2 + 1); + + const float angleIncrement = MathConstants::twoPi / (numberOfPoints * 2); + + // Start with the first outer vertex + float angle = startAngle; + float x = centre.getX() + outerRadius * std::cos (angle); + float y = centre.getY() + outerRadius * std::sin (angle); + + moveTo (x, y); + + // Alternate between inner and outer vertices + for (int i = 1; i < numberOfPoints * 2; ++i) + { + angle += angleIncrement; + float currentRadius = (i % 2 == 0) ? outerRadius : innerRadius; + x = centre.getX() + currentRadius * std::cos (angle); + y = centre.getY() + currentRadius * std::sin (angle); + lineTo (x, y); + } + + close(); +} + +//============================================================================== +void Path::addBubble (Rectangle bodyArea, Rectangle maximumArea, Point arrowTipPosition, float cornerSize, float arrowBaseWidth) +{ + if (bodyArea.isEmpty() || maximumArea.isEmpty() || arrowBaseWidth <= 0.0f) + return; + + // Clamp corner size to reasonable bounds + cornerSize = jmin (cornerSize, bodyArea.getWidth() * 0.5f, bodyArea.getHeight() * 0.5f); + + // Determine which side of the body the arrow should be on + Point bodyCenter = bodyArea.getCenter(); + + // Calculate which edge the arrow should attach to + float leftDist = std::abs (arrowTipPosition.getX() - bodyArea.getX()); + float rightDist = std::abs (arrowTipPosition.getX() - bodyArea.getRight()); + float topDist = std::abs (arrowTipPosition.getY() - bodyArea.getY()); + float bottomDist = std::abs (arrowTipPosition.getY() - bodyArea.getBottom()); + + float minDist = jmin (leftDist, rightDist, topDist, bottomDist); + + // Start with the main body (rounded rectangle) + addRoundedRectangle (bodyArea, cornerSize); + + // Add arrow based on closest edge + Point arrowBase1, arrowBase2; + + if (minDist == leftDist) // Arrow on left side + { + float arrowY = jlimit (bodyArea.getY() + cornerSize, bodyArea.getBottom() - cornerSize, arrowTipPosition.getY()); + arrowBase1 = Point (bodyArea.getX(), arrowY - arrowBaseWidth * 0.5f); + arrowBase2 = Point (bodyArea.getX(), arrowY + arrowBaseWidth * 0.5f); + } + else if (minDist == rightDist) // Arrow on right side + { + float arrowY = jlimit (bodyArea.getY() + cornerSize, bodyArea.getBottom() - cornerSize, arrowTipPosition.getY()); + arrowBase1 = Point (bodyArea.getRight(), arrowY - arrowBaseWidth * 0.5f); + arrowBase2 = Point (bodyArea.getRight(), arrowY + arrowBaseWidth * 0.5f); + } + else if (minDist == topDist) // Arrow on top side + { + float arrowX = jlimit (bodyArea.getX() + cornerSize, bodyArea.getRight() - cornerSize, arrowTipPosition.getX()); + arrowBase1 = Point (arrowX - arrowBaseWidth * 0.5f, bodyArea.getY()); + arrowBase2 = Point (arrowX + arrowBaseWidth * 0.5f, bodyArea.getY()); + } + else // Arrow on bottom side + { + float arrowX = jlimit (bodyArea.getX() + cornerSize, bodyArea.getRight() - cornerSize, arrowTipPosition.getX()); + arrowBase1 = Point (arrowX - arrowBaseWidth * 0.5f, bodyArea.getBottom()); + arrowBase2 = Point (arrowX + arrowBaseWidth * 0.5f, bodyArea.getBottom()); + } + + // Add the arrow triangle + moveTo (arrowBase1); + lineTo (arrowTipPosition); + lineTo (arrowBase2); + close(); +} + //============================================================================== Path& Path::appendPath (const Path& other) { @@ -393,6 +510,12 @@ void Path::appendPath (rive::rcp other, const AffineTransf path->addRenderPath (other.get(), transform.toMat2D()); } +//============================================================================== +void Path::swapWithPath (Path& other) noexcept +{ + path.swap (other.path); +} + //============================================================================== Path& Path::transform (const AffineTransform& t) { @@ -410,12 +533,48 @@ Path Path::transformed (const AffineTransform& t) const } //============================================================================== -Rectangle Path::getBoundingBox() const +Rectangle Path::getBounds() const { const auto& aabb = path->getBounds(); return { aabb.left(), aabb.top(), aabb.width(), aabb.height() }; } +Rectangle Path::getBoundsTransformed (const AffineTransform& transform) const +{ + return getBounds().transformed (transform); +} + +//============================================================================== +void Path::scaleToFit (float x, float y, float width, float height, bool preserveProportions) noexcept +{ + if (width <= 0.0f || height <= 0.0f) + return; + + Rectangle currentBounds = getBounds(); + + if (currentBounds.isEmpty()) + return; + + float scaleX = width / currentBounds.getWidth(); + float scaleY = height / currentBounds.getHeight(); + + if (preserveProportions) + { + float scale = jmin (scaleX, scaleY); + scaleX = scaleY = scale; + } + + // Calculate translation to move to target position + float translateX = x - currentBounds.getX() * scaleX; + float translateY = y - currentBounds.getY() * scaleY; + + // Apply the transformation + AffineTransform transform = AffineTransform::scaling (scaleX, scaleY) .translated (translateX, translateY); + + *this = transformed (transform); +} + +//============================================================================== rive::RiveRenderPath* Path::getRenderPath() const { return path.get(); @@ -960,18 +1119,6 @@ bool Path::parsePathData (const String& pathData) return true; } -//============================================================================== -Rectangle Path::getBounds() const -{ - return getBoundingBox(); -} - -Rectangle Path::getBoundsTransformed (const AffineTransform& transform) const -{ - Path transformedPath = transformed (transform); - return transformedPath.getBoundingBox(); -} - //============================================================================== Point Path::getPointAlongPath (float distance) const { @@ -1186,9 +1333,9 @@ Path Path::createStrokePolygon (float strokeWidth) const Point lastMovePoint (0.0f, 0.0f); std::vector> leftSide; - leftSide.reserve (verbs.size()); + leftSide.reserve(points.size()); std::vector> rightSide; - rightSide.reserve (verbs.size()); + rightSide.reserve(points.size()); for (size_t i = 0, pointIndex = 0; i < verbs.size(); ++i) { @@ -1442,12 +1589,12 @@ void addRoundedSubpath (Path& targetPath, const std::vector>& point } // namespace -Path Path::createPathWithRoundedCorners (const Path& originalPath, float cornerRadius) +Path Path::withRoundedCorners (float cornerRadius) const { - if (cornerRadius <= 0.0f) - return originalPath; + if (cornerRadius <= 0.0f || path == nullptr) + return *this; - const auto& rawPath = originalPath.path->getRawPath(); + const auto& rawPath = path->getRawPath(); const auto& points = rawPath.points(); const auto& verbs = rawPath.verbs(); @@ -1461,8 +1608,8 @@ Path Path::createPathWithRoundedCorners (const Path& originalPath, float cornerR bool hasPreviousPoint = false; std::vector> pathPoints; + pathPoints.reserve(points.size()); - // First pass: collect all points for (size_t i = 0, pointIndex = 0; i < verbs.size(); ++i) { auto verb = verbs[i]; diff --git a/modules/yup_graphics/primitives/yup_Path.h b/modules/yup_graphics/primitives/yup_Path.h index 3c8f9040b..35c85772d 100644 --- a/modules/yup_graphics/primitives/yup_Path.h +++ b/modules/yup_graphics/primitives/yup_Path.h @@ -258,7 +258,16 @@ class YUP_API Path */ Path& addRoundedRectangle (float x, float y, float width, float height, float radiusTopLeft, float radiusTopRight, float radiusBottomLeft, float radiusBottomRight); - // TODO - doxygen + /** Adds a rounded rectangle to the path. + + This method appends a rounded rectangle with specified position, size, and corner radius + to the path. Each corner has the same radius, allowing for simple rounded corners. + The rounded rectangle is added as a closed sub-path. + + @param x The x-coordinate of the top-left corner. + @param y The y-coordinate of the top-left corner. + @param width The width of the rectangle. + */ Path& addRoundedRectangle (float x, float y, float width, float height, float radius); /** Adds a rounded rectangle described by a Rectangle object with specific corner radii to the path. @@ -275,7 +284,14 @@ class YUP_API Path */ Path& addRoundedRectangle (const Rectangle& rect, float radiusTopLeft, float radiusTopRight, float radiusBottomLeft, float radiusBottomRight); - // TODO - doxygen + /** Adds a rounded rectangle to the path. + + This method appends a rounded rectangle with specified position, size, and corner radius + to the path. Each corner has the same radius, allowing for simple rounded corners. + The rounded rectangle is added as a closed sub-path. + + @param rect The rectangle to which rounded corners are to be added. + */ Path& addRoundedRectangle (const Rectangle& rect, float radius); //============================================================================== @@ -303,13 +319,38 @@ class YUP_API Path Path& addEllipse (const Rectangle& rect); //============================================================================== - // TODO - doxygen + /** Adds a centered ellipse to the path. + + This method appends an ellipse centered at (centerX, centerY) with specified radii. + The ellipse starts and ends at the rightmost point of the ellipse, forming a complete + and closed sub-path. + + @param centerX The x-coordinate of the center of the ellipse. + @param centerY The y-coordinate of the center of the ellipse. + @param radiusX The horizontal radius of the ellipse. + */ Path& addCenteredEllipse (float centerX, float centerY, float radiusX, float radiusY); - // TODO - doxygen + /** Adds a centered ellipse to the path. + + This method appends an ellipse centered at the specified point with given radii. + The ellipse starts and ends at the rightmost point of the ellipse, forming a complete + and closed sub-path. + + @param center The center point of the ellipse. + @param radiusX The horizontal radius of the ellipse. + */ Path& addCenteredEllipse (const Point& center, float radiusX, float radiusY); - // TODO - doxygen + /** Adds a centered ellipse to the path. + + This method appends an ellipse centered at the specified point with given diameter. + The ellipse starts and ends at the rightmost point of the ellipse, forming a complete + and closed sub-path. + + @param center The center point of the ellipse. + @param diameter The diameter of the ellipse. + */ Path& addCenteredEllipse (const Point& center, const Size& diameter); //============================================================================== @@ -339,11 +380,9 @@ class YUP_API Path @param toRadians The ending angle of the arc, in radians. @param startAsNewSubPath Whether to start this as a new sub-path or continue from the current point. */ - Path& addArc (const Rectangle& rect, - float fromRadians, - float toRadians, - bool startAsNewSubPath); + Path& addArc (const Rectangle& rect, float fromRadians, float toRadians, bool startAsNewSubPath); + //============================================================================== /** Adds a centered arc to the path. This method appends an arc centered at (centerX, centerY) with specified radii and rotation, @@ -376,8 +415,83 @@ class YUP_API Path */ Path& addCenteredArc (const Point& center, float radiusX, float radiusY, float rotationOfEllipse, float fromRadians, float toRadians, bool startAsNewSubPath); + /** Adds a centered arc described by a Point and Size object to the path. + + This method appends an arc centered at the specified point with given diameter and rotation, + between two radial angles. The arc can either start as a new sub-path or connect to the current point + depending on the specified boolean. + + @param center The center point of the arc. + @param diameter The diameter of the arc. + @param rotationOfEllipse The rotation angle of the ellipse, in radians. + */ Path& addCenteredArc (const Point& center, const Size& diameter, float rotationOfEllipse, float fromRadians, float toRadians, bool startAsNewSubPath); + //============================================================================== + /** Adds a regular polygon to the path. + + This method appends a regular polygon with the specified number of sides, centered at the given point + with the specified radius. The polygon starts at the given angle. + + @param centre The center point of the polygon. + @param numberOfSides The number of sides for the polygon (minimum 3). + @param radius The radius from the center to each vertex. + @param startAngle The starting angle in radians (0.0f starts at the right). + */ + void addPolygon (Point centre, int numberOfSides, float radius, float startAngle = 0.0f); + + //============================================================================== + /** Adds a star shape to the path. + + This method appends a star shape with the specified number of points, centered at the given point + with inner and outer radii. The star starts at the given angle. + + @param centre The center point of the star. + @param numberOfPoints The number of points for the star (minimum 3). + @param innerRadius The radius from the center to the inner vertices. + @param outerRadius The radius from the center to the outer vertices. + @param startAngle The starting angle in radians (0.0f starts at the right). + */ + void addStar (Point centre, int numberOfPoints, float innerRadius, float outerRadius, float startAngle = 0.0f); + + //============================================================================== + /** Adds a speech bubble shape to the path. + + This method creates a rounded rectangle with an arrow pointing to the specified tip position, + suitable for speech bubbles or callout shapes. + + @param bodyArea The main rectangular area of the bubble. + @param maximumArea The maximum area the bubble (including arrow) can occupy. + @param arrowTipPosition The point where the arrow should point to. + @param cornerSize The radius of the rounded corners. + @param arrowBaseWidth The width of the arrow at its base. + */ + void addBubble (Rectangle bodyArea, Rectangle maximumArea, Point arrowTipPosition, float cornerSize, float arrowBaseWidth); + + //============================================================================== + /** Converts the path to a stroke polygon with specified width. + + This method generates a closed polygon that represents the stroke of this path + with the given stroke width. The resulting path can be filled to achieve the + appearance of a stroked path. + + @param strokeWidth The width of the stroke. + @return A new Path representing the stroke as a closed polygon. + */ + Path createStrokePolygon (float strokeWidth) const; + + //============================================================================== + /** Creates a new path with rounded corners applied to this path. + + This method generates a new path where sharp corners are replaced with + rounded corners of the specified radius. + + @param cornerRadius The radius of the rounded corners. + + @return A new Path with rounded corners applied. + */ + Path withRoundedCorners (float cornerRadius) const; + //============================================================================== /** Appends another path to this one. @@ -398,16 +512,52 @@ class YUP_API Path Path& appendPath (const Path& other, const AffineTransform& transform); //============================================================================== - // TODO - doxygen + /** Swaps the contents of this path with another path. + + This method efficiently swaps the internal data of this path with another path. + + @param other The path to swap with. + */ + void swapWithPath (Path& other) noexcept; + + //============================================================================== + /** Transforms the path by applying an affine transformation. + + This method applies an affine transformation to the path, modifying its shape and position. + The transformation is specified by an AffineTransform object. + + @param t The affine transformation to apply. + + @return A reference to this path after the transformation. + */ Path& transform (const AffineTransform& t); - // TODO - doxygen + /** Returns a new path with the specified transformation applied. + + This method creates a new path with the same shape as this path, but with the specified + transformation applied. The transformation is specified by an AffineTransform object. + + @param t The affine transformation to apply. + + @return A new Path with the transformation applied. + */ Path transformed (const AffineTransform& t) const; //============================================================================== - /** Returns the bounding box of this path. */ - Rectangle getBoundingBox() const; + /** Scales the path to fit within the specified bounds. + + This method transforms the path so that it fits within the given rectangular area. + If preserveProportions is true, the aspect ratio is maintained. + + @param x The x-coordinate of the target area. + @param y The y-coordinate of the target area. + @param width The width of the target area. + @param height The height of the target area. + @param preserveProportions Whether to maintain the original aspect ratio. + */ + void scaleToFit (float x, float y, float width, float height, bool preserveProportions) noexcept; + //============================================================================== /** Returns the bounding box of this path. @return The bounding rectangle that contains all points in this path. @@ -432,29 +582,15 @@ class YUP_API Path */ Point getPointAlongPath (float distance) const; - /** Converts the path to a stroke polygon with specified width. - - This method generates a closed polygon that represents the stroke of this path - with the given stroke width. The resulting path can be filled to achieve the - appearance of a stroked path. - - @param strokeWidth The width of the stroke. - @return A new Path representing the stroke as a closed polygon. - */ - Path createStrokePolygon (float strokeWidth) const; + //============================================================================== + /** Parses the path data from a string. - /** Creates a new path with rounded corners applied to this path. + This method parses the path data from a string and updates the path accordingly. - This method generates a new path where sharp corners are replaced with - rounded corners of the specified radius. + @param pathData The string containing the path data. - @param cornerRadius The radius of the rounded corners. - @return A new Path with rounded corners applied. + @return True if the path data was parsed successfully, false otherwise. */ - static Path createPathWithRoundedCorners (const Path& originalPath, float cornerRadius); - - //============================================================================== - // TODO - doxygen bool parsePathData (const String& pathData); //============================================================================== From d11ac7c8dc0b95e2974cff2ffec2a45466f57e1f Mon Sep 17 00:00:00 2001 From: kunitoki Date: Thu, 12 Jun 2025 18:11:21 +0200 Subject: [PATCH 05/11] More tweaks to path --- examples/graphics/source/examples/Paths.h | 419 +++++++----------- .../yup_graphics/primitives/yup_Rectangle.h | 33 ++ 2 files changed, 184 insertions(+), 268 deletions(-) diff --git a/examples/graphics/source/examples/Paths.h b/examples/graphics/source/examples/Paths.h index 460140e3c..c2ac60b92 100644 --- a/examples/graphics/source/examples/Paths.h +++ b/examples/graphics/source/examples/Paths.h @@ -42,48 +42,40 @@ class PathsExample : public yup::Component yup::Justification::centred); */ - auto bounds = getLocalBounds().to().reduced (20, 60); - auto sectionHeight = bounds.getHeight() / 4.0f; - auto sectionWidth = bounds.getWidth() / 3.0f; + auto bounds = getLocalBounds().to().reduced (10, 20); + auto sectionHeight = bounds.getHeight() / 6.0f; // 6 rows instead of 4 + auto sectionWidth = bounds.getWidth() / 2.0f; // 2 columns instead of 3 - // Section 1: Basic Path Operations + // Row 1: Basic Operations and Basic Shapes drawBasicPathOperations (g, yup::Rectangle (bounds.getX(), bounds.getY(), sectionWidth, sectionHeight)); - - // Section 2: Basic Shapes drawBasicShapes (g, yup::Rectangle (bounds.getX() + sectionWidth, bounds.getY(), sectionWidth, sectionHeight)); - // Section 3: Complex Shapes (Polygons, Stars, Bubbles) - drawComplexShapes (g, yup::Rectangle (bounds.getX() + sectionWidth * 2, bounds.getY(), + // Row 2: Complex Shapes and Arcs & Curves + drawComplexShapes (g, yup::Rectangle (bounds.getX(), bounds.getY() + sectionHeight, sectionWidth, sectionHeight)); - - // Section 4: Arcs and Curves - drawArcsAndCurves (g, yup::Rectangle (bounds.getX(), bounds.getY() + sectionHeight, + drawArcsAndCurves (g, yup::Rectangle (bounds.getX() + sectionWidth, bounds.getY() + sectionHeight, sectionWidth, sectionHeight)); - // Section 5: Path Transformations - drawPathTransformations (g, yup::Rectangle (bounds.getX() + sectionWidth, bounds.getY() + sectionHeight, + // Row 3: Transformations and Advanced Features + drawPathTransformations (g, yup::Rectangle (bounds.getX(), bounds.getY() + sectionHeight * 2, sectionWidth, sectionHeight)); - - // Section 6: Advanced Features - drawAdvancedFeatures (g, yup::Rectangle (bounds.getX() + sectionWidth * 2, bounds.getY() + sectionHeight, + drawAdvancedFeatures (g, yup::Rectangle (bounds.getX() + sectionWidth, bounds.getY() + sectionHeight * 2, sectionWidth, sectionHeight)); - // Section 7: Path Utilities - drawPathUtilities (g, yup::Rectangle (bounds.getX(), bounds.getY() + sectionHeight * 2, + // Row 4: Path Utilities and SVG Path Data + drawPathUtilities (g, yup::Rectangle (bounds.getX(), bounds.getY() + sectionHeight * 3, sectionWidth, sectionHeight)); - - // Section 8: SVG Path Data - drawSVGPathData (g, yup::Rectangle (bounds.getX() + sectionWidth, bounds.getY() + sectionHeight * 2, + drawSVGPathData (g, yup::Rectangle (bounds.getX() + sectionWidth, bounds.getY() + sectionHeight * 3, sectionWidth, sectionHeight)); - // Section 9: Creative Examples - drawCreativeExamples (g, yup::Rectangle (bounds.getX() + sectionWidth * 2, bounds.getY() + sectionHeight * 2, - sectionWidth, sectionHeight)); + // Row 5: Creative Examples (full width) + drawCreativeExamples (g, yup::Rectangle (bounds.getX(), bounds.getY() + sectionHeight * 4, + bounds.getWidth(), sectionHeight)); - // Section 10: Interactive Features Demo - drawInteractiveDemo (g, yup::Rectangle (bounds.getX(), bounds.getY() + sectionHeight * 3, + // Row 6: Interactive Demo (full width) + drawInteractiveDemo (g, yup::Rectangle (bounds.getX(), bounds.getY() + sectionHeight * 5, bounds.getWidth(), sectionHeight)); } @@ -96,75 +88,69 @@ class PathsExample : public yup::Component auto modifier = text.startUpdate(); modifier.setMaxSize (area.getSize()); modifier.setHorizontalAlign (yup::StyledText::center); - modifier.appendText (title, yup::ApplicationTheme::getGlobalTheme()->getDefaultFont()); + modifier.appendText (title, yup::ApplicationTheme::getGlobalTheme()->getDefaultFont(), 12.0f); } g.setFillColor (yup::Colors::white); - g.fillFittedText(text, area.removeFromTop (20)); + g.fillFittedText(text, area.removeFromTop (16)); } void drawBasicPathOperations (yup::Graphics& g, yup::Rectangle area) { - drawSectionTitle (g, "Basic Path Operations", area); - area = area.reduced (10); // .withTrimmedTop (25); + drawSectionTitle (g, "Basic Operations", area); + area = area.reduced (5).withTrimmedTop (20); yup::Path path; - float x = area.getX() + 20; - float y = area.getY() + 30; + float x = area.getX() + 10; + float y = area.getY() + 10; - // MoveTo and LineTo demo + // Smaller rectangle path.moveTo (x, y); - path.lineTo (x + 60, y); - path.lineTo (x + 60, y + 40); - path.lineTo (x, y + 40); + path.lineTo (x + 40, y); + path.lineTo (x + 40, y + 25); + path.lineTo (x, y + 25); path.close(); g.setFillColor (yup::Color (100, 150, 255)); g.fillPath (path); g.setStrokeColor (yup::Color (50, 100, 200)); - g.setStrokeWidth (2.0f); + g.setStrokeWidth (1.5f); g.strokePath (path); - // QuadTo demo + // QuadTo demo - smaller yup::Path quadPath; - x += 80; - quadPath.moveTo (x, y + 40); - quadPath.quadTo (x + 30, y, x + 60, y + 40); + x += 50; + quadPath.moveTo (x, y + 25); + quadPath.quadTo (x + 20, y, x + 40, y + 25); g.setStrokeColor (yup::Color (255, 150, 100)); - g.setStrokeWidth (3.0f); + g.setStrokeWidth (2.0f); g.strokePath (quadPath); - // CubicTo demo + // CubicTo demo - smaller yup::Path cubicPath; - x += 80; - cubicPath.moveTo (x, y + 40); - cubicPath.cubicTo (x + 60, y + 40, x + 10, y, x + 50, y); + y += 35; + x = area.getX() + 10; + cubicPath.moveTo (x, y + 25); + cubicPath.cubicTo (x + 40, y + 25, x + 5, y, x + 35, y); g.setStrokeColor (yup::Color (150, 255, 150)); - g.setStrokeWidth (3.0f); + g.setStrokeWidth (2.0f); g.strokePath (cubicPath); - - // Draw labels - //g.setColour (yup::Color (80, 80, 80)); - //g.setFont (yup::Font (10.0f)); - //g.drawText ("Rectangle", yup::Rectangle (area.getX(), y + 50, 80, 15), yup::Justification::centred); - //g.drawText ("Quadratic", yup::Rectangle (area.getX() + 80, y + 50, 80, 15), yup::Justification::centred); - //g.drawText ("Cubic Curve", yup::Rectangle (area.getX() + 160, y + 50, 80, 15), yup::Justification::centred); } void drawBasicShapes (yup::Graphics& g, yup::Rectangle area) { drawSectionTitle (g, "Basic Shapes", area); - area = area.reduced (10); // .withTrimmedTop (25); + area = area.reduced (5).withTrimmedTop (20); - float x = area.getX() + 10; - float y = area.getY() + 10; - float spacing = 80; + float x = area.getX() + 5; + float y = area.getY() + 5; + float spacing = 60; - // Rectangle + // Rectangle - smaller yup::Path rectPath; - rectPath.addRectangle (x, y, 60, 40); + rectPath.addRectangle (x, y, 40, 25); g.setFillColor (yup::Color (255, 200, 200)); g.fillPath (rectPath); g.setStrokeColor (yup::Color (200, 100, 100)); @@ -173,7 +159,7 @@ class PathsExample : public yup::Component // Rounded Rectangle yup::Path roundedRectPath; - roundedRectPath.addRoundedRectangle (x, y + 60, 60, 40, 10); + roundedRectPath.addRoundedRectangle (x + spacing, y, 40, 25, 8); g.setFillColor (yup::Color (200, 255, 200)); g.fillPath (roundedRectPath); g.setStrokeColor (yup::Color (100, 200, 100)); @@ -181,7 +167,7 @@ class PathsExample : public yup::Component // Ellipse yup::Path ellipsePath; - ellipsePath.addEllipse (x + spacing, y, 60, 40); + ellipsePath.addEllipse (x, y + 35, 40, 25); g.setFillColor (yup::Color (200, 200, 255)); g.fillPath (ellipsePath); g.setStrokeColor (yup::Color (100, 100, 200)); @@ -189,147 +175,108 @@ class PathsExample : public yup::Component // Centered Ellipse yup::Path centeredEllipsePath; - centeredEllipsePath.addCenteredEllipse (yup::Point (x + spacing + 30, y + 80), 30, 20); + centeredEllipsePath.addCenteredEllipse (yup::Point (x + spacing + 20, y + 47), 20, 12); g.setFillColor (yup::Color (255, 255, 200)); g.fillPath (centeredEllipsePath); g.setStrokeColor (yup::Color (200, 200, 100)); g.strokePath (centeredEllipsePath); - - // Labels - /* - g.setColour (yup::Color (80, 80, 80)); - g.setFont (yup::Font (9.0f)); - g.drawText ("Rectangle", yup::Rectangle (x - 10, y + 45, 80, 12), yup::Justification::centred); - g.drawText ("Rounded", yup::Rectangle (x - 10, y + 105, 80, 12), yup::Justification::centred); - g.drawText ("Ellipse", yup::Rectangle (x + spacing - 10, y + 45, 80, 12), yup::Justification::centred); - g.drawText ("Centered", yup::Rectangle (x + spacing - 10, y + 105, 80, 12), yup::Justification::centred); - */ } void drawComplexShapes (yup::Graphics& g, yup::Rectangle area) { drawSectionTitle (g, "Complex Shapes", area); - area = area.reduced (10); // .withTrimmedTop (25); + area = area.reduced (5).withTrimmedTop (20); - float x = area.getX() + 40; - float y = area.getY() + 40; + float x = area.getX() + 30; + float y = area.getY() + 25; - // Pentagon + // Pentagon - smaller yup::Path pentagonPath; - pentagonPath.addPolygon (yup::Point (x, y), 5, 25, -yup::MathConstants::halfPi); + pentagonPath.addPolygon (yup::Point (x, y), 5, 18, -yup::MathConstants::halfPi); g.setFillColor (yup::Color (255, 180, 120)); g.fillPath (pentagonPath); g.setStrokeColor (yup::Color (200, 120, 60)); - g.setStrokeWidth (1.5f); + g.setStrokeWidth (1.0f); g.strokePath (pentagonPath); - // Hexagon - yup::Path hexagonPath; - hexagonPath.addPolygon (yup::Point (x + 80, y), 6, 25, 0); - g.setFillColor (yup::Color (180, 255, 180)); - g.fillPath (hexagonPath); - g.setStrokeColor (yup::Color (120, 200, 120)); - g.strokePath (hexagonPath); - // Star yup::Path starPath; - starPath.addStar (yup::Point (x + 160, y), 5, 15, 25, -yup::MathConstants::halfPi); + starPath.addStar (yup::Point (x + 60, y), 5, 10, 18, -yup::MathConstants::halfPi); g.setFillColor (yup::Color (255, 255, 120)); g.fillPath (starPath); g.setStrokeColor (yup::Color (200, 200, 60)); g.strokePath (starPath); - // Speech Bubble + // Speech Bubble - smaller yup::Path bubblePath; - yup::Rectangle bodyArea (x - 20, y + 70, 80, 40); - yup::Rectangle maxArea = bodyArea.enlarged(20); - yup::Point tipPosition (x + 70, y + 120); - bubblePath.addBubble (bodyArea, maxArea, tipPosition, 8, 12); + yup::Rectangle bodyArea (x - 15, y + 30, 50, 25); + yup::Rectangle maxArea = bodyArea.enlarged(10); + yup::Point tipPosition (x + 45, y + 65); + bubblePath.addBubble (bodyArea, maxArea, tipPosition, 5, 8); g.setFillColor (yup::Color (220, 240, 255)); g.fillPath (bubblePath); g.setStrokeColor (yup::Color (100, 150, 200)); g.strokePath (bubblePath); - // Triangle (3-sided polygon) + // Triangle yup::Path trianglePath; - trianglePath.addPolygon (yup::Point (x + 120, y + 85), 3, 20, -yup::MathConstants::halfPi); + trianglePath.addPolygon (yup::Point (x + 75, y + 42), 3, 15, -yup::MathConstants::halfPi); g.setFillColor (yup::Color (255, 200, 255)); g.fillPath (trianglePath); g.setStrokeColor (yup::Color (200, 100, 200)); g.strokePath (trianglePath); - - // Labels - /* - g.setColour (yup::Color (80, 80, 80)); - g.setFont (yup::Font (8.0f)); - g.drawText ("Pentagon", yup::Rectangle (x - 25, y + 30, 50, 12), yup::Justification::centred); - g.drawText ("Hexagon", yup::Rectangle (x + 55, y + 30, 50, 12), yup::Justification::centred); - g.drawText ("Star", yup::Rectangle (x + 135, y + 30, 50, 12), yup::Justification::centred); - g.drawText ("Bubble", yup::Rectangle (x - 25, y + 120, 50, 12), yup::Justification::centred); - g.drawText ("Triangle", yup::Rectangle (x + 95, y + 110, 50, 12), yup::Justification::centred); - */ } void drawArcsAndCurves (yup::Graphics& g, yup::Rectangle area) { drawSectionTitle (g, "Arcs & Curves", area); - area = area.reduced (10); // .withTrimmedTop (25); + area = area.reduced (5).withTrimmedTop (20); - float x = area.getX() + 30; - float y = area.getY() + 30; + float x = area.getX() + 15; + float y = area.getY() + 15; - // Simple Arc + // Simple Arc - smaller yup::Path arcPath; - arcPath.addArc (x, y, 60, 60, 0, yup::MathConstants::halfPi, true); + arcPath.addArc (x, y, 40, 40, 0, yup::MathConstants::halfPi, true); g.setStrokeColor (yup::Color (255, 150, 150)); - g.setStrokeWidth (3.0f); + g.setStrokeWidth (2.0f); g.strokePath (arcPath); // Centered Arc with rotation yup::Path centeredArcPath; - centeredArcPath.addCenteredArc (yup::Point (x + 100, y + 30), 25, 15, - yup::MathConstants::pi / 4, 0, yup::MathConstants::pi, true); + centeredArcPath.addCenteredArc (yup::Point (x + 70, y + 20), 18, 12, + yup::MathConstants::pi / 4, 0, yup::MathConstants::pi, true); g.setStrokeColor (yup::Color (150, 255, 150)); - g.setStrokeWidth (3.0f); + g.setStrokeWidth (2.0f); g.strokePath (centeredArcPath); // Complete circle using arc yup::Path circlePath; - circlePath.addArc (x + 160, y, 50, 50, 0, yup::MathConstants::twoPi, true); + circlePath.addArc (x + 100, y, 35, 35, 0, yup::MathConstants::twoPi, true); g.setFillColor (yup::Color (150, 150, 255)); g.fillPath (circlePath); // Complex curve combination yup::Path complexPath; - complexPath.moveTo (x, y + 80); - complexPath.quadTo (x + 50, y + 60, x + 100, y + 80); - complexPath.cubicTo (x + 150, y + 80, x + 180, y + 120, x + 200, y + 100); + complexPath.moveTo (x, y + 50); + complexPath.quadTo (x + 35, y + 35, x + 70, y + 50); + complexPath.cubicTo (x + 100, y + 50, x + 120, y + 75, x + 130, y + 60); g.setStrokeColor (yup::Color (255, 200, 100)); - g.setStrokeWidth (2.0f); + g.setStrokeWidth (1.5f); g.strokePath (complexPath); - - // Labels - /* - g.setColour (yup::Color (80, 80, 80)); - g.setFont (yup::Font (9.0f)); - g.drawText ("Arc", yup::Rectangle (x - 10, y + 70, 60, 12), yup::Justification::centred); - g.drawText ("Rotated Arc", yup::Rectangle (x + 70, y + 70, 60, 12), yup::Justification::centred); - g.drawText ("Circle", yup::Rectangle (x + 140, y + 60, 60, 12), yup::Justification::centred); - g.drawText ("Complex Curves", yup::Rectangle (x + 50, y + 110, 100, 12), yup::Justification::centred); - */ } void drawPathTransformations (yup::Graphics& g, yup::Rectangle area) { drawSectionTitle (g, "Transformations", area); - area = area.reduced (10); // .withTrimmedTop (25); + area = area.reduced (5).withTrimmedTop (20); - float x = area.getX() + 20; - float y = area.getY() + 20; + float x = area.getX() + 15; + float y = area.getY() + 15; - // Original shape + // Original shape - smaller yup::Path originalPath; - originalPath.addStar (yup::Point (x + 30, y + 30), 5, 15, 25, 0); + originalPath.addStar (yup::Point (x + 20, y + 20), 5, 10, 18, 0); g.setFillColor (yup::Color (200, 200, 200)); g.fillPath (originalPath); g.setStrokeColor (yup::Color (100, 100, 100)); @@ -337,8 +284,8 @@ class PathsExample : public yup::Component g.strokePath (originalPath); // Scaled version - yup::Path scaledPath = originalPath.transformed (yup::AffineTransform::scaling (0.7f, 1.3f)); - scaledPath.transform (yup::AffineTransform::translation (80, 0)); + yup::Path scaledPath = originalPath.transformed (yup::AffineTransform::scaling (0.6f, 1.2f)); + scaledPath.transform (yup::AffineTransform::translation (50, 0)); g.setFillColor (yup::Color (255, 200, 200)); g.fillPath (scaledPath); g.setStrokeColor (yup::Color (200, 100, 100)); @@ -346,47 +293,37 @@ class PathsExample : public yup::Component // Rotated version yup::Path rotatedPath = originalPath.transformed ( - yup::AffineTransform::rotation (yup::MathConstants::pi / 4, x + 30, y + 30)); - rotatedPath.transform (yup::AffineTransform::translation (160, 0)); + yup::AffineTransform::rotation (yup::MathConstants::pi / 4, x + 20, y + 20)); + rotatedPath.transform (yup::AffineTransform::translation (100, 0)); g.setFillColor (yup::Color (200, 255, 200)); g.fillPath (rotatedPath); g.setStrokeColor (yup::Color (100, 200, 100)); g.strokePath (rotatedPath); - // ScaleToFit demo + // ScaleToFit demo - smaller yup::Path scaleToFitPath; - scaleToFitPath.addPolygon (yup::Point (0, 0), 6, 20, 0); - scaleToFitPath.scaleToFit (x, y + 80, 180, 30, true); + scaleToFitPath.addPolygon (yup::Point (0, 0), 6, 15, 0); + scaleToFitPath.scaleToFit (x, y + 50, 120, 20, true); g.setFillColor (yup::Color (200, 200, 255)); g.fillPath (scaleToFitPath); g.setStrokeColor (yup::Color (100, 100, 200)); g.strokePath (scaleToFitPath); - - // Labels - /* - g.setColour (yup::Color (80, 80, 80)); - g.setFont (yup::Font (8.0f)); - g.drawText ("Original", yup::Rectangle (x - 5, y + 60, 60, 12), yup::Justification::centred); - g.drawText ("Scaled", yup::Rectangle (x + 55, y + 60, 60, 12), yup::Justification::centred); - g.drawText ("Rotated", yup::Rectangle (x + 135, y + 60, 60, 12), yup::Justification::centred); - g.drawText ("Scale to Fit", yup::Rectangle (x + 40, y + 115, 100, 12), yup::Justification::centred); - */ } void drawAdvancedFeatures (yup::Graphics& g, yup::Rectangle area) { drawSectionTitle (g, "Advanced Features", area); - area = area.reduced (10); // .withTrimmedTop (25); + area = area.reduced (5).withTrimmedTop (20); - float x = area.getX() + 20; - float y = area.getY() + 20; + float x = area.getX() + 10; + float y = area.getY() + 10; - // Stroke polygon demo + // Stroke polygon demo - smaller yup::Path originalCurve; - originalCurve.moveTo (x, y + 40); - originalCurve.quadTo (x + 40, y, x + 80, y + 40); + originalCurve.moveTo (x, y + 25); + originalCurve.quadTo (x + 25, y, x + 50, y + 25); - yup::Path strokePolygon = originalCurve.createStrokePolygon (8.0f); + yup::Path strokePolygon = originalCurve.createStrokePolygon (5.0f); g.setFillColor (yup::Color (255, 220, 180)); g.fillPath (strokePolygon); g.setStrokeColor (yup::Color (200, 150, 100)); @@ -395,57 +332,48 @@ class PathsExample : public yup::Component // Rounded corners demo yup::Path sharpPath; - sharpPath.addRectangle (x + 100, y, 60, 60); - yup::Path roundedPath = sharpPath.withRoundedCorners (10.0f); + sharpPath.addRectangle (x + 60, y, 40, 40); + yup::Path roundedPath = sharpPath.withRoundedCorners (8.0f); g.setFillColor (yup::Color (200, 255, 220)); g.fillPath (roundedPath); g.setStrokeColor (yup::Color (100, 200, 120)); g.strokePath (sharpPath); - // Point along path demo + // Point along path demo - smaller yup::Path curvePath; - curvePath.moveTo (x, y + 80); - curvePath.cubicTo (x + 60, y + 80, x + 120, y + 120, x + 180, y + 100); + curvePath.moveTo (x, y + 50); + curvePath.cubicTo (x + 40, y + 50, x + 80, y + 75, x + 120, y + 65); g.setStrokeColor (yup::Color (100, 150, 255)); - g.setStrokeWidth (2.0f); + g.setStrokeWidth (1.5f); g.strokePath (curvePath); - // Draw points along the path - for (float t = 0.0f; t <= 1.0f; t += 0.2f) + // Draw points along the path - smaller + for (float t = 0.0f; t <= 1.0f; t += 0.25f) { yup::Point point = curvePath.getPointAlongPath (t); yup::Path pointPath; - pointPath.addCenteredEllipse (point, 4, 4); + pointPath.addCenteredEllipse (point, 3, 3); g.setFillColor (yup::Color (255, 100, 100)); g.fillPath (pointPath); } - - // Labels - /* - g.setColour (yup::Color (80, 80, 80)); - g.setFont (yup::Font (8.0f)); - g.drawText ("Stroke Polygon", yup::Rectangle (x - 10, y + 60, 100, 12), yup::Justification::centred); - g.drawText ("Rounded Corners", yup::Rectangle (x + 90, y + 70, 80, 12), yup::Justification::centred); - g.drawText ("Points Along Path", yup::Rectangle (x + 40, y + 130, 100, 12), yup::Justification::centred); - */ } void drawPathUtilities (yup::Graphics& g, yup::Rectangle area) { drawSectionTitle (g, "Path Utilities", area); - area = area.reduced (10); // .withTrimmedTop (25); + area = area.reduced (5).withTrimmedTop (20); - float x = area.getX() + 20; - float y = area.getY() + 20; + float x = area.getX() + 10; + float y = area.getY() + 10; - // AppendPath demo + // AppendPath demo - smaller yup::Path path1; - path1.addEllipse (x, y, 40, 40); + path1.addEllipse (x, y, 30, 30); yup::Path path2; - path2.addRectangle (x + 20, y + 20, 40, 40); + path2.addRectangle (x + 15, y + 15, 30, 30); path1.appendPath (path2); g.setFillColor (yup::Color (255, 200, 255)); @@ -456,7 +384,7 @@ class PathsExample : public yup::Component // Bounds demonstration yup::Path boundsPath; - boundsPath.addStar (yup::Point (x + 120, y + 30), 5, 15, 25, 0); + boundsPath.addStar (yup::Point (x + 80, y + 20), 5, 10, 18, 0); yup::Rectangle bounds = boundsPath.getBounds(); g.setFillColor (yup::Color (180, 255, 180)); @@ -467,40 +395,27 @@ class PathsExample : public yup::Component // Size and clear demo yup::Path infoPath; - infoPath.addPolygon (yup::Point (x + 60, y + 80), 8, 20, 0); + infoPath.addPolygon (yup::Point (x + 40, y + 50), 6, 15, 0); g.setFillColor (yup::Color (200, 220, 255)); g.fillPath (infoPath); - - // Display path info - /* - g.setColour (yup::Color (80, 80, 80)); - g.setFont (yup::Font (8.0f)); - yup::String sizeText = "Size: " + yup::String (infoPath.size()); - g.drawText (sizeText, yup::Rectangle (x + 20, y + 110, 100, 12), yup::Justification::left); - - // Labels - g.drawText ("Append Paths", yup::Rectangle (x - 10, y + 70, 80, 12), yup::Justification::centred); - g.drawText ("Bounds", yup::Rectangle (x + 90, y + 70, 80, 12), yup::Justification::centred); - g.drawText ("Path Info", yup::Rectangle (x + 20, y + 130, 80, 12), yup::Justification::centred); - */ } void drawSVGPathData (yup::Graphics& g, yup::Rectangle area) { drawSectionTitle (g, "SVG Path Data", area); - area = area.reduced (10); // .withTrimmedTop (25); + area = area.reduced (5).withTrimmedTop (20); - float x = area.getX() + 20; - float y = area.getY() + 20; + float x = area.getX() + 10; + float y = area.getY() + 10; - // Parse SVG path data examples + // Parse SVG path data examples - smaller scale yup::Path svgHeart; svgHeart.parsePathData ("M12,21.35l-1.45-1.32C5.4,15.36,2,12.28,2,8.5 C2,5.42,4.42,3,7.5,3c1.74,0,3.41,0.81,4.5,2.09C13.09,3.81,14.76,3,16.5,3 C19.58,3,22,5.42,22,8.5c0,3.78-3.4,6.86-8.55,11.54L12,21.35z"); - // Scale and position the heart + // Scale and position the heart - smaller yup::Rectangle heartBounds = svgHeart.getBounds(); - float scale = 3.0f; + float scale = 1.8f; svgHeart.transform (yup::AffineTransform::scaling (scale)); svgHeart.transform (yup::AffineTransform::translation (x - heartBounds.getX() * scale, y - heartBounds.getY() * scale)); @@ -510,65 +425,57 @@ class PathsExample : public yup::Component g.setStrokeWidth (1.0f); g.strokePath (svgHeart); - // Simple SVG path + // Simple SVG path - smaller yup::Path svgTriangle; svgTriangle.parsePathData ("M100,20 L180,160 L20,160 Z"); - svgTriangle.scaleToFit (x + 100, y, 80, 80, true); + svgTriangle.scaleToFit (x + 60, y, 50, 50, true); g.setFillColor (yup::Color (150, 255, 150)); g.fillPath (svgTriangle); g.setStrokeColor (yup::Color (100, 200, 100)); g.setStrokeWidth (1.0f); g.strokePath (svgTriangle); - - // Labels - /* - g.setColour (yup::Color (80, 80, 80)); - g.setFont (yup::Font (8.0f)); - g.drawText ("SVG Heart", yup::Rectangle (x - 10, y + 90, 80, 12), yup::Justification::centred); - g.drawText ("SVG Triangle", yup::Rectangle (x + 90, y + 90, 80, 12), yup::Justification::centred); - */ } void drawCreativeExamples (yup::Graphics& g, yup::Rectangle area) { drawSectionTitle (g, "Creative Examples", area); - area = area.reduced (10); // .withTrimmedTop (25); + area = area.reduced (5).withTrimmedTop (20); - float x = area.getX() + 20; - float y = area.getY() + 20; + float x = area.getX() + 40; + float y = area.getY() + 40; - // Flower pattern using multiple shapes - yup::Point center (x + 60, y + 60); + // Flower pattern using multiple shapes - smaller + yup::Point center (x, y); - // Petals - for (int i = 0; i < 8; ++i) + // Petals - smaller + for (int i = 0; i < 6; ++i) { - float angle = i * yup::MathConstants::twoPi / 8.0f; + float angle = i * yup::MathConstants::twoPi / 6.0f; yup::Path petal; - petal.addCenteredEllipse (yup::Point (0, 0), 15, 30); + petal.addCenteredEllipse (yup::Point (0, 0), 8, 18); petal.transform (yup::AffineTransform::rotation (angle)); petal.transform (yup::AffineTransform::translation (center.getX(), center.getY())); - float hue = i / 8.0f; + float hue = i / 6.0f; g.setFillColor (yup::Color::fromHSV (hue, 0.7f, 1.0f, 0.8f)); g.fillPath (petal); } // Center yup::Path flowerCenter; - flowerCenter.addCenteredEllipse (center, 12, 12); + flowerCenter.addCenteredEllipse (center, 8, 8); g.setFillColor (yup::Color (255, 255, 100)); g.fillPath (flowerCenter); g.setStrokeColor (yup::Color (200, 200, 50)); g.setStrokeWidth (1.0f); g.strokePath (flowerCenter); - // Gear shape using polygons + // Gear shape using polygons - smaller yup::Path gear; - gear.addPolygon (yup::Point (x + 180, y + 60), 12, 35, 0); + gear.addPolygon (yup::Point (x + 120, y), 10, 25, 0); yup::Path innerGear; - innerGear.addPolygon (yup::Point (x + 180, y + 60), 12, 25, yup::MathConstants::pi / 12); + innerGear.addPolygon (yup::Point (x + 120, y), 10, 18, yup::MathConstants::pi / 10); g.setFillColor (yup::Color (180, 180, 180)); g.fillPath (gear); @@ -579,74 +486,50 @@ class PathsExample : public yup::Component // Center hole yup::Path centerHole; - centerHole.addCenteredEllipse (yup::Point (x + 180, y + 60), 10, 10); + centerHole.addCenteredEllipse (yup::Point (x + 120, y), 6, 6); g.setFillColor (yup::Color (245, 245, 250)); g.fillPath (centerHole); - - // Labels - /* - g.setColour (yup::Color (80, 80, 80)); - g.setFont (yup::Font (8.0f)); - g.drawText ("Flower", yup::Rectangle (x + 20, y + 130, 80, 12), yup::Justification::centred); - g.drawText ("Gear", yup::Rectangle (x + 140, y + 130, 80, 12), yup::Justification::centred); - */ } void drawInteractiveDemo (yup::Graphics& g, yup::Rectangle area) { - drawSectionTitle (g, "Interactive Path Builder Demo", area); - area = area.reduced (10); // .withTrimmedTop (25); + drawSectionTitle (g, "Interactive Demo", area); + area = area.reduced (5).withTrimmedTop (20); - // Create a complex path combining multiple features + // Create a complex path combining multiple features - mobile optimized yup::Path masterPath; float centerX = area.getCenterX(); float centerY = area.getCenterY(); - // Base shape - rounded rectangle - masterPath.addRoundedRectangle (centerX - 150, centerY - 40, 300, 80, 20); + // Base shape - rounded rectangle (smaller) + masterPath.addRoundedRectangle (centerX - 100, centerY - 25, 200, 50, 12); - // Add decorative elements - for (int i = 0; i < 5; ++i) + // Add decorative elements (fewer) + for (int i = 0; i < 3; ++i) { yup::Path star; - float x = centerX - 120 + i * 60; - star.addStar (yup::Point (x, centerY - 60), 5, 8, 15, 0); + float x = centerX - 60 + i * 60; + star.addStar (yup::Point (x, centerY - 40), 5, 5, 10, 0); masterPath.appendPath (star); } - // Add speech bubble + // Add speech bubble (smaller) yup::Path bubble; - yup::Rectangle bubbleBody (centerX + 160, centerY - 30, 80, 40); - bubble.addBubble (bubbleBody, bubbleBody.enlarged (10), yup::Point (centerX + 140, centerY), 8, 15); + yup::Rectangle bubbleBody (centerX + 110, centerY - 20, 60, 30); + bubble.addBubble (bubbleBody, bubbleBody.enlarged (8), yup::Point (centerX + 90, centerY), 6, 10); masterPath.appendPath (bubble); - // Add connecting arc + // Add connecting arc (smaller) yup::Path arc; - arc.addCenteredArc (yup::Point (centerX, centerY + 60), 200, 20, 0, -yup::MathConstants::pi, 0, true); + arc.addCenteredArc (yup::Point (centerX, centerY + 40), 150, 15, 0, -yup::MathConstants::pi, 0, true); masterPath.appendPath (arc); // Render with gradient-like effect g.setFillColor (yup::Color (100, 150, 255).withAlpha (0.3f)); g.fillPath (masterPath); g.setStrokeColor (yup::Color (50, 100, 200)); - g.setStrokeWidth (2.0f); + g.setStrokeWidth (1.5f); g.strokePath (masterPath); - - // Add text - /* - g.setColour (yup::Color (255, 255, 255)); - g.setFont (yup::Font (14.0f, yup::Font::bold)); - g.drawText ("YUP Path System", - yup::Rectangle (centerX - 150, centerY - 10, 300, 20), - yup::Justification::centred); - - // Info text in bubble - g.setColour (yup::Color (50, 50, 50)); - g.setFont (yup::Font (10.0f)); - g.drawText ("Powerful &\nFlexible", - yup::Rectangle (centerX + 165, centerY - 20, 70, 30), - yup::Justification::centred); - */ } }; diff --git a/modules/yup_graphics/primitives/yup_Rectangle.h b/modules/yup_graphics/primitives/yup_Rectangle.h index 6408bc47d..86ac29d76 100644 --- a/modules/yup_graphics/primitives/yup_Rectangle.h +++ b/modules/yup_graphics/primitives/yup_Rectangle.h @@ -197,6 +197,17 @@ class YUP_API Rectangle return xy.getX(); } + [[nodiscard]] constexpr Rectangle withLeft (ValueType amount) const noexcept + { + return { xy.withX (amount), size }; + } + + [[nodiscard]] constexpr Rectangle withTrimmedLeft (ValueType amountToTrim) const noexcept + { + return withLeft (xy.getX() + amountToTrim); + } + + //============================================================================== /** Returns the top-coordinate of the rectangle's top-left corner. @return The top-coordinate value. @@ -206,6 +217,17 @@ class YUP_API Rectangle return xy.getY(); } + [[nodiscard]] constexpr Rectangle withTop (ValueType amount) const noexcept + { + return { xy.withY (amount), size }; + } + + [[nodiscard]] constexpr Rectangle withTrimmedTop (ValueType amountToTrim) const noexcept + { + return withTop (xy.getY() + amountToTrim); + } + + //============================================================================== /** Returns the right-coordinate of the rectangle's bottom-right corner. @return The right-coordinate value. @@ -215,6 +237,12 @@ class YUP_API Rectangle return xy.getX() + size.getWidth(); } + [[nodiscard]] constexpr Rectangle withTrimmedRight (ValueType amountToTrim) const noexcept + { + return withWidth (size.getWidth() - amountToTrim); + } + + //============================================================================== /** Returns the bottom-coordinate of the rectangle's bottom-right corner. @return The bottom-coordinate value. @@ -224,6 +252,11 @@ class YUP_API Rectangle return xy.getY() + size.getHeight(); } + [[nodiscard]] constexpr Rectangle withTrimmedBottom (ValueType amountToTrim) const noexcept + { + return withHeight (size.getHeight() - amountToTrim); + } + //============================================================================== /** Returns the width of the rectangle. From f9da47072b4d8b12e499199c9243fc04c10f3426 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Thu, 12 Jun 2025 18:17:21 +0200 Subject: [PATCH 06/11] More doxygen --- .../yup_graphics/primitives/yup_Rectangle.h | 282 ++++++++++++++++-- 1 file changed, 262 insertions(+), 20 deletions(-) diff --git a/modules/yup_graphics/primitives/yup_Rectangle.h b/modules/yup_graphics/primitives/yup_Rectangle.h index 86ac29d76..a2db23052 100644 --- a/modules/yup_graphics/primitives/yup_Rectangle.h +++ b/modules/yup_graphics/primitives/yup_Rectangle.h @@ -137,7 +137,12 @@ class YUP_API Rectangle return xy.getX(); } - // TODO - doxygen + /** Sets the x-coordinate of the rectangle's top-left corner. + + @param newX The new x-coordinate for the top-left corner. + + @return A reference to this rectangle to allow method chaining. + */ constexpr Rectangle& setX (ValueType newX) noexcept { xy.setX (newX); @@ -167,7 +172,12 @@ class YUP_API Rectangle return xy.getY(); } - // TODO - doxygen + /** Sets the y-coordinate of the rectangle's top-left corner. + + @param newY The new y-coordinate for the top-left corner. + + @return A reference to this rectangle to allow method chaining. + */ constexpr Rectangle& setY (ValueType newY) noexcept { xy.setY (newY); @@ -197,11 +207,27 @@ class YUP_API Rectangle return xy.getX(); } + /** Returns a new rectangle with the left-coordinate of the top-left corner set to a new value. + + This method creates a new rectangle with the same size and y-coordinate, but with the left-coordinate of the top-left corner changed to the specified value. + + @param amount The new left-coordinate for the top-left corner. + + @return A new rectangle with the updated left-coordinate. + */ [[nodiscard]] constexpr Rectangle withLeft (ValueType amount) const noexcept { return { xy.withX (amount), size }; } + /** Returns a new rectangle with the left-coordinate of the top-left corner trimmed by a specified amount. + + This method creates a new rectangle with the same size and y-coordinate, but with the left-coordinate of the top-left corner reduced by the specified amount. + + @param amountToTrim The amount to trim from the left-coordinate. + + @return A new rectangle with the updated left-coordinate. + */ [[nodiscard]] constexpr Rectangle withTrimmedLeft (ValueType amountToTrim) const noexcept { return withLeft (xy.getX() + amountToTrim); @@ -217,11 +243,27 @@ class YUP_API Rectangle return xy.getY(); } + /** Returns a new rectangle with the top-coordinate of the top-left corner set to a new value. + + This method creates a new rectangle with the same size and x-coordinate, but with the top-coordinate of the top-left corner changed to the specified value. + + @param amount The new top-coordinate for the top-left corner. + + @return A new rectangle with the updated top-coordinate. + */ [[nodiscard]] constexpr Rectangle withTop (ValueType amount) const noexcept { return { xy.withY (amount), size }; } + /** Returns a new rectangle with the top-coordinate of the top-left corner trimmed by a specified amount. + + This method creates a new rectangle with the same size and x-coordinate, but with the top-coordinate of the top-left corner reduced by the specified amount. + + @param amountToTrim The amount to trim from the top-coordinate. + + @return A new rectangle with the updated top-coordinate. + */ [[nodiscard]] constexpr Rectangle withTrimmedTop (ValueType amountToTrim) const noexcept { return withTop (xy.getY() + amountToTrim); @@ -237,6 +279,14 @@ class YUP_API Rectangle return xy.getX() + size.getWidth(); } + /** Returns a new rectangle with the right-coordinate of the bottom-right corner trimmed by a specified amount. + + This method creates a new rectangle with the same size and y-coordinate, but with the right-coordinate of the bottom-right corner reduced by the specified amount. + + @param amountToTrim The amount to trim from the right-coordinate. + + @return A new rectangle with the updated right-coordinate. + */ [[nodiscard]] constexpr Rectangle withTrimmedRight (ValueType amountToTrim) const noexcept { return withWidth (size.getWidth() - amountToTrim); @@ -252,6 +302,14 @@ class YUP_API Rectangle return xy.getY() + size.getHeight(); } + /** Returns a new rectangle with the bottom-coordinate of the bottom-right corner trimmed by a specified amount. + + This method creates a new rectangle with the same size and x-coordinate, but with the bottom-coordinate of the bottom-right corner reduced by the specified amount. + + @param amountToTrim The amount to trim from the bottom-coordinate. + + @return A new rectangle with the updated bottom-coordinate. + */ [[nodiscard]] constexpr Rectangle withTrimmedBottom (ValueType amountToTrim) const noexcept { return withHeight (size.getHeight() - amountToTrim); @@ -267,7 +325,12 @@ class YUP_API Rectangle return size.getWidth(); } - // TODO - doxygen + /** Sets the width of the rectangle. + + @param newWidth The new width for the rectangle. + + @return A reference to this rectangle to allow method chaining. + */ constexpr Rectangle& setWidth (ValueType newWidth) noexcept { size.setWidth (newWidth); @@ -288,6 +351,15 @@ class YUP_API Rectangle return { xy, size.withWidth (newWidth) }; } + /** Returns a new rectangle with the width set to a specified proportion of the original width. + + This method creates a new rectangle with the same position but changes the width to a specified proportion of the original width. + The height remains unchanged. + + @param proportion The proportion of the original width to use for the new width. + + @return The new width value. + */ [[nodiscard]] constexpr ValueType proportionOfWidth (float proportion) const noexcept { return static_cast (size.getWidth() * proportion); @@ -303,7 +375,12 @@ class YUP_API Rectangle return size.getHeight(); } - // TODO - doxygen + /** Sets the height of the rectangle. + + @param newHeight The new height for the rectangle. + + @return A reference to this rectangle to allow method chaining. + */ constexpr Rectangle& setHeight (ValueType newHeight) noexcept { size.setHeight (newHeight); @@ -324,6 +401,15 @@ class YUP_API Rectangle return { xy, size.withHeight (newHeight) }; } + /** Returns a new rectangle with the height set to a specified proportion of the original height. + + This method creates a new rectangle with the same position but changes the height to a specified proportion of the original height. + The width remains unchanged. + + @param proportion The proportion of the original height to use for the new height. + + @return The new height value. + */ [[nodiscard]] constexpr ValueType proportionOfHeight (float proportion) const noexcept { return static_cast (size.getHeight() * proportion); @@ -657,6 +743,12 @@ class YUP_API Rectangle return xy.getX() + size.getWidth() / static_cast (2); } + /** Sets the center X of the rectangle. + + @param centerX The new center X for the rectangle. + + @return A reference to this rectangle to allow method chaining. + */ constexpr Rectangle& setCenterX (ValueType centerX) noexcept { xy.setX (centerX - size.getWidth() / static_cast (2)); @@ -673,6 +765,12 @@ class YUP_API Rectangle return xy.getY() + size.getHeight() / static_cast (2); } + /** Sets the center Y of the rectangle. + + @param centerY The new center Y for the rectangle. + + @return A reference to this rectangle to allow method chaining. + */ constexpr Rectangle& setCenterY (ValueType centerY) noexcept { xy.setY (centerY - size.getHeight() / static_cast (2)); @@ -751,6 +849,14 @@ class YUP_API Rectangle return result; } + /** Returns a new rectangle with its center X set to the specified value. + + This method creates a new rectangle with the same size but its position adjusted so that its center X matches the given value. + + @param centerX The new center X for the rectangle. + + @return A new rectangle with the updated center X. + */ [[nodiscard]] constexpr Rectangle withCenterX (ValueType centerX) noexcept { Rectangle result = *this; @@ -758,6 +864,14 @@ class YUP_API Rectangle return result; } + /** Returns a new rectangle with its center Y set to the specified value. + + This method creates a new rectangle with the same size but its position adjusted so that its center Y matches the given value. + + @param centerY The new center Y for the rectangle. + + @return A new rectangle with the updated center Y. + */ [[nodiscard]] constexpr Rectangle withCenterY (ValueType centerY) noexcept { Rectangle result = *this; @@ -896,7 +1010,15 @@ class YUP_API Rectangle return *this; } - // TODO - doxygen + /** Returns a new rectangle translated by the specified x and y offsets. + + This method creates a new rectangle with the same size but its position adjusted by the specified deltas. + + @param deltaX The amount to add to the x-coordinate. + @param deltaY The amount to add to the y-coordinate. + + @return A new rectangle with the updated position. + */ [[nodiscard]] constexpr Rectangle translated (ValueType deltaX, ValueType deltaY) const noexcept { return { xy.translated (deltaX, deltaY), size }; @@ -1094,7 +1216,17 @@ class YUP_API Rectangle return *this; } - // TODO - doxygen + /** Reduces the size of the rectangle by different amounts on all sides. + + This method shrinks the rectangle's width by left and right, and height by top and bottom, reducing the size from all edges by these respective amounts. + + @param left The amount to reduce the width by on the left side. + @param top The amount to reduce the height by on the top side. + @param right The amount to reduce the width by on the right side. + @param bottom The amount to reduce the height by on the bottom side. + + @return A reference to this rectangle to allow method chaining. + */ constexpr Rectangle& reduce (ValueType left, ValueType top, ValueType right, ValueType bottom) noexcept { xy = { xy.getX() + left, xy.getY() + top }; @@ -1135,7 +1267,17 @@ class YUP_API Rectangle return result; } - // TODO - doxygen + /** Returns a new rectangle with its size reduced by different amounts on all sides. + + This method creates a new rectangle with the same position but its width and height shrunk by the specified left, top, right, and bottom amounts. + + @param left The amount to reduce the width by on the left side. + @param top The amount to reduce the height by on the top side. + @param right The amount to reduce the width by on the right side. + @param bottom The amount to reduce the height by on the bottom side. + + @return A new rectangle with the reduced size. + */ [[nodiscard]] constexpr Rectangle reduced (ValueType left, ValueType top, ValueType right, ValueType bottom) const noexcept { Rectangle result = *this; @@ -1143,7 +1285,14 @@ class YUP_API Rectangle return result; } - // TODO - doxygen + /** Returns a new rectangle with its width reduced by a specified amount on the left side. + + This method creates a new rectangle with the same position but its width shrunk by the specified delta, reducing the size from the left edge. + + @param delta The amount to reduce the width by on the left side. + + @return A new rectangle with the reduced size. + */ [[nodiscard]] constexpr Rectangle reducedLeft (ValueType delta) const noexcept { Rectangle result = *this; @@ -1151,7 +1300,14 @@ class YUP_API Rectangle return result; } - // TODO - doxygen + /** Returns a new rectangle with its height reduced by a specified amount on the top side. + + This method creates a new rectangle with the same position but its height shrunk by the specified delta, reducing the size from the top edge. + + @param delta The amount to reduce the height by on the top side. + + @return A new rectangle with the reduced size. + */ [[nodiscard]] constexpr Rectangle reducedTop (ValueType delta) const noexcept { Rectangle result = *this; @@ -1159,7 +1315,14 @@ class YUP_API Rectangle return result; } - // TODO - doxygen + /** Returns a new rectangle with its width reduced by a specified amount on the right side. + + This method creates a new rectangle with the same position but its width shrunk by the specified delta, reducing the size from the right edge. + + @param delta The amount to reduce the width by on the right side. + + @return A new rectangle with the reduced size. + */ [[nodiscard]] constexpr Rectangle reducedRight (ValueType delta) const noexcept { Rectangle result = *this; @@ -1167,7 +1330,14 @@ class YUP_API Rectangle return result; } - // TODO - doxygen + /** Returns a new rectangle with its height reduced by a specified amount on the bottom side. + + This method creates a new rectangle with the same position but its height shrunk by the specified delta, reducing the size from the bottom edge. + + @param delta The amount to reduce the height by on the bottom side. + + @return A new rectangle with the reduced size. + */ [[nodiscard]] constexpr Rectangle reducedBottom (ValueType delta) const noexcept { Rectangle result = *this; @@ -1211,7 +1381,17 @@ class YUP_API Rectangle return *this; } - // TODO - doxygen + /** Enlarges the size of the rectangle by different amounts on all sides. + + This method expands the rectangle's width by left and right, and height by top and bottom, increasing the size on all edges by these respective amounts. + + @param left The amount to enlarge the width by on the left side. + @param top The amount to enlarge the height by on the top side. + @param right The amount to enlarge the width by on the right side. + @param bottom The amount to enlarge the height by on the bottom side. + + @return A reference to this rectangle to allow method chaining. + */ constexpr Rectangle& enlarge (ValueType left, ValueType top, ValueType right, ValueType bottom) noexcept { xy = { xy.getX() - left, xy.getY() - top }; @@ -1252,7 +1432,14 @@ class YUP_API Rectangle return result; } - // TODO - doxygen + /** Returns a new rectangle with its width enlarged by a specified amount on the left side. + + This method creates a new rectangle with the same position but its width enlarged by the specified delta, increasing the size from the left edge. + + @param delta The amount to enlarge the width by on the left side. + + @return A new rectangle with the enlarged size. + */ [[nodiscard]] constexpr Rectangle enlargedLeft (ValueType delta) const noexcept { Rectangle result = *this; @@ -1260,7 +1447,14 @@ class YUP_API Rectangle return result; } - // TODO - doxygen + /** Returns a new rectangle with its height enlarged by a specified amount on the top side. + + This method creates a new rectangle with the same position but its height enlarged by the specified delta, increasing the size from the top edge. + + @param delta The amount to enlarge the height by on the top side. + + @return A new rectangle with the enlarged size. + */ [[nodiscard]] constexpr Rectangle enlargedTop (ValueType delta) const noexcept { Rectangle result = *this; @@ -1268,7 +1462,14 @@ class YUP_API Rectangle return result; } - // TODO - doxygen + /** Returns a new rectangle with its width enlarged by a specified amount on the right side. + + This method creates a new rectangle with the same position but its width enlarged by the specified delta, increasing the size from the right edge. + + @param delta The amount to enlarge the width by on the right side. + + @return A new rectangle with the enlarged size. + */ [[nodiscard]] constexpr Rectangle enlargedRight (ValueType delta) const noexcept { Rectangle result = *this; @@ -1276,7 +1477,14 @@ class YUP_API Rectangle return result; } - // TODO - doxygen + /** Returns a new rectangle with its height enlarged by a specified amount on the bottom side. + + This method creates a new rectangle with the same position but its height enlarged by the specified delta, increasing the size from the bottom edge. + + @param delta The amount to enlarge the height by on the bottom side. + + @return A new rectangle with the enlarged size. + */ [[nodiscard]] constexpr Rectangle enlargedBottom (ValueType delta) const noexcept { Rectangle result = *this; @@ -1338,7 +1546,14 @@ class YUP_API Rectangle return ! (getX() > otherBottomRight.getX() || bottomRight.getX() < other.getX() || getY() > otherBottomRight.getY() || bottomRight.getY() < other.getY()); } - // TODO - doxygen + /** Returns the intersection of this rectangle with another rectangle. + + This method calculates and returns the area where this rectangle and another rectangle overlap. + + @param other The other rectangle to intersect with. + + @return A Rectangle representing the intersection of the two rectangles, or an empty rectangle if they do not intersect. + */ [[nodiscard]] constexpr Rectangle intersection (const Rectangle& other) const noexcept { const auto x1 = jmax (getX(), other.getX()); @@ -1418,7 +1633,14 @@ class YUP_API Rectangle } //============================================================================== - // TODO - doxygen + /** Transforms the rectangle by an affine transformation. + + This method applies an affine transformation to the rectangle, modifying its position and size. + + @param t The affine transformation to apply. + + @return A reference to this rectangle to allow method chaining. + */ Rectangle& transform (const AffineTransform& t) noexcept { auto x1 = static_cast (getX()); @@ -1439,7 +1661,14 @@ class YUP_API Rectangle return *this; } - // TODO - doxygen + /** Returns a new rectangle transformed by an affine transformation. + + This method creates a new rectangle with the same position and size, but with its position and size transformed by the given affine transformation. + + @param t The affine transformation to apply. + + @return A new rectangle with the transformed position and size. + */ [[nodiscard]] Rectangle transformed (const AffineTransform& t) const noexcept { Rectangle result (*this); @@ -1462,6 +1691,12 @@ class YUP_API Rectangle return { xy.template to(), size.template to() }; } + /** Rounds the rectangle's position and size to integers. + + This method creates a new rectangle with the same position and size, but with its position and size rounded to integers. + + @return A new rectangle with the rounded position and size. + */ template [[nodiscard]] constexpr auto roundToInt() const noexcept -> std::enable_if_t, Rectangle> @@ -1551,7 +1786,14 @@ class YUP_API Rectangle } //============================================================================== - /** Returns true if the two rectangles are approximately equal. */ + /** Returns true if the two rectangles are approximately equal. + + This method checks if the position and size of this rectangle are approximately equal to those of another rectangle. + + @param other The other rectangle to compare against. + + @return True if the rectangles are approximately equal, otherwise false. + */ constexpr bool approximatelyEqualTo (const Rectangle& other) const noexcept { if constexpr (std::is_floating_point_v) From 98fe216724b462b2aa206c051258694094922c08 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Thu, 12 Jun 2025 18:35:00 +0200 Subject: [PATCH 07/11] Fix bubble --- modules/yup_graphics/primitives/yup_Path.cpp | 191 +++++++++++++++---- 1 file changed, 159 insertions(+), 32 deletions(-) diff --git a/modules/yup_graphics/primitives/yup_Path.cpp b/modules/yup_graphics/primitives/yup_Path.cpp index 2eebeab40..b83ab6696 100644 --- a/modules/yup_graphics/primitives/yup_Path.cpp +++ b/modules/yup_graphics/primitives/yup_Path.cpp @@ -436,52 +436,179 @@ void Path::addBubble (Rectangle bodyArea, Rectangle maximumArea, P // Clamp corner size to reasonable bounds cornerSize = jmin (cornerSize, bodyArea.getWidth() * 0.5f, bodyArea.getHeight() * 0.5f); - // Determine which side of the body the arrow should be on - Point bodyCenter = bodyArea.getCenter(); + // Check if arrow tip is inside the body area - if so, draw no arrow + if (bodyArea.contains (arrowTipPosition)) + { + // Just draw a rounded rectangle + addRoundedRectangle (bodyArea, cornerSize); + return; + } - // Calculate which edge the arrow should attach to - float leftDist = std::abs (arrowTipPosition.getX() - bodyArea.getX()); - float rightDist = std::abs (arrowTipPosition.getX() - bodyArea.getRight()); - float topDist = std::abs (arrowTipPosition.getY() - bodyArea.getY()); - float bottomDist = std::abs (arrowTipPosition.getY() - bodyArea.getBottom()); + // Determine which side the arrow should be on based on tip position relative to rectangle + enum ArrowSide { Left, Right, Top, Bottom } arrowSide; + Point arrowBase1, arrowBase2; - float minDist = jmin (leftDist, rightDist, topDist, bottomDist); + // Get rectangle center for direction calculation + Point rectCenter = bodyArea.getCenter(); - // Start with the main body (rounded rectangle) - addRoundedRectangle (bodyArea, cornerSize); + // Calculate relative position of arrow tip + float deltaX = arrowTipPosition.getX() - rectCenter.getX(); + float deltaY = arrowTipPosition.getY() - rectCenter.getY(); - // Add arrow based on closest edge - Point arrowBase1, arrowBase2; + // Determine primary direction - use the larger absolute offset + if (std::abs(deltaX) > std::abs(deltaY)) + { + // Horizontal direction is dominant + if (deltaX < 0) + { + // Arrow tip is to the left of rectangle center + arrowSide = Left; + // Ensure arrow base doesn't overlap with corner radius + float minY = bodyArea.getY() + cornerSize + arrowBaseWidth * 0.5f; + float maxY = bodyArea.getBottom() - cornerSize - arrowBaseWidth * 0.5f; + float arrowY = jlimit (minY, maxY, arrowTipPosition.getY()); + // For left edge (going bottom to top in clockwise direction) + arrowBase1 = Point (bodyArea.getX(), arrowY + arrowBaseWidth * 0.5f); // bottom base point + arrowBase2 = Point (bodyArea.getX(), arrowY - arrowBaseWidth * 0.5f); // top base point + } + else + { + // Arrow tip is to the right of rectangle center + arrowSide = Right; + // Ensure arrow base doesn't overlap with corner radius + float minY = bodyArea.getY() + cornerSize + arrowBaseWidth * 0.5f; + float maxY = bodyArea.getBottom() - cornerSize - arrowBaseWidth * 0.5f; + float arrowY = jlimit (minY, maxY, arrowTipPosition.getY()); + // For right edge (going top to bottom in clockwise direction) + arrowBase1 = Point (bodyArea.getRight(), arrowY - arrowBaseWidth * 0.5f); // top base point + arrowBase2 = Point (bodyArea.getRight(), arrowY + arrowBaseWidth * 0.5f); // bottom base point + } + } + else + { + // Vertical direction is dominant + if (deltaY < 0) + { + // Arrow tip is above rectangle center + arrowSide = Top; + // Ensure arrow base doesn't overlap with corner radius + float minX = bodyArea.getX() + cornerSize + arrowBaseWidth * 0.5f; + float maxX = bodyArea.getRight() - cornerSize - arrowBaseWidth * 0.5f; + float arrowX = jlimit (minX, maxX, arrowTipPosition.getX()); + // For top edge (going left to right in clockwise direction) + arrowBase1 = Point (arrowX - arrowBaseWidth * 0.5f, bodyArea.getY()); // left base point + arrowBase2 = Point (arrowX + arrowBaseWidth * 0.5f, bodyArea.getY()); // right base point + } + else + { + // Arrow tip is below rectangle center + arrowSide = Bottom; + // Ensure arrow base doesn't overlap with corner radius + float minX = bodyArea.getX() + cornerSize + arrowBaseWidth * 0.5f; + float maxX = bodyArea.getRight() - cornerSize - arrowBaseWidth * 0.5f; + float arrowX = jlimit (minX, maxX, arrowTipPosition.getX()); + // For bottom edge (going right to left in clockwise direction) + arrowBase1 = Point (arrowX + arrowBaseWidth * 0.5f, bodyArea.getBottom()); // right base point + arrowBase2 = Point (arrowX - arrowBaseWidth * 0.5f, bodyArea.getBottom()); // left base point + } + } + + // Use the mathematically correct constant for circular arc approximation with cubic Bezier curves + constexpr float kappa = 0.5522847498f; + + float x = bodyArea.getX(); + float y = bodyArea.getY(); + float width = bodyArea.getWidth(); + float height = bodyArea.getHeight(); + + // Start drawing the integrated path clockwise from top-left + moveTo (x + cornerSize, y); + + // Top edge(left to right) + if (arrowSide == Top) + { + lineTo (arrowBase1.getX(), arrowBase1.getY()); + lineTo (arrowTipPosition.getX(), arrowTipPosition.getY()); + lineTo (arrowBase2.getX(), arrowBase2.getY()); + lineTo (x + width - cornerSize, y); + } + else + { + lineTo (x + width - cornerSize, y); + } + + // Top-right corner + if (cornerSize > 0.0f) + { + cubicTo (x + width - cornerSize + cornerSize * kappa, y, + x + width, y + cornerSize - cornerSize * kappa, + x + width, y + cornerSize); + } + + // Right edge (top to bottom) + if (arrowSide == Right) + { + lineTo (arrowBase1.getX(), arrowBase1.getY()); + lineTo (arrowTipPosition.getX(), arrowTipPosition.getY()); + lineTo (arrowBase2.getX(), arrowBase2.getY()); + lineTo (x + width, y + height - cornerSize); + } + else + { + lineTo (x + width, y + height - cornerSize); + } + + // Bottom-right corner + if (cornerSize > 0.0f) + { + cubicTo (x + width, y + height - cornerSize + cornerSize * kappa, + x + width - cornerSize + cornerSize * kappa, y + height, + x + width - cornerSize, y + height); + } + + // Bottom edge (right to left) + if (arrowSide == Bottom) + { + lineTo (arrowBase1.getX(), arrowBase1.getY()); + lineTo (arrowTipPosition.getX(), arrowTipPosition.getY()); + lineTo (arrowBase2.getX(), arrowBase2.getY()); + lineTo (x + cornerSize, y + height); + } + else + { + lineTo (x + cornerSize, y + height); + } - if (minDist == leftDist) // Arrow on left side + // Bottom-left corner + if (cornerSize > 0.0f) { - float arrowY = jlimit (bodyArea.getY() + cornerSize, bodyArea.getBottom() - cornerSize, arrowTipPosition.getY()); - arrowBase1 = Point (bodyArea.getX(), arrowY - arrowBaseWidth * 0.5f); - arrowBase2 = Point (bodyArea.getX(), arrowY + arrowBaseWidth * 0.5f); + cubicTo (x + cornerSize - cornerSize * kappa, y + height, + x, y + height - cornerSize + cornerSize * kappa, + x, y + height - cornerSize); } - else if (minDist == rightDist) // Arrow on right side + + // Left edge (bottom to top) + if (arrowSide == Left) { - float arrowY = jlimit (bodyArea.getY() + cornerSize, bodyArea.getBottom() - cornerSize, arrowTipPosition.getY()); - arrowBase1 = Point (bodyArea.getRight(), arrowY - arrowBaseWidth * 0.5f); - arrowBase2 = Point (bodyArea.getRight(), arrowY + arrowBaseWidth * 0.5f); + lineTo (arrowBase1.getX(), arrowBase1.getY()); + lineTo (arrowTipPosition.getX(), arrowTipPosition.getY()); + lineTo (arrowBase2.getX(), arrowBase2.getY()); + lineTo (x, y + cornerSize); } - else if (minDist == topDist) // Arrow on top side + else { - float arrowX = jlimit (bodyArea.getX() + cornerSize, bodyArea.getRight() - cornerSize, arrowTipPosition.getX()); - arrowBase1 = Point (arrowX - arrowBaseWidth * 0.5f, bodyArea.getY()); - arrowBase2 = Point (arrowX + arrowBaseWidth * 0.5f, bodyArea.getY()); + lineTo (x, y + cornerSize); } - else // Arrow on bottom side + + // Top-left corner + if (cornerSize > 0.0f) { - float arrowX = jlimit (bodyArea.getX() + cornerSize, bodyArea.getRight() - cornerSize, arrowTipPosition.getX()); - arrowBase1 = Point (arrowX - arrowBaseWidth * 0.5f, bodyArea.getBottom()); - arrowBase2 = Point (arrowX + arrowBaseWidth * 0.5f, bodyArea.getBottom()); + cubicTo (x, y + cornerSize - cornerSize * kappa, + x + cornerSize - cornerSize * kappa, y, + x + cornerSize, y); } - // Add the arrow triangle - moveTo (arrowBase1); - lineTo (arrowTipPosition); - lineTo (arrowBase2); + // Close the path close(); } From 42ba12d77efd96dbaaed97de91e4697509e26a39 Mon Sep 17 00:00:00 2001 From: Yup Bot Date: Thu, 12 Jun 2025 16:35:58 +0000 Subject: [PATCH 08/11] Code formatting --- examples/graphics/source/examples/Paths.h | 41 +++++-------- modules/yup_graphics/graphics/yup_Color.h | 60 ++++++++++++++------ modules/yup_graphics/primitives/yup_Path.cpp | 35 ++++++------ 3 files changed, 74 insertions(+), 62 deletions(-) diff --git a/examples/graphics/source/examples/Paths.h b/examples/graphics/source/examples/Paths.h index c2ac60b92..1f91ae106 100644 --- a/examples/graphics/source/examples/Paths.h +++ b/examples/graphics/source/examples/Paths.h @@ -43,40 +43,30 @@ class PathsExample : public yup::Component */ auto bounds = getLocalBounds().to().reduced (10, 20); - auto sectionHeight = bounds.getHeight() / 6.0f; // 6 rows instead of 4 - auto sectionWidth = bounds.getWidth() / 2.0f; // 2 columns instead of 3 + auto sectionHeight = bounds.getHeight() / 6.0f; // 6 rows instead of 4 + auto sectionWidth = bounds.getWidth() / 2.0f; // 2 columns instead of 3 // Row 1: Basic Operations and Basic Shapes - drawBasicPathOperations (g, yup::Rectangle (bounds.getX(), bounds.getY(), - sectionWidth, sectionHeight)); - drawBasicShapes (g, yup::Rectangle (bounds.getX() + sectionWidth, bounds.getY(), - sectionWidth, sectionHeight)); + drawBasicPathOperations (g, yup::Rectangle (bounds.getX(), bounds.getY(), sectionWidth, sectionHeight)); + drawBasicShapes (g, yup::Rectangle (bounds.getX() + sectionWidth, bounds.getY(), sectionWidth, sectionHeight)); // Row 2: Complex Shapes and Arcs & Curves - drawComplexShapes (g, yup::Rectangle (bounds.getX(), bounds.getY() + sectionHeight, - sectionWidth, sectionHeight)); - drawArcsAndCurves (g, yup::Rectangle (bounds.getX() + sectionWidth, bounds.getY() + sectionHeight, - sectionWidth, sectionHeight)); + drawComplexShapes (g, yup::Rectangle (bounds.getX(), bounds.getY() + sectionHeight, sectionWidth, sectionHeight)); + drawArcsAndCurves (g, yup::Rectangle (bounds.getX() + sectionWidth, bounds.getY() + sectionHeight, sectionWidth, sectionHeight)); // Row 3: Transformations and Advanced Features - drawPathTransformations (g, yup::Rectangle (bounds.getX(), bounds.getY() + sectionHeight * 2, - sectionWidth, sectionHeight)); - drawAdvancedFeatures (g, yup::Rectangle (bounds.getX() + sectionWidth, bounds.getY() + sectionHeight * 2, - sectionWidth, sectionHeight)); + drawPathTransformations (g, yup::Rectangle (bounds.getX(), bounds.getY() + sectionHeight * 2, sectionWidth, sectionHeight)); + drawAdvancedFeatures (g, yup::Rectangle (bounds.getX() + sectionWidth, bounds.getY() + sectionHeight * 2, sectionWidth, sectionHeight)); // Row 4: Path Utilities and SVG Path Data - drawPathUtilities (g, yup::Rectangle (bounds.getX(), bounds.getY() + sectionHeight * 3, - sectionWidth, sectionHeight)); - drawSVGPathData (g, yup::Rectangle (bounds.getX() + sectionWidth, bounds.getY() + sectionHeight * 3, - sectionWidth, sectionHeight)); + drawPathUtilities (g, yup::Rectangle (bounds.getX(), bounds.getY() + sectionHeight * 3, sectionWidth, sectionHeight)); + drawSVGPathData (g, yup::Rectangle (bounds.getX() + sectionWidth, bounds.getY() + sectionHeight * 3, sectionWidth, sectionHeight)); // Row 5: Creative Examples (full width) - drawCreativeExamples (g, yup::Rectangle (bounds.getX(), bounds.getY() + sectionHeight * 4, - bounds.getWidth(), sectionHeight)); + drawCreativeExamples (g, yup::Rectangle (bounds.getX(), bounds.getY() + sectionHeight * 4, bounds.getWidth(), sectionHeight)); // Row 6: Interactive Demo (full width) - drawInteractiveDemo (g, yup::Rectangle (bounds.getX(), bounds.getY() + sectionHeight * 5, - bounds.getWidth(), sectionHeight)); + drawInteractiveDemo (g, yup::Rectangle (bounds.getX(), bounds.getY() + sectionHeight * 5, bounds.getWidth(), sectionHeight)); } private: @@ -92,7 +82,7 @@ class PathsExample : public yup::Component } g.setFillColor (yup::Colors::white); - g.fillFittedText(text, area.removeFromTop (16)); + g.fillFittedText (text, area.removeFromTop (16)); } void drawBasicPathOperations (yup::Graphics& g, yup::Rectangle area) @@ -210,7 +200,7 @@ class PathsExample : public yup::Component // Speech Bubble - smaller yup::Path bubblePath; yup::Rectangle bodyArea (x - 15, y + 30, 50, 25); - yup::Rectangle maxArea = bodyArea.enlarged(10); + yup::Rectangle maxArea = bodyArea.enlarged (10); yup::Point tipPosition (x + 45, y + 65); bubblePath.addBubble (bodyArea, maxArea, tipPosition, 5, 8); g.setFillColor (yup::Color (220, 240, 255)); @@ -244,8 +234,7 @@ class PathsExample : public yup::Component // Centered Arc with rotation yup::Path centeredArcPath; - centeredArcPath.addCenteredArc (yup::Point (x + 70, y + 20), 18, 12, - yup::MathConstants::pi / 4, 0, yup::MathConstants::pi, true); + centeredArcPath.addCenteredArc (yup::Point (x + 70, y + 20), 18, 12, yup::MathConstants::pi / 4, 0, yup::MathConstants::pi, true); g.setStrokeColor (yup::Color (150, 255, 150)); g.setStrokeWidth (2.0f); g.strokePath (centeredArcPath); diff --git a/modules/yup_graphics/graphics/yup_Color.h b/modules/yup_graphics/graphics/yup_Color.h index 51b0b8c4c..3c67ed5e2 100644 --- a/modules/yup_graphics/graphics/yup_Color.h +++ b/modules/yup_graphics/graphics/yup_Color.h @@ -571,8 +571,8 @@ class YUP_API Color const float gf = getGreenFloat(); const float bf = getBlueFloat(); - const float max = jmax(rf, gf, bf); - const float min = jmin(rf, gf, bf); + const float max = jmax (rf, gf, bf); + const float min = jmin (rf, gf, bf); const float delta = max - min; float h = 0.0f; @@ -582,7 +582,7 @@ class YUP_API Color if (delta != 0.0f) { if (max == rf) - h = fmodf((gf - bf) / delta + (gf < bf ? 6.0f : 0.0f), 6.0f); + h = fmodf ((gf - bf) / delta + (gf < bf ? 6.0f : 0.0f), 6.0f); else if (max == gf) h = (bf - rf) / delta + 2.0f; else if (max == bf) @@ -591,7 +591,7 @@ class YUP_API Color h /= 6.0f; } - return std::make_tuple(h, s, v); + return std::make_tuple (h, s, v); } /** Constructs a color from HSV values. @@ -606,33 +606,57 @@ class YUP_API Color @return A Color object corresponding to the given HSV values. */ - constexpr static Color fromHSV(float h, float s, float v, float a = 1.0f) noexcept + constexpr static Color fromHSV (float h, float s, float v, float a = 1.0f) noexcept { float r = 0.0f, g = 0.0f, b = 0.0f; - h = modulo(h, 1.0f); // ensure h is in [0,1] + h = modulo (h, 1.0f); // ensure h is in [0,1] const float hh = h * 6.0f; - const int i = static_cast(hh); - const float f = hh - static_cast(i); + const int i = static_cast (hh); + const float f = hh - static_cast (i); const float p = v * (1.0f - s); const float q = v * (1.0f - f * s); const float t = v * (1.0f - (1.0f - f) * s); switch (i % 6) { - case 0: r = v; g = t; b = p; break; - case 1: r = q; g = v; b = p; break; - case 2: r = p; g = v; b = t; break; - case 3: r = p; g = q; b = v; break; - case 4: r = t; g = p; b = v; break; - case 5: r = v; g = p; b = q; break; + case 0: + r = v; + g = t; + b = p; + break; + case 1: + r = q; + g = v; + b = p; + break; + case 2: + r = p; + g = v; + b = t; + break; + case 3: + r = p; + g = q; + b = v; + break; + case 4: + r = t; + g = p; + b = v; + break; + case 5: + r = v; + g = p; + b = q; + break; } return { - static_cast(r * 255), - static_cast(g * 255), - static_cast(b * 255), - static_cast(a * 255) + static_cast (r * 255), + static_cast (g * 255), + static_cast (b * 255), + static_cast (a * 255) }; } diff --git a/modules/yup_graphics/primitives/yup_Path.cpp b/modules/yup_graphics/primitives/yup_Path.cpp index b83ab6696..68f63747b 100644 --- a/modules/yup_graphics/primitives/yup_Path.cpp +++ b/modules/yup_graphics/primitives/yup_Path.cpp @@ -445,7 +445,14 @@ void Path::addBubble (Rectangle bodyArea, Rectangle maximumArea, P } // Determine which side the arrow should be on based on tip position relative to rectangle - enum ArrowSide { Left, Right, Top, Bottom } arrowSide; + enum ArrowSide + { + Left, + Right, + Top, + Bottom + } arrowSide; + Point arrowBase1, arrowBase2; // Get rectangle center for direction calculation @@ -456,7 +463,7 @@ void Path::addBubble (Rectangle bodyArea, Rectangle maximumArea, P float deltaY = arrowTipPosition.getY() - rectCenter.getY(); // Determine primary direction - use the larger absolute offset - if (std::abs(deltaX) > std::abs(deltaY)) + if (std::abs (deltaX) > std::abs (deltaY)) { // Horizontal direction is dominant if (deltaX < 0) @@ -540,9 +547,7 @@ void Path::addBubble (Rectangle bodyArea, Rectangle maximumArea, P // Top-right corner if (cornerSize > 0.0f) { - cubicTo (x + width - cornerSize + cornerSize * kappa, y, - x + width, y + cornerSize - cornerSize * kappa, - x + width, y + cornerSize); + cubicTo (x + width - cornerSize + cornerSize * kappa, y, x + width, y + cornerSize - cornerSize * kappa, x + width, y + cornerSize); } // Right edge (top to bottom) @@ -561,9 +566,7 @@ void Path::addBubble (Rectangle bodyArea, Rectangle maximumArea, P // Bottom-right corner if (cornerSize > 0.0f) { - cubicTo (x + width, y + height - cornerSize + cornerSize * kappa, - x + width - cornerSize + cornerSize * kappa, y + height, - x + width - cornerSize, y + height); + cubicTo (x + width, y + height - cornerSize + cornerSize * kappa, x + width - cornerSize + cornerSize * kappa, y + height, x + width - cornerSize, y + height); } // Bottom edge (right to left) @@ -582,9 +585,7 @@ void Path::addBubble (Rectangle bodyArea, Rectangle maximumArea, P // Bottom-left corner if (cornerSize > 0.0f) { - cubicTo (x + cornerSize - cornerSize * kappa, y + height, - x, y + height - cornerSize + cornerSize * kappa, - x, y + height - cornerSize); + cubicTo (x + cornerSize - cornerSize * kappa, y + height, x, y + height - cornerSize + cornerSize * kappa, x, y + height - cornerSize); } // Left edge (bottom to top) @@ -603,9 +604,7 @@ void Path::addBubble (Rectangle bodyArea, Rectangle maximumArea, P // Top-left corner if (cornerSize > 0.0f) { - cubicTo (x, y + cornerSize - cornerSize * kappa, - x + cornerSize - cornerSize * kappa, y, - x + cornerSize, y); + cubicTo (x, y + cornerSize - cornerSize * kappa, x + cornerSize - cornerSize * kappa, y, x + cornerSize, y); } // Close the path @@ -696,7 +695,7 @@ void Path::scaleToFit (float x, float y, float width, float height, bool preserv float translateY = y - currentBounds.getY() * scaleY; // Apply the transformation - AffineTransform transform = AffineTransform::scaling (scaleX, scaleY) .translated (translateX, translateY); + AffineTransform transform = AffineTransform::scaling (scaleX, scaleY).translated (translateX, translateY); *this = transformed (transform); } @@ -1460,9 +1459,9 @@ Path Path::createStrokePolygon (float strokeWidth) const Point lastMovePoint (0.0f, 0.0f); std::vector> leftSide; - leftSide.reserve(points.size()); + leftSide.reserve (points.size()); std::vector> rightSide; - rightSide.reserve(points.size()); + rightSide.reserve (points.size()); for (size_t i = 0, pointIndex = 0; i < verbs.size(); ++i) { @@ -1735,7 +1734,7 @@ Path Path::withRoundedCorners (float cornerRadius) const bool hasPreviousPoint = false; std::vector> pathPoints; - pathPoints.reserve(points.size()); + pathPoints.reserve (points.size()); for (size_t i = 0, pointIndex = 0; i < verbs.size(); ++i) { From 00ecd51af037dae28e55888629e4dfb366ed456d Mon Sep 17 00:00:00 2001 From: kunitoki Date: Thu, 12 Jun 2025 21:23:17 +0200 Subject: [PATCH 09/11] More tests --- justfile | 6 + modules/yup_graphics/primitives/yup_Path.cpp | 43 +- modules/yup_graphics/primitives/yup_Path.h | 6 +- tests/yup_graphics/yup_Path.cpp | 548 +++++++++++++++++++ 4 files changed, 592 insertions(+), 11 deletions(-) create mode 100644 tests/yup_graphics/yup_Path.cpp diff --git a/justfile b/justfile index acb09b45e..f70516662 100644 --- a/justfile +++ b/justfile @@ -13,6 +13,12 @@ clean: build CONFIG="Debug": cmake --build build --config {{CONFIG}} +[doc("execute unit tests using cmake")] +test CONFIG="Debug": + cmake -G Xcode -B build + cmake --build build --target yup_tests --config {{CONFIG}} + build/tests/{{CONFIG}}/yup_tests --gtest_filter=PathTests.* + [doc("generate and open project in macOS using Xcode")] osx PROFILING="OFF": cmake -G Xcode -B build -DYUP_ENABLE_PROFILING={{PROFILING}} diff --git a/modules/yup_graphics/primitives/yup_Path.cpp b/modules/yup_graphics/primitives/yup_Path.cpp index 68f63747b..9a040c1b8 100644 --- a/modules/yup_graphics/primitives/yup_Path.cpp +++ b/modules/yup_graphics/primitives/yup_Path.cpp @@ -167,6 +167,9 @@ Path& Path::addLine (const Line& line) Path& Path::addRectangle (float x, float y, float width, float height) { + width = jmax (0.0f, width); + height = jmax (0.0f, height); + reserveSpace (size() + 5); moveTo (x, y); @@ -189,6 +192,9 @@ Path& Path::addRoundedRectangle (float x, float y, float width, float height, fl { reserveSpace (size() + 9); + width = jmax (0.0f, width); + height = jmax (0.0f, height); + const float centerWidth = width * 0.5f; const float centerHeight = height * 0.5f; radiusTopLeft = jmin (radiusTopLeft, centerWidth, centerHeight); @@ -245,6 +251,9 @@ Path& Path::addEllipse (float x, float y, float width, float height) { reserveSpace (size() + 6); + width = jmax (0.0f, width); + height = jmax (0.0f, height); + const float rx = width * 0.5f; const float ry = height * 0.5f; const float cx = x + rx; @@ -273,6 +282,9 @@ Path& Path::addCenteredEllipse (float centerX, float centerY, float radiusX, flo { reserveSpace (size() + 6); + radiusX = jmax (0.0f, radiusX); + radiusY = jmax (0.0f, radiusY); + const float rx = radiusX; const float ry = radiusY; const float cx = centerX; @@ -304,6 +316,9 @@ Path& Path::addCenteredEllipse (const Point& center, const Size& d Path& Path::addArc (float x, float y, float width, float height, float fromRadians, float toRadians, bool startAsNewSubPath) { + width = jmax (0.0f, width); + height = jmax (0.0f, height); + const float radiusX = width * 0.5f; const float radiusY = height * 0.5f; @@ -329,6 +344,9 @@ Path& Path::addCenteredArc (float centerX, float centerY, float radiusX, float r const float sinTheta = std::sin (rotationOfEllipse); // Initialize variables for the loop + radiusX = jmax (0.0f, radiusX); + radiusY = jmax (0.0f, radiusY); + float x = std::cos (fromRadians) * radiusX; float y = std::sin (fromRadians) * radiusY; float rotatedX = x * cosTheta - y * sinTheta + centerX; @@ -369,14 +387,15 @@ Path& Path::addCenteredArc (const Point& center, const Size& diame } //============================================================================== -void Path::addPolygon (Point centre, int numberOfSides, float radius, float startAngle) +Path& Path::addPolygon (Point centre, int numberOfSides, float radius, float startAngle) { - if (numberOfSides < 3 || radius <= 0.0f) - return; + if (numberOfSides < 3) + return *this; reserveSpace (size() + numberOfSides + 1); const float angleIncrement = MathConstants::twoPi / numberOfSides; + radius = jmax (0.0f, radius); // Start with the first vertex float angle = startAngle; @@ -395,17 +414,21 @@ void Path::addPolygon (Point centre, int numberOfSides, float radius, flo } close(); + + return *this; } //============================================================================== -void Path::addStar (Point centre, int numberOfPoints, float innerRadius, float outerRadius, float startAngle) +Path& Path::addStar (Point centre, int numberOfPoints, float innerRadius, float outerRadius, float startAngle) { - if (numberOfPoints < 3 || innerRadius <= 0.0f || outerRadius <= 0.0f) - return; + if (numberOfPoints < 3) + return *this; reserveSpace (size() + numberOfPoints * 2 + 1); const float angleIncrement = MathConstants::twoPi / (numberOfPoints * 2); + innerRadius = jmax (0.0f, innerRadius); + outerRadius = jmax (0.0f, outerRadius); // Start with the first outer vertex float angle = startAngle; @@ -425,13 +448,15 @@ void Path::addStar (Point centre, int numberOfPoints, float innerRadius, } close(); + + return *this; } //============================================================================== -void Path::addBubble (Rectangle bodyArea, Rectangle maximumArea, Point arrowTipPosition, float cornerSize, float arrowBaseWidth) +Path& Path::addBubble (Rectangle bodyArea, Rectangle maximumArea, Point arrowTipPosition, float cornerSize, float arrowBaseWidth) { if (bodyArea.isEmpty() || maximumArea.isEmpty() || arrowBaseWidth <= 0.0f) - return; + return *this; // Clamp corner size to reasonable bounds cornerSize = jmin (cornerSize, bodyArea.getWidth() * 0.5f, bodyArea.getHeight() * 0.5f); @@ -609,6 +634,8 @@ void Path::addBubble (Rectangle bodyArea, Rectangle maximumArea, P // Close the path close(); + + return *this; } //============================================================================== diff --git a/modules/yup_graphics/primitives/yup_Path.h b/modules/yup_graphics/primitives/yup_Path.h index 35c85772d..fc70ca61e 100644 --- a/modules/yup_graphics/primitives/yup_Path.h +++ b/modules/yup_graphics/primitives/yup_Path.h @@ -438,7 +438,7 @@ class YUP_API Path @param radius The radius from the center to each vertex. @param startAngle The starting angle in radians (0.0f starts at the right). */ - void addPolygon (Point centre, int numberOfSides, float radius, float startAngle = 0.0f); + Path& addPolygon (Point centre, int numberOfSides, float radius, float startAngle = 0.0f); //============================================================================== /** Adds a star shape to the path. @@ -452,7 +452,7 @@ class YUP_API Path @param outerRadius The radius from the center to the outer vertices. @param startAngle The starting angle in radians (0.0f starts at the right). */ - void addStar (Point centre, int numberOfPoints, float innerRadius, float outerRadius, float startAngle = 0.0f); + Path& addStar (Point centre, int numberOfPoints, float innerRadius, float outerRadius, float startAngle = 0.0f); //============================================================================== /** Adds a speech bubble shape to the path. @@ -466,7 +466,7 @@ class YUP_API Path @param cornerSize The radius of the rounded corners. @param arrowBaseWidth The width of the arrow at its base. */ - void addBubble (Rectangle bodyArea, Rectangle maximumArea, Point arrowTipPosition, float cornerSize, float arrowBaseWidth); + Path& addBubble (Rectangle bodyArea, Rectangle maximumArea, Point arrowTipPosition, float cornerSize, float arrowBaseWidth); //============================================================================== /** Converts the path to a stroke polygon with specified width. diff --git a/tests/yup_graphics/yup_Path.cpp b/tests/yup_graphics/yup_Path.cpp new file mode 100644 index 000000000..2af032dcd --- /dev/null +++ b/tests/yup_graphics/yup_Path.cpp @@ -0,0 +1,548 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +#include +#include +#include + +using namespace yup; + +namespace +{ +static constexpr float tol = 1e-4f; + +void expectPointNear (const Point& a, const Point& b, float tolerance = tol) +{ + EXPECT_NEAR (a.getX(), b.getX(), tolerance); + EXPECT_NEAR (a.getY(), b.getY(), tolerance); +} + +void expectRectNear (const Rectangle& a, const Rectangle& b, float tolerance = tol) +{ + EXPECT_NEAR (a.getX(), b.getX(), tolerance); + EXPECT_NEAR (a.getY(), b.getY(), tolerance); + EXPECT_NEAR (a.getWidth(), b.getWidth(), tolerance); + EXPECT_NEAR (a.getHeight(), b.getHeight(), tolerance); +} +} // namespace + +TEST (PathTests, DefaultConstruction) +{ + Path p; + EXPECT_EQ (p.size(), 0); + EXPECT_TRUE (p.getBounds().isEmpty()); +} + +TEST (PathTests, MoveAndCopyConstruction) +{ + Path p1 (10.0f, 20.0f); + Path p2 (p1); + Path p3 (std::move (p1)); + EXPECT_EQ (p2.size(), p3.size()); + EXPECT_TRUE (p2.getBounds() == p3.getBounds()); + Path p4; + p4 = p2; + Path p5; + p5 = std::move (p3); + EXPECT_EQ (p4.size(), p5.size()); +} + +TEST (PathTests, ClearAndReserve) +{ + Path p; + p.moveTo (0, 0).lineTo (10, 10); + EXPECT_GT (p.size(), 0); + p.clear(); + EXPECT_EQ (p.size(), 0); + p.reserveSpace (10); + EXPECT_EQ (p.size(), 0); +} + +TEST (PathTests, MoveToLineToQuadToCubicToClose) +{ + Path p; + p.moveTo (0, 0).lineTo (10, 0).quadTo (15, 5, 10, 10).cubicTo (5, 15, 0, 10, 0, 0).close(); + EXPECT_GT (p.size(), 0); + EXPECT_FALSE (p.getBounds().isEmpty()); +} + +TEST (PathTests, AddLine) +{ + Path p; + Point a (1, 2), b (3, 4); + p.addLine (a, b); + EXPECT_FALSE (p.getBounds().isEmpty()); + Line l (Point (5, 6), Point (7, 8)); + p.addLine (l); + EXPECT_FALSE (p.getBounds().isEmpty()); +} + +TEST (PathTests, AddRectangle) +{ + Path p; + p.addRectangle (0, 0, 10, 20); + Rectangle r (5, 5, 15, 25); + p.addRectangle (r); + EXPECT_FALSE (p.getBounds().isEmpty()); +} + +TEST (PathTests, AddRoundedRectangle) +{ + Path p; + p.addRoundedRectangle (0, 0, 10, 20, 2); + p.addRoundedRectangle (0, 0, 10, 20, 1, 2, 3, 4); + Rectangle r (5, 5, 15, 25); + p.addRoundedRectangle (r, 3); + p.addRoundedRectangle (r, 1, 2, 3, 4); + EXPECT_FALSE (p.getBounds().isEmpty()); +} + +TEST (PathTests, AddEllipse) +{ + Path p; + p.addEllipse (0, 0, 10, 20); + Rectangle r (5, 5, 15, 25); + p.addEllipse (r); + EXPECT_FALSE (p.getBounds().isEmpty()); +} + +TEST (PathTests, AddCenteredEllipse) +{ + Path p; + p.addCenteredEllipse (5, 5, 10, 20); + Point c (10, 10); + p.addCenteredEllipse (c, 8, 12); + Size sz (16, 24); + p.addCenteredEllipse (c, sz); + EXPECT_FALSE (p.getBounds().isEmpty()); +} + +TEST (PathTests, AddArc) +{ + Path p; + p.addArc (0, 0, 10, 10, 0, MathConstants::pi, true); + Rectangle r (5, 5, 10, 10); + p.addArc (r, 0, MathConstants::twoPi, false); + p.addCenteredArc (5, 5, 10, 10, 0, 0, MathConstants::halfPi, true); + Point c (10, 10); + p.addCenteredArc (c, 8, 12, 0, 0, MathConstants::pi, false); + Size sz (16, 24); + p.addCenteredArc (c, sz, 0, 0, MathConstants::pi, true); + EXPECT_FALSE (p.getBounds().isEmpty()); +} + +TEST (PathTests, AddPolygon) +{ + Path p; + Point center (10, 10); + p.addPolygon (center, 5, 8, 0.0f); + p.addPolygon (center, 3, 5, MathConstants::halfPi); + EXPECT_FALSE (p.getBounds().isEmpty()); +} + +TEST (PathTests, AddStar) +{ + Path p; + Point center (10, 10); + p.addStar (center, 5, 4, 8, 0.0f); + p.addStar (center, 3, 2, 5, MathConstants::halfPi); + EXPECT_FALSE (p.getBounds().isEmpty()); +} + +TEST (PathTests, AddBubble) +{ + Path p; + Rectangle body (10, 10, 40, 20); + Rectangle max (0, 0, 100, 100); + Point tip (30, 0); + p.addBubble (body, max, tip, 5, 10); + // Arrow inside body (no arrow) + p.addBubble (body, max, Point (20, 20), 5, 10); + EXPECT_FALSE (p.getBounds().isEmpty()); +} + +TEST (PathTests, AppendPath) +{ + Path p1; + p1.addRectangle (0, 0, 10, 10); + Path p2; + p2.addEllipse (5, 5, 10, 10); + p1.appendPath (p2); + EXPECT_FALSE (p1.getBounds().isEmpty()); + // With transform + AffineTransform t = AffineTransform::translation (10, 10).scaled (2.0f); + p1.appendPath (p2, t); + EXPECT_FALSE (p1.getBounds().isEmpty()); +} + +TEST (PathTests, SwapWithPath) +{ + Path p1; + p1.addRectangle (0, 0, 10, 10); + Path p2; + p2.addEllipse (5, 5, 10, 10); + Rectangle b1 = p1.getBounds(); + Rectangle b2 = p2.getBounds(); + p1.swapWithPath (p2); + expectRectNear (p1.getBounds(), b2); + expectRectNear (p2.getBounds(), b1); +} + +TEST (PathTests, TransformAndTransformed) +{ + Path p; + p.addRectangle (0, 0, 10, 10); + AffineTransform t = AffineTransform::translation (5, 5).scaled (2.0f); + Path p2 = p.transformed (t); + p.transform (t); + expectRectNear (p.getBounds(), p2.getBounds()); +} + +TEST (PathTests, ScaleToFit) +{ + Path p; + p.addRectangle (10, 10, 20, 20); + p.scaleToFit (0, 0, 100, 50, false); + Rectangle b = p.getBounds(); + EXPECT_NEAR (b.getWidth(), 100.0f, tol); + EXPECT_NEAR (b.getHeight(), 50.0f, tol); + // Proportional + p.addRectangle (0, 0, 10, 10); + p.scaleToFit (0, 0, 50, 100, true); + b = p.getBounds(); + // The bounds will be the union of both rectangles, so width==height is not guaranteed. + EXPECT_LE (b.getWidth(), 50.0f + tol); + EXPECT_LE (b.getHeight(), 100.0f + tol); + EXPECT_GT (b.getWidth(), 0.0f); + EXPECT_GT (b.getHeight(), 0.0f); +} + +TEST (PathTests, GetPointAlongPath) +{ + Path p; + p.moveTo (0, 0).lineTo (10, 0).lineTo (10, 10); + Point start = p.getPointAlongPath (0.0f); + Point mid = p.getPointAlongPath (0.5f); + Point end = p.getPointAlongPath (1.0f); + expectPointNear (start, Point (0, 0)); + expectPointNear (end, Point (10, 10)); + // Midpoint should be somewhere on the path + EXPECT_TRUE (mid.getX() >= 0 && mid.getX() <= 10); + EXPECT_TRUE (mid.getY() >= 0 && mid.getY() <= 10); +} + +TEST (PathTests, CreateStrokePolygon) +{ + Path p; + p.addRectangle (0, 0, 10, 10); + Path stroke = p.createStrokePolygon (2.0f); + EXPECT_FALSE (stroke.getBounds().isEmpty()); + // Edge: empty path + Path empty; + Path stroke2 = empty.createStrokePolygon (2.0f); + EXPECT_TRUE (stroke2.getBounds().isEmpty()); +} + +TEST (PathTests, WithRoundedCorners) +{ + Path p; + p.addPolygon (Point (10, 10), 5, 8); + Path rounded = p.withRoundedCorners (2.0f); + EXPECT_FALSE (rounded.getBounds().isEmpty()); + // Edge: zero/negative radius + Path same = p.withRoundedCorners (0.0f); + EXPECT_FALSE (same.getBounds().isEmpty()); +} + +TEST (PathTests, ParsePathData) +{ + Path p; + // Simple SVG path: M10 10 H 90 V 90 H 10 Z + bool ok = p.parsePathData ("M10 10 H 90 V 90 H 10 Z"); + EXPECT_TRUE (ok); + EXPECT_FALSE (p.getBounds().isEmpty()); + // Edge: malformed path + Path p2; + ok = p2.parsePathData ("M10 10 Q"); + EXPECT_TRUE (ok); // Should not throw, but result is empty +} + +TEST (PathTests, AddRectangleEdgeCases) +{ + Path p; + p.addRectangle (0, 0, -10, -20); + EXPECT_TRUE (p.getBounds().isEmpty()); + + p.addRectangle (0, 0, 0, 0); + EXPECT_TRUE (p.getBounds().isEmpty()); +} + +TEST (PathTests, AddEllipseEdgeCases) +{ + Path p; + p.addEllipse (0, 0, -10, -20); + EXPECT_TRUE (p.getBounds().isEmpty()); + + p.addEllipse (0, 0, 0, 0); + EXPECT_TRUE (p.getBounds().isEmpty()); +} + +TEST (PathTests, AddRoundedRectangleEdgeCases) +{ + Path p; + p.addRoundedRectangle (0, 0, -10, -20, 2); + EXPECT_TRUE (p.getBounds().isEmpty()); + + p.addRoundedRectangle (0, 0, 0, 0, 1, 2, 3, 4); + EXPECT_TRUE (p.getBounds().isEmpty()); +} + +TEST (PathTests, AddArcEdgeCases) +{ + Path p; + p.addArc (0, 0, -10, -10, 0, MathConstants::pi, true); + EXPECT_TRUE (p.getBounds().isEmpty()); + + p.addArc (0, 0, 0, 0, 0, MathConstants::twoPi, false); + EXPECT_TRUE (p.getBounds().isEmpty()); +} + +TEST (PathTests, AddPolygonEdgeCases) +{ + Path p; + Point center (10, 10); + p.addPolygon (center, 0, 5, 0.0f); + EXPECT_TRUE (p.getBounds().isEmpty()); + + p.addPolygon (center, 2, 5, 0.0f); + EXPECT_TRUE (p.getBounds().isEmpty()); + + p.addPolygon (center, 5, 0, 0.0f); + EXPECT_TRUE (p.getBounds().isEmpty()); +} + +TEST (PathTests, AddStarEdgeCases) +{ + Path p; + Point center (10, 10); + p.addStar (center, 0, 2, 5, 0.0f); + EXPECT_TRUE (p.getBounds().isEmpty()); + + p.addStar (center, 2, 2, 5, 0.0f); + EXPECT_TRUE (p.getBounds().isEmpty()); + + p.addStar (center, 5, 0, 5, 0.0f); + EXPECT_FALSE (p.getBounds().isEmpty()); + + p.addStar (center, 5, 2, 0, 0.0f); + EXPECT_FALSE (p.getBounds().isEmpty()); +} + +TEST (PathTests, AddBubbleEdgeCases) +{ + Path p; + Rectangle body (10, 10, 40, 20); + Rectangle max (0, 0, 100, 100); + Point tip (30, 0); + p.addBubble (Rectangle(), max, tip, 5, 10); + EXPECT_TRUE (p.getBounds().isEmpty()); + + p.addBubble (body, Rectangle(), tip, 5, 10); + EXPECT_TRUE (p.getBounds().isEmpty()); + + p.addBubble (body, max, tip, 5, 0); + EXPECT_TRUE (p.getBounds().isEmpty()); +} + +TEST (PathTests, AppendPathEdgeCases) +{ + Path p1, p2; + p1.appendPath (p2); + EXPECT_TRUE (p1.getBounds().isEmpty()); +} + +TEST (PathTests, AppendPathRcpOverloadsEdgeCases) +{ + Path p1; + auto raw = rive::make_rcp(); + Path p3 (raw); + p1.appendPath (raw); + EXPECT_NE (p1.getRenderPath(), nullptr); +} + +TEST (PathTests, ScaleToFitEdgeCases) +{ + Path p; + p.addRectangle (0, 0, 10, 10); + p.scaleToFit (0, 0, 0, 0, true); + EXPECT_FALSE (p.getBounds().isEmpty()); + + p.scaleToFit (0, 0, -10, -10, false); + EXPECT_FALSE (p.getBounds().isEmpty()); +} + +TEST (PathTests, TransformEdgeCases) +{ + Path p; + p.addRectangle (0, 0, 10, 10); + AffineTransform t = AffineTransform::scaling (0, 0); + p.transform (t); + EXPECT_TRUE (p.getBounds().isEmpty()); +} + +TEST (PathTests, GetPointAlongPathEdgeCases) +{ + Path p; + p.addLine (Point (0, 0), Point (10, 10)); + Point point = p.getPointAlongPath (1.5f); + EXPECT_EQ (point, Point (10, 10)); +} + +TEST (PathTests, AllPublicApiErrorCases) +{ + Path p; + p.reserveSpace (0); + p.clear(); + p.moveTo (0, 0); + p.lineTo (0, 0); + p.quadTo (0, 0, 0, 0); + p.cubicTo (0, 0, 0, 0, 0, 0); + p.close(); + p.addLine (Point (0, 0), Point (0, 0)); + p.addLine (Line (Point (0, 0), Point (0, 0))); + p.addRectangle (Rectangle()); + p.addRoundedRectangle (Rectangle(), 0); + p.addEllipse (Rectangle()); + p.addCenteredEllipse (Point (0, 0), 0, 0); + p.addCenteredEllipse (Point (0, 0), Size (0, 0)); + p.addArc (Rectangle(), 0, 0, true); + p.addCenteredArc (Point (0, 0), 0, 0, 0, 0, 0, true); + p.addCenteredArc (Point (0, 0), Size (0, 0), 0, 0, 0, true); + p.addPolygon (Point (0, 0), 0, 0); + p.addStar (Point (0, 0), 0, 0, 0); + p.addBubble (Rectangle(), Rectangle(), Point (0, 0), 0, 0); + p.appendPath (Path()); + + Path tmp; + p.swapWithPath (tmp); + p.transform (AffineTransform()); + p.transformed (AffineTransform()); + p.scaleToFit (0, 0, 0, 0, false); + p.getBounds(); + p.getBoundsTransformed (AffineTransform()); + p.getPointAlongPath (0.0f); + p.createStrokePolygon (0.0f); + p.withRoundedCorners (0.0f); + p.parsePathData (""); + SUCCEED(); +} + +TEST (PathTests, RcpConstructorAndGetRenderPath) +{ + auto raw = rive::make_rcp(); + Path p (raw); + EXPECT_EQ (p.getRenderPath(), raw.get()); +} + +TEST (PathTests, Iterators) +{ + Path p; + p.addRectangle (0, 0, 10, 10); + auto it = p.begin(); + auto end = p.end(); + int count = 0; + for (; it != end; ++count, ++it) + { + } + EXPECT_GT (count, 0); + const Path& cp = p; + auto cit = cp.begin(); + auto cend = cp.end(); + int ccount = 0; + for (; cit != cend; ++ccount, ++cit) + { + } + EXPECT_EQ (count, ccount); +} + +TEST (PathTests, AddRectanglePractical) +{ + Path p; + p.addRectangle (0, 0, 10, 20); + EXPECT_FALSE (p.getBounds().isEmpty()); + + p.addRectangle (5, 5, 15, 25); + EXPECT_FALSE (p.getBounds().isEmpty()); +} + +TEST (PathTests, AddEllipsePractical) +{ + Path p; + p.addEllipse (0, 0, 10, 20); + EXPECT_FALSE (p.getBounds().isEmpty()); + + p.addEllipse (5, 5, 15, 25); + EXPECT_FALSE (p.getBounds().isEmpty()); +} + +TEST (PathTests, AddRoundedRectanglePractical) +{ + Path p; + p.addRoundedRectangle (0, 0, 10, 20, 2); + EXPECT_FALSE (p.getBounds().isEmpty()); + + p.addRoundedRectangle (5, 5, 15, 25, 1, 2, 3, 4); + EXPECT_FALSE (p.getBounds().isEmpty()); +} + +TEST (PathTests, AddArcPractical) +{ + Path p; + p.addArc (0, 0, 10, 10, 0, MathConstants::pi, true); + EXPECT_FALSE (p.getBounds().isEmpty()); + + p.addArc (5, 5, 10, 10, 0, MathConstants::twoPi, false); + EXPECT_FALSE (p.getBounds().isEmpty()); +} + +TEST (PathTests, AppendPathPractical) +{ + Path p1; + p1.addRectangle (0, 0, 10, 10); + Path p2; + p2.addEllipse (5, 5, 10, 10); + p1.appendPath (p2); + EXPECT_FALSE (p1.getBounds().isEmpty()); +} + +TEST (PathTests, ScaleToFitPractical) +{ + Path p; + p.addRectangle (10, 10, 20, 20); + p.scaleToFit (0, 0, 100, 50, false); + Rectangle b = p.getBounds(); + EXPECT_NEAR (b.getWidth(), 100.0f, tol); + EXPECT_NEAR (b.getHeight(), 50.0f, tol); +} From 86950d2ab3a4cbea4d086d4992cca6ccdbe2a853 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Thu, 12 Jun 2025 21:28:07 +0200 Subject: [PATCH 10/11] Missing return --- modules/yup_graphics/primitives/yup_Path.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/yup_graphics/primitives/yup_Path.cpp b/modules/yup_graphics/primitives/yup_Path.cpp index 9a040c1b8..c92b04cd6 100644 --- a/modules/yup_graphics/primitives/yup_Path.cpp +++ b/modules/yup_graphics/primitives/yup_Path.cpp @@ -466,7 +466,7 @@ Path& Path::addBubble (Rectangle bodyArea, Rectangle maximumArea, { // Just draw a rounded rectangle addRoundedRectangle (bodyArea, cornerSize); - return; + return *this; } // Determine which side the arrow should be on based on tip position relative to rectangle From af4239706c4e38c9859591ed6b8359398f3bdf59 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Thu, 12 Jun 2025 21:44:02 +0200 Subject: [PATCH 11/11] More tweaks --- cmake/yup_modules.cmake | 3 + justfile | 2 +- tests/yup_graphics/yup_Line.cpp | 196 ++++++++++++++++++++++++++++++++ tests/yup_graphics/yup_Size.cpp | 178 +++++++++++++++++++++++++++++ 4 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 tests/yup_graphics/yup_Line.cpp create mode 100644 tests/yup_graphics/yup_Size.cpp diff --git a/cmake/yup_modules.cmake b/cmake/yup_modules.cmake index 4ed77ec5f..8e741331a 100644 --- a/cmake/yup_modules.cmake +++ b/cmake/yup_modules.cmake @@ -162,6 +162,7 @@ endfunction() #============================================================================== function (_yup_module_setup_target module_name + module_path module_cpp_standard module_include_paths module_options @@ -289,6 +290,7 @@ function (_yup_module_setup_plugin_client target_name plugin_client_target folde endif() _yup_module_setup_target ("${custom_target_name}" + "${module_path}" "${module_cpp_standard}" "${module_include_paths}" "${module_options}" @@ -524,6 +526,7 @@ function (yup_add_module module_path module_group) # ==== Setup module sources and properties _yup_module_setup_target ("${module_name}" + "${module_path}" "${module_cpp_standard}" "${module_include_paths}" "${module_options}" diff --git a/justfile b/justfile index f70516662..d6679fced 100644 --- a/justfile +++ b/justfile @@ -17,7 +17,7 @@ build CONFIG="Debug": test CONFIG="Debug": cmake -G Xcode -B build cmake --build build --target yup_tests --config {{CONFIG}} - build/tests/{{CONFIG}}/yup_tests --gtest_filter=PathTests.* + build/tests/{{CONFIG}}/yup_tests --gtest_filter=* [doc("generate and open project in macOS using Xcode")] osx PROFILING="OFF": diff --git a/tests/yup_graphics/yup_Line.cpp b/tests/yup_graphics/yup_Line.cpp new file mode 100644 index 000000000..af3122b6f --- /dev/null +++ b/tests/yup_graphics/yup_Line.cpp @@ -0,0 +1,196 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include +#include +#include + +using namespace yup; + +namespace +{ +static constexpr float tol = 1e-5f; +} // namespace + +TEST (LineTests, DefaultConstructor) +{ + Line l; + EXPECT_EQ (l.getStart(), Point (0, 0)); + EXPECT_EQ (l.getEnd(), Point (0, 0)); + EXPECT_FLOAT_EQ (l.length(), 0.0f); + EXPECT_FLOAT_EQ (l.slope(), 0.0f); + EXPECT_TRUE (l.contains (Point (0, 0))); +} + +TEST (LineTests, ParameterizedConstructor) +{ + Line l (1.0f, 2.0f, 3.0f, 4.0f); + EXPECT_EQ (l.getStart(), Point (1.0f, 2.0f)); + EXPECT_EQ (l.getEnd(), Point (3.0f, 4.0f)); + EXPECT_FLOAT_EQ (l.getStartX(), 1.0f); + EXPECT_FLOAT_EQ (l.getStartY(), 2.0f); + EXPECT_FLOAT_EQ (l.getEndX(), 3.0f); + EXPECT_FLOAT_EQ (l.getEndY(), 4.0f); +} + +TEST (LineTests, SetAndWithStartEnd) +{ + Line l (1.0f, 2.0f, 3.0f, 4.0f); + l.setStart (Point (5.0f, 6.0f)); + EXPECT_EQ (l.getStart(), Point (5.0f, 6.0f)); + auto l2 = l.withStart (Point (7.0f, 8.0f)); + EXPECT_EQ (l2.getStart(), Point (7.0f, 8.0f)); + l.setEnd (Point (9.0f, 10.0f)); + EXPECT_EQ (l.getEnd(), Point (9.0f, 10.0f)); + auto l3 = l.withEnd (Point (11.0f, 12.0f)); + EXPECT_EQ (l3.getEnd(), Point (11.0f, 12.0f)); +} + +TEST (LineTests, Reverse) +{ + Line l (1.0f, 2.0f, 3.0f, 4.0f); + auto rev = l.reversed(); + EXPECT_EQ (rev.getStart(), Point (3.0f, 4.0f)); + EXPECT_EQ (rev.getEnd(), Point (1.0f, 2.0f)); + l.reverse(); + EXPECT_EQ (l.getStart(), Point (3.0f, 4.0f)); + EXPECT_EQ (l.getEnd(), Point (1.0f, 2.0f)); +} + +TEST (LineTests, LengthAndSlope) +{ + Line l (0.0f, 0.0f, 3.0f, 4.0f); + EXPECT_FLOAT_EQ (l.length(), 5.0f); + EXPECT_FLOAT_EQ (l.slope(), 4.0f / 3.0f); + Line v (1.0f, 1.0f, 1.0f, 5.0f); + EXPECT_FLOAT_EQ (v.slope(), 0.0f); +} + +TEST (LineTests, Contains) +{ + Line l (0.0f, 0.0f, 10.0f, 10.0f); + EXPECT_TRUE (l.contains (Point (5.0f, 5.0f))); + EXPECT_FALSE (l.contains (Point (5.0f, 6.0f))); + EXPECT_TRUE (l.contains (Point (5.001f, 5.001f), 0.01f)); +} + +TEST (LineTests, PointAlong) +{ + Line l (0.0f, 0.0f, 10.0f, 0.0f); + EXPECT_EQ (l.pointAlong (0.0f), Point (0, 0)); + EXPECT_EQ (l.pointAlong (0.5f), Point (5, 0)); + EXPECT_EQ (l.pointAlong (1.0f), Point (10, 0)); + EXPECT_EQ (l.pointAlong (1.5f), Point (15, 0)); +} + +TEST (LineTests, Translate) +{ + Line l (0.0f, 0.0f, 1.0f, 1.0f); + auto t = l.translated (2.0f, 3.0f); + EXPECT_EQ (t.getStart(), Point (2.0f, 3.0f)); + EXPECT_EQ (t.getEnd(), Point (3.0f, 4.0f)); + l.translate (1.0f, 1.0f); + EXPECT_EQ (l.getStart(), Point (1.0f, 1.0f)); + EXPECT_EQ (l.getEnd(), Point (2.0f, 2.0f)); +} + +TEST (LineTests, ExtendBeforeAfter) +{ + Line l (0.0f, 0.0f, 10.0f, 0.0f); + auto eb = l.extendedBefore (5.0f); + EXPECT_EQ (eb.getStart(), Point (-5.0f, 0.0f)); + auto ea = l.extendedAfter (5.0f); + EXPECT_EQ (ea.getEnd(), Point (15.0f, 0.0f)); +} + +TEST (LineTests, KeepOnlyStartAndEnd) +{ + Line l (0.0f, 0.0f, 10.0f, 0.0f); + auto ks = l.keepOnlyStart (0.5f); + EXPECT_EQ (ks.getEnd(), Point (5.0f, 0.0f)); + auto ke = l.keepOnlyEnd (0.5f); + EXPECT_EQ (ke.getStart(), Point (5.0f, 0.0f)); +} + +TEST (LineTests, RotateAtPoint) +{ + Line l (2.0f, 0.0f, 4.0f, 0.0f); + auto rl = l.rotateAtPoint (Point (2.0f, 0.0f), MathConstants::halfPi); + EXPECT_NEAR (rl.getStartX(), 2.0f, tol); + EXPECT_NEAR (rl.getStartY(), 0.0f, tol); + EXPECT_NEAR (rl.getEndX(), 2.0f, tol); + EXPECT_NEAR (rl.getEndY(), 2.0f, tol); +} + +TEST (LineTests, ToAndRoundToInt) +{ + Line lf (1.2f, 2.3f, 3.4f, 4.5f); + auto lint = lf.roundToInt(); + EXPECT_EQ (lint.getStart(), Point (1, 2)); + EXPECT_EQ (lint.getEnd(), Point (3, 4)); + auto toInt = lf.to(); + EXPECT_EQ (toInt.getStart(), Point (1, 2)); + EXPECT_EQ (toInt.getEnd(), Point (3, 4)); +} + +TEST (LineTests, UnaryMinus) +{ + Line l (1.0f, 2.0f, 3.0f, 4.0f); + auto neg = -l; + EXPECT_EQ (neg.getStart(), Point (-1.0f, -2.0f)); + EXPECT_EQ (neg.getEnd(), Point (-3.0f, -4.0f)); +} + +TEST (LineTests, Equality) +{ + Line l1 (0.0f, 0.0f, 1.0f, 1.0f); + Line l2 (0.0f, 0.0f, 1.0f, 1.0f); + Line l3 (1.0f, 1.0f, 2.0f, 2.0f); + EXPECT_TRUE (l1 == l2); + EXPECT_FALSE (l1 != l2); + EXPECT_FALSE (l1 == l3); + EXPECT_TRUE (l1 != l3); +} + +TEST (LineTests, StructuredBinding) +{ + Line l (1.0f, 2.0f, 3.0f, 4.0f); + auto [x1, y1, x2, y2] = l; + EXPECT_EQ (x1, 1.0f); + EXPECT_EQ (y1, 2.0f); + EXPECT_EQ (x2, 3.0f); + EXPECT_EQ (y2, 4.0f); +} + +TEST (LineTests, Accessors) +{ + Line l (1.0f, 2.0f, 3.0f, 4.0f); + EXPECT_EQ (l.getStart(), Point (1.0f, 2.0f)); + EXPECT_EQ (l.getEnd(), Point (3.0f, 4.0f)); +} + +TEST (LineTests, StreamOutput) +{ + Line l (1.0f, 2.0f, 3.0f, 4.0f); + String str; + str << l; + EXPECT_EQ (str, "1, 2, 3, 4"); +} diff --git a/tests/yup_graphics/yup_Size.cpp b/tests/yup_graphics/yup_Size.cpp new file mode 100644 index 000000000..57417ffcd --- /dev/null +++ b/tests/yup_graphics/yup_Size.cpp @@ -0,0 +1,178 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include +#include + +using namespace yup; + +namespace +{ +static constexpr float tol = 1e-5f; +} // namespace + +TEST (SizeTests, DefaultConstructor) +{ + Size s; + EXPECT_FLOAT_EQ (s.getWidth(), 0.0f); + EXPECT_FLOAT_EQ (s.getHeight(), 0.0f); + EXPECT_TRUE (s.isZero()); + EXPECT_TRUE (s.isEmpty()); + EXPECT_TRUE (s.isSquare()); +} + +TEST (SizeTests, ParameterizedConstructor) +{ + Size s (3.5f, 4.5f); + EXPECT_FLOAT_EQ (s.getWidth(), 3.5f); + EXPECT_FLOAT_EQ (s.getHeight(), 4.5f); + EXPECT_FALSE (s.isZero()); + EXPECT_FALSE (s.isEmpty()); + EXPECT_FALSE (s.isSquare()); +} + +TEST (SizeTests, GetSetWidthHeight) +{ + Size s; + s.setWidth (5).setHeight (6); + EXPECT_EQ (s.getWidth(), 5); + EXPECT_EQ (s.getHeight(), 6); + auto s2 = s.withWidth (7); + EXPECT_EQ (s2.getWidth(), 7); + EXPECT_EQ (s2.getHeight(), 6); + auto s3 = s.withHeight (8); + EXPECT_EQ (s3.getWidth(), 5); + EXPECT_EQ (s3.getHeight(), 8); +} + +TEST (SizeTests, EmptyAndZero) +{ + Size s1 (0, 5); + EXPECT_TRUE (s1.isEmpty()); + EXPECT_FALSE (s1.isZero()); + EXPECT_TRUE (s1.isHorizontallyEmpty()); + EXPECT_FALSE (s1.isVerticallyEmpty()); + + Size s2 (5, 0); + EXPECT_TRUE (s2.isEmpty()); + EXPECT_FALSE (s2.isZero()); + EXPECT_FALSE (s2.isHorizontallyEmpty()); + EXPECT_TRUE (s2.isVerticallyEmpty()); +} + +TEST (SizeTests, SquareCheck) +{ + Size s (5.0f, 5.0f); + EXPECT_TRUE (s.isSquare()); + s.setHeight (6.0f); + EXPECT_FALSE (s.isSquare()); +} + +TEST (SizeTests, Area) +{ + Size s (3.0f, 4.0f); + EXPECT_FLOAT_EQ (s.area(), 12.0f); +} + +TEST (SizeTests, Reverse) +{ + Size s (2.0f, 3.0f); + auto rev = s.reversed(); + EXPECT_FLOAT_EQ (rev.getWidth(), 3.0f); + EXPECT_FLOAT_EQ (rev.getHeight(), 2.0f); + s.reverse(); + EXPECT_FLOAT_EQ (s.getWidth(), 3.0f); + EXPECT_FLOAT_EQ (s.getHeight(), 2.0f); +} + +TEST (SizeTests, EnlargeReduce) +{ + Size s (2.0f, 3.0f); + auto enlarged = s.enlarged (1.0f); + EXPECT_FLOAT_EQ (enlarged.getWidth(), 3.0f); + EXPECT_FLOAT_EQ (enlarged.getHeight(), 4.0f); + s.enlarge (2.0f, 1.0f); + EXPECT_FLOAT_EQ (s.getWidth(), 4.0f); + EXPECT_FLOAT_EQ (s.getHeight(), 4.0f); + + auto reduced = s.reduced (1.0f); + EXPECT_FLOAT_EQ (reduced.getWidth(), 3.0f); + EXPECT_FLOAT_EQ (reduced.getHeight(), 3.0f); + s.reduce (1.0f, 2.0f); + EXPECT_FLOAT_EQ (s.getWidth(), 3.0f); + EXPECT_FLOAT_EQ (s.getHeight(), 2.0f); +} + +TEST (SizeTests, Scale) +{ + Size s (3.0f, 4.0f); + auto scaled = s.scaled (2.0f); + EXPECT_FLOAT_EQ (scaled.getWidth(), 6.0f); + EXPECT_FLOAT_EQ (scaled.getHeight(), 8.0f); + s.scale (0.5f, 0.25f); + EXPECT_FLOAT_EQ (s.getWidth(), 1.5f); + EXPECT_FLOAT_EQ (s.getHeight(), 1.0f); +} + +TEST (SizeTests, ConvertAndRound) +{ + Size s (3.7f, 4.2f); + auto toInt = s.to(); + EXPECT_EQ (toInt.getWidth(), 3); + EXPECT_EQ (toInt.getHeight(), 4); + auto rounded = s.roundToInt(); + EXPECT_EQ (rounded.getWidth(), 4); + EXPECT_EQ (rounded.getHeight(), 4); +} + +TEST (SizeTests, ArithmeticOperators) +{ + Size s (2.0f, 3.0f); + auto mul = s * 2.0f; + EXPECT_FLOAT_EQ (mul.getWidth(), 4.0f); + EXPECT_FLOAT_EQ (mul.getHeight(), 6.0f); + s *= 0.5f; + EXPECT_FLOAT_EQ (s.getWidth(), 1.0f); + EXPECT_FLOAT_EQ (s.getHeight(), 1.5f); + auto div = mul / 2.0f; + EXPECT_FLOAT_EQ (div.getWidth(), 2.0f); + EXPECT_FLOAT_EQ (div.getHeight(), 3.0f); + mul /= 2.0f; + EXPECT_FLOAT_EQ (mul.getWidth(), 2.0f); + EXPECT_FLOAT_EQ (mul.getHeight(), 3.0f); +} + +TEST (SizeTests, EqualityAndApproxEqual) +{ + Size s1 (2.0f, 3.0f), s2 (2.0000001f, 3.0000001f), s3 (2.1f, 3.1f); + EXPECT_TRUE (s1 == s1); + EXPECT_FALSE (s1 != s1); + EXPECT_TRUE (s1.approximatelyEqualTo (s2)); + EXPECT_FALSE (s1.approximatelyEqualTo (s3)); +} + +TEST (SizeTests, StructuredBinding) +{ + Size s (1, 2); + auto [w, h] = s; + EXPECT_EQ (w, 1); + EXPECT_EQ (h, 2); +} \ No newline at end of file