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/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/examples/graphics/source/examples/Paths.h b/examples/graphics/source/examples/Paths.h index 04138bf24..1f91ae106 100644 --- a/examples/graphics/source/examples/Paths.h +++ b/examples/graphics/source/examples/Paths.h @@ -33,7 +33,492 @@ 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 (10, 20); + 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)); + + // 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)); + + // 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)); + + // 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)); + + // Row 5: Creative Examples (full width) + 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)); } 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(), 12.0f); + } + + g.setFillColor (yup::Colors::white); + g.fillFittedText (text, area.removeFromTop (16)); + } + + void drawBasicPathOperations (yup::Graphics& g, yup::Rectangle area) + { + drawSectionTitle (g, "Basic Operations", area); + area = area.reduced (5).withTrimmedTop (20); + + yup::Path path; + float x = area.getX() + 10; + float y = area.getY() + 10; + + // Smaller rectangle + path.moveTo (x, y); + 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 (1.5f); + g.strokePath (path); + + // QuadTo demo - smaller + yup::Path quadPath; + 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 (2.0f); + g.strokePath (quadPath); + + // CubicTo demo - smaller + yup::Path cubicPath; + 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 (2.0f); + g.strokePath (cubicPath); + } + + void drawBasicShapes (yup::Graphics& g, yup::Rectangle area) + { + drawSectionTitle (g, "Basic Shapes", area); + area = area.reduced (5).withTrimmedTop (20); + + float x = area.getX() + 5; + float y = area.getY() + 5; + float spacing = 60; + + // Rectangle - smaller + yup::Path rectPath; + rectPath.addRectangle (x, y, 40, 25); + 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 + spacing, y, 40, 25, 8); + 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, y + 35, 40, 25); + 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 + 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); + } + + void drawComplexShapes (yup::Graphics& g, yup::Rectangle area) + { + drawSectionTitle (g, "Complex Shapes", area); + area = area.reduced (5).withTrimmedTop (20); + + float x = area.getX() + 30; + float y = area.getY() + 25; + + // Pentagon - smaller + yup::Path pentagonPath; + 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.0f); + g.strokePath (pentagonPath); + + // Star + yup::Path starPath; + 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 - smaller + yup::Path bubblePath; + 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 + yup::Path trianglePath; + 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); + } + + void drawArcsAndCurves (yup::Graphics& g, yup::Rectangle area) + { + drawSectionTitle (g, "Arcs & Curves", area); + area = area.reduced (5).withTrimmedTop (20); + + float x = area.getX() + 15; + float y = area.getY() + 15; + + // Simple Arc - smaller + yup::Path arcPath; + arcPath.addArc (x, y, 40, 40, 0, yup::MathConstants::halfPi, true); + g.setStrokeColor (yup::Color (255, 150, 150)); + g.setStrokeWidth (2.0f); + g.strokePath (arcPath); + + // 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); + g.setStrokeColor (yup::Color (150, 255, 150)); + g.setStrokeWidth (2.0f); + g.strokePath (centeredArcPath); + + // Complete circle using arc + yup::Path circlePath; + 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 + 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 (1.5f); + g.strokePath (complexPath); + } + + void drawPathTransformations (yup::Graphics& g, yup::Rectangle area) + { + drawSectionTitle (g, "Transformations", area); + area = area.reduced (5).withTrimmedTop (20); + + float x = area.getX() + 15; + float y = area.getY() + 15; + + // Original shape - smaller + yup::Path originalPath; + 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)); + g.setStrokeWidth (1.0f); + g.strokePath (originalPath); + + // Scaled version + 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)); + g.strokePath (scaledPath); + + // Rotated version + yup::Path rotatedPath = originalPath.transformed ( + 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 - smaller + yup::Path scaleToFitPath; + 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); + } + + void drawAdvancedFeatures (yup::Graphics& g, yup::Rectangle area) + { + drawSectionTitle (g, "Advanced Features", area); + area = area.reduced (5).withTrimmedTop (20); + + float x = area.getX() + 10; + float y = area.getY() + 10; + + // Stroke polygon demo - smaller + yup::Path originalCurve; + originalCurve.moveTo (x, y + 25); + originalCurve.quadTo (x + 25, y, x + 50, y + 25); + + yup::Path strokePolygon = originalCurve.createStrokePolygon (5.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 + 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 - smaller + yup::Path curvePath; + 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 (1.5f); + g.strokePath (curvePath); + + // 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, 3, 3); + g.setFillColor (yup::Color (255, 100, 100)); + g.fillPath (pointPath); + } + } + + void drawPathUtilities (yup::Graphics& g, yup::Rectangle area) + { + drawSectionTitle (g, "Path Utilities", area); + area = area.reduced (5).withTrimmedTop (20); + + float x = area.getX() + 10; + float y = area.getY() + 10; + + // AppendPath demo - smaller + yup::Path path1; + path1.addEllipse (x, y, 30, 30); + + yup::Path path2; + path2.addRectangle (x + 15, y + 15, 30, 30); + + 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 + 80, y + 20), 5, 10, 18, 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 + 40, y + 50), 6, 15, 0); + + g.setFillColor (yup::Color (200, 220, 255)); + g.fillPath (infoPath); + } + + void drawSVGPathData (yup::Graphics& g, yup::Rectangle area) + { + drawSectionTitle (g, "SVG Path Data", area); + area = area.reduced (5).withTrimmedTop (20); + + float x = area.getX() + 10; + float y = area.getY() + 10; + + // 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 - smaller + yup::Rectangle heartBounds = svgHeart.getBounds(); + float scale = 1.8f; + 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 - smaller + yup::Path svgTriangle; + svgTriangle.parsePathData ("M100,20 L180,160 L20,160 Z"); + 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); + } + + void drawCreativeExamples (yup::Graphics& g, yup::Rectangle area) + { + drawSectionTitle (g, "Creative Examples", area); + area = area.reduced (5).withTrimmedTop (20); + + float x = area.getX() + 40; + float y = area.getY() + 40; + + // Flower pattern using multiple shapes - smaller + yup::Point center (x, y); + + // Petals - smaller + for (int i = 0; i < 6; ++i) + { + float angle = i * yup::MathConstants::twoPi / 6.0f; + yup::Path petal; + 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 / 6.0f; + g.setFillColor (yup::Color::fromHSV (hue, 0.7f, 1.0f, 0.8f)); + g.fillPath (petal); + } + + // Center + yup::Path flowerCenter; + 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 - smaller + yup::Path gear; + gear.addPolygon (yup::Point (x + 120, y), 10, 25, 0); + yup::Path innerGear; + innerGear.addPolygon (yup::Point (x + 120, y), 10, 18, yup::MathConstants::pi / 10); + + 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 + 120, y), 6, 6); + g.setFillColor (yup::Color (245, 245, 250)); + g.fillPath (centerHole); + } + + void drawInteractiveDemo (yup::Graphics& g, yup::Rectangle area) + { + drawSectionTitle (g, "Interactive Demo", area); + area = area.reduced (5).withTrimmedTop (20); + + // Create a complex path combining multiple features - mobile optimized + yup::Path masterPath; + + float centerX = area.getCenterX(); + float centerY = area.getCenterY(); + + // Base shape - rounded rectangle (smaller) + masterPath.addRoundedRectangle (centerX - 100, centerY - 25, 200, 50, 12); + + // Add decorative elements (fewer) + for (int i = 0; i < 3; ++i) + { + yup::Path star; + float x = centerX - 60 + i * 60; + star.addStar (yup::Point (x, centerY - 40), 5, 5, 10, 0); + masterPath.appendPath (star); + } + + // Add speech bubble (smaller) + yup::Path bubble; + 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 (smaller) + yup::Path arc; + 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 (1.5f); + g.strokePath (masterPath); + } }; diff --git a/justfile b/justfile index acb09b45e..d6679fced 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=* + [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/graphics/yup_Color.h b/modules/yup_graphics/graphics/yup_Color.h index 1666d59ac..3c67ed5e2 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,115 @@ 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/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..c92b04cd6 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,12 +251,15 @@ 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; 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); @@ -273,12 +282,15 @@ 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; 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); @@ -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; @@ -368,6 +386,258 @@ 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); } +//============================================================================== +Path& Path::addPolygon (Point centre, int numberOfSides, float radius, float startAngle) +{ + 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; + 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(); + + return *this; +} + +//============================================================================== +Path& Path::addStar (Point centre, int numberOfPoints, float innerRadius, float outerRadius, float startAngle) +{ + 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; + 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(); + + return *this; +} + +//============================================================================== +Path& Path::addBubble (Rectangle bodyArea, Rectangle maximumArea, Point arrowTipPosition, float cornerSize, float arrowBaseWidth) +{ + if (bodyArea.isEmpty() || maximumArea.isEmpty() || arrowBaseWidth <= 0.0f) + return *this; + + // Clamp corner size to reasonable bounds + cornerSize = jmin (cornerSize, bodyArea.getWidth() * 0.5f, bodyArea.getHeight() * 0.5f); + + // 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 *this; + } + + // 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; + + // Get rectangle center for direction calculation + Point rectCenter = bodyArea.getCenter(); + + // Calculate relative position of arrow tip + float deltaX = arrowTipPosition.getX() - rectCenter.getX(); + float deltaY = arrowTipPosition.getY() - rectCenter.getY(); + + // 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); + } + + // Bottom-left corner + if (cornerSize > 0.0f) + { + cubicTo (x + cornerSize - cornerSize * kappa, y + height, x, y + height - cornerSize + cornerSize * kappa, x, y + height - cornerSize); + } + + // Left edge (bottom to top) + if (arrowSide == Left) + { + lineTo (arrowBase1.getX(), arrowBase1.getY()); + lineTo (arrowTipPosition.getX(), arrowTipPosition.getY()); + lineTo (arrowBase2.getX(), arrowBase2.getY()); + lineTo (x, y + cornerSize); + } + else + { + lineTo (x, y + cornerSize); + } + + // Top-left corner + if (cornerSize > 0.0f) + { + cubicTo (x, y + cornerSize - cornerSize * kappa, x + cornerSize - cornerSize * kappa, y, x + cornerSize, y); + } + + // Close the path + close(); + + return *this; +} + //============================================================================== Path& Path::appendPath (const Path& other) { @@ -393,6 +663,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 +686,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,4 +1272,564 @@ bool Path::parsePathData (const String& pathData) return true; } +//============================================================================== +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 (points.size()); + std::vector> rightSide; + rightSide.reserve (points.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; +} + +//============================================================================== +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::withRoundedCorners (float cornerRadius) const +{ + if (cornerRadius <= 0.0f || path == nullptr) + return *this; + + const auto& rawPath = 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; + pathPoints.reserve (points.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()) + { + 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..fc70ca61e 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). + */ + Path& 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). + */ + Path& 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. + */ + Path& 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,18 +512,85 @@ 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. + */ + 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; //============================================================================== - // TODO - doxygen + /** Parses the path data from a string. + + This method parses the path data from a string and updates the path accordingly. + + @param pathData The string containing the path data. + + @return True if the path data was parsed successfully, false otherwise. + */ bool parsePathData (const String& pathData); //============================================================================== diff --git a/modules/yup_graphics/primitives/yup_Rectangle.h b/modules/yup_graphics/primitives/yup_Rectangle.h index 6408bc47d..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,6 +207,33 @@ 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); + } + + //============================================================================== /** Returns the top-coordinate of the rectangle's top-left corner. @return The top-coordinate value. @@ -206,6 +243,33 @@ 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); + } + + //============================================================================== /** Returns the right-coordinate of the rectangle's bottom-right corner. @return The right-coordinate value. @@ -215,6 +279,20 @@ 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); + } + + //============================================================================== /** Returns the bottom-coordinate of the rectangle's bottom-right corner. @return The bottom-coordinate value. @@ -224,6 +302,19 @@ 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); + } + //============================================================================== /** Returns the width of the rectangle. @@ -234,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); @@ -255,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); @@ -270,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); @@ -291,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); @@ -624,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)); @@ -640,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)); @@ -718,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; @@ -725,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; @@ -863,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 }; @@ -1061,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 }; @@ -1102,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; @@ -1110,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; @@ -1118,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; @@ -1126,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; @@ -1134,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; @@ -1178,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 }; @@ -1219,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; @@ -1227,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; @@ -1235,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; @@ -1243,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; @@ -1305,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()); @@ -1385,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()); @@ -1406,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); @@ -1429,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> @@ -1518,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) 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_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); +} 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