Files
leocad/common/project.cpp
T
Leonardo Zide 513e556ce9 Standardize model terminology and command descriptions
Use model and submodel terminology in user-facing help, errors, and the Windows file-type description. Standardize keyframe and spotlight text across menus, properties, undo history, and default light names while preserving internal identifiers and file-format tokens.

Correct the directional-light and movement-snapping descriptions, remove obsolete Ctrl advice from Zoom Extents, and fix deselection and rotation wording. Leave translation catalogs unchanged for the deferred catalog update.

Validation: debug build passes without warnings; UI XML and string-only diff checks pass.
2026-10-22 22:08:53 -07:00

4158 lines
131 KiB
C++

#include "lc_global.h"
#include "lc_math.h"
#include "lc_mesh.h"
#include "lc_previewwidget.h"
#include "piece.h"
#include "lc_piecedata.h"
#include "camera.h"
#include "project.h"
#include "lc_instructions.h"
#include "light.h"
#include "lc_mainwindow.h"
#include "lc_view.h"
#include "lc_library.h"
#include "lc_texture.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->CreatePieceData(this);
mActiveModel->SetSaved();
mModels.emplace_back(mActiveModel);
if (!mIsPreview)
{
mFileCheckTimer.setSingleShot(true);
mFileCheckTimer.setInterval(150);
QObject::connect(&mFileWatcher, &QFileSystemWatcher::fileChanged, &mFileWatcher, [this]() { mFileCheckTimer.start(); });
QObject::connect(&mFileWatcher, &QFileSystemWatcher::directoryChanged, &mFileWatcher, [this]() { mFileCheckTimer.start(); });
QObject::connect(&mFileCheckTimer, &QTimer::timeout, &mFileWatcher, [this]()
{
const QString Path = mFileName;
if (HasFileChanged(Path) && gMainWindow)
gMainWindow->ProjectFileChanged(Path);
});
}
}
Project::~Project()
{
mModels.clear();
lcGetPiecesLibrary()->ReleaseProjectPieceData(this);
}
lcPieceData* Project::FindPieceData(const std::string& Name) const
{
const std::map<std::string, lcPieceData*>::const_iterator It = mPieceIndex.find(Name);
return It == mPieceIndex.end() ? nullptr : It->second;
}
void Project::RegisterPieceData(const std::string& Name, lcPieceData* PieceData)
{
mPieceIndex[Name] = PieceData;
}
void Project::UnregisterPieceData(lcPieceData* PieceData)
{
for (std::map<std::string, lcPieceData*>::iterator It = mPieceIndex.begin(); It != mPieceIndex.end();)
{
if (It->second == PieceData)
It = mPieceIndex.erase(It);
else
It++;
}
}
void Project::ClearPieceDataIndex()
{
mPieceIndex.clear();
}
void Project::TransferPieceDataIndexTo(Project* Destination)
{
for (const std::map<std::string, lcPieceData*>::value_type& Entry : mPieceIndex)
{
if (!Destination->FindPieceData(Entry.first))
Destination->RegisterPieceData(Entry.first, Entry.second);
}
mPieceIndex.clear();
}
bool Project::CanShareMergePieceData(const lcPieceData* Existing, const lcPieceData* Incoming)
{
if (Existing == Incoming)
return true;
if (Existing->IsLibraryPiece() || Incoming->IsLibraryPiece() || Existing->IsModel() || Incoming->IsModel())
return false;
// The callers have already matched filenames. Two missing part references
// can share an identity, while failed library parts and models cannot.
if (!Existing->IsProject() && !Incoming->IsProject())
return Existing->mState == lcAssetState::Failed && Incoming->mState == lcAssetState::Failed;
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->UpdatePieceData(UpdatedModels);
}
mActiveModel = ActiveModel;
if (!mIsPreview && gMainWindow && UpdateInterface)
{
gMainWindow->SetCurrentModelTab(mActiveModel, true);
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, DialogTitle, 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, DialogTitle, tr("A submodel named '%1' already exists. Please enter a unique name.").arg(Name));
continue;
}
if (!CurrentName.isEmpty())
{
const std::string PieceName = lcPiecesLibrary::NormalizePieceDataName(Name.toLatin1().constData());
lcPieceData* ExistingPiece = FindPieceData(PieceName);
lcModel* CurrentModel = GetModel(CurrentName);
if (ExistingPiece && (!CurrentModel || ExistingPiece != CurrentModel->GetPieceData()) &&
(!ExistingPiece->IsModel() || ExistingModels.contains(Name, Qt::CaseInsensitive)))
{
QMessageBox::information(ParentWidget, DialogTitle, tr("A piece named '%1' already exists in this model. 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->CreatePieceData(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<lcPieceData*, lcPieceData*>> 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::NormalizePieceDataName(Entry.Name.toLatin1().constData());
if (!FinalNames.insert(Name).second)
{
QMessageBox::information(gMainWindow, tr("Submodels"), tr("A piece named '%1' already exists in this model.").arg(Entry.Name));
return;
}
if (!Entry.ExistingModel || Entry.ExistingModel->GetFileName() == Entry.Name)
continue;
lcPieceData* Occupant = FindPieceData(Name);
if (!Occupant || Occupant == Entry.ExistingModel->GetPieceData())
continue;
const bool Vacated = std::any_of(mModels.begin(), mModels.end(), [&Results, Occupant, &Name](const std::unique_ptr<lcModel>& OldModel)
{
if (OldModel->GetPieceData() != 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::NormalizePieceDataName(Result->Name.toLatin1().constData()) != Name;
});
if (!Vacated)
{
QMessageBox::information(gMainWindow, tr("Submodels"), tr("A piece named '%1' already exists in this model.").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())
UnregisterPieceData(Model->GetPieceData());
continue;
}
const std::string OldName = lcPiecesLibrary::NormalizePieceDataName(Model->GetFileName().toLatin1().constData());
for (const lcModelListDialogEntry& Entry : Results)
{
if (lcPiecesLibrary::NormalizePieceDataName(Entry.Name.toLatin1().constData()) != OldName)
continue;
// A new model can reuse this lcPieceData. A renamed model keeps its own
// lcPieceData, so references to the deleted model are rebound below.
lcGetPiecesLibrary()->InvalidatePieceDataLoad(OldModel->GetPieceData());
OldModel->GetPieceData()->DetachContainer();
OldModel->GetPieceData()->SetFailed(tr("Model '%1' was deleted.").arg(Model->GetFileName()));
if (Entry.ExistingModel)
{
UnregisterPieceData(OldModel->GetPieceData());
Replacements.emplace_back(OldModel->GetPieceData(), Entry.ExistingModel->GetPieceData());
}
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()->RenameModelPieceData(Model->GetPieceData(), Entry.Name.toLatin1().constData()))
{
QMessageBox::information(gMainWindow, tr("Submodels"), tr("A piece named '%1' already exists in this model.").arg(Entry.Name));
return;
}
Model->SetFileName(Entry.Name);
Renames.push_back({ Model->GetPieceData(), 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->CreatePieceData(this);
Model->SetSaved();
mModified = true;
}
NewModels.emplace_back(Model);
}
for (const std::pair<lcPieceData*, lcPieceData*>& Replacement : Replacements)
{
for (const std::unique_ptr<lcModel>& Model : NewModels)
{
if (Model->RebindPieceData(Replacement.first, Replacement.second) && std::find(ChangedModels.begin(), ChangedModels.end(), Model.get()) == ChangedModels.end())
ChangedModels.push_back(Model.get());
}
if (gMainWindow->GetInsertPieceData() == Replacement.first)
gMainWindow->SetInsertPieceData(Replacement.second);
gMainWindow->GetPreviewWidget()->RebindPieceData(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();
}
Project::FileState Project::ReadFileState(const QString& FileName)
{
FileState State;
State.Exists = QFileInfo(FileName).isFile();
if (!State.Exists)
return State;
QFile File(FileName);
QCryptographicHash Hash(QCryptographicHash::Sha256);
State.Readable = File.open(QIODevice::ReadOnly) && Hash.addData(&File);
if (State.Readable)
State.Hash = Hash.result();
return State;
}
bool Project::HasFileChanged(const QString& Path)
{
if (mIsPreview || this != lcGetActiveProject() || Path.isEmpty() || Path != mFileName)
return false;
UpdateFileWatcher();
const FileState State = ReadFileState(Path);
if (State.Exists && !State.Readable && ++mFileReadAttempts < 3)
{
mFileCheckTimer.start();
return false;
}
mFileReadAttempts = 0;
if (State.Hash == mFileHash && State.Exists == mFileExists && State.Readable == mFileReadable)
return false;
// Remember the observed contents so duplicate events do not prompt again.
mFileHash = State.Hash;
mFileExists = State.Exists;
mFileReadable = State.Readable;
return true;
}
void Project::UpdateFileWatcher()
{
QStringList Files;
QStringList Directories;
const bool Active = !mIsPreview && this == lcGetActiveProject() && !mFileName.isEmpty();
const bool WasWatching = !mFileWatcher.files().isEmpty() || !mFileWatcher.directories().isEmpty();
if (Active)
{
if (QFileInfo(mFileName).isFile())
Files.push_back(mFileName);
QDir Directory(QFileInfo(mFileName).absolutePath());
while (!Directory.exists())
{
const QString Parent = QFileInfo(Directory.absolutePath()).absolutePath();
if (Parent == Directory.absolutePath())
break;
Directory.setPath(Parent);
}
if (Directory.exists())
Directories.push_back(Directory.absolutePath());
}
for (const QString& Path : mFileWatcher.files())
if (!Files.contains(Path))
mFileWatcher.removePath(Path);
for (const QString& Path : mFileWatcher.directories())
if (!Directories.contains(Path))
mFileWatcher.removePath(Path);
for (const QString& Path : Files)
if (!mFileWatcher.files().contains(Path))
mFileWatcher.addPath(Path);
for (const QString& Path : Directories)
if (!mFileWatcher.directories().contains(Path))
mFileWatcher.addPath(Path);
if (!Active)
mFileCheckTimer.stop();
else if (!WasWatching)
mFileCheckTimer.start();
}
void Project::SetFileName(const QString& FileName)
{
if (mFileName == FileName)
return;
mFileName = FileName;
UpdateFileWatcher();
}
bool lcEmbeddedData::HasValidEncoding() const
{
const QByteArray& Encoded = EncodedData;
int Padding = 0;
bool Valid = !Encoded.isEmpty();
for (const unsigned char Character : Encoded)
{
if (Character == '=')
Padding++;
else if (Padding || !((Character >= 'A' && Character <= 'Z') || (Character >= 'a' && Character <= 'z') ||
(Character >= '0' && Character <= '9') || Character == '+' || Character == '/'))
{
Valid = false;
break;
}
}
return Valid && Padding <= 2 && Encoded.size() % 4 != 1 && (!Padding || Encoded.size() % 4 == 0);
}
QString Project::NormalizeEmbeddedDataName(const QString& FileName)
{
QString Name = FileName;
Name.replace(QLatin1Char('\\'), QLatin1Char('/'));
return QDir::cleanPath(Name).toUpper();
}
std::shared_ptr<const lcEmbeddedData> Project::FindEmbeddedData(const QString& FileName) const
{
if (FileName.isEmpty())
return std::shared_ptr<const lcEmbeddedData>();
const auto Entry = mEmbeddedDataIndex.find(NormalizeEmbeddedDataName(FileName));
if (Entry == mEmbeddedDataIndex.end())
return std::shared_ptr<const lcEmbeddedData>();
return Entry->second;
}
void Project::LoadLDrawDocument(const QByteArray& FileData, const Project* ReferenceProject, bool ReuseExistingModels)
{
enum class BlockType { ImplicitModel, Model, Data, Ignore };
struct ModelBlock
{
QString FileName;
qint64 Begin;
qint64 End;
size_t LayoutIndex;
};
std::vector<ModelBlock> ModelBlocks;
BlockType Type = BlockType::ImplicitModel;
qint64 BlockBegin = 0;
QString ModelName;
bool HasImplicitContent = false;
bool HasImplicitGeometry = false;
std::shared_ptr<lcEmbeddedData> Data;
const auto FinishBlock = [&](qint64 End, bool KeepImplicit)
{
DocumentBlock Block;
Block.Raw = FileData.mid(BlockBegin, End - BlockBegin);
if (Type == BlockType::Model)
{
ModelBlocks.push_back({ ModelName, BlockBegin, End, mDocumentBlocks.size() });
Block.ExplicitModel = true;
Block.OriginalName = ModelName;
}
else if (Type == BlockType::ImplicitModel && KeepImplicit && HasImplicitContent)
{
const QString Name = QFileInfo(mFileName).fileName();
ModelBlocks.push_back({ Name, BlockBegin, End, mDocumentBlocks.size() });
Block.OriginalName = Name;
}
else if (Type == BlockType::Data)
{
Data->RawBlock = Block.Raw;
if (Data->FileName.isEmpty())
Data->Error = tr("Embedded data has no filename.");
else
mEmbeddedDataIndex.emplace(NormalizeEmbeddedDataName(Data->FileName), Data);
Block.Data = Data;
mEmbeddedData.push_back(Data);
Data.reset();
}
mDocumentBlocks.push_back(std::move(Block));
};
QBuffer Buffer;
Buffer.setData(FileData);
Buffer.open(QIODevice::ReadOnly);
while (!Buffer.atEnd())
{
const qint64 Pos = Buffer.pos();
const QByteArray OriginalLine = Buffer.readLine();
QString Line = QString::fromUtf8(OriginalLine).trimmed();
QTextStream LineStream(&Line, QIODevice::ReadOnly);
QString LineType;
QString Meta;
LineStream >> LineType >> Meta;
if (LineType == QLatin1String("0") && Meta == QLatin1String("FILE"))
{
// Preamble before an explicit first FILE is not part of that model.
FinishBlock(Pos, false);
Type = BlockType::Model;
BlockBegin = Pos;
ModelName = LineStream.readAll().trimmed();
}
else if (LineType == QLatin1String("0") && Meta == QLatin1String("!DATA"))
{
// Also accept a plain LDR model followed by embedded data.
FinishBlock(Pos, HasImplicitGeometry);
Type = BlockType::Data;
BlockBegin = Pos;
Data = std::make_shared<lcEmbeddedData>();
Data->FileName = LineStream.readAll().trimmed();
}
else if (LineType == QLatin1String("0") && Meta == QLatin1String("NOFILE"))
{
FinishBlock(Pos, true);
Type = BlockType::Ignore;
BlockBegin = Pos;
}
else if (Type == BlockType::Data)
{
if (LineType == QLatin1String("0") && Meta == QLatin1String("!:"))
Data->EncodedData += LineStream.readAll().trimmed().toLatin1();
else if (!Line.isEmpty() && Data->Error.isEmpty())
Data->Error = tr("Unexpected statement in embedded data '%1'.").arg(Data->FileName);
}
else if (Type == BlockType::ImplicitModel && !Line.isEmpty())
{
HasImplicitContent = true;
int GeometryType = LineType.toInt();
if (LineType == QLatin1String("0") && Meta == QLatin1String("!:"))
{
QString Token;
LineStream >> Token;
GeometryType = Token.toInt();
}
HasImplicitGeometry |= GeometryType >= 1 && GeometryType <= 5;
}
}
FinishBlock(Buffer.pos(), true);
// Preserve the existing ability to open an empty ordinary LDraw document.
if (ModelBlocks.empty() && mEmbeddedData.empty())
ModelBlocks.push_back({ QFileInfo(mFileName).fileName(), 0, 0, 0 });
std::vector<std::pair<const ModelBlock*, lcModel*>> Models;
for (const ModelBlock& Block : ModelBlocks)
{
const auto Existing = std::find_if(mModels.begin(), mModels.end(), [&Block](const std::unique_ptr<lcModel>& Model)
{
return Model->GetFileName().compare(Block.FileName, Qt::CaseInsensitive) == 0;
});
if (Existing != mModels.end())
{
AddLoadWarning(tr("Duplicate model '%1' was ignored; model names are case-insensitive.").arg(Block.FileName));
mDocumentBlocks[Block.LayoutIndex].Model = Existing->get();
mDocumentBlocks[Block.LayoutIndex].DuplicateModel = true;
continue;
}
if (ReuseExistingModels && ReferenceProject && !Models.empty())
{
const std::string Name = lcPiecesLibrary::NormalizePieceDataName(Block.FileName.toLatin1().constData());
lcPieceData* ExistingPieceData = ReferenceProject->FindPieceData(Name);
if (ExistingPieceData && (ExistingPieceData->IsModel() || ExistingPieceData->IsProject()))
{
RegisterPieceData(Name, ExistingPieceData);
continue;
}
}
std::unique_ptr<lcModel> Model(new lcModel(Block.FileName, this, mIsPreview));
Models.emplace_back(&Block, Model.get());
mModels.emplace_back(std::move(Model));
mModels.back()->CreatePieceData(this);
mDocumentBlocks[Block.LayoutIndex].Model = mModels.back().get();
mDocumentBlocks[Block.LayoutIndex].OriginalName = Block.FileName;
}
// Legacy clipboard references can use destination pieces when no definition was copied.
if (ReferenceProject)
for (const auto& [Name, PieceData] : ReferenceProject->mPieceIndex)
if (!FindPieceData(Name))
RegisterPieceData(Name, PieceData);
// Every model and asset is registered before geometry resolves references.
for (const std::pair<const ModelBlock*, lcModel*>& Entry : Models)
{
QBuffer ModelBuffer;
ModelBuffer.setData(FileData.mid(Entry.first->Begin, Entry.first->End - Entry.first->Begin));
ModelBuffer.open(QIODevice::ReadOnly);
Entry.second->LoadLDraw(ModelBuffer, this);
Entry.second->SetSaved();
}
for (DocumentBlock& Block : mDocumentBlocks)
if (Block.Model)
Block.Baseline = SerializeModelText(Block.Model, false, 0);
}
QByteArray Project::SerializeModelText(const lcModel* Model, bool SelectedOnly, lcStep LastStep)
{
QByteArray Bytes;
QTextStream Stream(&Bytes, QIODevice::WriteOnly);
Model->SaveLDraw(Stream, SelectedOnly, LastStep);
Stream.flush();
return Bytes;
}
QByteArray Project::SerializeStoredModel(const lcModel* Model, const QString& Name, const DocumentBlock* Original, bool NeedsHeader, bool SelectedOnly, lcStep LastStep)
{
const QByteArray Current = SerializeModelText(Model, SelectedOnly, LastStep);
const bool Preserve = Original && !SelectedOnly && !LastStep && Current == Original->Baseline;
QByteArray LineEnding("\r\n");
if (Original && Original->Raw.contains('\n') && !Original->Raw.contains("\r\n"))
LineEnding = "\n";
if (Preserve && Original->ExplicitModel && Original->OriginalName == Name)
return Original->Raw;
QByteArray Text;
const bool Explicit = Original && Original->ExplicitModel;
const bool Renamed = Original && Original->OriginalName != Name;
if (NeedsHeader || Explicit || Renamed)
{
const int HeaderEnd = Original ? Original->Raw.indexOf('\n') : -1;
if (Explicit && !Renamed && HeaderEnd != -1)
Text = Original->Raw.left(HeaderEnd + 1);
else
Text = "0 FILE " + Name.toUtf8() + LineEnding;
}
if (Preserve)
{
if (Explicit)
{
const int HeaderEnd = Original->Raw.indexOf('\n');
Text += Original->Raw.mid(HeaderEnd == -1 ? Original->Raw.size() : HeaderEnd + 1);
}
else
Text += Original->Raw;
}
else
{
QByteArray Body = Current;
if (LineEnding == "\n")
Body.replace("\r\n", "\n");
if (Original && !Original->Raw.endsWith('\n') && !Original->Raw.endsWith('\r') && Body.endsWith(LineEnding))
Body.chop(LineEnding.size());
Text += Body;
}
return Text;
}
bool Project::WriteRaw(QTextStream& Stream, const QByteArray& Bytes)
{
Stream.flush();
if (Stream.status() != QTextStream::Ok)
return false;
if (QIODevice* Device = Stream.device())
return Device->write(Bytes) == Bytes.size();
Stream << QString::fromUtf8(Bytes);
return Stream.status() == QTextStream::Ok;
}
bool Project::SameEmbeddedData(const lcEmbeddedData& First, const lcEmbeddedData& Second)
{
if (First.Error != Second.Error)
return false;
if (First.EncodedData == Second.EncodedData)
return true;
if (!First.Error.isEmpty() || !First.HasValidEncoding() || !Second.HasValidEncoding())
return false;
int FirstSize = First.EncodedData.size();
int SecondSize = Second.EncodedData.size();
while (FirstSize && First.EncodedData[FirstSize - 1] == '=')
FirstSize--;
while (SecondSize && Second.EncodedData[SecondSize - 1] == '=')
SecondSize--;
return FirstSize == SecondSize && !memcmp(First.EncodedData.constData(), Second.EncodedData.constData(), FirstSize);
}
void Project::AddLoadWarning(const QString& Warning)
{
AddLoadWarning(LoadWarning{ mFileName.isEmpty() ? GetTitle() : mFileName, Warning });
}
void Project::AddLoadWarning(const LoadWarning& Warning)
{
const auto Existing = std::find_if(mLoadWarnings.begin(), mLoadWarnings.end(), [&Warning](const LoadWarning& Current)
{
return Current.FileName == Warning.FileName && Current.Message == Warning.Message;
});
if (Existing == mLoadWarnings.end())
mLoadWarnings.push_back(Warning);
}
std::vector<QString> Project::GetLoadWarningDetails() const
{
std::vector<QString> Details;
std::set<QString> SeenWarnings;
std::vector<const Project*> Projects{ this };
std::unordered_set<const Project*> SeenProjects{ this };
for (size_t ProjectIndex = 0; ProjectIndex < Projects.size(); ProjectIndex++)
{
const Project* Current = Projects[ProjectIndex];
for (const LoadWarning& Warning : Current->mLoadWarnings)
{
const QString Detail = QStringLiteral("'%1': %2").arg(QDir::toNativeSeparators(Warning.FileName), Warning.Message);
if (SeenWarnings.insert(Detail).second)
Details.push_back(Detail);
}
for (const std::unique_ptr<lcModel>& Model : Current->mModels)
for (const std::unique_ptr<lcPiece>& Piece : Model->GetPieces())
{
const lcPieceData* PieceData = Piece->mPieceData.Get();
if (!PieceData || !PieceData->IsProject())
continue;
const Project* Referenced = PieceData->GetProject();
if (Referenced && SeenProjects.insert(Referenced).second)
Projects.push_back(Referenced);
}
}
return Details;
}
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("Load Model"), tr("Error reading file '%1':\n%2").arg(FileName, File.errorString()));
return false;
}
QByteArray FileData = File.readAll();
if (File.error() != QFileDevice::NoError)
{
if (ShowErrors)
QMessageBox::warning(parent, tr("Load Model"), tr("Error reading file '%1':\n%2").arg(FileName, File.errorString()));
return false;
}
mModels.clear();
mDocumentBlocks.clear();
mLoadWarnings.clear();
mEmbeddedData.clear();
mEmbeddedDataIndex.clear();
lcGetPiecesLibrary()->ReleaseProjectPieceData(this);
SetFileName(FileName);
QFileInfo FileInfo(FileName);
QString Extension = FileInfo.suffix().toLower();
if (!mIsPreview)
{
mFileHash = QCryptographicHash::hash(FileData, QCryptographicHash::Sha256);
mFileExists = true;
mFileReadable = true;
mFileReadAttempts = 0;
}
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)
{
LoadLDrawDocument(FileData, nullptr, false);
}
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->CreatePieceData(this);
Model->SetSaved();
}
else
delete Model;
}
if (mModels.empty())
{
if (ShowErrors)
QMessageBox::warning(parent, tr("Load Model"), 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()->RenameModelPieceData(Model->GetPieceData(), ModelName.toLatin1().constData()))
{
const QString FileExtension = FileInfo.suffix().isEmpty() ? QString() : QLatin1Char('.') + FileInfo.suffix();
const QString NameSuffix = QLatin1Char(' ') + QString::number(Suffix++) + FileExtension;
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)
{
lcPieceData* PieceData = Model->GetPieceData();
PieceData->ReleaseMesh();
lcGetPiecesLibrary()->InvalidatePieceDataLoad(PieceData);
}
else
{
if (mDeferModelMeshRequests)
lcGetPiecesLibrary()->InvalidatePieceDataLoad(Model->GetPieceData());
Model->QueueMeshBuild();
}
}
if (!mIsPreview)
{
for (const std::unique_ptr<lcModel>& Model : mModels)
Model->UpdatePieceData(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()));
}
if (!mIsPreview)
{
const FileState State = ReadFileState(FileName);
mFileHash = State.Hash;
mFileExists = State.Exists;
mFileReadable = State.Readable;
mFileReadAttempts = 0;
}
SetFileName(FileName);
if (DirectoryChanged)
{
lcPiecesLibrary* Library = lcGetPiecesLibrary();
std::vector<lcPieceDataRef> LocalPieceData;
std::vector<lcPieceData*> ChangedPieces;
for (const std::map<std::string, lcPieceData*>::value_type& Entry : mPieceIndex)
{
lcPieceData* PieceData = Entry.second;
if (PieceData->IsModel())
continue;
LocalPieceData.push_back(lcPieceDataRef(PieceData));
}
for (const lcPieceDataRef& LocalData : LocalPieceData)
if (Library->RemapProjectPieceData(LocalData.Get(), DestinationDirectory, mIsPreview))
ChangedPieces.push_back(LocalData.Get());
for (const std::unique_ptr<lcModel>& Model : mModels)
{
if (!Model->HasDirectGeometry())
continue;
Library->InvalidatePieceDataLoad(Model->GetPieceData());
Model->QueueMeshBuild();
ChangedPieces.push_back(Model->GetPieceData());
}
if (!ChangedPieces.empty())
{
std::vector<lcModel*> UpdatedModels;
for (const std::unique_ptr<lcModel>& Model : mModels)
{
Model->UpdatePieceData(UpdatedModels);
ChangedPieces.push_back(Model->GetPieceData());
}
Library->GetThumbnailManager()->RefreshPieces(ChangedPieces);
lcView::UpdateProjectViews(this);
if (!mIsPreview && gMainWindow && gMainWindow->GetPreviewWidget())
gMainWindow->GetPreviewWidget()->RefreshAfterAssetChange(ChangedPieces);
}
}
for (const std::unique_ptr<lcModel>& Model : mModels)
Model->SetSaved();
mModified = false;
return lcResult<void>();
}
bool Project::Save(QTextStream& Stream) const
{
const bool MPD = mModels.size() > 1 || !mEmbeddedData.empty();
std::set<const lcModel*> WrittenModels;
std::set<const lcEmbeddedData*> WrittenData;
std::vector<const lcModel*> OriginalOrder;
const auto IsPresent = [this](const lcModel* Model)
{
return std::any_of(mModels.begin(), mModels.end(), [Model](const std::unique_ptr<lcModel>& Entry) { return Entry.get() == Model; });
};
for (const DocumentBlock& Block : mDocumentBlocks)
if (Block.Model && !Block.DuplicateModel && IsPresent(Block.Model))
OriginalOrder.push_back(Block.Model);
bool Reordered = false;
for (size_t Index = 0; Index < OriginalOrder.size(); Index++)
Reordered |= OriginalOrder[Index] != mModels[Index].get();
const auto OriginalFor = [this](const lcModel* Model) -> const DocumentBlock*
{
for (const DocumentBlock& Block : mDocumentBlocks)
if (Block.Model == Model && !Block.DuplicateModel)
return &Block;
return nullptr;
};
QByteArray Output;
const auto Append = [&Output](const QByteArray& Bytes, bool NewBlock)
{
if (NewBlock && !Output.isEmpty() && !Output.endsWith('\n') && !Output.endsWith('\r'))
Output += "\r\n";
Output += Bytes;
};
size_t NextModel = 0;
for (const DocumentBlock& Block : mDocumentBlocks)
{
if (Block.Model)
{
if (!IsPresent(Block.Model))
continue;
if (Block.DuplicateModel)
{
if (WrittenModels.find(Block.Model) != WrittenModels.end() && Block.Model->GetFileName().compare(Block.OriginalName, Qt::CaseInsensitive) == 0)
Append(Block.Raw, true);
continue;
}
const lcModel* Model = Reordered ? mModels[NextModel++].get() : Block.Model;
Append(SerializeStoredModel(Model, Model->GetFileName(), OriginalFor(Model), mModels.size() > 1, false, 0), true);
WrittenModels.insert(Model);
}
else if (Block.Data)
{
Append(Block.Raw, true);
WrittenData.insert(Block.Data.get());
}
else
Append(Block.Raw, false);
}
for (const std::unique_ptr<lcModel>& Model : mModels)
{
if (WrittenModels.find(Model.get()) != WrittenModels.end())
continue;
Append(SerializeStoredModel(Model.get(), Model->GetFileName(), OriginalFor(Model.get()), MPD || !mDocumentBlocks.empty(), false, 0), true);
}
for (const std::shared_ptr<const lcEmbeddedData>& Data : mEmbeddedData)
if (WrittenData.find(Data.get()) == WrittenData.end())
Append(Data->RawBlock, true);
return WriteRaw(Stream, Output);
}
std::vector<QString> Project::GetMergedAssetDirectories(const lcModel* Model) const
{
if (Model->GetProject() == this)
return Model->GetAssetSearchDirectories();
lcModelAssetSourceState State = Model->GetAssetSourceState();
const QString SourceFile = Model->GetProject()->GetFileName();
const QString SourceDirectory = SourceFile.isEmpty() ? QString() : QFileInfo(SourceFile).absolutePath();
if (!SourceDirectory.isEmpty())
{
State.Directories.erase(std::remove(State.Directories.begin(), State.Directories.end(), SourceDirectory), State.Directories.end());
if (State.PreferCurrentDirectory)
State.Directories.insert(State.Directories.begin(), SourceDirectory);
else
State.Directories.push_back(SourceDirectory);
}
const QString DestinationDirectory = mFileName.isEmpty() ? QString() : QFileInfo(mFileName).absolutePath();
if (!DestinationDirectory.isEmpty() && std::find(State.Directories.begin(), State.Directories.end(), DestinationDirectory) == State.Directories.end())
State.Directories.push_back(DestinationDirectory);
return State.Directories;
}
lcResult<void> Project::ValidateMergedTextureBindings(const Project& Source, const std::map<QString, std::shared_ptr<const lcEmbeddedData>>& MergedData, const lcModel* ClipboardRoot) const
{
lcPiecesLibrary* Library = lcGetPiecesLibrary();
for (const std::unique_ptr<lcModel>& Model : Source.mModels)
{
if (Model.get() == ClipboardRoot)
continue;
std::vector<std::string> References = lcMeshLoader::GetTextureReferences(SerializeModelText(Model.get(), false, 0));
if (const lcMesh* Mesh = Model->GetPieceData()->GetMesh())
for (const lcMeshLod& Lod : Mesh->mLods)
for (int SectionIndex = 0; SectionIndex < Lod.NumSections; SectionIndex++)
if (!Lod.Sections[SectionIndex].TextureName.isEmpty())
References.push_back(Lod.Sections[SectionIndex].TextureName.toLatin1().constData());
const std::vector<QString> BeforeDirectories = Model->GetAssetSearchDirectories();
const std::vector<QString> AfterDirectories = GetMergedAssetDirectories(Model.get());
for (const std::string& Name : References)
{
lcFileSource BeforeSource;
lcTextureRef Before = Library->AcquireTexture(Name.c_str(), BeforeDirectories, Source.mEmbeddedDataIndex, BeforeSource);
if (!Before)
continue;
lcFileSource AfterSource;
lcTextureRef After = Library->AcquireTexture(Name.c_str(), AfterDirectories, MergedData, AfterSource);
bool Same = bool(After);
if (Same && BeforeSource.EmbeddedData && AfterSource.EmbeddedData)
Same = SameEmbeddedData(*BeforeSource.EmbeddedData, *AfterSource.EmbeddedData);
else if (Same)
Same = BeforeSource == AfterSource;
if (!Same)
return lcUnexpected(tr("Merging would change texture '%1' used by model '%2'. Rename the conflicting texture before merging.").arg(QString::fromLatin1(Name.c_str()), Model->GetFileName()));
}
}
return lcResult<void>();
}
lcResult<void> Project::Merge(const std::vector<Project*>& Sources)
{
return MergeInternal(Sources, nullptr);
}
lcResult<void> Project::MergeInternal(const std::vector<Project*>& Sources, const lcModel* ClipboardRoot)
{
lcPiecesLibrary* Library = lcGetPiecesLibrary();
std::map<std::string, lcPieceData*> 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;
std::map<QString, std::shared_ptr<const lcEmbeddedData>> MergedData = mEmbeddedDataIndex;
// Validate every incoming asset before reserving names or moving models.
for (const Project* Source : Sources)
{
for (const auto& [Name, Data] : Source->mEmbeddedDataIndex)
{
const auto Existing = MergedData.find(Name);
if (Existing != MergedData.end() && !SameEmbeddedData(*Existing->second, *Data))
return lcUnexpected(tr("Both models contain different embedded data named '%1'. Rename one before merging.").arg(Data->FileName));
MergedData.emplace(Name, Data);
}
}
const lcResult<void> DestinationBindings = ValidateMergedTextureBindings(*this, MergedData, ClipboardRoot);
if (!DestinationBindings)
return DestinationBindings;
for (const Project* Source : Sources)
{
const lcResult<void> SourceBindings = ValidateMergedTextureBindings(*Source, MergedData, ClipboardRoot);
if (!SourceBindings)
return SourceBindings;
}
for (const std::map<std::string, lcPieceData*>::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::NormalizePieceDataName(Model->GetFileName().toLatin1().constData()));
for (const std::unique_ptr<lcPiece>& Piece : Model->GetPieces())
{
if (Piece->mPieceData && !Piece->mPieceData->IsModel())
NonModelPieces.emplace(lcPiecesLibrary::NormalizePieceDataName(Piece->GetID().toLatin1().constData()), Piece->mPieceData.Get());
}
}
const auto RegisterSourcePiece = [&NonModelPieces, &DestinationModelNames, &ConflictName](const std::string& Name, lcPieceData* PieceData)
{
if (!PieceData || PieceData->IsModel())
return true;
if (DestinationModelNames.find(Name) != DestinationModelNames.end())
{
ConflictName = QString::fromStdString(Name);
return false;
}
const std::map<std::string, lcPieceData*>::const_iterator Existing = NonModelPieces.find(Name);
if (Existing != NonModelPieces.end() && !CanShareMergePieceData(Existing->second, PieceData))
{
ConflictName = QString::fromStdString(Name);
return false;
}
NonModelPieces.emplace(Name, PieceData);
return true;
};
// Validate the identities behind every piece name before changing any project.
for (Project* Source : Sources)
{
for (const std::map<std::string, lcPieceData*>::value_type& Entry : Source->mPieceIndex)
{
if (!RegisterSourcePiece(Entry.first, Entry.second))
return lcUnexpected(tr("Both models contain different pieces named '%1'. Rename one before merging.").arg(ConflictName));
}
for (const std::unique_ptr<lcModel>& Model : Source->mModels)
{
OriginalModelNames.insert(lcPiecesLibrary::NormalizePieceDataName(Model->GetFileName().toLatin1().constData()));
for (const std::unique_ptr<lcPiece>& Piece : Model->GetPieces())
{
const std::string Name = lcPiecesLibrary::NormalizePieceDataName(Piece->GetID().toLatin1().constData());
if (!RegisterSourcePiece(Name, Piece->mPieceData.Get()))
return lcUnexpected(tr("Both models 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::NormalizePieceDataName(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->FindPieceData(FinalName.toLatin1().constData(), nullptr) != 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->RenameModelPieceData(Planned.Model->GetPieceData(), Planned.FinalName.toLatin1().constData()))
{
for (std::vector<const ModelPlan*>::reverse_iterator It = AppliedRenames.rbegin(); It != AppliedRenames.rend(); It++)
Library->RenameModelPieceData((*It)->Model->GetPieceData(), (*It)->Model->GetFileName().toLatin1().constData());
return lcUnexpected(tr("Could not reserve submodel name '%1'.").arg(Planned.FinalName));
}
AppliedRenames.push_back(&Planned);
}
mEmbeddedDataIndex = std::move(MergedData);
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())
{
lcPieceData* Incoming = Piece->mPieceData.Get();
if (!Incoming || Incoming->IsModel())
continue;
const std::string Name = lcPiecesLibrary::NormalizePieceDataName(Piece->GetID().toLatin1().constData());
lcPieceData* Existing = FindPieceData(Name);
if (!Existing || Existing == Incoming || !CanShareMergePieceData(Existing, Incoming))
continue;
const QString PieceId = Piece->GetID();
Piece->SetPieceData(lcPieceDataRef(Existing), PieceId, false, false);
}
}
for (std::unique_ptr<lcModel>& Model : Source->mModels)
{
const ModelPlan& Planned = PlannedModels[PlanIndex++];
if (Planned.FinalName != Model->GetFileName())
{
Renames.push_back({ Model->GetPieceData(), 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->TransferProjectPieceData(Source, this);
Source->mModels.clear();
mEmbeddedData.insert(mEmbeddedData.end(), Source->mEmbeddedData.begin(), Source->mEmbeddedData.end());
mDocumentBlocks.insert(mDocumentBlocks.end(), Source->mDocumentBlocks.begin(), Source->mDocumentBlocks.end());
for (const LoadWarning& Warning : Source->mLoadWarnings)
AddLoadWarning(Warning);
Source->mEmbeddedData.clear();
Source->mEmbeddedDataIndex.clear();
Source->mDocumentBlocks.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();
mDocumentBlocks.clear();
mEmbeddedData.clear();
mEmbeddedDataIndex.clear();
mModels.emplace_back(std::move(NewModel));
for (const std::unique_ptr<lcModel>& Model : mModels)
Model->CreatePieceData(this);
std::vector<lcModel*> UpdatedModels;
UpdatedModels.reserve(mModels.size());
for (const std::unique_ptr<lcModel>& Model : mModels)
Model->UpdatePieceData(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();
mDocumentBlocks.clear();
mEmbeddedData.clear();
mEmbeddedDataIndex.clear();
mModels.emplace_back(std::move(NewModel));
for (const std::unique_ptr<lcModel>& Model : mModels)
Model->CreatePieceData(this);
std::vector<lcModel*> UpdatedModels;
UpdatedModels.reserve(mModels.size());
for (const std::unique_ptr<lcModel>& Model : mModels)
Model->UpdatePieceData(UpdatedModels);
mModified = false;
return true;
}
std::vector<lcModelPartsEntry> Project::GetModelParts()
{
std::vector<lcModelPartsEntry> ModelParts;
if (mModels.empty())
return ModelParts;
mModels[0]->WaitForAssets();
for (const std::unique_ptr<lcModel>& Model : mModels)
Model->CalculateStep(LC_STEP_MAX);
mModels[0]->GetModelParts(lcMatrix44Identity(), gDefaultColor, ModelParts);
SetActiveModel(mActiveModel, false);
ModelParts.erase(std::remove_if(ModelParts.begin(), ModelParts.end(), [](const lcModelPartsEntry& ModelPart)
{
return ModelPart.PieceData->mState != lcAssetState::Loaded ||
(ModelPart.PieceData->GetSynthInfo() && !ModelPart.Mesh) ||
(!ModelPart.Mesh && !ModelPart.PieceData->GetMesh());
}), ModelParts.end());
return ModelParts;
}
std::vector<lcPieceDataRef> Project::GetRequiredPieceData() const
{
std::vector<lcPieceDataRef> RequiredPieceData;
for (const std::unique_ptr<lcModel>& Model : mModels)
{
std::vector<lcPieceDataRef> ModelPieceData = Model->GetRequiredPieceData();
RequiredPieceData.insert(RequiredPieceData.end(), ModelPieceData.begin(), ModelPieceData.end());
}
std::sort(RequiredPieceData.begin(), RequiredPieceData.end());
RequiredPieceData.erase(std::unique(RequiredPieceData.begin(), RequiredPieceData.end()), RequiredPieceData.end());
return RequiredPieceData;
}
std::vector<lcPiece*> Project::GetRequiredSynthPieces() const
{
std::vector<lcPiece*> Required;
std::unordered_set<lcPiece*> Seen;
for (const std::unique_ptr<lcModel>& Model : mModels)
{
const std::vector<lcPiece*> ModelRequired = Model->GetRequiredSynthPieces();
for (lcPiece* Piece : ModelRequired)
if (Seen.insert(Piece).second)
Required.push_back(Piece);
}
return Required;
}
lcResult<void> Project::EnsureAssetsReady() const
{
const lcModel* Model = GetMainModel();
return Model ? Model->EnsureAssetsReady() : lcResult<void>();
}
lcResult<QByteArray> Project::SerializeModel(const lcModel* Root, bool SelectedOnly, lcStep LastStep) const
{
struct ExportEntry
{
const lcModel* Model;
QString Name;
bool SelectedOnly;
};
std::vector<ExportEntry> Models;
std::map<QString, const lcModel*> Names;
std::vector<std::shared_ptr<const lcEmbeddedData>> Assets;
std::map<QString, std::shared_ptr<const lcEmbeddedData>> AssetLookup;
std::function<lcResult<void>(const lcModel*, const QString&, bool)> Collect = [&](const lcModel* Model, const QString& Name, bool FilterSelection) -> lcResult<void>
{
const QString Key = NormalizeEmbeddedDataName(Name);
const auto Existing = Names.find(Key);
if (Existing != Names.end())
{
if (Existing->second != Model)
return lcUnexpected(tr("Cannot export different submodels named '%1'.").arg(Name));
return lcResult<void>();
}
Names.emplace(Key, Model);
Models.push_back({ Model, Name, FilterSelection });
for (const std::unique_ptr<lcPiece>& Piece : Model->GetPieces())
{
if ((FilterSelection && !Piece->IsSelected()) || (LastStep && Piece->GetStepShow() > LastStep))
continue;
const lcPieceData* PieceData = Piece->mPieceData.Get();
const lcModel* Child = nullptr;
if (PieceData && PieceData->IsModel())
Child = PieceData->GetModel();
else if (PieceData && PieceData->IsProject())
Child = PieceData->GetProject()->GetMainModel();
if (Child)
{
const lcResult<void> Result = Collect(Child, Piece->GetID(), false);
if (!Result)
return Result;
}
}
return lcResult<void>();
};
const lcResult<void> Collected = Collect(Root, Root->GetFileName(), SelectedOnly);
if (!Collected)
return lcUnexpected(Collected.error());
// A complete model export can retain the original document layout too.
bool WholeProject = !SelectedOnly && !LastStep && Root == GetMainModel() && Models.size() == mModels.size();
for (const ExportEntry& Entry : Models)
WholeProject &= Entry.Model->GetProject() == this && NormalizeEmbeddedDataName(Entry.Name) == NormalizeEmbeddedDataName(Entry.Model->GetFileName());
if (WholeProject)
{
QByteArray Bytes;
QTextStream Stream(&Bytes, QIODevice::WriteOnly);
if (!Save(Stream))
return lcUnexpected(tr("Could not serialize all model data."));
Stream.flush();
return Bytes;
}
std::vector<const Project*> Owners;
std::map<const Project*, std::set<QString>> RequiredNames;
for (const ExportEntry& Entry : Models)
{
const Project* Owner = Entry.Model->GetProject();
if (!Owner)
continue;
if (RequiredNames.find(Owner) == RequiredNames.end())
Owners.push_back(Owner);
std::set<QString>& Required = RequiredNames[Owner];
const std::vector<std::string> References = lcMeshLoader::GetTextureReferences(SerializeModelText(Entry.Model, Entry.SelectedOnly, LastStep));
for (const std::string& Name : References)
{
const QString Bare = QString::fromLatin1(Name.c_str()) + QLatin1String(".png");
const QString Candidates[] = { QLatin1String("textures/") + Bare, Bare };
for (const QString& Candidate : Candidates)
{
const std::shared_ptr<const lcEmbeddedData> Data = Owner->FindEmbeddedData(Candidate);
if (!Data)
continue;
const QString Key = NormalizeEmbeddedDataName(Data->FileName);
const auto Existing = AssetLookup.find(Key);
if (Existing != AssetLookup.end() && !SameEmbeddedData(*Existing->second, *Data))
return lcUnexpected(tr("Cannot export different embedded data named '%1'.").arg(Data->FileName));
AssetLookup.emplace(Key, Data);
Required.insert(Key);
}
}
}
for (const Project* Owner : Owners)
for (const std::shared_ptr<const lcEmbeddedData>& Data : Owner->mEmbeddedData)
if (RequiredNames[Owner].find(NormalizeEmbeddedDataName(Data->FileName)) != RequiredNames[Owner].end())
Assets.push_back(Data);
const bool MPD = Models.size() > 1 || !Assets.empty();
QByteArray Output;
const auto Append = [&Output](const QByteArray& Bytes)
{
if (!Output.isEmpty() && !Output.endsWith('\n') && !Output.endsWith('\r'))
Output += "\r\n";
Output += Bytes;
};
for (const ExportEntry& Entry : Models)
{
const DocumentBlock* Original = nullptr;
const Project* Owner = Entry.Model->GetProject();
if (Owner)
for (const DocumentBlock& Block : Owner->mDocumentBlocks)
if (Block.Model == Entry.Model && !Block.DuplicateModel)
{
Original = &Block;
break;
}
Append(SerializeStoredModel(Entry.Model, Entry.Name, Original, MPD, Entry.SelectedOnly, LastStep));
}
for (const std::shared_ptr<const lcEmbeddedData>& Data : Assets)
Append(Data->RawBlock);
if (SelectedOnly)
Output.prepend("0 !LEOCAD CLIPBOARD PROJECT " + mClipboardIdentity.toString().toUtf8() + "\r\n");
return Output;
}
lcResult<lcModel*> Project::ImportClipboard(const QByteArray& FileData)
{
Project Source(mIsPreview);
Source.mModels.clear();
lcGetPiecesLibrary()->ReleaseProjectPieceData(&Source);
Source.SetDeferModelMeshRequests(true);
Source.mFileName = mFileName;
const QByteArray Marker("0 !LEOCAD CLIPBOARD PROJECT ");
const QByteArray FirstLine = FileData.left(FileData.indexOf('\n') == -1 ? FileData.size() : FileData.indexOf('\n')).trimmed();
const bool SameProject = FirstLine.startsWith(Marker) && QUuid(QString::fromUtf8(FirstLine.mid(Marker.size()))) == mClipboardIdentity;
Source.LoadLDrawDocument(FileData, this, SameProject);
if (Source.mModels.empty())
return lcUnexpected(tr("Clipboard contains no model."));
lcModel* Root = Source.GetMainModel();
for (const std::unique_ptr<lcModel>& Model : Source.mModels)
Model->QueueMeshBuild();
if (Root->GetPieces().empty())
return static_cast<lcModel*>(nullptr);
if (SameProject)
{
std::set<const lcModel*> Reachable;
std::function<void(const lcModel*)> Collect = [&](const lcModel* Model)
{
if (!Reachable.insert(Model).second)
return;
for (const std::unique_ptr<lcPiece>& Piece : Model->GetPieces())
if (Piece->mPieceData->IsModel() && Piece->mPieceData->GetModel()->GetProject() == &Source)
Collect(Piece->mPieceData->GetModel());
};
Collect(Root);
Source.mModels.erase(std::remove_if(Source.mModels.begin(), Source.mModels.end(), [&Reachable](const std::unique_ptr<lcModel>& Model)
{
return Reachable.find(Model.get()) == Reachable.end();
}), Source.mModels.end());
// Live reused models already own their current assets. Keep snapshots only
// for definitions that were missing and therefore still need importing.
std::set<QString> Required;
for (const std::unique_ptr<lcModel>& Model : Source.mModels)
{
if (Model.get() == Root)
continue;
for (const std::string& Name : lcMeshLoader::GetTextureReferences(SerializeModelText(Model.get(), false, 0)))
{
const QString Bare = QString::fromLatin1(Name.c_str()) + QLatin1String(".png");
Required.insert(NormalizeEmbeddedDataName(Bare));
Required.insert(NormalizeEmbeddedDataName(QLatin1String("textures/") + Bare));
}
}
Source.mEmbeddedData.erase(std::remove_if(Source.mEmbeddedData.begin(), Source.mEmbeddedData.end(), [&Required](const std::shared_ptr<const lcEmbeddedData>& Data)
{
return Required.find(NormalizeEmbeddedDataName(Data->FileName)) == Required.end();
}), Source.mEmbeddedData.end());
for (auto Entry = Source.mEmbeddedDataIndex.begin(); Entry != Source.mEmbeddedDataIndex.end();)
if (Required.find(Entry->first) == Required.end())
Entry = Source.mEmbeddedDataIndex.erase(Entry);
else
++Entry;
}
// Clipboard formatting is generated; only retain the actual assets and submodels.
Source.mDocumentBlocks.clear();
const lcResult<void> Result = MergeInternal({ &Source }, Root);
if (!Result)
return lcUnexpected(Result.error());
const auto Entry = std::find_if(mModels.begin(), mModels.end(), [Root](const std::unique_ptr<lcModel>& Model) { return Model.get() == Root; });
std::unique_ptr<lcModel> Wrapper = std::move(*Entry);
mModels.erase(Entry);
return Wrapper.release();
}
lcResult<void> Project::ExportCurrentStep(const QString& FileName)
{
const lcModel* Model = lcGetActiveModel();
const lcResult<QByteArray> Bytes = SerializeModel(Model, false, Model->GetCurrentStep());
if (!Bytes)
return lcUnexpected(Bytes.error());
QSaveFile File(FileName);
if (!File.open(QIODevice::WriteOnly) || File.write(Bytes.value()) != Bytes.value().size() || !File.commit())
return lcUnexpected(tr("Error writing to file '%1':\n%2").arg(FileName, File.errorString()));
lcSetProfileString(LC_PROFILE_PROJECTS_PATH, QFileInfo(FileName).absolutePath());
return lcResult<void>();
}
lcResult<void> Project::ExportModel(const QString& FileName, lcModel* Model) const
{
const lcResult<QByteArray> Bytes = SerializeModel(Model, false, 0);
if (!Bytes)
return lcUnexpected(Bytes.error());
QSaveFile File(FileName);
if (!File.open(QIODevice::WriteOnly) || File.write(Bytes.value()) != Bytes.value().size() || !File.commit())
return lcUnexpected(tr("Error writing to file '%1':\n%2").arg(FileName, File.errorString()));
return lcResult<void>();
}
QString Project::MakeExportNameFragment(const QString& Name)
{
QString Identifier;
bool AddSeparator = false;
for (QChar Character : Name)
{
const ushort Value = Character.unicode();
const bool IsAsciiLetter = (Value >= 'A' && Value <= 'Z') || (Value >= 'a' && Value <= 'z');
const bool IsAsciiDigit = Value >= '0' && Value <= '9';
if (IsAsciiLetter || IsAsciiDigit)
{
if (AddSeparator && !Identifier.isEmpty() && !Identifier.endsWith(QLatin1Char('_')))
Identifier.append(QLatin1Char('_'));
Identifier.append(Character);
AddSeparator = false;
}
else
AddSeparator = true;
}
if (Identifier.isEmpty())
Identifier = QLatin1String("Unnamed");
if (Identifier.size() > 80)
{
const QByteArray Hash = QCryptographicHash::hash(Name.toUtf8(), QCryptographicHash::Sha256).toHex();
Identifier = Identifier.left(67) + QLatin1Char('_') + QString::fromLatin1(Hash.left(12));
}
return Identifier;
}
QString Project::MakeUniqueExportIdentifier(const QString& BaseName, std::set<QString>& UsedNames, const QString& ReservedSuffix)
{
QString Identifier = BaseName;
int Suffix = 2;
while (UsedNames.find(Identifier) != UsedNames.end() ||
(!ReservedSuffix.isEmpty() && UsedNames.find(Identifier + ReservedSuffix) != UsedNames.end()))
Identifier = BaseName + QLatin1Char('_') + QString::number(Suffix++);
UsedNames.insert(Identifier);
if (!ReservedSuffix.isEmpty())
UsedNames.insert(Identifier + ReservedSuffix);
return Identifier;
}
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 std::vector<lcModelPartsEntry> ModelParts = GetModelParts();
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.PieceData->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 std::vector<lcModelPartsEntry> ModelParts = GetModelParts();
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";
std::vector<QString> ColorIDs(gColorList.size());
std::set<QString> UsedColorIDs;
for (size_t ColorIndex = 0; ColorIndex < gColorList.size(); ColorIndex++)
{
const QString BaseName = QLatin1String("Color_") + MakeExportNameFragment(QString::fromLatin1(gColorList[ColorIndex].SafeName));
QString Identifier = BaseName;
if (UsedColorIDs.find(Identifier) != UsedColorIDs.end())
Identifier += QLatin1Char('_') + QString::number(ColorIndex);
ColorIDs[ColorIndex] = MakeUniqueExportIdentifier(Identifier, UsedColorIDs, QString());
}
Stream << "<library_effects>\r\n";
for (size_t ColorIndex = 0; ColorIndex < gColorList.size(); ColorIndex++)
{
const lcColor& Color = gColorList[ColorIndex];
const QString& ColorName = ColorIDs[ColorIndex];
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 (size_t ColorIndex = 0; ColorIndex < gColorList.size(); ColorIndex++)
{
const QString& ColorName = ColorIDs[ColorIndex];
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::map<const lcMesh*, QString> MeshIDs;
std::set<QString> UsedMeshIDs;
for (const lcModelPartsEntry& ModelPart : ModelParts)
{
lcMesh* Mesh = !ModelPart.Mesh ? ModelPart.PieceData->GetMesh() : ModelPart.Mesh;
if (!Mesh)
continue;
if (MeshIDs.find(Mesh) != MeshIDs.end())
continue;
const QString BaseName = QLatin1String("Part_") + MakeExportNameFragment(QString::fromUtf8(ModelPart.PieceData->mFileName));
const QString ID = MakeUniqueExportIdentifier(BaseName, UsedMeshIDs, QString());
MeshIDs.emplace(Mesh, ID);
const int NumVertices = Mesh->mNumVertices + Mesh->mNumTexturedVertices;
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(NumVertices * 3));
const lcVertex* Verts = static_cast<const lcVertex*>(Mesh->mVertexData);
for (int VertexIdx = 0; VertexIdx < Mesh->mNumVertices; VertexIdx++)
{
const 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));
}
const lcVertexTextured* TexturedVerts = reinterpret_cast<const lcVertexTextured*>(Verts + Mesh->mNumVertices);
for (int VertexIdx = 0; VertexIdx < Mesh->mNumTexturedVertices; VertexIdx++)
{
const lcVector3& Position = TexturedVerts[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(NumVertices));
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(NumVertices * 3));
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));
}
for (int VertexIdx = 0; VertexIdx < Mesh->mNumTexturedVertices; VertexIdx++)
{
lcVector3 Normal = lcUnpackNormal(TexturedVerts[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(NumVertices));
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];
int Offset = 0;
if (Section->PrimitiveType == LC_MESH_TRIANGLES)
Offset = 0;
else if (Section->PrimitiveType == LC_MESH_TEXTURED_TRIANGLES)
Offset = Mesh->mNumVertices;
else
continue;
const QString& ColorName = ColorIDs[Section->ColorIndex];
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] + Offset);
QString idx2 = QString::number(Indices[Idx + 1] + Offset);
QString idx3 = QString::number(Indices[Idx + 2] + Offset);
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] + Offset);
QString idx2 = QString::number(Indices[Idx + 1] + Offset);
QString idx3 = QString::number(Indices[Idx + 2] + Offset);
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.PieceData->GetMesh() : ModelPart.Mesh;
if (!Mesh)
continue;
const QString& ID = MeshIDs.at(Mesh);
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 QString& SourceColorName = ColorIDs[Section->ColorIndex];
const QString* TargetColorName;
if (Section->ColorIndex == gDefaultColor)
TargetColorName = &ColorIDs[ModelPart.ColorIndex];
else
TargetColorName = &SourceColorName;
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 lcPieceData* PieceData = PartIt.first;
for (const auto& ColorIt : PartIt.second)
{
std::string Description = PieceData->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, PieceData->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)
Model->WaitForAssets();
QDir Dir(Options.PathName);
Dir.mkpath(QLatin1String("."));
const QString ProjectTitle = GetTitle();
auto MakeSafeFileName = [](const QString& Name)
{
const QByteArray Utf8Name = Name.toUtf8();
QString SafeName;
SafeName.reserve(Utf8Name.size() * 3);
for (char Byte : Utf8Name)
{
const unsigned char Value = static_cast<unsigned char>(Byte);
if ((Value >= 'A' && Value <= 'Z') || (Value >= 'a' && Value <= 'z') || (Value >= '0' && Value <= '9') || Value == '-')
SafeName.append(QLatin1Char(Value));
else
SafeName.append(QString("_%1").arg(Value, 2, 16, QLatin1Char('0')));
}
if (SafeName.isEmpty())
return QString("Model");
if (SafeName.size() > 80)
{
const QByteArray Digest = QCryptographicHash::hash(Utf8Name, QCryptographicHash::Sha256).toHex();
SafeName = SafeName.left(60) + '-' + QString::fromLatin1(Digest.left(16));
}
// Windows reserves device names even when a file has an extension.
const QString UpperName = SafeName.toUpper();
const bool NumberedDevice = UpperName.size() == 4 && (UpperName.startsWith(QLatin1String("COM")) || UpperName.startsWith(QLatin1String("LPT"))) &&
UpperName[3] >= QLatin1Char('1') && UpperName[3] <= QLatin1Char('9');
if (UpperName == QLatin1String("CON") || UpperName == QLatin1String("PRN") || UpperName == QLatin1String("AUX") || UpperName == QLatin1String("NUL") || NumberedDevice)
SafeName.prepend('_');
return SafeName;
};
const QString ProjectBaseName = MakeSafeFileName(QFileInfo(ProjectTitle).completeBaseName());
std::map<const lcModel*, QString> ModelBaseNames;
int ModelOrdinal = 0;
for (const std::unique_ptr<lcModel>& Model : mModels)
{
if (Models.find(Model.get()) == Models.end())
continue;
QString BaseName = ProjectBaseName;
if (Models.size() > 1)
BaseName += QString("-%1-%2").arg(++ModelOrdinal).arg(MakeSafeFileName(Model->GetProperties().mFileName));
ModelBaseNames.emplace(Model.get(), std::move(BaseName));
}
auto AddPartsListImage = [&Dir](QTextStream& Stream, lcModel* Model, lcStep Step, const QString& BaseName) -> lcResult<void>
{
lcPartsList PartsList;
if (Step == 0)
Model->GetPartsList(gDefaultColor, true, false, PartsList);
else
Model->GetPartsListForStep(Step, gDefaultColor, PartsList, false);
if (PartsList.empty())
return lcResult<void>();
const lcResult<QImage> Image = Model->GetPartsListImage(1024, Step, LC_RGBA(255, 255, 255, 0), QFont("Arial", 16, QFont::Bold), Qt::black, true);
if (!Image)
return lcUnexpected(Image.error());
QString ImageName = BaseName + QLatin1String("-parts.png");
QString FileName = QFileInfo(Dir, ImageName).absoluteFilePath();
QImageWriter Writer(FileName);
if (!Writer.write(Image.value()))
return lcUnexpected(tr("Error writing to file '%1':\n%2").arg(FileName, Writer.errorString()));
Stream << QString::fromLatin1("<p><IMG SRC=\"%1\" /></p><br><br>\r\n").arg(ImageName.toHtmlEscaped());
return lcResult<void>();
};
for (lcModel* Model : Models)
{
const QString& BaseName = ModelBaseNames.at(Model);
lcStep LastStep = Model->GetLastStep();
QString PageTitle;
if (Models.size() > 1)
PageTitle = Model->GetProperties().mFileName;
else
PageTitle = ProjectTitle;
const QString EscapedPageTitle = PageTitle.toHtmlEscaped();
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(EscapedPageTitle);
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.toHtmlEscaped(), 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(EscapedPageTitle);
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(EscapedPageTitle, QString::number(Step));
Stream << QString::fromLatin1("<IMG SRC=\"%1-%2.png\" ALT=\"Step %3\" WIDTH=%4 HEIGHT=%5><BR><BR>\r\n").arg(BaseName.toHtmlEscaped(), 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(EscapedPageTitle);
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, Options.TransparentImages ? lcImageBackground::Transparent : lcImageBackground::Opaque);
if (!ImagesSaved)
return ImagesSaved;
}
if (Models.size() > 1)
{
QString FileName = QFileInfo(Dir, ProjectBaseName + 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.toHtmlEscaped());
for (const lcModel* Model : Models)
{
const QString& BaseName = ModelBaseNames.at(Model);
if (Options.SinglePage)
{
FileName = BaseName + QLatin1String(".html");
Stream << QString::fromLatin1("<p><a href=\"%1\">%2</a>").arg(FileName.toHtmlEscaped(), Model->GetProperties().mFileName.toHtmlEscaped());
}
else if (Options.IndexPage)
{
FileName = BaseName + QLatin1String("-index.html");
Stream << QString::fromLatin1("<p><a href=\"%1\">%2</a>").arg(FileName.toHtmlEscaped(), Model->GetProperties().mFileName.toHtmlEscaped());
}
else
{
lcStep LastStep = Model->GetLastStep();
Stream << QString::fromLatin1("<p>%1</p>").arg(Model->GetProperties().mFileName.toHtmlEscaped());
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 std::vector<lcModelPartsEntry> ModelParts = GetModelParts();
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 lcPieceData*, 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();
auto MakeIdentifier = [](const QString& Name, const QString& Fallback)
{
QString Identifier;
for (QChar Character : Name)
{
const ushort Value = Character.unicode();
if ((Value >= 'A' && Value <= 'Z') || (Value >= 'a' && Value <= 'z') || (Value >= '0' && Value <= '9') || Value == '_')
Identifier.append(Character);
else
Identifier.append('_');
}
return Identifier.isEmpty() ? Fallback : Identifier.left(48);
};
const QString CameraName = MakeIdentifier(QString(Camera->GetName()), QLatin1String("Camera"));
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 = QLatin1String("LC_") + MakeExportNameFragment(QString(mModels[0]->GetFileName()));
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);
if (ModelParts.empty())
{
Min = lcVector3(-10.0f, -10.0f, -10.0f);
Max = lcVector3(10.0f, 10.0f, 10.0f);
}
for (const lcModelPartsEntry& ModelPart : ModelParts)
{
lcVector3 Points[8];
const lcMesh* Mesh = ModelPart.Mesh ? ModelPart.Mesh : ModelPart.PieceData->GetMesh();
lcGetBoxCorners(Mesh->mBoundingBox, 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
{
std::set<QString> UsedLightNames;
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 BaseLightName = MakeIdentifier(QString(Light->GetName()), QLatin1String("Light"));
QString LightName = BaseLightName;
int LightSuffix = 1;
while (!UsedLightNames.insert(LightName).second)
LightName = BaseLightName + '_' + QString::number(LightSuffix++);
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");
lcPieceData* PieceData = Library->FindPieceData(Src, nullptr);
if (!PieceData)
continue;
bool LgeoPartFound = false;
if ((LgeoPartFound = strchr(Flags, 'L')))
{
std::pair<char[LC_PIECE_NAME_LEN + 1], int>& Entry = PieceTable[PieceData];
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[PieceData];
Entry.second |= LGEO_PIECE_AR;
snprintf(Entry.first, sizeof(Entry.first), "ar_%s", Dst);
}
if (strchr(Flags, 'S'))
{
auto Entry = PieceTable.find(PieceData);
if (Entry != PieceTable.end() && Entry->second.first[0])
{
// The external include declares both the base object and its slope surface.
Entry->second.second |= LGEO_PIECE_SLOPE;
}
}
}
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<std::string> IncludedLgeoNames;
if (UseLGEO)
{
for (const lcModelPartsEntry& ModelPart : ModelParts)
{
if (ModelPart.Mesh)
continue;
auto Search = PieceTable.find(ModelPart.PieceData);
if (Search == PieceTable.end())
continue;
lcMesh* Mesh = ModelPart.PieceData->GetMesh();
if (!Mesh)
continue;
const std::pair<char[LC_PIECE_NAME_LEN + 1], int>& Entry = Search->second;
if (Entry.first[0] && IncludedLgeoNames.insert(Entry.first).second)
{
snprintf(Line, sizeof(Line), "#include \"%s.inc\" // %s\n", Entry.first, ModelPart.PieceData->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();
std::map<const lcMesh*, std::string> MeshNames;
std::set<QString> UsedPovNames;
UsedPovNames.insert(TopModelName);
for (const lcColor& Color : gColorList)
UsedPovNames.insert(QLatin1String("lc_") + QString::fromLatin1(Color.SafeName));
for (const lcModelPartsEntry& ModelPart : ModelParts)
{
lcMesh* Mesh = !ModelPart.Mesh ? ModelPart.PieceData->GetMesh() : ModelPart.Mesh;
if (!Mesh)
continue;
if (!ModelPart.Mesh)
{
const std::map<const lcPieceData*, std::pair<char[LC_PIECE_NAME_LEN + 1], int>>::const_iterator ExistingPiece = PieceTable.find(ModelPart.PieceData);
if (ExistingPiece != PieceTable.end() && ExistingPiece->second.first[0] &&
(ExistingPiece->second.second & (LGEO_PIECE_LGEO | LGEO_PIECE_AR)))
continue;
}
std::map<const lcMesh*, std::string>::iterator MeshNameIt = MeshNames.find(Mesh);
const bool Added = MeshNameIt == MeshNames.end();
if (Added)
{
const QString BaseName = QLatin1String("lc_") + MakeExportNameFragment(QString::fromUtf8(ModelPart.PieceData->mFileName));
const QString Identifier = MakeUniqueExportIdentifier(BaseName, UsedPovNames, QLatin1String("_clear"));
MeshNameIt = MeshNames.emplace(Mesh, Identifier.mid(3).toStdString()).first;
}
const std::string& MeshName = MeshNameIt->second;
if (!ModelPart.Mesh)
{
std::pair<char[LC_PIECE_NAME_LEN + 1], int>& Entry = PieceTable[ModelPart.PieceData];
snprintf(Entry.first, sizeof(Entry.first), "lc_%s", MeshName.c_str());
}
if (!Added)
continue;
Mesh->ExportPOVRay(POVFile, MeshName.c_str(), &ColorTablePointer[0]);
snprintf(Line, sizeof(Line), "#declare lc_%s_clear = lc_%s\n\n", MeshName.c_str(), MeshName.c_str());
POVFile.WriteLine(Line);
}
if (!ModelParts.empty())
{
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.PieceData];
if (!Entry.first[0] && !(Entry.second & LGEO_PIECE_SLOPE))
{
const std::map<const lcMesh*, std::string>::const_iterator MeshName = MeshNames.find(ModelPart.PieceData->GetMesh());
if (MeshName != MeshNames.end())
snprintf(Entry.first, sizeof(Entry.first), "lc_%s", MeshName->second.c_str());
}
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 %s { %s }\n normal { bumps 0.3 scale 0.02 }\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, 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
{
if (!ModelPart.PieceData || !ModelPart.PieceData->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
{
const std::string& Name = MeshNames.at(ModelPart.Mesh);
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.c_str(), 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);
}
if (!ModelParts.empty())
{
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 std::vector<lcModelPartsEntry> ModelParts = GetModelParts();
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));
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);
}
auto WritePositions=[&Line, &OBJFile](const auto* Verts, int VertexCount, const lcMatrix44& ModelWorld)
{
for (int VertexIndex = 0; VertexIndex < VertexCount; VertexIndex++)
{
lcVector3 Position = lcMul31(Verts[VertexIndex].Position, ModelWorld);
snprintf(Line, sizeof(Line), "v %.2f %.2f %.2f\n", Position[0], Position[1], Position[2]);
OBJFile.WriteLine(Line);
}
};
auto WriteNormals=[&Line, &OBJFile](const auto* Verts, int VertexCount, const lcMatrix44& ModelWorld)
{
for (int VertexIndex = 0; VertexIndex < VertexCount; VertexIndex++)
{
lcVector3 Normal = lcMul30(lcUnpackNormal(Verts[VertexIndex].Normal), ModelWorld);
snprintf(Line, sizeof(Line), "vn %.2f %.2f %.2f\n", Normal[0], Normal[1], Normal[2]);
OBJFile.WriteLine(Line);
}
};
for (const lcModelPartsEntry& ModelPart : ModelParts)
{
lcMesh* Mesh = !ModelPart.Mesh ? ModelPart.PieceData->GetMesh() : ModelPart.Mesh;
if (!Mesh)
continue;
const lcVertex* Verts = static_cast<const lcVertex*>(Mesh->mVertexData);
WritePositions(Verts, Mesh->mNumVertices, ModelPart.WorldMatrix);
const lcVertexTextured* VertsTextured = reinterpret_cast<const lcVertexTextured*>(Verts + Mesh->mNumVertices);
WritePositions(VertsTextured, Mesh->mNumTexturedVertices, ModelPart.WorldMatrix);
OBJFile.WriteLine("#\n\n");
}
for (const lcModelPartsEntry& ModelPart : ModelParts)
{
lcMesh* Mesh = !ModelPart.Mesh ? ModelPart.PieceData->GetMesh() : ModelPart.Mesh;
if (!Mesh)
continue;
const lcVertex* Verts = static_cast<const lcVertex*>(Mesh->mVertexData);
WriteNormals(Verts, Mesh->mNumVertices, ModelPart.WorldMatrix);
const lcVertexTextured* VertsTextured = reinterpret_cast<const lcVertexTextured*>(Verts + Mesh->mNumVertices);
WriteNormals(VertsTextured, Mesh->mNumTexturedVertices, ModelPart.WorldMatrix);
OBJFile.WriteLine("#\n\n");
}
int PieceCount = 0;
quint32 BaseVertex = 1;
for (const lcModelPartsEntry& ModelPart : ModelParts)
{
lcMesh* Mesh = !ModelPart.Mesh ? ModelPart.PieceData->GetMesh() : ModelPart.Mesh;
if (!Mesh)
continue;
snprintf(Line, sizeof(Line), "g Piece%.3d\n", PieceCount++);
OBJFile.WriteLine(Line);
Mesh->ExportWavefrontIndices(OBJFile, ModelPart.ColorIndex, BaseVertex);
BaseVertex += Mesh->mNumVertices + Mesh->mNumTexturedVertices;
}
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, lcImageBackground::Default);
}
void Project::UpdatePieceData(lcPieceData* PieceData) const
{
if (!mModels.empty())
{
std::vector<lcModel*> UpdatedModels;
mModels[0]->UpdatePieceData(UpdatedModels);
lcBoundingBox BoundingBox = mModels[0]->GetPieceData()->GetBoundingBox();
PieceData->SetBoundingBox(BoundingBox.Min, BoundingBox.Max);
}
}
void Project::MarkAsModified()
{
mModified = true;
}