src/parser.cpp

Wed, 09 Mar 2022 13:14:40 +0200

author
Teemu Piippo <teemu@hecknology.net>
date
Wed, 09 Mar 2022 13:14:40 +0200
changeset 174
3016b494685c
parent 152
03f8e6d42e13
child 176
cd9d6bf6f649
permissions
-rw-r--r--

show unnamed tabs as "<unnamed>" in the tab list

/*
 *  LDForge: LDraw parts authoring CAD
 *  Copyright (C) 2013 - 2020 Teemu Piippo
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "model.h"
#include "parser.h"
#include "linetypes/comment.h"
#include "linetypes/conditionaledge.h"
#include "linetypes/edge.h"
#include "linetypes/errorline.h"
#include "linetypes/metacommand.h"
#include "linetypes/object.h"
#include "linetypes/quadrilateral.h"
#include "linetypes/subfilereference.h"
#include "linetypes/triangle.h"

struct BodyParseError
{
	QString message;
};

/*
 * Constructs an LDraw parser
 */
Parser::Parser(QIODevice& device, QObject* parent) :
	QObject {parent},
	device {device} {}

/*
 * Reads a single line from the device.
 */
QString Parser::readLine()
{
	return QString::fromUtf8(this->device.readLine()).trimmed();
}

/*
 * Parses a single line of the header.
 * Possible parse results:
 *   · ParseSuccess: the header line was parsed successfully.
 *   · ParseFailure: the header line was parsed incorrectly and needs to be handled otherwise.
 *   · StopParsing: the line does not belong in the header and header parsing needs to stop.
 */
Parser::HeaderParseResult Parser::parseHeaderLine(
	LDHeader& header,
	Winding& winding,
	const QString& line
) {
	if (line.isEmpty())
	{
		return ParseSuccess;
	}
	else if (not line.startsWith("0") or line.startsWith("0 //"))
	{
		return StopParsing;
	}
	else if (line.startsWith("0 !LDRAW_ORG "))
	{
		QStringList tokens = line
			.mid(strlen("0 !LDRAW_ORG "))
			.split(" ", Qt::SkipEmptyParts);
		if (not tokens.isEmpty())
		{
			QString partTypeString = tokens[0];
			// Anything that enters LDForge becomes unofficial in any case if saved.
			// Therefore we don't need to give the Unofficial type any special
			// consideration.
			if (partTypeString.startsWith("Unofficial_"))
				partTypeString = partTypeString.mid(strlen("Unofficial_"));
			header.type = headerTypeFromString(partTypeString);
			header.qualfiers = {};
			if (tokens.contains("Alias"))
				header.qualfiers |= LDHeader::Alias;
			if (tokens.contains("Physical_Color"))
				header.qualfiers |= LDHeader::PhysicalColour;
			if (tokens.contains("Flexible_Section"))
				header.qualfiers |= LDHeader::FlexibleSection;
			return ParseSuccess;
		}
		else
		{
			return ParseFailure;
		}
	}
	else if (line == "0 BFC CERTIFY CCW")
	{
		winding = Anticlockwise;
		return ParseSuccess;
	}
	else if (line == "0 BFC CERTIFY CW")
	{
		winding = Clockwise;
		return ParseSuccess;
	}
	else if (line == "0 BFC NOCERTIFY")
	{
		winding = NoWinding;
		return ParseSuccess;
	}
	else if (line.startsWith("0 !HISTORY "))
	{
		static const QRegExp historyRegexp {
			R"(0 !HISTORY\s+(\d{4}-\d{2}-\d{2})\s+)"
			R"((\{[^}]+|\[[^]]+)[\]}]\s+(.+))"
		};
		if (historyRegexp.exactMatch(line))
		{
			QString dateString = historyRegexp.capturedTexts().value(1);
			QString authorWithPrefix = historyRegexp.capturedTexts().value(2);
			QString description = historyRegexp.capturedTexts().value(3);
			LDHeader::HistoryEntry historyEntry;
			historyEntry.date = QDate::fromString(dateString, Qt::ISODate);
			historyEntry.description = description;

			if (authorWithPrefix[0] == '{')
				historyEntry.author = authorWithPrefix + "}";
			else
				historyEntry.author = authorWithPrefix.mid(1);

			header.history.append(historyEntry);
			return ParseSuccess;
		}
		else
		{
			return ParseFailure;
		}
	}
	else if (line.startsWith("0 Author: "))
	{
		header.author = line.mid(strlen("0 Author: "));
		return ParseSuccess;
	}
	else if (line.startsWith("0 Name: "))
	{
		header.name = line.mid(strlen("0 Name: "));
		return ParseSuccess;
	}
	else if (line.startsWith("0 !HELP "))
	{
		if (not header.help.isEmpty())
			header.help += "\n";
		header.help += line.mid(strlen("0 !HELP "));
		return ParseSuccess;
	}
	else if (line.startsWith("0 !KEYWORDS "))
	{
		if (not header.keywords.isEmpty())
			header.keywords += "\n";
		header.keywords += line.mid(strlen("0 !KEYWORDS "));
		return ParseSuccess;
	}
	else if (line.startsWith("0 !CATEGORY "))
	{
		header.category = line.mid(strlen("0 !CATEGORY "));
		return ParseSuccess;
	}
	else if (line.startsWith("0 !CMDLINE "))
	{
		header.cmdline = line.mid(strlen("0 !CMDLINE "));
		return ParseSuccess;
	}
	else if (line.startsWith(LDHeader::caLicenseString))
	{
		header.license = LDHeader::CaLicense;
		return ParseSuccess;
	}
	else if (line.startsWith(LDHeader::nonCaLicenseString))
	{
		header.license = LDHeader::NonCaLicense;
		return ParseSuccess;
	}
	else
	{
		return ParseFailure;
	}
}

