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