// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "qssgsceneedit_p.h" #include #include #include #include #include #include #include #include #include #include #include #include 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 : node->properties) { auto &val = prop->value; if (qvariant_cast(val) == resource) { if (replacement) val = QVariant::fromValue(replacement); } if (val.metaType().id() == qMetaTypeId()) { const auto &list = *qvariant_cast(val); for (int i = 0, end = list.count; i != end; ++i) { if (list.head[i] == resource) { list.head[i] = replacement; } } } } for (auto *child : 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 : 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; typedef bool NodeFilter(QSSGSceneDesc::Node *); static NodeSet flattenTree(QSSGSceneDesc::Node *node, NodeFilter *excludeFunction = nullptr) { NodeSet ret = { node }; for (auto *child : 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 : 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 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(nodes.data()), nodes.count()); return nodeList; } /* JSON format Node reference: {"": ""} URL: {"url": ""} List: [ {"": ""}, ... ] */ 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