mirror of
https://github.com/leozide/leocad.git
synced 2026-10-27 06:20:55 +00:00
Persist enum preferences through named typed accessors using stable PascalCase tokens. Keep explicit legacy integer mappings so existing settings continue to load when enum members are reordered, and fall back to declared defaults for invalid values. Validate scalar conversions and reject non-finite floats while preserving unsigned color bits returned by signed native settings backends. Read fixed part colors as unsigned values and remove their override when unlocking so the unlocked state survives restart. Save both view-sphere enabled flags directly, preserving disabled spheres even when their remembered sizes are nonzero. Validation: macOS application build and temporary enum and persistence regression checks passed. Windows and Linux execution was not performed.
942 lines
26 KiB
C++
942 lines
26 KiB
C++
#include "lc_global.h"
|
|
#include "lc_renderdialog.h"
|
|
#include "ui_lc_renderdialog.h"
|
|
#include "project.h"
|
|
#include "lc_application.h"
|
|
#include "lc_profile.h"
|
|
#include "lc_blenderpreferences.h"
|
|
#include "lc_model.h"
|
|
#include "lc_qutils.h"
|
|
|
|
#ifndef LC_DISABLE_RENDER_DIALOG
|
|
|
|
#define LC_POVRAY_PREVIEW_WIDTH 768
|
|
#define LC_POVRAY_PREVIEW_HEIGHT 432
|
|
#if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
|
|
#define LC_POVRAY_MEMORY_MAPPED_FILE 1
|
|
#endif
|
|
|
|
void lcRenderPreviewWidget::resizeEvent(QResizeEvent* Event)
|
|
{
|
|
mScaledImage = QImage();
|
|
|
|
QWidget::resizeEvent(Event);
|
|
}
|
|
|
|
void lcRenderPreviewWidget::paintEvent(QPaintEvent* PaintEvent)
|
|
{
|
|
Q_UNUSED(PaintEvent);
|
|
|
|
QPainter Painter(this);
|
|
|
|
if (!mImage.isNull())
|
|
{
|
|
QSize Size = size();
|
|
|
|
if (mScaledImage.isNull())
|
|
mScaledImage = mImage.scaled(Size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
|
|
|
Painter.drawImage((Size.width() - mScaledImage.width()) / 2, (Size.height() - mScaledImage.height()) / 2, mScaledImage);
|
|
}
|
|
else
|
|
Painter.fillRect(rect(), Qt::transparent);
|
|
}
|
|
|
|
lcRenderDialog::lcRenderDialog(QWidget* Parent, lcRenderDialogMode RenderDialogMode)
|
|
: QDialog(Parent), mDialogMode(RenderDialogMode), ui(new Ui::lcRenderDialog)
|
|
{
|
|
ui->setupUi(this);
|
|
|
|
mWidth = lcGetProfileInt(LC_PROFILE_RENDER_WIDTH);
|
|
mHeight = lcGetProfileInt(LC_PROFILE_RENDER_HEIGHT);
|
|
mQuality = lcGetProfileRenderQuality();
|
|
mScale = 1.0f;
|
|
|
|
ui->OutputEdit->setText(lcGetActiveProject()->GetImageFileName(false));
|
|
|
|
mRenderButton = new QPushButton(tr("Render"), ui->buttonBox);
|
|
ui->buttonBox->addButton(mRenderButton, QDialogButtonBox::ActionRole);
|
|
|
|
mSettingsButton = new QPushButton(tr("Settings..."), ui->buttonBox);
|
|
ui->buttonBox->addButton(mSettingsButton, QDialogButtonBox::ResetRole);
|
|
|
|
mLogButton = new QPushButton(tr("View Log..."), ui->buttonBox);
|
|
ui->buttonBox->addButton(mLogButton, QDialogButtonBox::ResetRole);
|
|
mLogButton->setEnabled(false);
|
|
|
|
connect(mRenderButton, &QPushButton::clicked, this, &lcRenderDialog::RenderButtonClicked);
|
|
connect(mSettingsButton, &QPushButton::clicked, this, &lcRenderDialog::SettingsButtonClicked);
|
|
connect(mLogButton, &QPushButton::clicked, this, &lcRenderDialog::LogButtonClicked);
|
|
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &lcRenderDialog::reject);
|
|
connect(ui->OutputBrowseButton, &QPushButton::clicked, this, &lcRenderDialog::OutputBrowseButtonClicked);
|
|
|
|
if (mDialogMode == lcRenderDialogMode::RenderPOVRay)
|
|
{
|
|
setWindowTitle(tr("POV-Ray Image Render"));
|
|
|
|
mRenderButton->setToolTip(tr("Render Model"));
|
|
}
|
|
else
|
|
{
|
|
setWindowTitle(mDialogMode == lcRenderDialogMode::OpenInBlender ? tr("Blender LDraw Import") : tr("Blender Image Render"));
|
|
|
|
bool BlenderConfigured = !lcGetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE).isEmpty();
|
|
|
|
QStringList const& DataPathList = QStandardPaths::standardLocations(QStandardPaths::AppLocalDataLocation);
|
|
if (!QDir(QString("%1/Blender/addons/%2").arg(DataPathList.first(), LC_BLENDER_ADDON_RENDER_FOLDER)).isReadable())
|
|
{
|
|
BlenderConfigured = false;
|
|
lcSetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE, QString());
|
|
}
|
|
|
|
mRenderButton->setToolTip(BlenderConfigured ? tr("Render Model") : tr("Blender not configured. Use Settings to configure."));
|
|
mRenderButton->setEnabled(BlenderConfigured);
|
|
|
|
mSettingsButton->setToolTip(tr("Blender render settings"));
|
|
|
|
if (mDialogMode == lcRenderDialogMode::OpenInBlender)
|
|
{
|
|
mSettingsButton->setToolTip(tr("Blender import settings"));
|
|
|
|
mRenderButton->setText(tr("Open in Blender"));
|
|
|
|
if (BlenderConfigured)
|
|
mRenderButton->setToolTip(tr("Import and open LDraw model in Blender"));
|
|
|
|
ui->outputLabel->hide();
|
|
ui->OutputEdit->hide();
|
|
ui->RenderProgress->hide();
|
|
ui->OutputBrowseButton->hide();
|
|
mLogButton->hide();
|
|
|
|
adjustSize();
|
|
setMinimumWidth(ui->preview->geometry().width());
|
|
ui->preview->hide();
|
|
}
|
|
else
|
|
{
|
|
QImage Image(QPixmap(":/resources/file_render_blender_logo_1280x720.png").toImage());
|
|
Image = Image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
|
ui->preview->SetImage(Image);
|
|
}
|
|
}
|
|
|
|
connect(&mUpdateTimer, &QTimer::timeout, this, &lcRenderDialog::Update);
|
|
|
|
mUpdateTimer.start(500);
|
|
}
|
|
|
|
lcRenderDialog::~lcRenderDialog()
|
|
{
|
|
delete ui;
|
|
}
|
|
|
|
QString lcRenderDialog::GetStdOutFileName() const
|
|
{
|
|
QString LogFile = mDialogMode == lcRenderDialogMode::RenderPOVRay ? QLatin1String("leocad-povray-render.out") : QLatin1String("leocad-blender-render.out");
|
|
|
|
return QDir(QDir::tempPath()).absoluteFilePath(LogFile);
|
|
}
|
|
|
|
QString lcRenderDialog::GetStdErrFileName() const
|
|
{
|
|
QString LogFile = mDialogMode == lcRenderDialogMode::RenderPOVRay ? QLatin1String("leocad-povray-render.err") : QLatin1String("leocad-blender-render.err");
|
|
|
|
return QDir(QDir::tempPath()).absoluteFilePath(LogFile);
|
|
}
|
|
|
|
QString lcRenderDialog::GetPOVFileName() const
|
|
{
|
|
return QDir(QDir::tempPath()).absoluteFilePath("leocad-render.pov");
|
|
}
|
|
|
|
void lcRenderDialog::CloseProcess()
|
|
{
|
|
delete mProcess;
|
|
mProcess = nullptr;
|
|
|
|
if (mDialogMode == lcRenderDialogMode::RenderPOVRay)
|
|
{
|
|
#if LC_POVRAY_MEMORY_MAPPED_FILE
|
|
mOutputFile.unmap((uchar*)mOutputBuffer);
|
|
mOutputBuffer = nullptr;
|
|
mOutputFile.close();
|
|
|
|
QFile::remove(GetStdOutFileName());
|
|
#endif
|
|
|
|
QFile::remove(GetPOVFileName());
|
|
}
|
|
|
|
if (mDialogMode != lcRenderDialogMode::OpenInBlender)
|
|
mRenderButton->setText(tr("Render"));
|
|
}
|
|
|
|
bool lcRenderDialog::PromptCancel()
|
|
{
|
|
if (!mProcess)
|
|
return true;
|
|
|
|
if (QMessageBox::question(this, tr("Cancel Render"), tr("Are you sure you want to cancel the current render?"), QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
|
|
return false;
|
|
|
|
if (mProcess)
|
|
{
|
|
#ifdef Q_OS_WIN
|
|
lcTerminateChildProcess(this, mProcess->processId(), QCoreApplication::applicationPid());
|
|
#endif
|
|
mProcess->kill();
|
|
}
|
|
|
|
CloseProcess();
|
|
|
|
if (mStdOutList.size())
|
|
{
|
|
WriteStdOut();
|
|
mLogButton->setEnabled(true);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void lcRenderDialog::RenderButtonClicked()
|
|
{
|
|
if (mProcess)
|
|
{
|
|
PromptCancel();
|
|
return;
|
|
}
|
|
|
|
mLogButton->setEnabled(false);
|
|
|
|
mPreviewWidth = ui->preview->width();
|
|
mPreviewHeight = ui->preview->height();
|
|
|
|
mRenderTime.start();
|
|
|
|
if (mDialogMode == lcRenderDialogMode::RenderPOVRay)
|
|
RenderPOVRay();
|
|
else
|
|
RenderBlender();
|
|
}
|
|
|
|
void lcRenderDialog::reject()
|
|
{
|
|
if (PromptCancel())
|
|
QDialog::reject();
|
|
}
|
|
|
|
void lcRenderDialog::RenderPOVRay()
|
|
{
|
|
QString FileName = GetPOVFileName();
|
|
|
|
ui->preview->SetImage(QImage());
|
|
ui->RenderProgress->setValue(ui->RenderProgress->minimum());
|
|
ui->RenderProgress->setFormat(tr("Exporting Model"));
|
|
|
|
QApplication::setOverrideCursor(Qt::WaitCursor);
|
|
|
|
lcResult<void> ExportResult = lcGetActiveProject()->ExportPOVRay(FileName);
|
|
|
|
QGuiApplication::restoreOverrideCursor();
|
|
|
|
ui->RenderProgress->setFormat("%p%");
|
|
|
|
if (!ExportResult)
|
|
{
|
|
if (!ExportResult.error().isEmpty())
|
|
QMessageBox::information(this, tr("Render Error"), ExportResult.error());
|
|
|
|
return;
|
|
}
|
|
|
|
QStringList Arguments;
|
|
|
|
Arguments.append(QString::fromLatin1("+I\"%1\"").arg(FileName));
|
|
Arguments.append(QString::fromLatin1("+W%1").arg(QString::number(mWidth)));
|
|
Arguments.append(QString::fromLatin1("+H%1").arg(QString::number(mHeight)));
|
|
Arguments.append("-O-");
|
|
|
|
#if LC_POVRAY_MEMORY_MAPPED_FILE
|
|
Arguments.append(QString::fromLatin1("+SM\"%1\"").arg(GetStdOutFileName()));
|
|
#endif
|
|
|
|
switch (mQuality)
|
|
{
|
|
case lcPOVRayRenderQuality::High:
|
|
Arguments.append("+Q11");
|
|
Arguments.append("+R3");
|
|
Arguments.append("+A0.1");
|
|
Arguments.append("+J0.5");
|
|
break;
|
|
|
|
case lcPOVRayRenderQuality::Medium:
|
|
Arguments.append("+Q5");
|
|
Arguments.append("+A0.1");
|
|
break;
|
|
|
|
case lcPOVRayRenderQuality::Low:
|
|
break;
|
|
}
|
|
|
|
QString POVRayPath;
|
|
|
|
#ifdef Q_OS_WIN
|
|
POVRayPath = QDir::cleanPath(QCoreApplication::applicationDirPath() + QLatin1String("/povconsole32-sse2.exe"));
|
|
#endif
|
|
|
|
#ifdef Q_OS_LINUX
|
|
POVRayPath = lcGetProfileString(LC_PROFILE_POVRAY_PATH);
|
|
Arguments.append("+FN");
|
|
Arguments.append("-D");
|
|
#endif
|
|
|
|
#ifdef Q_OS_MACOS
|
|
POVRayPath = QDir::cleanPath(QCoreApplication::applicationDirPath() + QLatin1String("/povray"));
|
|
#endif
|
|
|
|
const QString POVRayDir = QFileInfo(POVRayPath).absolutePath();
|
|
const QString IncludePath = QDir::cleanPath(POVRayDir + "/include");
|
|
|
|
if (QFileInfo::exists(IncludePath))
|
|
Arguments.append(QString("+L\"%1\"").arg(IncludePath));
|
|
|
|
const QString IniPath = QDir::cleanPath(POVRayDir + "/ini");
|
|
|
|
if (QFileInfo::exists(IniPath))
|
|
Arguments.append(QString("+L\"%1\"").arg(IniPath));
|
|
|
|
if (lcGetActiveProject()->GetModels()[0]->GetPOVRayOptions().UseLGEO)
|
|
{
|
|
const QString LGEOPath = lcGetProfileString(LC_PROFILE_POVRAY_LGEO_PATH);
|
|
|
|
if (QFileInfo::exists(LGEOPath))
|
|
{
|
|
const QString LgPath = QDir::cleanPath(LGEOPath + "/lg");
|
|
if (QFileInfo::exists(LgPath))
|
|
Arguments.append(QString("+L\"%1\"").arg(LgPath));
|
|
const QString ArPath = QDir::cleanPath(LGEOPath + "/ar");
|
|
if (QFileInfo::exists(ArPath))
|
|
Arguments.append(QString("+L\"%1\"").arg(ArPath));
|
|
const QString StlPath = QDir::cleanPath(LGEOPath + "/stl");
|
|
if (QFileInfo::exists(StlPath))
|
|
Arguments.append(QString("+L\"%1\"").arg(StlPath));
|
|
}
|
|
}
|
|
|
|
lcRenderProcess* Process = new lcRenderProcess(this);
|
|
#ifdef Q_OS_LINUX
|
|
connect(Process, &QProcess::readyReadStandardError, this, [this]() { bool Error; ReadStdErr(Error); });
|
|
#endif
|
|
QStringList POVEnv = QProcess::systemEnvironment();
|
|
POVEnv.prepend("POV_IGNORE_SYSCONF_MSG=1");
|
|
Process->setEnvironment(POVEnv);
|
|
Process->setStandardErrorFile(GetStdErrFileName());
|
|
Process->start(POVRayPath, Arguments);
|
|
|
|
mImage = QImage(mWidth, mHeight, QImage::Format_ARGB32);
|
|
mImage.fill(Qt::transparent);
|
|
ui->preview->SetImage(mImage);
|
|
|
|
if (Process->waitForStarted())
|
|
{
|
|
mProcess = Process;
|
|
mRenderButton->setText(tr("Cancel"));
|
|
ui->RenderProgress->setValue(ui->RenderProgress->minimum());
|
|
mStdErrList.clear();
|
|
}
|
|
else
|
|
{
|
|
delete Process;
|
|
QMessageBox::information(this, tr("Render Error"), tr("Error starting POV-Ray."));
|
|
CloseProcess();
|
|
}
|
|
}
|
|
|
|
void lcRenderDialog::RenderBlender()
|
|
{
|
|
const QString BlenderLDrawConfigFile = lcGetProfileString(LC_PROFILE_BLENDER_LDRAW_CONFIG_PATH);
|
|
const QString BlenderImportModule = lcGetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE);
|
|
|
|
if (!QFileInfo(BlenderLDrawConfigFile).isReadable() && !BlenderImportModule.isEmpty())
|
|
lcBlenderPreferences::SaveSettings();
|
|
|
|
ui->RenderProgress->setFormat(tr("Saving Blender Model"));
|
|
QApplication::processEvents();
|
|
|
|
mBlendProgValue = 0;
|
|
mBlendProgMax = 0;
|
|
|
|
const QStringList DataPathList = QStandardPaths::standardLocations(QStandardPaths::AppLocalDataLocation);
|
|
mDataPath = DataPathList.first();
|
|
const QString DefaultBlendFile = QString("%1/blender/config/%2").arg(mDataPath, LC_BLENDER_ADDON_BLEND_FILE);
|
|
|
|
lcModel* Model = lcGetActiveProject()->GetActiveModel();
|
|
const QString ModelFileName = QFileInfo(QDir(lcGetProfileString(LC_PROFILE_PROJECTS_PATH)), QString("%1_Step_%2.ldr").arg(QFileInfo(Model->GetProperties().mFileName).baseName()).arg(Model->GetCurrentStep())).absoluteFilePath();
|
|
|
|
lcResult<void> ExportResult = lcGetActiveProject()->ExportCurrentStep(ModelFileName);
|
|
|
|
if (!ExportResult)
|
|
{
|
|
QMessageBox::warning(this, tr("Error"), ExportResult.error());
|
|
return;
|
|
}
|
|
|
|
ui->RenderProgress->setFormat("%p%");
|
|
QApplication::processEvents();
|
|
|
|
if (!QFileInfo(ModelFileName).isReadable())
|
|
return;
|
|
|
|
bool SearchCustomDir = true;
|
|
|
|
QString Message;
|
|
QStringList Arguments;
|
|
QString PythonExpression = QString("\"import bpy; bpy.ops.render_scene.lpub3d_render_ldraw("
|
|
"'EXEC_DEFAULT', "
|
|
"resolution_width=%1, resolution_height=%2, "
|
|
"render_percentage=%3, model_file=r'%4', "
|
|
"image_file=r'%5'")
|
|
.arg(mWidth).arg(mHeight)
|
|
.arg(mScale * 100)
|
|
.arg(QDir::toNativeSeparators(ModelFileName).replace("\\","\\\\"),
|
|
QDir::toNativeSeparators(ui->OutputEdit->text()).replace("\\","\\\\"));
|
|
if (BlenderImportModule == QLatin1String("MM"))
|
|
PythonExpression.append(", use_ldraw_import_mm=True");
|
|
if (SearchCustomDir)
|
|
PythonExpression.append(", search_additional_paths=True");
|
|
|
|
if (mDialogMode == lcRenderDialogMode::OpenInBlender)
|
|
{
|
|
PythonExpression.append(", import_only=True");
|
|
|
|
Arguments << QLatin1String("--window-geometry");
|
|
Arguments << QLatin1String("200 100 1440 900");
|
|
}
|
|
else
|
|
{
|
|
Arguments << QLatin1String("--background");
|
|
}
|
|
|
|
PythonExpression.append(", cli_render=True)\"");
|
|
|
|
if (QFileInfo::exists(DefaultBlendFile))
|
|
Arguments << QDir::toNativeSeparators(DefaultBlendFile);
|
|
Arguments << QString("--python-expr");
|
|
Arguments << PythonExpression;
|
|
|
|
QString ScriptName, ScriptCommand, ShellProgram;
|
|
|
|
#ifdef Q_OS_WIN
|
|
ScriptName = QLatin1String("render_ldraw_model.bat");
|
|
#else
|
|
ScriptName = QLatin1String("render_ldraw_model.sh");
|
|
#endif
|
|
ScriptCommand = QString("\"%1\" %2").arg(lcGetProfileString(LC_PROFILE_BLENDER_PATH), Arguments.join(" "));
|
|
|
|
if (mDialogMode == lcRenderDialogMode::OpenInBlender)
|
|
ScriptCommand.append(QString(" > %1").arg(QDir::toNativeSeparators(GetStdOutFileName())));
|
|
|
|
const QLatin1String LineEnding("\r\n");
|
|
|
|
QFile ScriptFile(QString("%1/%2").arg(QDir::tempPath(), ScriptName));
|
|
if (ScriptFile.open(QIODevice::WriteOnly | QIODevice::Text))
|
|
{
|
|
QTextStream Stream(&ScriptFile);
|
|
#ifdef Q_OS_WIN
|
|
Stream << QLatin1String("@ECHO OFF &SETLOCAL") << LineEnding;
|
|
#else
|
|
Stream << QLatin1String("#!/bin/bash") << LineEnding;
|
|
#endif
|
|
Stream << ScriptCommand << LineEnding;
|
|
ScriptFile.close();
|
|
}
|
|
else
|
|
{
|
|
QMessageBox::information(this, tr("Render Error"), tr("Cannot write Blender render script file '%1':\n%2.").arg(ScriptFile.fileName(), ScriptFile.errorString()));
|
|
|
|
return;
|
|
}
|
|
|
|
QThread::sleep(2);
|
|
|
|
#ifdef Q_OS_WIN
|
|
ShellProgram = QLatin1String(LC_WINDOWS_SHELL);
|
|
#else
|
|
ShellProgram = QLatin1String(LC_UNIX_SHELL);
|
|
#endif
|
|
|
|
mProcess = new lcRenderProcess(this);
|
|
|
|
connect(mProcess, &QProcess::readyReadStandardOutput, this, &lcRenderDialog::ReadStdOut);
|
|
|
|
const QString LDrawLibPath = QFileInfo(lcGetProfileString(LC_PROFILE_PARTS_LIBRARY)).absolutePath();
|
|
QStringList SystemEnvironment = QProcess::systemEnvironment();
|
|
SystemEnvironment.prepend("LDRAW_DIRECTORY=" + LDrawLibPath);
|
|
|
|
mProcess->setEnvironment(SystemEnvironment);
|
|
|
|
mProcess->setWorkingDirectory(QDir::toNativeSeparators(QString("%1/blender").arg(mDataPath)));
|
|
|
|
mProcess->setStandardErrorFile(GetStdErrFileName());
|
|
|
|
if (mDialogMode == lcRenderDialogMode::OpenInBlender)
|
|
{
|
|
QFileInfo Info(GetStdOutFileName());
|
|
if (Info.exists())
|
|
QFile::remove(Info.absoluteFilePath());
|
|
#ifdef Q_OS_WIN
|
|
mProcess->startDetached(ShellProgram, QStringList() << "/C" << ScriptFile.fileName());
|
|
#else
|
|
mProcess->startDetached(ShellProgram, QStringList() << ScriptFile.fileName());
|
|
#endif
|
|
if (mProcess)
|
|
{
|
|
mProcess->kill();
|
|
CloseProcess();
|
|
if (mStdOutList.size())
|
|
WriteStdOut();
|
|
if (Info.exists())
|
|
{
|
|
QFile Log(Info.absoluteFilePath());
|
|
QTime Wait = QTime::currentTime().addSecs(3);
|
|
while (!Log.size() || QTime::currentTime() < Wait)
|
|
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
|
|
if (Log.size()) {
|
|
if (Log.open(QFile::ReadOnly | QFile::Text))
|
|
{
|
|
QByteArray Ba = Log.readAll();
|
|
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
|
const bool Error = QString(Ba).contains(QRegularExpression("(?:\\w)*ERROR: ", QRegularExpression::CaseInsensitiveOption));
|
|
const bool Warning = QString(Ba).contains(QRegularExpression("(?:\\w)*WARNING: ", QRegularExpression::CaseInsensitiveOption));
|
|
#else
|
|
const bool Error = QString(Ba).contains(QRegExp("(?:\\w)*ERROR: ", Qt::CaseInsensitive));
|
|
const bool Warning = QString(Ba).contains(QRegExp("(?:\\w)*WARNING: ", Qt::CaseInsensitive));
|
|
#endif
|
|
if (Error || Warning)
|
|
{
|
|
QMessageBox::Icon Icon = QMessageBox::Information;
|
|
const QString Items = Error ? tr("errors%1").arg(Warning ? tr(" and warnings") : "") : Warning ? tr("warnings") : "";
|
|
const QString Title = tr("Open in Blender output");
|
|
const QString Body = tr("Open in Blender encountered %1. See Show Details...").arg(Items);
|
|
lcBlenderPreferences::ShowMessage(this, Body, Title, QString(), QString(Ba), 0, Icon);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
close();
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ui->RenderProgress->setRange(mBlendProgValue, mBlendProgMax);
|
|
ui->RenderProgress->setValue(1);
|
|
#ifdef Q_OS_WIN
|
|
mProcess->start(ShellProgram, QStringList() << "/C" << ScriptFile.fileName());
|
|
#else
|
|
mProcess->start(ShellProgram, QStringList() << ScriptFile.fileName());
|
|
#endif
|
|
}
|
|
|
|
if (mProcess->waitForStarted())
|
|
{
|
|
mRenderButton->setText(tr("Cancel"));
|
|
ui->RenderProgress->setValue(ui->RenderProgress->minimum());
|
|
QApplication::processEvents();
|
|
}
|
|
else
|
|
{
|
|
Message = tr("Error starting Blender render process");
|
|
QMessageBox::information(this, tr("Render Error"), Message);
|
|
CloseProcess();
|
|
}
|
|
}
|
|
|
|
void lcRenderDialog::ReadStdOut()
|
|
{
|
|
if (mDialogMode == lcRenderDialogMode::RenderPOVRay)
|
|
return;
|
|
|
|
QString StdOut = QString(mProcess->readAllStandardOutput());
|
|
mStdOutList.append(StdOut);
|
|
|
|
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
|
QRegularExpression RxRenderProgress;
|
|
RxRenderProgress.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
|
|
#else
|
|
QRegExp RxRenderProgress;
|
|
RxRenderProgress.setCaseSensitivity(Qt::CaseInsensitive);
|
|
#endif
|
|
int BlenderVersionNum = QString(lcGetProfileString(LC_PROFILE_BLENDER_VERSION).mid(1, 1)).toInt();
|
|
bool BlenderVersion3OrGreater = BlenderVersionNum > 2;
|
|
|
|
if (BlenderVersion3OrGreater)
|
|
RxRenderProgress.setPattern("Sample (\\d+)\\/(\\d+)");
|
|
else
|
|
RxRenderProgress.setPattern("(\\d+)\\/(\\d+) Tiles");
|
|
|
|
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
|
QRegularExpressionMatch Match = RxRenderProgress.match(StdOut);
|
|
if (Match.hasMatch())
|
|
{
|
|
mBlendProgValue = Match.captured(1).toInt();
|
|
mBlendProgMax = Match.captured(2).toInt();
|
|
ui->RenderProgress->setMaximum(mBlendProgMax);
|
|
ui->RenderProgress->setValue(mBlendProgValue);
|
|
}
|
|
#else
|
|
if (StdOut.contains(RxRenderProgress))
|
|
{
|
|
mBlendProgValue = RxRenderProgress.cap(1).toInt();
|
|
mBlendProgMax = RxRenderProgress.cap(2).toInt();
|
|
ui->RenderProgress->setMaximum(mBlendProgMax);
|
|
ui->RenderProgress->setValue(mBlendProgValue);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
QString lcRenderDialog::ReadStdErr(bool& HasError) const
|
|
{
|
|
HasError = mDialogMode == lcRenderDialogMode::RenderBlender ? false : true;
|
|
|
|
QFile File;
|
|
QStringList ReturnLines;
|
|
|
|
File.setFileName(GetStdErrFileName());
|
|
|
|
if (!File.open(QFile::ReadOnly | QFile::Text))
|
|
{
|
|
const QString Message = tr("Failed to open log file: %1:\n%2").arg(File.fileName(), File.errorString());
|
|
return Message;
|
|
}
|
|
|
|
QTextStream In(&File);
|
|
while (!In.atEnd())
|
|
{
|
|
QString Line = In.readLine(0);
|
|
ReturnLines << Line.trimmed() + "<br>";
|
|
|
|
if (mDialogMode == lcRenderDialogMode::RenderPOVRay)
|
|
{
|
|
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
|
if (Line.contains(QRegularExpression("^POV-Ray finished$", QRegularExpression::CaseInsensitiveOption)))
|
|
HasError = false;
|
|
#else
|
|
if (Line.contains(QRegExp("^POV-Ray finished$", Qt::CaseSensitive)))
|
|
HasError = false;
|
|
#endif
|
|
}
|
|
else if (mDialogMode == lcRenderDialogMode::RenderBlender)
|
|
{
|
|
if (!HasError && !Line.isEmpty())
|
|
HasError = true;
|
|
}
|
|
}
|
|
|
|
return ReturnLines.join(" ");
|
|
}
|
|
|
|
void lcRenderDialog::WriteStdOut()
|
|
{
|
|
QFile File(GetStdOutFileName());
|
|
|
|
if (File.open(QFile::WriteOnly | QIODevice::Truncate | QFile::Text))
|
|
{
|
|
QTextStream Out(&File);
|
|
|
|
for (const QString& Line : std::as_const(mStdOutList))
|
|
Out << Line;
|
|
|
|
File.close();
|
|
|
|
mLogButton->setEnabled(true);
|
|
}
|
|
}
|
|
|
|
void lcRenderDialog::Update()
|
|
{
|
|
if (!mProcess)
|
|
return;
|
|
|
|
const bool ProcessFinished = mProcess->state() == QProcess::NotRunning;
|
|
|
|
if (mDialogMode == lcRenderDialogMode::RenderPOVRay)
|
|
{
|
|
#if LC_POVRAY_MEMORY_MAPPED_FILE
|
|
if (!mOutputBuffer)
|
|
{
|
|
mOutputFile.setFileName(GetStdOutFileName());
|
|
|
|
if (!mOutputFile.open(QFile::ReadWrite))
|
|
{
|
|
if (ProcessFinished)
|
|
{
|
|
ShowResult();
|
|
CloseProcess();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
mOutputBuffer = mOutputFile.map(0, mOutputFile.size());
|
|
|
|
if (!mOutputBuffer)
|
|
{
|
|
mOutputFile.close();
|
|
|
|
if (ProcessFinished)
|
|
{
|
|
ShowResult();
|
|
CloseProcess();
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
struct lcSharedMemoryHeader
|
|
{
|
|
quint32 Version;
|
|
quint32 Width;
|
|
quint32 Height;
|
|
quint32 PixelsWritten;
|
|
quint32 PixelsRead;
|
|
};
|
|
|
|
lcSharedMemoryHeader* Header = (lcSharedMemoryHeader*)mOutputBuffer;
|
|
|
|
if (Header->PixelsWritten == Header->PixelsRead && !ProcessFinished)
|
|
return;
|
|
|
|
int Width = Header->Width;
|
|
int Height = Header->Height;
|
|
int PixelsWritten = Header->PixelsWritten;
|
|
|
|
if (!Header->PixelsRead)
|
|
mImage = QImage(Width, Height, QImage::Format_ARGB32);
|
|
|
|
quint8* Pixels = (quint8*)(Header + 1);
|
|
|
|
for (int y = 0; y < Height; y++)
|
|
{
|
|
for (int x = 0; x < Width; x++)
|
|
{
|
|
mImage.setPixel(x, y, qRgba(Pixels[0], Pixels[1], Pixels[2], Pixels[3]));
|
|
Pixels += 4;
|
|
}
|
|
}
|
|
|
|
Header->PixelsRead = PixelsWritten;
|
|
|
|
ui->RenderProgress->setMaximum(mImage.width() * mImage.height());
|
|
ui->RenderProgress->setValue(int(Header->PixelsRead));
|
|
|
|
if (PixelsWritten == Width * Height)
|
|
ui->RenderProgress->setValue(ui->RenderProgress->maximum());
|
|
|
|
ui->preview->SetImage(mImage.scaled(mPreviewWidth, mPreviewHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
|
#endif
|
|
}
|
|
|
|
if (ProcessFinished)
|
|
{
|
|
#ifdef Q_OS_LINUX
|
|
QByteArray Output = mProcess->readAllStandardOutput();
|
|
mImage = QImage::fromData(Output);
|
|
#endif
|
|
|
|
ShowResult();
|
|
CloseProcess();
|
|
}
|
|
}
|
|
|
|
void lcRenderDialog::ShowResult()
|
|
{
|
|
bool Error;
|
|
const QString StdErrLog = ReadStdErr(Error);
|
|
|
|
if (mProcess->exitStatus() != QProcess::NormalExit || mProcess->exitCode() != 0 || Error)
|
|
{
|
|
ui->RenderProgress->setRange(0, 1);
|
|
ui->RenderProgress->setValue(0);
|
|
|
|
const QString Body = tr("An error occurred while rendering.");
|
|
lcBlenderPreferences::ShowMessage(this, Body, tr("Render Error"), QString(), StdErrLog, 0, QMessageBox::Information);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
ui->RenderProgress->setValue(ui->RenderProgress->maximum());
|
|
}
|
|
|
|
bool Success = false;
|
|
QString FileName = ui->OutputEdit->text();
|
|
|
|
if (mDialogMode == lcRenderDialogMode::RenderPOVRay)
|
|
{
|
|
ui->preview->SetImage(mImage.scaled(mPreviewWidth, mPreviewHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
|
|
|
if (!FileName.isEmpty())
|
|
{
|
|
QImageWriter Writer(FileName);
|
|
|
|
Success = Writer.write(mImage);
|
|
|
|
if (!Success)
|
|
QMessageBox::information(this, tr("Render Error"), tr("Error writing to file '%1':\n%2").arg(FileName, Writer.errorString()));
|
|
}
|
|
|
|
}
|
|
else if (mDialogMode == lcRenderDialogMode::RenderBlender)
|
|
{
|
|
Success = QFileInfo::exists(FileName);
|
|
if (Success)
|
|
{
|
|
setMinimumSize(100, 100);
|
|
QImageReader Reader(FileName);
|
|
mImage = Reader.read();
|
|
mImage = mImage.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
|
|
|
mPreviewWidth = mImage.width();
|
|
mPreviewHeight = mImage.height();
|
|
|
|
ui->preview->SetImage(mImage.scaled(mPreviewWidth, mPreviewHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
|
}
|
|
}
|
|
|
|
if (!Success)
|
|
QMessageBox::information(this, tr("Render Error"), tr("Render failed."));
|
|
|
|
WriteStdOut();
|
|
}
|
|
|
|
void lcRenderDialog::OutputBrowseButtonClicked()
|
|
{
|
|
const QString Result = QFileDialog::getSaveFileName(this, tr("Select Output File"), ui->OutputEdit->text(), tr("Supported Image Files (*.bmp *.png *.jpg);;BMP Files (*.bmp);;PNG Files (*.png);;JPEG Files (*.jpg);;All Files (*.*)"));
|
|
|
|
if (!Result.isEmpty())
|
|
ui->OutputEdit->setText(QDir::toNativeSeparators(Result));
|
|
}
|
|
|
|
void lcRenderDialog::SettingsButtonClicked()
|
|
{
|
|
if (mDialogMode == lcRenderDialogMode::RenderPOVRay)
|
|
{
|
|
QDialog Dialog(this);
|
|
|
|
Dialog.setWindowTitle(tr("POV-Ray Settings"));
|
|
|
|
QFormLayout* Layout = new QFormLayout(&Dialog);
|
|
|
|
QLineEdit* WidthEdit = new QLineEdit(&Dialog);
|
|
Layout->addRow(tr("Width:"), WidthEdit);
|
|
|
|
WidthEdit->setText(QString::number(mWidth));
|
|
WidthEdit->setValidator(new QIntValidator(16, INT_MAX, this));
|
|
|
|
QLineEdit* HeightEdit = new QLineEdit(&Dialog);
|
|
Layout->addRow(tr("Height:"), HeightEdit);
|
|
|
|
HeightEdit->setText(QString::number(mHeight));
|
|
HeightEdit->setValidator(new QIntValidator(16, INT_MAX, this));
|
|
|
|
QComboBox* QualityComboBox = new QComboBox(&Dialog);
|
|
Layout->addRow(tr("Quality:"), QualityComboBox);
|
|
|
|
QualityComboBox->addItems({ tr("Low"), tr("Medium"), tr("High") });
|
|
QualityComboBox->setCurrentIndex(static_cast<int>(mQuality));
|
|
|
|
QDialogButtonBox* ButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &Dialog);
|
|
Layout->addWidget(ButtonBox);
|
|
|
|
QObject::connect(ButtonBox, &QDialogButtonBox::rejected, &Dialog, &QDialog::reject);
|
|
QObject::connect(ButtonBox, &QDialogButtonBox::accepted, &Dialog, &QDialog::accept);
|
|
|
|
if (Dialog.exec() != QDialog::Accepted)
|
|
return;
|
|
|
|
mWidth = std::clamp(WidthEdit->text().toInt(), 16, INT_MAX);
|
|
mHeight = std::clamp(HeightEdit->text().toInt(), 16, INT_MAX);
|
|
mQuality = static_cast<lcPOVRayRenderQuality>(QualityComboBox->currentIndex());
|
|
|
|
lcSetProfileInt(LC_PROFILE_RENDER_WIDTH, mWidth);
|
|
lcSetProfileInt(LC_PROFILE_RENDER_HEIGHT, mHeight);
|
|
lcSetProfileRenderQuality(mQuality);
|
|
}
|
|
else
|
|
{
|
|
lcBlenderPreferencesDialog::GetBlenderPreferences(mWidth, mHeight, mScale, this);
|
|
|
|
if (lcGetProfileString(LC_PROFILE_BLENDER_IMPORT_MODULE).isEmpty())
|
|
mRenderButton->setToolTip(tr("Blender not configured. Use Settings to configure."));
|
|
else
|
|
mRenderButton->setEnabled(true);
|
|
}
|
|
}
|
|
|
|
void lcRenderDialog::LogButtonClicked()
|
|
{
|
|
QFileInfo FileInfo;
|
|
QString Message ;
|
|
|
|
if (mDialogMode == lcRenderDialogMode::RenderPOVRay)
|
|
{
|
|
FileInfo.setFile(GetStdErrFileName());
|
|
Message = tr("POV-Ray standard error file not found: %1.").arg(FileInfo.absoluteFilePath());
|
|
}
|
|
else
|
|
{
|
|
FileInfo.setFile(GetStdOutFileName());
|
|
Message = tr("Blender standard output file not found: %1.").arg(FileInfo.absoluteFilePath());
|
|
}
|
|
|
|
if (!FileInfo.exists())
|
|
{
|
|
QMessageBox::information(this, tr("Render Log"), Message);
|
|
return;
|
|
}
|
|
|
|
QFile File(FileInfo.absoluteFilePath());
|
|
|
|
if (!File.open(QFile::ReadOnly | QFile::Text))
|
|
{
|
|
QMessageBox::information(this, tr("Render Log"), tr("Error opening log file '%1':\n%2.").arg(FileInfo.absoluteFilePath(), File.errorString()));
|
|
return;
|
|
}
|
|
|
|
QString Text = File.readAll();
|
|
File.close();
|
|
|
|
QDialog Dialog(this);
|
|
|
|
Dialog.setWindowTitle(tr("Render Log"));
|
|
Dialog.resize(800, 600);
|
|
|
|
QVBoxLayout* Layout = new QVBoxLayout(&Dialog);
|
|
|
|
QTextBrowser* TextBrowser = new QTextBrowser(&Dialog);
|
|
Layout->addWidget(TextBrowser);
|
|
|
|
TextBrowser->setText(Text);
|
|
|
|
QDialogButtonBox* ButtonBox = new QDialogButtonBox(QDialogButtonBox::Close, Qt::Horizontal, &Dialog);
|
|
Layout->addWidget(ButtonBox);
|
|
|
|
QObject::connect(ButtonBox, &QDialogButtonBox::rejected, &Dialog, &QDialog::reject);
|
|
QObject::connect(ButtonBox, &QDialogButtonBox::accepted, &Dialog, &QDialog::accept);
|
|
|
|
Dialog.exec();
|
|
}
|
|
|
|
lcRenderProcess::~lcRenderProcess()
|
|
{
|
|
if (state() == QProcess::Running || state() == QProcess::Starting)
|
|
{
|
|
terminate();
|
|
waitForFinished();
|
|
}
|
|
}
|
|
|
|
#endif // LC_DISABLE_RENDER_DIALOG
|