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 | do { |
616 | if (const auto it = g_idMap->constFind(key: sanitizedName); it == g_idMap->constEnd()) { |
617 | g_idMap->insert(key: sanitizedName, value: &node); |
618 | g_nodeNameMap->insert(key: &node, value: sanitizedName); |
619 | return sanitizedName; |
620 | } |
621 | |
622 | sanitizedName = QStringLiteral("%1%2" ).arg(a: sanitizedName).arg(a: id++); |
623 | } while (--attempts); |
624 | |
625 | return sanitizedName; |
626 | } |
627 | |
628 | static QString getIdForAnimation(const QByteArray &inName) |
629 | { |
630 | QString name = !inName.isEmpty() ? QString::fromUtf8(ba: inName + "_timeline" ) : "timeline0"_L1 ; |
631 | QString sanitizedName = QSSGQmlUtilities::sanitizeQmlId(id: name); |
632 | |
633 | int attempts = 1000; |
634 | quint16 id = 0; |
635 | do { |
636 | if (const auto it = g_idMap->constFind(key: sanitizedName); it == g_idMap->constEnd()) { |
637 | if (const auto oIt = g_idOthers->constFind(value: sanitizedName); oIt == g_idOthers->constEnd()) { |
638 | g_idOthers->insert(value: sanitizedName); |
639 | return sanitizedName; |
640 | } |
641 | } |
642 | |
643 | sanitizedName = QStringLiteral("%1%2" ).arg(a: sanitizedName).arg(a: ++id); |
644 | } while (--attempts); |
645 | |
646 | return sanitizedName; |
647 | } |
648 | |
649 | QString stripParentDirectory(const QString &filePath) { |
650 | QString sourceCopy = filePath; |
651 | while (sourceCopy.startsWith(c: QChar::fromLatin1(c: '.')) || sourceCopy.startsWith(c: QChar::fromLatin1(c: '/')) || sourceCopy.startsWith(c: QChar::fromLatin1(c: '\\'))) |
652 | sourceCopy.remove(i: 0, len: 1); |
653 | return sourceCopy; |
654 | } |
655 | |
656 | static const char *blockBegin() { return " {\n" ; } |
657 | static const char *blockEnd() { return "}\n" ; } |
658 | static const char *() { return "// " ; } |
659 | static const char *indent() { return " " ; } |
660 | |
661 | struct QSSGQmlScopedIndent |
662 | { |
663 | enum : quint8 { QSSG_INDENT = 4 }; |
664 | explicit QSSGQmlScopedIndent(OutputContext &out) : output(out) { out.indent += QSSG_INDENT; }; |
665 | ~QSSGQmlScopedIndent() { output.indent = qMax(a: output.indent - QSSG_INDENT, b: 0); } |
666 | OutputContext &output; |
667 | }; |
668 | |
669 | static QString indentString(OutputContext &output) |
670 | { |
671 | QString str; |
672 | for (quint8 i = 0; i < output.indent; i += QSSGQmlScopedIndent::QSSG_INDENT) |
673 | str += QString::fromLatin1(ba: indent()); |
674 | return str; |
675 | } |
676 | |
677 | static QTextStream &indent(OutputContext &output) |
678 | { |
679 | for (quint8 i = 0; i < output.indent; i += QSSGQmlScopedIndent::QSSG_INDENT) |
680 | output.stream << indent(); |
681 | return output.stream; |
682 | } |
683 | |
684 | static const char *blockBegin(OutputContext &output) |
685 | { |
686 | ++output.scopeDepth; |
687 | return blockBegin(); |
688 | } |
689 | |
690 | static const char *blockEnd(OutputContext &output) |
691 | { |
692 | output.scopeDepth = qMax(a: 0, b: output.scopeDepth - 1); |
693 | return blockEnd(); |
694 | } |
695 | |
696 | static void (OutputContext &output, bool hasAnimation = false) |
697 | { |
698 | output.stream << "import QtQuick\n" |
699 | << "import QtQuick3D\n\n" ; |
700 | if (hasAnimation) |
701 | output.stream << "import QtQuick.Timeline\n\n" ; |
702 | } |
703 | |
704 | static QString toQuotedString(const QString &text) { return QStringLiteral("\"%1\"" ).arg(a: text); } |
705 | |
706 | static inline QString getMeshFolder() { return QStringLiteral("meshes/" ); } |
707 | static inline QString getMeshExtension() { return QStringLiteral(".mesh" ); } |
708 | |
709 | QString getMeshSourceName(const QString &name) |
710 | { |
711 | const auto meshFolder = getMeshFolder(); |
712 | const auto extension = getMeshExtension(); |
713 | |
714 | return QString(meshFolder + name + extension); |
715 | } |
716 | |
717 | static inline QString getTextureFolder() { return QStringLiteral("maps/" ); } |
718 | |
719 | static inline QString getAnimationFolder() { return QStringLiteral("animations/" ); } |
720 | static inline QString getAnimationExtension() { return QStringLiteral(".qad" ); } |
721 | QString getAnimationSourceName(const QString &id, const QString &property, qsizetype index) |
722 | { |
723 | const auto animationFolder = getAnimationFolder(); |
724 | const auto extension = getAnimationExtension(); |
725 | return QString(animationFolder + id + QStringLiteral("_" ) |
726 | + property + QStringLiteral("_" ) |
727 | + QString::number(index) + extension); |
728 | } |
729 | |
730 | QString asString(const QVariant &var) |
731 | { |
732 | return var.toString(); |
733 | } |
734 | |
735 | QString builtinQmlType(const QVariant &var) |
736 | { |
737 | switch (var.metaType().id()) { |
738 | case QMetaType::QVector2D: { |
739 | const auto vec2 = qvariant_cast<QVector2D>(v: var); |
740 | return QLatin1String("Qt.vector2d(" ) + QString::number(vec2.x()) + QLatin1String(", " ) + QString::number(vec2.y()) + QLatin1Char(')'); |
741 | } |
742 | case QMetaType::QVector3D: { |
743 | const auto vec3 = qvariant_cast<QVector3D>(v: var); |
744 | return QLatin1String("Qt.vector3d(" ) + QString::number(vec3.x()) + QLatin1String(", " ) |
745 | + QString::number(vec3.y()) + QLatin1String(", " ) |
746 | + QString::number(vec3.z()) + QLatin1Char(')'); |
747 | } |
748 | case QMetaType::QVector4D: { |
749 | const auto vec4 = qvariant_cast<QVector4D>(v: var); |
750 | return QLatin1String("Qt.vector4d(" ) + QString::number(vec4.x()) + QLatin1String(", " ) |
751 | + QString::number(vec4.y()) + QLatin1String(", " ) |
752 | + QString::number(vec4.z()) + QLatin1String(", " ) |
753 | + QString::number(vec4.w()) + QLatin1Char(')'); |
754 | } |
755 | case QMetaType::QColor: { |
756 | const auto color = qvariant_cast<QColor>(v: var); |
757 | return colorToQml(color); |
758 | } |
759 | case QMetaType::QQuaternion: { |
760 | const auto &quat = qvariant_cast<QQuaternion>(v: var); |
761 | return QLatin1String("Qt.quaternion(" ) + QString::number(quat.scalar()) + QLatin1String(", " ) |
762 | + QString::number(quat.x()) + QLatin1String(", " ) |
763 | + QString::number(quat.y()) + QLatin1String(", " ) |
764 | + QString::number(quat.z()) + QLatin1Char(')'); |
765 | } |
766 | case QMetaType::QMatrix4x4: { |
767 | const auto mat44 = qvariant_cast<QMatrix4x4>(v: var); |
768 | return QLatin1String("Qt.matrix4x4(" ) |
769 | + QString::number(mat44(0, 0)) + u", " + QString::number(mat44(0, 1)) + u", " + QString::number(mat44(0, 2)) + u", " + QString::number(mat44(0, 3)) + u", " |
770 | + QString::number(mat44(1, 0)) + u", " + QString::number(mat44(1, 1)) + u", " + QString::number(mat44(1, 2)) + u", " + QString::number(mat44(1, 3)) + u", " |
771 | + QString::number(mat44(2, 0)) + u", " + QString::number(mat44(2, 1)) + u", " + QString::number(mat44(2, 2)) + u", " + QString::number(mat44(2, 3)) + u", " |
772 | + QString::number(mat44(3, 0)) + u", " + QString::number(mat44(3, 1)) + u", " + QString::number(mat44(3, 2)) + u", " + QString::number(mat44(3, 3)) + u')'; |
773 | } |
774 | case QMetaType::Float: |
775 | case QMetaType::Double: |
776 | case QMetaType::Int: |
777 | case QMetaType::Char: |
778 | case QMetaType::Long: |
779 | case QMetaType::LongLong: |
780 | case QMetaType::ULong: |
781 | case QMetaType::ULongLong: |
782 | case QMetaType::Bool: |
783 | return var.toString(); |
784 | case QMetaType::QUrl: // QUrl needs special handling. Return empty string to trigger that. |
785 | default: |
786 | break; |
787 | } |
788 | |
789 | return QString(); |
790 | } |
791 | |
792 | QString asString(QSSGSceneDesc::Animation::Channel::TargetProperty prop) |
793 | { |
794 | if (prop == QSSGSceneDesc::Animation::Channel::TargetProperty::Position) |
795 | return QStringLiteral("position" ); |
796 | if (prop == QSSGSceneDesc::Animation::Channel::TargetProperty::Rotation) |
797 | return QStringLiteral("rotation" ); |
798 | if (prop == QSSGSceneDesc::Animation::Channel::TargetProperty::Scale) |
799 | return QStringLiteral("scale" ); |
800 | if (prop == QSSGSceneDesc::Animation::Channel::TargetProperty::Weight) |
801 | return QStringLiteral("weight" ); |
802 | |
803 | return QStringLiteral("unknown" ); |
804 | } |
805 | |
806 | static std::pair<QString, QString> meshAssetName(const QSSGSceneDesc::Scene &scene, const QSSGSceneDesc::Mesh &meshNode, const QDir &outdir) |
807 | { |
808 | // Returns {name, notValidReason} |
809 | |
810 | const auto meshFolder = getMeshFolder(); |
811 | const auto meshId = QSSGQmlUtilities::getIdForNode(node: meshNode); |
812 | const auto meshSourceName = QSSGQmlUtilities::getMeshSourceName(name: meshId); |
813 | Q_ASSERT(scene.meshStorage.size() > meshNode.idx); |
814 | const auto &mesh = scene.meshStorage.at(i: meshNode.idx); |
815 | |
816 | // If a mesh folder does not exist, then create one |
817 | if (!outdir.exists(name: meshFolder) && !outdir.mkdir(dirName: meshFolder)) { |
818 | qDebug() << "Failed to create meshes folder at" << outdir; |
819 | return {}; // Error out |
820 | } |
821 | |
822 | const QString path = outdir.path() + QDir::separator() + meshSourceName; |
823 | QFile file(path); |
824 | if (!file.open(flags: QIODevice::WriteOnly)) { |
825 | return {QString(), QStringLiteral("Failed to find mesh at " ) + path}; |
826 | } |
827 | |
828 | if (mesh.save(device: &file) == 0) { |
829 | return {}; |
830 | } |
831 | |
832 | return {meshSourceName, QString()}; |
833 | }; |
834 | |
835 | static std::pair<QString, QString> copyTextureAsset(const QUrl &texturePath, OutputContext &output) |
836 | { |
837 | // Returns {path, notValidReason} |
838 | |
839 | // TODO: Use QUrl::resolved() instead of manual string manipulation |
840 | QString assetPath = output.outdir.isAbsolutePath(path: texturePath.path()) ? texturePath.toString() : texturePath.path(); |
841 | QFileInfo fi(assetPath); |
842 | if (fi.isRelative() && !output.sourceDir.isEmpty()) { |
843 | fi = QFileInfo(output.sourceDir + QChar(u'/') + assetPath); |
844 | } |
845 | if (!fi.exists()) { |
846 | indent(output) << comment() << "Source texture path expected: " << getTextureFolder() + texturePath.fileName() << "\n" ; |
847 | return {QString(), QStringLiteral("Failed to find texture at " ) + assetPath}; |
848 | } |
849 | |
850 | const auto mapsFolder = getTextureFolder(); |
851 | // If a maps folder does not exist, then create one |
852 | if (!output.outdir.exists(name: mapsFolder) && !output.outdir.mkdir(dirName: mapsFolder)) { |
853 | qDebug() << "Failed to create maps folder at" << output.outdir; |
854 | return {}; // Error out |
855 | } |
856 | |
857 | const QString relpath = mapsFolder + fi.fileName(); |
858 | const auto newfilepath = QString(output.outdir.canonicalPath() + QDir::separator() + relpath); |
859 | if (!QFile::exists(fileName: newfilepath) && !QFile::copy(fileName: fi.canonicalFilePath(), newName: newfilepath)) { |
860 | qDebug() << "Failed to copy file from" << fi.canonicalFilePath() << "to" << newfilepath; |
861 | return {}; |
862 | } |
863 | |
864 | return {relpath, QString()}; |
865 | }; |
866 | |
867 | static QStringList expandComponents(const QString &value, QMetaType mt) |
868 | { |
869 | static const QRegularExpression re(QLatin1String("^Qt.[a-z0-9]*\\(([0-9.e\\+\\-, ]*)\\)" )); |
870 | Q_ASSERT(re.isValid()); |
871 | |
872 | switch (mt.id()) { |
873 | case QMetaType::QVector2D: { |
874 | QRegularExpressionMatch match = re.match(subject: value); |
875 | if (match.hasMatch()) { |
876 | const auto comp = match.captured(nth: 1).split(sep: QLatin1Char(',')); |
877 | if (comp.size() == 2) { |
878 | return { QLatin1String(".x: " ) + comp.at(i: 0).trimmed(), |
879 | QLatin1String(".y: " ) + comp.at(i: 1).trimmed() }; |
880 | } |
881 | } |
882 | break; |
883 | } |
884 | case QMetaType::QVector3D: { |
885 | QRegularExpressionMatch match = re.match(subject: value); |
886 | if (match.hasMatch()) { |
887 | const auto comp = match.captured(nth: 1).split(sep: QLatin1Char(',')); |
888 | if (comp.size() == 3) { |
889 | return { QLatin1String(".x: " ) + comp.at(i: 0).trimmed(), |
890 | QLatin1String(".y: " ) + comp.at(i: 1).trimmed(), |
891 | QLatin1String(".z: " ) + comp.at(i: 2).trimmed() }; |
892 | } |
893 | } |
894 | break; |
895 | } |
896 | case QMetaType::QVector4D: { |
897 | QRegularExpressionMatch match = re.match(subject: value); |
898 | if (match.hasMatch()) { |
899 | const auto comp = match.captured(nth: 1).split(sep: QLatin1Char(',')); |
900 | if (comp.size() == 4) { |
901 | return { QLatin1String(".x: " ) + comp.at(i: 0).trimmed(), |
902 | QLatin1String(".y: " ) + comp.at(i: 1).trimmed(), |
903 | QLatin1String(".z: " ) + comp.at(i: 2).trimmed(), |
904 | QLatin1String(".w: " ) + comp.at(i: 3).trimmed() }; |
905 | } |
906 | } |
907 | break; |
908 | } |
909 | case QMetaType::QQuaternion: { |
910 | QRegularExpressionMatch match = re.match(subject: value); |
911 | if (match.hasMatch()) { |
912 | const auto comp = match.captured(nth: 1).split(sep: QLatin1Char(',')); |
913 | if (comp.size() == 4) { |
914 | return { QLatin1String(".x: " ) + comp.at(i: 0).trimmed(), |
915 | QLatin1String(".y: " ) + comp.at(i: 1).trimmed(), |
916 | QLatin1String(".z: " ) + comp.at(i: 2).trimmed(), |
917 | QLatin1String(".scalar: " ) + comp.at(i: 3).trimmed() }; |
918 | } |
919 | } |
920 | break; |
921 | } |
922 | default: |
923 | break; |
924 | } |
925 | |
926 | return { value }; |
927 | } |
928 | |
929 | static QStringList expandComponentsPartially(const QString &value, QMetaType mt) |
930 | { |
931 | // Workaround for DS |
932 | if (mt.id() != QMetaType::QQuaternion) |
933 | return expandComponents(value, mt); |
934 | |
935 | return { value }; |
936 | } |
937 | |
938 | struct ValueToQmlResult { |
939 | bool ok = false; |
940 | QString name; |
941 | QString value; |
942 | QString notValidReason; |
943 | bool isDynamicProperty = false; |
944 | QStringList expandedProperties; |
945 | }; |
946 | |
947 | static ValueToQmlResult valueToQml(const QSSGSceneDesc::Node &target, const QSSGSceneDesc::Property &property, OutputContext &output) |
948 | { |
949 | ValueToQmlResult result; |
950 | if (property.value.isNull()) { |
951 | result.ok = false; |
952 | result.notValidReason = QStringLiteral("Property value is null" ); |
953 | return result; |
954 | } |
955 | |
956 | const QVariant &value = property.value; |
957 | result.name = QString::fromUtf8(ba: property.name); |
958 | result.isDynamicProperty = property.type == QSSGSceneDesc::Property::Type::Dynamic; |
959 | |
960 | // Built-in types |
961 | QString valueAsString = builtinQmlType(var: value); |
962 | if (valueAsString.size() > 0) { |
963 | result.value = valueAsString; |
964 | result.ok = true; |
965 | } else if (value.metaType().flags() & (QMetaType::IsEnumeration | QMetaType::IsUnsignedEnumeration)) { |
966 | static const auto qmlEnumString = [](const QLatin1String &element, const QString &enumString) { |
967 | return QStringLiteral("%1.%2" ).arg(a: element).arg(a: enumString); |
968 | }; |
969 | QLatin1String qmlElementName(getQmlElementName(node: target)); |
970 | QString enumValue = asString(var: value); |
971 | if (enumValue.size() > 0) { |
972 | result.value = qmlEnumString(qmlElementName, enumValue); |
973 | result.ok = true; |
974 | } |
975 | } else if (value.metaType().id() == qMetaTypeId<QSSGSceneDesc::Flag>()) { |
976 | QByteArray element(getQmlElementName(node: target)); |
977 | if (element.size() > 0) { |
978 | const auto flag = qvariant_cast<QSSGSceneDesc::Flag>(v: value); |
979 | QByteArray keysString = flag.me.valueToKeys(value: int(flag.value)); |
980 | if (keysString.size() > 0) { |
981 | keysString.prepend(a: element + '.'); |
982 | QByteArray replacement(" | " + element + '.'); |
983 | keysString.replace(before: '|', after: replacement); |
984 | result.value = QString::fromLatin1(ba: keysString); |
985 | result.ok = true; |
986 | } |
987 | } |
988 | } else if (value.metaType().id() == qMetaTypeId<QSSGSceneDesc::NodeList *>()) { |
989 | const auto *list = qvariant_cast<QSSGSceneDesc::NodeList *>(v: value); |
990 | if (list->count > 0) { |
991 | const QString indentStr = indentString(output); |
992 | QSSGQmlScopedIndent scopedIndent(output); |
993 | const QString listIndentStr = indentString(output); |
994 | |
995 | QString str; |
996 | str.append(v: u"[\n" ); |
997 | |
998 | for (int i = 0, end = list->count; i != end; ++i) { |
999 | if (i != 0) |
1000 | str.append(v: u",\n" ); |
1001 | str.append(s: listIndentStr); |
1002 | str.append(s: getIdForNode(node: *(list->head[i]))); |
1003 | } |
1004 | |
1005 | str.append(s: u'\n' + indentStr + u']'); |
1006 | |
1007 | result.value = str; |
1008 | result.ok = true; |
1009 | } |
1010 | } else if (value.metaType().id() == qMetaTypeId<QSSGSceneDesc::ListView *>()) { |
1011 | const auto &list = *qvariant_cast<QSSGSceneDesc::ListView *>(v: value); |
1012 | if (list.count > 0) { |
1013 | const QString indentStr = indentString(output); |
1014 | QSSGQmlScopedIndent scopedIndent(output); |
1015 | const QString listIndentStr = indentString(output); |
1016 | |
1017 | QString str; |
1018 | str.append(v: u"[\n" ); |
1019 | |
1020 | char *vptr = reinterpret_cast<char *>(list.data); |
1021 | auto size = list.mt.sizeOf(); |
1022 | |
1023 | for (int i = 0, end = list.count; i != end; ++i) { |
1024 | if (i != 0) |
1025 | str.append(v: u",\n" ); |
1026 | |
1027 | const QVariant var{list.mt, reinterpret_cast<void *>(vptr + (size * i))}; |
1028 | QString valueString = builtinQmlType(var); |
1029 | if (valueString.isEmpty()) |
1030 | valueString = asString(var); |
1031 | |
1032 | str.append(s: listIndentStr); |
1033 | str.append(s: valueString); |
1034 | } |
1035 | |
1036 | str.append(s: u'\n' + indentStr + u']'); |
1037 | |
1038 | result.value = str; |
1039 | result.ok = true; |
1040 | } |
1041 | } else if (value.metaType().id() == qMetaTypeId<QSSGSceneDesc::Node *>()) { |
1042 | if (const auto node = qvariant_cast<QSSGSceneDesc::Node *>(v: value)) { |
1043 | // If this assert is triggerd it likely means that the node never got added |
1044 | // to the scene tree (see: addNode()) or that it's a type not handled as a resource, see: |
1045 | // writeQmlForResources() |
1046 | Q_ASSERT(node->id != 0); |
1047 | // The 'TextureData' node will have its data written out and become |
1048 | // a source url. |
1049 | |
1050 | if (node->runtimeType == QSSGSceneDesc::Node::RuntimeType::TextureData) { |
1051 | result.name = QStringLiteral("source" ); |
1052 | result.value = getIdForNode(node: *node->scene->root) + QLatin1Char('.') + getIdForNode(node: *node); |
1053 | } else { |
1054 | result.value = getIdForNode(node: *node); |
1055 | } |
1056 | result.ok = true; |
1057 | } |
1058 | } else if (value.metaType() == QMetaType::fromType<QSSGSceneDesc::Mesh *>()) { |
1059 | if (const auto meshNode = qvariant_cast<const QSSGSceneDesc::Mesh *>(v: value)) { |
1060 | Q_ASSERT(meshNode->nodeType == QSSGSceneDesc::Node::Type::Mesh); |
1061 | Q_ASSERT(meshNode->scene); |
1062 | const auto &scene = *meshNode->scene; |
1063 | const auto& [meshSourceName, notValidReason] = meshAssetName(scene, meshNode: *meshNode, outdir: output.outdir); |
1064 | result.notValidReason = notValidReason; |
1065 | if (!meshSourceName.isEmpty()) { |
1066 | result.value = toQuotedString(text: meshSourceName); |
1067 | result.ok = true; |
1068 | } |
1069 | } |
1070 | } else if (value.metaType() == QMetaType::fromType<QUrl>()) { |
1071 | if (const auto url = qvariant_cast<QUrl>(v: value); !url.isEmpty()) { |
1072 | // We need to adjust source url(s) as those should contain the canonical path |
1073 | QString path; |
1074 | if (QSSGRenderGraphObject::isTexture(type: target.runtimeType)) { |
1075 | const auto& [relpath, notValidReason] = copyTextureAsset(texturePath: url, output); |
1076 | result.notValidReason = notValidReason; |
1077 | if (!relpath.isEmpty()) { |
1078 | path = relpath; |
1079 | } |
1080 | } else |
1081 | path = url.path(); |
1082 | |
1083 | if (!path.isEmpty()) { |
1084 | result.value = toQuotedString(text: path); |
1085 | result.ok = true; |
1086 | } |
1087 | } |
1088 | } else if (target.runtimeType == QSSGSceneDesc::Material::RuntimeType::CustomMaterial) { |
1089 | // Workaround the TextureInput item that wraps textures for the Custom material. |
1090 | if (value.metaType().id() == qMetaTypeId<QSSGSceneDesc::Texture *>()) { |
1091 | if (const auto texture = qvariant_cast<QSSGSceneDesc::Texture *>(v: value)) { |
1092 | Q_ASSERT(QSSGRenderGraphObject::isTexture(texture->runtimeType)); |
1093 | result.value = QLatin1String("TextureInput { texture: " ) + |
1094 | getIdForNode(node: *texture) + QLatin1String(" }" ); |
1095 | result.ok = true; |
1096 | } |
1097 | } |
1098 | } else if (value.metaType() == QMetaType::fromType<QString>()) { |
1099 | // Plain strings in the scenedesc should map to QML string values |
1100 | result.value = toQuotedString(text: value.toString()); |
1101 | result.ok = true; |
1102 | } else { |
1103 | result.notValidReason = QStringLiteral("Unsupported value type: " ) + QString::fromUtf8(utf8: value.metaType().name()); |
1104 | qWarning() << result.notValidReason; |
1105 | result.ok = false; |
1106 | } |
1107 | |
1108 | if (result.ok && (output.options & OutputContext::Options::ExpandValueComponents)) { |
1109 | result.expandedProperties = ((output.options & OutputContext::Options::DesignStudioWorkarounds) == OutputContext::Options::DesignStudioWorkarounds) |
1110 | ? expandComponentsPartially(value: result.value, mt: value.metaType()) |
1111 | : expandComponents(value: result.value, mt: value.metaType()); |
1112 | } |
1113 | |
1114 | return result; |
1115 | } |
1116 | |
1117 | static void writeNodeProperties(const QSSGSceneDesc::Node &node, OutputContext &output) |
1118 | { |
1119 | QSSGQmlScopedIndent scopedIndent(output); |
1120 | |
1121 | indent(output) << u"id: "_s << getIdForNode(node) << u'\n'; |
1122 | |
1123 | // Set Object Name if one exists |
1124 | if (node.name.size()) { |
1125 | const QString objectName = QString::fromLocal8Bit(ba: node.name); |
1126 | if (!objectName.startsWith(c: u'*')) |
1127 | indent(output) << u"objectName: \""_s << node.name << u"\"\n"_s ; |
1128 | } |
1129 | |
1130 | const auto &properties = node.properties; |
1131 | auto it = properties.begin(); |
1132 | const auto end = properties.end(); |
1133 | for (; it != end; ++it) { |
1134 | const auto &property = *it; |
1135 | |
1136 | const ValueToQmlResult result = valueToQml(target: node, property: *property, output); |
1137 | if (result.ok) { |
1138 | if (result.isDynamicProperty) { |
1139 | indent(output) << "property " << typeName(mt: property->value.metaType()).toByteArray() << ' ' << result.name << u": "_s << result.value << u'\n'; |
1140 | } else if (!QSSGQmlUtilities::PropertyMap::instance()->isDefaultValue(type: node.runtimeType, property: property->name, value: property->value)) { |
1141 | if (result.expandedProperties.size() > 1) { |
1142 | for (const auto &va : result.expandedProperties) |
1143 | indent(output) << result.name << va << u'\n'; |
1144 | } else { |
1145 | indent(output) << result.name << u": "_s << result.value << u'\n'; |
1146 | } |
1147 | } |
1148 | } else if (!result.isDynamicProperty) { |
1149 | QString message = u"Skipped property: "_s + QString::fromUtf8(ba: property->name); |
1150 | if (!result.notValidReason.isEmpty()) |
1151 | message.append(s: u", reason: "_s + result.notValidReason); |
1152 | qDebug() << message; |
1153 | indent(output) << comment() << message + u'\n'; |
1154 | } |
1155 | } |
1156 | } |
1157 | |
1158 | static void writeQml(const QSSGSceneDesc::Node &transform, OutputContext &output) |
1159 | { |
1160 | using namespace QSSGSceneDesc; |
1161 | Q_ASSERT(transform.nodeType == QSSGSceneDesc::Node::Type::Transform && transform.runtimeType == QSSGSceneDesc::Node::RuntimeType::Node); |
1162 | indent(output) << qmlElementName<QSSGSceneDesc::Node::RuntimeType::Node>() << blockBegin(output); |
1163 | writeNodeProperties(node: transform, output); |
1164 | } |
1165 | |
1166 | void writeQml(const QSSGSceneDesc::Material &material, OutputContext &output) |
1167 | { |
1168 | using namespace QSSGSceneDesc; |
1169 | Q_ASSERT(material.nodeType == QSSGSceneDesc::Model::Type::Material); |
1170 | if (material.runtimeType == QSSGSceneDesc::Model::RuntimeType::SpecularGlossyMaterial) { |
1171 | indent(output) << qmlElementName<Material::RuntimeType::SpecularGlossyMaterial>() << blockBegin(output); |
1172 | } else if (material.runtimeType == Model::RuntimeType::PrincipledMaterial) { |
1173 | indent(output) << qmlElementName<Material::RuntimeType::PrincipledMaterial>() << blockBegin(output); |
1174 | } else if (material.runtimeType == Material::RuntimeType::CustomMaterial) { |
1175 | indent(output) << qmlElementName<Material::RuntimeType::CustomMaterial>() << blockBegin(output); |
1176 | } else if (material.runtimeType == Material::RuntimeType::SpecularGlossyMaterial) { |
1177 | indent(output) << qmlElementName<Material::RuntimeType::SpecularGlossyMaterial>() << blockBegin(output); |
1178 | } else { |
1179 | Q_UNREACHABLE(); |
1180 | } |
1181 | |
1182 | writeNodeProperties(node: material, output); |
1183 | } |
1184 | |
1185 | static void writeQml(const QSSGSceneDesc::Model &model, OutputContext &output) |
1186 | { |
1187 | using namespace QSSGSceneDesc; |
1188 | Q_ASSERT(model.nodeType == Node::Type::Model); |
1189 | indent(output) << qmlElementName<QSSGSceneDesc::Node::RuntimeType::Model>() << blockBegin(output); |
1190 | writeNodeProperties(node: model, output); |
1191 | } |
1192 | |
1193 | static void writeQml(const QSSGSceneDesc::Camera &camera, OutputContext &output) |
1194 | { |
1195 | using namespace QSSGSceneDesc; |
1196 | Q_ASSERT(camera.nodeType == Node::Type::Camera); |
1197 | if (camera.runtimeType == Camera::RuntimeType::PerspectiveCamera) |
1198 | indent(output) << qmlElementName<Camera::RuntimeType::PerspectiveCamera>() << blockBegin(output); |
1199 | else if (camera.runtimeType == Camera::RuntimeType::OrthographicCamera) |
1200 | indent(output) << qmlElementName<Camera::RuntimeType::OrthographicCamera>() << blockBegin(output); |
1201 | else |
1202 | Q_UNREACHABLE(); |
1203 | writeNodeProperties(node: camera, output); |
1204 | } |
1205 | |
1206 | static void writeQml(const QSSGSceneDesc::Texture &texture, OutputContext &output) |
1207 | { |
1208 | using namespace QSSGSceneDesc; |
1209 | Q_ASSERT(texture.nodeType == Node::Type::Texture && QSSGRenderGraphObject::isTexture(texture.runtimeType)); |
1210 | if (texture.runtimeType == Texture::RuntimeType::Image2D) |
1211 | indent(output) << qmlElementName<Texture::RuntimeType::Image2D>() << blockBegin(output); |
1212 | else if (texture.runtimeType == Texture::RuntimeType::ImageCube) |
1213 | indent(output) << qmlElementName<Texture::RuntimeType::ImageCube>() << blockBegin(output); |
1214 | writeNodeProperties(node: texture, output); |
1215 | } |
1216 | |
1217 | static void writeQml(const QSSGSceneDesc::Skin &skin, OutputContext &output) |
1218 | { |
1219 | using namespace QSSGSceneDesc; |
1220 | Q_ASSERT(skin.nodeType == Node::Type::Skin && skin.runtimeType == Node::RuntimeType::Skin); |
1221 | indent(output) << qmlElementName<Node::RuntimeType::Skin>() << blockBegin(output); |
1222 | writeNodeProperties(node: skin, output); |
1223 | } |
1224 | |
1225 | static void writeQml(const QSSGSceneDesc::MorphTarget &morphTarget, OutputContext &output) |
1226 | { |
1227 | using namespace QSSGSceneDesc; |
1228 | Q_ASSERT(morphTarget.nodeType == Node::Type::MorphTarget); |
1229 | indent(output) << qmlElementName<QSSGSceneDesc::Node::RuntimeType::MorphTarget>() << blockBegin(output); |
1230 | writeNodeProperties(node: morphTarget, output); |
1231 | } |
1232 | |
1233 | QString getTextureSourceName(const QString &name, const QString &fmt) |
1234 | { |
1235 | const auto textureFolder = getTextureFolder(); |
1236 | |
1237 | const auto sanitizedName = QSSGQmlUtilities::sanitizeQmlId(id: name); |
1238 | const auto ext = (fmt.length() != 3) ? u".png"_s |
1239 | : u"."_s + fmt; |
1240 | |
1241 | return QString(textureFolder + sanitizedName + ext); |
1242 | } |
1243 | |
1244 | static QString outputTextureAsset(const QSSGSceneDesc::TextureData &textureData, const QDir &outdir) |
1245 | { |
1246 | if (textureData.data.isEmpty()) |
1247 | return QString(); |
1248 | |
1249 | const auto mapsFolder = getTextureFolder(); |
1250 | const auto id = getIdForNode(node: textureData); |
1251 | const QString textureSourceName = getTextureSourceName(name: id, fmt: QString::fromUtf8(ba: textureData.fmt)); |
1252 | |
1253 | const bool isCompressed = ((textureData.flgs & quint8(QSSGSceneDesc::TextureData::Flags::Compressed)) != 0); |
1254 | |
1255 | // If a maps folder does not exist, then create one |
1256 | if (!outdir.exists(name: mapsFolder) && !outdir.mkdir(dirName: mapsFolder)) |
1257 | return QString(); // Error out |
1258 | |
1259 | const auto imagePath = QString(outdir.path() + QDir::separator() + textureSourceName); |
1260 | |
1261 | if (isCompressed) { |
1262 | QFile file(imagePath); |
1263 | file.open(flags: QIODevice::WriteOnly); |
1264 | file.write(data: textureData.data); |
1265 | file.close(); |
1266 | } else { |
1267 | const auto &texData = textureData.data; |
1268 | const auto &size = textureData.sz; |
1269 | QImage image; |
1270 | image = QImage(reinterpret_cast<const uchar *>(texData.data()), size.width(), size.height(), QImage::Format::Format_RGBA8888); |
1271 | if (!image.save(fileName: imagePath)) |
1272 | return QString(); |
1273 | } |
1274 | |
1275 | return textureSourceName; |
1276 | } |
1277 | |
1278 | static void writeQml(const QSSGSceneDesc::TextureData &textureData, OutputContext &output) |
1279 | { |
1280 | using namespace QSSGSceneDesc; |
1281 | Q_ASSERT(textureData.nodeType == Node::Type::Texture && textureData.runtimeType == Node::RuntimeType::TextureData); |
1282 | |
1283 | QString textureSourcePath = outputTextureAsset(textureData, outdir: output.outdir); |
1284 | |
1285 | static const auto writeProperty = [](const QString &type, const QString &name, const QString &value) { |
1286 | return QString::fromLatin1(ba: "property %1 %2: %3" ).arg(args: type, args: name, args: value); |
1287 | }; |
1288 | |
1289 | if (!textureSourcePath.isEmpty()) { |
1290 | const auto type = QLatin1String("url" ); |
1291 | const auto name = getIdForNode(node: textureData); |
1292 | |
1293 | indent(output) << writeProperty(type, name, toQuotedString(text: textureSourcePath)) << '\n'; |
1294 | } |
1295 | } |
1296 | |
1297 | static void writeQml(const QSSGSceneDesc::Light &light, OutputContext &output) |
1298 | { |
1299 | using namespace QSSGSceneDesc; |
1300 | Q_ASSERT(light.nodeType == Node::Type::Light); |
1301 | if (light.runtimeType == Light::RuntimeType::DirectionalLight) |
1302 | indent(output) << qmlElementName<Light::RuntimeType::DirectionalLight>() << blockBegin(output); |
1303 | else if (light.runtimeType == Light::RuntimeType::SpotLight) |
1304 | indent(output) << qmlElementName<Light::RuntimeType::SpotLight>() << blockBegin(output); |
1305 | else if (light.runtimeType == Light::RuntimeType::PointLight) |
1306 | indent(output) << qmlElementName<Light::RuntimeType::PointLight>() << blockBegin(output); |
1307 | else |
1308 | Q_UNREACHABLE(); |
1309 | writeNodeProperties(node: light, output); |
1310 | } |
1311 | |
1312 | static void writeQml(const QSSGSceneDesc::Skeleton &skeleton, OutputContext &output) |
1313 | { |
1314 | using namespace QSSGSceneDesc; |
1315 | Q_ASSERT(skeleton.nodeType == Node::Type::Skeleton && skeleton.runtimeType == Node::RuntimeType::Skeleton); |
1316 | indent(output) << qmlElementName<Node::RuntimeType::Skeleton>() << blockBegin(output); |
1317 | writeNodeProperties(node: skeleton, output); |
1318 | } |
1319 | |
1320 | static void writeQml(const QSSGSceneDesc::Joint &joint, OutputContext &output) |
1321 | { |
1322 | using namespace QSSGSceneDesc; |
1323 | Q_ASSERT(joint.nodeType == Node::Type::Joint && joint.runtimeType == Node::RuntimeType::Joint); |
1324 | indent(output) << qmlElementName<Node::RuntimeType::Joint>() << blockBegin(output); |
1325 | writeNodeProperties(node: joint, output); |
1326 | } |
1327 | |
1328 | static void writeQmlForResourceNode(const QSSGSceneDesc::Node &node, OutputContext &output) |
1329 | { |
1330 | using namespace QSSGSceneDesc; |
1331 | Q_ASSERT(output.type == OutputContext::Resource); |
1332 | Q_ASSERT(QSSGRenderGraphObject::isResource(node.runtimeType) || node.nodeType == Node::Type::Mesh || node.nodeType == Node::Type::Skeleton); |
1333 | |
1334 | const bool processNode = !node.properties.isEmpty() || (output.type == OutputContext::Resource); |
1335 | if (processNode) { |
1336 | QSSGQmlScopedIndent scopedIndent(output); |
1337 | switch (node.nodeType) { |
1338 | case Node::Type::Skin: |
1339 | writeQml(skin: static_cast<const Skin &>(node), output); |
1340 | break; |
1341 | case Node::Type::MorphTarget: |
1342 | writeQml(morphTarget: static_cast<const MorphTarget &>(node), output); |
1343 | break; |
1344 | case Node::Type::Skeleton: |
1345 | writeQml(skeleton: static_cast<const Skeleton &>(node), output); |
1346 | break; |
1347 | case Node::Type::Texture: |
1348 | if (node.runtimeType == Node::RuntimeType::Image2D) |
1349 | writeQml(texture: static_cast<const Texture &>(node), output); |
1350 | else if (node.runtimeType == Node::RuntimeType::ImageCube) |
1351 | writeQml(texture: static_cast<const Texture &>(node), output); |
1352 | else if (node.runtimeType == Node::RuntimeType::TextureData) |
1353 | writeQml(textureData: static_cast<const TextureData &>(node), output); |
1354 | else |
1355 | Q_UNREACHABLE(); |
1356 | break; |
1357 | case Node::Type::Material: |
1358 | writeQml(material: static_cast<const Material &>(node), output); |
1359 | break; |
1360 | case Node::Type::Mesh: |
1361 | // Only handled as a property (see: valueToQml()) |
1362 | break; |
1363 | default: |
1364 | qWarning(msg: "Unhandled resource type \'%d\'?" , int(node.runtimeType)); |
1365 | break; |
1366 | } |
1367 | } |
1368 | |
1369 | // Do something more convenient if this starts expending to more types... |
1370 | // NOTE: The TextureData type is written out as a url property... |
1371 | const bool skipBlockEnd = (node.runtimeType == Node::RuntimeType::TextureData || node.nodeType == Node::Type::Mesh); |
1372 | if (!skipBlockEnd && processNode && output.scopeDepth != 0) { |
1373 | QSSGQmlScopedIndent scopedIndent(output); |
1374 | indent(output) << blockEnd(output); |
1375 | } |
1376 | } |
1377 | |
1378 | static void writeQmlForNode(const QSSGSceneDesc::Node &node, OutputContext &output) |
1379 | { |
1380 | using namespace QSSGSceneDesc; |
1381 | |
1382 | const bool processNode = !(node.properties.isEmpty() && node.children.isEmpty()) |
1383 | || (output.type == OutputContext::Resource); |
1384 | if (processNode) { |
1385 | QSSGQmlScopedIndent scopedIndent(output); |
1386 | switch (node.nodeType) { |
1387 | case Node::Type::Skeleton: |
1388 | writeQml(skeleton: static_cast<const Skeleton &>(node), output); |
1389 | break; |
1390 | case Node::Type::Joint: |
1391 | writeQml(joint: static_cast<const Joint &>(node), output); |
1392 | break; |
1393 | case Node::Type::Light: |
1394 | writeQml(light: static_cast<const Light &>(node), output); |
1395 | break; |
1396 | case Node::Type::Transform: |
1397 | writeQml(transform: node, output); |
1398 | break; |
1399 | case Node::Type::Camera: |
1400 | writeQml(camera: static_cast<const Camera &>(node), output); |
1401 | break; |
1402 | case Node::Type::Model: |
1403 | writeQml(model: static_cast<const Model &>(node), output); |
1404 | break; |
1405 | default: |
1406 | break; |
1407 | } |
1408 | } |
1409 | |
1410 | for (const auto &cld : node.children) { |
1411 | if (!QSSGRenderGraphObject::isResource(type: cld->runtimeType) && output.type == OutputContext::NodeTree) { |
1412 | QSSGQmlScopedIndent scopedIndent(output); |
1413 | writeQmlForNode(node: *cld, output); |
1414 | } |
1415 | } |
1416 | |
1417 | // Do something more convenient if this starts expending to more types... |
1418 | // NOTE: The TextureData type is written out as a url property... |
1419 | const bool skipBlockEnd = (node.runtimeType == Node::RuntimeType::TextureData || node.nodeType == Node::Type::Mesh); |
1420 | if (!skipBlockEnd && processNode && output.scopeDepth != 0) { |
1421 | QSSGQmlScopedIndent scopedIndent(output); |
1422 | indent(output) << blockEnd(output); |
1423 | } |
1424 | } |
1425 | |
1426 | void writeQmlForResources(const QSSGSceneDesc::Scene::ResourceNodes &resources, OutputContext &output) |
1427 | { |
1428 | auto sortedResources = resources; |
1429 | std::sort(first: sortedResources.begin(), last: sortedResources.end(), comp: [](const QSSGSceneDesc::Node *a, const QSSGSceneDesc::Node *b) { |
1430 | using RType = QSSGSceneDesc::Node::RuntimeType; |
1431 | if (a->runtimeType == RType::TextureData && b->runtimeType != RType::TextureData) |
1432 | return true; |
1433 | if (a->runtimeType == RType::ImageCube && (b->runtimeType != RType::TextureData && b->runtimeType != RType::ImageCube)) |
1434 | return true; |
1435 | if (a->runtimeType == RType::Image2D && (b->runtimeType != RType::TextureData && b->runtimeType != RType::Image2D)) |
1436 | return true; |
1437 | |
1438 | return false; |
1439 | }); |
1440 | for (const auto &res : std::as_const(t&: sortedResources)) |
1441 | writeQmlForResourceNode(node: *res, output); |
1442 | } |
1443 | |
1444 | static void generateKeyframeData(const QSSGSceneDesc::Animation::Channel &channel, QByteArray &keyframeData) |
1445 | { |
1446 | #ifdef QT_QUICK3D_ENABLE_RT_ANIMATIONS |
1447 | QCborStreamWriter writer(&keyframeData); |
1448 | // Start root array |
1449 | writer.startArray(); |
1450 | // header name |
1451 | writer.append(str: "QTimelineKeyframes" ); |
1452 | // file version. Increase this if the format changes. |
1453 | const int keyframesDataVersion = 1; |
1454 | writer.append(i: keyframesDataVersion); |
1455 | writer.append(i: int(channel.keys.at(i: 0)->getValueQMetaType())); |
1456 | |
1457 | // Start Keyframes array |
1458 | writer.startArray(); |
1459 | quint8 compEnd = quint8(channel.keys.at(i: 0)->getValueType()); |
1460 | bool isQuaternion = false; |
1461 | if (compEnd == quint8(QSSGSceneDesc::Animation::KeyPosition::ValueType::Quaternion)) { |
1462 | isQuaternion = true; |
1463 | compEnd = 3; |
1464 | } else { |
1465 | compEnd++; |
1466 | } |
1467 | for (const auto &key : channel.keys) { |
1468 | writer.append(f: key->time); |
1469 | // Easing always linear |
1470 | writer.append(i: QEasingCurve::Linear); |
1471 | if (isQuaternion) |
1472 | writer.append(f: key->value[3]); |
1473 | for (quint8 i = 0; i < compEnd; ++i) |
1474 | writer.append(f: key->value[i]); |
1475 | } |
1476 | // End Keyframes array |
1477 | writer.endArray(); |
1478 | // End root array |
1479 | writer.endArray(); |
1480 | #else |
1481 | Q_UNUSED(channel) |
1482 | Q_UNUSED(keyframeData) |
1483 | #endif // QT_QUICK3D_ENABLE_RT_ANIMATIONS |
1484 | } |
1485 | |
1486 | void writeQmlForAnimation(const QSSGSceneDesc::Animation &anim, qsizetype index, OutputContext &output, bool useBinaryKeyframes = true) |
1487 | { |
1488 | indent(output) << "Timeline {\n" ; |
1489 | |
1490 | QSSGQmlScopedIndent scopedIndent(output); |
1491 | // The duration property of the TimelineAnimation is an int... |
1492 | const int duration = qCeil(v: anim.length); |
1493 | // Use the same name for objectName and id |
1494 | const QString objectName = getIdForAnimation(inName: anim.name); |
1495 | indent(output) << "id: " << objectName << "\n" ; |
1496 | indent(output) << "objectName: \"" << objectName << "\"\n" ; |
1497 | indent(output) << "property real framesPerSecond: " << anim.framesPerSecond << "\n" ; |
1498 | indent(output) << "startFrame: 0\n" ; |
1499 | indent(output) << "endFrame: " << duration << "\n" ; |
1500 | indent(output) << "currentFrame: 0\n" ; |
1501 | indent(output) << "enabled: true\n" ; |
1502 | indent(output) << "animations: TimelineAnimation {\n" ; |
1503 | { |
1504 | QSSGQmlScopedIndent scopedIndent(output); |
1505 | indent(output) << "duration: " << duration << "\n" ; |
1506 | indent(output) << "from: 0\n" ; |
1507 | indent(output) << "to: " << duration << "\n" ; |
1508 | indent(output) << "running: true\n" ; |
1509 | indent(output) << "loops: Animation.Infinite\n" ; |
1510 | } |
1511 | indent(output) << blockEnd(output); |
1512 | |
1513 | for (const auto &channel : anim.channels) { |
1514 | QString id = getIdForNode(node: *channel->target); |
1515 | QString propertyName = asString(prop: channel->targetProperty); |
1516 | |
1517 | indent(output) << "KeyframeGroup {\n" ; |
1518 | { |
1519 | QSSGQmlScopedIndent scopedIndent(output); |
1520 | indent(output) << "target: " << id << "\n" ; |
1521 | indent(output) << "property: " << toQuotedString(text: propertyName) << "\n" ; |
1522 | if (useBinaryKeyframes && channel->keys.size() != 1) { |
1523 | const auto animFolder = getAnimationFolder(); |
1524 | const auto animSourceName = getAnimationSourceName(id, property: propertyName, index); |
1525 | if (!output.outdir.exists(name: animFolder) && !output.outdir.mkdir(dirName: animFolder)) { |
1526 | // Make a warning |
1527 | continue; |
1528 | } |
1529 | QFile file(output.outdir.path() + QDir::separator() + animSourceName); |
1530 | if (!file.open(flags: QIODevice::WriteOnly)) |
1531 | continue; |
1532 | QByteArray keyframeData; |
1533 | // It is possible to store this keyframeData but we have to consider |
1534 | // all the cases including runtime only or writeQml only. |
1535 | // For now, we will generate it for each case. |
1536 | generateKeyframeData(channel: *channel, keyframeData); |
1537 | file.write(data: keyframeData); |
1538 | file.close(); |
1539 | indent(output) << "keyframeSource: " << toQuotedString(text: animSourceName) << "\n" ; |
1540 | } else { |
1541 | Q_ASSERT(!channel->keys.isEmpty()); |
1542 | for (const auto &key : channel->keys) { |
1543 | indent(output) << "Keyframe {\n" ; |
1544 | { |
1545 | QSSGQmlScopedIndent scopedIndent(output); |
1546 | indent(output) << "frame: " << key->time << "\n" ; |
1547 | indent(output) << "value: " << variantToQml(variant: key->getValue()) << "\n" ; |
1548 | } |
1549 | indent(output) << blockEnd(output); |
1550 | } |
1551 | } |
1552 | } |
1553 | indent(output) << blockEnd(output); |
1554 | } |
1555 | } |
1556 | |
1557 | void writeQml(const QSSGSceneDesc::Scene &scene, QTextStream &stream, const QDir &outdir, const QJsonObject &optionsObject) |
1558 | { |
1559 | static const auto checkBooleanOption = [](const QLatin1String &optionName, const QJsonObject &options, bool defaultValue = false) { |
1560 | const auto it = options.constFind(key: optionName); |
1561 | const auto end = options.constEnd(); |
1562 | QJsonValue value; |
1563 | if (it != end) { |
1564 | if (it->isObject()) |
1565 | value = it->toObject().value(key: QLatin1String("value" )); |
1566 | else |
1567 | value = it.value(); |
1568 | } |
1569 | return value.toBool(defaultValue); |
1570 | }; |
1571 | |
1572 | auto root = scene.root; |
1573 | Q_ASSERT(root); |
1574 | |
1575 | QJsonObject options = optionsObject; |
1576 | |
1577 | if (auto it = options.constFind(key: QLatin1String("options" )), end = options.constEnd(); it != end) |
1578 | options = it->toObject(); |
1579 | |
1580 | quint8 outputOptions{ OutputContext::Options::None }; |
1581 | if (checkBooleanOption(QLatin1String("expandValueComponents" ), options)) |
1582 | outputOptions |= OutputContext::Options::ExpandValueComponents; |
1583 | |
1584 | // Workaround for design studio type components |
1585 | if (checkBooleanOption(QLatin1String("designStudioWorkarounds" ), options)) |
1586 | outputOptions |= OutputContext::Options::DesignStudioWorkarounds; |
1587 | |
1588 | const bool useBinaryKeyframes = checkBooleanOption("useBinaryKeyframes"_L1 , options); |
1589 | |
1590 | OutputContext output { .stream: stream, .outdir: outdir, .sourceDir: scene.sourceDir, .indent: 0, .type: OutputContext::Header, .options: outputOptions }; |
1591 | |
1592 | writeImportHeader(output, hasAnimation: scene.animations.count() > 0); |
1593 | |
1594 | output.type = OutputContext::RootNode; |
1595 | writeQml(transform: *root, output); // Block scope will be left open! |
1596 | stream << "\n" ; |
1597 | stream << indent() << "// Resources\n" ; |
1598 | output.type = OutputContext::Resource; |
1599 | writeQmlForResources(resources: scene.resources, output); |
1600 | output.type = OutputContext::NodeTree; |
1601 | stream << "\n" ; |
1602 | stream << indent() << "// Nodes:\n" ; |
1603 | for (const auto &cld : root->children) |
1604 | writeQmlForNode(node: *cld, output); |
1605 | |
1606 | // animations |
1607 | qsizetype animId = 0; |
1608 | stream << "\n" ; |
1609 | stream << indent() << "// Animations:\n" ; |
1610 | for (const auto &cld : scene.animations) { |
1611 | QSSGQmlScopedIndent scopedIndent(output); |
1612 | writeQmlForAnimation(anim: *cld, index: animId++, output, useBinaryKeyframes); |
1613 | indent(output) << blockEnd(output); |
1614 | } |
1615 | |
1616 | // close the root |
1617 | indent(output) << blockEnd(output); |
1618 | } |
1619 | |
1620 | void createTimelineAnimation(const QSSGSceneDesc::Animation &anim, QObject *parent, bool isEnabled, bool useBinaryKeyframes) |
1621 | { |
1622 | #ifdef QT_QUICK3D_ENABLE_RT_ANIMATIONS |
1623 | auto timeline = new QQuickTimeline(parent); |
1624 | auto timelineKeyframeGroup = timeline->keyframeGroups(); |
1625 | for (const auto &channel : anim.channels) { |
1626 | auto keyframeGroup = new QQuickKeyframeGroup(timeline); |
1627 | keyframeGroup->setTargetObject(channel->target->obj); |
1628 | keyframeGroup->setProperty(asString(prop: channel->targetProperty)); |
1629 | |
1630 | Q_ASSERT(!channel->keys.isEmpty()); |
1631 | if (useBinaryKeyframes) { |
1632 | QByteArray keyframeData; |
1633 | generateKeyframeData(channel: *channel, keyframeData); |
1634 | |
1635 | keyframeGroup->setKeyframeData(keyframeData); |
1636 | } else { |
1637 | auto keyframes = keyframeGroup->keyframes(); |
1638 | for (const auto &key : channel->keys) { |
1639 | auto keyframe = new QQuickKeyframe(keyframeGroup); |
1640 | keyframe->setFrame(key->time); |
1641 | keyframe->setValue(key->getValue()); |
1642 | keyframes.append(&keyframes, keyframe); |
1643 | } |
1644 | } |
1645 | (qobject_cast<QQmlParserStatus *>(object: keyframeGroup))->componentComplete(); |
1646 | timelineKeyframeGroup.append(&timelineKeyframeGroup, keyframeGroup); |
1647 | } |
1648 | timeline->setEndFrame(anim.length); |
1649 | timeline->setEnabled(isEnabled); |
1650 | |
1651 | auto timelineAnimation = new QQuickTimelineAnimation(timeline); |
1652 | timelineAnimation->setObjectName(anim.name); |
1653 | timelineAnimation->setDuration(int(anim.length)); |
1654 | timelineAnimation->setFrom(0.0f); |
1655 | timelineAnimation->setTo(anim.length); |
1656 | timelineAnimation->setLoops(QQuickTimelineAnimation::Infinite); |
1657 | timelineAnimation->setTargetObject(timeline); |
1658 | |
1659 | (qobject_cast<QQmlParserStatus *>(object: timeline))->componentComplete(); |
1660 | |
1661 | timelineAnimation->setRunning(true); |
1662 | #else // QT_QUICK3D_ENABLE_RT_ANIMATIONS |
1663 | Q_UNUSED(anim) |
1664 | Q_UNUSED(parent) |
1665 | Q_UNUSED(isEnabled) |
1666 | Q_UNUSED(useBinaryKeyframes) |
1667 | #endif // QT_QUICK3D_ENABLE_RT_ANIMATIONS |
1668 | } |
1669 | |
1670 | void writeQmlComponent(const QSSGSceneDesc::Node &node, QTextStream &stream, const QDir &outDir) |
1671 | { |
1672 | using namespace QSSGSceneDesc; |
1673 | |
1674 | QSSG_ASSERT(node.scene != nullptr, return); |
1675 | |
1676 | if (node.runtimeType == Material::RuntimeType::CustomMaterial) { |
1677 | QString sourceDir = node.scene->sourceDir; |
1678 | OutputContext output { .stream: stream, .outdir: outDir, .sourceDir: sourceDir, .indent: 0, .type: OutputContext::Resource }; |
1679 | writeImportHeader(output); |
1680 | writeQml(material: static_cast<const Material &>(node), output); |
1681 | // Resources, if any, are written out as properties on the component |
1682 | const auto &resources = node.scene->resources; |
1683 | writeQmlForResources(resources, output); |
1684 | indent(output) << blockEnd(output); |
1685 | } else { |
1686 | Q_UNREACHABLE(); // Only implemented for Custom material at this point. |
1687 | } |
1688 | } |
1689 | |
1690 | } |
1691 | |
1692 | QT_END_NAMESPACE |
1693 | |