mirror of
https://github.com/leozide/leocad.git
synced 2026-10-26 22:10:32 +00:00
HTML export saved its transparency option without passing it to image rendering. Select a transparent clear background when enabled and preserve the configured background with fully opaque pixels when disabled. Keep the existing background behavior for other step-image and Save Image callers. Add tooltips, contextual overviews, and Help/F1 support to HTML Export, Submodels, and all three Render modes. Link field labels to matching help and explain export scope, file output, model management, and render controls. Refresh catalog keys and locations while preserving existing translations and review status. Leave the 56 new strings untranslated for the later translation pass. Validation: Qt 6.10 debug build; 16 actual PNG exports covering two steps, solid/gradient backgrounds, transparency on/off, tiled rendering, background colors, retained geometry, and unchanged Save Image behavior; dialog Help/F1, Escape, label-buddy and visual layout checks; catalog coverage for 2,170 active source messages; git diff --check.
4158 lines
131 KiB
C++
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 project. Please enter a unique name.").arg(Name));
|
|
continue;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
return Name;
|
|
}
|
|
|
|
lcModel* Project::CreateNewModel(bool ShowModel)
|
|
{
|
|
QStringList ModelNames;
|
|
|
|
for (const std::unique_ptr<lcModel> &Model : mModels)
|
|
ModelNames.append(Model->GetProperties().mFileName);
|
|
|
|
QString Name = GetNewModelName(gMainWindow, tr("New Submodel"), QString(), ModelNames);
|
|
|
|
if (Name.isEmpty())
|
|
return nullptr;
|
|
|
|
mModified = true;
|
|
lcModel* Model = new lcModel(Name, this, false);
|
|
Model->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 project.").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 project.").arg(Entry.Name));
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Free names being renamed or deleted before applying the new names.
|
|
for (const std::unique_ptr<lcModel>& OldModel : mModels)
|
|
{
|
|
const lcModel* Model = OldModel.get();
|
|
const std::vector<lcModelListDialogEntry>::const_iterator Kept = std::find_if(Results.begin(), Results.end(), [Model](const lcModelListDialogEntry& Entry)
|
|
{
|
|
return Entry.ExistingModel == Model;
|
|
});
|
|
|
|
if (Kept != Results.end())
|
|
{
|
|
if (Kept->Name != Model->GetFileName())
|
|
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 project.").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 projects 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 projects 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 projects contain different pieces named '%1'. Rename one before merging.").arg(ConflictName));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pick final model names against the complete set of incoming piece names.
|
|
for (Project* Source : Sources)
|
|
{
|
|
for (const std::unique_ptr<lcModel>& Model : Source->mModels)
|
|
{
|
|
const QString OriginalName = Model->GetFileName();
|
|
QString FinalName = OriginalName;
|
|
int RenameIndex = 0;
|
|
|
|
for (;;)
|
|
{
|
|
const std::string Name = lcPiecesLibrary::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;
|
|
}
|