Header parsing complete, moved all parsing code into a new class. Documents are now all loaded in one go.

Fri, 16 Mar 2018 11:50:35 +0200

author
Teemu Piippo <teemu@hecknology.net>
date
Fri, 16 Mar 2018 11:50:35 +0200
changeset 1288
d1e45f90654b
parent 1287
f1da43b7f5c6
child 1289
ec5a38d19cf5

Header parsing complete, moved all parsing code into a new class. Documents are now all loaded in one go.

CMakeLists.txt file | annotate | diff | comparison | revisions
src/documentloader.cpp file | annotate | diff | comparison | revisions
src/documentloader.h file | annotate | diff | comparison | revisions
src/documentmanager.cpp file | annotate | diff | comparison | revisions
src/documentmanager.h file | annotate | diff | comparison | revisions
src/lddocument.cpp file | annotate | diff | comparison | revisions
src/lddocument.h file | annotate | diff | comparison | revisions
src/model.cpp file | annotate | diff | comparison | revisions
src/model.h file | annotate | diff | comparison | revisions
src/parser.cpp file | annotate | diff | comparison | revisions
src/parser.h file | annotate | diff | comparison | revisions
src/toolsets/algorithmtoolset.cpp file | annotate | diff | comparison | revisions
src/toolsets/basictoolset.cpp file | annotate | diff | comparison | revisions
src/toolsets/extprogramtoolset.cpp file | annotate | diff | comparison | revisions
src/toolsets/filetoolset.cpp file | annotate | diff | comparison | revisions
--- 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();
 

mercurial