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
30QT_BEGIN_NAMESPACE
31
32using namespace Qt::StringLiterals;
33
34namespace QSSGQmlUtilities {
35
36class PropertyMap
37{
38public:
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
47private:
48 PropertyMap();
49
50 QHash<QSSGSceneDesc::Node::RuntimeType, PropertiesMap> m_properties;
51
52};
53
54QString 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
67QString colorToQml(const QColor &color) {
68 QString colorString;
69 colorString = QLatin1Char('\"') + color.name(format: QColor::HexArgb) + QLatin1Char('\"');
70 return colorString;
71}
72
73QString 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
117QString 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
252QString 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
265PropertyMap *PropertyMap::instance()
266{
267 static PropertyMap p;
268 return &p;
269}
270
271PropertyMap::PropertiesMap PropertyMap::propertiesForType(QSSGSceneDesc::Node::RuntimeType type)
272{
273 return m_properties[type];
274}
275
276QVariant 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
288bool 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
294static 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
306PropertyMap::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
379struct OutputContext
380{
381 enum Type : quint8 { Header, 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
397template<QSSGSceneDesc::Material::RuntimeType T>
398const char *qmlElementName() { static_assert(!std::is_same_v<decltype(T), decltype(T)>, "Unknown type"); return nullptr; }
399template<> const char *qmlElementName<QSSGSceneDesc::Node::RuntimeType::Node>() { return "Node"; }
400
401template<> const char *qmlElementName<QSSGSceneDesc::Material::RuntimeType::SpecularGlossyMaterial>() { return "SpecularGlossyMaterial"; }
402template<> const char *qmlElementName<QSSGSceneDesc::Material::RuntimeType::PrincipledMaterial>() { return "PrincipledMaterial"; }
403template<> const char *qmlElementName<QSSGSceneDesc::Material::RuntimeType::CustomMaterial>() { return "CustomMaterial"; }
404template<> const char *qmlElementName<QSSGSceneDesc::Material::RuntimeType::OrthographicCamera>() { return "OrthographicCamera"; }
405template<> const char *qmlElementName<QSSGSceneDesc::Material::RuntimeType::PerspectiveCamera>() { return "PerspectiveCamera"; }
406
407template<> const char *qmlElementName<QSSGSceneDesc::Node::RuntimeType::Model>() { return "Model"; }
408
409template<> const char *qmlElementName<QSSGSceneDesc::Texture::RuntimeType::Image2D>() { return "Texture"; }
410template<> const char *qmlElementName<QSSGSceneDesc::Texture::RuntimeType::ImageCube>() { return "CubeMapTexture"; }
411template<> const char *qmlElementName<QSSGSceneDesc::Texture::RuntimeType::TextureData>() { return "TextureData"; }
412
413template<> const char *qmlElementName<QSSGSceneDesc::Camera::RuntimeType::DirectionalLight>() { return "DirectionalLight"; }
414template<> const char *qmlElementName<QSSGSceneDesc::Camera::RuntimeType::SpotLight>() { return "SpotLight"; }
415template<> const char *qmlElementName<QSSGSceneDesc::Camera::RuntimeType::PointLight>() { return "PointLight"; }
416
417template<> const char *qmlElementName<QSSGSceneDesc::Joint::RuntimeType::Joint>() { return "Joint"; }
418template<> const char *qmlElementName<QSSGSceneDesc::Skeleton::RuntimeType::Skeleton>() { return "Skeleton"; }
419template<> const char *qmlElementName<QSSGSceneDesc::Node::RuntimeType::Skin>() { return "Skin"; }
420template<> const char *qmlElementName<QSSGSceneDesc::Node::RuntimeType::MorphTarget>() { return "MorphTarget"; }
421
422static 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
465enum 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
489static 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
511static_assert(std::size(qml_basic_types) == QMLBasicType::Unknown_Count, "Missing type?");
512
513static 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
575using NodeNameMap = QHash<const QSSGSceneDesc::Node *, QString>;
576Q_GLOBAL_STATIC(NodeNameMap, g_nodeNameMap)
577using UniqueIdMap = QHash<QString, const QSSGSceneDesc::Node *>;
578Q_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.
584using UniqueIdOthers = QSet<QString>;
585Q_GLOBAL_STATIC(UniqueIdOthers, g_idOthers)
586
587static 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
628static 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
649QString 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
656static const char *blockBegin() { return " {\n"; }
657static const char *blockEnd() { return "}\n"; }
658static const char *comment() { return "// "; }
659static const char *indent() { return " "; }
660
661struct 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
669static 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
677static 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
684static const char *blockBegin(OutputContext &output)
685{
686 ++output.scopeDepth;
687 return blockBegin();
688}
689
690static const char *blockEnd(OutputContext &output)
691{
692 output.scopeDepth = qMax(a: 0, b: output.scopeDepth - 1);
693 return blockEnd();
694}
695
696static void writeImportHeader(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
704static QString toQuotedString(const QString &text) { return QStringLiteral("\"%1\"").arg(a: text); }
705
706static inline QString getMeshFolder() { return QStringLiteral("meshes/"); }
707static inline QString getMeshExtension() { return QStringLiteral(".mesh"); }
708
709QString getMeshSourceName(const QString &name)
710{
711 const auto meshFolder = getMeshFolder();
712 const auto extension = getMeshExtension();
713
714 return QString(meshFolder + name + extension);
715}
716
717static inline QString getTextureFolder() { return QStringLiteral("maps/"); }
718
719static inline QString getAnimationFolder() { return QStringLiteral("animations/"); }
720static inline QString getAnimationExtension() { return QStringLiteral(".qad"); }
721QString 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
730QString asString(const QVariant &var)
731{
732 return var.toString();
733}
734
735QString 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
792QString 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
806static 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
835static 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
867static 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
929static 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
938struct ValueToQmlResult {
939 bool ok = false;
940 QString name;
941 QString value;
942 QString notValidReason;
943 bool isDynamicProperty = false;
944 QStringList expandedProperties;
945};
946
947static 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
1117static 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
1158static 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
1166void 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
1185static 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
1193static 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
1206static 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
1217static 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
1225static 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
1233QString 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
1244static 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
1278static 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
1297static 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
1312static 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
1320static 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
1328static 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
1378static 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
1426void 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
1444static 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
1486void 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
1557void 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
1620void 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
1670void 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
1692QT_END_NAMESPACE
1693

source code of qtquick3d/src/assetutils/qssgqmlutilities.cpp