SVG + cmd line parsing

This commit is contained in:
tovjemam 2025-09-26 18:39:24 +02:00
parent 9b517b6aee
commit 4f2819bd81
23 changed files with 264 additions and 108 deletions

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
.cache/
.vscode/
build/
*.txt
*.svg
*.pgm

View File

@ -7,8 +7,8 @@ set(CMAKE_CXX_STANDARD 20)
add_executable(drawing
"math/transforms.cpp"
"renderers/pgm_renderer.cpp"
"renderers/svg_renderer.cpp"
"shapes/circle.cpp"
"shapes/group.cpp"
"shapes/line.cpp"
"shapes/rectangle.cpp"
"input_file.cpp"

View File

@ -9,7 +9,9 @@
#include "shapes/line.hpp"
#include "shapes/rectangle.hpp"
InputFile::InputFile(const std::filesystem::path& path) : m_file(path)
#include "math/constants.hpp"
InputFile::InputFile(const std::filesystem::path& path) : m_file(path), m_cmdsProcessed(0)
{
if (m_file.bad())
{
@ -72,11 +74,6 @@ static void RegisterCommand(CommandMap& cmdMap, const std::string& cmd, THandler
};
}
static float DegToRad(float deg)
{
return deg * 0.0174532925f;
}
shapes::Group InputFile::Parse()
{
shapes::Group shapes;
@ -108,7 +105,7 @@ shapes::Group InputFile::Parse()
RegisterCommand<int, int>(cmds, "translate", [&shapes](int ox, int oy) { shapes.Translate(math::Vector{ox, oy}); });
RegisterCommand<int, int, float>(
cmds, "rotate", [&shapes](int cx, int cy, float deg) { shapes.Rotate(math::Vector{cx, cy}, DegToRad(deg)); });
cmds, "rotate", [&shapes](int cx, int cy, float deg) { shapes.Rotate(math::Vector{cx, cy}, deg * math::DEG_TO_RAD); });
RegisterCommand<int, int, float>(cmds, "scale", [&shapes](int cx, int cy, float factor) {
if (factor < std::numeric_limits<decltype(factor)>().epsilon())
@ -121,6 +118,7 @@ shapes::Group InputFile::Parse()
while (std::getline(m_file, line))
{
// TODO: find # mid line
if (line.empty() || line[0] == '#')
continue;
@ -133,6 +131,7 @@ shapes::Group InputFile::Parse()
throw std::runtime_error("Unknown command: " + cmdName);
cmd->second(iss);
++m_cmdsProcessed;
}
return shapes;

View File

@ -14,6 +14,12 @@ class InputFile
shapes::Group Parse();
size_t GetNumProcessedCmds() const
{
return m_cmdsProcessed;
}
private:
std::ifstream m_file;
size_t m_cmdsProcessed;
};

View File

@ -1,29 +1,83 @@
#include <iostream>
#include <stdexcept>
#include <tuple>
#include "input_file.hpp"
#include "renderers/pgm_renderer.hpp"
#include "renderers/svg_renderer.hpp"
int main()
template <class T>
void Render(const shapes::Group& shapes, const std::filesystem::path& path, size_t width, size_t height)
{
T renderer(path, width, height);
shapes.Draw(renderer);
}
static size_t ParseDim(const char* start, const char* end)
{
size_t val;
auto [ptr, ec] = std::from_chars(start, end, val);
if (ec != std::errc())
{
throw std::runtime_error("Cannot parse size");
}
return val;
}
static std::tuple<size_t, size_t> ParseSize(const std::string& sizeStr)
{
auto xPos = sizeStr.find('x');
if (xPos == std::string::npos)
{
throw std::runtime_error("Size must be in format of <width>x<height>");
}
size_t width = ParseDim(sizeStr.data(), sizeStr.data() + xPos);
size_t height = ParseDim(sizeStr.data() + xPos + 1, sizeStr.data() + sizeStr.size());
return std::make_tuple(width, height);
}
static void Run(const std::string& inputFile, const std::string& outputFile, const std::string& sizeStr)
{
try
{
InputFile file("test.txt");
auto [width, height] = ParseSize(sizeStr);
std::ofstream("here.txt");
InputFile file(inputFile);
shapes::Group shapes = file.Parse();
auto shapes = file.Parse();
PgmRenderer renderer("output.pgm", 300, 300);
shapes.Draw(renderer);
renderer.Flush();
std::cout << "sjeta" << std::endl;
if (outputFile.ends_with(".svg"))
Render<SvgRenderer>(shapes, outputFile, width, height);
else
Render<PgmRenderer>(shapes, outputFile, width, height);
std::cout << "OK" << std::endl;
std::cout << file.GetNumProcessedCmds() << std::endl;
}
catch (const std::exception& e)
{
std::cerr << "Error: " << e.what() << std::endl;
}
}
int main(int argc, char** argv)
{
if (argc < 4)
{
const char* command = "drawing";
if (argc > 0)
command = argv[0];
std::cerr << "Usage: " << command << " <input_file> <output_file> <size>" << std::endl;
return 1;
}
Run(argv[1], argv[2], argv[3]);
return 0;
}

10
math/constants.hpp Normal file
View File

@ -0,0 +1,10 @@
#pragma once
namespace math
{
constexpr float PI = 3.14159265358979323846f;
constexpr float DEG_TO_RAD = PI / 180.0f;
constexpr float RAD_TO_DEG = 1.0f / DEG_TO_RAD;
} // namespace math

View File

@ -1,5 +1,6 @@
#include "transforms.hpp"
#include <cmath>
#include "constants.hpp"
math::Vector math::RotatePoint(const Vector& center, float angle, const Vector& p)
{

View File

@ -5,8 +5,6 @@
namespace math
{
constexpr float PI = 3.14159265358979323846f;
Vector RotatePoint(const Vector& center, float angle, const Vector& p);
Vector ScalePoint(const Vector& center, float factor, const Vector& p);
float RotateAngle(float originalAngle, float rotationAngle);

View File

@ -6,9 +6,32 @@
#include <span>
#include <vector>
struct Color
class Color
{
uint8_t l{};
public:
Color() : l{0}
{
}
Color(uint8_t l) : l{l}
{
}
uint8_t l{}; // luminence
void Blend(const Color& src, uint8_t alpha)
{
l = BlendChannel(l, src.l, alpha);
// l = alpha;
}
private:
static uint8_t BlendChannel(uint8_t a, uint8_t b, uint8_t alpha)
{
return static_cast<uint8_t>((static_cast<int>(a) * (255 - static_cast<int>(alpha)) +
static_cast<int>(b) * static_cast<int>(alpha)) /
255);
}
};
class Bitmap
@ -43,12 +66,21 @@ class Bitmap
{
return m_width;
}
size_t GetHeight() const
{
return m_height;
}
void Put(int x, int y, const Color& color, uint8_t alpha)
{
if (x < 0 || y < 0 || x >= m_width || y >= m_height)
return; // out of bounds
(*this)[y][x].Blend(color, alpha);
}
private:
size_t m_width, m_height;
std::vector<Color> m_data;

View File

@ -37,9 +37,15 @@ void PgmRenderer::DrawRectangle(const math::Vector& pos, const math::Vector& siz
void PgmRenderer::DrawCircle(const math::Vector& center, float radius)
{
// TODO: cast
RasterizeCircle(center.x, center.y, radius);
}
PgmRenderer::~PgmRenderer()
{
Flush();
}
void PgmRenderer::Flush()
{
m_file << "P2" << std::endl;
@ -60,47 +66,57 @@ void PgmRenderer::Flush()
void PgmRenderer::RasterizeLine(int x0, int y0, int x1, int y1)
{
int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
int err = dx + dy, e2; /* error value e_xy */
// TODO: fix
const Color lineColor{0x00};
float w = 2.0f;
for (;;)
{ /* loop */
m_bitmap[y0][x0].l = 0x00;
if (x0 == x1 && y0 == y1)
break;
e2 = 2 * err;
if (e2 >= dy)
{
err += dy;
x0 += sx;
} /* e_xy+e_x > 0 */
if (e2 <= dx)
{
err += dx;
y0 += sy;
} /* e_xy+e_y < 0 */
int dx = abs(x1 - x0);
int dy = abs(y1 - y0);
int sx = (x0 < x1) ? 1 : -1;
int sy = (y0 < y1) ? 1 : -1;
int err = dx - dy;
// perpendicular offsets for thickness
int ox = 0, oy = 0;
if (dx > dy) {
oy = 1;
} else {
ox = 1;
}
while (1) {
// draw a square of pixels for thickness
for (int i = -w/2; i <= w/2; i++) {
for (int j = -w/2; j <= w/2; j++) {
m_bitmap.Put(x0 + i*ox, y0 + j*oy, lineColor, 0xFF);
}
}
if (x0 == x1 && y0 == y1) break;
int e2 = 2 * err;
if (e2 > -dy) { err -= dy; x0 += sx; }
if (e2 < dx) { err += dx; y0 += sy; }
}
}
void PgmRenderer::RasterizeCircle(int xm, int ym, int r)
void PgmRenderer::RasterizeCircle(int cx, int cy, int r)
{
int x = -r, y = 0, err = 2 - 2 * r; /* II. Quadrant */
do
{
m_bitmap[ym + y][xm - x].l = 0x00;
m_bitmap[ym - x][xm - y].l = 0x00;
m_bitmap[ym - y][xm + x].l = 0x00;
m_bitmap[ym + x][xm + y].l = 0x00;
// TODO: fix
// setPixel(xm - x, ym + y); /* I. Quadrant */
// setPixel(xm - y, ym - x); /* II. Quadrant */
// setPixel(xm + x, ym - y); /* III. Quadrant */
// setPixel(xm + y, ym + x); /* IV. Quadrant */
r = err;
if (r <= y)
err += ++y * 2 + 1; /* e_xy+e_y < 0 */
if (r > x || err > y)
err += ++x * 2 + 1; /* e_xy+e_x > 0 or no 2nd y-step */
} while (x < 0);
const Color circleColor{0x00};
float w = 2.0f;
int r_outer = r + w / 2;
int r_inner = r - w / 2;
if (r_inner < 0) r_inner = 0;
for (int y = -r_outer; y <= r_outer; y++) {
for (int x = -r_outer; x <= r_outer; x++) {
int dist2 = x*x + y*y;
if (dist2 <= r_outer*r_outer && dist2 >= r_inner*r_inner) {
m_bitmap.Put(cx + x, cy + y, circleColor, 0xFF);
}
}
}
}

View File

@ -16,9 +16,7 @@ class PgmRenderer : public Renderer
void DrawRectangle(const math::Vector& pos, const math::Vector& size, float angle) override;
void DrawCircle(const math::Vector& center, float radius) override;
void Flush() override;
~PgmRenderer() override = default;
~PgmRenderer() override;
private:
std::ofstream m_file;
@ -26,4 +24,6 @@ class PgmRenderer : public Renderer
void RasterizeLine(int x0, int y0, int x1, int y1);
void RasterizeCircle(int xm, int ym, int r);
void Flush();
};

View File

@ -8,7 +8,9 @@ class Renderer
virtual void DrawRectangle(const math::Vector& pos, const math::Vector& size, float angle) = 0;
virtual void DrawCircle(const math::Vector& center, float radius) = 0;
virtual void Flush() = 0;
virtual void Flush()
{
}
virtual ~Renderer() = default;
};

View File

@ -0,0 +1,38 @@
#include "svg_renderer.hpp"
#include "math/constants.hpp"
SvgRenderer::SvgRenderer(const std::filesystem::path& path, size_t width, size_t height) : m_file(path)
{
if (m_file.bad())
throw std::runtime_error("Cannot open " + path.string() + " for writing");
m_file << "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" << width << "\" height=\"" << height
<< "\" viewBox=\"0 0 " << width << " " << height << "\" role=\"img\" aria-label=\"KIV/CPP\">" << std::endl;
}
void SvgRenderer::DrawLine(const math::Vector& p0, const math::Vector& p1)
{
m_file << " <line x1=\"" << p0.x << "\" y1=\"" << p0.y << "\" x2=\"" << p1.x << "\" y2=\"" << p1.y << "\" "
<< "stroke=\"#000000\" stroke-width=\"2\" stroke-linecap=\"round\" />" << std::endl;
}
void SvgRenderer::DrawRectangle(const math::Vector& pos, const math::Vector& size, float angle)
{
float angleDeg = angle * math::RAD_TO_DEG;
m_file << " <rect x=\"" << pos.x << "\" y=\"" << pos.y << "\" width=\"" << size.x << "\" height=\"" << size.y
<< "\" fill=\"none\" stroke=\"#000000\" stroke-width=\"2\" stroke-linejoin=\"round\" transform=\"rotate("
<< angleDeg << ", " << pos.x << ", " << pos.y << ")\" />" << std::endl;
}
void SvgRenderer::DrawCircle(const math::Vector& center, float radius)
{
m_file << " <circle cx=\"" << center.x << "\" cy=\"" << center.y << "\" r=\"" << radius
<< "\" fill=\"none\" stroke=\"#000000\" stroke-width=\"2\" />" << std::endl;
}
SvgRenderer::~SvgRenderer()
{
m_file << "</svg>" << std::endl;
}

View File

@ -0,0 +1,21 @@
#pragma once
#include "renderer.hpp"
#include <filesystem>
#include <fstream>
class SvgRenderer : public Renderer
{
public:
SvgRenderer(const std::filesystem::path& path, size_t width, size_t height);
void DrawLine(const math::Vector& p0, const math::Vector& p1) override;
void DrawRectangle(const math::Vector& pos, const math::Vector& size, float angle) override;
void DrawCircle(const math::Vector& center, float radius) override;
~SvgRenderer() override;
private:
std::ofstream m_file;
};

View File

@ -18,7 +18,7 @@ void shapes::Circle::Scale(const math::Vector& center, float factor)
m_radius *= factor;
}
void shapes::Circle::Draw(Renderer& renderer)
void shapes::Circle::Draw(Renderer& renderer) const
{
renderer.DrawCircle(m_center, m_radius);
}

View File

@ -26,7 +26,7 @@ class Circle : public Shape
void Rotate(const math::Vector& center, float angle) override;
void Scale(const math::Vector& center, float factor) override;
void Draw(Renderer& renderer) override;
void Draw(Renderer& renderer) const override;
~Circle() override = default;

View File

@ -1,33 +0,0 @@
#include "group.hpp"
// void shapes::Group::Translate(const math::Vector& offset)
// {
// for (const auto& shape : m_shapes)
// {
// shape->Translate(offset);
// }
// }
// void shapes::Group::Rotate(const math::Vector& center, float angle)
// {
// for (const auto& shape : m_shapes)
// {
// shape->Rotate(center, angle);
// }
// }
// void shapes::Group::Scale(const math::Vector& center, float factor)
// {
// for (const auto& shape : m_shapes)
// {
// shape->Scale(center, factor);
// }
// }
// void shapes::Group::Draw(Renderer& renderer)
// {
// for (const auto& shape : m_shapes)
// {
// shape->Draw(renderer);
// }
// }

View File

@ -13,12 +13,13 @@ class Group : public Shape
public:
Group() = default;
Group(Group&& other)
Group(Group&& other) : m_shapes{std::move(other.m_shapes)}
{
}
Group& operator=(Group&& other)
{
// TODO: overit
m_shapes = std::move(other.m_shapes);
return *this;
}
@ -43,7 +44,7 @@ class Group : public Shape
ShapesCall<&Shape::Scale>(center, factor);
}
void Draw(Renderer& renderer) override
void Draw(Renderer& renderer) const override
{
ShapesCall<&Shape::Draw>(renderer);
}
@ -60,6 +61,14 @@ class Group : public Shape
(shape.get()->*TFun)(std::forward<TArgs>(args)...);
}
}
template <auto TFun, class... TArgs> void ShapesCall(TArgs&&... args) const
{
for (const auto& shape : m_shapes)
{
(shape.get()->*TFun)(std::forward<TArgs>(args)...);
}
}
};
} // namespace shapes

