962 lines
29 KiB
C++
962 lines
29 KiB
C++
// An OpenVG path on the Canvas
|
|
//
|
|
// Copyright (C) 2012 Thomas Geymayer <tomgey@gmail.com>
|
|
//
|
|
// This library is free software; you can redistribute it and/or
|
|
// modify it under the terms of the GNU Library General Public
|
|
// License as published by the Free Software Foundation; either
|
|
// version 2 of the License, or (at your option) any later version.
|
|
//
|
|
// This library is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
// Library General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Library General Public
|
|
// License along with this library; if not, write to the Free Software
|
|
// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
|
|
|
|
#include <simgear_config.h>
|
|
|
|
#include "CanvasPath.hxx"
|
|
#include <simgear/scene/util/parse_color.hxx>
|
|
#include <simgear/misc/strutils.hxx>
|
|
|
|
#include <osg/Drawable>
|
|
#include <osg/Version>
|
|
|
|
#include <vg/openvg.h>
|
|
#include <cassert>
|
|
#include <cctype>
|
|
|
|
namespace simgear
|
|
{
|
|
namespace canvas
|
|
{
|
|
typedef std::vector<VGubyte> CmdList;
|
|
typedef std::vector<VGfloat> CoordList;
|
|
|
|
|
|
static const VGubyte shCoordsPerCommand[] = {
|
|
0, /* VG_CLOSE_PATH */
|
|
2, /* VG_MOVE_TO */
|
|
2, /* VG_LINE_TO */
|
|
1, /* VG_HLINE_TO */
|
|
1, /* VG_VLINE_TO */
|
|
4, /* VG_QUAD_TO */
|
|
6, /* VG_CUBIC_TO */
|
|
2, /* VG_SQUAD_TO */
|
|
4, /* VG_SCUBIC_TO */
|
|
5, /* VG_SCCWARC_TO */
|
|
5, /* VG_SCWARC_TO */
|
|
5, /* VG_LCCWARC_TO */
|
|
5 /* VG_LCWARC_TO */
|
|
};
|
|
static const VGubyte shNumCommands = sizeof(shCoordsPerCommand)
|
|
/ sizeof(shCoordsPerCommand[0]);
|
|
|
|
/**
|
|
* Helper to split and convert comma/whitespace separated floating point
|
|
* values
|
|
*/
|
|
std::vector<float> splitAndConvert(const char del[], const std::string& str);
|
|
|
|
static float parseCSSNumber(const std::string& s)
|
|
{
|
|
if (strutils::ends_with(s, "px")) {
|
|
return std::stof(s.substr(0, s.length() - 2));
|
|
} else if (s.back() == '%') {
|
|
float f = std::stof(s.substr(0, s.length() - 1));
|
|
return f / 100.0f;
|
|
}
|
|
return std::stof(s);
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
static bool parseSVGPathToVGPath(const std::string& svgPath, CmdList& commands, CoordList& coords)
|
|
{
|
|
const string_list& tokens = simgear::strutils::split_on_any_of(svgPath, "\t \n\r,");
|
|
char activeSVGCommand = 0;
|
|
bool isRelative = false;
|
|
int tokensNeeded = 0;
|
|
|
|
for (auto it = tokens.begin(); it != tokens.end(); ) {
|
|
// set up the new command data
|
|
if ((it->size() == 1) && std::isalpha(it->at(0))) {
|
|
const char svgCommand = std::toupper(it->at(0));
|
|
isRelative = std::islower(it->at(0));
|
|
switch (svgCommand) {
|
|
case 'Z':
|
|
tokensNeeded = 0;
|
|
break;
|
|
case 'M':
|
|
case 'L':
|
|
case 'T':
|
|
tokensNeeded = 2;
|
|
break;
|
|
case 'H':
|
|
case 'V':
|
|
tokensNeeded = 1;
|
|
break;
|
|
case 'C':
|
|
tokensNeeded = 6;
|
|
break;
|
|
case 'S':
|
|
case 'Q':
|
|
tokensNeeded = 4;
|
|
break;
|
|
case 'A':
|
|
tokensNeeded = 7;
|
|
break;
|
|
default:
|
|
SG_LOG(SG_GENERAL, SG_WARN, "unrecognized SVG path command: "
|
|
<< *it << " at token " << std::distance(tokens.begin(), it));
|
|
return false;
|
|
}
|
|
|
|
activeSVGCommand = svgCommand;
|
|
++it; // advance to first coordinate token
|
|
}
|
|
|
|
const int numTokensRemaining = std::distance(it, tokens.end());
|
|
if (numTokensRemaining < tokensNeeded) {
|
|
SG_LOG(SG_GENERAL, SG_WARN, "insufficent SVG path tokens");
|
|
return false;
|
|
}
|
|
|
|
bool pushTokensDirectly = true;
|
|
if (activeSVGCommand == 'Z') {
|
|
commands.push_back(VG_CLOSE_PATH);
|
|
activeSVGCommand = 0;
|
|
} else if (activeSVGCommand == 'M') {
|
|
commands.push_back(VG_MOVE_TO | isRelative);
|
|
activeSVGCommand = 'L';
|
|
} else if (activeSVGCommand == 'L') {
|
|
commands.push_back(VG_LINE_TO | isRelative);
|
|
} else if (activeSVGCommand == 'H') {
|
|
commands.push_back(VG_HLINE_TO | isRelative);
|
|
} else if (activeSVGCommand == 'V') {
|
|
commands.push_back(VG_HLINE_TO | isRelative);
|
|
} else if (activeSVGCommand == 'C') {
|
|
commands.push_back(VG_CUBIC_TO | isRelative);
|
|
} else if (activeSVGCommand == 'S') {
|
|
commands.push_back(VG_SCUBIC_TO | isRelative);
|
|
} else if (activeSVGCommand == 'Q') {
|
|
commands.push_back(VG_SCUBIC_TO | isRelative);
|
|
} else if (activeSVGCommand == 'T') {
|
|
commands.push_back(VG_SCUBIC_TO | isRelative);
|
|
} else if (activeSVGCommand == 'A') {
|
|
pushTokensDirectly = false; // deal with tokens manually
|
|
coords.push_back(parseCSSNumber(*it++)); // rx
|
|
coords.push_back(parseCSSNumber(*it++)); // ry
|
|
coords.push_back(parseCSSNumber(*it++)); // x-axis rotation
|
|
|
|
const bool isLargeArc = std::stoi(*it++); // large-angle
|
|
const bool isCCW = std::stoi(*it++); // sweep-flag
|
|
|
|
int vgCmd = isLargeArc ? (isCCW ? VG_LCCWARC_TO : VG_LCWARC_TO) :
|
|
(isCCW ? VG_SCCWARC_TO : VG_SCWARC_TO);
|
|
|
|
coords.push_back(parseCSSNumber(*it++));
|
|
coords.push_back(parseCSSNumber(*it++));
|
|
commands.push_back(vgCmd | isRelative);
|
|
} else {
|
|
SG_LOG(SG_GENERAL, SG_WARN, "malformed SVG path string: expected a command at token:"
|
|
<< std::distance(tokens.begin(), it) << " :" << *it);
|
|
return false;
|
|
}
|
|
|
|
if (pushTokensDirectly) {
|
|
for (int i=0; i<tokensNeeded;++i) {
|
|
coords.push_back(parseCSSNumber(*it++));
|
|
}
|
|
}
|
|
} // of tokens iteration
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------------------------
|
|
|
|
static SGVec2f parseRectCornerRadius(SGPropertyNode* node, const std::string& xDir, const std::string& yDir, bool& haveCorner)
|
|
{
|
|
haveCorner = false;
|
|
std::string propName = "border-" + yDir + "-" + xDir + "-radius";
|
|
if (!node->hasChild(propName)) {
|
|
propName = "border-" + yDir + "-radius";
|
|
if (!node->hasChild(propName)) {
|
|
propName = "border-radius";
|
|
}
|
|
}
|
|
|
|
PropertyList props = node->getChildren(propName);
|
|
if (props.size() == 1) {
|
|
double r = props.at(0)->getDoubleValue(propName);
|
|
haveCorner = true;
|
|
return SGVec2f(r, r);
|
|
}
|
|
|
|
if (props.size() >= 2 ) {
|
|
haveCorner = true;
|
|
return SGVec2f(props.at(0)->getDoubleValue(),
|
|
props.at(1)->getDoubleValue());
|
|
}
|
|
|
|
return SGVec2f(-1.0f, -1.0f);
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
class Path::PathDrawable:
|
|
public osg::Drawable
|
|
{
|
|
public:
|
|
PathDrawable(Path* path):
|
|
_path_element(path)
|
|
{
|
|
setSupportsDisplayList(false);
|
|
setDataVariance(Object::DYNAMIC);
|
|
|
|
setUpdateCallback(new PathUpdateCallback());
|
|
}
|
|
|
|
virtual ~PathDrawable()
|
|
{
|
|
if( _path != VG_INVALID_HANDLE )
|
|
vgDestroyPath(_path);
|
|
if( _paint != VG_INVALID_HANDLE )
|
|
vgDestroyPaint(_paint);
|
|
if( _paint_fill != VG_INVALID_HANDLE )
|
|
vgDestroyPaint(_paint_fill);
|
|
}
|
|
|
|
virtual const char* className() const { return "PathDrawable"; }
|
|
virtual osg::Object* cloneType() const { return new PathDrawable(_path_element); }
|
|
virtual osg::Object* clone(const osg::CopyOp&) const { return new PathDrawable(_path_element); }
|
|
|
|
/**
|
|
* Replace the current path segments with the new ones
|
|
*
|
|
* @param cmds List of OpenVG path commands
|
|
* @param coords List of coordinates/parameters used by #cmds
|
|
*/
|
|
void setSegments(const CmdList& cmds, const CoordList& coords)
|
|
{
|
|
_cmds = cmds;
|
|
_coords = coords;
|
|
|
|
_attributes_dirty |= (PATH | BOUNDING_BOX);
|
|
}
|
|
|
|
/**
|
|
* Set path fill paint ("none" if not filled)
|
|
*/
|
|
void setFill(const std::string& fill)
|
|
{
|
|
if( fill.empty() || fill == "none" )
|
|
{
|
|
_mode &= ~VG_FILL_PATH;
|
|
}
|
|
else if( parseColor(fill, _fill_color) )
|
|
{
|
|
_mode |= VG_FILL_PATH;
|
|
_attributes_dirty |= FILL_COLOR;
|
|
}
|
|
else
|
|
{
|
|
SG_LOG
|
|
(
|
|
SG_GENERAL,
|
|
SG_WARN,
|
|
"canvas::Path Unknown fill: " << fill
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set path fill rule ("pseudo-nonzero" or "evenodd")
|
|
*
|
|
* @warning As the current nonzero implementation causes sever artifacts
|
|
* for every concave path we call it pseudo-nonzero, so that
|
|
* everyone is warned that it won't work as expected :)
|
|
*/
|
|
void setFillRule(const std::string& fill_rule)
|
|
{
|
|
if( fill_rule == "pseudo-nonzero" )
|
|
_fill_rule = VG_NON_ZERO;
|
|
else // if( fill_rule == "evenodd" )
|
|
_fill_rule = VG_EVEN_ODD;
|
|
}
|
|
|
|
/**
|
|
* Set path stroke paint ("none" if no stroke)
|
|
*/
|
|
void setStroke(const std::string& stroke)
|
|
{
|
|
if( stroke.empty() || stroke == "none" )
|
|
{
|
|
_mode &= ~VG_STROKE_PATH;
|
|
}
|
|
else if( parseColor(stroke, _stroke_color) )
|
|
{
|
|
_mode |= VG_STROKE_PATH;
|
|
_attributes_dirty |= STROKE_COLOR;
|
|
}
|
|
else
|
|
{
|
|
SG_LOG
|
|
(
|
|
SG_GENERAL,
|
|
SG_WARN,
|
|
"canvas::Path Unknown stroke: " << stroke
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set stroke width
|
|
*/
|
|
void setStrokeWidth(float width)
|
|
{
|
|
_stroke_width = width;
|
|
_attributes_dirty |= BOUNDING_BOX;
|
|
}
|
|
|
|
/**
|
|
* Set stroke dash (line stipple)
|
|
*/
|
|
void setStrokeDashArray(const std::string& dash)
|
|
{
|
|
_stroke_dash = splitAndConvert(",\t\n ", dash);
|
|
}
|
|
|
|
/**
|
|
* Set stroke-linecap
|
|
*
|
|
* @see http://www.w3.org/TR/SVG/painting.html#StrokeLinecapProperty
|
|
*/
|
|
void setStrokeLinecap(const std::string& linecap)
|
|
{
|
|
if( linecap == "round" )
|
|
_stroke_linecap = VG_CAP_ROUND;
|
|
else if( linecap == "square" )
|
|
_stroke_linecap = VG_CAP_SQUARE;
|
|
else
|
|
_stroke_linecap = VG_CAP_BUTT;
|
|
}
|
|
|
|
/**
|
|
* Set stroke-linejoin
|
|
*
|
|
* @see http://www.w3.org/TR/SVG/painting.html#StrokeLinejoinProperty
|
|
*/
|
|
void setStrokeLinejoin(const std::string& linejoin)
|
|
{
|
|
if( linejoin == "round" )
|
|
_stroke_linejoin = VG_JOIN_ROUND;
|
|
else if( linejoin == "bevel" )
|
|
_stroke_linejoin = VG_JOIN_BEVEL;
|
|
else
|
|
_stroke_linejoin = VG_JOIN_MITER;
|
|
}
|
|
|
|
/**
|
|
* Draw callback
|
|
*/
|
|
virtual void drawImplementation(osg::RenderInfo& renderInfo) const
|
|
{
|
|
if( _attributes_dirty & PATH )
|
|
return;
|
|
|
|
osg::State* state = renderInfo.getState();
|
|
assert(state);
|
|
|
|
state->setActiveTextureUnit(0);
|
|
state->setClientActiveTextureUnit(0);
|
|
state->disableAllVertexArrays();
|
|
|
|
bool was_blend_enabled = state->getLastAppliedMode(GL_BLEND);
|
|
bool was_stencil_enabled = state->getLastAppliedMode(GL_STENCIL_TEST);
|
|
osg::StateAttribute const* blend_func =
|
|
state->getLastAppliedAttribute(osg::StateAttribute::BLENDFUNC);
|
|
|
|
// Initialize/Update the paint
|
|
if( _attributes_dirty & STROKE_COLOR )
|
|
{
|
|
if( _paint == VG_INVALID_HANDLE )
|
|
_paint = vgCreatePaint();
|
|
|
|
vgSetParameterfv(_paint, VG_PAINT_COLOR, 4, _stroke_color._v);
|
|
|
|
_attributes_dirty &= ~STROKE_COLOR;
|
|
}
|
|
|
|
// Initialize/update fill paint
|
|
if( _attributes_dirty & FILL_COLOR )
|
|
{
|
|
if( _paint_fill == VG_INVALID_HANDLE )
|
|
_paint_fill = vgCreatePaint();
|
|
|
|
vgSetParameterfv(_paint_fill, VG_PAINT_COLOR, 4, _fill_color._v);
|
|
|
|
_attributes_dirty &= ~FILL_COLOR;
|
|
}
|
|
|
|
// Setup paint
|
|
if( _mode & VG_STROKE_PATH )
|
|
{
|
|
vgSetPaint(_paint, VG_STROKE_PATH);
|
|
|
|
vgSetf(VG_STROKE_LINE_WIDTH, _stroke_width);
|
|
vgSeti(VG_STROKE_CAP_STYLE, _stroke_linecap);
|
|
vgSeti(VG_STROKE_JOIN_STYLE, _stroke_linejoin);
|
|
vgSetfv( VG_STROKE_DASH_PATTERN,
|
|
_stroke_dash.size(),
|
|
_stroke_dash.empty() ? 0 : &_stroke_dash[0] );
|
|
}
|
|
if( _mode & VG_FILL_PATH )
|
|
{
|
|
vgSetPaint(_paint_fill, VG_FILL_PATH);
|
|
|
|
vgSeti(VG_FILL_RULE, _fill_rule);
|
|
}
|
|
|
|
// And finally draw the path
|
|
if( _mode )
|
|
vgDrawPath(_path, _mode);
|
|
|
|
VGErrorCode err = vgGetError();
|
|
if( err != VG_NO_ERROR )
|
|
SG_LOG(SG_GL, SG_ALERT, "vgError: " << err);
|
|
|
|
// Restore OpenGL state (TODO check if more is needed or integrate
|
|
// better with OpenSceneGraph)
|
|
if( was_blend_enabled ) glEnable(GL_BLEND);
|
|
if( was_stencil_enabled ) glEnable(GL_STENCIL_TEST);
|
|
if( blend_func ) blend_func->apply(*state);
|
|
}
|
|
|
|
osg::BoundingBox getTransformedBounds(const osg::Matrix& mat) const
|
|
{
|
|
osg::BoundingBox bb;
|
|
|
|
osg::Vec2f cur(0, 0), // VG "Current point" (in local coordinates)
|
|
sub(0, 0); // beginning of current sub path
|
|
VGubyte cmd_index = 0;
|
|
for( size_t i = 0, ci = 0;
|
|
i < _cmds.size() && ci < _coords.size();
|
|
++i, ci += shCoordsPerCommand[cmd_index] )
|
|
{
|
|
VGubyte rel = _cmds[i] & 1,
|
|
cmd = _cmds[i] & ~1;
|
|
|
|
cmd_index = cmd / 2;
|
|
if( cmd_index >= shNumCommands )
|
|
return osg::BoundingBox();
|
|
|
|
const VGubyte max_coords = 3;
|
|
osg::Vec2f points[max_coords];
|
|
VGubyte num_coords = 0;
|
|
|
|
switch( cmd )
|
|
{
|
|
case VG_CLOSE_PATH:
|
|
cur = sub;
|
|
break;
|
|
case VG_MOVE_TO:
|
|
case VG_LINE_TO:
|
|
case VG_SQUAD_TO:
|
|
// x0, y0
|
|
points[ num_coords++ ].set(_coords[ci], _coords[ci + 1]);
|
|
break;
|
|
case VG_HLINE_TO:
|
|
// x0
|
|
points[ num_coords++ ].set( _coords[ci] + (rel ? cur.x() : 0),
|
|
cur.y() );
|
|
// We have handled rel/abs already, so no need to do it again...
|
|
rel = 0;
|
|
break;
|
|
case VG_VLINE_TO:
|
|
// y0
|
|
points[ num_coords++ ].set( cur.x(),
|
|
_coords[ci] + (rel ? cur.y() : 0) );
|
|
// We have handled rel/abs already, so no need to do it again...
|
|
rel = 0;
|
|
break;
|
|
case VG_QUAD_TO:
|
|
case VG_SCUBIC_TO:
|
|
// x0,y0,x1,y1
|
|
points[ num_coords++ ].set(_coords[ci ], _coords[ci + 1]);
|
|
points[ num_coords++ ].set(_coords[ci + 2], _coords[ci + 3]);
|
|
break;
|
|
case VG_CUBIC_TO:
|
|
// x0,y0,x1,y1,x2,y2
|
|
points[ num_coords++ ].set(_coords[ci ], _coords[ci + 1]);
|
|
points[ num_coords++ ].set(_coords[ci + 2], _coords[ci + 3]);
|
|
points[ num_coords++ ].set(_coords[ci + 4], _coords[ci + 5]);
|
|
break;
|
|
case VG_SCCWARC_TO:
|
|
case VG_SCWARC_TO:
|
|
case VG_LCCWARC_TO:
|
|
case VG_LCWARC_TO:
|
|
// rh,rv,rot,x0,y0
|
|
points[ num_coords++ ].set(_coords[ci + 3], _coords[ci + 4]);
|
|
break;
|
|
default:
|
|
SG_LOG(SG_GL, SG_WARN, "Unknown VG command: " << (int)cmd);
|
|
return osg::BoundingBox();
|
|
}
|
|
|
|
assert(num_coords <= max_coords);
|
|
for(VGubyte i = 0; i < num_coords; ++i)
|
|
{
|
|
if( rel )
|
|
points[i] += cur;
|
|
|
|
bb.expandBy( transformPoint(mat, points[i]) );
|
|
}
|
|
|
|
if( num_coords > 0 )
|
|
{
|
|
cur = points[ num_coords - 1 ];
|
|
|
|
if( cmd == VG_MOVE_TO )
|
|
sub = cur;
|
|
}
|
|
}
|
|
|
|
return bb;
|
|
}
|
|
|
|
/**
|
|
* Compute the bounding box
|
|
*/
|
|
virtual osg::BoundingBox
|
|
#if OSG_VERSION_LESS_THAN(3,3,2)
|
|
computeBound()
|
|
#else
|
|
computeBoundingBox()
|
|
#endif
|
|
const
|
|
{
|
|
if( _path == VG_INVALID_HANDLE || (_attributes_dirty & PATH) )
|
|
return osg::BoundingBox();
|
|
|
|
VGfloat min[2], size[2];
|
|
vgPathBounds(_path, &min[0], &min[1], &size[0], &size[1]);
|
|
|
|
_attributes_dirty &= ~BOUNDING_BOX;
|
|
|
|
// vgPathBounds doesn't take stroke width into account
|
|
float ext = 0.5 * _stroke_width;
|
|
|
|
return osg::BoundingBox
|
|
(
|
|
min[0] - ext, min[1] - ext, -0.1,
|
|
min[0] + size[0] + ext, min[1] + size[1] + ext, 0.1
|
|
);
|
|
}
|
|
|
|
private:
|
|
|
|
enum Attributes
|
|
{
|
|
PATH = 0x0001,
|
|
STROKE_COLOR = PATH << 1,
|
|
FILL_COLOR = STROKE_COLOR << 1,
|
|
BOUNDING_BOX = FILL_COLOR << 1
|
|
};
|
|
|
|
Path *_path_element;
|
|
|
|
mutable VGPath _path {VG_INVALID_HANDLE};
|
|
mutable VGPaint _paint {VG_INVALID_HANDLE};
|
|
mutable VGPaint _paint_fill {VG_INVALID_HANDLE};
|
|
mutable uint32_t _attributes_dirty {~0u};
|
|
|
|
CmdList _cmds;
|
|
CoordList _coords;
|
|
|
|
VGbitfield _mode {0};
|
|
osg::Vec4f _fill_color;
|
|
VGFillRule _fill_rule {VG_EVEN_ODD};
|
|
osg::Vec4f _stroke_color;
|
|
VGfloat _stroke_width {1};
|
|
std::vector<VGfloat> _stroke_dash;
|
|
VGCapStyle _stroke_linecap {VG_CAP_BUTT};
|
|
VGJoinStyle _stroke_linejoin {VG_JOIN_MITER};
|
|
|
|
osg::Vec3f transformPoint( const osg::Matrix& m,
|
|
osg::Vec2f pos ) const
|
|
{
|
|
return osg::Vec3
|
|
(
|
|
m(0, 0) * pos[0] + m(1, 0) * pos[1] + m(3, 0),
|
|
m(0, 1) * pos[0] + m(1, 1) * pos[1] + m(3, 1),
|
|
0
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Initialize/Update the OpenVG path
|
|
*/
|
|
void update()
|
|
{
|
|
if( !vgHasContextSH() )
|
|
return;
|
|
|
|
if( _attributes_dirty & PATH )
|
|
{
|
|
const VGbitfield caps = VG_PATH_CAPABILITY_APPEND_TO
|
|
| VG_PATH_CAPABILITY_MODIFY
|
|
| VG_PATH_CAPABILITY_PATH_BOUNDS;
|
|
|
|
if( _path == VG_INVALID_HANDLE )
|
|
_path = vgCreatePath(
|
|
VG_PATH_FORMAT_STANDARD,
|
|
VG_PATH_DATATYPE_F,
|
|
1.f, 0.f, // scale,bias
|
|
_cmds.size(), _coords.size(),
|
|
caps
|
|
);
|
|
else
|
|
vgClearPath(_path, caps);
|
|
|
|
if( !_cmds.empty() && !_coords.empty() )
|
|
vgAppendPathData(_path, _cmds.size(), &_cmds[0], &_coords[0]);
|
|
|
|
_attributes_dirty &= ~PATH;
|
|
_attributes_dirty |= BOUNDING_BOX;
|
|
}
|
|
|
|
if( _attributes_dirty & BOUNDING_BOX )
|
|
{
|
|
dirtyBound();
|
|
|
|
// Recalculate bounding box now (prevent race condition)
|
|
getBound();
|
|
}
|
|
}
|
|
|
|
struct PathUpdateCallback:
|
|
public osg::Drawable::UpdateCallback
|
|
{
|
|
virtual void update(osg::NodeVisitor*, osg::Drawable* drawable)
|
|
{
|
|
static_cast<PathDrawable*>(drawable)->update();
|
|
}
|
|
};
|
|
};
|
|
|
|
//----------------------------------------------------------------------------
|
|
const std::string Path::TYPE_NAME = "path";
|
|
|
|
//----------------------------------------------------------------------------
|
|
void Path::staticInit()
|
|
{
|
|
if( isInit<Path>() )
|
|
return;
|
|
|
|
PathDrawableRef Path::*path = &Path::_path;
|
|
|
|
addStyle("fill", "color", &PathDrawable::setFill, path);
|
|
addStyle("fill-rule", "", &PathDrawable::setFillRule, path);
|
|
addStyle("stroke", "color", &PathDrawable::setStroke, path);
|
|
addStyle("stroke-width", "numeric", &PathDrawable::setStrokeWidth, path);
|
|
addStyle("stroke-dasharray", "", &PathDrawable::setStrokeDashArray, path);
|
|
addStyle("stroke-linecap", "", &PathDrawable::setStrokeLinecap, path);
|
|
addStyle("stroke-linejoin", "", &PathDrawable::setStrokeLinejoin, path);
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
Path::Path( const CanvasWeakPtr& canvas,
|
|
const SGPropertyNode_ptr& node,
|
|
const Style& parent_style,
|
|
ElementWeakPtr parent ):
|
|
Element(canvas, node, parent_style, parent),
|
|
_path( new PathDrawable(this) ),
|
|
_hasSVG(false),
|
|
_hasRect(false)
|
|
{
|
|
staticInit();
|
|
|
|
setDrawable(_path);
|
|
setupStyle();
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
Path::~Path()
|
|
{
|
|
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
osg::BoundingBox Path::getTransformedBounds(const osg::Matrix& m) const
|
|
{
|
|
return _path->getTransformedBounds(m);
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
Path& Path::addSegment(uint8_t cmd, std::initializer_list<float> coords)
|
|
{
|
|
_node->addChild("cmd")->setIntValue(cmd);
|
|
|
|
for(float coord: coords)
|
|
_node->addChild("coord")->setFloatValue(coord);
|
|
|
|
return *this;
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
Path& Path::moveTo(float x_abs, float y_abs)
|
|
{
|
|
return addSegment(VG_MOVE_TO_ABS, {x_abs, y_abs});
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
Path& Path::move(float x_rel, float y_rel)
|
|
{
|
|
return addSegment(VG_MOVE_TO_REL, {x_rel, y_rel});
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
Path& Path::lineTo(float x_abs, float y_abs)
|
|
{
|
|
return addSegment(VG_LINE_TO_ABS, {x_abs, y_abs});
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
Path& Path::line(float x_rel, float y_rel)
|
|
{
|
|
return addSegment(VG_LINE_TO_REL, {x_rel, y_rel});
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
Path& Path::horizTo(float x_abs)
|
|
{
|
|
return addSegment(VG_HLINE_TO_ABS, {x_abs});
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
Path& Path::horiz(float x_rel)
|
|
{
|
|
return addSegment(VG_HLINE_TO_REL, {x_rel});
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
Path& Path::vertTo(float y_abs)
|
|
{
|
|
return addSegment(VG_VLINE_TO_ABS, {y_abs});
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
Path& Path::vert(float y_rel)
|
|
{
|
|
return addSegment(VG_VLINE_TO_REL, {y_rel});
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
Path& Path::close()
|
|
{
|
|
return addSegment(VG_CLOSE_PATH);
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
void Path::childRemoved(SGPropertyNode* child)
|
|
{
|
|
childChanged(child);
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
void Path::setSVGPath(const std::string& svgPath)
|
|
{
|
|
_node->setStringValue("svg", svgPath);
|
|
_hasSVG = true;
|
|
_attributes_dirty |= SVG;
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
void Path::setRect(const SGRect<float> &r)
|
|
{
|
|
_rect = r;
|
|
_hasRect = true;
|
|
_attributes_dirty |= RECT;
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
void Path::setRoundRect(const SGRect<float> &r, float radiusX, float radiusY)
|
|
{
|
|
if (radiusY < 0.0) {
|
|
radiusY = radiusX;
|
|
}
|
|
|
|
setRect(r);
|
|
_node->getChild("border-radius", 0, true)->setDoubleValue(radiusX);
|
|
_node->getChild("border-radius", 1, true)->setDoubleValue(radiusY);
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
void Path::updateImpl(double dt)
|
|
{
|
|
Element::updateImpl(dt);
|
|
|
|
if( _attributes_dirty & (CMDS | COORDS) )
|
|
{
|
|
_path->setSegments
|
|
(
|
|
_node->getChildValues<VGubyte, int>("cmd"),
|
|
_node->getChildValues<VGfloat, float>("coord")
|
|
);
|
|
|
|
_attributes_dirty &= ~(CMDS | COORDS);
|
|
}
|
|
|
|
// SVG path overrides manual cmd/coord specification
|
|
if( _hasSVG && (_attributes_dirty & SVG) )
|
|
{
|
|
CmdList cmds;
|
|
CoordList coords;
|
|
parseSVGPathToVGPath(_node->getStringValue("svg"), cmds, coords);
|
|
_path->setSegments(cmds, coords);
|
|
_attributes_dirty &= ~SVG;
|
|
}
|
|
|
|
if( _hasRect &&(_attributes_dirty & RECT) )
|
|
{
|
|
parseRectToVGPath();
|
|
_attributes_dirty &= ~RECT;
|
|
}
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
void Path::childChanged(SGPropertyNode* child)
|
|
{
|
|
const std::string& name = child->getNameString();
|
|
const std::string &prName = child->getParent()->getNameString();
|
|
|
|
if( strutils::starts_with(name, "border-") )
|
|
{
|
|
_attributes_dirty |= RECT;
|
|
return;
|
|
}
|
|
|
|
if (prName == "rect")
|
|
{
|
|
_hasRect = true;
|
|
if (name == "left") {
|
|
_rect.setLeft(child->getDoubleValue());
|
|
} else if (name == "top") {
|
|
_rect.setTop(child->getDoubleValue());
|
|
} else if (name == "right") {
|
|
_rect.setRight(child->getDoubleValue());
|
|
} else if (name == "bottom") {
|
|
_rect.setBottom(child->getDoubleValue());
|
|
} else if (name == "width") {
|
|
_rect.setWidth(child->getDoubleValue());
|
|
} else if (name == "height") {
|
|
_rect.setHeight(child->getDoubleValue());
|
|
}
|
|
_attributes_dirty |= RECT;
|
|
return;
|
|
}
|
|
|
|
if( child->getParent() != _node )
|
|
return;
|
|
|
|
if( name == "cmd" )
|
|
_attributes_dirty |= CMDS;
|
|
else if( name == "coord" )
|
|
_attributes_dirty |= COORDS;
|
|
else if ( name == "svg")
|
|
_hasSVG = true;
|
|
_attributes_dirty |= SVG;
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
std::vector<float> splitAndConvert(const char del[], const std::string& str)
|
|
{
|
|
std::vector<float> values;
|
|
size_t pos = 0;
|
|
for(;;)
|
|
{
|
|
pos = str.find_first_not_of(del, pos);
|
|
if( pos == std::string::npos )
|
|
break;
|
|
|
|
char *end = 0;
|
|
float val = strtod(&str[pos], &end);
|
|
if( end == &str[pos] || !end )
|
|
break;
|
|
|
|
values.push_back(val);
|
|
pos = end - &str[0];
|
|
}
|
|
return values;
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
void operator+=(CoordList& base, const std::initializer_list<VGfloat>& other)
|
|
{
|
|
base.insert(base.end(), other.begin(), other.end());
|
|
}
|
|
|
|
//----------------------------------------------------------------------------
|
|
void Path::parseRectToVGPath()
|
|
{
|
|
CmdList commands;
|
|
CoordList coords;
|
|
commands.reserve(4);
|
|
coords.reserve(8);
|
|
|
|
bool haveCorner = false;
|
|
SGVec2f topLeft = parseRectCornerRadius(_node, "left", "top", haveCorner);
|
|
if (haveCorner) {
|
|
commands.push_back(VG_MOVE_TO_ABS);
|
|
coords += {_rect.l(), _rect.t() + topLeft.y()};
|
|
commands.push_back(VG_SCCWARC_TO_REL);
|
|
coords += {topLeft.x(), topLeft.y(), 0.0, topLeft.x(), -topLeft.y()};
|
|
} else {
|
|
commands.push_back(VG_MOVE_TO_ABS);
|
|
coords += {_rect.l(), _rect.t()};
|
|
}
|
|
|
|
SGVec2f topRight = parseRectCornerRadius(_node, "right", "top", haveCorner);
|
|
if (haveCorner) {
|
|
commands.push_back(VG_HLINE_TO_ABS);
|
|
coords += {_rect.r() - topRight.x()};
|
|
commands.push_back(VG_SCCWARC_TO_REL);
|
|
coords += {topRight.x(), topRight.y(), 0.0, topRight.x(), topRight.y()};
|
|
} else {
|
|
commands.push_back(VG_HLINE_TO_ABS);
|
|
coords += {_rect.r()};
|
|
}
|
|
|
|
SGVec2f bottomRight = parseRectCornerRadius(_node, "right", "bottom", haveCorner);
|
|
if (haveCorner) {
|
|
commands.push_back(VG_VLINE_TO_ABS);
|
|
coords += {_rect.b() - bottomRight.y()};
|
|
commands.push_back(VG_SCCWARC_TO_REL);
|
|
coords += {bottomRight.x(), bottomRight.y(), 0.0, -bottomRight.x(), bottomRight.y()};
|
|
} else {
|
|
commands.push_back(VG_VLINE_TO_ABS);
|
|
coords += {_rect.b()};
|
|
}
|
|
|
|
SGVec2f bottomLeft = parseRectCornerRadius(_node, "left", "bottom", haveCorner);
|
|
if (haveCorner) {
|
|
commands.push_back(VG_HLINE_TO_ABS);
|
|
coords += {_rect.l() + bottomLeft.x()};
|
|
commands.push_back(VG_SCCWARC_TO_REL);
|
|
coords += {bottomLeft.x(), bottomLeft.y(), 0.0, -bottomLeft.x(), -bottomLeft.y()};
|
|
} else {
|
|
commands.push_back(VG_HLINE_TO_ABS);
|
|
coords += {_rect.l()};
|
|
}
|
|
|
|
commands.push_back(VG_CLOSE_PATH);
|
|
_path->setSegments(commands, coords);
|
|
}
|
|
|
|
} // namespace canvas
|
|
} // namespace simgear
|