/** * @file llfloatergltfasseteditor.cpp * @author Andrii Kleshchev * @brief LLFloaterGltfAssetEditor class implementation * * $LicenseInfo:firstyear=2024&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2024, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License only. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ #include "llviewerprecompiledheaders.h" #include "llfloatergltfasseteditor.h" #include "gltf/asset.h" #include "llcallbacklist.h" #include "llmenubutton.h" #include "llselectmgr.h" #include "llspinctrl.h" #include "llviewerobject.h" const LLColor4U DEFAULT_WHITE(255, 255, 255); /// LLFloaterGLTFAssetEditor LLFloaterGLTFAssetEditor::LLFloaterGLTFAssetEditor(const LLSD& key) : LLFloater(key) , mUIColor(LLUIColorTable::instance().getColor("MenuItemEnabledColor", DEFAULT_WHITE)) { setTitle("GLTF Asset Editor (WIP)"); mCommitCallbackRegistrar.add("PanelObject.menuDoToSelected", [this](LLUICtrl* ctrl, const LLSD& data) { onMenuDoToSelected(data); }); mEnableCallbackRegistrar.add("PanelObject.menuEnable", [this](LLUICtrl* ctrl, const LLSD& data) { return onMenuEnableItem(data); }); } LLFloaterGLTFAssetEditor::~LLFloaterGLTFAssetEditor() { if (mScroller) { mItemListPanel->removeChild(mScroller); delete mScroller; mScroller = NULL; } } bool LLFloaterGLTFAssetEditor::postBuild() { // Position mMenuClipboardPos = getChild("clipboard_pos_btn"); mCtrlPosX = getChild("Pos X", true); mCtrlPosX->setCommitCallback([this](LLUICtrl* ctrl, const LLSD& param) { onCommitTransform(); }); mCtrlPosY = getChild("Pos Y", true); mCtrlPosY->setCommitCallback([this](LLUICtrl* ctrl, const LLSD& param) { onCommitTransform(); }); mCtrlPosZ = getChild("Pos Z", true); mCtrlPosZ->setCommitCallback([this](LLUICtrl* ctrl, const LLSD& param) { onCommitTransform(); }); // Scale mMenuClipboardScale = getChild("clipboard_size_btn"); mCtrlScaleX = getChild("Scale X", true); mCtrlScaleX->setCommitCallback([this](LLUICtrl* ctrl, const LLSD& param) { onCommitTransform(); }); mCtrlScaleY = getChild("Scale Y", true); mCtrlScaleY->setCommitCallback([this](LLUICtrl* ctrl, const LLSD& param) { onCommitTransform(); }); mCtrlScaleZ = getChild("Scale Z", true); mCtrlScaleZ->setCommitCallback([this](LLUICtrl* ctrl, const LLSD& param) { onCommitTransform(); }); // Rotation mMenuClipboardRot = getChild("clipboard_rot_btn"); mCtrlRotX = getChild("Rot X", true); mCtrlRotX->setCommitCallback([this](LLUICtrl* ctrl, const LLSD& param) { onCommitTransform(); }); mCtrlRotY = getChild("Rot Y", true); mCtrlRotY->setCommitCallback([this](LLUICtrl* ctrl, const LLSD& param) { onCommitTransform(); }); mCtrlRotZ = getChild("Rot Z", true); mCtrlPosZ->setCommitCallback([this](LLUICtrl* ctrl, const LLSD& param) { onCommitTransform(); }); setTransformsEnabled(false); // todo: do multiple panels based on selected element. mTransformsPanel = getChild("transform_panel", true); mTransformsPanel->setVisible(false); mItemListPanel = getChild("item_list_panel", true); initFolderRoot(); return true; } void LLFloaterGLTFAssetEditor::initFolderRoot() { if (mScroller || mFolderRoot) { LL_ERRS() << "Folder root already initialized" << LL_ENDL; return; } LLRect scroller_view_rect = mItemListPanel->getRect(); scroller_view_rect.translate(-scroller_view_rect.mLeft, -scroller_view_rect.mBottom); LLScrollContainer::Params scroller_params(LLUICtrlFactory::getDefaultParams()); scroller_params.rect(scroller_view_rect); scroller_params.name("folder_scroller"); mScroller = LLUICtrlFactory::create(scroller_params); mScroller->setFollowsAll(); // Insert that scroller into the panel widgets hierarchy mItemListPanel->addChild(mScroller); // Create the root model LLGLTFFolderItem* base_item = new LLGLTFFolderItem(mGLTFViewModel); LLFolderView::Params p(LLUICtrlFactory::getDefaultParams()); p.name = "Root"; p.title = "Root"; p.rect = LLRect(0, 0, getRect().getWidth(), 0); p.parent_panel = mItemListPanel; p.tool_tip = p.name; p.listener = base_item; p.view_model = &mGLTFViewModel; p.root = NULL; p.use_ellipses = true; p.options_menu = "menu_gltf.xml"; // *TODO : create this or fix to be optional mFolderRoot = LLUICtrlFactory::create(p); mFolderRoot->setCallbackRegistrar(&mCommitCallbackRegistrar); mFolderRoot->setEnableRegistrar(&mEnableCallbackRegistrar); // Attach root to the scroller mScroller->addChild(mFolderRoot); mFolderRoot->setScrollContainer(mScroller); mFolderRoot->setFollowsAll(); mFolderRoot->setOpen(true); mFolderRoot->setSelectCallback([this](const std::deque& items, bool user_action) { onFolderSelectionChanged(items, user_action); }); mScroller->setVisible(true); } void LLFloaterGLTFAssetEditor::onOpen(const LLSD& key) { gIdleCallbacks.addFunction(idle, this); loadFromSelection(); } void LLFloaterGLTFAssetEditor::onClose(bool app_quitting) { gIdleCallbacks.deleteFunction(idle, this); mAsset = nullptr; mObject = nullptr; } void LLFloaterGLTFAssetEditor::clearRoot() { LLFolderViewFolder::folders_t::iterator folders_it = mFolderRoot->getFoldersBegin(); while (folders_it != mFolderRoot->getFoldersEnd()) { (*folders_it)->destroyView(); folders_it = mFolderRoot->getFoldersBegin(); } mNodeToItemMap.clear(); } void LLFloaterGLTFAssetEditor::idle(void* user_data) { LLFloaterGLTFAssetEditor* floater = (LLFloaterGLTFAssetEditor*)user_data; if (floater->mFolderRoot) { floater->mFolderRoot->update(); } } void LLFloaterGLTFAssetEditor::loadItem(S32 id, const std::string& name, LLGLTFFolderItem::EType type, LLFolderViewFolder* parent) { LLGLTFFolderItem* listener = new LLGLTFFolderItem(id, name, type, mGLTFViewModel); LLFolderViewItem::Params params; params.name(name); params.creation_date(0); params.root(mFolderRoot); params.listener(listener); params.rect(LLRect()); params.tool_tip = params.name; params.font_color = mUIColor; params.font_highlight_color = mUIColor; LLFolderViewItem* view = LLUICtrlFactory::create(params); view->addToFolder(parent); view->setVisible(true); } void LLFloaterGLTFAssetEditor::loadFromNode(S32 node_id, LLFolderViewFolder* parent) { if (mAsset->mNodes.size() <= node_id) { return; } LL::GLTF::Node& node = mAsset->mNodes[node_id]; std::string name = node.mName; if (node.mName.empty()) { name = getString("node_tittle"); } else { name = node.mName; } LLGLTFFolderItem* listener = new LLGLTFFolderItem(node_id, name, LLGLTFFolderItem::TYPE_NODE, mGLTFViewModel); LLFolderViewFolder::Params p; p.root = mFolderRoot; p.listener = listener; p.name = name; p.tool_tip = name; p.font_color = mUIColor; p.font_highlight_color = mUIColor; LLFolderViewFolder* view = LLUICtrlFactory::create(p); view->addToFolder(parent); view->setVisible(true); view->setOpen(true); mNodeToItemMap[node_id] = view; for (S32& node_id : node.mChildren) { loadFromNode(node_id, view); } if (node.mMesh != LL::GLTF::INVALID_INDEX && mAsset->mMeshes.size() > node.mMesh) { std::string name = mAsset->mMeshes[node.mMesh].mName; if (name.empty()) { name = getString("mesh_tittle"); } loadItem(node.mMesh, name, LLGLTFFolderItem::TYPE_MESH, view); } if (node.mSkin != LL::GLTF::INVALID_INDEX && mAsset->mSkins.size() > node.mSkin) { std::string name = mAsset->mSkins[node.mSkin].mName; if (name.empty()) { name = getString("skin_tittle"); } loadItem(node.mSkin, name, LLGLTFFolderItem::TYPE_SKIN, view); } view->setChildrenInited(true); } void LLFloaterGLTFAssetEditor::loadFromSelection() { clearRoot(); if (LLSelectMgr::getInstance()->getSelection()->getObjectCount() != 1) { mAsset = nullptr; mObject = nullptr; return; } LLSelectNode* node = LLSelectMgr::getInstance()->getSelection()->getFirstNode(NULL); LLViewerObject* objectp = node->getObject(); if (!objectp) { mAsset = nullptr; mObject = nullptr; return; } if (!objectp->mGLTFAsset) { mAsset = nullptr; mObject = nullptr; return; } mAsset = objectp->mGLTFAsset; mObject = objectp; if (node->mName.empty()) { setTitle(getString("floater_title")); } else { setTitle(node->mName); } LLUIColor item_color = LLUIColorTable::instance().getColor("MenuItemEnabledColor", DEFAULT_WHITE); for (S32 i = 0; i < mAsset->mScenes.size(); i++) { LL::GLTF::Scene& scene = mAsset->mScenes[i]; std::string name = scene.mName; if (scene.mName.empty()) { name = getString("scene_tittle"); } else { name = scene.mName; } LLGLTFFolderItem* listener = new LLGLTFFolderItem(i, name, LLGLTFFolderItem::TYPE_SCENE, mGLTFViewModel); LLFolderViewFolder::Params p; p.name = name; p.root = mFolderRoot; p.listener = listener; p.tool_tip = name; p.font_color = mUIColor; p.font_highlight_color = mUIColor; LLFolderViewFolder* view = LLUICtrlFactory::create(p); view->addToFolder(mFolderRoot); view->setVisible(true); view->setOpen(true); for (S32& node_id : scene.mNodes) { loadFromNode(node_id, view); } view->setChildrenInited(true); } mGLTFViewModel.requestSortAll(); mFolderRoot->setChildrenInited(true); mFolderRoot->arrangeAll(); mFolderRoot->update(); } void LLFloaterGLTFAssetEditor::dirty() { if (!mObject || !mAsset || !mFolderRoot) { return; } if (LLSelectMgr::getInstance()->getSelection()->getObjectCount() > 1) { if (getVisible()) { closeFloater(); } return; } LLSelectNode* node = LLSelectMgr::getInstance()->getSelection()->getFirstNode(NULL); if (!node) { // not yet updated? // Todo: Subscribe to deletion in some way return; } LLViewerObject* objectp = node->getObject(); if (mObject != objectp || !objectp->mGLTFAsset) { if (getVisible()) { closeFloater(); } return; } if (mAsset != objectp->mGLTFAsset) { loadFromSelection(); return; } auto found = mNodeToItemMap.find(node->mSelectedGLTFNode); if (found != mNodeToItemMap.end()) { LLFolderViewItem* itemp = found->second; itemp->arrangeAndSet(true, false); loadNodeTransforms(node->mSelectedGLTFNode); } } void LLFloaterGLTFAssetEditor::onFolderSelectionChanged(const std::deque& items, bool user_action) { if (items.empty()) { setTransformsEnabled(false); return; } LLFolderViewItem* item = items.front(); LLGLTFFolderItem* vmi = static_cast(item->getViewModelItem()); switch (vmi->getType()) { case LLGLTFFolderItem::TYPE_SCENE: { setTransformsEnabled(false); LLSelectMgr::getInstance()->selectObjectOnly(mObject, SELECT_ALL_TES, -1, -1); break; } case LLGLTFFolderItem::TYPE_NODE: { setTransformsEnabled(true); loadNodeTransforms(vmi->getItemId()); LLSelectMgr::getInstance()->selectObjectOnly(mObject, SELECT_ALL_TES, vmi->getItemId(), 0); break; } case LLGLTFFolderItem::TYPE_MESH: case LLGLTFFolderItem::TYPE_SKIN: { if (item->getParent()) // should be a node { LLFolderViewFolder* parent = item->getParentFolder(); LLGLTFFolderItem* parent_vmi = static_cast(parent->getViewModelItem()); LLSelectMgr::getInstance()->selectObjectOnly(mObject, SELECT_ALL_TES, parent_vmi->getItemId(), 0); } setTransformsEnabled(false); break; } default: { setTransformsEnabled(false); break; } } } void LLFloaterGLTFAssetEditor::setTransformsEnabled(bool val) { mMenuClipboardPos->setEnabled(val); mCtrlPosX->setEnabled(val); mCtrlPosY->setEnabled(val); mCtrlPosZ->setEnabled(val); mMenuClipboardScale->setEnabled(val); mCtrlScaleX->setEnabled(val); mCtrlScaleY->setEnabled(val); mCtrlScaleZ->setEnabled(val); mMenuClipboardRot->setEnabled(val); mCtrlRotX->setEnabled(val); mCtrlRotY->setEnabled(val); mCtrlRotZ->setEnabled(val); } void LLFloaterGLTFAssetEditor::loadNodeTransforms(S32 node_id) { if (node_id < 0 || node_id >= mAsset->mNodes.size()) { LL_ERRS() << "Node id out of range: " << node_id << LL_ENDL; return; } LL::GLTF::Node& node = mAsset->mNodes[node_id]; node.makeTRSValid(); mCtrlPosX->set(node.mTranslation[0]); mCtrlPosY->set(node.mTranslation[1]); mCtrlPosZ->set(node.mTranslation[2]); mCtrlScaleX->set(node.mScale[0]); mCtrlScaleY->set(node.mScale[1]); mCtrlScaleZ->set(node.mScale[2]); LLQuaternion object_rot = LLQuaternion(node.mRotation[0], node.mRotation[1], node.mRotation[2], node.mRotation[3]); object_rot.getEulerAngles(&(mLastEulerDegrees.mV[VX]), &(mLastEulerDegrees.mV[VY]), &(mLastEulerDegrees.mV[VZ])); mLastEulerDegrees *= RAD_TO_DEG; mLastEulerDegrees.mV[VX] = fmod(ll_round(mLastEulerDegrees.mV[VX], OBJECT_ROTATION_PRECISION) + 360.f, 360.f); mLastEulerDegrees.mV[VY] = fmod(ll_round(mLastEulerDegrees.mV[VY], OBJECT_ROTATION_PRECISION) + 360.f, 360.f); mLastEulerDegrees.mV[VZ] = fmod(ll_round(mLastEulerDegrees.mV[VZ], OBJECT_ROTATION_PRECISION) + 360.f, 360.f); mCtrlRotX->set(mLastEulerDegrees.mV[VX]); mCtrlRotY->set(mLastEulerDegrees.mV[VY]); mCtrlRotZ->set(mLastEulerDegrees.mV[VZ]); } void LLFloaterGLTFAssetEditor::onCommitTransform() { if (!mFolderRoot) { LL_ERRS() << "Folder root not initialized" << LL_ENDL; return; } LLFolderViewItem* item = mFolderRoot->getCurSelectedItem(); if (!item) { LL_ERRS() << "Nothing selected" << LL_ENDL; return; } LLGLTFFolderItem* vmi = static_cast(item->getViewModelItem()); if (!vmi || vmi->getType() != LLGLTFFolderItem::TYPE_NODE) { LL_ERRS() << "Only nodes implemented" << LL_ENDL; return; } S32 node_id = vmi->getItemId(); LL::GLTF::Node& node = mAsset->mNodes[node_id]; LL::GLTF::vec3 tr(mCtrlPosX->get(), mCtrlPosY->get(), mCtrlPosZ->get()); node.setTranslation(tr); LL::GLTF::vec3 scale(mCtrlScaleX->get(), mCtrlScaleY->get(), mCtrlScaleZ->get()); node.setScale(scale); LLVector3 new_rot(mCtrlRotX->get(), mCtrlRotY->get(), mCtrlRotZ->get()); new_rot.mV[VX] = ll_round(new_rot.mV[VX], OBJECT_ROTATION_PRECISION); new_rot.mV[VY] = ll_round(new_rot.mV[VY], OBJECT_ROTATION_PRECISION); new_rot.mV[VZ] = ll_round(new_rot.mV[VZ], OBJECT_ROTATION_PRECISION); // Note: must compare before conversion to radians, some value can go 'around' 360 LLVector3 delta = new_rot - mLastEulerDegrees; if (delta.magVec() >= 0.0005f) { mLastEulerDegrees = new_rot; new_rot *= DEG_TO_RAD; LLQuaternion rotation; rotation.setQuat(new_rot.mV[VX], new_rot.mV[VY], new_rot.mV[VZ]); LL::GLTF::quat q; q[0] = rotation.mQ[VX]; q[1] = rotation.mQ[VY]; q[2] = rotation.mQ[VZ]; q[3] = rotation.mQ[VW]; node.setRotation(q); } mAsset->updateTransforms(); } void LLFloaterGLTFAssetEditor::onMenuDoToSelected(const LLSD& userdata) { std::string command = userdata.asString(); if (command == "psr_paste") { // todo: implement // onPastePos(); // onPasteSize(); // onPasteRot(); } else if (command == "pos_paste") { // todo: implement } else if (command == "size_paste") { // todo: implement } else if (command == "rot_paste") { // todo: implement } else if (command == "psr_copy") { // onCopyPos(); // onCopySize(); // onCopyRot(); } else if (command == "pos_copy") { // todo: implement } else if (command == "size_copy") { // todo: implement } else if (command == "rot_copy") { // todo: implement } } bool LLFloaterGLTFAssetEditor::onMenuEnableItem(const LLSD& userdata) { if (!mFolderRoot) { return false; } LLFolderViewItem* item = mFolderRoot->getCurSelectedItem(); if (!item) { return false; } LLGLTFFolderItem* vmi = static_cast(item->getViewModelItem()); if (!vmi || vmi->getType() != LLGLTFFolderItem::TYPE_NODE) { return false; } std::string command = userdata.asString(); if (command == "pos_paste" || command == "size_paste" || command == "rot_paste") { // todo: implement return true; } if (command == "psr_copy") { // todo: implement return true; } return false; }