Files
leocad/common/project.cpp
T
Leonardo Zide bd6bd50ecc 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.
2026-09-28 11:39:49 -07:00

3183 lines
97 KiB
C++

#include "lc_global.h"
#include "lc_math.h"
#include "lc_mesh.h"
#include "lc_previewwidget.h"
#include "piece.h"
#include "pieceinf.h"
#include "camera.h"
#include "project.h"
#include "lc_instructions.h"
#include "image.h"
#include "light.h"
#include "lc_mainwindow.h"
#include "lc_view.h"
#include "lc_library.h"
#include "lc_thumbnailmanager.h"
#include "lc_application.h"
#include "lc_profile.h"
#include "lc_file.h"
#include "lc_zipfile.h"
#include "lc_modellistdialog.h"
#include "lc_bricklink.h"
#include "lc_string.h"
#include "lc_colors.h"
lcHTMLExportOptions::lcHTMLExportOptions(const Project* Project)
{
QString FileName = Project->GetFileName();
if (!FileName.isEmpty())
PathName = QFileInfo(FileName).canonicalPath();
int ImageOptions = lcGetProfileInt(LC_PROFILE_HTML_IMAGE_OPTIONS);
int HTMLOptions = lcGetProfileInt(LC_PROFILE_HTML_OPTIONS);
TransparentImages = (ImageOptions & LC_IMAGE_TRANSPARENT) != 0;
SubModels = (HTMLOptions & (LC_HTML_SUBMODELS)) != 0;
CurrentOnly = (HTMLOptions & LC_HTML_CURRENT_ONLY) != 0;
SinglePage = (HTMLOptions & LC_HTML_SINGLEPAGE) != 0;
IndexPage = (HTMLOptions & LC_HTML_INDEX) != 0;
StepImagesWidth = lcGetProfileInt(LC_PROFILE_HTML_IMAGE_WIDTH);
StepImagesHeight = lcGetProfileInt(LC_PROFILE_HTML_IMAGE_HEIGHT);
PartsListStep = (HTMLOptions & LC_HTML_LISTSTEP) != 0;
PartsListEnd = (HTMLOptions & LC_HTML_LISTEND) != 0;
}
void lcHTMLExportOptions::SaveDefaults()
{
int HTMLOptions = 0;
if (SubModels)
HTMLOptions |= LC_HTML_SUBMODELS;
if (CurrentOnly)
HTMLOptions |= LC_HTML_CURRENT_ONLY;
if (SinglePage)
HTMLOptions |= LC_HTML_SINGLEPAGE;
if (IndexPage)
HTMLOptions |= LC_HTML_INDEX;
if (PartsListStep)
HTMLOptions |= LC_HTML_LISTSTEP;
if (PartsListEnd)
HTMLOptions |= LC_HTML_LISTEND;
lcSetProfileInt(LC_PROFILE_HTML_IMAGE_OPTIONS, TransparentImages ? LC_IMAGE_TRANSPARENT : 0);
lcSetProfileInt(LC_PROFILE_HTML_OPTIONS, HTMLOptions);
lcSetProfileInt(LC_PROFILE_HTML_IMAGE_WIDTH, StepImagesWidth);
lcSetProfileInt(LC_PROFILE_HTML_IMAGE_HEIGHT, StepImagesHeight);
}
Project::Project(bool IsPreview)
: mIsPreview(IsPreview)
{
mModified = false;
mActiveModel = new lcModel(tr(mIsPreview ? "Preview.ldr" : "New Model.ldr"), this, mIsPreview);
mActiveModel->CreatePieceInfo(this);
mActiveModel->SetSaved();
mModels.emplace_back(mActiveModel);
if (!mIsPreview && gMainWindow)
QObject::connect(&mFileWatcher, &QFileSystemWatcher::fileChanged, gMainWindow, &lcMainWindow::ProjectFileChanged);
}
Project::~Project()
{
mModels.clear();
lcGetPiecesLibrary()->ReleaseProjectPieces(this);
}
PieceInfo* Project::FindPiece(const std::string& Name) const
{
const std::map<std::string, PieceInfo*>::const_iterator It = mPieceIndex.find(Name);
return It == mPieceIndex.end() ? nullptr : It->second;
}
void Project::RegisterPiece(const std::string& Name, PieceInfo* Info)
{
mPieceIndex[Name] = Info;
}
void Project::UnregisterPiece(PieceInfo* Info)
{
for (std::map<std::string, PieceInfo*>::iterator It = mPieceIndex.begin(); It != mPieceIndex.end();)
{
if (It->second == Info)
It = mPieceIndex.erase(It);
else
It++;
}
}
void Project::ClearPieceIndex()
{
mPieceIndex.clear();
}
void Project::TransferPieceIndexTo(Project* Destination)
{
for (const std::map<std::string, PieceInfo*>::value_type& Entry : mPieceIndex)
{
if (!Destination->FindPiece(Entry.first))
Destination->RegisterPiece(Entry.first, Entry.second);
}
mPieceIndex.clear();
}
bool Project::CanShareMergePiece(const PieceInfo* Existing, const PieceInfo* Incoming)
{
if (Existing == Incoming)
return true;
if (Existing->IsPlaceholder() && Incoming->IsPlaceholder())
return true;
if (!Existing->IsProject() || !Incoming->IsProject())
return false;
const QString ExistingPath = QFileInfo(Existing->GetProject()->GetFileName()).canonicalFilePath();
const QString IncomingPath = QFileInfo(Incoming->GetProject()->GetFileName()).canonicalFilePath();
return !ExistingPath.isEmpty() && ExistingPath == IncomingPath;
}
lcModel* Project::GetModel(const QString& FileName) const
{
for (const std::unique_ptr<lcModel>& Model : mModels)
if (Model->GetProperties().mFileName == FileName)
return Model.get();
return nullptr;
}
bool Project::IsModified() const
{
if (mModified)
return true;
for (const std::unique_ptr<lcModel>& Model : mModels)
if (Model->IsModified())
return true;
return false;
}
QString Project::GetTitle() const
{
if (!mFileName.isEmpty())
return QFileInfo(mFileName).fileName();
return mModels.size() == 1 ? tr("New Model.ldr") : tr("New Model.mpd");
}
QString Project::GetImageFileName(bool AllowCurrentFolder) const
{
QString FileName = GetFileName();
if (!FileName.isEmpty())
{
QString Extension = QFileInfo(FileName).suffix();
if (!Extension.isEmpty())
FileName = FileName.left(FileName.length() - Extension.length() - 1);
}
else
{
if (AllowCurrentFolder)
FileName = QLatin1String("image");
else
{
QStringList CachePathList = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation);
FileName = CachePathList.first();
FileName = QDir(FileName).absoluteFilePath(QLatin1String("image"));
}
}
return QDir::toNativeSeparators(FileName) + lcGetProfileString(LC_PROFILE_IMAGE_EXTENSION);
}
void Project::SetActiveModel(lcModel* ActiveModel, bool UpdateInterface)
{
if (!ActiveModel)
return;
for (const std::unique_ptr<lcModel> &Model : mModels)
Model->SetActive(Model.get() == ActiveModel);
if (!mIsPreview)
{
std::vector<lcModel*> UpdatedModels;
UpdatedModels.reserve(mModels.size());
for (const std::unique_ptr<lcModel> &Model : mModels)
Model->UpdatePieceInfo(UpdatedModels);
}
mActiveModel = ActiveModel;
if (!mIsPreview && gMainWindow && UpdateInterface)
{
gMainWindow->SetCurrentModelTab(mActiveModel);
mActiveModel->UpdateInterface();
}
}
void Project::SetActiveModel(int ModelIndex, bool UpdateInterface)
{
if (ModelIndex < 0 || ModelIndex >= static_cast<int>(mModels.size()))
return;
SetActiveModel(mModels[ModelIndex].get(), UpdateInterface);
}
void Project::SetActiveModel(const QString& FileName, bool UpdateInterface)
{
for (const std::unique_ptr<lcModel>& Model : mModels)
{
if (FileName.compare(Model->GetFileName(), Qt::CaseInsensitive) == 0)
{
SetActiveModel(Model.get(), UpdateInterface);
return;
}
}
}
QString Project::GetNewModelName(QWidget* ParentWidget, const QString& DialogTitle, const QString& CurrentName, const QStringList& ExistingModels) const
{
QString Name = CurrentName;
if (Name.isEmpty())
{
const QString Prefix = tr("Submodel #");
int Max = 0;
for (int ModelIdx = 0; ModelIdx < ExistingModels.size(); ModelIdx++)
{
const QString& ExistingName = ExistingModels[ModelIdx];
if (ExistingName.startsWith(Prefix))
{
QString NumberString = ExistingName.mid(Prefix.length());
QTextStream Stream(&NumberString);
int Number;
Stream >> Number;
Max = qMax(Max, Number);
}
}
Name = Prefix + QString::number(Max + 1) + ".ldr";
}
for (;;)
{
bool Ok = false;
Name = QInputDialog::getText(ParentWidget, DialogTitle, tr("Submodel Name:"), QLineEdit::Normal, Name, &Ok);
if (!Ok)
return QString();
if (Name.isEmpty())
{
QMessageBox::information(ParentWidget, tr("Empty Name"), tr("The submodel name cannot be empty."));
continue;
}
bool ExtensionValid = false;
if (Name.length() < 5)
ExtensionValid = false;
else
{
QString Extension = Name.right(4);
if (Extension.compare(".dat", Qt::CaseInsensitive) == 0 || Extension.compare(".ldr", Qt::CaseInsensitive) == 0 || Extension.compare(".mpd", Qt::CaseInsensitive) == 0)
ExtensionValid = true;
}
if (!ExtensionValid)
Name += ".ldr";
if (ExistingModels.contains(Name, Qt::CaseInsensitive) && Name != CurrentName)
{
QMessageBox::information(ParentWidget, tr("Duplicate Submodel"), tr("A submodel named '%1' already exists, please enter a unique name.").arg(Name));
continue;
}
if (!CurrentName.isEmpty())
{
const std::string PieceName = lcPiecesLibrary::NormalizePieceName(Name.toLatin1().constData());
PieceInfo* ExistingPiece = FindPiece(PieceName);
lcModel* CurrentModel = GetModel(CurrentName);
if (ExistingPiece && (!CurrentModel || ExistingPiece != CurrentModel->GetPieceInfo()) &&
(!ExistingPiece->IsModel() || ExistingModels.contains(Name, Qt::CaseInsensitive)))
{
QMessageBox::information(ParentWidget, tr("Duplicate Submodel"), tr("A piece named '%1' already exists in this project, please enter a unique name.").arg(Name));
continue;
}
}
break;
}
return Name;
}
lcModel* Project::CreateNewModel(bool ShowModel)
{
QStringList ModelNames;
for (const std::unique_ptr<lcModel> &Model : mModels)
ModelNames.append(Model->GetProperties().mFileName);
QString Name = GetNewModelName(gMainWindow, tr("New Submodel"), QString(), ModelNames);
if (Name.isEmpty())
return nullptr;
mModified = true;
lcModel* Model = new lcModel(Name, this, false);
Model->CreatePieceInfo(this);
Model->SetSaved();
mModels.emplace_back(Model);
if (ShowModel)
{
SetActiveModel(mModels.back().get(), true);
lcView* ActiveView = gMainWindow ? gMainWindow->GetActiveView() : nullptr;
if (ActiveView)
ActiveView->GetCamera()->SetViewpoint(lcViewpoint::Home);
if (gMainWindow)
gMainWindow->UpdateTitle();
}
else
SetActiveModel(mActiveModel, true);
return Model;
}
void Project::ShowModelListDialog()
{
lcModelListDialog Dialog(gMainWindow, mModels);
if (Dialog.exec() != QDialog::Accepted)
return;
std::vector<std::unique_ptr<lcModel>> NewModels;
std::vector<lcModelListDialogEntry> Results = Dialog.GetResults();
std::vector<std::pair<PieceInfo*, PieceInfo*>> Replacements;
std::vector<lcModel*> ChangedModels;
std::vector<lcModelPieceRename> Renames;
std::set<std::string> FinalNames;
int ModelIndex = Dialog.GetActiveModelIndex();
if (ModelIndex == -1)
{
for (size_t Index = 0; Index < Results.size(); Index++)
{
if (Results[Index].ExistingModel != mActiveModel)
continue;
ModelIndex = static_cast<int>(Index);
break;
}
if (ModelIndex == -1)
ModelIndex = 0;
}
// Validate the final names before changing any model or index entry.
for (const lcModelListDialogEntry& Entry : Results)
{
const std::string Name = lcPiecesLibrary::NormalizePieceName(Entry.Name.toLatin1().constData());
if (!FinalNames.insert(Name).second)
{
QMessageBox::information(gMainWindow, tr("Duplicate Submodel"), tr("A piece named '%1' already exists in this project.").arg(Entry.Name));
return;
}
if (!Entry.ExistingModel || Entry.ExistingModel->GetFileName() == Entry.Name)
continue;
PieceInfo* Occupant = FindPiece(Name);
if (!Occupant || Occupant == Entry.ExistingModel->GetPieceInfo())
continue;
const bool Vacated = std::any_of(mModels.begin(), mModels.end(), [&Results, Occupant, &Name](const std::unique_ptr<lcModel>& OldModel)
{
if (OldModel->GetPieceInfo() != Occupant)
return false;
const std::vector<lcModelListDialogEntry>::const_iterator Result = std::find_if(Results.begin(), Results.end(), [OldModel = OldModel.get()](const lcModelListDialogEntry& CheckEntry)
{
return CheckEntry.ExistingModel == OldModel;
});
return Result == Results.end() || lcPiecesLibrary::NormalizePieceName(Result->Name.toLatin1().constData()) != Name;
});
if (!Vacated)
{
QMessageBox::information(gMainWindow, tr("Duplicate Submodel"), tr("A piece named '%1' already exists in this project.").arg(Entry.Name));
return;
}
}
// Free names being renamed or deleted before applying the new names.
for (const std::unique_ptr<lcModel>& OldModel : mModels)
{
const lcModel* Model = OldModel.get();
const std::vector<lcModelListDialogEntry>::const_iterator Kept = std::find_if(Results.begin(), Results.end(), [Model](const lcModelListDialogEntry& Entry)
{
return Entry.ExistingModel == Model;
});
if (Kept != Results.end())
{
if (Kept->Name != Model->GetFileName())
UnregisterPiece(Model->GetPieceInfo());
continue;
}
const std::string OldName = lcPiecesLibrary::NormalizePieceName(Model->GetFileName().toLatin1().constData());
for (const lcModelListDialogEntry& Entry : Results)
{
if (lcPiecesLibrary::NormalizePieceName(Entry.Name.toLatin1().constData()) != OldName)
continue;
// A new model can reuse this PieceInfo. A renamed model keeps its own
// PieceInfo, so references to the deleted model are rebound below.
lcGetPiecesLibrary()->InvalidatePiece(OldModel->GetPieceInfo());
OldModel->GetPieceInfo()->SetPlaceholder();
OldModel->GetPieceInfo()->mState = lcPieceInfoState::Loaded;
if (Entry.ExistingModel)
{
UnregisterPiece(OldModel->GetPieceInfo());
Replacements.emplace_back(OldModel->GetPieceInfo(), Entry.ExistingModel->GetPieceInfo());
}
break;
}
}
// Rename surviving models before loading any duplicates from them.
for (const lcModelListDialogEntry& Entry : Results)
{
lcModel* Model = Entry.ExistingModel;
if (!Model || Model->GetFileName() == Entry.Name)
continue;
const QString OldName = Model->GetFileName();
if (!lcGetPiecesLibrary()->RenamePiece(Model->GetPieceInfo(), Entry.Name.toLatin1().constData()))
{
QMessageBox::information(gMainWindow, tr("Duplicate Submodel"), tr("A piece named '%1' already exists in this project.").arg(Entry.Name));
return;
}
Model->SetFileName(Entry.Name);
Renames.push_back({ Model->GetPieceInfo(), OldName, Entry.Name });
mModified = true;
}
// Apply original-to-final names together so a later rename cannot rewrite
// an earlier rename's history snapshots a second time.
for (const std::unique_ptr<lcModel>& Model : mModels)
Model->RenamePieces(Renames);
for (const lcModelListDialogEntry& Entry : Results)
{
lcModel* Model = Entry.ExistingModel;
if (!Model)
{
const lcModel* Source = Entry.DuplicateSource;
if (!Source)
{
Model = new lcModel(Entry.Name, this, false);
}
else
{
Model = new lcModel(Source->GetProperties().mFileName, this, false);
QByteArray File;
QTextStream SaveStream(&File);
Source->SaveLDraw(SaveStream, false, 0);
SaveStream.flush();
QBuffer Buffer(&File);
Buffer.open(QIODevice::ReadOnly);
Model->LoadLDraw(Buffer, this);
Model->SetFileName(Entry.Name);
}
Model->CreatePieceInfo(this);
Model->SetSaved();
mModified = true;
}
NewModels.emplace_back(Model);
}
for (const std::pair<PieceInfo*, PieceInfo*>& Replacement : Replacements)
{
for (const std::unique_ptr<lcModel>& Model : NewModels)
{
if (Model->RebindPieceInfo(Replacement.first, Replacement.second) && std::find(ChangedModels.begin(), ChangedModels.end(), Model.get()) == ChangedModels.end())
ChangedModels.push_back(Model.get());
}
if (gMainWindow->GetCurrentPieceInfo() == Replacement.first)
gMainWindow->SetCurrentPieceInfo(Replacement.second);
gMainWindow->GetPreviewWidget()->RebindPieceInfo(Replacement.first, Replacement.second);
}
for (std::unique_ptr<lcModel>& Model : mModels)
{
if (std::find(NewModels.begin(), NewModels.end(), Model) != NewModels.end())
Model.release();
else
mModified = true;
}
mModels = std::move(NewModels);
gMainWindow->UpdateTitle();
SetActiveModel(ModelIndex, true);
gMainWindow->UpdateModels();
if (!Renames.empty())
gMainWindow->GetPreviewWidget()->RefreshDescription();
for (lcModel* Model : ChangedModels)
Model->RefreshPreview();
}
void Project::SetFileName(const QString& FileName)
{
if (mFileName == FileName)
return;
if (!mIsPreview && !mFileName.isEmpty())
mFileWatcher.removePath(mFileName);
if (!mIsPreview && !FileName.isEmpty())
mFileWatcher.addPath(FileName);
mFileName = FileName;
}
bool Project::Load(const QString& FileName, bool ShowErrors)
{
QWidget *parent = nullptr;
if (!mIsPreview)
parent = gMainWindow;
QFile File(FileName);
if (!File.open(QIODevice::ReadOnly))
{
if (ShowErrors)
QMessageBox::warning(parent, tr("Error"), tr("Error reading file '%1':\n%2").arg(FileName, File.errorString()));
return false;
}
mModels.clear();
lcGetPiecesLibrary()->ReleaseProjectPieces(this);
SetFileName(FileName);
QFileInfo FileInfo(FileName);
QString Extension = FileInfo.suffix().toLower();
QByteArray FileData = File.readAll();
bool LoadDAT;
if (Extension == QLatin1String("dat") || Extension == QLatin1String("ldr") || Extension == QLatin1String("mpd"))
LoadDAT = true;
else if (Extension == QLatin1String("lcd") || Extension == QLatin1String("leocad"))
LoadDAT = false;
else
LoadDAT = FileData.size() > 7 && memcmp(FileData, "LeoCAD ", 7);
if (LoadDAT)
{
QBuffer Buffer(&FileData);
Buffer.open(QIODevice::ReadOnly);
std::vector<std::pair<int, lcModel*>> Models;
while (!Buffer.atEnd())
{
lcModel* Model = new lcModel(QString(), this, mIsPreview);
int Pos = Model->SplitMPD(Buffer);
if (Models.empty() || !Model->GetFileName().isEmpty())
{
auto ModelCompare = [Model](const std::pair<int, lcModel*>& ModelIt)
{
return ModelIt.second->GetFileName().compare(Model->GetFileName(), Qt::CaseInsensitive) == 0;
};
if (std::find_if(Models.begin(), Models.end(), ModelCompare) == Models.end())
{
mModels.emplace_back(Model);
Models.emplace_back(std::make_pair(Pos, Model));
Model->CreatePieceInfo(this);
}
else
delete Model;
}
else
delete Model;
}
for (size_t ModelIdx = 0; ModelIdx < Models.size(); ModelIdx++)
{
Buffer.seek(Models[ModelIdx].first);
lcModel* Model = Models[ModelIdx].second;
Model->LoadLDraw(Buffer, this);
Model->SetSaved();
}
}
else
{
lcMemFile MemFile;
MemFile.WriteBuffer(FileData.constData(), FileData.size());
MemFile.Seek(0, SEEK_SET);
lcModel* Model = new lcModel(QString(), this, mIsPreview);
if (Model->LoadBinary(&MemFile))
{
mModels.emplace_back(Model);
Model->CreatePieceInfo(this);
Model->SetSaved();
}
else
delete Model;
}
if (mModels.empty())
{
if (ShowErrors)
QMessageBox::warning(parent, tr("Error"), tr("Error loading file '%1':\nFile format is not recognized.").arg(FileName));
return false;
}
if (mModels.size() == 1)
{
lcModel* Model = mModels.front().get();
if (Model->GetProperties().mFileName.isEmpty())
{
QString ModelName = FileInfo.fileName();
int Suffix = 2;
while (!lcGetPiecesLibrary()->RenamePiece(Model->GetPieceInfo(), ModelName.toLatin1().constData()))
{
const QString Extension = FileInfo.suffix().isEmpty() ? QString() : QLatin1Char('.') + FileInfo.suffix();
const QString NameSuffix = QLatin1Char(' ') + QString::number(Suffix++) + Extension;
const int BaseLength = LC_PIECE_NAME_LEN - 1 - NameSuffix.length();
ModelName = FileInfo.completeBaseName().left(BaseLength) + NameSuffix;
}
Model->SetFileName(ModelName);
}
}
std::vector<lcModel*> UpdatedModels;
UpdatedModels.reserve(mModels.size());
for (const std::unique_ptr<lcModel>& Model : mModels)
{
if (mIsPreview)
{
PieceInfo* Info = Model->GetPieceInfo();
Info->ReleaseMesh();
Info->mState = lcPieceInfoState::Unloaded;
}
else
{
if (mDeferModelMeshRequests)
lcGetPiecesLibrary()->InvalidatePiece(Model->GetPieceInfo());
Model->QueueMeshBuild();
}
}
if (!mIsPreview)
{
for (const std::unique_ptr<lcModel>& Model : mModels)
Model->UpdatePieceInfo(UpdatedModels);
}
mModified = false;
return true;
}
lcResult<void> Project::Save(const QString& FileName)
{
std::vector<std::pair<lcModel*, lcModelAssetSourceState>> PreviousAssetSources;
const QString SourceDirectory = mFileName.isEmpty() ? QString() : QFileInfo(mFileName).absolutePath();
const QString DestinationDirectory = QFileInfo(FileName).absolutePath();
const bool DirectoryChanged = SourceDirectory.isEmpty() ||
(SourceDirectory != DestinationDirectory && QDir(SourceDirectory).canonicalPath() != QDir(DestinationDirectory).canonicalPath());
if (DirectoryChanged && !SourceDirectory.isEmpty())
{
for (const std::unique_ptr<lcModel>& Model : mModels)
{
PreviousAssetSources.emplace_back(Model.get(), Model->GetAssetSourceState());
Model->PreserveAssetSourceDirectory(SourceDirectory, true);
}
}
const auto RestoreAssetSources = [&PreviousAssetSources]()
{
for (const std::pair<lcModel*, lcModelAssetSourceState>& Entry : PreviousAssetSources)
Entry.first->RestoreAssetSourceState(Entry.second);
};
QSaveFile File(FileName);
if (!File.open(QIODevice::WriteOnly))
{
RestoreAssetSources();
return lcUnexpected(tr("Error writing to file '%1':\n%2").arg(FileName, File.errorString()));
}
{
QTextStream Stream(&File);
const bool Serialized = Save(Stream);
Stream.flush();
if (!Serialized || Stream.status() != QTextStream::Ok)
{
File.cancelWriting();
RestoreAssetSources();
return lcUnexpected(tr("Error writing to file '%1':\n%2").arg(FileName, tr("Could not write all model data.")));
}
}
const QString PreviousFileName = mFileName;
SetFileName(QString());
if (!File.commit())
{
SetFileName(PreviousFileName);
RestoreAssetSources();
return lcUnexpected(tr("Error writing to file '%1':\n%2").arg(FileName, File.errorString()));
}
SetFileName(FileName);
if (DirectoryChanged)
{
lcPiecesLibrary* Library = lcGetPiecesLibrary();
std::vector<PieceInfo*> LocalPieces;
std::vector<PieceInfo*> ChangedPieces;
for (const std::map<std::string, PieceInfo*>::value_type& Entry : mPieceIndex)
{
PieceInfo* Info = Entry.second;
if (Info->IsModel())
continue;
Library->AddPieceReference(Info);
LocalPieces.push_back(Info);
}
for (PieceInfo* Info : LocalPieces)
if (Library->RemapProjectPiece(Info, DestinationDirectory, mIsPreview))
ChangedPieces.push_back(Info);
for (const std::unique_ptr<lcModel>& Model : mModels)
{
if (!Model->HasDirectGeometry())
continue;
Library->InvalidatePiece(Model->GetPieceInfo());
Model->QueueMeshBuild();
ChangedPieces.push_back(Model->GetPieceInfo());
}
if (!ChangedPieces.empty())
{
std::vector<lcModel*> UpdatedModels;
for (const std::unique_ptr<lcModel>& Model : mModels)
{
Model->UpdatePieceInfo(UpdatedModels);
ChangedPieces.push_back(Model->GetPieceInfo());
}
Library->GetThumbnailManager()->RefreshPieces(ChangedPieces);
lcView::UpdateProjectViews(this);
if (!mIsPreview && gMainWindow && gMainWindow->GetPreviewWidget())
gMainWindow->GetPreviewWidget()->RefreshAfterAssetChange(ChangedPieces);
}
for (PieceInfo* Info : LocalPieces)
Library->ReleasePieceInfo(Info);
}
for (const std::unique_ptr<lcModel>& Model : mModels)
Model->SetSaved();
mModified = false;
return lcResult<void>();
}
bool Project::Save(QTextStream& Stream)
{
bool MPD = mModels.size() > 1;
for (const std::unique_ptr<lcModel>& Model : mModels)
{
if (MPD)
Stream << QLatin1String("0 FILE ") << Model->GetProperties().mFileName << QLatin1String("\r\n");
Model->SaveLDraw(Stream, false, 0);
if (MPD)
Stream << QLatin1String("0 NOFILE\r\n");
}
return true;
}
lcResult<void> Project::Merge(const std::vector<Project*>& Sources)
{
lcPiecesLibrary* Library = lcGetPiecesLibrary();
std::map<std::string, PieceInfo*> NonModelPieces;
std::set<std::string> DestinationModelNames;
std::set<std::string> OriginalModelNames;
std::set<std::string> PlannedModelNames;
QString ConflictName;
struct ModelPlan
{
lcModel* Model;
QString FinalName;
};
std::vector<ModelPlan> PlannedModels;
for (const std::map<std::string, PieceInfo*>::value_type& Entry : mPieceIndex)
{
if (Entry.second->IsModel())
DestinationModelNames.insert(Entry.first);
else
NonModelPieces.emplace(Entry.first, Entry.second);
}
for (const std::unique_ptr<lcModel>& Model : mModels)
{
DestinationModelNames.insert(lcPiecesLibrary::NormalizePieceName(Model->GetFileName().toLatin1().constData()));
for (const std::unique_ptr<lcPiece>& Piece : Model->GetPieces())
{
if (Piece->mPieceInfo && !Piece->mPieceInfo->IsModel())
NonModelPieces.emplace(lcPiecesLibrary::NormalizePieceName(Piece->GetID().toLatin1().constData()), Piece->mPieceInfo);
}
}
const auto RegisterSourcePiece = [&NonModelPieces, &DestinationModelNames, &ConflictName](const std::string& Name, PieceInfo* Info)
{
if (!Info || Info->IsModel())
return true;
if (DestinationModelNames.find(Name) != DestinationModelNames.end())
{
ConflictName = QString::fromStdString(Name);
return false;
}
const std::map<std::string, PieceInfo*>::const_iterator Existing = NonModelPieces.find(Name);
if (Existing != NonModelPieces.end() && !CanShareMergePiece(Existing->second, Info))
{
ConflictName = QString::fromStdString(Name);
return false;
}
NonModelPieces.emplace(Name, Info);
return true;
};
// Validate the identities behind every piece name before changing any project.
for (Project* Source : Sources)
{
for (const std::map<std::string, PieceInfo*>::value_type& Entry : Source->mPieceIndex)
{
if (!RegisterSourcePiece(Entry.first, Entry.second))
return lcUnexpected(tr("Both projects contain different pieces named '%1'. Rename one before merging.").arg(ConflictName));
}
for (const std::unique_ptr<lcModel>& Model : Source->mModels)
{
OriginalModelNames.insert(lcPiecesLibrary::NormalizePieceName(Model->GetFileName().toLatin1().constData()));
for (const std::unique_ptr<lcPiece>& Piece : Model->GetPieces())
{
const std::string Name = lcPiecesLibrary::NormalizePieceName(Piece->GetID().toLatin1().constData());
if (!RegisterSourcePiece(Name, Piece->mPieceInfo))
return lcUnexpected(tr("Both projects contain different pieces named '%1'. Rename one before merging.").arg(ConflictName));
}
}
}
// Pick final model names against the complete set of incoming piece names.
for (Project* Source : Sources)
{
for (const std::unique_ptr<lcModel>& Model : Source->mModels)
{
const QString OriginalName = Model->GetFileName();
QString FinalName = OriginalName;
int RenameIndex = 0;
for (;;)
{
const std::string Name = lcPiecesLibrary::NormalizePieceName(FinalName.toLatin1().constData());
const bool ReservedOriginal = FinalName != OriginalName && OriginalModelNames.find(Name) != OriginalModelNames.end();
const bool Duplicate = DestinationModelNames.find(Name) != DestinationModelNames.end() || NonModelPieces.find(Name) != NonModelPieces.end() ||
PlannedModelNames.find(Name) != PlannedModelNames.end() || ReservedOriginal ||
Library->FindPiece(FinalName.toLatin1().constData(), nullptr, false, false) != nullptr;
if (!Duplicate)
{
PlannedModelNames.insert(Name);
PlannedModels.push_back({ Model.get(), FinalName });
break;
}
RenameIndex++;
const QString Prefix = RenameIndex == 1 ? tr("Merged ") : tr("Merged %1 ").arg(RenameIndex);
FinalName = QString::fromLatin1((Prefix + OriginalName).toLatin1().left(LC_PIECE_NAME_LEN - 1));
}
}
}
// Reserve all planned names before moving models, so a failed reservation
// can be rolled back without leaving either project partly merged.
std::vector<const ModelPlan*> AppliedRenames;
for (const ModelPlan& Planned : PlannedModels)
{
if (Planned.FinalName == Planned.Model->GetFileName())
continue;
if (!Library->RenamePiece(Planned.Model->GetPieceInfo(), Planned.FinalName.toLatin1().constData()))
{
for (std::vector<const ModelPlan*>::reverse_iterator It = AppliedRenames.rbegin(); It != AppliedRenames.rend(); It++)
Library->RenamePiece((*It)->Model->GetPieceInfo(), (*It)->Model->GetFileName().toLatin1().constData());
return lcUnexpected(tr("Could not reserve submodel name '%1'.").arg(Planned.FinalName));
}
AppliedRenames.push_back(&Planned);
}
size_t PlanIndex = 0;
for (Project* Source : Sources)
{
std::vector<lcModelPieceRename> Renames;
std::vector<lcModel*> ImportedModels;
for (const std::unique_ptr<lcModel>& Model : Source->mModels)
{
for (const std::unique_ptr<lcPiece>& Piece : Model->GetPieces())
{
PieceInfo* Incoming = Piece->mPieceInfo;
if (!Incoming || Incoming->IsModel())
continue;
const std::string Name = lcPiecesLibrary::NormalizePieceName(Piece->GetID().toLatin1().constData());
PieceInfo* Existing = FindPiece(Name);
if (!Existing || Existing == Incoming || !CanShareMergePiece(Existing, Incoming))
continue;
const QString PieceId = Piece->GetID();
Piece->SetPieceInfo(Existing, PieceId, false, false);
Library->ReleasePieceInfo(Incoming);
}
}
for (std::unique_ptr<lcModel>& Model : Source->mModels)
{
const ModelPlan& Planned = PlannedModels[PlanIndex++];
if (Planned.FinalName != Model->GetFileName())
{
Renames.push_back({ Model->GetPieceInfo(), Model->GetFileName(), Planned.FinalName });
Model->SetFileName(Planned.FinalName);
}
Model->SetProject(this);
ImportedModels.push_back(Model.get());
mModels.emplace_back(std::move(Model));
}
// Only incoming histories used these original names for the incoming models.
for (lcModel* Model : ImportedModels)
Model->RenamePieces(Renames);
Library->TransferProjectPieces(Source, this);
Source->mModels.clear();
}
if (!Sources.empty())
mModified = true;
return lcResult<void>();
}
bool Project::ImportLDD(const QString& FileName)
{
lcZipFile ZipFile;
if (!ZipFile.OpenRead(FileName))
return false;
lcMemFile XMLFile;
if (!ZipFile.ExtractFile("IMAGE100.LXFML", XMLFile))
return false;
QString ModelName = QFileInfo(FileName).completeBaseName();
std::unique_ptr<lcModel> NewModel(new lcModel(ModelName, this, false));
if (!NewModel->LoadLDD(QString::fromUtf8((const char*)XMLFile.mBuffer)))
return false;
NewModel->SetSaved();
mModels.clear();
mModels.emplace_back(std::move(NewModel));
for (const std::unique_ptr<lcModel>& Model : mModels)
Model->CreatePieceInfo(this);
std::vector<lcModel*> UpdatedModels;
UpdatedModels.reserve(mModels.size());
for (const std::unique_ptr<lcModel>& Model : mModels)
Model->UpdatePieceInfo(UpdatedModels);
mModified = false;
return true;
}
bool Project::ImportInventory(const std::vector<lcSetInventoryItem>& SetInventory, const QString& Name, const QString& Description)
{
if (SetInventory.empty())
return false;
std::unique_ptr<lcModel> NewModel(new lcModel(Name, this, false));
if (!NewModel->LoadInventory(SetInventory))
return false;
NewModel->SetSaved();
NewModel->SetDescription(Description);
mModels.clear();
mModels.emplace_back(std::move(NewModel));
for (const std::unique_ptr<lcModel>& Model : mModels)
Model->CreatePieceInfo(this);
std::vector<lcModel*> UpdatedModels;
UpdatedModels.reserve(mModels.size());
for (const std::unique_ptr<lcModel>& Model : mModels)
Model->UpdatePieceInfo(UpdatedModels);
mModified = false;
return true;
}
lcResult<std::vector<lcModelPartsEntry>> Project::GetModelParts()
{
std::vector<lcModelPartsEntry> ModelParts;
if (mModels.empty())
return ModelParts;
for (const std::unique_ptr<lcModel>& Model : mModels)
Model->CalculateStep(LC_STEP_MAX);
mModels[0]->GetModelParts(lcMatrix44Identity(), gDefaultColor, ModelParts);
SetActiveModel(mActiveModel, false);
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
{
std::vector<PieceInfo*> Required;
for (const std::unique_ptr<lcModel>& Model : mModels)
{
std::vector<PieceInfo*> ModelRequired = Model->GetRequiredPieces();
Required.insert(Required.end(), ModelRequired.begin(), ModelRequired.end());
}
std::sort(Required.begin(), Required.end());
Required.erase(std::unique(Required.begin(), Required.end()), Required.end());
return Required;
}
lcResult<void> Project::EnsureAssetsReady() const
{
const lcModel* Model = GetMainModel();
return Model ? Model->EnsureAssetsReady() : lcResult<void>();
}
lcResult<void> Project::ExportCurrentStep(const QString& FileName)
{
QFile File(FileName);
if (!File.open(QIODevice::WriteOnly))
return lcUnexpected(tr("Error writing to file '%1':\n%2").arg(FileName, File.errorString()));
QStringList Models;
Models.append(lcGetActiveModel()->GetProperties().mFileName);
std::function<void(const QString&)> ParseStepModel = [&](const QString& ModelName)
{
Models.append(ModelName);
for (const std::unique_ptr<lcModel>& Model : mModels)
{
if (Model->GetProperties().mFileName == ModelName)
{
lcPartsList ModelParts;
Model->GetPartsList(gDefaultColor, false, true, ModelParts);
for (const auto& PartIt : ModelParts)
{
const PieceInfo* PartInfo = PartIt.first;
if (PartInfo->IsModel())
{
ParseStepModel(PartInfo->mFileName);
}
else
continue;
}
break;
}
}
};
const lcStep CurrentStep = lcGetActiveModel()->GetCurrentStep();
bool MPD = mModels.size() > 1;
if (MPD)
{
lcPartsList StepParts;
lcGetActiveModel()->GetPartsListForStep(CurrentStep, gDefaultColor, StepParts, true);
if (!StepParts.empty())
{
for (const auto& PartIt : StepParts)
{
const PieceInfo *PartInfo = PartIt.first;
if (PartInfo->IsModel())
{
ParseStepModel(PartInfo->mFileName);
}
else
continue;
}
}
}
QTextStream Stream(&File);
for (const std::unique_ptr<lcModel>& Model : mModels)
{
if (!Models.contains(Model->GetProperties().mFileName))
continue;
const lcStep ModelStep = Model->GetCurrentStep();
if (!Model->IsActive())
Model->SetTemporaryStep(CurrentStep);
if (MPD)
Stream << QLatin1String("0 FILE ") << Model->GetProperties().mFileName << QLatin1String("\r\n");
Model->SaveLDraw(Stream, false, CurrentStep);
if (MPD)
Stream << QLatin1String("0 NOFILE\r\n");
if (!Model->IsActive())
{
Model->SetTemporaryStep(ModelStep);
Model->CalculateStep(LC_STEP_MAX);
}
}
File.close();
lcSetProfileString(LC_PROFILE_PROJECTS_PATH, QFileInfo(FileName).absolutePath());
return lcResult<void>();
}
lcResult<void> Project::ExportModel(const QString& FileName, lcModel* Model) const
{
QFile File(FileName);
if (!File.open(QIODevice::WriteOnly))
return lcUnexpected(tr("Error writing to file '%1':\n%2").arg(FileName, File.errorString()));
QTextStream Stream(&File);
Model->SaveLDraw(Stream, false, 0);
return lcResult<void>();
}
QString Project::GetExportFileName(const QString& FileName, const QString& DefaultExtension, const QString& DialogTitle, const QString& DialogFilter) const
{
if (!FileName.isEmpty())
return FileName;
QString SaveFileName;
if (!mFileName.isEmpty())
SaveFileName = mFileName;
else
SaveFileName = GetTitle();
QString Extension = QFileInfo(SaveFileName).suffix().toLower();
if (Extension.isEmpty())
SaveFileName += "." + DefaultExtension;
else if (Extension != DefaultExtension && SaveFileName.length() > 4)
{
SaveFileName = SaveFileName.left(SaveFileName.length() - Extension.length() - 1);
SaveFileName += "." + DefaultExtension;
}
return QFileDialog::getSaveFileName(gMainWindow, DialogTitle, SaveFileName, DialogFilter);
}
lcResult<void> Project::Export3DStudio(const QString& FileName)
{
const lcResult<void> Ready = EnsureAssetsReady();
if (!Ready)
return Ready;
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."));
QString SaveFileName = GetExportFileName(FileName, "3ds", tr("Export 3D Studio"), tr("3DS Files (*.3ds);;All Files (*.*)"));
if (SaveFileName.isEmpty())
return lcUnexpected(QString());
lcDiskFile File(SaveFileName);
if (!File.Open(QIODevice::WriteOnly))
return lcUnexpected(tr("Could not open file '%1' for writing.").arg(SaveFileName));
long M3DStart = File.GetPosition();
File.WriteU16(0x4D4D); // CHK_M3DMAGIC
File.WriteU32(0);
File.WriteU16(0x0002); // CHK_M3D_VERSION
File.WriteU32(10);
File.WriteU32(3);
long MDataStart = File.GetPosition();
File.WriteU16(0x3D3D); // CHK_MDATA
File.WriteU32(0);
File.WriteU16(0x3D3E); // CHK_MESH_VERSION
File.WriteU32(10);
File.WriteU32(3);
const int MaterialNameLength = 11;
char MaterialName[32];
for (size_t ColorIdx = 0; ColorIdx < gColorList.size(); ColorIdx++)
{
lcColor* Color = &gColorList[ColorIdx];
snprintf(MaterialName, sizeof(MaterialName), "Material%03d", (int)ColorIdx);
long MaterialStart = File.GetPosition();
File.WriteU16(0xAFFF); // CHK_MAT_ENTRY
File.WriteU32(0);
File.WriteU16(0xA000); // CHK_MAT_NAME
File.WriteU32(6 + MaterialNameLength + 1);
File.WriteBuffer(MaterialName, MaterialNameLength + 1);
File.WriteU16(0xA010); // CHK_MAT_AMBIENT
File.WriteU32(24);
File.WriteU16(0x0011); // CHK_COLOR_24
File.WriteU32(9);
File.WriteU8(floor(255.0 * Color->Value[0] + 0.5));
File.WriteU8(floor(255.0 * Color->Value[1] + 0.5));
File.WriteU8(floor(255.0 * Color->Value[2] + 0.5));
File.WriteU16(0x0012); // CHK_LIN_COLOR_24
File.WriteU32(9);
File.WriteU8(floor(255.0 * Color->Value[0] + 0.5));
File.WriteU8(floor(255.0 * Color->Value[1] + 0.5));
File.WriteU8(floor(255.0 * Color->Value[2] + 0.5));
File.WriteU16(0xA020); // CHK_MAT_AMBIENT
File.WriteU32(24);
File.WriteU16(0x0011); // CHK_COLOR_24
File.WriteU32(9);
File.WriteU8(floor(255.0 * Color->Value[0] + 0.5));
File.WriteU8(floor(255.0 * Color->Value[1] + 0.5));
File.WriteU8(floor(255.0 * Color->Value[2] + 0.5));
File.WriteU16(0x0012); // CHK_LIN_COLOR_24
File.WriteU32(9);
File.WriteU8(floor(255.0 * Color->Value[0] + 0.5));
File.WriteU8(floor(255.0 * Color->Value[1] + 0.5));
File.WriteU8(floor(255.0 * Color->Value[2] + 0.5));
File.WriteU16(0xA030); // CHK_MAT_SPECULAR
File.WriteU32(24);
File.WriteU16(0x0011); // CHK_COLOR_24
File.WriteU32(9);
File.WriteU8(floor(255.0 * 0.9f + 0.5));
File.WriteU8(floor(255.0 * 0.9f + 0.5));
File.WriteU8(floor(255.0 * 0.9f + 0.5));
File.WriteU16(0x0012); // CHK_LIN_COLOR_24
File.WriteU32(9);
File.WriteU8(floor(255.0 * 0.9f + 0.5));
File.WriteU8(floor(255.0 * 0.9f + 0.5));
File.WriteU8(floor(255.0 * 0.9f + 0.5));
File.WriteU16(0xA040); // CHK_MAT_SHININESS
File.WriteU32(14);
File.WriteU16(0x0030); // CHK_INT_PERCENTAGE
File.WriteU32(8);
File.WriteS16((quint8)floor(100.0 * 0.25 + 0.5));
File.WriteU16(0xA041); // CHK_MAT_SHIN2PCT
File.WriteU32(14);
File.WriteU16(0x0030); // CHK_INT_PERCENTAGE
File.WriteU32(8);
File.WriteS16((quint8)floor(100.0 * 0.05 + 0.5));
File.WriteU16(0xA050); // CHK_MAT_TRANSPARENCY
File.WriteU32(14);
File.WriteU16(0x0030); // CHK_INT_PERCENTAGE
File.WriteU32(8);
File.WriteS16((quint8)floor(100.0 * (1.0f - Color->Value[3]) + 0.5));
File.WriteU16(0xA052); // CHK_MAT_XPFALL
File.WriteU32(14);
File.WriteU16(0x0030); // CHK_INT_PERCENTAGE
File.WriteU32(8);
File.WriteS16((quint8)floor(100.0 * 0.0 + 0.5));
File.WriteU16(0xA053); // CHK_MAT_REFBLUR
File.WriteU32(14);
File.WriteU16(0x0030); // CHK_INT_PERCENTAGE
File.WriteU32(8);
File.WriteS16((quint8)floor(100.0 * 0.2 + 0.5));
File.WriteU16(0xA100); // CHK_MAT_SHADING
File.WriteU32(8);
File.WriteS16(3);
File.WriteU16(0xA084); // CHK_MAT_SELF_ILPCT
File.WriteU32(14);
File.WriteU16(0x0030); // CHK_INT_PERCENTAGE
File.WriteU32(8);
File.WriteS16((quint8)floor(100.0 * 0.0 + 0.5));
File.WriteU16(0xA081); // CHK_MAT_TWO_SIDE
File.WriteU32(6);
File.WriteU16(0xA087); // CHK_MAT_WIRE_SIZE
File.WriteU32(10);
File.WriteFloat(1.0f);
long MaterialEnd = File.GetPosition();
File.Seek(MaterialStart + 2, SEEK_SET);
File.WriteU32(MaterialEnd - MaterialStart);
File.Seek(MaterialEnd, SEEK_SET);
}
const lcModelProperties& Properties = mModels[0]->GetProperties();
File.WriteU16(0x0100); // CHK_MASTER_SCALE
File.WriteU32(10);
File.WriteFloat(1.0f);
File.WriteU16(0x1400); // CHK_LO_SHADOW_BIAS
File.WriteU32(10);
File.WriteFloat(1.0f);
File.WriteU16(0x1420); // CHK_SHADOW_MAP_SIZE
File.WriteU32(8);
File.WriteS16(512);
File.WriteU16(0x1450); // CHK_SHADOW_FILTER
File.WriteU32(10);
File.WriteFloat(3.0f);
File.WriteU16(0x1460); // CHK_RAY_BIAS
File.WriteU32(10);
File.WriteFloat(1.0f);
File.WriteU16(0x1500); // CHK_O_CONSTS
File.WriteU32(18);
File.WriteFloat(0.0f);
File.WriteFloat(0.0f);
File.WriteFloat(0.0f);
File.WriteU16(0x2100); // CHK_AMBIENT_LIGHT
File.WriteU32(42);
File.WriteU16(0x0010); // CHK_COLOR_F
File.WriteU32(18);
File.WriteFloats(Properties.mAmbientColor.GetFloats(), 3);
File.WriteU16(0x0013); // CHK_LIN_COLOR_F
File.WriteU32(18);
File.WriteFloats(Properties.mAmbientColor.GetFloats(), 3);
File.WriteU16(0x1200); // CHK_SOLID_BGND
File.WriteU32(42);
File.WriteU16(0x0010); // CHK_COLOR_F
File.WriteU32(18);
const lcPreferences& Preferences = lcGetPreferences();
lcVector3 BackgroundSolidColor = lcVector3FromColor(Preferences.mBackgroundSolidColor);
File.WriteFloats(BackgroundSolidColor.GetFloats(), 3);
File.WriteU16(0x0013); // CHK_LIN_COLOR_F
File.WriteU32(18);
File.WriteFloats(BackgroundSolidColor.GetFloats(), 3);
File.WriteU16(0x1300); // CHK_V_GRADIENT
File.WriteU32(118);
File.WriteFloat(1.0f);
File.WriteU16(0x0010); // CHK_COLOR_F
File.WriteU32(18);
const lcVector3 BackgroundGradientColor1 = lcVector3FromColor(Preferences.mBackgroundGradientColorTop);
const lcVector3 BackgroundGradientColor2 = lcVector3FromColor(Preferences.mBackgroundGradientColorBottom);
File.WriteFloats(BackgroundGradientColor1.GetFloats(), 3);
File.WriteU16(0x0013); // CHK_LIN_COLOR_F
File.WriteU32(18);
File.WriteFloats(BackgroundGradientColor1.GetFloats(), 3);
File.WriteU16(0x0010); // CHK_COLOR_F
File.WriteU32(18);
File.WriteFloats(((BackgroundGradientColor1 + BackgroundGradientColor2) / 2.0f).GetFloats(), 3);
File.WriteU16(0x0013); // CHK_LIN_COLOR_F
File.WriteU32(18);
File.WriteFloats(((BackgroundGradientColor1 + BackgroundGradientColor2) / 2.0f).GetFloats(), 3);
File.WriteU16(0x0010); // CHK_COLOR_F
File.WriteU32(18);
File.WriteFloats(BackgroundGradientColor2.GetFloats(), 3);
File.WriteU16(0x0013); // CHK_LIN_COLOR_F
File.WriteU32(18);
File.WriteFloats(BackgroundGradientColor2.GetFloats(), 3);
if (Preferences.mBackgroundGradient)
{
File.WriteU16(0x1301); // LIB3DS_USE_V_GRADIENT
File.WriteU32(6);
}
else
{
File.WriteU16(0x1201); // LIB3DS_USE_SOLID_BGND
File.WriteU32(6);
}
int NumPieces = 0;
for (const lcModelPartsEntry& ModelPart : ModelParts)
{
lcMesh* Mesh = !ModelPart.Mesh ? ModelPart.Info->GetMesh() : ModelPart.Mesh;
if (!Mesh || Mesh->mIndexType == GL_UNSIGNED_INT)
continue;
long NamedObjectStart = File.GetPosition();
File.WriteU16(0x4000); // CHK_NAMED_OBJECT
File.WriteU32(0);
char Name[32];
snprintf(Name, sizeof(Name), "Piece%.3d", NumPieces);
NumPieces++;
File.WriteBuffer(Name, strlen(Name) + 1);
long TriObjectStart = File.GetPosition();
File.WriteU16(0x4100); // CHK_N_TRI_OBJECT
File.WriteU32(0);
File.WriteU16(0x4110); // CHK_POINT_ARRAY
File.WriteU32(8 + 12 * Mesh->mNumVertices);
File.WriteU16(Mesh->mNumVertices);
lcVertex* Verts = (lcVertex*)Mesh->mVertexData;
const lcMatrix44& ModelWorld = ModelPart.WorldMatrix;
for (int VertexIdx = 0; VertexIdx < Mesh->mNumVertices; VertexIdx++)
{
lcVector3 Pos = lcMul31(Verts[VertexIdx].Position, ModelWorld);
File.WriteFloat(Pos[0]);
File.WriteFloat(Pos[1]);
File.WriteFloat(Pos[2]);
}
File.WriteU16(0x4160); // CHK_MESH_MATRIX
File.WriteU32(54);
lcMatrix44 Matrix = lcMatrix44Identity();
File.WriteFloats(Matrix[0].GetFloats(), 3);
File.WriteFloats(Matrix[1].GetFloats(), 3);
File.WriteFloats(Matrix[2].GetFloats(), 3);
File.WriteFloats(Matrix[3].GetFloats(), 3);
File.WriteU16(0x4165); // CHK_MESH_COLOR
File.WriteU32(7);
File.WriteU8(0);
long FaceArrayStart = File.GetPosition();
File.WriteU16(0x4120); // CHK_FACE_ARRAY
File.WriteU32(0);
int NumTriangles = 0;
for (int SectionIdx = 0; SectionIdx < Mesh->mLods[LC_MESH_LOD_HIGH].NumSections; SectionIdx++)
{
lcMeshSection* Section = &Mesh->mLods[LC_MESH_LOD_HIGH].Sections[SectionIdx];
if (Section->PrimitiveType != LC_MESH_TRIANGLES && Section->PrimitiveType != LC_MESH_TEXTURED_TRIANGLES)
continue;
NumTriangles += Section->DrawCount / 3;
}
File.WriteU16(NumTriangles);
for (int SectionIdx = 0; SectionIdx < Mesh->mLods[LC_MESH_LOD_HIGH].NumSections; SectionIdx++)
{
lcMeshSection* Section = &Mesh->mLods[LC_MESH_LOD_HIGH].Sections[SectionIdx];
if (Section->PrimitiveType != LC_MESH_TRIANGLES && Section->PrimitiveType != LC_MESH_TEXTURED_TRIANGLES)
continue;
quint16* Indices = (quint16*)Mesh->mIndexData + Section->DrawOffset / sizeof(quint16);
for (int IndexIdx = 0; IndexIdx < Section->DrawCount; IndexIdx += 3)
{
File.WriteU16(Indices[IndexIdx + 0]);
File.WriteU16(Indices[IndexIdx + 1]);
File.WriteU16(Indices[IndexIdx + 2]);
File.WriteU16(7);
}
}
NumTriangles = 0;
for (int SectionIdx = 0; SectionIdx < Mesh->mLods[LC_MESH_LOD_HIGH].NumSections; SectionIdx++)
{
lcMeshSection* Section = &Mesh->mLods[LC_MESH_LOD_HIGH].Sections[SectionIdx];
if (Section->PrimitiveType != LC_MESH_TRIANGLES && Section->PrimitiveType != LC_MESH_TEXTURED_TRIANGLES)
continue;
int MaterialIndex = Section->ColorIndex == gDefaultColor ? ModelPart.ColorIndex : Section->ColorIndex;
File.WriteU16(0x4130); // CHK_MSH_MAT_GROUP
File.WriteU32(6 + MaterialNameLength + 1 + 2 + 2 * Section->DrawCount / 3);
snprintf(MaterialName, sizeof(MaterialName), "Material%03d", MaterialIndex);
File.WriteBuffer(MaterialName, MaterialNameLength + 1);
File.WriteU16(Section->DrawCount / 3);
for (int IndexIdx = 0; IndexIdx < Section->DrawCount; IndexIdx += 3)
File.WriteU16(NumTriangles++);
}
long FaceArrayEnd = File.GetPosition();
File.Seek(FaceArrayStart + 2, SEEK_SET);
File.WriteU32(FaceArrayEnd - FaceArrayStart);
File.Seek(FaceArrayEnd, SEEK_SET);
long TriObjectEnd = File.GetPosition();
File.Seek(TriObjectStart + 2, SEEK_SET);
File.WriteU32(TriObjectEnd - TriObjectStart);
File.Seek(TriObjectEnd, SEEK_SET);
long NamedObjectEnd = File.GetPosition();
File.Seek(NamedObjectStart + 2, SEEK_SET);
File.WriteU32(NamedObjectEnd - NamedObjectStart);
File.Seek(NamedObjectEnd, SEEK_SET);
}
long MDataEnd = File.GetPosition();
File.Seek(MDataStart + 2, SEEK_SET);
File.WriteU32(MDataEnd - MDataStart);
File.Seek(MDataEnd, SEEK_SET);
long KFDataStart = File.GetPosition();
File.WriteU16(0xB000); // CHK_KFDATA
File.WriteU32(0);
File.WriteU16(0xB00A); // LIB3DS_KFHDR
File.WriteU32(6 + 2 + 1 + 4);
File.WriteS16(5);
File.WriteU8(0);
File.WriteS32(100);
long KFDataEnd = File.GetPosition();
File.Seek(KFDataStart + 2, SEEK_SET);
File.WriteU32(KFDataEnd - KFDataStart);
File.Seek(KFDataEnd, SEEK_SET);
long M3DEnd = File.GetPosition();
File.Seek(M3DStart + 2, SEEK_SET);
File.WriteU32(M3DEnd - M3DStart);
File.Seek(M3DEnd, SEEK_SET);
return lcResult<void>();
}
lcResult<void> Project::ExportBrickLink()
{
lcPartsList PartsList;
if (!mModels.empty())
mModels[0]->GetPartsList(gDefaultColor, true, false, PartsList);
if (PartsList.empty())
return lcUnexpected(tr("Nothing to export."));
QString SaveFileName = GetExportFileName(QString(), "xml", tr("Export BrickLink"), tr("XML Files (*.xml);;All Files (*.*)"));
if (SaveFileName.isEmpty())
return lcUnexpected(QString());
return lcExportBrickLink(SaveFileName, PartsList);
}
lcResult<void> Project::ExportCOLLADA(const QString& FileName)
{
const lcResult<void> Ready = EnsureAssetsReady();
if (!Ready)
return Ready;
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."));
QString SaveFileName = GetExportFileName(FileName, "dae", tr("Export COLLADA"), tr("COLLADA Files (*.dae);;All Files (*.*)"));
if (SaveFileName.isEmpty())
return lcUnexpected(QString());
QFile File(SaveFileName);
if (!File.open(QIODevice::WriteOnly))
return lcUnexpected(tr("Could not open file '%1' for writing.").arg(SaveFileName));
QTextStream Stream(&File);
Stream << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n";
Stream << "<COLLADA xmlns=\"http://www.collada.org/2005/11/COLLADASchema\" version=\"1.4.1\">\r\n";
Stream << "<asset>\r\n";
Stream << "\t<created>" << QDateTime::currentDateTime().toString(Qt::ISODate) << "</created>\r\n";
Stream << "\t<modified>" << QDateTime::currentDateTime().toString(Qt::ISODate) << "</modified>\r\n";
Stream << "<unit name=\"LeoCAD\" meter=\"0.0004\" />\r\n";
Stream << "\t<up_axis>Z_UP</up_axis>\r\n";
Stream << "</asset>\r\n";
Stream << "<library_effects>\r\n";
for (const lcColor& Color : gColorList)
{
const char* ColorName = Color.SafeName;
Stream << QString("\t<effect id=\"%1-phong\">\r\n").arg(ColorName);
Stream << "\t\t<profile_COMMON>\r\n";
Stream << "\t\t\t<technique sid=\"phong1\">\r\n";
Stream << "\t\t\t\t<phong>\r\n";
Stream << "\t\t\t\t\t<emission>\r\n";
Stream << "\t\t\t\t\t\t<color>0.0 0.0 0.0 0.0</color>\r\n";
Stream << "\t\t\t\t\t</emission>\r\n";
Stream << "\t\t\t\t\t<ambient>\r\n";
Stream << QString("\t\t\t\t\t\t<color>%1 %2 %3 1.0</color>\r\n").arg(QString::number(Color.Value[0]), QString::number(Color.Value[1]), QString::number(Color.Value[2]));
Stream << "\t\t\t\t\t</ambient>\r\n";
Stream << "\t\t\t\t\t<diffuse>\r\n";
Stream << QString("\t\t\t\t\t\t<color>%1 %2 %3 1.0</color>\r\n").arg(QString::number(Color.Value[0]), QString::number(Color.Value[1]), QString::number(Color.Value[2]));
Stream << "\t\t\t\t\t</diffuse>\r\n";
Stream << "\t\t\t\t\t<specular>\r\n";
Stream << "\t\t\t\t\t\t<color>0.9 0.9 0.9 1.0</color>\r\n";
Stream << "\t\t\t\t\t</specular>\r\n";
Stream << "\t\t\t\t\t<shininess>\r\n";
Stream << "\t\t\t\t\t\t<float>20.0</float>\r\n";
Stream << "\t\t\t\t\t</shininess>\r\n";
Stream << "\t\t\t\t\t<transparent>\r\n";
Stream << QString("\t\t\t\t\t\t<color>%1 %2 %3 %4</color>\r\n").arg(QString::number(Color.Value[0]), QString::number(Color.Value[1]), QString::number(Color.Value[2]), QString::number(Color.Value[3]));
Stream << "\t\t\t\t\t</transparent>\r\n";
Stream << "\t\t\t\t\t<transparency>\r\n";
Stream << "\t\t\t\t\t\t<float>1.0</float>\r\n";
Stream << "\t\t\t\t\t</transparency>\r\n";
Stream << "\t\t\t\t</phong>\r\n";
Stream << "\t\t\t</technique>\r\n";
Stream << "\t\t</profile_COMMON>\r\n";
Stream << "\t</effect>\r\n";
}
Stream << "</library_effects>\r\n";
Stream << "<library_materials>\r\n";
for (const lcColor& Color : gColorList)
{
const char* ColorName = Color.SafeName;
Stream << QString("\t<material id=\"%1-material\">\r\n").arg(ColorName);
Stream << QString("\t\t<instance_effect url=\"#%1-phong\" />\r\n").arg(ColorName);
Stream << "\t</material>\r\n";
}
Stream << "</library_materials>\r\n";
Stream << "<library_geometries>\r\n";
std::set<lcMesh*> AddedMeshes;
auto GetMeshID = [](const lcModelPartsEntry& ModelPart)
{
const PieceInfo* Info = ModelPart.Info;
QString ID = QString(Info->mFileName).replace('.', '_');
if (ModelPart.Mesh)
ID += "_" + QString::number((quintptr)ModelPart.Mesh, 16);
return ID;
};
for (const lcModelPartsEntry& ModelPart : ModelParts)
{
lcMesh* Mesh = !ModelPart.Mesh ? ModelPart.Info->GetMesh() : ModelPart.Mesh;
if (!AddedMeshes.insert(Mesh).second)
continue;
QString ID = GetMeshID(ModelPart);
if (!Mesh)
continue;
Stream << QString("\t<geometry id=\"%1\">\r\n").arg(ID);
Stream << "\t\t<mesh>\r\n";
Stream << QString("\t\t\t<source id=\"%1-pos\">\r\n").arg(ID);
Stream << QString("\t\t\t\t<float_array id=\"%1-pos-array\" count=\"%2\">\r\n").arg(ID, QString::number(Mesh->mNumVertices));
lcVertex* Verts = (lcVertex*)Mesh->mVertexData;
for (int VertexIdx = 0; VertexIdx < Mesh->mNumVertices; VertexIdx++)
{
lcVector3& Position = Verts[VertexIdx].Position;
Stream << QString("\t\t\t\t\t%1 %2 %3\r\n").arg(QString::number(Position.x), QString::number(Position.y), QString::number(Position.z));
}
Stream << "\t\t\t\t</float_array>\r\n";
Stream << "\t\t\t\t<technique_common>\r\n";
Stream << QString("\t\t\t\t\t<accessor source=\"#%1-pos-array\" count=\"%2\" stride=\"3\">\r\n").arg(ID, QString::number(Mesh->mNumVertices));
Stream << "\t\t\t\t\t\t<param name=\"X\" type=\"float\" />\r\n";
Stream << "\t\t\t\t\t\t<param name=\"Y\" type=\"float\" />\r\n";
Stream << "\t\t\t\t\t\t<param name=\"Z\" type=\"float\" />\r\n";
Stream << "\t\t\t\t\t</accessor>\r\n";
Stream << "\t\t\t\t</technique_common>\r\n";
Stream << "\t\t\t</source>\r\n";
Stream << QString("\t\t\t<source id=\"%1-normal\">\r\n").arg(ID);
Stream << QString("\t\t\t\t<float_array id=\"%1-normal-array\" count=\"%2\">\r\n").arg(ID, QString::number(Mesh->mNumVertices));
for (int VertexIdx = 0; VertexIdx < Mesh->mNumVertices; VertexIdx++)
{
lcVector3 Normal = lcUnpackNormal(Verts[VertexIdx].Normal);
Stream << QString("\t\t\t\t\t%1 %2 %3\r\n").arg(QString::number(Normal.x), QString::number(Normal.y), QString::number(Normal.z));
}
Stream << "\t\t\t\t</float_array>\r\n";
Stream << "\t\t\t\t<technique_common>\r\n";
Stream << QString("\t\t\t\t\t<accessor source=\"#%1-normal-array\" count=\"%2\" stride=\"3\">\r\n").arg(ID, QString::number(Mesh->mNumVertices));
Stream << "\t\t\t\t\t\t<param name=\"X\" type=\"float\" />\r\n";
Stream << "\t\t\t\t\t\t<param name=\"Y\" type=\"float\" />\r\n";
Stream << "\t\t\t\t\t\t<param name=\"Z\" type=\"float\" />\r\n";
Stream << "\t\t\t\t\t</accessor>\r\n";
Stream << "\t\t\t\t</technique_common>\r\n";
Stream << "\t\t\t</source>\r\n";
Stream << QString("\t\t\t<vertices id=\"%1-vertices\">\r\n").arg(ID);
Stream << QString("\t\t\t\t<input semantic=\"POSITION\" source=\"#%1-pos\"/>\r\n").arg(ID);
Stream << "\t\t\t</vertices>\r\n";
for (int SectionIdx = 0; SectionIdx < Mesh->mLods[LC_MESH_LOD_HIGH].NumSections; SectionIdx++)
{
lcMeshSection* Section = &Mesh->mLods[LC_MESH_LOD_HIGH].Sections[SectionIdx];
if (Section->PrimitiveType != LC_MESH_TRIANGLES && Section->PrimitiveType != LC_MESH_TEXTURED_TRIANGLES)
continue;
const char* ColorName = gColorList[Section->ColorIndex].SafeName;
if (Mesh->mIndexType == GL_UNSIGNED_SHORT)
{
quint16* Indices = (quint16*)Mesh->mIndexData + Section->DrawOffset / sizeof(quint16);
Stream << QString("\t\t\t<triangles count=\"%1\" material=\"%2\">\r\n").arg(QString::number(Section->DrawCount / 3), ColorName);
Stream << QString("\t\t\t<input semantic=\"VERTEX\" source=\"#%1-vertices\" offset=\"0\" />\r\n").arg(ID);
Stream << QString("\t\t\t<input semantic=\"NORMAL\" source=\"#%1-normal\" offset=\"0\" />\r\n").arg(ID);
Stream << "\t\t\t<p>\r\n";
for (int Idx = 0; Idx < Section->DrawCount; Idx += 3)
{
QString idx1 = QString::number(Indices[Idx + 0]);
QString idx2 = QString::number(Indices[Idx + 1]);
QString idx3 = QString::number(Indices[Idx + 2]);
Stream << QString("\t\t\t\t %1 %2 %3\r\n").arg(idx1, idx2, idx3);
}
}
else
{
quint32* Indices = (quint32*)Mesh->mIndexData + Section->DrawOffset / sizeof(quint32);
Stream << QString("\t\t\t<triangles count=\"%1\" material=\"%2\">\r\n").arg(QString::number(Section->DrawCount / 3), ColorName);
Stream << QString("\t\t\t<input semantic=\"VERTEX\" source=\"#%1-vertices\" offset=\"0\" />\r\n").arg(ID);
Stream << QString("\t\t\t<input semantic=\"NORMAL\" source=\"#%1-normal\" offset=\"0\" />\r\n").arg(ID);
Stream << "\t\t\t<p>\r\n";
for (int Idx = 0; Idx < Section->DrawCount; Idx += 3)
{
QString idx1 = QString::number(Indices[Idx + 0]);
QString idx2 = QString::number(Indices[Idx + 1]);
QString idx3 = QString::number(Indices[Idx + 2]);
Stream << QString("\t\t\t\t %1 %2 %3\r\n").arg(idx1, idx2, idx3);
}
}
Stream << "\t\t\t\t</p>\r\n";
Stream << "\t\t\t</triangles>\r\n";
}
Stream << "\t\t</mesh>\r\n";
Stream << "\t</geometry>\r\n";
}
Stream << "</library_geometries>\r\n";
Stream << "<library_visual_scenes>\r\n";
Stream << "\t<visual_scene id=\"DefaultScene\">\r\n";
for (const lcModelPartsEntry& ModelPart : ModelParts)
{
lcMesh* Mesh = !ModelPart.Mesh ? ModelPart.Info->GetMesh() : ModelPart.Mesh;
if (!Mesh)
continue;
QString ID = GetMeshID(ModelPart);
Stream << "\t\t<node>\r\n";
Stream << "\t\t\t<matrix>\r\n";
const lcMatrix44& Matrix = ModelPart.WorldMatrix;
Stream << QString("\t\t\t\t%1 %2 %3 %4\r\n").arg(QString::number(Matrix[0][0]), QString::number(Matrix[1][0]), QString::number(Matrix[2][0]), QString::number(Matrix[3][0]));
Stream << QString("\t\t\t\t%1 %2 %3 %4\r\n").arg(QString::number(Matrix[0][1]), QString::number(Matrix[1][1]), QString::number(Matrix[2][1]), QString::number(Matrix[3][1]));
Stream << QString("\t\t\t\t%1 %2 %3 %4\r\n").arg(QString::number(Matrix[0][2]), QString::number(Matrix[1][2]), QString::number(Matrix[2][2]), QString::number(Matrix[3][2]));
Stream << QString("\t\t\t\t%1 %2 %3 %4\r\n").arg(QString::number(Matrix[0][3]), QString::number(Matrix[1][3]), QString::number(Matrix[2][3]), QString::number(Matrix[3][3]));
Stream << "\t\t\t</matrix>\r\n";
Stream << QString("\t\t\t<instance_geometry url=\"#%1\">\r\n").arg(ID);
Stream << "\t\t\t\t<bind_material>\r\n";
Stream << "\t\t\t\t\t<technique_common>\r\n";
for (int SectionIdx = 0; SectionIdx < Mesh->mLods[LC_MESH_LOD_HIGH].NumSections; SectionIdx++)
{
lcMeshSection* Section = &Mesh->mLods[LC_MESH_LOD_HIGH].Sections[SectionIdx];
if (Section->PrimitiveType != LC_MESH_TRIANGLES && Section->PrimitiveType != LC_MESH_TEXTURED_TRIANGLES)
continue;
const char* SourceColorName = gColorList[Section->ColorIndex].SafeName;
const char* TargetColorName;
if (Section->ColorIndex == gDefaultColor)
TargetColorName = gColorList[ModelPart.ColorIndex].SafeName;
else
TargetColorName = gColorList[Section->ColorIndex].SafeName;
Stream << QString("\t\t\t\t\t\t<instance_material symbol=\"%1\" target=\"#%2-material\"/>\r\n").arg(SourceColorName, TargetColorName);
}
Stream << "\t\t\t\t\t</technique_common>\r\n";
Stream << "\t\t\t\t</bind_material>\r\n";
Stream << "\t\t\t</instance_geometry>\r\n";
Stream << "\t\t</node>\r\n";
}
Stream << "\t</visual_scene>\r\n";
Stream << "</library_visual_scenes>\r\n";
Stream << "<scene>\r\n";
Stream << "\t<instance_visual_scene url=\"#DefaultScene\"/>\r\n";
Stream << "</scene>\r\n";
Stream << "</COLLADA>\r\n";
return lcResult<void>();
}
lcResult<void> Project::ExportCSV(const QString& FileName)
{
lcPartsList PartsList;
if (!mModels.empty())
mModels[0]->GetPartsList(gDefaultColor, true, false, PartsList);
if (PartsList.empty())
return lcUnexpected(tr("Nothing to export."));
QString SaveFileName = GetExportFileName(FileName, "csv", tr("Export CSV"), tr("CSV Files (*.csv);;All Files (*.*)"));
if (SaveFileName.isEmpty())
return lcUnexpected(QString());
lcDiskFile CSVFile(SaveFileName);
char Line[1024];
if (!CSVFile.Open(QIODevice::WriteOnly))
return lcUnexpected(tr("Could not open file '%1' for writing.").arg(SaveFileName));
CSVFile.WriteLine("Part Name,Color,Quantity,Part ID,Color Code\n");
for (const auto& PartIt : PartsList)
{
const PieceInfo* Info = PartIt.first;
for (const auto& ColorIt : PartIt.second)
{
std::string Description = Info->m_strDescription;
Description.erase(std::remove(Description.begin(), Description.end(), ','), Description.end());
snprintf(Line, sizeof(Line), "\"%s\",\"%s\",%d,%s,%d\n", Description.c_str(), gColorList[ColorIt.first].Name, ColorIt.second, Info->mFileName, gColorList[ColorIt.first].Code);
CSVFile.WriteLine(Line);
}
}
return lcResult<void>();
}
lcInstructions* Project::GetInstructions()
{
mInstructions.reset();
mInstructions = std::unique_ptr<lcInstructions>(new lcInstructions(this));
return mInstructions.get();
}
lcResult<void> Project::ExportHTML(const lcHTMLExportOptions& Options)
{
std::set<lcModel*> Models;
if (Options.CurrentOnly)
Models.insert(mActiveModel);
else if (Options.SubModels)
{
Models.insert(mActiveModel);
mActiveModel->GetSubModels(Models);
}
else
{
for (const std::unique_ptr<lcModel>& Model : mModels)
Models.insert(Model.get());
}
for (const lcModel* Model : Models)
{
const lcResult<void> Ready = Model->EnsureAssetsReady();
if (!Ready)
return Ready;
}
QDir Dir(Options.PathName);
Dir.mkpath(QLatin1String("."));
QString ProjectTitle = GetTitle();
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())
return lcUnexpected(tr("Could not render the pieces list image."));
QString ImageName = BaseName + QLatin1String("-parts.png");
QString FileName = QFileInfo(Dir, ImageName).absoluteFilePath();
QImageWriter Writer(FileName);
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)
{
QString BaseName = ProjectTitle.left(ProjectTitle.length() - QFileInfo(ProjectTitle).suffix().length() - 1);
lcStep LastStep = Model->GetLastStep();
QString PageTitle;
if (Models.size() > 1)
{
BaseName += '-' + Model->GetProperties().mFileName;
PageTitle = Model->GetProperties().mFileName;
}
else
PageTitle = ProjectTitle;
BaseName.replace('#', '_');
if (Options.SinglePage)
{
QString FileName = QFileInfo(Dir, BaseName + QLatin1String(".html")).absoluteFilePath();
QFile File(FileName);
if (!File.open(QIODevice::WriteOnly))
return lcUnexpected(tr("Error writing to file '%1':\n%2").arg(FileName, File.errorString()));
QTextStream Stream(&File);
Stream << QString::fromLatin1("<HTML>\r\n<HEAD>\r\n<TITLE>Instructions for %1</TITLE>\r\n</HEAD>\r\n<BR>\r\n<CENTER>\r\n").arg(PageTitle);
for (lcStep Step = 1; Step <= LastStep; Step++)
{
QString StepString = QString::fromLatin1("%1").arg(Step, 2, 10, QLatin1Char('0'));
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)
{
const lcResult<void> PartsImageSaved = AddPartsListImage(Stream, Model, Step, QString("%1-%2").arg(BaseName, StepString));
if (!PartsImageSaved)
return PartsImageSaved;
}
}
if (Options.PartsListEnd)
{
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");
}
else
{
if (Options.IndexPage)
{
QString FileName = QFileInfo(Dir, BaseName + QLatin1String("-index.html")).absoluteFilePath();
QFile File(FileName);
if (!File.open(QIODevice::WriteOnly))
return lcUnexpected(tr("Error writing to file '%1':\n%2").arg(FileName, File.errorString()));
QTextStream Stream(&File);
Stream << QString::fromLatin1("<HTML>\r\n<HEAD>\r\n<TITLE>Instructions for %1</TITLE>\r\n</HEAD>\r\n<BR>\r\n<CENTER>\r\n").arg(PageTitle);
for (lcStep Step = 1; Step <= LastStep; Step++)
Stream << QString::fromLatin1("<A HREF=\"%1-%2.html\">Step %3<BR>\r\n</A>").arg(BaseName, QString("%1").arg(Step, 2, 10, QLatin1Char('0')), QString::number(Step));
if (Options.PartsListEnd)
Stream << QString::fromLatin1("<A HREF=\"%1-pieces.html\">Pieces Used</A><BR>\r\n").arg(BaseName);
Stream << QLatin1String("</CENTER>\r\n<BR><HR><BR><B><I>Created by <A HREF=\"https://www.leocad.org\">LeoCAD</A></B></I><BR></HTML>\r\n");
}
for (lcStep Step = 1; Step <= LastStep; Step++)
{
QString StepString = QString::fromLatin1("%1").arg(Step, 2, 10, QLatin1Char('0'));
QString FileName = QFileInfo(Dir, QString("%1-%2.html").arg(BaseName, StepString)).absoluteFilePath();
QFile File(FileName);
if (!File.open(QIODevice::WriteOnly))
return lcUnexpected(tr("Error writing to file '%1':\n%2").arg(FileName, File.errorString()));
QTextStream Stream(&File);
Stream << QString::fromLatin1("<HTML>\r\n<HEAD>\r\n<TITLE>%1 - Step %2</TITLE>\r\n</HEAD>\r\n<BR>\r\n<CENTER>\r\n").arg(PageTitle, QString::number(Step));
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)
{
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)
Stream << QString::fromLatin1("<A HREF=\"%1-%2.html\">Previous</A> ").arg(BaseName, QString("%1").arg(Step - 1, 2, 10, QLatin1Char('0')));
if (Options.IndexPage)
Stream << QString::fromLatin1("<A HREF=\"%1-index.html\">Index</A> ").arg(BaseName);
if (Step != LastStep)
Stream << QString::fromLatin1("<A HREF=\"%1-%2.html\">Next</A>").arg(BaseName, QString("%1").arg(Step + 1, 2, 10, QLatin1Char('0')));
else if (Options.PartsListEnd)
Stream << QString::fromLatin1("<A HREF=\"%1-pieces.html\">Pieces Used</A>").arg(BaseName);
Stream << QLatin1String("<BR></HTML>\r\n");
}
if (Options.PartsListEnd)
{
QString FileName = QFileInfo(Dir, BaseName + QLatin1String("-pieces.html")).absoluteFilePath();
QFile File(FileName);
if (!File.open(QIODevice::WriteOnly))
return lcUnexpected(tr("Error writing to file '%1':\n%2").arg(FileName, File.errorString()));
QTextStream Stream(&File);
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);
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')));
if (Options.IndexPage)
Stream << QString::fromLatin1("<A HREF=\"%1-index.html\">Index</A> ").arg(BaseName);
Stream << QLatin1String("<BR></HTML>\r\n");
}
}
QString StepImageBaseName = QFileInfo(Dir, BaseName + QLatin1String("-%1.png")).absoluteFilePath();
const lcResult<void> ImagesSaved = Model->SaveStepImages(StepImageBaseName, true, false, Options.StepImagesWidth, Options.StepImagesHeight, 1, LastStep);
if (!ImagesSaved)
return ImagesSaved;
}
if (Models.size() > 1)
{
QString BaseName = ProjectTitle.left(ProjectTitle.length() - QFileInfo(ProjectTitle).suffix().length() - 1);
QString FileName = QFileInfo(Dir, BaseName + QLatin1String("-index.html")).absoluteFilePath();
QFile File(FileName);
if (!File.open(QIODevice::WriteOnly))
return lcUnexpected(tr("Error writing to file '%1':\n%2").arg(FileName, File.errorString()));
QTextStream Stream(&File);
Stream << QString::fromLatin1("<HTML>\r\n<HEAD>\r\n<TITLE>Instructions for %1</TITLE>\r\n</HEAD>\r\n<BR>\r\n<CENTER>\r\n").arg(ProjectTitle);
for (const lcModel* Model : Models)
{
BaseName = ProjectTitle.left(ProjectTitle.length() - QFileInfo(ProjectTitle).suffix().length() - 1) + '-' + Model->GetProperties().mFileName;
BaseName.replace('#', '_');
if (Options.SinglePage)
{
FileName = BaseName + QLatin1String(".html");
Stream << QString::fromLatin1("<p><a href=\"%1\">%2</a>").arg(FileName, Model->GetProperties().mFileName);
}
else if (Options.IndexPage)
{
FileName = BaseName + QLatin1String("-index.html");
Stream << QString::fromLatin1("<p><a href=\"%1\">%2</a>").arg(FileName, Model->GetProperties().mFileName);
}
else
{
lcStep LastStep = Model->GetLastStep();
Stream << QString::fromLatin1("<p>%1</p>").arg(Model->GetProperties().mFileName);
for (lcStep Step = 1; Step <= LastStep; Step++)
Stream << QString::fromLatin1("<A HREF=\"%1-%2.html\">Step %3<BR>\r\n</A>").arg(BaseName, QString("%1").arg(Step, 2, 10, QLatin1Char('0')), QString::number(Step));
if (Options.PartsListEnd)
Stream << QString::fromLatin1("<A HREF=\"%1-pieces.html\">Pieces Used</A><BR>\r\n").arg(BaseName);
}
}
Stream << QLatin1String("</CENTER>\r\n<BR><HR><BR><B><I>Created by <A HREF=\"https://www.leocad.org\">LeoCAD</A></B></I><BR></HTML>\r\n");
}
return lcResult<void>();
}
lcResult<void> Project::ExportPOVRay(const QString& FileName)
{
const lcResult<void> Ready = EnsureAssetsReady();
if (!Ready)
return Ready;
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."));
QString SaveFileName = GetExportFileName(FileName, QLatin1String("pov"), tr("Export POV-Ray"), tr("POV-Ray Files (*.pov);;All Files (*.*)"));
if (SaveFileName.isEmpty())
return lcUnexpected(QString());
lcDiskFile POVFile(SaveFileName);
if (!POVFile.Open(QIODevice::WriteOnly))
return lcUnexpected(tr("Could not open file '%1' for writing.").arg(SaveFileName));
enum
{
LGEO_PIECE_LGEO = 0x01,
LGEO_PIECE_AR = 0x02,
LGEO_PIECE_SLOPE = 0x04
};
enum
{
LGEO_COLOR_SOLID = 0x01,
LGEO_COLOR_TRANSPARENT = 0x02,
LGEO_COLOR_CHROME = 0x04,
LGEO_COLOR_PEARL = 0x08,
LGEO_COLOR_METALLIC = 0x10,
LGEO_COLOR_RUBBER = 0x20,
LGEO_COLOR_GLITTER = 0x40
};
char Line[2048];
snprintf(Line, sizeof(Line), "// Generated By: LeoCAD %s\n// LDraw File: %s\n// Date: %s\n\n",
LC_VERSION_TEXT,
mModels[0]->GetProperties().mFileName.toLatin1().constData(),
QDateTime::currentDateTime().toString(Qt::ISODate).toLatin1().constData());
POVFile.WriteLine(Line);
POVFile.WriteLine("#version 3.7;\n\n");
POVFile.WriteLine("global_settings { assumed_gamma 1.0 }\n\n");
lcPiecesLibrary* Library = lcGetPiecesLibrary();
std::map<const PieceInfo*, std::pair<char[LC_PIECE_NAME_LEN + 1], int>> PieceTable;
size_t NumColors = gColorList.size();
std::vector<std::string> LgeoColorTable(NumColors);
std::vector<std::array<char, LC_MAX_COLOR_NAME + 3>> ColorTable(NumColors);
const std::vector<std::unique_ptr<lcLight>>& Lights = gMainWindow->GetActiveModel()->GetLights();
const lcCamera* Camera = gMainWindow->GetActiveView()->GetCamera();
const QString CameraName = QString(Camera->GetName()).replace(" ","_");
const lcVector3& Position = Camera->mPosition;
const lcVector3& Target = Camera->mTargetPosition;
const lcVector3& Up = Camera->mUpVector;
const lcVector3 BackgroundColor = lcVector3FromColor(lcGetPreferences().mBackgroundSolidColor);
const lcPOVRayOptions& POVRayOptions = mModels[0]->GetPOVRayOptions();
const QString TopModelName = QString("LC_%1").arg(QString(mModels[0]->GetFileName()).replace(" ","_").replace(".","_dot_"));
const QString LGEOPath = lcGetProfileString(LC_PROFILE_POVRAY_LGEO_PATH);
const bool UseLGEO = POVRayOptions.UseLGEO && !LGEOPath.isEmpty();
const int TopModelColorCode = 7;
QStringList ColorMacros, MaterialColors;
lcVector3 Min(FLT_MAX, FLT_MAX, FLT_MAX);
lcVector3 Max(-FLT_MAX, -FLT_MAX, -FLT_MAX);
for (const lcModelPartsEntry& ModelPart : ModelParts)
{
lcVector3 Points[8];
lcGetBoxCorners(ModelPart.Info->GetBoundingBox(), Points);
for (int PointIdx = 0; PointIdx < 8; PointIdx++)
{
lcVector3 Point = lcMul31(Points[PointIdx], ModelPart.WorldMatrix);
Min = lcMin(Point, Min);
Max = lcMax(Point, Max);
}
}
lcVector3 Center = (Min + Max) / 2.0f;
float Radius = (Max - Center).Length() / 25.0f;
Center = lcVector3(Center[1], Center[0], Center[2]) / 25.0f;
lcVector3 FloorColor = POVRayOptions.FloorColor;
float FloorAmbient = POVRayOptions.FloorAmbient;
float FloorDiffuse = POVRayOptions.FloorDiffuse;
const char* FloorLocation;
const char* FloorAxis;
if (POVRayOptions.FloorAxis == 0)
{
FloorAxis = "x";
FloorLocation = "MaxX";
}
else if (POVRayOptions.FloorAxis == 1)
{
FloorAxis = "y";
FloorLocation = "MaxY";
}
else
{
FloorAxis = "z";
FloorLocation = "MaxZ";
}
for (const std::unique_ptr<lcLight>& Light : Lights)
{
if (Light->GetLightType() == lcLightType::Area)
{
if (FloorColor == lcVector3(0.8f,0.8f,0.8f))
FloorColor = {1.0f,1.0f,1.0f};
if (FloorAmbient == 0.4f)
FloorAmbient = 0.0f;
if (FloorDiffuse == 0.4f)
FloorDiffuse = 0.9f;
break;
}
}
if (!POVRayOptions.HeaderIncludeFile.isEmpty())
{
snprintf(Line, sizeof(Line), "#include \"%s\"\n\n", POVRayOptions.HeaderIncludeFile.toLatin1().constData());
POVFile.WriteLine(Line);
}
snprintf(Line, sizeof(Line),
"#ifndef (MinX) #declare MinX = %g; #end\n"
"#ifndef (MinY) #declare MinY = %g; #end\n"
"#ifndef (MinZ) #declare MinZ = %g; #end\n"
"#ifndef (MaxX) #declare MaxX = %g; #end\n"
"#ifndef (MaxY) #declare MaxY = %g; #end\n"
"#ifndef (MaxZ) #declare MaxZ = %g; #end\n"
"#ifndef (CenterX) #declare CenterX = %g; #end\n"
"#ifndef (CenterY) #declare CenterY = %g; #end\n"
"#ifndef (CenterZ) #declare CenterZ = %g; #end\n"
"#ifndef (Center) #declare Center = <CenterX,CenterY,CenterZ>; #end\n"
"#ifndef (Radius) #declare Radius = %g; #end\n",
Min[0], Min[1], Min[2], Max[0], -Max[1], Max[2], Center[0], Center[1], Center[2], Radius);
POVFile.WriteLine(Line);
snprintf(Line, sizeof(Line),
"#ifndef (CameraSky) #declare CameraSky = <%g,%g,%g>; #end\n"
"#ifndef (CameraLocation) #declare CameraLocation = <%g, %g, %g>; #end\n"
"#ifndef (CameraTarget) #declare CameraTarget = <%g, %g, %g>; #end\n"
"#ifndef (CameraAngle) #declare CameraAngle = %g; #end\n",
Up[1], Up[0], Up[2], Position[1] / 25.0f, Position[0] / 25.0f, Position[2] / 25.0f, Target[1] / 25.0f, Target[0] / 25.0f, Target[2] / 25.0f, Camera->m_fovy);
POVFile.WriteLine(Line);
snprintf(Line, sizeof(Line),
"#ifndef (BackgroundColor) #declare BackgroundColor = <%1g, %1g, %1g>; #end\n"
"#ifndef (Background) #declare Background = %s; #end\n",
BackgroundColor[0], BackgroundColor[1], BackgroundColor[2], (POVRayOptions.ExcludeBackground ? "false" : "true"));
POVFile.WriteLine(Line);
snprintf(Line, sizeof(Line),
"#ifndef (FloorAxis) #declare FloorAxis = %s; #end\n"
"#ifndef (FloorLocation) #declare FloorLocation = %s; #end\n"
"#ifndef (FloorColor) #declare FloorColor = <%1g, %1g, %1g>; #end\n"
"#ifndef (FloorAmbient) #declare FloorAmbient = %1g; #end\n"
"#ifndef (FloorDiffuse) #declare FloorDiffuse = %1g; #end\n"
"#ifndef (Floor) #declare Floor = %s; #end\n",
FloorAxis, FloorLocation, FloorColor[0], FloorColor[1], FloorColor[2], FloorAmbient, FloorDiffuse, (POVRayOptions.ExcludeFloor ? "false" : "true"));
POVFile.WriteLine(Line);
snprintf(Line, sizeof(Line),
"#ifndef (Ambient) #declare Ambient = 0.4; #end\n"
"#ifndef (Diffuse) #declare Diffuse = 0.4; #end\n"
"#ifndef (Reflection) #declare Reflection = 0.08; #end\n"
"#ifndef (Phong) #declare Phong = 0.5; #end\n"
"#ifndef (PhongSize) #declare PhongSize = 40; #end\n"
"#ifndef (TransReflection) #declare TransReflection = 0.2; #end\n"
"#ifndef (TransFilter) #declare TransFilter = 0.85; #end\n"
"#ifndef (TransIoR) #declare TransIoR = 1.25; #end\n"
"#ifndef (RubberReflection) #declare RubberReflection = 0; #end\n"
"#ifndef (RubberPhong) #declare RubberPhong = 0.1; #end\n"
"#ifndef (RubberPhongS) #declare RubberPhongS = 10; #end\n"
"#ifndef (ChromeReflection) #declare ChromeReflection = 0.85; #end\n"
"#ifndef (ChromeBrilliance) #declare ChromeBrilliance = 5; #end\n"
"#ifndef (ChromeSpecular) #declare ChromeSpecular = 0.8; #end\n"
"#ifndef (ChromeRough) #declare ChromeRough = 0.01; #end\n"
"#ifndef (OpaqueNormal) #declare OpaqueNormal = normal { bumps 0.001 scale 0.5 }; #end\n"
"#ifndef (TransNormal) #declare TransNormal = normal { bumps 0.001 scale 0.5 }; #end\n");
POVFile.WriteLine(Line);
snprintf(Line, sizeof(Line),
"#ifndef (Quality) #declare Quality = 3; #end\n"
"#ifndef (Studs) #declare Studs = 1; #end\n"
"#ifndef (LgeoLibrary) #declare LgeoLibrary = %s; #end\n"
"#ifndef (ModelReflection) #declare ModelReflection = %i; #end\n"
"#ifndef (ModelShadow) #declare ModelShadow = %i; #end\n\n",
(POVRayOptions.UseLGEO ? "true" : "false"), (POVRayOptions.NoReflection ? 0 : 1), (POVRayOptions.NoShadow ? 0 : 1));
POVFile.WriteLine(Line);
snprintf(Line, sizeof(Line),
"#ifndef (SkipWriteLightMacro)\n"
"#macro WriteLight(Type, Shadowless, Location, Target, Color, Power, FadeDistance, FadePower, SpotRadius, SpotFalloff, SpotTightness, AreaCircle, AreaWidth, AreaHeight, AreaRows, AreaColumns)\n"
" #local PointLight = %i;\n"
" #local Spotlight = %i;\n"
" #local DirectionalLight = %i;\n"
" #local AreaLight = %i;\n"
" light_source {\n"
" Location\n"
" color rgb Color*Power\n"
" #if (Shadowless > 0)\n"
" shadowless\n"
" #end\n"
" #if (FadeDistance > 0)\n"
" fade_distance FadeDistance\n"
" #end\n"
" #if (FadePower > 0)\n"
" fade_power FadePower\n"
" #end\n"
" #if (Type = Spotlight)\n"
" spotlight\n"
" radius SpotRadius\n"
" falloff SpotFalloff\n"
" tightness SpotTightness\n"
" point_at Target\n"
" #elseif (Type = DirectionalLight)\n"
" parallel\n"
" point_at Target\n"
" #elseif (Type = AreaLight)\n"
" area_light AreaWidth, AreaHeight, AreaRows, AreaColumns\n"
" jitter\n"
" #if (AreaCircle > 0 & AreaRows > 1 & AreaColumns > 1)\n"
" circular \n"
" #if (AreaWidth = AreaHeight & AreaRows = AreaColumns)\n"
" orient\n"
" #end\n"
" #end\n"
" #end\n"
" }\n"
"#end\n"
"#end\n\n",
static_cast<int>(lcLightType::Point), static_cast<int>(lcLightType::Spot), static_cast<int>(lcLightType::Directional), static_cast<int>(lcLightType::Area));
POVFile.WriteLine(Line);
for (const lcModelPartsEntry& ModelPart : ModelParts)
{
int ColorIdx = ModelPart.ColorIndex;
if (lcIsColorTranslucent(ColorIdx))
{
if (!ColorMacros.contains("TranslucentColor"))
{
snprintf(Line, sizeof(Line),
"#ifndef (SkipTranslucentColorMacro)\n"
"#macro TranslucentColor(r, g, b, f)\n"
" material {\n"
" texture {\n"
" pigment { srgbf <r,g,b,f> }\n"
" finish { emission 0 ambient Ambient diffuse Diffuse }\n"
" finish { phong Phong phong_size PhongSize reflection TransReflection }\n"
" normal { TransNormal }\n"
" }\n"
" interior { ior TransIoR }\n"
" }\n"
"#end\n"
"#end\n\n");
POVFile.WriteLine(Line);
ColorMacros.append("TranslucentColor");
}
}
else if (lcIsColorChrome(ColorIdx))
{
if (!ColorMacros.contains("ChromeColor"))
{
snprintf(Line, sizeof(Line),
"#ifndef (SkipChromeColorMacro)\n"
"#macro ChromeColor(r, g, b)\n"
"#if (LgeoLibrary) material { #end\n"
" texture {\n"
" pigment { srgbf <r,g,b,0> }\n"
" finish { emission 0 ambient Ambient diffuse Diffuse }\n"
" finish { phong Phong phong_size PhongSize reflection ChromeReflection brilliance ChromeBrilliance metallic specular ChromeSpecular roughness ChromeRough }\n"
" }\n"
"#if (LgeoLibrary) } #end\n"
"#end\n"
"#end\n\n");
POVFile.WriteLine(Line);
ColorMacros.append("ChromeColor");
}
}
else if (lcIsColorRubber(ColorIdx))
{
if (!ColorMacros.contains("RubberColor"))
{
snprintf(Line, sizeof(Line),
"#ifndef (SkipRubberColorMacro)\n"
"#macro RubberColor(r, g, b)\n"
"#if (LgeoLibrary) material { #end\n"
" texture {\n"
" pigment { srgbf <r,g,b,0> }\n"
" finish { emission 0 ambient Ambient diffuse Diffuse }\n"
" finish { phong RubberPhong phong_size RubberPhongS reflection RubberReflection }\n"
" }\n"
"#if (LgeoLibrary) } #end\n"
"#end\n"
"#end\n\n");
POVFile.WriteLine(Line);
ColorMacros.append("RubberColor");
}
}
else
{
if (!ColorMacros.contains("OpaqueColor"))
{
snprintf(Line, sizeof(Line),
"#ifndef (SkipOpaqueColorMacro)\n"
"#macro OpaqueColor(r, g, b)\n"
"#if (LgeoLibrary) material { #end\n"
" texture {\n"
" pigment { srgbf <r,g,b,0> }\n"
" finish { emission 0 ambient Ambient diffuse Diffuse }\n"
" finish { phong Phong phong_size PhongSize reflection Reflection }\n"
" normal { OpaqueNormal }\n"
" }\n"
"#if (LgeoLibrary) } #end\n"
"#end\n"
"#end\n\n");
POVFile.WriteLine(Line);
ColorMacros.append("OpaqueColor");
}
}
}
snprintf(Line, sizeof(Line), "#if (Background)\n background {\n color rgb BackgroundColor\n }\n#end\n\n");
POVFile.WriteLine(Line);
snprintf(Line, sizeof(Line), "#ifndef (Skip%s)\n camera {\n perspective\n right x * image_width / image_height\n sky CameraSky\n location CameraLocation\n look_at CameraTarget\n angle CameraAngle * image_width / image_height\n }\n#end\n\n",
(CameraName.isEmpty() ? "Camera" : CameraName.toLatin1().constData()));
POVFile.WriteLine(Line);
lcVector3 AreaX(1.0f, 0.0f, 0.0f), AreaY(0.0f, 1.0f, 0.0f);
lcVector2i AreaGrid(1, 1);
int AreaCircle = 0, Shadowless = 0;
lcLightType LightType = lcLightType::Area;
float Power = 0.0f, FadeDistance = 0.0f, FadePower = 0.0f, SpotRadius = 0.0f, SpotFalloff = 0.0f, SpotTightness = 0.0f;
if (Lights.empty())
{
const lcVector3 LightTarget(0.0f, 0.0f, 0.0f), LightColor(1.0f, 1.0f, 1.0f);
lcVector3 Location[4];
Location[0] = {0.0f * Radius + Center[0], -1.5f * Radius + Center[1], -1.5f * Radius + Center[2]};
Location[1] = {1.5f * Radius + Center[0], -1.0f * Radius + Center[1], 0.866026f * Radius + Center[2]};
Location[2] = {0.0f * Radius + Center[0], -2.0f * Radius + Center[1], 0.0f * Radius + Center[2]};
Location[3] = {2.0f * Radius + Center[0], 0.0f * Radius + Center[1], -2.0f * Radius + Center[2]};
for (int Idx = 0; Idx < 4; Idx++)
{
Power = Idx < 2 ? 0.75f : 0.5f;
snprintf(Line, sizeof(Line),
"#ifndef (SkipLight%i)\nWriteLight(%i, %i, <%g, %g, %g>, <%g, %g, %g>, <%g, %g, %g>, %g, %g, %g, %g, %g, %g, %i, <%g, %g, %g>, <%g, %g, %g>, %i, %i)\n#end\n\n",
Idx,
static_cast<int>(LightType),
Shadowless,
Location[Idx][0], Location[Idx][1], Location[Idx][2],
LightTarget[0], LightTarget[1], LightTarget[2],
LightColor[0], LightColor[1], LightColor[2],
Power,
FadeDistance, FadePower,
SpotRadius, SpotFalloff, SpotTightness,
AreaCircle, AreaX[0], AreaX[1], AreaX[2], AreaY[0], AreaY[1], AreaY[2], AreaGrid.x, AreaGrid.y);
POVFile.WriteLine(Line);
}
}
else
{
for (const std::unique_ptr<lcLight>& Light : Lights)
{
const lcVector3 LightPosition = Light->GetPosition();
const lcVector3 LightTarget = LightPosition + Light->GetDirection();
const lcVector3 LightColor = Light->GetColor();
const QString LightName = QString(Light->GetName()).replace(" ", "_");
LightType = Light->GetLightType();
Shadowless = Light->GetCastShadow() ? 0 : 1;
Power = Light->GetPOVRayPower();
FadeDistance = Light->GetPOVRayFadeDistance();
FadePower = Light->GetPOVRayFadePower();
SpotFalloff = Light->GetSpotConeAngle() / 2.0f;
SpotRadius = SpotFalloff - Light->GetSpotPenumbraAngle();
SpotTightness = Light->GetSpotPOVRayTightness();
AreaCircle = (Light->GetAreaShape() == lcLightAreaShape::Disk || Light->GetAreaShape() == lcLightAreaShape::Ellipse) ? 1 : 0;
AreaX = lcVector3(Light->GetWorldMatrix()[0]) * Light->GetAreaSizeX();
AreaY = lcVector3(Light->GetWorldMatrix()[1]) * Light->GetAreaSizeY();
AreaGrid = lcVector2i(Light->GetAreaPOVRayGridX(), Light->GetAreaPOVRayGridY());
snprintf(Line, sizeof(Line),
"#ifndef (Skip%s)\n WriteLight(%i, %i, <%g, %g, %g>, <%g, %g, %g>, <%g, %g, %g>, %g, %g, %g, %g, %g, %g, %i, <%g, %g, %g>, <%g, %g, %g>, %i, %i)\n#end\n\n",
LightName.toLatin1().constData(),
static_cast<int>(LightType),
Shadowless,
LightPosition[1], LightPosition[0], LightPosition[2],
LightTarget[1], LightTarget[0], LightTarget[2],
LightColor[0], LightColor[1], LightColor[2],
Power,
FadeDistance, FadePower,
SpotRadius, SpotFalloff, SpotTightness,
AreaCircle, AreaX[0], AreaX[1], AreaX[2], AreaY[0], AreaY[1], AreaY[2], AreaGrid.x, AreaGrid.y);
POVFile.WriteLine(Line);
}
}
POVFile.WriteLine("#ifndef (lg_quality) #declare lg_quality = Quality; #end\n\n");
if (UseLGEO)
{
memset(Line, 0, 1024);
POVFile.WriteLine("#ifndef (lg_studs) #declare lg_studs = Studs; #end\n\n");
POVFile.WriteLine("#if (lg_quality = 3) #declare lg_quality = 4; #end\n\n");
POVFile.WriteLine("#include \"lg_defs.inc\"\n\n#include \"lg_color.inc\"\n\n");
lcDiskFile TableFile(QFileInfo(QDir(LGEOPath), QLatin1String("lg_elements.lst")).absoluteFilePath());
if (!TableFile.Open(QIODevice::ReadOnly))
return lcUnexpected(tr("Could not find LGEO files in folder '%1'.").arg(LGEOPath));
while (TableFile.ReadLine(Line, sizeof(Line)))
{
char Src[129], Dst[129], Flags[11];
if (*Line == ';')
continue;
if (sscanf(Line,"%128s%128s%10s", Src, Dst, Flags) != 3)
continue;
lcstrcat(Src, ".dat");
PieceInfo* Info = Library->FindPiece(Src, nullptr, false, false);
if (!Info)
continue;
bool LgeoPartFound = false;
if ((LgeoPartFound = strchr(Flags, 'L')))
{
std::pair<char[LC_PIECE_NAME_LEN + 1], int>& Entry = PieceTable[Info];
Entry.second |= LGEO_PIECE_LGEO;
snprintf(Entry.first, sizeof(Entry.first), "lg_%s", Dst);
}
if (strchr(Flags, 'A') && !LgeoPartFound)
{
std::pair<char[LC_PIECE_NAME_LEN + 1], int>& Entry = PieceTable[Info];
Entry.second |= LGEO_PIECE_AR;
snprintf(Entry.first, sizeof(Entry.first), "ar_%s", Dst);
}
if (strchr(Flags, 'S'))
{
std::pair<char[LC_PIECE_NAME_LEN + 1], int>& Entry = PieceTable[Info];
Entry.second |= LGEO_PIECE_SLOPE;
Entry.first[0] = 0;
}
}
lcDiskFile LgeoColorFile(QFileInfo(QDir(LGEOPath), QLatin1String("lg_colors.lst")).absoluteFilePath());
if (!LgeoColorFile.Open(QIODevice::ReadOnly))
return lcUnexpected(tr("Could not find LGEO files in folder '%1'.").arg(LGEOPath));
while (LgeoColorFile.ReadLine(Line, sizeof(Line)))
{
char Name[1024], Flags[1024];
int Code;
if (*Line == ';')
continue;
if (sscanf(Line,"%d%s%s", &Code, Name, Flags) != 3)
continue;
size_t ColorIndex = lcGetColorIndex(Code);
if (ColorIndex >= NumColors)
continue;
LgeoColorTable[ColorIndex] = Name;
}
}
auto AddColorDefinition=[UseLGEO, &ColorTable, &LgeoColorTable, &MaterialColors, &Line, &POVFile](int ColorIndex)
{
if (!ColorTable[ColorIndex][0])
{
lcColor* Color = &gColorList[ColorIndex];
if (LgeoColorTable[ColorIndex].empty())
{
snprintf(ColorTable[ColorIndex].data(), ColorTable[ColorIndex].size(), "lc_%s", Color->SafeName);
if (lcIsColorTranslucent(ColorIndex))
{
if (!UseLGEO && !MaterialColors.contains(ColorTable[ColorIndex].data()))
MaterialColors.append(ColorTable[ColorIndex].data());
snprintf(Line, sizeof(Line), "#ifndef (lc_%s)\n#declare lc_%s = TranslucentColor(%g, %g, %g, TransFilter)\n#end\n\n",
Color->SafeName, Color->SafeName, Color->Value[0], Color->Value[1], Color->Value[2]);
}
else
{
const char* MacroName;
if (lcIsColorChrome(ColorIndex))
MacroName = "Chrome";
else if (lcIsColorRubber(ColorIndex))
MacroName = "Rubber";
else
MacroName = "Opaque";
snprintf(Line, sizeof(Line), "#ifndef (lc_%s)\n#declare lc_%s = %sColor(%g, %g, %g)\n#end\n\n", Color->SafeName, Color->SafeName, MacroName, Color->Value[0], Color->Value[1], Color->Value[2]);
}
}
else
{
snprintf(ColorTable[ColorIndex].data(), ColorTable[ColorIndex].size(), "LDXColor%i", Color->Code);
snprintf(Line, sizeof(Line), "#ifndef (LDXColor%i) // %s\n#declare LDXColor%i = material { texture { %s } }\n#end\n\n",
Color->Code, Color->Name, Color->Code, LgeoColorTable[ColorIndex].c_str());
}
POVFile.WriteLine(Line);
}
};
for (const lcModelPartsEntry& ModelPart : ModelParts)
AddColorDefinition(ModelPart.ColorIndex);
AddColorDefinition(lcGetColorIndex(TopModelColorCode));
std::set<lcMesh*> AddedMeshes;
if (UseLGEO)
{
for (const lcModelPartsEntry& ModelPart : ModelParts)
{
if (ModelPart.Mesh)
continue;
auto Search = PieceTable.find(ModelPart.Info);
if (Search == PieceTable.end())
continue;
lcMesh* Mesh = ModelPart.Info->GetMesh();
if (!Mesh)
continue;
if (!AddedMeshes.insert(Mesh).second)
continue;
const std::pair<char[LC_PIECE_NAME_LEN + 1], int>& Entry = Search->second;
if (Entry.first[0])
{
snprintf(Line, sizeof(Line), "#include \"%s.inc\" // %s\n", Entry.first, ModelPart.Info->m_strDescription);
POVFile.WriteLine(Line);
}
}
POVFile.WriteLine("\n");
}
std::vector<const char*> ColorTablePointer;
ColorTablePointer.resize(NumColors);
for (size_t ColorIdx = 0; ColorIdx < NumColors; ColorIdx++)
ColorTablePointer[ColorIdx] = ColorTable[ColorIdx].data();
auto GetMeshName = [](const lcModelPartsEntry& ModelPart, char (&Name)[LC_PIECE_NAME_LEN])
{
strncpy(Name, ModelPart.Info->mFileName, sizeof(Name));
Name[sizeof(Name) - 1] = 0;
for (char* c = Name; *c; c++)
if (*c == '-' || *c == '.')
*c = '_';
if (ModelPart.Mesh)
{
char Suffix[32];
snprintf(Suffix, sizeof(Suffix), "_%p", ModelPart.Mesh);
strncat(Name, Suffix, sizeof(Name) - strlen(Name) - 1);
Name[sizeof(Name) - 1] = 0;
}
};
for (const lcModelPartsEntry& ModelPart : ModelParts)
{
lcMesh* Mesh = !ModelPart.Mesh ? ModelPart.Info->GetMesh() : ModelPart.Mesh;
if (!AddedMeshes.insert(Mesh).second)
continue;
if (!Mesh)
continue;
char Name[LC_PIECE_NAME_LEN];
GetMeshName(ModelPart, Name);
if (!ModelPart.Mesh)
{
std::pair<char[LC_PIECE_NAME_LEN + 1], int>& Entry = PieceTable[ModelPart.Info];
lcstrcpy(Entry.first, "lc_");
strncat(Entry.first, Name, sizeof(Entry.first) - 1);
Entry.first[sizeof(Entry.first) - 1] = 0;
}
Mesh->ExportPOVRay(POVFile, Name, &ColorTablePointer[0]);
snprintf(Line, sizeof(Line), "#declare lc_%s_clear = lc_%s\n\n", Name, Name);
POVFile.WriteLine(Line);
}
snprintf(Line, sizeof(Line), "#declare %s = union {\n", TopModelName.toLatin1().constData());
POVFile.WriteLine(Line);
for (const lcModelPartsEntry& ModelPart : ModelParts)
{
int ColorIdx = ModelPart.ColorIndex;
const char* Suffix = lcIsColorTranslucent(ColorIdx) ? "_clear" : "";
const float* f = ModelPart.WorldMatrix.GetFloats();
char Modifier[32];
if (UseLGEO || MaterialColors.contains(ColorTable[ColorIdx].data()))
snprintf(Modifier, sizeof(Modifier), "material");
else
snprintf(Modifier, sizeof(Modifier), "texture");
if (!ModelPart.Mesh)
{
std::pair<char[LC_PIECE_NAME_LEN + 1], int>& Entry = PieceTable[ModelPart.Info];
if (Entry.second & LGEO_PIECE_SLOPE)
{
snprintf(Line, sizeof(Line),
" merge {\n object {\n %s%s\n %s { %s }\n }\n"
" object {\n %s_slope\n texture {\n %s normal { bumps 0.3 scale 0.02 }\n }\n }\n"
" matrix <%g, %g, %g, %g, %g, %g, %g, %g, %g, %g, %g, %g>\n }\n",
Entry.first, Suffix, Modifier, ColorTable[ColorIdx].data(), Entry.first, ColorTable[ColorIdx].data(),
-f[5], -f[4], -f[6], -f[1], -f[0], -f[2], f[9], f[8], f[10], f[13] / 25.0f, f[12] / 25.0f, f[14] / 25.0f);
}
else
{
if (!ModelPart.Info || !ModelPart.Info->GetMesh())
continue;
snprintf(Line, sizeof(Line), " object {\n %s%s\n %s { %s }\n matrix <%g, %g, %g, %g, %g, %g, %g, %g, %g, %g, %g, %g>\n }\n",
Entry.first, Suffix, Modifier, ColorTable[ColorIdx].data(), -f[5], -f[4], -f[6], -f[1], -f[0], -f[2], f[9], f[8], f[10], f[13] / 25.0f, f[12] / 25.0f, f[14] / 25.0f);
}
}
else
{
char Name[LC_PIECE_NAME_LEN];
GetMeshName(ModelPart, Name);
snprintf(Line, sizeof(Line), " object {\n lc_%s%s\n %s { %s }\n matrix <%g, %g, %g, %g, %g, %g, %g, %g, %g, %g, %g, %g>\n }\n",
Name, Suffix, Modifier, ColorTable[ColorIdx].data(), -f[5], -f[4], -f[6], -f[1], -f[0], -f[2], f[9], f[8], f[10], f[13] / 25.0f, f[12] / 25.0f, f[14] / 25.0f);
}
POVFile.WriteLine(Line);
}
snprintf(Line, sizeof(Line), "\n #if (ModelReflection = 0)\n no_reflection\n #end\n #if (ModelShadow = 0)\n no_shadow\n #end\n}\n\n");
POVFile.WriteLine(Line);
snprintf(Line, sizeof(Line), "object {\n %s\n %s { %s }\n}\n\n",
TopModelName.toLatin1().constData(), (UseLGEO ? "material" : "texture"), ColorTable[lcGetColorIndex(TopModelColorCode)].data());
POVFile.WriteLine(Line);
snprintf(Line, sizeof(Line), "#if (Floor)\n object {\n plane { FloorAxis, FloorLocation hollow }\n texture {\n pigment { color srgb FloorColor }\n finish { emission 0 ambient FloorAmbient diffuse FloorDiffuse }\n }\n }\n#end\n");
POVFile.WriteLine(Line);
if (!POVRayOptions.FooterIncludeFile.isEmpty())
{
snprintf(Line, sizeof(Line), "\n#include \"%s\"\n", POVRayOptions.FooterIncludeFile.toLatin1().constData());
POVFile.WriteLine(Line);
}
return lcResult<void>();
}
lcResult<void> Project::ExportWavefront(const QString& FileName)
{
const lcResult<void> Ready = EnsureAssetsReady();
if (!Ready)
return Ready;
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."));
QString SaveFileName = GetExportFileName(FileName, QLatin1String("obj"), tr("Export Wavefront"), tr("Wavefront Files (*.obj);;All Files (*.*)"));
if (SaveFileName.isEmpty())
return lcUnexpected(QString());
lcDiskFile OBJFile(SaveFileName);
char Line[1024];
if (!OBJFile.Open(QIODevice::WriteOnly))
return lcUnexpected(tr("Could not open file '%1' for writing.").arg(SaveFileName));
quint32 vert = 1;
OBJFile.WriteLine("# Model exported from LeoCAD\n");
QFileInfo SaveInfo(SaveFileName);
QString MaterialFileName = QDir(SaveInfo.absolutePath()).absoluteFilePath(SaveInfo.completeBaseName() + QLatin1String(".mtl"));
snprintf(Line, sizeof(Line), "#\n\nmtllib %s\n\n", QFileInfo(MaterialFileName).fileName().toLatin1().constData());
OBJFile.WriteLine(Line);
lcDiskFile MaterialFile(MaterialFileName);
if (!MaterialFile.Open(QIODevice::WriteOnly))
return lcUnexpected(tr("Could not open file '%1' for writing.").arg(MaterialFileName));
MaterialFile.WriteLine("# Colors used by LeoCAD\n\n");
for (const lcColor& Color : gColorList)
{
if (Color.Translucent)
snprintf(Line, sizeof(Line), "newmtl %s\nKd %.2f %.2f %.2f\nD %.2f\n\n", Color.SafeName, Color.Value[0], Color.Value[1], Color.Value[2], Color.Value[3]);
else
snprintf(Line, sizeof(Line), "newmtl %s\nKd %.2f %.2f %.2f\n\n", Color.SafeName, Color.Value[0], Color.Value[1], Color.Value[2]);
MaterialFile.WriteLine(Line);
}
for (const lcModelPartsEntry& ModelPart : ModelParts)
{
lcMesh* Mesh = !ModelPart.Mesh ? ModelPart.Info->GetMesh() : ModelPart.Mesh;
if (!Mesh)
continue;
const lcMatrix44& ModelWorld = ModelPart.WorldMatrix;
lcVertex* Verts = (lcVertex*)Mesh->mVertexData;
for (int VertexIdx = 0; VertexIdx < Mesh->mNumVertices; VertexIdx++)
{
lcVector3 Vertex = lcMul31(Verts[VertexIdx].Position, ModelWorld);
snprintf(Line, sizeof(Line), "v %.2f %.2f %.2f\n", Vertex[0], Vertex[1], Vertex[2]);
OBJFile.WriteLine(Line);
}
OBJFile.WriteLine("#\n\n");
}
for (const lcModelPartsEntry& ModelPart : ModelParts)
{
lcMesh* Mesh = !ModelPart.Mesh ? ModelPart.Info->GetMesh() : ModelPart.Mesh;
if (!Mesh)
continue;
const lcMatrix44& ModelWorld = ModelPart.WorldMatrix;
lcVertex* Verts = (lcVertex*)Mesh->mVertexData;
for (int VertexIdx = 0; VertexIdx < Mesh->mNumVertices; VertexIdx++)
{
lcVector3 Normal = lcMul30(lcUnpackNormal(Verts[VertexIdx].Normal), ModelWorld);
snprintf(Line, sizeof(Line), "vn %.2f %.2f %.2f\n", Normal[0], Normal[1], Normal[2]);
OBJFile.WriteLine(Line);
}
OBJFile.WriteLine("#\n\n");
}
int NumPieces = 0;
for (const lcModelPartsEntry& ModelPart : ModelParts)
{
snprintf(Line, sizeof(Line), "g Piece%.3d\n", NumPieces++);
OBJFile.WriteLine(Line);
lcMesh* Mesh = !ModelPart.Mesh ? ModelPart.Info->GetMesh() : ModelPart.Mesh;
if (Mesh)
{
Mesh->ExportWavefrontIndices(OBJFile, ModelPart.ColorIndex, vert);
vert += Mesh->mNumVertices;
}
}
return lcResult<void>();
}
lcResult<void> Project::SaveImage(const lcImageDialogOptions& Options)
{
QString FilePath = Options.FilePath;
QString Extension = QFileInfo(FilePath).suffix();
if (!Extension.isEmpty())
lcSetProfileString(LC_PROFILE_IMAGE_EXTENSION, FilePath.right(Extension.length() + 1));
if (Options.Start != Options.End)
FilePath = FilePath.insert(FilePath.length() - Extension.length() - 1, QLatin1String("%1"));
return mActiveModel->SaveStepImages(FilePath, Options.Start != Options.End, false, Options.Width, Options.Height, Options.Start, Options.End);
}
void Project::UpdatePieceInfo(PieceInfo* Info) const
{
if (!mModels.empty())
{
std::vector<lcModel*> UpdatedModels;
mModels[0]->UpdatePieceInfo(UpdatedModels);
lcBoundingBox BoundingBox = mModels[0]->GetPieceInfo()->GetBoundingBox();
Info->SetBoundingBox(BoundingBox.Min, BoundingBox.Max);
}
}
void Project::MarkAsModified()
{
mModified = true;
}