Fri, 16 Mar 2018 11:50:35 +0200
Header parsing complete, moved all parsing code into a new class. Documents are now all loaded in one go.
--- a/CMakeLists.txt Thu Mar 15 18:51:58 2018 +0200 +++ b/CMakeLists.txt Fri Mar 16 11:50:35 2018 +0200 @@ -32,7 +32,6 @@ src/colors.cpp src/crashCatcher.cpp src/documentation.cpp - src/documentloader.cpp src/documentmanager.cpp src/editHistory.cpp src/glcamera.cpp @@ -52,6 +51,7 @@ src/model.cpp src/partdownloader.cpp src/partdownloadrequest.cpp + src/parser.cpp src/primitives.cpp src/serializer.cpp src/ringFinder.cpp @@ -95,7 +95,6 @@ src/colors.h src/crashCatcher.h src/documentation.h - src/documentloader.h src/documentmanager.h src/editHistory.h src/format.h @@ -119,6 +118,7 @@ src/model.h src/partdownloader.h src/partdownloadrequest.h + src/parser.h src/primitives.h src/ringFinder.h src/serializer.h
--- a/src/documentloader.cpp Thu Mar 15 18:51:58 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,202 +0,0 @@ -/* - * 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 "documentloader.h" -#include "lddocument.h" -#include "linetypes/modelobject.h" -#include "mainwindow.h" -#include "dialogs/openprogressdialog.h" - -DocumentLoader::DocumentLoader( - Model* model, - LDHeader& header, - bool onForeground, - QObject *parent, -) : - QObject {parent}, - _model {model}, - _header {header}, - m_isOnForeground {onForeground} {} - -bool DocumentLoader::hasAborted() -{ - return m_hasAborted; -} - -bool DocumentLoader::isDone() const -{ - return m_isDone; -} - -int DocumentLoader::progress() const -{ - return m_progress; -} - -int DocumentLoader::warningCount() const -{ - return m_warningCount; -} - -bool DocumentLoader::isOnForeground() const -{ - return m_isOnForeground; -} - -const QVector<LDObject*>& DocumentLoader::objects() const -{ - return _model->objects(); -} - -void DocumentLoader::read (QIODevice* fp) -{ - if (fp and fp->isOpen()) - { - while (not fp->atEnd()) - m_lines << QString::fromUtf8(fp->readLine()).simplified(); - } -} - -void DocumentLoader::start() -{ - m_isDone = false; - m_progress = 0; - m_hasAborted = false; - - if (isOnForeground()) - { - // Show a progress dialog if we're loading the main lddocument.here so we can show progress updates and keep the - // WM posted that we're still here. - m_progressDialog = new OpenProgressDialog(qobject_cast<QWidget*>(parent())); - m_progressDialog->setNumLines (countof(m_lines)); - m_progressDialog->setModal (true); - m_progressDialog->show(); - connect (this, SIGNAL (workDone()), m_progressDialog, SLOT (accept())); - connect (m_progressDialog, SIGNAL (rejected()), this, SLOT (abort())); - } - else - { - m_progressDialog = nullptr; - } - - // Parse the header - while (m_progress < m_lines.size()) - { - const QString& line = m_lines[m_progress]; - - if (not line.isEmpty()) - { - if (line.startsWith("0")) - { - if (m_progress == 0) - { - _header.description = line.mid(1).simplified(); - } - else if (line.startsWith("0 !LDRAW_ORG")) - { - QStringList tokens = line.mid(strlen("0 !LDRAW_ORG")); - } - else if (line.startsWith("0 BFC")) - { - ...; - } - else - { - _model->addFromString(line); - } - m_progress += 1; - } - else - { - break; - } - } - } - - // Begin working - work (0); -} - -void DocumentLoader::work (int i) -{ - // User wishes to abort, so stop here now. - if (hasAborted()) - { - m_isDone = true; - return; - } - - // Parse up to 200 lines per iteration - int max = i + 200; - - bool invertNext = false; - - for (; i < max and i < (int) countof(m_lines); ++i) - { - const QString& line = m_lines[i]; - - if (line == "0 BFC INVERTNEXT") - { - invertNext = true; - continue; - } - - LDObject* obj = _model->addFromString(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 obj->type() == LDObjectType::SubfileReference) - obj->setInverted(true); - - invertNext = false; - } - - m_progress = i; - - if (m_progressDialog) - m_progressDialog->setProgress (i); - - if (i >= countof(m_lines) - 1) - { - emit workDone(); - m_isDone = true; - } - else - { - // If we have a dialog to show progress output to, we cannot just call work() again immediately as the dialog - // needs to be updated as well. Thus, we take a detour through the event loop by using the meta-object system. - // - // This terminates the loop here and control goes back to the function which called the file loader. It will - // keep processing the event loop until we're ready (see loadFileContents), thus the event loop will eventually - // catch the invokation we throw here and send us back. - if (isOnForeground()) - QMetaObject::invokeMethod (this, "work", Qt::QueuedConnection, Q_ARG (int, i)); - else - work (i); - } -} - -void DocumentLoader::abort() -{ - m_hasAborted = true; -}
--- a/src/documentloader.h Thu Mar 15 18:51:58 2018 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,71 +0,0 @@ -/* - * 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/>. - */ - -#pragma once -#include "main.h" -#include "model.h" - -struct LDHeader; - -// -// DocumentLoader -// -// Loads the given file and parses it to LDObjects. It's a separate class so as to be able to do the work progressively -// through the event loop, allowing the program to maintain responsivity during loading. -// -class DocumentLoader : public QObject -{ - Q_OBJECT - -public: - DocumentLoader( - Model* model, - LDHeader& header, - bool onForeground = false, - QObject* parent = nullptr, - ); - - Q_SLOT void abort(); - bool hasAborted(); - bool isDone() const; - bool isOnForeground() const; - const QVector<LDObject*>& objects() const; - int progress() const; - void read (QIODevice* fp); - Q_SLOT void start(); - int warningCount() const; - -private: - class OpenProgressDialog* m_progressDialog; - Model* _model; - LDHeader& _header; - QStringList m_lines; - int m_progress; - int m_warningCount = 0; - bool m_isDone = false; - bool m_hasAborted = false; - const bool m_isOnForeground = false; - -private slots: - void work (int i); - -signals: - void progressUpdate (int progress); - void workDone(); - void parseErrorMessage(QString message); -};
--- a/src/documentmanager.cpp Thu Mar 15 18:51:58 2018 +0200 +++ b/src/documentmanager.cpp Fri Mar 16 11:50:35 2018 +0200 @@ -23,7 +23,7 @@ #include "lddocument.h" #include "mainwindow.h" #include "partdownloader.h" -#include "documentloader.h" +#include "parser.h" #include "glrenderer.h" const QStringList DocumentManager::specialSubdirectories {"s", "48", "8"}; @@ -103,18 +103,13 @@ file->clear(); } - bool aborted; - file = openDocument (path, false, false, file, &aborted); + file = openDocument (path, false, false, file); if (file == nullptr) { - if (not aborted) - { - // Tell the user loading failed. - setlocale (LC_ALL, "C"); - QMessageBox::critical(m_window, tr("Error"), format(tr("Failed to open %1: %2"), path, strerror (errno))); - } - + // Tell the user loading failed. + setlocale (LC_ALL, "C"); + QMessageBox::critical(m_window, tr("Error"), format(tr("Failed to open %1: %2"), path, strerror (errno))); m_loadingMainFile = false; return; } @@ -273,35 +268,12 @@ return nullptr; } -void DocumentManager::loadFileContents(QIODevice* input, Model& model, int* numWarnings, bool* ok) -{ - if (numWarnings) - *numWarnings = 0; - - DocumentLoader* loader = new DocumentLoader {&model, m_loadingMainFile}; - connect(loader, SIGNAL(parseErrorMessage(QString)), this, SLOT(printParseErrorMessage(QString))); - loader->read(input); - loader->start(); - - // After start() returns, if the loader isn't done yet, it's delaying - // its next iteration through the event loop. We need to catch this here - // by telling the event loop to tick, which will tick the file loader again. - // We keep doing this until the file loader is ready. - while (not loader->isDone()) - qApp->processEvents(); - - // If we wanted the success value, supply that now - if (ok) - *ok = not loader->hasAborted(); -} - void DocumentManager::printParseErrorMessage(QString message) { print(message); } -LDDocument* DocumentManager::openDocument (QString path, bool search, bool implicit, LDDocument* fileToOverride, - bool* aborted) +LDDocument* DocumentManager::openDocument (QString path, bool search, bool implicit, LDDocument* fileToOverride) { // Convert the file name to lowercase when searching because some parts contain subfile // subfile references with uppercase file names. I'll assume here that the library will always @@ -336,24 +308,22 @@ load->history()->setIgnoring (true); int numWarnings; - bool ok; - Model model {this}; - loadFileContents(fp, model, &numWarnings, &ok); - load->merge(model); + Parser parser {*fp}; + load->setHeader(parser.parseHeader()); + parser.parseBody(*load); fp->close(); fp->deleteLater(); - if (aborted) - *aborted = ok == false; - - if (not ok) - { - load->close(); - return nullptr; - } - if (m_loadingMainFile) { + int numWarnings = 0; + + for (LDObject* object : load->objects()) + { + if (object->type() == LDObjectType::Error) + numWarnings += 1; + } + m_window->changeDocument (load); print (tr ("File %1 parsed successfully (%2 errors)."), path, numWarnings); }
--- a/src/documentmanager.h Thu Mar 15 18:51:58 2018 +0200 +++ b/src/documentmanager.h Fri Mar 16 11:50:35 2018 +0200 @@ -41,10 +41,8 @@ QString findDocumentPath (QString relpath, bool subdirs); LDDocument* getDocumentByName (QString filename); bool isSafeToCloseAll(); - void loadFileContents(QIODevice* fp, Model& model, int* numWarnings, bool* ok); void loadLogoedStuds(); - LDDocument* openDocument (QString path, bool search, bool implicit, LDDocument* fileToOverride = nullptr, - bool* aborted = nullptr); + LDDocument* openDocument (QString path, bool search, bool implicit, LDDocument* fileToOverride = nullptr); QFile* openLDrawFile (QString relpath, bool subdirs, QString* pathpointer); void openMainModel (QString path); bool preInline (LDDocument* doc, Model& model, bool deep, bool renderinline);
--- a/src/lddocument.cpp Thu Mar 15 18:51:58 2018 +0200 +++ b/src/lddocument.cpp Fri Mar 16 11:50:35 2018 +0200 @@ -22,7 +22,6 @@ #include "miscallenous.h" #include "mainwindow.h" #include "canvas.h" -#include "documentloader.h" #include "dialogs/openprogressdialog.h" #include "documentmanager.h" #include "linetypes/comment.h" @@ -70,6 +69,11 @@ return m_name; } +void LDDocument::setHeader(LDHeader&& header) +{ + this->m_header = header; +} + void LDDocument::setName (QString value) { m_name = value; @@ -400,8 +404,9 @@ return polygonData(); } -// ============================================================================= -// ----------------------------------------------------------------------------- +/* + * Inlines this document into the given model + */ void LDDocument::inlineContents(Model& model, bool deep, bool renderinline) { if (m_manager->preInline(this, model, deep, renderinline)) @@ -409,16 +414,16 @@ for (LDObject* object : objects()) { - // Skip those without scemantic meaning + // Skip those without effect on the model meaning if (not object->isScemantic()) continue; - // Got another sub-file reference, inline it if we're deep-inlining. If not, - // just add it into the objects normally. Yay, recursion! + // Got another sub-file reference, recurse and inline it too if we're deep-inlining. + // If not, just add it into the objects normally. if (deep and object->type() == LDObjectType::SubfileReference) static_cast<LDSubfileReference*>(object)->inlineContents(documentManager(), model, deep, renderinline); else - model.addFromString(object->asText()); + model.insertCopy(model.size(), object); } } @@ -447,3 +452,19 @@ { m_verticesOutdated = true; } + +/* + * Special operator definition that implements the XOR operator for the winding. + * However, if either winding is NoWinding, then this function returns NoWinding. + */ +decltype(LDHeader::winding) operator^( + decltype(LDHeader::winding) one, + decltype(LDHeader::winding) other +) { + if (one == LDHeader::NoWinding or other == LDHeader::NoWinding) + return LDHeader::NoWinding; + else if (one != other) + return LDHeader::Clockwise; + else + return LDHeader::CounterClockwise; +}
--- a/src/lddocument.h Thu Mar 15 18:51:58 2018 +0200 +++ b/src/lddocument.h Fri Mar 16 11:50:35 2018 +0200 @@ -30,6 +30,13 @@ struct LDHeader { + struct HistoryEntry + { + QDate date; + QString author; + QString description; + enum { UserName, RealName } authorType = UserName; + }; enum FileType { Part, @@ -48,7 +55,7 @@ }; QFlags<Qualifier> qualfiers; QString description; - QString filename; + QString name; struct { QString realName; @@ -58,6 +65,7 @@ QString cmdline; QStringList help; QStringList keywords; + QVector<HistoryEntry> history; enum { NoWinding, @@ -71,6 +79,13 @@ } license = CaLicense; }; +Q_DECLARE_OPERATORS_FOR_FLAGS(QFlags<LDHeader::Qualifier>) + +decltype(LDHeader::winding) operator^( + decltype(LDHeader::winding) one, + decltype(LDHeader::winding) other +); + // // This class stores a document either as a editable file for the user or for // subfile caching. @@ -114,6 +129,7 @@ void setDefaultName (QString value); void setFrozen(bool value); void setFullPath (QString value); + void setHeader(LDHeader&& header); void setName (QString value); void setSavePosition (long value); void setTabIndex (int value); @@ -156,7 +172,5 @@ void handleImminentObjectRemoval(const QModelIndex& index); }; -Q_DECLARE_OPERATORS_FOR_FLAGS(QFlags<LDHeader::Qualifier>) - // Parses a string line containing an LDraw object and returns the object parsed. LDObject* ParseLine (QString line);
--- a/src/model.cpp Thu Mar 15 18:51:58 2018 +0200 +++ b/src/model.cpp Fri Mar 16 11:50:35 2018 +0200 @@ -20,12 +20,6 @@ #include "linetypes/modelobject.h" #include "documentmanager.h" #include "generics/migrate.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" #include "editHistory.h" Model::Model(DocumentManager* manager) : @@ -291,244 +285,6 @@ return _manager; } -// ============================================================================= -// -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& s, const int n) -{ - Vertex v; - v.apply ([&] (Axis ax, double& a) { a = s[n + ax].toDouble(); }); - return v; -} - -static qint32 StringToNumber (QString a, bool* ok = nullptr) -{ - int base = 10; - - if (a.startsWith ("0x")) - { - a.remove (0, 2); - base = 16; - } - - return a.toLong (ok, base); -} - -// ============================================================================= -// This is the LDraw code parser function. It takes in a string containing LDraw -// code and returns the object parsed from it. parseLine never returns null, -// the object will be LDError if it could not be parsed properly. -// ============================================================================= -LDObject* Model::insertFromString(int position, QString line) -{ - try - { - QStringList tokens = line.split(" ", QString::SkipEmptyParts); - - if (countof(tokens) <= 0) - { - // Line was empty, or only consisted of whitespace - return 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 emplaceAt<LDBfc>(position, statement); - } - - // MLCAD is notorious for stuffing these statements in parts it - // creates. The above block only handles valid statements, so we - // need to handle MLCAD-style invertnext, clip and noclip separately. - if (commentTextSimplified == "BFC CERTIFY INVERTNEXT") - return emplaceAt<LDBfc>(position, BfcStatement::InvertNext); - else if (commentTextSimplified == "BFC CERTIFY CLIP") - return emplaceAt<LDBfc>(position, BfcStatement::Clip); - else if (commentTextSimplified == "BFC CERTIFY NOCLIP") - return 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 = emplaceAt<LDBezierCurve>(position); - obj->setColor (StringToNumber (tokens[3])); - - for (int i = 0; i < 4; ++i) - obj->setVertex (i, ParseVertex (tokens, 4 + (i * 3))); - - return obj; - } - } - - // Just a regular comment: - return 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 = emplaceAt<LDSubfileReference>(position, tokens[14], transform, referncePosition); - obj->setColor (StringToNumber (tokens[1])); - return obj; - } - - case 2: - { - CheckTokenCount (tokens, 8); - CheckTokenNumbers (tokens, 1, 7); - - // Line - LDEdgeLine* obj = emplaceAt<LDEdgeLine>(position); - obj->setColor (StringToNumber (tokens[1])); - - 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 = emplaceAt<LDTriangle>(position); - obj->setColor (StringToNumber (tokens[1])); - - 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 = emplaceAt<LDQuadrilateral>(position); - else - obj = emplaceAt<LDConditionalEdge>(position); - - obj->setColor (StringToNumber (tokens[1])); - - 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 emplaceAt<LDError>(position, line, errorMessage); - } -} - -/* - * Given an LDraw object string, parses it and inserts it into the model. - */ -LDObject* Model::addFromString(QString line) -{ - return insertFromString(size(), line); -} - -/* - * Replaces the given object with a new one that is parsed from the given LDraw object string. - * If the parsing fails, the object is replaced with an error object. - */ -LDObject* Model::replaceWithFromString(LDObject* object, QString line) -{ - QModelIndex index = this->indexOf(object); - - if (index.isValid()) - { - removeAt(index.row()); - return insertFromString(index.row(), line); - } - else - return nullptr; -} - IndexGenerator Model::indices() const { return {this};
--- a/src/model.h Thu Mar 15 18:51:58 2018 +0200 +++ b/src/model.h Fri Mar 16 11:50:35 2018 +0200 @@ -103,9 +103,6 @@ QModelIndex indexOf(LDObject* object) const; bool isEmpty() const; class DocumentManager* documentManager() const; - LDObject* insertFromString(int position, QString line); - LDObject* addFromString(QString line); - LDObject* replaceWithFromString(LDObject* object, QString line); IndexGenerator indices() const; LDObject* lookup(const QModelIndex& index) const; QColor pickingColorForObject(const QModelIndex& objectIndex) const;
--- /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); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parser.h Fri Mar 16 11:50:35 2018 +0200 @@ -0,0 +1,52 @@ +/* + * 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/>. + */ + +#pragma once +#include "main.h" + +class LDHeader; +class Model; + +class Parser : public QObject +{ + Q_OBJECT + +public: + enum { EndOfModel = -1 }; + + Parser(QIODevice& device, QObject* parent = nullptr); + + LDHeader parseHeader(); + void parseBody(Model& model); + + static LDObject* parseFromString(Model& model, int position, QString line); + +/* +signals: + void parseErrorMessage(QString message); +*/ + +private: + enum HeaderParseResult {ParseSuccess, ParseFailure, StopParsing}; + + QString readLine(); + HeaderParseResult parseHeaderLine(LDHeader& header, const QString& line); + + QIODevice& device; + QStringList bag; +};
--- a/src/toolsets/algorithmtoolset.cpp Thu Mar 15 18:51:58 2018 +0200 +++ b/src/toolsets/algorithmtoolset.cpp Fri Mar 16 11:50:35 2018 +0200 @@ -35,6 +35,7 @@ #include "../linetypes/empty.h" #include "../linetypes/quadrilateral.h" #include "../linetypes/triangle.h" +#include "../parser.h" #include "ui_replacecoordinatesdialog.h" #include "ui_editrawdialog.h" #include "ui_flipdialog.h" @@ -85,10 +86,11 @@ void AlgorithmToolset::editRaw() { - if (countof(selectedObjects()) != 1) + if (countof(m_window->selectedIndexes()) != 1) return; - LDObject* object = *(selectedObjects().begin()); + QModelIndex index = *(m_window->selectedIndexes().begin()); + LDObject* object = currentDocument()->lookup(index); QDialog dialog; Ui::EditRawUI ui; ui.setupUi(&dialog); @@ -107,7 +109,9 @@ if (dialog.exec() == QDialog::Accepted) { // Reinterpret it from the text of the input field - currentDocument()->replaceWithFromString(object, ui.code->text()); + int row = index.row(); + currentDocument()->removeAt(row); + Parser::parseFromString(*currentDocument(), row, ui.code->text()); } } @@ -543,7 +547,7 @@ // Copy the body over to the new document for (LDObject* object : selectedObjects()) - subfile->addFromString(object->asText()); + Parser::parseFromString(*subfile, Parser::EndOfModel, object->asText()); // Try save it if (m_window->save(subfile, true))
--- a/src/toolsets/basictoolset.cpp Thu Mar 15 18:51:58 2018 +0200 +++ b/src/toolsets/basictoolset.cpp Fri Mar 16 11:50:35 2018 +0200 @@ -29,6 +29,7 @@ #include "../mainwindow.h" #include "../dialogs/colorselector.h" #include "../grid.h" +#include "../parser.h" #include "basictoolset.h" BasicToolset::BasicToolset (MainWindow *parent) : @@ -76,7 +77,7 @@ for (QString line : clipboardText.split("\n")) { - currentDocument()->insertFromString(row, line); + Parser::parseFromString(*currentDocument(), row, line); mainWindow()->select(currentDocument()->index(row)); row += 1; count += 1; @@ -186,7 +187,7 @@ for (QString line : QString (inputbox->toPlainText()).split ("\n")) { - currentDocument()->insertFromString(row, line); + Parser::parseFromString(*currentDocument(), row, line); mainWindow()->select(currentDocument()->index(row)); row += 1; }
--- a/src/toolsets/extprogramtoolset.cpp Thu Mar 15 18:51:58 2018 +0200 +++ b/src/toolsets/extprogramtoolset.cpp Fri Mar 16 11:50:35 2018 +0200 @@ -35,6 +35,7 @@ #include "../editHistory.h" #include "../documentmanager.h" #include "../grid.h" +#include "../parser.h" #include "../dialogs/externalprogrampathdialog.h" #include "extprogramtoolset.h" #include "ui_ytruderdialog.h" @@ -309,10 +310,9 @@ return; } - // TODO: I don't like how I need to go to the document manager to load objects from a file... - // We're not loading this as a document so it shouldn't be necessary. Model model {m_documents}; - m_documents->loadFileContents(&f, model, nullptr, nullptr); + Parser parser {f}; + parser.parseBody(model); // If we replace the objects, delete the selection now. if (replace)
--- a/src/toolsets/filetoolset.cpp Thu Mar 15 18:51:58 2018 +0200 +++ b/src/toolsets/filetoolset.cpp Fri Mar 16 11:50:35 2018 +0200 @@ -22,6 +22,7 @@ #include "../lddocument.h" #include "../mainwindow.h" #include "../partdownloader.h" +#include "../parser.h" #include "../primitives.h" #include "../dialogs/configdialog.h" #include "../dialogs/ldrawpathdialog.h" @@ -117,7 +118,8 @@ if (file.open(QIODevice::ReadOnly)) { Model model {m_documents}; - m_documents->loadFileContents(&file, model, nullptr, nullptr); + Parser parser {file}; + parser.parseBody(model); mainWindow()->clearSelection();