14 * |
14 * |
15 * You should have received a copy of the GNU General Public License |
15 * You should have received a copy of the GNU General Public License |
16 * along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 * along with this program. If not, see <http://www.gnu.org/licenses/>. |
17 */ |
17 */ |
18 |
18 |
19 #include <QRegExp> |
19 #include <QRegularExpression> |
20 #include <QIODevice> |
20 #include <QIODevice> |
21 #include "src/ldrawalgorithm.h" |
21 #include "src/ldrawalgorithm.h" |
22 #include "src/model.h" |
22 #include "src/model.h" |
23 #include "src/parser.h" |
23 #include "src/parser.h" |
24 |
24 |
25 struct BodyParseError |
25 #define NUMBER_REGEX R"([+-]?(?:(?:\d+\.?\d*)|(?:\.\d+)))" |
|
26 #define SPACE_REGEX R"(\s+)" |
|
27 #define VEC3_REGEX "(" NUMBER_REGEX SPACE_REGEX NUMBER_REGEX SPACE_REGEX NUMBER_REGEX ")" |
|
28 #define TWO_VECTORS VEC3_REGEX SPACE_REGEX VEC3_REGEX |
|
29 #define THREE_VECTORS TWO_VECTORS SPACE_REGEX VEC3_REGEX |
|
30 #define FOUR_VECTORS THREE_VECTORS SPACE_REGEX VEC3_REGEX |
|
31 |
|
32 static const auto& exprs() |
26 { |
33 { |
27 QString message; |
34 static const struct |
28 }; |
35 { |
29 |
36 QRegularExpression subfileRe{QStringLiteral( |
30 /* |
37 R"(^\s*(1)\s+(\d+)\s+)" FOUR_VECTORS SPACE_REGEX R"(([^ ]+)\s*$)" |
31 * Constructs an LDraw parser |
38 )}; |
32 */ |
39 QRegularExpression edgeRe{QStringLiteral( |
33 Parser::Parser(QTextStream& stream, QObject* parent) : |
40 R"(^\s*(2)\s+(\d+)\s+)" TWO_VECTORS R"(\s*$)" |
34 QObject {parent}, |
41 )}; |
35 stream {stream} {} |
42 QRegularExpression triangleRe{QStringLiteral( |
36 |
43 R"(^\s*(3)\s+(\d+)\s+)" THREE_VECTORS R"(\s*$)" |
37 /* |
44 )}; |
38 * Reads a single line from the device. |
45 QRegularExpression quadRe{QStringLiteral( |
39 */ |
46 R"(^\s*(4)\s+(\d+)\s+)" FOUR_VECTORS R"(\s*$)" |
40 QString Parser::readLine() |
47 )}; |
|
48 QRegularExpression cedgeRe{QStringLiteral( |
|
49 R"(^\s*(5)\s+(\d+)\s+)" FOUR_VECTORS R"(\s*$)" |
|
50 )}; |
|
51 #if 0 |
|
52 QRegularExpression bfcRe{QStringLiteral( |
|
53 R"(^\s*(0) (BFC (?:CERTIFY CCW|CERTIFY CW|NOCERTIFY|INVERTNEXT|CLIP|NOCLIP))\s*$)" |
|
54 )}; |
|
55 #endif |
|
56 QRegularExpression commentRe{QStringLiteral( |
|
57 R"(^\s*(0)\s+(.+)$)" |
|
58 )}; |
|
59 } result; |
|
60 return result; |
|
61 } |
|
62 template<Attribute Attrib, typename T, Attribute... Attribs> |
|
63 QString attrib(const LineType<T, Attribs...>& parsed) |
41 { |
64 { |
42 return this->stream.readLine().trimmed(); |
65 const int index = attribIndex<LineType<T, Attribs...>, Attrib>; |
|
66 const TextRange& range = parsed.positions[index]; |
|
67 return parsed.content.mid(range.start, range.length); |
43 } |
68 } |
44 |
69 |
45 /** |
70 template<Attribute X, Attribute Y, Attribute Z, typename T, Attribute... Attribs> |
46 * @brief Parses the model body into the given model. |
71 glm::vec3 vectorAttrib(const LineType<T, Attribs...>& parsed) |
47 * @param editor Handle to model edit context |
|
48 */ |
|
49 void Parser::parseBody(Model& model) |
|
50 { |
72 { |
51 bool invertNext = false; |
73 return glm::vec3{ |
52 while (not this->stream.atEnd()) |
74 attrib<X>(parsed).toFloat(), |
53 { |
75 attrib<Y>(parsed).toFloat(), |
54 // Some LDraw parts such as 53588.dat can contain "BFC INVERTNEXT" with multiple inner whitespaces. |
76 attrib<Z>(parsed).toFloat(), |
55 // So we need to pass the string through QString::simplified to catch these cases. |
77 }; |
56 const QString line = this->readLine().trimmed(); |
|
57 if (line == "0 BFC INVERTNEXT" or line == "0 BFC CERTIFY INVERTNEXT") |
|
58 { |
|
59 invertNext = true; |
|
60 continue; |
|
61 } |
|
62 ModelElement element = parseLDrawLine(line); |
|
63 if (invertNext) |
|
64 { |
|
65 element = inverted(element); |
|
66 } |
|
67 model.append(element); |
|
68 invertNext = false; |
|
69 } |
|
70 } |
78 } |
71 |
79 |
72 static ldraw::Color colorFromString(const QString& colorString) |
80 template<typename T> |
|
81 ColorIndex colorAttrib(T* parsed) |
73 { |
82 { |
74 bool colorSucceeded; |
83 return ColorIndex{attrib<Attribute::Color>(*parsed).toInt()}; |
75 const ldraw::Color color = {colorString.toInt(&colorSucceeded)}; |
|
76 if (colorSucceeded) |
|
77 { |
|
78 return color; |
|
79 } |
|
80 else |
|
81 { |
|
82 throw BodyParseError{"colour was not an integer value"}; |
|
83 } |
|
84 } |
84 } |
85 |
85 |
86 static glm::vec3 vertexFromStrings( |
86 opt<ParsedLine> parse(const QString& line) |
87 const QStringList& tokens, |
|
88 const int startingPosition) |
|
89 { |
87 { |
90 bool ok_x; |
88 const auto tryRe = [&line](const QRegularExpression& re){ |
91 const float x = tokens[startingPosition].toFloat(&ok_x); |
89 opt<QRegularExpressionMatch> result = re.match(line); |
92 bool ok_y; |
90 if (not result->hasMatch()) { |
93 const float y = tokens[startingPosition + 1].toFloat(&ok_y); |
91 result.reset(); |
94 bool ok_z; |
92 } |
95 const float z = tokens[startingPosition + 2].toFloat(&ok_z); |
93 return result; |
96 if (not ok_x or not ok_y or not ok_z) |
94 }; |
97 { |
95 opt<ParsedLine> result; |
98 throw BodyParseError{"vertex contained illegal co-ordinates"}; |
96 //! \brief Put value into result, add matched ranges to it and return a pointer |
|
97 const auto init = [&]<typename T>(T&& value, const QRegularExpressionMatch& match){ |
|
98 result = value; |
|
99 T* parsed = &std::get<T>(*result); |
|
100 for (int i = 0; i < countof(T::attributes); ++i) { |
|
101 parsed->positions[i] = { |
|
102 .start = match.capturedStart(i + 1), |
|
103 .length = match.capturedLength(i + 1), |
|
104 }; |
|
105 } |
|
106 return parsed; |
|
107 }; |
|
108 if (auto line1Match = tryRe(exprs().subfileRe)) { |
|
109 LineType1* const parsed = init(LineType1{}, *line1Match); |
|
110 parsed->value = { |
|
111 SubfileReference{ |
|
112 .name = attrib<Attribute::Name>(*parsed), |
|
113 .transformation = glm::mat4{ |
|
114 glm::vec4{vectorAttrib<Attribute::X2, Attribute::Y2, Attribute::Z2>(*parsed), 0}, |
|
115 glm::vec4{vectorAttrib<Attribute::X3, Attribute::Y3, Attribute::Z3>(*parsed), 0}, |
|
116 glm::vec4{vectorAttrib<Attribute::X4, Attribute::Y4, Attribute::Z4>(*parsed), 0}, |
|
117 glm::vec4{vectorAttrib<Attribute::X1, Attribute::Y1, Attribute::Z1>(*parsed), 0} |
|
118 }, |
|
119 }, |
|
120 colorAttrib(parsed), |
|
121 }; |
99 } |
122 } |
100 return {x, y, z}; |
123 else if (auto line2Match = tryRe(exprs().edgeRe)) { |
101 } |
124 LineType2* const parsed = init(LineType2{}, *line2Match); |
102 |
125 parsed->value = { |
103 static glm::mat4 matrixFromStrings( |
126 LineSegment{ |
104 const QStringList& tokens, |
127 .p1 = vectorAttrib<Attribute::X1, Attribute::Y1, Attribute::Z1>(*parsed), |
105 const int startingPosition, |
128 .p2 = vectorAttrib<Attribute::X2, Attribute::Y2, Attribute::Z2>(*parsed), |
106 const int positionStartingIndex) |
129 }, |
107 { |
130 colorAttrib(parsed), |
108 glm::mat4 result = glm::mat4{1}; |
131 }; |
109 for (int i = 0; i < 9; i += 1) |
|
110 { |
|
111 const int row = i / 3; |
|
112 const int column = i % 3; |
|
113 const int index = i + startingPosition; |
|
114 if (index >= tokens.size()) |
|
115 { |
|
116 throw BodyParseError{"too few tokens available"}; |
|
117 } |
|
118 bool ok; |
|
119 // note that glm::mat4 is column-major |
|
120 result[column][row] = tokens[index].toFloat(&ok); |
|
121 if (not ok) |
|
122 { |
|
123 throw BodyParseError{"non-numeric values for matrix"}; |
|
124 } |
|
125 } |
132 } |
126 for (int i = 0; i < 3; i += 1) |
133 else if (auto line3Match = tryRe(exprs().triangleRe)) { |
127 { |
134 LineType3* const parsed = init(LineType3{}, *line3Match); |
128 bool ok; |
135 parsed->value = { |
129 const auto value = tokens[i + positionStartingIndex].toFloat(&ok); |
136 Triangle{ |
130 result[3][i] = value; |
137 .p1 = vectorAttrib<Attribute::X1, Attribute::Y1, Attribute::Z1>(*parsed), |
131 if (not ok) |
138 .p2 = vectorAttrib<Attribute::X2, Attribute::Y2, Attribute::Z2>(*parsed), |
132 { |
139 .p3 = vectorAttrib<Attribute::X3, Attribute::Y3, Attribute::Z3>(*parsed), |
133 throw BodyParseError{"non-numeric values for matrix"}; |
140 }, |
134 } |
141 colorAttrib(parsed), |
|
142 }; |
|
143 } |
|
144 else if (auto line4Match = tryRe(exprs().quadRe)) { |
|
145 LineType4* const parsed = init(LineType4{}, *line4Match); |
|
146 parsed->value = { |
|
147 Quadrilateral{ |
|
148 .p1 = vectorAttrib<Attribute::X1, Attribute::Y1, Attribute::Z1>(*parsed), |
|
149 .p2 = vectorAttrib<Attribute::X2, Attribute::Y2, Attribute::Z2>(*parsed), |
|
150 .p3 = vectorAttrib<Attribute::X3, Attribute::Y3, Attribute::Z3>(*parsed), |
|
151 .p4 = vectorAttrib<Attribute::X4, Attribute::Y4, Attribute::Z4>(*parsed), |
|
152 }, |
|
153 colorAttrib(parsed), |
|
154 }; |
|
155 } |
|
156 else if (auto line5Match = tryRe(exprs().cedgeRe)) { |
|
157 LineType5* const parsed = init(LineType5{}, *line5Match); |
|
158 parsed->value = { |
|
159 ConditionalEdge{ |
|
160 .p1 = vectorAttrib<Attribute::X1, Attribute::Y1, Attribute::Z1>(*parsed), |
|
161 .p2 = vectorAttrib<Attribute::X2, Attribute::Y2, Attribute::Z2>(*parsed), |
|
162 .c1 = vectorAttrib<Attribute::X3, Attribute::Y3, Attribute::Z3>(*parsed), |
|
163 .c2 = vectorAttrib<Attribute::X4, Attribute::Y4, Attribute::Z4>(*parsed), |
|
164 }, |
|
165 colorAttrib(parsed), |
|
166 }; |
135 } |
167 } |
136 return result; |
168 return result; |
137 } |
169 } |
138 |
170 |
139 static Comment parseType0Line(const QString& line) |
171 #if 0 |
140 { |
172 static QRegExp re{R"((?:(\d+)\\)?(\d+)-(\d)+([a-z]+)\.dat)"}; |
141 return {line.mid(1).trimmed()}; |
173 if (re.exactMatch(name)) { |
142 } |
174 const auto p = std::find(std::begin(circularPrimitiveStems), std::end(circularPrimitiveStems), re.cap(4)); |
143 |
175 const unsigned int divisions = (re.cap(1).isEmpty()) ? 16 : re.cap(1).toUInt(); |
144 static ModelElement parseType1Line(const QStringList& tokens) |
176 const unsigned int segments = re.cap(2).toUInt() * divisions / re.cap(3).toUInt(); |
145 { |
177 if (p != std::end(circularPrimitiveStems)) { |
146 constexpr int colorPosition = 1; |
178 const auto type = static_cast<CircularPrimitive::Type>(p - std::begin(circularPrimitiveStems)); |
147 constexpr int positionPosition = 2; // 2..4 |
179 return Colored<CircularPrimitive>{ |
148 constexpr int transformPosition = 5; // 5..13 |
180 CircularPrimitive{ |
149 constexpr int namePosition = 14; |
181 .type = type, |
150 if (tokens.size() != 15) |
182 .fraction = {segments, divisions}, |
151 { |
183 .transformation = transform, |
152 throw BodyParseError{"wrong amount of tokens in a type-1 line"}; |
184 }, |
153 } |
185 color, |
154 const ldraw::Color color = colorFromString(tokens[colorPosition]); |
186 }; |
155 const glm::mat4 transform = matrixFromStrings(tokens, transformPosition, positionPosition); |
|
156 const QString& name = tokens[namePosition]; |
|
157 static QRegExp re{R"((?:(\d+)\\)?(\d+)-(\d)+([a-z]+)\.dat)"}; |
|
158 if (re.exactMatch(name)) { |
|
159 const auto p = std::find(std::begin(circularPrimitiveStems), std::end(circularPrimitiveStems), re.cap(4)); |
|
160 const unsigned int divisions = (re.cap(1).isEmpty()) ? 16 : re.cap(1).toUInt(); |
|
161 const unsigned int segments = re.cap(2).toUInt() * divisions / re.cap(3).toUInt(); |
|
162 if (p != std::end(circularPrimitiveStems)) { |
|
163 const auto type = static_cast<CircularPrimitive::Type>(p - std::begin(circularPrimitiveStems)); |
|
164 return Colored<CircularPrimitive>{ |
|
165 CircularPrimitive{ |
|
166 .type = type, |
|
167 .fraction = {segments, divisions}, |
|
168 .transformation = transform, |
|
169 }, |
|
170 color, |
|
171 }; |
|
172 } |
|
173 } |
|
174 return Colored<SubfileReference>{ |
|
175 { |
|
176 .name = name, |
|
177 .transformation = transform, |
|
178 }, |
|
179 color, |
|
180 }; |
|
181 } |
|
182 |
|
183 template<int NumVertices> |
|
184 static auto parsePolygon(const QStringList& tokens) |
|
185 { |
|
186 constexpr int colorPosition = 1; |
|
187 auto vertexPosition = [](int n) { return 2 + 3*n; }; |
|
188 if (tokens.size() != 2 + 3 * NumVertices) |
|
189 { |
|
190 throw BodyParseError{"wrong amount of tokens"}; |
|
191 } |
|
192 const ldraw::Color color = colorFromString(tokens[colorPosition]); |
|
193 std::array<glm::vec3, NumVertices> vertices; |
|
194 for (int i = 0; i < NumVertices; i += 1) |
|
195 { |
|
196 vertices[unsigned_cast(i)] = vertexFromStrings(tokens, vertexPosition(i)); |
|
197 } |
|
198 return std::make_pair(vertices, color); |
|
199 } |
|
200 |
|
201 ModelElement parseLDrawLine(QString line) |
|
202 { |
|
203 try |
|
204 { |
|
205 const QStringList tokens = line.simplified().split(" "); |
|
206 if (tokens.empty() or tokens == QStringList{{""}}) |
|
207 { |
|
208 return Empty{}; |
|
209 } |
|
210 bool ok_code; |
|
211 const int code = tokens[0].toInt(&ok_code); |
|
212 if (not ok_code) |
|
213 { |
|
214 throw BodyParseError{QObject::tr("line type was not an integer")}; |
|
215 } |
|
216 switch (code) |
|
217 { |
|
218 case 0: |
|
219 return parseType0Line(line); |
|
220 case 1: |
|
221 return parseType1Line(tokens); |
|
222 case 2: |
|
223 { |
|
224 const auto pair = parsePolygon<2>(tokens); |
|
225 return Colored<LineSegment>{{pair.first[0], pair.first[1]}, pair.second}; |
|
226 } |
|
227 case 3: |
|
228 { |
|
229 const auto pair = parsePolygon<3>(tokens); |
|
230 return Colored<Triangle>{{pair.first[0], pair.first[1], pair.first[2]}, pair.second |
|
231 }; |
|
232 } |
|
233 case 4: |
|
234 { |
|
235 const auto pair = parsePolygon<4>(tokens); |
|
236 const Quadrilateral quad{pair.first[0], pair.first[1], pair.first[2], pair.first[3]}; |
|
237 return Colored<Quadrilateral>{quad, pair.second}; |
|
238 } |
|
239 case 5: |
|
240 { |
|
241 const auto pair = parsePolygon<4>(tokens); |
|
242 const ConditionalEdge cedge{pair.first[0], pair.first[1], pair.first[2], pair.first[3]}; |
|
243 return Colored<ConditionalEdge>{cedge, pair.second}; |
|
244 } |
|
245 default: |
|
246 throw BodyParseError{QObject::tr("bad line type '%1'").arg(code)}; |
|
247 } |
|
248 } |
|
249 catch(const BodyParseError& error) |
|
250 { |
|
251 return ParseError{line}; |
|
252 } |
187 } |
253 } |
188 } |
|
189 #endif |
|
190 |