From 529b9ce9aee9bbcdf79bc8fa5e5ff59bbe80e60c Mon Sep 17 00:00:00 2001 From: kunitoki Date: Tue, 1 Jul 2025 15:42:56 +0200 Subject: [PATCH 01/25] Improved drawable --- examples/graphics/source/examples/Svg.h | 97 ++++ examples/graphics/source/main.cpp | 14 + modules/yup_graphics/drawables/yup_Drawable.h | 435 ++++++++++++++++++ modules/yup_graphics/primitives/yup_Path.cpp | 216 +++++++++ modules/yup_graphics/primitives/yup_Path.h | 120 ++++- modules/yup_graphics/yup_graphics.h | 1 + 6 files changed, 859 insertions(+), 24 deletions(-) create mode 100644 examples/graphics/source/examples/Svg.h create mode 100644 modules/yup_graphics/drawables/yup_Drawable.h diff --git a/examples/graphics/source/examples/Svg.h b/examples/graphics/source/examples/Svg.h new file mode 100644 index 000000000..b6dd89670 --- /dev/null +++ b/examples/graphics/source/examples/Svg.h @@ -0,0 +1,97 @@ +/* + ============================================================================== + + 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. + + ============================================================================== +*/ + +#pragma once + +namespace yup +{ + +class SvgDemo : public yup::Component +{ +public: + SvgDemo() + { + updateListOfSvgFiles(); + + parseSvgFile (currentSvgFileIndex); + } + + void resized() override + { + //drawable.setBounds (getLocalBounds()); + } + + void mouseDown (const yup::MouseEvent& event) override + { + ++currentSvgFileIndex; + + parseSvgFile (currentSvgFileIndex); + } + + void paint (Graphics& g) override + { + g.setFillColor (findColor (yup::DocumentWindow::Style::backgroundColorId).value_or (yup::Colors::dimgray)); + g.fillAll(); + + drawable.paint (g); + } + +private: + void updateListOfSvgFiles() + { + yup::File riveBasePath = yup::File (__FILE__) + .getParentDirectory() + .getParentDirectory() + .getParentDirectory(); + + auto files = riveBasePath.getChildFile ("data/svg").findChildFiles (yup::File::findFiles, false, "*.svg"); + if (files.isEmpty()) + return; + + for (const auto& svgFile : files) + svgFiles.add (svgFile); + } + + void parseSvgFile (int index) + { + if (svgFiles.isEmpty()) + return; + + if (index < 0) + index = svgFiles.size() - 1; + + if (index >= svgFiles.size()) + index = 0; + + currentSvgFileIndex = index; + + drawable.clear(); + drawable.parseSVG (svgFiles[currentSvgFileIndex]); + + repaint(); + } + + yup::Drawable drawable; + Array svgFiles; + int currentSvgFileIndex = 0; +}; + +} // namespace yup diff --git a/examples/graphics/source/main.cpp b/examples/graphics/source/main.cpp index 25d8eaa20..53f7d9fbc 100644 --- a/examples/graphics/source/main.cpp +++ b/examples/graphics/source/main.cpp @@ -40,6 +40,7 @@ #include "examples/Paths.h" #include "examples/PopupMenu.h" #include "examples/TextEditor.h" +#include "examples/Svg.h" #include "examples/VariableFonts.h" #include "examples/Widgets.h" @@ -222,6 +223,19 @@ class CustomWindow addChildComponent (components.getLast()); } + { + auto button = std::make_unique ("SVG"); + button->onClick = [this, number = counter++] + { + selectComponent (number); + }; + addAndMakeVisible (button.get()); + buttons.add (std::move (button)); + + components.add (std::make_unique()); + addChildComponent (components.getLast()); + } + selectComponent (0); startTimerHz (10); diff --git a/modules/yup_graphics/drawables/yup_Drawable.h b/modules/yup_graphics/drawables/yup_Drawable.h new file mode 100644 index 000000000..521479b11 --- /dev/null +++ b/modules/yup_graphics/drawables/yup_Drawable.h @@ -0,0 +1,435 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - 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. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== + +class YUP_API Drawable +{ +public: + Drawable() + { + // transform = AffineTransform::scaling (1.0f).translated (100.0f, 100.0f); + } + + bool parseSVG (const File& svgFile) + { + XmlDocument svgDoc (svgFile); + std::unique_ptr svgRoot (svgDoc.getDocumentElement()); + + if (svgRoot == nullptr || !svgRoot->hasTagName ("svg")) + return false; + + if (auto view = svgRoot->getStringAttribute ("viewBox"); view.isNotEmpty()) + { + auto coords = StringArray::fromTokens (view, " ,", ""); + if (coords.size() == 4) + { + viewBox.setX (coords.getReference (0).getFloatValue()); + viewBox.setY (coords.getReference (1).getFloatValue()); + viewBox.setWidth (coords.getReference (2).getFloatValue()); + viewBox.setHeight (coords.getReference (3).getFloatValue()); + } + } + + auto width = svgRoot->getDoubleAttribute ("width"); + size.setWidth (width == 0.0 ? viewBox.getWidth() : width); + + auto height = svgRoot->getDoubleAttribute ("height"); + size.setWidth (height == 0.0 ? viewBox.getHeight() : height); + + //AffineTransform currentTransform; + //if (! viewBox.getTopLeft().isOrigin()) + // currentTransform = currentTransform.translated (-viewBox.getX(), -viewBox.getY()); + + return parseElement (*svgRoot, true, {}); + } + + void clear() + { + viewBox = { 0.0f, 0.0f, 0.0f, 0.0f }; + size = { 0.0f, 0.0f }; + + elements.clear(); + elementsById.clear(); + } + + void paint (Graphics& g) + { + g.setTransform (transform); + g.setStrokeWidth (1.0f); + g.setFillColor (Colors::black); + + for (const auto& element : elements) + paintElement (g, *element, true, false); + } + +private: + struct Element + { + std::optional id; + + std::optional transform; + std::optional path; + std::optional reference; + + std::optional fillColor; + std::optional strokeColor; + std::optional strokeWidth; + std::optional strokeJoin; + std::optional strokeCap; + bool noFill = false; + bool noStroke = false; + + std::optional opacity; + + std::vector> children; + }; + + void paintElement (Graphics& g, const Element& element, bool hasParentFillEnabled, bool hasParentStrokeEnabled) + { + const auto savedState = g.saveState(); + + bool isFillDefined = hasParentFillEnabled; + bool isStrokeDefined = hasParentStrokeEnabled; + + if (element.transform) + g.setTransform (element.transform->followedBy (g.getTransform())); + + if (element.opacity) + g.setOpacity (g.getOpacity() * (*element.opacity)); + + // Setup fill + if (element.fillColor) + { + g.setFillColor (*element.fillColor); + isFillDefined = true; + } + + if (isFillDefined && ! element.noFill) + { + if (element.path) + { + g.fillPath (*element.path); + } + else if (element.reference) + { + if (auto refElement = elementsById[*element.reference]; refElement != nullptr && refElement->path) + g.fillPath (*refElement->path); + } + } + + // Setup stroke + if (element.strokeColor) + { + g.setStrokeColor (*element.strokeColor); + isStrokeDefined = true; + } + + if (element.strokeJoin) + g.setStrokeJoin (*element.strokeJoin); + + if (element.strokeCap) + g.setStrokeCap (*element.strokeCap); + + if (element.strokeWidth) + g.setStrokeWidth (*element.strokeWidth); + + if (isStrokeDefined && ! element.noStroke) + { + if (element.path) + { + g.strokePath (*element.path); + } + else if (element.reference) + { + if (auto refElement = elementsById[*element.reference]; refElement != nullptr && refElement->path) + g.strokePath (*refElement->path); + } + } + + for (const auto& childElement : element.children) + paintElement (g, *childElement, isFillDefined, isStrokeDefined); + + // paintDebugElement (g, element); + } + + void paintDebugElement (Graphics& g, const Element& element) + { + if (! element.path) + return; + + for (const auto& segment : *element.path) + { + auto color = Color::opaqueRandom(); + + g.setFillColor (color); + g.fillRect (segment.point.getX() - 4, segment.point.getY() - 4, 8, 8); + + g.setStrokeColor (Colors::white); + g.setStrokeWidth (2.0f); + g.strokeRect (segment.point.getX() - 4, segment.point.getY() - 4, 8, 8); + + if (segment.verb == PathVerb::CubicTo) + { + g.setFillColor (color.brighter (0.05f)); + g.fillRect (segment.controlPoint1.getX() - 4, segment.controlPoint1.getY() - 4, 8, 8); + + g.setFillColor (color.brighter (0.1f)); + g.fillRect (segment.controlPoint2.getX() - 4, segment.controlPoint2.getY() - 4, 8, 8); + } + } + } + + bool parseElement (const XmlElement& element, bool parentIsRoot, AffineTransform currentTransform, Element* parent = nullptr) + { + auto e = std::make_shared(); + bool isRootElement = element.hasTagName ("svg"); + + if (auto id = element.getStringAttribute ("id"); id.isNotEmpty()) + { + e->id = id; + elementsById.set (id, e); + } + + if (element.hasTagName ("path")) + { + auto path = Path(); + + String pathData = element.getStringAttribute ("d"); + if (pathData.isEmpty() || ! path.fromString (pathData)) + return false; + + e->path = std::move (path); + + currentTransform = parseTransform (element, currentTransform, *e); + parseStyle (element, currentTransform, *e); + } + else if (element.hasTagName ("g")) + { + currentTransform = parseTransform (element, currentTransform, *e); + parseStyle (element, currentTransform, *e); + } + else if (element.hasTagName ("use")) + { + String href = element.getStringAttribute ("href"); + if (href.isNotEmpty() && href.startsWith ("#")) + e->reference = href.substring (1); + + currentTransform = parseTransform (element, currentTransform, *e); + parseStyle (element, currentTransform, *e); + } + else if (element.hasTagName ("ellipse")) + { + auto cx = element.getDoubleAttribute ("cx"); + auto cy = element.getDoubleAttribute ("cy"); + auto rx = element.getDoubleAttribute ("rx"); + auto ry = element.getDoubleAttribute ("ry"); + + auto path = Path(); + path.addCenteredEllipse (cx, cy, rx, ry); + e->path = std::move (path); + + currentTransform = parseTransform (element, currentTransform, *e); + parseStyle (element, currentTransform, *e); + } + else if (element.hasTagName ("circle")) + { + auto cx = element.getDoubleAttribute ("cx"); + auto cy = element.getDoubleAttribute ("cy"); + auto r = element.getDoubleAttribute ("r"); + + auto path = Path(); + path.addCenteredEllipse (cx, cy, r, r); + e->path = std::move (path); + + currentTransform = parseTransform (element, currentTransform, *e); + parseStyle (element, currentTransform, *e); + } + + for (auto* child = element.getFirstChildElement(); child != nullptr; child = child->getNextElement()) + parseElement (*child, isRootElement, currentTransform, e.get()); + + if (isRootElement) + return true; + + if (parent != nullptr && ! parentIsRoot) + parent->children.push_back (std::move (e)); + else + elements.push_back (std::move (e)); + + return true; + } + + void parseStyle (const XmlElement& element, const AffineTransform& currentTransform, Element& e) + { + String fill = element.getStringAttribute ("fill"); + if (fill.isNotEmpty()) + { + if (fill != "none") + e.fillColor = Color::fromString (fill); + else + e.noFill = true; + } + + String stroke = element.getStringAttribute ("stroke"); + if (stroke.isNotEmpty()) + { + if (stroke != "none") + e.strokeColor = Color::fromString (stroke); + else + e.noStroke = true; + } + + String strokeJoin = element.getStringAttribute ("stroke-linejoin"); + if (strokeJoin == "round") + e.strokeJoin = StrokeJoin::Round; + else if (strokeJoin == "miter") + e.strokeJoin = StrokeJoin::Miter; + else if (strokeJoin == "bevel") + e.strokeJoin = StrokeJoin::Bevel; + + String strokeCap = element.getStringAttribute ("stroke-linecap"); + if (strokeCap == "round") + e.strokeCap = StrokeCap::Round; + else if (strokeCap == "square") + e.strokeCap = StrokeCap::Square; + else if (strokeCap == "butt") + e.strokeCap = StrokeCap::Butt; + + float strokeWidth = element.getDoubleAttribute ("stroke-width", -1.0); + if (strokeWidth > 0.0) + { + auto transformScale = std::sqrtf (std::fabsf (currentTransform.followedBy (transform).getDeterminant())); + e.strokeWidth = transformScale * strokeWidth; + } + + float opacity = element.getDoubleAttribute ("opacity", -1.0); + if (opacity >= 0.0 && opacity <= 1.0) + e.opacity = opacity; + } + + AffineTransform parseTransform (const XmlElement& element, const AffineTransform& currentTransform, Element& e) + { + AffineTransform result; + + String transformString = element.getStringAttribute ("transform"); + if (transformString.isNotEmpty()) + { + auto data = transformString.getCharPointer(); + while (! data.isEmpty()) + { + // Skip whitespace + while (data.isWhitespace()) + ++data; + + if (data.isEmpty()) + break; + + // Parse transform type + String type; + while (! data.isEmpty() && CharacterFunctions::isLetter (*data)) + { + type += *data; + ++data; + } + + // Skip whitespace and the opening parenthesis + while (data.isWhitespace() || *data == '(') + ++data; + + // Parse parameters + Array params; + while (! data.isEmpty() && *data != ')') + { + if (*data == ',' || *data == ' ') + { + ++data; + continue; + } + + String number; + while (! data.isEmpty() && (*data == '-' || *data == '.' || (*data >= '0' && *data <= '9'))) + { + number += *data; + ++data; + } + + if (! number.isEmpty()) + params.add (number.getFloatValue()); + + // Skip whitespace or commas + while (data.isWhitespace() || *data == ',') + ++data; + } + + // Skip the closing parenthesis + if (*data == ')') + ++data; + + // Apply the parsed transform + if (type == "translate" && (params.size() == 1 || params.size() == 2)) + { + result = result.translated (params[0], (params.size() == 2) ? params[1] : 0.0f); + } + else if (type == "scale" && (params.size() == 1 || params.size() == 2)) + { + result = result.scaled (params[0], (params.size() == 2) ? params[1] : params[0]); + } + else if (type == "rotate" && (params.size() == 1 || params.size() == 3)) + { + if (params.size() == 1) + result = result.rotated (degreesToRadians (params[0])); + else + result = result.rotated (degreesToRadians (params[0]), params[1], params[2]); + } + else if (type == "skewX" && params.size() == 1) + { + result = result.sheared (std::tanf (degreesToRadians (params[0])), 0.0f); + } + else if (type == "skewY" && params.size() == 1) + { + result = result.sheared (0.0f, std::tanf (degreesToRadians (params[0]))); + } + else if (type == "matrix" && params.size() == 6) + { + result = result.followedBy (AffineTransform( + params[0], params[1], params[2], + params[3], params[4], params[5])); + } + } + + e.transform = result; + } + + return currentTransform.followedBy (result); + } + + Rectangle viewBox; + Size size; + AffineTransform transform; + std::vector> elements; + HashMap> elementsById; +}; + +} // namespace yup diff --git a/modules/yup_graphics/primitives/yup_Path.cpp b/modules/yup_graphics/primitives/yup_Path.cpp index f482a31f5..69577787a 100644 --- a/modules/yup_graphics/primitives/yup_Path.cpp +++ b/modules/yup_graphics/primitives/yup_Path.cpp @@ -24,6 +24,186 @@ namespace yup //============================================================================== +PathIterator::PathIterator (const rive::RawPath& rawPath, bool atEnd) + : rawPath (std::addressof (rawPath)) + , verbIndex (0) + , pointIndex (0) + , isAtEnd (atEnd) +{ + if (! isAtEnd) + updateToValidPosition(); +} + +PathSegment PathIterator::operator*() const +{ + jassert (! isAtEnd); + return createCurrentSegment(); +} + +PathIterator& PathIterator::operator++() +{ + if (isAtEnd) + return *this; + + const auto& verbs = rawPath->verbs(); + + if (verbIndex >= verbs.size()) + { + isAtEnd = true; + return *this; + } + + auto verb = verbs[verbIndex]; + + // Advance point index based on verb type + switch (verb) + { + case rive::PathVerb::move: + case rive::PathVerb::line: + pointIndex += 1; + break; + + case rive::PathVerb::quad: + pointIndex += 2; + break; + + case rive::PathVerb::cubic: + pointIndex += 3; + break; + + case rive::PathVerb::close: + // Close doesn't consume points + break; + } + + ++verbIndex; + updateToValidPosition(); + + return *this; +} + +PathIterator PathIterator::operator++ (int) +{ + PathIterator temp = *this; + ++(*this); + return temp; +} + +bool PathIterator::operator== (const PathIterator& other) const +{ + if (isAtEnd && other.isAtEnd) + return true; + + return rawPath == other.rawPath + && verbIndex == other.verbIndex + && pointIndex == other.pointIndex + && isAtEnd == other.isAtEnd; +} + +bool PathIterator::operator!= (const PathIterator& other) const +{ + return ! (*this == other); +} + +void PathIterator::updateToValidPosition() +{ + const auto& verbs = rawPath->verbs(); + + if (verbIndex >= verbs.size()) + { + isAtEnd = true; + return; + } + + // Ensure we have enough points for the current verb + const auto& points = rawPath->points(); + auto verb = verbs[verbIndex]; + + size_t requiredPoints = 0; + switch (verb) + { + case rive::PathVerb::move: + case rive::PathVerb::line: + requiredPoints = 1; + break; + + case rive::PathVerb::quad: + requiredPoints = 2; + break; + + case rive::PathVerb::cubic: + requiredPoints = 3; + break; + + case rive::PathVerb::close: + requiredPoints = 0; + break; + } + + if (pointIndex + requiredPoints > points.size() && requiredPoints > 0) + { + isAtEnd = true; + return; + } +} + +PathSegment PathIterator::createCurrentSegment() const +{ + const auto& verbs = rawPath->verbs(); + const auto& points = rawPath->points(); + + if (verbIndex >= verbs.size()) + return PathSegment::close(); // Should not happen with proper usage + + auto verb = verbs[verbIndex]; + + switch (verb) + { + case rive::PathVerb::move: + if (pointIndex < points.size()) + { + return PathSegment (PathVerb::MoveTo, + Point (points[pointIndex].x, points[pointIndex].y)); + } + break; + + case rive::PathVerb::line: + if (pointIndex < points.size()) + { + return PathSegment (PathVerb::LineTo, + Point (points[pointIndex].x, points[pointIndex].y)); + } + break; + + case rive::PathVerb::quad: + if (pointIndex + 1 < points.size()) + { + return PathSegment (PathVerb::QuadTo, + Point (points[pointIndex + 1].x, points[pointIndex + 1].y), // end point + Point (points[pointIndex].x, points[pointIndex].y)); // control point + } + break; + + case rive::PathVerb::cubic: + if (pointIndex + 2 < points.size()) + { + return PathSegment (PathVerb::CubicTo, + Point (points[pointIndex + 2].x, points[pointIndex + 2].y), // end point + Point (points[pointIndex].x, points[pointIndex].y), // control point 1 + Point (points[pointIndex + 1].x, points[pointIndex + 1].y)); // control point 2 + } + break; + + case rive::PathVerb::close: + return PathSegment::close(); + } + + // Fallback for invalid states + return PathSegment::close(); +} + +//============================================================================== + Path::Path() : path (rive::make_rcp()) { @@ -387,6 +567,7 @@ Path& Path::addCenteredArc (const Point& center, const Size& diame } //============================================================================== + Path& Path::addPolygon (Point centre, int numberOfSides, float radius, float startAngle) { if (numberOfSides < 3) @@ -419,6 +600,7 @@ Path& Path::addPolygon (Point centre, int numberOfSides, float radius, fl } //============================================================================== + Path& Path::addStar (Point centre, int numberOfPoints, float innerRadius, float outerRadius, float startAngle) { if (numberOfPoints < 3) @@ -453,6 +635,7 @@ Path& Path::addStar (Point centre, int numberOfPoints, float innerRadius, } //============================================================================== + Path& Path::addBubble (Rectangle bodyArea, Rectangle maximumArea, Point arrowTipPosition, float cornerSize, float arrowBaseWidth) { if (bodyArea.isEmpty() || maximumArea.isEmpty() || arrowBaseWidth <= 0.0f) @@ -639,6 +822,7 @@ Path& Path::addBubble (Rectangle bodyArea, Rectangle maximumArea, } //============================================================================== + Path& Path::appendPath (const Path& other) { path->addRenderPath (other.getRenderPath(), rive::Mat2D()); @@ -664,12 +848,14 @@ void Path::appendPath (rive::rcp other, const AffineTransf } //============================================================================== + void Path::swapWithPath (Path& other) noexcept { path.swap (other.path); } //============================================================================== + Path& Path::transform (const AffineTransform& t) { auto newPath = rive::make_rcp(); @@ -686,6 +872,7 @@ Path Path::transformed (const AffineTransform& t) const } //============================================================================== + Rectangle Path::getBounds() const { const auto& aabb = path->getBounds(); @@ -698,6 +885,7 @@ Rectangle Path::getBoundsTransformed (const AffineTransform& transform) c } //============================================================================== + void Path::scaleToFit (float x, float y, float width, float height, bool preserveProportions) noexcept { if (width <= 0.0f || height <= 0.0f) @@ -728,12 +916,36 @@ void Path::scaleToFit (float x, float y, float width, float height, bool preserv } //============================================================================== + +PathIterator Path::begin() +{ + return PathIterator (path->getRawPath(), false); +} + +PathIterator Path::begin() const +{ + return PathIterator (path->getRawPath(), false); +} + +PathIterator Path::end() +{ + return PathIterator (path->getRawPath(), true); +} + +PathIterator Path::end() const +{ + return PathIterator (path->getRawPath(), true); +} + +//============================================================================== + rive::RiveRenderPath* Path::getRenderPath() const { return path.get(); } //============================================================================== + String Path::toString() const { const auto& rawPath = path->getRawPath(); @@ -807,8 +1019,10 @@ String Path::toString() const } //============================================================================== + namespace { + bool isControlMarker (String::CharPointerType data) { return ! data.isEmpty() && String ("MmLlHhVvQqCcSsZz").containsChar (*data); @@ -1346,6 +1560,7 @@ bool Path::fromString (const String& pathData) } //============================================================================== + Point Path::getPointAlongPath (float distance) const { // Clamp distance to valid range @@ -1538,6 +1753,7 @@ Point Path::getPointAlongPath (float distance) const } //============================================================================== + Path Path::createStrokePolygon (float strokeWidth) const { // For now, create a simple approximation by offsetting the path diff --git a/modules/yup_graphics/primitives/yup_Path.h b/modules/yup_graphics/primitives/yup_Path.h index 38175f576..ee242439d 100644 --- a/modules/yup_graphics/primitives/yup_Path.h +++ b/modules/yup_graphics/primitives/yup_Path.h @@ -22,6 +22,90 @@ namespace yup { +//============================================================================== +/** Represents the type of operation in a path segment. */ +enum class PathVerb +{ + MoveTo, /**< Move to a point without drawing. */ + LineTo, /**< Draw a line to a point. */ + QuadTo, /**< Draw a quadratic Bezier curve. */ + CubicTo, /**< Draw a cubic Bezier curve. */ + Close /**< Close the current sub-path. */ +}; + +//============================================================================== +/** Represents a segment in a path with its verb and associated points. */ +struct PathSegment +{ + PathVerb verb; /**< The type of path operation. */ + Point point; /**< The main point (end point for most operations). */ + Point controlPoint1; /**< First control point for curves. */ + Point controlPoint2; /**< Second control point for cubic curves. */ + + /** Creates a MoveTo or LineTo segment. */ + PathSegment (PathVerb v, Point p) + : verb (v), point (p), controlPoint1 (0.0f, 0.0f), controlPoint2 (0.0f, 0.0f) + { + } + + /** Creates a QuadTo segment. */ + PathSegment (PathVerb v, Point p, Point c1) + : verb (v), point (p), controlPoint1 (c1), controlPoint2 (0.0f, 0.0f) + { + } + + /** Creates a CubicTo segment. */ + PathSegment (PathVerb v, Point p, Point c1, Point c2) + : verb (v), point (p), controlPoint1 (c1), controlPoint2 (c2) + { + } + + /** Creates a Close segment. */ + static PathSegment close() + { + return PathSegment (PathVerb::Close, Point (0.0f, 0.0f)); + } +}; + +//============================================================================== +/** A forward iterator for iterating through path segments. */ +class PathIterator +{ +public: + /** Creates an iterator for the given path. */ + PathIterator (const rive::RawPath& rawPath, bool atEnd = false); + + /** Copy constructor. */ + PathIterator (const PathIterator& other) = default; + + /** Assignment operator. */ + PathIterator& operator= (const PathIterator& other) = default; + + /** Dereference operator to get the current segment. */ + PathSegment operator*() const; + + /** Pre-increment operator. */ + PathIterator& operator++(); + + /** Post-increment operator. */ + PathIterator operator++ (int); + + /** Equality comparison. */ + bool operator== (const PathIterator& other) const; + + /** Inequality comparison. */ + bool operator!= (const PathIterator& other) const; + +private: + const rive::RawPath* rawPath; + size_t verbIndex; + size_t pointIndex; + bool isAtEnd; + + void updateToValidPosition(); + PathSegment createCurrentSegment() const; +}; + //============================================================================== /** Represents a 2D geometric path. @@ -605,51 +689,39 @@ class YUP_API Path //============================================================================== /** Provides an iterator to the beginning of the path data. - This method returns an iterator pointing to the first segment in the path's internal data. + This method returns an iterator pointing to the first segment in the path's data. It allows for iteration over the path's segments from the beginning to the end. - @return An iterator to the beginning of the path data. + @return A PathIterator to the beginning of the path data. */ - auto begin() - { - return path->getRawPath().begin(); - } + PathIterator begin(); /** Provides a constant iterator to the beginning of the path data. This method returns a constant iterator pointing to the first segment in the path's - internal data. It ensures that the path data cannot be modified during the iteration. + data. It ensures that the path data cannot be modified during the iteration. - @return A constant iterator to the beginning of the path data. + @return A constant PathIterator to the beginning of the path data. */ - auto begin() const - { - return path->getRawPath().begin(); - } + PathIterator begin() const; /** Provides an iterator to the end of the path data. - This method returns an iterator pointing just past the last segment in the path's internal data. + This method returns an iterator pointing just past the last segment in the path's data. It is used in conjunction with begin() for range-based iteration over the path's segments. - @return An iterator to the end of the path data. + @return A PathIterator to the end of the path data. */ - auto end() - { - return path->getRawPath().end(); - } + PathIterator end(); /** Provides a constant iterator to the end of the path data. This method returns a constant iterator pointing just past the last segment in the path's - internal data. It ensures safe iteration over the path segments without modifying them. + data. It ensures safe iteration over the path segments without modifying them. - @return A constant iterator to the end of the path data. + @return A constant PathIterator to the end of the path data. */ - auto end() const - { - return path->getRawPath().end(); - } + PathIterator end() const; //============================================================================== /** @internal Constructs a path from a raw render path. */ diff --git a/modules/yup_graphics/yup_graphics.h b/modules/yup_graphics/yup_graphics.h index 21b8b97f3..c552f7a9e 100644 --- a/modules/yup_graphics/yup_graphics.h +++ b/modules/yup_graphics/yup_graphics.h @@ -83,3 +83,4 @@ YUP_END_IGNORE_WARNINGS_GCC_LIKE #include "graphics/yup_StrokeCap.h" #include "graphics/yup_Graphics.h" #include "context/yup_GraphicsContext.h" +#include "drawables/yup_Drawable.h" From f58e1882a8e0613ef085a5f96b72087ca28212c4 Mon Sep 17 00:00:00 2001 From: Yup Bot Date: Tue, 1 Jul 2025 13:43:38 +0000 Subject: [PATCH 02/25] Code formatting --- examples/graphics/source/examples/Svg.h | 6 ++--- modules/yup_graphics/drawables/yup_Drawable.h | 7 +++--- modules/yup_graphics/primitives/yup_Path.cpp | 14 +++++------ modules/yup_graphics/primitives/yup_Path.h | 25 +++++++++++++------ 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/examples/graphics/source/examples/Svg.h b/examples/graphics/source/examples/Svg.h index b6dd89670..6cf9767d8 100644 --- a/examples/graphics/source/examples/Svg.h +++ b/examples/graphics/source/examples/Svg.h @@ -58,9 +58,9 @@ class SvgDemo : public yup::Component void updateListOfSvgFiles() { yup::File riveBasePath = yup::File (__FILE__) - .getParentDirectory() - .getParentDirectory() - .getParentDirectory(); + .getParentDirectory() + .getParentDirectory() + .getParentDirectory(); auto files = riveBasePath.getChildFile ("data/svg").findChildFiles (yup::File::findFiles, false, "*.svg"); if (files.isEmpty()) diff --git a/modules/yup_graphics/drawables/yup_Drawable.h b/modules/yup_graphics/drawables/yup_Drawable.h index 521479b11..1dec324a0 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.h +++ b/modules/yup_graphics/drawables/yup_Drawable.h @@ -37,7 +37,7 @@ class YUP_API Drawable XmlDocument svgDoc (svgFile); std::unique_ptr svgRoot (svgDoc.getDocumentElement()); - if (svgRoot == nullptr || !svgRoot->hasTagName ("svg")) + if (svgRoot == nullptr || ! svgRoot->hasTagName ("svg")) return false; if (auto view = svgRoot->getStringAttribute ("viewBox"); view.isNotEmpty()) @@ -413,9 +413,8 @@ class YUP_API Drawable } else if (type == "matrix" && params.size() == 6) { - result = result.followedBy (AffineTransform( - params[0], params[1], params[2], - params[3], params[4], params[5])); + result = result.followedBy (AffineTransform ( + params[0], params[1], params[2], params[3], params[4], params[5])); } } diff --git a/modules/yup_graphics/primitives/yup_Path.cpp b/modules/yup_graphics/primitives/yup_Path.cpp index 69577787a..7050ac33c 100644 --- a/modules/yup_graphics/primitives/yup_Path.cpp +++ b/modules/yup_graphics/primitives/yup_Path.cpp @@ -163,7 +163,7 @@ PathSegment PathIterator::createCurrentSegment() const if (pointIndex < points.size()) { return PathSegment (PathVerb::MoveTo, - Point (points[pointIndex].x, points[pointIndex].y)); + Point (points[pointIndex].x, points[pointIndex].y)); } break; @@ -171,7 +171,7 @@ PathSegment PathIterator::createCurrentSegment() const if (pointIndex < points.size()) { return PathSegment (PathVerb::LineTo, - Point (points[pointIndex].x, points[pointIndex].y)); + Point (points[pointIndex].x, points[pointIndex].y)); } break; @@ -179,8 +179,8 @@ PathSegment PathIterator::createCurrentSegment() const if (pointIndex + 1 < points.size()) { return PathSegment (PathVerb::QuadTo, - Point (points[pointIndex + 1].x, points[pointIndex + 1].y), // end point - Point (points[pointIndex].x, points[pointIndex].y)); // control point + Point (points[pointIndex + 1].x, points[pointIndex + 1].y), // end point + Point (points[pointIndex].x, points[pointIndex].y)); // control point } break; @@ -188,9 +188,9 @@ PathSegment PathIterator::createCurrentSegment() const if (pointIndex + 2 < points.size()) { return PathSegment (PathVerb::CubicTo, - Point (points[pointIndex + 2].x, points[pointIndex + 2].y), // end point - Point (points[pointIndex].x, points[pointIndex].y), // control point 1 - Point (points[pointIndex + 1].x, points[pointIndex + 1].y)); // control point 2 + Point (points[pointIndex + 2].x, points[pointIndex + 2].y), // end point + Point (points[pointIndex].x, points[pointIndex].y), // control point 1 + Point (points[pointIndex + 1].x, points[pointIndex + 1].y)); // control point 2 } break; diff --git a/modules/yup_graphics/primitives/yup_Path.h b/modules/yup_graphics/primitives/yup_Path.h index ee242439d..46ea9c4ca 100644 --- a/modules/yup_graphics/primitives/yup_Path.h +++ b/modules/yup_graphics/primitives/yup_Path.h @@ -26,11 +26,11 @@ namespace yup /** Represents the type of operation in a path segment. */ enum class PathVerb { - MoveTo, /**< Move to a point without drawing. */ - LineTo, /**< Draw a line to a point. */ - QuadTo, /**< Draw a quadratic Bezier curve. */ - CubicTo, /**< Draw a cubic Bezier curve. */ - Close /**< Close the current sub-path. */ + MoveTo, /**< Move to a point without drawing. */ + LineTo, /**< Draw a line to a point. */ + QuadTo, /**< Draw a quadratic Bezier curve. */ + CubicTo, /**< Draw a cubic Bezier curve. */ + Close /**< Close the current sub-path. */ }; //============================================================================== @@ -44,19 +44,28 @@ struct PathSegment /** Creates a MoveTo or LineTo segment. */ PathSegment (PathVerb v, Point p) - : verb (v), point (p), controlPoint1 (0.0f, 0.0f), controlPoint2 (0.0f, 0.0f) + : verb (v) + , point (p) + , controlPoint1 (0.0f, 0.0f) + , controlPoint2 (0.0f, 0.0f) { } /** Creates a QuadTo segment. */ PathSegment (PathVerb v, Point p, Point c1) - : verb (v), point (p), controlPoint1 (c1), controlPoint2 (0.0f, 0.0f) + : verb (v) + , point (p) + , controlPoint1 (c1) + , controlPoint2 (0.0f, 0.0f) { } /** Creates a CubicTo segment. */ PathSegment (PathVerb v, Point p, Point c1, Point c2) - : verb (v), point (p), controlPoint1 (c1), controlPoint2 (c2) + : verb (v) + , point (p) + , controlPoint1 (c1) + , controlPoint2 (c2) { } From d6ef79566e0c9afd405ad05e13b739779ef50695 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Tue, 1 Jul 2025 15:46:58 +0200 Subject: [PATCH 03/25] Fix issues --- .../yup_graphics/drawables/yup_Drawable.cpp | 419 ++++++++++++++++++ modules/yup_graphics/yup_graphics.cpp | 1 + 2 files changed, 420 insertions(+) create mode 100644 modules/yup_graphics/drawables/yup_Drawable.cpp diff --git a/modules/yup_graphics/drawables/yup_Drawable.cpp b/modules/yup_graphics/drawables/yup_Drawable.cpp new file mode 100644 index 000000000..0c7855b44 --- /dev/null +++ b/modules/yup_graphics/drawables/yup_Drawable.cpp @@ -0,0 +1,419 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - 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. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== + +Drawable::Drawable() +{ + // transform = AffineTransform::scaling (1.0f).translated (100.0f, 100.0f); +} + +//============================================================================== + +bool Drawable::parseSVG (const File& svgFile) +{ + XmlDocument svgDoc (svgFile); + std::unique_ptr svgRoot (svgDoc.getDocumentElement()); + + if (svgRoot == nullptr || !svgRoot->hasTagName ("svg")) + return false; + + if (auto view = svgRoot->getStringAttribute ("viewBox"); view.isNotEmpty()) + { + auto coords = StringArray::fromTokens (view, " ,", ""); + if (coords.size() == 4) + { + viewBox.setX (coords.getReference (0).getFloatValue()); + viewBox.setY (coords.getReference (1).getFloatValue()); + viewBox.setWidth (coords.getReference (2).getFloatValue()); + viewBox.setHeight (coords.getReference (3).getFloatValue()); + } + } + + auto width = svgRoot->getDoubleAttribute ("width"); + size.setWidth (width == 0.0 ? viewBox.getWidth() : width); + + auto height = svgRoot->getDoubleAttribute ("height"); + size.setWidth (height == 0.0 ? viewBox.getHeight() : height); + + //AffineTransform currentTransform; + //if (! viewBox.getTopLeft().isOrigin()) + // currentTransform = currentTransform.translated (-viewBox.getX(), -viewBox.getY()); + + return parseElement (*svgRoot, true, {}); +} + +//============================================================================== + +void Drawable::clear() +{ + viewBox = { 0.0f, 0.0f, 0.0f, 0.0f }; + size = { 0.0f, 0.0f }; + + elements.clear(); + elementsById.clear(); +} + +//============================================================================== + +void Drawable::paint (Graphics& g) +{ + g.setTransform (transform); + g.setStrokeWidth (1.0f); + g.setFillColor (Colors::black); + + for (const auto& element : elements) + paintElement (g, *element, true, false); +} + +//============================================================================== + +void Drawable::paintElement (Graphics& g, const Element& element, bool hasParentFillEnabled, bool hasParentStrokeEnabled) +{ + const auto savedState = g.saveState(); + + bool isFillDefined = hasParentFillEnabled; + bool isStrokeDefined = hasParentStrokeEnabled; + + if (element.transform) + g.setTransform (element.transform->followedBy (g.getTransform())); + + if (element.opacity) + g.setOpacity (g.getOpacity() * (*element.opacity)); + + // Setup fill + if (element.fillColor) + { + g.setFillColor (*element.fillColor); + isFillDefined = true; + } + + if (isFillDefined && ! element.noFill) + { + if (element.path) + { + g.fillPath (*element.path); + } + else if (element.reference) + { + if (auto refElement = elementsById[*element.reference]; refElement != nullptr && refElement->path) + g.fillPath (*refElement->path); + } + } + + // Setup stroke + if (element.strokeColor) + { + g.setStrokeColor (*element.strokeColor); + isStrokeDefined = true; + } + + if (element.strokeJoin) + g.setStrokeJoin (*element.strokeJoin); + + if (element.strokeCap) + g.setStrokeCap (*element.strokeCap); + + if (element.strokeWidth) + g.setStrokeWidth (*element.strokeWidth); + + if (isStrokeDefined && ! element.noStroke) + { + if (element.path) + { + g.strokePath (*element.path); + } + else if (element.reference) + { + if (auto refElement = elementsById[*element.reference]; refElement != nullptr && refElement->path) + g.strokePath (*refElement->path); + } + } + + for (const auto& childElement : element.children) + paintElement (g, *childElement, isFillDefined, isStrokeDefined); + + // paintDebugElement (g, element); +} + +//============================================================================== + +bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, AffineTransform currentTransform, Element* parent) +{ + auto e = std::make_shared(); + bool isRootElement = element.hasTagName ("svg"); + + if (auto id = element.getStringAttribute ("id"); id.isNotEmpty()) + { + e->id = id; + elementsById.set (id, e); + } + + if (element.hasTagName ("path")) + { + auto path = Path(); + + String pathData = element.getStringAttribute ("d"); + if (pathData.isEmpty() || ! path.fromString (pathData)) + return false; + + e->path = std::move (path); + + currentTransform = parseTransform (element, currentTransform, *e); + parseStyle (element, currentTransform, *e); + } + else if (element.hasTagName ("g")) + { + currentTransform = parseTransform (element, currentTransform, *e); + parseStyle (element, currentTransform, *e); + } + else if (element.hasTagName ("use")) + { + String href = element.getStringAttribute ("href"); + if (href.isNotEmpty() && href.startsWith ("#")) + e->reference = href.substring (1); + + currentTransform = parseTransform (element, currentTransform, *e); + parseStyle (element, currentTransform, *e); + } + else if (element.hasTagName ("ellipse")) + { + auto cx = element.getDoubleAttribute ("cx"); + auto cy = element.getDoubleAttribute ("cy"); + auto rx = element.getDoubleAttribute ("rx"); + auto ry = element.getDoubleAttribute ("ry"); + + auto path = Path(); + path.addCenteredEllipse (cx, cy, rx, ry); + e->path = std::move (path); + + currentTransform = parseTransform (element, currentTransform, *e); + parseStyle (element, currentTransform, *e); + } + else if (element.hasTagName ("circle")) + { + auto cx = element.getDoubleAttribute ("cx"); + auto cy = element.getDoubleAttribute ("cy"); + auto r = element.getDoubleAttribute ("r"); + + auto path = Path(); + path.addCenteredEllipse (cx, cy, r, r); + e->path = std::move (path); + + currentTransform = parseTransform (element, currentTransform, *e); + parseStyle (element, currentTransform, *e); + } + + for (auto* child = element.getFirstChildElement(); child != nullptr; child = child->getNextElement()) + parseElement (*child, isRootElement, currentTransform, e.get()); + + if (isRootElement) + return true; + + if (parent != nullptr && ! parentIsRoot) + parent->children.push_back (std::move (e)); + else + elements.push_back (std::move (e)); + + return true; +} + +//============================================================================== + +void Drawable::parseStyle (const XmlElement& element, const AffineTransform& currentTransform, Element& e) +{ + String fill = element.getStringAttribute ("fill"); + if (fill.isNotEmpty()) + { + if (fill != "none") + e.fillColor = Color::fromString (fill); + else + e.noFill = true; + } + + String stroke = element.getStringAttribute ("stroke"); + if (stroke.isNotEmpty()) + { + if (stroke != "none") + e.strokeColor = Color::fromString (stroke); + else + e.noStroke = true; + } + + String strokeJoin = element.getStringAttribute ("stroke-linejoin"); + if (strokeJoin == "round") + e.strokeJoin = StrokeJoin::Round; + else if (strokeJoin == "miter") + e.strokeJoin = StrokeJoin::Miter; + else if (strokeJoin == "bevel") + e.strokeJoin = StrokeJoin::Bevel; + + String strokeCap = element.getStringAttribute ("stroke-linecap"); + if (strokeCap == "round") + e.strokeCap = StrokeCap::Round; + else if (strokeCap == "square") + e.strokeCap = StrokeCap::Square; + else if (strokeCap == "butt") + e.strokeCap = StrokeCap::Butt; + + float strokeWidth = element.getDoubleAttribute ("stroke-width", -1.0); + if (strokeWidth > 0.0) + { + auto transformScale = std::sqrtf (std::fabsf (currentTransform.followedBy (transform).getDeterminant())); + e.strokeWidth = transformScale * strokeWidth; + } + + float opacity = element.getDoubleAttribute ("opacity", -1.0); + if (opacity >= 0.0 && opacity <= 1.0) + e.opacity = opacity; +} + +//============================================================================== + +AffineTransform Drawable::parseTransform (const XmlElement& element, const AffineTransform& currentTransform, Element& e) +{ + AffineTransform result; + + String transformString = element.getStringAttribute ("transform"); + if (transformString.isNotEmpty()) + { + auto data = transformString.getCharPointer(); + while (! data.isEmpty()) + { + // Skip whitespace + while (data.isWhitespace()) + ++data; + + if (data.isEmpty()) + break; + + // Parse transform type + String type; + while (! data.isEmpty() && CharacterFunctions::isLetter (*data)) + { + type += *data; + ++data; + } + + // Skip whitespace and the opening parenthesis + while (data.isWhitespace() || *data == '(') + ++data; + + // Parse parameters + Array params; + while (! data.isEmpty() && *data != ')') + { + if (*data == ',' || *data == ' ') + { + ++data; + continue; + } + + String number; + while (! data.isEmpty() && (*data == '-' || *data == '.' || (*data >= '0' && *data <= '9'))) + { + number += *data; + ++data; + } + + if (! number.isEmpty()) + params.add (number.getFloatValue()); + + // Skip whitespace or commas + while (data.isWhitespace() || *data == ',') + ++data; + } + + // Skip the closing parenthesis + if (*data == ')') + ++data; + + // Apply the parsed transform + if (type == "translate" && (params.size() == 1 || params.size() == 2)) + { + result = result.translated (params[0], (params.size() == 2) ? params[1] : 0.0f); + } + else if (type == "scale" && (params.size() == 1 || params.size() == 2)) + { + result = result.scaled (params[0], (params.size() == 2) ? params[1] : params[0]); + } + else if (type == "rotate" && (params.size() == 1 || params.size() == 3)) + { + if (params.size() == 1) + result = result.rotated (degreesToRadians (params[0])); + else + result = result.rotated (degreesToRadians (params[0]), params[1], params[2]); + } + else if (type == "skewX" && params.size() == 1) + { + result = result.sheared (std::tanf (degreesToRadians (params[0])), 0.0f); + } + else if (type == "skewY" && params.size() == 1) + { + result = result.sheared (0.0f, std::tanf (degreesToRadians (params[0]))); + } + else if (type == "matrix" && params.size() == 6) + { + result = result.followedBy (AffineTransform( + params[0], params[1], params[2], + params[3], params[4], params[5])); + } + } + + e.transform = result; + } + + return currentTransform.followedBy (result); +} + +//============================================================================== + +void Drawable::paintDebugElement (Graphics& g, const Element& element) +{ + if (! element.path) + return; + + for (const auto& segment : *element.path) + { + auto color = Color::opaqueRandom(); + + g.setFillColor (color); + g.fillRect (segment.point.getX() - 4, segment.point.getY() - 4, 8, 8); + + g.setStrokeColor (Colors::white); + g.setStrokeWidth (2.0f); + g.strokeRect (segment.point.getX() - 4, segment.point.getY() - 4, 8, 8); + + if (segment.verb == PathVerb::CubicTo) + { + g.setFillColor (color.brighter (0.05f)); + g.fillRect (segment.controlPoint1.getX() - 4, segment.controlPoint1.getY() - 4, 8, 8); + + g.setFillColor (color.brighter (0.1f)); + g.fillRect (segment.controlPoint2.getX() - 4, segment.controlPoint2.getY() - 4, 8, 8); + } + } +} + +} // namespace yup diff --git a/modules/yup_graphics/yup_graphics.cpp b/modules/yup_graphics/yup_graphics.cpp index b5632bb19..82d9bf18c 100644 --- a/modules/yup_graphics/yup_graphics.cpp +++ b/modules/yup_graphics/yup_graphics.cpp @@ -109,3 +109,4 @@ #include "graphics/yup_Color.cpp" #include "graphics/yup_Colors.cpp" #include "graphics/yup_Graphics.cpp" +#include "drawables/yup_Drawable.cpp" From d58a84bda8838248c11286d0e756fe2f65fd8694 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Tue, 1 Jul 2025 16:53:46 +0200 Subject: [PATCH 04/25] Added tiger --- examples/graphics/data/svg/tiger.svg | 725 +++++++++++++++++++++++++++ 1 file changed, 725 insertions(+) create mode 100644 examples/graphics/data/svg/tiger.svg diff --git a/examples/graphics/data/svg/tiger.svg b/examples/graphics/data/svg/tiger.svg new file mode 100644 index 000000000..679edec2e --- /dev/null +++ b/examples/graphics/data/svg/tiger.svg @@ -0,0 +1,725 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 69ce37b885f3f93f6d75fa0a1cf252808dbf2946 Mon Sep 17 00:00:00 2001 From: Yup Bot Date: Tue, 1 Jul 2025 14:56:14 +0000 Subject: [PATCH 05/25] Code formatting --- modules/yup_graphics/drawables/yup_Drawable.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/yup_graphics/drawables/yup_Drawable.cpp b/modules/yup_graphics/drawables/yup_Drawable.cpp index 0c7855b44..710ebef17 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.cpp +++ b/modules/yup_graphics/drawables/yup_Drawable.cpp @@ -36,7 +36,7 @@ bool Drawable::parseSVG (const File& svgFile) XmlDocument svgDoc (svgFile); std::unique_ptr svgRoot (svgDoc.getDocumentElement()); - if (svgRoot == nullptr || !svgRoot->hasTagName ("svg")) + if (svgRoot == nullptr || ! svgRoot->hasTagName ("svg")) return false; if (auto view = svgRoot->getStringAttribute ("viewBox"); view.isNotEmpty()) @@ -375,9 +375,8 @@ AffineTransform Drawable::parseTransform (const XmlElement& element, const Affin } else if (type == "matrix" && params.size() == 6) { - result = result.followedBy (AffineTransform( - params[0], params[1], params[2], - params[3], params[4], params[5])); + result = result.followedBy (AffineTransform ( + params[0], params[1], params[2], params[3], params[4], params[5])); } } From c46c01cbccb16388b3dce02e0b951da8a6cbc43e Mon Sep 17 00:00:00 2001 From: kunitoki Date: Tue, 1 Jul 2025 16:57:01 +0200 Subject: [PATCH 06/25] Fix --- modules/yup_graphics/drawables/yup_Drawable.h | 379 +----------------- 1 file changed, 9 insertions(+), 370 deletions(-) diff --git a/modules/yup_graphics/drawables/yup_Drawable.h b/modules/yup_graphics/drawables/yup_Drawable.h index 1dec324a0..299b5176a 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.h +++ b/modules/yup_graphics/drawables/yup_Drawable.h @@ -27,62 +27,13 @@ namespace yup class YUP_API Drawable { public: - Drawable() - { - // transform = AffineTransform::scaling (1.0f).translated (100.0f, 100.0f); - } - - bool parseSVG (const File& svgFile) - { - XmlDocument svgDoc (svgFile); - std::unique_ptr svgRoot (svgDoc.getDocumentElement()); - - if (svgRoot == nullptr || ! svgRoot->hasTagName ("svg")) - return false; - - if (auto view = svgRoot->getStringAttribute ("viewBox"); view.isNotEmpty()) - { - auto coords = StringArray::fromTokens (view, " ,", ""); - if (coords.size() == 4) - { - viewBox.setX (coords.getReference (0).getFloatValue()); - viewBox.setY (coords.getReference (1).getFloatValue()); - viewBox.setWidth (coords.getReference (2).getFloatValue()); - viewBox.setHeight (coords.getReference (3).getFloatValue()); - } - } - - auto width = svgRoot->getDoubleAttribute ("width"); - size.setWidth (width == 0.0 ? viewBox.getWidth() : width); - - auto height = svgRoot->getDoubleAttribute ("height"); - size.setWidth (height == 0.0 ? viewBox.getHeight() : height); - - //AffineTransform currentTransform; - //if (! viewBox.getTopLeft().isOrigin()) - // currentTransform = currentTransform.translated (-viewBox.getX(), -viewBox.getY()); - - return parseElement (*svgRoot, true, {}); - } + Drawable(); - void clear() - { - viewBox = { 0.0f, 0.0f, 0.0f, 0.0f }; - size = { 0.0f, 0.0f }; - - elements.clear(); - elementsById.clear(); - } + bool parseSVG (const File& svgFile); - void paint (Graphics& g) - { - g.setTransform (transform); - g.setStrokeWidth (1.0f); - g.setFillColor (Colors::black); + void clear(); - for (const auto& element : elements) - paintElement (g, *element, true, false); - } + void paint (Graphics& g); private: struct Element @@ -106,323 +57,11 @@ class YUP_API Drawable std::vector> children; }; - void paintElement (Graphics& g, const Element& element, bool hasParentFillEnabled, bool hasParentStrokeEnabled) - { - const auto savedState = g.saveState(); - - bool isFillDefined = hasParentFillEnabled; - bool isStrokeDefined = hasParentStrokeEnabled; - - if (element.transform) - g.setTransform (element.transform->followedBy (g.getTransform())); - - if (element.opacity) - g.setOpacity (g.getOpacity() * (*element.opacity)); - - // Setup fill - if (element.fillColor) - { - g.setFillColor (*element.fillColor); - isFillDefined = true; - } - - if (isFillDefined && ! element.noFill) - { - if (element.path) - { - g.fillPath (*element.path); - } - else if (element.reference) - { - if (auto refElement = elementsById[*element.reference]; refElement != nullptr && refElement->path) - g.fillPath (*refElement->path); - } - } - - // Setup stroke - if (element.strokeColor) - { - g.setStrokeColor (*element.strokeColor); - isStrokeDefined = true; - } - - if (element.strokeJoin) - g.setStrokeJoin (*element.strokeJoin); - - if (element.strokeCap) - g.setStrokeCap (*element.strokeCap); - - if (element.strokeWidth) - g.setStrokeWidth (*element.strokeWidth); - - if (isStrokeDefined && ! element.noStroke) - { - if (element.path) - { - g.strokePath (*element.path); - } - else if (element.reference) - { - if (auto refElement = elementsById[*element.reference]; refElement != nullptr && refElement->path) - g.strokePath (*refElement->path); - } - } - - for (const auto& childElement : element.children) - paintElement (g, *childElement, isFillDefined, isStrokeDefined); - - // paintDebugElement (g, element); - } - - void paintDebugElement (Graphics& g, const Element& element) - { - if (! element.path) - return; - - for (const auto& segment : *element.path) - { - auto color = Color::opaqueRandom(); - - g.setFillColor (color); - g.fillRect (segment.point.getX() - 4, segment.point.getY() - 4, 8, 8); - - g.setStrokeColor (Colors::white); - g.setStrokeWidth (2.0f); - g.strokeRect (segment.point.getX() - 4, segment.point.getY() - 4, 8, 8); - - if (segment.verb == PathVerb::CubicTo) - { - g.setFillColor (color.brighter (0.05f)); - g.fillRect (segment.controlPoint1.getX() - 4, segment.controlPoint1.getY() - 4, 8, 8); - - g.setFillColor (color.brighter (0.1f)); - g.fillRect (segment.controlPoint2.getX() - 4, segment.controlPoint2.getY() - 4, 8, 8); - } - } - } - - bool parseElement (const XmlElement& element, bool parentIsRoot, AffineTransform currentTransform, Element* parent = nullptr) - { - auto e = std::make_shared(); - bool isRootElement = element.hasTagName ("svg"); - - if (auto id = element.getStringAttribute ("id"); id.isNotEmpty()) - { - e->id = id; - elementsById.set (id, e); - } - - if (element.hasTagName ("path")) - { - auto path = Path(); - - String pathData = element.getStringAttribute ("d"); - if (pathData.isEmpty() || ! path.fromString (pathData)) - return false; - - e->path = std::move (path); - - currentTransform = parseTransform (element, currentTransform, *e); - parseStyle (element, currentTransform, *e); - } - else if (element.hasTagName ("g")) - { - currentTransform = parseTransform (element, currentTransform, *e); - parseStyle (element, currentTransform, *e); - } - else if (element.hasTagName ("use")) - { - String href = element.getStringAttribute ("href"); - if (href.isNotEmpty() && href.startsWith ("#")) - e->reference = href.substring (1); - - currentTransform = parseTransform (element, currentTransform, *e); - parseStyle (element, currentTransform, *e); - } - else if (element.hasTagName ("ellipse")) - { - auto cx = element.getDoubleAttribute ("cx"); - auto cy = element.getDoubleAttribute ("cy"); - auto rx = element.getDoubleAttribute ("rx"); - auto ry = element.getDoubleAttribute ("ry"); - - auto path = Path(); - path.addCenteredEllipse (cx, cy, rx, ry); - e->path = std::move (path); - - currentTransform = parseTransform (element, currentTransform, *e); - parseStyle (element, currentTransform, *e); - } - else if (element.hasTagName ("circle")) - { - auto cx = element.getDoubleAttribute ("cx"); - auto cy = element.getDoubleAttribute ("cy"); - auto r = element.getDoubleAttribute ("r"); - - auto path = Path(); - path.addCenteredEllipse (cx, cy, r, r); - e->path = std::move (path); - - currentTransform = parseTransform (element, currentTransform, *e); - parseStyle (element, currentTransform, *e); - } - - for (auto* child = element.getFirstChildElement(); child != nullptr; child = child->getNextElement()) - parseElement (*child, isRootElement, currentTransform, e.get()); - - if (isRootElement) - return true; - - if (parent != nullptr && ! parentIsRoot) - parent->children.push_back (std::move (e)); - else - elements.push_back (std::move (e)); - - return true; - } - - void parseStyle (const XmlElement& element, const AffineTransform& currentTransform, Element& e) - { - String fill = element.getStringAttribute ("fill"); - if (fill.isNotEmpty()) - { - if (fill != "none") - e.fillColor = Color::fromString (fill); - else - e.noFill = true; - } - - String stroke = element.getStringAttribute ("stroke"); - if (stroke.isNotEmpty()) - { - if (stroke != "none") - e.strokeColor = Color::fromString (stroke); - else - e.noStroke = true; - } - - String strokeJoin = element.getStringAttribute ("stroke-linejoin"); - if (strokeJoin == "round") - e.strokeJoin = StrokeJoin::Round; - else if (strokeJoin == "miter") - e.strokeJoin = StrokeJoin::Miter; - else if (strokeJoin == "bevel") - e.strokeJoin = StrokeJoin::Bevel; - - String strokeCap = element.getStringAttribute ("stroke-linecap"); - if (strokeCap == "round") - e.strokeCap = StrokeCap::Round; - else if (strokeCap == "square") - e.strokeCap = StrokeCap::Square; - else if (strokeCap == "butt") - e.strokeCap = StrokeCap::Butt; - - float strokeWidth = element.getDoubleAttribute ("stroke-width", -1.0); - if (strokeWidth > 0.0) - { - auto transformScale = std::sqrtf (std::fabsf (currentTransform.followedBy (transform).getDeterminant())); - e.strokeWidth = transformScale * strokeWidth; - } - - float opacity = element.getDoubleAttribute ("opacity", -1.0); - if (opacity >= 0.0 && opacity <= 1.0) - e.opacity = opacity; - } - - AffineTransform parseTransform (const XmlElement& element, const AffineTransform& currentTransform, Element& e) - { - AffineTransform result; - - String transformString = element.getStringAttribute ("transform"); - if (transformString.isNotEmpty()) - { - auto data = transformString.getCharPointer(); - while (! data.isEmpty()) - { - // Skip whitespace - while (data.isWhitespace()) - ++data; - - if (data.isEmpty()) - break; - - // Parse transform type - String type; - while (! data.isEmpty() && CharacterFunctions::isLetter (*data)) - { - type += *data; - ++data; - } - - // Skip whitespace and the opening parenthesis - while (data.isWhitespace() || *data == '(') - ++data; - - // Parse parameters - Array params; - while (! data.isEmpty() && *data != ')') - { - if (*data == ',' || *data == ' ') - { - ++data; - continue; - } - - String number; - while (! data.isEmpty() && (*data == '-' || *data == '.' || (*data >= '0' && *data <= '9'))) - { - number += *data; - ++data; - } - - if (! number.isEmpty()) - params.add (number.getFloatValue()); - - // Skip whitespace or commas - while (data.isWhitespace() || *data == ',') - ++data; - } - - // Skip the closing parenthesis - if (*data == ')') - ++data; - - // Apply the parsed transform - if (type == "translate" && (params.size() == 1 || params.size() == 2)) - { - result = result.translated (params[0], (params.size() == 2) ? params[1] : 0.0f); - } - else if (type == "scale" && (params.size() == 1 || params.size() == 2)) - { - result = result.scaled (params[0], (params.size() == 2) ? params[1] : params[0]); - } - else if (type == "rotate" && (params.size() == 1 || params.size() == 3)) - { - if (params.size() == 1) - result = result.rotated (degreesToRadians (params[0])); - else - result = result.rotated (degreesToRadians (params[0]), params[1], params[2]); - } - else if (type == "skewX" && params.size() == 1) - { - result = result.sheared (std::tanf (degreesToRadians (params[0])), 0.0f); - } - else if (type == "skewY" && params.size() == 1) - { - result = result.sheared (0.0f, std::tanf (degreesToRadians (params[0]))); - } - else if (type == "matrix" && params.size() == 6) - { - result = result.followedBy (AffineTransform ( - params[0], params[1], params[2], params[3], params[4], params[5])); - } - } - - e.transform = result; - } - - return currentTransform.followedBy (result); - } + void paintElement (Graphics& g, const Element& element, bool hasParentFillEnabled, bool hasParentStrokeEnabled); + void paintDebugElement (Graphics& g, const Element& element); + bool parseElement (const XmlElement& element, bool parentIsRoot, AffineTransform currentTransform, Element* parent = nullptr); + void parseStyle (const XmlElement& element, const AffineTransform& currentTransform, Element& e); + AffineTransform parseTransform (const XmlElement& element, const AffineTransform& currentTransform, Element& e); Rectangle viewBox; Size size; From 7e68640b2fb64c3565ce3a066a5d4adca33d509d Mon Sep 17 00:00:00 2001 From: kunitoki Date: Tue, 1 Jul 2025 17:11:30 +0200 Subject: [PATCH 07/25] Fix height --- modules/yup_graphics/drawables/yup_Drawable.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/yup_graphics/drawables/yup_Drawable.cpp b/modules/yup_graphics/drawables/yup_Drawable.cpp index 710ebef17..aa0f1bab2 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.cpp +++ b/modules/yup_graphics/drawables/yup_Drawable.cpp @@ -55,7 +55,7 @@ bool Drawable::parseSVG (const File& svgFile) size.setWidth (width == 0.0 ? viewBox.getWidth() : width); auto height = svgRoot->getDoubleAttribute ("height"); - size.setWidth (height == 0.0 ? viewBox.getHeight() : height); + size.setHeight (height == 0.0 ? viewBox.getHeight() : height); //AffineTransform currentTransform; //if (! viewBox.getTopLeft().isOrigin()) From 2e9177b190c48eae2d85d45a586cb69cb64612e9 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 2 Jul 2025 08:48:11 +0200 Subject: [PATCH 08/25] Improve Path api --- modules/yup_graphics/primitives/yup_Path.cpp | 40 ++++++++++++++++++-- modules/yup_graphics/primitives/yup_Path.h | 20 ++++++++-- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/modules/yup_graphics/primitives/yup_Path.cpp b/modules/yup_graphics/primitives/yup_Path.cpp index 7050ac33c..558acd3a1 100644 --- a/modules/yup_graphics/primitives/yup_Path.cpp +++ b/modules/yup_graphics/primitives/yup_Path.cpp @@ -568,7 +568,24 @@ Path& Path::addCenteredArc (const Point& center, const Size& diame //============================================================================== -Path& Path::addPolygon (Point centre, int numberOfSides, float radius, float startAngle) +void Path::addTriangle (float x1, float y1, float x2, float y2, float x3, float y3) +{ + addTriangle ({ x1, y1 }, { x2, y2 }, { x3, y3 }); +} + +void Path::addTriangle (const Point& p1, const Point& p2, const Point& p3) +{ + reserveSpace (size() + 4); + + moveTo (p1); + lineTo (p2); + lineTo (p3); + close(); +} + +//============================================================================== + +Path& Path::addPolygon (const Point& centre, int numberOfSides, float radius, float startAngle) { if (numberOfSides < 3) return *this; @@ -601,7 +618,7 @@ Path& Path::addPolygon (Point centre, int numberOfSides, float radius, fl //============================================================================== -Path& Path::addStar (Point centre, int numberOfPoints, float innerRadius, float outerRadius, float startAngle) +Path& Path::addStar (const Point& centre, int numberOfPoints, float innerRadius, float outerRadius, float startAngle) { if (numberOfPoints < 3) return *this; @@ -636,7 +653,7 @@ Path& Path::addStar (Point centre, int numberOfPoints, float innerRadius, //============================================================================== -Path& Path::addBubble (Rectangle bodyArea, Rectangle maximumArea, Point arrowTipPosition, float cornerSize, float arrowBaseWidth) +Path& Path::addBubble (const Rectangle& bodyArea, const Rectangle& maximumArea, const Point& arrowTipPosition, float cornerSize, float arrowBaseWidth) { if (bodyArea.isEmpty() || maximumArea.isEmpty() || arrowBaseWidth <= 0.0f) return *this; @@ -823,6 +840,23 @@ Path& Path::addBubble (Rectangle bodyArea, Rectangle maximumArea, //============================================================================== +void Path::startNewSubPath (float x, float y) +{ + moveTo (x, y); +} + +void Path::startNewSubPath (const Point& p) +{ + moveTo (p.getX(), p.getY()); +} + +void Path::closeSubPath() +{ + close(); +} + +//============================================================================== + Path& Path::appendPath (const Path& other) { path->addRenderPath (other.getRenderPath(), rive::Mat2D()); diff --git a/modules/yup_graphics/primitives/yup_Path.h b/modules/yup_graphics/primitives/yup_Path.h index 46ea9c4ca..971454b77 100644 --- a/modules/yup_graphics/primitives/yup_Path.h +++ b/modules/yup_graphics/primitives/yup_Path.h @@ -180,6 +180,8 @@ class YUP_API Path */ int size() const; + bool isEmpty() const { return size() == 0; } + //============================================================================== /** Clears all the segments from the path. @@ -520,6 +522,11 @@ class YUP_API Path */ Path& addCenteredArc (const Point& center, const Size& diameter, float rotationOfEllipse, float fromRadians, float toRadians, bool startAsNewSubPath); + //============================================================================== + void addTriangle (float x1, float y1, float x2, float y2, float x3, float y3); + + void addTriangle (const Point& p1, const Point& p2, const Point& p3); + //============================================================================== /** Adds a regular polygon to the path. @@ -531,7 +538,7 @@ class YUP_API Path @param radius The radius from the center to each vertex. @param startAngle The starting angle in radians (0.0f starts at the right). */ - Path& addPolygon (Point centre, int numberOfSides, float radius, float startAngle = 0.0f); + Path& addPolygon (const Point& centre, int numberOfSides, float radius, float startAngle = 0.0f); //============================================================================== /** Adds a star shape to the path. @@ -545,7 +552,7 @@ class YUP_API Path @param outerRadius The radius from the center to the outer vertices. @param startAngle The starting angle in radians (0.0f starts at the right). */ - Path& addStar (Point centre, int numberOfPoints, float innerRadius, float outerRadius, float startAngle = 0.0f); + Path& addStar (const Point& centre, int numberOfPoints, float innerRadius, float outerRadius, float startAngle = 0.0f); //============================================================================== /** Adds a speech bubble shape to the path. @@ -559,7 +566,7 @@ class YUP_API Path @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); + Path& addBubble (const Rectangle& bodyArea, const Rectangle& maximumArea, const Point& arrowTipPosition, float cornerSize, float arrowBaseWidth); //============================================================================== /** Converts the path to a stroke polygon with specified width. @@ -585,6 +592,13 @@ class YUP_API Path */ Path withRoundedCorners (float cornerRadius) const; + //============================================================================== + + void startNewSubPath (float x, float y); + void startNewSubPath (const Point& p); + + void closeSubPath(); + //============================================================================== /** Appends another path to this one. From 36a1401dc2c646f78c6bca2abf22e4d1a6707089 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 2 Jul 2025 08:48:29 +0200 Subject: [PATCH 09/25] Improved ColorGradient --- .../yup_graphics/graphics/yup_ColorGradient.h | 195 +++++++++++++----- .../yup_graphics/graphics/yup_Graphics.cpp | 31 ++- 2 files changed, 168 insertions(+), 58 deletions(-) diff --git a/modules/yup_graphics/graphics/yup_ColorGradient.h b/modules/yup_graphics/graphics/yup_ColorGradient.h index 25bf0fef1..24edea6ab 100644 --- a/modules/yup_graphics/graphics/yup_ColorGradient.h +++ b/modules/yup_graphics/graphics/yup_ColorGradient.h @@ -23,10 +23,11 @@ namespace yup { //============================================================================== -/** Represents a gradient for graphical use, defined by two colors and their positions. +/** Represents a gradient for graphical use, defined by multiple colors and their positions. - This class encapsulates a gradient, which can be either linear or radial, specified by two color stops. + This class encapsulates a gradient, which can be either linear or radial, specified by color stops. Each stop has a color and a position, and in the case of radial gradients, a radius is calculated. + Supports multiple color stops for complex gradients while maintaining backward compatibility. */ class YUP_API ColorGradient { @@ -39,6 +40,31 @@ class YUP_API ColorGradient Radial ///< A radial gradient transitions smoothly between colors in a circular pattern. }; + //============================================================================== + /** Represents a single color stop in a gradient. */ + struct ColorStop + { + constexpr ColorStop() = default; + + constexpr ColorStop (Color color, float x, float y, float delta) + : color (color) + , x (x) + , y (y) + , delta (delta) + { + } + + constexpr ColorStop (const ColorStop& other) noexcept = default; + constexpr ColorStop (ColorStop&& other) noexcept = default; + constexpr ColorStop& operator= (const ColorStop& other) noexcept = default; + constexpr ColorStop& operator= (ColorStop&& other) noexcept = default; + + Color color; + float x = 0.0f; + float y = 0.0f; + float delta = 0.0f; + }; + //============================================================================== /** Default constructor, initializes an empty gradient. @@ -58,13 +84,31 @@ class YUP_API ColorGradient */ ColorGradient (Color color1, float x1, float y1, Color color2, float x2, float y2, Type type) noexcept : type (type) - , start (color1, x1, y1, 0.0f) - , finish (color2, x2, y2, 1.0f) { + stops.emplace_back (color1, x1, y1, 0.0f); + stops.emplace_back (color2, x2, y2, 1.0f); + if (type == Radial) radius = std::sqrt (square (x2 - x1) + square (y2 - y1)); } + /** Constructs a gradient with multiple color stops. + + @param type The type of gradient (Linear or Radial). + @param colorStops Vector of ColorStop objects defining the gradient. + */ + ColorGradient (Type type, const std::vector& colorStops) noexcept + : type (type) + , stops (colorStops) + { + if (type == Radial && stops.size() >= 2) + { + const auto& first = stops.front(); + const auto& last = stops.back(); + radius = std::sqrt (square (last.x - first.x) + square (last.y - first.y)); + } + } + //============================================================================== /** Copy and move constructors and assignment operators. */ ColorGradient (const ColorGradient& other) = default; @@ -83,13 +127,13 @@ class YUP_API ColorGradient } //============================================================================== - /** Gets the starting color of the gradient. + /** Gets the starting color of the gradient (first color stop). @return The starting color. */ Color getStartColor() const { - return start.color; + return stops.empty() ? Color() : stops.front().color; } /** Gets the x-coordinate of the starting color position. @@ -98,7 +142,7 @@ class YUP_API ColorGradient */ float getStartX() const { - return start.x; + return stops.empty() ? 0.0f : stops.front().x; } /** Gets the y-coordinate of the starting color position. @@ -107,7 +151,7 @@ class YUP_API ColorGradient */ float getStartY() const { - return start.y; + return stops.empty() ? 0.0f : stops.front().y; } /** Gets the relative position of the starting color in the gradient. @@ -116,17 +160,17 @@ class YUP_API ColorGradient */ float getStartDelta() const { - return start.delta; + return stops.empty() ? 0.0f : stops.front().delta; } //============================================================================== - /** Gets the ending color of the gradient. + /** Gets the ending color of the gradient (last color stop). @return The ending color. */ Color getFinishColor() const { - return finish.color; + return stops.empty() ? Color() : stops.back().color; } /** Gets the x-coordinate of the ending color position. @@ -135,7 +179,7 @@ class YUP_API ColorGradient */ float getFinishX() const { - return finish.x; + return stops.empty() ? 0.0f : stops.back().x; } /** Gets the y-coordinate of the ending color position. @@ -144,7 +188,7 @@ class YUP_API ColorGradient */ float getFinishY() const { - return finish.y; + return stops.empty() ? 0.0f : stops.back().y; } /** Gets the relative position of the ending color in the gradient. @@ -153,7 +197,58 @@ class YUP_API ColorGradient */ float getFinishDelta() const { - return finish.delta; + return stops.empty() ? 1.0f : stops.back().delta; + } + + //============================================================================== + /** Gets the number of color stops in the gradient. + + @return The number of color stops. + */ + size_t getNumStops() const + { + return stops.size(); + } + + /** Gets a color stop by index. + + @param index The index of the color stop to retrieve. + @return The color stop at the specified index. + */ + const ColorStop& getStop (size_t index) const + { + jassert (index < stops.size()); + return stops[index]; + } + + /** Gets all color stops. + + @return A const reference to the vector of color stops. + */ + const std::vector& getStops() const + { + return stops; + } + + /** Adds a color stop to the gradient. + + @param color The color of the new stop. + @param x The x-coordinate of the stop. + @param y The y-coordinate of the stop. + @param delta The relative position of the stop (0.0-1.0). + */ + void addColorStop (Color color, float x, float y, float delta) + { + stops.emplace_back (color, x, y, delta); + std::sort (stops.begin(), stops.end(), [](const ColorStop& a, const ColorStop& b) { + return a.delta < b.delta; + }); + } + + /** Clears all color stops. */ + void clearStops() + { + stops.clear(); } //============================================================================== @@ -169,24 +264,27 @@ class YUP_API ColorGradient } //============================================================================== - /** Sets the alpha value for both color stops in the gradient. + /** Sets the alpha value for all color stops in the gradient. @param alpha The alpha value to set, affecting the transparency of the entire gradient. */ void setAlpha (uint8 alpha) { - start.color.setAlpha (alpha); - finish.color.setAlpha (alpha); + for (auto& stop : stops) + stop.color.setAlpha (alpha); } - // TODO - doxygen + /** Sets the alpha value for all color stops in the gradient. + + @param alpha The alpha value to set, affecting the transparency of the entire gradient. + */ void setAlpha (float alpha) { - start.color.setAlpha (alpha); - finish.color.setAlpha (alpha); + for (auto& stop : stops) + stop.color.setAlpha (alpha); } - /** Creates a new gradient with a specified alpha value for both color stops. + /** Creates a new gradient with a specified alpha value for all color stops. This method allows you to create a new gradient identical to the current one but with a different transparency level. @@ -201,7 +299,12 @@ class YUP_API ColorGradient return result; } - // TODO - doxygen + /** Creates a new gradient with a specified alpha value for all color stops. + + @param alpha The alpha value for the new gradient. + + @return A new ColorGradient object with the specified alpha value. + */ ColorGradient withAlpha (float alpha) const { ColorGradient result (*this); @@ -209,51 +312,37 @@ class YUP_API ColorGradient return result; } - // TODO - doxygen + /** Creates a new gradient with multiplied alpha values for all color stops. + + @param alpha The alpha multiplier for the new gradient. + + @return A new ColorGradient object with multiplied alpha values. + */ ColorGradient withMultipliedAlpha (uint8 alpha) const { ColorGradient result (*this); - result.start.color = result.start.color.withMultipliedAlpha (alpha); - result.finish.color = result.finish.color.withMultipliedAlpha (alpha); + for (auto& stop : result.stops) + stop.color = stop.color.withMultipliedAlpha (alpha); return result; } - // TODO - doxygen + /** Creates a new gradient with multiplied alpha values for all color stops. + + @param alpha The alpha multiplier for the new gradient. + + @return A new ColorGradient object with multiplied alpha values. + */ ColorGradient withMultipliedAlpha (float alpha) const { ColorGradient result (*this); - result.start.color = result.start.color.withMultipliedAlpha (alpha); - result.finish.color = result.finish.color.withMultipliedAlpha (alpha); + for (auto& stop : result.stops) + stop.color = stop.color.withMultipliedAlpha (alpha); return result; } private: - struct ColorStop - { - constexpr ColorStop() = default; - - constexpr ColorStop (Color color, float x, float y, float delta) - : color (color) - , x (x) - , y (y) - , delta (delta) - { - } - - constexpr ColorStop (const ColorStop& other) noexcept = default; - constexpr ColorStop (ColorStop&& other) noexcept = default; - constexpr ColorStop& operator= (const ColorStop& other) noexcept = default; - constexpr ColorStop& operator= (ColorStop&& other) noexcept = default; - - Color color; - float x = 0.0f; - float y = 0.0f; - float delta = 0.0f; - }; - Type type = Type::Linear; - ColorStop start; - ColorStop finish; + std::vector stops; float radius = 0.0f; }; diff --git a/modules/yup_graphics/graphics/yup_Graphics.cpp b/modules/yup_graphics/graphics/yup_Graphics.cpp index 5ccff9e26..e175d2231 100644 --- a/modules/yup_graphics/graphics/yup_Graphics.cpp +++ b/modules/yup_graphics/graphics/yup_Graphics.cpp @@ -122,10 +122,31 @@ void convertRawPathToRenderPath (const rive::RawPath& input, rive::RenderPath* o rive::rcp toColorGradient (rive::Factory& factory, const ColorGradient& gradient, const AffineTransform& transform) { - const uint32 colors[] = { gradient.getStartColor(), gradient.getFinishColor() }; + const auto& colorStops = gradient.getStops(); + + if (colorStops.empty()) + return nullptr; + + // Handle single color stop as solid color + if (colorStops.size() == 1) + { + uint32 color = colorStops[0].color; + float stops[] = { 0.0f }; + return factory.makeLinearGradient (0.0f, 0.0f, 1.0f, 0.0f, &color, stops, 1); + } - float stops[] = { gradient.getStartDelta(), gradient.getFinishDelta() }; - transform.transformPoints (stops[0], stops[1]); + // Create dynamic arrays for colors and stops + std::vector colors; + std::vector stops; + + colors.reserve (colorStops.size()); + stops.reserve (colorStops.size()); + + for (const auto& stop : colorStops) + { + colors.push_back (stop.color); + stops.push_back (stop.delta); + } if (gradient.getType() == ColorGradient::Linear) { @@ -135,7 +156,7 @@ rive::rcp toColorGradient (rive::Factory& factory, const Col float y2 = gradient.getFinishY(); transform.transformPoints (x1, y1, x2, y2); - return factory.makeLinearGradient (x1, y1, x2, y2, colors, stops, sizeof (colors)); + return factory.makeLinearGradient (x1, y1, x2, y2, colors.data(), stops.data(), colors.size()); } else { @@ -145,7 +166,7 @@ rive::rcp toColorGradient (rive::Factory& factory, const Col [[maybe_unused]] float radiusY = gradient.getRadius(); transform.transformPoints (x1, y1, radiusX, radiusY); - return factory.makeRadialGradient (x1, y1, radiusX, colors, stops, sizeof (colors)); + return factory.makeRadialGradient (x1, y1, radiusX, colors.data(), stops.data(), colors.size()); } } From fefaa6ebdb7589876a6b94559022782e2786f7f7 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 2 Jul 2025 08:49:33 +0200 Subject: [PATCH 10/25] Improved drawable --- .../yup_graphics/drawables/yup_Drawable.cpp | 515 +++++++++++++++++- modules/yup_graphics/drawables/yup_Drawable.h | 60 ++ 2 files changed, 573 insertions(+), 2 deletions(-) diff --git a/modules/yup_graphics/drawables/yup_Drawable.cpp b/modules/yup_graphics/drawables/yup_Drawable.cpp index aa0f1bab2..6b01b9e6e 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.cpp +++ b/modules/yup_graphics/drawables/yup_Drawable.cpp @@ -73,6 +73,10 @@ void Drawable::clear() elements.clear(); elementsById.clear(); + gradients.clear(); + gradientsById.clear(); + clipPaths.clear(); + clipPathsById.clear(); } //============================================================================== @@ -101,6 +105,28 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent if (element.opacity) g.setOpacity (g.getOpacity() * (*element.opacity)); + + // Apply clipping path if specified + bool hasClipping = false; + if (element.clipPathUrl) + { + if (auto* clipPath = getClipPathById (*element.clipPathUrl)) + { + // Create a combined path from all clip path elements + Path combinedClipPath; + for (const auto& clipElement : clipPath->elements) + { + if (clipElement->path) + combinedClipPath.appendPath (*clipElement->path); + } + + if (!combinedClipPath.isEmpty()) + { + g.setClipPath (combinedClipPath); + hasClipping = true; + } + } + } // Setup fill if (element.fillColor) @@ -108,6 +134,15 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent g.setFillColor (*element.fillColor); isFillDefined = true; } + else if (element.fillUrl) + { + if (auto* gradient = getGradientById (*element.fillUrl)) + { + ColorGradient colorGradient = createColorGradientFromSVG (*gradient); + g.setFillColorGradient (colorGradient); + isFillDefined = true; + } + } if (isFillDefined && ! element.noFill) { @@ -120,6 +155,48 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent if (auto refElement = elementsById[*element.reference]; refElement != nullptr && refElement->path) g.fillPath (*refElement->path); } + else if (element.text && element.textPosition) + { + /* + // Create StyledText for text rendering + StyledText styledText; + styledText.setText (*element.text); + + // Set font properties + if (element.fontSize) + styledText.setFontSize (*element.fontSize); + else + styledText.setFontSize (12.0f); + + if (element.fontFamily) + styledText.setFontFamily (*element.fontFamily); + + // Set text alignment based on text-anchor + if (element.textAnchor) + { + if (*element.textAnchor == "middle") + styledText.setHorizontalAlignment (StyledText::HorizontalAlignment::Center); + else if (*element.textAnchor == "end") + styledText.setHorizontalAlignment (StyledText::HorizontalAlignment::Right); + else + styledText.setHorizontalAlignment (StyledText::HorizontalAlignment::Left); + } + + // Render text at specified position + auto textBounds = styledText.getBounds(); + g.drawStyledText (styledText, element.textPosition->getX(), element.textPosition->getY() - textBounds.getHeight()); + */ + } + else if (element.imageHref && element.imageBounds) + { + // TODO: Load and render image + // For now, draw a placeholder rectangle + g.setFillColor (Colors::lightgray); + g.fillRect (*element.imageBounds); + g.setStrokeColor (Colors::darkgray); + g.setStrokeWidth (1.0f); + g.strokeRect (*element.imageBounds); + } } // Setup stroke @@ -128,6 +205,15 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent g.setStrokeColor (*element.strokeColor); isStrokeDefined = true; } + else if (element.strokeUrl) + { + if (auto* gradient = getGradientById (*element.strokeUrl)) + { + ColorGradient colorGradient = createColorGradientFromSVG (*gradient); + g.setStrokeColorGradient (colorGradient); + isStrokeDefined = true; + } + } if (element.strokeJoin) g.setStrokeJoin (*element.strokeJoin); @@ -224,6 +310,143 @@ bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, Affin currentTransform = parseTransform (element, currentTransform, *e); parseStyle (element, currentTransform, *e); } + else if (element.hasTagName ("rect")) + { + auto x = element.getDoubleAttribute ("x"); + auto y = element.getDoubleAttribute ("y"); + auto width = element.getDoubleAttribute ("width"); + auto height = element.getDoubleAttribute ("height"); + auto rx = element.getDoubleAttribute ("rx"); + auto ry = element.getDoubleAttribute ("ry"); + + auto path = Path(); + if (rx > 0.0 || ry > 0.0) + { + if (rx == 0.0) rx = ry; + if (ry == 0.0) ry = rx; + path.addRoundedRectangle (x, y, width, height, rx, ry, rx, ry); + } + else + { + path.addRectangle (x, y, width, height); + } + e->path = std::move (path); + + currentTransform = parseTransform (element, currentTransform, *e); + parseStyle (element, currentTransform, *e); + } + else if (element.hasTagName ("line")) + { + auto x1 = element.getDoubleAttribute ("x1"); + auto y1 = element.getDoubleAttribute ("y1"); + auto x2 = element.getDoubleAttribute ("x2"); + auto y2 = element.getDoubleAttribute ("y2"); + + auto path = Path(); + path.startNewSubPath (x1, y1); + path.lineTo (x2, y2); + e->path = std::move (path); + + currentTransform = parseTransform (element, currentTransform, *e); + parseStyle (element, currentTransform, *e); + } + else if (element.hasTagName ("polygon")) + { + String points = element.getStringAttribute ("points"); + if (points.isNotEmpty()) + { + auto path = Path(); + auto coords = StringArray::fromTokens (points, " ,", ""); + + if (coords.size() >= 4 && coords.size() % 2 == 0) + { + path.startNewSubPath (coords[0].getFloatValue(), coords[1].getFloatValue()); + + for (int i = 2; i < coords.size(); i += 2) + path.lineTo (coords[i].getFloatValue(), coords[i + 1].getFloatValue()); + + path.closeSubPath(); + } + e->path = std::move (path); + } + + currentTransform = parseTransform (element, currentTransform, *e); + parseStyle (element, currentTransform, *e); + } + else if (element.hasTagName ("polyline")) + { + String points = element.getStringAttribute ("points"); + if (points.isNotEmpty()) + { + auto path = Path(); + auto coords = StringArray::fromTokens (points, " ,", ""); + + if (coords.size() >= 4 && coords.size() % 2 == 0) + { + path.startNewSubPath (coords[0].getFloatValue(), coords[1].getFloatValue()); + + for (int i = 2; i < coords.size(); i += 2) + path.lineTo (coords[i].getFloatValue(), coords[i + 1].getFloatValue()); + } + e->path = std::move (path); + } + + currentTransform = parseTransform (element, currentTransform, *e); + parseStyle (element, currentTransform, *e); + } + else if (element.hasTagName ("text")) + { + auto x = (float) element.getDoubleAttribute ("x"); + auto y = (float) element.getDoubleAttribute ("y"); + e->textPosition = Point (x, y); + + e->text = element.getAllSubText(); + + String fontFamily = element.getStringAttribute ("font-family"); + if (fontFamily.isNotEmpty()) + e->fontFamily = fontFamily; + + auto fontSize = element.getDoubleAttribute ("font-size"); + if (fontSize > 0.0) + e->fontSize = fontSize; + + String textAnchor = element.getStringAttribute ("text-anchor"); + if (textAnchor.isNotEmpty()) + e->textAnchor = textAnchor; + + currentTransform = parseTransform (element, currentTransform, *e); + parseStyle (element, currentTransform, *e); + } + else if (element.hasTagName ("image")) + { + auto x = element.getDoubleAttribute ("x"); + auto y = element.getDoubleAttribute ("y"); + auto width = element.getDoubleAttribute ("width"); + auto height = element.getDoubleAttribute ("height"); + + e->imageBounds = Rectangle (x, y, width, height); + + String href = element.getStringAttribute ("href"); + if (href.isEmpty()) + href = element.getStringAttribute ("xlink:href"); + + if (href.isNotEmpty()) + e->imageHref = href; + + currentTransform = parseTransform (element, currentTransform, *e); + parseStyle (element, currentTransform, *e); + } + else if (element.hasTagName ("defs")) + { + // Parse definitions like gradients and clip paths + for (auto* child = element.getFirstChildElement(); child != nullptr; child = child->getNextElement()) + { + if (child->hasTagName ("linearGradient") || child->hasTagName ("radialGradient")) + parseGradient (*child); + else if (child->hasTagName ("clipPath")) + parseClipPath (*child); + } + } for (auto* child = element.getFirstChildElement(); child != nullptr; child = child->getNextElement()) parseElement (*child, isRootElement, currentTransform, e.get()); @@ -243,22 +466,50 @@ bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, Affin void Drawable::parseStyle (const XmlElement& element, const AffineTransform& currentTransform, Element& e) { + // Parse CSS style attribute first + String styleAttr = element.getStringAttribute ("style"); + if (styleAttr.isNotEmpty()) + parseCSSStyle (styleAttr, e); + + // Parse individual attributes (these override style attribute values) String fill = element.getStringAttribute ("fill"); if (fill.isNotEmpty()) { if (fill != "none") - e.fillColor = Color::fromString (fill); + { + if (fill.startsWith ("url(#")) + { + e.fillUrl = fill.substring (5, fill.length() - 1); + } + else + { + e.fillColor = Color::fromString (fill); + } + } else + { e.noFill = true; + } } String stroke = element.getStringAttribute ("stroke"); if (stroke.isNotEmpty()) { if (stroke != "none") - e.strokeColor = Color::fromString (stroke); + { + if (stroke.startsWith ("url(#")) + { + e.strokeUrl = stroke.substring (5, stroke.length() - 1); + } + else + { + e.strokeColor = Color::fromString (stroke); + } + } else + { e.noStroke = true; + } } String strokeJoin = element.getStringAttribute ("stroke-linejoin"); @@ -287,6 +538,12 @@ void Drawable::parseStyle (const XmlElement& element, const AffineTransform& cur float opacity = element.getDoubleAttribute ("opacity", -1.0); if (opacity >= 0.0 && opacity <= 1.0) e.opacity = opacity; + + String clipPath = element.getStringAttribute ("clip-path"); + if (clipPath.isNotEmpty() && clipPath.startsWith ("url(#")) + { + e.clipPathUrl = clipPath.substring (5, clipPath.length() - 1); + } } //============================================================================== @@ -415,4 +672,258 @@ void Drawable::paintDebugElement (Graphics& g, const Element& element) } } +//============================================================================== + +void Drawable::parseGradient (const XmlElement& element) +{ + Gradient gradient; + + String id = element.getStringAttribute ("id"); + if (id.isEmpty()) + return; + + gradient.id = id; + + if (element.hasTagName ("linearGradient")) + { + gradient.type = Gradient::Linear; + gradient.start = { (float) element.getDoubleAttribute ("x1"), (float) element.getDoubleAttribute ("y1") }; + gradient.end = { (float) element.getDoubleAttribute ("x2"), (float) element.getDoubleAttribute ("y2") }; + } + else if (element.hasTagName ("radialGradient")) + { + gradient.type = Gradient::Radial; + gradient.center = { (float) element.getDoubleAttribute ("cx"), (float) element.getDoubleAttribute ("cy") }; + gradient.radius = element.getDoubleAttribute ("r"); + + auto fx = element.getDoubleAttribute ("fx", gradient.center.getX()); + auto fy = element.getDoubleAttribute ("fy", gradient.center.getY()); + gradient.focal = { (float) fx, (float) fy }; + } + + // Parse gradient stops + for (auto* child = element.getFirstChildElement(); child != nullptr; child = child->getNextElement()) + { + if (child->hasTagName ("stop")) + { + GradientStop stop; + stop.offset = child->getDoubleAttribute ("offset"); + + String stopColor = child->getStringAttribute ("stop-color"); + if (stopColor.isNotEmpty()) + stop.color = Color::fromString (stopColor); + + stop.opacity = child->getDoubleAttribute ("stop-opacity", 1.0); + + gradient.stops.push_back (stop); + } + } + + gradients.push_back (gradient); + gradientsById.set (id, &gradients.back()); +} + +//============================================================================== + +Drawable::Gradient* Drawable::getGradientById (const String& id) +{ + return gradientsById[id]; +} + +//============================================================================== + +ColorGradient Drawable::createColorGradientFromSVG (const Gradient& gradient) +{ + if (gradient.stops.empty()) + return ColorGradient(); + + if (gradient.stops.size() == 1) + { + const auto& stop = gradient.stops[0]; + Color color = stop.color.withAlpha (stop.opacity); + return ColorGradient (color, 0, 0, color, 1, 0, + gradient.type == Gradient::Linear ? ColorGradient::Linear : ColorGradient::Radial); + } + + // Create ColorStop vector for YUP ColorGradient + std::vector colorStops; + for (const auto& stop : gradient.stops) + { + Color color = stop.color.withAlpha (stop.opacity); + + if (gradient.type == Gradient::Linear) + { + // For linear gradients, interpolate position based on offset + float x = gradient.start.getX() + stop.offset * (gradient.end.getX() - gradient.start.getX()); + float y = gradient.start.getY() + stop.offset * (gradient.end.getY() - gradient.start.getY()); + colorStops.emplace_back (color, x, y, stop.offset); + } + else + { + // For radial gradients, use center as base position + colorStops.emplace_back (color, gradient.center.getX(), gradient.center.getY(), stop.offset); + } + } + + ColorGradient::Type type = (gradient.type == Gradient::Linear) ? ColorGradient::Linear : ColorGradient::Radial; + return ColorGradient (type, colorStops); +} + +//============================================================================== + +void Drawable::parseClipPath (const XmlElement& element) +{ + ClipPath clipPath; + + String id = element.getStringAttribute ("id"); + if (id.isEmpty()) + return; + + clipPath.id = id; + + // Parse child elements that make up the clipping path + for (auto* child = element.getFirstChildElement(); child != nullptr; child = child->getNextElement()) + { + auto clipElement = std::make_shared(); + + if (child->hasTagName ("path")) + { + auto path = Path(); + String pathData = child->getStringAttribute ("d"); + if (pathData.isNotEmpty() && path.fromString (pathData)) + clipElement->path = std::move (path); + } + else if (child->hasTagName ("rect")) + { + auto x = child->getDoubleAttribute ("x"); + auto y = child->getDoubleAttribute ("y"); + auto width = child->getDoubleAttribute ("width"); + auto height = child->getDoubleAttribute ("height"); + + auto path = Path(); + path.addRectangle (x, y, width, height); + clipElement->path = std::move (path); + } + else if (child->hasTagName ("circle")) + { + auto cx = child->getDoubleAttribute ("cx"); + auto cy = child->getDoubleAttribute ("cy"); + auto r = child->getDoubleAttribute ("r"); + + auto path = Path(); + path.addCenteredEllipse (cx, cy, r, r); + clipElement->path = std::move (path); + } + + if (clipElement->path) + clipPath.elements.push_back (clipElement); + } + + clipPaths.push_back (clipPath); + clipPathsById.set (id, &clipPaths.back()); +} + +//============================================================================== + +Drawable::ClipPath* Drawable::getClipPathById (const String& id) +{ + return clipPathsById[id]; +} + +//============================================================================== + +void Drawable::parseCSSStyle (const String& styleString, Element& e) +{ + // Parse CSS style declarations separated by semicolons + auto declarations = StringArray::fromTokens (styleString, ";", ""); + + for (const auto& declaration : declarations) + { + auto colonPos = declaration.indexOf (":"); + if (colonPos > 0) + { + String property = declaration.substring (0, colonPos).trim(); + String value = declaration.substring (colonPos + 1).trim(); + + if (property == "fill") + { + if (value != "none") + { + if (value.startsWith ("url(#")) + e.fillUrl = value.substring (5, value.length() - 1); + else + e.fillColor = Color::fromString (value); + } + else + { + e.noFill = true; + } + } + else if (property == "stroke") + { + if (value != "none") + { + if (value.startsWith ("url(#")) + e.strokeUrl = value.substring (5, value.length() - 1); + else + e.strokeColor = Color::fromString (value); + } + else + { + e.noStroke = true; + } + } + else if (property == "stroke-width") + { + float strokeWidth = value.getFloatValue(); + if (strokeWidth > 0.0f) + e.strokeWidth = strokeWidth; + } + else if (property == "stroke-linejoin") + { + if (value == "round") + e.strokeJoin = StrokeJoin::Round; + else if (value == "miter") + e.strokeJoin = StrokeJoin::Miter; + else if (value == "bevel") + e.strokeJoin = StrokeJoin::Bevel; + } + else if (property == "stroke-linecap") + { + if (value == "round") + e.strokeCap = StrokeCap::Round; + else if (value == "square") + e.strokeCap = StrokeCap::Square; + else if (value == "butt") + e.strokeCap = StrokeCap::Butt; + } + else if (property == "opacity") + { + float opacity = value.getFloatValue(); + if (opacity >= 0.0f && opacity <= 1.0f) + e.opacity = opacity; + } + else if (property == "font-family") + { + e.fontFamily = value; + } + else if (property == "font-size") + { + float fontSize = value.getFloatValue(); + if (fontSize > 0.0f) + e.fontSize = fontSize; + } + else if (property == "text-anchor") + { + e.textAnchor = value; + } + else if (property == "clip-path") + { + if (value.startsWith ("url(#")) + e.clipPathUrl = value.substring (5, value.length() - 1); + } + } + } +} + } // namespace yup diff --git a/modules/yup_graphics/drawables/yup_Drawable.h b/modules/yup_graphics/drawables/yup_Drawable.h index 299b5176a..b26931184 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.h +++ b/modules/yup_graphics/drawables/yup_Drawable.h @@ -54,20 +54,80 @@ class YUP_API Drawable std::optional opacity; + // Text properties + std::optional text; + std::optional> textPosition; + std::optional fontFamily; + std::optional fontSize; + std::optional textAnchor; + + // Gradient properties + std::optional fillUrl; + std::optional strokeUrl; + + // Image properties + std::optional imageHref; + std::optional> imageBounds; + + // Clipping properties + std::optional clipPathUrl; + std::vector> children; }; + struct GradientStop + { + float offset; + Color color; + float opacity = 1.0f; + }; + + struct Gradient + { + enum Type { Linear, Radial }; + Type type; + String id; + + // Linear gradient properties + Point start; + Point end; + + // Radial gradient properties + Point center; + float radius = 0.0f; + Point focal; + + std::vector stops; + AffineTransform transform; + }; + + struct ClipPath + { + String id; + std::vector> elements; + }; + void paintElement (Graphics& g, const Element& element, bool hasParentFillEnabled, bool hasParentStrokeEnabled); void paintDebugElement (Graphics& g, const Element& element); bool parseElement (const XmlElement& element, bool parentIsRoot, AffineTransform currentTransform, Element* parent = nullptr); void parseStyle (const XmlElement& element, const AffineTransform& currentTransform, Element& e); AffineTransform parseTransform (const XmlElement& element, const AffineTransform& currentTransform, Element& e); + void parseGradient (const XmlElement& element); + Gradient* getGradientById (const String& id); + ColorGradient createColorGradientFromSVG (const Gradient& gradient); + void parseClipPath (const XmlElement& element); + ClipPath* getClipPathById (const String& id); + void parseCSSStyle (const String& styleString, Element& e); Rectangle viewBox; Size size; AffineTransform transform; std::vector> elements; HashMap> elementsById; + std::vector gradients; + HashMap gradientsById; + std::vector clipPaths; + HashMap clipPathsById; }; } // namespace yup From 5ec1031b44925c38ade54c3623e4374cdeefc004 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 2 Jul 2025 10:25:49 +0200 Subject: [PATCH 11/25] More tweaks --- .../yup_graphics/drawables/yup_Drawable.cpp | 158 +++++++++++++++++- modules/yup_graphics/drawables/yup_Drawable.h | 3 + modules/yup_graphics/primitives/yup_Path.cpp | 59 ++++++- 3 files changed, 205 insertions(+), 15 deletions(-) diff --git a/modules/yup_graphics/drawables/yup_Drawable.cpp b/modules/yup_graphics/drawables/yup_Drawable.cpp index 6b01b9e6e..233ac5d6f 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.cpp +++ b/modules/yup_graphics/drawables/yup_Drawable.cpp @@ -57,9 +57,40 @@ bool Drawable::parseSVG (const File& svgFile) auto height = svgRoot->getDoubleAttribute ("height"); size.setHeight (height == 0.0 ? viewBox.getHeight() : height); - //AffineTransform currentTransform; - //if (! viewBox.getTopLeft().isOrigin()) - // currentTransform = currentTransform.translated (-viewBox.getX(), -viewBox.getY()); + // Calculate the viewBox to viewport transformation + if (!viewBox.isEmpty()) + { + /* + // If no explicit width/height, use viewBox dimensions as size + if (size.getWidth() == viewBox.getWidth() && size.getHeight() == viewBox.getHeight()) + { + // No scaling needed, just handle viewBox offset + if (!viewBox.getTopLeft().isOrigin()) + transform = AffineTransform::translation (-viewBox.getX(), -viewBox.getY()); + } + else if (size.getWidth() > 0 && size.getHeight() > 0) + { + // Calculate scale factors to fit viewBox into specified size + float scaleX = size.getWidth() / viewBox.getWidth(); + float scaleY = size.getHeight() / viewBox.getHeight(); + + // Use uniform scaling to preserve aspect ratio + float scale = jmin (scaleX, scaleY); + + // Calculate translation to center the scaled viewBox + float translateX = (size.getWidth() - viewBox.getWidth() * scale) * 0.5f - viewBox.getX() * scale; + float translateY = (size.getHeight() - viewBox.getHeight() * scale) * 0.5f - viewBox.getY() * scale; + + transform = AffineTransform::scaling (scale).translated (translateX, translateY); + } + else + { + // Just handle viewBox offset + if (!viewBox.getTopLeft().isOrigin()) + transform = AffineTransform::translation (-viewBox.getX(), -viewBox.getY()); + } + */ + } return parseElement (*svgRoot, true, {}); } @@ -223,6 +254,21 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent if (element.strokeWidth) g.setStrokeWidth (*element.strokeWidth); + + // Apply stroke dash patterns + if (element.strokeDashArray) + { + // Convert Array to what Graphics expects + const auto& dashArray = *element.strokeDashArray; + if (!dashArray.isEmpty()) + { + // TODO: Graphics class needs stroke dash pattern support + // For now, this is prepared for when Graphics supports it + // g.setStrokeDashPattern (dashArray.getRawDataPointer(), dashArray.size()); + // if (element.strokeDashOffset) + // g.setStrokeDashOffset (*element.strokeDashOffset); + } + } if (isStrokeDefined && ! element.noStroke) { @@ -531,8 +577,7 @@ void Drawable::parseStyle (const XmlElement& element, const AffineTransform& cur float strokeWidth = element.getDoubleAttribute ("stroke-width", -1.0); if (strokeWidth > 0.0) { - auto transformScale = std::sqrtf (std::fabsf (currentTransform.followedBy (transform).getDeterminant())); - e.strokeWidth = transformScale * strokeWidth; + e.strokeWidth = strokeWidth; } float opacity = element.getDoubleAttribute ("opacity", -1.0); @@ -544,6 +589,32 @@ void Drawable::parseStyle (const XmlElement& element, const AffineTransform& cur { e.clipPathUrl = clipPath.substring (5, clipPath.length() - 1); } + + // Parse stroke-dasharray + String dashArray = element.getStringAttribute ("stroke-dasharray"); + if (dashArray.isNotEmpty() && dashArray != "none") + { + auto dashValues = StringArray::fromTokens (dashArray, " ,", ""); + if (!dashValues.isEmpty()) + { + Array dashes; + for (const auto& dash : dashValues) + { + float value = parseUnit (dash); + if (value >= 0.0f) + dashes.add (value); + } + if (!dashes.isEmpty()) + e.strokeDashArray = dashes; + } + } + + // Parse stroke-dashoffset + String dashOffset = element.getStringAttribute ("stroke-dashoffset"); + if (dashOffset.isNotEmpty()) + { + e.strokeDashOffset = parseUnit (dashOffset); + } } //============================================================================== @@ -632,8 +703,12 @@ AffineTransform Drawable::parseTransform (const XmlElement& element, const Affin } else if (type == "matrix" && params.size() == 6) { + // SVG matrix(a,b,c,d,e,f) maps to different parameter order in YUP AffineTransform + // SVG: matrix(scaleX, shearY, shearX, scaleY, translateX, translateY) + // YUP: AffineTransform(scaleX, shearX, translateX, shearY, scaleY, translateY) + // Conversion: AffineTransform(a, c, e, b, d, f) result = result.followedBy (AffineTransform ( - params[0], params[1], params[2], params[3], params[4], params[5])); + params[0], params[2], params[4], params[1], params[3], params[5])); } } @@ -922,8 +997,79 @@ void Drawable::parseCSSStyle (const String& styleString, Element& e) if (value.startsWith ("url(#")) e.clipPathUrl = value.substring (5, value.length() - 1); } + else if (property == "stroke-dasharray") + { + if (value != "none") + { + auto dashValues = StringArray::fromTokens (value, " ,", ""); + if (!dashValues.isEmpty()) + { + Array dashes; + for (const auto& dash : dashValues) + { + float dashValue = parseUnit (dash); + if (dashValue >= 0.0f) + dashes.add (dashValue); + } + if (!dashes.isEmpty()) + e.strokeDashArray = dashes; + } + } + } + else if (property == "stroke-dashoffset") + { + e.strokeDashOffset = parseUnit (value); + } } } } +//============================================================================== + +float Drawable::parseUnit (const String& value, float defaultValue, float fontSize, float viewportSize) +{ + if (value.isEmpty()) + return defaultValue; + + String trimmed = value.trim(); + if (trimmed.isEmpty()) + return defaultValue; + + // Extract numeric part and unit + int unitStart = 0; + while (unitStart < trimmed.length() && + (CharacterFunctions::isDigit (trimmed[unitStart]) || + trimmed[unitStart] == '.' || + trimmed[unitStart] == '-' || + trimmed[unitStart] == '+')) + { + unitStart++; + } + + float numericValue = trimmed.substring (0, unitStart).getFloatValue(); + String unit = trimmed.substring (unitStart).trim().toLowerCase(); + + // Handle different SVG units + if (unit.isEmpty() || unit == "px") + return numericValue; // Default user units or pixels + else if (unit == "pt") + return numericValue * 1.333333f; // 1pt = 1.333px + else if (unit == "pc") + return numericValue * 16.0f; // 1pc = 16px + else if (unit == "mm") + return numericValue * 3.779528f; // 1mm = 3.779528px (96 DPI) + else if (unit == "cm") + return numericValue * 37.79528f; // 1cm = 37.79528px (96 DPI) + else if (unit == "in") + return numericValue * 96.0f; // 1in = 96px (96 DPI) + else if (unit == "em") + return numericValue * fontSize; // Relative to font size + else if (unit == "ex") + return numericValue * fontSize * 0.5f; // Approximately 0.5em + else if (unit == "%") + return numericValue * viewportSize * 0.01f; // Percentage of viewport + else + return numericValue; // Unknown unit, treat as user units +} + } // namespace yup diff --git a/modules/yup_graphics/drawables/yup_Drawable.h b/modules/yup_graphics/drawables/yup_Drawable.h index b26931184..c5f236d77 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.h +++ b/modules/yup_graphics/drawables/yup_Drawable.h @@ -49,6 +49,8 @@ class YUP_API Drawable std::optional strokeWidth; std::optional strokeJoin; std::optional strokeCap; + std::optional> strokeDashArray; + std::optional strokeDashOffset; bool noFill = false; bool noStroke = false; @@ -118,6 +120,7 @@ class YUP_API Drawable void parseClipPath (const XmlElement& element); ClipPath* getClipPathById (const String& id); void parseCSSStyle (const String& styleString, Element& e); + float parseUnit (const String& value, float defaultValue = 0.0f, float fontSize = 12.0f, float viewportSize = 100.0f); Rectangle viewBox; Size size; diff --git a/modules/yup_graphics/primitives/yup_Path.cpp b/modules/yup_graphics/primitives/yup_Path.cpp index 558acd3a1..93e3c7e2c 100644 --- a/modules/yup_graphics/primitives/yup_Path.cpp +++ b/modules/yup_graphics/primitives/yup_Path.cpp @@ -1249,7 +1249,7 @@ void handleVerticalLineTo (String::CharPointerType& data, Path& path, float curr } } -void handleQuadTo (String::CharPointerType& data, Path& path, float& currentX, float& currentY, bool relative) +void handleQuadTo (String::CharPointerType& data, Path& path, float& currentX, float& currentY, float& lastQuadX, float& lastQuadY, bool relative) { float x1, y1, x, y; @@ -1270,6 +1270,10 @@ void handleQuadTo (String::CharPointerType& data, Path& path, float& currentX, f currentX = x; currentY = y; + + // Update the last control point for smooth curves + lastQuadX = x1; + lastQuadY = y1; skipWhitespace (data); } @@ -1318,7 +1322,7 @@ void handleSmoothQuadTo (String::CharPointerType& data, Path& path, float& curre } } -void handleCubicTo (String::CharPointerType& data, Path& path, float& currentX, float& currentY, bool relative) +void handleCubicTo (String::CharPointerType& data, Path& path, float& currentX, float& currentY, float& lastControlX, float& lastControlY, bool relative) { float x1, y1, x2, y2, x, y; @@ -1342,6 +1346,10 @@ void handleCubicTo (String::CharPointerType& data, Path& path, float& currentX, currentX = x; currentY = y; + + // Update the last control point for smooth curves (second control point) + lastControlX = x2; + lastControlY = y2; skipWhitespace (data); } @@ -1526,50 +1534,83 @@ bool Path::fromString (const String& pathData) case 'M': // Move to absolute case 'm': // Move to relative handleMoveTo (data, *this, currentX, currentY, startX, startY, command == 'm'); + // Reset control points after move + lastControlX = currentX; + lastControlY = currentY; + lastQuadX = currentX; + lastQuadY = currentY; break; case 'L': // Line to absolute case 'l': // Line to relative handleLineTo (data, *this, currentX, currentY, command == 'l'); + // Reset control points after line (non-curve command) + lastControlX = currentX; + lastControlY = currentY; + lastQuadX = currentX; + lastQuadY = currentY; break; case 'H': // Horizontal line to absolute case 'h': // Horizontal line to relative handleHorizontalLineTo (data, *this, currentX, currentY, command == 'h'); + // Reset control points after line (non-curve command) + lastControlX = currentX; + lastControlY = currentY; + lastQuadX = currentX; + lastQuadY = currentY; break; case 'V': // Vertical line to absolute case 'v': // Vertical line to relative handleVerticalLineTo (data, *this, currentX, currentY, command == 'v'); + // Reset control points after line (non-curve command) + lastControlX = currentX; + lastControlY = currentY; + lastQuadX = currentX; + lastQuadY = currentY; break; case 'Q': // Quadratic Bezier curve to absolute case 'q': // Quadratic Bezier curve to relative - handleQuadTo (data, *this, currentX, currentY, command == 'q'); - lastQuadX = currentX; - lastQuadY = currentY; + handleQuadTo (data, *this, currentX, currentY, lastQuadX, lastQuadY, command == 'q'); + // Reset cubic control points (quadratic doesn't affect cubic control points) + lastControlX = currentX; + lastControlY = currentY; break; case 'T': // Quadratic Smooth Bezier curve to absolute case 't': // Quadratic Smooth Bezier curve to relative - handleSmoothQuadTo (data, *this, currentX, currentY, lastQuadX, lastQuadY, command == 'q'); + handleSmoothQuadTo (data, *this, currentX, currentY, lastQuadX, lastQuadY, command == 't'); + // Reset cubic control points (quadratic doesn't affect cubic control points) + lastControlX = currentX; + lastControlY = currentY; break; case 'C': // Cubic Bezier curve to absolute case 'c': // Cubic Bezier curve to relative - handleCubicTo (data, *this, currentX, currentY, command == 'c'); - lastControlX = currentX; - lastControlY = currentY; + handleCubicTo (data, *this, currentX, currentY, lastControlX, lastControlY, command == 'c'); + // Reset quadratic control points (cubic doesn't affect quadratic control points) + lastQuadX = currentX; + lastQuadY = currentY; break; case 'S': // Cubic Smooth Bezier curve to absolute case 's': // Cubic Smooth Bezier curve to relative handleSmoothCubicTo (data, *this, currentX, currentY, lastControlX, lastControlY, command == 's'); + // Reset quadratic control points (cubic doesn't affect quadratic control points) + lastQuadX = currentX; + lastQuadY = currentY; break; case 'A': // Elliptical Arc to absolute case 'a': // Elliptical Arc to relative handleEllipticalArc (data, *this, currentX, currentY, command == 'a'); + // Reset control points after arc (non-curve command) + lastControlX = currentX; + lastControlY = currentY; + lastQuadX = currentX; + lastQuadY = currentY; break; case 'Z': // Close path From 5aef778a00acf927a5dad3792b5e205001d48ce2 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 2 Jul 2025 10:52:02 +0200 Subject: [PATCH 12/25] Working tiger --- .../yup_graphics/drawables/yup_Drawable.cpp | 51 +++++++++++-------- modules/yup_graphics/primitives/yup_Path.cpp | 11 ++++ modules/yup_graphics/primitives/yup_Path.h | 2 + 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/modules/yup_graphics/drawables/yup_Drawable.cpp b/modules/yup_graphics/drawables/yup_Drawable.cpp index 233ac5d6f..0c8dffc56 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.cpp +++ b/modules/yup_graphics/drawables/yup_Drawable.cpp @@ -174,17 +174,26 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent isFillDefined = true; } } + else if (hasParentFillEnabled) + { + // Inherit parent fill - don't change graphics state, just mark as defined + isFillDefined = true; + } if (isFillDefined && ! element.noFill) { if (element.path) { - g.fillPath (*element.path); + if (element.path->isClosed()) + g.fillPath (*element.path); } else if (element.reference) { if (auto refElement = elementsById[*element.reference]; refElement != nullptr && refElement->path) - g.fillPath (*refElement->path); + { + if (refElement.path->isClosed()) + g.fillPath (*refElement->path); + } } else if (element.text && element.textPosition) { @@ -245,6 +254,11 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent isStrokeDefined = true; } } + else if (hasParentStrokeEnabled) + { + // Inherit parent stroke - don't change graphics state, just mark as defined + isStrokeDefined = true; + } if (element.strokeJoin) g.setStrokeJoin (*element.strokeJoin); @@ -524,13 +538,9 @@ void Drawable::parseStyle (const XmlElement& element, const AffineTransform& cur if (fill != "none") { if (fill.startsWith ("url(#")) - { e.fillUrl = fill.substring (5, fill.length() - 1); - } else - { e.fillColor = Color::fromString (fill); - } } else { @@ -544,13 +554,9 @@ void Drawable::parseStyle (const XmlElement& element, const AffineTransform& cur if (stroke != "none") { if (stroke.startsWith ("url(#")) - { e.strokeUrl = stroke.substring (5, stroke.length() - 1); - } else - { e.strokeColor = Color::fromString (stroke); - } } else { @@ -576,9 +582,7 @@ void Drawable::parseStyle (const XmlElement& element, const AffineTransform& cur float strokeWidth = element.getDoubleAttribute ("stroke-width", -1.0); if (strokeWidth > 0.0) - { e.strokeWidth = strokeWidth; - } float opacity = element.getDoubleAttribute ("opacity", -1.0); if (opacity >= 0.0 && opacity <= 1.0) @@ -586,10 +590,8 @@ void Drawable::parseStyle (const XmlElement& element, const AffineTransform& cur String clipPath = element.getStringAttribute ("clip-path"); if (clipPath.isNotEmpty() && clipPath.startsWith ("url(#")) - { e.clipPathUrl = clipPath.substring (5, clipPath.length() - 1); - } - + // Parse stroke-dasharray String dashArray = element.getStringAttribute ("stroke-dasharray"); if (dashArray.isNotEmpty() && dashArray != "none") @@ -604,6 +606,7 @@ void Drawable::parseStyle (const XmlElement& element, const AffineTransform& cur if (value >= 0.0f) dashes.add (value); } + if (!dashes.isEmpty()) e.strokeDashArray = dashes; } @@ -612,9 +615,7 @@ void Drawable::parseStyle (const XmlElement& element, const AffineTransform& cur // Parse stroke-dashoffset String dashOffset = element.getStringAttribute ("stroke-dashoffset"); if (dashOffset.isNotEmpty()) - { e.strokeDashOffset = parseUnit (dashOffset); - } } //============================================================================== @@ -703,10 +704,6 @@ AffineTransform Drawable::parseTransform (const XmlElement& element, const Affin } else if (type == "matrix" && params.size() == 6) { - // SVG matrix(a,b,c,d,e,f) maps to different parameter order in YUP AffineTransform - // SVG: matrix(scaleX, shearY, shearX, scaleY, translateX, translateY) - // YUP: AffineTransform(scaleX, shearX, translateX, shearY, scaleY, translateY) - // Conversion: AffineTransform(a, c, e, b, d, f) result = result.followedBy (AffineTransform ( params[0], params[2], params[4], params[1], params[3], params[5])); } @@ -817,7 +814,7 @@ ColorGradient Drawable::createColorGradientFromSVG (const Gradient& gradient) const auto& stop = gradient.stops[0]; Color color = stop.color.withAlpha (stop.opacity); return ColorGradient (color, 0, 0, color, 1, 0, - gradient.type == Gradient::Linear ? ColorGradient::Linear : ColorGradient::Radial); + gradient.type == Gradient::Linear ? ColorGradient::Linear : ColorGradient::Radial); } // Create ColorStop vector for YUP ColorGradient @@ -1011,6 +1008,7 @@ void Drawable::parseCSSStyle (const String& styleString, Element& e) if (dashValue >= 0.0f) dashes.add (dashValue); } + if (!dashes.isEmpty()) e.strokeDashArray = dashes; } @@ -1052,22 +1050,31 @@ float Drawable::parseUnit (const String& value, float defaultValue, float fontSi // Handle different SVG units if (unit.isEmpty() || unit == "px") return numericValue; // Default user units or pixels + else if (unit == "pt") return numericValue * 1.333333f; // 1pt = 1.333px + else if (unit == "pc") return numericValue * 16.0f; // 1pc = 16px + else if (unit == "mm") return numericValue * 3.779528f; // 1mm = 3.779528px (96 DPI) + else if (unit == "cm") return numericValue * 37.79528f; // 1cm = 37.79528px (96 DPI) + else if (unit == "in") return numericValue * 96.0f; // 1in = 96px (96 DPI) + else if (unit == "em") return numericValue * fontSize; // Relative to font size + else if (unit == "ex") return numericValue * fontSize * 0.5f; // Approximately 0.5em + else if (unit == "%") return numericValue * viewportSize * 0.01f; // Percentage of viewport + else return numericValue; // Unknown unit, treat as user units } diff --git a/modules/yup_graphics/primitives/yup_Path.cpp b/modules/yup_graphics/primitives/yup_Path.cpp index 93e3c7e2c..5a2d0794b 100644 --- a/modules/yup_graphics/primitives/yup_Path.cpp +++ b/modules/yup_graphics/primitives/yup_Path.cpp @@ -855,6 +855,17 @@ void Path::closeSubPath() close(); } +bool Path::isClosed() const +{ + for (const auto& segment : *this) + { + if (segment.verb == PathVerb::Close) + return true; + } + + return false; +} + //============================================================================== Path& Path::appendPath (const Path& other) diff --git a/modules/yup_graphics/primitives/yup_Path.h b/modules/yup_graphics/primitives/yup_Path.h index 971454b77..0fc08d6c5 100644 --- a/modules/yup_graphics/primitives/yup_Path.h +++ b/modules/yup_graphics/primitives/yup_Path.h @@ -599,6 +599,8 @@ class YUP_API Path void closeSubPath(); + bool isClosed() const; + //============================================================================== /** Appends another path to this one. From 4b94037d344312c610c8f9bfa7806ddf3c671cd4 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 2 Jul 2025 11:22:45 +0200 Subject: [PATCH 13/25] More tweaks --- modules/yup_graphics/drawables/yup_Drawable.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/yup_graphics/drawables/yup_Drawable.cpp b/modules/yup_graphics/drawables/yup_Drawable.cpp index 0c8dffc56..ad93af1b3 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.cpp +++ b/modules/yup_graphics/drawables/yup_Drawable.cpp @@ -191,7 +191,7 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent { if (auto refElement = elementsById[*element.reference]; refElement != nullptr && refElement->path) { - if (refElement.path->isClosed()) + if (refElement->path->isClosed()) g.fillPath (*refElement->path); } } From 934e24b3bcec151c64fb82ca79c741fc0b9f04ec Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 2 Jul 2025 11:54:23 +0200 Subject: [PATCH 14/25] Fix issue --- modules/yup_graphics/primitives/yup_Path.cpp | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/modules/yup_graphics/primitives/yup_Path.cpp b/modules/yup_graphics/primitives/yup_Path.cpp index 5a2d0794b..ab385ecad 100644 --- a/modules/yup_graphics/primitives/yup_Path.cpp +++ b/modules/yup_graphics/primitives/yup_Path.cpp @@ -1179,6 +1179,7 @@ bool parseCoordinates (String::CharPointerType& data, float& x, float& y) void handleMoveTo (String::CharPointerType& data, Path& path, float& currentX, float& currentY, float& startX, float& startY, bool relative) { float x, y; + bool isFirstCoordinate = true; while (! data.isEmpty() && ! isControlMarker (data) @@ -1190,10 +1191,20 @@ void handleMoveTo (String::CharPointerType& data, Path& path, float& currentX, f y += currentY; } - path.moveTo (x, y); - - currentX = startX = x; - currentY = startY = y; + if (isFirstCoordinate) + { + path.moveTo (x, y); + currentX = startX = x; + currentY = startY = y; + isFirstCoordinate = false; + } + else + { + // Subsequent coordinates are implicit line commands according to SVG spec + path.lineTo (x, y); + currentX = x; + currentY = y; + } skipWhitespace (data); } From bc9509a9b8d9d73613b113a343701d3f6444b005 Mon Sep 17 00:00:00 2001 From: Yup Bot Date: Wed, 2 Jul 2025 09:55:02 +0000 Subject: [PATCH 15/25] Code formatting --- .../yup_graphics/drawables/yup_Drawable.cpp | 137 +++++++++--------- modules/yup_graphics/drawables/yup_Drawable.h | 15 +- .../yup_graphics/graphics/yup_ColorGradient.h | 5 +- .../yup_graphics/graphics/yup_Graphics.cpp | 8 +- modules/yup_graphics/primitives/yup_Path.cpp | 4 +- 5 files changed, 86 insertions(+), 83 deletions(-) diff --git a/modules/yup_graphics/drawables/yup_Drawable.cpp b/modules/yup_graphics/drawables/yup_Drawable.cpp index ad93af1b3..9dfa77e5e 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.cpp +++ b/modules/yup_graphics/drawables/yup_Drawable.cpp @@ -58,7 +58,7 @@ bool Drawable::parseSVG (const File& svgFile) size.setHeight (height == 0.0 ? viewBox.getHeight() : height); // Calculate the viewBox to viewport transformation - if (!viewBox.isEmpty()) + if (! viewBox.isEmpty()) { /* // If no explicit width/height, use viewBox dimensions as size @@ -136,7 +136,7 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent if (element.opacity) g.setOpacity (g.getOpacity() * (*element.opacity)); - + // Apply clipping path if specified bool hasClipping = false; if (element.clipPathUrl) @@ -150,8 +150,8 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent if (clipElement->path) combinedClipPath.appendPath (*clipElement->path); } - - if (!combinedClipPath.isEmpty()) + + if (! combinedClipPath.isEmpty()) { g.setClipPath (combinedClipPath); hasClipping = true; @@ -268,13 +268,13 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent if (element.strokeWidth) g.setStrokeWidth (*element.strokeWidth); - + // Apply stroke dash patterns if (element.strokeDashArray) { // Convert Array to what Graphics expects const auto& dashArray = *element.strokeDashArray; - if (!dashArray.isEmpty()) + if (! dashArray.isEmpty()) { // TODO: Graphics class needs stroke dash pattern support // For now, this is prepared for when Graphics supports it @@ -382,8 +382,10 @@ bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, Affin auto path = Path(); if (rx > 0.0 || ry > 0.0) { - if (rx == 0.0) rx = ry; - if (ry == 0.0) ry = rx; + if (rx == 0.0) + rx = ry; + if (ry == 0.0) + ry = rx; path.addRoundedRectangle (x, y, width, height, rx, ry, rx, ry); } else @@ -417,14 +419,14 @@ bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, Affin { auto path = Path(); auto coords = StringArray::fromTokens (points, " ,", ""); - + if (coords.size() >= 4 && coords.size() % 2 == 0) { path.startNewSubPath (coords[0].getFloatValue(), coords[1].getFloatValue()); - + for (int i = 2; i < coords.size(); i += 2) path.lineTo (coords[i].getFloatValue(), coords[i + 1].getFloatValue()); - + path.closeSubPath(); } e->path = std::move (path); @@ -440,11 +442,11 @@ bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, Affin { auto path = Path(); auto coords = StringArray::fromTokens (points, " ,", ""); - + if (coords.size() >= 4 && coords.size() % 2 == 0) { path.startNewSubPath (coords[0].getFloatValue(), coords[1].getFloatValue()); - + for (int i = 2; i < coords.size(); i += 2) path.lineTo (coords[i].getFloatValue(), coords[i + 1].getFloatValue()); } @@ -461,15 +463,15 @@ bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, Affin e->textPosition = Point (x, y); e->text = element.getAllSubText(); - + String fontFamily = element.getStringAttribute ("font-family"); if (fontFamily.isNotEmpty()) e->fontFamily = fontFamily; - + auto fontSize = element.getDoubleAttribute ("font-size"); if (fontSize > 0.0) e->fontSize = fontSize; - + String textAnchor = element.getStringAttribute ("text-anchor"); if (textAnchor.isNotEmpty()) e->textAnchor = textAnchor; @@ -483,13 +485,13 @@ bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, Affin auto y = element.getDoubleAttribute ("y"); auto width = element.getDoubleAttribute ("width"); auto height = element.getDoubleAttribute ("height"); - + e->imageBounds = Rectangle (x, y, width, height); - + String href = element.getStringAttribute ("href"); if (href.isEmpty()) href = element.getStringAttribute ("xlink:href"); - + if (href.isNotEmpty()) e->imageHref = href; @@ -530,7 +532,7 @@ void Drawable::parseStyle (const XmlElement& element, const AffineTransform& cur String styleAttr = element.getStringAttribute ("style"); if (styleAttr.isNotEmpty()) parseCSSStyle (styleAttr, e); - + // Parse individual attributes (these override style attribute values) String fill = element.getStringAttribute ("fill"); if (fill.isNotEmpty()) @@ -587,7 +589,7 @@ void Drawable::parseStyle (const XmlElement& element, const AffineTransform& cur float opacity = element.getDoubleAttribute ("opacity", -1.0); if (opacity >= 0.0 && opacity <= 1.0) e.opacity = opacity; - + String clipPath = element.getStringAttribute ("clip-path"); if (clipPath.isNotEmpty() && clipPath.startsWith ("url(#")) e.clipPathUrl = clipPath.substring (5, clipPath.length() - 1); @@ -597,7 +599,7 @@ void Drawable::parseStyle (const XmlElement& element, const AffineTransform& cur if (dashArray.isNotEmpty() && dashArray != "none") { auto dashValues = StringArray::fromTokens (dashArray, " ,", ""); - if (!dashValues.isEmpty()) + if (! dashValues.isEmpty()) { Array dashes; for (const auto& dash : dashValues) @@ -607,11 +609,11 @@ void Drawable::parseStyle (const XmlElement& element, const AffineTransform& cur dashes.add (value); } - if (!dashes.isEmpty()) + if (! dashes.isEmpty()) e.strokeDashArray = dashes; } } - + // Parse stroke-dashoffset String dashOffset = element.getStringAttribute ("stroke-dashoffset"); if (dashOffset.isNotEmpty()) @@ -749,13 +751,13 @@ void Drawable::paintDebugElement (Graphics& g, const Element& element) void Drawable::parseGradient (const XmlElement& element) { Gradient gradient; - + String id = element.getStringAttribute ("id"); if (id.isEmpty()) return; - + gradient.id = id; - + if (element.hasTagName ("linearGradient")) { gradient.type = Gradient::Linear; @@ -767,12 +769,12 @@ void Drawable::parseGradient (const XmlElement& element) gradient.type = Gradient::Radial; gradient.center = { (float) element.getDoubleAttribute ("cx"), (float) element.getDoubleAttribute ("cy") }; gradient.radius = element.getDoubleAttribute ("r"); - + auto fx = element.getDoubleAttribute ("fx", gradient.center.getX()); auto fy = element.getDoubleAttribute ("fy", gradient.center.getY()); gradient.focal = { (float) fx, (float) fy }; } - + // Parse gradient stops for (auto* child = element.getFirstChildElement(); child != nullptr; child = child->getNextElement()) { @@ -780,17 +782,17 @@ void Drawable::parseGradient (const XmlElement& element) { GradientStop stop; stop.offset = child->getDoubleAttribute ("offset"); - + String stopColor = child->getStringAttribute ("stop-color"); if (stopColor.isNotEmpty()) stop.color = Color::fromString (stopColor); - + stop.opacity = child->getDoubleAttribute ("stop-opacity", 1.0); - + gradient.stops.push_back (stop); } } - + gradients.push_back (gradient); gradientsById.set (id, &gradients.back()); } @@ -808,21 +810,20 @@ ColorGradient Drawable::createColorGradientFromSVG (const Gradient& gradient) { if (gradient.stops.empty()) return ColorGradient(); - + if (gradient.stops.size() == 1) { const auto& stop = gradient.stops[0]; Color color = stop.color.withAlpha (stop.opacity); - return ColorGradient (color, 0, 0, color, 1, 0, - gradient.type == Gradient::Linear ? ColorGradient::Linear : ColorGradient::Radial); + return ColorGradient (color, 0, 0, color, 1, 0, gradient.type == Gradient::Linear ? ColorGradient::Linear : ColorGradient::Radial); } - + // Create ColorStop vector for YUP ColorGradient std::vector colorStops; for (const auto& stop : gradient.stops) { Color color = stop.color.withAlpha (stop.opacity); - + if (gradient.type == Gradient::Linear) { // For linear gradients, interpolate position based on offset @@ -836,7 +837,7 @@ ColorGradient Drawable::createColorGradientFromSVG (const Gradient& gradient) colorStops.emplace_back (color, gradient.center.getX(), gradient.center.getY(), stop.offset); } } - + ColorGradient::Type type = (gradient.type == Gradient::Linear) ? ColorGradient::Linear : ColorGradient::Radial; return ColorGradient (type, colorStops); } @@ -846,18 +847,18 @@ ColorGradient Drawable::createColorGradientFromSVG (const Gradient& gradient) void Drawable::parseClipPath (const XmlElement& element) { ClipPath clipPath; - + String id = element.getStringAttribute ("id"); if (id.isEmpty()) return; - + clipPath.id = id; - + // Parse child elements that make up the clipping path for (auto* child = element.getFirstChildElement(); child != nullptr; child = child->getNextElement()) { auto clipElement = std::make_shared(); - + if (child->hasTagName ("path")) { auto path = Path(); @@ -871,7 +872,7 @@ void Drawable::parseClipPath (const XmlElement& element) auto y = child->getDoubleAttribute ("y"); auto width = child->getDoubleAttribute ("width"); auto height = child->getDoubleAttribute ("height"); - + auto path = Path(); path.addRectangle (x, y, width, height); clipElement->path = std::move (path); @@ -881,16 +882,16 @@ void Drawable::parseClipPath (const XmlElement& element) auto cx = child->getDoubleAttribute ("cx"); auto cy = child->getDoubleAttribute ("cy"); auto r = child->getDoubleAttribute ("r"); - + auto path = Path(); path.addCenteredEllipse (cx, cy, r, r); clipElement->path = std::move (path); } - + if (clipElement->path) clipPath.elements.push_back (clipElement); } - + clipPaths.push_back (clipPath); clipPathsById.set (id, &clipPaths.back()); } @@ -908,7 +909,7 @@ void Drawable::parseCSSStyle (const String& styleString, Element& e) { // Parse CSS style declarations separated by semicolons auto declarations = StringArray::fromTokens (styleString, ";", ""); - + for (const auto& declaration : declarations) { auto colonPos = declaration.indexOf (":"); @@ -916,7 +917,7 @@ void Drawable::parseCSSStyle (const String& styleString, Element& e) { String property = declaration.substring (0, colonPos).trim(); String value = declaration.substring (colonPos + 1).trim(); - + if (property == "fill") { if (value != "none") @@ -999,7 +1000,7 @@ void Drawable::parseCSSStyle (const String& styleString, Element& e) if (value != "none") { auto dashValues = StringArray::fromTokens (value, " ,", ""); - if (!dashValues.isEmpty()) + if (! dashValues.isEmpty()) { Array dashes; for (const auto& dash : dashValues) @@ -1009,7 +1010,7 @@ void Drawable::parseCSSStyle (const String& styleString, Element& e) dashes.add (dashValue); } - if (!dashes.isEmpty()) + if (! dashes.isEmpty()) e.strokeDashArray = dashes; } } @@ -1028,55 +1029,51 @@ float Drawable::parseUnit (const String& value, float defaultValue, float fontSi { if (value.isEmpty()) return defaultValue; - + String trimmed = value.trim(); if (trimmed.isEmpty()) return defaultValue; - + // Extract numeric part and unit int unitStart = 0; - while (unitStart < trimmed.length() && - (CharacterFunctions::isDigit (trimmed[unitStart]) || - trimmed[unitStart] == '.' || - trimmed[unitStart] == '-' || - trimmed[unitStart] == '+')) + while (unitStart < trimmed.length() && (CharacterFunctions::isDigit (trimmed[unitStart]) || trimmed[unitStart] == '.' || trimmed[unitStart] == '-' || trimmed[unitStart] == '+')) { unitStart++; } - + float numericValue = trimmed.substring (0, unitStart).getFloatValue(); String unit = trimmed.substring (unitStart).trim().toLowerCase(); - + // Handle different SVG units if (unit.isEmpty() || unit == "px") - return numericValue; // Default user units or pixels + return numericValue; // Default user units or pixels else if (unit == "pt") - return numericValue * 1.333333f; // 1pt = 1.333px + return numericValue * 1.333333f; // 1pt = 1.333px else if (unit == "pc") - return numericValue * 16.0f; // 1pc = 16px + return numericValue * 16.0f; // 1pc = 16px else if (unit == "mm") - return numericValue * 3.779528f; // 1mm = 3.779528px (96 DPI) + return numericValue * 3.779528f; // 1mm = 3.779528px (96 DPI) else if (unit == "cm") - return numericValue * 37.79528f; // 1cm = 37.79528px (96 DPI) + return numericValue * 37.79528f; // 1cm = 37.79528px (96 DPI) else if (unit == "in") - return numericValue * 96.0f; // 1in = 96px (96 DPI) + return numericValue * 96.0f; // 1in = 96px (96 DPI) else if (unit == "em") - return numericValue * fontSize; // Relative to font size + return numericValue * fontSize; // Relative to font size else if (unit == "ex") - return numericValue * fontSize * 0.5f; // Approximately 0.5em + return numericValue * fontSize * 0.5f; // Approximately 0.5em else if (unit == "%") - return numericValue * viewportSize * 0.01f; // Percentage of viewport + return numericValue * viewportSize * 0.01f; // Percentage of viewport else - return numericValue; // Unknown unit, treat as user units + return numericValue; // Unknown unit, treat as user units } } // namespace yup diff --git a/modules/yup_graphics/drawables/yup_Drawable.h b/modules/yup_graphics/drawables/yup_Drawable.h index c5f236d77..37830b3fc 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.h +++ b/modules/yup_graphics/drawables/yup_Drawable.h @@ -86,19 +86,24 @@ class YUP_API Drawable struct Gradient { - enum Type { Linear, Radial }; + enum Type + { + Linear, + Radial + }; + Type type; String id; - + // Linear gradient properties Point start; Point end; - - // Radial gradient properties + + // Radial gradient properties Point center; float radius = 0.0f; Point focal; - + std::vector stops; AffineTransform transform; }; diff --git a/modules/yup_graphics/graphics/yup_ColorGradient.h b/modules/yup_graphics/graphics/yup_ColorGradient.h index 24edea6ab..0556669a3 100644 --- a/modules/yup_graphics/graphics/yup_ColorGradient.h +++ b/modules/yup_graphics/graphics/yup_ColorGradient.h @@ -87,7 +87,7 @@ class YUP_API ColorGradient { stops.emplace_back (color1, x1, y1, 0.0f); stops.emplace_back (color2, x2, y2, 1.0f); - + if (type == Radial) radius = std::sqrt (square (x2 - x1) + square (y2 - y1)); } @@ -240,7 +240,8 @@ class YUP_API ColorGradient void addColorStop (Color color, float x, float y, float delta) { stops.emplace_back (color, x, y, delta); - std::sort (stops.begin(), stops.end(), [](const ColorStop& a, const ColorStop& b) { + std::sort (stops.begin(), stops.end(), [] (const ColorStop& a, const ColorStop& b) + { return a.delta < b.delta; }); } diff --git a/modules/yup_graphics/graphics/yup_Graphics.cpp b/modules/yup_graphics/graphics/yup_Graphics.cpp index e175d2231..d3ea6b743 100644 --- a/modules/yup_graphics/graphics/yup_Graphics.cpp +++ b/modules/yup_graphics/graphics/yup_Graphics.cpp @@ -123,10 +123,10 @@ void convertRawPathToRenderPath (const rive::RawPath& input, rive::RenderPath* o rive::rcp toColorGradient (rive::Factory& factory, const ColorGradient& gradient, const AffineTransform& transform) { const auto& colorStops = gradient.getStops(); - + if (colorStops.empty()) return nullptr; - + // Handle single color stop as solid color if (colorStops.size() == 1) { @@ -138,10 +138,10 @@ rive::rcp toColorGradient (rive::Factory& factory, const Col // Create dynamic arrays for colors and stops std::vector colors; std::vector stops; - + colors.reserve (colorStops.size()); stops.reserve (colorStops.size()); - + for (const auto& stop : colorStops) { colors.push_back (stop.color); diff --git a/modules/yup_graphics/primitives/yup_Path.cpp b/modules/yup_graphics/primitives/yup_Path.cpp index ab385ecad..101776141 100644 --- a/modules/yup_graphics/primitives/yup_Path.cpp +++ b/modules/yup_graphics/primitives/yup_Path.cpp @@ -1292,7 +1292,7 @@ void handleQuadTo (String::CharPointerType& data, Path& path, float& currentX, f currentX = x; currentY = y; - + // Update the last control point for smooth curves lastQuadX = x1; lastQuadY = y1; @@ -1368,7 +1368,7 @@ void handleCubicTo (String::CharPointerType& data, Path& path, float& currentX, currentX = x; currentY = y; - + // Update the last control point for smooth curves (second control point) lastControlX = x2; lastControlY = y2; From 5fc6307be79be436dcfc3a0b01b37016c26b67df Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 2 Jul 2025 13:13:16 +0200 Subject: [PATCH 16/25] More work --- examples/graphics/source/examples/Svg.h | 2 +- .../yup_graphics/drawables/yup_Drawable.cpp | 218 ++++++++++++++++-- modules/yup_graphics/drawables/yup_Drawable.h | 43 ++++ modules/yup_graphics/layout/yup_Fitting.h | 45 ++++ modules/yup_graphics/yup_graphics.h | 1 + 5 files changed, 294 insertions(+), 15 deletions(-) create mode 100644 modules/yup_graphics/layout/yup_Fitting.h diff --git a/examples/graphics/source/examples/Svg.h b/examples/graphics/source/examples/Svg.h index 6cf9767d8..2ab0b0f4a 100644 --- a/examples/graphics/source/examples/Svg.h +++ b/examples/graphics/source/examples/Svg.h @@ -51,7 +51,7 @@ class SvgDemo : public yup::Component g.setFillColor (findColor (yup::DocumentWindow::Style::backgroundColorId).value_or (yup::Colors::dimgray)); g.fillAll(); - drawable.paint (g); + drawable.paint (g, getLocalBounds()); } private: diff --git a/modules/yup_graphics/drawables/yup_Drawable.cpp b/modules/yup_graphics/drawables/yup_Drawable.cpp index 9dfa77e5e..d9a4a93ce 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.cpp +++ b/modules/yup_graphics/drawables/yup_Drawable.cpp @@ -26,13 +26,14 @@ namespace yup Drawable::Drawable() { - // transform = AffineTransform::scaling (1.0f).translated (100.0f, 100.0f); } //============================================================================== bool Drawable::parseSVG (const File& svgFile) { + clear(); + XmlDocument svgDoc (svgFile); std::unique_ptr svgRoot (svgDoc.getDocumentElement()); @@ -92,7 +93,12 @@ bool Drawable::parseSVG (const File& svgFile) */ } - return parseElement (*svgRoot, true, {}); + auto result = parseElement (*svgRoot, true, {}); + + if (result) + bounds = calculateBounds(); + + return result; } //============================================================================== @@ -101,6 +107,7 @@ void Drawable::clear() { viewBox = { 0.0f, 0.0f, 0.0f, 0.0f }; size = { 0.0f, 0.0f }; + bounds = { 0.0f, 0.0f, 0.0f, 0.0f }; elements.clear(); elementsById.clear(); @@ -112,9 +119,46 @@ void Drawable::clear() //============================================================================== +void Drawable::setTransform (const AffineTransform& newTransform) +{ + transform = newTransform; +} + +AffineTransform Drawable::getTransform() const +{ + return transform; +} + +//============================================================================== + +Rectangle Drawable::getBounds() const +{ + return bounds; +} + +//============================================================================== + void Drawable::paint (Graphics& g) { g.setTransform (transform); + + g.setStrokeWidth (1.0f); + g.setFillColor (Colors::black); + + for (const auto& element : elements) + paintElement (g, *element, true, false); +} + +void Drawable::paint (Graphics& g, const Rectangle& targetArea, Fitting fitting, Justification justification) +{ + if (bounds.isEmpty()) + return; + + const auto savedState = g.saveState(); + + auto finalTransform = calculateTransformForTarget (bounds, targetArea, fitting, justification); + g.setTransform (finalTransform); + g.setStrokeWidth (1.0f); g.setFillColor (Colors::black); @@ -131,8 +175,8 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent bool isFillDefined = hasParentFillEnabled; bool isStrokeDefined = hasParentStrokeEnabled; - if (element.transform) - g.setTransform (element.transform->followedBy (g.getTransform())); + // Element transforms are now applied to path geometry during parsing + // so all elements exist in the same coordinate space if (element.opacity) g.setOpacity (g.getOpacity() * (*element.opacity)); @@ -324,9 +368,14 @@ bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, Affin if (pathData.isEmpty() || ! path.fromString (pathData)) return false; - e->path = std::move (path); - currentTransform = parseTransform (element, currentTransform, *e); + + // Apply accumulated transform (including parent group transforms) to path geometry + if (!currentTransform.isIdentity()) + path.transform (currentTransform); + + e->path = std::move (path); + parseStyle (element, currentTransform, *e); } else if (element.hasTagName ("g")) @@ -352,9 +401,14 @@ bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, Affin auto path = Path(); path.addCenteredEllipse (cx, cy, rx, ry); - e->path = std::move (path); - currentTransform = parseTransform (element, currentTransform, *e); + + // Apply accumulated transform (including parent group transforms) to path geometry + if (!currentTransform.isIdentity()) + path.transform (currentTransform); + + e->path = std::move (path); + parseStyle (element, currentTransform, *e); } else if (element.hasTagName ("circle")) @@ -365,9 +419,14 @@ bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, Affin auto path = Path(); path.addCenteredEllipse (cx, cy, r, r); - e->path = std::move (path); - currentTransform = parseTransform (element, currentTransform, *e); + + // Apply accumulated transform (including parent group transforms) to path geometry + if (!currentTransform.isIdentity()) + path.transform (currentTransform); + + e->path = std::move (path); + parseStyle (element, currentTransform, *e); } else if (element.hasTagName ("rect")) @@ -392,9 +451,14 @@ bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, Affin { path.addRectangle (x, y, width, height); } - e->path = std::move (path); - currentTransform = parseTransform (element, currentTransform, *e); + + // Apply accumulated transform (including parent group transforms) to path geometry + if (!currentTransform.isIdentity()) + path.transform (currentTransform); + + e->path = std::move (path); + parseStyle (element, currentTransform, *e); } else if (element.hasTagName ("line")) @@ -407,9 +471,14 @@ bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, Affin auto path = Path(); path.startNewSubPath (x1, y1); path.lineTo (x2, y2); - e->path = std::move (path); - currentTransform = parseTransform (element, currentTransform, *e); + + // Apply accumulated transform (including parent group transforms) to path geometry + if (!currentTransform.isIdentity()) + path.transform (currentTransform); + + e->path = std::move (path); + parseStyle (element, currentTransform, *e); } else if (element.hasTagName ("polygon")) @@ -1076,4 +1145,125 @@ float Drawable::parseUnit (const String& value, float defaultValue, float fontSi return numericValue; // Unknown unit, treat as user units } +//============================================================================== + +Rectangle Drawable::calculateBounds() const +{ + // Use viewBox if available, otherwise use size + if (!viewBox.isEmpty()) + return viewBox; + + if (size.getWidth() > 0 && size.getHeight() > 0) + return Rectangle (0, 0, size.getWidth(), size.getHeight()); + + // Fallback: calculate bounds from all elements with their transforms applied + // This gives us the actual visual bounds of the rendered content + Rectangle bounds; + bool hasValidBounds = false; + + for (const auto& element : elements) + { + if (element->path) + { + auto pathBounds = element->path->getBounds(); + if (element->transform) + pathBounds = element->path->getBoundsTransformed (*element->transform); + + if (hasValidBounds) + bounds = bounds.unionWith (pathBounds); + else + { + bounds = pathBounds; + hasValidBounds = true; + } + } + } + + return hasValidBounds ? bounds : Rectangle (0, 0, 100, 100); +} + +//============================================================================== + +AffineTransform Drawable::calculateTransformForTarget (const Rectangle& sourceBounds, const Rectangle& targetArea, Fitting fitting, Justification justification) const +{ + if (sourceBounds.isEmpty() || targetArea.isEmpty()) + return AffineTransform::identity(); + + float scaleX = targetArea.getWidth() / sourceBounds.getWidth(); + float scaleY = targetArea.getHeight() / sourceBounds.getHeight(); + + // Apply scaling based on fitting mode + switch (fitting) + { + case Fitting::none: + scaleX = scaleY = 1.0f; + break; + + case Fitting::scaleToFit: + scaleX = scaleY = jmin (scaleX, scaleY); // Scale to fit both dimensions + break; + + case Fitting::fitWidth: + scaleY = scaleX; // Scale to fit width, preserve aspect ratio + break; + + case Fitting::fitHeight: + scaleX = scaleY; // Scale to fit height, preserve aspect ratio + break; + + case Fitting::scaleToFill: + case Fitting::centerCrop: + scaleX = scaleY = jmax (scaleX, scaleY); // Scale to fill, may crop + break; + + case Fitting::fill: + // Use calculated scales as-is (non-uniform scaling) + break; + + case Fitting::centerInside: + // Like scaleToFit but don't upscale beyond original size + scaleX = scaleY = jmin (1.0f, jmin (scaleX, scaleY)); + break; + + case Fitting::stretchWidth: + scaleY = 1.0f; // Stretch horizontally only + break; + + case Fitting::stretchHeight: + scaleX = 1.0f; // Stretch vertically only + break; + + case Fitting::tile: + // For tile mode, use no scaling (tiling would be handled elsewhere) + scaleX = scaleY = 1.0f; + break; + } + + // Calculate scaled size + float scaledWidth = sourceBounds.getWidth() * scaleX; + float scaledHeight = sourceBounds.getHeight() * scaleY; + + // Calculate offset based on justification + float offsetX = targetArea.getX(); + float offsetY = targetArea.getY(); + + // Horizontal justification + if ((static_cast(justification) & static_cast(Justification::horizontalCenter)) != 0) + offsetX += (targetArea.getWidth() - scaledWidth) * 0.5f; + else if ((static_cast(justification) & static_cast(Justification::right)) != 0) + offsetX += targetArea.getWidth() - scaledWidth; + + // Vertical justification + if ((static_cast(justification) & static_cast(Justification::verticalCenter)) != 0) + offsetY += (targetArea.getHeight() - scaledHeight) * 0.5f; + else if ((static_cast(justification) & static_cast(Justification::bottom)) != 0) + offsetY += targetArea.getHeight() - scaledHeight; + + // Create transform: translate to origin, scale, then translate to target position + return AffineTransform::translation (-sourceBounds.getX(), -sourceBounds.getY()) + .scaled (scaleX, scaleY) + .translated (offsetX, offsetY) + .followedBy (transform); // Apply the drawable's own transform +} + } // namespace yup diff --git a/modules/yup_graphics/drawables/yup_Drawable.h b/modules/yup_graphics/drawables/yup_Drawable.h index 37830b3fc..c86639c45 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.h +++ b/modules/yup_graphics/drawables/yup_Drawable.h @@ -27,14 +27,52 @@ namespace yup class YUP_API Drawable { public: + //============================================================================== Drawable(); + //============================================================================== bool parseSVG (const File& svgFile); + //============================================================================== void clear(); + //============================================================================== + /** Sets a transform to be applied when painting the drawable. + + This transform is applied on top of any transforms defined within the SVG itself. + + @param newTransform The transform to apply when painting. + */ + void setTransform (const AffineTransform& newTransform); + + /** Gets the current transform that will be applied when painting. + + @return The current transform. + */ + AffineTransform getTransform() const; + + //============================================================================== + /** Gets the bounds of the drawable content. + + @return The bounding rectangle of the drawable's content. + */ + Rectangle getBounds() const; + + //============================================================================== void paint (Graphics& g); + /** Paints the drawable with the specified fitting and justification. + + @param g The graphics context to paint to. + @param targetArea The rectangle to fit the drawable within. + @param fitting How to scale and fit the drawable to the target area. + @param justification How to position the drawable within the target area. + */ + void paint (Graphics& g, + const Rectangle& targetArea, + Fitting fitting = Fitting::scaleToFit, + Justification justification = Justification::center); + private: struct Element { @@ -126,9 +164,14 @@ class YUP_API Drawable ClipPath* getClipPathById (const String& id); void parseCSSStyle (const String& styleString, Element& e); float parseUnit (const String& value, float defaultValue = 0.0f, float fontSize = 12.0f, float viewportSize = 100.0f); + + // Helper methods for layout and painting + Rectangle calculateBounds() const; + AffineTransform calculateTransformForTarget (const Rectangle& sourceBounds, const Rectangle& targetArea, Fitting fitting, Justification justification) const; Rectangle viewBox; Size size; + Rectangle bounds; AffineTransform transform; std::vector> elements; HashMap> elementsById; diff --git a/modules/yup_graphics/layout/yup_Fitting.h b/modules/yup_graphics/layout/yup_Fitting.h new file mode 100644 index 000000000..5b15d9c5c --- /dev/null +++ b/modules/yup_graphics/layout/yup_Fitting.h @@ -0,0 +1,45 @@ +/* + ============================================================================== + + 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. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** Specifies how to scale and fit an item to a target area. + + This enum is used to specify how to scale and fit an item to a target area. +*/ +enum class Fitting +{ + none, /**< Do not scale or fit at all. */ + scaleToFit, /**< Scale proportionally to fit within bounds (preserve aspect ratio, no cropping). */ + fitWidth, /**< Scale to match width, preserve aspect ratio. */ + fitHeight, /**< Scale to match height, preserve aspect ratio. */ + scaleToFill, /**< Scale proportionally to completely fill bounds (may crop, preserves aspect ratio). */ + fill, /**< Stretch to fill bounds completely (aspect ratio not preserved). */ + tile, /**< Repeat content to fill bounds (used in backgrounds, patterns). */ + centerCrop, /**< Like scaleToFill, but ensures the center remains visible (common in media). */ + centerInside, /**< Like scaleToFit, but does not upscale beyond original size. */ + stretchWidth, /**< Stretch horizontally, preserve vertical size. */ + stretchHeight /**< Stretch vertically, preserve horizontal size. */ +}; + +} // namespace yup diff --git a/modules/yup_graphics/yup_graphics.h b/modules/yup_graphics/yup_graphics.h index c552f7a9e..704184c9c 100644 --- a/modules/yup_graphics/yup_graphics.h +++ b/modules/yup_graphics/yup_graphics.h @@ -66,6 +66,7 @@ YUP_END_IGNORE_WARNINGS_GCC_LIKE //============================================================================== #include "layout/yup_Justification.h" +#include "layout/yup_Fitting.h" #include "primitives/yup_AffineTransform.h" #include "primitives/yup_Size.h" #include "primitives/yup_Point.h" From a7b6cf01d8e6468c10f3bb1157398fa08107cc97 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 2 Jul 2025 17:43:03 +0200 Subject: [PATCH 17/25] More work on svg --- examples/graphics/source/examples/Svg.h | 2 + .../graphics/source/examples/VariableFonts.h | 2 +- .../yup_graphics/drawables/yup_Drawable.cpp | 209 ++++++++++-------- modules/yup_graphics/drawables/yup_Drawable.h | 19 +- .../yup_graphics/graphics/yup_Graphics.cpp | 5 + modules/yup_graphics/graphics/yup_Graphics.h | 7 + .../yup_graphics/layout/yup_Justification.h | 15 ++ .../primitives/yup_AffineTransform.h | 9 + modules/yup_graphics/primitives/yup_Line.h | 9 + modules/yup_graphics/primitives/yup_Path.cpp | 24 +- modules/yup_graphics/primitives/yup_Point.h | 9 + .../yup_graphics/primitives/yup_Rectangle.h | 9 + modules/yup_graphics/primitives/yup_Size.h | 9 + tests/yup_graphics/yup_Graphics.cpp | 20 +- 14 files changed, 233 insertions(+), 115 deletions(-) diff --git a/examples/graphics/source/examples/Svg.h b/examples/graphics/source/examples/Svg.h index 2ab0b0f4a..08c76f66d 100644 --- a/examples/graphics/source/examples/Svg.h +++ b/examples/graphics/source/examples/Svg.h @@ -83,6 +83,8 @@ class SvgDemo : public yup::Component currentSvgFileIndex = index; + YUP_DBG ("Showing " << svgFiles[currentSvgFileIndex].getFullPathName()); + drawable.clear(); drawable.parseSVG (svgFiles[currentSvgFileIndex]); diff --git a/examples/graphics/source/examples/VariableFonts.h b/examples/graphics/source/examples/VariableFonts.h index 38150412f..02ab41957 100644 --- a/examples/graphics/source/examples/VariableFonts.h +++ b/examples/graphics/source/examples/VariableFonts.h @@ -128,7 +128,7 @@ class VariableFontsExample : public yup::Component g.setFillColor (findColor (yup::DocumentWindow::Style::backgroundColorId).value_or (yup::Colors::dimgray)); g.fillAll(); - g.setTransform (yup::AffineTransform::rotation ( + g.addTransform (yup::AffineTransform::rotation ( yup::degreesToRadians (-rotation), getLocalBounds().getCenterX(), 100.0f)); if (feather > 0.0f) diff --git a/modules/yup_graphics/drawables/yup_Drawable.cpp b/modules/yup_graphics/drawables/yup_Drawable.cpp index d9a4a93ce..7cf9f858f 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.cpp +++ b/modules/yup_graphics/drawables/yup_Drawable.cpp @@ -58,40 +58,8 @@ bool Drawable::parseSVG (const File& svgFile) auto height = svgRoot->getDoubleAttribute ("height"); size.setHeight (height == 0.0 ? viewBox.getHeight() : height); - // Calculate the viewBox to viewport transformation - if (! viewBox.isEmpty()) - { - /* - // If no explicit width/height, use viewBox dimensions as size - if (size.getWidth() == viewBox.getWidth() && size.getHeight() == viewBox.getHeight()) - { - // No scaling needed, just handle viewBox offset - if (!viewBox.getTopLeft().isOrigin()) - transform = AffineTransform::translation (-viewBox.getX(), -viewBox.getY()); - } - else if (size.getWidth() > 0 && size.getHeight() > 0) - { - // Calculate scale factors to fit viewBox into specified size - float scaleX = size.getWidth() / viewBox.getWidth(); - float scaleY = size.getHeight() / viewBox.getHeight(); - - // Use uniform scaling to preserve aspect ratio - float scale = jmin (scaleX, scaleY); - - // Calculate translation to center the scaled viewBox - float translateX = (size.getWidth() - viewBox.getWidth() * scale) * 0.5f - viewBox.getX() * scale; - float translateY = (size.getHeight() - viewBox.getHeight() * scale) * 0.5f - viewBox.getY() * scale; - - transform = AffineTransform::scaling (scale).translated (translateX, translateY); - } - else - { - // Just handle viewBox offset - if (!viewBox.getTopLeft().isOrigin()) - transform = AffineTransform::translation (-viewBox.getX(), -viewBox.getY()); - } - */ - } + // ViewBox transform is now calculated at render-time based on actual target area + YUP_DBG("Parse complete - viewBox: " << viewBox.toString() << " size: " << size.getWidth() << "x" << size.getHeight()); auto result = parseElement (*svgRoot, true, {}); @@ -108,6 +76,7 @@ void Drawable::clear() viewBox = { 0.0f, 0.0f, 0.0f, 0.0f }; size = { 0.0f, 0.0f }; bounds = { 0.0f, 0.0f, 0.0f, 0.0f }; + transform = AffineTransform::identity(); elements.clear(); elementsById.clear(); @@ -119,18 +88,6 @@ void Drawable::clear() //============================================================================== -void Drawable::setTransform (const AffineTransform& newTransform) -{ - transform = newTransform; -} - -AffineTransform Drawable::getTransform() const -{ - return transform; -} - -//============================================================================== - Rectangle Drawable::getBounds() const { return bounds; @@ -140,24 +97,31 @@ Rectangle Drawable::getBounds() const void Drawable::paint (Graphics& g) { - g.setTransform (transform); - + const auto savedState = g.saveState(); + g.setStrokeWidth (1.0f); g.setFillColor (Colors::black); + if (! transform.isIdentity()) + g.addTransform (transform); + for (const auto& element : elements) paintElement (g, *element, true, false); } void Drawable::paint (Graphics& g, const Rectangle& targetArea, Fitting fitting, Justification justification) { + YUP_DBG("Fitted paint called - bounds: " << bounds.toString() << " targetArea: " << targetArea.toString()); + if (bounds.isEmpty()) return; const auto savedState = g.saveState(); - auto finalTransform = calculateTransformForTarget (bounds, targetArea, fitting, justification); - g.setTransform (finalTransform); + auto finalBounds = viewBox.isEmpty() ? bounds : viewBox; + auto finalTransform = calculateTransformForTarget (finalBounds, targetArea, fitting, justification); + if (! finalTransform.isIdentity()) + g.addTransform (finalTransform); g.setStrokeWidth (1.0f); g.setFillColor (Colors::black); @@ -174,9 +138,18 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent bool isFillDefined = hasParentFillEnabled; bool isStrokeDefined = hasParentStrokeEnabled; + + YUP_DBG("paintElement called - hasPath: " << (element.path ? "true" : "false") << " hasTransform: " << (element.transform ? "true" : "false")); - // Element transforms are now applied to path geometry during parsing - // so all elements exist in the same coordinate space + // Apply element transform if present - use proper composition for coordinate systems + if (element.transform) + { + YUP_DBG("Applying element transform - before: " << g.getTransform().toString() << " adding: " << element.transform->toString()); + // For proper coordinate system handling, we need to apply element transform + // in the element's local space, then transform to viewport space + g.setTransform (element.transform->followedBy (g.getTransform())); + YUP_DBG("After transform: " << g.getTransform().toString()); + } if (element.opacity) g.setOpacity (g.getOpacity() * (*element.opacity)); @@ -235,6 +208,15 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent { if (auto refElement = elementsById[*element.reference]; refElement != nullptr && refElement->path) { + /* + // For use elements, apply the referenced element's transform and render its path + // This ensures proper coordinate system handling for referenced elements + const auto referencedSavedState = g.saveState(); + + if (refElement->transform) + g.setTransform (*refElement->transform); + */ + if (refElement->path->isClosed()) g.fillPath (*refElement->path); } @@ -337,7 +319,17 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent else if (element.reference) { if (auto refElement = elementsById[*element.reference]; refElement != nullptr && refElement->path) + { + /* + // For use elements, apply the referenced element's transform for stroke rendering + const auto referencedSavedState = g.saveState(); + + if (refElement->transform) + g.setTransform (*refElement->transform); + */ + g.strokePath (*refElement->path); + } } } @@ -368,14 +360,9 @@ bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, Affin if (pathData.isEmpty() || ! path.fromString (pathData)) return false; - currentTransform = parseTransform (element, currentTransform, *e); - - // Apply accumulated transform (including parent group transforms) to path geometry - if (!currentTransform.isIdentity()) - path.transform (currentTransform); - e->path = std::move (path); - + + currentTransform = parseTransform (element, currentTransform, *e); parseStyle (element, currentTransform, *e); } else if (element.hasTagName ("g")) @@ -389,7 +376,24 @@ bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, Affin if (href.isNotEmpty() && href.startsWith ("#")) e->reference = href.substring (1); + // Handle x,y positioning for use elements (SVG spec requirement) + auto x = element.getDoubleAttribute ("x"); + auto y = element.getDoubleAttribute ("y"); + AffineTransform useTransform; + if (x != 0.0 || y != 0.0) + useTransform = AffineTransform::translation (x, y); + currentTransform = parseTransform (element, currentTransform, *e); + + // Combine use element positioning with any explicit transform + if (!useTransform.isIdentity()) + { + if (e->transform.has_value()) + e->transform = useTransform.followedBy (*e->transform); + else + e->transform = useTransform; + } + parseStyle (element, currentTransform, *e); } else if (element.hasTagName ("ellipse")) @@ -401,14 +405,9 @@ bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, Affin auto path = Path(); path.addCenteredEllipse (cx, cy, rx, ry); - currentTransform = parseTransform (element, currentTransform, *e); - - // Apply accumulated transform (including parent group transforms) to path geometry - if (!currentTransform.isIdentity()) - path.transform (currentTransform); - e->path = std::move (path); - + + currentTransform = parseTransform (element, currentTransform, *e); parseStyle (element, currentTransform, *e); } else if (element.hasTagName ("circle")) @@ -419,14 +418,9 @@ bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, Affin auto path = Path(); path.addCenteredEllipse (cx, cy, r, r); - currentTransform = parseTransform (element, currentTransform, *e); - - // Apply accumulated transform (including parent group transforms) to path geometry - if (!currentTransform.isIdentity()) - path.transform (currentTransform); - e->path = std::move (path); - + + currentTransform = parseTransform (element, currentTransform, *e); parseStyle (element, currentTransform, *e); } else if (element.hasTagName ("rect")) @@ -445,20 +439,17 @@ bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, Affin rx = ry; if (ry == 0.0) ry = rx; + path.addRoundedRectangle (x, y, width, height, rx, ry, rx, ry); } else { path.addRectangle (x, y, width, height); } - currentTransform = parseTransform (element, currentTransform, *e); - - // Apply accumulated transform (including parent group transforms) to path geometry - if (!currentTransform.isIdentity()) - path.transform (currentTransform); - + e->path = std::move (path); - + + currentTransform = parseTransform (element, currentTransform, *e); parseStyle (element, currentTransform, *e); } else if (element.hasTagName ("line")) @@ -471,14 +462,9 @@ bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, Affin auto path = Path(); path.startNewSubPath (x1, y1); path.lineTo (x2, y2); - currentTransform = parseTransform (element, currentTransform, *e); - - // Apply accumulated transform (including parent group transforms) to path geometry - if (!currentTransform.isIdentity()) - path.transform (currentTransform); - e->path = std::move (path); - + + currentTransform = parseTransform (element, currentTransform, *e); parseStyle (element, currentTransform, *e); } else if (element.hasTagName ("polygon")) @@ -498,6 +484,7 @@ bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, Affin path.closeSubPath(); } + e->path = std::move (path); } @@ -519,6 +506,7 @@ bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, Affin for (int i = 2; i < coords.size(); i += 2) path.lineTo (coords[i].getFloatValue(), coords[i + 1].getFloatValue()); } + e->path = std::move (path); } @@ -781,6 +769,7 @@ AffineTransform Drawable::parseTransform (const XmlElement& element, const Affin } e.transform = result; + YUP_DBG("Parsed element transform: " << result.toString()); } return currentTransform.followedBy (result); @@ -1262,8 +1251,50 @@ AffineTransform Drawable::calculateTransformForTarget (const Rectangle& s // Create transform: translate to origin, scale, then translate to target position return AffineTransform::translation (-sourceBounds.getX(), -sourceBounds.getY()) .scaled (scaleX, scaleY) - .translated (offsetX, offsetY) - .followedBy (transform); // Apply the drawable's own transform + .translated (offsetX, offsetY); +} + +//============================================================================== + +Fitting Drawable::parsePreserveAspectRatio (const String& preserveAspectRatio) +{ + if (preserveAspectRatio.isEmpty() || preserveAspectRatio == "xMidYMid meet") + return Fitting::scaleToFit; // Default SVG behavior + + if (preserveAspectRatio.contains ("none")) + return Fitting::fill; // Non-uniform scaling allowed + + if (preserveAspectRatio.contains ("slice")) + return Fitting::scaleToFill; // Scale to fill, may crop + + // Default to uniform scaling (meet) + return Fitting::scaleToFit; +} + +Justification Drawable::parseAspectRatioAlignment (const String& preserveAspectRatio) +{ + if (preserveAspectRatio.isEmpty()) + return Justification::center; // Default SVG alignment + + Justification result = Justification::left; + + // Parse horizontal alignment + if (preserveAspectRatio.contains ("xMin")) + result = result | Justification::left; + else if (preserveAspectRatio.contains ("xMax")) + result = result | Justification::right; + else // xMid (default) + result = result | Justification::horizontalCenter; + + // Parse vertical alignment + if (preserveAspectRatio.contains ("YMin")) + result = result | Justification::top; + else if (preserveAspectRatio.contains ("YMax")) + result = result | Justification::bottom; + else // YMid (default) + result = result | Justification::verticalCenter; + + return result; } } // namespace yup diff --git a/modules/yup_graphics/drawables/yup_Drawable.h b/modules/yup_graphics/drawables/yup_Drawable.h index c86639c45..5c52dc9a1 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.h +++ b/modules/yup_graphics/drawables/yup_Drawable.h @@ -36,21 +36,6 @@ class YUP_API Drawable //============================================================================== void clear(); - //============================================================================== - /** Sets a transform to be applied when painting the drawable. - - This transform is applied on top of any transforms defined within the SVG itself. - - @param newTransform The transform to apply when painting. - */ - void setTransform (const AffineTransform& newTransform); - - /** Gets the current transform that will be applied when painting. - - @return The current transform. - */ - AffineTransform getTransform() const; - //============================================================================== /** Gets the bounds of the drawable content. @@ -165,6 +150,10 @@ class YUP_API Drawable void parseCSSStyle (const String& styleString, Element& e); float parseUnit (const String& value, float defaultValue = 0.0f, float fontSize = 12.0f, float viewportSize = 100.0f); + // SVG preserveAspectRatio parsing + Fitting parsePreserveAspectRatio (const String& preserveAspectRatio); + Justification parseAspectRatioAlignment (const String& preserveAspectRatio); + // Helper methods for layout and painting Rectangle calculateBounds() const; AffineTransform calculateTransformForTarget (const Rectangle& sourceBounds, const Rectangle& targetArea, Fitting fitting, Justification justification) const; diff --git a/modules/yup_graphics/graphics/yup_Graphics.cpp b/modules/yup_graphics/graphics/yup_Graphics.cpp index d3ea6b743..df747a490 100644 --- a/modules/yup_graphics/graphics/yup_Graphics.cpp +++ b/modules/yup_graphics/graphics/yup_Graphics.cpp @@ -391,6 +391,11 @@ Rectangle Graphics::getDrawingArea() const //============================================================================== void Graphics::setTransform (const AffineTransform& transform) +{ + currentRenderOptions().transform = transform; +} + +void Graphics::addTransform (const AffineTransform& transform) { currentRenderOptions().transform = currentRenderOptions().transform.followedBy (transform); } diff --git a/modules/yup_graphics/graphics/yup_Graphics.h b/modules/yup_graphics/graphics/yup_Graphics.h index c5645c3ef..022485f25 100644 --- a/modules/yup_graphics/graphics/yup_Graphics.h +++ b/modules/yup_graphics/graphics/yup_Graphics.h @@ -239,6 +239,13 @@ class YUP_API Graphics */ void setTransform (const AffineTransform& transform); + /** Adds a transformation to the current transformation matrix. + The new transform is applied on top of the existing transform. + + @param transform The transformation to add to the current transform. + */ + void addTransform (const AffineTransform& transform); + /** Retrieves the current affine transformation. @return The current affine transformation. diff --git a/modules/yup_graphics/layout/yup_Justification.h b/modules/yup_graphics/layout/yup_Justification.h index a88821993..ea9f31652 100644 --- a/modules/yup_graphics/layout/yup_Justification.h +++ b/modules/yup_graphics/layout/yup_Justification.h @@ -48,4 +48,19 @@ enum class Justification centerBottom = horizontalCenter | bottom /**< Centers the content horizontally and aligns it to the bottom. */ }; +constexpr Justification operator| (Justification lhs, Justification rhs) noexcept +{ + return static_cast (static_cast (lhs) | static_cast (rhs)); +} + +constexpr Justification operator& (Justification lhs, Justification rhs) noexcept +{ + return static_cast (static_cast (lhs) & static_cast (rhs)); +} + +constexpr Justification operator~ (Justification lhs) noexcept +{ + return static_cast (~static_cast (lhs)); +} + } // namespace yup diff --git a/modules/yup_graphics/primitives/yup_AffineTransform.h b/modules/yup_graphics/primitives/yup_AffineTransform.h index 0bec1c6cf..3065fdc81 100644 --- a/modules/yup_graphics/primitives/yup_AffineTransform.h +++ b/modules/yup_graphics/primitives/yup_AffineTransform.h @@ -722,6 +722,15 @@ class YUP_API AffineTransform transformPoints (std::forward (args)...); } + //============================================================================== + + String toString() const + { + String result; + result << m[0] << ", " << m[1] << ", " << m[2] << ", " << m[3] << ", " << m[4] << ", " << m[5]; + return result; + } + //============================================================================== /** Equality operator diff --git a/modules/yup_graphics/primitives/yup_Line.h b/modules/yup_graphics/primitives/yup_Line.h index 18f73249e..3becfb468 100644 --- a/modules/yup_graphics/primitives/yup_Line.h +++ b/modules/yup_graphics/primitives/yup_Line.h @@ -596,6 +596,15 @@ class YUP_API Line return { p1.roundToInt(), p2.roundToInt() }; } + //============================================================================== + + String toString() const + { + String result; + result << p1 << ", " << p2; + return result; + } + //============================================================================== /** Unary minus operator to negate both points of the line. diff --git a/modules/yup_graphics/primitives/yup_Path.cpp b/modules/yup_graphics/primitives/yup_Path.cpp index 101776141..5460684e3 100644 --- a/modules/yup_graphics/primitives/yup_Path.cpp +++ b/modules/yup_graphics/primitives/yup_Path.cpp @@ -289,10 +289,13 @@ Path& Path::quadTo (float x, float y, float x1, float y1) moveTo (x, y); const rive::Vec2D& last = rawPath.points().back(); + const rive::Vec2D control (x1, y1); + const rive::Vec2D end (x, y); + + const rive::Vec2D c1 = rive::Vec2D::lerp (last, control, 2.0f / 3.0f); + const rive::Vec2D c2 = rive::Vec2D::lerp (end, control, 2.0f / 3.0f); - path->cubic (rive::Vec2D::lerp (last, rive::Vec2D (x1, y1), 2 / 3.f), - rive::Vec2D::lerp (rive::Vec2D (x, y), rive::Vec2D (x1, y1), 2 / 3.f), - rive::Vec2D (x, y)); + path->cubic (c1, c2, end); return *this; } @@ -1288,7 +1291,7 @@ void handleQuadTo (String::CharPointerType& data, Path& path, float& currentX, f y += currentY; } - path.quadTo (x1, y1, x, y); + path.quadTo (x, y, x1, y1); currentX = x; currentY = y; @@ -1330,7 +1333,7 @@ void handleSmoothQuadTo (String::CharPointerType& data, Path& path, float& curre y += currentY; } - path.quadTo (cx, cy, x, y); + path.quadTo (x, y, cx, cy); // Update the current position currentX = x; @@ -1485,7 +1488,11 @@ void handleEllipticalArc (String::CharPointerType& data, Path& path, float& curr // Calculate the center point (cx, cy) float sign = (largeArc != sweep) ? 1.0f : -1.0f; - float sqrtFactor = std::sqrt ((rxSq * rySq - rxSq * y1PrimeSq - rySq * x1PrimeSq) / (rxSq * y1PrimeSq + rySq * x1PrimeSq)); + float numerator = rxSq * rySq - rxSq * y1PrimeSq - rySq * x1PrimeSq; + float denominator = rxSq * y1PrimeSq + rySq * x1PrimeSq; + float sqrtFactor = 0.0f; + if (denominator > 0.0f && numerator >= 0.0f) + sqrtFactor = std::sqrt (numerator / denominator); float cxPrime = sign * sqrtFactor * (rx * y1Prime / ry); float cyPrime = sign * sqrtFactor * (-ry * x1Prime / rx); @@ -1514,8 +1521,9 @@ void handleEllipticalArc (String::CharPointerType& data, Path& path, float& curr // Ensure the delta angle is within the range [-2Ï€, 2Ï€] deltaAngle = std::fmod (deltaAngle, MathConstants::twoPi); - // Add the arc to the path - path.addCenteredArc (centreX, centreY, rx, ry, xAxisRotation, startAngle, startAngle + deltaAngle, true); + // Add the arc to the path (use angleRad which is already in radians) + // Don't start new subpath - SVG arcs continue from current position + path.addCenteredArc (centreX, centreY, rx, ry, angleRad, startAngle, startAngle + deltaAngle, false); // Update the current position currentX = x; diff --git a/modules/yup_graphics/primitives/yup_Point.h b/modules/yup_graphics/primitives/yup_Point.h index 394c1663b..94ffc7cc9 100644 --- a/modules/yup_graphics/primitives/yup_Point.h +++ b/modules/yup_graphics/primitives/yup_Point.h @@ -1250,6 +1250,15 @@ class YUP_API Point return reflectedOverOrigin(); } + //============================================================================== + + String toString() const + { + String result; + result << x << ", " << y; + return result; + } + //============================================================================== /** Returns true if the two points are approximately equal. */ constexpr bool approximatelyEqualTo (const Point& other) const noexcept diff --git a/modules/yup_graphics/primitives/yup_Rectangle.h b/modules/yup_graphics/primitives/yup_Rectangle.h index 1cbb526fe..5511ac5db 100644 --- a/modules/yup_graphics/primitives/yup_Rectangle.h +++ b/modules/yup_graphics/primitives/yup_Rectangle.h @@ -1968,6 +1968,15 @@ class YUP_API Rectangle return *this; } + //============================================================================== + + String toString() const + { + String result; + result << xy << ", " << size; + return result; + } + //============================================================================== /** Returns true if the two rectangles are approximately equal. diff --git a/modules/yup_graphics/primitives/yup_Size.h b/modules/yup_graphics/primitives/yup_Size.h index be89a5b43..ed22d8a65 100644 --- a/modules/yup_graphics/primitives/yup_Size.h +++ b/modules/yup_graphics/primitives/yup_Size.h @@ -635,6 +635,15 @@ class YUP_API Size return *this; } + //============================================================================== + + String toString() const + { + String result; + result << width << ", " << height; + return result; + } + //============================================================================== /** Returns true if the two sizes are approximately equal. */ constexpr bool approximatelyEqualTo (const Size& other) const noexcept diff --git a/tests/yup_graphics/yup_Graphics.cpp b/tests/yup_graphics/yup_Graphics.cpp index 6f4566cda..2e4beb3c7 100644 --- a/tests/yup_graphics/yup_Graphics.cpp +++ b/tests/yup_graphics/yup_Graphics.cpp @@ -446,12 +446,12 @@ TEST_F (GraphicsTest, Image_Drawing_Operations) TEST_F (GraphicsTest, Transform_Accumulation) { - // Test that transforms accumulate properly + // Test that transforms accumulate properly using addTransform auto translation1 = AffineTransform::translation (10.0f, 20.0f); auto translation2 = AffineTransform::translation (5.0f, 15.0f); graphics->setTransform (translation1); - graphics->setTransform (translation2); + graphics->addTransform (translation2); auto result = graphics->getTransform(); @@ -460,6 +460,22 @@ TEST_F (GraphicsTest, Transform_Accumulation) EXPECT_FLOAT_EQ (result.getTranslateY(), 35.0f); // 20 + 15 } +TEST_F (GraphicsTest, Transform_SetReplace) +{ + // Test that setTransform replaces instead of accumulates + auto translation1 = AffineTransform::translation (10.0f, 20.0f); + auto translation2 = AffineTransform::translation (5.0f, 15.0f); + + graphics->setTransform (translation1); + graphics->setTransform (translation2); + + auto result = graphics->getTransform(); + + // The second transform should replace the first + EXPECT_FLOAT_EQ (result.getTranslateX(), 5.0f); // Only second transform + EXPECT_FLOAT_EQ (result.getTranslateY(), 15.0f); // Only second transform +} + TEST_F (GraphicsTest, Edge_Case_Values) { // Test edge case values From f5f01c9dc2581c606d44dfd2815401722a3fcb49 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 2 Jul 2025 18:15:21 +0200 Subject: [PATCH 18/25] More tweaks --- .../yup_graphics/drawables/yup_Drawable.cpp | 51 +++++++++++-------- modules/yup_graphics/graphics/yup_Color.cpp | 2 +- modules/yup_graphics/primitives/yup_Path.cpp | 34 ++++++++++++- modules/yup_graphics/primitives/yup_Path.h | 3 +- 4 files changed, 65 insertions(+), 25 deletions(-) diff --git a/modules/yup_graphics/drawables/yup_Drawable.cpp b/modules/yup_graphics/drawables/yup_Drawable.cpp index 7cf9f858f..03442670c 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.cpp +++ b/modules/yup_graphics/drawables/yup_Drawable.cpp @@ -208,15 +208,7 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent { if (auto refElement = elementsById[*element.reference]; refElement != nullptr && refElement->path) { - /* - // For use elements, apply the referenced element's transform and render its path - // This ensures proper coordinate system handling for referenced elements - const auto referencedSavedState = g.saveState(); - if (refElement->transform) - g.setTransform (*refElement->transform); - */ - if (refElement->path->isClosed()) g.fillPath (*refElement->path); } @@ -319,22 +311,15 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent else if (element.reference) { if (auto refElement = elementsById[*element.reference]; refElement != nullptr && refElement->path) - { - /* - // For use elements, apply the referenced element's transform for stroke rendering - const auto referencedSavedState = g.saveState(); - - if (refElement->transform) - g.setTransform (*refElement->transform); - */ - g.strokePath (*refElement->path); - } } } for (const auto& childElement : element.children) + { + YUP_DBG("Rendering child element - current graphics transform: " << g.getTransform().toString()); paintElement (g, *childElement, isFillDefined, isStrokeDefined); + } // paintDebugElement (g, element); } @@ -599,7 +584,10 @@ void Drawable::parseStyle (const XmlElement& element, const AffineTransform& cur if (fill.startsWith ("url(#")) e.fillUrl = fill.substring (5, fill.length() - 1); else + { e.fillColor = Color::fromString (fill); + YUP_DBG("Parsed fill color: " << fill << " -> " << e.fillColor->toString()); + } } else { @@ -740,29 +728,48 @@ AffineTransform Drawable::parseTransform (const XmlElement& element, const Affin // Apply the parsed transform if (type == "translate" && (params.size() == 1 || params.size() == 2)) { - result = result.translated (params[0], (params.size() == 2) ? params[1] : 0.0f); + auto tx = params[0]; + auto ty = (params.size() == 2) ? params[1] : 0.0f; + YUP_DBG("Applying translate(" << tx << ", " << ty << ")"); + result = result.translated (tx, ty); } else if (type == "scale" && (params.size() == 1 || params.size() == 2)) { - result = result.scaled (params[0], (params.size() == 2) ? params[1] : params[0]); + auto sx = params[0]; + auto sy = (params.size() == 2) ? params[1] : params[0]; + YUP_DBG("Applying scale(" << sx << ", " << sy << ")"); + result = result.scaled (sx, sy); } else if (type == "rotate" && (params.size() == 1 || params.size() == 3)) { if (params.size() == 1) + { + YUP_DBG("Applying rotate(" << params[0] << ")"); result = result.rotated (degreesToRadians (params[0])); + } else + { + YUP_DBG("Applying rotate(" << params[0] << ", " << params[1] << ", " << params[2] << ")"); result = result.rotated (degreesToRadians (params[0]), params[1], params[2]); + } } else if (type == "skewX" && params.size() == 1) { - result = result.sheared (std::tanf (degreesToRadians (params[0])), 0.0f); + auto angle = params[0]; + auto factor = std::tanf (degreesToRadians (angle)); + YUP_DBG("Applying skewX(" << angle << ") -> factor=" << factor); + result = result.sheared (factor, 0.0f); } else if (type == "skewY" && params.size() == 1) { - result = result.sheared (0.0f, std::tanf (degreesToRadians (params[0]))); + auto angle = params[0]; + auto factor = std::tanf (degreesToRadians (angle)); + YUP_DBG("Applying skewY(" << angle << ") -> factor=" << factor); + result = result.sheared (0.0f, factor); } else if (type == "matrix" && params.size() == 6) { + YUP_DBG("Applying matrix(" << params[0] << ", " << params[1] << ", " << params[2] << ", " << params[3] << ", " << params[4] << ", " << params[5] << ")"); result = result.followedBy (AffineTransform ( params[0], params[2], params[4], params[1], params[3], params[5])); } diff --git a/modules/yup_graphics/graphics/yup_Color.cpp b/modules/yup_graphics/graphics/yup_Color.cpp index fde626d77..c2bf2fac5 100644 --- a/modules/yup_graphics/graphics/yup_Color.cpp +++ b/modules/yup_graphics/graphics/yup_Color.cpp @@ -85,7 +85,7 @@ Color parseHexColor (const String& hexString) uint8 blue = static_cast (hexCharToInt (data[5]) * 16 + hexCharToInt (data[6])); uint8 alpha = static_cast (hexCharToInt (data[7]) * 16 + hexCharToInt (data[8])); - return { red, green, blue, alpha }; + return { alpha, red, green, blue }; } else { diff --git a/modules/yup_graphics/primitives/yup_Path.cpp b/modules/yup_graphics/primitives/yup_Path.cpp index 5460684e3..f9dc2648f 100644 --- a/modules/yup_graphics/primitives/yup_Path.cpp +++ b/modules/yup_graphics/primitives/yup_Path.cpp @@ -858,7 +858,39 @@ void Path::closeSubPath() close(); } -bool Path::isClosed() const +bool Path::isClosed (float tolerance) const +{ + auto first = begin(); + auto last = end(); + if (first == last) + return false; + + Point firstPoint (0.0f, 0.0f); + Point lastPoint (0.0f, 0.0f); + bool hasFirstPoint = false; + + for (; first != last; ++first) + { + auto segment = *first; + if (segment.verb == PathVerb::MoveTo && ! hasFirstPoint) + { + firstPoint = segment.point; + hasFirstPoint = true; + } + + if (segment.verb != PathVerb::Close) + lastPoint = segment.point; + else + return true; + } + + if (! hasFirstPoint) + return false; + + return firstPoint.distanceTo (lastPoint) <= tolerance; +} + +bool Path::isExplicitlyClosed() const { for (const auto& segment : *this) { diff --git a/modules/yup_graphics/primitives/yup_Path.h b/modules/yup_graphics/primitives/yup_Path.h index 0fc08d6c5..6444dfa4a 100644 --- a/modules/yup_graphics/primitives/yup_Path.h +++ b/modules/yup_graphics/primitives/yup_Path.h @@ -599,7 +599,8 @@ class YUP_API Path void closeSubPath(); - bool isClosed() const; + bool isClosed (float tolerance = 0.001f) const; + bool isExplicitlyClosed() const; //============================================================================== /** Appends another path to this one. From 2ca87a8e851529c1d5deb995ab0af29ddf0adfad Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 2 Jul 2025 18:26:01 +0200 Subject: [PATCH 19/25] More output --- modules/yup_graphics/drawables/yup_Drawable.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/modules/yup_graphics/drawables/yup_Drawable.cpp b/modules/yup_graphics/drawables/yup_Drawable.cpp index 03442670c..85268993b 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.cpp +++ b/modules/yup_graphics/drawables/yup_Drawable.cpp @@ -208,6 +208,10 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent { if (auto refElement = elementsById[*element.reference]; refElement != nullptr && refElement->path) { + YUP_DBG("Rendering use element - reference: " << *element.reference); + YUP_DBG("Use element transform: " << (element.transform ? element.transform->toString() : "none")); + YUP_DBG("Referenced element transform: " << (refElement->transform ? refElement->transform->toString() : "none")); + YUP_DBG("Graphics transform during use fill: " << g.getTransform().toString()); if (refElement->path->isClosed()) g.fillPath (*refElement->path); @@ -311,7 +315,11 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent else if (element.reference) { if (auto refElement = elementsById[*element.reference]; refElement != nullptr && refElement->path) + { + YUP_DBG("Stroking use element - reference: " << *element.reference); + YUP_DBG("Graphics transform during stroke: " << g.getTransform().toString()); g.strokePath (*refElement->path); + } } } From 3d2a22efc8467bf2be5abd2fb846dd52f70f3282 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 2 Jul 2025 18:55:57 +0200 Subject: [PATCH 20/25] Avoid strange behaviour --- modules/yup_events/native/yup_MessageManager_mac.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/yup_events/native/yup_MessageManager_mac.mm b/modules/yup_events/native/yup_MessageManager_mac.mm index 150d5a18e..b63e97a4b 100644 --- a/modules/yup_events/native/yup_MessageManager_mac.mm +++ b/modules/yup_events/native/yup_MessageManager_mac.mm @@ -431,7 +431,7 @@ static void shutdownNSApp() constexpr int millisecondsToRunFor = static_cast(1000.0f / 60.0f); // TODO - runNSApplication(); + //runNSApplication(); while (quitMessagePosted.get() == 0) { From 49e94a2189eaa78610543746d914bd415d33e511 Mon Sep 17 00:00:00 2001 From: Yup Bot Date: Wed, 2 Jul 2025 16:56:44 +0000 Subject: [PATCH 21/25] Code formatting --- .../native/yup_MessageManager_mac.mm | 2 +- .../yup_graphics/drawables/yup_Drawable.cpp | 140 +++++++++--------- modules/yup_graphics/drawables/yup_Drawable.h | 4 +- modules/yup_graphics/layout/yup_Fitting.h | 22 +-- .../yup_graphics/layout/yup_Justification.h | 2 +- modules/yup_graphics/primitives/yup_Path.cpp | 10 +- 6 files changed, 90 insertions(+), 90 deletions(-) diff --git a/modules/yup_events/native/yup_MessageManager_mac.mm b/modules/yup_events/native/yup_MessageManager_mac.mm index b63e97a4b..01f44e798 100644 --- a/modules/yup_events/native/yup_MessageManager_mac.mm +++ b/modules/yup_events/native/yup_MessageManager_mac.mm @@ -431,7 +431,7 @@ static void shutdownNSApp() constexpr int millisecondsToRunFor = static_cast(1000.0f / 60.0f); // TODO - //runNSApplication(); + // runNSApplication(); while (quitMessagePosted.get() == 0) { diff --git a/modules/yup_graphics/drawables/yup_Drawable.cpp b/modules/yup_graphics/drawables/yup_Drawable.cpp index 85268993b..e8281ade0 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.cpp +++ b/modules/yup_graphics/drawables/yup_Drawable.cpp @@ -59,7 +59,7 @@ bool Drawable::parseSVG (const File& svgFile) size.setHeight (height == 0.0 ? viewBox.getHeight() : height); // ViewBox transform is now calculated at render-time based on actual target area - YUP_DBG("Parse complete - viewBox: " << viewBox.toString() << " size: " << size.getWidth() << "x" << size.getHeight()); + YUP_DBG ("Parse complete - viewBox: " << viewBox.toString() << " size: " << size.getWidth() << "x" << size.getHeight()); auto result = parseElement (*svgRoot, true, {}); @@ -98,7 +98,7 @@ Rectangle Drawable::getBounds() const void Drawable::paint (Graphics& g) { const auto savedState = g.saveState(); - + g.setStrokeWidth (1.0f); g.setFillColor (Colors::black); @@ -111,11 +111,11 @@ void Drawable::paint (Graphics& g) void Drawable::paint (Graphics& g, const Rectangle& targetArea, Fitting fitting, Justification justification) { - YUP_DBG("Fitted paint called - bounds: " << bounds.toString() << " targetArea: " << targetArea.toString()); - + YUP_DBG ("Fitted paint called - bounds: " << bounds.toString() << " targetArea: " << targetArea.toString()); + if (bounds.isEmpty()) return; - + const auto savedState = g.saveState(); auto finalBounds = viewBox.isEmpty() ? bounds : viewBox; @@ -138,17 +138,17 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent bool isFillDefined = hasParentFillEnabled; bool isStrokeDefined = hasParentStrokeEnabled; - - YUP_DBG("paintElement called - hasPath: " << (element.path ? "true" : "false") << " hasTransform: " << (element.transform ? "true" : "false")); + + YUP_DBG ("paintElement called - hasPath: " << (element.path ? "true" : "false") << " hasTransform: " << (element.transform ? "true" : "false")); // Apply element transform if present - use proper composition for coordinate systems if (element.transform) { - YUP_DBG("Applying element transform - before: " << g.getTransform().toString() << " adding: " << element.transform->toString()); - // For proper coordinate system handling, we need to apply element transform + YUP_DBG ("Applying element transform - before: " << g.getTransform().toString() << " adding: " << element.transform->toString()); + // For proper coordinate system handling, we need to apply element transform // in the element's local space, then transform to viewport space g.setTransform (element.transform->followedBy (g.getTransform())); - YUP_DBG("After transform: " << g.getTransform().toString()); + YUP_DBG ("After transform: " << g.getTransform().toString()); } if (element.opacity) @@ -208,11 +208,11 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent { if (auto refElement = elementsById[*element.reference]; refElement != nullptr && refElement->path) { - YUP_DBG("Rendering use element - reference: " << *element.reference); - YUP_DBG("Use element transform: " << (element.transform ? element.transform->toString() : "none")); - YUP_DBG("Referenced element transform: " << (refElement->transform ? refElement->transform->toString() : "none")); - YUP_DBG("Graphics transform during use fill: " << g.getTransform().toString()); - + YUP_DBG ("Rendering use element - reference: " << *element.reference); + YUP_DBG ("Use element transform: " << (element.transform ? element.transform->toString() : "none")); + YUP_DBG ("Referenced element transform: " << (refElement->transform ? refElement->transform->toString() : "none")); + YUP_DBG ("Graphics transform during use fill: " << g.getTransform().toString()); + if (refElement->path->isClosed()) g.fillPath (*refElement->path); } @@ -316,8 +316,8 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent { if (auto refElement = elementsById[*element.reference]; refElement != nullptr && refElement->path) { - YUP_DBG("Stroking use element - reference: " << *element.reference); - YUP_DBG("Graphics transform during stroke: " << g.getTransform().toString()); + YUP_DBG ("Stroking use element - reference: " << *element.reference); + YUP_DBG ("Graphics transform during stroke: " << g.getTransform().toString()); g.strokePath (*refElement->path); } } @@ -325,7 +325,7 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent for (const auto& childElement : element.children) { - YUP_DBG("Rendering child element - current graphics transform: " << g.getTransform().toString()); + YUP_DBG ("Rendering child element - current graphics transform: " << g.getTransform().toString()); paintElement (g, *childElement, isFillDefined, isStrokeDefined); } @@ -377,16 +377,16 @@ bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, Affin useTransform = AffineTransform::translation (x, y); currentTransform = parseTransform (element, currentTransform, *e); - + // Combine use element positioning with any explicit transform - if (!useTransform.isIdentity()) + if (! useTransform.isIdentity()) { if (e->transform.has_value()) e->transform = useTransform.followedBy (*e->transform); else e->transform = useTransform; } - + parseStyle (element, currentTransform, *e); } else if (element.hasTagName ("ellipse")) @@ -594,7 +594,7 @@ void Drawable::parseStyle (const XmlElement& element, const AffineTransform& cur else { e.fillColor = Color::fromString (fill); - YUP_DBG("Parsed fill color: " << fill << " -> " << e.fillColor->toString()); + YUP_DBG ("Parsed fill color: " << fill << " -> " << e.fillColor->toString()); } } else @@ -738,26 +738,26 @@ AffineTransform Drawable::parseTransform (const XmlElement& element, const Affin { auto tx = params[0]; auto ty = (params.size() == 2) ? params[1] : 0.0f; - YUP_DBG("Applying translate(" << tx << ", " << ty << ")"); + YUP_DBG ("Applying translate(" << tx << ", " << ty << ")"); result = result.translated (tx, ty); } else if (type == "scale" && (params.size() == 1 || params.size() == 2)) { auto sx = params[0]; auto sy = (params.size() == 2) ? params[1] : params[0]; - YUP_DBG("Applying scale(" << sx << ", " << sy << ")"); + YUP_DBG ("Applying scale(" << sx << ", " << sy << ")"); result = result.scaled (sx, sy); } else if (type == "rotate" && (params.size() == 1 || params.size() == 3)) { if (params.size() == 1) { - YUP_DBG("Applying rotate(" << params[0] << ")"); + YUP_DBG ("Applying rotate(" << params[0] << ")"); result = result.rotated (degreesToRadians (params[0])); } else { - YUP_DBG("Applying rotate(" << params[0] << ", " << params[1] << ", " << params[2] << ")"); + YUP_DBG ("Applying rotate(" << params[0] << ", " << params[1] << ", " << params[2] << ")"); result = result.rotated (degreesToRadians (params[0]), params[1], params[2]); } } @@ -765,26 +765,26 @@ AffineTransform Drawable::parseTransform (const XmlElement& element, const Affin { auto angle = params[0]; auto factor = std::tanf (degreesToRadians (angle)); - YUP_DBG("Applying skewX(" << angle << ") -> factor=" << factor); + YUP_DBG ("Applying skewX(" << angle << ") -> factor=" << factor); result = result.sheared (factor, 0.0f); } else if (type == "skewY" && params.size() == 1) { auto angle = params[0]; auto factor = std::tanf (degreesToRadians (angle)); - YUP_DBG("Applying skewY(" << angle << ") -> factor=" << factor); + YUP_DBG ("Applying skewY(" << angle << ") -> factor=" << factor); result = result.sheared (0.0f, factor); } else if (type == "matrix" && params.size() == 6) { - YUP_DBG("Applying matrix(" << params[0] << ", " << params[1] << ", " << params[2] << ", " << params[3] << ", " << params[4] << ", " << params[5] << ")"); + YUP_DBG ("Applying matrix(" << params[0] << ", " << params[1] << ", " << params[2] << ", " << params[3] << ", " << params[4] << ", " << params[5] << ")"); result = result.followedBy (AffineTransform ( params[0], params[2], params[4], params[1], params[3], params[5])); } } e.transform = result; - YUP_DBG("Parsed element transform: " << result.toString()); + YUP_DBG ("Parsed element transform: " << result.toString()); } return currentTransform.followedBy (result); @@ -1154,17 +1154,17 @@ float Drawable::parseUnit (const String& value, float defaultValue, float fontSi Rectangle Drawable::calculateBounds() const { // Use viewBox if available, otherwise use size - if (!viewBox.isEmpty()) + if (! viewBox.isEmpty()) return viewBox; - + if (size.getWidth() > 0 && size.getHeight() > 0) return Rectangle (0, 0, size.getWidth(), size.getHeight()); - + // Fallback: calculate bounds from all elements with their transforms applied // This gives us the actual visual bounds of the rendered content Rectangle bounds; bool hasValidBounds = false; - + for (const auto& element : elements) { if (element->path) @@ -1172,7 +1172,7 @@ Rectangle Drawable::calculateBounds() const auto pathBounds = element->path->getBounds(); if (element->transform) pathBounds = element->path->getBoundsTransformed (*element->transform); - + if (hasValidBounds) bounds = bounds.unionWith (pathBounds); else @@ -1182,7 +1182,7 @@ Rectangle Drawable::calculateBounds() const } } } - + return hasValidBounds ? bounds : Rectangle (0, 0, 100, 100); } @@ -1192,77 +1192,77 @@ AffineTransform Drawable::calculateTransformForTarget (const Rectangle& s { if (sourceBounds.isEmpty() || targetArea.isEmpty()) return AffineTransform::identity(); - + float scaleX = targetArea.getWidth() / sourceBounds.getWidth(); float scaleY = targetArea.getHeight() / sourceBounds.getHeight(); - + // Apply scaling based on fitting mode switch (fitting) { case Fitting::none: scaleX = scaleY = 1.0f; break; - + case Fitting::scaleToFit: - scaleX = scaleY = jmin (scaleX, scaleY); // Scale to fit both dimensions + scaleX = scaleY = jmin (scaleX, scaleY); // Scale to fit both dimensions break; - + case Fitting::fitWidth: - scaleY = scaleX; // Scale to fit width, preserve aspect ratio + scaleY = scaleX; // Scale to fit width, preserve aspect ratio break; - + case Fitting::fitHeight: - scaleX = scaleY; // Scale to fit height, preserve aspect ratio + scaleX = scaleY; // Scale to fit height, preserve aspect ratio break; - + case Fitting::scaleToFill: case Fitting::centerCrop: - scaleX = scaleY = jmax (scaleX, scaleY); // Scale to fill, may crop + scaleX = scaleY = jmax (scaleX, scaleY); // Scale to fill, may crop break; - + case Fitting::fill: // Use calculated scales as-is (non-uniform scaling) break; - + case Fitting::centerInside: // Like scaleToFit but don't upscale beyond original size scaleX = scaleY = jmin (1.0f, jmin (scaleX, scaleY)); break; - + case Fitting::stretchWidth: - scaleY = 1.0f; // Stretch horizontally only + scaleY = 1.0f; // Stretch horizontally only break; - + case Fitting::stretchHeight: - scaleX = 1.0f; // Stretch vertically only + scaleX = 1.0f; // Stretch vertically only break; - + case Fitting::tile: // For tile mode, use no scaling (tiling would be handled elsewhere) scaleX = scaleY = 1.0f; break; } - + // Calculate scaled size float scaledWidth = sourceBounds.getWidth() * scaleX; float scaledHeight = sourceBounds.getHeight() * scaleY; - + // Calculate offset based on justification float offsetX = targetArea.getX(); float offsetY = targetArea.getY(); - + // Horizontal justification - if ((static_cast(justification) & static_cast(Justification::horizontalCenter)) != 0) + if ((static_cast (justification) & static_cast (Justification::horizontalCenter)) != 0) offsetX += (targetArea.getWidth() - scaledWidth) * 0.5f; - else if ((static_cast(justification) & static_cast(Justification::right)) != 0) + else if ((static_cast (justification) & static_cast (Justification::right)) != 0) offsetX += targetArea.getWidth() - scaledWidth; - + // Vertical justification - if ((static_cast(justification) & static_cast(Justification::verticalCenter)) != 0) + if ((static_cast (justification) & static_cast (Justification::verticalCenter)) != 0) offsetY += (targetArea.getHeight() - scaledHeight) * 0.5f; - else if ((static_cast(justification) & static_cast(Justification::bottom)) != 0) + else if ((static_cast (justification) & static_cast (Justification::bottom)) != 0) offsetY += targetArea.getHeight() - scaledHeight; - + // Create transform: translate to origin, scale, then translate to target position return AffineTransform::translation (-sourceBounds.getX(), -sourceBounds.getY()) .scaled (scaleX, scaleY) @@ -1275,13 +1275,13 @@ Fitting Drawable::parsePreserveAspectRatio (const String& preserveAspectRatio) { if (preserveAspectRatio.isEmpty() || preserveAspectRatio == "xMidYMid meet") return Fitting::scaleToFit; // Default SVG behavior - + if (preserveAspectRatio.contains ("none")) return Fitting::fill; // Non-uniform scaling allowed - + if (preserveAspectRatio.contains ("slice")) return Fitting::scaleToFill; // Scale to fill, may crop - + // Default to uniform scaling (meet) return Fitting::scaleToFit; } @@ -1290,9 +1290,9 @@ Justification Drawable::parseAspectRatioAlignment (const String& preserveAspectR { if (preserveAspectRatio.isEmpty()) return Justification::center; // Default SVG alignment - + Justification result = Justification::left; - + // Parse horizontal alignment if (preserveAspectRatio.contains ("xMin")) result = result | Justification::left; @@ -1300,15 +1300,15 @@ Justification Drawable::parseAspectRatioAlignment (const String& preserveAspectR result = result | Justification::right; else // xMid (default) result = result | Justification::horizontalCenter; - - // Parse vertical alignment + + // Parse vertical alignment if (preserveAspectRatio.contains ("YMin")) result = result | Justification::top; else if (preserveAspectRatio.contains ("YMax")) result = result | Justification::bottom; else // YMid (default) result = result | Justification::verticalCenter; - + return result; } diff --git a/modules/yup_graphics/drawables/yup_Drawable.h b/modules/yup_graphics/drawables/yup_Drawable.h index 5c52dc9a1..2c7196c69 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.h +++ b/modules/yup_graphics/drawables/yup_Drawable.h @@ -149,11 +149,11 @@ class YUP_API Drawable ClipPath* getClipPathById (const String& id); void parseCSSStyle (const String& styleString, Element& e); float parseUnit (const String& value, float defaultValue = 0.0f, float fontSize = 12.0f, float viewportSize = 100.0f); - + // SVG preserveAspectRatio parsing Fitting parsePreserveAspectRatio (const String& preserveAspectRatio); Justification parseAspectRatioAlignment (const String& preserveAspectRatio); - + // Helper methods for layout and painting Rectangle calculateBounds() const; AffineTransform calculateTransformForTarget (const Rectangle& sourceBounds, const Rectangle& targetArea, Fitting fitting, Justification justification) const; diff --git a/modules/yup_graphics/layout/yup_Fitting.h b/modules/yup_graphics/layout/yup_Fitting.h index 5b15d9c5c..d231da085 100644 --- a/modules/yup_graphics/layout/yup_Fitting.h +++ b/modules/yup_graphics/layout/yup_Fitting.h @@ -29,17 +29,17 @@ namespace yup */ enum class Fitting { - none, /**< Do not scale or fit at all. */ - scaleToFit, /**< Scale proportionally to fit within bounds (preserve aspect ratio, no cropping). */ - fitWidth, /**< Scale to match width, preserve aspect ratio. */ - fitHeight, /**< Scale to match height, preserve aspect ratio. */ - scaleToFill, /**< Scale proportionally to completely fill bounds (may crop, preserves aspect ratio). */ - fill, /**< Stretch to fill bounds completely (aspect ratio not preserved). */ - tile, /**< Repeat content to fill bounds (used in backgrounds, patterns). */ - centerCrop, /**< Like scaleToFill, but ensures the center remains visible (common in media). */ - centerInside, /**< Like scaleToFit, but does not upscale beyond original size. */ - stretchWidth, /**< Stretch horizontally, preserve vertical size. */ - stretchHeight /**< Stretch vertically, preserve horizontal size. */ + none, /**< Do not scale or fit at all. */ + scaleToFit, /**< Scale proportionally to fit within bounds (preserve aspect ratio, no cropping). */ + fitWidth, /**< Scale to match width, preserve aspect ratio. */ + fitHeight, /**< Scale to match height, preserve aspect ratio. */ + scaleToFill, /**< Scale proportionally to completely fill bounds (may crop, preserves aspect ratio). */ + fill, /**< Stretch to fill bounds completely (aspect ratio not preserved). */ + tile, /**< Repeat content to fill bounds (used in backgrounds, patterns). */ + centerCrop, /**< Like scaleToFill, but ensures the center remains visible (common in media). */ + centerInside, /**< Like scaleToFit, but does not upscale beyond original size. */ + stretchWidth, /**< Stretch horizontally, preserve vertical size. */ + stretchHeight /**< Stretch vertically, preserve horizontal size. */ }; } // namespace yup diff --git a/modules/yup_graphics/layout/yup_Justification.h b/modules/yup_graphics/layout/yup_Justification.h index ea9f31652..055ab8c5c 100644 --- a/modules/yup_graphics/layout/yup_Justification.h +++ b/modules/yup_graphics/layout/yup_Justification.h @@ -58,7 +58,7 @@ constexpr Justification operator& (Justification lhs, Justification rhs) noexcep return static_cast (static_cast (lhs) & static_cast (rhs)); } -constexpr Justification operator~ (Justification lhs) noexcept +constexpr Justification operator~(Justification lhs) noexcept { return static_cast (~static_cast (lhs)); } diff --git a/modules/yup_graphics/primitives/yup_Path.cpp b/modules/yup_graphics/primitives/yup_Path.cpp index f9dc2648f..9445a9bcf 100644 --- a/modules/yup_graphics/primitives/yup_Path.cpp +++ b/modules/yup_graphics/primitives/yup_Path.cpp @@ -291,7 +291,7 @@ Path& Path::quadTo (float x, float y, float x1, float y1) const rive::Vec2D& last = rawPath.points().back(); const rive::Vec2D control (x1, y1); const rive::Vec2D end (x, y); - + const rive::Vec2D c1 = rive::Vec2D::lerp (last, control, 2.0f / 3.0f); const rive::Vec2D c2 = rive::Vec2D::lerp (end, control, 2.0f / 3.0f); @@ -864,7 +864,7 @@ bool Path::isClosed (float tolerance) const auto last = end(); if (first == last) return false; - + Point firstPoint (0.0f, 0.0f); Point lastPoint (0.0f, 0.0f); bool hasFirstPoint = false; @@ -877,16 +877,16 @@ bool Path::isClosed (float tolerance) const firstPoint = segment.point; hasFirstPoint = true; } - + if (segment.verb != PathVerb::Close) lastPoint = segment.point; else return true; } - + if (! hasFirstPoint) return false; - + return firstPoint.distanceTo (lastPoint) <= tolerance; } From 28c7a5eceaa42ecdb96633bc92168897862d5745 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Thu, 3 Jul 2025 00:39:08 +0200 Subject: [PATCH 22/25] More gradient fun --- .../yup_graphics/drawables/yup_Drawable.cpp | 576 +++++++++++++----- modules/yup_graphics/drawables/yup_Drawable.h | 51 +- 2 files changed, 470 insertions(+), 157 deletions(-) diff --git a/modules/yup_graphics/drawables/yup_Drawable.cpp b/modules/yup_graphics/drawables/yup_Drawable.cpp index e8281ade0..60e669d20 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.cpp +++ b/modules/yup_graphics/drawables/yup_Drawable.cpp @@ -158,7 +158,7 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent bool hasClipping = false; if (element.clipPathUrl) { - if (auto* clipPath = getClipPathById (*element.clipPathUrl)) + if (auto clipPath = getClipPathById (*element.clipPathUrl)) { // Create a combined path from all clip path elements Path combinedClipPath; @@ -179,16 +179,27 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent // Setup fill if (element.fillColor) { - g.setFillColor (*element.fillColor); + Color fillColor = *element.fillColor; + if (element.fillOpacity) + fillColor = fillColor.withMultipliedAlpha (*element.fillOpacity); + g.setFillColor (fillColor); isFillDefined = true; } else if (element.fillUrl) { - if (auto* gradient = getGradientById (*element.fillUrl)) + YUP_DBG ("Looking for gradient with ID: " << *element.fillUrl); + if (auto gradient = getGradientById (*element.fillUrl)) { - ColorGradient colorGradient = createColorGradientFromSVG (*gradient); + YUP_DBG ("Found gradient, resolving references..."); + auto resolvedGradient = resolveGradient (gradient); + ColorGradient colorGradient = createColorGradientFromSVG (*resolvedGradient, g.getTransform()); g.setFillColorGradient (colorGradient); isFillDefined = true; + YUP_DBG ("Applied gradient to fill"); + } + else + { + YUP_DBG ("Gradient not found for ID: " << *element.fillUrl); } } else if (hasParentFillEnabled) @@ -202,7 +213,12 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent if (element.path) { if (element.path->isClosed()) + { + // TODO: Apply fill-rule when Graphics class supports it + // if (element.fillRule) + // g.setFillRule (*element.fillRule == "evenodd" ? FillRule::EvenOdd : FillRule::NonZero); g.fillPath (*element.path); + } } else if (element.reference) { @@ -210,11 +226,24 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent { YUP_DBG ("Rendering use element - reference: " << *element.reference); YUP_DBG ("Use element transform: " << (element.transform ? element.transform->toString() : "none")); - YUP_DBG ("Referenced element transform: " << (refElement->transform ? refElement->transform->toString() : "none")); + YUP_DBG ("Referenced element local transform: " << (refElement->localTransform ? refElement->localTransform->toString() : "none")); YUP_DBG ("Graphics transform during use fill: " << g.getTransform().toString()); + // For elements, apply only the referenced element's local transform (if any) + const auto savedTransform = g.getTransform(); + if (refElement->localTransform) + g.setTransform (refElement->localTransform->followedBy (savedTransform)); + if (refElement->path->isClosed()) + { + // TODO: Apply fill-rule when Graphics class supports it + // if (element.fillRule) + // g.setFillRule (*element.fillRule == "evenodd" ? FillRule::EvenOdd : FillRule::NonZero); g.fillPath (*refElement->path); + } + + if (refElement->localTransform) + g.setTransform (savedTransform); } } else if (element.text && element.textPosition) @@ -223,16 +252,16 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent // Create StyledText for text rendering StyledText styledText; styledText.setText (*element.text); - + // Set font properties if (element.fontSize) styledText.setFontSize (*element.fontSize); else styledText.setFontSize (12.0f); - + if (element.fontFamily) styledText.setFontFamily (*element.fontFamily); - + // Set text alignment based on text-anchor if (element.textAnchor) { @@ -243,7 +272,7 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent else styledText.setHorizontalAlignment (StyledText::HorizontalAlignment::Left); } - + // Render text at specified position auto textBounds = styledText.getBounds(); g.drawStyledText (styledText, element.textPosition->getX(), element.textPosition->getY() - textBounds.getHeight()); @@ -264,14 +293,18 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent // Setup stroke if (element.strokeColor) { - g.setStrokeColor (*element.strokeColor); + Color strokeColor = *element.strokeColor; + if (element.strokeOpacity) + strokeColor = strokeColor.withMultipliedAlpha (*element.strokeOpacity); + g.setStrokeColor (strokeColor); isStrokeDefined = true; } else if (element.strokeUrl) { - if (auto* gradient = getGradientById (*element.strokeUrl)) + if (auto gradient = getGradientById (*element.strokeUrl)) { - ColorGradient colorGradient = createColorGradientFromSVG (*gradient); + auto resolvedGradient = resolveGradient (gradient); + ColorGradient colorGradient = createColorGradientFromSVG (*resolvedGradient, g.getTransform()); g.setStrokeColorGradient (colorGradient); isStrokeDefined = true; } @@ -318,7 +351,18 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent { YUP_DBG ("Stroking use element - reference: " << *element.reference); YUP_DBG ("Graphics transform during stroke: " << g.getTransform().toString()); + + // For elements, apply only the referenced element's local transform (if any) + const auto savedTransform = g.getTransform(); + if (refElement->localTransform) + { + g.setTransform (refElement->localTransform->followedBy (savedTransform)); + } + g.strokePath (*refElement->path); + + if (refElement->localTransform) + g.setTransform (savedTransform); } } } @@ -336,7 +380,7 @@ void Drawable::paintElement (Graphics& g, const Element& element, bool hasParent bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, AffineTransform currentTransform, Element* parent) { - auto e = std::make_shared(); + Element::Ptr e = new Element; bool isRootElement = element.hasTagName ("svg"); if (auto id = element.getStringAttribute ("id"); id.isNotEmpty()) @@ -561,7 +605,15 @@ bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, Affin } for (auto* child = element.getFirstChildElement(); child != nullptr; child = child->getNextElement()) - parseElement (*child, isRootElement, currentTransform, e.get()); + { + // Parse gradients and clip paths regardless of whether they're in or not + if (child->hasTagName ("linearGradient") || child->hasTagName ("radialGradient")) + parseGradient (*child); + else if (child->hasTagName ("clipPath")) + parseClipPath (*child); + else + parseElement (*child, isRootElement, currentTransform, e.get()); + } if (isRootElement) return true; @@ -589,8 +641,9 @@ void Drawable::parseStyle (const XmlElement& element, const AffineTransform& cur { if (fill != "none") { - if (fill.startsWith ("url(#")) - e.fillUrl = fill.substring (5, fill.length() - 1); + String gradientUrl = extractGradientUrl (fill); + if (gradientUrl.isNotEmpty()) + e.fillUrl = gradientUrl; else { e.fillColor = Color::fromString (fill); @@ -608,8 +661,9 @@ void Drawable::parseStyle (const XmlElement& element, const AffineTransform& cur { if (stroke != "none") { - if (stroke.startsWith ("url(#")) - e.strokeUrl = stroke.substring (5, stroke.length() - 1); + String gradientUrl = extractGradientUrl (stroke); + if (gradientUrl.isNotEmpty()) + e.strokeUrl = gradientUrl; else e.strokeColor = Color::fromString (stroke); } @@ -644,8 +698,12 @@ void Drawable::parseStyle (const XmlElement& element, const AffineTransform& cur e.opacity = opacity; String clipPath = element.getStringAttribute ("clip-path"); - if (clipPath.isNotEmpty() && clipPath.startsWith ("url(#")) - e.clipPathUrl = clipPath.substring (5, clipPath.length() - 1); + if (clipPath.isNotEmpty()) + { + String clipPathUrl = extractGradientUrl (clipPath); + if (clipPathUrl.isNotEmpty()) + e.clipPathUrl = clipPathUrl; + } // Parse stroke-dasharray String dashArray = element.getStringAttribute ("stroke-dasharray"); @@ -671,6 +729,21 @@ void Drawable::parseStyle (const XmlElement& element, const AffineTransform& cur String dashOffset = element.getStringAttribute ("stroke-dashoffset"); if (dashOffset.isNotEmpty()) e.strokeDashOffset = parseUnit (dashOffset); + + // Parse fill-opacity + float fillOpacity = element.getDoubleAttribute ("fill-opacity", -1.0); + if (fillOpacity >= 0.0 && fillOpacity <= 1.0) + e.fillOpacity = fillOpacity; + + // Parse stroke-opacity + float strokeOpacity = element.getDoubleAttribute ("stroke-opacity", -1.0); + if (strokeOpacity >= 0.0 && strokeOpacity <= 1.0) + e.strokeOpacity = strokeOpacity; + + // Parse fill-rule + String fillRule = element.getStringAttribute ("fill-rule"); + if (fillRule == "evenodd" || fillRule == "nonzero") + e.fillRule = fillRule; } //============================================================================== @@ -679,115 +752,115 @@ AffineTransform Drawable::parseTransform (const XmlElement& element, const Affin { AffineTransform result; - String transformString = element.getStringAttribute ("transform"); - if (transformString.isNotEmpty()) + if (auto transformString = element.getStringAttribute ("transform"); transformString.isNotEmpty()) { - auto data = transformString.getCharPointer(); - while (! data.isEmpty()) - { - // Skip whitespace - while (data.isWhitespace()) - ++data; + result = parseTransform (transformString); - if (data.isEmpty()) - break; + e.transform = result; + e.localTransform = result; // Store the local transform separately for use by elements - // Parse transform type - String type; - while (! data.isEmpty() && CharacterFunctions::isLetter (*data)) - { - type += *data; - ++data; - } + YUP_DBG ("Parsed element transform: " << result.toString()); + } - // Skip whitespace and the opening parenthesis - while (data.isWhitespace() || *data == '(') - ++data; + return currentTransform.followedBy (result); +} - // Parse parameters - Array params; - while (! data.isEmpty() && *data != ')') - { - if (*data == ',' || *data == ' ') - { - ++data; - continue; - } +//============================================================================== - String number; - while (! data.isEmpty() && (*data == '-' || *data == '.' || (*data >= '0' && *data <= '9'))) - { - number += *data; - ++data; - } +AffineTransform Drawable::parseTransform (const String& transformString) +{ + if (transformString.isEmpty()) + return AffineTransform::identity(); - if (! number.isEmpty()) - params.add (number.getFloatValue()); + AffineTransform result; + auto data = transformString.getCharPointer(); + + while (! data.isEmpty()) + { + // Skip whitespace + while (data.isWhitespace()) + ++data; - // Skip whitespace or commas - while (data.isWhitespace() || *data == ',') - ++data; - } + if (data.isEmpty()) + break; - // Skip the closing parenthesis - if (*data == ')') - ++data; + // Parse transform type + String type; + while (! data.isEmpty() && CharacterFunctions::isLetter (*data)) + { + type += *data; + ++data; + } - // Apply the parsed transform - if (type == "translate" && (params.size() == 1 || params.size() == 2)) - { - auto tx = params[0]; - auto ty = (params.size() == 2) ? params[1] : 0.0f; - YUP_DBG ("Applying translate(" << tx << ", " << ty << ")"); - result = result.translated (tx, ty); - } - else if (type == "scale" && (params.size() == 1 || params.size() == 2)) - { - auto sx = params[0]; - auto sy = (params.size() == 2) ? params[1] : params[0]; - YUP_DBG ("Applying scale(" << sx << ", " << sy << ")"); - result = result.scaled (sx, sy); - } - else if (type == "rotate" && (params.size() == 1 || params.size() == 3)) - { - if (params.size() == 1) - { - YUP_DBG ("Applying rotate(" << params[0] << ")"); - result = result.rotated (degreesToRadians (params[0])); - } - else - { - YUP_DBG ("Applying rotate(" << params[0] << ", " << params[1] << ", " << params[2] << ")"); - result = result.rotated (degreesToRadians (params[0]), params[1], params[2]); - } - } - else if (type == "skewX" && params.size() == 1) - { - auto angle = params[0]; - auto factor = std::tanf (degreesToRadians (angle)); - YUP_DBG ("Applying skewX(" << angle << ") -> factor=" << factor); - result = result.sheared (factor, 0.0f); - } - else if (type == "skewY" && params.size() == 1) + // Skip whitespace and the opening parenthesis + while (data.isWhitespace() || *data == '(') + ++data; + + // Parse parameters + Array params; + while (! data.isEmpty() && *data != ')') + { + if (*data == ',' || *data == ' ') { - auto angle = params[0]; - auto factor = std::tanf (degreesToRadians (angle)); - YUP_DBG ("Applying skewY(" << angle << ") -> factor=" << factor); - result = result.sheared (0.0f, factor); + ++data; + continue; } - else if (type == "matrix" && params.size() == 6) + + String number; + while (! data.isEmpty() && (*data == '-' || *data == '.' || *data == 'e' || (*data >= '0' && *data <= '9'))) { - YUP_DBG ("Applying matrix(" << params[0] << ", " << params[1] << ", " << params[2] << ", " << params[3] << ", " << params[4] << ", " << params[5] << ")"); - result = result.followedBy (AffineTransform ( - params[0], params[2], params[4], params[1], params[3], params[5])); + number += *data; + ++data; } + + if (! number.isEmpty()) + params.add (number.getFloatValue()); + + // Skip whitespace or commas + while (data.isWhitespace() || *data == ',') + ++data; } - e.transform = result; - YUP_DBG ("Parsed element transform: " << result.toString()); + // Skip the closing parenthesis + if (*data == ')') + ++data; + + // Apply the parsed transform + if (type == "translate" && (params.size() == 1 || params.size() == 2)) + { + const auto tx = params[0]; + const auto ty = (params.size() == 2) ? params[1] : 0.0f; + result = result.translated (tx, ty); + } + else if (type == "scale" && (params.size() == 1 || params.size() == 2)) + { + const auto sx = params[0]; + const auto sy = (params.size() == 2) ? params[1] : params[0]; + result = result.scaled (sx, sy); + } + else if (type == "rotate" && (params.size() == 1 || params.size() == 3)) + { + if (params.size() == 1) + result = result.rotated (degreesToRadians (params[0])); + else + result = result.rotated (degreesToRadians (params[0]), params[1], params[2]); + } + else if (type == "skewX" && params.size() == 1) + { + result = result.sheared (std::tanf (degreesToRadians (params[0])), 0.0f); + } + else if (type == "skewY" && params.size() == 1) + { + result = result.sheared (0.0f, std::tanf (degreesToRadians (params[0]))); + } + else if (type == "matrix" && params.size() == 6) + { + result = result.followedBy (AffineTransform ( + params[0], params[2], params[4], params[1], params[3], params[5])); + } } - return currentTransform.followedBy (result); + return result; } //============================================================================== @@ -823,29 +896,66 @@ void Drawable::paintDebugElement (Graphics& g, const Element& element) void Drawable::parseGradient (const XmlElement& element) { - Gradient gradient; - String id = element.getStringAttribute ("id"); if (id.isEmpty()) return; - gradient.id = id; + YUP_DBG ("Parsing gradient with ID: " << id); + + Gradient::Ptr gradient = new Gradient; + gradient->id = id; + + // Parse xlink:href reference + String href = element.getStringAttribute ("xlink:href"); + if (href.isNotEmpty() && href.startsWith ("#")) + { + gradient->href = href.substring (1); // Remove the # prefix + YUP_DBG ("Gradient references: " << gradient->href); + } if (element.hasTagName ("linearGradient")) { - gradient.type = Gradient::Linear; - gradient.start = { (float) element.getDoubleAttribute ("x1"), (float) element.getDoubleAttribute ("y1") }; - gradient.end = { (float) element.getDoubleAttribute ("x2"), (float) element.getDoubleAttribute ("y2") }; + gradient->type = Gradient::Linear; + gradient->start = { (float) element.getDoubleAttribute ("x1"), (float) element.getDoubleAttribute ("y1") }; + gradient->end = { (float) element.getDoubleAttribute ("x2"), (float) element.getDoubleAttribute ("y2") }; + + YUP_DBG ("Linear gradient - start: (" << gradient->start.getX() << ", " << gradient->start.getY() << + ") end: (" << gradient->end.getX() << ", " << gradient->end.getY() << ")"); } else if (element.hasTagName ("radialGradient")) { - gradient.type = Gradient::Radial; - gradient.center = { (float) element.getDoubleAttribute ("cx"), (float) element.getDoubleAttribute ("cy") }; - gradient.radius = element.getDoubleAttribute ("r"); + gradient->type = Gradient::Radial; + gradient->center = { (float) element.getDoubleAttribute ("cx"), (float) element.getDoubleAttribute ("cy") }; + gradient->radius = element.getDoubleAttribute ("r"); + + auto fx = element.getDoubleAttribute ("fx", gradient->center.getX()); + auto fy = element.getDoubleAttribute ("fy", gradient->center.getY()); + gradient->focal = { (float) fx, (float) fy }; + + YUP_DBG ("Radial gradient - center: (" << gradient->center.getX() << ", " << gradient->center.getY() << + ") radius: " << gradient->radius); + } + + // Parse gradientUnits attribute + String gradientUnits = element.getStringAttribute ("gradientUnits"); + if (gradientUnits == "userSpaceOnUse") + { + gradient->units = Gradient::UserSpaceOnUse; + YUP_DBG ("Gradient units: userSpaceOnUse"); + } + else + { + gradient->units = Gradient::ObjectBoundingBox; + YUP_DBG ("Gradient units: objectBoundingBox (default)"); + } - auto fx = element.getDoubleAttribute ("fx", gradient.center.getX()); - auto fy = element.getDoubleAttribute ("fy", gradient.center.getY()); - gradient.focal = { (float) fx, (float) fy }; + // Parse gradientTransform attribute + String gradientTransform = element.getStringAttribute ("gradientTransform"); + if (gradientTransform.isNotEmpty()) + { + YUP_DBG ("Parsing gradientTransform: " << gradientTransform); + gradient->transform = parseTransform (gradientTransform); + YUP_DBG ("Gradient transform: " << gradient->transform.toString()); } // Parse gradient stops @@ -856,38 +966,142 @@ void Drawable::parseGradient (const XmlElement& element) GradientStop stop; stop.offset = child->getDoubleAttribute ("offset"); + // First try to get stop-color from attributes String stopColor = child->getStringAttribute ("stop-color"); + float stopOpacity = child->getDoubleAttribute ("stop-opacity", 1.0); + + // If not found in attributes, parse from CSS style + if (stopColor.isEmpty()) + { + String styleAttr = child->getStringAttribute ("style"); + if (styleAttr.isNotEmpty()) + { + YUP_DBG ("Parsing CSS style for gradient stop: " << styleAttr); + + // Parse CSS-style stop-color + auto declarations = StringArray::fromTokens (styleAttr, ";", ""); + for (const auto& declaration : declarations) + { + auto colonPos = declaration.indexOf (":"); + if (colonPos > 0) + { + String property = declaration.substring (0, colonPos).trim(); + String value = declaration.substring (colonPos + 1).trim(); + + if (property == "stop-color") + { + stopColor = value; + YUP_DBG ("Found stop-color in CSS: " << stopColor); + } + else if (property == "stop-opacity") + { + stopOpacity = value.getFloatValue(); + YUP_DBG ("Found stop-opacity in CSS: " << stopOpacity); + } + } + } + } + } + if (stopColor.isNotEmpty()) + { + YUP_DBG ("Parsing color string: '" << stopColor << "' (length: " << stopColor.length() << ")"); stop.color = Color::fromString (stopColor); + YUP_DBG ("Gradient stop - offset: " << stop.offset << " color: " << stopColor << + " parsed: " << stop.color.toString()); + } - stop.opacity = child->getDoubleAttribute ("stop-opacity", 1.0); + stop.opacity = stopOpacity; - gradient.stops.push_back (stop); + gradient->stops.push_back (stop); } } + YUP_DBG ("Gradient parsed with " << gradient->stops.size() << " stops"); + gradients.push_back (gradient); - gradientsById.set (id, &gradients.back()); + gradientsById.set (id, gradient); } //============================================================================== -Drawable::Gradient* Drawable::getGradientById (const String& id) +Drawable::Gradient::Ptr Drawable::getGradientById (const String& id) { return gradientsById[id]; } //============================================================================== -ColorGradient Drawable::createColorGradientFromSVG (const Gradient& gradient) +Drawable::Gradient::Ptr Drawable::resolveGradient (Gradient::Ptr gradient) { + if (gradient == nullptr || gradient->href.isEmpty()) + return gradient; + + auto referencedGradient = getGradientById (gradient->href); + if (referencedGradient == nullptr) + { + YUP_DBG ("Referenced gradient not found: " << gradient->href); + return gradient; + } + + // Recursively resolve the referenced gradient first + referencedGradient = resolveGradient (referencedGradient); + + // Create a new gradient that inherits from the referenced gradient + Gradient::Ptr resolvedGradient = new Gradient; + + // Copy properties from referenced gradient + resolvedGradient->type = referencedGradient->type; + resolvedGradient->id = gradient->id; // Keep the original ID + resolvedGradient->units = referencedGradient->units; + resolvedGradient->start = referencedGradient->start; + resolvedGradient->end = referencedGradient->end; + resolvedGradient->center = referencedGradient->center; + resolvedGradient->radius = referencedGradient->radius; + resolvedGradient->focal = referencedGradient->focal; + resolvedGradient->transform = referencedGradient->transform; + resolvedGradient->stops = referencedGradient->stops; + + // Override with properties from the current gradient (if specified) + if (gradient->start.getX() != 0.0f || gradient->start.getY() != 0.0f) + resolvedGradient->start = gradient->start; + if (gradient->end.getX() != 0.0f || gradient->end.getY() != 0.0f) + resolvedGradient->end = gradient->end; + if (gradient->center.getX() != 0.0f || gradient->center.getY() != 0.0f) + resolvedGradient->center = gradient->center; + if (gradient->radius != 0.0f) + resolvedGradient->radius = gradient->radius; + if (! gradient->transform.isIdentity()) + resolvedGradient->transform = gradient->transform; + if (gradient->units != Gradient::ObjectBoundingBox) // Only override if explicitly set + resolvedGradient->units = gradient->units; + if (! gradient->stops.empty()) // Use local stops if defined + resolvedGradient->stops = gradient->stops; + + YUP_DBG ("Resolved gradient " << gradient->id << " from reference " << gradient->href); + return resolvedGradient; +} + +//============================================================================== + +ColorGradient Drawable::createColorGradientFromSVG (const Gradient& gradient, const AffineTransform& currentTransform) +{ + YUP_DBG ("Creating ColorGradient from SVG gradient ID: " << gradient.id << + " type: " << (gradient.type == Gradient::Linear ? "Linear" : "Radial") << + " units: " << (gradient.units == Gradient::UserSpaceOnUse ? "userSpaceOnUse" : "objectBoundingBox") << + " currentTransform: " << currentTransform.toString()); + if (gradient.stops.empty()) + { + YUP_DBG ("No stops in gradient, returning empty"); return ColorGradient(); + } if (gradient.stops.size() == 1) { const auto& stop = gradient.stops[0]; Color color = stop.color.withAlpha (stop.opacity); + YUP_DBG ("Single stop gradient with color: " << color.toString()); return ColorGradient (color, 0, 0, color, 1, 0, gradient.type == Gradient::Linear ? ColorGradient::Linear : ColorGradient::Radial); } @@ -902,16 +1116,52 @@ ColorGradient Drawable::createColorGradientFromSVG (const Gradient& gradient) // For linear gradients, interpolate position based on offset float x = gradient.start.getX() + stop.offset * (gradient.end.getX() - gradient.start.getX()); float y = gradient.start.getY() + stop.offset * (gradient.end.getY() - gradient.start.getY()); + + // Apply transformations: first gradient transform, then current viewport transform + AffineTransform combinedTransform = gradient.transform; + if (gradient.units == Gradient::UserSpaceOnUse && ! currentTransform.isIdentity()) + { + // For userSpaceOnUse, apply the current graphics transform to make gradient scale with viewport + combinedTransform = combinedTransform.followedBy (currentTransform); + } + + if (! combinedTransform.isIdentity()) + { + float originalX = x; + float originalY = y; + + combinedTransform.transformPoint (x, y); + YUP_DBG ("Transformed gradient stop: offset=" << stop.offset << " original=(" << originalX << "," << originalY << + ") transformed=(" << x << "," << y << ")"); + } + colorStops.emplace_back (color, x, y, stop.offset); + YUP_DBG ("Linear gradient stop: offset=" << stop.offset << " pos=(" << x << "," << y << ") color=" << color.toString()); } else { // For radial gradients, use center as base position - colorStops.emplace_back (color, gradient.center.getX(), gradient.center.getY(), stop.offset); + float x = gradient.center.getX(); + float y = gradient.center.getY(); + + // Apply transformations: first gradient transform, then current viewport transform + AffineTransform combinedTransform = gradient.transform; + if (gradient.units == Gradient::UserSpaceOnUse && ! currentTransform.isIdentity()) + { + // For userSpaceOnUse, apply the current graphics transform to make gradient scale with viewport + combinedTransform = combinedTransform.followedBy (currentTransform); + } + + if (! combinedTransform.isIdentity()) + combinedTransform.transformPoint (x, y); + + colorStops.emplace_back (color, x, y, stop.offset); + YUP_DBG ("Radial gradient stop: offset=" << stop.offset << " color=" << color.toString()); } } ColorGradient::Type type = (gradient.type == Gradient::Linear) ? ColorGradient::Linear : ColorGradient::Radial; + YUP_DBG ("Created ColorGradient with " << colorStops.size() << " stops"); return ColorGradient (type, colorStops); } @@ -919,18 +1169,17 @@ ColorGradient Drawable::createColorGradientFromSVG (const Gradient& gradient) void Drawable::parseClipPath (const XmlElement& element) { - ClipPath clipPath; - String id = element.getStringAttribute ("id"); if (id.isEmpty()) return; - clipPath.id = id; + ClipPath::Ptr clipPath = new ClipPath; + clipPath->id = id; // Parse child elements that make up the clipping path for (auto* child = element.getFirstChildElement(); child != nullptr; child = child->getNextElement()) { - auto clipElement = std::make_shared(); + Element::Ptr clipElement = new Element; if (child->hasTagName ("path")) { @@ -962,16 +1211,16 @@ void Drawable::parseClipPath (const XmlElement& element) } if (clipElement->path) - clipPath.elements.push_back (clipElement); + clipPath->elements.push_back (clipElement); } clipPaths.push_back (clipPath); - clipPathsById.set (id, &clipPaths.back()); + clipPathsById.set (id, clipPath); } //============================================================================== -Drawable::ClipPath* Drawable::getClipPathById (const String& id) +Drawable::ClipPath::Ptr Drawable::getClipPathById (const String& id) { return clipPathsById[id]; } @@ -995,8 +1244,9 @@ void Drawable::parseCSSStyle (const String& styleString, Element& e) { if (value != "none") { - if (value.startsWith ("url(#")) - e.fillUrl = value.substring (5, value.length() - 1); + String gradientUrl = extractGradientUrl (value); + if (gradientUrl.isNotEmpty()) + e.fillUrl = gradientUrl; else e.fillColor = Color::fromString (value); } @@ -1009,8 +1259,9 @@ void Drawable::parseCSSStyle (const String& styleString, Element& e) { if (value != "none") { - if (value.startsWith ("url(#")) - e.strokeUrl = value.substring (5, value.length() - 1); + String gradientUrl = extractGradientUrl (value); + if (gradientUrl.isNotEmpty()) + e.strokeUrl = gradientUrl; else e.strokeColor = Color::fromString (value); } @@ -1065,8 +1316,9 @@ void Drawable::parseCSSStyle (const String& styleString, Element& e) } else if (property == "clip-path") { - if (value.startsWith ("url(#")) - e.clipPathUrl = value.substring (5, value.length() - 1); + String clipPathUrl = extractGradientUrl (value); + if (clipPathUrl.isNotEmpty()) + e.clipPathUrl = clipPathUrl; } else if (property == "stroke-dasharray") { @@ -1092,6 +1344,23 @@ void Drawable::parseCSSStyle (const String& styleString, Element& e) { e.strokeDashOffset = parseUnit (value); } + else if (property == "fill-opacity") + { + float opacity = value.getFloatValue(); + if (opacity >= 0.0f && opacity <= 1.0f) + e.fillOpacity = opacity; + } + else if (property == "stroke-opacity") + { + float opacity = value.getFloatValue(); + if (opacity >= 0.0f && opacity <= 1.0f) + e.strokeOpacity = opacity; + } + else if (property == "fill-rule") + { + if (value == "evenodd" || value == "nonzero") + e.fillRule = value; + } } } } @@ -1312,4 +1581,27 @@ Justification Drawable::parseAspectRatioAlignment (const String& preserveAspectR return result; } +//============================================================================== + +String Drawable::extractGradientUrl (const String& value) +{ + if (! value.contains ("url(#")) + return String(); + + // Find the start of the URL + int urlStart = value.indexOf ("url(#"); + if (urlStart == -1) + return String(); + + // Find the end of the URL (first closing parenthesis after the URL start) + int urlEnd = value.indexOf (urlStart, ")"); + if (urlEnd == -1) + return String(); + + // Extract the ID part (between "url(#" and ")") + String url = value.substring (urlStart + 5, urlEnd); // +5 to skip "url(#" + YUP_DBG ("Extracted gradient URL: '" << url << "' from: '" << value << "'"); + return url; +} + } // namespace yup diff --git a/modules/yup_graphics/drawables/yup_Drawable.h b/modules/yup_graphics/drawables/yup_Drawable.h index 2c7196c69..34066821c 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.h +++ b/modules/yup_graphics/drawables/yup_Drawable.h @@ -38,7 +38,7 @@ class YUP_API Drawable //============================================================================== /** Gets the bounds of the drawable content. - + @return The bounding rectangle of the drawable's content. */ Rectangle getBounds() const; @@ -59,21 +59,27 @@ class YUP_API Drawable Justification justification = Justification::center); private: - struct Element + struct Element : public ReferenceCountedObject { + using Ptr = ReferenceCountedObjectPtr; + std::optional id; std::optional transform; + std::optional localTransform; // Transform from the element itself (not accumulated) std::optional path; std::optional reference; std::optional fillColor; std::optional strokeColor; + std::optional fillOpacity; + std::optional strokeOpacity; std::optional strokeWidth; std::optional strokeJoin; std::optional strokeCap; std::optional> strokeDashArray; std::optional strokeDashOffset; + std::optional fillRule; // "evenodd" or "nonzero" bool noFill = false; bool noStroke = false; @@ -97,7 +103,7 @@ class YUP_API Drawable // Clipping properties std::optional clipPathUrl; - std::vector> children; + std::vector children; }; struct GradientStop @@ -107,16 +113,26 @@ class YUP_API Drawable float opacity = 1.0f; }; - struct Gradient + struct Gradient : public ReferenceCountedObject { + using Ptr = ReferenceCountedObjectPtr; + enum Type { Linear, Radial }; + enum Units + { + UserSpaceOnUse, + ObjectBoundingBox + }; + Type type; String id; + Units units = ObjectBoundingBox; // Default per SVG spec + String href; // xlink:href reference to another gradient // Linear gradient properties Point start; @@ -131,10 +147,12 @@ class YUP_API Drawable AffineTransform transform; }; - struct ClipPath + struct ClipPath : public ReferenceCountedObject { + using Ptr = ReferenceCountedObjectPtr; + String id; - std::vector> elements; + std::vector elements; }; void paintElement (Graphics& g, const Element& element, bool hasParentFillEnabled, bool hasParentStrokeEnabled); @@ -143,12 +161,15 @@ class YUP_API Drawable void parseStyle (const XmlElement& element, const AffineTransform& currentTransform, Element& e); AffineTransform parseTransform (const XmlElement& element, const AffineTransform& currentTransform, Element& e); void parseGradient (const XmlElement& element); - Gradient* getGradientById (const String& id); - ColorGradient createColorGradientFromSVG (const Gradient& gradient); + Gradient::Ptr getGradientById (const String& id); + Gradient::Ptr resolveGradient (Gradient::Ptr gradient); + ColorGradient createColorGradientFromSVG (const Gradient& gradient, const AffineTransform& currentTransform = AffineTransform::identity()); void parseClipPath (const XmlElement& element); - ClipPath* getClipPathById (const String& id); + ClipPath::Ptr getClipPathById (const String& id); void parseCSSStyle (const String& styleString, Element& e); float parseUnit (const String& value, float defaultValue = 0.0f, float fontSize = 12.0f, float viewportSize = 100.0f); + AffineTransform parseTransform (const String& transformString); + String extractGradientUrl (const String& value); // SVG preserveAspectRatio parsing Fitting parsePreserveAspectRatio (const String& preserveAspectRatio); @@ -162,12 +183,12 @@ class YUP_API Drawable Size size; Rectangle bounds; AffineTransform transform; - std::vector> elements; - HashMap> elementsById; - std::vector gradients; - HashMap gradientsById; - std::vector clipPaths; - HashMap clipPathsById; + std::vector elements; + HashMap elementsById; + std::vector gradients; + HashMap gradientsById; + std::vector clipPaths; + HashMap clipPathsById; }; } // namespace yup From 1dc4dbfdbe3e9b9d51a707455eb6fa5992838a76 Mon Sep 17 00:00:00 2001 From: Yup Bot Date: Wed, 2 Jul 2025 22:39:55 +0000 Subject: [PATCH 23/25] Code formatting --- .../yup_graphics/drawables/yup_Drawable.cpp | 49 ++++++++----------- modules/yup_graphics/drawables/yup_Drawable.h | 2 +- 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/modules/yup_graphics/drawables/yup_Drawable.cpp b/modules/yup_graphics/drawables/yup_Drawable.cpp index 60e669d20..cb9b08b85 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.cpp +++ b/modules/yup_graphics/drawables/yup_Drawable.cpp @@ -729,17 +729,17 @@ void Drawable::parseStyle (const XmlElement& element, const AffineTransform& cur String dashOffset = element.getStringAttribute ("stroke-dashoffset"); if (dashOffset.isNotEmpty()) e.strokeDashOffset = parseUnit (dashOffset); - + // Parse fill-opacity float fillOpacity = element.getDoubleAttribute ("fill-opacity", -1.0); if (fillOpacity >= 0.0 && fillOpacity <= 1.0) e.fillOpacity = fillOpacity; - + // Parse stroke-opacity float strokeOpacity = element.getDoubleAttribute ("stroke-opacity", -1.0); if (strokeOpacity >= 0.0 && strokeOpacity <= 1.0) e.strokeOpacity = strokeOpacity; - + // Parse fill-rule String fillRule = element.getStringAttribute ("fill-rule"); if (fillRule == "evenodd" || fillRule == "nonzero") @@ -774,7 +774,7 @@ AffineTransform Drawable::parseTransform (const String& transformString) AffineTransform result; auto data = transformString.getCharPointer(); - + while (! data.isEmpty()) { // Skip whitespace @@ -918,9 +918,8 @@ void Drawable::parseGradient (const XmlElement& element) gradient->type = Gradient::Linear; gradient->start = { (float) element.getDoubleAttribute ("x1"), (float) element.getDoubleAttribute ("y1") }; gradient->end = { (float) element.getDoubleAttribute ("x2"), (float) element.getDoubleAttribute ("y2") }; - - YUP_DBG ("Linear gradient - start: (" << gradient->start.getX() << ", " << gradient->start.getY() << - ") end: (" << gradient->end.getX() << ", " << gradient->end.getY() << ")"); + + YUP_DBG ("Linear gradient - start: (" << gradient->start.getX() << ", " << gradient->start.getY() << ") end: (" << gradient->end.getX() << ", " << gradient->end.getY() << ")"); } else if (element.hasTagName ("radialGradient")) { @@ -931,9 +930,8 @@ void Drawable::parseGradient (const XmlElement& element) auto fx = element.getDoubleAttribute ("fx", gradient->center.getX()); auto fy = element.getDoubleAttribute ("fy", gradient->center.getY()); gradient->focal = { (float) fx, (float) fy }; - - YUP_DBG ("Radial gradient - center: (" << gradient->center.getX() << ", " << gradient->center.getY() << - ") radius: " << gradient->radius); + + YUP_DBG ("Radial gradient - center: (" << gradient->center.getX() << ", " << gradient->center.getY() << ") radius: " << gradient->radius); } // Parse gradientUnits attribute @@ -969,7 +967,7 @@ void Drawable::parseGradient (const XmlElement& element) // First try to get stop-color from attributes String stopColor = child->getStringAttribute ("stop-color"); float stopOpacity = child->getDoubleAttribute ("stop-opacity", 1.0); - + // If not found in attributes, parse from CSS style if (stopColor.isEmpty()) { @@ -977,7 +975,7 @@ void Drawable::parseGradient (const XmlElement& element) if (styleAttr.isNotEmpty()) { YUP_DBG ("Parsing CSS style for gradient stop: " << styleAttr); - + // Parse CSS-style stop-color auto declarations = StringArray::fromTokens (styleAttr, ";", ""); for (const auto& declaration : declarations) @@ -987,7 +985,7 @@ void Drawable::parseGradient (const XmlElement& element) { String property = declaration.substring (0, colonPos).trim(); String value = declaration.substring (colonPos + 1).trim(); - + if (property == "stop-color") { stopColor = value; @@ -1007,8 +1005,7 @@ void Drawable::parseGradient (const XmlElement& element) { YUP_DBG ("Parsing color string: '" << stopColor << "' (length: " << stopColor.length() << ")"); stop.color = Color::fromString (stopColor); - YUP_DBG ("Gradient stop - offset: " << stop.offset << " color: " << stopColor << - " parsed: " << stop.color.toString()); + YUP_DBG ("Gradient stop - offset: " << stop.offset << " color: " << stopColor << " parsed: " << stop.color.toString()); } stop.opacity = stopOpacity; @@ -1049,7 +1046,7 @@ Drawable::Gradient::Ptr Drawable::resolveGradient (Gradient::Ptr gradient) // Create a new gradient that inherits from the referenced gradient Gradient::Ptr resolvedGradient = new Gradient; - + // Copy properties from referenced gradient resolvedGradient->type = referencedGradient->type; resolvedGradient->id = gradient->id; // Keep the original ID @@ -1086,11 +1083,8 @@ Drawable::Gradient::Ptr Drawable::resolveGradient (Gradient::Ptr gradient) ColorGradient Drawable::createColorGradientFromSVG (const Gradient& gradient, const AffineTransform& currentTransform) { - YUP_DBG ("Creating ColorGradient from SVG gradient ID: " << gradient.id << - " type: " << (gradient.type == Gradient::Linear ? "Linear" : "Radial") << - " units: " << (gradient.units == Gradient::UserSpaceOnUse ? "userSpaceOnUse" : "objectBoundingBox") << - " currentTransform: " << currentTransform.toString()); - + YUP_DBG ("Creating ColorGradient from SVG gradient ID: " << gradient.id << " type: " << (gradient.type == Gradient::Linear ? "Linear" : "Radial") << " units: " << (gradient.units == Gradient::UserSpaceOnUse ? "userSpaceOnUse" : "objectBoundingBox") << " currentTransform: " << currentTransform.toString()); + if (gradient.stops.empty()) { YUP_DBG ("No stops in gradient, returning empty"); @@ -1116,7 +1110,7 @@ ColorGradient Drawable::createColorGradientFromSVG (const Gradient& gradient, co // For linear gradients, interpolate position based on offset float x = gradient.start.getX() + stop.offset * (gradient.end.getX() - gradient.start.getX()); float y = gradient.start.getY() + stop.offset * (gradient.end.getY() - gradient.start.getY()); - + // Apply transformations: first gradient transform, then current viewport transform AffineTransform combinedTransform = gradient.transform; if (gradient.units == Gradient::UserSpaceOnUse && ! currentTransform.isIdentity()) @@ -1124,17 +1118,16 @@ ColorGradient Drawable::createColorGradientFromSVG (const Gradient& gradient, co // For userSpaceOnUse, apply the current graphics transform to make gradient scale with viewport combinedTransform = combinedTransform.followedBy (currentTransform); } - + if (! combinedTransform.isIdentity()) { float originalX = x; float originalY = y; combinedTransform.transformPoint (x, y); - YUP_DBG ("Transformed gradient stop: offset=" << stop.offset << " original=(" << originalX << "," << originalY << - ") transformed=(" << x << "," << y << ")"); + YUP_DBG ("Transformed gradient stop: offset=" << stop.offset << " original=(" << originalX << "," << originalY << ") transformed=(" << x << "," << y << ")"); } - + colorStops.emplace_back (color, x, y, stop.offset); YUP_DBG ("Linear gradient stop: offset=" << stop.offset << " pos=(" << x << "," << y << ") color=" << color.toString()); } @@ -1143,7 +1136,7 @@ ColorGradient Drawable::createColorGradientFromSVG (const Gradient& gradient, co // For radial gradients, use center as base position float x = gradient.center.getX(); float y = gradient.center.getY(); - + // Apply transformations: first gradient transform, then current viewport transform AffineTransform combinedTransform = gradient.transform; if (gradient.units == Gradient::UserSpaceOnUse && ! currentTransform.isIdentity()) @@ -1151,7 +1144,7 @@ ColorGradient Drawable::createColorGradientFromSVG (const Gradient& gradient, co // For userSpaceOnUse, apply the current graphics transform to make gradient scale with viewport combinedTransform = combinedTransform.followedBy (currentTransform); } - + if (! combinedTransform.isIdentity()) combinedTransform.transformPoint (x, y); diff --git a/modules/yup_graphics/drawables/yup_Drawable.h b/modules/yup_graphics/drawables/yup_Drawable.h index 34066821c..1adde8a18 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.h +++ b/modules/yup_graphics/drawables/yup_Drawable.h @@ -132,7 +132,7 @@ class YUP_API Drawable Type type; String id; Units units = ObjectBoundingBox; // Default per SVG spec - String href; // xlink:href reference to another gradient + String href; // xlink:href reference to another gradient // Linear gradient properties Point start; From 5f33d30eef8bccfc1314745abab96d666379980e Mon Sep 17 00:00:00 2001 From: kunitoki Date: Thu, 3 Jul 2025 08:08:43 +0200 Subject: [PATCH 24/25] More doxygen --- modules/yup_graphics/drawables/yup_Drawable.h | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/modules/yup_graphics/drawables/yup_Drawable.h b/modules/yup_graphics/drawables/yup_Drawable.h index 1adde8a18..8700cbfc5 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.h +++ b/modules/yup_graphics/drawables/yup_Drawable.h @@ -23,17 +23,29 @@ namespace yup { //============================================================================== +/** A class that can parse and paint SVG files. + The Drawable class is used to parse and paint SVG files. + It can be used to paint SVG files to a Graphics context. +*/ class YUP_API Drawable { public: //============================================================================== + /** Constructor. */ Drawable(); //============================================================================== + /** Parses an SVG file. + + @param svgFile The SVG file to parse. + + @return True if the SVG file was parsed successfully, false otherwise. + */ bool parseSVG (const File& svgFile); //============================================================================== + /** Clears the drawable. */ void clear(); //============================================================================== @@ -44,8 +56,13 @@ class YUP_API Drawable Rectangle getBounds() const; //============================================================================== + /** Paints the drawable to a Graphics context. + + @param g The graphics context to paint to. + */ void paint (Graphics& g); + //============================================================================== /** Paints the drawable with the specified fitting and justification. @param g The graphics context to paint to. From d5a06b2565b972c16140c4419adacc932f48a763 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sat, 5 Jul 2025 09:07:26 +0200 Subject: [PATCH 25/25] Update yup_Drawable.cpp --- modules/yup_graphics/drawables/yup_Drawable.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/yup_graphics/drawables/yup_Drawable.cpp b/modules/yup_graphics/drawables/yup_Drawable.cpp index cb9b08b85..c55fc9d2b 100644 --- a/modules/yup_graphics/drawables/yup_Drawable.cpp +++ b/modules/yup_graphics/drawables/yup_Drawable.cpp @@ -2,7 +2,7 @@ ============================================================================== This file is part of the YUP library. - Copyright (c) 2024 - kunitoki@gmail.com + Copyright (c) 2025 - kunitoki@gmail.com YUP is an open source library subject to open-source licensing. @@ -847,11 +847,11 @@ AffineTransform Drawable::parseTransform (const String& transformString) } else if (type == "skewX" && params.size() == 1) { - result = result.sheared (std::tanf (degreesToRadians (params[0])), 0.0f); + result = result.sheared (tanf (degreesToRadians (params[0])), 0.0f); } else if (type == "skewY" && params.size() == 1) { - result = result.sheared (0.0f, std::tanf (degreesToRadians (params[0]))); + result = result.sheared (0.0f, tanf (degreesToRadians (params[0]))); } else if (type == "matrix" && params.size() == 6) {