View File

@ -20,7 +20,7 @@ void shapes::Line::Scale(const math::Vector& center, float factor)
m_p1 = math::ScalePoint(center, factor, m_p1);
}
void shapes::Line::Draw(Renderer& renderer)
void shapes::Line::Draw(Renderer& renderer) const
{
renderer.DrawLine(m_p0, m_p1);
}

View File

@ -17,7 +17,7 @@ class Line : public Shape
void Rotate(const math::Vector& center, float angle) override;
void Scale(const math::Vector& center, float factor) override;
void Draw(Renderer& renderer) override;
void Draw(Renderer& renderer) const override;
private:
math::Vector m_p0;

View File

@ -19,7 +19,7 @@ void shapes::Rectangle::Scale(const math::Vector& center, float factor)
m_size *= factor;
}
void shapes::Rectangle::Draw(Renderer& renderer)
void shapes::Rectangle::Draw(Renderer& renderer) const
{
renderer.DrawRectangle(m_pos, m_size, m_angle);
}

View File

@ -32,7 +32,7 @@ class Rectangle : public Shape
void Rotate(const math::Vector& center, float angle) override;
void Scale(const math::Vector& center, float factor) override;
void Draw(Renderer& renderer) override;
void Draw(Renderer& renderer) const override;
~Rectangle() override = default;

View File

@ -16,7 +16,7 @@ class Shape
virtual void Rotate(const math::Vector& center, float angle) = 0;
virtual void Scale(const math::Vector& center, float factor) = 0;
virtual void Draw(Renderer& renderer) = 0;
virtual void Draw(Renderer& renderer) const = 0;
virtual ~Shape() = default;
};