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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/graphics/source/examples/Svg.h b/examples/graphics/source/examples/Svg.h new file mode 100644 index 000000000..08c76f66d --- /dev/null +++ b/examples/graphics/source/examples/Svg.h @@ -0,0 +1,99 @@ +/* + ============================================================================== + + 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, getLocalBounds()); + } + +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; + + YUP_DBG ("Showing " << svgFiles[currentSvgFileIndex].getFullPathName()); + + drawable.clear(); + drawable.parseSVG (svgFiles[currentSvgFileIndex]); + + repaint(); + } + + yup::Drawable drawable; + Array svgFiles; + int currentSvgFileIndex = 0; +}; + +} // namespace yup 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/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.cpp b/modules/yup_graphics/drawables/yup_Drawable.cpp new file mode 100644 index 000000000..c55fc9d2b --- /dev/null +++ b/modules/yup_graphics/drawables/yup_Drawable.cpp @@ -0,0 +1,1600 @@ +/* + ============================================================================== + + 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 +{ + +//============================================================================== + +Drawable::Drawable() +{ +} + +//============================================================================== + +bool Drawable::parseSVG (const File& svgFile) +{ + clear(); + + 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.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()); + + auto result = parseElement (*svgRoot, true, {}); + + if (result) + bounds = calculateBounds(); + + return result; +} + +//============================================================================== + +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(); + gradients.clear(); + gradientsById.clear(); + clipPaths.clear(); + clipPathsById.clear(); +} + +//============================================================================== + +Rectangle Drawable::getBounds() const +{ + return bounds; +} + +//============================================================================== + +void Drawable::paint (Graphics& g) +{ + 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 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); + + 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; + + 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 + // 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)); + + // 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) + { + Color fillColor = *element.fillColor; + if (element.fillOpacity) + fillColor = fillColor.withMultipliedAlpha (*element.fillOpacity); + g.setFillColor (fillColor); + isFillDefined = true; + } + else if (element.fillUrl) + { + YUP_DBG ("Looking for gradient with ID: " << *element.fillUrl); + if (auto gradient = getGradientById (*element.fillUrl)) + { + 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) + { + // Inherit parent fill - don't change graphics state, just mark as defined + isFillDefined = true; + } + + if (isFillDefined && ! element.noFill) + { + 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) + { + 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 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) + { + /* + // 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 + if (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)) + { + auto resolvedGradient = resolveGradient (gradient); + ColorGradient colorGradient = createColorGradientFromSVG (*resolvedGradient, g.getTransform()); + g.setStrokeColorGradient (colorGradient); + 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); + + if (element.strokeCap) + g.setStrokeCap (*element.strokeCap); + + 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) + { + if (element.path) + { + g.strokePath (*element.path); + } + 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()); + + // 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); + } + } + } + + 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); +} + +//============================================================================== + +bool Drawable::parseElement (const XmlElement& element, bool parentIsRoot, AffineTransform currentTransform, Element* parent) +{ + Element::Ptr e = new Element; + 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); + + // 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")) + { + 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); + } + 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()) + { + // 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; + + 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) +{ + // 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") + { + String gradientUrl = extractGradientUrl (fill); + if (gradientUrl.isNotEmpty()) + e.fillUrl = gradientUrl; + else + { + e.fillColor = Color::fromString (fill); + YUP_DBG ("Parsed fill color: " << fill << " -> " << e.fillColor->toString()); + } + } + else + { + e.noFill = true; + } + } + + String stroke = element.getStringAttribute ("stroke"); + if (stroke.isNotEmpty()) + { + if (stroke != "none") + { + String gradientUrl = extractGradientUrl (stroke); + if (gradientUrl.isNotEmpty()) + e.strokeUrl = gradientUrl; + else + 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) + e.strokeWidth = strokeWidth; + + 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()) + { + String clipPathUrl = extractGradientUrl (clipPath); + if (clipPathUrl.isNotEmpty()) + e.clipPathUrl = clipPathUrl; + } + + // 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); + + // 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; +} + +//============================================================================== + +AffineTransform Drawable::parseTransform (const XmlElement& element, const AffineTransform& currentTransform, Element& e) +{ + AffineTransform result; + + if (auto transformString = element.getStringAttribute ("transform"); transformString.isNotEmpty()) + { + result = parseTransform (transformString); + + e.transform = result; + e.localTransform = result; // Store the local transform separately for use by elements + + YUP_DBG ("Parsed element transform: " << result.toString()); + } + + return currentTransform.followedBy (result); +} + +//============================================================================== + +AffineTransform Drawable::parseTransform (const String& transformString) +{ + if (transformString.isEmpty()) + return AffineTransform::identity(); + + AffineTransform result; + 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 == 'e' || (*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)) + { + 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 (tanf (degreesToRadians (params[0])), 0.0f); + } + else if (type == "skewY" && params.size() == 1) + { + result = result.sheared (0.0f, 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 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); + } + } +} + +//============================================================================== + +void Drawable::parseGradient (const XmlElement& element) +{ + String id = element.getStringAttribute ("id"); + if (id.isEmpty()) + return; + + 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") }; + + 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"); + + 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)"); + } + + // 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 + for (auto* child = element.getFirstChildElement(); child != nullptr; child = child->getNextElement()) + { + if (child->hasTagName ("stop")) + { + 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 = stopOpacity; + + gradient->stops.push_back (stop); + } + } + + YUP_DBG ("Gradient parsed with " << gradient->stops.size() << " stops"); + + gradients.push_back (gradient); + gradientsById.set (id, gradient); +} + +//============================================================================== + +Drawable::Gradient::Ptr Drawable::getGradientById (const String& id) +{ + return gradientsById[id]; +} + +//============================================================================== + +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); + } + + // 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()); + + // 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 + 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); +} + +//============================================================================== + +void Drawable::parseClipPath (const XmlElement& element) +{ + String id = element.getStringAttribute ("id"); + if (id.isEmpty()) + return; + + 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()) + { + Element::Ptr clipElement = new Element; + + 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, clipPath); +} + +//============================================================================== + +Drawable::ClipPath::Ptr 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") + { + String gradientUrl = extractGradientUrl (value); + if (gradientUrl.isNotEmpty()) + e.fillUrl = gradientUrl; + else + e.fillColor = Color::fromString (value); + } + else + { + e.noFill = true; + } + } + else if (property == "stroke") + { + if (value != "none") + { + String gradientUrl = extractGradientUrl (value); + if (gradientUrl.isNotEmpty()) + e.strokeUrl = gradientUrl; + 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") + { + String clipPathUrl = extractGradientUrl (value); + if (clipPathUrl.isNotEmpty()) + e.clipPathUrl = clipPathUrl; + } + 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); + } + 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; + } + } + } +} + +//============================================================================== + +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 +} + +//============================================================================== + +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); +} + +//============================================================================== + +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; +} + +//============================================================================== + +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 new file mode 100644 index 000000000..8700cbfc5 --- /dev/null +++ b/modules/yup_graphics/drawables/yup_Drawable.h @@ -0,0 +1,211 @@ +/* + ============================================================================== + + 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 +{ + +//============================================================================== +/** 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(); + + //============================================================================== + /** Gets the bounds of the drawable content. + + @return The bounding rectangle of the drawable's content. + */ + 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. + @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 : 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; + + 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 : 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; + Point end; + + // Radial gradient properties + Point center; + float radius = 0.0f; + Point focal; + + std::vector stops; + AffineTransform transform; + }; + + struct ClipPath : public ReferenceCountedObject + { + using Ptr = ReferenceCountedObjectPtr; + + 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::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::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); + 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; + + Rectangle viewBox; + Size size; + Rectangle bounds; + AffineTransform transform; + std::vector elements; + HashMap elementsById; + std::vector gradients; + HashMap gradientsById; + std::vector clipPaths; + HashMap clipPathsById; +}; + +} // namespace yup 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/graphics/yup_ColorGradient.h b/modules/yup_graphics/graphics/yup_ColorGradient.h index 25bf0fef1..0556669a3 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,59 @@ 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 +265,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 +300,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 +313,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..df747a490 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(); - float stops[] = { gradient.getStartDelta(), gradient.getFinishDelta() }; - transform.transformPoints (stops[0], stops[1]); + 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); + } + + // 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()); } } @@ -370,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_Fitting.h b/modules/yup_graphics/layout/yup_Fitting.h new file mode 100644 index 000000000..d231da085 --- /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/layout/yup_Justification.h b/modules/yup_graphics/layout/yup_Justification.h index a88821993..055ab8c5c 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 f482a31f5..9445a9bcf 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()) { @@ -109,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); - 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)); + 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 (c1, c2, end); return *this; } @@ -387,7 +570,25 @@ 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; @@ -419,7 +620,8 @@ 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; @@ -453,7 +655,8 @@ 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; @@ -639,6 +842,67 @@ 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(); +} + +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) + { + if (segment.verb == PathVerb::Close) + return true; + } + + return false; +} + +//============================================================================== + Path& Path::appendPath (const Path& other) { path->addRenderPath (other.getRenderPath(), rive::Mat2D()); @@ -664,12 +928,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 +952,7 @@ Path Path::transformed (const AffineTransform& t) const } //============================================================================== + Rectangle Path::getBounds() const { const auto& aabb = path->getBounds(); @@ -698,6 +965,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 +996,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 +1099,10 @@ String Path::toString() const } //============================================================================== + namespace { + bool isControlMarker (String::CharPointerType data) { return ! data.isEmpty() && String ("MmLlHhVvQqCcSsZz").containsChar (*data); @@ -920,6 +1214,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) @@ -931,10 +1226,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); } @@ -1001,7 +1306,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; @@ -1018,11 +1323,15 @@ 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; + // Update the last control point for smooth curves + lastQuadX = x1; + lastQuadY = y1; + skipWhitespace (data); } } @@ -1056,7 +1365,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; @@ -1070,7 +1379,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; @@ -1095,6 +1404,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); } } @@ -1207,7 +1520,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); @@ -1236,8 +1553,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; @@ -1278,50 +1596,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 @@ -1346,6 +1697,7 @@ bool Path::fromString (const String& pathData) } //============================================================================== + Point Path::getPointAlongPath (float distance) const { // Clamp distance to valid range @@ -1538,6 +1890,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..6444dfa4a 100644 --- a/modules/yup_graphics/primitives/yup_Path.h +++ b/modules/yup_graphics/primitives/yup_Path.h @@ -22,6 +22,99 @@ 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. @@ -87,6 +180,8 @@ class YUP_API Path */ int size() const; + bool isEmpty() const { return size() == 0; } + //============================================================================== /** Clears all the segments from the path. @@ -427,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. @@ -438,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. @@ -452,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. @@ -466,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. @@ -492,6 +592,16 @@ class YUP_API Path */ Path withRoundedCorners (float cornerRadius) const; + //============================================================================== + + void startNewSubPath (float x, float y); + void startNewSubPath (const Point& p); + + void closeSubPath(); + + bool isClosed (float tolerance = 0.001f) const; + bool isExplicitlyClosed() const; + //============================================================================== /** Appends another path to this one. @@ -605,51 +715,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/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/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" diff --git a/modules/yup_graphics/yup_graphics.h b/modules/yup_graphics/yup_graphics.h index 21b8b97f3..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" @@ -83,3 +84,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" 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