Thu, 03 Oct 2019 11:45:44 +0300
stuff
/* * LDForge: LDraw parts authoring CAD * Copyright (C) 2013 - 2018 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 "objecttypes/comment.h" #include "objecttypes/conditionaledge.h" #include "objecttypes/edge.h" #include "objecttypes/errorline.h" #include "objecttypes/modelobject.h" #include "objecttypes/polygon.h" #include "objecttypes/subfilereference.h" /* * 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, decltype(LDHeader::type)> 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(" ", QString::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 = 0; if (tokens.contains("Alias")) header.qualfiers |= LDHeader::Alias; if (tokens.contains("Physical_Color")) header.qualfiers |= LDHeader::Physical_Color; if (tokens.contains("Flexible_Section")) header.qualfiers |= LDHeader::Flexible_Section; return ParseSuccess; } else { return ParseFailure; } } else if (line == "0 BFC CERTIFY CCW") { winding = CounterClockwise; 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; } modelobjects::BaseObject* object = parseFromString(editor, line); if (invertNext) { editor.setObjectProperty(object, modelobjects::Property::IsInverted, true); } invertNext = false; } } namespace { namespace regexes { static const QRegExp comment {R"(^\s*0\s*\/\/\s*(.+)$)"}; static const QRegExp metacommand {R"(^\s*0\s*(.+)$)"}; static const QRegExp edgeline { R"(^\s*2)" // starting 2-token R"(\s+(\d+))" // colour R"(((?:\s+[^\s]+){3}))" // 1st vertex R"(((?:\s+[^\s]+){3}))" // 2nd vertex R"(\s*$)" // end }; static const QRegExp triangle { R"(^\s*3)" // starting 3-token R"(\s+(\d+))" // colour R"(((?:\s+[^\s]+){3}))" // 1st vertex R"(((?:\s+[^\s]+){3}))" // 2nd vertex R"(((?:\s+[^\s]+){3}))" // 3rd vertex R"(\s*$)" // end }; static const QRegExp quadrilateral { R"(^\s*4)" // starting 4-token R"(\s+(\d+))" // colour R"(((?:\s+[^\s]+){3}))" // 1st vertex R"(((?:\s+[^\s]+){3}))" // 2nd vertex R"(((?:\s+[^\s]+){3}))" // 3rd vertex R"(((?:\s+[^\s]+){3}))" // 4th vertex R"(\s*$)" // end }; static const QRegExp conditionaledge { R"(^\s*5)" // starting 5-token R"(\s+(\d+))" // colour R"(((?:\s+[^\s]+){3}))" // 1st vertex R"(((?:\s+[^\s]+){3}))" // 2nd vertex R"(((?:\s+[^\s]+){3}))" // 1st control point R"(((?:\s+[^\s]+){3}))" // 2nd control point R"(\s*$)" // end }; } } static Vertex vertexFromString(const QString& vertex_string) { static const QRegExp pattern {R"(^\s*([^\s]+)\s+([^\s]+)\s+([^\s]+)\s*$)"}; const bool succeeded = pattern.exactMatch(vertex_string); if (succeeded) { const float x = pattern.cap(1).toFloat(nullptr); const float y = pattern.cap(2).toFloat(nullptr); const float z = pattern.cap(3).toFloat(nullptr); return {x, y, z}; } else { return {}; } } static modelobjects::Edge* parseEdgeline( Model::EditContext& editor, const QString& line) { const bool succeeded = regexes::edgeline.exactMatch(line); if (succeeded) { const Color colour = {regexes::edgeline.cap(1).toInt(nullptr)}; const Vertex v_1 = vertexFromString(regexes::edgeline.cap(2)); const Vertex v_2 = vertexFromString(regexes::edgeline.cap(3)); return editor.append<modelobjects::Edge>(v_1, v_2, colour); } else { return nullptr; } } modelobjects::BaseObject* Parser::parseFromString( Model::EditContext& editor, const QString& line) { modelobjects::Edge* edge = parseEdgeline(editor, line); if (edge) { return edge; } return editor.append<modelobjects::ErrorLine>(line); }