Browse Source

Added dashed-line support to the SVG parser and DrawablePath

tags/2021-05-28
jules 9 years ago
parent
commit
606e7be12f
4 changed files with 218 additions and 91 deletions
  1. +0
    -3
      modules/juce_graphics/geometry/juce_PathStrokeType.cpp
  2. +17
    -1
      modules/juce_gui_basics/drawables/juce_DrawableShape.cpp
  3. +7
    -0
      modules/juce_gui_basics/drawables/juce_DrawableShape.h
  4. +194
    -87
      modules/juce_gui_basics/drawables/juce_SVGParser.cpp

+ 0
- 3
modules/juce_graphics/geometry/juce_PathStrokeType.cpp View File

@@ -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);


+ 17
- 1
modules/juce_gui_basics/drawables/juce_DrawableShape.cpp View File

@@ -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<float>& 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();


+ 7
- 0
modules/juce_gui_basics/drawables/juce_DrawableShape.h View File

@@ -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<float>& newDashLengths);
/** Returns the set of dash lengths that the path is using. */
const Array<float>& getDashLengths() const noexcept { return dashLengths; };
//==============================================================================
/** @internal */
class FillAndStrokeState : public Drawable::ValueTreeWrapperBase
@@ -165,6 +171,7 @@ protected:
//==============================================================================
PathStrokeType strokeType;
Array<float> dashLengths;
Path path, strokePath;
private:


+ 194
- 87
modules/juce_gui_basics/drawables/juce_SVGParser.cpp View File

@@ -42,6 +42,26 @@ public:
const XmlElement* operator->() const noexcept { return xml; }
XmlPath getChild (const XmlElement* e) const noexcept { return XmlPath (e, this); }
template <typename OperationType>
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<float> 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<float> 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 <typename OperationType>
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;
};


Loading…
Cancel
Save