Sun, 09 Apr 2023 12:23:32 +0300
`PartRenderer::renderVao` no longer throws if bad array class is given, this is now checked on compile time
/* * 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 <QPlainTextDocumentLayout> #include <deque> #include "src/documentmanager.h" #include "src/parser.h" DocumentManager::DocumentManager(QObject *parent) : QObject{parent} { } static std::unique_ptr<QTextDocument> newTextDocument() { std::unique_ptr<QTextDocument> newModel = std::make_unique<QTextDocument>(nullptr); newModel->setDocumentLayout(new QPlainTextDocumentLayout{newModel.get()}); return newModel; } /** * @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 = newTextDocument(), .id = modelId, .opentype = OpenType::ManuallyOpened, })); this->makePolygonCacheForModel(modelId); Q_EMIT this->message(logInfo(tr("New model %1 created").arg(modelId.value))); return modelId; } QTextDocument* 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 */ QTextDocument *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<QTextDocument> newModel = newTextDocument(); newModel->setPlainText(file.readAll()); 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)) { file.write(info->model->toPlainText().replace("\n", "\r\n").toUtf8()); 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 QTextDocument *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) { QTextDocument* 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, &QTextDocument::contentsChanged, 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 QTextDocument* model) { std::set<QString> result; for (const QString& line : model->toPlainText().split("\n")) { const opt<ParsedLine> parsed = parse(line); if (parsed.has_value() and std::holds_alternative<LineType1>(*parsed)) { result.insert(std::get<LineType1>(*parsed).value.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; const auto olddeps = info->dependencies; info->dependencies.clear(); while (repeat) { repeat = false; const std::set<Dependency> dependencies = resolveReferencePaths(info, libraries); for (const Dependency& dep : dependencies) { const ModelId* const idp = findInMap(olddeps, dep.name); if (idp != nullptr) { info->dependencies[dep.name] = *idp; } else 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; }