src/parser.cpp

Wed, 09 Mar 2022 14:22:22 +0200

author
Teemu Piippo <teemu@hecknology.net>
date
Wed, 09 Mar 2022 14:22:22 +0200
changeset 177
f69d53c053df
parent 176
cd9d6bf6f649
child 183
97b591813c8b
permissions
-rw-r--r--

Show type of object in the object editor

/*
 *  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/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();
}

/**
 * @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())
	{
		// 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 line = this->readLine().simplified();
		if (line == "0 BFC INVERTNEXT" or line == "0 BFC CERTIFY INVERTNEXT")
		{
			invertNext = true;
			continue;
		}
		model.append(parseFromString(line));
		if (invertNext)
		{
			model[model.size() - 1]->invert();
		}
		invertNext = false;
	}
}

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)
	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