src/documentmanager.cpp

Fri, 01 Jul 2022 16:46:43 +0300

author
Teemu Piippo <teemu.s.piippo@gmail.com>
date
Fri, 01 Jul 2022 16:46:43 +0300
changeset 312
2637134bc37c
parent 264
76a025db4948
child 328
3ea38fd469ca
permissions
-rw-r--r--

Fix right click to delete not really working properly
Instead of removing the point that had been added, it would remove
the point that is being drawn, which would cause it to overwrite the
previous point using the new point, causing a bit of a delay

/*
 *  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 <deque>
#include "src/documentmanager.h"
#include "src/parser.h"

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};
	this->openModels.emplace(std::make_pair(modelId, ModelInfo{
		.model = std::make_unique<Model>(this),
		.id = modelId,
		.opentype = OpenType::ManuallyOpened,
	}));
	this->makePolygonCacheForModel(modelId);
	Q_EMIT this->message(logInfo(tr("New model %1 created").arg(modelId.value)));
	return modelId;
}

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(QFileInfo{path});
	file.open(QFile::ReadOnly | QFile::Text);
	std::unique_ptr<Model> newModel = std::make_unique<Model>(nullptr);
	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.emplace(std::make_pair(modelId, ModelInfo{
			.model = std::move(newModel),
			.id = modelId,
			.path = path,
			.opentype = openType,
			.polygonCache = {},
		}));
		this->makePolygonCacheForModel(modelId);
		result = modelId;
		Q_EMIT this->message(logInfo(tr("Opened %1 as model %2").arg(quoted(path)).arg(modelId.value)));
	}
	else
	{
		errorStream << file.errorString();
	}
	return result;
}

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. This can cause dependencies
//! to be resolved differently. As such, dependencies need to be resolved for
//! all files after this operation.
void DocumentManager::setModelPath(
	const ModelId modelId,
	const QString &newPath,
	const LibrariesModel &libraries,
	QTextStream &errorStream)
{
	ModelInfo* info = findInMap(this->openModels, modelId);
	if (info != nullptr and info->opentype == OpenType::ManuallyOpened) {
		info->path = newPath;
		const MissingDependencies missing = this->loadDependenciesForAllModels(libraries);
		if (not missing.empty()) {
			errorStream << errorStringFromMissingDependencies(missing);
		}
	}
}

bool DocumentManager::saveModel(const ModelId modelId, QTextStream &errors)
{
	ModelInfo* info = findInMap(this->openModels, modelId);
	if (info != nullptr)
	{
		QSaveFile file{info->path};
		file.setDirectWriteFallback(true);
		if (file.open(QSaveFile::WriteOnly)) {
			::save(*info->model.get(), &file);
			const bool commitSucceeded = file.commit();
			if (not commitSucceeded) {
				errors << QObject::tr("Could not save: %1").arg(file.errorString());
				return false;
			}
			else {
				return true;
			}
		}
		else {
			errors << QObject::tr("Could not open %1 for writing: %2")
				.arg(file.fileName(), file.errorString());
			return false;
		}
	}
	else {
		errors << QObject::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)
{
	ModelInfo* info = findInMap(this->openModels, modelId);
	if (info != nullptr) {
		return &info->polygonCache;
	}
	else {
		return nullptr;
	}
}

const DocumentManager::ModelInfo *DocumentManager::find(ModelId modelId) const
{
	return findInMap(this->openModels, modelId);
}

void DocumentManager::setModelPayload(ModelId modelId, QObject *object)
{
	ModelInfo* info = findInMap(this->openModels, modelId);
	if (info != nullptr) {
		info->payload = object;
		object->setParent(this);
	}
}

QString errorStringFromMissingDependencies(const DocumentManager::MissingDependencies& missing)
{
	QString missingString;
	forValueInMap(missing, [&missingString](const QString& path){
		missingString = joined(missingString, QStringLiteral(", "), path);
	});
	return QObject::tr("The following files could not be opened: %1")
		.arg(missingString);
}

template<typename T, typename K>
void removeFromSet(std::set<T>& set, K&& valueToRemove)
{
	const auto it = std::lower_bound(set.begin(), set.end(), valueToRemove);
	if (it != set.end() and *it == valueToRemove) {
		set.erase(it);
	}
}

//! @brief Cleans up and erases models that are no longer required.
void DocumentManager::prune()
{
	Graph<ModelId> dependencyGraph;
	forValueInMap(this->openModels, [&dependencyGraph](const ModelInfo& info) {
		forValueInMap(info.dependencies, [&dependencyGraph, &info](ModelId dep){
			dependencyGraph.push_back({.from = info.id, .to = dep});
		});
	});
	std::set<ModelId> autoOpened;
	forValueInMap(this->openModels, [&autoOpened](const ModelInfo& info) {
		if (info.opentype == OpenType::AutomaticallyOpened) {
			autoOpened.insert(info.id);
		}
	});
	bool repeat = true;
	while (repeat) {
		repeat = false;
		std::set<ModelId> prunable = autoOpened;
		for (const auto& pair : dependencyGraph) {
			removeFromSet(prunable, pair.to);
		}
		for (ModelId idToPrune : prunable) {
			auto it = this->openModels.find(idToPrune);
			if (it != this->openModels.end()) {
				Q_EMIT this->message(logInfo(tr("Model %1 (%2) pruned").arg(idToPrune.value).arg(it->second.path)));
				this->openModels.erase(it);
			}
			removeFromSet(autoOpened, idToPrune);
			std::erase_if(dependencyGraph, [&idToPrune](const GraphEdge<ModelId>& edge) {
				return edge.from == idToPrune;
			});
			repeat = true;
		}
	}
}

void DocumentManager::makePolygonCacheForModel(const ModelId modelId)
{
	Model* model = this->getModelById(modelId);
	if (model != nullptr)
	{
		const auto modelModified = [this, model]{
			const std::optional<ModelId> modelId = this->findIdForModel(model);
			if (modelId.has_value()) {
				ModelInfo* info = findInMap(this->openModels, *modelId);
				if (info != nullptr) {
					info->polygonCache.needRecache = true;
				}
			}
		};
		QObject::connect(model, &Model::dataChanged, modelModified);
		QObject::connect(model, &Model::rowsInserted, modelModified);
		QObject::connect(model, &Model::rowsRemoved, modelModified);
	}
}

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

static std::set<QString> referenceNames(const Model* model)
{
	std::set<QString> result;
	iterate<Colored<SubfileReference>>(*model, [&result](const SubfileReference& ref){
		result.insert(ref.name);
	});
	return result;
}

struct Dependency
{
	QString name;
	QString path;
	bool operator<(const Dependency& other) const
	{
		if (this->name != other.name) {
			return this->name < other.name;
		}
		else {
			return this->path < other.path;
		}
	}
};

static std::set<Dependency> resolveReferencePaths(
	const DocumentManager::ModelInfo* modelInfo,
	const LibrariesModel* libraries)
{
	std::set<Dependency> result;
	const std::set<QString> refNames = referenceNames(modelInfo->model.get());
	if (modelInfo != nullptr) {
		for (const QString& name : refNames) {
			const QString path = findFile(name, modelInfo->path, *libraries);
			if (not path.isEmpty()) {
				result.insert(Dependency{.name = name, .path = path});
			}
		}
	}
	return result;
}

static void loadDependenciesForModel(
	DocumentManager::ModelInfo* info,
	DocumentManager* documents,
	const LibrariesModel* libraries,
	std::map<QString, QString>& missing)
{
	bool repeat = true;
	info->dependencies.clear();
	while (repeat) {
		repeat = false;
		const std::set<Dependency> dependencies = resolveReferencePaths(info, libraries);
		for (const Dependency& dep : dependencies) {
			if (not info->dependencies.contains(dep.name) and not missing.contains(dep.path)) {
				QString loadErrorString;
				QTextStream localErrorStream{&loadErrorString};
				const std::optional<ModelId> modelIdOpt = documents->openModel(
					dep.path,
					localErrorStream,
					OpenType::AutomaticallyOpened);
				if (not modelIdOpt.has_value()) {
					const QString& errorMessage = QObject::tr("could not load '%1': %2")
						.arg(dep.path, loadErrorString);
					missing[dep.path] = errorMessage;
				}
				else {
					info->dependencies[dep.name] = modelIdOpt.value();
					repeat = true;
				}
			}
		}
	}
}

std::map<QString, QString> DocumentManager::loadDependenciesForAllModels(const LibrariesModel& libraries)
{
	std::map<QString, QString> missing;
	for (auto& modelInfoPair : this->openModels)
	{
		loadDependenciesForModel(&modelInfoPair.second, this, &libraries, missing);
	}
	this->prune();
	return missing;
}

mercurial