src/parser.cpp

Mon, 04 Jul 2022 15:37:22 +0300

author
Teemu Piippo <teemu.s.piippo@gmail.com>
date
Mon, 04 Jul 2022 15:37:22 +0300
changeset 332
ae7f7fbb9cda
parent 328
3ea38fd469ca
child 333
07e65a4c6611
permissions
-rw-r--r--

Only enable the text editor when having a model open

/*
 *  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 <QRegExp>
#include <QIODevice>
#include "src/ldrawalgorithm.h"
#include "src/model.h"
#include "src/parser.h"

struct BodyParseError
{
	QString message;
};

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

/*
 * Reads a single line from the device.
 */
QString Parser::readLine()
{
	return this->stream.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->stream.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().trimmed();
		if (line == "0 BFC INVERTNEXT" or line == "0 BFC CERTIFY INVERTNEXT")
		{
			invertNext = true;
			continue;
		}
		ModelElement element = parseLDrawLine(line);
		if (invertNext)
		{
			element = inverted(element);
		}
		model.append(element);
		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 Comment parseType0Line(const QString& line)
{
	return {line.mid(1).trimmed()};
}

static ModelElement parseType1Line(const QStringList& tokens)
{
	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];
	static QRegExp re{R"((?:(\d+)\\)?(\d+)-(\d)+([a-z]+)\.dat)"};
	if (re.exactMatch(name)) {
		const auto p = std::find(std::begin(circularPrimitiveStems), std::end(circularPrimitiveStems), re.cap(4));
		const unsigned int divisions = (re.cap(1).isEmpty()) ? 16 : re.cap(1).toUInt();
		const unsigned int segments = re.cap(2).toUInt() * divisions / re.cap(3).toUInt();
		if (p != std::end(circularPrimitiveStems)) {
			const auto type = static_cast<CircularPrimitive::Type>(p - std::begin(circularPrimitiveStems));
			return Colored<CircularPrimitive>{
				CircularPrimitive{
					.type = type,
					.fraction = {segments, divisions},
					.transformation = transform,
				},
				color,
			};
		}
	}
	return Colored<SubfileReference>{
		{
			.name = name,
			.transformation = transform,
		},
		color,
	};
}

template<int NumVertices>
static auto parsePolygon(const QStringList& tokens)
{
	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_pair(vertices, color);
}

ModelElement parseLDrawLine(QString line)
{
	try
	{
		const QStringList tokens = line.simplified().split(" ");
		if (tokens.empty() or tokens == QStringList{{""}})
		{
			return Empty{};
		}
		bool ok_code;
		const int code = tokens[0].toInt(&ok_code);
		if (not ok_code)
		{
			throw BodyParseError{QObject::tr("line type was not an integer")};
		}
		switch (code)
		{
		case 0:
			return parseType0Line(line);
		case 1:
			return parseType1Line(tokens);
		case 2:
		{
			const auto pair = parsePolygon<2>(tokens);
			return Colored<LineSegment>{{pair.first[0], pair.first[1]}, pair.second};
		}
		case 3:
		{
			const auto pair = parsePolygon<3>(tokens);
			return Colored<Triangle>{{pair.first[0], pair.first[1], pair.first[2]}, pair.second
			};
		}
		case 4:
		{
			const auto pair = parsePolygon<4>(tokens);
			const Quadrilateral quad{pair.first[0], pair.first[1], pair.first[2], pair.first[3]};
			return Colored<Quadrilateral>{quad, pair.second};
		}
		case 5:
		{
			const auto pair = parsePolygon<4>(tokens);
			const ConditionalEdge cedge{pair.first[0], pair.first[1], pair.first[2], pair.first[3]};
			return Colored<ConditionalEdge>{cedge, pair.second};
		}
		default:
			throw BodyParseError{QObject::tr("bad line type '%1'").arg(code)};
		}
	}
	catch(const BodyParseError& error)
	{
		return ParseError{line};
	}
}

mercurial