Files
leocad/qt/lc_renderdialog.cpp
T
Leonardo Zide 2125868dc6 Validate localized numeric input and unify image limits
Parse dialog numbers using their validator locale so localized decimals and grouped input round trip correctly, while displaying values without digit grouping. Keep invalid input open with a validation message instead of silently saving or clamping it, and serialize Blender settings in their canonical format.

Use a 1–32768 pixel range for image dimensions across Save Image, HTML export, command-line output, POV-Ray, and Blender. Bound export step edits by the model step count, grid spacing to 10000 studs, and area-light grids to 64 samples per axis. Preserve existing show/hide step bounds and avoid a total-pixel cap.

Validation: macOS Qt debug build and numeric dialog checks across ten locales, including localized input, validation messages, image dimension boundaries, and settings round trips.
2026-10-23 12:54:55 -07:00

999 lines
31 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);
lcSetupDialogHelp(this, ui->buttonBox);
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);
mLogButton->setToolTip(tr("Show the renderer output and diagnostic messages."));
mLogButton->setWhatsThis(tr("<p><b>View Log</b></p><p>Open the renderer output to investigate a failed render or warnings. This button becomes available when a log has been captured.</p>"));
mRenderButton->setWhatsThis(tr("<p><b>Render</b></p><p>Start the render using the current settings. While rendering, this button becomes Cancel so you can stop the operation.</p>"));
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"));
setWhatsThis(tr("<p><b>POV-Ray Image Render</b></p><p>Render the current model step using POV-Ray. Choose an output image, then use Settings to set its dimensions and quality. Select Render to start.</p><p>The preview is scaled to fit the dialog. View Log shows diagnostic output when available.</p>"));
mRenderButton->setToolTip(tr("Render the current model step with POV-Ray."));
mSettingsButton->setToolTip(tr("Set the POV-Ray image dimensions and render quality."));
mSettingsButton->setWhatsThis(tr("<p><b>POV-Ray Settings</b></p><p>Choose the image width and height in pixels and the rendering quality. Higher resolution and quality can increase rendering time.</p>"));
}
else
{
setWindowTitle(mDialogMode == lcRenderDialogMode::OpenInBlender ? tr("Blender LDraw Import") : tr("Blender Image Render"));
setWhatsThis(tr("<p><b>Blender Image Render</b></p><p>Render the current model step with the Blender LDraw add-on. Configure Blender and its add-on through Settings, choose an output image, then select Render.</p><p>The preview is scaled to fit the dialog. View Log shows diagnostic output when available.</p>"));
mSettingsButton->setWhatsThis(tr("<p><b>Blender Settings</b></p><p>Configure the Blender executable, LDraw import modules, library paths, and render settings. Blender and an import module must be configured before rendering or opening the model.</p>"));
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 the current model step with Blender.") : tr("Blender not configured. Use Settings to configure."));
mRenderButton->setEnabled(BlenderConfigured);
mSettingsButton->setToolTip(tr("Configure Blender, its add-on, and render settings."));
if (mDialogMode == lcRenderDialogMode::OpenInBlender)
{
mSettingsButton->setToolTip(tr("Configure Blender and its LDraw import settings."));
mRenderButton->setText(tr("Open in Blender"));
setWhatsThis(tr("<p><b>Open in Blender</b></p><p>Export the current model step and open it in Blender for editing. Use Settings to configure Blender and its LDraw import add-on, then select Open in Blender.</p><p>This mode does not save a rendered image.</p>"));
mRenderButton->setWhatsThis(tr("<p><b>Open in Blender</b></p><p>Import the current model step into Blender for editing. Configure Blender and its LDraw import add-on through Settings if this action is unavailable.</p>"));
if (BlenderConfigured)
mRenderButton->setToolTip(tr("Import the current model step and open it 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::warning(this, windowTitle(), 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::warning(this, windowTitle(), 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, windowTitle(), 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::warning(this, windowTitle(), 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::Warning;
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::warning(this, windowTitle(), 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, windowTitle(), QString(), StdErrLog, 0, QMessageBox::Warning);
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::warning(this, windowTitle(), 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::warning(this, windowTitle(), 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"));
Dialog.setWhatsThis(tr("<p><b>POV-Ray Settings</b></p><p>Set the output image dimensions and rendering quality. Width and height are in pixels, from 1 to 32768. Larger images and higher quality can take longer to render.</p><p>Choose OK to save these settings for subsequent renders, or Cancel to keep the previous settings. This dialog does not start rendering.</p>"));
QFormLayout* Layout = new QFormLayout(&Dialog);
QLineEdit* WidthEdit = new QLineEdit(&Dialog);
Layout->addRow(tr("Width:"), WidthEdit);
WidthEdit->setValidator(new QIntValidator(1, LC_MAX_IMAGE_DIMENSION, WidthEdit));
WidthEdit->setText(lcFormatIntegerLocalized(mWidth, WidthEdit->validator()->locale()));
WidthEdit->setToolTip(tr("Set the rendered image width in pixels (1 to 32768)."));
QLineEdit* HeightEdit = new QLineEdit(&Dialog);
Layout->addRow(tr("Height:"), HeightEdit);
HeightEdit->setValidator(new QIntValidator(1, LC_MAX_IMAGE_DIMENSION, HeightEdit));
HeightEdit->setText(lcFormatIntegerLocalized(mHeight, HeightEdit->validator()->locale()));
HeightEdit->setToolTip(tr("Set the rendered image height in pixels (1 to 32768)."));
QComboBox* QualityComboBox = new QComboBox(&Dialog);
Layout->addRow(tr("Quality:"), QualityComboBox);
QualityComboBox->addItems({ tr("Low"), tr("Medium"), tr("High") });
QualityComboBox->setCurrentIndex(static_cast<int>(mQuality));
QualityComboBox->setToolTip(tr("Choose the rendering quality; higher quality can take longer."));
QualityComboBox->setWhatsThis(tr("<p><b>Rendering Quality</b></p><p>Low uses POV-Ray's default quality without additional antialiasing options. Medium uses quality level 5 with antialiasing. High uses quality level 11 with additional antialiasing settings.</p>"));
for (QWidget* Control : { static_cast<QWidget*>(WidthEdit), static_cast<QWidget*>(HeightEdit), static_cast<QWidget*>(QualityComboBox) })
{
QLabel* Label = qobject_cast<QLabel*>(Layout->labelForField(Control));
Label->setBuddy(Control);
Label->setToolTip(Control->toolTip());
if (Control->whatsThis().isEmpty())
Control->setWhatsThis(Control->toolTip());
Label->setWhatsThis(Control->whatsThis());
}
QDialogButtonBox* ButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Help, Qt::Horizontal, &Dialog);
Layout->addRow(ButtonBox);
lcSetupDialogHelp(&Dialog, ButtonBox);
QObject::connect(ButtonBox, &QDialogButtonBox::rejected, &Dialog, &QDialog::reject);
int Width = mWidth;
int Height = mHeight;
QObject::connect(ButtonBox, &QDialogButtonBox::accepted, &Dialog, [&Dialog, WidthEdit, HeightEdit, &Width, &Height]()
{
bool WidthOk, HeightOk;
const int NewWidth = WidthEdit->validator()->locale().toInt(WidthEdit->text(), &WidthOk);
const int NewHeight = HeightEdit->validator()->locale().toInt(HeightEdit->text(), &HeightOk);
if (!WidthOk || !WidthEdit->hasAcceptableInput())
{
QMessageBox::information(&Dialog, Dialog.windowTitle(), tr("Please enter a width between 1 and %1 pixels.").arg(QString::number(LC_MAX_IMAGE_DIMENSION)));
WidthEdit->setFocus();
return;
}
if (!HeightOk || !HeightEdit->hasAcceptableInput())
{
QMessageBox::information(&Dialog, Dialog.windowTitle(), tr("Please enter a height between 1 and %1 pixels.").arg(QString::number(LC_MAX_IMAGE_DIMENSION)));
HeightEdit->setFocus();
return;
}
Width = NewWidth;
Height = NewHeight;
Dialog.accept();
});
if (Dialog.exec() != QDialog::Accepted)
return;
mWidth = Width;
mHeight = Height;
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::warning(this, tr("Render Log"), Message);
return;
}
QFile File(FileInfo.absoluteFilePath());
if (!File.open(QFile::ReadOnly | QFile::Text))
{
QMessageBox::warning(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.setWhatsThis(tr("<p><b>Render Log</b></p><p>Read the renderer output to investigate warnings or a failed render. Select text and copy it with the standard keyboard shortcut when reporting a problem. Close returns to the Render dialog.</p>"));
Dialog.resize(800, 600);
QVBoxLayout* Layout = new QVBoxLayout(&Dialog);
QTextBrowser* TextBrowser = new QTextBrowser(&Dialog);
Layout->addWidget(TextBrowser);
TextBrowser->setAccessibleName(tr("Renderer Output"));
TextBrowser->setToolTip(tr("Read and copy renderer output and diagnostic messages."));
TextBrowser->setText(Text);
QDialogButtonBox* ButtonBox = new QDialogButtonBox(QDialogButtonBox::Close | QDialogButtonBox::Help, Qt::Horizontal, &Dialog);
Layout->addWidget(ButtonBox);
lcSetupDialogHelp(&Dialog, 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