Tue, 28 Sep 2021 22:17:52 +0300
Fix handling of "BFC INVERTNEXT" with multiple inner whitespaces
/* * 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 <QBrush> #include <QFile> #include <QFileInfo> #include <QFont> #include "model.h" #include "modeleditcontext.h" /** * @brief Constructs a model * @param parent QObject parent to pass forward */ Model::Model(QObject* parent) : Model{"", parent} {} /** * @brief Constructs a model * @param path Path that was used to open the model * @param parent QObject parent to pass forward */ Model::Model(const QString& path, QObject *parent) : QAbstractListModel{parent}, storedPath{path} { connect(this, &Model::dataChanged, [&](){ this->needRecache = true; }); } /** * @returns the amount of elements in the model */ int Model::size() const { return static_cast<int>(this->body.size()); } /** * @brief Looks up the object ID at the specified index. If out of bounds, returns NULL_ID. * @param index Index of object to look up * @return object ID */ ldraw::id_t Model::at(int index) const { if (index >= 0 and index < this->size()) { return this->body[index]->id; } else { return ldraw::NULL_ID; } } /** * @brief Obtains an editing context for this model. * Editing contexts are used to perform modifications to the model. * @return editing context */ Model::EditContext Model::edit() { this->editCounter += 1; return {*this}; } /** * @brief @overload QAbstractListModel::rowCount * @return size */ int Model::rowCount(const QModelIndex&) const { return this->size(); } /** * @brief @overload QAbstractListModel::data * @param index * @param role * @return QVariant */ QVariant Model::data(const QModelIndex& index, int role) const { const ldraw::Object* object = this->objectAt(index); switch(role) { case Qt::DisplayRole: return object->textRepresentation(); case Qt::ForegroundRole: return object->textRepresentationForeground(); case Qt::BackgroundRole: return object->textRepresentationBackground(); case Qt::FontRole: return object->textRepresentationFont(); default: return {}; } } /** * @brief Gets a property of the header * @param property * @return QVariant */ QVariant Model::getHeaderProperty(const HeaderProperty property) { switch (property) { case HeaderProperty::Name: return header.name; } return {}; } /** * @brief Gets the specified property from the object at the specified index in the model. * @param index Index of object in the model * @param property Property to look up * @return QVariant */ QVariant Model::getObjectProperty(const int index, const ldraw::Property property) const { const ldraw::Object* object = this->body[unsigned_cast(index)].get(); return object->getProperty(property); } /** * @brief Gets a list of GL polygons that are used to represent this model. * @details Will build polygons if polygons are outdated. * @param documents Documents to use to resolve subfile references * @return vector of GL polygons */ std::vector<gl::Polygon> Model::getPolygons(DocumentManager* documents) const { if (this->needRecache) { this->cachedPolygons.clear(); ldraw::GetPolygonsContext context{documents}; for (int i = 0; i < this->size(); i += 1) { this->getObjectPolygons(i, this->cachedPolygons, &context); } this->needRecache = false; } return this->cachedPolygons; } /** * @brief Finds the position of the specified object in the model * @param id Object id to look for * @return model index */ QModelIndex Model::lookup(ldraw::id_t id) const { // FIXME: This linear search will probably cause performance issues for (std::size_t i = 0; i < this->body.size(); i += 1) { if (this->body[i]->id == id) { return this->index(static_cast<int>(i)); } } return {}; } /** * @brief Gets an object id by position in the model * @param index Position of the object in the model * @return id */ ldraw::id_t Model::resolve(const QModelIndex& index) const { return this->objectAt(index)->id; } /** * @brief Gets the path to the model * @return path */ const QString& Model::path() const { return this->storedPath; } /** * @brief Sets the path to the model * @param path New path to use */ void Model::setPath(const QString &path) { this->storedPath = path; this->header.name = QFileInfo{path}.fileName(); // Update the "Name: 1234.dat" comment if (this->body.size() >= 2) { const ldraw::id_t id = this->body[1]->id; if (this->isA<ldraw::MetaCommand>(id)) { const QString& textBody = this->body[1]->getProperty<ldraw::Property::Text>(); if (textBody.startsWith("Name: ")) { auto editor = this->edit(); editor.setObjectProperty<ldraw::Property::Text>(id, "Name: " + this->header.name); } } } } /** * @brief Gets the GL polygons of the object at the specified position in the model * @param index Index of object in the model * @param polygons_out Vector to add polygons into * @param context Context to use to resolve subfile references */ void Model::getObjectPolygons( const int index, std::vector<gl::Polygon>& polygons_out, ldraw::GetPolygonsContext* context) const { const ldraw::Object* object = this->body[unsigned_cast(index)].get(); object->getPolygons(polygons_out, context); } /** * @brief Called by the editing context to signal to the model that editing is done. */ void Model::editFinished() { this->editCounter -= 1; } /** * @brief Called by the editing context to indicate that the specified object has been modified. * @param id ID of the object that has been modified */ void Model::objectModified(ldraw::id_t id) { const QModelIndex index = this->lookup(id); Q_EMIT this->dataChanged(index, index); } /** * @brief Adds the given object into the model. * @param object r-value reference to the object */ void Model::append(ModelObjectPointer&& object) { const int position = static_cast<int>(this->body.size()); Q_EMIT beginInsertRows({}, position, position); this->body.push_back(std::move(object)); Q_EMIT endInsertRows(); this->needRecache = true; } /** * @brief Removes the object at the specified position * @param position */ void Model::remove(int position) { if (position >= 0 and position < signed_cast(this->body.size())) { Q_EMIT beginRemoveRows({}, position, position); this->body.erase(std::begin(this->body) + position); Q_EMIT endRemoveRows(); this->needRecache = true; } } /** * @brief Gets the object pointer at the specified position * @param index Position of the object * @returns object pointer */ ldraw::Object* Model::objectAt(const QModelIndex& index) { return this->body[unsigned_cast(index.row())].get(); } /** * @brief Gets the object pointer at the specified position * @param index Position of the object * @returns object pointer */ const ldraw::Object* Model::objectAt(const QModelIndex& index) const { return this->body[unsigned_cast(index.row())].get(); } /** * @brief Write out the model as text */ bool Model::save(QTextStream &errors) const { QFile file{this->storedPath}; if (file.open(QIODevice::WriteOnly)) { QTextStream out{&file}; for (const ModelObjectPointer& object : this->body) { out << object.get()->toLDrawCode() << "\r\n"; } file.close(); if (out.status() != QTextStream::Ok) { errors << tr("Write error while writing to %1").arg(this->storedPath); return false; } else { return true; } } else { errors << tr("Could not open %1 for writing: %2") .arg(this->storedPath) .arg(file.errorString()); return false; } } /** * @brief Modifies the !LDRAW_ORG line so that it becomes unofficial */ void Model::makeUnofficial() { if (this->body.size() >= 4) { const ldraw::id_t id = this->body[3]->id; if (this->isA<ldraw::MetaCommand>(id)) { const QString& body = this->body[3]->getProperty<ldraw::Property::Text>(); if (body.startsWith("!LDRAW_ORG ") and not body.startsWith("!LDRAW_ORG Unofficial_")) { QStringList tokens = body.split(" "); tokens[1] = "Unofficial_" + tokens[1]; // Remove the UPDATE tag if (tokens.size() >= 4 && tokens[2] == "UPDATE") { tokens.removeAt(3); tokens.removeAt(2); } EditContext editor = this->edit(); editor.setObjectProperty<ldraw::Property::Text>(id, tokens.join(" ")); } } } }