--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/hhtracer/CMakeLists.txt Fri Jan 10 17:37:34 2025 +0100
@@ -0,0 +1,44 @@
+cmake_minimum_required(VERSION 3.16)
+
+project(hhtracer VERSION 0.1 LANGUAGES CXX)
+
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+find_package(Qt6 REQUIRED COMPONENTS Quick Svg)
+
+qt_standard_project_setup(REQUIRES 6.5)
+
+qt_add_executable(apphhtracer
+ main.cpp
+)
+
+qt_add_qml_module(apphhtracer
+ URI hhtracer
+ VERSION 1.0
+ QML_FILES
+ Main.qml
+ SOURCES tracer.h tracer.cpp
+)
+
+# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1.
+# If you are developing for iOS or macOS you should consider setting an
+# explicit, fixed bundle identifier manually though.
+set_target_properties(apphhtracer PROPERTIES
+# MACOSX_BUNDLE_GUI_IDENTIFIER com.example.apphhtracer
+ MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
+ MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
+ MACOSX_BUNDLE TRUE
+ WIN32_EXECUTABLE TRUE
+)
+
+target_link_libraries(apphhtracer
+ PRIVATE Qt6::Quick
+ Qt6::Svg
+)
+
+include(GNUInstallDirs)
+install(TARGETS apphhtracer
+ BUNDLE DESTINATION .
+ LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+ RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/hhtracer/Main.qml Fri Jan 10 17:37:34 2025 +0100
@@ -0,0 +1,101 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Dialogs
+import QtQuick.Layouts
+
+ApplicationWindow {
+ height: 900
+ title: qsTr("Tracer")
+ visible: true
+ width: 1200
+
+ header: ToolBar {
+ RowLayout {
+ Button {
+ text: qsTr("Choose Image...")
+
+ onClicked: fileDialog.open()
+ }
+
+ Button {
+ text: qsTr("Start")
+
+ onClicked: {
+ stepTimer.start();
+ }
+ }
+
+ Button {
+ text: qsTr("Stop")
+
+ onClicked: {
+ stepTimer.stop();
+ }
+ }
+ }
+ }
+
+ FileDialog {
+ id: fileDialog
+
+ nameFilters: ["Hedgehog images (*.png)"]
+
+ onAccepted: {
+ console.log("Hello")
+ baseImage.source = selectedFile;
+ tracer.start(fileDialog.selectedFile);
+ }
+ }
+
+ Tracer {
+ id: tracer
+ }
+
+
+ Timer {
+ id: stepTimer
+
+ interval: 1500
+ repeat: true
+ running: false
+ triggeredOnStart: true
+
+ onTriggered: tracer.step()
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+
+ Image {
+ id: baseImage
+
+ Layout.fillWidth: true
+ Layout.preferredHeight: 128
+ fillMode: Image.PreserveAspectFit
+ }
+
+ GridLayout {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ columns: 10
+
+ Repeater {
+ model: tracer.solutions
+
+ Image {
+ width: 32
+ height: 32
+ source: "file://" + modelData
+ fillMode: Image.PreserveAspectFit
+
+ Rectangle {
+ border.width: 1
+ border.color: "black"
+ color: "transparent"
+ anchors.fill: parent
+ }
+ }
+ }
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/hhtracer/main.cpp Fri Jan 10 17:37:34 2025 +0100
@@ -0,0 +1,22 @@
+#include <QGuiApplication>
+#include <QQmlApplicationEngine>
+#include <QQmlContext>
+
+#include "tracer.h"
+
+int main(int argc, char *argv[]) {
+ QGuiApplication app(argc, argv);
+
+ QQmlApplicationEngine engine;
+
+ // Tracer tracer;
+ // engine.rootContext()->setContextProperty(QStringLiteral("tracer"),
+ // &tracer);
+
+ QObject::connect(
+ &engine, &QQmlApplicationEngine::objectCreationFailed, &app,
+ []() { QCoreApplication::exit(-1); }, Qt::QueuedConnection);
+ engine.loadFromModule("hhtracer", "Main");
+
+ return app.exec();
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/hhtracer/tracer.cpp Fri Jan 10 17:37:34 2025 +0100
@@ -0,0 +1,149 @@
+#include "tracer.h"
+
+#include <QRandomGenerator>
+#include <QSvgGenerator>
+
+Tracer::Tracer(QObject* parent)
+ : QObject{parent},
+ palette_{{Qt::black,
+ Qt::white,
+ {"#f29ce7"},
+ {"#9f086e"},
+ {"#54a2fa"},
+ {"#2c78d2"}}} {}
+
+QList<QColor> Tracer::palette() const { return palette_; }
+
+void Tracer::setPalette(const QList<QColor>& newPalette) {
+ if (palette_ == newPalette) return;
+ palette_ = newPalette;
+ emit paletteChanged();
+}
+
+double Tracer::bestSolution() const { return bestSolution_; }
+
+void Tracer::start(const QString& fileName) {
+ qDebug() << "Starting using" << fileName;
+
+ bestSolution_ = 0;
+ solutions_.clear();
+ generation_.clear();
+ image_ = QImage{};
+
+ if (palette_.isEmpty()) {
+ qDebug("Empty palette");
+ return;
+ }
+
+ image_.load(QUrl(fileName).toLocalFile());
+
+ if (image_.isNull()) {
+ qDebug("Failed to load image");
+ return;
+ }
+
+ for (int i = 0; i < 100; ++i) {
+ generation_.append(Solution{{32, 32}, palette_});
+ }
+}
+
+void Tracer::step() {
+ solutions_.clear();
+
+ for (auto& solution : generation_) {
+ const auto fileName = newFileName();
+ solutions_.append(fileName);
+
+ solution.render(fileName);
+ }
+
+ qDebug() << solutions_;
+
+ emit solutionsChanged();
+}
+
+QStringList Tracer::solutions() const { return solutions_; }
+
+QString Tracer::newFileName() {
+ static qlonglong counter{0};
+ counter += 1;
+ return tempDir_.filePath(
+ QStringLiteral("hedgehog_%1.svg").arg(counter, 3, 32, QChar(u'_')));
+}
+
+Solution::Solution(QSizeF size, const QList<QColor>& palette) : size{size} {
+ fitness = 0;
+ primitives = {Primitive(size, palette)};
+}
+
+void Solution::render(const QString& fileName) const {
+ const auto imageSize = size.toSize();
+
+ QSvgGenerator generator;
+ generator.setFileName(fileName);
+ generator.setSize(imageSize);
+ generator.setViewBox(QRect(0, 0, imageSize.width(), imageSize.height()));
+ generator.setTitle("Hedgehog");
+ generator.setDescription("Approximation of a target image using primitives");
+
+ QPainter painter;
+ painter.begin(&generator);
+ painter.setRenderHint(QPainter::Antialiasing, true);
+
+ for (const auto& primitive : primitives) {
+ painter.setPen(primitive.pen);
+ painter.setBrush(primitive.brush);
+ painter.resetTransform();
+ painter.translate(primitive.origin);
+ painter.rotate(primitive.rotation);
+
+ switch (primitive.type) {
+ case Polygon: {
+ QPolygonF polygon;
+ polygon.append({0, 0});
+ polygon.append(primitive.points);
+
+ painter.drawPolygon(polygon);
+ break;
+ }
+ case Circle:
+ painter.drawEllipse({0, 0}, primitive.radius1, primitive.radius2);
+ break;
+ }
+ }
+
+ painter.end();
+}
+
+double Solution::cost() const {
+ return std::accumulate(primitives.constBegin(), primitives.constEnd(), 0,
+ [](auto a, auto p) { return a + p.cost(); });
+}
+
+Primitive::Primitive(QSizeF size, const QList<QColor>& palette) {
+ auto rg = QRandomGenerator::global();
+ auto randomPoint = [&]() -> QPointF {
+ return {rg->bounded(size.width()), rg->bounded(size.height())};
+ };
+
+ if (rg->bounded(2) == 0) {
+ type = Polygon;
+
+ points.append(randomPoint());
+ points.append(randomPoint());
+ } else {
+ type = Circle;
+
+ radius1 = rg->bounded(size.width());
+ radius2 = rg->bounded(size.width());
+ rotation = rg->bounded(90);
+ }
+
+ pen = QPen(palette[rg->bounded(palette.length())]);
+ pen.setWidthF(rg->bounded(size.width() * 0.1));
+ brush = QBrush(palette[rg->bounded(palette.length())]);
+
+ origin = randomPoint();
+}
+
+double Primitive::cost() const { return 1.0 + 0.1 * points.length(); }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tools/hhtracer/tracer.h Fri Jan 10 17:37:34 2025 +0100
@@ -0,0 +1,70 @@
+#pragma once
+
+#include <QObject>
+#include <QPainter>
+#include <QQmlEngine>
+#include <QTemporaryDir>
+
+enum PrimitiveType { Polygon, Circle };
+
+struct Primitive {
+ PrimitiveType type;
+ QPen pen;
+ QBrush brush;
+ QPointF origin;
+ QList<QPointF> points; // polygon
+ double radius1{}, radius2{}, rotation{}; // ellipse
+
+ explicit Primitive(QSizeF size, const QList<QColor>& palette);
+ double cost() const;
+};
+
+struct Solution {
+ QList<Primitive> primitives;
+ double fitness;
+ QSizeF size;
+
+ explicit Solution(QSizeF size, const QList<QColor>& palette);
+ void calculateFitness(const QImage& image);
+ void render(const QString& fileName) const;
+ double cost() const;
+};
+
+class Tracer : public QObject {
+ Q_OBJECT
+ QML_ELEMENT
+
+ Q_PROPERTY(QList<QColor> palette READ palette WRITE setPalette NOTIFY
+ paletteChanged FINAL)
+ Q_PROPERTY(
+ double bestSolution READ bestSolution NOTIFY bestSolutionChanged FINAL)
+ Q_PROPERTY(QStringList solutions READ solutions NOTIFY solutionsChanged FINAL)
+
+ public:
+ explicit Tracer(QObject *parent = nullptr);
+
+ QList<QColor> palette() const;
+ void setPalette(const QList<QColor>& newPalette);
+
+ double bestSolution() const;
+
+ Q_INVOKABLE void start(const QString& fileName);
+ Q_INVOKABLE void step();
+
+ QStringList solutions() const;
+
+ Q_SIGNALS:
+ void paletteChanged();
+ void bestSolutionChanged();
+ void solutionsChanged();
+
+ private:
+ QList<QColor> palette_;
+ double bestSolution_;
+ QStringList solutions_;
+ QList<Solution> generation_;
+ QTemporaryDir tempDir_;
+ QImage image_;
+
+ QString newFileName();
+};