Revision control

++ // Copyright (C) 2022 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++
++ #include "qssgsceneedit_p.h"
++
++ #include <QtGui/QGuiApplication>
++
++ #include <QtCore/QVariant>
++ #include <QtCore/QHash>
++ #include <QtCore/QMetaProperty>
++ #include <QtCore/QUrl>
++
++ #include <QtCore/QJsonObject>
++ #include <QtCore/QJsonDocument>
++ #include <QtCore/QJsonArray>
++
++ #include <QtQuick3DAssetImport/private/qssgassetimportmanager_p.h>
++ #include <QtQuick3DAssetUtils/private/qssgscenedesc_p.h>
++ #include <QtQuick3DAssetUtils/private/qssgqmlutilities_p.h>
++ #include <QtQuick3DAssetUtils/private/qssgrtutilities_p.h>
++
++ QT_BEGIN_NAMESPACE
++ using namespace Qt::Literals::StringLiterals;
++ namespace QSSGQmlUtilities {
++
++ static const char* typeNames[] =
++ {
++ "Transform",
++ "Camera",
++ "Model",
++ "Texture",
++ "Material",
++ "Light",
++ "Mesh",
++ "Skin",
++ "Skeleton",
++ "Joint",
++ "MorphTarget",
++ "ERROR"
++ };
++
++ static constexpr qsizetype nNodeTypes = std::size(typeNames) - 1;
++
++ static QSSGSceneDesc::Node::Type nodeTypeFromName(const QByteArrayView &typeName)
++ {
++ int i = 0;
++ while (i < nNodeTypes) {
++ if (typeName == typeNames[i])
++ break;
++ ++i;
++ }
++ return QSSGSceneDesc::Node::Type(i);
++ }
++
++ static void replaceReferencesToResource(QSSGSceneDesc::Node *node, QSSGSceneDesc::Node *resource, QSSGSceneDesc::Node *replacement)
++ {
++ for (auto *prop : std::as_const(node->properties)) {
++ auto &val = prop->value;
++ if (qvariant_cast<QSSGSceneDesc::Node *>(val) == resource) {
++ if (replacement)
++ val = QVariant::fromValue(replacement);
++ }
++ if (val.metaType().id() == qMetaTypeId<QSSGSceneDesc::NodeList *>()) {
++ const auto &list = *qvariant_cast<QSSGSceneDesc::NodeList *>(val);
++ for (int i = 0, end = list.count; i != end; ++i) {
++ if (list.head[i] == resource) {
++ list.head[i] = replacement;
++ }
++ }
++ }
++ }
++ for (auto *child : std::as_const(node->children))
++ replaceReferencesToResource(child, resource, replacement);
++ }
++
++ // TODO: optimize this by using a hashmap or similar
++ static QSSGSceneDesc::Node *findNode(QSSGSceneDesc::Node *root, const QByteArrayView name,
++ QSSGSceneDesc::Node::Type type, QSSGSceneDesc::Node **parent = nullptr)
++ {
++ if (!root || name.isEmpty())
++ return nullptr;
++
++ if (root->name == name && root->nodeType == type)
++ return root;
++
++ for (auto *child : std::as_const(root->children)) {
++ if (auto *ret = findNode(child, name, type, parent)) {
++ if (parent && !*parent)
++ *parent = root;
++ return ret;
++ }
++ }
++ return nullptr;
++ }
++
++ static QSSGSceneDesc::Node *findResource(const QSSGSceneDesc::Scene *scene, const QByteArrayView &name, QSSGSceneDesc::Node::Type nodeType)
++ {
++ if (name.isEmpty())
++ return nullptr; // Empty strings by definition means nothing
++ for (auto *resource : scene->resources) {
++ if (resource->name == name && resource->nodeType == nodeType)
++ return resource;
++ }
++
++ return nullptr;
++ }
++
++ using NodeSet = QSet<QSSGSceneDesc::Node *>;
++ typedef bool NodeFilter(QSSGSceneDesc::Node *);
++
++ static NodeSet flattenTree(QSSGSceneDesc::Node *node, NodeFilter *excludeFunction = nullptr)
++ {
++ NodeSet ret = { node };
++ for (auto *child : std::as_const(node->children))
++ if (!excludeFunction || !excludeFunction(child))
++ ret.unite(flattenTree(child));
++ return ret;
++ }
++
++ static void unlinkChild(QSSGSceneDesc::Node *child, QSSGSceneDesc::Node *parent)
++ {
++ parent->children.removeOne(child);
++ }
++
++ static void removeFromAnimation(QSSGSceneDesc::Animation *animation, const NodeSet &nodes)
++ {
++ auto isTargeted = [nodes](QSSGSceneDesc::Animation::Channel *channel) { return nodes.contains(channel->target); };
++ const auto end_it = animation->channels.end();
++ auto remove_it = std::remove_if(animation->channels.begin(), end_it, isTargeted);
++ for (auto it = remove_it; it != end_it; ++it)
++ delete *it;
++ animation->channels.erase(remove_it, end_it);
++ }
++
++ static void deleteTree(QSSGSceneDesc::Node *node)
++ {
++ const auto children = flattenTree(node);
++ for (auto *animation : std::as_const(node->scene->animations))
++ removeFromAnimation(animation, children);
++ for (auto *child : children)
++ delete child;
++ }
++
++ static void removeProperty(QSSGSceneDesc::Node *node, const QByteArrayView &name)
++ {
++ auto *propList = &node->properties;
++
++ auto findName = [name](QSSGSceneDesc::Property *p) { return p->name == name; };
++ auto it = std::find_if(propList->begin(), propList->end(), findName);
++ if (it != propList->end()) {
++ QSSGSceneDesc::Property *p = *it;
++ propList->erase(it);
++ delete p;
++ }
++ }
++
++ static QSSGSceneDesc::Node *nodeFromJson(const QSSGSceneDesc::Scene *scene, const QJsonObject &nodeRef)
++ {
++ auto it = nodeRef.constBegin();
++ if (it == nodeRef.constEnd())
++ return nullptr;
++ auto nodeType = nodeTypeFromName(it.key().toUtf8());
++ auto nodeName = it.value().toString().toUtf8();
++ auto *node = findResource(scene, nodeName, nodeType);
++ if (!node)
++ node = findNode(scene->root, nodeName, nodeType);
++ return node;
++ }
++
++ static QSSGSceneDesc::NodeList *nodeListFromJson(const QSSGSceneDesc::Scene *scene, const QJsonArray &array)
++ {
++ QVarLengthArray<QSSGSceneDesc::Node *> nodes;
++
++ for (auto json : array) {
++ auto *node = nodeFromJson(scene, json.toObject());
++ if (!node) {
++ qWarning() << "Could not find node for" << json;
++ continue;
++ }
++ nodes.append(node);
++ }
++ auto *nodeList = new QSSGSceneDesc::NodeList(reinterpret_cast<void **>(nodes.data()), nodes.count());
++ return nodeList;
++ }
++
++ /*
++ JSON format
++
++ Node reference: {"<nodeTypeName>": "<name>"}
++ URL: {"url": "<filepath>"}
++ List: [ {"<nodeTypeName>": "<name>"}, ... ]
++ */
++
++ void setProperty(QSSGSceneDesc::Node *node, const QStringView propertyName, const QJsonValue &value)
++ {
++ QVariant var;
++
++ if (value.isArray()) {
++ var = QVariant::fromValue(nodeListFromJson(node->scene, value.toArray()));
++ } else if (value.isObject()) {
++ auto obj = value.toObject();
++ if (obj.contains(u"url")) {
++ auto path = obj.value(u"url").toString();
++ var = QVariant::fromValue(QUrl(path));
++ } else {
++ QSSGSceneDesc::Node *n = nodeFromJson(node->scene, obj);
++ var = QVariant::fromValue(n);
++ }
++ } else {
++ var = value.toVariant(); // The rest of the special handling happens in QSSGRuntimeUtils::applyPropertyValue
++ }
++
++ const auto name = propertyName.toUtf8();
++ removeProperty(node, name); // TODO: change property if it exists, instead of deleting and adding
++ auto *property = QSSGSceneDesc::setProperty(*node, name, std::move(var));
++
++ if (node->obj)
++ QSSGRuntimeUtils::applyPropertyValue(node, node->obj, property);
++ }
++
++
++ QSSGSceneDesc::Node *addResource(QSSGSceneDesc::Scene *scene, const QJsonObject &addition)
++ {
++ auto name = addition.value(u"name").toString().toUtf8();
++ auto typeName = addition.value(u"type").toString().toUtf8();
++ if (name.isEmpty() || typeName.isEmpty()) {
++ qWarning("Can't create node without name or type");
++ return nullptr;
++ }
++
++ QSSGSceneDesc::Node *node = nullptr;
++ QSSGSceneDesc::Node *prevResource = findResource(scene, name, nodeTypeFromName(typeName));
++
++ if (typeName == "Material") {
++ bool isSpecGlossy = addition.contains(u"albedoColor") || addition.contains(u"albedoMap")
++ || addition.contains(u"glossinessMap") || addition.contains(u"glossiness");
++ typeName = isSpecGlossy ? "SpecularGlossyMaterial" : "PrincipledMaterial";
++ }
++
++ if (typeName == "PrincipledMaterial") {
++ node = new QSSGSceneDesc::Node(name, QSSGSceneDesc::Node::Type::Material,
++ QSSGRenderGraphObject::Type::PrincipledMaterial);
++ } else if (typeName == "SpecularGlossyMaterial") {
++ node = new QSSGSceneDesc::Node(name, QSSGSceneDesc::Node::Type::Material,
++ QSSGRenderGraphObject::Type::SpecularGlossyMaterial);
++ } else if (typeName == "Texture") {
++ node = new QSSGSceneDesc::Node(name, QSSGSceneDesc::Node::Type::Texture,
++ QSSGRenderGraphObject::Type::Image2D);
++ } else {
++ qWarning() << "Not supported. Don't know how to create" << typeName;
++ return nullptr;
++ }
++ Q_ASSERT(node);
++ node->scene = scene;
++ for (auto it = addition.constBegin(); it != addition.constEnd(); ++it) {
++ const auto &propertyName = it.key();
++ if (propertyName == u"name" || propertyName == u"type" || propertyName == u"comment" || propertyName == u"command")
++ continue;
++ setProperty(node, it.key(), it.value());
++ }
++
++ if (prevResource) {
++ replaceReferencesToResource(scene->root, prevResource, node);
++ scene->resources.removeOne(prevResource);
++ delete prevResource;
++ }
++
++ QSSGSceneDesc::addNode(*scene, *node);
++ return node;
++ }
++
++ void applyEdit(QSSGSceneDesc::Scene *scene, const QJsonObject &changes)
++ {
++ auto doApply = [scene](const QJsonObject &obj) {
++ QByteArray name = obj.value(u"name").toString().toUtf8();
++ QByteArray typeName = obj.value(u"type").toString().toUtf8();
++ auto command = obj.value(u"command").toString(u"edit"_s);
++ auto nodeType = nodeTypeFromName(typeName);
++ if (command == u"edit") {
++ auto *node = findNode(scene->root, name, nodeType);
++ if (!node)
++ node = findResource(scene, name, nodeType);
++ if (node) {
++ for (auto it = obj.constBegin(); it != obj.constEnd(); ++it) {
++ const auto &propertyName = it.key();
++ if (propertyName == u"name" || propertyName == u"type" || propertyName == u"comment" || propertyName == u"command")
++ continue;
++ setProperty(node, it.key(), it.value());
++ }
++ }
++ } else if (command == u"add") {
++ addResource(scene, obj);
++ } else if (command == u"delete") {
++ QSSGSceneDesc::Node *parent = nullptr;
++ auto *node = findNode(scene->root, name, nodeType, &parent);
++ if (node) {
++ deleteTree(node);
++ if (parent)
++ unlinkChild(node, parent);
++ else
++ qWarning("Delete: could not find parent for node");
++ }
++ }
++ };
++
++ const auto editList = changes.value(u"editList").toArray();
++
++ // Do all the adds first, since the edits may depend on them
++ // If adds depend on each other, they need to be in dependency order
++ for (auto edit : editList) {
++ auto obj = edit.toObject();
++ if (obj.value(u"command") == u"add"_s)
++ doApply(obj);
++ }
++
++ for (auto edit : editList) {
++ auto obj = edit.toObject();
++ if (obj.value(u"command") != u"add"_s)
++ doApply(obj);
++ }
++
++ }
++
++ }
++
++ QT_END_NAMESPACE