/*
 * Parses the header from the device given at construction and returns
 * the resulting header structure.
 */
LDHeader Parser::parseHeader(Winding& winding)
{
	LDHeader header = {};
	if (not this->device.atEnd())
	{
		// Parse the description
		QString descriptionLine = this->readLine();
		if (descriptionLine.startsWith("0 "))
		{
			header.description = descriptionLine.mid(strlen("0 ")).trimmed();
			// Parse the rest of the header
			while (not this->device.atEnd())
			{
				const QString& line = this->readLine();
				auto result = parseHeaderLine(header, winding, line);
				if (result == ParseFailure)
				{
					// Failed to parse this header line, add it as a comment into the body later.
					this->bag.append(line);
				}
				else if (result == StopParsing)
				{
					// Header parsing stops, add this line to the body.
					this->bag.append(line);
					break;
				}
			}
		}
		else
		{
			this->bag.append(descriptionLine);
		}
	}
	return header;
}

/**
 * @brief Parses the model body into the given model.
 * @param editor Handle to model edit context
 */
void Parser::parseBody(Model& model)
{
	bool invertNext = false;
	while (not this->device.atEnd())
		this->bag.append(this->readLine());
	for (const QString& line : this->bag)
	{
		// Some LDraw parts such as 53588.dat can contain "BFC  INVERTNEXT" with multiple inner whitespaces.
		// So we need to pass the string through QString::simplified to catch these cases.
		const QString simplifiedLine = line.simplified();
		if (simplifiedLine == "0 BFC INVERTNEXT" or simplifiedLine == "0 BFC CERTIFY INVERTNEXT")
		{
			invertNext = true;
			continue;
		}
		std::unique_ptr<ldraw::Object> object = parseFromString(line);
		model.append(std::move(object));
		if (invertNext)
		{
			model[model.size() - 1]->invert();
		}
		invertNext = false;
	}
	/*
	// Test quadrilateral splitting by splitting all the quadrilaterals
	QVector<ldraw::quadrilateralid_t> quadrilateral_ids;
	for (int i = 0; i < editor.model().size(); i += 1)
	{
		const ldraw::id_t id = editor.model().resolve(editor.model().index(i));
		const ldraw::quadrilateralid_t quad_id = editor.model().checkType<ldraw::Quadrilateral>(id);
		if (not(quad_id == ldraw::NULL_ID))
		{
			quadrilateral_ids.push_back(quad_id);
		}
	}
	for (const ldraw::quadrilateralid_t id : quadrilateral_ids)
	{
		ldraw::splitQuadrilateral(editor, id);
	}
	*/
}

static ldraw::Color colorFromString(const QString& colorString)
{
	bool colorSucceeded;
	const ldraw::Color color = {colorString.toInt(&colorSucceeded)};
	if (colorSucceeded)
	{
		return color;
	}
	else
	{
		throw BodyParseError{"colour was not an integer value"};
	}
}

