导出数据转spine
This commit is contained in:
1 parent
d258d4f435
commit
3a7c0b4092
65 files changed
+6000
No files matched your search
@@ -0,0 +1,218 @@
|
||||
#include "conversion/ConversionJob.hpp"
|
||||
|
||||
#include "atlas/AtlasParser.hpp"
|
||||
#include "domain/SpineVersion.hpp"
|
||||
#include "editor/SpineVersionAvailability.hpp"
|
||||
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
#include <cmath>
|
||||
#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 uniqueDirectory(const QString& parent, const QString& name) {
|
||||
QDir directory(parent);
|
||||
QString candidate = directory.filePath(name);
|
||||
for (int suffix = 2; QFileInfo::exists(candidate); ++suffix)
|
||||
candidate = directory.filePath(QStringLiteral("%1-%2").arg(name).arg(suffix));
|
||||
return candidate;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ConversionJob::ConversionJob(ConversionRequest request, QObject* parent)
|
||||
: QObject(parent), request_(std::move(request)) {}
|
||||
|
||||
ConversionJob::~ConversionJob() = default;
|
||||
|
||||
void ConversionJob::start() {
|
||||
QString error;
|
||||
if (!prepare(error)) {
|
||||
fail(error);
|
||||
return;
|
||||
}
|
||||
startUnpack();
|
||||
}
|
||||
|
||||
bool ConversionJob::prepare(QString& error) {
|
||||
const auto version = SpineVersion::parse(request_.editorVersion.toStdString());
|
||||
if (!version || !SpineVersionAvailability{}.isCached(*version)) {
|
||||
error = tr("本地未缓存 Spine %1。为避免触发下载,转换已停止。")
|
||||
.arg(request_.editorVersion);
|
||||
return false;
|
||||
}
|
||||
if (!QFileInfo::exists(request_.editorPath)) {
|
||||
error = tr("找不到已指定的 Spine Editor。请重新指定程序路径。");
|
||||
return false;
|
||||
}
|
||||
const auto atlas = AtlasParser{}.parseFile(localPath(request_.atlasPath));
|
||||
if (!atlas.document || atlas.document->pages.empty()) {
|
||||
error = tr("Atlas 无法识别,不能确定官方导入比例。");
|
||||
return false;
|
||||
}
|
||||
double scale = atlas.document->pages.front().scale.value_or(1.0);
|
||||
if (!std::isfinite(scale) || scale <= 0.0) {
|
||||
error = tr("Atlas 中的 scale 无效。");
|
||||
return false;
|
||||
}
|
||||
for (const auto& page : atlas.document->pages) {
|
||||
const double pageScale = page.scale.value_or(1.0);
|
||||
if (!std::isfinite(pageScale) || std::abs(pageScale - scale) > 0.000001) {
|
||||
error = tr("Atlas 的纹理页使用了不同 scale,无法生成尺寸一致的工程。");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
importScale_ = QString::number(scale, 'g', 15);
|
||||
QDir parent(request_.outputParent);
|
||||
if (!parent.exists() && !parent.mkpath(QStringLiteral("."))) {
|
||||
error = tr("无法创建输出目录。");
|
||||
return false;
|
||||
}
|
||||
finalDirectory_ = uniqueDirectory(request_.outputParent, request_.assetName);
|
||||
staging_ = std::make_unique<QTemporaryDir>(
|
||||
parent.filePath(QStringLiteral(".datetospine-XXXXXX")));
|
||||
if (!staging_->isValid()) {
|
||||
error = tr("无法创建临时转换目录。");
|
||||
return false;
|
||||
}
|
||||
QDir staging(staging_->path());
|
||||
if (!staging.mkpath(QStringLiteral("images"))) {
|
||||
error = tr("无法准备图片目录。");
|
||||
return false;
|
||||
}
|
||||
projectPath_ = staging.filePath(request_.assetName + QStringLiteral(".spine"));
|
||||
// The official unpacker reads `pma` from the Atlas. When conversion to
|
||||
// straight alpha is requested, pass the source Atlas unchanged so Spine
|
||||
// performs the conversion itself. Otherwise a temporary metadata copy
|
||||
// disables that conversion while leaving all geometry fields intact.
|
||||
unpackAtlasPath_ = request_.unpremultiplyAlpha
|
||||
? request_.atlasPath
|
||||
: staging.filePath(QStringLiteral("unpack.atlas"));
|
||||
return writeUnpackAtlas(error);
|
||||
}
|
||||
|
||||
bool ConversionJob::writeUnpackAtlas(QString& error) {
|
||||
if (request_.unpremultiplyAlpha) return true;
|
||||
|
||||
QFile source(request_.atlasPath);
|
||||
if (!source.open(QIODevice::ReadOnly)) {
|
||||
error = tr("无法读取 Atlas 文件。");
|
||||
return false;
|
||||
}
|
||||
auto contents = QString::fromUtf8(source.readAll());
|
||||
source.close();
|
||||
// Preserve packed pixel values when the user explicitly leaves PMA enabled.
|
||||
// Pixel decoding, cropping and Alpha handling remain entirely inside Spine.
|
||||
contents.replace(QRegularExpression(
|
||||
QStringLiteral("(?m)^(\\s*pma\\s*:\\s*)true(\\s*)$")), QStringLiteral("\\1false\\2"));
|
||||
QFile temporary(unpackAtlasPath_);
|
||||
const auto encoded = contents.toUtf8();
|
||||
if (!temporary.open(QIODevice::WriteOnly | QIODevice::Truncate)
|
||||
|| temporary.write(encoded) != encoded.size()) {
|
||||
error = tr("无法准备离线图集解包配置。");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ConversionJob::startUnpack() {
|
||||
stage_ = Stage::Unpacking;
|
||||
process_ = new QProcess(this);
|
||||
process_->setProcessChannelMode(QProcess::MergedChannels);
|
||||
connect(process_, &QProcess::finished, this, &ConversionJob::processFinished);
|
||||
connect(process_, &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) {
|
||||
if (error == QProcess::FailedToStart && stage_ != Stage::Done)
|
||||
fail(tr("无法启动本机 Spine Editor。"));
|
||||
});
|
||||
process_->start(request_.editorPath, {
|
||||
"-u", request_.editorVersion,
|
||||
"-i", QFileInfo(request_.atlasPath).absolutePath(),
|
||||
"-o", QDir(staging_->path()).filePath(QStringLiteral("images")),
|
||||
"-c", unpackAtlasPath_});
|
||||
}
|
||||
|
||||
void ConversionJob::startImport() {
|
||||
stage_ = Stage::Importing;
|
||||
process_->start(request_.editorPath, {
|
||||
"-u", request_.editorVersion,
|
||||
"-i", request_.skeletonPath,
|
||||
// Numeric scale works with all supported Editor lines. Older Editors
|
||||
// (notably 4.0) do not accept an Atlas path for this argument.
|
||||
"-s", importScale_,
|
||||
"-o", projectPath_,
|
||||
"--import"});
|
||||
}
|
||||
|
||||
bool ConversionJob::hasUnpackedImages() const {
|
||||
QDirIterator iterator(QDir(staging_->path()).filePath(QStringLiteral("images")),
|
||||
{"*.png", "*.jpg", "*.jpeg", "*.webp"}, QDir::Files,
|
||||
QDirIterator::Subdirectories);
|
||||
return iterator.hasNext();
|
||||
}
|
||||
|
||||
void ConversionJob::processFinished(int exitCode) {
|
||||
if (stage_ == Stage::Done) return;
|
||||
const auto output = QString::fromUtf8(process_->readAll());
|
||||
if (exitCode != 0) {
|
||||
fail(stage_ == Stage::Unpacking
|
||||
? tr("官方图集解包失败:\n%1").arg(output.right(1600))
|
||||
: tr("Spine 工程导入失败:\n%1").arg(output.right(1600)));
|
||||
return;
|
||||
}
|
||||
if (stage_ == Stage::Unpacking) {
|
||||
if (!hasUnpackedImages()) {
|
||||
fail(tr("官方图集解包未生成任何图片。"));
|
||||
return;
|
||||
}
|
||||
if (unpackAtlasPath_ != request_.atlasPath) QFile::remove(unpackAtlasPath_);
|
||||
startImport();
|
||||
return;
|
||||
}
|
||||
if (stage_ == Stage::Importing) {
|
||||
if (QFileInfo(projectPath_).size() <= 0) {
|
||||
fail(tr("Spine Editor 未生成有效的 .spine 工程。"));
|
||||
return;
|
||||
}
|
||||
publish();
|
||||
}
|
||||
}
|
||||
|
||||
void ConversionJob::publish() {
|
||||
stage_ = Stage::Publishing;
|
||||
const auto source = localPath(staging_->path());
|
||||
const auto destination = localPath(finalDirectory_);
|
||||
std::error_code error;
|
||||
std::filesystem::rename(source, destination, error);
|
||||
if (error) {
|
||||
fail(tr("无法发布转换结果:%1").arg(QString::fromStdString(error.message())));
|
||||
return;
|
||||
}
|
||||
staging_->setAutoRemove(false);
|
||||
stage_ = Stage::Done;
|
||||
emit finished(true, finalDirectory_, {});
|
||||
}
|
||||
|
||||
void ConversionJob::fail(const QString& reason) {
|
||||
if (stage_ == Stage::Done) return;
|
||||
stage_ = Stage::Done;
|
||||
if (process_) process_->kill();
|
||||
emit finished(false, {}, reason);
|
||||
}
|
||||
|
||||
} // namespace dts
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include <memory>
|
||||
|
||||
class QProcess;
|
||||
class QTemporaryDir;
|
||||
|
||||
namespace dts {
|
||||
|
||||
struct ConversionRequest {
|
||||
QString editorPath;
|
||||
QString editorVersion;
|
||||
QString skeletonPath;
|
||||
QString atlasPath;
|
||||
QString outputParent;
|
||||
QString assetName;
|
||||
bool unpremultiplyAlpha = false;
|
||||
};
|
||||
|
||||
class ConversionJob final : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ConversionJob(ConversionRequest request, QObject* parent = nullptr);
|
||||
~ConversionJob() override;
|
||||
void start();
|
||||
|
||||
signals:
|
||||
void finished(bool success, const QString& outputDirectory, const QString& error);
|
||||
|
||||
private:
|
||||
enum class Stage { Preparing, Unpacking, Importing, Publishing, Done };
|
||||
|
||||
bool prepare(QString& error);
|
||||
bool writeUnpackAtlas(QString& error);
|
||||
bool hasUnpackedImages() const;
|
||||
void startUnpack();
|
||||
void startImport();
|
||||
void processFinished(int exitCode);
|
||||
void fail(const QString& reason);
|
||||
void publish();
|
||||
|
||||
ConversionRequest request_;
|
||||
Stage stage_ = Stage::Preparing;
|
||||
std::unique_ptr<QTemporaryDir> staging_;
|
||||
QProcess* process_ = nullptr;
|
||||
QString finalDirectory_;
|
||||
QString projectPath_;
|
||||
QString unpackAtlasPath_;
|
||||
QString importScale_;
|
||||
};
|
||||
|
||||
} // namespace dts
|
||||
Reference in new issue
Block a user