diff --git a/modules/juce_graphics/geometry/juce_PathStrokeType.cpp b/modules/juce_graphics/geometry/juce_PathStrokeType.cpp index dbb5cb944c..96cbc60d8b 100644 --- a/modules/juce_graphics/geometry/juce_PathStrokeType.cpp +++ b/modules/juce_graphics/geometry/juce_PathStrokeType.cpp @@ -666,9 +666,6 @@ void PathStrokeType::createDashedStroke (Path& destPath, if (thickness <= 0) return; - // this should really be an even number.. - jassert ((numDashLengths & 1) == 0); - Path newDestPath; PathFlatteningIterator it (sourcePath, transform, PathFlatteningIterator::defaultTolerance / extraAccuracy); diff --git a/modules/juce_gui_basics/drawables/juce_DrawableShape.cpp b/modules/juce_gui_basics/drawables/juce_DrawableShape.cpp old mode 100644 new mode 100755 index b2a65d70bd..96d8e90928 --- a/modules/juce_gui_basics/drawables/juce_DrawableShape.cpp +++ b/modules/juce_gui_basics/drawables/juce_DrawableShape.cpp @@ -32,6 +32,7 @@ DrawableShape::DrawableShape() DrawableShape::DrawableShape (const DrawableShape& other) : Drawable (other), strokeType (other.strokeType), + dashLengths (other.dashLengths), mainFill (other.mainFill), strokeFill (other.strokeFill) { @@ -132,6 +133,15 @@ void DrawableShape::setStrokeType (const PathStrokeType& newStrokeType) } } +void DrawableShape::setDashLengths (const Array& newDashLengths) +{ + if (dashLengths != newDashLengths) + { + dashLengths = newDashLengths; + strokeChanged(); + } +} + void DrawableShape::setStrokeThickness (const float newThickness) { setStrokeType (PathStrokeType (newThickness, strokeType.getJointStyle(), strokeType.getEndStyle())); @@ -178,7 +188,13 @@ void DrawableShape::pathChanged() void DrawableShape::strokeChanged() { strokePath.clear(); - strokeType.createStrokedPath (strokePath, path, AffineTransform(), 4.0f); + const float extraAccuracy = 4.0f; + + if (dashLengths.empty()) + strokeType.createStrokedPath (strokePath, path, AffineTransform(), extraAccuracy); + else + strokeType.createDashedStroke (strokePath, path, dashLengths.getRawDataPointer(), + dashLengths.size(), AffineTransform(), extraAccuracy); setBoundsToEnclose (getDrawableBounds()); repaint(); diff --git a/modules/juce_gui_basics/drawables/juce_DrawableShape.h b/modules/juce_gui_basics/drawables/juce_DrawableShape.h old mode 100644 new mode 100755 index 5fa5b3ed44..20cbc3cf7d --- a/modules/juce_gui_basics/drawables/juce_DrawableShape.h +++ b/modules/juce_gui_basics/drawables/juce_DrawableShape.h @@ -122,6 +122,12 @@ public: /** Returns the current outline style. */ const PathStrokeType& getStrokeType() const noexcept { return strokeType; } + /** Provides a set of dash lengths to use for stroking the path. */ + void setDashLengths (const Array& newDashLengths); + + /** Returns the set of dash lengths that the path is using. */ + const Array& getDashLengths() const noexcept { return dashLengths; }; + //============================================================================== /** @internal */ class FillAndStrokeState : public Drawable::ValueTreeWrapperBase @@ -165,6 +171,7 @@ protected: //============================================================================== PathStrokeType strokeType; + Array dashLengths; Path path, strokePath; private: diff --git a/modules/juce_gui_basics/drawables/juce_SVGParser.cpp b/modules/juce_gui_basics/drawables/juce_SVGParser.cpp index 01d487228f..d31243bb1b 100644 --- a/modules/juce_gui_basics/drawables/juce_SVGParser.cpp +++ b/modules/juce_gui_basics/drawables/juce_SVGParser.cpp @@ -42,6 +42,26 @@ public: const XmlElement* operator->() const noexcept { return xml; } XmlPath getChild (const XmlElement* e) const noexcept { return XmlPath (e, this); } + template + bool applyOperationToChildWithID (const String& id, OperationType& op) const + { + forEachXmlChildElement (*xml, e) + { + XmlPath child (e, this); + + if (e->compareAttribute ("id", id)) + { + op (child); + return true; + } + + if (child.applyOperationToChildWithID (id, op)) + return true; + } + + return false; + } + const XmlElement* xml; const XmlPath* parent; }; @@ -364,25 +384,40 @@ private: Drawable* parseSubElement (const XmlPath& xml) { + { + Path path; + if (parsePathElement (xml, path)) + return parseShape (xml, path); + } + const String tag (xml->getTagNameWithoutNamespace()); - if (tag == "g") return parseGroupElement (xml); - if (tag == "svg") return parseSVGElement (xml); - if (tag == "path") return parsePath (xml); - if (tag == "rect") return parseRect (xml); - if (tag == "circle") return parseCircle (xml); - if (tag == "ellipse") return parseEllipse (xml); - if (tag == "line") return parseLine (xml); - if (tag == "polyline") return parsePolygon (xml, true); - if (tag == "polygon") return parsePolygon (xml, false); - if (tag == "text") return parseText (xml, true); - if (tag == "switch") return parseSwitch (xml); - if (tag == "a") return parseLinkElement (xml); - if (tag == "style") parseCSSStyle (xml); + if (tag == "g") return parseGroupElement (xml); + if (tag == "svg") return parseSVGElement (xml); + if (tag == "text") return parseText (xml, true); + if (tag == "switch") return parseSwitch (xml); + if (tag == "a") return parseLinkElement (xml); + if (tag == "style") parseCSSStyle (xml); return nullptr; } + bool parsePathElement (const XmlPath& xml, Path& path) const + { + const String tag (xml->getTagNameWithoutNamespace()); + + if (tag == "path") { parsePath (xml, path); return true; } + if (tag == "rect") { parseRect (xml, path); return true; } + if (tag == "circle") { parseCircle (xml, path); return true; } + if (tag == "ellipse") { parseEllipse (xml, path); return true; } + if (tag == "line") { parseLine (xml, path); return true; } + if (tag == "polyline") { parsePolygon (xml, true, path); return true; } + if (tag == "polygon") { parsePolygon (xml, false, path); return true; } + if (tag == "use") { parseUse (xml, path); return true; } + + return false; + } + DrawableComposite* parseSwitch (const XmlPath& xml) { if (const XmlElement* const group = xml->getChildByName ("g")) @@ -419,21 +454,16 @@ private: } //============================================================================== - Drawable* parsePath (const XmlPath& xml) const + void parsePath (const XmlPath& xml, Path& path) const { - Path path; parsePathString (path, xml->getStringAttribute ("d")); if (getStyleAttribute (xml, "fill-rule").trim().equalsIgnoreCase ("evenodd")) path.setUsingNonZeroWinding (false); - - return parseShape (xml, path); } - Drawable* parseRect (const XmlPath& xml) const + void parseRect (const XmlPath& xml, Path& rect) const { - Path rect; - const bool hasRX = xml->hasAttribute ("rx"); const bool hasRY = xml->hasAttribute ("ry"); @@ -460,41 +490,29 @@ private: getCoordLength (xml, "width", viewBoxW), getCoordLength (xml, "height", viewBoxH)); } - - return parseShape (xml, rect); } - Drawable* parseCircle (const XmlPath& xml) const + void parseCircle (const XmlPath& xml, Path& circle) const { - Path circle; - const float cx = getCoordLength (xml, "cx", viewBoxW); const float cy = getCoordLength (xml, "cy", viewBoxH); const float radius = getCoordLength (xml, "r", viewBoxW); circle.addEllipse (cx - radius, cy - radius, radius * 2.0f, radius * 2.0f); - - return parseShape (xml, circle); } - Drawable* parseEllipse (const XmlPath& xml) const + void parseEllipse (const XmlPath& xml, Path& ellipse) const { - Path ellipse; - const float cx = getCoordLength (xml, "cx", viewBoxW); const float cy = getCoordLength (xml, "cy", viewBoxH); const float radiusX = getCoordLength (xml, "rx", viewBoxW); const float radiusY = getCoordLength (xml, "ry", viewBoxH); ellipse.addEllipse (cx - radiusX, cy - radiusY, radiusX * 2.0f, radiusY * 2.0f); - - return parseShape (xml, ellipse); } - Drawable* parseLine (const XmlPath& xml) const + void parseLine (const XmlPath& xml, Path& line) const { - Path line; - const float x1 = getCoordLength (xml, "x1", viewBoxW); const float y1 = getCoordLength (xml, "y1", viewBoxH); const float x2 = getCoordLength (xml, "x2", viewBoxW); @@ -502,15 +520,12 @@ private: line.startNewSubPath (x1, y1); line.lineTo (x2, y2); - - return parseShape (xml, line); } - Drawable* parsePolygon (const XmlPath& xml, const bool isPolyline) const + void parsePolygon (const XmlPath& xml, const bool isPolyline, Path& path) const { const String pointsAtt (xml->getStringAttribute ("points")); String::CharPointerType points (pointsAtt.getCharPointer()); - Path path; Point p; if (parseCoords (points, p, true)) @@ -528,8 +543,39 @@ private: if ((! isPolyline) || first == last) path.closeSubPath(); } + } + + void parseUse (const XmlPath& xml, Path& path) const + { + const String link (xml->getStringAttribute ("xlink:href")); + + if (link.startsWithChar ('#')) + { + const String linkedID = link.substring (1); + + struct UsePathOp + { + const SVGState* state; + Path* targetPath; + + void operator() (const XmlPath& xmlPath) + { + state->parsePathElement (xmlPath, *targetPath); + } + }; + + UsePathOp op = { this, &path }; + topLevelXml.applyOperationToChildWithID (linkedID, op); + } + } + + static String parseURL (const String& str) + { + if (str.startsWithIgnoreCase ("url")) + return str.fromFirstOccurrenceOf ("#", false, false) + .upToLastOccurrenceOf (")", false, false).trim(); - return parseShape (xml, path); + return String(); } //============================================================================== @@ -570,6 +616,13 @@ private: dp->setStrokeType (getStrokeFor (xml)); } + const String strokeDashArray (getStyleAttribute (xml, "stroke-dasharray")); + + if (strokeDashArray.isNotEmpty()) + parseDashArray (strokeDashArray.getCharPointer(), *dp); + + parseClipPath (xml, *dp); + return dp; } @@ -582,16 +635,79 @@ private: return false; } - struct SetGradientStopsOp + void parseDashArray (String::CharPointerType t, DrawablePath& dp) const { - const SVGState* state; - ColourGradient* gradient; + Array dashLengths; - void operator() (const XmlPath& xml) + for (;;) { - state->addGradientStopsIn (*gradient, xml); + float value; + if (! parseCoord (t, value, true, true)) + break; + + dashLengths.add (value); + + t = t.findEndOfWhitespace(); + + if (*t == ',') + ++t; } - }; + + float* const dashes = dashLengths.getRawDataPointer(); + + for (int i = 0; i < dashLengths.size(); ++i) + { + if (dashes[i] <= 0) // SVG uses zero-length dashes to mean a dotted line + { + const float nonZeroLength = 0.001f; + dashes[i] = nonZeroLength; + + const int pairedIndex = i ^ 1; + + if (isPositiveAndBelow (pairedIndex, dashLengths.size()) + && dashes[pairedIndex] > nonZeroLength) + dashes[pairedIndex] -= nonZeroLength; + } + } + + dp.setDashLengths (dashLengths); + } + + void parseClipPath (const XmlPath& xml, Drawable& d) const + { + const String clipPath (getStyleAttribute (xml, "clip-path")); + + if (clipPath.isNotEmpty()) + { + String urlID = parseURL (clipPath); + + if (urlID.isNotEmpty()) + { + struct GetClipPathOp + { + const SVGState* state; + Drawable* target; + + void operator() (const XmlPath& xmlPath) + { + state->applyClipPath (*target, xmlPath); + } + }; + + GetClipPathOp op = { this, &d }; + topLevelXml.applyOperationToChildWithID (urlID, op); + } + } + } + + void applyClipPath (Drawable& target, const XmlPath& xmlPath) const + { + if (xmlPath->hasTagNameIgnoringNamespace ("clippath")) + { + // TODO: implement clipping.. + ignoreUnused (target); + } + } void addGradientStopsIn (ColourGradient& cg, const XmlPath& fillXml) const { @@ -626,8 +742,19 @@ private: if (id.startsWithChar ('#')) { + struct SetGradientStopsOp + { + const SVGState* state; + ColourGradient* gradient; + + void operator() (const XmlPath& xml) + { + state->addGradientStopsIn (*gradient, xml); + } + }; + SetGradientStopsOp op = { this, &gradient, }; - findElementForId (topLevelXml, id.substring (1), op); + topLevelXml.applyOperationToChildWithID (id.substring (1), op); } } @@ -736,21 +863,6 @@ private: return type; } - struct GetFillTypeOp - { - const SVGState* state; - FillType* dest; - const Path* path; - float opacity; - - void operator() (const XmlPath& xml) - { - if (xml->hasTagNameIgnoringNamespace ("linearGradient") - || xml->hasTagNameIgnoringNamespace ("radialGradient")) - *dest = state->getGradientFillType (xml, *path, opacity); - } - }; - FillType getPathFillType (const Path& path, const String& fill, const String& fillOpacity, @@ -765,16 +877,29 @@ private: if (fillOpacity.isNotEmpty()) opacity *= (jlimit (0.0f, 1.0f, fillOpacity.getFloatValue())); - if (fill.startsWithIgnoreCase ("url")) + String urlID = parseURL (fill); + + if (urlID.isNotEmpty()) { - const String id (fill.fromFirstOccurrenceOf ("#", false, false) - .upToLastOccurrenceOf (")", false, false).trim()); + struct GetFillTypeOp + { + const SVGState* state; + const Path* path; + float opacity; + FillType fillType; + + void operator() (const XmlPath& xml) + { + if (xml->hasTagNameIgnoringNamespace ("linearGradient") + || xml->hasTagNameIgnoringNamespace ("radialGradient")) + fillType = state->getGradientFillType (xml, *path, opacity); + } + }; - FillType result; - GetFillTypeOp op = { this, &result, &path, opacity }; + GetFillTypeOp op = { this, &path, opacity }; - if (findElementForId (topLevelXml, id, op)) - return result; + if (topLevelXml.applyOperationToChildWithID (urlID, op)) + return op.fillType; } if (fill.equalsIgnoreCase ("none")) @@ -1337,24 +1462,6 @@ private: deltaAngle = fmod (deltaAngle, double_Pi * 2.0); } - template - static bool findElementForId (const XmlPath& parent, const String& id, OperationType& op) - { - forEachXmlChildElement (*parent, e) - { - if (e->compareAttribute ("id", id)) - { - op (parent.getChild (e)); - return true; - } - - if (findElementForId (parent.getChild (e), id, op)) - return true; - } - - return false; - } - SVGState& operator= (const SVGState&) JUCE_DELETED_FUNCTION; };