diff -r ae7f7fbb9cda -r 07e65a4c6611 src/parser.cpp --- a/src/parser.cpp Mon Jul 04 15:37:22 2022 +0300 +++ b/src/parser.cpp Mon Jul 04 19:53:13 2022 +0300 @@ -16,238 +16,175 @@ * along with this program. If not, see . */ -#include +#include #include #include "src/ldrawalgorithm.h" #include "src/model.h" #include "src/parser.h" -struct BodyParseError -{ - QString message; -}; +#define NUMBER_REGEX R"([+-]?(?:(?:\d+\.?\d*)|(?:\.\d+)))" +#define SPACE_REGEX R"(\s+)" +#define VEC3_REGEX "(" NUMBER_REGEX SPACE_REGEX NUMBER_REGEX SPACE_REGEX NUMBER_REGEX ")" +#define TWO_VECTORS VEC3_REGEX SPACE_REGEX VEC3_REGEX +#define THREE_VECTORS TWO_VECTORS SPACE_REGEX VEC3_REGEX +#define FOUR_VECTORS THREE_VECTORS SPACE_REGEX VEC3_REGEX -/* - * Constructs an LDraw parser - */ -Parser::Parser(QTextStream& stream, QObject* parent) : - QObject {parent}, - stream {stream} {} - -/* - * Reads a single line from the device. - */ -QString Parser::readLine() +static const auto& exprs() { - return this->stream.readLine().trimmed(); + static const struct + { + QRegularExpression subfileRe{QStringLiteral( + R"(^\s*(1)\s+(\d+)\s+)" FOUR_VECTORS SPACE_REGEX R"(([^ ]+)\s*$)" + )}; + QRegularExpression edgeRe{QStringLiteral( + R"(^\s*(2)\s+(\d+)\s+)" TWO_VECTORS R"(\s*$)" + )}; + QRegularExpression triangleRe{QStringLiteral( + R"(^\s*(3)\s+(\d+)\s+)" THREE_VECTORS R"(\s*$)" + )}; + QRegularExpression quadRe{QStringLiteral( + R"(^\s*(4)\s+(\d+)\s+)" FOUR_VECTORS R"(\s*$)" + )}; + QRegularExpression cedgeRe{QStringLiteral( + R"(^\s*(5)\s+(\d+)\s+)" FOUR_VECTORS R"(\s*$)" + )}; +#if 0 + QRegularExpression bfcRe{QStringLiteral( + R"(^\s*(0) (BFC (?:CERTIFY CCW|CERTIFY CW|NOCERTIFY|INVERTNEXT|CLIP|NOCLIP))\s*$)" + )}; +#endif + QRegularExpression commentRe{QStringLiteral( + R"(^\s*(0)\s+(.+)$)" + )}; + } result; + return result; +} +template +QString attrib(const LineType& parsed) +{ + const int index = attribIndex, Attrib>; + const TextRange& range = parsed.positions[index]; + return parsed.content.mid(range.start, range.length); } -/** - * @brief Parses the model body into the given model. - * @param editor Handle to model edit context - */ -void Parser::parseBody(Model& model) +template +glm::vec3 vectorAttrib(const LineType& parsed) { - 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; - } + return glm::vec3{ + attrib(parsed).toFloat(), + attrib(parsed).toFloat(), + attrib(parsed).toFloat(), + }; +} + +template +ColorIndex colorAttrib(T* parsed) +{ + return ColorIndex{attrib(*parsed).toInt()}; } -static ldraw::Color colorFromString(const QString& colorString) +opt parse(const QString& line) { - 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"}; + const auto tryRe = [&line](const QRegularExpression& re){ + opt result = re.match(line); + if (not result->hasMatch()) { + result.reset(); + } + return result; + }; + opt result; + //! \brief Put value into result, add matched ranges to it and return a pointer + const auto init = [&](T&& value, const QRegularExpressionMatch& match){ + result = value; + T* parsed = &std::get(*result); + for (int i = 0; i < countof(T::attributes); ++i) { + parsed->positions[i] = { + .start = match.capturedStart(i + 1), + .length = match.capturedLength(i + 1), + }; + } + return parsed; + }; + if (auto line1Match = tryRe(exprs().subfileRe)) { + LineType1* const parsed = init(LineType1{}, *line1Match); + parsed->value = { + SubfileReference{ + .name = attrib(*parsed), + .transformation = glm::mat4{ + glm::vec4{vectorAttrib(*parsed), 0}, + glm::vec4{vectorAttrib(*parsed), 0}, + glm::vec4{vectorAttrib(*parsed), 0}, + glm::vec4{vectorAttrib(*parsed), 0} + }, + }, + colorAttrib(parsed), + }; } - 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"}; - } + else if (auto line2Match = tryRe(exprs().edgeRe)) { + LineType2* const parsed = init(LineType2{}, *line2Match); + parsed->value = { + LineSegment{ + .p1 = vectorAttrib(*parsed), + .p2 = vectorAttrib(*parsed), + }, + colorAttrib(parsed), + }; + } + else if (auto line3Match = tryRe(exprs().triangleRe)) { + LineType3* const parsed = init(LineType3{}, *line3Match); + parsed->value = { + Triangle{ + .p1 = vectorAttrib(*parsed), + .p2 = vectorAttrib(*parsed), + .p3 = vectorAttrib(*parsed), + }, + colorAttrib(parsed), + }; } - 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"}; - } + else if (auto line4Match = tryRe(exprs().quadRe)) { + LineType4* const parsed = init(LineType4{}, *line4Match); + parsed->value = { + Quadrilateral{ + .p1 = vectorAttrib(*parsed), + .p2 = vectorAttrib(*parsed), + .p3 = vectorAttrib(*parsed), + .p4 = vectorAttrib(*parsed), + }, + colorAttrib(parsed), + }; + } + else if (auto line5Match = tryRe(exprs().cedgeRe)) { + LineType5* const parsed = init(LineType5{}, *line5Match); + parsed->value = { + ConditionalEdge{ + .p1 = vectorAttrib(*parsed), + .p2 = vectorAttrib(*parsed), + .c1 = vectorAttrib(*parsed), + .c2 = vectorAttrib(*parsed), + }, + colorAttrib(parsed), + }; } 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(p - std::begin(circularPrimitiveStems)); - return Colored{ - CircularPrimitive{ - .type = type, - .fraction = {segments, divisions}, - .transformation = transform, - }, - color, - }; - } - } - return Colored{ - { - .name = name, - .transformation = transform, - }, - color, - }; -} - -template -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 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{{pair.first[0], pair.first[1]}, pair.second}; - } - case 3: - { - const auto pair = parsePolygon<3>(tokens); - return Colored{{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{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{cedge, pair.second}; - } - default: - throw BodyParseError{QObject::tr("bad line type '%1'").arg(code)}; - } - } - catch(const BodyParseError& error) - { - return ParseError{line}; +#if 0 +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(p - std::begin(circularPrimitiveStems)); + return Colored{ + CircularPrimitive{ + .type = type, + .fraction = {segments, divisions}, + .transformation = transform, + }, + color, + }; } } +#endif +