Require complete geometry for image and 3D exports

Report missing generated meshes and failed image writes, keep thumbnails and interactive geometry using their fallback behavior, and frame command-line images at the requested step or full range. Use explicit mesh loader flags and reject incomplete synthesized meshes. Keep selection and ray tests aligned with the rendered mesh.
This commit is contained in:
Leonardo Zide committed 2026-09-28 11:39:49 -07:00
1 parent 50d4e15c53
commit bd6bd50ecc
19 files changed
+363 -131

No files matched your search

+16 -3
View File
@@ -1148,15 +1148,28 @@ lcStartupMode lcApplication::Initialize(const QList<QPair<QString, bool>>& Libra
return lcStartupMode::Error;
}
if (Options.CameraName.isEmpty() && !Options.SetCameraPosition)
ActiveView->ZoomExtents();
const bool UsesCameraPosition = Options.SetCameraPosition && Options.Viewpoint == lcViewpoint::Count && !Options.SetCameraAngles;
if (Options.CameraName.isEmpty() && !UsesCameraPosition)
{
const lcStep FitStep = Options.ImageStart == Options.ImageEnd ? Options.ImageStart : ActiveModel->GetLastStep();
ActiveModel->ZoomExtentsAtStep(ActiveView->GetCamera(), (float)ActiveView->GetWidth() / (float)ActiveView->GetHeight(), FitStep);
}
auto ProgressCallback = [&StdOut](const QString& FileName)
{
StdOut << tr("Saved '%1'.\n").arg(FileName);
};
ActiveView->SaveStepImages(Frame, Options.ImageStart != Options.ImageEnd, Options.ImageStart, Options.ImageEnd, ProgressCallback);
const lcResult<void> ImagesSaved = ActiveView->SaveStepImages(Frame, Options.ImageStart != Options.ImageEnd, Options.ImageStart, Options.ImageEnd, ProgressCallback);
if (!ImagesSaved)
{
StdErr << ImagesSaved.error() << '\n';
return lcStartupMode::Error;
}
}
if (Options.SaveWavefront)
+3
View File
@@ -3060,6 +3060,9 @@ void lcBlenderPreferences::SetModelSize(bool Update)
NativeImage& Image = Images.back();
Image.RenderedImage = Model->GetStepImage(false, ImageWidth, ImageHeight, Model->GetCurrentStep());
if (Image.RenderedImage.isNull())
return;
auto CalculateImageBounds = [](NativeImage& Image)
{
QImage& RenderedImage = Image.RenderedImage;
+2
View File
@@ -25,6 +25,8 @@ void lcInstructionsStepImageItem::Update()
if (!Images.empty())
setPixmap(QPixmap::fromImage(Images.front()));
else
setPixmap(QPixmap());
}
lcInstructionsStepNumberItem::lcInstructionsStepNumberItem(QGraphicsItem* Parent, lcInstructions* Instructions, lcModel* Model, lcStep Step)
+5 -5
View File
@@ -401,7 +401,7 @@ PieceInfo* lcPiecesLibrary::FindPiece(const char* PieceName, Project* CurrentPro
if (ProjectFile.isFile())
{
Project* NewProject = new Project(CurrentProject && CurrentProject->IsPreview());
NewProject->SetStreamMeshes(CurrentProject && CurrentProject->UsesStreamingMeshes());
NewProject->SetDeferModelMeshRequests(CurrentProject && CurrentProject->DefersModelMeshRequests());
if (NewProject->Load(ProjectFile.absoluteFilePath(), false))
{
@@ -454,7 +454,7 @@ bool lcPiecesLibrary::RemapProjectPiece(PieceInfo* Info, const QString& ProjectD
if (Candidate.isFile())
{
Project* LocalProject = new Project(IsPreview);
LocalProject->SetStreamMeshes(true);
LocalProject->SetDeferModelMeshRequests(true);
if (LocalProject->Load(Candidate.absoluteFilePath(), false))
Info->CreateProject(LocalProject, PieceName.constData());
@@ -1771,7 +1771,7 @@ lcPartBuildResult lcPiecesLibrary::BuildPieceData(const lcPartSourceSnapshot& So
PieceFile.Seek(0, SEEK_SET);
lcMeshLoader MeshLoader(*Result.MeshData, true, nullptr, false);
lcMeshLoader MeshLoader(*Result.MeshData, nullptr, lcMeshLoaderFlag::Optimize);
if (!MeshLoader.LoadMesh(PieceFile, LC_MESHDATA_SHARED))
{
@@ -1812,7 +1812,7 @@ lcPartBuildResult lcPiecesLibrary::BuildPieceData(const lcPartSourceSnapshot& So
Result.MeshData.reset(new lcLibraryMeshData);
lcMeshLoader MeshLoader(*Result.MeshData, true, nullptr, false);
lcMeshLoader MeshLoader(*Result.MeshData, nullptr, lcMeshLoaderFlag::Optimize);
bool Loaded = false;
@@ -2337,7 +2337,7 @@ bool lcPiecesLibrary::LoadPrimitive(lcLibraryPrimitive* Primitive)
return false;
};
lcMeshLoader MeshLoader(Primitive->mMeshData, true, nullptr, false);
lcMeshLoader MeshLoader(Primitive->mMeshData, nullptr, lcMeshLoaderFlag::Optimize | lcMeshLoaderFlag::RequireAllIncludes);
if (mZipFiles[static_cast<int>(lcZipFileType::Official)])
{
+15 -20
View File
@@ -990,7 +990,6 @@ void lcMainWindow::UpdateAssets()
if (mStatusProject != Project)
{
mStatusProject = Project;
mHadPendingAssets = false;
mStatusRequiredAssets.clear();
mStatusLoadProgress->reset();
mStatusLoadProgress->setFormat(QString());
@@ -1016,7 +1015,6 @@ void lcMainWindow::UpdateAssets()
if (Required.empty())
{
mHadPendingAssets = false;
mStatusLoadProgress->reset();
mStatusLoadProgress->hide();
return;
@@ -1044,33 +1042,21 @@ void lcMainWindow::UpdateAssets()
if (Failed)
{
mHadPendingAssets = false;
mStatusLoadProgress->setFormat(tr("%n failed (%v/%m)", nullptr, Failed));
mStatusLoadProgress->setToolTip(FailureDetails);
}
else if (Pending)
{
mHadPendingAssets = true;
mStatusLoadProgress->setFormat(tr("Loading %v/%m"));
}
else if (mHadPendingAssets)
{
mHadPendingAssets = false;
mStatusLoadProgress->setFormat(tr("All pieces loaded"));
QTimer::singleShot(2500, this, [this, Project]()
{
if (mStatusProject == Project && !mHadPendingAssets && mStatusLoadProgress->format() == tr("All pieces loaded"))
{
mStatusLoadProgress->setFormat(QString());
mStatusLoadProgress->hide();
}
});
}
else
{
mStatusLoadProgress->setFormat(QString());
mStatusLoadProgress->hide();
return;
}
if (Failed || Pending || mStatusLoadProgress->format() == tr("All pieces loaded"))
mStatusLoadProgress->show();
mStatusLoadProgress->show();
}
void lcMainWindow::closeEvent(QCloseEvent* Event)
@@ -1389,6 +1375,12 @@ void lcMainWindow::Print(QPrinter* Printer)
lcStep Step = PageLayout.Steps[0].Step;
QImage Image = Model->GetStepImage(false, StepWidth, StepHeight, Step);
if (Image.isNull())
{
Printer->abort();
return;
}
Painter.drawImage(MarginRect.left(), MarginRect.top(), Image);
// TODO: add print options somewhere but Qt doesn't allow changes to the page setup dialog
@@ -1532,7 +1524,10 @@ void lcMainWindow::ShowImageDialog()
lcSetProfileInt(LC_PROFILE_IMAGE_WIDTH, Options.Width);
lcSetProfileInt(LC_PROFILE_IMAGE_HEIGHT, Options.Height);
lcGetActiveProject()->SaveImage(Options);
const lcResult<void> Saved = lcGetActiveProject()->SaveImage(Options);
if (!Saved)
QMessageBox::warning(this, tr("LeoCAD"), Saved.error());
}
void lcMainWindow::ShowSelectDialog()
-1
View File
@@ -380,7 +380,6 @@ protected:
const Project* mStatusProject = nullptr;
std::unordered_set<const PieceInfo*> mStatusRequiredAssets;
bool mAssetUpdateScheduled = false;
bool mHadPendingAssets = false;
QMenu* mTransformMenu = nullptr;
QMenu* mToolsMenu = nullptr;
+21 -10
View File
@@ -1052,7 +1052,7 @@ bool lcLibraryMeshData::WriteSections(lcMesh* Mesh, const std::vector<lcMeshLoad
if (TextureLookup)
DstSection.Texture = TextureLookup(FinalSection.Name);
else if (mMeshLoader)
DstSection.Texture = lcGetPiecesLibrary()->FindTexture(FinalSection.Name, mMeshLoader->mCurrentProject, mMeshLoader->mSearchProjectFolder);
DstSection.Texture = lcGetPiecesLibrary()->FindTexture(FinalSection.Name, mMeshLoader->mCurrentProject, mMeshLoader->SearchProjectFolder());
else
DstSection.Texture = lcGetPiecesLibrary()->FindTexture(FinalSection.Name, nullptr, false);
}
@@ -1226,8 +1226,8 @@ void lcLibraryMeshData::UpdateMeshSectionBoundingBox(const lcMesh* Mesh, const l
}
}
lcMeshLoader::lcMeshLoader(lcLibraryMeshData& MeshData, bool Optimize, Project* CurrentProject, bool SearchProjectFolder)
: mCurrentProject(CurrentProject), mSearchProjectFolder(SearchProjectFolder), mMeshData(MeshData), mOptimize(Optimize)
lcMeshLoader::lcMeshLoader(lcLibraryMeshData& MeshData, Project* CurrentProject, lcMeshLoaderFlags Flags)
: mCurrentProject(CurrentProject), mMeshData(MeshData), mFlags(Flags)
{
MeshData.SetMeshLoader(this);
}
@@ -1247,6 +1247,7 @@ bool lcMeshLoader::ReadMeshData(lcFile& File, const lcMatrix44& CurrentTransform
bool InvertNext = false;
bool WindingCCW = !InvertWinding;
lcPiecesLibrary* Library = lcGetPiecesLibrary();
const bool Optimize = mFlags.testFlag(lcMeshLoaderFlag::Optimize);
while (File.ReadLine(Buffer, sizeof(Buffer)))
{
@@ -1539,18 +1540,28 @@ bool lcMeshLoader::ReadMeshData(lcFile& File, const lcMatrix44& CurrentTransform
mMeshData.AddMeshDataNoDuplicateCheck(Primitive->mMeshData, IncludeTransform, ColorCode, Mirror ^ InvertNext, InvertNext, TextureMap, MeshDataType);
else if (!Primitive->mSubFile)
{
if (mOptimize)
if (Optimize)
mMeshData.AddMeshData(Primitive->mMeshData, IncludeTransform, ColorCode, Mirror ^ InvertNext, InvertNext, TextureMap, MeshDataType);
else
mMeshData.AddMeshDataNoDuplicateCheck(Primitive->mMeshData, IncludeTransform, ColorCode, Mirror ^ InvertNext, InvertNext, TextureMap, MeshDataType);
}
else
{
if (mFlags.testFlag(lcMeshLoaderFlag::RequireAllIncludes))
IncludeLoaded = false;
Library->GetPrimitiveFile(Primitive, FileCallback);
}
mMeshData.mHasStyleStud |= Primitive->mStudStyle | Primitive->mMeshData.mHasStyleStud;
}
else
{
if (mFlags.testFlag(lcMeshLoaderFlag::RequireAllIncludes))
IncludeLoaded = false;
Library->GetPieceFile(FileName, FileCallback);
}
if (!IncludeLoaded)
return false;
@@ -1563,7 +1574,7 @@ bool lcMeshLoader::ReadMeshData(lcFile& File, const lcMatrix44& CurrentTransform
Points[0] = lcMul31(Points[0], CurrentTransform);
Points[1] = lcMul31(Points[1], CurrentTransform);
mMeshData.mData[MeshDataType].ProcessLine(LineType, mMeshData.GetMaterial(ColorCode), WindingCCW, Points, mOptimize);
mMeshData.mData[MeshDataType].ProcessLine(LineType, mMeshData.GetMaterial(ColorCode), WindingCCW, Points, Optimize);
break;
case 3:
@@ -1576,11 +1587,11 @@ bool lcMeshLoader::ReadMeshData(lcFile& File, const lcMatrix44& CurrentTransform
Points[2] = lcMul31(Points[2], CurrentTransform);
if (!TextureMap)
mMeshData.mData[MeshDataType].ProcessLine(LineType, mMeshData.GetMaterial(ColorCode), WindingCCW, Points, mOptimize);
mMeshData.mData[MeshDataType].ProcessLine(LineType, mMeshData.GetMaterial(ColorCode), WindingCCW, Points, Optimize);
else
{
mMeshData.mHasTextures = true;
mMeshData.mData[MeshDataType].ProcessLine(LineType, mMeshData.GetTexturedMaterial(ColorCode, *TextureMap), WindingCCW, Points, mOptimize);
mMeshData.mData[MeshDataType].ProcessLine(LineType, mMeshData.GetTexturedMaterial(ColorCode, *TextureMap), WindingCCW, Points, Optimize);
}
break;
@@ -1596,11 +1607,11 @@ bool lcMeshLoader::ReadMeshData(lcFile& File, const lcMatrix44& CurrentTransform
Points[3] = lcMul31(Points[3], CurrentTransform);
if (!TextureMap)
mMeshData.mData[MeshDataType].ProcessLine(LineType, mMeshData.GetMaterial(ColorCode), WindingCCW, Points, mOptimize);
mMeshData.mData[MeshDataType].ProcessLine(LineType, mMeshData.GetMaterial(ColorCode), WindingCCW, Points, Optimize);
else
{
mMeshData.mHasTextures = true;
mMeshData.mData[MeshDataType].ProcessLine(LineType, mMeshData.GetTexturedMaterial(ColorCode, *TextureMap), WindingCCW, Points, mOptimize);
mMeshData.mData[MeshDataType].ProcessLine(LineType, mMeshData.GetTexturedMaterial(ColorCode, *TextureMap), WindingCCW, Points, Optimize);
}
break;
@@ -1615,7 +1626,7 @@ bool lcMeshLoader::ReadMeshData(lcFile& File, const lcMatrix44& CurrentTransform
Points[2] = lcMul31(Points[2], CurrentTransform);
Points[3] = lcMul31(Points[3], CurrentTransform);
mMeshData.mData[MeshDataType].ProcessLine(LineType, mMeshData.GetMaterial(ColorCode), WindingCCW, Points, mOptimize);
mMeshData.mData[MeshDataType].ProcessLine(LineType, mMeshData.GetMaterial(ColorCode), WindingCCW, Points, Optimize);
break;
}
+14 -3
View File
@@ -201,15 +201,26 @@ protected:
static void UpdateMeshSectionBoundingBox(const lcMesh* Mesh, const lcMeshSection& Section, lcVector3& SectionMin, lcVector3& SectionMax);
};
enum class lcMeshLoaderFlag
{
None = 0,
Optimize = 1 << 0,
SearchProjectFolder = 1 << 1,
RequireAllIncludes = 1 << 2
};
Q_DECLARE_FLAGS(lcMeshLoaderFlags, lcMeshLoaderFlag)
Q_DECLARE_OPERATORS_FOR_FLAGS(lcMeshLoaderFlags)
class lcMeshLoader
{
public:
lcMeshLoader(lcLibraryMeshData& MeshData, bool Optimize, Project* CurrentProject, bool SearchProjectFolder);
lcMeshLoader(lcLibraryMeshData& MeshData, Project* CurrentProject, lcMeshLoaderFlags Flags);
bool LoadMesh(lcFile& File, lcMeshDataType MeshDataType);
bool SearchProjectFolder() const { return mFlags.testFlag(lcMeshLoaderFlag::SearchProjectFolder); }
Project* mCurrentProject;
bool mSearchProjectFolder;
protected:
bool ReadMeshData(lcFile& File, const lcMatrix44& CurrentTransform, quint32 CurrentColorCode, bool InvertWinding, lcMeshDataType MeshDataType);
@@ -217,5 +228,5 @@ protected:
std::vector<lcMeshLoaderTextureMap> mTextureStack;
lcLibraryMeshData& mMeshData;
bool mOptimize;
lcMeshLoaderFlags mFlags;
};
+79 -31
View File
@@ -388,9 +388,9 @@ void lcModel::CreatePieceInfo(Project* Project)
}), mPieces.end());
}
// A streamed external model retains its initial CreateModelPiece reference
// A model with deferred mesh requests retains its initial CreateModelPiece reference
// until its source lines have been parsed and QueueMeshBuild() runs.
if (!Project->UsesStreamingMeshes())
if (!Project->DefersModelMeshRequests())
{
Library->LoadPieceInfo(mPieceInfo, mIsPreview ? lcPieceLoadFlag::Visible : lcPieceLoadFlag::Wait | lcPieceLoadFlag::Visible);
Library->ReleasePieceInfo(mPieceInfo);
@@ -400,7 +400,7 @@ void lcModel::CreatePieceInfo(Project* Project)
{
UpdateAllViews();
if (!Project->UsesStreamingMeshes())
if (!Project->DefersModelMeshRequests())
RefreshPreview();
}
}
@@ -1657,25 +1657,51 @@ QImage lcModel::GetStepImage(bool Zoom, int Width, int Height, lcStep Step)
return QImage();
}
const lcView* ActiveView = gMainWindow->GetActiveView();
const lcStep CurrentStep = mCurrentStep;
lcCamera* Camera = ActiveView->GetCamera();
const lcResult<QImage> Image = RenderStepImageWithReadyAssets(Zoom, Width, Height, Step, nullptr);
lcView View(lcViewType::View, this);
View.SetCamera(Camera, true);
View.SetOffscreenContext();
View.MakeCurrent();
if (!View.BeginRenderToImage(Width, Height))
if (!Image)
{
QMessageBox::warning(gMainWindow, tr("LeoCAD"), tr("Error creating images."));
if (gMainWindow)
QMessageBox::warning(gMainWindow, tr("LeoCAD"), Image.error());
return QImage();
}
return Image.value();
}
lcResult<QImage> lcModel::RenderStepImageWithReadyAssets(bool Zoom, int Width, int Height, lcStep Step, lcCamera* OutputCamera)
{
const lcView* ActiveView = gMainWindow ? gMainWindow->GetActiveView() : nullptr;
const lcStep CurrentStep = mCurrentStep;
lcView View(lcViewType::View, this);
if (OutputCamera)
View.SetCamera(OutputCamera, true);
else if (ActiveView)
View.SetCamera(ActiveView->GetCamera(), true);
View.SetOffscreenContext();
View.MakeCurrent();
if (!OutputCamera && !ActiveView)
ZoomExtentsAtStep(View.GetCamera(), (float)Width / (float)Height, GetLastStep());
SetTemporaryStep(Step);
if (Zoom)
ZoomExtents(Camera, (float)Width / (float)Height, lcMatrix44Identity());
if (Zoom && ActiveView && !OutputCamera)
ZoomExtents(View.GetCamera(), (float)Width / (float)Height, lcMatrix44Identity());
if (!View.BeginRenderToImage(Width, Height))
{
SetTemporaryStep(CurrentStep);
if (!mActive)
CalculateStep(LC_STEP_MAX);
return lcUnexpected(tr("Error creating images."));
}
View.OnDraw();
@@ -1693,8 +1719,8 @@ QImage lcModel::GetStepImage(bool Zoom, int Width, int Height, lcStep Step)
if (!mActive)
CalculateStep(LC_STEP_MAX);
if (MissingAssets && gMainWindow)
QMessageBox::warning(gMainWindow, tr("LeoCAD"), tr("Could not render all required pieces."));
if (MissingAssets || Image.isNull())
return lcUnexpected(tr("Could not render all required pieces."));
return Image;
}
@@ -1765,7 +1791,9 @@ QImage lcModel::GetPartsListImage(int MaxWidth, lcStep Step, quint32 BackgroundC
if (!View.BeginRenderToImage(ThumbnailSize, ThumbnailSize))
{
QMessageBox::warning(gMainWindow, tr("LeoCAD"), tr("Error creating images."));
if (gMainWindow)
QMessageBox::warning(gMainWindow, tr("LeoCAD"), tr("Error creating images."));
return QImage();
}
@@ -1946,16 +1974,21 @@ QImage lcModel::GetPartsListImage(int MaxWidth, lcStep Step, quint32 BackgroundC
return PainterImage;
}
void lcModel::SaveStepImages(const QString& BaseName, bool AddStepSuffix, bool Zoom, int Width, int Height, lcStep Start, lcStep End)
lcResult<void> lcModel::SaveStepImages(const QString& BaseName, bool AddStepSuffix, bool Zoom, int Width, int Height, lcStep Start, lcStep End)
{
const lcResult<void> Ready = EnsureAssetsReady();
if (!Ready)
{
if (gMainWindow)
QMessageBox::warning(gMainWindow, tr("LeoCAD"), Ready.error());
return Ready;
return;
lcCamera OutputCamera(true);
lcCamera* Camera = nullptr;
if (!gMainWindow || !gMainWindow->GetActiveView())
{
OutputCamera.SetViewpoint(lcViewpoint::Home);
ZoomExtentsAtStep(&OutputCamera, (float)Width / (float)Height, GetLastStep());
Camera = &OutputCamera;
}
for (lcStep Step = Start; Step <= End; Step++)
@@ -1967,18 +2000,21 @@ void lcModel::SaveStepImages(const QString& BaseName, bool AddStepSuffix, bool Z
else
FileName = BaseName;
const lcResult<QImage> Image = RenderStepImageWithReadyAssets(Zoom, Width, Height, Step, Camera);
if (!Image)
return lcUnexpected(Image.error());
QImageWriter Writer(FileName);
if (Writer.format().isEmpty())
Writer.setFormat("png");
QImage Image = GetStepImage(Zoom, Width, Height, Step);
if (!Writer.write(Image))
{
QMessageBox::information(gMainWindow, tr("Error"), tr("Error writing to file '%1':\n%2").arg(FileName, Writer.errorString()));
break;
}
if (!Writer.write(Image.value()))
return lcUnexpected(tr("Error writing to file '%1':\n%2").arg(FileName, Writer.errorString()));
}
return lcResult<void>();
}
void lcModel::RayTest(lcObjectRayTest& ObjectRayTest) const
@@ -2026,7 +2062,7 @@ bool lcModel::SubModelMinIntersectDist(const lcVector3& WorldStart, const lcVect
if (Piece->IsVisibleInSubModel())
{
if (Piece->mPieceInfo->MinIntersectDist(Start, End, MinDistance, PieceInfoRayTest)) // todo: this should check for piece->mMesh first
if (Piece->MinIntersectDist(Start, End, MinDistance, PieceInfoRayTest))
{
MinIntersect = true;
PieceInfoRayTest.Transform = lcMul(PieceInfoRayTest.Transform, Piece->mModelWorld);
@@ -2040,7 +2076,7 @@ bool lcModel::SubModelMinIntersectDist(const lcVector3& WorldStart, const lcVect
bool lcModel::SubModelBoxTest(const lcVector4 Planes[6]) const
{
for (const std::unique_ptr<lcPiece>& Piece : mPieces)
if (Piece->IsVisibleInSubModel() && Piece->mPieceInfo->BoxTest(Piece->mModelWorld, Planes))
if (Piece->IsVisibleInSubModel() && Piece->IntersectsPlanes(Piece->mModelWorld, Planes))
return true;
return false;
@@ -5964,6 +6000,18 @@ void lcModel::ZoomExtents(lcCamera* Camera, float Aspect, const lcMatrix44& Worl
}
}
void lcModel::ZoomExtentsAtStep(lcCamera* Camera, float Aspect, lcStep Step)
{
const lcStep CurrentStep = mCurrentStep;
SetTemporaryStep(Step);
ZoomExtents(Camera, Aspect, lcMatrix44Identity());
SetTemporaryStep(CurrentStep);
if (!mActive)
CalculateStep(LC_STEP_MAX);
}
void lcModel::Zoom(lcCamera* Camera, float Amount)
{
if (!Camera->IsSimple())
+4 -1
View File
@@ -331,7 +331,7 @@ public:
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) const;
void SaveStepImages(const QString& BaseName, bool AddStepSuffix, bool Zoom, int Width, int Height, lcStep Start, lcStep End);
lcResult<void> SaveStepImages(const QString& BaseName, bool AddStepSuffix, bool Zoom, int Width, int Height, lcStep Start, lcStep End);
void RayTest(lcObjectRayTest& ObjectRayTest) const;
void BoxTest(lcObjectBoxTest& ObjectBoxTest) const;
@@ -423,6 +423,7 @@ public:
void LookAt(lcCamera* Camera);
void MoveCamera(lcCamera* Camera, const lcVector3& Direction);
void ZoomExtents(lcCamera* Camera, float Aspect, const lcMatrix44& WorldMatrix);
void ZoomExtentsAtStep(lcCamera* Camera, float Aspect, lcStep Step);
void Zoom(lcCamera* Camera, float Amount);
void MoveSelectedObjects(const lcVector3& Distance, lcModelTransformFlags Flags, lcModelHistoryEditMerge ModelHistoryEditMerge)
@@ -452,6 +453,8 @@ public:
void UpdateInterface();
protected:
lcResult<QImage> RenderStepImageWithReadyAssets(bool Zoom, int Width, int Height, lcStep Step, lcCamera* OutputCamera);
void DeleteModel();
void AddSelectionHistory(std::function<void()> Callback);
+11
View File
@@ -68,6 +68,16 @@ public:
return mRequireCompleteAssets;
}
void SetRequireGeneratedMeshes(bool RequireGeneratedMeshes)
{
mRequireGeneratedMeshes = RequireGeneratedMeshes;
}
bool GetRequireGeneratedMeshes() const
{
return mRequireGeneratedMeshes;
}
void MarkMissingAssets()
{
mMissingAssets = true;
@@ -136,6 +146,7 @@ protected:
lcShadingMode mShadingMode;
bool mDrawInterface = false;
bool mRequireCompleteAssets = false;
bool mRequireGeneratedMeshes = false;
bool mMissingAssets = false;
bool mDrawInsertPreview = false;
bool mAllowLOD = true;
+2 -2
View File
@@ -1526,8 +1526,8 @@ lcMesh* lcSynthInfo::CreateMesh(const std::vector<lcPieceControlPoint>& ControlP
File.WriteU8(0);
File.Seek(0, SEEK_SET);
lcMeshLoader MeshLoader(MeshData, false, nullptr, false);
if (MeshLoader.LoadMesh(File, LC_MESHDATA_SHARED))
lcMeshLoader MeshLoader(MeshData, nullptr, lcMeshLoaderFlag::RequireAllIncludes);
if (MeshLoader.LoadMesh(File, LC_MESHDATA_SHARED) && !MeshData.IsEmpty())
return MeshData.CreateMesh();
return nullptr;
+8
View File
@@ -256,6 +256,7 @@ void lcThumbnailManager::DrawThumbnail(lcPartThumbnailId ThumbnailId, lcPartThum
if (!mView->BeginRenderToImage(Width, Height))
{
mView.reset();
DrawFailedThumbnail(ThumbnailId, Thumbnail);
return;
}
}
@@ -293,6 +294,13 @@ void lcThumbnailManager::DrawThumbnail(lcPartThumbnailId ThumbnailId, lcPartThum
}
QImage Image = mView->GetRenderImage().convertToFormat(QImage::Format_ARGB32);
if (Image.isNull())
{
DrawFailedThumbnail(ThumbnailId, Thumbnail);
return;
}
const char* IconName = nullptr;
if (Info->GetSynthInfo())
+19 -8
View File
@@ -734,7 +734,9 @@ std::vector<QImage> lcView::GetStepImages(lcStep Start, lcStep End)
if (!BeginRenderToImage(mWidth, mHeight))
{
QMessageBox::warning(gMainWindow, tr("LeoCAD"), tr("Error creating images."));
if (gMainWindow)
QMessageBox::warning(gMainWindow, tr("LeoCAD"), tr("Error creating images."));
return Images;
}
@@ -754,7 +756,16 @@ std::vector<QImage> lcView::GetStepImages(lcStep Start, lcStep End)
break;
}
Images.emplace_back(GetRenderImage());
QImage Image = GetRenderImage();
if (Image.isNull())
{
MissingAssets = true;
Images.clear();
break;
}
Images.emplace_back(std::move(Image));
}
EndRenderToImage();
@@ -770,12 +781,12 @@ std::vector<QImage> lcView::GetStepImages(lcStep Start, lcStep End)
return Images;
}
void lcView::SaveStepImages(const QString& BaseName, bool AddStepSuffix, lcStep Start, lcStep End, std::function<void(const QString&)> ProgressCallback)
lcResult<void> lcView::SaveStepImages(const QString& BaseName, bool AddStepSuffix, lcStep Start, lcStep End, std::function<void(const QString&)> ProgressCallback)
{
std::vector<QImage> Images = GetStepImages(Start, End);
if (Images.size() != static_cast<size_t>(End - Start + 1))
return;
return lcUnexpected(tr("Could not render all required pieces."));
for (lcStep Step = Start; Step <= End; Step++)
{
@@ -792,14 +803,13 @@ void lcView::SaveStepImages(const QString& BaseName, bool AddStepSuffix, lcStep
Writer.setFormat("png");
if (!Writer.write(Images[Step - Start]))
{
QMessageBox::information(gMainWindow, tr("Error"), tr("Error writing to file '%1':\n%2").arg(FileName, Writer.errorString()));
break;
}
return lcUnexpected(tr("Error writing to file '%1':\n%2").arg(FileName, Writer.errorString()));
if (ProgressCallback)
ProgressCallback(FileName);
}
return lcResult<void>();
}
bool lcView::BeginRenderToImage(int Width, int Height)
@@ -896,6 +906,7 @@ void lcView::OnDraw()
mScene->SetShadingMode(ShadingMode);
mScene->SetAllowLOD(Preferences.mAllowLOD && mWidget != nullptr);
mScene->SetRequireCompleteAssets(static_cast<bool>(mRenderFramebuffer));
mScene->SetRequireGeneratedMeshes(mRenderFramebuffer && mViewType != lcViewType::PartsList);
mScene->SetLODDistance(Preferences.mMeshLODDistance);
mScene->Begin(mCamera->mWorldView);
+2 -1
View File
@@ -3,6 +3,7 @@
#include "lc_context.h"
#include "lc_math.h"
#include "lc_commands.h"
#include "lc_result.h"
struct lcInsertPieceInfo;
enum class lcCameraProjection;
@@ -304,7 +305,7 @@ public:
void UnbindRenderFramebuffer();
QImage GetRenderFramebufferImage() const;
std::vector<QImage> GetStepImages(lcStep Start, lcStep End);
void SaveStepImages(const QString& BaseName, bool AddStepSuffix, lcStep Start, lcStep End, std::function<void(const QString&)> ProgressCallback);
lcResult<void> SaveStepImages(const QString& BaseName, bool AddStepSuffix, lcStep Start, lcStep End, std::function<void(const QString&)> ProgressCallback);
lcContext* mContext = nullptr;
+70 -17
View File
@@ -480,23 +480,11 @@ void lcPiece::RayTest(lcObjectRayTest& ObjectRayTest) const
const lcVector3 Start = lcMul31(ObjectRayTest.Start, InverseWorldMatrix);
const lcVector3 End = lcMul31(ObjectRayTest.End, InverseWorldMatrix);
if (mMesh && !mPieceInfo->IsLoading())
if (MinIntersectDist(Start, End, ObjectRayTest.Distance, ObjectRayTest.PieceInfoRayTest))
{
if (mMesh->MinIntersectDist(Start, End, ObjectRayTest.Distance, ObjectRayTest.PieceInfoRayTest.Plane))
{
ObjectRayTest.ObjectSection.Object = const_cast<lcPiece*>(this);
ObjectRayTest.ObjectSection.Section = LC_PIECE_SECTION_POSITION;
ObjectRayTest.PieceInfoRayTest.Transform = mModelWorld;
}
}
else
{
if (mPieceInfo->MinIntersectDist(Start, End, ObjectRayTest.Distance, ObjectRayTest.PieceInfoRayTest))
{
ObjectRayTest.ObjectSection.Object = const_cast<lcPiece*>(this);
ObjectRayTest.ObjectSection.Section = LC_PIECE_SECTION_POSITION;
ObjectRayTest.PieceInfoRayTest.Transform = lcMul(ObjectRayTest.PieceInfoRayTest.Transform, mModelWorld);
}
ObjectRayTest.ObjectSection.Object = const_cast<lcPiece*>(this);
ObjectRayTest.ObjectSection.Section = LC_PIECE_SECTION_POSITION;
ObjectRayTest.PieceInfoRayTest.Transform = lcMul(ObjectRayTest.PieceInfoRayTest.Transform, mModelWorld);
}
if (mPieceInfo->GetSynthInfo() && AreControlPointsVisible())
@@ -524,12 +512,67 @@ void lcPiece::RayTest(lcObjectRayTest& ObjectRayTest) const
}
}
bool lcPiece::MinIntersectDist(const lcVector3& Start, const lcVector3& End, float& MinDistance, lcPieceInfoRayTest& PieceInfoRayTest) const
{
if (!mMesh || mPieceInfo->IsLoading())
return mPieceInfo->MinIntersectDist(Start, End, MinDistance, PieceInfoRayTest);
if (!mMesh->MinIntersectDist(Start, End, MinDistance, PieceInfoRayTest.Plane))
return false;
PieceInfoRayTest.Info = mPieceInfo;
PieceInfoRayTest.Transform = lcMatrix44Identity();
return true;
}
void lcPiece::BoxTest(lcObjectBoxTest& ObjectBoxTest) const
{
if (mPieceInfo->BoxTest(mModelWorld, ObjectBoxTest.Planes))
if (IntersectsPlanes(mModelWorld, ObjectBoxTest.Planes))
ObjectBoxTest.Objects.emplace_back(const_cast<lcPiece*>(this));
}
bool lcPiece::IntersectsPlanes(const lcMatrix44& WorldMatrix, const lcVector4 Planes[6]) const
{
if (!mMesh || mPieceInfo->IsLoading())
return mPieceInfo->BoxTest(WorldMatrix, Planes);
const lcMatrix44 InverseWorldMatrix = lcMatrix44AffineInverse(WorldMatrix);
lcVector4 LocalPlanes[6];
for (int PlaneIdx = 0; PlaneIdx < 6; PlaneIdx++)
{
const lcVector3 PlaneNormal = lcMul30(Planes[PlaneIdx], InverseWorldMatrix);
LocalPlanes[PlaneIdx] = lcVector4(PlaneNormal, Planes[PlaneIdx][3] - lcDot3(InverseWorldMatrix[3], PlaneNormal));
}
lcVector3 Corners[8];
lcGetBoxCorners(mMesh->mBoundingBox, Corners);
int OutcodesOR = 0;
int OutcodesAND = 0x3f;
for (const lcVector3& Corner : Corners)
{
int Outcode = 0;
for (int PlaneIdx = 0; PlaneIdx < 6; PlaneIdx++)
if (lcDot3(Corner, LocalPlanes[PlaneIdx]) + LocalPlanes[PlaneIdx][3] > 0)
Outcode |= 1 << PlaneIdx;
OutcodesOR |= Outcode;
OutcodesAND &= Outcode;
}
if (OutcodesAND != 0)
return false;
if (OutcodesOR == 0)
return true;
return mMesh->IntersectsPlanes(LocalPlanes);
}
void lcPiece::DrawInterface(lcContext* Context, const lcScene& Scene) const
{
const lcPreferences& Preferences = lcGetPreferences();
@@ -1012,7 +1055,12 @@ void lcPiece::AddMainModelRenderMeshes(lcScene* Scene, bool Highlight, bool Fade
}
if (!mMesh || mPieceInfo->IsLoading())
{
if (!mMesh && mPieceInfo->GetSynthInfo() && Scene->GetRequireGeneratedMeshes())
Scene->MarkMissingAssets();
mPieceInfo->AddRenderMeshes(Scene, mModelWorld, mColorIndex, RenderMeshState, ParentActive);
}
else
Scene->AddMesh(mMesh, mModelWorld, mColorIndex, RenderMeshState);
@@ -1035,7 +1083,12 @@ void lcPiece::AddSubModelRenderMeshes(lcScene* Scene, const lcMatrix44& WorldMat
RenderMeshState = IsFocused() ? lcRenderMeshState::Focused : (IsSelected() ? lcRenderMeshState::Selected : lcRenderMeshState::Default);
if (!mMesh || mPieceInfo->IsLoading())
{
if (!mMesh && mPieceInfo->GetSynthInfo() && Scene->GetRequireGeneratedMeshes())
Scene->MarkMissingAssets();
mPieceInfo->AddRenderMeshes(Scene, lcMul(mModelWorld, WorldMatrix), ColorIndex, RenderMeshState, ActiveSubmodelInstance == this);
}
else
Scene->AddMesh(mMesh, lcMul(mModelWorld, WorldMatrix), ColorIndex, RenderMeshState);
+2
View File
@@ -85,6 +85,8 @@ public:
void RayTest(lcObjectRayTest& ObjectRayTest) const override;
void BoxTest(lcObjectBoxTest& ObjectBoxTest) const override;
bool MinIntersectDist(const lcVector3& Start, const lcVector3& End, float& MinDistance, lcPieceInfoRayTest& PieceInfoRayTest) const;
bool IntersectsPlanes(const lcMatrix44& WorldMatrix, const lcVector4 Planes[6]) const;
void DrawInterface(lcContext* Context, const lcScene& Scene) const override;
QVariant GetPropertyValue(lcObjectPropertyId PropertyId) const override;
bool SetPropertyValue(lcObjectPropertyId PropertyId, lcStep Step, bool AddKey, QVariant Value) override;
+83 -22
View File
@@ -710,7 +710,7 @@ bool Project::Load(const QString& FileName, bool ShowErrors)
}
else
{
if (mStreamMeshes)
if (mDeferModelMeshRequests)
lcGetPiecesLibrary()->InvalidatePiece(Model->GetPieceInfo());
Model->QueueMeshBuild();
@@ -1118,7 +1118,7 @@ bool Project::ImportInventory(const std::vector<lcSetInventoryItem>& SetInventor
return true;
}
std::vector<lcModelPartsEntry> Project::GetModelParts()
lcResult<std::vector<lcModelPartsEntry>> Project::GetModelParts()
{
std::vector<lcModelPartsEntry> ModelParts;
@@ -1132,7 +1132,13 @@ std::vector<lcModelPartsEntry> Project::GetModelParts()
SetActiveModel(mActiveModel, false);
return ModelParts;
for (const lcModelPartsEntry& ModelPart : ModelParts)
{
if (ModelPart.Info->GetSynthInfo() && !ModelPart.Mesh)
return lcUnexpected(tr("Could not generate geometry for piece '%1'.").arg(QString::fromLatin1(ModelPart.Info->mFileName)));
}
return std::move(ModelParts);
}
std::vector<PieceInfo*> Project::GetRequiredPieces() const
@@ -1304,7 +1310,12 @@ lcResult<void> Project::Export3DStudio(const QString& FileName)
if (!Ready)
return Ready;
std::vector<lcModelPartsEntry> ModelParts = GetModelParts();
const lcResult<std::vector<lcModelPartsEntry>> ModelPartsResult = GetModelParts();
if (!ModelPartsResult)
return lcUnexpected(ModelPartsResult.error());
const std::vector<lcModelPartsEntry>& ModelParts = ModelPartsResult.value();
if (ModelParts.empty())
return lcUnexpected(tr("Nothing to export."));
@@ -1762,7 +1773,12 @@ lcResult<void> Project::ExportCOLLADA(const QString& FileName)
if (!Ready)
return Ready;
std::vector<lcModelPartsEntry> ModelParts = GetModelParts();
const lcResult<std::vector<lcModelPartsEntry>> ModelPartsResult = GetModelParts();
if (!ModelPartsResult)
return lcUnexpected(ModelPartsResult.error());
const std::vector<lcModelPartsEntry>& ModelParts = ModelPartsResult.value();
if (ModelParts.empty())
return lcUnexpected(tr("Nothing to export."));
@@ -2104,19 +2120,33 @@ lcResult<void> Project::ExportHTML(const lcHTMLExportOptions& Options)
QString ProjectTitle = GetTitle();
auto AddPartsListImage = [&Dir](QTextStream& Stream, lcModel* Model, lcStep Step, const QString& BaseName)
auto AddPartsListImage = [&Dir](QTextStream& Stream, lcModel* Model, lcStep Step, const QString& BaseName) -> lcResult<void>
{
lcPartsList PartsList;
if (Step == 0)
Model->GetPartsList(gDefaultColor, true, false, PartsList);
else
Model->GetPartsListForStep(Step, gDefaultColor, PartsList, false);
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);
if (!Image.isNull())
{
QString ImageName = BaseName + QLatin1String("-parts.png");
QString FileName = QFileInfo(Dir, ImageName).absoluteFilePath();
if (Image.isNull())
return lcUnexpected(tr("Could not render the pieces list image."));
Image.save(FileName);
QString ImageName = BaseName + QLatin1String("-parts.png");
QString FileName = QFileInfo(Dir, ImageName).absoluteFilePath();
QImageWriter Writer(FileName);
Stream << QString::fromLatin1("<p><IMG SRC=\"%1\" /></p><br><br>\r\n").arg(ImageName);
}
if (!Writer.write(Image))
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);
return lcResult<void>();
};
for (lcModel* Model : Models)
@@ -2152,11 +2182,21 @@ lcResult<void> Project::ExportHTML(const lcHTMLExportOptions& Options)
Stream << QString::fromLatin1("<p><IMG SRC=\"%1-%2.png\" ALT=\"Step %3\" WIDTH=%4 HEIGHT=%5></p><BR><BR>\r\n").arg(BaseName, StepString, StepString, QString::number(Options.StepImagesWidth), QString::number(Options.StepImagesHeight));
if (Options.PartsListStep)
AddPartsListImage(Stream, Model, Step, QString("%1-%2").arg(BaseName, StepString));
{
const lcResult<void> PartsImageSaved = AddPartsListImage(Stream, Model, Step, QString("%1-%2").arg(BaseName, StepString));
if (!PartsImageSaved)
return PartsImageSaved;
}
}
if (Options.PartsListEnd)
AddPartsListImage(Stream, Model, 0, BaseName);
{
const lcResult<void> PartsImageSaved = AddPartsListImage(Stream, Model, 0, BaseName);
if (!PartsImageSaved)
return PartsImageSaved;
}
Stream << QLatin1String("</CENTER>\r\n<BR><HR><BR><B><I>Created by <A HREF=\"https://www.leocad.org\">LeoCAD</A></I></B><BR></HTML>\r\n");
}
@@ -2198,7 +2238,12 @@ lcResult<void> Project::ExportHTML(const lcHTMLExportOptions& Options)
Stream << QString::fromLatin1("<IMG SRC=\"%1-%2.png\" ALT=\"Step %3\" WIDTH=%4 HEIGHT=%5><BR><BR>\r\n").arg(BaseName, StepString, StepString, QString::number(Options.StepImagesWidth), QString::number(Options.StepImagesHeight));
if (Options.PartsListStep)
AddPartsListImage(Stream, Model, Step, QString("%1-%2").arg(BaseName, StepString));
{
const lcResult<void> PartsImageSaved = AddPartsListImage(Stream, Model, Step, QString("%1-%2").arg(BaseName, StepString));
if (!PartsImageSaved)
return PartsImageSaved;
}
Stream << QLatin1String("</CENTER>\r\n<BR><HR><BR>");
if (Step != 1)
@@ -2227,7 +2272,10 @@ lcResult<void> Project::ExportHTML(const lcHTMLExportOptions& Options)
Stream << QString::fromLatin1("<HTML>\r\n<HEAD>\r\n<TITLE>Pieces used by %1</TITLE>\r\n</HEAD>\r\n<BR>\r\n<CENTER>\n").arg(PageTitle);
AddPartsListImage(Stream, Model, 0, BaseName);
const lcResult<void> PartsImageSaved = AddPartsListImage(Stream, Model, 0, BaseName);
if (!PartsImageSaved)
return PartsImageSaved;
Stream << QLatin1String("</CENTER>\r\n<BR><HR><BR>");
Stream << QString::fromLatin1("<A HREF=\"%1-%2.html\">Previous</A> ").arg(BaseName, QString("%1").arg(LastStep, 2, 10, QLatin1Char('0')));
@@ -2240,7 +2288,10 @@ lcResult<void> Project::ExportHTML(const lcHTMLExportOptions& Options)
}
QString StepImageBaseName = QFileInfo(Dir, BaseName + QLatin1String("-%1.png")).absoluteFilePath();
Model->SaveStepImages(StepImageBaseName, true, false, Options.StepImagesWidth, Options.StepImagesHeight, 1, LastStep);
const lcResult<void> ImagesSaved = Model->SaveStepImages(StepImageBaseName, true, false, Options.StepImagesWidth, Options.StepImagesHeight, 1, LastStep);
if (!ImagesSaved)
return ImagesSaved;
}
if (Models.size() > 1)
@@ -2298,7 +2349,12 @@ lcResult<void> Project::ExportPOVRay(const QString& FileName)
if (!Ready)
return Ready;
std::vector<lcModelPartsEntry> ModelParts = GetModelParts();
const lcResult<std::vector<lcModelPartsEntry>> ModelPartsResult = GetModelParts();
if (!ModelPartsResult)
return lcUnexpected(ModelPartsResult.error());
const std::vector<lcModelPartsEntry>& ModelParts = ModelPartsResult.value();
if (ModelParts.empty())
return lcUnexpected(tr("Nothing to export."));
@@ -2989,7 +3045,12 @@ lcResult<void> Project::ExportWavefront(const QString& FileName)
if (!Ready)
return Ready;
std::vector<lcModelPartsEntry> ModelParts = GetModelParts();
const lcResult<std::vector<lcModelPartsEntry>> ModelPartsResult = GetModelParts();
if (!ModelPartsResult)
return lcUnexpected(ModelPartsResult.error());
const std::vector<lcModelPartsEntry>& ModelParts = ModelPartsResult.value();
if (ModelParts.empty())
return lcUnexpected(tr("Nothing to export."));
@@ -3089,7 +3150,7 @@ lcResult<void> Project::ExportWavefront(const QString& FileName)
return lcResult<void>();
}
void Project::SaveImage(const lcImageDialogOptions& Options)
lcResult<void> Project::SaveImage(const lcImageDialogOptions& Options)
{
QString FilePath = Options.FilePath;
QString Extension = QFileInfo(FilePath).suffix();
@@ -3100,7 +3161,7 @@ void Project::SaveImage(const lcImageDialogOptions& Options)
if (Options.Start != Options.End)
FilePath = FilePath.insert(FilePath.length() - Extension.length() - 1, QLatin1String("%1"));
mActiveModel->SaveStepImages(FilePath, Options.Start != Options.End, false, Options.Width, Options.Height, Options.Start, Options.End);
return mActiveModel->SaveStepImages(FilePath, Options.Start != Options.End, false, Options.Width, Options.Height, Options.Start, Options.End);
}
void Project::UpdatePieceInfo(PieceInfo* Info) const
+7 -7
View File
@@ -82,14 +82,14 @@ public:
return mIsPreview;
}
bool UsesStreamingMeshes() const
bool DefersModelMeshRequests() const
{
return mStreamMeshes;
return mDeferModelMeshRequests;
}
void SetStreamMeshes(bool StreamMeshes)
void SetDeferModelMeshRequests(bool Defer)
{
mStreamMeshes = StreamMeshes;
mDeferModelMeshRequests = Defer;
}
bool IsModified() const;
@@ -120,7 +120,7 @@ public:
bool ImportLDD(const QString& FileName);
bool ImportInventory(const std::vector<lcSetInventoryItem>& SetInventory, const QString& Name, const QString& Description);
void SaveImage(const lcImageDialogOptions& Options);
lcResult<void> SaveImage(const lcImageDialogOptions& Options);
std::vector<PieceInfo*> GetRequiredPieces() const;
lcResult<void> EnsureAssetsReady() const;
lcResult<void> ExportCurrentStep(const QString& FileName);
@@ -139,12 +139,12 @@ protected:
static bool CanShareMergePiece(const PieceInfo* Existing, const PieceInfo* Incoming);
QString GetExportFileName(const QString& FileName, const QString& DefaultExtension, const QString& DialogTitle, const QString& DialogFilter) const;
std::vector<lcModelPartsEntry> GetModelParts();
lcResult<std::vector<lcModelPartsEntry>> GetModelParts();
void SetFileName(const QString& FileName);
bool mIsPreview;
// External projects can parse their models before queuing direct meshes.
bool mStreamMeshes = false;
bool mDeferModelMeshRequests = false;
bool mModified;
QString mFileName;
QFileSystemWatcher mFileWatcher;