Wed, 22 Sep 2021 13:28:53 +0300
Document model.h
/* * 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 "model.h" #include "parser.h" #include "linetypes/comment.h" #include "linetypes/conditionaledge.h" #include "linetypes/edge.h" #include "linetypes/errorline.h" #include "linetypes/metacommand.h" #include "linetypes/object.h" #include "linetypes/quadrilateral.h" #include "linetypes/subfilereference.h" #include "linetypes/triangle.h" struct BodyParseError { QString message; }; /* * Constructs an LDraw parser */ Parser::Parser(QIODevice& device, QObject* parent) : QObject {parent}, device {device} {} /* * Reads a single line from the device. */ QString Parser::readLine() { return QString::fromUtf8(this->device.readLine()).trimmed(); } static const QMap<QString, LDHeader::FileType> typeStrings { {"Part", LDHeader::Part}, {"Subpart", LDHeader::Subpart}, {"Shortcut", LDHeader::Shortcut}, {"Primitive", LDHeader::Primitive}, {"8_Primitive", LDHeader::Primitive_8}, {"48_Primitive", LDHeader::Primitive_48}, {"Configuration", LDHeader::Configuration}, }; /* * Parses a single line of the header. * Possible parse results: * · ParseSuccess: the header line was parsed successfully. * · ParseFailure: the header line was parsed incorrectly and needs to be handled otherwise. * · StopParsing: the line does not belong in the header and header parsing needs to stop. */ Parser::HeaderParseResult Parser::parseHeaderLine( LDHeader& header, Winding& winding, const QString& line ) { if (line.isEmpty()) { return ParseSuccess; } else if (not line.startsWith("0") or line.startsWith("0 //")) { return StopParsing; } else if (line.startsWith("0 !LDRAW_ORG ")) { QStringList tokens = line .mid(strlen("0 !LDRAW_ORG ")) .split(" ", Qt::SkipEmptyParts); if (not tokens.isEmpty()) { QString partTypeString = tokens[0]; // Anything that enters LDForge becomes unofficial in any case if saved. // Therefore we don't need to give the Unofficial type any special // consideration. if (partTypeString.startsWith("Unofficial_")) partTypeString = partTypeString.mid(strlen("Unofficial_")); header.type = typeStrings.value(partTypeString, LDHeader::Part); header.qualfiers = {}; if (tokens.contains("Alias")) header.qualfiers |= LDHeader::Alias; if (tokens.contains("Physical_Color")) header.qualfiers |= LDHeader::PhysicalColour; if (tokens.contains("Flexible_Section")) header.qualfiers |= LDHeader::FlexibleSection; return ParseSuccess; } else { return ParseFailure; } } else if (line == "0 BFC CERTIFY CCW") { winding = Anticlockwise; return ParseSuccess; } else if (line == "0 BFC CERTIFY CW") { winding = Clockwise; return ParseSuccess; } else if (line == "0 BFC NOCERTIFY") { winding = NoWinding; return ParseSuccess; } else if (line.startsWith("0 !HISTORY ")) { static const QRegExp historyRegexp { R"(0 !HISTORY\s+(\d{4}-\d{2}-\d{2})\s+)" R"((\{[^}]+|\[[^]]+)[\]}]\s+(.+))" }; if (historyRegexp.exactMatch(line)) { QString dateString = historyRegexp.capturedTexts().value(1); QString authorWithPrefix = historyRegexp.capturedTexts().value(2); QString description = historyRegexp.capturedTexts().value(3); LDHeader::HistoryEntry historyEntry; historyEntry.date = QDate::fromString(dateString, Qt::ISODate); historyEntry.description = description; if (authorWithPrefix[0] == '{') historyEntry.author = authorWithPrefix + "}"; else historyEntry.author = authorWithPrefix.mid(1); header.history.append(historyEntry); return ParseSuccess; } else { return ParseFailure; } } else if (line.startsWith("0 Author: ")) { header.author = line.mid(strlen("0 Author: ")); return ParseSuccess; } else if (line.startsWith("0 Name: ")) { header.name = line.mid(strlen("0 Name: ")); return ParseSuccess; } else if (line.startsWith("0 !HELP ")) { if (not header.help.isEmpty()) header.help += "\n"; header.help += line.mid(strlen("0 !HELP ")); return ParseSuccess; } else if (line.startsWith("0 !KEYWORDS ")) { if (not header.keywords.isEmpty()) header.keywords += "\n"; header.keywords += line.mid(strlen("0 !KEYWORDS ")); return ParseSuccess; } else if (line.startsWith("0 !CATEGORY ")) { header.category = line.mid(strlen("0 !CATEGORY ")); return ParseSuccess; } else if (line.startsWith("0 !CMDLINE ")) { header.cmdline = line.mid(strlen("0 !CMDLINE ")); return ParseSuccess; } else if (line.startsWith("0 !LICENSE Redistributable under CCAL version 2.0")) { header.license = LDHeader::CaLicense; return ParseSuccess; } else if (line.startsWith("0 !LICENSE Not redistributable")) { header.license = LDHeader::NonCaLicense; return ParseSuccess; } else { return ParseFailure; } } /* * Parses the header from the device given at construction and returns * the resulting header structure. */ LDHeader Parser::parseHeader(Winding& winding) { LDHeader header = {}; if (not this->device.atEnd()) { // Parse the description QString descriptionLine = this->readLine(); if (descriptionLine.startsWith("0 ")) { header.description = descriptionLine.mid(strlen("0 ")).trimmed(); // Parse the rest of the header while (not this->device.atEnd()) { const QString& line = this->readLine(); auto result = parseHeaderLine(header, winding, line); if (result == ParseFailure) { // Failed to parse this header line, add it as a comment into the body later. this->bag.append(line); } else if (result == StopParsing) { // Header parsing stops, add this line to the body. this->bag.append(line); break; } } } else { this->bag.append(descriptionLine); } } return header; } /** * @brief Parses the model body into the given model. * @param editor Handle to model edit context */ void Parser::parseBody(Model::EditContext& editor) { bool invertNext = false; while (not this->device.atEnd()) this->bag.append(this->readLine()); for (const QString& line : this->bag) { if (line == "0 BFC INVERTNEXT" or line == "0 BFC CERTIFY INVERTNEXT") { invertNext = true; continue; } std::unique_ptr<ldraw::Object> object = parseFromString(line); auto id = editor.append(std::move(object)); if (invertNext) { editor.invertObject(id); } invertNext = false; } // Test quadrilateral splitting by splitting all the quadrilaterals QVector<ldraw::quadrilateralid_t> quadrilateral_ids; for (int i = 0; i < editor.model().size(); i += 1) { const ldraw::id_t id = editor.model().resolve(editor.model().index(i)); const ldraw::quadrilateralid_t quad_id = editor.model().checkType<ldraw::Quadrilateral>(id); if (not(quad_id == ldraw::NULL_ID)) { quadrilateral_ids.push_back(quad_id); } } for (const ldraw::quadrilateralid_t id : quadrilateral_ids) { ldraw::splitQuadrilateral(editor, id); } } static ldraw::Color colorFromString(const QString& colorString) { bool colorSucceeded; const ldraw::Color color = {colorString.toInt(&colorSucceeded)}; if (colorSucceeded) { return color; } else { throw BodyParseError{"colour was not an integer value"}; } } static glm::vec3 vertexFromStrings( const QStringList& tokens, const int startingPosition) { bool ok_x; const float x = tokens[startingPosition].toFloat(&ok_x); bool ok_y; const float y = tokens[startingPosition + 1].toFloat(&ok_y); bool ok_z; const float z = tokens[startingPosition + 2].toFloat(&ok_z); if (not ok_x or not ok_y or not ok_z) { throw BodyParseError{"vertex contained illegal co-ordinates"}; } return {x, y, z}; } static glm::mat4 matrixFromStrings( const QStringList& tokens, const int startingPosition, const int positionStartingIndex) { glm::mat4 result = glm::mat4{1}; for (int i = 0; i < 9; i += 1) { const int row = i / 3; const int column = i % 3; const int index = i + startingPosition; if (index >= tokens.size()) { throw BodyParseError{"too few tokens available"}; } bool ok; // note that glm::mat4 is column-major result[column][row] = tokens[index].toFloat(&ok); if (not ok) { throw BodyParseError{"non-numeric values for matrix"}; } } for (int i = 0; i < 3; i += 1) { bool ok; const auto value = tokens[i + positionStartingIndex].toFloat(&ok); result[3][i] = value; if (not ok) { throw BodyParseError{"non-numeric values for matrix"}; } } return result; } static std::unique_ptr<ldraw::Object> parseType0Line( const QString& line, const QStringList& tokens) { Q_UNUSED(tokens) if (line.startsWith("0 //")) { return std::make_unique<ldraw::Comment>(line.mid(std::strlen("0 //")).simplified()); } else { return std::make_unique<ldraw::MetaCommand>(line.mid(1).simplified()); } } static std::unique_ptr<ldraw::SubfileReference> parseType1Line( const QString& line, const QStringList& tokens) { Q_UNUSED(line) constexpr int colorPosition = 1; constexpr int positionPosition = 2; // 2..4 constexpr int transformPosition = 5; // 5..13 constexpr int namePosition = 14; if (tokens.size() != 15) { throw BodyParseError{"wrong amount of tokens in a type-1 line"}; } const ldraw::Color color = colorFromString(tokens[colorPosition]); const glm::mat4 transform = matrixFromStrings(tokens, transformPosition, positionPosition); const QString& name = tokens[namePosition]; return std::make_unique<ldraw::SubfileReference>(transform, name, color); } template<typename T, int NumVertices> static std::unique_ptr<T> parsePolygon( const QString& line, const QStringList& tokens) { Q_UNUSED(line) constexpr int colorPosition = 1; auto vertexPosition = [](int n) { return 2 + 3*n; }; if (tokens.size() != 2 + 3 * NumVertices) { throw BodyParseError{"wrong amount of tokens"}; } const ldraw::Color color = colorFromString(tokens[colorPosition]); std::array<glm::vec3, NumVertices> vertices; for (int i = 0; i < NumVertices; i += 1) { vertices[unsigned_cast(i)] = vertexFromStrings(tokens, vertexPosition(i)); } return std::make_unique<T>(vertices, color); } std::unique_ptr<ldraw::Object> Parser::parseFromString(QString line) { line = line.simplified(); try { const QStringList tokens = line.split(QRegExp{R"(\s+)"}); if (tokens.empty() or tokens == QStringList{{""}}) { return std::make_unique<ldraw::Empty>(); } bool ok_code; const int code = tokens[0].toInt(&ok_code); if (not ok_code) { throw BodyParseError{"line type was not an integer"}; } switch (code) { case 0: return parseType0Line(line, tokens); case 1: return parseType1Line(line, tokens); case 2: return parsePolygon<ldraw::Edge, 2>(line, tokens); case 3: return parsePolygon<ldraw::Triangle, 3>(line, tokens); case 4: return parsePolygon<ldraw::Quadrilateral, 4>(line, tokens); case 5: return parsePolygon<ldraw::ConditionalEdge, 4>(line, tokens); default: throw BodyParseError{utility::format("bad line type '%1'", code)}; } } catch(const BodyParseError& error) { return std::make_unique<ldraw::ErrorLine>(line, error.message); } }