diff --git a/docs/reference/changelog-r2025.md b/docs/reference/changelog-r2025.md index 91905c17870..0c73537266a 100644 --- a/docs/reference/changelog-r2025.md +++ b/docs/reference/changelog-r2025.md @@ -2,6 +2,7 @@ ## Webots R2025b - Enhancements + - `WbCamera`, `WbContactProperties`, `WbMotor`, and `WbSkin` will now locally-download their resources if-necessary (like other nodes) when steaming or exporting to `w3d` ([#6856](https://github.com/cyberbotics/webots/pull/6856)). - Added implementations of `wbu_system_tmpdir` and `wbu_system_webots_instance_path` to the MATLAB API ([#6756](https://github.com/cyberbotics/webots/pull/6756)). - Added missing import libraries on Windows ([#6753](https://github.com/cyberbotics/webots/pull/6753)). - Added some missing function definitions to the existing Windows libraries ([#6753](https://github.com/cyberbotics/webots/pull/6753)). @@ -11,6 +12,9 @@ - Fixed a bug preventing the `webots-controller` executable from running on arm-based mac devices ([#6806](https://github.com/cyberbotics/webots/pull/6806)). - Fixed a bug causing Webots to crash when multiple sounds were used with a [Speaker](speaker.md) node ([#6843](https://github.com/cyberbotics/webots/pull/6843)). - Fixed a bug causing the "Reload/Reset" buttons in the controller recompilation popup to not work on Windows ([#6844](https://github.com/cyberbotics/webots/pull/6844)). + - Fixed resolution of relative paths when converting protos to their base nodes ([#6856](https://github.com/cyberbotics/webots/pull/6856)). + - **As a result of this change, relative paths that are not within a Webots-recognized resource field (ex. `url`) will no longer be updated when a proto is converted to its base nodes.** This should not affect most users. + - Fixed a bug causing `TrackWheel` nodes to lose their field values when used in a proto converted to a base node ([#6856](https://github.com/cyberbotics/webots/pull/6856)). - Fixed a bug causing supervisors to occasionally read stale field values after the simulation was reset ([#6758](https://github.com/cyberbotics/webots/pull/6758)). - Fixed a bug causing Webots to occasionally crash when unloading a world ([#6857](https://github.com/cyberbotics/webots/pull/6857)). diff --git a/src/webots/nodes/WbBackground.cpp b/src/webots/nodes/WbBackground.cpp index 6b445a25665..e623cb0f79b 100644 --- a/src/webots/nodes/WbBackground.cpp +++ b/src/webots/nodes/WbBackground.cpp @@ -679,10 +679,7 @@ WbRgb WbBackground::skyColor() const { } void WbBackground::exportNodeFields(WbWriter &writer) const { - if (writer.isWebots()) { - WbBaseNode::exportNodeFields(writer); - return; - } + WbBaseNode::exportNodeFields(writer); if (writer.isW3d()) { QString backgroundFileNames[6]; @@ -690,18 +687,8 @@ void WbBackground::exportNodeFields(WbWriter &writer) const { if (mUrlFields[i]->size() == 0) continue; - WbField urlFieldCopy(*findField(gUrlNames(i), true)); - const QString &imagePath = WbUrl::computePath(this, gUrlNames(i), mUrlFields[i]->item(0)); - if (WbUrl::isLocalUrl(imagePath)) - backgroundFileNames[i] = WbUrl::computeLocalAssetUrl(imagePath, writer.isW3d()); - else if (WbUrl::isWeb(imagePath)) - backgroundFileNames[i] = imagePath; - else { - if (writer.isWritingToFile()) - backgroundFileNames[i] = WbUrl::exportResource(this, imagePath, imagePath, writer.relativeTexturesPath(), writer); - else - backgroundFileNames[i] = WbUrl::expressRelativeToWorld(imagePath); - } + const QString &resolvedURL = WbUrl::computePath(this, gUrlNames(i), mUrlFields[i], 0); + backgroundFileNames[i] = exportResource(mUrlFields[i]->item(0), resolvedURL, writer.relativeTexturesPath(), writer); } QString irradianceFileNames[6]; @@ -709,18 +696,9 @@ void WbBackground::exportNodeFields(WbWriter &writer) const { if (mIrradianceUrlFields[i]->size() == 0) continue; - const QString &irradiancePath = WbUrl::computePath(this, gIrradianceUrlNames(i), mIrradianceUrlFields[i]->item(0)); - if (WbUrl::isLocalUrl(irradiancePath)) - irradianceFileNames[i] = WbUrl::computeLocalAssetUrl(irradiancePath, writer.isW3d()); - else if (WbUrl::isWeb(irradiancePath)) - irradianceFileNames[i] = irradiancePath; - else { - if (writer.isWritingToFile()) - irradianceFileNames[i] = - WbUrl::exportResource(this, irradiancePath, irradiancePath, writer.relativeTexturesPath(), writer); - else - irradianceFileNames[i] = WbUrl::expressRelativeToWorld(irradiancePath); - } + const QString &resolvedURL = WbUrl::computePath(this, gIrradianceUrlNames(i), mIrradianceUrlFields[i], 0); + irradianceFileNames[i] = + exportResource(mIrradianceUrlFields[i]->item(0), resolvedURL, writer.relativeTexturesPath(), writer); } writer << " "; @@ -730,7 +708,19 @@ void WbBackground::exportNodeFields(WbWriter &writer) const { if (!irradianceFileNames[i].isEmpty()) writer << gIrradianceUrlNames(i) << "='\"" << irradianceFileNames[i] << "\"' "; } + } else { + for (int i = 0; i < 6; ++i) { + exportMFResourceField(gUrlNames(i), mUrlFields[i], writer.relativeTexturesPath(), writer); + exportMFResourceField(gIrradianceUrlNames(i), mIrradianceUrlFields[i], writer.relativeTexturesPath(), writer); + } } +} - WbBaseNode::exportNodeFields(writer); +QStringList WbBackground::customExportedFields() const { + QStringList fields; + for (int i = 0; i < 6; ++i) { + fields << gUrlNames(i); + fields << gIrradianceUrlNames(i); + } + return fields; } diff --git a/src/webots/nodes/WbBackground.hpp b/src/webots/nodes/WbBackground.hpp index a90c9ad4b1b..c6340596215 100644 --- a/src/webots/nodes/WbBackground.hpp +++ b/src/webots/nodes/WbBackground.hpp @@ -59,6 +59,7 @@ class WbBackground : public WbBaseNode { protected: void exportNodeFields(WbWriter &writer) const override; + QStringList customExportedFields() const override; private: static QList cBackgroundList; diff --git a/src/webots/nodes/WbCadShape.cpp b/src/webots/nodes/WbCadShape.cpp index aad3201001d..9b5b4dbd2a3 100644 --- a/src/webots/nodes/WbCadShape.cpp +++ b/src/webots/nodes/WbCadShape.cpp @@ -568,46 +568,31 @@ const WbVector3 WbCadShape::absoluteScale() const { } void WbCadShape::exportNodeFields(WbWriter &writer) const { - if (!(writer.isW3d() || writer.isProto())) - return; + WbBaseNode::exportNodeFields(writer); if (mUrl->size() == 0) return; - WbBaseNode::exportNodeFields(writer); + if (!(writer.isW3d() || writer.isProto())) { + findField("url", true)->write(writer); + return; + } // export model WbField urlFieldCopy(*findField("url", true)); for (int i = 0; i < mUrl->size(); ++i) { - const QString &completeUrl = WbUrl::computePath(this, "url", mUrl, i); + const QString &resolvedURL = WbUrl::computePath(this, "url", mUrl, i); WbMFString *urlFieldValue = dynamic_cast(urlFieldCopy.value()); - if (WbUrl::isLocalUrl(completeUrl)) - urlFieldValue->setItem(i, WbUrl::computeLocalAssetUrl(completeUrl, writer.isW3d())); - else if (WbUrl::isWeb(completeUrl)) - urlFieldValue->setItem(i, completeUrl); - else { - urlFieldValue->setItem( - i, writer.isWritingToFile() ? WbUrl::exportMesh(this, mUrl, i, writer) : WbUrl::expressRelativeToWorld(completeUrl)); - } + urlFieldValue->setItem(i, exportResource(mUrl->item(i), resolvedURL, writer.relativeMeshesPath(), writer)); } // export materials if (writer.isW3d()) { // only needs to be included in the w3d, when converting to base node it shouldn't be included - const QString &parentUrl = WbUrl::computePath(this, "url", mUrl->item(0)); + const QString &parentUrl = WbUrl::computePath(this, "url", mUrl, 0); for (const QString &material : objMaterialList(parentUrl)) { QString materialUrl = WbUrl::combinePaths(material, parentUrl); WbMFString *urlFieldValue = dynamic_cast(urlFieldCopy.value()); - if (WbUrl::isLocalUrl(materialUrl)) - urlFieldValue->addItem(WbUrl::computeLocalAssetUrl(materialUrl, writer.isW3d())); - else if (WbUrl::isWeb(materialUrl)) - urlFieldValue->addItem(materialUrl); - else { - if (writer.isWritingToFile()) - urlFieldValue->addItem( - WbUrl::exportResource(this, materialUrl, materialUrl, writer.relativeMeshesPath(), writer, false)); - else - urlFieldValue->addItem(WbUrl::expressRelativeToWorld(materialUrl)); - } + urlFieldValue->addItem(exportResource(material, materialUrl, writer.relativeMeshesPath(), writer)); } } @@ -618,6 +603,12 @@ void WbCadShape::exportNodeFields(WbWriter &writer) const { urlFieldCopy.write(writer); } +QStringList WbCadShape::customExportedFields() const { + QStringList fields; + fields << "url"; + return fields; +} + QString WbCadShape::cadPath() const { return WbUrl::computePath(this, "url", mUrl, 0); } diff --git a/src/webots/nodes/WbCadShape.hpp b/src/webots/nodes/WbCadShape.hpp index fd02124b011..924f4556541 100644 --- a/src/webots/nodes/WbCadShape.hpp +++ b/src/webots/nodes/WbCadShape.hpp @@ -56,6 +56,7 @@ class WbCadShape : public WbBaseNode { protected: void exportNodeFields(WbWriter &writer) const override; + QStringList customExportedFields() const override; WbBoundingSphere *boundingSphere() const override { return mBoundingSphere; } void recomputeBoundingSphere() const; diff --git a/src/webots/nodes/WbCamera.cpp b/src/webots/nodes/WbCamera.cpp index 63133128695..eaeb1384fb0 100644 --- a/src/webots/nodes/WbCamera.cpp +++ b/src/webots/nodes/WbCamera.cpp @@ -1017,6 +1017,18 @@ WbVector3 WbCamera::urdfRotation(const WbMatrix3 &rotationMatrix) const { return eulerRotation; } +void WbCamera::exportNodeFields(WbWriter &writer) const { + WbAbstractCamera::exportNodeFields(writer); + + exportSFResourceField("noiseMaskUrl", mNoiseMaskUrl, writer.relativeMeshesPath(), writer); +} + +QStringList WbCamera::customExportedFields() const { + QStringList fields; + fields << "noiseMaskUrl"; + return fields; +} + ///////////////////// // Update methods // ///////////////////// diff --git a/src/webots/nodes/WbCamera.hpp b/src/webots/nodes/WbCamera.hpp index 456a6c09ca2..b3086d5f32e 100644 --- a/src/webots/nodes/WbCamera.hpp +++ b/src/webots/nodes/WbCamera.hpp @@ -67,6 +67,8 @@ class WbCamera : public WbAbstractCamera { void setup() override; void render() override; bool needToRender() const override; + void exportNodeFields(WbWriter &writer) const override; + QStringList customExportedFields() const override; private: WbSFNode *mFocus; diff --git a/src/webots/nodes/WbContactProperties.cpp b/src/webots/nodes/WbContactProperties.cpp index d179d00b829..b5a7fa07e9c 100644 --- a/src/webots/nodes/WbContactProperties.cpp +++ b/src/webots/nodes/WbContactProperties.cpp @@ -121,6 +121,20 @@ void WbContactProperties::postFinalize() { connect(mMaxContactJoints, &WbSFInt::changed, this, &WbContactProperties::updateMaxContactJoints); } +void WbContactProperties::exportNodeFields(WbWriter &writer) const { + WbBaseNode::exportNodeFields(writer); + + exportSFResourceField(gUrlNames[0], mBumpSound, writer.relativeSoundsPath(), writer); + exportSFResourceField(gUrlNames[1], mRollSound, writer.relativeSoundsPath(), writer); + exportSFResourceField(gUrlNames[2], mSlideSound, writer.relativeSoundsPath(), writer); +} + +QStringList WbContactProperties::customExportedFields() const { + QStringList fields; + fields << "url"; + return fields; +} + void WbContactProperties::updateCoulombFriction() { const int nbElements = mCoulombFriction->size(); if (nbElements < 1 || nbElements > 4) { diff --git a/src/webots/nodes/WbContactProperties.hpp b/src/webots/nodes/WbContactProperties.hpp index 10b9386e41c..a3dce99978f 100644 --- a/src/webots/nodes/WbContactProperties.hpp +++ b/src/webots/nodes/WbContactProperties.hpp @@ -64,6 +64,10 @@ class WbContactProperties : public WbBaseNode { void valuesChanged(); void needToEnableBodies(); +protected: + void exportNodeFields(WbWriter &writer) const override; + QStringList customExportedFields() const override; + private: // user accessible fields WbSFString *mMaterial1; diff --git a/src/webots/nodes/WbImageTexture.cpp b/src/webots/nodes/WbImageTexture.cpp index be09007f51c..026f5da5e3a 100644 --- a/src/webots/nodes/WbImageTexture.cpp +++ b/src/webots/nodes/WbImageTexture.cpp @@ -528,24 +528,7 @@ bool WbImageTexture::exportNodeHeader(WbWriter &writer) const { void WbImageTexture::exportNodeFields(WbWriter &writer) const { WbBaseNode::exportNodeFields(writer); - // export to ./textures folder relative to writer path - WbField urlFieldCopy(*findField("url", true)); - for (int i = 0; i < mUrl->size(); ++i) { - QString completeUrl = WbUrl::computePath(this, "url", mUrl, i); - WbMFString *urlFieldValue = dynamic_cast(urlFieldCopy.value()); - if (WbUrl::isLocalUrl(completeUrl)) - urlFieldValue->setItem(i, WbUrl::computeLocalAssetUrl(completeUrl, writer.isW3d())); - else if (WbUrl::isWeb(completeUrl)) - urlFieldValue->setItem(i, completeUrl); - else { - if (writer.isWritingToFile()) - urlFieldValue->setItem(i, WbUrl::exportTexture(this, mUrl, i, writer)); - else - urlFieldValue->setItem(i, WbUrl::expressRelativeToWorld(completeUrl)); - } - } - - urlFieldCopy.write(writer); + exportMFResourceField("url", mUrl, writer.relativeTexturesPath(), writer); if (writer.isW3d()) { if (!mRole.isEmpty()) @@ -553,6 +536,12 @@ void WbImageTexture::exportNodeFields(WbWriter &writer) const { } } +QStringList WbImageTexture::customExportedFields() const { + QStringList fields; + fields << "url"; + return fields; +} + void WbImageTexture::exportShallowNode(const WbWriter &writer) const { if (!writer.isW3d() || mUrl->size() == 0) return; @@ -560,7 +549,7 @@ void WbImageTexture::exportShallowNode(const WbWriter &writer) const { // note: the texture of the shallow nodes needs to be exported only if the URL is locally defined but not of type // 'webots://' since this case would be converted to a remote one that targets the current branch if (!WbUrl::isWeb(mUrl->item(0)) && !WbUrl::isLocalUrl(mUrl->item(0)) && !WbWorld::isW3dStreaming()) - WbUrl::exportTexture(this, mUrl, 0, writer); + WbUrl::exportResource(this, mUrl->item(0), WbUrl::computePath(this, "url", mUrl, 0), writer.relativeTexturesPath(), writer); } QStringList WbImageTexture::fieldsToSynchronizeWithW3d() const { diff --git a/src/webots/nodes/WbImageTexture.hpp b/src/webots/nodes/WbImageTexture.hpp index 04bbde70135..b68e81e0073 100644 --- a/src/webots/nodes/WbImageTexture.hpp +++ b/src/webots/nodes/WbImageTexture.hpp @@ -80,6 +80,7 @@ class WbImageTexture : public WbBaseNode { protected: bool exportNodeHeader(WbWriter &writer) const override; void exportNodeFields(WbWriter &writer) const override; + QStringList customExportedFields() const override; private: // user accessible fields diff --git a/src/webots/nodes/WbMesh.cpp b/src/webots/nodes/WbMesh.cpp index f2cc552c65d..0d6623d5f10 100644 --- a/src/webots/nodes/WbMesh.cpp +++ b/src/webots/nodes/WbMesh.cpp @@ -401,31 +401,15 @@ void WbMesh::updateMaterialIndex() { } void WbMesh::exportNodeFields(WbWriter &writer) const { - if (!(writer.isW3d() || writer.isProto())) - return; - - if (mUrl->size() == 0) - return; - - WbField urlFieldCopy(*findField("url", true)); - for (int i = 0; i < mUrl->size(); ++i) { - const QString &completeUrl = WbUrl::computePath(this, "url", mUrl, i); - WbMFString *urlFieldValue = dynamic_cast(urlFieldCopy.value()); - if (WbUrl::isLocalUrl(completeUrl)) - urlFieldValue->setItem(i, WbUrl::computeLocalAssetUrl(completeUrl, writer.isW3d())); - else if (WbUrl::isWeb(completeUrl)) - urlFieldValue->setItem(i, completeUrl); - else { - if (writer.isWritingToFile()) - urlFieldValue->setItem(i, WbUrl::exportMesh(this, mUrl, i, writer)); - else - urlFieldValue->setItem(i, WbUrl::expressRelativeToWorld(completeUrl)); - } - } + WbGeometry::exportNodeFields(writer); - urlFieldCopy.write(writer); + exportMFResourceField("url", mUrl, writer.relativeMeshesPath(), writer); +} - WbGeometry::exportNodeFields(writer); +QStringList WbMesh::customExportedFields() const { + QStringList fields; + fields << "url"; + return fields; } QStringList WbMesh::fieldsToSynchronizeWithW3d() const { diff --git a/src/webots/nodes/WbMesh.hpp b/src/webots/nodes/WbMesh.hpp index 2de1cdf027b..67acd664b05 100644 --- a/src/webots/nodes/WbMesh.hpp +++ b/src/webots/nodes/WbMesh.hpp @@ -48,6 +48,7 @@ class WbMesh : public WbTriangleMeshGeometry { protected: void exportNodeFields(WbWriter &writer) const override; + QStringList customExportedFields() const override; private: // user accessible fields diff --git a/src/webots/nodes/WbMotor.cpp b/src/webots/nodes/WbMotor.cpp index d5b0dcfe4d0..19cfe3b2385 100644 --- a/src/webots/nodes/WbMotor.cpp +++ b/src/webots/nodes/WbMotor.cpp @@ -853,3 +853,15 @@ QList WbMotor::findClosestDescendantNodesWithDedicatedWrenNo list << static_cast(it.next()); return list; } + +void WbMotor::exportNodeFields(WbWriter &writer) const { + WbJointDevice::exportNodeFields(writer); + + exportSFResourceField("sound", mSound, writer.relativeSoundsPath(), writer); +} + +QStringList WbMotor::customExportedFields() const { + QStringList fields; + fields << "sound"; + return fields; +} diff --git a/src/webots/nodes/WbMotor.hpp b/src/webots/nodes/WbMotor.hpp index 9d7bc97a0c8..48f5452456a 100644 --- a/src/webots/nodes/WbMotor.hpp +++ b/src/webots/nodes/WbMotor.hpp @@ -100,6 +100,9 @@ class WbMotor : public WbJointDevice { void enableMotorFeedback(int rate); virtual double computeFeedback() const = 0; + void exportNodeFields(WbWriter &writer) const override; + QStringList customExportedFields() const override; + protected slots: void updateMaxForceOrTorque(); void updateMinAndMaxPosition(); diff --git a/src/webots/nodes/WbSkin.cpp b/src/webots/nodes/WbSkin.cpp index e8d9be9825b..25a782c8c20 100644 --- a/src/webots/nodes/WbSkin.cpp +++ b/src/webots/nodes/WbSkin.cpp @@ -527,6 +527,18 @@ void WbSkin::reset(const QString &id) { } } +void WbSkin::exportNodeFields(WbWriter &writer) const { + WbBaseNode::exportNodeFields(writer); + + exportSFResourceField("modelUrl", mModelUrl, writer.relativeMeshesPath(), writer); +} + +QStringList WbSkin::customExportedFields() const { + QStringList fields; + fields << "modelUrl"; + return fields; +} + void WbSkin::updateModel() { applyTranslationToWren(); applyRotationToWren(); diff --git a/src/webots/nodes/WbSkin.hpp b/src/webots/nodes/WbSkin.hpp index 340a3a6c506..b39edaef082 100644 --- a/src/webots/nodes/WbSkin.hpp +++ b/src/webots/nodes/WbSkin.hpp @@ -67,6 +67,10 @@ class WbSkin : public WbBaseNode, public WbAbstractPose, public WbDevice { signals: void wrenMaterialChanged(); +protected: + void exportNodeFields(WbWriter &writer) const override; + QStringList customExportedFields() const override; + private: WbSkin &operator=(const WbSkin &); // non copyable WbNode *clone() const override { return new WbSkin(*this); } diff --git a/src/webots/nodes/WbTrackWheel.cpp b/src/webots/nodes/WbTrackWheel.cpp index a6288a6e642..b10e3462766 100644 --- a/src/webots/nodes/WbTrackWheel.cpp +++ b/src/webots/nodes/WbTrackWheel.cpp @@ -106,9 +106,8 @@ QStringList WbTrackWheel::fieldsToSynchronizeWithW3d() const { } void WbTrackWheel::exportNodeFields(WbWriter &writer) const { - if (!writer.isW3d()) - return; - WbBaseNode::exportNodeFields(writer); - writer << " rotation=\'" << mRotation->value() << "\'"; + + if (writer.isW3d()) + writer << " rotation=\'" << mRotation->value() << "\'"; } diff --git a/src/webots/scene_tree/WbSceneTree.cpp b/src/webots/scene_tree/WbSceneTree.cpp index c04698b5306..71765c4def0 100644 --- a/src/webots/scene_tree/WbSceneTree.cpp +++ b/src/webots/scene_tree/WbSceneTree.cpp @@ -750,20 +750,6 @@ void WbSceneTree::convertProtoToBaseNode(bool rootOnly) { writer.setRootNode(NULL); currentNode->write(writer); - // relative urls that get exposed by the conversion need to be changed to remote ones - QRegularExpressionMatchIterator it = WbUrl::vrmlResourceRegex().globalMatch(nodeString); - while (it.hasNext()) { - const QRegularExpressionMatch match = it.next(); - if (match.hasMatch()) { - QString asset = match.captured(0); - asset.replace("\"", ""); - if (!WbUrl::isWeb(asset) && QDir::isRelativePath(asset)) { - QString newUrl = QString("\"%1\"").arg(WbUrl::combinePaths(asset, currentNode->proto()->url())); - nodeString.replace(QString("\"%1\"").arg(asset), newUrl.replace(WbStandardPaths::webotsHomePath(), "webots://")); - } - } - } - const bool skipTemplateRegeneration = WbVrmlNodeUtilities::findUpperTemplateNeedingRegenerationFromField(parentField, parentNode); if (skipTemplateRegeneration) diff --git a/src/webots/vrml/WbNode.cpp b/src/webots/vrml/WbNode.cpp index 5648120bdd9..685e59ee0e4 100644 --- a/src/webots/vrml/WbNode.cpp +++ b/src/webots/vrml/WbNode.cpp @@ -1112,7 +1112,8 @@ void WbNode::exportNodeFields(WbWriter &writer) const { return; foreach (const WbField *f, fields()) { - if (!f->isDeprecated() && ((f->isW3d() || writer.isProto()) && f->singleType() != WB_SF_NODE)) + if (!f->isDeprecated() && ((f->isW3d() || writer.isProto()) && f->singleType() != WB_SF_NODE) && + !customExportedFields().contains(f->name())) f->write(writer); } } @@ -1245,6 +1246,70 @@ void WbNode::writeExport(WbWriter &writer) const { } } +QString WbNode::exportResource(const QString &rawURL, const QString &resolvedURL, const QString &relativeResourcePath, + const WbWriter &writer) const { + if (WbUrl::isLocalUrl(resolvedURL)) + return WbUrl::computeLocalAssetUrl(resolvedURL, writer.isW3d()); + else if (WbUrl::isWeb(resolvedURL)) + return resolvedURL; + else { + if (writer.isWritingToFile()) + return WbUrl::exportResource(this, rawURL, resolvedURL, relativeResourcePath, writer); + else + return WbUrl::expressRelativeToWorld(resolvedURL); + } +} + +void WbNode::exportMFResourceField(const QString &fieldName, const WbMFString *value, const QString &relativeResourcePath, + WbWriter &writer) const { + const WbField *originalField = findField(fieldName, true); + assert(originalField && originalField->type() == WB_MF_STRING); + + // only w3c and proto exports need urls to be resolved + if (!(writer.isW3d() || writer.isProto())) { + originalField->write(writer); + return; + } + + if (value->size() == 0) + return; + + WbField copiedField(*originalField); + WbMFString *newValue = dynamic_cast(copiedField.value()); + + for (int i = 0; i < value->size(); ++i) { + const QString &rawURL = value->item(i); + const QString &resolvedURL = WbUrl::computePath(this, fieldName, rawURL); + newValue->setItem(i, exportResource(rawURL, resolvedURL, relativeResourcePath, writer)); + } + + copiedField.write(writer); +} + +void WbNode::exportSFResourceField(const QString &fieldName, const WbSFString *value, const QString &relativeResourcePath, + WbWriter &writer) const { + const WbField *originalField = findField(fieldName, true); + assert(originalField && originalField->type() == WB_SF_STRING); + + // only w3c and proto exports need urls to be resolved + if (!(writer.isW3d() || writer.isProto())) { + originalField->write(writer); + return; + } + + const QString &rawURL = value->value(); + if (rawURL.isEmpty()) + return; + + WbField copiedField(*originalField); + WbSFString *newValue = dynamic_cast(copiedField.value()); + + const QString &resolvedURL = WbUrl::computePath(this, fieldName, rawURL); + newValue->setValue(exportResource(rawURL, resolvedURL, relativeResourcePath, writer)); + + copiedField.write(writer); +} + bool WbNode::operator==(const WbNode &other) const { if (mModel != other.mModel || isProtoInstance() != other.isProtoInstance() || (mProto && mProto->url() != other.mProto->url()) || mDefName != other.mDefName) diff --git a/src/webots/vrml/WbNode.hpp b/src/webots/vrml/WbNode.hpp index db1bc282f3e..ed95147a369 100644 --- a/src/webots/vrml/WbNode.hpp +++ b/src/webots/vrml/WbNode.hpp @@ -319,6 +319,18 @@ class WbNode : public QObject { virtual void exportNodeFooter(WbWriter &writer) const; virtual void exportExternalSubProto(WbWriter &writer) const; + // fields which should not be handled by the default exportNodeFields function + virtual QStringList customExportedFields() const { return QStringList(); } + + // Helper to handle exporting fields with resources which reference external files + QString exportResource(const QString &rawURL, const QString &resolvedURL, const QString &relativeResourcePath, + const WbWriter &writer) const; + // Wrappers for the most common use case (simply exporting a field with no additional processing) + void exportMFResourceField(const QString &fieldName, const WbMFString *value, const QString &relativeResourcePath, + WbWriter &writer) const; + void exportSFResourceField(const QString &fieldName, const WbSFString *value, const QString &relativeResourcePath, + WbWriter &writer) const; + // Methods related to URDF export const WbNode *findUrdfLinkRoot() const; // Finds first upper Webots node that is considered as URDF link virtual bool isUrdfRootLink() const; // Determines whether the Webots node is considered as URDF link as well diff --git a/src/webots/vrml/WbUrl.cpp b/src/webots/vrml/WbUrl.cpp index d126f6696e4..e996024276f 100644 --- a/src/webots/vrml/WbUrl.cpp +++ b/src/webots/vrml/WbUrl.cpp @@ -85,6 +85,7 @@ QString WbUrl::computePath(const WbNode *node, const QString &field, const QStri if (QDir::isRelativePath(url)) { const WbField *f = node->findField(field, true); + assert(f); const WbNode *protoNode = node->containingProto(false); protoNode = WbVrmlNodeUtilities::findFieldProtoScope(f, protoNode); @@ -132,7 +133,10 @@ QString WbUrl::resolveUrl(const QString &rawUrl) { } QString WbUrl::exportResource(const WbNode *node, const QString &url, const QString &sourcePath, - const QString &relativeResourcePath, const WbWriter &writer, const bool isTexture) { + const QString &relativeResourcePath, const WbWriter &writer) { + // in addition to writing the node, we want to ensure that the resource file exists + // at the expected location. If not, we should copy it, possibly creating the expected + // directory structure. const QFileInfo urlFileInfo(url); const QString fileName = urlFileInfo.fileName(); const QString expectedRelativePath = relativeResourcePath + fileName; @@ -155,11 +159,7 @@ QString WbUrl::exportResource(const WbNode *node, const QString &url, const QStr const QString extension = urlFileInfo.suffix(); for (int i = 1; i < 100; ++i) { // number of trials before failure - QString newRelativePath; - if (isTexture) - newRelativePath = writer.relativeTexturesPath() + baseName + '.' + QString::number(i) + '.' + extension; - else - newRelativePath = writer.relativeMeshesPath() + baseName + '.' + QString::number(i) + '.' + extension; + QString newRelativePath = relativeResourcePath + baseName + '.' + QString::number(i) + '.' + extension; const QString newAbsolutePath = writer.path() + "/" + newRelativePath; if (QFileInfo(newAbsolutePath).exists()) { @@ -170,11 +170,8 @@ QString WbUrl::exportResource(const WbNode *node, const QString &url, const QStr return newRelativePath; } } - if (isTexture) - node->warn(QObject::tr("Failure exporting texture, too many textures share the same name: %1.").arg(url)); - else - node->warn(QObject::tr("Failure exporting mesh, too many meshes share the same name: %1.").arg(url)); + node->warn(QObject::tr("Failure exporting resource, too many resources share the same name: %1.").arg(url)); return ""; } } else { // simple case @@ -183,22 +180,6 @@ QString WbUrl::exportResource(const WbNode *node, const QString &url, const QStr } } -QString WbUrl::exportTexture(const WbNode *node, const WbMFString *urlField, int index, const WbWriter &writer) { - // in addition to writing the node, we want to ensure that the texture file exists - // at the expected location. If not, we should copy it, possibly creating the expected - // directory structure. - return exportResource(node, QDir::fromNativeSeparators(urlField->item(index)), computePath(node, "url", urlField, index), - writer.relativeTexturesPath(), writer); -} - -QString WbUrl::exportMesh(const WbNode *node, const WbMFString *urlField, int index, const WbWriter &writer) { - // in addition to writing the node, we want to ensure that the mesh file exists - // at the expected location. If not, we should copy it, possibly creating the expected - // directory structure. - return exportResource(node, QDir::fromNativeSeparators(urlField->item(index)), computePath(node, "url", urlField, index), - writer.relativeMeshesPath(), writer, false); -} - bool WbUrl::isWeb(const QString &url) { return url.startsWith("https://") || url.startsWith("http://"); } diff --git a/src/webots/vrml/WbUrl.hpp b/src/webots/vrml/WbUrl.hpp index a52cf10a501..48fa8753b82 100644 --- a/src/webots/vrml/WbUrl.hpp +++ b/src/webots/vrml/WbUrl.hpp @@ -30,9 +30,7 @@ namespace WbUrl { QString combinePaths(const QString &rawUrl, const QString &rawParentUrl); QString exportResource(const WbNode *node, const QString &url, const QString &sourcePath, const QString &relativeResourcePath, - const WbWriter &writer, const bool isTexture = true); - QString exportTexture(const WbNode *node, const WbMFString *urlField, int index, const WbWriter &writer); - QString exportMesh(const WbNode *node, const WbMFString *urlField, int index, const WbWriter &writer); + const WbWriter &writer); QString missing(const QString &url); const QString &missingTexture(); diff --git a/src/webots/vrml/WbWriter.hpp b/src/webots/vrml/WbWriter.hpp index 408e3d905d3..c766d9b383f 100644 --- a/src/webots/vrml/WbWriter.hpp +++ b/src/webots/vrml/WbWriter.hpp @@ -91,6 +91,7 @@ class WbWriter { static QString relativeTexturesPath() { return "textures/"; } static QString relativeMeshesPath() { return "meshes/"; } + static QString relativeSoundsPath() { return "sounds/"; } private: void setType();