1 | // Copyright (C) 2019 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include "qssgqmlutilities_p.h" |
5 | #include "qssgscenedesc_p.h" |
6 | |
7 | #include <QVector2D> |
8 | #include <QVector3D> |
9 | #include <QVector4D> |
10 | #include <QQuaternion> |
11 | #include <QDebug> |
12 | #include <QRegularExpression> |
13 | #include <QtCore/qdir.h> |
14 | #include <QtCore/qfile.h> |
15 | #include <QtCore/qbuffer.h> |
16 | |
17 | #include <QtGui/qimage.h> |
18 | #include <QtGui/qimagereader.h> |
19 | |
20 | #include <QtQuick3DUtils/private/qssgmesh_p.h> |
21 | #include <QtQuick3DUtils/private/qssgassert_p.h> |
22 | |
23 | #include <QtQuick3DRuntimeRender/private/qssgrenderbuffermanager_p.h> |
24 | |
25 | #ifdef QT_QUICK3D_ENABLE_RT_ANIMATIONS |
26 | #include <QtCore/QCborStreamWriter> |
27 | #include <QtQuickTimeline/private/qquicktimeline_p.h> |
28 | #endif // QT_QUICK3D_ENABLE_RT_ANIMATIONS |
29 | |
30 | QT_BEGIN_NAMESPACE |
31 | |
32 | using namespace Qt::StringLiterals; |
33 | |
34 | namespace QSSGQmlUtilities { |
35 | |
36 | class PropertyMap |
37 | { |
38 | public: |
39 | typedef QHash<QByteArray, QVariant> PropertiesMap; |
40 | |
41 | static PropertyMap *instance(); |
42 | |
43 | PropertiesMap propertiesForType(QSSGSceneDesc::Node::RuntimeType type); |
44 | QVariant getDefaultValue(QSSGSceneDesc::Node::RuntimeType type, const char *property); |
45 | bool isDefaultValue(QSSGSceneDesc::Node::RuntimeType type, const char *property, const QVariant &value); |
46 | |
47 | private: |
48 | PropertyMap(); |
49 | |
50 | QHash<QSSGSceneDesc::Node::RuntimeType, PropertiesMap> m_properties; |
51 | |
52 | }; |
53 | |
54 | QString qmlComponentName(const QString &name) { |
55 | QString nameCopy = name; |
56 | if (nameCopy.isEmpty()) |
57 | return QStringLiteral("Presentation" ); |
58 | |
59 | nameCopy = sanitizeQmlId(id: nameCopy); |
60 | |
61 | if (nameCopy[0].isLower()) |
62 | nameCopy[0] = nameCopy[0].toUpper(); |
63 | |
64 | return nameCopy; |
65 | } |
66 | |
67 | QString colorToQml(const QColor &color) { |
68 | QString colorString; |
69 | colorString = QLatin1Char('\"') + color.name(format: QColor::HexArgb) + QLatin1Char('\"'); |
70 | return colorString; |
71 | } |
72 | |
73 | QString variantToQml(const QVariant &variant) { |
74 | switch (variant.typeId()) { |
75 | case QMetaType::Float: { |
76 | auto value = variant.toDouble(); |
77 | return QString::number(value); |
78 | } |
79 | case QMetaType::QVector2D: { |
80 | auto value = variant.value<QVector2D>(); |
81 | return QString(QStringLiteral("Qt.vector2d(" ) + QString::number(double(value.x())) + |
82 | QStringLiteral(", " ) + QString::number(double(value.y())) + |
83 | QStringLiteral(")" )); |
84 | } |
85 | case QMetaType::QVector3D: { |
86 | auto value = variant.value<QVector3D>(); |
87 | return QString(QStringLiteral("Qt.vector3d(" ) + QString::number(double(value.x())) + |
88 | QStringLiteral(", " ) + QString::number(double(value.y())) + |
89 | QStringLiteral(", " ) + QString::number(double(value.z())) + |
90 | QStringLiteral(")" )); |
91 | } |
92 | case QMetaType::QVector4D: { |
93 | auto value = variant.value<QVector4D>(); |
94 | return QString(QStringLiteral("Qt.vector4d(" ) + QString::number(double(value.x())) + |
95 | QStringLiteral(", " ) + QString::number(double(value.y())) + |
96 | QStringLiteral(", " ) + QString::number(double(value.z())) + |
97 | QStringLiteral(", " ) + QString::number(double(value.w())) + |
98 | QStringLiteral(")" )); |
99 | } |
100 | case QMetaType::QColor: { |
101 | auto value = variant.value<QColor>(); |
102 | return colorToQml(color: value); |
103 | } |
104 | case QMetaType::QQuaternion: { |
105 | auto value = variant.value<QQuaternion>(); |
106 | return QString(QStringLiteral("Qt.quaternion(" ) + QString::number(double(value.scalar())) + |
107 | QStringLiteral(", " ) + QString::number(double(value.x())) + |
108 | QStringLiteral(", " ) + QString::number(double(value.y())) + |
109 | QStringLiteral(", " ) + QString::number(double(value.z())) + |
110 | QStringLiteral(")" )); |
111 | } |
112 | default: |
113 | return variant.toString(); |
114 | } |
115 | } |
116 | |
117 | QString sanitizeQmlId(const QString &id) |
118 | { |
119 | QString idCopy = id; |
120 | // If the id starts with a number... |
121 | if (!idCopy.isEmpty() && idCopy.at(i: 0).isNumber()) |
122 | idCopy.prepend(QStringLiteral("node" )); |
123 | |
124 | // sometimes first letter is a # (don't replace with underscore) |
125 | if (idCopy.startsWith(c: QChar::fromLatin1(c: '#'))) |
126 | idCopy.remove(i: 0, len: 1); |
127 | |
128 | // Replace all the characters other than ascii letters, numbers or underscore to underscores. |
129 | static QRegularExpression regExp(QStringLiteral("\\W" )); |
130 | idCopy.replace(re: regExp, QStringLiteral("_" )); |
131 | |
132 | // first letter of id can not be upper case |
133 | // to make it look nicer, lower-case the initial run of all-upper-case characters |
134 | if (!idCopy.isEmpty() && idCopy[0].isUpper()) { |
135 | |
136 | int i = 0; |
137 | int len = idCopy.length(); |
138 | while (i < len && idCopy[i].isUpper()) { |
139 | idCopy[i] = idCopy[i].toLower(); |
140 | ++i; |
141 | } |
142 | } |
143 | |
144 | // ### qml keywords as names |
145 | static QSet<QByteArray> keywords { |
146 | "x" , |
147 | "y" , |
148 | "as" , |
149 | "do" , |
150 | "if" , |
151 | "in" , |
152 | "on" , |
153 | "of" , |
154 | "for" , |
155 | "get" , |
156 | "int" , |
157 | "let" , |
158 | "new" , |
159 | "set" , |
160 | "try" , |
161 | "var" , |
162 | "top" , |
163 | "byte" , |
164 | "case" , |
165 | "char" , |
166 | "else" , |
167 | "num" , |
168 | "from" , |
169 | "goto" , |
170 | "null" , |
171 | "this" , |
172 | "true" , |
173 | "void" , |
174 | "with" , |
175 | "clip" , |
176 | "item" , |
177 | "flow" , |
178 | "font" , |
179 | "text" , |
180 | "left" , |
181 | "data" , |
182 | "alias" , |
183 | "break" , |
184 | "state" , |
185 | "scale" , |
186 | "color" , |
187 | "right" , |
188 | "catch" , |
189 | "class" , |
190 | "const" , |
191 | "false" , |
192 | "float" , |
193 | "layer" , // Design Studio doesn't like "layer" as an id |
194 | "short" , |
195 | "super" , |
196 | "throw" , |
197 | "while" , |
198 | "yield" , |
199 | "border" , |
200 | "source" , |
201 | "delete" , |
202 | "double" , |
203 | "export" , |
204 | "import" , |
205 | "native" , |
206 | "public" , |
207 | "pragma" , |
208 | "return" , |
209 | "signal" , |
210 | "static" , |
211 | "switch" , |
212 | "throws" , |
213 | "bottom" , |
214 | "parent" , |
215 | "typeof" , |
216 | "boolean" , |
217 | "opacity" , |
218 | "enabled" , |
219 | "anchors" , |
220 | "padding" , |
221 | "default" , |
222 | "extends" , |
223 | "finally" , |
224 | "package" , |
225 | "private" , |
226 | "abstract" , |
227 | "continue" , |
228 | "debugger" , |
229 | "function" , |
230 | "property" , |
231 | "readonly" , |
232 | "children" , |
233 | "volatile" , |
234 | "interface" , |
235 | "protected" , |
236 | "transient" , |
237 | "implements" , |
238 | "instanceof" , |
239 | "synchronized" |
240 | }; |
241 | if (keywords.contains(value: idCopy.toUtf8())) { |
242 | idCopy += QStringLiteral("_" ); |
243 | } |
244 | |
245 | // We may have removed all the characters by now |
246 | if (idCopy.isEmpty()) |
247 | idCopy = QStringLiteral("node" ); |
248 | |
249 | return idCopy; |
250 | } |
251 | |
252 | QString sanitizeQmlSourcePath(const QString &source, bool removeParentDirectory) |
253 | { |
254 | QString sourceCopy = source; |
255 | |
256 | if (removeParentDirectory) |
257 | sourceCopy = QSSGQmlUtilities::stripParentDirectory(filePath: sourceCopy); |
258 | |
259 | sourceCopy.replace(before: QChar::fromLatin1(c: '\\'), after: QChar::fromLatin1(c: '/')); |
260 | |
261 | // must be surrounded in quotes |
262 | return QString(QStringLiteral("\"" ) + sourceCopy + QStringLiteral("\"" )); |
263 | } |
264 | |
265 | PropertyMap *PropertyMap::instance() |
266 | { |
267 | static PropertyMap p; |
268 | return &p; |
269 | } |
270 | |
271 | PropertyMap::PropertiesMap PropertyMap::propertiesForType(QSSGSceneDesc::Node::RuntimeType type) |
272 | { |
273 | return m_properties[type]; |
274 | } |
275 | |
276 | QVariant PropertyMap::getDefaultValue(QSSGSceneDesc::Node::RuntimeType type, const char *property) |
277 | { |
278 | QVariant value; |
279 | |
280 | if (m_properties.contains(key: type)) { |
281 | auto properties = m_properties[type]; |
282 | value = properties.value(key: property); |
283 | } |
284 | |
285 | return value; |
286 | } |
287 | |
288 | bool PropertyMap::isDefaultValue(QSSGSceneDesc::Node::RuntimeType type, const char *property, const QVariant &value) |
289 | { |
290 | bool isTheSame = value == getDefaultValue(type, property); |
291 | return isTheSame; |
292 | } |
293 | |
294 | static PropertyMap::PropertiesMap getObjectPropertiesMap(QObject *object) { |
295 | PropertyMap::PropertiesMap propertiesMap; |
296 | auto metaObject = object->metaObject(); |
297 | for (auto i = 0; i < metaObject->propertyCount(); ++i) { |
298 | auto property = metaObject->property(index: i); |
299 | const auto name = property.name(); |
300 | const auto value = property.read(obj: object); |
301 | propertiesMap.insert(key: name, value); |
302 | } |
303 | return propertiesMap; |
304 | } |
305 | |
306 | PropertyMap::PropertyMap() |
307 | { |
308 | // Create a table containing the default values for each property for each supported type |
309 | { |
310 | QQuick3DNode node; |
311 | m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::Node, value: getObjectPropertiesMap(object: &node)); |
312 | } |
313 | { |
314 | QQuick3DPrincipledMaterial principledMaterial; |
315 | m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::PrincipledMaterial, value: getObjectPropertiesMap(object: &principledMaterial)); |
316 | } |
317 | { |
318 | QQuick3DSpecularGlossyMaterial specularGlossyMaterial; |
319 | m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::SpecularGlossyMaterial, value: getObjectPropertiesMap(object: &specularGlossyMaterial)); |
320 | } |
321 | { |
322 | QQuick3DCustomMaterial customMaterial; |
323 | m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::CustomMaterial, value: getObjectPropertiesMap(object: &customMaterial)); |
324 | } |
325 | { |
326 | QQuick3DTexture texture; |
327 | m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::Image2D, value: getObjectPropertiesMap(object: &texture)); |
328 | } |
329 | { |
330 | QQuick3DCubeMapTexture cubeMapTexture; |
331 | m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::ImageCube, value: getObjectPropertiesMap(object: &cubeMapTexture)); |
332 | } |
333 | { |
334 | QQuick3DTextureData textureData; |
335 | m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::TextureData, value: getObjectPropertiesMap(object: &textureData)); |
336 | } |
337 | { |
338 | QQuick3DModel model; |
339 | m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::Model, value: getObjectPropertiesMap(object: &model)); |
340 | } |
341 | { |
342 | QQuick3DOrthographicCamera orthographicCamera; |
343 | m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::OrthographicCamera, value: getObjectPropertiesMap(object: &orthographicCamera)); |
344 | } |
345 | { |
346 | QQuick3DPerspectiveCamera perspectiveCamera; |
347 | m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::PerspectiveCamera, value: getObjectPropertiesMap(object: &perspectiveCamera)); |
348 | } |
349 | { |
350 | QQuick3DDirectionalLight directionalLight; |
351 | m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::DirectionalLight, value: getObjectPropertiesMap(object: &directionalLight)); |
352 | } |
353 | { |
354 | QQuick3DPointLight pointLight; |
355 | m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::PointLight, value: getObjectPropertiesMap(object: &pointLight)); |
356 | } |
357 | { |
358 | QQuick3DSpotLight spotLight; |
359 | m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::SpotLight, value: getObjectPropertiesMap(object: &spotLight)); |
360 | } |
361 | { |
362 | QQuick3DSkeleton skeleton; |
363 | m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::Skeleton, value: getObjectPropertiesMap(object: &skeleton)); |
364 | } |
365 | { |
366 | QQuick3DJoint joint; |
367 | m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::Joint, value: getObjectPropertiesMap(object: &joint)); |
368 | } |
369 | { |
370 | QQuick3DSkin skin; |
371 | m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::Skin, value: getObjectPropertiesMap(object: &skin)); |
372 | } |
373 | { |
374 | QQuick3DMorphTarget morphTarget; |
375 | m_properties.insert(key: QSSGSceneDesc::Node::RuntimeType::MorphTarget, value: getObjectPropertiesMap(object: &morphTarget)); |
376 | } |
377 | } |
378 | |
379 | struct OutputContext |
380 | { |
381 | enum Type : quint8 { , RootNode, NodeTree, Resource }; |
382 | enum Options : quint8 |
383 | { |
384 | None, |
385 | ExpandValueComponents = 0x1, |
386 | DesignStudioWorkarounds = ExpandValueComponents | 0x2 |
387 | }; |
388 | QTextStream &stream; |
389 | QDir outdir; |
390 | QString sourceDir; |
391 | quint8 indent = 0; |
392 | Type type = NodeTree; |
393 | quint8 options = Options::None; |
394 | quint16 scopeDepth = 0; |
395 | }; |
396 | |
397 | template<QSSGSceneDesc::Material::RuntimeType T> |
398 | const char *qmlElementName() { static_assert(!std::is_same_v<decltype(T), decltype(T)>, "Unknown type" ); return nullptr; } |
399 | template<> const char *qmlElementName<QSSGSceneDesc::Node::RuntimeType::Node>() { return "Node" ; } |
400 | |
401 | template<> const char *qmlElementName<QSSGSceneDesc::Material::RuntimeType::SpecularGlossyMaterial>() { return "SpecularGlossyMaterial" ; } |
402 | template<> const char *qmlElementName<QSSGSceneDesc::Material::RuntimeType::PrincipledMaterial>() { return "PrincipledMaterial" ; } |
403 | template<> const char *qmlElementName<QSSGSceneDesc::Material::RuntimeType::CustomMaterial>() { return "CustomMaterial" ; } |
404 | template<> const char *qmlElementName<QSSGSceneDesc::Material::RuntimeType::OrthographicCamera>() { return "OrthographicCamera" ; } |
405 | template<> const char *qmlElementName<QSSGSceneDesc::Material::RuntimeType::PerspectiveCamera>() { return "PerspectiveCamera" ; } |
406 | |
407 | template<> const char *qmlElementName<QSSGSceneDesc::Node::RuntimeType::Model>() { return "Model" ; } |
408 | |
409 | template<> const char *qmlElementName<QSSGSceneDesc::Texture::RuntimeType::Image2D>() { return "Texture" ; } |
410 | template<> const char *qmlElementName<QSSGSceneDesc::Texture::RuntimeType::ImageCube>() { return "CubeMapTexture" ; } |
411 | template<> const char *qmlElementName<QSSGSceneDesc::Texture::RuntimeType::TextureData>() { return "TextureData" ; } |
412 | |
413 | template<> const char *qmlElementName<QSSGSceneDesc::Camera::RuntimeType::DirectionalLight>() { return "DirectionalLight" ; } |
414 | template<> const char *qmlElementName<QSSGSceneDesc::Camera::RuntimeType::SpotLight>() { return "SpotLight" ; } |
415 | template<> const char *qmlElementName<QSSGSceneDesc::Camera::RuntimeType::PointLight>() { return "PointLight" ; } |
416 | |
417 | template<> const char *qmlElementName<QSSGSceneDesc::Joint::RuntimeType::Joint>() { return "Joint" ; } |
418 | template<> const char *qmlElementName<QSSGSceneDesc::Skeleton::RuntimeType::Skeleton>() { return "Skeleton" ; } |
419 | template<> const char *qmlElementName<QSSGSceneDesc::Node::RuntimeType::Skin>() { return "Skin" ; } |
420 | template<> const char *qmlElementName<QSSGSceneDesc::Node::RuntimeType::MorphTarget>() { return "MorphTarget" ; } |
421 | |
422 | static const char *getQmlElementName(const QSSGSceneDesc::Node &node) |
423 | { |
424 | using RuntimeType = QSSGSceneDesc::Node::RuntimeType; |
425 | switch (node.runtimeType) { |
426 | case RuntimeType::Node: |
427 | return qmlElementName<RuntimeType::Node>(); |
428 | case RuntimeType::PrincipledMaterial: |
429 | return qmlElementName<RuntimeType::PrincipledMaterial>(); |
430 | case RuntimeType::SpecularGlossyMaterial: |
431 | return qmlElementName<RuntimeType::SpecularGlossyMaterial>(); |
432 | case RuntimeType::CustomMaterial: |
433 | return qmlElementName<RuntimeType::CustomMaterial>(); |
434 | case RuntimeType::Image2D: |
435 | return qmlElementName<RuntimeType::Image2D>(); |
436 | case RuntimeType::ImageCube: |
437 | return qmlElementName<RuntimeType::ImageCube>(); |
438 | case RuntimeType::TextureData: |
439 | return qmlElementName<RuntimeType::TextureData>(); |
440 | case RuntimeType::Model: |
441 | return qmlElementName<RuntimeType::Model>(); |
442 | case RuntimeType::OrthographicCamera: |
443 | return qmlElementName<RuntimeType::OrthographicCamera>(); |
444 | case RuntimeType::PerspectiveCamera: |
445 | return qmlElementName<RuntimeType::PerspectiveCamera>(); |
446 | case RuntimeType::DirectionalLight: |
447 | return qmlElementName<RuntimeType::DirectionalLight>(); |
448 | case RuntimeType::PointLight: |
449 | return qmlElementName<RuntimeType::PointLight>(); |
450 | case RuntimeType::SpotLight: |
451 | return qmlElementName<RuntimeType::SpotLight>(); |
452 | case RuntimeType::Skeleton: |
453 | return qmlElementName<RuntimeType::Skeleton>(); |
454 | case RuntimeType::Joint: |
455 | return qmlElementName<RuntimeType::Joint>(); |
456 | case RuntimeType::Skin: |
457 | return qmlElementName<RuntimeType::Skin>(); |
458 | case RuntimeType::MorphTarget: |
459 | return qmlElementName<RuntimeType::MorphTarget>(); |
460 | default: |
461 | return "UNKNOWN_TYPE" ; |
462 | } |
463 | } |
464 | |
465 | enum QMLBasicType |
466 | { |
467 | Bool, |
468 | Double, |
469 | Int, |
470 | List, |
471 | Real, |
472 | String, |
473 | Url, |
474 | Var, |
475 | Color, |
476 | Date, |
477 | Font, |
478 | Mat44, |
479 | Point, |
480 | Quaternion, |
481 | Rect, |
482 | Size, |
483 | Vector2D, |
484 | Vector3D, |
485 | Vector4D, |
486 | Unknown_Count |
487 | }; |
488 | |
489 | static constexpr QByteArrayView qml_basic_types[] { |
490 | "bool" , |
491 | "double" , |
492 | "int" , |
493 | "list" , |
494 | "real" , |
495 | "string" , |
496 | "url" , |
497 | "var" , |
498 | "color" , |
499 | "date" , |
500 | "font" , |
501 | "matrix4x4" , |
502 | "point" , |
503 | "quaternion" , |
504 | "rect" , |
505 | "size" , |
506 | "vector2d" , |
507 | "vector3d" , |
508 | "vector4d" |
509 | }; |
510 | |
511 | static_assert(std::size(qml_basic_types) == QMLBasicType::Unknown_Count, "Missing type?" ); |
512 | |
513 | static QByteArrayView typeName(QMetaType mt) |
514 | { |
515 | switch (mt.id()) { |
516 | case QMetaType::Bool: |
517 | return qml_basic_types[QMLBasicType::Bool]; |
518 | case QMetaType::Char: |
519 | case QMetaType::SChar: |
520 | case QMetaType::UChar: |
521 | case QMetaType::Char16: |
522 | case QMetaType::Char32: |
523 | case QMetaType::QChar: |
524 | case QMetaType::Short: |
525 | case QMetaType::UShort: |
526 | case QMetaType::Int: |
527 | case QMetaType::UInt: |
528 | case QMetaType::Long: |
529 | case QMetaType::ULong: |
530 | case QMetaType::LongLong: |
531 | case QMetaType::ULongLong: |
532 | return qml_basic_types[QMLBasicType::Int]; |
533 | case QMetaType::Float: |
534 | case QMetaType::Double: |
535 | return qml_basic_types[QMLBasicType::Real]; |
536 | case QMetaType::QByteArray: |
537 | case QMetaType::QString: |
538 | return qml_basic_types[QMLBasicType::String]; |
539 | case QMetaType::QDate: |
540 | case QMetaType::QTime: |
541 | case QMetaType::QDateTime: |
542 | return qml_basic_types[QMLBasicType::Date]; |
543 | case QMetaType::QUrl: |
544 | return qml_basic_types[QMLBasicType::Url]; |
545 | case QMetaType::QRect: |
546 | case QMetaType::QRectF: |
547 | return qml_basic_types[QMLBasicType::Rect]; |
548 | case QMetaType::QSize: |
549 | case QMetaType::QSizeF: |
550 | return qml_basic_types[QMLBasicType::Size]; |
551 | case QMetaType::QPoint: |
552 | case QMetaType::QPointF: |
553 | return qml_basic_types[QMLBasicType::Point]; |
554 | case QMetaType::QVariant: |
555 | return qml_basic_types[QMLBasicType::Var]; |
556 | case QMetaType::QColor: |
557 | return qml_basic_types[QMLBasicType::Color]; |
558 | case QMetaType::QMatrix4x4: |
559 | return qml_basic_types[QMLBasicType::Mat44]; |
560 | case QMetaType::QVector2D: |
561 | return qml_basic_types[QMLBasicType::Vector2D]; |
562 | case QMetaType::QVector3D: |
563 | return qml_basic_types[QMLBasicType::Vector3D]; |
564 | case QMetaType::QVector4D: |
565 | return qml_basic_types[QMLBasicType::Vector4D]; |
566 | case QMetaType::QQuaternion: |
567 | return qml_basic_types[QMLBasicType::Quaternion]; |
568 | case QMetaType::QFont: |
569 | return qml_basic_types[QMLBasicType::Font]; |
570 | default: |
571 | return qml_basic_types[QMLBasicType::Var]; |
572 | } |
573 | } |
574 | |
575 | using NodeNameMap = QHash<const QSSGSceneDesc::Node *, QString>; |
576 | Q_GLOBAL_STATIC(NodeNameMap, g_nodeNameMap) |
577 | using UniqueIdMap = QHash<QString, const QSSGSceneDesc::Node *>; |
578 | Q_GLOBAL_STATIC(UniqueIdMap, g_idMap) |
579 | // Normally g_idMap will contain all the ids but in some cases |
580 | // (like Animation, not Node) the ids will just be stored |
581 | // to avoid conflict. |
582 | // Now, Animations will be processed after all the Nodes, |
583 | // For Nodes, it is not used. |
584 | using UniqueIdOthers = QSet<QString>; |
585 | Q_GLOBAL_STATIC(UniqueIdOthers, g_idOthers) |
586 | |
587 | static QString getIdForNode(const QSSGSceneDesc::Node &node) |
588 | { |
589 | static constexpr const char *typeNames[] = { |
590 | "" , // Transform |
591 | "_camera" , |
592 | "" , // Model |
593 | "_texture" , |
594 | "_material" , |
595 | "_light" , |
596 | "_mesh" , |
597 | "_skin" , |
598 | "_skeleton" , |
599 | "_joint" , |
600 | "_morphtarget" , |
601 | "_unknown" |
602 | }; |
603 | constexpr uint nameCount = sizeof(typeNames)/sizeof(const char*); |
604 | const bool nodeHasName = (node.name.size() > 0); |
605 | uint nameIdx = qMin(a: uint(node.nodeType), b: nameCount); |
606 | QString name = nodeHasName ? QString::fromUtf8(ba: node.name + typeNames[nameIdx]) : QString::fromLatin1(ba: getQmlElementName(node)); |
607 | QString sanitizedName = QSSGQmlUtilities::sanitizeQmlId(id: name); |
608 | |
609 | // Make sure we return a unique id. |
610 | if (const auto it = g_nodeNameMap->constFind(key: &node); it != g_nodeNameMap->constEnd()) |
611 | return *it; |
612 | |
613 | quint64 id = node.id; |
614 | int attempts = 1000; |
615 | QString candidate = sanitizedName; |
616 | do { |
617 | if (const auto it = g_idMap->constFind(key: candidate); it == g_idMap->constEnd()) { |
618 | g_idMap->insert(key: candidate, value: &node); |
619 | g_nodeNameMap->insert(key: &node, value: candidate); |
620 | return candidate; |
621 | } |
622 | |
623 | candidate = QStringLiteral("%1%2" ).arg(a: sanitizedName).arg(a: id++); |
624 | } while (--attempts); |
625 | |
626 | return candidate; |
627 | } |
628 | |
629 | static QString getIdForAnimation(const QByteArray &inName) |
630 | { |
631 | QString name = !inName.isEmpty() ? QString::fromUtf8(ba: inName + "_timeline" ) : "timeline0"_L1 ; |
632 | QString sanitizedName = QSSGQmlUtilities::sanitizeQmlId(id: name); |
633 | |
634 | int attempts = 1000; |
635 | quint16 id = 0; |
636 | QString candidate = sanitizedName; |
637 | do { |
638 | if (const auto it = g_idMap->constFind(key: candidate); it == g_idMap->constEnd()) { |
639 | if (const auto oIt = g_idOthers->constFind(value: candidate); oIt == g_idOthers->constEnd()) { |
640 | g_idOthers->insert(value: candidate); |
641 | return candidate; |
642 | } |
643 | } |
644 | |
645 | candidate = QStringLiteral("%1%2" ).arg(a: sanitizedName).arg(a: ++id); |
646 | } while (--attempts); |
647 | |
648 | return candidate; |
649 | } |
650 | |
651 | QString stripParentDirectory(const QString &filePath) { |
652 | QString sourceCopy = filePath; |
653 | while (sourceCopy.startsWith(c: QChar::fromLatin1(c: '.')) || sourceCopy.startsWith(c: QChar::fromLatin1(c: '/')) || sourceCopy.startsWith(c: QChar::fromLatin1(c: '\\'))) |
654 | sourceCopy.remove(i: 0, len: 1); |
655 | return sourceCopy; |
656 | } |
657 | |
658 | static const char *blockBegin() { return " {\n" ; } |
659 | static const char *blockEnd() { return "}\n" ; } |
660 | static const char *() { return "// " ; } |
661 | static const char *indent() { return " " ; } |
662 | |
663 | struct QSSGQmlScopedIndent |
664 | { |
665 | enum : quint8 { QSSG_INDENT = 4 }; |
666 | explicit QSSGQmlScopedIndent(OutputContext &out) : output(out) { out.indent += QSSG_INDENT; }; |
667 | ~QSSGQmlScopedIndent() { output.indent = qMax(a: output.indent - QSSG_INDENT, b: 0); } |
668 | OutputContext &output; |
669 | }; |
670 | |
671 | static QString indentString(OutputContext &output) |
672 | { |
673 | QString str; |
674 | for (quint8 i = 0; i < output.indent; i += QSSGQmlScopedIndent::QSSG_INDENT) |
675 | str += QString::fromLatin1(ba: indent()); |
676 | return str; |
677 | } |
678 | |
679 | static QTextStream &indent(OutputContext &output) |
680 | { |
681 | for (quint8 i = 0; i < output.indent; i += QSSGQmlScopedIndent::QSSG_INDENT) |
682 | output.stream << indent(); |
683 | return output.stream; |
684 | } |
685 | |
686 | static const char *blockBegin(OutputContext &output) |
687 | { |
688 | ++output.scopeDepth; |
689 | return blockBegin(); |
690 | } |
691 | |
692 | static const char *blockEnd(OutputContext &output) |
693 | { |
694 | output.scopeDepth = qMax(a: 0, b: output.scopeDepth - 1); |
695 | return blockEnd(); |
696 | } |
697 | |
698 | static void (OutputContext &output, bool hasAnimation = false) |
699 | { |
700 | output.stream << "import QtQuick\n" |
701 | << "import QtQuick3D\n\n" ; |
702 | if (hasAnimation) |
703 | output.stream << "import QtQuick.Timeline\n\n" ; |
704 | } |
705 | |
706 | static QString toQuotedString(const QString &text) { return QStringLiteral("\"%1\"" ).arg(a: text); } |
707 | |
708 | static inline QString getMeshFolder() { return QStringLiteral("meshes/" ); } |
709 | static inline QString getMeshExtension() { return QStringLiteral(".mesh" ); } |
710 | |
711 | QString getMeshSourceName(const QString &name) |
712 | { |
713 | const auto meshFolder = getMeshFolder(); |
714 | const auto extension = getMeshExtension(); |
715 | |
716 | return QString(meshFolder + name + extension); |
717 | } |
718 | |
719 | static inline QString getTextureFolder() { return QStringLiteral("maps/" ); } |
720 | |
721 | static inline QString getAnimationFolder() { return QStringLiteral("animations/" ); } |
722 | static inline QString getAnimationExtension() { return QStringLiteral(".qad" ); } |
723 | QString getAnimationSourceName(const QString &id, const QString &property, qsizetype index) |
724 | { |
725 | const auto animationFolder = getAnimationFolder(); |
726 | const auto extension = getAnimationExtension(); |
727 | return QString(animationFolder + id + QStringLiteral("_" ) |
728 | + property + QStringLiteral("_" ) |
729 | + QString::number(index) + extension); |
730 | } |
731 | |
732 | QString asString(const QVariant &var) |
733 | { |
734 | return var.toString(); |
735 | } |
736 | |
737 | QString builtinQmlType(const QVariant &var) |
738 | { |
739 | switch (var.metaType().id()) { |
740 | case QMetaType::QVector2D: { |
741 | const auto vec2 = qvariant_cast<QVector2D>(v: var); |
742 | return QLatin1String("Qt.vector2d(" ) + QString::number(vec2.x()) + QLatin1String(", " ) + QString::number(vec2.y()) + QLatin1Char(')'); |
743 | } |
744 | case QMetaType::QVector3D: { |
745 | const auto vec3 = qvariant_cast<QVector3D>(v: var); |
746 | return QLatin1String("Qt.vector3d(" ) + QString::number(vec3.x()) + QLatin1String(", " ) |
747 | + QString::number(vec3.y()) + QLatin1String(", " ) |
748 | + QString::number(vec3.z()) + QLatin1Char(')'); |
749 | } |
750 | case QMetaType::QVector4D: { |
751 | const auto vec4 = qvariant_cast<QVector4D>(v: var); |
752 | return QLatin1String("Qt.vector4d(" ) + QString::number(vec4.x()) + QLatin1String(", " ) |
753 | + QString::number(vec4.y()) + QLatin1String(", " ) |
754 | + QString::number(vec4.z()) + QLatin1String(", " ) |
755 | + QString::number(vec4.w()) + QLatin1Char(')'); |
756 | } |
757 | case QMetaType::QColor: { |
758 | const auto color = qvariant_cast<QColor>(v: var); |
759 | return colorToQml(color); |
760 | } |
761 | case QMetaType::QQuaternion: { |
762 | const auto &quat = qvariant_cast<QQuaternion>(v: var); |
763 | return QLatin1String("Qt.quaternion(" ) + QString::number(quat.scalar()) + QLatin1String(", " ) |
764 | + QString::number(quat.x()) + QLatin1String(", " ) |
765 | + QString::number(quat.y()) + QLatin1String(", " ) |
766 | + QString::number(quat.z()) + QLatin1Char(')'); |
767 | } |
768 | case QMetaType::QMatrix4x4: { |
769 | const auto mat44 = qvariant_cast<QMatrix4x4>(v: var); |
770 | return QLatin1String("Qt.matrix4x4(" ) |
771 | + QString::number(mat44(0, 0)) + u", " + QString::number(mat44(0, 1)) + u", " + QString::number(mat44(0, 2)) + u", " + QString::number(mat44(0, 3)) + u", " |
772 | + QString::number(mat44(1, 0)) + u", " + QString::number(mat44(1, 1)) + u", " + QString::number(mat44(1, 2)) + u", " + QString::number(mat44(1, 3)) + u", " |
773 | + QString::number(mat44(2, 0)) + u", " + QString::number(mat44(2, 1)) + u", " + QString::number(mat44(2, 2)) + u", " + QString::number(mat44(2, 3)) + u", " |
774 | + QString::number(mat44(3, 0)) + u", " + QString::number(mat44(3, 1)) + u", " + QString::number(mat44(3, 2)) + u", " + QString::number(mat44(3, 3)) + u')'; |
775 | } |
776 | case QMetaType::Float: |
777 | case QMetaType::Double: |
778 | case QMetaType::Int: |
779 | case QMetaType::Char: |
780 | case QMetaType::Long: |
781 | case QMetaType::LongLong: |
782 | case QMetaType::ULong: |
783 | case QMetaType::ULongLong: |
784 | case QMetaType::Bool: |
785 | return var.toString(); |
786 | case QMetaType::QUrl: // QUrl needs special handling. Return empty string to trigger that. |
787 | default: |
788 | break; |
789 | } |
790 | |
791 | return QString(); |
792 | } |
793 | |
794 | QString asString(QSSGSceneDesc::Animation::Channel::TargetProperty prop) |
795 | { |
796 | if (prop == QSSGSceneDesc::Animation::Channel::TargetProperty::Position) |
797 | return QStringLiteral("position" ); |
798 | if (prop == QSSGSceneDesc::Animation::Channel::TargetProperty::Rotation) |
799 | return QStringLiteral("rotation" ); |
800 | if (prop == QSSGSceneDesc::Animation::Channel::TargetProperty::Scale) |
801 | return QStringLiteral("scale" ); |
802 | if (prop == QSSGSceneDesc::Animation::Channel::TargetProperty::Weight) |
803 | return QStringLiteral("weight" ); |
804 | |
805 | return QStringLiteral("unknown" ); |
806 | } |
807 | |
808 | static std::pair<QString, QString> meshAssetName(const QSSGSceneDesc::Scene &scene, const QSSGSceneDesc::Mesh &meshNode, const QDir &outdir) |
809 | { |
810 | // Returns {name, notValidReason} |
811 | |
812 | const auto meshFolder = getMeshFolder(); |
813 | const auto meshId = QSSGQmlUtilities::getIdForNode(node: meshNode); |
814 | const auto meshSourceName = QSSGQmlUtilities::getMeshSourceName(name: meshId); |
815 | Q_ASSERT(scene.meshStorage.size() > meshNode.idx); |
816 | const auto &mesh = scene.meshStorage.at(i: meshNode.idx); |
817 | |
818 | // If a mesh folder does not exist, then create one |
819 | if (!outdir.exists(name: meshFolder) && !outdir.mkdir(dirName: meshFolder)) { |
820 | qDebug() << "Failed to create meshes folder at" << outdir; |
821 | return {}; // Error out |
822 | } |
823 | |
824 | const QString path = outdir.path() + QDir::separator() + meshSourceName; |
825 | QFile file(path); |
826 | if (!file.open(flags: QIODevice::WriteOnly)) { |
827 | return {QString(), QStringLiteral("Failed to find mesh at " ) + path}; |
828 | } |
829 | |
830 | if (mesh.save(device: &file) == 0) { |
831 | return {}; |
832 | } |
833 | |
834 | return {meshSourceName, QString()}; |
835 | }; |
836 | |
837 | static std::pair<QString, QString> copyTextureAsset(const QUrl &texturePath, OutputContext &output) |
838 | { |
839 | // Returns {path, notValidReason} |
840 | |
841 | // TODO: Use QUrl::resolved() instead of manual string manipulation |
842 | QString assetPath = output.outdir.isAbsolutePath(path: texturePath.path()) ? texturePath.toString() : texturePath.path(); |
843 | QFileInfo fi(assetPath); |
844 | if (fi.isRelative() && !output.sourceDir.isEmpty()) { |
845 | fi = QFileInfo(output.sourceDir + QChar(u'/') + assetPath); |
846 | } |
847 | if (!fi.exists()) { |
848 | indent(output) << comment() << "Source texture path expected: " << getTextureFolder() + texturePath.fileName() << "\n" ; |
849 | return {QString(), QStringLiteral("Failed to find texture at " ) + assetPath}; |
850 | } |
851 | |
852 | const auto mapsFolder = getTextureFolder(); |
853 | // If a maps folder does not exist, then create one |
854 | if (!output.outdir.exists(name: mapsFolder) && !output.outdir.mkdir(dirName: mapsFolder)) { |
855 | qDebug() << "Failed to create maps folder at" << output.outdir; |
856 | return {}; // Error out |
857 | } |
858 | |
859 | const QString relpath = mapsFolder + fi.fileName(); |
860 | const auto newfilepath = QString(output.outdir.canonicalPath() + QDir::separator() + relpath); |
861 | if (!QFile::exists(fileName: newfilepath) && !QFile::copy(fileName: fi.canonicalFilePath(), newName: newfilepath)) { |
862 | qDebug() << "Failed to copy file from" << fi.canonicalFilePath() << "to" << newfilepath; |
863 | return {}; |
864 | } |
865 | |
866 | return {relpath, QString()}; |
867 | }; |
868 | |
869 | static QStringList expandComponents(const QString &value, QMetaType mt) |
870 | { |
871 | static const QRegularExpression re(QLatin1String("^Qt.[a-z0-9]*\\(([0-9.e\\+\\-, ]*)\\)" )); |
872 | Q_ASSERT(re.isValid()); |
873 | |
874 | switch (mt.id()) { |
875 | case QMetaType::QVector2D: { |
876 | QRegularExpressionMatch match = re.match(subject: value); |
877 | if (match.hasMatch()) { |
878 | const auto comp = match.captured(nth: 1).split(sep: QLatin1Char(',')); |
879 | if (comp.size() == 2) { |
880 | return { QLatin1String(".x: " ) + comp.at(i: 0).trimmed(), |
881 | QLatin1String(".y: " ) + comp.at(i: 1).trimmed() }; |
882 | } |
883 | } |
884 | break; |
885 | } |
886 | case QMetaType::QVector3D: { |
887 | QRegularExpressionMatch match = re.match(subject: value); |
888 | if (match.hasMatch()) { |
889 | const auto comp = match.captured(nth: 1).split(sep: QLatin1Char(',')); |
890 | if (comp.size() == 3) { |
891 | return { QLatin1String(".x: " ) + comp.at(i: 0).trimmed(), |
892 | QLatin1String(".y: " ) + comp.at(i: 1).trimmed(), |
893 | QLatin1String(".z: " ) + comp.at(i: 2).trimmed() }; |
894 | } |
895 | } |
896 | break; |
897 | } |
898 | case QMetaType::QVector4D: { |
899 | QRegularExpressionMatch match = re.match(subject: value); |
900 | if (match.hasMatch()) { |
901 | const auto comp = match.captured(nth: 1).split(sep: QLatin1Char(',')); |
902 | if (comp.size() == 4) { |
903 | return { QLatin1String(".x: " ) + comp.at(i: 0).trimmed(), |
904 | QLatin1String(".y: " ) + comp.at(i: 1).trimmed(), |
905 | QLatin1String(".z: " ) + comp.at(i: 2).trimmed(), |
906 | QLatin1String(".w: " ) + comp.at(i: 3).trimmed() }; |
907 | } |
908 | } |
909 | break; |
910 | } |
911 | case QMetaType::QQuaternion: { |
912 | QRegularExpressionMatch match = re.match(subject: value); |
913 | if (match.hasMatch()) { |
914 | const auto comp = match.captured(nth: 1).split(sep: QLatin1Char(',')); |
915 | if (comp.size() == 4) { |
916 | return { QLatin1String(".x: " ) + comp.at(i: 0).trimmed(), |
917 | QLatin1String(".y: " ) + comp.at(i: 1).trimmed(), |
918 | QLatin1String(".z: " ) + comp.at(i: 2).trimmed(), |
919 | QLatin1String(".scalar: " ) + comp.at(i: 3).trimmed() }; |
920 | } |
921 | } |
922 | break; |
923 | } |
924 | default: |
925 | break; |
926 | } |
927 | |
928 | return { value }; |
929 | } |
930 | |
931 | static QStringList expandComponentsPartially(const QString &value, QMetaType mt) |
932 | { |
933 | // Workaround for DS |
934 | if (mt.id() != QMetaType::QQuaternion) |
935 | return expandComponents(value, mt); |
936 | |
937 | return { value }; |
938 | } |
939 | |
940 | struct ValueToQmlResult { |
941 | bool ok = false; |
942 | QString name; |
943 | QString value; |
944 | QString notValidReason; |
945 | bool isDynamicProperty = false; |
946 | QStringList expandedProperties; |
947 | }; |
948 | |
949 | static ValueToQmlResult valueToQml(const QSSGSceneDesc::Node &target, const QSSGSceneDesc::Property &property, OutputContext &output) |
950 | { |
951 | ValueToQmlResult result; |
952 | if (property.value.isNull()) { |
953 | result.ok = false; |
954 | result.notValidReason = QStringLiteral("Property value is null" ); |
955 | return result; |
956 | } |
957 | |
958 | const QVariant &value = property.value; |
959 | result.name = QString::fromUtf8(ba: property.name); |
960 | result.isDynamicProperty = property.type == QSSGSceneDesc::Property::Type::Dynamic; |
961 | |
962 | // Built-in types |
963 | QString valueAsString = builtinQmlType(var: value); |
964 | if (valueAsString.size() > 0) { |
965 | result.value = valueAsString; |
966 | result.ok = true; |
967 | } else if (value.metaType().flags() & (QMetaType::IsEnumeration | QMetaType::IsUnsignedEnumeration)) { |
968 | static const auto qmlEnumString = [](const QLatin1String &element, const QString &enumString) { |
969 | return QStringLiteral("%1.%2" ).arg(a: element).arg(a: enumString); |
970 | }; |
971 | QLatin1String qmlElementName(getQmlElementName(node: target)); |
972 | QString enumValue = asString(var: value); |
973 | if (enumValue.size() > 0) { |
974 | result.value = qmlEnumString(qmlElementName, enumValue); |
975 | result.ok = true; |
976 | } |
977 | } else if (value.metaType().id() == qMetaTypeId<QSSGSceneDesc::Flag>()) { |
978 | QByteArray element(getQmlElementName(node: target)); |
979 | if (element.size() > 0) { |
980 | const auto flag = qvariant_cast<QSSGSceneDesc::Flag>(v: value); |
981 | QByteArray keysString = flag.me.valueToKeys(value: int(flag.value)); |
982 | if (keysString.size() > 0) { |
983 | keysString.prepend(a: element + '.'); |
984 | QByteArray replacement(" | " + element + '.'); |
985 | keysString.replace(before: '|', after: replacement); |
986 | result.value = QString::fromLatin1(ba: keysString); |
987 | result.ok = true; |
988 | } |
989 | } |
990 | } else if (value.metaType().id() == qMetaTypeId<QSSGSceneDesc::NodeList *>()) { |
991 | const auto *list = qvariant_cast<QSSGSceneDesc::NodeList *>(v: value); |
992 | if (list->count > 0) { |
993 | const QString indentStr = indentString(output); |
994 | QSSGQmlScopedIndent scopedIndent(output); |
995 | const QString listIndentStr = indentString(output); |
996 | |
997 | QString str; |
998 | str.append(v: u"[\n" ); |
999 | |
1000 | for (int i = 0, end = list->count; i != end; ++i) { |
1001 | if (i != 0) |
1002 | str.append(v: u",\n" ); |
1003 | str.append(s: listIndentStr); |
1004 | str.append(s: getIdForNode(node: *(list->head[i]))); |
1005 | } |
1006 | |
1007 | str.append(s: u'\n' + indentStr + u']'); |
1008 | |
1009 | result.value = str; |
1010 | result.ok = true; |
1011 | } |
1012 | } else if (value.metaType().id() == qMetaTypeId<QSSGSceneDesc::ListView *>()) { |
1013 | const auto &list = *qvariant_cast<QSSGSceneDesc::ListView *>(v: value); |
1014 | if (list.count > 0) { |
1015 | const QString indentStr = indentString(output); |
1016 | QSSGQmlScopedIndent scopedIndent(output); |
1017 | const QString listIndentStr = indentString(output); |
1018 | |
1019 | QString str; |
1020 | str.append(v: u"[\n" ); |
1021 | |
1022 | char *vptr = reinterpret_cast<char *>(list.data); |
1023 | auto size = list.mt.sizeOf(); |
1024 | |
1025 | for (int i = 0, end = list.count; i != end; ++i) { |
1026 | if (i != 0) |
1027 | str.append(v: u",\n" ); |
1028 | |
1029 | const QVariant var{list.mt, reinterpret_cast<void *>(vptr + (size * i))}; |
1030 | QString valueString = builtinQmlType(var); |
1031 | if (valueString.isEmpty()) |
1032 | valueString = asString(var); |
1033 | |
1034 | str.append(s: listIndentStr); |
1035 | str.append(s: valueString); |
1036 | } |
1037 | |
1038 | str.append(s: u'\n' + indentStr + u']'); |
1039 | |
1040 | result.value = str; |
1041 | result.ok = true; |
1042 | } |
1043 | } else if (value.metaType().id() == qMetaTypeId<QSSGSceneDesc::Node *>()) { |
1044 | if (const auto node = qvariant_cast<QSSGSceneDesc::Node *>(v: value)) { |
1045 | // If this assert is triggerd it likely means that the node never got added |
1046 | // to the scene tree (see: addNode()) or that it's a type not handled as a resource, see: |
1047 | // writeQmlForResources() |
1048 | Q_ASSERT(node->id != 0); |
1049 | // The 'TextureData' node will have its data written out and become |
1050 | // a source url. |
1051 | |
1052 | if (node->runtimeType == QSSGSceneDesc::Node::RuntimeType::TextureData) { |
1053 | result.name = QStringLiteral("source" ); |
1054 | result.value = getIdForNode(node: *node->scene->root) + QLatin1Char('.') + getIdForNode(node: *node); |
1055 | } else { |
1056 | result.value = getIdForNode(node: *node); |
1057 | } |
1058 | result.ok = true; |
1059 | } |
1060 | } else if (value.metaType() == QMetaType::fromType<QSSGSceneDesc::Mesh *>()) { |
1061 | if (const auto meshNode = qvariant_cast<const QSSGSceneDesc::Mesh *>(v: value)) { |
1062 | Q_ASSERT(meshNode->nodeType == QSSGSceneDesc::Node::Type::Mesh); |
1063 | Q_ASSERT(meshNode->scene); |
1064 | const auto &scene = *meshNode->scene; |
1065 | const auto& [meshSourceName, notValidReason] = meshAssetName(scene, meshNode: *meshNode, outdir: output.outdir); |
1066 | result.notValidReason = notValidReason; |
1067 | if (!meshSourceName.isEmpty()) { |
1068 | result.value = toQuotedString(text: meshSourceName); |
1069 | result.ok = true; |
1070 | } |
1071 | } |
1072 | } else if (value.metaType() == QMetaType::fromType<QUrl>()) { |
1073 | if (const auto url = qvariant_cast<QUrl>(v: value); !url.isEmpty()) { |
1074 | // We need to adjust source url(s) as those should contain the canonical path |
1075 | QString path; |
1076 | if (QSSGRenderGraphObject::isTexture(type: target.runtimeType)) { |
1077 | const auto& [relpath, notValidReason] = copyTextureAsset(texturePath: url, output); |
1078 | result.notValidReason = notValidReason; |
1079 | if (!relpath.isEmpty()) { |
1080 | path = relpath; |
1081 | } |
1082 | } else |
1083 | path = url.path(); |
1084 | |
1085 | if (!path.isEmpty()) { |
1086 | result.value = toQuotedString(text: path); |
1087 | result.ok = true; |
1088 | } |
1089 | } |
1090 | } else if (target.runtimeType == QSSGSceneDesc::Material::RuntimeType::CustomMaterial) { |
1091 | // Workaround the TextureInput item that wraps textures for the Custom material. |
1092 | if (value.metaType().id() == qMetaTypeId<QSSGSceneDesc::Texture *>()) { |
1093 | if (const auto texture = qvariant_cast<QSSGSceneDesc::Texture *>(v: value)) { |
1094 | Q_ASSERT(QSSGRenderGraphObject::isTexture(texture->runtimeType)); |
1095 | result.value = QLatin1String("TextureInput { texture: " ) + |
1096 | getIdForNode(node: *texture) + QLatin1String(" }" ); |
1097 | result.ok = true; |
1098 | } |
1099 | } |
1100 | } else if (value.metaType() == QMetaType::fromType<QString>()) { |
1101 | // Plain strings in the scenedesc should map to QML string values |
1102 | result.value = toQuotedString(text: value.toString()); |
1103 | result.ok = true; |
1104 | } else { |
1105 | result.notValidReason = QStringLiteral("Unsupported value type: " ) + QString::fromUtf8(utf8: value.metaType().name()); |
1106 | qWarning() << result.notValidReason; |
1107 | result.ok = false; |
1108 | } |
1109 | |
1110 | if (result.ok && (output.options & OutputContext::Options::ExpandValueComponents)) { |
1111 | result.expandedProperties = ((output.options & OutputContext::Options::DesignStudioWorkarounds) == OutputContext::Options::DesignStudioWorkarounds) |
1112 | ? expandComponentsPartially(value: result.value, mt: value.metaType()) |
1113 | : expandComponents(value: result.value, mt: value.metaType()); |
1114 | } |
1115 | |
1116 | return result; |
1117 | } |
1118 | |
1119 | static void writeNodeProperties(const QSSGSceneDesc::Node &node, OutputContext &output) |
1120 | { |
1121 | QSSGQmlScopedIndent scopedIndent(output); |
1122 | |
1123 | indent(output) << u"id: "_s << getIdForNode(node) << u'\n'; |
1124 | |
1125 | // Set Object Name if one exists |
1126 | if (node.name.size()) { |
1127 | const QString objectName = QString::fromLocal8Bit(ba: node.name); |
1128 | if (!objectName.startsWith(c: u'*')) |
1129 | indent(output) << u"objectName: \""_s << node.name << u"\"\n"_s ; |
1130 | } |
1131 | |
1132 | const auto &properties = node.properties; |
1133 | auto it = properties.begin(); |
1134 | const auto end = properties.end(); |
1135 | for (; it != end; ++it) { |
1136 | const auto &property = *it; |
1137 | |
1138 | const ValueToQmlResult result = valueToQml(target: node, property: *property, output); |
1139 | if (result.ok) { |
1140 | if (result.isDynamicProperty) { |
1141 | indent(output) << "property " << typeName(mt: property->value.metaType()).toByteArray() << ' ' << result.name << u": "_s << result.value << u'\n'; |
1142 | } else if (!QSSGQmlUtilities::PropertyMap::instance()->isDefaultValue(type: node.runtimeType, property: property->name, value: property->value)) { |
1143 | if (result.expandedProperties.size() > 1) { |
1144 | for (const auto &va : result.expandedProperties) |
1145 | indent(output) << result.name << va << u'\n'; |
1146 | } else { |
1147 | indent(output) << result.name << u": "_s << result.value << u'\n'; |
1148 | } |
1149 | } |
1150 | } else if (!result.isDynamicProperty) { |
1151 | QString message = u"Skipped property: "_s + QString::fromUtf8(ba: property->name); |
1152 | if (!result.notValidReason.isEmpty()) |
1153 | message.append(s: u", reason: "_s + result.notValidReason); |
1154 | qDebug() << message; |
1155 | indent(output) << comment() << message + u'\n'; |
1156 | } |
1157 | } |
1158 | } |
1159 | |
1160 | static void writeQml(const QSSGSceneDesc::Node &transform, OutputContext &output) |
1161 | { |
1162 | using namespace QSSGSceneDesc; |
1163 | Q_ASSERT(transform.nodeType == QSSGSceneDesc::Node::Type::Transform && transform.runtimeType == QSSGSceneDesc::Node::RuntimeType::Node); |
1164 | indent(output) << qmlElementName<QSSGSceneDesc::Node::RuntimeType::Node>() << blockBegin(output); |
1165 | writeNodeProperties(node: transform, output); |
1166 | } |
1167 | |
1168 | void writeQml(const QSSGSceneDesc::Material &material, OutputContext &output) |
1169 | { |
1170 | using namespace QSSGSceneDesc; |
1171 | Q_ASSERT(material.nodeType == QSSGSceneDesc::Model::Type::Material); |
1172 | if (material.runtimeType == QSSGSceneDesc::Model::RuntimeType::SpecularGlossyMaterial) { |
1173 | indent(output) << qmlElementName<Material::RuntimeType::SpecularGlossyMaterial>() << blockBegin(output); |
1174 | } else if (material.runtimeType == Model::RuntimeType::PrincipledMaterial) { |
1175 | indent(output) << qmlElementName<Material::RuntimeType::PrincipledMaterial>() << blockBegin(output); |
1176 | } else if (material.runtimeType == Material::RuntimeType::CustomMaterial) { |
1177 | indent(output) << qmlElementName<Material::RuntimeType::CustomMaterial>() << blockBegin(output); |
1178 | } else if (material.runtimeType == Material::RuntimeType::SpecularGlossyMaterial) { |
1179 | indent(output) << qmlElementName<Material::RuntimeType::SpecularGlossyMaterial>() << blockBegin(output); |
1180 | } else { |
1181 | Q_UNREACHABLE(); |
1182 | } |
1183 | |
1184 | writeNodeProperties(node: material, output); |
1185 | } |
1186 | |
1187 | static void writeQml(const QSSGSceneDesc::Model &model, OutputContext &output) |
1188 | { |
1189 | using namespace QSSGSceneDesc; |
1190 | Q_ASSERT(model.nodeType == Node::Type::Model); |
1191 | indent(output) << qmlElementName<QSSGSceneDesc::Node::RuntimeType::Model>() << blockBegin(output); |
1192 | writeNodeProperties(node: model, output); |
1193 | } |
1194 | |
1195 | static void writeQml(const QSSGSceneDesc::Camera &camera, OutputContext &output) |
1196 | { |
1197 | using namespace QSSGSceneDesc; |
1198 | Q_ASSERT(camera.nodeType == Node::Type::Camera); |
1199 | if (camera.runtimeType == Camera::RuntimeType::PerspectiveCamera) |
1200 | indent(output) << qmlElementName<Camera::RuntimeType::PerspectiveCamera>() << blockBegin(output); |
1201 | else if (camera.runtimeType == Camera::RuntimeType::OrthographicCamera) |
1202 | indent(output) << qmlElementName<Camera::RuntimeType::OrthographicCamera>() << blockBegin(output); |
1203 | else |
1204 | Q_UNREACHABLE(); |
1205 | writeNodeProperties(node: camera, output); |
1206 | } |
1207 | |
1208 | static void writeQml(const QSSGSceneDesc::Texture &texture, OutputContext &output) |
1209 | { |
1210 | using namespace QSSGSceneDesc; |
1211 | Q_ASSERT(texture.nodeType == Node::Type::Texture && QSSGRenderGraphObject::isTexture(texture.runtimeType)); |
1212 | if (texture.runtimeType == Texture::RuntimeType::Image2D) |
1213 | indent(output) << qmlElementName<Texture::RuntimeType::Image2D>() << blockBegin(output); |
1214 | else if (texture.runtimeType == Texture::RuntimeType::ImageCube) |
1215 | indent(output) << qmlElementName<Texture::RuntimeType::ImageCube>() << blockBegin(output); |
1216 | writeNodeProperties(node: texture, output); |
1217 | } |
1218 | |
1219 | static void writeQml(const QSSGSceneDesc::Skin &skin, OutputContext &output) |
1220 | { |
1221 | using namespace QSSGSceneDesc; |
1222 | Q_ASSERT(skin.nodeType == Node::Type::Skin && skin.runtimeType == Node::RuntimeType::Skin); |
1223 | indent(output) << qmlElementName<Node::RuntimeType::Skin>() << blockBegin(output); |
1224 | writeNodeProperties(node: skin, output); |
1225 | } |
1226 | |
1227 | static void writeQml(const QSSGSceneDesc::MorphTarget &morphTarget, OutputContext &output) |
1228 | { |
1229 | using namespace QSSGSceneDesc; |
1230 | Q_ASSERT(morphTarget.nodeType == Node::Type::MorphTarget); |
1231 | indent(output) << qmlElementName<QSSGSceneDesc::Node::RuntimeType::MorphTarget>() << blockBegin(output); |
1232 | writeNodeProperties(node: morphTarget, output); |
1233 | } |
1234 | |
1235 | QString getTextureSourceName(const QString &name, const QString &fmt) |
1236 | { |
1237 | const auto textureFolder = getTextureFolder(); |
1238 | |
1239 | const auto sanitizedName = QSSGQmlUtilities::sanitizeQmlId(id: name); |
1240 | const auto ext = (fmt.length() != 3) ? u".png"_s |
1241 | : u"."_s + fmt; |
1242 | |
1243 | return QString(textureFolder + sanitizedName + ext); |
1244 | } |
1245 | |
1246 | static QString outputTextureAsset(const QSSGSceneDesc::TextureData &textureData, const QDir &outdir) |
1247 | { |
1248 | if (textureData.data.isEmpty()) |
1249 | return QString(); |
1250 | |
1251 | const auto mapsFolder = getTextureFolder(); |
1252 | const auto id = getIdForNode(node: textureData); |
1253 | const QString textureSourceName = getTextureSourceName(name: id, fmt: QString::fromUtf8(ba: textureData.fmt)); |
1254 | |
1255 | const bool isCompressed = ((textureData.flgs & quint8(QSSGSceneDesc::TextureData::Flags::Compressed)) != 0); |
1256 | |
1257 | // If a maps folder does not exist, then create one |
1258 | if (!outdir.exists(name: mapsFolder) && !outdir.mkdir(dirName: mapsFolder)) |
1259 | return QString(); // Error out |
1260 | |
1261 | const auto imagePath = QString(outdir.path() + QDir::separator() + textureSourceName); |
1262 | |
1263 | if (isCompressed) { |
1264 | QFile file(imagePath); |
1265 | file.open(flags: QIODevice::WriteOnly); |
1266 | file.write(data: textureData.data); |
1267 | file.close(); |
1268 | } else { |
1269 | const auto &texData = textureData.data; |
1270 | const auto &size = textureData.sz; |
1271 | QImage image; |
1272 | image = QImage(reinterpret_cast<const uchar *>(texData.data()), size.width(), size.height(), QImage::Format::Format_RGBA8888); |
1273 | if (!image.save(fileName: imagePath)) |
1274 | return QString(); |
1275 | } |
1276 | |
1277 | return textureSourceName; |
1278 | } |
1279 | |
1280 | static void writeQml(const QSSGSceneDesc::TextureData &textureData, OutputContext &output) |
1281 | { |
1282 | using namespace QSSGSceneDesc; |
1283 | Q_ASSERT(textureData.nodeType == Node::Type::Texture && textureData.runtimeType == Node::RuntimeType::TextureData); |
1284 | |
1285 | QString textureSourcePath = outputTextureAsset(textureData, outdir: output.outdir); |
1286 | |
1287 | static const auto writeProperty = [](const QString &type, const QString &name, const QString &value) { |
1288 | return QString::fromLatin1(ba: "property %1 %2: %3" ).arg(args: type, args: name, args: value); |
1289 | }; |
1290 | |
1291 | if (!textureSourcePath.isEmpty()) { |
1292 | const auto type = QLatin1String("url" ); |
1293 | const auto name = getIdForNode(node: textureData); |
1294 | |
1295 | indent(output) << writeProperty(type, name, toQuotedString(text: textureSourcePath)) << '\n'; |
1296 | } |
1297 | } |
1298 | |
1299 | static void writeQml(const QSSGSceneDesc::Light &light, OutputContext &output) |
1300 | { |
1301 | using namespace QSSGSceneDesc; |
1302 | Q_ASSERT(light.nodeType == Node::Type::Light); |
1303 | if (light.runtimeType == Light::RuntimeType::DirectionalLight) |
1304 | indent(output) << qmlElementName<Light::RuntimeType::DirectionalLight>() << blockBegin(output); |
1305 | else if (light.runtimeType == Light::RuntimeType::SpotLight) |
1306 | indent(output) << qmlElementName<Light::RuntimeType::SpotLight>() << blockBegin(output); |
1307 | else if (light.runtimeType == Light::RuntimeType::PointLight) |
1308 | indent(output) << qmlElementName<Light::RuntimeType::PointLight>() << blockBegin(output); |
1309 | else |
1310 | Q_UNREACHABLE(); |
1311 | writeNodeProperties(node: light, output); |
1312 | } |
1313 | |
1314 | static void writeQml(const QSSGSceneDesc::Skeleton &skeleton, OutputContext &output) |
1315 | { |
1316 | using namespace QSSGSceneDesc; |
1317 | Q_ASSERT(skeleton.nodeType == Node::Type::Skeleton && skeleton.runtimeType == Node::RuntimeType::Skeleton); |
1318 | indent(output) << qmlElementName<Node::RuntimeType::Skeleton>() << blockBegin(output); |
1319 | writeNodeProperties(node: skeleton, output); |
1320 | } |
1321 | |
1322 | static void writeQml(const QSSGSceneDesc::Joint &joint, OutputContext &output) |
1323 | { |
1324 | using namespace QSSGSceneDesc; |
1325 | Q_ASSERT(joint.nodeType == Node::Type::Joint && joint.runtimeType == Node::RuntimeType::Joint); |
1326 | indent(output) << qmlElementName<Node::RuntimeType::Joint>() << blockBegin(output); |
1327 | writeNodeProperties(node: joint, output); |
1328 | } |
1329 | |
1330 | static void writeQmlForResourceNode(const QSSGSceneDesc::Node &node, OutputContext &output) |
1331 | { |
1332 | using namespace QSSGSceneDesc; |
1333 | Q_ASSERT(output.type == OutputContext::Resource); |
1334 | Q_ASSERT(QSSGRenderGraphObject::isResource(node.runtimeType) || node.nodeType == Node::Type::Mesh || node.nodeType == Node::Type::Skeleton); |
1335 | |
1336 | const bool processNode = !node.properties.isEmpty() || (output.type == OutputContext::Resource); |
1337 | if (processNode) { |
1338 | QSSGQmlScopedIndent scopedIndent(output); |
1339 | switch (node.nodeType) { |
1340 | case Node::Type::Skin: |
1341 | writeQml(skin: static_cast<const Skin &>(node), output); |
1342 | break; |
1343 | case Node::Type::MorphTarget: |
1344 | writeQml(morphTarget: static_cast<const MorphTarget &>(node), output); |
1345 | break; |
1346 | case Node::Type::Skeleton: |
1347 | writeQml(skeleton: static_cast<const Skeleton &>(node), output); |
1348 | break; |
1349 | case Node::Type::Texture: |
1350 | if (node.runtimeType == Node::RuntimeType::Image2D) |
1351 | writeQml(texture: static_cast<const Texture &>(node), output); |
1352 | else if (node.runtimeType == Node::RuntimeType::ImageCube) |
1353 | writeQml(texture: static_cast<const Texture &>(node), output); |
1354 | else if (node.runtimeType == Node::RuntimeType::TextureData) |
1355 | writeQml(textureData: static_cast<const TextureData &>(node), output); |
1356 | else |
1357 | Q_UNREACHABLE(); |
1358 | break; |
1359 | case Node::Type::Material: |
1360 | writeQml(material: static_cast<const Material &>(node), output); |
1361 | break; |
1362 | case Node::Type::Mesh: |
1363 | // Only handled as a property (see: valueToQml()) |
1364 | break; |
1365 | default: |
1366 | qWarning(msg: "Unhandled resource type \'%d\'?" , int(node.runtimeType)); |
1367 | break; |
1368 | } |
1369 | } |
1370 | |
1371 | // Do something more convenient if this starts expending to more types... |
1372 | // NOTE: The TextureData type is written out as a url property... |
1373 | const bool skipBlockEnd = (node.runtimeType == Node::RuntimeType::TextureData || node.nodeType == Node::Type::Mesh); |
1374 | if (!skipBlockEnd && processNode && output.scopeDepth != 0) { |
1375 | QSSGQmlScopedIndent scopedIndent(output); |
1376 | indent(output) << blockEnd(output); |
1377 | } |
1378 | } |
1379 | |
1380 | static void writeQmlForNode(const QSSGSceneDesc::Node &node, OutputContext &output) |
1381 | { |
1382 | using namespace QSSGSceneDesc; |
1383 | |
1384 | const bool processNode = !(node.properties.isEmpty() && node.children.isEmpty()) |
1385 | || (output.type == OutputContext::Resource); |
1386 | if (processNode) { |
1387 | QSSGQmlScopedIndent scopedIndent(output); |
1388 | switch (node.nodeType) { |
1389 | case Node::Type::Skeleton: |
1390 | writeQml(skeleton: static_cast<const Skeleton &>(node), output); |
1391 | break; |
1392 | case Node::Type::Joint: |
1393 | writeQml(joint: static_cast<const Joint &>(node), output); |
1394 | break; |
1395 | case Node::Type::Light: |
1396 | writeQml(light: static_cast<const Light &>(node), output); |
1397 | break; |
1398 | case Node::Type::Transform: |
1399 | writeQml(transform: node, output); |
1400 | break; |
1401 | case Node::Type::Camera: |
1402 | writeQml(camera: static_cast<const Camera &>(node), output); |
1403 | break; |
1404 | case Node::Type::Model: |
1405 | writeQml(model: static_cast<const Model &>(node), output); |
1406 | break; |
1407 | default: |
1408 | break; |
1409 | } |
1410 | } |
1411 | |
1412 | for (const auto &cld : node.children) { |
1413 | if (!QSSGRenderGraphObject::isResource(type: cld->runtimeType) && output.type == OutputContext::NodeTree) { |
1414 | QSSGQmlScopedIndent scopedIndent(output); |
1415 | writeQmlForNode(node: *cld, output); |
1416 | } |
1417 | } |
1418 | |
1419 | // Do something more convenient if this starts expending to more types... |
1420 | // NOTE: The TextureData type is written out as a url property... |
1421 | const bool skipBlockEnd = (node.runtimeType == Node::RuntimeType::TextureData || node.nodeType == Node::Type::Mesh); |
1422 | if (!skipBlockEnd && processNode && output.scopeDepth != 0) { |
1423 | QSSGQmlScopedIndent scopedIndent(output); |
1424 | indent(output) << blockEnd(output); |
1425 | } |
1426 | } |
1427 | |
1428 | void writeQmlForResources(const QSSGSceneDesc::Scene::ResourceNodes &resources, OutputContext &output) |
1429 | { |
1430 | auto sortedResources = resources; |
1431 | std::sort(first: sortedResources.begin(), last: sortedResources.end(), comp: [](const QSSGSceneDesc::Node *a, const QSSGSceneDesc::Node *b) { |
1432 | using RType = QSSGSceneDesc::Node::RuntimeType; |
1433 | if (a->runtimeType == RType::TextureData && b->runtimeType != RType::TextureData) |
1434 | return true; |
1435 | if (a->runtimeType == RType::ImageCube && (b->runtimeType != RType::TextureData && b->runtimeType != RType::ImageCube)) |
1436 | return true; |
1437 | if (a->runtimeType == RType::Image2D && (b->runtimeType != RType::TextureData && b->runtimeType != RType::Image2D)) |
1438 | return true; |
1439 | |
1440 | return false; |
1441 | }); |
1442 | for (const auto &res : std::as_const(t&: sortedResources)) |
1443 | writeQmlForResourceNode(node: *res, output); |
1444 | } |
1445 | |
1446 | static void generateKeyframeData(const QSSGSceneDesc::Animation::Channel &channel, QByteArray &keyframeData) |
1447 | { |
1448 | #ifdef QT_QUICK3D_ENABLE_RT_ANIMATIONS |
1449 | QCborStreamWriter writer(&keyframeData); |
1450 | // Start root array |
1451 | writer.startArray(); |
1452 | // header name |
1453 | writer.append(str: "QTimelineKeyframes" ); |
1454 | // file version. Increase this if the format changes. |
1455 | const int keyframesDataVersion = 1; |
1456 | writer.append(i: keyframesDataVersion); |
1457 | writer.append(i: int(channel.keys.at(i: 0)->getValueQMetaType())); |
1458 | |
1459 | // Start Keyframes array |
1460 | writer.startArray(); |
1461 | quint8 compEnd = quint8(channel.keys.at(i: 0)->getValueType()); |
1462 | bool isQuaternion = false; |
1463 | if (compEnd == quint8(QSSGSceneDesc::Animation::KeyPosition::ValueType::Quaternion)) { |
1464 | isQuaternion = true; |
1465 | compEnd = 3; |
1466 | } else { |
1467 | compEnd++; |
1468 | } |
1469 | for (const auto &key : channel.keys) { |
1470 | writer.append(f: key->time); |
1471 | // Easing always linear |
1472 | writer.append(i: QEasingCurve::Linear); |
1473 | if (isQuaternion) |
1474 | writer.append(f: key->value[3]); |
1475 | for (quint8 i = 0; i < compEnd; ++i) |
1476 | writer.append(f: key->value[i]); |
1477 | } |
1478 | // End Keyframes array |
1479 | writer.endArray(); |
1480 | // End root array |
1481 | writer.endArray(); |
1482 | #else |
1483 | Q_UNUSED(channel) |
1484 | Q_UNUSED(keyframeData) |
1485 | #endif // QT_QUICK3D_ENABLE_RT_ANIMATIONS |
1486 | } |
1487 | |
1488 | QPair<QString, QString> writeQmlForAnimation(const QSSGSceneDesc::Animation &anim, qsizetype index, OutputContext &output, bool useBinaryKeyframes = true, bool generateTimelineAnimations = true) |
1489 | { |
1490 | indent(output) << "Timeline {\n" ; |
1491 | |
1492 | QSSGQmlScopedIndent scopedIndent(output); |
1493 | // The duration property of the TimelineAnimation is an int... |
1494 | const int duration = qCeil(v: anim.length); |
1495 | // Use the same name for objectName and id |
1496 | const QString animationId = getIdForAnimation(inName: anim.name); |
1497 | indent(output) << "id: " << animationId << "\n" ; |
1498 | QString animationName = animationId; |
1499 | if (!anim.name.isEmpty()) |
1500 | animationName = QString::fromLocal8Bit(ba: anim.name); |
1501 | indent(output) << "objectName: \"" << animationName << "\"\n" ; |
1502 | indent(output) << "property real framesPerSecond: " << anim.framesPerSecond << "\n" ; |
1503 | indent(output) << "startFrame: 0\n" ; |
1504 | indent(output) << "endFrame: " << duration << "\n" ; |
1505 | indent(output) << "currentFrame: 0\n" ; |
1506 | // Only generate the TimelineAnimation component here if requested |
1507 | // enabled is only set to true up front if we expect to autoplay |
1508 | // the generated TimelineAnimation |
1509 | if (generateTimelineAnimations) { |
1510 | indent(output) << "enabled: true\n" ; |
1511 | indent(output) << "animations: TimelineAnimation {\n" ; |
1512 | { |
1513 | QSSGQmlScopedIndent scopedIndent(output); |
1514 | indent(output) << "duration: " << duration << "\n" ; |
1515 | indent(output) << "from: 0\n" ; |
1516 | indent(output) << "to: " << duration << "\n" ; |
1517 | indent(output) << "running: true\n" ; |
1518 | indent(output) << "loops: Animation.Infinite\n" ; |
1519 | } |
1520 | indent(output) << blockEnd(output); |
1521 | } |
1522 | |
1523 | for (const auto &channel : anim.channels) { |
1524 | QString id = getIdForNode(node: *channel->target); |
1525 | QString propertyName = asString(prop: channel->targetProperty); |
1526 | |
1527 | indent(output) << "KeyframeGroup {\n" ; |
1528 | { |
1529 | QSSGQmlScopedIndent scopedIndent(output); |
1530 | indent(output) << "target: " << id << "\n" ; |
1531 | indent(output) << "property: " << toQuotedString(text: propertyName) << "\n" ; |
1532 | if (useBinaryKeyframes && channel->keys.size() != 1) { |
1533 | const auto animFolder = getAnimationFolder(); |
1534 | const auto animSourceName = getAnimationSourceName(id, property: propertyName, index); |
1535 | if (!output.outdir.exists(name: animFolder) && !output.outdir.mkdir(dirName: animFolder)) { |
1536 | // Make a warning |
1537 | continue; |
1538 | } |
1539 | QFile file(output.outdir.path() + QDir::separator() + animSourceName); |
1540 | if (!file.open(flags: QIODevice::WriteOnly)) |
1541 | continue; |
1542 | QByteArray keyframeData; |
1543 | // It is possible to store this keyframeData but we have to consider |
1544 | // all the cases including runtime only or writeQml only. |
1545 | // For now, we will generate it for each case. |
1546 | generateKeyframeData(channel: *channel, keyframeData); |
1547 | file.write(data: keyframeData); |
1548 | file.close(); |
1549 | indent(output) << "keyframeSource: " << toQuotedString(text: animSourceName) << "\n" ; |
1550 | } else { |
1551 | Q_ASSERT(!channel->keys.isEmpty()); |
1552 | for (const auto &key : channel->keys) { |
1553 | indent(output) << "Keyframe {\n" ; |
1554 | { |
1555 | QSSGQmlScopedIndent scopedIndent(output); |
1556 | indent(output) << "frame: " << key->time << "\n" ; |
1557 | indent(output) << "value: " << variantToQml(variant: key->getValue()) << "\n" ; |
1558 | } |
1559 | indent(output) << blockEnd(output); |
1560 | } |
1561 | } |
1562 | } |
1563 | indent(output) << blockEnd(output); |
1564 | } |
1565 | return {animationName, animationId}; |
1566 | } |
1567 | |
1568 | void writeQml(const QSSGSceneDesc::Scene &scene, QTextStream &stream, const QDir &outdir, const QJsonObject &optionsObject) |
1569 | { |
1570 | static const auto checkBooleanOption = [](const QLatin1String &optionName, const QJsonObject &options, bool defaultValue = false) { |
1571 | const auto it = options.constFind(key: optionName); |
1572 | const auto end = options.constEnd(); |
1573 | QJsonValue value; |
1574 | if (it != end) { |
1575 | if (it->isObject()) |
1576 | value = it->toObject().value(key: QLatin1String("value" )); |
1577 | else |
1578 | value = it.value(); |
1579 | } |
1580 | return value.toBool(defaultValue); |
1581 | }; |
1582 | |
1583 | auto root = scene.root; |
1584 | Q_ASSERT(root); |
1585 | |
1586 | QJsonObject options = optionsObject; |
1587 | |
1588 | if (auto it = options.constFind(key: QLatin1String("options" )), end = options.constEnd(); it != end) |
1589 | options = it->toObject(); |
1590 | |
1591 | quint8 outputOptions{ OutputContext::Options::None }; |
1592 | if (checkBooleanOption(QLatin1String("expandValueComponents" ), options)) |
1593 | outputOptions |= OutputContext::Options::ExpandValueComponents; |
1594 | |
1595 | // Workaround for design studio type components |
1596 | if (checkBooleanOption(QLatin1String("designStudioWorkarounds" ), options)) |
1597 | outputOptions |= OutputContext::Options::DesignStudioWorkarounds; |
1598 | |
1599 | const bool useBinaryKeyframes = checkBooleanOption("useBinaryKeyframes"_L1 , options); |
1600 | const bool generateTimelineAnimations = !checkBooleanOption("manualAnimations"_L1 , options); |
1601 | |
1602 | OutputContext output { .stream: stream, .outdir: outdir, .sourceDir: scene.sourceDir, .indent: 0, .type: OutputContext::Header, .options: outputOptions }; |
1603 | |
1604 | writeImportHeader(output, hasAnimation: scene.animations.count() > 0); |
1605 | |
1606 | output.type = OutputContext::RootNode; |
1607 | writeQml(transform: *root, output); // Block scope will be left open! |
1608 | stream << "\n" ; |
1609 | stream << indent() << "// Resources\n" ; |
1610 | output.type = OutputContext::Resource; |
1611 | writeQmlForResources(resources: scene.resources, output); |
1612 | output.type = OutputContext::NodeTree; |
1613 | stream << "\n" ; |
1614 | stream << indent() << "// Nodes:\n" ; |
1615 | for (const auto &cld : root->children) |
1616 | writeQmlForNode(node: *cld, output); |
1617 | |
1618 | // animations |
1619 | qsizetype animId = 0; |
1620 | stream << "\n" ; |
1621 | stream << indent() << "// Animations:\n" ; |
1622 | QList<QPair<QString, QString>> animationMap; |
1623 | for (const auto &cld : scene.animations) { |
1624 | QSSGQmlScopedIndent scopedIndent(output); |
1625 | auto mapValues = writeQmlForAnimation(anim: *cld, index: animId++, output, useBinaryKeyframes, generateTimelineAnimations); |
1626 | animationMap.append(t: mapValues); |
1627 | indent(output) << blockEnd(output); |
1628 | } |
1629 | |
1630 | if (!generateTimelineAnimations) { |
1631 | // Expose a map of timelines |
1632 | stream << "\n" ; |
1633 | stream << indent() << "// An exported mapping of Timelines (--manualAnimations)\n" ; |
1634 | stream << indent() << "property var timelineMap: {\n" ; |
1635 | QSSGQmlScopedIndent scopedIndent(output); |
1636 | for (const auto &mapValues : animationMap) { |
1637 | QSSGQmlScopedIndent scopedIndent(output); |
1638 | indent(output) << "\"" << mapValues.first << "\": " << mapValues.second << ",\n" ; |
1639 | } |
1640 | indent(output) << blockEnd(output); |
1641 | stream << indent() << "// A simple list of Timelines (--manualAnimations)\n" ; |
1642 | stream << indent() << "property var timelineList: [\n" ; |
1643 | for (const auto &mapValues : animationMap) { |
1644 | QSSGQmlScopedIndent scopedIndent(output); |
1645 | indent(output) << mapValues.second << ",\n" ; |
1646 | } |
1647 | indent(output) << "]\n" ; |
1648 | } |
1649 | |
1650 | |
1651 | // close the root |
1652 | indent(output) << blockEnd(output); |
1653 | } |
1654 | |
1655 | void createTimelineAnimation(const QSSGSceneDesc::Animation &anim, QObject *parent, bool isEnabled, bool useBinaryKeyframes) |
1656 | { |
1657 | #ifdef QT_QUICK3D_ENABLE_RT_ANIMATIONS |
1658 | auto timeline = new QQuickTimeline(parent); |
1659 | auto timelineKeyframeGroup = timeline->keyframeGroups(); |
1660 | for (const auto &channel : anim.channels) { |
1661 | auto keyframeGroup = new QQuickKeyframeGroup(timeline); |
1662 | keyframeGroup->setTargetObject(channel->target->obj); |
1663 | keyframeGroup->setProperty(asString(prop: channel->targetProperty)); |
1664 | |
1665 | Q_ASSERT(!channel->keys.isEmpty()); |
1666 | if (useBinaryKeyframes) { |
1667 | QByteArray keyframeData; |
1668 | generateKeyframeData(channel: *channel, keyframeData); |
1669 | |
1670 | keyframeGroup->setKeyframeData(keyframeData); |
1671 | } else { |
1672 | auto keyframes = keyframeGroup->keyframes(); |
1673 | for (const auto &key : channel->keys) { |
1674 | auto keyframe = new QQuickKeyframe(keyframeGroup); |
1675 | keyframe->setFrame(key->time); |
1676 | keyframe->setValue(key->getValue()); |
1677 | keyframes.append(&keyframes, keyframe); |
1678 | } |
1679 | } |
1680 | (qobject_cast<QQmlParserStatus *>(object: keyframeGroup))->componentComplete(); |
1681 | timelineKeyframeGroup.append(&timelineKeyframeGroup, keyframeGroup); |
1682 | } |
1683 | timeline->setEndFrame(anim.length); |
1684 | timeline->setEnabled(isEnabled); |
1685 | |
1686 | auto timelineAnimation = new QQuickTimelineAnimation(timeline); |
1687 | timelineAnimation->setObjectName(anim.name); |
1688 | timelineAnimation->setDuration(int(anim.length)); |
1689 | timelineAnimation->setFrom(0.0f); |
1690 | timelineAnimation->setTo(anim.length); |
1691 | timelineAnimation->setLoops(QQuickTimelineAnimation::Infinite); |
1692 | timelineAnimation->setTargetObject(timeline); |
1693 | |
1694 | (qobject_cast<QQmlParserStatus *>(object: timeline))->componentComplete(); |
1695 | |
1696 | timelineAnimation->setRunning(true); |
1697 | #else // QT_QUICK3D_ENABLE_RT_ANIMATIONS |
1698 | Q_UNUSED(anim) |
1699 | Q_UNUSED(parent) |
1700 | Q_UNUSED(isEnabled) |
1701 | Q_UNUSED(useBinaryKeyframes) |
1702 | #endif // QT_QUICK3D_ENABLE_RT_ANIMATIONS |
1703 | } |
1704 | |
1705 | void writeQmlComponent(const QSSGSceneDesc::Node &node, QTextStream &stream, const QDir &outDir) |
1706 | { |
1707 | using namespace QSSGSceneDesc; |
1708 | |
1709 | QSSG_ASSERT(node.scene != nullptr, return); |
1710 | |
1711 | if (node.runtimeType == Material::RuntimeType::CustomMaterial) { |
1712 | QString sourceDir = node.scene->sourceDir; |
1713 | OutputContext output { .stream: stream, .outdir: outDir, .sourceDir: sourceDir, .indent: 0, .type: OutputContext::Resource }; |
1714 | writeImportHeader(output); |
1715 | writeQml(material: static_cast<const Material &>(node), output); |
1716 | // Resources, if any, are written out as properties on the component |
1717 | const auto &resources = node.scene->resources; |
1718 | writeQmlForResources(resources, output); |
1719 | indent(output) << blockEnd(output); |
1720 | } else { |
1721 | Q_UNREACHABLE(); // Only implemented for Custom material at this point. |
1722 | } |
1723 | } |
1724 | |
1725 | } |
1726 | |
1727 | QT_END_NAMESPACE |
1728 | |