src/documentmanager.cpp

Wed, 09 Mar 2022 12:42:45 +0200

author
Teemu Piippo <teemu@hecknology.net>
date
Wed, 09 Mar 2022 12:42:45 +0200
changeset 172
50f055543ff6
parent 153
2f79053c2e9a
child 176
cd9d6bf6f649
permissions
-rw-r--r--

Render vertices as spheres

/*
 *  LDForge: LDraw parts authoring CAD
 *  Copyright (C) 2013 - 2020 Teemu Piippo
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <QFile>
#include <QDir>
#include <QFileInfo>
#include <QSaveFile>
#include "documentmanager.h"
#include "modeleditor.h"
#include "linetypes/comment.h"
#include "linetypes/subfilereference.h"
#include "parser.h"

/**
 * @brief Constructs a new document manager
 * @param parent Parent object
 */
DocumentManager::DocumentManager(QObject* parent) :
	QObject{parent}
{
}

/**
 * @brief Creates a new model.
 * @returns the ID of the new model
 */
ModelId DocumentManager::newModel()
{
	const ModelId modelId{++this->modelIdCounter};
	const QString name = makeNewModelName();
	this->openModels[modelId] = ModelInfo{
		.model = std::make_unique<Model>(),
		.id = modelId,
		.opentype = OpenType::ManuallyOpened,
	};
	this->makePolygonCacheForModel(modelId);
	return modelId;
}

/**
 * @brief Looks for a model by name
 * @param name Name of the model
 * @returns model or null
 */
Model* DocumentManager::findDependencyByName(const ModelId modelId, const QString& name)
{
	const auto modelsIterator = this->openModels.find(modelId);
	if (modelsIterator != std::end(this->openModels))
	{
		const auto& dependencies = modelsIterator->second.dependencies;
		const auto dependenciesIterator = dependencies.find(name);
		if (dependenciesIterator != dependencies.end())
		{
			ModelInfo& modelInfo = this->openModels[dependenciesIterator->second];
			return modelInfo.model.get();
		}
		else
		{
			return nullptr;
		}
	}
	else
	{
		return nullptr;
	}
}

/**
 * @brief Gets a model pointer by id or nullptr if not found
 * @param modelId id of model to find
 * @returns model pointer or null
 */
Model *DocumentManager::getModelById(ModelId modelId)
{
	const auto iterator = this->openModels.find(modelId);
	if (iterator != this->openModels.end())
	{
		return iterator->second.model.get();
	}
	else
	{
		return nullptr;
	}
}

QString pathToName(const QFileInfo& path)
{
	static const char* paths[] = {
		"s",
		"48"
		"8"
	};
	const QString baseName = path.fileName();
	const QString dirName = QFileInfo{path.dir().path()}.fileName();
	QString result;
	if (std::find(std::begin(paths), std::end(paths), dirName) != std::end(paths))
	{
		result = dirName + "\\" + baseName;
	}
	else
	{
		result = baseName;
	}
	return result;
}

/**
 * @brief Tries to open the model at the specified path
 * @param path Path to the model to open
 * @param errorStream Where to write any errors
 * @param openType rationale behind opening this file
 * @returns model id, or no value on error
 */
std::optional<ModelId> DocumentManager::openModel(
	const QString& path,
	QTextStream& errorStream,
	const OpenType openType
) {
	QFile file{path};
	const QString name = pathToName(path);
	file.open(QFile::ReadOnly | QFile::Text);
	std::unique_ptr<Model> newModel = std::make_unique<Model>(this);
	QTextStream textStream{&file};
	Parser parser{file};
	parser.parseBody(*newModel);
	std::optional<ModelId> result;
	if (file.error() == QFile::NoError)
	{
		const ModelId modelId{++this->modelIdCounter};
		this->openModels[modelId] = {std::move(newModel), modelId, path, openType};
		this->makePolygonCacheForModel(modelId);
		result = modelId;
	}
	else
	{
		errorStream << file.errorString();
	}
	return result;
}

QString DocumentManager::makeNewModelName()
{
	untitledNameCounter += 1;
	return "untitled-" + QString::number(untitledNameCounter);
}

