This commit is contained in:
2025-04-09 10:22:44 +02:00
commit 96ff5392c8
330 changed files with 28300 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
cmake_minimum_required(VERSION 3.28)
# define CXX interface library
# define target & alias (for other projects consuming the "source-tree")
add_library(iue-svg_svg INTERFACE)
# define sources of target
target_sources(iue-svg_svg
INTERFACE FILE_SET public_headers TYPE HEADERS BASE_DIRS ${PROJECT_SOURCE_DIR} FILES tags.hpp tree.hpp render.hpp)
# define installation step
install(TARGETS iue-svg_svg EXPORT iue-svg-export
FILE_SET public_headers DESTINATION "./")
# define test
if(BUILD_TESTING AND PROJECT_IS_TOP_LEVEL)
add_executable(iue-svg_tree.test tree.test.cpp)
target_link_libraries(iue-svg_tree.test iue-svg_svg)
add_test(NAME iue-svg_tree.test COMMAND iue-svg_tree.test)
add_executable(iue-svg_tags.test tags.test.cpp)
target_link_libraries(iue-svg_tags.test iue-svg_svg)
add_test(NAME iue-svg_tags.test COMMAND iue-svg_tags.test)
add_executable(iue-svg_render.test render.test.cpp)
target_link_libraries(iue-svg_render.test iue-svg_svg)
add_test(NAME iue-svg_render.test COMMAND iue-svg_render.test)
endif()
# export targets for consuming cmake projects
# generate and install a .cmake file to allow dependend projects consuming the "install-tree" to import all targets
install(EXPORT iue-svg-export DESTINATION "iue-svg" )
# allow dependend projects consuming the "build-tree" to use all targets
export(EXPORT iue-svg-export)
+157
View File
@@ -0,0 +1,157 @@
/// @file
/// @brief Educational implementation of a SVG-Writer
#pragma once
#include "iue-svg/tags.hpp" // iue::svg:::XMLTag, iue::svg::svg
#include "iue-svg/tree.hpp" // iue::svg::Tree
#include <cassert> // assert
#include <filesystem> // std::filesystem::path
#include <fstream> // std::ofstream
#include <sstream> // std::stringstream
#include <string> // std::string
#include <vector> // std::vector
namespace iue::svg {
/// @brief Generate a SVG file from sequences of compatible primitives
/// @return Enclosing bounding box (original unscaled coordinates)
/// Imporant notes:
/// 1. The outline of the svg will be the bounding box enclosing all occuring geometric primitives (based on coordinate
/// based)
/// - the detail of the extend of an object (textlength, fontsize, linewidth, ...) is **not** considered
/// 2. The coordinates are **scaled/rotated/translated before serializing**
/// - this is not so nice, but a scaling is required to improve the viewing experience for small coordinate spans
/// 3. All primitives are rendered with black lines and no fill
/// - the linewidth is choosen relative the the dimension of the enclosing bounding box
/// - the font-size for text is relative to the linewith
inline iue::svg::BBox render(std::filesystem::path filepath, const std::vector<iue::svg::BBox>& bboxes,
const std::vector<iue::svg::Circle>& circles, const std::vector<iue::svg::Triangle>& triangles) {
using XMLTree = Tree<XMLTag>;
assert(bboxes.size() != 0 || circles.size() != 0 || triangles.size() != 0);
// calculate maximum extends
Vec2d bbmin = {+std::numeric_limits<double>::max(), +std::numeric_limits<double>::max()};
Vec2d bbmax = {-std::numeric_limits<double>::max(), -std::numeric_limits<double>::max()};
// update minmax from single coordinate
auto minmax = [&bbmin, &bbmax](const Vec2d& p) {
bbmin[0] = bbmin[0] < p[0] ? bbmin[0] : p[0];
bbmin[1] = bbmin[1] < p[1] ? bbmin[1] : p[1];
bbmax[0] = bbmax[0] > p[0] ? bbmax[0] : p[0];
bbmax[1] = bbmax[1] > p[1] ? bbmax[1] : p[1];
};
for (const auto& circle : circles) {
const auto& [c, r] = circle;
minmax({c[0] - r, c[1] - r});
minmax({c[0] + r, c[1] + r});
}
for (const auto& bbox : bboxes) {
const auto& [min, max] = bbox;
minmax(min);
minmax(max);
}
for (const auto& triangle : triangles) {
for (const auto& p : triangle)
minmax(p);
}
// scaling
double resolution;
{
auto const& [xmin, ymin] = bbmin;
auto const& [xmax, ymax] = bbmax;
double width = std::abs(xmax - xmin);
double height = std::abs(ymax - ymin);
resolution = std::max(width, height);
}
double unit = resolution / 800; // stroke referens is "thin" for a reference resolution
double scale = 1 / unit;
// scaling to have a unit stroke of 1
auto sc = [scale](const Vec2d& p) -> Vec2d { return {p[0] * scale, p[1] * scale}; };
auto scd = [scale](const double& val) -> double { return val * scale; };
// transform to have bottom left origin (svg default is top left origin)
auto tf = [xmin = bbmin[0], ymax = bbmax[1]](const Vec2d& p) -> Vec2d { return {(p[0] - xmin), (-p[1] + ymax)}; };
// create tree with svg
XMLTree dom{svg(sc(bbmin), sc(bbmax))};
dom.org.nodes.push_back({svg::group()});
auto& g = dom.org.nodes.back();
{ // plot all triangles
for (const auto& triangle : triangles) {
const auto& [a, b, c] = triangle;
g.nodes.push_back({svg::polygon({sc(tf(a)), sc(tf(b)), sc(tf(c))})});
}
}
{ // plot all BBoxes
for (const auto& bbox : bboxes) {
const auto& [min, max] = bbox;
const auto& [xmin, ymin] = min;
const auto& [xmax, ymax] = max;
std::vector<Vec2d> coords;
coords.push_back(sc(tf({xmin, ymin})));
coords.push_back(sc(tf({xmax, ymin})));
coords.push_back(sc(tf({xmax, ymax})));
coords.push_back(sc(tf({xmin, ymax})));
g.nodes.push_back({svg::polygon(coords)});
}
}
{ // plot all circles
for (const auto& circle : circles) {
const auto& [c, r] = circle;
g.nodes.push_back({svg::circle({sc(tf(c)), scd(r)})});
}
}
{ // plot bounding box of viewport
const auto& [xmin, ymin] = bbmin;
const auto& [xmax, ymax] = bbmax;
std::vector<Vec2d> limits;
limits.push_back(sc(tf({xmin, ymin})));
limits.push_back(sc(tf({xmax, ymin})));
limits.push_back(sc(tf({xmax, ymax})));
limits.push_back(sc(tf({xmin, ymax})));
g.nodes.push_back({svg::polygon(limits)});
}
// serialize tree to string
std::stringstream stream;
{
auto down = [&stream](const XMLTag& value, std::size_t level) -> void {
stream << std::string(2 * level, ' ') << value.open() << std::endl;
auto text = value.text();
if (!text.empty())
stream << std::string(2 * level, ' ') << value.text() << std::endl;
};
auto up = [&stream](const XMLTag& value, std::size_t level) -> void {
auto close = value.close();
if (!close.empty())
stream << std::string(2 * level, ' ') << value.close() << std::endl;
};
XMLTree::preorder(dom.org, down, up);
}
// save as file
std::ofstream fs;
fs.exceptions(std::ifstream::badbit);
fs.open(filepath);
fs << stream.str();
return {bbmin, bbmax};
}
} // namespace iue::svg
+37
View File
@@ -0,0 +1,37 @@
/// @file
/// @brief Test for 'iue::svg::render'
#include "iue-svg/render.hpp" // iue::svg::render
#include "iue-num/numerics.hpp" // iue::num::isclose
#include "iue-svg/tags.hpp" // iue::svg::Circle, iue::svg::Triangle, iue::svg::BBox
int main() {
std::vector<iue::svg::Circle> circles;
circles.push_back(iue::svg::Circle{{1, 1}, 1});
circles.push_back(iue::svg::Circle{{2, 2}, 2});
circles.push_back(iue::svg::Circle{{3, 3}, 3});
circles.push_back(iue::svg::Circle{{10, 10}, 10});
std::vector<iue::svg::Triangle> triangles;
triangles.push_back(iue::svg::Triangle{{{50, 50}, {49, 50}, {50, 49}}});
triangles.push_back(iue::svg::Triangle{{{50, 50}, {48, 50}, {50, 48}}});
triangles.push_back(iue::svg::Triangle{{{50, 50}, {47, 50}, {50, 47}}});
triangles.push_back(iue::svg::Triangle{{{50, 50}, {40, 50}, {50, 40}}});
std::vector<iue::svg::BBox> bboxes;
bboxes.push_back(iue::svg::BBox{{0, 49}, {1, 50}});
bboxes.push_back(iue::svg::BBox{{0, 48}, {2, 50}});
bboxes.push_back(iue::svg::BBox{{0, 47}, {4, 50}});
bboxes.push_back(iue::svg::BBox{{0, 40}, {10, 50}});
auto [bbmin, bbmax] = iue::svg::render("render.test.svg", bboxes, circles, triangles);
assert(iue::num::isclose(bbmin[0], 0));
assert(iue::num::isclose(bbmin[1], 0));
assert(iue::num::isclose(bbmax[0], 50));
assert(iue::num::isclose(bbmax[1], 50));
return 0;
}
+151
View File
@@ -0,0 +1,151 @@
/// @file
/// @brief Implementation of SVG-Tags for the SVG-writer in render.hpp
#pragma once
#include <array> // std::array
#include <sstream> // std::stringstream
#include <string> // std::string
#include <unordered_map> // std::unordered_map
#include <vector> // std::vector
namespace iue::svg {
/// @brief convenience aliases for short notations
using Vec2d = std::array<double, 2>;
using Circle = std::tuple<Vec2d, double>;
using Polygon = std::vector<Vec2d>;
using Triangle = std::array<Vec2d, 3>;
using BBox = std::tuple<Vec2d, Vec2d>;
using Text = std::tuple<Vec2d, std::string>;
/// @brief Map for storing an open set of attribute/value pairs
using Attributes = std::unordered_map<std::string, std::string>;
/// @brief Requirements towards an XMLTag
template <typename T>
concept Tag = requires(const T& t) {
{ t.open() } -> std::same_as<std::string>;
{ t.text() } -> std::same_as<std::string>;
{ t.close() } -> std::same_as<std::string>;
};
/// @brief Implementation of an SVG XML-tag
struct XMLTag {
std::string name;
Attributes attributes;
std::string content;
bool self_closing;
std::string text() const { return this->content; }
std::string open() const {
std::stringstream res;
res << "<" << name;
for (auto const& [key, val] : attributes)
res << " " << key << "='" << val << "'";
if (self_closing)
res << "/>";
else
res << ">";
return res.str();
}
std::string close() const {
std::stringstream res;
if (!self_closing)
res << "</" << name << ">";
return res.str();
}
};
/// @brief Check if Tag requirements are fullfilled
static_assert(Tag<XMLTag>);
/// @brief Root SVG Element
/// <svg preserveAspectRatio='xMidYMid meet' viewbox='0 0 w h' height='w' width='h'></svg>
inline XMLTag svg(const Vec2d& bbmin, const Vec2d& bbmax) {
Attributes attributes;
// add data attributes
auto const& [xmin, ymin] = bbmin;
auto const& [xmax, ymax] = bbmax;
double width = std::abs(xmax - xmin);
double height = std::abs(ymax - ymin);
attributes.insert({"width", std::to_string(width)});
attributes.insert({"height", std::to_string(height)});
std::stringstream viewbox;
for (const auto& val : {xmin, ymin, width, height})
viewbox << val << " ";
attributes.insert({"viewbox", viewbox.str()});
// add default attributes
attributes.insert({"xmlns", "http://www.w3.org/2000/svg"});
attributes.insert({"version", "1.1"});
attributes.insert({"preserveAspectRatio", "xMidYMid meet"});
// final assembly
return {"svg", attributes, std::string{}, false};
}
/// @brief Group SVG Element
/// <g></g>
inline XMLTag group(const Attributes& custom_attributes = {}) {
Attributes attributes;
// add custom attributes
attributes.insert(custom_attributes.begin(), custom_attributes.end());
// final assembly
return {"g", attributes, std::string{}, false};
}
/// @brief Circle SVG Element
/// <circle r='r0' cx='cx' cy='cy' stroke-width='1' fill='none' stroke='black'/>
inline XMLTag circle(const Circle& circle, const Attributes& custom_attributes = {}) {
Attributes attributes;
// add data attributes
const auto& [center, radius] = circle;
const auto& [cx, cy] = center;
attributes.insert({"cx", std::to_string(cx)});
attributes.insert({"cy", std::to_string(cy)});
attributes.insert({"r", std::to_string(radius)});
// add custom attributes
attributes.insert(custom_attributes.begin(), custom_attributes.end());
// add default attributes
attributes.insert({"stroke-width", std::to_string(1)});
attributes.insert({"stroke", "black"});
attributes.insert({"fill", "none"});
// final assembly
return {"circle", attributes, std::string{}, true};
}
/// @brief Polygon SVG Element
/// <polygon fill='none' stroke='black' stroke-width='1' points=' x1,y1 x2,y2, x3,y3 ... '/>
inline XMLTag polygon(const Polygon& polygon, const Attributes& custom_attributes = {}) {
Attributes attributes;
// add data attributes
std::stringstream points;
for (const auto& [x, y] : polygon)
points << x << "," << y << " ";
attributes.insert({"points", points.str()});
// add custom attributes
attributes.insert(custom_attributes.begin(), custom_attributes.end());
// add default attributes
attributes.insert({"stroke-width", std::to_string(1)});
attributes.insert({"stroke", "black"});
attributes.insert({"fill", "none"});
// final assembly
return {"polygon", attributes, std::string{}, true};
}
/// @brief Text SVG Element
/// <text font-size='10' text-anchor='middle' x='x' y='y' > content </text>
inline XMLTag text(const Text& text, const Attributes& custom_attributes = {}) {
Attributes attributes;
// add data attributes
const auto& [anchor, content] = text;
const auto& [x, y] = anchor;
attributes.insert({"x", std::to_string(x)});
attributes.insert({"y", std::to_string(y)});
// add custom attributes
attributes.insert(custom_attributes.begin(), custom_attributes.end());
// add default attributes
attributes.insert({"font-size", std::to_string(10)});
attributes.insert({"text-anchor", "middle"});
return {"text", attributes, content, false};
}
} // namespace iue::svg
+62
View File
@@ -0,0 +1,62 @@
/// @file
/// @brief Test for iue::svg::XMLTag
#include "iue-svg/tags.hpp" // iue::svg::XMLTag, iue::svg::svg, iue::svg::circle, iue::svg::polygon, iue::svg::, iue::svg::text
#include <cstdlib> // EXIT_SUCCESS
#include <iostream> // std::cout, std::endl
#include <string> // std::string
struct Value {
int a = 1;
};
int main() {
{ // w/ default attributes
auto svg = iue::svg::svg({0, 0}, {100, 100});
auto group = iue::svg::group();
auto text = iue::svg::text({{50, 50}, "text"});
auto circle = iue::svg::circle({{50, 50}, 0.5});
auto polygon = iue::svg::polygon({{50, 50}, {52, 51}, {51, 52}, {78, 100}});
std::cout << svg.open() << std::endl;
std::cout << group.open() << std::endl;
std::cout << text.open() << std::endl;
std::cout << text.text() << std::endl;
std::cout << text.close() << std::endl;
std::cout << circle.open() << std::endl;
std::cout << circle.close() << std::endl;
std::cout << polygon.open() << std::endl;
std::cout << polygon.close() << std::endl;
std::cout << group.close() << std::endl;
std::cout << svg.close() << std::endl;
}
{ // w/ custom attributes
iue::svg::Attributes attributes;
attributes.insert({"stroke-width", std::to_string(10)});
attributes.insert({"stroke", "green"});
attributes.insert({"fill", "blue"});
attributes.insert({"invalid-attribute", "no check is performed"});
auto svg = iue::svg::svg({0, 0}, {100, 100});
auto group = iue::svg::group(attributes);
auto text = iue::svg::text({{50, 50}, "text"}, attributes);
auto circle = iue::svg::circle({{50, 50}, 0.5}, attributes);
auto polygon = iue::svg::polygon({{50, 50}, {52, 51}, {51, 52}, {78, 100}}, attributes);
std::cout << svg.open() << std::endl;
std::cout << group.open() << std::endl;
std::cout << text.open() << std::endl;
std::cout << text.text() << std::endl;
std::cout << text.close() << std::endl;
std::cout << circle.open() << std::endl;
std::cout << circle.close() << std::endl;
std::cout << polygon.open() << std::endl;
std::cout << polygon.close() << std::endl;
std::cout << group.close() << std::endl;
std::cout << svg.close() << std::endl;
}
return EXIT_SUCCESS;
}
+56
View File
@@ -0,0 +1,56 @@
/// @file
/// @brief Educational implementation of a tree data structure with value semantics
/// @note The implementation relies on the "minimal incomplete type support" incomplete type support for std::list
/// - https://en.cppreference.com/w/cpp/feature_test#cpp_lib_incomplete_container_elements
#pragma once
#include <cstddef> // std::size_t
#include <functional> // std::function
#include <list> // std::string
namespace iue::svg {
template <typename T>
concept Value = requires(const T& a, T& b) {
T(a); ///< copy constructor (std::copy_constructible)
b = a; ///< copy assignment (std::assignable_from)
};
/// @brief Generic tree (tree with a dynamic unbound number of child nodes)
template <Value Value> struct Tree {
/// @brief Node in the tree
struct Node {
Value value; ///< Stored value
std::list<Node> nodes; ///< List of child nodes
};
using ValueVisitor = std::function<void(const Value& value, std::size_t level)>;
using NodeVisitor = std::function<void(const Node& node, std::size_t level)>;
Node org; ///< Root node of the tree
/// @brief Constructs a tree
/// @param Value held by the root node of the tree
/// @note This constructor avoids that the value type needs to be default-constructible
Tree(const Value& value) : org{value} {}
/// @brief Visit all values in the nodes in preorder using two visitors for down and up path
static void preorder(Node& root, const ValueVisitor& down, const ValueVisitor& up, std::size_t level = 0) {
down(root.value, level);
for (auto& child : root.nodes)
preorder(child, down, up, level + 1);
up(root.value, level);
}
/// @brief Visit all nodes in preorder using two visitors for down and up path
static void preorder(Node& root, const NodeVisitor& down, const NodeVisitor& up, std::size_t level = 0) {
down(root, level);
for (auto& child : root.nodes)
preorder(child, down, up, level + 1);
up(root, level);
}
};
} // namespace iue::svg
+60
View File
@@ -0,0 +1,60 @@
/// @file
/// @brief Test for iue::svg::Tree
#include "iue-svg/tree.hpp" // iue::svg::Tree
#include <cassert> // assert
#include <cstddef> // std::size_t
#include <cstdlib> // EXIT_SUCCESS
#include <iostream> // std::cout, std::endl
#include <sstream> // std::stringstream
#include <string> // std::string
struct Value {
int a = 1;
};
int main() {
using Tree = iue::svg::Tree<Value>;
using Node = Tree::Node;
Tree tree = {Value{0}};
auto& l0 = tree.org;
l0.nodes.push_back(Node{Value{1}});
l0.nodes.push_back(Node{Value{1}});
l0.nodes.push_back(Node{Value{1}});
for (auto& l1 : l0.nodes) {
l1.nodes.push_back(Node{Value{2}});
l1.nodes.push_back(Node{Value{2}});
l1.nodes.push_back(Node{Value{2}});
for (auto& l1 : l1.nodes) {
l1.nodes.push_back(Node{Value{3}});
l1.nodes.push_back(Node{Value{3}});
}
}
auto noop = [](const Value& value, std::size_t level) -> void {};
{ // print tree
std::stringstream stream;
auto print = [&stream](const Value& value, std::size_t level) -> void {
stream << std::string(2 * level, ' ') << value.a << std::endl;
};
tree.preorder(l0, print, noop);
std::cout << stream.str() << std::endl;
}
{ // sum values in tree
int sum = 0;
auto count = [&sum](const Value& value, std::size_t level) -> void { sum += value.a; };
tree.preorder(l0, count, noop);
assert(sum == 1 * 0 + (1 * 3 * 1) + (1 * 3 * 3 * 2) + (1 * 3 * 3 * 2 * 3));
}
return EXIT_SUCCESS;
}