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