void DocumentManager::loadDependenciesForAllModels(const LibraryManager& libraries, QTextStream& errorStream)
{
	for (const auto& modelInfoPair : this->openModels)
	{
		this->loadDependenciesForModel(modelInfoPair.first, modelInfoPair.second.path, libraries, errorStream);
	}
}

struct DocumentManager::LoadDepedenciesBag
{
	const LibraryManager& libraries;
	QStringList missing;
	QSet<ModelId> processed;
	QTextStream& errorStream;
};

void DocumentManager::loadDependenciesForModel(
	const ModelId modelId,
	const QString& path,
	const LibraryManager& libraries,
	QTextStream& errorStream)
{
	LoadDepedenciesBag bag {
		.libraries = libraries,
		.missing = {},
		.processed = {},
		.errorStream = errorStream,
	};
	this->loadDependenciesForModel(modelId, path, bag);
	if (not bag.missing.empty())
	{
		bag.missing.sort(Qt::CaseInsensitive);
		errorStream << utility::format(
			"The following files could not be opened: %1",
			bag.missing.join(", "));
	}
}

void DocumentManager::closeDocument(const ModelId modelId)
{
	ModelInfo* modelInfo = findInMap(this->openModels, modelId);
	if (modelInfo != nullptr)
	{
		modelInfo->opentype = OpenType::AutomaticallyOpened;
		this->prune();
	}
}

const QString *DocumentManager::modelPath(ModelId modelId) const
{
	const auto iterator = this->openModels.find(modelId);
	if (iterator != this->openModels.end())
	{
		return &iterator->second.path;
	}
	else
	{
		return nullptr;
	}
}

/**
 * @brief Changes the path of the specified model. Since the name of the file may change,
 * changing the path can cause dependencies to be resolved differently. As such, dependencies
 * need to be resolved for all files after this operation.
 * @param modelId Model to change the path of
 * @param newPath New path
 * @param libraries Library manager for the purpose of dependency resolving
 * @param errorStream Where to write any errors regarding dependency resolving
 */
void DocumentManager::setModelPath(
	const ModelId modelId,
	const QString &newPath,
	const LibraryManager &libraries,
	QTextStream &errorStream)
{
	auto modelInfoPair = this->openModels.find(modelId);
	if (true
		and modelInfoPair != this->openModels.end()
		and modelInfoPair->second.opentype == OpenType::ManuallyOpened
	) {
		modelInfoPair->second.path = newPath;
		this->loadDependenciesForAllModels(libraries, errorStream);
	}
}

bool DocumentManager::saveModel(const ModelId modelId, QTextStream &errors)
{
	const QString* const path = this->modelPath(modelId);
	if (path != nullptr)
	{
		QSaveFile file{*path};
		file.setDirectWriteFallback(true);
		if (file.open(QSaveFile::WriteOnly))
		{
			// if path is not nullptr, getModelById will always return a value as well
			::save(*this->getModelById(modelId), &file);
			const bool commitSucceeded = file.commit();
			if (not commitSucceeded)
			{
				errors << tr("Could not save: %1").arg(file.errorString());
				return false;
			}
			else
			{
				return true;
			}
		}
		else
		{
			errors << tr("Could not open %1 for writing: %2")
				.arg(file.fileName())
				.arg(file.errorString());
			return false;
		}
	}
	else
	{
		errors << tr("Bad model ID %1").arg(modelId.value);
		return false;
	}
}

/**
 * @brief Searches the open models for the specified model and returns its id if found
 * @param model model to look for
 * @return id or no value if not found
 */
std::optional<ModelId> DocumentManager::findIdForModel(const Model *model) const
{
	std::optional<ModelId> result;
	for (auto it = this->openModels.begin(); it != this->openModels.end(); ++it)
	{
		if (it->second.model.get() == model)
		{
			result = it->first;
			break;
		}
	}
	return result;
}

PolygonCache *DocumentManager::getPolygonCacheForModel(ModelId modelId)
{
	auto it = this->polygonCaches.find(modelId);
	if (it != this->polygonCaches.end())
	{
		return &it->second;
	}
	else
	{
		return nullptr;
	}
}

/**
 * @brief Cleans up and erases models that are no longer required.
 */