static glm::vec3 vertexFromStrings(
	const QStringList& tokens,
	const int startingPosition)
{
	bool ok_x;
	const float x = tokens[startingPosition].toFloat(&ok_x);
	bool ok_y;
	const float y = tokens[startingPosition + 1].toFloat(&ok_y);
	bool ok_z;
	const float z = tokens[startingPosition + 2].toFloat(&ok_z);
	if (not ok_x or not ok_y or not ok_z)
	{
		throw BodyParseError{"vertex contained illegal co-ordinates"};
	}
	return {x, y, z};
}

static glm::mat4 matrixFromStrings(
	const QStringList& tokens,
	const int startingPosition,
	const int positionStartingIndex)
{
	glm::mat4 result = glm::mat4{1};
	for (int i = 0; i < 9; i += 1)
	{
		const int row = i / 3;
		const int column = i % 3;
		const int index = i + startingPosition;
		if (index >= tokens.size())
		{
			throw BodyParseError{"too few tokens available"};
		}
		bool ok;
		// note that glm::mat4 is column-major
		result[column][row] = tokens[index].toFloat(&ok);
		if (not ok)
		{
			throw BodyParseError{"non-numeric values for matrix"};
		}
	}
	for (int i = 0; i < 3; i += 1)
	{
		bool ok;
		const auto value = tokens[i + positionStartingIndex].toFloat(&ok);
		result[3][i] = value;
		if (not ok)
		{
			throw BodyParseError{"non-numeric values for matrix"};
		}
	}
	return result;
}

static std::unique_ptr<ldraw::Object> parseType0Line(
	const QString& line,
	const QStringList& tokens)
{
	Q_UNUSED(tokens)
	if (line.startsWith("0 //"))
	{
		// lol wut
		return std::make_unique<ldraw::Comment>(line.mid(std::strlen("0 //")).trimmed());
	}
	else
	{
		return std::make_unique<ldraw::MetaCommand>(line.mid(1).trimmed());
	}
}

static std::unique_ptr<ldraw::SubfileReference> parseType1Line(
	const QString& line,
	const QStringList& tokens)
{
	Q_UNUSED(line)
	constexpr int colorPosition = 1;
	constexpr int positionPosition = 2; // 2..4
	constexpr int transformPosition = 5; // 5..13
	constexpr int namePosition = 14;
	if (tokens.size() != 15)
	{
		throw BodyParseError{"wrong amount of tokens in a type-1 line"};
	}
	const ldraw::Color color = colorFromString(tokens[colorPosition]);
	const glm::mat4 transform = matrixFromStrings(tokens, transformPosition, positionPosition);
	const QString& name = tokens[namePosition];
	return std::make_unique<ldraw::SubfileReference>(transform, name, color);
}

template<typename T, int NumVertices>
static std::unique_ptr<T> parsePolygon(
	const QString& line,
	const QStringList& tokens)
{
	Q_UNUSED(line)
	constexpr int colorPosition = 1;
	auto vertexPosition = [](int n) { return 2 + 3*n; };
	if (tokens.size() != 2 + 3 * NumVertices)
	{
		throw BodyParseError{"wrong amount of tokens"};
	}
	const ldraw::Color color = colorFromString(tokens[colorPosition]);
	std::array<glm::vec3, NumVertices> vertices;
	for (int i = 0; i < NumVertices; i += 1)
	{
		vertices[unsigned_cast(i)] = vertexFromStrings(tokens, vertexPosition(i));
	}
	return std::make_unique<T>(vertices, color);
}

std::unique_ptr<ldraw::Object> Parser::parseFromString(QString line)
{
	line = line.trimmed();
	try
	{
		const QStringList tokens = line.split(QRegExp{R"(\s+)"});
		if (tokens.empty() or tokens == QStringList{{""}})
		{
			return std::make_unique<ldraw::Empty>();
		}
		bool ok_code;
		const int code = tokens[0].toInt(&ok_code);
		if (not ok_code)
		{
			throw BodyParseError{"line type was not an integer"};
		}
		switch (code)
		{
		case 0:
			return parseType0Line(line, tokens);
		case 1:
			return parseType1Line(line, tokens);
		case 2:
			return parsePolygon<ldraw::Edge, 2>(line, tokens);
		case 3:
			return parsePolygon<ldraw::Triangle, 3>(line, tokens);
		case 4:
			return parsePolygon<ldraw::Quadrilateral, 4>(line, tokens);
		case 5:
			return parsePolygon<ldraw::ConditionalEdge, 4>(line, tokens);
		default:
			throw BodyParseError{utility::format("bad line type '%1'", code)};
		}
	}
	catch(const BodyParseError& error)
	{
		return std::make_unique<ldraw::ErrorLine>(line, error.message);
	}
}

mercurial