Files
EVEngine/modelconverter/plugin/ModelConverter.cpp
T

371 lines
12 KiB
C++

#include "ModelConverter.h"
#include "common/Exception.h"
#include <simplesquirrel/simplesquirrel.hpp>
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <sstream>
#include <sys/stat.h>
#include <sys/types.h>
#if defined(_WIN32)
# include <direct.h>
# include <windows.h>
#else
# include <dirent.h>
# include <sys/stat.h>
#endif
namespace eve::modelconverter {
namespace {
bool fileExists(const std::string &path) {
struct stat st;
return stat(path.c_str(), &st) == 0;
}
bool makeDir(const std::string &path) {
if (path.empty()) return true;
if (fileExists(path)) return true;
#if defined(_WIN32)
return _mkdir(path.c_str()) == 0;
#else
return mkdir(path.c_str(), 0755) == 0;
#endif
}
std::string joinPath(const std::string &a, const std::string &b) {
if (a.empty()) return b;
if (a.back() == '/' || a.back() == '\\') return a + b;
return a + "/" + b;
}
std::string quote(const std::string &s) {
return "\"" + s + "\"";
}
// Escape a string for inclusion inside a JSON string literal.
std::string jsonEscape(const std::string &s) {
std::ostringstream out;
for (char c : s) {
switch (c) {
case '"': out << "\\\""; break;
case '\\': out << "\\\\"; break;
case '\n': out << "\\n"; break;
case '\r': out << "\\r"; break;
case '\t': out << "\\t"; break;
default:
if (static_cast<unsigned char>(c) < 0x20) {
char buf[8];
snprintf(buf, sizeof(buf), "\\u%04x", c);
out << buf;
} else {
out << c;
}
}
}
return out.str();
}
} // namespace
// ---- Params ----
void ModelConverterParams::setInt(const std::string &key, int value) {
values_[key] = std::to_string(value);
}
void ModelConverterParams::setFloat(const std::string &key, float value) {
std::ostringstream ss;
ss << value;
values_[key] = ss.str();
}
void ModelConverterParams::setString(const std::string &key, const std::string &value) {
values_[key] = value;
}
bool ModelConverterParams::has(const std::string &key) const {
return values_.count(key) > 0;
}
int ModelConverterParams::getInt(const std::string &key, int defaultValue) const {
auto it = values_.find(key);
if (it == values_.end()) return defaultValue;
return std::atoi(it->second.c_str());
}
float ModelConverterParams::getFloat(const std::string &key, float defaultValue) const {
auto it = values_.find(key);
if (it == values_.end()) return defaultValue;
return static_cast<float>(std::atof(it->second.c_str()));
}
std::string ModelConverterParams::getString(const std::string &key,
const std::string &defaultValue) const {
auto it = values_.find(key);
if (it == values_.end()) return defaultValue;
return it->second;
}
std::string ModelConverterParams::toJson() const {
std::ostringstream out;
out << "{";
bool first = true;
for (const auto &kv : values_) {
if (!first) out << ",";
first = false;
out << "\"" << jsonEscape(kv.first) << "\":\"" << jsonEscape(kv.second) << "\"";
}
out << "}";
return out.str();
}
// ---- Module ----
Module_IMPL(ModelConverter, new ModelConverter());
ModelConverter::ModelConverter() {
const char *tmp = std::getenv("TEMP");
if (!tmp || !*tmp) tmp = std::getenv("TMP");
if (!tmp || !*tmp) tmp = ".";
tempDir_ = tmp;
}
ModelConverterParams *ModelConverter::newParams() { return new ModelConverterParams(); }
bool ModelConverter::configure(const std::string &converterDir, const std::string &pythonExe,
const std::string &pythonRuntimeDir, const std::string &tempDir) {
converterDir_ = converterDir;
pythonExe_ = pythonExe;
pythonRuntimeDir_ = pythonRuntimeDir;
if (!tempDir.empty()) tempDir_ = tempDir;
lastError_.clear();
return true;
}
int ModelConverter::getConverterCount() const {
converterIdsCache_.clear();
if (converterDir_.empty()) return 0;
#if defined(_WIN32)
const std::string pattern = converterDir_ + "/*";
WIN32_FIND_DATAA fd;
HANDLE h = FindFirstFileA(pattern.c_str(), &fd);
if (h == INVALID_HANDLE_VALUE) return 0;
do {
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
const std::string name = fd.cFileName;
if (name == "." || name == "..") continue;
if (fileExists(joinPath(joinPath(converterDir_, name), "manifest.json")))
converterIdsCache_.push_back(name);
}
} while (FindNextFileA(h, &fd));
FindClose(h);
#else
// Non-Windows: directory scan via <dirent.h>.
DIR *dir = opendir(converterDir_.c_str());
if (!dir) return 0;
struct dirent *ent;
while ((ent = readdir(dir)) != nullptr) {
if (ent->d_name[0] == '.') continue;
if (fileExists(joinPath(joinPath(converterDir_, ent->d_name), "manifest.json")))
converterIdsCache_.push_back(ent->d_name);
}
closedir(dir);
#endif
std::sort(converterIdsCache_.begin(), converterIdsCache_.end());
return int(converterIdsCache_.size());
}
std::string ModelConverter::getConverterId(int index) const {
if (converterIdsCache_.empty()) getConverterCount();
if (index < 0 || index >= int(converterIdsCache_.size())) return {};
return converterIdsCache_[size_t(index)];
}
bool ModelConverter::hasConverter(const std::string &id) const {
if (converterIdsCache_.empty()) getConverterCount();
for (const auto &cid : converterIdsCache_)
if (cid == id) return true;
return false;
}
bool ModelConverter::spawn(const std::string &command) {
#if defined(_WIN32)
std::string full = "cmd /c " + command;
return std::system(full.c_str()) == 0;
#else
return std::system(command.c_str()) == 0;
#endif
}
bool ModelConverter::runPython(const std::vector<std::string> &args, std::string &capturedError) {
if (pythonExe_.empty()) {
capturedError = "modelconverter: python executable not configured (call configure)";
return false;
}
makeDir(tempDir_);
std::string prefix;
if (!pythonRuntimeDir_.empty()) {
// Make `-m eve_blender_converter` importable for the child process.
#if defined(_WIN32)
prefix = "set PYTHONPATH=" + quote(pythonRuntimeDir_) + " && ";
#else
prefix = "PYTHONPATH=" + quote(pythonRuntimeDir_) + " ";
#endif
}
std::ostringstream cmd;
cmd << prefix << quote(pythonExe_) << " -m eve_blender_converter";
for (const auto &arg : args) cmd << " " << quote(arg);
const std::string logPath = joinPath(tempDir_, "modelconverter_last.log");
cmd << " 2>&1 > " << quote(logPath);
const int rc = spawn(cmd.str());
std::string log;
{
std::ifstream in(logPath, std::ios::binary);
if (in) {
std::ostringstream ss;
ss << in.rdbuf();
log = ss.str();
}
}
// Trim trailing whitespace.
while (!log.empty() && (log.back() == '\n' || log.back() == '\r')) log.pop_back();
if (rc != 0) {
capturedError = log.empty() ? "python exited with code " + std::to_string(rc) : log;
return false;
}
capturedError = log;
return true;
}
bool ModelConverter::check() {
std::string captured;
std::vector<std::string> args = {"check"};
if (!runPython(args, captured)) {
lastError_ = captured.empty() ? "check failed" : captured;
return false;
}
if (captured.find("bpy OK") == std::string::npos) {
lastError_ = "bpy is not available: pip install bpy\n" + captured;
return false;
}
lastError_.clear();
return true;
}
bool ModelConverter::convert(const std::string &converterId, const std::string &inputModel,
const std::string &outputModel, const std::string &format,
const ModelConverterParams *params) {
lastError_.clear();
lastOutput_.clear();
if (converterDir_.empty() || pythonExe_.empty()) {
lastError_ = "modelconverter: call configure(converterDir, pythonExe, ...) first";
return false;
}
if (!hasConverter(converterId)) {
lastError_ = "modelconverter: unknown converter '" + converterId + "'";
return false;
}
if (inputModel.empty() || outputModel.empty()) {
lastError_ = "modelconverter: input and output paths are required";
return false;
}
makeDir(tempDir_);
const std::string jobPath = joinPath(tempDir_, "modelconverter_job.txt");
const std::string resultPath = joinPath(tempDir_, "modelconverter_result.txt");
{
std::ofstream job(jobPath, std::ios::trunc);
if (!job) {
lastError_ = "modelconverter: cannot write job file " + jobPath;
return false;
}
job << "converter=" << converterId << "\n";
job << "input=" << inputModel << "\n";
job << "output=" << outputModel << "\n";
job << "format=" << (format.empty() ? "glb" : format) << "\n";
job << "converter_dir=" << converterDir_ << "\n";
job << "temp_dir=" << tempDir_ << "\n";
job << "params=" << (params ? params->toJson() : "{}") << "\n";
}
std::string captured;
std::vector<std::string> args = {"convert", jobPath};
runPython(args, captured);
// The result file is authoritative; python may exit non-zero yet still
// write a useful error into it (e.g. missing bpy).
std::ifstream result(resultPath);
if (result) {
std::string line;
bool ok = false;
std::string errorFromResult;
while (std::getline(result, line)) {
const auto eq = line.find('=');
if (eq == std::string::npos) continue;
const std::string key = line.substr(0, eq);
const std::string value = line.substr(eq + 1);
if (key == "ok") ok = (value == "1");
else if (key == "error" && !value.empty()) errorFromResult = value;
else if (key == "output") lastOutput_ = value;
}
if (ok) return true;
lastError_ = errorFromResult.empty() ? "converter reported failure (see log)" : errorFromResult;
lastOutput_.clear();
return false;
}
lastError_ = "modelconverter: no result file from python runtime:\n" + captured;
return false;
}
std::string ModelConverter::lastError() const { return lastError_; }
std::string ModelConverter::lastOutput() const { return lastOutput_; }
void ModelConverter::expose(ssq::Table &table) {
auto cls = table.addClass(name, ModelConverter::create, false);
expose(cls);
auto params = table.addClass<ModelConverterParams>(
"ModelConverterParams",
std::function<ModelConverterParams *()>([]() -> ModelConverterParams * { return nullptr; }),
true);
params.addFunc("setInt", &ModelConverterParams::setInt);
params.addFunc("setFloat", &ModelConverterParams::setFloat);
params.addFunc("setString", &ModelConverterParams::setString);
params.addFunc("has", &ModelConverterParams::has);
params.addFunc("getInt", &ModelConverterParams::getInt);
params.addFunc("getFloat", &ModelConverterParams::getFloat);
params.addFunc("getString", &ModelConverterParams::getString);
}
void ModelConverter::expose(ssq::Class &cls) {
cls.addFunc("getName", &ModelConverter::getName);
cls.addFunc("newParams", &ModelConverter::newParams);
cls.addFunc("configure", &ModelConverter::configure);
cls.addFunc("check", &ModelConverter::check);
cls.addFunc("getConverterCount", &ModelConverter::getConverterCount);
cls.addFunc("getConverterId", &ModelConverter::getConverterId);
cls.addFunc("hasConverter", &ModelConverter::hasConverter);
cls.addFunc("convert", &ModelConverter::convert);
cls.addFunc("lastError", &ModelConverter::lastError);
cls.addFunc("lastOutput", &ModelConverter::lastOutput);
}
} // namespace eve::modelconverter