diff --git a/addons/ofxSvg/addon_config.mk b/addons/ofxSvg/addon_config.mk index bbd68296e55..1774fa56a15 100644 --- a/addons/ofxSvg/addon_config.mk +++ b/addons/ofxSvg/addon_config.mk @@ -16,8 +16,8 @@ meta: ADDON_NAME = ofxSvg - ADDON_DESCRIPTION = Addon for parsing svg files into ofPaths - ADDON_AUTHOR = Joshua Noble, maintained by OF Team + ADDON_DESCRIPTION = Addon for parsing, manipulating and saving svg files. + ADDON_AUTHOR = Nick Hardeman, original by Joshua Noble, maintained by OF Team ADDON_TAGS = "svg" ADDON_URL = http://github.com/openframeworks/openFrameworks @@ -59,30 +59,3 @@ common: # when parsing the file system looking for libraries exclude this for all or # a specific platform # ADDON_LIBS_EXCLUDE = - -osx: - ADDON_LIBS = libs/svgtiny/lib/macos/svgtiny.xcframework/macos-arm64_x86_64/libsvgtiny.a - ADDON_LIBS += libs/libxml2/lib/macos/libxml2.xcframework/macos-arm64_x86_64/libxml2.a - -ios: - ADDON_LIBS = libs/svgtiny/lib/ios/svgtiny.a - ADDON_LIBS += libs/libxml2/lib/ios/xml2.a - -linux64: - ADDON_LIBS = libs/svgtiny/lib/linux64/libsvgtiny.a - ADDON_LIBS += libs/libxml2/lib/linux64/libxml2.a - -linuxarmv6l: - ADDON_LIBS = libs/svgtiny/lib/linuxarmv6l/libsvgtiny.a - ADDON_LIBS += libs/libxml2/lib/linuxarmv6l/libxml2.a - -linuxarmv7l: - ADDON_LIBS = libs/svgtiny/lib/linuxarmv7l/libsvgtiny.a - ADDON_LIBS += libs/libxml2/lib/linuxarmv7l/libxml2.a - -linuxaarch64: - ADDON_LIBS = libs/svgtiny/lib/linuxaarch64/libsvgtiny.a - ADDON_LIBS += libs/libxml2/lib/linuxaarch64/libxml2.a - -msys2: - ADDON_PKG_CONFIG_LIBRARIES = libxml-2.0 diff --git a/addons/ofxSvg/src/ofxSvg.cpp b/addons/ofxSvg/src/ofxSvg.cpp old mode 100644 new mode 100755 index bbbf54024f1..ee7bea08c82 --- a/addons/ofxSvg/src/ofxSvg.cpp +++ b/addons/ofxSvg/src/ofxSvg.cpp @@ -1,245 +1,2544 @@ -#include "ofConstants.h" +// +// ofxSvgParser.cpp +// +// Created by Nick Hardeman on 8/31/24. +// + #include "ofxSvg.h" -#include +#include "ofUtils.h" +#include +#include "ofGraphics.h" using std::string; +using std::vector; +using std::shared_ptr; + +ofPath ofxSvg::sDummyPath; + +struct Measurement { + double value; + std::string unit; +}; -extern "C" { -#include "svgtiny.h" +//-------------------------------------------------------------- +Measurement parseMeasurement(const std::string& input) { + + if( input.empty() ) { + Measurement result; + result.value = 0.0; + result.unit = ""; + return result; + } + + size_t i = 0; + + // Extract numeric part + while (i < input.size() && (std::isdigit(input[i]) || input[i] == '.' || input[i] == '-')) { + i++; + } + + Measurement result; + result.value = std::stod(input.substr(0, i)); // Convert number part to double + result.unit = input.substr(i); // Extract the unit part + + return result; } -ofxSvg::ofxSvg(const of::filesystem::path & fileName) { - load(fileName); +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::clone( const std::shared_ptr& aele ) { + if (aele) { + if( aele->getType() == ofxSvgType::TYPE_ELEMENT ) { + return std::make_shared(*aele); + } else if( aele->getType() == ofxSvgType::TYPE_GROUP ) { + auto pg = std::dynamic_pointer_cast(aele); + return std::make_shared(*pg); + } else if( aele->getType() == ofxSvgType::TYPE_RECTANGLE ) { + auto pg = std::dynamic_pointer_cast(aele); + return std::make_shared(*pg); + } else if( aele->getType() == ofxSvgType::TYPE_IMAGE ) { + auto pg = std::dynamic_pointer_cast(aele); + return std::make_shared(*pg); + } else if( aele->getType() == ofxSvgType::TYPE_ELLIPSE ) { + auto pg = std::dynamic_pointer_cast(aele); + return std::make_shared(*pg); + } else if( aele->getType() == ofxSvgType::TYPE_CIRCLE ) { + auto pg = std::dynamic_pointer_cast(aele); + return std::make_shared(*pg); + } else if( aele->getType() == ofxSvgType::TYPE_PATH ) { + auto pg = std::dynamic_pointer_cast(aele); + return std::make_shared(*pg); + } else if( aele->getType() == ofxSvgType::TYPE_TEXT ) { + auto pg = std::dynamic_pointer_cast(aele); + return std::make_shared(*pg); + } + } + return std::shared_ptr(); } -float ofxSvg::getWidth() const { - return width; +// Function to deep copy a vector of shared_ptrs +std::vector> ofxSvg::deepCopyVector(const std::vector>& original) { + std::vector> copy; + copy.reserve(original.size()); // Reserve space for efficiency + + for (auto ptr : original) { + if (ptr) { + copy.push_back(clone(ptr)); + } else { + ofLogVerbose("ofxSvg") << "deepCopyVector :: nullptr"; + copy.push_back(std::shared_ptr()); // Preserve nullptr entries + } + } + return copy; } -float ofxSvg::getHeight() const { - return height; +void ofxSvg::deepCopyFrom( const ofxSvg & mom ) { + if( mom.mChildren.size() > 0 ) { + mChildren = deepCopyVector(mom.mChildren); + } + if( mom.mDefElements.size() > 0 ) { + mDefElements = deepCopyVector(mom.mDefElements); + } + + mViewbox = mom.mViewbox; + mBounds = mom.mBounds; + + fontsDirectory = mom.fontsDirectory; + folderPath = mom.folderPath; + svgPath = mom.svgPath; + + mCurrentLayer = mom.mCurrentLayer; + mUnitStr = mom.mUnitStr; + + if(mom.mCurrentSvgCss) { + mCurrentSvgCss = std::make_shared(*mom.mCurrentSvgCss); + } + + mSvgCss = mom.mSvgCss; + mCurrentCss = mom.mCurrentCss; + mFillColor = mom.mFillColor; + mStrokeColor = mom.mStrokeColor; + + mModelMatrix = mom.mModelMatrix; + mModelMatrixStack = mom.mModelMatrixStack; + + mCircleResolution = mom.mCircleResolution; + mCurveResolution = mom.mCurveResolution; + + mPaths = mom.mPaths; } -int ofxSvg::getNumPath() { - return paths.size(); +//-------------------------------------------------------------- +void ofxSvg::moveFrom( ofxSvg&& mom ) { + mChildren = std::move(mom.mChildren); + mDefElements = std::move(mom.mDefElements); + + mViewbox = mom.mViewbox; + mBounds = mom.mBounds; + + fontsDirectory = mom.fontsDirectory; + folderPath = mom.folderPath; + svgPath = mom.svgPath; + + mCurrentLayer = mom.mCurrentLayer; + mUnitStr = mom.mUnitStr; + + mCurrentSvgCss = mom.mCurrentSvgCss; + + mSvgCss = mom.mSvgCss; + mCurrentCss = mom.mCurrentCss; + mFillColor = mom.mFillColor; + mStrokeColor = mom.mStrokeColor; + + mModelMatrix = mom.mModelMatrix; + mModelMatrixStack = mom.mModelMatrixStack; + + mCircleResolution = mom.mCircleResolution; + mCurveResolution = mom.mCurveResolution; + + mPaths = mom.mPaths; } -ofPath & ofxSvg::getPathAt(int n) { - return paths[n]; + + +// Copy constructor (deep copy) +//-------------------------------------------------------------- +ofxSvg::ofxSvg(const ofxSvg & mom) { + clear(); + ofLogVerbose("ofxSvg") << "ofxSvg(const ofxSvg & mom)"; + deepCopyFrom(mom); } -void ofxSvg::load(const of::filesystem::path & fileName) { - of::filesystem::path file = ofToDataPath(fileName); - if (!of::filesystem::exists(file)) { - ofLogError("ofxSVG") << "load(): path does not exist: " << file ; - return; +// Copy assignment operator (deep copy) +//-------------------------------------------------------------- +ofxSvg& ofxSvg::operator=(const ofxSvg& mom) { + if (this != &mom) { + ofLogVerbose("ofxSvg") << "ofxSvg::operator=(const ofxSvg& mom)"; + clear(); + deepCopyFrom(mom); } + return *this; +} - ofBuffer buffer = ofBufferFromFile(fileName); - loadFromString(buffer.getText(), file.string()); +// Move constructor +//-------------------------------------------------------------- +ofxSvg::ofxSvg(ofxSvg && mom) { + ofLogVerbose("ofxSvg") << "ofxSvg(ofxSvg && mom)"; + clear(); + moveFrom(std::move(mom)); } -void ofxSvg::loadFromString(std::string stringdata, std::string urlstring) { +// Move assignment operator +ofxSvg& ofxSvg::operator=(ofxSvg&& mom) { + if (this != &mom) { + ofLogVerbose("ofxSvg") << "ofxSvg::operator=(ofxSvg&& mom)"; + clear(); + moveFrom(std::move(mom)); + } + return *this; +} - // goes some way to improving SVG compatibility - fixSvgString(stringdata); +//-------------------------------------------------------------- +ofxSvg::ofxSvg(const of::filesystem::path & fileName) { + load(fileName); +} - const char * data = stringdata.c_str(); - int size = stringdata.size(); - const char * url = urlstring.c_str(); +//-------------------------------------------------------------- +bool ofxSvg::load( const of::filesystem::path& fileName ) { + ofFile mainXmlFile( fileName, ofFile::ReadOnly ); + ofBuffer tMainXmlBuffer( mainXmlFile ); + + svgPath = fileName; + folderPath = ofFilePath::getEnclosingDirectory( fileName, false ); + + ofXml xml; + if( !xml.load(tMainXmlBuffer )) { + ofLogWarning("ofxSvg") << " unable to load svg from " << fileName; + return false; + } + + return loadFromString(tMainXmlBuffer.getText()); +} + +//-------------------------------------------------------------- +bool ofxSvg::loadFromString(const std::string& data, std::string url) { + clear(); + + ofXml xml; + xml.parse(data); + + if( xml ) { + ofXml svgNode = xml.getFirstChild(); + + validateXmlSvgRoot( svgNode ); + ofXml::Attribute viewBoxAttr = svgNode.getAttribute("viewBox"); + if(svgNode) { + + std::vector values = { + parseMeasurement(svgNode.getAttribute("x").getValue()), + parseMeasurement(svgNode.getAttribute("y").getValue()), + parseMeasurement(svgNode.getAttribute("width").getValue()), + parseMeasurement(svgNode.getAttribute("height").getValue()) + }; + + for( auto& tv : values ) { + if( !tv.unit.empty() ) { + mUnitStr = tv.unit; + } + } + if( mUnitStr.empty() ) { + mUnitStr = "px"; + } + + mBounds.x = values[0].value; + mBounds.y = values[1].value; + mBounds.width = values[2].value; + mBounds.height = values[3].value; + +// mBounds.x = ofToFloat( cleanString( svgNode.getAttribute("x").getValue(), "px") ); +// mBounds.y = ofToFloat( cleanString( svgNode.getAttribute("y").getValue(), "px" )); +// mBounds.width = ofToFloat( cleanString( svgNode.getAttribute("width").getValue(), "px" )); +// mBounds.height = ofToFloat( cleanString( svgNode.getAttribute("height").getValue(), "px" )); + mViewbox = mBounds; + } + + if( viewBoxAttr ) { + string tboxstr = viewBoxAttr.getValue(); + vector< string > tvals = ofSplitString( tboxstr, " " ); + if( tvals.size() == 4 ) { + mViewbox.x = ofToFloat(tvals[0] ); + mViewbox.y = ofToFloat( tvals[1] ); + mViewbox.width = ofToFloat( tvals[2] ); + mViewbox.height = ofToFloat( tvals[3] ); + } + } + + if(svgNode) { + ofLogVerbose("ofxSvg") << svgNode.findFirst("style").toString() << " bounds: " << mBounds; + } else { + ofLogVerbose("ofxSvg") << __FUNCTION__ << " : NO svgNode: "; + } + + + ofXml styleXmlNode = svgNode.findFirst("//style"); + if( styleXmlNode ) { + ofLogVerbose("ofxSvg") << __FUNCTION__ << " : STYLE NODE" << styleXmlNode.getAttribute("type").getValue() << " string: " << styleXmlNode.getValue(); + + mSvgCss.parse(styleXmlNode.getValue()); + + ofLogVerbose("ofxSvg") << "-----------------------------"; + ofLogVerbose() << mSvgCss.toString(); + ofLogVerbose("ofxSvg") << "-----------------------------"; + } else { + ofLogVerbose("ofxSvg") << __FUNCTION__ << " : NO STYLE NODE"; + } + + + // the defs are added in the _parseXmlNode function // + _parseXmlNode( svgNode, mChildren ); + + ofLogVerbose("ofxSvg") << " number of defs elements: " << mDefElements.size(); + } + + return true; +} - struct svgtiny_diagram * diagram = svgtiny_create(); - // Switch to "C" locale as svgtiny expect it to parse floating points (issue 6644) - std::locale prev_locale = std::locale::global(std::locale::classic()); - svgtiny_code code = svgtiny_parse(diagram, data, size, url, 0, 0); - // Restore locale - std::locale::global(prev_locale); +//-------------------------------------------------------------- +bool ofxSvg::reload() { + if( svgPath.empty() ) { + ofLogError("ofxSvg") << __FUNCTION__ << " : svg path is empty, please call load with file path before calling reload"; + return false; + } + return load( svgPath ); +} - if (code != svgtiny_OK) { - std::string msg; - switch (code) { - case svgtiny_OUT_OF_MEMORY: - msg = "svgtiny_OUT_OF_MEMORY"; - break; +//-------------------------------------------------------------- +bool ofxSvg::save( of::filesystem::path apath ) { + // https://www.w3.org/TR/SVG2/struct.html#NewDocument + ofXml svgXml; + ofXml svgXmlNode = svgXml.appendChild("svg"); + if( auto vattr = svgXmlNode.appendAttribute("version")) { + vattr.set("1.1"); + } + if( auto vattr = svgXmlNode.appendAttribute("xmlns")) { + vattr.set("http://www.w3.org/2000/svg"); + } + if( auto vattr = svgXmlNode.appendAttribute("xmlns:xlink")) { + vattr.set("http://www.w3.org/1999/xlink"); + } + // + + if( auto vattr = svgXmlNode.appendAttribute("x")) { + vattr.set(ofToString(mBounds.x,2)+mUnitStr); + } + if( auto vattr = svgXmlNode.appendAttribute("y")) { + vattr.set(ofToString(mBounds.y,2)+mUnitStr); + } + if( auto vattr = svgXmlNode.appendAttribute("width")) { + vattr.set(ofToString(mBounds.getWidth(),2)+mUnitStr); + } + if( auto vattr = svgXmlNode.appendAttribute("height")) { + vattr.set(ofToString(mBounds.getHeight(),2)+mUnitStr); + } + if( auto vattr = svgXmlNode.appendAttribute("viewBox")) { + vattr.set(ofToString(mViewbox.x,0)+" "+ofToString(mViewbox.y,0)+" "+ofToString(mViewbox.getWidth(),0)+" "+ofToString(mViewbox.getHeight(),0)); + } + + ofXml cssNode = svgXmlNode.appendChild("style"); + +// if( mCurrentCss.properties.size() > 0 ) { +// mSvgCss.getAddClass(mCurrentCss); +// } + + // now we need to save out the children // + for( auto& kid : mChildren ) { + _toXml( svgXmlNode, kid ); + } + + if( mSvgCss.classes.size() > 0 && cssNode) { + if( auto vattr = cssNode.appendAttribute("type")) { + vattr.set("text/css"); + } + cssNode.set(mSvgCss.toString(false)); + } else { + svgXmlNode.removeChild(cssNode); + } + +// ofLogNotice("Parser::CSS") << mSvgCss.toString(); + + return svgXml.save(apath); +} - /*case svgtiny_LIBXML_ERROR: - msg = "svgtiny_LIBXML_ERROR"; - break;*/ +//-------------------------------------------------------------- +void ofxSvg::clear() { + mChildren.clear(); + mDefElements.clear(); + mCurrentLayer = 0; + mCurrentSvgCss.reset(); + mSvgCss.clear(); + mCPoints.clear(); + mCenterPoints.clear(); + + mCurrentCss.clear(); + + mGroupStack.clear(); + mModelMatrix = glm::mat4(1.f); + mModelMatrixStack = std::stack(); + + mPaths.clear(); +} - case svgtiny_NOT_SVG: - msg = "svgtiny_NOT_SVG"; - break; +//-------------------------------------------------------------- +const int ofxSvg::getTotalLayers(){ + return mCurrentLayer; +} - case svgtiny_SVG_ERROR: - msg = "svgtiny_SVG_ERROR: line " + ofToString(diagram->error_line) + ": " + diagram->error_message; - break; +//-------------------------------------------------------------- +void ofxSvg::recalculateLayers() { + mCurrentLayer = 0; + auto allKids = getAllChildren(true); + for( auto& kid : allKids ) { + kid->layer = mCurrentLayer += 1.0; + } +} - default: - msg = "unknown svgtiny_code " + ofToString(code); - break; +// including these for legacy considerations // +//-------------------------------------------------------------- +int ofxSvg::getNumPath() { + if( mPaths.size() < 1 ) { + getPaths(); + } + return mPaths.size(); +} + +//-------------------------------------------------------------- +ofPath& ofxSvg::getPathAt(int n) { + if( mPaths.size() < 1 ) { + getPaths(); + } + if( n < 0 || n >= mPaths.size() ) { + ofLogWarning("ofxSvg") << "getPathAt: " << n << " out of bounds for number of paths: " << mPaths.size(); + return sDummyPath; + } + return mPaths[n]; +} + +//-------------------------------------------------------------- +const std::vector& ofxSvg::getPaths() const { +// auto spaths = const_cast(this)->getAllElementsForType(); +// std::size_t num = spaths.size(); + if( mPaths.size() < 1 ) { + // previous ofxSvg also included, circles, ellipses, rects, paths, etc. + auto spaths = const_cast(this)->getAllElementsWithPath(); + std::size_t num = spaths.size(); + mPaths.resize(num); + for( std::size_t i = 0; i < num; i++ ) { + mPaths[i] = spaths[i]->path; } - ofLogError("ofxSVG") << "load(): couldn't parse \"" << urlstring << "\": " << msg; } + return mPaths; +} - setupDiagram(diagram); +//-------------------------------------------------------------- +void ofxSvg::setFontsDirectory( string aDir ) { + fontsDirectory = aDir; + if( fontsDirectory.back() != '/' ) { + fontsDirectory += '/'; + } +} - svgtiny_free(diagram); +//-------------------------------------------------------------- +string ofxSvg::toString(int nlevel) { + string tstr = ""; + if( mChildren.size() ) { + for( std::size_t i = 0; i < mChildren.size(); i++ ) { + tstr += mChildren[i]->toString( nlevel ); + } + } + return tstr; } -void ofxSvg::fixSvgString(std::string & xmlstring) { +//-------------------------------------------------------------- +void ofxSvg::validateXmlSvgRoot( ofXml& aRootSvgNode ) { + // if there is no width and height set in the svg base node, svg tiny no likey // + if(aRootSvgNode) { + // check for x, y, width and height // + { + auto xattr = aRootSvgNode.getAttribute("x"); + if( !xattr ) { + auto nxattr = aRootSvgNode.appendAttribute("x"); + if(nxattr) nxattr.set("0px"); + } + } + { + auto yattr = aRootSvgNode.getAttribute("y"); + if( !yattr ) { + auto yattr = aRootSvgNode.appendAttribute("y"); + if( yattr ) yattr.set("0px"); + } + } + + ofXml::Attribute viewBoxAttr = aRootSvgNode.getAttribute("viewBox"); + + ofRectangle vrect; + if( viewBoxAttr ) { + string tboxstr = viewBoxAttr.getValue(); + vector< string > tvals = ofSplitString( tboxstr, " " ); + if( tvals.size() == 4 ) { + vrect.x = ofToFloat( cleanString( tvals[0], "px") ); + vrect.y = ofToFloat( cleanString( tvals[1], "px") ); + vrect.width = ofToFloat( cleanString( tvals[2], "px") ); + vrect.height = ofToFloat( cleanString( tvals[3], "px") ); + } + } + + auto wattr = aRootSvgNode.getAttribute("width"); + auto hattr = aRootSvgNode.getAttribute("height"); + + if( !wattr || !hattr ) { +// ofXml::Attribute viewBoxAttr = aRootSvgNode.getAttribute("viewBox"); + if( vrect.getWidth() > 0.0f && vrect.getHeight() > 0.0f ) { +// string tboxstr = viewBoxAttr.getValue(); +// vector< string > tvals = ofSplitString( tboxstr, " " ); +// if( tvals.size() >= 4 ) { + if( !wattr ) { + auto nwattr = aRootSvgNode.appendAttribute("width"); + if(nwattr) nwattr.set( ofToString(vrect.getWidth())+"px" ); + } + + if( !hattr ) { + auto nhattr = aRootSvgNode.appendAttribute("height"); + if(nhattr) nhattr.set( ofToString(vrect.getHeight())+"px" ); + } +// } + } + } + // from previous version of ofxSvg + // Affinity Designer does not set width/height as pixels but as a percentage + // and relies on the "viewBox" to convey the size of things. this applies the + // viewBox to the width and height. + if( vrect.getWidth() > 0.0f ) { + if(wattr) { + if( ofIsStringInString( wattr.getValue(), "%")) { + float wpct = ofToFloat( cleanString(wattr.getValue(), "%" )) / 100.0f; + wattr.set( ofToString(wpct * vrect.getWidth())+"px" ); + } + } + } + if( vrect.getHeight() > 0.0f ) { + if(hattr) { + if( ofIsStringInString( hattr.getValue(), "%")) { + float hpct = ofToFloat( cleanString(hattr.getValue(), "%" )) / 100.0f; + hattr.set( ofToString(hpct * vrect.getHeight())+"px" ); + } + } + } + } +} - ofXml xml; +//-------------------------------------------------------------- +string ofxSvg::cleanString( string aStr, string aReplace ) { + ofStringReplace( aStr, aReplace, ""); + return aStr; +} - xml.parse(xmlstring); +//-------------------------------------------------------------- +void ofxSvg::_parseXmlNode( ofXml& aParentNode, vector< shared_ptr >& aElements ) { + + auto kids = aParentNode.getChildren(); + for( auto& kid : kids ) { + if( kid.getName() == "g" ) { + auto fkid = kid.getFirstChild(); + if( fkid ) { + mCurrentSvgCss.reset(); + auto tgroup = std::make_shared(); + tgroup->layer = mCurrentLayer += 1.0; + auto idattr = kid.getAttribute("id"); + if( idattr ) { + tgroup->name = idattr.getValue(); + } + + mCurrentSvgCss = std::make_shared( _parseStyle(kid) ); + + aElements.push_back( tgroup ); + _parseXmlNode( kid, tgroup->getChildren() ); + } + } else if( kid.getName() == "defs") { + ofLogVerbose("ofxSvg") << __FUNCTION__ << " found a defs node."; + _parseXmlNode(kid, mDefElements ); + } else { + + bool bAddOk = _addElementFromXmlNode( kid, aElements ); +// cout << "----------------------------------" << endl; +// cout << kid.getName() << " kid: " << kid.getAttribute("id").getValue() << " out xml: " << txml.toString() << endl; + } + } +} - // so it turns out that if the stroke width is <1 it rounds it down to 0, - // and makes it disappear because svgtiny stores strokewidth as an integer! - ofXml::Search strokeWidthElements = xml.find("//*[@stroke-width]"); - if (!strokeWidthElements.empty()) { - for (ofXml & element : strokeWidthElements) { - //cout << element.toString() << endl; - float strokewidth = element.getAttribute("stroke-width").getFloatValue(); - strokewidth = std::fmax(1.0, std::round(strokewidth)); - element.getAttribute("stroke-width").set(strokewidth); +//-------------------------------------------------------------- +bool ofxSvg::_addElementFromXmlNode( ofXml& tnode, vector< shared_ptr >& aElements ) { + shared_ptr telement; + + if( tnode.getName() == "use") { + if( auto hrefAtt = tnode.getAttribute("xlink:href")) { + ofLogVerbose("ofxSvg") << "found a use node with href " << hrefAtt.getValue(); + std::string href = hrefAtt.getValue(); + if( href.size() > 1 && href[0] == '#' ) { + // try to find by id + href = href.substr(1, std::string::npos); + ofLogVerbose("ofxSvg") << "going to look for href " << href; + for( auto & def : mDefElements ) { + if( def->name == href ) { + telement = clone(def); + if( !telement ) { + ofLogWarning("Parser") << "could not find type for def : " << def->name; + } + break; + } + } + } else { + ofLogWarning("ofxSvg") << "could not parse use node with href : " << href; + } + } else { + ofLogWarning("ofxSvg") << "found a use node but no href!"; } + } else if( tnode.getName() == "image" ) { + auto image = std::make_shared(); + auto wattr = tnode.getAttribute("width"); + if(wattr) image->width = wattr.getFloatValue(); + auto hattr = tnode.getAttribute("height"); + if(hattr) image->height = hattr.getFloatValue(); + auto xlinkAttr = tnode.getAttribute("xlink:href"); + if( xlinkAttr ) { + image->filepath = folderPath; + image->filepath.append(xlinkAttr.getValue()); +// image->filepath = folderPath+xlinkAttr.getValue(); + } + telement = image; + + } else if( tnode.getName() == "ellipse" ) { + auto ellipse = std::make_shared(); + auto cxAttr = tnode.getAttribute("cx"); + if(cxAttr) ellipse->pos.x = cxAttr.getFloatValue(); + auto cyAttr = tnode.getAttribute("cy"); + if(cyAttr) ellipse->pos.y = cyAttr.getFloatValue(); + + auto rxAttr = tnode.getAttribute( "rx" ); + if(rxAttr) ellipse->radiusX = rxAttr.getFloatValue(); + auto ryAttr = tnode.getAttribute( "ry" ); + if(ryAttr) ellipse->radiusY = ryAttr.getFloatValue(); + + ellipse->path.setCircleResolution(mCircleResolution); + ellipse->path.setCurveResolution(mCurveResolution); + // make local so we can apply transform later in the function + ellipse->path.ellipse({0.f,0.f}, ellipse->radiusX * 2.0f, ellipse->radiusY * 2.0f ); + + _applyStyleToPath( tnode, ellipse ); + + telement = ellipse; + } else if( tnode.getName() == "circle" ) { + auto circle = std::make_shared(); + auto cxAttr = tnode.getAttribute("cx"); + if(cxAttr) circle->pos.x = cxAttr.getFloatValue(); + auto cyAttr = tnode.getAttribute("cy"); + if(cyAttr) circle->pos.y = cyAttr.getFloatValue(); + + auto rAttr = tnode.getAttribute( "r" ); + if(rAttr) circle->radius = rAttr.getFloatValue(); + + // make local so we can apply transform later in the function + // position is from the top left + circle->path.setCircleResolution(mCircleResolution); + circle->path.setCurveResolution(mCurveResolution); + circle->path.circle({0.f,0.f}, circle->radius ); + + _applyStyleToPath( tnode, circle ); + + telement = circle; + + } else if( tnode.getName() == "line" ) { + auto telePath = std::make_shared(); + + glm::vec3 p1 = {0.f, 0.f, 0.f}; + glm::vec3 p2 = {0.f, 0.f, 0.f}; + auto x1Attr = tnode.getAttribute("x1"); + if(x1Attr) p1.x = x1Attr.getFloatValue(); + auto y1Attr = tnode.getAttribute("y1"); + if(y1Attr) p1.y = y1Attr.getFloatValue(); + + auto x2Attr = tnode.getAttribute("x2"); + if(x2Attr) p2.x = x2Attr.getFloatValue(); + auto y2Attr = tnode.getAttribute("y2"); + if(y2Attr) p2.y = y2Attr.getFloatValue(); + + // set the colors and stroke width, etc. + telePath->path.clear(); + telePath->path.moveTo(p1); + telePath->path.lineTo(p2); + + _applyStyleToPath( tnode, telePath ); + + telement = telePath; + + } else if(tnode.getName() == "polyline" || tnode.getName() == "polygon") { + auto tpath = std::make_shared(); + _parsePolylinePolygon(tnode, tpath); + _applyStyleToPath( tnode, tpath ); + telement = tpath; + } else if( tnode.getName() == "path" ) { + auto tpath = std::make_shared(); + _parsePath( tnode, tpath ); + _applyStyleToPath( tnode, tpath ); + telement = tpath; + } else if( tnode.getName() == "rect" ) { + auto rect = std::make_shared(); + auto xattr = tnode.getAttribute("x"); + if(xattr) rect->rectangle.x = xattr.getFloatValue(); + auto yattr = tnode.getAttribute("y"); + if(yattr) rect->rectangle.y = yattr.getFloatValue(); + auto wattr = tnode.getAttribute("width"); + if(wattr) rect->rectangle.width = wattr.getFloatValue(); + auto hattr = tnode.getAttribute("height"); + if(hattr) rect->rectangle.height = hattr.getFloatValue(); + rect->pos.x = rect->rectangle.x; + rect->pos.y = rect->rectangle.y; + + auto rxAttr = tnode.getAttribute("rx"); + auto ryAttr = tnode.getAttribute("ry"); + + rect->path.setCircleResolution(mCircleResolution); + rect->path.setCurveResolution(mCurveResolution); + + // make local so we can apply transform later in the function + if( !ofxSvgCssClass::sIsNone(rxAttr.getValue()) || !ofxSvgCssClass::sIsNone(ryAttr.getValue())) { + + rect->round = std::max(ofxSvgCssClass::sGetFloat(rxAttr.getValue()), + ofxSvgCssClass::sGetFloat(ryAttr.getValue())); + + rect->path.rectRounded(0.f, 0.f, rect->rectangle.getWidth(), rect->rectangle.getHeight(), + rect->round + ); + + } else { + rect->path.rectangle(0.f, 0.f, rect->getWidth(), rect->getHeight()); + } + + telement = rect; + + _applyStyleToPath( tnode, rect ); + + // this shouldn't be drawn at all, may be a rect that for some reason is generated + // by text blocks // + if( !rect->isFilled() && !rect->hasStroke() ) { + telement->setVisible(false); + } + + } else if( tnode.getName() == "text" ) { + auto text = std::make_shared(); + telement = text; +// std::cout << "has kids: " << tnode.getFirstChild() << " node value: " << tnode.getValue() << std::endl; + if( tnode.getFirstChild() ) { + + auto kids = tnode.getChildren(); + for( auto& kid : kids ) { + if(kid) { + if( kid.getName() == "tspan" ) { + text->textSpans.push_back( _getTextSpanFromXmlNode( kid ) ); + } + } + } + + // this may not be a text block or it may have no text // + if( text->textSpans.size() == 0 ) { + text->textSpans.push_back( _getTextSpanFromXmlNode( tnode ) ); + } + } + + string tempFolderPath = ofFilePath::addTrailingSlash(folderPath); + if( ofDirectory::doesDirectoryExist( tempFolderPath+"fonts/" )) { + text->setFontDirectory( tempFolderPath+"fonts/" ); + } + if( fontsDirectory != "" ) { + if( ofDirectory::doesDirectoryExist(fontsDirectory)) { + text->setFontDirectory(fontsDirectory); + } + } + + } else if( tnode.getName() == "g" ) { + + } + + if( !telement ) { + return false; + } + + auto idAttr = tnode.getAttribute("id"); + if( idAttr ) { + telement->name = idAttr.getValue(); + } + + if( telement->getType() == ofxSvgType::TYPE_RECTANGLE || telement->getType() == ofxSvgType::TYPE_IMAGE || telement->getType() == ofxSvgType::TYPE_TEXT || telement->getType() == ofxSvgType::TYPE_CIRCLE || telement->getType() == ofxSvgType::TYPE_ELLIPSE ) { + auto transAttr = tnode.getAttribute("transform"); + if( transAttr ) { +// getTransformFromSvgMatrix( transAttr.getValue(), telement->pos, telement->scale.x, telement->scale.y, telement->rotation ); + setTransformFromSvgMatrixString( transAttr.getValue(), telement ); + } + + std::vector typesToApplyTransformToPath = { + ofxSvgType::TYPE_RECTANGLE, + ofxSvgType::TYPE_CIRCLE, + ofxSvgType::TYPE_ELLIPSE + }; + + bool bApplyTransformToPath = false; + for( auto & etype : typesToApplyTransformToPath ) { + if( etype == telement->getType() ) { + bApplyTransformToPath = true; + break; + } + } + + if( bApplyTransformToPath ) { + auto epath = std::dynamic_pointer_cast( telement ); + auto outlines = epath->path.getOutline(); + auto transform = epath->getTransformMatrix(); + for( auto& outline : outlines ) { + for( auto& v : outline ) { + v = transform * glm::vec4(v, 1.0f); + } + } + // now we have new outlines, what do we do? + epath->path.clear(); + bool bFirstOne = true; + for( auto& outline : outlines ) { + for( auto& v : outline ) { + if(bFirstOne) { + bFirstOne = false; + epath->path.moveTo(v); + } else { + epath->path.lineTo(v); + } + } + if( outline.isClosed() ) { + epath->path.close(); + } + } + } + } + + if( telement->getType() == ofxSvgType::TYPE_TEXT ) { + auto text = std::dynamic_pointer_cast( telement ); + text->ogPos = text->pos; + text->create(); + } + + _applyStyleToElement(tnode, telement); + + telement->layer = mCurrentLayer += 1.0; + aElements.push_back( telement ); + return true; +} + +std::vector parsePoints(const std::string& input) { + std::vector points; + std::regex regex("[-]?\\d*\\.?\\d+"); // Matches positive/negative floats + std::sregex_iterator begin(input.begin(), input.end(), regex), end; + + std::vector values; + + // Extract all floating-point values using regex + for (std::sregex_iterator i = begin; i != end; ++i) { + try { + values.push_back(std::stof((*i).str())); + } catch (const std::invalid_argument&) { + std::cerr << "Invalid number found: " << (*i).str() << std::endl; + } + } + + // Create vec2 pairs from the values + for (size_t i = 0; i < values.size(); i += 2) { + if (i + 1 < values.size()) { + glm::vec3 point(values[i], values[i + 1], 0.f); + points.push_back(point); + } + } + + if( values.size() == 1 && points.size() < 1) { + glm::vec3 point(values[0], values[0], 0.f); + points.push_back(point); } + + return points; +} - // Affinity Designer does not set width/height as pixels but as a percentage - // and relies on the "viewBox" to convey the size of things. this applies the - // viewBox to the width and height. - std::vector rect; - for (ofXml & element : xml.find("//*[@viewBox]")) { - rect = ofSplitString(element.getAttribute("viewBox").getValue(), " "); +//---------------------------------------------------- +std::vector _parseSvgArc(const std::string& arcStr) { + std::vector result; + std::regex numberRegex(R"([-+]?[0-9]*\.?[0-9]+)"); // Improved regex for better compatibility + std::sregex_iterator iter(arcStr.begin(), arcStr.end(), numberRegex); + std::sregex_iterator end; + + while (iter != end) { + try { + result.push_back(std::stod(iter->str())); + } catch (const std::exception& e) { + std::cerr << "Error parsing number: " << iter->str() << " - " << e.what() << std::endl; + } + ++iter; } + + return result; +} - if (rect.size() == 4) { +//---------------------------------------------------- +int _getWindingOrderOnArc( glm::vec3& aStartPos, glm::vec3& aCenterPos, glm::vec3& aendPos ) { + glm::vec3 sdiff = (aStartPos - aCenterPos); + if( glm::length2(sdiff) > 0.0f ) { + sdiff = glm::normalize(sdiff); + } + glm::vec3 ediff = (aendPos - aCenterPos); + if( glm::length2(ediff) > 0.0f ) { + ediff = glm::normalize(ediff); + } + float tcross = sdiff.x * ediff.y - sdiff.y * ediff.x; +// ofLogNotice("_getWindingOrderOnArc") << "tcross is " << tcross; + if( tcross > 0.0f ) { + // clockwise + return 1; + } else if( tcross < 0.0f ) { + // counter clockwise + return -1; + } + // co-linear + return 0; + +} - for (ofXml & element : xml.find("//*[@width]")) { - if (element.getAttribute("width").getValue() == "100%") { - auto w = ofToFloat(rect.at(2)); - ofLogWarning("ofxSvg::fixSvgString()") << "the SVG size is provided as percentage, which svgtiny translates to 0. The width is corrected from the viewBox width: " << w; - element.getAttribute("width").set(w); +//---------------------------------------------------- +// Function to find the center of the elliptical arc from SVG arc parameters +glm::vec2 findArcCenter(glm::vec2 start, glm::vec2 end, double rx, double ry, double x_axis_rotation, bool large_arc_flag, bool sweep_flag) { + // Convert the rotation to radians + double phi = glm::radians(x_axis_rotation); + double cos_phi = cos(phi); + double sin_phi = sin(phi); + + // Step 1: Compute (x1', y1') - the coordinates of the start point in the transformed coordinate system + glm::vec2 diff = (start - end) / 2.0f; + glm::vec2 p1_prime(cos_phi * diff.x + sin_phi * diff.y, -sin_phi * diff.x + cos_phi * diff.y); + + // Step 2: Correct radii if necessary + double p1_prime_x_sq = p1_prime.x * p1_prime.x; + double p1_prime_y_sq = p1_prime.y * p1_prime.y; + double rx_sq = rx * rx; + double ry_sq = ry * ry; + double radii_check = p1_prime_x_sq / rx_sq + p1_prime_y_sq / ry_sq; + if (radii_check > 1) { + // Scale radii to ensure the arc can fit between the two points + double scale = std::sqrt(radii_check); + rx *= scale; + ry *= scale; + rx_sq = rx * rx; + ry_sq = ry * ry; + } + + // Step 3: Compute (cx', cy') - the center point in the transformed coordinate system + double factor_numerator = rx_sq * ry_sq - rx_sq * p1_prime_y_sq - ry_sq * p1_prime_x_sq; + double factor_denominator = rx_sq * p1_prime_y_sq + ry_sq * p1_prime_x_sq; + if (factor_numerator < 0) { + factor_numerator = 0; // Precision error correction to avoid sqrt of negative numbers + } + + double factor = std::sqrt(factor_numerator / factor_denominator); + if (large_arc_flag == sweep_flag) { + factor = -factor; + } + + glm::vec2 center_prime(factor * rx * p1_prime.y / ry, factor * -ry * p1_prime.x / rx); + + // Step 4: Compute the center point in the original coordinate system + glm::vec2 center( + cos_phi * center_prime.x - sin_phi * center_prime.y + (start.x + end.x) / 2.0, + sin_phi * center_prime.x + cos_phi * center_prime.y + (start.y + end.y) / 2.0 + ); + + return center; +} + + +//-------------------------------------------------------------- +void ofxSvg::_parsePolylinePolygon( ofXml& tnode, std::shared_ptr aSvgPath ) { + auto pointsAttr = tnode.getAttribute("points"); + if( !pointsAttr ) { + ofLogWarning("ofxSvg") << __FUNCTION__ << " polyline or polygon does not have a points attriubute."; + return; + } + + if( pointsAttr.getValue().empty() ) { + ofLogWarning("ofxSvg") << __FUNCTION__ << " polyline or polygon does not have points."; + return; + } + + auto points = parsePoints(pointsAttr.getValue()); + std::size_t numPoints = points.size(); + for( std::size_t i = 0; i < numPoints; i++ ) { + if( i == 0 ) { + aSvgPath->path.moveTo(points[i]); + } else { + aSvgPath->path.lineTo(points[i]); + } + } + if( numPoints > 2 ) { + if(tnode.getName() == "polygon" ) { + aSvgPath->path.close(); + } + } +} +// reference: https://www.w3.org/TR/SVG2/paths.html#PathData +//-------------------------------------------------------------- +void ofxSvg::_parsePath( ofXml& tnode, std::shared_ptr aSvgPath ) { + aSvgPath->path.clear(); + + auto dattr = tnode.getAttribute("d"); + if( !dattr ) { + ofLogWarning("ofxSvg") << __FUNCTION__ << " path node does not have d attriubute."; + return; + } + + aSvgPath->path.setCircleResolution(mCircleResolution); + aSvgPath->path.setCurveResolution(mCurveResolution); + + std::vector splitChars = { + 'M', 'm', // move to + 'V', 'v', // vertical line + 'H', 'h', // horizontal line + 'L','l', // line + 'z','Z', // close path + 'c','C','s','S', // cubic bézier + 'Q', 'q', 'T', 't', // quadratic bézier + 'A', 'a' // elliptical arc + }; + std::string ostring = dattr.getValue(); +// ofLogNotice("ofxSvg") << __FUNCTION__ << " dattr: " << ostring; + + if( ostring.empty() ) { + ofLogError("ofxSvg") << __FUNCTION__ << " there is no data in the d string."; + return; + } + + std::size_t index = 0; + if( ostring[index] != 'm' && ostring[index] != 'M' ) { + ofLogWarning("ofxSvg") << __FUNCTION__ << " first char is not a m or M, ostring[index]: " << ostring[index]; + return; + } + + glm::vec3 currentPos = {0.f, 0.f, 0.f}; + glm::vec3 secondControlPoint = currentPos; + glm::vec3 qControlPoint = currentPos; + + auto convertToAbsolute = [](bool aBRelative, glm::vec3& aCurrentPos, std::vector& aposes) -> glm::vec3 { + for(auto& apos : aposes ) { + if( aBRelative ) { + apos += aCurrentPos; } } - - for (ofXml & element : xml.find("//*[@height]")) { - if (element.getAttribute("height").getValue() == "100%") { - auto w = ofToFloat(rect.at(3)); - ofLogWarning("ofxSvg::fixSvgString()") << "the SVG size is provided as percentage, which svgtiny translates to 0. The height is corrected from the viewBox height: " << w; - element.getAttribute("height").set(w); + if( aposes.size() > 0 ) { + aCurrentPos = aposes.back(); + } + return aCurrentPos; + }; + + auto convertToAbsolute2 = [](bool aBRelative, glm::vec3& aCurrentPos, std::vector& aposes) -> glm::vec3 { + for( std::size_t k = 0; k < aposes.size(); k+= 1 ) { + if( aBRelative ) { + aposes[k] += aCurrentPos; + } + if( k > 0 && k % 2 == 1 ) { + aCurrentPos = aposes[k]; + } + } + return aCurrentPos; + }; + + auto convertToAbsolute3 = [](bool aBRelative, glm::vec3& aCurrentPos, std::vector& aposes) -> glm::vec3 { + for( std::size_t k = 0; k < aposes.size(); k+= 1 ) { + if( aBRelative ) { + aposes[k] += aCurrentPos; + } + + if( k > 0 && k % 3 == 2 ) { + aCurrentPos = aposes[k]; } + + } + return aCurrentPos; + }; + + + + aSvgPath->path.clear(); + + unsigned int justInCase = 0; +// std::vector commands; + bool breakMe = false; + while( index < ostring.size() && !breakMe && justInCase < 999999) { + // figure out what we have here . + auto cchar = ostring[index]; + // check for valid character // + bool bFoundValidChar = false; + for( auto& sc : splitChars ) { + if( sc == cchar ) { + bFoundValidChar = true; + break; + } + } + if( !bFoundValidChar ) { + ofLogWarning("svgParser") << "Did not find valid character: " << cchar; + breakMe = true; + break; + } + + ofLogVerbose("ofxSvg") << " o : ["<< ostring[index] <<"]"; + + // up to next valid character // + std::string currentString; + bool bFoundValidNextChar = false; + auto pos = index+1; + if( pos >= ostring.size() ) { + ofLogVerbose("ofxSvg") << "pos is greater than string size: " << pos << " / " << ostring.size(); +// break; + breakMe = true; } + + bFoundValidChar = false; + for( pos = index+1; pos < ostring.size(); pos++ ) { + for( auto& sc : splitChars ) { + if( sc == ostring[pos] ) { + bFoundValidChar = true; + break; + } + } + if( bFoundValidChar ) { + break; + } + currentString.push_back(ostring[pos]); + } + + index += currentString.size()+1; + + + if( currentString.empty() ) { + ofLogVerbose("ofxSvg") << "currentString is empty: " << cchar; +// break; + } + + + ofLogVerbose("ofxSvg") << "["< npositions= {glm::vec3(0.f, 0.f, 0.f)}; + std::optional ctype; + + // check if we are looking for a position + if( cchar == 'm' || cchar == 'M' ) { + if( cchar == 'm' ) { + bRelative = true; + } + npositions = parsePoints(currentString); + ctype = ofPath::Command::moveTo; + } else if( cchar == 'v' || cchar == 'V' ) { + if( cchar == 'v' ) { + bRelative = true; + npositions[0].x = 0.f; + } else { + npositions[0].x = currentPos.x; + } + + npositions[0].y = ofToFloat(currentString); + ctype = ofPath::Command::lineTo; + } else if( cchar == 'H' || cchar == 'h' ) { + if( cchar == 'h' ) { + bRelative = true; + npositions[0].y = 0.f; + } else { + npositions[0].y = currentPos.y; + } + npositions[0].x = ofToFloat(currentString); + + ctype = ofPath::Command::lineTo; + } else if( cchar == 'L' || cchar == 'l' ) { + if( cchar == 'l' ) { + bRelative = true; + } + npositions = parsePoints(currentString); +// for( auto& np : npositions ) { +// ofLogVerbose("ofxSvg") << cchar << " line to: " << np; +// } + ctype = ofPath::Command::lineTo; + } else if( cchar == 'z' || cchar == 'Z' ) { + ctype = ofPath::Command::close; + npositions.clear(); + } else if( cchar == 'c' || cchar == 'C' || cchar == 'S' || cchar == 's' ) { + if( cchar == 'c' || cchar == 's') { + bRelative = true; + } + ctype = ofPath::Command::bezierTo; + npositions = parsePoints(currentString); +// for( auto& np : npositions ) { +// ofLogVerbose("ofxSvg") << cchar << " bezier to: " << np; +// } + } else if( cchar == 'Q' || cchar == 'q' || cchar == 'T' || cchar == 't' ) { + if( cchar == 'q' ) { + bRelative = true; + } + + ctype = ofPath::Command::quadBezierTo; + npositions = parsePoints(currentString); + +// for( auto& np : npositions ) { +// ofLogNotice("ofxSvg") << " Quad bezier to: " << np; +// } + } else if(cchar == 'a' || cchar == 'A' ) { + if( cchar == 'a' ) { + bRelative = true; + } + ctype = ofPath::Command::arc; +// npositions = _parseStrCoordsFunc(currentString); + auto arcValues = _parseSvgArc(currentString); + if( arcValues.size() == 7 ) { + npositions = { + glm::vec3(arcValues[0], arcValues[1], 0.0f), + glm::vec3(arcValues[2], 0.0f, 0.0f), + glm::vec3(arcValues[3], arcValues[4], 0.0f), + glm::vec3(arcValues[5], arcValues[6], 0.0f) + }; + } else { + ofLogError("ofxSvg") << "unable to parse arc command, incorrect number of parameters detected: " << arcValues.size(); + ofLogError("ofxSvg") << "-- Arc values ---------------------- "; + for( std::size_t n = 0; n < arcValues.size(); n++ ) { + ofLogError("ofxSvg") << n << ": " << arcValues[n]; + } + + } +// for( auto& np : npositions ) { +// ofLogNotice("ofxSvg") << " arc parsed positions: " << np; +// } + } + + if( ctype.has_value() ) { + +// for( auto& np : npositions ) { +// ofLogNotice("ofxSvg") << cchar << " position: " << np; +// } + + auto prevPos = currentPos; + + auto commandT = ctype.value(); + + if( commandT == ofPath::Command::arc ) { + if( npositions.size() == 4 ) { + std::vector tpositions = {npositions[3]}; + currentPos = convertToAbsolute(bRelative, currentPos, tpositions ); + npositions[3] = tpositions[0]; + } else { + ofLogWarning("ofxSvg") << "invalid number of arc commands."; + } + } else if( commandT == ofPath::Command::bezierTo ) { + if( cchar == 'S' || cchar == 's' ) { + currentPos = convertToAbsolute2(bRelative, currentPos, npositions ); + } else { + currentPos = convertToAbsolute3(bRelative, currentPos, npositions ); + } +// } else if( commandT == ofPath::Command::quadBezierTo ) { + // TODO: Check quad bezier for poly bezier like cubic bezier + + } else { +// if( commandT == ofPath::Command::moveTo ) { +// ofLogNotice("ofxSvg") << "before current pos is altered: move to: " << npositions[0] << " current Pos: " << currentPos << " relative: " << bRelative; +// } + if( npositions.size() > 0 && commandT != ofPath::Command::close ) { + currentPos = convertToAbsolute(bRelative, currentPos, npositions ); + } + } + +// if( npositions.size() > 0 ) { +// ofLogNotice("ofxSvg") << "before current pos is altered: move to: " << npositions[0] << " current Pos: " << currentPos << " relative: " << bRelative; +// } + + if( commandT != ofPath::Command::bezierTo ) { + secondControlPoint = currentPos; + } + if( commandT != ofPath::Command::quadBezierTo ) { + qControlPoint = currentPos; + } + + if( commandT == ofPath::Command::moveTo ) { + aSvgPath->path.moveTo(currentPos); + } else if( commandT == ofPath::Command::lineTo ) { + aSvgPath->path.lineTo(currentPos); + } else if( commandT == ofPath::Command::close ) { +// ofLogNotice("ofxSvg") << "Closing the path"; + aSvgPath->path.close(); + } else if( commandT == ofPath::Command::bezierTo ) { + + if( cchar == 'S' || cchar == 's' ) { + // these can come in as multiple sets of points // + std::vector ppositions;// = npositions; + auto tppos = prevPos; + for( std::size_t i = 0; i < npositions.size(); i += 2 ) { + auto cp2 = (secondControlPoint - tppos) * -1.f; + cp2 += tppos; + ppositions.push_back( cp2 ); + ppositions.push_back(npositions[i+0]); + ppositions.push_back(npositions[i+1]); + tppos = npositions[i+1]; + secondControlPoint = npositions[i+0]; + } + + npositions = ppositions; + +// if( npositions.size() == 2 ) { +// auto cp2 = (secondControlPoint - prevPos) * -1.f; +// cp2 += prevPos; +// npositions.insert(npositions.begin(), cp2 ); +// } + } + + auto tcpos = prevPos; + + for( std::size_t k = 0; k < npositions.size(); k +=3 ) { + aSvgPath->path.bezierTo(npositions[k+0], npositions[k+1], npositions[k+2]); + secondControlPoint = npositions[k+1]; + + mCPoints.push_back(prevPos); + mCPoints.push_back(npositions[k+0]); + mCPoints.push_back(npositions[k+1]); + tcpos = npositions[k+2]; + +// mCPoints.push_back(npositions[k+0]); +// mCPoints.push_back(npositions[k+1]); +// mCenterPoints.push_back(npositions[k+2]); + } + +// mCPoints.insert( mCPoints.end(), npositions.begin(), npositions.end() ); + +// if( npositions.size() == 3 ) { +// aSvgPath->path.bezierTo(npositions[0], npositions[1], npositions[2]); +// } +// +// secondControlPoint = npositions[1]; + } else if( commandT == ofPath::Command::quadBezierTo ) { + if( cchar == 'T' || cchar == 't' ) { + if( npositions.size() == 1 ) { + auto cp2 = (qControlPoint - prevPos) * -1.f; + cp2 += prevPos; + npositions.insert(npositions.begin(), cp2 ); + } + } + + if( npositions.size() == 2 ) { + aSvgPath->path.quadBezierTo(prevPos, npositions[0], npositions[1] ); + } + qControlPoint = npositions[0]; + } else if( commandT == ofPath::Command::arc ) { + if( npositions.size() == 4 ) { + // first point is rx, ry + // second point x value is x-axis rotation + // third point x value is large-arc-flag, y value is sweep-flag + // fourth point is x and y: When a relative a command is used, the end point of the arc is (cpx + x, cpy + y). + glm::vec3 radii = npositions[0]; + float xAxisRotation = npositions[1].x; + float largeArcFlag = std::clamp( npositions[2].x, 0.f, 1.f ); + float sweepFlag = std::clamp( npositions[2].y, 0.f, 1.f ); + + glm::vec3 spt = prevPos; + glm::vec3 ept = npositions[3]; + + +// glm::vec3 cpt(spt.x, ept.y, 0.0f); + auto cpt = glm::vec3(findArcCenter(spt, ept, radii.x, radii.y, xAxisRotation, largeArcFlag, sweepFlag ), 0.f); + auto windingOrder = _getWindingOrderOnArc( spt, cpt, ept ); + + if( largeArcFlag < 1 ) { + if( sweepFlag > 0 ) { + if( windingOrder < 0 ) { + windingOrder *= -1.f; + } + } else { + if( windingOrder > 0 ) { + windingOrder *= -1.f; + } + } + } else { + if( sweepFlag > 0 ) { + if( windingOrder < 1 ) { + windingOrder *= -1.f; + } + } else { + if( windingOrder > -1 ) { + windingOrder *= -1.f; + } else { + + } + } + } + + + auto startDiff = (spt - cpt); + if( glm::length2(startDiff) > 0.0f ) { + startDiff = glm::normalize(startDiff); + } else { + startDiff = glm::vec3(1.f, 0.f, 0.f ); + } + auto endDiff = (ept - cpt); + if( glm::length2(endDiff) > 0.0f ) { + endDiff = glm::normalize(endDiff); + } else { + endDiff = glm::vec3(1.f, 0.f, 0.f ); + } + + float startAngle = atan2f( startDiff.y, startDiff.x );// - glm::radians(40.f); + float endAngle = atan2f( endDiff.y, endDiff.x ); + + float xrotRad = glm::radians(xAxisRotation); + + startAngle = ofWrapRadians(startAngle); + endAngle = ofWrapRadians(endAngle); + + + std::string worderS = "co linear"; + if( windingOrder > 0 ) { + worderS = "clockwise"; + } else if( windingOrder < 0 ) { + worderS = "counter clockwise"; + } + + ofLogVerbose("ofxSvg") << "Arc winding order is: " << worderS << " order: " << windingOrder << " startDiff: " << startDiff << " endDiff: " << endDiff << " xAxisRotation: " << xAxisRotation; + + ofPolyline tline; + + if( windingOrder < 0 ) { +// aSvgPath->path.arcNegative(cpt, radii.x, radii.y, glm::degrees(startAngle), glm::degrees(endAngle) ); + tline.arcNegative(cpt, radii.x, radii.y, glm::degrees(startAngle-xrotRad), glm::degrees(endAngle-xrotRad), mCircleResolution ); +// tline.arcNegative(cpt, radii.x, radii.y, glm::degrees(startAngle), glm::degrees(endAngle) ); +// aSvgPath->path.arcNegative(cpt, radii.x, radii.y, glm::degrees(startAngle-xrotRad), glm::degrees(endAngle-xrotRad) ); + } else { + tline.arc(cpt, radii.x, radii.y, glm::degrees(startAngle-xrotRad), glm::degrees(endAngle-xrotRad), mCircleResolution ); +// tline.arc(cpt, radii.x, radii.y, glm::degrees(startAngle), glm::degrees(endAngle) ); +// aSvgPath->path.arc(cpt, radii.x, radii.y, glm::degrees(startAngle-xrotRad), glm::degrees(endAngle-xrotRad) ); +// aSvgPath->path.arc(cpt, radii.x, radii.y, glm::degrees(startAngle), glm::degrees(endAngle) ); + } + + // rotate based on x-axis rotation // + +// aSvgPath->path.rotateRad(xrotRad, glm::vec3(0.0f, 0.0f, 1.f)); + + for( auto& pv : tline.getVertices() ) { + auto nv = pv - cpt; + if( glm::length2(nv) > 0.0f ) { + nv = glm::vec3( glm::rotate(glm::vec2(nv.x, nv.y), xrotRad), 0.f); + } + nv += cpt; + pv.x = nv.x; + pv.y = nv.y; + } +//// +// // I guess we have to copy the line via commands + if( tline.size() > 0 ) { +// aSvgPath->path.moveTo(spt); + for( std::size_t i = 0; i < tline.size(); i++ ) { +// if( i == 0 ) { +// aSvgPath->path.moveTo(tline[0]); +// } else { + aSvgPath->path.lineTo(tline[i]); +// } + } + } + +// auto centers = findEllipseCenter( spt, ept, radii.x, radii.y ); + +// ofLogNotice("centers: ") << std::get<0>(centers) << " and " << std::get<1>(centers) << " spt: " << spt << " ept: " << ept << " radii: " << radii; + + + mCenterPoints.push_back(cpt); +// mCenterPoints.push_back(cpt); + npositions.clear(); + npositions.push_back(ept); + } else { + ofLogWarning("ofxSvg") << "unable to parse arc segment."; + } + } + +// mCenterPoints.push_back(currentPos); +// mCPoints.insert( mCPoints.end(), npositions.begin(), npositions.end() ); + } + +// ofLogNotice("ofxSvg") << "["<properties ) { + if( tprop.first.empty() ) { + ofLogNotice("ofxSvg") << "First prop is empty"; + } + css.addProperty(tprop.first, tprop.second); + } + } + + // now apply all of the other via css classes // + // now lets figure out if there is any css applied // + if( auto classAttr = anode.getAttribute("class") ) { + // get a list of classes, is this separated by commas? + auto classList = ofSplitString(classAttr.getValue(), ","); +// ofLogNotice("ofxSvg") << " going to try and parse style classes string: " << classAttr.getValue(); + for( auto& className : classList ) { + if( mSvgCss.hasClass(className) ) { +// ofLogNotice("ofxSvg") << " has class " << className; + // now lets try to apply it to the path + auto& tCss = mSvgCss.getClass(className); + for( auto& tprop : tCss.properties ) { + css.addProperty(tprop.first, tprop.second); + } + } + } + } + + // locally set on node overrides the class listing + // are there any properties on the node? + + // avoid the following + std::vector reservedAtts = { + "d", "id", "xlink:href", "width", "height", "rx", "ry", "cx", "cy", "r", "style", "font-family", + "x","y","x1","y1","x2","y2" + }; + + // lets try to do this a better way + for( auto& att : anode.getAttributes() ) { + auto atName = ofToLower(att.getName()); + bool bFileIt = true; + for( auto& rattName : reservedAtts ) { + if( atName == rattName ) { + bFileIt=false; + break; + } + } + if( bFileIt ) { + css.addProperty(att.getName(), att.getValue()); + } + } + + if( auto ffattr = anode.getAttribute("font-family") ) { + std::string tFontFam = ffattr.getValue(); + ofStringReplace( tFontFam, "'", "" ); + css.addProperty("font-family", tFontFam); + } + + // and lastly style + if( auto styleAttr = anode.getAttribute("style") ) { + css.addProperties(styleAttr.getValue()); + } + + return css; +} - bool finished = false; - while (!finished) { +//-------------------------------------------------------------- +void ofxSvg::_applyStyleToElement( ofXml& tnode, std::shared_ptr aEle ) { + auto css = _parseStyle(tnode); + if( css.hasAndIsNone("display")) { + ofLogVerbose("ofxSvg") << "setting element to invisible: " << aEle->name; + aEle->setVisible(false); + } +} - ofXml::Search invisibleElements = xml.find("//*[@display=\"none\"]"); +//-------------------------------------------------------------- +void ofxSvg::_applyStyleToPath( ofXml& tnode, std::shared_ptr aSvgPath ) { + auto css = _parseStyle(tnode); + _applyStyleToPath(css, aSvgPath); +} - if (invisibleElements.empty()) { - finished = true; +//-------------------------------------------------------------- +void ofxSvg::_applyStyleToPath( ofxSvgCssClass& aclass, std::shared_ptr aSvgPath ) { + // now lets figure out if there is any css applied // + + if( aclass.hasProperty("fill")) { + if( !aclass.isNone("fill")) { + aSvgPath->path.setFillColor(aclass.getColor("fill")); + } else { + aSvgPath->path.setFilled(false); + } + } else { +// aSvgPath->path.setFilled(false); + aSvgPath->path.setFillColor(ofColor(0)); + } + + if( aclass.hasProperty("fill-opacity")) { + if( aclass.isNone("fill-opacity")) { + aSvgPath->path.setFilled(false); } else { - const ofXml & element = invisibleElements[0]; - ofXml parent = element.getParent(); - if (parent && element) parent.removeChild(element); + float val = aclass.getFloatValue("fill-opacity", 1.0f); + if( val <= 0.0001f ) { + aSvgPath->path.setFilled(false); + } else { + auto pcolor = aSvgPath->path.getFillColor(); + pcolor.a = val; + aSvgPath->path.setFillColor(pcolor); + } } } + + if( !aclass.isNone("stroke") ) { + aSvgPath->path.setStrokeColor(aclass.getColor("stroke")); + } + + if( aclass.hasProperty("stroke-width")) { + if( aclass.isNone("stroke-width")) { + aSvgPath->path.setStrokeWidth(0.f); + } else { + aSvgPath->path.setStrokeWidth( aclass.getFloatValue("stroke-width", 0.f)); + } + } else { + // default with no value is 1.f +// aSvgPath->path.setStrokeWidth(1.f); + } + + // if the color is not set and the width is not set, then it should be 0 + if( !aclass.isNone("stroke") ) { + if( !aclass.hasProperty("stroke-width")) { + aSvgPath->path.setStrokeWidth(1.f); + } + } +} - // implement the SVG "use" element by expanding out those elements into - // XML that svgtiny will parse correctly. - ofXml::Search useElements = xml.find("//use"); - if (!useElements.empty()) { +//-------------------------------------------------------------- +void ofxSvg::_applyStyleToText( ofXml& anode, std::shared_ptr aTextSpan ) { + auto css = _parseStyle(anode); + _applyStyleToText(css, aTextSpan); +} - for (ofXml & element : useElements) { +//-------------------------------------------------------------- +void ofxSvg::_applyStyleToText( ofxSvgCssClass& aclass, std::shared_ptr aTextSpan ) { + // default font family + aTextSpan->fontFamily = aclass.getValue("font-family", "Arial"); + aTextSpan->fontSize = aclass.getIntValue("font-size", 18 ); + aTextSpan->color = aclass.getColor("fill"); +} + - // get the id attribute - string id = element.getAttribute("xlink:href").getValue(); - // remove the leading "#" from the id - id.erase(id.begin()); - // find the original definition of that element - TODO add defs into path? - string searchstring = "//*[@id='" + id + "']"; - ofXml idelement = xml.findFirst(searchstring); +//-------------------------------------------------------------- +glm::vec3 ofxSvg::_parseMatrixString(const std::string& input, const std::string& aprefix, bool abDefaultZero ) { +// std::string prefix = aprefix+"("; +// std::string suffix = ")"; + ofLogVerbose("ofxSvg") << __FUNCTION__ << " input: " << input;; + std::string searchStr = aprefix + "("; + size_t startPos = input.find(searchStr); + + if (startPos != std::string::npos) { + startPos += searchStr.size(); + size_t endPos = input.find(")", startPos); + + if (endPos != std::string::npos) { + // Extract the part inside the parentheses + std::string inside = input.substr(startPos, endPos - startPos); + + // Ensure numbers like ".5" are correctly handled by adding a leading zero if needed + if (inside[0] == '.') { + inside = "0" + inside; + } + + float tx = 0.f, ty = 0.f, tz = 0.f; + std::stringstream ss(inside); + if (ss >> tx) { + if (!(ss >> ty)) { + if(abDefaultZero) { + ty = 0.0f; + } else { + ty = tx; // If only one value is provided, duplicate it + } + } + if (!(ss >> tz)) { + if( abDefaultZero) { + tz = 0.0f; + } else { + tz = ty; // If only two values are provided, duplicate the second one + } + } + return glm::vec3(tx, ty, tz); + } + } + } + return glm::vec3(0.f, 0.f, 0.0f); +} - // if we found one then use it! (find first returns an empty xml on failure) - if (idelement.getAttribute("id").getValue() != "") { +//-------------------------------------------------------------- +//bool Parser::getTransformFromSvgMatrix( string aStr, glm::vec2& apos, float& scaleX, float& scaleY, float& arotation ) { +bool ofxSvg::setTransformFromSvgMatrixString( string aStr, std::shared_ptr aele ) { + + aele->scale = glm::vec2(1.0f, 1.0f); + aele->rotation = 0.0; + aele->mModelRotationPoint = glm::vec2(0.0f, 0.0f); + //TODO: implement matrix push and pop structure, similar to renderers + ofLogVerbose("ofxSvg") << __FUNCTION__ << " going to parse string: " << aStr << " pos: " << aele->pos; + + glm::mat4 mat = glm::mat4(1.f); + + if( ofIsStringInString(aStr, "translate")) { + auto transStr = aStr; + auto tp = _parseMatrixString(transStr, "translate", false ); + ofLogVerbose("ofxSvg") << __FUNCTION__ << " translate: " << tp; +// apos += tp; + mat = glm::translate(glm::mat4(1.0f), glm::vec3(tp.x, tp.y, 0.0f)); + } else { + mat = glm::translate(glm::mat4(1.0f), glm::vec3(0.f, 0.f, 0.0f)); + } + + if( ofIsStringInString(aStr, "rotate")) { + auto transStr = aStr; + auto tr = _parseMatrixString(transStr, "rotate", true ); + aele->rotation = tr.x; + if( aele->rotation != 0.f ) { + glm::vec2 rcenter(0.f, 0.f); + if( tr.y != 0.0f || tr.z != 0.0f ) { + rcenter.x = tr.y; + rcenter.y = tr.z; + + aele->mModelRotationPoint = rcenter; + + glm::vec3 pivot(rcenter.x, rcenter.y, 0.f); + // Step 1: Translate to pivot (move pivot to origin) + glm::mat4 toOrigin = glm::translate(glm::mat4(1.0f), -pivot ); + + // Step 2: Apply rotation + glm::mat4 rotation = glm::rotate(glm::mat4(1.0f), glm::radians(aele->rotation), glm::vec3(0.f, 0.f, 1.f) ); + + // Step 3: Translate back to original position + glm::mat4 backToPivot = glm::translate(glm::mat4(1.0f), pivot); + + // Apply transformations in the correct order: T_back * R * T_origin * Original_Transform + mat = backToPivot * rotation * toOrigin * mat; + } else { + mat = mat * glm::toMat4((const glm::quat&)glm::angleAxis(glm::radians(aele->rotation), glm::vec3(0.f, 0.f, 1.f))); + } +// ofLogNotice("ofxSvg") << "rcenter: " << rcenter.x << ", " << rcenter.y; + } + ofLogVerbose("ofxSvg") << __FUNCTION__ << " arotation: " << aele->rotation; + } + + if( ofIsStringInString(aStr, "scale")) { + auto transStr = aStr; + auto ts = _parseMatrixString(transStr, "scale", false ); + aele->scale.x = ts.x; + aele->scale.y = ts.y; + ofLogVerbose("ofxSvg") << __FUNCTION__ << " scale: " << ts; + + mat = glm::scale(mat, glm::vec3(aele->scale.x, aele->scale.y, 1.f)); + } + + glm::vec3 pos3 = mat * glm::vec4( aele->pos.x, aele->pos.y, 0.0f, 1.f ); + aele->pos.x = pos3.x; + aele->pos.y = pos3.y; + + + if( ofIsStringInString(aStr, "matrix")) { + auto matrix = aStr; + ofStringReplace(matrix, "matrix(", ""); + ofStringReplace(matrix, ")", ""); + vector matrixNum = ofSplitString(matrix, " ", false, true); + vector matrixF; + for(std::size_t i = 0; i < matrixNum.size(); i++){ + matrixF.push_back(ofToFloat(matrixNum[i])); +// std::cout << " matrix[" << i << "] = " << matrixF[i] << " string version is " << matrixNum[i] << std::endl; + } + + if( matrixNum.size() == 6 ) { + + mat = glm::translate(glm::mat4(1.0f), glm::vec3(matrixF[4], matrixF[5], 0.0f)); + + aele->rotation = glm::degrees( atan2f(matrixF[1],matrixF[0]) ); + if( aele->rotation != 0.f ) { + mat = mat * glm::toMat4((const glm::quat&)glm::angleAxis(glm::radians(aele->rotation), glm::vec3(0.f, 0.f, 1.f))); + } + + aele->scale.x = glm::sqrt(matrixF[0] * matrixF[0] + matrixF[1] * matrixF[1]); + aele->scale.y = glm::sqrt(matrixF[2] * matrixF[2] + matrixF[3] * matrixF[3]); + + mat = glm::scale(mat, glm::vec3(aele->scale.x, aele->scale.y, 1.f)); + + pos3 = mat * glm::vec4( aele->pos.x, aele->pos.y, 0.0f, 1.f ); + + aele->pos.x = pos3.x; + aele->pos.y = pos3.y; + +// apos.x = matrixF[4]; +// apos.y = matrixF[5]; +// +// scaleX = std::sqrtf(matrixF[0] * matrixF[0] + matrixF[1] * matrixF[1]) * (float)ofSign(matrixF[0]); +// scaleY = std::sqrtf(matrixF[2] * matrixF[2] + matrixF[3] * matrixF[3]) * (float)ofSign(matrixF[3]); +// +// arotation = glm::degrees( std::atan2f(matrixF[2],matrixF[3]) ); +// if( scaleX < 0 && scaleY < 0 ){ +// +// }else{ +// arotation *= -1.0f; +// } + // cout << " rotation is " << arotation << endl; +// std::cout << "matrix rotation is " << arotation << " ScaleX: " << scaleX << " scaleY: " << scaleY << " apos: " << apos << std::endl; + +// return true; + } + } + return false; +} - // make a copy of that element - element.appendChild(idelement); +//-------------------------------------------------------------- +std::string ofxSvg::getSvgMatrixStringFromElement( std::shared_ptr aele ) { + // matrix(1 0 0 1 352.4516 349.0799)"> - // then turn the use element into a g element - element.setName("g"); - } + // there's probably a better way to determine if this should be rotated in a certain way + if( aele->mModelRotationPoint.x != 0.0f || aele->mModelRotationPoint.y != 0.0f ) { + glm::vec2 rcenter(0.f, 0.f); + rcenter.x = aele->mModelRotationPoint.x; + rcenter.y = aele->mModelRotationPoint.y; + + std::ostringstream matrixStream; + matrixStream << std::fixed << std::setprecision(6) << "rotate(" << aele->rotation << " " << rcenter.x << " " << rcenter.y <<")"; + if( aele->scale.x != 1.f || aele->scale.y != 1.f ) { + matrixStream << " scale(" << aele->scale.x << " " << aele->scale.y <<")"; } + return matrixStream.str(); + + } else { + + // Create the transformation matrix + glm::mat4 transform = glm::mat4(1.0f); // Identity matrix + // transform = glm::translate(transform, glm::vec3(aele->pos, 0.0f) ); + transform = glm::translate(transform, glm::vec3(aele->mModelPos, 0.0f) ); + + transform = glm::rotate(transform, glm::radians(aele->rotation), glm::vec3( 0.0f, 0.0f, 1.f)); + + + transform = glm::scale(transform, glm::vec3(aele->scale, 1.0f) ); + + // Extract the 2D matrix values (first two rows and three columns) + float a = transform[0][0]; // m00 + float b = transform[0][1]; // m01 + float c = transform[1][0]; // m10 + float d = transform[1][1]; // m11 + float e = transform[3][0]; // m20 (translation x) + float f = transform[3][1]; // m21 (translation y) + + // Create the SVG transform matrix string + std::ostringstream matrixStream; + matrixStream << std::fixed << std::setprecision(6) + << "matrix(" << a << " " << b << " " << c << " " << d << " " << e << " " << f << ")"; + return matrixStream.str(); } + return ""; + +} - xmlstring = xml.toString(); +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::_getTextSpanFromXmlNode( ofXml& anode ) { + auto tspan = std::make_shared(); + + string tText = anode.getValue(); + float tx = 0; + auto txattr = anode.getAttribute("x"); + if( txattr) { + tx = txattr.getFloatValue(); + } + float ty = 0; + auto tyattr = anode.getAttribute("y"); + if( tyattr ) { + ty = tyattr.getFloatValue(); + } + + tspan->text = tText; + tspan->rect.x = tx; + tspan->rect.y = ty; + + _applyStyleToText(anode, tspan); + + return tspan; } -void ofxSvg::draw() { - for (int i = 0; i < (int)paths.size(); i++) { - paths[i].draw(); +//-------------------------------------------------------------- +float ofxSvg::getWidth() const { + return mViewbox.getWidth(); +} + +//-------------------------------------------------------------- +float ofxSvg::getHeight() const { + return mViewbox.getHeight(); +} + +//-------------------------------------------------------------- +ofRectangle ofxSvg::getViewbox() const { + return mViewbox; +} + +//-------------------------------------------------------------- +float ofxSvg::getBoundsWidth() const { + return mBounds.getWidth(); +} + +//-------------------------------------------------------------- +float ofxSvg::getBoundsHeight() const { + return mBounds.getHeight(); +} + +//-------------------------------------------------------------- +ofRectangle ofxSvg::getBounds() const { + return mBounds; +} + +//-------------------------------------------------------------- +void ofxSvg::setWidth(float aw) { + mViewbox.width = aw; + if( mBounds.width < 1 ) { + mBounds.width = aw; } } -void ofxSvg::setupDiagram(struct svgtiny_diagram * diagram) { +//-------------------------------------------------------------- +void ofxSvg::setHeight(float ah) { + mViewbox.height = ah; + if( mBounds.height < 1 ) { + mBounds.height = ah; + } +} - width = diagram->width; - height = diagram->height; +//-------------------------------------------------------------- +void ofxSvg::setViewBox( const ofRectangle& arect ) { + mViewbox = arect; +} - paths.clear(); +//-------------------------------------------------------------- +void ofxSvg::setBoundsWidth( float aw ) { + mBounds.width = aw; +} - for (int i = 0; i < (int)diagram->shape_count; i++) { - if (diagram->shape[i].path) { - paths.push_back(ofPath()); - setupShape(&diagram->shape[i], paths.back()); - } else if (diagram->shape[i].text) { - ofLogWarning("ofxSVG") << "setupDiagram(): text: not implemented yet"; - } +//-------------------------------------------------------------- +void ofxSvg::setBoundsHeight( float ah ) { + mBounds.height = ah; +} + +//-------------------------------------------------------------- +void ofxSvg::setBounds( const ofRectangle& arect ) { + mBounds = arect; +} + +//-------------------------------------------------------------- +void ofxSvg::pushGroup( const std::string& apath ) { + std::shared_ptr cgroup; + if( mGroupStack.size() > 0 ) { + mGroupStack.back()->get( apath ); + } else { + cgroup = get( apath ); + } + + if( cgroup ) { + pushGroup(cgroup); + } else { + ofLogWarning("ofx::svg::Parser") << "could not find group with path " << apath; } } -void ofxSvg::setupShape(struct svgtiny_shape * shape, ofPath & path) { - float * p = shape->path; +//-------------------------------------------------------------- +void ofxSvg::pushGroup( const std::shared_ptr& agroup ) { + if( agroup ) { + mGroupStack.push_back(agroup); + } +} + +//-------------------------------------------------------------- +void ofxSvg::popGroup() { + if( mGroupStack.size() > 0 ) { + mGroupStack.pop_back(); + } +} - path.setFilled(false); +//-------------------------------------------------------------- +void ofxSvg::setFillColor(ofColor acolor) { + mFillColor = acolor; + mCurrentCss.setFillColor(acolor); +} - if (shape->fill != svgtiny_TRANSPARENT) { - path.setFilled(true); - path.setFillHexColor(shape->fill); - path.setPolyWindingMode(OF_POLY_WINDING_NONZERO); +//-------------------------------------------------------------- +void ofxSvg::setFilled(bool abFilled) { + if( abFilled ) { + mCurrentCss.setFillColor(mFillColor); + } else { + mCurrentCss.setNoFill(); } +} + +//-------------------------------------------------------------- +void ofxSvg::setStrokeColor(ofColor acolor) { + mStrokeColor = acolor; + mCurrentCss.setStrokeColor(acolor); +} + +//-------------------------------------------------------------- +void ofxSvg::setStrokeWidth(float aLineWidth) { + mCurrentCss.setStrokeWidth(aLineWidth); +} - if (shape->stroke != svgtiny_TRANSPARENT) { - path.setStrokeWidth(shape->stroke_width); - path.setStrokeHexColor(shape->stroke); +//-------------------------------------------------------------- +void ofxSvg::setHasStroke(bool abStroke) { + if( abStroke ) { + mCurrentCss.setStrokeColor(mStrokeColor); + } else { + mCurrentCss.setNoStroke(); } +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addGroup(std::string aname) { + auto tgroup = std::make_shared(); + tgroup->name = aname; + _getPushedGroup()->add(tgroup); + recalculateLayers(); + return tgroup; +} - for (int i = 0; i < (int)shape->path_length;) { - if (p[i] == svgtiny_PATH_MOVE) { - path.moveTo(p[i + 1], p[i + 2]); - i += 3; - } else if (p[i] == svgtiny_PATH_CLOSE) { - path.close(); +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::add( const ofPath& apath ) { + auto path = std::make_shared(); + path->path = apath; +// _config(path); + _applyModelMatrixToElement( path, glm::vec2(0.f, 0.f) ); + _applyStyleToPath( mCurrentCss, path ); + _getPushedGroup()->add(path); + recalculateLayers(); + mPaths.clear(); + return path; +} - i += 1; - } else if (p[i] == svgtiny_PATH_LINE) { - path.lineTo(p[i + 1], p[i + 2]); - i += 3; - } else if (p[i] == svgtiny_PATH_BEZIER) { - path.bezierTo(p[i + 1], p[i + 2], - p[i + 3], p[i + 4], - p[i + 5], p[i + 6]); - i += 7; +//-------------------------------------------------------------- +std::vector< std::shared_ptr > ofxSvg::add( const std::vector& apaths ) { + std::vector< std::shared_ptr > rpaths; + for( auto& path : apaths ) { + rpaths.push_back( add(path) ); + } + return rpaths; +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::add( const ofPolyline& apoly ) { + if( apoly.size() < 2 ) { + return std::shared_ptr(); + } + + ofPath opath; + const auto& verts = apoly.getVertices(); + for( std::size_t i = 0; i < verts.size(); i++ ) { + if( i == 0 ) { + opath.moveTo(verts[i]); } else { - ofLogError("ofxSVG") << "setupShape(): SVG parse error"; - i += 1; + opath.lineTo(verts[i]); + } + } + if( apoly.isClosed() ) { + opath.close(); + } + return add( opath ); +} + +//-------------------------------------------------------------- +std::vector< std::shared_ptr > ofxSvg::add( const std::vector& apolys ) { + std::vector< std::shared_ptr > rpaths; + for( auto& poly : apolys ) { + rpaths.push_back( add(poly) ); + } + return rpaths; +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::add( const ofRectangle& arect ) { + return add( arect, 0.0f); +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::add( const ofRectangle& arect, float aRoundRadius ) { + auto rect = std::make_shared(); + rect->rectangle = arect; +// rect->pos = arect.getPosition(); + _applyModelMatrixToElement( rect, arect.getPosition() ); + rect->round = aRoundRadius; + rect->path.rectangle(arect); +// _config(rect); + _applyStyleToPath( mCurrentCss, rect ); + _getPushedGroup()->add(rect); + recalculateLayers(); + mPaths.clear(); + return rect; +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addCircle( float aradius ) { + return addCircle(glm::vec2(0.f, 0.f), aradius ); +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addCircle( const glm::vec2& apos, float aradius ) { + auto circle = std::make_shared(); +// circle->pos = apos; + _applyModelMatrixToElement( circle, apos ); + circle->radius = aradius; + circle->path.setCircleResolution(mCircleResolution); + circle->path.circle(apos, aradius); +// _config(circle); + _applyStyleToPath( mCurrentCss, circle ); + _getPushedGroup()->add(circle); + recalculateLayers(); + mPaths.clear(); + return circle; +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addCircle( const glm::vec3& apos, float aradius ) { + return addCircle( glm::vec2(apos.x, apos.y), aradius ); +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addCircle( const float& ax, const float& ay, float aradius ) { + return addCircle( glm::vec2(ax, ay), aradius ); +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addEllipse( float aradiusX, float aradiusY ) { + return addEllipse( glm::vec2(0.f, 0.f), aradiusX, aradiusY ); +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addEllipse( const glm::vec2& apos, float aradiusX, float aradiusY ) { + auto ellipse = std::make_shared(); + _applyModelMatrixToElement( ellipse, apos ); + + ellipse->radiusX = aradiusX; + ellipse->radiusY = aradiusY; + ellipse->path.setCircleResolution(mCircleResolution); + ellipse->path.ellipse(apos, aradiusX, aradiusY); + + _applyStyleToPath( mCurrentCss, ellipse ); + _getPushedGroup()->add(ellipse); + recalculateLayers(); + mPaths.clear(); + return ellipse; +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addEllipse( const glm::vec3& apos, float aradiusX, float aradiusY ) { + return addEllipse( glm::vec2(apos.x, apos.y), aradiusX, aradiusY ); +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addEllipse( const float& ax, const float& ay, float aradiusX, float aradiusY ) { + return addEllipse( glm::vec2(ax, ay), aradiusX, aradiusY ); +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addImage( const of::filesystem::path& apath, const ofTexture& atex ) { + return addImage(glm::vec2(0.f, 0.f), apath, atex ); +} + +//-------------------------------------------------------------- +std::shared_ptr ofxSvg::addImage( const glm::vec2& apos, const of::filesystem::path& apath, const ofTexture& atex ) { + auto img = std::make_shared(); + img->filepath = apath; + img->width = atex.getWidth(); + img->height = atex.getHeight(); + _applyModelMatrixToElement( img, apos ); + _getPushedGroup()->add(img); + recalculateLayers(); + return img; +} + +//---------------------------------------------------------- +void ofxSvg::pushMatrix() { + mModelMatrixStack.push(mModelMatrix); +} + +//---------------------------------------------------------- +bool ofxSvg::popMatrix() { + if( !mModelMatrixStack.empty() ) { + mModelMatrix = mModelMatrixStack.top(); + mModelMatrixStack.pop(); + return true; + } else { + loadIdentityMatrix(); + } + return false; +} + +//---------------------------------------------------------- +void ofxSvg::translate(const glm::vec2 & p) { + translate(p.x, p.y); +} + +//---------------------------------------------------------- +void ofxSvg::translate(float x, float y) { + mModelMatrix = glm::translate(mModelMatrix, glm::vec3(x, y, 0.0f)); +} + +//---------------------------------------------------------- +void ofxSvg::scale(float xAmnt, float yAmnt) { + mModelMatrix = glm::scale(mModelMatrix, glm::vec3(xAmnt, yAmnt, 1.f)); +} + +//---------------------------------------------------------- +void ofxSvg::rotateRadians(float aradians) { + mModelMatrix = glm::rotate(mModelMatrix, aradians, glm::vec3(0.f, 0.f, 1.f)); +} + +//---------------------------------------------------------- +void ofxSvg::rotateDegrees(float adegrees) { + rotateRadians( ofDegToRad(adegrees)); +} + +//---------------------------------------------------------- +void ofxSvg::multMatrix(const glm::mat4 & m) { + mModelMatrix = mModelMatrix * m; +} + +//---------------------------------------------------------- +void ofxSvg::loadMatrix(const glm::mat4 & m) { + mModelMatrix = m; +} + +//---------------------------------------------------------- +void ofxSvg::loadIdentityMatrix() { + mModelMatrix = glm::mat4(1.f); +} + + +//-------------------------------------------------------------- +void ofxSvg::drawDebug() { +// Group::draw(); + ofSetColor( ofColor::limeGreen ); + ofNoFill(); + +// int cindex = 0; +// for( auto& cp : mCPoints ) { +// ofSetColor( (float)(cindex % 2) * 255, 200, 60 ); +//// ofDrawCircle( cp, (cindex+1) * 1.0f ); +// ofDrawCircle( cp, 3. ); +// cindex ++; +// } +// ofFill(); + + for( std::size_t k = 0; k < mCPoints.size(); k += 3 ) { + ofSetColor( ofColor::orange ); + ofDrawCircle( mCPoints[k+0], 6.f ); + ofSetColor( ofColor::white ); + ofDrawCircle( mCPoints[k+1], 3.f ); + ofDrawCircle( mCPoints[k+2], 3.f ); + ofDrawLine( mCPoints[k+0], mCPoints[k+1] ); + ofDrawLine( mCPoints[k+0], mCPoints[k+2] ); + } + + ofFill(); + + ofSetColor( ofColor::orange ); + for( auto& cp : mCenterPoints ) { + ofDrawCircle(cp, 4.f); + } + +} + +//-------------------------------------------------------------- +ofxSvgGroup* ofxSvg::_getPushedGroup() { + if( mGroupStack.size() > 0 ) { + return mGroupStack.back().get(); + } + return this; +} + +//-------------------------------------------------------------- +bool ofxSvg::_hasPushedMatrix() { + return mModelMatrix != glm::mat4(1.0f); +} + +//-------------------------------------------------------------- +void ofxSvg::_applyModelMatrixToElement( std::shared_ptr aele, glm::vec2 aDefaultPos ) { + if(_hasPushedMatrix() ) { + aele->pos = aDefaultPos; + aele->mModelPos = _getPos2d(mModelMatrix); + aele->rotation = glm::degrees(_getZRotationRadians(mModelMatrix)); + aele->scale = _getScale2d(mModelMatrix); + + } else { + aele->mModelPos = glm::vec2(0.f, 0.f); + aele->pos = aDefaultPos; + } +} + +//-------------------------------------------------------------- +glm::vec2 ofxSvg::_getPos2d(const glm::mat4& amat) { + // Extract translation (position) + return glm::vec2(amat[3][0], amat[3][1]); +} + +//-------------------------------------------------------------- +glm::vec2 ofxSvg::_getScale2d(const glm::mat4& amat) { + // Extract scale (length of column vectors) + return glm::vec2(glm::length(glm::vec2(amat[0][0], amat[0][1])), // Length of first column + glm::length(glm::vec2(amat[1][0], amat[1][1])) // Length of second column + ); +} + +// Function to extract Z-axis rotation (in degrees) from a glm::mat4 +//-------------------------------------------------------------- +float ofxSvg::_getZRotationRadians(const glm::mat4& amat) { + // Normalize the first column (remove scale effect) + glm::vec2 xAxis = glm::vec2(amat[0][0], amat[0][1]); + if( glm::length2(xAxis) > 0.0f ) { + xAxis = glm::normalize(xAxis); + } else { + return 0.0f; + } + + // Compute rotation angle using atan2 + float angleRadians = atan2f(xAxis.y, xAxis.x); + return angleRadians; +} + +//-------------------------------------------------------------- +ofxSvgCssClass& ofxSvg::_addCssClassFromPath( std::shared_ptr aSvgPath ) { + ofxSvgCssClass tcss; +// tcss.name = aSvgPath->name+"st"; + tcss.name = "st"; + if( aSvgPath->path.isFilled() ) { + tcss.setFillColor(aSvgPath->path.getFillColor()); + } else { + tcss.setNoFill(); + } + + if( aSvgPath->path.hasOutline() ) { + tcss.setStrokeColor(aSvgPath->path.getStrokeColor()); + tcss.setStrokeWidth(aSvgPath->path.getStrokeWidth()); + } else { + tcss.setNoStroke(); + } + if( !aSvgPath->isVisible() ) { + tcss.addProperty("display", "none" ); + } + + return mSvgCss.getAddClass(tcss); +} + +//-------------------------------------------------------------- +void ofxSvg::_addCssClassFromPath( std::shared_ptr aSvgPath, ofXml& anode ) { + auto& css = _addCssClassFromPath(aSvgPath); + if( auto xattr = anode.appendAttribute("class") ) { + xattr.set(css.name); + } +} + +//-------------------------------------------------------------- +void ofxSvg::_addCssClassFromImage( std::shared_ptr aSvgImage, ofXml& anode ) { + + if( !aSvgImage->isVisible() ) { + ofxSvgCssClass tcss; + tcss.name = "st"; + tcss.addProperty("display", "none" ); + + auto& addedClass = mSvgCss.getAddClass(tcss); + + if( auto xattr = anode.appendAttribute("class") ) { + xattr.set(addedClass.name); } } } -const std::vector & ofxSvg::getPaths() const { - return paths; +//-------------------------------------------------------------- +bool ofxSvg::_toXml( ofXml& aParentNode, std::shared_ptr aele ) { + ofXml txml = aParentNode.appendChild( ofxSvgElement::sGetSvgXmlName(aele->getType())); + if( !aele->getName().empty() ) { + if( auto iattr = txml.appendAttribute("id")) { + iattr.set(aele->getName()); + } + } + if( aele->getType() == ofxSvgType::TYPE_GROUP ) { + auto tgroup = std::dynamic_pointer_cast(aele); + if( tgroup ) { + if( tgroup->getNumChildren() > 0 ) { + for( auto& kid : tgroup->getChildren() ) { + _toXml( txml, kid ); + } + } + } + } else if( aele->getType() == ofxSvgType::TYPE_RECTANGLE ) { + auto trect = std::dynamic_pointer_cast(aele); + _addCssClassFromPath( trect, txml ); + + if( auto xattr = txml.appendAttribute("x")) { + xattr.set(trect->pos.x); + } + if( auto xattr = txml.appendAttribute("y")) { + xattr.set(trect->pos.y); + } + if( auto xattr = txml.appendAttribute("width")) { + xattr.set(trect->rectangle.getWidth()); + } + if( auto xattr = txml.appendAttribute("height")) { + xattr.set(trect->rectangle.getHeight()); + } + if( trect->round > 0.0f ) { + if( auto xattr = txml.appendAttribute("rx")) { + xattr.set(trect->round); + } + if( auto xattr = txml.appendAttribute("ry")) { + xattr.set(trect->round); + } + } + + } else if( aele->getType() == ofxSvgType::TYPE_IMAGE ) { + auto timage = std::dynamic_pointer_cast(aele); + + _addCssClassFromImage( timage, txml ); + + if( auto xattr = txml.appendAttribute("width")) { + xattr.set(timage->width); + } + if( auto xattr = txml.appendAttribute("height")) { + xattr.set(timage->height); + } + if( !timage->getFilePath().empty() ) { + if( auto xattr = txml.appendAttribute("xlink:href")) { + xattr.set(timage->getFilePath().string()); + } + } + + + } else if( aele->getType() == ofxSvgType::TYPE_ELLIPSE ) { + auto tellipse = std::dynamic_pointer_cast(aele); + _addCssClassFromPath( tellipse, txml ); + + if( auto xattr = txml.appendAttribute("cx")) { + xattr.set(tellipse->pos.x); + } + if( auto xattr = txml.appendAttribute("cy")) { + xattr.set(tellipse->pos.y); + } + if( auto xattr = txml.appendAttribute("rx")) { + xattr.set(tellipse->radiusX); + } + if( auto xattr = txml.appendAttribute("ry")) { + xattr.set(tellipse->radiusY); + } + + } else if( aele->getType() == ofxSvgType::TYPE_CIRCLE ) { + auto tcircle = std::dynamic_pointer_cast(aele); + _addCssClassFromPath( tcircle, txml ); + + if( auto xattr = txml.appendAttribute("cx")) { + xattr.set(tcircle->pos.x); + } + if( auto xattr = txml.appendAttribute("cy")) { + xattr.set(tcircle->pos.y); + } + if( auto xattr = txml.appendAttribute("r")) { + xattr.set(tcircle->getRadius()); + } + + } else if( aele->getType() == ofxSvgType::TYPE_PATH ) { + auto tpath = std::dynamic_pointer_cast(aele); + + _addCssClassFromPath( tpath, txml ); + + std::stringstream vstr; + + if( tpath->path.getMode() == ofPath::Mode::POLYLINES ) { + + auto outlines = tpath->path.getOutline(); + for( auto& polyline : outlines ) { + const auto& pverts = polyline.getVertices(); + if( pverts.size() > 1 ) { + for( std::size_t i = 0; i < pverts.size(); i++ ) { + if( i == 0 ) { + vstr << "M"<path.getCommands(); + if( commands.size() > 1 ) { +// std::stringstream vstr; + for( auto& command : commands ) { + if( command.type == ofPath::Command::moveTo ) { + vstr << "M" << command.to.x << "," << command.to.y << " "; + } else if( command.type == ofPath::Command::lineTo ) { + vstr << "L" << command.to.x << "," << command.to.y << " "; + } else if( command.type == ofPath::Command::curveTo ) { + // hmm, not sure how to handle this at the moment + } else if( command.type == ofPath::Command::bezierTo ) { + vstr << "C" << command.cp1.x << "," << command.cp1.y << " " << command.cp2.x << "," << command.cp2.y << " " << command.to.x << "," << command.to.y << " "; + } else if( command.type == ofPath::Command::quadBezierTo ) { + vstr << "Q" << command.cp2.x << "," << command.cp2.y << " " << command.to.x << "," << command.to.y << " "; + } else if( command.type == ofPath::Command::arc ) { + // TODO: Not so sure about these + glm::vec2 ept = glm::vec2(command.to.x + cosf( command.radiusX ), command.to.y + sinf(command.radiusY )); + vstr << "A" << command.radiusX << "," << command.radiusY << " 0 " << "0,1 " << ept.x << "," <getType() == ofxSvgType::TYPE_TEXT ) { + // TODO: Maybe at some point ;/ + } + + // figure out if we need a transform attribute + if( aele->getType() == ofxSvgType::TYPE_IMAGE || aele->rotation != 0.0f || aele->scale.x != 1.0f || aele->scale.y != 1.0f ) { + if( auto xattr = txml.appendAttribute("transform")) { + xattr.set( getSvgMatrixStringFromElement(aele) ); + } + } + + + + return txml; } + + diff --git a/addons/ofxSvg/src/ofxSvg.h b/addons/ofxSvg/src/ofxSvg.h old mode 100644 new mode 100755 index c2c678deb45..79fc45690aa --- a/addons/ofxSvg/src/ofxSvg.h +++ b/addons/ofxSvg/src/ofxSvg.h @@ -1,61 +1,295 @@ -#pragma once +// +// ofxSvgParser.h +// +// Created by Nick Hardeman on 8/31/24. +// -//#include "ofMain.h" -#include "ofPath.h" -#include "ofTypes.h" +#pragma once +#include "ofxSvgGroup.h" #include "ofXml.h" +#include "ofxSvgCss.h" +#include /// \file -/// ofxSVG is used for loading and rendering SVG files. It's a wrapper -/// for the open source C library [Libsvgtiny](https://www.netsurf-browser.org/projects/libsvgtiny/ "Libsvgtiny website"), -/// and it supports files in the [SVG Tiny format](https://www.w3.org/TR/SVGMobile/ "SVG Tiny 1.2 -/// format specification at the W3C"). -/// -/// Libsvgtiny supports a subset of SVG elements, (for a full list, see the Libsvgtiny readme file) -/// but we have gone some way to improving this by manually implementing some extra features (such as the -/// SVG "use" element). - -class ofxSvg { -public: +/// ofxSVG is used for loading, manipulating, rendering and saving of SVG files. +/// Based on this spec: https://www.w3.org/TR/SVG/Overview.html +class ofxSvg : public ofxSvgGroup { +protected: + std::shared_ptr clone( const std::shared_ptr& aele ); + std::vector> deepCopyVector(const std::vector>& original); + void deepCopyFrom( const ofxSvg & mom ); + void moveFrom( ofxSvg&& mom ); + +public: + virtual ofxSvgType getType() override {return ofxSvgType::TYPE_DOCUMENT;} + + // Default constructor ofxSvg() = default; + // Copy constructor (deep copy) + ofxSvg(const ofxSvg & mom);// = default; + // Copy assignment operator (deep copy) + ofxSvg& operator=(const ofxSvg& mom); + // Move constructor + ofxSvg(ofxSvg&& mom); + // Move assignment operator + ofxSvg& operator=(ofxSvg&& mom); + ~ofxSvg() = default; - ofxSvg(const ofxSvg & a) = default; - + ofxSvg(const of::filesystem::path & fileName); + /// \brief Loads an SVG file from the provided filename. + /// \return true if the load was successful. + bool load( const of::filesystem::path & fileName ); + /// \brief provided for legacy support. + /// \return true if the load was successful. + bool loadFromString( const std::string& data, std::string url = "local"); + /// \brief Reload from filepath saved from a previouly called load(). + /// \return true if the reload was successful. + bool reload(); + /// \brief Save the svg to the path. + /// Use the add functions to provide data to be saved or load data using the load() function. + /// \return true if the save was successful. + bool save( of::filesystem::path apath ); + /// \brief Remove all of the data from the document. + void clear(); + /// \brief Set the directory to search for fonts if using text elements. + /// Needs to be set before calling load(); + /// \param aDir string representation of the directory. + void setFontsDirectory( std::string aDir ); + + /// \brief A string of the element hierarchy. + /// Helpful for visualizing structure. + std::string toString(int nlevel = 0) override; + /// \brief Get the units, ie. px, cm, in, etc. + /// \return string description of the units used. + std::string getUnitString() { return mUnitStr; } + /// \brief Set the units using a string, ie. px, cm, in, etc. + void setUnitString(std::string astring ) { mUnitStr = astring; } + /// \brief The total layers in the svg, should also be the number of groups + elements. + /// \return int that is the total layers. + const int getTotalLayers(); + /// \brief If an element has been added or removed, the function should be called internally. + /// The function updates the 'layer' property on each element. + /// If the layers appear to be incorrect, call this function. + void recalculateLayers(); + /// \brief Provided for legacy support. + /// Includes circles, ellipses, rectangles and paths as ofPaths. + /// \return int as the number of paths in the document. + int getNumPath(); + /// \brief Provided for legacy support. Use getNumPath() to acquire the total number of paths detected. + /// \param n the index of the ofPath to return. + /// \return An ofPath using the provided index. + ofPath & getPathAt(int n); + /// \brief Provided for legacy support. + /// Includes circles, ellipses, rectangles and paths as ofPaths. + /// \return A vector of ofPaths in the entire document. + const std::vector & getPaths() const; + /// \brief Parse the svg transform string into global position, scale and rotation. + /// The elements pos, scale and rotation is set. + /// \param aStr svg transform string. + /// \param aele ofx::svg::Element to be updated. + bool setTransformFromSvgMatrixString( std::string aStr, std::shared_ptr aele ); + /// \brief Return a string used to represent matrix transforms in svg + /// The matrix can be represented as an array of values like : + /// Or using individual components like tranform(translateX translateY), scale(scaleX scaleY) and rotate(degrees ptx pty ) + /// Skew is currently not supported. + /// Reference: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform + /// \param aele the element to extract the svg matrix from using its pos, scale and rotation properties. + /// \return string that represents the svg matrix for using in a svg file. + std::string getSvgMatrixStringFromElement( std::shared_ptr aele ); + /// \brief The viewbox width. + /// \return float width of the viewbox. float getWidth() const; + /// \brief The viewbox height. + /// \return float height of the viewbox. float getHeight() const; + /// \brief The viewbox rectangle. The viewbox attribute on the svg node. + /// \return ofRectangle with the dimenstions of the viewbox. + ofRectangle getViewbox() const; + /// \brief The bounds width. + /// \return float width of the bounds. + float getBoundsWidth() const; + /// \brief The bounds height. + /// \return float height of the bounds. + float getBoundsHeight() const; + /// \brief The bounds rectangle. The width and height attribute on the svg node. + /// \return ofRectangle with the dimenstions of the bounds. + ofRectangle getBounds() const; + + /// \brief Set the width of the viewbox. + /// If the bounds width is less than 1, set the bounds to the same width. + /// \param aw float width. + void setWidth( float aw ); + /// \brief Set the height of the viewbox. + /// If the bounds height is less than 1, set the bounds to the same height. + /// \param ah float height. + void setHeight( float ah ); + /// \brief Set the dimensions of the viewbox. + /// \param arect an ofRectangle with the dimensions of the viewbox. + void setViewBox( const ofRectangle& arect ); + /// \brief Set the width of the bounds. + /// \param aw float width. + void setBoundsWidth( float aw ); + /// \brief Set the height of the viewbox. + /// \param ah float height. + void setBoundsHeight( float ah ); + /// \brief Set the dimensions of the bounds. + /// \param arect an ofRectangle with the dimensions of the bounds. + void setBounds( const ofRectangle& arect ); + + /// \brief Push a svg group to be active for adding elements to. + /// \param aname The path to the svg group in the document, if a group is already pushed, search in that group. + void pushGroup( const std::string& aname ); + /// \brief Push a svg group to be active for adding elements to. + /// \param aname The group to push and make active. + void pushGroup( const std::shared_ptr& agroup ); + /// \brief Remove the most recent pushed group if any. + void popGroup(); + /// \brief Set the current fill color. Any subsequent items using a fill color will adopt this color. + /// \param acolor is the color to set. + void setFillColor(ofColor acolor); + /// \brief Set if items should be filled or not. Any subsequent added items will use this value. + /// \param abFilled should the items be filled or not. + void setFilled(bool abFilled); + /// \brief Set the current stroke color. Any subsequent items using a stroke color will adopt this color. + /// \param acolor is the color to set. + void setStrokeColor(ofColor acolor); + /// \brief Set the current stroke width. Any subsequent items using a stroke width will adopt this value. + /// \param aLineWidth is the width of the lines. + void setStrokeWidth(float aLineWidth); + /// \brief Set if items should have a stroke or not. Any subsequent items using a stroke will adopt this value. + /// \param abStroke activates or deactivates strokes. + void setHasStroke(bool abStroke); + /// \brief Set the circle resolution for rendering. + /// Set this value before calling load. + /// \param ac (int) the resolution to use. + void setCircleResolution( int ac ) { mCircleResolution = ac; } + /// \brief Set the curve resolution for rendering. + /// Set this value before calling load. + /// \param ac (int) the resolution to use. + void setCurveResolution( int ac ) { mCurveResolution = ac; } + /// \brief Get the circle resolution for rendering. + /// \return int of the circle resolution. + int getCircleResolution() { return mCircleResolution; } + /// \brief Get the curve resolution for rendering. + /// \return int of the circle resolution. + int getCurveResolution() { return mCurveResolution; }; + /// \brief Get the current css used for items. + /// \return ofx::svg::CssClass. + ofxSvgCssClass& getCurrentCss() { return mCurrentCss;} + /// \brief Add a group to the document. This will also push back the group as current. + /// \param aname is the name of the group. + /// \return std::shared_ptr as the group that was created. + std::shared_ptr addGroup(std::string aname); + + std::shared_ptr add( const ofPath& apath ); + std::vector< std::shared_ptr > add( const std::vector& apaths ); + + std::shared_ptr add( const ofPolyline& apoly ); + std::vector< std::shared_ptr > add( const std::vector& apolys ); + + std::shared_ptr add( const ofRectangle& arect ); + std::shared_ptr add( const ofRectangle& arect, float aRoundRadius ); + + std::shared_ptr addCircle( float aradius ); + std::shared_ptr addCircle( const glm::vec2& apos, float aradius ); + std::shared_ptr addCircle( const glm::vec3& apos, float aradius ); + std::shared_ptr addCircle( const float& ax, const float& ay, float aradius ); + + std::shared_ptr addEllipse( float aradiusX, float aradiusY ); + std::shared_ptr addEllipse( const glm::vec2& apos, float aradiusX, float aradiusY ); + std::shared_ptr addEllipse( const glm::vec3& apos, float aradiusX, float aradiusY ); + std::shared_ptr addEllipse( const float& ax, const float& ay, float aradiusX, float aradiusY ); + + std::shared_ptr addImage( const of::filesystem::path& apath, const ofTexture& atex ); + std::shared_ptr addImage( const glm::vec2& apos, const of::filesystem::path& apath, const ofTexture& atex ); + + // adapted from ofGLProgrammableRenderer for some sort of conformity + void pushMatrix(); + bool popMatrix(); + void translate(float x, float y); + void translate(const glm::vec2 & p); + void scale(float xAmnt, float yAmnt); + void rotateRadians(float radians); + void rotateDegrees(float adegrees); + void multMatrix(const glm::mat4 & m); + void loadMatrix(const glm::mat4 & m); + void loadIdentityMatrix(); + + virtual void drawDebug(); + +protected: + std::string fontsDirectory = ""; + of::filesystem::path folderPath, svgPath; + ofRectangle mViewbox; + ofRectangle mBounds; + void validateXmlSvgRoot( ofXml& aRootSvgNode ); + std::string cleanString( std::string aStr, std::string aReplace ); + void _parseXmlNode( ofXml& aParentNode, std::vector< std::shared_ptr >& aElements ); + bool _addElementFromXmlNode( ofXml& tnode, std::vector< std::shared_ptr >& aElements ); + + void _parsePolylinePolygon( ofXml& tnode, std::shared_ptr aSvgPath ); + // reference: https://www.w3.org/TR/SVG/paths.html + void _parsePath( ofXml& tnode, std::shared_ptr aSvgPath ); + + ofxSvgCssClass _parseStyle( ofXml& tnode ); + void _applyStyleToElement( ofXml& tnode, std::shared_ptr aEle ); + void _applyStyleToPath( ofXml& tnode, std::shared_ptr aSvgPath ); + void _applyStyleToPath( ofxSvgCssClass& aclass, std::shared_ptr aSvgPath ); + void _applyStyleToText( ofXml& tnode, std::shared_ptr aTextSpan ); + void _applyStyleToText( ofxSvgCssClass& aclass, std::shared_ptr aTextSpan ); + + glm::vec3 _parseMatrixString(const std::string& input, const std::string& aprefix, bool abDefaultZero ); + + std::shared_ptr _getTextSpanFromXmlNode( ofXml& anode ); + + ofxSvgGroup* _getPushedGroup(); + bool _hasPushedMatrix(); + void _applyModelMatrixToElement( std::shared_ptr aele, glm::vec2 aDefaultPos ); + glm::vec2 _getPos2d(const glm::mat4& amat); + glm::vec2 _getScale2d(const glm::mat4& amat); + float _getZRotationRadians( const glm::mat4& amat ); + + ofxSvgCssClass& _addCssClassFromPath( std::shared_ptr aSvgPath ); + void _addCssClassFromPath( std::shared_ptr aSvgPath, ofXml& anode ); + void _addCssClassFromImage( std::shared_ptr aSvgImage, ofXml& anode ); + bool _toXml( ofXml& aParentNode, std::shared_ptr aele ); + + unsigned int mCurrentLayer = 0; + + std::string mUnitStr = "px"; + + ofxSvgCssStyleSheet mSvgCss; + ofxSvgCssClass mCurrentCss; + ofColor mFillColor, mStrokeColor; + + std::vector< std::shared_ptr > mGroupStack; + + std::shared_ptr mCurrentSvgCss; + + std::vector< std::shared_ptr > mDefElements; + + // just used for debugging + std::vector mCPoints; + std::vector mCenterPoints; + + glm::mat4 mModelMatrix = glm::mat4(1.f); + std::stack mModelMatrixStack; + + int mCircleResolution = 64; + int mCurveResolution = 24; + + // for legacy purposes // + static ofPath sDummyPath; + mutable std::vector mPaths; +}; - /// \brief Loads an SVG file from the provided filename. - /// - /// ~~~~ - void load(const of::filesystem::path & fileName); - - /// \brief Loads an SVG from a text string. - /// - /// Useful for parsing SVG text from sources other than a file. As the - /// underlying SVG parsing library requires a url, this method gives - /// you the option of providing one. - /// - /// ~~~~ - void loadFromString(std::string data, std::string url = "local"); - void draw(); - int getNumPath(); - ofPath & getPathAt(int n); - const std::vector & getPaths() const; - static void fixSvgString(std::string & xmlstring); -private: - float width, height; - std::vector paths; - void setupDiagram(struct svgtiny_diagram * diagram); - void setupShape(struct svgtiny_shape * shape, ofPath & path); -}; -typedef ofxSvg ofxSVG; diff --git a/addons/ofxSvg/src/ofxSvgCss.cpp b/addons/ofxSvg/src/ofxSvgCss.cpp new file mode 100644 index 00000000000..7d9a93e1a4a --- /dev/null +++ b/addons/ofxSvg/src/ofxSvgCss.cpp @@ -0,0 +1,498 @@ +// +// ofxSvg2Css.cp +// Example +// +// Created by Nick Hardeman on 8/22/24. +// + +#include "ofxSvgCss.h" +#include "ofUtils.h" +#include "ofLog.h" +#include +#include +#include + + +std::map sCommonColors = { + {"white", ofColor(255, 255, 255)}, + {"black", ofColor(0, 0, 0)}, + {"red", ofColor(255, 0, 0)}, + {"green", ofColor(0, 255, 0)}, + {"blue", ofColor(0, 0, 255)}, + {"yellow", ofColor(255, 255, 0)}, + {"cyan", ofColor(0, 255, 255)}, + {"magenta", ofColor(255, 0, 255)}, + {"gray", ofColor(128, 128, 128)}, + {"orange", ofColor(255, 165, 0)}, + {"brown", ofColor(165, 42, 42)}, + {"pink", ofColor(255, 192, 203)}, + {"purple", ofColor(128, 0, 128)}, + {"lime", ofColor(0, 255, 0)}, + {"maroon", ofColor(128, 0, 0)}, + {"navy", ofColor(0, 0, 128)}, + {"olive", ofColor(128, 128, 0)}, + {"teal", ofColor(0, 128, 128)}, + {"violet", ofColor(238, 130, 238)}, + {"indigo", ofColor(75, 0, 130)}, + {"gold", ofColor(255, 215, 0)}, + {"silver", ofColor(192, 192, 192)}, + {"beige", ofColor(245, 245, 220)}, + {"lavender", ofColor(230, 230, 250)}, + {"turquoise", ofColor(64, 224, 208)}, + {"sky blue", ofColor(135, 206, 235)}, + {"mint", ofColor(189, 252, 201)}, + {"coral", ofColor(255, 127, 80)}, + {"salmon", ofColor(250, 128, 114)}, + {"khaki", ofColor(240, 230, 140)}, + {"ivory", ofColor(255, 255, 240)}, + {"peach", ofColor(255, 218, 185)}, + {"aquamarine", ofColor(127, 255, 212)}, + {"chartreuse", ofColor(127, 255, 0)}, + {"plum", ofColor(221, 160, 221)}, + {"chocolate", ofColor(210, 105, 30)}, + {"orchid", ofColor(218, 112, 214)}, + {"tan", ofColor(210, 180, 140)}, + {"slate gray", ofColor(112, 128, 144)}, + {"periwinkle", ofColor(204, 204, 255)}, + {"sea green", ofColor(46, 139, 87)}, + {"mauve", ofColor(224, 176, 255)}, + {"rose", ofColor(255, 0, 127)}, + {"rust", ofColor(183, 65, 14)}, + {"amber", ofColor(255, 191, 0)}, + {"crimson", ofColor(220, 20, 60)}, + {"sand", ofColor(194, 178, 128)}, + {"jade", ofColor(0, 168, 107)}, + {"denim", ofColor(21, 96, 189)}, + {"copper", ofColor(184, 115, 51)} +}; + +//-------------------------------------------------------------- +void ofxSvgCssClass::clear() { + properties.clear(); + name = "default"; +} + +//-------------------------------------------------------------- +std::string ofxSvgCssClass::sRgbaToHexString(unsigned char r, unsigned char g, unsigned char b, unsigned char a) { + std::stringstream ss; + ss << std::hex << std::setfill('0') << std::uppercase; + ss << std::setw(2) << static_cast(r); + ss << std::setw(2) << static_cast(g); + ss << std::setw(2) << static_cast(b); + ss << std::setw(2) << static_cast(a); + return "#"+ss.str(); +} + +//-------------------------------------------------------------- +std::string ofxSvgCssClass::sRgbToHexString(unsigned char r, unsigned char g, unsigned char b) { + std::stringstream ss; + ss << std::hex << std::setfill('0') << std::uppercase; + ss << std::setw(2) << static_cast(r); + ss << std::setw(2) << static_cast(g); + ss << std::setw(2) << static_cast(b); + return "#"+ss.str(); +} + +//-------------------------------------------------------------- +bool ofxSvgCssClass::sIsNone( const std::string& astr ) { + if( astr.empty() ) { + return true; + } + if( ofToLower(astr) == "none" ) { + return true; + } + return false; +} + +//-------------------------------------------------------------- +ofColor ofxSvgCssClass::sGetColor(const std::string& astr ) { + bool bHasHash = false; + std::string cstr = astr; + if( ofIsStringInString(cstr, "#")) { + ofStringReplace(cstr, "#", ""); + bHasHash = true; + } + cstr = ofToLower(cstr); + + if( bHasHash ) { + ofColor tcolor(255); + int hint = ofHexToInt(cstr); + tcolor.setHex(hint); +// ofLogNotice("ofxSvgCssClass") << "color: " << cstr << " ofColor: " << tcolor; + return tcolor; + } else if( !astr.empty() ) { + if( sCommonColors.count(cstr)) { + return sCommonColors[cstr]; + } + } + return ofColor(255); +} + +//-------------------------------------------------------------- +float ofxSvgCssClass::sGetFloat(const std::string& astr) { + if( astr.empty() ) return 0.f; + +// bool bHasPix = false; + std::string cstr = astr; + if( ofIsStringInString(cstr, "px")) { +// bHasPix = true; + ofStringReplace(cstr, "px", ""); + } + return ofToFloat( cstr ); +} + +//-------------------------------------------------------------- +bool ofxSvgCssClass::addProperties( std::string aPropertiesString ) { + if( aPropertiesString.size() > 0 ) { + auto propertiesStr = ofSplitString(aPropertiesString, ";", true, true); +// int pindex = 0; + for( auto& propStr : propertiesStr ) { +// std::cout << " " << pindex << " - property: " << propStr << std::endl; + addProperty(propStr); +// pindex++; + } + +// for( auto& prop : properties ) { +// ofLogNotice("ofx::svg2::CssClass") << " prop: " << prop.first << " : " << prop.second.srcString; +// } + } + return properties.size() > 0; +} + +//-------------------------------------------------------------- +bool ofxSvgCssClass::addProperty( std::string aPropString ) { + auto splitProps = ofSplitString(aPropString, ":", true ); + if( splitProps.size() == 2 ) { + return addProperty(splitProps[0], splitProps[1]); + } + return false; +} + +//-------------------------------------------------------------- +bool ofxSvgCssClass::addProperty( std::string aName, std::string avalue ) { + if( !aName.empty() && !avalue.empty() ) { + Property newProp; + newProp.srcString = avalue; + ofStringReplace(newProp.srcString, ";", ""); + ofStringReplace(newProp.srcString, "'", ""); + properties[aName] = newProp; + return true; + } + return false; +} + +//-------------------------------------------------- +bool ofxSvgCssClass::addProperty( const std::string& aName, const Property& aprop ) { + return addProperty(aName, aprop.srcString); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::addProperty( const std::string& aName, const float& avalue ) { + ofxSvgCssClass::Property prop; + prop.fvalue = avalue; + prop.srcString = ofToString(avalue); + return addProperty(aName, prop ); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::addProperty( const std::string& aName, const ofColor& acolor ) { + ofxSvgCssClass::Property prop; + prop.cvalue = acolor; + prop.srcString = sRgbToHexString(acolor.r, acolor.g, acolor.b); +// ofLogNotice(" CssClass::addProperty") << prop.srcString << " color: " << acolor; + return addProperty(aName, prop ); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::setFillColor(const ofColor& acolor) { + return addProperty("fill", acolor); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::setNoFill() { + return addProperty("fill", "none" ); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::isFilled() { + return !isNone("fill"); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::setStrokeColor(const ofColor& acolor) { + return addProperty("stroke", acolor); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::setStrokeWidth( const float& awidth ) { + return addProperty("stroke-width", awidth); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::setNoStroke() { + return addProperty("stroke", "none" ); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::hasStroke() { + return !isNone("stroke"); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::hasProperty( const std::string& akey ) { + return (properties.count(akey) > 0); +} + +//-------------------------------------------------- +ofxSvgCssClass::Property& ofxSvgCssClass::getProperty( const std::string& akey ) { + if( properties.count(akey) < 1 ) { + return dummyProp; + } + return properties[akey]; +} + +//-------------------------------------------------- +bool ofxSvgCssClass::isNone(const std::string& akey) { + if( properties.count(akey) < 1 ) { + return true; + } + return sIsNone( properties[akey].srcString ); +} + +//-------------------------------------------------- +bool ofxSvgCssClass::hasAndIsNone(const std::string& akey) { + if( hasProperty(akey)) { + return sIsNone( properties[akey].srcString ); + } + return false; +} + +//-------------------------------------------------- +std::string ofxSvgCssClass::getValue(const std::string& akey, const std::string& adefault) { + if( properties.count(akey) < 1 ) { + return adefault; + } + auto& prop = properties[akey]; + if(!prop.svalue.has_value()) { + prop.svalue = prop.srcString; + } + return prop.svalue.value(); +} + +//-------------------------------------------------- +int ofxSvgCssClass::getIntValue(const std::string& akey, const int& adefault) { + if( properties.count(akey) < 1 ) { + return adefault; + } + auto& prop = properties[akey]; + if(!prop.ivalue.has_value()) { + prop.ivalue = ofToInt(prop.srcString); + } + return prop.ivalue.value(); +} + +//-------------------------------------------------- +float ofxSvgCssClass::getFloatValue(const std::string& akey, const float& adefault) { + if( properties.count(akey) < 1 ) { + return adefault; + } + auto& prop = properties[akey]; + if( !prop.fvalue.has_value() ) { + prop.fvalue = sGetFloat(prop.srcString); + } + return prop.fvalue.value(); +} + +//-------------------------------------------------- +ofColor ofxSvgCssClass::getColor(const std::string& akey) { + if( properties.count(akey) < 1 ) { + return ofColor(255); + } + auto& prop = properties[akey]; + if( !prop.cvalue.has_value() ) { + prop.cvalue = sGetColor(prop.srcString); + } + return prop.cvalue.value(); +} + +//-------------------------------------------------- +std::string ofxSvgCssClass::toString(bool aBPrettyPrint) { + std::stringstream ss; + for( auto& piter : properties ) { + if(aBPrettyPrint) { + ss << std::endl << " "; + } + ss << piter.first << ":" << piter.second.srcString << ";"; + } + return ss.str(); +} + +//-------------------------------------------------- +bool ofxSvgCssStyleSheet::parse( std::string aCssString ) { + if( aCssString.empty() ) { + return false; + } + + classes.clear(); + + ofStringReplace(aCssString, "", ""); + + // Regular expression to match class names (e.g., .cls-1, .cls-2, etc.) + std::regex class_regex(R"((\.[\w\-]+(?:,\s*\.[\w\-]+)*))"); + // Regular expression to match properties within curly braces (e.g., fill: none; stroke-miterlimit: 10;) + std::regex property_regex(R"(\{([^}]+)\})"); + + std::smatch class_match; + std::smatch property_match; + + // Search for each class rule block + auto search_start = aCssString.cbegin(); + while (std::regex_search(search_start, aCssString.cend(), class_match, class_regex)) { + std::string class_list = class_match[1]; // Extract the class list (e.g., .cls-1, .cls-2) + + // Move search start forward to find the corresponding property block + std::string::const_iterator prop_search_start = class_match.suffix().first; + + // Find the corresponding properties block + if (std::regex_search(prop_search_start, aCssString.cend(), property_match, property_regex)) { + std::string properties_block = property_match[1]; // Extract properties + std::map properties; + + // Split properties into key-value pairs + std::regex property_pair_regex(R"(([\w\-]+)\s*:\s*([^;]+);?)"); + std::smatch property_pair_match; + + auto prop_search_start = properties_block.cbegin(); + while (std::regex_search(prop_search_start, properties_block.cend(), property_pair_match, property_pair_regex)) { + std::string key = property_pair_match[1]; // e.g., "fill" + std::string value = property_pair_match[2]; // e.g., "none" + properties[key] = value; // Add the property to the map + prop_search_start = property_pair_match.suffix().first; // Continue searching for more properties + } + + // Process the list of classes (comma-separated classes) + std::regex individual_class_regex(R"(\.[\w\-]+)"); + auto class_search_start = class_list.cbegin(); + std::smatch individual_class_match; + while (std::regex_search(class_search_start, class_list.cend(), individual_class_match, individual_class_regex)) { + std::string class_name = individual_class_match[0]; // Extract the individual class (e.g., .cls-1) + + if( class_name.size() > 0 && class_name[0] == '.' ) { + class_name = class_name.substr(1, std::string::npos); + } + + // Merge properties for the class (with priority for the latest properties) + for (const auto& prop : properties) { + auto& svgCssClass = addClass(class_name); + svgCssClass.addProperty(prop.first, prop.second); +// cssClasses[class_name][prop.first] = prop.second; + } + + class_search_start = individual_class_match.suffix().first; // Move to the next class + } + } + + search_start = property_match.suffix().first; // Move to the next block + } + return classes.size() > 0; +} + +//-------------------------------------------------- +void ofxSvgCssStyleSheet::clear() { + classes.clear(); +} + +//-------------------------------------------------- +ofxSvgCssClass& ofxSvgCssStyleSheet::addClass(std::string aname) { + if( hasClass(aname) ) { + return classes[aname]; + } + ofxSvgCssClass tclass; + tclass.name = aname; + classes[aname] = tclass; + return classes[aname]; +} + +//-------------------------------------------------- +bool ofxSvgCssStyleSheet::hasClass(const std::string& aname) { + return classes.count(aname) > 0; +} + +//-------------------------------------------------- +ofxSvgCssClass& ofxSvgCssStyleSheet::getClass( const std::string& aname ) { + if( hasClass(aname)) { + return classes[aname]; + } + ofLogWarning("ofxSvgCssStyleSheet") << "could not find class " << aname; + return dummyClass; +} + +//-------------------------------------------------- +ofxSvgCssClass& ofxSvgCssStyleSheet::getAddClass( ofxSvgCssClass& aclass ) { + + for( auto& tclass : classes ) { + bool bFoundAll = true; + + // check for the same number of properties + if( tclass.second.properties.size() != aclass.properties.size() ) { + continue; + } + + for( auto& aprop : aclass.properties ) { + bool bFound = false; + for( auto& tprop : tclass.second.properties ) { + if( tprop.first == aprop.first && tprop.second.srcString == aprop.second.srcString) { + bFound = true; + break; + } + } + if( !bFound ) { + bFoundAll = false; + } + } + + if( bFoundAll ) { + // we found another class that has all the things +// ofLogNotice("CssStyleSheet::getAddClass") << "found matching class: " << tclass.second.name; + return classes[tclass.second.name]; + } + } + + // check if name already exists + std::string className = aclass.name; + int ccounter = 0; + bool bKeepGoing = true; + while( bKeepGoing ) { + bool bFound = false; + for( auto& tclass : classes ) { + if( className == tclass.first ) { + bFound = true; + break; + } + } + if( bFound ) { + ccounter ++; + className = aclass.name + ofToString(ccounter); + } else { + bKeepGoing = false; + } + } + + aclass.name = className; + classes[className] = aclass; + return classes[className]; +} + +//-------------------------------------------------- +std::string ofxSvgCssStyleSheet::toString(bool aBPrettyPrint) { + std::stringstream ss; + for( auto& citer : classes ) { + ss << std::endl; + ss << "." << citer.first << " { "; + ss << citer.second.toString(aBPrettyPrint); + ss << "}" << std::endl; + } + + return ss.str(); +} diff --git a/addons/ofxSvg/src/ofxSvgCss.h b/addons/ofxSvg/src/ofxSvgCss.h new file mode 100644 index 00000000000..04794767092 --- /dev/null +++ b/addons/ofxSvg/src/ofxSvgCss.h @@ -0,0 +1,134 @@ +// +// ofxSvg2Css.h +// Example +// +// Created by Nick Hardeman on 8/22/24. +// + +#pragma once +#include +#include "ofColor.h" +#include "ofLog.h" +#include "ofXml.h" + +class ofxSvgCssClass { +public: + + // adding this Optional class since std::optional is not a part of all std:: distributions at the moment, looking at you gcc < 10 + template + class Optional { + public: + Optional() : hasValue(false) {} // Default constructor, no value + Optional(const T& value) : hasValue(true), data(value) {} // Construct with a value + Optional(T&& value) : hasValue(true), data(std::move(value)) {} // Move constructor + + // Copy and move constructors + Optional(const Optional& other) = default; + Optional(Optional&& other) noexcept = default; + + // Assignment operators + Optional& operator=(const Optional& other) = default; + Optional& operator=(Optional&& other) noexcept = default; + + // Destructor + ~Optional() = default; + + // Check if there's a value + bool has_value() const { return hasValue; } + + // Accessors for the value + T& value() { +// if (!hasValue) throw std::runtime_error("No value present"); + if (!hasValue) { + ofLogError("ofx::svg::CssClass") << "No value present"; + } + return data; + } + + const T& value() const { +// if (!hasValue) throw std::runtime_error("No value present"); + if (!hasValue) { + ofLogError("ofx::svg::CssClass") << "No value present"; + } + return data; + } + + // Reset to an empty state + void reset() { hasValue = false; } + + private: + bool hasValue; + T data; + }; + + class Property { + public: + std::string srcString; + Optional fvalue; + Optional ivalue; + Optional svalue; + Optional cvalue; + }; + + std::unordered_map properties; + std::string name = "default"; + + void clear(); + + static std::string sRgbaToHexString(unsigned char r, unsigned char g, unsigned char b, unsigned char a); + static std::string sRgbToHexString(unsigned char r, unsigned char g, unsigned char b); + static bool sIsNone( const std::string& astr ); + static ofColor sGetColor(const std::string& astr); + static float sGetFloat(const std::string& astr); + + bool addProperties( std::string aPropertiesString ); + bool addProperty( std::string aPropString ); + bool addProperty( std::string aName, std::string avalue ); + bool addProperty( const std::string& aName, const Property& aprop ); + bool addProperty( const std::string& aName, const float& avalue ); + bool addProperty( const std::string& aName, const ofColor& acolor ); + + bool setFillColor(const ofColor& acolor); + bool setNoFill(); + bool isFilled(); + + bool setStrokeColor(const ofColor& acolor); + bool setStrokeWidth( const float& awidth ); + bool setNoStroke(); + bool hasStroke(); + + bool hasProperty( const std::string& akey ); + Property& getProperty( const std::string& akey ); + bool isNone(const std::string& akey); + bool hasAndIsNone(const std::string& akey); + + std::string getValue(const std::string& akey, const std::string& adefault); + int getIntValue(const std::string& akey, const int& adefault); + float getFloatValue(const std::string& akey, const float& adefault); + ofColor getColor(const std::string& akey); + + std::string toString(bool aBPrettyPrint=true); + +protected: + Property dummyProp; +}; + +class ofxSvgCssStyleSheet { +public: + + bool parse( std::string aCssString ); + void clear(); + + ofxSvgCssClass& addClass( std::string aname ); + bool hasClass( const std::string& aname ); + ofxSvgCssClass& getClass( const std::string& aname ); + + ofxSvgCssClass& getAddClass( ofxSvgCssClass& aclass ); + + std::unordered_map classes; + + std::string toString(bool aBPrettyPrint=true); + +protected: + ofxSvgCssClass dummyClass; +}; diff --git a/addons/ofxSvg/src/ofxSvgElements.cpp b/addons/ofxSvg/src/ofxSvgElements.cpp new file mode 100755 index 00000000000..968e47e0f99 --- /dev/null +++ b/addons/ofxSvg/src/ofxSvgElements.cpp @@ -0,0 +1,545 @@ +// +// ofxSvgElements.cpp +// +// Created by Nick Hardeman on 7/31/15. +// + +#include "ofxSvgElements.h" +#include "ofGraphics.h" + +using std::vector; +using std::string; + +std::map< string, ofxSvgText::Font > ofxSvgText::fonts; +ofTrueTypeFont ofxSvgText::defaultFont; + +//-------------------------------------------------------------- +std::string ofxSvgElement::sGetTypeAsString(ofxSvgType atype) { + switch (atype) { + case ofxSvgType::TYPE_GROUP: + return "Group"; + break; + case ofxSvgType::TYPE_RECTANGLE: + return "Rectangle"; + break; + case ofxSvgType::TYPE_IMAGE: + return "Image"; + break; + case ofxSvgType::TYPE_ELLIPSE: + return "Ellipse"; + break; + case ofxSvgType::TYPE_CIRCLE: + return "Circle"; + break; + case ofxSvgType::TYPE_PATH: + return "Path"; + break; + case ofxSvgType::TYPE_TEXT: + return "Text"; + break; + case ofxSvgType::TYPE_DOCUMENT: + return "Document"; + break; + case ofxSvgType::TYPE_ELEMENT: + return "Element"; + break; + default: + break; + } + return "Unknown"; +} + +//-------------------------------------------------------------- +std::string ofxSvgElement::sGetSvgXmlName(ofxSvgType atype) { + switch (atype) { + case ofxSvgType::TYPE_GROUP: + return "g"; + break; + case ofxSvgType::TYPE_RECTANGLE: + return "rect"; + break; + case ofxSvgType::TYPE_IMAGE: + return "image"; + break; + case ofxSvgType::TYPE_ELLIPSE: + return "ellipse"; + break; + case ofxSvgType::TYPE_CIRCLE: + return "circle"; + break; + case ofxSvgType::TYPE_PATH: + return "path"; + break; + case ofxSvgType::TYPE_TEXT: + return "text"; + break; + case ofxSvgType::TYPE_DOCUMENT: + return "svg"; + break; + case ofxSvgType::TYPE_ELEMENT: + return "element"; + break; + default: + break; + } + return "unknown"; +} + +//-------------------------------------------------------------- +string ofxSvgElement::getTypeAsString() { + return sGetTypeAsString(getType()); +} + +//-------------------------------------------------------------- +string ofxSvgElement::toString( int nlevel ) { + + string tstr = ""; + for( int k = 0; k < nlevel; k++ ) { + tstr += " "; + } + tstr += getTypeAsString() + " - " + getName() + "\n"; + + return tstr; +} + +////-------------------------------------------------------------- +//glm::mat4 ofxSvgBase::getTransformMatrix() { +// glm::mat4 rmat = glm::translate(glm::mat4(1.0f), glm::vec3(pos.x, pos.y, 0.0f)); +// return rmat; +//} +// +////-------------------------------------------------------------- +//ofNode ofxSvgBase::getNodeTransform() { +// ofNode tnode; +// tnode.setPosition( pos.x, pos.y, 0.0f ); +// return tnode; +//} + +//-------------------------------------------------------------- +glm::mat4 ofxSvgElement::getTransformMatrix() { + glm::mat4 rmat = glm::translate(glm::mat4(1.0f), glm::vec3(pos.x, pos.y, 0.0f)); + if( rotation != 0.0f ) { + glm::quat rq = glm::angleAxis(ofDegToRad(rotation), glm::vec3(0.f, 0.f, 1.0f )); + rmat = rmat * glm::toMat4((const glm::quat&)rq); + } + if( scale.x != 1.0f || scale.y != 1.0f ) { + rmat = glm::scale(rmat, glm::vec3(scale.x, scale.y, 1.0f)); + } + return rmat; +}; + +//-------------------------------------------------------------- +ofNode ofxSvgElement::getNodeTransform() { + ofNode tnode;// = ofxSvgBase::getNodeTransform(); + tnode.setPosition(pos.x, pos.y, 0.0f); + if( rotation != 0.0f ) { + glm::quat rq = glm::angleAxis(ofDegToRad(rotation), glm::vec3(0.f, 0.f, 1.0f )); + tnode.setOrientation(rq); + } + tnode.setScale(1.0f); + if( scale.x != 1.0f || scale.y != 1.0f ) { + tnode.setScale(scale.x, scale.y, 1.f ); + } + return tnode; +} + +#pragma mark - Image +//-------------------------------------------------------------- +ofRectangle ofxSvgImage::getRectangle() { + return ofRectangle(pos.x, pos.y, getWidth(), getHeight()); +} + +//-------------------------------------------------------------- +void ofxSvgImage::draw() { + if( !bTryLoad ) { + img.load( getFilePath() ); + bTryLoad = true; + } + + if( isVisible() ) { + if( img.isAllocated() ) { + ofPushMatrix(); { + ofTranslate( pos.x, pos.y ); + if( rotation != 0.0 ) ofRotateZDeg( rotation ); + ofScale( scale.x, scale.y ); + if(bUseShapeColor) ofSetColor( getColor() ); + img.draw( 0, 0 ); + } ofPopMatrix(); + } + } +} + +//-------------------------------------------------------------- +glm::vec2 ofxSvgImage::getAnchorPointForPercent( float ax, float ay ) { + glm::vec2 ap = glm::vec2( width * ax * scale.x, height * ay * scale.y ); + ap = glm::rotate(ap, glm::radians(rotation)); + return ap; +} + +#pragma mark - Text + +//-------------------------------------------------------------- +void ofxSvgText::create() { + meshes.clear(); + + // now lets sort the text based on meshes that we need to create // + vector< std::shared_ptr > tspans = textSpans; + + std::map< string, std::map< int, vector > > > tspanFonts; + for( std::size_t i = 0; i < tspans.size(); i++ ) { + if( tspanFonts.count( tspans[i]->fontFamily ) == 0 ) { + std::map< int, vector< std::shared_ptr> > tmapap; + tspanFonts[ tspans[i]->fontFamily ] = tmapap; + } + std::map< int, vector< std::shared_ptr> >& spanMap = tspanFonts[ tspans[i]->fontFamily ]; + if( spanMap.count(tspans[i]->fontSize) == 0 ) { + vector< std::shared_ptr > tvec; + spanMap[ tspans[i]->fontSize ] = tvec; + } + spanMap[ tspans[i]->fontSize ].push_back( tspans[i] ); + } + + + bool bHasFontDirectory = false; +// cout << "checking directory: " << fdirectory+"/fonts/" << endl; + string fontsDirectory = ofToDataPath("", true); + if( fdirectory != "" ) { + fontsDirectory = fdirectory;//+"/fonts/"; + } + if( ofFile::doesFileExist( fontsDirectory )) { + bHasFontDirectory = true; + } + + std::map< string, std::map< int, vector< std::shared_ptr> > >::iterator mainIt; + for( mainIt = tspanFonts.begin(); mainIt != tspanFonts.end(); ++mainIt ) { + if( fonts.count(mainIt->first) == 0 ) { + Font tafont; + tafont.fontFamily = mainIt->first; + fonts[ mainIt->first ] = tafont; + } + + // now create a mesh for the family // + // map< string, map > meshes; + if( meshes.count(mainIt->first) == 0 ) { + std::map< int, ofMesh > tempMeshMap; + meshes[ mainIt->first ] = tempMeshMap; + } + + Font& tfont = fonts[ mainIt->first ]; + std::map< int, ofMesh >& meshMap = meshes[ mainIt->first ]; + + std::map< int, vector> >::iterator vIt; + for( vIt = mainIt->second.begin(); vIt != mainIt->second.end(); ++vIt ) { + vector>& spanSpans = vIt->second; + bool bFontLoadOk = true; + if (tfont.sizes.count(vIt->first) == 0) { +// string _filename, int _fontSize, bool _bAntiAliased, bool _bFullCharacterSet, bool _makeContours, float _simplifyAmt, int _dpi + // first let's see if the fonts are provided. Some system fonts are .dfont that have several of the faces + // in them, but OF isn't setup to parse them, so we need each bold, regular, italic, etc to be a .ttf font // + string tfontPath = tfont.fontFamily; + if (bHasFontDirectory) { + + ofLogNotice("ofxSvgText") << __FUNCTION__ << " : " << tfont.fontFamily << " : starting off searching directory : " << fontsDirectory; + string tNewFontPath = ""; + bool bFoundTheFont = _recursiveFontDirSearch(fontsDirectory, tfont.fontFamily, tNewFontPath); + if (bFoundTheFont) { + tfontPath = tNewFontPath; + } + + /*ofDirectory tfDir; + tfDir.listDir( fontsDirectory ); + for( int ff = 0; ff < tfDir.size(); ff++ ) { + ofFile tfFile = tfDir.getFile(ff); + if( tfFile.getExtension() == "ttf" || tfFile.getExtension() == "otf" ) { + cout << ff << " - font family: " << tfont.fontFamily << " file name: " << tfFile.getBaseName() << endl; + if( ofToLower(tfFile.getBaseName()) == ofToLower(tfont.fontFamily) ) { + ofLogNotice(" >> ofxSvgText found font file for " ) << tfont.fontFamily; + tfontPath = tfFile.getAbsolutePath(); + break; + } + } + }*/ + } + + ofLogNotice("ofxSvgText") << __FUNCTION__ << " : Trying to load font from: " << tfontPath; + + if (tfontPath == "") { + bFontLoadOk = false; + } + else { + // load(const std::string& _filename, int _fontSize, bool _bAntiAliased, bool _bFullCharacterSet, bool _makeContours, float _simplifyAmt, int _dpi) + bFontLoadOk = tfont.sizes[vIt->first].load(tfontPath, vIt->first, true, true, false, 0.5, 72); + } + if(bFontLoadOk) { +// tfont.sizes[ vIt->first ].setSpaceSize( 0.57 ); +// tfont.sizes[ vIt->first ] = datFontTho; + tfont.textures[ vIt->first ] = tfont.sizes[ vIt->first ].getFontTexture(); + } else { + ofLogError("ofxSvgText") << __FUNCTION__ << " : error loading font family: " << tfont.fontFamily << " size: " << vIt->first; + tfont.sizes.erase(vIt->first); + } + } + if( !bFontLoadOk ) continue; + + if( meshMap.count(vIt->first) == 0 ) { + meshMap[ vIt->first ] = ofMesh(); + } + ofMesh& tmesh = meshMap[ vIt->first ]; + + if( !tfont.sizes.count( vIt->first ) ) { + ofLogError("ofxSvgText") << __FUNCTION__ << " : Could not find that font size in the map: " << vIt->first; + continue; + } + + ofTrueTypeFont& ttfont = tfont.sizes[ vIt->first ]; + for( std::size_t i = 0; i < spanSpans.size(); i++ ) { + // create a mesh here // + std::shared_ptr& cspan = spanSpans[i]; + if( cspan->text == "" ) continue; +// cout << "font family: " << cspan.fontFamily << " size: " << cspan.fontSize << " text: " << cspan.text << endl; + +// const ofMesh& stringMesh = ttfont.getStringMesh( "please work", 20, 20 ); + + ofRectangle tempBounds = ttfont.getStringBoundingBox( cspan->text, 0, 0 ); + float tffontx = bCentered ? cspan->rect.x - tempBounds.width/2 : cspan->rect.x; +// const ofMesh& stringMesh = ttfont.getStringMesh( cspan.text, tffontx-ogPos.x, cspan.rect.y-ogPos.y ); + const ofMesh& stringMesh = ttfont.getStringMesh( cspan->text, tffontx, cspan->rect.y ); + int offsetIndex = tmesh.getNumVertices(); + + vector tsIndices = stringMesh.getIndices(); + for( std::size_t k = 0; k < tsIndices.size(); k++ ) { + tsIndices[k] = tsIndices[k] + offsetIndex; + } + + ofFloatColor tcolor = cspan->color; + vector< ofFloatColor > tcolors; + tcolors.assign( stringMesh.getVertices().size(), tcolor ); + + tmesh.addIndices( tsIndices ); + tmesh.addVertices( stringMesh.getVertices() ); + tmesh.addTexCoords( stringMesh.getTexCoords() ); + tmesh.addColors( tcolors ); + } + } + } + + // now loop through and set the width and height of the text spans // + for( std::size_t i = 0; i < textSpans.size(); i++ ) { + auto& tempSpan = textSpans[i]; + ofTrueTypeFont& tfont = tempSpan->getFont(); + if( tfont.isLoaded() ) { + ofRectangle tempBounds = tfont.getStringBoundingBox( tempSpan->text, 0, 0 ); + tempSpan->rect.width = tempBounds.width; + tempSpan->rect.height = tempBounds.height; + tempSpan->lineHeight = tfont.getStringBoundingBox("M", 0, 0).height; +// tempSpan.rect.x = tempSpan.rect.x - ogPos.x; +// tempSpan.rect.y = tempSpan.rect.x - ogPos.x; + //tempSpan.rect.y -= tempSpan.lineHeight; + } + } +} + +//-------------------------------------------------------------- +void ofxSvgText::draw() { + if( !isVisible() ) return; +// map< string, map > meshes; + if(bUseShapeColor) { + ofSetColor( 255, 255, 255, 255.f * alpha ); + } + std::map< string, std::map >::iterator mainIt; + + ofPushMatrix(); { + ofSetColor( 255, 0, 0 ); + ofDrawCircle(pos, 6); + ofNoFill(); +// ofSetColor( 0, 0, 224 ); +// ofDrawCircle( ogPos, 10); + ofDrawRectangle(getRectangle()); + ofFill(); + +// ofLogNotice("ofx::svg2") << "Text: num text spans: " << textSpans.size() << " meshes size: " << meshes.size(); + + ofTranslate( pos.x, pos.y ); + +// ofSetColor( 255, 255, 255, 255.f * alpha ); + if( rotation > 0 ) ofRotateZDeg( rotation ); + ofTexture* tex = NULL; + for( mainIt = meshes.begin(); mainIt != meshes.end(); ++mainIt ) { + string fontFam = mainIt->first; + std::map< int, ofMesh >::iterator mIt; + for( mIt = meshes[ fontFam ].begin(); mIt != meshes[ fontFam ].end(); ++mIt ) { + int fontSize = mIt->first; + // let's check to make sure that the texture is there, so that we can bind it // + bool bHasTexture = false; + // static map< string, Font > fonts; + if( fonts.count( fontFam ) ) { + if( fonts[ fontFam ].textures.count( fontSize ) ) { + bHasTexture = true; + tex = &fonts[ fontFam ].textures[ fontSize ]; + } + } + + if( bHasTexture ) tex->bind(); + ofMesh& tMeshMesh = mIt->second; + if( bUseShapeColor ) { + vector< ofFloatColor >& tcolors = tMeshMesh.getColors(); + for( auto& tc : tcolors ) { + if( bOverrideColor ) { + tc = _overrideColor; + } else { + tc.a = alpha; + } + } + } else { + tMeshMesh.disableColors(); + } + tMeshMesh.draw(); + if( bHasTexture ) tex->unbind(); + tMeshMesh.enableColors(); + } + } + } ofPopMatrix(); + +} + +//-------------------------------------------------------------- +void ofxSvgText::draw(const std::string &astring, bool abCentered ) { + if( textSpans.size() > 0 ) { + ofPushMatrix(); { + ofTranslate( pos.x, pos.y ); + if( rotation > 0 ) ofRotateZDeg( rotation ); + textSpans[0]->draw(astring, abCentered ); + } ofPopMatrix(); + } else { + ofLogVerbose("ofxSvgText") << __FUNCTION__ << " : no text spans to draw with."; + } +} + +//-------------------------------------------------------------- +void ofxSvgText::draw(const std::string& astring, const ofColor& acolor, bool abCentered ) { + if( textSpans.size() > 0 ) { + ofPushMatrix(); { + ofTranslate( pos.x, pos.y ); + if( rotation > 0 ) ofRotateZDeg( rotation ); + textSpans[0]->draw(astring, acolor, abCentered ); + } ofPopMatrix(); + } else { + ofLogVerbose("ofxSvgText") << __FUNCTION__ << " : no text spans to draw with."; + } +} + +//-------------------------------------------------------------- +bool ofxSvgText::_recursiveFontDirSearch(const string& afile, const string& aFontFamToLookFor, string& fontpath) { + if (fontpath != "") { + return true; + } + ofFile tfFile( afile, ofFile::Reference ); + if (tfFile.isDirectory()) { + ofLogVerbose("ofxSvgText") << __FUNCTION__ << " : searching in directory : " << afile << " | " << ofGetFrameNum(); + ofDirectory tdir; + tdir.listDir(afile); + tdir.sort(); + for (std::size_t i = 0; i < tdir.size(); i++) { + bool youGoodOrWhat = _recursiveFontDirSearch(tdir.getPath(i), aFontFamToLookFor, fontpath); + if( youGoodOrWhat ) { + return true; + } + } + tdir.close(); + } else { + if ( tfFile.getExtension() == "ttf" || tfFile.getExtension() == "otf") { + if (ofToLower( tfFile.getBaseName() ) == ofToLower(aFontFamToLookFor)) { + ofLogNotice("ofxSvgText") << __FUNCTION__ << " : found font file for " << aFontFamToLookFor; + fontpath = tfFile.getAbsolutePath(); + return true; + } + string tAltFileName = ofToLower(tfFile.getBaseName()); + ofStringReplace(tAltFileName, " ", "-"); + if (tAltFileName == ofToLower(aFontFamToLookFor)) { + ofLogNotice("ofxSvgText") << __FUNCTION__ << " : found font file for " << aFontFamToLookFor; + fontpath = tfFile.getAbsolutePath(); + return true; + } + } + } + return false; +} + +// must return a reference for some reason here // +//-------------------------------------------------------------- +ofTrueTypeFont& ofxSvgText::TextSpan::getFont() { + if( ofxSvgText::fonts.count( fontFamily ) > 0 ) { + Font& tfont = fonts[ fontFamily ]; + if( tfont.sizes.count(fontSize) > 0 ) { + return tfont.sizes[ fontSize ]; + } + } + return defaultFont; +} + +//-------------------------------------------------------------- +void ofxSvgText::TextSpan::draw(const std::string &astring, bool abCentered ) { + draw( astring, color, abCentered ); +} + +//-------------------------------------------------------------- +void ofxSvgText::TextSpan::draw(const std::string &astring, const ofColor& acolor, bool abCentered ) { + ofSetColor( acolor ); + auto& cfont = getFont(); + ofRectangle tempBounds = cfont.getStringBoundingBox( astring, 0, 0 ); + float tffontx = abCentered ? rect.getCenter().x - tempBounds.width/2 : rect.x; + // const ofMesh& stringMesh = cfont.getStringMesh( astring, tffontx, rect.y ); + // cfont.drawString(astring, tffontx, rect.y + (tempBounds.getHeight() - lineHeight) ); + cfont.drawString(astring, tffontx, rect.y ); +} + +//-------------------------------------------------------------- +ofTrueTypeFont& ofxSvgText::getFont() { + if( textSpans.size() > 0 ) { + return textSpans[0]->getFont(); + } + ofLogWarning("ofxSvgText") << __FUNCTION__ << " : no font detected from text spans, returning default font."; + return defaultFont; +} + +//-------------------------------------------------------------- +ofColor ofxSvgText::getColor() { + if( textSpans.size() > 0 ) { + return textSpans[0]->color; + } + ofLogWarning("ofxSvgText") << __FUNCTION__ << " : no font detected from text spans, returning path fill color."; + return path.getFillColor(); +} + +// get the bounding rect for all of the text spans in this svg'ness +// should be called after create // +//-------------------------------------------------------------- +ofRectangle ofxSvgText::getRectangle() { + ofRectangle temp( 0, 0, 1, 1 ); + for( std::size_t i = 0; i < textSpans.size(); i++ ) { + ofRectangle trect = textSpans[i]->rect; + trect.x = trect.x;// - ogPos.x; + trect.y = trect.y;// - ogPos.y; + trect.y -= textSpans[i]->lineHeight; + if( i == 0 ) { + temp = trect; + } else { + temp.growToInclude( trect ); + } + } + + + temp.x += pos.x; + temp.y += pos.y; + return temp; +} + + + + + + + + + + diff --git a/addons/ofxSvg/src/ofxSvgElements.h b/addons/ofxSvg/src/ofxSvgElements.h new file mode 100755 index 00000000000..a4ccd301ba5 --- /dev/null +++ b/addons/ofxSvg/src/ofxSvgElements.h @@ -0,0 +1,250 @@ +// +// ofxSvgElements.h +// +// Created by Nick Hardeman on 7/31/15. +// + +#pragma once +#include "ofNode.h" +#include "ofImage.h" +#include "ofPath.h" +#include +#include "ofTrueTypeFont.h" + +enum ofxSvgType { + TYPE_ELEMENT = 0, + TYPE_GROUP, + TYPE_RECTANGLE, + TYPE_IMAGE, + TYPE_ELLIPSE, + TYPE_CIRCLE, + TYPE_PATH, + TYPE_TEXT, + TYPE_DOCUMENT, + TYPE_TOTAL +}; + +class ofxSvgElement { +public: + + static std::string sGetTypeAsString(ofxSvgType atype); + static std::string sGetSvgXmlName(ofxSvgType atype); + + virtual ofxSvgType getType() {return ofxSvgType::TYPE_ELEMENT;} + std::string getTypeAsString(); + + std::string getName() { return name; } + bool isGroup() { + return (getType() == ofxSvgType::TYPE_GROUP); + } + + void setVisible( bool ab ) { bVisible = ab; } + bool isVisible() { return bVisible; } + + virtual std::string toString(int nlevel = 0); + + std::string name = ""; + float layer = -1.f; + bool bVisible=true; + bool bUseShapeColor = true; + + glm::vec2 pos = glm::vec2(0.f, 0.f); + glm::vec2 scale = glm::vec2(1.0f, 1.0f); + float rotation = 0.0f; + + virtual glm::mat4 getTransformMatrix(); + virtual ofNode getNodeTransform(); + + virtual void draw() {} + + virtual void setUseShapeColor( bool ab ) { + bUseShapeColor = ab; + } + + virtual ofPolyline getFirstPolyline() { + ofLogWarning("ofxSvgElement") << __FUNCTION__ << " : Element " << getTypeAsString() << " does not have a path."; + return ofPolyline(); + } +//protected: + // used for saving to set the model position of the current mat4 // + glm::vec2 mModelPos = glm::vec2(0.f, 0.f); + glm::vec2 mModelRotationPoint = glm::vec2(0.f, 0.f); + +}; + +class ofxSvgPath : public ofxSvgElement { +public: + virtual ofxSvgType getType() override {return ofxSvgType::TYPE_PATH;} + + virtual void setUseShapeColor( bool ab ) override { + ofxSvgElement::setUseShapeColor(ab); + path.setUseShapeColor(ab); + } + + virtual void draw() override { + if(isVisible()) path.draw(); + } + + bool isFilled() { return path.isFilled(); } + bool hasStroke() { return path.hasOutline(); } + float getStrokeWidth() { return path.getStrokeWidth(); } + ofColor getFillColor() { return path.getFillColor(); } + ofColor getStrokeColor() { return path.getStrokeColor(); } + + ofPolyline getFirstPolyline() override { + if( path.getOutline().size() > 0 ) { + return path.getOutline()[0]; + } + ofLogWarning("ofxSvgPath") << __FUNCTION__ << " : path does not have an outline."; + return ofPolyline(); + } + + ofPath path; +}; + +class ofxSvgRectangle : public ofxSvgPath { +public: + virtual ofxSvgType getType() override {return ofxSvgType::TYPE_RECTANGLE;} + ofRectangle rectangle; + + float getWidth() { return rectangle.getWidth() * scale.x;} + float getHeight() { return rectangle.getHeight() * scale.y;} + + float round = 0.f; + +// float getWidthScaled() { return rectangle.getWidth() * scale.x;} +// float getHeightScaled() { return rectangle.getHeight() * scale.y;} +}; + +class ofxSvgImage : public ofxSvgElement { +public: + virtual ofxSvgType getType() override {return ofxSvgType::TYPE_IMAGE;} + + float getWidth() { return width * scale.x;} + float getHeight() { return height * scale.y;} + + ofRectangle getRectangle(); +// float getWidthScaled() { return getWidth() * scale.x;} +// float getHeightScaled() { return getHeight() * scale.y;} + + virtual void draw() override; + glm::vec2 getAnchorPointForPercent( float ax, float ay ); + + std::filesystem::path getFilePath() { return filepath; } + + void setColor( ofColor aColor ) { + color = aColor; + } + ofColor getColor() { + return color; + } + + ofColor color; + ofImage img; + bool bTryLoad = false; + of::filesystem::path filepath; + float width = 0.f; + float height = 0.f; +}; + +class ofxSvgCircle : public ofxSvgPath { +public: + virtual ofxSvgType getType() override {return ofxSvgType::TYPE_CIRCLE;} + float getRadius() {return radius;} + float radius = 10.0; +}; + +class ofxSvgEllipse : public ofxSvgPath { +public: + virtual ofxSvgType getType() override {return ofxSvgType::TYPE_ELLIPSE;} + float radiusX, radiusY = 10.0f; +}; + + +class ofxSvgText : public ofxSvgRectangle { +public: + class Font { + public: + std::string fontFamily; + std::map< int, ofTrueTypeFont > sizes; + std::map< int, ofTexture > textures; + }; + + static std::map< std::string, Font > fonts; + + class TextSpan { + public: + TextSpan() { + text = ""; + fontSize = 12; + lineHeight = 0; + } + + std::string text; + int fontSize = 12; + std::string fontFamily; + ofRectangle rect; + ofColor color; + float lineHeight = 12; + ofTrueTypeFont& getFont(); + void draw( const std::string& astring, bool abCentered ); + void draw(const std::string &astring, const ofColor& acolor, bool abCentered ); + }; + + static bool sortSpanOnFontFamily( const TextSpan& a, const TextSpan& b ) { + return a.fontFamily < b.fontFamily; + } + + static bool sortSpanOnFontSize( const TextSpan& a, const TextSpan& b ) { + return a.fontSize < b.fontSize; + } + +// Text() { type = OFX_SVG_TYPE_TEXT; fdirectory=""; bCentered=false; alpha=1.0; bOverrideColor=false; } + virtual ofxSvgType getType() override {return ofxSvgType::TYPE_TEXT;} + + ofTrueTypeFont& getFont(); + ofColor getColor(); + + void create(); + void draw() override; + void draw(const std::string &astring, bool abCentered ); + void draw(const std::string &astring, const ofColor& acolor, bool abCentered ); + + void setFontDirectory( std::string aPath ) { + fdirectory = aPath; + } + + void overrideColor( ofColor aColor ) { + bOverrideColor = true; + _overrideColor = aColor; + } + + ofRectangle getRectangle(); + + std::map< std::string, std::map > meshes; + std::vector< std::shared_ptr > textSpans; + + std::string fdirectory; + bool bCentered = false; + float alpha = 1.; + glm::vec2 ogPos = glm::vec2(0.f, 0.f); + +protected: + static ofTrueTypeFont defaultFont; + bool _recursiveFontDirSearch(const std::string& afile, const std::string& aFontFamToLookFor, std::string& fontpath); + ofFloatColor _overrideColor; + bool bOverrideColor = false; +}; + + + + + + + + + + + + + diff --git a/addons/ofxSvg/src/ofxSvgGroup.cpp b/addons/ofxSvg/src/ofxSvgGroup.cpp new file mode 100755 index 00000000000..e28138b1c0f --- /dev/null +++ b/addons/ofxSvg/src/ofxSvgGroup.cpp @@ -0,0 +1,288 @@ +// +// ofxSvgGroup.cpp +// +// Created by Nick Hardeman on 7/31/15. +// + +#include "ofxSvgGroup.h" +#include "ofGraphics.h" + +using std::vector; +using std::shared_ptr; +using std::string; + +//-------------------------------------------------------------- +void ofxSvgGroup::draw() { + std::size_t numElements = mChildren.size(); + bool bTrans = pos.x != 0 || pos.y != 0.0; + if( bTrans ) { + ofPushMatrix(); + ofTranslate(pos.x, pos.y); + } + for( std::size_t i = 0; i < numElements; i++ ) { + mChildren[i]->draw(); + } + if( bTrans ) { + ofPopMatrix(); + } +} + +//-------------------------------------------------------------- +std::size_t ofxSvgGroup::getNumChildren() { + return mChildren.size(); +} + +//-------------------------------------------------------------- +vector< shared_ptr >& ofxSvgGroup::getChildren() { + return mChildren; +} + +//-------------------------------------------------------------- +vector< shared_ptr > ofxSvgGroup::getAllChildren(bool aBIncludeGroups) { + vector< shared_ptr > retElements; + + for( auto ele : mChildren ) { + _getAllElementsRecursive( retElements, ele, aBIncludeGroups ); + } + + return retElements; +} + +//-------------------------------------------------------------- +std::vector< std::shared_ptr > ofxSvgGroup::getAllChildGroups() { + vector< shared_ptr > retGroups; + for( auto ele : mChildren ) { + _getAllGroupsRecursive( retGroups, ele ); + } + return retGroups; +} + +// flattens out hierarchy // +//-------------------------------------------------------------- +void ofxSvgGroup::_getAllElementsRecursive( vector< shared_ptr >& aElesToReturn, shared_ptr aele, bool aBIncludeGroups ) { + if( aele ) { + if( aele->isGroup() ) { + shared_ptr tgroup = std::dynamic_pointer_cast(aele); + if(aBIncludeGroups) {aElesToReturn.push_back(tgroup);} + for( auto ele : tgroup->getChildren() ) { + _getAllElementsRecursive( aElesToReturn, ele, aBIncludeGroups ); + } + } else { + aElesToReturn.push_back( aele ); + } + } +} + +//-------------------------------------------------------------- +void ofxSvgGroup::_getAllGroupsRecursive( std::vector< std::shared_ptr >& aGroupsToReturn, std::shared_ptr aele ) { + if( aele ) { + if( aele->isGroup() ) { + shared_ptr tgroup = std::dynamic_pointer_cast(aele); + aGroupsToReturn.push_back(tgroup); + for( auto ele : tgroup->getChildren() ) { + if( ele->isGroup() ) { + _getAllGroupsRecursive( aGroupsToReturn, ele ); + } + } + } + } +} + +//-------------------------------------------------------------- +std::vector< std::shared_ptr > ofxSvgGroup::getAllElementsWithPath() { + auto allKids = getAllChildren(false); + std::vector< std::shared_ptr > rpaths; + for( auto kid : allKids ) { + if( kid->getType() == ofxSvgType::TYPE_RECTANGLE ) { + rpaths.push_back(std::dynamic_pointer_cast(kid)); + } else if( kid->getType() == ofxSvgType::TYPE_PATH ) { + rpaths.push_back(std::dynamic_pointer_cast(kid)); + } else if( kid->getType() == ofxSvgType::TYPE_CIRCLE ) { + rpaths.push_back(std::dynamic_pointer_cast(kid)); + } else if( kid->getType() == ofxSvgType::TYPE_ELLIPSE ) { + rpaths.push_back(std::dynamic_pointer_cast(kid)); + } + } + + return rpaths; +} + +//-------------------------------------------------------------- +shared_ptr ofxSvgGroup::getElementForName( std::string aPath, bool bStrict ) { + + vector< std::string > tsearches; + if( ofIsStringInString( aPath, ":" ) ) { + tsearches = ofSplitString( aPath, ":" ); + } else { + tsearches.push_back( aPath ); + } + + shared_ptr temp; + _getElementForNameRecursive( tsearches, temp, mChildren, bStrict ); + return temp; +} + +//-------------------------------------------------------------- +std::vector< std::shared_ptr > ofxSvgGroup::getChildrenForName( const std::string& aname, bool bStrict ) { + std::vector< std::shared_ptr > relements; + for( auto& kid : mChildren ) { + if( bStrict ) { + if( kid->getName() == aname ) { + relements.push_back(kid); + } + } else { + if( ofIsStringInString( kid->getName(), aname )) { + relements.push_back(kid); + } + } + } + return relements; +} + +//-------------------------------------------------------------- +void ofxSvgGroup::_getElementForNameRecursive( vector& aNamesToFind, shared_ptr& aTarget, vector< shared_ptr >& aElements, bool bStrict ) { + + if( aNamesToFind.size() < 1 ) { + return; + } + if(aTarget) { + return; + } + + bool bKeepGoing = false; + std::string nameToFind = aNamesToFind[0]; + if( aNamesToFind.size() > 1 ) { + bKeepGoing = (aNamesToFind[0] == "*"); + nameToFind = aNamesToFind[1]; + } + for( std::size_t i = 0; i < aElements.size(); i++ ) { + bool bFound = false; + if(bStrict) { + if( aElements[i]->getName() == nameToFind ) { + bFound = true; + } + } else { +// std::cout << "Group::_getElementForNameRecursive: ele name: " << aElements[i]->getName() << " nameToFind: " << nameToFind << " keep going: " << bKeepGoing << std::endl; + if( ofIsStringInString( aElements[i]->getName(), nameToFind )) { + bFound = true; + } + + if (!bFound && aElements[i]->getType() == ofxSvgType::TYPE_TEXT) { + + if (aElements[i]->getName() == "No Name") { + // the ids for text block in illustrator are weird, + // so try to grab the name from the text contents // + auto etext = std::dynamic_pointer_cast(aElements[i]); + if (etext) { + if (etext->textSpans.size()) { +// cout << "Searching for " << aNamesToFind[0] << " in " << etext->textSpans.front().text << endl; + if(ofIsStringInString( etext->textSpans.front()->text, aNamesToFind[0] )) { + bFound = true; + } + } + } + } + } + } + + if( bFound && !bKeepGoing ) { + if( !bKeepGoing && aNamesToFind.size() > 0 ) { + aNamesToFind.erase( aNamesToFind.begin() ); + } + if(aNamesToFind.size() == 0 ) { + aTarget = aElements[i]; + break; + } else { + if( aElements[i]->getType() == ofxSvgType::TYPE_GROUP ) { + auto tgroup = std::dynamic_pointer_cast( aElements[i] ); + _getElementForNameRecursive( aNamesToFind, aTarget, tgroup->getChildren(), bStrict ); + break; + } + } + } + + if( bKeepGoing ) { + if( bFound ) { +// std::cout << "Group::_getElementForNameRecursive: SETTING TARGET: " << aElements[i]->getName() << " keep going: " << bKeepGoing << std::endl; + aTarget = aElements[i]; + break; + } else { + if( aElements[i]->getType() == ofxSvgType::TYPE_GROUP ) { +// std::cout << "Group::_getElementForNameRecursive: FOUND A GROUP, But still going: " << aElements[i]->getName() << " keep going: " << bKeepGoing << std::endl; + auto tgroup = std::dynamic_pointer_cast( aElements[i] ); + _getElementForNameRecursive( aNamesToFind, aTarget, tgroup->getChildren(), bStrict ); + } + } + } + } +} + +//-------------------------------------------------------------- +bool ofxSvgGroup::replace( shared_ptr aOriginal, shared_ptr aNew ) { + bool bReplaced = false; + _replaceElementRecursive( aOriginal, aNew, mChildren, bReplaced ); + return bReplaced; +} + +//-------------------------------------------------------------- +void ofxSvgGroup::_replaceElementRecursive( shared_ptr aTarget, shared_ptr aNew, vector< shared_ptr >& aElements, bool& aBSuccessful ) { + for( std::size_t i = 0; i < aElements.size(); i++ ) { + bool bFound = false; + if( aTarget == aElements[i] ) { + bFound = true; + aBSuccessful = true; + aElements[i] = aNew; + aNew->layer = aTarget->layer; + break; + } + if( !bFound ) { + if( aElements[i]->getType() == ofxSvgType::TYPE_GROUP ) { + auto tgroup = std::dynamic_pointer_cast( aElements[i] ); + _replaceElementRecursive(aTarget, aNew, tgroup->mChildren, aBSuccessful ); + } + } + } +} + +//-------------------------------------------------------------- +string ofxSvgGroup::toString(int nlevel) { + + string tstr = ""; + for( int k = 0; k < nlevel; k++ ) { + tstr += " "; + } + tstr += getTypeAsString() + " - " + getName() + "\n"; + + if( mChildren.size() ) { + for( std::size_t i = 0; i < mChildren.size(); i++ ) { + tstr += mChildren[i]->toString( nlevel+1); + } + } + + return tstr; +} + + +//-------------------------------------------------------------- +void ofxSvgGroup::disableColors() { + auto telements = getAllChildren(false); + for( auto& ele : telements ) { + ele->setUseShapeColor(false); + } +} + +//-------------------------------------------------------------- +void ofxSvgGroup::enableColors() { + auto telements = getAllChildren(false); + for( auto& ele : telements ) { + ele->setUseShapeColor(true); + } +} + + + + + + + + diff --git a/addons/ofxSvg/src/ofxSvgGroup.h b/addons/ofxSvg/src/ofxSvgGroup.h new file mode 100755 index 00000000000..8f4aa9a71cb --- /dev/null +++ b/addons/ofxSvg/src/ofxSvgGroup.h @@ -0,0 +1,196 @@ +// +// ofxSvgGroup.h +// +// Created by Nick Hardeman on 7/31/15. +// + +#pragma once +#include "ofxSvgElements.h" + +class ofxSvgGroup : public ofxSvgElement { +public: + + virtual ofxSvgType getType() override {return TYPE_GROUP;} + + virtual void draw() override; + + std::size_t getNumChildren();// override; + std::vector< std::shared_ptr >& getChildren(); + std::vector< std::shared_ptr > getAllChildren(bool aBIncludeGroups); + std::vector< std::shared_ptr > getAllChildGroups(); + + template + std::vector< std::shared_ptr > getElementsForType( std::string aPathToGroup="", bool bStrict= false ) { + auto temp = std::make_shared(); + auto sType = temp->getType(); + + std::vector< std::shared_ptr > telements; + std::vector< std::shared_ptr > elementsToSearch; + if( aPathToGroup == "" ) { + elementsToSearch = mChildren; + } else { + std::shared_ptr< ofxSvgElement > temp = getElementForName( aPathToGroup, bStrict ); + if( temp ) { + if( temp->isGroup() ) { + std::shared_ptr< ofxSvgGroup > tgroup = std::dynamic_pointer_cast( temp ); + elementsToSearch = tgroup->mChildren; + } + } + } + + if( !elementsToSearch.size() && mChildren.size() ) { + ofLogNotice("ofx::svg::Group") << __FUNCTION__ << " did not find group with name: " << aPathToGroup; + elementsToSearch = mChildren; + } + + for( std::size_t i = 0; i < elementsToSearch.size(); i++ ) { + if( elementsToSearch[i]->getType() == sType ) { + telements.push_back( std::dynamic_pointer_cast(elementsToSearch[i]) ); + } + } + return telements; + } + + template + std::shared_ptr getFirstElementForType( std::string aPathToGroup="", bool bStrict= false ) { + auto eles = getElementsForType(aPathToGroup, bStrict ); + if( eles.size() > 0 ) { + return eles[0]; + } + return std::shared_ptr(); + } + + template + std::vector< std::shared_ptr > getAllElementsForType() { + + auto temp = std::make_shared(); + auto sType = temp->getType(); + + std::vector< std::shared_ptr > telements; + auto elementsToSearch = getAllChildren(true); + + for( std::size_t i = 0; i < elementsToSearch.size(); i++ ) { + if( elementsToSearch[i]->getType() == sType ) { + telements.push_back( std::dynamic_pointer_cast(elementsToSearch[i]) ); + } + } + return telements; + } + + template + std::vector< std::shared_ptr > getAllElementsContainingNameForType(std::string aname) { + + auto temp = std::make_shared(); + auto sType = temp->getType(); + + std::vector< std::shared_ptr > telements; + // get all children does not include groups, since it's meant to flatten hierarchy + + if( sType == ofxSvgType::TYPE_GROUP ) { + auto groupsToSearch = getAllChildGroups(); + + for( std::size_t i = 0; i < groupsToSearch.size(); i++ ) { + if( groupsToSearch[i]->getType() == sType ) { + if( ofIsStringInString(groupsToSearch[i]->getName(), aname) ) { + telements.push_back( std::dynamic_pointer_cast(groupsToSearch[i]) ); + } + } + } + + } else { + auto elementsToSearch = getAllChildren(false); + for( std::size_t i = 0; i < elementsToSearch.size(); i++ ) { + if( elementsToSearch[i]->getType() == sType ) { + if( ofIsStringInString(elementsToSearch[i]->getName(), aname) ) { + telements.push_back( std::dynamic_pointer_cast(elementsToSearch[i]) ); + } + } + } + } + + + return telements; + } + + std::vector< std::shared_ptr > getAllElementsWithPath(); + + std::shared_ptr getElementForName( std::string aPath, bool bStrict = false ); + std::vector< std::shared_ptr > getChildrenForName( const std::string& aname, bool bStrict = false ); + + template + std::vector< std::shared_ptr > getChildrenForTypeForName( const std::string& aname, bool bStrict = false ) { + auto temp = std::make_shared(); + auto sType = temp->getType(); + + std::vector< std::shared_ptr > relements; + for( auto& kid : mChildren ) { + if( kid->getType() != sType ) {continue;} + if( bStrict ) { + if( kid->getName() == aname ) { + relements.push_back( std::dynamic_pointer_cast(kid)); + } + } else { + if( ofIsStringInString( kid->getName(), aname )) { + relements.push_back(std::dynamic_pointer_cast(kid)); + } + } + } + return relements; + } + + template + std::shared_ptr< ofxSvg_T > get( std::string aPath, bool bStrict = false ) { + auto stemp = std::dynamic_pointer_cast( getElementForName( aPath, bStrict ) ); + return stemp; + } + + template + std::shared_ptr< ofxSvg_T > get( int aIndex ) { + auto stemp = std::dynamic_pointer_cast( mChildren[ aIndex ] ); + return stemp; + } + + bool replace( std::shared_ptr aOriginal, std::shared_ptr aNew ); + + // adding + template + std::shared_ptr add(std::string aname) { + auto element = std::make_shared(); + element->name = aname; + mChildren.push_back(element); + return element; + }; + + void add( std::shared_ptr aele ) { mChildren.push_back(aele); } + + virtual std::string toString(int nlevel = 0) override; + + void disableColors(); + void enableColors(); + +protected: + void _getElementForNameRecursive( std::vector< std::string >& aNamesToFind, std::shared_ptr& aTarget, std::vector< std::shared_ptr >& aElements, bool bStrict ); + void _getAllElementsRecursive( std::vector< std::shared_ptr >& aElesToReturn, std::shared_ptr aele, bool aBIncludeGroups ); + + void _getAllGroupsRecursive( std::vector< std::shared_ptr >& aGroupsToReturn, std::shared_ptr aele ); + + void _replaceElementRecursive( std::shared_ptr aTarget, std::shared_ptr aNew, std::vector< std::shared_ptr >& aElements, bool& aBSuccessful ); + + std::vector< std::shared_ptr > mChildren; +}; + + + + + + + + + + + + + + + +