src/parser.cpp

Fri, 01 Jul 2022 16:46:43 +0300

author
Teemu Piippo <teemu.s.piippo@gmail.com>
date
Fri, 01 Jul 2022 16:46:43 +0300
changeset 312
2637134bc37c
parent 264
76a025db4948
child 327
2aa15daa0216
permissions
-rw-r--r--

Fix right click to delete not really working properly
Instead of removing the point that had been added, it would remove
the point that is being drawn, which would cause it to overwrite the
previous point using the new point, causing a bit of a delay

/*
 *  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(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;
		}
		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)
{
	line = line.trimmed();
	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