Tue, 28 Jun 2022 12:29:38 +0300
Make settings editor scrollable
/* * 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}; } }