1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "qssgsceneedit_p.h"
5
6#include <QtGui/QGuiApplication>
7
8#include <QtCore/QVariant>
9#include <QtCore/QHash>
10#include <QtCore/QMetaProperty>
11#include <QtCore/QUrl>
12
13#include <QtCore/QJsonObject>
14#include <QtCore/QJsonDocument>
15#include <QtCore/QJsonArray>
16
17#include <QtQuick3DAssetImport/private/qssgassetimportmanager_p.h>
18#include <QtQuick3DAssetUtils/private/qssgscenedesc_p.h>
19#include <QtQuick3DAssetUtils/private/qssgqmlutilities_p.h>
20#include <QtQuick3DAssetUtils/private/qssgrtutilities_p.h>
21
22QT_BEGIN_NAMESPACE
23using namespace Qt::Literals::StringLiterals;
24namespace QSSGQmlUtilities {
25
26static const char* typeNames[] =
27{
28 "Transform",
29 "Camera",
30 "Model",
31 "Texture",
32 "Material",
33 "Light",
34 "Mesh",
35 "Skin",
36 "Skeleton",
37 "Joint",
38 "MorphTarget",
39 "ERROR"
40};
41
42static constexpr qsizetype nNodeTypes = std::size(typeNames) - 1;
43
44static QSSGSceneDesc::Node::Type nodeTypeFromName(const QByteArrayView &typeName)
45{
46 int i = 0;
47 while (i < nNodeTypes) {
48 if (typeName == typeNames[i])
49 break;
50 ++i;
51 }
52 return QSSGSceneDesc::Node::Type(i);
53}
54
55static void replaceReferencesToResource(QSSGSceneDesc::Node *node, QSSGSceneDesc::Node *resource, QSSGSceneDesc::Node *replacement)
56{
57 for (auto *prop : node->properties) {
58 auto &val = prop->value;
59 if (qvariant_cast<QSSGSceneDesc::Node *>(v: val) == resource) {
60 if (replacement)
61 val = QVariant::fromValue(value: replacement);
62 }
63 if (val.metaType().id() == qMetaTypeId<QSSGSceneDesc::NodeList *>()) {
64 const auto &list = *qvariant_cast<QSSGSceneDesc::NodeList *>(v: val);
65 for (int i = 0, end = list.count; i != end; ++i) {
66 if (list.head[i] == resource) {
67 list.head[i] = replacement;
68 }
69 }
70 }
71 }
72 for (auto *child : node->children)
73 replaceReferencesToResource(node: child, resource, replacement);
74}
75
76// TODO: optimize this by using a hashmap or similar
77static QSSGSceneDesc::Node *findNode(QSSGSceneDesc::Node *root, const QByteArrayView name,
78 QSSGSceneDesc::Node::Type type, QSSGSceneDesc::Node **parent = nullptr)
79{
80 if (!root || name.isEmpty())
81 return nullptr;
82
83 if (root->name == name && root->nodeType == type)
84 return root;
85
86 for (auto *child : root->children) {
87 if (auto *ret = findNode(root: child, name, type, parent)) {
88 if (parent && !*parent)
89 *parent = root;
90 return ret;
91 }
92 }
93 return nullptr;
94}
95
96static QSSGSceneDesc::Node *findResource(const QSSGSceneDesc::Scene *scene, const QByteArrayView &name, QSSGSceneDesc::Node::Type nodeType)
97{
98 if (name.isEmpty())
99 return nullptr; // Empty strings by definition means nothing
100 for (auto *resource : scene->resources) {
101 if (resource->name == name && resource->nodeType == nodeType)
102 return resource;
103 }
104
105 return nullptr;
106}
107
108using NodeSet = QSet<QSSGSceneDesc::Node *>;
109typedef bool NodeFilter(QSSGSceneDesc::Node *);
110
111static NodeSet flattenTree(QSSGSceneDesc::Node *node, NodeFilter *excludeFunction = nullptr)
112{
113 NodeSet ret = { node };
114 for (auto *child : node->children)
115 if (!excludeFunction || !excludeFunction(child))
116 ret.unite(other: flattenTree(node: child));
117 return ret;
118}
119
120static void unlinkChild(QSSGSceneDesc::Node *child, QSSGSceneDesc::Node *parent)
121{
122 parent->children.removeOne(t: child);
123}
124
125static void removeFromAnimation(QSSGSceneDesc::Animation *animation, const NodeSet &nodes)
126{
127 auto isTargeted = [nodes](QSSGSceneDesc::Animation::Channel *channel) { return nodes.contains(value: channel->target); };
128 const auto end_it = animation->channels.end();
129 auto remove_it = std::remove_if(first: animation->channels.begin(), last: end_it, pred: isTargeted);
130 for (auto it = remove_it; it != end_it; ++it)
131 delete *it;
132 animation->channels.erase(abegin: remove_it, aend: end_it);
133}
134
135static void deleteTree(QSSGSceneDesc::Node *node)
136{
137 const auto children = flattenTree(node);
138 for (auto *animation : node->scene->animations)
139 removeFromAnimation(animation, nodes: children);
140 for (auto *child : children)
141 delete child;
142}
143
144static void removeProperty(QSSGSceneDesc::Node *node, const QByteArrayView &name)
145{
146 auto *propList = &node->properties;
147
148 auto findName = [name](QSSGSceneDesc::Property *p) { return p->name == name; };
149 auto it = std::find_if(first: propList->begin(), last: propList->end(), pred: findName);
150 if (it != propList->end()) {
151 QSSGSceneDesc::Property *p = *it;
152 propList->erase(pos: it);
153 delete p;
154 }
155}
156
157static QSSGSceneDesc::Node *nodeFromJson(const QSSGSceneDesc::Scene *scene, const QJsonObject &nodeRef)
158{
159 auto it = nodeRef.constBegin();
160 if (it == nodeRef.constEnd())
161 return nullptr;
162 auto nodeType = nodeTypeFromName(typeName: it.key().toUtf8());
163 auto nodeName = it.value().toString().toUtf8();
164 auto *node = findResource(scene, name: nodeName, nodeType);
165 if (!node)
166 node = findNode(root: scene->root, name: nodeName, type: nodeType);
167 return node;
168}
169
170static QSSGSceneDesc::NodeList *nodeListFromJson(const QSSGSceneDesc::Scene *scene, const QJsonArray &array)
171{
172 QVarLengthArray<QSSGSceneDesc::Node *> nodes;
173
174 for (auto json : array) {
175 auto *node = nodeFromJson(scene, nodeRef: json.toObject());
176 if (!node) {
177 qWarning() << "Could not find node for" << json;
178 continue;
179 }
180 nodes.append(t: node);
181 }
182 auto *nodeList = new QSSGSceneDesc::NodeList(reinterpret_cast<void **>(nodes.data()), nodes.count());
183 return nodeList;
184}
185
186/*
187 JSON format
188
189 Node reference: {"<nodeTypeName>": "<name>"}
190 URL: {"url": "<filepath>"}
191 List: [ {"<nodeTypeName>": "<name>"}, ... ]
192 */
193
194void setProperty(QSSGSceneDesc::Node *node, const QStringView propertyName, const QJsonValue &value)
195{
196 QVariant var;
197
198 if (value.isArray()) {
199 var = QVariant::fromValue(value: nodeListFromJson(scene: node->scene, array: value.toArray()));
200 } else if (value.isObject()) {
201 auto obj = value.toObject();
202 if (obj.contains(key: u"url")) {
203 auto path = obj.value(key: u"url").toString();
204 var = QVariant::fromValue(value: QUrl(path));
205 } else {
206 QSSGSceneDesc::Node *n = nodeFromJson(scene: node->scene, nodeRef: obj);
207 var = QVariant::fromValue(value: n);
208 }
209 } else {
210 var = value.toVariant(); // The rest of the special handling happens in QSSGRuntimeUtils::applyPropertyValue
211 }
212
213 const auto name = propertyName.toUtf8();
214 removeProperty(node, name); // TODO: change property if it exists, instead of deleting and adding
215 auto *property = QSSGSceneDesc::setProperty(node&: *node, name, value: std::move(var));
216
217 if (node->obj)
218 QSSGRuntimeUtils::applyPropertyValue(node, obj: node->obj, property);
219}
220
221
222QSSGSceneDesc::Node *addResource(QSSGSceneDesc::Scene *scene, const QJsonObject &addition)
223{
224 auto name = addition.value(key: u"name").toString().toUtf8();
225 auto typeName = addition.value(key: u"type").toString().toUtf8();
226 if (name.isEmpty() || typeName.isEmpty()) {
227 qWarning(msg: "Can't create node without name or type");
228 return nullptr;
229 }
230
231 QSSGSceneDesc::Node *node = nullptr;
232 QSSGSceneDesc::Node *prevResource = findResource(scene, name, nodeType: nodeTypeFromName(typeName));
233
234 if (typeName == "Material") {
235 bool isSpecGlossy = addition.contains(key: u"albedoColor") || addition.contains(key: u"albedoMap")
236 || addition.contains(key: u"glossinessMap") || addition.contains(key: u"glossiness");
237 typeName = isSpecGlossy ? "SpecularGlossyMaterial" : "PrincipledMaterial";
238 }
239
240 if (typeName == "PrincipledMaterial") {
241 node = new QSSGSceneDesc::Node(name, QSSGSceneDesc::Node::Type::Material,
242 QSSGRenderGraphObject::Type::PrincipledMaterial);
243 } else if (typeName == "SpecularGlossyMaterial") {
244 node = new QSSGSceneDesc::Node(name, QSSGSceneDesc::Node::Type::Material,
245 QSSGRenderGraphObject::Type::SpecularGlossyMaterial);
246 } else if (typeName == "Texture") {
247 node = new QSSGSceneDesc::Node(name, QSSGSceneDesc::Node::Type::Texture,
248 QSSGRenderGraphObject::Type::Image2D);
249 } else {
250 qWarning() << "Not supported. Don't know how to create" << typeName;
251 return nullptr;
252 }
253 Q_ASSERT(node);
254 node->scene = scene;
255 for (auto it = addition.constBegin(); it != addition.constEnd(); ++it) {
256 const auto &propertyName = it.key();
257 if (propertyName == u"name" || propertyName == u"type" || propertyName == u"comment" || propertyName == u"command")
258 continue;
259 setProperty(node, propertyName: it.key(), value: it.value());
260 }
261
262 if (prevResource) {
263 replaceReferencesToResource(node: scene->root, resource: prevResource, replacement: node);
264 scene->resources.removeOne(t: prevResource);
265 delete prevResource;
266 }
267
268 QSSGSceneDesc::addNode(scene&: *scene, node&: *node);
269 return node;
270}
271
272void applyEdit(QSSGSceneDesc::Scene *scene, const QJsonObject &changes)
273{
274 auto doApply = [scene](const QJsonObject &obj) {
275 QByteArray name = obj.value(key: u"name").toString().toUtf8();
276 QByteArray typeName = obj.value(key: u"type").toString().toUtf8();
277 auto command = obj.value(key: u"command").toString(defaultValue: u"edit"_s);
278 auto nodeType = nodeTypeFromName(typeName);
279 if (command == u"edit") {
280 auto *node = findNode(root: scene->root, name, type: nodeType);
281 if (!node)
282 node = findResource(scene, name, nodeType);
283 if (node) {
284 for (auto it = obj.constBegin(); it != obj.constEnd(); ++it) {
285 const auto &propertyName = it.key();
286 if (propertyName == u"name" || propertyName == u"type" || propertyName == u"comment" || propertyName == u"command")
287 continue;
288 setProperty(node, propertyName: it.key(), value: it.value());
289 }
290 }
291 } else if (command == u"add") {
292 addResource(scene, addition: obj);
293 } else if (command == u"delete") {
294 QSSGSceneDesc::Node *parent = nullptr;
295 auto *node = findNode(root: scene->root, name, type: nodeType, parent: &parent);
296 if (node) {
297 deleteTree(node);
298 if (parent)
299 unlinkChild(child: node, parent);
300 else
301 qWarning(msg: "Delete: could not find parent for node");
302 }
303 }
304 };
305
306 const auto editList = changes.value(key: u"editList").toArray();
307
308 // Do all the adds first, since the edits may depend on them
309 // If adds depend on each other, they need to be in dependency order
310 for (auto edit : editList) {
311 auto obj = edit.toObject();
312 if (obj.value(key: u"command") == u"add"_s)
313 doApply(obj);
314 }
315
316 for (auto edit : editList) {
317 auto obj = edit.toObject();
318 if (obj.value(key: u"command") != u"add"_s)
319 doApply(obj);
320 }
321
322}
323
324}
325
326QT_END_NAMESPACE
327

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