--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parser.cpp Fri Mar 16 11:50:35 2018 +0200 @@ -0,0 +1,447 @@ +/* + * LDForge: LDraw parts authoring CAD + * Copyright (C) 2013 - 2017 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 "parser.h" +#include "lddocument.h" +#include "linetypes/comment.h" +#include "linetypes/conditionaledge.h" +#include "linetypes/edgeline.h" +#include "linetypes/empty.h" +#include "linetypes/quadrilateral.h" +#include "linetypes/triangle.h" + +Parser::Parser(QIODevice& device, QObject* parent) : + QObject {parent}, + device {device} {} + +QString Parser::readLine() +{ + return QString::fromUtf8(this->device.readLine()).simplified(); +} + +Parser::HeaderParseResult Parser::parseHeaderLine(LDHeader& header, 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()) + { + 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}, + }; + 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") + { + header.winding = LDHeader::CounterClockwise; + return ParseSuccess; + } + else if (line == "0 BFC CERTIFY CW") + { + header.winding = LDHeader::Clockwise; + return ParseSuccess; + } + else if (line == "0 BFC NOCERTIFY") + { + header.winding = LDHeader::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(0); + QString authorWithPrefix = historyRegexp.capturedTexts().value(1); + QString description = historyRegexp.capturedTexts().value(2); + LDHeader::HistoryEntry historyEntry; + historyEntry.date = QDate::fromString(dateString, Qt::ISODate); + historyEntry.author = authorWithPrefix.mid(1); + historyEntry.description = description; + + if (authorWithPrefix[0] == '{') + historyEntry.authorType = LDHeader::HistoryEntry::RealName; + else + historyEntry.authorType = LDHeader::HistoryEntry::UserName; + + header.history.append(historyEntry); + return ParseSuccess; + } + else + { + return ParseFailure; + } + } + else if (line.startsWith("0 Author: ")) + { + static const QRegExp authorRegexp {R"(0 Author: ([^[]+)(?: \[([^]]+)\])?)"}; + if (authorRegexp.exactMatch(line)) + { + QStringList tokens = authorRegexp.capturedTexts(); + header.author.realName = tokens.value(0); + header.author.userName = tokens.value(1); + return ParseSuccess; + } + else + { + return ParseFailure; + } + } + else if (line.startsWith("0 Name: ")) + { + header.name = line.mid(strlen("0 Name: ")); + return ParseSuccess; + } + else if (line.startsWith("0 !HELP ")) + { + header.help.append(line.mid(strlen("0 !HELP "))); + return ParseSuccess; + } + else if (line.startsWith("0 !KEYWORDS ")) + { + header.keywords.append(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; + } +} + +LDHeader Parser::parseHeader() +{ + LDHeader header = {}; + + if (not this->device.atEnd()) + { + // Parse the description + QString descriptionLine = this->readLine(); + if (descriptionLine.startsWith("0 ")) + { + header.description = descriptionLine.mid(strlen("0 ")).simplified(); + + // Parse the rest of the header + while (not this->device.atEnd()) + { + const QString& line = this->readLine(); + auto result = parseHeaderLine(header, line); + + if (result == ParseFailure) + { + this->bag.append(line); + } + else if (result == StopParsing) + { + this->bag.append(line); + break; + } + } + } + else + { + this->bag.append(descriptionLine); + } + } + + return header; +} + +void Parser::parseBody(Model& model) +{ + 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; + } + + LDObject* object = parseFromString(model, model.size(), line); + + /* + // Check for parse errors and warn about them + if (obj->type() == LDObjectType::Error) + { + emit parseErrorMessage(format( + tr("Couldn't parse line #%1: %2"), + progress() + 1, static_cast<LDError*> (obj)->reason())); + ++m_warningCount; + } + */ + + if (invertNext and object->type() == LDObjectType::SubfileReference) + object->setInverted(true); + + invertNext = false; + } +} + +// ============================================================================= +// +static void CheckTokenCount (const QStringList& tokens, int num) +{ + if (countof(tokens) != num) + throw QString (format ("Bad amount of tokens, expected %1, got %2", num, countof(tokens))); +} + +// ============================================================================= +// +static void CheckTokenNumbers (const QStringList& tokens, int min, int max) +{ + bool ok; + QRegExp scientificRegex ("\\-?[0-9]+\\.[0-9]+e\\-[0-9]+"); + + for (int i = min; i <= max; ++i) + { + // Check for floating point + tokens[i].toDouble (&ok); + if (ok) + return; + + // Check hex + if (tokens[i].startsWith ("0x")) + { + tokens[i].mid (2).toInt (&ok, 16); + + if (ok) + return; + } + + // Check scientific notation, e.g. 7.99361e-15 + if (scientificRegex.exactMatch (tokens[i])) + return; + + throw QString (format ("Token #%1 was `%2`, expected a number (matched length: %3)", + (i + 1), tokens[i], scientificRegex.matchedLength())); + } +} + +static Vertex parseVertex(QStringList& tokens, const int n) +{ + return {tokens[n].toDouble(), tokens[n + 1].toDouble(), tokens[n + 2].toDouble()}; +} + +// TODO: rewrite this using regular expressions +LDObject* Parser::parseFromString(Model& model, int position, QString line) +{ + if (position == EndOfModel) + position = model.size(); + + try + { + QStringList tokens = line.split(" ", QString::SkipEmptyParts); + + if (tokens.isEmpty()) + { + // Line was empty, or only consisted of whitespace + return model.emplaceAt<LDEmpty>(position); + } + + if (countof(tokens[0]) != 1 or not tokens[0][0].isDigit()) + throw QString ("Illogical line code"); + + int num = tokens[0][0].digitValue(); + + switch (num) + { + case 0: + { + // Comment + QString commentText = line.mid(line.indexOf("0") + 2); + QString commentTextSimplified = commentText.simplified(); + + // Handle BFC statements + if (countof(tokens) > 2 and tokens[1] == "BFC") + { + for (BfcStatement statement : iterateEnum<BfcStatement>()) + { + if (commentTextSimplified == format("BFC %1", LDBfc::statementToString(statement))) + return model.emplaceAt<LDBfc>(position, statement); + } + + // handle MLCAD nonsense + if (commentTextSimplified == "BFC CERTIFY CLIP") + return model.emplaceAt<LDBfc>(position, BfcStatement::Clip); + else if (commentTextSimplified == "BFC CERTIFY NOCLIP") + return model.emplaceAt<LDBfc>(position, BfcStatement::NoClip); + } + + if (countof(tokens) > 2 and tokens[1] == "!LDFORGE") + { + // Handle LDForge-specific types, they're embedded into comments too + if (tokens[2] == "BEZIER_CURVE") + { + CheckTokenCount (tokens, 16); + CheckTokenNumbers (tokens, 3, 15); + LDBezierCurve* obj = model.emplaceAt<LDBezierCurve>(position); + obj->setColor(tokens[3].toInt(nullptr, 0)); + + for (int i = 0; i < 4; ++i) + obj->setVertex (i, parseVertex (tokens, 4 + (i * 3))); + + return obj; + } + } + + // Just a regular comment: + return model.emplaceAt<LDComment>(position, commentText); + } + + case 1: + { + // Subfile + CheckTokenCount (tokens, 15); + CheckTokenNumbers (tokens, 1, 13); + + Vertex referncePosition = parseVertex (tokens, 2); // 2 - 4 + Matrix transform; + + for (int i = 0; i < 9; ++i) + transform.value(i) = tokens[i + 5].toDouble(); // 5 - 13 + + LDSubfileReference* obj = model.emplaceAt<LDSubfileReference>(position, tokens[14], transform, referncePosition); + obj->setColor (tokens[1].toInt(nullptr, 0)); + return obj; + } + + case 2: + { + CheckTokenCount (tokens, 8); + CheckTokenNumbers (tokens, 1, 7); + + // Line + LDEdgeLine* obj = model.emplaceAt<LDEdgeLine>(position); + obj->setColor (tokens[1].toInt(nullptr, 0)); + + for (int i = 0; i < 2; ++i) + obj->setVertex (i, parseVertex (tokens, 2 + (i * 3))); // 2 - 7 + + return obj; + } + + case 3: + { + CheckTokenCount (tokens, 11); + CheckTokenNumbers (tokens, 1, 10); + + // Triangle + LDTriangle* obj = model.emplaceAt<LDTriangle>(position); + obj->setColor (tokens[1].toInt(nullptr, 0)); + + for (int i = 0; i < 3; ++i) + obj->setVertex (i, parseVertex (tokens, 2 + (i * 3))); // 2 - 10 + + return obj; + } + + case 4: + case 5: + { + CheckTokenCount (tokens, 14); + CheckTokenNumbers (tokens, 1, 13); + + // Quadrilateral / Conditional line + LDObject* obj; + + if (num == 4) + obj = model.emplaceAt<LDQuadrilateral>(position); + else + obj = model.emplaceAt<LDConditionalEdge>(position); + + obj->setColor (tokens[1].toInt(nullptr, 0)); + + for (int i = 0; i < 4; ++i) + obj->setVertex (i, parseVertex (tokens, 2 + (i * 3))); // 2 - 13 + + return obj; + } + + default: + throw QString {"Unknown line code number"}; + } + } + catch (QString& errorMessage) + { + // Strange line we couldn't parse + return model.emplaceAt<LDError>(position, line, errorMessage); + } +}