void DocumentManager::prune()
{
	for (auto it = this->openModels.begin(); it != this->openModels.end(); ++it)
	{
		// Find models that are not edited by the user and are not needed by any other model
		if (true
			and it->second.opentype == OpenType::AutomaticallyOpened
			and not this->isReferencedByAnything(it->first)
		) {
			// Remove its polygon cache
			const auto polygonCache = this->polygonCaches.find(it->first);
			if (polygonCache != this->polygonCaches.end())
			{
				this->polygonCaches.erase(polygonCache);
			}
			// Remove the model
			this->openModels.erase(it);
			// We need to start over now. It is possible that other models that previously
			// were referenced by the model we just erased have become prunable.
			// Moreover, our iterator is invalid now and we cannot continue in this for loop.
			this->prune();
			break;
		}
	}
}

/**
 * @brief Finds out whether the specified model id is referenced by any other model
 * @param modelId
 * @returns bool
 */
bool DocumentManager::isReferencedByAnything(const ModelId modelId) const
{
	for (auto& haystackModelPair : this->openModels)
	{
		if (haystackModelPair.first != modelId)
		{
			for (auto& dependencyPair : haystackModelPair.second.dependencies)
			{
				if (dependencyPair.second == modelId)
				{
					return true;
				}
			}
		}
	}
	return false;
}

void DocumentManager::makePolygonCacheForModel(const ModelId modelId)
{
	Model* model = this->getModelById(modelId);
	if (model != nullptr)
	{
		this->polygonCaches.emplace(
			std::piecewise_construct,
			std::forward_as_tuple(modelId),
			std::forward_as_tuple(model));
	}
}

static QString findFile(QString referenceName, const QString& path, const LibraryManager& libraries)
{
	// Try to find the file in the same place as the model itself
	referenceName.replace("\\", "/");
	const QDir dir = QFileInfo{path}.dir();
	QString referencedFilePath = dir.filePath(referenceName);
	if (not QFileInfo{referencedFilePath}.exists())
	{
		// Look for it in the libraries
		referencedFilePath = libraries.findFile(referenceName);
	}
	return referencedFilePath;
}

void DocumentManager::loadDependenciesForModel(
	const ModelId modelId,
	const QString &path,
	LoadDepedenciesBag& bag)
{
	QSet<QString> failedToOpen;
	struct LoadingError
	{
		QString message;
	};
	bag.processed.insert(modelId);
	if (not this->openModels.contains(modelId))
	{
		bag.errorStream << tr("bad model ID %1").arg(modelId.value);
		return;
	}
	ModelInfo& modelInfo = this->openModels[modelId];
	modelInfo.dependencies.clear();
	for (int i = 0; i < modelInfo.model->size(); i += 1)
	{
		const QString referenceName = (*modelInfo.model)[i]->getProperty(ldraw::Property::ReferenceName).toString();
		if (not referenceName.isEmpty()
			and modelInfo.dependencies.count(referenceName) == 0
			and not failedToOpen.contains(referenceName))
		{
			try
			{
				const QString referencedFilePath = ::findFile(referenceName, path, bag.libraries);
				if (referencedFilePath.isEmpty())
				{
					throw LoadingError{tr("could not find '%1'").arg(referenceName)};
				}
				QString loadErrorString;
				QTextStream localErrorStream{&loadErrorString};
				const std::optional<ModelId> modelIdOpt = this->openModel(
					referencedFilePath,
					localErrorStream,
					OpenType::AutomaticallyOpened);
				if (not modelIdOpt.has_value())
				{
					const QString& errorMessage = tr("could not load '%1': %2")
						.arg(referencedFilePath)
						.arg(loadErrorString);
					throw LoadingError{errorMessage};
				}
				modelInfo.dependencies[referenceName] = modelIdOpt.value();
				if (not bag.processed.contains(modelIdOpt.value()))
				{
					this->loadDependenciesForModel(modelIdOpt.value(), referencedFilePath, bag);
				}
			}
			catch(const LoadingError& error)
			{
				bag.errorStream << error.message << "\n";
				failedToOpen.insert(referenceName);
				bag.missing.append(referenceName);
			}
		}
	}
}

mercurial