init
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
cmake_minimum_required(VERSION 3.23)
|
||||
project(cppbridge LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Qml Quick)
|
||||
find_package(OpenCV 4 REQUIRED COMPONENTS core imgcodecs imgproc)
|
||||
qt_standard_project_setup(REQUIRES 6.5)
|
||||
|
||||
qt_add_qml_module(cppbridgeplugin
|
||||
URI cppbridge
|
||||
VERSION 1.0
|
||||
OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../package/contents/ui/cppbridge"
|
||||
SOURCES
|
||||
src/clockimageprovider.hpp
|
||||
src/clockimageprovider.cpp
|
||||
src/config.hpp
|
||||
src/renderer.hpp
|
||||
src/renderer.cpp
|
||||
src/timebridge.h
|
||||
src/timebridge.cpp
|
||||
)
|
||||
|
||||
target_include_directories(cppbridgeplugin PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src"
|
||||
${OpenCV_INCLUDE_DIRS}
|
||||
)
|
||||
target_link_libraries(cppbridgeplugin PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Qml
|
||||
Qt6::Quick
|
||||
opencv_core
|
||||
opencv_imgcodecs
|
||||
opencv_imgproc
|
||||
)
|
||||
set_target_properties(cppbridgeplugin PROPERTIES
|
||||
CXX_STANDARD 20
|
||||
CXX_STANDARD_REQUIRED ON
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"version": 6,
|
||||
"cmakeMinimumRequired": {
|
||||
"major": 3,
|
||||
"minor": 23,
|
||||
"patch": 0
|
||||
},
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "clang-default",
|
||||
"displayName": "Clang Default",
|
||||
"description": "Configure build/cpp with clang/clang++ and export compile commands.",
|
||||
"generator": "Unix Makefiles",
|
||||
"binaryDir": "${sourceDir}/../build/cpp",
|
||||
"cacheVariables": {
|
||||
"CMAKE_C_COMPILER": "clang",
|
||||
"CMAKE_CXX_COMPILER": "clang++",
|
||||
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
|
||||
}
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{
|
||||
"name": "clang-default",
|
||||
"configurePreset": "clang-default"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "clockimageprovider.hpp"
|
||||
|
||||
#include <QMutexLocker>
|
||||
|
||||
ClockImageProvider::ClockImageProvider()
|
||||
: QQuickImageProvider(QQuickImageProvider::Image) {}
|
||||
|
||||
QImage ClockImageProvider::requestImage(const QString&, QSize* size,
|
||||
const QSize& requestedSize) {
|
||||
QImage frame;
|
||||
{
|
||||
QMutexLocker locker(&mutex_);
|
||||
frame = image_;
|
||||
}
|
||||
|
||||
if (size != nullptr) {
|
||||
*size = frame.size();
|
||||
}
|
||||
|
||||
if (!frame.isNull() && requestedSize.width() > 0 &&
|
||||
requestedSize.height() > 0) {
|
||||
return frame.scaled(requestedSize, Qt::KeepAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
void ClockImageProvider::setImage(const QImage& image) {
|
||||
QMutexLocker locker(&mutex_);
|
||||
image_ = image;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <QImage>
|
||||
#include <QMutex>
|
||||
#include <QQuickImageProvider>
|
||||
|
||||
class ClockImageProvider : public QQuickImageProvider {
|
||||
public:
|
||||
ClockImageProvider();
|
||||
|
||||
QImage requestImage(const QString& id, QSize* size,
|
||||
const QSize& requestedSize) override;
|
||||
|
||||
void setImage(const QImage& image);
|
||||
|
||||
private:
|
||||
QMutex mutex_;
|
||||
QImage image_;
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
// file full of magic numbers
|
||||
// most are hand tuned based on the cat image I have
|
||||
|
||||
namespace Config {
|
||||
const cv::Point2f CLOCK_CENTER(300.0f, 286.0f);
|
||||
|
||||
const double HAND_SCALE = 0.22;
|
||||
const double HOUR_HAND_SCALE = HAND_SCALE * 0.7;
|
||||
const double MINUTE_HAND_SCALE = HAND_SCALE * 0.7;
|
||||
|
||||
const cv::Point2f HOUR_HAND_PIVOT_FRACTION(0.126f, 0.502f);
|
||||
const cv::Point2f MINUTE_HAND_PIVOT_FRACTION(0.101f, 0.488f);
|
||||
// Asset convention: unrotated hand points right (+X).
|
||||
// Clock convention: 12:00 points up (-Y), hence +90 degrees.
|
||||
const double HAND_BASE_ANGLE_OFFSET = 90.0;
|
||||
|
||||
} // namespace Config
|
||||
@@ -0,0 +1,112 @@
|
||||
#include "renderer.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <opencv2/imgcodecs.hpp>
|
||||
#include <opencv2/imgproc.hpp>
|
||||
|
||||
#include "config.hpp"
|
||||
|
||||
Renderer::Renderer() {}
|
||||
|
||||
bool Renderer::load_assets(const std::string& bg_path,
|
||||
const std::string& hour_hand_path,
|
||||
const std::string& minute_hand_path) {
|
||||
background_ = cv::imread(bg_path, cv::IMREAD_UNCHANGED);
|
||||
if (background_.empty()) {
|
||||
std::cerr << "failed to load background: " << bg_path << std::endl;
|
||||
return false;
|
||||
}
|
||||
if (background_.channels() != 4) {
|
||||
std::cerr << "expected rgba png for background";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load hands with alpha channel.
|
||||
hour_hand_ = cv::imread(hour_hand_path, cv::IMREAD_UNCHANGED);
|
||||
if (hour_hand_.empty()) {
|
||||
std::cerr << "failed to load hour hand: " << hour_hand_path
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
minute_hand_ = cv::imread(minute_hand_path, cv::IMREAD_UNCHANGED);
|
||||
if (minute_hand_.empty()) {
|
||||
std::cerr << "failed to load minute hand: " << minute_hand_path
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hour_hand_.channels() != 4 || minute_hand_.channels() != 4) {
|
||||
std::cerr << "expected rgba png for hands" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
cv::Mat Renderer::render(double hour_angle, double minute_angle) {
|
||||
auto clock_to_cv_rotation = [](double clock_angle) {
|
||||
return Config::HAND_BASE_ANGLE_OFFSET - clock_angle;
|
||||
};
|
||||
|
||||
cv::Mat warped_hour = transform_hand(
|
||||
hour_hand_, clock_to_cv_rotation(hour_angle), Config::HOUR_HAND_SCALE,
|
||||
Config::HOUR_HAND_PIVOT_FRACTION);
|
||||
cv::Mat warped_minute = transform_hand(
|
||||
minute_hand_, clock_to_cv_rotation(minute_angle),
|
||||
Config::MINUTE_HAND_SCALE, Config::MINUTE_HAND_PIVOT_FRACTION);
|
||||
|
||||
cv::Mat keyframe = background_.clone();
|
||||
|
||||
// Layer order matters: hour underneath minute.
|
||||
blend_overlay(keyframe, warped_hour);
|
||||
blend_overlay(keyframe, warped_minute);
|
||||
|
||||
return keyframe;
|
||||
}
|
||||
|
||||
cv::Mat Renderer::transform_hand(const cv::Mat& hand_sprite, double angle_deg,
|
||||
double scale,
|
||||
const cv::Point2f& pivot_fraction) {
|
||||
cv::Mat oriented_sprite;
|
||||
cv::flip(hand_sprite, oriented_sprite, 0);
|
||||
|
||||
if (scale <= 0.0) {
|
||||
return cv::Mat::zeros(background_.size(), CV_8UC4);
|
||||
}
|
||||
|
||||
cv::Point2f pivot(oriented_sprite.cols * pivot_fraction.x,
|
||||
oriented_sprite.rows * pivot_fraction.y);
|
||||
|
||||
cv::Mat rot_mat = cv::getRotationMatrix2D(pivot, angle_deg, scale);
|
||||
rot_mat.at<double>(0, 2) += Config::CLOCK_CENTER.x - pivot.x;
|
||||
rot_mat.at<double>(1, 2) += Config::CLOCK_CENTER.y - pivot.y;
|
||||
|
||||
cv::Mat overlay = cv::Mat::zeros(background_.size(), CV_8UC4);
|
||||
cv::warpAffine(oriented_sprite, overlay, rot_mat, background_.size(),
|
||||
cv::INTER_CUBIC, cv::BORDER_CONSTANT,
|
||||
cv::Scalar(0, 0, 0, 0));
|
||||
|
||||
return overlay;
|
||||
}
|
||||
|
||||
void Renderer::blend_overlay(cv::Mat& bg, const cv::Mat& fg) {
|
||||
// Blend hand RGB onto background RGB using hand alpha
|
||||
for (int y = 0; y < fg.rows; ++y) {
|
||||
const cv::Vec4b* fg_ptr = fg.ptr<cv::Vec4b>(y);
|
||||
cv::Vec4b* bg_ptr = bg.ptr<cv::Vec4b>(y);
|
||||
|
||||
for (int x = 0; x < fg.cols; ++x) {
|
||||
const cv::Vec4b& fg_pixel = fg_ptr[x];
|
||||
const double alpha = fg_pixel[3] / 255.0;
|
||||
|
||||
if (alpha > 0.0) {
|
||||
const cv::Vec4b bg_pixel = bg_ptr[x];
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
bg_ptr[x][c] = static_cast<uchar>(
|
||||
fg_pixel[c] * alpha + bg_pixel[c] * (1.0 - alpha));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <opencv2/core.hpp>
|
||||
#include <string>
|
||||
|
||||
class Renderer {
|
||||
public:
|
||||
Renderer();
|
||||
|
||||
// Load assets from disk. Returns false if any fail to load.
|
||||
bool load_assets(const std::string& bg_path,
|
||||
const std::string& hour_hand_path,
|
||||
const std::string& minute_hand_path);
|
||||
|
||||
// Composites the hands onto the background at the given angles (degrees).
|
||||
// Returns the result image.
|
||||
cv::Mat render(double hour_angle, double minute_angle);
|
||||
|
||||
private:
|
||||
cv::Mat background_;
|
||||
cv::Mat hour_hand_;
|
||||
cv::Mat minute_hand_;
|
||||
|
||||
// Helpers
|
||||
cv::Mat transform_hand(const cv::Mat& hand_sprite, double angle_deg,
|
||||
double scale, const cv::Point2f& pivot_fraction);
|
||||
void blend_overlay(cv::Mat& bg, const cv::Mat& fg);
|
||||
};
|
||||
@@ -0,0 +1,216 @@
|
||||
#include "timebridge.h"
|
||||
|
||||
#include <QQmlEngine>
|
||||
#include <QTime>
|
||||
#include <QUuid>
|
||||
#include <opencv2/imgproc.hpp>
|
||||
|
||||
#include "clockimageprovider.hpp"
|
||||
#include "renderer.hpp"
|
||||
|
||||
namespace {
|
||||
constexpr int kMillisPerMinute = 60 * 1000;
|
||||
}
|
||||
|
||||
ClockBridge::ClockBridge(QObject* parent) : QObject(parent) {
|
||||
updateTimer_.setSingleShot(true);
|
||||
connect(&updateTimer_, &QTimer::timeout, this,
|
||||
&ClockBridge::onUpdateTimerTimeout);
|
||||
}
|
||||
|
||||
ClockBridge::~ClockBridge() {
|
||||
updateTimer_.stop();
|
||||
if (renderer_ != nullptr) {
|
||||
delete renderer_;
|
||||
renderer_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
QString ClockBridge::imageSource() const { return imageSource_; }
|
||||
|
||||
bool ClockBridge::ready() const { return ready_; }
|
||||
|
||||
QString ClockBridge::errorString() const { return errorString_; }
|
||||
|
||||
bool ClockBridge::initialize(const QUrl& bg, const QUrl& hour,
|
||||
const QUrl& minute) {
|
||||
updateTimer_.stop();
|
||||
initialized_ = false;
|
||||
setReady(false);
|
||||
|
||||
const QString bgPath = toLocalPath(bg);
|
||||
const QString hourPath = toLocalPath(hour);
|
||||
const QString minutePath = toLocalPath(minute);
|
||||
if (bgPath.isEmpty() || hourPath.isEmpty() || minutePath.isEmpty()) {
|
||||
setErrorString(QStringLiteral("Clock assets must be local file URLs."));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ensureImageProvider()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (renderer_ == nullptr) {
|
||||
renderer_ = new Renderer();
|
||||
}
|
||||
|
||||
if (!renderer_->load_assets(bgPath.toStdString(), hourPath.toStdString(),
|
||||
minutePath.toStdString())) {
|
||||
setErrorString(QStringLiteral("Failed to load clock assets."));
|
||||
return false;
|
||||
}
|
||||
|
||||
initialized_ = true;
|
||||
if (!renderForCurrentTime()) {
|
||||
initialized_ = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
scheduleNextMinuteTick();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClockBridge::refreshNow() {
|
||||
if (!initialized_) {
|
||||
setErrorString(QStringLiteral("ClockBridge is not initialized."));
|
||||
setReady(false);
|
||||
return;
|
||||
}
|
||||
|
||||
renderForCurrentTime();
|
||||
scheduleNextMinuteTick();
|
||||
}
|
||||
|
||||
void ClockBridge::onUpdateTimerTimeout() {
|
||||
if (!initialized_) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderForCurrentTime();
|
||||
scheduleNextMinuteTick();
|
||||
}
|
||||
|
||||
bool ClockBridge::ensureImageProvider() {
|
||||
if (imageProvider_ != nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
QQmlEngine* engine = qmlEngine(this);
|
||||
if (engine == nullptr) {
|
||||
setErrorString(QStringLiteral("ClockBridge has no active QQmlEngine."));
|
||||
return false;
|
||||
}
|
||||
|
||||
providerId_ = QStringLiteral("catclock_%1")
|
||||
.arg(QUuid::createUuid().toString(QUuid::Id128));
|
||||
if (engine->imageProvider(providerId_) != nullptr) {
|
||||
setErrorString(QStringLiteral("Image provider ID collision."));
|
||||
return false;
|
||||
}
|
||||
|
||||
imageProvider_ = new ClockImageProvider();
|
||||
engine->addImageProvider(providerId_, imageProvider_);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClockBridge::renderForCurrentTime() {
|
||||
if (renderer_ == nullptr || imageProvider_ == nullptr) {
|
||||
setErrorString(QStringLiteral("ClockBridge is not initialized."));
|
||||
setReady(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const QTime now = QTime::currentTime();
|
||||
const int minute = now.minute();
|
||||
const int hour = now.hour();
|
||||
|
||||
const double minuteAngle = minute * 6.0;
|
||||
const double hourAngle = (hour % 12) * 30.0 + minute * 0.5;
|
||||
|
||||
cv::Mat frame = renderer_->render(hourAngle, minuteAngle);
|
||||
if (frame.empty()) {
|
||||
setErrorString(QStringLiteral("Renderer returned an empty frame."));
|
||||
setReady(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
cv::Mat rgbaFrame;
|
||||
cv::cvtColor(frame, rgbaFrame, cv::COLOR_BGRA2RGBA);
|
||||
QImage image(rgbaFrame.data, rgbaFrame.cols, rgbaFrame.rows,
|
||||
static_cast<int>(rgbaFrame.step), QImage::Format_RGBA8888);
|
||||
imageProvider_->setImage(image.copy());
|
||||
|
||||
setErrorString(QString());
|
||||
setReady(true);
|
||||
bumpFrameRevision();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClockBridge::scheduleNextMinuteTick() {
|
||||
if (!initialized_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QTime now = QTime::currentTime();
|
||||
int delayMs = kMillisPerMinute - (now.second() * 1000 + now.msec());
|
||||
if (delayMs <= 0 || delayMs > kMillisPerMinute) {
|
||||
delayMs = kMillisPerMinute;
|
||||
}
|
||||
|
||||
updateTimer_.start(delayMs);
|
||||
}
|
||||
|
||||
void ClockBridge::setReady(bool value) {
|
||||
if (ready_ == value) {
|
||||
return;
|
||||
}
|
||||
ready_ = value;
|
||||
emit readyChanged();
|
||||
}
|
||||
|
||||
void ClockBridge::setErrorString(const QString& value) {
|
||||
if (errorString_ == value) {
|
||||
return;
|
||||
}
|
||||
errorString_ = value;
|
||||
emit errorStringChanged();
|
||||
}
|
||||
|
||||
QString ClockBridge::toLocalPath(const QUrl& url) const {
|
||||
if (url.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
if (url.isLocalFile()) {
|
||||
return url.toLocalFile();
|
||||
}
|
||||
|
||||
if (url.scheme().isEmpty()) {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString ClockBridge::buildImageSource() const {
|
||||
if (providerId_.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
return QStringLiteral("image://%1/frame?rev=%2")
|
||||
.arg(providerId_)
|
||||
.arg(frameRevision_);
|
||||
}
|
||||
|
||||
// to signal image inddeed changed even if the path is same
|
||||
// we update revision
|
||||
void ClockBridge::bumpFrameRevision() {
|
||||
++frameRevision_;
|
||||
const QString nextSource = buildImageSource();
|
||||
if (imageSource_ == nextSource) {
|
||||
return;
|
||||
}
|
||||
|
||||
imageSource_ = nextSource;
|
||||
emit imageSourceChanged();
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
class ClockImageProvider;
|
||||
class Renderer;
|
||||
|
||||
class ClockBridge : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
Q_PROPERTY(QString imageSource READ imageSource NOTIFY imageSourceChanged)
|
||||
Q_PROPERTY(bool ready READ ready NOTIFY readyChanged)
|
||||
Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged)
|
||||
|
||||
public:
|
||||
explicit ClockBridge(QObject* parent = nullptr);
|
||||
~ClockBridge() override;
|
||||
|
||||
QString imageSource() const;
|
||||
bool ready() const;
|
||||
QString errorString() const;
|
||||
|
||||
Q_INVOKABLE bool initialize(const QUrl& bg, const QUrl& hour,
|
||||
const QUrl& minute);
|
||||
Q_INVOKABLE void refreshNow();
|
||||
|
||||
signals:
|
||||
void imageSourceChanged();
|
||||
void readyChanged();
|
||||
void errorStringChanged();
|
||||
|
||||
private slots:
|
||||
void onUpdateTimerTimeout();
|
||||
|
||||
private:
|
||||
bool ensureImageProvider();
|
||||
bool renderForCurrentTime();
|
||||
void scheduleNextMinuteTick();
|
||||
void setReady(bool value);
|
||||
void setErrorString(const QString& value);
|
||||
QString toLocalPath(const QUrl& url) const;
|
||||
QString buildImageSource() const;
|
||||
void bumpFrameRevision();
|
||||
|
||||
Renderer* renderer_ = nullptr;
|
||||
ClockImageProvider* imageProvider_ = nullptr;
|
||||
QString providerId_;
|
||||
QString imageSource_;
|
||||
QString errorString_;
|
||||
bool ready_ = false;
|
||||
bool initialized_ = false;
|
||||
int frameRevision_ = 0;
|
||||
QTimer updateTimer_;
|
||||
};
|
||||
Reference in New Issue
Block a user