Propagate parts-list rendering failures with lcResult

A null framebuffer readback was treated as empty geometry, so a parts
list could retain a blank entry or silently omit it after rendering failed.
The QImage return type also conflated an empty inventory with an error.

Return lcResult<QImage> and abort on failed thumbnail readback after
framebuffer and renderer cleanup. Report framebuffer creation and image
allocation failures explicitly while preserving successful empty inventory
and unavailable-geometry behavior. HTML export propagates the error text;
the instructions UI clears its stale image and displays a warning parented
to the scene's view.

Validation: full debug build and whitespace checks passed. A temporary
OpenGL fault-injection harness covered first and later readback failures
with both missing-parts policies, framebuffer creation failure, successful
empty results, and restoration of caller context, surface, and framebuffer
bindings. The warning dialog was not exercised interactively.
This commit is contained in:
Leonardo Zide committed 2026-10-05 14:18:57 -07:00
1 parent a152d3442c
commit cd6cf53c7b
4 files changed
+48 -14

No files matched your search

+13 -2
View File
@@ -65,8 +65,19 @@ void lcInstructionsPartsListItem::Update()
QFont Font = mInstructions->GetFontProperty(lcInstructionsPropertyType::PLIFont, mModel, mStep);
QColor TextColor = mInstructions->GetColorProperty(lcInstructionsPropertyType::PLITextColor, mModel, mStep);
QImage PartsImage = mModel->GetPartsListImage(300, mStep, lcRGBAFromQColor(BackgroundColor), Font, TextColor, false);
setPixmap(QPixmap::fromImage(PartsImage));
const lcResult<QImage> PartsImage = mModel->GetPartsListImage(300, mStep, lcRGBAFromQColor(BackgroundColor), Font, TextColor, false);
if (!PartsImage)
{
setPixmap(QPixmap());
QWidget* Parent = scene() && !scene()->views().isEmpty() ? scene()->views().front() : nullptr;
QMessageBox::warning(Parent, QObject::tr("LeoCAD"), PartsImage.error());
return;
}
setPixmap(QPixmap::fromImage(PartsImage.value()));
}
lcInstructionsPageWidget::lcInstructionsPageWidget(QWidget* Parent, lcInstructions* Instructions, lcInstructionsPropertiesWidget* PropertiesWidget)
+29 -7
View File
@@ -1764,7 +1764,7 @@ lcResult<QImage> lcModel::RenderStepImageWithReadyAssets(bool Zoom, int Width, i
return Image;
}
QImage lcModel::GetPartsListImage(int MaxWidth, lcStep Step, quint32 BackgroundColor, QFont Font, QColor TextColor, bool KeepMissingParts) const
lcResult<QImage> lcModel::GetPartsListImage(int MaxWidth, lcStep Step, quint32 BackgroundColor, QFont Font, QColor TextColor, bool KeepMissingParts) const
{
lcScopedGLContextState GLState;
@@ -1812,6 +1812,10 @@ QImage lcModel::GetPartsListImage(int MaxWidth, lcStep Step, quint32 BackgroundC
if (Images.empty())
{
QImage Image(1, 1, QImage::Format_ARGB32);
if (Image.isNull())
return lcUnexpected(tr("Could not allocate the pieces list image."));
Image.fill(lcQColorFromRGBA(BackgroundColor));
return Image;
}
@@ -1835,12 +1839,7 @@ QImage lcModel::GetPartsListImage(int MaxWidth, lcStep Step, quint32 BackgroundC
View.SetSize(ThumbnailSize, ThumbnailSize);
if (!View.BeginRenderToImage(ThumbnailSize, ThumbnailSize))
{
if (gMainWindow)
QMessageBox::warning(gMainWindow, tr("LeoCAD"), tr("Error creating images."));
return QImage();
}
return lcUnexpected(tr("Could not create the pieces list framebuffer."));
float OrthoSize = 200.0f;
@@ -1879,6 +1878,8 @@ QImage lcModel::GetPartsListImage(int MaxWidth, lcStep Step, quint32 BackgroundC
Context->SetDefaultState();
Context->SetProjectionMatrix(ProjectionMatrix);
bool RenderFailed = false;
for (lcPartsListImage& Image : Images)
{
View.BindRenderFramebuffer();
@@ -1904,11 +1905,20 @@ QImage lcModel::GetPartsListImage(int MaxWidth, lcStep Step, quint32 BackgroundC
View.UnbindRenderFramebuffer();
Image.Thumbnail = View.GetRenderFramebufferImage().convertToFormat(QImage::Format_ARGB32);
if (Image.Thumbnail.isNull())
{
RenderFailed = true;
break;
}
}
View.EndRenderToImage();
Context->ClearResources();
if (RenderFailed)
return lcUnexpected(tr("Could not read the pieces list image."));
auto CalculateImageBounds = [](lcPartsListImage& Image)
{
const QImage& Thumbnail = Image.Thumbnail;
@@ -1956,11 +1966,19 @@ QImage lcModel::GetPartsListImage(int MaxWidth, lcStep Step, quint32 BackgroundC
if (Images.empty())
{
QImage Image(1, 1, QImage::Format_ARGB32);
if (Image.isNull())
return lcUnexpected(tr("Could not allocate the pieces list image."));
Image.fill(lcQColorFromRGBA(BackgroundColor));
return Image;
}
QImage DummyImage(16, 16, QImage::Format_ARGB32);
if (DummyImage.isNull())
return lcUnexpected(tr("Could not allocate the pieces list image."));
QPainter DummyPainter(&DummyImage);
DummyPainter.setFont(Font);
@@ -2010,6 +2028,10 @@ QImage lcModel::GetPartsListImage(int MaxWidth, lcStep Step, quint32 BackgroundC
}
QImage PainterImage(ImageWidth + 40, CurrentHeight + 40, QImage::Format_ARGB32);
if (PainterImage.isNull())
return lcUnexpected(tr("Could not allocate the pieces list image."));
PainterImage.fill(lcQColorFromRGBA(BackgroundColor));
QPainter Painter(&PainterImage);
+2 -1
View File
@@ -332,7 +332,8 @@ public:
void GetScene(lcScene* Scene, const lcCamera* ViewCamera, bool AllowHighlight, bool AllowFade) const;
void AddSubModelRenderMeshes(lcScene* Scene, const lcMatrix44& WorldMatrix, int DefaultColorIndex, lcRenderMeshState RenderMeshState, bool ParentActive) const;
QImage GetStepImage(bool Zoom, int Width, int Height, lcStep Step);
QImage GetPartsListImage(int MaxWidth, lcStep Step, quint32 BackgroundColor, QFont Font, QColor TextColor, bool KeepMissingParts) const;
// Empty inventory succeeds with a null image; rendering failures return an error.
lcResult<QImage> GetPartsListImage(int MaxWidth, lcStep Step, quint32 BackgroundColor, QFont Font, QColor TextColor, bool KeepMissingParts) const;
lcResult<void> SaveStepImages(const QString& BaseName, bool AddStepSuffix, bool Zoom, int Width, int Height, lcStep Start, lcStep End);
void RayTest(lcObjectRayTest& ObjectRayTest) const;
+4 -4
View File
@@ -2257,16 +2257,16 @@ lcResult<void> Project::ExportHTML(const lcHTMLExportOptions& Options)
if (PartsList.empty())
return lcResult<void>();
QImage Image = Model->GetPartsListImage(1024, Step, LC_RGBA(255, 255, 255, 0), QFont("Arial", 16, QFont::Bold), Qt::black, true);
const lcResult<QImage> Image = Model->GetPartsListImage(1024, Step, LC_RGBA(255, 255, 255, 0), QFont("Arial", 16, QFont::Bold), Qt::black, true);
if (Image.isNull())
return lcUnexpected(tr("Could not render the pieces list image."));
if (!Image)
return lcUnexpected(Image.error());
QString ImageName = BaseName + QLatin1String("-parts.png");
QString FileName = QFileInfo(Dir, ImageName).absoluteFilePath();
QImageWriter Writer(FileName);
if (!Writer.write(Image))
if (!Writer.write(Image.value()))
return lcUnexpected(tr("Error writing to file '%1':\n%2").arg(FileName, Writer.errorString()));
Stream << QString::fromLatin1("<p><IMG SRC=\"%1\" /></p><br><br>\r\n").arg(ImageName.toHtmlEscaped());