导出数据转spine

This commit is contained in:
tianmo
2026-09-07 21:29:15 +08:00
parent d258d4f435
commit 3a7c0b4092
65 changed files with 6000 additions and 0 deletions
+437
View File
@@ -0,0 +1,437 @@
#include "ui/MainWindow.hpp"
#include "atlas/AtlasParser.hpp"
#include "conversion/ConversionJob.hpp"
#include "editor/SpineEditorLocator.hpp"
#include "editor/SpineVersionAvailability.hpp"
#include "scan/SpineDataVersionDetector.hpp"
#include "ui/SpinePreviewWidget.hpp"
#include <QComboBox>
#include <QCheckBox>
#include <QCoreApplication>
#include <QDir>
#include <QDirIterator>
#include <QDragEnterEvent>
#include <QDropEvent>
#include <QFileDialog>
#include <QFileInfo>
#include <QHBoxLayout>
#include <QLabel>
#include <QMimeData>
#include <QMessageBox>
#include <QPushButton>
#include <QSettings>
#include <QTimer>
#include <QUrl>
#include <QVBoxLayout>
#include <filesystem>
namespace dts {
namespace {
std::filesystem::path localPath(const QString& value) {
#if defined(_WIN32)
return std::filesystem::path(value.toStdWString());
#else
return std::filesystem::path(value.toStdString());
#endif
}
QString platformLibraryName(const QString& line) {
#if defined(_WIN32)
return QStringLiteral("dts-runtime-%1.dll").arg(line);
#elif defined(__APPLE__)
return QStringLiteral("libdts-runtime-%1.dylib").arg(line);
#else
return QStringLiteral("libdts-runtime-%1.so").arg(line);
#endif
}
} // namespace
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
setupUi();
setAcceptDrops(true);
setWindowTitle(tr("DateToSpine"));
resize(960, 680);
}
void MainWindow::openResourcePath(const QString& path) {
addInput(path);
}
void MainWindow::setupUi() {
auto* root = new QWidget;
auto* layout = new QVBoxLayout(root);
layout->setContentsMargins(28, 24, 28, 24);
layout->setSpacing(16);
auto* top = new QHBoxLayout;
auto* title = new QLabel(tr("DateToSpine"));
title->setObjectName(QStringLiteral("title"));
availabilityLabel_ = new QLabel;
availabilityLabel_->setAlignment(Qt::AlignCenter);
availabilityLabel_->setMinimumWidth(190);
availabilityLabel_->setVisible(false);
resourceCombo_ = new QComboBox;
resourceCombo_->setMinimumWidth(260);
resourceCombo_->setVisible(false);
auto* addButton = new QPushButton(tr("选择资源"));
addButton->setObjectName(QStringLiteral("quietButton"));
top->addWidget(title);
top->addStretch();
top->addWidget(availabilityLabel_);
top->addStretch();
top->addWidget(resourceCombo_);
top->addWidget(addButton);
layout->addLayout(top);
previewWidget_ = new SpinePreviewWidget;
previewWidget_->setObjectName(QStringLiteral("preview"));
layout->addWidget(previewWidget_, 1);
auto* controls = new QHBoxLayout;
animationCombo_ = new QComboBox;
animationCombo_->setMinimumWidth(220);
animationCombo_->setEnabled(false);
statusLabel_ = new QLabel;
statusLabel_->setObjectName(QStringLiteral("status"));
unpremultiplyCheck_ = new QCheckBox(tr("去除预乘 Alpha"));
unpremultiplyCheck_->setToolTip(tr("由 Spine 官方纹理解包器转换为普通透明图片"));
convertButton_ = new QPushButton(tr("一键转换为 .spine"));
convertButton_->setObjectName(QStringLiteral("primaryButton"));
convertButton_->setEnabled(false);
controls->addWidget(new QLabel(tr("动画")));
controls->addWidget(animationCombo_);
controls->addWidget(statusLabel_, 1);
controls->addWidget(unpremultiplyCheck_);
controls->addWidget(convertButton_);
layout->addLayout(controls);
setCentralWidget(root);
setStyleSheet(QStringLiteral(R"(
QMainWindow, QWidget { background: #17191e; color: #f3f4f6; font-size: 14px; }
QLabel#title { font-size: 22px; font-weight: 650; }
QLabel#preview { background: #101217; border: 2px dashed #343944; border-radius: 14px; color: #a7adb8; }
QLabel#status { color: #9299a5; padding-left: 8px; }
QComboBox { background: #242832; border: 1px solid #3a404c; border-radius: 8px; padding: 8px 12px; }
QPushButton { border-radius: 8px; padding: 9px 16px; }
QPushButton#quietButton { background: #292d36; border: 1px solid #3d4350; }
QPushButton#primaryButton { background: #6e5eea; border: none; font-weight: 650; padding: 11px 20px; }
QPushButton#primaryButton:disabled { background: #343743; color: #777d89; }
QCheckBox { spacing: 8px; color: #c9ced8; }
QCheckBox::indicator { width: 18px; height: 18px; }
)"));
showEmptyState();
connect(addButton, &QPushButton::clicked, this, &MainWindow::chooseResources);
connect(resourceCombo_, &QComboBox::currentIndexChanged,
this, &MainWindow::resourceChanged);
connect(animationCombo_, &QComboBox::currentTextChanged,
this, &MainWindow::animationChanged);
connect(convertButton_, &QPushButton::clicked, this, &MainWindow::convertCurrent);
previewTimer_ = new QTimer(this);
previewTimer_->setInterval(16);
connect(previewTimer_, &QTimer::timeout, this, &MainWindow::advancePreview);
}
void MainWindow::showEmptyState() {
previewWidget_->resetViewport();
previewWidget_->setMessage(
tr("把 .json / .skel、.atlas 和图片拖到这里\n\n也可以拖入整个资源文件夹"));
statusLabel_->clear();
}
void MainWindow::chooseResources() {
const auto paths = QFileDialog::getOpenFileNames(this, tr("选择导出资源"), {},
tr("Spine 资源 (*.json *.skel *.atlas *.png *.jpg *.jpeg *.webp);;所有文件 (*)"));
for (const auto& path : paths) addInput(path);
}
void MainWindow::addInput(const QString& path) {
const QFileInfo info(path);
const auto scanDirectory = [this](const QString& directory, bool recursive) {
QDirIterator iterator(directory, {"*.json", "*.skel"}, QDir::Files,
recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags);
while (iterator.hasNext()) addSkeleton(iterator.next());
};
if (info.isDir()) scanDirectory(path, true);
else if (info.suffix().compare("json", Qt::CaseInsensitive) == 0
|| info.suffix().compare("skel", Qt::CaseInsensitive) == 0) addSkeleton(path);
else scanDirectory(info.absolutePath(), false);
scheduleAvailabilityCheck();
}
void MainWindow::addSkeleton(const QString& path) {
const auto result = SpineDataVersionDetector{}.inspect(localPath(path));
if (result.status != VersionDetectionStatus::Detected || skeletonPaths_.contains(path)) return;
skeletonPaths_.append(path);
resourceCombo_->addItem(QFileInfo(path).completeBaseName(), path);
resourceCombo_->setVisible(true);
resourceCombo_->setCurrentIndex(resourceCombo_->count() - 1);
}
QString MainWindow::findAtlas(const QString& skeletonPath) const {
const QFileInfo skeleton(skeletonPath);
QDir directory(skeleton.absolutePath());
const QString base = skeleton.completeBaseName();
for (const auto& name : {base + ".atlas", base + ".atlas.txt"}) {
const auto candidate = directory.filePath(name);
if (QFileInfo::exists(candidate)) return candidate;
}
const auto atlases = directory.entryInfoList({"*.atlas", "*.atlas.txt"}, QDir::Files);
if (atlases.size() == 1) return atlases.front().absoluteFilePath();
for (const auto& atlas : atlases) {
if (base.startsWith(atlas.completeBaseName())
|| atlas.completeBaseName().startsWith(base)) return atlas.absoluteFilePath();
}
return {};
}
QString MainWindow::runtimeLibrary(const QString& versionLine) const {
const QDir executableDirectory(QCoreApplication::applicationDirPath());
#if defined(__APPLE__)
return QDir::cleanPath(executableDirectory.filePath(
"../PlugIns/runtimes/" + platformLibraryName(versionLine)));
#else
return executableDirectory.filePath("runtimes/" + platformLibraryName(versionLine));
#endif
}
void MainWindow::resourceChanged(int index) {
if (index >= 0) {
loadCurrentPreview();
scheduleAvailabilityCheck();
}
}
void MainWindow::loadCurrentPreview() {
previewTimer_->stop();
previewWidget_->resetViewport();
animationCombo_->clear();
animationCombo_->setEnabled(false);
convertButton_->setEnabled(false);
const QString skeletonPath = resourceCombo_->currentData().toString();
const QString atlasPath = findAtlas(skeletonPath);
const auto detection = SpineDataVersionDetector{}.inspect(localPath(skeletonPath));
if (!detection.version || atlasPath.isEmpty()) {
previewWidget_->setMessage(tr("没有找到与骨骼数据配套的 .atlas 文件"));
statusLabel_->setText(tr("请把完整导出资源一起拖入"));
return;
}
currentVersion_ = QString::fromStdString(detection.version->toString());
std::string error;
if (!runtime_.load(localPath(runtimeLibrary(
QString::fromStdString(detection.version->line()))), error)) {
previewWidget_->setMessage(tr("暂时无法加载这套资源"));
statusLabel_->setText(QString::fromStdString(error));
return;
}
const auto metadata = runtime_.loadSkeleton(
localPath(skeletonPath), localPath(atlasPath), 1.0F, error);
if (!metadata) {
previewWidget_->setMessage(tr("此版本的预览模块仍在接入中"));
statusLabel_->setText(tr("资源已识别,可以稍后转换"));
return;
}
for (const auto& animation : metadata->animations)
animationCombo_->addItem(QString::fromStdString(animation.name), animation.duration);
animationCombo_->setEnabled(animationCombo_->count() > 0);
statusLabel_->setText(tr("已识别 %1 个动画").arg(metadata->animations.size()));
if (animationCombo_->count() > 0) {
animationChanged(animationCombo_->itemText(0));
} else {
auto frame = runtime_.updatePreview(0.0F, error);
if (frame) previewWidget_->setFrame(std::move(*frame));
else previewWidget_->setMessage(tr("没有可播放的动画"));
}
}
void MainWindow::animationChanged(const QString& name) {
if (name.isEmpty()) return;
std::string error;
if (!runtime_.setAnimation(name.toStdString(), true, error)) {
statusLabel_->setText(QString::fromStdString(error));
return;
}
advancePreview();
previewTimer_->start();
}
void MainWindow::advancePreview() {
std::string error;
auto frame = runtime_.updatePreview(0.016F, error);
if (!frame) {
previewTimer_->stop();
previewWidget_->setMessage(tr("动画预览暂时不可用"));
statusLabel_->setText(QString::fromStdString(error));
return;
}
previewWidget_->setFrame(std::move(*frame));
}
QString MainWindow::resolveEditor() {
QSettings settings;
SpineEditorLocator locator;
const auto saved = settings.value("spineEditorPath").toString();
if (!saved.isEmpty()) {
if (const auto normalized = locator.normalize(localPath(saved)))
return QString::fromStdString(normalized->string());
}
const auto found = locator.discover();
if (!found.empty()) return QString::fromStdString(found.front().string());
return {};
}
void MainWindow::scheduleAvailabilityCheck() {
if (availabilityCheckScheduled_) return;
availabilityCheckScheduled_ = true;
QTimer::singleShot(0, this, [this] {
availabilityCheckScheduled_ = false;
refreshAvailability();
});
}
void MainWindow::showAvailability(const QString& text, const QString& color) {
availabilityLabel_->setText(QStringLiteral("") + text);
availabilityLabel_->setStyleSheet(QStringLiteral(
"QLabel { color: %1; background: #20242c; border: 1px solid #343a46; "
"border-radius: 8px; padding: 7px 12px; font-weight: 600; }").arg(color));
availabilityLabel_->setVisible(true);
}
void MainWindow::refreshAvailability() {
availableVersions_.clear();
currentEditorVersion_.clear();
editorPath_ = resolveEditor();
SpineVersionAvailability availability;
for (const auto& path : skeletonPaths_) {
const auto detection = SpineDataVersionDetector{}.inspect(localPath(path));
if (!detection.version) continue;
const auto line = QString::fromStdString(detection.version->line());
if (const auto installed = availability.newestForLine(detection.version->line()))
availableVersions_.insert(line, QString::fromStdString(installed->toString()));
}
const auto current = SpineDataVersionDetector{}.inspect(
localPath(resourceCombo_->currentData().toString()));
if (!current.version) return;
const auto atlasPath = findAtlas(resourceCombo_->currentData().toString());
if (atlasPath.isEmpty()) {
showAvailability(tr("缺少配套 Atlas"), QStringLiteral("#ef6b73"));
convertButton_->setText(tr("资源不完整"));
convertButton_->setEnabled(false);
return;
}
const auto atlas = AtlasParser{}.parseFile(localPath(atlasPath));
if (!atlas.document) {
showAvailability(tr("Atlas 无法识别"), QStringLiteral("#ef6b73"));
convertButton_->setText(tr("资源不完整"));
convertButton_->setEnabled(false);
return;
}
const QDir atlasDirectory = QFileInfo(atlasPath).dir();
for (const auto& page : atlas.document->pages) {
const auto pagePath = QString::fromStdString(page.imagePath.string());
if (!QFileInfo::exists(atlasDirectory.filePath(pagePath))) {
showAvailability(tr("缺少纹理页 %1").arg(QFileInfo(pagePath).fileName()),
QStringLiteral("#ef6b73"));
convertButton_->setText(tr("资源不完整"));
convertButton_->setEnabled(false);
return;
}
}
const auto line = QString::fromStdString(current.version->line());
if (editorPath_.isEmpty()) {
showAvailability(tr("未找到 Spine Editor"), QStringLiteral("#f2b84b"));
convertButton_->setText(tr("指定 Spine 路径"));
convertButton_->setEnabled(true);
return;
}
currentEditorVersion_ = availableVersions_.value(line);
if (currentEditorVersion_.isEmpty()) {
showAvailability(tr("缺少 Spine %1").arg(line), QStringLiteral("#ef6b73"));
convertButton_->setText(tr("指定 Spine %1 路径").arg(line));
convertButton_->setEnabled(true);
return;
}
showAvailability(tr("可转换 · Spine %1").arg(currentEditorVersion_),
QStringLiteral("#55d68b"));
convertButton_->setText(tr("一键转换为 .spine"));
convertButton_->setEnabled(true);
}
bool MainWindow::chooseEditorPath() {
const auto selected = QFileDialog::getOpenFileName(this,
tr("指定 Spine 程序"), {}, tr("所有文件 (*)"));
if (selected.isEmpty()) return false;
SpineEditorLocator locator;
const auto normalized = locator.normalize(localPath(selected));
if (!normalized) {
showAvailability(tr("选择的路径不是 Spine Editor"), QStringLiteral("#ef6b73"));
return false;
}
QSettings{}.setValue("spineEditorPath", selected);
editorPath_ = QString::fromStdString(normalized->string());
refreshAvailability();
return !currentEditorVersion_.isEmpty();
}
void MainWindow::convertCurrent() {
if (editorPath_.isEmpty() || currentEditorVersion_.isEmpty())
if (!chooseEditorPath()) return;
const QString input = resourceCombo_->currentData().toString();
const auto outputParent = QFileDialog::getExistingDirectory(this,
tr("选择输出位置"), QFileInfo(input).absolutePath());
if (outputParent.isEmpty()) return;
const auto atlas = findAtlas(input);
if (atlas.isEmpty()) {
QMessageBox::critical(this, tr("转换失败"), tr("没有找到配套的 .atlas 文件。"));
return;
}
ConversionRequest request{
editorPath_, currentEditorVersion_, input, atlas, outputParent,
QFileInfo(input).completeBaseName(), unpremultiplyCheck_->isChecked()};
conversionJob_ = new ConversionJob(std::move(request), this);
connect(conversionJob_, &ConversionJob::finished,
this, &MainWindow::conversionFinished);
setBusy(true);
conversionJob_->start();
}
void MainWindow::setBusy(bool busy) {
convertButton_->setEnabled(!busy);
convertButton_->setText(busy ? tr("正在转换…") : tr("一键转换为 .spine"));
if (busy) showAvailability(tr("正在使用 Spine %1 转换").arg(currentEditorVersion_),
QStringLiteral("#72a7ff"));
}
void MainWindow::conversionFinished(bool success, const QString& outputDirectory,
const QString& error) {
conversionJob_->deleteLater();
conversionJob_ = nullptr;
setBusy(false);
statusLabel_->setText(success ? tr("转换完成") : tr("转换失败"));
statusLabel_->setToolTip(success ? outputDirectory : error);
refreshAvailability();
if (success) {
QMessageBox::information(this, tr("转换完成"),
tr("完整资源已生成:\n%1").arg(outputDirectory));
} else {
QMessageBox::critical(this, tr("转换失败"),
error.isEmpty() ? tr("转换未完成,Spine Editor 未提供详细原因。") : error);
}
}
void MainWindow::dragEnterEvent(QDragEnterEvent* event) {
if (event->mimeData()->hasUrls()) event->acceptProposedAction();
}
void MainWindow::dropEvent(QDropEvent* event) {
for (const auto& url : event->mimeData()->urls())
if (url.isLocalFile()) addInput(url.toLocalFile());
event->acceptProposedAction();
}
} // namespace dts
+73
View File
@@ -0,0 +1,73 @@
#pragma once
#include "preview/RuntimePlugin.hpp"
#include <QMainWindow>
#include <QHash>
#include <QStringList>
class QComboBox;
class QCheckBox;
class QLabel;
class QPushButton;
class QTimer;
namespace dts {
class SpinePreviewWidget;
class ConversionJob;
class MainWindow final : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget* parent = nullptr);
void openResourcePath(const QString& path);
protected:
void dragEnterEvent(QDragEnterEvent* event) override;
void dropEvent(QDropEvent* event) override;
private slots:
void chooseResources();
void resourceChanged(int index);
void convertCurrent();
void conversionFinished(bool success, const QString& outputDirectory,
const QString& error);
void animationChanged(const QString& name);
void advancePreview();
private:
void setupUi();
void addInput(const QString& path);
void addSkeleton(const QString& path);
void loadCurrentPreview();
QString findAtlas(const QString& skeletonPath) const;
QString runtimeLibrary(const QString& versionLine) const;
QString resolveEditor();
void scheduleAvailabilityCheck();
void refreshAvailability();
bool chooseEditorPath();
void showAvailability(const QString& text, const QString& color);
void showEmptyState();
void setBusy(bool busy);
QComboBox* resourceCombo_ = nullptr;
QComboBox* animationCombo_ = nullptr;
QCheckBox* unpremultiplyCheck_ = nullptr;
SpinePreviewWidget* previewWidget_ = nullptr;
QLabel* statusLabel_ = nullptr;
QLabel* availabilityLabel_ = nullptr;
QPushButton* convertButton_ = nullptr;
QStringList skeletonPaths_;
QString currentVersion_;
QString currentEditorVersion_;
QString editorPath_;
QHash<QString, QString> availableVersions_;
bool availabilityCheckScheduled_ = false;
RuntimePlugin runtime_;
ConversionJob* conversionJob_ = nullptr;
QTimer* previewTimer_ = nullptr;
};
} // namespace dts
+195
View File
@@ -0,0 +1,195 @@
#include "ui/SpinePreviewWidget.hpp"
#include <QApplication>
#include <QColor>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QPolygonF>
#include <QTransform>
#include <QWheelEvent>
#include <algorithm>
#include <cmath>
#include <limits>
namespace dts {
namespace {
QString localString(const std::filesystem::path& path) {
#if defined(_WIN32)
return QString::fromStdWString(path.wstring());
#else
const auto text = path.u8string();
return QString::fromUtf8(reinterpret_cast<const char*>(text.data()),
static_cast<qsizetype>(text.size()));
#endif
}
QPainter::CompositionMode compositionMode(std::uint32_t blendMode) {
switch (blendMode) {
case 1: return QPainter::CompositionMode_Plus;
case 2: return QPainter::CompositionMode_Multiply;
case 3: return QPainter::CompositionMode_Screen;
default: return QPainter::CompositionMode_SourceOver;
}
}
} // namespace
SpinePreviewWidget::SpinePreviewWidget(QWidget* parent) : QWidget(parent) {
setMinimumSize(640, 440);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
setAutoFillBackground(false);
setMouseTracking(true);
setToolTip(tr("滚轮缩放 · 中键重置 · 右键拖动 · 右键单击居中"));
}
void SpinePreviewWidget::setFrame(std::vector<PreviewDrawCommand> commands) {
if (!viewBounds_ && !commands.empty()) {
float left = std::numeric_limits<float>::max();
float right = std::numeric_limits<float>::lowest();
float bottom = std::numeric_limits<float>::max();
float top = std::numeric_limits<float>::lowest();
for (const auto& command : commands) for (const auto& vertex : command.vertices) {
left = std::min(left, vertex.x);
right = std::max(right, vertex.x);
bottom = std::min(bottom, vertex.y);
top = std::max(top, vertex.y);
}
if (right > left && top > bottom)
viewBounds_ = QRectF(left, bottom, right - left, top - bottom);
}
commands_ = std::move(commands);
message_.clear();
update();
}
void SpinePreviewWidget::resetViewport() {
viewBounds_.reset();
zoom_ = 1.0F;
pan_ = {};
}
void SpinePreviewWidget::setMessage(const QString& message) {
commands_.clear();
message_ = message;
update();
}
const QImage& SpinePreviewWidget::texture(const std::filesystem::path& path) {
const auto key = localString(path);
auto found = textures_.find(key);
if (found == textures_.end()) found = textures_.insert(key, QImage(key));
return found.value();
}
void SpinePreviewWidget::paintEvent(QPaintEvent*) {
QPainter painter(this);
painter.setRenderHint(QPainter::SmoothPixmapTransform);
painter.fillRect(rect(), QColor("#101217"));
if (commands_.empty()) {
painter.setPen(QColor("#a7adb8"));
painter.drawText(rect(), Qt::AlignCenter | Qt::TextWordWrap, message_);
return;
}
const QRectF bounds = viewBounds_.value_or(QRectF(0, 0, 1, 1));
const float contentWidth = std::max(1.0, bounds.width());
const float contentHeight = std::max(1.0, bounds.height());
const float baseScale = 0.94F * std::min(width() / contentWidth, height() / contentHeight);
const float scale = baseScale * zoom_;
const QPointF center = bounds.center();
const auto toScreen = [this, center, scale](const PreviewVertex& vertex) {
return QPointF(width() * 0.5 + pan_.x() + (vertex.x - center.x()) * scale,
height() * 0.5 + pan_.y() + (vertex.y - center.y()) * scale);
};
for (const auto& command : commands_) {
const auto& image = texture(command.texturePath);
if (image.isNull()) continue;
painter.setCompositionMode(compositionMode(command.blendMode));
for (std::size_t index = 0; index + 2 < command.indices.size(); index += 3) {
const auto i0 = command.indices[index];
const auto i1 = command.indices[index + 1];
const auto i2 = command.indices[index + 2];
if (i0 >= command.vertices.size() || i1 >= command.vertices.size()
|| i2 >= command.vertices.size()) continue;
const auto& v0 = command.vertices[i0];
const auto& v1 = command.vertices[i1];
const auto& v2 = command.vertices[i2];
const QPointF s0(v0.u * image.width(), v0.v * image.height());
const QPointF s1(v1.u * image.width(), v1.v * image.height());
const QPointF s2(v2.u * image.width(), v2.v * image.height());
const QPointF d0 = toScreen(v0);
const QPointF d1 = toScreen(v1);
const QPointF d2 = toScreen(v2);
QPolygonF source{s0, s1, s2, s1 + s2 - s0};
QPolygonF destination{d0, d1, d2, d1 + d2 - d0};
QTransform transform;
if (!QTransform::quadToQuad(source, destination, transform)) continue;
QPainterPath clip;
clip.addPolygon(QPolygonF{d0, d1, d2});
painter.save();
painter.setClipPath(clip);
painter.setOpacity(QColor::fromRgba(v0.color).alphaF());
painter.setTransform(transform);
painter.drawImage(QPointF(0, 0), image);
painter.restore();
}
}
}
void SpinePreviewWidget::wheelEvent(QWheelEvent* event) {
if (commands_.empty() || event->angleDelta().y() == 0) return;
const float previousZoom = zoom_;
const float factor = event->angleDelta().y() > 0 ? 1.12F : 1.0F / 1.12F;
zoom_ = std::clamp(previousZoom * factor, 0.15F, 8.0F);
const QPointF canvasCenter(width() * 0.5, height() * 0.5);
const QPointF cursorOffset = event->position() - canvasCenter;
pan_ = cursorOffset - (cursorOffset - pan_) * (zoom_ / previousZoom);
update();
event->accept();
}
void SpinePreviewWidget::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::MiddleButton) {
zoom_ = 1.0F;
pan_ = {};
update();
event->accept();
return;
}
if (event->button() == Qt::RightButton) {
rightPressPosition_ = event->position();
lastMousePosition_ = event->position();
rightDragging_ = false;
setCursor(Qt::ClosedHandCursor);
event->accept();
}
}
void SpinePreviewWidget::mouseMoveEvent(QMouseEvent* event) {
if (!(event->buttons() & Qt::RightButton)) return;
if (!rightDragging_ && (event->position() - rightPressPosition_).manhattanLength()
>= QApplication::startDragDistance()) rightDragging_ = true;
if (rightDragging_) {
pan_ += event->position() - lastMousePosition_;
update();
}
lastMousePosition_ = event->position();
event->accept();
}
void SpinePreviewWidget::mouseReleaseEvent(QMouseEvent* event) {
if (event->button() != Qt::RightButton) return;
unsetCursor();
if (!rightDragging_) {
pan_ = {};
update();
}
rightDragging_ = false;
event->accept();
}
} // namespace dts
+44
View File
@@ -0,0 +1,44 @@
#pragma once
#include "preview/RuntimePlugin.hpp"
#include <QHash>
#include <QImage>
#include <QPointF>
#include <QRectF>
#include <QWidget>
#include <optional>
namespace dts {
class SpinePreviewWidget final : public QWidget {
public:
explicit SpinePreviewWidget(QWidget* parent = nullptr);
void setFrame(std::vector<PreviewDrawCommand> commands);
void setMessage(const QString& message);
void resetViewport();
protected:
void paintEvent(QPaintEvent* event) override;
void wheelEvent(QWheelEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
private:
const QImage& texture(const std::filesystem::path& path);
std::vector<PreviewDrawCommand> commands_;
QHash<QString, QImage> textures_;
QString message_;
std::optional<QRectF> viewBounds_;
float zoom_ = 1.0F;
QPointF pan_;
QPointF rightPressPosition_;
QPointF lastMousePosition_;
bool rightDragging_ = false;
};
} // namespace dts